/* === Marsirius × KKTCELL — UI components (liquid glass) === */

const { useState, useEffect, useRef, useMemo, useCallback } = React;

/* ---------- helpers ---------- */
const fmt = {
  dur(s) {
    if (s == null || isNaN(s)) return "—";
    const t = Math.round(Number(s));
    const m = Math.floor(t / 60);
    const r = t % 60;
    return `${m}:${String(r).padStart(2, "0")}`;
  },
  pct(x, digits = 0) {
    if (x == null || isNaN(x)) return "—";
    return `${(Number(x) * 100).toFixed(digits)}%`;
  },
  num(x, digits = 0) {
    if (x == null || isNaN(x)) return "—";
    return Number(x).toLocaleString("tr-TR", {
      maximumFractionDigits: digits,
      minimumFractionDigits: digits
    });
  },
  time(s) {
    if (s == null || isNaN(s)) return "0:00";
    const t = Math.floor(Number(s));
    const m = Math.floor(t / 60);
    const r = t % 60;
    return `${m}:${String(r).padStart(2, "0")}`;
  },
  initials(s) {
    if (!s) return "—";
    const cleaned = String(s).replace(/[_\-]+/g, " ").trim();
    const parts = cleaned.split(/\s+/);
    if (parts.length === 1) return cleaned.slice(0, 2).toUpperCase();
    return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
  }
};

const SENTIMENT_TR = {
  positive: "Olumlu",
  negative: "Olumsuz",
  neutral: "Nötr",
  mixed: "Karışık"
};

const DAY_LABELS_TR = ["Pzr", "Pzt", "Sal", "Çar", "Per", "Cum", "Cmt"];

/* ---------- atoms ---------- */
function Pill({ tone = "slate", children, noDot = false, className = "" }) {
  return (
    <span className={`pill is-${tone} ${noDot ? "no-dot" : ""} ${className}`}>
      {children}
    </span>);

}

function MarsiriusMark({ size = 24, invert = false, className = "" }) {
  return (
    <img
      src="assets/marsirius-mark.png"
      width={size}
      height={size}
      alt="Marsirius"
      className={className}
      style={{ width: size, height: size, objectFit: "contain", filter: invert ? "invert(1) brightness(2.4)" : undefined }} />);


}

function MarsiriusWordmark({ height = 32, className = "" }) {
  return (
    <img
      src="assets/marsirius-full.png"
      alt="Marsirius AI Labs"
      className={className}
      style={{ ...{ height, objectFit: "contain", display: "block", width: "120px" }, width: "180px", height: "122px" }} />);


}

function KKTCellMark({ size = 22, className = "", rounded = true }) {
  return (
    <img
      src="assets/kktcell-mark.png"
      width={size}
      height={size}
      alt="Turkcell KKTC"
      className={className}
      style={{ width: size, height: size, objectFit: "contain", borderRadius: rounded ? 6 : 0 }} />);


}

function KKTCellWordmark({ height = 22, className = "" }) {
  return (
    <img
      src="assets/kktcell-full.png"
      alt="Turkcell Kuzey Kıbrıs"
      className={className}
      style={{ ...{ height, objectFit: "contain", display: "block", width: "6px" }, borderStyle: "none", width: "129px", height: "60px" }} />);


}

function Spinner({ size = 14 }) {
  return (
    <span
      className="spinner"
      style={{ width: size, height: size, borderWidth: Math.max(2, size / 8) }} />);


}

function CopyButton({ text, label = "Panoya kopyala" }) {
  const [ok, setOk] = useState(false);
  return (
    <button
      className="btn-ghost"
      onClick={async () => {
        try {
          await navigator.clipboard.writeText(text);
          setOk(true);
          setTimeout(() => setOk(false), 1500);
        } catch (e) {}
      }}>
      
      {ok ? "✓ Kopyalandı" : label}
    </button>);

}

/* ============================================
   VERTICAL BAR CHART (Activity)
   ============================================ */
function VBarChart({ data }) {
  // data: [{ label, value }]
  const max = Math.max(...data.map((d) => d.value), 1);
  const peakIdx = data.reduce((bi, d, i) => d.value > data[bi].value ? i : bi, 0);
  return (
    <div className="act-bars">
      {data.map((d, i) => {
        const h = d.value / max * 100;
        return (
          <div key={i} className={`act-bar ${i === peakIdx && d.value > 0 ? "is-peak" : ""}`}>
            <div className="act-bar-val">{d.label2 || (d.value ? Math.round(h) + "%" : "0%")}</div>
            <div className="act-bar-track">
              <div className="act-bar-fill" style={{ height: `${Math.max(4, h)}%` }} />
            </div>
            <div className="act-bar-label">{d.label}</div>
          </div>);

      })}
    </div>);

}

