Layish

Composer

A prompt shell on input-group with mode and mic slots shaped for AI SDK useChat.

Built on shadcn Input Group. Distributed as a shadcn registry block from GitHub.

Composer

Multi-line prompt with attach, mode slot, mic slot, and circular ↑ send.

Open in

Composer

Installation

npx shadcn@latest add layishsieger/registry/composer

The CLI copies the block into your project. You own the source.

Usage

import { useState } from "react"
import { Composer } from "@/components/composer"

export function Example() {
  const [text, setText] = useState("")
  // const { sendMessage, status, stop } = useChat(...)

  return (
    <Composer
      value={text}
      onValueChange={setText}
      status="ready"
      onStop={() => {
        // stop()
      }}
      onSubmit={({ text, files }) => {
        // sendMessage({ text, files })
        setText("")
      }}
      modeSlot={<button type="button">Ask</button>}
    />
  )
}

Host owns transport. Wire onSubmit, status, and onStop from useChat. The block does not import @ai-sdk/react.

Enter key

Default Enter sends. Shift+Enter inserts a newline. Set enterKeyBehavior="focus-send" so the first Enter focuses send and the second Enter submits. After submit, focus returns to the field so Enter does not immediately hit Stop.

Enter focuses send

Keyboard

ShortcutAction
CtrlAAttach
CtrlOCycle mode + expand chips
CtrlDDictation / mic
/CtrlSend (or stop when busy)
Newline

On desktop (fine pointer), modifier is on macOS and Ctrl elsewhere — both currently match as Mod. Shortcuts only run while focus is inside the composer. On touch / coarse pointers, Mod shortcuts and Kbd tooltips are off and Enter inserts a newline — send with the button. Tooltips dismiss on click.

shortcuts={false} turns off Mod action bindings (attach / mode / dictation) and tooltips. It does not disable field Enter behavior: still inserts a newline, and / Ctrl still send or stop while busy (on fine pointers). Set shortcutTooltips={false} to keep Mod bindings without hover hints.

shortcuts off

No Mod action bindings or Kbd tooltips. Enter / Shift+Enter / Mod+Enter send behavior still applies on desktop.

shortcuts={false}

tooltips off

Bindings stay; hover Kbd tooltips hidden.

shortcutTooltips={false}

SpeechInput

The registry block keeps micSlot only — no AI Elements dependency. Install SpeechInput from AI Elements when you want voice. In Chrome/Edge it uses the browser Web Speech API — free, no Layish tokens or API keys. Optional onAudioRecorded (Whisper/etc.) is host-paid if you wire it for Firefox/Safari.

npx ai-elements@latest add speech-input
import { useState } from "react"
import { Composer } from "@/components/composer"
import { SpeechInput } from "@/components/ai-elements/speech-input"

export function Example() {
  const [text, setText] = useState("")

  return (
    <Composer
      value={text}
      onValueChange={setText}
      micSlot={({ disabled }) => (
        <SpeechInput
          size="icon-sm"
          variant="ghost"
          disabled={disabled}
          onTranscriptionChange={(transcript) => {
            const next = transcript.trim()
            if (!next) return
            setText((current) => {
              const base = current.trim()
              return base ? `${base} ${next}` : next
            })
          }}
        />
      )}
    />
  )
}

SpeechInput in micSlot

Appends final transcripts into the composer value.

SpeechInput in micSlot

Uses the browser Web Speech API in Chrome/Edge — free, no API keys or tokens from Layish. Optional Whisper via onAudioRecorded is host-paid if you add it.

AI SDK

Map sendMessage, status, and stop. Busy status flips the primary control to stop.

Ask
// Wired like useChat — submit → sendMessage, busy → stop()

API

Composer

PropTypeDefault
value / onValueChangecontrolled text
onSubmit(message) => void
statusready | submitted | streaming | errorready
onStop() => void
enterKeyBehaviorsubmit | focus-sendsubmit
shortcutsbooleantrue
shortcutTooltipsbooleantrue
modeSlot / micSlot / attachSlotReactNode | render propmic + attach defaults
className / disabledlayout / disable