smart-components
Components

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

PropTypeDefaultDescription
value*stringControlled input value.
onChange*(value: string) => voidCalled on user input or accept.
contextstringOptional context string included in the prompt to bias completion.
minCharsnumber3Minimum chars before suggestions fetch.
debounceMsnumber300Debounce window in ms.
streambooleanfalseUse the streaming capability for faster first paint.
disableAIbooleanfalseDisable all AI calls.
acceptKeystring'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".
dismissKeystring'Escape'Key that dismisses the ghost.
maxTokensnumber32Max tokens for the completion call.
renderGhost(suggestion: string) => ReactNodeRender-prop override for the ghost text.
ghostClassNamestringClass name applied to the ghost text span.
ghostStyleCSSPropertiesInline style applied to the ghost text span. Defaults to opacity: 0.4 inheriting input color.
wrapperClassNamestringClass name applied to the outer wrapper.
onAccept(accepted, finalValue) => voidCalled when the suggestion is accepted.
onGhostChange(suggestion: string) => voidCalled whenever the visible ghost changes ('' when no ghost). Use to render a tap-to-accept button on mobile.
...restInputHTMLAttributes<HTMLInputElement>All native input props minus value, onChange, defaultValue.

Imperative handle

forwardRef exposes a SmartTextboxHandle:

PropTypeDefaultDescription
accept() => booleanCommit the current ghost. Returns true if accepted.
dismiss() => voidDiscard the current ghost.
focus() => voidFocus the underlying input.
blur() => voidBlur the underlying input.
getSuggestion() => stringCurrent ghost text or ''. Useful for rendering a tap-to-accept button.

Mobile notes

  • ArrowRight is keyboard-only. Use the imperative accept() or set acceptKey="Enter".
  • iOS Safari zoom-on-focus: any input with font-size < 16px triggers viewport zoom. Use font-size: 16px+.
  • IME composition (Gboard predictive text) is gated internally — no flicker or misfired accepts.