@protolabsai/ui 0.20.0 → 0.22.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/dist/plugin-kit.css +379 -0
- package/package.json +2 -1
- package/src/Empty.stories.tsx +32 -0
- package/src/Grid.stories.tsx +37 -0
- package/src/TabBar.stories.tsx +74 -0
- package/src/ToolCard.stories.tsx +71 -0
- package/src/layout.tsx +40 -1
- package/src/navigation.tsx +119 -0
- package/src/primitives.tsx +31 -3
- package/src/styles/layout.css +43 -0
- package/src/styles/navigation.css +131 -0
- package/src/styles/primitives.css +36 -0
- package/src/styles/tool-card.css +167 -0
- package/src/styles.css +1 -0
- package/src/tool-card.tsx +177 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { cx } from "./internal";
|
|
4
|
+
|
|
5
|
+
// The disclosure FRAME for a streamed tool call. Presentation only: the host owns
|
|
6
|
+
// the ToolCall data (name/status/duration come straight off the wire type) and fills
|
|
7
|
+
// the body slot with its own per-tool value renderers. The DS deliberately does NOT
|
|
8
|
+
// render tool output — that's domain logic (calculator equations, fetched-page
|
|
9
|
+
// previews, KV grids …) that stays app-side. See protoContent issue #187.
|
|
10
|
+
|
|
11
|
+
/** Mirrors the host's `ToolCall.status`. */
|
|
12
|
+
export type ToolCardStatus = "running" | "done" | "error";
|
|
13
|
+
|
|
14
|
+
/** Optional wrapper around a stream of ToolCards — a gapped column. */
|
|
15
|
+
export function ToolCardList({ children, className }: { children: ReactNode; className?: string }) {
|
|
16
|
+
return <div className={cx("pl-toolcard-list", className)}>{children}</div>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function formatDuration(ms: number): string {
|
|
20
|
+
return ms < 1000 ? `${Math.round(ms)}ms` : `${(ms / 1000).toFixed(1)}s`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function StatusGlyph({ status }: { status: ToolCardStatus }) {
|
|
24
|
+
if (status === "running") {
|
|
25
|
+
return (
|
|
26
|
+
<span className="pl-toolcard__status pl-toolcard__status--running" aria-label="running">
|
|
27
|
+
<svg className="pl-toolcard__spin" viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round">
|
|
28
|
+
<path d="M21 12a9 9 0 1 1-6.2-8.5" />
|
|
29
|
+
</svg>
|
|
30
|
+
</span>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
if (status === "error") {
|
|
34
|
+
return (
|
|
35
|
+
<span className="pl-toolcard__status pl-toolcard__status--error" aria-label="error">
|
|
36
|
+
<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round">
|
|
37
|
+
<path d="M6 6l12 12M18 6L6 18" />
|
|
38
|
+
</svg>
|
|
39
|
+
</span>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
return (
|
|
43
|
+
<span className="pl-toolcard__status pl-toolcard__status--done" aria-label="done">
|
|
44
|
+
<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round">
|
|
45
|
+
<path d="M20 6L9 17l-5-5" />
|
|
46
|
+
</svg>
|
|
47
|
+
</span>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** A single tool-call disclosure: a row with leading icon, name, duration, and a
|
|
52
|
+
* running→done/error status glyph, expanding to a body slot. Collapsed and STICKY
|
|
53
|
+
* by default — the open state is the user's explicit choice, so the message never
|
|
54
|
+
* reflows as tools start and finish. `status`/`name`/`duration` come off the host's
|
|
55
|
+
* ToolCall; `children` is the rendered input/result (compose `ToolSection`s);
|
|
56
|
+
* `nested` holds child cards for a subagent `task` (indented). */
|
|
57
|
+
export function ToolCard({
|
|
58
|
+
name,
|
|
59
|
+
status,
|
|
60
|
+
icon,
|
|
61
|
+
duration,
|
|
62
|
+
defaultOpen = false,
|
|
63
|
+
nested,
|
|
64
|
+
children,
|
|
65
|
+
className,
|
|
66
|
+
}: {
|
|
67
|
+
name: ReactNode;
|
|
68
|
+
status: ToolCardStatus;
|
|
69
|
+
/** Leading glyph — the host maps tool→icon (the DS stays icon-agnostic). */
|
|
70
|
+
icon?: ReactNode;
|
|
71
|
+
/** Elapsed ms (`ToolCall.durationMs`) — rendered "820ms" / "1.2s". */
|
|
72
|
+
duration?: number;
|
|
73
|
+
defaultOpen?: boolean;
|
|
74
|
+
/** Indented child tool cards for a subagent `task`. */
|
|
75
|
+
nested?: ReactNode;
|
|
76
|
+
/** Expanded body — the rendered input/result (compose `ToolSection`s). */
|
|
77
|
+
children?: ReactNode;
|
|
78
|
+
className?: string;
|
|
79
|
+
}) {
|
|
80
|
+
const [open, setOpen] = useState(defaultOpen);
|
|
81
|
+
const hasBody = children != null;
|
|
82
|
+
const card = (
|
|
83
|
+
<div className={cx("pl-toolcard", `pl-toolcard--${status}`, className)}>
|
|
84
|
+
<button
|
|
85
|
+
type="button"
|
|
86
|
+
className="pl-toolcard__head"
|
|
87
|
+
aria-expanded={hasBody ? open : undefined}
|
|
88
|
+
disabled={!hasBody}
|
|
89
|
+
onClick={() => setOpen((v) => !v)}
|
|
90
|
+
>
|
|
91
|
+
<span
|
|
92
|
+
className={cx(
|
|
93
|
+
"pl-toolcard__caret",
|
|
94
|
+
!hasBody && "pl-toolcard__caret--hidden",
|
|
95
|
+
open && "pl-toolcard__caret--open",
|
|
96
|
+
)}
|
|
97
|
+
aria-hidden
|
|
98
|
+
>
|
|
99
|
+
<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round">
|
|
100
|
+
<path d="M9 6l6 6-6 6" />
|
|
101
|
+
</svg>
|
|
102
|
+
</span>
|
|
103
|
+
{icon != null && (
|
|
104
|
+
<span className="pl-toolcard__icon" aria-hidden>
|
|
105
|
+
{icon}
|
|
106
|
+
</span>
|
|
107
|
+
)}
|
|
108
|
+
<span className="pl-toolcard__name">{name}</span>
|
|
109
|
+
{duration != null && <span className="pl-toolcard__dur">{formatDuration(duration)}</span>}
|
|
110
|
+
<StatusGlyph status={status} />
|
|
111
|
+
</button>
|
|
112
|
+
{hasBody && open && <div className="pl-toolcard__body">{children}</div>}
|
|
113
|
+
</div>
|
|
114
|
+
);
|
|
115
|
+
if (nested == null) return card;
|
|
116
|
+
return (
|
|
117
|
+
<div className="pl-toolcard-group">
|
|
118
|
+
{card}
|
|
119
|
+
<div className="pl-toolcard__children">{nested}</div>
|
|
120
|
+
</div>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function CopyButton({ text }: { text: string }) {
|
|
125
|
+
const [copied, setCopied] = useState(false);
|
|
126
|
+
return (
|
|
127
|
+
<button
|
|
128
|
+
type="button"
|
|
129
|
+
className="pl-toolcard__copy"
|
|
130
|
+
title="Copy to clipboard"
|
|
131
|
+
aria-label={copied ? "Copied" : "Copy"}
|
|
132
|
+
onClick={async () => {
|
|
133
|
+
try {
|
|
134
|
+
await navigator.clipboard.writeText(text);
|
|
135
|
+
setCopied(true);
|
|
136
|
+
setTimeout(() => setCopied(false), 1200);
|
|
137
|
+
} catch {
|
|
138
|
+
/* clipboard unavailable (insecure context / denied) */
|
|
139
|
+
}
|
|
140
|
+
}}
|
|
141
|
+
>
|
|
142
|
+
{copied ? (
|
|
143
|
+
<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round">
|
|
144
|
+
<path d="M20 6L9 17l-5-5" />
|
|
145
|
+
</svg>
|
|
146
|
+
) : (
|
|
147
|
+
<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
148
|
+
<rect x="9" y="9" width="13" height="13" rx="2" />
|
|
149
|
+
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
|
|
150
|
+
</svg>
|
|
151
|
+
)}
|
|
152
|
+
</button>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** A labelled body section with an optional copy affordance — e.g. input / result.
|
|
157
|
+
* Compose inside `ToolCard`'s body; fill `children` with the rendered value. */
|
|
158
|
+
export function ToolSection({
|
|
159
|
+
label,
|
|
160
|
+
copyText,
|
|
161
|
+
children,
|
|
162
|
+
}: {
|
|
163
|
+
label: ReactNode;
|
|
164
|
+
/** When set, renders a self-contained copy-to-clipboard button. */
|
|
165
|
+
copyText?: string;
|
|
166
|
+
children: ReactNode;
|
|
167
|
+
}) {
|
|
168
|
+
return (
|
|
169
|
+
<div className="pl-toolcard__section">
|
|
170
|
+
<div className="pl-toolcard__section-head">
|
|
171
|
+
<span className="pl-toolcard__label">{label}</span>
|
|
172
|
+
{copyText != null && <CopyButton text={copyText} />}
|
|
173
|
+
</div>
|
|
174
|
+
{children}
|
|
175
|
+
</div>
|
|
176
|
+
);
|
|
177
|
+
}
|