update position page design

This commit is contained in:
2026-08-13 21:41:14 +05:30
parent 6340962907
commit 1c653264b5
124 changed files with 5746 additions and 596 deletions

View File

@@ -1,5 +1,6 @@
import * as React from 'react';
import { ArrowDownRight, ArrowUpRight, Check, TriangleAlert } from 'lucide-react';
import { Link } from 'react-router-dom';
import { ArrowDownRight, ArrowUpRight, Check, ChevronRight, TriangleAlert } from 'lucide-react';
import { cn } from '@/lib/utils';
import { Badge } from '@/components/ui/badge';
import { ProgressBar, toneForScore } from '@/components/ds/Progress';
@@ -269,20 +270,63 @@ const ListBlock = React.memo(({ block }) => {
});
ListBlock.displayName = 'ListBlock';
/** Findings, each in a tinted card so severity is scannable. */
const InsightsBlock = React.memo(({ block }) => (
/**
* Findings, and — where the item carries one — a way to act on it.
*
* Two kinds of action, and the distinction is the point:
*
* `prompt` asks Owliver the next question, in place. The panel stays open,
* the thread keeps its history, and the reader never loses the
* position they were working on. This is the default for a
* recommended person: inspecting a candidate is part of the
* conversation, not a trip to another page.
* `to` leaves for a route the app already has. Reserved for the explicit
* "open the full record" case.
*
* `hint` is for an item that can do neither — it says why, instead of looking
* clickable and doing nothing.
*/
const InsightsBlock = React.memo(({ block, onPrompt }) => (
<div className="space-y-2">
{block.items.map((item, i) => (
<div
key={i}
className={cn('rounded-xl border px-3 py-2.5', TONE_SURFACE[item.tone] || TONE_SURFACE.neutral)}
>
<p className="text-body-sm font-semibold text-ink-1">{item.title}</p>
{item.body && (
<p className="mt-0.5 text-caption leading-relaxed text-ink-2"><Inline value={item.body} /></p>
)}
</div>
))}
{block.items.map((item, i) => {
const surface = cn(
'block w-full text-left rounded-xl border px-3 py-2.5',
TONE_SURFACE[item.tone] || TONE_SURFACE.neutral
);
const interactive = cn(
surface,
'transition-colors hover:border-krow-blue/50',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-krow-blue/50'
);
const actionable = Boolean(item.prompt && onPrompt) || Boolean(item.to);
const body = (
<>
<p className="flex items-center gap-1 text-body-sm font-semibold text-ink-1">
{item.title}
{actionable && <ChevronRight className="h-3.5 w-3.5 shrink-0 opacity-60" aria-hidden="true" />}
</p>
{item.body && (
<p className="mt-0.5 text-caption leading-relaxed text-ink-2"><Inline value={item.body} /></p>
)}
{item.hint && (
<p className="mt-1 text-[11px] italic leading-relaxed text-ink-4">{item.hint}</p>
)}
</>
);
if (item.prompt && onPrompt) {
return (
<button key={i} type="button" onClick={() => onPrompt(item.prompt)} className={interactive}>
{body}
</button>
);
}
if (item.to) {
return <Link key={i} to={item.to} className={interactive}>{body}</Link>;
}
return <div key={i} className={surface}>{body}</div>;
})}
</div>
));
InsightsBlock.displayName = 'InsightsBlock';
@@ -381,7 +425,7 @@ const RENDERERS = {
* blocks has consistent rhythm — a heading hugs what follows it, everything
* else breathes.
*/
export const ResponseDocument = React.memo(({ blocks = [], streaming = false }) => (
export const ResponseDocument = React.memo(({ blocks = [], streaming = false, onPrompt }) => (
<div className="space-y-3">
{blocks.map((block, i) => {
const Renderer = RENDERERS[block.type];
@@ -391,7 +435,7 @@ export const ResponseDocument = React.memo(({ blocks = [], streaming = false })
return (
<div key={i} className={cn(hugsNext && '-mb-1.5', 'animate-fade-in')}>
<Renderer block={block} />
<Renderer block={block} onPrompt={onPrompt} />
{/* The cursor trails the final block only while text is still arriving. */}
{streaming && isLast && (block.type === 'text' || block.type === 'heading') && (
<span