@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
|
|
@@ -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) {
|
|
@@ -234,6 +246,22 @@ export default function ChatPanel({
|
|
|
234
246
|
}, [sending])
|
|
235
247
|
|
|
236
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
|
+
|
|
237
265
|
const audioRef = useRef<HTMLAudioElement | null>(null)
|
|
238
266
|
const speechRunRef = useRef(0)
|
|
239
267
|
const historyReadyRef = useRef(false)
|
|
@@ -437,6 +465,15 @@ export default function ChatPanel({
|
|
|
437
465
|
const event = { target: input } as ChangeEvent<HTMLInputElement>
|
|
438
466
|
onSelectImage(event)
|
|
439
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
|
+
}
|
|
440
477
|
} else if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
|
|
441
478
|
const input = document.getElementById('document-file-input') as HTMLInputElement | null
|
|
442
479
|
if (input) {
|
|
@@ -449,12 +486,154 @@ export default function ChatPanel({
|
|
|
449
486
|
}
|
|
450
487
|
}
|
|
451
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
|
+
|
|
452
598
|
const submitOnEnter = (event: KeyboardEvent<HTMLTextAreaElement>) => {
|
|
453
599
|
if (event.key !== 'Enter' || event.shiftKey || event.nativeEvent.isComposing) return
|
|
454
600
|
event.preventDefault()
|
|
455
601
|
event.currentTarget.form?.requestSubmit()
|
|
456
602
|
}
|
|
457
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
|
+
}
|
|
458
637
|
submitOnEnter(event)
|
|
459
638
|
}
|
|
460
639
|
const resizeInput = (element: HTMLTextAreaElement) => {
|
|
@@ -500,6 +679,10 @@ export default function ChatPanel({
|
|
|
500
679
|
setToolMenuOpen(false)
|
|
501
680
|
document.getElementById('vision-file-input')?.click()
|
|
502
681
|
}
|
|
682
|
+
const openVideoPicker = () => {
|
|
683
|
+
setToolMenuOpen(false)
|
|
684
|
+
document.getElementById('video-file-input')?.click()
|
|
685
|
+
}
|
|
503
686
|
const openDocumentPicker = () => {
|
|
504
687
|
setToolMenuOpen(false)
|
|
505
688
|
document.getElementById('document-file-input')?.click()
|
|
@@ -825,7 +1008,7 @@ export default function ChatPanel({
|
|
|
825
1008
|
>
|
|
826
1009
|
<div style={{ fontSize: '3.5rem', marginBottom: '16px' }}>🖼️</div>
|
|
827
1010
|
<div style={{ fontSize: '1.25rem', fontWeight: 'bold', letterSpacing: '0.5px' }}>Drag files to attach data</div>
|
|
828
|
-
<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>
|
|
829
1012
|
</div>
|
|
830
1013
|
)}
|
|
831
1014
|
|
|
@@ -833,6 +1016,8 @@ export default function ChatPanel({
|
|
|
833
1016
|
{interactions.map((interaction) => {
|
|
834
1017
|
const isSystemEvent = interaction.provider === 'system' && interaction.model === 'provider_change';
|
|
835
1018
|
if (isSystemEvent) {
|
|
1019
|
+
const rawText = interaction.userText || ''
|
|
1020
|
+
const cleanText = rawText.replace(/^Changed model to\s*/i, '').trim()
|
|
836
1021
|
return (
|
|
837
1022
|
<div key={interaction.id} className="system-event-divider">
|
|
838
1023
|
<div className="system-event-line" />
|
|
@@ -840,7 +1025,7 @@ export default function ChatPanel({
|
|
|
840
1025
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
|
841
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" />
|
|
842
1027
|
</svg>
|
|
843
|
-
<span>{
|
|
1028
|
+
<span>{cleanText}</span>
|
|
844
1029
|
</div>
|
|
845
1030
|
<div className="system-event-line" />
|
|
846
1031
|
</div>
|
|
@@ -852,7 +1037,17 @@ export default function ChatPanel({
|
|
|
852
1037
|
<div className="message user-message">
|
|
853
1038
|
<div className="bubble-wrapper">
|
|
854
1039
|
<div className="message-bubble">{renderFormattedMessage(interaction.userText)}</div>
|
|
855
|
-
<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>
|
|
856
1051
|
</div>
|
|
857
1052
|
</div>
|
|
858
1053
|
)}
|
|
@@ -873,32 +1068,30 @@ export default function ChatPanel({
|
|
|
873
1068
|
/>
|
|
874
1069
|
)
|
|
875
1070
|
})()}
|
|
876
|
-
<div className="message-bubble"
|
|
1071
|
+
<div className="message-bubble">{renderFormattedMessage(interaction.aiText)}</div>
|
|
877
1072
|
{renderWebSearchSources(interaction)}
|
|
878
1073
|
<div className="message-time" style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
879
1074
|
<button className="provider-badge">{interaction.provider} • {interaction.model}</button>
|
|
880
1075
|
{fallbackNotice(interaction) && <span className="provider-fallback-notice">{fallbackNotice(interaction)}</span>}
|
|
881
1076
|
<span>{new Date(interaction.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</span>
|
|
882
|
-
<
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
>
|
|
900
|
-
{renderSpeakerIcon(speakingText === interaction.aiText)}
|
|
901
|
-
</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>
|
|
902
1095
|
</div>
|
|
903
1096
|
</div>
|
|
904
1097
|
</div>
|
|
@@ -908,7 +1101,7 @@ export default function ChatPanel({
|
|
|
908
1101
|
|
|
909
1102
|
{sending && (
|
|
910
1103
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px', marginBottom: '16px' }}>
|
|
911
|
-
<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>
|
|
912
1105
|
{agentMode && (
|
|
913
1106
|
<AgentActivityDrawer
|
|
914
1107
|
activityView={agentActivities}
|
|
@@ -967,26 +1160,24 @@ export default function ChatPanel({
|
|
|
967
1160
|
<button className="provider-badge">{badge(streamedResponse.provider, streamedResponse.model)}</button>
|
|
968
1161
|
{activeFallbackNotice && <span className="provider-fallback-notice">{activeFallbackNotice}</span>}
|
|
969
1162
|
{streamedReply && (
|
|
970
|
-
<
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
>
|
|
988
|
-
{renderSpeakerIcon(speakingText === streamedReply)}
|
|
989
|
-
</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>
|
|
990
1181
|
)}
|
|
991
1182
|
</div>
|
|
992
1183
|
)}
|
|
@@ -1037,6 +1228,65 @@ export default function ChatPanel({
|
|
|
1037
1228
|
</div>
|
|
1038
1229
|
)}
|
|
1039
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
|
+
|
|
1040
1290
|
<form
|
|
1041
1291
|
id="chat-form"
|
|
1042
1292
|
onSubmit={onSubmit}
|
|
@@ -1044,7 +1294,7 @@ export default function ChatPanel({
|
|
|
1044
1294
|
if (onPasteImage(event.clipboardData)) event.preventDefault()
|
|
1045
1295
|
}}
|
|
1046
1296
|
>
|
|
1047
|
-
{(imageAttachments.length > 0 || documentName) && (
|
|
1297
|
+
{(imageAttachments.length > 0 || videoAttachments.length > 0 || documentName) && (
|
|
1048
1298
|
<div className="mint-attachment">
|
|
1049
1299
|
{imageAttachments.map((attachment, idx) => (
|
|
1050
1300
|
<div className="mint-image-attachment" key={idx}>
|
|
@@ -1052,6 +1302,17 @@ export default function ChatPanel({
|
|
|
1052
1302
|
<button className="mint-attachment-remove" type="button" onClick={() => onRemoveImage(idx)} aria-label="Remove image">×</button>
|
|
1053
1303
|
</div>
|
|
1054
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
|
+
))}
|
|
1055
1316
|
{documentName && (
|
|
1056
1317
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', minWidth: 0 }}>
|
|
1057
1318
|
<span aria-hidden="true" style={{ display: 'inline-flex', alignItems: 'center', color: 'var(--accent)' }}>
|
|
@@ -1070,6 +1331,7 @@ export default function ChatPanel({
|
|
|
1070
1331
|
)}
|
|
1071
1332
|
<textarea
|
|
1072
1333
|
id="chat-input"
|
|
1334
|
+
ref={textareaRef}
|
|
1073
1335
|
value={message}
|
|
1074
1336
|
onChange={(event) => {
|
|
1075
1337
|
resizeInput(event.currentTarget)
|
|
@@ -1080,47 +1342,57 @@ export default function ChatPanel({
|
|
|
1080
1342
|
rows={1}
|
|
1081
1343
|
/>
|
|
1082
1344
|
<div className="chat-tool-menu-wrap" ref={toolMenuRef}>
|
|
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
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
</
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
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>
|
|
1123
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' }} />
|
|
1124
1396
|
<input id="document-file-input" type="file" accept="application/pdf,.pdf" onChange={onSelectDocument} style={{ display: 'none' }} />
|
|
1125
1397
|
<div className="chat-provider-select" style={{ display: 'flex', gap: '4px', padding: 0, background: 'transparent', border: 0, width: '100%', height: '32px' }}>
|
|
1126
1398
|
<select
|