rift-ds 1.7.0 → 1.9.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 +31 -8
- package/bin/rift-ds.mjs +142 -29
- package/bin/templates/chat/.env.chat.example +21 -0
- package/bin/templates/chat/app/api/chat/feedback/route.ts +12 -0
- package/bin/templates/chat/app/api/chat/followups/route.ts +23 -0
- package/bin/templates/chat/app/api/chat/guardrails.ts +18 -0
- package/bin/templates/chat/app/api/chat/route.ts +31 -0
- package/bin/templates/chat/components/chat/ChatMount.tsx +52 -0
- package/bin/templates/chat/components/chat/models.ts +29 -0
- package/chat/AssistantTurn.d.ts +5 -0
- package/chat/AssistantTurn.js +82 -0
- package/chat/ChatAgentPane.css +56 -0
- package/chat/ChatAgentPane.d.ts +24 -0
- package/chat/ChatAgentPane.js +28 -0
- package/chat/ChatArtifactCard.d.ts +13 -0
- package/chat/ChatArtifactCard.js +21 -0
- package/chat/ChatArtifactPane.d.ts +18 -0
- package/chat/ChatArtifactPane.js +43 -0
- package/chat/ChatLauncher.d.ts +64 -0
- package/chat/ChatLauncher.js +347 -0
- package/chat/ChatProvider.d.ts +55 -0
- package/chat/ChatProvider.js +196 -0
- package/chat/ChatTabs.d.ts +13 -0
- package/chat/ChatTabs.js +24 -0
- package/chat/ChatThreads.d.ts +26 -0
- package/chat/ChatThreads.js +55 -0
- package/chat/ChatWidget.css +1259 -0
- package/chat/ChatWidget.d.ts +115 -0
- package/chat/ChatWidget.js +709 -0
- package/chat/DemoWelcome.d.ts +13 -0
- package/chat/DemoWelcome.js +131 -0
- package/chat/FollowupSuggestions.d.ts +23 -0
- package/chat/FollowupSuggestions.js +34 -0
- package/chat/ResponseActions.d.ts +18 -0
- package/chat/ResponseActions.js +79 -0
- package/chat/TurnContent.d.ts +11 -0
- package/chat/TurnContent.js +19 -0
- package/chat/artifacts.d.ts +5 -0
- package/chat/artifacts.js +14 -0
- package/chat/context.d.ts +82 -0
- package/chat/context.js +29 -0
- package/chat/demo-agent-panes.d.ts +5 -0
- package/chat/demo-agent-panes.js +49 -0
- package/chat/demo-agent.d.ts +3 -0
- package/chat/demo-agent.js +28 -0
- package/chat/demo-artifacts.d.ts +6 -0
- package/chat/demo-artifacts.js +80 -0
- package/chat/demo-card-components.d.ts +48 -0
- package/chat/demo-card-components.js +66 -0
- package/chat/demo-cards.d.ts +32 -0
- package/chat/demo-cards.js +59 -0
- package/chat/demo-fixtures.d.ts +20 -0
- package/chat/demo-fixtures.js +113 -0
- package/chat/demo-script.d.ts +6 -0
- package/chat/demo-script.js +237 -0
- package/chat/demo.css +52 -0
- package/chat/demo.d.ts +10 -0
- package/chat/demo.js +28 -0
- package/chat/followups.d.ts +41 -0
- package/chat/followups.js +55 -0
- package/chat/greeting.d.ts +11 -0
- package/chat/greeting.js +21 -0
- package/chat/index.d.ts +37 -0
- package/chat/index.js +90 -0
- package/chat/placement.d.ts +48 -0
- package/chat/placement.js +39 -0
- package/chat/queries.d.ts +18 -0
- package/chat/queries.js +26 -0
- package/chat/registry.d.ts +25 -0
- package/chat/registry.js +9 -0
- package/chat/registry.json.d.ts +74 -0
- package/chat/registry.json.js +11 -0
- package/chat/server.d.ts +217 -0
- package/chat/server.js +488 -0
- package/chat/sim.d.ts +71 -0
- package/chat/sim.js +125 -0
- package/chat/starters.d.ts +15 -0
- package/chat/starters.js +38 -0
- package/chat/suggestions.d.ts +22 -0
- package/chat/suggestions.js +9 -0
- package/chat/threads.d.ts +73 -0
- package/chat/threads.js +270 -0
- package/chat/transport.d.ts +49 -0
- package/chat/transport.js +105 -0
- package/chat/types.d.ts +246 -0
- package/chat/types.js +18 -0
- package/chat/upstash.d.ts +63 -0
- package/chat/upstash.js +147 -0
- package/chat/useChat.d.ts +64 -0
- package/chat/useChat.js +241 -0
- package/cli/templates/chat/app/api/chat/feedback/route.d.ts +3 -0
- package/cli/templates/chat/app/api/chat/followups/route.d.ts +3 -0
- package/cli/templates/chat/app/api/chat/guardrails.d.ts +10 -0
- package/cli/templates/chat/app/api/chat/route.d.ts +4 -0
- package/cli/templates/chat/components/chat/ChatMount.d.ts +1 -0
- package/cli/templates/chat/components/chat/models.d.ts +11 -0
- package/components/AgentRail/AgentRail.css +14 -5
- package/components/AgentRail/AgentRail.d.ts +2 -0
- package/components/AgentRail/AgentRail.js +3 -2
- package/components/ArtifactCard/ArtifactCard.css +106 -0
- package/components/ArtifactCard/ArtifactCard.d.ts +27 -0
- package/components/ArtifactCard/ArtifactCard.js +29 -0
- package/components/ArtifactPanel/ArtifactPanel.css +159 -0
- package/components/ArtifactPanel/ArtifactPanel.d.ts +70 -0
- package/components/ArtifactPanel/ArtifactPanel.js +131 -0
- package/components/ShaderField/ShaderField.d.ts +3 -2
- package/components/ShaderField/useShaderField.d.ts +2 -1
- package/components/ShaderField/useShaderField.js +6 -6
- package/components/ThreadPanel/ThreadPanel.css +87 -33
- package/components/ThreadPanel/ThreadPanel.js +21 -10
- package/components/registry.json +16 -0
- package/components/registry.json.d.ts +16 -0
- package/components/registry.json.js +1 -1
- package/fonts/material-symbols.css +2 -1
- package/index.d.ts +2 -0
- package/index.js +4 -0
- package/package.json +39 -2
- package/tokens/presets/coral.css +18 -2
- package/tokens/presets/forest.css +18 -2
- package/tokens/presets/gold.css +18 -2
- package/tokens/presets/mono.css +18 -2
- package/tokens/presets/pink.css +18 -2
- package/tokens/presets/terminal.css +18 -2
- package/tokens/presets/violet.css +18 -2
- package/tokens/presets/warm.css +18 -2
- package/tokens/presets/zest.css +18 -2
- package/tokens/registry.json +6 -0
- package/tokens/registry.json.d.ts +6 -0
- package/tokens/registry.json.js +1 -1
- package/tokens/tokens-dark.css +10 -0
- package/tokens/tokens-light.css +10 -0
package/chat/useChat.js
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useState, useRef, useEffect, useCallback } from "react";
|
|
3
|
+
import { revokeAttachmentUrls } from "../components/Attachment/useAttachments.js";
|
|
4
|
+
import { createStreamReveal } from "../components/StreamingText/useStreamReveal.js";
|
|
5
|
+
let nextId = 0;
|
|
6
|
+
const makeId = () => `turn-${++nextId}`;
|
|
7
|
+
const DEFAULT_PLACEHOLDER_COPY = {
|
|
8
|
+
empty: "The reply came back empty. Try sending that again.",
|
|
9
|
+
stopped: "Stopped before a reply arrived.",
|
|
10
|
+
failed: "Something went wrong while replying. Try sending that again."
|
|
11
|
+
};
|
|
12
|
+
function useChat(transport, placeholders = DEFAULT_PLACEHOLDER_COPY) {
|
|
13
|
+
const [turns, setTurns] = useState([]);
|
|
14
|
+
const [live, setLive] = useState(null);
|
|
15
|
+
const [modelLabel, setModelLabel] = useState(null);
|
|
16
|
+
const [modelPolicy, setModelPolicy] = useState(null);
|
|
17
|
+
const abortRef = useRef(null);
|
|
18
|
+
const busyRef = useRef(false);
|
|
19
|
+
const historyRef = useRef([]);
|
|
20
|
+
const generationRef = useRef(0);
|
|
21
|
+
useEffect(() => () => abortRef.current?.abort(), []);
|
|
22
|
+
const turnsRef = useRef(turns);
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
turnsRef.current = turns;
|
|
25
|
+
}, [turns]);
|
|
26
|
+
const releaseAttachments = useCallback(() => {
|
|
27
|
+
for (const turn of turnsRef.current) {
|
|
28
|
+
if (turn.attachments) revokeAttachmentUrls(turn.attachments);
|
|
29
|
+
}
|
|
30
|
+
}, []);
|
|
31
|
+
useEffect(() => releaseAttachments, [releaseAttachments]);
|
|
32
|
+
const streaming = live !== null;
|
|
33
|
+
const commit = useCallback(
|
|
34
|
+
(finished, startedAt, historyText) => {
|
|
35
|
+
if (historyText !== null && historyText !== "") {
|
|
36
|
+
historyRef.current = [
|
|
37
|
+
...historyRef.current,
|
|
38
|
+
{ role: "assistant", content: historyText }
|
|
39
|
+
];
|
|
40
|
+
}
|
|
41
|
+
setLive(null);
|
|
42
|
+
setTurns((prev) => [
|
|
43
|
+
...prev,
|
|
44
|
+
{
|
|
45
|
+
id: finished.id,
|
|
46
|
+
role: "assistant",
|
|
47
|
+
text: finished.text,
|
|
48
|
+
tracePoints: finished.tracePoints,
|
|
49
|
+
exchangeId: finished.exchangeId,
|
|
50
|
+
followups: finished.followups,
|
|
51
|
+
content: finished.content,
|
|
52
|
+
card: finished.card,
|
|
53
|
+
durationSeconds: finished.durationSeconds ?? Math.max(1, Math.round((Date.now() - startedAt) / 1e3))
|
|
54
|
+
}
|
|
55
|
+
]);
|
|
56
|
+
},
|
|
57
|
+
[]
|
|
58
|
+
);
|
|
59
|
+
const send = useCallback(
|
|
60
|
+
(text, options) => {
|
|
61
|
+
const trimmed = text.trim();
|
|
62
|
+
const attachments = options?.attachments?.length ? options.attachments : void 0;
|
|
63
|
+
if (trimmed === "" && !attachments || busyRef.current) return false;
|
|
64
|
+
busyRef.current = true;
|
|
65
|
+
const controller = new AbortController();
|
|
66
|
+
abortRef.current = controller;
|
|
67
|
+
const generation = generationRef.current;
|
|
68
|
+
const startedAt = Date.now();
|
|
69
|
+
const history = [
|
|
70
|
+
...historyRef.current,
|
|
71
|
+
{
|
|
72
|
+
role: "user",
|
|
73
|
+
content: trimmed,
|
|
74
|
+
attachments: attachments?.map(({ name, kind, size }) => ({ name, kind, size }))
|
|
75
|
+
}
|
|
76
|
+
];
|
|
77
|
+
historyRef.current = history;
|
|
78
|
+
const userTurnId = makeId();
|
|
79
|
+
setTurns((prev) => [
|
|
80
|
+
...prev,
|
|
81
|
+
{
|
|
82
|
+
id: userTurnId,
|
|
83
|
+
role: "user",
|
|
84
|
+
text: trimmed,
|
|
85
|
+
content: options?.content,
|
|
86
|
+
attachments
|
|
87
|
+
}
|
|
88
|
+
]);
|
|
89
|
+
let current = {
|
|
90
|
+
id: makeId(),
|
|
91
|
+
phase: "thinking",
|
|
92
|
+
statusLabel: "Thinking",
|
|
93
|
+
tracePoints: [],
|
|
94
|
+
text: ""
|
|
95
|
+
};
|
|
96
|
+
setLive(current);
|
|
97
|
+
const update = (next) => {
|
|
98
|
+
current = next;
|
|
99
|
+
setLive(next);
|
|
100
|
+
};
|
|
101
|
+
const reveal = createStreamReveal({
|
|
102
|
+
onUpdate: (visible) => update({ ...current, text: visible }),
|
|
103
|
+
/* Someone who has asked for less motion is asking for less of this. */
|
|
104
|
+
paced: !window.matchMedia("(prefers-reduced-motion: reduce)").matches
|
|
105
|
+
});
|
|
106
|
+
const run = async () => {
|
|
107
|
+
try {
|
|
108
|
+
for await (const event of transport(history, controller.signal)) {
|
|
109
|
+
if (generation !== generationRef.current || controller.signal.aborted) break;
|
|
110
|
+
switch (event.type) {
|
|
111
|
+
case "model":
|
|
112
|
+
setModelLabel(event.label);
|
|
113
|
+
break;
|
|
114
|
+
case "models":
|
|
115
|
+
setModelPolicy({ default: event.default, locked: event.locked });
|
|
116
|
+
break;
|
|
117
|
+
case "exchange":
|
|
118
|
+
current = { ...current, exchangeId: event.id };
|
|
119
|
+
break;
|
|
120
|
+
case "followups":
|
|
121
|
+
current = { ...current, followups: event.items };
|
|
122
|
+
break;
|
|
123
|
+
case "content":
|
|
124
|
+
current = { ...current, content: event.node };
|
|
125
|
+
break;
|
|
126
|
+
case "card":
|
|
127
|
+
current = { ...current, card: event.card };
|
|
128
|
+
break;
|
|
129
|
+
case "status":
|
|
130
|
+
update({
|
|
131
|
+
...current,
|
|
132
|
+
statusLabel: event.label,
|
|
133
|
+
tracePoints: event.point ? [...current.tracePoints, event.point] : current.tracePoints
|
|
134
|
+
});
|
|
135
|
+
break;
|
|
136
|
+
case "delta":
|
|
137
|
+
current = {
|
|
138
|
+
...current,
|
|
139
|
+
phase: "streaming",
|
|
140
|
+
// Thinking ends at the first delta; freeze the duration then.
|
|
141
|
+
durationSeconds: current.durationSeconds ?? Math.max(1, Math.round((Date.now() - startedAt) / 1e3))
|
|
142
|
+
};
|
|
143
|
+
reveal.append(event.text);
|
|
144
|
+
break;
|
|
145
|
+
/* A guardrail line and a failure are not the model speaking, so
|
|
146
|
+
they are not paced like it: they appear whole. */
|
|
147
|
+
case "notice":
|
|
148
|
+
case "error":
|
|
149
|
+
current = {
|
|
150
|
+
...current,
|
|
151
|
+
phase: "streaming",
|
|
152
|
+
durationSeconds: current.durationSeconds ?? 1
|
|
153
|
+
};
|
|
154
|
+
reveal.showWhole(event.type === "notice" ? event.text : event.message);
|
|
155
|
+
break;
|
|
156
|
+
case "done":
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
if (event.type === "done") break;
|
|
160
|
+
}
|
|
161
|
+
if (controller.signal.aborted || generation !== generationRef.current) {
|
|
162
|
+
reveal.flush();
|
|
163
|
+
} else {
|
|
164
|
+
await reveal.drained();
|
|
165
|
+
}
|
|
166
|
+
reveal.cancel();
|
|
167
|
+
if (generation !== generationRef.current) return;
|
|
168
|
+
if (current.text === "") {
|
|
169
|
+
commit({ ...current, text: placeholders.empty }, startedAt, null);
|
|
170
|
+
} else {
|
|
171
|
+
commit(current, startedAt, current.text);
|
|
172
|
+
}
|
|
173
|
+
} catch (err) {
|
|
174
|
+
reveal.flush();
|
|
175
|
+
if (generation !== generationRef.current) ;
|
|
176
|
+
else if (controller.signal.aborted) {
|
|
177
|
+
if (current.text === "") {
|
|
178
|
+
commit({ ...current, text: placeholders.stopped }, startedAt, null);
|
|
179
|
+
} else {
|
|
180
|
+
commit(current, startedAt, current.text);
|
|
181
|
+
}
|
|
182
|
+
} else {
|
|
183
|
+
if (current.text === "") {
|
|
184
|
+
commit({ ...current, text: placeholders.failed }, startedAt, null);
|
|
185
|
+
} else {
|
|
186
|
+
commit(current, startedAt, current.text);
|
|
187
|
+
}
|
|
188
|
+
console.error(err);
|
|
189
|
+
}
|
|
190
|
+
} finally {
|
|
191
|
+
if (generation === generationRef.current) busyRef.current = false;
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
void run();
|
|
195
|
+
return true;
|
|
196
|
+
},
|
|
197
|
+
[transport, commit, placeholders]
|
|
198
|
+
);
|
|
199
|
+
const setTurnFeedback = useCallback((turnId, feedback) => {
|
|
200
|
+
setTurns(
|
|
201
|
+
(prev) => prev.map((turn) => turn.id === turnId ? { ...turn, feedback } : turn)
|
|
202
|
+
);
|
|
203
|
+
}, []);
|
|
204
|
+
const setTurnFollowups = useCallback((turnId, followups) => {
|
|
205
|
+
setTurns(
|
|
206
|
+
(prev) => prev.map((turn) => turn.id === turnId ? { ...turn, followups } : turn)
|
|
207
|
+
);
|
|
208
|
+
}, []);
|
|
209
|
+
const injectTurn = useCallback((turn) => {
|
|
210
|
+
setTurns((prev) => [...prev, { ...turn, id: makeId() }]);
|
|
211
|
+
}, []);
|
|
212
|
+
const stop = useCallback(() => {
|
|
213
|
+
abortRef.current?.abort();
|
|
214
|
+
}, []);
|
|
215
|
+
const reset = useCallback(() => {
|
|
216
|
+
generationRef.current += 1;
|
|
217
|
+
abortRef.current?.abort();
|
|
218
|
+
busyRef.current = false;
|
|
219
|
+
historyRef.current = [];
|
|
220
|
+
releaseAttachments();
|
|
221
|
+
setTurns([]);
|
|
222
|
+
setLive(null);
|
|
223
|
+
}, [releaseAttachments]);
|
|
224
|
+
return {
|
|
225
|
+
turns,
|
|
226
|
+
live,
|
|
227
|
+
streaming,
|
|
228
|
+
modelLabel,
|
|
229
|
+
modelPolicy,
|
|
230
|
+
send,
|
|
231
|
+
stop,
|
|
232
|
+
reset,
|
|
233
|
+
injectTurn,
|
|
234
|
+
setTurnFeedback,
|
|
235
|
+
setTurnFollowups
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
export {
|
|
239
|
+
DEFAULT_PLACEHOLDER_COPY,
|
|
240
|
+
useChat
|
|
241
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rate limits, a daily budget with model tiers, the exchange log and the
|
|
3
|
+
* feedback store, over Upstash Redis. Shared by the three chat routes.
|
|
4
|
+
*
|
|
5
|
+
* Fails open: with KV_REST_API_URL and KV_REST_API_TOKEN unset, every
|
|
6
|
+
* request is allowed and nothing is logged, so the chat works on day one
|
|
7
|
+
* and the limits arrive when you add the two variables. Set CHAT_VISITOR_SALT
|
|
8
|
+
* too: it salts the hashed visitor address the limits key on.
|
|
9
|
+
*/
|
|
10
|
+
export declare const guardrails: import('../../../../../../chat/server').ChatGuardrails;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function ChatMount(): import("react").JSX.Element;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ChatModelOption } from 'rift-ds/chat';
|
|
2
|
+
/**
|
|
3
|
+
* The models your chat offers. The widget's picker shows `label` and
|
|
4
|
+
* `description`; the client sends `value`; the server calls `id`. Shared by
|
|
5
|
+
* the route and the mount so the two can never disagree. Edit freely.
|
|
6
|
+
*/
|
|
7
|
+
export declare const CHAT_MODELS: ChatModelOption[];
|
|
8
|
+
/** What serves when the visitor has not chosen. */
|
|
9
|
+
export declare const DEFAULT_CHAT_MODEL: string;
|
|
10
|
+
/** The model that writes the three follow-up questions under an answer. */
|
|
11
|
+
export declare const FOLLOWUP_MODEL = "claude-haiku-5-5";
|
|
@@ -38,11 +38,12 @@
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
/* ============================================
|
|
41
|
-
COLLAPSE — the way out
|
|
42
|
-
belongs to the host
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
41
|
+
COLLAPSE — the way out. By default a chevron pinned to the LEADING
|
|
42
|
+
corner: the trailing one usually belongs to the host, since a rail seated
|
|
43
|
+
at a card's edge puts the card's own controls above it, and two crosses a
|
|
44
|
+
few pixels apart read as two ways to dismiss the same thing. A host that
|
|
45
|
+
has weighed that and wants the way out where a panel's close is looked
|
|
46
|
+
for takes the close variant below: a cross in the trailing corner.
|
|
46
47
|
============================================ */
|
|
47
48
|
|
|
48
49
|
.ds-agent-rail__collapse {
|
|
@@ -72,6 +73,14 @@
|
|
|
72
73
|
transition: background-color var(--motion-duration-base) var(--motion-ease-standard);
|
|
73
74
|
}
|
|
74
75
|
|
|
76
|
+
/* The close variant: a cross in the trailing corner, where a panel's own
|
|
77
|
+
way out usually sits. The host that asks for it has decided its controls
|
|
78
|
+
and this one do not collide. */
|
|
79
|
+
.ds-agent-rail__collapse--close {
|
|
80
|
+
left: auto;
|
|
81
|
+
right: var(--padding-300);
|
|
82
|
+
}
|
|
83
|
+
|
|
75
84
|
.ds-agent-rail__collapse .material-symbols-rounded {
|
|
76
85
|
--icon-size: var(--icon-size-600);
|
|
77
86
|
}
|
|
@@ -49,6 +49,8 @@ type AgentRailOwnProps = {
|
|
|
49
49
|
onCollapse?: () => void;
|
|
50
50
|
/** Accessible name for the collapse affordance. */
|
|
51
51
|
collapseLabel?: string;
|
|
52
|
+
/** How the collapse affordance is drawn. `chevron` (the default) pins a chevron to the leading corner, clear of a host's own controls. `close` pins a cross to the trailing corner, for a rail that is its card's only way out or whose host wants the way out where a panel's close usually is. */
|
|
53
|
+
collapseVariant?: 'chevron' | 'close';
|
|
52
54
|
/** Rendered under the pane, outside its scroll — a sign-out row, a quiet hint. */
|
|
53
55
|
footer?: React.ReactNode;
|
|
54
56
|
/** Additional CSS classes */
|
|
@@ -15,6 +15,7 @@ const AgentRail = React.forwardRef(
|
|
|
15
15
|
ariaLabel,
|
|
16
16
|
onCollapse,
|
|
17
17
|
collapseLabel = "Collapse the agent panel",
|
|
18
|
+
collapseVariant = "chevron",
|
|
18
19
|
footer,
|
|
19
20
|
className = "",
|
|
20
21
|
...rest
|
|
@@ -26,10 +27,10 @@ const AgentRail = React.forwardRef(
|
|
|
26
27
|
"button",
|
|
27
28
|
{
|
|
28
29
|
type: "button",
|
|
29
|
-
className: `${baseClass}__collapse`,
|
|
30
|
+
className: `${baseClass}__collapse${collapseVariant === "close" ? ` ${baseClass}__collapse--close` : ""}`,
|
|
30
31
|
"aria-label": collapseLabel,
|
|
31
32
|
onClick: onCollapse,
|
|
32
|
-
children: /* @__PURE__ */ jsx("span", { className: "material-symbols-rounded", "aria-hidden": "true", children: "chevron_right" })
|
|
33
|
+
children: /* @__PURE__ */ jsx("span", { className: "material-symbols-rounded", "aria-hidden": "true", children: collapseVariant === "close" ? "close" : "chevron_right" })
|
|
33
34
|
}
|
|
34
35
|
),
|
|
35
36
|
profile && /* @__PURE__ */ jsxs("header", { className: `${baseClass}__header`, children: [
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/* ============================================
|
|
2
|
+
ARTIFACT CARD COMPONENT
|
|
3
|
+
An artifact's place in a conversation: a mark, the name over its kind,
|
|
4
|
+
and a cue for what a press does. One row, the width of a card, and a
|
|
5
|
+
toggle button: pressed while its artifact is on stage in the panel.
|
|
6
|
+
============================================ */
|
|
7
|
+
|
|
8
|
+
/* Base */
|
|
9
|
+
|
|
10
|
+
.ds-artifact-card {
|
|
11
|
+
box-sizing: border-box;
|
|
12
|
+
display: flex;
|
|
13
|
+
align-items: center;
|
|
14
|
+
gap: var(--gap-300);
|
|
15
|
+
width: 100%;
|
|
16
|
+
max-width: 360px;
|
|
17
|
+
padding: var(--padding-300);
|
|
18
|
+
border: var(--border-025) solid var(--color-bg-container-border);
|
|
19
|
+
border-radius: var(--radius-600);
|
|
20
|
+
background-color: var(--color-bg-container-primary);
|
|
21
|
+
cursor: pointer;
|
|
22
|
+
text-align: left;
|
|
23
|
+
transition:
|
|
24
|
+
background-color var(--motion-duration-fast) var(--motion-ease-standard),
|
|
25
|
+
border-color var(--motion-duration-fast) var(--motion-ease-standard);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.ds-artifact-card:hover {
|
|
29
|
+
background-color: var(--color-action-passive-bg-hover);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.ds-artifact-card:focus-visible {
|
|
33
|
+
outline: 2px solid var(--color-action-primary-bg);
|
|
34
|
+
outline-offset: 2px;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/* States */
|
|
38
|
+
|
|
39
|
+
/* On stage, the tile holds the "on stage" fill ThreadPanel's active row and
|
|
40
|
+
ThreadTabs' active tab wear. */
|
|
41
|
+
.ds-artifact-card--open {
|
|
42
|
+
background-color: var(--color-bg-container-tertiary);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/* The mark steps to the card's resting fill while the card holds the
|
|
46
|
+
on-stage one, so it stays a tile on the card instead of dissolving into
|
|
47
|
+
a fill of nearly its own value. */
|
|
48
|
+
.ds-artifact-card--open .ds-artifact-card__mark {
|
|
49
|
+
background-color: var(--color-bg-container-primary);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.ds-artifact-card__mark {
|
|
53
|
+
--icon-size: var(--icon-size-600);
|
|
54
|
+
display: flex;
|
|
55
|
+
flex: none;
|
|
56
|
+
align-items: center;
|
|
57
|
+
justify-content: center;
|
|
58
|
+
width: 40px;
|
|
59
|
+
height: 40px;
|
|
60
|
+
/* Concentric with the card: its radius less the padding between them,
|
|
61
|
+
computed from the live tokens so the corner holds under any theme's
|
|
62
|
+
radius scale, and squares off with a squared theme. */
|
|
63
|
+
border-radius: max(0px, calc(var(--radius-600) - var(--padding-300)));
|
|
64
|
+
background-color: var(--color-bg-container-secondary);
|
|
65
|
+
color: var(--color-icon-primary);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.ds-artifact-card__text {
|
|
69
|
+
display: flex;
|
|
70
|
+
flex: 1;
|
|
71
|
+
flex-direction: column;
|
|
72
|
+
min-width: 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.ds-artifact-card__title {
|
|
76
|
+
overflow: hidden;
|
|
77
|
+
text-overflow: ellipsis;
|
|
78
|
+
white-space: nowrap;
|
|
79
|
+
font-family: var(--font-paragraph-sm-emphasis-family);
|
|
80
|
+
font-size: var(--font-paragraph-sm-emphasis-size);
|
|
81
|
+
font-weight: var(--font-paragraph-sm-emphasis-weight);
|
|
82
|
+
line-height: var(--font-paragraph-sm-emphasis-line-height);
|
|
83
|
+
color: var(--color-text-primary);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.ds-artifact-card__meta {
|
|
87
|
+
overflow: hidden;
|
|
88
|
+
text-overflow: ellipsis;
|
|
89
|
+
white-space: nowrap;
|
|
90
|
+
font-family: var(--font-caption-family);
|
|
91
|
+
font-size: var(--font-caption-size);
|
|
92
|
+
font-weight: var(--font-caption-weight);
|
|
93
|
+
line-height: var(--font-caption-line-height);
|
|
94
|
+
color: var(--color-text-tertiary);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.ds-artifact-card__cue {
|
|
98
|
+
--icon-size: var(--icon-size-500);
|
|
99
|
+
flex: none;
|
|
100
|
+
color: var(--color-icon-secondary);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.ds-artifact-card:disabled {
|
|
104
|
+
opacity: 0.4;
|
|
105
|
+
cursor: not-allowed;
|
|
106
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { default as React } from 'react';
|
|
2
|
+
/** Props owned by ArtifactCard itself — everything else falls through to the root `<button>`. */
|
|
3
|
+
type ArtifactCardOwnProps = {
|
|
4
|
+
/** The artifact's name. Collides with the native `title` tooltip attribute on purpose: the card shows its name, it does not hide it in a tooltip. */
|
|
5
|
+
title: React.ReactNode;
|
|
6
|
+
/** A quiet line under the name: what kind of thing this is, or which version. */
|
|
7
|
+
meta?: React.ReactNode;
|
|
8
|
+
/** The leading mark — Material Symbol name (string) or custom element (ReactNode). */
|
|
9
|
+
icon?: string | React.ReactNode;
|
|
10
|
+
/** Whether this card's artifact is on stage in its panel. The card holds the on-stage fill, announces itself pressed, and its trailing cue turns from open to close. */
|
|
11
|
+
open?: boolean;
|
|
12
|
+
/** Additional CSS classes */
|
|
13
|
+
className?: string;
|
|
14
|
+
};
|
|
15
|
+
export interface ArtifactCardProps extends ArtifactCardOwnProps, Omit<React.ComponentPropsWithoutRef<'button'>, keyof ArtifactCardOwnProps | 'type'> {
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* ArtifactCard is an artifact's place in a conversation: one row naming
|
|
19
|
+
* something the chat made, which opens it in an ArtifactPanel. It is a
|
|
20
|
+
* toggle button, so `open` is the whole state and `onClick` the whole
|
|
21
|
+
* behaviour; the host decides what opening means.
|
|
22
|
+
*
|
|
23
|
+
* Hook-free and stateless, so it renders from a Server Component until it
|
|
24
|
+
* is given a handler.
|
|
25
|
+
*/
|
|
26
|
+
export declare const ArtifactCard: React.ForwardRefExoticComponent<ArtifactCardProps & React.RefAttributes<HTMLButtonElement>>;
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import "./ArtifactCard.css";
|
|
4
|
+
import "../../fonts/material-symbols.css";
|
|
5
|
+
const baseClass = "ds-artifact-card";
|
|
6
|
+
const ArtifactCard = React.forwardRef(
|
|
7
|
+
({ title, meta, icon = "draft", open = false, className = "", ...rest }, ref) => /* @__PURE__ */ jsxs(
|
|
8
|
+
"button",
|
|
9
|
+
{
|
|
10
|
+
ref,
|
|
11
|
+
...rest,
|
|
12
|
+
type: "button",
|
|
13
|
+
className: [baseClass, open && `${baseClass}--open`, className].filter(Boolean).join(" "),
|
|
14
|
+
"aria-pressed": open,
|
|
15
|
+
children: [
|
|
16
|
+
typeof icon === "string" ? /* @__PURE__ */ jsx("span", { className: `${baseClass}__mark material-symbols-rounded`, "aria-hidden": "true", children: icon }) : /* @__PURE__ */ jsx("span", { className: `${baseClass}__mark`, children: icon }),
|
|
17
|
+
/* @__PURE__ */ jsxs("span", { className: `${baseClass}__text`, children: [
|
|
18
|
+
/* @__PURE__ */ jsx("span", { className: `${baseClass}__title`, children: title }),
|
|
19
|
+
meta && /* @__PURE__ */ jsx("span", { className: `${baseClass}__meta`, children: meta })
|
|
20
|
+
] }),
|
|
21
|
+
/* @__PURE__ */ jsx("span", { className: `${baseClass}__cue material-symbols-rounded`, "aria-hidden": "true", children: open ? "close" : "open_in_new_down" })
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
)
|
|
25
|
+
);
|
|
26
|
+
ArtifactCard.displayName = "ArtifactCard";
|
|
27
|
+
export {
|
|
28
|
+
ArtifactCard
|
|
29
|
+
};
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/* ============================================
|
|
2
|
+
ARTIFACT PANEL COMPONENT
|
|
3
|
+
The surface a chat opens beside the conversation to show something it
|
|
4
|
+
made. A header names the artifact and carries its controls; the body is
|
|
5
|
+
the preview (live content, or a sandboxed frame) or the source.
|
|
6
|
+
Deliberately surface-less, like AgentRail and ThreadPanel — the host
|
|
7
|
+
paints the card it sits in, and decides where that card goes.
|
|
8
|
+
|
|
9
|
+
One scroll region only: the body. The header stays put, so the controls
|
|
10
|
+
never leave with the content.
|
|
11
|
+
============================================ */
|
|
12
|
+
|
|
13
|
+
/* Base */
|
|
14
|
+
|
|
15
|
+
.ds-artifact-panel {
|
|
16
|
+
/* Width, height and padding combine on the root, so the component declares
|
|
17
|
+
its own sizing model rather than leaning on a host reset. */
|
|
18
|
+
box-sizing: border-box;
|
|
19
|
+
display: flex;
|
|
20
|
+
flex-direction: column;
|
|
21
|
+
width: 100%;
|
|
22
|
+
height: 100%;
|
|
23
|
+
min-width: 0;
|
|
24
|
+
min-height: 0;
|
|
25
|
+
overflow: hidden;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/* ============================================
|
|
29
|
+
HEADER — the artifact's name, then its controls
|
|
30
|
+
============================================ */
|
|
31
|
+
|
|
32
|
+
/* The controls stand --padding-300 in from the top and trailing edges, the
|
|
33
|
+
inset AgentRail gives its own corner control, so the two panels that
|
|
34
|
+
share a chat's trailing seat put their close in the same place and a
|
|
35
|
+
rounded host card has room to turn its corner around it. */
|
|
36
|
+
.ds-artifact-panel__header {
|
|
37
|
+
display: flex;
|
|
38
|
+
flex: none;
|
|
39
|
+
align-items: center;
|
|
40
|
+
justify-content: space-between;
|
|
41
|
+
gap: var(--gap-300);
|
|
42
|
+
min-width: 0;
|
|
43
|
+
padding: var(--padding-300) var(--padding-300) var(--padding-300)
|
|
44
|
+
var(--padding-400);
|
|
45
|
+
border-bottom: var(--border-025) solid var(--color-bg-container-border);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.ds-artifact-panel__heading {
|
|
49
|
+
display: flex;
|
|
50
|
+
flex: 1;
|
|
51
|
+
align-items: baseline;
|
|
52
|
+
gap: var(--gap-200);
|
|
53
|
+
min-width: 0;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* The mark sits on the row's centre line while the name and its meta share
|
|
57
|
+
a baseline. */
|
|
58
|
+
.ds-artifact-panel__icon {
|
|
59
|
+
--icon-size: var(--icon-size-500);
|
|
60
|
+
display: inline-flex;
|
|
61
|
+
flex: none;
|
|
62
|
+
align-self: center;
|
|
63
|
+
color: var(--color-icon-secondary);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* ChatHeader's title face, so the two headers read as one line of type.
|
|
67
|
+
The name outranks its meta when the row runs short: the meta carries a
|
|
68
|
+
shrink factor large enough to give way entirely before the name loses a
|
|
69
|
+
letter. */
|
|
70
|
+
.ds-artifact-panel__title {
|
|
71
|
+
flex: 0 1 auto;
|
|
72
|
+
min-width: 0;
|
|
73
|
+
margin: 0;
|
|
74
|
+
overflow: hidden;
|
|
75
|
+
text-overflow: ellipsis;
|
|
76
|
+
white-space: nowrap;
|
|
77
|
+
font-family: var(--font-title-body-family);
|
|
78
|
+
font-size: var(--font-title-body-size);
|
|
79
|
+
font-weight: var(--font-title-body-weight);
|
|
80
|
+
line-height: var(--font-title-body-line-height);
|
|
81
|
+
color: var(--color-text-primary);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.ds-artifact-panel__meta {
|
|
85
|
+
flex: 0 1000 auto;
|
|
86
|
+
min-width: 0;
|
|
87
|
+
overflow: hidden;
|
|
88
|
+
text-overflow: ellipsis;
|
|
89
|
+
white-space: nowrap;
|
|
90
|
+
font-family: var(--font-caption-family);
|
|
91
|
+
font-size: var(--font-caption-size);
|
|
92
|
+
font-weight: var(--font-caption-weight);
|
|
93
|
+
line-height: var(--font-caption-line-height);
|
|
94
|
+
color: var(--color-text-tertiary);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.ds-artifact-panel__controls {
|
|
98
|
+
display: flex;
|
|
99
|
+
flex: none;
|
|
100
|
+
align-items: center;
|
|
101
|
+
/* The row is the buttons' height whatever it holds: the view switch
|
|
102
|
+
stands a hair taller than a button, and a header that grew with it
|
|
103
|
+
would move every control when an artifact with source took the stage. */
|
|
104
|
+
height: 40px;
|
|
105
|
+
gap: var(--gap-100);
|
|
106
|
+
min-width: 0;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/* The view switch stands a beat clear of the icon buttons it leads. */
|
|
110
|
+
.ds-artifact-panel__controls > .ds-segmented-control {
|
|
111
|
+
margin-inline-end: var(--gap-100);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/* ============================================
|
|
115
|
+
BODY — the preview or the source
|
|
116
|
+
============================================ */
|
|
117
|
+
|
|
118
|
+
.ds-artifact-panel__body {
|
|
119
|
+
display: flex;
|
|
120
|
+
flex: 1;
|
|
121
|
+
flex-direction: column;
|
|
122
|
+
min-width: 0;
|
|
123
|
+
min-height: 0;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/* Live content scrolls inside the body and brings its own padding: a
|
|
127
|
+
dashboard wants an inset, a full-bleed page does not. */
|
|
128
|
+
.ds-artifact-panel__content,
|
|
129
|
+
.ds-artifact-panel__code {
|
|
130
|
+
flex: 1;
|
|
131
|
+
min-width: 0;
|
|
132
|
+
min-height: 0;
|
|
133
|
+
overflow: auto;
|
|
134
|
+
/* ThreadPanel's tucked-away thin scrollbar: no track fill, the thumb
|
|
135
|
+
showing only while the region is hovered or scrolled from the keyboard. */
|
|
136
|
+
scrollbar-width: thin;
|
|
137
|
+
scrollbar-color: transparent transparent;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.ds-artifact-panel__content:hover,
|
|
141
|
+
.ds-artifact-panel__content:focus-within,
|
|
142
|
+
.ds-artifact-panel__code:hover,
|
|
143
|
+
.ds-artifact-panel__code:focus-within {
|
|
144
|
+
scrollbar-color: var(--color-bg-container-border) transparent;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.ds-artifact-panel__code {
|
|
148
|
+
padding: var(--padding-400);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/* A framed document is its own page: it fills the body edge to edge and
|
|
152
|
+
owns its own scrolling. */
|
|
153
|
+
.ds-artifact-panel__frame {
|
|
154
|
+
display: block;
|
|
155
|
+
flex: 1;
|
|
156
|
+
width: 100%;
|
|
157
|
+
min-height: 0;
|
|
158
|
+
border: none;
|
|
159
|
+
}
|