feat(diarization-ui): add inline document editor

Adds an edit mode to the document view: click "Bearbeiten" to switch
from rendered Markdown to a monospace textarea, save via POST
/document/{id}/edit, or cancel to discard changes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-27 10:08:11 +01:00
parent bdff373959
commit 5bed920266

54
app.py
View File

@@ -754,22 +754,55 @@ def view_document(doc_id: int):
rendered = md.markdown(d["content_md"] or "", extensions=["fenced_code", "tables", "nl2br"]) rendered = md.markdown(d["content_md"] or "", extensions=["fenced_code", "tables", "nl2br"])
projects = get_projects() projects = get_projects()
content_escaped = (d['content_md'] or '').replace('`', '&#96;').replace('</script>', '<\\/script>')
body = f""" body = f"""
<div class='d-flex justify-content-between align-items-start flex-wrap gap-2 mb-2'> <div class='d-flex justify-content-between align-items-start flex-wrap gap-2 mb-2'>
<div> <div>
<h2 class='h4 mb-1'>Dokument #{d['id']} {d['title']}</h2> <h2 class='h4 mb-1'>Dokument #{d['id']} {d['title']}</h2>
<div class='text-secondary small'>Projekt: {d['project']} · Typ: {d['kind']} · {d['created_at']}</div> <div class='text-secondary small'>Projekt: {d['project']} · Typ: {d['kind']} · {d['created_at']}</div>
</div> </div>
<div class='btn-group btn-group-sm'> <div class='d-flex gap-2 flex-wrap'>
<a class='btn btn-outline-secondary' title='Download .md' href='/document/{doc_id}/download.md'>⬇️</a> <div class='btn-group btn-group-sm' id='view-actions'>
<a class='btn btn-outline-secondary' title='Umbenennen' href='#' onclick='renameDoc();return false;'>✏️</a> <a class='btn btn-outline-secondary' title='Download .md' href='/document/{doc_id}/download.md'><i class='bi bi-download'></i></a>
<a class='btn btn-outline-secondary' title='Verschieben' href='#' onclick='moveDoc();return false;'>📁</a> <button class='btn btn-outline-primary' title='Bearbeiten' onclick='startEdit()'><i class='bi bi-pencil'></i> Bearbeiten</button>
<a class='btn btn-outline-danger' title='Löschen' href='#' onclick='deleteDoc();return false;'>🗑️</a> <a class='btn btn-outline-secondary' title='Umbenennen' href='#' onclick='renameDoc();return false;'><i class='bi bi-fonts'></i></a>
<a class='btn btn-outline-secondary' title='Verschieben' href='#' onclick='moveDoc();return false;'><i class='bi bi-folder-symlink'></i></a>
<a class='btn btn-outline-danger' title='Löschen' href='#' onclick='deleteDoc();return false;'><i class='bi bi-trash'></i></a>
</div>
<div class='btn-group btn-group-sm d-none' id='edit-actions'>
<button class='btn btn-success' onclick='saveEdit()'><i class='bi bi-check-lg'></i> Speichern</button>
<button class='btn btn-outline-secondary' onclick='cancelEdit()'><i class='bi bi-x-lg'></i> Abbrechen</button>
</div>
</div> </div>
</div> </div>
<div class='card mdview'>{rendered}</div> <div class='card mdview' id='doc-view'>{rendered}</div>
<div class='d-none' id='doc-edit'>
<textarea id='doc-textarea' class='form-control' style='min-height:60vh;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.9rem;resize:vertical'></textarea>
</div>
<script> <script>
const _originalContent = {json.dumps(d['content_md'] or '')};
function startEdit() {{
document.getElementById('doc-textarea').value = _originalContent;
document.getElementById('doc-view').classList.add('d-none');
document.getElementById('doc-edit').classList.remove('d-none');
document.getElementById('view-actions').classList.add('d-none');
document.getElementById('edit-actions').classList.remove('d-none');
document.getElementById('doc-textarea').focus();
}}
function cancelEdit() {{
document.getElementById('doc-view').classList.remove('d-none');
document.getElementById('doc-edit').classList.add('d-none');
document.getElementById('view-actions').classList.remove('d-none');
document.getElementById('edit-actions').classList.add('d-none');
}}
async function saveEdit() {{
const content = document.getElementById('doc-textarea').value;
const body = new URLSearchParams({{content_md: content}});
const r = await fetch('/document/{doc_id}/edit', {{method:'POST', headers:{{'Content-Type':'application/x-www-form-urlencoded'}}, body}});
if(!r.ok) {{ alert('Fehler: '+r.status); return; }}
location.reload();
}}
window.postForm = async function(url, data) {{ window.postForm = async function(url, data) {{
const body = new URLSearchParams(data); const body = new URLSearchParams(data);
const r = await fetch(url, {{method:'POST', headers:{{'Content-Type':'application/x-www-form-urlencoded'}}, body}}); const r = await fetch(url, {{method:'POST', headers:{{'Content-Type':'application/x-www-form-urlencoded'}}, body}});
@@ -782,7 +815,7 @@ window.renameDoc = async function() {{
window.postForm('/document/{doc_id}/rename', {{title:v}}); window.postForm('/document/{doc_id}/rename', {{title:v}});
}}; }};
window.moveDoc = async function() {{ window.moveDoc = async function() {{
const options = {json.dumps([{"value": p['id'], "label": p['name']} for p in projects], ensure_ascii=False)}; const options = {json.dumps([{{"value": p['id'], "label": p['name']}} for p in projects], ensure_ascii=False)};
const v = await window.uiSelect('In Projekt verschieben', options, 'Projekt wählen'); const v = await window.uiSelect('In Projekt verschieben', options, 'Projekt wählen');
if (v===null || v==='') return; if (v===null || v==='') return;
window.postForm('/document/{doc_id}/move', {{project_id:v}}); window.postForm('/document/{doc_id}/move', {{project_id:v}});
@@ -815,6 +848,13 @@ def download_md(doc_id: int):
) )
@app.post("/document/{doc_id}/edit", response_class=HTMLResponse)
def edit_document(doc_id: int, content_md: str = Form(...)):
with db() as c:
c.execute("UPDATE documents SET content_md=? WHERE id=?", (content_md, doc_id))
return HTMLResponse(f"<meta http-equiv='refresh' content='0; url=/document/{doc_id}'>")
@app.post("/document/{doc_id}/rename", response_class=HTMLResponse) @app.post("/document/{doc_id}/rename", response_class=HTMLResponse)
def rename_document(doc_id: int, title: str = Form(...)): def rename_document(doc_id: int, title: str = Form(...)):
with db() as c: with db() as c: