SmartTextbox
Single-line input with Copilot-style ghost-text completion.
A single-line input that streams ghost-text suggestions inline. ArrowRight accepts (configurable), Esc dismisses. Caret-at-end gating means ArrowRight retains its normal cursor-movement behavior elsewhere.
Preview
import { useState } from 'react';import { SmartProvider, SmartTextbox } from '@extedcoud/smart-components';import { createMockClient } from '@extedcoud/smart-components/adapters/mock';const client = createMockClient({complete: async (req) => req.prompt.endsWith('Hello') ? ', how can I help you today?' : ' …keep typing for more.',});export function Example() {const [value, setValue] = useState('');return ( <SmartProvider client={client}> <SmartTextbox value={value} onChange={setValue} placeholder="Type Hello…" context="user is writing a support reply" /> </SmartProvider>);}Styled example
A polished take with a gradient border, leading sparkle, violet italic ghost-text, and a visible accept hint.
Ghost-text in violet italic. Press → to accept.
'use client';import { useState } from 'react';import { SmartProvider, SmartTextbox } from '@extedcoud/smart-components';import { makeGhostMock } from '@/lib/mock-client';const client = makeGhostMock();export default function SmartTextboxStylized() { const [value, setValue] = useState(''); return ( <SmartProvider client={client}> <div className="w-full max-w-md"> <div className="group relative rounded-2xl bg-gradient-to-r from-indigo-500 via-fuchsia-500 to-cyan-500 p-[1.5px] shadow-lg shadow-fuchsia-500/10 transition focus-within:shadow-fuchsia-500/30"> <div className="flex items-center gap-2 rounded-[14px] bg-fd-card px-3"> <span className="text-fd-muted-foreground" aria-hidden> ✦ </span> <SmartTextbox value={value} onChange={setValue} placeholder="Type Hello, Dear, or Thanks…" context="user is writing a support reply" className="flex-1 bg-transparent py-3 text-base text-fd-foreground outline-none placeholder:text-fd-muted-foreground" ghostStyle={{ color: '#a855f7', opacity: 0.7, fontStyle: 'italic', }} /> <kbd className="hidden rounded-md border border-fd-border bg-fd-background px-1.5 py-0.5 text-[10px] font-mono text-fd-muted-foreground sm:inline"> → </kbd> </div> </div> <p className="mt-2 px-1 text-xs text-fd-muted-foreground"> Ghost-text in violet italic. Press <kbd className="rounded bg-fd-card px-1 py-0.5 text-[10px]">→</kbd> to accept. </p> </div> </SmartProvider> );}Styling the ghost
By default the ghost is opacity: 0.4 inheriting the input's color. To recolor it:
<SmartTextbox
value={v}
onChange={setV}
ghostStyle={{ color: '#0066cc', fontStyle: 'italic', opacity: 0.7 }}
/>For richer markup (icons, badges) use the renderGhost render-prop. For global styling outside the component, target [data-testid="smart-textbox-ghost"].
Imperative API (mobile-friendly accept)
ArrowRight doesn't exist on most soft keyboards. For mobile, expose accept via a tap button using forwardRef:
import { useRef } from 'react';
import { SmartTextbox, type SmartTextboxHandle } from '@extedcoud/smart-components';
function MobileFriendly() {
const ref = useRef<SmartTextboxHandle>(null);
const [v, setV] = useState('');
const [ghost, setGhost] = useState('');
return (
<>
<SmartTextbox ref={ref} value={v} onChange={setV} onGhostChange={setGhost} />
{ghost && <button onClick={() => ref.current?.accept()}>Accept "{ghost}"</button>}
</>
);
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
| value* | string | — | Controlled input value. |
| onChange* | (value: string) => void | — | Called on user input or accept. |
| context | string | — | Optional context string included in the prompt to bias completion. |
| minChars | number | 3 | Minimum chars before suggestions fetch. |
| debounceMs | number | 300 | Debounce window in ms. |
| stream | boolean | false | Use the streaming capability for faster first paint. |
| disableAI | boolean | false | Disable all AI calls. |
| acceptKey | string | 'ArrowRight' | Key that accepts the ghost. Fires only when caret is at end of value. Note: ArrowRight does not exist on most soft keyboards — for mobile use the imperative accept() via ref, or set acceptKey="Enter". |
| dismissKey | string | 'Escape' | Key that dismisses the ghost. |
| maxTokens | number | 32 | Max tokens for the completion call. |
| renderGhost | (suggestion: string) => ReactNode | — | Render-prop override for the ghost text. |
| ghostClassName | string | — | Class name applied to the ghost text span. |
| ghostStyle | CSSProperties | — | Inline style applied to the ghost text span. Defaults to opacity: 0.4 inheriting input color. |
| wrapperClassName | string | — | Class name applied to the outer wrapper. |
| onAccept | (accepted, finalValue) => void | — | Called when the suggestion is accepted. |
| onGhostChange | (suggestion: string) => void | — | Called whenever the visible ghost changes ('' when no ghost). Use to render a tap-to-accept button on mobile. |
| ...rest | InputHTMLAttributes<HTMLInputElement> | — | All native input props minus value, onChange, defaultValue. |
Imperative handle
forwardRef exposes a SmartTextboxHandle:
| Prop | Type | Default | Description |
|---|---|---|---|
| accept | () => boolean | — | Commit the current ghost. Returns true if accepted. |
| dismiss | () => void | — | Discard the current ghost. |
| focus | () => void | — | Focus the underlying input. |
| blur | () => void | — | Blur the underlying input. |
| getSuggestion | () => string | — | Current ghost text or ''. Useful for rendering a tap-to-accept button. |
Mobile notes
ArrowRightis keyboard-only. Use the imperativeaccept()or setacceptKey="Enter".- iOS Safari zoom-on-focus: any input with
font-size < 16pxtriggers viewport zoom. Usefont-size: 16px+. - IME composition (Gboard predictive text) is gated internally — no flicker or misfired accepts.