mirror of
https://github.com/escalante29/WealthySmart.git
synced 2026-05-19 09:28:47 +02:00
Add Asistente chat page with A2UI render tools
Wires CopilotKit v2 chat into the SPA as the Asistente page, declares a render_spending_summary action backed by a custom SpendingSummaryCard, and configures static suggestions shown before the first message. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
39
frontend/src/components/AgentHomeClient.tsx
Normal file
39
frontend/src/components/AgentHomeClient.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { CopilotChat } from "@copilotkit/react-ui";
|
||||||
|
import { Sparkles } from "lucide-react";
|
||||||
|
|
||||||
|
const SUGGESTIONS = [
|
||||||
|
{ title: "¿Cuál es mi saldo neto hoy?", message: "¿Cuál es mi saldo neto hoy?" },
|
||||||
|
{ title: "¿Cuánto gasté en el ciclo anterior?", message: "¿Cuánto gasté en el ciclo anterior?" },
|
||||||
|
{ title: "Últimas 10 transacciones", message: "Muéstrame mis últimas 10 transacciones" },
|
||||||
|
{ title: "¿Cómo va mi pensión?", message: "¿Cómo va mi pensión?" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function AgentHomeClient() {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col h-[calc(100vh-105px)]">
|
||||||
|
<div className="mb-4">
|
||||||
|
<h1 className="text-2xl font-bold tracking-tight font-heading flex items-center gap-2">
|
||||||
|
<Sparkles className="w-5 h-5 text-primary" />
|
||||||
|
Asistente
|
||||||
|
</h1>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Pregúntale a WealthySmart sobre tus finanzas.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 min-h-0 rounded-xl border border-border overflow-hidden bg-card">
|
||||||
|
<CopilotChat
|
||||||
|
className="h-full"
|
||||||
|
labels={{
|
||||||
|
title: "WealthySmart",
|
||||||
|
initial: "¿Qué quieres saber sobre tus finanzas?",
|
||||||
|
placeholder: "Escribe tu pregunta…",
|
||||||
|
}}
|
||||||
|
suggestions={SUGGESTIONS}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
456
frontend/src/components/chat/ChatCards.tsx
Normal file
456
frontend/src/components/chat/ChatCards.tsx
Normal file
@@ -0,0 +1,456 @@
|
|||||||
|
import { TrendingDown, TrendingUp, Wallet, ArrowRightLeft } from "lucide-react";
|
||||||
|
|
||||||
|
// ── helpers ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function fmtCRC(n: number | undefined | null) {
|
||||||
|
if (n == null) return "₡0";
|
||||||
|
return `₡${Math.round(n).toLocaleString("es-CR")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmtCurrency(amount: number | undefined | null, currency: string | undefined | null) {
|
||||||
|
if (amount == null) return "₡0";
|
||||||
|
if (currency === "USD") return `$${amount.toFixed(2)}`;
|
||||||
|
if (currency === "EUR") return `€${amount.toFixed(2)}`;
|
||||||
|
return fmtCRC(amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sourceLabel(source: string | undefined | null) {
|
||||||
|
if (!source) return "Otro";
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
CREDIT_CARD: "Tarjeta",
|
||||||
|
CASH: "Efectivo",
|
||||||
|
TRANSFER: "Transferencia",
|
||||||
|
SINPE: "SINPE",
|
||||||
|
OTHER: "Otro",
|
||||||
|
};
|
||||||
|
return map[source] ?? source.replace(/_/g, " ");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Spinner ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function Spinner() {
|
||||||
|
return (
|
||||||
|
<div className="w-3 h-3 border-2 border-primary border-t-transparent rounded-full animate-spin inline-block" />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── SpendingSummaryCard ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface SpendingBySource {
|
||||||
|
source: string;
|
||||||
|
total_crc: number;
|
||||||
|
count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SpendingByCategory {
|
||||||
|
category: string;
|
||||||
|
amount_crc: number;
|
||||||
|
count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SpendingSummaryArgs {
|
||||||
|
title?: string;
|
||||||
|
period?: string;
|
||||||
|
total_crc?: number;
|
||||||
|
by_source?: SpendingBySource[];
|
||||||
|
by_category?: SpendingByCategory[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SpendingSummaryCard({
|
||||||
|
args,
|
||||||
|
status,
|
||||||
|
}: {
|
||||||
|
args: SpendingSummaryArgs;
|
||||||
|
status: string;
|
||||||
|
}) {
|
||||||
|
const { title, period, total_crc, by_source = [], by_category = [] } = args;
|
||||||
|
const max = Math.max(...by_category.map((c) => c.amount_crc), 1);
|
||||||
|
|
||||||
|
const PALETTE = [
|
||||||
|
"bg-primary",
|
||||||
|
"bg-chart-1",
|
||||||
|
"bg-chart-2",
|
||||||
|
"bg-chart-3",
|
||||||
|
"bg-chart-4",
|
||||||
|
"bg-chart-5",
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rounded-xl border border-border bg-card text-card-foreground p-4 space-y-4 my-2 shadow-sm">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-start justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<p className="text-[11px] font-medium uppercase tracking-widest text-muted-foreground">
|
||||||
|
{title ?? "Resumen de gastos"}
|
||||||
|
</p>
|
||||||
|
{period && (
|
||||||
|
<p className="text-xs text-muted-foreground mt-0.5">{period}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="text-right shrink-0">
|
||||||
|
<p className="text-2xl font-bold tabular-nums">{fmtCRC(total_crc)}</p>
|
||||||
|
<p className="text-[11px] text-muted-foreground">total gastado</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* By source */}
|
||||||
|
{by_source.length > 0 && (
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
{by_source.filter((s) => s?.source != null).map((s) => (
|
||||||
|
<div
|
||||||
|
key={s.source}
|
||||||
|
className="rounded-lg bg-secondary/40 border border-border/50 px-3 py-2"
|
||||||
|
>
|
||||||
|
<p className="text-[11px] text-muted-foreground">
|
||||||
|
{sourceLabel(s.source)}
|
||||||
|
</p>
|
||||||
|
<p className="font-semibold text-sm tabular-nums">
|
||||||
|
{fmtCRC(s.total_crc)}
|
||||||
|
</p>
|
||||||
|
<p className="text-[11px] text-muted-foreground">
|
||||||
|
{s.count} mov.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* By category */}
|
||||||
|
{by_category.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<p className="text-[11px] font-medium uppercase tracking-widest text-muted-foreground mb-2">
|
||||||
|
Por categoría
|
||||||
|
</p>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{by_category.filter((c) => c?.category != null).slice(0, 7).map((c, i) => (
|
||||||
|
<div key={c.category}>
|
||||||
|
<div className="flex justify-between text-xs mb-1">
|
||||||
|
<span className="text-card-foreground">{c.category}</span>
|
||||||
|
<span className="text-muted-foreground tabular-nums">
|
||||||
|
{fmtCRC(c.amount_crc)}
|
||||||
|
<span className="text-[10px] ml-1">({c.count})</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="h-1.5 bg-secondary rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
className={`h-full rounded-full transition-all ${PALETTE[i % PALETTE.length]}`}
|
||||||
|
style={{
|
||||||
|
width: `${Math.round(((c.amount_crc ?? 0) / max) * 100)}%`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{status === "inProgress" && (
|
||||||
|
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||||
|
<Spinner /> Obteniendo datos…
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── TransactionListCard ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface TransactionRow {
|
||||||
|
date: string;
|
||||||
|
merchant: string;
|
||||||
|
amount: number;
|
||||||
|
currency: string;
|
||||||
|
category: string | null;
|
||||||
|
source: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TransactionListArgs {
|
||||||
|
title?: string;
|
||||||
|
transactions?: TransactionRow[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const SOURCE_ICON: Record<string, React.ReactNode> = {
|
||||||
|
CREDIT_CARD: <Wallet className="w-3 h-3" />,
|
||||||
|
TRANSFER: <ArrowRightLeft className="w-3 h-3" />,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function TransactionListCard({
|
||||||
|
args,
|
||||||
|
status,
|
||||||
|
}: {
|
||||||
|
args: TransactionListArgs;
|
||||||
|
status: string;
|
||||||
|
}) {
|
||||||
|
const { title, transactions = [] } = args;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rounded-xl border border-border bg-card text-card-foreground p-4 my-2 shadow-sm">
|
||||||
|
<p className="text-[11px] font-medium uppercase tracking-widest text-muted-foreground mb-3">
|
||||||
|
{title ?? "Transacciones"}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{transactions.length === 0 && status !== "inProgress" && (
|
||||||
|
<p className="text-sm text-muted-foreground">Sin transacciones.</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="divide-y divide-border/50">
|
||||||
|
{transactions.map((t, i) => (
|
||||||
|
<div key={i} className="flex items-center justify-between py-2 gap-3">
|
||||||
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
|
<div className="shrink-0 w-6 h-6 rounded-full bg-secondary flex items-center justify-center text-muted-foreground">
|
||||||
|
{SOURCE_ICON[t.source] ?? <Wallet className="w-3 h-3" />}
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<p className="text-sm font-medium truncate">{t.merchant}</p>
|
||||||
|
<p className="text-[11px] text-muted-foreground">
|
||||||
|
{t.date}
|
||||||
|
{t.category && ` · ${t.category}`}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm font-semibold tabular-nums shrink-0 text-destructive">
|
||||||
|
{fmtCurrency(t.amount, t.currency)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{status === "inProgress" && (
|
||||||
|
<div className="flex items-center gap-2 text-xs text-muted-foreground mt-2">
|
||||||
|
<Spinner /> Cargando…
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── NetWorthCard ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface AccountRow {
|
||||||
|
bank: string;
|
||||||
|
label: string;
|
||||||
|
balance_crc: number;
|
||||||
|
account_type: string;
|
||||||
|
currency: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NetWorthArgs {
|
||||||
|
total_assets_crc?: number;
|
||||||
|
total_liabilities_crc?: number;
|
||||||
|
net_worth_crc?: number;
|
||||||
|
accounts?: AccountRow[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NetWorthCard({
|
||||||
|
args,
|
||||||
|
status,
|
||||||
|
}: {
|
||||||
|
args: NetWorthArgs;
|
||||||
|
status: string;
|
||||||
|
}) {
|
||||||
|
const {
|
||||||
|
total_assets_crc = 0,
|
||||||
|
total_liabilities_crc = 0,
|
||||||
|
net_worth_crc = 0,
|
||||||
|
accounts = [],
|
||||||
|
} = args;
|
||||||
|
|
||||||
|
const isPositive = net_worth_crc >= 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rounded-xl border border-border bg-card text-card-foreground p-4 space-y-4 my-2 shadow-sm">
|
||||||
|
{/* Net worth headline */}
|
||||||
|
<div className="flex items-start justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<p className="text-[11px] font-medium uppercase tracking-widest text-muted-foreground">
|
||||||
|
Patrimonio neto
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-1 mt-0.5">
|
||||||
|
{isPositive ? (
|
||||||
|
<TrendingUp className="w-4 h-4 text-green-500" />
|
||||||
|
) : (
|
||||||
|
<TrendingDown className="w-4 h-4 text-destructive" />
|
||||||
|
)}
|
||||||
|
<span
|
||||||
|
className={`text-2xl font-bold tabular-nums ${isPositive ? "text-green-500" : "text-destructive"}`}
|
||||||
|
>
|
||||||
|
{fmtCRC(net_worth_crc)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-right text-xs space-y-1">
|
||||||
|
<p>
|
||||||
|
<span className="text-muted-foreground">Activos </span>
|
||||||
|
<span className="font-semibold text-green-500">
|
||||||
|
{fmtCRC(total_assets_crc)}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<span className="text-muted-foreground">Pasivos </span>
|
||||||
|
<span className="font-semibold text-destructive">
|
||||||
|
{fmtCRC(total_liabilities_crc)}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Asset bar */}
|
||||||
|
{total_assets_crc + total_liabilities_crc > 0 && (
|
||||||
|
<div className="h-2 bg-secondary rounded-full overflow-hidden flex">
|
||||||
|
<div
|
||||||
|
className="h-full bg-green-500 transition-all"
|
||||||
|
style={{
|
||||||
|
width: `${Math.round(
|
||||||
|
(total_assets_crc /
|
||||||
|
(total_assets_crc + total_liabilities_crc)) *
|
||||||
|
100,
|
||||||
|
)}%`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="h-full bg-destructive flex-1" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Accounts */}
|
||||||
|
{accounts.length > 0 && (
|
||||||
|
<div className="divide-y divide-border/50">
|
||||||
|
{accounts.map((a, i) => (
|
||||||
|
<div key={i} className="flex items-center justify-between py-2">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium">{a.label || a.bank}</p>
|
||||||
|
<p className="text-[11px] text-muted-foreground">
|
||||||
|
{a.bank} · {a.account_type} · {a.currency}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
className={`text-sm font-semibold tabular-nums ${a.balance_crc >= 0 ? "text-green-500" : "text-destructive"}`}
|
||||||
|
>
|
||||||
|
{fmtCRC(a.balance_crc)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{status === "inProgress" && (
|
||||||
|
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||||
|
<Spinner /> Calculando…
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── BudgetMonthCard ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const MONTH_NAMES = [
|
||||||
|
"", "Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio",
|
||||||
|
"Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre",
|
||||||
|
];
|
||||||
|
|
||||||
|
export interface BudgetMonthArgs {
|
||||||
|
year?: number;
|
||||||
|
month?: number;
|
||||||
|
projected_income_crc?: number;
|
||||||
|
projected_expenses_crc?: number;
|
||||||
|
actual_total_crc?: number;
|
||||||
|
net_balance_crc?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BudgetMonthCard({
|
||||||
|
args,
|
||||||
|
status,
|
||||||
|
}: {
|
||||||
|
args: BudgetMonthArgs;
|
||||||
|
status: string;
|
||||||
|
}) {
|
||||||
|
const {
|
||||||
|
year,
|
||||||
|
month,
|
||||||
|
projected_income_crc = 0,
|
||||||
|
projected_expenses_crc = 0,
|
||||||
|
actual_total_crc = 0,
|
||||||
|
net_balance_crc = 0,
|
||||||
|
} = args;
|
||||||
|
|
||||||
|
const usedPct =
|
||||||
|
projected_expenses_crc > 0
|
||||||
|
? Math.min(Math.round((actual_total_crc / projected_expenses_crc) * 100), 100)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
const isOver = actual_total_crc > projected_expenses_crc;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rounded-xl border border-border bg-card text-card-foreground p-4 space-y-4 my-2 shadow-sm">
|
||||||
|
<div className="flex items-start justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<p className="text-[11px] font-medium uppercase tracking-widest text-muted-foreground">
|
||||||
|
Presupuesto
|
||||||
|
</p>
|
||||||
|
{month != null && year != null && (
|
||||||
|
<p className="text-sm font-semibold mt-0.5">
|
||||||
|
{MONTH_NAMES[month]} {year}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`text-right text-2xl font-bold tabular-nums ${net_balance_crc >= 0 ? "text-green-500" : "text-destructive"}`}
|
||||||
|
>
|
||||||
|
{fmtCRC(net_balance_crc)}
|
||||||
|
<p className="text-[11px] font-normal text-muted-foreground">
|
||||||
|
balance neto
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Rows */}
|
||||||
|
<div className="space-y-1.5 text-sm">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-muted-foreground">Ingresos proyectados</span>
|
||||||
|
<span className="font-medium tabular-nums text-green-500">
|
||||||
|
{fmtCRC(projected_income_crc)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-muted-foreground">Gastos proyectados</span>
|
||||||
|
<span className="font-medium tabular-nums">
|
||||||
|
{fmtCRC(projected_expenses_crc)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between border-t border-border/50 pt-1.5">
|
||||||
|
<span className="text-muted-foreground">Gastado real</span>
|
||||||
|
<span
|
||||||
|
className={`font-semibold tabular-nums ${isOver ? "text-destructive" : ""}`}
|
||||||
|
>
|
||||||
|
{fmtCRC(actual_total_crc)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Progress bar */}
|
||||||
|
{projected_expenses_crc > 0 && (
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-between text-[11px] text-muted-foreground mb-1">
|
||||||
|
<span>Ejecución presupuestaria</span>
|
||||||
|
<span className={isOver ? "text-destructive font-semibold" : ""}>
|
||||||
|
{usedPct}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="h-2 bg-secondary rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
className={`h-full rounded-full transition-all ${isOver ? "bg-destructive" : "bg-primary"}`}
|
||||||
|
style={{ width: `${usedPct}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{status === "inProgress" && (
|
||||||
|
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||||
|
<Spinner /> Cargando…
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
80
frontend/src/pages/Asistente.tsx
Normal file
80
frontend/src/pages/Asistente.tsx
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import { CopilotChat, useConfigureSuggestions } from "@copilotkit/react-core/v2";
|
||||||
|
import { useCopilotAction } from "@copilotkit/react-core";
|
||||||
|
import { Sparkles } from "lucide-react";
|
||||||
|
import { SpendingSummaryCard, type SpendingSummaryArgs } from "@/components/chat/ChatCards";
|
||||||
|
|
||||||
|
const STATIC_SUGGESTIONS = {
|
||||||
|
available: "before-first-message" as const,
|
||||||
|
suggestions: [
|
||||||
|
{ title: "¿Cuál es mi saldo neto hoy?", message: "¿Cuál es mi saldo neto hoy?" },
|
||||||
|
{ title: "¿Cuánto gasté en el ciclo anterior?", message: "¿Cuánto gasté en el ciclo anterior?" },
|
||||||
|
{ title: "Últimas 10 transacciones", message: "Muéstrame mis últimas 10 transacciones" },
|
||||||
|
{ title: "¿Cómo va mi pensión?", message: "¿Cómo va mi pensión?" },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Asistente() {
|
||||||
|
useConfigureSuggestions(STATIC_SUGGESTIONS);
|
||||||
|
|
||||||
|
useCopilotAction({
|
||||||
|
name: "render_spending_summary",
|
||||||
|
description:
|
||||||
|
"Render a visual spending summary card with source breakdown and category progress bars. " +
|
||||||
|
"Call this for any cycle summary, spending totals, or category breakdown.",
|
||||||
|
parameters: [
|
||||||
|
{ name: "title", type: "string", description: "Card title (e.g. 'Ciclo actual')" },
|
||||||
|
{ name: "period", type: "string", description: "Human-readable period (e.g. '18 mar → 18 abr 2026')" },
|
||||||
|
{ name: "total_crc", type: "number", description: "Total spend in CRC" },
|
||||||
|
{
|
||||||
|
name: "by_source",
|
||||||
|
type: "object[]",
|
||||||
|
description: "Breakdown by payment source",
|
||||||
|
attributes: [
|
||||||
|
{ name: "source", type: "string" },
|
||||||
|
{ name: "total_crc", type: "number" },
|
||||||
|
{ name: "count", type: "number" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "by_category",
|
||||||
|
type: "object[]",
|
||||||
|
required: false,
|
||||||
|
description: "Top spending categories with CRC amounts",
|
||||||
|
attributes: [
|
||||||
|
{ name: "category", type: "string" },
|
||||||
|
{ name: "amount_crc", type: "number" },
|
||||||
|
{ name: "count", type: "number" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
handler: async () => "ok",
|
||||||
|
render: (props) => (
|
||||||
|
<SpendingSummaryCard args={props.args as SpendingSummaryArgs} status={props.status} />
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col h-[calc(100vh-105px)]">
|
||||||
|
<div className="mb-4">
|
||||||
|
<h1 className="text-2xl font-bold tracking-tight flex items-center gap-2" style={{ fontFamily: "var(--font-heading)" }}>
|
||||||
|
<Sparkles className="w-5 h-5 text-primary" />
|
||||||
|
Asistente
|
||||||
|
</h1>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Pregúntale a WealthySmart sobre tus finanzas.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 min-h-0 rounded-xl border border-border overflow-hidden bg-card">
|
||||||
|
<CopilotChat
|
||||||
|
className="h-full"
|
||||||
|
labels={{
|
||||||
|
modalHeaderTitle: "WealthySmart",
|
||||||
|
welcomeMessageText: "¿Qué quieres saber sobre tus finanzas?",
|
||||||
|
chatInputPlaceholder: "Escribe tu pregunta…",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user