Files
CasaDePrompt/atelier/static/app.js
T

101 lines
18 KiB
JavaScript
Raw 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 suggestion=await api('/organize',{method:'POST',body:JSON.stringify({body:$('p-body').value})});if(generation!==editorGeneration||!$('editor').open)return;organization=suggestion;$('organize-preview').textContent=organization.category+' · '+organization.tags.join(', ')+' — '+organization.description;$('organize-result').classList.remove('hidden');}));
bind('accept-organize','click',()=>{if(!organization)return;$('p-category').value=organization.category;$('p-tags').value=organization.tags.join(', ');$('p-description').value=organization.description;dirty=true;$('organize-result').classList.add('hidden');toast('Einordnung im Entwurf übernommen. Zum Behalten speichern.');});
function renderReview(report) {
const box=$('ai-review');box.replaceChildren();
const titles={passed:'Prüfung: Keine Abweichungen erkannt',corrected:'Prüfung: Abweichungen korrigiert; erneut geprüft',issues:'Prüfung: Abweichungen verbleiben',unchecked:'Nicht geprüft – keine abgeschlossene Verifikation'};
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);}
issues('Befunde der ersten Prüfung',report?.initial_issues);
if(report?.status==='issues')issues('Weiterhin erkannte Abweichungen',report.issues);
const hint=document.createElement('p');hint.textContent='Modellprüfung, keine Garantie. Bitte den Vorschlag vor dem Speichern selbst prüfen.';box.append(hint);
}
bind('ai-draft','input',()=>renderReview({status:'unchecked',warning:'Vorschlag manuell geändert; diese Fassung wurde nicht geprüft.'}));