Files

114 lines
20 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
const $ = id => document.getElementById(id);
let prompts = [], current = null, view = 'all', category = '', revision = null, searchSerial = 0, toastTimer, dirty = false, organization = null, editorGeneration = 0;
const esc = text => String(text ?? '').replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
const date = value => new Intl.DateTimeFormat('de', {day:'2-digit',month:'short',year:'numeric'}).format(new Date(value));
function toast(message) { $('toast').textContent = message; $('toast').classList.remove('hidden'); clearTimeout(toastTimer); toastTimer = setTimeout(()=>$('toast').classList.add('hidden'),6500); }
async function api(path, options={}) {
const response = await fetch('/api'+path,{...options,headers:{'Content-Type':'application/json',...options.headers}});
let data; try { data=await response.json(); } catch { throw new Error('Unerwartete Serverantwort.'); }
if (!response.ok) {
if(response.status===401) { document.querySelectorAll('dialog[open]').forEach(d=>d.close()); $('shell').classList.add('hidden');$('login').classList.remove('hidden'); }
throw new Error(typeof data.detail==='string' ? data.detail : 'Eingaben prüfen: Ein Feld ist ungültig oder zu lang.');
}
return data;
}
async function busy(button, operation) { const text=button.textContent;button.disabled=true;button.textContent=button.id==='improve'?'Erstellen & prüfen …':'Einen Moment …';try{return await operation();}catch(e){toast(e.message);}finally{button.disabled=false;button.textContent=text;} }
function bind(id,event,handler){$(id).addEventListener(event, async e=>{try{await handler(e);}catch(err){toast(err.message);}});}
async function load() {
const serial=++searchSerial;
const result=await api('/prompts?'+new URLSearchParams({q:$('search').value,mode:$('search-mode').value,trash:view==='trash'}));
if(serial!==searchSerial)return;
prompts=result.items;
$('search-warning').textContent=result.warning||'';$('search-warning').classList.toggle('hidden',!result.warning);
$('search-status').textContent=({all:'Deine persönliche Sammlung',hybrid:'Text + Bedeutung · nach Relevanz',semantic:'Nach Ähnlichkeit · Relevanz prüfen',text:'Textsuche',trash:'Gelöschte Prompts'})[result.mode];
render();
if(!$('search').value && view!=='trash') renderCategories();
}
function renderCategories(){
$('all-count').textContent=prompts.length;
const categories=[...new Set(prompts.map(p=>p.category).filter(Boolean))].sort();
$('categories').replaceChildren();$('category-list').replaceChildren();
categories.forEach(name=>{const b=document.createElement('button');b.className='category-button'+(category===name?' active':'');b.innerHTML='<i class="category-dot"></i>'+esc(name);b.onclick=()=>{category=category===name?'':name;view='all';syncView();load().catch(e=>toast(e.message));};$('categories').append(b);const o=document.createElement('option');o.value=name;$('category-list').append(o);});
}
function syncView(){document.querySelectorAll('[data-view]').forEach(b=>b.classList.toggle('active',b.dataset.view===view&&!category));$('view-title').textContent=category||({all:'Deine besten Worte.',favorites:'Die bleiben hängen.',trash:'Noch nicht verloren.'})[view];$('view-subtitle').textContent=view==='trash'?'Hier kannst du gelöschte Prompts wiederherstellen.':'Sammeln, verfeinern und genau dann wiederfinden, wenn du sie brauchst.';}
function render(){
let items=prompts.filter(p=>(view!=='favorites'||p.favorite)&&(!category||p.category===category));
if($('sort').value==='title')items.sort((a,b)=>a.title.localeCompare(b.title,'de'));else if(!$('search').value)items.sort((a,b)=>b.updated.localeCompare(a.updated));
$('result-count').textContent=items.length+' Prompt'+(items.length===1?'':'s');$('cards').replaceChildren();
$('empty').classList.toggle('hidden',items.length>0);
if(!items.length){$('empty').querySelector('h2').textContent=$('search').value?'Hier passt noch nichts.':view==='trash'?'Alles an seinem Platz.':'Platz für deine nächste gute Idee.';$('empty').querySelector('p').textContent=$('search').value?'Versuche eine andere Beschreibung oder einen anderen Suchmodus.':view==='trash'?'Dein Papierkorb ist leer.':'Lege deinen ersten Prompt an. Du kannst ihn später mit deinem Modell verfeinern und nach Bedeutung finden.';$('empty-new').classList.toggle('hidden',view==='trash'||!!$('search').value);}
items.forEach(p=>{
const card=document.createElement('article');card.className='card';card.tabIndex=0;card.setAttribute('aria-label',p.title+' öffnen');
card.innerHTML=`<div class="card-top"><span class="card-category"><i class="category-dot"></i>${esc(p.category||'Freie Gedanken')}</span><button class="star ${p.favorite?'on':''}" aria-label="Favorit umschalten">${p.favorite?'★':'☆'}</button></div><h3>${esc(p.title)}</h3><p>${esc(p.description||p.body.slice(0,180))}</p><div class="tags">${p.tags.slice(0,4).map(t=>`<span class="tag">${esc(t)}</span>`).join('')}</div><div class="card-footer"><span>v${p.version} · ${esc(date(p.updated))}</span><div class="card-actions"><button data-action="copy" title="Prompt kopieren">Kopieren</button><button data-action="duplicate" title="Duplizieren">⧉</button><button data-action="trash" title="${view==='trash'?'Wiederherstellen':'In Papierkorb'}">${view==='trash'?'↶':'⌫'}</button></div></div>`;
card.addEventListener('click',async e=>{try{
const button=e.target.closest('button');if(!button){if(view==='trash')return;await openEditor(p);return;}
if(button.classList.contains('star')){const result=await api('/prompts/'+p.id,{method:'PUT',body:JSON.stringify({...p,favorite:!p.favorite,note:'Favorit geändert'})});if(result.warning)toast(result.warning);await load();}
else if(button.dataset.action==='copy')await copy(p.body);
else if(button.dataset.action==='duplicate'){await openEditor({...p,id:null,title:p.title+' (Kopie)',version:null});}
else {await api('/prompts/'+p.id+'/trash?deleted='+(view!=='trash'),{method:'POST'});toast(view==='trash'?'Prompt wiederhergestellt.':'In den Papierkorb verschoben.');await load();}
}catch(err){toast(err.message);}});
card.addEventListener('keydown',e=>{if(e.target===card&&(e.key==='Enter'||e.key===' ')){e.preventDefault();if(view!=='trash')openEditor(p).catch(err=>toast(err.message));}});$('cards').append(card);
});
}
async function copy(text){if(navigator.clipboard&&window.isSecureContext){await navigator.clipboard.writeText(text);}else{const t=document.createElement('textarea');t.value=text;(document.querySelector('dialog[open]')||document.body).append(t);t.select();const ok=document.execCommand('copy');t.remove();if(!ok)throw new Error('Kopieren nicht möglich. Bitte den Text manuell auswählen.');}toast('In die Zwischenablage kopiert.');}
function fillPrompt(p){$('p-title').value=p.title||'';$('p-description').value=p.description||'';$('p-body').value=p.body||'';$('p-category').value=p.category||'';$('p-tags').value=(p.tags||[]).join(', ');$('p-favorite').checked=!!p.favorite;updateChars();}
function updateChars(){$('char-count').textContent=$('p-body').value.length.toLocaleString('de')+' Zeichen';}
async function openEditor(p=null){current=p;editorGeneration++;dirty=false;organization=null;$('organize-result').classList.add('hidden');fillPrompt(p||{});$('p-note').value='';$('ai-result').classList.add('hidden');$('ai-draft').value='';$('editor-heading').textContent=p?.id?'Prompt bearbeiten':'Neuer Prompt';$('current-version').textContent=p?.id?'Aktuell: v'+p.version:'Entwurf';$('history').innerHTML='<p class="muted">Deine erste Version beginnt hier.</p>';$('editor').showModal();if(p?.id){const history=await api('/prompts/'+p.id+'/versions');$('history').replaceChildren();history.forEach(v=>{const b=document.createElement('button');b.type='button';b.className='version-button';b.innerHTML=`<strong>Version ${v.version} · ${esc(v.note)}</strong><span>${esc(date(v.created))} ↗ ansehen</span>`;b.onclick=()=>{revision=v;$('revision-title').textContent='Version '+v.version+' · '+v.data.title;$('revision-description').textContent=v.data.description;$('revision-body').textContent=v.data.body;$('revision').showModal();};$('history').append(b);});}}
async function saveSettings(){const data={};['base_url','chat_model','embedding_url','embedding_model'].forEach(k=>data[k]=$('s-'+k).value.trim());for(const [key,clear]of [['api_key','clear-api-key'],['embedding_key','clear-embedding-key']]){data[key]=$(clear).checked?'':$('s-'+key).value||null;}const result=await api('/settings',{method:'PUT',body:JSON.stringify(data)});$('s-api_key').value='';$('s-embedding_key').value='';$('clear-api-key').checked=false;$('clear-embedding-key').checked=false;setKeyHints(result);$('index-status').textContent=`${result.indexed} von ${result.total} Prompts indexiert`;return result;}
function setKeyHints(s){$('s-api_key').placeholder=s.api_key_set?'Schlüssel gespeichert · leer lassen zum Beibehalten':'Optional, wenn dein Endpoint keinen Key braucht';$('s-embedding_key').placeholder=s.embedding_key_set?'Schlüssel gespeichert · leer lassen zum Beibehalten':'Optional für den separaten Endpoint';}
async function openSettings(){const s=await api('/settings');['base_url','chat_model','embedding_url','embedding_model'].forEach(k=>$('s-'+k).value=s[k]||'');$('s-api_key').value='';$('s-embedding_key').value='';setKeyHints(s);$('index-status').textContent=`${s.indexed} von ${s.total} Prompts indexiert`;$('mcp-url').value=location.origin+'/mcp/';$('mcp-config').classList.add('hidden');$('mcp-config').textContent='';$('settings').showModal();}
async function scan(embedding){await saveSettings();const {models}=await api('/models?embedding='+embedding);const list=$(embedding?'embedding-models':'chat-models');list.replaceChildren(...models.map(id=>{const o=document.createElement('option');o.value=id;return o;}));toast(models.length+' Modelle gefunden. Im Modellfeld auswählen.');$(embedding?'s-embedding_model':'s-chat_model').focus();}
async function indexAll(rebuild){await saveSettings();$('index-fill').disabled=true;$('index-rebuild').disabled=true;try{let result;do{result=await api('/index?rebuild='+rebuild,{method:'POST'});rebuild=false;$('index-status').textContent=`${result.indexed} von ${result.total} Prompts indexiert`;}while(result.remaining>0);toast('Bedeutungsindex ist aktuell.');}finally{$('index-fill').disabled=false;$('index-rebuild').disabled=false;}}
$('login-form').onsubmit=async e=>{e.preventDefault();$('login-error').textContent='';try{await api('/login',{method:'POST',body:JSON.stringify({token:$('login-token').value})});$('login-token').value='';$('login').classList.add('hidden');$('shell').classList.remove('hidden');await load();}catch(err){$('login-error').textContent=err.message;}};
document.querySelectorAll('[data-close]').forEach(b=>b.onclick=()=>{if(b.dataset.close==='editor'&&dirty&&!confirm('Ungespeicherte Änderungen verwerfen?'))return;$(b.dataset.close).close();});
$('editor').addEventListener('cancel',e=>{if(dirty&&!confirm('Ungespeicherte Änderungen verwerfen?'))e.preventDefault();});
$('editor-form').addEventListener('input',()=>{dirty=true;});
bind('editor-form','submit',async e=>{e.preventDefault();await busy($('save-prompt'),async()=>{const data={title:$('p-title').value,description:$('p-description').value,body:$('p-body').value,category:$('p-category').value,tags:$('p-tags').value.split(',').map(t=>t.trim()).filter(Boolean),favorite:$('p-favorite').checked,version:current?.version,note:$('p-note').value||'Gespeichert'};const result=await api('/prompts'+(current?.id?'/'+current.id:''),{method:current?.id?'PUT':'POST',body:JSON.stringify(data)});dirty=false;$('editor').close();toast(result.warning||'Prompt als Version '+result.prompt.version+' gespeichert.');await load();});});
for(const id of ['new-prompt','empty-new'])bind(id,'click',()=>openEditor());
bind('settings-open','click',openSettings);
bind('logout','click',async()=>{await api('/logout',{method:'POST'});location.reload();});
bind('p-body','input',updateChars);bind('copy-body','click',()=>copy($('p-body').value));
bind('improve','click',()=>busy($('improve'),async()=>{if(!$('p-body').value.trim())throw new Error('Schreibe zuerst einen Prompt.');const generation=editorGeneration;const originalBody=$('p-body').value;const originalInstruction=$('ai-instruction').value;const result=await api('/improve',{method:'POST',body:JSON.stringify({body:$('p-body').value,instruction:$('ai-instruction').value})});if(generation!==editorGeneration||!$('editor').open)return;$('ai-draft').value=result.body;renderReview(result.review);if(originalBody!==$('p-body').value||originalInstruction!==$('ai-instruction').value)renderReview({status:'unchecked',warning:'Original oder Überarbeitungswunsch wurde während der Prüfung geändert. Bitte erneut verfeinern.'});$('ai-result').classList.remove('hidden');}));
bind('accept-ai','click',()=>{$('p-body').value=$('ai-draft').value;dirty=true;$('p-note').value='KI-Vorschlag übernommen';updateChars();$('ai-result').classList.add('hidden');toast('Vorschlag im Editor. Zum Übernehmen noch speichern.');});
bind('restore-version','click',()=>{fillPrompt(revision.data);$('p-note').value='Wiederhergestellt aus Version '+revision.version;dirty=true;$('revision').close();toast('Version im Editor. Speichern legt eine neue Version an.');});
bind('settings-form','submit',async e=>{e.preventDefault();await busy(e.submitter,async()=>{await saveSettings();toast('Einstellungen gespeichert.');});});
bind('scan-chat','click',()=>busy($('scan-chat'),()=>scan(false)));bind('scan-embedding','click',()=>busy($('scan-embedding'),()=>scan(true)));
bind('index-fill','click',()=>indexAll(false));bind('index-rebuild','click',()=>indexAll(true));
bind('mcp-reveal','click',async()=>{const data=await api('/mcp');$('mcp-config').textContent=JSON.stringify({url:location.origin+data.path,transport:'streamable-http',headers:{Authorization:'Bearer '+data.token}},null,2);$('mcp-config').classList.remove('hidden');});
bind('import-button','click',()=>$('import-file').click());bind('import-file','change',async()=>{const file=$('import-file').files[0];if(!file)return;try{if(file.size>20000000)throw new Error('Maximal 20 MB pro Archiv.');const result=await api('/import',{method:'POST',body:await file.text()});toast(result.imported+' Prompts importiert. Bitte Bedeutungsindex ergänzen.');await load();await openSettingsRefresh();}finally{$('import-file').value='';}});
async function openSettingsRefresh(){const s=await api('/settings');$('index-status').textContent=`${s.indexed} von ${s.total} Prompts indexiert`;}
document.querySelectorAll('[data-view]').forEach(b=>b.onclick=()=>{view=b.dataset.view;category='';syncView();load().catch(e=>toast(e.message));});
let debounce;bind('search','input',()=>{clearTimeout(debounce);debounce=setTimeout(()=>load().catch(e=>toast(e.message)),450);});bind('search-mode','change',load);bind('sort','change',render);
document.addEventListener('keydown',e=>{if((e.metaKey||e.ctrlKey)&&e.key==='k'){e.preventDefault();$('search').focus();}if((e.metaKey||e.ctrlKey)&&e.key==='s'&&$('editor').open){e.preventDefault();$('editor-form').requestSubmit();}});
window.addEventListener('beforeunload',e=>{if(dirty&&$('editor').open){e.preventDefault();e.returnValue='';}});
(async()=>{try{await load();$('shell').classList.remove('hidden');}catch(e){$('login').classList.remove('hidden');if(!e.message.includes('anmelden'))toast(e.message);}})();
bind('organize','click',()=>busy($('organize'),async()=>{if(!$('p-body').value.trim())throw new Error('Schreibe zuerst einen Prompt.');const generation=editorGeneration;const originalBody=$('p-body').value;const suggestion=await api('/organize',{method:'POST',body:JSON.stringify({body:$('p-body').value})});if(generation!==editorGeneration||!$('editor').open)return;if(originalBody!==$('p-body').value)throw new Error('Der Prompt wurde inzwischen geändert. Bitte Metadaten erneut erzeugen.');organization={...suggestion,sourceBody:originalBody};$('organize-preview').textContent=organization.title+' — '+organization.category+' · '+organization.tags.join(', ')+' — '+organization.description;$('organize-result').classList.remove('hidden');}));
bind('accept-organize','click',()=>{if(!organization)return;if(organization.sourceBody!==$('p-body').value){organization=null;$('organize-result').classList.add('hidden');toast('Der Prompt wurde geändert. Bitte Metadaten erneut erzeugen.');return;}$('p-title').value=organization.title;$('p-category').value=organization.category;$('p-tags').value=organization.tags.join(', ');$('p-description').value=organization.description;dirty=true;$('organize-result').classList.add('hidden');toast('Titel, Kurzbeschreibung, Kategorie und Tags übernommen. Zum Behalten speichern.');});
function renderReview(report) {
const box=$('ai-review');box.replaceChildren();
const titles={passed:'KI-Selbstprüfung: Keine Abweichungen erkannt',corrected:'Korrigierter Entwurf: KI-Selbstprüfung abgeschlossen',issues:'KI-Selbstprüfung: Abweichungen im aktuellen Vorschlag',unchecked:report?.draft_kind==='corrected'?'Korrigierter Entwurf – Abschlussprüfung fehlgeschlagen':'Aktueller Entwurf – KI-Selbstprüfung nicht abgeschlossen'};
const heading=document.createElement('strong');heading.textContent=titles[report?.status]||titles.unchecked;box.append(heading);
if(report?.warning){const p=document.createElement('p');p.textContent=report.warning;box.append(p);}
function issues(label, entries){if(!entries?.length)return;const details=document.createElement('details');details.open=true;const summary=document.createElement('summary');summary.textContent=label+' ('+entries.length+')';details.append(summary);const ul=document.createElement('ul');for(const issue of entries){const li=document.createElement('li');li.textContent=issue.description;for(const [key,title]of [['original_quote','Original'],['draft_quote','Vorschlag']]){if(issue[key]){const p=document.createElement('p');p.textContent=title+': „'+issue[key]+'“';li.append(p);}}ul.append(li);}details.append(ul);box.append(details);}
if(report?.draft_kind==='corrected')issues('Historische Befunde zum ERSTEN Entwurf (vor der Korrektur)',report?.initial_issues);
else if(report?.status!=='issues')issues('Befunde zum angezeigten Entwurf',report?.initial_issues);
issues('Code-Prüfung: Unterschiede im aktuellen Vorschlag – bitte beurteilen',report?.literal_issues);
if(report?.status==='issues')issues('Befunde zum AKTUELLEN Vorschlag',report.issues);
const hint=document.createElement('p');hint.textContent='Selbstprüfung durch dasselbe Modell, keine unabhängige Verifikation. Die Code-Prüfung sucht nur erkennbare Platzhalter, Ports und Pfade; gewünschte Änderungen können ebenfalls markiert werden. Bitte selbst vergleichen.';box.append(hint);
}
bind('ai-draft','input',()=>renderReview({status:'unchecked',warning:'Vorschlag manuell geändert; diese Fassung wurde nicht geprüft.'}));
bind('recheck-ai','click',()=>busy($('recheck-ai'),async()=>{
const generation=editorGeneration;
const original=$('p-body').value, instruction=$('ai-instruction').value, draft=$('ai-draft').value;
if(!original.trim()||!draft.trim())throw new Error('Original und Vorschlag dürfen nicht leer sein.');
const result=await api('/recheck',{method:'POST',body:JSON.stringify({body:original,instruction,draft})});
if(generation!==editorGeneration||!$('editor').open)return;
if(original!==$('p-body').value||instruction!==$('ai-instruction').value||draft!==$('ai-draft').value){renderReview({status:'unchecked',warning:'Text während der Prüfung geändert. Bitte erneut prüfen.'});return;}
renderReview(result.review);
}));
for(const id of ['p-body','ai-instruction'])bind(id,'input',()=>{if(!$('ai-result').classList.contains('hidden'))renderReview({status:'unchecked',warning:'Vergleichsgrundlage geändert. Bitte erneut prüfen.'});});