Files
krow_employer/src/components/ai-assistant/PromptChips.jsx
2026-09-07 20:36:18 +05:30

84 lines
3.5 KiB
JavaScript

import * as React from 'react';
import { cn } from '@/lib/utils';
/**
* PromptChips — suggested prompts.
*
* Clicking a chip runs it immediately. A chip is already a complete question, so
* making the user press Enter afterwards adds a step that buys nothing.
*
* Chips always wrap onto as many lines as they need. The earlier horizontal-scroll
* variant is gone: a scroller can always leave a chip half-visible at the edge,
* and a half-visible button is a bug no amount of fade masking fixes. Wrapping
* makes clipping structurally impossible instead of merely unlikely.
*
* `max` caps how many are offered in a conversation, where vertical space belongs
* to the thread rather than to suggestions.
*
* A chip is shrink-to-fit up to the panel's width, so a long suggestion spends the
* whole row before it is allowed to take a second line. When it does take one, the
* radius stays at the single-line pill's 14px rather than following the height:
* `rounded-full` on a two-line chip resolves to half of 46px, and that corner is
* what turns the same component into something that reads as a card.
*
* Arrow keys move between chips, so the whole set is one tab stop.
*/
/** @param {any} props */
export function PromptChips({ prompts = [], onSelect, max = 0, align = 'center', className = '' }) {
const chipRefs = React.useRef([]);
const visible = max ? prompts.slice(0, max) : prompts;
const onKeyDown = (event, index) => {
const delta = event.key === 'ArrowRight' ? 1 : event.key === 'ArrowLeft' ? -1 : 0;
if (!delta) return;
const next = chipRefs.current[index + delta];
if (!next) return;
event.preventDefault();
next.focus();
};
if (!visible.length) return null;
return (
<div
role="group"
aria-label="Suggested prompts"
className={cn(
'flex flex-wrap gap-1.5',
align === 'center' ? 'justify-center' : 'justify-start',
className
)}
>
{visible.map((prompt, i) => (
<button
/* Position, not label. Callers de-duplicate, but a follow-up list is
built per answer and two chips can legitimately read the same —
the key has to stay unique either way. */
key={`${i}:${prompt.label}`}
ref={(el) => { chipRefs.current[i] = el; }}
type="button"
onClick={() => onSelect(prompt)}
onKeyDown={(e) => onKeyDown(e, i)}
title={prompt.prompt}
className={cn(
/* 14px is the radius `rounded-full` already produces on a one-line
chip, pinned so a wrapped chip keeps the same corner. */
'flex max-w-full items-center rounded-[14px] border border-border bg-surface px-2.5 py-1',
'text-caption font-medium text-ink-2 shadow-xs text-left',
'transition-all duration-base ease-out',
'hover:-translate-y-px hover:border-krow-blue/40 hover:bg-krow-blue-tint hover:text-krow-blue hover:shadow-sm',
'active:translate-y-0 active:shadow-xs',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-krow-blue/60 focus-visible:ring-offset-1'
)}
>
{/* `pretty` rather than `balance`: the row should be spent before a
second line starts, and balancing would leave the pill wide with
two short lines inside it. Clamped so no suggestion can grow the
chip into a paragraph — the full text stays in `title`. */}
<span className="line-clamp-3 text-pretty">{prompt.label}</span>
</button>
))}
</div>
);
}