@pheem49/mint 1.8.2 → 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 +297 -107
- 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 +382 -127
- package/crates/mint-cli/src/gmail.rs +22 -2
- package/crates/mint-cli/src/image.rs +8 -8
- package/crates/mint-cli/src/interactive.rs +2888 -0
- package/crates/mint-cli/src/main.rs +391 -2506
- package/crates/mint-cli/src/markdown.rs +294 -0
- package/crates/mint-cli/src/onboard.rs +152 -109
- package/crates/mint-cli/src/setup.rs +143 -143
- 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 +431 -65
- package/crates/mint-core/src/browser.rs +630 -102
- package/crates/mint-core/src/channels.rs +105 -6
- package/crates/mint-core/src/chat.rs +174 -11
- package/crates/mint-core/src/code_tools.rs +47 -31
- package/crates/mint-core/src/config.rs +302 -17
- package/crates/mint-core/src/image_gen.rs +299 -38
- package/crates/mint-core/src/lib.rs +10 -7
- package/crates/mint-core/src/mcp.rs +119 -6
- package/crates/mint-core/src/memory.rs +6 -1
- package/crates/mint-core/src/orchestration.rs +237 -96
- package/crates/mint-core/src/pictures.rs +67 -5
- package/crates/mint-core/src/semantic.rs +1 -1
- package/crates/mint-core/src/shell.rs +1 -1
- package/crates/mint-core/src/skills.rs +128 -25
- package/crates/mint-core/src/video_gen.rs +235 -0
- package/package.json +8 -2
- package/shared-modules/linux-audio/fluidsynth2-static.json +9 -3
- package/shared-modules/linux-audio/fluidsynth2.json +8 -2
- package/shared-modules/linux-audio/lilv.json +26 -19
- package/shared-modules/linux-audio/lv2.json +0 -1
- 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/components/AgentActivityTable.tsx +25 -6
- 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/fileIcons.ts +135 -0
- 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 +486 -69
- 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 +195 -7
- package/src/renderer/src/components/PicturesLibrary.tsx +156 -15
- package/src/renderer/src/components/Settings/AgentsTab.tsx +21 -2
- package/src/renderer/src/components/Settings/AutomationTab.tsx +316 -18
- package/src/renderer/src/components/Settings/GeneralTab.tsx +795 -413
- 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 +106 -3
- package/src/renderer/src/components/VeoStudioPanel.tsx +549 -0
- package/src/renderer/src/components/WorkspacePanel.tsx +7 -125
- 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 +368 -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 +385 -93
- 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 +156 -5
- package/src/renderer/src-web/components/PicturesLibrary.tsx +155 -15
- package/src/renderer/src-web/components/Settings/AgentsTab.tsx +21 -2
- package/src/renderer/src-web/components/Settings/AutomationTab.tsx +316 -18
- package/src/renderer/src-web/components/Settings/GeneralTab.tsx +795 -411
- 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 +110 -3
- 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 +369 -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/desktop.rs +18 -16
- package/src-tauri/src/discord_rpc.rs +1 -1
- package/src-tauri/src/lib.rs +155 -25
- 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
|
|
@@ -55,11 +60,13 @@ interface ChatPanelProps {
|
|
|
55
60
|
welcomeInteraction: any
|
|
56
61
|
onSubmit: (event: FormEvent<HTMLFormElement>) => void
|
|
57
62
|
onSelectImage: (event: ChangeEvent<HTMLInputElement>) => void
|
|
63
|
+
onSelectVideo: (event: ChangeEvent<HTMLInputElement>) => void
|
|
58
64
|
onSelectDocument: (event: ChangeEvent<HTMLInputElement>) => void
|
|
59
65
|
onPasteImage: (clipboardData: DataTransfer) => boolean
|
|
60
66
|
onSetMessage: (message: string) => void
|
|
61
67
|
onSendVoiceMessage: (message: string, audioDataUri?: string | null) => Promise<void>
|
|
62
68
|
onRemoveImage: (idx: number) => void
|
|
69
|
+
onRemoveVideo: (idx: number) => void
|
|
63
70
|
onRemoveDocument: () => void
|
|
64
71
|
onStartWebSearch: () => void
|
|
65
72
|
onCaptureScreen: () => void
|
|
@@ -79,6 +86,7 @@ export default function ChatPanel({
|
|
|
79
86
|
sending,
|
|
80
87
|
sendingMessage,
|
|
81
88
|
sendingImageCount,
|
|
89
|
+
sendingVideoCount,
|
|
82
90
|
streamedReply,
|
|
83
91
|
streamedResponse,
|
|
84
92
|
agentProgress,
|
|
@@ -87,6 +95,7 @@ export default function ChatPanel({
|
|
|
87
95
|
onThinkingExpandedChange,
|
|
88
96
|
message,
|
|
89
97
|
imageAttachments,
|
|
98
|
+
videoAttachments,
|
|
90
99
|
documentName,
|
|
91
100
|
pendingApproval,
|
|
92
101
|
smartContext,
|
|
@@ -96,11 +105,13 @@ export default function ChatPanel({
|
|
|
96
105
|
welcomeInteraction,
|
|
97
106
|
onSubmit,
|
|
98
107
|
onSelectImage,
|
|
108
|
+
onSelectVideo,
|
|
99
109
|
onSelectDocument,
|
|
100
110
|
onPasteImage,
|
|
101
111
|
onSetMessage,
|
|
102
112
|
onSendVoiceMessage,
|
|
103
113
|
onRemoveImage,
|
|
114
|
+
onRemoveVideo,
|
|
104
115
|
onRemoveDocument,
|
|
105
116
|
onStartWebSearch,
|
|
106
117
|
onCaptureScreen,
|
|
@@ -150,8 +161,9 @@ export default function ChatPanel({
|
|
|
150
161
|
}
|
|
151
162
|
fetchOllamaModels();
|
|
152
163
|
}, [status?.activeProvider, settingsConfig?.ollamaHost])
|
|
153
|
-
const canSubmit = Boolean(message.trim() || imageAttachments.length > 0 || documentName)
|
|
164
|
+
const canSubmit = Boolean(message.trim() || imageAttachments.length > 0 || videoAttachments.length > 0 || documentName)
|
|
154
165
|
const sendingImageMarkers = Array.from({ length: sendingImageCount }, (_, index) => `[Image #${index + 1}]`).join(' ')
|
|
166
|
+
const sendingVideoMarkers = Array.from({ length: sendingVideoCount || 0 }, (_, index) => `[Video #${index + 1}]`).join(' ')
|
|
155
167
|
|
|
156
168
|
const getAvailableModels = (provider: string) => {
|
|
157
169
|
switch (provider) {
|
|
@@ -172,8 +184,13 @@ export default function ChatPanel({
|
|
|
172
184
|
case 'ollama':
|
|
173
185
|
return dynamicOllamaModels
|
|
174
186
|
default:
|
|
187
|
+
if (provider.startsWith('custom:')) {
|
|
188
|
+
const id = provider.replace(/^custom:/, '')
|
|
189
|
+
const cp = (settingsConfig?.customProviders ?? []).find(p => p.id === id)
|
|
190
|
+
return cp?.models.map(m => m.modelId) ?? []
|
|
191
|
+
}
|
|
175
192
|
return []
|
|
176
|
-
|
|
193
|
+
}
|
|
177
194
|
}
|
|
178
195
|
|
|
179
196
|
const activeProvider = status?.activeProvider ?? ''
|
|
@@ -199,6 +216,11 @@ export default function ChatPanel({
|
|
|
199
216
|
case 'ollama':
|
|
200
217
|
return settingsConfig.ollamaModel
|
|
201
218
|
default:
|
|
219
|
+
if (provider.startsWith('custom:')) {
|
|
220
|
+
const id = provider.replace(/^custom:/, '')
|
|
221
|
+
const cp = (settingsConfig?.customProviders ?? []).find(p => p.id === id)
|
|
222
|
+
return (settingsConfig.customModelSelections ?? {})[id] ?? cp?.models[0]?.modelId ?? ''
|
|
223
|
+
}
|
|
202
224
|
return ''
|
|
203
225
|
}
|
|
204
226
|
}
|
|
@@ -224,6 +246,22 @@ export default function ChatPanel({
|
|
|
224
246
|
}, [sending])
|
|
225
247
|
|
|
226
248
|
const [speakingText, setSpeakingText] = useState<string | null>(null)
|
|
249
|
+
const [copiedId, setCopiedId] = useState<string | number | null>(null)
|
|
250
|
+
|
|
251
|
+
const handleCopyMessage = async (id: string | number, text: string) => {
|
|
252
|
+
try {
|
|
253
|
+
const cleanText = readableAssistantText(text) || text
|
|
254
|
+
await navigator.clipboard.writeText(cleanText)
|
|
255
|
+
setCopiedId(id)
|
|
256
|
+
setTimeout(() => {
|
|
257
|
+
setCopiedId((current) => (current === id ? null : current))
|
|
258
|
+
}, 2000)
|
|
259
|
+
} catch (err) {
|
|
260
|
+
console.error('Failed to copy message:', err)
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
|
|
227
265
|
const audioRef = useRef<HTMLAudioElement | null>(null)
|
|
228
266
|
const speechRunRef = useRef(0)
|
|
229
267
|
const historyReadyRef = useRef(false)
|
|
@@ -370,14 +408,19 @@ export default function ChatPanel({
|
|
|
370
408
|
}
|
|
371
409
|
}
|
|
372
410
|
if (sending) return
|
|
373
|
-
if (!
|
|
374
|
-
|
|
411
|
+
if (!latest?.aiText || latest.id === lastAutoSpokenIdRef.current) return
|
|
412
|
+
|
|
413
|
+
if (!settingsConfig?.enableVoiceReply) {
|
|
414
|
+
lastAutoSpokenIdRef.current = latest.id
|
|
415
|
+
if (voiceMode) {
|
|
416
|
+
scheduleVoiceListen(350)
|
|
417
|
+
}
|
|
375
418
|
return
|
|
376
419
|
}
|
|
377
|
-
|
|
420
|
+
|
|
378
421
|
lastAutoSpokenIdRef.current = latest.id
|
|
379
422
|
speak(latest.aiText)
|
|
380
|
-
}, [interactions, sending, settingsConfig?.enableVoiceReply])
|
|
423
|
+
}, [interactions, sending, settingsConfig?.enableVoiceReply, voiceMode])
|
|
381
424
|
|
|
382
425
|
// Drag and Drop Zone Overlay
|
|
383
426
|
const [isDragging, setIsDragging] = useState(false)
|
|
@@ -422,6 +465,15 @@ export default function ChatPanel({
|
|
|
422
465
|
const event = { target: input } as ChangeEvent<HTMLInputElement>
|
|
423
466
|
onSelectImage(event)
|
|
424
467
|
}
|
|
468
|
+
} else if (file.type.startsWith('video/')) {
|
|
469
|
+
const input = document.getElementById('video-file-input') as HTMLInputElement | null
|
|
470
|
+
if (input) {
|
|
471
|
+
const dt = new DataTransfer()
|
|
472
|
+
dt.items.add(file)
|
|
473
|
+
input.files = dt.files
|
|
474
|
+
const event = { target: input } as ChangeEvent<HTMLInputElement>
|
|
475
|
+
onSelectVideo(event)
|
|
476
|
+
}
|
|
425
477
|
} else if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
|
|
426
478
|
const input = document.getElementById('document-file-input') as HTMLInputElement | null
|
|
427
479
|
if (input) {
|
|
@@ -434,12 +486,154 @@ export default function ChatPanel({
|
|
|
434
486
|
}
|
|
435
487
|
}
|
|
436
488
|
}
|
|
489
|
+
const [skillsList, setSkillsList] = useState<LearnedSkill[]>([])
|
|
490
|
+
const [slashMenuOpen, setSlashMenuOpen] = useState(true)
|
|
491
|
+
const [slashSelectedIndex, setSlashSelectedIndex] = useState(0)
|
|
492
|
+
const slashMenuRef = useRef<HTMLDivElement>(null)
|
|
493
|
+
const slashListRef = useRef<HTMLDivElement>(null)
|
|
494
|
+
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
|
495
|
+
|
|
496
|
+
const isSlashInput = message.trimStart().startsWith('/')
|
|
497
|
+
const isSkillInput = message.trimStart().startsWith('$')
|
|
498
|
+
const atMatch = message.match(/@([\w\-\.\/]*)$/)
|
|
499
|
+
const isAtInput = Boolean(atMatch)
|
|
500
|
+
const atQuery = atMatch ? atMatch[1].toLowerCase() : ''
|
|
501
|
+
|
|
502
|
+
const CONTEXT_SUGGESTIONS = [
|
|
503
|
+
{ label: '@workspace', desc: 'Include workspace path & context' },
|
|
504
|
+
{ label: '@file', desc: 'Reference workspace file' },
|
|
505
|
+
{ label: '@docs', desc: 'Include documentation context' },
|
|
506
|
+
{ label: '@memory', desc: 'Include long-term memory store' },
|
|
507
|
+
]
|
|
508
|
+
|
|
509
|
+
const filteredContexts = isAtInput
|
|
510
|
+
? CONTEXT_SUGGESTIONS.filter((item) => item.label.toLowerCase().includes(atQuery))
|
|
511
|
+
: []
|
|
512
|
+
|
|
513
|
+
useEffect(() => {
|
|
514
|
+
let isMounted = true
|
|
515
|
+
listLearnedSkills()
|
|
516
|
+
.then((res) => {
|
|
517
|
+
if (isMounted && Array.isArray(res)) setSkillsList(res)
|
|
518
|
+
})
|
|
519
|
+
.catch(() => {})
|
|
520
|
+
return () => { isMounted = false }
|
|
521
|
+
}, [isSkillInput])
|
|
522
|
+
|
|
523
|
+
const slashQuery = isSlashInput ? message.trimStart().toLowerCase() : ''
|
|
524
|
+
const filteredSlashCommands = isSlashInput
|
|
525
|
+
? SLASH_COMMANDS.filter((c) => c.command.toLowerCase().startsWith(slashQuery))
|
|
526
|
+
: []
|
|
527
|
+
|
|
528
|
+
const skillQuery = isSkillInput ? message.trimStart().slice(1).toLowerCase() : ''
|
|
529
|
+
const filteredSkills = isSkillInput
|
|
530
|
+
? skillsList.filter((s) => s.name.toLowerCase().startsWith(skillQuery))
|
|
531
|
+
: []
|
|
532
|
+
|
|
533
|
+
const showSuggestionMenu = slashMenuOpen && (
|
|
534
|
+
(isSlashInput && filteredSlashCommands.length > 0) ||
|
|
535
|
+
(isSkillInput && filteredSkills.length > 0) ||
|
|
536
|
+
(isAtInput && filteredContexts.length > 0)
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
const suggestionCount = isSlashInput
|
|
540
|
+
? filteredSlashCommands.length
|
|
541
|
+
: isSkillInput
|
|
542
|
+
? filteredSkills.length
|
|
543
|
+
: filteredContexts.length
|
|
544
|
+
|
|
545
|
+
useEffect(() => {
|
|
546
|
+
if (isSlashInput || isSkillInput || isAtInput) {
|
|
547
|
+
setSlashMenuOpen(true)
|
|
548
|
+
setSlashSelectedIndex(0)
|
|
549
|
+
}
|
|
550
|
+
}, [message])
|
|
551
|
+
|
|
552
|
+
useEffect(() => {
|
|
553
|
+
if (showSuggestionMenu && slashListRef.current) {
|
|
554
|
+
const activeItem = slashListRef.current.children[slashSelectedIndex] as HTMLElement
|
|
555
|
+
if (activeItem) {
|
|
556
|
+
activeItem.scrollIntoView({ block: 'nearest' })
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}, [slashSelectedIndex, showSuggestionMenu])
|
|
560
|
+
|
|
561
|
+
useEffect(() => {
|
|
562
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
563
|
+
if (
|
|
564
|
+
slashMenuRef.current &&
|
|
565
|
+
!slashMenuRef.current.contains(event.target as Node) &&
|
|
566
|
+
textareaRef.current &&
|
|
567
|
+
!textareaRef.current.contains(event.target as Node)
|
|
568
|
+
) {
|
|
569
|
+
setSlashMenuOpen(false)
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
document.addEventListener('mousedown', handleClickOutside)
|
|
573
|
+
return () => document.removeEventListener('mousedown', handleClickOutside)
|
|
574
|
+
}, [])
|
|
575
|
+
|
|
576
|
+
const selectSlashCommand = (cmd: string) => {
|
|
577
|
+
onSetMessage(cmd + ' ')
|
|
578
|
+
setSlashMenuOpen(false)
|
|
579
|
+
setSlashSelectedIndex(0)
|
|
580
|
+
setTimeout(() => textareaRef.current?.focus(), 0)
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const selectSkillCommand = (name: string) => {
|
|
584
|
+
onSetMessage('$' + name + ' ')
|
|
585
|
+
setSlashMenuOpen(false)
|
|
586
|
+
setSlashSelectedIndex(0)
|
|
587
|
+
setTimeout(() => textareaRef.current?.focus(), 0)
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const selectAtCommand = (label: string) => {
|
|
591
|
+
const nextMsg = message.replace(/@[\w\-\.\/]*$/, label + ' ')
|
|
592
|
+
onSetMessage(nextMsg)
|
|
593
|
+
setSlashMenuOpen(false)
|
|
594
|
+
setSlashSelectedIndex(0)
|
|
595
|
+
setTimeout(() => textareaRef.current?.focus(), 0)
|
|
596
|
+
}
|
|
597
|
+
|
|
437
598
|
const submitOnEnter = (event: KeyboardEvent<HTMLTextAreaElement>) => {
|
|
438
599
|
if (event.key !== 'Enter' || event.shiftKey || event.nativeEvent.isComposing) return
|
|
439
600
|
event.preventDefault()
|
|
440
601
|
event.currentTarget.form?.requestSubmit()
|
|
441
602
|
}
|
|
442
603
|
const handleInputKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
|
|
604
|
+
if (showSuggestionMenu) {
|
|
605
|
+
if (event.key === 'ArrowDown') {
|
|
606
|
+
event.preventDefault()
|
|
607
|
+
setSlashSelectedIndex((prev) => (prev + 1) % suggestionCount)
|
|
608
|
+
return
|
|
609
|
+
}
|
|
610
|
+
if (event.key === 'ArrowUp') {
|
|
611
|
+
event.preventDefault()
|
|
612
|
+
setSlashSelectedIndex((prev) => (prev - 1 + suggestionCount) % suggestionCount)
|
|
613
|
+
return
|
|
614
|
+
}
|
|
615
|
+
if (event.key === 'Enter' || event.key === 'Tab') {
|
|
616
|
+
if (!event.shiftKey && !event.nativeEvent.isComposing) {
|
|
617
|
+
event.preventDefault()
|
|
618
|
+
if (isSlashInput) {
|
|
619
|
+
const selected = filteredSlashCommands[slashSelectedIndex] || filteredSlashCommands[0]
|
|
620
|
+
if (selected) selectSlashCommand(selected.command)
|
|
621
|
+
} else if (isSkillInput) {
|
|
622
|
+
const selected = filteredSkills[slashSelectedIndex] || filteredSkills[0]
|
|
623
|
+
if (selected) selectSkillCommand(selected.name)
|
|
624
|
+
} else if (isAtInput) {
|
|
625
|
+
const selected = filteredContexts[slashSelectedIndex] || filteredContexts[0]
|
|
626
|
+
if (selected) selectAtCommand(selected.label)
|
|
627
|
+
}
|
|
628
|
+
return
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
if (event.key === 'Escape') {
|
|
632
|
+
event.preventDefault()
|
|
633
|
+
setSlashMenuOpen(false)
|
|
634
|
+
return
|
|
635
|
+
}
|
|
636
|
+
}
|
|
443
637
|
submitOnEnter(event)
|
|
444
638
|
}
|
|
445
639
|
const resizeInput = (element: HTMLTextAreaElement) => {
|
|
@@ -485,6 +679,10 @@ export default function ChatPanel({
|
|
|
485
679
|
setToolMenuOpen(false)
|
|
486
680
|
document.getElementById('vision-file-input')?.click()
|
|
487
681
|
}
|
|
682
|
+
const openVideoPicker = () => {
|
|
683
|
+
setToolMenuOpen(false)
|
|
684
|
+
document.getElementById('video-file-input')?.click()
|
|
685
|
+
}
|
|
488
686
|
const openDocumentPicker = () => {
|
|
489
687
|
setToolMenuOpen(false)
|
|
490
688
|
document.getElementById('document-file-input')?.click()
|
|
@@ -810,7 +1008,7 @@ export default function ChatPanel({
|
|
|
810
1008
|
>
|
|
811
1009
|
<div style={{ fontSize: '3.5rem', marginBottom: '16px' }}>🖼️</div>
|
|
812
1010
|
<div style={{ fontSize: '1.25rem', fontWeight: 'bold', letterSpacing: '0.5px' }}>Drag files to attach data</div>
|
|
813
|
-
<div style={{ fontSize: '0.85rem', color: '#94a3b8', marginTop: '8px' }}>Supports images (PNG, JPEG, WebP, GIF) and PDF files</div>
|
|
1011
|
+
<div style={{ fontSize: '0.85rem', color: '#94a3b8', marginTop: '8px' }}>Supports images (PNG, JPEG, WebP, GIF), videos (MP4, WebM, MOV, MKV), and PDF files</div>
|
|
814
1012
|
</div>
|
|
815
1013
|
)}
|
|
816
1014
|
|
|
@@ -818,6 +1016,8 @@ export default function ChatPanel({
|
|
|
818
1016
|
{interactions.map((interaction) => {
|
|
819
1017
|
const isSystemEvent = interaction.provider === 'system' && interaction.model === 'provider_change';
|
|
820
1018
|
if (isSystemEvent) {
|
|
1019
|
+
const rawText = interaction.userText || ''
|
|
1020
|
+
const cleanText = rawText.replace(/^Changed model to\s*/i, '').trim()
|
|
821
1021
|
return (
|
|
822
1022
|
<div key={interaction.id} className="system-event-divider">
|
|
823
1023
|
<div className="system-event-line" />
|
|
@@ -825,7 +1025,7 @@ export default function ChatPanel({
|
|
|
825
1025
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
|
826
1026
|
<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" />
|
|
827
1027
|
</svg>
|
|
828
|
-
<span>{
|
|
1028
|
+
<span>{cleanText}</span>
|
|
829
1029
|
</div>
|
|
830
1030
|
<div className="system-event-line" />
|
|
831
1031
|
</div>
|
|
@@ -837,7 +1037,17 @@ export default function ChatPanel({
|
|
|
837
1037
|
<div className="message user-message">
|
|
838
1038
|
<div className="bubble-wrapper">
|
|
839
1039
|
<div className="message-bubble">{renderFormattedMessage(interaction.userText)}</div>
|
|
840
|
-
<div className="message-time"
|
|
1040
|
+
<div className="message-time" style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
1041
|
+
<span>{new Date(interaction.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</span>
|
|
1042
|
+
<button
|
|
1043
|
+
type="button"
|
|
1044
|
+
className={`msg-action-btn copy-btn ${copiedId === `user-${interaction.id}` ? 'is-copied' : ''}`}
|
|
1045
|
+
onClick={() => handleCopyMessage(`user-${interaction.id}`, interaction.userText)}
|
|
1046
|
+
title={copiedId === `user-${interaction.id}` ? 'คัดลอกแล้ว (Copied!)' : 'คัดลอกข้อความ (Copy)'}
|
|
1047
|
+
>
|
|
1048
|
+
{renderCopyIcon(copiedId === `user-${interaction.id}`)}
|
|
1049
|
+
</button>
|
|
1050
|
+
</div>
|
|
841
1051
|
</div>
|
|
842
1052
|
</div>
|
|
843
1053
|
)}
|
|
@@ -858,32 +1068,30 @@ export default function ChatPanel({
|
|
|
858
1068
|
/>
|
|
859
1069
|
)
|
|
860
1070
|
})()}
|
|
861
|
-
<div className="message-bubble"
|
|
1071
|
+
<div className="message-bubble">{renderFormattedMessage(interaction.aiText)}</div>
|
|
862
1072
|
{renderWebSearchSources(interaction)}
|
|
863
1073
|
<div className="message-time" style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
864
1074
|
<button className="provider-badge">{interaction.provider} • {interaction.model}</button>
|
|
865
1075
|
{fallbackNotice(interaction) && <span className="provider-fallback-notice">{fallbackNotice(interaction)}</span>}
|
|
866
1076
|
<span>{new Date(interaction.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</span>
|
|
867
|
-
<
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
>
|
|
885
|
-
{renderSpeakerIcon(speakingText === interaction.aiText)}
|
|
886
|
-
</button>
|
|
1077
|
+
<div className="message-action-buttons" style={{ display: 'flex', alignItems: 'center', gap: '4px', marginLeft: 'auto' }}>
|
|
1078
|
+
<button
|
|
1079
|
+
type="button"
|
|
1080
|
+
className={`msg-action-btn copy-btn ${copiedId === interaction.id ? 'is-copied' : ''}`}
|
|
1081
|
+
onClick={() => handleCopyMessage(interaction.id, interaction.aiText)}
|
|
1082
|
+
title={copiedId === interaction.id ? 'คัดลอกแล้ว (Copied!)' : 'คัดลอกข้อความ (Copy message)'}
|
|
1083
|
+
>
|
|
1084
|
+
{renderCopyIcon(copiedId === interaction.id)}
|
|
1085
|
+
</button>
|
|
1086
|
+
<button
|
|
1087
|
+
type="button"
|
|
1088
|
+
className={`msg-action-btn tts-btn ${speakingText === interaction.aiText ? 'is-speaking' : ''}`}
|
|
1089
|
+
onClick={() => speak(interaction.aiText)}
|
|
1090
|
+
title={speakingText === interaction.aiText ? 'Stop reading' : 'Read aloud'}
|
|
1091
|
+
>
|
|
1092
|
+
{renderSpeakerIcon(speakingText === interaction.aiText)}
|
|
1093
|
+
</button>
|
|
1094
|
+
</div>
|
|
887
1095
|
</div>
|
|
888
1096
|
</div>
|
|
889
1097
|
</div>
|
|
@@ -893,7 +1101,7 @@ export default function ChatPanel({
|
|
|
893
1101
|
|
|
894
1102
|
{sending && (
|
|
895
1103
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px', marginBottom: '16px' }}>
|
|
896
|
-
<div className="message user-message"><div className="bubble-wrapper"><div className="message-bubble">{
|
|
1104
|
+
<div className="message user-message"><div className="bubble-wrapper"><div className="message-bubble">{renderFormattedMessage([sendingMessage, sendingImageMarkers, sendingVideoMarkers].filter(Boolean).join(' '))}</div></div></div>
|
|
897
1105
|
{agentMode && (
|
|
898
1106
|
<AgentActivityDrawer
|
|
899
1107
|
activityView={agentActivities}
|
|
@@ -952,26 +1160,24 @@ export default function ChatPanel({
|
|
|
952
1160
|
<button className="provider-badge">{badge(streamedResponse.provider, streamedResponse.model)}</button>
|
|
953
1161
|
{activeFallbackNotice && <span className="provider-fallback-notice">{activeFallbackNotice}</span>}
|
|
954
1162
|
{streamedReply && (
|
|
955
|
-
<
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
>
|
|
973
|
-
{renderSpeakerIcon(speakingText === streamedReply)}
|
|
974
|
-
</button>
|
|
1163
|
+
<div className="message-action-buttons" style={{ display: 'flex', alignItems: 'center', gap: '4px', marginLeft: 'auto' }}>
|
|
1164
|
+
<button
|
|
1165
|
+
type="button"
|
|
1166
|
+
className={`msg-action-btn copy-btn ${copiedId === 'live' ? 'is-copied' : ''}`}
|
|
1167
|
+
onClick={() => handleCopyMessage('live', streamedReply)}
|
|
1168
|
+
title={copiedId === 'live' ? 'คัดลอกแล้ว (Copied!)' : 'คัดลอกข้อความ (Copy message)'}
|
|
1169
|
+
>
|
|
1170
|
+
{renderCopyIcon(copiedId === 'live')}
|
|
1171
|
+
</button>
|
|
1172
|
+
<button
|
|
1173
|
+
type="button"
|
|
1174
|
+
className={`msg-action-btn tts-btn ${speakingText === streamedReply ? 'is-speaking' : ''}`}
|
|
1175
|
+
onClick={() => speak(streamedReply)}
|
|
1176
|
+
title={speakingText === streamedReply ? 'Stop reading' : 'Read aloud'}
|
|
1177
|
+
>
|
|
1178
|
+
{renderSpeakerIcon(speakingText === streamedReply)}
|
|
1179
|
+
</button>
|
|
1180
|
+
</div>
|
|
975
1181
|
)}
|
|
976
1182
|
</div>
|
|
977
1183
|
)}
|
|
@@ -1022,6 +1228,65 @@ export default function ChatPanel({
|
|
|
1022
1228
|
</div>
|
|
1023
1229
|
)}
|
|
1024
1230
|
|
|
1231
|
+
{showSuggestionMenu && (
|
|
1232
|
+
<div className="slash-suggestions-popup" ref={slashMenuRef}>
|
|
1233
|
+
<div className="slash-suggestions-header">
|
|
1234
|
+
{isSlashInput ? 'Slash Commands' : isSkillInput ? 'Learned Skills' : 'Context Mentions'} ({slashSelectedIndex + 1}/{suggestionCount})
|
|
1235
|
+
</div>
|
|
1236
|
+
<div className="slash-suggestions-list" ref={slashListRef}>
|
|
1237
|
+
{isSlashInput
|
|
1238
|
+
? filteredSlashCommands.map((item, idx) => (
|
|
1239
|
+
<button
|
|
1240
|
+
key={item.command}
|
|
1241
|
+
type="button"
|
|
1242
|
+
className={`slash-suggestion-item ${idx === slashSelectedIndex ? 'active' : ''}`}
|
|
1243
|
+
onMouseDown={(e) => {
|
|
1244
|
+
e.preventDefault()
|
|
1245
|
+
selectSlashCommand(item.command)
|
|
1246
|
+
}}
|
|
1247
|
+
onMouseEnter={() => setSlashSelectedIndex(idx)}
|
|
1248
|
+
>
|
|
1249
|
+
<span className="slash-cmd-name">{item.command}</span>
|
|
1250
|
+
<span className="slash-cmd-desc">{item.description}</span>
|
|
1251
|
+
</button>
|
|
1252
|
+
))
|
|
1253
|
+
: isSkillInput
|
|
1254
|
+
? filteredSkills.map((item, idx) => (
|
|
1255
|
+
<button
|
|
1256
|
+
key={item.id || item.name}
|
|
1257
|
+
type="button"
|
|
1258
|
+
className={`slash-suggestion-item ${idx === slashSelectedIndex ? 'active' : ''}`}
|
|
1259
|
+
onMouseDown={(e) => {
|
|
1260
|
+
e.preventDefault()
|
|
1261
|
+
selectSkillCommand(item.name)
|
|
1262
|
+
}}
|
|
1263
|
+
onMouseEnter={() => setSlashSelectedIndex(idx)}
|
|
1264
|
+
>
|
|
1265
|
+
<span className="slash-cmd-name">${item.name}</span>
|
|
1266
|
+
<span className="skill-badge">[Skill]</span>
|
|
1267
|
+
<span className="slash-cmd-desc">{item.description || item.content?.slice(0, 60)}</span>
|
|
1268
|
+
</button>
|
|
1269
|
+
))
|
|
1270
|
+
: filteredContexts.map((item, idx) => (
|
|
1271
|
+
<button
|
|
1272
|
+
key={item.label}
|
|
1273
|
+
type="button"
|
|
1274
|
+
className={`slash-suggestion-item ${idx === slashSelectedIndex ? 'active' : ''}`}
|
|
1275
|
+
onMouseDown={(e) => {
|
|
1276
|
+
e.preventDefault()
|
|
1277
|
+
selectAtCommand(item.label)
|
|
1278
|
+
}}
|
|
1279
|
+
onMouseEnter={() => setSlashSelectedIndex(idx)}
|
|
1280
|
+
>
|
|
1281
|
+
<span className="slash-cmd-name">{item.label}</span>
|
|
1282
|
+
<span className="skill-badge">[Context]</span>
|
|
1283
|
+
<span className="slash-cmd-desc">{item.desc}</span>
|
|
1284
|
+
</button>
|
|
1285
|
+
))}
|
|
1286
|
+
</div>
|
|
1287
|
+
</div>
|
|
1288
|
+
)}
|
|
1289
|
+
|
|
1025
1290
|
<form
|
|
1026
1291
|
id="chat-form"
|
|
1027
1292
|
onSubmit={onSubmit}
|
|
@@ -1029,7 +1294,7 @@ export default function ChatPanel({
|
|
|
1029
1294
|
if (onPasteImage(event.clipboardData)) event.preventDefault()
|
|
1030
1295
|
}}
|
|
1031
1296
|
>
|
|
1032
|
-
{(imageAttachments.length > 0 || documentName) && (
|
|
1297
|
+
{(imageAttachments.length > 0 || videoAttachments.length > 0 || documentName) && (
|
|
1033
1298
|
<div className="mint-attachment">
|
|
1034
1299
|
{imageAttachments.map((attachment, idx) => (
|
|
1035
1300
|
<div className="mint-image-attachment" key={idx}>
|
|
@@ -1037,6 +1302,17 @@ export default function ChatPanel({
|
|
|
1037
1302
|
<button className="mint-attachment-remove" type="button" onClick={() => onRemoveImage(idx)} aria-label="Remove image">×</button>
|
|
1038
1303
|
</div>
|
|
1039
1304
|
))}
|
|
1305
|
+
{videoAttachments.map((attachment, idx) => (
|
|
1306
|
+
<div className="mint-image-attachment" key={idx}>
|
|
1307
|
+
<video className="mint-image-preview" src={attachment.dataUri} muted playsInline preload="metadata" />
|
|
1308
|
+
<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' }}>
|
|
1309
|
+
<svg width="10" height="10" viewBox="0 0 24 24" fill="white" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
1310
|
+
<polygon points="5 3 19 12 5 21 5 3"></polygon>
|
|
1311
|
+
</svg>
|
|
1312
|
+
</div>
|
|
1313
|
+
<button className="mint-attachment-remove" type="button" onClick={() => onRemoveVideo(idx)} aria-label="Remove video">×</button>
|
|
1314
|
+
</div>
|
|
1315
|
+
))}
|
|
1040
1316
|
{documentName && (
|
|
1041
1317
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', minWidth: 0 }}>
|
|
1042
1318
|
<span aria-hidden="true" style={{ display: 'inline-flex', alignItems: 'center', color: 'var(--accent)' }}>
|
|
@@ -1055,6 +1331,7 @@ export default function ChatPanel({
|
|
|
1055
1331
|
)}
|
|
1056
1332
|
<textarea
|
|
1057
1333
|
id="chat-input"
|
|
1334
|
+
ref={textareaRef}
|
|
1058
1335
|
value={message}
|
|
1059
1336
|
onChange={(event) => {
|
|
1060
1337
|
resizeInput(event.currentTarget)
|
|
@@ -1065,47 +1342,57 @@ export default function ChatPanel({
|
|
|
1065
1342
|
rows={1}
|
|
1066
1343
|
/>
|
|
1067
1344
|
<div className="chat-tool-menu-wrap" ref={toolMenuRef}>
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
</
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1345
|
+
<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' }}>
|
|
1346
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round">
|
|
1347
|
+
<line x1="12" y1="5" x2="12" y2="19"></line>
|
|
1348
|
+
<line x1="5" y1="12" x2="19" y2="12"></line>
|
|
1349
|
+
</svg>
|
|
1350
|
+
</button>
|
|
1351
|
+
{toolMenuOpen && (
|
|
1352
|
+
<div className="chat-tool-menu" role="menu">
|
|
1353
|
+
<button type="button" role="menuitem" onClick={openImagePicker}>
|
|
1354
|
+
<span aria-hidden="true" style={{ display: 'inline-flex', alignItems: 'center' }}>
|
|
1355
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
|
1356
|
+
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
|
1357
|
+
<circle cx="8.5" cy="8.5" r="1.5"></circle>
|
|
1358
|
+
<polyline points="21 15 16 10 5 21"></polyline>
|
|
1359
|
+
</svg>
|
|
1360
|
+
</span>
|
|
1361
|
+
<span>Add image</span>
|
|
1362
|
+
</button>
|
|
1363
|
+
<button type="button" role="menuitem" onClick={openVideoPicker}>
|
|
1364
|
+
<span aria-hidden="true" style={{ display: 'inline-flex', alignItems: 'center' }}>
|
|
1365
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
|
1366
|
+
<polygon points="23 7 16 12 23 17 23 7"></polygon>
|
|
1367
|
+
<rect x="1" y="5" width="15" height="14" rx="2" ry="2"></rect>
|
|
1368
|
+
</svg>
|
|
1369
|
+
</span>
|
|
1370
|
+
<span>Add video</span>
|
|
1371
|
+
</button>
|
|
1372
|
+
<button type="button" role="menuitem" onClick={openDocumentPicker}>
|
|
1373
|
+
<span aria-hidden="true" style={{ display: 'inline-flex', alignItems: 'center' }}>
|
|
1374
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
|
1375
|
+
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
|
1376
|
+
<polyline points="14 2 14 8 20 8"></polyline>
|
|
1377
|
+
</svg>
|
|
1378
|
+
</span>
|
|
1379
|
+
<span>Add file</span>
|
|
1380
|
+
</button>
|
|
1381
|
+
<button type="button" role="menuitem" onClick={startWebSearch}>
|
|
1382
|
+
<span aria-hidden="true" style={{ display: 'inline-flex', alignItems: 'center' }}>
|
|
1383
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
|
1384
|
+
<circle cx="12" cy="12" r="10"></circle>
|
|
1385
|
+
<line x1="2" y1="12" x2="22" y2="12"></line>
|
|
1386
|
+
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path>
|
|
1387
|
+
</svg>
|
|
1388
|
+
</span>
|
|
1389
|
+
<span>Search web</span>
|
|
1390
|
+
</button>
|
|
1391
|
+
</div>
|
|
1392
|
+
)}
|
|
1393
|
+
</div>
|
|
1108
1394
|
<input id="vision-file-input" type="file" accept="image/png,image/jpeg,image/webp,image/gif" onChange={onSelectImage} style={{ display: 'none' }} />
|
|
1395
|
+
<input id="video-file-input" type="file" accept="video/mp4,video/webm,video/quicktime,video/x-matroska" onChange={onSelectVideo} style={{ display: 'none' }} />
|
|
1109
1396
|
<input id="document-file-input" type="file" accept="application/pdf,.pdf" onChange={onSelectDocument} style={{ display: 'none' }} />
|
|
1110
1397
|
<div className="chat-provider-select" style={{ display: 'flex', gap: '4px', padding: 0, background: 'transparent', border: 0, width: '100%', height: '32px' }}>
|
|
1111
1398
|
<select
|
|
@@ -1137,6 +1424,11 @@ export default function ChatPanel({
|
|
|
1137
1424
|
else if (provider === 'huggingface') displayName = 'HF'
|
|
1138
1425
|
else if (provider === 'local_openai') displayName = 'Local'
|
|
1139
1426
|
else if (provider === 'ollama') displayName = 'Ollama'
|
|
1427
|
+
else if (provider.startsWith('custom:')) {
|
|
1428
|
+
const id = provider.replace(/^custom:/, '')
|
|
1429
|
+
const cp = (settingsConfig?.customProviders ?? []).find(p => p.id === id)
|
|
1430
|
+
displayName = cp?.displayName || id
|
|
1431
|
+
}
|
|
1140
1432
|
return <option key={provider} value={provider}>{displayName}</option>
|
|
1141
1433
|
})}
|
|
1142
1434
|
</select>
|