@pheem49/mint 1.8.1 → 1.8.2

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 (184) hide show
  1. package/Cargo.lock +3 -3
  2. package/Cargo.toml +1 -1
  3. package/README.md +0 -17
  4. package/Release_Note.md +92 -58
  5. package/bin/mint +0 -0
  6. package/crates/mint-cli/src/agent.rs +100 -6
  7. package/crates/mint-cli/src/main.rs +166 -52
  8. package/crates/mint-core/src/agent_loop.rs +1 -0
  9. package/crates/mint-core/src/api_server.rs +55 -2
  10. package/crates/mint-core/src/browser.rs +55 -0
  11. package/crates/mint-core/src/channels.rs +1 -0
  12. package/crates/mint-core/src/chat.rs +3 -0
  13. package/crates/mint-core/src/config.rs +61 -0
  14. package/crates/mint-core/src/lib.rs +14 -0
  15. package/crates/mint-core/src/orchestration.rs +180 -11
  16. package/crates/mint-core/src/tts.rs +3 -1
  17. package/flatpak/com.pheem49.mint.desktop +9 -0
  18. package/flatpak/com.pheem49.mint.metainfo.xml +35 -0
  19. package/flatpak/com.pheem49.mint.yaml +52 -0
  20. package/package.json +1 -1
  21. package/shared-modules/.github/workflows/ci.yml +120 -0
  22. package/shared-modules/.github/workflows/update.yaml +28 -0
  23. package/shared-modules/CODEOWNERS +23 -0
  24. package/shared-modules/README.md +79 -0
  25. package/shared-modules/SDL/README.md +3 -0
  26. package/shared-modules/SDL/SDL-1.2.15.json +51 -0
  27. package/shared-modules/SDL/SDL_Pango-0.1.2-API-adds.patch +118 -0
  28. package/shared-modules/SDL/SDL_image-1.2.12.json +25 -0
  29. package/shared-modules/SDL/SDL_mixer-1.2.12.json +27 -0
  30. package/shared-modules/SDL/SDL_net-1.2.8.json +19 -0
  31. package/shared-modules/SDL/SDL_pango-0.1.2.json +26 -0
  32. package/shared-modules/SDL/SDL_ttf-2.0.11.json +28 -0
  33. package/shared-modules/SDL/libsdl-1.2.15-strict-prototypes.patch +316 -0
  34. package/shared-modules/SDL/sdl-check-for-SDL_VIDEO_X11_BACKINGSTORE.patch +24 -0
  35. package/shared-modules/SDL/sdl-libx11-build.patch +59 -0
  36. package/shared-modules/SDL/sdl-sysloadso-buffer-length.patch +27 -0
  37. package/shared-modules/SDL/sdl12-compat.json +22 -0
  38. package/shared-modules/SDL2/SDL2_image.json +27 -0
  39. package/shared-modules/SDL2/SDL2_ttf.json +29 -0
  40. package/shared-modules/cld2/CMakeLists.txt +157 -0
  41. package/shared-modules/cld2/cld2.json +24 -0
  42. package/shared-modules/clutter/clutter.json +72 -0
  43. package/shared-modules/dbus-glib/dbus-glib.json +29 -0
  44. package/shared-modules/glew/glew.json +36 -0
  45. package/shared-modules/glu/glu-9.json +12 -0
  46. package/shared-modules/gtk2/arc-gtk2-theme-Replace-Inkscape-dependency-with-rsvg-convert.patch +180 -0
  47. package/shared-modules/gtk2/gtk2-common-themes.json +360 -0
  48. package/shared-modules/gtk2/gtk2-fix-crash-in-show-uri.patch +30 -0
  49. package/shared-modules/gtk2/gtk2-gcc14.patch +281 -0
  50. package/shared-modules/gtk2/gtk2-gcc15.patch +250 -0
  51. package/shared-modules/gtk2/gtk2-murrine-c99.patch +34 -0
  52. package/shared-modules/gtk2/gtk2-use-adwaita-theme.patch +80 -0
  53. package/shared-modules/gtk2/gtk2.json +120 -0
  54. package/shared-modules/gtk2/ibus-gcc15.patch +13 -0
  55. package/shared-modules/gtk2/murrine-engine-fix-crash.patch +12 -0
  56. package/shared-modules/gtk2/readme.md +10 -0
  57. package/shared-modules/intltool/intltool-0.51.json +20 -0
  58. package/shared-modules/intltool/intltool-perl5.26-regex-fixes.patch +59 -0
  59. package/shared-modules/lame/0001-Build-xmm_quantize_sub.c-with-msse.patch +34 -0
  60. package/shared-modules/lame/0002-Include-GTK-1-autoconf-directives-in-build-system.patch +222 -0
  61. package/shared-modules/lame/0003-Patch-out-remaining-ansi2knr.patch +70 -0
  62. package/shared-modules/lame/0004-initscr-is-not-used-anywhere-in-lame-sourcetree.patch +41 -0
  63. package/shared-modules/lame/0005-Update-autoconf.patch +81 -0
  64. package/shared-modules/lame/lame-3.99.5.json +32 -0
  65. package/shared-modules/libappindicator/libappindicator-gtk2-12.10.json +140 -0
  66. package/shared-modules/libappindicator/libappindicator-gtk3-12.10.json +140 -0
  67. package/shared-modules/libappindicator/libappindicator-gtk3-introspection-12.10.json +140 -0
  68. package/shared-modules/libappindicator/patches/0001-libappindicator-ftbfs.patch +20 -0
  69. package/shared-modules/libappindicator/patches/0002-libappindicator-no-python.patch +888 -0
  70. package/shared-modules/libappindicator/patches/0003-libappindicator-fix-crash-from-incorrect-signal-emission.patch +74 -0
  71. package/shared-modules/libappindicator/patches/0004-libappindicator-fix-crash-iterating-icon-themes.patch +11 -0
  72. package/shared-modules/libayatana-appindicator/0001-Fix-HAVE_VALGRIND-AM_CONDITIONAL.patch +51 -0
  73. package/shared-modules/libayatana-appindicator/0001-Make-introspection-configurable.patch +44 -0
  74. package/shared-modules/libayatana-appindicator/libayatana-appindicator-gtk3.json +121 -0
  75. package/shared-modules/libcanberra/0001-gtk-Don-t-assume-all-GdkDisplays-are-GdkX11Displays-.patch +123 -0
  76. package/shared-modules/libcanberra/libcanberra.json +44 -0
  77. package/shared-modules/libmad/libmad.json +19 -0
  78. package/shared-modules/libsecret/libsecret.json +23 -0
  79. package/shared-modules/libsoup/libsoup-2.4.json +14 -0
  80. package/shared-modules/libusb/libusb.json +27 -0
  81. package/shared-modules/linux-audio/dssi.json +31 -0
  82. package/shared-modules/linux-audio/fftw3f-static.json +44 -0
  83. package/shared-modules/linux-audio/fftw3f.json +42 -0
  84. package/shared-modules/linux-audio/fluidsynth2-static.json +30 -0
  85. package/shared-modules/linux-audio/fluidsynth2.json +19 -0
  86. package/shared-modules/linux-audio/ladspa.json +26 -0
  87. package/shared-modules/linux-audio/lash.json +35 -0
  88. package/shared-modules/linux-audio/libinstpatch.json +16 -0
  89. package/shared-modules/linux-audio/liblo-static.json +22 -0
  90. package/shared-modules/linux-audio/liblo.json +17 -0
  91. package/shared-modules/linux-audio/lilv-static.json +94 -0
  92. package/shared-modules/linux-audio/lilv.json +82 -0
  93. package/shared-modules/linux-audio/lrdf.json +57 -0
  94. package/shared-modules/linux-audio/lv2.json +21 -0
  95. package/shared-modules/linux-audio/patches/lash/0001-configure-remove-aclocal-m4.patch +29 -0
  96. package/shared-modules/linux-audio/patches/lash/0002-Fix-build-with-recent-gcc.patch +73 -0
  97. package/shared-modules/linux-audio/patches/lash/0003-Fix-linking.patch +136 -0
  98. package/shared-modules/linux-audio/patches/raptor-libxml.patch +31 -0
  99. package/shared-modules/linux-audio/patches/stk-install.sh +54 -0
  100. package/shared-modules/linux-audio/stk.json +28 -0
  101. package/shared-modules/lua5.1/0004-Fix-stack-overflow-in-vararg-functions.patch +21 -0
  102. package/shared-modules/lua5.1/lua-5.1.5-so.patch +43 -0
  103. package/shared-modules/lua5.1/lua-5.1.5.json +42 -0
  104. package/shared-modules/lua5.3/lua-5.3.5.json +41 -0
  105. package/shared-modules/lua5.3/lua.pc.in +8 -0
  106. package/shared-modules/lua5.3/luaconf.patch +47 -0
  107. package/shared-modules/lua5.3/makefile-add-liblua.so-rule.patch +47 -0
  108. package/shared-modules/lua5.4/lua-5.4.json +41 -0
  109. package/shared-modules/lua5.4/lua.pc.in +8 -0
  110. package/shared-modules/lua5.4/luaconf.patch +34 -0
  111. package/shared-modules/lua5.4/makefile-add-liblua.so-rule.patch +42 -0
  112. package/shared-modules/luajit/luajit.json +30 -0
  113. package/shared-modules/lzo/lzo.json +27 -0
  114. package/shared-modules/mac/mac.json +18 -0
  115. package/shared-modules/physfs/physfs.json +26 -0
  116. package/shared-modules/pygame/audiofile-gcc6.patch +21 -0
  117. package/shared-modules/pygame/cython-fixes.patch +34 -0
  118. package/shared-modules/pygame/fluidsynth-no-rawmidi.patch +69 -0
  119. package/shared-modules/pygame/glib.patch +25 -0
  120. package/shared-modules/pygame/pygame1-python2.json +157 -0
  121. package/shared-modules/pygtk/pygtk-c99.patch +15 -0
  122. package/shared-modules/pygtk/pygtk.json +117 -0
  123. package/shared-modules/pygtk/pygtk.patch +39 -0
  124. package/shared-modules/python2.7/python-2.7.json +48 -0
  125. package/shared-modules/smpeg/smpeg-0.4.5.json +50 -0
  126. package/shared-modules/smpeg/smpeg-am-prog-as.patch +12 -0
  127. package/shared-modules/smpeg/smpeg-export-mpegaudio-class.patch +17 -0
  128. package/shared-modules/smpeg/smpeg-gcc6.patch +40 -0
  129. package/shared-modules/smpeg/smpeg-no-gtk.patch +37 -0
  130. package/shared-modules/squashfs-tools/squashfs-tools.json +25 -0
  131. package/shared-modules/update-checker.sh +50 -0
  132. package/shared-modules/vorbisgain/0001-temp_files.patch +75 -0
  133. package/shared-modules/vorbisgain/0002-errno.patch +33 -0
  134. package/shared-modules/vorbisgain/0003-manpage.patch +248 -0
  135. package/shared-modules/vorbisgain/0004-vorbisgain_mtime.patch +183 -0
  136. package/shared-modules/vorbisgain/0005-double_fclose.patch +21 -0
  137. package/shared-modules/vorbisgain/0006-manpage_hyphens.patch +56 -0
  138. package/shared-modules/vorbisgain/0007-recursively_spelling.patch +14 -0
  139. package/shared-modules/vorbisgain/0008-manpage_recursion_mistake.patch +23 -0
  140. package/shared-modules/vorbisgain/0009-hardening.patch +13 -0
  141. package/shared-modules/vorbisgain/0010-fclose.patch +13 -0
  142. package/shared-modules/vorbisgain/README.md +23 -0
  143. package/shared-modules/vorbisgain/vorbisgain_0.37-2.json +29 -0
  144. package/src/renderer/shared/agentProgress.ts +145 -0
  145. package/src/renderer/shared/calculator.ts +52 -0
  146. package/src/renderer/shared/components/AgentActivityDrawer.tsx +72 -0
  147. package/src/renderer/shared/components/AgentActivityTable.tsx +66 -0
  148. package/src/renderer/shared/components/ApprovalCard.tsx +93 -0
  149. package/src/renderer/shared/components/ChatCodeBlock.tsx +75 -0
  150. package/src/renderer/shared/components/ThinkingBlock.tsx +101 -0
  151. package/src/renderer/shared/constants/models.ts +91 -0
  152. package/src/renderer/shared/platform.ts +82 -0
  153. package/src/renderer/shared/types.ts +140 -0
  154. package/src/renderer/shared/utils/agentActivity.ts +187 -0
  155. package/src/renderer/shared/utils/approval.ts +1 -0
  156. package/src/renderer/shared/utils/approval.tsx +64 -0
  157. package/src/renderer/shared/utils/markdown.tsx +347 -0
  158. package/src/renderer/shared/utils/providers.ts +36 -0
  159. package/src/renderer/shared/utils/speech.ts +216 -0
  160. package/src/renderer/shared/utils/ui.ts +147 -0
  161. package/src/renderer/src/agentProgress.ts +11 -39
  162. package/src/renderer/src/calculator.ts +3 -46
  163. package/src/renderer/src/components/ChatPanel.tsx +194 -1303
  164. package/src/renderer/src/components/MintDashboard.tsx +23 -169
  165. package/src/renderer/src/components/PicturesLibrary.tsx +32 -8
  166. package/src/renderer/src/components/Settings/AgentsTab.tsx +362 -0
  167. package/src/renderer/src/components/Settings/AutomationTab.tsx +0 -22
  168. package/src/renderer/src/components/SettingsWindow.tsx +53 -3
  169. package/src/renderer/src/css/styles.css +84 -0
  170. package/src/renderer/src/tauri.ts +128 -143
  171. package/src/renderer/src-web/agentProgress.ts +11 -39
  172. package/src/renderer/src-web/calculator.ts +3 -46
  173. package/src/renderer/src-web/components/ChatPanel.tsx +194 -1301
  174. package/src/renderer/src-web/components/MintDashboard.tsx +23 -169
  175. package/src/renderer/src-web/components/PicturesLibrary.tsx +23 -29
  176. package/src/renderer/src-web/components/Settings/AgentsTab.tsx +362 -0
  177. package/src/renderer/src-web/components/Settings/AutomationTab.tsx +0 -22
  178. package/src/renderer/src-web/components/SettingsWindow.tsx +53 -3
  179. package/src/renderer/src-web/css/styles.css +84 -0
  180. package/src/renderer/src-web/tauri.ts +180 -102
  181. package/src-tauri/src/lib.rs +137 -33
  182. package/src-tauri/src/webhooks.rs +1 -0
  183. package/src-tauri/src/workflows.rs +1 -0
  184. package/src-tauri/tauri.conf.json +1 -1
@@ -0,0 +1,347 @@
1
+ /**
2
+ * shared/utils/markdown.tsx
3
+ * Custom markdown parsing and rendering engine for message bubbles.
4
+ * Shared by both Desktop and Web ChatPanel — do NOT duplicate this.
5
+ */
6
+ import { Fragment, type ReactNode } from 'react'
7
+ import { ChatCodeBlock } from '../components/ChatCodeBlock'
8
+
9
+ export const isTableLine = (line: string): boolean => {
10
+ const trimmed = line.trim()
11
+ return trimmed.startsWith('|') && trimmed.endsWith('|') && trimmed.length > 1
12
+ }
13
+
14
+ export function readableAssistantText(text: string): string {
15
+ if (typeof text !== 'string') return ''
16
+ const trimmed = text.trim()
17
+ if (!trimmed.startsWith('{')) return sanitizeLatex(text)
18
+ try {
19
+ const value = JSON.parse(trimmed)
20
+ if (value?.action === 'finish' && typeof value?.input?.summary === 'string' && value.input.summary.trim()) {
21
+ return sanitizeLatex(value.input.summary)
22
+ }
23
+ if (typeof value?.finish?.summary === 'string' && value.finish.summary.trim()) {
24
+ return sanitizeLatex(value.finish.summary)
25
+ }
26
+ } catch {
27
+ return sanitizeLatex(text)
28
+ }
29
+ return sanitizeLatex(text)
30
+ }
31
+
32
+ /** Replace common LaTeX math symbols with their Unicode equivalents */
33
+ function sanitizeLatex(text: string): string {
34
+ return text
35
+ // arrows
36
+ .replace(/\$\\rightarrow\$|\\\(\\rightarrow\\\)/g, '→')
37
+ .replace(/\$\\leftarrow\$|\\\(\\leftarrow\\\)/g, '←')
38
+ .replace(/\$\\Rightarrow\$|\\\(\\Rightarrow\\\)/g, '⇒')
39
+ .replace(/\$\\Leftarrow\$|\\\(\\Leftarrow\\\)/g, '⇐')
40
+ .replace(/\$\\leftrightarrow\$|\\\(\\leftrightarrow\\\)/g, '↔')
41
+ // comparison
42
+ .replace(/\$\\leq\$|\\\(\\leq\\\)/g, '≤')
43
+ .replace(/\$\\geq\$|\\\(\\geq\\\)/g, '≥')
44
+ .replace(/\$\\neq\$|\\\(\\neq\\\)/g, '≠')
45
+ .replace(/\$\\approx\$|\\\(\\approx\\\)/g, '≈')
46
+ // math
47
+ .replace(/\$\\times\$|\\\(\\times\\\)/g, '×')
48
+ .replace(/\$\\div\$|\\\(\\div\\\)/g, '÷')
49
+ .replace(/\$\\pm\$|\\\(\\pm\\\)/g, '±')
50
+ .replace(/\$\\infty\$|\\\(\\infty\\\)/g, '∞')
51
+ // other common symbols
52
+ .replace(/\$\\cdot\$|\\\(\\cdot\\\)/g, '·')
53
+ .replace(/\$\\in\$|\\\(\\in\\\)/g, '∈')
54
+ .replace(/\$\\subset\$|\\\(\\subset\\\)/g, '⊂')
55
+ .replace(/\$\\cup\$|\\\(\\cup\\\)/g, '∪')
56
+ .replace(/\$\\cap\$|\\\(\\cap\\\)/g, '∩')
57
+ // strip remaining inline math delimiters $...$ and \(...\) that weren't matched
58
+ .replace(/\$([^$\n]{1,60})\$/g, '$1')
59
+ .replace(/\\\(([^)]{1,60})\\\)/g, '$1')
60
+ }
61
+
62
+
63
+
64
+ export function cleanSpeechText(text: string): string {
65
+ return readableAssistantText(text)
66
+ .replace(/\*\*([\s\S]*?)\*\*/g, '$1')
67
+ .replace(/[*_`#]/g, '')
68
+ .trim()
69
+ }
70
+
71
+ export function renderSpeakerIcon(isSpeaking: boolean): ReactNode {
72
+ if (isSpeaking) {
73
+ return (
74
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" style={{ display: 'block' }}>
75
+ <polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5" />
76
+ <path d="M15.54 8.46a5 5 0 0 1 0 7.07" />
77
+ <path d="M19.07 4.93a10 10 0 0 1 0 14.14" />
78
+ </svg>
79
+ )
80
+ }
81
+ return (
82
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" style={{ display: 'block' }}>
83
+ <polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5" />
84
+ </svg>
85
+ )
86
+ }
87
+
88
+ export function renderFormattedMessage(text: string): ReactNode {
89
+ const displayText = readableAssistantText(text)
90
+ if (!displayText) return null
91
+
92
+ const formatInline = (str: string) => {
93
+ const codeParts = str.split(/`([\s\S]*?)`/g)
94
+ return codeParts.map((codePart, codeIndex) => {
95
+ if (codeIndex % 2 === 1) {
96
+ return (
97
+ <code key={`code-${codeIndex}`} className="chat-inline-code">
98
+ {codePart}
99
+ </code>
100
+ )
101
+ }
102
+ const boldParts = codePart.split(/\*\*([\s\S]*?)\*\*/g)
103
+ return boldParts.map((boldPart, boldIndex) => {
104
+ if (boldIndex % 2 === 1) {
105
+ return (
106
+ <strong key={`bold-${boldIndex}`} className="chat-bold-highlight">
107
+ {boldPart}
108
+ </strong>
109
+ )
110
+ }
111
+ const mentionParts = boldPart.split(/(@[\w\-\.\/]+)/g)
112
+ return mentionParts.map((mentionPart, mentionIndex) => {
113
+ if (mentionIndex % 2 === 1) {
114
+ return (
115
+ <span key={`mention-${mentionIndex}`} className="chat-mention">
116
+ {mentionPart}
117
+ </span>
118
+ )
119
+ }
120
+ return mentionPart
121
+ })
122
+ })
123
+ })
124
+ }
125
+
126
+ const renderHtmlTable = (tableLines: string[], key: string) => {
127
+ const rows: string[][] = []
128
+ for (const line of tableLines) {
129
+ const trimmed = line.trim()
130
+ if (!trimmed) continue
131
+ const content = trimmed.slice(1, -1)
132
+ const contentEscaped = content.replace(/\\\|/g, '\u0000')
133
+ const cols = contentEscaped.split('|').map(s => s.replace(/\u0000/g, '|').trim())
134
+ rows.push(cols)
135
+ }
136
+
137
+ if (rows.length === 0) return null
138
+
139
+ let hasSeparator = false
140
+ let separatorIdx = -1
141
+ for (let i = 0; i < rows.length; i++) {
142
+ const row = rows[i]
143
+ if (row.length > 0 && row.every(col => /^[-\s:]+$/.test(col))) {
144
+ hasSeparator = true
145
+ separatorIdx = i
146
+ break
147
+ }
148
+ }
149
+
150
+ let headerRow: string[] | null = null
151
+ const dataRows: string[][] = []
152
+
153
+ if (hasSeparator) {
154
+ if (separatorIdx > 0) {
155
+ headerRow = rows[separatorIdx - 1]
156
+ for (let i = 0; i < rows.length; i++) {
157
+ if (i !== separatorIdx && i !== separatorIdx - 1) {
158
+ dataRows.push(rows[i])
159
+ }
160
+ }
161
+ } else {
162
+ for (let i = 0; i < rows.length; i++) {
163
+ if (i !== separatorIdx) {
164
+ dataRows.push(rows[i])
165
+ }
166
+ }
167
+ }
168
+ } else {
169
+ headerRow = rows[0]
170
+ for (let i = 1; i < rows.length; i++) {
171
+ dataRows.push(rows[i])
172
+ }
173
+ }
174
+
175
+ return (
176
+ <div key={key} className="chat-table-container" style={{
177
+ overflowX: 'auto',
178
+ margin: '14px 0',
179
+ width: '100%',
180
+ borderRadius: '8px',
181
+ border: '1px solid rgba(255, 255, 255, 0.12)',
182
+ background: 'rgba(30, 41, 59, 0.35)',
183
+ boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
184
+ }}>
185
+ <table className="chat-table" style={{
186
+ width: '100%',
187
+ borderCollapse: 'collapse',
188
+ fontSize: '0.86rem',
189
+ textAlign: 'left',
190
+ lineHeight: '1.5',
191
+ }}>
192
+ {headerRow && (
193
+ <thead>
194
+ <tr style={{
195
+ background: 'rgba(255, 255, 255, 0.04)',
196
+ borderBottom: '2px solid rgba(255, 255, 255, 0.15)',
197
+ }}>
198
+ {headerRow.map((col, idx) => (
199
+ <th key={`th-${idx}`} style={{
200
+ padding: '12px 16px',
201
+ fontWeight: 700,
202
+ color: 'var(--accent, #38bdf8)',
203
+ fontFamily: 'ui-sans-serif, system-ui, sans-serif',
204
+ }}>
205
+ {formatInline(col)}
206
+ </th>
207
+ ))}
208
+ </tr>
209
+ </thead>
210
+ )}
211
+ <tbody>
212
+ {dataRows.map((row, rIdx) => (
213
+ <tr key={`tr-${rIdx}`} style={{
214
+ background: rIdx % 2 === 1 ? 'rgba(255, 255, 255, 0.015)' : 'transparent',
215
+ borderBottom: rIdx < dataRows.length - 1 ? '1px solid rgba(255, 255, 255, 0.08)' : 'none',
216
+ }}>
217
+ {row.map((col, cIdx) => (
218
+ <td key={`td-${cIdx}`} style={{
219
+ padding: '12px 16px',
220
+ color: 'var(--text-main, #e2e8f0)',
221
+ }}>
222
+ {formatInline(col)}
223
+ </td>
224
+ ))}
225
+ </tr>
226
+ ))}
227
+ </tbody>
228
+ </table>
229
+ </div>
230
+ )
231
+ }
232
+
233
+ const lines = displayText.split('\n')
234
+ const items: ReactNode[] = []
235
+
236
+ let inCodeBlock = false
237
+ let codeBlockLang = ''
238
+ let codeBlockLines: string[] = []
239
+
240
+ let inTable = false
241
+ let tableLines: string[] = []
242
+
243
+ const flushTable = (index: number) => {
244
+ if (tableLines.length > 0) {
245
+ items.push(renderHtmlTable(tableLines, `table-${index}`))
246
+ tableLines = []
247
+ inTable = false
248
+ }
249
+ }
250
+
251
+ for (let i = 0; i < lines.length; i++) {
252
+ const line = lines[i]
253
+
254
+ if (line.trim().startsWith('```')) {
255
+ if (inTable) {
256
+ flushTable(i)
257
+ }
258
+ if (inCodeBlock) {
259
+ const codeText = codeBlockLines.join('\n')
260
+ items.push(
261
+ <ChatCodeBlock
262
+ key={`code-block-${i}`}
263
+ code={codeText}
264
+ language={codeBlockLang}
265
+ />
266
+ )
267
+ inCodeBlock = false
268
+ codeBlockLines = []
269
+ } else {
270
+ inCodeBlock = true
271
+ codeBlockLang = line.trim().slice(3).trim() || 'plaintext'
272
+ }
273
+ continue
274
+ }
275
+
276
+ if (inCodeBlock) {
277
+ codeBlockLines.push(line)
278
+ continue
279
+ }
280
+
281
+ if (isTableLine(line)) {
282
+ inTable = true
283
+ tableLines.push(line)
284
+ } else {
285
+ if (inTable) {
286
+ flushTable(i)
287
+ }
288
+
289
+ const headerMatch = line.match(/^(#{1,6})\s+(.*)$/)
290
+ if (headerMatch) {
291
+ const level = headerMatch[1].length
292
+ const content = headerMatch[2]
293
+
294
+ const style = {
295
+ fontWeight: 'bold',
296
+ display: 'block',
297
+ marginTop: level === 1 ? '16px' : level === 2 ? '14px' : '10px',
298
+ marginBottom: '6px',
299
+ fontSize: level === 1 ? '1.25em' : level === 2 ? '1.15em' : '1.05em',
300
+ color: 'var(--text-main)',
301
+ }
302
+
303
+ items.push(
304
+ <span key={`line-${i}`} style={style}>
305
+ {formatInline(content)}
306
+ </span>
307
+ )
308
+ } else {
309
+ const listMatch = line.match(/^(\s*)([-*+])\s+(.*)$/)
310
+ if (listMatch) {
311
+ const indent = listMatch[1]
312
+ const content = listMatch[3]
313
+ items.push(
314
+ <Fragment key={`line-${i}`}>
315
+ {indent}• {formatInline(content)}
316
+ {i < lines.length - 1 && '\n'}
317
+ </Fragment>
318
+ )
319
+ } else {
320
+ items.push(
321
+ <Fragment key={`line-${i}`}>
322
+ {formatInline(line)}
323
+ {i < lines.length - 1 && '\n'}
324
+ </Fragment>
325
+ )
326
+ }
327
+ }
328
+ }
329
+ }
330
+
331
+ if (inTable) {
332
+ flushTable(lines.length)
333
+ }
334
+
335
+ if (inCodeBlock && codeBlockLines.length > 0) {
336
+ const codeText = codeBlockLines.join('\n')
337
+ items.push(
338
+ <ChatCodeBlock
339
+ key={`code-block-end`}
340
+ code={codeText}
341
+ language={codeBlockLang}
342
+ />
343
+ )
344
+ }
345
+
346
+ return <>{items}</>
347
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * shared/utils/providers.ts
3
+ * Provider label helpers — shared by both Desktop and Web ChatPanel.
4
+ */
5
+
6
+ /** Minimal shape needed — matches the full ChatResponse in both tauri.ts files. */
7
+ interface ProviderResponse {
8
+ provider: string
9
+ fallbackProvider?: string | null
10
+ }
11
+
12
+ export function badge(provider: string, model: string): string {
13
+ return [provider, model].filter(Boolean).join(' / ')
14
+ }
15
+
16
+ export function providerLabel(provider: string): string {
17
+ switch (provider) {
18
+ case 'gemini': return 'Gemini'
19
+ case 'openai': return 'OpenAI'
20
+ case 'openrouter': return 'OpenRouter'
21
+ case 'deepseek': return 'DeepSeek'
22
+ case 'anthropic': return 'Claude'
23
+ case 'huggingface': return 'Hugging Face'
24
+ case 'local_openai': return 'Local OpenAI'
25
+ case 'ollama': return 'Ollama'
26
+ default: return provider || 'Primary provider'
27
+ }
28
+ }
29
+
30
+ export function fallbackNotice(
31
+ response: ProviderResponse | null | undefined,
32
+ ): string {
33
+ if (!response?.fallbackProvider) return ''
34
+ return `${providerLabel(response.fallbackProvider)} unavailable, fell back to ${providerLabel(response.provider)}.`
35
+ }
36
+
@@ -0,0 +1,216 @@
1
+ import { useState, useRef, useEffect } from 'react'
2
+
3
+ export interface SpeechToTextOptions {
4
+ language?: string
5
+ message: string
6
+ sending: boolean
7
+ isSpeaking: boolean
8
+ onSendVoiceMessage: (text: string) => Promise<any>
9
+ onSetMessage: (text: string) => void
10
+ }
11
+
12
+ export function useSpeechToText({
13
+ language,
14
+ message,
15
+ sending,
16
+ isSpeaking,
17
+ onSendVoiceMessage,
18
+ onSetMessage
19
+ }: SpeechToTextOptions) {
20
+ const [isRecording, setIsRecording] = useState(false)
21
+ const [voiceMode, setVoiceMode] = useState(false)
22
+ const [voiceTranscript, setVoiceTranscript] = useState('')
23
+ const [voiceAwaitingResponse, setVoiceAwaitingResponse] = useState(false)
24
+
25
+ const recognitionRef = useRef<any>(null)
26
+ const silenceTimerRef = useRef<number | null>(null)
27
+ const restartTimerRef = useRef<number | null>(null)
28
+
29
+ const voiceModeRef = useRef(false)
30
+ const sendingRef = useRef(false)
31
+ const voiceAwaitingResponseRef = useRef(false)
32
+ const isSpeakingRef = useRef(false)
33
+ const messageRef = useRef(message)
34
+
35
+ // Sync state to refs for event handlers
36
+ useEffect(() => {
37
+ voiceModeRef.current = voiceMode
38
+ if (!voiceMode) {
39
+ clearRestartTimer()
40
+ setVoiceTranscript('')
41
+ }
42
+ }, [voiceMode])
43
+
44
+ useEffect(() => {
45
+ sendingRef.current = sending
46
+ }, [sending])
47
+
48
+ useEffect(() => {
49
+ voiceAwaitingResponseRef.current = voiceAwaitingResponse
50
+ }, [voiceAwaitingResponse])
51
+
52
+ useEffect(() => {
53
+ isSpeakingRef.current = isSpeaking
54
+ }, [isSpeaking])
55
+
56
+ useEffect(() => {
57
+ messageRef.current = message
58
+ }, [message])
59
+
60
+ const clearRestartTimer = () => {
61
+ if (restartTimerRef.current !== null) {
62
+ window.clearTimeout(restartTimerRef.current)
63
+ restartTimerRef.current = null
64
+ }
65
+ }
66
+
67
+ const clearSilenceTimer = () => {
68
+ if (silenceTimerRef.current !== null) {
69
+ window.clearTimeout(silenceTimerRef.current)
70
+ silenceTimerRef.current = null
71
+ }
72
+ }
73
+
74
+ const stopRecognition = () => {
75
+ clearRestartTimer()
76
+ clearSilenceTimer()
77
+ recognitionRef.current?.stop()
78
+ recognitionRef.current = null
79
+ setIsRecording(false)
80
+ }
81
+
82
+ const scheduleVoiceListen = (delayMs = 350) => {
83
+ clearRestartTimer()
84
+ if (!voiceModeRef.current || sendingRef.current || voiceAwaitingResponseRef.current || isSpeakingRef.current) return
85
+ restartTimerRef.current = window.setTimeout(() => {
86
+ restartTimerRef.current = null
87
+ startRecognition(true)
88
+ }, delayMs)
89
+ }
90
+
91
+ const startRecognition = (autoSend: boolean) => {
92
+ if (recognitionRef.current || sendingRef.current || voiceAwaitingResponseRef.current || isSpeakingRef.current) return
93
+
94
+ const SpeechRecognitionApi = (window as any).SpeechRecognition || (window as any).webkitSpeechRecognition
95
+ if (!SpeechRecognitionApi) {
96
+ alert('Speech-to-text (Speech Recognition) is not supported in this browser/WebView. Please use Google Chrome or Microsoft Edge to enable voice conversation.')
97
+ voiceModeRef.current = false
98
+ setVoiceMode(false)
99
+ return
100
+ }
101
+
102
+ let accumulatedTranscript = ''
103
+ clearSilenceTimer()
104
+
105
+ try {
106
+ const recognition = new SpeechRecognitionApi()
107
+ recognition.continuous = true
108
+ recognition.interimResults = true
109
+ recognition.lang = language === 'en' ? 'en-US' : 'th-TH'
110
+
111
+ const resetSilenceTimeout = () => {
112
+ clearSilenceTimer()
113
+ if (autoSend) {
114
+ silenceTimerRef.current = window.setTimeout(() => {
115
+ recognition.stop()
116
+ }, 2000)
117
+ }
118
+ }
119
+
120
+ recognition.onstart = () => {
121
+ setIsRecording(true)
122
+ }
123
+ recognition.onresult = (event: any) => {
124
+ let interimText = ''
125
+ let finalText = ''
126
+ for (let index = event.resultIndex; index < event.results.length; index += 1) {
127
+ const transcript = event.results[index]?.[0]?.transcript ?? ''
128
+ if (event.results[index]?.isFinal) {
129
+ finalText += transcript
130
+ } else {
131
+ interimText += transcript
132
+ }
133
+ }
134
+ const displayText = (finalText || interimText).trim()
135
+ if (displayText) setVoiceTranscript(displayText)
136
+ if (finalText.trim()) {
137
+ accumulatedTranscript = finalText.trim()
138
+ }
139
+ if (displayText) {
140
+ resetSilenceTimeout()
141
+ }
142
+ }
143
+ recognition.onerror = (event: any) => {
144
+ console.error('Speech recognition error', event)
145
+ setIsRecording(false)
146
+ clearSilenceTimer()
147
+ }
148
+ recognition.onend = () => {
149
+ recognitionRef.current = null
150
+ setIsRecording(false)
151
+ clearSilenceTimer()
152
+ const finalText = accumulatedTranscript.trim()
153
+ if (finalText) {
154
+ if (autoSend) {
155
+ voiceAwaitingResponseRef.current = true
156
+ setVoiceAwaitingResponse(true)
157
+ onSendVoiceMessage(finalText)
158
+ .catch((error: any) => console.error('Voice message failed', error))
159
+ .finally(() => {
160
+ voiceAwaitingResponseRef.current = false
161
+ setVoiceAwaitingResponse(false)
162
+ scheduleVoiceListen()
163
+ })
164
+ } else {
165
+ onSetMessage(messageRef.current.trim() ? `${messageRef.current.trimEnd()} ${finalText}` : finalText)
166
+ }
167
+ } else {
168
+ if (autoSend && voiceModeRef.current) scheduleVoiceListen()
169
+ }
170
+ }
171
+ recognitionRef.current = recognition
172
+ recognition.start()
173
+ } catch (error) {
174
+ console.error('Failed to start speech recognition', error)
175
+ recognitionRef.current = null
176
+ setIsRecording(false)
177
+ clearSilenceTimer()
178
+ }
179
+ }
180
+
181
+ // Auto-listen trigger
182
+ useEffect(() => {
183
+ if (!voiceMode || sending || voiceAwaitingResponse || isSpeaking || isRecording) return
184
+ scheduleVoiceListen()
185
+ }, [voiceMode, sending, voiceAwaitingResponse, isSpeaking, isRecording])
186
+
187
+ // Cleanup on unmount
188
+ useEffect(() => {
189
+ return () => {
190
+ clearRestartTimer()
191
+ clearSilenceTimer()
192
+ if (recognitionRef.current) {
193
+ try {
194
+ recognitionRef.current.stop()
195
+ } catch (e) {
196
+ // ignore
197
+ }
198
+ }
199
+ }
200
+ }, [])
201
+
202
+ return {
203
+ isRecording,
204
+ voiceMode,
205
+ setVoiceMode,
206
+ voiceTranscript,
207
+ setVoiceTranscript,
208
+ voiceAwaitingResponse,
209
+ voiceAwaitingResponseRef,
210
+ voiceModeRef,
211
+ startRecognition,
212
+ stopRecognition,
213
+ scheduleVoiceListen,
214
+ clearRestartTimer
215
+ }
216
+ }