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