@slipstream-ui/ai 0.0.0-stage → 0.4.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/LICENSE +25 -0
- package/dist/index.d.mts +579 -0
- package/dist/index.d.ts +579 -0
- package/dist/index.js +1778 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1706 -0
- package/dist/index.mjs.map +1 -0
- package/dist/manifests/attachment.manifest.json +33 -0
- package/dist/manifests/branch.manifest.json +65 -0
- package/dist/manifests/chain-thought.manifest.json +39 -0
- package/dist/manifests/code-block.manifest.json +50 -0
- package/dist/manifests/command-palette.manifest.json +80 -0
- package/dist/manifests/confirmation.manifest.json +143 -0
- package/dist/manifests/conversation.manifest.json +290 -0
- package/dist/manifests/inline-citation.manifest.json +46 -0
- package/dist/manifests/message-actions.manifest.json +35 -0
- package/dist/manifests/message.manifest.json +235 -0
- package/dist/manifests/prompt-input.manifest.json +334 -0
- package/dist/manifests/reasoning.manifest.json +177 -0
- package/dist/manifests/response.manifest.json +51 -0
- package/dist/manifests/source.manifest.json +78 -0
- package/dist/manifests/streaming-indicator.manifest.json +39 -0
- package/dist/manifests/suggestions.manifest.json +45 -0
- package/dist/manifests/tool-call.manifest.json +158 -0
- package/package.json +61 -3
- package/src/adapter/adapter.test.tsx +83 -0
- package/src/adapter/hooks.ts +82 -0
- package/src/adapter/index.ts +3 -0
- package/src/adapter/messages.ts +49 -0
- package/src/adapter/types.ts +24 -0
- package/src/components/attachment/Attachment.test.tsx +72 -0
- package/src/components/attachment/Attachment.tsx +84 -0
- package/src/components/attachment/AttachmentList.tsx +30 -0
- package/src/components/attachment/index.ts +5 -0
- package/src/components/branch/Branch.test.tsx +52 -0
- package/src/components/branch/Branch.tsx +77 -0
- package/src/components/branch/index.ts +2 -0
- package/src/components/chainthought/ChainThought.test.tsx +34 -0
- package/src/components/chainthought/ChainThought.tsx +45 -0
- package/src/components/chainthought/index.ts +2 -0
- package/src/components/code-block/CodeBlock.test.tsx +46 -0
- package/src/components/code-block/CodeBlock.tsx +95 -0
- package/src/components/code-block/index.ts +2 -0
- package/src/components/command-palette/CommandPalette.test.tsx +74 -0
- package/src/components/command-palette/CommandPalette.tsx +156 -0
- package/src/components/command-palette/index.ts +2 -0
- package/src/components/confirmation/Confirmation.test.tsx +97 -0
- package/src/components/confirmation/Confirmation.tsx +146 -0
- package/src/components/confirmation/index.ts +2 -0
- package/src/components/conversation/Conversation.test.tsx +49 -0
- package/src/components/conversation/Conversation.tsx +301 -0
- package/src/components/conversation/Conversation.welcome.test.tsx +73 -0
- package/src/components/conversation/index.ts +2 -0
- package/src/components/inline-citation/InlineCitation.test.tsx +42 -0
- package/src/components/inline-citation/InlineCitation.tsx +56 -0
- package/src/components/inline-citation/index.ts +2 -0
- package/src/components/message/Message.error-edit.test.tsx +60 -0
- package/src/components/message/Message.test.tsx +80 -0
- package/src/components/message/Message.tsx +205 -0
- package/src/components/message/index.ts +2 -0
- package/src/components/message-actions/MessageActions.test.tsx +77 -0
- package/src/components/message-actions/MessageActions.tsx +134 -0
- package/src/components/message-actions/index.ts +2 -0
- package/src/components/prompt-input/PromptInput.compound.test.tsx +150 -0
- package/src/components/prompt-input/PromptInput.test.tsx +74 -0
- package/src/components/prompt-input/PromptInput.tsx +584 -0
- package/src/components/prompt-input/index.ts +21 -0
- package/src/components/reasoning/Reasoning.duration.test.tsx +35 -0
- package/src/components/reasoning/Reasoning.test.tsx +58 -0
- package/src/components/reasoning/Reasoning.tsx +85 -0
- package/src/components/reasoning/index.ts +2 -0
- package/src/components/response/Response.test.tsx +69 -0
- package/src/components/response/Response.tsx +75 -0
- package/src/components/response/close-incomplete-markdown.ts +47 -0
- package/src/components/response/index.ts +3 -0
- package/src/components/source/Source.tsx +51 -0
- package/src/components/source/Sources.tsx +42 -0
- package/src/components/source/index.ts +5 -0
- package/src/components/source/source.test.tsx +72 -0
- package/src/components/streaming/StreamingIndicator.test.tsx +26 -0
- package/src/components/streaming/StreamingIndicator.tsx +46 -0
- package/src/components/streaming/index.ts +2 -0
- package/src/components/suggestion/Suggestion.test.tsx +43 -0
- package/src/components/suggestion/Suggestion.tsx +47 -0
- package/src/components/suggestion/index.ts +2 -0
- package/src/components/tool-call/ToolCall.test.tsx +45 -0
- package/src/components/tool-call/ToolCall.tsx +101 -0
- package/src/components/tool-call/index.ts +2 -0
- package/src/index.ts +29 -0
- package/src/manifests/attachment.manifest.json +33 -0
- package/src/manifests/branch.manifest.json +65 -0
- package/src/manifests/chain-thought.manifest.json +39 -0
- package/src/manifests/code-block.manifest.json +50 -0
- package/src/manifests/command-palette.manifest.json +80 -0
- package/src/manifests/confirmation.manifest.json +143 -0
- package/src/manifests/conversation.manifest.json +290 -0
- package/src/manifests/inline-citation.manifest.json +46 -0
- package/src/manifests/message-actions.manifest.json +35 -0
- package/src/manifests/message.manifest.json +235 -0
- package/src/manifests/prompt-input.manifest.json +334 -0
- package/src/manifests/reasoning.manifest.json +177 -0
- package/src/manifests/response.manifest.json +51 -0
- package/src/manifests/source.manifest.json +78 -0
- package/src/manifests/streaming-indicator.manifest.json +39 -0
- package/src/manifests/suggestions.manifest.json +45 -0
- package/src/manifests/tool-call.manifest.json +158 -0
- package/src/test/jest-axe.d.ts +16 -0
- package/src/test/setup.ts +18 -0
- package/src/test/vitest-matchers.d.ts +10 -0
- package/src/types/index.ts +40 -0
- package/README.md +0 -3
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1706 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
// src/components/message/Message.tsx
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { Avatar, AvatarImage, AvatarFallback, Button } from "@slipstream-ui/core";
|
|
6
|
+
import { cn } from "@slipstream-ui/core";
|
|
7
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
+
var Message = React.forwardRef(
|
|
9
|
+
({
|
|
10
|
+
message,
|
|
11
|
+
className,
|
|
12
|
+
showAvatar = true,
|
|
13
|
+
avatarSrc,
|
|
14
|
+
avatarFallback,
|
|
15
|
+
userLabel = "You",
|
|
16
|
+
assistantLabel = "Assistant",
|
|
17
|
+
renderContent,
|
|
18
|
+
isStreaming = false,
|
|
19
|
+
error,
|
|
20
|
+
onRetry,
|
|
21
|
+
onEditContent,
|
|
22
|
+
labels,
|
|
23
|
+
...props
|
|
24
|
+
}, ref) => {
|
|
25
|
+
const [isEditing, setIsEditing] = React.useState(false);
|
|
26
|
+
const [draft, setDraft] = React.useState(message.content);
|
|
27
|
+
const t = {
|
|
28
|
+
retry: labels?.retry ?? "Retry",
|
|
29
|
+
edit: labels?.edit ?? "Edit",
|
|
30
|
+
save: labels?.save ?? "Save & resend",
|
|
31
|
+
cancel: labels?.cancel ?? "Cancel"
|
|
32
|
+
};
|
|
33
|
+
const isUser = message.role === "user";
|
|
34
|
+
const isSystem = message.role === "system";
|
|
35
|
+
const displayLabel = isUser ? userLabel : message.role === "assistant" ? assistantLabel : message.role;
|
|
36
|
+
const startEdit = () => {
|
|
37
|
+
setDraft(message.content);
|
|
38
|
+
setIsEditing(true);
|
|
39
|
+
};
|
|
40
|
+
const commitEdit = () => {
|
|
41
|
+
const trimmed = draft.trim();
|
|
42
|
+
if (!trimmed) return;
|
|
43
|
+
setIsEditing(false);
|
|
44
|
+
onEditContent?.(trimmed);
|
|
45
|
+
};
|
|
46
|
+
return /* @__PURE__ */ jsxs(
|
|
47
|
+
"div",
|
|
48
|
+
{
|
|
49
|
+
ref,
|
|
50
|
+
"aria-busy": isStreaming || void 0,
|
|
51
|
+
className: cn(
|
|
52
|
+
"flex gap-3 p-4 rounded-lg",
|
|
53
|
+
isUser ? "bg-primary/10" : "bg-muted",
|
|
54
|
+
isSystem && "border-l-4 border-muted-foreground",
|
|
55
|
+
error && "border border-destructive/40",
|
|
56
|
+
className
|
|
57
|
+
),
|
|
58
|
+
...props,
|
|
59
|
+
children: [
|
|
60
|
+
showAvatar && !isSystem && /* @__PURE__ */ jsxs(Avatar, { className: "h-8 w-8 flex-shrink-0", children: [
|
|
61
|
+
avatarSrc && /* @__PURE__ */ jsx(AvatarImage, { src: avatarSrc, alt: displayLabel }),
|
|
62
|
+
/* @__PURE__ */ jsx(AvatarFallback, { className: cn(
|
|
63
|
+
"text-xs font-medium",
|
|
64
|
+
isUser ? "bg-primary text-primary-foreground" : "bg-secondary text-secondary-foreground"
|
|
65
|
+
), children: avatarFallback || (isUser ? "U" : "A") })
|
|
66
|
+
] }),
|
|
67
|
+
/* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
|
|
68
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 mb-1", children: [
|
|
69
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm font-medium", children: displayLabel }),
|
|
70
|
+
message.timestamp && /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: new Date(message.timestamp).toLocaleTimeString() })
|
|
71
|
+
] }),
|
|
72
|
+
error ? /* @__PURE__ */ jsxs("div", { role: "alert", className: "text-sm", children: [
|
|
73
|
+
/* @__PURE__ */ jsx("div", { className: "text-destructive mb-2", children: error }),
|
|
74
|
+
onRetry && /* @__PURE__ */ jsxs(Button, { size: "sm", variant: "outline", onClick: onRetry, children: [
|
|
75
|
+
"\u21BB ",
|
|
76
|
+
t.retry
|
|
77
|
+
] })
|
|
78
|
+
] }) : isEditing ? /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
79
|
+
/* @__PURE__ */ jsx(
|
|
80
|
+
"textarea",
|
|
81
|
+
{
|
|
82
|
+
value: draft,
|
|
83
|
+
onChange: (event) => setDraft(event.target.value),
|
|
84
|
+
onKeyDown: (event) => {
|
|
85
|
+
if (event.key === "Enter" && !event.shiftKey) {
|
|
86
|
+
event.preventDefault();
|
|
87
|
+
commitEdit();
|
|
88
|
+
}
|
|
89
|
+
if (event.key === "Escape") setIsEditing(false);
|
|
90
|
+
},
|
|
91
|
+
"aria-label": "Edit message",
|
|
92
|
+
autoFocus: true,
|
|
93
|
+
rows: Math.min(6, draft.split("\n").length + 1),
|
|
94
|
+
className: "w-full resize-none rounded-md border border-input bg-background px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
95
|
+
}
|
|
96
|
+
),
|
|
97
|
+
/* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
|
|
98
|
+
/* @__PURE__ */ jsx(Button, { size: "sm", onClick: commitEdit, children: t.save }),
|
|
99
|
+
/* @__PURE__ */ jsx(Button, { size: "sm", variant: "ghost", onClick: () => setIsEditing(false), children: t.cancel })
|
|
100
|
+
] })
|
|
101
|
+
] }) : /* @__PURE__ */ jsxs("div", { className: "text-sm whitespace-pre-wrap break-words", children: [
|
|
102
|
+
renderContent ? renderContent(message.content) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
103
|
+
message.content,
|
|
104
|
+
isStreaming && /* @__PURE__ */ jsx(
|
|
105
|
+
"span",
|
|
106
|
+
{
|
|
107
|
+
className: "inline-block w-[2px] h-[1em] ml-0.5 align-text-bottom bg-current animate-pulse motion-reduce:animate-none",
|
|
108
|
+
"data-testid": "streaming-cursor"
|
|
109
|
+
}
|
|
110
|
+
)
|
|
111
|
+
] }),
|
|
112
|
+
onEditContent && !isStreaming && /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsxs(
|
|
113
|
+
"button",
|
|
114
|
+
{
|
|
115
|
+
type: "button",
|
|
116
|
+
onClick: startEdit,
|
|
117
|
+
"aria-label": t.edit,
|
|
118
|
+
className: "mt-1 rounded px-1 py-0.5 text-xs text-muted-foreground hover:bg-accent hover:text-accent-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
119
|
+
children: [
|
|
120
|
+
"\u270E ",
|
|
121
|
+
t.edit
|
|
122
|
+
]
|
|
123
|
+
}
|
|
124
|
+
) })
|
|
125
|
+
] })
|
|
126
|
+
] })
|
|
127
|
+
]
|
|
128
|
+
}
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
);
|
|
132
|
+
Message.displayName = "Message";
|
|
133
|
+
var MemoizedMessage = React.memo(Message);
|
|
134
|
+
MemoizedMessage.displayName = "Message";
|
|
135
|
+
|
|
136
|
+
// src/components/message-actions/MessageActions.tsx
|
|
137
|
+
import * as React2 from "react";
|
|
138
|
+
import { cn as cn2 } from "@slipstream-ui/core";
|
|
139
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
140
|
+
var MessageActions = React2.forwardRef(
|
|
141
|
+
({ content, onCopy, onFeedback, onRegenerate, autohide = "always", labels, className, ...props }, ref) => {
|
|
142
|
+
const [copied, setCopied] = React2.useState(false);
|
|
143
|
+
const [rating, setRating] = React2.useState();
|
|
144
|
+
const t = {
|
|
145
|
+
copy: labels?.copy ?? "Copy",
|
|
146
|
+
copied: labels?.copied ?? "Copied",
|
|
147
|
+
thumbUp: labels?.thumbUp ?? "Good response",
|
|
148
|
+
thumbDown: labels?.thumbDown ?? "Bad response",
|
|
149
|
+
regenerate: labels?.regenerate ?? "Regenerate"
|
|
150
|
+
};
|
|
151
|
+
const handleCopy = async () => {
|
|
152
|
+
if (!content) return;
|
|
153
|
+
try {
|
|
154
|
+
await navigator.clipboard.writeText(content);
|
|
155
|
+
} catch {
|
|
156
|
+
}
|
|
157
|
+
setCopied(true);
|
|
158
|
+
window.setTimeout(() => setCopied(false), 2e3);
|
|
159
|
+
onCopy?.(content);
|
|
160
|
+
};
|
|
161
|
+
const handleRating = (next) => {
|
|
162
|
+
const updated = rating === next ? void 0 : next;
|
|
163
|
+
setRating(updated);
|
|
164
|
+
onFeedback?.(updated);
|
|
165
|
+
};
|
|
166
|
+
const actionButton = "rounded px-1.5 py-0.5 text-xs transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring";
|
|
167
|
+
const actionButtonIdle = "text-muted-foreground hover:bg-accent hover:text-accent-foreground";
|
|
168
|
+
return /* @__PURE__ */ jsxs2(
|
|
169
|
+
"div",
|
|
170
|
+
{
|
|
171
|
+
ref,
|
|
172
|
+
role: "toolbar",
|
|
173
|
+
"aria-label": "Message actions",
|
|
174
|
+
className: cn2(
|
|
175
|
+
"flex items-center gap-1 mt-2",
|
|
176
|
+
autohide !== "always" && "opacity-0 transition-opacity focus-within:opacity-100 group-hover:opacity-100 motion-reduce:opacity-100",
|
|
177
|
+
className
|
|
178
|
+
),
|
|
179
|
+
...props,
|
|
180
|
+
children: [
|
|
181
|
+
content !== void 0 && /* @__PURE__ */ jsx2("button", { type: "button", onClick: handleCopy, className: cn2(actionButton, actionButtonIdle), children: copied ? t.copied : t.copy }),
|
|
182
|
+
/* @__PURE__ */ jsx2(
|
|
183
|
+
"button",
|
|
184
|
+
{
|
|
185
|
+
type: "button",
|
|
186
|
+
onClick: () => handleRating("up"),
|
|
187
|
+
"aria-label": t.thumbUp,
|
|
188
|
+
"aria-pressed": rating === "up",
|
|
189
|
+
className: cn2(
|
|
190
|
+
actionButton,
|
|
191
|
+
rating === "up" ? "text-primary" : actionButtonIdle
|
|
192
|
+
),
|
|
193
|
+
children: "\u{1F44D}"
|
|
194
|
+
}
|
|
195
|
+
),
|
|
196
|
+
/* @__PURE__ */ jsx2(
|
|
197
|
+
"button",
|
|
198
|
+
{
|
|
199
|
+
type: "button",
|
|
200
|
+
onClick: () => handleRating("down"),
|
|
201
|
+
"aria-label": t.thumbDown,
|
|
202
|
+
"aria-pressed": rating === "down",
|
|
203
|
+
className: cn2(
|
|
204
|
+
actionButton,
|
|
205
|
+
rating === "down" ? "text-destructive" : actionButtonIdle
|
|
206
|
+
),
|
|
207
|
+
children: "\u{1F44E}"
|
|
208
|
+
}
|
|
209
|
+
),
|
|
210
|
+
onRegenerate && /* @__PURE__ */ jsx2("button", { type: "button", onClick: onRegenerate, className: cn2(actionButton, actionButtonIdle), children: t.regenerate })
|
|
211
|
+
]
|
|
212
|
+
}
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
);
|
|
216
|
+
MessageActions.displayName = "MessageActions";
|
|
217
|
+
|
|
218
|
+
// src/components/attachment/Attachment.tsx
|
|
219
|
+
import * as React3 from "react";
|
|
220
|
+
import { Spinner } from "@slipstream-ui/core";
|
|
221
|
+
import { cn as cn3 } from "@slipstream-ui/core";
|
|
222
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
223
|
+
var Attachment = React3.forwardRef(
|
|
224
|
+
({ attachment, onRemove, className, ...props }, ref) => {
|
|
225
|
+
return /* @__PURE__ */ jsxs3(
|
|
226
|
+
"div",
|
|
227
|
+
{
|
|
228
|
+
ref,
|
|
229
|
+
className: cn3(
|
|
230
|
+
"inline-flex items-center gap-2 rounded-md border border-border bg-muted/50 px-2 py-1.5 text-sm max-w-xs",
|
|
231
|
+
className
|
|
232
|
+
),
|
|
233
|
+
...props,
|
|
234
|
+
children: [
|
|
235
|
+
attachment.previewUrl ? /* @__PURE__ */ jsx3(
|
|
236
|
+
"img",
|
|
237
|
+
{
|
|
238
|
+
src: attachment.previewUrl,
|
|
239
|
+
alt: "",
|
|
240
|
+
"aria-hidden": "true",
|
|
241
|
+
className: "h-8 w-8 rounded object-cover flex-shrink-0"
|
|
242
|
+
}
|
|
243
|
+
) : /* @__PURE__ */ jsx3(
|
|
244
|
+
"span",
|
|
245
|
+
{
|
|
246
|
+
"aria-hidden": "true",
|
|
247
|
+
className: "flex h-8 w-8 items-center justify-center rounded bg-secondary text-[10px] font-semibold uppercase text-secondary-foreground flex-shrink-0",
|
|
248
|
+
children: (attachment.type ?? attachment.name.split(".").pop() ?? "?").slice(0, 4)
|
|
249
|
+
}
|
|
250
|
+
),
|
|
251
|
+
/* @__PURE__ */ jsxs3("div", { className: "min-w-0 flex-1", children: [
|
|
252
|
+
/* @__PURE__ */ jsx3("div", { className: "truncate font-medium text-foreground", title: attachment.name, children: attachment.name }),
|
|
253
|
+
/* @__PURE__ */ jsx3("div", { className: "text-xs text-muted-foreground", children: attachment.status === "uploading" ? "Uploading..." : [attachment.size, attachment.type].filter(Boolean).join(" \xB7 ") || null })
|
|
254
|
+
] }),
|
|
255
|
+
attachment.status === "uploading" && /* @__PURE__ */ jsx3(Spinner, { size: "sm", label: "Uploading", "aria-label": "Uploading", className: "flex-shrink-0" }),
|
|
256
|
+
onRemove && /* @__PURE__ */ jsx3(
|
|
257
|
+
"button",
|
|
258
|
+
{
|
|
259
|
+
type: "button",
|
|
260
|
+
onClick: () => onRemove(attachment),
|
|
261
|
+
"aria-label": `Remove ${attachment.name}`,
|
|
262
|
+
className: "flex-shrink-0 rounded p-0.5 text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
263
|
+
children: "\u2715"
|
|
264
|
+
}
|
|
265
|
+
)
|
|
266
|
+
]
|
|
267
|
+
}
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
);
|
|
271
|
+
Attachment.displayName = "Attachment";
|
|
272
|
+
|
|
273
|
+
// src/components/attachment/AttachmentList.tsx
|
|
274
|
+
import * as React4 from "react";
|
|
275
|
+
import { cn as cn4 } from "@slipstream-ui/core";
|
|
276
|
+
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
277
|
+
var AttachmentList = React4.forwardRef(
|
|
278
|
+
({ attachments, onRemove, className, ...props }, ref) => {
|
|
279
|
+
if (attachments.length === 0) {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
return /* @__PURE__ */ jsx4("div", { ref, className: cn4("flex flex-wrap gap-2", className), ...props, children: attachments.map((attachment) => /* @__PURE__ */ jsx4(Attachment, { attachment, onRemove }, attachment.id)) });
|
|
283
|
+
}
|
|
284
|
+
);
|
|
285
|
+
AttachmentList.displayName = "AttachmentList";
|
|
286
|
+
|
|
287
|
+
// src/components/response/Response.tsx
|
|
288
|
+
import * as React6 from "react";
|
|
289
|
+
import ReactMarkdown from "react-markdown";
|
|
290
|
+
import remarkGfm from "remark-gfm";
|
|
291
|
+
import { cn as cn6 } from "@slipstream-ui/core";
|
|
292
|
+
|
|
293
|
+
// src/components/code-block/CodeBlock.tsx
|
|
294
|
+
import * as React5 from "react";
|
|
295
|
+
import { Highlight, themes } from "prism-react-renderer";
|
|
296
|
+
import { cn as cn5 } from "@slipstream-ui/core";
|
|
297
|
+
import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
298
|
+
function useIsDark() {
|
|
299
|
+
const [dark, setDark] = React5.useState(false);
|
|
300
|
+
React5.useEffect(() => {
|
|
301
|
+
const el = document.documentElement;
|
|
302
|
+
setDark(el.classList.contains("dark"));
|
|
303
|
+
const observer = new MutationObserver(() => setDark(el.classList.contains("dark")));
|
|
304
|
+
observer.observe(el, { attributes: true, attributeFilter: ["class"] });
|
|
305
|
+
return () => observer.disconnect();
|
|
306
|
+
}, []);
|
|
307
|
+
return dark;
|
|
308
|
+
}
|
|
309
|
+
var CodeBlock = React5.forwardRef(
|
|
310
|
+
({ code, language = "tsx", showCopyButton = true, className, ...props }, ref) => {
|
|
311
|
+
const [copied, setCopied] = React5.useState(false);
|
|
312
|
+
const isDark = useIsDark();
|
|
313
|
+
const trimmedCode = code.replace(/\n$/, "");
|
|
314
|
+
const handleCopy = async () => {
|
|
315
|
+
try {
|
|
316
|
+
await navigator.clipboard.writeText(trimmedCode);
|
|
317
|
+
} catch {
|
|
318
|
+
}
|
|
319
|
+
setCopied(true);
|
|
320
|
+
window.setTimeout(() => setCopied(false), 2e3);
|
|
321
|
+
};
|
|
322
|
+
return /* @__PURE__ */ jsxs4(
|
|
323
|
+
"div",
|
|
324
|
+
{
|
|
325
|
+
ref,
|
|
326
|
+
className: cn5(
|
|
327
|
+
// bg-background (not muted): Prism light themes assume a white
|
|
328
|
+
// surface — on gray the comment/keyword tokens fall below AA.
|
|
329
|
+
"relative group rounded-md border border-border bg-background overflow-hidden",
|
|
330
|
+
className
|
|
331
|
+
),
|
|
332
|
+
...props,
|
|
333
|
+
children: [
|
|
334
|
+
/* @__PURE__ */ jsxs4("div", { className: "flex items-center justify-between px-3 py-1.5 border-b border-border bg-muted/60", children: [
|
|
335
|
+
/* @__PURE__ */ jsx5("span", { className: "text-xs font-mono text-muted-foreground uppercase", children: language }),
|
|
336
|
+
showCopyButton && /* @__PURE__ */ jsx5(
|
|
337
|
+
"button",
|
|
338
|
+
{
|
|
339
|
+
type: "button",
|
|
340
|
+
onClick: handleCopy,
|
|
341
|
+
"aria-label": copied ? "Copied" : "Copy code",
|
|
342
|
+
className: "text-xs text-muted-foreground hover:text-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded px-1",
|
|
343
|
+
children: copied ? "Copied!" : "Copy"
|
|
344
|
+
}
|
|
345
|
+
)
|
|
346
|
+
] }),
|
|
347
|
+
/* @__PURE__ */ jsx5(Highlight, { theme: isDark ? themes.vsDark : themes.github, code: trimmedCode, language, children: ({ style, tokens, getLineProps, getTokenProps }) => /* @__PURE__ */ jsx5(
|
|
348
|
+
"pre",
|
|
349
|
+
{
|
|
350
|
+
className: "overflow-x-auto p-4 text-xs leading-relaxed m-0",
|
|
351
|
+
style: { ...style, background: "transparent" },
|
|
352
|
+
children: tokens.map((line, i) => /* @__PURE__ */ jsx5("div", { ...getLineProps({ line }), children: line.map((token, key) => /* @__PURE__ */ jsx5("span", { ...getTokenProps({ token }) }, key)) }, i))
|
|
353
|
+
}
|
|
354
|
+
) })
|
|
355
|
+
]
|
|
356
|
+
}
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
);
|
|
360
|
+
CodeBlock.displayName = "CodeBlock";
|
|
361
|
+
|
|
362
|
+
// src/components/response/close-incomplete-markdown.ts
|
|
363
|
+
function closeIncompleteMarkdown(markdown) {
|
|
364
|
+
let text = markdown;
|
|
365
|
+
const fenceMatches = text.match(/^`{3,}/gm);
|
|
366
|
+
if (fenceMatches && fenceMatches.length % 2 === 1) {
|
|
367
|
+
text = `${text}
|
|
368
|
+
\`\`\``;
|
|
369
|
+
}
|
|
370
|
+
const firstFence = text.indexOf("```");
|
|
371
|
+
const hasUnclosedFence = fenceMatches !== null && fenceMatches.length % 2 === 1;
|
|
372
|
+
const prose = hasUnclosedFence && firstFence !== -1 ? text.slice(0, firstFence) : text;
|
|
373
|
+
const rest = hasUnclosedFence && firstFence !== -1 ? text.slice(firstFence) : "";
|
|
374
|
+
let repaired = prose;
|
|
375
|
+
for (const marker of ["**", "__", "~~"]) {
|
|
376
|
+
const occurrences = prose.split(marker).length - 1;
|
|
377
|
+
if (occurrences % 2 === 1) {
|
|
378
|
+
repaired += marker;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
for (const marker of ["*", "_"]) {
|
|
382
|
+
const singles = repaired.replace(/\*\*/g, "").replace(/__/g, "").split(marker).length - 1;
|
|
383
|
+
if (singles % 2 === 1) {
|
|
384
|
+
repaired += marker;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
const backtickCount = repaired.replace(/```/g, "").split("`").length - 1;
|
|
388
|
+
if (backtickCount % 2 === 1) {
|
|
389
|
+
repaired += "`";
|
|
390
|
+
}
|
|
391
|
+
return repaired + rest;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// src/components/response/Response.tsx
|
|
395
|
+
import { Fragment as Fragment2, jsx as jsx6 } from "react/jsx-runtime";
|
|
396
|
+
var Response = React6.forwardRef(
|
|
397
|
+
({ children, parseIncompleteMarkdown = true, className, ...props }, ref) => {
|
|
398
|
+
const content = React6.useMemo(
|
|
399
|
+
() => parseIncompleteMarkdown ? closeIncompleteMarkdown(children) : children,
|
|
400
|
+
[children, parseIncompleteMarkdown]
|
|
401
|
+
);
|
|
402
|
+
return /* @__PURE__ */ jsx6(
|
|
403
|
+
"div",
|
|
404
|
+
{
|
|
405
|
+
ref,
|
|
406
|
+
className: cn6(
|
|
407
|
+
"text-sm space-y-3 [&_a]:text-primary [&_a]:underline underline-offset-2",
|
|
408
|
+
"[&_ul]:list-disc [&_ul]:pl-5 [&_ol]:list-decimal [&_ol]:pl-5",
|
|
409
|
+
"[&_h1]:text-lg [&_h1]:font-semibold [&_h2]:text-base [&_h2]:font-semibold",
|
|
410
|
+
"[&_h3]:text-sm [&_h3]:font-semibold",
|
|
411
|
+
"[&_blockquote]:border-l-2 [&_blockquote]:border-border [&_blockquote]:pl-3 [&_blockquote]:text-muted-foreground",
|
|
412
|
+
"[&_table]:w-full [&_table]:text-xs [&_th]:border [&_th]:border-border [&_th]:p-1.5 [&_td]:border [&_td]:border-border [&_td]:p-1.5",
|
|
413
|
+
className
|
|
414
|
+
),
|
|
415
|
+
...props,
|
|
416
|
+
children: /* @__PURE__ */ jsx6(
|
|
417
|
+
ReactMarkdown,
|
|
418
|
+
{
|
|
419
|
+
remarkPlugins: [remarkGfm],
|
|
420
|
+
components: {
|
|
421
|
+
pre: ({ children: children2 }) => /* @__PURE__ */ jsx6(Fragment2, { children: children2 }),
|
|
422
|
+
code: ({ className: codeClassName, children: codeChildren }) => {
|
|
423
|
+
const raw = String(codeChildren).replace(/\n$/, "");
|
|
424
|
+
const match = /language-(\w+)/.exec(codeClassName ?? "");
|
|
425
|
+
const isInline = !match && !raw.includes("\n");
|
|
426
|
+
if (isInline) {
|
|
427
|
+
return /* @__PURE__ */ jsx6("code", { className: "bg-muted px-1 py-0.5 rounded text-xs font-mono before:content-none after:content-none", children: raw });
|
|
428
|
+
}
|
|
429
|
+
return /* @__PURE__ */ jsx6(CodeBlock, { code: raw, language: match?.[1] ?? "text" });
|
|
430
|
+
}
|
|
431
|
+
},
|
|
432
|
+
children: content
|
|
433
|
+
}
|
|
434
|
+
)
|
|
435
|
+
}
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
);
|
|
439
|
+
Response.displayName = "Response";
|
|
440
|
+
|
|
441
|
+
// src/components/confirmation/Confirmation.tsx
|
|
442
|
+
import * as React7 from "react";
|
|
443
|
+
import { Button as Button2 } from "@slipstream-ui/core";
|
|
444
|
+
import { cn as cn7 } from "@slipstream-ui/core";
|
|
445
|
+
import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
446
|
+
var statusChipClasses = {
|
|
447
|
+
pending: "bg-warning/10 text-warning border-warning/20",
|
|
448
|
+
approved: "bg-success/10 text-success border-success/20",
|
|
449
|
+
rejected: "bg-destructive/10 text-destructive border-destructive/20"
|
|
450
|
+
};
|
|
451
|
+
var pendingVariantClasses = {
|
|
452
|
+
default: "border-warning/40",
|
|
453
|
+
warning: "border-warning/40",
|
|
454
|
+
destructive: "border-destructive/40 bg-destructive/5",
|
|
455
|
+
info: "border-info/40",
|
|
456
|
+
success: "border-success/40"
|
|
457
|
+
};
|
|
458
|
+
var Confirmation = React7.forwardRef(
|
|
459
|
+
({
|
|
460
|
+
title,
|
|
461
|
+
description,
|
|
462
|
+
toolName,
|
|
463
|
+
status: controlledStatus,
|
|
464
|
+
variant = "default",
|
|
465
|
+
onApprove,
|
|
466
|
+
onReject,
|
|
467
|
+
approveVariant = "default",
|
|
468
|
+
rejectVariant = "outline",
|
|
469
|
+
showActions = true,
|
|
470
|
+
labels,
|
|
471
|
+
className,
|
|
472
|
+
...props
|
|
473
|
+
}, ref) => {
|
|
474
|
+
const [internalStatus, setInternalStatus] = React7.useState("pending");
|
|
475
|
+
const status = controlledStatus ?? internalStatus;
|
|
476
|
+
const t = {
|
|
477
|
+
approve: labels?.approve ?? "Approve",
|
|
478
|
+
reject: labels?.reject ?? "Reject",
|
|
479
|
+
approved: labels?.approved ?? "Approved",
|
|
480
|
+
rejected: labels?.rejected ?? "Rejected"
|
|
481
|
+
};
|
|
482
|
+
const decide = (next) => {
|
|
483
|
+
setInternalStatus(next);
|
|
484
|
+
(next === "approved" ? onApprove : onReject)?.();
|
|
485
|
+
};
|
|
486
|
+
return /* @__PURE__ */ jsxs5(
|
|
487
|
+
"div",
|
|
488
|
+
{
|
|
489
|
+
ref,
|
|
490
|
+
role: status === "pending" ? "alertdialog" : void 0,
|
|
491
|
+
"aria-label": status === "pending" ? title : void 0,
|
|
492
|
+
className: cn7(
|
|
493
|
+
"rounded-lg border p-4 bg-muted/30 max-w-md",
|
|
494
|
+
status === "pending" && pendingVariantClasses[variant],
|
|
495
|
+
className
|
|
496
|
+
),
|
|
497
|
+
...props,
|
|
498
|
+
children: [
|
|
499
|
+
/* @__PURE__ */ jsxs5("div", { className: "flex items-center justify-between gap-2 mb-2", children: [
|
|
500
|
+
/* @__PURE__ */ jsx7("div", { className: "text-sm font-medium text-foreground", children: title }),
|
|
501
|
+
/* @__PURE__ */ jsx7(
|
|
502
|
+
"span",
|
|
503
|
+
{
|
|
504
|
+
className: cn7(
|
|
505
|
+
"text-xs px-2 py-0.5 rounded-full border whitespace-nowrap",
|
|
506
|
+
statusChipClasses[status]
|
|
507
|
+
),
|
|
508
|
+
children: status === "approved" ? t.approved : status === "rejected" ? t.rejected : "Pending"
|
|
509
|
+
}
|
|
510
|
+
)
|
|
511
|
+
] }),
|
|
512
|
+
description && /* @__PURE__ */ jsx7("div", { className: "text-sm text-muted-foreground mb-1", children: description }),
|
|
513
|
+
toolName && /* @__PURE__ */ jsx7("div", { className: "text-xs font-mono text-muted-foreground mb-3", children: toolName }),
|
|
514
|
+
status === "pending" && showActions && /* @__PURE__ */ jsxs5("div", { className: "flex gap-2 mt-2", children: [
|
|
515
|
+
/* @__PURE__ */ jsx7(Button2, { size: "sm", variant: approveVariant, onClick: () => decide("approved"), children: t.approve }),
|
|
516
|
+
/* @__PURE__ */ jsx7(Button2, { size: "sm", variant: rejectVariant, onClick: () => decide("rejected"), children: t.reject })
|
|
517
|
+
] })
|
|
518
|
+
]
|
|
519
|
+
}
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
);
|
|
523
|
+
Confirmation.displayName = "Confirmation";
|
|
524
|
+
|
|
525
|
+
// src/components/branch/Branch.tsx
|
|
526
|
+
import * as React8 from "react";
|
|
527
|
+
import { cn as cn8 } from "@slipstream-ui/core";
|
|
528
|
+
import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
529
|
+
var Branch = React8.forwardRef(
|
|
530
|
+
({ count, selected, defaultSelected = 0, onSelect, labels, className, ...props }, ref) => {
|
|
531
|
+
const [internalIndex, setInternalIndex] = React8.useState(defaultSelected);
|
|
532
|
+
const isControlled = selected !== void 0;
|
|
533
|
+
const index = Math.min(Math.max(isControlled ? selected : internalIndex, 0), count - 1);
|
|
534
|
+
const t = {
|
|
535
|
+
previous: labels?.previous ?? "Previous response",
|
|
536
|
+
next: labels?.next ?? "Next response"
|
|
537
|
+
};
|
|
538
|
+
if (count < 2) return null;
|
|
539
|
+
const goTo = (next) => {
|
|
540
|
+
const clamped = Math.min(Math.max(next, 0), count - 1);
|
|
541
|
+
if (!isControlled) setInternalIndex(clamped);
|
|
542
|
+
onSelect?.(clamped);
|
|
543
|
+
};
|
|
544
|
+
return /* @__PURE__ */ jsxs6(
|
|
545
|
+
"div",
|
|
546
|
+
{
|
|
547
|
+
ref,
|
|
548
|
+
className: cn8("flex items-center gap-1 text-xs text-muted-foreground", className),
|
|
549
|
+
...props,
|
|
550
|
+
children: [
|
|
551
|
+
/* @__PURE__ */ jsx8(
|
|
552
|
+
"button",
|
|
553
|
+
{
|
|
554
|
+
type: "button",
|
|
555
|
+
onClick: () => goTo(index - 1),
|
|
556
|
+
disabled: index === 0,
|
|
557
|
+
"aria-label": t.previous,
|
|
558
|
+
className: "rounded px-1 hover:bg-accent hover:text-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-40",
|
|
559
|
+
children: "\u2039"
|
|
560
|
+
}
|
|
561
|
+
),
|
|
562
|
+
/* @__PURE__ */ jsxs6("span", { "aria-live": "polite", className: "tabular-nums", children: [
|
|
563
|
+
index + 1,
|
|
564
|
+
" / ",
|
|
565
|
+
count
|
|
566
|
+
] }),
|
|
567
|
+
/* @__PURE__ */ jsx8(
|
|
568
|
+
"button",
|
|
569
|
+
{
|
|
570
|
+
type: "button",
|
|
571
|
+
onClick: () => goTo(index + 1),
|
|
572
|
+
disabled: index === count - 1,
|
|
573
|
+
"aria-label": t.next,
|
|
574
|
+
className: "rounded px-1 hover:bg-accent hover:text-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-40",
|
|
575
|
+
children: "\u203A"
|
|
576
|
+
}
|
|
577
|
+
)
|
|
578
|
+
]
|
|
579
|
+
}
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
);
|
|
583
|
+
Branch.displayName = "Branch";
|
|
584
|
+
|
|
585
|
+
// src/components/inline-citation/InlineCitation.tsx
|
|
586
|
+
import * as React9 from "react";
|
|
587
|
+
import { cn as cn9 } from "@slipstream-ui/core";
|
|
588
|
+
import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
589
|
+
var InlineCitation = React9.forwardRef(
|
|
590
|
+
({ source, children, className, ...props }, ref) => {
|
|
591
|
+
return /* @__PURE__ */ jsxs7(
|
|
592
|
+
"span",
|
|
593
|
+
{
|
|
594
|
+
ref,
|
|
595
|
+
className: cn9("group relative inline focus-within:outline-none", className),
|
|
596
|
+
...props,
|
|
597
|
+
children: [
|
|
598
|
+
/* @__PURE__ */ jsxs7(
|
|
599
|
+
"button",
|
|
600
|
+
{
|
|
601
|
+
type: "button",
|
|
602
|
+
"aria-describedby": `citation-${source.id}-card`,
|
|
603
|
+
className: "inline-flex items-baseline rounded-sm text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
604
|
+
children: [
|
|
605
|
+
children ?? /* @__PURE__ */ jsx9("span", { children: source.title }),
|
|
606
|
+
/* @__PURE__ */ jsx9(
|
|
607
|
+
"span",
|
|
608
|
+
{
|
|
609
|
+
"aria-hidden": "true",
|
|
610
|
+
className: "inline-flex items-center justify-center align-super ml-0.5 h-4 min-w-4 px-1 rounded-full border border-border bg-muted text-[10px] font-medium text-muted-foreground group-hover:text-foreground transition-colors",
|
|
611
|
+
children: source.title.charAt(0).toUpperCase()
|
|
612
|
+
}
|
|
613
|
+
)
|
|
614
|
+
]
|
|
615
|
+
}
|
|
616
|
+
),
|
|
617
|
+
/* @__PURE__ */ jsxs7(
|
|
618
|
+
"span",
|
|
619
|
+
{
|
|
620
|
+
id: `citation-${source.id}-card`,
|
|
621
|
+
role: "tooltip",
|
|
622
|
+
className: "pointer-events-none absolute bottom-full left-1/2 z-10 mb-1 -translate-x-1/2 w-64 rounded-md border border-border bg-popover p-2.5 text-xs text-popover-foreground shadow-md opacity-0 group-hover:opacity-100 focus-within:opacity-100 transition-opacity",
|
|
623
|
+
children: [
|
|
624
|
+
/* @__PURE__ */ jsx9("span", { className: "block font-medium text-foreground truncate", children: source.title }),
|
|
625
|
+
source.snippet && /* @__PURE__ */ jsx9("span", { className: "mt-0.5 block text-muted-foreground line-clamp-3", children: source.snippet })
|
|
626
|
+
]
|
|
627
|
+
}
|
|
628
|
+
)
|
|
629
|
+
]
|
|
630
|
+
}
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
);
|
|
634
|
+
InlineCitation.displayName = "InlineCitation";
|
|
635
|
+
|
|
636
|
+
// src/components/conversation/Conversation.tsx
|
|
637
|
+
import * as React10 from "react";
|
|
638
|
+
import { cn as cn10 } from "@slipstream-ui/core";
|
|
639
|
+
import { jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
640
|
+
var PINNED_THRESHOLD = 80;
|
|
641
|
+
var DEFAULT_WELCOME_TITLE = "Start the conversation";
|
|
642
|
+
var DEFAULT_WELCOME_DESCRIPTION = "Ask anything \u2014 responses stream in real time.";
|
|
643
|
+
var DEFAULT_WELCOME_SUGGESTIONS = [
|
|
644
|
+
"Explain what you can do",
|
|
645
|
+
"Summarize a topic for me"
|
|
646
|
+
];
|
|
647
|
+
function transcriptToMarkdown(messages) {
|
|
648
|
+
return messages.map((message) => {
|
|
649
|
+
const speaker = message.role === "user" ? "You" : message.role === "assistant" ? "Assistant" : message.role;
|
|
650
|
+
const time = message.timestamp ? ` (${new Date(message.timestamp).toISOString()})` : "";
|
|
651
|
+
return `**${speaker}${time}:**
|
|
652
|
+
|
|
653
|
+
${message.content}`;
|
|
654
|
+
}).join("\n\n---\n\n");
|
|
655
|
+
}
|
|
656
|
+
var Conversation = React10.forwardRef(
|
|
657
|
+
({
|
|
658
|
+
messages,
|
|
659
|
+
className,
|
|
660
|
+
showAvatars = true,
|
|
661
|
+
userAvatarSrc,
|
|
662
|
+
userAvatarFallback,
|
|
663
|
+
assistantAvatarSrc,
|
|
664
|
+
assistantAvatarFallback,
|
|
665
|
+
isLoading = false,
|
|
666
|
+
loadingMessage = "Thinking...",
|
|
667
|
+
renderContent,
|
|
668
|
+
streamingMessageId,
|
|
669
|
+
showScrollButton = true,
|
|
670
|
+
showDownload = false,
|
|
671
|
+
messageErrors,
|
|
672
|
+
onRetryMessage,
|
|
673
|
+
welcomeTitle = DEFAULT_WELCOME_TITLE,
|
|
674
|
+
welcomeDescription = DEFAULT_WELCOME_DESCRIPTION,
|
|
675
|
+
welcomeSuggestions = DEFAULT_WELCOME_SUGGESTIONS,
|
|
676
|
+
onWelcomeSelect,
|
|
677
|
+
hasMoreHistory = false,
|
|
678
|
+
isLoadingMore = false,
|
|
679
|
+
onLoadMore,
|
|
680
|
+
...props
|
|
681
|
+
}, ref) => {
|
|
682
|
+
const scrollRef = React10.useRef(null);
|
|
683
|
+
const messagesEndRef = React10.useRef(null);
|
|
684
|
+
const [isUnpinned, setIsUnpinned] = React10.useState(false);
|
|
685
|
+
const prevScrollMetrics = React10.useRef(null);
|
|
686
|
+
const [announcement, setAnnouncement] = React10.useState("");
|
|
687
|
+
const prevStreamingRef = React10.useRef(void 0);
|
|
688
|
+
React10.useEffect(() => {
|
|
689
|
+
if (prevStreamingRef.current && !streamingMessageId) {
|
|
690
|
+
setAnnouncement("Response complete.");
|
|
691
|
+
}
|
|
692
|
+
prevStreamingRef.current = streamingMessageId;
|
|
693
|
+
}, [streamingMessageId]);
|
|
694
|
+
React10.useEffect(() => {
|
|
695
|
+
const container = scrollRef.current;
|
|
696
|
+
if (!container) return;
|
|
697
|
+
const updatePinned = () => {
|
|
698
|
+
const distanceFromBottom = container.scrollHeight - container.scrollTop - container.clientHeight;
|
|
699
|
+
setIsUnpinned(distanceFromBottom > PINNED_THRESHOLD);
|
|
700
|
+
};
|
|
701
|
+
updatePinned();
|
|
702
|
+
container.addEventListener("scroll", updatePinned, { passive: true });
|
|
703
|
+
return () => container.removeEventListener("scroll", updatePinned);
|
|
704
|
+
}, []);
|
|
705
|
+
React10.useLayoutEffect(() => {
|
|
706
|
+
const container = scrollRef.current;
|
|
707
|
+
if (!container || prevScrollMetrics.current === null) return;
|
|
708
|
+
const { height, top } = prevScrollMetrics.current;
|
|
709
|
+
container.scrollTop = container.scrollHeight - height + top;
|
|
710
|
+
prevScrollMetrics.current = null;
|
|
711
|
+
}, [messages]);
|
|
712
|
+
const captureAnchor = () => {
|
|
713
|
+
const container = scrollRef.current;
|
|
714
|
+
if (!container) return;
|
|
715
|
+
prevScrollMetrics.current = { height: container.scrollHeight, top: container.scrollTop };
|
|
716
|
+
};
|
|
717
|
+
const handleLoadMore = () => {
|
|
718
|
+
captureAnchor();
|
|
719
|
+
onLoadMore?.();
|
|
720
|
+
};
|
|
721
|
+
React10.useEffect(() => {
|
|
722
|
+
const container = scrollRef.current;
|
|
723
|
+
if (!container) return;
|
|
724
|
+
const distanceFromBottom = container.scrollHeight - container.scrollTop - container.clientHeight;
|
|
725
|
+
if (distanceFromBottom > PINNED_THRESHOLD) return;
|
|
726
|
+
container.scrollTo?.({
|
|
727
|
+
top: container.scrollHeight,
|
|
728
|
+
behavior: streamingMessageId ? "auto" : "smooth"
|
|
729
|
+
});
|
|
730
|
+
}, [messages, streamingMessageId]);
|
|
731
|
+
const scrollToLatest = () => {
|
|
732
|
+
const container = scrollRef.current;
|
|
733
|
+
if (!container) return;
|
|
734
|
+
container.scrollTo?.({ top: container.scrollHeight, behavior: "smooth" });
|
|
735
|
+
};
|
|
736
|
+
const handleDownload = () => {
|
|
737
|
+
const blob = new Blob([transcriptToMarkdown(messages)], { type: "text/markdown" });
|
|
738
|
+
const url = URL.createObjectURL(blob);
|
|
739
|
+
const anchor = document.createElement("a");
|
|
740
|
+
anchor.href = url;
|
|
741
|
+
anchor.download = "conversation.md";
|
|
742
|
+
anchor.click();
|
|
743
|
+
URL.revokeObjectURL(url);
|
|
744
|
+
};
|
|
745
|
+
const isEmpty = messages.length === 0 && !isLoading;
|
|
746
|
+
return /* @__PURE__ */ jsxs8("div", { ref, className: cn10("relative h-full w-full", className), ...props, children: [
|
|
747
|
+
/* @__PURE__ */ jsxs8("div", { ref: scrollRef, className: "flex flex-col gap-4 p-4 overflow-y-auto h-full", children: [
|
|
748
|
+
(hasMoreHistory || isLoadingMore) && /* @__PURE__ */ jsx10(
|
|
749
|
+
"button",
|
|
750
|
+
{
|
|
751
|
+
type: "button",
|
|
752
|
+
onClick: handleLoadMore,
|
|
753
|
+
disabled: isLoadingMore,
|
|
754
|
+
className: "mx-auto rounded-full border border-border bg-background px-3 py-1 text-xs text-muted-foreground hover:text-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50",
|
|
755
|
+
children: isLoadingMore ? "Loading\u2026" : "Load earlier messages"
|
|
756
|
+
}
|
|
757
|
+
),
|
|
758
|
+
isEmpty ? /* @__PURE__ */ jsxs8("div", { className: "flex flex-1 flex-col items-center justify-center gap-3 text-center py-16", children: [
|
|
759
|
+
/* @__PURE__ */ jsx10("h3", { className: "text-lg font-semibold", children: welcomeTitle }),
|
|
760
|
+
/* @__PURE__ */ jsx10("p", { className: "text-sm text-muted-foreground max-w-sm", children: welcomeDescription }),
|
|
761
|
+
welcomeSuggestions.length > 0 && /* @__PURE__ */ jsx10("div", { className: "mt-2 flex flex-col gap-2 w-full max-w-sm", children: welcomeSuggestions.map((suggestion) => /* @__PURE__ */ jsx10(
|
|
762
|
+
"button",
|
|
763
|
+
{
|
|
764
|
+
type: "button",
|
|
765
|
+
onClick: () => onWelcomeSelect?.(suggestion),
|
|
766
|
+
className: "rounded-md border border-border bg-background px-4 py-2.5 text-sm text-left text-muted-foreground hover:bg-accent hover:text-accent-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
767
|
+
children: suggestion
|
|
768
|
+
},
|
|
769
|
+
suggestion
|
|
770
|
+
)) })
|
|
771
|
+
] }) : messages.map((message) => /* @__PURE__ */ jsx10(
|
|
772
|
+
MemoizedMessage,
|
|
773
|
+
{
|
|
774
|
+
message,
|
|
775
|
+
showAvatar: showAvatars,
|
|
776
|
+
avatarSrc: message.role === "user" ? userAvatarSrc : assistantAvatarSrc,
|
|
777
|
+
avatarFallback: message.role === "user" ? userAvatarFallback : assistantAvatarFallback,
|
|
778
|
+
renderContent,
|
|
779
|
+
isStreaming: message.id === streamingMessageId,
|
|
780
|
+
error: messageErrors?.[message.id],
|
|
781
|
+
onRetry: onRetryMessage && messageErrors?.[message.id] ? () => onRetryMessage(message) : void 0
|
|
782
|
+
},
|
|
783
|
+
message.id
|
|
784
|
+
)),
|
|
785
|
+
isLoading && !isEmpty && /* @__PURE__ */ jsx10(
|
|
786
|
+
MemoizedMessage,
|
|
787
|
+
{
|
|
788
|
+
message: {
|
|
789
|
+
id: "__loading__",
|
|
790
|
+
role: "assistant",
|
|
791
|
+
content: loadingMessage
|
|
792
|
+
},
|
|
793
|
+
showAvatar: showAvatars,
|
|
794
|
+
avatarSrc: assistantAvatarSrc,
|
|
795
|
+
avatarFallback: assistantAvatarFallback,
|
|
796
|
+
className: "bg-muted/50",
|
|
797
|
+
renderContent: (content) => /* @__PURE__ */ jsx10("span", { className: "text-muted-foreground animate-pulse motion-reduce:animate-none", children: content })
|
|
798
|
+
}
|
|
799
|
+
),
|
|
800
|
+
/* @__PURE__ */ jsx10("div", { "aria-live": "polite", className: "sr-only", children: announcement }),
|
|
801
|
+
/* @__PURE__ */ jsx10("div", { ref: messagesEndRef })
|
|
802
|
+
] }),
|
|
803
|
+
showDownload && messages.length > 0 && /* @__PURE__ */ jsx10(
|
|
804
|
+
"button",
|
|
805
|
+
{
|
|
806
|
+
type: "button",
|
|
807
|
+
onClick: handleDownload,
|
|
808
|
+
"aria-label": "Download transcript",
|
|
809
|
+
className: "absolute top-2 right-2 z-10 rounded-md border border-border bg-background/90 px-2 py-1 text-xs text-muted-foreground hover:text-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
810
|
+
children: "\u2193 .md"
|
|
811
|
+
}
|
|
812
|
+
),
|
|
813
|
+
showScrollButton && isUnpinned && /* @__PURE__ */ jsx10(
|
|
814
|
+
"button",
|
|
815
|
+
{
|
|
816
|
+
type: "button",
|
|
817
|
+
onClick: scrollToLatest,
|
|
818
|
+
"aria-label": "Scroll to latest message",
|
|
819
|
+
className: "absolute bottom-3 left-1/2 z-10 -translate-x-1/2 rounded-full border border-border bg-background/95 px-3 py-1.5 text-xs font-medium shadow-sm hover:bg-accent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
820
|
+
children: "\u2193 Latest"
|
|
821
|
+
}
|
|
822
|
+
)
|
|
823
|
+
] });
|
|
824
|
+
}
|
|
825
|
+
);
|
|
826
|
+
Conversation.displayName = "Conversation";
|
|
827
|
+
|
|
828
|
+
// src/components/prompt-input/PromptInput.tsx
|
|
829
|
+
import * as React11 from "react";
|
|
830
|
+
import { Button as Button3 } from "@slipstream-ui/core";
|
|
831
|
+
import { cn as cn11 } from "@slipstream-ui/core";
|
|
832
|
+
import { Fragment as Fragment3, jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
833
|
+
function usePromptInputAttachments(options = {}) {
|
|
834
|
+
const { accept, maxFiles, maxFileSize, multiple = true, onError } = options;
|
|
835
|
+
const [files, setFiles] = React11.useState([]);
|
|
836
|
+
const inputRef = React11.useRef(null);
|
|
837
|
+
const add = React11.useCallback(
|
|
838
|
+
(fileList) => {
|
|
839
|
+
const incoming = Array.from(fileList);
|
|
840
|
+
setFiles((prev) => {
|
|
841
|
+
const next = [...prev];
|
|
842
|
+
for (const file of incoming) {
|
|
843
|
+
if (accept && !file.type.match(accept.replace(/\*/g, ".*"))) {
|
|
844
|
+
onError?.({ code: "accept", message: `"${file.name}" is not an accepted file type.` });
|
|
845
|
+
continue;
|
|
846
|
+
}
|
|
847
|
+
if (maxFileSize && file.size > maxFileSize) {
|
|
848
|
+
onError?.({
|
|
849
|
+
code: "max_file_size",
|
|
850
|
+
message: `"${file.name}" exceeds the ${Math.round(maxFileSize / 1024)} KB limit.`
|
|
851
|
+
});
|
|
852
|
+
continue;
|
|
853
|
+
}
|
|
854
|
+
if (maxFiles !== void 0 && next.length >= maxFiles) {
|
|
855
|
+
onError?.({ code: "max_files", message: `At most ${maxFiles} files can be attached.` });
|
|
856
|
+
break;
|
|
857
|
+
}
|
|
858
|
+
if (!multiple && next.length >= 1) {
|
|
859
|
+
onError?.({ code: "max_files", message: "Only one file can be attached." });
|
|
860
|
+
break;
|
|
861
|
+
}
|
|
862
|
+
next.push({
|
|
863
|
+
id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
864
|
+
name: file.name,
|
|
865
|
+
size: `${Math.max(1, Math.round(file.size / 1024))} KB`,
|
|
866
|
+
type: file.type.split("/")[1]?.toUpperCase() ?? file.name.split(".").pop()?.toUpperCase()
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
return next;
|
|
870
|
+
});
|
|
871
|
+
},
|
|
872
|
+
[accept, maxFileSize, maxFiles, multiple, onError]
|
|
873
|
+
);
|
|
874
|
+
const remove = React11.useCallback((id) => {
|
|
875
|
+
setFiles((prev) => prev.filter((file) => file.id !== id));
|
|
876
|
+
}, []);
|
|
877
|
+
const clear = React11.useCallback(() => setFiles([]), []);
|
|
878
|
+
const openFileDialog = React11.useCallback(() => inputRef.current?.click(), []);
|
|
879
|
+
const input = /* @__PURE__ */ jsx11(
|
|
880
|
+
"input",
|
|
881
|
+
{
|
|
882
|
+
ref: inputRef,
|
|
883
|
+
type: "file",
|
|
884
|
+
hidden: true,
|
|
885
|
+
accept,
|
|
886
|
+
multiple,
|
|
887
|
+
onChange: (event) => {
|
|
888
|
+
if (event.target.files) add(event.target.files);
|
|
889
|
+
event.target.value = "";
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
);
|
|
893
|
+
return { files, add, remove, clear, openFileDialog, input };
|
|
894
|
+
}
|
|
895
|
+
function useSpeechRecognition(onTranscript) {
|
|
896
|
+
const [isListening, setIsListening] = React11.useState(false);
|
|
897
|
+
const Ctor = typeof window !== "undefined" ? window.SpeechRecognition ?? window.webkitSpeechRecognition : void 0;
|
|
898
|
+
const recognitionRef = React11.useRef(null);
|
|
899
|
+
const toggle = React11.useCallback(() => {
|
|
900
|
+
if (!Ctor) return;
|
|
901
|
+
if (isListening) {
|
|
902
|
+
recognitionRef.current?.stop();
|
|
903
|
+
setIsListening(false);
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
const recognition = new Ctor();
|
|
907
|
+
recognition.lang = navigator.language || "en-US";
|
|
908
|
+
recognition.continuous = false;
|
|
909
|
+
recognition.interimResults = false;
|
|
910
|
+
recognition.onresult = (event) => {
|
|
911
|
+
const transcript = Array.from({ length: event.results.length }, (_, i) => event.results[i][0].transcript).join(" ");
|
|
912
|
+
onTranscript(transcript.trim());
|
|
913
|
+
};
|
|
914
|
+
recognition.onend = () => setIsListening(false);
|
|
915
|
+
recognitionRef.current = recognition;
|
|
916
|
+
recognition.start();
|
|
917
|
+
setIsListening(true);
|
|
918
|
+
}, [Ctor, isListening, onTranscript]);
|
|
919
|
+
return { supported: Boolean(Ctor), isListening, toggle };
|
|
920
|
+
}
|
|
921
|
+
var PromptInputContext = React11.createContext(null);
|
|
922
|
+
function usePromptInputContext() {
|
|
923
|
+
const context = React11.useContext(PromptInputContext);
|
|
924
|
+
if (!context) throw new Error("PromptInput primitives must be rendered inside <PromptInput>.");
|
|
925
|
+
return context;
|
|
926
|
+
}
|
|
927
|
+
var PromptInputRoot = React11.forwardRef(
|
|
928
|
+
({
|
|
929
|
+
onSubmit,
|
|
930
|
+
onStop,
|
|
931
|
+
attachments: controlledAttachments,
|
|
932
|
+
onRemoveAttachment,
|
|
933
|
+
onAttachmentsChange,
|
|
934
|
+
hasError = false,
|
|
935
|
+
accept,
|
|
936
|
+
multiple = true,
|
|
937
|
+
maxFiles,
|
|
938
|
+
maxFileSize,
|
|
939
|
+
onAttachmentError,
|
|
940
|
+
globalDrop = false,
|
|
941
|
+
placeholder = "Type your message...",
|
|
942
|
+
disabled = false,
|
|
943
|
+
isLoading = false,
|
|
944
|
+
className,
|
|
945
|
+
autoFocus = false,
|
|
946
|
+
maxLength,
|
|
947
|
+
showSendButton = true,
|
|
948
|
+
sendButtonText = "Send",
|
|
949
|
+
stopButtonText = "Stop generating",
|
|
950
|
+
children,
|
|
951
|
+
...props
|
|
952
|
+
}, ref) => {
|
|
953
|
+
const [value, setValue] = React11.useState("");
|
|
954
|
+
const textareaRef = React11.useRef(null);
|
|
955
|
+
const managed = usePromptInputAttachments({ accept, maxFiles, maxFileSize, multiple, onError: onAttachmentError });
|
|
956
|
+
const prevManagedCount = React11.useRef(0);
|
|
957
|
+
React11.useEffect(() => {
|
|
958
|
+
if (!controlledAttachments && managed.files.length !== prevManagedCount.current) {
|
|
959
|
+
prevManagedCount.current = managed.files.length;
|
|
960
|
+
onAttachmentsChange?.(managed.files);
|
|
961
|
+
}
|
|
962
|
+
}, [managed.files, controlledAttachments, onAttachmentsChange]);
|
|
963
|
+
const activeAttachments = controlledAttachments ?? managed.files;
|
|
964
|
+
React11.useEffect(() => {
|
|
965
|
+
if (autoFocus && textareaRef.current) textareaRef.current.focus();
|
|
966
|
+
}, [autoFocus]);
|
|
967
|
+
React11.useImperativeHandle(ref, () => textareaRef.current);
|
|
968
|
+
React11.useEffect(() => {
|
|
969
|
+
if (textareaRef.current) {
|
|
970
|
+
textareaRef.current.style.height = "auto";
|
|
971
|
+
textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`;
|
|
972
|
+
}
|
|
973
|
+
}, [value]);
|
|
974
|
+
React11.useEffect(() => {
|
|
975
|
+
if (!globalDrop || controlledAttachments) return;
|
|
976
|
+
const prevent = (event) => event.preventDefault();
|
|
977
|
+
const drop = (event) => {
|
|
978
|
+
event.preventDefault();
|
|
979
|
+
if (event.dataTransfer?.files.length) managed.add(event.dataTransfer.files);
|
|
980
|
+
};
|
|
981
|
+
document.addEventListener("dragover", prevent);
|
|
982
|
+
document.addEventListener("drop", drop);
|
|
983
|
+
return () => {
|
|
984
|
+
document.removeEventListener("dragover", prevent);
|
|
985
|
+
document.removeEventListener("drop", drop);
|
|
986
|
+
};
|
|
987
|
+
}, [globalDrop, controlledAttachments, managed]);
|
|
988
|
+
const canSubmit = Boolean(value.trim()) && !disabled && !isLoading;
|
|
989
|
+
const handleSubmit = () => {
|
|
990
|
+
const trimmedValue = value.trim();
|
|
991
|
+
if (!trimmedValue || disabled || isLoading) return;
|
|
992
|
+
onSubmit(trimmedValue);
|
|
993
|
+
setValue("");
|
|
994
|
+
if (textareaRef.current) textareaRef.current.style.height = "auto";
|
|
995
|
+
if (!controlledAttachments) managed.clear();
|
|
996
|
+
};
|
|
997
|
+
const handleStop = () => onStop?.();
|
|
998
|
+
const handleKeyDown = (event) => {
|
|
999
|
+
if (event.key === "Enter" && !event.shiftKey) {
|
|
1000
|
+
event.preventDefault();
|
|
1001
|
+
handleSubmit();
|
|
1002
|
+
}
|
|
1003
|
+
};
|
|
1004
|
+
const dictate = useSpeechRecognition(
|
|
1005
|
+
(transcript) => setValue((current) => current ? `${current} ${transcript}` : transcript)
|
|
1006
|
+
);
|
|
1007
|
+
const contextValue = {
|
|
1008
|
+
value,
|
|
1009
|
+
setValue,
|
|
1010
|
+
disabled,
|
|
1011
|
+
isLoading,
|
|
1012
|
+
canSubmit,
|
|
1013
|
+
submit: handleSubmit,
|
|
1014
|
+
stop: handleStop,
|
|
1015
|
+
hasStop: Boolean(onStop),
|
|
1016
|
+
attachments: activeAttachments,
|
|
1017
|
+
removeAttachment: (id) => controlledAttachments ? onRemoveAttachment?.({ id }) : managed.remove(id),
|
|
1018
|
+
addFiles: (files) => !controlledAttachments && managed.add(files),
|
|
1019
|
+
openFilePicker: managed.openFileDialog,
|
|
1020
|
+
hiddenInput: managed.input,
|
|
1021
|
+
dictate
|
|
1022
|
+
};
|
|
1023
|
+
const showStop = isLoading && Boolean(onStop);
|
|
1024
|
+
const submitIcon = showStop ? "\u25A0" : hasError ? "\u26A0" : "\u2192";
|
|
1025
|
+
const submitLabel = showStop ? stopButtonText : hasError ? "Failed \u2014 check message" : sendButtonText;
|
|
1026
|
+
const textarea = /* @__PURE__ */ jsx11(
|
|
1027
|
+
"textarea",
|
|
1028
|
+
{
|
|
1029
|
+
ref: textareaRef,
|
|
1030
|
+
value,
|
|
1031
|
+
onChange: (event) => setValue(event.target.value),
|
|
1032
|
+
onKeyDown: handleKeyDown,
|
|
1033
|
+
placeholder,
|
|
1034
|
+
disabled: disabled || isLoading,
|
|
1035
|
+
maxLength,
|
|
1036
|
+
rows: 1,
|
|
1037
|
+
className: cn11(
|
|
1038
|
+
"flex-1 resize-none rounded-md border border-input bg-background px-3 py-2 text-sm",
|
|
1039
|
+
"placeholder:text-muted-foreground",
|
|
1040
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
1041
|
+
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
1042
|
+
"min-h-[40px] max-h-[200px] overflow-y-auto"
|
|
1043
|
+
)
|
|
1044
|
+
}
|
|
1045
|
+
);
|
|
1046
|
+
const sendButton = showSendButton ? /* @__PURE__ */ jsx11(
|
|
1047
|
+
Button3,
|
|
1048
|
+
{
|
|
1049
|
+
onClick: showStop ? handleStop : handleSubmit,
|
|
1050
|
+
disabled: showStop ? false : !canSubmit || disabled,
|
|
1051
|
+
variant: showStop ? "outline" : "default",
|
|
1052
|
+
size: "icon",
|
|
1053
|
+
type: "button",
|
|
1054
|
+
"aria-label": submitLabel,
|
|
1055
|
+
children: /* @__PURE__ */ jsx11("span", { "aria-hidden": "true", children: submitIcon })
|
|
1056
|
+
}
|
|
1057
|
+
) : null;
|
|
1058
|
+
const dropHandlers = controlledAttachments ? {} : {
|
|
1059
|
+
onDrop: (event) => {
|
|
1060
|
+
event.preventDefault();
|
|
1061
|
+
if (event.dataTransfer.files.length) managed.add(event.dataTransfer.files);
|
|
1062
|
+
},
|
|
1063
|
+
onPaste: (event) => {
|
|
1064
|
+
const files = event.clipboardData?.files;
|
|
1065
|
+
if (files && files.length > 0) {
|
|
1066
|
+
event.preventDefault();
|
|
1067
|
+
managed.add(files);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
};
|
|
1071
|
+
return /* @__PURE__ */ jsx11(PromptInputContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsxs9("div", { className: cn11("flex flex-col gap-2", className), ...dropHandlers, ...props, children: [
|
|
1072
|
+
activeAttachments.length > 0 && /* @__PURE__ */ jsx11(
|
|
1073
|
+
AttachmentList,
|
|
1074
|
+
{
|
|
1075
|
+
attachments: activeAttachments,
|
|
1076
|
+
onRemove: controlledAttachments ? onRemoveAttachment : (a) => managed.remove(a.id)
|
|
1077
|
+
}
|
|
1078
|
+
),
|
|
1079
|
+
children ? /* @__PURE__ */ jsxs9(Fragment3, { children: [
|
|
1080
|
+
children,
|
|
1081
|
+
managed.input
|
|
1082
|
+
] }) : /* @__PURE__ */ jsxs9(Fragment3, { children: [
|
|
1083
|
+
/* @__PURE__ */ jsxs9("div", { className: "flex gap-2 items-end", children: [
|
|
1084
|
+
textarea,
|
|
1085
|
+
sendButton
|
|
1086
|
+
] }),
|
|
1087
|
+
managed.input
|
|
1088
|
+
] })
|
|
1089
|
+
] }) });
|
|
1090
|
+
}
|
|
1091
|
+
);
|
|
1092
|
+
PromptInputRoot.displayName = "PromptInput";
|
|
1093
|
+
var PromptInputTextarea = React11.forwardRef(({ className, onKeyDown, ...props }, ref) => {
|
|
1094
|
+
const { value, setValue, disabled, isLoading, submit } = usePromptInputContext();
|
|
1095
|
+
return /* @__PURE__ */ jsx11(
|
|
1096
|
+
"textarea",
|
|
1097
|
+
{
|
|
1098
|
+
ref,
|
|
1099
|
+
value,
|
|
1100
|
+
onChange: (event) => setValue(event.target.value),
|
|
1101
|
+
onKeyDown: (event) => {
|
|
1102
|
+
if (event.key === "Enter" && !event.shiftKey) {
|
|
1103
|
+
event.preventDefault();
|
|
1104
|
+
submit();
|
|
1105
|
+
}
|
|
1106
|
+
onKeyDown?.(event);
|
|
1107
|
+
},
|
|
1108
|
+
disabled: disabled || isLoading,
|
|
1109
|
+
rows: 1,
|
|
1110
|
+
className: cn11(
|
|
1111
|
+
"flex-1 resize-none rounded-md border border-input bg-background px-3 py-2 text-sm",
|
|
1112
|
+
"placeholder:text-muted-foreground",
|
|
1113
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
1114
|
+
"min-h-[40px]",
|
|
1115
|
+
className
|
|
1116
|
+
),
|
|
1117
|
+
...props
|
|
1118
|
+
}
|
|
1119
|
+
);
|
|
1120
|
+
});
|
|
1121
|
+
PromptInputTextarea.displayName = "PromptInputTextarea";
|
|
1122
|
+
var PromptInputSubmit = React11.forwardRef(({ className }, ref) => {
|
|
1123
|
+
const { canSubmit, isLoading, hasStop, submit, stop, disabled } = usePromptInputContext();
|
|
1124
|
+
return /* @__PURE__ */ jsx11(
|
|
1125
|
+
Button3,
|
|
1126
|
+
{
|
|
1127
|
+
ref,
|
|
1128
|
+
onClick: isLoading && hasStop ? stop : submit,
|
|
1129
|
+
disabled: isLoading && hasStop ? false : !canSubmit || disabled,
|
|
1130
|
+
variant: isLoading && hasStop ? "outline" : "default",
|
|
1131
|
+
size: "icon",
|
|
1132
|
+
type: "button",
|
|
1133
|
+
"aria-label": isLoading && hasStop ? "Stop generating" : "Send",
|
|
1134
|
+
className,
|
|
1135
|
+
children: /* @__PURE__ */ jsx11("span", { "aria-hidden": "true", children: isLoading && hasStop ? "\u25A0" : "\u2192" })
|
|
1136
|
+
}
|
|
1137
|
+
);
|
|
1138
|
+
});
|
|
1139
|
+
PromptInputSubmit.displayName = "PromptInputSubmit";
|
|
1140
|
+
function PromptInputFooter({ className, ...props }) {
|
|
1141
|
+
return /* @__PURE__ */ jsx11("div", { className: cn11("flex items-center justify-between gap-2 pt-1", className), ...props });
|
|
1142
|
+
}
|
|
1143
|
+
function PromptInputTools({ className, ...props }) {
|
|
1144
|
+
return /* @__PURE__ */ jsx11("div", { className: cn11("flex items-center gap-1", className), ...props });
|
|
1145
|
+
}
|
|
1146
|
+
function PromptInputHeader({ className, ...props }) {
|
|
1147
|
+
return /* @__PURE__ */ jsx11("div", { className: cn11("mb-1", className), ...props });
|
|
1148
|
+
}
|
|
1149
|
+
function PromptInputBody({ className, ...props }) {
|
|
1150
|
+
return /* @__PURE__ */ jsx11("div", { className: cn11("flex gap-2 items-end", className), ...props });
|
|
1151
|
+
}
|
|
1152
|
+
var PromptInputButton = React11.forwardRef(
|
|
1153
|
+
({ tooltip, active = false, className, ...props }, ref) => {
|
|
1154
|
+
const tip = typeof tooltip === "string" ? { content: tooltip } : tooltip;
|
|
1155
|
+
return /* @__PURE__ */ jsxs9("span", { className: "relative inline-flex group/tip", children: [
|
|
1156
|
+
/* @__PURE__ */ jsx11(
|
|
1157
|
+
"button",
|
|
1158
|
+
{
|
|
1159
|
+
ref,
|
|
1160
|
+
type: "button",
|
|
1161
|
+
"aria-pressed": active || void 0,
|
|
1162
|
+
className: cn11(
|
|
1163
|
+
"inline-flex h-7 items-center gap-1 rounded-md border border-transparent px-2 text-xs text-muted-foreground transition-colors",
|
|
1164
|
+
"hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
1165
|
+
active && "border-border bg-muted text-foreground",
|
|
1166
|
+
className
|
|
1167
|
+
),
|
|
1168
|
+
...props
|
|
1169
|
+
}
|
|
1170
|
+
),
|
|
1171
|
+
tip && /* @__PURE__ */ jsxs9(
|
|
1172
|
+
"span",
|
|
1173
|
+
{
|
|
1174
|
+
role: "tooltip",
|
|
1175
|
+
className: "pointer-events-none absolute bottom-full left-0 mb-1 z-10 flex items-center gap-1 whitespace-nowrap rounded border border-border bg-popover px-2 py-0.5 text-[11px] text-popover-foreground opacity-0 shadow-sm transition-opacity group-hover/tip:opacity-100",
|
|
1176
|
+
children: [
|
|
1177
|
+
tip.content,
|
|
1178
|
+
tip.shortcut && /* @__PURE__ */ jsx11("kbd", { className: "rounded bg-muted px-1 font-mono", children: tip.shortcut })
|
|
1179
|
+
]
|
|
1180
|
+
}
|
|
1181
|
+
)
|
|
1182
|
+
] });
|
|
1183
|
+
}
|
|
1184
|
+
);
|
|
1185
|
+
PromptInputButton.displayName = "PromptInputButton";
|
|
1186
|
+
function PromptInputActionDictate({ labels }) {
|
|
1187
|
+
const { dictate } = usePromptInputContext();
|
|
1188
|
+
if (!dictate.supported) return null;
|
|
1189
|
+
return /* @__PURE__ */ jsx11(
|
|
1190
|
+
PromptInputButton,
|
|
1191
|
+
{
|
|
1192
|
+
onClick: dictate.toggle,
|
|
1193
|
+
active: dictate.isListening,
|
|
1194
|
+
tooltip: { content: dictate.isListening ? labels?.stop ?? "Stop dictating" : labels?.start ?? "Dictate" },
|
|
1195
|
+
"aria-label": dictate.isListening ? labels?.stop ?? "Stop dictation" : labels?.start ?? "Start dictation",
|
|
1196
|
+
children: /* @__PURE__ */ jsx11("span", { "aria-hidden": "true", children: "\u{1F399}" })
|
|
1197
|
+
}
|
|
1198
|
+
);
|
|
1199
|
+
}
|
|
1200
|
+
function PromptInputActionAddAttachments({ label = "Add photos or files" }) {
|
|
1201
|
+
const { openFilePicker } = usePromptInputContext();
|
|
1202
|
+
return /* @__PURE__ */ jsxs9(PromptInputButton, { onClick: openFilePicker, children: [
|
|
1203
|
+
/* @__PURE__ */ jsx11("span", { "aria-hidden": "true", children: "\uFF0B" }),
|
|
1204
|
+
/* @__PURE__ */ jsx11("span", { children: label })
|
|
1205
|
+
] });
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
// src/components/reasoning/Reasoning.tsx
|
|
1209
|
+
import * as React12 from "react";
|
|
1210
|
+
import { cn as cn12 } from "@slipstream-ui/core";
|
|
1211
|
+
import { jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
1212
|
+
var Reasoning = React12.forwardRef(
|
|
1213
|
+
({ steps, className, collapsible = true, defaultExpanded = false, duration, isStreaming = false, ...props }, ref) => {
|
|
1214
|
+
const [isExpanded, setIsExpanded] = React12.useState(defaultExpanded);
|
|
1215
|
+
const contentId = React12.useId();
|
|
1216
|
+
if (steps.length === 0) {
|
|
1217
|
+
return null;
|
|
1218
|
+
}
|
|
1219
|
+
const headerText = [
|
|
1220
|
+
"Reasoning",
|
|
1221
|
+
`(${steps.length} ${steps.length === 1 ? "step" : "steps"})`,
|
|
1222
|
+
typeof duration === "number" ? `\xB7 ${duration}s` : duration ? `\xB7 ${duration}` : "",
|
|
1223
|
+
isStreaming ? "\xB7 thinking\u2026" : ""
|
|
1224
|
+
].filter(Boolean).join(" ");
|
|
1225
|
+
return /* @__PURE__ */ jsxs10("div", { ref, className: cn12("border-l-2 border-muted-foreground/30 pl-4", className), ...props, children: [
|
|
1226
|
+
collapsible ? /* @__PURE__ */ jsxs10(
|
|
1227
|
+
"button",
|
|
1228
|
+
{
|
|
1229
|
+
onClick: () => setIsExpanded(!isExpanded),
|
|
1230
|
+
className: "flex items-center gap-2 text-sm font-medium text-muted-foreground hover:text-foreground transition-colors",
|
|
1231
|
+
"aria-expanded": isExpanded,
|
|
1232
|
+
"aria-controls": contentId,
|
|
1233
|
+
children: [
|
|
1234
|
+
/* @__PURE__ */ jsx12("span", { className: cn12("transition-transform", isExpanded ? "rotate-90" : ""), children: "\u25B6" }),
|
|
1235
|
+
/* @__PURE__ */ jsx12("span", { children: headerText }),
|
|
1236
|
+
isStreaming && /* @__PURE__ */ jsx12("span", { className: "flex gap-0.5", "aria-hidden": "true", children: [0, 150, 300].map((delay) => /* @__PURE__ */ jsx12(
|
|
1237
|
+
"span",
|
|
1238
|
+
{
|
|
1239
|
+
className: "h-1 w-1 rounded-full bg-muted-foreground animate-pulse motion-reduce:animate-none",
|
|
1240
|
+
style: { animationDelay: `${delay}ms` }
|
|
1241
|
+
},
|
|
1242
|
+
delay
|
|
1243
|
+
)) })
|
|
1244
|
+
]
|
|
1245
|
+
}
|
|
1246
|
+
) : null,
|
|
1247
|
+
(isExpanded || !collapsible) && /* @__PURE__ */ jsx12("div", { id: contentId, className: "mt-3 space-y-3", children: steps.map((step, index) => /* @__PURE__ */ jsxs10("div", { className: "flex gap-3", children: [
|
|
1248
|
+
/* @__PURE__ */ jsx12("div", { className: "w-3 h-3 rounded-full bg-primary/20 border-2 border-primary flex-shrink-0 mt-1" }),
|
|
1249
|
+
/* @__PURE__ */ jsxs10("div", { className: "text-sm flex-1", children: [
|
|
1250
|
+
/* @__PURE__ */ jsxs10("div", { className: "font-medium text-foreground mb-1", children: [
|
|
1251
|
+
"Step ",
|
|
1252
|
+
index + 1
|
|
1253
|
+
] }),
|
|
1254
|
+
/* @__PURE__ */ jsx12("div", { className: "text-muted-foreground", children: step.content }),
|
|
1255
|
+
step.timestamp && /* @__PURE__ */ jsx12("div", { className: "text-xs text-muted-foreground mt-1", children: new Date(step.timestamp).toLocaleTimeString() })
|
|
1256
|
+
] })
|
|
1257
|
+
] }, step.id)) })
|
|
1258
|
+
] });
|
|
1259
|
+
}
|
|
1260
|
+
);
|
|
1261
|
+
Reasoning.displayName = "Reasoning";
|
|
1262
|
+
|
|
1263
|
+
// src/components/tool-call/ToolCall.tsx
|
|
1264
|
+
import * as React13 from "react";
|
|
1265
|
+
import { cn as cn13 } from "@slipstream-ui/core";
|
|
1266
|
+
import { jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
1267
|
+
var statusChipClasses2 = {
|
|
1268
|
+
pending: "bg-warning/10 text-warning border-warning/20",
|
|
1269
|
+
running: "bg-info/10 text-info border-info/20",
|
|
1270
|
+
completed: "bg-success/10 text-success border-success/20",
|
|
1271
|
+
failed: "bg-destructive/10 text-destructive border-destructive/20"
|
|
1272
|
+
};
|
|
1273
|
+
var statusLabels = {
|
|
1274
|
+
pending: "Pending",
|
|
1275
|
+
running: "Running",
|
|
1276
|
+
completed: "Completed",
|
|
1277
|
+
failed: "Failed"
|
|
1278
|
+
};
|
|
1279
|
+
var ToolCall = React13.forwardRef(
|
|
1280
|
+
({ toolCall, className, showResult = true, ...props }, ref) => {
|
|
1281
|
+
const serializedArguments = React13.useMemo(
|
|
1282
|
+
() => JSON.stringify(toolCall.arguments, null, 2),
|
|
1283
|
+
[toolCall.arguments]
|
|
1284
|
+
);
|
|
1285
|
+
const serializedResult = React13.useMemo(() => {
|
|
1286
|
+
if (toolCall.result === void 0) return void 0;
|
|
1287
|
+
return typeof toolCall.result === "string" ? toolCall.result : JSON.stringify(toolCall.result, null, 2);
|
|
1288
|
+
}, [toolCall.result]);
|
|
1289
|
+
return /* @__PURE__ */ jsxs11(
|
|
1290
|
+
"div",
|
|
1291
|
+
{
|
|
1292
|
+
ref,
|
|
1293
|
+
className: cn13(
|
|
1294
|
+
"border rounded-lg p-4 bg-muted/30",
|
|
1295
|
+
className
|
|
1296
|
+
),
|
|
1297
|
+
...props,
|
|
1298
|
+
children: [
|
|
1299
|
+
/* @__PURE__ */ jsxs11("div", { className: "flex items-center justify-between mb-3", children: [
|
|
1300
|
+
/* @__PURE__ */ jsxs11("div", { className: "flex items-center gap-2", children: [
|
|
1301
|
+
/* @__PURE__ */ jsx13("span", { className: "font-medium text-sm", children: toolCall.name }),
|
|
1302
|
+
/* @__PURE__ */ jsx13(
|
|
1303
|
+
"span",
|
|
1304
|
+
{
|
|
1305
|
+
className: cn13(
|
|
1306
|
+
"text-xs px-2 py-0.5 rounded-full border",
|
|
1307
|
+
statusChipClasses2[toolCall.status]
|
|
1308
|
+
),
|
|
1309
|
+
children: statusLabels[toolCall.status]
|
|
1310
|
+
}
|
|
1311
|
+
)
|
|
1312
|
+
] }),
|
|
1313
|
+
/* @__PURE__ */ jsx13("span", { className: "text-xs text-muted-foreground", children: toolCall.id })
|
|
1314
|
+
] }),
|
|
1315
|
+
/* @__PURE__ */ jsxs11("div", { className: "space-y-2", children: [
|
|
1316
|
+
/* @__PURE__ */ jsxs11("div", { children: [
|
|
1317
|
+
/* @__PURE__ */ jsx13("div", { className: "text-xs font-medium text-muted-foreground mb-1", children: "Arguments" }),
|
|
1318
|
+
/* @__PURE__ */ jsx13("pre", { className: "text-xs bg-muted p-2 rounded overflow-x-auto", children: /* @__PURE__ */ jsx13("code", { children: serializedArguments }) })
|
|
1319
|
+
] }),
|
|
1320
|
+
showResult && serializedResult !== void 0 && /* @__PURE__ */ jsxs11("div", { children: [
|
|
1321
|
+
/* @__PURE__ */ jsx13("div", { className: "text-xs font-medium text-muted-foreground mb-1", children: "Result" }),
|
|
1322
|
+
/* @__PURE__ */ jsx13("pre", { className: "text-xs bg-muted p-2 rounded overflow-x-auto", children: /* @__PURE__ */ jsx13("code", { children: serializedResult }) })
|
|
1323
|
+
] }),
|
|
1324
|
+
toolCall.status === "failed" && toolCall.error && /* @__PURE__ */ jsxs11("div", { role: "alert", children: [
|
|
1325
|
+
/* @__PURE__ */ jsx13("div", { className: "text-xs font-medium text-destructive mb-1", children: "Error" }),
|
|
1326
|
+
/* @__PURE__ */ jsx13("pre", { className: "text-xs bg-destructive/10 text-destructive p-2 rounded overflow-x-auto whitespace-pre-wrap", children: /* @__PURE__ */ jsx13("code", { children: toolCall.error }) })
|
|
1327
|
+
] })
|
|
1328
|
+
] })
|
|
1329
|
+
]
|
|
1330
|
+
}
|
|
1331
|
+
);
|
|
1332
|
+
}
|
|
1333
|
+
);
|
|
1334
|
+
ToolCall.displayName = "ToolCall";
|
|
1335
|
+
|
|
1336
|
+
// src/components/source/Source.tsx
|
|
1337
|
+
import * as React14 from "react";
|
|
1338
|
+
import { cn as cn14 } from "@slipstream-ui/core";
|
|
1339
|
+
import { jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
1340
|
+
var Source = React14.forwardRef(
|
|
1341
|
+
({ source, index, className, ...props }, ref) => {
|
|
1342
|
+
return /* @__PURE__ */ jsxs12("div", { ref, className: cn14("flex gap-2 items-start text-sm", className), ...props, children: [
|
|
1343
|
+
index !== void 0 && /* @__PURE__ */ jsxs12("span", { className: "text-muted-foreground select-none", "aria-hidden": "true", children: [
|
|
1344
|
+
"[",
|
|
1345
|
+
index,
|
|
1346
|
+
"]"
|
|
1347
|
+
] }),
|
|
1348
|
+
/* @__PURE__ */ jsxs12("div", { className: "flex-1 min-w-0", children: [
|
|
1349
|
+
/* @__PURE__ */ jsx14("div", { className: "font-medium text-foreground", children: source.url ? /* @__PURE__ */ jsx14(
|
|
1350
|
+
"a",
|
|
1351
|
+
{
|
|
1352
|
+
href: source.url,
|
|
1353
|
+
target: "_blank",
|
|
1354
|
+
rel: "noopener noreferrer",
|
|
1355
|
+
className: "hover:text-primary transition-colors",
|
|
1356
|
+
children: source.title
|
|
1357
|
+
}
|
|
1358
|
+
) : /* @__PURE__ */ jsx14("span", { children: source.title }) }),
|
|
1359
|
+
source.snippet && /* @__PURE__ */ jsx14("div", { className: "text-xs text-muted-foreground mt-1 line-clamp-2", children: source.snippet })
|
|
1360
|
+
] })
|
|
1361
|
+
] });
|
|
1362
|
+
}
|
|
1363
|
+
);
|
|
1364
|
+
Source.displayName = "Source";
|
|
1365
|
+
|
|
1366
|
+
// src/components/source/Sources.tsx
|
|
1367
|
+
import * as React15 from "react";
|
|
1368
|
+
import { cn as cn15 } from "@slipstream-ui/core";
|
|
1369
|
+
import { jsx as jsx15, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
1370
|
+
var Sources = React15.forwardRef(
|
|
1371
|
+
({ sources, className, maxDisplay = 3, label = "Sources", ...props }, ref) => {
|
|
1372
|
+
const displaySources = maxDisplay ? sources.slice(0, maxDisplay) : sources;
|
|
1373
|
+
const hasMore = maxDisplay && sources.length > maxDisplay;
|
|
1374
|
+
if (sources.length === 0) {
|
|
1375
|
+
return null;
|
|
1376
|
+
}
|
|
1377
|
+
return /* @__PURE__ */ jsxs13("div", { ref, className: cn15("mt-4 pt-4 border-t border-border", className), ...props, children: [
|
|
1378
|
+
/* @__PURE__ */ jsx15("div", { className: "text-xs font-medium text-muted-foreground mb-2", children: label }),
|
|
1379
|
+
/* @__PURE__ */ jsxs13("div", { className: "space-y-2", children: [
|
|
1380
|
+
displaySources.map((source, index) => /* @__PURE__ */ jsx15(Source, { source, index: index + 1 }, source.id)),
|
|
1381
|
+
hasMore && /* @__PURE__ */ jsxs13("div", { className: "text-xs text-muted-foreground", children: [
|
|
1382
|
+
"+",
|
|
1383
|
+
sources.length - (maxDisplay ?? 0),
|
|
1384
|
+
" more sources"
|
|
1385
|
+
] })
|
|
1386
|
+
] })
|
|
1387
|
+
] });
|
|
1388
|
+
}
|
|
1389
|
+
);
|
|
1390
|
+
Sources.displayName = "Sources";
|
|
1391
|
+
|
|
1392
|
+
// src/components/suggestion/Suggestion.tsx
|
|
1393
|
+
import * as React16 from "react";
|
|
1394
|
+
import { cn as cn16 } from "@slipstream-ui/core";
|
|
1395
|
+
import { jsx as jsx16 } from "react/jsx-runtime";
|
|
1396
|
+
var Suggestions = React16.forwardRef(
|
|
1397
|
+
({ suggestions, onSelect, className, maxDisplay = 4, ...props }, ref) => {
|
|
1398
|
+
const displaySuggestions = maxDisplay ? suggestions.slice(0, maxDisplay) : suggestions;
|
|
1399
|
+
if (suggestions.length === 0) {
|
|
1400
|
+
return null;
|
|
1401
|
+
}
|
|
1402
|
+
return /* @__PURE__ */ jsx16("div", { ref, className: cn16("mt-4 space-y-2", className), ...props, children: displaySuggestions.map((suggestion) => /* @__PURE__ */ jsx16(
|
|
1403
|
+
"button",
|
|
1404
|
+
{
|
|
1405
|
+
className: cn16(
|
|
1406
|
+
"w-full text-left h-auto py-2 px-3 text-sm rounded-md",
|
|
1407
|
+
"hover:bg-accent hover:text-accent-foreground",
|
|
1408
|
+
"transition-colors",
|
|
1409
|
+
"text-muted-foreground",
|
|
1410
|
+
"focus:outline-none focus:ring-2 focus:ring-ring"
|
|
1411
|
+
),
|
|
1412
|
+
onClick: () => onSelect?.(suggestion),
|
|
1413
|
+
children: suggestion.text
|
|
1414
|
+
},
|
|
1415
|
+
suggestion.id
|
|
1416
|
+
)) });
|
|
1417
|
+
}
|
|
1418
|
+
);
|
|
1419
|
+
Suggestions.displayName = "Suggestions";
|
|
1420
|
+
|
|
1421
|
+
// src/components/chainthought/ChainThought.tsx
|
|
1422
|
+
import * as React17 from "react";
|
|
1423
|
+
import { cn as cn17 } from "@slipstream-ui/core";
|
|
1424
|
+
import { jsx as jsx17, jsxs as jsxs14 } from "react/jsx-runtime";
|
|
1425
|
+
var ChainThought = React17.forwardRef(
|
|
1426
|
+
({ steps, className, compact = false, ...props }, ref) => {
|
|
1427
|
+
if (steps.length === 0) {
|
|
1428
|
+
return null;
|
|
1429
|
+
}
|
|
1430
|
+
return /* @__PURE__ */ jsx17("div", { ref, className: cn17("space-y-2", className), ...props, children: steps.map((step, index) => /* @__PURE__ */ jsxs14("div", { className: "flex gap-3 items-start", children: [
|
|
1431
|
+
/* @__PURE__ */ jsx17("div", { className: cn17(
|
|
1432
|
+
"w-2 h-2 rounded-full mt-1.5 flex-shrink-0",
|
|
1433
|
+
step.status === "completed" ? "bg-success" : step.status === "failed" ? "bg-destructive" : step.status === "pending" ? "bg-warning" : "bg-primary"
|
|
1434
|
+
) }),
|
|
1435
|
+
/* @__PURE__ */ jsxs14("div", { className: cn17("flex-1", compact && "text-sm"), children: [
|
|
1436
|
+
/* @__PURE__ */ jsxs14("div", { className: "text-sm text-muted-foreground", children: [
|
|
1437
|
+
"Step ",
|
|
1438
|
+
index + 1
|
|
1439
|
+
] }),
|
|
1440
|
+
/* @__PURE__ */ jsx17("div", { className: "text-foreground", children: step.content })
|
|
1441
|
+
] })
|
|
1442
|
+
] }, step.id)) });
|
|
1443
|
+
}
|
|
1444
|
+
);
|
|
1445
|
+
ChainThought.displayName = "ChainThought";
|
|
1446
|
+
|
|
1447
|
+
// src/components/streaming/StreamingIndicator.tsx
|
|
1448
|
+
import * as React18 from "react";
|
|
1449
|
+
import { cn as cn18 } from "@slipstream-ui/core";
|
|
1450
|
+
import { jsx as jsx18, jsxs as jsxs15 } from "react/jsx-runtime";
|
|
1451
|
+
var StreamingIndicator = React18.forwardRef(
|
|
1452
|
+
({ isStreaming, className, text = "Generating...", ...props }, ref) => {
|
|
1453
|
+
if (!isStreaming) {
|
|
1454
|
+
return null;
|
|
1455
|
+
}
|
|
1456
|
+
return /* @__PURE__ */ jsxs15(
|
|
1457
|
+
"div",
|
|
1458
|
+
{
|
|
1459
|
+
ref,
|
|
1460
|
+
role: "status",
|
|
1461
|
+
"aria-live": "polite",
|
|
1462
|
+
"aria-label": text,
|
|
1463
|
+
className: cn18("flex items-center gap-2 text-sm text-muted-foreground", className),
|
|
1464
|
+
...props,
|
|
1465
|
+
children: [
|
|
1466
|
+
/* @__PURE__ */ jsx18("div", { className: "flex gap-1", "aria-hidden": "true", children: [0, 150, 300].map((delay) => /* @__PURE__ */ jsx18(
|
|
1467
|
+
"span",
|
|
1468
|
+
{
|
|
1469
|
+
className: "w-2 h-2 bg-primary rounded-full animate-pulse motion-reduce:animate-none",
|
|
1470
|
+
style: { animationDelay: `${delay}ms` }
|
|
1471
|
+
},
|
|
1472
|
+
delay
|
|
1473
|
+
)) }),
|
|
1474
|
+
/* @__PURE__ */ jsx18("span", { children: text })
|
|
1475
|
+
]
|
|
1476
|
+
}
|
|
1477
|
+
);
|
|
1478
|
+
}
|
|
1479
|
+
);
|
|
1480
|
+
StreamingIndicator.displayName = "StreamingIndicator";
|
|
1481
|
+
|
|
1482
|
+
// src/components/command-palette/CommandPalette.tsx
|
|
1483
|
+
import * as React19 from "react";
|
|
1484
|
+
import { cn as cn19 } from "@slipstream-ui/core";
|
|
1485
|
+
import { jsx as jsx19, jsxs as jsxs16 } from "react/jsx-runtime";
|
|
1486
|
+
function CommandPalette({
|
|
1487
|
+
open,
|
|
1488
|
+
onOpenChange,
|
|
1489
|
+
commands,
|
|
1490
|
+
onSelect,
|
|
1491
|
+
placeholder = "Type a command\u2026"
|
|
1492
|
+
}) {
|
|
1493
|
+
const [query, setQuery] = React19.useState("");
|
|
1494
|
+
const [activeIndex, setActiveIndex] = React19.useState(0);
|
|
1495
|
+
const inputRef = React19.useRef(null);
|
|
1496
|
+
const listboxId = React19.useId();
|
|
1497
|
+
const optionId = (index) => `${listboxId}-option-${index}`;
|
|
1498
|
+
const filtered = React19.useMemo(() => {
|
|
1499
|
+
const terms = query.trim().toLowerCase().split(/\s+/).filter(Boolean);
|
|
1500
|
+
if (terms.length === 0) return commands;
|
|
1501
|
+
return commands.filter((command) => {
|
|
1502
|
+
const haystack = `${command.label} ${command.section ?? ""} ${(command.keywords ?? []).join(" ")}`.toLowerCase();
|
|
1503
|
+
return terms.every((term) => haystack.includes(term));
|
|
1504
|
+
});
|
|
1505
|
+
}, [commands, query]);
|
|
1506
|
+
React19.useEffect(() => {
|
|
1507
|
+
if (open) {
|
|
1508
|
+
setQuery("");
|
|
1509
|
+
setActiveIndex(0);
|
|
1510
|
+
const timer = setTimeout(() => inputRef.current?.focus(), 0);
|
|
1511
|
+
return () => clearTimeout(timer);
|
|
1512
|
+
}
|
|
1513
|
+
return void 0;
|
|
1514
|
+
}, [open]);
|
|
1515
|
+
React19.useEffect(() => {
|
|
1516
|
+
setActiveIndex((index) => Math.min(index, Math.max(0, filtered.length - 1)));
|
|
1517
|
+
}, [filtered.length]);
|
|
1518
|
+
if (!open) return null;
|
|
1519
|
+
const onKeyDown = (event) => {
|
|
1520
|
+
if (event.key === "Escape") {
|
|
1521
|
+
event.preventDefault();
|
|
1522
|
+
onOpenChange(false);
|
|
1523
|
+
} else if (event.key === "ArrowDown") {
|
|
1524
|
+
event.preventDefault();
|
|
1525
|
+
setActiveIndex((index) => filtered.length === 0 ? 0 : (index + 1) % filtered.length);
|
|
1526
|
+
} else if (event.key === "ArrowUp") {
|
|
1527
|
+
event.preventDefault();
|
|
1528
|
+
setActiveIndex((index) => filtered.length === 0 ? 0 : (index - 1 + filtered.length) % filtered.length);
|
|
1529
|
+
} else if (event.key === "Enter") {
|
|
1530
|
+
event.preventDefault();
|
|
1531
|
+
const command = filtered[activeIndex];
|
|
1532
|
+
if (command) {
|
|
1533
|
+
onSelect(command);
|
|
1534
|
+
onOpenChange(false);
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
};
|
|
1538
|
+
let lastSection;
|
|
1539
|
+
return /* @__PURE__ */ jsx19(
|
|
1540
|
+
"div",
|
|
1541
|
+
{
|
|
1542
|
+
className: "fixed inset-0 z-50 flex items-start justify-center bg-background/80 p-4 pt-[15vh]",
|
|
1543
|
+
onMouseDown: (event) => {
|
|
1544
|
+
if (event.target === event.currentTarget) onOpenChange(false);
|
|
1545
|
+
},
|
|
1546
|
+
children: /* @__PURE__ */ jsxs16("div", { className: "w-full max-w-lg overflow-hidden rounded-xl border border-border bg-card shadow-2xl", children: [
|
|
1547
|
+
/* @__PURE__ */ jsx19(
|
|
1548
|
+
"input",
|
|
1549
|
+
{
|
|
1550
|
+
ref: inputRef,
|
|
1551
|
+
type: "text",
|
|
1552
|
+
role: "combobox",
|
|
1553
|
+
"aria-expanded": "true",
|
|
1554
|
+
"aria-controls": listboxId,
|
|
1555
|
+
"aria-activedescendant": filtered.length > 0 ? optionId(activeIndex) : void 0,
|
|
1556
|
+
"aria-autocomplete": "list",
|
|
1557
|
+
"aria-label": "Command palette",
|
|
1558
|
+
placeholder,
|
|
1559
|
+
value: query,
|
|
1560
|
+
onChange: (event) => {
|
|
1561
|
+
setQuery(event.target.value);
|
|
1562
|
+
setActiveIndex(0);
|
|
1563
|
+
},
|
|
1564
|
+
onKeyDown,
|
|
1565
|
+
className: "w-full border-b border-border bg-transparent px-4 py-3 text-sm outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring placeholder:text-muted-foreground"
|
|
1566
|
+
}
|
|
1567
|
+
),
|
|
1568
|
+
/* @__PURE__ */ jsxs16("ul", { id: listboxId, role: "listbox", "aria-label": "Commands", className: "max-h-72 overflow-y-auto py-1", children: [
|
|
1569
|
+
filtered.length === 0 && /* @__PURE__ */ jsx19("li", { role: "presentation", className: "px-4 py-3 text-sm text-muted-foreground", children: "No matching commands." }),
|
|
1570
|
+
filtered.map((command, index) => {
|
|
1571
|
+
const showSection = command.section && command.section !== lastSection;
|
|
1572
|
+
lastSection = command.section ?? lastSection;
|
|
1573
|
+
return /* @__PURE__ */ jsxs16(React19.Fragment, { children: [
|
|
1574
|
+
showSection && /* @__PURE__ */ jsx19("li", { role: "presentation", className: "px-4 pb-1 pt-3 text-xs font-medium uppercase text-muted-foreground", children: command.section }),
|
|
1575
|
+
/* @__PURE__ */ jsxs16(
|
|
1576
|
+
"li",
|
|
1577
|
+
{
|
|
1578
|
+
id: optionId(index),
|
|
1579
|
+
role: "option",
|
|
1580
|
+
"aria-selected": index === activeIndex,
|
|
1581
|
+
onMouseEnter: () => setActiveIndex(index),
|
|
1582
|
+
onClick: () => {
|
|
1583
|
+
onSelect(command);
|
|
1584
|
+
onOpenChange(false);
|
|
1585
|
+
},
|
|
1586
|
+
className: cn19(
|
|
1587
|
+
"flex cursor-pointer items-center justify-between px-4 py-2 text-sm",
|
|
1588
|
+
index === activeIndex ? "bg-primary/10 text-foreground" : "text-muted-foreground"
|
|
1589
|
+
),
|
|
1590
|
+
children: [
|
|
1591
|
+
/* @__PURE__ */ jsx19("span", { children: command.label }),
|
|
1592
|
+
command.shortcut && /* @__PURE__ */ jsx19("span", { className: "font-mono text-xs", children: command.shortcut })
|
|
1593
|
+
]
|
|
1594
|
+
}
|
|
1595
|
+
)
|
|
1596
|
+
] }, command.id);
|
|
1597
|
+
})
|
|
1598
|
+
] })
|
|
1599
|
+
] })
|
|
1600
|
+
}
|
|
1601
|
+
);
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
// src/adapter/messages.ts
|
|
1605
|
+
function toSlipstreamToolCall(invocation) {
|
|
1606
|
+
const status = invocation.state === "result" ? "completed" : invocation.state === "error" ? "failed" : invocation.state === "call" ? "pending" : "pending";
|
|
1607
|
+
return {
|
|
1608
|
+
id: invocation.toolCallId,
|
|
1609
|
+
name: invocation.toolName,
|
|
1610
|
+
arguments: invocation.args ?? {},
|
|
1611
|
+
status,
|
|
1612
|
+
...status === "completed" && invocation.result !== void 0 ? { result: invocation.result } : {},
|
|
1613
|
+
...status === "failed" ? { error: invocation.error instanceof Error ? invocation.error.message : String(invocation.error ?? "Tool call failed") } : {}
|
|
1614
|
+
};
|
|
1615
|
+
}
|
|
1616
|
+
function toSlipstreamToolCalls(message) {
|
|
1617
|
+
return (message.toolInvocations ?? []).map(toSlipstreamToolCall);
|
|
1618
|
+
}
|
|
1619
|
+
function toSlipstreamMessage(message) {
|
|
1620
|
+
return {
|
|
1621
|
+
id: message.id,
|
|
1622
|
+
role: message.role,
|
|
1623
|
+
content: message.content,
|
|
1624
|
+
...message.toolInvocations?.length ? { metadata: { toolCalls: toSlipstreamToolCalls(message) } } : {}
|
|
1625
|
+
};
|
|
1626
|
+
}
|
|
1627
|
+
function toSlipstreamMessages(messages) {
|
|
1628
|
+
return messages.map(toSlipstreamMessage);
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
// src/adapter/hooks.ts
|
|
1632
|
+
import { useMemo as useMemo4 } from "react";
|
|
1633
|
+
function useSlipstreamMessages({ messages }) {
|
|
1634
|
+
const mapped = useMemo4(() => toSlipstreamMessages(messages), [messages]);
|
|
1635
|
+
return { messages: mapped };
|
|
1636
|
+
}
|
|
1637
|
+
function useSlipstreamStream({
|
|
1638
|
+
status,
|
|
1639
|
+
error,
|
|
1640
|
+
onRetry,
|
|
1641
|
+
messages = []
|
|
1642
|
+
}) {
|
|
1643
|
+
const isLoading = status === "submitted" || status === "streaming";
|
|
1644
|
+
const streamingMessageId = useMemo4(() => {
|
|
1645
|
+
if (status !== "streaming") return void 0;
|
|
1646
|
+
for (let index = messages.length - 1; index >= 0; index--) {
|
|
1647
|
+
const message = messages[index];
|
|
1648
|
+
if (message?.role === "assistant") return message.id;
|
|
1649
|
+
}
|
|
1650
|
+
return messages.at(-1)?.id;
|
|
1651
|
+
}, [status, messages]);
|
|
1652
|
+
const messageErrors = useMemo4(() => {
|
|
1653
|
+
if (status !== "error" || !error) return void 0;
|
|
1654
|
+
const last = messages.at(-1);
|
|
1655
|
+
if (!last) return void 0;
|
|
1656
|
+
return {
|
|
1657
|
+
[last.id]: error instanceof Error ? error.message : String(error)
|
|
1658
|
+
};
|
|
1659
|
+
}, [status, error, messages]);
|
|
1660
|
+
return {
|
|
1661
|
+
isLoading,
|
|
1662
|
+
...streamingMessageId ? { streamingMessageId } : {},
|
|
1663
|
+
...messageErrors ? { messageErrors } : {},
|
|
1664
|
+
...onRetry ? { onRetryMessage: onRetry } : {}
|
|
1665
|
+
};
|
|
1666
|
+
}
|
|
1667
|
+
export {
|
|
1668
|
+
Attachment,
|
|
1669
|
+
AttachmentList,
|
|
1670
|
+
Branch,
|
|
1671
|
+
ChainThought,
|
|
1672
|
+
CodeBlock,
|
|
1673
|
+
CommandPalette,
|
|
1674
|
+
Confirmation,
|
|
1675
|
+
Conversation,
|
|
1676
|
+
InlineCitation,
|
|
1677
|
+
MemoizedMessage as Message,
|
|
1678
|
+
MessageActions,
|
|
1679
|
+
PromptInputRoot as PromptInput,
|
|
1680
|
+
PromptInputActionAddAttachments,
|
|
1681
|
+
PromptInputActionDictate,
|
|
1682
|
+
PromptInputBody,
|
|
1683
|
+
PromptInputButton,
|
|
1684
|
+
PromptInputFooter,
|
|
1685
|
+
PromptInputHeader,
|
|
1686
|
+
PromptInputSubmit,
|
|
1687
|
+
PromptInputTextarea,
|
|
1688
|
+
PromptInputTools,
|
|
1689
|
+
Reasoning,
|
|
1690
|
+
Response,
|
|
1691
|
+
Source,
|
|
1692
|
+
Sources,
|
|
1693
|
+
StreamingIndicator,
|
|
1694
|
+
Suggestions,
|
|
1695
|
+
ToolCall,
|
|
1696
|
+
closeIncompleteMarkdown,
|
|
1697
|
+
toSlipstreamMessage,
|
|
1698
|
+
toSlipstreamMessages,
|
|
1699
|
+
toSlipstreamToolCall,
|
|
1700
|
+
toSlipstreamToolCalls,
|
|
1701
|
+
usePromptInputAttachments,
|
|
1702
|
+
useSlipstreamMessages,
|
|
1703
|
+
useSlipstreamStream,
|
|
1704
|
+
useSpeechRecognition
|
|
1705
|
+
};
|
|
1706
|
+
//# sourceMappingURL=index.mjs.map
|