/** * IndexedDB database for storing analysis reports * Uses Dexie.js for a cleaner API */ import Dexie, { type Table } from "dexie"; import type { AnalysisResponse } from "./types"; import { normalizeLanguage } from "./report-utils"; // Saved report interface export interface SavedReport { id?: number; // Auto-generated primary key ticker: string; // Stock ticker symbol market_type: "us" | "twse" | "tpex"; // Market type analysis_date: string; // Analysis date (YYYY-MM-DD) saved_at: Date; // Save timestamp task_id?: string; // Original task ID result: AnalysisResponse; // Full analysis result language?: "en" | "zh-TW"; // Language of the report (for filtering) cloud_id?: string; // Corresponding cloud report ID (for sync tracking) pending_sync?: boolean; // Whether report is waiting to be synced to cloud } // Database class extending Dexie class ReportsDatabase extends Dexie { reports!: Table; constructor() { super("TradingAgentsReports"); // Version 1: Original schema this.version(1).stores({ reports: "++id, ticker, market_type, analysis_date, saved_at", }); // Version 2: Added language field for filtering by UI language this.version(2).stores({ reports: "++id, ticker, market_type, analysis_date, saved_at, language", }); // Version 3: Added cloud_id and pending_sync for sync tracking this.version(3).stores({ reports: "++id, ticker, market_type, analysis_date, saved_at, language, cloud_id, pending_sync", }); } } // Database singleton instance const db = new ReportsDatabase(); /** * Save a report to the database */ export async function saveReport( ticker: string, market_type: "us" | "twse" | "tpex", analysis_date: string, result: AnalysisResponse, task_id?: string, language?: "en" | "zh-TW", ): Promise { const report: SavedReport = { ticker, market_type, analysis_date, saved_at: new Date(), task_id, result, language, }; return await db.reports.add(report); } /** * Get all reports by market type */ export async function getReportsByMarketType( market_type: "us" | "twse" | "tpex", ): Promise { return await db.reports .where("market_type") .equals(market_type) .reverse() .sortBy("saved_at"); } /** * Get all saved reports, sorted by saved_at descending */ export async function getAllReports(): Promise { return await db.reports.orderBy("saved_at").reverse().toArray(); } /** * Get a single report by ID */ export async function getReportById( id: number, ): Promise { return await db.reports.get(id); } /** * Delete a report by ID */ export async function deleteReport(id: number): Promise { await db.reports.delete(id); } /** * Delete multiple reports by IDs */ export async function deleteReports(ids: number[]): Promise { await db.reports.bulkDelete(ids); } /** * Get report count by market type */ export async function getReportCountByMarketType(): Promise<{ us: number; twse: number; tpex: number; }> { const [us, twse, tpex] = await Promise.all([ db.reports.where("market_type").equals("us").count(), db.reports.where("market_type").equals("twse").count(), db.reports.where("market_type").equals("tpex").count(), ]); return { us, twse, tpex }; } /** * Check if a report with the same signature already exists. * Supports optional market_type and language for precise matching. */ export async function checkDuplicateReport( ticker: string, analysis_date: string, market_type?: "us" | "twse" | "tpex", language?: "en" | "zh-TW", ): Promise { const normalizedLang = normalizeLanguage(language); return await db.reports .where("ticker") .equals(ticker) .and((report) => { if (report.analysis_date !== analysis_date) return false; if (market_type && report.market_type !== market_type) return false; if (normalizeLanguage(report.language) !== normalizedLang) return false; return true; }) .first(); } /** * Check if a report exists by ticker, date, market type, and language * Used for bidirectional sync to prevent duplicates. * Language is normalized so null/undefined matches "zh-TW". */ export async function findExistingReport( ticker: string, analysis_date: string, market_type: "us" | "twse" | "tpex", language?: "en" | "zh-TW", ): Promise { const normalizedLang = normalizeLanguage(language); return await db.reports .where("ticker") .equals(ticker) .and( (report) => report.analysis_date === analysis_date && report.market_type === market_type && normalizeLanguage(report.language) === normalizedLang ) .first(); } /** * Bulk save reports to the database (for syncing from cloud) * Skips reports that already exist locally */ export async function bulkSaveReports( reports: Omit[] ): Promise { let savedCount = 0; for (const report of reports) { const existing = await findExistingReport( report.ticker, report.analysis_date, report.market_type, report.language ); if (!existing) { await db.reports.add(report as SavedReport); savedCount++; } } return savedCount; } /** * Clear all reports from the database (for logout) */ export async function clearAllReports(): Promise { await db.reports.clear(); } // Export the db instance for advanced usage export { db };