agnets done
This commit is contained in:
@@ -491,8 +491,107 @@ const SkillSectionBlock = React.memo(/** @param {any} props */ ({ block }) => {
|
||||
});
|
||||
SkillSectionBlock.displayName = 'SkillSectionBlock';
|
||||
|
||||
/**
|
||||
* A write the agent has proposed. The only block a person can act on.
|
||||
*
|
||||
* Three things it must do, and they are all about not being clicked past:
|
||||
*
|
||||
* - **Say what will happen, in the server's words.** The title, summary and
|
||||
* details are rendered as they arrived. A browser paraphrasing them would
|
||||
* be describing a different act than the one the token authorises.
|
||||
* - **Put warnings above the button.** A clash or an over-headcount is
|
||||
* exactly what somebody is about to approve without noticing, and a warning
|
||||
* underneath the decision is a warning read afterwards.
|
||||
* - **Not pretend to be finished.** Once approved, the block stays visible
|
||||
* and says so. Replacing it with a tick would lose what was agreed to.
|
||||
*/
|
||||
const ConfirmationBlock = React.memo(/** @param {any} props */ ({ block, onConfirm }) => {
|
||||
const [state, setState] = React.useState('pending');
|
||||
|
||||
const approve = React.useCallback(() => {
|
||||
/* Guarded rather than debounced. The token is single-use server-side, so a
|
||||
double click costs a confusing refusal rather than a duplicate write —
|
||||
but a button that visibly does nothing the second time is kinder than
|
||||
one that reports an error somebody caused by being quick. */
|
||||
if (state !== 'pending') return;
|
||||
setState('approved');
|
||||
onConfirm?.(block);
|
||||
}, [state, block, onConfirm]);
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-krow-blue/30 bg-krow-blue/5 p-3.5">
|
||||
<div className="flex items-start gap-2.5">
|
||||
<span className="mt-0.5 grid h-5 w-5 shrink-0 place-items-center rounded-full bg-krow-blue text-white">
|
||||
<TriangleAlert className="h-3 w-3" aria-hidden="true" />
|
||||
</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-body-sm font-semibold text-ink-1">{block.title}</p>
|
||||
{block.summary && (
|
||||
<p className="mt-1 text-caption leading-relaxed text-ink-2">{block.summary}</p>
|
||||
)}
|
||||
|
||||
{block.details?.length > 0 && (
|
||||
<dl className="mt-2.5 grid grid-cols-[auto_1fr] gap-x-3 gap-y-1">
|
||||
{block.details.map((d, i) => (
|
||||
<React.Fragment key={i}>
|
||||
<dt className="text-caption text-ink-3">{d.label}</dt>
|
||||
<dd className="text-caption font-medium text-ink-1">{d.value}</dd>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</dl>
|
||||
)}
|
||||
|
||||
{/* Above the button, deliberately. */}
|
||||
{block.warnings?.length > 0 && (
|
||||
<ul className="mt-2.5 space-y-1">
|
||||
{block.warnings.map((w, i) => (
|
||||
<li key={i} className="flex gap-1.5 text-caption text-amber-700 dark:text-amber-400">
|
||||
<TriangleAlert className="mt-0.5 h-3 w-3 shrink-0" aria-hidden="true" />
|
||||
<span>{w}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
<div className="mt-3 flex items-center gap-2">
|
||||
{state === 'pending' ? (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={approve}
|
||||
className="rounded-lg bg-krow-blue px-3 py-1.5 text-caption font-semibold text-white
|
||||
transition hover:opacity-90 focus-visible:outline focus-visible:outline-2
|
||||
focus-visible:outline-offset-2 focus-visible:outline-krow-blue"
|
||||
>
|
||||
Approve
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setState('dismissed')}
|
||||
className="rounded-lg px-3 py-1.5 text-caption font-medium text-ink-3
|
||||
transition hover:text-ink-1 focus-visible:outline focus-visible:outline-2
|
||||
focus-visible:outline-offset-2 focus-visible:outline-krow-blue"
|
||||
>
|
||||
Not now
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<p className="flex items-center gap-1.5 text-caption font-medium text-ink-3">
|
||||
<Check className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
{state === 'approved' ? 'Approved — carrying on.' : 'Left for now. Nothing was changed.'}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
ConfirmationBlock.displayName = 'ConfirmationBlock';
|
||||
|
||||
const RENDERERS = {
|
||||
text: TextBlock,
|
||||
confirmation: ConfirmationBlock,
|
||||
skillSection: SkillSectionBlock,
|
||||
heading: HeadingBlock,
|
||||
kpis: KpisBlock,
|
||||
@@ -515,7 +614,7 @@ const RENDERERS = {
|
||||
* blocks has consistent rhythm — a heading hugs what follows it, everything
|
||||
* else breathes.
|
||||
*/
|
||||
export const ResponseDocument = React.memo(/** @param {any} props */ ({ blocks = [], streaming = false, onPrompt }) => (
|
||||
export const ResponseDocument = React.memo(/** @param {any} props */ ({ blocks = [], streaming = false, onPrompt, onConfirm }) => (
|
||||
<div className="space-y-3">
|
||||
{blocks.map((block, i) => {
|
||||
const Renderer = RENDERERS[block.type];
|
||||
@@ -525,7 +624,7 @@ export const ResponseDocument = React.memo(/** @param {any} props */ ({ blocks =
|
||||
|
||||
return (
|
||||
<div key={i} className={cn(hugsNext && '-mb-1.5', 'animate-fade-in')}>
|
||||
<Renderer block={block} onPrompt={onPrompt} />
|
||||
<Renderer block={block} onPrompt={onPrompt} onConfirm={onConfirm} />
|
||||
{/* 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