Files
ai-agent/frontend/src/lib/visibility.ts
Gabriel Vidal cb30999711 feat(viewer): inline widgets for WebSearch, WebFetch, ToolSearch & Artifact tool calls
WebSearch renders favicon'd link chips + count + collapsible analysis;
WebFetch a favicon/host card; ToolSearch matched-tool chips (backend now
flattens tool_reference blocks); Artifact a published-page link card.
New 'Web & search widgets' visibility switch gates the search-flavoured ones.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 19:27:21 +02:00

204 lines
6.2 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useMemo } from "react";
import { create } from "zustand";
import { createJSONStorage, persist } from "zustand/middleware";
import { serverStorage } from "./serverStorage";
/**
* What the conversation page renders. Every switch is ON by default; the store
* only records what the user has *hidden*, so a new toggle added later starts
* visible for everyone without a migration.
*
* The state is persisted server-side (like Settings), so the reading setup —
* "no costs, no timestamps, no animations" — follows the user across devices.
*/
export type VisKey =
// metadata carried by a message / tool call
| "cost"
| "tokens"
| "duration"
| "timestamp"
| "model"
| "convModel"
| "convEffort"
| "toolId"
| "resultSize"
| "diffStat"
| "contextSplit"
| "footprint"
// content in the thread
| "thinking"
| "toolCalls"
| "bashCalls"
| "toolOutput"
| "widgets"
| "searchWidgets"
| "forms"
| "errors"
| "forkBanner"
| "msgNav"
// chrome floating over the thread
| "taskPanel"
| "fastForward"
| "deployGizmo"
| "contextGizmo"
| "costCurve"
| "animations"
| "graphPhysics"
| "avatar"
| "avatarDrag"
| "railAvatars";
export interface VisItem {
key: VisKey;
label: string;
hint: string;
}
export interface VisGroup {
title: string;
items: VisItem[];
}
export const VIS_GROUPS: VisGroup[] = [
{
title: "Metadata",
items: [
{ key: "cost", label: "Price", hint: "per-turn dollar cost" },
{ key: "tokens", label: "Tokens", hint: "per-turn token count" },
{ key: "duration", label: "Duration", hint: "how long a turn or tool call took" },
{ key: "timestamp", label: "Timestamp", hint: "date + time on each message" },
{ key: "model", label: "Model", hint: "the model that produced the turn" },
{
key: "convModel",
label: "Model tags",
hint: "the model(s) this conversation ran on, in the header",
},
{
key: "convEffort",
label: "Effort tags",
hint: "the --effort level(s) this conversation's turns ran at",
},
{ key: "toolId", label: "Tool ID", hint: "the tool_use id of a call" },
{ key: "resultSize", label: "Result size", hint: "lines of output a tool returned" },
{ key: "diffStat", label: "Diff stat", hint: "+N M on file edits" },
{
key: "contextSplit",
label: "Context split",
hint: "context vs message tokens on your first prompt",
},
{
key: "footprint",
label: "Footprint",
hint: "estimated carbon, water & energy of the conversation",
},
],
},
{
title: "Content",
items: [
{ key: "thinking", label: "Thinking blocks", hint: "the model's reasoning" },
{ key: "toolCalls", label: "Tool calls", hint: "every tool card in the thread" },
{ key: "bashCalls", label: "Run-command calls", hint: "Bash / shell tool cards" },
{ key: "toolOutput", label: "Tool output", hint: "the result body inside a card" },
{ key: "widgets", label: "Rich widgets", hint: "screenshots, images, videos, plans, notifications" },
{
key: "searchWidgets",
label: "Web & search widgets",
hint: "web-search link chips, fetched-page cards, tool-search matches",
},
{ key: "forms", label: "Form cards", hint: "interactive ask-form questions + their answers" },
{ key: "errors", label: "Error states", hint: "red styling + error dots" },
{ key: "forkBanner", label: "Fork origin", hint: "banner linking a forked conversation to its source" },
{
key: "msgNav",
label: "Message jumper",
hint: "up/down arrows above your messages, to hop turn by turn",
},
],
},
{
title: "Gizmos",
items: [
{ key: "taskPanel", label: "Task panel", hint: "floating task checklist" },
{ key: "fastForward", label: "Fast-forward", hint: "hold-to-3× button" },
{
key: "deployGizmo",
label: "Deploy preview",
hint: "open the deployed site, bottom-right",
},
{
key: "contextGizmo",
label: "Context meter",
hint: "how full the model's context window is, bottom-right",
},
{
key: "costCurve",
label: "Accumulated cost",
hint: "running-total curve + $1/$5/$10 guides over the cost-growth bars",
},
{ key: "animations", label: "Animations", hint: "typewriter + staggered reveal" },
{
key: "graphPhysics",
label: "Graph physics",
hint: "live force simulation on the graph view",
},
{
key: "avatar",
label: "Agent avatar",
hint: "Hemp Henry roaming the bottom of the screen",
},
{
key: "avatarDrag",
label: "Pick up avatar",
hint: "drag the roaming avatar around — it dangles while held and drops back to the floor",
},
{
key: "railAvatars",
label: "Running avatars",
hint: "one avatar per running conversation on the home page, following its state",
},
],
},
];
export const VIS_KEYS: VisKey[] = VIS_GROUPS.flatMap((g) => g.items.map((i) => i.key));
interface VisState {
/** Keys the user has switched off. Everything not listed is visible. */
hidden: VisKey[];
toggle: (k: VisKey) => void;
setAll: (visible: boolean) => void;
}
export const useVisibility = create<VisState>()(
persist(
(set) => ({
hidden: [],
toggle: (k) =>
set((s) => ({
hidden: s.hidden.includes(k) ? s.hidden.filter((x) => x !== k) : [...s.hidden, k],
})),
setAll: (visible) => set({ hidden: visible ? [] : [...VIS_KEYS] }),
}),
{
name: "ai-agent-visibility",
storage: createJSONStorage(() => serverStorage),
},
),
);
/** Every switch as a plain `key → visible` record (one store subscription). */
export function useVis(): Record<VisKey, boolean> {
const hidden = useVisibility((s) => s.hidden);
return useMemo(() => {
const out = {} as Record<VisKey, boolean>;
for (const k of VIS_KEYS) out[k] = !hidden.includes(k);
return out;
}, [hidden]);
}
/** A single switch, for components that only care about one. */
export function useVisible(k: VisKey): boolean {
return useVisibility((s) => !s.hidden.includes(k));
}