agnets done

This commit is contained in:
2026-08-28 11:02:02 +05:30
parent ab9adde6cd
commit eaf08e061d
58 changed files with 3506 additions and 4308 deletions

View File

@@ -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