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:
@@ -0,0 +1,13 @@
|
||||
CREATE TABLE "ai_conversations" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"user_id" text NOT NULL,
|
||||
"title" text,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "chat_messages" ADD COLUMN "conversation_id" text;--> statement-breakpoint
|
||||
ALTER TABLE "ai_conversations" ADD CONSTRAINT "ai_conversations_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
CREATE INDEX "ai_conversations_user_idx" ON "ai_conversations" USING btree ("user_id","updated_at");--> statement-breakpoint
|
||||
ALTER TABLE "chat_messages" ADD CONSTRAINT "chat_messages_conversation_id_ai_conversations_id_fk" FOREIGN KEY ("conversation_id") REFERENCES "public"."ai_conversations"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
CREATE INDEX "chat_messages_conversation_idx" ON "chat_messages" USING btree ("conversation_id");
|
||||
File diff suppressed because it is too large
Load Diff
@@ -295,6 +295,13 @@
|
||||
"when": 1784300027960,
|
||||
"tag": "0041_perfect_dexter_bennett",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 42,
|
||||
"version": "7",
|
||||
"when": 1784301168549,
|
||||
"tag": "0042_windy_masked_marvel",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -5,22 +5,46 @@ import { recipes } from "./recipes";
|
||||
|
||||
export const chatRoleEnum = pgEnum("chat_role", ["user", "assistant"]);
|
||||
|
||||
// The general cooking assistant (recipeId null on chatMessages) can have
|
||||
// multiple named conversations per user — the per-recipe chat doesn't need
|
||||
// this, since each recipe is naturally its own single thread already.
|
||||
export const aiConversations = pgTable("ai_conversations", {
|
||||
id: text("id").primaryKey(),
|
||||
userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
||||
title: text("title"),
|
||||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at").notNull().defaultNow(),
|
||||
}, (t) => [
|
||||
index("ai_conversations_user_idx").on(t.userId, t.updatedAt),
|
||||
]);
|
||||
|
||||
export const aiConversationsRelations = relations(aiConversations, ({ one, many }) => ({
|
||||
user: one(users, { fields: [aiConversations.userId], references: [users.id] }),
|
||||
messages: many(chatMessages),
|
||||
}));
|
||||
|
||||
// Persists both the per-recipe chat (recipeId set) and the general cooking
|
||||
// assistant on the recipes homepage (recipeId null) — same table, since a
|
||||
// user searching "their chat history" expects both to show up together.
|
||||
// conversationId is only set for general-assistant messages created after
|
||||
// multi-conversation support shipped; older general messages and all
|
||||
// per-recipe messages leave it null.
|
||||
export const chatMessages = pgTable("chat_messages", {
|
||||
id: text("id").primaryKey(),
|
||||
userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
||||
recipeId: text("recipe_id").references(() => recipes.id, { onDelete: "cascade" }),
|
||||
conversationId: text("conversation_id").references(() => aiConversations.id, { onDelete: "cascade" }),
|
||||
role: chatRoleEnum("role").notNull(),
|
||||
content: text("content").notNull(),
|
||||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||
}, (t) => [
|
||||
index("chat_messages_user_idx").on(t.userId, t.createdAt),
|
||||
index("chat_messages_recipe_idx").on(t.recipeId),
|
||||
index("chat_messages_conversation_idx").on(t.conversationId),
|
||||
]);
|
||||
|
||||
export const chatMessagesRelations = relations(chatMessages, ({ one }) => ({
|
||||
user: one(users, { fields: [chatMessages.userId], references: [users.id] }),
|
||||
recipe: one(recipes, { fields: [chatMessages.recipeId], references: [recipes.id] }),
|
||||
conversation: one(aiConversations, { fields: [chatMessages.conversationId], references: [aiConversations.id] }),
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user