| import Dexie, { type EntityTable } from 'dexie'; |
| import type { Scene, SceneType, SceneContent, Whiteboard } from '@/lib/types/stage'; |
| import type { Action } from '@/lib/types/action'; |
| import type { |
| SessionType, |
| SessionStatus, |
| SessionConfig, |
| ToolCallRecord, |
| ToolCallRequest, |
| } from '@/lib/types/chat'; |
| import type { SceneOutline } from '@/lib/types/generation'; |
| import type { UIMessage } from 'ai'; |
| import { createLogger } from '@/lib/logger'; |
|
|
| const log = createLogger('Database'); |
|
|
| |
| |
| |
| |
| export interface Snapshot { |
| id?: number; |
| index: number; |
| slides: Scene[]; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
|
|
| |
| |
| |
| export interface StageRecord { |
| id: string; |
| name: string; |
| description?: string; |
| createdAt: number; |
| updatedAt: number; |
| languageDirective?: string; |
| style?: string; |
| currentSceneId?: string; |
| agentIds?: string[]; |
| interactiveMode?: boolean; |
| } |
|
|
| |
| |
| |
| export interface SceneRecord { |
| id: string; |
| stageId: string; |
| type: SceneType; |
| title: string; |
| order: number; |
| content: SceneContent; |
| actions?: Action[]; |
| whiteboard?: Whiteboard[]; |
| createdAt: number; |
| updatedAt: number; |
| } |
|
|
| |
| |
| |
| export interface AudioFileRecord { |
| id: string; |
| blob: Blob; |
| duration?: number; |
| format: string; |
| text?: string; |
| voice?: string; |
| createdAt: number; |
| ossKey?: string; |
| } |
|
|
| |
| |
| |
| export interface ImageFileRecord { |
| id: string; |
| blob: Blob; |
| filename: string; |
| mimeType: string; |
| size: number; |
| createdAt: number; |
| } |
|
|
| |
| |
| |
| export interface ChatSessionRecord { |
| id: string; |
| stageId: string; |
| type: SessionType; |
| title: string; |
| status: SessionStatus; |
| messages: UIMessage[]; |
| config: SessionConfig; |
| toolCalls: ToolCallRecord[]; |
| pendingToolCalls: ToolCallRequest[]; |
| createdAt: number; |
| updatedAt: number; |
| sceneId?: string; |
| lastActionIndex?: number; |
| } |
|
|
| |
| |
| |
| export interface PlaybackStateRecord { |
| stageId: string; |
| sceneIndex: number; |
| actionIndex: number; |
| consumedDiscussions: string[]; |
| updatedAt: number; |
| } |
|
|
| |
| |
| |
| export interface StageOutlinesRecord { |
| stageId: string; |
| outlines: SceneOutline[]; |
| createdAt: number; |
| updatedAt: number; |
| } |
|
|
| |
| |
| |
| export interface MediaFileRecord { |
| id: string; |
| stageId: string; |
| type: 'image' | 'video'; |
| blob: Blob; |
| mimeType: string; |
| size: number; |
| poster?: Blob; |
| prompt: string; |
| params: string; |
| error?: string; |
| errorCode?: string; |
| ossKey?: string; |
| posterOssKey?: string; |
| createdAt: number; |
| } |
|
|
| |
| |
| |
| export interface GeneratedAgentRecord { |
| id: string; |
| stageId: string; |
| name: string; |
| role: string; |
| persona: string; |
| avatar: string; |
| color: string; |
| priority: number; |
| createdAt: number; |
| } |
|
|
| |
| |
| |
| export interface VoiceProfileRecord { |
| id: string; |
| providerId: string; |
| kind: 'prompt' | 'clone'; |
| name: string; |
| voicePrompt?: string; |
| promptText?: string; |
| referenceAudio?: Blob; |
| referenceAudioName?: string; |
| referenceAudioMimeType?: string; |
| createdAt: number; |
| updatedAt: number; |
| } |
|
|
| |
| export function mediaFileKey(stageId: string, elementId: string): string { |
| return `${stageId}:${elementId}`; |
| } |
|
|
| |
|
|
| const DATABASE_NAME = 'MultiMind-Database'; |
| const _DATABASE_VERSION = 10; |
|
|
| |
| |
| |
| class MultiMindDatabase extends Dexie { |
| |
| stages!: EntityTable<StageRecord, 'id'>; |
| scenes!: EntityTable<SceneRecord, 'id'>; |
| audioFiles!: EntityTable<AudioFileRecord, 'id'>; |
| imageFiles!: EntityTable<ImageFileRecord, 'id'>; |
| snapshots!: EntityTable<Snapshot, 'id'>; |
| chatSessions!: EntityTable<ChatSessionRecord, 'id'>; |
| playbackState!: EntityTable<PlaybackStateRecord, 'stageId'>; |
| stageOutlines!: EntityTable<StageOutlinesRecord, 'stageId'>; |
| mediaFiles!: EntityTable<MediaFileRecord, 'id'>; |
| generatedAgents!: EntityTable<GeneratedAgentRecord, 'id'>; |
| voiceProfiles!: EntityTable<VoiceProfileRecord, 'id'>; |
|
|
| constructor() { |
| super(DATABASE_NAME); |
|
|
| |
| this.version(1).stores({ |
| stages: 'id, updatedAt', |
| scenes: 'id, stageId, order, [stageId+order]', |
| audioFiles: 'id, createdAt', |
| imageFiles: 'id, createdAt', |
| snapshots: '++id', |
| |
| }); |
|
|
| |
| this.version(2).stores({ |
| stages: 'id, updatedAt', |
| scenes: 'id, stageId, order, [stageId+order]', |
| audioFiles: 'id, createdAt', |
| imageFiles: 'id, createdAt', |
| snapshots: '++id', |
| |
| messages: null, |
| participants: null, |
| discussions: null, |
| sceneSnapshots: null, |
| }); |
|
|
| |
| this.version(3).stores({ |
| stages: 'id, updatedAt', |
| scenes: 'id, stageId, order, [stageId+order]', |
| audioFiles: 'id, createdAt', |
| imageFiles: 'id, createdAt', |
| snapshots: '++id', |
| chatSessions: 'id, stageId, [stageId+createdAt]', |
| playbackState: 'stageId', |
| }); |
|
|
| |
| this.version(4).stores({ |
| stages: 'id, updatedAt', |
| scenes: 'id, stageId, order, [stageId+order]', |
| audioFiles: 'id, createdAt', |
| imageFiles: 'id, createdAt', |
| snapshots: '++id', |
| chatSessions: 'id, stageId, [stageId+createdAt]', |
| playbackState: 'stageId', |
| stageOutlines: 'stageId', |
| }); |
|
|
| |
| this.version(5).stores({ |
| stages: 'id, updatedAt', |
| scenes: 'id, stageId, order, [stageId+order]', |
| audioFiles: 'id, createdAt', |
| imageFiles: 'id, createdAt', |
| snapshots: '++id', |
| chatSessions: 'id, stageId, [stageId+createdAt]', |
| playbackState: 'stageId', |
| stageOutlines: 'stageId', |
| mediaFiles: 'id, stageId, [stageId+type]', |
| }); |
|
|
| |
| |
| this.version(6) |
| .stores({ |
| stages: 'id, updatedAt', |
| scenes: 'id, stageId, order, [stageId+order]', |
| audioFiles: 'id, createdAt', |
| imageFiles: 'id, createdAt', |
| snapshots: '++id', |
| chatSessions: 'id, stageId, [stageId+createdAt]', |
| playbackState: 'stageId', |
| stageOutlines: 'stageId', |
| mediaFiles: 'id, stageId, [stageId+type]', |
| }) |
| .upgrade(async (tx) => { |
| const table = tx.table('mediaFiles'); |
| const allRecords = await table.toArray(); |
| for (const rec of allRecords) { |
| const newKey = `${rec.stageId}:${rec.id}`; |
| |
| if (rec.id.includes(':')) continue; |
| await table.delete(rec.id); |
| await table.put({ ...rec, id: newKey }); |
| } |
| }); |
|
|
| |
| |
| this.version(7).stores({ |
| stages: 'id, updatedAt', |
| scenes: 'id, stageId, order, [stageId+order]', |
| audioFiles: 'id, createdAt', |
| imageFiles: 'id, createdAt', |
| snapshots: '++id', |
| chatSessions: 'id, stageId, [stageId+createdAt]', |
| playbackState: 'stageId', |
| stageOutlines: 'stageId', |
| mediaFiles: 'id, stageId, [stageId+type]', |
| }); |
|
|
| |
| this.version(8).stores({ |
| stages: 'id, updatedAt', |
| scenes: 'id, stageId, order, [stageId+order]', |
| audioFiles: 'id, createdAt', |
| imageFiles: 'id, createdAt', |
| snapshots: '++id', |
| chatSessions: 'id, stageId, [stageId+createdAt]', |
| playbackState: 'stageId', |
| stageOutlines: 'stageId', |
| mediaFiles: 'id, stageId, [stageId+type]', |
| generatedAgents: 'id, stageId', |
| }); |
|
|
| |
| |
| |
| const LOCALE_TO_DIRECTIVE: Record<string, string> = { |
| 'zh-CN': 'Deliver the entire course in Chinese (Simplified, zh-CN).', |
| 'en-US': 'Deliver the entire course in English (en-US).', |
| 'ja-JP': 'Deliver the entire course in Japanese (ja-JP).', |
| 'ru-RU': 'Deliver the entire course in Russian (ru-RU).', |
| }; |
| this.version(9) |
| .stores({ |
| stages: 'id, updatedAt', |
| scenes: 'id, stageId, order, [stageId+order]', |
| audioFiles: 'id, createdAt', |
| imageFiles: 'id, createdAt', |
| snapshots: '++id', |
| chatSessions: 'id, stageId, [stageId+createdAt]', |
| playbackState: 'stageId', |
| stageOutlines: 'stageId', |
| mediaFiles: 'id, stageId, [stageId+type]', |
| generatedAgents: 'id, stageId', |
| }) |
| .upgrade(async (tx) => { |
| const table = tx.table('stages'); |
| await table.toCollection().modify((stage: Record<string, unknown>) => { |
| const lang = stage.language as string | undefined; |
| if (lang && !stage.languageDirective) { |
| stage.languageDirective = |
| LOCALE_TO_DIRECTIVE[lang] || `Deliver the entire course in ${lang}.`; |
| } |
| delete stage.language; |
| }); |
| }); |
|
|
| |
| this.version(10).stores({ |
| stages: 'id, updatedAt', |
| scenes: 'id, stageId, order, [stageId+order]', |
| audioFiles: 'id, createdAt', |
| imageFiles: 'id, createdAt', |
| snapshots: '++id', |
| chatSessions: 'id, stageId, [stageId+createdAt]', |
| playbackState: 'stageId', |
| stageOutlines: 'stageId', |
| mediaFiles: 'id, stageId, [stageId+type]', |
| generatedAgents: 'id, stageId', |
| voiceProfiles: 'id, providerId, kind, updatedAt', |
| }); |
| } |
| } |
|
|
| |
| export const db = new MultiMindDatabase(); |
|
|
| |
|
|
| |
| |
| |
| |
| export async function initDatabase(): Promise<void> { |
| try { |
| await db.open(); |
| |
| |
| void navigator.storage?.persist?.(); |
| log.info('Database initialized successfully'); |
| } catch (error) { |
| log.error('Failed to initialize database:', error); |
| throw error; |
| } |
| } |
|
|
| |
| |
| |
| |
| export async function clearDatabase(): Promise<void> { |
| await db.delete(); |
| log.info('Database cleared'); |
| } |
|
|
| |
| |
| |
| export async function exportDatabase(): Promise<{ |
| stages: StageRecord[]; |
| scenes: SceneRecord[]; |
| chatSessions: ChatSessionRecord[]; |
| playbackState: PlaybackStateRecord[]; |
| }> { |
| return { |
| stages: await db.stages.toArray(), |
| scenes: await db.scenes.toArray(), |
| chatSessions: await db.chatSessions.toArray(), |
| playbackState: await db.playbackState.toArray(), |
| }; |
| } |
|
|
| |
| |
| |
| export async function importDatabase(data: { |
| stages?: StageRecord[]; |
| scenes?: SceneRecord[]; |
| chatSessions?: ChatSessionRecord[]; |
| playbackState?: PlaybackStateRecord[]; |
| }): Promise<void> { |
| await db.transaction( |
| 'rw', |
| [db.stages, db.scenes, db.chatSessions, db.playbackState], |
| async () => { |
| if (data.stages) await db.stages.bulkPut(data.stages); |
| if (data.scenes) await db.scenes.bulkPut(data.scenes); |
| if (data.chatSessions) await db.chatSessions.bulkPut(data.chatSessions); |
| if (data.playbackState) await db.playbackState.bulkPut(data.playbackState); |
| }, |
| ); |
| log.info('Database imported successfully'); |
| } |
|
|
| |
|
|
| |
| |
| |
| export async function getScenesByStageId(stageId: string): Promise<SceneRecord[]> { |
| return db.scenes.where('stageId').equals(stageId).sortBy('order'); |
| } |
|
|
| |
| |
| |
| export async function deleteStageWithRelatedData(stageId: string): Promise<void> { |
| await db.transaction( |
| 'rw', |
| [ |
| db.stages, |
| db.scenes, |
| db.chatSessions, |
| db.playbackState, |
| db.stageOutlines, |
| db.mediaFiles, |
| db.generatedAgents, |
| ], |
| async () => { |
| await db.stages.delete(stageId); |
| await db.scenes.where('stageId').equals(stageId).delete(); |
| await db.chatSessions.where('stageId').equals(stageId).delete(); |
| await db.playbackState.delete(stageId); |
| await db.stageOutlines.delete(stageId); |
| await db.mediaFiles.where('stageId').equals(stageId).delete(); |
| await db.generatedAgents.where('stageId').equals(stageId).delete(); |
| }, |
| ); |
| } |
|
|
| |
| |
| |
| export async function getGeneratedAgentsByStageId( |
| stageId: string, |
| ): Promise<GeneratedAgentRecord[]> { |
| return db.generatedAgents.where('stageId').equals(stageId).toArray(); |
| } |
|
|
| |
| |
| |
| export async function getDatabaseStats() { |
| return { |
| stages: await db.stages.count(), |
| scenes: await db.scenes.count(), |
| audioFiles: await db.audioFiles.count(), |
| imageFiles: await db.imageFiles.count(), |
| snapshots: await db.snapshots.count(), |
| chatSessions: await db.chatSessions.count(), |
| playbackState: await db.playbackState.count(), |
| stageOutlines: await db.stageOutlines.count(), |
| mediaFiles: await db.mediaFiles.count(), |
| generatedAgents: await db.generatedAgents.count(), |
| }; |
| } |
|
|