feat: Prompt-Tunen-Dialog mit LLM-Generierung (llama.cpp)
This commit is contained in:
parent
c106384c4e
commit
9910d48bd6
3 changed files with 256 additions and 0 deletions
|
|
@ -49,6 +49,32 @@
|
||||||
<!-- Toast -->
|
<!-- Toast -->
|
||||||
<div class="toast" id="toast"></div>
|
<div class="toast" id="toast"></div>
|
||||||
|
|
||||||
|
<!-- Tune Modal -->
|
||||||
|
<div class="modal-overlay" id="tune-modal" role="dialog" aria-label="Prompt tunen">
|
||||||
|
<div class="modal" style="max-width: 1200px;">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2 id="tune-title">Prompt tunen</h2>
|
||||||
|
<button class="modal-close" onclick="closeTuneModal()">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body" id="tune-content" style="display: flex; flex-direction: column; gap: 16px; min-height: 400px; overflow-y: auto;"></div>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button class="btn btn-primary" id="tune-generate-btn" onclick="generateTunedTemplate()">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M12 2v4m0 12v4m8-8h-4M6 12H2m14.5-7.5L16 5.5m-8 8L5.5 16m13-10.5L16 8m-8 8L5.5 16"/>
|
||||||
|
</svg>
|
||||||
|
Generieren
|
||||||
|
</button>
|
||||||
|
<button class="btn" id="tune-apply-btn" onclick="applyTunedContent()" style="display:none;">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M20 6L9 17l-5-5"/>
|
||||||
|
</svg>
|
||||||
|
Übernehmen
|
||||||
|
</button>
|
||||||
|
<button class="btn" onclick="closeTuneModal()">Schließen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<h1>Prompt Templates</h1>
|
<h1>Prompt Templates</h1>
|
||||||
<span class="badge">Git Managed</span>
|
<span class="badge">Git Managed</span>
|
||||||
|
|
|
||||||
229
web/js/editor.js
229
web/js/editor.js
|
|
@ -295,6 +295,235 @@ function extractJsonFromForm(formDiv) {
|
||||||
return result;
|
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 = `
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M12 2v4m0 12v4m8-8h-4M6 12H2m14.5-7.5L16 5.5m-8 8L5.5 16m13-10.5L16 8m-8 8L5.5 16"/>
|
||||||
|
</svg>
|
||||||
|
Generieren`;
|
||||||
|
|
||||||
|
const container = document.getElementById('tune-content');
|
||||||
|
container.innerHTML = '<div style="text-align:center;padding:40px;color:var(--text-muted)">Lade Template...</div>';
|
||||||
|
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 = `<div style="color:var(--text-error);text-align:center;padding:20px">✗ Fehler: ${e.message}</div>`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 = `
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M12 2v4m0 12v4m8-8h-4M6 12H2m14.5-7.5L16 5.5m-8 8L5.5 16m13-10.5L16 8m-8 8L5.5 16"/>
|
||||||
|
</svg>
|
||||||
|
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() {
|
||||||
|
document.getElementById('tune-modal').classList.remove('active');
|
||||||
|
window._tuneState = { path: '', rawPath: '', template: '' };
|
||||||
|
}
|
||||||
|
|
||||||
// Export for api.js (global scope, loaded before api.js)
|
// Export for api.js (global scope, loaded before api.js)
|
||||||
// editModalContent, createTextEditUI, createJsonEditUI, extractInputValue, extractJsonFromForm
|
// editModalContent, createTextEditUI, createJsonEditUI, extractInputValue, extractJsonFromForm
|
||||||
// sind als globale Funktionen verfügbar
|
// sind als globale Funktionen verfügbar
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ function renderTemplates(templates) {
|
||||||
<button class="btn btn-icon" onclick="viewTemplate('${esc(t.path)}')">Anzeigen</button>
|
<button class="btn btn-icon" onclick="viewTemplate('${esc(t.path)}')">Anzeigen</button>
|
||||||
<button class="btn btn-icon" onclick="editModalContent('${esc(t.path)}')">📝 Bearbeiten</button>
|
<button class="btn btn-icon" onclick="editModalContent('${esc(t.path)}')">📝 Bearbeiten</button>
|
||||||
<button class="btn btn-icon" onclick="copyContent('${esc(t.path)}')">Inhalt kopieren</button>
|
<button class="btn btn-icon" onclick="copyContent('${esc(t.path)}')">Inhalt kopieren</button>
|
||||||
|
<button class="btn btn-icon" onclick="tuneModalContent('${esc(t.path)}')">🎯 Tunen</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`).join('');
|
`).join('');
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue