|
154 | 154 |
|
155 | 155 | let taskIds = null; |
156 | 156 |
|
| 157 | + // Streaming render state |
| 158 | + let renderRafId = null; |
| 159 | + let lastTtsProcessTime = 0; |
| 160 | +
|
157 | 161 | // Chat Input |
158 | 162 | let prompt = ''; |
159 | 163 | let chatFiles = []; |
|
382 | 386 | }; |
383 | 387 |
|
384 | 388 | const chatEventHandler = async (event, cb) => { |
385 | | - console.log(event); |
386 | | -
|
387 | 389 | if (event.chat_id === $chatId) { |
388 | 390 | await tick(); |
389 | 391 | let message = history.messages[event.message_id]; |
|
508 | 510 | console.log('Unknown message type', data); |
509 | 511 | } |
510 | 512 |
|
511 | | - history.messages[event.message_id] = message; |
| 513 | + // Batch reactivity updates during streaming to reduce rendering overhead. |
| 514 | + // During active streaming, content changes accumulate on the message reference |
| 515 | + // and a single render is scheduled per animation frame (~60fps). |
| 516 | + // Non-streaming events (done, status changes, etc.) render immediately. |
| 517 | + if (type === 'chat:completion' && !data?.done) { |
| 518 | + if (!renderRafId) { |
| 519 | + const msgId = event.message_id; |
| 520 | + renderRafId = requestAnimationFrame(() => { |
| 521 | + renderRafId = null; |
| 522 | + history.messages[msgId] = history.messages[msgId]; |
| 523 | + if (autoScroll) { |
| 524 | + scrollToBottom(); |
| 525 | + } |
| 526 | + }); |
| 527 | + } |
| 528 | + } else { |
| 529 | + history.messages[event.message_id] = message; |
| 530 | + } |
512 | 531 | } |
513 | 532 | } |
514 | 533 | }; |
|
1479 | 1498 | navigator.vibrate(5); |
1480 | 1499 | } |
1481 | 1500 |
|
1482 | | - // Emit chat event for TTS |
1483 | | - const messageContentParts = getMessageContentParts( |
1484 | | - removeAllDetails(message.content), |
1485 | | - $config?.audio?.tts?.split_on ?? 'punctuation' |
1486 | | - ); |
1487 | | - messageContentParts.pop(); |
| 1501 | + // Throttle TTS processing to avoid re-parsing content on every token |
| 1502 | + const now = Date.now(); |
| 1503 | + if (now - lastTtsProcessTime >= 150) { |
| 1504 | + lastTtsProcessTime = now; |
1488 | 1505 |
|
1489 | | - // dispatch only last sentence and make sure it hasn't been dispatched before |
1490 | | - if ( |
1491 | | - messageContentParts.length > 0 && |
1492 | | - messageContentParts[messageContentParts.length - 1] !== message.lastSentence |
1493 | | - ) { |
1494 | | - message.lastSentence = messageContentParts[messageContentParts.length - 1]; |
1495 | | - eventTarget.dispatchEvent( |
1496 | | - new CustomEvent('chat', { |
1497 | | - detail: { |
1498 | | - id: message.id, |
1499 | | - content: messageContentParts[messageContentParts.length - 1] |
1500 | | - } |
1501 | | - }) |
| 1506 | + const messageContentParts = getMessageContentParts( |
| 1507 | + removeAllDetails(message.content), |
| 1508 | + $config?.audio?.tts?.split_on ?? 'punctuation' |
1502 | 1509 | ); |
| 1510 | + messageContentParts.pop(); |
| 1511 | +
|
| 1512 | + if ( |
| 1513 | + messageContentParts.length > 0 && |
| 1514 | + messageContentParts[messageContentParts.length - 1] !== message.lastSentence |
| 1515 | + ) { |
| 1516 | + message.lastSentence = messageContentParts[messageContentParts.length - 1]; |
| 1517 | + eventTarget.dispatchEvent( |
| 1518 | + new CustomEvent('chat', { |
| 1519 | + detail: { |
| 1520 | + id: message.id, |
| 1521 | + content: messageContentParts[messageContentParts.length - 1] |
| 1522 | + } |
| 1523 | + }) |
| 1524 | + ); |
| 1525 | + } |
1503 | 1526 | } |
1504 | 1527 | } |
1505 | 1528 | } |
|
1513 | 1536 | navigator.vibrate(5); |
1514 | 1537 | } |
1515 | 1538 |
|
1516 | | - // Emit chat event for TTS |
1517 | | - const messageContentParts = getMessageContentParts( |
1518 | | - removeAllDetails(message.content), |
1519 | | - $config?.audio?.tts?.split_on ?? 'punctuation' |
1520 | | - ); |
1521 | | - messageContentParts.pop(); |
| 1539 | + // Throttle TTS processing to avoid re-parsing content on every token |
| 1540 | + const now = Date.now(); |
| 1541 | + if (now - lastTtsProcessTime >= 150) { |
| 1542 | + lastTtsProcessTime = now; |
1522 | 1543 |
|
1523 | | - // dispatch only last sentence and make sure it hasn't been dispatched before |
1524 | | - if ( |
1525 | | - messageContentParts.length > 0 && |
1526 | | - messageContentParts[messageContentParts.length - 1] !== message.lastSentence |
1527 | | - ) { |
1528 | | - message.lastSentence = messageContentParts[messageContentParts.length - 1]; |
1529 | | - eventTarget.dispatchEvent( |
1530 | | - new CustomEvent('chat', { |
1531 | | - detail: { |
1532 | | - id: message.id, |
1533 | | - content: messageContentParts[messageContentParts.length - 1] |
1534 | | - } |
1535 | | - }) |
| 1544 | + const messageContentParts = getMessageContentParts( |
| 1545 | + removeAllDetails(message.content), |
| 1546 | + $config?.audio?.tts?.split_on ?? 'punctuation' |
1536 | 1547 | ); |
| 1548 | + messageContentParts.pop(); |
| 1549 | +
|
| 1550 | + if ( |
| 1551 | + messageContentParts.length > 0 && |
| 1552 | + messageContentParts[messageContentParts.length - 1] !== message.lastSentence |
| 1553 | + ) { |
| 1554 | + message.lastSentence = messageContentParts[messageContentParts.length - 1]; |
| 1555 | + eventTarget.dispatchEvent( |
| 1556 | + new CustomEvent('chat', { |
| 1557 | + detail: { |
| 1558 | + id: message.id, |
| 1559 | + content: messageContentParts[messageContentParts.length - 1] |
| 1560 | + } |
| 1561 | + }) |
| 1562 | + ); |
| 1563 | + } |
1537 | 1564 | } |
1538 | 1565 | } |
1539 | 1566 |
|
|
1546 | 1573 | message.usage = usage; |
1547 | 1574 | } |
1548 | 1575 |
|
1549 | | - history.messages[message.id] = message; |
1550 | | -
|
1551 | 1576 | if (done) { |
1552 | 1577 | message.done = true; |
1553 | 1578 |
|
|
1596 | 1621 | createMessagesList(history, message.id) |
1597 | 1622 | ); |
1598 | 1623 | } |
1599 | | -
|
1600 | | - console.log(data); |
1601 | | - await tick(); |
1602 | | -
|
1603 | | - if (autoScroll) { |
1604 | | - scrollToBottom(); |
1605 | | - } |
1606 | 1624 | }; |
1607 | 1625 |
|
1608 | 1626 | ////////////////////////// |
|
0 commit comments