/* ============================================
   MINI STAT CARD
   ============================================ */
function MiniStat({ icon, iconTone = "violet", value, unit, label }) {
  return (
    <div className="mini-card">
      <div className={`mini-card-icon is-${iconTone}`}>{icon}</div>
      <div>
        <div className="mini-card-val">
          {value}{unit && <small>{unit}</small>}
        </div>
        <div className="mini-card-label">{label}</div>
      </div>
    </div>);

}

/* ============================================
   DONUT (Overview)
   ============================================ */
function Donut({ segments, centerValue, centerUnit, centerLabel, size = 132 }) {
  // segments: [{ key, label, value, color }]
  const R = 44;
  const C = 2 * Math.PI * R;
  const total = segments.reduce((s, x) => s + (x.value || 0), 0) || 1;
  let offset = 0;
  return (
    <div className="donut" style={{ width: size, height: size }}>
      <svg viewBox="0 0 100 100">
        <circle cx="50" cy="50" r={R} className="donut-track" strokeWidth="10" />
        {segments.map((s, i) => {
          const len = (s.value || 0) / total * C;
          const seg =
          <circle
            key={i}
            cx="50" cy="50" r={R}
            className="donut-fill"
            stroke={s.color}
            strokeWidth="10"
            strokeDasharray={`${len} ${C - len}`}
            strokeDashoffset={-offset}
            style={{ filter: `drop-shadow(0 0 6px ${s.color}66)` }} />;


          offset += len;
          return seg;
        })}
      </svg>
      <div className="donut-center">
        <div className="donut-num">{centerValue}<small>{centerUnit}</small></div>
        <div className="donut-of">{centerLabel}</div>
      </div>
    </div>);

}

/* ============================================
   CIRCULAR PROGRESS (challenge)
   ============================================ */
function CircleProgress({ on, size = 32 }) {
  const R = 13;
  const C = 2 * Math.PI * R;
  return (
    <div className={`chal-ring ${on ? "chal-ring-on" : "chal-ring-off"}`} style={{ width: size, height: size }}>
      <svg viewBox="0 0 32 32">
        <circle cx="16" cy="16" r={R} className="chal-ring-track" strokeWidth="3" />
        <circle
          cx="16" cy="16" r={R}
          className="chal-ring-fill"
          strokeWidth="3"
          strokeDasharray={C}
          strokeDashoffset={on ? 0 : C * 0.85} />
        
      </svg>
      <div className="chal-ring-center">{on ? "✓" : ""}</div>
    </div>);

}

/* ============================================
   AUDIO WAVE + TRANSCRIPT
   ============================================ */
function AudioWave({ playing, count = 36 }) {
  const bars = useMemo(() => {
    return Array.from({ length: count }).map((_, i) => {
      const h = 0.25 + Math.abs(Math.sin(i * 1.7) * 0.5 + Math.cos(i * 0.6) * 0.3) * 0.75;
      const d = i * 0.06 % 1.4;
      return { h: Math.min(1, h), d };
    });
  }, [count]);
  return (
    <div className="wave">
      {bars.map((b, i) =>
      <span
        key={i}
        className="wave-bar"
        style={{
          height: `${Math.round(b.h * 100)}%`,
          animationDelay: `${b.d}s`,
          animationPlayState: playing ? "running" : "paused",
          opacity: playing ? 1 : 0.45
        }} />

      )}
    </div>);

}

function Transcript({ view, audioRef, currentTime, onSeek, liveAudioUrl }) {
  const lines = view.transcript && view.transcript.lines || [];
  const [playing, setPlaying] = useState(false);
  const audioUrl =
    liveAudioUrl ||
    (view.audio_url ? window.MarsiriusAPI.apiUrl(view.audio_url) : null) ||
    (view.call_id && view.source === "demo_cohort" ? window.MarsiriusAPI.demoAudioUrl(view.call_id) : null);

  useEffect(() => {
    const a = audioRef && audioRef.current;
    if (!a) return;
    const on = () => setPlaying(true);
    const off = () => setPlaying(false);
    a.addEventListener("play", on);
    a.addEventListener("pause", off);
    a.addEventListener("ended", off);
    return () => {
      a.removeEventListener("play", on);
      a.removeEventListener("pause", off);
      a.removeEventListener("ended", off);
    };
  }, [audioRef, audioUrl]);

  return (
    <div className="transcript">
      {audioUrl &&
      <div className="audio-card">
          <div className="audio-card-icon">
            <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
              <polygon points="11 5 6 9 2 9 2 15 6 15 11 19" />
              <path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07" />
            </svg>
          </div>
          <div className="audio-card-body">
            <div className="audio-card-meta">
              <span>Çağrı kaydı</span>
              <span className="live-tag">{playing ? "Çalıyor" : "Hazır"}</span>
            </div>
            <AudioWave playing={playing} />
            <audio ref={audioRef} controls src={audioUrl} preload="metadata" />
          </div>
        </div>
      }
      <div className="chat">
        {lines.length === 0 &&
        <div className="empty">Bu çağrı için transcript satırı bulunamadı.</div>
        }
        {lines.map((l, i) => {
          const isAgent = l.role === "agent" || l.label_tr === "Temsilci";
          const isActive =
          currentTime != null &&
          l.start != null &&
          l.end != null &&
          currentTime >= l.start &&
          currentTime <= l.end;
          return (
            <div
              key={i}
              className={`bubble ${isAgent ? "is-agent" : "is-customer"} ${isActive ? "is-active" : ""}`}
              style={{ animationDelay: `${Math.min(i * 0.02, 0.4)}s` }}
              onClick={() => onSeek && l.start != null && onSeek(l.start)}>
              
              <div className="bubble-meta">
                <span className="bubble-role">{l.label_tr || (isAgent ? "Temsilci" : "Müşteri")}</span>
                {l.start != null && <span className="bubble-time">{fmt.time(l.start)}</span>}
              </div>
              <div className="bubble-text">{l.text}</div>
            </div>);

        })}
      </div>
    </div>);

}

/* ============================================
   METRICS / SIGNALS / CRM
   ============================================ */
function MetricsTab({ view }) {
  const m = view.metrics || {};
  const items = [
  ["Toplam süre", fmt.dur(m.duration_s)],
  ["Konuşma", fmt.dur(m.speech_s)],
  ["Sessizlik", fmt.pct(m.silence_ratio, 1)],
  ["Hız", m.speech_rate_wpm != null ? <>{Math.round(m.speech_rate_wpm)}<small>wpm</small></> : "—"],
  ["Konuşmacı", m.n_speakers != null ? `${m.n_speakers}` : "—"],
  ["Üst üste", m.overlap_detected ? "Var" : "Yok"],
  ["Uzun sessizlik", m.long_silence_count != null ? `${m.long_silence_count}` : "—"],
  ["Ses kalitesi", m.audio_quality || "—"],
  ["SNR", m.snr_db != null ? <>{m.snr_db.toFixed(1)}<small>dB</small></> : "—"],
  ["Temsilci s.", fmt.dur(m.agent_silence_s)],
  ["Müşteri s.", fmt.dur(m.customer_silence_s)]];

  const ps = m.prosody_per_speaker || {};
  const agentPs = ps.spk_agent || {};
  const custPs = ps.spk_customer || {};
  const prosodyItems = [];
  if (m.pitch_mean_hz != null) prosodyItems.push(["Pitch (ort.)", <>{m.pitch_mean_hz.toFixed(1)}<small>Hz</small></>]);
  if (m.pitch_range_hz != null) prosodyItems.push(["Pitch aralığı", <>{m.pitch_range_hz.toFixed(1)}<small>Hz</small></>]);
  if (m.intensity_db_mean != null) prosodyItems.push(["Yoğunluk (ort.)", <>{m.intensity_db_mean.toFixed(1)}<small>dB</small></>]);
  if (m.voiced_fraction != null) prosodyItems.push(["Sesli oran", fmt.pct(m.voiced_fraction, 0)]);
  if (agentPs.pitch_mean_hz != null) prosodyItems.push(["Temsilci pitch", <>{agentPs.pitch_mean_hz.toFixed(1)}<small>Hz</small></>]);
  if (custPs.pitch_mean_hz != null) prosodyItems.push(["Müşteri pitch", <>{custPs.pitch_mean_hz.toFixed(1)}<small>Hz</small></>]);

  const allItems = items.concat(prosodyItems);

  return (
    <div className="metrics-grid">
      {allItems.map(([k, v], i) =>
      <div key={i} className="metric">
          <div className="metric-key">{k}</div>
          <div className="metric-val">{v}</div>
        </div>
      )}
    </div>);

}

