@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
|
|
@@ -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
|
|
|
@@ -224,8 +318,13 @@ export default function ChatPanel({
|
|
|
224
318
|
case 'ollama':
|
|
225
319
|
return dynamicOllamaModels
|
|
226
320
|
default:
|
|
321
|
+
if (provider.startsWith('custom:')) {
|
|
322
|
+
const id = provider.replace(/^custom:/, '')
|
|
323
|
+
const cp = (settingsConfig?.customProviders ?? []).find(p => p.id === id)
|
|
324
|
+
return cp?.models.map(m => m.modelId) ?? []
|
|
325
|
+
}
|
|
227
326
|
return []
|
|
228
|
-
|
|
327
|
+
}
|
|
229
328
|
}
|
|
230
329
|
|
|
231
330
|
const activeProvider = status?.activeProvider ?? ''
|
|
@@ -251,6 +350,11 @@ export default function ChatPanel({
|
|
|
251
350
|
case 'ollama':
|
|
252
351
|
return settingsConfig.ollamaModel
|
|
253
352
|
default:
|
|
353
|
+
if (provider.startsWith('custom:')) {
|
|
354
|
+
const id = provider.replace(/^custom:/, '')
|
|
355
|
+
const cp = (settingsConfig?.customProviders ?? []).find(p => p.id === id)
|
|
356
|
+
return (settingsConfig.customModelSelections ?? {})[id] ?? cp?.models[0]?.modelId ?? ''
|
|
357
|
+
}
|
|
254
358
|
return ''
|
|
255
359
|
}
|
|
256
360
|
}
|
|
@@ -258,12 +362,154 @@ export default function ChatPanel({
|
|
|
258
362
|
const workspaceName = workspacePath
|
|
259
363
|
? workspacePath.split(/[\\/]/).filter(Boolean).pop() || workspacePath
|
|
260
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
|
+
|
|
261
474
|
const submitOnEnter = (event: KeyboardEvent<HTMLTextAreaElement>) => {
|
|
262
475
|
if (event.key !== 'Enter' || event.shiftKey || event.nativeEvent.isComposing) return
|
|
263
476
|
event.preventDefault()
|
|
264
477
|
event.currentTarget.form?.requestSubmit()
|
|
265
478
|
}
|
|
266
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
|
+
}
|
|
267
513
|
submitOnEnter(event)
|
|
268
514
|
}
|
|
269
515
|
|
|
@@ -382,14 +628,19 @@ export default function ChatPanel({
|
|
|
382
628
|
}
|
|
383
629
|
}
|
|
384
630
|
if (sending) return
|
|
385
|
-
if (!
|
|
386
|
-
|
|
631
|
+
if (!latest?.aiText || latest.id === lastAutoSpokenIdRef.current) return
|
|
632
|
+
|
|
633
|
+
if (!settingsConfig?.enableVoiceReply) {
|
|
634
|
+
lastAutoSpokenIdRef.current = latest.id
|
|
635
|
+
if (voiceMode) {
|
|
636
|
+
scheduleVoiceListen(350)
|
|
637
|
+
}
|
|
387
638
|
return
|
|
388
639
|
}
|
|
389
|
-
|
|
640
|
+
|
|
390
641
|
lastAutoSpokenIdRef.current = latest.id
|
|
391
642
|
speak(latest.aiText)
|
|
392
|
-
}, [interactions, sending, settingsConfig?.enableVoiceReply])
|
|
643
|
+
}, [interactions, sending, settingsConfig?.enableVoiceReply, voiceMode])
|
|
393
644
|
useEffect(() => {
|
|
394
645
|
if (!toolMenuOpen) return
|
|
395
646
|
const closeMenu = (event: MouseEvent) => {
|
|
@@ -429,6 +680,10 @@ export default function ChatPanel({
|
|
|
429
680
|
setToolMenuOpen(false)
|
|
430
681
|
document.getElementById('vision-file-input')?.click()
|
|
431
682
|
}
|
|
683
|
+
const openVideoPicker = () => {
|
|
684
|
+
setToolMenuOpen(false)
|
|
685
|
+
document.getElementById('video-file-input')?.click()
|
|
686
|
+
}
|
|
432
687
|
const openDocumentPicker = () => {
|
|
433
688
|
setToolMenuOpen(false)
|
|
434
689
|
document.getElementById('document-file-input')?.click()
|
|
@@ -715,7 +970,46 @@ export default function ChatPanel({
|
|
|
715
970
|
}
|
|
716
971
|
|
|
717
972
|
return (
|
|
718
|
-
<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
|
+
)}
|
|
719
1013
|
<div className="chat-header">
|
|
720
1014
|
<div className="chat-header-title">
|
|
721
1015
|
<img src="./assets/icon.png" alt="Logo" className="chat-header-logo" />
|
|
@@ -726,6 +1020,8 @@ export default function ChatPanel({
|
|
|
726
1020
|
{interactions.map((interaction) => {
|
|
727
1021
|
const isSystemEvent = interaction.provider === 'system' && interaction.model === 'provider_change';
|
|
728
1022
|
if (isSystemEvent) {
|
|
1023
|
+
const rawText = interaction.userText || ''
|
|
1024
|
+
const cleanText = rawText.replace(/^Changed model to\s*/i, '').trim()
|
|
729
1025
|
return (
|
|
730
1026
|
<div key={interaction.id} className="system-event-divider">
|
|
731
1027
|
<div className="system-event-line" />
|
|
@@ -733,7 +1029,7 @@ export default function ChatPanel({
|
|
|
733
1029
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
|
734
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" />
|
|
735
1031
|
</svg>
|
|
736
|
-
<span>{
|
|
1032
|
+
<span>{cleanText}</span>
|
|
737
1033
|
</div>
|
|
738
1034
|
<div className="system-event-line" />
|
|
739
1035
|
</div>
|
|
@@ -745,7 +1041,17 @@ export default function ChatPanel({
|
|
|
745
1041
|
<div className="message user-message">
|
|
746
1042
|
<div className="bubble-wrapper">
|
|
747
1043
|
<div className="message-bubble">{renderFormattedMessage(interaction.userText)}</div>
|
|
748
|
-
<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>
|
|
749
1055
|
</div>
|
|
750
1056
|
</div>
|
|
751
1057
|
)}
|
|
@@ -766,20 +1072,30 @@ export default function ChatPanel({
|
|
|
766
1072
|
/>
|
|
767
1073
|
)
|
|
768
1074
|
})()}
|
|
769
|
-
<div className="message-bubble"
|
|
1075
|
+
<div className="message-bubble">{renderFormattedMessage(interaction.aiText)}</div>
|
|
770
1076
|
{renderWebSearchSources(interaction)}
|
|
771
1077
|
<div className="message-time" style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
772
1078
|
<button className="provider-badge">{interaction.provider} • {interaction.model}</button>
|
|
773
1079
|
{fallbackNotice(interaction) && <span className="provider-fallback-notice">{fallbackNotice(interaction)}</span>}
|
|
774
1080
|
<span>{new Date(interaction.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</span>
|
|
775
|
-
<
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
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>
|
|
783
1099
|
</div>
|
|
784
1100
|
</div>
|
|
785
1101
|
</div>
|
|
@@ -789,7 +1105,7 @@ export default function ChatPanel({
|
|
|
789
1105
|
|
|
790
1106
|
{sending && (
|
|
791
1107
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px', marginBottom: '16px' }}>
|
|
792
|
-
<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>
|
|
793
1109
|
{agentMode && (
|
|
794
1110
|
<AgentActivityDrawer
|
|
795
1111
|
activityView={agentActivities}
|
|
@@ -848,14 +1164,24 @@ export default function ChatPanel({
|
|
|
848
1164
|
<button className="provider-badge">{badge(streamedResponse.provider, streamedResponse.model)}</button>
|
|
849
1165
|
{activeFallbackNotice && <span className="provider-fallback-notice">{activeFallbackNotice}</span>}
|
|
850
1166
|
{streamedReply && (
|
|
851
|
-
<
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
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>
|
|
859
1185
|
)}
|
|
860
1186
|
</div>
|
|
861
1187
|
)}
|
|
@@ -922,6 +1248,65 @@ export default function ChatPanel({
|
|
|
922
1248
|
</div>
|
|
923
1249
|
)}
|
|
924
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
|
+
|
|
925
1310
|
<form
|
|
926
1311
|
id="chat-form"
|
|
927
1312
|
onSubmit={onSubmit}
|
|
@@ -936,7 +1321,7 @@ export default function ChatPanel({
|
|
|
936
1321
|
if (onPasteImage(event.clipboardData)) event.preventDefault()
|
|
937
1322
|
}}
|
|
938
1323
|
>
|
|
939
|
-
{(imageAttachments.length > 0 || documentName) && (
|
|
1324
|
+
{(imageAttachments.length > 0 || videoAttachments.length > 0 || documentName) && (
|
|
940
1325
|
<div className="mint-attachment">
|
|
941
1326
|
{imageAttachments.map((attachment, idx) => (
|
|
942
1327
|
<div className="mint-image-attachment" key={idx}>
|
|
@@ -949,6 +1334,22 @@ export default function ChatPanel({
|
|
|
949
1334
|
</button>
|
|
950
1335
|
</div>
|
|
951
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
|
+
))}
|
|
952
1353
|
{documentName && (
|
|
953
1354
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', minWidth: 0 }}>
|
|
954
1355
|
<span aria-hidden="true" style={{ display: 'inline-flex', alignItems: 'center', color: 'var(--text-soft)' }}>
|
|
@@ -959,7 +1360,7 @@ export default function ChatPanel({
|
|
|
959
1360
|
</span>
|
|
960
1361
|
<span style={{ fontSize: '0.76rem', color: 'var(--text-soft)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: '220px' }}>{documentName}</span>
|
|
961
1362
|
<button type="button" onClick={onRemoveDocument} style={{ background: 'transparent', border: 0, color: '#ef4444', cursor: 'pointer', display: 'inline-flex', alignItems: 'center' }}>
|
|
962
|
-
<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">
|
|
963
1364
|
<line x1="18" y1="6" x2="6" y2="18"></line>
|
|
964
1365
|
<line x1="6" y1="6" x2="18" y2="18"></line>
|
|
965
1366
|
</svg>
|
|
@@ -970,6 +1371,7 @@ export default function ChatPanel({
|
|
|
970
1371
|
)}
|
|
971
1372
|
<textarea
|
|
972
1373
|
id="chat-input"
|
|
1374
|
+
ref={textareaRef}
|
|
973
1375
|
value={message}
|
|
974
1376
|
onChange={(event) => {
|
|
975
1377
|
resizeInput(event.currentTarget)
|
|
@@ -981,46 +1383,56 @@ export default function ChatPanel({
|
|
|
981
1383
|
rows={1}
|
|
982
1384
|
/>
|
|
983
1385
|
<div className="chat-tool-menu-wrap" ref={toolMenuRef}>
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
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>
|
|
1023
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' }} />
|
|
1024
1436
|
<input id="document-file-input" type="file" accept="application/pdf,.pdf" onChange={onSelectDocument} style={{ display: 'none' }} />
|
|
1025
1437
|
<button id="screen-capture-btn" type="button" onClick={onCaptureScreen} aria-label="Capture screen">
|
|
1026
1438
|
<span className="screen-capture-eye" aria-hidden="true" />
|
|
@@ -1055,6 +1467,11 @@ export default function ChatPanel({
|
|
|
1055
1467
|
else if (provider === 'huggingface') displayName = 'HF'
|
|
1056
1468
|
else if (provider === 'local_openai') displayName = 'Local'
|
|
1057
1469
|
else if (provider === 'ollama') displayName = 'Ollama'
|
|
1470
|
+
else if (provider.startsWith('custom:')) {
|
|
1471
|
+
const id = provider.replace(/^custom:/, '')
|
|
1472
|
+
const cp = (settingsConfig?.customProviders ?? []).find(p => p.id === id)
|
|
1473
|
+
displayName = cp?.displayName || id
|
|
1474
|
+
}
|
|
1058
1475
|
return <option key={provider} value={provider}>{displayName}</option>
|
|
1059
1476
|
})}
|
|
1060
1477
|
</select>
|