{
"tool_response": {
"filePath": "\/var\/www\/dev.campus.systemische-tools.de\/docs\/plan-form-components-refactoring.md",
"oldString": "15. [ ] Semantic Explorer auf Repository umstellen\n\n---\n\n## 0d. User-Präferenzen (Zentrale Defaults)",
"newString": "15. [ ] Semantic Explorer auf Repository umstellen\n\n### 0b.11 KRITISCH: Embedding-Dimension Validierung\n\n> **Problem:** Bei Multi-Collection-Suche können Collections mit unterschiedlichen\n> Embedding-Modellen (z.B. 1024-dim vs 384-dim) kombiniert werden → Qdrant-Fehler.\n>\n> **Lösung:** Validierung VOR Suche - sowohl bei Collection-Auswahl als auch bei Suche.\n\n```php\n\/\/ ══════════════════════════════════════════════════════════════════\n\/\/ CollectionValidator.php (NEU)\n\/\/ ══════════════════════════════════════════════════════════════════\n\nnamespace Infrastructure\\Validation;\n\nuse Infrastructure\\Persistence\\CollectionRepository;\n\nclass CollectionValidator\n{\n public function __construct(\n private CollectionRepository $repository\n ) {}\n\n \/**\n * Prüft ob alle ausgewählten Collections kompatible Vektorgrößen haben\n *\n * @param array $collectionIds Array von Collection-IDs\n * @return ValidationResult\n *\/\n public function validateSelection(array $collectionIds): ValidationResult\n {\n if (count($collectionIds) === 0) {\n return ValidationResult::ok();\n }\n\n $collections = $this->repository->findByIds($collectionIds);\n $vectorSizes = array_unique(array_column($collections, 'vector_size'));\n\n if (count($vectorSizes) > 1) {\n $details = [];\n foreach ($collections as $col) {\n $details[] = \"{$col['display_name']}: {$col['vector_size']}d\";\n }\n return ValidationResult::error(\n 'Inkompatible Collections: Unterschiedliche Vektorgrößen ('\n . implode(', ', $details) . ')'\n );\n }\n\n return ValidationResult::ok($vectorSizes[0] ?? null);\n }\n\n \/**\n * Prüft ob ein Embedding-Vektor zur Collection passt\n *\/\n public function validateEmbedding(string $collectionId, array $embedding): ValidationResult\n {\n $collection = $this->repository->find($collectionId);\n\n if ($collection === null) {\n return ValidationResult::error(\"Collection '$collectionId' nicht gefunden\");\n }\n\n $expectedSize = (int) $collection['vector_size'];\n $actualSize = count($embedding);\n\n if ($expectedSize !== $actualSize) {\n return ValidationResult::error(\n \"Dimension Mismatch: Collection erwartet {$expectedSize}d, \"\n . \"Embedding hat {$actualSize}d\"\n );\n }\n\n return ValidationResult::ok();\n }\n}\n\n\/\/ ══════════════════════════════════════════════════════════════════\n\/\/ ValidationResult.php (NEU)\n\/\/ ══════════════════════════════════════════════════════════════════\n\nnamespace Infrastructure\\Validation;\n\nclass ValidationResult\n{\n private function __construct(\n public readonly bool $valid,\n public readonly ?string $error,\n public readonly mixed $data = null\n ) {}\n\n public static function ok(mixed $data = null): self\n {\n return new self(true, null, $data);\n }\n\n public static function error(string $message): self\n {\n return new self(false, $message);\n }\n\n public function isValid(): bool\n {\n return $this->valid;\n }\n}\n```\n\n**Integration in QdrantService:**\n\n```php\n\/\/ QdrantService.php - Ergänzung zu search()\npublic function searchMultiple(\n array $collectionIds,\n array $embedding\n): array {\n \/\/ Dimension-Check vor Suche\n $validator = new CollectionValidator($this->repository);\n $result = $validator->validateSelection($collectionIds);\n\n if (!$result->isValid()) {\n throw new DimensionMismatchException($result->error);\n }\n\n \/\/ Zusätzlich: Embedding-Dimension gegen erwartete Größe prüfen\n $expectedSize = $result->data; \/\/ von validateSelection zurückgegeben\n if ($expectedSize !== null && count($embedding) !== $expectedSize) {\n throw new DimensionMismatchException(\n \"Embedding hat \" . count($embedding) . \"d, \"\n . \"Collections erwarten {$expectedSize}d\"\n );\n }\n\n \/\/ Suche in allen Collections\n $allResults = [];\n foreach ($collectionIds as $collectionId) {\n $results = $this->search($embedding, $collectionId);\n $allResults = array_merge($allResults, $results);\n }\n\n \/\/ Nach Score sortieren\n usort($allResults, fn($a, $b) => $b['score'] <=> $a['score']);\n\n return $allResults;\n}\n```\n\n**UI-Feedback bei Validierungsfehler:**\n\n```php\n\/\/ ChatController.php - Im send() vor RAG-Suche\n$collections = json_decode($session['collections'], true);\n$validator = new CollectionValidator($this->collectionRepo);\n$validation = $validator->validateSelection($collections);\n\nif (!$validation->isValid()) {\n return $this->jsonError([\n 'error' => 'Collection-Konfiguration ungültig',\n 'details' => $validation->error,\n 'hint' => 'Bitte wähle nur Collections mit gleichem Embedding-Modell'\n ]);\n}\n```\n\n**Frontend-Warnung (optional, Phase 2):**\n\n```javascript\n\/\/ Bei Collection-Änderung im Multi-Select\ndocument.querySelectorAll('.collections-select').forEach(select => {\n select.addEventListener('change', async (e) => {\n const selected = Array.from(e.target.selectedOptions).map(o => o.value);\n if (selected.length > 1) {\n const resp = await fetch('\/api\/collections\/validate', {\n method: 'POST',\n body: JSON.stringify({ collections: selected })\n });\n const result = await resp.json();\n if (!result.valid) {\n showWarning(result.error);\n }\n }\n });\n});\n```\n\n**Migrations-Schritte:**\n\n16. [ ] `ValidationResult.php` erstellen\n17. [ ] `CollectionValidator.php` erstellen\n18. [ ] `DimensionMismatchException.php` erstellen\n19. [ ] `QdrantService::searchMultiple()` implementieren\n20. [ ] `ChatController::send()` Validierung ergänzen\n21. [ ] `ContentController::generate()` Validierung ergänzen\n\n---\n\n## 0d. User-Präferenzen (Zentrale Defaults)",
"originalFile": "# Plan: Einheitliche Form-Komponenten (Chat & Content Studio)\n\n## Status: PLANUNG\n**Erstellt:** 2025-12-21\n**Ziel:** DRY, KISS, SRP - Shared Partials für alle wiederverwendbaren Form-Elemente\n\n---\n\n## Projekt-Kontext (für externe Supervision)\n\n### Was ist das System?\n\n**Campus.systemische-tools.de** ist ein KI-gestütztes Content-Management-System mit zwei Hauptmodulen:\n\n| Modul | Beschreibung | Hauptfunktionen |\n|-------|--------------|-----------------|\n| **Chat-Interface** | RAG-basierte Konversation mit verschiedenen LLMs | Session-Management, Multi-Collection-Suche, Streaming-Responses |\n| **Content Studio** | Automatisierte Content-Generierung mit Qualitätssicherung | Briefing → Generierung → Kritik → Revision → Freigabe |\n\n### Warum dieses Refactoring?\n\n**Problem:**\n- Chat und Content Studio wurden unabhängig entwickelt\n- Gleiche UI-Elemente (Modell-Dropdown, Collections, etc.) existieren in unterschiedlichen Implementierungen\n- Inkonsistente UX: Modell-Dropdown mit\/ohne Optgroups, Collections als Multi-Select\/Checkboxes\/Single-Select\n\n**Symptome:**\n- Doppelter Code in Views\n- Unterschiedliche Variablennamen (`$profiles` vs `$authorProfiles`)\n- Keine zentrale Verwaltung für LLM-Modelle und Collections\n- Keine persistenten User-Präferenzen\n\n**Ziel:**\n- Einheitliche Partials für alle Form-Elemente\n- Zentrale Admin-Seiten für LLM-Modelle und Collections\n- User-Präferenzen mit 3-stufiger Werte-Hierarchie\n- Contracts und Structures in beiden Systemen nutzbar (Multi-Select)\n\n### Technologie-Stack\n\n| Schicht | Technologie | Details |\n|---------|-------------|---------|\n| **Backend** | PHP 8.2 | MVC-Pattern in `\/src\/` |\n| **Frontend** | HTML + HTMX | Reactive ohne SPA-Framework |\n| **Datenbank** | MariaDB 10.11 | Zwei Datenbanken (siehe unten) |\n| **Vector-DB** | Qdrant | RAG-Embeddings, Collections |\n| **LLM-APIs** | Anthropic Claude, Ollama | Cloud + lokal |\n| **Pipeline** | Python 3.11 | Content-Generierung, Embedding |\n| **Styling** | CSS Custom Properties | Theme-Support |\n\n### Datenbank-Architektur\n\n```\n┌─────────────────────────────────────────────────────────────────┐\n│ MariaDB │\n├─────────────────────────────┬───────────────────────────────────┤\n│ ki_dev │ ki_content │\n│ (Infrastruktur\/Config) │ (Laufzeitdaten) │\n├─────────────────────────────┼───────────────────────────────────┤\n│ • llm_models [NEU] │ • chat_sessions [ERWEITERT] │\n│ • llm_presets [NEU] │ • chat_messages │\n│ • rag_collections [NEU] │ • content_orders [MIGRIERT] │\n│ • user_preferences [NEU] │ • content_versions │\n│ • content_config │ • content_critiques │\n│ • tasks │ • content_sources │\n│ • contracts │ │\n│ • dokumentation │ │\n└─────────────────────────────┴───────────────────────────────────┘\n```\n\n### Externe Abhängigkeiten\n\n| Dienst | Verwendung | Ausfallverhalten |\n|--------|------------|------------------|\n| **Anthropic API** | Claude-Modelle, Chat-Completion | Fallback auf Ollama |\n| **Ollama (lokal)** | Lokale Modelle, Embeddings | Pflichtkomponente |\n| **Qdrant (lokal)** | Vektor-Suche, Collection-Metadaten | Pflichtkomponente |\n\n### Architektur-Prinzipien (angewendet)\n\n| Prinzip | Anwendung in diesem Plan |\n|---------|--------------------------|\n| **DRY** | Partials als Single Source für Form-Elemente |\n| **SRP** | Jedes Partial = eine Aufgabe |\n| **KISS** | Context Limit hardcoded, keine Over-Engineering |\n| **YAGNI** | Section 8 definiert klare Grenzen |\n| **MVC** | Controller → View (Partials) → Repository |\n\n### Wichtige Konventionen\n\n1. **MCP-Server:** Alle DB-Zugriffe via MCP-Tools (kein direktes SQL)\n2. **Kein Git:** Server ohne Versionskontrolle\n3. **Dev-First:** Entwicklung auf dev.campus, dann Sync zu prod\n4. **Quality-Gate:** `\/opt\/scripts\/php-check.sh` muss vor Sync bestehen\n\n---\n\n## 0. LLM-Verwaltung (Grundlage für Modell-Dropdown)\n\n### 0.1 Architektur-Entscheidung\n\n**Gewählt: Variante B - Dedizierte LLM-Verwaltung**\n\nStatt dynamischer API-Abfrage bei jedem Seitenaufruf:\n- Zentrale Admin-Seite `\/ressourcen\/llm`\n- Datenbank-Tabelle als Single Source of Truth\n- Sync-Buttons für Provider (Anthropic, Ollama, weitere)\n- Sprechende Namen und Zusatzmetadaten\n\n### 0.2 Datenbank-Tabelle `llm_models`\n\n```sql\nCREATE TABLE llm_models (\n id INT AUTO_INCREMENT PRIMARY KEY,\n provider ENUM('anthropic', 'ollama', 'openai', 'google', 'mistral', 'custom') NOT NULL,\n model_id VARCHAR(100) NOT NULL, -- API-ID: \"claude-opus-4-5-20251101\"\n display_name VARCHAR(100) NOT NULL, -- Anzeige: \"Claude Opus 4.5\"\n description TEXT, -- Kurzbeschreibung\n context_window INT, -- Max. Tokens Input: 200000\n max_output_tokens INT, -- Max. Tokens Output: 8192\n input_price_per_mtok DECIMAL(10,4), -- Preis Input $\/MTok\n output_price_per_mtok DECIMAL(10,4), -- Preis Output $\/MTok\n capabilities JSON, -- {\"vision\": true, \"function_calling\": true}\n is_local BOOLEAN DEFAULT FALSE, -- Lokal (Ollama) vs. Cloud\n is_active BOOLEAN DEFAULT TRUE, -- In Dropdowns anzeigen?\n sort_order INT DEFAULT 0, -- Reihenfolge in Dropdowns\n last_synced_at DATETIME, -- Letzte Synchronisation\n created_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,\n UNIQUE KEY unique_provider_model (provider, model_id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;\n```\n\n### 0.3 Admin-Seite `\/ressourcen\/llm`\n\n**URL:** `\/ressourcen\/llm`\n**Controller:** `RessourcenController::llmIndex()`\n\n**Funktionen:**\n\n| Button | Aktion |\n|--------|--------|\n| \"Anthropic synchronisieren\" | `GET api.anthropic.com\/v1\/models` → DB aktualisieren |\n| \"Ollama synchronisieren\" | `ollama list` → DB aktualisieren |\n| \"Neuer Provider\" | Manuell weiteren Provider hinzufügen |\n\n**Tabellen-Ansicht:**\n\n| Provider | Model-ID | Display-Name | Context | Preis In\/Out | Lokal | Aktiv | Aktionen |\n|----------|----------|--------------|---------|--------------|-------|-------|----------|\n| Anthropic | claude-opus-4-5-20251101 | Claude Opus 4.5 | 200k | $15\/$75 | - | ✓ | Bearbeiten |\n| Ollama | mistral:latest | Mistral 7B | 32k | - | ✓ | ✓ | Bearbeiten |\n\n**Bearbeiten-Dialog:**\n- Display-Name ändern\n- Beschreibung hinzufügen\n- Context-Window \/ Max-Output korrigieren\n- Preise eintragen (für Kostenberechnung)\n- Aktivieren\/Deaktivieren\n- Sortierung ändern\n\n### 0.4 Sync-Logik\n\n**Anthropic Sync:**\n```php\n\/\/ GET https:\/\/api.anthropic.com\/v1\/models\n\/\/ Response: {\"data\": [{\"id\": \"claude-opus-4-5-20251101\", ...}]}\n\nforeach ($apiModels as $model) {\n \/\/ INSERT ... ON DUPLICATE KEY UPDATE\n \/\/ Neue Modelle: is_active = true, display_name = model_id (initial)\n \/\/ Existierende: last_synced_at aktualisieren\n \/\/ Fehlende: NICHT löschen, nur last_synced_at bleibt alt\n}\n```\n\n**Ollama Sync:**\n```php\n\/\/ $ ollama list\n\/\/ NAME ID SIZE MODIFIED\n\/\/ mistral:latest abc123... 4.1 GB 2 days ago\n\n$output = shell_exec('ollama list');\n\/\/ Parsen und in DB einfügen\n```\n\n### 0.5 Dropdown-Query\n\n```php\n\/\/ ModelService::getActiveModels()\npublic function getActiveModels(): array\n{\n return $this->db->query(\"\n SELECT model_id, display_name, provider, is_local, context_window\n FROM llm_models\n WHERE is_active = 1\n ORDER BY is_local ASC, sort_order ASC, display_name ASC\n \")->fetchAll();\n}\n```\n\n### 0.6 Partial nutzt DB-Daten\n\n```php\n\/\/ \/src\/View\/partials\/form\/model-select.php\n<?php\n$models = $models ?? [];\n$selected = $selected ?? '';\n$variant = $variant ?? 'default';\n$class = $variant === 'inline' ? 'form-select--inline' : 'form-select';\n?>\n<select name=\"model\" id=\"model\" class=\"<?= $class ?>\">\n <optgroup label=\"Cloud\">\n <?php foreach ($models as $m): ?>\n <?php if (!$m['is_local']): ?>\n <option value=\"<?= $m['model_id'] ?>\" <?= $selected === $m['model_id'] ? 'selected' : '' ?>>\n <?= htmlspecialchars($m['display_name']) ?>\n <\/option>\n <?php endif; ?>\n <?php endforeach; ?>\n <\/optgroup>\n <optgroup label=\"Lokal\">\n <?php foreach ($models as $m): ?>\n <?php if ($m['is_local']): ?>\n <option value=\"<?= $m['model_id'] ?>\" <?= $selected === $m['model_id'] ? 'selected' : '' ?>>\n <?= htmlspecialchars($m['display_name']) ?>\n <\/option>\n <?php endif; ?>\n <?php endforeach; ?>\n <\/optgroup>\n<\/select>\n```\n\n### 0.7 Migrations-Schritte (LLM-Verwaltung)\n\n1. [ ] Tabelle `llm_models` in `ki_dev` erstellen\n2. [ ] `LlmModelRepository` erstellen\n3. [ ] `LlmSyncService` erstellen (Anthropic + Ollama)\n4. [ ] Route `\/ressourcen\/llm` anlegen\n5. [ ] `RessourcenController::llmIndex()` implementieren\n6. [ ] View `\/ressourcen\/llm\/index.php` erstellen\n7. [ ] Sync-Buttons implementieren\n8. [ ] Bearbeiten-Funktionalität\n9. [ ] Initial-Sync durchführen (bestehende Modelle importieren)\n10. [ ] `ModelConfig.php` durch `LlmModelRepository` ersetzen\n11. [ ] Partial `model-select.php` erstellen\n12. [ ] Chat und Content auf Partial umstellen\n\n---\n\n## 0b. Collection-Verwaltung (Grundlage für Collection-Dropdown)\n\n### 0b.1 Architektur-Entscheidung\n\n**Gewählt: DB-Tabelle analog zu LLMs**\n\n- Zentrale Admin-Seite `\/ressourcen\/collections`\n- Datenbank-Tabelle als Single Source of Truth\n- Sync mit Qdrant (Metadaten abrufen)\n- Sprechende Namen und Beschreibungen\n- **Multi-Select** als einheitliche Darstellung\n\n### 0b.2 Datenbank-Tabelle `rag_collections`\n\n```sql\nCREATE TABLE rag_collections (\n id INT AUTO_INCREMENT PRIMARY KEY,\n collection_id VARCHAR(100) NOT NULL UNIQUE, -- Qdrant-Name: \"documents\"\n display_name VARCHAR(100) NOT NULL, -- ... [TRUNCATED-4e3f41d799439f7b]",
"structuredPatch": [
{
"oldStart": 573,
"oldLines": 6,
"newStart": 573,
"newLines": 199,
"lines": [
" 14. [ ] Python-Pipeline auf DB-Query umstellen",
" 15. [ ] Semantic Explorer auf Repository umstellen",
" ",
"+### 0b.11 KRITISCH: Embedding-Dimension Validierung",
"+",
"+> **Problem:** Bei Multi-Collection-Suche können Collections mit unterschiedlichen",
"+> Embedding-Modellen (z.B. 1024-dim vs 384-dim) kombiniert werden → Qdrant-Fehler.",
"+>",
"+> **Lösung:** Validierung VOR Suche - sowohl bei Collection-Auswahl als auch bei Suche.",
"+",
"+```php",
"+\/\/ ══════════════════════════════════════════════════════════════════",
"+\/\/ CollectionValidator.php (NEU)",
"+\/\/ ══════════════════════════════════════════════════════════════════",
"+",
"+namespace Infrastructure\\Validation;",
"+",
"+use Infrastructure\\Persistence\\CollectionRepository;",
"+",
"+class CollectionValidator",
"+{",
"+ public function __construct(",
"+ private CollectionRepository $repository",
"+ ) {}",
"+",
"+ \/**",
"+ * Prüft ob alle ausgewählten Collections kompatible Vektorgrößen haben",
"+ *",
"+ * @param array $collectionIds Array von Collection-IDs",
"+ * @return ValidationResult",
"+ *\/",
"+ public function validateSelection(array $collectionIds): ValidationResult",
"+ {",
"+ if (count($collectionIds) === 0) {",
"+ return ValidationResult::ok();",
"+ }",
"+",
"+ $collections = $this->repository->findByIds($collectionIds);",
"+ $vectorSizes = array_unique(array_column($collections, 'vector_size'));",
"+",
"+ if (count($vectorSizes) > 1) {",
"+ $details = [];",
"+ foreach ($collections as $col) {",
"+ $details[] = \"{$col['display_name']}: {$col['vector_size']}d\";",
"+ }",
"+ return ValidationResult::error(",
"+ 'Inkompatible Collections: Unterschiedliche Vektorgrößen ('",
"+ . implode(', ', $details) . ')'",
"+ );",
"+ }",
"+",
"+ return ValidationResult::ok($vectorSizes[0] ?? null);",
"+ }",
"+",
"+ \/**",
"+ * Prüft ob ein Embedding-Vektor zur Collection passt",
"+ *\/",
"+ public function validateEmbedding(string $collectionId, array $embedding): ValidationResult",
"+ {",
"+ $collection = $this->repository->find($collectionId);",
"+",
"+ if ($collection === null) {",
"+ return ValidationResult::error(\"Collection '$collectionId' nicht gefunden\");",
"+ }",
"+",
"+ $expectedSize = (int) $collection['vector_size'];",
"+ $actualSize = count($embedding);",
"+",
"+ if ($expectedSize !== $actualSize) {",
"+ return ValidationResult::error(",
"+ \"Dimension Mismatch: Collection erwartet {$expectedSize}d, \"",
"+ . \"Embedding hat {$actualSize}d\"",
"+ );",
"+ }",
"+",
"+ return ValidationResult::ok();",
"+ }",
"+}",
"+",
"+\/\/ ══════════════════════════════════════════════════════════════════",
"+\/\/ ValidationResult.php (NEU)",
"+\/\/ ══════════════════════════════════════════════════════════════════",
"+",
"+namespace Infrastructure\\Validation;",
"+",
"+class ValidationResult",
"+{",
"+ private function __construct(",
"+ public readonly bool $valid,",
"+ public readonly ?string $error,",
"+ public readonly mixed $data = null",
"+ ) {}",
"+",
"+ public static function ok(mixed $data = null): self",
"+ {",
"+ return new self(true, null, $data);",
"+ }",
"+",
"+ public static function error(string $message): self",
"+ {",
"+ return new self(false, $message);",
"+ }",
"+",
"+ public function isValid(): bool",
"+ {",
"+ return $this->valid;",
"+ }",
"+}",
"+```",
"+",
"+**Integration in QdrantService:**",
"+",
"+```php",
"+\/\/ QdrantService.php - Ergänzung zu search()",
"+public function searchMultiple(",
"+ array $collectionIds,",
"+ array $embedding",
"+): array {",
"+ \/\/ Dimension-Check vor Suche",
"+ $validator = new CollectionValidator($this->repository);",
"+ $result = $validator->validateSelection($collectionIds);",
"+",
"+ if (!$result->isValid()) {",
"+ throw new DimensionMismatchException($result->error);",
"+ }",
"+",
"+ \/\/ Zusätzlich: Embedding-Dimension gegen erwartete Größe prüfen",
"+ $expectedSize = $result->data; \/\/ von validateSelection zurückgegeben",
"+ if ($expectedSize !== null && count($embedding) !== $expectedSize) {",
"+ throw new DimensionMismatchException(",
"+ \"Embedding hat \" . count($embedding) . \"d, \"",
"+ . \"Collections erwarten {$expectedSize}d\"",
"+ );",
"+ }",
"+",
"+ \/\/ Suche in allen Collections",
"+ $allResults = [];",
"+ foreach ($collectionIds as $collectionId) {",
"+ $results = $this->search($embedding, $collectionId);",
"+ $allResults = array_merge($allResults, $results);",
"+ }",
"+",
"+ \/\/ Nach Score sortieren",
"+ usort($allResults, fn($a, $b) => $b['score'] <=> $a['score']);",
"+",
"+ return $allResults;",
"+}",
"+```",
"+",
"+**UI-Feedback bei Validierungsfehler:**",
"+",
"+```php",
"+\/\/ ChatController.php - Im send() vor RAG-Suche",
"+$collections = json_decode($session['collections'], true);",
"+$validator = new CollectionValidator($this->collectionRepo);",
"+$validation = $validator->validateSelection($collections);",
"+",
"+if (!$validation->isValid()) {",
"+ return $this->jsonError([",
"+ 'error' => 'Collection-Konfiguration ungültig',",
"+ 'details' => $validation->error,",
"+ 'hint' => 'Bitte wähle nur Collections mit gleichem Embedding-Modell'",
"+ ]);",
"+}",
"+```",
"+",
"+**Frontend-Warnung (optional, Phase 2):**",
"+",
"+```javascript",
"+\/\/ Bei Collection-Änderung im Multi-Select",
"+document.querySelectorAll('.collections-select').forEach(select => {",
"+ select.addEventListener('change', async (e) => {",
"+ const selected = Array.from(e.target.selectedOptions).map(o => o.value);",
"+ if (selected.length > 1) {",
"+ const resp = await fetch('\/api\/collections\/validate', {",
"+ method: 'POST',",
"+ body: JSON.stringify({ collections: selected })",
"+ });",
"+ const result = await resp.json();",
"+ if (!result.valid) {",
"+ showWarning(result.error);",
"+ }",
"+ }",
"+ });",
"+});",
"+```",
"+",
"+**Migrations-Schritte:**",
"+",
"+16. [ ] `ValidationResult.php` erstellen",
"+17. [ ] `CollectionValidator.php` erstellen",
"+18. [ ] `DimensionMismatchException.php` erstellen",
"+19. [ ] `QdrantService::searchMultiple()` implementieren",
"+20. [ ] `ChatController::send()` Validierung ergänzen",
"+21. [ ] `ContentController::generate()` Validierung ergänzen",
"+",
" ---",
" ",
" ## 0d. User-Präferenzen (Zentrale Defaults)"
]
}
],
"userModified": false,
"replaceAll": false
}
}