feat(diarization-ui): prompt pipeline on upload + email delivery
- Upload page: select and reorder prompts via drag & drop; after transcription each prompt runs sequentially, feeding its output into the next prompt in the chain - After the last pipeline step the final document is optionally sent as a Markdown attachment via SMTP to selected recipients - Settings: new SMTP configuration section (host, port, user, password, sender, STARTTLS toggle) - Settings: managed list of email recipient addresses (add/delete) - DB migrations: jobs.pipeline_prompt_ids, jobs.email_to, email_addresses table Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
363
app.py
363
app.py
@@ -1,5 +1,11 @@
|
|||||||
|
import email as email_lib
|
||||||
|
import email.encoders
|
||||||
|
import email.mime.base
|
||||||
|
import email.mime.multipart
|
||||||
|
import email.mime.text
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import smtplib
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import threading
|
import threading
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
@@ -144,6 +150,26 @@ def init_db():
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
for col in ["pipeline_prompt_ids TEXT", "email_to TEXT"]:
|
||||||
|
try:
|
||||||
|
c.execute(f"ALTER TABLE jobs ADD COLUMN {col}")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
c.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS email_addresses (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
label TEXT NOT NULL,
|
||||||
|
email TEXT UNIQUE NOT NULL,
|
||||||
|
created_at TEXT NOT NULL
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
# defaults
|
# defaults
|
||||||
c.execute("INSERT OR IGNORE INTO projects(name, created_at) VALUES (?,?)", ("Default", now_iso()))
|
c.execute("INSERT OR IGNORE INTO projects(name, created_at) VALUES (?,?)", ("Default", now_iso()))
|
||||||
c.execute(
|
c.execute(
|
||||||
@@ -177,6 +203,41 @@ def set_setting(key: str, value: str):
|
|||||||
c.execute("INSERT OR REPLACE INTO settings(key, value) VALUES (?,?)", (key, value))
|
c.execute("INSERT OR REPLACE INTO settings(key, value) VALUES (?,?)", (key, value))
|
||||||
|
|
||||||
|
|
||||||
|
def _send_email(to_addrs: list[str], subject: str, body_md: str, attachment_name: str):
|
||||||
|
host = get_setting("smtp_host", "")
|
||||||
|
if not host:
|
||||||
|
raise RuntimeError("SMTP-Host nicht konfiguriert")
|
||||||
|
port = int(get_setting("smtp_port", "587"))
|
||||||
|
user = get_setting("smtp_user", "")
|
||||||
|
password = get_setting("smtp_password", "")
|
||||||
|
from_addr = get_setting("smtp_from", user)
|
||||||
|
use_tls = get_setting("smtp_tls", "true") == "true"
|
||||||
|
|
||||||
|
msg = email.mime.multipart.MIMEMultipart()
|
||||||
|
msg["From"] = from_addr
|
||||||
|
msg["To"] = ", ".join(to_addrs)
|
||||||
|
msg["Subject"] = subject
|
||||||
|
msg.attach(email.mime.text.MIMEText("Anbei das Ergebnis als Markdown-Datei.", "plain", "utf-8"))
|
||||||
|
|
||||||
|
part = email.mime.base.MIMEBase("text", "markdown")
|
||||||
|
part.set_payload(body_md.encode("utf-8"))
|
||||||
|
email.encoders.encode_base64(part)
|
||||||
|
part.add_header("Content-Disposition", "attachment", filename=attachment_name)
|
||||||
|
msg.attach(part)
|
||||||
|
|
||||||
|
if use_tls:
|
||||||
|
with smtplib.SMTP(host, port, timeout=30) as s:
|
||||||
|
s.starttls()
|
||||||
|
if user:
|
||||||
|
s.login(user, password)
|
||||||
|
s.sendmail(from_addr, to_addrs, msg.as_string())
|
||||||
|
else:
|
||||||
|
with smtplib.SMTP(host, port, timeout=30) as s:
|
||||||
|
if user:
|
||||||
|
s.login(user, password)
|
||||||
|
s.sendmail(from_addr, to_addrs, msg.as_string())
|
||||||
|
|
||||||
|
|
||||||
def layout(title: str, body: str) -> str:
|
def layout(title: str, body: str) -> str:
|
||||||
return f"""
|
return f"""
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
@@ -381,7 +442,20 @@ def _process_upload_job(job_id: int):
|
|||||||
)
|
)
|
||||||
new_doc_id = cur.lastrowid
|
new_doc_id = cur.lastrowid
|
||||||
|
|
||||||
|
pipeline_ids = json.loads(j.get("pipeline_prompt_ids") or "[]")
|
||||||
|
email_to = json.loads(j.get("email_to") or "[]")
|
||||||
_job_set(job_id, status="done", result_document_id=new_doc_id, finished_at=now_iso())
|
_job_set(job_id, status="done", result_document_id=new_doc_id, finished_at=now_iso())
|
||||||
|
|
||||||
|
if pipeline_ids:
|
||||||
|
first_id, rest = pipeline_ids[0], pipeline_ids[1:]
|
||||||
|
enqueue_job(
|
||||||
|
"analysis",
|
||||||
|
project_id=j["project_id"],
|
||||||
|
document_id=new_doc_id,
|
||||||
|
prompt_id=first_id,
|
||||||
|
pipeline_prompt_ids=json.dumps(rest),
|
||||||
|
email_to=json.dumps(email_to),
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
_job_set(job_id, status="error", error=str(e), finished_at=now_iso())
|
_job_set(job_id, status="error", error=str(e), finished_at=now_iso())
|
||||||
|
|
||||||
@@ -492,7 +566,27 @@ def _process_analysis_job(job_id: int):
|
|||||||
)
|
)
|
||||||
new_doc_id = cur.lastrowid
|
new_doc_id = cur.lastrowid
|
||||||
|
|
||||||
|
pipeline_ids = json.loads(j.get("pipeline_prompt_ids") or "[]")
|
||||||
|
email_to = json.loads(j.get("email_to") or "[]")
|
||||||
_job_set(job_id, status="done", result_document_id=new_doc_id, finished_at=now_iso(), llm_response=answer, llm_thinking=acc_thinking or None)
|
_job_set(job_id, status="done", result_document_id=new_doc_id, finished_at=now_iso(), llm_response=answer, llm_thinking=acc_thinking or None)
|
||||||
|
|
||||||
|
if pipeline_ids:
|
||||||
|
first_id, rest = pipeline_ids[0], pipeline_ids[1:]
|
||||||
|
enqueue_job(
|
||||||
|
"analysis",
|
||||||
|
project_id=doc["project_id"],
|
||||||
|
document_id=new_doc_id,
|
||||||
|
prompt_id=first_id,
|
||||||
|
pipeline_prompt_ids=json.dumps(rest),
|
||||||
|
email_to=json.dumps(email_to),
|
||||||
|
)
|
||||||
|
elif email_to:
|
||||||
|
safe_title = (doc["title"] or "ergebnis").replace("/", "_").replace(" ", "_")
|
||||||
|
subject = f"VoiceLog: {prm['name']} – {doc['title']}"
|
||||||
|
try:
|
||||||
|
_send_email(email_to, subject, answer, f"{safe_title}_{prm['name']}.md")
|
||||||
|
except Exception as mail_err:
|
||||||
|
_job_set(job_id, error=f"[E-Mail-Fehler] {mail_err}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
_job_set(job_id, status="error", error=str(e), finished_at=now_iso())
|
_job_set(job_id, status="error", error=str(e), finished_at=now_iso())
|
||||||
finally:
|
finally:
|
||||||
@@ -505,8 +599,8 @@ def enqueue_job(kind: str, **kwargs) -> int:
|
|||||||
with db() as c:
|
with db() as c:
|
||||||
cur = c.execute(
|
cur = c.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO jobs(kind,status,project_id,document_id,prompt_id,title,file_path,user_prompt,created_at)
|
INSERT INTO jobs(kind,status,project_id,document_id,prompt_id,title,file_path,user_prompt,pipeline_prompt_ids,email_to,created_at)
|
||||||
VALUES (?,?,?,?,?,?,?,?,?)
|
VALUES (?,?,?,?,?,?,?,?,?,?,?)
|
||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
kind,
|
kind,
|
||||||
@@ -517,6 +611,8 @@ def enqueue_job(kind: str, **kwargs) -> int:
|
|||||||
kwargs.get("title"),
|
kwargs.get("title"),
|
||||||
kwargs.get("file_path"),
|
kwargs.get("file_path"),
|
||||||
kwargs.get("user_prompt") or None,
|
kwargs.get("user_prompt") or None,
|
||||||
|
kwargs.get("pipeline_prompt_ids") or None,
|
||||||
|
kwargs.get("email_to") or None,
|
||||||
now_iso(),
|
now_iso(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -594,6 +690,31 @@ def settings_page(msg: str = ""):
|
|||||||
current_repeat_penalty = get_setting("ollama_repeat_penalty", "1.15")
|
current_repeat_penalty = get_setting("ollama_repeat_penalty", "1.15")
|
||||||
current_repeat_last_n = get_setting("ollama_repeat_last_n", "128")
|
current_repeat_last_n = get_setting("ollama_repeat_last_n", "128")
|
||||||
|
|
||||||
|
smtp_host = get_setting("smtp_host", "")
|
||||||
|
smtp_port = get_setting("smtp_port", "587")
|
||||||
|
smtp_user = get_setting("smtp_user", "")
|
||||||
|
smtp_password = get_setting("smtp_password", "")
|
||||||
|
smtp_from = get_setting("smtp_from", "")
|
||||||
|
smtp_tls = get_setting("smtp_tls", "true") == "true"
|
||||||
|
smtp_tls_checked = "checked" if smtp_tls else ""
|
||||||
|
|
||||||
|
with db() as c:
|
||||||
|
email_addrs = c.execute("SELECT id, label, email FROM email_addresses ORDER BY label").fetchall()
|
||||||
|
|
||||||
|
email_addr_rows = "".join(
|
||||||
|
f"""<tr>
|
||||||
|
<td>{a["label"]}</td>
|
||||||
|
<td>{a["email"]}</td>
|
||||||
|
<td class='text-end'>
|
||||||
|
<form method='post' action='/settings/email-addresses/{a["id"]}/delete' class='d-inline'
|
||||||
|
onsubmit='return confirm("Adresse löschen?")'>
|
||||||
|
<button class='btn btn-sm btn-outline-danger py-0 px-2'><i class='bi bi-trash'></i></button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>"""
|
||||||
|
for a in email_addrs
|
||||||
|
)
|
||||||
|
|
||||||
think_checked = "checked" if current_think else ""
|
think_checked = "checked" if current_think else ""
|
||||||
|
|
||||||
ctx_options = [("auto", "Automatisch (empfohlen)"), ("4096", "4 096"), ("8192", "8 192"),
|
ctx_options = [("auto", "Automatisch (empfohlen)"), ("4096", "4 096"), ("8192", "8 192"),
|
||||||
@@ -669,6 +790,62 @@ def settings_page(msg: str = ""):
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class='card mt-3' id='smtp'>
|
||||||
|
<h5 class='h6 fw-semibold mb-3'><i class='bi bi-envelope text-primary me-1'></i> E-Mail / SMTP</h5>
|
||||||
|
<form method='post' action='/settings/smtp'>
|
||||||
|
<div class='row g-3'>
|
||||||
|
<div class='col-12 col-md-6'>
|
||||||
|
<label class='form-label'>SMTP-Host</label>
|
||||||
|
<input class='form-control' name='smtp_host' value='{smtp_host}' placeholder='mail.example.com'>
|
||||||
|
</div>
|
||||||
|
<div class='col-6 col-md-2'>
|
||||||
|
<label class='form-label'>Port</label>
|
||||||
|
<input class='form-control' type='number' name='smtp_port' value='{smtp_port}' min='1' max='65535'>
|
||||||
|
</div>
|
||||||
|
<div class='col-6 col-md-4'>
|
||||||
|
<label class='form-label'>Absender (From)</label>
|
||||||
|
<input class='form-control' type='email' name='smtp_from' value='{smtp_from}' placeholder='voicelog@example.com'>
|
||||||
|
</div>
|
||||||
|
<div class='col-12 col-md-6'>
|
||||||
|
<label class='form-label'>Benutzername</label>
|
||||||
|
<input class='form-control' name='smtp_user' value='{smtp_user}' autocomplete='off'>
|
||||||
|
</div>
|
||||||
|
<div class='col-12 col-md-6'>
|
||||||
|
<label class='form-label'>Passwort</label>
|
||||||
|
<input class='form-control' type='password' name='smtp_password' value='{smtp_password}' autocomplete='new-password'>
|
||||||
|
</div>
|
||||||
|
<div class='col-12'>
|
||||||
|
<div class='form-check form-switch'>
|
||||||
|
<input class='form-check-input' type='checkbox' role='switch' id='smtpTls'
|
||||||
|
name='smtp_tls' value='true' {smtp_tls_checked}>
|
||||||
|
<label class='form-check-label' for='smtpTls'>STARTTLS verwenden</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class='mt-3'>
|
||||||
|
<button class='btn btn-primary' type='submit'><i class='bi bi-check2-circle'></i> Speichern</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class='card mt-3' id='email-adressen'>
|
||||||
|
<h5 class='h6 fw-semibold mb-3'><i class='bi bi-person-lines-fill text-primary me-1'></i> E-Mail-Adressen</h5>
|
||||||
|
{"<table class='table table-sm mb-3'><thead><tr><th>Name</th><th>E-Mail</th><th></th></tr></thead><tbody>" + email_addr_rows + "</tbody></table>" if email_addrs else "<p class='text-secondary small'>Noch keine Adressen gespeichert.</p>"}
|
||||||
|
<form method='post' action='/settings/email-addresses' class='row g-2 align-items-end'>
|
||||||
|
<div class='col-12 col-md-4'>
|
||||||
|
<label class='form-label'>Name / Label</label>
|
||||||
|
<input class='form-control' name='label' placeholder='z.B. Max Mustermann' required>
|
||||||
|
</div>
|
||||||
|
<div class='col-12 col-md-5'>
|
||||||
|
<label class='form-label'>E-Mail-Adresse</label>
|
||||||
|
<input class='form-control' type='email' name='email' placeholder='max@example.com' required>
|
||||||
|
</div>
|
||||||
|
<div class='col-auto'>
|
||||||
|
<button class='btn btn-outline-primary' type='submit'><i class='bi bi-plus-lg'></i> Hinzufügen</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class='card mt-3'>
|
<div class='card mt-3'>
|
||||||
<h5 class='h6 fw-semibold mb-2'><i class='bi bi-info-circle text-secondary me-1'></i> Systemkonfiguration (read-only)</h5>
|
<h5 class='h6 fw-semibold mb-2'><i class='bi bi-info-circle text-secondary me-1'></i> Systemkonfiguration (read-only)</h5>
|
||||||
<table class='table table-sm mb-0'>
|
<table class='table table-sm mb-0'>
|
||||||
@@ -736,16 +913,94 @@ def settings_save(
|
|||||||
return RedirectResponse("/settings?msg=Einstellungen+gespeichert.", status_code=303)
|
return RedirectResponse("/settings?msg=Einstellungen+gespeichert.", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/settings/smtp", response_class=HTMLResponse)
|
||||||
|
def settings_smtp_save(
|
||||||
|
smtp_host: str = Form(""),
|
||||||
|
smtp_port: str = Form("587"),
|
||||||
|
smtp_user: str = Form(""),
|
||||||
|
smtp_password: str = Form(""),
|
||||||
|
smtp_from: str = Form(""),
|
||||||
|
smtp_tls: Optional[str] = Form(None),
|
||||||
|
):
|
||||||
|
set_setting("smtp_host", smtp_host.strip())
|
||||||
|
set_setting("smtp_port", smtp_port.strip() or "587")
|
||||||
|
set_setting("smtp_user", smtp_user.strip())
|
||||||
|
set_setting("smtp_from", smtp_from.strip())
|
||||||
|
if smtp_password:
|
||||||
|
set_setting("smtp_password", smtp_password)
|
||||||
|
set_setting("smtp_tls", "true" if smtp_tls == "true" else "false")
|
||||||
|
return RedirectResponse("/settings?msg=SMTP-Einstellungen+gespeichert.#smtp", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/settings/email-addresses", response_class=HTMLResponse)
|
||||||
|
def add_email_address(label: str = Form(...), email: str = Form(...)):
|
||||||
|
with db() as c:
|
||||||
|
c.execute(
|
||||||
|
"INSERT OR REPLACE INTO email_addresses(label, email, created_at) VALUES (?,?,?)",
|
||||||
|
(label.strip(), email.strip().lower(), now_iso()),
|
||||||
|
)
|
||||||
|
return RedirectResponse("/settings?msg=E-Mail-Adresse+gespeichert.#email-adressen", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/settings/email-addresses/{addr_id}/delete", response_class=HTMLResponse)
|
||||||
|
def delete_email_address(addr_id: int):
|
||||||
|
with db() as c:
|
||||||
|
c.execute("DELETE FROM email_addresses WHERE id=?", (addr_id,))
|
||||||
|
return RedirectResponse("/settings?msg=E-Mail-Adresse+gelöscht.#email-adressen", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/", response_class=HTMLResponse)
|
@app.get("/", response_class=HTMLResponse)
|
||||||
def upload_page(msg: str = ""):
|
def upload_page(msg: str = ""):
|
||||||
projects = get_projects()
|
projects = get_projects()
|
||||||
existing_names = json.dumps([p["name"] for p in projects], ensure_ascii=False)
|
existing_names = json.dumps([p["name"] for p in projects], ensure_ascii=False)
|
||||||
existing_map = json.dumps({p["name"]: p["id"] for p in projects}, ensure_ascii=False)
|
existing_map = json.dumps({p["name"]: p["id"] for p in projects}, ensure_ascii=False)
|
||||||
|
|
||||||
|
with db() as c:
|
||||||
|
prompts = c.execute("SELECT id, name FROM prompts ORDER BY name").fetchall()
|
||||||
|
email_addrs = c.execute("SELECT id, label, email FROM email_addresses ORDER BY label").fetchall()
|
||||||
|
|
||||||
|
smtp_configured = bool(get_setting("smtp_host", ""))
|
||||||
|
|
||||||
|
prompt_items = "".join(
|
||||||
|
f"""<li class='list-group-item d-flex align-items-center gap-2 py-2' draggable='true'
|
||||||
|
data-id='{p["id"]}' style='cursor:grab'>
|
||||||
|
<span class='drag-handle text-secondary' style='cursor:grab'><i class='bi bi-grip-vertical'></i></span>
|
||||||
|
<input class='form-check-input flex-shrink-0 mt-0' type='checkbox' id='pchk_{p["id"]}' value='{p["id"]}' onchange='updateOrder()'>
|
||||||
|
<label class='form-check-label mb-0 flex-grow-1' for='pchk_{p["id"]}'>{p["name"]}</label>
|
||||||
|
<span class='badge bg-secondary order-badge d-none'></span>
|
||||||
|
</li>"""
|
||||||
|
for p in prompts
|
||||||
|
)
|
||||||
|
|
||||||
|
email_items = "".join(
|
||||||
|
f"""<div class='form-check'>
|
||||||
|
<input class='form-check-input' type='checkbox' id='eml_{a["id"]}' value='{a["email"]}' onchange='updateEmails()'>
|
||||||
|
<label class='form-check-label' for='eml_{a["id"]}'>{a["label"]} <span class='text-secondary small'><{a["email"]}></span></label>
|
||||||
|
</div>"""
|
||||||
|
for a in email_addrs
|
||||||
|
)
|
||||||
|
|
||||||
|
email_section = ""
|
||||||
|
if smtp_configured and email_addrs:
|
||||||
|
email_section = f"""
|
||||||
|
<div class='card mt-3'>
|
||||||
|
<h6 class='fw-semibold mb-2'><i class='bi bi-envelope text-primary me-1'></i> E-Mail-Versand (optional)</h6>
|
||||||
|
<p class='text-secondary small mb-2'>Letztes Ergebnis der Pipeline nach Abschluss als Anhang versenden:</p>
|
||||||
|
{email_items}
|
||||||
|
<div id='emailPreview' class='text-secondary small mt-1 d-none'>Sende an: <span id='emailPreviewList'></span></div>
|
||||||
|
</div>"""
|
||||||
|
elif not smtp_configured:
|
||||||
|
email_section = "<div class='text-secondary small mt-3'><i class='bi bi-envelope-x me-1'></i> E-Mail-Versand nicht konfiguriert — <a href='/settings#smtp'>Einstellungen</a></div>"
|
||||||
|
elif not email_addrs:
|
||||||
|
email_section = "<div class='text-secondary small mt-3'><i class='bi bi-envelope-x me-1'></i> Keine E-Mail-Adressen gespeichert — <a href='/settings#email-adressen'>Einstellungen</a></div>"
|
||||||
|
|
||||||
|
no_prompts_hint = "" if prompts else "<div class='text-secondary small'>Keine Prompts vorhanden — <a href='/prompts'>Prompts anlegen</a></div>"
|
||||||
|
|
||||||
body = f"""
|
body = f"""
|
||||||
<div class='d-flex justify-content-between align-items-center mb-3'>
|
<div class='d-flex justify-content-between align-items-center mb-3'>
|
||||||
<h2 class='h4 mb-0'>Audio Upload</h2>
|
<h2 class='h4 mb-0'>Audio Upload</h2>
|
||||||
</div>
|
</div>
|
||||||
<p class='text-secondary'>Mehrere Audiodateien gleichzeitig hochladen — je Datei wird ein Transkriptions-Job erstellt.</p>
|
<p class='text-secondary'>Audiodateien hochladen, transkribieren lassen und optional eine Prompt-Pipeline durchlaufen.</p>
|
||||||
{f"<div class='alert alert-info py-2'>{msg}</div>" if msg else ""}
|
{f"<div class='alert alert-info py-2'>{msg}</div>" if msg else ""}
|
||||||
<div class='card'>
|
<div class='card'>
|
||||||
<div class='row g-3 mb-3'>
|
<div class='row g-3 mb-3'>
|
||||||
@@ -768,7 +1023,20 @@ def upload_page(msg: str = ""):
|
|||||||
<i class='bi bi-cloud-upload'></i> <span id='uploadBtnLabel'>Hochladen & verarbeiten</span>
|
<i class='bi bi-cloud-upload'></i> <span id='uploadBtnLabel'>Hochladen & verarbeiten</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div id='uploadProgress' class='d-none'>
|
|
||||||
|
<div class='card mt-3'>
|
||||||
|
<h6 class='fw-semibold mb-2'><i class='bi bi-list-ol text-primary me-1'></i> Prompt-Pipeline <span class='text-secondary fw-normal small'>(optional, Reihenfolge per Drag & Drop)</span></h6>
|
||||||
|
{no_prompts_hint}
|
||||||
|
<ul class='list-group list-group-flush' id='promptList' style='user-select:none'>
|
||||||
|
{prompt_items}
|
||||||
|
</ul>
|
||||||
|
<div id='pipelinePreview' class='text-secondary small mt-2 d-none'>
|
||||||
|
Pipeline: <span id='pipelinePreviewText'></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{email_section}
|
||||||
|
|
||||||
|
<div id='uploadProgress' class='d-none mt-3'>
|
||||||
<div class='progress mb-2' style='height:8px'>
|
<div class='progress mb-2' style='height:8px'>
|
||||||
<div class='progress-bar progress-bar-striped progress-bar-animated' id='progressBar' style='width:0%'></div>
|
<div class='progress-bar progress-bar-striped progress-bar-animated' id='progressBar' style='width:0%'></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -803,6 +1071,64 @@ fileInput.addEventListener('change', () => {{
|
|||||||
}});
|
}});
|
||||||
projectInput.addEventListener('input', updateBtn);
|
projectInput.addEventListener('input', updateBtn);
|
||||||
|
|
||||||
|
// --- Prompt pipeline ordering ---
|
||||||
|
function updateOrder() {{
|
||||||
|
const items = document.querySelectorAll('#promptList li');
|
||||||
|
let n = 1;
|
||||||
|
items.forEach(li => {{
|
||||||
|
const chk = li.querySelector('input[type=checkbox]');
|
||||||
|
const badge = li.querySelector('.order-badge');
|
||||||
|
if (chk.checked) {{
|
||||||
|
badge.textContent = n++;
|
||||||
|
badge.classList.remove('d-none');
|
||||||
|
}} else {{
|
||||||
|
badge.classList.add('d-none');
|
||||||
|
}}
|
||||||
|
}});
|
||||||
|
const checked = [...items].filter(li => li.querySelector('input').checked);
|
||||||
|
const names = checked.map(li => li.querySelector('label').textContent.trim());
|
||||||
|
const preview = document.getElementById('pipelinePreview');
|
||||||
|
const previewText = document.getElementById('pipelinePreviewText');
|
||||||
|
if (names.length) {{
|
||||||
|
previewText.textContent = names.join(' → ');
|
||||||
|
preview.classList.remove('d-none');
|
||||||
|
}} else {{
|
||||||
|
preview.classList.add('d-none');
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
|
||||||
|
// Drag-and-drop reorder
|
||||||
|
let _dragSrc = null;
|
||||||
|
document.getElementById('promptList').addEventListener('dragstart', e => {{
|
||||||
|
_dragSrc = e.target.closest('li');
|
||||||
|
e.dataTransfer.effectAllowed = 'move';
|
||||||
|
}});
|
||||||
|
document.getElementById('promptList').addEventListener('dragover', e => {{
|
||||||
|
e.preventDefault();
|
||||||
|
const target = e.target.closest('li');
|
||||||
|
if (target && target !== _dragSrc) {{
|
||||||
|
const rect = target.getBoundingClientRect();
|
||||||
|
const after = e.clientY > rect.top + rect.height / 2;
|
||||||
|
target.parentNode.insertBefore(_dragSrc, after ? target.nextSibling : target);
|
||||||
|
}}
|
||||||
|
}});
|
||||||
|
document.getElementById('promptList').addEventListener('dragend', () => {{ updateOrder(); }});
|
||||||
|
|
||||||
|
// --- Email selection ---
|
||||||
|
let _selectedEmails = [];
|
||||||
|
function updateEmails() {{
|
||||||
|
_selectedEmails = [...document.querySelectorAll('#emailCheckboxes input:checked')].map(i => i.value);
|
||||||
|
const preview = document.getElementById('emailPreview');
|
||||||
|
const list = document.getElementById('emailPreviewList');
|
||||||
|
if (!preview) return;
|
||||||
|
if (_selectedEmails.length) {{
|
||||||
|
list.textContent = _selectedEmails.join(', ');
|
||||||
|
preview.classList.remove('d-none');
|
||||||
|
}} else {{
|
||||||
|
preview.classList.add('d-none');
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
|
||||||
async function startUpload() {{
|
async function startUpload() {{
|
||||||
const name = projectInput.value.trim();
|
const name = projectInput.value.trim();
|
||||||
if(!name || fileInput.files.length === 0) return;
|
if(!name || fileInput.files.length === 0) return;
|
||||||
@@ -821,6 +1147,10 @@ async function startUpload() {{
|
|||||||
projectId = (await r.json()).id;
|
projectId = (await r.json()).id;
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
// collect pipeline prompt IDs in current list order, only checked ones
|
||||||
|
const pipelineIds = [...document.querySelectorAll('#promptList li input:checked')].map(i => parseInt(i.value));
|
||||||
|
const emailTo = [...document.querySelectorAll('[id^="eml_"]:checked')].map(i => i.value);
|
||||||
|
|
||||||
for(let i=0; i<files.length; i++) {{
|
for(let i=0; i<files.length; i++) {{
|
||||||
const f = files[i];
|
const f = files[i];
|
||||||
const stem = f.name.replace(/\\.[^/.]+$/, '');
|
const stem = f.name.replace(/\\.[^/.]+$/, '');
|
||||||
@@ -829,6 +1159,8 @@ async function startUpload() {{
|
|||||||
const fd = new FormData();
|
const fd = new FormData();
|
||||||
fd.append('project_id', projectId);
|
fd.append('project_id', projectId);
|
||||||
fd.append('file', f);
|
fd.append('file', f);
|
||||||
|
fd.append('pipeline_prompt_ids', JSON.stringify(pipelineIds));
|
||||||
|
fd.append('email_to', JSON.stringify(emailTo));
|
||||||
const r = await fetch('/upload', {{method:'POST', body: fd}});
|
const r = await fetch('/upload', {{method:'POST', body: fd}});
|
||||||
if(!r.ok) {{ status.textContent = `Fehler bei ${{stem}}`; }}
|
if(!r.ok) {{ status.textContent = `Fehler bei ${{stem}}`; }}
|
||||||
}}
|
}}
|
||||||
@@ -885,7 +1217,12 @@ def create_project_api(name: str = Form(...)):
|
|||||||
|
|
||||||
|
|
||||||
@app.post("/upload")
|
@app.post("/upload")
|
||||||
async def upload(project_id: int = Form(...), file: UploadFile = File(...)):
|
async def upload(
|
||||||
|
project_id: int = Form(...),
|
||||||
|
file: UploadFile = File(...),
|
||||||
|
pipeline_prompt_ids: str = Form("[]"),
|
||||||
|
email_to: str = Form("[]"),
|
||||||
|
):
|
||||||
data = await file.read()
|
data = await file.read()
|
||||||
if not data:
|
if not data:
|
||||||
raise HTTPException(400, "Leere Datei")
|
raise HTTPException(400, "Leere Datei")
|
||||||
@@ -896,11 +1233,27 @@ async def upload(project_id: int = Form(...), file: UploadFile = File(...)):
|
|||||||
temp_path = JOB_DIR / f"{now_iso().replace(':','-')}_{filename}"
|
temp_path = JOB_DIR / f"{now_iso().replace(':','-')}_{filename}"
|
||||||
temp_path.write_bytes(data)
|
temp_path.write_bytes(data)
|
||||||
|
|
||||||
|
try:
|
||||||
|
ids = json.loads(pipeline_prompt_ids)
|
||||||
|
if not isinstance(ids, list):
|
||||||
|
ids = []
|
||||||
|
except Exception:
|
||||||
|
ids = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
emails = json.loads(email_to)
|
||||||
|
if not isinstance(emails, list):
|
||||||
|
emails = []
|
||||||
|
except Exception:
|
||||||
|
emails = []
|
||||||
|
|
||||||
job_id = enqueue_job(
|
job_id = enqueue_job(
|
||||||
"upload",
|
"upload",
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
title=stem,
|
title=stem,
|
||||||
file_path=str(temp_path),
|
file_path=str(temp_path),
|
||||||
|
pipeline_prompt_ids=json.dumps(ids) if ids else None,
|
||||||
|
email_to=json.dumps(emails) if emails else None,
|
||||||
)
|
)
|
||||||
return {"job_id": job_id}
|
return {"job_id": job_id}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user