@pheem49/mint 1.6.4 → 1.7.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.
Files changed (34) hide show
  1. package/Cargo.toml +1 -1
  2. package/bin/mint +0 -0
  3. package/crates/mint-cli/src/agent.rs +42 -14
  4. package/crates/mint-cli/src/main.rs +389 -2
  5. package/crates/mint-cli/src/setup.rs +173 -32
  6. package/crates/mint-core/src/api_server.rs +39 -0
  7. package/crates/mint-core/src/config.rs +2 -2
  8. package/crates/mint-core/src/memory.rs +27 -1
  9. package/crates/mint-core/src/orchestration.rs +1 -1
  10. package/crates/mint-core/src/plugins.rs +32 -0
  11. package/crates/mint-core/tests/memory_persistence.rs +28 -0
  12. package/package.json +1 -1
  13. package/src/renderer/src/agentProgress.ts +39 -0
  14. package/src/renderer/src/components/ChatPanel.tsx +308 -34
  15. package/src/renderer/src/components/DashboardSidebar.tsx +3 -3
  16. package/src/renderer/src/components/MintDashboard.tsx +71 -27
  17. package/src/renderer/src/components/ModelPanel.tsx +2 -2
  18. package/src/renderer/src/components/PicturesLibrary.tsx +5 -1
  19. package/src/renderer/src/components/SpotlightWindow.tsx +8 -8
  20. package/src/renderer/src/components/WorkspacePanel.tsx +1 -1
  21. package/src/renderer/src/css/styles.css +195 -17
  22. package/src/renderer/src/index.css +46 -0
  23. package/src/renderer/src/tauri.ts +25 -0
  24. package/src/renderer/src-web/App.tsx +16 -1
  25. package/src/renderer/src-web/agentProgress.ts +39 -0
  26. package/src/renderer/src-web/components/ChatPanel.tsx +309 -35
  27. package/src/renderer/src-web/components/DashboardSidebar.tsx +1 -1
  28. package/src/renderer/src-web/components/MintDashboard.tsx +53 -16
  29. package/src/renderer/src-web/components/PicturesLibrary.tsx +5 -1
  30. package/src/renderer/src-web/css/styles.css +242 -18
  31. package/src/renderer/src-web/index.css +46 -0
  32. package/src/renderer/src-web/tauri.ts +25 -0
  33. package/src-tauri/src/lib.rs +17 -0
  34. package/src-tauri/tauri.conf.json +1 -1
@@ -1,4 +1,5 @@
1
1
  import { useEffect, useRef, useState, Fragment, type ChangeEvent, type ClipboardEvent, type DragEvent, type FormEvent, type KeyboardEvent, type RefObject } 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
@@ -419,6 +422,11 @@ function ChatCodeBlock({ code, language }: { code: string; language: string; key
419
422
  )
420
423
  }
421
424
 
425
+ const isTableLine = (line: string): boolean => {
426
+ const trimmed = line.trim()
427
+ return trimmed.startsWith('|') && trimmed.endsWith('|') && trimmed.length > 1
428
+ }
429
+
422
430
  function renderFormattedMessage(text: string) {
423
431
  const displayText = readableAssistantText(text)
424
432
  if (!displayText) return null
@@ -447,6 +455,113 @@ function renderFormattedMessage(text: string) {
447
455
  })
448
456
  }
449
457
 
458
+ const renderHtmlTable = (tableLines: string[], key: string) => {
459
+ const rows: string[][] = []
460
+ for (const line of tableLines) {
461
+ const trimmed = line.trim()
462
+ if (!trimmed) continue
463
+ const content = trimmed.slice(1, -1)
464
+ const contentEscaped = content.replace(/\\\|/g, '\u0000')
465
+ const cols = contentEscaped.split('|').map(s => s.replace(/\u0000/g, '|').trim())
466
+ rows.push(cols)
467
+ }
468
+
469
+ if (rows.length === 0) return null
470
+
471
+ let hasSeparator = false
472
+ let separatorIdx = -1
473
+ for (let i = 0; i < rows.length; i++) {
474
+ const row = rows[i]
475
+ if (row.length > 0 && row.every(col => /^[-\s:]+$/.test(col))) {
476
+ hasSeparator = true
477
+ separatorIdx = i
478
+ break
479
+ }
480
+ }
481
+
482
+ let headerRow: string[] | null = null
483
+ const dataRows: string[][] = []
484
+
485
+ if (hasSeparator) {
486
+ if (separatorIdx > 0) {
487
+ headerRow = rows[separatorIdx - 1]
488
+ for (let i = 0; i < rows.length; i++) {
489
+ if (i !== separatorIdx && i !== separatorIdx - 1) {
490
+ dataRows.push(rows[i])
491
+ }
492
+ }
493
+ } else {
494
+ for (let i = 0; i < rows.length; i++) {
495
+ if (i !== separatorIdx) {
496
+ dataRows.push(rows[i])
497
+ }
498
+ }
499
+ }
500
+ } else {
501
+ headerRow = rows[0]
502
+ for (let i = 1; i < rows.length; i++) {
503
+ dataRows.push(rows[i])
504
+ }
505
+ }
506
+
507
+ return (
508
+ <div key={key} className="chat-table-container" style={{
509
+ overflowX: 'auto',
510
+ margin: '14px 0',
511
+ width: '100%',
512
+ borderRadius: '8px',
513
+ border: '1px solid rgba(255, 255, 255, 0.12)',
514
+ background: 'rgba(30, 41, 59, 0.35)',
515
+ boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
516
+ }}>
517
+ <table className="chat-table" style={{
518
+ width: '100%',
519
+ borderCollapse: 'collapse',
520
+ fontSize: '0.86rem',
521
+ textAlign: 'left',
522
+ lineHeight: '1.5',
523
+ }}>
524
+ {headerRow && (
525
+ <thead>
526
+ <tr style={{
527
+ background: 'rgba(255, 255, 255, 0.04)',
528
+ borderBottom: '2px solid rgba(255, 255, 255, 0.15)',
529
+ }}>
530
+ {headerRow.map((col, idx) => (
531
+ <th key={`th-${idx}`} style={{
532
+ padding: '12px 16px',
533
+ fontWeight: 700,
534
+ color: 'var(--accent, #38bdf8)',
535
+ fontFamily: 'ui-sans-serif, system-ui, sans-serif',
536
+ }}>
537
+ {formatInline(col)}
538
+ </th>
539
+ ))}
540
+ </tr>
541
+ </thead>
542
+ )}
543
+ <tbody>
544
+ {dataRows.map((row, rIdx) => (
545
+ <tr key={`tr-${rIdx}`} style={{
546
+ background: rIdx % 2 === 1 ? 'rgba(255, 255, 255, 0.015)' : 'transparent',
547
+ borderBottom: rIdx < dataRows.length - 1 ? '1px solid rgba(255, 255, 255, 0.08)' : 'none',
548
+ }}>
549
+ {row.map((col, cIdx) => (
550
+ <td key={`td-${cIdx}`} style={{
551
+ padding: '12px 16px',
552
+ color: 'var(--text-main, #e2e8f0)',
553
+ }}>
554
+ {formatInline(col)}
555
+ </td>
556
+ ))}
557
+ </tr>
558
+ ))}
559
+ </tbody>
560
+ </table>
561
+ </div>
562
+ )
563
+ }
564
+
450
565
  const lines = displayText.split('\n')
451
566
  const items: any[] = []
452
567
 
@@ -454,10 +569,24 @@ function renderFormattedMessage(text: string) {
454
569
  let codeBlockLang = ''
455
570
  let codeBlockLines: string[] = []
456
571
 
572
+ let inTable = false
573
+ let tableLines: string[] = []
574
+
575
+ const flushTable = (index: number) => {
576
+ if (tableLines.length > 0) {
577
+ items.push(renderHtmlTable(tableLines, `table-${index}`))
578
+ tableLines = []
579
+ inTable = false
580
+ }
581
+ }
582
+
457
583
  for (let i = 0; i < lines.length; i++) {
458
584
  const line = lines[i]
459
585
 
460
586
  if (line.trim().startsWith('```')) {
587
+ if (inTable) {
588
+ flushTable(i)
589
+ }
461
590
  if (inCodeBlock) {
462
591
  const codeText = codeBlockLines.join('\n')
463
592
  items.push(
@@ -481,35 +610,60 @@ function renderFormattedMessage(text: string) {
481
610
  continue
482
611
  }
483
612
 
484
- const headerMatch = line.match(/^(#{1,6})\s+(.*)$/)
485
- if (headerMatch) {
486
- const level = headerMatch[1].length
487
- const content = headerMatch[2]
488
-
489
- const style = {
490
- fontWeight: 'bold',
491
- display: 'block',
492
- marginTop: level === 1 ? '16px' : level === 2 ? '14px' : '10px',
493
- marginBottom: '6px',
494
- fontSize: level === 1 ? '1.25em' : level === 2 ? '1.15em' : '1.05em',
495
- color: 'var(--text-main)',
613
+ if (isTableLine(line)) {
614
+ inTable = true
615
+ tableLines.push(line)
616
+ } else {
617
+ if (inTable) {
618
+ flushTable(i)
496
619
  }
497
620
 
498
- items.push(
499
- <span key={`line-${i}`} style={style}>
500
- {formatInline(content)}
501
- </span>
502
- )
503
- } else {
504
- items.push(
505
- <Fragment key={`line-${i}`}>
506
- {formatInline(line)}
507
- {i < lines.length - 1 && '\n'}
508
- </Fragment>
509
- )
621
+ const headerMatch = line.match(/^(#{1,6})\s+(.*)$/)
622
+ if (headerMatch) {
623
+ const level = headerMatch[1].length
624
+ const content = headerMatch[2]
625
+
626
+ const style = {
627
+ fontWeight: 'bold',
628
+ display: 'block',
629
+ marginTop: level === 1 ? '16px' : level === 2 ? '14px' : '10px',
630
+ marginBottom: '6px',
631
+ fontSize: level === 1 ? '1.25em' : level === 2 ? '1.15em' : '1.05em',
632
+ color: 'var(--text-main)',
633
+ }
634
+
635
+ items.push(
636
+ <span key={`line-${i}`} style={style}>
637
+ {formatInline(content)}
638
+ </span>
639
+ )
640
+ } else {
641
+ const listMatch = line.match(/^(\s*)([-*+])\s+(.*)$/)
642
+ if (listMatch) {
643
+ const indent = listMatch[1]
644
+ const content = listMatch[3]
645
+ items.push(
646
+ <Fragment key={`line-${i}`}>
647
+ {indent}• {formatInline(content)}
648
+ {i < lines.length - 1 && '\n'}
649
+ </Fragment>
650
+ )
651
+ } else {
652
+ items.push(
653
+ <Fragment key={`line-${i}`}>
654
+ {formatInline(line)}
655
+ {i < lines.length - 1 && '\n'}
656
+ </Fragment>
657
+ )
658
+ }
659
+ }
510
660
  }
511
661
  }
512
662
 
663
+ if (inTable) {
664
+ flushTable(lines.length)
665
+ }
666
+
513
667
  if (inCodeBlock && codeBlockLines.length > 0) {
514
668
  const codeText = codeBlockLines.join('\n')
515
669
  items.push(
@@ -524,6 +678,98 @@ function renderFormattedMessage(text: string) {
524
678
  return items
525
679
  }
526
680
 
681
+ const THINKING_LABELS = {
682
+ live: 'Thinking…',
683
+ completed: (count: number) => `Thinking process (${count} steps)`,
684
+ step: (index: number) => `Step ${index}`,
685
+ emptyHint: 'Model didn\'t send thinking steps this time',
686
+ } as const
687
+
688
+ function ThinkingBlock({
689
+ thoughts,
690
+ isLive = false,
691
+ blockKey,
692
+ expanded,
693
+ onExpandedChange,
694
+ showEmptyHint = false,
695
+ }: {
696
+ thoughts: string[]
697
+ isLive?: boolean
698
+ blockKey: string
699
+ expanded?: boolean
700
+ onExpandedChange?: (key: string, open: boolean) => void
701
+ showEmptyHint?: boolean
702
+ }) {
703
+ const contentRef = useRef<HTMLDivElement>(null)
704
+ const isControlled = expanded !== undefined && Boolean(onExpandedChange)
705
+ const [localOpen, setLocalOpen] = useState(isLive)
706
+ const isOpen = isControlled ? Boolean(expanded) : localOpen
707
+
708
+ const setOpen = (open: boolean) => {
709
+ if (isControlled && onExpandedChange) onExpandedChange(blockKey, open)
710
+ else setLocalOpen(open)
711
+ }
712
+
713
+ useEffect(() => {
714
+ if (isLive && !isControlled) setLocalOpen(true)
715
+ }, [isLive, isControlled])
716
+
717
+ useEffect(() => {
718
+ if (!isLive || !isOpen || !contentRef.current) return
719
+ contentRef.current.scrollTop = contentRef.current.scrollHeight
720
+ }, [thoughts, isLive, isOpen])
721
+
722
+ if (thoughts.length === 0 && !showEmptyHint) return null
723
+
724
+ if (thoughts.length === 0) {
725
+ return (
726
+ <div className="thinking-block thinking-block-empty-state" data-live={isLive ? 'true' : 'false'}>
727
+ <span className="thinking-block-empty">{THINKING_LABELS.emptyHint}</span>
728
+ </div>
729
+ )
730
+ }
731
+
732
+ return (
733
+ <div className={`thinking-block${isLive ? ' is-live' : ''}`}>
734
+ <button
735
+ type="button"
736
+ className="thinking-block-header"
737
+ onClick={() => setOpen(!isOpen)}
738
+ aria-expanded={isOpen}
739
+ >
740
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ flexShrink: 0 }}>
741
+ <path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" />
742
+ <line x1="12" y1="17" x2="12.01" y2="17" />
743
+ <circle cx="12" cy="12" r="10" />
744
+ </svg>
745
+ <span className="thinking-block-label">
746
+ {isLive ? THINKING_LABELS.live : THINKING_LABELS.completed(thoughts.length)}
747
+ </span>
748
+ {isLive && <span className="thinking-block-live-dot" aria-hidden="true" />}
749
+ <span className={`thinking-block-chevron${isOpen ? ' is-open' : ''}`} aria-hidden="true">
750
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
751
+ <polyline points="9 18 15 12 9 6" />
752
+ </svg>
753
+ </span>
754
+ </button>
755
+ {isOpen && (
756
+ <div className="thinking-block-body">
757
+ <div className="thinking-block-content" ref={contentRef}>
758
+ {thoughts.map((thought, index) => (
759
+ <div className="thinking-block-step" key={`${blockKey}-${index}`}>
760
+ {thoughts.length > 1 && (
761
+ <div className="thinking-block-step-label">{THINKING_LABELS.step(index + 1)}</div>
762
+ )}
763
+ <div className="thinking-block-step-body">{renderFormattedMessage(thought)}</div>
764
+ </div>
765
+ ))}
766
+ </div>
767
+ </div>
768
+ )}
769
+ </div>
770
+ )
771
+ }
772
+
527
773
  function readableAssistantText(text: string) {
528
774
  if (typeof text !== 'string') return ''
529
775
  const trimmed = text.trim()
@@ -693,6 +939,8 @@ export default function ChatPanel({
693
939
  streamedResponse,
694
940
  agentProgress,
695
941
  agentActivitySnapshots,
942
+ thinkingExpanded,
943
+ onThinkingExpandedChange,
696
944
  message,
697
945
  imageAttachments,
698
946
  documentName,
@@ -770,7 +1018,7 @@ export default function ChatPanel({
770
1018
  const canSubmit = Boolean(message.trim() || imageAttachments.length > 0 || documentName)
771
1019
  const sendingImageMarkers = Array.from({ length: sendingImageCount }, (_, index) => `[Image #${index + 1}]`).join(' ')
772
1020
  const voiceStatus = speakingText ? 'speaking' : (sending || voiceAwaitingResponse) ? 'thinking' : isRecording ? 'listening' : voiceMode ? 'ready' : 'off'
773
- const voiceStatusLabel = voiceStatus === 'speaking' ? 'กำลังตอบ' : voiceStatus === 'thinking' ? 'กำลังคิด' : voiceStatus === 'listening' ? 'กำลังฟัง' : 'พร้อมฟัง'
1021
+ const voiceStatusLabel = voiceStatus === 'speaking' ? 'Speaking' : voiceStatus === 'thinking' ? 'Thinking' : voiceStatus === 'listening' ? 'Listening' : 'Ready'
774
1022
 
775
1023
  const getAvailableModels = (provider: string) => {
776
1024
  switch (provider) {
@@ -954,7 +1202,7 @@ export default function ChatPanel({
954
1202
  }
955
1203
  const startAudioRecording = async (autoSend: boolean) => {
956
1204
  if (!navigator.mediaDevices?.getUserMedia || typeof MediaRecorder === 'undefined') {
957
- alert('ขออภัย ระบบนี้ไม่สามารถเข้าถึงไมค์หรืออัดเสียงใน WebView นี้ได้')
1205
+ alert('Sorry, this system cannot access the microphone or record audio in this WebView')
958
1206
  voiceModeRef.current = false
959
1207
  setVoiceMode(false)
960
1208
  return
@@ -977,7 +1225,7 @@ export default function ChatPanel({
977
1225
  mediaStreamRef.current = stream
978
1226
  mediaRecorderRef.current = recorder
979
1227
  audioContextRef.current = audioContext
980
- setVoiceTranscript('กำลังฟังเสียงจากไมค์')
1228
+ setVoiceTranscript('Listening to microphone...')
981
1229
 
982
1230
  recorder.ondataavailable = (event) => {
983
1231
  if (event.data.size > 0) chunks.push(event.data)
@@ -992,13 +1240,13 @@ export default function ChatPanel({
992
1240
  setIsRecording(false)
993
1241
  if (!autoSend || !voiceModeRef.current || chunks.length === 0) return
994
1242
  if (!heardVoice) {
995
- setVoiceTranscript('ยังไม่ได้ยินเสียงพูด')
1243
+ setVoiceTranscript('No speech detected')
996
1244
  scheduleVoiceListen(700)
997
1245
  return
998
1246
  }
999
1247
  const blob = new Blob(chunks, { type: recorder.mimeType || 'audio/webm' })
1000
1248
  const audioDataUri = await blobToDataUri(blob)
1001
- setVoiceTranscript('ส่งเสียงให้ AI แล้ว')
1249
+ setVoiceTranscript('Sent audio to AI')
1002
1250
  voiceAwaitingResponseRef.current = true
1003
1251
  setVoiceAwaitingResponse(true)
1004
1252
  onSendVoiceMessage('', audioDataUri)
@@ -1022,7 +1270,7 @@ export default function ChatPanel({
1022
1270
  if (peak > 12) {
1023
1271
  heardVoice = true
1024
1272
  quietSince = 0
1025
- setVoiceTranscript('กำลังฟัง...')
1273
+ setVoiceTranscript('Listening...')
1026
1274
  } else if (heardVoice) {
1027
1275
  quietSince = quietSince || now
1028
1276
  }
@@ -1037,7 +1285,7 @@ export default function ChatPanel({
1037
1285
  setIsRecording(false)
1038
1286
  voiceModeRef.current = false
1039
1287
  setVoiceMode(false)
1040
- alert('เปิดไมค์ไม่สำเร็จ กรุณาตรวจสิทธิ์ microphone ของแอป')
1288
+ alert('Failed to open microphone. Please check microphone permissions for this app')
1041
1289
  }
1042
1290
  }
1043
1291
  const startRecognition = (autoSend: boolean) => {
@@ -1472,6 +1720,19 @@ export default function ChatPanel({
1472
1720
  <div className="bubble-wrapper">
1473
1721
  {renderCompletedActivity(interaction)}
1474
1722
  {renderFileChanges(interaction)}
1723
+ {(() => {
1724
+ const progress = agentActivitySnapshots[String(interaction.id)] ?? interaction.agentActivity ?? []
1725
+ const thoughts = thoughtsFrom(progress)
1726
+ return (
1727
+ <ThinkingBlock
1728
+ blockKey={String(interaction.id)}
1729
+ thoughts={thoughts}
1730
+ expanded={thinkingExpanded[String(interaction.id)] ?? false}
1731
+ onExpandedChange={onThinkingExpandedChange}
1732
+ showEmptyHint={hasAgentToolActivity(progress) && thoughts.length === 0}
1733
+ />
1734
+ )
1735
+ })()}
1475
1736
  <div className="message-bubble" style={{ whiteSpace: 'pre-wrap' }}>{renderFormattedMessage(interaction.aiText)}</div>
1476
1737
  <div className="message-time" style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
1477
1738
  <button className="provider-badge">{interaction.provider} • {interaction.model}</button>
@@ -1481,7 +1742,7 @@ export default function ChatPanel({
1481
1742
  type="button"
1482
1743
  className={`tts-btn ${speakingText === interaction.aiText ? 'is-speaking' : ''}`}
1483
1744
  onClick={() => speak(interaction.aiText)}
1484
- title={speakingText === interaction.aiText ? 'หยุดอ่านออกเสียง' : 'อ่านออกเสียง'}
1745
+ title={speakingText === interaction.aiText ? 'Stop reading' : 'Read aloud'}
1485
1746
  >
1486
1747
  {renderSpeakerIcon(speakingText === interaction.aiText)}
1487
1748
  </button>
@@ -1510,6 +1771,19 @@ export default function ChatPanel({
1510
1771
  {renderActiveFileChanges()}
1511
1772
  <div className="message ai-message thinking-message">
1512
1773
  <div className="bubble-wrapper">
1774
+ <ThinkingBlock
1775
+ blockKey="live"
1776
+ thoughts={thoughtsFrom(agentProgress)}
1777
+ isLive={true}
1778
+ expanded={thinkingExpanded.live ?? true}
1779
+ onExpandedChange={onThinkingExpandedChange}
1780
+ showEmptyHint={
1781
+ agentMode
1782
+ && hasAgentToolActivity(agentProgress)
1783
+ && thoughtsFrom(agentProgress).length === 0
1784
+ && !streamedReply
1785
+ }
1786
+ />
1513
1787
  <div className="message-bubble">
1514
1788
  <span>
1515
1789
  {streamedReply ? (
@@ -1543,7 +1817,7 @@ export default function ChatPanel({
1543
1817
  type="button"
1544
1818
  className={`tts-btn ${speakingText === streamedReply ? 'is-speaking' : ''}`}
1545
1819
  onClick={() => speak(streamedReply)}
1546
- title={speakingText === streamedReply ? 'หยุดอ่านออกเสียง' : 'อ่านออกเสียง'}
1820
+ title={speakingText === streamedReply ? 'Stop reading' : 'Read aloud'}
1547
1821
  >
1548
1822
  {renderSpeakerIcon(speakingText === streamedReply)}
1549
1823
  </button>
@@ -1795,7 +2069,7 @@ export default function ChatPanel({
1795
2069
  className={`${isRecording ? 'is-recording' : ''} ${voiceMode ? 'voice-mode-active' : ''}`}
1796
2070
  type="button"
1797
2071
  onClick={toggleRecording}
1798
- title={voiceMode ? 'ปิดโหมดสนทนาเสียง' : 'เปิดโหมดสนทนาเสียง'}
2072
+ title={voiceMode ? 'Disable voice conversation' : 'Enable voice conversation'}
1799
2073
  style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}
1800
2074
  >
1801
2075
  {isRecording ? (
@@ -73,13 +73,13 @@ export default function DashboardSidebar({
73
73
  const toggleInteractionGuide = () => {
74
74
  const next = !showInteractionGuide
75
75
  onSetShowInteractionGuide(next)
76
- onShowToast(next ? 'เปิดการแสดงจุดสัมผัส (Interaction Zones) ⊹' : 'ปิดการแสดงจุดสัมผัส ⊹')
76
+ onShowToast(next ? 'Show interaction zones ⊹' : 'Hide interaction zones ⊹')
77
77
  }
78
78
 
79
79
  const toggleInteraction = () => {
80
80
  const next = !interactionEnabled
81
81
  onSetInteractionEnabled(next)
82
- onShowToast(next ? 'เปิดการโต้ตอบกับโมเดล ⦸' : 'ปิดการโต้ตอบกับโมเดล ⦸')
82
+ onShowToast(next ? 'Enable model interaction ⦸' : 'Disable model interaction ⦸')
83
83
  }
84
84
 
85
85
  const conversationSessions = chatSessions.filter((session) => session.kind !== 'cli' && session.id !== 'conversation-default')
@@ -94,7 +94,7 @@ export default function DashboardSidebar({
94
94
  <div
95
95
  className="sidebar-brand clickable"
96
96
  onClick={onToggleSidebar}
97
- title={sidebarCollapsed ? "ขยายแถบด้านข้าง" : "ยุบแถบด้านข้าง"}
97
+ title={sidebarCollapsed ? "Expand sidebar" : "Collapse sidebar"}
98
98
  role="button"
99
99
  tabIndex={0}
100
100
  onKeyDown={(event) => {