update position page design
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user