Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/www/src/components/docs/search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export default function DocsSearch({ pageTree }: { pageTree: Root }) {
excluded until the user searches. */
const items = flattened.reduce<Record<string, Item[]>>((acc, item) => {
const folder = getFolderFromUrl(item.url);
if (folder === 'components') return acc;
if (folder === 'components' || folder === 'ai-elements') return acc;
if (!acc[folder]) {
acc[folder] = [];
}
Expand Down
4 changes: 4 additions & 0 deletions apps/www/src/content/docs/ai-elements/meta.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"title": "AI Elements",
"pages": ["prompt-input"]
}
87 changes: 87 additions & 0 deletions apps/www/src/content/docs/ai-elements/prompt-input/demo.ts
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>`
};
106 changes: 106 additions & 0 deletions apps/www/src/content/docs/ai-elements/prompt-input/index.mdx
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.
95 changes: 95 additions & 0 deletions apps/www/src/content/docs/ai-elements/prompt-input/props.ts
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;
Comment on lines +17 to +21

Copy link
Copy Markdown
Contributor

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 controlled value remains unchanged until the parent updates it. Document this distinction; the controlled example correctly calls setValue('').

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/www/src/content/docs/ai-elements/prompt-input/props.ts` around lines 17
- 21, Update the onSubmit documentation to distinguish reset behavior:
event.currentTarget.reset() clears uncontrolled input drafts, while controlled
value props remain unchanged until the parent updates them. Mention that
controlled consumers should clear their state, as demonstrated by calling
setValue(''), and preserve the existing submission details.


/**
* 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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: PromptInputTextareaProps forwards TextAreaProps (packages/raystack/components/prompt-input/prompt-input-textarea.tsx:10-14), PromptInputSubmitProps forwards native button props (packages/raystack/components/prompt-input/prompt-input-submit.tsx:10), and PromptInputButtonProps forwards Button props (packages/raystack/components/prompt-input/prompt-input-parts.tsx:31). The generated tables therefore omit supported props such as onKeyDown, onClick, disabled, and ARIA attributes. Derive these docs types from the component contracts or document the full forwarded surface.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/www/src/content/docs/ai-elements/prompt-input/props.ts` around lines 47
- 95, Update PromptInputTextareaProps, PromptInputSubmitProps, and
PromptInputButtonProps to derive from their forwarded TextAreaProps, native
button props, and Button props respectively, while omitting only props
intentionally overridden or documented locally. Preserve the existing custom
descriptions/defaults and ensure supported handlers, disabled, className, and
ARIA attributes remain represented in the generated prop tables.

2 changes: 1 addition & 1 deletion apps/www/src/content/docs/meta.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"pages": ["(overview)", "theme", "components"]
"pages": ["(overview)", "theme", "ai-elements", "components"]
}
Loading
Loading