TradingAgents/frontend/lib/reports-db.ts

212 lines
5.5 KiB
TypeScript

/**
* 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<SavedReport>;
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<number> {
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<SavedReport[]> {
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<SavedReport[]> {
return await db.reports.orderBy("saved_at").reverse().toArray();
}
/**
* Get a single report by ID
*/
export async function getReportById(
id: number,
): Promise<SavedReport | undefined> {
return await db.reports.get(id);
}
/**
* Delete a report by ID
*/
export async function deleteReport(id: number): Promise<void> {
await db.reports.delete(id);
}
/**
* Delete multiple reports by IDs
*/
export async function deleteReports(ids: number[]): Promise<void> {
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<SavedReport | undefined> {
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<SavedReport | undefined> {
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<SavedReport, "id">[]
): Promise<number> {
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<void> {
await db.reports.clear();
}
// Export the db instance for advanced usage
export { db };