Implement AI Assistant with Ollama LLM integration and UI enhancements#15
Implement AI Assistant with Ollama LLM integration and UI enhancements#15PalmarHealer wants to merge 11 commits into
Conversation
Implements an AI assistant that can query and modify wochenplan data through a chat interface. Runs on a self-hosted Qwen 3 8B model via Ollama on the Leipzig server (RTX 3050 GPU). Features: - Filament-native UI: text input widget + conversations table on list page, chat view with streaming responses on conversation page - 15 composite tools covering all resources (lessons, templates, absences, rooms, times, colors, layouts, deviations, users, roles, activity logs, PDF export, FAQ, lunch reload) - All tools permission-gated via Spatie/Shield policies - Streaming responses via SSE (Server-Sent Events) - Tool calls handled non-streaming for reliability, final response streamed token-by-token from Ollama - Chat conversations persisted in DB with title auto-generation - Conversations renameable and deleteable via Filament table actions - URL-encoded conversation IDs (?chat=ID) - Auto-focus input on keypress - PDF export with download button in chat - docker-compose.local.yml for local Docker testing New files: - Database: chat_conversations, chat_messages tables - Services: OllamaClient, ChatService, ToolRegistry, 15 composite tools - UI: AiChat Filament page with Livewire + Alpine.js streaming - Controller: AiChatStreamController for SSE endpoint - Config: ai-chat.php for Ollama connection settings - Route: /assistant/stream (SSE), /assistant/pdf (PDF download) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Switch to Qwen 3 8B with /no_think for fast responses - Keep model loaded permanently (keep_alive: -1) - Stream all responses including post-tool-call replies - Filter <think> tags during streaming (no text jumping) - Require user approval for all mutating tool calls with preview - Complete server-side even if user navigates away (ignore_user_abort) - Consolidated 45 tools into 15 composite tools (prevents GPU OOM) - Added tools: FAQ, lunch reload, layout deviations, users, roles - Shortened all tool descriptions for faster prompt processing - Page subheading disclaimer instead of inline text - Autoscroll via Livewire morph hook + DOM ID - Matching bubble layout during streaming and after (flex gap-0.5) - Strip leading newlines from streamed content - Tool call counter shown live during streaming - No flash between streamed and final message - PDF download button without duplicate text link - Scrollbar padding (pr-3) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds an Ollama-backed AI assistant to the Wochenplan app, including a Filament UI for chat, persistent conversations/messages, an SSE streaming endpoint, and a set of “tools” the model can call to read/update application data.
Changes:
- Introduces chat persistence (conversations + messages) and an Ollama client/service layer with tool-call support.
- Adds a Filament “Assistant” page + Blade/Alpine UI to stream responses and approve/reject pending actions.
- Adds new web routes and Docker/env configuration for connecting to an Ollama server.
Reviewed changes
Copilot reviewed 72 out of 73 changed files in this pull request and generated 17 comments.
Show a summary per file
| File | Description |
|---|---|
| routes/web.php | Adds assistant streaming route and a PDF download route. |
| resources/views/filament/pages/ai-chat.blade.php | New chat UI (list view + streaming chat view with pending-action cards). |
| docker-compose.yml | Adds env vars for Ollama base URL/model. |
| docker-compose.local.yml | Adds a local compose setup with Ollama env defaults. |
| database/seeders/ShieldSeeder.php | Adds Filament page permission for AiChat and auto_approve_ai_actions. |
| database/migrations/2026_03_22_000001_create_chat_conversations_table.php | Creates chat_conversations table. |
| database/migrations/2026_03_22_000002_create_chat_messages_table.php | Creates chat_messages table with tool-call/pending-action fields. |
| config/ai-chat.php | New config for Ollama base URL/model/token/temperature. |
| app/Services/AiChat/AiChatTool.php | Defines the tool interface for LLM tool calls. |
| app/Services/AiChat/ToolRegistry.php | Registers tools and exposes tool schemas to Ollama. |
| app/Services/AiChat/OllamaClient.php | Implements non-streaming + streaming Ollama chat calls and title generation. |
| app/Services/AiChat/ChatService.php | Implements message processing, tool execution, pending approvals, and message formatting. |
| app/Services/AiChat/Tools/GetScheduleForDate.php | Tool to assemble a “day schedule” payload (lessons/templates/absences/lunch). |
| app/Services/AiChat/Tools/GetFaq.php | Tool providing static FAQ/help content. |
| app/Services/AiChat/Tools/ExportDayPdf.php | Tool to generate a day PDF and return a download URL. |
| app/Services/AiChat/Tools/ReloadLunch.php | Tool to clear/reload lunch cache for a date. |
| app/Services/AiChat/Tools/ListActivityLogs.php | Tool to query activity logs with filters. |
| app/Services/AiChat/Tools/ListUsers.php | Standalone list-users tool (not currently registered). |
| app/Services/AiChat/Tools/ListTimes.php | Standalone list-times tool (not currently registered). |
| app/Services/AiChat/Tools/ListRooms.php | Standalone list-rooms tool (not currently registered). |
| app/Services/AiChat/Tools/ListRoles.php | Standalone list-roles tool (not currently registered). |
| app/Services/AiChat/Tools/ListLessons.php | Standalone list-lessons tool (not currently registered). |
| app/Services/AiChat/Tools/ListLessonTemplates.php | Standalone list-templates tool (not currently registered). |
| app/Services/AiChat/Tools/ListLayouts.php | Standalone list-layouts tool (not currently registered). |
| app/Services/AiChat/Tools/ListLayoutDeviations.php | Standalone list-layout-deviations tool (not currently registered). |
| app/Services/AiChat/Tools/ListColors.php | Standalone list-colors tool (not currently registered). |
| app/Services/AiChat/Tools/ListAbsences.php | Standalone list-absences tool (not currently registered). |
| app/Services/AiChat/Tools/CreateUser.php | Standalone create-user tool (not currently registered). |
| app/Services/AiChat/Tools/UpdateUser.php | Standalone update-user tool (not currently registered). |
| app/Services/AiChat/Tools/DeleteUser.php | Standalone delete-user tool (not currently registered). |
| app/Services/AiChat/Tools/CreateTime.php | Standalone create-time tool (not currently registered). |
| app/Services/AiChat/Tools/UpdateTime.php | Standalone update-time tool (not currently registered). |
| app/Services/AiChat/Tools/DeleteTime.php | Standalone delete-time tool (not currently registered). |
| app/Services/AiChat/Tools/CreateRoom.php | Standalone create-room tool (not currently registered). |
| app/Services/AiChat/Tools/UpdateRoom.php | Standalone update-room tool (not currently registered). |
| app/Services/AiChat/Tools/DeleteRoom.php | Standalone delete-room tool (not currently registered). |
| app/Services/AiChat/Tools/CreateRole.php | Standalone create-role tool (not currently registered). |
| app/Services/AiChat/Tools/UpdateRole.php | Standalone update-role tool (not currently registered). |
| app/Services/AiChat/Tools/DeleteRole.php | Standalone delete-role tool (not currently registered). |
| app/Services/AiChat/Tools/CreateLesson.php | Standalone create-lesson tool (not currently registered). |
| app/Services/AiChat/Tools/UpdateLesson.php | Standalone update-lesson tool (not currently registered). |
| app/Services/AiChat/Tools/DeleteLesson.php | Standalone delete-lesson tool (not currently registered). |
| app/Services/AiChat/Tools/CreateLessonTemplate.php | Standalone create-template tool (not currently registered). |
| app/Services/AiChat/Tools/UpdateLessonTemplate.php | Standalone update-template tool (not currently registered). |
| app/Services/AiChat/Tools/DeleteLessonTemplate.php | Standalone delete-template tool (not currently registered). |
| app/Services/AiChat/Tools/CreateLayout.php | Standalone create-layout tool (not currently registered). |
| app/Services/AiChat/Tools/UpdateLayout.php | Standalone update-layout tool (not currently registered). |
| app/Services/AiChat/Tools/DeleteLayout.php | Standalone delete-layout tool (not currently registered). |
| app/Services/AiChat/Tools/CreateLayoutDeviation.php | Standalone create-layout-deviation tool (not currently registered). |
| app/Services/AiChat/Tools/UpdateLayoutDeviation.php | Standalone update-layout-deviation tool (not currently registered). |
| app/Services/AiChat/Tools/DeleteLayoutDeviation.php | Standalone delete-layout-deviation tool (not currently registered). |
| app/Services/AiChat/Tools/CreateColor.php | Standalone create-color tool (not currently registered). |
| app/Services/AiChat/Tools/UpdateColor.php | Standalone update-color tool (not currently registered). |
| app/Services/AiChat/Tools/DeleteColor.php | Standalone delete-color tool (not currently registered). |
| app/Services/AiChat/Tools/CreateAbsence.php | Standalone create-absence tool (not currently registered). |
| app/Services/AiChat/Tools/UpdateAbsence.php | Standalone update-absence tool (not currently registered). |
| app/Services/AiChat/Tools/DeleteAbsence.php | Standalone delete-absence tool (not currently registered). |
| app/Services/AiChat/Tools/Composite/ManageUsers.php | Composite “manage users” tool (list/create/update/delete via one entrypoint). |
| app/Services/AiChat/Tools/Composite/ManageTimes.php | Composite “manage times” tool. |
| app/Services/AiChat/Tools/Composite/ManageRooms.php | Composite “manage rooms” tool. |
| app/Services/AiChat/Tools/Composite/ManageRoles.php | Composite “manage roles” tool. |
| app/Services/AiChat/Tools/Composite/ManageLessons.php | Composite “manage lessons” tool. |
| app/Services/AiChat/Tools/Composite/ManageLessonTemplates.php | Composite “manage lesson templates” tool. |
| app/Services/AiChat/Tools/Composite/ManageLayouts.php | Composite “manage layouts” tool. |
| app/Services/AiChat/Tools/Composite/ManageLayoutDeviations.php | Composite “manage layout deviations” tool. |
| app/Services/AiChat/Tools/Composite/ManageColors.php | Composite “manage colors” tool. |
| app/Services/AiChat/Tools/Composite/ManageAbsences.php | Composite “manage absences” tool. |
| app/Models/ChatConversation.php | New Eloquent model for chat conversations. |
| app/Models/ChatMessage.php | New Eloquent model for chat messages (tool calls + pending actions). |
| app/Http/Controllers/AiChatStreamController.php | New SSE controller to drive tool-call rounds + final streaming response. |
| app/Filament/Pages/AiChat.php | New Filament page implementing chat list + chat view behavior. |
| .env.example | Adds Ollama env variable placeholders. |
| docker-compose.yml | Adds Ollama env variables to app service. |
| .gitignore | Ignores .claude/settings.local.json. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| private function registerAll(): void | ||
| { | ||
| $this->tools = [ | ||
| new GetScheduleForDate, | ||
| new ManageLessons, | ||
| new ManageLessonTemplates, | ||
| new ManageAbsences, | ||
| new ManageRooms, | ||
| new ManageTimes, | ||
| new ManageColors, | ||
| new ManageLayouts, | ||
| new ManageLayoutDeviations, | ||
| new ManageUsers, | ||
| new ManageRoles, | ||
| new ListActivityLogs, | ||
| new ExportDayPdf, | ||
| new GetFaq, | ||
| new ReloadLunch, | ||
| ]; |
There was a problem hiding this comment.
ToolRegistry registers only the composite manage_* tools, but the system prompt + several tool descriptions instruct the model to call list_rooms, list_times, list_colors, create_lesson, etc. Those tool names are not registered here, so the model will produce "Unknown tool" calls. Either register the list_* / create_* / update_* / delete_* tools, or update the prompt + descriptions to use only the registered manage_* tools.
| ->query( | ||
| ChatConversation::where('user_id', auth()->id())->latest('updated_at') | ||
| ) |
There was a problem hiding this comment.
The conversation list is sorted by updated_at, but sending messages doesn’t update the conversation timestamp (no touch() and ChatMessage doesn’t declare $touches = ['conversation']). After the title is set, active chats may stop bubbling to the top. Consider touching the conversation when creating messages or adding $touches on ChatMessage so updated_at reflects activity.
| // AI Chat streaming endpoint | ||
| Route::get('/assistant/stream', [AiChatStreamController::class, 'stream']) | ||
| ->name('assistant.stream') | ||
| ->middleware(['auth']); | ||
|
|
||
| // AI Chat PDF download | ||
| Route::get('/assistant/pdf', function (\Illuminate\Http\Request $request) { | ||
| $request->validate(['date' => 'required|date']); | ||
| $date = \Carbon\Carbon::parse($request->input('date')); | ||
| $pdfService = app(\App\Services\PdfExportService::class); | ||
| $base64 = $pdfService->getOrGeneratePdf($date->toDateString()); | ||
| if (! $base64) { | ||
| abort(404, 'PDF nicht verfügbar.'); | ||
| } | ||
| $binary = base64_decode($base64); | ||
| $filename = $date->locale('de')->translatedFormat('l, d.m.Y').'.pdf'; | ||
|
|
||
| return response()->streamDownload(fn () => print ($binary), $filename, ['Content-Type' => 'application/pdf']); | ||
| })->name('assistant.pdf')->middleware(['auth']); |
There was a problem hiding this comment.
New routes add important auth/authorization behavior (/assistant/stream, /assistant/pdf) but there are no accompanying feature tests. Given existing route/controller feature tests, add coverage for: unauthenticated requests are rejected, /assistant/pdf enforces view_day::pdf, and /assistant/stream rejects invalid/missing conversation_id and enforces conversation ownership.
- Change /assistant/stream from GET to POST with CSRF protection - Add view_day::pdf authorization check on /assistant/pdf route - Add strict mode and error handling to base64_decode for PDF downloads - Re-check permissions in approveAction() before executing pending tools - Scope absences by view_any_absence permission in GetScheduleForDate - Treat composite tool "list" actions as read-only (no approval needed) - Fix tool_call_id uniqueness across rounds in both controller and service - Update system prompt and tool descriptions to reference manage_* tools - Add $touches to ChatMessage so conversations sort by latest activity - Remove dead $tmpFile/$tmpPath and unused $hadToolCalls variables - Default ollama_base_url to localhost instead of private IP Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 72 out of 73 changed files in this pull request and generated 7 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| } | ||
| $messages[] = [ | ||
| 'role' => 'tool', | ||
| 'content' => $msg->content ?? '{}', |
There was a problem hiding this comment.
In buildMessages(), tool messages are sent to the LLM without their tool_call_id. For OpenAI-style tool calling (which Ollama mirrors), the model uses tool_call_id to associate tool results with the corresponding tool_calls; omitting it can break multi-tool-call rounds or cause the model to ignore tool results. Include tool_call_id (and typically tool name) in the message payload for role=tool when building $messages.
| 'content' => $msg->content ?? '{}', | |
| 'content' => $msg->content ?? '{}', | |
| 'tool_call_id' => $msg->tool_call_id ?? null, | |
| 'name' => $msg->tool_name ?? null, |
| <div class="prose dark:prose-invert prose-sm max-w-none text-sm [&>*]:my-0 [&>*+*]:mt-1.5"> | ||
| {!! \Illuminate\Support\Str::markdown($msg['content']) !!} | ||
| </div> |
There was a problem hiding this comment.
Assistant messages are rendered with {!! Str::markdown(...) !!} which outputs unescaped HTML. Since the content comes from an LLM (untrusted), this can enable stored XSS if the markdown converter allows raw HTML (Laravel's default CommonMark config does unless explicitly disabled). Render markdown in a safe mode (strip/escape HTML + disallow unsafe links) or sanitize the generated HTML before output.
| try { | ||
| const resp = await fetch('/assistant/stream', { | ||
| method: 'POST', | ||
| headers: { | ||
| 'Accept': 'text/event-stream', | ||
| 'Content-Type': 'application/json', | ||
| 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content, | ||
| }, | ||
| body: JSON.stringify({ conversation_id: convId }), | ||
| }); | ||
| const reader = resp.body.getReader(); | ||
| const dec = new TextDecoder(); | ||
| let buf = '', evt = 'content'; |
There was a problem hiding this comment.
The streaming fetch() handler assumes a successful response and immediately calls resp.body.getReader(). If the request fails (non-2xx) or the browser provides no body (e.g., network error), this will throw and leave the UI stuck in streaming=true. Add an resp.ok/resp.body guard and emit an error state (and set streaming=false) before returning.
| AZURE_PROXY: | ||
| OLLAMA_BASE_URL: http://100.90.166.15:11434 | ||
| AI_CHAT_MODEL: qwen3:8b |
There was a problem hiding this comment.
docker-compose.local.yml hardcodes OLLAMA_BASE_URL to a specific private IP. Committing environment-specific infrastructure details makes local setup brittle and can leak internal network topology. Prefer leaving it blank, using a default like http://host.docker.internal:11434, or referencing a .env/override file for per-developer values.
| // AI Chat streaming endpoint | ||
| Route::post('/assistant/stream', [AiChatStreamController::class, 'stream']) | ||
| ->name('assistant.stream') | ||
| ->middleware(['auth']); | ||
|
|
||
| // AI Chat PDF download | ||
| Route::get('/assistant/pdf', function (\Illuminate\Http\Request $request) { | ||
| $request->validate(['date' => 'required|date']); | ||
|
|
||
| if (! $request->user()->can('view_day::pdf')) { | ||
| abort(403, 'Keine Berechtigung.'); | ||
| } | ||
|
|
||
| $date = \Carbon\Carbon::parse($request->input('date')); | ||
| $pdfService = app(\App\Services\PdfExportService::class); | ||
| $base64 = $pdfService->getOrGeneratePdf($date->toDateString()); | ||
| if (! $base64) { | ||
| abort(404, 'PDF nicht verfügbar.'); | ||
| } | ||
| $binary = base64_decode($base64, true); | ||
| if ($binary === false) { | ||
| abort(500, 'PDF-Daten fehlerhaft.'); | ||
| } | ||
| $filename = $date->locale('de')->translatedFormat('l, d.m.Y').'.pdf'; | ||
|
|
||
| return response()->streamDownload(fn () => print ($binary), $filename, ['Content-Type' => 'application/pdf']); | ||
| })->name('assistant.pdf')->middleware(['auth']); |
There was a problem hiding this comment.
New assistant endpoints (/assistant/stream and /assistant/pdf) introduce significant behavior (SSE streaming, tool approval workflow, permission-gated PDF download) but there are no feature tests added alongside. The repo already has feature tests for similar HTTP endpoints (e.g., LunchController); adding tests here would help prevent regressions around auth/authorization and error handling.
- Disable tools in approve/reject LLM continuation to force natural language - Include tool_call_id and name in buildMessages() for proper tool correlation - Sanitize markdown output with html_input=strip and allow_unsafe_links=false - Add resp.ok/resp.body guard in streaming fetch to prevent stuck UI - Add curl error handling in streamFromOllama with SSE error event - Change docker-compose.local.yml OLLAMA_BASE_URL to host.docker.internal Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 72 out of 73 changed files in this pull request and generated 7 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| try { | ||
| const resp = await fetch('/assistant/stream', { | ||
| method: 'POST', | ||
| headers: { | ||
| 'Accept': 'text/event-stream', | ||
| 'Content-Type': 'application/json', | ||
| 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content, | ||
| }, | ||
| body: JSON.stringify({ conversation_id: convId }), |
There was a problem hiding this comment.
In startStream() wird der Endpoint hart auf fetch('/assistant/stream', ...) verdrahtet. Das bricht in Setups mit Prefix/Subdirectory (oder wenn sich die Route ändert). Bitte die URL serverseitig via route('assistant.stream') in die View injizieren (z.B. als data- Attribut) und im JS verwenden.
| # AI Chat (Ollama on Leipzig server) | ||
| OLLAMA_BASE_URL: | ||
| AI_CHAT_MODEL: qwen3:8b |
There was a problem hiding this comment.
OLLAMA_BASE_URL: ist in docker-compose als leerer Wert definiert. Das setzt die ENV-Variable im Container typischerweise auf leeren String und überschreibt damit den Default aus config/ai-chat.php (führt dazu, dass Ollama-Requests gegen '/api/chat' statt gegen http://... gehen). Bitte den Eintrag entfernen oder einen gültigen Default/Platzhalter setzen (oder per ${OLLAMA_BASE_URL:-http://...} arbeiten).
| if (trim($after) !== '') { | ||
| $ctrl->sse('content', ['text' => $after]); | ||
| } | ||
| } | ||
| // Skip think content — don't send to client | ||
| } else { | ||
| $ctrl->sse('content', ['text' => $chunk]); |
There was a problem hiding this comment.
Beim Streaming werden aktuell nur <think>...</think> Blöcke herausgefiltert. OllamaClient::stripThinkingTags() entfernt aber zusätzlich <tool_call>...</tool_call>. Wenn das Modell (trotz Prompt) <tool_call> ausgibt, würde das ungefiltert zum Client gestreamt und im UI sichtbar. Bitte beim Streaming ebenfalls <tool_call>-Tags filtern/strippen (oder zentral die bestehende Strip-Funktion nutzen).
| if (trim($after) !== '') { | |
| $ctrl->sse('content', ['text' => $after]); | |
| } | |
| } | |
| // Skip think content — don't send to client | |
| } else { | |
| $ctrl->sse('content', ['text' => $chunk]); | |
| // Additionally strip any <tool_call>...</tool_call> blocks before sending | |
| $afterStripped = preg_replace('/<tool_call>.*?<\/tool_call>/s', '', $after); | |
| if (trim($afterStripped) !== '') { | |
| $ctrl->sse('content', ['text' => $afterStripped]); | |
| } | |
| } | |
| // Skip think content — don't send to client | |
| } else { | |
| // Strip any <tool_call>...</tool_call> blocks before sending regular chunks | |
| $chunkStripped = preg_replace('/<tool_call>.*?<\/tool_call>/s', '', $chunk); | |
| if (trim($chunkStripped) !== '') { | |
| $ctrl->sse('content', ['text' => $chunkStripped]); | |
| } |
| Log::error('AI Chat tool execution error', [ | ||
| 'tool' => $tool->name(), | ||
| 'error' => $e->getMessage(), | ||
| ]); | ||
|
|
||
| return ['error' => 'Fehler bei der Ausführung: '.$e->getMessage()]; |
There was a problem hiding this comment.
executeTool() gibt bei Exceptions die rohe Exception-Message an den Chat zurück (Fehler bei der Ausführung: ...). Das kann internere Details (SQL, Pfade, etc.) an UI/LLM leaken. Bitte für Benutzer/LLM eine generische Fehlermeldung zurückgeben und Details nur serverseitig loggen (ggf. mit Korrelations-ID).
| Log::error('AI Chat tool execution error', [ | |
| 'tool' => $tool->name(), | |
| 'error' => $e->getMessage(), | |
| ]); | |
| return ['error' => 'Fehler bei der Ausführung: '.$e->getMessage()]; | |
| $correlationId = uniqid('tool_', true); | |
| Log::error('AI Chat tool execution error', [ | |
| 'tool' => $tool->name(), | |
| 'error' => $e->getMessage(), | |
| 'correlation_id' => $correlationId, | |
| ]); | |
| return [ | |
| 'error' => 'Es ist ein Fehler bei der Ausführung des Werkzeugs aufgetreten. Bitte versuchen Sie es später erneut. (Fehler-ID: ' . $correlationId . ')', | |
| ]; |
| return [ | ||
| 'success' => true, | ||
| 'download_url' => "/assistant/pdf?date={$date}", | ||
| 'message' => "PDF für {$date} wurde erstellt.", |
There was a problem hiding this comment.
Der Tool-Return enthält einen hartcodierten Pfad "/assistant/pdf?date=...". Das ist fragil bei URL-Prefixes oder Route-Änderungen und erfordert manuelles URL-Encoding. Bitte die URL über route('assistant.pdf', ['date' => $date]) erzeugen (und idealerweise url() für absolute Links), damit Download-Links zuverlässig bleiben.
| $date = \Carbon\Carbon::parse($request->input('date')); | ||
| $pdfService = app(\App\Services\PdfExportService::class); | ||
| $base64 = $pdfService->getOrGeneratePdf($date->toDateString()); |
There was a problem hiding this comment.
In diesem Download-Endpoint wird immer getOrGeneratePdf() genutzt, wodurch auch für Vergangenheitsdaten PDFs (re)generiert und DB-Einträge verändert werden können. In app/Filament/Pages/Day::downloadPdf() wird für vergangene Daten bewusst getExistingPdf() verwendet. Bitte hier konsistent sein (für vergangene Daten nur ausliefern, für heute/zukünftig ggf. generieren) und zusätzlich die Locale/Formatierung wie im Day-Download über config('app.locale') statt fest de verwenden.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
- Use route() helper for stream URL in Blade and PDF download URL in tool
- Set sensible OLLAMA_BASE_URL defaults in .env.example and docker-compose
- Filter <tool_call> tags from streamed chunks (not just <think>)
- Use generic error messages in executeTool() with correlation IDs
- Past dates: serve existing PDFs only, consistent with Day page behavior
- Use config('app.locale') for PDF filename locale
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Local compose overrides should not be tracked in version control. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
9d0d84e to
e084d20
Compare
- Add requiredPermissionForAction() to AiChatTool interface - Composite tools return action-specific permissions (e.g. create_lesson, update_lesson, delete_lesson) instead of just the base view permission - Controller and service now check granular permissions before executing or showing pending action prompts - Enable custom_permissions in Shield config so auto_approve_ai_actions is visible and manageable in the role editor UI Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Use Blade csrf_token() instead of meta tag query (Filament layout may not have the csrf-token meta tag, causing 419 errors) - Add migration to create auto_approve_ai_actions permission in DB so it appears in Shield's custom permissions section - Show error message to user when stream request fails Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ActivityLogResource only uses view prefix, not view_any. Also cleaned up page_Dashboard and view_any_activity::log permissions from DB (Dashboard is excluded from Shield). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
No description provided.