@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,128 @@
|
|
|
1
|
+
import { ArrowUp, Check, DotsThree } from "@phosphor-icons/react";
|
|
2
|
+
import type { ComponentProps, ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
import { cn } from "../lib/cn";
|
|
5
|
+
import { Button } from "./button";
|
|
6
|
+
import { IconButton } from "./icon-button";
|
|
7
|
+
import { Menu, MenuContent, MenuTrigger } from "./menu";
|
|
8
|
+
import { Switch } from "./switch";
|
|
9
|
+
|
|
10
|
+
export type PluginRowProps = Omit<ComponentProps<"div">, "children"> & {
|
|
11
|
+
/** Two-letter initials shown on the 40px tile. */
|
|
12
|
+
initials: string;
|
|
13
|
+
name: ReactNode;
|
|
14
|
+
version?: ReactNode;
|
|
15
|
+
/** Text of the update pill next to the version; omit when up to date. */
|
|
16
|
+
updateBadge?: ReactNode;
|
|
17
|
+
author?: ReactNode;
|
|
18
|
+
verified?: boolean;
|
|
19
|
+
/** Capability chips, each with an optional 11px icon. */
|
|
20
|
+
chips?: { id: string; label: ReactNode; icon?: ReactNode }[];
|
|
21
|
+
enabled?: boolean;
|
|
22
|
+
onEnabledChange?: (enabled: boolean) => void;
|
|
23
|
+
/** Shows the Update button when set. */
|
|
24
|
+
onUpdate?: () => void;
|
|
25
|
+
updateLabel?: string;
|
|
26
|
+
toggleLabel?: string;
|
|
27
|
+
overflowLabel?: string;
|
|
28
|
+
/** Items of the overflow menu; the menu button is hidden without them. */
|
|
29
|
+
menu?: ReactNode;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** Paper `Plugin row` (Settings 1VKS-0): tile, name with version and update pill, verified author, chips, and Update, Switch and overflow actions. */
|
|
33
|
+
export function PluginRow({
|
|
34
|
+
initials,
|
|
35
|
+
name,
|
|
36
|
+
version,
|
|
37
|
+
updateBadge,
|
|
38
|
+
author,
|
|
39
|
+
verified,
|
|
40
|
+
chips,
|
|
41
|
+
enabled,
|
|
42
|
+
onEnabledChange,
|
|
43
|
+
onUpdate,
|
|
44
|
+
updateLabel = "Update",
|
|
45
|
+
toggleLabel = "Enabled",
|
|
46
|
+
overflowLabel = "More",
|
|
47
|
+
menu,
|
|
48
|
+
className,
|
|
49
|
+
...props
|
|
50
|
+
}: PluginRowProps) {
|
|
51
|
+
return (
|
|
52
|
+
<div
|
|
53
|
+
className={cn(
|
|
54
|
+
"flex flex-wrap items-center gap-x-3.5 gap-y-3 border-b border-border-default py-4 last:border-b-0",
|
|
55
|
+
className,
|
|
56
|
+
)}
|
|
57
|
+
{...props}
|
|
58
|
+
>
|
|
59
|
+
<span
|
|
60
|
+
aria-hidden
|
|
61
|
+
className="flex size-10 shrink-0 items-center justify-center rounded-[10px] bg-sky-500 text-label-md text-neutral-50"
|
|
62
|
+
>
|
|
63
|
+
{initials}
|
|
64
|
+
</span>
|
|
65
|
+
<span className="flex min-w-0 grow basis-40 flex-col gap-1.5">
|
|
66
|
+
<span className="flex flex-wrap items-center gap-2">
|
|
67
|
+
<span className="min-w-0 truncate text-label-md text-text-primary">{name}</span>
|
|
68
|
+
{version && <span className="shrink-0 font-mono text-label-sm text-text-secondary">{version}</span>}
|
|
69
|
+
{updateBadge && (
|
|
70
|
+
<span className="inline-flex h-5.5 shrink-0 items-center rounded-full bg-accent-sky-500-18 px-2 text-label-sm text-text-brand">
|
|
71
|
+
{updateBadge}
|
|
72
|
+
</span>
|
|
73
|
+
)}
|
|
74
|
+
</span>
|
|
75
|
+
{author && (
|
|
76
|
+
<span className="flex items-center gap-1 text-label-sm text-text-secondary">
|
|
77
|
+
{author}
|
|
78
|
+
{verified && (
|
|
79
|
+
<span
|
|
80
|
+
role="img"
|
|
81
|
+
aria-label="Verified"
|
|
82
|
+
className="flex size-3.5 shrink-0 items-center justify-center rounded-full bg-sky-500 text-neutral-50"
|
|
83
|
+
>
|
|
84
|
+
<Check aria-hidden weight="bold" className="size-2" />
|
|
85
|
+
</span>
|
|
86
|
+
)}
|
|
87
|
+
</span>
|
|
88
|
+
)}
|
|
89
|
+
{chips && chips.length > 0 && (
|
|
90
|
+
<span className="flex flex-wrap gap-1.5">
|
|
91
|
+
{chips.map((chip) => (
|
|
92
|
+
<span
|
|
93
|
+
key={chip.id}
|
|
94
|
+
className="inline-flex h-5.5 items-center gap-1 rounded-full bg-overlay-ink-6 px-2 text-label-sm text-text-secondary [&_svg]:size-2.75"
|
|
95
|
+
>
|
|
96
|
+
{chip.icon}
|
|
97
|
+
{chip.label}
|
|
98
|
+
</span>
|
|
99
|
+
))}
|
|
100
|
+
</span>
|
|
101
|
+
)}
|
|
102
|
+
</span>
|
|
103
|
+
<span className="flex shrink-0 flex-wrap items-center gap-2">
|
|
104
|
+
<span className="motion-reveal-x" data-collapsed={!onUpdate || undefined} inert={!onUpdate || undefined}>
|
|
105
|
+
<Button variant="solid" size="sm" icon={<ArrowUp aria-hidden className="size-3.5" />} onClick={onUpdate}>
|
|
106
|
+
{updateLabel}
|
|
107
|
+
</Button>
|
|
108
|
+
</span>
|
|
109
|
+
<Switch aria-label={toggleLabel} checked={enabled} onCheckedChange={onEnabledChange} />
|
|
110
|
+
{menu && (
|
|
111
|
+
<Menu>
|
|
112
|
+
<MenuTrigger
|
|
113
|
+
render={
|
|
114
|
+
<IconButton
|
|
115
|
+
label={overflowLabel}
|
|
116
|
+
variant="ghost"
|
|
117
|
+
size="sm"
|
|
118
|
+
icon={<DotsThree aria-hidden weight="bold" />}
|
|
119
|
+
/>
|
|
120
|
+
}
|
|
121
|
+
/>
|
|
122
|
+
<MenuContent align="end">{menu}</MenuContent>
|
|
123
|
+
</Menu>
|
|
124
|
+
)}
|
|
125
|
+
</span>
|
|
126
|
+
</div>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Progress as BaseProgress } from "@base-ui/react/progress";
|
|
2
|
+
import type { ComponentProps } from "react";
|
|
3
|
+
|
|
4
|
+
import { cn } from "../lib/cn";
|
|
5
|
+
|
|
6
|
+
export type ProgressProps = Omit<ComponentProps<typeof BaseProgress.Root>, "className" | "value"> & {
|
|
7
|
+
className?: string;
|
|
8
|
+
/** 0-100. Omit it (or pass `null`) for the indeterminate bar. */
|
|
9
|
+
value?: number | null;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/** Figma `Progress bar` (in Loading 109:733): a 6px 8% ink track with an ink fill. */
|
|
13
|
+
export function Progress({ className, value = null, ...props }: ProgressProps) {
|
|
14
|
+
return (
|
|
15
|
+
<BaseProgress.Root value={value} className={cn("w-60", className)} {...props}>
|
|
16
|
+
<BaseProgress.Track className="h-1.5 w-full overflow-clip rounded-full bg-overlay-ink-8">
|
|
17
|
+
<BaseProgress.Indicator className="h-full rounded-full bg-interactive-primary transition-[width] duration-400 ease-out-quint data-indeterminate:w-1/3 data-indeterminate:animate-sweep" />
|
|
18
|
+
</BaseProgress.Track>
|
|
19
|
+
</BaseProgress.Root>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { Radio as BaseRadio } from "@base-ui/react/radio";
|
|
2
|
+
import { RadioGroup as BaseRadioGroup } from "@base-ui/react/radio-group";
|
|
3
|
+
import { type ComponentProps, createContext, type ReactNode, useContext } from "react";
|
|
4
|
+
|
|
5
|
+
import { cn } from "../lib/cn";
|
|
6
|
+
import type { ControlSize } from "../lib/size";
|
|
7
|
+
|
|
8
|
+
export type RadioSize = ControlSize;
|
|
9
|
+
|
|
10
|
+
/** The group owns the size: every radio in one list reads the same mark. */
|
|
11
|
+
const RadioSizeContext = createContext<ControlSize>("md");
|
|
12
|
+
|
|
13
|
+
export type RadioGroupProps = Omit<ComponentProps<typeof BaseRadioGroup>, "className"> & {
|
|
14
|
+
/** Mark size for every `Radio` inside; a radio can still override it. */
|
|
15
|
+
size?: RadioSize;
|
|
16
|
+
className?: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function RadioGroup({ size = "md", className, ...props }: RadioGroupProps) {
|
|
20
|
+
return (
|
|
21
|
+
<RadioSizeContext.Provider value={size}>
|
|
22
|
+
<BaseRadioGroup className={cn("flex flex-col gap-3", className)} {...props} />
|
|
23
|
+
</RadioSizeContext.Provider>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const labelClass: Record<RadioSize, string> = {
|
|
28
|
+
sm: "gap-2 text-body-sm",
|
|
29
|
+
md: "gap-2.5 text-body-md",
|
|
30
|
+
lg: "gap-3 text-body-lg",
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const circleClass: Record<RadioSize, string> = {
|
|
34
|
+
sm: "size-4",
|
|
35
|
+
md: "size-5",
|
|
36
|
+
lg: "size-6",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const dotClass: Record<RadioSize, string> = {
|
|
40
|
+
sm: "size-1.5",
|
|
41
|
+
md: "size-2",
|
|
42
|
+
lg: "size-2.5",
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export type RadioProps = Omit<ComponentProps<typeof BaseRadio.Root>, "className"> & {
|
|
46
|
+
label?: ReactNode;
|
|
47
|
+
/** Overrides the enclosing `RadioGroup` size. */
|
|
48
|
+
size?: RadioSize;
|
|
49
|
+
className?: string;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/** Figma `Radio` (58:239): a 20px circle; checked fills with ink and an 8px dot. */
|
|
53
|
+
export function Radio({ label, size, className, ...props }: RadioProps) {
|
|
54
|
+
const groupSize = useContext(RadioSizeContext);
|
|
55
|
+
const resolvedSize = size ?? groupSize;
|
|
56
|
+
return (
|
|
57
|
+
<label
|
|
58
|
+
className={cn(
|
|
59
|
+
"inline-flex max-w-full min-w-0 cursor-pointer items-center text-text-primary has-[[data-disabled]]:cursor-not-allowed has-[[data-disabled]]:opacity-40",
|
|
60
|
+
labelClass[resolvedSize],
|
|
61
|
+
className,
|
|
62
|
+
)}
|
|
63
|
+
>
|
|
64
|
+
<BaseRadio.Root
|
|
65
|
+
className={cn(
|
|
66
|
+
"flex shrink-0 items-center justify-center rounded-full border-[1.5px] border-border-strong bg-bg-surface motion-colors",
|
|
67
|
+
circleClass[resolvedSize],
|
|
68
|
+
"hover:not-data-disabled:border-text-secondary",
|
|
69
|
+
"data-checked:border-interactive-primary data-checked:bg-interactive-primary hover:not-data-disabled:data-checked:border-interactive-primary-hover hover:not-data-disabled:data-checked:bg-interactive-primary-hover",
|
|
70
|
+
"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-text-brand",
|
|
71
|
+
)}
|
|
72
|
+
{...props}
|
|
73
|
+
>
|
|
74
|
+
<BaseRadio.Indicator
|
|
75
|
+
className={cn(
|
|
76
|
+
"animate-pop-in rounded-full bg-interactive-on-primary data-ending-style:animate-scale-out",
|
|
77
|
+
dotClass[resolvedSize],
|
|
78
|
+
)}
|
|
79
|
+
/>
|
|
80
|
+
</BaseRadio.Root>
|
|
81
|
+
{label}
|
|
82
|
+
</label>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { ArrowUp, CaretDown, Check, MagnifyingGlass, Plus, Stop } from "@phosphor-icons/react";
|
|
2
|
+
import { Fragment, type CSSProperties, type ComponentProps, type ReactNode, useRef } from "react";
|
|
3
|
+
|
|
4
|
+
import { cn } from "../lib/cn";
|
|
5
|
+
import { ChatBubble } from "./chat-bubble";
|
|
6
|
+
import { Spinner } from "./spinner";
|
|
7
|
+
|
|
8
|
+
export type ResearchQuery = {
|
|
9
|
+
id: string;
|
|
10
|
+
/** `searching` shows a spinner and the muted text; `done` a check. */
|
|
11
|
+
state?: "done" | "searching";
|
|
12
|
+
text: ReactNode;
|
|
13
|
+
/** Result count shown at the end of the row, such as "8 results". */
|
|
14
|
+
results?: ReactNode;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type ResearchRunPanelProps = Omit<ComponentProps<"section">, "title"> & {
|
|
18
|
+
title?: ReactNode;
|
|
19
|
+
newLabel?: ReactNode;
|
|
20
|
+
onNewResearch?: () => void;
|
|
21
|
+
/** The question bubble, right-aligned. */
|
|
22
|
+
question: ReactNode;
|
|
23
|
+
/** Shows the spinner, status line and caret while the run works. */
|
|
24
|
+
running?: boolean;
|
|
25
|
+
statusLabel?: ReactNode;
|
|
26
|
+
elapsed?: ReactNode;
|
|
27
|
+
read?: number;
|
|
28
|
+
total?: number;
|
|
29
|
+
queries?: ResearchQuery[];
|
|
30
|
+
/** Initials for the stacked source avatars under the queries. */
|
|
31
|
+
sources?: string[];
|
|
32
|
+
onStop?: () => void;
|
|
33
|
+
/** Makes the sources pill actionable; without it the pill is a plain label. */
|
|
34
|
+
onOpenSources?: () => void;
|
|
35
|
+
/** Makes the running status line expandable; without it the row carries no caret. */
|
|
36
|
+
onToggleStatus?: () => void;
|
|
37
|
+
followUpPlaceholder?: string;
|
|
38
|
+
onFollowUp?: (value: string) => void;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const stagger = (index: number) => ({ "--i": index, animationDelay: `calc(var(--i) * 30ms)` }) as CSSProperties;
|
|
42
|
+
|
|
43
|
+
/** Paper `Research run` (Agents N10): a 24px-radius panel with a header, the question, a live status line and the queries read so far. */
|
|
44
|
+
export function ResearchRunPanel({
|
|
45
|
+
title = "Research",
|
|
46
|
+
newLabel = "New research",
|
|
47
|
+
onNewResearch,
|
|
48
|
+
question,
|
|
49
|
+
running = true,
|
|
50
|
+
statusLabel = "Researching",
|
|
51
|
+
elapsed,
|
|
52
|
+
read,
|
|
53
|
+
total,
|
|
54
|
+
queries,
|
|
55
|
+
sources,
|
|
56
|
+
onStop,
|
|
57
|
+
onOpenSources,
|
|
58
|
+
onToggleStatus,
|
|
59
|
+
followUpPlaceholder = "Ask a follow-up",
|
|
60
|
+
onFollowUp,
|
|
61
|
+
className,
|
|
62
|
+
...props
|
|
63
|
+
}: ResearchRunPanelProps) {
|
|
64
|
+
const field = useRef<HTMLInputElement>(null);
|
|
65
|
+
const count = read !== undefined && total !== undefined ? `${read} of ${total} read` : undefined;
|
|
66
|
+
const meta = [
|
|
67
|
+
{ id: "elapsed", value: elapsed },
|
|
68
|
+
{ id: "count", value: count },
|
|
69
|
+
].filter((entry) => entry.value !== undefined);
|
|
70
|
+
const sourcesLabel =
|
|
71
|
+
read !== undefined && total !== undefined ? `${read} of ${total} sources read` : `${sources?.length ?? 0} sources`;
|
|
72
|
+
const avatarStack = (
|
|
73
|
+
<span className="flex shrink-0">
|
|
74
|
+
{sources?.map((initial, index) => (
|
|
75
|
+
<span
|
|
76
|
+
key={initial}
|
|
77
|
+
className="flex size-7 shrink-0 items-center justify-center rounded-full bg-sky-700 text-label-sm text-text-on-ink ring-2 ring-bg-surface"
|
|
78
|
+
style={{ marginLeft: index === 0 ? 0 : -6 }}
|
|
79
|
+
>
|
|
80
|
+
{initial}
|
|
81
|
+
</span>
|
|
82
|
+
))}
|
|
83
|
+
</span>
|
|
84
|
+
);
|
|
85
|
+
return (
|
|
86
|
+
<section
|
|
87
|
+
className={cn(
|
|
88
|
+
"flex w-full max-w-150 min-w-0 flex-col overflow-clip rounded-[24px] border border-border-default bg-bg-surface",
|
|
89
|
+
className,
|
|
90
|
+
)}
|
|
91
|
+
{...props}
|
|
92
|
+
>
|
|
93
|
+
<header className="flex flex-wrap items-center gap-x-2 gap-y-2 px-5 pt-4 pb-3.5">
|
|
94
|
+
<MagnifyingGlass aria-hidden className="size-4 shrink-0 text-text-primary" />
|
|
95
|
+
<h3 className="min-w-0 flex-1 truncate text-label-lg text-text-primary">{title}</h3>
|
|
96
|
+
<button
|
|
97
|
+
type="button"
|
|
98
|
+
onClick={onNewResearch}
|
|
99
|
+
className="flex h-7.5 shrink-0 motion-press cursor-pointer items-center gap-1.5 rounded-full bg-overlay-ink-6 pr-3 pl-3.5 text-label-sm text-text-primary hover:bg-overlay-ink-10 focus-visible:outline-2 focus-visible:outline-text-brand"
|
|
100
|
+
>
|
|
101
|
+
{newLabel}
|
|
102
|
+
<Plus aria-hidden className="size-3.5" />
|
|
103
|
+
</button>
|
|
104
|
+
</header>
|
|
105
|
+
<div role="separator" className="h-px bg-overlay-ink-8" />
|
|
106
|
+
<div className="flex flex-col gap-3 px-5 pt-4 pb-4">
|
|
107
|
+
<ChatBubble from="me" className="max-w-[85%] self-end">
|
|
108
|
+
{question}
|
|
109
|
+
</ChatBubble>
|
|
110
|
+
<div className="motion-reveal" data-collapsed={!running || undefined}>
|
|
111
|
+
<div>
|
|
112
|
+
{onToggleStatus ? (
|
|
113
|
+
<button
|
|
114
|
+
type="button"
|
|
115
|
+
onClick={onToggleStatus}
|
|
116
|
+
aria-live="polite"
|
|
117
|
+
className="flex w-full cursor-pointer flex-wrap items-center gap-x-1.5 gap-y-1 rounded-md text-left focus-visible:outline-2 focus-visible:outline-text-brand"
|
|
118
|
+
>
|
|
119
|
+
<Spinner size={14} />
|
|
120
|
+
<span className="min-w-0 text-label-md text-text-primary">{statusLabel}</span>
|
|
121
|
+
<span aria-hidden className="h-3 w-0.5 shrink-0 animate-caret rounded-[1px] bg-interactive-primary" />
|
|
122
|
+
{meta.length > 0 && (
|
|
123
|
+
<span className="text-caption text-text-secondary tabular-nums">
|
|
124
|
+
{meta.map((entry, index) => (
|
|
125
|
+
<Fragment key={entry.id}>
|
|
126
|
+
{index > 0 && " · "}
|
|
127
|
+
{entry.value}
|
|
128
|
+
</Fragment>
|
|
129
|
+
))}
|
|
130
|
+
</span>
|
|
131
|
+
)}
|
|
132
|
+
<CaretDown aria-hidden className="size-3 shrink-0 text-text-secondary" />
|
|
133
|
+
</button>
|
|
134
|
+
) : (
|
|
135
|
+
<div aria-live="polite" className="flex flex-wrap items-center gap-x-1.5 gap-y-1">
|
|
136
|
+
<Spinner size={14} />
|
|
137
|
+
<span className="min-w-0 text-label-md text-text-primary">{statusLabel}</span>
|
|
138
|
+
<span aria-hidden className="h-3 w-0.5 shrink-0 animate-caret rounded-[1px] bg-interactive-primary" />
|
|
139
|
+
{meta.length > 0 && (
|
|
140
|
+
<span className="text-caption text-text-secondary tabular-nums">
|
|
141
|
+
{meta.map((entry, index) => (
|
|
142
|
+
<Fragment key={entry.id}>
|
|
143
|
+
{index > 0 && " · "}
|
|
144
|
+
{entry.value}
|
|
145
|
+
</Fragment>
|
|
146
|
+
))}
|
|
147
|
+
</span>
|
|
148
|
+
)}
|
|
149
|
+
</div>
|
|
150
|
+
)}
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
{queries && queries.length > 0 && (
|
|
154
|
+
<ul className="relative flex flex-col gap-2.5 py-0.5 pl-3.5">
|
|
155
|
+
<span aria-hidden className="absolute top-0 bottom-0 left-0 w-[1.5px] rounded-full bg-overlay-ink-14" />
|
|
156
|
+
{queries.map((query, index) => (
|
|
157
|
+
<li key={query.id} className="flex motion-rise items-center gap-2.5" style={stagger(index)}>
|
|
158
|
+
{query.state === "searching" ? (
|
|
159
|
+
<Spinner size={14} />
|
|
160
|
+
) : (
|
|
161
|
+
<Check aria-hidden className="size-3.5 shrink-0 text-text-secondary" />
|
|
162
|
+
)}
|
|
163
|
+
<span
|
|
164
|
+
className={cn(
|
|
165
|
+
"min-w-0 flex-1 truncate text-body-sm",
|
|
166
|
+
query.state === "searching" ? "text-text-secondary" : "text-text-primary",
|
|
167
|
+
)}
|
|
168
|
+
>
|
|
169
|
+
{query.text}
|
|
170
|
+
</span>
|
|
171
|
+
{query.results !== undefined && (
|
|
172
|
+
<span className="shrink-0 text-caption text-text-secondary">{query.results}</span>
|
|
173
|
+
)}
|
|
174
|
+
</li>
|
|
175
|
+
))}
|
|
176
|
+
</ul>
|
|
177
|
+
)}
|
|
178
|
+
{sources && sources.length > 0 && (
|
|
179
|
+
<div className="animate-pop-in">
|
|
180
|
+
{onOpenSources ? (
|
|
181
|
+
<button
|
|
182
|
+
type="button"
|
|
183
|
+
onClick={onOpenSources}
|
|
184
|
+
className="flex h-9 w-fit motion-press cursor-pointer items-center gap-2 rounded-full bg-overlay-ink-5 pr-3 pl-1 hover:bg-overlay-ink-8 focus-visible:outline-2 focus-visible:outline-text-brand"
|
|
185
|
+
>
|
|
186
|
+
{avatarStack}
|
|
187
|
+
<span className="text-label-md text-text-primary tabular-nums">{sourcesLabel}</span>
|
|
188
|
+
<CaretDown aria-hidden className="size-3 text-text-secondary" />
|
|
189
|
+
</button>
|
|
190
|
+
) : (
|
|
191
|
+
<div className="flex h-9 w-fit items-center gap-2 rounded-full bg-overlay-ink-5 pr-3 pl-1">
|
|
192
|
+
{avatarStack}
|
|
193
|
+
<span className="text-label-md text-text-primary tabular-nums">{sourcesLabel}</span>
|
|
194
|
+
</div>
|
|
195
|
+
)}
|
|
196
|
+
</div>
|
|
197
|
+
)}
|
|
198
|
+
</div>
|
|
199
|
+
<form
|
|
200
|
+
className="px-5 pt-1 pb-4"
|
|
201
|
+
onSubmit={(event) => {
|
|
202
|
+
event.preventDefault();
|
|
203
|
+
const input = field.current;
|
|
204
|
+
if (!input) return;
|
|
205
|
+
onFollowUp?.(input.value);
|
|
206
|
+
input.value = "";
|
|
207
|
+
}}
|
|
208
|
+
>
|
|
209
|
+
<div className="flex h-11 items-center gap-2 rounded-full bg-overlay-ink-5 pr-1.5 pl-4 motion-colors focus-within:bg-overlay-ink-8">
|
|
210
|
+
<input
|
|
211
|
+
ref={field}
|
|
212
|
+
name="follow-up"
|
|
213
|
+
aria-label={followUpPlaceholder}
|
|
214
|
+
placeholder={followUpPlaceholder}
|
|
215
|
+
className="min-w-0 flex-1 bg-transparent text-body-sm text-text-primary outline-none placeholder:text-text-secondary"
|
|
216
|
+
/>
|
|
217
|
+
<button
|
|
218
|
+
type={running ? "button" : "submit"}
|
|
219
|
+
onClick={running ? onStop : undefined}
|
|
220
|
+
aria-label={running ? "Stop" : "Send"}
|
|
221
|
+
className="flex size-8 shrink-0 motion-press cursor-pointer items-center justify-center rounded-full bg-interactive-primary text-text-inverse focus-visible:outline-2 focus-visible:outline-text-brand"
|
|
222
|
+
>
|
|
223
|
+
<span className="motion-swap size-8 place-items-center">
|
|
224
|
+
<Stop aria-hidden weight="fill" data-hidden={!running || undefined} className="size-3.5" />
|
|
225
|
+
<ArrowUp aria-hidden data-hidden={running || undefined} className="size-4" />
|
|
226
|
+
</span>
|
|
227
|
+
</button>
|
|
228
|
+
</div>
|
|
229
|
+
</form>
|
|
230
|
+
</section>
|
|
231
|
+
);
|
|
232
|
+
}
|