adds ability to star chats
This commit is contained in:
@@ -321,6 +321,34 @@ type SearchRunRequest = z.infer<typeof SearchRunBody>;
|
||||
|
||||
const activeChatStreams = new Map<string, ActiveSseStream>();
|
||||
const activeSearchStreams = new Map<string, ActiveSseStream>();
|
||||
const STARRED_PROJECT_ID = "starred";
|
||||
|
||||
const starredProjectItemsSelect = {
|
||||
where: { projectId: STARRED_PROJECT_ID },
|
||||
select: { createdAt: true },
|
||||
take: 1,
|
||||
} as const;
|
||||
|
||||
const chatSummarySelect = {
|
||||
id: true,
|
||||
title: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
initiatedProvider: true,
|
||||
initiatedModel: true,
|
||||
lastUsedProvider: true,
|
||||
lastUsedModel: true,
|
||||
projectItems: starredProjectItemsSelect,
|
||||
} as const;
|
||||
|
||||
const searchSummarySelect = {
|
||||
id: true,
|
||||
title: true,
|
||||
query: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
projectItems: starredProjectItemsSelect,
|
||||
} as const;
|
||||
|
||||
function getErrorMessage(err: unknown) {
|
||||
return err instanceof Error ? err.message : String(err);
|
||||
@@ -330,32 +358,111 @@ function compareUpdatedAtDesc(a: { updatedAt: Date | string }, b: { updatedAt: D
|
||||
return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime();
|
||||
}
|
||||
|
||||
function serializeStarFields(item: { projectItems?: Array<{ createdAt: Date }> }) {
|
||||
const star = item.projectItems?.[0];
|
||||
return {
|
||||
starred: Boolean(star),
|
||||
starredAt: star?.createdAt ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
function serializeChatLike<T extends Record<string, any>>(chat: T) {
|
||||
const { projectItems: _projectItems, ...rest } = chat;
|
||||
return {
|
||||
...serializeProviderFields(rest),
|
||||
...serializeStarFields(chat),
|
||||
};
|
||||
}
|
||||
|
||||
function serializeSearchLike<T extends Record<string, any>>(search: T) {
|
||||
const { projectItems: _projectItems, ...rest } = search;
|
||||
return {
|
||||
...rest,
|
||||
...serializeStarFields(search),
|
||||
};
|
||||
}
|
||||
|
||||
async function ensureStarredProject() {
|
||||
await prisma.project.upsert({
|
||||
where: { id: STARRED_PROJECT_ID },
|
||||
update: {},
|
||||
create: {
|
||||
id: STARRED_PROJECT_ID,
|
||||
kind: "starred" as any,
|
||||
title: "Starred",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function getChatSummary(chatId: string) {
|
||||
const chat = await prisma.chat.findUnique({
|
||||
where: { id: chatId },
|
||||
select: chatSummarySelect,
|
||||
});
|
||||
return chat ? serializeChatLike(chat) : null;
|
||||
}
|
||||
|
||||
async function getSearchSummary(searchId: string) {
|
||||
const search = await prisma.search.findUnique({
|
||||
where: { id: searchId },
|
||||
select: searchSummarySelect,
|
||||
});
|
||||
return search ? serializeSearchLike(search) : null;
|
||||
}
|
||||
|
||||
async function setChatStarred(chatId: string, starred: boolean) {
|
||||
const exists = await prisma.chat.findUnique({ where: { id: chatId }, select: { id: true } });
|
||||
if (!exists) return null;
|
||||
|
||||
if (starred) {
|
||||
await ensureStarredProject();
|
||||
await prisma.projectItem.upsert({
|
||||
where: { projectId_chatId: { projectId: STARRED_PROJECT_ID, chatId } },
|
||||
update: {},
|
||||
create: { projectId: STARRED_PROJECT_ID, chatId },
|
||||
});
|
||||
} else {
|
||||
await prisma.projectItem.deleteMany({ where: { projectId: STARRED_PROJECT_ID, chatId } });
|
||||
}
|
||||
|
||||
return getChatSummary(chatId);
|
||||
}
|
||||
|
||||
async function setSearchStarred(searchId: string, starred: boolean) {
|
||||
const exists = await prisma.search.findUnique({ where: { id: searchId }, select: { id: true } });
|
||||
if (!exists) return null;
|
||||
|
||||
if (starred) {
|
||||
await ensureStarredProject();
|
||||
await prisma.projectItem.upsert({
|
||||
where: { projectId_searchId: { projectId: STARRED_PROJECT_ID, searchId } },
|
||||
update: {},
|
||||
create: { projectId: STARRED_PROJECT_ID, searchId },
|
||||
});
|
||||
} else {
|
||||
await prisma.projectItem.deleteMany({ where: { projectId: STARRED_PROJECT_ID, searchId } });
|
||||
}
|
||||
|
||||
return getSearchSummary(searchId);
|
||||
}
|
||||
|
||||
async function listWorkspaceItems() {
|
||||
const [chats, searches] = await Promise.all([
|
||||
prisma.chat.findMany({
|
||||
orderBy: { updatedAt: "desc" },
|
||||
take: 100,
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
initiatedProvider: true,
|
||||
initiatedModel: true,
|
||||
lastUsedProvider: true,
|
||||
lastUsedModel: true,
|
||||
},
|
||||
select: chatSummarySelect,
|
||||
}),
|
||||
prisma.search.findMany({
|
||||
orderBy: { updatedAt: "desc" },
|
||||
take: 100,
|
||||
select: { id: true, title: true, query: true, createdAt: true, updatedAt: true },
|
||||
select: searchSummarySelect,
|
||||
}),
|
||||
]);
|
||||
|
||||
return [
|
||||
...chats.map((chat) => ({ type: "chat" as const, ...serializeProviderFields(chat) })),
|
||||
...searches.map((search) => ({ type: "search" as const, ...search })),
|
||||
...chats.map((chat) => ({ type: "chat" as const, ...serializeChatLike(chat) })),
|
||||
...searches.map((search) => ({ type: "search" as const, ...serializeSearchLike(search) })),
|
||||
].sort(compareUpdatedAtDesc);
|
||||
}
|
||||
|
||||
@@ -562,12 +669,15 @@ async function executeSearchRunStream(searchId: string, body: SearchRunRequest,
|
||||
|
||||
const search = await prisma.search.findUnique({
|
||||
where: { id: searchId },
|
||||
include: { results: { orderBy: { rank: "asc" } } },
|
||||
include: {
|
||||
results: { orderBy: { rank: "asc" } },
|
||||
projectItems: starredProjectItemsSelect,
|
||||
},
|
||||
});
|
||||
if (!search) {
|
||||
stream.complete({ event: "error", data: { message: "search not found" } });
|
||||
} else {
|
||||
stream.complete({ event: "done", data: { search } });
|
||||
stream.complete({ event: "done", data: { search: serializeSearchLike(search) } });
|
||||
}
|
||||
} catch (err) {
|
||||
const message = getErrorMessage(err);
|
||||
@@ -621,18 +731,9 @@ export async function registerRoutes(app: FastifyInstance) {
|
||||
const chats = await prisma.chat.findMany({
|
||||
orderBy: { updatedAt: "desc" },
|
||||
take: 100,
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
initiatedProvider: true,
|
||||
initiatedModel: true,
|
||||
lastUsedProvider: true,
|
||||
lastUsedModel: true,
|
||||
},
|
||||
select: chatSummarySelect,
|
||||
});
|
||||
return { chats: chats.map((chat) => serializeProviderFields(chat)) };
|
||||
return { chats: chats.map((chat) => serializeChatLike(chat)) };
|
||||
});
|
||||
|
||||
app.post("/v1/chats", async (req) => {
|
||||
@@ -681,18 +782,9 @@ export async function registerRoutes(app: FastifyInstance) {
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
initiatedProvider: true,
|
||||
initiatedModel: true,
|
||||
lastUsedProvider: true,
|
||||
lastUsedModel: true,
|
||||
},
|
||||
select: chatSummarySelect,
|
||||
});
|
||||
return { chat: serializeProviderFields(chat) };
|
||||
return { chat: serializeChatLike(chat) };
|
||||
});
|
||||
|
||||
app.patch("/v1/chats/:chatId", async (req) => {
|
||||
@@ -709,21 +801,21 @@ export async function registerRoutes(app: FastifyInstance) {
|
||||
|
||||
if (updated.count === 0) return app.httpErrors.notFound("chat not found");
|
||||
|
||||
const chat = await prisma.chat.findUnique({
|
||||
where: { id: chatId },
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
initiatedProvider: true,
|
||||
initiatedModel: true,
|
||||
lastUsedProvider: true,
|
||||
lastUsedModel: true,
|
||||
},
|
||||
});
|
||||
const chat = await getChatSummary(chatId);
|
||||
if (!chat) return app.httpErrors.notFound("chat not found");
|
||||
return { chat: serializeProviderFields(chat) };
|
||||
return { chat };
|
||||
});
|
||||
|
||||
app.patch("/v1/chats/:chatId/star", async (req) => {
|
||||
requireAdmin(req);
|
||||
const Params = z.object({ chatId: z.string() });
|
||||
const Body = z.object({ starred: z.boolean() });
|
||||
const { chatId } = Params.parse(req.params);
|
||||
const body = Body.parse(req.body ?? {});
|
||||
|
||||
const chat = await setChatStarred(chatId, body.starred);
|
||||
if (!chat) return app.httpErrors.notFound("chat not found");
|
||||
return { chat };
|
||||
});
|
||||
|
||||
app.post("/v1/chats/title/suggest", async (req) => {
|
||||
@@ -736,19 +828,10 @@ export async function registerRoutes(app: FastifyInstance) {
|
||||
|
||||
const existing = await prisma.chat.findUnique({
|
||||
where: { id: body.chatId },
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
initiatedProvider: true,
|
||||
initiatedModel: true,
|
||||
lastUsedProvider: true,
|
||||
lastUsedModel: true,
|
||||
},
|
||||
select: chatSummarySelect,
|
||||
});
|
||||
if (!existing) return app.httpErrors.notFound("chat not found");
|
||||
if (existing.title?.trim()) return { chat: serializeProviderFields(existing) };
|
||||
if (existing.title?.trim()) return { chat: serializeChatLike(existing) };
|
||||
|
||||
const fallback = body.content.split(/\r?\n/)[0]?.trim().slice(0, 48) || "New chat";
|
||||
const suggestedRaw = await generateChatTitle(body.content);
|
||||
@@ -759,22 +842,10 @@ export async function registerRoutes(app: FastifyInstance) {
|
||||
data: { title },
|
||||
});
|
||||
|
||||
const chat = await prisma.chat.findUnique({
|
||||
where: { id: body.chatId },
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
initiatedProvider: true,
|
||||
initiatedModel: true,
|
||||
lastUsedProvider: true,
|
||||
lastUsedModel: true,
|
||||
},
|
||||
});
|
||||
const chat = await getChatSummary(body.chatId);
|
||||
if (!chat) return app.httpErrors.notFound("chat not found");
|
||||
|
||||
return { chat: serializeProviderFields(chat) };
|
||||
return { chat };
|
||||
});
|
||||
|
||||
app.delete("/v1/chats/:chatId", async (req) => {
|
||||
@@ -799,9 +870,9 @@ export async function registerRoutes(app: FastifyInstance) {
|
||||
const searches = await prisma.search.findMany({
|
||||
orderBy: { updatedAt: "desc" },
|
||||
take: 100,
|
||||
select: { id: true, title: true, query: true, createdAt: true, updatedAt: true },
|
||||
select: searchSummarySelect,
|
||||
});
|
||||
return { searches };
|
||||
return { searches: searches.map((search) => serializeSearchLike(search)) };
|
||||
});
|
||||
|
||||
app.post("/v1/searches", async (req) => {
|
||||
@@ -815,8 +886,20 @@ export async function registerRoutes(app: FastifyInstance) {
|
||||
title: title || null,
|
||||
query,
|
||||
},
|
||||
select: { id: true, title: true, query: true, createdAt: true, updatedAt: true },
|
||||
select: searchSummarySelect,
|
||||
});
|
||||
return { search: serializeSearchLike(search) };
|
||||
});
|
||||
|
||||
app.patch("/v1/searches/:searchId/star", async (req) => {
|
||||
requireAdmin(req);
|
||||
const Params = z.object({ searchId: z.string() });
|
||||
const Body = z.object({ starred: z.boolean() });
|
||||
const { searchId } = Params.parse(req.params);
|
||||
const body = Body.parse(req.body ?? {});
|
||||
|
||||
const search = await setSearchStarred(searchId, body.starred);
|
||||
if (!search) return app.httpErrors.notFound("search not found");
|
||||
return { search };
|
||||
});
|
||||
|
||||
@@ -843,10 +926,13 @@ export async function registerRoutes(app: FastifyInstance) {
|
||||
|
||||
const search = await prisma.search.findUnique({
|
||||
where: { id: searchId },
|
||||
include: { results: { orderBy: { rank: "asc" } } },
|
||||
include: {
|
||||
results: { orderBy: { rank: "asc" } },
|
||||
projectItems: starredProjectItemsSelect,
|
||||
},
|
||||
});
|
||||
if (!search) return app.httpErrors.notFound("search not found");
|
||||
return { search };
|
||||
return { search: serializeSearchLike(search) };
|
||||
});
|
||||
|
||||
app.post("/v1/searches/:searchId/chat", async (req) => {
|
||||
@@ -882,19 +968,10 @@ export async function registerRoutes(app: FastifyInstance) {
|
||||
},
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
initiatedProvider: true,
|
||||
initiatedModel: true,
|
||||
lastUsedProvider: true,
|
||||
lastUsedModel: true,
|
||||
},
|
||||
select: chatSummarySelect,
|
||||
});
|
||||
|
||||
return { chat: serializeProviderFields(chat) };
|
||||
return { chat: serializeChatLike(chat) };
|
||||
});
|
||||
|
||||
app.post("/v1/searches/:searchId/run", async (req) => {
|
||||
@@ -979,10 +1056,13 @@ export async function registerRoutes(app: FastifyInstance) {
|
||||
|
||||
const search = await prisma.search.findUnique({
|
||||
where: { id: searchId },
|
||||
include: { results: { orderBy: { rank: "asc" } } },
|
||||
include: {
|
||||
results: { orderBy: { rank: "asc" } },
|
||||
projectItems: starredProjectItemsSelect,
|
||||
},
|
||||
});
|
||||
if (!search) return app.httpErrors.notFound("search not found");
|
||||
return { search };
|
||||
return { search: serializeSearchLike(search) };
|
||||
} catch (err: any) {
|
||||
await prisma.search.update({
|
||||
where: { id: searchId },
|
||||
@@ -1037,10 +1117,14 @@ export async function registerRoutes(app: FastifyInstance) {
|
||||
|
||||
const chat = await prisma.chat.findUnique({
|
||||
where: { id: chatId },
|
||||
include: { messages: { orderBy: { createdAt: "asc" } }, calls: { orderBy: { createdAt: "desc" } } },
|
||||
include: {
|
||||
messages: { orderBy: { createdAt: "asc" } },
|
||||
calls: { orderBy: { createdAt: "desc" } },
|
||||
projectItems: starredProjectItemsSelect,
|
||||
},
|
||||
});
|
||||
if (!chat) return app.httpErrors.notFound("chat not found");
|
||||
return { chat: serializeProviderFields(chat) };
|
||||
return { chat: serializeChatLike(chat) };
|
||||
});
|
||||
|
||||
app.post("/v1/chats/:chatId/messages", async (req) => {
|
||||
|
||||
Reference in New Issue
Block a user