|
1168 | 1168 | const messages = history ? createMessagesList(history, history.currentId) : []; |
1169 | 1169 | let contents = []; |
1170 | 1170 | messages.forEach((message) => { |
1171 | | - if (message?.role !== 'user' && message?.content) { |
| 1171 | + if (message?.role !== 'user') { |
| 1172 | + let text = message?.content || ''; |
| 1173 | + if (!text && message?.output?.length) { |
| 1174 | + text = message.output |
| 1175 | + .filter((i) => i.type === 'message') |
| 1176 | + .flatMap((i) => (i.content ?? []).map((p) => p.text ?? '')) |
| 1177 | + .join('\n'); |
| 1178 | + } |
| 1179 | + if (!text) return; |
1172 | 1180 | const { codeBlocks: codeBlocks, htmlGroups: htmlGroups } = getCodeBlockContents( |
1173 | | - message.content |
| 1181 | + text |
1174 | 1182 | ); |
1175 | 1183 |
|
1176 | 1184 | if (htmlGroups && htmlGroups.length > 0) { |
|
1825 | 1833 | const chatCompletionEventHandler = async (data, message, chatId) => { |
1826 | 1834 | const { id, done, choices, content, output, sources, selected_model_id, error, usage } = data; |
1827 | 1835 |
|
| 1836 | + const extractTextFromOutput = (o) => |
| 1837 | + o |
| 1838 | + .filter((i) => i.type === 'message') |
| 1839 | + .flatMap((i) => (i.content ?? []).map((p) => (p.text ?? '').trim())) |
| 1840 | + .filter(Boolean) |
| 1841 | + .join('\n'); |
| 1842 | +
|
| 1843 | + const dispatchStreamingTTS = (text) => { |
| 1844 | + if (!$showCallOverlay) return; |
| 1845 | + const parts = getMessageContentParts( |
| 1846 | + text, |
| 1847 | + $config?.audio?.tts?.split_on ?? 'punctuation' |
| 1848 | + ); |
| 1849 | + parts.pop(); |
| 1850 | + if (parts.length > 0 && parts[parts.length - 1] !== message.lastSentence) { |
| 1851 | + message.lastSentence = parts[parts.length - 1]; |
| 1852 | + eventTarget.dispatchEvent( |
| 1853 | + new CustomEvent('chat', { |
| 1854 | + detail: { id: message.id, content: parts[parts.length - 1] } |
| 1855 | + }) |
| 1856 | + ); |
| 1857 | + } |
| 1858 | + }; |
| 1859 | +
|
1828 | 1860 | // Store raw OR-aligned output items from backend |
1829 | 1861 | if (output) { |
1830 | 1862 | message.output = output; |
| 1863 | + dispatchStreamingTTS(extractTextFromOutput(output)); |
1831 | 1864 | } |
1832 | 1865 |
|
1833 | 1866 | if (error) { |
|
1838 | 1871 | message.sources = sources; |
1839 | 1872 | } |
1840 | 1873 |
|
1841 | | - if (choices) { |
| 1874 | + // Only accumulate content from choices when there's no structured output |
| 1875 | + // (output-based rendering takes priority — avoids redundant serialize/parse round-trip) |
| 1876 | + if (choices && !output) { |
1842 | 1877 | if (choices[0]?.message?.content) { |
1843 | 1878 | // Non-stream response |
1844 | 1879 | message.content += choices[0]?.message?.content; |
|
1854 | 1889 | navigator.vibrate(5); |
1855 | 1890 | } |
1856 | 1891 |
|
1857 | | - // Emit chat event for TTS (only when call overlay is active) |
1858 | | - if ($showCallOverlay) { |
1859 | | - const messageContentParts = getMessageContentParts( |
1860 | | - removeAllDetails(message.content), |
1861 | | - $config?.audio?.tts?.split_on ?? 'punctuation' |
1862 | | - ); |
1863 | | - messageContentParts.pop(); |
1864 | | -
|
1865 | | - // dispatch only last sentence and make sure it hasn't been dispatched before |
1866 | | - if ( |
1867 | | - messageContentParts.length > 0 && |
1868 | | - messageContentParts[messageContentParts.length - 1] !== message.lastSentence |
1869 | | - ) { |
1870 | | - message.lastSentence = messageContentParts[messageContentParts.length - 1]; |
1871 | | - eventTarget.dispatchEvent( |
1872 | | - new CustomEvent('chat', { |
1873 | | - detail: { |
1874 | | - id: message.id, |
1875 | | - content: messageContentParts[messageContentParts.length - 1] |
1876 | | - } |
1877 | | - }) |
1878 | | - ); |
1879 | | - } |
1880 | | - } |
| 1892 | + dispatchStreamingTTS(removeAllDetails(message.content)); |
1881 | 1893 | } |
1882 | 1894 | } |
1883 | 1895 | } |
|
1890 | 1902 | navigator.vibrate(5); |
1891 | 1903 | } |
1892 | 1904 |
|
1893 | | - // Emit chat event for TTS (only when call overlay is active) |
1894 | | - if ($showCallOverlay) { |
1895 | | - const messageContentParts = getMessageContentParts( |
1896 | | - removeAllDetails(message.content), |
1897 | | - $config?.audio?.tts?.split_on ?? 'punctuation' |
1898 | | - ); |
1899 | | - messageContentParts.pop(); |
1900 | | -
|
1901 | | - // dispatch only last sentence and make sure it hasn't been dispatched before |
1902 | | - if ( |
1903 | | - messageContentParts.length > 0 && |
1904 | | - messageContentParts[messageContentParts.length - 1] !== message.lastSentence |
1905 | | - ) { |
1906 | | - message.lastSentence = messageContentParts[messageContentParts.length - 1]; |
1907 | | - eventTarget.dispatchEvent( |
1908 | | - new CustomEvent('chat', { |
1909 | | - detail: { |
1910 | | - id: message.id, |
1911 | | - content: messageContentParts[messageContentParts.length - 1] |
1912 | | - } |
1913 | | - }) |
1914 | | - ); |
1915 | | - } |
1916 | | - } |
| 1905 | + dispatchStreamingTTS(removeAllDetails(message.content)); |
1917 | 1906 | } |
1918 | 1907 |
|
1919 | 1908 | if (selected_model_id) { |
|
0 commit comments