Skip to content
Mayura
UI gallery

Source you own

Make the conversation yours.

Copy accessible components into your project. Keep your styles, your application state and every approval decision. These previews use local fixtures.

chat

Source 1.0.0 · Mayura 1.2.0+ · React. Local fixture; no service actions.

Conversation

Your next idea starts here.

mayura add ui chat --directory ./my-app --apply
Component source
import { useRef, useState, type ReactNode, type FormEvent } from 'react';
import { useMayuraRun } from 'mayura/client-react';
import type { HeadlessRunStore } from 'mayura/client/headless';
import './ui.css';
/** Explicit send, one in-flight command, no mount-time spend or automatic retry. Plain text only. */
export function Chat({ store, onSend, history }: { readonly history?: ReactNode; readonly store: HeadlessRunStore; readonly onSend: (text: string) => Promise<void> }) {
  const state = useMayuraRun(store); const [draft, setDraft] = useState(''); const [pending, setPending] = useState(false); const [error, setError] = useState(''); const busy = useRef(false);
  async function send(event: FormEvent) { event.preventDefault(); const text = draft.trim(); if (busy.current || !text || text.length > 16000) return;
    busy.current = true; setPending(true); setError('');
    try { await onSend(text); setDraft(''); } catch { setError('Send was not confirmed. Inspect the conversation before sending again.'); }
    finally { busy.current = false; setPending(false); }
  }
  return <section className="mayura-ui" aria-label="Chat"><h2>Conversation</h2><p role="status">{state.snapshot?.status ?? state.connection}</p>
    <div role="log" aria-label="Messages">{history ?? <p>Send a message to start the conversation.</p>}{state.streamedOutput ? <><p>{state.streamedOutput.text}</p><small>{state.streamedOutput.withheld ? 'Further text withheld. Inspect the final result.' : 'Streaming preview; the final response is authoritative.'}</small></> : null}</div>
    <form onSubmit={send}><label>Message<textarea value={draft} maxLength={16000} disabled={pending} onChange={event => setDraft(event.target.value)} /></label>
      <button disabled={pending || !draft.trim()}>{pending ? 'Sending…' : 'Send message'}</button></form>{error ? <p role="alert">{error}</p> : null}</section>;
}
Shared CSS
.mayura-ui { --ui-paper: #faf8f1; --ui-ink: #253329; --ui-muted: #526357; --ui-line: #c7cfc1; --ui-accent: #28583b; color: var(--ui-ink); background: var(--ui-paper); font: 1rem/1.6 Georgia, serif; padding: clamp(1rem,3vw,2rem); overflow-wrap: anywhere; }
.mayura-ui :where(h2,h3,p) { margin: 0 0 .75rem; }
.mayura-ui :where(button,textarea,input) { font: inherit; color: inherit; }
.mayura-ui button { border: 1px solid var(--ui-line); padding: .5rem .9rem; background: transparent; cursor: pointer; }
.mayura-ui button:disabled { opacity: .55; cursor: default; }
.mayura-ui :focus-visible { outline: 2px solid var(--ui-accent); outline-offset: 4px; }
.mayura-ui textarea { width: 100%; min-height: 6rem; box-sizing: border-box; background: transparent; border: 1px solid var(--ui-line); padding: .7rem; resize: vertical; }
.mayura-ui pre { white-space: pre-wrap; max-height: 20rem; overflow: auto; }
.mayura-ui [role=log] { max-height: 32rem; overflow: auto; }
.mayura-ui a { color: var(--ui-accent); text-decoration-thickness: .1em; text-underline-offset: .2em; }
.mayura-ui small { color: var(--ui-muted); }
.mayura-ui .ui-row { display: flex; flex-wrap: wrap; align-items: center; gap: .65rem; }
.mayura-ui li { margin-block: .6rem; }
.mayura-ui [role=alert] { border-block: 1px solid var(--ui-line); padding-block: .5rem; }