import * as React from 'react'; import { Maximize2, Minimize2, PanelRightClose, RotateCcw, Sparkles } from 'lucide-react'; import { cn } from '@/lib/utils'; import { Surface } from '@/components/ds/Surface'; import { IconButton } from '@/components/ds/IconButton'; import { Alert } from '@/components/ds/Alert'; import { useAssistantFacts, useConversation, useCurrentUserName } from './useAssistant'; import { buildIntro, buildPrompts } from './dynamic'; import { Message, ThinkingIndicator, TurnDivider } from './AssistantMessage'; import { PromptInput } from './PromptInput'; import { PromptChips } from './PromptChips'; /** * Owliver — the dashboard's contextual panel. * * One component, reused on every supported page; only `context` differs. * * The frame is fixed at three parts — header, body, composer — and only the body * scrolls. That is what lets the panel stay pinned beside a long dashboard * without ever growing the page or scrolling its own input out of reach. * * The frame does not change between states, only the body's content does: an * empty thread shows the greeting and this page's most relevant fact, and once a * question is asked the same region holds the conversation. Keeping the composer * anchored either way means the primary action never moves under the user, and * the panel never reads as two different components. * * `expanded` widens the same panel into an analysis workspace. `data-wide` lets * individual response blocks use the extra room (a KPI row goes to three * columns) without any of them needing to know the panel's pixel width. * * Independent of Owliver. Answers come from a provider (see provider.js) that * can later be pointed at an external service without changing anything here. */ export default function KrowAssistant({ context, className, expanded = false, onClose, onExpand, onRestore, }) { const facts = useAssistantFacts(); const userName = useCurrentUserName(); const { messages, pending, error, busy, send, stop, reset } = useConversation({ contextId: context.id, facts, }); const [input, setInput] = React.useState(''); const scrollRef = React.useRef(null); const isEmpty = messages.length === 0 && !pending; /* Greeting and suggestions come from live data, so they recompute only when the data or the page actually changes. */ const intro = React.useMemo( () => buildIntro(context.id, facts, userName), [context.id, facts, userName] ); const prompts = React.useMemo(() => buildPrompts(context.id, facts), [context.id, facts]); /* Follow the newest content. Direct `scrollTop` rather than smooth scrolling: at streaming frequency a smooth scroll never catches up and the thread visibly lags the text. */ React.useEffect(() => { const el = scrollRef.current; if (el) el.scrollTop = el.scrollHeight; }, [messages, pending]); const submit = React.useCallback((text) => { const question = String(text).trim(); if (!question || busy) return; send({ question }); setInput(''); }, [busy, send]); /* A chip is already a complete question, so it runs on click. */ const runPrompt = React.useCallback((prompt) => { if (busy) return; setInput(''); send({ question: prompt.prompt, capability: prompt.capability }); }, [busy, send]); const composer = ( ); return ( {/* Header */}

Owliver

{context.page}

{/* Window controls */}
{messages.length > 0 && ( )} {expanded ? onRestore && ( ) : onExpand && ( )} {onClose && ( )}
{/* ── Body: the only region that scrolls ─────────────────────────── */}
{isEmpty ? ( /* Greeting and the single most relevant fact about this page. Top aligned rather than centred: it is the first thing in a conversation, not a splash screen, so it belongs where the first message would be. */

{intro.title}

{intro.greeting}

{intro.description}

) : (
{messages.map((message, i) => ( {i > 0 && message.role === 'user' && } ))} {pending?.thinking && } {pending && !pending.thinking && ( )} {error && {error}}
)}
{/* ── Composer: fixed to the bottom in both states ───────────────── */}
{/* All suggestions on the landing screen, where they teach what can be asked. Capped once a thread exists, because from then on the vertical space belongs to the conversation. Expanded fits more per line, so it can afford one more. */} {!busy && ( )} {composer}

Owliver reads this page's data. Check anything you act on.

); }