-
Notifications
You must be signed in to change notification settings - Fork 13
feat: add PromptInput AI element #862
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| { | ||
| "title": "AI Elements", | ||
| "pages": ["prompt-input"] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| 'use client'; | ||
|
|
||
| export const preview = { | ||
| type: 'code', | ||
| code: `function PromptInputPreview() { | ||
| const [status, setStatus] = React.useState('idle'); | ||
| const timerRef = React.useRef(null); | ||
|
|
||
| React.useEffect(() => () => clearTimeout(timerRef.current), []); | ||
|
|
||
| return ( | ||
| <div style={{ width: 420 }}> | ||
| <PromptInput | ||
| status={status} | ||
| onSubmit={(value, event) => { | ||
| event.currentTarget.reset(); | ||
| setStatus('streaming'); | ||
| timerRef.current = setTimeout(() => setStatus('idle'), 2500); | ||
| }} | ||
| onStop={() => { | ||
| clearTimeout(timerRef.current); | ||
| setStatus('idle'); | ||
| }} | ||
| > | ||
| <PromptInput.Textarea placeholder="Ask anything…" /> | ||
| <PromptInput.Footer> | ||
| <PromptInput.Button>Skills</PromptInput.Button> | ||
| <PromptInput.Button aria-label="Attach file">📎</PromptInput.Button> | ||
| <PromptInput.Submit /> | ||
| </PromptInput.Footer> | ||
| </PromptInput> | ||
| </div> | ||
| ); | ||
| }` | ||
| }; | ||
|
|
||
| export const attachmentsDemo = { | ||
| type: 'code', | ||
| code: `<div style={{ width: 420 }}> | ||
| <PromptInput onSubmit={value => console.log(value)}> | ||
| <PromptInput.Header> | ||
| <Chat.Attachment title="design-spec.pdf" description="1.2 MB" onRemove={() => {}} /> | ||
| <Chat.Attachment title="screenshot.png" state="uploading" description="Uploading…" /> | ||
| </PromptInput.Header> | ||
| <PromptInput.Textarea placeholder="Reply…" /> | ||
| <PromptInput.Footer> | ||
| <PromptInput.Submit /> | ||
| </PromptInput.Footer> | ||
| </PromptInput> | ||
| </div>` | ||
| }; | ||
|
|
||
| export const controlledDemo = { | ||
| type: 'code', | ||
| code: `function ControlledPromptInput() { | ||
| const [value, setValue] = React.useState(''); | ||
|
|
||
| return ( | ||
| <Flex direction="column" gap={4} style={{ width: 420 }}> | ||
| <PromptInput | ||
| value={value} | ||
| onValueChange={setValue} | ||
| onSubmit={() => setValue('')} | ||
| > | ||
| <PromptInput.Textarea placeholder="Write a message…" /> | ||
| <PromptInput.Footer> | ||
| <PromptInput.Submit /> | ||
| </PromptInput.Footer> | ||
| </PromptInput> | ||
| <Text size="small" variant="secondary">{value.length} characters</Text> | ||
| </Flex> | ||
| ); | ||
| }` | ||
| }; | ||
|
|
||
| export const disabledDemo = { | ||
| type: 'code', | ||
| code: `<div style={{ width: 420 }}> | ||
| <PromptInput disabled> | ||
| <PromptInput.Textarea placeholder="Read-only conversation" /> | ||
| <PromptInput.Footer> | ||
| <PromptInput.Button>Skills</PromptInput.Button> | ||
| <PromptInput.Submit /> | ||
| </PromptInput.Footer> | ||
| </PromptInput> | ||
| </div>` | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| --- | ||
| title: PromptInput | ||
| description: A composer for AI chat and standalone "Ask anything" boxes — auto-growing textarea, toolbar slots, and a status-aware submit button. | ||
| source: packages/raystack/components/prompt-input | ||
| tag: new | ||
| --- | ||
|
|
||
| import { preview, attachmentsDemo, controlledDemo, disabledDemo } from "./demo.ts"; | ||
|
|
||
| <Demo data={preview} /> | ||
|
|
||
| ## Anatomy | ||
|
|
||
| Import and assemble the composer. The root is a form that owns submit | ||
| semantics: Enter submits, Shift+Enter inserts a newline, and IME composition | ||
| is respected. | ||
|
|
||
| ```tsx | ||
| import { PromptInput } from '@raystack/apsara' | ||
|
|
||
| <PromptInput onSubmit={send} onStop={stop} status={status}> | ||
| <PromptInput.Header>{/* attachment previews */}</PromptInput.Header> | ||
| <PromptInput.Textarea placeholder="Reply…" /> | ||
| <PromptInput.Footer> | ||
| <PromptInput.Button>Skills</PromptInput.Button> | ||
| <PromptInput.Submit /> | ||
| </PromptInput.Footer> | ||
| </PromptInput> | ||
| ``` | ||
|
|
||
| `PromptInput` is purely presentational: it never talks to a model or manages a | ||
| request. Pass the lifecycle in through `status` and react to `onSubmit` / | ||
| `onStop` — it works with the AI SDK's `useChat`, a custom SSE client, or | ||
| anything else. | ||
|
|
||
| ## API Reference | ||
|
|
||
| ### Root | ||
|
|
||
| The `<form>` that holds the value and submit semantics. | ||
|
|
||
| <auto-type-table path="./props.ts" name="PromptInputProps" /> | ||
|
|
||
| ### Textarea | ||
|
|
||
| The text field, built on `TextArea variant="borderless"`. It grows with its | ||
| content up to a max-height cap (override with | ||
| `--prompt-input-textarea-max-height`), then scrolls. | ||
|
|
||
| <auto-type-table path="./props.ts" name="PromptInputTextareaProps" /> | ||
|
|
||
| ### Header | ||
|
|
||
| A slot row above the textarea, usually for `Chat.Attachment` previews. Hidden | ||
| while empty. | ||
|
|
||
| ### Footer | ||
|
|
||
| The bottom toolbar row. Hidden while empty. | ||
|
|
||
| ### Button | ||
|
|
||
| A small neutral-ghost `Button` for the toolbar — attach, skills, model pickers | ||
| and similar consumer-owned controls. | ||
|
|
||
| <auto-type-table path="./props.ts" name="PromptInputButtonProps" /> | ||
|
|
||
| ### Submit | ||
|
|
||
| The trailing send button. It renders ↑ when idle, disables itself while the | ||
| input is empty, shows a spinner while `status="submitted"`, and flips to a | ||
| stop square that calls `onStop` while `status="streaming"`. | ||
|
|
||
| <auto-type-table path="./props.ts" name="PromptInputSubmitProps" /> | ||
|
|
||
| ## Examples | ||
|
|
||
| ### Attachments in the header | ||
|
|
||
| Attachment previews are presentational (`Chat.Attachment`); file picking, | ||
| drag-drop and uploads belong to your app. | ||
|
|
||
| <Demo data={attachmentsDemo} /> | ||
|
|
||
| ### Controlled value | ||
|
|
||
| Control `value` to sync the draft elsewhere — persistence, slash-command | ||
| menus, mention pickers. | ||
|
|
||
| <Demo data={controlledDemo} /> | ||
|
|
||
| ### Disabled | ||
|
|
||
| <Demo data={disabledDemo} /> | ||
|
|
||
| ## Accessibility | ||
|
|
||
| - The root is a native `<form>`; `PromptInput.Submit` is a real submit button, | ||
| so assistive tech announces the submit affordance for free. | ||
| - Enter submits and Shift+Enter inserts a newline. During IME composition | ||
| Enter confirms the composition instead of submitting. | ||
| - While a response is in flight the submit button becomes `type="button"` with | ||
| an updated accessible name ("Stop response") so it can never resubmit the | ||
| form. | ||
| - The composer frame carries the focus treatment (`:focus-within`), while the | ||
| borderless textarea suppresses its own duplicate focus ring. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| import type React from 'react'; | ||
|
|
||
| export interface PromptInputProps { | ||
| /** The composed text (controlled). */ | ||
| value?: string; | ||
|
|
||
| /** | ||
| * The initial text when uncontrolled. | ||
| * @defaultValue "" | ||
| */ | ||
| defaultValue?: string; | ||
|
|
||
| /** Called when the composed text changes. */ | ||
| onValueChange?: (value: string) => void; | ||
|
|
||
| /** | ||
| * Called with the trimmed text when the prompt is submitted — Enter in the | ||
| * textarea or a click on `PromptInput.Submit`. Call | ||
| * `event.currentTarget.reset()` to clear the input after sending. | ||
| */ | ||
| onSubmit?: (value: string, event: React.FormEvent<HTMLFormElement>) => void; | ||
|
|
||
| /** | ||
| * Called when `PromptInput.Submit` is pressed while `status` is | ||
| * `"submitted"` or `"streaming"`. | ||
| */ | ||
| onStop?: () => void; | ||
|
|
||
| /** | ||
| * The consumer-owned request lifecycle. Drives `PromptInput.Submit`: | ||
| * `"idle"`/`"error"` show a send arrow, `"submitted"` a spinner and | ||
| * `"streaming"` a stop square (both routed to `onStop`). | ||
| * @defaultValue "idle" | ||
| */ | ||
| status?: 'idle' | 'submitted' | 'streaming' | 'error'; | ||
|
|
||
| /** | ||
| * Disables the whole composer. | ||
| * @defaultValue false | ||
| */ | ||
| disabled?: boolean; | ||
|
|
||
| /** Custom CSS class names. */ | ||
| className?: string; | ||
| } | ||
|
|
||
| export interface PromptInputTextareaProps { | ||
| /** Placeholder shown while empty. @defaultValue "Write a message…" */ | ||
| placeholder?: string; | ||
|
|
||
| /** Disables just the textarea. Inherits the root `disabled` by default. */ | ||
| disabled?: boolean; | ||
|
|
||
| /** Custom CSS class names. */ | ||
| className?: string; | ||
| } | ||
|
|
||
| export interface PromptInputSubmitProps { | ||
| /** | ||
| * Replaces the status-derived icon (send arrow, spinner or stop square). | ||
| */ | ||
| children?: React.ReactNode; | ||
|
|
||
| /** | ||
| * Accessible name. Defaults to "Send message", or "Stop response" while a | ||
| * response is in flight. | ||
| */ | ||
| 'aria-label'?: string; | ||
|
|
||
| /** Custom CSS class names. */ | ||
| className?: string; | ||
| } | ||
|
|
||
| export interface PromptInputButtonProps { | ||
| /** | ||
| * Visual style, forwarded to `Button`. | ||
| * @defaultValue "ghost" | ||
| */ | ||
| variant?: 'solid' | 'outline' | 'ghost' | 'text'; | ||
|
|
||
| /** | ||
| * Color, forwarded to `Button`. | ||
| * @defaultValue "neutral" | ||
| */ | ||
| color?: 'accent' | 'danger' | 'neutral' | 'success'; | ||
|
|
||
| /** | ||
| * Size, forwarded to `Button`. | ||
| * @defaultValue "small" | ||
| */ | ||
| size?: 'small' | 'normal'; | ||
|
|
||
| /** Custom CSS class names. */ | ||
| className?: string; | ||
| } | ||
|
Comment on lines
+47
to
+95
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Keep the prop reference aligned with forwarded component props. These interfaces are narrower than the actual contracts: 🤖 Prompt for AI Agents |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,3 @@ | ||
| { | ||
| "pages": ["(overview)", "theme", "components"] | ||
| "pages": ["(overview)", "theme", "ai-elements", "components"] | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Clarify reset behavior for controlled inputs.
event.currentTarget.reset()clears an uncontrolled draft, but a controlledvalueremains unchanged until the parent updates it. Document this distinction; the controlled example correctly callssetValue('').🤖 Prompt for AI Agents