Untitled
unknown
plain_text
a year ago
7.6 kB
9
Indexable
import React, { useMemo, useState } from "react";
// Self-contained page: includes AlertCenter + sample RULES_CONFIG so it builds without any external imports
type Stage = "pre" | "in" | "post" | "daily";
type Severity = "info" | "watch" | "alert" | "critical";
type Rule = {
id: string;
stage: Stage;
source: string; // tbi | batch | realtime
metric: string;
operator: "==" | "!=" | ">" | ">=" | "<" | "<=";
value: number | boolean | string;
severity: Severity;
personalize_by?: string[];
message_en: string;
message_vi: string;
action?: string;
};
// --- SAMPLE RULES (you can replace with full RULES_CONFIG later) ---
const RULES_CONFIG: Rule[] = [
{ id: "pre_checklist_missing", stage: "pre", source: "tbi|realtime", metric: "pre_checklist_done", operator: "==", value: false, severity: "alert", personalize_by: ["CI"], message_en: "Checklist not completed. Please finish before opening a trade.", message_vi: "Checklist chưa hoàn tất. Hoàn tất trước khi mở lệnh.", action: "block_open" },
{ id: "pre_risk_cap", stage: "pre", source: "tbi|realtime", metric: "risk_pct", operator: ">", value: 2, severity: "alert", personalize_by: ["RPI"], message_en: "Risk per trade exceeds your cap. Resize to stay sustainable.", message_vi: "Risk/trade vượt mức cá nhân. Giảm khối lượng để bền vững.", action: "suggest_resize" },
{ id: "in_move_sl_wider", stage: "in", source: "realtime", metric: "sl_distance_increase_pct", operator: ">", value: 10, severity: "alert", personalize_by: ["CI"], message_en: "You widened the stop. Stay disciplined.", message_vi: "Bạn vừa nới SL. Giữ kỷ luật.", action: "confirm_or_revert_sl" },
{ id: "post_overtrade", stage: "post", source: "batch|realtime", metric: "trades_today", operator: ">", value: 10, severity: "alert", personalize_by: ["CI"], message_en: "You exceeded your daily trade count.", message_vi: "Bạn vượt số lệnh kế hoạch trong ngày.", action: "pause_and_review" },
{ id: "daily_no_journal_7d", stage: "daily", source: "batch|realtime", metric: "no_journal_7d", operator: "==", value: true, severity: "alert", personalize_by: ["CI"], message_en: "No journal in 7 days. Restart the habit.", message_vi: "7 ngày không journal. Bắt đầu lại thói quen nhé.", action: "restart_journal" }
];
function borderBySeverity(s: Severity) {
switch (s) {
case "info": return "border-sky-200";
case "watch": return "border-amber-200";
case "alert": return "border-orange-300";
case "critical": return "border-red-300";
}
}
function Badge({ severity }: { severity: Severity }) {
const map = {
info: "bg-sky-100 text-sky-700 border-sky-200",
watch: "bg-amber-100 text-amber-700 border-amber-200",
alert: "bg-orange-100 text-orange-700 border-orange-200",
critical: "bg-red-100 text-red-700 border-red-200",
} as const;
return <span className={`inline-block rounded-md px-2 py-1 text-xs font-semibold border ${map[severity]}`}>{severity.toUpperCase()}</span>;
}
function AlertCenter({ locale = "vi", rules = RULES_CONFIG }: { locale?: "vi" | "en"; rules?: Rule[] }) {
const [stage, setStage] = useState<Stage | "all">("all");
const [severity, setSeverity] = useState<Severity | "all">("all");
const [query, setQuery] = useState("");
const [resolved, setResolved] = useState<Record<string, boolean>>({});
const filtered = useMemo(() => {
return rules
.filter((r) => (stage === "all" || r.stage === stage) && (severity === "all" || r.severity === severity))
.filter((r) => {
const txt = (locale === "vi" ? r.message_vi : r.message_en).toLowerCase();
return !query || txt.includes(query.toLowerCase());
});
}, [rules, stage, severity, query, locale]);
const counts = useMemo(() => {
const c = { all: 0, pre: 0, in: 0, post: 0, daily: 0 } as Record<Stage | "all", number>;
rules.forEach((r) => { c.all++; c[r.stage]++; });
return c;
}, [rules]);
return (
<div className="w-full max-w-5xl mx-auto p-4">
<h2 className="text-2xl font-bold mb-3">Behavioral Alert Center</h2>
<div className="flex flex-wrap items-center gap-2 mb-4">
{(["all","pre","in","post","daily"] as const).map((s) => (
<button key={s} onClick={() => setStage(s as any)} className={`px-3 py-1.5 rounded-md text-sm border ${stage===s?"bg-amber-500 text-white border-amber-500":"bg-white text-gray-700 border-gray-200 hover:bg-gray-50"}`}>
{s.toString().toUpperCase()} <span className="ml-1 text-xs">({counts[s]})</span>
</button>
))}
<select value={severity} onChange={(e)=>setSeverity(e.target.value as any)} className="ml-auto rounded-md border border-gray-300 px-3 py-1.5 text-sm">
<option value="all">All Severities</option>
<option value="info">Info</option>
<option value="watch">Watch</option>
<option value="alert">Alert</option>
<option value="critical">Critical</option>
</select>
<input value={query} onChange={(e)=>setQuery(e.target.value)} placeholder={locale==="vi"?"Tìm cảnh báo…":"Search alerts…"} className="rounded-md border border-gray-300 px-3 py-1.5 text-sm w-56" />
</div>
<div className="space-y-3">
{filtered.map((r) => (
<div key={r.id} className={`rounded-xl border p-4 flex items-start gap-3 ${borderBySeverity(r.severity)}`}>
<Badge severity={r.severity} />
<div className="flex-1">
<div className="flex items-center gap-2">
<span className="text-xs uppercase tracking-wide text-gray-500">{r.stage}</span>
<span className="text-xs text-gray-400">• {r.source}</span>
{r.personalize_by && r.personalize_by.length>0 && (
<span className="text-xs text-gray-400">• TBI: {r.personalize_by.join(", ")}</span>
)}
</div>
<div className="mt-1 text-gray-900">{locale === "vi" ? r.message_vi : r.message_en}</div>
{r.action && (
<div className="mt-2">
<button onClick={()=>setResolved((x)=>({ ...x, [r.id]: true }))} className="text-sm px-3 py-1.5 rounded-md bg-indigo-600 text-white hover:bg-indigo-700">
{locale==="vi"?"Thực hiện hành động":"Resolve / Take action"}
</button>
</div>
)}
</div>
<div className="pt-1">
{resolved[r.id] ? (
<span className="text-xs text-emerald-600">{locale==="vi"?"Đã xử lý":"Resolved"}</span>
) : (
<button onClick={()=>setResolved((x)=>({ ...x, [r.id]: true }))} className="text-xs text-gray-500 hover:text-gray-700">{locale==="vi"?"Đánh dấu đã xử lý":"Mark resolved"}</button>
)}
</div>
</div>
))}
{filtered.length===0 && (
<div className="rounded-lg border border-dashed p-8 text-center text-gray-500">{locale==="vi"?"Không có cảnh báo phù hợp bộ lọc":"No alerts match current filters"}</div>
)}
</div>
</div>
);
}
export default function Dashboard() {
return (
<div className="p-6">
<h1 className="text-2xl font-bold mb-4">Bảng điều khiển</h1>
<AlertCenter locale="vi" />
</div>
);
}
Editor is loading...
Leave a Comment