"use client"; import { useCallback, useEffect, useRef, useState } from "react"; import { usePathname } from "next/navigation"; import { trpc } from "~/lib/trpc/client.js"; import { ChatMessage, TypingIndicator } from "./ChatMessage.js"; /** Map route prefixes to human-readable page context for the AI */ const ROUTE_CONTEXT: Record = { "/dashboard": "Dashboard — Übersicht mit KPIs, aktive Projekte, Ressourcen-Auslastung", "/timeline": "Timeline — Gantt-artige Ansicht aller Allokationen und Projekte", "/allocations": "Allokationen — Liste aller Zuweisungen von Ressourcen zu Projekten", "/staffing": "Staffing — Projektbesetzung und Kapazitätsplanung", "/resources": "Ressourcen — Liste aller Mitarbeiter mit Details (FTE, LCR, Skills, Chapter)", "/projects": "Projekte — Liste aller Projekte mit Budget, Status, Zeitraum", "/roles": "Rollen — Verwaltung der verfügbaren Rollen", "/estimates": "Estimating — Aufwandsschätzungen für Projekte", "/vacations/my": "Meine Urlaube — Eigene Urlaubsanträge und Saldo", "/vacations": "Urlaubsverwaltung — Alle Urlaubsanträge, Genehmigungen, Team-Kalender", "/analytics/skills": "Skills Analytics — Skill-Verteilung und -Analyse über alle Ressourcen", "/analytics/computation-graph": "Computation Graph — Berechnungsvisualisierung für Budget/Kosten", "/reports/chargeability": "Chargeability Report — Auslastungsanalyse pro Ressource", "/admin/settings": "Admin-Einstellungen — System-Konfiguration, AI-Credentials, SMTP", "/admin/users": "Benutzerverwaltung — Rollen, Berechtigungen, Zugänge", }; function resolvePageContext(pathname: string): string { // Try exact match first, then prefix match (longest first) const exact = ROUTE_CONTEXT[pathname]; if (exact) return exact; const sorted = Object.keys(ROUTE_CONTEXT).sort((a, b) => b.length - a.length); for (const prefix of sorted) { const ctx = ROUTE_CONTEXT[prefix]; if (pathname.startsWith(prefix) && ctx) return ctx; } return pathname; } interface Message { role: "user" | "assistant"; content: string; } export function ChatDrawer({ onClose }: { onClose: () => void }) { const pathname = usePathname(); const [messages, setMessages] = useState([]); const [input, setInput] = useState(""); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const scrollRef = useRef(null); const inputRef = useRef(null); const chatMutation = trpc.assistant.chat.useMutation(); // Auto-scroll to bottom on new messages useEffect(() => { const el = scrollRef.current; if (el) el.scrollTop = el.scrollHeight; }, [messages, isLoading]); // Focus input on mount useEffect(() => { inputRef.current?.focus(); }, []); const sendMessage = useCallback(async () => { const text = input.trim(); if (!text || isLoading) return; setInput(""); setError(null); const userMsg: Message = { role: "user", content: text }; const updated = [...messages, userMsg]; setMessages(updated); setIsLoading(true); try { const reply = await chatMutation.mutateAsync({ messages: updated.map((m) => ({ role: m.role, content: m.content })), ...(pathname ? { pageContext: resolvePageContext(pathname) } : {}), }); setMessages((prev) => [...prev, { role: "assistant", content: reply.content }]); } catch (err) { const msg = err instanceof Error ? err.message : "Something went wrong"; setError(msg); } finally { setIsLoading(false); } }, [input, isLoading, messages, chatMutation]); const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); void sendMessage(); } }; return ( <> {/* Backdrop */}
{/* Panel */}
{/* Header */}

Planarchy Assistant

{/* Messages */}
{messages.length === 0 && !isLoading && (

Frag mich etwas!

z.B. "Welche Ressourcen gibt es?" oder "Budget von Z033T593?"

)} {messages.map((msg, i) => ( ))} {isLoading && } {error && (
{error}
)}
{/* Input */}