From dc59596f011b477fc2cd3fa01c23369097a61a98 Mon Sep 17 00:00:00 2001 From: Lars Date: Sat, 4 Apr 2026 17:56:00 +0200 Subject: [PATCH] feat: Phase 5 - Visual Workflow Editor (Option B) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend (Mini-Backend 1-2h): - Migration 016: ai_prompts.graph_data JSONB column - workflow_executor: graph_data parameter support (backward-compatible) - prompt_executor: execute_workflow_prompt uses graph_data Frontend (Main effort 25-35h): - WorkflowCanvas: React Flow wrapper component - 5 Custom Nodes: Start, End, Analysis, Logic, Join - 4 Config Panels: QuestionAugmentation, LogicExpression, Fallback, Join - workflowValidation: Structural + logical validation - workflowSerializer: Canvas ↔ JSONB conversion - WorkflowEditorPage: Main orchestration (420 LOC) - Route: /workflow-editor/:id - CSS: workflowEditor.css (300 LOC) Architecture: - Option B: ai_prompts.type='workflow' (not separate table) - panels/ subdirectory for clean separation - WorkflowCanvas reusable component - User GUI identical (Workflows = Prompts) - Backward-compatible (type='pipeline' unchanged) Version: v0.9m → v0.9n (Phase 5 complete) Module: workflow 0.5.0 → 0.6.0 Co-Authored-By: Claude Opus 4.6 --- .../migrations/016_workflows_graph_data.sql | 35 ++ backend/prompt_executor.py | 17 +- backend/workflow_executor.py | 52 +- docs/issues/PHASE_PLAN_RESPONSIVE_UI.md | 264 ++++++++++ docs/issues/REVIEW_OPEN_ISSUES_2026-04-04.md | 22 +- frontend/package-lock.json | 427 ++++++++++++++++ frontend/package.json | 11 +- frontend/src/App.jsx | 2 + .../components/workflow/WorkflowCanvas.jsx | 70 +++ .../workflow/nodes/AnalysisNode.jsx | 51 ++ .../src/components/workflow/nodes/EndNode.jsx | 25 + .../components/workflow/nodes/JoinNode.jsx | 74 +++ .../components/workflow/nodes/LogicNode.jsx | 68 +++ .../components/workflow/nodes/StartNode.jsx | 25 + .../workflow/panels/FallbackConfig.jsx | 70 +++ .../components/workflow/panels/JoinConfig.jsx | 67 +++ .../workflow/panels/LogicExpressionEditor.jsx | 353 +++++++++++++ .../panels/QuestionAugmentationPanel.jsx | 218 ++++++++ frontend/src/pages/WorkflowEditorPage.jsx | 392 +++++++++++++++ frontend/src/styles/workflowEditor.css | 471 ++++++++++++++++++ frontend/src/utils/api.js | 1 + frontend/src/utils/workflowSerializer.js | 117 +++++ frontend/src/utils/workflowValidation.js | 226 +++++++++ 23 files changed, 3010 insertions(+), 48 deletions(-) create mode 100644 backend/migrations/016_workflows_graph_data.sql create mode 100644 docs/issues/PHASE_PLAN_RESPONSIVE_UI.md create mode 100644 frontend/src/components/workflow/WorkflowCanvas.jsx create mode 100644 frontend/src/components/workflow/nodes/AnalysisNode.jsx create mode 100644 frontend/src/components/workflow/nodes/EndNode.jsx create mode 100644 frontend/src/components/workflow/nodes/JoinNode.jsx create mode 100644 frontend/src/components/workflow/nodes/LogicNode.jsx create mode 100644 frontend/src/components/workflow/nodes/StartNode.jsx create mode 100644 frontend/src/components/workflow/panels/FallbackConfig.jsx create mode 100644 frontend/src/components/workflow/panels/JoinConfig.jsx create mode 100644 frontend/src/components/workflow/panels/LogicExpressionEditor.jsx create mode 100644 frontend/src/components/workflow/panels/QuestionAugmentationPanel.jsx create mode 100644 frontend/src/pages/WorkflowEditorPage.jsx create mode 100644 frontend/src/styles/workflowEditor.css create mode 100644 frontend/src/utils/workflowSerializer.js create mode 100644 frontend/src/utils/workflowValidation.js diff --git a/backend/migrations/016_workflows_graph_data.sql b/backend/migrations/016_workflows_graph_data.sql new file mode 100644 index 0000000..4e4f9ee --- /dev/null +++ b/backend/migrations/016_workflows_graph_data.sql @@ -0,0 +1,35 @@ +-- Migration 016: Workflow Support in ai_prompts (Phase 5) +-- Erweitert ai_prompts für type='workflow' (Option B - Backward-kompatibel) + +-- Neue Spalte für Workflow-Graphen +ALTER TABLE ai_prompts ADD COLUMN graph_data JSONB; + +-- Index für Workflow-Queries +CREATE INDEX IF NOT EXISTS idx_ai_prompts_type ON ai_prompts(type); +CREATE INDEX IF NOT EXISTS idx_ai_prompts_graph_data ON ai_prompts USING GIN (graph_data); + +-- Kommentar +COMMENT ON COLUMN ai_prompts.graph_data IS 'Workflow-Graph (nur für type=workflow): {nodes: [...], edges: [...], metadata: {...}}'; + +-- Beispiel-Struktur (Dokumentation): +-- { +-- "nodes": [ +-- {"id": "start", "type": "start", "label": "Start", "position": {"x": 100, "y": 100}}, +-- {"id": "analysis_1", "type": "analysis", "prompt_id": 5, "questions": [...], ...}, +-- {"id": "logic_1", "type": "logic", "condition": {...}, ...}, +-- {"id": "join_1", "type": "join", "join_strategy": "wait_all", ...}, +-- {"id": "end", "type": "end", "label": "Ende", "position": {"x": 500, "y": 300}} +-- ], +-- "edges": [ +-- {"id": "e1", "source": "start", "target": "analysis_1"}, +-- {"id": "e2", "source": "analysis_1", "target": "logic_1"}, +-- {"id": "e3", "source": "logic_1", "target": "join_1"}, +-- {"id": "e4", "source": "join_1", "target": "end"} +-- ], +-- "metadata": { +-- "created_at": "2026-04-04T12:00:00Z", +-- "version": "1.0" +-- } +-- } + +-- Migration erfolgreich diff --git a/backend/prompt_executor.py b/backend/prompt_executor.py index 5352ef7..769a8d9 100644 --- a/backend/prompt_executor.py +++ b/backend/prompt_executor.py @@ -588,11 +588,11 @@ async def execute_workflow_prompt( """ Execute a workflow-type prompt (graph-based execution). - Phase 2: Sequenzielle Workflow-Execution (ohne Logik/Routing) - Phase 3: Conditional branching + Phase 2-4: Sequenzielle Workflow-Execution, conditional branching, path consolidation + Phase 5: Graph aus ai_prompts.graph_data (nicht workflow_definitions) Args: - prompt: Prompt dict from database (must have 'id' field for workflow_id) + prompt: Prompt dict from database (must have 'graph_data' field) variables: Dict of variables for placeholder replacement openrouter_call_func: Async function(prompt_text, model) -> response_text enable_debug: If True, include debug information in response @@ -611,13 +611,14 @@ async def execute_workflow_prompt( """ from workflow_executor import execute_workflow - workflow_id = prompt.get('id') - if not workflow_id: - raise HTTPException(400, "Workflow-Prompt fehlt 'id' Feld") + # Phase 5: Graph aus ai_prompts.graph_data + graph_data = prompt.get('graph_data') + if not graph_data: + raise HTTPException(400, "Workflow-Prompt fehlt 'graph_data' Feld") - # Execute workflow + # Execute workflow (mit graph_data statt workflow_id) result = await execute_workflow( - workflow_id=workflow_id, + graph_data=graph_data, # NEU: Direkt graph_data übergeben profile_id=variables.get('profile_id', 'unknown'), # From context variables=variables, openrouter_call_func=openrouter_call_func, diff --git a/backend/workflow_executor.py b/backend/workflow_executor.py index 511a142..262688a 100644 --- a/backend/workflow_executor.py +++ b/backend/workflow_executor.py @@ -35,10 +35,11 @@ logger = logging.getLogger(__name__) async def execute_workflow( - workflow_id: str, - profile_id: str, - variables: Dict[str, Any], - openrouter_call_func, # Callback für LLM-Calls: async (prompt, model) -> str + workflow_id: Optional[str] = None, + graph_data: Optional[Dict] = None, # Phase 5: Direkt von ai_prompts.graph_data + profile_id: str = None, + variables: Dict[str, Any] = None, + openrouter_call_func = None, # Callback für LLM-Calls: async (prompt, model) -> str enable_debug: bool = False ) -> ExecutionResult: """ @@ -47,9 +48,11 @@ async def execute_workflow( Phase 2: Linear execution in topological order. Phase 3: Conditional branching basierend auf logic nodes. Phase 4: Join nodes und path consolidation. + Phase 5: Unterstützt graph_data direkt (aus ai_prompts, nicht workflow_definitions) Args: - workflow_id: UUID des Workflows + workflow_id: UUID des Workflows (legacy, für workflow_definitions Tabelle) + graph_data: Workflow-Graph als Dict (NEU, für ai_prompts.graph_data) profile_id: UUID des Profils variables: Platzhalter-Werte (z.B. {"name": "Lars", ...}) openrouter_call_func: async (prompt, model) -> str @@ -58,36 +61,41 @@ async def execute_workflow( Returns: ExecutionResult mit allen node_states - Beispiel: + Beispiel (Phase 5): >>> result = await execute_workflow( - ... workflow_id="test-workflow", + ... graph_data={"nodes": [...], "edges": [...]}, ... profile_id="test-profile", ... variables={"name": "Lars"}, ... openrouter_call_func=my_llm_func ... ) - >>> result.status - 'completed' - >>> len(result.node_states) - 3 """ execution_id = str(uuid.uuid4()) started_at = datetime.utcnow().isoformat() - logger.info(f"Starting workflow execution: {execution_id} (workflow: {workflow_id})") + logger.info(f"Starting workflow execution: {execution_id}") try: # 1. Lade Workflow-Definition - with get_db() as conn: - cur = get_cursor(conn) - cur.execute( - "SELECT graph FROM workflow_definitions WHERE id = %s AND active = true", - (workflow_id,) - ) - row = cur.fetchone() - if not row: - raise ValueError(f"Workflow not found: {workflow_id}") + if graph_data: + # Phase 5: Graph direkt aus ai_prompts.graph_data + graph_json = json.dumps(graph_data) + logger.debug(f"Using provided graph_data") + elif workflow_id: + # Phase 0-4: Graph aus workflow_definitions Tabelle (legacy) + with get_db() as conn: + cur = get_cursor(conn) + cur.execute( + "SELECT graph FROM workflow_definitions WHERE id = %s AND active = true", + (workflow_id,) + ) + row = cur.fetchone() + if not row: + raise ValueError(f"Workflow not found: {workflow_id}") - graph_json = row['graph'] + graph_json = row['graph'] + logger.debug(f"Loaded graph from workflow_definitions: {workflow_id}") + else: + raise ValueError("Entweder workflow_id oder graph_data muss übergeben werden") # 2. Parse Graph graph = parse_workflow_graph(graph_json) diff --git a/docs/issues/PHASE_PLAN_RESPONSIVE_UI.md b/docs/issues/PHASE_PLAN_RESPONSIVE_UI.md new file mode 100644 index 0000000..1e60f56 --- /dev/null +++ b/docs/issues/PHASE_PLAN_RESPONSIVE_UI.md @@ -0,0 +1,264 @@ +# Phasenplan: Responsive UI (Desktop Sidebar + Mobile/PWA) + +> **Gitea:** [#30 – Responsive UI](http://192.168.2.144:3000/Lars/mitai-jinkendo/issues/30) +> **Spec:** `.claude/docs/functional/RESPONSIVE_UI.md` +> **Breakpoint:** `<1024px` = Mobile (Bottom-Nav, bestehendes Verhalten), `≥1024px` = Desktop (Sidebar 220px) +> **Letzte Plan-Aktualisierung:** 2026-04-04 + +--- + +## Fortschritt (kurz) + +| Phase | Titel | Status | Datum / Notiz | +|-------|--------|--------|----------------| +| P0 | Vorbereitung & Baseline | ☐ pending | | +| P1 | App-Shell: Sidebar + Breakpoint + gemeinsame Navigation | ☐ pending | | +| P2 | Globales Layout & Content-Bereich (CSS) | ☐ pending | | +| P3 | Dashboard (Desktop-Grid) | ☐ pending | | +| P4 | Verlauf (Tabs links / Content rechts) | ☐ pending | | +| P5 | Analyse (Prompts links / Ergebnis rechts) | ☐ pending | | +| P6 | Erfassung / Capture & Formularseiten | ☐ pending | | +| P7 | Admin & restliche Vollbreiten-Seiten | ☐ pending | | +| P8 | Abschluss, Regression, Spec-Pflege | ☐ pending | | + +**Status-Legende:** `☐ pending` · `◐ in Arbeit` · `☑ erledigt` · `⏸ blockiert` + +--- + +## Testumgebung (für alle Phasen) + +| ID | Umgebung | Verwendung | +|----|-----------|------------| +| T-H1 | Chromium Desktop, Fenster **≥1280px** | Desktop-Layout, Sidebar | +| T-H2 | Chromium, DevTools **375×812** (o. ä.) | Mobile-Layout, Bottom-Nav | +| T-H3 | Chromium, Breite **1023px** vs **1024px** | Breakpoint-Grenze | +| T-H4 | **iPhone** (Safari), installierte **PWA** | Regression Mobile/PWA | +| T-H5 | optional: iPad quer (**≥1024px**) | Desktop-Sidebar auf Tablet quer | + +**Allgemein:** Kein Pflicht-E2E-Framework im Repo; Abnahme primär **manuell** nach Checklisten unten. Optional später: Playwright-Smoke (`viewport`-Wechsel). + +--- + +## Phase P0 – Vorbereitung & Baseline + +### Ziel +Risikoarm starten: Ist-Stand dokumentieren, Spec bereinigen, keine funktionale UI-Änderung. + +### Aufgaben +- [ ] Bekannte Artefakte am Ende von `.claude/docs/functional/RESPONSIVE_UI.md` entfernen (Zeilen `EOF` / `echo …`), falls noch vorhanden. +- [ ] Kurz notieren: aktuelle `app-shell`/`max-width:600px` in `app.css` ist die Mobile-Baseline (siehe Code-Review). + +### Abnahmekriterien +- Spec-Datei endet mit den „Offenen Fragen“; keine Shell-Zeilenduplikate aus Fremdkopien. +- Plan-Datei (dieses Dokument) ist im Repo und verlinkt (optional in Gitea #30 kommentieren). + +### Tests (P0) +| Test | Schritt | Erwartung | +|------|---------|-----------| +| P0-T1 | `RESPONSIVE_UI.md` öffnen | Letzte sinnvolle Zeile ist Frage 4 oder Abschnittsende; kein `echo`/`EOF` | +| P0-T2 | `npm run build` im `frontend/` | Exit 0 | + +--- + +## Phase P1 – App-Shell: Sidebar + Breakpoint + gemeinsame Navigation + +### Ziel +Desktop: feste **Sidebar 220px** links gemäß Spec; Mobile: **unverändert** Bottom-Nav. **Eine** Navigationsdefinition (Routes/Labels/Icons) für beide Darstellungen. Öffentliche Auth-Routen (`/register`, `/verify`, …) **nicht** in Sidebar/Bottom-Nav einbinden (wie heute). + +### Aufgaben (technisch orientierend) +- [ ] Nav-Items aus `App.jsx` (`Nav`-Komponente) in **eine** exportierbare Struktur (z. B. `navItems` Array + `admin`-Eintrag nur bei `role === 'admin'`), von **Desktop-Sidebar** und **Mobile `Nav`** gemeinsam genutzt. +- [ ] Neue Komponente z. B. `DesktopSidebar.jsx` (oder `AppSidebar.jsx`): Logo/Branding, gleiche Links wie Bottom-Nav, unten Nutzerbereich (Avatar, Name/Tier laut Spec – Daten aus `useProfile`/`useAuth` wo möglich). +- [ ] `AppShell`: bei `≥1024px` Sidebar sichtbar, **Bottom-Nav ausgeblendet**; bei `<1024px` **umgekehrt**. +- [ ] Aktiver Route-State: `NavLink`/`active` in Sidebar **gleiche Semantik** wie Bottom-Nav (`end` bei `/` beachten). +- [ ] **Kein** `resize`-Listener pflichtig; primär **CSS** (`@media (min-width: 1024px)`) für Sichtbarkeit; falls nötig nur für Edge-Cases. + +### Abnahmekriterien +- Unter **1024px**: UI wirkt **wie vor P1** (Bottom-Nav sichtbar, Sidebar unsichtbar); PWA-Start auf iPhone unverändert nutzbar. +- Ab **1024px**: Sidebar sichtbar, fixed links, ~220px; Bottom-Nav **nicht** sichtbar. +- Admin-Link nur für Admins in **beiden** Navs. +- Abmelden erreichbar (Spec: im Sidebar-Footer; falls Mobile weiter nur im Header: ** dokumentieren als Abweichung** oder gleich ziehen). + +### Tests (P1) +| Test | Schritt | Erwartung | +|------|---------|-----------| +| P1-T1 | T-H2: einloggen, alle 5 Haupt-Routen antippen | Bottom-Nav aktiv, kein Layout-Bruch | +| P1-T2 | T-H1: gleiche Routen | Nur Sidebar-Navigation sichtbar, korrekte Aktiv-Markierung | +| P1-T3 | T-H3: Breite 1023→1024 wechseln | Sidebar erscheint / Bottom verschwindet ohne Reload; kein „Flackern“-Loop | +| P1-T4 | T-H4: PWA | Login, Hauptflows kurz (Dashboard, Erfassung) – Bottom-Nav ok | +| P1-T5 | Admin-User T-H1 | Admin-Eintrag in Sidebar; Nicht-Admin ohne Admin | + +--- + +## Phase P2 – Globales Layout & Content-Bereich + +### Ziel +Desktop-**Content** rechts von der Sidebar: Spec **margin-left 220px**, Padding **24px 32px**, **max-width 1200px** zentriert im verbleibenden Raum. Mobile: beibehaltene Abstände (**16px**, **80px** bottom für Nav). + +### Aufgaben +- [ ] `app.css`: `.app-shell` **nicht** mehr global `max-width: 600px` für Desktop; Mobile beibehalten oder per Media Query trennen. +- [ ] `.app-main` Padding/Bottom: Mobile `calc(nav + 16px)`; Desktop **ohne** Bottom-Nav-Padding (oder nur safe-area falls nötig). +- [ ] Optional Desktop: **Top-Header** (`app-header`) redundant mit Sidebar-Branding – **vereinheitlichen** (z. B. Header auf Desktop ausblenden oder auf kompakte Toolbar reduzieren), damit keine doppelte Logo-Zeile. + +### Abnahmekriterien +- Desktop: Content nutzt **sichtbar mehr** horizontalen Raum als heute (max 1200px Inhalt, zentriert). +- Mobile: **kein** zusätzliches horizontales Scrollen der Gesamtseite durch P1/P2. +- Keine Überlappung Sidebar/Content. + +### Tests (P2) +| Test | Schritt | Erwartung | +|------|---------|-----------| +| P2-T1 | T-H1: lange Seite scrollen | Nur Main-Area scrollt; Sidebar bleibt sichtbar (fixed) | +| P2-T2 | T-H2 | Weiterhin nutzbarer unterer Rand für Bottom-Nav | +| P2-T3 | T-H1: sehr breiter Monitor | Content max ~1200px, optisch zentriert rechts von Sidebar | + +--- + +## Phase P3 – Dashboard (Desktop-Grid) + +### Ziel +Spec **§5.1**: Desktop mehrspaltige Karten (z. B. 4 Spalten für Kennzahlen-Zeile); Mobile Karten untereinander. + +### Aufgaben +- [ ] `Dashboard.jsx`: Layout in **CSS Grid** o. ä. mit `@media (min-width: 1024px)` für 4-Spalten-Zeile(n) laut Wireframe; Mobile unveränderte Reihenfolge. +- [ ] Charts/„volle Breite“-Blöcke unter den Karten laut Spec. + +### Abnahmekriterien +- Mobile Dashboard **visuell vergleichbar** zum Stand vor P3 (keine funktionalen Regressionen). +- Desktop: klar erkennbares **Grid**; keine übermäßigen Lücken bei typischer Viewport-Höhe. + +### Tests (P3) +| Test | Schritt | Erwartung | +|------|---------|-----------| +| P3-T1 | T-H2 Dashboard | Karten untereinander, lesbar | +| P3-T2 | T-H1 Dashboard | Mehrspaltigkeit wie Spec; kein horizontaler Overflow | +| P3-T3 | Daten mit/ohne Werte | Keine Layout-Crashes (leere Zustände) | + +--- + +## Phase P4 – Verlauf (`History.jsx`) + +### Ziel +Spec **§5.2**: Desktop **Tabs vertikal links**, Chart/Tabelle **rechts** (volle restliche Breite); Mobile Tabs oben wie heute. + +### Aufgaben +- [ ] Tab-Steuerung refaktorieren oder per CSS **ohne** doppelte State-Logik. +- [ ] Desktop: flex/grid `240–280px` Tab-Spalte + flex 1 Chart-Bereich (feinjustierbar). + +### Abnahmekriterien +- Alle bisherigen Verlauf-**Tabs** funktionieren (Gewicht, KF, Umfänge, … – wie im aktuellen Code). +- Mobile UX **unverändert** vom Nutzergefühl her (Tabs oben). +- Desktop: klar **zwei Spalten**; Chart nutzt rechte Fläche. + +### Tests (P4) +| Test | Schritt | Erwartung | +|------|---------|-----------| +| P4-T1 | T-H2: Tab wechseln | Wie bisher | +| P4-T2 | T-H1: jeder Tab | Linke Liste + rechter Chart-Bereich sichtbar | +| P4-T3 | Langes Tab-Label / viele Tabs | Kein Layout-Bruch (Scroll in Tab-Spalte falls nötig) | + +--- + +## Phase P5 – Analyse (`Analysis.jsx`) + +### Ziel +Spec **§5.3**: Desktop **Prompt-Liste links (~300px)**, Ergebnis **rechts**; Mobile untereinander. + +### Aufgaben +- [ ] Layout splitten; Pipeline/Prompt-Bereiche so umbauen, dass Lesbarkeit und Scroll-Verhalten stimmen. + +### Abnahmekriterien +- KI-Ausführung, Platzhalter-Tabelle, Experten-Modus: weiter bedienbar. +- Desktop: Ergebnisbereich hat **deutlich mehr** Breite als Mobile. + +### Tests (P5) +| Test | Schritt | Erwartung | +|------|---------|-----------| +| P5-T1 | T-H2: Analyse ausführen | Flow wie bisher | +| P5-T2 | T-H1: langes Ergebnis | Rechte Spalte scrollbar, linke Prompt-Liste fix oder eigen-scroll | +| P5-T3 | Kleines Fenster knapp unter 1024px | Kein „mittendrin“ kaputtes Layout | + +--- + +## Phase P6 – Erfassung / Capture & Formularseiten + +### Ziel +Spec **§5.4**: Desktop Formulare **zentriert**, **max-width ~600px** im Content-Bereich; Mobile volle Breite wie heute. + +### Aufgaben +- [ ] `CaptureHub` und relevante Seiten (`WeightScreen`, …) unter Desktop-Wrapper; wo sinnvoll **gemeinsame** Klasse `.capture-form-desktop` in `app.css`. + +### Abnahmekriterien +- Mobile: keine Verschlechterung der Eingabe. +- Desktop: Formulare nicht „volle 1200px“, sondern **lesbar begrenzt**. + +### Tests (P6) +| Test | Schritt | Erwartung | +|------|---------|-----------| +| P6-T1 | T-H2: Gewicht erfassen | Vollbreite ok | +| P6-T2 | T-H1: gleiche Seite | Schmale, zentrierte Spalte | +| P6-T3 | Capture-Hub Kacheln | Umbruch auf Desktop ordentlich | + +--- + +## Phase P7 – Admin & übrige Seiten + +### Ziel +Spec **§5.5**: Admin-Tabellen nutzen auf Desktop **mehr Breite**; Mobile weiterhin horizontales Scrollen wo nötig. + +### Aufgaben +- [ ] `Admin*Page.jsx` und ähnliche Tabellen-Seiten: unnötige `max-width`-Erbe von Mobile entfernen; **nicht** jede Admin-Seite einzeln zerstören – priorisiert häufig genutzte. + +### Abnahmekriterien +- Keine regressiven Auth-Schutz-Änderungen. +- Desktop: **mehr sichtbare Spalten** bei typischen Admin-Tabellen. + +### Tests (P7) +| Test | Schritt | Erwartung | +|------|---------|-----------| +| P7-T1 | T-H1: z. B. Admin Training Types | Tabelle nutzt Breite; horizontales Scrollen nur bei Bedarf | +| P7-T2 | T-H2: gleiche Seite | Weiter bedienbar mit Scroll | + +--- + +## Phase P8 – Abschluss, Regression, Dokumentation + +### Ziel +Release-tauglich machen; Spec und Issue aktualisieren. + +### Aufgaben +- [ ] Alle Phasen P1–P7 in Fortschrittstabelle auf ☑ setzen. +- [ ] Vollständiger **Regression-Pass** (Routenliste aus `App.jsx`). +- [ ] Gitea #30: Abschlusskommentar mit **Screenshots** (optional) + Hinweis auf diesen Plan. +- [ ] `REVIEW_OPEN_ISSUES_2026-04-04.md` oder Nachfolger: #30 auf DONE setzen, wenn aus eurer Sicht erledigt. + +### Abnahmekriterien (Gesamt gem. #30 / Spec) +- [ ] Desktop **≥1024px**: Sidebar links, Bottom-Nav aus. +- [ ] Mobile **<1024px**: Bottom-Nav unten, Sidebar aus. +- [ ] Aktive Route in beiden Navs korrekt. +- [ ] Dashboard / Verlauf / Analyse / Erfassung / Admin gemäß Spec umgesetzt. +- [ ] Resize ohne JavaScript-Zwang stabil (CSS-first). +- [ ] **PWA iPhone** weiterhin funktionsfähig (Kernflows). + +### Tests (P8 – Smoke-Checkliste) +| Route | T-H2 | T-H1 | +|-------|------|------| +| `/` | ☐ | ☐ | +| `/capture` | ☐ | ☐ | +| `/history` | ☐ | ☐ | +| `/analysis` | ☐ | ☐ | +| `/settings` | ☐ | ☐ | +| 1× Admin (falls verfügbar) | ☐ | ☐ | +| T-H4 PWA kurz | ☐ | n/a | + +--- + +## Parallelität (Canvas / Workflow) + +- Während **P1–P2** möglichst **keine** parallelenden Änderungen an `App.jsx` / globalem `app.css` ohne Absprache. +- Neue **Workflow-/Canvas-Routen**: nur innerhalb von `
`; Sidebar- und Breakpoint-**Kontrakt** einhalten (`margin-left`, keine zweite globale Spalte). + +--- + +## Planpflege + +Änderungen an Phasen, Kriterien oder Status: **dieses File** anpassen und kurz „Letzte Plan-Aktualisierung“ oben datieren. Bei größeren Scope-Änderungen Gitea #30 kommentieren. diff --git a/docs/issues/REVIEW_OPEN_ISSUES_2026-04-04.md b/docs/issues/REVIEW_OPEN_ISSUES_2026-04-04.md index 7232f69..9f6e98b 100644 --- a/docs/issues/REVIEW_OPEN_ISSUES_2026-04-04.md +++ b/docs/issues/REVIEW_OPEN_ISSUES_2026-04-04.md @@ -25,8 +25,8 @@ | 27 | Korrelationen & Insights | TEILWEISE | C-Charts + offene Data-Layer-TODOs | | 29 | Abilities-Matrix UI | TEILWEISE | Admin/ProfileBuilder, UX offen | | 30 | Responsive UI Sidebar | OFFEN | Weiterhin Bottom-Nav-fokussiert | -| 32 | version.py + `/api/version` | OFFEN | `version.py` ja, dedizierter Endpoint nein | -| 33 | main.py Hardcoded Version | OFFEN | FastAPI `3.0.0`, Root `v9c-dev` | +| 32 | Version-System (inkl. ehem. #33) | OFFEN | Gitea: Body/Titel 2026-04-04 aktualisiert; Runner/Build-Git bewusst später | +| 33 | — | GESCHLOSSEN | In #32 konsolidiert (superseded) | | 34 | External Volumes Doku | PRÜFEN | Gegen Compose abgleichen | | 35 | `subscriptions` Tabelle | PRÜFEN | Schema prüfen | | 36 | BUG Trainingstyp ISE | PRÜFEN | Logs nötig | @@ -117,23 +117,19 @@ **Code-Stand:** Weiterhin stark **Mobile-first** (z. B. `bottom-nav` in `App.jsx`); keine ausgebaute Desktop-Sidebar wie im klassischen Admin-Dashboard. +**Umsetzungsplan:** `docs/issues/PHASE_PLAN_RESPONSIVE_UI.md` (Phasen P0–P8, Abnahmekriterien & Tests, Fortschrittstabelle). + **Vorschlag:** `OFFEN`. --- -### #32 – Version-System (`version.py` + `/api/version`) +### #32 – Version-System (inkl. ehem. #33) -**Code-Stand:** `backend/version.py` existiert mit `APP_VERSION`. **`GET /api/version`** im Backend **nicht** gefunden (Suche nach Route); Root liefert u. a. `"version": "v9c-dev"`. +**Gitea (2026-04-04):** #33 geschlossen; Inhalt in **#32** zusammengeführt (ein Epic: `/api/version`, `version.js`, Settings, `main.py`-Konsistenz). Automatische Git-/Build-Identität über den Runner: **zurückgestellt**, geplant als **separates Issue** nach der Basis. -**Vorschlag:** `OFFEN` für #32 – `/api/version` implementieren oder Issue anpassen („nur version.py ohne Endpoint“). +**Code-Stand:** `backend/version.py` vorhanden; **`GET /api/version`** fehlt; `main.py`: Root `v9c-dev`, `FastAPI(..., version="3.0.0")`. ---- - -### #33 – main.py hardcoded Version entfernen - -**Code-Stand:** `main.py`: `FastAPI(..., version="3.0.0")`; Root-JSON noch `v9c-dev`. - -**Vorschlag:** `OFFEN` – auf `version.py` vereinheitlichen (inkl. FastAPI-`version`-Feld und Health-Payload). +**Vorschlag:** `OFFEN` – Umsetzung laut aktualisiertem Gitea-Issue #32. --- @@ -254,7 +250,7 @@ - [ ] Duplikate schließen und verlinken (#42/#43, #54/#55, #56–#58). - [ ] „DONE?“-Issues manuell testen (`#25`, `#38`, `#40`). - [ ] `#37` umsetzen oder Kommentar „noch offen“ bestätigen. -- [ ] `#32`–`#33` Versionierung planen (ein gemeinsames Mini-Epic). +- [x] `#32`–`#33` in Gitea zusammengeführt (#33 geschlossen); Umsetzung weiter über #32. - [ ] Kommentare aus diesem Dokument kopieren/anpassen. - [ ] Optional: Labels in Gitea setzen (`duplicate`, `blocked`, `needs-retest`). diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 75c2000..6c0c00d 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -15,6 +15,7 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^6.23.1", + "reactflow": "^11.11.4", "recharts": "^2.12.7" }, "devDependencies": { @@ -2059,6 +2060,108 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@reactflow/background": { + "version": "11.3.14", + "resolved": "https://registry.npmjs.org/@reactflow/background/-/background-11.3.14.tgz", + "integrity": "sha512-Gewd7blEVT5Lh6jqrvOgd4G6Qk17eGKQfsDXgyRSqM+CTwDqRldG2LsWN4sNeno6sbqVIC2fZ+rAUBFA9ZEUDA==", + "license": "MIT", + "dependencies": { + "@reactflow/core": "11.11.4", + "classcat": "^5.0.3", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/controls": { + "version": "11.2.14", + "resolved": "https://registry.npmjs.org/@reactflow/controls/-/controls-11.2.14.tgz", + "integrity": "sha512-MiJp5VldFD7FrqaBNIrQ85dxChrG6ivuZ+dcFhPQUwOK3HfYgX2RHdBua+gx+40p5Vw5It3dVNp/my4Z3jF0dw==", + "license": "MIT", + "dependencies": { + "@reactflow/core": "11.11.4", + "classcat": "^5.0.3", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/core": { + "version": "11.11.4", + "resolved": "https://registry.npmjs.org/@reactflow/core/-/core-11.11.4.tgz", + "integrity": "sha512-H4vODklsjAq3AMq6Np4LE12i1I4Ta9PrDHuBR9GmL8uzTt2l2jh4CiQbEMpvMDcp7xi4be0hgXj+Ysodde/i7Q==", + "license": "MIT", + "dependencies": { + "@types/d3": "^7.4.0", + "@types/d3-drag": "^3.0.1", + "@types/d3-selection": "^3.0.3", + "@types/d3-zoom": "^3.0.1", + "classcat": "^5.0.3", + "d3-drag": "^3.0.0", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/minimap": { + "version": "11.7.14", + "resolved": "https://registry.npmjs.org/@reactflow/minimap/-/minimap-11.7.14.tgz", + "integrity": "sha512-mpwLKKrEAofgFJdkhwR5UQ1JYWlcAAL/ZU/bctBkuNTT1yqV+y0buoNVImsRehVYhJwffSWeSHaBR5/GJjlCSQ==", + "license": "MIT", + "dependencies": { + "@reactflow/core": "11.11.4", + "@types/d3-selection": "^3.0.3", + "@types/d3-zoom": "^3.0.1", + "classcat": "^5.0.3", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/node-resizer": { + "version": "2.2.14", + "resolved": "https://registry.npmjs.org/@reactflow/node-resizer/-/node-resizer-2.2.14.tgz", + "integrity": "sha512-fwqnks83jUlYr6OHcdFEedumWKChTHRGw/kbCxj0oqBd+ekfs+SIp4ddyNU0pdx96JIm5iNFS0oNrmEiJbbSaA==", + "license": "MIT", + "dependencies": { + "@reactflow/core": "11.11.4", + "classcat": "^5.0.4", + "d3-drag": "^3.0.0", + "d3-selection": "^3.0.0", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/node-toolbar": { + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@reactflow/node-toolbar/-/node-toolbar-1.3.14.tgz", + "integrity": "sha512-rbynXQnH/xFNu4P9H+hVqlEUafDCkEoCy0Dg9mG22Sg+rY/0ck6KkrAQrYrTgXusd+cEJOMK0uOOFCK2/5rSGQ==", + "license": "MIT", + "dependencies": { + "@reactflow/core": "11.11.4", + "classcat": "^5.0.3", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, "node_modules/@remix-run/router": { "version": "1.23.2", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", @@ -2554,24 +2657,159 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, "node_modules/@types/d3-array": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", "license": "MIT" }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "license": "MIT" + }, "node_modules/@types/d3-color": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", "license": "MIT" }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "license": "MIT" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", + "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "license": "MIT" + }, "node_modules/@types/d3-ease": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", "license": "MIT" }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "license": "MIT", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "license": "MIT" + }, "node_modules/@types/d3-interpolate": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", @@ -2587,6 +2825,24 @@ "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", "license": "MIT" }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "license": "MIT" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "license": "MIT" + }, + "node_modules/@types/d3-random": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", + "license": "MIT" + }, "node_modules/@types/d3-scale": { "version": "4.0.9", "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", @@ -2596,6 +2852,18 @@ "@types/d3-time": "*" } }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", + "license": "MIT" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT" + }, "node_modules/@types/d3-shape": { "version": "3.1.8", "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", @@ -2611,12 +2879,37 @@ "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", "license": "MIT" }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "license": "MIT" + }, "node_modules/@types/d3-timer": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", "license": "MIT" }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -2624,6 +2917,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, "node_modules/@types/raf": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz", @@ -3024,6 +3323,12 @@ "node": ">=10.0.0" } }, + "node_modules/classcat": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", + "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==", + "license": "MIT" + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -3145,6 +3450,28 @@ "node": ">=12" } }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/d3-ease": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", @@ -3200,6 +3527,16 @@ "node": ">=12" } }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "peer": true, + "engines": { + "node": ">=12" + } + }, "node_modules/d3-shape": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", @@ -3245,6 +3582,41 @@ "node": ">=12" } }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -5161,6 +5533,24 @@ "react-dom": ">=16.6.0" } }, + "node_modules/reactflow": { + "version": "11.11.4", + "resolved": "https://registry.npmjs.org/reactflow/-/reactflow-11.11.4.tgz", + "integrity": "sha512-70FOtJkUWH3BAOsN+LU9lCrKoKbtOPnz2uq0CV2PLdNSwxTXOhCbsZr50GmZ+Rtw3jx8Uv7/vBFtCGixLfd4Og==", + "license": "MIT", + "dependencies": { + "@reactflow/background": "11.3.14", + "@reactflow/controls": "11.2.14", + "@reactflow/core": "11.11.4", + "@reactflow/minimap": "11.7.14", + "@reactflow/node-resizer": "2.2.14", + "@reactflow/node-toolbar": "1.3.14" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, "node_modules/recharts": { "version": "2.15.4", "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz", @@ -6187,6 +6577,15 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/utrie": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", @@ -6761,6 +7160,34 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true, "license": "ISC" + }, + "node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } } } } diff --git a/frontend/package.json b/frontend/package.json index b7d31bf..fd1d304 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,14 +9,15 @@ "preview": "vite preview" }, "dependencies": { + "dayjs": "^1.11.11", + "jspdf": "^2.5.1", + "jspdf-autotable": "^3.8.2", + "lucide-react": "^0.383.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^6.23.1", - "recharts": "^2.12.7", - "jspdf": "^2.5.1", - "jspdf-autotable": "^3.8.2", - "dayjs": "^1.11.11", - "lucide-react": "^0.383.0" + "reactflow": "^11.11.4", + "recharts": "^2.12.7" }, "devDependencies": { "@vitejs/plugin-react": "^4.3.0", diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index c75f6cf..5175e20 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -39,6 +39,7 @@ import RestDaysPage from './pages/RestDaysPage' import VitalsPage from './pages/VitalsPage' import GoalsPage from './pages/GoalsPage' import CustomGoalsPage from './pages/CustomGoalsPage' +import WorkflowEditorPage from './pages/WorkflowEditorPage' import './app.css' function Nav() { @@ -194,6 +195,7 @@ function AppShell() { }/> }/> }/> + }/> }/>
diff --git a/frontend/src/components/workflow/WorkflowCanvas.jsx b/frontend/src/components/workflow/WorkflowCanvas.jsx new file mode 100644 index 0000000..91ca62d --- /dev/null +++ b/frontend/src/components/workflow/WorkflowCanvas.jsx @@ -0,0 +1,70 @@ +import ReactFlow, { Background, Controls, MiniMap } from 'reactflow' +import 'reactflow/dist/style.css' + +/** + * WorkflowCanvas - React Flow Wrapper Component + * + * Kapselt React Flow Setup (Background, Controls, MiniMap). + * Separation of Concerns: Canvas-Logik getrennt von Editor-Orchestrierung. + * + * Props: + * - nodes: Array of React Flow nodes + * - edges: Array of React Flow edges + * - nodeTypes: Object mapping node type to component + * - onNodesChange: Handler for node changes (drag, delete, etc.) + * - onEdgesChange: Handler for edge changes + * - onConnect: Handler for new edge connections + * - onNodeClick: Handler for node selection + */ +export function WorkflowCanvas({ + nodes, + edges, + nodeTypes, + onNodesChange, + onEdgesChange, + onConnect, + onNodeClick +}) { + return ( +
+ + + + + +
+ ) +} diff --git a/frontend/src/components/workflow/nodes/AnalysisNode.jsx b/frontend/src/components/workflow/nodes/AnalysisNode.jsx new file mode 100644 index 0000000..550f2f0 --- /dev/null +++ b/frontend/src/components/workflow/nodes/AnalysisNode.jsx @@ -0,0 +1,51 @@ +import { Handle, Position } from 'reactflow' + +/** + * AnalysisNode - KI-Prompt Knoten + * + * Properties: + * - data.label: Node-Label + * - data.prompt_id: ID des referenzierten Basis-Prompts + * - data.prompt_name: Name des Prompts (optional, für Display) + * - data.questions: Array von Question Augmentations + * - selected: Boolean + */ +export function AnalysisNode({ data, selected }) { + const hasQuestions = data.questions?.length > 0 + const promptName = data.prompt_name || (data.prompt_id ? `Prompt #${data.prompt_id}` : 'Kein Prompt') + const questionCount = data.questions?.length || 0 + + return ( +
+
+
🤖
+
{data.label || 'Analyse'}
+
+ +
+
+ {promptName} +
+ + {hasQuestions && ( +
+ 📋 {questionCount} {questionCount === 1 ? 'Frage' : 'Fragen'} +
+ )} +
+ + + +
+ ) +} diff --git a/frontend/src/components/workflow/nodes/EndNode.jsx b/frontend/src/components/workflow/nodes/EndNode.jsx new file mode 100644 index 0000000..2e03bcf --- /dev/null +++ b/frontend/src/components/workflow/nodes/EndNode.jsx @@ -0,0 +1,25 @@ +import { Handle, Position } from 'reactflow' + +/** + * EndNode - Workflow Austritt + * + * Properties: + * - data.label: Node-Label (default: "Ende") + * - selected: Boolean (Node ist ausgewählt) + */ +export function EndNode({ data, selected }) { + return ( +
+
🏁
+
{data.label || 'Ende'}
+ + {/* Nur Target Handle (kein Source, da Endpunkt) */} + +
+ ) +} diff --git a/frontend/src/components/workflow/nodes/JoinNode.jsx b/frontend/src/components/workflow/nodes/JoinNode.jsx new file mode 100644 index 0000000..820dac0 --- /dev/null +++ b/frontend/src/components/workflow/nodes/JoinNode.jsx @@ -0,0 +1,74 @@ +import { Handle, Position } from 'reactflow' + +/** + * JoinNode - Pfad-Konsolidierung + * + * Properties: + * - data.label: Node-Label + * - data.join_strategy: 'wait_all' | 'wait_any' | 'best_effort' + * - data.skip_handling: 'ignore_skipped' | 'use_placeholder' | 'require_minimum' + * - selected: Boolean + */ +export function JoinNode({ data, selected }) { + const strategy = data.join_strategy || 'wait_all' + const skipHandling = data.skip_handling || 'ignore_skipped' + + // Strategy Display Names + const strategyLabels = { + 'wait_all': 'Alle warten', + 'wait_any': 'Beliebig', + 'best_effort': 'Best Effort' + } + + const skipLabels = { + 'ignore_skipped': 'Ignorieren', + 'use_placeholder': 'Platzhalter', + 'require_minimum': 'Minimum' + } + + return ( +
+
+
🔀
+
{data.label || 'Join'}
+
+ +
+
+ Strategie: {strategyLabels[strategy] || strategy} +
+
+ Skip: {skipLabels[skipHandling] || skipHandling} +
+
+ + {/* Mehrere Target Handles für eingehende Pfade */} + + + + + {/* Ein Source Handle für konsolidierten Ausgang */} + +
+ ) +} diff --git a/frontend/src/components/workflow/nodes/LogicNode.jsx b/frontend/src/components/workflow/nodes/LogicNode.jsx new file mode 100644 index 0000000..617ffda --- /dev/null +++ b/frontend/src/components/workflow/nodes/LogicNode.jsx @@ -0,0 +1,68 @@ +import { Handle, Position } from 'reactflow' + +/** + * LogicNode - Bedingungs-Knoten (If-Then-Else) + * + * Properties: + * - data.label: Node-Label + * - data.condition: Logic Expression Object + * - selected: Boolean + */ +export function LogicNode({ data, selected }) { + const hasCondition = !!data.condition && !!data.condition.operator + + // Condition Summary (vereinfacht für Display) + const getConditionSummary = () => { + if (!hasCondition) return 'Keine Bedingung' + + const op = data.condition.operator?.toUpperCase() + const operandCount = data.condition.operands?.length || 0 + + if (op === 'AND' || op === 'OR' || op === 'NOT') { + return `${op} (${operandCount} Bedingungen)` + } + + // Simple condition (ref, operator, value) + if (data.condition.ref) { + return `${data.condition.ref} ${data.condition.operator} ...` + } + + return 'Bedingung definiert' + } + + return ( +
+
+
+
{data.label || 'Logik'}
+
+ +
+
+ {getConditionSummary()} +
+
+ + + + {/* Zwei Source Handles für True/False Pfade */} + + +
+ ) +} diff --git a/frontend/src/components/workflow/nodes/StartNode.jsx b/frontend/src/components/workflow/nodes/StartNode.jsx new file mode 100644 index 0000000..327475c --- /dev/null +++ b/frontend/src/components/workflow/nodes/StartNode.jsx @@ -0,0 +1,25 @@ +import { Handle, Position } from 'reactflow' + +/** + * StartNode - Workflow Einstiegspunkt + * + * Properties: + * - data.label: Node-Label (default: "Start") + * - selected: Boolean (Node ist ausgewählt) + */ +export function StartNode({ data, selected }) { + return ( +
+
🚀
+
{data.label || 'Start'}
+ + {/* Nur Source Handle (kein Target, da Einstiegspunkt) */} + +
+ ) +} diff --git a/frontend/src/components/workflow/panels/FallbackConfig.jsx b/frontend/src/components/workflow/panels/FallbackConfig.jsx new file mode 100644 index 0000000..a1bf5b9 --- /dev/null +++ b/frontend/src/components/workflow/panels/FallbackConfig.jsx @@ -0,0 +1,70 @@ +/** + * FallbackConfig - Fallback-Strategie Konfiguration + * + * Props: + * - node: React Flow Node object + * - edges: Array of React Flow edges + * - onChange: (nodeId, updates) => void + */ +export function FallbackConfig({ node, edges, onChange }) { + const fallbackStrategy = node.data.fallback_strategy || 'conservative_skip' + const fallbackEdge = node.data.fallback_edge || null + + // Outgoing Edges von diesem Node + const outgoingEdges = edges.filter(e => e.source === node.id) + + const handleStrategyChange = (e) => { + const strategy = e.target.value + onChange(node.id, { fallback_strategy: strategy }) + + // Reset fallback_edge wenn strategy geändert wird + if (strategy === 'conservative_skip' || strategy === 'document_only') { + onChange(node.id, { fallback_edge: null }) + } + } + + const handleEdgeChange = (e) => { + onChange(node.id, { fallback_edge: e.target.value || null }) + } + + return ( +
+

Fallback-Strategie

+ + + + +
+ {fallbackStrategy === 'conservative_skip' && 'Bei Unklarheit: Pfad nicht routen.'} + {fallbackStrategy === 'default_path' && 'Bei Unklarheit: Definierter Standardpfad wird ausgeführt.'} + {fallbackStrategy === 'uncertainty_path' && 'Bei Unklarheit: Expliziter Klärungspfad.'} + {fallbackStrategy === 'document_only' && 'Unklarheit dokumentieren, aber kein Routing.'} +
+ + {(fallbackStrategy === 'default_path' || fallbackStrategy === 'uncertainty_path') && ( + <> + + + + {outgoingEdges.length === 0 && ( +
+ ⚠️ Keine ausgehenden Kanten gefunden. Bitte verbinden Sie diesen Node zuerst. +
+ )} + + )} +
+ ) +} diff --git a/frontend/src/components/workflow/panels/JoinConfig.jsx b/frontend/src/components/workflow/panels/JoinConfig.jsx new file mode 100644 index 0000000..c954e43 --- /dev/null +++ b/frontend/src/components/workflow/panels/JoinConfig.jsx @@ -0,0 +1,67 @@ +/** + * JoinConfig - Konfiguration für Join Nodes + * + * Props: + * - node: React Flow Node object (type='join') + * - onChange: (nodeId, updates) => void + */ +export function JoinConfig({ node, onChange }) { + const joinStrategy = node.data.join_strategy || 'wait_all' + const skipHandling = node.data.skip_handling || 'ignore_skipped' + const minPaths = node.data.min_paths || 2 + + const handleStrategyChange = (e) => { + onChange(node.id, { join_strategy: e.target.value }) + } + + const handleSkipChange = (e) => { + onChange(node.id, { skip_handling: e.target.value }) + } + + const handleMinPathsChange = (e) => { + const value = parseInt(e.target.value) || 2 + onChange(node.id, { min_paths: value }) + } + + return ( +
+

Join-Konfiguration

+ + + + +
+ {joinStrategy === 'wait_all' && 'Wartet auf alle eingehenden Pfade. Fehler wenn einer fehlt.'} + {joinStrategy === 'wait_any' && 'Wartet auf mindestens einen Pfad. Erste verfügbare Ausführung.'} + {joinStrategy === 'best_effort' && 'Fehlertoleranz: Nutzt verfügbare Pfade, auch wenn nicht alle da sind.'} +
+ + + + + {skipHandling === 'require_minimum' && ( + <> + + + + )} + +
+ 💡 Phase 4: Path Consolidation +
+
+ ) +} diff --git a/frontend/src/components/workflow/panels/LogicExpressionEditor.jsx b/frontend/src/components/workflow/panels/LogicExpressionEditor.jsx new file mode 100644 index 0000000..986b1e5 --- /dev/null +++ b/frontend/src/components/workflow/panels/LogicExpressionEditor.jsx @@ -0,0 +1,353 @@ +import { useState, useEffect, useMemo } from 'react' + +/** + * LogicExpressionEditor - Visueller Baukasten für Logik-Bedingungen + * + * KEIN JSON-Editor! Visuelles Drag & Drop Interface. + * + * Props: + * - node: React Flow Node object (type='logic') + * - nodes: Array of all nodes (for signal reference lookup) + * - edges: Array of all edges (for topological sort) + * - onChange: (nodeId, updates) => void + */ +export function LogicExpressionEditor({ node, nodes, edges, onChange }) { + const [expression, setExpression] = useState(node.data.condition || { + operator: 'and', + operands: [] + }) + + // Verfügbare Signale aus vorangegangenen Nodes + const availableSignals = useMemo(() => { + return getAvailableSignals(node.id, nodes, edges) + }, [node.id, nodes, edges]) + + // Sync to parent when expression changes + useEffect(() => { + onChange(node.id, { condition: expression }) + }, [expression]) + + const handleOperatorChange = (e) => { + setExpression({ ...expression, operator: e.target.value }) + } + + const handleAddCondition = () => { + setExpression({ + ...expression, + operands: [...(expression.operands || []), { + ref: '', + operator: 'eq', + value: '' + }] + }) + } + + const handleAddGroup = () => { + setExpression({ + ...expression, + operands: [...(expression.operands || []), { + operator: 'and', + operands: [] + }] + }) + } + + const handleOperandChange = (index, updates) => { + const updated = [...(expression.operands || [])] + updated[index] = { ...updated[index], ...updates } + setExpression({ ...expression, operands: updated }) + } + + const handleRemoveOperand = (index) => { + setExpression({ + ...expression, + operands: (expression.operands || []).filter((_, i) => i !== index) + }) + } + + return ( +
+

Logik-Bedingung

+ + {availableSignals.length === 0 && ( +
+ ⚠️ Keine Signale verfügbar. Fügen Sie zuerst einen Analysis-Node VOR diesem Node hinzu. +
+ )} + +
+ + +
+ +
+ {(expression.operands || []).map((operand, idx) => ( + handleOperandChange(idx, updates)} + onRemove={() => handleRemoveOperand(idx)} + /> + ))} + + {(!expression.operands || expression.operands.length === 0) && ( +
+ Keine Bedingungen definiert. Fügen Sie Bedingungen oder Gruppen hinzu. +
+ )} +
+ +
+ + +
+ +
+ 💡 Signale: {availableSignals.length} verfügbar +
+
+ ) +} + +/** + * ConditionBlock - Einzelne Bedingung oder Gruppe (rekursiv) + */ +function ConditionBlock({ operand, availableSignals, onChange, onRemove }) { + // Verschachtelte Gruppe? (AND/OR/NOT) + const isGroup = operand.operator === 'and' || operand.operator === 'or' || operand.operator === 'not' + + if (isGroup) { + return ( +
+
+ + +
+ + {/* Rekursiv: Nested operands */} +
+ {(operand.operands || []).map((subOp, idx) => ( + { + const updated = [...(operand.operands || [])] + updated[idx] = { ...updated[idx], ...updates } + onChange({ operands: updated }) + }} + onRemove={() => { + onChange({ operands: (operand.operands || []).filter((_, i) => i !== idx) }) + }} + /> + ))} + + +
+
+ ) + } + + // Einfache Bedingung + const selectedSignal = availableSignals.find(s => s.ref === operand.ref) + + return ( +
+ {/* Signal-Referenz (Dropdown) */} + + + {/* Operator */} + + + {/* Wert (abhängig von Operator) */} + {(operand.operator === 'in' || operand.operator === 'not_in') ? ( + onChange({ value: val })} + placeholder="Werte wählen" + /> + ) : ( + onChange({ value: e.target.value })} + placeholder="Wert" + className="value-input" + style={{ flex: 1 }} + /> + )} + + +
+ ) +} + +/** + * MultiSelect - Multi-Auswahl für IN/NOT_IN Operatoren + */ +function MultiSelect({ options, value, onChange, placeholder }) { + const [isOpen, setIsOpen] = useState(false) + const selected = Array.isArray(value) ? value : [] + + const handleToggle = (option) => { + if (selected.includes(option)) { + onChange(selected.filter(v => v !== option)) + } else { + onChange([...selected, option]) + } + } + + return ( +
+
setIsOpen(!isOpen)} + style={{ + padding: '6px', + border: '1px solid var(--border)', + borderRadius: '4px', + cursor: 'pointer', + fontSize: '12px', + background: 'var(--bg)' + }} + > + {selected.length > 0 ? selected.join(', ') : placeholder} +
+ + {isOpen && ( +
+ {options.map((option, i) => ( +
handleToggle(option)} + style={{ + padding: '8px', + cursor: 'pointer', + fontSize: '12px', + background: selected.includes(option) ? 'var(--accent)' : 'transparent', + color: selected.includes(option) ? 'white' : 'var(--text1)' + }} + > + {selected.includes(option) && '✓ '} + {option} +
+ ))} + + {options.length === 0 && ( +
+ Keine Optionen verfügbar +
+ )} +
+ )} +
+ ) +} + +/** + * Helper: Verfügbare Signale aus vorangegangenen Nodes extrahieren + */ +function getAvailableSignals(nodeId, nodes, edges) { + // Topologische Sortierung: Alle Nodes VOR diesem Node + const predecessors = new Set() + + function collectPredecessors(currentId) { + const incoming = edges.filter(e => e.target === currentId) + for (const edge of incoming) { + if (!predecessors.has(edge.source)) { + predecessors.add(edge.source) + collectPredecessors(edge.source) + } + } + } + + collectPredecessors(nodeId) + + // Signale extrahieren (nur von ANALYSIS Nodes) + const signals = [] + for (const predId of predecessors) { + const predNode = nodes.find(n => n.id === predId) + if (predNode && predNode.type === 'analysis') { + const questions = predNode.data.questions || [] + for (const q of questions) { + signals.push({ + ref: `${predId}.${q.id}`, + label: `${predNode.data.label} → ${q.question || q.id}`, + spectrum: q.answer_spectrum || [] + }) + } + } + } + + return signals +} diff --git a/frontend/src/components/workflow/panels/QuestionAugmentationPanel.jsx b/frontend/src/components/workflow/panels/QuestionAugmentationPanel.jsx new file mode 100644 index 0000000..2def832 --- /dev/null +++ b/frontend/src/components/workflow/panels/QuestionAugmentationPanel.jsx @@ -0,0 +1,218 @@ +import { useState, useEffect } from 'react' + +/** + * QuestionAugmentationPanel - Fragenergänzungs-Konfiguration + * + * Props: + * - node: React Flow Node object (type='analysis') + * - onChange: (nodeId, updates) => void + */ +export function QuestionAugmentationPanel({ node, onChange }) { + const [questions, setQuestions] = useState(node.data.questions || []) + + // Sync to parent when questions change + useEffect(() => { + onChange(node.id, { questions }) + }, [questions]) + + const handleAddQuestion = () => { + const newQuestion = { + id: `q${Date.now()}`, + type: 'relevanz', + question: '', + answer_spectrum: [] + } + setQuestions([...questions, newQuestion]) + } + + const handleRemoveQuestion = (index) => { + setQuestions(questions.filter((_, i) => i !== index)) + } + + const handleQuestionChange = (index, field, value) => { + const updated = [...questions] + updated[index] = { ...updated[index], [field]: value } + setQuestions(updated) + } + + return ( +
+

Fragenergänzung

+ + {questions.length === 0 && ( +
+ Keine Fragen definiert. Fügen Sie Fragen hinzu, um Signale für Logik-Knoten zu erzeugen. +
+ )} + + {questions.map((q, idx) => ( + handleQuestionChange(idx, field, value)} + onRemove={() => handleRemoveQuestion(idx)} + /> + ))} + + +
+ ) +} + +/** + * QuestionEditor - Einzelne Frage editieren + */ +function QuestionEditor({ question, index, onChange, onRemove }) { + const [spectrumInput, setSpectrumInput] = useState('') + + const handleAddAnswer = () => { + if (!spectrumInput.trim()) return + + const newSpectrum = [...(question.answer_spectrum || []), spectrumInput.trim()] + onChange('answer_spectrum', newSpectrum) + setSpectrumInput('') + } + + const handleRemoveAnswer = (answer) => { + const newSpectrum = question.answer_spectrum.filter(a => a !== answer) + onChange('answer_spectrum', newSpectrum) + } + + const handleKeyPress = (e) => { + if (e.key === 'Enter') { + e.preventDefault() + handleAddAnswer() + } + } + + return ( +
+
+ Frage {index + 1} + +
+ + + onChange('id', e.target.value)} + placeholder="z.B. relevanz" + /> + + + + + +