function SignalsTab({ view }) {
  const s = view.signals || {};
  const ex = view.executive || {};
  const items = [
  ["customer_dissatisfaction", "Müşteri memnuniyetsizliği", "neg"],
  ["churn_risk", "Churn riski", "neg"],
  ["sales_opportunity", "Satış fırsatı", "pos"]];

  // F16 retention card data — only if real data backs it
  const retentionAction =
    (ex.churn_risk && (ex.recommended_action || ex.action_required)) ||
    (ex.action_required === "retention_call" ? "Retention çağrısı önerildi" : null);

  return (
    <div className="signals">
      {items.map(([key, label, tone]) => {
        const sig = s[key] || {};
        const present = !!sig.present;
        return (
          <div key={key} className={`signal ${present ? `is-${tone}` : ""}`}>
            <div className="signal-head">
              <div className="signal-label">{label}</div>
              <Pill tone={present ? tone === "pos" ? "green" : "red" : "slate"}>
                {present ? "Tespit" : "Yok"}
                {sig.confidence != null && <span>· {Math.round(sig.confidence * 100)}%</span>}
              </Pill>
            </div>
            {sig.evidence && sig.evidence.length > 0 ?
            <ul className="signal-evidence">
                {sig.evidence.map((e, i) => <li key={i}>“{e}”</li>)}
              </ul> :

            <div className="signal-empty">Kanıt cümlesi yok.</div>
            }
          </div>);

      })}

      {retentionAction && (
        <div className="signal is-pos">
          <div className="signal-head">
            <div className="signal-label">Retention / paket önerisi</div>
            <Pill tone="violet">F16</Pill>
          </div>
          <ul className="signal-evidence">
            <li>“{typeof retentionAction === "string" ? retentionAction : (ex.recommended_action || ex.action_required)}”</li>
          </ul>
        </div>
      )}

      <div className="signal-footnote">
        Anahtar kelime motoru: sunucu tarafı YAML · <span className="mono">F17</span>
        {ex.alert_events && ex.alert_events.length > 0 && (
          <> · <b style={{ color: "var(--ink)" }}>{ex.alert_events.length}</b> tetiklenmiş kelime</>
        )}
      </div>
    </div>);

}

function CRMTab({ view }) {
  const crm = view.crm || {};
  const json = JSON.stringify(crm, null, 2);
  return (
    <div className="crm">
      <div className="crm-head">
        <div className="crm-title">CRM Payload</div>
        <CopyButton text={json} />
      </div>
      <pre className="crm-json">{json}</pre>
    </div>);

}

/* ============================================
   LIVE UPLOADER
   ============================================ */
