chat: remember last model
This commit is contained in:
@@ -111,6 +111,7 @@ Behavior notes:
|
||||
- If `chatId` is present, server validates chat existence.
|
||||
- For `chatId` calls, server stores only *new* non-assistant messages from provided history to avoid duplicates.
|
||||
- Server persists final assistant output and call metadata (`LlmCall`) in DB.
|
||||
- Server updates chat-level model metadata on each call: `lastUsedProvider`/`lastUsedModel`; first successful/failed call also initializes `initiatedProvider`/`initiatedModel` if unset.
|
||||
|
||||
## Searches
|
||||
|
||||
@@ -151,7 +152,16 @@ Search run notes:
|
||||
|
||||
`ChatSummary`
|
||||
```json
|
||||
{ "id": "...", "title": null, "createdAt": "...", "updatedAt": "..." }
|
||||
{
|
||||
"id": "...",
|
||||
"title": null,
|
||||
"createdAt": "...",
|
||||
"updatedAt": "...",
|
||||
"initiatedProvider": "openai|anthropic|xai|null",
|
||||
"initiatedModel": "string|null",
|
||||
"lastUsedProvider": "openai|anthropic|xai|null",
|
||||
"lastUsedModel": "string|null"
|
||||
}
|
||||
```
|
||||
|
||||
`Message`
|
||||
@@ -172,6 +182,10 @@ Search run notes:
|
||||
"title": null,
|
||||
"createdAt": "...",
|
||||
"updatedAt": "...",
|
||||
"initiatedProvider": "openai|anthropic|xai|null",
|
||||
"initiatedModel": "string|null",
|
||||
"lastUsedProvider": "openai|anthropic|xai|null",
|
||||
"lastUsedModel": "string|null",
|
||||
"messages": [Message]
|
||||
}
|
||||
```
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Chat" ADD COLUMN "initiatedProvider" TEXT;
|
||||
ALTER TABLE "Chat" ADD COLUMN "initiatedModel" TEXT;
|
||||
ALTER TABLE "Chat" ADD COLUMN "lastUsedProvider" TEXT;
|
||||
ALTER TABLE "Chat" ADD COLUMN "lastUsedModel" TEXT;
|
||||
@@ -45,6 +45,11 @@ model Chat {
|
||||
|
||||
title String?
|
||||
|
||||
initiatedProvider Provider?
|
||||
initiatedModel String?
|
||||
lastUsedProvider Provider?
|
||||
lastUsedModel String?
|
||||
|
||||
user User? @relation(fields: [userId], references: [id])
|
||||
userId String?
|
||||
|
||||
|
||||
@@ -10,11 +10,12 @@ function asProviderEnum(p: Provider) {
|
||||
|
||||
export async function runMultiplex(req: MultiplexRequest): Promise<MultiplexResponse> {
|
||||
const t0 = performance.now();
|
||||
const chatId = req.chatId ?? (await prisma.chat.create({ data: {}, select: { id: true } })).id;
|
||||
|
||||
// Persist call record early so we can attach errors.
|
||||
const call = await prisma.llmCall.create({
|
||||
data: {
|
||||
chatId: req.chatId ?? (await prisma.chat.create({ data: {} })).id,
|
||||
chatId,
|
||||
provider: asProviderEnum(req.provider) as any,
|
||||
model: req.model,
|
||||
request: req as any,
|
||||
@@ -22,6 +23,23 @@ export async function runMultiplex(req: MultiplexRequest): Promise<MultiplexResp
|
||||
select: { id: true, chatId: true },
|
||||
});
|
||||
|
||||
await prisma.$transaction([
|
||||
prisma.chat.update({
|
||||
where: { id: chatId },
|
||||
data: {
|
||||
lastUsedProvider: asProviderEnum(req.provider) as any,
|
||||
lastUsedModel: req.model,
|
||||
},
|
||||
}),
|
||||
prisma.chat.updateMany({
|
||||
where: { id: chatId, initiatedProvider: null },
|
||||
data: {
|
||||
initiatedProvider: asProviderEnum(req.provider) as any,
|
||||
initiatedModel: req.model,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
try {
|
||||
let outText = "";
|
||||
let usage: MultiplexResponse["usage"] | undefined;
|
||||
|
||||
@@ -29,6 +29,23 @@ export async function* runMultiplexStream(req: MultiplexRequest): AsyncGenerator
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
await prisma.$transaction([
|
||||
prisma.chat.update({
|
||||
where: { id: chatId },
|
||||
data: {
|
||||
lastUsedProvider: req.provider as any,
|
||||
lastUsedModel: req.model,
|
||||
},
|
||||
}),
|
||||
prisma.chat.updateMany({
|
||||
where: { id: chatId, initiatedProvider: null },
|
||||
data: {
|
||||
initiatedProvider: req.provider as any,
|
||||
initiatedModel: req.model,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
yield { type: "meta", chatId, callId: call.id, provider: req.provider, model: req.model };
|
||||
|
||||
let text = "";
|
||||
|
||||
@@ -174,7 +174,16 @@ 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 },
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
initiatedProvider: true,
|
||||
initiatedModel: true,
|
||||
lastUsedProvider: true,
|
||||
lastUsedModel: true,
|
||||
},
|
||||
});
|
||||
return { chats };
|
||||
});
|
||||
@@ -183,7 +192,19 @@ export async function registerRoutes(app: FastifyInstance) {
|
||||
requireAdmin(req);
|
||||
const Body = z.object({ title: z.string().optional() });
|
||||
const body = Body.parse(req.body ?? {});
|
||||
const chat = await prisma.chat.create({ data: { title: body.title } });
|
||||
const chat = await prisma.chat.create({
|
||||
data: { title: body.title },
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
initiatedProvider: true,
|
||||
initiatedModel: true,
|
||||
lastUsedProvider: true,
|
||||
lastUsedModel: true,
|
||||
},
|
||||
});
|
||||
return { chat };
|
||||
});
|
||||
|
||||
@@ -203,7 +224,16 @@ export async function registerRoutes(app: FastifyInstance) {
|
||||
|
||||
const chat = await prisma.chat.findUnique({
|
||||
where: { id: chatId },
|
||||
select: { id: true, title: true, createdAt: true, updatedAt: true },
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
initiatedProvider: true,
|
||||
initiatedModel: true,
|
||||
lastUsedProvider: true,
|
||||
lastUsedModel: true,
|
||||
},
|
||||
});
|
||||
if (!chat) return app.httpErrors.notFound("chat not found");
|
||||
return { chat };
|
||||
@@ -219,7 +249,16 @@ 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 },
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
initiatedProvider: true,
|
||||
initiatedModel: true,
|
||||
lastUsedProvider: true,
|
||||
lastUsedModel: true,
|
||||
},
|
||||
});
|
||||
if (!existing) return app.httpErrors.notFound("chat not found");
|
||||
if (existing.title?.trim()) return { chat: existing };
|
||||
@@ -231,7 +270,16 @@ export async function registerRoutes(app: FastifyInstance) {
|
||||
const chat = await prisma.chat.update({
|
||||
where: { id: body.chatId },
|
||||
data: { title },
|
||||
select: { id: true, title: true, createdAt: true, updatedAt: true },
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
initiatedProvider: true,
|
||||
initiatedModel: true,
|
||||
lastUsedProvider: true,
|
||||
lastUsedModel: true,
|
||||
},
|
||||
});
|
||||
|
||||
return { chat };
|
||||
|
||||
@@ -37,6 +37,10 @@ type SidebarItem = SidebarSelection & {
|
||||
title: string;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
initiatedProvider: Provider | null;
|
||||
initiatedModel: string | null;
|
||||
lastUsedProvider: Provider | null;
|
||||
lastUsedModel: string | null;
|
||||
};
|
||||
type ContextMenuState = {
|
||||
item: SidebarSelection;
|
||||
@@ -89,11 +93,26 @@ function loadStoredModelPreferences() {
|
||||
}
|
||||
|
||||
function pickProviderModel(options: string[], preferred: string | null, fallback: string | null = null) {
|
||||
if (preferred && options.includes(preferred)) return preferred;
|
||||
if (fallback && options.includes(fallback)) return fallback;
|
||||
if (preferred && options.includes(preferred)) return preferred;
|
||||
return options[0] ?? "";
|
||||
}
|
||||
|
||||
function getProviderLabel(provider: Provider | null | undefined) {
|
||||
if (provider === "openai") return "OpenAI";
|
||||
if (provider === "anthropic") return "Anthropic";
|
||||
if (provider === "xai") return "xAI";
|
||||
return "";
|
||||
}
|
||||
|
||||
function getChatModelSelection(chat: Pick<ChatSummary, "lastUsedProvider" | "lastUsedModel"> | Pick<ChatDetail, "lastUsedProvider" | "lastUsedModel"> | null) {
|
||||
if (!chat?.lastUsedProvider || !chat.lastUsedModel?.trim()) return null;
|
||||
return {
|
||||
provider: chat.lastUsedProvider,
|
||||
model: chat.lastUsedModel.trim(),
|
||||
};
|
||||
}
|
||||
|
||||
type ModelComboboxProps = {
|
||||
options: string[];
|
||||
value: string;
|
||||
@@ -212,6 +231,10 @@ function buildSidebarItems(chats: ChatSummary[], searches: SearchSummary[]): Sid
|
||||
title: getChatTitle(chat),
|
||||
updatedAt: chat.updatedAt,
|
||||
createdAt: chat.createdAt,
|
||||
initiatedProvider: chat.initiatedProvider,
|
||||
initiatedModel: chat.initiatedModel,
|
||||
lastUsedProvider: chat.lastUsedProvider,
|
||||
lastUsedModel: chat.lastUsedModel,
|
||||
})),
|
||||
...searches.map((search) => ({
|
||||
kind: "search" as const,
|
||||
@@ -219,6 +242,10 @@ function buildSidebarItems(chats: ChatSummary[], searches: SearchSummary[]): Sid
|
||||
title: getSearchTitle(search),
|
||||
updatedAt: search.updatedAt,
|
||||
createdAt: search.createdAt,
|
||||
initiatedProvider: null,
|
||||
initiatedModel: null,
|
||||
lastUsedProvider: null,
|
||||
lastUsedModel: null,
|
||||
})),
|
||||
];
|
||||
|
||||
@@ -473,6 +500,16 @@ export default function App() {
|
||||
return searches.find((search) => search.id === selectedItem.id) ?? null;
|
||||
}, [searches, selectedItem]);
|
||||
|
||||
useEffect(() => {
|
||||
if (draftKind || selectedItem?.kind !== "chat") return;
|
||||
const detailSelection = selectedChat?.id === selectedItem.id ? getChatModelSelection(selectedChat) : null;
|
||||
const summarySelection = getChatModelSelection(selectedChatSummary);
|
||||
const nextSelection = detailSelection ?? summarySelection;
|
||||
if (!nextSelection) return;
|
||||
setProvider(nextSelection.provider);
|
||||
setModel(nextSelection.model);
|
||||
}, [draftKind, selectedChat, selectedChatSummary, selectedItem]);
|
||||
|
||||
const selectedTitle = useMemo(() => {
|
||||
if (draftKind === "chat") return "New chat";
|
||||
if (draftKind === "search") return "New search";
|
||||
@@ -611,6 +648,10 @@ export default function App() {
|
||||
title: chat.title,
|
||||
createdAt: chat.createdAt,
|
||||
updatedAt: chat.updatedAt,
|
||||
initiatedProvider: chat.initiatedProvider,
|
||||
initiatedModel: chat.initiatedModel,
|
||||
lastUsedProvider: chat.lastUsedProvider,
|
||||
lastUsedModel: chat.lastUsedModel,
|
||||
messages: [],
|
||||
});
|
||||
setSelectedSearch(null);
|
||||
@@ -937,6 +978,9 @@ export default function App() {
|
||||
) : null}
|
||||
{sidebarItems.map((item) => {
|
||||
const active = selectedItem?.kind === item.kind && selectedItem.id === item.id;
|
||||
const initiatedLabel = item.kind === "chat" && item.initiatedModel
|
||||
? `${getProviderLabel(item.initiatedProvider)}${item.initiatedProvider ? " · " : ""}${item.initiatedModel}`
|
||||
: null;
|
||||
return (
|
||||
<button
|
||||
key={`${item.kind}-${item.id}`}
|
||||
@@ -956,7 +1000,12 @@ export default function App() {
|
||||
{item.kind === "chat" ? <MessageSquare className="h-3.5 w-3.5" /> : <Search className="h-3.5 w-3.5" />}
|
||||
<p className="truncate text-sm font-medium">{item.title}</p>
|
||||
</div>
|
||||
<p className={cn("mt-1 text-xs", active ? "text-violet-100/90" : "text-violet-300/60")}>{formatDate(item.updatedAt)}</p>
|
||||
<div className="mt-1 flex items-center gap-2 text-xs">
|
||||
<p className={cn("shrink-0", active ? "text-violet-100/90" : "text-violet-300/60")}>{formatDate(item.updatedAt)}</p>
|
||||
{initiatedLabel ? (
|
||||
<p className={cn("ml-auto truncate text-right", active ? "text-violet-200/65" : "text-violet-300/45")}>{initiatedLabel}</p>
|
||||
) : null}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -3,6 +3,10 @@ export type ChatSummary = {
|
||||
title: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
initiatedProvider: Provider | null;
|
||||
initiatedModel: string | null;
|
||||
lastUsedProvider: Provider | null;
|
||||
lastUsedModel: string | null;
|
||||
};
|
||||
|
||||
export type SearchSummary = {
|
||||
@@ -26,6 +30,10 @@ export type ChatDetail = {
|
||||
title: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
initiatedProvider: Provider | null;
|
||||
initiatedModel: string | null;
|
||||
lastUsedProvider: Provider | null;
|
||||
lastUsedModel: string | null;
|
||||
messages: Message[];
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user