diff --git a/server/workspace-ki.js b/server/workspace-ki.js
index ab7ef6f8..9fe889dd 100644
--- a/server/workspace-ki.js
+++ b/server/workspace-ki.js
@@ -196,7 +196,6 @@ kiRouter.post("/workspace/api/ki/todos", gleicheHerkunft, express.json(), (req,
kiRouter.post("/workspace/api/ki/aufgabentitel", gleicheHerkunft, express.json(), (req, res) => {
const befund = String(req.body?.befund ?? "").trim().slice(0, 600);
- const bereich = String(req.body?.bereich ?? "").trim().slice(0, 120);
if (befund.length < 8) {
return res.status(400).json({ fehler: "Schreib erst eine Notiz zu dem Punkt." });
}
@@ -204,7 +203,12 @@ kiRouter.post("/workspace/api/ki/aufgabentitel", gleicheHerkunft, express.json()
const roh = await frage(
"Du formulierst kurze Aufgabentitel. Nur der Titel, hoechstens 8 Woerter, "
+ "Deutsch, Verb am Anfang, kein Punkt, keine Anfuehrungszeichen.",
- `Befund${bereich ? " (" + bereich + ")" : ""}: ${befund}`,
+ /* Der Bereichsname wird bewusst NICHT mitgegeben. Mit ihm kam
+ "LIVE-Struktur auf 18 und 21 Uhr festlegen" zurueck -- das
+ Modell baut ihn in den Titel ein, statt ihn als Zusammenhang zu
+ behandeln. Ohne ihn: "Zeitpunkt der Sendung festlegen". Der
+ Befund allein reicht, er steht ja im richtigen Bereich. */
+ `Befund: ${befund}`,
40,
[
{ role: "user", content: "Befund: Profilbild ist unscharf und alt." },
diff --git a/workspace/assets/css/gate.css b/workspace/assets/css/gate.css
index ca08f7f5..73e2e352 100644
--- a/workspace/assets/css/gate.css
+++ b/workspace/assets/css/gate.css
@@ -631,3 +631,66 @@ body.suche-offen { overflow: hidden; }
.stufe-knopf:hover, .suche-knopf:hover, .suche-zu:hover,
.todo__weg:hover, .karte__stift:hover { transform: none; }
}
+
+/* ===================================================================
+ KI-Vorschläge (28.08.2026).
+
+ Bewusst ANDERS als die normalen Knöpfe: Ein KI-Knopf tut nichts
+ Endgültiges, er schlägt vor. Deshalb gestrichelter Rand statt
+ geschliffener Kante -- man soll auf den ersten Blick sehen, dass hier
+ etwas Unverbindliches passiert.
+
+ Liegt in gate.css, weil drei Seiten ihn benutzen.
+ =================================================================== */
+
+.ki-knopf {
+ display: inline-flex; align-items: center; gap: 8px;
+ padding: 8px 15px; cursor: pointer;
+ background: rgba(138, 118, 255, .07);
+ border: 1px dashed rgba(138, 118, 255, .45);
+ border-radius: 999px;
+ color: #b6a9ff; font: inherit; font-size: .82rem; font-weight: 600;
+ transition: color .16s ease, border-color .16s ease, background .16s ease;
+}
+.ki-knopf:hover:not([disabled]) {
+ color: #fff;
+ border-color: rgba(138, 118, 255, .8);
+ background: rgba(138, 118, 255, .16);
+}
+.ki-knopf:focus-visible {
+ outline: none; box-shadow: 0 0 0 3px rgba(138, 118, 255, .3);
+}
+.ki-knopf[disabled] { cursor: progress; }
+.ki-knopf__funke {
+ width: 15px; height: 15px; flex: none;
+ fill: currentColor; stroke: none;
+}
+/* Denkt gerade: der Funke pulst langsam. Zwölf Sekunden ohne
+ Rückmeldung fühlen sich sonst an wie ein Fehler. */
+.ki-knopf[data-denkt="ja"] { border-style: solid; }
+.ki-knopf[data-denkt="ja"] .ki-knopf__funke { animation: ki-puls 1.6s ease-in-out infinite; }
+@keyframes ki-puls { 0%, 100% { opacity: .35; } 50% { opacity: 1; } }
+
+.ki-fehler, .ki-hinweis {
+ display: block; margin-top: 7px;
+ font-size: .8rem; color: var(--text-still);
+}
+.ki-fehler { color: var(--warn); }
+
+.ki-reihe { margin-top: 22px; }
+.ki-text {
+ margin: 10px 0 0; padding: 13px 15px;
+ background: rgba(138, 118, 255, .06);
+ border: 1px solid rgba(138, 118, 255, .28);
+ border-left-width: 3px;
+ border-radius: var(--radius-klein);
+ font-size: .9rem; color: var(--text-leise); line-height: 1.6;
+ max-width: 80ch;
+}
+
+.todo-knoepfe { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; margin-bottom: 14px; }
+
+@media (prefers-reduced-motion: reduce) {
+ .ki-knopf { transition: none; }
+ .ki-knopf[data-denkt="ja"] .ki-knopf__funke { animation: none; opacity: .7; }
+}
diff --git a/workspace/assets/js/calls.js b/workspace/assets/js/calls.js
index 38251c0c..9b0edca6 100644
--- a/workspace/assets/js/calls.js
+++ b/workspace/assets/js/calls.js
@@ -13,6 +13,7 @@
let ich = null;
let daten = null;
let offen = new Set();
+ let kiDa = false;
const el = (tag, klasse, text) => {
const k = document.createElement(tag);
@@ -108,7 +109,36 @@
const mehr = el('button', 'knopf-still', '+ Zeile');
mehr.type = 'button';
mehr.addEventListener('click', () => todoZeile(true));
- f.append(mehr);
+
+ const reihe = el('div', 'todo-knoepfe');
+ reihe.append(mehr);
+
+ /* Vorschlaege aus dem, was oben schon steht. Der Knopf erscheint
+ nur, wenn die KI ueberhaupt laeuft -- sonst gaebe es einen Knopf,
+ der immer scheitert. */
+ if (kiDa) {
+ reihe.append(window.KI.knopf('To-dos vorschlagen', async () => {
+ const text = [fPunkte.value, fEnt.value].filter((x) => x.trim()).join(' ');
+ if (text.trim().length < 20) {
+ throw new Error('Schreib erst ein paar Sätze oben – daraus kann die KI dann ableiten.');
+ }
+ const d = await window.KI.frage('todos', { text });
+ if (!d.todos?.length) throw new Error('Die KI hat nichts gefunden.');
+ /* Leere Zeilen zuerst fuellen, dann neue anhaengen. Bereits
+ Getipptes wird nie ueberschrieben. */
+ for (const vorschlag of d.todos) {
+ let ziel = [...todoBox.querySelectorAll('.todo__titel')].find((i) => !i.value.trim());
+ if (!ziel) { todoZeile(false); ziel = todoBox.lastElementChild.querySelector('.todo__titel'); }
+ ziel.value = vorschlag;
+ ziel.dataset.vonKi = 'ja';
+ }
+ meldung.textContent = '';
+ const hinweis = el('p', 'ki-hinweis', d.hinweis);
+ reihe.after(hinweis);
+ setTimeout(() => hinweis.remove(), 9000);
+ }));
+ }
+ f.append(reihe);
/* Nächster Termin */
f.append(el('label', 'feld-schild', 'Nächster Termin'));
@@ -329,6 +359,7 @@
ich = await a.json();
} catch { return; }
$('wer').textContent = ich.name + ' · ' + (ich.rolle_name || ich.rolle);
+ kiDa = await window.KI.verfuegbar();
await laden();
})();
})();
diff --git a/workspace/assets/js/ki.js b/workspace/assets/js/ki.js
new file mode 100644
index 00000000..2add25e6
--- /dev/null
+++ b/workspace/assets/js/ki.js
@@ -0,0 +1,88 @@
+/* ===================================================================
+ Gemeinsamer Zugang zur lokalen KI.
+
+ Liegt in einer eigenen Datei, weil drei Seiten sie benutzen (Calls,
+ Start-Check, Report) und die Absicherung sonst dreimal dastuende --
+ und beim naechsten Mal in zwei davon.
+
+ ZWEI REGELN:
+
+ 1. IST DIE KI AUS, GIBT ES DEN KNOPF NICHT. Ein Knopf, der immer eine
+ Fehlermeldung bringt, ist schlimmer als gar kein Knopf. Deshalb
+ fragt jede Seite beim Laden einmal nach und baut ihn nur dann.
+
+ 2. DIE KI SCHLAEGT VOR, DER MENSCH ENTSCHEIDET. Kein Aufruf hier
+ speichert etwas. Was uebernommen wird, traegt der Mensch an
+ derselben Stelle ein wie ohne KI.
+ =================================================================== */
+window.KI = (() => {
+ 'use strict';
+
+ let bekannt = null; // null = noch nicht gefragt
+
+ /* Einmal pro Seitenaufruf fragen, danach gemerkt. */
+ async function verfuegbar() {
+ if (bekannt !== null) return bekannt;
+ try {
+ const a = await fetch('/workspace/api/ki/status', { credentials: 'same-origin' });
+ bekannt = a.ok ? (await a.json()).da === true : false;
+ } catch { bekannt = false; }
+ return bekannt;
+ }
+
+ async function frage(pfad, koerper) {
+ const a = await fetch('/workspace/api/ki/' + pfad, {
+ method: 'POST',
+ credentials: 'same-origin',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(koerper),
+ });
+ if (a.status === 401) { location.assign('/workspace/'); throw new Error('abgemeldet'); }
+ const d = await a.json().catch(() => ({}));
+ if (!a.ok) throw new Error(d.fehler || 'Die KI konnte nicht antworten.');
+ return d;
+ }
+
+ /* Baut einen Knopf, der waehrend des Denkens sagt, dass er denkt --
+ zwoelf Sekunden ohne Rueckmeldung fuehlen sich an wie ein Fehler. */
+ function knopf(beschriftung, tuWas) {
+ const b = document.createElement('button');
+ b.type = 'button';
+ b.className = 'ki-knopf';
+ const funke = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
+ funke.setAttribute('viewBox', '0 0 24 24');
+ funke.setAttribute('class', 'ki-knopf__funke');
+ funke.setAttribute('aria-hidden', 'true');
+ funke.innerHTML = '