@horon-ai/react 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +168 -2
- package/package.json +50 -5
- package/src/assets/sidebar-photo.jpg +0 -0
- package/src/components/agent-activity.tsx +202 -0
- package/src/components/agent-lane.tsx +80 -0
- package/src/components/agent-lanes-timeline.tsx +312 -0
- package/src/components/agent-panel.tsx +121 -0
- package/src/components/agent-steering-panel.tsx +274 -0
- package/src/components/approval-card.tsx +89 -0
- package/src/components/avatar.tsx +104 -0
- package/src/components/badge.tsx +63 -0
- package/src/components/banner.tsx +103 -0
- package/src/components/bot-instructions-panel.tsx +365 -0
- package/src/components/bot-list-row.tsx +42 -0
- package/src/components/bot-memory-panel.tsx +229 -0
- package/src/components/bottom-sheet.tsx +140 -0
- package/src/components/breadcrumb-header.tsx +87 -0
- package/src/components/breadcrumb.tsx +86 -0
- package/src/components/button.tsx +146 -0
- package/src/components/calendar.tsx +410 -0
- package/src/components/card.tsx +79 -0
- package/src/components/chat-bubble.tsx +36 -0
- package/src/components/chat-controls.tsx +118 -0
- package/src/components/checkbox.tsx +73 -0
- package/src/components/chip.tsx +81 -0
- package/src/components/choice-card.tsx +55 -0
- package/src/components/code-panel.tsx +27 -0
- package/src/components/combobox.tsx +312 -0
- package/src/components/command-palette.tsx +191 -0
- package/src/components/composer.tsx +124 -0
- package/src/components/data-table.tsx +429 -0
- package/src/components/date-field.tsx +100 -0
- package/src/components/dialog.tsx +93 -0
- package/src/components/directory-card.tsx +59 -0
- package/src/components/document-extract-panel.tsx +398 -0
- package/src/components/empty-state.tsx +29 -0
- package/src/components/field.tsx +78 -0
- package/src/components/file-drop.tsx +89 -0
- package/src/components/filter-bar.tsx +83 -0
- package/src/components/flue-background.tsx +402 -0
- package/src/components/horon-icon.tsx +280 -0
- package/src/components/horon-wordmark.tsx +26 -0
- package/src/components/icon-button.tsx +61 -0
- package/src/components/input-group.tsx +112 -0
- package/src/components/input.tsx +72 -0
- package/src/components/integration-row.tsx +103 -0
- package/src/components/kbd.tsx +35 -0
- package/src/components/knowledge-base-panel.tsx +185 -0
- package/src/components/liquid-glass.tsx +24 -0
- package/src/components/list-detail.tsx +275 -0
- package/src/components/marketplace-card.tsx +97 -0
- package/src/components/mcp-server-row.tsx +122 -0
- package/src/components/menu.tsx +165 -0
- package/src/components/message-extras.tsx +156 -0
- package/src/components/modal.tsx +108 -0
- package/src/components/orchestrator-panel.tsx +175 -0
- package/src/components/page-header.tsx +48 -0
- package/src/components/plan-card.tsx +70 -0
- package/src/components/plan-step.tsx +111 -0
- package/src/components/plugin-row.tsx +128 -0
- package/src/components/progress.tsx +21 -0
- package/src/components/radio-group.tsx +84 -0
- package/src/components/research-run-panel.tsx +232 -0
- package/src/components/schedule.tsx +332 -0
- package/src/components/segmented-control.tsx +136 -0
- package/src/components/select.tsx +154 -0
- package/src/components/settings-list.tsx +99 -0
- package/src/components/sidebar.tsx +542 -0
- package/src/components/skeleton.tsx +75 -0
- package/src/components/slider.tsx +29 -0
- package/src/components/source-row.tsx +112 -0
- package/src/components/spinner.tsx +29 -0
- package/src/components/split-button.tsx +72 -0
- package/src/components/switch.tsx +79 -0
- package/src/components/tab-bar.tsx +90 -0
- package/src/components/tabs.tsx +162 -0
- package/src/components/task-row.tsx +159 -0
- package/src/components/textarea-field.tsx +127 -0
- package/src/components/textarea.tsx +47 -0
- package/src/components/toast.tsx +128 -0
- package/src/components/tool-call.tsx +224 -0
- package/src/components/tool-calls-header.tsx +65 -0
- package/src/components/tool-permissions.tsx +105 -0
- package/src/components/tooltip.tsx +33 -0
- package/src/components/top-bar.tsx +65 -0
- package/src/components/usage-meter.tsx +47 -0
- package/src/components/workspace-menu.tsx +77 -0
- package/src/index.ts +86 -0
- package/src/lib/cn.ts +25 -0
- package/src/lib/cursor-ghost.ts +244 -0
- package/src/lib/size.ts +10 -0
- package/src/lib/use-active-indicator.ts +60 -0
- package/src/styles/globals.css +564 -0
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import { CaretDown, Play } from "@phosphor-icons/react";
|
|
2
|
+
import { type ComponentProps, type KeyboardEvent, type ReactNode, useRef, useState } from "react";
|
|
3
|
+
|
|
4
|
+
import { cn } from "../lib/cn";
|
|
5
|
+
import { useActiveIndicator } from "../lib/use-active-indicator";
|
|
6
|
+
import { Kbd } from "./kbd";
|
|
7
|
+
|
|
8
|
+
type WithClass<T> = Omit<T, "className"> & { className?: string };
|
|
9
|
+
|
|
10
|
+
export type PanelPillTabsProps = {
|
|
11
|
+
items: { value: string; label: ReactNode }[];
|
|
12
|
+
value?: string;
|
|
13
|
+
defaultValue?: string;
|
|
14
|
+
onValueChange?: (value: string) => void;
|
|
15
|
+
"aria-label"?: string;
|
|
16
|
+
className?: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The 5% ink pill track shared by the Bot panels (Paper `YNQ-1`): 3px padding, 2px gap, 28px options with
|
|
21
|
+
* 12px inset and a 10% ink selection that slides between options in 280ms.
|
|
22
|
+
*/
|
|
23
|
+
export function PanelPillTabs({ items, value, defaultValue, onValueChange, className, ...props }: PanelPillTabsProps) {
|
|
24
|
+
const [inner, setInner] = useState(defaultValue ?? items[0]?.value ?? "");
|
|
25
|
+
const selected = value ?? inner;
|
|
26
|
+
const track = useRef<HTMLDivElement>(null);
|
|
27
|
+
const { rect, animate } = useActiveIndicator(track, '[aria-selected="true"]');
|
|
28
|
+
|
|
29
|
+
const select = (next: string) => {
|
|
30
|
+
setInner(next);
|
|
31
|
+
onValueChange?.(next);
|
|
32
|
+
};
|
|
33
|
+
const onKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
|
|
34
|
+
if (event.key !== "ArrowRight" && event.key !== "ArrowLeft") return;
|
|
35
|
+
const at = items.findIndex((item) => item.value === selected);
|
|
36
|
+
const step = event.key === "ArrowRight" ? 1 : -1;
|
|
37
|
+
const next = items[(at + step + items.length) % items.length];
|
|
38
|
+
if (!next) return;
|
|
39
|
+
event.preventDefault();
|
|
40
|
+
select(next.value);
|
|
41
|
+
track.current?.querySelectorAll<HTMLElement>('[role="tab"]')[items.indexOf(next)]?.focus();
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<div
|
|
46
|
+
ref={track}
|
|
47
|
+
role="tablist"
|
|
48
|
+
aria-label={props["aria-label"]}
|
|
49
|
+
onKeyDown={onKeyDown}
|
|
50
|
+
className={cn(
|
|
51
|
+
"relative flex max-w-full min-w-0 items-center gap-0.5 overflow-x-auto rounded-full bg-overlay-ink-5 p-[3px]",
|
|
52
|
+
className,
|
|
53
|
+
)}
|
|
54
|
+
>
|
|
55
|
+
<span
|
|
56
|
+
aria-hidden
|
|
57
|
+
className={cn("pointer-events-none absolute rounded-full bg-overlay-ink-10", animate && "motion-indicator")}
|
|
58
|
+
style={rect ? { ...rect, opacity: 1 } : { opacity: 0 }}
|
|
59
|
+
/>
|
|
60
|
+
{items.map((item) => {
|
|
61
|
+
const active = item.value === selected;
|
|
62
|
+
return (
|
|
63
|
+
<button
|
|
64
|
+
key={item.value}
|
|
65
|
+
type="button"
|
|
66
|
+
role="tab"
|
|
67
|
+
aria-selected={active}
|
|
68
|
+
tabIndex={active ? 0 : -1}
|
|
69
|
+
onClick={() => select(item.value)}
|
|
70
|
+
className={cn(
|
|
71
|
+
"relative flex h-7.5 cursor-pointer items-center rounded-full px-3 text-label-md whitespace-nowrap motion-colors",
|
|
72
|
+
"focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-text-brand",
|
|
73
|
+
active ? "text-text-primary" : "text-text-secondary hover:text-text-primary",
|
|
74
|
+
)}
|
|
75
|
+
>
|
|
76
|
+
{item.label}
|
|
77
|
+
</button>
|
|
78
|
+
);
|
|
79
|
+
})}
|
|
80
|
+
</div>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export type InstructionVariableTone = "sky" | "brand" | "neutral";
|
|
85
|
+
|
|
86
|
+
const variableToneClass: Record<InstructionVariableTone, string> = {
|
|
87
|
+
sky: "bg-accent-sky-500-18 ring-accent-sky-500-90/40",
|
|
88
|
+
brand: "bg-accent-sky-700-18 ring-accent-sky-700-90/40",
|
|
89
|
+
neutral: "bg-text-placeholder-18 ring-text-placeholder-90/40",
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const variableDotClass: Record<InstructionVariableTone, string> = {
|
|
93
|
+
sky: "bg-sky-500",
|
|
94
|
+
brand: "bg-sky-700",
|
|
95
|
+
neutral: "bg-neutral-500",
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/** One piece of an instruction line: plain text, or a `{{variable}}` chip. */
|
|
99
|
+
export type InstructionPart = string | { variable: string; tone?: InstructionVariableTone };
|
|
100
|
+
|
|
101
|
+
export type InstructionVariable = { name: string; tone?: InstructionVariableTone; value: string };
|
|
102
|
+
|
|
103
|
+
export type BotInstructionsPanelProps = WithClass<Omit<ComponentProps<"section">, "title">> & {
|
|
104
|
+
title?: ReactNode;
|
|
105
|
+
/** Version label in the header pill, for example `v4`. */
|
|
106
|
+
version?: string;
|
|
107
|
+
/** Status next to the version, rendered with a live dot. */
|
|
108
|
+
versionStatus?: string;
|
|
109
|
+
bot: { initial: string; name: string };
|
|
110
|
+
/** The editable prompt, one array of parts per line. */
|
|
111
|
+
instructions: InstructionPart[][];
|
|
112
|
+
/** Shown under the Example tab instead of the prompt. */
|
|
113
|
+
example?: ReactNode;
|
|
114
|
+
tokenCount?: number;
|
|
115
|
+
/** Test values for each variable in the prompt. */
|
|
116
|
+
variables?: InstructionVariable[];
|
|
117
|
+
scenarios?: string[];
|
|
118
|
+
defaultScenario?: string;
|
|
119
|
+
onRun?: (scenario: string | undefined) => void;
|
|
120
|
+
/** The last run's output, or a hint until there is one. */
|
|
121
|
+
output?: ReactNode;
|
|
122
|
+
/** Makes the version pill actionable; without it the pill is a plain label. */
|
|
123
|
+
onVersionClick?: () => void;
|
|
124
|
+
/** Makes the bot pill actionable; without it the pill is a plain label. */
|
|
125
|
+
onBotClick?: () => void;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
function partKey(part: InstructionPart) {
|
|
129
|
+
return typeof part === "string" ? part : part.variable;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function VariableChip({
|
|
133
|
+
tone = "sky",
|
|
134
|
+
className,
|
|
135
|
+
children,
|
|
136
|
+
}: {
|
|
137
|
+
tone?: InstructionVariableTone;
|
|
138
|
+
className?: string;
|
|
139
|
+
children: ReactNode;
|
|
140
|
+
}) {
|
|
141
|
+
return (
|
|
142
|
+
<span className={cn("rounded-xs px-1.5 py-px ring-1 ring-inset", variableToneClass[tone], className)}>
|
|
143
|
+
{children}
|
|
144
|
+
</span>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function Divider() {
|
|
149
|
+
return <div role="separator" className="h-px self-stretch bg-overlay-ink-8" />;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Bot instructions panel (Paper `YN3-1` / `GOZ-0`): a 24px-radius surface with the bot header, an
|
|
154
|
+
* Instructions / Example tab pair over the prompt text with `{{variable}}` chips, and a test-run block.
|
|
155
|
+
*/
|
|
156
|
+
export function BotInstructionsPanel({
|
|
157
|
+
title = "Instructions",
|
|
158
|
+
version,
|
|
159
|
+
versionStatus,
|
|
160
|
+
bot,
|
|
161
|
+
instructions,
|
|
162
|
+
example,
|
|
163
|
+
tokenCount,
|
|
164
|
+
variables,
|
|
165
|
+
scenarios,
|
|
166
|
+
defaultScenario,
|
|
167
|
+
onRun,
|
|
168
|
+
output,
|
|
169
|
+
onVersionClick,
|
|
170
|
+
onBotClick,
|
|
171
|
+
className,
|
|
172
|
+
...props
|
|
173
|
+
}: BotInstructionsPanelProps) {
|
|
174
|
+
const [tab, setTab] = useState("instructions");
|
|
175
|
+
const [scenario, setScenario] = useState(defaultScenario ?? scenarios?.[0]);
|
|
176
|
+
const [runCount, setRunCount] = useState(0);
|
|
177
|
+
|
|
178
|
+
return (
|
|
179
|
+
<section
|
|
180
|
+
className={cn(
|
|
181
|
+
"flex w-full max-w-155 min-w-0 flex-col overflow-clip rounded-[24px] border border-border-default bg-bg-surface",
|
|
182
|
+
className,
|
|
183
|
+
)}
|
|
184
|
+
{...props}
|
|
185
|
+
>
|
|
186
|
+
<header className="flex flex-wrap items-center gap-2 px-5 pt-4 pb-3.5">
|
|
187
|
+
<h3 className="min-w-0 text-label-lg text-text-primary">{title}</h3>
|
|
188
|
+
{version ? (
|
|
189
|
+
onVersionClick ? (
|
|
190
|
+
<button
|
|
191
|
+
type="button"
|
|
192
|
+
onClick={onVersionClick}
|
|
193
|
+
className="flex h-7.5 shrink-0 motion-press cursor-pointer items-center gap-1.5 rounded-full bg-overlay-ink-6 px-2.5 text-label-sm text-text-primary hover:bg-overlay-ink-10 focus-visible:outline-2 focus-visible:outline-text-brand"
|
|
194
|
+
>
|
|
195
|
+
{version}
|
|
196
|
+
{versionStatus ? (
|
|
197
|
+
<>
|
|
198
|
+
<span
|
|
199
|
+
aria-hidden
|
|
200
|
+
className="size-1.5 animate-live-pulse rounded-full bg-status-live text-status-live"
|
|
201
|
+
/>
|
|
202
|
+
<span className="text-text-secondary">{versionStatus}</span>
|
|
203
|
+
</>
|
|
204
|
+
) : null}
|
|
205
|
+
<CaretDown aria-hidden className="size-3 text-text-secondary" />
|
|
206
|
+
</button>
|
|
207
|
+
) : (
|
|
208
|
+
<span className="flex h-7.5 shrink-0 items-center gap-1.5 rounded-full bg-overlay-ink-6 px-2.5 text-label-sm text-text-primary">
|
|
209
|
+
{version}
|
|
210
|
+
{versionStatus ? (
|
|
211
|
+
<>
|
|
212
|
+
<span
|
|
213
|
+
aria-hidden
|
|
214
|
+
className="size-1.5 animate-live-pulse rounded-full bg-status-live text-status-live"
|
|
215
|
+
/>
|
|
216
|
+
<span className="text-text-secondary">{versionStatus}</span>
|
|
217
|
+
</>
|
|
218
|
+
) : null}
|
|
219
|
+
</span>
|
|
220
|
+
)
|
|
221
|
+
) : null}
|
|
222
|
+
<span className="min-w-0 flex-1" />
|
|
223
|
+
{onBotClick ? (
|
|
224
|
+
<button
|
|
225
|
+
type="button"
|
|
226
|
+
onClick={onBotClick}
|
|
227
|
+
className="flex h-7.5 shrink-0 motion-press cursor-pointer items-center gap-1.5 rounded-full bg-overlay-ink-6 pr-2.5 pl-1.5 text-label-sm text-text-primary hover:bg-overlay-ink-10 focus-visible:outline-2 focus-visible:outline-text-brand"
|
|
228
|
+
>
|
|
229
|
+
<span className="flex size-4.5 items-center justify-center rounded-[5px] bg-interactive-primary text-label-sm text-text-inverse">
|
|
230
|
+
{bot.initial}
|
|
231
|
+
</span>
|
|
232
|
+
{bot.name}
|
|
233
|
+
<CaretDown aria-hidden className="size-3 text-text-secondary" />
|
|
234
|
+
</button>
|
|
235
|
+
) : (
|
|
236
|
+
<span className="flex h-7.5 shrink-0 items-center gap-1.5 rounded-full bg-overlay-ink-6 pr-2.5 pl-1.5 text-label-sm text-text-primary">
|
|
237
|
+
<span className="flex size-4.5 items-center justify-center rounded-[5px] bg-interactive-primary text-label-sm text-text-inverse">
|
|
238
|
+
{bot.initial}
|
|
239
|
+
</span>
|
|
240
|
+
{bot.name}
|
|
241
|
+
</span>
|
|
242
|
+
)}
|
|
243
|
+
</header>
|
|
244
|
+
<Divider />
|
|
245
|
+
<div className="flex flex-col gap-1 px-5 pt-3 pb-3.5">
|
|
246
|
+
<div className="flex items-center gap-2 pb-2">
|
|
247
|
+
<PanelPillTabs
|
|
248
|
+
aria-label="Instructions view"
|
|
249
|
+
value={tab}
|
|
250
|
+
onValueChange={setTab}
|
|
251
|
+
items={[
|
|
252
|
+
{ value: "instructions", label: "Instructions" },
|
|
253
|
+
{ value: "example", label: "Example" },
|
|
254
|
+
]}
|
|
255
|
+
/>
|
|
256
|
+
<span className="min-w-0 flex-1" />
|
|
257
|
+
{tokenCount === undefined ? null : (
|
|
258
|
+
<span className="text-caption text-text-secondary">{tokenCount} tokens</span>
|
|
259
|
+
)}
|
|
260
|
+
</div>
|
|
261
|
+
<div
|
|
262
|
+
key={tab}
|
|
263
|
+
className="group flex motion-rise flex-col gap-1 rounded-lg outline-none focus-visible:outline-2 focus-visible:outline-text-brand"
|
|
264
|
+
tabIndex={0}
|
|
265
|
+
>
|
|
266
|
+
{tab === "example" && example !== undefined ? (
|
|
267
|
+
<div className="text-body-sm text-text-primary">{example}</div>
|
|
268
|
+
) : (
|
|
269
|
+
instructions.map((line, index) => (
|
|
270
|
+
<p
|
|
271
|
+
key={line.map(partKey).join("|")}
|
|
272
|
+
className="flex flex-wrap items-center gap-1 text-body-sm text-text-primary"
|
|
273
|
+
>
|
|
274
|
+
{/* A lone period after a `{{variable}}` is its own flex item; pull it back
|
|
275
|
+
so the 4px gap between items does not open before the punctuation. */}
|
|
276
|
+
{line.map((part) =>
|
|
277
|
+
typeof part === "string" ? (
|
|
278
|
+
<span key={part} className={cn(/^[,.;:!?%)\]]/.test(part) && "-ml-1")}>
|
|
279
|
+
{part}
|
|
280
|
+
</span>
|
|
281
|
+
) : (
|
|
282
|
+
<VariableChip key={part.variable} tone={part.tone}>
|
|
283
|
+
{part.variable}
|
|
284
|
+
</VariableChip>
|
|
285
|
+
),
|
|
286
|
+
)}
|
|
287
|
+
{index === instructions.length - 1 ? (
|
|
288
|
+
<span
|
|
289
|
+
aria-hidden
|
|
290
|
+
className="hidden h-4 w-px animate-caret bg-text-primary group-focus-within:inline-block"
|
|
291
|
+
/>
|
|
292
|
+
) : null}
|
|
293
|
+
</p>
|
|
294
|
+
))
|
|
295
|
+
)}
|
|
296
|
+
</div>
|
|
297
|
+
<div className="flex items-center gap-1.5 pt-2 text-caption text-text-secondary">
|
|
298
|
+
Type
|
|
299
|
+
<span className="rounded-md bg-overlay-ink-6 px-1.5 py-px">{"{{"}</span>
|
|
300
|
+
to add a variable
|
|
301
|
+
</div>
|
|
302
|
+
</div>
|
|
303
|
+
{variables || scenarios ? (
|
|
304
|
+
<>
|
|
305
|
+
<Divider />
|
|
306
|
+
<div className="flex flex-col gap-2.5 px-5 pt-3 pb-3.5">
|
|
307
|
+
{variables ? (
|
|
308
|
+
<div className="flex flex-wrap gap-2">
|
|
309
|
+
{variables.map((variable) => (
|
|
310
|
+
<div key={variable.name} className="flex min-w-0 flex-1 basis-35 flex-col gap-1.5">
|
|
311
|
+
<div className="flex items-center gap-1.5 text-caption text-text-secondary">
|
|
312
|
+
<span
|
|
313
|
+
aria-hidden
|
|
314
|
+
className={cn("size-1.5 rounded-full", variableDotClass[variable.tone ?? "sky"])}
|
|
315
|
+
/>
|
|
316
|
+
{variable.name}
|
|
317
|
+
</div>
|
|
318
|
+
<div className="flex h-10 min-w-0 items-center rounded-md bg-overlay-ink-5 px-3 text-body-sm text-text-primary">
|
|
319
|
+
<span className="min-w-0 truncate">{variable.value}</span>
|
|
320
|
+
</div>
|
|
321
|
+
</div>
|
|
322
|
+
))}
|
|
323
|
+
</div>
|
|
324
|
+
) : null}
|
|
325
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
326
|
+
{scenarios ? (
|
|
327
|
+
<PanelPillTabs
|
|
328
|
+
aria-label="Scenario"
|
|
329
|
+
value={scenario}
|
|
330
|
+
onValueChange={setScenario}
|
|
331
|
+
items={scenarios.map((name) => ({ value: name, label: name }))}
|
|
332
|
+
/>
|
|
333
|
+
) : null}
|
|
334
|
+
<span className="min-w-0 flex-1" />
|
|
335
|
+
<button
|
|
336
|
+
type="button"
|
|
337
|
+
onClick={() => {
|
|
338
|
+
setRunCount((count) => count + 1);
|
|
339
|
+
onRun?.(scenario);
|
|
340
|
+
}}
|
|
341
|
+
className="flex h-7.5 shrink-0 motion-press cursor-pointer items-center justify-center gap-1.5 rounded-full bg-interactive-primary pr-3 pl-3.5 text-label-sm text-text-inverse hover:bg-interactive-primary-hover focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-text-brand"
|
|
342
|
+
>
|
|
343
|
+
<Play aria-hidden className="size-3.5" />
|
|
344
|
+
Run
|
|
345
|
+
<Kbd className="bg-bg-surface-6 text-text-inverse/80">⌘↵</Kbd>
|
|
346
|
+
</button>
|
|
347
|
+
</div>
|
|
348
|
+
</div>
|
|
349
|
+
</>
|
|
350
|
+
) : null}
|
|
351
|
+
<div className="motion-reveal" data-collapsed={output === undefined || undefined}>
|
|
352
|
+
<div>
|
|
353
|
+
{output === undefined ? null : (
|
|
354
|
+
<div
|
|
355
|
+
key={runCount}
|
|
356
|
+
className="motion-rise bg-overlay-ink-3 px-5 pt-3.5 pb-4 text-body-sm text-text-secondary"
|
|
357
|
+
>
|
|
358
|
+
{output}
|
|
359
|
+
</div>
|
|
360
|
+
)}
|
|
361
|
+
</div>
|
|
362
|
+
</div>
|
|
363
|
+
</section>
|
|
364
|
+
);
|
|
365
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { ComponentProps, ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
import { cn } from "../lib/cn";
|
|
4
|
+
import { Avatar } from "./avatar";
|
|
5
|
+
|
|
6
|
+
export type BotListRowProps = Omit<ComponentProps<"button">, "className"> & {
|
|
7
|
+
name: ReactNode;
|
|
8
|
+
/** Last activity time, such as "8:41 AM". */
|
|
9
|
+
time?: ReactNode;
|
|
10
|
+
/** One-line preview of the latest activity. */
|
|
11
|
+
preview?: ReactNode;
|
|
12
|
+
/** Running bots show the live dot on the avatar. */
|
|
13
|
+
live?: boolean;
|
|
14
|
+
selected?: boolean;
|
|
15
|
+
className?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/** Figma `Bot list row` (64:456): a flat 12px-padded row; hover 5% ink, selected 10% ink. */
|
|
19
|
+
export function BotListRow({ name, time, preview, live, selected, className, ...props }: BotListRowProps) {
|
|
20
|
+
return (
|
|
21
|
+
<button
|
|
22
|
+
type="button"
|
|
23
|
+
aria-pressed={selected}
|
|
24
|
+
className={cn(
|
|
25
|
+
"flex w-full min-w-0 motion-press cursor-pointer items-center gap-3 rounded-lg p-3 text-left",
|
|
26
|
+
selected ? "bg-overlay-ink-10" : "hover:bg-overlay-ink-5",
|
|
27
|
+
"focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-text-brand",
|
|
28
|
+
className,
|
|
29
|
+
)}
|
|
30
|
+
{...props}
|
|
31
|
+
>
|
|
32
|
+
<Avatar type="bot" size="lg" live={live} />
|
|
33
|
+
<span className="flex min-w-0 flex-1 flex-col gap-0.5">
|
|
34
|
+
<span className="flex min-w-0 items-center gap-2">
|
|
35
|
+
<span className="min-w-0 flex-1 truncate text-label-lg text-text-primary">{name}</span>
|
|
36
|
+
{time && <span className="shrink-0 text-caption text-text-secondary">{time}</span>}
|
|
37
|
+
</span>
|
|
38
|
+
{preview && <span className="min-w-0 truncate text-body-sm text-text-secondary">{preview}</span>}
|
|
39
|
+
</span>
|
|
40
|
+
</button>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { BookmarkSimple, MagnifyingGlass, Plus, X } from "@phosphor-icons/react";
|
|
2
|
+
import { type ComponentProps, type ReactNode, useState } from "react";
|
|
3
|
+
|
|
4
|
+
import { cn } from "../lib/cn";
|
|
5
|
+
import { PanelPillTabs } from "./bot-instructions-panel";
|
|
6
|
+
import { Switch } from "./switch";
|
|
7
|
+
|
|
8
|
+
type WithClass<T> = Omit<T, "className"> & { className?: string };
|
|
9
|
+
|
|
10
|
+
export type BotMemorySetting = {
|
|
11
|
+
id: string;
|
|
12
|
+
label: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
defaultChecked?: boolean;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type BotMemoryItem = {
|
|
18
|
+
id: string;
|
|
19
|
+
text: string;
|
|
20
|
+
/** Where the memory came from, for example `From a chat · Oct 8`. */
|
|
21
|
+
source: string;
|
|
22
|
+
/** Group label; the filter tabs are built from these in order of appearance. */
|
|
23
|
+
group: string;
|
|
24
|
+
/** Highlights a memory that was just added. */
|
|
25
|
+
fresh?: boolean;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type BotMemoryToast = { title: string; message?: string };
|
|
29
|
+
|
|
30
|
+
export type BotMemoryPanelProps = WithClass<Omit<ComponentProps<"section">, "title">> & {
|
|
31
|
+
title?: ReactNode;
|
|
32
|
+
subtitle?: ReactNode;
|
|
33
|
+
/** Fill of the memory meter, 0 to 100. */
|
|
34
|
+
usage: number;
|
|
35
|
+
settings?: BotMemorySetting[];
|
|
36
|
+
memories: BotMemoryItem[];
|
|
37
|
+
/** The "Memory updated" pill; it enters with a pop and leaves through its close button. */
|
|
38
|
+
toast?: BotMemoryToast;
|
|
39
|
+
onToastDismiss?: () => void;
|
|
40
|
+
onAdd?: () => void;
|
|
41
|
+
onSettingChange?: (id: string, checked: boolean) => void;
|
|
42
|
+
/** Makes the search button actionable; without it the icon is a plain decoration. */
|
|
43
|
+
onSearch?: () => void;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function Divider({ inset }: { inset?: boolean }) {
|
|
47
|
+
return (
|
|
48
|
+
<div className={cn("flex self-stretch", inset && "px-5")}>
|
|
49
|
+
<div role="separator" className="h-px flex-1 bg-overlay-ink-8" />
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Bot memory panel (Paper `ZO2-1` / `HPY-0`): a 24px-radius surface with a memory meter, two switch rows,
|
|
56
|
+
* a filter pill track, a transient "Memory updated" pill and the grouped memories.
|
|
57
|
+
*/
|
|
58
|
+
export function BotMemoryPanel({
|
|
59
|
+
title = "Memory",
|
|
60
|
+
subtitle,
|
|
61
|
+
usage,
|
|
62
|
+
settings,
|
|
63
|
+
memories,
|
|
64
|
+
toast,
|
|
65
|
+
onToastDismiss,
|
|
66
|
+
onAdd,
|
|
67
|
+
onSettingChange,
|
|
68
|
+
onSearch,
|
|
69
|
+
className,
|
|
70
|
+
...props
|
|
71
|
+
}: BotMemoryPanelProps) {
|
|
72
|
+
const [filter, setFilter] = useState("all");
|
|
73
|
+
// Both marks are tied to the toast object, so a new toast event revives the pill without an effect.
|
|
74
|
+
const [leavingFor, setLeavingFor] = useState<BotMemoryToast | undefined>();
|
|
75
|
+
const [dismissedFor, setDismissedFor] = useState<BotMemoryToast | undefined>();
|
|
76
|
+
const leaving = toast !== undefined && leavingFor === toast;
|
|
77
|
+
const dismissed = toast !== undefined && dismissedFor === toast;
|
|
78
|
+
|
|
79
|
+
const groups = [...new Set(memories.map((memory) => memory.group))];
|
|
80
|
+
const visible = filter === "all" ? groups : groups.filter((group) => group === filter);
|
|
81
|
+
const percent = Math.min(100, Math.max(0, usage));
|
|
82
|
+
let rowIndex = 0;
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<section
|
|
86
|
+
className={cn(
|
|
87
|
+
"flex w-full max-w-140 min-w-0 flex-col overflow-clip rounded-[24px] border border-border-default bg-bg-surface",
|
|
88
|
+
className,
|
|
89
|
+
)}
|
|
90
|
+
{...props}
|
|
91
|
+
>
|
|
92
|
+
<header className="flex flex-wrap items-center gap-x-2.5 gap-y-2 px-5 pt-4 pb-3.5">
|
|
93
|
+
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
|
|
94
|
+
<h3 className="min-w-0 truncate text-label-lg text-text-primary">{title}</h3>
|
|
95
|
+
{subtitle ? <p className="min-w-0 truncate text-caption text-text-secondary">{subtitle}</p> : null}
|
|
96
|
+
</div>
|
|
97
|
+
<div className="flex flex-col items-end gap-1">
|
|
98
|
+
<span className="text-caption text-text-secondary">{percent}% full</span>
|
|
99
|
+
<div
|
|
100
|
+
role="progressbar"
|
|
101
|
+
aria-label="Memory used"
|
|
102
|
+
aria-valuemin={0}
|
|
103
|
+
aria-valuemax={100}
|
|
104
|
+
aria-valuenow={percent}
|
|
105
|
+
className="h-1.25 w-20 shrink-0 overflow-clip rounded-full bg-overlay-ink-8"
|
|
106
|
+
>
|
|
107
|
+
<div
|
|
108
|
+
className="h-full rounded-full bg-interactive-primary transition-[width] duration-280 ease-emphasized"
|
|
109
|
+
style={{ width: `${percent}%` }}
|
|
110
|
+
/>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
</header>
|
|
114
|
+
{settings?.map((setting, index) => (
|
|
115
|
+
<div key={setting.id} className="flex flex-col">
|
|
116
|
+
{index > 0 ? <Divider inset /> : null}
|
|
117
|
+
<div className="flex items-center gap-3 px-5 py-2.5">
|
|
118
|
+
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
|
|
119
|
+
<span className="text-label-md text-text-primary">{setting.label}</span>
|
|
120
|
+
{setting.description ? (
|
|
121
|
+
<span className="text-caption text-text-secondary">{setting.description}</span>
|
|
122
|
+
) : null}
|
|
123
|
+
</div>
|
|
124
|
+
<Switch
|
|
125
|
+
size="compact"
|
|
126
|
+
aria-label={setting.label}
|
|
127
|
+
defaultChecked={setting.defaultChecked}
|
|
128
|
+
onCheckedChange={(checked) => onSettingChange?.(setting.id, checked)}
|
|
129
|
+
/>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
))}
|
|
133
|
+
<Divider />
|
|
134
|
+
<div className="flex items-center gap-2 px-5 pt-3 pb-1.5">
|
|
135
|
+
<PanelPillTabs
|
|
136
|
+
aria-label="Memory filter"
|
|
137
|
+
value={filter}
|
|
138
|
+
onValueChange={setFilter}
|
|
139
|
+
items={[{ value: "all", label: "All" }, ...groups.map((group) => ({ value: group, label: group }))]}
|
|
140
|
+
/>
|
|
141
|
+
<span className="min-w-0 flex-1" />
|
|
142
|
+
{onSearch ? (
|
|
143
|
+
<button
|
|
144
|
+
type="button"
|
|
145
|
+
aria-label="Search memories"
|
|
146
|
+
onClick={onSearch}
|
|
147
|
+
className="flex size-7.5 shrink-0 motion-press cursor-pointer items-center justify-center rounded-full bg-overlay-ink-6 text-text-secondary hover:bg-overlay-ink-10 hover:text-text-primary focus-visible:outline-2 focus-visible:outline-text-brand"
|
|
148
|
+
>
|
|
149
|
+
<MagnifyingGlass aria-hidden className="size-4" />
|
|
150
|
+
</button>
|
|
151
|
+
) : (
|
|
152
|
+
<span
|
|
153
|
+
aria-hidden
|
|
154
|
+
className="flex size-7.5 shrink-0 items-center justify-center rounded-full bg-overlay-ink-6 text-text-secondary"
|
|
155
|
+
>
|
|
156
|
+
<MagnifyingGlass className="size-4" />
|
|
157
|
+
</span>
|
|
158
|
+
)}
|
|
159
|
+
</div>
|
|
160
|
+
<div className="motion-reveal" data-collapsed={!toast || dismissed || undefined}>
|
|
161
|
+
<div className="px-5 py-1.5">
|
|
162
|
+
{toast && !dismissed ? (
|
|
163
|
+
<div
|
|
164
|
+
role="status"
|
|
165
|
+
onAnimationEnd={() => {
|
|
166
|
+
if (leaving && toast) {
|
|
167
|
+
setDismissedFor(toast);
|
|
168
|
+
onToastDismiss?.();
|
|
169
|
+
}
|
|
170
|
+
}}
|
|
171
|
+
className={cn(
|
|
172
|
+
"inline-flex h-8 max-w-full min-w-0 items-center gap-2 rounded-full bg-accent-sky-500-16 pr-3 pl-2.5",
|
|
173
|
+
leaving ? "animate-pop-out" : "animate-pop-in",
|
|
174
|
+
)}
|
|
175
|
+
>
|
|
176
|
+
<BookmarkSimple aria-hidden className="size-3.5 shrink-0 text-text-primary" />
|
|
177
|
+
<span className="min-w-0 truncate text-label-sm text-text-primary">{toast.title}</span>
|
|
178
|
+
{toast.message ? (
|
|
179
|
+
<span className="min-w-0 truncate text-caption text-text-secondary">{toast.message}</span>
|
|
180
|
+
) : null}
|
|
181
|
+
<button
|
|
182
|
+
type="button"
|
|
183
|
+
aria-label="Dismiss"
|
|
184
|
+
onClick={() => setLeavingFor(toast)}
|
|
185
|
+
className="flex size-3 shrink-0 cursor-pointer items-center justify-center text-text-secondary motion-colors hover:text-text-primary focus-visible:outline-2 focus-visible:outline-text-brand"
|
|
186
|
+
>
|
|
187
|
+
<X aria-hidden className="size-3" />
|
|
188
|
+
</button>
|
|
189
|
+
</div>
|
|
190
|
+
) : null}
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
<div key={filter} className="flex flex-col">
|
|
194
|
+
{visible.map((group) => {
|
|
195
|
+
const items = memories.filter((memory) => memory.group === group);
|
|
196
|
+
return (
|
|
197
|
+
<div key={group} className="flex flex-col">
|
|
198
|
+
<div className="flex items-center gap-1.5 px-5 pt-3 pb-1">
|
|
199
|
+
<span className="text-label-sm text-text-secondary">{group}</span>
|
|
200
|
+
<span className="text-caption text-text-secondary">{items.length}</span>
|
|
201
|
+
</div>
|
|
202
|
+
{items.map((memory) => (
|
|
203
|
+
<div
|
|
204
|
+
key={memory.id}
|
|
205
|
+
className={cn(
|
|
206
|
+
"flex motion-rise flex-col gap-0.5 px-5 py-2 motion-colors",
|
|
207
|
+
memory.fresh && "bg-accent-sky-500-10",
|
|
208
|
+
)}
|
|
209
|
+
style={{ animationDelay: `${rowIndex++ * 30}ms` }}
|
|
210
|
+
>
|
|
211
|
+
<span className="text-body-sm wrap-break-word text-text-primary">{memory.text}</span>
|
|
212
|
+
<span className="text-caption wrap-break-word text-text-secondary">{memory.source}</span>
|
|
213
|
+
</div>
|
|
214
|
+
))}
|
|
215
|
+
</div>
|
|
216
|
+
);
|
|
217
|
+
})}
|
|
218
|
+
</div>
|
|
219
|
+
<button
|
|
220
|
+
type="button"
|
|
221
|
+
onClick={onAdd}
|
|
222
|
+
className="flex cursor-pointer items-center gap-2 px-5 pt-3 pb-4 text-label-md text-text-secondary motion-colors hover:text-text-primary focus-visible:outline-2 focus-visible:outline-text-brand"
|
|
223
|
+
>
|
|
224
|
+
<Plus aria-hidden className="size-3.5" />
|
|
225
|
+
Add memory
|
|
226
|
+
</button>
|
|
227
|
+
</section>
|
|
228
|
+
);
|
|
229
|
+
}
|