function LiveUploader({ enabled, maxMb, onAnalyzed, onError }) {
  const [drag, setDrag] = useState(false);
  const [file, setFile] = useState(null);
  const [agentId, setAgentId] = useState("");
  const [crmId, setCrmId] = useState("");
  const [step, setStep] = useState(null);
  const [elapsed, setElapsed] = useState(0);
  const [err, setErr] = useState(null);
  const inputRef = useRef(null);
  const timerRef = useRef(null);

  const start = useCallback(async () => {
    if (!file) return;
    setErr(null);
    setStep("busy");
    setElapsed(0);
    timerRef.current = setInterval(() => setElapsed((e) => e + 1), 1000);
    try {
      const res = await window.MarsiriusAPI.analyzeAudio(file, {
        agent_id: agentId || undefined,
        crm_call_id: crmId || undefined
      });
      setStep("done");
      // Pass the original File so the parent can build a blob URL for playback.
      onAnalyzed && onAnalyzed(res, file);
    } catch (e) {
      setErr(e.message || String(e));
      onError && onError(e);
      setStep(null);
    } finally {
      clearInterval(timerRef.current);
    }
  }, [file, agentId, crmId, onAnalyzed, onError]);

  useEffect(() => () => clearInterval(timerRef.current), []);

  const onDrop = (e) => {
    e.preventDefault();
    setDrag(false);
    const f = e.dataTransfer.files && e.dataTransfer.files[0];
    if (f) setFile(f);
  };

  const busy = step && step !== "done";

  return (
    <div className="uploader">
      {!enabled &&
      <div className="uploader-disabled">
          Canlı analiz devre dışı. Demo sekmesini kullanabilirsiniz.
        </div>
      }
      <div
        className={`drop ${drag ? "is-drag" : ""} ${busy ? "is-busy" : ""}`}
        onDragOver={(e) => {e.preventDefault();setDrag(true);}}
        onDragLeave={() => setDrag(false)}
        onDrop={onDrop}
        onClick={() => !busy && inputRef.current && inputRef.current.click()}>
        
        <input
          ref={inputRef}
          type="file"
          accept=".wav,.mp3,.m4a,audio/*"
          hidden
          onChange={(e) => {
            const f = e.target.files && e.target.files[0];
            if (f) setFile(f);
          }} />
        
        <div className="drop-icon">
          <svg viewBox="0 0 24 24" width="32" height="32" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
            <path d="M12 3v12" />
            <path d="m7 8 5-5 5 5" />
            <path d="M4 14v5a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-5" />
          </svg>
        </div>
        <div className="drop-title">{file ? file.name : "Ses dosyasını sürükle"}</div>
        <div className="drop-sub">
          {file ? `${(file.size / 1024 / 1024).toFixed(2)} MB` : `wav · mp3 · m4a · maks ${maxMb || 25} MB`}
        </div>
        {file &&
        <div className="drop-file-meta">
            <button className="link" onClick={(e) => {e.stopPropagation();setFile(null);}}>
              Kaldır
            </button>
          </div>
        }
      </div>

      <div className="form-row">
        <label>
          <span>agent_id <em>opsiyonel</em></span>
          <input value={agentId} onChange={(e) => setAgentId(e.target.value)} placeholder="agent_042" disabled={busy} />
        </label>
        <label>
          <span>crm_call_id <em>opsiyonel</em></span>
          <input value={crmId} onChange={(e) => setCrmId(e.target.value)} placeholder="CRM-XYZ-1029" disabled={busy} />
        </label>
      </div>

      <button className="btn-primary" disabled={!file || busy || !enabled} onClick={start}>
        {busy ?
        <><Spinner size={14} /> Analiz ediliyor… {elapsed}s</> :

        <>
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
              <path d="M3 12h2l2-6 4 12 3-9 2 5h5" />
            </svg>
            Canlı analiz başlat
          </>
        }
      </button>

      {busy && (
        <div className="upload-hint">
          STT + LLM birlikte çalışıyor. Genellikle 30–180 sn sürer. Railway CPU'da yavaş olabilir.
        </div>
      )}

      {err && <div className="err">{err}</div>}
    </div>);

}

function Step({ active, done, label }) {
  return (
    <div className={`step ${active ? "is-active" : ""} ${done ? "is-done" : ""}`}>
      <div className="step-dot">{done ? "✓" : active ? <Spinner size={10} /> : ""}</div>
      <div>{label}</div>
    </div>);

}

/* ============================================
   24 FEATURES
   ============================================ */
