Commit ef3652c4 authored by ThinhNC's avatar ThinhNC

Merge branch 'feat/guest-trial-mode' into 'develop'

Feat/guest trial mode

See merge request !64
parents 9c34634c a9756283
...@@ -47,7 +47,7 @@ export const GuestModeBanner: React.FC<{ className?: string }> = ({ className = ...@@ -47,7 +47,7 @@ export const GuestModeBanner: React.FC<{ className?: string }> = ({ className =
</div> </div>
<div className="space-y-1 text-left"> <div className="space-y-1 text-left">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="inline-flex items-center justify-center h-5 rounded-md bg-amber-500/20 border border-amber-500/30 px-2 font-nunito text-[11px] font-bold text-amber-700 dark:text-amber-300 uppercase tracking-wide leading-none"> <span className="inline-flex items-center justify-center rounded-md bg-amber-500/20 border border-amber-500/30 px-2 pt-0.5 font-nunito text-[11px] font-bold text-amber-700 dark:text-amber-300 leading-normal">
{badgeText} {badgeText}
</span> </span>
</div> </div>
......
...@@ -629,8 +629,24 @@ export const ShieldCheckIcon: React.FC<IconProps> = ({ size = 18, ...props }) => ...@@ -629,8 +629,24 @@ export const ShieldCheckIcon: React.FC<IconProps> = ({ size = 18, ...props }) =>
</svg> </svg>
); );
// Info Icon
export const InfoIcon: React.FC<IconProps> = ({ size = 18, ...props }) => (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2.5"
strokeLinecap="round"
strokeLinejoin="round"
{...props}
>
<circle cx="12" cy="12" r="10" />
<line x1="12" y1="16" x2="12" y2="12" />
<line x1="12" y1="8" x2="12.01" y2="8" />
</svg>
);
......
...@@ -1412,6 +1412,28 @@ ...@@ -1412,6 +1412,28 @@
"shortHeader": "AI Assistant", "shortHeader": "AI Assistant",
"liveBadge": "Live AI", "liveBadge": "Live AI",
"subtitle": "Smart analysis and personal finance guidance", "subtitle": "Smart analysis and personal finance guidance",
"disclaimer": {
"button": "AI Notice",
"title": "Notice & Disclaimer on AI Features",
"badge": "Important Notice",
"lead": "To ensure transparency and accuracy in managing your personal finances, please note the following when using AI features:",
"points": {
"accuracy": {
"title": "Possibility of AI Inaccuracies",
"desc": "The AI Assistant and Chatbot are powered by artificial intelligence models. AI-generated responses and analytics may contain inaccuracies or errors."
},
"advisory": {
"title": "Informational Purpose Only",
"desc": "All insights, trend analysis, recommendations, and receipt extractions are for reference purposes only and do not constitute professional financial, tax, or investment advice."
},
"responsibility": {
"title": "Verify Important Information",
"desc": "Users should always verify data against actual bank statements, receipts, and invoices before making any financial decisions."
}
},
"confirm": "I Understand",
"learnMore": "Learn more"
},
"tabs": { "tabs": {
"label": "AI Assistant features", "label": "AI Assistant features",
"chat": "Advisor", "chat": "Advisor",
...@@ -1420,6 +1442,7 @@ ...@@ -1420,6 +1442,7 @@
"chat": { "chat": {
"title": "FinWise AI Advisor", "title": "FinWise AI Advisor",
"subtitle": "Ask questions about income, expenses, budgets & saving goals", "subtitle": "Ask questions about income, expenses, budgets & saving goals",
"disclaimer": "AI assistant may make mistakes. Please verify important financial information.",
"placeholder": "Ask AI about your personal finances...", "placeholder": "Ask AI about your personal finances...",
"send": "Send", "send": "Send",
"clearHistory": "New conversation", "clearHistory": "New conversation",
...@@ -1442,6 +1465,7 @@ ...@@ -1442,6 +1465,7 @@
"insights": { "insights": {
"title": "AI Financial Insights & Recommendations", "title": "AI Financial Insights & Recommendations",
"subtitle": "Automated trend analysis, anomaly detection, and optimization advice", "subtitle": "Automated trend analysis, anomaly detection, and optimization advice",
"disclaimer": "Notice: AI-generated insights and recommendations are for reference purposes only and may contain inaccuracies. Please cross-check with your actual financial data.",
"period": "Analysis Period", "period": "Analysis Period",
"tabsLabel": "Financial insights and recommendations", "tabsLabel": "Financial insights and recommendations",
"analysisTab": "Analysis", "analysisTab": "Analysis",
...@@ -1490,6 +1514,7 @@ ...@@ -1490,6 +1514,7 @@
"ocr": { "ocr": {
"title": "AI Receipt Extraction (OCR)", "title": "AI Receipt Extraction (OCR)",
"subtitle": "Upload or take a photo of a receipt for automatic data extraction", "subtitle": "Upload or take a photo of a receipt for automatic data extraction",
"disclaimer": "Notice: Automatic AI receipt extraction may contain errors. Please review the details before saving.",
"uploadTitle": "Upload or capture a receipt", "uploadTitle": "Upload or capture a receipt",
"uploadHint": "Supports JPEG, PNG, WebP or PDF files (max 5 MB)", "uploadHint": "Supports JPEG, PNG, WebP or PDF files (max 5 MB)",
"selectFile": "Select Receipt File", "selectFile": "Select Receipt File",
...@@ -1512,6 +1537,8 @@ ...@@ -1512,6 +1537,8 @@
"previewAlt": "Receipt preview", "previewAlt": "Receipt preview",
"itemCount": "{{count}} items", "itemCount": "{{count}} items",
"warnings": "AI Warnings", "warnings": "AI Warnings",
"defaultWarning": "Please verify extracted values against the original receipt before creating a transaction.",
"guestDemoWarning": "Sample receipt simulated in Demo Mode (processed 100% locally, no real AI tokens consumed).",
"confirmTransaction": "Confirm Create Transaction", "confirmTransaction": "Confirm Create Transaction",
"confirmHint": "Pre-fill this extracted receipt data into a new Transaction", "confirmHint": "Pre-fill this extracted receipt data into a new Transaction",
"reScan": "Scan Another Receipt" "reScan": "Scan Another Receipt"
...@@ -1519,6 +1546,7 @@ ...@@ -1519,6 +1546,7 @@
"preview": { "preview": {
"title": "AI summary and suggestions", "title": "AI summary and suggestions",
"subtitle": "The most important takeaways from the current report period", "subtitle": "The most important takeaways from the current report period",
"disclaimer": "AI-generated analysis is for informational purposes only and may contain errors.",
"summaryLabel": "Overview", "summaryLabel": "Overview",
"highlightsLabel": "{{count}} notable insights", "highlightsLabel": "{{count}} notable insights",
"scopeHint": "Detailed analysis will keep this report's date range and currency.", "scopeHint": "Detailed analysis will keep this report's date range and currency.",
......
...@@ -1513,6 +1513,28 @@ ...@@ -1513,6 +1513,28 @@
"shortHeader": "Trợ lý AI", "shortHeader": "Trợ lý AI",
"liveBadge": "AI trực tiếp", "liveBadge": "AI trực tiếp",
"subtitle": "Phân tích thông minh và tư vấn tài chính cá nhân", "subtitle": "Phân tích thông minh và tư vấn tài chính cá nhân",
"disclaimer": {
"button": "Lưu ý về AI",
"title": "Lưu ý & Khuyến cáo về Trợ lý AI",
"badge": "Thông tin quan trọng",
"lead": "Để đảm bảo an toàn và tính chính xác trong quá trình quản lý tài chính cá nhân, vui lòng lưu ý các điều sau khi sử dụng tính năng AI:",
"points": {
"accuracy": {
"title": "Khả năng sai sót của AI",
"desc": "Trợ lý AI và Chatbot được vận hành bởi mô hình trí tuệ nhân tạo. Phản hồi và phân tích của AI có thể không hoàn toàn chính xác hoặc có thể có sai sót trong việc nhận diện và tính toán."
},
"advisory": {
"title": "Chỉ mang tính chất tham khảo",
"desc": "Mọi câu trả lời, nhận định xu hướng chi tiêu, phân tích ngân sách hay bóc tách hóa đơn chỉ mang tính chất hỗ trợ và tham khảo, không cấu thành lời khuyên đầu tư, kế toán hay tài chính chuyên nghiệp."
},
"responsibility": {
"title": "Tự kiểm chứng dữ liệu",
"desc": "Người dùng cần luôn kiểm tra, đối chiếu lại với hóa đơn, sao kê ngân hàng và thực tế trước khi đưa ra các quyết định tài chính quan trọng."
}
},
"confirm": "Tôi đã hiểu",
"learnMore": "Xem lưu ý"
},
"tabs": { "tabs": {
"label": "Các chức năng của Trợ lý AI", "label": "Các chức năng của Trợ lý AI",
"chat": "Tư vấn", "chat": "Tư vấn",
...@@ -1521,6 +1543,7 @@ ...@@ -1521,6 +1543,7 @@
"chat": { "chat": {
"title": "FinWise AI Advisor", "title": "FinWise AI Advisor",
"subtitle": "Hỏi đáp thu chi, ngân sách & mục tiêu tiết kiệm", "subtitle": "Hỏi đáp thu chi, ngân sách & mục tiêu tiết kiệm",
"disclaimer": "Trợ lý AI có thể đưa ra câu trả lời không chính xác. Hãy kiểm chứng lại thông tin quan trọng.",
"placeholder": "Hỏi AI về tài chính cá nhân...", "placeholder": "Hỏi AI về tài chính cá nhân...",
"send": "Gửi", "send": "Gửi",
"clearHistory": "Cuộc trò chuyện mới", "clearHistory": "Cuộc trò chuyện mới",
...@@ -1543,6 +1566,7 @@ ...@@ -1543,6 +1566,7 @@
"insights": { "insights": {
"title": "Phân tích Tài chính và Đề xuất của AI", "title": "Phân tích Tài chính và Đề xuất của AI",
"subtitle": "Tự động phân tích xu hướng chi tiêu, phát hiện bất thường và đề xuất kế hoạch tài chính", "subtitle": "Tự động phân tích xu hướng chi tiêu, phát hiện bất thường và đề xuất kế hoạch tài chính",
"disclaimer": "Lưu ý: Các phân tích và gợi ý do AI tạo ra mang tính chất tham khảo và có thể có sai sót. Vui lòng đối chiếu với dữ liệu thực tế.",
"period": "Kỳ báo cáo", "period": "Kỳ báo cáo",
"tabsLabel": "Phân tích và đề xuất tài chính", "tabsLabel": "Phân tích và đề xuất tài chính",
"analysisTab": "Phân tích", "analysisTab": "Phân tích",
...@@ -1591,6 +1615,7 @@ ...@@ -1591,6 +1615,7 @@
"ocr": { "ocr": {
"title": "Trích xuất Hóa đơn bằng AI (OCR)", "title": "Trích xuất Hóa đơn bằng AI (OCR)",
"subtitle": "Tải hoặc chụp ảnh hóa đơn để AI tự động bóc tách thông tin giao dịch", "subtitle": "Tải hoặc chụp ảnh hóa đơn để AI tự động bóc tách thông tin giao dịch",
"disclaimer": "Lưu ý: Kết quả trích xuất tự động bằng AI có thể có sai sót. Vui lòng kiểm tra kỹ lại số tiền và hạng mục trước khi lưu.",
"uploadTitle": "Tải lên hoặc chụp ảnh hóa đơn", "uploadTitle": "Tải lên hoặc chụp ảnh hóa đơn",
"uploadHint": "Hỗ trợ tệp JPEG, PNG, WebP hoặc PDF (tối đa 5 MB)", "uploadHint": "Hỗ trợ tệp JPEG, PNG, WebP hoặc PDF (tối đa 5 MB)",
"selectFile": "Chọn tệp hóa đơn", "selectFile": "Chọn tệp hóa đơn",
...@@ -1613,6 +1638,8 @@ ...@@ -1613,6 +1638,8 @@
"previewAlt": "Bản xem trước hóa đơn", "previewAlt": "Bản xem trước hóa đơn",
"itemCount": "{{count}} món", "itemCount": "{{count}} món",
"warnings": "Cảnh báo từ AI", "warnings": "Cảnh báo từ AI",
"defaultWarning": "Vui lòng đối chiếu các giá trị đã trích xuất với hóa đơn gốc trước khi tạo giao dịch.",
"guestDemoWarning": "Hóa đơn mẫu mô phỏng trong Chế độ Dùng thử (100% xử lý tại máy, không tốn token AI thật).",
"confirmTransaction": "Xác nhận tạo Giao dịch", "confirmTransaction": "Xác nhận tạo Giao dịch",
"confirmHint": "Tự động điền dữ liệu hóa đơn này vào Form tạo giao dịch mới", "confirmHint": "Tự động điền dữ liệu hóa đơn này vào Form tạo giao dịch mới",
"reScan": "Quét hóa đơn khác" "reScan": "Quét hóa đơn khác"
...@@ -1620,6 +1647,7 @@ ...@@ -1620,6 +1647,7 @@
"preview": { "preview": {
"title": "Tóm tắt và đề xuất từ AI", "title": "Tóm tắt và đề xuất từ AI",
"subtitle": "Những điểm quan trọng nhất trong kỳ báo cáo hiện tại", "subtitle": "Những điểm quan trọng nhất trong kỳ báo cáo hiện tại",
"disclaimer": "Thông tin phân tích do AI tạo ra chỉ mang tính tham khảo và có thể có sai sót.",
"summaryLabel": "Nhận định tổng quan", "summaryLabel": "Nhận định tổng quan",
"highlightsLabel": "{{count}} điểm đáng chú ý", "highlightsLabel": "{{count}} điểm đáng chú ý",
"scopeHint": "Phân tích chi tiết sẽ tiếp tục sử dụng khoảng thời gian và tiền tệ của báo cáo này.", "scopeHint": "Phân tích chi tiết sẽ tiếp tục sử dụng khoảng thời gian và tiền tệ của báo cáo này.",
......
...@@ -1528,11 +1528,29 @@ export function handleGuestRequest(config: InternalAxiosRequestConfig): AxiosRes ...@@ -1528,11 +1528,29 @@ export function handleGuestRequest(config: InternalAxiosRequestConfig): AxiosRes
responseData = { responseData = {
success: true, success: true,
data: { data: {
merchant: "Siêu thị Co.opmart",
merchantName: "Siêu thị Co.opmart", merchantName: "Siêu thị Co.opmart",
totalAmount: "250000", totalAmount: "250000",
currency: "VND",
taxAmount: "20000",
transactionDate: todayStr, transactionDate: todayStr,
category: {
id: "cat-expense-1",
name: "Ăn uống",
icon: "utensils",
color: "#EF4444",
},
lineItems: [ lineItems: [
{ name: "Sữa chua hoa quả", quantity: 2, unitPrice: "35000", totalAmount: "70000" }, { name: "Sữa chua hoa quả Co.op", quantity: 2, unitPrice: "35000", totalAmount: "70000" },
{ name: "Bánh mì sandwich bơ", quantity: 1, unitPrice: "45000", totalAmount: "45000" },
{ name: "Nước ép cam ép tươi 1L", quantity: 2, unitPrice: "55000", totalAmount: "110000" },
{ name: "Khăn giấy ướt cao cấp", quantity: 1, unitPrice: "25000", totalAmount: "25000" },
],
rawText: `SIEU THI CO.OPMART\nNgay: ${todayStr}\n1. Sua chua hoa qua: 70.000\n2. Banh mi sandwich: 45.000\n3. Nuoc ep cam: 110.000\n4. Khan giay: 25.000\nTong tien: 250.000 VND\nVAT 8%: 20.000 VND`,
confidence: 0.96,
warnings: [
"Vui lòng đối chiếu các giá trị đã trích xuất với hóa đơn gốc trước khi tạo giao dịch.",
"Hóa đơn mẫu mô phỏng trong Chế độ Dùng thử (100% xử lý tại máy, không tốn token AI thật).",
], ],
}, },
}; };
......
...@@ -4,6 +4,7 @@ import { Card } from "@/components/ui/Card"; ...@@ -4,6 +4,7 @@ import { Card } from "@/components/ui/Card";
import { Input } from "@/components/ui/Input"; import { Input } from "@/components/ui/Input";
import { import {
AIAssistantIcon, AIAssistantIcon,
AlertTriangleIcon,
ChevronRightIcon, ChevronRightIcon,
ClockIcon, ClockIcon,
LightbulbIcon, LightbulbIcon,
...@@ -16,7 +17,11 @@ import { useAIChat } from "@/hooks/use-ai-assistant"; ...@@ -16,7 +17,11 @@ import { useAIChat } from "@/hooks/use-ai-assistant";
import { AIAssistantError } from "@/services/ai-assistant.service"; import { AIAssistantError } from "@/services/ai-assistant.service";
import { AIChatMessage, useAIChatStore } from "@/stores/ai-chat-store"; import { AIChatMessage, useAIChatStore } from "@/stores/ai-chat-store";
export const AIChatView: React.FC = () => { export interface AIChatViewProps {
onOpenDisclaimer?: () => void;
}
export const AIChatView: React.FC<AIChatViewProps> = ({ onOpenDisclaimer }) => {
const { t } = useI18n(); const { t } = useI18n();
const chatMutation = useAIChat(); const chatMutation = useAIChat();
...@@ -344,6 +349,23 @@ export const AIChatView: React.FC = () => { ...@@ -344,6 +349,23 @@ export const AIChatView: React.FC = () => {
{chatMutation.isPending ? t("common.processing") : t("ai.chat.send")} {chatMutation.isPending ? t("common.processing") : t("ai.chat.send")}
</Button> </Button>
</Card> </Card>
{/* Permanent disclaimer notice for AI content accuracy compliant with Zalo review */}
<div className="flex items-center justify-center gap-1.5 px-2 text-center">
<AlertTriangleIcon size={12} className="shrink-0 text-clay-warning" />
<p className="font-nunito text-[11px] text-clay-text-muted leading-tight">
{t("ai.chat.disclaimer")}
{onOpenDisclaimer && (
<button
type="button"
onClick={onOpenDisclaimer}
className="ml-1 font-semibold text-clay-primary hover:underline inline-flex items-center"
>
{t("ai.disclaimer.learnMore")}
</button>
)}
</p>
</div>
</div> </div>
</div> </div>
); );
......
import React from "react";
import { Modal } from "@/components/ui/Modal";
import { Button } from "@/components/ui/Button";
import {
AlertTriangleIcon,
ShieldCheckIcon,
SparklesIcon,
} from "@/components/ui/icons";
import { useI18n } from "@/i18n";
export interface AIDisclaimerModalProps {
isOpen: boolean;
onClose: () => void;
}
export const AIDisclaimerModal: React.FC<AIDisclaimerModalProps> = ({
isOpen,
onClose,
}) => {
const { t } = useI18n();
return (
<Modal
isOpen={isOpen}
onClose={onClose}
title={t("ai.disclaimer.title")}
footer={
<Button
variant="primary"
className="w-full py-2.5 text-sm font-bold shadow-clay-button"
onClick={onClose}
>
{t("ai.disclaimer.confirm")}
</Button>
}
>
<div className="flex flex-col gap-3.5 text-left">
{/* Warning Badge Header */}
<div className="flex items-start gap-3 rounded-2xl border border-clay-warning/35 bg-clay-warning/10 p-3 shadow-clay-card">
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-xl bg-clay-warning/20 text-clay-warning">
<AlertTriangleIcon size={18} />
</div>
<div className="space-y-0.5">
<h4 className="font-baloo text-xs font-bold uppercase tracking-wider text-clay-warning">
{t("ai.disclaimer.badge")}
</h4>
<p className="font-nunito text-xs text-clay-text-muted leading-relaxed">
{t("ai.disclaimer.lead")}
</p>
</div>
</div>
{/* 3 Key Warning Points */}
<div className="space-y-2.5">
{/* Point 1: Possibility of AI Errors */}
<div className="flex items-start gap-3 rounded-xl border border-clay-highlight/40 bg-clay-surface p-3 shadow-clay-card">
<div className="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg bg-clay-expense/15 text-clay-expense">
<AlertTriangleIcon size={15} />
</div>
<div className="space-y-0.5">
<h5 className="font-baloo text-xs font-bold text-clay-text">
{t("ai.disclaimer.points.accuracy.title")}
</h5>
<p className="font-nunito text-xs text-clay-text-muted leading-relaxed">
{t("ai.disclaimer.points.accuracy.desc")}
</p>
</div>
</div>
{/* Point 2: Informational Purpose Only */}
<div className="flex items-start gap-3 rounded-xl border border-clay-highlight/40 bg-clay-surface p-3 shadow-clay-card">
<div className="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg bg-clay-primary/15 text-clay-primary">
<SparklesIcon size={15} />
</div>
<div className="space-y-0.5">
<h5 className="font-baloo text-xs font-bold text-clay-text">
{t("ai.disclaimer.points.advisory.title")}
</h5>
<p className="font-nunito text-xs text-clay-text-muted leading-relaxed">
{t("ai.disclaimer.points.advisory.desc")}
</p>
</div>
</div>
{/* Point 3: User Verification Responsibility */}
<div className="flex items-start gap-3 rounded-xl border border-clay-highlight/40 bg-clay-surface p-3 shadow-clay-card">
<div className="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg bg-clay-income/15 text-clay-income">
<ShieldCheckIcon size={15} />
</div>
<div className="space-y-0.5">
<h5 className="font-baloo text-xs font-bold text-clay-text">
{t("ai.disclaimer.points.responsibility.title")}
</h5>
<p className="font-nunito text-xs text-clay-text-muted leading-relaxed">
{t("ai.disclaimer.points.responsibility.desc")}
</p>
</div>
</div>
</div>
</div>
</Modal>
);
};
...@@ -5,6 +5,7 @@ import { Card } from "@/components/ui/Card"; ...@@ -5,6 +5,7 @@ import { Card } from "@/components/ui/Card";
import { Select } from "@/components/ui/Select"; import { Select } from "@/components/ui/Select";
import { import {
AIAssistantIcon, AIAssistantIcon,
AlertTriangleIcon,
BudgetIcon, BudgetIcon,
ChevronRightIcon, ChevronRightIcon,
NotificationIcon, NotificationIcon,
...@@ -127,6 +128,14 @@ export const AIInsightsView: React.FC<AIInsightsViewProps> = ({ ...@@ -127,6 +128,14 @@ export const AIInsightsView: React.FC<AIInsightsViewProps> = ({
return ( return (
<div className="flex flex-col gap-4 pb-8"> <div className="flex flex-col gap-4 pb-8">
{/* AI Accuracy Disclaimer Notice */}
<div className="flex items-center gap-2 rounded-xl border border-clay-warning/30 bg-clay-warning/10 p-2.5 px-3 text-left">
<AlertTriangleIcon size={15} className="shrink-0 text-clay-warning" />
<p className="font-nunito text-[11px] text-clay-text-muted leading-tight">
{t("ai.insights.disclaimer")}
</p>
</div>
{/* Sub-tab switcher */} {/* Sub-tab switcher */}
<div <div
className="flex rounded-clay border border-clay-highlight/30 bg-clay-bg p-1 shadow-clay-pressed" className="flex rounded-clay border border-clay-highlight/30 bg-clay-bg p-1 shadow-clay-pressed"
......
import React, { useEffect, useRef, useState } from "react"; import React, { useEffect, useRef, useState } from "react";
import api from "zmp-sdk"; import api from "zmp-sdk";
import { Button } from "@/components/ui/Button"; import { Button } from "@/components/ui/Button";
import { AlertTriangleIcon } from "@/components/ui/icons";
import { useI18n } from "@/i18n"; import { useI18n } from "@/i18n";
import { useExtractReceipt } from "@/hooks/use-ai-assistant"; import { useExtractReceipt } from "@/hooks/use-ai-assistant";
import { formatBusinessDate } from "@/lib/business-time"; import { formatBusinessDate } from "@/lib/business-time";
...@@ -60,11 +61,30 @@ export const ReceiptScannerView: React.FC<ReceiptScannerViewProps> = ({ onTransa ...@@ -60,11 +61,30 @@ export const ReceiptScannerView: React.FC<ReceiptScannerViewProps> = ({ onTransa
// Auto trigger extraction // Auto trigger extraction
extractMutation.mutate( extractMutation.mutate(
{ file }, {
file,
hints: {
languageHint: intlLocale === "vi" ? "vi" : "en",
currencyHint: "VND",
},
},
{ {
onSuccess: (res) => { onSuccess: (res) => {
if (res.success && res.data) { if (res.success && res.data) {
setExtractedData(res.data); const raw = res.data as any;
const normalizedData: ExtractReceiptData = {
merchant: raw.merchant || raw.merchantName || null,
transactionDate: raw.transactionDate || null,
totalAmount: raw.totalAmount || null,
currency: raw.currency || "VND",
taxAmount: raw.taxAmount || null,
category: raw.category || null,
lineItems: Array.isArray(raw.lineItems) ? raw.lineItems : [],
rawText: raw.rawText || "",
confidence: typeof raw.confidence === "number" ? raw.confidence : 0.95,
warnings: Array.isArray(raw.warnings) ? raw.warnings : [],
};
setExtractedData(normalizedData);
} }
}, },
} }
...@@ -97,19 +117,22 @@ export const ReceiptScannerView: React.FC<ReceiptScannerViewProps> = ({ onTransa ...@@ -97,19 +117,22 @@ export const ReceiptScannerView: React.FC<ReceiptScannerViewProps> = ({ onTransa
const handleConfirmTransaction = () => { const handleConfirmTransaction = () => {
if (!extractedData) return; if (!extractedData) return;
const lineItemsCount = Array.isArray(extractedData.lineItems) ? extractedData.lineItems.length : 0;
const merchantName = extractedData.merchant || (extractedData as any).merchantName || "";
const prefillData = { const prefillData = {
amount: extractedData.totalAmount || "", amount: extractedData.totalAmount || "",
type: "EXPENSE" as const, type: "EXPENSE" as const,
categoryId: extractedData.category?.id || "", categoryId: extractedData.category?.id || "",
date: extractedData.transactionDate || "", date: extractedData.transactionDate || "",
description: extractedData.merchant description: merchantName
? `${extractedData.merchant}${ ? `${merchantName}${
extractedData.lineItems.length > 0 lineItemsCount > 0
? ` (${t("ai.ocr.itemCount", { count: extractedData.lineItems.length })})` ? ` (${t("ai.ocr.itemCount", { count: lineItemsCount })})`
: "" : ""
}` }`
: "", : "",
location: extractedData.merchant || "", location: merchantName,
}; };
onTransactionPrefill(prefillData, selectedFile); onTransactionPrefill(prefillData, selectedFile);
...@@ -124,10 +147,11 @@ export const ReceiptScannerView: React.FC<ReceiptScannerViewProps> = ({ onTransa ...@@ -124,10 +147,11 @@ export const ReceiptScannerView: React.FC<ReceiptScannerViewProps> = ({ onTransa
if (fileInputRef.current) fileInputRef.current.value = ""; if (fileInputRef.current) fileInputRef.current.value = "";
}; };
const confidence = typeof extractedData?.confidence === "number" ? extractedData.confidence : 0.95;
const confidenceClassName = extractedData const confidenceClassName = extractedData
? extractedData.confidence >= 0.8 ? confidence >= 0.8
? "border-clay-income/30 bg-clay-income/20 text-clay-income" ? "border-clay-income/30 bg-clay-income/20 text-clay-income"
: extractedData.confidence >= 0.5 : confidence >= 0.5
? "border-clay-warning/30 bg-clay-warning/20 text-clay-warning" ? "border-clay-warning/30 bg-clay-warning/20 text-clay-warning"
: "border-clay-expense/30 bg-clay-expense/20 text-clay-expense" : "border-clay-expense/30 bg-clay-expense/20 text-clay-expense"
: ""; : "";
...@@ -147,6 +171,39 @@ export const ReceiptScannerView: React.FC<ReceiptScannerViewProps> = ({ onTransa ...@@ -147,6 +171,39 @@ export const ReceiptScannerView: React.FC<ReceiptScannerViewProps> = ({ onTransa
}) })
: "—"; : "—";
const localizeWarning = (warn: string): string => {
const trimmed = warn.trim();
const standardEn = "please verify extracted values against the original receipt before creating a transaction.";
const standardVi = "vui lòng đối chiếu các giá trị đã trích xuất với hóa đơn gốc trước khi tạo giao dịch.";
const demoVi = "hóa đơn mẫu mô phỏng trong chế độ dùng thử (100% xử lý tại máy, không tốn token ai thật).";
const demoEn = "sample receipt simulated in demo mode (processed 100% locally, no real ai tokens consumed).";
const lower = trimmed.toLowerCase();
if (lower === standardEn || lower === standardVi) {
return t("ai.ocr.defaultWarning");
}
if (
lower === demoVi ||
lower === demoEn ||
lower.includes("hóa đơn mẫu mô phỏng trong chế độ dùng thử") ||
lower.includes("sample receipt simulated in demo mode")
) {
return t("ai.ocr.guestDemoWarning");
}
return trimmed;
};
const standardWarningTexts = [
"please verify extracted values against the original receipt before creating a transaction.",
"vui lòng đối chiếu các giá trị đã trích xuất với hóa đơn gốc trước khi tạo giao dịch.",
];
const specificWarnings = Array.isArray(extractedData?.warnings)
? extractedData.warnings.filter(
(w) => !standardWarningTexts.includes(w.trim().toLowerCase())
)
: [];
return ( return (
<div className="flex flex-col gap-5 px-2 pb-4 pt-2"> <div className="flex flex-col gap-5 px-2 pb-4 pt-2">
<section className="text-center"> <section className="text-center">
...@@ -262,7 +319,7 @@ export const ReceiptScannerView: React.FC<ReceiptScannerViewProps> = ({ onTransa ...@@ -262,7 +319,7 @@ export const ReceiptScannerView: React.FC<ReceiptScannerViewProps> = ({ onTransa
{t("ai.ocr.resultTitle")} {t("ai.ocr.resultTitle")}
</h4> </h4>
<span className={`shrink-0 rounded-full border px-3 py-1 font-baloo text-xs font-bold shadow-clay-pressed ${confidenceClassName}`}> <span className={`shrink-0 rounded-full border px-3 py-1 font-baloo text-xs font-bold shadow-clay-pressed ${confidenceClassName}`}>
{t("ai.ocr.confidence")}: {formatNumber(extractedData.confidence, { {t("ai.ocr.confidence")}: {formatNumber(confidence, {
style: "percent", style: "percent",
maximumFractionDigits: 0, maximumFractionDigits: 0,
})} })}
...@@ -275,7 +332,7 @@ export const ReceiptScannerView: React.FC<ReceiptScannerViewProps> = ({ onTransa ...@@ -275,7 +332,7 @@ export const ReceiptScannerView: React.FC<ReceiptScannerViewProps> = ({ onTransa
{t("ai.ocr.merchant")} {t("ai.ocr.merchant")}
</span> </span>
<span className="break-words font-nunito text-sm font-bold text-clay-text"> <span className="break-words font-nunito text-sm font-bold text-clay-text">
{extractedData.merchant || "—"} {extractedData.merchant || (extractedData as any).merchantName || "—"}
</span> </span>
</div> </div>
...@@ -317,7 +374,7 @@ export const ReceiptScannerView: React.FC<ReceiptScannerViewProps> = ({ onTransa ...@@ -317,7 +374,7 @@ export const ReceiptScannerView: React.FC<ReceiptScannerViewProps> = ({ onTransa
</div> </div>
{/* Line Items Table */} {/* Line Items Table */}
{extractedData.lineItems.length > 0 && ( {Array.isArray(extractedData.lineItems) && extractedData.lineItems.length > 0 && (
<div className="mt-2 border-t border-clay-highlight/40 pt-3"> <div className="mt-2 border-t border-clay-highlight/40 pt-3">
<h5 className="mb-2 font-nunito text-xs font-bold text-clay-text"> <h5 className="mb-2 font-nunito text-xs font-bold text-clay-text">
{t("ai.ocr.lineItems")} ({extractedData.lineItems.length}) {t("ai.ocr.lineItems")} ({extractedData.lineItems.length})
...@@ -366,17 +423,22 @@ export const ReceiptScannerView: React.FC<ReceiptScannerViewProps> = ({ onTransa ...@@ -366,17 +423,22 @@ export const ReceiptScannerView: React.FC<ReceiptScannerViewProps> = ({ onTransa
</details> </details>
)} )}
{/* Warnings */} {/* AI Accuracy Disclaimer & Warnings */}
{extractedData.warnings.length > 0 && ( <div className="mt-4 rounded-xl border border-clay-warning/30 bg-clay-warning/10 p-3 text-left">
<div className="mt-4 rounded-clay-sm bg-clay-warning/10 p-3 border border-clay-warning/30 text-xs font-medium text-clay-text space-y-1"> <div className="flex items-start gap-2.5">
<span className="block font-bold text-clay-warning"> <AlertTriangleIcon size={16} className="shrink-0 text-clay-warning mt-0.5" />
{t("ai.ocr.warnings")}: <p className="font-nunito text-xs text-clay-text-muted leading-relaxed">
</span> {t("ai.ocr.disclaimer")}
{extractedData.warnings.map((warn, i) => ( </p>
<p key={i}>• {warn}</p>
))}
</div> </div>
)} {specificWarnings.length > 0 && (
<div className="mt-2 pt-2 border-t border-clay-warning/20 font-nunito text-[11px] text-clay-text-muted space-y-1 pl-6">
{specificWarnings.map((warn, i) => (
<p key={i}>• {localizeWarning(warn)}</p>
))}
</div>
)}
</div>
{/* Confirm Create Transaction Action */} {/* Confirm Create Transaction Action */}
<div className="mt-5 flex flex-col items-center justify-between gap-4 border-t border-clay-highlight/40 px-2 pb-2 pt-4 sm:flex-row"> <div className="mt-5 flex flex-col items-center justify-between gap-4 border-t border-clay-highlight/40 px-2 pb-2 pt-4 sm:flex-row">
......
...@@ -4,19 +4,33 @@ import { Card } from "@/components/ui/Card"; ...@@ -4,19 +4,33 @@ import { Card } from "@/components/ui/Card";
import { import {
AIAssistantIcon, AIAssistantIcon,
IconGradients, IconGradients,
InfoIcon,
} from "@/components/ui/icons"; } from "@/components/ui/icons";
import { useI18n } from "@/i18n"; import { useI18n } from "@/i18n";
import { safeStorage } from "@/lib/storage";
import { FinWiseNavigationState, isFromReports } from "@/lib/navigation-state"; import { FinWiseNavigationState, isFromReports } from "@/lib/navigation-state";
import { AIChatView } from "./components/AIChatView"; import { AIChatView } from "./components/AIChatView";
import { AIInsightsView } from "./components/AIInsightsView"; import { AIInsightsView } from "./components/AIInsightsView";
import { AIDisclaimerModal } from "./components/AIDisclaimerModal";
type AITab = "chat" | "insights"; type AITab = "chat" | "insights";
const AI_DISCLAIMER_KEY = "finwise.aiDisclaimerAcknowledged";
const AIAssistantPage: React.FC = () => { const AIAssistantPage: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const { t } = useI18n(); const { t } = useI18n();
const [isDisclaimerOpen, setIsDisclaimerOpen] = useState<boolean>(() => {
return safeStorage.getItem(AI_DISCLAIMER_KEY) !== "true";
});
const handleCloseDisclaimer = () => {
safeStorage.setItem(AI_DISCLAIMER_KEY, "true");
setIsDisclaimerOpen(false);
};
const navigationState = location.state as FinWiseNavigationState | null; const navigationState = location.state as FinWiseNavigationState | null;
const initialTab: AITab = navigationState?.tab === "insights" ? "insights" : "chat"; const initialTab: AITab = navigationState?.tab === "insights" ? "insights" : "chat";
const [activeTab, setActiveTab] = useState<AITab>(initialTab); const [activeTab, setActiveTab] = useState<AITab>(initialTab);
...@@ -50,18 +64,29 @@ const AIAssistantPage: React.FC = () => { ...@@ -50,18 +64,29 @@ const AIAssistantPage: React.FC = () => {
<Card className="relative overflow-hidden !bg-clay-primary p-5 !text-clay-on-primary shadow-clay-raised"> <Card className="relative overflow-hidden !bg-clay-primary p-5 !text-clay-on-primary shadow-clay-raised">
<div className="absolute -right-8 -top-8 h-32 w-32 rounded-full bg-clay-highlight/20" /> <div className="absolute -right-8 -top-8 h-32 w-32 rounded-full bg-clay-highlight/20" />
<div className="absolute -bottom-10 left-1/2 h-28 w-28 rounded-full bg-clay-info/20" /> <div className="absolute -bottom-10 left-1/2 h-28 w-28 rounded-full bg-clay-info/20" />
<div className="relative flex items-center gap-4"> <div className="relative flex items-center justify-between gap-4">
<div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-clay bg-clay-surface text-clay-primary shadow-clay-raised"> <div className="flex items-center gap-4 min-w-0">
<AIAssistantIcon size={28} /> <div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-clay bg-clay-surface text-clay-primary shadow-clay-raised">
</div> <AIAssistantIcon size={28} />
<div> </div>
<h1 className="font-baloo text-xl font-bold leading-tight sm:text-2xl"> <div className="min-w-0">
{t("ai.header")} <h1 className="font-baloo text-xl font-bold leading-tight sm:text-2xl truncate">
</h1> {t("ai.header")}
<p className="mt-0.5 font-nunito text-xs text-clay-on-primary/80"> </h1>
{t("ai.subtitle")} <p className="mt-0.5 font-nunito text-xs text-clay-on-primary/80 line-clamp-1">
</p> {t("ai.subtitle")}
</p>
</div>
</div> </div>
<button
type="button"
onClick={() => setIsDisclaimerOpen(true)}
className="shrink-0 flex items-center gap-1.5 rounded-full bg-clay-surface/20 hover:bg-clay-surface/30 px-3 py-1.5 text-xs font-bold text-clay-on-primary backdrop-blur-sm transition-all focus:outline-none focus:ring-2 focus:ring-white/40 active:scale-95"
title={t("ai.disclaimer.title")}
>
<InfoIcon size={14} />
<span className="hidden sm:inline">{t("ai.disclaimer.button")}</span>
</button>
</div> </div>
</Card> </Card>
...@@ -104,7 +129,7 @@ const AIAssistantPage: React.FC = () => { ...@@ -104,7 +129,7 @@ const AIAssistantPage: React.FC = () => {
role="tabpanel" role="tabpanel"
aria-labelledby="ai-tab-chat" aria-labelledby="ai-tab-chat"
> >
<AIChatView /> <AIChatView onOpenDisclaimer={() => setIsDisclaimerOpen(true)} />
</div> </div>
)} )}
{visitedTabs.has("insights") && ( {visitedTabs.has("insights") && (
...@@ -122,6 +147,12 @@ const AIAssistantPage: React.FC = () => { ...@@ -122,6 +147,12 @@ const AIAssistantPage: React.FC = () => {
</div> </div>
)} )}
</main> </main>
{/* AI Accuracy Disclaimer Modal */}
<AIDisclaimerModal
isOpen={isDisclaimerOpen}
onClose={handleCloseDisclaimer}
/>
</Page> </Page>
); );
}; };
......
...@@ -214,11 +214,6 @@ function HomePage() { ...@@ -214,11 +214,6 @@ function HomePage() {
{t("home.wallets")} {t("home.wallets")}
</Button> </Button>
</PermissionGate> </PermissionGate>
<PermissionGate permission={PERMISSIONS.DEBT_READ}>
<Button variant="secondary" fullWidth onClick={() => navigate("/debts")} className="gap-2">
{t("home.debts")}
</Button>
</PermissionGate>
<PermissionGate permission={PERMISSIONS.TRANSACTION_READ}> <PermissionGate permission={PERMISSIONS.TRANSACTION_READ}>
<Button variant="secondary" fullWidth onClick={() => navigate("/transactions")}> <Button variant="secondary" fullWidth onClick={() => navigate("/transactions")}>
{t("home.transactions")} {t("home.transactions")}
...@@ -276,6 +271,11 @@ function HomePage() { ...@@ -276,6 +271,11 @@ function HomePage() {
{t("home.savingGoals")} {t("home.savingGoals")}
</Button> </Button>
</PermissionGate> </PermissionGate>
<PermissionGate permission={PERMISSIONS.DEBT_READ}>
<Button variant="secondary" fullWidth onClick={() => navigate("/debts")} className="gap-2">
{t("home.debts")}
</Button>
</PermissionGate>
<PermissionGate permission={PERMISSIONS.CATEGORY_READ}> <PermissionGate permission={PERMISSIONS.CATEGORY_READ}>
<Button variant="secondary" fullWidth onClick={() => navigate("/categories")}> <Button variant="secondary" fullWidth onClick={() => navigate("/categories")}>
{t("home.categories")} {t("home.categories")}
......
...@@ -215,9 +215,12 @@ export const AIInsightsPreview: React.FC<AIInsightsPreviewProps> = ({ context }) ...@@ -215,9 +215,12 @@ export const AIInsightsPreview: React.FC<AIInsightsPreviewProps> = ({ context })
)} )}
<div className="border-t border-clay-text/10 pt-1"> <div className="border-t border-clay-text/10 pt-1">
<p className="mb-3 text-center font-nunito text-[11px] font-semibold text-clay-text-muted"> <p className="mb-1 text-center font-nunito text-[11px] font-semibold text-clay-text-muted">
{t("ai.preview.scopeHint")} {t("ai.preview.scopeHint")}
</p> </p>
<p className="mb-3 text-center font-nunito text-[10px] text-clay-text-muted/80">
{t("ai.preview.disclaimer")}
</p>
<Button <Button
fullWidth fullWidth
className="gap-2 text-sm font-bold" className="gap-2 text-sm font-bold"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment