@pheem49/mint 1.6.3 → 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.
- package/Cargo.lock +3 -3
- package/Cargo.toml +1 -1
- package/README.md +82 -21
- package/assets/Mint_CLI.png +0 -0
- package/assets/Mint_Desktop.png +0 -0
- package/assets/Mint_Web.png +0 -0
- package/assets/Mint_WorkSpace.png +0 -0
- package/assets/Setting.png +0 -0
- package/assets/bridges.svg +4 -0
- package/assets/chat.svg +3 -0
- package/assets/code.svg +4 -0
- package/assets/features.svg +3 -0
- package/assets/imagegen.svg +5 -0
- package/assets/live2d.svg +6 -0
- package/assets/memory.svg +5 -0
- package/assets/screencapture.svg +4 -0
- package/assets/setup.svg +3 -0
- package/assets/tools.svg +4 -0
- package/bin/mint +0 -0
- package/crates/mint-cli/src/agent.rs +47 -15
- package/crates/mint-cli/src/main.rs +534 -7
- package/crates/mint-cli/src/onboard.rs +134 -1
- package/crates/mint-cli/src/setup.rs +173 -32
- package/crates/mint-core/src/api_server.rs +147 -3
- package/crates/mint-core/src/config.rs +22 -3
- package/crates/mint-core/src/image_gen.rs +895 -0
- package/crates/mint-core/src/lib.rs +4 -0
- 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 +32 -0
- package/crates/mint-core/tests/memory_persistence.rs +28 -0
- package/package.json +1 -1
- package/src/bin/index.js +0 -0
- package/src/renderer/src/agentProgress.ts +39 -0
- package/src/renderer/src/components/ChatPanel.tsx +547 -47
- package/src/renderer/src/components/DashboardSidebar.tsx +12 -4
- package/src/renderer/src/components/ImageStudioPanel.tsx +638 -0
- package/src/renderer/src/components/MintDashboard.tsx +84 -31
- package/src/renderer/src/components/ModelPanel.tsx +2 -2
- package/src/renderer/src/components/PicturesLibrary.tsx +5 -1
- package/src/renderer/src/components/SettingsWindow.tsx +29 -20
- package/src/renderer/src/components/SpotlightWindow.tsx +8 -8
- package/src/renderer/src/components/WorkspacePanel.tsx +1 -1
- package/src/renderer/src/css/image-studio.css +722 -0
- package/src/renderer/src/css/settings.css +13 -3
- package/src/renderer/src/css/styles.css +245 -43
- package/src/renderer/src/index.css +152 -0
- package/src/renderer/src/tauri.ts +148 -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 +568 -46
- package/src/renderer/src-web/components/DashboardSidebar.tsx +10 -2
- package/src/renderer/src-web/components/ImageStudioPanel.tsx +638 -0
- package/src/renderer/src-web/components/MintDashboard.tsx +65 -19
- package/src/renderer/src-web/components/PicturesLibrary.tsx +5 -1
- package/src/renderer/src-web/components/SettingsWindow.tsx +29 -20
- package/src/renderer/src-web/css/image-studio.css +722 -0
- package/src/renderer/src-web/css/settings.css +14 -4
- package/src/renderer/src-web/css/styles.css +292 -44
- package/src/renderer/src-web/index.css +152 -0
- package/src/renderer/src-web/tauri.ts +149 -0
- package/src-tauri/src/lib.rs +65 -5
- package/src-tauri/tauri.conf.json +1 -1
|
@@ -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,
|
|
@@ -260,6 +261,64 @@ function renderApprovalDetails(approval: any): ApprovalDetails {
|
|
|
260
261
|
return { title: 'Unknown Action', body: JSON.stringify(approval, null, 2), reason: 'Requires approval to proceed.', isDangerous: false }
|
|
261
262
|
}
|
|
262
263
|
|
|
264
|
+
function renderDiff(diffText: string) {
|
|
265
|
+
if (!diffText) return null
|
|
266
|
+
const lines = diffText.split('\n')
|
|
267
|
+
return (
|
|
268
|
+
<div style={{
|
|
269
|
+
background: '#0b0f19',
|
|
270
|
+
borderRadius: '6px',
|
|
271
|
+
padding: '8px',
|
|
272
|
+
border: '1px solid rgba(255, 255, 255, 0.08)',
|
|
273
|
+
overflowX: 'auto',
|
|
274
|
+
maxHeight: '400px',
|
|
275
|
+
fontFamily: 'monospace',
|
|
276
|
+
fontSize: '0.74rem',
|
|
277
|
+
lineHeight: '1.4',
|
|
278
|
+
}}>
|
|
279
|
+
{lines.map((line, idx) => {
|
|
280
|
+
let style: any = {
|
|
281
|
+
whiteSpace: 'pre-wrap',
|
|
282
|
+
padding: '2px 6px',
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (line.startsWith('@@') || line.startsWith('---') || line.startsWith('+++')) {
|
|
286
|
+
style = {
|
|
287
|
+
...style,
|
|
288
|
+
color: '#64748b',
|
|
289
|
+
fontWeight: 'bold',
|
|
290
|
+
}
|
|
291
|
+
} else if (line.startsWith('+')) {
|
|
292
|
+
style = {
|
|
293
|
+
...style,
|
|
294
|
+
background: 'rgba(16, 185, 129, 0.12)',
|
|
295
|
+
borderLeft: '3px solid #10b981',
|
|
296
|
+
color: '#a7f3d0'
|
|
297
|
+
}
|
|
298
|
+
} else if (line.startsWith('-')) {
|
|
299
|
+
style = {
|
|
300
|
+
...style,
|
|
301
|
+
background: 'rgba(239, 68, 68, 0.12)',
|
|
302
|
+
borderLeft: '3px solid #ef4444',
|
|
303
|
+
color: '#fca5a5'
|
|
304
|
+
}
|
|
305
|
+
} else {
|
|
306
|
+
style = {
|
|
307
|
+
...style,
|
|
308
|
+
color: '#e2e8f0',
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return (
|
|
313
|
+
<div key={idx} style={style}>
|
|
314
|
+
{line}
|
|
315
|
+
</div>
|
|
316
|
+
)
|
|
317
|
+
})}
|
|
318
|
+
</div>
|
|
319
|
+
)
|
|
320
|
+
}
|
|
321
|
+
|
|
263
322
|
interface ChatPanelProps {
|
|
264
323
|
interactions: any[]
|
|
265
324
|
sending: boolean
|
|
@@ -269,6 +328,8 @@ interface ChatPanelProps {
|
|
|
269
328
|
streamedResponse: ChatResponse | null
|
|
270
329
|
agentProgress: AgentProgress[]
|
|
271
330
|
agentActivitySnapshots: Record<string, AgentProgress[]>
|
|
331
|
+
thinkingExpanded: Record<string, boolean>
|
|
332
|
+
onThinkingExpandedChange: (key: string, open: boolean) => void
|
|
272
333
|
message: string
|
|
273
334
|
imageAttachments: Array<{ dataUri: string; name: string; previewDataUri?: string }>
|
|
274
335
|
documentName: string
|
|
@@ -298,55 +359,414 @@ interface ChatPanelProps {
|
|
|
298
359
|
onSetModel: (model: string) => void
|
|
299
360
|
}
|
|
300
361
|
|
|
362
|
+
function ChatCodeBlock({ code, language }: { code: string; language: string; key?: any }) {
|
|
363
|
+
const [copied, setCopied] = useState(false)
|
|
364
|
+
|
|
365
|
+
const handleCopy = async () => {
|
|
366
|
+
try {
|
|
367
|
+
await navigator.clipboard.writeText(code)
|
|
368
|
+
setCopied(true)
|
|
369
|
+
setTimeout(() => setCopied(false), 2000)
|
|
370
|
+
} catch (err) {
|
|
371
|
+
console.error('Failed to copy code: ', err)
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const handleDownload = () => {
|
|
376
|
+
try {
|
|
377
|
+
const blob = new Blob([code], { type: 'text/plain;charset=utf-8' })
|
|
378
|
+
const url = URL.createObjectURL(blob)
|
|
379
|
+
const link = document.createElement('a')
|
|
380
|
+
link.href = url
|
|
381
|
+
link.download = `code.${language === 'plaintext' ? 'txt' : language}`
|
|
382
|
+
link.click()
|
|
383
|
+
URL.revokeObjectURL(url)
|
|
384
|
+
} catch (err) {
|
|
385
|
+
console.error('Failed to download code: ', err)
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const displayLang = language.charAt(0).toUpperCase() + language.slice(1).toLowerCase()
|
|
390
|
+
|
|
391
|
+
return (
|
|
392
|
+
<div className="chat-code-block-container" style={{ whiteSpace: 'normal' }}>
|
|
393
|
+
<div className="chat-code-block-header">
|
|
394
|
+
<span className="chat-code-block-lang">{displayLang}</span>
|
|
395
|
+
<div className="chat-code-block-actions">
|
|
396
|
+
<button type="button" onClick={handleDownload} title="Download code" className="chat-code-action-btn">
|
|
397
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
398
|
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
|
399
|
+
<polyline points="7 10 12 15 17 10" />
|
|
400
|
+
<line x1="12" y1="15" x2="12" y2="3" />
|
|
401
|
+
</svg>
|
|
402
|
+
</button>
|
|
403
|
+
<button type="button" onClick={handleCopy} title="Copy code" className="chat-code-action-btn">
|
|
404
|
+
{copied ? (
|
|
405
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
|
|
406
|
+
<polyline points="20 6 9 17 4 12" />
|
|
407
|
+
</svg>
|
|
408
|
+
) : (
|
|
409
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
410
|
+
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
|
|
411
|
+
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
|
|
412
|
+
</svg>
|
|
413
|
+
)}
|
|
414
|
+
</button>
|
|
415
|
+
</div>
|
|
416
|
+
</div>
|
|
417
|
+
<pre className="chat-code-block-body">
|
|
418
|
+
<code>{code}</code>
|
|
419
|
+
</pre>
|
|
420
|
+
</div>
|
|
421
|
+
)
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const isTableLine = (line: string): boolean => {
|
|
425
|
+
const trimmed = line.trim()
|
|
426
|
+
return trimmed.startsWith('|') && trimmed.endsWith('|') && trimmed.length > 1
|
|
427
|
+
}
|
|
428
|
+
|
|
301
429
|
function renderFormattedMessage(text: string) {
|
|
302
430
|
const displayText = readableAssistantText(text)
|
|
303
431
|
if (!displayText) return null
|
|
304
|
-
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
432
|
+
|
|
433
|
+
const formatInline = (str: string) => {
|
|
434
|
+
const codeParts = str.split(/`([\s\S]*?)`/g)
|
|
435
|
+
return codeParts.map((codePart, codeIndex) => {
|
|
436
|
+
if (codeIndex % 2 === 1) {
|
|
437
|
+
return (
|
|
438
|
+
<code key={`code-${codeIndex}`} className="chat-inline-code">
|
|
439
|
+
{codePart}
|
|
440
|
+
</code>
|
|
441
|
+
)
|
|
442
|
+
}
|
|
443
|
+
const boldParts = codePart.split(/\*\*([\s\S]*?)\*\*/g)
|
|
444
|
+
return boldParts.map((boldPart, boldIndex) => {
|
|
445
|
+
if (boldIndex % 2 === 1) {
|
|
313
446
|
return (
|
|
314
|
-
<strong key={
|
|
315
|
-
{
|
|
447
|
+
<strong key={`bold-${boldIndex}`} className="chat-bold-highlight">
|
|
448
|
+
{boldPart}
|
|
316
449
|
</strong>
|
|
317
450
|
)
|
|
318
451
|
}
|
|
319
|
-
return
|
|
452
|
+
return boldPart
|
|
320
453
|
})
|
|
454
|
+
})
|
|
455
|
+
}
|
|
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)
|
|
321
466
|
}
|
|
322
467
|
|
|
323
|
-
if (
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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])
|
|
334
503
|
}
|
|
335
|
-
|
|
336
|
-
return (
|
|
337
|
-
<span key={lineIndex} style={style}>
|
|
338
|
-
{formatInline(content)}
|
|
339
|
-
</span>
|
|
340
|
-
)
|
|
341
504
|
}
|
|
342
505
|
|
|
343
506
|
return (
|
|
344
|
-
<
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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>
|
|
348
561
|
)
|
|
349
|
-
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const lines = displayText.split('\n')
|
|
565
|
+
const items: any[] = []
|
|
566
|
+
|
|
567
|
+
let inCodeBlock = false
|
|
568
|
+
let codeBlockLang = ''
|
|
569
|
+
let codeBlockLines: string[] = []
|
|
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
|
+
|
|
582
|
+
for (let i = 0; i < lines.length; i++) {
|
|
583
|
+
const line = lines[i]
|
|
584
|
+
|
|
585
|
+
if (line.trim().startsWith('```')) {
|
|
586
|
+
if (inTable) {
|
|
587
|
+
flushTable(i)
|
|
588
|
+
}
|
|
589
|
+
if (inCodeBlock) {
|
|
590
|
+
const codeText = codeBlockLines.join('\n')
|
|
591
|
+
items.push(
|
|
592
|
+
<ChatCodeBlock
|
|
593
|
+
key={`code-block-${i}`}
|
|
594
|
+
code={codeText}
|
|
595
|
+
language={codeBlockLang}
|
|
596
|
+
/>
|
|
597
|
+
)
|
|
598
|
+
inCodeBlock = false
|
|
599
|
+
codeBlockLines = []
|
|
600
|
+
} else {
|
|
601
|
+
inCodeBlock = true
|
|
602
|
+
codeBlockLang = line.trim().slice(3).trim() || 'plaintext'
|
|
603
|
+
}
|
|
604
|
+
continue
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
if (inCodeBlock) {
|
|
608
|
+
codeBlockLines.push(line)
|
|
609
|
+
continue
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
if (isTableLine(line)) {
|
|
613
|
+
inTable = true
|
|
614
|
+
tableLines.push(line)
|
|
615
|
+
} else {
|
|
616
|
+
if (inTable) {
|
|
617
|
+
flushTable(i)
|
|
618
|
+
}
|
|
619
|
+
|
|
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
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
if (inTable) {
|
|
663
|
+
flushTable(lines.length)
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
if (inCodeBlock && codeBlockLines.length > 0) {
|
|
667
|
+
const codeText = codeBlockLines.join('\n')
|
|
668
|
+
items.push(
|
|
669
|
+
<ChatCodeBlock
|
|
670
|
+
key={`code-block-end`}
|
|
671
|
+
code={codeText}
|
|
672
|
+
language={codeBlockLang}
|
|
673
|
+
/>
|
|
674
|
+
)
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
return items
|
|
678
|
+
}
|
|
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
|
+
)
|
|
350
770
|
}
|
|
351
771
|
|
|
352
772
|
function readableAssistantText(text: string) {
|
|
@@ -518,6 +938,8 @@ export default function ChatPanel({
|
|
|
518
938
|
streamedResponse,
|
|
519
939
|
agentProgress,
|
|
520
940
|
agentActivitySnapshots,
|
|
941
|
+
thinkingExpanded,
|
|
942
|
+
onThinkingExpandedChange,
|
|
521
943
|
message,
|
|
522
944
|
imageAttachments,
|
|
523
945
|
documentName,
|
|
@@ -607,6 +1029,25 @@ export default function ChatPanel({
|
|
|
607
1029
|
}
|
|
608
1030
|
const activeModel = getActiveModel(activeProvider)
|
|
609
1031
|
|
|
1032
|
+
const [elapsedSeconds, setElapsedSeconds] = useState(0)
|
|
1033
|
+
|
|
1034
|
+
useEffect(() => {
|
|
1035
|
+
if (!sending) {
|
|
1036
|
+
setElapsedSeconds(0)
|
|
1037
|
+
return
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
const startTime = Date.now()
|
|
1041
|
+
setElapsedSeconds(0)
|
|
1042
|
+
|
|
1043
|
+
const timer = setInterval(() => {
|
|
1044
|
+
const elapsed = Math.round((Date.now() - startTime) / 1000)
|
|
1045
|
+
setElapsedSeconds(elapsed)
|
|
1046
|
+
}, 1000)
|
|
1047
|
+
|
|
1048
|
+
return () => clearInterval(timer)
|
|
1049
|
+
}, [sending])
|
|
1050
|
+
|
|
610
1051
|
const [isRecording, setIsRecording] = useState(false)
|
|
611
1052
|
const [voiceMode, setVoiceMode] = useState(false)
|
|
612
1053
|
const [voiceTranscript, setVoiceTranscript] = useState('')
|
|
@@ -628,7 +1069,7 @@ export default function ChatPanel({
|
|
|
628
1069
|
const speakingRef = useRef<string | null>(null)
|
|
629
1070
|
const restartTimerRef = useRef<number | null>(null)
|
|
630
1071
|
const voiceStatus = speakingText ? 'speaking' : (sending || voiceAwaitingResponse) ? 'thinking' : isRecording ? 'listening' : voiceMode ? 'ready' : 'off'
|
|
631
|
-
const voiceStatusLabel = voiceStatus === 'speaking' ? '
|
|
1072
|
+
const voiceStatusLabel = voiceStatus === 'speaking' ? 'Speaking' : voiceStatus === 'thinking' ? 'Thinking' : voiceStatus === 'listening' ? 'Listening' : 'Ready'
|
|
632
1073
|
|
|
633
1074
|
const clearRestartTimer = () => {
|
|
634
1075
|
if (restartTimerRef.current === null) return
|
|
@@ -750,7 +1191,7 @@ export default function ChatPanel({
|
|
|
750
1191
|
}
|
|
751
1192
|
const startAudioRecording = async (autoSend: boolean) => {
|
|
752
1193
|
if (!navigator.mediaDevices?.getUserMedia || typeof MediaRecorder === 'undefined') {
|
|
753
|
-
alert(
|
|
1194
|
+
alert("Sorry, this system cannot access the microphone or record audio in this browser")
|
|
754
1195
|
voiceModeRef.current = false
|
|
755
1196
|
setVoiceMode(false)
|
|
756
1197
|
return
|
|
@@ -773,7 +1214,7 @@ export default function ChatPanel({
|
|
|
773
1214
|
mediaStreamRef.current = stream
|
|
774
1215
|
mediaRecorderRef.current = recorder
|
|
775
1216
|
audioContextRef.current = audioContext
|
|
776
|
-
setVoiceTranscript('
|
|
1217
|
+
setVoiceTranscript('Listening to microphone...')
|
|
777
1218
|
|
|
778
1219
|
recorder.ondataavailable = (event) => {
|
|
779
1220
|
if (event.data.size > 0) chunks.push(event.data)
|
|
@@ -788,13 +1229,13 @@ export default function ChatPanel({
|
|
|
788
1229
|
setIsRecording(false)
|
|
789
1230
|
if (!autoSend || !voiceModeRef.current || chunks.length === 0) return
|
|
790
1231
|
if (!heardVoice) {
|
|
791
|
-
setVoiceTranscript('
|
|
1232
|
+
setVoiceTranscript('No speech detected')
|
|
792
1233
|
scheduleVoiceListen(700)
|
|
793
1234
|
return
|
|
794
1235
|
}
|
|
795
1236
|
const blob = new Blob(chunks, { type: recorder.mimeType || 'audio/webm' })
|
|
796
1237
|
const audioDataUri = await blobToDataUri(blob)
|
|
797
|
-
setVoiceTranscript('
|
|
1238
|
+
setVoiceTranscript('Sent audio to AI')
|
|
798
1239
|
voiceAwaitingResponseRef.current = true
|
|
799
1240
|
setVoiceAwaitingResponse(true)
|
|
800
1241
|
onSendVoiceMessage('', audioDataUri)
|
|
@@ -818,7 +1259,7 @@ export default function ChatPanel({
|
|
|
818
1259
|
if (peak > 12) {
|
|
819
1260
|
heardVoice = true
|
|
820
1261
|
quietSince = 0
|
|
821
|
-
setVoiceTranscript('
|
|
1262
|
+
setVoiceTranscript('Listening...')
|
|
822
1263
|
} else if (heardVoice) {
|
|
823
1264
|
quietSince = quietSince || now
|
|
824
1265
|
}
|
|
@@ -833,7 +1274,7 @@ export default function ChatPanel({
|
|
|
833
1274
|
setIsRecording(false)
|
|
834
1275
|
voiceModeRef.current = false
|
|
835
1276
|
setVoiceMode(false)
|
|
836
|
-
alert('
|
|
1277
|
+
alert('Failed to open microphone. Please check microphone permissions in your browser')
|
|
837
1278
|
}
|
|
838
1279
|
}
|
|
839
1280
|
const startRecognition = (autoSend: boolean) => {
|
|
@@ -1351,8 +1792,8 @@ export default function ChatPanel({
|
|
|
1351
1792
|
}}
|
|
1352
1793
|
>
|
|
1353
1794
|
<div style={{ fontSize: '3.5rem', marginBottom: '16px' }}>🖼️</div>
|
|
1354
|
-
<div style={{ fontSize: '1.25rem', fontWeight: 'bold', letterSpacing: '0.5px' }}
|
|
1355
|
-
<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>
|
|
1356
1797
|
</div>
|
|
1357
1798
|
)}
|
|
1358
1799
|
|
|
@@ -1375,6 +1816,19 @@ export default function ChatPanel({
|
|
|
1375
1816
|
<div className="bubble-wrapper">
|
|
1376
1817
|
{renderCompletedActivity(interaction)}
|
|
1377
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
|
+
})()}
|
|
1378
1832
|
<div className="message-bubble" style={{ whiteSpace: 'pre-wrap' }}>{renderFormattedMessage(interaction.aiText)}</div>
|
|
1379
1833
|
<div className="message-time" style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
1380
1834
|
<button className="provider-badge">{interaction.provider} • {interaction.model}</button>
|
|
@@ -1396,7 +1850,7 @@ export default function ChatPanel({
|
|
|
1396
1850
|
opacity: 0.7,
|
|
1397
1851
|
transition: 'all 0.2s',
|
|
1398
1852
|
}}
|
|
1399
|
-
title={speakingText === interaction.aiText ? "
|
|
1853
|
+
title={speakingText === interaction.aiText ? "Stop reading" : "Read aloud"}
|
|
1400
1854
|
>
|
|
1401
1855
|
{renderSpeakerIcon(speakingText === interaction.aiText)}
|
|
1402
1856
|
</button>
|
|
@@ -1425,7 +1879,43 @@ export default function ChatPanel({
|
|
|
1425
1879
|
{renderActiveFileChanges()}
|
|
1426
1880
|
<div className="message ai-message thinking-message">
|
|
1427
1881
|
<div className="bubble-wrapper">
|
|
1428
|
-
<
|
|
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
|
+
/>
|
|
1895
|
+
<div className="message-bubble">
|
|
1896
|
+
<span>
|
|
1897
|
+
{streamedReply ? (
|
|
1898
|
+
renderFormattedMessage(streamedReply)
|
|
1899
|
+
) : (
|
|
1900
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', color: 'var(--text-soft, #94a3b8)' }}>
|
|
1901
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" style={{ display: 'inline-block', flexShrink: 0 }}>
|
|
1902
|
+
<circle cx="12" cy="12" r="10" stroke="rgba(255, 255, 255, 0.12)" />
|
|
1903
|
+
<path d="M12 2a10 10 0 0 1 10 10" stroke="var(--accent)" strokeLinecap="round">
|
|
1904
|
+
<animateTransform
|
|
1905
|
+
attributeName="transform"
|
|
1906
|
+
type="rotate"
|
|
1907
|
+
from="0 12 12"
|
|
1908
|
+
to="360 12 12"
|
|
1909
|
+
dur="0.9s"
|
|
1910
|
+
repeatCount="indefinite"
|
|
1911
|
+
/>
|
|
1912
|
+
</path>
|
|
1913
|
+
</svg>
|
|
1914
|
+
<span>Thinking for {elapsedSeconds}s</span>
|
|
1915
|
+
</div>
|
|
1916
|
+
)}
|
|
1917
|
+
</span>
|
|
1918
|
+
</div>
|
|
1429
1919
|
{streamedResponse && (
|
|
1430
1920
|
<div className="message-time" style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
1431
1921
|
<button className="provider-badge">{badge(streamedResponse.provider, streamedResponse.model)}</button>
|
|
@@ -1447,7 +1937,7 @@ export default function ChatPanel({
|
|
|
1447
1937
|
opacity: 0.7,
|
|
1448
1938
|
transition: 'all 0.2s',
|
|
1449
1939
|
}}
|
|
1450
|
-
title={speakingText === streamedReply ? "
|
|
1940
|
+
title={speakingText === streamedReply ? "Stop reading" : "Read aloud"}
|
|
1451
1941
|
>
|
|
1452
1942
|
{renderSpeakerIcon(speakingText === streamedReply)}
|
|
1453
1943
|
</button>
|
|
@@ -1459,6 +1949,38 @@ export default function ChatPanel({
|
|
|
1459
1949
|
</div>
|
|
1460
1950
|
)}
|
|
1461
1951
|
|
|
1952
|
+
{pendingApproval && (() => {
|
|
1953
|
+
const details = renderApprovalDetails(pendingApproval.approval)
|
|
1954
|
+
const writeFile = pendingApproval.approval?.WriteFile
|
|
1955
|
+
const applyPatch = pendingApproval.approval?.ApplyPatch
|
|
1956
|
+
const diffText = writeFile?.diff || applyPatch?.diff
|
|
1957
|
+
|
|
1958
|
+
return (
|
|
1959
|
+
<div className="message ai-message" style={{ width: '100%' }}>
|
|
1960
|
+
<div className="bubble-wrapper" style={{ width: '100%' }}>
|
|
1961
|
+
<div className="action-card approval-card" data-tier={details.isDangerous ? 'dangerous' : undefined} style={{ width: '100%' }}>
|
|
1962
|
+
<div className="approval-card-content" style={{ width: '100%' }}>
|
|
1963
|
+
<div className="approval-card-title">{details.title}</div>
|
|
1964
|
+
<div className="approval-card-body" style={{ whiteSpace: 'pre-wrap', fontFamily: 'monospace' }}>{details.body}</div>
|
|
1965
|
+
{diffText ? (
|
|
1966
|
+
<div className="approval-card-diff-container" style={{ marginTop: '8px', width: '100%' }}>
|
|
1967
|
+
<div style={{ fontSize: '0.8rem', fontWeight: 600, color: 'var(--text-soft, #94a3b8)', marginBottom: '4px' }}>Diff:</div>
|
|
1968
|
+
{renderDiff(diffText)}
|
|
1969
|
+
</div>
|
|
1970
|
+
) : (
|
|
1971
|
+
details.reason && <div className="approval-card-reason" style={{ whiteSpace: 'pre-wrap', fontFamily: 'monospace' }}>{details.reason}</div>
|
|
1972
|
+
)}
|
|
1973
|
+
</div>
|
|
1974
|
+
<div className="approval-card-actions">
|
|
1975
|
+
<button type="button" className="approval-btn approval-btn-approve" onClick={() => onApproval(true)}>Approve</button>
|
|
1976
|
+
<button type="button" className="approval-btn" style={{ backgroundColor: 'rgba(16, 185, 129, 0.22)', borderColor: 'rgba(16, 185, 129, 0.4)', color: '#a7f3d0' }} onClick={() => onApproval(true, true)}>Approve this session</button>
|
|
1977
|
+
<button type="button" className="approval-btn approval-btn-cancel" onClick={() => onApproval(false)}>Cancel</button>
|
|
1978
|
+
</div>
|
|
1979
|
+
</div>
|
|
1980
|
+
</div>
|
|
1981
|
+
</div>
|
|
1982
|
+
)
|
|
1983
|
+
})()}
|
|
1462
1984
|
<div ref={chatEnd} />
|
|
1463
1985
|
</div>
|
|
1464
1986
|
|