/** * JSON-Editor für die Template-Bearbeitung. * * Generiert ein Formular aus JSON-Daten und extrahiert * die Werte zurück in ein JSON-Objekt. */ let currentIndent = 2; /** * Öffne Edit-Modal für eine Template-Datei * @param {string} path - Pfad zur Template-Datei */ function editModalContent(path) { window.currentEditTemplate = path; const title = path.split('/').pop(); document.getElementById('edit-title').textContent = `Template bearbeiten: ${title}`; window._wasViewModalOpen = document.getElementById('modal').classList.contains('active'); // Inhalt laden und editierbare Formulare abhängig vom Dateityp erstellen fetch(path) .then(r => r.text()) .then(content => { const editContainer = document.getElementById('edit-content-content'); editContainer.innerHTML = ''; if (path.endsWith('.json')) { try { const jsonData = JSON.parse(content); // Original-Indent erkennen const indentMatch = content.match(/^\s{2,8}/m); currentIndent = indentMatch ? indentMatch[0].length : 2; createJsonEditUI(editContainer, jsonData, path); } catch (e) { // Falls JSON ungültig, als Text bearbeiten createTextEditUI(editContainer, content); } } else { // Markdown als einfaches Textfeld createTextEditUI(editContainer, content); } document.getElementById('edit-modal').classList.add('active'); }) .catch(e => showToast(`✗ Fehler beim Laden: ${e.message}`)); } /** * Erstelle ein Textarea für die Bearbeitung * @param {HTMLElement} container - Ziel-Container * @param {string} content - Dateiinhalt */ function createTextEditUI(container, content) { const textarea = document.createElement('textarea'); textarea.id = 'edit-textarea'; textarea.value = content; textarea.style.cssText = 'width: 100%; min-height: 300px; background: var(--bg-input); color: var(--text-primary); border: 1px solid var(--border); border-radius: 4px; padding: 10px; font-family: var(--mono); font-size: 13px;'; textarea.spellcheck = false; container.appendChild(textarea); } /** * Erstelle ein Formular aus JSON-Daten * @param {HTMLElement} container - Ziel-Container * @param {Object} jsonData - Zu bearbeitende JSON-Daten * @param {string} editPathHint - Pfad-Hinweis */ function createJsonEditUI(container, jsonData, editPathHint = '') { editContainerRef = container; container.innerHTML = ''; const formDiv = document.createElement('div'); formDiv.style.cssText = 'display: flex; flex-direction: column; gap: 16px; padding: 8px; background: var(--bg-card); border-radius: 4px; margin: 0; min-height: 300px; overflow-y: auto;'; // Rekursive Funktion zum Erstellen von Eingabefeldern für alle Properties function buildJsonForm(data, prefix = '', level = 0, targetElement = null) { if (typeof data !== 'object' || data === null) return; const container = targetElement || document.getElementById('edit-content-content'); if (!container) return; Object.keys(data).forEach(key => { if (key.includes('__proto__') || key.includes('constructor') || key.includes('prototype')) { return; } const fullKey = prefix ? `${prefix}.${key}` : key; const value = data[key]; const isObject = typeof value === 'object' && value !== null && !Array.isArray(value); const isArray = Array.isArray(value); // Für Objekte: Rekursiv alle inneren Properties anzeigen if (isObject) { const fieldContainer = document.createElement('div'); fieldContainer.style.cssText = 'background: #1a1a1a; padding: 12px; border-radius: 4px; border-left: 3px solid #4CAF50;'; const label = document.createElement('label'); label.htmlFor = `edit-${fullKey.replace(/[.\s]/g, '-')}`; label.textContent = fullKey + ' (Objekt)'; label.style.cssText = 'font-weight: 600; color: #4CAF50; margin-bottom: 8px; display: block; font-size: 14px;'; fieldContainer.appendChild(label); // Rekursiv innere Properties in den Container einfügen const innerContainer = document.createElement('div'); innerContainer.style.cssText = 'padding-left: 12px; margin-top: 4px;'; buildJsonForm(value, fullKey, level + 1, innerContainer); fieldContainer.appendChild(innerContainer); container.appendChild(fieldContainer); return; } // Für Arrays: Rekursiv jedes Element als eigenes Feld anzeigen if (isArray) { const arrayContainer = document.createElement('div'); arrayContainer.style.cssText = 'background: #1a1a1a; padding: 12px; border-radius: 4px; border-left: 3px solid #2196F3;'; const arrayLabel = document.createElement('label'); arrayLabel.htmlFor = `edit-${fullKey.replace(/[.\s]/g, '-')}`; arrayLabel.textContent = fullKey + ' (Array) - ' + value.length + ' Elemente'; arrayLabel.style.cssText = 'font-weight: 600; color: #2196F3; margin-bottom: 8px; display: block; font-size: 14px;'; arrayContainer.appendChild(arrayLabel); // Für jedes Array-Element ein separates Eingabefeld const arrayItemsContainer = document.createElement('div'); arrayItemsContainer.style.cssText = 'margin-top: 4px; padding-left: 12px;'; value.forEach((item, index) => { const itemKey = `${fullKey}[${index}]`; const itemContainer = document.createElement('div'); itemContainer.style.cssText = 'background: #2a2a2a; padding: 8px; margin: 4px 0; border-radius: 3px; border-left: 2px solid #FF9800;'; const itemLabel = document.createElement('label'); itemLabel.htmlFor = `edit-${itemKey.replace(/[.\s]/g, '-')}`; itemLabel.textContent = `Element [${index}]`; itemLabel.style.cssText = 'font-weight: 500; color: #FF9800; margin-bottom: 4px; display: block; font-size: 13px;'; itemContainer.appendChild(itemLabel); // Prüfen, ob das Array-Element selbst ein Objekt ist if (typeof item === 'object' && item !== null) { const innerObjContainer = document.createElement('div'); innerObjContainer.style.cssText = 'padding-left: 12px; margin-top: 4px;'; buildJsonForm(item, itemKey, level + 1, innerObjContainer); itemContainer.appendChild(innerObjContainer); } else { const itemFieldContainer = document.createElement('div'); itemFieldContainer.style.cssText = 'background: var(--bg-input); padding: 8px; border-radius: 3px; margin-bottom: 4px;'; const input = document.createElement('input'); const type = typeof item === 'boolean' ? 'checkbox' : typeof item === 'number' ? 'number' : 'text'; input.type = type; input.value = item !== null && item !== undefined ? item : ''; input.dataset.key = itemKey; input.dataset.type = typeof item; input.dataset.arrayIndex = index; if (type === 'checkbox') input.checked = item; else input.value = String(item !== null && item !== undefined ? item : ''); input.style.cssText = 'width: 100%; padding: 8px; background: #222222; color: #ffffff; border: 1px solid #555; border-radius: 3px; font-family: var(--mono); font-size: 13px;'; itemFieldContainer.appendChild(input); itemContainer.appendChild(itemFieldContainer); } arrayItemsContainer.appendChild(itemContainer); }); arrayContainer.appendChild(arrayItemsContainer); container.appendChild(arrayContainer); return; } // Für primitive Werte: Standard-Eingabefeld erstellen const fieldContainer = document.createElement('div'); fieldContainer.style.cssText = 'background: var(--bg-input); padding: 8px; border-radius: 4px; border: 1px solid transparent;'; const label = document.createElement('label'); label.htmlFor = `edit-${fullKey.replace(/[.\s]/g, '-')}`; label.textContent = fullKey; label.style.cssText = 'font-weight: 600; color: var(--text-primary); display: block; margin-bottom: 4px; font-size: 14px; margin-top: 0;'; fieldContainer.appendChild(label); // Das 'template'-Feld als Textarea rendern const isTemplateField = (fullKey === 'template'); const type = typeof value === 'boolean' ? 'checkbox' : typeof value === 'number' ? 'number' : 'text'; if (isTemplateField) { const textarea = document.createElement('textarea'); textarea.id = `edit-${fullKey.replace(/[.\s]/g, '-')}`; textarea.value = value !== null && value !== undefined ? value : ''; textarea.dataset.key = fullKey; textarea.dataset.type = typeof value; textarea.style.cssText = 'width: 100%; min-height: 200px; padding: 8px; background: #222222; color: #ffffff; border: 1px solid #555; border-radius: 3px; font-family: var(--mono); font-size: 13px; resize: vertical;'; textarea.spellcheck = false; fieldContainer.appendChild(textarea); } else { const input = document.createElement('input'); input.type = type; input.value = value !== null && value !== undefined ? value : ''; input.dataset.key = fullKey; input.dataset.type = typeof value; if (type === 'checkbox') input.checked = value; else input.value = String(value !== null && value !== undefined ? value : ''); input.style.cssText = 'width: 100%; padding: 8px; background: #222222; color: #ffffff; border: 1px solid #555; border-radius: 3px; font-family: var(--mono); font-size: 13px;'; fieldContainer.appendChild(input); } container.appendChild(fieldContainer); }); } buildJsonForm(jsonData, '', 0, formDiv); container.appendChild(formDiv); } /** * Extrahiere den Wert aus einem Input-Element * @param {HTMLElement} input - Input-Element * @returns {*} Extrahierter Wert */ function extractInputValue(input) { if (input.type === 'checkbox') { return input.checked; } const value = input.value; try { // Nur wenn der Input-Typ "text" ist und Wert JSON-ähnlich formatiert if (input.type === 'text' && value.length > 0) { if ((value.startsWith('{') && value.endsWith('}')) || (value.startsWith('[') && value.endsWith(']'))) { try { return JSON.parse(value); } catch (parseErr) { // Kein gültiges JSON, behalte String-Wert } } } if (input.dataset.type === 'number') { return Number(value); } return value; } catch (e) { return value; } } /** * Extrahiere JSON aus dem Formular-Container * @param {HTMLElement} formDiv - Formular-Container * @returns {Object} Extrahiertes JSON-Objekt */ function extractJsonFromForm(formDiv) { // Map: full key -> { value, type } const inputEntries = []; const inputs = formDiv.querySelectorAll('[data-key]'); inputs.forEach(input => { const key = input.dataset.key || ''; inputEntries.push({ key, value: extractInputValue(input) }); }); // Sort by key to ensure stable parent-first construction inputEntries.sort((a,b) => a.key.localeCompare(b.key)); const result = Object.create(null); inputEntries.forEach(({ key, value }) => { const parts = key.split('.'); let cur = result; for (let i = 0; i < parts.length; i++) { const part = parts[i]; const isLast = i === parts.length - 1; const lastBracket = part.lastIndexOf('['); const lastCloseBracket = part.lastIndexOf(']'); let m = null; if (lastBracket !== -1 && lastCloseBracket !== -1 && lastCloseBracket > lastBracket) { const beforeBracket = part.substring(0, lastBracket); const between = part.substring(lastBracket + 1, lastCloseBracket); const afterBracket = part.substring(lastCloseBracket + 1); if (afterBracket === '' && /^\d+$/.test(between)) { m = [part, beforeBracket, between]; } } if (m && isLast) { const base = m[1]; const idx = Number(m[2]); if (!cur[base]) cur[base] = Object.create(null); cur[base][idx] = value; } else if (m && !isLast) { const base = m[1]; const idx = Number(m[2]); if (!cur[base]) cur[base] = []; if (!cur[base][idx]) cur[base][idx] = Object.create(null); cur = cur[base][idx]; } else if (isLast) { cur[part] = value; } else { if (!cur[part]) cur[part] = Object.create(null); cur = cur[part]; } } }); return result; } /** * LLM-API-Konfiguration (llama.cpp / Kilo-Instanz) */ const LLM_BASE = 'http://localhost:8001/v1'; const LLM_MODEL = 'Qwen3.6-35B-A3B-UD-Q3_K_S.gguf'; /** * Zustand für den Tunen-Dialog */ window._tuneState = { path: '', rawPath: '', template: '' }; /** * Öffne Tune-Modal für eine Template-Datei * @param {string} path - Pfad zur Template-Datei */ function tuneModalContent(path) { window._tuneState.path = path; window._tuneState.rawPath = ''; window._tuneState.template = ''; window._tuneState.generated = ''; const title = path.split('/').pop(); document.getElementById('tune-title').textContent = `Prompt tunen: ${title}`; // Generate-Button aktivieren, Apply-Button verstecken document.getElementById('tune-generate-btn').style.display = ''; document.getElementById('tune-apply-btn').style.display = 'none'; document.getElementById('tune-generate-btn').disabled = false; document.getElementById('tune-generate-btn').innerHTML = ` Generieren`; const container = document.getElementById('tune-content'); container.innerHTML = '
Lade Template...
'; document.getElementById('tune-modal').classList.add('active'); fetch(path) .then(r => r.json()) .then(data => { window._tuneState.template = data.template || ''; renderTuneUI(window._tuneState); }) .catch(e => { container.innerHTML = `
✗ Fehler: ${e.message}
`; }); } /** * Render die Tune-UI mit Original-Template und Preprompt * @param {Object} state - Tune-Zustand */ function renderTuneUI(state) { const container = document.getElementById('tune-content'); container.innerHTML = ''; // Original-Template const originalPanel = document.createElement('div'); originalPanel.style.cssText = 'display:flex;flex-direction:column;gap:6px;'; const origLabel = document.createElement('label'); origLabel.style.cssText = 'font-weight:600;font-size:13px;color:var(--text-secondary);'; origLabel.textContent = 'Original-Template'; originalPanel.appendChild(origLabel); const origTextarea = document.createElement('textarea'); origTextarea.id = 'tune-original'; origTextarea.value = state.template; origTextarea.readOnly = true; origTextarea.style.cssText = 'width:100%;min-height:200px;padding:10px;background:var(--bg-input);color:var(--text-primary);border:1px solid var(--border);border-radius:4px;font-family:var(--mono);font-size:13px;resize:vertical;'; originalPanel.appendChild(origTextarea); container.appendChild(originalPanel); // Preprompt-Feld const prepromptPanel = document.createElement('div'); prepromptPanel.style.cssText = 'display:flex;flex-direction:column;gap:6px;'; const prepromptLabel = document.createElement('label'); prepromptLabel.style.cssText = 'font-weight:600;font-size:13px;color:var(--text-secondary);'; prepromptLabel.textContent = 'Preprompt (deine Wünsche zur Verbesserung)'; prepromptPanel.appendChild(prepromptLabel); const prepromptTextarea = document.createElement('textarea'); prepromptTextarea.id = 'tune-preprompt'; prepromptTextarea.style.cssText = 'width:100%;min-height:80px;padding:10px;background:var(--bg-input);color:var(--text-primary);border:1px solid var(--border);border-radius:4px;font-family:var(--mono);font-size:13px;resize:vertical;'; prepromptTextarea.placeholder = 'z.B. Mach das Template kürzer, füge Security-Checks hinzu, verwende einen professionelleren Ton...'; prepromptPanel.appendChild(prepromptTextarea); container.appendChild(prepromptPanel); // Ergebnis-Panel (initial versteckt) const resultPanel = document.createElement('div'); resultPanel.id = 'tune-result-panel'; resultPanel.style.cssText = 'display:none;flex-direction:column;gap:6px;'; const resultLabel = document.createElement('label'); resultLabel.style.cssText = 'font-weight:600;font-size:13px;color:var(--text-secondary);'; resultLabel.textContent = 'Generiertes Template'; resultPanel.appendChild(resultLabel); const resultTextarea = document.createElement('textarea'); resultTextarea.id = 'tune-result'; resultTextarea.readOnly = true; resultTextarea.style.cssText = 'width:100%;min-height:200px;padding:10px;background:var(--bg-input);color:var(--text-primary);border:1px solid var(--border);border-radius:4px;font-family:var(--mono);font-size:13px;resize:vertical;'; resultPanel.appendChild(resultTextarea); container.appendChild(resultPanel); } /** * Sende Preprompt + Template an LLM und zeige Ergebnis */ async function generateTunedTemplate() { const preprompt = document.getElementById('tune-preprompt').value.trim(); const original = document.getElementById('tune-original').value; if (!preprompt) { showToast('✗ Bitte Preprompt eingeben'); return; } const btn = document.getElementById('tune-generate-btn'); btn.disabled = true; btn.innerHTML = '⏳ Generiere...'; const resultPanel = document.getElementById('tune-result-panel'); const resultTextarea = document.getElementById('tune-result'); resultPanel.style.display = 'flex'; resultTextarea.value = 'Warte auf Generierung...'; const systemPrompt = 'Du bist ein Experte für Prompt-Engineering. Du erhältst ein vorhandenes Template und Verbesserungswünsche. Gib NUR das verbesserte Template zurück. Keine Erklärungen, keine Markdown-Codeblöcke, keine Zitatzeichen. Nur den reinen Template-Text.'; const userPrompt = `Verbessere dieses Template basierend auf den folgenden Anweisungen: === Vorhandenes Template === ${original} === Verbesserungswünsche === ${preprompt} Gib das verbesserte Template zurück.`; try { const response = await fetch(`${LLM_BASE}/chat/completions`, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ model: LLM_MODEL, messages: [ { role: 'system', content: systemPrompt }, { role: 'user', content: userPrompt } ], stream: false, temperature: 0.7, max_tokens: 2048 }) }); if (!response.ok) throw new Error(`HTTP ${response.status}`); const data = await response.json(); let generated = data.choices?.[0]?.message?.content || ''; // Bereinige Markdown-Codeblöcke falls vorhanden generated = generated.replace(/^```(?:markdown|text)?\s*\n?/i, '').replace(/\n```$/, ''); generated = generated.trim(); window._tuneState.generated = generated; resultTextarea.value = generated || '(Leer - nothing generated)'; // Ergebnis-Panel hervorheben und einblenden resultPanel.style.border = '1px solid var(--accent)'; resultPanel.style.transition = 'border-color 0.3s'; document.getElementById('tune-apply-btn').style.display = ''; resultPanel.scrollIntoView({ behavior: 'smooth', block: 'center' }); showToast('✓ Template generiert'); } catch (e) { resultTextarea.value = `Fehler: ${e.message}\n\nStelle sicher, dass der llama.cpp Server auf ${LLM_BASE} läuft.`; showToast(`✗ ${e.message}`); } finally { btn.disabled = false; btn.innerHTML = ` Generieren`; } } /** * Generiertes Template übernehmen (ersetzt template-Feld im JSON) */ async function applyTunedContent() { const state = window._tuneState; const generated = document.getElementById('tune-result').value; if (!generated || generated.startsWith('Fehler:') || generated.startsWith('Leer')) { showToast('✗ Kein gültiges Ergebnis zum Übernehmen'); return; } try { const response = await fetch(state.path); if (!response.ok) throw new Error('Konnte Template nicht laden'); const data = await response.json(); data.template = generated; const saveResponse = await fetch(state.path, { method: 'PUT', headers: {'Content-Type': 'text/plain'}, body: JSON.stringify(data, null, 2) }); if (saveResponse.ok) { showToast('✓ Template übernommen und gespeichert'); closeTuneModal(); } else { throw new Error(`HTTP ${saveResponse.status}`); } } catch (e) { showToast(`✗ Fehler beim Speichern: ${e.message}`); } } /** * Schließe Tune-Modal */ function closeTuneModal() { window._lastFocusedElement = document.activeElement; var modal = document.getElementById('tune-modal'); modal.classList.remove('active'); _setAriaHidden('tune-modal', true); if (window._lastFocusedElement && window._lastFocusedElement.focus) { window._lastFocusedElement.focus(); } window._tuneState = { path: '', rawPath: '', template: '' }; } // Export for api.js (global scope, loaded before api.js) // editModalContent, createTextEditUI, createJsonEditUI, extractInputValue, extractJsonFromForm // sind als globale Funktionen verfügbar