This commit is contained in:
MarkLo127 2026-01-27 13:22:52 +08:00
parent 26ff5ce220
commit d58e80a962
7 changed files with 94 additions and 17 deletions

View File

@ -21,7 +21,7 @@ export default function AnalysisPage() {
const { setAnalysisResult, setTaskId, setMarketType, marketType } = useAnalysisContext(); const { setAnalysisResult, setTaskId, setMarketType, marketType } = useAnalysisContext();
const { runAnalysis, loading, error, result, taskId } = useAnalysis(); const { runAnalysis, loading, error, result, taskId } = useAnalysis();
const { isAuthenticated } = useAuth(); const { isAuthenticated } = useAuth();
const { t } = useLanguage(); const { t, locale } = useLanguage();
// Ref to track if we've already saved (to prevent duplicate saves) // Ref to track if we've already saved (to prevent duplicate saves)
const hasSavedRef = useRef(false); const hasSavedRef = useRef(false);
@ -47,7 +47,8 @@ export default function AnalysisPage() {
marketType, marketType,
result.analysis_date, result.analysis_date,
result, result,
taskId || undefined taskId || undefined,
locale as "en" | "zh-TW" // Pass current language for filtering
); );
console.log("📁 Auto-saved report to local storage"); console.log("📁 Auto-saved report to local storage");
@ -58,6 +59,7 @@ export default function AnalysisPage() {
market_type: marketType, market_type: marketType,
analysis_date: result.analysis_date, analysis_date: result.analysis_date,
result: result, result: result,
language: locale as "en" | "zh-TW",
}); });
if (cloudId) { if (cloudId) {
console.log("☁️ Auto-saved report to cloud"); console.log("☁️ Auto-saved report to cloud");
@ -68,7 +70,7 @@ export default function AnalysisPage() {
} catch (error) { } catch (error) {
console.error("Auto-save failed:", error); console.error("Auto-save failed:", error);
} }
}, [result, marketType, taskId, isAuthenticated]); }, [result, marketType, taskId, isAuthenticated, locale]);
// Auto-save when page unloads (closing tab, navigating away, etc.) // Auto-save when page unloads (closing tab, navigating away, etc.)
useEffect(() => { useEffect(() => {

View File

@ -97,7 +97,8 @@ export default function AnalysisResultsPage() {
marketType, marketType,
analysisResult.analysis_date, analysisResult.analysis_date,
analysisResult, analysisResult,
taskId || undefined taskId || undefined,
locale as "en" | "zh-TW" // Pass current language for filtering
); );
// If authenticated, also save to cloud // If authenticated, also save to cloud
@ -107,6 +108,7 @@ export default function AnalysisResultsPage() {
market_type: marketType, market_type: marketType,
analysis_date: analysisResult.analysis_date, analysis_date: analysisResult.analysis_date,
result: analysisResult, result: analysisResult,
language: locale as "en" | "zh-TW",
}); });
if (cloudId) { if (cloudId) {
setSavedToCloud(true); setSavedToCloud(true);

View File

@ -207,6 +207,41 @@ const extractDecisionFromReport = (report: SavedReport): { action: string; color
return { action: "N/A", color: "text-gray-500" }; return { action: "N/A", color: "text-gray-500" };
}; };
/**
* Detect report language from content (for backward compatibility with old reports)
* Checks trader_investment_plan for Chinese/English keywords
*/
const detectReportLanguage = (reports: any): "en" | "zh-TW" => {
const traderPlan = reports?.trader_investment_plan;
if (!traderPlan || typeof traderPlan !== 'string') {
// If no trader plan, check other reports for Chinese characters
const allText = JSON.stringify(reports || {});
const chineseRegex = /[\u4e00-\u9fa5]/;
return chineseRegex.test(allText) ? 'zh-TW' : 'en';
}
// Check for Chinese decision keywords
const chineseKeywords = ['買入', '賣出', '持有', '最終交易提案'];
for (const keyword of chineseKeywords) {
if (traderPlan.includes(keyword)) {
return 'zh-TW';
}
}
// Check for English decision keywords
const englishKeywords = ['buy', 'sell', 'hold', 'Final Trading Proposal'];
const lowerPlan = traderPlan.toLowerCase();
for (const keyword of englishKeywords) {
if (lowerPlan.includes(keyword.toLowerCase())) {
return 'en';
}
}
// Fallback: check for Chinese characters in the content
const chineseRegex = /[\u4e00-\u9fa5]/;
return chineseRegex.test(traderPlan) ? 'zh-TW' : 'en';
};
export default function HistoryPage() { export default function HistoryPage() {
const router = useRouter(); const router = useRouter();
const { setAnalysisResult, setTaskId, setMarketType } = useAnalysisContext(); const { setAnalysisResult, setTaskId, setMarketType } = useAnalysisContext();
@ -232,10 +267,10 @@ export default function HistoryPage() {
// Auto-sync tracking ref // Auto-sync tracking ref
const hasAutoSyncedRef = useRef(false); const hasAutoSyncedRef = useRef(false);
// Load reports when tab changes or auth state changes // Load reports when tab changes, auth state changes, or language changes
useEffect(() => { useEffect(() => {
loadReports(); loadReports();
}, [activeTab, isAuthenticated]); }, [activeTab, isAuthenticated, locale]);
// Load counts on mount or auth change // Load counts on mount or auth change
useEffect(() => { useEffect(() => {
@ -332,6 +367,7 @@ export default function HistoryPage() {
analysis_date: r.analysis_date, analysis_date: r.analysis_date,
saved_at: new Date(r.created_at), saved_at: new Date(r.created_at),
result: r.result, result: r.result,
language: r.language, // Include language from cloud
})) as (SavedReport & { cloudId?: string })[]; })) as (SavedReport & { cloudId?: string })[];
if (cloudFiltered.length > 0) { if (cloudFiltered.length > 0) {
@ -354,20 +390,43 @@ export default function HistoryPage() {
new Date(b.saved_at).getTime() - new Date(a.saved_at).getTime() new Date(b.saved_at).getTime() - new Date(a.saved_at).getTime()
); );
setReports(merged); // Filter by current language
const languageFiltered = merged.filter(report => {
// Use stored language if available
if (report.language) {
return report.language === locale;
}
// Fallback: detect from content for old reports without language field
return detectReportLanguage(report.result?.reports) === locale;
});
setReports(languageFiltered);
setIsCloudData(true); setIsCloudData(true);
return; return;
} }
} }
// If no cloud data or not authenticated, use local only // If no cloud data or not authenticated, use local only
setReports(localData); // Filter by current language
const languageFiltered = localData.filter(report => {
if (report.language) {
return report.language === locale;
}
return detectReportLanguage(report.result?.reports) === locale;
});
setReports(languageFiltered);
setIsCloudData(false); setIsCloudData(false);
} catch (error) { } catch (error) {
console.error("Failed to load reports:", error); console.error("Failed to load reports:", error);
// Fall back to local on error // Fall back to local on error
const data = await getReportsByMarketType(activeTab); const data = await getReportsByMarketType(activeTab);
setReports(data); const languageFiltered = data.filter(report => {
if (report.language) {
return report.language === locale;
}
return detectReportLanguage(report.result?.reports) === locale;
});
setReports(languageFiltered);
setIsCloudData(false); setIsCloudData(false);
} finally { } finally {
setLoading(false); setLoading(false);

View File

@ -13,12 +13,14 @@ import { getPendingTask, clearPendingTask, isPendingTaskValid, type PendingTask
import { saveReport, checkDuplicateReport } from "@/lib/reports-db"; import { saveReport, checkDuplicateReport } from "@/lib/reports-db";
import { saveCloudReport, isCloudSyncEnabled } from "@/lib/user-api"; import { saveCloudReport, isCloudSyncEnabled } from "@/lib/user-api";
import { useAuth } from "@/contexts/auth-context"; import { useAuth } from "@/contexts/auth-context";
import { useLanguage } from "@/contexts/LanguageContext";
export function PendingTaskRecovery() { export function PendingTaskRecovery() {
const [pendingTask, setPendingTask] = useState<PendingTask | null>(null); const [pendingTask, setPendingTask] = useState<PendingTask | null>(null);
const [status, setStatus] = useState<'checking' | 'found' | 'recovering' | 'success' | 'failed' | 'not_found'>('checking'); const [status, setStatus] = useState<'checking' | 'found' | 'recovering' | 'success' | 'failed' | 'not_found'>('checking');
const [message, setMessage] = useState<string>(""); const [message, setMessage] = useState<string>("");
const { isAuthenticated } = useAuth(); const { isAuthenticated } = useAuth();
const { locale } = useLanguage();
useEffect(() => { useEffect(() => {
// Check for pending tasks on mount // Check for pending tasks on mount
@ -72,7 +74,8 @@ export function PendingTaskRecovery() {
pendingTask.marketType, pendingTask.marketType,
taskStatus.result.analysis_date, taskStatus.result.analysis_date,
taskStatus.result, taskStatus.result,
pendingTask.taskId pendingTask.taskId,
locale as "en" | "zh-TW" // Pass current language for filtering
); );
// If authenticated, also save to cloud // If authenticated, also save to cloud
@ -82,6 +85,7 @@ export function PendingTaskRecovery() {
market_type: pendingTask.marketType, market_type: pendingTask.marketType,
analysis_date: taskStatus.result.analysis_date, analysis_date: taskStatus.result.analysis_date,
result: taskStatus.result, result: taskStatus.result,
language: locale as "en" | "zh-TW",
}); });
} }
@ -121,7 +125,7 @@ export function PendingTaskRecovery() {
setStatus('failed'); setStatus('failed');
} }
} }
}, [pendingTask, isAuthenticated]); }, [pendingTask, isAuthenticated, locale]);
const handleDismiss = () => { const handleDismiss = () => {
clearPendingTask(); clearPendingTask();

View File

@ -219,7 +219,8 @@ export function AuthProvider({ children }: { children: ReactNode }) {
report.market_type, report.market_type,
report.analysis_date, report.analysis_date,
report.result, report.result,
(report as any).task_id (report as any).task_id,
report.language // Preserve language from cloud
); );
} }
console.log(`Restored ${cloudReports.length} reports from cloud`); console.log(`Restored ${cloudReports.length} reports from cloud`);

View File

@ -15,6 +15,7 @@ export interface SavedReport {
saved_at: Date; // Save timestamp saved_at: Date; // Save timestamp
task_id?: string; // Original task ID task_id?: string; // Original task ID
result: AnalysisResponse; // Full analysis result result: AnalysisResponse; // Full analysis result
language?: "en" | "zh-TW"; // Language of the report (for filtering)
} }
// Database class extending Dexie // Database class extending Dexie
@ -23,10 +24,14 @@ class ReportsDatabase extends Dexie {
constructor() { constructor() {
super("TradingAgentsReports"); super("TradingAgentsReports");
// Version 1: Original schema
this.version(1).stores({ this.version(1).stores({
// Define indexes: ++id = auto-increment, others are indexed fields
reports: "++id, ticker, market_type, analysis_date, saved_at", 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",
});
} }
} }
@ -41,7 +46,8 @@ export async function saveReport(
market_type: "us" | "twse" | "tpex", market_type: "us" | "twse" | "tpex",
analysis_date: string, analysis_date: string,
result: AnalysisResponse, result: AnalysisResponse,
task_id?: string task_id?: string,
language?: "en" | "zh-TW",
): Promise<number> { ): Promise<number> {
const report: SavedReport = { const report: SavedReport = {
ticker, ticker,
@ -50,6 +56,7 @@ export async function saveReport(
saved_at: new Date(), saved_at: new Date(),
task_id, task_id,
result, result,
language,
}; };
return await db.reports.add(report); return await db.reports.add(report);
@ -59,7 +66,7 @@ export async function saveReport(
* Get all reports by market type * Get all reports by market type
*/ */
export async function getReportsByMarketType( export async function getReportsByMarketType(
market_type: "us" | "twse" | "tpex" market_type: "us" | "twse" | "tpex",
): Promise<SavedReport[]> { ): Promise<SavedReport[]> {
return await db.reports return await db.reports
.where("market_type") .where("market_type")
@ -79,7 +86,7 @@ export async function getAllReports(): Promise<SavedReport[]> {
* Get a single report by ID * Get a single report by ID
*/ */
export async function getReportById( export async function getReportById(
id: number id: number,
): Promise<SavedReport | undefined> { ): Promise<SavedReport | undefined> {
return await db.reports.get(id); return await db.reports.get(id);
} }
@ -120,7 +127,7 @@ export async function getReportCountByMarketType(): Promise<{
*/ */
export async function checkDuplicateReport( export async function checkDuplicateReport(
ticker: string, ticker: string,
analysis_date: string analysis_date: string,
): Promise<SavedReport | undefined> { ): Promise<SavedReport | undefined> {
return await db.reports return await db.reports
.where("ticker") .where("ticker")

View File

@ -15,6 +15,7 @@ interface CloudReport {
analysis_date: string; analysis_date: string;
result: any; result: any;
created_at: string; created_at: string;
language?: "en" | "zh-TW"; // Language of the report
} }
/** /**
@ -102,6 +103,7 @@ export async function saveCloudReport(report: {
market_type: "us" | "twse" | "tpex"; market_type: "us" | "twse" | "tpex";
analysis_date: string; analysis_date: string;
result: any; result: any;
language?: "en" | "zh-TW";
}): Promise<string | null> { }): Promise<string | null> {
if (!isCloudSyncEnabled()) { if (!isCloudSyncEnabled()) {
console.warn("☁️ Cloud sync not enabled (no auth token)"); console.warn("☁️ Cloud sync not enabled (no auth token)");