Loading editor/html/css/editor.css +16 −0 Changes for editor/html/css/editor.css: 16 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -807,6 +807,22 @@ dialog.ai-panel::backdrop { font-style: italic; margin-top: 4px; } .ai-msg-thinking { color: var(--text-secondary); font-size: 12px; font-style: italic; margin-bottom: 6px; border-left: 2px solid var(--border); padding-left: 8px; } .ai-msg-thinking summary { cursor: pointer; font-style: normal; } .ai-msg-thinking > div { margin-top: 4px; white-space: pre-wrap; } .ai-msg.assistant .ai-msg-actions { display: flex; gap: 6px; margin-top: 6px; } Loading editor/html/js/api.js +2 −2 Changes for editor/html/js/api.js: 2 added lines, 2 removed lines. Original line number Diff line number Diff line Loading @@ -343,9 +343,9 @@ var EditorApi = (function() { if (contentType.indexOf('text/event-stream') === -1) { // Non-streaming fallback (HTTP/2 or HTTP/3 connection). return response.json().then(function(data) { var done = { type: 'done', result: data.result, applied: data.applied, tool_calls: data.tool_calls }; var done = { type: 'done', result: data.result, thinking: data.thinking, applied: data.applied, tool_calls: data.tool_calls }; if (onEvent) onEvent(done); return { result: data.result, applied: data.applied, tool_calls: data.tool_calls }; return { result: data.result, thinking: data.thinking, applied: data.applied, tool_calls: data.tool_calls }; }); } Loading editor/html/js/editor.js +24 −0 Changes for editor/html/js/editor.js: 24 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -1651,11 +1651,14 @@ if (ev.type === 'content_delta') { streamMsg.appendText(ev.text || ''); } else if (ev.type === 'thinking_delta') { streamMsg.appendThinking(ev.text || ''); } else if (ev.type === 'tool_call') { streamMsg.setStatus(I18n.t('I18N_AI_CALLING_TOOL', 'Werkzeug wird aufgerufen: ') + (ev.name || '')); } else if (ev.type === 'error') { addMessage('error', I18n.t('I18N_AI_FAILED', 'KI-Anfrage fehlgeschlagen') + ': ' + (ev.message || '')); } else if (ev.type === 'done') { if (ev.thinking) streamMsg.appendThinking(ev.thinking); if (ev.applied) { Canvas.clearSelection(); DocumentTree.clearSelection(); PropertiesPanel.clear(); Loading Loading @@ -1710,6 +1713,8 @@ var div = document.createElement('div'); div.className = 'ai-msg assistant'; var thinkingDetails = null, thinkingBody = null; var content = document.createElement('div'); div.appendChild(content); Loading @@ -1724,7 +1729,25 @@ var fullText = ''; return { appendThinking: function(piece) { if (!thinkingDetails) { thinkingDetails = document.createElement('details'); thinkingDetails.className = 'ai-msg-thinking'; thinkingDetails.open = true; var summary = document.createElement('summary'); summary.textContent = I18n.t('I18N_AI_THINKING', 'Gedanken'); thinkingDetails.appendChild(summary); thinkingBody = document.createElement('div'); thinkingDetails.appendChild(thinkingBody); div.insertBefore(thinkingDetails, content); } thinkingBody.textContent += piece; chatHistory.scrollTop = chatHistory.scrollHeight; }, appendText: function(piece) { // The model has moved from thinking to answering -- collapse // the thoughts so the answer is what's visible by default. if (thinkingDetails && thinkingDetails.open) thinkingDetails.open = false; fullText += piece; content.textContent = fullText; statusLine.style.display = 'none'; Loading @@ -1736,6 +1759,7 @@ chatHistory.scrollTop = chatHistory.scrollHeight; }, finalize: function(applied, toolCalls) { if (thinkingDetails) thinkingDetails.open = false; if (applied) { statusLine.textContent = '✓ ' + toolCalls + ' ' + I18n.t('I18N_AI_APPLIED', 'Änderung(en) direkt am Dokument angewendet'); Loading editor/src/webedit_api.cpp +26 −0 Changes for editor/src/webedit_api.cpp: 26 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -3757,6 +3757,7 @@ void webedit::Api::handleAiPrompt(libhttppp::HttpRequest &curreq, if (!canStream) { std::string finalResult; std::string finalThinking; bool anyMutation = false; int toolCallCount = 0; bool haveFinal = false; Loading Loading @@ -3883,6 +3884,10 @@ void webedit::Api::handleAiPrompt(libhttppp::HttpRequest &curreq, json_object *content = nullptr; if (json_object_object_get_ex(message, "content", &content) && content) finalResult = json_object_get_string(content); json_object *reasoning = nullptr; if ((json_object_object_get_ex(message, "reasoning_content", &reasoning) && reasoning) || (json_object_object_get_ex(message, "reasoning", &reasoning) && reasoning)) finalThinking = json_object_get_string(reasoning); json_object_put(aiResp); haveFinal = true; } Loading @@ -3901,6 +3906,8 @@ void webedit::Api::handleAiPrompt(libhttppp::HttpRequest &curreq, json_object *resp = json_object_new_object(); json_object_object_add(resp, "result", json_object_new_string(finalResult.c_str())); if (!finalThinking.empty()) json_object_object_add(resp, "thinking", json_object_new_string(finalThinking.c_str())); json_object_object_add(resp, "applied", json_object_new_boolean(anyMutation)); json_object_object_add(resp, "tool_calls", json_object_new_int(toolCallCount)); sendJson(curreq, resp); Loading Loading @@ -4015,6 +4022,25 @@ void webedit::Api::handleAiPrompt(libhttppp::HttpRequest &curreq, } } // Reasoning models (DeepSeek-R1, QwQ, etc. served behind an // OpenAI-compatible endpoint) stream their chain-of-thought // separately from the answer, as "reasoning_content" (or // "reasoning" on some backends) -- surface it as its own // event so the UI can show it distinctly instead of dropping // it silently. json_object *jReasoning = nullptr; if (!json_object_object_get_ex(delta, "reasoning_content", &jReasoning) || !jReasoning) json_object_object_get_ex(delta, "reasoning", &jReasoning); if (jReasoning && json_object_get_type(jReasoning) == json_type_string) { std::string piece = json_object_get_string(jReasoning); if (!piece.empty()) { json_object *ev = json_object_new_object(); json_object_object_add(ev, "type", json_object_new_string("thinking_delta")); json_object_object_add(ev, "text", json_object_new_string(piece.c_str())); pushEvent(ev); } } json_object *jToolCalls = nullptr; if (json_object_object_get_ex(delta, "tool_calls", &jToolCalls) && jToolCalls && json_object_get_type(jToolCalls) == json_type_array) { Loading src/translations.h +2 −0 Changes for src/translations.h: 2 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -365,6 +365,7 @@ namespace blogi { {"I18N_AI_MODEL", "Modell"}, {"I18N_AI_MAX_TOKENS", "Max Tokens"}, {"I18N_AI_MAX_TOOL_ROUNDS", "Max Werkzeug-Runden"}, {"I18N_AI_THINKING", "Gedanken"}, {"I18N_AI_CONTEXT", "Kontext"}, {"I18N_AI_GENERATE", "Text generieren"}, {"I18N_AI_REWRITE", "Element umschreiben"}, Loading Loading @@ -867,6 +868,7 @@ namespace blogi { {"I18N_AI_MODEL", "Model"}, {"I18N_AI_MAX_TOKENS", "Max Tokens"}, {"I18N_AI_MAX_TOOL_ROUNDS", "Max tool rounds"}, {"I18N_AI_THINKING", "Thoughts"}, {"I18N_AI_CONTEXT", "Context"}, {"I18N_AI_GENERATE", "Generate text"}, {"I18N_AI_REWRITE", "Rewrite element"}, Loading Loading
editor/html/css/editor.css +16 −0 Changes for editor/html/css/editor.css: 16 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -807,6 +807,22 @@ dialog.ai-panel::backdrop { font-style: italic; margin-top: 4px; } .ai-msg-thinking { color: var(--text-secondary); font-size: 12px; font-style: italic; margin-bottom: 6px; border-left: 2px solid var(--border); padding-left: 8px; } .ai-msg-thinking summary { cursor: pointer; font-style: normal; } .ai-msg-thinking > div { margin-top: 4px; white-space: pre-wrap; } .ai-msg.assistant .ai-msg-actions { display: flex; gap: 6px; margin-top: 6px; } Loading
editor/html/js/api.js +2 −2 Changes for editor/html/js/api.js: 2 added lines, 2 removed lines. Original line number Diff line number Diff line Loading @@ -343,9 +343,9 @@ var EditorApi = (function() { if (contentType.indexOf('text/event-stream') === -1) { // Non-streaming fallback (HTTP/2 or HTTP/3 connection). return response.json().then(function(data) { var done = { type: 'done', result: data.result, applied: data.applied, tool_calls: data.tool_calls }; var done = { type: 'done', result: data.result, thinking: data.thinking, applied: data.applied, tool_calls: data.tool_calls }; if (onEvent) onEvent(done); return { result: data.result, applied: data.applied, tool_calls: data.tool_calls }; return { result: data.result, thinking: data.thinking, applied: data.applied, tool_calls: data.tool_calls }; }); } Loading
editor/html/js/editor.js +24 −0 Changes for editor/html/js/editor.js: 24 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -1651,11 +1651,14 @@ if (ev.type === 'content_delta') { streamMsg.appendText(ev.text || ''); } else if (ev.type === 'thinking_delta') { streamMsg.appendThinking(ev.text || ''); } else if (ev.type === 'tool_call') { streamMsg.setStatus(I18n.t('I18N_AI_CALLING_TOOL', 'Werkzeug wird aufgerufen: ') + (ev.name || '')); } else if (ev.type === 'error') { addMessage('error', I18n.t('I18N_AI_FAILED', 'KI-Anfrage fehlgeschlagen') + ': ' + (ev.message || '')); } else if (ev.type === 'done') { if (ev.thinking) streamMsg.appendThinking(ev.thinking); if (ev.applied) { Canvas.clearSelection(); DocumentTree.clearSelection(); PropertiesPanel.clear(); Loading Loading @@ -1710,6 +1713,8 @@ var div = document.createElement('div'); div.className = 'ai-msg assistant'; var thinkingDetails = null, thinkingBody = null; var content = document.createElement('div'); div.appendChild(content); Loading @@ -1724,7 +1729,25 @@ var fullText = ''; return { appendThinking: function(piece) { if (!thinkingDetails) { thinkingDetails = document.createElement('details'); thinkingDetails.className = 'ai-msg-thinking'; thinkingDetails.open = true; var summary = document.createElement('summary'); summary.textContent = I18n.t('I18N_AI_THINKING', 'Gedanken'); thinkingDetails.appendChild(summary); thinkingBody = document.createElement('div'); thinkingDetails.appendChild(thinkingBody); div.insertBefore(thinkingDetails, content); } thinkingBody.textContent += piece; chatHistory.scrollTop = chatHistory.scrollHeight; }, appendText: function(piece) { // The model has moved from thinking to answering -- collapse // the thoughts so the answer is what's visible by default. if (thinkingDetails && thinkingDetails.open) thinkingDetails.open = false; fullText += piece; content.textContent = fullText; statusLine.style.display = 'none'; Loading @@ -1736,6 +1759,7 @@ chatHistory.scrollTop = chatHistory.scrollHeight; }, finalize: function(applied, toolCalls) { if (thinkingDetails) thinkingDetails.open = false; if (applied) { statusLine.textContent = '✓ ' + toolCalls + ' ' + I18n.t('I18N_AI_APPLIED', 'Änderung(en) direkt am Dokument angewendet'); Loading
editor/src/webedit_api.cpp +26 −0 Changes for editor/src/webedit_api.cpp: 26 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -3757,6 +3757,7 @@ void webedit::Api::handleAiPrompt(libhttppp::HttpRequest &curreq, if (!canStream) { std::string finalResult; std::string finalThinking; bool anyMutation = false; int toolCallCount = 0; bool haveFinal = false; Loading Loading @@ -3883,6 +3884,10 @@ void webedit::Api::handleAiPrompt(libhttppp::HttpRequest &curreq, json_object *content = nullptr; if (json_object_object_get_ex(message, "content", &content) && content) finalResult = json_object_get_string(content); json_object *reasoning = nullptr; if ((json_object_object_get_ex(message, "reasoning_content", &reasoning) && reasoning) || (json_object_object_get_ex(message, "reasoning", &reasoning) && reasoning)) finalThinking = json_object_get_string(reasoning); json_object_put(aiResp); haveFinal = true; } Loading @@ -3901,6 +3906,8 @@ void webedit::Api::handleAiPrompt(libhttppp::HttpRequest &curreq, json_object *resp = json_object_new_object(); json_object_object_add(resp, "result", json_object_new_string(finalResult.c_str())); if (!finalThinking.empty()) json_object_object_add(resp, "thinking", json_object_new_string(finalThinking.c_str())); json_object_object_add(resp, "applied", json_object_new_boolean(anyMutation)); json_object_object_add(resp, "tool_calls", json_object_new_int(toolCallCount)); sendJson(curreq, resp); Loading Loading @@ -4015,6 +4022,25 @@ void webedit::Api::handleAiPrompt(libhttppp::HttpRequest &curreq, } } // Reasoning models (DeepSeek-R1, QwQ, etc. served behind an // OpenAI-compatible endpoint) stream their chain-of-thought // separately from the answer, as "reasoning_content" (or // "reasoning" on some backends) -- surface it as its own // event so the UI can show it distinctly instead of dropping // it silently. json_object *jReasoning = nullptr; if (!json_object_object_get_ex(delta, "reasoning_content", &jReasoning) || !jReasoning) json_object_object_get_ex(delta, "reasoning", &jReasoning); if (jReasoning && json_object_get_type(jReasoning) == json_type_string) { std::string piece = json_object_get_string(jReasoning); if (!piece.empty()) { json_object *ev = json_object_new_object(); json_object_object_add(ev, "type", json_object_new_string("thinking_delta")); json_object_object_add(ev, "text", json_object_new_string(piece.c_str())); pushEvent(ev); } } json_object *jToolCalls = nullptr; if (json_object_object_get_ex(delta, "tool_calls", &jToolCalls) && jToolCalls && json_object_get_type(jToolCalls) == json_type_array) { Loading
src/translations.h +2 −0 Changes for src/translations.h: 2 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -365,6 +365,7 @@ namespace blogi { {"I18N_AI_MODEL", "Modell"}, {"I18N_AI_MAX_TOKENS", "Max Tokens"}, {"I18N_AI_MAX_TOOL_ROUNDS", "Max Werkzeug-Runden"}, {"I18N_AI_THINKING", "Gedanken"}, {"I18N_AI_CONTEXT", "Kontext"}, {"I18N_AI_GENERATE", "Text generieren"}, {"I18N_AI_REWRITE", "Element umschreiben"}, Loading Loading @@ -867,6 +868,7 @@ namespace blogi { {"I18N_AI_MODEL", "Model"}, {"I18N_AI_MAX_TOKENS", "Max Tokens"}, {"I18N_AI_MAX_TOOL_ROUNDS", "Max tool rounds"}, {"I18N_AI_THINKING", "Thoughts"}, {"I18N_AI_CONTEXT", "Context"}, {"I18N_AI_GENERATE", "Generate text"}, {"I18N_AI_REWRITE", "Rewrite element"}, Loading