islandflow/apps/web/app/desktop-ai-panels.tsx

1203 lines
36 KiB
TypeScript

"use client";
import Link from "next/link";
import { useMemo, useState, type ReactNode } from "react";
import type {
AlertEvent,
ClassifierHitEvent,
FlowPacket,
IslandflowAiCompiledScreen,
IslandflowAiPlanType,
IslandflowAiRateLimitSnapshot,
IslandflowAiReasoningEffort,
IslandflowAiTaskKind,
OptionFlowFilters,
OptionPrint,
SmartMoneyEvent,
} from "@islandflow/types";
import { useDesktopAi } from "./desktop-ai";
const numberFormatter = new Intl.NumberFormat("en-US");
const usdFormatter = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
minimumFractionDigits: 2,
maximumFractionDigits: 4,
});
const humanizeValue = (value: string | null | undefined): string => {
if (!value) {
return "Unknown";
}
return value
.replace(/_/g, " ")
.replace(/\b\w/g, (char) => char.toUpperCase());
};
const formatTokens = (value: number): string => numberFormatter.format(value);
const formatUsd = (value: number | null): string =>
value === null ? "Unavailable" : usdFormatter.format(value);
const formatTimestamp = (value: number | null): string => {
if (!value) {
return "Not reported";
}
return new Intl.DateTimeFormat("en-US", {
dateStyle: "medium",
timeStyle: "short",
}).format(value);
};
const formatPercent = (value: number): string => `${Math.round(value)}%`;
const getTaskStatusLabel = (value: string): string => humanizeValue(value);
type DesktopAiSettingsNotice = {
title: string;
body: string;
};
export const getDesktopAiSettingsBridgeNotice = (
shellAvailable: boolean,
bridgeAvailable: boolean,
): DesktopAiSettingsNotice | null => {
if (!shellAvailable) {
return {
title: "Desktop app required",
body: "Open Islandflow Desktop to connect ChatGPT, load managed models, and use native Copilot controls.",
};
}
if (!bridgeAvailable) {
return {
title: "Bridge unavailable in this window",
body: "This Islandflow Desktop window is missing its native AI bridge, so login actions and model controls stay disabled until the bridge reconnects. Reload the window or restart Islandflow if this keeps happening.",
};
}
return null;
};
export const getDesktopAiModelSelectLabel = (
shellAvailable: boolean,
bridgeAvailable: boolean,
loggedIn: boolean,
modelCount: number,
): string => {
if (!shellAvailable) {
return "Desktop app required";
}
if (!bridgeAvailable) {
return "Bridge unavailable";
}
if (!loggedIn) {
return "Connect ChatGPT to load models";
}
if (modelCount === 0) {
return "Waiting for managed models";
}
return "Use server default";
};
export const getDesktopAiModelListEmptyCopy = (
shellAvailable: boolean,
bridgeAvailable: boolean,
loggedIn: boolean,
): string => {
if (!shellAvailable) {
return "Open Islandflow Desktop to load managed model options.";
}
if (!bridgeAvailable) {
return "Managed model options will appear here after the native AI bridge reconnects.";
}
if (!loggedIn) {
return "Connect a ChatGPT or Codex account to load the managed model catalog.";
}
return "No model catalog has been reported yet.";
};
export const getDesktopAiProfileBadgeLabel = (
selected: boolean,
statusLabel: string,
bridgeAvailable: boolean,
): string => {
if (!bridgeAvailable) {
return statusLabel;
}
return selected ? "Selected" : statusLabel;
};
const findTask = <T extends { taskId: string }>(
tasks: T[],
taskId: string | null,
): T | null => {
if (!taskId) {
return null;
}
return tasks.find((task) => task.taskId === taskId) ?? null;
};
const getCompiledScreenSummary = (
compiled: IslandflowAiCompiledScreen,
): string[] => {
const filters = compiled.compiledFilters;
if (!filters) {
return [];
}
const parts: string[] = [];
if (filters.view) {
parts.push(`View: ${filters.view}`);
}
if (filters.securityTypes?.length) {
parts.push(`Security: ${filters.securityTypes.join(", ")}`);
}
if (filters.optionTypes?.length) {
parts.push(`Options: ${filters.optionTypes.join(", ")}`);
}
if (filters.nbboSides?.length) {
parts.push(`NBBO: ${filters.nbboSides.join(", ")}`);
}
if (typeof filters.minNotional === "number") {
parts.push(`Min notional: $${numberFormatter.format(filters.minNotional)}`);
}
return parts;
};
const CopilotPane = ({
title,
eyebrow,
actions,
wide = false,
children,
}: {
title: string;
eyebrow?: string;
actions?: ReactNode;
wide?: boolean;
children: ReactNode;
}) => {
return (
<section
className={`terminal-pane copilot-pane${wide ? " copilot-pane-wide" : ""}`}
>
<div className="terminal-pane-head">
<div className="terminal-pane-title-row">
<div>
{eyebrow ? <div className="copilot-kicker">{eyebrow}</div> : null}
<h2 className="terminal-pane-title">{title}</h2>
</div>
</div>
{actions ? (
<div className="terminal-pane-actions">{actions}</div>
) : null}
</div>
<div className="terminal-pane-body copilot-pane-body">{children}</div>
</section>
);
};
const UsageBreakdown = ({
title,
breakdown,
normalizedCostUsd,
turnCount,
activeDays,
}: {
title: string;
breakdown: {
totalTokens: number;
inputTokens: number;
cachedInputTokens: number;
outputTokens: number;
reasoningOutputTokens: number;
};
normalizedCostUsd: number | null;
turnCount: number;
activeDays: number;
}) => {
return (
<div className="copilot-usage-block">
<div className="copilot-usage-title-row">
<h3>{title}</h3>
<span className="copilot-usage-cost">
{formatUsd(normalizedCostUsd)}
</span>
</div>
<div className="copilot-token-grid">
<div className="copilot-token-row">
<span>Total tokens</span>
<strong>{formatTokens(breakdown.totalTokens)}</strong>
</div>
<div className="copilot-token-row">
<span>Input</span>
<strong>{formatTokens(breakdown.inputTokens)}</strong>
</div>
<div className="copilot-token-row">
<span>Cached input</span>
<strong>{formatTokens(breakdown.cachedInputTokens)}</strong>
</div>
<div className="copilot-token-row">
<span>Output</span>
<strong>{formatTokens(breakdown.outputTokens)}</strong>
</div>
<div className="copilot-token-row">
<span>Reasoning</span>
<strong>{formatTokens(breakdown.reasoningOutputTokens)}</strong>
</div>
<div className="copilot-token-row">
<span>Turns</span>
<strong>{formatTokens(turnCount)}</strong>
</div>
<div className="copilot-token-row">
<span>Active days</span>
<strong>{formatTokens(activeDays)}</strong>
</div>
</div>
</div>
);
};
const RateLimitBoard = ({
limit,
}: {
limit: IslandflowAiRateLimitSnapshot;
}) => {
return (
<div
className="copilot-limit-card"
key={limit.limitId ?? limit.limitName ?? "default"}
>
<div className="copilot-limit-head">
<div>
<strong>{limit.limitName ?? "Default rate window"}</strong>
<p className="copilot-note">
{limit.planType
? `Plan ${humanizeValue(limit.planType)}`
: "Plan not reported"}
</p>
</div>
{limit.reachedType ? (
<span className="copilot-badge warning">
{humanizeValue(limit.reachedType)}
</span>
) : null}
</div>
<div className="copilot-limit-grid">
{limit.primary ? (
<div className="copilot-limit-window">
<span>Primary</span>
<strong>{formatPercent(limit.primary.usedPercent)}</strong>
<p className="copilot-note">
Resets {formatTimestamp(limit.primary.resetsAt)}
</p>
</div>
) : null}
{limit.secondary ? (
<div className="copilot-limit-window">
<span>Secondary</span>
<strong>{formatPercent(limit.secondary.usedPercent)}</strong>
<p className="copilot-note">
Resets {formatTimestamp(limit.secondary.resetsAt)}
</p>
</div>
) : null}
</div>
{limit.creditsBalance || limit.unlimitedCredits !== null ? (
<p className="copilot-note">
Credits:{" "}
{limit.unlimitedCredits
? "unlimited"
: limit.creditsBalance
? limit.creditsBalance
: limit.hasCredits === false
? "none"
: "not reported"}
</p>
) : null}
</div>
);
};
const TaskOutput = ({
taskId,
emptyMessage,
}: {
taskId: string | null;
emptyMessage: string;
}) => {
const { state } = useDesktopAi();
const task = findTask(state.tasks, taskId);
if (!task) {
return <p className="copilot-empty">{emptyMessage}</p>;
}
return (
<div className="copilot-task-output" aria-live="polite">
<div className="copilot-task-head">
<div>
<strong>{task.title}</strong>
<p className="copilot-note">
{task.subtitle} · {getTaskStatusLabel(task.status)}
</p>
</div>
<span className={`copilot-badge status-${task.status}`}>
{getTaskStatusLabel(task.status)}
</span>
</div>
{task.error ? <p className="copilot-error">{task.error}</p> : null}
{task.text ? <pre className="copilot-task-text">{task.text}</pre> : null}
{task.compiledScreen ? (
<CompiledScreenResult compiled={task.compiledScreen} />
) : null}
</div>
);
};
const CompiledScreenResult = ({
compiled,
}: {
compiled: IslandflowAiCompiledScreen;
}) => {
const summary = getCompiledScreenSummary(compiled);
return (
<div className="copilot-compiled-screen">
{summary.length > 0 ? (
<div className="copilot-chip-row">
{summary.map((item) => (
<span className="copilot-chip" key={item}>
{item}
</span>
))}
</div>
) : (
<p className="copilot-note">
No filter fields were compiled from this prompt.
</p>
)}
{compiled.unhandledClauses.length > 0 ? (
<div className="copilot-unhandled-list">
<div className="copilot-list-title">Unhandled clauses</div>
{compiled.unhandledClauses.map((item) => (
<div className="copilot-inline-row" key={item}>
<span>{item}</span>
</div>
))}
</div>
) : null}
</div>
);
};
const AccountSummary = ({
loggedIn,
email,
planType,
}: {
loggedIn: boolean;
email: string | null;
planType: IslandflowAiPlanType | null;
}) => {
return (
<div className="copilot-hero">
<div>
<p className="copilot-kicker">Desktop-only official Codex bridge</p>
<h1 className="page-title">Analyst Copilot</h1>
<p className="copilot-hero-copy">
Managed ChatGPT login stays user-scoped, deterministic smart-money
classification stays in charge, and every AI turn is tracked with
exact token telemetry from the app-server.
</p>
</div>
<div className="copilot-hero-meta">
<div className="copilot-stat">
<span>Account</span>
<strong>{loggedIn ? (email ?? "Connected") : "Disconnected"}</strong>
</div>
<div className="copilot-stat">
<span>Plan</span>
<strong>
{loggedIn ? humanizeValue(planType) : "Not connected"}
</strong>
</div>
</div>
</div>
);
};
const LoginStatePanel = () => {
const {
bridgeAvailable,
shellAvailable,
state,
loginWithBrowser,
loginWithDeviceCode,
cancelLogin,
logout,
} = useDesktopAi();
const [busyAction, setBusyAction] = useState<string | null>(null);
const [actionError, setActionError] = useState<string | null>(null);
const loginState = state.account.login;
const actionsDisabled = busyAction !== null || !bridgeAvailable;
const bridgeNotice = getDesktopAiSettingsBridgeNotice(
shellAvailable,
bridgeAvailable,
);
const runAction = async (label: string, action: () => Promise<void>) => {
setBusyAction(label);
setActionError(null);
try {
await action();
} catch (error) {
setActionError(error instanceof Error ? error.message : String(error));
} finally {
setBusyAction(null);
}
};
return (
<CopilotPane
title="Account and access"
eyebrow="Managed auth"
wide
actions={
<>
{state.account.loggedIn ? (
<button
className="terminal-button"
type="button"
onClick={() => void runAction("logout", logout)}
disabled={actionsDisabled}
>
{busyAction === "logout" ? "Logging out" : "Logout"}
</button>
) : bridgeNotice ? (
shellAvailable ? (
<button
className="terminal-button terminal-button-primary"
type="button"
onClick={() => window.location.reload()}
>
Reload window
</button>
) : null
) : (
<>
<button
className="terminal-button terminal-button-primary"
type="button"
onClick={() => void runAction("browser", loginWithBrowser)}
disabled={actionsDisabled}
>
{busyAction === "browser" ? "Opening browser" : "Browser login"}
</button>
<button
className="terminal-button"
type="button"
onClick={() => void runAction("device", loginWithDeviceCode)}
disabled={actionsDisabled}
>
{busyAction === "device" ? "Preparing code" : "Device code"}
</button>
</>
)}
{(loginState.status === "browser_pending" ||
loginState.status === "device_code_pending") &&
!state.account.loggedIn ? (
<button
className="terminal-button"
type="button"
onClick={() => void runAction("cancel", cancelLogin)}
disabled={actionsDisabled}
>
Cancel
</button>
) : null}
</>
}
>
<AccountSummary
loggedIn={state.account.loggedIn}
email={state.account.email}
planType={state.account.planType}
/>
{bridgeNotice ? (
<div className="copilot-callout copilot-callout-warning">
<strong>{bridgeNotice.title}</strong>
<p className="copilot-note">{bridgeNotice.body}</p>
</div>
) : null}
<div className="copilot-account-grid">
<div className="copilot-account-card">
<div className="copilot-list-title">Profile slots</div>
{state.profiles.map((profile) => (
<div className="copilot-inline-row" key={profile.id}>
<div>
<strong>{profile.label}</strong>
<p className="copilot-note">{profile.description}</p>
</div>
<span
className={`copilot-badge${profile.enabled ? "" : " muted"}`}
>
{getDesktopAiProfileBadgeLabel(
profile.selected,
profile.statusLabel,
bridgeAvailable,
)}
</span>
</div>
))}
</div>
<div className="copilot-account-card">
<div className="copilot-list-title">Session status</div>
<div className="copilot-inline-row">
<span>Transport</span>
<strong>{humanizeValue(state.transportStatus)}</strong>
</div>
<div className="copilot-inline-row">
<span>Auth mode</span>
<strong>{humanizeValue(state.account.authMode)}</strong>
</div>
<div className="copilot-inline-row">
<span>OpenAI auth required</span>
<strong>{state.account.requiresOpenaiAuth ? "Yes" : "No"}</strong>
</div>
{state.transportError ? (
<p className="copilot-error">{state.transportError}</p>
) : null}
{loginState.message ? (
<p className="copilot-note">{loginState.message}</p>
) : null}
{loginState.status === "browser_pending" ? (
<div className="copilot-callout">
<strong>Browser login in progress</strong>
<p className="copilot-note">
Finish the ChatGPT sign-in flow in your browser. Islandflow will
update automatically.
</p>
</div>
) : null}
{loginState.status === "device_code_pending" ? (
<div className="copilot-callout">
<strong>Device code</strong>
<pre className="copilot-device-code">{loginState.userCode}</pre>
<p className="copilot-note">
Visit {loginState.verificationUrl} in any browser and enter the
code above.
</p>
</div>
) : null}
{actionError ? <p className="copilot-error">{actionError}</p> : null}
</div>
</div>
</CopilotPane>
);
};
export function DesktopAiSettingsRoute() {
const { bridgeAvailable, shellAvailable, state, updatePreferences } =
useDesktopAi();
const [busyPreference, setBusyPreference] = useState<
"model" | "reasoning" | null
>(null);
const [preferenceError, setPreferenceError] = useState<string | null>(null);
const rateLimits = Object.values(state.rateLimitsByLimitId);
const selectedModel = state.preferences.model ?? "";
const selectedReasoning = state.preferences.reasoningEffort ?? "";
const modelControlsNotice = getDesktopAiSettingsBridgeNotice(
shellAvailable,
bridgeAvailable,
);
const modelSelectLabel = getDesktopAiModelSelectLabel(
shellAvailable,
bridgeAvailable,
state.account.loggedIn,
state.models.length,
);
const modelListEmptyCopy = getDesktopAiModelListEmptyCopy(
shellAvailable,
bridgeAvailable,
state.account.loggedIn,
);
const savePreference = async (
key: "model" | "reasoning",
next: Partial<{
model: string | null;
reasoningEffort: IslandflowAiReasoningEffort | null;
}>,
) => {
setBusyPreference(key);
setPreferenceError(null);
try {
await updatePreferences(next);
} catch (error) {
setPreferenceError(
error instanceof Error ? error.message : String(error),
);
} finally {
setBusyPreference(null);
}
};
return (
<div className="page-shell">
{!shellAvailable ? (
<CopilotPane
title="Desktop required"
eyebrow="Browser-only fallback"
wide
>
<div className="copilot-unavailable">
<p>
AI controls are intentionally read-only in the browser build. Open
Islandflow Desktop to use managed ChatGPT login, structured
Copilot turns, and app-server token telemetry.
</p>
</div>
</CopilotPane>
) : null}
<LoginStatePanel />
<div className="page-grid page-grid-settings">
<CopilotPane title="Model controls" eyebrow="Execution">
{modelControlsNotice ? (
<div className="copilot-callout copilot-callout-warning">
<strong>{modelControlsNotice.title}</strong>
<p className="copilot-note">{modelControlsNotice.body}</p>
</div>
) : state.models.length === 0 ? (
<div className="copilot-callout">
<strong>Managed models are not loaded yet</strong>
<p className="copilot-note">{modelListEmptyCopy}</p>
</div>
) : null}
<div className="copilot-field-grid">
<label className="copilot-field">
<span className="copilot-field-label">Model</span>
<select
className="copilot-select"
value={selectedModel}
onChange={(event) =>
void savePreference("model", {
model: event.target.value.trim()
? event.target.value
: null,
})
}
disabled={
busyPreference !== null ||
state.models.length === 0 ||
!bridgeAvailable
}
>
<option value="">{modelSelectLabel}</option>
{state.models.map((model) => (
<option key={model.id} value={model.model}>
{model.displayName}
</option>
))}
</select>
</label>
<label className="copilot-field">
<span className="copilot-field-label">Reasoning</span>
<select
className="copilot-select"
value={selectedReasoning}
onChange={(event) =>
void savePreference("reasoning", {
reasoningEffort: event.target.value.trim()
? (event.target.value as IslandflowAiReasoningEffort)
: null,
})
}
disabled={busyPreference !== null || !bridgeAvailable}
>
<option value="">Use model default</option>
<option value="none">None</option>
<option value="minimal">Minimal</option>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
<option value="xhigh">XHigh</option>
</select>
</label>
</div>
<div className="copilot-model-list">
{state.models.length === 0 ? (
<p className="copilot-empty">{modelListEmptyCopy}</p>
) : (
state.models.map((model) => (
<div className="copilot-model-row" key={model.id}>
<div>
<strong>{model.displayName}</strong>
<p className="copilot-note">{model.description}</p>
</div>
<div className="copilot-model-meta">
<span>{model.model}</span>
{model.pricing ? (
<span>
{formatUsd(model.pricing.inputUsdPer1MTokens)} / 1M
input
</span>
) : null}
</div>
</div>
))
)}
</div>
{state.models.find((model) => model.model === state.preferences.model)
?.pricing ? (
<p className="copilot-note">
Normalized estimates use current API pricing for the selected
model, not your literal ChatGPT subscription bill.
</p>
) : null}
{preferenceError ? (
<p className="copilot-error">{preferenceError}</p>
) : null}
</CopilotPane>
<CopilotPane title="Rate limits" eyebrow="Live windows">
{rateLimits.length === 0 ? (
<p className="copilot-empty">
No rate-limit snapshots have been reported yet.
</p>
) : (
<div className="copilot-limit-list">
{rateLimits.map((limit) => (
<RateLimitBoard
key={limit.limitId ?? limit.limitName ?? "default"}
limit={limit}
/>
))}
</div>
)}
</CopilotPane>
<CopilotPane
title="Usage dashboard"
eyebrow="Exact app-server telemetry"
wide
>
<div className="copilot-usage-grid">
<UsageBreakdown
title="Today"
breakdown={state.usage.today.breakdown}
normalizedCostUsd={state.usage.today.normalizedCostUsd}
turnCount={state.usage.today.turnCount}
activeDays={state.usage.today.activeDays}
/>
<UsageBreakdown
title="Lifetime"
breakdown={state.usage.lifetime.breakdown}
normalizedCostUsd={state.usage.lifetime.normalizedCostUsd}
turnCount={state.usage.lifetime.turnCount}
activeDays={state.usage.lifetime.activeDays}
/>
</div>
</CopilotPane>
<CopilotPane title="Recent turns" eyebrow="Per-thread usage">
{state.usage.recentTurns.length === 0 ? (
<p className="copilot-empty">No tracked turns yet.</p>
) : (
<div className="copilot-turn-list">
{state.usage.recentTurns.map((turn) => (
<div
className="copilot-turn-row"
key={`${turn.threadId}:${turn.turnId}`}
>
<div>
<strong>{turn.taskTitle ?? "Ad hoc turn"}</strong>
<p className="copilot-note">
{turn.model ?? "default"} ·{" "}
{formatTimestamp(turn.updatedAt)}
</p>
</div>
<div className="copilot-turn-metrics">
<span>{formatTokens(turn.breakdown.totalTokens)} tok</span>
<span>{formatUsd(turn.normalizedCostUsd)}</span>
</div>
</div>
))}
</div>
)}
</CopilotPane>
<CopilotPane title="Recent analyses" eyebrow="Task feed">
{state.tasks.length === 0 ? (
<p className="copilot-empty">No Copilot tasks have been run yet.</p>
) : (
<div className="copilot-task-list">
{state.tasks.map((task) => (
<div className="copilot-task-list-row" key={task.taskId}>
<div>
<strong>{task.title}</strong>
<p className="copilot-note">
{task.subtitle} · {humanizeValue(task.model)}
</p>
</div>
<span className={`copilot-badge status-${task.status}`}>
{getTaskStatusLabel(task.status)}
</span>
</div>
))}
</div>
)}
</CopilotPane>
</div>
</div>
);
}
export const requireDesktopActionCopy = (
shellAvailable: boolean,
bridgeAvailable: boolean,
loggedIn: boolean,
): string => {
if (!shellAvailable) {
return "This control is desktop-only. Open Islandflow Desktop to run Copilot tasks.";
}
if (!bridgeAvailable) {
return "Islandflow Desktop is open, but this window is missing the native AI bridge. Reload the window or restart the app.";
}
if (!loggedIn) {
return "Connect a ChatGPT or Codex account in Settings before running Copilot analysis.";
}
return "";
};
const SmartMoneyTaskButton = ({
label,
kind,
symbol,
disabled,
busyKind,
onRun,
}: {
label: string;
kind: IslandflowAiTaskKind;
symbol: string;
disabled: boolean;
busyKind: IslandflowAiTaskKind | null;
onRun: (kind: IslandflowAiTaskKind) => void;
}) => {
return (
<button
className={`terminal-button${kind === "smart-money-explain" ? " terminal-button-primary" : ""}`}
type="button"
onClick={() => onRun(kind)}
disabled={busyKind !== null || disabled}
title={`${label} for ${symbol}`}
>
{busyKind === kind ? "Running" : label}
</button>
);
};
export function SmartMoneyCopilotPanel({
event,
flowPacket,
evidencePrints,
relatedPackets,
}: {
event: SmartMoneyEvent;
flowPacket: FlowPacket | null;
evidencePrints: OptionPrint[];
relatedPackets: FlowPacket[];
}) {
const { bridgeAvailable, shellAvailable, state, runTask } = useDesktopAi();
const [busyKind, setBusyKind] = useState<IslandflowAiTaskKind | null>(null);
const [activeTaskId, setActiveTaskId] = useState<string | null>(null);
const [taskError, setTaskError] = useState<string | null>(null);
const disabledCopy = requireDesktopActionCopy(
shellAvailable,
bridgeAvailable,
state.account.loggedIn,
);
const actionsDisabled = !bridgeAvailable || !state.account.loggedIn;
const handleRun = async (kind: IslandflowAiTaskKind) => {
setBusyKind(kind);
setTaskError(null);
try {
const result = await runTask({
kind: kind as
| "smart-money-explain"
| "smart-money-skeptic"
| "smart-money-burst-summary"
| "watchlist-synthesis",
context: {
event,
flowPacket,
evidencePrints,
relatedPackets,
},
});
setActiveTaskId(result.taskId);
} catch (error) {
setTaskError(error instanceof Error ? error.message : String(error));
} finally {
setBusyKind(null);
}
};
return (
<div className="copilot-inline-panel">
<div className="copilot-inline-head">
<div>
<div className="copilot-list-title">Analyst Copilot</div>
<p className="copilot-note">
Structured interpretation only, the deterministic classifier remains
the source of truth.
</p>
</div>
<Link className="terminal-button" href="/settings">
AI settings
</Link>
</div>
<div className="copilot-action-grid">
<SmartMoneyTaskButton
label="Explain"
kind="smart-money-explain"
symbol={event.underlying_id}
disabled={actionsDisabled}
busyKind={busyKind}
onRun={(kind) => void handleRun(kind)}
/>
<SmartMoneyTaskButton
label="Counter-thesis"
kind="smart-money-skeptic"
symbol={event.underlying_id}
disabled={actionsDisabled}
busyKind={busyKind}
onRun={(kind) => void handleRun(kind)}
/>
<SmartMoneyTaskButton
label="Burst summary"
kind="smart-money-burst-summary"
symbol={event.underlying_id}
disabled={actionsDisabled}
busyKind={busyKind}
onRun={(kind) => void handleRun(kind)}
/>
<SmartMoneyTaskButton
label="Watchlist"
kind="watchlist-synthesis"
symbol={event.underlying_id}
disabled={actionsDisabled}
busyKind={busyKind}
onRun={(kind) => void handleRun(kind)}
/>
</div>
{disabledCopy ? <p className="copilot-note">{disabledCopy}</p> : null}
{taskError ? <p className="copilot-error">{taskError}</p> : null}
<TaskOutput
taskId={activeTaskId}
emptyMessage="Run an explanation, skepticism pass, burst summary, or watchlist synthesis to see the result here."
/>
</div>
);
}
export function ReplayCopilotPanel({
ticker,
flowFilters,
alerts,
smartMoneyEvents,
classifierHits,
flowPackets,
optionPrints,
}: {
ticker: string | null;
flowFilters: OptionFlowFilters;
alerts: AlertEvent[];
smartMoneyEvents: SmartMoneyEvent[];
classifierHits: ClassifierHitEvent[];
flowPackets: FlowPacket[];
optionPrints: OptionPrint[];
}) {
const { bridgeAvailable, shellAvailable, state, runTask } = useDesktopAi();
const [activeTaskId, setActiveTaskId] = useState<string | null>(null);
const [busy, setBusy] = useState(false);
const [taskError, setTaskError] = useState<string | null>(null);
const disabledCopy = requireDesktopActionCopy(
shellAvailable,
bridgeAvailable,
state.account.loggedIn,
);
const actionsDisabled = busy || !bridgeAvailable || !state.account.loggedIn;
const handleRun = async () => {
setBusy(true);
setTaskError(null);
try {
const result = await runTask({
kind: "replay-postmortem",
context: {
ticker,
flowFilters,
alerts,
smartMoneyEvents,
classifierHits,
flowPackets,
optionPrints,
},
});
setActiveTaskId(result.taskId);
} catch (error) {
setTaskError(error instanceof Error ? error.message : String(error));
} finally {
setBusy(false);
}
};
return (
<CopilotPane
title="Replay postmortem"
eyebrow="Structured recap"
actions={
<>
<Link className="terminal-button" href="/settings">
AI settings
</Link>
<button
className="terminal-button terminal-button-primary"
type="button"
onClick={() => void handleRun()}
disabled={actionsDisabled}
>
{busy ? "Running" : "Generate postmortem"}
</button>
</>
}
>
<p className="copilot-note">
Copilot uses the current replay slice only: ticker scope, flow filters,
visible alerts, classifier hits, packets, and option prints.
</p>
{disabledCopy ? <p className="copilot-note">{disabledCopy}</p> : null}
{taskError ? <p className="copilot-error">{taskError}</p> : null}
<TaskOutput
taskId={activeTaskId}
emptyMessage="Generate a replay postmortem to capture the cleanest read from the current session slice."
/>
</CopilotPane>
);
}
export function ScreenCompilerPanel({
currentFilters,
onApplyFilters,
}: {
currentFilters: OptionFlowFilters;
onApplyFilters: (next: OptionFlowFilters) => void;
}) {
const { bridgeAvailable, shellAvailable, state, runTask } = useDesktopAi();
const [prompt, setPrompt] = useState("");
const [activeTaskId, setActiveTaskId] = useState<string | null>(null);
const [busy, setBusy] = useState(false);
const [taskError, setTaskError] = useState<string | null>(null);
const activeTask = useMemo(
() => findTask(state.tasks, activeTaskId),
[state.tasks, activeTaskId],
);
const disabledCopy = requireDesktopActionCopy(
shellAvailable,
bridgeAvailable,
state.account.loggedIn,
);
const actionsDisabled = busy || !bridgeAvailable || !state.account.loggedIn;
const handleCompile = async () => {
const trimmedPrompt = prompt.trim();
if (!trimmedPrompt) {
setTaskError("Write a screen request first.");
return;
}
setBusy(true);
setTaskError(null);
try {
const result = await runTask({
kind: "screen-compile",
context: {
prompt: trimmedPrompt,
currentFilters,
},
});
setActiveTaskId(result.taskId);
} catch (error) {
setTaskError(error instanceof Error ? error.message : String(error));
} finally {
setBusy(false);
}
};
const compiledFilters = activeTask?.compiledScreen?.compiledFilters ?? null;
return (
<CopilotPane
title="Natural-language screens"
eyebrow="Tape workflow"
actions={
<>
<Link className="terminal-button" href="/settings">
AI settings
</Link>
<button
className="terminal-button terminal-button-primary"
type="button"
onClick={() => void handleCompile()}
disabled={actionsDisabled}
>
{busy ? "Compiling" : "Compile screen"}
</button>
</>
}
>
<div className="copilot-inline-form">
<label className="copilot-field">
<span className="copilot-field-label">Prompt</span>
<textarea
className="copilot-textarea"
rows={4}
value={prompt}
onChange={(event) => setPrompt(event.target.value)}
placeholder="High-notional single-name call buying near the ask, ignore ETFs, keep it signal-only."
/>
</label>
<div className="copilot-current-filters">
<div className="copilot-list-title">Current filter baseline</div>
<pre className="copilot-json-block">
{JSON.stringify(currentFilters, null, 2)}
</pre>
</div>
</div>
{disabledCopy ? <p className="copilot-note">{disabledCopy}</p> : null}
{taskError ? <p className="copilot-error">{taskError}</p> : null}
{compiledFilters ? (
<div className="copilot-apply-row">
<button
className="terminal-button"
type="button"
onClick={() => onApplyFilters(compiledFilters)}
>
Apply compiled filters
</button>
</div>
) : null}
<TaskOutput
taskId={activeTaskId}
emptyMessage="Compile a natural-language screen to preview the translated filter set and rationale."
/>
</CopilotPane>
);
}