Loading editor/html/css/editor.css +4 −0 Original line number Diff line number Diff line Loading @@ -550,6 +550,10 @@ dialog.ai-panel::backdrop { } .ai-send-btn:hover { opacity: 0.85; } .ai-send-btn:disabled { opacity: 0.4; cursor: default; } .ai-send-btn.ai-stop-btn { background: #c00; color: #fff; } .ai-attachments { display: flex; flex-wrap: wrap; Loading editor/html/js/api.js +41 −1 Original line number Diff line number Diff line Loading @@ -42,6 +42,33 @@ var EditorApi = (function() { }); } function requestCancellable(method, url, data) { var xhr = new XMLHttpRequest(); var promise = new Promise(function(resolve, reject) { xhr.open(method, url, true); xhr.setRequestHeader('Content-Type', 'application/json'); xhr.onreadystatechange = function() { if (xhr.readyState !== 4) return; if (xhr.status === 401) { window.location.href = '/login.html'; return; } if (xhr.status >= 200 && xhr.status < 300) { try { resolve(JSON.parse(xhr.responseText)); } catch(e) { resolve(xhr.responseText); } } else { try { reject(JSON.parse(xhr.responseText)); } catch(e) { reject({error: xhr.statusText || 'Request failed'}); } } }; xhr.onerror = function() { reject({error: 'Network error'}); }; xhr.onabort = function() { reject({error: 'aborted', aborted: true}); }; if (data !== undefined) xhr.send(JSON.stringify(data)); else xhr.send(); }); return { promise: promise, abort: function() { xhr.abort(); } }; } return { login: function(username, password) { return request('POST', '/api/login', { Loading Loading @@ -180,7 +207,7 @@ var EditorApi = (function() { }, aiPrompt: function(prompt, desktop, mobile, includeDocument) { return request('POST', '/api/ai/prompt', { return requestCancellable('POST', '/api/ai/prompt', { prompt: prompt, desktop: desktop, mobile: mobile, Loading @@ -188,6 +215,19 @@ var EditorApi = (function() { }); }, // AI chat persistence loadChatMessages: function() { return request('GET', '/api/ai/chat'); }, saveChatMessages: function(messages) { return request('POST', '/api/ai/chat', { messages: messages }); }, clearChatMessages: function() { return request('DELETE', '/api/ai/chat'); }, // Blog connection endpoints getConnectionSettings: function() { return request('GET', '/api/connection/settings'); Loading editor/html/js/editor.js +101 −18 Original line number Diff line number Diff line Loading @@ -141,8 +141,7 @@ }); document.getElementById('btn-import-xml').addEventListener('click', function() { document.getElementById('import-xml-content').value = ''; document.getElementById('import-dialog').showModal(); document.getElementById('import-xml-file').click(); }); document.getElementById('btn-export-xml').addEventListener('click', function() { Loading Loading @@ -178,16 +177,13 @@ document.getElementById('btn-ai').addEventListener('click', function() { var dlg = document.getElementById('ai-dialog'); if (dlg.open) { dlg.close(); } else { dlg.show(); // non-modal if (!dlg.open) { dlg.show(); } }); document.getElementById('btn-import-html').addEventListener('click', function() { document.getElementById('import-html-content').value = ''; document.getElementById('import-html-dialog').showModal(); document.getElementById('import-html-file').click(); }); document.getElementById('btn-export-html').addEventListener('click', function() { Loading Loading @@ -269,7 +265,15 @@ if (!file) return; var reader = new FileReader(); reader.onload = function(e) { document.getElementById('import-xml-content').value = e.target.result; var xml = e.target.result; if (!xml.trim()) return; EditorApi.importXml(xml).then(function() { DocumentTree.clearSelection(); PropertiesPanel.clear(); refreshDocument(); }).catch(function(err) { alert(I18n.t('I18N_IMPORT_FAILED') + ': ' + (err.error || '')); }); }; reader.readAsText(file); this.value = ''; Loading Loading @@ -314,7 +318,15 @@ if (!file) return; var reader = new FileReader(); reader.onload = function(e) { document.getElementById('import-html-content').value = e.target.result; var html = e.target.result; if (!html.trim()) return; EditorApi.importHtml(html).then(function() { DocumentTree.clearSelection(); PropertiesPanel.clear(); refreshDocument(); }).catch(function(err) { alert(I18n.t('I18N_IMPORT_FAILED') + ': ' + (err.error || '')); }); }; reader.readAsText(file); this.value = ''; Loading Loading @@ -645,6 +657,46 @@ var fileInput = document.getElementById('ai-file-input'); var attachmentsDiv = document.getElementById('ai-attachments'); var pendingFiles = []; // {name, content} var savePending = null; var currentAiRequest = null; // {abort} for cancellation // Restore chat from server function restoreChat() { EditorApi.loadChatMessages().then(function(resp) { var msgs = resp.messages || []; for (var i = 0; i < msgs.length; i++) { if (msgs[i].type === 'assistant') { addAssistantMessage(msgs[i].text, true); } else { addMessage(msgs[i].type, msgs[i].text, true); } } }).catch(function() {}); } function saveChat() { // Debounce saves to avoid excessive requests if (savePending) clearTimeout(savePending); savePending = setTimeout(function() { savePending = null; var msgs = []; var nodes = chatHistory.querySelectorAll('.ai-msg'); for (var i = 0; i < nodes.length; i++) { var el = nodes[i]; if (el.classList.contains('status')) continue; var type = el.classList.contains('user') ? 'user' : el.classList.contains('assistant') ? 'assistant' : el.classList.contains('error') ? 'error' : 'status'; var text = type === 'assistant' ? (el.querySelector('div') ? el.querySelector('div').textContent : el.textContent) : el.textContent; msgs.push({type: type, text: text}); } EditorApi.saveChatMessages(msgs).catch(function() {}); }, 500); } restoreChat(); // Close document.getElementById('btn-ai-close').addEventListener('click', function() { Loading @@ -656,6 +708,7 @@ chatHistory.innerHTML = ''; pendingFiles = []; attachmentsDiv.innerHTML = ''; EditorApi.clearChatMessages().catch(function() {}); }); // Drag support Loading Loading @@ -730,6 +783,13 @@ // Send prompt sendBtn.addEventListener('click', function() { // If request running, abort it if (currentAiRequest) { currentAiRequest.abort(); currentAiRequest = null; return; } var prompt = promptEl.value.trim(); if (!prompt && pendingFiles.length === 0) return; Loading @@ -746,7 +806,7 @@ // Show user message var userLabel = prompt; if (pendingFiles.length > 0) { userLabel += '\n📄 ' + pendingFiles.map(function(f) { return f.name; }).join(', '); userLabel += '\n\uD83D\uDCC4 ' + pendingFiles.map(function(f) { return f.name; }).join(', '); } addMessage('user', userLabel); Loading @@ -756,30 +816,52 @@ promptEl.value = ''; pendingFiles = []; attachmentsDiv.innerHTML = ''; sendBtn.disabled = true; EditorApi.aiPrompt(fullPrompt, desktop, mobile, includeDoc).then(function(resp) { // Switch send button to stop button sendBtn.innerHTML = '⏹'; sendBtn.title = 'Stopp'; sendBtn.classList.add('ai-stop-btn'); var req = EditorApi.aiPrompt(fullPrompt, desktop, mobile, includeDoc); currentAiRequest = req; req.promise.then(function(resp) { currentAiRequest = null; statusEl.remove(); var result = resp.result || ''; addAssistantMessage(result); sendBtn.disabled = false; resetSendBtn(); }).catch(function(err) { currentAiRequest = null; statusEl.remove(); if (err && err.aborted) { addMessage('status', I18n.t('I18N_AI_CANCELLED', 'Abgebrochen')); } else { addMessage('error', I18n.t('I18N_AI_FAILED', 'KI-Anfrage fehlgeschlagen') + ': ' + (err.error || '')); sendBtn.disabled = false; } resetSendBtn(); }); }); function addMessage(type, text) { function resetSendBtn() { sendBtn.innerHTML = '➤'; sendBtn.title = 'Senden'; sendBtn.classList.remove('ai-stop-btn'); sendBtn.disabled = false; } }); function addMessage(type, text, noSave) { var div = document.createElement('div'); div.className = 'ai-msg ' + type; div.textContent = text; chatHistory.appendChild(div); chatHistory.scrollTop = chatHistory.scrollHeight; if (!noSave) saveChat(); return div; } function addAssistantMessage(text) { function addAssistantMessage(text, noSave) { var div = document.createElement('div'); div.className = 'ai-msg assistant'; Loading Loading @@ -833,6 +915,7 @@ div.appendChild(actions); chatHistory.appendChild(div); chatHistory.scrollTop = chatHistory.scrollHeight; if (!noSave) saveChat(); return div; } Loading editor/src/webedit_api.cpp +67 −0 Original line number Diff line number Diff line Loading @@ -221,6 +221,12 @@ bool webedit::Api::handleRequest(libhttppp::HttpRequest &curreq, const int tid, return true; } // Route: /api/ai/chat (GET/POST/DELETE) if (path == "/api/ai/chat") { handleAiChat(curreq, sessionid); return true; } // Route: /api/connection/settings (GET/POST) if (path == "/api/connection/settings") { handleConnectionSettings(curreq); Loading Loading @@ -1506,6 +1512,67 @@ void webedit::Api::handleAiPrompt(libhttppp::HttpRequest &curreq, } } // --- AI Chat persistence handler --- void webedit::Api::handleAiChat(libhttppp::HttpRequest &curreq, const std::string &sessionid) { std::string uid; _session.getData(sessionid, "uid", uid); if (uid.empty()) { sendJsonError(curreq, 401, "Not authenticated"); return; } int reqType = curreq.getRequestType(); if (reqType == GETREQUEST) { std::string msgs = _db.loadChatMessages(uid); json_object *arr = json_tokener_parse(msgs.c_str()); if (!arr) arr = json_object_new_array(); json_object *resp = json_object_new_object(); json_object_object_add(resp, "messages", arr); sendJson(curreq, resp); json_object_put(resp); return; } if (reqType == DELETEREQUEST) { _db.clearChatMessages(uid); json_object *resp = json_object_new_object(); json_object_object_add(resp, "status", json_object_new_string("ok")); sendJson(curreq, resp); json_object_put(resp); return; } if (reqType == POSTREQUEST) { std::string body = getRequestBody(curreq); json_object *req = json_tokener_parse(body.c_str()); if (!req) { sendJsonError(curreq, 400, "Invalid JSON"); return; } json_object *msgsObj = nullptr; json_object_object_get_ex(req, "messages", &msgsObj); if (!msgsObj) { json_object_put(req); sendJsonError(curreq, 400, "Missing messages"); return; } const char *msgsStr = json_object_to_json_string_ext(msgsObj, JSON_C_TO_STRING_NOSLASHESCAPE); _db.saveChatMessages(uid, msgsStr); json_object_put(req); json_object *resp = json_object_new_object(); json_object_object_add(resp, "status", json_object_new_string("ok")); sendJson(curreq, resp); json_object_put(resp); return; } sendJsonError(curreq, 405, "Method not allowed"); } // --- Blog connection settings handler --- void webedit::Api::handleConnectionSettings(libhttppp::HttpRequest &curreq) { Loading editor/src/webedit_api.h +1 −0 Original line number Diff line number Diff line Loading @@ -105,6 +105,7 @@ namespace webedit { // AI handlers void handleAiSettings(libhttppp::HttpRequest &curreq); void handleAiPrompt(libhttppp::HttpRequest &curreq, const std::string &sessionid); void handleAiChat(libhttppp::HttpRequest &curreq, const std::string &sessionid); // Blog connection handlers void handleConnectionSettings(libhttppp::HttpRequest &curreq); Loading Loading
editor/html/css/editor.css +4 −0 Original line number Diff line number Diff line Loading @@ -550,6 +550,10 @@ dialog.ai-panel::backdrop { } .ai-send-btn:hover { opacity: 0.85; } .ai-send-btn:disabled { opacity: 0.4; cursor: default; } .ai-send-btn.ai-stop-btn { background: #c00; color: #fff; } .ai-attachments { display: flex; flex-wrap: wrap; Loading
editor/html/js/api.js +41 −1 Original line number Diff line number Diff line Loading @@ -42,6 +42,33 @@ var EditorApi = (function() { }); } function requestCancellable(method, url, data) { var xhr = new XMLHttpRequest(); var promise = new Promise(function(resolve, reject) { xhr.open(method, url, true); xhr.setRequestHeader('Content-Type', 'application/json'); xhr.onreadystatechange = function() { if (xhr.readyState !== 4) return; if (xhr.status === 401) { window.location.href = '/login.html'; return; } if (xhr.status >= 200 && xhr.status < 300) { try { resolve(JSON.parse(xhr.responseText)); } catch(e) { resolve(xhr.responseText); } } else { try { reject(JSON.parse(xhr.responseText)); } catch(e) { reject({error: xhr.statusText || 'Request failed'}); } } }; xhr.onerror = function() { reject({error: 'Network error'}); }; xhr.onabort = function() { reject({error: 'aborted', aborted: true}); }; if (data !== undefined) xhr.send(JSON.stringify(data)); else xhr.send(); }); return { promise: promise, abort: function() { xhr.abort(); } }; } return { login: function(username, password) { return request('POST', '/api/login', { Loading Loading @@ -180,7 +207,7 @@ var EditorApi = (function() { }, aiPrompt: function(prompt, desktop, mobile, includeDocument) { return request('POST', '/api/ai/prompt', { return requestCancellable('POST', '/api/ai/prompt', { prompt: prompt, desktop: desktop, mobile: mobile, Loading @@ -188,6 +215,19 @@ var EditorApi = (function() { }); }, // AI chat persistence loadChatMessages: function() { return request('GET', '/api/ai/chat'); }, saveChatMessages: function(messages) { return request('POST', '/api/ai/chat', { messages: messages }); }, clearChatMessages: function() { return request('DELETE', '/api/ai/chat'); }, // Blog connection endpoints getConnectionSettings: function() { return request('GET', '/api/connection/settings'); Loading
editor/html/js/editor.js +101 −18 Original line number Diff line number Diff line Loading @@ -141,8 +141,7 @@ }); document.getElementById('btn-import-xml').addEventListener('click', function() { document.getElementById('import-xml-content').value = ''; document.getElementById('import-dialog').showModal(); document.getElementById('import-xml-file').click(); }); document.getElementById('btn-export-xml').addEventListener('click', function() { Loading Loading @@ -178,16 +177,13 @@ document.getElementById('btn-ai').addEventListener('click', function() { var dlg = document.getElementById('ai-dialog'); if (dlg.open) { dlg.close(); } else { dlg.show(); // non-modal if (!dlg.open) { dlg.show(); } }); document.getElementById('btn-import-html').addEventListener('click', function() { document.getElementById('import-html-content').value = ''; document.getElementById('import-html-dialog').showModal(); document.getElementById('import-html-file').click(); }); document.getElementById('btn-export-html').addEventListener('click', function() { Loading Loading @@ -269,7 +265,15 @@ if (!file) return; var reader = new FileReader(); reader.onload = function(e) { document.getElementById('import-xml-content').value = e.target.result; var xml = e.target.result; if (!xml.trim()) return; EditorApi.importXml(xml).then(function() { DocumentTree.clearSelection(); PropertiesPanel.clear(); refreshDocument(); }).catch(function(err) { alert(I18n.t('I18N_IMPORT_FAILED') + ': ' + (err.error || '')); }); }; reader.readAsText(file); this.value = ''; Loading Loading @@ -314,7 +318,15 @@ if (!file) return; var reader = new FileReader(); reader.onload = function(e) { document.getElementById('import-html-content').value = e.target.result; var html = e.target.result; if (!html.trim()) return; EditorApi.importHtml(html).then(function() { DocumentTree.clearSelection(); PropertiesPanel.clear(); refreshDocument(); }).catch(function(err) { alert(I18n.t('I18N_IMPORT_FAILED') + ': ' + (err.error || '')); }); }; reader.readAsText(file); this.value = ''; Loading Loading @@ -645,6 +657,46 @@ var fileInput = document.getElementById('ai-file-input'); var attachmentsDiv = document.getElementById('ai-attachments'); var pendingFiles = []; // {name, content} var savePending = null; var currentAiRequest = null; // {abort} for cancellation // Restore chat from server function restoreChat() { EditorApi.loadChatMessages().then(function(resp) { var msgs = resp.messages || []; for (var i = 0; i < msgs.length; i++) { if (msgs[i].type === 'assistant') { addAssistantMessage(msgs[i].text, true); } else { addMessage(msgs[i].type, msgs[i].text, true); } } }).catch(function() {}); } function saveChat() { // Debounce saves to avoid excessive requests if (savePending) clearTimeout(savePending); savePending = setTimeout(function() { savePending = null; var msgs = []; var nodes = chatHistory.querySelectorAll('.ai-msg'); for (var i = 0; i < nodes.length; i++) { var el = nodes[i]; if (el.classList.contains('status')) continue; var type = el.classList.contains('user') ? 'user' : el.classList.contains('assistant') ? 'assistant' : el.classList.contains('error') ? 'error' : 'status'; var text = type === 'assistant' ? (el.querySelector('div') ? el.querySelector('div').textContent : el.textContent) : el.textContent; msgs.push({type: type, text: text}); } EditorApi.saveChatMessages(msgs).catch(function() {}); }, 500); } restoreChat(); // Close document.getElementById('btn-ai-close').addEventListener('click', function() { Loading @@ -656,6 +708,7 @@ chatHistory.innerHTML = ''; pendingFiles = []; attachmentsDiv.innerHTML = ''; EditorApi.clearChatMessages().catch(function() {}); }); // Drag support Loading Loading @@ -730,6 +783,13 @@ // Send prompt sendBtn.addEventListener('click', function() { // If request running, abort it if (currentAiRequest) { currentAiRequest.abort(); currentAiRequest = null; return; } var prompt = promptEl.value.trim(); if (!prompt && pendingFiles.length === 0) return; Loading @@ -746,7 +806,7 @@ // Show user message var userLabel = prompt; if (pendingFiles.length > 0) { userLabel += '\n📄 ' + pendingFiles.map(function(f) { return f.name; }).join(', '); userLabel += '\n\uD83D\uDCC4 ' + pendingFiles.map(function(f) { return f.name; }).join(', '); } addMessage('user', userLabel); Loading @@ -756,30 +816,52 @@ promptEl.value = ''; pendingFiles = []; attachmentsDiv.innerHTML = ''; sendBtn.disabled = true; EditorApi.aiPrompt(fullPrompt, desktop, mobile, includeDoc).then(function(resp) { // Switch send button to stop button sendBtn.innerHTML = '⏹'; sendBtn.title = 'Stopp'; sendBtn.classList.add('ai-stop-btn'); var req = EditorApi.aiPrompt(fullPrompt, desktop, mobile, includeDoc); currentAiRequest = req; req.promise.then(function(resp) { currentAiRequest = null; statusEl.remove(); var result = resp.result || ''; addAssistantMessage(result); sendBtn.disabled = false; resetSendBtn(); }).catch(function(err) { currentAiRequest = null; statusEl.remove(); if (err && err.aborted) { addMessage('status', I18n.t('I18N_AI_CANCELLED', 'Abgebrochen')); } else { addMessage('error', I18n.t('I18N_AI_FAILED', 'KI-Anfrage fehlgeschlagen') + ': ' + (err.error || '')); sendBtn.disabled = false; } resetSendBtn(); }); }); function addMessage(type, text) { function resetSendBtn() { sendBtn.innerHTML = '➤'; sendBtn.title = 'Senden'; sendBtn.classList.remove('ai-stop-btn'); sendBtn.disabled = false; } }); function addMessage(type, text, noSave) { var div = document.createElement('div'); div.className = 'ai-msg ' + type; div.textContent = text; chatHistory.appendChild(div); chatHistory.scrollTop = chatHistory.scrollHeight; if (!noSave) saveChat(); return div; } function addAssistantMessage(text) { function addAssistantMessage(text, noSave) { var div = document.createElement('div'); div.className = 'ai-msg assistant'; Loading Loading @@ -833,6 +915,7 @@ div.appendChild(actions); chatHistory.appendChild(div); chatHistory.scrollTop = chatHistory.scrollHeight; if (!noSave) saveChat(); return div; } Loading
editor/src/webedit_api.cpp +67 −0 Original line number Diff line number Diff line Loading @@ -221,6 +221,12 @@ bool webedit::Api::handleRequest(libhttppp::HttpRequest &curreq, const int tid, return true; } // Route: /api/ai/chat (GET/POST/DELETE) if (path == "/api/ai/chat") { handleAiChat(curreq, sessionid); return true; } // Route: /api/connection/settings (GET/POST) if (path == "/api/connection/settings") { handleConnectionSettings(curreq); Loading Loading @@ -1506,6 +1512,67 @@ void webedit::Api::handleAiPrompt(libhttppp::HttpRequest &curreq, } } // --- AI Chat persistence handler --- void webedit::Api::handleAiChat(libhttppp::HttpRequest &curreq, const std::string &sessionid) { std::string uid; _session.getData(sessionid, "uid", uid); if (uid.empty()) { sendJsonError(curreq, 401, "Not authenticated"); return; } int reqType = curreq.getRequestType(); if (reqType == GETREQUEST) { std::string msgs = _db.loadChatMessages(uid); json_object *arr = json_tokener_parse(msgs.c_str()); if (!arr) arr = json_object_new_array(); json_object *resp = json_object_new_object(); json_object_object_add(resp, "messages", arr); sendJson(curreq, resp); json_object_put(resp); return; } if (reqType == DELETEREQUEST) { _db.clearChatMessages(uid); json_object *resp = json_object_new_object(); json_object_object_add(resp, "status", json_object_new_string("ok")); sendJson(curreq, resp); json_object_put(resp); return; } if (reqType == POSTREQUEST) { std::string body = getRequestBody(curreq); json_object *req = json_tokener_parse(body.c_str()); if (!req) { sendJsonError(curreq, 400, "Invalid JSON"); return; } json_object *msgsObj = nullptr; json_object_object_get_ex(req, "messages", &msgsObj); if (!msgsObj) { json_object_put(req); sendJsonError(curreq, 400, "Missing messages"); return; } const char *msgsStr = json_object_to_json_string_ext(msgsObj, JSON_C_TO_STRING_NOSLASHESCAPE); _db.saveChatMessages(uid, msgsStr); json_object_put(req); json_object *resp = json_object_new_object(); json_object_object_add(resp, "status", json_object_new_string("ok")); sendJson(curreq, resp); json_object_put(resp); return; } sendJsonError(curreq, 405, "Method not allowed"); } // --- Blog connection settings handler --- void webedit::Api::handleConnectionSettings(libhttppp::HttpRequest &curreq) { Loading
editor/src/webedit_api.h +1 −0 Original line number Diff line number Diff line Loading @@ -105,6 +105,7 @@ namespace webedit { // AI handlers void handleAiSettings(libhttppp::HttpRequest &curreq); void handleAiPrompt(libhttppp::HttpRequest &curreq, const std::string &sessionid); void handleAiChat(libhttppp::HttpRequest &curreq, const std::string &sessionid); // Blog connection handlers void handleConnectionSettings(libhttppp::HttpRequest &curreq); Loading