@pheem49/mint 1.9.0 → 1.10.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/Cargo.lock +55 -3
- package/Cargo.toml +2 -1
- package/Release_Note.md +285 -140
- package/bin/mint +0 -0
- package/crates/mint-cli/Cargo.toml +1 -0
- package/crates/mint-cli/src/actions.rs +233 -0
- package/crates/mint-cli/src/agent.rs +287 -43
- package/crates/mint-cli/src/gmail.rs +22 -2
- package/crates/mint-cli/src/interactive.rs +2888 -0
- package/crates/mint-cli/src/main.rs +336 -2854
- package/crates/mint-cli/src/markdown.rs +294 -0
- package/crates/mint-cli/src/onboard.rs +44 -1
- package/crates/mint-cli/src/updater.rs +48 -6
- package/crates/mint-core/Cargo.toml +1 -0
- package/crates/mint-core/src/agent_loop.rs +1 -0
- package/crates/mint-core/src/api_server.rs +376 -13
- package/crates/mint-core/src/browser.rs +629 -101
- package/crates/mint-core/src/channels.rs +105 -6
- package/crates/mint-core/src/chat.rs +52 -4
- package/crates/mint-core/src/code_tools.rs +29 -11
- package/crates/mint-core/src/config.rs +118 -0
- package/crates/mint-core/src/image_gen.rs +294 -33
- package/crates/mint-core/src/lib.rs +5 -2
- package/crates/mint-core/src/mcp.rs +119 -6
- package/crates/mint-core/src/orchestration.rs +133 -11
- package/crates/mint-core/src/pictures.rs +64 -1
- package/crates/mint-core/src/skills.rs +82 -6
- package/crates/mint-core/src/video_gen.rs +235 -0
- package/package.json +8 -2
- package/shared-modules/luajit/luajit.json +2 -2
- package/skills-lock.json +41 -0
- package/src/renderer/index-web.html +1 -1
- package/src/renderer/index.html +1 -1
- package/src/renderer/shared/constants/slashCommands.ts +27 -0
- package/src/renderer/shared/platform.ts +3 -0
- package/src/renderer/shared/types.ts +4 -0
- package/src/renderer/shared/utils/approval.tsx +86 -7
- package/src/renderer/shared/utils/markdown.tsx +217 -88
- package/src/renderer/shared/utils/slashCommandProcessor.ts +239 -0
- package/src/renderer/src/components/ChatPanel.tsx +461 -64
- package/src/renderer/src/components/DashboardSidebar.tsx +11 -2
- package/src/renderer/src/components/ImageStudioPanel.tsx +87 -4
- package/src/renderer/src/components/MintDashboard.tsx +150 -5
- package/src/renderer/src/components/PicturesLibrary.tsx +156 -15
- package/src/renderer/src/components/Settings/AutomationTab.tsx +316 -18
- package/src/renderer/src/components/Settings/GeneralTab.tsx +762 -634
- package/src/renderer/src/components/Settings/PluginsTab.tsx +304 -70
- package/src/renderer/src/components/Settings/ThemeTab.tsx +6 -1
- package/src/renderer/src/components/SettingsWindow.tsx +38 -2
- package/src/renderer/src/components/VeoStudioPanel.tsx +549 -0
- package/src/renderer/src/css/image-studio.css +106 -0
- package/src/renderer/src/css/settings.css +158 -0
- package/src/renderer/src/css/styles.css +352 -1
- package/src/renderer/src/css/veo-studio.css +626 -0
- package/src/renderer/src/index.css +1 -0
- package/src/renderer/src/tauri.ts +144 -17
- package/src/renderer/src-web/components/ChatPanel.tsx +360 -88
- package/src/renderer/src-web/components/DashboardSidebar.tsx +16 -1
- package/src/renderer/src-web/components/ImageStudioPanel.tsx +87 -4
- package/src/renderer/src-web/components/MintDashboard.tsx +137 -4
- package/src/renderer/src-web/components/PicturesLibrary.tsx +155 -15
- package/src/renderer/src-web/components/Settings/AutomationTab.tsx +316 -18
- package/src/renderer/src-web/components/Settings/GeneralTab.tsx +762 -629
- package/src/renderer/src-web/components/Settings/PluginsTab.tsx +304 -70
- package/src/renderer/src-web/components/Settings/ThemeTab.tsx +6 -1
- package/src/renderer/src-web/components/SettingsWindow.tsx +35 -2
- package/src/renderer/src-web/components/VeoStudioPanel.tsx +549 -0
- package/src/renderer/src-web/css/image-studio.css +106 -0
- package/src/renderer/src-web/css/settings.css +158 -0
- package/src/renderer/src-web/css/styles.css +353 -1
- package/src/renderer/src-web/css/veo-studio.css +650 -0
- package/src/renderer/src-web/index.css +1 -0
- package/src/renderer/src-web/tauri.ts +135 -16
- package/src-tauri/src/lib.rs +114 -8
- package/src-tauri/src/webhooks.rs +45 -31
- package/src-tauri/src/workflows.rs +1 -0
- package/src-tauri/tauri.conf.json +1 -1
- package/temp_gridgeist/AGENTS.md +8 -0
- package/temp_gridgeist/CHANGELOG.md +130 -0
- package/temp_gridgeist/LICENSE +21 -0
- package/temp_gridgeist/README.md +323 -0
- package/temp_gridgeist/README.th.md +325 -0
- package/temp_gridgeist/skills/gridgeist/SKILL.md +88 -0
- package/temp_gridgeist/skills/gridgeist/agents/openai.yaml +9 -0
- package/temp_gridgeist/skills/gridgeist/assets/gridgeist-small.svg +10 -0
- package/temp_gridgeist/skills/gridgeist/assets/gridgeist.png +0 -0
- package/temp_gridgeist/skills/gridgeist/assets/gridgeist.svg +18 -0
- package/temp_gridgeist/skills/gridgeist/references/design-language.md +88 -0
- package/temp_gridgeist/skills/gridgeist/references/review-checklist.md +128 -0
- package/temp_gridgeist/skills/gridgeist/references/system-contract.md +109 -0
|
@@ -12,11 +12,12 @@ import {
|
|
|
12
12
|
} from '../../shared/constants/models'
|
|
13
13
|
import { badge, providerLabel, fallbackNotice } from '../../shared/utils/providers'
|
|
14
14
|
import { activitiesFrom, parseWebSearchSources, type AgentActivity, type AgentActivityView } from '../../shared/utils/agentActivity'
|
|
15
|
+
import { SLASH_COMMANDS } from '../../shared/constants/slashCommands'
|
|
15
16
|
import { AgentActivityTable } from '../../shared/components/AgentActivityTable'
|
|
16
17
|
import { ChatCodeBlock } from '../../shared/components/ChatCodeBlock'
|
|
17
18
|
import { renderApprovalDetails, renderDiff, type ApprovalDetails } from '../../shared/utils/approval'
|
|
18
19
|
import { ApprovalCard } from '../../shared/components/ApprovalCard'
|
|
19
|
-
import { renderFormattedMessage, readableAssistantText, cleanSpeechText, renderSpeakerIcon } from '../../shared/utils/markdown'
|
|
20
|
+
import { renderFormattedMessage, readableAssistantText, cleanSpeechText, renderSpeakerIcon, renderCopyIcon } from '../../shared/utils/markdown'
|
|
20
21
|
import { ThinkingBlock } from '../../shared/components/ThinkingBlock'
|
|
21
22
|
import { AgentActivityDrawer } from '../../shared/components/AgentActivityDrawer'
|
|
22
23
|
import type { DiffHunk, FileChange } from '../../shared/types'
|
|
@@ -26,6 +27,8 @@ import { useSpeechToText } from '../../shared/utils/speech'
|
|
|
26
27
|
|
|
27
28
|
|
|
28
29
|
import {
|
|
30
|
+
listLearnedSkills,
|
|
31
|
+
type LearnedSkill,
|
|
29
32
|
type AgentProgress,
|
|
30
33
|
type ChatResponse,
|
|
31
34
|
type RuntimeStatus,
|
|
@@ -38,6 +41,7 @@ interface ChatPanelProps {
|
|
|
38
41
|
sending: boolean
|
|
39
42
|
sendingMessage: string
|
|
40
43
|
sendingImageCount: number
|
|
44
|
+
sendingVideoCount?: number
|
|
41
45
|
streamedReply: string
|
|
42
46
|
streamedResponse: ChatResponse | null
|
|
43
47
|
agentProgress: AgentProgress[]
|
|
@@ -46,6 +50,7 @@ interface ChatPanelProps {
|
|
|
46
50
|
onThinkingExpandedChange: (key: string, open: boolean) => void
|
|
47
51
|
message: string
|
|
48
52
|
imageAttachments: Array<{ dataUri: string; name: string; previewDataUri?: string }>
|
|
53
|
+
videoAttachments: Array<{ dataUri: string; name: string }>
|
|
49
54
|
documentName: string
|
|
50
55
|
pendingApproval: any | null
|
|
51
56
|
smartContext: boolean
|
|
@@ -56,11 +61,13 @@ interface ChatPanelProps {
|
|
|
56
61
|
welcomeInteraction: any
|
|
57
62
|
onSubmit: (event: FormEvent<HTMLFormElement>) => void
|
|
58
63
|
onSelectImage: (event: ChangeEvent<HTMLInputElement>) => void
|
|
64
|
+
onSelectVideo: (event: ChangeEvent<HTMLInputElement>) => void
|
|
59
65
|
onSelectDocument: (event: ChangeEvent<HTMLInputElement>) => void
|
|
60
66
|
onPasteImage: (clipboardData: DataTransfer) => boolean
|
|
61
67
|
onSetMessage: (message: string) => void
|
|
62
68
|
onSendVoiceMessage: (message: string, audioDataUri?: string | null) => Promise<void>
|
|
63
69
|
onRemoveImage: (idx: number) => void
|
|
70
|
+
onRemoveVideo: (idx: number) => void
|
|
64
71
|
onRemoveDocument: () => void
|
|
65
72
|
onStartWebSearch: () => void
|
|
66
73
|
onCaptureScreen: () => void
|
|
@@ -80,6 +87,7 @@ export default function ChatPanel({
|
|
|
80
87
|
sending,
|
|
81
88
|
sendingMessage,
|
|
82
89
|
sendingImageCount,
|
|
90
|
+
sendingVideoCount,
|
|
83
91
|
streamedReply,
|
|
84
92
|
streamedResponse,
|
|
85
93
|
agentProgress,
|
|
@@ -88,6 +96,7 @@ export default function ChatPanel({
|
|
|
88
96
|
onThinkingExpandedChange,
|
|
89
97
|
message,
|
|
90
98
|
imageAttachments,
|
|
99
|
+
videoAttachments,
|
|
91
100
|
documentName,
|
|
92
101
|
pendingApproval,
|
|
93
102
|
smartContext,
|
|
@@ -98,11 +107,13 @@ export default function ChatPanel({
|
|
|
98
107
|
welcomeInteraction,
|
|
99
108
|
onSubmit,
|
|
100
109
|
onSelectImage,
|
|
110
|
+
onSelectVideo,
|
|
101
111
|
onSelectDocument,
|
|
102
112
|
onPasteImage,
|
|
103
113
|
onSetMessage,
|
|
104
114
|
onSendVoiceMessage,
|
|
105
115
|
onRemoveImage,
|
|
116
|
+
onRemoveVideo,
|
|
106
117
|
onRemoveDocument,
|
|
107
118
|
onStartWebSearch,
|
|
108
119
|
onCaptureScreen,
|
|
@@ -170,6 +181,22 @@ export default function ChatPanel({
|
|
|
170
181
|
return () => clearInterval(timer)
|
|
171
182
|
}, [sending])
|
|
172
183
|
const [speakingText, setSpeakingText] = useState<string | null>(null)
|
|
184
|
+
const [copiedId, setCopiedId] = useState<string | number | null>(null)
|
|
185
|
+
|
|
186
|
+
const handleCopyMessage = async (id: string | number, text: string) => {
|
|
187
|
+
try {
|
|
188
|
+
const cleanText = readableAssistantText(text) || text
|
|
189
|
+
await navigator.clipboard.writeText(cleanText)
|
|
190
|
+
setCopiedId(id)
|
|
191
|
+
setTimeout(() => {
|
|
192
|
+
setCopiedId((current) => (current === id ? null : current))
|
|
193
|
+
}, 2000)
|
|
194
|
+
} catch (err) {
|
|
195
|
+
console.error('Failed to copy message:', err)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
|
|
173
200
|
const toolMenuRef = useRef<HTMLDivElement | null>(null)
|
|
174
201
|
const audioRef = useRef<HTMLAudioElement | null>(null)
|
|
175
202
|
const speechRunRef = useRef(0)
|
|
@@ -178,6 +205,72 @@ export default function ChatPanel({
|
|
|
178
205
|
const lastAutoSpokenIdRef = useRef<number | string | null>(null)
|
|
179
206
|
const speakingRef = useRef<string | null>(null)
|
|
180
207
|
|
|
208
|
+
// Drag and Drop Zone Overlay
|
|
209
|
+
const [isDragging, setIsDragging] = useState(false)
|
|
210
|
+
const dragCounter = useRef(0)
|
|
211
|
+
|
|
212
|
+
const handleDragEnter = (e: DragEvent<HTMLElement>) => {
|
|
213
|
+
e.preventDefault()
|
|
214
|
+
if (e.dataTransfer?.types?.includes('Files')) {
|
|
215
|
+
dragCounter.current++
|
|
216
|
+
setIsDragging(true)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const handleDragLeave = (e: DragEvent<HTMLElement>) => {
|
|
221
|
+
e.preventDefault()
|
|
222
|
+
if (e.dataTransfer?.types?.includes('Files')) {
|
|
223
|
+
dragCounter.current--
|
|
224
|
+
if (dragCounter.current === 0) {
|
|
225
|
+
setIsDragging(false)
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const handleDragOver = (e: DragEvent<HTMLElement>) => {
|
|
231
|
+
e.preventDefault()
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const handleDrop = (e: DragEvent<HTMLElement>) => {
|
|
235
|
+
e.preventDefault()
|
|
236
|
+
dragCounter.current = 0
|
|
237
|
+
setIsDragging(false)
|
|
238
|
+
|
|
239
|
+
const files = e.dataTransfer?.files
|
|
240
|
+
if (files && files.length > 0) {
|
|
241
|
+
const file = files[0]
|
|
242
|
+
if (file.type.startsWith('image/')) {
|
|
243
|
+
const input = document.getElementById('vision-file-input') as HTMLInputElement | null
|
|
244
|
+
if (input) {
|
|
245
|
+
const dt = new DataTransfer()
|
|
246
|
+
dt.items.add(file)
|
|
247
|
+
input.files = dt.files
|
|
248
|
+
const event = { target: input } as ChangeEvent<HTMLInputElement>
|
|
249
|
+
onSelectImage(event)
|
|
250
|
+
}
|
|
251
|
+
} else if (file.type.startsWith('video/')) {
|
|
252
|
+
const input = document.getElementById('video-file-input') as HTMLInputElement | null
|
|
253
|
+
if (input) {
|
|
254
|
+
const dt = new DataTransfer()
|
|
255
|
+
dt.items.add(file)
|
|
256
|
+
input.files = dt.files
|
|
257
|
+
const event = { target: input } as ChangeEvent<HTMLInputElement>
|
|
258
|
+
onSelectVideo(event)
|
|
259
|
+
}
|
|
260
|
+
} else if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
|
|
261
|
+
const input = document.getElementById('document-file-input') as HTMLInputElement | null
|
|
262
|
+
if (input) {
|
|
263
|
+
const dt = new DataTransfer()
|
|
264
|
+
dt.items.add(file)
|
|
265
|
+
input.files = dt.files
|
|
266
|
+
const event = { target: input } as ChangeEvent<HTMLInputElement>
|
|
267
|
+
onSelectDocument(event)
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
|
|
181
274
|
const {
|
|
182
275
|
isRecording,
|
|
183
276
|
voiceMode,
|
|
@@ -200,8 +293,9 @@ export default function ChatPanel({
|
|
|
200
293
|
onSetMessage: (val) => onSetMessage(val)
|
|
201
294
|
})
|
|
202
295
|
|
|
203
|
-
const canSubmit = Boolean(message.trim() || imageAttachments.length > 0 || documentName)
|
|
296
|
+
const canSubmit = Boolean(message.trim() || imageAttachments.length > 0 || videoAttachments.length > 0 || documentName)
|
|
204
297
|
const sendingImageMarkers = Array.from({ length: sendingImageCount }, (_, index) => `[Image #${index + 1}]`).join(' ')
|
|
298
|
+
const sendingVideoMarkers = Array.from({ length: sendingVideoCount || 0 }, (_, index) => `[Video #${index + 1}]`).join(' ')
|
|
205
299
|
const voiceStatus = speakingText ? 'speaking' : (sending || voiceAwaitingResponse) ? 'thinking' : isRecording ? 'listening' : voiceMode ? 'ready' : 'off'
|
|
206
300
|
const voiceStatusLabel = voiceStatus === 'speaking' ? 'Speaking' : voiceStatus === 'thinking' ? 'Thinking' : voiceStatus === 'listening' ? 'Listening' : 'Ready'
|
|
207
301
|
|
|
@@ -268,12 +362,154 @@ export default function ChatPanel({
|
|
|
268
362
|
const workspaceName = workspacePath
|
|
269
363
|
? workspacePath.split(/[\\/]/).filter(Boolean).pop() || workspacePath
|
|
270
364
|
: 'Select Project'
|
|
365
|
+
const [skillsList, setSkillsList] = useState<LearnedSkill[]>([])
|
|
366
|
+
const [slashMenuOpen, setSlashMenuOpen] = useState(true)
|
|
367
|
+
const [slashSelectedIndex, setSlashSelectedIndex] = useState(0)
|
|
368
|
+
const slashMenuRef = useRef<HTMLDivElement>(null)
|
|
369
|
+
const slashListRef = useRef<HTMLDivElement>(null)
|
|
370
|
+
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
|
371
|
+
|
|
372
|
+
const isSlashInput = message.trimStart().startsWith('/')
|
|
373
|
+
const isSkillInput = message.trimStart().startsWith('$')
|
|
374
|
+
const atMatch = message.match(/@([\w\-\.\/]*)$/)
|
|
375
|
+
const isAtInput = Boolean(atMatch)
|
|
376
|
+
const atQuery = atMatch ? atMatch[1].toLowerCase() : ''
|
|
377
|
+
|
|
378
|
+
const CONTEXT_SUGGESTIONS = [
|
|
379
|
+
{ label: '@workspace', desc: 'Include workspace path & context' },
|
|
380
|
+
{ label: '@file', desc: 'Reference workspace file' },
|
|
381
|
+
{ label: '@docs', desc: 'Include documentation context' },
|
|
382
|
+
{ label: '@memory', desc: 'Include long-term memory store' },
|
|
383
|
+
]
|
|
384
|
+
|
|
385
|
+
const filteredContexts = isAtInput
|
|
386
|
+
? CONTEXT_SUGGESTIONS.filter((item) => item.label.toLowerCase().includes(atQuery))
|
|
387
|
+
: []
|
|
388
|
+
|
|
389
|
+
useEffect(() => {
|
|
390
|
+
let isMounted = true
|
|
391
|
+
listLearnedSkills(workspacePath)
|
|
392
|
+
.then((res) => {
|
|
393
|
+
if (isMounted && Array.isArray(res)) setSkillsList(res)
|
|
394
|
+
})
|
|
395
|
+
.catch(() => {})
|
|
396
|
+
return () => { isMounted = false }
|
|
397
|
+
}, [workspacePath, isSkillInput])
|
|
398
|
+
|
|
399
|
+
const slashQuery = isSlashInput ? message.trimStart().toLowerCase() : ''
|
|
400
|
+
const filteredSlashCommands = isSlashInput
|
|
401
|
+
? SLASH_COMMANDS.filter((c) => c.command.toLowerCase().startsWith(slashQuery))
|
|
402
|
+
: []
|
|
403
|
+
|
|
404
|
+
const skillQuery = isSkillInput ? message.trimStart().slice(1).toLowerCase() : ''
|
|
405
|
+
const filteredSkills = isSkillInput
|
|
406
|
+
? skillsList.filter((s) => s.name.toLowerCase().startsWith(skillQuery))
|
|
407
|
+
: []
|
|
408
|
+
|
|
409
|
+
const showSuggestionMenu = slashMenuOpen && (
|
|
410
|
+
(isSlashInput && filteredSlashCommands.length > 0) ||
|
|
411
|
+
(isSkillInput && filteredSkills.length > 0) ||
|
|
412
|
+
(isAtInput && filteredContexts.length > 0)
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
const suggestionCount = isSlashInput
|
|
416
|
+
? filteredSlashCommands.length
|
|
417
|
+
: isSkillInput
|
|
418
|
+
? filteredSkills.length
|
|
419
|
+
: filteredContexts.length
|
|
420
|
+
|
|
421
|
+
useEffect(() => {
|
|
422
|
+
if (isSlashInput || isSkillInput || isAtInput) {
|
|
423
|
+
setSlashMenuOpen(true)
|
|
424
|
+
setSlashSelectedIndex(0)
|
|
425
|
+
}
|
|
426
|
+
}, [message])
|
|
427
|
+
|
|
428
|
+
useEffect(() => {
|
|
429
|
+
if (showSuggestionMenu && slashListRef.current) {
|
|
430
|
+
const activeItem = slashListRef.current.children[slashSelectedIndex] as HTMLElement
|
|
431
|
+
if (activeItem) {
|
|
432
|
+
activeItem.scrollIntoView({ block: 'nearest' })
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}, [slashSelectedIndex, showSuggestionMenu])
|
|
436
|
+
|
|
437
|
+
useEffect(() => {
|
|
438
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
439
|
+
if (
|
|
440
|
+
slashMenuRef.current &&
|
|
441
|
+
!slashMenuRef.current.contains(event.target as Node) &&
|
|
442
|
+
textareaRef.current &&
|
|
443
|
+
!textareaRef.current.contains(event.target as Node)
|
|
444
|
+
) {
|
|
445
|
+
setSlashMenuOpen(false)
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
document.addEventListener('mousedown', handleClickOutside)
|
|
449
|
+
return () => document.removeEventListener('mousedown', handleClickOutside)
|
|
450
|
+
}, [])
|
|
451
|
+
|
|
452
|
+
const selectSlashCommand = (cmd: string) => {
|
|
453
|
+
onSetMessage(cmd + ' ')
|
|
454
|
+
setSlashMenuOpen(false)
|
|
455
|
+
setSlashSelectedIndex(0)
|
|
456
|
+
setTimeout(() => textareaRef.current?.focus(), 0)
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const selectSkillCommand = (name: string) => {
|
|
460
|
+
onSetMessage('$' + name + ' ')
|
|
461
|
+
setSlashMenuOpen(false)
|
|
462
|
+
setSlashSelectedIndex(0)
|
|
463
|
+
setTimeout(() => textareaRef.current?.focus(), 0)
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const selectAtCommand = (label: string) => {
|
|
467
|
+
const nextMsg = message.replace(/@[\w\-\.\/]*$/, label + ' ')
|
|
468
|
+
onSetMessage(nextMsg)
|
|
469
|
+
setSlashMenuOpen(false)
|
|
470
|
+
setSlashSelectedIndex(0)
|
|
471
|
+
setTimeout(() => textareaRef.current?.focus(), 0)
|
|
472
|
+
}
|
|
473
|
+
|
|
271
474
|
const submitOnEnter = (event: KeyboardEvent<HTMLTextAreaElement>) => {
|
|
272
475
|
if (event.key !== 'Enter' || event.shiftKey || event.nativeEvent.isComposing) return
|
|
273
476
|
event.preventDefault()
|
|
274
477
|
event.currentTarget.form?.requestSubmit()
|
|
275
478
|
}
|
|
276
479
|
const handleInputKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
|
|
480
|
+
if (showSuggestionMenu) {
|
|
481
|
+
if (event.key === 'ArrowDown') {
|
|
482
|
+
event.preventDefault()
|
|
483
|
+
setSlashSelectedIndex((prev) => (prev + 1) % suggestionCount)
|
|
484
|
+
return
|
|
485
|
+
}
|
|
486
|
+
if (event.key === 'ArrowUp') {
|
|
487
|
+
event.preventDefault()
|
|
488
|
+
setSlashSelectedIndex((prev) => (prev - 1 + suggestionCount) % suggestionCount)
|
|
489
|
+
return
|
|
490
|
+
}
|
|
491
|
+
if (event.key === 'Enter' || event.key === 'Tab') {
|
|
492
|
+
if (!event.shiftKey && !event.nativeEvent.isComposing) {
|
|
493
|
+
event.preventDefault()
|
|
494
|
+
if (isSlashInput) {
|
|
495
|
+
const selected = filteredSlashCommands[slashSelectedIndex] || filteredSlashCommands[0]
|
|
496
|
+
if (selected) selectSlashCommand(selected.command)
|
|
497
|
+
} else if (isSkillInput) {
|
|
498
|
+
const selected = filteredSkills[slashSelectedIndex] || filteredSkills[0]
|
|
499
|
+
if (selected) selectSkillCommand(selected.name)
|
|
500
|
+
} else if (isAtInput) {
|
|
501
|
+
const selected = filteredContexts[slashSelectedIndex] || filteredContexts[0]
|
|
502
|
+
if (selected) selectAtCommand(selected.label)
|
|
503
|
+
}
|
|
504
|
+
return
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
if (event.key === 'Escape') {
|
|
508
|
+
event.preventDefault()
|
|
509
|
+
setSlashMenuOpen(false)
|
|
510
|
+
return
|
|
511
|
+
}
|
|
512
|
+
}
|
|
277
513
|
submitOnEnter(event)
|
|
278
514
|
}
|
|
279
515
|
|
|
@@ -444,6 +680,10 @@ export default function ChatPanel({
|
|
|
444
680
|
setToolMenuOpen(false)
|
|
445
681
|
document.getElementById('vision-file-input')?.click()
|
|
446
682
|
}
|
|
683
|
+
const openVideoPicker = () => {
|
|
684
|
+
setToolMenuOpen(false)
|
|
685
|
+
document.getElementById('video-file-input')?.click()
|
|
686
|
+
}
|
|
447
687
|
const openDocumentPicker = () => {
|
|
448
688
|
setToolMenuOpen(false)
|
|
449
689
|
document.getElementById('document-file-input')?.click()
|
|
@@ -730,7 +970,46 @@ export default function ChatPanel({
|
|
|
730
970
|
}
|
|
731
971
|
|
|
732
972
|
return (
|
|
733
|
-
<section
|
|
973
|
+
<section
|
|
974
|
+
className={`conversation-panel ${isEmptyChat ? 'is-empty' : ''}`}
|
|
975
|
+
onDragEnter={handleDragEnter}
|
|
976
|
+
onDragOver={handleDragOver}
|
|
977
|
+
onDragLeave={handleDragLeave}
|
|
978
|
+
onDrop={handleDrop}
|
|
979
|
+
style={{ position: 'relative' }}
|
|
980
|
+
>
|
|
981
|
+
{isDragging && (
|
|
982
|
+
<div
|
|
983
|
+
className="drag-drop-overlay"
|
|
984
|
+
onDragEnter={handleDragEnter}
|
|
985
|
+
onDragLeave={handleDragLeave}
|
|
986
|
+
onDragOver={handleDragOver}
|
|
987
|
+
onDrop={handleDrop}
|
|
988
|
+
style={{
|
|
989
|
+
position: 'absolute',
|
|
990
|
+
top: 0,
|
|
991
|
+
left: 0,
|
|
992
|
+
right: 0,
|
|
993
|
+
bottom: 0,
|
|
994
|
+
background: 'rgba(15, 23, 42, 0.82)',
|
|
995
|
+
backdropFilter: 'blur(8px)',
|
|
996
|
+
border: '2px dashed var(--accent)',
|
|
997
|
+
borderRadius: '16px',
|
|
998
|
+
margin: '12px',
|
|
999
|
+
display: 'flex',
|
|
1000
|
+
flexDirection: 'column',
|
|
1001
|
+
alignItems: 'center',
|
|
1002
|
+
justifyContent: 'center',
|
|
1003
|
+
color: 'white',
|
|
1004
|
+
zIndex: 1000,
|
|
1005
|
+
pointerEvents: 'auto',
|
|
1006
|
+
}}
|
|
1007
|
+
>
|
|
1008
|
+
<div style={{ fontSize: '3.5rem', marginBottom: '16px' }}>🖼️</div>
|
|
1009
|
+
<div style={{ fontSize: '1.25rem', fontWeight: 'bold', letterSpacing: '0.5px' }}>Drag files to attach data</div>
|
|
1010
|
+
<div style={{ fontSize: '0.85rem', color: '#94a3b8', marginTop: '8px' }}>Supports images (PNG, JPEG, WebP, GIF), videos (MP4, WebM, MOV, MKV), and PDF files</div>
|
|
1011
|
+
</div>
|
|
1012
|
+
)}
|
|
734
1013
|
<div className="chat-header">
|
|
735
1014
|
<div className="chat-header-title">
|
|
736
1015
|
<img src="./assets/icon.png" alt="Logo" className="chat-header-logo" />
|
|
@@ -741,6 +1020,8 @@ export default function ChatPanel({
|
|
|
741
1020
|
{interactions.map((interaction) => {
|
|
742
1021
|
const isSystemEvent = interaction.provider === 'system' && interaction.model === 'provider_change';
|
|
743
1022
|
if (isSystemEvent) {
|
|
1023
|
+
const rawText = interaction.userText || ''
|
|
1024
|
+
const cleanText = rawText.replace(/^Changed model to\s*/i, '').trim()
|
|
744
1025
|
return (
|
|
745
1026
|
<div key={interaction.id} className="system-event-divider">
|
|
746
1027
|
<div className="system-event-line" />
|
|
@@ -748,7 +1029,7 @@ export default function ChatPanel({
|
|
|
748
1029
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
|
749
1030
|
<path d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z" />
|
|
750
1031
|
</svg>
|
|
751
|
-
<span>{
|
|
1032
|
+
<span>{cleanText}</span>
|
|
752
1033
|
</div>
|
|
753
1034
|
<div className="system-event-line" />
|
|
754
1035
|
</div>
|
|
@@ -760,7 +1041,17 @@ export default function ChatPanel({
|
|
|
760
1041
|
<div className="message user-message">
|
|
761
1042
|
<div className="bubble-wrapper">
|
|
762
1043
|
<div className="message-bubble">{renderFormattedMessage(interaction.userText)}</div>
|
|
763
|
-
<div className="message-time"
|
|
1044
|
+
<div className="message-time" style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
1045
|
+
<span>{new Date(interaction.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</span>
|
|
1046
|
+
<button
|
|
1047
|
+
type="button"
|
|
1048
|
+
className={`msg-action-btn copy-btn ${copiedId === `user-${interaction.id}` ? 'is-copied' : ''}`}
|
|
1049
|
+
onClick={() => handleCopyMessage(`user-${interaction.id}`, interaction.userText)}
|
|
1050
|
+
title={copiedId === `user-${interaction.id}` ? 'คัดลอกแล้ว (Copied!)' : 'คัดลอกข้อความ (Copy)'}
|
|
1051
|
+
>
|
|
1052
|
+
{renderCopyIcon(copiedId === `user-${interaction.id}`)}
|
|
1053
|
+
</button>
|
|
1054
|
+
</div>
|
|
764
1055
|
</div>
|
|
765
1056
|
</div>
|
|
766
1057
|
)}
|
|
@@ -781,20 +1072,30 @@ export default function ChatPanel({
|
|
|
781
1072
|
/>
|
|
782
1073
|
)
|
|
783
1074
|
})()}
|
|
784
|
-
<div className="message-bubble"
|
|
1075
|
+
<div className="message-bubble">{renderFormattedMessage(interaction.aiText)}</div>
|
|
785
1076
|
{renderWebSearchSources(interaction)}
|
|
786
1077
|
<div className="message-time" style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
787
1078
|
<button className="provider-badge">{interaction.provider} • {interaction.model}</button>
|
|
788
1079
|
{fallbackNotice(interaction) && <span className="provider-fallback-notice">{fallbackNotice(interaction)}</span>}
|
|
789
1080
|
<span>{new Date(interaction.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</span>
|
|
790
|
-
<
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
1081
|
+
<div className="message-action-buttons" style={{ display: 'flex', alignItems: 'center', gap: '4px', marginLeft: 'auto' }}>
|
|
1082
|
+
<button
|
|
1083
|
+
type="button"
|
|
1084
|
+
className={`msg-action-btn copy-btn ${copiedId === interaction.id ? 'is-copied' : ''}`}
|
|
1085
|
+
onClick={() => handleCopyMessage(interaction.id, interaction.aiText)}
|
|
1086
|
+
title={copiedId === interaction.id ? 'คัดลอกแล้ว (Copied!)' : 'คัดลอกข้อความ (Copy message)'}
|
|
1087
|
+
>
|
|
1088
|
+
{renderCopyIcon(copiedId === interaction.id)}
|
|
1089
|
+
</button>
|
|
1090
|
+
<button
|
|
1091
|
+
type="button"
|
|
1092
|
+
className={`msg-action-btn tts-btn ${speakingText === interaction.aiText ? 'is-speaking' : ''}`}
|
|
1093
|
+
onClick={() => speak(interaction.aiText)}
|
|
1094
|
+
title={speakingText === interaction.aiText ? 'Stop reading' : 'Read aloud'}
|
|
1095
|
+
>
|
|
1096
|
+
{renderSpeakerIcon(speakingText === interaction.aiText)}
|
|
1097
|
+
</button>
|
|
1098
|
+
</div>
|
|
798
1099
|
</div>
|
|
799
1100
|
</div>
|
|
800
1101
|
</div>
|
|
@@ -804,7 +1105,7 @@ export default function ChatPanel({
|
|
|
804
1105
|
|
|
805
1106
|
{sending && (
|
|
806
1107
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px', marginBottom: '16px' }}>
|
|
807
|
-
<div className="message user-message"><div className="bubble-wrapper"><div className="message-bubble">{
|
|
1108
|
+
<div className="message user-message"><div className="bubble-wrapper"><div className="message-bubble">{renderFormattedMessage([sendingMessage, sendingImageMarkers, sendingVideoMarkers].filter(Boolean).join(' '))}</div></div></div>
|
|
808
1109
|
{agentMode && (
|
|
809
1110
|
<AgentActivityDrawer
|
|
810
1111
|
activityView={agentActivities}
|
|
@@ -863,14 +1164,24 @@ export default function ChatPanel({
|
|
|
863
1164
|
<button className="provider-badge">{badge(streamedResponse.provider, streamedResponse.model)}</button>
|
|
864
1165
|
{activeFallbackNotice && <span className="provider-fallback-notice">{activeFallbackNotice}</span>}
|
|
865
1166
|
{streamedReply && (
|
|
866
|
-
<
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
1167
|
+
<div className="message-action-buttons" style={{ display: 'flex', alignItems: 'center', gap: '4px', marginLeft: 'auto' }}>
|
|
1168
|
+
<button
|
|
1169
|
+
type="button"
|
|
1170
|
+
className={`msg-action-btn copy-btn ${copiedId === 'live' ? 'is-copied' : ''}`}
|
|
1171
|
+
onClick={() => handleCopyMessage('live', streamedReply)}
|
|
1172
|
+
title={copiedId === 'live' ? 'คัดลอกแล้ว (Copied!)' : 'คัดลอกข้อความ (Copy message)'}
|
|
1173
|
+
>
|
|
1174
|
+
{renderCopyIcon(copiedId === 'live')}
|
|
1175
|
+
</button>
|
|
1176
|
+
<button
|
|
1177
|
+
type="button"
|
|
1178
|
+
className={`msg-action-btn tts-btn ${speakingText === streamedReply ? 'is-speaking' : ''}`}
|
|
1179
|
+
onClick={() => speak(streamedReply)}
|
|
1180
|
+
title={speakingText === streamedReply ? 'Stop reading' : 'Read aloud'}
|
|
1181
|
+
>
|
|
1182
|
+
{renderSpeakerIcon(speakingText === streamedReply)}
|
|
1183
|
+
</button>
|
|
1184
|
+
</div>
|
|
874
1185
|
)}
|
|
875
1186
|
</div>
|
|
876
1187
|
)}
|
|
@@ -937,6 +1248,65 @@ export default function ChatPanel({
|
|
|
937
1248
|
</div>
|
|
938
1249
|
)}
|
|
939
1250
|
|
|
1251
|
+
{showSuggestionMenu && (
|
|
1252
|
+
<div className="slash-suggestions-popup" ref={slashMenuRef}>
|
|
1253
|
+
<div className="slash-suggestions-header">
|
|
1254
|
+
{isSlashInput ? 'Slash Commands' : isSkillInput ? 'Learned Skills' : 'Context Mentions'} ({slashSelectedIndex + 1}/{suggestionCount})
|
|
1255
|
+
</div>
|
|
1256
|
+
<div className="slash-suggestions-list" ref={slashListRef}>
|
|
1257
|
+
{isSlashInput
|
|
1258
|
+
? filteredSlashCommands.map((item, idx) => (
|
|
1259
|
+
<button
|
|
1260
|
+
key={item.command}
|
|
1261
|
+
type="button"
|
|
1262
|
+
className={`slash-suggestion-item ${idx === slashSelectedIndex ? 'active' : ''}`}
|
|
1263
|
+
onMouseDown={(e) => {
|
|
1264
|
+
e.preventDefault()
|
|
1265
|
+
selectSlashCommand(item.command)
|
|
1266
|
+
}}
|
|
1267
|
+
onMouseEnter={() => setSlashSelectedIndex(idx)}
|
|
1268
|
+
>
|
|
1269
|
+
<span className="slash-cmd-name">{item.command}</span>
|
|
1270
|
+
<span className="slash-cmd-desc">{item.description}</span>
|
|
1271
|
+
</button>
|
|
1272
|
+
))
|
|
1273
|
+
: isSkillInput
|
|
1274
|
+
? filteredSkills.map((item, idx) => (
|
|
1275
|
+
<button
|
|
1276
|
+
key={item.id || item.name}
|
|
1277
|
+
type="button"
|
|
1278
|
+
className={`slash-suggestion-item ${idx === slashSelectedIndex ? 'active' : ''}`}
|
|
1279
|
+
onMouseDown={(e) => {
|
|
1280
|
+
e.preventDefault()
|
|
1281
|
+
selectSkillCommand(item.name)
|
|
1282
|
+
}}
|
|
1283
|
+
onMouseEnter={() => setSlashSelectedIndex(idx)}
|
|
1284
|
+
>
|
|
1285
|
+
<span className="slash-cmd-name">${item.name}</span>
|
|
1286
|
+
<span className="skill-badge">[Skill]</span>
|
|
1287
|
+
<span className="slash-cmd-desc">{item.description || item.content?.slice(0, 60)}</span>
|
|
1288
|
+
</button>
|
|
1289
|
+
))
|
|
1290
|
+
: filteredContexts.map((item, idx) => (
|
|
1291
|
+
<button
|
|
1292
|
+
key={item.label}
|
|
1293
|
+
type="button"
|
|
1294
|
+
className={`slash-suggestion-item ${idx === slashSelectedIndex ? 'active' : ''}`}
|
|
1295
|
+
onMouseDown={(e) => {
|
|
1296
|
+
e.preventDefault()
|
|
1297
|
+
selectAtCommand(item.label)
|
|
1298
|
+
}}
|
|
1299
|
+
onMouseEnter={() => setSlashSelectedIndex(idx)}
|
|
1300
|
+
>
|
|
1301
|
+
<span className="slash-cmd-name">{item.label}</span>
|
|
1302
|
+
<span className="skill-badge">[Context]</span>
|
|
1303
|
+
<span className="slash-cmd-desc">{item.desc}</span>
|
|
1304
|
+
</button>
|
|
1305
|
+
))}
|
|
1306
|
+
</div>
|
|
1307
|
+
</div>
|
|
1308
|
+
)}
|
|
1309
|
+
|
|
940
1310
|
<form
|
|
941
1311
|
id="chat-form"
|
|
942
1312
|
onSubmit={onSubmit}
|
|
@@ -951,7 +1321,7 @@ export default function ChatPanel({
|
|
|
951
1321
|
if (onPasteImage(event.clipboardData)) event.preventDefault()
|
|
952
1322
|
}}
|
|
953
1323
|
>
|
|
954
|
-
{(imageAttachments.length > 0 || documentName) && (
|
|
1324
|
+
{(imageAttachments.length > 0 || videoAttachments.length > 0 || documentName) && (
|
|
955
1325
|
<div className="mint-attachment">
|
|
956
1326
|
{imageAttachments.map((attachment, idx) => (
|
|
957
1327
|
<div className="mint-image-attachment" key={idx}>
|
|
@@ -964,6 +1334,22 @@ export default function ChatPanel({
|
|
|
964
1334
|
</button>
|
|
965
1335
|
</div>
|
|
966
1336
|
))}
|
|
1337
|
+
{videoAttachments.map((attachment, idx) => (
|
|
1338
|
+
<div className="mint-image-attachment" key={idx}>
|
|
1339
|
+
<video className="mint-image-preview" src={attachment.dataUri} muted playsInline preload="metadata" />
|
|
1340
|
+
<div className="mint-video-play-indicator" style={{ position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', pointerEvents: 'none', background: 'rgba(0,0,0,0.6)', borderRadius: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center', width: '22px', height: '22px', border: '1.5px solid white' }}>
|
|
1341
|
+
<svg width="10" height="10" viewBox="0 0 24 24" fill="white" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
1342
|
+
<polygon points="5 3 19 12 5 21 5 3"></polygon>
|
|
1343
|
+
</svg>
|
|
1344
|
+
</div>
|
|
1345
|
+
<button className="mint-attachment-remove" type="button" onClick={() => onRemoveVideo(idx)} aria-label="Remove video">
|
|
1346
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
|
1347
|
+
<line x1="18" y1="6" x2="6" y2="18"></line>
|
|
1348
|
+
<line x1="6" y1="6" x2="18" y2="18"></line>
|
|
1349
|
+
</svg>
|
|
1350
|
+
</button>
|
|
1351
|
+
</div>
|
|
1352
|
+
))}
|
|
967
1353
|
{documentName && (
|
|
968
1354
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', minWidth: 0 }}>
|
|
969
1355
|
<span aria-hidden="true" style={{ display: 'inline-flex', alignItems: 'center', color: 'var(--text-soft)' }}>
|
|
@@ -974,7 +1360,7 @@ export default function ChatPanel({
|
|
|
974
1360
|
</span>
|
|
975
1361
|
<span style={{ fontSize: '0.76rem', color: 'var(--text-soft)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: '220px' }}>{documentName}</span>
|
|
976
1362
|
<button type="button" onClick={onRemoveDocument} style={{ background: 'transparent', border: 0, color: '#ef4444', cursor: 'pointer', display: 'inline-flex', alignItems: 'center' }}>
|
|
977
|
-
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
1363
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
|
978
1364
|
<line x1="18" y1="6" x2="6" y2="18"></line>
|
|
979
1365
|
<line x1="6" y1="6" x2="18" y2="18"></line>
|
|
980
1366
|
</svg>
|
|
@@ -985,6 +1371,7 @@ export default function ChatPanel({
|
|
|
985
1371
|
)}
|
|
986
1372
|
<textarea
|
|
987
1373
|
id="chat-input"
|
|
1374
|
+
ref={textareaRef}
|
|
988
1375
|
value={message}
|
|
989
1376
|
onChange={(event) => {
|
|
990
1377
|
resizeInput(event.currentTarget)
|
|
@@ -996,46 +1383,56 @@ export default function ChatPanel({
|
|
|
996
1383
|
rows={1}
|
|
997
1384
|
/>
|
|
998
1385
|
<div className="chat-tool-menu-wrap" ref={toolMenuRef}>
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1386
|
+
<button id="chat-tool-btn" type="button" aria-haspopup="menu" aria-expanded={toolMenuOpen} onClick={() => setToolMenuOpen((open) => !open)} style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>
|
|
1387
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
|
1388
|
+
<line x1="12" y1="5" x2="12" y2="19"></line>
|
|
1389
|
+
<line x1="5" y1="12" x2="19" y2="12"></line>
|
|
1390
|
+
</svg>
|
|
1391
|
+
</button>
|
|
1392
|
+
{toolMenuOpen && (
|
|
1393
|
+
<div className="chat-tool-menu" role="menu">
|
|
1394
|
+
<button type="button" role="menuitem" onClick={openImagePicker} style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
1395
|
+
<span aria-hidden="true" style={{ display: 'inline-flex', alignItems: 'center' }}>
|
|
1396
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
|
1397
|
+
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
|
1398
|
+
<circle cx="8.5" cy="8.5" r="1.5"></circle>
|
|
1399
|
+
<polyline points="21 15 16 10 5 21"></polyline>
|
|
1400
|
+
</svg>
|
|
1401
|
+
</span>
|
|
1402
|
+
<span>Add image</span>
|
|
1403
|
+
</button>
|
|
1404
|
+
<button type="button" role="menuitem" onClick={openVideoPicker} style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
1405
|
+
<span aria-hidden="true" style={{ display: 'inline-flex', alignItems: 'center' }}>
|
|
1406
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
|
1407
|
+
<polygon points="23 7 16 12 23 17 23 7"></polygon>
|
|
1408
|
+
<rect x="1" y="5" width="15" height="14" rx="2" ry="2"></rect>
|
|
1409
|
+
</svg>
|
|
1410
|
+
</span>
|
|
1411
|
+
<span>Add video</span>
|
|
1412
|
+
</button>
|
|
1413
|
+
<button type="button" role="menuitem" onClick={openDocumentPicker} style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
1414
|
+
<span aria-hidden="true" style={{ display: 'inline-flex', alignItems: 'center' }}>
|
|
1415
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
|
1416
|
+
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
|
1417
|
+
<polyline points="14 2 14 8 20 8"></polyline>
|
|
1418
|
+
</svg>
|
|
1419
|
+
</span>
|
|
1420
|
+
<span>Add file</span>
|
|
1421
|
+
</button>
|
|
1422
|
+
<button type="button" role="menuitem" onClick={startWebSearch} style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
1423
|
+
<span aria-hidden="true" style={{ display: 'inline-flex', alignItems: 'center' }}>
|
|
1424
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
|
1425
|
+
<circle cx="11" cy="11" r="8"></circle>
|
|
1426
|
+
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
|
1427
|
+
</svg>
|
|
1428
|
+
</span>
|
|
1429
|
+
<span>Search web</span>
|
|
1430
|
+
</button>
|
|
1431
|
+
</div>
|
|
1432
|
+
)}
|
|
1433
|
+
</div>
|
|
1038
1434
|
<input id="vision-file-input" type="file" accept="image/png,image/jpeg,image/webp,image/gif" onChange={onSelectImage} style={{ display: 'none' }} />
|
|
1435
|
+
<input id="video-file-input" type="file" accept="video/mp4,video/webm,video/quicktime,video/x-matroska" onChange={onSelectVideo} style={{ display: 'none' }} />
|
|
1039
1436
|
<input id="document-file-input" type="file" accept="application/pdf,.pdf" onChange={onSelectDocument} style={{ display: 'none' }} />
|
|
1040
1437
|
<button id="screen-capture-btn" type="button" onClick={onCaptureScreen} aria-label="Capture screen">
|
|
1041
1438
|
<span className="screen-capture-eye" aria-hidden="true" />
|