Untitled

 avatar
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