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