@moldable-ai/ui 0.2.8 → 0.2.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/app-error-boundary.d.ts +26 -0
- package/dist/components/app-error-boundary.d.ts.map +1 -0
- package/dist/components/app-error-boundary.js +87 -0
- package/dist/components/chat/chat-input.d.ts +8 -1
- package/dist/components/chat/chat-input.d.ts.map +1 -1
- package/dist/components/chat/chat-input.js +6 -3
- package/dist/components/chat/chat-message.d.ts +1 -0
- package/dist/components/chat/chat-message.d.ts.map +1 -1
- package/dist/components/chat/chat-message.js +100 -12
- package/dist/components/chat/chat-messages.d.ts +2 -1
- package/dist/components/chat/chat-messages.d.ts.map +1 -1
- package/dist/components/chat/chat-messages.js +3 -3
- package/dist/components/chat/chat-panel.d.ts +25 -3
- package/dist/components/chat/chat-panel.d.ts.map +1 -1
- package/dist/components/chat/chat-panel.js +158 -42
- package/dist/components/chat/index.d.ts +3 -3
- package/dist/components/chat/index.d.ts.map +1 -1
- package/dist/components/chat/index.js +1 -1
- package/dist/components/chat/model-selector.d.ts.map +1 -1
- package/dist/components/chat/model-selector.js +1 -1
- package/dist/components/chat/tool-handlers.d.ts.map +1 -1
- package/dist/components/chat/tool-handlers.js +89 -1
- package/dist/components/markdown.d.ts.map +1 -1
- package/dist/components/markdown.js +16 -2
- package/dist/components/rich-media-player.d.ts +8 -0
- package/dist/components/rich-media-player.d.ts.map +1 -0
- package/dist/components/rich-media-player.js +99 -0
- package/dist/components/ui/resizable.d.ts +4 -1
- package/dist/components/ui/resizable.d.ts.map +1 -1
- package/dist/components/ui/resizable.js +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/package.json +4 -1
- package/src/styles/index.css +25 -0
- package/dist/lib/commands.test.d.ts +0 -2
- package/dist/lib/commands.test.d.ts.map +0 -1
- package/dist/lib/commands.test.js +0 -111
|
@@ -1,36 +1,102 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { jsx as _jsx,
|
|
3
|
-
import { AlertCircle, ChevronDown, Code2, CornerDownRight, Image as ImageIcon, ListPlus, MessageCircle,
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { AlertCircle, ArrowDown, ChevronDown, ClipboardList, Code2, CornerDownRight, GripVertical, Image as ImageIcon, ListPlus, MessageCircle, Pencil, Trash2, } from 'lucide-react';
|
|
4
4
|
import { useCallback, useEffect, useMemo, useRef, useState, } from 'react';
|
|
5
5
|
import { cn } from '../../lib/utils';
|
|
6
6
|
import { Button } from '../ui/button';
|
|
7
7
|
import { ScrollArea } from '../ui/scroll-area';
|
|
8
8
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from '../ui/tooltip';
|
|
9
9
|
import { ChatInput, } from './chat-input';
|
|
10
|
-
import { ThinkingMessage } from './chat-message';
|
|
10
|
+
import { CompactingMessage, ThinkingMessage, } from './chat-message';
|
|
11
11
|
import { MessageRow, computeIsThinking, } from './chat-messages';
|
|
12
12
|
import { ConversationHistory, } from './conversation-history';
|
|
13
13
|
import { ModelSelector } from './model-selector';
|
|
14
14
|
import { ReasoningEffortSelector, } from './reasoning-effort-selector';
|
|
15
15
|
import { ToolApprovalProvider, } from './tool-approval-context';
|
|
16
16
|
import { ToolProgressProvider } from './tool-progress-context';
|
|
17
|
+
import { DndContext, KeyboardSensor, PointerSensor, closestCenter, useSensor, useSensors, } from '@dnd-kit/core';
|
|
18
|
+
import { SortableContext, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy, } from '@dnd-kit/sortable';
|
|
19
|
+
import { CSS } from '@dnd-kit/utilities';
|
|
17
20
|
import { AnimatePresence, motion } from 'framer-motion';
|
|
18
21
|
import { Virtualizer } from 'virtua';
|
|
22
|
+
function clampPercent(value) {
|
|
23
|
+
if (!Number.isFinite(value))
|
|
24
|
+
return 0;
|
|
25
|
+
return Math.max(0, Math.min(100, value));
|
|
26
|
+
}
|
|
27
|
+
function formatTokenCount(tokens) {
|
|
28
|
+
if (tokens < 1_000)
|
|
29
|
+
return `${tokens}`;
|
|
30
|
+
if (tokens < 1_000_000)
|
|
31
|
+
return `${Math.round(tokens / 1_000)}k`;
|
|
32
|
+
const millions = tokens / 1_000_000;
|
|
33
|
+
return `${Number.isInteger(millions) ? millions.toFixed(0) : millions.toFixed(2)}M`;
|
|
34
|
+
}
|
|
35
|
+
function ContextWindowIndicator({ usage }) {
|
|
36
|
+
if (!usage || usage.maxTokens <= 0)
|
|
37
|
+
return null;
|
|
38
|
+
const usedPercent = clampPercent(Math.round((usage.usedTokens / usage.maxTokens) * 100));
|
|
39
|
+
const ringColor = usedPercent >= 90
|
|
40
|
+
? 'var(--destructive)'
|
|
41
|
+
: usedPercent >= 75
|
|
42
|
+
? 'var(--warning)'
|
|
43
|
+
: 'var(--muted-foreground)';
|
|
44
|
+
return (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("button", { type: "button", className: "text-muted-foreground hover:text-foreground focus-visible:ring-ring flex size-8 cursor-pointer items-center justify-center rounded-full bg-transparent p-0 focus-visible:outline-none focus-visible:ring-2", "aria-label": `Context window ${usedPercent}% full`, children: _jsx("span", { className: "relative size-4 rounded-full", style: {
|
|
45
|
+
background: `conic-gradient(${ringColor} ${usedPercent}%, color-mix(in oklch, var(--muted-foreground) 22%, transparent) 0)`,
|
|
46
|
+
}, children: _jsx("span", { className: "bg-background absolute inset-[3px] rounded-full" }) }) }) }), _jsxs(TooltipContent, { align: "end", side: "top", sideOffset: 8, className: "w-56 px-3 py-2.5 text-center shadow-lg", children: [_jsx("div", { className: "opacity-70", children: "Context window:" }), _jsxs("div", { className: "mt-1 text-sm font-medium", children: [usedPercent, "% full"] }), _jsxs("div", { className: "mt-1 text-xs", children: [formatTokenCount(usage.usedTokens), " /", ' ', formatTokenCount(usage.maxTokens), " tokens used"] }), usage.compactsAutomatically && (_jsx("div", { className: "mt-2.5 text-xs font-medium leading-4", children: "Moldable automatically compacts its context" }))] })] }));
|
|
47
|
+
}
|
|
48
|
+
function ContextCompactedDivider() {
|
|
49
|
+
return (_jsx("div", { "data-testid": "context-compacted-divider", className: "text-muted-foreground/80 w-full px-4 py-2", children: _jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "bg-border h-px min-w-8 flex-1" }), _jsxs("div", { className: "flex select-none items-center gap-2 whitespace-nowrap text-[13px] font-medium leading-5", children: [_jsx(ClipboardList, { className: "size-4" }), _jsx("span", { children: "Context automatically compacted" })] }), _jsx("div", { className: "bg-border h-px min-w-8 flex-1" })] }) }));
|
|
50
|
+
}
|
|
51
|
+
function QueuedMessageRow({ message, isResponding, onRetryQueuedMessage, onSteerQueuedMessage, onRemoveQueuedMessage, onEditQueuedMessage, }) {
|
|
52
|
+
const isSteering = message.status === 'steering';
|
|
53
|
+
const { attributes, listeners, setNodeRef, transform, transition, isDragging, } = useSortable({ id: message.id, disabled: isSteering });
|
|
54
|
+
const style = {
|
|
55
|
+
transform: CSS.Transform.toString(transform),
|
|
56
|
+
transition,
|
|
57
|
+
};
|
|
58
|
+
return (_jsxs("div", { ref: setNodeRef, style: style, className: cn('bg-muted/60 text-foreground flex min-h-9 items-center gap-2 rounded-lg px-2 py-1.5 text-xs', isDragging && 'relative z-20 opacity-80 shadow-lg'), children: [_jsx("button", { type: "button", className: "text-muted-foreground hover:text-foreground flex size-6 shrink-0 cursor-grab items-center justify-center rounded-md transition-colors active:cursor-grabbing disabled:cursor-not-allowed disabled:opacity-40", "aria-label": "Reorder queued follow-up", disabled: isSteering, ...attributes, ...listeners, children: _jsx(GripVertical, { className: "size-3.5" }) }), _jsxs("span", { className: "min-w-0 flex-1 truncate", children: [message.preview, message.attachmentCount > 0 &&
|
|
59
|
+
` (${message.attachmentCount} image${message.attachmentCount === 1 ? '' : 's'})`] }), message.error && (_jsx(TooltipProvider, { children: _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("button", { type: "button", onClick: (event) => event.stopPropagation(), className: "text-destructive hover:bg-destructive/10 flex size-6 shrink-0 cursor-pointer items-center justify-center rounded-md transition-colors", "aria-label": "View queued follow-up error", children: _jsx(AlertCircle, { className: "size-3.5" }) }) }), _jsx(TooltipContent, { side: "top", className: "max-w-80", children: _jsx("p", { className: "break-words text-xs", children: message.error }) })] }) })), message.status === 'failed' && onRetryQueuedMessage && (_jsxs("button", { type: "button", onClick: (event) => {
|
|
60
|
+
event.stopPropagation();
|
|
61
|
+
onRetryQueuedMessage(message.id);
|
|
62
|
+
}, className: "text-muted-foreground hover:bg-accent hover:text-foreground flex h-7 shrink-0 cursor-pointer items-center gap-1.5 rounded-md px-2 text-xs transition-colors", "aria-label": "Retry queued follow-up", children: [_jsx(CornerDownRight, { className: "size-3.5" }), _jsx("span", { children: "Retry" })] })), isSteering && (_jsx("span", { className: "text-muted-foreground shrink-0 px-2 text-xs", children: "Steering" })), !isSteering && isResponding && onSteerQueuedMessage && (_jsxs("button", { type: "button", onClick: (event) => {
|
|
63
|
+
event.stopPropagation();
|
|
64
|
+
onSteerQueuedMessage(message.id);
|
|
65
|
+
}, className: "text-muted-foreground hover:bg-accent hover:text-foreground flex h-7 shrink-0 cursor-pointer items-center gap-1.5 rounded-md px-2 text-xs transition-colors", "aria-label": "Steer active response with queued follow-up", children: [_jsx(CornerDownRight, { className: "size-3.5" }), _jsx("span", { children: "Steer" })] })), !isSteering && onEditQueuedMessage && (_jsx("button", { type: "button", onClick: (event) => {
|
|
66
|
+
event.stopPropagation();
|
|
67
|
+
onEditQueuedMessage(message.id);
|
|
68
|
+
}, className: "text-muted-foreground hover:bg-accent hover:text-foreground flex size-6 shrink-0 cursor-pointer items-center justify-center rounded-md transition-colors", "aria-label": "Edit queued follow-up", children: _jsx(Pencil, { className: "size-3.5" }) })), !isSteering && onRemoveQueuedMessage && (_jsx("button", { type: "button", onClick: (event) => {
|
|
69
|
+
event.stopPropagation();
|
|
70
|
+
onRemoveQueuedMessage(message.id);
|
|
71
|
+
}, className: "text-muted-foreground hover:bg-destructive/10 hover:text-destructive flex size-6 shrink-0 cursor-pointer items-center justify-center rounded-md transition-colors", "aria-label": "Remove queued follow-up", children: _jsx(Trash2, { className: "size-3.5" }) }))] }));
|
|
72
|
+
}
|
|
19
73
|
/**
|
|
20
74
|
* Floating chat panel with model selector
|
|
21
75
|
*/
|
|
22
|
-
export function ChatPanel({ messages, status, isTurnInFlight, input, onInputChange, onSubmit, onStop, onNewChat, models, selectedModel, onModelChange, reasoningEffortOptions, selectedReasoningEffort, onReasoningEffortChange, conversations, currentConversationId, onSelectConversation, onDeleteConversation, placeholder = 'Ask anything...', welcomeMessage, isExpanded, onExpandedChange, isMinimized = false, onMinimizedChange, className, error, missingApiKey, onAddApiKey, toolProgress = {}, onApprovalResponse, isEditingApp = true, onEditingAppChange, showEditingAppToggle = false, checkpoints, restoringMessageId, onRestoreCheckpoint, imageAttachments = [], onRemoveImageAttachment, isImageDropActive = false, queuedMessages = [], onRemoveQueuedMessage, onRetryQueuedMessage, onSteerQueuedMessage, }) {
|
|
76
|
+
export function ChatPanel({ messages, status, isTurnInFlight, input, onInputChange, onSubmit, onStop, onNewChat, models, selectedModel, onModelChange, reasoningEffortOptions, selectedReasoningEffort, onReasoningEffortChange, conversations, currentConversationId, scrollToBottomSignal = 0, onSelectConversation, onDeleteConversation, placeholder = 'Ask anything...', welcomeMessage, isExpanded, onExpandedChange, isMinimized = false, onMinimizedChange, className, error, missingApiKey, onAddApiKey, toolProgress = {}, onApprovalResponse, isEditingApp = true, onEditingAppChange, showEditingAppToggle = false, checkpoints, restoringMessageId, onRestoreCheckpoint, imageAttachments = [], onRemoveImageAttachment, isImageDropActive = false, queuedMessages = [], onRemoveQueuedMessage, onRetryQueuedMessage, onSteerQueuedMessage, onReorderQueuedMessages, onEditQueuedMessage, slashCommands, onSlashCommandSelect, isCompacting = false, compactedHistoryStartIndex, contextUsage, }) {
|
|
23
77
|
const inputRef = useRef(null);
|
|
24
78
|
const scrollAreaRef = useRef(null);
|
|
25
79
|
const viewportRef = useRef(null);
|
|
26
80
|
const virtualizerRef = useRef(null);
|
|
27
81
|
const prevMessageCountRef = useRef(messages.length);
|
|
82
|
+
const prevConversationIdRef = useRef(currentConversationId ?? null);
|
|
83
|
+
const prevScrollToBottomSignalRef = useRef(scrollToBottomSignal);
|
|
84
|
+
const pendingConversationScrollIdRef = useRef(null);
|
|
85
|
+
const hasPendingScrollToBottomSignalRef = useRef(false);
|
|
28
86
|
const scrollRafRef = useRef(null);
|
|
29
87
|
const [viewportVersion, setViewportVersion] = useState(0);
|
|
30
88
|
const [isAtBottom, setIsAtBottom] = useState(true);
|
|
89
|
+
const queuedMessageIds = useMemo(() => queuedMessages.map((message) => message.id), [queuedMessages]);
|
|
90
|
+
const queueSensors = useSensors(useSensor(PointerSensor, {
|
|
91
|
+
activationConstraint: {
|
|
92
|
+
distance: 6,
|
|
93
|
+
},
|
|
94
|
+
}), useSensor(KeyboardSensor, {
|
|
95
|
+
coordinateGetter: sortableKeyboardCoordinates,
|
|
96
|
+
}));
|
|
31
97
|
const isResponding = status === 'streaming' || status === 'submitted';
|
|
32
98
|
const controlsDisabled = isTurnInFlight ?? isResponding;
|
|
33
|
-
const composerControls = (_jsxs(_Fragment, { children: [_jsx(ModelSelector, { models: models, selectedModel: selectedModel, onModelChange: onModelChange, disabled: controlsDisabled, dropdownAlign: "end", dropdownSide: "top" }), reasoningEffortOptions &&
|
|
99
|
+
const composerControls = (_jsxs(_Fragment, { children: [_jsx(ContextWindowIndicator, { usage: contextUsage }), _jsx(ModelSelector, { models: models, selectedModel: selectedModel, onModelChange: onModelChange, disabled: controlsDisabled, dropdownAlign: "end", dropdownSide: "top" }), reasoningEffortOptions &&
|
|
34
100
|
selectedReasoningEffort &&
|
|
35
101
|
onReasoningEffortChange && (_jsx(ReasoningEffortSelector, { options: reasoningEffortOptions, selectedEffort: selectedReasoningEffort, onEffortChange: onReasoningEffortChange, disabled: controlsDisabled, showLabel: true, dropdownAlign: "end", dropdownSide: "top" }))] }));
|
|
36
102
|
// Capture the Radix viewport element so virtua can use it as the scroll container
|
|
@@ -88,10 +154,6 @@ export function ChatPanel({ messages, status, isTurnInFlight, input, onInputChan
|
|
|
88
154
|
if (!input.trim() && imageAttachments.length === 0)
|
|
89
155
|
return;
|
|
90
156
|
const submitMode = mode ?? (isResponding ? 'steer' : 'send');
|
|
91
|
-
// Steer interrupts the current response and sends the new instruction now.
|
|
92
|
-
if (submitMode === 'steer' && isResponding && onStop) {
|
|
93
|
-
onStop();
|
|
94
|
-
}
|
|
95
157
|
if (!isExpanded) {
|
|
96
158
|
onExpandedChange(true);
|
|
97
159
|
}
|
|
@@ -105,9 +167,21 @@ export function ChatPanel({ messages, status, isTurnInFlight, input, onInputChan
|
|
|
105
167
|
isExpanded,
|
|
106
168
|
onExpandedChange,
|
|
107
169
|
onSubmit,
|
|
108
|
-
onStop,
|
|
109
170
|
submitDisabled,
|
|
110
171
|
]);
|
|
172
|
+
const handleQueuedDragEnd = useCallback((event) => {
|
|
173
|
+
const { active, over } = event;
|
|
174
|
+
if (!over || active.id === over.id)
|
|
175
|
+
return;
|
|
176
|
+
onReorderQueuedMessages?.(String(active.id), String(over.id));
|
|
177
|
+
}, [onReorderQueuedMessages]);
|
|
178
|
+
const handleEditQueuedMessage = useCallback((id) => {
|
|
179
|
+
onEditQueuedMessage?.(id);
|
|
180
|
+
if (!isExpanded) {
|
|
181
|
+
onExpandedChange(true);
|
|
182
|
+
}
|
|
183
|
+
window.setTimeout(() => inputRef.current?.focus(), 0);
|
|
184
|
+
}, [isExpanded, onEditQueuedMessage, onExpandedChange]);
|
|
111
185
|
const handleNewChat = useCallback(() => {
|
|
112
186
|
onNewChat?.();
|
|
113
187
|
inputRef.current?.focus();
|
|
@@ -156,12 +230,24 @@ export function ChatPanel({ messages, status, isTurnInFlight, input, onInputChan
|
|
|
156
230
|
if (messages.length === 0 && welcomeMessage) {
|
|
157
231
|
items.push({ key: 'welcome', kind: 'welcome' });
|
|
158
232
|
}
|
|
159
|
-
|
|
233
|
+
const shouldShowCompactedDivider = typeof compactedHistoryStartIndex === 'number' &&
|
|
234
|
+
compactedHistoryStartIndex >= 0 &&
|
|
235
|
+
compactedHistoryStartIndex < messages.length;
|
|
236
|
+
for (const [index, message] of messages.entries()) {
|
|
237
|
+
if (shouldShowCompactedDivider && index === compactedHistoryStartIndex) {
|
|
238
|
+
items.push({
|
|
239
|
+
key: `context-compacted-${compactedHistoryStartIndex}`,
|
|
240
|
+
kind: 'context-compacted-divider',
|
|
241
|
+
});
|
|
242
|
+
}
|
|
160
243
|
items.push({ key: message.id, kind: 'message', message });
|
|
161
244
|
}
|
|
162
245
|
if (isThinking) {
|
|
163
246
|
items.push({ key: 'thinking', kind: 'thinking' });
|
|
164
247
|
}
|
|
248
|
+
if (isCompacting) {
|
|
249
|
+
items.push({ key: 'compacting', kind: 'compacting' });
|
|
250
|
+
}
|
|
165
251
|
if (showMissingApiKeyPrompt) {
|
|
166
252
|
items.push({ key: 'missing-api-key', kind: 'missing-api-key' });
|
|
167
253
|
}
|
|
@@ -171,6 +257,8 @@ export function ChatPanel({ messages, status, isTurnInFlight, input, onInputChan
|
|
|
171
257
|
return items;
|
|
172
258
|
}, [
|
|
173
259
|
isThinking,
|
|
260
|
+
isCompacting,
|
|
261
|
+
compactedHistoryStartIndex,
|
|
174
262
|
messages,
|
|
175
263
|
showErrorMessage,
|
|
176
264
|
showMissingApiKeyPrompt,
|
|
@@ -180,12 +268,16 @@ export function ChatPanel({ messages, status, isTurnInFlight, input, onInputChan
|
|
|
180
268
|
switch (item.kind) {
|
|
181
269
|
case 'welcome':
|
|
182
270
|
return (_jsx("div", { className: "px-2 pb-4", children: messages.length === 0 && welcomeMessage && (_jsx("div", { className: "bg-muted/50 text-muted-foreground mx-2 rounded-2xl p-4 text-sm", children: welcomeMessage })) }));
|
|
271
|
+
case 'context-compacted-divider':
|
|
272
|
+
return (_jsx("div", { className: "px-2 pb-3", children: _jsx(ContextCompactedDivider, {}) }));
|
|
183
273
|
case 'message': {
|
|
184
274
|
const checkpoint = checkpoints?.get(item.message.id);
|
|
185
275
|
return (_jsx("div", { className: "px-2 pb-3", children: _jsx(MessageRow, { message: item.message, isLast: item.message.id === lastMessageId, isStreaming: isStreaming, checkpoint: checkpoint, restoringMessageId: restoringMessageId, onRestoreCheckpoint: onRestoreCheckpoint }) }));
|
|
186
276
|
}
|
|
187
277
|
case 'thinking':
|
|
188
278
|
return (_jsx("div", { className: "px-2 pb-3", children: _jsx(ThinkingMessage, {}) }));
|
|
279
|
+
case 'compacting':
|
|
280
|
+
return (_jsx("div", { className: "px-2 pb-3", children: _jsx(CompactingMessage, {}) }));
|
|
189
281
|
case 'missing-api-key':
|
|
190
282
|
return (_jsx("div", { className: "px-2 pb-4", children: _jsxs("div", { className: "border-primary/30 bg-primary/5 mx-2 flex flex-col items-center gap-3 rounded-lg border p-4 text-center", children: [_jsx("div", { className: "bg-primary/10 flex size-10 items-center justify-center rounded-full", children: _jsx(AlertCircle, { className: "text-primary size-5" }) }), _jsxs("div", { children: [_jsx("p", { className: "text-foreground font-medium", children: "API key required" }), _jsx("p", { className: "text-muted-foreground mt-1 text-sm", children: "Add an API key to Vault to start chatting with AI" })] }), onAddApiKey && (_jsx(Button, { onClick: onAddApiKey, size: "sm", className: "cursor-pointer", children: "Add API key" }))] }) }));
|
|
191
283
|
case 'error':
|
|
@@ -202,35 +294,71 @@ export function ChatPanel({ messages, status, isTurnInFlight, input, onInputChan
|
|
|
202
294
|
restoringMessageId,
|
|
203
295
|
welcomeMessage,
|
|
204
296
|
]);
|
|
205
|
-
|
|
206
|
-
useEffect(() => {
|
|
207
|
-
const previousMessageCount = prevMessageCountRef.current;
|
|
208
|
-
const hasNewMessage = messages.length > previousMessageCount;
|
|
209
|
-
prevMessageCountRef.current = messages.length;
|
|
210
|
-
if (!isExpanded || !isAtBottom) {
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
297
|
+
const scrollToBottom = useCallback((smooth = true) => {
|
|
213
298
|
const lastIndex = listItems.length - 1;
|
|
214
299
|
if (lastIndex < 0)
|
|
215
300
|
return;
|
|
216
301
|
if (scrollRafRef.current !== null) {
|
|
217
302
|
cancelAnimationFrame(scrollRafRef.current);
|
|
218
303
|
}
|
|
304
|
+
setIsAtBottom(true);
|
|
219
305
|
scrollRafRef.current = requestAnimationFrame(() => {
|
|
220
306
|
virtualizerRef.current?.scrollToIndex(lastIndex, {
|
|
221
307
|
align: 'end',
|
|
222
|
-
smooth
|
|
308
|
+
smooth,
|
|
223
309
|
});
|
|
224
310
|
scrollRafRef.current = null;
|
|
225
311
|
});
|
|
312
|
+
}, [listItems.length]);
|
|
313
|
+
// New conversations loaded from history should start at the latest message.
|
|
314
|
+
useEffect(() => {
|
|
315
|
+
const previousConversationId = prevConversationIdRef.current;
|
|
316
|
+
const nextConversationId = currentConversationId ?? null;
|
|
317
|
+
prevConversationIdRef.current = nextConversationId;
|
|
318
|
+
if (previousConversationId !== nextConversationId) {
|
|
319
|
+
pendingConversationScrollIdRef.current = nextConversationId;
|
|
320
|
+
setIsAtBottom(true);
|
|
321
|
+
}
|
|
322
|
+
if (!nextConversationId ||
|
|
323
|
+
pendingConversationScrollIdRef.current !== nextConversationId ||
|
|
324
|
+
!isExpanded ||
|
|
325
|
+
listItems.length === 0) {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
scrollToBottom(false);
|
|
329
|
+
pendingConversationScrollIdRef.current = null;
|
|
330
|
+
}, [currentConversationId, isExpanded, listItems.length, scrollToBottom]);
|
|
331
|
+
useEffect(() => {
|
|
332
|
+
if (prevScrollToBottomSignalRef.current !== scrollToBottomSignal) {
|
|
333
|
+
prevScrollToBottomSignalRef.current = scrollToBottomSignal;
|
|
334
|
+
hasPendingScrollToBottomSignalRef.current = true;
|
|
335
|
+
setIsAtBottom(true);
|
|
336
|
+
}
|
|
337
|
+
if (!hasPendingScrollToBottomSignalRef.current ||
|
|
338
|
+
!isExpanded ||
|
|
339
|
+
listItems.length === 0) {
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
scrollToBottom(false);
|
|
343
|
+
hasPendingScrollToBottomSignalRef.current = false;
|
|
344
|
+
}, [isExpanded, listItems.length, scrollToBottom, scrollToBottomSignal]);
|
|
345
|
+
// Scroll to bottom on new messages if the user is already at bottom
|
|
346
|
+
useEffect(() => {
|
|
347
|
+
const previousMessageCount = prevMessageCountRef.current;
|
|
348
|
+
const hasNewMessage = messages.length > previousMessageCount;
|
|
349
|
+
prevMessageCountRef.current = messages.length;
|
|
350
|
+
if (!isExpanded || !isAtBottom) {
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
scrollToBottom(hasNewMessage);
|
|
226
354
|
return () => {
|
|
227
355
|
if (scrollRafRef.current !== null) {
|
|
228
356
|
cancelAnimationFrame(scrollRafRef.current);
|
|
229
357
|
scrollRafRef.current = null;
|
|
230
358
|
}
|
|
231
359
|
};
|
|
232
|
-
}, [isAtBottom, isExpanded,
|
|
233
|
-
return (_jsxs(_Fragment, { children: [_jsx(AnimatePresence, { children: isExpanded && !isMinimized && (_jsx(motion.div, { initial: { opacity: 0 }, animate: { opacity: 1 }, exit: { opacity: 0 }, transition: { duration: 0.2 }, className: "fixed inset-0 z-40 bg-black/20", onClick: () => onExpandedChange(false) })) }), _jsx(AnimatePresence, { children: isMinimized && (_jsx(motion.div, { initial: { y: 20, opacity: 0 }, animate: { y: 0, opacity: 1 }, exit: { y: 20, opacity: 0 }, transition: { type: 'tween', duration: 0.2, ease: 'easeOut' }, className: "fixed bottom-0 left-[calc(50%+var(--sidebar-width,0px)/2)] z-50 -translate-x-1/2", children: _jsxs("button", { onClick: handleFlapClick, className: cn('bg-background/95 hover:bg-background group flex cursor-pointer items-center gap-2 rounded-t-xl border border-b-0 px-4 py-2 shadow-lg backdrop-blur transition-colors', 'text-muted-foreground hover:text-foreground'), children: [_jsx(MessageCircle, { className: "size-4" }), _jsx("span", { className: "text-sm font-medium", children: "Chat" }), _jsx("span", { className: "bg-muted text-muted-foreground group-hover:bg-primary/10 group-hover:text-primary rounded px-1.5 py-0.5 text-xs transition-colors", children: "\u2318M" })] }) })) }), _jsx(motion.div, { initial: false, animate: {
|
|
360
|
+
}, [isAtBottom, isExpanded, messages.length, scrollToBottom]);
|
|
361
|
+
return (_jsxs(_Fragment, { children: [_jsx(AnimatePresence, { children: isExpanded && !isMinimized && (_jsx(motion.div, { "data-moldable-chat-backdrop": true, initial: { opacity: 0 }, animate: { opacity: 1 }, exit: { opacity: 0 }, transition: { duration: 0.2 }, className: "fixed inset-0 z-40 bg-black/20", onClick: () => onExpandedChange(false) })) }), _jsx(AnimatePresence, { children: isMinimized && (_jsx(motion.div, { "data-moldable-chat-panel": true, initial: { y: 20, opacity: 0 }, animate: { y: 0, opacity: 1 }, exit: { y: 20, opacity: 0 }, transition: { type: 'tween', duration: 0.2, ease: 'easeOut' }, className: "fixed bottom-0 left-[calc(50%+var(--sidebar-width,0px)/2)] z-50 -translate-x-1/2", children: _jsxs("button", { onClick: handleFlapClick, className: cn('bg-background/95 hover:bg-background group flex cursor-pointer items-center gap-2 rounded-t-xl border border-b-0 px-4 py-2 shadow-lg backdrop-blur transition-colors', 'text-muted-foreground hover:text-foreground'), children: [_jsx(MessageCircle, { className: "size-4" }), _jsx("span", { className: "text-sm font-medium", children: "Chat" }), _jsx("span", { className: "bg-muted text-muted-foreground group-hover:bg-primary/10 group-hover:text-primary rounded px-1.5 py-0.5 text-xs transition-colors", children: "\u2318M" })] }) })) }), _jsx(motion.div, { "data-moldable-chat-panel": true, initial: false, animate: {
|
|
234
362
|
width: isExpanded ? 'min(90vw, 640px)' : '400px',
|
|
235
363
|
y: isMinimized ? 'calc(100% + 24px)' : 0,
|
|
236
364
|
opacity: isMinimized ? 0 : 1,
|
|
@@ -256,24 +384,12 @@ export function ChatPanel({ messages, status, isTurnInFlight, input, onInputChan
|
|
|
256
384
|
repeat: Infinity,
|
|
257
385
|
repeatDelay: 5,
|
|
258
386
|
ease: [0.4, 0, 0.2, 1],
|
|
259
|
-
} })), _jsxs(motion.div, { initial: false, animate: {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
` (${message.attachmentCount} image${message.attachmentCount === 1 ? '' : 's'})`] }), message.status === 'failed' &&
|
|
268
|
-
message.error &&
|
|
269
|
-
onRetryQueuedMessage && (_jsxs("button", { type: "button", onClick: (event) => {
|
|
270
|
-
event.stopPropagation();
|
|
271
|
-
onRetryQueuedMessage(message.id);
|
|
272
|
-
}, className: "text-muted-foreground hover:bg-accent hover:text-foreground flex h-7 shrink-0 cursor-pointer items-center gap-1.5 rounded-md px-2 text-xs transition-colors", "aria-label": "Retry queued follow-up", children: [_jsx(CornerDownRight, { className: "size-3.5" }), _jsx("span", { children: "Retry" })] })), isResponding && onSteerQueuedMessage && (_jsxs("button", { type: "button", onClick: (event) => {
|
|
273
|
-
event.stopPropagation();
|
|
274
|
-
onSteerQueuedMessage(message.id);
|
|
275
|
-
}, className: "text-muted-foreground hover:bg-accent hover:text-foreground flex h-7 shrink-0 cursor-pointer items-center gap-1.5 rounded-md px-2 text-xs transition-colors", "aria-label": "Steer active response with queued follow-up", children: [_jsx(CornerDownRight, { className: "size-3.5" }), _jsx("span", { children: "Steer" })] })), onRemoveQueuedMessage && (_jsx("button", { type: "button", onClick: (event) => {
|
|
276
|
-
event.stopPropagation();
|
|
277
|
-
onRemoveQueuedMessage(message.id);
|
|
278
|
-
}, className: "text-muted-foreground hover:bg-accent hover:text-foreground flex size-6 shrink-0 cursor-pointer items-center justify-center rounded-md transition-colors", "aria-label": "Remove queued follow-up", children: _jsx(X, { className: "size-3.5" }) }))] }, message.id))) })] })), _jsx(ChatInput, { input: input, onInputChange: onInputChange, onSubmit: handleSubmit, isResponding: isResponding, placeholder: placeholder, inputRef: inputRef, onStop: onStop, compact: !isExpanded, imageAttachments: imageAttachments, onRemoveImageAttachment: onRemoveImageAttachment, footerStartControls: isExpanded ? composerStartControls : null, footerControls: isExpanded ? composerControls : null, submitDisabled: submitDisabled })] })] }) })] }));
|
|
387
|
+
} })), _jsxs("div", { className: "relative z-10 overflow-hidden rounded-[inherit]", children: [_jsxs(motion.div, { initial: false, animate: {
|
|
388
|
+
height: isExpanded ? 'min(60vh, 480px)' : '0px',
|
|
389
|
+
opacity: isExpanded ? 1 : 0,
|
|
390
|
+
}, transition: { type: 'tween', duration: 0.2, ease: 'easeOut' }, className: "flex flex-col overflow-hidden", style: { pointerEvents: isExpanded ? 'auto' : 'none' }, children: [_jsx("div", { className: "absolute right-3 top-3 z-20", children: _jsx(TooltipProvider, { children: _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon-sm", onClick: handleMinimize, className: "text-muted-foreground hover:text-foreground cursor-pointer", children: _jsx(ChevronDown, { className: "size-4" }) }) }), _jsx(TooltipContent, { side: "bottom", children: _jsx("p", { children: "Minimize" }) })] }) }) }), _jsxs("div", { className: "relative min-h-0 min-w-0 flex-1", children: [_jsx(ScrollArea, { ref: scrollAreaRef, className: "h-full px-2", children: _jsx("div", { className: "min-w-0 max-w-full overflow-hidden py-4 pr-10", children: _jsx(ToolProgressProvider, { value: toolProgress, children: _jsx(ToolApprovalProvider, { onApprovalResponse: onApprovalResponse ?? null, children: viewportVersion > 0 ? (_jsx(Virtualizer, { ref: virtualizerRef, data: listItems, scrollRef: viewportRef, bufferSize: 300, children: (item) => renderVirtualItem(item) }, viewportVersion)) : (listItems.map((item) => (_jsx("div", { children: renderVirtualItem(item) }, item.key)))) }) }) }) }), !isAtBottom && listItems.length > 0 && (_jsx(TooltipProvider, { children: _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx(Button, { type: "button", variant: "secondary", size: "icon-sm", onClick: () => scrollToBottom(true), className: "absolute bottom-3 right-4 z-20 size-8 cursor-pointer rounded-full shadow-lg", children: _jsx(ArrowDown, { className: "size-4" }) }) }), _jsx(TooltipContent, { side: "top", children: "Scroll to bottom" })] }) }))] })] }), _jsxs("div", { className: "relative z-10", onClick: () => {
|
|
391
|
+
if (!isExpanded) {
|
|
392
|
+
onExpandedChange(true);
|
|
393
|
+
}
|
|
394
|
+
}, children: [queuedMessages.length > 0 && (_jsxs("div", { className: "border-border/70 bg-background/95 border-t px-4 py-2", children: [_jsxs("div", { className: "text-muted-foreground mb-2 flex items-center gap-2 text-xs font-medium", children: [_jsx(ListPlus, { className: "size-3.5" }), _jsx("span", { children: "Queued follow-ups" })] }), _jsx(DndContext, { sensors: queueSensors, collisionDetection: closestCenter, onDragEnd: handleQueuedDragEnd, children: _jsx(SortableContext, { items: queuedMessageIds, strategy: verticalListSortingStrategy, children: _jsx("div", { className: "space-y-1.5", children: queuedMessages.map((message) => (_jsx(QueuedMessageRow, { message: message, isResponding: isResponding, onRetryQueuedMessage: onRetryQueuedMessage, onSteerQueuedMessage: onSteerQueuedMessage, onRemoveQueuedMessage: onRemoveQueuedMessage, onEditQueuedMessage: handleEditQueuedMessage }, message.id))) }) }) })] })), _jsx(ChatInput, { input: input, onInputChange: onInputChange, onSubmit: handleSubmit, isResponding: isResponding, placeholder: placeholder, inputRef: inputRef, onStop: onStop, compact: !isExpanded, imageAttachments: imageAttachments, onRemoveImageAttachment: onRemoveImageAttachment, footerStartControls: isExpanded ? composerStartControls : null, footerControls: isExpanded ? composerControls : null, submitDisabled: submitDisabled, slashCommands: slashCommands, onSlashCommandSelect: onSlashCommandSelect })] })] })] }) })] }));
|
|
279
395
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
export { ChatInput, type ChatImageAttachment, type ChatSubmitMode, } from './chat-input';
|
|
2
|
-
export { Message, ThinkingMessage, type ChatMessage, type ChatMessagePart, } from './chat-message';
|
|
1
|
+
export { ChatInput, type ChatImageAttachment, type ChatSlashCommandOption, type ChatSubmitMode, } from './chat-input';
|
|
2
|
+
export { CompactingMessage, Message, ThinkingMessage, type ChatMessage, type ChatMessagePart, } from './chat-message';
|
|
3
3
|
export { Messages, type MessageCheckpoint } from './chat-messages';
|
|
4
4
|
export { CheckpointBadge, type CheckpointBadgeProps } from './checkpoint-badge';
|
|
5
5
|
export { RestoreDialog, type CheckpointInfo, type RestoreDialogProps, } from './restore-dialog';
|
|
6
6
|
export { ModelSelector, type ModelOption } from './model-selector';
|
|
7
7
|
export { ReasoningEffortSelector, type ReasoningEffortOption, } from './reasoning-effort-selector';
|
|
8
8
|
export { ConversationHistory, type ConversationMeta, } from './conversation-history';
|
|
9
|
-
export { ChatPanel, type ChatPanelProps, type QueuedChatMessage, type ToolProgressData, } from './chat-panel';
|
|
9
|
+
export { ChatPanel, type ChatContextUsage, type ChatPanelProps, type QueuedChatMessage, type ToolProgressData, } from './chat-panel';
|
|
10
10
|
export { ThinkingTimeline, ThinkingTimelineMarker, type ThinkingTimelineItem, type ThinkingTimelineProps, } from './thinking-timeline';
|
|
11
11
|
export { getToolHandler, DEFAULT_TOOL_HANDLERS, type ToolHandler, } from './tool-handlers';
|
|
12
12
|
export { ToolProgressProvider, useToolProgress, useToolCallProgress, } from './tool-progress-context';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/chat/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,SAAS,EACT,KAAK,mBAAmB,EACxB,KAAK,cAAc,GACpB,MAAM,cAAc,CAAA;AACrB,OAAO,EACL,OAAO,EACP,eAAe,EACf,KAAK,WAAW,EAChB,KAAK,eAAe,GACrB,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAAE,QAAQ,EAAE,KAAK,iBAAiB,EAAE,MAAM,iBAAiB,CAAA;AAClE,OAAO,EAAE,eAAe,EAAE,KAAK,oBAAoB,EAAE,MAAM,oBAAoB,CAAA;AAC/E,OAAO,EACL,aAAa,EACb,KAAK,cAAc,EACnB,KAAK,kBAAkB,GACxB,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAE,aAAa,EAAE,KAAK,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAClE,OAAO,EACL,uBAAuB,EACvB,KAAK,qBAAqB,GAC3B,MAAM,6BAA6B,CAAA;AACpC,OAAO,EACL,mBAAmB,EACnB,KAAK,gBAAgB,GACtB,MAAM,wBAAwB,CAAA;AAC/B,OAAO,EACL,SAAS,EACT,KAAK,cAAc,EACnB,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,GACtB,MAAM,cAAc,CAAA;AACrB,OAAO,EACL,gBAAgB,EAChB,sBAAsB,EACtB,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,GAC3B,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EACL,cAAc,EACd,qBAAqB,EACrB,KAAK,WAAW,GACjB,MAAM,iBAAiB,CAAA;AACxB,OAAO,EACL,oBAAoB,EACpB,eAAe,EACf,mBAAmB,GACpB,MAAM,yBAAyB,CAAA;AAChC,OAAO,EACL,oBAAoB,EACpB,uBAAuB,EACvB,KAAK,uBAAuB,GAC7B,MAAM,yBAAyB,CAAA;AAChC,OAAO,EACL,YAAY,EACZ,kBAAkB,EAClB,mBAAmB,EACnB,oBAAoB,EACpB,oBAAoB,EACpB,mBAAmB,EACnB,kBAAkB,EAClB,KAAK,iBAAiB,GACvB,MAAM,iBAAiB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/chat/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,SAAS,EACT,KAAK,mBAAmB,EACxB,KAAK,sBAAsB,EAC3B,KAAK,cAAc,GACpB,MAAM,cAAc,CAAA;AACrB,OAAO,EACL,iBAAiB,EACjB,OAAO,EACP,eAAe,EACf,KAAK,WAAW,EAChB,KAAK,eAAe,GACrB,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAAE,QAAQ,EAAE,KAAK,iBAAiB,EAAE,MAAM,iBAAiB,CAAA;AAClE,OAAO,EAAE,eAAe,EAAE,KAAK,oBAAoB,EAAE,MAAM,oBAAoB,CAAA;AAC/E,OAAO,EACL,aAAa,EACb,KAAK,cAAc,EACnB,KAAK,kBAAkB,GACxB,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAE,aAAa,EAAE,KAAK,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAClE,OAAO,EACL,uBAAuB,EACvB,KAAK,qBAAqB,GAC3B,MAAM,6BAA6B,CAAA;AACpC,OAAO,EACL,mBAAmB,EACnB,KAAK,gBAAgB,GACtB,MAAM,wBAAwB,CAAA;AAC/B,OAAO,EACL,SAAS,EACT,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,GACtB,MAAM,cAAc,CAAA;AACrB,OAAO,EACL,gBAAgB,EAChB,sBAAsB,EACtB,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,GAC3B,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EACL,cAAc,EACd,qBAAqB,EACrB,KAAK,WAAW,GACjB,MAAM,iBAAiB,CAAA;AACxB,OAAO,EACL,oBAAoB,EACpB,eAAe,EACf,mBAAmB,GACpB,MAAM,yBAAyB,CAAA;AAChC,OAAO,EACL,oBAAoB,EACpB,uBAAuB,EACvB,KAAK,uBAAuB,GAC7B,MAAM,yBAAyB,CAAA;AAChC,OAAO,EACL,YAAY,EACZ,kBAAkB,EAClB,mBAAmB,EACnB,oBAAoB,EACpB,oBAAoB,EACpB,mBAAmB,EACnB,kBAAkB,EAClB,KAAK,iBAAiB,GACvB,MAAM,iBAAiB,CAAA"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { ChatInput, } from './chat-input';
|
|
2
|
-
export { Message, ThinkingMessage, } from './chat-message';
|
|
2
|
+
export { CompactingMessage, Message, ThinkingMessage, } from './chat-message';
|
|
3
3
|
export { Messages } from './chat-messages';
|
|
4
4
|
export { CheckpointBadge } from './checkpoint-badge';
|
|
5
5
|
export { RestoreDialog, } from './restore-dialog';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"model-selector.d.ts","sourceRoot":"","sources":["../../../src/components/chat/model-selector.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAUtC,MAAM,MAAM,WAAW,GAAG;IACxB,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,SAAS,CAAA;CAChB,CAAA;AAED,KAAK,kBAAkB,GAAG;IACxB,MAAM,EAAE,WAAW,EAAE,CAAA;IACrB,aAAa,EAAE,MAAM,CAAA;IACrB,aAAa,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;IACxC,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,aAAa,CAAC,EAAE,OAAO,GAAG,QAAQ,GAAG,KAAK,CAAA;IAC1C,YAAY,CAAC,EAAE,KAAK,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,CAAA;CACnD,CAAA;AAED,wBAAgB,aAAa,CAAC,EAC5B,MAAM,EACN,aAAa,EACb,aAAa,EACb,QAAgB,EAChB,SAAS,EACT,aAAuB,EACvB,YAAuB,GACxB,EAAE,kBAAkB,
|
|
1
|
+
{"version":3,"file":"model-selector.d.ts","sourceRoot":"","sources":["../../../src/components/chat/model-selector.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAUtC,MAAM,MAAM,WAAW,GAAG;IACxB,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,SAAS,CAAA;CAChB,CAAA;AAED,KAAK,kBAAkB,GAAG;IACxB,MAAM,EAAE,WAAW,EAAE,CAAA;IACrB,aAAa,EAAE,MAAM,CAAA;IACrB,aAAa,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;IACxC,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,aAAa,CAAC,EAAE,OAAO,GAAG,QAAQ,GAAG,KAAK,CAAA;IAC1C,YAAY,CAAC,EAAE,KAAK,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,CAAA;CACnD,CAAA;AAED,wBAAgB,aAAa,CAAC,EAC5B,MAAM,EACN,aAAa,EACb,aAAa,EACb,QAAgB,EAChB,SAAS,EACT,aAAuB,EACvB,YAAuB,GACxB,EAAE,kBAAkB,2CAwCpB"}
|
|
@@ -5,5 +5,5 @@ import { Button } from '../ui/button';
|
|
|
5
5
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '../ui/dropdown-menu';
|
|
6
6
|
export function ModelSelector({ models, selectedModel, onModelChange, disabled = false, className, dropdownAlign = 'start', dropdownSide = 'bottom', }) {
|
|
7
7
|
const currentModel = models.find((m) => m.id === selectedModel) ?? models[0];
|
|
8
|
-
return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(Button, { variant: "ghost", size: "sm", disabled: disabled, className: cn('text-muted-foreground hover:text-foreground h-8 min-w-0 max-w-[180px] cursor-pointer gap-1
|
|
8
|
+
return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(Button, { variant: "ghost", size: "sm", disabled: disabled, className: cn('text-muted-foreground hover:text-foreground h-8 min-w-0 max-w-[180px] cursor-pointer gap-1 rounded-full px-2.5 text-xs font-medium', className), children: [_jsx("span", { className: "min-w-0 truncate", children: currentModel?.name }), _jsx(ChevronDown, { className: "size-3 opacity-50" })] }) }), _jsx(DropdownMenuContent, { align: dropdownAlign, side: dropdownSide, className: "min-w-[160px]", children: models.map((model) => (_jsxs(DropdownMenuItem, { onClick: () => onModelChange(model.id), className: "flex cursor-pointer items-center gap-2", children: [_jsx("span", { children: model.icon }), _jsx("span", { className: "flex-1", children: model.name }), model.id === selectedModel && (_jsx(Check, { className: "text-primary size-4" }))] }, model.id))) })] }));
|
|
9
9
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tool-handlers.d.ts","sourceRoot":"","sources":["../../../src/components/chat/tool-handlers.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"tool-handlers.d.ts","sourceRoot":"","sources":["../../../src/components/chat/tool-handlers.tsx"],"names":[],"mappings":"AAwBA,OAAO,EAAE,KAAK,SAAS,EAAY,MAAM,OAAO,CAAA;AAEhD,OAAO,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAA;AAW5D,OAAO,EAAE,KAAK,uBAAuB,EAAE,MAAM,yBAAyB,CAAA;AAEtE;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,UAAU,EAAE,MAAM,CAAA;IAClB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,SAAS,GAAG,UAAU,CAAA;CAC/B;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;IAClB,IAAI,EAAE,OAAO,CAAA;CACd;AAED;;GAEG;AACH,MAAM,MAAM,WAAW,GAAG;IAExB,YAAY,EAAE,MAAM,CAAA;IAEpB,MAAM,CAAC,EAAE,sBAAsB,CAAA;IAE/B,MAAM,CAAC,EAAE,OAAO,CAAA;IAEhB,YAAY,EAAE,CAAC,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,KAAK,SAAS,CAAA;IAGhE,aAAa,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,OAAO,KAAK,SAAS,CAAA;IAG7C,eAAe,CAAC,EAAE,CAChB,IAAI,EAAE,OAAO,EACb,QAAQ,EAAE,qBAAqB,KAC5B,SAAS,CAAA;IAGd,cAAc,CAAC,EAAE,CACf,QAAQ,EAAE,gBAAgB,EAC1B,SAAS,EAAE,uBAAuB,KAC/B,SAAS,CAAA;CACf,CAAA;AAoSD;;GAEG;AACH,eAAO,MAAM,qBAAqB,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAqvE7D,CAAA;AAoBD;;GAEG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,WAAW,CAmC5D"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { AlertTriangle, BookOpen, Check, CheckCheck, Copy, Database, Download, FileCode, FileText, FolderOpen, Globe, Info, Package, Plus, Search, Sparkles, Terminal, Trash2, X, } from 'lucide-react';
|
|
3
|
+
import { AlertTriangle, BookOpen, Camera, Check, CheckCheck, Copy, Database, Download, FileCode, FileText, FolderOpen, Globe, Info, Package, Plus, Search, Sparkles, Terminal, Trash2, X, } from 'lucide-react';
|
|
4
4
|
import { useState } from 'react';
|
|
5
5
|
import { cn } from '../../lib/utils';
|
|
6
6
|
import { ThinkingTimelineMarker } from './thinking-timeline';
|
|
@@ -108,6 +108,94 @@ function appApiItemCount(result) {
|
|
|
108
108
|
* Default tool handlers for Moldable tools
|
|
109
109
|
*/
|
|
110
110
|
export const DEFAULT_TOOL_HANDLERS = {
|
|
111
|
+
viewImage: {
|
|
112
|
+
loadingLabel: 'Viewing image...',
|
|
113
|
+
marker: ThinkingTimelineMarker.Default,
|
|
114
|
+
inline: false,
|
|
115
|
+
renderLoading: (args) => {
|
|
116
|
+
const { imagePath } = (args ?? {});
|
|
117
|
+
return (_jsx(LoadingIndicator, { icon: Sparkles, children: imagePath
|
|
118
|
+
? `Viewing ${getFileName(imagePath)}...`
|
|
119
|
+
: 'Viewing image...' }));
|
|
120
|
+
},
|
|
121
|
+
renderOutput: (output, toolCallId) => {
|
|
122
|
+
const result = output;
|
|
123
|
+
if (!result) {
|
|
124
|
+
return (_jsx(LoadingIndicator, { icon: Sparkles, children: "Viewing image..." }, toolCallId));
|
|
125
|
+
}
|
|
126
|
+
return (_jsxs("div", { className: "bg-muted text-muted-foreground my-1 inline-flex max-w-full items-center gap-2 rounded-md px-2 py-1 text-xs", children: [_jsx(Sparkles, { className: "size-3.5 shrink-0" }), _jsx("span", { className: "min-w-0 truncate", children: result.imagePath
|
|
127
|
+
? `Viewed ${getFileName(result.imagePath)}`
|
|
128
|
+
: 'Viewed image' })] }, toolCallId));
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
generateImage: {
|
|
132
|
+
loadingLabel: 'Generating image...',
|
|
133
|
+
marker: ThinkingTimelineMarker.Default,
|
|
134
|
+
inline: true,
|
|
135
|
+
renderLoading: (args) => {
|
|
136
|
+
const { prompt, aspectRatio } = (args ?? {});
|
|
137
|
+
const ratioClass = aspectRatio === 'portrait'
|
|
138
|
+
? 'aspect-[3/4]'
|
|
139
|
+
: aspectRatio === 'story'
|
|
140
|
+
? 'aspect-[9/16]'
|
|
141
|
+
: aspectRatio === 'landscape'
|
|
142
|
+
? 'aspect-[4/3]'
|
|
143
|
+
: aspectRatio === 'widescreen'
|
|
144
|
+
? 'aspect-video'
|
|
145
|
+
: 'aspect-square';
|
|
146
|
+
return (_jsx("figure", { className: "my-2 min-w-0 max-w-2xl", children: _jsxs("div", { className: cn('border-border bg-muted relative w-full overflow-hidden rounded-lg border', ratioClass), children: [_jsx("div", { className: "from-muted via-background/45 to-muted absolute inset-0 animate-pulse bg-gradient-to-br" }), _jsx("div", { className: "bg-foreground/5 absolute inset-0" }), _jsxs("div", { className: "text-muted-foreground absolute inset-0 flex flex-col items-center justify-center gap-2 px-6 text-center", children: [_jsx(Sparkles, { className: "size-5 animate-pulse" }), _jsx("span", { className: "max-w-full truncate text-xs font-medium", children: prompt
|
|
147
|
+
? `Generating "${prompt.slice(0, 54)}${prompt.length > 54 ? '...' : ''}"`
|
|
148
|
+
: 'Generating image...' })] })] }) }));
|
|
149
|
+
},
|
|
150
|
+
renderOutput: (output, toolCallId) => {
|
|
151
|
+
const result = output;
|
|
152
|
+
if (!result) {
|
|
153
|
+
return (_jsx(LoadingIndicator, { icon: Sparkles, children: "Generating image..." }, toolCallId));
|
|
154
|
+
}
|
|
155
|
+
const parts = Array.isArray(result.value) ? result.value : [];
|
|
156
|
+
const image = parts.find((part) => (part.type === 'image-data' || part.type === 'media') &&
|
|
157
|
+
part.data &&
|
|
158
|
+
part.mediaType?.startsWith('image/'));
|
|
159
|
+
const imageSrc = result.imageUrl ??
|
|
160
|
+
(image?.data && image.mediaType
|
|
161
|
+
? `data:${image.mediaType};base64,${image.data}`
|
|
162
|
+
: undefined);
|
|
163
|
+
if (!imageSrc) {
|
|
164
|
+
return (_jsxs("div", { className: "bg-muted text-muted-foreground my-1 flex max-w-full items-start gap-2 rounded-md px-2 py-1.5 text-xs", children: [_jsx(Sparkles, { className: "mt-0.5 size-3.5 shrink-0" }), _jsx("span", { className: "min-w-0 whitespace-normal break-words leading-relaxed", children: result.imagePath
|
|
165
|
+
? `Generated image saved at ${result.imagePath}.`
|
|
166
|
+
: 'Generated image saved.' })] }, toolCallId));
|
|
167
|
+
}
|
|
168
|
+
return (_jsxs("figure", { className: "my-2 min-w-0 max-w-2xl", children: [_jsx("div", { className: "border-border bg-muted overflow-hidden rounded-lg border", children: _jsx("img", { src: imageSrc, alt: result.prompt || 'Generated image', className: "block max-h-[420px] w-full object-contain" }) }), _jsxs("figcaption", { className: "text-muted-foreground mt-1 flex min-w-0 items-center gap-1 text-xs", children: [_jsx(Sparkles, { className: "size-3 shrink-0" }), _jsx("span", { className: "truncate", children: result.imagePath
|
|
169
|
+
? `${getFileName(result.imagePath)}${result.size ? ` - ${result.size}` : ''}`
|
|
170
|
+
: result.size || 'Generated image' })] })] }, toolCallId));
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
take_screenshot: {
|
|
174
|
+
loadingLabel: 'Taking screenshot...',
|
|
175
|
+
marker: ThinkingTimelineMarker.Default,
|
|
176
|
+
inline: true,
|
|
177
|
+
renderLoading: () => (_jsx(LoadingIndicator, { icon: Camera, children: "Taking screenshot..." })),
|
|
178
|
+
renderOutput: (output, toolCallId) => {
|
|
179
|
+
const result = output;
|
|
180
|
+
if (!result) {
|
|
181
|
+
return (_jsx(LoadingIndicator, { icon: Camera, children: "Taking screenshot..." }, toolCallId));
|
|
182
|
+
}
|
|
183
|
+
if (result.type === 'error-text') {
|
|
184
|
+
return (_jsxs("div", { className: "bg-destructive/10 text-destructive my-1 flex max-w-full items-start gap-2 rounded-md px-2 py-1.5 text-xs", children: [_jsx(Camera, { className: "mt-0.5 size-3.5 shrink-0" }), _jsx("span", { className: "min-w-0 whitespace-normal break-words leading-relaxed", children: typeof result.value === 'string'
|
|
185
|
+
? result.value
|
|
186
|
+
: 'Screenshot failed' })] }, toolCallId));
|
|
187
|
+
}
|
|
188
|
+
const parts = Array.isArray(result.value) ? result.value : [];
|
|
189
|
+
const image = parts.find((part) => (part.type === 'image-data' || part.type === 'media') &&
|
|
190
|
+
part.data &&
|
|
191
|
+
part.mediaType?.startsWith('image/'));
|
|
192
|
+
const text = parts.find((part) => part.type === 'text')?.text;
|
|
193
|
+
if (!image?.data || !image.mediaType) {
|
|
194
|
+
return (_jsxs("div", { className: "bg-muted text-muted-foreground my-1 inline-flex max-w-full items-center gap-2 rounded-md px-2 py-1 text-xs", children: [_jsx(Camera, { className: "size-3.5 shrink-0" }), _jsx("span", { className: "truncate", children: text || 'Screenshot captured' }), _jsx(Check, { className: "size-3 shrink-0 text-green-600" })] }, toolCallId));
|
|
195
|
+
}
|
|
196
|
+
return (_jsxs("figure", { className: "my-2 min-w-0 max-w-2xl", children: [_jsx("div", { className: "border-border bg-muted overflow-hidden rounded-lg border", children: _jsx("img", { src: `data:${image.mediaType};base64,${image.data}`, alt: "Current app screenshot", className: "block max-h-[420px] w-full object-contain" }) }), text && (_jsxs("figcaption", { className: "text-muted-foreground mt-1 flex items-center gap-1 text-xs", children: [_jsx(Camera, { className: "size-3" }), _jsx("span", { className: "truncate", children: text })] }))] }, toolCallId));
|
|
197
|
+
},
|
|
198
|
+
},
|
|
111
199
|
listMoldableAppApi: {
|
|
112
200
|
loadingLabel: 'Listing app APIs...',
|
|
113
201
|
marker: ThinkingTimelineMarker.Default,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"markdown.d.ts","sourceRoot":"","sources":["../../src/components/markdown.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAyB,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"markdown.d.ts","sourceRoot":"","sources":["../../src/components/markdown.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAyB,MAAM,OAAO,CAAA;AAiF7C,KAAK,mBAAmB,GAAG;IACzB,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,SAAS,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAAA;IACvC,mBAAmB,CAAC,EAAE,MAAM,CAAA;CAC7B,CAAA;AA8TD,eAAO,MAAM,QAAQ,uFA5DlB,mBAAmB,6CAmErB,CAAA"}
|
|
@@ -4,6 +4,7 @@ import React, { memo, useState } from 'react';
|
|
|
4
4
|
import ReactMarkdown from 'react-markdown';
|
|
5
5
|
import { cn } from '../lib/utils';
|
|
6
6
|
import { CodeBlock } from './code-block';
|
|
7
|
+
import { RichMediaPlayer } from './rich-media-player';
|
|
7
8
|
import { Button } from './ui/button';
|
|
8
9
|
import { Checkbox } from './ui/checkbox';
|
|
9
10
|
import rehypeRaw from 'rehype-raw';
|
|
@@ -62,10 +63,23 @@ function processMentions(text) {
|
|
|
62
63
|
return parts.length > 0 ? parts : [text];
|
|
63
64
|
}
|
|
64
65
|
const components = {
|
|
65
|
-
img({ src, alt }) {
|
|
66
|
+
img({ src, alt, className, style, width }) {
|
|
66
67
|
if (!src || typeof src !== 'string')
|
|
67
68
|
return null;
|
|
68
|
-
|
|
69
|
+
const imageWidth = typeof width === 'number' || typeof width === 'string'
|
|
70
|
+
? width
|
|
71
|
+
: undefined;
|
|
72
|
+
return (_jsx("span", { className: "flex justify-center", children: _jsx("img", { src: src, alt: alt ?? '', className: cn('h-auto max-w-full rounded-lg', className), style: style, width: imageWidth }) }));
|
|
73
|
+
},
|
|
74
|
+
audio({ src, title }) {
|
|
75
|
+
if (!src || typeof src !== 'string')
|
|
76
|
+
return null;
|
|
77
|
+
return (_jsx(RichMediaPlayer, { kind: "audio", src: src, title: typeof title === 'string' ? title : undefined }));
|
|
78
|
+
},
|
|
79
|
+
video({ src, title }) {
|
|
80
|
+
if (!src || typeof src !== 'string')
|
|
81
|
+
return null;
|
|
82
|
+
return (_jsx(RichMediaPlayer, { kind: "video", src: src, title: typeof title === 'string' ? title : undefined }));
|
|
69
83
|
},
|
|
70
84
|
table({ children }) {
|
|
71
85
|
return (_jsx("table", { className: "border-collapse border px-3 py-1", children: children }));
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type RichMediaPlayerProps = {
|
|
2
|
+
className?: string;
|
|
3
|
+
kind: 'audio' | 'video';
|
|
4
|
+
src: string;
|
|
5
|
+
title?: string;
|
|
6
|
+
};
|
|
7
|
+
export declare function RichMediaPlayer({ className, kind, src, title, }: RichMediaPlayerProps): import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
//# sourceMappingURL=rich-media-player.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rich-media-player.d.ts","sourceRoot":"","sources":["../../src/components/rich-media-player.tsx"],"names":[],"mappings":"AAaA,MAAM,MAAM,oBAAoB,GAAG;IACjC,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,IAAI,EAAE,OAAO,GAAG,OAAO,CAAA;IACvB,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,CAAA;AAuBD,wBAAgB,eAAe,CAAC,EAC9B,SAAS,EACT,IAAI,EACJ,GAAG,EACH,KAAK,GACN,EAAE,oBAAoB,2CAmNtB"}
|