const FEATURES = [
{ id: "F01", label: "Dil tespiti", pick: (v) => v.language },
{ id: "F02", label: "Konuşmacı sayısı", pick: (v) => v.metrics && v.metrics.n_speakers },
{ id: "F03", label: "Speech-to-Text", pick: (v) => v.transcript && v.transcript.full_text },
{ id: "F04", label: "Ses tonu / prosody", pick: (v) => v.metrics && (v.metrics.pitch_mean_hz != null || v.metrics.audio_quality) },
{ id: "F05", label: "Gürültü / SNR", pick: (v) => v.metrics && v.metrics.snr_db },
{ id: "F06", label: "Kıbrıs şivesi", pick: (v, ctx) => {
    const lang = (v.language || "") + "";
    const idHit = (v.call_id || "").includes("kibris");
    const accent = ctx && ctx.activeCatalog && ctx.activeCatalog.accent;
    return lang.includes("kibris") || idHit || accent === "kibris_turkce";
  } },
{ id: "F07", label: "Çağrı özeti", pick: (v) => v.executive && v.executive.summary },
{ id: "F08", label: "Duygu analizi", pick: (v) => v.executive && v.executive.sentiment && v.executive.sentiment.label },
{ id: "F09", label: "Sessizlik tespiti", pick: (v) => v.metrics && v.metrics.silence_ratio != null },
{ id: "F10", label: "Üst üste konuşma", pick: (v) => v.metrics && v.metrics.overlap_detected !== null && v.metrics.overlap_detected !== undefined },
{ id: "F11", label: "Temsilci sessizliği", pick: (v) => v.metrics && v.metrics.agent_silence_s != null },
{ id: "F12", label: "Konuşma hızı (wpm)", pick: (v) => v.metrics && v.metrics.speech_rate_wpm },
{ id: "F13", label: "Memnuniyetsizlik", pick: (v) => v.executive && v.executive.dissatisfaction != null },
{ id: "F14", label: "Memnuniyetsizlik sebebi", pick: (v) => v.executive && v.executive.dissatisfaction_reason },
{ id: "F15", label: "Satış fırsatı", pick: (v) => v.executive && v.executive.sales_opportunity != null },
{ id: "F16", label: "Retention / paket önerisi", pick: (v) => v.executive && v.executive.churn_risk != null && (v.executive.recommended_action || v.executive.action_required) },
{ id: "F17", label: "Anahtar kelime / sinyal", pick: (v) => v.signals },
{ id: "F18", label: "Alert / uyarılar", pick: (v) => v.executive && (v.executive.alerts && v.executive.alerts.length > 0 || v.executive.alert_events && v.executive.alert_events.length > 0) },
{ id: "F19", label: "Agent karşılaştırmalı rapor", pick: (v, ctx) => ctx && ctx.agentReport && (ctx.agentReport.n_agents >= 1 || (ctx.agentReport.agents && ctx.agentReport.agents.length >= 1)) },
{ id: "F20", label: "Kalite skoru", pick: (v) => v.executive && v.executive.quality_score != null },
{ id: "F21", label: "CRM payload", pick: (v) => v.crm && Object.keys(v.crm).length > 0 },
{ id: "F22", label: "Tüm çağrılar galerisi" },
{ id: "F23", label: "NRT / canlı analiz" },
{ id: "F24", label: "Hybrid mimari (onaylı)" }];


function computeFeatStates(view, meta, catalog, ctx) {
  const c = ctx || {};
  return FEATURES.map((f) => {
    let has = false;
    if (f.id === "F22") has = !!(catalog && catalog.length >= 1);else
    if (f.id === "F23") has = !!(meta && meta.live_analyze_enabled === true);else
    if (f.id === "F24") {
      has = !!(meta && (meta.deployment_model === "hybrid" || meta.stt_location && meta.llm_location));
    } else
    {
      try {
        const v = f.pick(view || {}, c);
        has = v !== undefined && v !== null && v !== "" && v !== false;
      } catch (e) {has = false;}
    }
    return { ...f, has };
  });
}

function FeatureMatrix({ view, meta, catalog, open, onClose, agentReport, activeCatalog }) {
  const states = computeFeatStates(view, meta, catalog, { agentReport, activeCatalog });
  const onCount = states.filter((s) => s.has).length;
  const pct = onCount / 24 * 100;
  return (
    <>
      <div className={`matrix-overlay ${open ? "is-open" : ""}`} onClick={onClose} />
      <aside className={`matrix-panel ${open ? "is-open" : ""}`}>
        <div className="modal-head">
          <div>
            <div className="modal-title">24 Özellik</div>
            <div className="modal-sub">{onCount}/24 · veri: canlı API</div>
          </div>
          <button className="modal-close" onClick={onClose} aria-label="Kapat">×</button>
        </div>
        <div className="matrix-stats">
          <div className="matrix-progress-num"><b>{onCount}</b> / 24</div>
          <div className="matrix-progress">
            <div className="matrix-progress-fill" style={{ width: `${pct}%` }} />
          </div>
        </div>
        <div className="feature-list">
          {states.map((f) =>
          <div key={f.id} className={`feat ${f.has ? "is-on" : ""}`}>
              <div className="feat-id">{f.id}</div>
              <div className="feat-label">{f.label}</div>
              <div className="feat-dot" />
            </div>
          )}
        </div>
      </aside>
    </>);

}

window.MarsiriusUI = {
  fmt, SENTIMENT_TR, DAY_LABELS_TR,
  Pill, MarsiriusMark, MarsiriusWordmark, KKTCellMark, KKTCellWordmark, Spinner, CopyButton,
  VBarChart, MiniStat, Donut, CircleProgress,
  Transcript, MetricsTab, SignalsTab, CRMTab,
  LiveUploader,
  FeatureMatrix, FEATURES, computeFeatStates
};