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