NitishStark's picture
Upload folder using huggingface_hub
c20f20c verified
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');
/**
* Legacy Snapshot type for undo/redo functionality
* Used by useSnapshotStore
*/
export interface Snapshot {
id?: number;
index: number;
slides: Scene[];
}
/**
* MAIC Local Database
*
* Uses IndexedDB to store all user data locally
* - Does not delete expired data; all data is stored permanently
* - Uses a fixed database name
* - Supports multi-course management
*/
// ==================== Database Table Type Definitions ====================
/**
* Stage table - Course basic info
*/
export interface StageRecord {
id: string; // Primary key
name: string;
description?: string;
createdAt: number; // timestamp
updatedAt: number; // timestamp
language?: string;
style?: string;
currentSceneId?: string;
}
/**
* Scene table - Scene/page data
*/
export interface SceneRecord {
id: string; // Primary key
stageId: string; // Foreign key -> stages.id
type: SceneType;
title: string;
order: number; // Display order
content: SceneContent; // Stored as JSON
actions?: Action[]; // Stored as JSON
whiteboard?: Whiteboard[]; // Stored as JSON
createdAt: number;
updatedAt: number;
}
/**
* AudioFile table - Audio files (TTS)
*/
export interface AudioFileRecord {
id: string; // Primary key (audioId)
blob: Blob; // Audio binary data
duration?: number; // Duration (seconds)
format: string; // mp3, wav, etc.
text?: string; // Corresponding text content
voice?: string; // Voice used
createdAt: number;
ossKey?: string; // Full CDN URL for this audio blob
}
/**
* ImageFile table - Image files
*/
export interface ImageFileRecord {
id: string; // Primary key
blob: Blob; // Image binary data
filename: string; // Original filename
mimeType: string; // image/png, image/jpeg, etc.
size: number; // File size (bytes)
createdAt: number;
}
/**
* ChatSession table - Chat session data
*/
export interface ChatSessionRecord {
id: string; // PK (session id)
stageId: string; // FK -> stages.id
type: SessionType;
title: string;
status: SessionStatus;
messages: UIMessage[]; // JSON-safe serialized messages
config: SessionConfig;
toolCalls: ToolCallRecord[];
pendingToolCalls: ToolCallRequest[];
createdAt: number;
updatedAt: number;
sceneId?: string;
lastActionIndex?: number;
}
/**
* PlaybackState table - Playback state snapshot (at most one per stage)
*/
export interface PlaybackStateRecord {
stageId: string; // PK
sceneIndex: number;
actionIndex: number;
consumedDiscussions: string[];
updatedAt: number;
}
/**
* StageOutlines table - Persisted outlines for resume-on-refresh
*/
export interface StageOutlinesRecord {
stageId: string; // Primary key (FK -> stages.id)
outlines: SceneOutline[];
createdAt: number;
updatedAt: number;
}
/**
* MediaFile table - AI-generated media files (images/videos)
*/
export interface MediaFileRecord {
id: string; // Compound key: `${stageId}:${elementId}`
stageId: string; // FK → stages.id
type: 'image' | 'video';
blob: Blob; // Media binary
mimeType: string; // image/png, video/mp4
size: number;
poster?: Blob; // Video thumbnail blob
prompt: string; // Original prompt (for retry)
params: string; // JSON-serialized generation params
error?: string; // If set, this is a failed task (blob is empty placeholder)
errorCode?: string; // Structured error code (e.g. 'CONTENT_SENSITIVE')
ossKey?: string; // Full CDN URL for this media blob
posterOssKey?: string; // Full CDN URL for the poster blob
createdAt: number;
}
/**
* GeneratedAgent table - AI-generated agent profiles
*/
export interface GeneratedAgentRecord {
id: string; // PK: agent ID (e.g. "gen-abc123")
stageId: string; // FK -> stages.id
name: string;
role: string; // 'teacher' | 'assistant' | 'student'
persona: string;
avatar: string;
color: string;
priority: number;
createdAt: number;
}
/** Build the compound primary key for mediaFiles: `${stageId}:${elementId}` */
export function mediaFileKey(stageId: string, elementId: string): string {
return `${stageId}:${elementId}`;
}
// ==================== Database Definition ====================
const DATABASE_NAME = 'MAIC-Database';
const _DATABASE_VERSION = 8;
/**
* MAIC Database Instance
*/
class MAICDatabase extends Dexie {
// Table definitions
stages!: EntityTable<StageRecord, 'id'>;
scenes!: EntityTable<SceneRecord, 'id'>;
audioFiles!: EntityTable<AudioFileRecord, 'id'>;
imageFiles!: EntityTable<ImageFileRecord, 'id'>;
snapshots!: EntityTable<Snapshot, 'id'>; // Undo/redo snapshots (legacy)
chatSessions!: EntityTable<ChatSessionRecord, 'id'>;
playbackState!: EntityTable<PlaybackStateRecord, 'stageId'>;
stageOutlines!: EntityTable<StageOutlinesRecord, 'stageId'>;
mediaFiles!: EntityTable<MediaFileRecord, 'id'>;
generatedAgents!: EntityTable<GeneratedAgentRecord, 'id'>;
constructor() {
super(DATABASE_NAME);
// Version 1: Initial schema
this.version(1).stores({
stages: 'id, updatedAt',
scenes: 'id, stageId, order, [stageId+order]',
audioFiles: 'id, createdAt',
imageFiles: 'id, createdAt',
snapshots: '++id',
// Previously had: messages, participants, discussions, sceneSnapshots
});
// Version 2: Remove unused tables
this.version(2).stores({
stages: 'id, updatedAt',
scenes: 'id, stageId, order, [stageId+order]',
audioFiles: 'id, createdAt',
imageFiles: 'id, createdAt',
snapshots: '++id',
// Delete removed tables
messages: null,
participants: null,
discussions: null,
sceneSnapshots: null,
});
// Version 3: Add chatSessions and playbackState tables
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',
});
// Version 4: Add stageOutlines table for resume-on-refresh
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',
});
// Version 5: Add mediaFiles table for async media generation
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]',
});
// Version 6: Fix mediaFiles primary key — use compound key stageId:elementId
// to prevent cross-course collisions (gen_img_1 is NOT globally unique)
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}`;
// Skip if already migrated (idempotent)
if (rec.id.includes(':')) continue;
await table.delete(rec.id);
await table.put({ ...rec, id: newKey });
}
});
// Version 7: Add ossKey fields to mediaFiles and audioFiles for OSS storage plugin
// Non-indexed optional fields — Dexie handles these transparently.
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]',
});
// Version 8: Add generatedAgents table for AI-generated agent profiles
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',
});
}
}
// Create database instance
export const db = new MAICDatabase();
// ==================== Helper Functions ====================
/**
* Initialize database
* Call at application startup
*/
export async function initDatabase(): Promise<void> {
try {
await db.open();
// Request persistent storage to prevent browser from evicting IndexedDB
// under storage pressure (large media blobs can trigger LRU cleanup)
void navigator.storage?.persist?.();
log.info('Database initialized successfully');
} catch (error) {
log.error('Failed to initialize database:', error);
throw error;
}
}
/**
* Clear database (optional)
* Use with caution: deletes all data
*/
export async function clearDatabase(): Promise<void> {
await db.delete();
log.info('Database cleared');
}
/**
* Export database contents (for backup)
*/
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(),
};
}
/**
* Import database contents (for restoring backups)
*/
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');
}
// ==================== Convenience Query Functions ====================
/**
* Get all scenes for a course
*/
export async function getScenesByStageId(stageId: string): Promise<SceneRecord[]> {
return db.scenes.where('stageId').equals(stageId).sortBy('order');
}
/**
* Delete a course and all its related data
*/
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();
},
);
}
/**
* Get all generated agents for a course
*/
export async function getGeneratedAgentsByStageId(
stageId: string,
): Promise<GeneratedAgentRecord[]> {
return db.generatedAgents.where('stageId').equals(stageId).toArray();
}
/**
* Get database statistics
*/
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(),
};
}