From 5aed1affabb628f3c193ac7c5a3da8e71ab4f853 Mon Sep 17 00:00:00 2001 From: Innei Date: Sat, 1 Nov 2025 02:07:31 +0800 Subject: [PATCH] fix(ai): fetch remote messages if not exist Signed-off-by: Innei --- .../src/modules/ai-chat-session/service.ts | 15 +- .../src/modules/ai-chat-session/store.ts | 2 + .../components/message/AIMessageParts.tsx | 21 +- .../components/message/UserChatMessage.tsx | 1 + .../modules/ai-chat/hooks/useLoadMessages.ts | 7 + .../src/modules/ai-chat/services/index.ts | 56 +- .../ai-chat/store/chat-core/chat-actions.ts | 47 +- .../ai-chat/store/chat-core/chat-state.ts | 17 +- .../modules/ai-chat/store/chat-core/types.ts | 2 + .../src/modules/ai-chat/store/hooks.ts | 15 + .../ai-chat/store/slices/chat.slice.ts | 6 +- .../src/modules/ai-chat/store/types.ts | 2 + .../src/modules/ai-chat/types/ChatSession.ts | 2 + packages/internal/database/src/db.desktop.ts | 10 +- .../drizzle/0035_last_valeria_richards.sql | 21 + .../src/drizzle/meta/0035_snapshot.json | 947 ++++++++++++++++++ .../database/src/drizzle/meta/_journal.json | 7 + .../database/src/drizzle/migrations.js | 2 + .../internal/database/src/schemas/index.ts | 1 + 19 files changed, 1147 insertions(+), 34 deletions(-) create mode 100644 packages/internal/database/src/drizzle/0035_last_valeria_richards.sql create mode 100644 packages/internal/database/src/drizzle/meta/0035_snapshot.json diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat-session/service.ts b/apps/desktop/layer/renderer/src/modules/ai-chat-session/service.ts index 3ac09a875..3701d6246 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat-session/service.ts +++ b/apps/desktop/layer/renderer/src/modules/ai-chat-session/service.ts @@ -49,6 +49,7 @@ class AIChatSessionServiceStatic { createdAt: new Date(session.createdAt), // Use createdAt for updatedAt as we are syncing session instead of messages updatedAt: new Date(session.createdAt), + isLocal: false, }) }) await this.loadSessionsFromDb() @@ -84,11 +85,15 @@ class AIChatSessionServiceStatic { force?: boolean }, ): Promise { - const dbSession = await AIPersistService.getChatSession(session.chatId) + const [dbSession, hasPersistedMessages] = await Promise.all([ + AIPersistService.getChatSession(session.chatId), + AIPersistService.hasPersistedMessages(session.chatId), + ]) + const lastUpdatedAt = dbSession ? dbSession.updatedAt : new Date(0) const hasUpToDateSession = lastUpdatedAt >= new Date(session.updatedAt) - if (!options?.force && hasUpToDateSession) { + if (!options?.force && hasUpToDateSession && hasPersistedMessages) { // If local session is already up-to-date, skip fetching messages return } @@ -104,6 +109,7 @@ class AIChatSessionServiceStatic { // Use createdAt for updatedAt // Because we are fetching session data instead of messages updatedAt: new Date(session.createdAt), + isLocal: false, }) await AIPersistService.upsertMessages(session.chatId, normalized) @@ -120,6 +126,11 @@ class AIChatSessionServiceStatic { async syncSessionMessages(chatId: string) { try { + const sessionRecord = await AIPersistService.getChatSession(chatId) + if (sessionRecord?.isLocal) { + return AIPersistService.loadUIMessages(chatId) + } + const sessionResponse = await followApi.aiChatSessions.get({ chatId }) const session = sessionResponse.data diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat-session/store.ts b/apps/desktop/layer/renderer/src/modules/ai-chat-session/store.ts index 1f4589350..6a18b3b17 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat-session/store.ts +++ b/apps/desktop/layer/renderer/src/modules/ai-chat-session/store.ts @@ -82,6 +82,8 @@ export const aiChatSessionStoreActions = { title: session.title || t("ai:common.new_chat"), createdAt: new Date(session.createdAt), updatedAt: new Date(session.updatedAt), + isLocal: false, + syncStatus: "synced" as const, })), ) } catch (error) { diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIMessageParts.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIMessageParts.tsx index 277a56902..e61a9fb1b 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIMessageParts.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIMessageParts.tsx @@ -25,20 +25,21 @@ interface AIMessagePartsProps { export const AIMessageParts: React.FC = React.memo( ({ message, isLastMessage }) => { - const [shouldStreamingAnimation, setShouldStreamingAnimation] = React.useState(false) + // const [shouldStreamingAnimation, setShouldStreamingAnimation] = React.useState(false) const chatStatus = useChatStatus() - React.useEffect(() => { - // Delay 2s to set shouldStreamingAnimation - const timerId = setTimeout(() => { - setShouldStreamingAnimation(true) - }, 2000) - return () => clearTimeout(timerId) - }, []) + // React.useEffect(() => { + // I forgot why do this + // // Delay 2s to set shouldStreamingAnimation + // const timerId = setTimeout(() => { + // setShouldStreamingAnimation(true) + // }, 2000) + // return () => clearTimeout(timerId) + // }, []) const shouldMessageAnimation = React.useMemo(() => { - return chatStatus === "streaming" && shouldStreamingAnimation && isLastMessage - }, [chatStatus, isLastMessage, shouldStreamingAnimation]) + return chatStatus === "streaming" && isLastMessage // && shouldStreamingAnimation + }, [chatStatus, isLastMessage]) const chainThoughtParts = React.useMemo(() => { const parts = [] as (ChainReasoningPart[] | TextUIPart | ToolUIPart)[] diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/UserChatMessage.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/UserChatMessage.tsx index bd650994a..6b9e45ba4 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/UserChatMessage.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/UserChatMessage.tsx @@ -31,6 +31,7 @@ export const UserChatMessage: React.FC = React.memo(({ mes const setEditingMessageId = useSetEditingMessageId() const chatStatus = useChatStatus() + const isStreaming = chatStatus === "submitted" || chatStatus === "streaming" const isEditing = editingMessageId === messageId diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/hooks/useLoadMessages.ts b/apps/desktop/layer/renderer/src/modules/ai-chat/hooks/useLoadMessages.ts index 9d6af2bca..fababb8b4 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/hooks/useLoadMessages.ts +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/hooks/useLoadMessages.ts @@ -3,6 +3,7 @@ import { useEventCallback } from "usehooks-ts" import { AIChatSessionService } from "~/modules/ai-chat-session/service" +import { AIPersistService } from "../services" import { useChatActions } from "../store/hooks" import type { BizUIMessage } from "../store/types" @@ -20,6 +21,12 @@ export const useLoadMessages = ( }) useEffect(() => { + if (chatActions.get().isLocal) { + AIPersistService.loadUIMessages(chatId) + setIsLoading(false) + + return + } let mounted = true setIsLoading(true) setIsSyncingRemote(false) diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/services/index.ts b/apps/desktop/layer/renderer/src/modules/ai-chat/services/index.ts index 12d2c385b..500426df7 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/services/index.ts +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/services/index.ts @@ -38,6 +38,17 @@ class AIPersistServiceStatic { }) } + async hasPersistedMessages(chatId: string): Promise { + const existingMessage = await db.query.aiChatMessagesTable.findFirst({ + where: eq(aiChatMessagesTable.chatId, chatId), + columns: { + id: true, + }, + }) + + return Boolean(existingMessage?.id === chatId) + } + /** * Convert enhanced database message to BizUIMessage format for compatibility */ @@ -74,6 +85,8 @@ class AIPersistServiceStatic { title?: string createdAt: Date updatedAt: Date + isLocal: boolean + syncStatus: "local" | "synced" } | null messages: BizUIMessage[] }> { @@ -97,9 +110,14 @@ class AIPersistServiceStatic { await this.updateSessionTitle(sessionRaw.chatId, resolvedTitle) } + const isLocal = Boolean(sessionRaw.isLocal) + const syncStatus: "local" | "synced" = isLocal ? "local" : "synced" + const session = { ...sessionRaw, title: resolvedTitle ?? undefined, + isLocal, + syncStatus, } return { session, messages } @@ -282,10 +300,11 @@ class AIPersistServiceStatic { */ async ensureSession( chatId: string, - options: { title?: string; createdAt?: Date; updatedAt?: Date } = {}, + options: { title?: string; createdAt?: Date; updatedAt?: Date; isLocal?: boolean } = {}, ): Promise { const cachedExists = this.getSessionExistsFromCache(chatId) - const shouldCheckDb = cachedExists !== true || options.title + const shouldCheckDb = + cachedExists !== true || options.title !== undefined || typeof options.isLocal === "boolean" if (!shouldCheckDb) { return @@ -311,6 +330,11 @@ class AIPersistServiceStatic { } } + if (typeof options.isLocal === "boolean" && existing.isLocal !== options.isLocal) { + updates.isLocal = options.isLocal + shouldUpdate = true + } + if (shouldUpdate) { updates.updatedAt = new Date() await db.update(aiChatTable).set(updates).where(eq(aiChatTable.chatId, chatId)) @@ -325,7 +349,7 @@ class AIPersistServiceStatic { async createSession( chatId: string, - options: { title?: string; createdAt?: Date; updatedAt?: Date } = {}, + options: { title?: string; createdAt?: Date; updatedAt?: Date; isLocal?: boolean } = {}, ) { const now = new Date() await db.insert(aiChatTable).values({ @@ -333,6 +357,7 @@ class AIPersistServiceStatic { title: this.resolveSessionTitle(chatId, options.title, { createdAt: now, updatedAt: now }), createdAt: options.createdAt ?? now, updatedAt: options.updatedAt ?? now, + isLocal: options.isLocal ?? true, }) // Mark session as existing in cache this.markSessionExists(chatId, true) @@ -369,6 +394,7 @@ class AIPersistServiceStatic { title: true, createdAt: true, updatedAt: true, + isLocal: true, }, }) return result?.chatId ? result : null @@ -381,6 +407,7 @@ class AIPersistServiceStatic { title: true, createdAt: true, updatedAt: true, + isLocal: true, }, orderBy: (t, { desc }) => desc(t.updatedAt), limit, @@ -408,12 +435,19 @@ class AIPersistServiceStatic { }), ) - return normalizedChats.map((chat) => ({ - chatId: chat.chatId, - title: chat.title, - createdAt: chat.createdAt, - updatedAt: chat.updatedAt, - })) + return normalizedChats.map((chat) => { + const isLocal = Boolean(chat.isLocal) + const syncStatus: "local" | "synced" = isLocal ? "local" : "synced" + + return { + chatId: chat.chatId, + title: chat.title, + createdAt: chat.createdAt, + updatedAt: chat.updatedAt, + isLocal, + syncStatus, + } + }) } async deleteSession(chatId: string) { @@ -445,6 +479,10 @@ class AIPersistServiceStatic { .where(eq(aiChatTable.chatId, chatId)) } + async markSessionSynced(chatId: string) { + await this.ensureSession(chatId, { isLocal: false }) + } + async cleanupEmptySessions() { const emptySessions = await db.values<[string]>( sql` diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/store/chat-core/chat-actions.ts b/apps/desktop/layer/renderer/src/modules/ai-chat/store/chat-core/chat-actions.ts index 01d1094da..bec68d5ab 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/store/chat-core/chat-actions.ts +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/store/chat-core/chat-actions.ts @@ -48,6 +48,42 @@ export class ChatSliceActions { return this.params[1] } + private computeSyncStatus(isLocal: boolean): "local" | "synced" { + return isLocal ? "local" : "synced" + } + + private setSyncState(isLocal: boolean) { + this.set((state) => { + const nextStatus = this.computeSyncStatus(isLocal) + if (state.isLocal === isLocal && state.syncStatus === nextStatus) { + return state + } + return { + isLocal, + syncStatus: nextStatus, + } + }) + } + + async markSessionSynced() { + const currentChatId = this.get().chatId + if (!currentChatId) { + return + } + + if (!this.get().isLocal) { + return + } + + this.setSyncState(false) + + try { + await AIPersistService.markSessionSynced(currentChatId) + } catch (error) { + console.error("Failed to mark chat session as synced:", error) + } + } + // Direct message management methods (delegating to chat instance state) setMessages = ( messagesParam: BizUIMessage[] | ((messages: BizUIMessage[]) => BizUIMessage[]), @@ -176,8 +212,8 @@ export class ChatSliceActions { }, options, ) - const response = await this.chatInstance.sendMessage(messageObj, finalOptions) - return response + + return await this.chatInstance.sendMessage(messageObj, finalOptions) } catch (error) { this.setError(error as Error) throw error @@ -193,8 +229,7 @@ export class ChatSliceActions { }, options, ) - const response = await this.chatInstance.regenerate({ messageId, ...finalOptions }) - return response + return await this.chatInstance.regenerate({ messageId, ...finalOptions }) } catch (error) { this.setError(error as Error) throw error @@ -252,6 +287,8 @@ export class ChatSliceActions { isStreaming: false, currentTitle: undefined, chatInstance: newChatInstance, + isLocal: true, + syncStatus: "local", })) // Update the reference @@ -293,6 +330,8 @@ export class ChatSliceActions { isStreaming: false, currentTitle: chatSession?.title || undefined, chatInstance: newChatInstance, + isLocal: chatSession ? chatSession.isLocal : true, + syncStatus: chatSession ? chatSession.syncStatus : "local", })) newChatInstance.resumeStream() diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/store/chat-core/chat-state.ts b/apps/desktop/layer/renderer/src/modules/ai-chat/store/chat-core/chat-state.ts index 6c05ff329..60d227dcf 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/store/chat-core/chat-state.ts +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/store/chat-core/chat-state.ts @@ -61,11 +61,18 @@ export class ZustandChatState implements ChatState { }) this.#eventEmitter.on("status", ({ status }) => { - this.updateZustandState((state) => ({ - ...state, - status, - isStreaming: status === "streaming", - })) + this.updateZustandState((state) => { + const isStreaming = status === "streaming" + if (isStreaming) { + void state.chatActions.markSessionSynced() + } + + return { + ...state, + status, + isStreaming, + } + }) }) this.#eventEmitter.on("error", ({ error }) => { diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/store/chat-core/types.ts b/apps/desktop/layer/renderer/src/modules/ai-chat/store/chat-core/types.ts index a9e562e99..c1891844f 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/store/chat-core/types.ts +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/store/chat-core/types.ts @@ -11,6 +11,8 @@ export interface ChatSlice { status: ChatStatus error: Error | undefined isStreaming: boolean + isLocal: boolean + syncStatus: "local" | "synced" // UI state currentTitle: string | undefined diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/store/hooks.ts b/apps/desktop/layer/renderer/src/modules/ai-chat/store/hooks.ts index c75110017..ca3c13ab8 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/store/hooks.ts +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/store/hooks.ts @@ -65,6 +65,21 @@ export const useHasMessages = () => { return store((state) => state.messages.length > 0) } +export const useIsLocalChat = () => { + const store = useAIChatStore() + return store((state) => state.isLocal) +} + +export const useSyncStatus = () => { + const store = useAIChatStore() + return store((state) => state.syncStatus) +} + +export const useSyncStateActions = () => { + const store = useAIChatStore() + return store((state) => state.chatActions) +} + export const useChatBlockActions = () => useAIChatStore()((state) => state.blockActions) /** * Hook to get the chat status diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/store/slices/chat.slice.ts b/apps/desktop/layer/renderer/src/modules/ai-chat/store/slices/chat.slice.ts index 977f2c619..313788855 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/store/slices/chat.slice.ts +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/store/slices/chat.slice.ts @@ -9,11 +9,13 @@ import { createChatTitleHandler, createChatTransport } from "../transport" export const createChatSlice: (options: { chatId: string generateId?: IdGenerator + isLocal?: boolean + syncStatus?: "local" | "synced" }) => StateCreator = (options) => (...params) => { const [set, get] = params - const { chatId, generateId } = options + const { chatId, generateId, isLocal, syncStatus } = options const chatInstance = new ZustandChat( { @@ -43,6 +45,8 @@ export const createChatSlice: (options: { status: "ready", error: undefined, isStreaming: false, + isLocal: isLocal ?? true, + syncStatus: syncStatus ?? (isLocal === false ? "synced" : "local"), currentTitle: undefined, chatInstance, chatActions, diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/store/types.ts b/apps/desktop/layer/renderer/src/modules/ai-chat/store/types.ts index 2fa2fc059..6fbf54e1f 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/store/types.ts +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/store/types.ts @@ -51,6 +51,8 @@ export interface AIChatStoreInitial { blocks: AIChatContextBlock[] chatId?: string generateId?: IdGenerator + isLocal?: boolean + syncStatus?: "local" | "synced" } export interface AIChatContextBlocks { diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/types/ChatSession.ts b/apps/desktop/layer/renderer/src/modules/ai-chat/types/ChatSession.ts index 305398345..4dd5dd7a5 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/types/ChatSession.ts +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/types/ChatSession.ts @@ -5,6 +5,8 @@ export interface ChatSession { title?: string createdAt: Date updatedAt: Date + isLocal: boolean + syncStatus: "local" | "synced" } export type RichTextPart = { diff --git a/packages/internal/database/src/db.desktop.ts b/packages/internal/database/src/db.desktop.ts index b9bd0fbb5..66e8ce6af 100644 --- a/packages/internal/database/src/db.desktop.ts +++ b/packages/internal/database/src/db.desktop.ts @@ -49,10 +49,14 @@ export async function initializeDB() { try { const rows = await query(dbSqlite3, sql) if (method === "get") { - return { rows: rows[0] || [] } - } else { - return { rows } + if (rows.length > 0) { + return { rows: rows[0] } + } + + return { rows: undefined } } + + return { rows } } catch (error) { console.error(`Error executing SQL: ${sql} with params:${params}`, error) return { rows: [] } diff --git a/packages/internal/database/src/drizzle/0035_last_valeria_richards.sql b/packages/internal/database/src/drizzle/0035_last_valeria_richards.sql new file mode 100644 index 000000000..63d5e08f0 --- /dev/null +++ b/packages/internal/database/src/drizzle/0035_last_valeria_richards.sql @@ -0,0 +1,21 @@ +PRAGMA foreign_keys=OFF;--> statement-breakpoint +CREATE TABLE `__new_ai_chat_messages` ( + `id` text PRIMARY KEY NOT NULL, + `chat_id` text NOT NULL, + `role` text NOT NULL, + `created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL, + `metadata` text, + `status` text DEFAULT 'completed', + `finished_at` integer, + `message_parts` text, + FOREIGN KEY (`chat_id`) REFERENCES `ai_chat_sessions`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +INSERT INTO `__new_ai_chat_messages`("id", "chat_id", "role", "created_at", "metadata", "status", "finished_at", "message_parts") SELECT "id", "chat_id", "role", "created_at", "metadata", "status", "finished_at", "message_parts" FROM `ai_chat_messages`;--> statement-breakpoint +DROP TABLE `ai_chat_messages`;--> statement-breakpoint +ALTER TABLE `__new_ai_chat_messages` RENAME TO `ai_chat_messages`;--> statement-breakpoint +PRAGMA foreign_keys=ON;--> statement-breakpoint +CREATE INDEX `idx_ai_chat_messages_chat_id_created_at` ON `ai_chat_messages` (`chat_id`,`created_at`);--> statement-breakpoint +CREATE INDEX `idx_ai_chat_messages_status` ON `ai_chat_messages` (`status`);--> statement-breakpoint +CREATE INDEX `idx_ai_chat_messages_chat_id_role` ON `ai_chat_messages` (`chat_id`,`role`);--> statement-breakpoint +ALTER TABLE `ai_chat_sessions` ADD `is_local` integer DEFAULT false NOT NULL; \ No newline at end of file diff --git a/packages/internal/database/src/drizzle/meta/0035_snapshot.json b/packages/internal/database/src/drizzle/meta/0035_snapshot.json new file mode 100644 index 000000000..c3b935cf7 --- /dev/null +++ b/packages/internal/database/src/drizzle/meta/0035_snapshot.json @@ -0,0 +1,947 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "1ba7c166-20b5-473e-aea4-5fa6e99dff23", + "prevId": "6cfe24ba-739c-4f3f-a894-dd70ecb47d02", + "tables": { + "ai_chat_messages": { + "name": "ai_chat_messages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "chat_id": { + "name": "chat_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'completed'" + }, + "finished_at": { + "name": "finished_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "message_parts": { + "name": "message_parts", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_ai_chat_messages_chat_id_created_at": { + "name": "idx_ai_chat_messages_chat_id_created_at", + "columns": ["chat_id", "created_at"], + "isUnique": false + }, + "idx_ai_chat_messages_status": { + "name": "idx_ai_chat_messages_status", + "columns": ["status"], + "isUnique": false + }, + "idx_ai_chat_messages_chat_id_role": { + "name": "idx_ai_chat_messages_chat_id_role", + "columns": ["chat_id", "role"], + "isUnique": false + } + }, + "foreignKeys": { + "ai_chat_messages_chat_id_ai_chat_sessions_id_fk": { + "name": "ai_chat_messages_chat_id_ai_chat_sessions_id_fk", + "tableFrom": "ai_chat_messages", + "tableTo": "ai_chat_sessions", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ai_chat_sessions": { + "name": "ai_chat_sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "is_local": { + "name": "is_local", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "idx_ai_chat_sessions_updated_at": { + "name": "idx_ai_chat_sessions_updated_at", + "columns": ["updated_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "collections": { + "name": "collections", + "columns": { + "feed_id": { + "name": "feed_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "entry_id": { + "name": "entry_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "view": { + "name": "view", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "entries": { + "name": "entries", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_content": { + "name": "source_content", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "readability_updated_at": { + "name": "readability_updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "guid": { + "name": "guid", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "author_url": { + "name": "author_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "author_avatar": { + "name": "author_avatar", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "inserted_at": { + "name": "inserted_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "published_at": { + "name": "published_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "media": { + "name": "media", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "categories": { + "name": "categories", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "attachments": { + "name": "attachments", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "extra": { + "name": "extra", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "feed_id": { + "name": "feed_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "inbox_handle": { + "name": "inbox_handle", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "read": { + "name": "read", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sources": { + "name": "sources", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "settings": { + "name": "settings", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "feeds": { + "name": "feeds", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_at": { + "name": "error_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "site_url": { + "name": "site_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "subscription_count": { + "name": "subscription_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updates_per_week": { + "name": "updates_per_week", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "latest_entry_published_at": { + "name": "latest_entry_published_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tip_users": { + "name": "tip_users", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "published_at": { + "name": "published_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "images": { + "name": "images", + "columns": { + "url": { + "name": "url", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "colors": { + "name": "colors", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "inboxes": { + "name": "inboxes", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "lists": { + "name": "lists", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "feed_ids": { + "name": "feed_ids", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "view": { + "name": "view", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "fee": { + "name": "fee", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "subscription_count": { + "name": "subscription_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "purchase_amount": { + "name": "purchase_amount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "subscriptions": { + "name": "subscriptions", + "columns": { + "feed_id": { + "name": "feed_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "list_id": { + "name": "list_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "inbox_id": { + "name": "inbox_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "view": { + "name": "view", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_private": { + "name": "is_private", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "hide_from_timeline": { + "name": "hide_from_timeline", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "summaries": { + "name": "summaries", + "columns": { + "entry_id": { + "name": "entry_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "readability_summary": { + "name": "readability_summary", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "unq": { + "name": "unq", + "columns": ["entry_id", "language"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "translations": { + "name": "translations", + "columns": { + "entry_id": { + "name": "entry_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "readability_content": { + "name": "readability_content", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "translation-unique-index": { + "name": "translation-unique-index", + "columns": ["entry_id", "language"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "unread": { + "name": "unread", + "columns": { + "subscription_id": { + "name": "subscription_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_me": { + "name": "is_me", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email_verified": { + "name": "email_verified", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "website": { + "name": "website", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "social_links": { + "name": "social_links", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} diff --git a/packages/internal/database/src/drizzle/meta/_journal.json b/packages/internal/database/src/drizzle/meta/_journal.json index 9d90c0f87..da905dc4d 100644 --- a/packages/internal/database/src/drizzle/meta/_journal.json +++ b/packages/internal/database/src/drizzle/meta/_journal.json @@ -246,6 +246,13 @@ "when": 1753937164043, "tag": "0034_curly_darkstar", "breakpoints": true + }, + { + "idx": 35, + "version": "6", + "when": 1761930812297, + "tag": "0035_last_valeria_richards", + "breakpoints": true } ] } diff --git a/packages/internal/database/src/drizzle/migrations.js b/packages/internal/database/src/drizzle/migrations.js index 0ec674a8d..348727c20 100644 --- a/packages/internal/database/src/drizzle/migrations.js +++ b/packages/internal/database/src/drizzle/migrations.js @@ -35,6 +35,7 @@ import m0031 from "./0031_kind_ikaris.sql" import m0032 from "./0032_orange_prima.sql" import m0033 from "./0033_shiny_sebastian_shaw.sql" import m0034 from "./0034_curly_darkstar.sql" +import m0035 from "./0035_last_valeria_richards.sql" import journal from "./meta/_journal.json" export default { @@ -75,5 +76,6 @@ export default { m0032, m0033, m0034, + m0035, }, } diff --git a/packages/internal/database/src/schemas/index.ts b/packages/internal/database/src/schemas/index.ts index c19ef702b..9aa6f665c 100644 --- a/packages/internal/database/src/schemas/index.ts +++ b/packages/internal/database/src/schemas/index.ts @@ -169,6 +169,7 @@ export const aiChatTable = sqliteTable( .integer("updated_at", { mode: "timestamp_ms" }) .notNull() .default(sql`(unixepoch() * 1000)`), + isLocal: t.integer("is_local", { mode: "boolean" }).notNull().default(false), }), (table) => [index("idx_ai_chat_sessions_updated_at").on(table.updatedAt)], )