@pheem49/mint 1.6.4 → 1.7.1
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.toml +1 -1
- package/bin/mint +0 -0
- package/crates/mint-cli/src/agent.rs +42 -14
- package/crates/mint-cli/src/main.rs +412 -2
- package/crates/mint-cli/src/setup.rs +173 -32
- package/crates/mint-core/src/api_server.rs +35 -0
- package/crates/mint-core/src/config.rs +2 -2
- package/crates/mint-core/src/memory.rs +27 -1
- package/crates/mint-core/src/orchestration.rs +1 -1
- package/crates/mint-core/src/plugins.rs +35 -0
- package/crates/mint-core/tests/memory_persistence.rs +28 -0
- package/package.json +12 -2
- package/src/renderer/src/agentProgress.ts +39 -0
- package/src/renderer/src/components/ChatPanel.tsx +308 -34
- package/src/renderer/src/components/DashboardSidebar.tsx +3 -3
- package/src/renderer/src/components/MintDashboard.tsx +71 -27
- package/src/renderer/src/components/ModelPanel.tsx +2 -2
- package/src/renderer/src/components/PicturesLibrary.tsx +5 -1
- package/src/renderer/src/components/SpotlightWindow.tsx +8 -8
- package/src/renderer/src/components/WorkspacePanel.tsx +1 -1
- package/src/renderer/src/css/styles.css +195 -17
- package/src/renderer/src/index.css +46 -0
- package/src/renderer/src/tauri.ts +25 -0
- package/src/renderer/src-web/App.tsx +16 -1
- package/src/renderer/src-web/agentProgress.ts +39 -0
- package/src/renderer/src-web/components/ChatPanel.tsx +309 -35
- package/src/renderer/src-web/components/DashboardSidebar.tsx +1 -1
- package/src/renderer/src-web/components/MintDashboard.tsx +53 -16
- package/src/renderer/src-web/components/PicturesLibrary.tsx +5 -1
- package/src/renderer/src-web/css/styles.css +242 -18
- package/src/renderer/src-web/index.css +46 -0
- package/src/renderer/src-web/tauri.ts +25 -0
- package/src-tauri/src/lib.rs +16 -0
- package/src-tauri/tauri.conf.json +1 -1
|
@@ -19,7 +19,22 @@ export default function App() {
|
|
|
19
19
|
|
|
20
20
|
let content = <MintDashboard />
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
// For web, show settings as a centered modal overlay instead of a full-page route
|
|
23
|
+
if (route.startsWith('/settings')) {
|
|
24
|
+
content = (
|
|
25
|
+
<>
|
|
26
|
+
<MintDashboard />
|
|
27
|
+
<div
|
|
28
|
+
className="settings-modal-overlay"
|
|
29
|
+
onClick={() => window.settingsApi?.closeSettings?.()}
|
|
30
|
+
>
|
|
31
|
+
<div className="settings-modal" onClick={(e) => e.stopPropagation()}>
|
|
32
|
+
<SettingsWindow />
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
</>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
23
38
|
|
|
24
39
|
return <Suspense fallback={null}>{content}</Suspense>
|
|
25
40
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { AgentProgress, InteractionMemory } from './tauri'
|
|
2
|
+
|
|
3
|
+
export function thoughtsFrom(progress: AgentProgress[]): string[] {
|
|
4
|
+
return progress
|
|
5
|
+
.filter((event) => event.type === 'Thought')
|
|
6
|
+
.map((event) => (event as Extract<AgentProgress, { type: 'Thought' }>).data.thought)
|
|
7
|
+
.filter(Boolean)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function trimAgentProgress(progress: AgentProgress[], maxNonThought = 20): AgentProgress[] {
|
|
11
|
+
if (progress.length === 0) return progress
|
|
12
|
+
const keptNonThought = new Set(
|
|
13
|
+
progress.filter((event) => event.type !== 'Thought').slice(-maxNonThought),
|
|
14
|
+
)
|
|
15
|
+
return progress.filter((event) => event.type === 'Thought' || keptNonThought.has(event))
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function hasAgentToolActivity(progress: AgentProgress[]): boolean {
|
|
19
|
+
return progress.some((event) => event.type === 'ToolStart' || event.type === 'ToolEnd')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function mergeActivitySnapshots(
|
|
23
|
+
current: Record<string, AgentProgress[]>,
|
|
24
|
+
interactions: InteractionMemory[],
|
|
25
|
+
): Record<string, AgentProgress[]> {
|
|
26
|
+
const merged = { ...current }
|
|
27
|
+
for (const interaction of interactions) {
|
|
28
|
+
const key = String(interaction.id)
|
|
29
|
+
if (interaction.agentActivity?.length && !merged[key]) {
|
|
30
|
+
merged[key] = interaction.agentActivity
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return merged
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function parseAgentActivity(value: unknown): AgentProgress[] | undefined {
|
|
37
|
+
if (!Array.isArray(value)) return undefined
|
|
38
|
+
return value as AgentProgress[]
|
|
39
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useEffect, useRef, useState, Fragment, type ChangeEvent, type ClipboardEvent, type FormEvent, type KeyboardEvent, type RefObject, type DragEvent } from 'react'
|
|
2
|
+
import { hasAgentToolActivity, thoughtsFrom } from '../agentProgress'
|
|
2
3
|
import {
|
|
3
4
|
type AgentProgress,
|
|
4
5
|
type ChatResponse,
|
|
@@ -327,6 +328,8 @@ interface ChatPanelProps {
|
|
|
327
328
|
streamedResponse: ChatResponse | null
|
|
328
329
|
agentProgress: AgentProgress[]
|
|
329
330
|
agentActivitySnapshots: Record<string, AgentProgress[]>
|
|
331
|
+
thinkingExpanded: Record<string, boolean>
|
|
332
|
+
onThinkingExpandedChange: (key: string, open: boolean) => void
|
|
330
333
|
message: string
|
|
331
334
|
imageAttachments: Array<{ dataUri: string; name: string; previewDataUri?: string }>
|
|
332
335
|
documentName: string
|
|
@@ -418,6 +421,11 @@ function ChatCodeBlock({ code, language }: { code: string; language: string; key
|
|
|
418
421
|
)
|
|
419
422
|
}
|
|
420
423
|
|
|
424
|
+
const isTableLine = (line: string): boolean => {
|
|
425
|
+
const trimmed = line.trim()
|
|
426
|
+
return trimmed.startsWith('|') && trimmed.endsWith('|') && trimmed.length > 1
|
|
427
|
+
}
|
|
428
|
+
|
|
421
429
|
function renderFormattedMessage(text: string) {
|
|
422
430
|
const displayText = readableAssistantText(text)
|
|
423
431
|
if (!displayText) return null
|
|
@@ -446,6 +454,113 @@ function renderFormattedMessage(text: string) {
|
|
|
446
454
|
})
|
|
447
455
|
}
|
|
448
456
|
|
|
457
|
+
const renderHtmlTable = (tableLines: string[], key: string) => {
|
|
458
|
+
const rows: string[][] = []
|
|
459
|
+
for (const line of tableLines) {
|
|
460
|
+
const trimmed = line.trim()
|
|
461
|
+
if (!trimmed) continue
|
|
462
|
+
const content = trimmed.slice(1, -1)
|
|
463
|
+
const contentEscaped = content.replace(/\\\|/g, '\u0000')
|
|
464
|
+
const cols = contentEscaped.split('|').map(s => s.replace(/\u0000/g, '|').trim())
|
|
465
|
+
rows.push(cols)
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (rows.length === 0) return null
|
|
469
|
+
|
|
470
|
+
let hasSeparator = false
|
|
471
|
+
let separatorIdx = -1
|
|
472
|
+
for (let i = 0; i < rows.length; i++) {
|
|
473
|
+
const row = rows[i]
|
|
474
|
+
if (row.length > 0 && row.every(col => /^[-\s:]+$/.test(col))) {
|
|
475
|
+
hasSeparator = true
|
|
476
|
+
separatorIdx = i
|
|
477
|
+
break
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
let headerRow: string[] | null = null
|
|
482
|
+
const dataRows: string[][] = []
|
|
483
|
+
|
|
484
|
+
if (hasSeparator) {
|
|
485
|
+
if (separatorIdx > 0) {
|
|
486
|
+
headerRow = rows[separatorIdx - 1]
|
|
487
|
+
for (let i = 0; i < rows.length; i++) {
|
|
488
|
+
if (i !== separatorIdx && i !== separatorIdx - 1) {
|
|
489
|
+
dataRows.push(rows[i])
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
} else {
|
|
493
|
+
for (let i = 0; i < rows.length; i++) {
|
|
494
|
+
if (i !== separatorIdx) {
|
|
495
|
+
dataRows.push(rows[i])
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
} else {
|
|
500
|
+
headerRow = rows[0]
|
|
501
|
+
for (let i = 1; i < rows.length; i++) {
|
|
502
|
+
dataRows.push(rows[i])
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
return (
|
|
507
|
+
<div key={key} className="chat-table-container" style={{
|
|
508
|
+
overflowX: 'auto',
|
|
509
|
+
margin: '14px 0',
|
|
510
|
+
width: '100%',
|
|
511
|
+
borderRadius: '8px',
|
|
512
|
+
border: '1px solid rgba(255, 255, 255, 0.12)',
|
|
513
|
+
background: 'rgba(30, 41, 59, 0.35)',
|
|
514
|
+
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
|
|
515
|
+
}}>
|
|
516
|
+
<table className="chat-table" style={{
|
|
517
|
+
width: '100%',
|
|
518
|
+
borderCollapse: 'collapse',
|
|
519
|
+
fontSize: '0.86rem',
|
|
520
|
+
textAlign: 'left',
|
|
521
|
+
lineHeight: '1.5',
|
|
522
|
+
}}>
|
|
523
|
+
{headerRow && (
|
|
524
|
+
<thead>
|
|
525
|
+
<tr style={{
|
|
526
|
+
background: 'rgba(255, 255, 255, 0.04)',
|
|
527
|
+
borderBottom: '2px solid rgba(255, 255, 255, 0.15)',
|
|
528
|
+
}}>
|
|
529
|
+
{headerRow.map((col, idx) => (
|
|
530
|
+
<th key={`th-${idx}`} style={{
|
|
531
|
+
padding: '12px 16px',
|
|
532
|
+
fontWeight: 700,
|
|
533
|
+
color: 'var(--accent, #38bdf8)',
|
|
534
|
+
fontFamily: 'ui-sans-serif, system-ui, sans-serif',
|
|
535
|
+
}}>
|
|
536
|
+
{formatInline(col)}
|
|
537
|
+
</th>
|
|
538
|
+
))}
|
|
539
|
+
</tr>
|
|
540
|
+
</thead>
|
|
541
|
+
)}
|
|
542
|
+
<tbody>
|
|
543
|
+
{dataRows.map((row, rIdx) => (
|
|
544
|
+
<tr key={`tr-${rIdx}`} style={{
|
|
545
|
+
background: rIdx % 2 === 1 ? 'rgba(255, 255, 255, 0.015)' : 'transparent',
|
|
546
|
+
borderBottom: rIdx < dataRows.length - 1 ? '1px solid rgba(255, 255, 255, 0.08)' : 'none',
|
|
547
|
+
}}>
|
|
548
|
+
{row.map((col, cIdx) => (
|
|
549
|
+
<td key={`td-${cIdx}`} style={{
|
|
550
|
+
padding: '12px 16px',
|
|
551
|
+
color: 'var(--text-main, #e2e8f0)',
|
|
552
|
+
}}>
|
|
553
|
+
{formatInline(col)}
|
|
554
|
+
</td>
|
|
555
|
+
))}
|
|
556
|
+
</tr>
|
|
557
|
+
))}
|
|
558
|
+
</tbody>
|
|
559
|
+
</table>
|
|
560
|
+
</div>
|
|
561
|
+
)
|
|
562
|
+
}
|
|
563
|
+
|
|
449
564
|
const lines = displayText.split('\n')
|
|
450
565
|
const items: any[] = []
|
|
451
566
|
|
|
@@ -453,10 +568,24 @@ function renderFormattedMessage(text: string) {
|
|
|
453
568
|
let codeBlockLang = ''
|
|
454
569
|
let codeBlockLines: string[] = []
|
|
455
570
|
|
|
571
|
+
let inTable = false
|
|
572
|
+
let tableLines: string[] = []
|
|
573
|
+
|
|
574
|
+
const flushTable = (index: number) => {
|
|
575
|
+
if (tableLines.length > 0) {
|
|
576
|
+
items.push(renderHtmlTable(tableLines, `table-${index}`))
|
|
577
|
+
tableLines = []
|
|
578
|
+
inTable = false
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
456
582
|
for (let i = 0; i < lines.length; i++) {
|
|
457
583
|
const line = lines[i]
|
|
458
584
|
|
|
459
585
|
if (line.trim().startsWith('```')) {
|
|
586
|
+
if (inTable) {
|
|
587
|
+
flushTable(i)
|
|
588
|
+
}
|
|
460
589
|
if (inCodeBlock) {
|
|
461
590
|
const codeText = codeBlockLines.join('\n')
|
|
462
591
|
items.push(
|
|
@@ -480,35 +609,60 @@ function renderFormattedMessage(text: string) {
|
|
|
480
609
|
continue
|
|
481
610
|
}
|
|
482
611
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
fontWeight: 'bold',
|
|
490
|
-
display: 'block',
|
|
491
|
-
marginTop: level === 1 ? '16px' : level === 2 ? '14px' : '10px',
|
|
492
|
-
marginBottom: '6px',
|
|
493
|
-
fontSize: level === 1 ? '1.25em' : level === 2 ? '1.15em' : '1.05em',
|
|
494
|
-
color: 'var(--text-main)',
|
|
612
|
+
if (isTableLine(line)) {
|
|
613
|
+
inTable = true
|
|
614
|
+
tableLines.push(line)
|
|
615
|
+
} else {
|
|
616
|
+
if (inTable) {
|
|
617
|
+
flushTable(i)
|
|
495
618
|
}
|
|
496
619
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
620
|
+
const headerMatch = line.match(/^(#{1,6})\s+(.*)$/)
|
|
621
|
+
if (headerMatch) {
|
|
622
|
+
const level = headerMatch[1].length
|
|
623
|
+
const content = headerMatch[2]
|
|
624
|
+
|
|
625
|
+
const style = {
|
|
626
|
+
fontWeight: 'bold',
|
|
627
|
+
display: 'block',
|
|
628
|
+
marginTop: level === 1 ? '16px' : level === 2 ? '14px' : '10px',
|
|
629
|
+
marginBottom: '6px',
|
|
630
|
+
fontSize: level === 1 ? '1.25em' : level === 2 ? '1.15em' : '1.05em',
|
|
631
|
+
color: 'var(--text-main)',
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
items.push(
|
|
635
|
+
<span key={`line-${i}`} style={style}>
|
|
636
|
+
{formatInline(content)}
|
|
637
|
+
</span>
|
|
638
|
+
)
|
|
639
|
+
} else {
|
|
640
|
+
const listMatch = line.match(/^(\s*)([-*+])\s+(.*)$/)
|
|
641
|
+
if (listMatch) {
|
|
642
|
+
const indent = listMatch[1]
|
|
643
|
+
const content = listMatch[3]
|
|
644
|
+
items.push(
|
|
645
|
+
<Fragment key={`line-${i}`}>
|
|
646
|
+
{indent}• {formatInline(content)}
|
|
647
|
+
{i < lines.length - 1 && '\n'}
|
|
648
|
+
</Fragment>
|
|
649
|
+
)
|
|
650
|
+
} else {
|
|
651
|
+
items.push(
|
|
652
|
+
<Fragment key={`line-${i}`}>
|
|
653
|
+
{formatInline(line)}
|
|
654
|
+
{i < lines.length - 1 && '\n'}
|
|
655
|
+
</Fragment>
|
|
656
|
+
)
|
|
657
|
+
}
|
|
658
|
+
}
|
|
509
659
|
}
|
|
510
660
|
}
|
|
511
661
|
|
|
662
|
+
if (inTable) {
|
|
663
|
+
flushTable(lines.length)
|
|
664
|
+
}
|
|
665
|
+
|
|
512
666
|
if (inCodeBlock && codeBlockLines.length > 0) {
|
|
513
667
|
const codeText = codeBlockLines.join('\n')
|
|
514
668
|
items.push(
|
|
@@ -523,6 +677,98 @@ function renderFormattedMessage(text: string) {
|
|
|
523
677
|
return items
|
|
524
678
|
}
|
|
525
679
|
|
|
680
|
+
const THINKING_LABELS = {
|
|
681
|
+
live: 'Thinking…',
|
|
682
|
+
completed: (count: number) => `Thinking process (${count} steps)`,
|
|
683
|
+
step: (index: number) => `Step ${index}`,
|
|
684
|
+
emptyHint: 'Model didn\'t send thinking steps this time',
|
|
685
|
+
} as const
|
|
686
|
+
|
|
687
|
+
function ThinkingBlock({
|
|
688
|
+
thoughts,
|
|
689
|
+
isLive = false,
|
|
690
|
+
blockKey,
|
|
691
|
+
expanded,
|
|
692
|
+
onExpandedChange,
|
|
693
|
+
showEmptyHint = false,
|
|
694
|
+
}: {
|
|
695
|
+
thoughts: string[]
|
|
696
|
+
isLive?: boolean
|
|
697
|
+
blockKey: string
|
|
698
|
+
expanded?: boolean
|
|
699
|
+
onExpandedChange?: (key: string, open: boolean) => void
|
|
700
|
+
showEmptyHint?: boolean
|
|
701
|
+
}) {
|
|
702
|
+
const contentRef = useRef<HTMLDivElement>(null)
|
|
703
|
+
const isControlled = expanded !== undefined && Boolean(onExpandedChange)
|
|
704
|
+
const [localOpen, setLocalOpen] = useState(isLive)
|
|
705
|
+
const isOpen = isControlled ? Boolean(expanded) : localOpen
|
|
706
|
+
|
|
707
|
+
const setOpen = (open: boolean) => {
|
|
708
|
+
if (isControlled && onExpandedChange) onExpandedChange(blockKey, open)
|
|
709
|
+
else setLocalOpen(open)
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
useEffect(() => {
|
|
713
|
+
if (isLive && !isControlled) setLocalOpen(true)
|
|
714
|
+
}, [isLive, isControlled])
|
|
715
|
+
|
|
716
|
+
useEffect(() => {
|
|
717
|
+
if (!isLive || !isOpen || !contentRef.current) return
|
|
718
|
+
contentRef.current.scrollTop = contentRef.current.scrollHeight
|
|
719
|
+
}, [thoughts, isLive, isOpen])
|
|
720
|
+
|
|
721
|
+
if (thoughts.length === 0 && !showEmptyHint) return null
|
|
722
|
+
|
|
723
|
+
if (thoughts.length === 0) {
|
|
724
|
+
return (
|
|
725
|
+
<div className="thinking-block thinking-block-empty-state" data-live={isLive ? 'true' : 'false'}>
|
|
726
|
+
<span className="thinking-block-empty">{THINKING_LABELS.emptyHint}</span>
|
|
727
|
+
</div>
|
|
728
|
+
)
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
return (
|
|
732
|
+
<div className={`thinking-block${isLive ? ' is-live' : ''}`}>
|
|
733
|
+
<button
|
|
734
|
+
type="button"
|
|
735
|
+
className="thinking-block-header"
|
|
736
|
+
onClick={() => setOpen(!isOpen)}
|
|
737
|
+
aria-expanded={isOpen}
|
|
738
|
+
>
|
|
739
|
+
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ flexShrink: 0 }}>
|
|
740
|
+
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" />
|
|
741
|
+
<line x1="12" y1="17" x2="12.01" y2="17" />
|
|
742
|
+
<circle cx="12" cy="12" r="10" />
|
|
743
|
+
</svg>
|
|
744
|
+
<span className="thinking-block-label">
|
|
745
|
+
{isLive ? THINKING_LABELS.live : THINKING_LABELS.completed(thoughts.length)}
|
|
746
|
+
</span>
|
|
747
|
+
{isLive && <span className="thinking-block-live-dot" aria-hidden="true" />}
|
|
748
|
+
<span className={`thinking-block-chevron${isOpen ? ' is-open' : ''}`} aria-hidden="true">
|
|
749
|
+
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
|
750
|
+
<polyline points="9 18 15 12 9 6" />
|
|
751
|
+
</svg>
|
|
752
|
+
</span>
|
|
753
|
+
</button>
|
|
754
|
+
{isOpen && (
|
|
755
|
+
<div className="thinking-block-body">
|
|
756
|
+
<div className="thinking-block-content" ref={contentRef}>
|
|
757
|
+
{thoughts.map((thought, index) => (
|
|
758
|
+
<div className="thinking-block-step" key={`${blockKey}-${index}`}>
|
|
759
|
+
{thoughts.length > 1 && (
|
|
760
|
+
<div className="thinking-block-step-label">{THINKING_LABELS.step(index + 1)}</div>
|
|
761
|
+
)}
|
|
762
|
+
<div className="thinking-block-step-body">{renderFormattedMessage(thought)}</div>
|
|
763
|
+
</div>
|
|
764
|
+
))}
|
|
765
|
+
</div>
|
|
766
|
+
</div>
|
|
767
|
+
)}
|
|
768
|
+
</div>
|
|
769
|
+
)
|
|
770
|
+
}
|
|
771
|
+
|
|
526
772
|
function readableAssistantText(text: string) {
|
|
527
773
|
if (typeof text !== 'string') return ''
|
|
528
774
|
const trimmed = text.trim()
|
|
@@ -692,6 +938,8 @@ export default function ChatPanel({
|
|
|
692
938
|
streamedResponse,
|
|
693
939
|
agentProgress,
|
|
694
940
|
agentActivitySnapshots,
|
|
941
|
+
thinkingExpanded,
|
|
942
|
+
onThinkingExpandedChange,
|
|
695
943
|
message,
|
|
696
944
|
imageAttachments,
|
|
697
945
|
documentName,
|
|
@@ -821,7 +1069,7 @@ export default function ChatPanel({
|
|
|
821
1069
|
const speakingRef = useRef<string | null>(null)
|
|
822
1070
|
const restartTimerRef = useRef<number | null>(null)
|
|
823
1071
|
const voiceStatus = speakingText ? 'speaking' : (sending || voiceAwaitingResponse) ? 'thinking' : isRecording ? 'listening' : voiceMode ? 'ready' : 'off'
|
|
824
|
-
const voiceStatusLabel = voiceStatus === 'speaking' ? '
|
|
1072
|
+
const voiceStatusLabel = voiceStatus === 'speaking' ? 'Speaking' : voiceStatus === 'thinking' ? 'Thinking' : voiceStatus === 'listening' ? 'Listening' : 'Ready'
|
|
825
1073
|
|
|
826
1074
|
const clearRestartTimer = () => {
|
|
827
1075
|
if (restartTimerRef.current === null) return
|
|
@@ -943,7 +1191,7 @@ export default function ChatPanel({
|
|
|
943
1191
|
}
|
|
944
1192
|
const startAudioRecording = async (autoSend: boolean) => {
|
|
945
1193
|
if (!navigator.mediaDevices?.getUserMedia || typeof MediaRecorder === 'undefined') {
|
|
946
|
-
alert(
|
|
1194
|
+
alert("Sorry, this system cannot access the microphone or record audio in this browser")
|
|
947
1195
|
voiceModeRef.current = false
|
|
948
1196
|
setVoiceMode(false)
|
|
949
1197
|
return
|
|
@@ -966,7 +1214,7 @@ export default function ChatPanel({
|
|
|
966
1214
|
mediaStreamRef.current = stream
|
|
967
1215
|
mediaRecorderRef.current = recorder
|
|
968
1216
|
audioContextRef.current = audioContext
|
|
969
|
-
setVoiceTranscript('
|
|
1217
|
+
setVoiceTranscript('Listening to microphone...')
|
|
970
1218
|
|
|
971
1219
|
recorder.ondataavailable = (event) => {
|
|
972
1220
|
if (event.data.size > 0) chunks.push(event.data)
|
|
@@ -981,13 +1229,13 @@ export default function ChatPanel({
|
|
|
981
1229
|
setIsRecording(false)
|
|
982
1230
|
if (!autoSend || !voiceModeRef.current || chunks.length === 0) return
|
|
983
1231
|
if (!heardVoice) {
|
|
984
|
-
setVoiceTranscript('
|
|
1232
|
+
setVoiceTranscript('No speech detected')
|
|
985
1233
|
scheduleVoiceListen(700)
|
|
986
1234
|
return
|
|
987
1235
|
}
|
|
988
1236
|
const blob = new Blob(chunks, { type: recorder.mimeType || 'audio/webm' })
|
|
989
1237
|
const audioDataUri = await blobToDataUri(blob)
|
|
990
|
-
setVoiceTranscript('
|
|
1238
|
+
setVoiceTranscript('Sent audio to AI')
|
|
991
1239
|
voiceAwaitingResponseRef.current = true
|
|
992
1240
|
setVoiceAwaitingResponse(true)
|
|
993
1241
|
onSendVoiceMessage('', audioDataUri)
|
|
@@ -1011,7 +1259,7 @@ export default function ChatPanel({
|
|
|
1011
1259
|
if (peak > 12) {
|
|
1012
1260
|
heardVoice = true
|
|
1013
1261
|
quietSince = 0
|
|
1014
|
-
setVoiceTranscript('
|
|
1262
|
+
setVoiceTranscript('Listening...')
|
|
1015
1263
|
} else if (heardVoice) {
|
|
1016
1264
|
quietSince = quietSince || now
|
|
1017
1265
|
}
|
|
@@ -1026,7 +1274,7 @@ export default function ChatPanel({
|
|
|
1026
1274
|
setIsRecording(false)
|
|
1027
1275
|
voiceModeRef.current = false
|
|
1028
1276
|
setVoiceMode(false)
|
|
1029
|
-
alert('
|
|
1277
|
+
alert('Failed to open microphone. Please check microphone permissions in your browser')
|
|
1030
1278
|
}
|
|
1031
1279
|
}
|
|
1032
1280
|
const startRecognition = (autoSend: boolean) => {
|
|
@@ -1544,8 +1792,8 @@ export default function ChatPanel({
|
|
|
1544
1792
|
}}
|
|
1545
1793
|
>
|
|
1546
1794
|
<div style={{ fontSize: '3.5rem', marginBottom: '16px' }}>🖼️</div>
|
|
1547
|
-
<div style={{ fontSize: '1.25rem', fontWeight: 'bold', letterSpacing: '0.5px' }}
|
|
1548
|
-
<div style={{ fontSize: '0.85rem', color: '#94a3b8', marginTop: '8px' }}
|
|
1795
|
+
<div style={{ fontSize: '1.25rem', fontWeight: 'bold', letterSpacing: '0.5px' }}>Drag files to attach data</div>
|
|
1796
|
+
<div style={{ fontSize: '0.85rem', color: '#94a3b8', marginTop: '8px' }}>Supports images (PNG, JPEG, WebP, GIF) and PDF files</div>
|
|
1549
1797
|
</div>
|
|
1550
1798
|
)}
|
|
1551
1799
|
|
|
@@ -1568,6 +1816,19 @@ export default function ChatPanel({
|
|
|
1568
1816
|
<div className="bubble-wrapper">
|
|
1569
1817
|
{renderCompletedActivity(interaction)}
|
|
1570
1818
|
{renderFileChanges(interaction)}
|
|
1819
|
+
{(() => {
|
|
1820
|
+
const progress = agentActivitySnapshots[String(interaction.id)] ?? interaction.agentActivity ?? []
|
|
1821
|
+
const thoughts = thoughtsFrom(progress)
|
|
1822
|
+
return (
|
|
1823
|
+
<ThinkingBlock
|
|
1824
|
+
blockKey={String(interaction.id)}
|
|
1825
|
+
thoughts={thoughts}
|
|
1826
|
+
expanded={thinkingExpanded[String(interaction.id)] ?? false}
|
|
1827
|
+
onExpandedChange={onThinkingExpandedChange}
|
|
1828
|
+
showEmptyHint={hasAgentToolActivity(progress) && thoughts.length === 0}
|
|
1829
|
+
/>
|
|
1830
|
+
)
|
|
1831
|
+
})()}
|
|
1571
1832
|
<div className="message-bubble" style={{ whiteSpace: 'pre-wrap' }}>{renderFormattedMessage(interaction.aiText)}</div>
|
|
1572
1833
|
<div className="message-time" style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
1573
1834
|
<button className="provider-badge">{interaction.provider} • {interaction.model}</button>
|
|
@@ -1589,7 +1850,7 @@ export default function ChatPanel({
|
|
|
1589
1850
|
opacity: 0.7,
|
|
1590
1851
|
transition: 'all 0.2s',
|
|
1591
1852
|
}}
|
|
1592
|
-
title={speakingText === interaction.aiText ? "
|
|
1853
|
+
title={speakingText === interaction.aiText ? "Stop reading" : "Read aloud"}
|
|
1593
1854
|
>
|
|
1594
1855
|
{renderSpeakerIcon(speakingText === interaction.aiText)}
|
|
1595
1856
|
</button>
|
|
@@ -1618,6 +1879,19 @@ export default function ChatPanel({
|
|
|
1618
1879
|
{renderActiveFileChanges()}
|
|
1619
1880
|
<div className="message ai-message thinking-message">
|
|
1620
1881
|
<div className="bubble-wrapper">
|
|
1882
|
+
<ThinkingBlock
|
|
1883
|
+
blockKey="live"
|
|
1884
|
+
thoughts={thoughtsFrom(agentProgress)}
|
|
1885
|
+
isLive={true}
|
|
1886
|
+
expanded={thinkingExpanded.live ?? true}
|
|
1887
|
+
onExpandedChange={onThinkingExpandedChange}
|
|
1888
|
+
showEmptyHint={
|
|
1889
|
+
agentMode
|
|
1890
|
+
&& hasAgentToolActivity(agentProgress)
|
|
1891
|
+
&& thoughtsFrom(agentProgress).length === 0
|
|
1892
|
+
&& !streamedReply
|
|
1893
|
+
}
|
|
1894
|
+
/>
|
|
1621
1895
|
<div className="message-bubble">
|
|
1622
1896
|
<span>
|
|
1623
1897
|
{streamedReply ? (
|
|
@@ -1663,7 +1937,7 @@ export default function ChatPanel({
|
|
|
1663
1937
|
opacity: 0.7,
|
|
1664
1938
|
transition: 'all 0.2s',
|
|
1665
1939
|
}}
|
|
1666
|
-
title={speakingText === streamedReply ? "
|
|
1940
|
+
title={speakingText === streamedReply ? "Stop reading" : "Read aloud"}
|
|
1667
1941
|
>
|
|
1668
1942
|
{renderSpeakerIcon(speakingText === streamedReply)}
|
|
1669
1943
|
</button>
|
|
@@ -56,7 +56,7 @@ export default function DashboardSidebar({
|
|
|
56
56
|
<div
|
|
57
57
|
className="sidebar-brand clickable"
|
|
58
58
|
onClick={onToggleSidebar}
|
|
59
|
-
title={sidebarCollapsed ? "
|
|
59
|
+
title={sidebarCollapsed ? "Expand sidebar" : "Collapse sidebar"}
|
|
60
60
|
role="button"
|
|
61
61
|
tabIndex={0}
|
|
62
62
|
onKeyDown={(event) => {
|