Commit 20cf6123 authored by jan.koester's avatar jan.koester
Browse files

ai improvements

parent fb2493b3
Loading
Loading
Loading
Loading
+4 −0
Original line number Diff line number Diff line
@@ -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;
+41 −1
Original line number Diff line number Diff line
@@ -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', {
@@ -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,
@@ -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');
+101 −18
Original line number Diff line number Diff line
@@ -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() {
@@ -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() {
@@ -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 = '';
@@ -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 = '';
@@ -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() {
@@ -656,6 +708,7 @@
            chatHistory.innerHTML = '';
            pendingFiles = [];
            attachmentsDiv.innerHTML = '';
            EditorApi.clearChatMessages().catch(function() {});
        });

        // Drag support
@@ -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;

@@ -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);

@@ -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 = '&#x23F9;';
            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 = '&#x27A4;';
            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';

@@ -833,6 +915,7 @@
            div.appendChild(actions);
            chatHistory.appendChild(div);
            chatHistory.scrollTop = chatHistory.scrollHeight;
            if (!noSave) saveChat();
            return div;
        }

+67 −0
Original line number Diff line number Diff line
@@ -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);
@@ -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) {
+1 −0
Original line number Diff line number Diff line
@@ -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