diff --git a/.claude/docs/working/Test_status_Wkf.md b/.claude/docs/working/Test_status_Wkf.md new file mode 100644 index 0000000..f62468c --- /dev/null +++ b/.claude/docs/working/Test_status_Wkf.md @@ -0,0 +1,11 @@ +Folgende Ergebnisse des Tests: +- Valididierung gibt immer noch keine Aufschlüsse was für Fehler und Warning es sind, Es zeigt immer nur noch die Anzahl der entsprechenden Fehler/Warnungen +- Speichern als kurzes PopUp -- gut +- In der Node selbst wird nun eine Fehlermeldung ausgegeben. Das ist gut. In größen Workflows aber schwierig den Fehler zu lokalisieren. +- In der automatischen Zusammenfassung in der Endnode kommt als Überschrift, z.B. Node 10, anstatt den Node-Name auszugeben. +- Alle Änderungen an Nodes scheinen automatisch in den Gesamtflow übernommen zu werden. Diese werden dann nach dem Speichern aktiv. Da muss man sehr vorsichtig sein, bei kurzen Änderungen und dem Ausprobieren. +- Der Testlauf "Execute" sollte auf dem aktuellen Workflowstand ausgeführt werden, auch wenn dieser vom gespeicherten Abweicht. Ich würde natürlich vor dem Speichern den Workflow testen können. Prüfe und bewerte diesen Punkt, setze ihn aber noch nicht um. +- Die Workflows werden aktuell nicht in Analyse und den verfügbaren KI-Asuwertungen angezeigt. ggf. weil wir sie aktuell noch keinem Bereich zuordnen können. Diesen könnten wir ggf. über die Start-Node im Workflow konfigurieren. +- Das löschen von Knoten und Kanten funktioniert aktuell nur über Backspace nicht über entfernen +- Wir sollten auch dafür sorgen, dass jeweils nur eine Start-Node, End-Node in einem Workflow existiert, Prüfe ob mehrere End-Nodes sinnvoll sind, da wir ja auch Logik-Pfade abbilden und ggf. auch eine route beschreiten, die ein anderes Ende hat. (Prüfe, ob das heute schon möglich wäre!) +- Als zukünftige Ausbaustufe sollten wir überlegen, ob wir auch Trigger implementieren, z.B. um Kurzstatements zu generieren, wenn neue Daten hereinkommen und wir diese Bewertungen aktualisieren wollen \ No newline at end of file diff --git a/backend/routers/prompts.py b/backend/routers/prompts.py index 732bfea..5e7d76f 100644 --- a/backend/routers/prompts.py +++ b/backend/routers/prompts.py @@ -31,6 +31,42 @@ OPENROUTER_MODEL = os.getenv("OPENROUTER_MODEL", "anthropic/claude-sonnet-4") router = APIRouter(prefix="/api/prompts", tags=["prompts"]) +# Metadaten-Schlüssel in workflow aggregate_results (nicht als „einziger“ Nutzer-Output) +_WORKFLOW_AGG_META_KEYS = frozenset({ + "combined_analysis", + "all_signals", + "total_nodes", + "executed_nodes", + "failed_nodes", + "skipped_nodes", +}) + + +def _workflow_user_facing_content(agg: object) -> str: + """ + Nutzer-sichtbarer Text wie im Admin WorkflowResultViewer („Final Output“): + primär aggregated_result['analysis_core'], nicht das gesamte JSON. + """ + if agg is None: + return "" + if isinstance(agg, str): + return agg + if not isinstance(agg, dict): + return json.dumps(agg, ensure_ascii=False) + core = agg.get("analysis_core") + if isinstance(core, str) and core.strip(): + return core + combined = agg.get("combined_analysis") + if isinstance(combined, str) and combined.strip(): + return combined + non_meta = [k for k in agg.keys() if k not in _WORKFLOW_AGG_META_KEYS] + if len(non_meta) == 1: + v = agg[non_meta[0]] + if isinstance(v, str): + return v + return json.dumps(v, ensure_ascii=False) + return json.dumps(agg, ensure_ascii=False) + @router.get("") def list_prompts(session: dict=Depends(require_auth)): @@ -1253,6 +1289,8 @@ async def execute_unified_prompt( content = list(final_output.values())[0] else: content = json.dumps(final_output, ensure_ascii=False) + elif result['type'] == 'workflow': + content = _workflow_user_facing_content(result.get('aggregated_result')) else: # For base prompts, use output directly content = result.get('output', '') diff --git a/frontend/src/app.css b/frontend/src/app.css index 9f4d219..a237ef6 100644 --- a/frontend/src/app.css +++ b/frontend/src/app.css @@ -18,6 +18,8 @@ --header-h: 52px; --font: system-ui, -apple-system, 'Segoe UI', sans-serif; --capture-content-max: 800px; + /* Admin: nutzt volle Hauptspalte bis zu dieser Obergrenze (siehe .app-main:has(.admin-shell)) */ + --admin-main-max: min(1560px, calc(100vw - 220px)); } @media (prefers-color-scheme: dark) { :root { @@ -619,13 +621,16 @@ a.analysis-split__nav-item { .admin-page { width: 100%; + min-width: 0; } +/* Desktop: volle Breite der Admin-Spalte (nicht wie Erfassung 800px); Lesegröße leicht skaliert */ @media (min-width: 1024px) { .admin-page { - max-width: var(--capture-content-max); - margin-left: auto; - margin-right: auto; + max-width: 100%; + margin-left: 0; + margin-right: 0; + font-size: clamp(15px, 0.88rem + 0.25vw, 18px); } } @@ -841,6 +846,11 @@ a.analysis-split__nav-item { box-sizing: border-box; } + /* Admin: mehr horizontaler Raum für Tabellen auf großen Screens (:has ~2022+, sonst bleibt 1200px) */ + .app-main:has(.admin-shell) { + max-width: var(--admin-main-max); + } + /* Dashboard (P3): Begrüßung + Kennzahlen-Zeile */ .dashboard-greeting { display: flex; diff --git a/frontend/src/components/workflow/nodes/StartNode.jsx b/frontend/src/components/workflow/nodes/StartNode.jsx index 327475c..5f31a5f 100644 --- a/frontend/src/components/workflow/nodes/StartNode.jsx +++ b/frontend/src/components/workflow/nodes/StartNode.jsx @@ -3,9 +3,8 @@ import { Handle, Position } from 'reactflow' /** * StartNode - Workflow Einstiegspunkt * - * Properties: - * - data.label: Node-Label (default: "Start") - * - selected: Boolean (Node ist ausgewählt) + * - data.label: Anzeige auf dem Canvas (Node-Name) + * - data.analysis_title: nur für KI-Analyse-UI, nicht auf dem Canvas */ export function StartNode({ data, selected }) { return ( diff --git a/frontend/src/config/analysisCategories.js b/frontend/src/config/analysisCategories.js new file mode 100644 index 0000000..f942055 --- /dev/null +++ b/frontend/src/config/analysisCategories.js @@ -0,0 +1,21 @@ +/** DB `ai_prompts.category` – Reihenfolge der Gruppen in der KI-Analyse-Navigation */ + +export const ANALYSIS_CATEGORY_ORDER = [ + 'körper', + 'ernährung', + 'training', + 'schlaf', + 'vitalwerte', + 'ziele', + 'ganzheitlich', +] + +export const ANALYSIS_CATEGORY_LABELS = { + körper: 'Körper', + ernährung: 'Ernährung', + training: 'Training', + schlaf: 'Schlaf', + vitalwerte: 'Vitalwerte', + ziele: 'Ziele', + ganzheitlich: 'Ganzheitlich', +} diff --git a/frontend/src/pages/Analysis.jsx b/frontend/src/pages/Analysis.jsx index 25cea80..53dac0b 100644 --- a/frontend/src/pages/Analysis.jsx +++ b/frontend/src/pages/Analysis.jsx @@ -2,6 +2,11 @@ import React, { useState, useEffect, useMemo } from 'react' import { Brain, Trash2, ChevronDown, ChevronUp } from 'lucide-react' import { Link } from 'react-router-dom' import { api } from '../utils/api' +import { getWorkflowDisplayContent } from '../utils/workflowDisplay' +import { + ANALYSIS_CATEGORY_ORDER, + ANALYSIS_CATEGORY_LABELS, +} from '../config/analysisCategories' import { useAuth } from '../context/AuthContext' import Markdown from '../utils/Markdown' import UsageBadge from '../components/UsageBadge' @@ -14,19 +19,6 @@ const SLUG_LABELS = { pipeline: '🔬 Mehrstufige Gesamtanalyse' } -/** DB `ai_prompts.category` – Reihenfolge der Gruppen in der Analyse-Navigation */ -const ANALYSIS_CATEGORY_ORDER = ['körper', 'ernährung', 'training', 'schlaf', 'vitalwerte', 'ziele', 'ganzheitlich'] - -const ANALYSIS_CATEGORY_LABELS = { - körper: 'Körper', - ernährung: 'Ernährung', - training: 'Training', - schlaf: 'Schlaf', - vitalwerte: 'Vitalwerte', - ziele: 'Ziele', - ganzheitlich: 'Ganzheitlich', -} - function sortAnalysisCategoryKeys(keys) { return [...keys].sort((a, b) => { const na = String(a).toLowerCase() @@ -45,6 +37,11 @@ function analysisCategoryLabel(key) { return ANALYSIS_CATEGORY_LABELS[k] || String(key) } +/** Analyse-Angebote: klassische Pipelines + graphbasierte KI-Workflows (`type === 'workflow'`) */ +function isAnalysisOfferPrompt(p) { + return p.active && (p.type === 'pipeline' || p.type === 'workflow') +} + /** Pipeline-Prompts nach `category` gruppieren (Backend-Feld), innerhalb Gruppe nach sort_order */ function buildPipelineGroups(pipelinePrompts) { const m = new Map() @@ -81,7 +78,8 @@ function InsightCard({ ins, onDelete, defaultOpen=false, prompts=[] }) { // Find matching prompt to get display_name const prompt = prompts.find(p => p.slug === ins.scope) - const displayName = prompt?.display_name || SLUG_LABELS[ins.scope] || ins.scope + const displayName = + prompt?.display_name || prompt?.name || SLUG_LABELS[ins.scope] || ins.scope // Use already-parsed metadata const metadata = metadataRaw @@ -360,7 +358,7 @@ export default function Analysis() { },[]) useEffect(() => { - const list = prompts.filter(p => p.active && p.type === 'pipeline') + const list = prompts.filter(isAnalysisOfferPrompt) setActiveCategoryKey(prev => { if (!list.length) return null const groups = buildPipelineGroups(list) @@ -372,7 +370,7 @@ export default function Analysis() { useEffect(() => { if (!newResult?.scope) return - const list = prompts.filter(p => p.active && p.type === 'pipeline') + const list = prompts.filter(isAnalysisOfferPrompt) const groups = buildPipelineGroups(list) const g = groups.find(gg => gg.prompts.some(p => p.slug === newResult.scope)) if (g) setActiveCategoryKey(g.categoryKey) @@ -394,6 +392,8 @@ export default function Analysis() { } else { content = JSON.stringify(finalOutput, null, 2) } + } else if (result.type === 'workflow') { + content = getWorkflowDisplayContent(result.aggregated_result) } else { // For base prompts, use output directly content = typeof result.output === 'string' ? result.output : JSON.stringify(result.output, null, 2) @@ -405,7 +405,7 @@ export default function Analysis() { const placeholders = {} const resolved = result.debug.resolved_placeholders - // For pipeline, collect from all stages + // For pipeline, collect from all stages (Workflow: kein gleiches debug-Schema) if (result.type === 'pipeline' && result.debug.stages) { for (const stage of result.debug.stages) { for (const promptDebug of (stage.prompts || [])) { @@ -456,9 +456,9 @@ export default function Analysis() { grouped[key].push(ins) }) - // Show only active pipeline-type prompts (und nach DB-Kategorie gruppiert) + // Aktive Pipeline- + Workflow-Prompts (nach DB-Kategorie gruppiert) const { pipelinePrompts, pipelineGroups } = useMemo(() => { - const list = prompts.filter(p => p.active && p.type === 'pipeline') + const list = prompts.filter(isAnalysisOfferPrompt) return { pipelinePrompts: list, pipelineGroups: buildPipelineGroups(list) } }, [prompts]) @@ -538,9 +538,10 @@ export default function Analysis() { {canUseAI && pipelinePrompts.length > 0 && ( <>
- Zuerst die Kategorie wählen (Chip-Leiste bzw. Seitenleiste). Alle Pipeline-Analysen - dieser Kategorie erscheinen im Detailbereich (rechts auf Desktop, darunter auf Mobil). - Kategorien kommen aus dem Feld „Kategorie“ beim jeweiligen Prompt im Admin. + Zuerst die Kategorie wählen (Chip-Leiste bzw. Seitenleiste). Alle{' '} + Pipeline- und Workflow-Auswertungen dieser Kategorie erscheinen im Detailbereich + (rechts auf Desktop, darunter auf Mobil). Bei Workflows legst du Kategorie, Titel und Kurztext in der{' '} + Start-Node des Workflow-Editors fest; bei Pipelines im Admin unter KI-Prompts.
Keine aktiven Pipeline-Prompts verfügbar.
+Keine aktiven Pipeline- oder Workflow-Auswertungen verfügbar.
- Erstelle Pipeline-Prompts im Admin-Bereich unter Admin → KI-Prompts. + Pipelines und Workflows werden im Admin unter KI-Prompts bzw. Workflow-Editor angelegt.
+ Titel, Kurzbeschreibung und Kategorie erscheinen auf der Analyse-Seite, im Verlauf und in der + Kategorie-Navigation – analog zu Pipeline-Prompts im Admin. +
+ + + handleNodeUpdate(selectedNode.id, { analysis_title: e.target.value }) + } + placeholder="z. B. Ganzheitliche Auswertung" + style={{ + width: '100%', + padding: '8px', + borderRadius: '4px', + border: '1px solid var(--border)', + background: 'var(--surface)', + color: 'var(--text1)', + fontSize: '14px', + marginBottom: 12, + }} + /> + +