ui changes

This commit is contained in:
2026-08-10 19:36:45 +05:30
parent 409dffe938
commit 5cd49a1655
49 changed files with 7988 additions and 470 deletions

View File

@@ -1,10 +1,9 @@
import * as React from 'react';
import { Maximize2, Minimize2, RotateCcw, Sparkles, X } from 'lucide-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 { KROW_LOGO_URL } from '@/assets/brand';
import { useAssistantFacts, useConversation, useCurrentUserName } from './useAssistant';
import { buildIntro, buildPrompts } from './dynamic';
import { Message, ThinkingIndicator, TurnDivider } from './AssistantMessage';
@@ -12,7 +11,7 @@ import { PromptInput } from './PromptInput';
import { PromptChips } from './PromptChips';
/**
* Krow AI Assistant — the dashboard's contextual assistant.
* Owliver — the dashboard's contextual panel.
*
* One component, reused on every supported page; only `context` differs.
*
@@ -20,13 +19,11 @@ import { PromptChips } from './PromptChips';
* 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.
*
* Two states share that frame:
*
* - **Landing.** One centred block: mark, contextual label, greeting,
* description, input, suggestions. Deliberately light — it is an invitation,
* not a page, so nothing in it competes with the dashboard beside it.
* - **Conversation.** The thread takes the body and the composer returns to the
* bottom, because from then on the thread is what the user is reading.
* 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
@@ -104,7 +101,7 @@ export default function KrowAssistant({
data-wide={expanded || undefined}
className={cn('flex flex-col overflow-hidden', className)}
role="region"
aria-label="Krow AI Assistant"
aria-label="Owliver assistant"
>
{/* Header */}
<div className="flex shrink-0 items-center gap-2.5 border-b border-white/60 px-3.5 py-2.5">
@@ -113,7 +110,7 @@ export default function KrowAssistant({
</span>
<div className="min-w-0 flex-1">
<h2 className="font-heading text-body-sm font-semibold leading-tight text-ink-1">Krow AI</h2>
<h2 className="font-heading text-body-sm font-semibold leading-tight text-ink-1">Owliver</h2>
<p className="truncate text-caption leading-tight text-ink-3">{context.page}</p>
</div>
@@ -124,56 +121,42 @@ export default function KrowAssistant({
)}
{expanded
? onRestore && (
<IconButton icon={Minimize2} label="Restore panel width" variant="ghost" size="sm" onClick={onRestore} />
<IconButton icon={Minimize2} label="Restore the default workspace width" variant="ghost" size="sm" onClick={onRestore} />
)
: onExpand && (
<IconButton icon={Maximize2} label="Expand to workspace" variant="ghost" size="sm" onClick={onExpand} />
<IconButton icon={Maximize2} label="Expand the Owliver workspace" variant="ghost" size="sm" onClick={onExpand} />
)}
{onClose && (
<IconButton icon={X} label="Close assistant" variant="ghost" size="sm" onClick={onClose} />
<IconButton icon={PanelRightClose} label="Collapse the Owliver workspace" variant="ghost" size="sm" onClick={onClose} />
)}
</div>
</div>
{isEmpty ? (
/* ── Landing: one compact centred block ───────────────────────── */
<div className="flex min-h-0 flex-1 flex-col justify-center overflow-y-auto px-4 py-4">
{/* Capped so the block stays a tight composition when expanded. */}
<div className="mx-auto w-full max-w-sm animate-fade-in space-y-3.5">
<div className="flex flex-col items-center text-center">
<span className="relative mb-2.5 grid h-10 w-10 place-items-center rounded-xl bg-white shadow-brand ring-1 ring-krow-blue/10">
<img src={KROW_LOGO_URL} alt="" aria-hidden="true" className="h-3.5 w-auto object-contain" />
<span
aria-hidden="true"
className="absolute -right-1 -top-1 grid h-4 w-4 place-items-center rounded-full bg-krow-blue text-white shadow-sm"
>
<Sparkles className="h-2 w-2" />
</span>
</span>
<p className="text-[10px] font-semibold uppercase tracking-wide text-krow-blue">
{intro.title}
</p>
<h3 className="mt-1 text-balance font-heading text-body-lg font-semibold leading-snug text-ink-1">
{intro.greeting}
</h3>
<p className="mt-1 text-caption leading-relaxed text-ink-3">{intro.description}</p>
</div>
{composer}
<PromptChips prompts={prompts} onSelect={runPrompt} />
{/* ── Body: the only region that scrolls ─────────────────────────── */}
<div
ref={scrollRef}
className="min-h-0 flex-1 overflow-y-auto px-4 py-3.5"
aria-live="polite"
aria-relevant="additions text"
>
{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. */
<div className="animate-fade-in">
<p className="font-heading text-body-lg font-semibold leading-snug text-ink-1">
{intro.title}
</p>
<p className="mt-1 text-balance text-body-sm leading-relaxed text-ink-2">
{intro.greeting}
</p>
<p className="mt-2.5 border-t border-border/70 pt-2.5 text-caption leading-relaxed text-ink-3">
{intro.description}
</p>
</div>
</div>
) : (
/* ── Conversation ─────────────────────────────────────────────── */
<>
<div
ref={scrollRef}
className="min-h-0 flex-1 space-y-4 overflow-y-auto px-4 py-4"
aria-live="polite"
aria-relevant="additions text"
>
) : (
<div className="space-y-4">
{messages.map((message, i) => (
<React.Fragment key={i}>
{i > 0 && message.role === 'user' && <TurnDivider />}
@@ -193,25 +176,28 @@ export default function KrowAssistant({
{error && <Alert tone="destructive">{error}</Alert>}
</div>
)}
</div>
<div className="shrink-0 space-y-2 border-t border-white/60 px-3 pb-2.5 pt-2.5">
{/* Capped in conversation: the vertical space belongs to the thread.
Expanded fits more per line, so it can afford one more. */}
{!busy && (
<PromptChips
prompts={prompts}
onSelect={runPrompt}
max={expanded ? 4 : 3}
align="start"
/>
)}
{composer}
<p className="px-1 text-[10px] leading-tight text-ink-4">
Krow AI reads this page&apos;s data. Check anything you act on.
</p>
</div>
</>
)}
{/* ── Composer: fixed to the bottom in both states ───────────────── */}
<div className="shrink-0 space-y-2 border-t border-white/60 px-3 pb-2.5 pt-2.5">
{/* 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 && (
<PromptChips
prompts={prompts}
onSelect={runPrompt}
max={isEmpty ? undefined : expanded ? 4 : 3}
align="start"
/>
)}
{composer}
<p className="px-1 text-[10px] leading-tight text-ink-4">
Owliver reads this page&apos;s data. Check anything you act on.
</p>
</div>
</Surface>
);
}