improve ai alert copilot ux
This commit is contained in:
parent
ebdc4ab8e6
commit
32e965d782
15 changed files with 931 additions and 15 deletions
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import Link from "next/link";
|
||||
import { useMemo, useState, type ReactNode } from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import type {
|
||||
AlertEvent,
|
||||
ClassifierHitEvent,
|
||||
|
|
@ -11,6 +12,8 @@ import type {
|
|||
IslandflowAiRateLimitSnapshot,
|
||||
IslandflowAiReasoningEffort,
|
||||
IslandflowAiTaskKind,
|
||||
IslandflowAiTaskSnapshot,
|
||||
IslandflowAiTaskStatus,
|
||||
OptionFlowFilters,
|
||||
OptionPrint,
|
||||
SmartMoneyEvent,
|
||||
|
|
@ -53,6 +56,55 @@ const formatPercent = (value: number): string => `${Math.round(value)}%`;
|
|||
|
||||
const getTaskStatusLabel = (value: string): string => humanizeValue(value);
|
||||
|
||||
type CopilotTaskCache = Record<string, string>;
|
||||
|
||||
const RUNNING_TASK_STATUSES: IslandflowAiTaskStatus[] = ["queued", "running"];
|
||||
|
||||
export const createCopilotTaskCacheKey = (
|
||||
kind: IslandflowAiTaskKind,
|
||||
contextKey: string,
|
||||
): string => `${kind}:${contextKey}`;
|
||||
|
||||
export const getCachedCopilotTaskId = (
|
||||
cache: CopilotTaskCache,
|
||||
kind: IslandflowAiTaskKind,
|
||||
contextKey: string,
|
||||
): string | null => cache[createCopilotTaskCacheKey(kind, contextKey)] ?? null;
|
||||
|
||||
export const shouldShowCopilotRegenerate = (
|
||||
task: Pick<IslandflowAiTaskSnapshot, "status" | "text" | "compiledScreen"> | null,
|
||||
): boolean =>
|
||||
Boolean(
|
||||
task &&
|
||||
task.status === "completed" &&
|
||||
(task.text.trim().length > 0 || task.compiledScreen),
|
||||
);
|
||||
|
||||
export const isCopilotTaskRunning = (
|
||||
task: Pick<IslandflowAiTaskSnapshot, "status"> | null,
|
||||
): boolean => Boolean(task && RUNNING_TASK_STATUSES.includes(task.status));
|
||||
|
||||
export const getCopilotTaskSurfaceState = (
|
||||
task: Pick<IslandflowAiTaskSnapshot, "status" | "text" | "compiledScreen" | "error"> | null,
|
||||
): "empty" | "running" | "completed" | "failed" | "cancelled" => {
|
||||
if (!task) {
|
||||
return "empty";
|
||||
}
|
||||
if (RUNNING_TASK_STATUSES.includes(task.status)) {
|
||||
return "running";
|
||||
}
|
||||
if (task.status === "completed") {
|
||||
return "completed";
|
||||
}
|
||||
if (task.status === "failed") {
|
||||
return "failed";
|
||||
}
|
||||
return "cancelled";
|
||||
};
|
||||
|
||||
const getSmartMoneyContextKey = (event: SmartMoneyEvent): string =>
|
||||
[event.underlying_id, event.source_ts, event.event_id].join(":");
|
||||
|
||||
type DesktopAiSettingsNotice = {
|
||||
title: string;
|
||||
body: string;
|
||||
|
|
@ -324,19 +376,26 @@ const RateLimitBoard = ({
|
|||
const TaskOutput = ({
|
||||
taskId,
|
||||
emptyMessage,
|
||||
onCancel,
|
||||
}: {
|
||||
taskId: string | null;
|
||||
emptyMessage: string;
|
||||
onCancel?: (taskId: string) => void;
|
||||
}) => {
|
||||
const { state } = useDesktopAi();
|
||||
const task = findTask(state.tasks, taskId);
|
||||
const surfaceState = getCopilotTaskSurfaceState(task);
|
||||
|
||||
if (!task) {
|
||||
return <p className="copilot-empty">{emptyMessage}</p>;
|
||||
return (
|
||||
<div className="copilot-task-output copilot-task-output-empty">
|
||||
<p className="copilot-empty">{emptyMessage}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="copilot-task-output" aria-live="polite">
|
||||
<div className={`copilot-task-output state-${surfaceState}`} aria-live="polite">
|
||||
<div className="copilot-task-head">
|
||||
<div>
|
||||
<strong>{task.title}</strong>
|
||||
|
|
@ -348,8 +407,34 @@ const TaskOutput = ({
|
|||
{getTaskStatusLabel(task.status)}
|
||||
</span>
|
||||
</div>
|
||||
{surfaceState === "running" ? (
|
||||
<div className="copilot-task-running-row">
|
||||
<span className="copilot-spinner" aria-hidden="true" />
|
||||
<p className="copilot-note">
|
||||
{task.status === "queued"
|
||||
? "Queued with the desktop Copilot bridge."
|
||||
: "Generating analysis as deltas arrive."}
|
||||
</p>
|
||||
{onCancel ? (
|
||||
<button
|
||||
className="terminal-button"
|
||||
type="button"
|
||||
onClick={() => onCancel(task.taskId)}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
{task.error ? <p className="copilot-error">{task.error}</p> : null}
|
||||
{task.text ? <pre className="copilot-task-text">{task.text}</pre> : null}
|
||||
{task.status === "cancelled" && !task.text ? (
|
||||
<p className="copilot-note">This Copilot task was cancelled locally.</p>
|
||||
) : null}
|
||||
{task.text ? (
|
||||
<div className="copilot-markdown">
|
||||
<ReactMarkdown>{task.text}</ReactMarkdown>
|
||||
</div>
|
||||
) : null}
|
||||
{task.compiledScreen ? (
|
||||
<CompiledScreenResult compiled={task.compiledScreen} />
|
||||
) : null}
|
||||
|
|
@ -880,6 +965,8 @@ const SmartMoneyTaskButton = ({
|
|||
symbol,
|
||||
disabled,
|
||||
busyKind,
|
||||
selected,
|
||||
cached,
|
||||
onRun,
|
||||
}: {
|
||||
label: string;
|
||||
|
|
@ -887,17 +974,19 @@ const SmartMoneyTaskButton = ({
|
|||
symbol: string;
|
||||
disabled: boolean;
|
||||
busyKind: IslandflowAiTaskKind | null;
|
||||
selected: boolean;
|
||||
cached: boolean;
|
||||
onRun: (kind: IslandflowAiTaskKind) => void;
|
||||
}) => {
|
||||
return (
|
||||
<button
|
||||
className={`terminal-button${kind === "smart-money-explain" ? " terminal-button-primary" : ""}`}
|
||||
className={`terminal-button${kind === "smart-money-explain" ? " terminal-button-primary" : ""}${selected ? " is-active" : ""}`}
|
||||
type="button"
|
||||
onClick={() => onRun(kind)}
|
||||
disabled={busyKind !== null || disabled}
|
||||
title={`${label} for ${symbol}`}
|
||||
>
|
||||
{busyKind === kind ? "Running" : label}
|
||||
{busyKind === kind ? "Running" : cached ? `${label} cached` : label}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
|
@ -913,10 +1002,17 @@ export function SmartMoneyCopilotPanel({
|
|||
evidencePrints: OptionPrint[];
|
||||
relatedPackets: FlowPacket[];
|
||||
}) {
|
||||
const { bridgeAvailable, shellAvailable, state, runTask } = useDesktopAi();
|
||||
const { bridgeAvailable, shellAvailable, state, runTask, cancelTask } = useDesktopAi();
|
||||
const [busyKind, setBusyKind] = useState<IslandflowAiTaskKind | null>(null);
|
||||
const [selectedKind, setSelectedKind] = useState<IslandflowAiTaskKind>("smart-money-explain");
|
||||
const [activeTaskId, setActiveTaskId] = useState<string | null>(null);
|
||||
const [taskCache, setTaskCache] = useState<CopilotTaskCache>({});
|
||||
const [taskError, setTaskError] = useState<string | null>(null);
|
||||
const contextKey = useMemo(() => getSmartMoneyContextKey(event), [event]);
|
||||
const activeTask = useMemo(
|
||||
() => findTask(state.tasks, activeTaskId),
|
||||
[state.tasks, activeTaskId],
|
||||
);
|
||||
const disabledCopy = requireDesktopActionCopy(
|
||||
shellAvailable,
|
||||
bridgeAvailable,
|
||||
|
|
@ -924,7 +1020,16 @@ export function SmartMoneyCopilotPanel({
|
|||
);
|
||||
const actionsDisabled = !bridgeAvailable || !state.account.loggedIn;
|
||||
|
||||
const handleRun = async (kind: IslandflowAiTaskKind) => {
|
||||
const handleRun = async (kind: IslandflowAiTaskKind, options?: { regenerate?: boolean }) => {
|
||||
setSelectedKind(kind);
|
||||
const cachedTaskId = getCachedCopilotTaskId(taskCache, kind, contextKey);
|
||||
const cachedTask = findTask(state.tasks, cachedTaskId);
|
||||
if (!options?.regenerate && cachedTask) {
|
||||
setActiveTaskId(cachedTask.taskId);
|
||||
setTaskError(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setBusyKind(kind);
|
||||
setTaskError(null);
|
||||
try {
|
||||
|
|
@ -942,6 +1047,10 @@ export function SmartMoneyCopilotPanel({
|
|||
},
|
||||
});
|
||||
setActiveTaskId(result.taskId);
|
||||
setTaskCache((current) => ({
|
||||
...current,
|
||||
[createCopilotTaskCacheKey(kind, contextKey)]: result.taskId,
|
||||
}));
|
||||
} catch (error) {
|
||||
setTaskError(error instanceof Error ? error.message : String(error));
|
||||
} finally {
|
||||
|
|
@ -949,6 +1058,17 @@ export function SmartMoneyCopilotPanel({
|
|||
}
|
||||
};
|
||||
|
||||
const handleCancel = async (taskId: string) => {
|
||||
setTaskError(null);
|
||||
try {
|
||||
await cancelTask(taskId);
|
||||
} catch (error) {
|
||||
setTaskError(error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
};
|
||||
|
||||
const canRegenerate = shouldShowCopilotRegenerate(activeTask);
|
||||
|
||||
return (
|
||||
<div className="copilot-inline-panel">
|
||||
<div className="copilot-inline-head">
|
||||
|
|
@ -970,6 +1090,8 @@ export function SmartMoneyCopilotPanel({
|
|||
symbol={event.underlying_id}
|
||||
disabled={actionsDisabled}
|
||||
busyKind={busyKind}
|
||||
selected={selectedKind === "smart-money-explain"}
|
||||
cached={Boolean(getCachedCopilotTaskId(taskCache, "smart-money-explain", contextKey))}
|
||||
onRun={(kind) => void handleRun(kind)}
|
||||
/>
|
||||
<SmartMoneyTaskButton
|
||||
|
|
@ -978,6 +1100,8 @@ export function SmartMoneyCopilotPanel({
|
|||
symbol={event.underlying_id}
|
||||
disabled={actionsDisabled}
|
||||
busyKind={busyKind}
|
||||
selected={selectedKind === "smart-money-skeptic"}
|
||||
cached={Boolean(getCachedCopilotTaskId(taskCache, "smart-money-skeptic", contextKey))}
|
||||
onRun={(kind) => void handleRun(kind)}
|
||||
/>
|
||||
<SmartMoneyTaskButton
|
||||
|
|
@ -986,6 +1110,8 @@ export function SmartMoneyCopilotPanel({
|
|||
symbol={event.underlying_id}
|
||||
disabled={actionsDisabled}
|
||||
busyKind={busyKind}
|
||||
selected={selectedKind === "smart-money-burst-summary"}
|
||||
cached={Boolean(getCachedCopilotTaskId(taskCache, "smart-money-burst-summary", contextKey))}
|
||||
onRun={(kind) => void handleRun(kind)}
|
||||
/>
|
||||
<SmartMoneyTaskButton
|
||||
|
|
@ -994,14 +1120,29 @@ export function SmartMoneyCopilotPanel({
|
|||
symbol={event.underlying_id}
|
||||
disabled={actionsDisabled}
|
||||
busyKind={busyKind}
|
||||
selected={selectedKind === "watchlist-synthesis"}
|
||||
cached={Boolean(getCachedCopilotTaskId(taskCache, "watchlist-synthesis", contextKey))}
|
||||
onRun={(kind) => void handleRun(kind)}
|
||||
/>
|
||||
</div>
|
||||
{canRegenerate ? (
|
||||
<div className="copilot-apply-row">
|
||||
<button
|
||||
className="terminal-button"
|
||||
type="button"
|
||||
onClick={() => void handleRun(selectedKind, { regenerate: true })}
|
||||
disabled={actionsDisabled || busyKind !== null}
|
||||
>
|
||||
Regenerate
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
{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."
|
||||
onCancel={(taskId) => void handleCancel(taskId)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -1024,10 +1165,14 @@ export function ReplayCopilotPanel({
|
|||
flowPackets: FlowPacket[];
|
||||
optionPrints: OptionPrint[];
|
||||
}) {
|
||||
const { bridgeAvailable, shellAvailable, state, runTask } = useDesktopAi();
|
||||
const { bridgeAvailable, shellAvailable, state, runTask, cancelTask } = useDesktopAi();
|
||||
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,
|
||||
|
|
@ -1059,6 +1204,17 @@ export function ReplayCopilotPanel({
|
|||
}
|
||||
};
|
||||
|
||||
const handleCancel = async (taskId: string) => {
|
||||
setTaskError(null);
|
||||
try {
|
||||
await cancelTask(taskId);
|
||||
} catch (error) {
|
||||
setTaskError(error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
};
|
||||
|
||||
const canRegenerate = shouldShowCopilotRegenerate(activeTask);
|
||||
|
||||
return (
|
||||
<CopilotPane
|
||||
title="Replay postmortem"
|
||||
|
|
@ -1074,7 +1230,7 @@ export function ReplayCopilotPanel({
|
|||
onClick={() => void handleRun()}
|
||||
disabled={actionsDisabled}
|
||||
>
|
||||
{busy ? "Running" : "Generate postmortem"}
|
||||
{busy ? "Running" : canRegenerate ? "Regenerate" : "Generate postmortem"}
|
||||
</button>
|
||||
</>
|
||||
}
|
||||
|
|
@ -1088,6 +1244,7 @@ export function ReplayCopilotPanel({
|
|||
<TaskOutput
|
||||
taskId={activeTaskId}
|
||||
emptyMessage="Generate a replay postmortem to capture the cleanest read from the current session slice."
|
||||
onCancel={(taskId) => void handleCancel(taskId)}
|
||||
/>
|
||||
</CopilotPane>
|
||||
);
|
||||
|
|
@ -1100,7 +1257,7 @@ export function ScreenCompilerPanel({
|
|||
currentFilters: OptionFlowFilters;
|
||||
onApplyFilters: (next: OptionFlowFilters) => void;
|
||||
}) {
|
||||
const { bridgeAvailable, shellAvailable, state, runTask } = useDesktopAi();
|
||||
const { bridgeAvailable, shellAvailable, state, runTask, cancelTask } = useDesktopAi();
|
||||
const [prompt, setPrompt] = useState("");
|
||||
const [activeTaskId, setActiveTaskId] = useState<string | null>(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
|
@ -1141,7 +1298,17 @@ export function ScreenCompilerPanel({
|
|||
}
|
||||
};
|
||||
|
||||
const handleCancel = async (taskId: string) => {
|
||||
setTaskError(null);
|
||||
try {
|
||||
await cancelTask(taskId);
|
||||
} catch (error) {
|
||||
setTaskError(error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
};
|
||||
|
||||
const compiledFilters = activeTask?.compiledScreen?.compiledFilters ?? null;
|
||||
const canRegenerate = shouldShowCopilotRegenerate(activeTask);
|
||||
|
||||
return (
|
||||
<CopilotPane
|
||||
|
|
@ -1158,7 +1325,7 @@ export function ScreenCompilerPanel({
|
|||
onClick={() => void handleCompile()}
|
||||
disabled={actionsDisabled}
|
||||
>
|
||||
{busy ? "Compiling" : "Compile screen"}
|
||||
{busy ? "Compiling" : canRegenerate ? "Regenerate" : "Compile screen"}
|
||||
</button>
|
||||
</>
|
||||
}
|
||||
|
|
@ -1197,6 +1364,7 @@ export function ScreenCompilerPanel({
|
|||
<TaskOutput
|
||||
taskId={activeTaskId}
|
||||
emptyMessage="Compile a natural-language screen to preview the translated filter set and rationale."
|
||||
onCancel={(taskId) => void handleCancel(taskId)}
|
||||
/>
|
||||
</CopilotPane>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -10,7 +10,11 @@ import {
|
|||
getDesktopAiModelSelectLabel,
|
||||
getDesktopAiProfileBadgeLabel,
|
||||
getDesktopAiSettingsBridgeNotice,
|
||||
createCopilotTaskCacheKey,
|
||||
getCachedCopilotTaskId,
|
||||
getCopilotTaskSurfaceState,
|
||||
requireDesktopActionCopy,
|
||||
shouldShowCopilotRegenerate,
|
||||
} from "./desktop-ai-panels";
|
||||
|
||||
describe("desktop ai runtime detection", () => {
|
||||
|
|
@ -41,6 +45,7 @@ describe("desktop ai runtime detection", () => {
|
|||
logout: async () => {},
|
||||
updatePreferences: async () => {},
|
||||
runTask: async () => ({ taskId: "task-1" }),
|
||||
cancelTask: async () => {},
|
||||
subscribe: () => () => {},
|
||||
},
|
||||
},
|
||||
|
|
@ -146,3 +151,49 @@ describe("desktop ai settings copy", () => {
|
|||
).toBe("Selected");
|
||||
});
|
||||
});
|
||||
|
||||
describe("desktop ai panel task helpers", () => {
|
||||
it("looks up cached task ids by action kind and context key", () => {
|
||||
const key = createCopilotTaskCacheKey("smart-money-explain", "AAPL:1:event-1");
|
||||
|
||||
expect(key).toBe("smart-money-explain:AAPL:1:event-1");
|
||||
expect(
|
||||
getCachedCopilotTaskId(
|
||||
{ [key]: "task-1" },
|
||||
"smart-money-explain",
|
||||
"AAPL:1:event-1",
|
||||
),
|
||||
).toBe("task-1");
|
||||
});
|
||||
|
||||
it("only shows regenerate once a completed result has output", () => {
|
||||
expect(
|
||||
shouldShowCopilotRegenerate({
|
||||
status: "completed",
|
||||
text: "## Thesis",
|
||||
compiledScreen: null,
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
shouldShowCopilotRegenerate({
|
||||
status: "running",
|
||||
text: "partial",
|
||||
compiledScreen: null,
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(shouldShowCopilotRegenerate(null)).toBe(false);
|
||||
});
|
||||
|
||||
it("maps queued, running, failed, and cancelled tasks to result surface states", () => {
|
||||
expect(getCopilotTaskSurfaceState(null)).toBe("empty");
|
||||
expect(getCopilotTaskSurfaceState({ status: "queued", text: "", compiledScreen: null, error: null })).toBe(
|
||||
"running",
|
||||
);
|
||||
expect(getCopilotTaskSurfaceState({ status: "running", text: "", compiledScreen: null, error: null })).toBe(
|
||||
"running",
|
||||
);
|
||||
expect(getCopilotTaskSurfaceState({ status: "cancelled", text: "", compiledScreen: null, error: null })).toBe(
|
||||
"cancelled",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ type DesktopAiBridge = {
|
|||
next: Partial<{ model: string | null; reasoningEffort: IslandflowAiReasoningEffort | null }>
|
||||
) => Promise<void>;
|
||||
runTask: (request: IslandflowAiTaskRequest) => Promise<{ taskId: string }>;
|
||||
cancelTask: (taskId: string) => Promise<void>;
|
||||
subscribe: (listener: (state: IslandflowAiState) => void) => () => void;
|
||||
};
|
||||
};
|
||||
|
|
@ -53,6 +54,7 @@ type DesktopAiContextValue = {
|
|||
next: Partial<{ model: string | null; reasoningEffort: IslandflowAiReasoningEffort | null }>
|
||||
) => Promise<void>;
|
||||
runTask: (request: IslandflowAiTaskRequest) => Promise<{ taskId: string }>;
|
||||
cancelTask: (taskId: string) => Promise<void>;
|
||||
};
|
||||
|
||||
const BRIDGE_POLL_INTERVAL_MS = 250;
|
||||
|
|
@ -261,7 +263,8 @@ export function DesktopAiProvider({ children }: { children: ReactNode }) {
|
|||
cancelLogin: bridge?.ai.cancelLogin ?? rejectDesktopOnly,
|
||||
logout: bridge?.ai.logout ?? rejectDesktopOnly,
|
||||
updatePreferences: bridge?.ai.updatePreferences ?? rejectDesktopOnly,
|
||||
runTask: bridge?.ai.runTask ?? rejectDesktopOnly
|
||||
runTask: bridge?.ai.runTask ?? rejectDesktopOnly,
|
||||
cancelTask: bridge?.ai.cancelTask ?? rejectDesktopOnly
|
||||
}),
|
||||
[bridge, shellAvailable, state]
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1182,6 +1182,7 @@ h3 {
|
|||
.copilot-empty,
|
||||
.copilot-device-code,
|
||||
.copilot-task-text,
|
||||
.copilot-markdown,
|
||||
.copilot-json-block {
|
||||
margin: 0;
|
||||
}
|
||||
|
|
@ -1317,6 +1318,45 @@ h3 {
|
|||
background: oklch(0.12 0.01 250 / 0.72);
|
||||
}
|
||||
|
||||
.copilot-task-output-empty {
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.copilot-task-running-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid oklch(0.78 0.12 74 / 0.24);
|
||||
border-radius: 10px;
|
||||
background: oklch(0.78 0.12 74 / 0.06);
|
||||
}
|
||||
|
||||
.copilot-task-running-row .terminal-button {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.copilot-spinner {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex: 0 0 auto;
|
||||
border-radius: 999px;
|
||||
border: 2px solid oklch(0.88 0.06 76 / 0.3);
|
||||
border-top-color: oklch(0.88 0.06 76);
|
||||
animation: copilot-spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes copilot-spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.terminal-button.is-active {
|
||||
border-color: var(--border-strong);
|
||||
box-shadow: 0 0 0 1px oklch(0.78 0.12 74 / 0.22);
|
||||
}
|
||||
|
||||
.copilot-task-text,
|
||||
.copilot-json-block,
|
||||
.copilot-device-code {
|
||||
|
|
@ -1329,6 +1369,78 @@ h3 {
|
|||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.copilot-markdown {
|
||||
padding: 14px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--border);
|
||||
background: oklch(0.1 0.009 250 / 0.92);
|
||||
color: var(--text);
|
||||
font-family: var(--font-sans), sans-serif;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.copilot-markdown > :first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.copilot-markdown > :last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.copilot-markdown h1,
|
||||
.copilot-markdown h2,
|
||||
.copilot-markdown h3,
|
||||
.copilot-markdown h4 {
|
||||
margin: 1.1em 0 0.45em;
|
||||
color: var(--text);
|
||||
font-family: var(--font-display), sans-serif;
|
||||
line-height: 1.18;
|
||||
}
|
||||
|
||||
.copilot-markdown p,
|
||||
.copilot-markdown ul,
|
||||
.copilot-markdown ol,
|
||||
.copilot-markdown blockquote,
|
||||
.copilot-markdown pre {
|
||||
margin: 0.75em 0;
|
||||
}
|
||||
|
||||
.copilot-markdown ul,
|
||||
.copilot-markdown ol {
|
||||
padding-left: 1.25rem;
|
||||
}
|
||||
|
||||
.copilot-markdown li + li {
|
||||
margin-top: 0.35em;
|
||||
}
|
||||
|
||||
.copilot-markdown code {
|
||||
font-family: var(--font-mono), monospace;
|
||||
font-size: 0.92em;
|
||||
color: oklch(0.88 0.06 76);
|
||||
}
|
||||
|
||||
.copilot-markdown :not(pre) > code {
|
||||
padding: 0.1rem 0.32rem;
|
||||
border-radius: 6px;
|
||||
background: oklch(0.97 0.008 250 / 0.08);
|
||||
border: 1px solid oklch(0.72 0.012 250 / 0.14);
|
||||
}
|
||||
|
||||
.copilot-markdown pre {
|
||||
overflow-x: auto;
|
||||
padding: 12px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--border);
|
||||
background: oklch(0.07 0.008 250 / 0.9);
|
||||
}
|
||||
|
||||
.copilot-markdown blockquote {
|
||||
padding-left: 12px;
|
||||
border-left: 3px solid var(--accent);
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.copilot-device-code {
|
||||
font-size: clamp(1.3rem, 2vw, 1.7rem);
|
||||
letter-spacing: 0.18em;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue