Skip to content

Commit 02e6592

Browse files
authored
feat(prompt-input): add PromptInput with token highlighting
PromptInput is a fully controlled form. PromptInputTextarea draws @mention and /command tokens on an aria-hidden mirror beneath transparent native text, grows through field-sizing with a measured fallback, and never submits during IME composition. usePromptInput drives PromptInputSuggestions from the keyboard, and PromptInputSubmit turns Send into Stop while a reply is running.
1 parent 4bfccbc commit 02e6592

10 files changed

Lines changed: 2132 additions & 0 deletions

‎e2e/fixtures.tsx‎

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ import {
2121
type ChatMessage,
2222
type ConversationScrollPolicy,
2323
useConversation,
24+
PromptInput,
25+
PromptInputSubmit,
26+
PromptInputSuggestions,
27+
PromptInputTextarea,
28+
type PromptInputStatus,
29+
type PromptToken,
2430
} from "@corbits/react-ui";
2531
import {
2632
CommandPalette,
@@ -427,6 +433,69 @@ function ConversationGlideScenario() {
427433
);
428434
}
429435

436+
const PROMPT_TOKENS: PromptToken[] = [
437+
{ id: "ada", kind: "mention", token: "@Ada", detail: "Research agent" },
438+
{ id: "adam", kind: "mention", token: "@Adam", detail: "Review agent" },
439+
{ id: "brief", kind: "command", token: "/brief", detail: "Summarize" },
440+
];
441+
442+
/** Records every submit with its text, and every stop. "Reply" puts the
443+
* prompt into the streaming state a host is in while an agent answers. */
444+
function Prompt() {
445+
const [value, setValue] = useState("");
446+
const [status, setStatus] = useState<PromptInputStatus>("ready");
447+
const { log, record } = useEventLog();
448+
return (
449+
<div style={{ width: 480, paddingTop: 160 }}>
450+
<PromptInput
451+
value={value}
452+
onValueChange={setValue}
453+
onSubmit={() => record(`submit:${value}`)}
454+
tokens={PROMPT_TOKENS}
455+
status={status}
456+
>
457+
<PromptInputSuggestions />
458+
<PromptInputTextarea />
459+
<PromptInputSubmit
460+
onStop={() => {
461+
record("stop");
462+
setStatus("ready");
463+
}}
464+
/>
465+
</PromptInput>
466+
<Button onClick={() => setStatus("streaming")}>Reply</Button>
467+
{log}
468+
</div>
469+
);
470+
}
471+
472+
/** A host that sends attachments with the text: "Attach file" toggles one,
473+
* and with it attached there is something to send even with no text. */
474+
function PromptWithAttachment() {
475+
const [value, setValue] = useState("");
476+
const [attached, setAttached] = useState(false);
477+
const { log, record } = useEventLog();
478+
return (
479+
<div style={{ width: 480 }}>
480+
<PromptInput
481+
value={value}
482+
onValueChange={setValue}
483+
onSubmit={() =>
484+
record(`submit:${value}${attached ? "+attachment" : ""}`)
485+
}
486+
canSubmit={value.trim() !== "" || attached}
487+
>
488+
<PromptInputTextarea />
489+
<PromptInputSubmit />
490+
</PromptInput>
491+
<Button aria-pressed={attached} onClick={() => setAttached(!attached)}>
492+
Attach file
493+
</Button>
494+
{log}
495+
</div>
496+
);
497+
}
498+
430499
export const scenarios: Record<string, () => ReactNode> = {
431500
smoke: () => <Button>Ready</Button>,
432501
"approval-card": () => <Approval state="idle" />,
@@ -459,4 +528,6 @@ export const scenarios: Record<string, () => ReactNode> = {
459528
"conversation-settle": () => <ConversationScenario policy="settle" />,
460529
"conversation-stick": () => <ConversationScenario policy="stick" />,
461530
"conversation-glide": () => <ConversationGlideScenario />,
531+
"prompt-input": () => <Prompt />,
532+
"prompt-input-attachment": () => <PromptWithAttachment />,
462533
};

0 commit comments

Comments
 (0)