fix(ai): fetch remote messages if not exist

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei 2025-11-01 02:07:31 +08:00
parent a939842ca9
commit 5aed1affab
No known key found for this signature in database
GPG Key ID: 0F62D33977F021F7
19 changed files with 1147 additions and 34 deletions

View File

@ -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<void> {
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

View File

@ -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) {

View File

@ -25,20 +25,21 @@ interface AIMessagePartsProps {
export const AIMessageParts: React.FC<AIMessagePartsProps> = 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<BizUITools>)[]

View File

@ -31,6 +31,7 @@ export const UserChatMessage: React.FC<UserChatMessageProps> = React.memo(({ mes
const setEditingMessageId = useSetEditingMessageId()
const chatStatus = useChatStatus()
const isStreaming = chatStatus === "submitted" || chatStatus === "streaming"
const isEditing = editingMessageId === messageId

View File

@ -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)

View File

@ -38,6 +38,17 @@ class AIPersistServiceStatic {
})
}
async hasPersistedMessages(chatId: string): Promise<boolean> {
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<void> {
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`

View File

@ -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()

View File

@ -61,11 +61,18 @@ export class ZustandChatState implements ChatState<BizUIMessage> {
})
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 }) => {

View File

@ -11,6 +11,8 @@ export interface ChatSlice {
status: ChatStatus
error: Error | undefined
isStreaming: boolean
isLocal: boolean
syncStatus: "local" | "synced"
// UI state
currentTitle: string | undefined

View File

@ -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

View File

@ -9,11 +9,13 @@ import { createChatTitleHandler, createChatTransport } from "../transport"
export const createChatSlice: (options: {
chatId: string
generateId?: IdGenerator
isLocal?: boolean
syncStatus?: "local" | "synced"
}) => StateCreator<ChatSlice, [], [], ChatSlice> =
(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,

View File

@ -51,6 +51,8 @@ export interface AIChatStoreInitial {
blocks: AIChatContextBlock[]
chatId?: string
generateId?: IdGenerator
isLocal?: boolean
syncStatus?: "local" | "synced"
}
export interface AIChatContextBlocks {

View File

@ -5,6 +5,8 @@ export interface ChatSession {
title?: string
createdAt: Date
updatedAt: Date
isLocal: boolean
syncStatus: "local" | "synced"
}
export type RichTextPart = {

View File

@ -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: [] }

View File

@ -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;

View File

@ -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": {}
}
}

View File

@ -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
}
]
}

View File

@ -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,
},
}

View File

@ -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)],
)