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