@hienlh/ppm 0.4.5 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/bun.lock +3 -0
- package/dist/web/assets/api-client-ANLU-Irq.js +1 -0
- package/dist/web/assets/chat-tab-C6iTYbRI.js +7 -0
- package/dist/web/assets/code-editor-hnDDc8JZ.js +1 -0
- package/dist/web/assets/{diff-viewer-B9oX4DDx.js → diff-viewer-BWeMVAvK.js} +1 -1
- package/dist/web/assets/git-graph-D6oftHHC.js +1 -0
- package/dist/web/assets/index-CWwJBtaO.js +21 -0
- package/dist/web/assets/index-jmj5f_bQ.css +2 -0
- package/dist/web/assets/{input-AESbQWjx.js → input-D-F4ITU0.js} +1 -1
- package/dist/web/assets/jsx-runtime-B4BJKQ1u.js +1 -0
- package/dist/web/assets/{markdown-renderer-DdDDhQDx.js → markdown-renderer-PHBaNQ3l.js} +2 -2
- package/dist/web/assets/react-WvgCEYPV.js +1 -0
- package/dist/web/assets/rotate-ccw-BesidNnx.js +1 -0
- package/dist/web/assets/settings-store-CGtTcr8r.js +1 -0
- package/dist/web/assets/settings-tab-BpETyigv.js +1 -0
- package/dist/web/assets/tab-store-Dq1kMOkJ.js +1 -0
- package/dist/web/assets/{terminal-tab-BeFf07MH.js → terminal-tab-BTumEYyO.js} +2 -2
- package/dist/web/assets/{use-monaco-theme-Bb9W0CI2.js → use-monaco-theme-CsNwoeyj.js} +1 -1
- package/dist/web/index.html +8 -7
- package/dist/web/sw.js +1 -1
- package/package.json +2 -1
- package/src/providers/claude-agent-sdk.ts +36 -13
- package/src/server/index.ts +3 -0
- package/src/server/routes/chat.ts +31 -3
- package/src/server/ws/chat.ts +40 -0
- package/src/services/claude-usage.service.ts +51 -23
- package/src/types/api.ts +1 -0
- package/src/types/chat.ts +1 -0
- package/src/web/app.tsx +5 -0
- package/src/web/components/chat/chat-history-bar.tsx +15 -3
- package/src/web/components/chat/chat-tab.tsx +45 -50
- package/src/web/components/chat/message-input.tsx +116 -55
- package/src/web/components/chat/message-list.tsx +156 -69
- package/src/web/components/chat/usage-badge.tsx +4 -4
- package/src/web/components/layout/command-palette.tsx +37 -8
- package/src/web/components/layout/draggable-tab.tsx +4 -4
- package/src/web/components/layout/mobile-drawer.tsx +2 -2
- package/src/web/components/layout/mobile-nav.tsx +3 -2
- package/src/web/components/layout/project-bar.tsx +5 -3
- package/src/web/components/layout/project-bottom-sheet.tsx +3 -1
- package/src/web/components/layout/tab-bar.tsx +4 -4
- package/src/web/components/shared/bug-report-popup.tsx +58 -0
- package/src/web/hooks/use-chat.ts +63 -7
- package/src/web/hooks/use-usage.ts +15 -17
- package/src/web/lib/report-bug.ts +12 -3
- package/src/web/stores/project-store.ts +7 -1
- package/vite.config.ts +2 -0
- package/dist/web/assets/api-client-BsHoRDAn.js +0 -1
- package/dist/web/assets/chat-tab-Bj1hZQ4x.js +0 -6
- package/dist/web/assets/code-editor-Bj9jdnLm.js +0 -1
- package/dist/web/assets/copy-BNk4Z75P.js +0 -1
- package/dist/web/assets/external-link-CrtbmtJ6.js +0 -1
- package/dist/web/assets/git-graph-DoLRBTMk.js +0 -1
- package/dist/web/assets/index-C_yeSRZ0.css +0 -2
- package/dist/web/assets/index-D27GI6gs.js +0 -21
- package/dist/web/assets/jsx-runtime-BFALxl05.js +0 -1
- package/dist/web/assets/settings-store-DWYkr_a3.js +0 -1
- package/dist/web/assets/settings-tab-BLoiK6Nc.js +0 -1
- package/dist/web/assets/tab-store-B1wzyDLQ.js +0 -1
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { useState, useCallback, useRef, useEffect, type DragEvent } from "react";
|
|
2
|
-
import { Upload,
|
|
2
|
+
import { Upload, X } from "lucide-react";
|
|
3
3
|
import { api, projectUrl } from "@/lib/api-client";
|
|
4
4
|
import { useChat } from "@/hooks/use-chat";
|
|
5
5
|
import { useUsage } from "@/hooks/use-usage";
|
|
6
6
|
import { useTabStore } from "@/stores/tab-store";
|
|
7
7
|
import { useSettingsStore } from "@/stores/settings-store";
|
|
8
|
-
import {
|
|
8
|
+
import { openBugReportPopup } from "@/lib/report-bug";
|
|
9
9
|
import { MessageList } from "./message-list";
|
|
10
10
|
import { MessageInput, type ChatAttachment } from "./message-input";
|
|
11
11
|
import { SlashCommandPicker, type SlashItem } from "./slash-command-picker";
|
|
@@ -39,10 +39,6 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
|
|
|
39
39
|
const [fileFilter, setFileFilter] = useState("");
|
|
40
40
|
const [fileSelected, setFileSelected] = useState<FileNode | null>(null);
|
|
41
41
|
|
|
42
|
-
// Bug report popup
|
|
43
|
-
const [bugReportText, setBugReportText] = useState<string | null>(null);
|
|
44
|
-
const [copied, setCopied] = useState(false);
|
|
45
|
-
|
|
46
42
|
// Drag-and-drop state
|
|
47
43
|
const [isDragging, setIsDragging] = useState(false);
|
|
48
44
|
const [externalFiles, setExternalFiles] = useState<File[] | null>(null);
|
|
@@ -54,7 +50,7 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
|
|
|
54
50
|
const version = useSettingsStore((s) => s.version);
|
|
55
51
|
|
|
56
52
|
// Usage runs independently — auto-refreshes on interval
|
|
57
|
-
const { usageInfo, usageLoading,
|
|
53
|
+
const { usageInfo, usageLoading, lastFetchedAt, refreshUsage, mergeUsage } =
|
|
58
54
|
useUsage(projectName, providerId);
|
|
59
55
|
|
|
60
56
|
// Persist sessionId and providerId to tab metadata so reload restores the session
|
|
@@ -69,6 +65,9 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
|
|
|
69
65
|
messages,
|
|
70
66
|
messagesLoading,
|
|
71
67
|
isStreaming,
|
|
68
|
+
streamingStatus,
|
|
69
|
+
connectingElapsed,
|
|
70
|
+
thinkingWarningThreshold,
|
|
72
71
|
pendingApproval,
|
|
73
72
|
sendMessage,
|
|
74
73
|
respondToApproval,
|
|
@@ -78,6 +77,17 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
|
|
|
78
77
|
isConnected,
|
|
79
78
|
} = useChat(sessionId, providerId, projectName, { onUsageEvent: mergeUsage });
|
|
80
79
|
|
|
80
|
+
// Auto-send pending message for forked sessions (set by handleFork)
|
|
81
|
+
const pendingForkMsgRef = useRef(metadata?.pendingMessage as string | undefined);
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (pendingForkMsgRef.current && isConnected && sessionId) {
|
|
84
|
+
const msg = pendingForkMsgRef.current;
|
|
85
|
+
pendingForkMsgRef.current = undefined;
|
|
86
|
+
if (tabId) updateTab(tabId, { metadata: { ...metadata, pendingMessage: undefined } });
|
|
87
|
+
setTimeout(() => sendMessage(msg), 100);
|
|
88
|
+
}
|
|
89
|
+
}, [isConnected, sessionId]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
90
|
+
|
|
81
91
|
const handleNewSession = useCallback(() => {
|
|
82
92
|
useTabStore.getState().openTab({
|
|
83
93
|
type: "chat",
|
|
@@ -93,6 +103,27 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
|
|
|
93
103
|
setProviderId(session.providerId);
|
|
94
104
|
}, []);
|
|
95
105
|
|
|
106
|
+
/** Fork current session and open new tab with the forked session, resending userMessage */
|
|
107
|
+
const handleFork = useCallback(async (userMessage: string) => {
|
|
108
|
+
if (!sessionId || !projectName) return;
|
|
109
|
+
try {
|
|
110
|
+
const { api, projectUrl } = await import("@/lib/api-client");
|
|
111
|
+
const forked = await api.post<{ id: string; forkedFrom: string }>(
|
|
112
|
+
`${projectUrl(projectName)}/chat/sessions/${sessionId}/fork?providerId=${providerId}`,
|
|
113
|
+
);
|
|
114
|
+
// Open new chat tab with forked session — it will send userMessage on connect
|
|
115
|
+
useTabStore.getState().openTab({
|
|
116
|
+
type: "chat",
|
|
117
|
+
title: `Fork: ${userMessage.slice(0, 30)}`,
|
|
118
|
+
metadata: { projectName, sessionId: forked.id, providerId, pendingMessage: userMessage },
|
|
119
|
+
projectId: projectName || null,
|
|
120
|
+
closable: true,
|
|
121
|
+
});
|
|
122
|
+
} catch (e) {
|
|
123
|
+
console.error("Fork failed:", e);
|
|
124
|
+
}
|
|
125
|
+
}, [sessionId, projectName, providerId]);
|
|
126
|
+
|
|
96
127
|
/** Build message content with file references prepended */
|
|
97
128
|
const buildMessageWithAttachments = useCallback(
|
|
98
129
|
(content: string, attachments: ChatAttachment[]): string => {
|
|
@@ -238,7 +269,11 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
|
|
|
238
269
|
pendingApproval={pendingApproval}
|
|
239
270
|
onApprovalResponse={respondToApproval}
|
|
240
271
|
isStreaming={isStreaming}
|
|
272
|
+
streamingStatus={streamingStatus}
|
|
273
|
+
connectingElapsed={connectingElapsed}
|
|
274
|
+
thinkingWarningThreshold={thinkingWarningThreshold}
|
|
241
275
|
projectName={projectName}
|
|
276
|
+
onFork={!isStreaming ? handleFork : undefined}
|
|
242
277
|
/>
|
|
243
278
|
|
|
244
279
|
{/* Bottom toolbar */}
|
|
@@ -249,14 +284,10 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
|
|
|
249
284
|
usageInfo={usageInfo}
|
|
250
285
|
usageLoading={usageLoading}
|
|
251
286
|
refreshUsage={refreshUsage}
|
|
252
|
-
|
|
287
|
+
lastFetchedAt={lastFetchedAt}
|
|
253
288
|
sessionId={sessionId}
|
|
254
289
|
onSelectSession={handleSelectSession}
|
|
255
|
-
onBugReport={sessionId ?
|
|
256
|
-
const text = await buildBugReport(version, { sessionId, projectName });
|
|
257
|
-
setBugReportText(text);
|
|
258
|
-
setCopied(false);
|
|
259
|
-
} : undefined}
|
|
290
|
+
onBugReport={sessionId ? () => openBugReportPopup(version, { sessionId, projectName }) : undefined}
|
|
260
291
|
isConnected={isConnected}
|
|
261
292
|
onReconnect={() => {
|
|
262
293
|
if (!isConnected) reconnect();
|
|
@@ -296,43 +327,7 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
|
|
|
296
327
|
/>
|
|
297
328
|
</div>
|
|
298
329
|
|
|
299
|
-
{/* Bug report popup */}
|
|
300
|
-
{bugReportText && (
|
|
301
|
-
<>
|
|
302
|
-
<div className="fixed inset-0 z-50 bg-black/50" onClick={() => setBugReportText(null)} />
|
|
303
|
-
<div className="fixed inset-x-4 top-[10%] bottom-[10%] z-50 mx-auto max-w-lg flex flex-col rounded-lg border border-border bg-background shadow-xl">
|
|
304
|
-
<div className="flex items-center justify-between px-4 py-2 border-b border-border">
|
|
305
|
-
<span className="text-sm font-medium">Bug Report</span>
|
|
306
|
-
<button onClick={() => setBugReportText(null)} className="p-1 rounded hover:bg-surface-elevated">
|
|
307
|
-
<X className="size-4" />
|
|
308
|
-
</button>
|
|
309
|
-
</div>
|
|
310
|
-
<pre className="flex-1 overflow-auto px-4 py-2 text-xs font-mono whitespace-pre-wrap break-all">{bugReportText}</pre>
|
|
311
|
-
<div className="flex gap-2 px-4 py-3 border-t border-border">
|
|
312
|
-
<button
|
|
313
|
-
onClick={async () => {
|
|
314
|
-
const ok = await copyToClipboard(bugReportText);
|
|
315
|
-
if (ok) setCopied(true);
|
|
316
|
-
}}
|
|
317
|
-
className="flex-1 flex items-center justify-center gap-2 px-3 py-2 text-sm rounded-lg bg-surface hover:bg-surface-elevated border border-border transition-colors"
|
|
318
|
-
>
|
|
319
|
-
<Copy className="size-4" />
|
|
320
|
-
{copied ? "Copied!" : "Copy"}
|
|
321
|
-
</button>
|
|
322
|
-
<button
|
|
323
|
-
onClick={() => {
|
|
324
|
-
openGithubIssue(bugReportText);
|
|
325
|
-
setBugReportText(null);
|
|
326
|
-
}}
|
|
327
|
-
className="flex-1 flex items-center justify-center gap-2 px-3 py-2 text-sm rounded-lg bg-primary text-white hover:bg-primary/90 transition-colors"
|
|
328
|
-
>
|
|
329
|
-
<ExternalLink className="size-4" />
|
|
330
|
-
GitHub Issue
|
|
331
|
-
</button>
|
|
332
|
-
</div>
|
|
333
|
-
</div>
|
|
334
|
-
</>
|
|
335
|
-
)}
|
|
330
|
+
{/* Bug report popup is now global — see BugReportPopup in app.tsx */}
|
|
336
331
|
</div>
|
|
337
332
|
);
|
|
338
333
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useState, useRef, useCallback, useEffect, type KeyboardEvent, type DragEvent, type ClipboardEvent } from "react";
|
|
2
|
-
import {
|
|
2
|
+
import { ArrowUp, Square, Paperclip } from "lucide-react";
|
|
3
3
|
import { api, projectUrl, getAuthToken } from "@/lib/api-client";
|
|
4
4
|
import { randomId } from "@/lib/utils";
|
|
5
5
|
import { isSupportedFile, isImageFile } from "@/lib/file-support";
|
|
@@ -35,6 +35,8 @@ interface MessageInputProps {
|
|
|
35
35
|
fileSelected?: FileNode | null;
|
|
36
36
|
/** External files added via drag-drop on parent */
|
|
37
37
|
externalFiles?: File[] | null;
|
|
38
|
+
/** Pre-fill input value (e.g. from command palette "Ask AI") */
|
|
39
|
+
initialValue?: string;
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
export function MessageInput({
|
|
@@ -50,14 +52,27 @@ export function MessageInput({
|
|
|
50
52
|
onFileItemsLoaded,
|
|
51
53
|
fileSelected,
|
|
52
54
|
externalFiles,
|
|
55
|
+
initialValue,
|
|
53
56
|
}: MessageInputProps) {
|
|
54
|
-
const [value, setValue] = useState("");
|
|
57
|
+
const [value, setValue] = useState(initialValue ?? "");
|
|
55
58
|
const [attachments, setAttachments] = useState<ChatAttachment[]>([]);
|
|
56
59
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
57
60
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
58
61
|
const slashItemsRef = useRef<SlashItem[]>([]);
|
|
59
62
|
const fileItemsRef = useRef<FileNode[]>([]);
|
|
60
63
|
|
|
64
|
+
// Apply initialValue when it changes (e.g. "Ask AI" from command palette)
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
if (initialValue) {
|
|
67
|
+
setValue(initialValue);
|
|
68
|
+
// Focus and move cursor to end
|
|
69
|
+
setTimeout(() => {
|
|
70
|
+
const ta = textareaRef.current;
|
|
71
|
+
if (ta) { ta.focus(); ta.selectionStart = ta.selectionEnd = ta.value.length; }
|
|
72
|
+
}, 50);
|
|
73
|
+
}
|
|
74
|
+
}, [initialValue]);
|
|
75
|
+
|
|
61
76
|
// Fetch slash items when projectName changes
|
|
62
77
|
useEffect(() => {
|
|
63
78
|
if (!projectName) {
|
|
@@ -366,65 +381,111 @@ export function MessageInput({
|
|
|
366
381
|
const showCancel = isStreaming && !hasContent;
|
|
367
382
|
|
|
368
383
|
return (
|
|
369
|
-
<div className="
|
|
370
|
-
{/* Attachment chips */}
|
|
384
|
+
<div className="p-2 md:p-3 bg-background">
|
|
385
|
+
{/* Attachment chips (above input) */}
|
|
371
386
|
<AttachmentChips attachments={attachments} onRemove={removeAttachment} />
|
|
372
387
|
|
|
373
|
-
{/*
|
|
374
|
-
<div
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
className="flex items-center justify-center rounded-lg p-2 text-text-subtle hover:text-text-primary hover:bg-surface transition-colors shrink-0 disabled:opacity-50"
|
|
381
|
-
aria-label="Attach file"
|
|
382
|
-
>
|
|
383
|
-
<Paperclip className="size-4" />
|
|
384
|
-
</button>
|
|
385
|
-
<input
|
|
386
|
-
ref={fileInputRef}
|
|
387
|
-
type="file"
|
|
388
|
-
multiple
|
|
389
|
-
className="hidden"
|
|
390
|
-
onChange={handleFileInputChange}
|
|
391
|
-
/>
|
|
392
|
-
|
|
393
|
-
<textarea
|
|
394
|
-
ref={textareaRef}
|
|
395
|
-
value={value}
|
|
396
|
-
onChange={(e) => {
|
|
397
|
-
handleChange(e.target.value);
|
|
398
|
-
handleInput();
|
|
399
|
-
}}
|
|
400
|
-
onKeyDown={handleKeyDown}
|
|
401
|
-
onPaste={handlePaste}
|
|
402
|
-
onDrop={handleDrop}
|
|
403
|
-
onDragOver={handleDragOver}
|
|
404
|
-
placeholder={isStreaming ? "Follow-up or Stop..." : "Message... (↵ to send)"}
|
|
405
|
-
disabled={disabled}
|
|
406
|
-
rows={1}
|
|
407
|
-
className="flex-1 resize-none rounded-lg border border-border bg-surface px-3 py-2 text-base md:text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:border-ring disabled:opacity-50 max-h-40"
|
|
408
|
-
/>
|
|
409
|
-
{showCancel ? (
|
|
388
|
+
{/* Rounded input container */}
|
|
389
|
+
<div
|
|
390
|
+
className="border border-border rounded-xl md:rounded-2xl bg-surface shadow-sm cursor-text"
|
|
391
|
+
onClick={() => !disabled && textareaRef.current?.focus()}
|
|
392
|
+
>
|
|
393
|
+
{/* Mobile: single row — attach + textarea + send */}
|
|
394
|
+
<div className="flex items-end gap-1 md:hidden px-2 py-2">
|
|
410
395
|
<button
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
396
|
+
type="button"
|
|
397
|
+
onClick={(e) => { e.stopPropagation(); handleAttachClick(); }}
|
|
398
|
+
disabled={disabled}
|
|
399
|
+
className="flex items-center justify-center size-7 shrink-0 rounded-full text-text-subtle hover:text-text-primary transition-colors disabled:opacity-50"
|
|
400
|
+
aria-label="Attach file"
|
|
414
401
|
>
|
|
415
|
-
<
|
|
402
|
+
<Paperclip className="size-4" />
|
|
416
403
|
</button>
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
404
|
+
<textarea
|
|
405
|
+
ref={textareaRef}
|
|
406
|
+
value={value}
|
|
407
|
+
onChange={(e) => { handleChange(e.target.value); handleInput(); }}
|
|
408
|
+
onKeyDown={handleKeyDown}
|
|
409
|
+
onPaste={handlePaste}
|
|
410
|
+
onDrop={handleDrop}
|
|
411
|
+
onDragOver={handleDragOver}
|
|
412
|
+
placeholder={isStreaming ? "Follow-up..." : "Ask anything..."}
|
|
413
|
+
disabled={disabled}
|
|
414
|
+
rows={1}
|
|
415
|
+
className="flex-1 resize-none bg-transparent py-1.5 text-sm text-foreground placeholder:text-text-subtle focus:outline-none disabled:opacity-50 max-h-32"
|
|
416
|
+
/>
|
|
417
|
+
{showCancel ? (
|
|
418
|
+
<button
|
|
419
|
+
onClick={(e) => { e.stopPropagation(); onCancel?.(); }}
|
|
420
|
+
className="flex items-center justify-center size-7 shrink-0 rounded-full bg-red-600 text-white hover:bg-red-500 transition-colors"
|
|
421
|
+
aria-label="Stop"
|
|
422
|
+
>
|
|
423
|
+
<Square className="size-3" />
|
|
424
|
+
</button>
|
|
425
|
+
) : (
|
|
426
|
+
<button
|
|
427
|
+
onClick={(e) => { e.stopPropagation(); handleSend(); }}
|
|
428
|
+
disabled={disabled || !hasContent}
|
|
429
|
+
className="flex items-center justify-center size-7 shrink-0 rounded-full bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-30 transition-colors"
|
|
430
|
+
aria-label="Send"
|
|
431
|
+
>
|
|
432
|
+
<ArrowUp className="size-3.5" />
|
|
433
|
+
</button>
|
|
434
|
+
)}
|
|
435
|
+
</div>
|
|
436
|
+
|
|
437
|
+
{/* Desktop: textarea + action bar below */}
|
|
438
|
+
<div className="hidden md:block">
|
|
439
|
+
<textarea
|
|
440
|
+
ref={textareaRef}
|
|
441
|
+
value={value}
|
|
442
|
+
onChange={(e) => { handleChange(e.target.value); handleInput(); }}
|
|
443
|
+
onKeyDown={handleKeyDown}
|
|
444
|
+
onPaste={handlePaste}
|
|
445
|
+
onDrop={handleDrop}
|
|
446
|
+
onDragOver={handleDragOver}
|
|
447
|
+
placeholder={isStreaming ? "Follow-up or Stop..." : "Ask anything..."}
|
|
448
|
+
disabled={disabled}
|
|
449
|
+
rows={1}
|
|
450
|
+
className="w-full resize-none bg-transparent px-4 pt-3 pb-1 text-sm text-foreground placeholder:text-text-subtle focus:outline-none disabled:opacity-50 max-h-40"
|
|
451
|
+
/>
|
|
452
|
+
<div className="flex items-center justify-between px-3 pb-2">
|
|
453
|
+
<div className="flex items-center gap-1">
|
|
454
|
+
<button
|
|
455
|
+
type="button"
|
|
456
|
+
onClick={(e) => { e.stopPropagation(); handleAttachClick(); }}
|
|
457
|
+
disabled={disabled}
|
|
458
|
+
className="flex items-center justify-center size-8 rounded-full text-text-subtle hover:text-text-primary hover:bg-surface-elevated transition-colors disabled:opacity-50"
|
|
459
|
+
aria-label="Attach file"
|
|
460
|
+
>
|
|
461
|
+
<Paperclip className="size-4" />
|
|
462
|
+
</button>
|
|
463
|
+
</div>
|
|
464
|
+
<div className="flex items-center gap-1">
|
|
465
|
+
{showCancel ? (
|
|
466
|
+
<button
|
|
467
|
+
onClick={(e) => { e.stopPropagation(); onCancel?.(); }}
|
|
468
|
+
className="flex items-center justify-center size-8 rounded-full bg-red-600 text-white hover:bg-red-500 transition-colors"
|
|
469
|
+
aria-label="Stop response"
|
|
470
|
+
>
|
|
471
|
+
<Square className="size-3.5" />
|
|
472
|
+
</button>
|
|
473
|
+
) : (
|
|
474
|
+
<button
|
|
475
|
+
onClick={(e) => { e.stopPropagation(); handleSend(); }}
|
|
476
|
+
disabled={disabled || !hasContent}
|
|
477
|
+
className="flex items-center justify-center size-8 rounded-full bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
|
|
478
|
+
aria-label="Send message"
|
|
479
|
+
>
|
|
480
|
+
<ArrowUp className="size-4" />
|
|
481
|
+
</button>
|
|
482
|
+
)}
|
|
483
|
+
</div>
|
|
484
|
+
</div>
|
|
485
|
+
</div>
|
|
427
486
|
</div>
|
|
487
|
+
|
|
488
|
+
<input ref={fileInputRef} type="file" multiple className="hidden" onChange={handleFileInputChange} />
|
|
428
489
|
</div>
|
|
429
490
|
);
|
|
430
491
|
}
|