|
1126 | 1126 | const messages = history ? createMessagesList(history, history.currentId) : []; |
1127 | 1127 | let contents = []; |
1128 | 1128 | messages.forEach((message) => { |
1129 | | - if (message?.role !== 'user' && message?.content) { |
| 1129 | + if (message?.role !== 'user') { |
| 1130 | + let text = message?.content || ''; |
| 1131 | + if (!text && message?.output?.length) { |
| 1132 | + text = message.output |
| 1133 | + .filter((i) => i.type === 'message') |
| 1134 | + .flatMap((i) => (i.content ?? []).map((p) => p.text ?? '')) |
| 1135 | + .join('\n'); |
| 1136 | + } |
| 1137 | + if (!text) return; |
1130 | 1138 | const { codeBlocks: codeBlocks, htmlGroups: htmlGroups } = getCodeBlockContents( |
1131 | | - message.content |
| 1139 | + text |
1132 | 1140 | ); |
1133 | 1141 |
|
1134 | 1142 | if (htmlGroups && htmlGroups.length > 0) { |
|
1783 | 1791 | const chatCompletionEventHandler = async (data, message, chatId) => { |
1784 | 1792 | const { id, done, choices, content, output, sources, selected_model_id, error, usage } = data; |
1785 | 1793 |
|
| 1794 | + const extractTextFromOutput = (o) => |
| 1795 | + o |
| 1796 | + .filter((i) => i.type === 'message') |
| 1797 | + .flatMap((i) => (i.content ?? []).map((p) => (p.text ?? '').trim())) |
| 1798 | + .filter(Boolean) |
| 1799 | + .join('\n'); |
| 1800 | +
|
| 1801 | + const dispatchStreamingTTS = (text) => { |
| 1802 | + if (!$showCallOverlay) return; |
| 1803 | + const parts = getMessageContentParts( |
| 1804 | + text, |
| 1805 | + $config?.audio?.tts?.split_on ?? 'punctuation' |
| 1806 | + ); |
| 1807 | + parts.pop(); |
| 1808 | + if (parts.length > 0 && parts[parts.length - 1] !== message.lastSentence) { |
| 1809 | + message.lastSentence = parts[parts.length - 1]; |
| 1810 | + eventTarget.dispatchEvent( |
| 1811 | + new CustomEvent('chat', { |
| 1812 | + detail: { id: message.id, content: parts[parts.length - 1] } |
| 1813 | + }) |
| 1814 | + ); |
| 1815 | + } |
| 1816 | + }; |
| 1817 | +
|
1786 | 1818 | // Store raw OR-aligned output items from backend |
1787 | 1819 | if (output) { |
1788 | 1820 | message.output = output; |
| 1821 | + dispatchStreamingTTS(extractTextFromOutput(output)); |
1789 | 1822 | } |
1790 | 1823 |
|
1791 | 1824 | if (error) { |
|
1796 | 1829 | message.sources = sources; |
1797 | 1830 | } |
1798 | 1831 |
|
1799 | | - if (choices) { |
| 1832 | + // Only accumulate content from choices when there's no structured output |
| 1833 | + // (output-based rendering takes priority — avoids redundant serialize/parse round-trip) |
| 1834 | + if (choices && !output) { |
1800 | 1835 | if (choices[0]?.message?.content) { |
1801 | 1836 | // Non-stream response |
1802 | 1837 | message.content += choices[0]?.message?.content; |
|
1812 | 1847 | navigator.vibrate(5); |
1813 | 1848 | } |
1814 | 1849 |
|
1815 | | - // Emit chat event for TTS (only when call overlay is active) |
1816 | | - if ($showCallOverlay) { |
1817 | | - const messageContentParts = getMessageContentParts( |
1818 | | - removeAllDetails(message.content), |
1819 | | - $config?.audio?.tts?.split_on ?? 'punctuation' |
1820 | | - ); |
1821 | | - messageContentParts.pop(); |
1822 | | -
|
1823 | | - // dispatch only last sentence and make sure it hasn't been dispatched before |
1824 | | - if ( |
1825 | | - messageContentParts.length > 0 && |
1826 | | - messageContentParts[messageContentParts.length - 1] !== message.lastSentence |
1827 | | - ) { |
1828 | | - message.lastSentence = messageContentParts[messageContentParts.length - 1]; |
1829 | | - eventTarget.dispatchEvent( |
1830 | | - new CustomEvent('chat', { |
1831 | | - detail: { |
1832 | | - id: message.id, |
1833 | | - content: messageContentParts[messageContentParts.length - 1] |
1834 | | - } |
1835 | | - }) |
1836 | | - ); |
1837 | | - } |
1838 | | - } |
| 1850 | + dispatchStreamingTTS(removeAllDetails(message.content)); |
1839 | 1851 | } |
1840 | 1852 | } |
1841 | 1853 | } |
|
1848 | 1860 | navigator.vibrate(5); |
1849 | 1861 | } |
1850 | 1862 |
|
1851 | | - // Emit chat event for TTS (only when call overlay is active) |
1852 | | - if ($showCallOverlay) { |
1853 | | - const messageContentParts = getMessageContentParts( |
1854 | | - removeAllDetails(message.content), |
1855 | | - $config?.audio?.tts?.split_on ?? 'punctuation' |
1856 | | - ); |
1857 | | - messageContentParts.pop(); |
1858 | | -
|
1859 | | - // dispatch only last sentence and make sure it hasn't been dispatched before |
1860 | | - if ( |
1861 | | - messageContentParts.length > 0 && |
1862 | | - messageContentParts[messageContentParts.length - 1] !== message.lastSentence |
1863 | | - ) { |
1864 | | - message.lastSentence = messageContentParts[messageContentParts.length - 1]; |
1865 | | - eventTarget.dispatchEvent( |
1866 | | - new CustomEvent('chat', { |
1867 | | - detail: { |
1868 | | - id: message.id, |
1869 | | - content: messageContentParts[messageContentParts.length - 1] |
1870 | | - } |
1871 | | - }) |
1872 | | - ); |
1873 | | - } |
1874 | | - } |
| 1863 | + dispatchStreamingTTS(removeAllDetails(message.content)); |
1875 | 1864 | } |
1876 | 1865 |
|
1877 | 1866 | if (selected_model_id) { |
|
0 commit comments