{
"tool_response": {
"filePath": "\/var\/www\/dev.campus.systemische-tools.de\/src\/Controller\/ChatController.php",
"oldString": " \/**\n * Render chat response HTML\n *\/\n private function renderResponse(string $question, array $result, string $model): void\n {\n $answer = $result['answer'] ?? '';\n $sources = $result['sources'] ?? [];\n $usage = $result['usage'] ?? null;\n $modelLabel = str_starts_with($model, 'ollama:') ? substr($model, 7) : $model;\n\n \/\/ Convert markdown-style formatting to HTML\n $answer = $this->formatAnswer($answer);\n\n echo '<div class=\"chat-message chat-message--user\">';\n echo '<div class=\"message-content\">' . htmlspecialchars($question) . '<\/div>';\n echo '<\/div>';\n\n echo '<div class=\"chat-message chat-message--assistant\">';\n echo '<div class=\"message-content\">' . $answer . '<\/div>';\n\n if (count($sources) > 0) {\n $sourceCount = count($sources);\n $uniqueId = uniqid('sources-');\n echo '<div class=\"chat-sources chat-sources--collapsed\" id=\"' . $uniqueId . '\">';\n echo '<button type=\"button\" class=\"chat-sources__toggle\" onclick=\"toggleSources(\\'' . $uniqueId . '\\')\">';\n echo '<span class=\"chat-sources__count\">' . $sourceCount . ' Quelle' . ($sourceCount > 1 ? 'n' : '') . '<\/span>';\n echo '<span class=\"chat-sources__arrow\">▼<\/span>';\n echo '<\/button>';\n echo '<div class=\"chat-sources__list\">';\n foreach ($sources as $source) {\n $title = htmlspecialchars($source['title'] ?? 'Unbekannt');\n $score = round(($source['score'] ?? 0) * 100);\n $content = isset($source['content']) ? htmlspecialchars(mb_substr($source['content'], 0, 200)) : '';\n echo '<div class=\"source-item\">';\n echo '<div class=\"source-item__header\">';\n echo '<span class=\"source-item__title\">' . $title . '<\/span>';\n echo '<span class=\"source-item__score\">' . $score . '%<\/span>';\n echo '<\/div>';\n if ($content !== '') {\n echo '<div class=\"source-item__content\">\"' . $content . (mb_strlen($source['content'] ?? '') > 200 ? '...' : '') . '\"<\/div>';\n }\n echo '<\/div>';\n }\n echo '<\/div>';\n echo $this->renderMessageMeta($modelLabel, $usage, $model);\n echo '<\/div>';\n } else {\n echo $this->renderMessageMeta($modelLabel, $usage, $model);\n }\n\n echo '<\/div>';\n }\n\n \/**\n * Render message meta (model, tokens, cost)\n *\/\n private function renderMessageMeta(string $modelLabel, ?array $usage, string $model): string\n {\n $html = '<div class=\"message-meta\">';\n $html .= '<span class=\"model-info\">Modell: ' . htmlspecialchars($modelLabel) . '<\/span>';\n\n if (str_starts_with($model, 'claude-') && $usage !== null) {\n $inputTokens = $usage['input_tokens'] ?? 0;\n $outputTokens = $usage['output_tokens'] ?? 0;\n $cost = $this->calculateCost($inputTokens, $outputTokens);\n\n $html .= '<span class=\"tokens-info\">';\n $html .= '<span class=\"tokens-input\" title=\"Input-Tokens\">↓' . number_format($inputTokens) . '<\/span> ';\n $html .= '<span class=\"tokens-output\" title=\"Output-Tokens\">↑' . number_format($outputTokens) . '<\/span>';\n $html .= '<\/span>';\n $html .= '<span class=\"cost-info\" title=\"Geschätzte Kosten\">~$' . number_format($cost, 4) . '<\/span>';\n } elseif (str_starts_with($model, 'ollama:')) {\n $html .= '<span class=\"tokens-info local\">lokal<\/span>';\n }\n\n $html .= '<\/div>';\n\n return $html;\n }",
"newString": " \/**\n * Render chat response HTML using view partial\n *\/\n private function renderResponse(string $question, array $result, string $model): void\n {\n \/\/ Convert markdown-style formatting to HTML\n $formattedAnswer = $this->formatAnswer($result['answer'] ?? '');\n\n $this->view('chat.partials.response', [\n 'question' => $question,\n 'result' => $result,\n 'model' => $model,\n 'formattedAnswer' => $formattedAnswer,\n ]);\n }",
"originalFile": "<?php\n\nnamespace Controller;\n\nuse Framework\\Controller;\nuse Infrastructure\\AI\\AIConfig;\nuse Infrastructure\\AI\\ChatService;\nuse Infrastructure\\AI\\ModelConfig;\nuse Infrastructure\\Persistence\\CollectionRepository;\nuse Infrastructure\\Validation\\CollectionValidator;\n\nclass ChatController extends Controller\n{\n private ChatService $chatService;\n private CollectionRepository $collectionRepository;\n private CollectionValidator $collectionValidator;\n private \\PDO $db;\n\n \/** @var array<array<string,mixed>>|null Cached collections list *\/\n private ?array $collectionsCache = null;\n\n public function __construct()\n {\n $config = AIConfig::fromCredentialsFile();\n $this->chatService = $config->createChatService();\n $this->collectionRepository = new CollectionRepository();\n $this->collectionValidator = new CollectionValidator($this->collectionRepository);\n $this->db = $this->initializeDatabase();\n }\n\n \/**\n * GET \/chat\n * Show chat interface with session list, create new session if none\n *\/\n public function index(): void\n {\n $sessions = $this->getSessions();\n\n \/\/ Create new session and redirect\n $uuid = $this->createSession();\n header('Location: \/chat\/' . $uuid);\n exit;\n }\n\n \/**\n * GET \/chat\/{uuid}\n * Show specific chat session\n *\/\n public function show(string $uuid): void\n {\n $session = $this->getSession($uuid);\n\n if ($session === null) {\n header('Location: \/chat');\n exit;\n }\n\n $messages = $this->getMessages($session['id']);\n $sessions = $this->getSessions();\n $authorProfiles = $this->getAuthorProfiles();\n $systemPrompts = $this->getSystemPrompts();\n $collections = $this->getAvailableCollections();\n\n $this->view('chat.index', [\n 'title' => $session['title'] ?? 'KI-Chat',\n 'session' => $session,\n 'messages' => $messages,\n 'sessions' => $sessions,\n 'authorProfiles' => $authorProfiles,\n 'systemPrompts' => $systemPrompts,\n 'collections' => $collections,\n 'models' => ModelConfig::getAll(),\n 'defaultModel' => ModelConfig::DEFAULT_MODEL,\n ]);\n }\n\n \/**\n * GET \/chat\/sessions (HTMX partial)\n * Return session list HTML\n *\/\n public function sessionList(): void\n {\n $sessions = $this->getSessions();\n $this->view('chat.partials.session-list', [\n 'sessions' => $sessions,\n 'currentUuid' => $_GET['current'] ?? null,\n ]);\n }\n\n \/**\n * POST \/chat\/{uuid}\/message (HTMX)\n * Process message and return HTML response\n *\/\n public function message(string $uuid): void\n {\n $session = $this->getSession($uuid);\n\n if ($session === null) {\n $this->view('chat.partials.error', ['error' => 'Session nicht gefunden.']);\n\n return;\n }\n\n $question = trim($_POST['message'] ?? '');\n $model = $this->validateModel($_POST['model'] ?? $session['model']);\n $sessionCollections = json_decode($session['collections'] ?? '[\"documents\"]', true) ?: ['documents'];\n $collections = $this->validateCollections($_POST['collections'] ?? $sessionCollections);\n $contextLimit = $this->validateContextLimit((int) ($_POST['context_limit'] ?? $session['context_limit'] ?? 5));\n $authorProfileId = $this->validateAuthorProfileId((int) ($_POST['author_profile_id'] ?? $session['author_profile_id'] ?? 0));\n $systemPromptId = (int) ($_POST['system_prompt_id'] ?? $session['system_prompt_id'] ?? 1);\n $temperature = $this->validateTemperature((float) ($_POST['temperature'] ?? $session['temperature'] ?? 0.7));\n $maxTokens = $this->validateMaxTokens((int) ($_POST['max_tokens'] ?? $session['max_tokens'] ?? 4096));\n\n \/\/ Update session if settings changed\n $currentLimit = (int) ($session['context_limit'] ?? 5);\n $currentProfileId = (int) ($session['author_profile_id'] ?? 0);\n $currentTemperature = (float) ($session['temperature'] ?? 0.7);\n $currentMaxTokens = (int) ($session['max_tokens'] ?? 4096);\n $collectionsJson = json_encode($collections);\n if ($model !== $session['model'] || $collectionsJson !== ($session['collections'] ?? '[\"documents\"]') || $contextLimit !== $currentLimit || $authorProfileId !== $currentProfileId || $temperature !== $currentTemperature || $maxTokens !== $currentMaxTokens) {\n $this->updateSessionSettings($session['id'], $model, $collections, $contextLimit, $authorProfileId, $temperature, $maxTokens);\n }\n\n if ($question === '') {\n $this->view('chat.partials.error', ['error' => 'Bitte gib eine Frage ein.']);\n\n return;\n }\n\n \/\/ Validate collection compatibility (vector dimensions)\n if (!empty($collections)) {\n $compatibility = $this->validateCollectionCompatibility($collections);\n if (!$compatibility['valid']) {\n $this->view('chat.partials.error', [\n 'error' => 'Collection-Fehler: ' . $compatibility['error'],\n 'details' => 'Bitte wähle nur Collections mit gleichem Embedding-Modell.',\n ]);\n\n return;\n }\n }\n\n try {\n \/\/ Save user message\n $this->saveMessage($session['id'], 'user', $question, $model);\n\n \/\/ Auto-set title from first message\n if ($session['title'] === null) {\n $title = mb_substr($question, 0, 50) . (mb_strlen($question) > 50 ? '...' : '');\n $this->updateSessionTitle($session['id'], $title);\n }\n\n \/\/ Get style prompt from author profile\n $stylePrompt = $this->getStylePromptFromProfile($authorProfileId);\n\n \/\/ Get system prompt from content_config\n $systemPromptData = $this->getSystemPromptById($systemPromptId);\n $customSystemPrompt = $this->extractPromptText($systemPromptData);\n\n \/\/ Track timing\n $startMicrotime = microtime(true);\n\n \/\/ Get response from AI\n $result = $this->askChat(\n $question,\n $model,\n $collections,\n $contextLimit,\n $stylePrompt,\n $customSystemPrompt,\n $temperature,\n $maxTokens\n );\n\n $endMicrotime = microtime(true);\n\n if (isset($result['error'])) {\n $this->view('chat.partials.error', ['error' => $result['error']]);\n\n return;\n }\n\n \/\/ Save assistant message with full tracking\n $tokensInput = $result['usage']['input_tokens'] ?? null;\n $tokensOutput = $result['usage']['output_tokens'] ?? null;\n $sources = $result['sources'] ?? [];\n\n $this->saveMessage(\n $session['id'],\n 'assistant',\n $result['answer'],\n $model,\n $tokensInput,\n $tokensOutput,\n $sources,\n $startMicrotime,\n $endMicrotime,\n $authorProfileId > 0 ? $authorProfileId : null,\n $systemPromptId > 0 ? $systemPromptId : null,\n $collectionsJson,\n $contextLimit\n );\n\n $this->renderResponse($question, $result, $model);\n } catch (\\Exception $e) {\n $this->view('chat.partials.error', ['error' => 'Fehler: ' . $e->getMessage()]);\n }\n }\n\n \/**\n * POST \/chat\/{uuid}\/title (HTMX)\n * Update session title\n *\/\n public function updateTitle(string $uuid): void\n {\n $session = $this->getSession($uuid);\n\n if ($session === null) {\n $this->notFound('Session nicht gefunden');\n }\n\n $title = trim($_POST['title'] ?? '');\n\n \/\/ Validate title\n if ($title === '') {\n $title = 'Neuer Chat';\n }\n\n \/\/ Max 100 characters\n $title = mb_substr($title, 0, 100);\n\n \/\/ Save to database\n $this->updateSessionTitle($session['id'], $title);\n\n \/\/ Return updated title HTML\n echo htmlspecialchars($title);\n }\n\n \/**\n * POST \/chat\/{uuid}\/system-prompt (HTMX)\n * Update session system prompt\n *\/\n public function systemPrompt(string $uuid): void\n {\n $session = $this->getSession($uuid);\n\n if ($session === null) {\n $this->notFound('Session nicht gefunden');\n }\n\n $systemPrompt = trim($_POST['system_prompt'] ?? '');\n\n \/\/ Validate and sanitize system prompt\n $systemPrompt = $this->validateSystemPrompt($systemPrompt);\n\n \/\/ Save to database\n $this->updateSystemPrompt($session['id'], $systemPrompt);\n\n \/\/ Return success message\n $this->view('chat.partials.success', ['message' => 'System-Prompt gespeichert.']);\n }\n\n \/**\n * GET \/chat\/{uuid}\/system-prompt (HTMX)\n * Get system prompt form\n *\/\n public function getSystemPrompt(string $uuid): void\n {\n $session = $this->getSession($uuid);\n\n if ($session === null) {\n $this->notFound('Session nicht gefunden');\n }\n\n $defaultPrompt = $this->getDefaultSystemPrompt();\n $currentPrompt = $session['system_prompt'] ?? '';\n\n $this->view('chat.partials.system-prompt-modal', [\n 'session' => $session,\n 'currentPrompt' => $currentPrompt,\n 'defaultPrompt' => $defaultPrompt,\n ]);\n }\n\n \/**\n * DELETE \/chat\/{uuid}\n * Delete a session\n *\/\n public function delete(string $uuid): void\n {\n $session = $this->getSession($uuid);\n\n if ($session !== null) {\n $stmt = $this->db->prepare('DELETE FROM chat_sessions WHERE id = ?');\n $stmt-... [TRUNCATED-f5386d48d086b377]",
"structuredPatch": [
{
"oldStart": 789,
"oldLines": 86,
"newStart": 789,
"newLines": 22,
"lines": [
" }",
" ",
" \/**",
"- * Render chat response HTML",
"+ * Render chat response HTML using view partial",
" *\/",
" private function renderResponse(string $question, array $result, string $model): void",
" {",
"- $answer = $result['answer'] ?? '';",
"- $sources = $result['sources'] ?? [];",
"- $usage = $result['usage'] ?? null;",
"- $modelLabel = str_starts_with($model, 'ollama:') ? substr($model, 7) : $model;",
"-",
" \/\/ Convert markdown-style formatting to HTML",
"- $answer = $this->formatAnswer($answer);",
"+ $formattedAnswer = $this->formatAnswer($result['answer'] ?? '');",
" ",
"- echo '<div class=\"chat-message chat-message--user\">';",
"- echo '<div class=\"message-content\">' . htmlspecialchars($question) . '<\/div>';",
"- echo '<\/div>';",
"-",
"- echo '<div class=\"chat-message chat-message--assistant\">';",
"- echo '<div class=\"message-content\">' . $answer . '<\/div>';",
"-",
"- if (count($sources) > 0) {",
"- $sourceCount = count($sources);",
"- $uniqueId = uniqid('sources-');",
"- echo '<div class=\"chat-sources chat-sources--collapsed\" id=\"' . $uniqueId . '\">';",
"- echo '<button type=\"button\" class=\"chat-sources__toggle\" onclick=\"toggleSources(\\'' . $uniqueId . '\\')\">';",
"- echo '<span class=\"chat-sources__count\">' . $sourceCount . ' Quelle' . ($sourceCount > 1 ? 'n' : '') . '<\/span>';",
"- echo '<span class=\"chat-sources__arrow\">▼<\/span>';",
"- echo '<\/button>';",
"- echo '<div class=\"chat-sources__list\">';",
"- foreach ($sources as $source) {",
"- $title = htmlspecialchars($source['title'] ?? 'Unbekannt');",
"- $score = round(($source['score'] ?? 0) * 100);",
"- $content = isset($source['content']) ? htmlspecialchars(mb_substr($source['content'], 0, 200)) : '';",
"- echo '<div class=\"source-item\">';",
"- echo '<div class=\"source-item__header\">';",
"- echo '<span class=\"source-item__title\">' . $title . '<\/span>';",
"- echo '<span class=\"source-item__score\">' . $score . '%<\/span>';",
"- echo '<\/div>';",
"- if ($content !== '') {",
"- echo '<div class=\"source-item__content\">\"' . $content . (mb_strlen($source['content'] ?? '') > 200 ? '...' : '') . '\"<\/div>';",
"- }",
"- echo '<\/div>';",
"- }",
"- echo '<\/div>';",
"- echo $this->renderMessageMeta($modelLabel, $usage, $model);",
"- echo '<\/div>';",
"- } else {",
"- echo $this->renderMessageMeta($modelLabel, $usage, $model);",
"- }",
"-",
"- echo '<\/div>';",
"+ $this->view('chat.partials.response', [",
"+ 'question' => $question,",
"+ 'result' => $result,",
"+ 'model' => $model,",
"+ 'formattedAnswer' => $formattedAnswer,",
"+ ]);",
" }",
" ",
" \/**",
"- * Render message meta (model, tokens, cost)",
"- *\/",
"- private function renderMessageMeta(string $modelLabel, ?array $usage, string $model): string",
"- {",
"- $html = '<div class=\"message-meta\">';",
"- $html .= '<span class=\"model-info\">Modell: ' . htmlspecialchars($modelLabel) . '<\/span>';",
"-",
"- if (str_starts_with($model, 'claude-') && $usage !== null) {",
"- $inputTokens = $usage['input_tokens'] ?? 0;",
"- $outputTokens = $usage['output_tokens'] ?? 0;",
"- $cost = $this->calculateCost($inputTokens, $outputTokens);",
"-",
"- $html .= '<span class=\"tokens-info\">';",
"- $html .= '<span class=\"tokens-input\" title=\"Input-Tokens\">↓' . number_format($inputTokens) . '<\/span> ';",
"- $html .= '<span class=\"tokens-output\" title=\"Output-Tokens\">↑' . number_format($outputTokens) . '<\/span>';",
"- $html .= '<\/span>';",
"- $html .= '<span class=\"cost-info\" title=\"Geschätzte Kosten\">~$' . number_format($cost, 4) . '<\/span>';",
"- } elseif (str_starts_with($model, 'ollama:')) {",
"- $html .= '<span class=\"tokens-info local\">lokal<\/span>';",
"- }",
"-",
"- $html .= '<\/div>';",
"-",
"- return $html;",
"- }",
"-",
"- \/**",
" * Calculate cost for Claude API usage",
" * Opus 4.5 Pricing: $15 per 1M input, $75 per 1M output",
" *\/"
]
}
],
"userModified": false,
"replaceAll": false
}
}