feat: multiple named conversations for the cooking assistant
The general assistant had exactly one conversation per user forever (recipeId null on chat_messages) — no way to start fresh or organize by topic. Adds an ai_conversations table (title, timestamps) and a nullable conversationId FK on chat_messages; the assistant panel gets a conversations menu to create/switch/rename/delete, auto-titling a new conversation from its first question. Per-recipe chat is untouched — each recipe already has one natural thread, so multi-conversation support only applies to the homepage assistant. Pre-existing general messages (no conversationId) aren't migrated into the new model and won't appear in the conversation list. Requires migration 0042 to run against a live DB — not applied in this sandbox (no Docker here); run `pnpm db:migrate`. v0.43.0
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { generateText } from "ai";
|
||||
import { db, chatMessages } from "@epicure/db";
|
||||
import { db, chatMessages, aiConversations, sql } from "@epicure/db";
|
||||
import { requireSessionOrApiKey } from "@/lib/api-auth";
|
||||
import { applyRateLimit } from "@/lib/rate-limit";
|
||||
import { withAiQuota, resolveAiConfigOrError } from "@/lib/ai/ai-error";
|
||||
@@ -11,6 +11,7 @@ import { getUserPrivateBio, buildUserBioContext } from "@/lib/ai/user-bio";
|
||||
|
||||
const Schema = z.object({
|
||||
question: z.string().min(1).max(500),
|
||||
conversationId: z.string().uuid().optional(),
|
||||
});
|
||||
|
||||
const LANG: Record<string, string> = { en: "English", fr: "French" };
|
||||
@@ -48,10 +49,24 @@ export async function POST(req: NextRequest) {
|
||||
);
|
||||
if (!result.ok) return result.response;
|
||||
|
||||
const { conversationId } = parsed.data;
|
||||
|
||||
void db.insert(chatMessages).values([
|
||||
{ id: crypto.randomUUID(), userId: session!.user.id, recipeId: null, role: "user", content: parsed.data.question },
|
||||
{ id: crypto.randomUUID(), userId: session!.user.id, recipeId: null, role: "assistant", content: result.data.text },
|
||||
{ id: crypto.randomUUID(), userId: session!.user.id, recipeId: null, conversationId, role: "user", content: parsed.data.question },
|
||||
{ id: crypto.randomUUID(), userId: session!.user.id, recipeId: null, conversationId, role: "assistant", content: result.data.text },
|
||||
]).catch((err) => console.error("[cooking-chat] failed to persist chat history", err));
|
||||
|
||||
if (conversationId) {
|
||||
// Bumps updatedAt (for the conversation list's sort order) and, only if
|
||||
// this is the conversation's first message, auto-titles it from the
|
||||
// opening question — so users aren't left staring at "Untitled" entries;
|
||||
// they can still rename it later.
|
||||
void db.execute(sql`
|
||||
UPDATE ${aiConversations}
|
||||
SET updated_at = now(), title = COALESCE(title, ${parsed.data.question.slice(0, 60)})
|
||||
WHERE ${aiConversations.id} = ${conversationId} AND ${aiConversations.userId} = ${session!.user.id}
|
||||
`).catch((err) => console.error("[cooking-chat] failed to touch conversation", err));
|
||||
}
|
||||
|
||||
return NextResponse.json({ answer: result.data.text });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user