@pheem49/mint 1.6.3 → 1.6.4
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 +6 -2
- package/crates/mint-cli/src/main.rs +145 -5
- package/crates/mint-cli/src/onboard.rs +134 -1
- package/crates/mint-core/src/api_server.rs +108 -3
- package/crates/mint-core/src/config.rs +20 -1
- package/crates/mint-core/src/image_gen.rs +895 -0
- package/crates/mint-core/src/lib.rs +4 -0
- package/package.json +1 -1
- package/src/bin/index.js +0 -0
- package/src/renderer/src/components/ChatPanel.tsx +252 -26
- package/src/renderer/src/components/DashboardSidebar.tsx +9 -1
- package/src/renderer/src/components/ImageStudioPanel.tsx +638 -0
- package/src/renderer/src/components/MintDashboard.tsx +13 -4
- package/src/renderer/src/components/SettingsWindow.tsx +29 -20
- 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 +50 -26
- package/src/renderer/src/index.css +106 -0
- package/src/renderer/src/tauri.ts +123 -0
- package/src/renderer/src-web/components/ChatPanel.tsx +272 -24
- package/src/renderer/src-web/components/DashboardSidebar.tsx +9 -1
- package/src/renderer/src-web/components/ImageStudioPanel.tsx +638 -0
- package/src/renderer/src-web/components/MintDashboard.tsx +12 -3
- 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 +50 -26
- package/src/renderer/src-web/index.css +106 -0
- package/src/renderer/src-web/tauri.ts +124 -0
- package/src-tauri/src/lib.rs +48 -5
- package/src-tauri/tauri.conf.json +1 -1
|
@@ -5,6 +5,7 @@ pub mod chat;
|
|
|
5
5
|
pub mod code_tools;
|
|
6
6
|
pub mod config;
|
|
7
7
|
pub mod files;
|
|
8
|
+
pub mod image_gen;
|
|
8
9
|
pub mod knowledge;
|
|
9
10
|
pub mod mcp;
|
|
10
11
|
pub mod memory;
|
|
@@ -39,6 +40,9 @@ pub use config::{
|
|
|
39
40
|
set_config_value,
|
|
40
41
|
};
|
|
41
42
|
pub use files::{FileOperationError, PathKind, PathMatch, create_folder, find_paths};
|
|
43
|
+
pub use image_gen::{
|
|
44
|
+
GeneratedImage, ImageGenError, ImageGenRequest, ImageGenResponse, generate_images,
|
|
45
|
+
};
|
|
42
46
|
pub use knowledge::{
|
|
43
47
|
KnowledgeError, KnowledgeHit, KnowledgeSource, KnowledgeStore, extract_document_text,
|
|
44
48
|
};
|
package/package.json
CHANGED
package/src/bin/index.js
CHANGED
|
File without changes
|
|
@@ -260,6 +260,64 @@ function renderApprovalDetails(approval: any): ApprovalDetails {
|
|
|
260
260
|
return { title: 'Unknown Action', body: JSON.stringify(approval, null, 2), reason: 'Requires approval to proceed.', isDangerous: false }
|
|
261
261
|
}
|
|
262
262
|
|
|
263
|
+
function renderDiff(diffText: string) {
|
|
264
|
+
if (!diffText) return null
|
|
265
|
+
const lines = diffText.split('\n')
|
|
266
|
+
return (
|
|
267
|
+
<div style={{
|
|
268
|
+
background: '#0b0f19',
|
|
269
|
+
borderRadius: '6px',
|
|
270
|
+
padding: '8px',
|
|
271
|
+
border: '1px solid rgba(255, 255, 255, 0.08)',
|
|
272
|
+
overflowX: 'auto',
|
|
273
|
+
maxHeight: '400px',
|
|
274
|
+
fontFamily: 'monospace',
|
|
275
|
+
fontSize: '0.74rem',
|
|
276
|
+
lineHeight: '1.4',
|
|
277
|
+
}}>
|
|
278
|
+
{lines.map((line, idx) => {
|
|
279
|
+
let style: any = {
|
|
280
|
+
whiteSpace: 'pre-wrap',
|
|
281
|
+
padding: '2px 6px',
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (line.startsWith('@@') || line.startsWith('---') || line.startsWith('+++')) {
|
|
285
|
+
style = {
|
|
286
|
+
...style,
|
|
287
|
+
color: '#64748b',
|
|
288
|
+
fontWeight: 'bold',
|
|
289
|
+
}
|
|
290
|
+
} else if (line.startsWith('+')) {
|
|
291
|
+
style = {
|
|
292
|
+
...style,
|
|
293
|
+
background: 'rgba(16, 185, 129, 0.12)',
|
|
294
|
+
borderLeft: '3px solid #10b981',
|
|
295
|
+
color: '#a7f3d0'
|
|
296
|
+
}
|
|
297
|
+
} else if (line.startsWith('-')) {
|
|
298
|
+
style = {
|
|
299
|
+
...style,
|
|
300
|
+
background: 'rgba(239, 68, 68, 0.12)',
|
|
301
|
+
borderLeft: '3px solid #ef4444',
|
|
302
|
+
color: '#fca5a5'
|
|
303
|
+
}
|
|
304
|
+
} else {
|
|
305
|
+
style = {
|
|
306
|
+
...style,
|
|
307
|
+
color: '#e2e8f0',
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return (
|
|
312
|
+
<div key={idx} style={style}>
|
|
313
|
+
{line}
|
|
314
|
+
</div>
|
|
315
|
+
)
|
|
316
|
+
})}
|
|
317
|
+
</div>
|
|
318
|
+
)
|
|
319
|
+
}
|
|
320
|
+
|
|
263
321
|
interface ChatPanelProps {
|
|
264
322
|
interactions: any[]
|
|
265
323
|
sending: boolean
|
|
@@ -299,55 +357,171 @@ interface ChatPanelProps {
|
|
|
299
357
|
onSetModel: (model: string) => void
|
|
300
358
|
}
|
|
301
359
|
|
|
360
|
+
function ChatCodeBlock({ code, language }: { code: string; language: string; key?: any }) {
|
|
361
|
+
const [copied, setCopied] = useState(false)
|
|
362
|
+
|
|
363
|
+
const handleCopy = async () => {
|
|
364
|
+
try {
|
|
365
|
+
await navigator.clipboard.writeText(code)
|
|
366
|
+
setCopied(true)
|
|
367
|
+
setTimeout(() => setCopied(false), 2000)
|
|
368
|
+
} catch (err) {
|
|
369
|
+
console.error('Failed to copy code: ', err)
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const handleDownload = () => {
|
|
374
|
+
try {
|
|
375
|
+
const blob = new Blob([code], { type: 'text/plain;charset=utf-8' })
|
|
376
|
+
const url = URL.createObjectURL(blob)
|
|
377
|
+
const link = document.createElement('a')
|
|
378
|
+
link.href = url
|
|
379
|
+
link.download = `code.${language === 'plaintext' ? 'txt' : language}`
|
|
380
|
+
link.click()
|
|
381
|
+
URL.revokeObjectURL(url)
|
|
382
|
+
} catch (err) {
|
|
383
|
+
console.error('Failed to download code: ', err)
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const displayLang = language.charAt(0).toUpperCase() + language.slice(1).toLowerCase()
|
|
388
|
+
|
|
389
|
+
return (
|
|
390
|
+
<div className="chat-code-block-container" style={{ whiteSpace: 'normal' }}>
|
|
391
|
+
<div className="chat-code-block-header">
|
|
392
|
+
<span className="chat-code-block-lang">{displayLang}</span>
|
|
393
|
+
<div className="chat-code-block-actions">
|
|
394
|
+
<button type="button" onClick={handleDownload} title="Download code" className="chat-code-action-btn">
|
|
395
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
396
|
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
|
397
|
+
<polyline points="7 10 12 15 17 10" />
|
|
398
|
+
<line x1="12" y1="15" x2="12" y2="3" />
|
|
399
|
+
</svg>
|
|
400
|
+
</button>
|
|
401
|
+
<button type="button" onClick={handleCopy} title="Copy code" className="chat-code-action-btn">
|
|
402
|
+
{copied ? (
|
|
403
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
|
|
404
|
+
<polyline points="20 6 9 17 4 12" />
|
|
405
|
+
</svg>
|
|
406
|
+
) : (
|
|
407
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
408
|
+
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
|
|
409
|
+
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
|
|
410
|
+
</svg>
|
|
411
|
+
)}
|
|
412
|
+
</button>
|
|
413
|
+
</div>
|
|
414
|
+
</div>
|
|
415
|
+
<pre className="chat-code-block-body">
|
|
416
|
+
<code>{code}</code>
|
|
417
|
+
</pre>
|
|
418
|
+
</div>
|
|
419
|
+
)
|
|
420
|
+
}
|
|
421
|
+
|
|
302
422
|
function renderFormattedMessage(text: string) {
|
|
303
423
|
const displayText = readableAssistantText(text)
|
|
304
424
|
if (!displayText) return null
|
|
305
|
-
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
425
|
+
|
|
426
|
+
const formatInline = (str: string) => {
|
|
427
|
+
const codeParts = str.split(/`([\s\S]*?)`/g)
|
|
428
|
+
return codeParts.map((codePart, codeIndex) => {
|
|
429
|
+
if (codeIndex % 2 === 1) {
|
|
430
|
+
return (
|
|
431
|
+
<code key={`code-${codeIndex}`} className="chat-inline-code">
|
|
432
|
+
{codePart}
|
|
433
|
+
</code>
|
|
434
|
+
)
|
|
435
|
+
}
|
|
436
|
+
const boldParts = codePart.split(/\*\*([\s\S]*?)\*\*/g)
|
|
437
|
+
return boldParts.map((boldPart, boldIndex) => {
|
|
438
|
+
if (boldIndex % 2 === 1) {
|
|
314
439
|
return (
|
|
315
|
-
<strong key={
|
|
316
|
-
{
|
|
440
|
+
<strong key={`bold-${boldIndex}`} className="chat-bold-highlight">
|
|
441
|
+
{boldPart}
|
|
317
442
|
</strong>
|
|
318
443
|
)
|
|
319
444
|
}
|
|
320
|
-
return
|
|
445
|
+
return boldPart
|
|
321
446
|
})
|
|
447
|
+
})
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const lines = displayText.split('\n')
|
|
451
|
+
const items: any[] = []
|
|
452
|
+
|
|
453
|
+
let inCodeBlock = false
|
|
454
|
+
let codeBlockLang = ''
|
|
455
|
+
let codeBlockLines: string[] = []
|
|
456
|
+
|
|
457
|
+
for (let i = 0; i < lines.length; i++) {
|
|
458
|
+
const line = lines[i]
|
|
459
|
+
|
|
460
|
+
if (line.trim().startsWith('```')) {
|
|
461
|
+
if (inCodeBlock) {
|
|
462
|
+
const codeText = codeBlockLines.join('\n')
|
|
463
|
+
items.push(
|
|
464
|
+
<ChatCodeBlock
|
|
465
|
+
key={`code-block-${i}`}
|
|
466
|
+
code={codeText}
|
|
467
|
+
language={codeBlockLang}
|
|
468
|
+
/>
|
|
469
|
+
)
|
|
470
|
+
inCodeBlock = false
|
|
471
|
+
codeBlockLines = []
|
|
472
|
+
} else {
|
|
473
|
+
inCodeBlock = true
|
|
474
|
+
codeBlockLang = line.trim().slice(3).trim() || 'plaintext'
|
|
475
|
+
}
|
|
476
|
+
continue
|
|
322
477
|
}
|
|
323
478
|
|
|
479
|
+
if (inCodeBlock) {
|
|
480
|
+
codeBlockLines.push(line)
|
|
481
|
+
continue
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const headerMatch = line.match(/^(#{1,6})\s+(.*)$/)
|
|
324
485
|
if (headerMatch) {
|
|
325
486
|
const level = headerMatch[1].length
|
|
326
487
|
const content = headerMatch[2]
|
|
327
|
-
|
|
488
|
+
|
|
328
489
|
const style = {
|
|
329
490
|
fontWeight: 'bold',
|
|
330
491
|
display: 'block',
|
|
331
492
|
marginTop: level === 1 ? '16px' : level === 2 ? '14px' : '10px',
|
|
332
493
|
marginBottom: '6px',
|
|
333
494
|
fontSize: level === 1 ? '1.25em' : level === 2 ? '1.15em' : '1.05em',
|
|
334
|
-
color: 'var(--text-main)'
|
|
495
|
+
color: 'var(--text-main)',
|
|
335
496
|
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
<span key={
|
|
497
|
+
|
|
498
|
+
items.push(
|
|
499
|
+
<span key={`line-${i}`} style={style}>
|
|
339
500
|
{formatInline(content)}
|
|
340
501
|
</span>
|
|
341
502
|
)
|
|
503
|
+
} else {
|
|
504
|
+
items.push(
|
|
505
|
+
<Fragment key={`line-${i}`}>
|
|
506
|
+
{formatInline(line)}
|
|
507
|
+
{i < lines.length - 1 && '\n'}
|
|
508
|
+
</Fragment>
|
|
509
|
+
)
|
|
342
510
|
}
|
|
511
|
+
}
|
|
343
512
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
513
|
+
if (inCodeBlock && codeBlockLines.length > 0) {
|
|
514
|
+
const codeText = codeBlockLines.join('\n')
|
|
515
|
+
items.push(
|
|
516
|
+
<ChatCodeBlock
|
|
517
|
+
key={`code-block-end`}
|
|
518
|
+
code={codeText}
|
|
519
|
+
language={codeBlockLang}
|
|
520
|
+
/>
|
|
349
521
|
)
|
|
350
|
-
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
return items
|
|
351
525
|
}
|
|
352
526
|
|
|
353
527
|
function readableAssistantText(text: string) {
|
|
@@ -554,6 +728,24 @@ export default function ChatPanel({
|
|
|
554
728
|
const [openReviewIds, setOpenReviewIds] = useState<Record<string, boolean>>({})
|
|
555
729
|
const [openFileDiffs, setOpenFileDiffs] = useState<Record<string, boolean>>({})
|
|
556
730
|
const [toolMenuOpen, setToolMenuOpen] = useState(false)
|
|
731
|
+
const [elapsedSeconds, setElapsedSeconds] = useState(0)
|
|
732
|
+
|
|
733
|
+
useEffect(() => {
|
|
734
|
+
if (!sending) {
|
|
735
|
+
setElapsedSeconds(0)
|
|
736
|
+
return
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
const startTime = Date.now()
|
|
740
|
+
setElapsedSeconds(0)
|
|
741
|
+
|
|
742
|
+
const timer = setInterval(() => {
|
|
743
|
+
const elapsed = Math.round((Date.now() - startTime) / 1000)
|
|
744
|
+
setElapsedSeconds(elapsed)
|
|
745
|
+
}, 1000)
|
|
746
|
+
|
|
747
|
+
return () => clearInterval(timer)
|
|
748
|
+
}, [sending])
|
|
557
749
|
const [isRecording, setIsRecording] = useState(false)
|
|
558
750
|
const [voiceMode, setVoiceMode] = useState(false)
|
|
559
751
|
const [voiceTranscript, setVoiceTranscript] = useState('')
|
|
@@ -1318,7 +1510,30 @@ export default function ChatPanel({
|
|
|
1318
1510
|
{renderActiveFileChanges()}
|
|
1319
1511
|
<div className="message ai-message thinking-message">
|
|
1320
1512
|
<div className="bubble-wrapper">
|
|
1321
|
-
<div className="message-bubble"
|
|
1513
|
+
<div className="message-bubble">
|
|
1514
|
+
<span>
|
|
1515
|
+
{streamedReply ? (
|
|
1516
|
+
renderFormattedMessage(streamedReply)
|
|
1517
|
+
) : (
|
|
1518
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', color: 'var(--text-soft, #94a3b8)' }}>
|
|
1519
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" style={{ display: 'inline-block', flexShrink: 0 }}>
|
|
1520
|
+
<circle cx="12" cy="12" r="10" stroke="rgba(255, 255, 255, 0.12)" />
|
|
1521
|
+
<path d="M12 2a10 10 0 0 1 10 10" stroke="var(--accent)" strokeLinecap="round">
|
|
1522
|
+
<animateTransform
|
|
1523
|
+
attributeName="transform"
|
|
1524
|
+
type="rotate"
|
|
1525
|
+
from="0 12 12"
|
|
1526
|
+
to="360 12 12"
|
|
1527
|
+
dur="0.9s"
|
|
1528
|
+
repeatCount="indefinite"
|
|
1529
|
+
/>
|
|
1530
|
+
</path>
|
|
1531
|
+
</svg>
|
|
1532
|
+
<span>Thinking for {elapsedSeconds}s</span>
|
|
1533
|
+
</div>
|
|
1534
|
+
)}
|
|
1535
|
+
</span>
|
|
1536
|
+
</div>
|
|
1322
1537
|
{streamedResponse && (
|
|
1323
1538
|
<div className="message-time" style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
1324
1539
|
<button className="provider-badge">{badge(streamedResponse.provider, streamedResponse.model)}</button>
|
|
@@ -1342,14 +1557,25 @@ export default function ChatPanel({
|
|
|
1342
1557
|
|
|
1343
1558
|
{pendingApproval && (() => {
|
|
1344
1559
|
const details = renderApprovalDetails(pendingApproval.approval)
|
|
1560
|
+
const writeFile = pendingApproval.approval?.WriteFile
|
|
1561
|
+
const applyPatch = pendingApproval.approval?.ApplyPatch
|
|
1562
|
+
const diffText = writeFile?.diff || applyPatch?.diff
|
|
1563
|
+
|
|
1345
1564
|
return (
|
|
1346
1565
|
<div className="message ai-message" style={{ width: '100%' }}>
|
|
1347
1566
|
<div className="bubble-wrapper" style={{ width: '100%' }}>
|
|
1348
1567
|
<div className="action-card approval-card" data-tier={details.isDangerous ? 'dangerous' : undefined} style={{ width: '100%' }}>
|
|
1349
|
-
<div className="approval-card-content">
|
|
1568
|
+
<div className="approval-card-content" style={{ width: '100%' }}>
|
|
1350
1569
|
<div className="approval-card-title">{details.title}</div>
|
|
1351
1570
|
<div className="approval-card-body" style={{ whiteSpace: 'pre-wrap', fontFamily: 'monospace' }}>{details.body}</div>
|
|
1352
|
-
{
|
|
1571
|
+
{diffText ? (
|
|
1572
|
+
<div className="approval-card-diff-container" style={{ marginTop: '8px', width: '100%' }}>
|
|
1573
|
+
<div style={{ fontSize: '0.8rem', fontWeight: 600, color: 'var(--text-soft, #94a3b8)', marginBottom: '4px' }}>Diff:</div>
|
|
1574
|
+
{renderDiff(diffText)}
|
|
1575
|
+
</div>
|
|
1576
|
+
) : (
|
|
1577
|
+
details.reason && <div className="approval-card-reason" style={{ whiteSpace: 'pre-wrap', fontFamily: 'monospace' }}>{details.reason}</div>
|
|
1578
|
+
)}
|
|
1353
1579
|
</div>
|
|
1354
1580
|
<div className="approval-card-actions">
|
|
1355
1581
|
<button type="button" className="approval-btn approval-btn-approve" onClick={() => onApproval(true)}>Approve</button>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useState } from 'react'
|
|
2
2
|
|
|
3
|
-
export type DashboardView = 'chat' | 'pictures' | 'model' | 'workspeac'
|
|
3
|
+
export type DashboardView = 'chat' | 'pictures' | 'model' | 'workspeac' | 'imagine'
|
|
4
4
|
|
|
5
5
|
interface ChatSessionItem {
|
|
6
6
|
id: string
|
|
@@ -133,6 +133,14 @@ export default function DashboardSidebar({
|
|
|
133
133
|
</span>
|
|
134
134
|
<span>Pictures</span>
|
|
135
135
|
</button>
|
|
136
|
+
<button className={`sidebar-top-action ${view === 'imagine' ? 'is-active' : ''}`} onClick={() => onSetView('imagine')}>
|
|
137
|
+
<span aria-hidden="true" style={{ display: 'inline-flex', alignItems: 'center' }}>
|
|
138
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
|
139
|
+
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon>
|
|
140
|
+
</svg>
|
|
141
|
+
</span>
|
|
142
|
+
<span>Image Studio</span>
|
|
143
|
+
</button>
|
|
136
144
|
<button className={`sidebar-top-action ${view === 'workspeac' ? 'is-active' : ''}`} onClick={() => onSetView('workspeac')}>
|
|
137
145
|
<span aria-hidden="true" style={{ display: 'inline-flex', alignItems: 'center' }}>
|
|
138
146
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|