|
1095 | 1095 | const messages = history ? createMessagesList(history, history.currentId) : []; |
1096 | 1096 | let contents = []; |
1097 | 1097 | messages.forEach((message) => { |
1098 | | - if (message?.role !== 'user' && message?.content) { |
| 1098 | + if (message?.role !== 'user') { |
| 1099 | + let text = message?.content || ''; |
| 1100 | + if (!text && message?.output?.length) { |
| 1101 | + text = message.output |
| 1102 | + .filter((i) => i.type === 'message') |
| 1103 | + .flatMap((i) => (i.content ?? []).map((p) => p.text ?? '')) |
| 1104 | + .join('\n'); |
| 1105 | + } |
| 1106 | + if (!text) return; |
1099 | 1107 | const { codeBlocks: codeBlocks, htmlGroups: htmlGroups } = getCodeBlockContents( |
1100 | | - message.content |
| 1108 | + text |
1101 | 1109 | ); |
1102 | 1110 |
|
1103 | 1111 | if (htmlGroups && htmlGroups.length > 0) { |
|
1752 | 1760 | const chatCompletionEventHandler = async (data, message, chatId) => { |
1753 | 1761 | const { id, done, choices, content, output, sources, selected_model_id, error, usage } = data; |
1754 | 1762 |
|
| 1763 | + const extractTextFromOutput = (o) => |
| 1764 | + o |
| 1765 | + .filter((i) => i.type === 'message') |
| 1766 | + .flatMap((i) => (i.content ?? []).map((p) => (p.text ?? '').trim())) |
| 1767 | + .filter(Boolean) |
| 1768 | + .join('\n'); |
| 1769 | +
|
| 1770 | + const dispatchStreamingTTS = (text) => { |
| 1771 | + if (!$showCallOverlay) return; |
| 1772 | + const parts = getMessageContentParts( |
| 1773 | + text, |
| 1774 | + $config?.audio?.tts?.split_on ?? 'punctuation' |
| 1775 | + ); |
| 1776 | + parts.pop(); |
| 1777 | + if (parts.length > 0 && parts[parts.length - 1] !== message.lastSentence) { |
| 1778 | + message.lastSentence = parts[parts.length - 1]; |
| 1779 | + eventTarget.dispatchEvent( |
| 1780 | + new CustomEvent('chat', { |
| 1781 | + detail: { id: message.id, content: parts[parts.length - 1] } |
| 1782 | + }) |
| 1783 | + ); |
| 1784 | + } |
| 1785 | + }; |
| 1786 | +
|
1755 | 1787 | // Store raw OR-aligned output items from backend |
1756 | 1788 | if (output) { |
1757 | 1789 | message.output = output; |
| 1790 | + dispatchStreamingTTS(extractTextFromOutput(output)); |
1758 | 1791 | } |
1759 | 1792 |
|
1760 | 1793 | if (error) { |
|
1765 | 1798 | message.sources = sources; |
1766 | 1799 | } |
1767 | 1800 |
|
1768 | | - if (choices) { |
| 1801 | + // Only accumulate content from choices when there's no structured output |
| 1802 | + // (output-based rendering takes priority — avoids redundant serialize/parse round-trip) |
| 1803 | + if (choices && !output) { |
1769 | 1804 | if (choices[0]?.message?.content) { |
1770 | 1805 | // Non-stream response |
1771 | 1806 | message.content += choices[0]?.message?.content; |
|
1781 | 1816 | navigator.vibrate(5); |
1782 | 1817 | } |
1783 | 1818 |
|
1784 | | - // Emit chat event for TTS (only when call overlay is active) |
1785 | | - if ($showCallOverlay) { |
1786 | | - const messageContentParts = getMessageContentParts( |
1787 | | - removeAllDetails(message.content), |
1788 | | - $config?.audio?.tts?.split_on ?? 'punctuation' |
1789 | | - ); |
1790 | | - messageContentParts.pop(); |
1791 | | -
|
1792 | | - // dispatch only last sentence and make sure it hasn't been dispatched before |
1793 | | - if ( |
1794 | | - messageContentParts.length > 0 && |
1795 | | - messageContentParts[messageContentParts.length - 1] !== message.lastSentence |
1796 | | - ) { |
1797 | | - message.lastSentence = messageContentParts[messageContentParts.length - 1]; |
1798 | | - eventTarget.dispatchEvent( |
1799 | | - new CustomEvent('chat', { |
1800 | | - detail: { |
1801 | | - id: message.id, |
1802 | | - content: messageContentParts[messageContentParts.length - 1] |
1803 | | - } |
1804 | | - }) |
1805 | | - ); |
1806 | | - } |
1807 | | - } |
| 1819 | + dispatchStreamingTTS(removeAllDetails(message.content)); |
1808 | 1820 | } |
1809 | 1821 | } |
1810 | 1822 | } |
|
1817 | 1829 | navigator.vibrate(5); |
1818 | 1830 | } |
1819 | 1831 |
|
1820 | | - // Emit chat event for TTS (only when call overlay is active) |
1821 | | - if ($showCallOverlay) { |
1822 | | - const messageContentParts = getMessageContentParts( |
1823 | | - removeAllDetails(message.content), |
1824 | | - $config?.audio?.tts?.split_on ?? 'punctuation' |
1825 | | - ); |
1826 | | - messageContentParts.pop(); |
1827 | | -
|
1828 | | - // dispatch only last sentence and make sure it hasn't been dispatched before |
1829 | | - if ( |
1830 | | - messageContentParts.length > 0 && |
1831 | | - messageContentParts[messageContentParts.length - 1] !== message.lastSentence |
1832 | | - ) { |
1833 | | - message.lastSentence = messageContentParts[messageContentParts.length - 1]; |
1834 | | - eventTarget.dispatchEvent( |
1835 | | - new CustomEvent('chat', { |
1836 | | - detail: { |
1837 | | - id: message.id, |
1838 | | - content: messageContentParts[messageContentParts.length - 1] |
1839 | | - } |
1840 | | - }) |
1841 | | - ); |
1842 | | - } |
1843 | | - } |
| 1832 | + dispatchStreamingTTS(removeAllDetails(message.content)); |
1844 | 1833 | } |
1845 | 1834 |
|
1846 | 1835 | if (selected_model_id) { |
|
0 commit comments