@moldable-ai/ui 0.2.5 → 0.2.7
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 +75 -71
- package/dist/components/chat/chat-input.d.ts +15 -3
- package/dist/components/chat/chat-input.d.ts.map +1 -1
- package/dist/components/chat/chat-input.js +19 -8
- package/dist/components/chat/chat-message.d.ts +6 -0
- package/dist/components/chat/chat-message.d.ts.map +1 -1
- package/dist/components/chat/chat-message.js +20 -5
- package/dist/components/chat/chat-messages.d.ts +26 -1
- package/dist/components/chat/chat-messages.d.ts.map +1 -1
- package/dist/components/chat/chat-messages.js +33 -22
- package/dist/components/chat/chat-panel.d.ts +40 -3
- package/dist/components/chat/chat-panel.d.ts.map +1 -1
- package/dist/components/chat/chat-panel.js +173 -36
- package/dist/components/chat/checkpoint-badge.d.ts +20 -0
- package/dist/components/chat/checkpoint-badge.d.ts.map +1 -0
- package/dist/components/chat/checkpoint-badge.js +16 -0
- package/dist/components/chat/conversation-history.d.ts +6 -1
- package/dist/components/chat/conversation-history.d.ts.map +1 -1
- package/dist/components/chat/conversation-history.js +6 -6
- package/dist/components/chat/index.d.ts +5 -3
- package/dist/components/chat/index.d.ts.map +1 -1
- package/dist/components/chat/index.js +3 -1
- package/dist/components/chat/markdown.d.ts +9 -0
- package/dist/components/chat/markdown.d.ts.map +1 -0
- package/dist/components/chat/markdown.js +47 -0
- package/dist/components/chat/model-selector.d.ts +3 -1
- package/dist/components/chat/model-selector.d.ts.map +1 -1
- package/dist/components/chat/model-selector.js +2 -2
- package/dist/components/chat/reasoning-effort-selector.d.ts +4 -1
- package/dist/components/chat/reasoning-effort-selector.d.ts.map +1 -1
- package/dist/components/chat/reasoning-effort-selector.js +6 -6
- package/dist/components/chat/restore-dialog.d.ts +33 -0
- package/dist/components/chat/restore-dialog.d.ts.map +1 -0
- package/dist/components/chat/restore-dialog.js +23 -0
- package/dist/components/chat/tool-handlers.d.ts.map +1 -1
- package/dist/components/chat/tool-handlers.js +194 -2
- package/dist/components/ui/alert-dialog.d.ts +8 -4
- package/dist/components/ui/alert-dialog.d.ts.map +1 -1
- package/dist/components/ui/alert-dialog.js +14 -11
- package/dist/components/ui/avatar.d.ts +7 -2
- package/dist/components/ui/avatar.d.ts.map +1 -1
- package/dist/components/ui/avatar.js +13 -4
- package/dist/components/ui/badge.d.ts +1 -1
- package/dist/components/ui/badge.d.ts.map +1 -1
- package/dist/components/ui/badge.js +9 -7
- package/dist/components/ui/button.d.ts +3 -4
- package/dist/components/ui/button.d.ts.map +1 -1
- package/dist/components/ui/button.js +6 -6
- package/dist/components/ui/card.d.ts +8 -7
- package/dist/components/ui/card.d.ts.map +1 -1
- package/dist/components/ui/card.js +22 -14
- package/dist/components/ui/checkbox.js +1 -1
- package/dist/components/ui/collapsible.d.ts +3 -3
- package/dist/components/ui/collapsible.d.ts.map +1 -1
- package/dist/components/ui/collapsible.js +11 -3
- package/dist/components/ui/combobox.d.ts +25 -0
- package/dist/components/ui/combobox.d.ts.map +1 -0
- package/dist/components/ui/combobox.js +58 -0
- package/dist/components/ui/command.d.ts +15 -15
- package/dist/components/ui/command.d.ts.map +1 -1
- package/dist/components/ui/command.js +29 -24
- package/dist/components/ui/dialog.d.ts +15 -17
- package/dist/components/ui/dialog.d.ts.map +1 -1
- package/dist/components/ui/dialog.js +33 -19
- package/dist/components/ui/dropdown-menu.js +6 -6
- package/dist/components/ui/input-group.d.ts +2 -2
- package/dist/components/ui/input.d.ts +1 -2
- package/dist/components/ui/input.d.ts.map +1 -1
- package/dist/components/ui/input.js +4 -5
- package/dist/components/ui/item.d.ts +1 -1
- package/dist/components/ui/label.d.ts +1 -1
- package/dist/components/ui/label.d.ts.map +1 -1
- package/dist/components/ui/label.js +3 -3
- package/dist/components/ui/popover.d.ts +4 -1
- package/dist/components/ui/popover.d.ts.map +1 -1
- package/dist/components/ui/popover.js +10 -1
- package/dist/components/ui/scroll-area.d.ts +1 -1
- package/dist/components/ui/scroll-area.d.ts.map +1 -1
- package/dist/components/ui/scroll-area.js +9 -5
- package/dist/components/ui/select.d.ts +13 -11
- package/dist/components/ui/select.d.ts.map +1 -1
- package/dist/components/ui/select.js +35 -23
- package/dist/components/ui/sheet.d.ts +2 -1
- package/dist/components/ui/sheet.d.ts.map +1 -1
- package/dist/components/ui/sheet.js +2 -2
- package/dist/components/ui/switch.d.ts +4 -2
- package/dist/components/ui/switch.d.ts.map +1 -1
- package/dist/components/ui/switch.js +4 -4
- package/dist/components/ui/tabs.d.ts +7 -3
- package/dist/components/ui/tabs.d.ts.map +1 -1
- package/dist/components/ui/tabs.js +18 -6
- package/dist/components/ui/textarea.d.ts +1 -2
- package/dist/components/ui/textarea.d.ts.map +1 -1
- package/dist/components/ui/textarea.js +4 -5
- package/dist/components/ui/tooltip.js +1 -1
- package/dist/lib/commands.d.ts +49 -0
- package/dist/lib/commands.d.ts.map +1 -1
- package/dist/lib/commands.js +56 -0
- package/package.json +10 -7
- package/src/styles/index.css +7 -0
|
@@ -1,35 +1,55 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { jsx as _jsx,
|
|
3
|
-
import { AlertCircle, ChevronDown, MessageCircle,
|
|
4
|
-
import { useCallback, useEffect, useRef, useState, } from 'react';
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { AlertCircle, ChevronDown, Code2, CornerDownRight, Image as ImageIcon, ListPlus, MessageCircle, X, } from 'lucide-react';
|
|
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
|
-
import { ChatInput } from './chat-input';
|
|
10
|
-
import {
|
|
9
|
+
import { ChatInput, } from './chat-input';
|
|
10
|
+
import { ThinkingMessage } from './chat-message';
|
|
11
|
+
import { MessageRow, computeIsThinking, } from './chat-messages';
|
|
11
12
|
import { ConversationHistory, } from './conversation-history';
|
|
12
13
|
import { ModelSelector } from './model-selector';
|
|
13
14
|
import { ReasoningEffortSelector, } from './reasoning-effort-selector';
|
|
14
15
|
import { ToolApprovalProvider, } from './tool-approval-context';
|
|
15
16
|
import { ToolProgressProvider } from './tool-progress-context';
|
|
16
17
|
import { AnimatePresence, motion } from 'framer-motion';
|
|
18
|
+
import { Virtualizer } from 'virtua';
|
|
17
19
|
/**
|
|
18
20
|
* Floating chat panel with model selector
|
|
19
21
|
*/
|
|
20
|
-
export function ChatPanel({ messages, status, 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, }) {
|
|
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, }) {
|
|
21
23
|
const inputRef = useRef(null);
|
|
22
|
-
const messagesEndRef = useRef(null);
|
|
23
24
|
const scrollAreaRef = useRef(null);
|
|
25
|
+
const viewportRef = useRef(null);
|
|
26
|
+
const virtualizerRef = useRef(null);
|
|
27
|
+
const prevMessageCountRef = useRef(messages.length);
|
|
28
|
+
const scrollRafRef = useRef(null);
|
|
29
|
+
const [viewportVersion, setViewportVersion] = useState(0);
|
|
24
30
|
const [isAtBottom, setIsAtBottom] = useState(true);
|
|
25
31
|
const isResponding = status === 'streaming' || status === 'submitted';
|
|
26
|
-
|
|
32
|
+
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 &&
|
|
34
|
+
selectedReasoningEffort &&
|
|
35
|
+
onReasoningEffortChange && (_jsx(ReasoningEffortSelector, { options: reasoningEffortOptions, selectedEffort: selectedReasoningEffort, onEffortChange: onReasoningEffortChange, disabled: controlsDisabled, showLabel: true, dropdownAlign: "end", dropdownSide: "top" }))] }));
|
|
36
|
+
// Capture the Radix viewport element so virtua can use it as the scroll container
|
|
27
37
|
useEffect(() => {
|
|
28
38
|
const scrollArea = scrollAreaRef.current;
|
|
29
39
|
if (!scrollArea)
|
|
30
40
|
return;
|
|
31
|
-
// Radix ScrollArea puts the viewport as first child element
|
|
32
41
|
const viewport = scrollArea.querySelector('[data-radix-scroll-area-viewport]');
|
|
42
|
+
if (!viewport)
|
|
43
|
+
return;
|
|
44
|
+
viewport.style.overflowAnchor = 'none';
|
|
45
|
+
if (viewportRef.current !== viewport) {
|
|
46
|
+
viewportRef.current = viewport;
|
|
47
|
+
setViewportVersion((version) => version + 1);
|
|
48
|
+
}
|
|
49
|
+
}, [isExpanded]);
|
|
50
|
+
// Track scroll position to detect if user is at bottom
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
const viewport = viewportRef.current;
|
|
33
53
|
if (!viewport)
|
|
34
54
|
return;
|
|
35
55
|
const handleScroll = () => {
|
|
@@ -38,9 +58,10 @@ export function ChatPanel({ messages, status, input, onInputChange, onSubmit, on
|
|
|
38
58
|
const atBottom = scrollHeight - scrollTop - clientHeight < 50;
|
|
39
59
|
setIsAtBottom(atBottom);
|
|
40
60
|
};
|
|
61
|
+
handleScroll();
|
|
41
62
|
viewport.addEventListener('scroll', handleScroll);
|
|
42
63
|
return () => viewport.removeEventListener('scroll', handleScroll);
|
|
43
|
-
}, [
|
|
64
|
+
}, [viewportVersion]);
|
|
44
65
|
// Focus input when expanded
|
|
45
66
|
useEffect(() => {
|
|
46
67
|
if (isExpanded) {
|
|
@@ -50,18 +71,13 @@ export function ChatPanel({ messages, status, input, onInputChange, onSubmit, on
|
|
|
50
71
|
return () => clearTimeout(timer);
|
|
51
72
|
}
|
|
52
73
|
}, [isExpanded]);
|
|
53
|
-
|
|
54
|
-
useEffect(() => {
|
|
55
|
-
if (isExpanded && messagesEndRef.current && isAtBottom) {
|
|
56
|
-
messagesEndRef.current.scrollIntoView({ behavior: 'smooth' });
|
|
57
|
-
}
|
|
58
|
-
}, [messages, isExpanded, isAtBottom]);
|
|
59
|
-
const handleSubmit = useCallback((e) => {
|
|
74
|
+
const handleSubmit = useCallback((e, mode) => {
|
|
60
75
|
e?.preventDefault();
|
|
61
|
-
if (!input.trim())
|
|
76
|
+
if (!input.trim() && imageAttachments.length === 0)
|
|
62
77
|
return;
|
|
63
|
-
|
|
64
|
-
|
|
78
|
+
const submitMode = mode ?? (isResponding ? 'steer' : 'send');
|
|
79
|
+
// Steer interrupts the current response and sends the new instruction now.
|
|
80
|
+
if (submitMode === 'steer' && isResponding && onStop) {
|
|
65
81
|
onStop();
|
|
66
82
|
}
|
|
67
83
|
if (!isExpanded) {
|
|
@@ -69,26 +85,44 @@ export function ChatPanel({ messages, status, input, onInputChange, onSubmit, on
|
|
|
69
85
|
}
|
|
70
86
|
// Reset to bottom when user sends a message
|
|
71
87
|
setIsAtBottom(true);
|
|
72
|
-
onSubmit(e);
|
|
73
|
-
}, [
|
|
88
|
+
onSubmit(e, submitMode);
|
|
89
|
+
}, [
|
|
90
|
+
imageAttachments.length,
|
|
91
|
+
input,
|
|
92
|
+
isResponding,
|
|
93
|
+
isExpanded,
|
|
94
|
+
onExpandedChange,
|
|
95
|
+
onSubmit,
|
|
96
|
+
onStop,
|
|
97
|
+
]);
|
|
74
98
|
const handleNewChat = useCallback(() => {
|
|
75
99
|
onNewChat?.();
|
|
76
100
|
inputRef.current?.focus();
|
|
77
101
|
}, [onNewChat]);
|
|
102
|
+
const composerStartControls = (_jsxs(_Fragment, { children: [conversations &&
|
|
103
|
+
(conversations.length > 0 || onNewChat) &&
|
|
104
|
+
onSelectConversation && (_jsx(ConversationHistory, { conversations: conversations, currentConversationId: currentConversationId, onSelect: onSelectConversation, onDelete: onDeleteConversation, onNewChat: handleNewChat, newChatDisabled: controlsDisabled, disabled: controlsDisabled, dropdownAlign: "start", dropdownSide: "top", tooltipSide: "top" })), showEditingAppToggle && onEditingAppChange && (_jsx(TooltipProvider, { children: _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx(Button, { type: "button", variant: "ghost", size: "icon-sm", onClick: (event) => {
|
|
105
|
+
event.preventDefault();
|
|
106
|
+
onEditingAppChange(!isEditingApp);
|
|
107
|
+
}, className: cn('cursor-pointer', isEditingApp
|
|
108
|
+
? 'text-primary bg-primary/10 hover:bg-primary/20'
|
|
109
|
+
: 'text-muted-foreground hover:text-foreground'), children: _jsx(Code2, { className: "size-4" }) }) }), _jsx(TooltipContent, { side: "top", children: _jsx("p", { children: isEditingApp
|
|
110
|
+
? 'Editing this app (click to disable)'
|
|
111
|
+
: 'Not editing app (click to enable)' }) })] }) }))] }));
|
|
78
112
|
// Keyboard shortcut: Cmd+Shift+O for new chat
|
|
79
113
|
useEffect(() => {
|
|
80
114
|
const handleKeyDown = (e) => {
|
|
81
115
|
if (e.key === 'o' && e.metaKey && e.shiftKey) {
|
|
82
116
|
e.preventDefault();
|
|
83
|
-
// Only create new chat if
|
|
84
|
-
if (!
|
|
117
|
+
// Only create new chat if conversation controls are currently available.
|
|
118
|
+
if (!controlsDisabled && isExpanded) {
|
|
85
119
|
handleNewChat();
|
|
86
120
|
}
|
|
87
121
|
}
|
|
88
122
|
};
|
|
89
123
|
document.addEventListener('keydown', handleKeyDown);
|
|
90
124
|
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
91
|
-
}, [
|
|
125
|
+
}, [controlsDisabled, isExpanded, handleNewChat]);
|
|
92
126
|
// Handle clicking the minimized flap
|
|
93
127
|
const handleFlapClick = useCallback(() => {
|
|
94
128
|
onMinimizedChange?.(false);
|
|
@@ -100,11 +134,110 @@ export function ChatPanel({ messages, status, input, onInputChange, onSubmit, on
|
|
|
100
134
|
}
|
|
101
135
|
onMinimizedChange?.(true);
|
|
102
136
|
}, [isExpanded, onExpandedChange, onMinimizedChange]);
|
|
137
|
+
const isStreaming = status === 'streaming' || status === 'submitted';
|
|
138
|
+
const isThinking = useMemo(() => computeIsThinking(messages, status), [messages, status]);
|
|
139
|
+
const showMissingApiKeyPrompt = missingApiKey ||
|
|
140
|
+
(error &&
|
|
141
|
+
(error.message?.includes('API_KEY not configured') ||
|
|
142
|
+
error.message?.includes('credential required in Vault') ||
|
|
143
|
+
error.message?.includes('credential not configured in Vault')));
|
|
144
|
+
const showErrorMessage = Boolean(error) && status === 'error' && !showMissingApiKeyPrompt;
|
|
145
|
+
const lastMessageId = messages[messages.length - 1]?.id;
|
|
146
|
+
const listItems = useMemo(() => {
|
|
147
|
+
const items = [];
|
|
148
|
+
if (messages.length === 0 && welcomeMessage) {
|
|
149
|
+
items.push({ key: 'welcome', kind: 'welcome' });
|
|
150
|
+
}
|
|
151
|
+
for (const message of messages) {
|
|
152
|
+
items.push({ key: message.id, kind: 'message', message });
|
|
153
|
+
}
|
|
154
|
+
if (isThinking) {
|
|
155
|
+
items.push({ key: 'thinking', kind: 'thinking' });
|
|
156
|
+
}
|
|
157
|
+
if (showMissingApiKeyPrompt) {
|
|
158
|
+
items.push({ key: 'missing-api-key', kind: 'missing-api-key' });
|
|
159
|
+
}
|
|
160
|
+
if (showErrorMessage) {
|
|
161
|
+
items.push({ key: 'error', kind: 'error' });
|
|
162
|
+
}
|
|
163
|
+
return items;
|
|
164
|
+
}, [
|
|
165
|
+
isThinking,
|
|
166
|
+
messages,
|
|
167
|
+
showErrorMessage,
|
|
168
|
+
showMissingApiKeyPrompt,
|
|
169
|
+
welcomeMessage,
|
|
170
|
+
]);
|
|
171
|
+
const renderVirtualItem = useCallback((item) => {
|
|
172
|
+
switch (item.kind) {
|
|
173
|
+
case 'welcome':
|
|
174
|
+
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 })) }));
|
|
175
|
+
case 'message': {
|
|
176
|
+
const checkpoint = checkpoints?.get(item.message.id);
|
|
177
|
+
return (_jsx("div", { className: "px-2 pb-4", children: _jsx(MessageRow, { message: item.message, isLast: item.message.id === lastMessageId, isStreaming: isStreaming, checkpoint: checkpoint, restoringMessageId: restoringMessageId, onRestoreCheckpoint: onRestoreCheckpoint }) }));
|
|
178
|
+
}
|
|
179
|
+
case 'thinking':
|
|
180
|
+
return (_jsx("div", { className: "px-2 pb-4", children: _jsx(ThinkingMessage, {}) }));
|
|
181
|
+
case 'missing-api-key':
|
|
182
|
+
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" }))] }) }));
|
|
183
|
+
case 'error':
|
|
184
|
+
return (_jsx("div", { className: "px-2 pb-4", children: error && (_jsxs("div", { className: "border-destructive/30 bg-destructive/10 text-destructive mx-2 flex items-start gap-2 rounded-lg border p-3 text-sm", children: [_jsx(AlertCircle, { className: "mt-0.5 size-4 shrink-0" }), _jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "font-medium", children: "Request failed" }), _jsx("p", { className: "text-destructive/80 mt-0.5 break-words", children: error.message || 'An unknown error occurred' })] })] })) }));
|
|
185
|
+
}
|
|
186
|
+
}, [
|
|
187
|
+
checkpoints,
|
|
188
|
+
error,
|
|
189
|
+
isStreaming,
|
|
190
|
+
lastMessageId,
|
|
191
|
+
messages.length,
|
|
192
|
+
onAddApiKey,
|
|
193
|
+
onRestoreCheckpoint,
|
|
194
|
+
restoringMessageId,
|
|
195
|
+
welcomeMessage,
|
|
196
|
+
]);
|
|
197
|
+
// Scroll to bottom on new messages if the user is already at bottom
|
|
198
|
+
useEffect(() => {
|
|
199
|
+
const previousMessageCount = prevMessageCountRef.current;
|
|
200
|
+
const hasNewMessage = messages.length > previousMessageCount;
|
|
201
|
+
prevMessageCountRef.current = messages.length;
|
|
202
|
+
if (!isExpanded || !isAtBottom) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const lastIndex = listItems.length - 1;
|
|
206
|
+
if (lastIndex < 0)
|
|
207
|
+
return;
|
|
208
|
+
if (scrollRafRef.current !== null) {
|
|
209
|
+
cancelAnimationFrame(scrollRafRef.current);
|
|
210
|
+
}
|
|
211
|
+
scrollRafRef.current = requestAnimationFrame(() => {
|
|
212
|
+
virtualizerRef.current?.scrollToIndex(lastIndex, {
|
|
213
|
+
align: 'end',
|
|
214
|
+
smooth: hasNewMessage,
|
|
215
|
+
});
|
|
216
|
+
scrollRafRef.current = null;
|
|
217
|
+
});
|
|
218
|
+
return () => {
|
|
219
|
+
if (scrollRafRef.current !== null) {
|
|
220
|
+
cancelAnimationFrame(scrollRafRef.current);
|
|
221
|
+
scrollRafRef.current = null;
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
}, [isAtBottom, isExpanded, listItems.length, messages.length]);
|
|
103
225
|
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: {
|
|
104
226
|
width: isExpanded ? 'min(90vw, 640px)' : '400px',
|
|
105
227
|
y: isMinimized ? 'calc(100% + 24px)' : 0,
|
|
106
228
|
opacity: isMinimized ? 0 : 1,
|
|
107
|
-
}, transition: { type: 'tween', duration: 0.25, ease: 'easeOut' }, className: cn('pointer-events-none fixed bottom-6 left-[calc(50%+var(--sidebar-width,0px)/2)] z-50 flex -translate-x-1/2 justify-center', isMinimized && 'pointer-events-none', className), children: _jsxs("div", { className: cn('bg-background pointer-events-auto relative w-full
|
|
229
|
+
}, transition: { type: 'tween', duration: 0.25, ease: 'easeOut' }, className: cn('pointer-events-none fixed bottom-6 left-[calc(50%+var(--sidebar-width,0px)/2)] z-50 flex -translate-x-1/2 justify-center', isMinimized && 'pointer-events-none', className), children: _jsxs("div", { "data-chat-drop-target": true, className: cn('bg-background/80 pointer-events-auto relative w-full shadow-[0_8px_40px_-12px_rgba(0,0,0,0.15)]', isExpanded
|
|
230
|
+
? 'bg-background'
|
|
231
|
+
: 'bg-background/80 supports-[backdrop-filter]:backdrop-blur-xl', 'rounded-[28px]', isExpanded ? 'overflow-hidden' : 'overflow-visible'), children: [!isExpanded && (_jsx("div", { className: "pointer-events-none absolute inset-0 z-0 rounded-[inherit]", style: {
|
|
232
|
+
background: 'linear-gradient(134deg, rgba(255,255,255,0.08) 0%, rgba(255,255,255,0.02) 50%, transparent 55%)',
|
|
233
|
+
} })), isImageDropActive && (_jsxs("div", { className: "bg-background/90 border-primary/50 text-foreground absolute inset-0 z-[70] flex flex-col items-center justify-center gap-2 rounded-[inherit] border-2 border-dashed backdrop-blur-sm", children: [_jsx(ImageIcon, { className: "text-primary size-7" }), _jsxs("div", { className: "text-center", children: [_jsx("p", { className: "text-sm font-medium", children: "Drop images to attach" }), _jsx("p", { className: "text-muted-foreground text-xs", children: "They stay on disk and are read when sent" })] })] })), _jsx("div", { className: "pointer-events-none absolute inset-0 z-50 rounded-[inherit]", style: {
|
|
234
|
+
padding: '1px',
|
|
235
|
+
background: 'linear-gradient(135deg, rgba(255,255,255,0.25) 0%, rgba(255,255,255,0.1) 20%, rgba(255,255,255,0.05) 45%, rgba(255,255,255,0.05) 55%, rgba(255,255,255,0.1) 80%, rgba(255,255,255,0.25) 100%)',
|
|
236
|
+
mask: 'linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)',
|
|
237
|
+
WebkitMask: 'linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)',
|
|
238
|
+
maskComposite: 'exclude',
|
|
239
|
+
WebkitMaskComposite: 'xor',
|
|
240
|
+
} }), !isExpanded && (_jsx(motion.div, { className: "rounded-4xl pointer-events-none absolute inset-0 z-0", animate: {
|
|
108
241
|
boxShadow: [
|
|
109
242
|
'0 0 0px 0px color-mix(in oklch, var(--primary) 0%, #ff8800 00%)',
|
|
110
243
|
'0 0 20px 0px color-mix(in oklch, var(--primary) 20%, #ff8800 30%)',
|
|
@@ -118,17 +251,21 @@ export function ChatPanel({ messages, status, input, onInputChange, onSubmit, on
|
|
|
118
251
|
} })), _jsxs(motion.div, { initial: false, animate: {
|
|
119
252
|
height: isExpanded ? 'min(60vh, 480px)' : '0px',
|
|
120
253
|
opacity: isExpanded ? 1 : 0,
|
|
121
|
-
}, transition: { type: 'tween', duration: 0.2, ease: 'easeOut' }, className: "relative z-10 flex flex-col overflow-hidden", style: { pointerEvents: isExpanded ? 'auto' : 'none' }, children: [
|
|
122
|
-
selectedReasoningEffort &&
|
|
123
|
-
onReasoningEffortChange && (_jsx(ReasoningEffortSelector, { options: reasoningEffortOptions, selectedEffort: selectedReasoningEffort, onEffortChange: onReasoningEffortChange, disabled: isResponding }))] }), _jsxs("div", { className: "flex items-center gap-1", children: [conversations &&
|
|
124
|
-
conversations.length > 0 &&
|
|
125
|
-
onSelectConversation && (_jsx(ConversationHistory, { conversations: conversations, currentConversationId: currentConversationId, onSelect: onSelectConversation, onDelete: onDeleteConversation, disabled: isResponding })), _jsxs(TooltipProvider, { children: [messages.length > 0 && (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon-sm", onClick: handleNewChat, disabled: isResponding, className: "text-muted-foreground hover:text-foreground", children: _jsx(Plus, { className: "size-4" }) }) }), _jsx(TooltipContent, { side: "bottom", children: _jsx("p", { children: "New chat" }) })] })), _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon-sm", onClick: handleMinimize, className: "text-muted-foreground hover:text-foreground", children: _jsx(ChevronDown, { className: "size-4" }) }) }), _jsx(TooltipContent, { side: "bottom", children: _jsx("p", { children: "Minimize" }) })] })] })] })] }), _jsx(ScrollArea, { ref: scrollAreaRef, className: "min-w-0 flex-1 px-2", children: _jsxs("div", { className: "min-w-0 max-w-full space-y-4 overflow-hidden py-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 })), _jsx(ToolProgressProvider, { value: toolProgress, children: _jsx(ToolApprovalProvider, { onApprovalResponse: onApprovalResponse ?? null, children: _jsx(Messages, { messages: messages, status: status }) }) }), (missingApiKey ||
|
|
126
|
-
(error &&
|
|
127
|
-
error.message?.includes('API_KEY not configured'))) && (_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 start chatting with AI" })] }), onAddApiKey && (_jsx(Button, { onClick: onAddApiKey, size: "sm", className: "cursor-pointer", children: "Add API key" }))] })), error &&
|
|
128
|
-
status === 'error' &&
|
|
129
|
-
!error.message?.includes('API_KEY not configured') && (_jsxs("div", { className: "border-destructive/30 bg-destructive/10 text-destructive mx-2 flex items-start gap-2 rounded-lg border p-3 text-sm", children: [_jsx(AlertCircle, { className: "mt-0.5 size-4 shrink-0" }), _jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "font-medium", children: "Request failed" }), _jsx("p", { className: "text-destructive/80 mt-0.5 break-words", children: error.message || 'An unknown error occurred' })] })] })), _jsx("div", { ref: messagesEndRef })] }) })] }), _jsx("div", { className: "relative z-10", onClick: () => {
|
|
254
|
+
}, transition: { type: 'tween', duration: 0.2, ease: 'easeOut' }, className: "relative z-10 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" }) })] }) }) }), _jsx(ScrollArea, { ref: scrollAreaRef, className: "min-h-0 min-w-0 flex-1 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)))) }) }) }) })] }), _jsxs("div", { className: "relative z-10", onClick: () => {
|
|
130
255
|
if (!isExpanded) {
|
|
131
256
|
onExpandedChange(true);
|
|
132
257
|
}
|
|
133
|
-
}, children:
|
|
258
|
+
}, 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("div", { className: "space-y-1.5", children: queuedMessages.map((message) => (_jsxs("div", { className: "bg-muted/60 text-foreground flex min-h-9 items-center gap-2 rounded-lg px-2.5 py-1.5 text-xs", children: [_jsxs("span", { className: "min-w-0 flex-1 truncate", children: [message.preview, message.attachmentCount > 0 &&
|
|
259
|
+
` (${message.attachmentCount} image${message.attachmentCount === 1 ? '' : 's'})`] }), message.status === 'failed' &&
|
|
260
|
+
message.error &&
|
|
261
|
+
onRetryQueuedMessage && (_jsxs("button", { type: "button", onClick: (event) => {
|
|
262
|
+
event.stopPropagation();
|
|
263
|
+
onRetryQueuedMessage(message.id);
|
|
264
|
+
}, 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) => {
|
|
265
|
+
event.stopPropagation();
|
|
266
|
+
onSteerQueuedMessage(message.id);
|
|
267
|
+
}, 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) => {
|
|
268
|
+
event.stopPropagation();
|
|
269
|
+
onRemoveQueuedMessage(message.id);
|
|
270
|
+
}, 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 })] })] }) })] }));
|
|
134
271
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface CheckpointBadgeProps {
|
|
2
|
+
/** Message ID this checkpoint belongs to */
|
|
3
|
+
messageId: string;
|
|
4
|
+
/** Number of files in the checkpoint */
|
|
5
|
+
fileCount: number;
|
|
6
|
+
/** Total bytes of the checkpoint */
|
|
7
|
+
totalBytes?: number;
|
|
8
|
+
/** Whether a restore is in progress */
|
|
9
|
+
isRestoring?: boolean;
|
|
10
|
+
/** Callback when restore is requested */
|
|
11
|
+
onRestore: () => void;
|
|
12
|
+
/** Additional class name */
|
|
13
|
+
className?: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Badge shown on messages that have a checkpoint restore point.
|
|
17
|
+
* Clicking triggers the restore confirmation dialog.
|
|
18
|
+
*/
|
|
19
|
+
export declare function CheckpointBadge({ isRestoring, onRestore, className, }: CheckpointBadgeProps): import("react/jsx-runtime").JSX.Element;
|
|
20
|
+
//# sourceMappingURL=checkpoint-badge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"checkpoint-badge.d.ts","sourceRoot":"","sources":["../../../src/components/chat/checkpoint-badge.tsx"],"names":[],"mappings":"AAYA,MAAM,WAAW,oBAAoB;IACnC,4CAA4C;IAC5C,SAAS,EAAE,MAAM,CAAA;IACjB,wCAAwC;IACxC,SAAS,EAAE,MAAM,CAAA;IACjB,oCAAoC;IACpC,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,uCAAuC;IACvC,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,yCAAyC;IACzC,SAAS,EAAE,MAAM,IAAI,CAAA;IACrB,4BAA4B;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,EAC9B,WAAW,EACX,SAAS,EACT,SAAS,GACV,EAAE,oBAAoB,2CAiCtB"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { History, Loader2 } from 'lucide-react';
|
|
4
|
+
import { cn } from '../../lib/utils';
|
|
5
|
+
import { Button } from '../ui/button';
|
|
6
|
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from '../ui/tooltip';
|
|
7
|
+
/**
|
|
8
|
+
* Badge shown on messages that have a checkpoint restore point.
|
|
9
|
+
* Clicking triggers the restore confirmation dialog.
|
|
10
|
+
*/
|
|
11
|
+
export function CheckpointBadge({ isRestoring, onRestore, className, }) {
|
|
12
|
+
return (_jsx(TooltipProvider, { children: _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon-sm", onClick: (e) => {
|
|
13
|
+
e.stopPropagation();
|
|
14
|
+
onRestore();
|
|
15
|
+
}, disabled: isRestoring, className: cn('text-muted-foreground hover:text-foreground hover:bg-muted/50 size-6', !isRestoring && 'cursor-pointer', isRestoring && 'cursor-wait', className), children: isRestoring ? (_jsx(Loader2, { className: "size-3.5 animate-spin" })) : (_jsx(History, { className: "size-3.5" })) }) }), _jsx(TooltipContent, { side: "top", children: _jsx("p", { children: "Undo all changes from this point onwards" }) })] }) }));
|
|
16
|
+
}
|
|
@@ -10,9 +10,14 @@ interface ConversationHistoryProps {
|
|
|
10
10
|
currentConversationId?: string | null;
|
|
11
11
|
onSelect: (id: string) => void;
|
|
12
12
|
onDelete?: (id: string) => void;
|
|
13
|
+
onNewChat?: () => void;
|
|
14
|
+
newChatDisabled?: boolean;
|
|
13
15
|
disabled?: boolean;
|
|
14
16
|
className?: string;
|
|
17
|
+
dropdownAlign?: 'start' | 'center' | 'end';
|
|
18
|
+
dropdownSide?: 'top' | 'right' | 'bottom' | 'left';
|
|
19
|
+
tooltipSide?: 'top' | 'right' | 'bottom' | 'left';
|
|
15
20
|
}
|
|
16
|
-
export declare function ConversationHistory({ conversations, currentConversationId, onSelect, onDelete, disabled, className, }: ConversationHistoryProps): import("react/jsx-runtime").JSX.Element | null;
|
|
21
|
+
export declare function ConversationHistory({ conversations, currentConversationId, onSelect, onDelete, onNewChat, newChatDisabled, disabled, className, dropdownAlign, dropdownSide, tooltipSide, }: ConversationHistoryProps): import("react/jsx-runtime").JSX.Element | null;
|
|
17
22
|
export {};
|
|
18
23
|
//# sourceMappingURL=conversation-history.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"conversation-history.d.ts","sourceRoot":"","sources":["../../../src/components/chat/conversation-history.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"conversation-history.d.ts","sourceRoot":"","sources":["../../../src/components/chat/conversation-history.tsx"],"names":[],"mappings":"AAkBA,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,YAAY,EAAE,MAAM,CAAA;CACrB;AAED,UAAU,wBAAwB;IAChC,aAAa,EAAE,gBAAgB,EAAE,CAAA;IACjC,qBAAqB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrC,QAAQ,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAA;IAC9B,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAA;IAC/B,SAAS,CAAC,EAAE,MAAM,IAAI,CAAA;IACtB,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,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;IAClD,WAAW,CAAC,EAAE,KAAK,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,CAAA;CAClD;AAiBD,wBAAgB,mBAAmB,CAAC,EAClC,aAAa,EACb,qBAAqB,EACrB,QAAQ,EACR,QAAQ,EACR,SAAS,EACT,eAAuB,EACvB,QAAgB,EAChB,SAAS,EACT,aAAuB,EACvB,YAAuB,EACvB,WAAsB,GACvB,EAAE,wBAAwB,kDA6F1B"}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Clock, Trash2 } from 'lucide-react';
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Clock, Plus, Trash2 } from 'lucide-react';
|
|
3
3
|
import { cn } from '../../lib/utils';
|
|
4
4
|
import { Button } from '../ui/button';
|
|
5
|
-
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from '../ui/dropdown-menu';
|
|
5
|
+
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuTrigger, } from '../ui/dropdown-menu';
|
|
6
6
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from '../ui/tooltip';
|
|
7
7
|
function formatRelativeTime(dateStr) {
|
|
8
8
|
const date = new Date(dateStr);
|
|
@@ -21,11 +21,11 @@ function formatRelativeTime(dateStr) {
|
|
|
21
21
|
return `${diffDays}d ago`;
|
|
22
22
|
return date.toLocaleDateString();
|
|
23
23
|
}
|
|
24
|
-
export function ConversationHistory({ conversations, currentConversationId, onSelect, onDelete, disabled = false, className, }) {
|
|
25
|
-
if (conversations.length === 0) {
|
|
24
|
+
export function ConversationHistory({ conversations, currentConversationId, onSelect, onDelete, onNewChat, newChatDisabled = false, disabled = false, className, dropdownAlign = 'start', dropdownSide = 'bottom', tooltipSide = 'bottom', }) {
|
|
25
|
+
if (conversations.length === 0 && !onNewChat) {
|
|
26
26
|
return null;
|
|
27
27
|
}
|
|
28
|
-
return (_jsx(TooltipProvider, { children: _jsxs(DropdownMenu, { children: [_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon-sm", disabled: disabled, className: cn('text-muted-foreground hover:text-foreground', className), children: _jsx(Clock, { className: "size-4" }) }) }) }), _jsx(TooltipContent, { side:
|
|
28
|
+
return (_jsx(TooltipProvider, { children: _jsxs(DropdownMenu, { children: [_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon-sm", disabled: disabled, className: cn('text-muted-foreground hover:text-foreground cursor-pointer', className), children: _jsx(Clock, { className: "size-4" }) }) }) }), _jsx(TooltipContent, { side: tooltipSide, children: _jsx("p", { children: "Conversation history" }) })] }), _jsxs(DropdownMenuContent, { align: dropdownAlign, side: dropdownSide, className: "w-64", children: [onNewChat && (_jsxs(_Fragment, { children: [_jsxs(DropdownMenuItem, { onClick: onNewChat, disabled: newChatDisabled, title: "New conversation (Cmd+Shift+O)", className: "flex cursor-pointer items-center gap-2", children: [_jsx(Plus, { className: "size-4" }), _jsx("span", { children: "New conversation" }), _jsx(DropdownMenuShortcut, { children: "\u2318\u21E7O" })] }), conversations.length > 0 && _jsx(DropdownMenuSeparator, {})] })), conversations.length > 0 && (_jsxs(_Fragment, { children: [_jsx("div", { className: "text-muted-foreground px-2 py-1.5 text-xs font-medium", children: "Recent conversations" }), _jsx(DropdownMenuSeparator, {})] })), _jsx("div", { className: "max-h-64 overflow-y-auto", children: conversations.slice(0, 20).map((conv) => (_jsxs(DropdownMenuItem, { onClick: () => onSelect(conv.id), className: cn('group flex cursor-pointer items-start gap-2 py-2', conv.id === currentConversationId && 'bg-accent'), children: [_jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("p", { className: "truncate text-sm", children: conv.title }), _jsxs("p", { className: "text-muted-foreground text-xs", children: [formatRelativeTime(conv.updatedAt), " \u00B7 ", conv.messageCount, ' ', "messages"] })] }), onDelete && (_jsx(Button, { variant: "ghost", size: "icon-sm", onClick: (e) => {
|
|
29
29
|
e.stopPropagation();
|
|
30
30
|
onDelete(conv.id);
|
|
31
31
|
}, className: "text-muted-foreground hover:text-destructive size-6 shrink-0 opacity-0 group-hover:opacity-100", children: _jsx(Trash2, { className: "size-3" }) }))] }, conv.id))) })] })] }) }));
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
export { ChatInput } from './chat-input';
|
|
1
|
+
export { ChatInput, type ChatImageAttachment, type ChatSubmitMode, } from './chat-input';
|
|
2
2
|
export { Message, ThinkingMessage, type ChatMessage, type ChatMessagePart, } from './chat-message';
|
|
3
|
-
export { Messages } from './chat-messages';
|
|
3
|
+
export { Messages, type MessageCheckpoint } from './chat-messages';
|
|
4
|
+
export { CheckpointBadge, type CheckpointBadgeProps } from './checkpoint-badge';
|
|
5
|
+
export { RestoreDialog, type CheckpointInfo, type RestoreDialogProps, } from './restore-dialog';
|
|
4
6
|
export { ModelSelector, type ModelOption } from './model-selector';
|
|
5
7
|
export { ReasoningEffortSelector, type ReasoningEffortOption, } from './reasoning-effort-selector';
|
|
6
8
|
export { ConversationHistory, type ConversationMeta, } from './conversation-history';
|
|
7
|
-
export { ChatPanel, type ChatPanelProps, type ToolProgressData, } from './chat-panel';
|
|
9
|
+
export { ChatPanel, type ChatPanelProps, type QueuedChatMessage, type ToolProgressData, } from './chat-panel';
|
|
8
10
|
export { ThinkingTimeline, ThinkingTimelineMarker, type ThinkingTimelineItem, type ThinkingTimelineProps, } from './thinking-timeline';
|
|
9
11
|
export { getToolHandler, DEFAULT_TOOL_HANDLERS, type ToolHandler, } from './tool-handlers';
|
|
10
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,
|
|
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,6 +1,8 @@
|
|
|
1
|
-
export { ChatInput } from './chat-input';
|
|
1
|
+
export { ChatInput, } from './chat-input';
|
|
2
2
|
export { Message, ThinkingMessage, } from './chat-message';
|
|
3
3
|
export { Messages } from './chat-messages';
|
|
4
|
+
export { CheckpointBadge } from './checkpoint-badge';
|
|
5
|
+
export { RestoreDialog, } from './restore-dialog';
|
|
4
6
|
export { ModelSelector } from './model-selector';
|
|
5
7
|
export { ReasoningEffortSelector, } from './reasoning-effort-selector';
|
|
6
8
|
export { ConversationHistory, } from './conversation-history';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
type MarkdownProps = {
|
|
2
|
+
markdown: string;
|
|
3
|
+
className?: string;
|
|
4
|
+
proseSize?: 'sm' | 'base' | 'lg';
|
|
5
|
+
};
|
|
6
|
+
declare function NonMemoizedMarkdown({ markdown, className, proseSize }: MarkdownProps): import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
export declare const Markdown: import("react").MemoExoticComponent<typeof NonMemoizedMarkdown>;
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=markdown.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"markdown.d.ts","sourceRoot":"","sources":["../../../src/components/chat/markdown.tsx"],"names":[],"mappings":"AAMA,KAAK,aAAa,GAAG;IACnB,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,SAAS,CAAC,EAAE,IAAI,GAAG,MAAM,GAAG,IAAI,CAAA;CACjC,CAAA;AAwFD,iBAAS,mBAAmB,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAgB,EAAE,EAAE,aAAa,2CAyBpF;AAED,eAAO,MAAM,QAAQ,iEAMpB,CAAA"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { memo } from 'react';
|
|
3
|
+
import ReactMarkdown from 'react-markdown';
|
|
4
|
+
import { cn } from '../../lib/utils';
|
|
5
|
+
import rehypeRaw from 'rehype-raw';
|
|
6
|
+
import remarkGfm from 'remark-gfm';
|
|
7
|
+
const components = {
|
|
8
|
+
img({ src, alt }) {
|
|
9
|
+
if (!src)
|
|
10
|
+
return null;
|
|
11
|
+
return (_jsx("span", { className: "flex justify-center", children: _jsx("img", { src: src, alt: alt ?? '', className: "h-auto max-w-full rounded-lg" }) }));
|
|
12
|
+
},
|
|
13
|
+
table({ children }) {
|
|
14
|
+
return (_jsx("table", { className: "border-collapse border px-3 py-1", children: children }));
|
|
15
|
+
},
|
|
16
|
+
th({ children }) {
|
|
17
|
+
return (_jsx("th", { className: "bg-muted border px-3 py-1 break-words", children: children }));
|
|
18
|
+
},
|
|
19
|
+
td({ children }) {
|
|
20
|
+
return _jsx("td", { className: "border px-3 py-1 break-words", children: children });
|
|
21
|
+
},
|
|
22
|
+
ol: ({ children, ...props }) => (_jsx("ol", { className: "ml-4 list-outside list-decimal", ...props, children: children })),
|
|
23
|
+
ul: ({ children, ...props }) => (_jsx("ul", { className: "ml-4 list-outside list-disc", ...props, children: children })),
|
|
24
|
+
li: ({ children, ...props }) => (_jsx("li", { className: "py-1", ...props, children: children })),
|
|
25
|
+
strong: ({ children, ...props }) => (_jsx("span", { className: "font-semibold", ...props, children: children })),
|
|
26
|
+
a: ({ children, href, ...props }) => (_jsx("a", { className: "font-bold hover:underline", href: href ?? '#', target: "_blank", rel: "noreferrer", ...props, children: children })),
|
|
27
|
+
pre: ({ children, ...props }) => (_jsx("pre", { className: "overflow-x-auto rounded-lg bg-muted/50 p-3 text-sm", ...props, children: children })),
|
|
28
|
+
code: ({ children, className, ...props }) => {
|
|
29
|
+
// Check if this is an inline code block (no className means inline)
|
|
30
|
+
const isInline = !className;
|
|
31
|
+
if (isInline) {
|
|
32
|
+
return (_jsx("code", { className: "rounded bg-muted px-1.5 py-0.5 text-sm font-mono", ...props, children: children }));
|
|
33
|
+
}
|
|
34
|
+
return (_jsx("code", { className: cn('font-mono text-sm', className), ...props, children: children }));
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
function NonMemoizedMarkdown({ markdown, className, proseSize = 'sm' }) {
|
|
38
|
+
const proseSizeClass = {
|
|
39
|
+
sm: 'prose-sm',
|
|
40
|
+
base: 'prose-base',
|
|
41
|
+
lg: 'prose-lg',
|
|
42
|
+
}[proseSize];
|
|
43
|
+
return (_jsx("div", { className: cn('prose w-full max-w-none dark:prose-invert', proseSizeClass, 'text-foreground', className), children: _jsx(ReactMarkdown, { remarkPlugins: [remarkGfm], rehypePlugins: [rehypeRaw], components: components, children: markdown }) }));
|
|
44
|
+
}
|
|
45
|
+
export const Markdown = memo(NonMemoizedMarkdown, (prevProps, nextProps) => prevProps.markdown === nextProps.markdown &&
|
|
46
|
+
prevProps.className === nextProps.className &&
|
|
47
|
+
prevProps.proseSize === nextProps.proseSize);
|
|
@@ -10,7 +10,9 @@ type ModelSelectorProps = {
|
|
|
10
10
|
onModelChange: (modelId: string) => void;
|
|
11
11
|
disabled?: boolean;
|
|
12
12
|
className?: string;
|
|
13
|
+
dropdownAlign?: 'start' | 'center' | 'end';
|
|
14
|
+
dropdownSide?: 'top' | 'right' | 'bottom' | 'left';
|
|
13
15
|
};
|
|
14
|
-
export declare function ModelSelector({ models, selectedModel, onModelChange, disabled, className, }: ModelSelectorProps): import("react/jsx-runtime").JSX.Element;
|
|
16
|
+
export declare function ModelSelector({ models, selectedModel, onModelChange, disabled, className, dropdownAlign, dropdownSide, }: ModelSelectorProps): import("react/jsx-runtime").JSX.Element;
|
|
15
17
|
export {};
|
|
16
18
|
//# sourceMappingURL=model-selector.d.ts.map
|
|
@@ -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;
|
|
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,2CAyCpB"}
|
|
@@ -3,7 +3,7 @@ import { Check, ChevronDown } from 'lucide-react';
|
|
|
3
3
|
import { cn } from '../../lib/utils';
|
|
4
4
|
import { Button } from '../ui/button';
|
|
5
5
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '../ui/dropdown-menu';
|
|
6
|
-
export function ModelSelector({ models, selectedModel, onModelChange, disabled = false, className, }) {
|
|
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
|
+
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.5 rounded-full px-2.5 text-xs font-medium', className), children: [_jsx("span", { children: currentModel?.icon }), _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
|
}
|
|
@@ -8,7 +8,10 @@ type ReasoningEffortSelectorProps = {
|
|
|
8
8
|
onEffortChange: (effort: string) => void;
|
|
9
9
|
disabled?: boolean;
|
|
10
10
|
className?: string;
|
|
11
|
+
showLabel?: boolean;
|
|
12
|
+
dropdownAlign?: 'start' | 'center' | 'end';
|
|
13
|
+
dropdownSide?: 'top' | 'right' | 'bottom' | 'left';
|
|
11
14
|
};
|
|
12
|
-
export declare function ReasoningEffortSelector({ options, selectedEffort, onEffortChange, disabled, className, }: ReasoningEffortSelectorProps): import("react/jsx-runtime").JSX.Element;
|
|
15
|
+
export declare function ReasoningEffortSelector({ options, selectedEffort, onEffortChange, disabled, className, showLabel, dropdownAlign, dropdownSide, }: ReasoningEffortSelectorProps): import("react/jsx-runtime").JSX.Element;
|
|
13
16
|
export {};
|
|
14
17
|
//# sourceMappingURL=reasoning-effort-selector.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reasoning-effort-selector.d.ts","sourceRoot":"","sources":["../../../src/components/chat/reasoning-effort-selector.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"reasoning-effort-selector.d.ts","sourceRoot":"","sources":["../../../src/components/chat/reasoning-effort-selector.tsx"],"names":[],"mappings":"AAWA,MAAM,MAAM,qBAAqB,GAAG;IAClC,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;CACd,CAAA;AAED,KAAK,4BAA4B,GAAG;IAClC,OAAO,EAAE,qBAAqB,EAAE,CAAA;IAChC,cAAc,EAAE,MAAM,CAAA;IACtB,cAAc,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAA;IACxC,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,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,uBAAuB,CAAC,EACtC,OAAO,EACP,cAAc,EACd,cAAc,EACd,QAAgB,EAChB,SAAS,EACT,SAAiB,EACjB,aAAuB,EACvB,YAAuB,GACxB,EAAE,4BAA4B,2CAoD9B"}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Brain, Check } from 'lucide-react';
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Brain, Check, ChevronDown } from 'lucide-react';
|
|
3
3
|
import { cn } from '../../lib/utils';
|
|
4
4
|
import { Button } from '../ui/button';
|
|
5
|
-
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '../ui/dropdown-menu';
|
|
6
|
-
|
|
7
|
-
export function ReasoningEffortSelector({ options, selectedEffort, onEffortChange, disabled = false, className, }) {
|
|
5
|
+
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuTrigger, } from '../ui/dropdown-menu';
|
|
6
|
+
export function ReasoningEffortSelector({ options, selectedEffort, onEffortChange, disabled = false, className, showLabel = false, dropdownAlign = 'start', dropdownSide = 'bottom', }) {
|
|
8
7
|
const currentOption = options.find((o) => o.value === selectedEffort) ?? options[1]; // Default to medium
|
|
9
|
-
return (
|
|
8
|
+
return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon-sm", disabled: disabled, className: cn('text-muted-foreground hover:text-foreground cursor-pointer', showLabel &&
|
|
9
|
+
'h-8 w-auto gap-1.5 rounded-full px-3 text-xs font-medium', className), children: showLabel ? (_jsxs(_Fragment, { children: [_jsx("span", { children: currentOption?.label }), _jsx(ChevronDown, { className: "size-3 opacity-50" })] })) : (_jsx(Brain, { className: "size-4" })) }) }), _jsxs(DropdownMenuContent, { align: dropdownAlign, side: dropdownSide, className: "min-w-[140px]", children: [_jsx(DropdownMenuLabel, { className: "text-muted-foreground font-normal", children: "Reasoning" }), options.map((option) => (_jsxs(DropdownMenuItem, { onClick: () => onEffortChange(option.value), className: "flex cursor-pointer items-center gap-2", children: [!showLabel && _jsx(Brain, { className: "text-muted-foreground size-3.5" }), _jsx("span", { className: "flex-1", children: option.label }), option.value === selectedEffort && (_jsx(Check, { className: "text-primary size-4" }))] }, option.value)))] })] }));
|
|
10
10
|
}
|