{
"tool_response": {
"filePath": "\/var\/www\/dev.campus.systemische-tools.de\/docs\/plan-form-components-refactoring.md",
"oldString": "## 0e. Context Limit (Hardcoded + User-Präferenz)",
"newString": "## 0e. Temperature, Max Tokens & Presets\n\n### 0e.1 Architektur-Entscheidung\n\n**Gewählt:**\n- Temperature: Hardcoded Range 0.0-1.0\n- Max Tokens: Dynamisch aus `llm_models.max_output_tokens` (4 Stufen)\n- Presets: 3 feste Kombinationen, Default in User-Präferenzen\n- UI: Überall identisch (Chat + Content Studio), nicht collapsed\n\n### 0e.2 Max Tokens: Dynamisch aus Modell\n\nDie `llm_models`-Tabelle enthält `max_output_tokens` pro Modell:\n\n```sql\n-- Beispiel-Daten in llm_models\n| model_id | max_output_tokens |\n|-----------------------------|-------------------|\n| claude-opus-4-5-20251101 | 8192 |\n| claude-sonnet-4-20250514 | 8192 |\n| ollama:mistral:latest | 4096 |\n| ollama:gemma3:4b-it-qat | 2048 |\n```\n\n**4-Stufen-Berechnung:**\n```php\nfunction getTokenSteps(int $maxTokens): array\n{\n return [\n (int) ($maxTokens * 0.25), \/\/ 25%\n (int) ($maxTokens * 0.50), \/\/ 50%\n (int) ($maxTokens * 0.75), \/\/ 75%\n $maxTokens, \/\/ 100%\n ];\n}\n\n\/\/ Beispiel: max_output_tokens = 8192\n\/\/ → [2048, 4096, 6144, 8192]\n\n\/\/ Beispiel: max_output_tokens = 4096\n\/\/ → [1024, 2048, 3072, 4096]\n```\n\n### 0e.3 Presets\n\n**3 feste Presets:**\n\n| Preset | Temperature | Max Tokens | Beschreibung |\n|--------|-------------|------------|--------------|\n| **Präzise** | 0.3 | 50% | Fokussiert, weniger kreativ |\n| **Ausgewogen** | 0.7 | 75% | Standard für die meisten Aufgaben |\n| **Kreativ** | 0.9 | 100% | Explorativ, längere Antworten |\n\n**User-Präferenz:** `user_preferences.default_preset`\n\n```sql\n-- Erweiterung user_preferences\nALTER TABLE user_preferences\nADD COLUMN default_preset ENUM('precise', 'balanced', 'creative') DEFAULT 'balanced';\n```\n\n### 0e.4 Partial: temperature-tokens.php\n\n```php\n\/\/ \/src\/View\/partials\/form\/temperature-tokens.php\n<?php\n\/\/ Werte aus Session\/Order oder User-Präferenzen\n$temperature = $temperature ?? ($userPrefs['default_temperature'] ?? 0.7);\n$maxTokens = $maxTokens ?? ($userPrefs['default_max_tokens'] ?? null);\n$preset = $preset ?? ($userPrefs['default_preset'] ?? 'balanced');\n\n\/\/ Modell-Info für Token-Stufen (wird per JS aktualisiert)\n$modelMaxTokens = $modelMaxTokens ?? 8192;\n$tokenSteps = [\n (int) ($modelMaxTokens * 0.25),\n (int) ($modelMaxTokens * 0.50),\n (int) ($modelMaxTokens * 0.75),\n $modelMaxTokens,\n];\n\n\/\/ Default max_tokens basierend auf Preset\nif ($maxTokens === null) {\n $maxTokens = match($preset) {\n 'precise' => $tokenSteps[1], \/\/ 50%\n 'balanced' => $tokenSteps[2], \/\/ 75%\n 'creative' => $tokenSteps[3], \/\/ 100%\n default => $tokenSteps[2],\n };\n}\n\n$variant = $variant ?? 'default';\n$class = $variant === 'inline' ? 'form-control--inline' : 'form-control';\n?>\n\n<div class=\"llm-settings <?= $class ?>\" data-model-max-tokens=\"<?= $modelMaxTokens ?>\">\n <!-- Presets -->\n <div class=\"preset-buttons\">\n <button type=\"button\" class=\"preset-btn <?= $preset === 'precise' ? 'active' : '' ?>\"\n data-preset=\"precise\" data-temperature=\"0.3\" data-tokens-percent=\"50\"\n title=\"Fokussiert, weniger kreativ\">\n Präzise\n <\/button>\n <button type=\"button\" class=\"preset-btn <?= $preset === 'balanced' ? 'active' : '' ?>\"\n data-preset=\"balanced\" data-temperature=\"0.7\" data-tokens-percent=\"75\"\n title=\"Standard für die meisten Aufgaben\">\n Ausgewogen\n <\/button>\n <button type=\"button\" class=\"preset-btn <?= $preset === 'creative' ? 'active' : '' ?>\"\n data-preset=\"creative\" data-temperature=\"0.9\" data-tokens-percent=\"100\"\n title=\"Explorativ, längere Antworten\">\n Kreativ\n <\/button>\n <\/div>\n\n <!-- Temperature Slider -->\n <div class=\"temperature-control\">\n <label for=\"temperature\">\n Temperature: <span class=\"temperature-value\"><?= number_format($temperature, 1) ?><\/span>\n <\/label>\n <input type=\"range\" name=\"temperature\" id=\"temperature\"\n min=\"0\" max=\"1\" step=\"0.1\" value=\"<?= $temperature ?>\"\n class=\"form-range\">\n <\/div>\n\n <!-- Max Tokens Select -->\n <div class=\"max-tokens-control\">\n <label for=\"max_tokens\">Max Tokens<\/label>\n <select name=\"max_tokens\" id=\"max_tokens\" class=\"form-select\">\n <?php foreach ($tokenSteps as $step): ?>\n <option value=\"<?= $step ?>\" <?= (int)$maxTokens === $step ? 'selected' : '' ?>>\n <?= number_format($step) ?>\n <\/option>\n <?php endforeach; ?>\n <\/select>\n <\/div>\n\n <!-- Hidden: Preset für Form-Submit -->\n <input type=\"hidden\" name=\"preset\" id=\"preset\" value=\"<?= $preset ?>\">\n<\/div>\n```\n\n### 0e.5 JavaScript: Preset & Modell-Wechsel\n\n```javascript\n\/\/ \/public\/js\/llm-settings.js\n\ndocument.addEventListener('DOMContentLoaded', function() {\n const container = document.querySelector('.llm-settings');\n if (!container) return;\n\n const tempSlider = container.querySelector('#temperature');\n const tempValue = container.querySelector('.temperature-value');\n const tokensSelect = container.querySelector('#max_tokens');\n const presetInput = container.querySelector('#preset');\n const presetBtns = container.querySelectorAll('.preset-btn');\n\n \/\/ Temperature-Slider: Wert anzeigen\n tempSlider?.addEventListener('input', function() {\n tempValue.textContent = parseFloat(this.value).toFixed(1);\n updatePresetState();\n });\n\n \/\/ Preset-Buttons\n presetBtns.forEach(btn => {\n btn.addEventListener('click', function() {\n const temp = parseFloat(this.dataset.temperature);\n const tokensPercent = parseInt(this.dataset.tokensPercent);\n const preset = this.dataset.preset;\n const maxTokens = parseInt(container.dataset.modelMaxTokens);\n\n \/\/ Werte setzen\n tempSlider.value = temp;\n tempValue.textContent = temp.toFixed(1);\n\n \/\/ Token-Wert berechnen\n const targetTokens = Math.round(maxTokens * tokensPercent \/ 100);\n selectClosestOption(tokensSelect, targetTokens);\n\n \/\/ Preset markieren\n presetBtns.forEach(b => b.classList.remove('active'));\n this.classList.add('active');\n presetInput.value = preset;\n });\n });\n\n \/\/ Modell-Wechsel: Token-Optionen aktualisieren\n const modelSelect = document.querySelector('#model, [name=\"model\"]');\n modelSelect?.addEventListener('change', function() {\n updateTokenOptions(this.value);\n });\n\n function updateTokenOptions(modelId) {\n \/\/ Fetch model info (max_output_tokens)\n fetch(`\/api\/v1\/llm\/${encodeURIComponent(modelId)}`)\n .then(r => r.json())\n .then(model => {\n const max = model.max_output_tokens || 8192;\n container.dataset.modelMaxTokens = max;\n\n const steps = [\n Math.round(max * 0.25),\n Math.round(max * 0.50),\n Math.round(max * 0.75),\n max\n ];\n\n tokensSelect.innerHTML = steps.map(s =>\n `<option value=\"${s}\">${s.toLocaleString()}<\/option>`\n ).join('');\n\n \/\/ Aktuellen Preset neu anwenden\n const activePreset = container.querySelector('.preset-btn.active');\n activePreset?.click();\n });\n }\n\n function selectClosestOption(select, target) {\n let closest = select.options[0];\n let closestDiff = Math.abs(parseInt(closest.value) - target);\n\n for (const opt of select.options) {\n const diff = Math.abs(parseInt(opt.value) - target);\n if (diff < closestDiff) {\n closest = opt;\n closestDiff = diff;\n }\n }\n closest.selected = true;\n }\n\n function updatePresetState() {\n \/\/ Preset-Buttons: \"active\" entfernen wenn manuell geändert\n const currentTemp = parseFloat(tempSlider.value);\n let matchedPreset = null;\n\n presetBtns.forEach(btn => {\n if (parseFloat(btn.dataset.temperature) === currentTemp) {\n matchedPreset = btn;\n }\n });\n\n presetBtns.forEach(b => b.classList.remove('active'));\n if (matchedPreset) {\n matchedPreset.classList.add('active');\n presetInput.value = matchedPreset.dataset.preset;\n } else {\n presetInput.value = 'custom';\n }\n }\n});\n```\n\n### 0e.6 API-Endpoint für Modell-Info\n\n```php\n\/\/ Route: GET \/api\/v1\/llm\/{modelId}\n\/\/ Returns: { \"model_id\": \"...\", \"max_output_tokens\": 8192, ... }\n\n$router->get('\/api\/v1\/llm\/{modelId}', [ApiController::class, 'llmShow']);\n```\n\n### 0e.7 CSS: Einheitliches Styling\n\n```css\n\/* \/public\/css\/llm-settings.css *\/\n\n.llm-settings {\n display: flex;\n flex-wrap: wrap;\n gap: var(--space-md);\n align-items: center;\n}\n\n.preset-buttons {\n display: flex;\n gap: var(--space-xs);\n}\n\n.preset-btn {\n padding: 0.4rem 0.8rem;\n border: 1px solid var(--color-border);\n border-radius: 4px;\n background: var(--color-bg);\n cursor: pointer;\n font-size: 0.85rem;\n transition: all 0.15s;\n}\n\n.preset-btn:hover {\n border-color: var(--color-primary);\n}\n\n.preset-btn.active {\n background: var(--color-primary);\n color: white;\n border-color: var(--color-primary);\n}\n\n.temperature-control {\n display: flex;\n align-items: center;\n gap: var(--space-sm);\n}\n\n.temperature-control label {\n font-size: 0.85rem;\n white-space: nowrap;\n}\n\n.temperature-value {\n font-weight: 600;\n min-width: 2rem;\n}\n\n.form-range {\n width: 100px;\n}\n\n.max-tokens-control {\n display: flex;\n align-items: center;\n gap: var(--space-sm);\n}\n\n.max-tokens-control label {\n font-size: 0.85rem;\n}\n\n\/* Inline-Variante *\/\n.llm-settings.form-control--inline {\n gap: var(--... [TRUNCATED-5a931b8a609ba100]",
"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## 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, -- Anzeige: \"Dokumente\"\n description TEXT, -- \"PDF-Dokumente aus Nextcloud\"\n\n -- Qdrant-Metadaten (via Sync)\n vector_size INT, -- 1024\n distance_metric VARCHAR(20), -- \"Cosine\"\n points_count INT DEFAULT 0, -- Anzahl Vektoren\n\n -- Konfiguration\n embedding_model VARCHAR(100), -- \"mxbai-embed-large\"\n chunk_size INT, -- 2000\n chunk_overlap INT, -- 200\n\n -- Verwaltung\n source_type ENUM('nextcloud', 'mail', 'manual', 'system') DEFAULT 'manual',\n source_path VARCHAR(500), -- \"\/var\/www\/nextcloud\/data\/...\"\n is_active BOOLEAN DEFAULT TRUE, -- In Dropdowns anzeigen?\n is_searchable BOOLEAN DEFAULT TRUE, -- Für RAG verfügbar?\n sort_order INT DEFAULT 0,\n\n -- Timestamps\n last_synced_at DATETIME,\n created_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;\n```\n\n### 0b.3 Impact-Analyse: Wer nutzt Collections?\n\n| System\/Seite | Aktueller Zugriff | Neuer Zugriff | Änderung |\n|--------------|-------------------|---------------|----------|\n| **Chat** | `$qdrantService->listCollections()` | `CollectionRepository::getActive()` | Query aus DB |\n| **Content Studio** | `$qdrantService->listCollections()` | `CollectionRepository::getActive()` | Query aus DB |\n| **Content Show** | `$qdrantService->listCollections()` | `CollectionRepository::getActive()` | Query aus DB |\n| **Pipeline (Python)** | `config.py QDRANT_COLLECTIONS` | DB-Query oder Config-Sync | Python liest DB |\n| **Semantic Explorer** | Direkt Qdrant | `CollectionRepository` + Qdrant | Metadaten aus DB |\n| **System Explorer** | - | Statistiken aus DB | Neu |\n| **API \/api\/v1\/search** | Qdrant direkt | Validierung gegen DB | Nur aktive Collections |\n| **Embedding-Service** | `config.py` | DB oder Sync | Konsistenz |\n\n### 0b.4 Admin-Seite `\/ressourcen\/collections`\n\n**URL:** `\/ressourcen\/collections`\n**Controller:** `RessourcenController::collectionsIndex()`\n\n**Funktionen:**\n\n| Button | Aktion |\n|--------|--------|\n| \"Qdrant synchronisieren\" | Collections + Metadaten von Qdrant abrufen |\n| \"Neue Collection\" | Manuell Collection registrieren |\n\n**Tabellen-Ansicht:**\n\n| Collection-ID | Display-Name | Vektoren | Größe | Quelle | Aktiv | Suchbar | Aktionen |\n|---------------|--------------|----------|-------|--------|-------|---------|----------|\n| documents | Dokumente | 1.247 | 1024d | Nextcloud | ✓ | ✓ | Bearbeiten |\n| dokumentation_chunks | Doku-Chunks | 892 | 1024d | System | ✓ | ✓ | Bearbeiten |\n| mail | E-Mails | 3.421 | 1024d | Mail | ✓ | ✓ | Bearbeiten |\n| entities | Entitäten | 156 | 1024d | System | ✓ | - | Bearbeiten |\n\n**Bearbeiten-Dialog:**\n- Display-Name ändern\n- Beschreibung hinzufügen\n- Quelle\/Pfad dokumentieren\n- Aktivieren\/Deaktivieren (Dropdown)\n- Suchbar ja\/nein (RAG-Verfügbarkeit)\n- Sortierung ändern\n\n### 0b.5 Sync-Logik\n\n**Qdrant Sync:**\n```php\n\/\/ QdrantService::getCollectionsInfo()\npublic function syncCollections(): array\n{\n $qdrantCollections = $this->client->getCollections();\n\n foreach ($qdrantCollections as $col) {\n $info = $this->client->getCollectionInfo($col['name']);\n\n \/\/ INSERT ... ON DUPLICATE KEY UPDATE\n $this->repository->upsert([\n 'collection_id' => $col['name'],\n 'display_name' => $col['name'], \/\/ Initial = ID\n 'vector_size' => $info['config']['params']['size'],\n 'distance_metric' => $info['config']['params']['distance'],\n 'points_count' => $info['points_count'],\n 'last_synced_at' => now(),\n ]);\n }\n}\n```\n\n### 0b.6 Repository-Interface\n\n```php\n\/\/ CollectionRepository.php\ninterface CollectionRepositoryInterfac... [TRUNCATED-fa4c94eea7d458c2]",
"structuredPatch": [
{
"oldStart": 717,
"oldLines": 10,
"newStart": 717,
"newLines": 416,
"lines": [
" ",
" ---",
" ",
"-## 0e. Context Limit (Hardcoded + User-Präferenz)",
"+## 0e. Temperature, Max Tokens & Presets",
" ",
" ### 0e.1 Architektur-Entscheidung",
" ",
"+**Gewählt:**",
"+- Temperature: Hardcoded Range 0.0-1.0",
"+- Max Tokens: Dynamisch aus `llm_models.max_output_tokens` (4 Stufen)",
"+- Presets: 3 feste Kombinationen, Default in User-Präferenzen",
"+- UI: Überall identisch (Chat + Content Studio), nicht collapsed",
"+",
"+### 0e.2 Max Tokens: Dynamisch aus Modell",
"+",
"+Die `llm_models`-Tabelle enthält `max_output_tokens` pro Modell:",
"+",
"+```sql",
"+-- Beispiel-Daten in llm_models",
"+| model_id | max_output_tokens |",
"+|-----------------------------|-------------------|",
"+| claude-opus-4-5-20251101 | 8192 |",
"+| claude-sonnet-4-20250514 | 8192 |",
"+| ollama:mistral:latest | 4096 |",
"+| ollama:gemma3:4b-it-qat | 2048 |",
"+```",
"+",
"+**4-Stufen-Berechnung:**",
"+```php",
"+function getTokenSteps(int $maxTokens): array",
"+{",
"+ return [",
"+ (int) ($maxTokens * 0.25), \/\/ 25%",
"+ (int) ($maxTokens * 0.50), \/\/ 50%",
"+ (int) ($maxTokens * 0.75), \/\/ 75%",
"+ $maxTokens, \/\/ 100%",
"+ ];",
"+}",
"+",
"+\/\/ Beispiel: max_output_tokens = 8192",
"+\/\/ → [2048, 4096, 6144, 8192]",
"+",
"+\/\/ Beispiel: max_output_tokens = 4096",
"+\/\/ → [1024, 2048, 3072, 4096]",
"+```",
"+",
"+### 0e.3 Presets",
"+",
"+**3 feste Presets:**",
"+",
"+| Preset | Temperature | Max Tokens | Beschreibung |",
"+|--------|-------------|------------|--------------|",
"+| **Präzise** | 0.3 | 50% | Fokussiert, weniger kreativ |",
"+| **Ausgewogen** | 0.7 | 75% | Standard für die meisten Aufgaben |",
"+| **Kreativ** | 0.9 | 100% | Explorativ, längere Antworten |",
"+",
"+**User-Präferenz:** `user_preferences.default_preset`",
"+",
"+```sql",
"+-- Erweiterung user_preferences",
"+ALTER TABLE user_preferences",
"+ADD COLUMN default_preset ENUM('precise', 'balanced', 'creative') DEFAULT 'balanced';",
"+```",
"+",
"+### 0e.4 Partial: temperature-tokens.php",
"+",
"+```php",
"+\/\/ \/src\/View\/partials\/form\/temperature-tokens.php",
"+<?php",
"+\/\/ Werte aus Session\/Order oder User-Präferenzen",
"+$temperature = $temperature ?? ($userPrefs['default_temperature'] ?? 0.7);",
"+$maxTokens = $maxTokens ?? ($userPrefs['default_max_tokens'] ?? null);",
"+$preset = $preset ?? ($userPrefs['default_preset'] ?? 'balanced');",
"+",
"+\/\/ Modell-Info für Token-Stufen (wird per JS aktualisiert)",
"+$modelMaxTokens = $modelMaxTokens ?? 8192;",
"+$tokenSteps = [",
"+ (int) ($modelMaxTokens * 0.25),",
"+ (int) ($modelMaxTokens * 0.50),",
"+ (int) ($modelMaxTokens * 0.75),",
"+ $modelMaxTokens,",
"+];",
"+",
"+\/\/ Default max_tokens basierend auf Preset",
"+if ($maxTokens === null) {",
"+ $maxTokens = match($preset) {",
"+ 'precise' => $tokenSteps[1], \/\/ 50%",
"+ 'balanced' => $tokenSteps[2], \/\/ 75%",
"+ 'creative' => $tokenSteps[3], \/\/ 100%",
"+ default => $tokenSteps[2],",
"+ };",
"+}",
"+",
"+$variant = $variant ?? 'default';",
"+$class = $variant === 'inline' ? 'form-control--inline' : 'form-control';",
"+?>",
"+",
"+<div class=\"llm-settings <?= $class ?>\" data-model-max-tokens=\"<?= $modelMaxTokens ?>\">",
"+ <!-- Presets -->",
"+ <div class=\"preset-buttons\">",
"+ <button type=\"button\" class=\"preset-btn <?= $preset === 'precise' ? 'active' : '' ?>\"",
"+ data-preset=\"precise\" data-temperature=\"0.3\" data-tokens-percent=\"50\"",
"+ title=\"Fokussiert, weniger kreativ\">",
"+ Präzise",
"+ <\/button>",
"+ <button type=\"button\" class=\"preset-btn <?= $preset === 'balanced' ? 'active' : '' ?>\"",
"+ data-preset=\"balanced\" data-temperature=\"0.7\" data-tokens-percent=\"75\"",
"+ title=\"Standard für die meisten Aufgaben\">",
"+ Ausgewogen",
"+ <\/button>",
"+ <button type=\"button\" class=\"preset-btn <?= $preset === 'creative' ? 'active' : '' ?>\"",
"+ data-preset=\"creative\" data-temperature=\"0.9\" data-tokens-percent=\"100\"",
"+ title=\"Explorativ, längere Antworten\">",
"+ Kreativ",
"+ <\/button>",
"+ <\/div>",
"+",
"+ <!-- Temperature Slider -->",
"+ <div class=\"temperature-control\">",
"+ <label for=\"temperature\">",
"+ Temperature: <span class=\"temperature-value\"><?= number_format($temperature, 1) ?><\/span>",
"+ <\/label>",
"+ <input type=\"range\" name=\"temperature\" id=\"temperature\"",
"+ min=\"0\" max=\"1\" step=\"0.1\" value=\"<?= $temperature ?>\"",
"+ class=\"form-range\">",
"+ <\/div>",
"+",
"+ <!-- Max Tokens Select -->",
"+ <div class=\"max-tokens-control\">",
"+ <label for=\"max_tokens\">Max Tokens<\/label>",
"+ <select name=\"max_tokens\" id=\"max_tokens\" class=\"form-select\">",
"+ <?php foreach ($tokenSteps as $step): ?>",
"+ <option value=\"<?= $step ?>\" <?= (int)$maxTokens === $step ? 'selected' : '' ?>>",
"+ <?= number_format($step) ?>",
"+ <\/option>",
"+ <?php endforeach; ?>",
"+ <\/select>",
"+ <\/div>",
"+",
"+ <!-- Hidden: Preset für Form-Submit -->",
"+ <input type=\"hidden\" name=\"preset\" id=\"preset\" value=\"<?= $preset ?>\">",
"+<\/div>",
"+```",
"+",
"+### 0e.5 JavaScript: Preset & Modell-Wechsel",
"+",
"+```javascript",
"+\/\/ \/public\/js\/llm-settings.js",
"+",
"+document.addEventListener('DOMContentLoaded', function() {",
"+ const container = document.querySelector('.llm-settings');",
"+ if (!container) return;",
"+",
"+ const tempSlider = container.querySelector('#temperature');",
"+ const tempValue = container.querySelector('.temperature-value');",
"+ const tokensSelect = container.querySelector('#max_tokens');",
"+ const presetInput = container.querySelector('#preset');",
"+ const presetBtns = container.querySelectorAll('.preset-btn');",
"+",
"+ \/\/ Temperature-Slider: Wert anzeigen",
"+ tempSlider?.addEventListener('input', function() {",
"+ tempValue.textContent = parseFloat(this.value).toFixed(1);",
"+ updatePresetState();",
"+ });",
"+",
"+ \/\/ Preset-Buttons",
"+ presetBtns.forEach(btn => {",
"+ btn.addEventListener('click', function() {",
"+ const temp = parseFloat(this.dataset.temperature);",
"+ const tokensPercent = parseInt(this.dataset.tokensPercent);",
"+ const preset = this.dataset.preset;",
"+ const maxTokens = parseInt(container.dataset.modelMaxTokens);",
"+",
"+ \/\/ Werte setzen",
"+ tempSlider.value = temp;",
"+ tempValue.textContent = temp.toFixed(1);",
"+",
"+ \/\/ Token-Wert berechnen",
"+ const targetTokens = Math.round(maxTokens * tokensPercent \/ 100);",
"+ selectClosestOption(tokensSelect, targetTokens);",
"+",
"+ \/\/ Preset markieren",
"+ presetBtns.forEach(b => b.classList.remove('active'));",
"+ this.classList.add('active');",
"+ presetInput.value = preset;",
"+ });",
"+ });",
"+",
"+ \/\/ Modell-Wechsel: Token-Optionen aktualisieren",
"+ const modelSelect = document.querySelector('#model, [name=\"model\"]');",
"+ modelSelect?.addEventListener('change', function() {",
"+ updateTokenOptions(this.value);",
"+ });",
"+",
"+ function updateTokenOptions(modelId) {",
"+ \/\/ Fetch model info (max_output_tokens)",
"+ fetch(`\/api\/v1\/llm\/${encodeURIComponent(modelId)}`)",
"+ .then(r => r.json())",
"+ .then(model => {",
"+ const max = model.max_output_tokens || 8192;",
"+ container.dataset.modelMaxTokens = max;",
"+",
"+ const steps = [",
"+ Math.round(max * 0.25),",
"+ Math.round(max * 0.50),",
"+ Math.round(max * 0.75),",
"+ max",
"+ ];",
"+",
"+ tokensSelect.innerHTML = steps.map(s =>",
"+ `<option value=\"${s}\">${s.toLocaleString()}<\/option>`",
"+ ).join('');",
"+",
"+ \/\/ Aktuellen Preset neu anwenden",
"+ const activePreset = container.querySelector('.preset-btn.active');",
"+ activePreset?.click();",
"+ });",
"+ }",
"+",
"+ function selectClosestOption(select, target) {",
"+ let closest = select.options[0];",
"+ let closestDiff = Math.abs(parseInt(closest.value) - target);",
"+",
"+ for (const opt of select.options) {",
"+ const diff = Math.abs(parseInt(opt.value) - target);",
"+ if (diff < closestDiff) {",
"+ closest = opt;",
"+ closestDiff = diff;",
"+ }",
"+ }",
"+ closest.selected = true;",
"+ }",
"+",
"+ function updatePresetState() {",
"+ \/\/ Preset-Buttons: \"active\" entfernen wenn manuell geändert",
"+ const currentTemp = parseFloat(tempSlider.value);",
"+ let matchedPreset = null;",
"+",
"+ presetBtns.forEach(btn => {",
"+ if (parseFloat(btn.dataset.temperature) === currentTemp) {",
"+ matchedPreset = btn;",
"+ }",
"+ });",
"+",
"+ presetBtns.forEach(b => b.classList.remove('active'));",
"+ if (matchedPreset) {",
"+ matchedPreset.classList.add('active');",
"+ presetInput.value = matchedPreset.dataset.preset;",
"+ } else {",
"+ presetInput.value = 'custom';",
"+ }",
"+ }",
"+});",
"+```",
"+",
"+### 0e.6 API-Endpoint für Modell-Info",
"+",
"+```php",
"+\/\/ Route: GET \/api\/v1\/llm\/{modelId}",
"+\/\/ Returns: { \"model_id\": \"...\", \"max_output_tokens\": 8192, ... }",
"+",
"+$router->get('\/api\/v1\/llm\/{modelId}', [ApiController::class, 'llmShow']);",
"+```",
"+",
"+### 0e.7 CSS: Einheitliches Styling",
"+",
"+```css",
"+\/* \/public\/css\/llm-settings.css *\/",
"+",
"+.llm-settings {",
"+ display: flex;",
"+ flex-wrap: wrap;",
"+ gap: var(--space-md);",
"+ align-items: center;",
"+}",
"+",
"+.preset-buttons {",
"+ display: flex;",
"+ gap: var(--space-xs);",
"+}",
"+",
"+.preset-btn {",
"+ padding: 0.4rem 0.8rem;",
"+ border: 1px solid var(--color-border);",
"+ border-radius: 4px;",
"+ background: var(--color-bg);",
"+ cursor: pointer;",
"+ font-size: 0.85rem;",
"+ transition: all 0.15s;",
"+}",
"+",
"+.preset-btn:hover {",
"+ border-color: var(--color-primary);",
"+}",
"+",
"+.preset-btn.active {",
"+ background: var(--color-primary);",
"+ color: white;",
"+ border-color: var(--color-primary);",
"+}",
"+",
"+.temperature-control {",
"+ display: flex;",
"+ align-items: center;",
"+ gap: var(--space-sm);",
"+}",
"+",
"+.temperature-control label {",
"+ font-size: 0.85rem;",
"+ white-space: nowrap;",
"+}",
"+",
"+.temperature-value {",
"+ font-weight: 600;",
"+ min-width: 2rem;",
"+}",
"+",
"+.form-range {",
"+ width: 100px;",
"+}",
"+",
"+.max-tokens-control {",
"+ display: flex;",
"+ align-items: center;",
"+ gap: var(--space-sm);",
"+}",
"+",
"+.max-tokens-control label {",
"+ font-size: 0.85rem;",
"+}",
"+",
"+\/* Inline-Variante *\/",
"+.llm-settings.form-control--inline {",
"+ gap: var(--space-sm);",
"+}",
"+",
"+.llm-settings.form-control--inline .preset-btn {",
"+ padding: 0.3rem 0.6rem;",
"+ font-size: 0.8rem;",
"+}",
"+",
"+.llm-settings.form-control--inline .form-range {",
"+ width: 80px;",
"+}",
"+```",
"+",
"+### 0e.8 User-Präferenzen Erweiterung",
"+",
"+```sql",
"+-- Bereits in user_preferences vorhanden:",
"+-- default_temperature DECIMAL(3,2) DEFAULT 0.70",
"+-- default_max_tokens INT DEFAULT 4096",
"+",
"+-- Neu hinzufügen:",
"+ALTER TABLE user_preferences",
"+ADD COLUMN default_preset ENUM('precise', 'balanced', 'creative', 'custom') DEFAULT 'balanced';",
"+```",
"+",
"+### 0e.9 Integration in Views",
"+",
"+**Chat (bereits vorhanden, anpassen):**",
"+```php",
"+\/\/ chat\/index.php",
"+<?php include VIEW_PATH . '\/partials\/form\/temperature-tokens.php'; ?>",
"+```",
"+",
"+**Content New (hinzufügen):**",
"+```php",
"+\/\/ content\/new.php - in Fieldset \"KI-Einstellungen\"",
"+<?php include VIEW_PATH . '\/partials\/form\/temperature-tokens.php'; ?>",
"+```",
"+",
"+**Content Show (hinzufügen):**",
"+```php",
"+\/\/ content\/show.php - in Action-Bar",
"+<?php $variant = 'inline'; ?>",
"+<?php include VIEW_PATH . '\/partials\/form\/temperature-tokens.php'; ?>",
"+```",
"+",
"+### 0e.10 Migrations-Schritte (Temperature, Max Tokens, Presets)",
"+",
"+#### Phase 1: Datenbank",
"+1. [ ] `llm_models.max_output_tokens` sicherstellen (bereits geplant)",
"+2. [ ] `user_preferences.default_preset` hinzufügen",
"+",
"+#### Phase 2: API",
"+3. [ ] API-Endpoint `GET \/api\/v1\/llm\/{modelId}` erstellen",
"+",
"+#### Phase 3: Frontend",
"+4. [ ] `\/public\/js\/llm-settings.js` erstellen",
"+5. [ ] `\/public\/css\/llm-settings.css` erstellen",
"+6. [ ] In Layout einbinden",
"+",
"+#### Phase 4: Partial",
"+7. [ ] Partial `temperature-tokens.php` erstellen",
"+",
"+#### Phase 5: Views",
"+8. [ ] Chat auf neues Partial umstellen",
"+9. [ ] Content New: Partial hinzufügen",
"+10. [ ] Content Show: Partial hinzufügen (inline)",
"+",
"+#### Phase 6: Testing",
"+11. [ ] Preset-Buttons funktionieren",
"+12. [ ] Modell-Wechsel aktualisiert Token-Optionen",
"+13. [ ] Temperature-Slider zeigt Wert",
"+14. [ ] User-Präferenz wird angewendet",
"+",
"+---",
"+",
"+## 0f. Context Limit (Hardcoded + User-Präferenz)",
"+",
"+### 0e.1 Architektur-Entscheidung",
"+",
" **Gewählt: KISS - Hardcoded Werte, Default aus User-Präferenzen**",
" ",
" - Feste Werte: `[3, 5, 10, 15]` (kein DB-Management nötig)"
]
}
],
"userModified": false,
"replaceAll": false
}
}