Files
krow_talent_app/src/components/ai-assistant/PromptInput.jsx
2026-08-10 19:36:45 +05:30

140 lines
4.5 KiB
JavaScript

import * as React from 'react';
import { ArrowUp, Square } from 'lucide-react';
import { cn } from '@/lib/utils';
/**
* PromptInput — the assistant's primary action.
*
* A textarea rather than an input, because these prompts are sentences: it grows
* with the content up to a cap, keeps Enter as send, and leaves Shift+Enter for a
* deliberate newline. That combination is what makes typing a real question feel
* natural instead of cramped.
*
* `placeholders` is a list, but only the first is shown. It used to rotate every
* few seconds; that was defensible when the panel opened on demand and wrong once
* Owliver became permanent furniture, because a permanently visible field that
* rewrites itself forever is movement in the corner of the eye with nothing to
* gain — the suggestion chips already teach the range of what can be asked, and
* they do it without a timer or a rerender every four seconds.
*/
export function PromptInput({
value,
onChange,
onSubmit,
onStop,
busy = false,
placeholders = ['Ask anything…'],
autoFocus = false,
align = 'left',
className,
}) {
const textareaRef = React.useRef(null);
const [focused, setFocused] = React.useState(false);
/* Grow to fit the content, up to a cap, then scroll internally. */
React.useEffect(() => {
const el = textareaRef.current;
if (!el) return;
el.style.height = 'auto';
el.style.height = `${Math.min(el.scrollHeight, 148)}px`;
}, [value]);
React.useEffect(() => {
if (autoFocus) textareaRef.current?.focus();
}, [autoFocus]);
const submit = () => {
if (!value.trim() || busy) return;
onSubmit(value);
};
const onKeyDown = (event) => {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
submit();
}
};
const isCenter = align === 'center';
return (
<form
onSubmit={(e) => { e.preventDefault(); submit(); }}
className={cn(
'relative flex items-center gap-2 rounded-2xl border bg-surface transition-all duration-base ease-out',
'px-3 py-2',
focused
? 'border-krow-blue/50 shadow-[0_8px_28px_-8px_rgba(10,57,223,0.28)] ring-2 ring-krow-blue/15'
: 'border-border shadow-md hover:border-krow-blue/30',
className
)}
>
<div className="relative min-w-0 flex-1">
<textarea
ref={textareaRef}
rows={1}
value={value}
onChange={(e) => onChange(e.target.value)}
onKeyDown={onKeyDown}
onFocus={() => setFocused(true)}
onBlur={() => setFocused(false)}
disabled={busy}
aria-label="Ask Owliver"
aria-busy={busy || undefined}
className={cn(
'block w-full resize-none bg-transparent leading-relaxed text-ink-1',
'no-scrollbar outline-none disabled:cursor-wait disabled:text-ink-3',
'py-0.5 text-body-sm',
isCenter ? 'text-center' : 'text-left'
)}
/>
{/* Rendered as an element rather than the native `placeholder` so it can
be truncated and swapped for the busy state. Hidden from assistive
tech — the textarea's aria-label is the accessible name. */}
{!value && (
<span
aria-hidden="true"
className={cn(
'pointer-events-none absolute inset-0 truncate text-ink-4',
'py-0.5 text-body-sm',
isCenter ? 'text-center' : 'text-left'
)}
>
{busy ? 'Thinking…' : placeholders[0]}
</span>
)}
</div>
{busy ? (
<button
type="button"
onClick={onStop}
aria-label="Stop generating"
className={cn(
'grid shrink-0 place-items-center rounded-full border border-border bg-surface text-ink-2',
'transition-colors duration-base hover:bg-surface-sunken focus-ring',
'h-8 w-8'
)}
>
<Square className="h-3 w-3" aria-hidden="true" />
</button>
) : (
<button
type="submit"
disabled={!value.trim()}
aria-label="Send message"
className={cn(
'grid shrink-0 place-items-center rounded-full bg-krow-blue text-white',
'transition-all duration-base ease-out hover:bg-krow-blue-dark',
'disabled:bg-surface-sunken disabled:text-ink-4 focus-ring',
'h-8 w-8'
)}
>
<ArrowUp className="h-3.5 w-3.5" aria-hidden="true" />
</button>
)}
</form>
);
}