/* ===========================================================================
   components.jsx — presentational pieces: icons, sidebar, message list,
   tool-activity line, settings modal, lightbox, toast. Exposed on window.
   =========================================================================== */
(function () {
  const { useState, useEffect, useRef, useCallback } = React;

  /* ----------------------------- icons ------------------------------ */
  const I = (p) => <svg width={p.s || 18} height={p.s || 18} viewBox="0 0 24 24" fill="none"
    stroke="currentColor" strokeWidth={p.w || 2} strokeLinecap="round" strokeLinejoin="round">{p.children}</svg>;
  const Icons = {
    Plus: (p) => <I {...p}><path d="M12 5v14M5 12h14" /></I>,
    Menu: (p) => <I {...p}><path d="M3 6h18M3 12h18M3 18h18" /></I>,
    Send: (p) => <I {...p}><path d="M12 19V5M5 12l7-7 7 7" /></I>,
    Stop: (p) => <I {...p}><rect x="6" y="6" width="12" height="12" rx="2" fill="currentColor" stroke="none" /></I>,
    Image: (p) => <I {...p}><rect x="3" y="3" width="18" height="18" rx="3" /><circle cx="8.5" cy="8.5" r="1.6" /><path d="M21 15l-5-5L5 21" /></I>,
    Mic: (p) => <I {...p}><rect x="9" y="2" width="6" height="12" rx="3" /><path d="M5 10a7 7 0 0 0 14 0M12 19v3" /></I>,
    Settings: (p) => <I {...p}><circle cx="12" cy="12" r="3" /><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" /></I>,
    Sun: (p) => <I {...p}><circle cx="12" cy="12" r="4" /><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4" /></I>,
    Moon: (p) => <I {...p}><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z" /></I>,
    Trash: (p) => <I {...p}><path d="M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2m2 0v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" /></I>,
    Copy: (p) => <I {...p}><rect x="9" y="9" width="12" height="12" rx="2" /><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" /></I>,
    Check: (p) => <I {...p}><path d="M20 6L9 17l-5-5" /></I>,
    Close: (p) => <I {...p}><path d="M18 6L6 18M6 6l12 12" /></I>,
    Chevron: (p) => <I {...p}><path d="M9 18l6-6-6-6" /></I>,
    Refresh: (p) => <I {...p}><path d="M21 12a9 9 0 0 1-15.5 6.2M3 12A9 9 0 0 1 18.5 5.8" /><path d="M21 4v5h-5M3 20v-5h5" /></I>,
    Sidebar: (p) => <I {...p}><rect x="3" y="3" width="18" height="18" rx="2" /><path d="M9 3v18" /></I>,
  };

  /* ----------------------------- Sidebar ---------------------------- */
  function Sidebar({ conversations, currentId, onSelect, onNew, onDelete, onOpenSettings, theme, onToggleTheme }) {
    const list = Object.values(conversations).sort((a, b) => b.updatedAt - a.updatedAt);
    return (
      <aside className="sidebar">
        <div className="sidebar-header">
          <div className="brand">
            <div className="brand-mark" aria-hidden="true"></div>
            <div className="brand-name">OpenClaw<small>multimodal gateway</small></div>
          </div>
          <button className="new-chat-btn" onClick={onNew}>
            <Icons.Plus s={17} /> New chat
          </button>
        </div>
        <div className="conv-list">
          {list.length === 0 ? (
            <div className="conv-section-label">No conversations yet</div>
          ) : (
            <React.Fragment>
              <div className="conv-section-label">Recent</div>
              {list.map((c) => (
                <div key={c.id} className={"conv-item" + (c.id === currentId ? " active" : "")} onClick={() => onSelect(c.id)}>
                  <span className="conv-title">{c.title || "New chat"}</span>
                  <button className="conv-del" title="Delete" onClick={(e) => { e.stopPropagation(); onDelete(c.id); }}>
                    <Icons.Trash s={14} />
                  </button>
                </div>
              ))}
            </React.Fragment>
          )}
        </div>
        <div className="sidebar-footer">
          <button className="ghost-btn ghost-btn-label" onClick={onOpenSettings}><Icons.Settings s={16} /> Settings</button>
          <button className="ghost-btn ghost-btn-icon" title={theme === "dark" ? "Switch to light theme" : "Switch to dark theme"} onClick={onToggleTheme}>
            {theme === "dark" ? <Icons.Sun s={16} /> : <Icons.Moon s={16} />}
          </button>
        </div>
      </aside>
    );
  }

  /* ------------------------- Tool activity -------------------------- */
  function ToolActivity({ events, active }) {
    const [open, setOpen] = useState(false);
    if (!events || !events.length) return null;
    const visibleEvents = dedupe(events).map((e) => (!active && e.status === "running" ? { ...e, status: "done" } : e));
    const running = !!active && visibleEvents.some((e) => e.status === "running");
    const lastRunning = [...visibleEvents].reverse().find((e) => e.status === "running");
    const headline = running
      ? (lastRunning ? lastRunning.label : "Working…")
      : `Used ${countSkills(events)}`;
    return (
      <div className={"tool-activity" + (open ? " open" : "")}>
        <div className="tool-head" onClick={() => setOpen((o) => !o)}>
          {running ? <span className="spinner" /> : <span className="done-check"><Icons.Check s={14} /></span>}
          <span>{headline}</span>
          <span className="tool-caret"><Icons.Chevron s={15} /></span>
        </div>
        <div className="tool-body">
          {visibleEvents.map((e) => (
            <div key={e.id + e.status} className={"tool-step" + (e.status === "running" ? " running" : "")}>
              <div style={{ flex: 1 }}>
                <div>{e.label}</div>
                {e.skill ? <div className="step-skill">{e.skill}</div> : null}
              </div>
              {e.status === "running" ? <span className="spinner" /> : <span style={{ color: "var(--good)" }}><Icons.Check s={13} /></span>}
            </div>
          ))}
        </div>
      </div>
    );
  }
  function dedupe(events) {
    // collapse running→done on same id, keep latest state, preserve order
    const map = new Map();
    for (const e of events) map.set(e.id, e);
    return Array.from(map.values());
  }
  function countSkills(events) {
    const skills = new Set(dedupe(events).map((e) => e.skill).filter(Boolean));
    const n = skills.size;
    if (n === 0) return "1 agent";
    if (n === 1) return [...skills][0].split(".")[0] + " agent";
    return n + " agents on Blocks.ai";
  }

  function ThinkingTabs({ thinking, streaming }) {
    const [tab, setTab] = useState("steps");
    if (!thinking) return null;
    const steps = (thinking.steps || []).map((s) => (!streaming && s.status === "running" ? { ...s, status: "done" } : s));
    const errors = thinking.errors || [];
    const running = !!streaming && thinking.status === "running";
    const activeStep = [...steps].reverse().find((s) => s.status === "running") || steps[steps.length - 1];
    const visibleStatus = !streaming && thinking.status === "running" ? "done" : (thinking.status || "done");
    const headline = thinking.label || (running
      ? (activeStep ? activeStep.label : "Thinking…")
      : errors.length ? "Finished with issues" : "Finished");

    return (
      <div className={"thinking-tabs " + (running ? "running" : visibleStatus)}>
        <div className="thinking-head">
          <div className="thinking-title">
            {running ? <span className="spinner" /> : errors.length ? <span className="error-dot" /> : <span className="done-check"><Icons.Check s={13} /></span>}
            <span>{headline}</span>
          </div>
          <div className="thinking-tabbar">
            <button className={tab === "steps" ? "active" : ""} onClick={() => setTab("steps")}>Thinking</button>
            <button className={tab === "errors" ? "active" : ""} onClick={() => setTab("errors")} disabled={!errors.length}>
              Errors{errors.length ? ` ${errors.length}` : ""}
            </button>
          </div>
        </div>
        {tab === "steps" ? (
          <div className="thinking-body">
            {steps.length ? steps.map((s) => (
              <div key={s.id} className={"thinking-step " + (s.status || "done")}>
                <span className="thinking-step-mark">{s.status === "running" ? <span className="spinner" /> : <Icons.Check s={12} />}</span>
                <div>
                  <div>{s.label}</div>
                  {s.detail ? <small>{s.detail}</small> : null}
                </div>
              </div>
            )) : (
              <div className="thinking-empty">{running ? "Waiting for the first Blocks status…" : "No processing steps recorded."}</div>
            )}
          </div>
        ) : (
          <div className="thinking-body">
            {errors.length ? errors.map((e) => (
              <div key={e.id} className="thinking-error">
                <b>{e.title || "Error"}</b>
                <span>{e.message}</span>
              </div>
            )) : <div className="thinking-empty">No errors for this run.</div>}
          </div>
        )}
      </div>
    );
  }

  /* ----------------------------- Message ---------------------------- */
  function Message({ msg, streaming, onCopy, onAssistantConfirm }) {
    const isUser = msg.role === "user";
    const [copied, setCopied] = useState(false);
    const imgs = (msg.attachments || []).filter((a) => a.kind === "image");
    const auds = (msg.attachments || []).filter((a) => a.kind === "audio");
    const assistantArtifact = !isUser && msg.meta && msg.meta.assistant;
    const confirmState = !isUser && msg.meta && msg.meta.confirmAction;
    const copy = () => {
      navigator.clipboard && navigator.clipboard.writeText(msg.text || "");
      setCopied(true); setTimeout(() => setCopied(false), 1400);
      onCopy && onCopy();
    };
    return (
      <div className={"msg-row " + (isUser ? "user" : "assistant") + (streaming ? " streaming" : "")}>
        <div className={"avatar " + (isUser ? "user" : "assistant")} aria-hidden="true">{isUser ? "You" : ""}</div>
        <div className="msg-col">
          {imgs.length > 0 && (
            <div className="msg-images">
              {imgs.map((a) => <img key={a.id} className="msg-thumb" src={a.url} alt={a.name}
                onClick={() => window.dispatchEvent(new CustomEvent("openclaw:lightbox", { detail: { src: a.url } }))} />)}
            </div>
          )}
          {auds.map((a) => (
            <div key={a.id} className="msg-audio-chip">
              <Icons.Mic s={15} /> Voice message
              <audio controls src={a.url} style={{ height: 30, maxWidth: 220 }}></audio>
            </div>
          ))}

          {!isUser && msg.toolEvents && msg.toolEvents.length > 0 && (
            <ToolActivity events={msg.toolEvents} active={streaming} />
          )}
          {!isUser && msg.thinking && (
            <ThinkingTabs thinking={msg.thinking} streaming={streaming} />
          )}

          {isUser ? (
            msg.text ? <div className="bubble">{msg.text}</div> : null
          ) : (
            <div className="assistant-content">
              {msg.text
                ? <window.MarkdownRenderer text={msg.text} streaming={streaming} />
                : (streaming && (!msg.toolEvents || !msg.toolEvents.length)
                    ? <span className="typing"><span></span><span></span><span></span></span>
                    : null)}
              {streaming && msg.text ? <span className="typing" style={{ marginLeft: 4 }}><span></span><span></span><span></span></span> : null}
            </div>
          )}

          {!isUser && assistantArtifact && assistantArtifact.confirmToken && assistantArtifact.proposal ? (
            <AssistantConfirmCard
              artifact={assistantArtifact}
              state={confirmState}
              disabled={streaming}
              onAction={(action) => onAssistantConfirm && onAssistantConfirm(msg, action)}
            />
          ) : null}

          <div className="msg-footer">
            <span className="timestamp">{window.fmtTime(msg.ts)}</span>
            {!isUser && msg.meta && msg.meta.latency != null && (
              <span className="chip" title="Round-trip latency">{window.fmtDuration(msg.meta.latency)}</span>
            )}
            {!isUser && msg.meta && msg.meta.cost != null && (
              <span className="chip" title="Estimated cost">${Number(msg.meta.cost).toFixed(4)}</span>
            )}
            {!isUser && msg.meta && msg.meta.usage && msg.meta.usage.total_tokens != null && (
              <span className="chip" title="Tokens">{msg.meta.usage.total_tokens} tok</span>
            )}
            {!isUser && msg.text && !streaming && (
              <button className="mini-btn" onClick={copy}>
                {copied ? <Icons.Check s={13} /> : <Icons.Copy s={13} />} {copied ? "Copied" : "Copy"}
              </button>
            )}
          </div>
        </div>
      </div>
    );
  }

  function AssistantConfirmCard({ artifact, state, disabled, onAction }) {
    const status = state && state.status;
    const proposal = artifact.proposal || {};
    const args = proposal.args || {};
    const detail = bookingDetail(args);
    const pending = status === "pending";
    const confirmed = status === "confirmed";
    const dismissed = status === "dismissed";
    const error = status === "error";
    if (dismissed) {
      return (
        <div className="confirm-pop dismissed">
          <div className="confirm-pop-icon"><Icons.Close s={15} /></div>
          <div className="confirm-pop-copy">
            <b>Confirmation dismissed</b>
            <span>No calendar event was booked.</span>
          </div>
        </div>
      );
    }
    return (
      <div className={"confirm-pop " + (pending ? "pending" : confirmed ? "confirmed" : error ? "error" : "ready")}>
        <div className="confirm-pop-main">
          <div className="confirm-pop-icon">
            {pending ? <span className="spinner" /> : confirmed ? <Icons.Check s={16} /> : error ? <Icons.Close s={16} /> : <Icons.Check s={16} />}
          </div>
          <div className="confirm-pop-copy">
            <b>{confirmed ? "Calendar event booked" : error ? "Calendar did not book it" : "Confirm calendar booking"}</b>
            <span>{error ? (state.error || "The Calendar integration rejected the write.") : detail}</span>
          </div>
        </div>
        {!confirmed ? (
          <div className="confirm-pop-actions">
            <button
              className="confirm-action confirm"
              title={error ? "Try confirming again" : "Confirm booking"}
              disabled={disabled || pending}
              onClick={() => onAction && onAction({ type: "confirm", token: artifact.confirmToken })}
            >
              {pending ? <span className="spinner" /> : <Icons.Check s={20} />}
            </button>
            <button
              className="confirm-action dismiss"
              title="Dismiss"
              disabled={disabled || pending}
              onClick={() => onAction && onAction({ type: "dismiss", token: artifact.confirmToken })}
            >
              <Icons.Close s={20} />
            </button>
          </div>
        ) : null}
      </div>
    );
  }

  function bookingDetail(args) {
    const summary = typeof args.summary === "string" && args.summary.trim() ? args.summary.trim() : "Meeting";
    const start = formatBookingDateTime(args.start);
    const end = formatBookingEnd(args.start, args.end);
    if (start && end) return `${summary} · ${start} to ${end}`;
    if (start) return `${summary} · ${start}`;
    return summary;
  }

  function formatBookingDateTime(value) {
    if (typeof value !== "string" || !value.trim()) return "";
    const d = new Date(value);
    if (Number.isNaN(d.getTime())) return value;
    return d.toLocaleString([], { weekday: "short", month: "short", day: "numeric", hour: "numeric", minute: "2-digit" });
  }

  function formatBookingEnd(startValue, endValue) {
    if (typeof endValue !== "string" || !endValue.trim()) return "";
    const end = new Date(endValue);
    if (Number.isNaN(end.getTime())) return endValue;
    const start = typeof startValue === "string" ? new Date(startValue) : null;
    const sameDay = start && !Number.isNaN(start.getTime()) && start.toDateString() === end.toDateString();
    return end.toLocaleString([], sameDay
      ? { hour: "numeric", minute: "2-digit" }
      : { weekday: "short", month: "short", day: "numeric", hour: "numeric", minute: "2-digit" });
  }

  /* --------------------------- Empty state -------------------------- */
  function EmptyState({ onPick, demoMode }) {
    const suggestions = [
      { t: "Make a poster", s: "for a calm productivity app", p: "Make a poster for a calm productivity app called Driftwork." },
      { t: "Narrate a welcome", s: "short voiceover", p: "Narrate a short, friendly welcome message for new users." },
      { t: "Hire two agents", s: "image + audio together", p: "Make a poster for a jazz night, and narrate the tagline." },
      { t: "Describe an image", s: "attach one below", p: "What's in this image? Suggest a caption." },
    ];
    return (
      <div className="empty-state">
        <div className="empty-mark" aria-hidden="true"></div>
        <h2>What can I get made for you?</h2>
        <p>I’m OpenClaw. Ask me anything — and when a task needs a specialist, I’ll hire one on the Blocks.ai network and bring the result back inline.{demoMode ? " You’re in demo mode." : ""}</p>
        <div className="suggestions">
          {suggestions.map((s, i) => (
            <button key={i} className="suggestion" onClick={() => onPick(s.p)}>
              <b>{s.t}</b><span>{s.s}</span>
            </button>
          ))}
        </div>
      </div>
    );
  }

  /* --------------------- Assistant overview panel -------------------- */
  function AssistantOverviewPanel({ settings }) {
    const [state, setState] = useState({ loading: true, data: null, error: "" });
    const refresh = useCallback(() => {
      const controller = new AbortController();
      setState((prev) => ({ ...prev, loading: true, error: "" }));
      window.assistantOverview(settings, controller.signal)
        .then((data) => setState({ loading: false, data, error: "" }))
        .catch((err) => setState({ loading: false, data: null, error: String(err.message || err) }));
      return () => controller.abort();
    }, [settings.baseUrl]);

    useEffect(() => refresh(), [refresh]);

    const assistants = state.data && Array.isArray(state.data.assistants)
      ? state.data.assistants.filter((assistant) => String(assistant.agentName || "").startsWith("pa_"))
      : [];
    const privateAgents = state.data && state.data.blocksPrivateAgents ? state.data.blocksPrivateAgents : null;
    return (
      <section className="overview-panel" aria-label="Assistant overview">
        <div className="overview-head">
          <div>
            <div className="overview-title">Assistant overview</div>
            <div className="overview-sub">
              {state.loading ? "Refreshing…" : state.error ? "Unavailable" : `${assistants.length} assistant${assistants.length === 1 ? "" : "s"} · ${state.data.a2aCallsToday || 0}/${state.data.dailyCap || 0} A2A today`}
            </div>
          </div>
          <button className="icon-btn" title="Refresh overview" onClick={refresh} disabled={state.loading}>
            <Icons.Refresh s={16} />
          </button>
        </div>
        {state.error ? (
          <div className="overview-empty">{state.error}</div>
        ) : (
          <React.Fragment>
            {privateAgents ? <BlocksPrivateAgentsPanel privateAgents={privateAgents} /> : null}
            {assistants.length === 0 ? (
              <div className="overview-empty">{state.loading ? "Loading assistants…" : "No assistants yet."}</div>
            ) : (
              <div className="overview-grid">
                {assistants.map((assistant) => <AssistantOverviewCard key={assistant.agentName} assistant={assistant} />)}
              </div>
            )}
          </React.Fragment>
        )}
      </section>
    );
  }

  function BlocksPrivateAgentsPanel({ privateAgents }) {
    const agents = Array.isArray(privateAgents.agents) ? privateAgents.agents : [];
    const visible = agents.slice(0, 6);
    const status = privateAgents.status || "unavailable";
    const total = privateAgents.totalCount || agents.length;
    const hiddenOwned = privateAgents.hiddenOwnedCount || 0;
    const subtitle = status === "online"
      ? `${total} invited private agent${total === 1 ? "" : "s"}${hiddenOwned ? ` · ${hiddenOwned} owned hidden` : ""}`
      : privateAgents.note || "Private listing unavailable.";
    return (
      <div className="blocks-private-panel">
        <div className="blocks-private-head">
          <div>
            <b>Invited private agents</b>
            <span>{subtitle}</span>
          </div>
          <span className={"private-status " + status}>{status}</span>
        </div>
        {status === "online" && visible.length ? (
          <div className="blocks-private-list">
            {visible.map((agent) => (
              <span key={agent.agentName} title={agent.description || agent.displayName || agent.agentName}>
                <b>{agent.agentName}</b>
                <small>{agent.displayName || "private agent"}</small>
              </span>
            ))}
          </div>
        ) : (
          <div className="blocks-private-empty">{privateAgents.error || privateAgents.note || "No private agents found."}</div>
        )}
      </div>
    );
  }

  function AssistantOverviewCard({ assistant }) {
    const peers = Array.isArray(assistant.peers) ? assistant.peers : [];
    const hops = Array.isArray(assistant.hops) ? assistant.hops.slice(0, 3) : [];
    const spend = assistant.spendToday || {};
    const integrations = assistant.integrations || {};
    return (
      <article className="assistant-card">
        <div className="assistant-card-head">
          <div className="assistant-id">
            <b>{assistant.agentName || "assistant"}</b>
            <span>{assistant.owner || "owner unknown"}</span>
          </div>
          <span className={"live-pill" + (assistant.live ? " on" : "")}>{assistant.live ? "Live" : "Offline"}</span>
        </div>
        <div className="assistant-metrics">
          <Metric label="Peers" value={peers.length} />
          <Metric label="A2A" value={`${spend.a2aCalls || 0}/${spend.dailyCap || 0}`} />
        </div>
        <div className="integration-pills">
          <IntegrationPill label="Calendar" connected={connected(integrations.calendar) || connected(integrations.google)} />
          <IntegrationPill label="Gmail" connected={connected(integrations.gmail) || connected(integrations.google)} />
        </div>
        <div className="peer-list">
          {peers.length ? peers.slice(0, 4).map((peer) => (
            <span key={peer.agentName} title={peer.owner}>{peer.agentName}</span>
          )) : <em>No peers</em>}
        </div>
        <div className="hop-list">
          {hops.length ? hops.map((hop) => (
            <div key={`${hop.at}-${hop.direction}-${hop.from}-${hop.to}`} className="hop-row">
              <span className={"hop-dir " + hop.direction}>{hop.direction === "out" ? "Out" : "In"}</span>
              <span>{hop.from} → {hop.to}</span>
              <small>{hop.outcome || "recorded"}</small>
            </div>
          )) : <div className="hop-empty">No recent hops</div>}
        </div>
      </article>
    );
  }

  function Metric({ label, value }) {
    return <div className="metric"><span>{label}</span><b>{value}</b></div>;
  }

  function IntegrationPill({ label, connected }) {
    return <span className={"integration-pill" + (connected ? " connected" : "")}>{label}: {connected ? "connected" : "not connected"}</span>;
  }

  function connected(value) {
    return !!(value && value.connected);
  }

  /* --------------------------- Settings ----------------------------- */
  function SettingsModal({ settings, onClose, onSave }) {
    const [s, setS] = useState(settings);
    const set = (k, v) => setS((prev) => ({ ...prev, [k]: v }));
    return (
      <div className="modal-backdrop" onMouseDown={(e) => { if (e.target === e.currentTarget) onClose(); }}>
        <div className="modal" role="dialog" aria-label="Settings">
          <div className="modal-head">
            <h3>Settings</h3>
            <button className="icon-btn" onClick={onClose}><Icons.Close s={18} /></button>
          </div>
          <div className="modal-body">
            <div className="toggle-row">
              <div className="toggle-text">
                <b>Demo mode</b>
                <span>Simulate OpenClaw locally — streaming, agent dispatch, image & audio output.</span>
              </div>
              <label className="switch">
                <input type="checkbox" checked={!!s.demoMode} onChange={(e) => set("demoMode", e.target.checked)} />
                <span className="track"></span>
              </label>
            </div>
            <div className="field" style={{ opacity: s.demoMode ? 0.5 : 1 }}>
              <label>Gateway base URL</label>
              <input type="text" value={s.baseUrl} disabled={s.demoMode}
                onChange={(e) => set("baseUrl", e.target.value)} placeholder="(blank = via foundation bridge)" />
              <span className="hint">Blank routes through this dashboard’s <code style={{ fontFamily: "var(--mono)" }}>/v1/chat/completions</code> proxy to the gateway. Set a full URL to call a gateway directly.</span>
            </div>
            <div className="field">
              <label>Owner ID</label>
              <input type="text" value={s.ownerId || ""}
                onChange={(e) => set("ownerId", e.target.value)} placeholder="auto-filled from bridge" />
              <span className="hint">Used for private assistant authorization and per-owner Google Calendar/Gmail connections.</span>
            </div>
            <div className="field" style={{ opacity: s.demoMode ? 0.5 : 1 }}>
              <label>Gateway token</label>
              <input type="password" value={s.token} disabled={s.demoMode}
                onChange={(e) => set("token", e.target.value)} placeholder="(blank = injected by the bridge)" />
              <span className="hint">Sent as <code style={{ fontFamily: "var(--mono)" }}>Authorization: Bearer …</code>. Leave blank to let the bridge inject <code style={{ fontFamily: "var(--mono)" }}>OPENCLAW_GATEWAY_TOKEN</code>.</span>
            </div>
          </div>
          <div className="modal-footer">
            <button className="btn secondary" onClick={onClose}>Cancel</button>
            <button className="btn primary" onClick={() => onSave(s)}>Save</button>
          </div>
        </div>
      </div>
    );
  }

  /* --------------------------- Lightbox ----------------------------- */
  function Lightbox() {
    const [src, setSrc] = useState(null);
    useEffect(() => {
      const open = (e) => setSrc(e.detail.src);
      const onKey = (e) => { if (e.key === "Escape") setSrc(null); };
      window.addEventListener("openclaw:lightbox", open);
      window.addEventListener("keydown", onKey);
      return () => { window.removeEventListener("openclaw:lightbox", open); window.removeEventListener("keydown", onKey); };
    }, []);
    if (!src) return null;
    return (
      <div className="lightbox" onClick={() => setSrc(null)}>
        <button className="lb-close" onClick={() => setSrc(null)}><Icons.Close s={20} /></button>
        <img src={src} alt="" onClick={(e) => e.stopPropagation()} />
      </div>
    );
  }

  /* ----------------------------- Toast ------------------------------ */
  function Toast({ text }) { return text ? <div className="toast">{text}</div> : null; }

  // Memoize Message: while a reply streams, only the streaming message's
  // props change each token. Without this, every prior message (including
  // any with heavy inline media) re-renders on every token, which locks the
  // main thread and can crash the tab.
  const MemoMessage = React.memo(Message);

  Object.assign(window, { Icons, Sidebar, ToolActivity, ThinkingTabs, Message: MemoMessage, EmptyState, AssistantOverviewPanel, SettingsModal, Lightbox, Toast });
})();
