@farazirfan/costar-server-executor 1.7.4 → 1.7.7
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/dist/agent/agent.d.ts +14 -0
- package/dist/agent/agent.d.ts.map +1 -1
- package/dist/agent/agent.js +2 -0
- package/dist/agent/agent.js.map +1 -1
- package/dist/agent/pi-embedded-runner/run.js +2 -0
- package/dist/agent/pi-embedded-runner/run.js.map +1 -1
- package/dist/agent/pi-embedded-runner/subscribe.d.ts +14 -0
- package/dist/agent/pi-embedded-runner/subscribe.d.ts.map +1 -1
- package/dist/agent/pi-embedded-runner/subscribe.js +45 -4
- package/dist/agent/pi-embedded-runner/subscribe.js.map +1 -1
- package/dist/agent/pi-embedded-runner/types.d.ts +14 -0
- package/dist/agent/pi-embedded-runner/types.d.ts.map +1 -1
- package/dist/api/chat.d.ts +2 -0
- package/dist/api/chat.d.ts.map +1 -1
- package/dist/api/chat.js +98 -0
- package/dist/api/chat.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -5
- package/dist/index.js.map +1 -1
- package/dist/tools/fetch-api-data.d.ts +15 -4
- package/dist/tools/fetch-api-data.d.ts.map +1 -1
- package/dist/tools/fetch-api-data.js +169 -81
- package/dist/tools/fetch-api-data.js.map +1 -1
- package/dist/tools/index.d.ts +5 -5
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +8 -7
- package/dist/tools/index.js.map +1 -1
- package/dist/web-server.d.ts.map +1 -1
- package/dist/web-server.js +31 -1
- package/dist/web-server.js.map +1 -1
- package/package.json +1 -1
- package/public/index.html +387 -21
package/public/index.html
CHANGED
|
@@ -509,6 +509,146 @@
|
|
|
509
509
|
flex: 1;
|
|
510
510
|
}
|
|
511
511
|
|
|
512
|
+
/* ─── Chat Tool Cards ──────────────────────────── */
|
|
513
|
+
.tool-card {
|
|
514
|
+
background: var(--bg-secondary);
|
|
515
|
+
border: 1px solid var(--border);
|
|
516
|
+
border-radius: var(--radius);
|
|
517
|
+
margin: 8px 0;
|
|
518
|
+
font-size: 13px;
|
|
519
|
+
overflow: hidden;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
.tool-card-header {
|
|
523
|
+
display: flex;
|
|
524
|
+
align-items: center;
|
|
525
|
+
gap: 8px;
|
|
526
|
+
padding: 8px 12px;
|
|
527
|
+
cursor: pointer;
|
|
528
|
+
user-select: none;
|
|
529
|
+
transition: background 0.15s;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
.tool-card-header:hover {
|
|
533
|
+
background: var(--bg-hover);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
.tool-icon {
|
|
537
|
+
font-size: 14px;
|
|
538
|
+
flex-shrink: 0;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
.tool-icon.running {
|
|
542
|
+
animation: spin 1s linear infinite;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
@keyframes spin {
|
|
546
|
+
from { transform: rotate(0deg); }
|
|
547
|
+
to { transform: rotate(360deg); }
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
.tool-name {
|
|
551
|
+
font-weight: 600;
|
|
552
|
+
color: var(--accent);
|
|
553
|
+
font-family: var(--mono);
|
|
554
|
+
font-size: 12px;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
.tool-status {
|
|
558
|
+
margin-left: auto;
|
|
559
|
+
font-size: 11px;
|
|
560
|
+
font-family: var(--mono);
|
|
561
|
+
color: var(--text-muted);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
.tool-status.success {
|
|
565
|
+
color: var(--green);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
.tool-status.error {
|
|
569
|
+
color: var(--red);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
.tool-chevron {
|
|
573
|
+
font-size: 10px;
|
|
574
|
+
color: var(--text-muted);
|
|
575
|
+
transition: transform 0.2s;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
.tool-card.expanded .tool-chevron {
|
|
579
|
+
transform: rotate(90deg);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
.tool-card-body {
|
|
583
|
+
display: none;
|
|
584
|
+
border-top: 1px solid var(--border);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
.tool-card.expanded .tool-card-body {
|
|
588
|
+
display: block;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
.tool-section {
|
|
592
|
+
padding: 8px 12px;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
.tool-section + .tool-section {
|
|
596
|
+
border-top: 1px solid var(--border);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
.tool-section-label {
|
|
600
|
+
font-size: 10px;
|
|
601
|
+
font-weight: 600;
|
|
602
|
+
text-transform: uppercase;
|
|
603
|
+
letter-spacing: 0.05em;
|
|
604
|
+
color: var(--text-muted);
|
|
605
|
+
margin-bottom: 4px;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
.tool-section pre {
|
|
609
|
+
font-family: var(--mono);
|
|
610
|
+
font-size: 11px;
|
|
611
|
+
color: var(--text-secondary);
|
|
612
|
+
white-space: pre-wrap;
|
|
613
|
+
word-break: break-all;
|
|
614
|
+
max-height: 200px;
|
|
615
|
+
overflow-y: auto;
|
|
616
|
+
margin: 0;
|
|
617
|
+
line-height: 1.5;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/* Streaming cursor */
|
|
621
|
+
.streaming-cursor::after {
|
|
622
|
+
content: "";
|
|
623
|
+
display: inline-block;
|
|
624
|
+
width: 8px;
|
|
625
|
+
height: 16px;
|
|
626
|
+
background: var(--accent);
|
|
627
|
+
margin-left: 2px;
|
|
628
|
+
animation: blink 0.8s step-end infinite;
|
|
629
|
+
vertical-align: text-bottom;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
@keyframes blink {
|
|
633
|
+
50% { opacity: 0; }
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/* Chat metadata bar */
|
|
637
|
+
.chat-meta {
|
|
638
|
+
display: flex;
|
|
639
|
+
gap: 12px;
|
|
640
|
+
padding: 6px 0;
|
|
641
|
+
font-size: 11px;
|
|
642
|
+
color: var(--text-muted);
|
|
643
|
+
font-family: var(--mono);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
.chat-meta span {
|
|
647
|
+
display: flex;
|
|
648
|
+
align-items: center;
|
|
649
|
+
gap: 4px;
|
|
650
|
+
}
|
|
651
|
+
|
|
512
652
|
/* ─── Logs ─────────────────────────────────────── */
|
|
513
653
|
.log-toolbar {
|
|
514
654
|
display: flex;
|
|
@@ -1253,7 +1393,9 @@
|
|
|
1253
1393
|
return `${m}m`;
|
|
1254
1394
|
}
|
|
1255
1395
|
|
|
1256
|
-
// ─── Chat
|
|
1396
|
+
// ─── Chat (Real-time Streaming) ─────────────────
|
|
1397
|
+
let chatStreaming = false;
|
|
1398
|
+
|
|
1257
1399
|
async function initChat() {
|
|
1258
1400
|
if (!chatSessionId) {
|
|
1259
1401
|
try {
|
|
@@ -1268,7 +1410,7 @@
|
|
|
1268
1410
|
async function sendMessage() {
|
|
1269
1411
|
const input = document.getElementById('chat-input');
|
|
1270
1412
|
const msg = input.value.trim();
|
|
1271
|
-
if (!msg) return;
|
|
1413
|
+
if (!msg || chatStreaming) return;
|
|
1272
1414
|
|
|
1273
1415
|
if (!chatSessionId) await initChat();
|
|
1274
1416
|
|
|
@@ -1280,41 +1422,265 @@
|
|
|
1280
1422
|
if (emptyState) emptyState.remove();
|
|
1281
1423
|
|
|
1282
1424
|
// Add user message
|
|
1283
|
-
messagesEl.
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
//
|
|
1287
|
-
const
|
|
1288
|
-
messagesEl.
|
|
1289
|
-
|
|
1425
|
+
messagesEl.insertAdjacentHTML('beforeend', chatMsgHTML('user', msg));
|
|
1426
|
+
scrollChat();
|
|
1427
|
+
|
|
1428
|
+
// Create streaming assistant message container
|
|
1429
|
+
const streamId = 'stream-' + Date.now();
|
|
1430
|
+
messagesEl.insertAdjacentHTML('beforeend', `
|
|
1431
|
+
<div id="${streamId}" class="chat-msg assistant">
|
|
1432
|
+
<div class="avatar">C</div>
|
|
1433
|
+
<div class="msg-content">
|
|
1434
|
+
<div class="msg-role">CoStar</div>
|
|
1435
|
+
<div id="${streamId}-tools"></div>
|
|
1436
|
+
<div id="${streamId}-text" class="streaming-cursor"></div>
|
|
1437
|
+
<div id="${streamId}-meta" class="chat-meta" style="display:none;"></div>
|
|
1438
|
+
</div>
|
|
1439
|
+
</div>
|
|
1440
|
+
`);
|
|
1441
|
+
scrollChat();
|
|
1290
1442
|
|
|
1291
1443
|
// Disable send
|
|
1292
1444
|
const sendBtn = document.getElementById('chat-send-btn');
|
|
1293
1445
|
sendBtn.disabled = true;
|
|
1294
|
-
sendBtn.textContent = '...';
|
|
1446
|
+
sendBtn.textContent = 'Thinking...';
|
|
1447
|
+
chatStreaming = true;
|
|
1295
1448
|
|
|
1296
1449
|
try {
|
|
1297
|
-
|
|
1298
|
-
method: 'POST',
|
|
1299
|
-
body: JSON.stringify({ message: msg }),
|
|
1300
|
-
});
|
|
1301
|
-
|
|
1302
|
-
document.getElementById(loadingId).remove();
|
|
1303
|
-
messagesEl.innerHTML += chatMsgHTML('assistant', res.content);
|
|
1304
|
-
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
1450
|
+
await streamChat(chatSessionId, msg, streamId);
|
|
1305
1451
|
} catch (err) {
|
|
1306
|
-
document.getElementById(
|
|
1307
|
-
|
|
1452
|
+
const textEl = document.getElementById(`${streamId}-text`);
|
|
1453
|
+
if (textEl) {
|
|
1454
|
+
textEl.classList.remove('streaming-cursor');
|
|
1455
|
+
textEl.innerHTML = `<span style="color:var(--red);">Error: ${esc(err.message)}</span>`;
|
|
1456
|
+
}
|
|
1308
1457
|
} finally {
|
|
1309
1458
|
sendBtn.disabled = false;
|
|
1310
1459
|
sendBtn.textContent = 'Send';
|
|
1460
|
+
chatStreaming = false;
|
|
1461
|
+
// Remove streaming cursor
|
|
1462
|
+
const textEl = document.getElementById(`${streamId}-text`);
|
|
1463
|
+
if (textEl) textEl.classList.remove('streaming-cursor');
|
|
1311
1464
|
}
|
|
1312
1465
|
}
|
|
1313
1466
|
|
|
1467
|
+
function streamChat(sessionId, message, streamId) {
|
|
1468
|
+
return new Promise((resolve, reject) => {
|
|
1469
|
+
const textEl = document.getElementById(`${streamId}-text`);
|
|
1470
|
+
const toolsEl = document.getElementById(`${streamId}-tools`);
|
|
1471
|
+
const metaEl = document.getElementById(`${streamId}-meta`);
|
|
1472
|
+
let fullText = '';
|
|
1473
|
+
let toolCount = 0;
|
|
1474
|
+
|
|
1475
|
+
// Use fetch + ReadableStream for POST-based SSE
|
|
1476
|
+
fetch(`/api/sessions/${sessionId}/messages/stream`, {
|
|
1477
|
+
method: 'POST',
|
|
1478
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1479
|
+
body: JSON.stringify({ message }),
|
|
1480
|
+
}).then(response => {
|
|
1481
|
+
if (!response.ok) {
|
|
1482
|
+
throw new Error(`HTTP ${response.status}`);
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
const reader = response.body.getReader();
|
|
1486
|
+
const decoder = new TextDecoder();
|
|
1487
|
+
let buffer = '';
|
|
1488
|
+
|
|
1489
|
+
function processChunk(chunk) {
|
|
1490
|
+
buffer += chunk;
|
|
1491
|
+
const lines = buffer.split('\n');
|
|
1492
|
+
buffer = lines.pop() || '';
|
|
1493
|
+
|
|
1494
|
+
let eventType = '';
|
|
1495
|
+
let eventData = '';
|
|
1496
|
+
|
|
1497
|
+
for (const line of lines) {
|
|
1498
|
+
if (line.startsWith('event: ')) {
|
|
1499
|
+
eventType = line.slice(7).trim();
|
|
1500
|
+
} else if (line.startsWith('data: ')) {
|
|
1501
|
+
eventData = line.slice(6);
|
|
1502
|
+
if (eventType && eventData) {
|
|
1503
|
+
handleSSEEvent(eventType, eventData, textEl, toolsEl, metaEl, streamId, {
|
|
1504
|
+
fullText: () => fullText,
|
|
1505
|
+
setFullText: (t) => { fullText = t; },
|
|
1506
|
+
toolCount: () => toolCount,
|
|
1507
|
+
incToolCount: () => { toolCount++; },
|
|
1508
|
+
});
|
|
1509
|
+
eventType = '';
|
|
1510
|
+
eventData = '';
|
|
1511
|
+
}
|
|
1512
|
+
} else if (line === '') {
|
|
1513
|
+
eventType = '';
|
|
1514
|
+
eventData = '';
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
function pump() {
|
|
1520
|
+
return reader.read().then(({ done, value }) => {
|
|
1521
|
+
if (done) {
|
|
1522
|
+
if (buffer.trim()) processChunk('\n');
|
|
1523
|
+
resolve();
|
|
1524
|
+
return;
|
|
1525
|
+
}
|
|
1526
|
+
processChunk(decoder.decode(value, { stream: true }));
|
|
1527
|
+
return pump();
|
|
1528
|
+
});
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
return pump();
|
|
1532
|
+
}).catch(reject);
|
|
1533
|
+
});
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
function handleSSEEvent(type, dataStr, textEl, toolsEl, metaEl, streamId, state) {
|
|
1537
|
+
let data;
|
|
1538
|
+
try { data = JSON.parse(dataStr); } catch { return; }
|
|
1539
|
+
|
|
1540
|
+
switch (type) {
|
|
1541
|
+
case 'connected':
|
|
1542
|
+
break;
|
|
1543
|
+
|
|
1544
|
+
case 'text_delta':
|
|
1545
|
+
if (data.text && textEl) {
|
|
1546
|
+
state.setFullText(state.fullText() + data.text);
|
|
1547
|
+
textEl.innerHTML = formatMessage(state.fullText());
|
|
1548
|
+
scrollChat();
|
|
1549
|
+
}
|
|
1550
|
+
break;
|
|
1551
|
+
|
|
1552
|
+
case 'tool_start':
|
|
1553
|
+
if (toolsEl) {
|
|
1554
|
+
state.incToolCount();
|
|
1555
|
+
const cardId = `${streamId}-tool-${data.toolCallId || state.toolCount()}`;
|
|
1556
|
+
const argsStr = data.args ? JSON.stringify(data.args, null, 2) : '{}';
|
|
1557
|
+
const shortArgs = argsStr.length > 100 ? argsStr.slice(0, 100) + '...' : argsStr;
|
|
1558
|
+
|
|
1559
|
+
toolsEl.insertAdjacentHTML('beforeend', `
|
|
1560
|
+
<div class="tool-card" id="${cardId}" onclick="toggleToolCard('${cardId}')">
|
|
1561
|
+
<div class="tool-card-header">
|
|
1562
|
+
<span class="tool-icon running">⚙</span>
|
|
1563
|
+
<span class="tool-name">${esc(data.name || 'tool')}</span>
|
|
1564
|
+
<span class="tool-status">running...</span>
|
|
1565
|
+
<span class="tool-chevron">▶</span>
|
|
1566
|
+
</div>
|
|
1567
|
+
<div class="tool-card-body">
|
|
1568
|
+
<div class="tool-section">
|
|
1569
|
+
<div class="tool-section-label">Arguments</div>
|
|
1570
|
+
<pre>${esc(argsStr)}</pre>
|
|
1571
|
+
</div>
|
|
1572
|
+
<div class="tool-section" id="${cardId}-result" style="display:none;">
|
|
1573
|
+
<div class="tool-section-label">Result</div>
|
|
1574
|
+
<pre></pre>
|
|
1575
|
+
</div>
|
|
1576
|
+
</div>
|
|
1577
|
+
</div>
|
|
1578
|
+
`);
|
|
1579
|
+
scrollChat();
|
|
1580
|
+
|
|
1581
|
+
// Update send button with tool count
|
|
1582
|
+
const sendBtn = document.getElementById('chat-send-btn');
|
|
1583
|
+
sendBtn.textContent = `Tool ${state.toolCount()}...`;
|
|
1584
|
+
}
|
|
1585
|
+
break;
|
|
1586
|
+
|
|
1587
|
+
case 'tool_end':
|
|
1588
|
+
if (toolsEl) {
|
|
1589
|
+
const cardId = `${streamId}-tool-${data.toolCallId || state.toolCount()}`;
|
|
1590
|
+
const card = document.getElementById(cardId);
|
|
1591
|
+
if (card) {
|
|
1592
|
+
// Update icon — stop spinning
|
|
1593
|
+
const icon = card.querySelector('.tool-icon');
|
|
1594
|
+
if (icon) {
|
|
1595
|
+
icon.classList.remove('running');
|
|
1596
|
+
icon.innerHTML = data.isError ? '❌' : '✅';
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
// Update status
|
|
1600
|
+
const status = card.querySelector('.tool-status');
|
|
1601
|
+
if (status) {
|
|
1602
|
+
const durationStr = data.durationMs ? ` (${formatDurationShort(data.durationMs)})` : '';
|
|
1603
|
+
status.textContent = data.isError ? `error${durationStr}` : `done${durationStr}`;
|
|
1604
|
+
status.className = `tool-status ${data.isError ? 'error' : 'success'}`;
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
// Show result
|
|
1608
|
+
const resultSection = document.getElementById(`${cardId}-result`);
|
|
1609
|
+
if (resultSection && data.result) {
|
|
1610
|
+
resultSection.style.display = '';
|
|
1611
|
+
resultSection.querySelector('pre').textContent = data.result;
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
scrollChat();
|
|
1615
|
+
}
|
|
1616
|
+
break;
|
|
1617
|
+
|
|
1618
|
+
case 'done':
|
|
1619
|
+
if (textEl) {
|
|
1620
|
+
textEl.classList.remove('streaming-cursor');
|
|
1621
|
+
// Ensure final text is rendered
|
|
1622
|
+
if (data.response) {
|
|
1623
|
+
textEl.innerHTML = formatMessage(data.response);
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
if (metaEl && (data.toolCalls > 0 || data.inputTokens > 0)) {
|
|
1627
|
+
metaEl.style.display = '';
|
|
1628
|
+
let metaHTML = '';
|
|
1629
|
+
if (data.toolCalls > 0) metaHTML += `<span>🔧 ${data.toolCalls} tool${data.toolCalls > 1 ? 's' : ''}</span>`;
|
|
1630
|
+
if (data.inputTokens > 0) metaHTML += `<span>→ ${data.inputTokens.toLocaleString()} in</span>`;
|
|
1631
|
+
if (data.outputTokens > 0) metaHTML += `<span>← ${data.outputTokens.toLocaleString()} out</span>`;
|
|
1632
|
+
metaEl.innerHTML = metaHTML;
|
|
1633
|
+
}
|
|
1634
|
+
break;
|
|
1635
|
+
|
|
1636
|
+
case 'error':
|
|
1637
|
+
if (textEl) {
|
|
1638
|
+
textEl.classList.remove('streaming-cursor');
|
|
1639
|
+
const errorMsg = data.message || 'Unknown error';
|
|
1640
|
+
const currentText = state.fullText();
|
|
1641
|
+
textEl.innerHTML = (currentText ? formatMessage(currentText) + '<br><br>' : '') +
|
|
1642
|
+
`<span style="color:var(--red);">Error: ${esc(errorMsg)}</span>`;
|
|
1643
|
+
}
|
|
1644
|
+
break;
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
function toggleToolCard(cardId) {
|
|
1649
|
+
const card = document.getElementById(cardId);
|
|
1650
|
+
if (card) card.classList.toggle('expanded');
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
function formatDurationShort(ms) {
|
|
1654
|
+
if (ms < 1000) return `${ms}ms`;
|
|
1655
|
+
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
|
1656
|
+
return `${Math.floor(ms / 60000)}m ${Math.floor((ms % 60000) / 1000)}s`;
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
function formatMessage(text) {
|
|
1660
|
+
if (!text) return '';
|
|
1661
|
+
// Simple markdown-ish formatting
|
|
1662
|
+
let html = esc(text);
|
|
1663
|
+
// Code blocks: ```...```
|
|
1664
|
+
html = html.replace(/```(\w*)\n([\s\S]*?)```/g, '<pre style="background:var(--bg-primary);border:1px solid var(--border);border-radius:var(--radius);padding:10px;margin:8px 0;overflow-x:auto;font-family:var(--mono);font-size:12px;">$2</pre>');
|
|
1665
|
+
// Inline code: `...`
|
|
1666
|
+
html = html.replace(/`([^`]+)`/g, '<code style="background:var(--bg-primary);padding:2px 6px;border-radius:4px;font-family:var(--mono);font-size:12px;">$1</code>');
|
|
1667
|
+
// Bold: **...**
|
|
1668
|
+
html = html.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
|
|
1669
|
+
// Newlines
|
|
1670
|
+
html = html.replace(/\n/g, '<br>');
|
|
1671
|
+
return html;
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
function scrollChat() {
|
|
1675
|
+
const el = document.getElementById('chat-messages');
|
|
1676
|
+
if (el) el.scrollTop = el.scrollHeight;
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1314
1679
|
function chatMsgHTML(role, content) {
|
|
1315
1680
|
const avatar = role === 'user' ? 'U' : 'C';
|
|
1316
1681
|
const label = role === 'user' ? 'You' : 'CoStar';
|
|
1317
|
-
|
|
1682
|
+
const formattedContent = role === 'user' ? esc(content) : formatMessage(content);
|
|
1683
|
+
return `<div class="chat-msg ${role}"><div class="avatar">${avatar}</div><div class="msg-content"><div class="msg-role">${label}</div><div>${formattedContent}</div></div></div>`;
|
|
1318
1684
|
}
|
|
1319
1685
|
|
|
1320
1686
|
// ─── Cron ────────────────────────────────────────
|