Patterns · Original
AI composer
A local prompt composer with model selection, prompt chips, attachments, and a real submit callback.
Live preview
Try it in context
Customize preview
Source delivery
v0.3.0Usage & source
"use client";
import type { CSSProperties } from "react";
import { AIComposer } from "./components/stepkit/AIComposer";
import "./components/stepkit/styles.css";
import "./components/stepkit/showcase.css";
const themeStyle = {"--stepkit-bg":"#111217","--stepkit-surface":"#1b1c24","--stepkit-surface-muted":"#22232d","--stepkit-fg":"#f6f5fb","--stepkit-muted":"#a4a6b4","--stepkit-border":"#343641","--stepkit-border-strong":"#565866","--stepkit-code":"#20212a","--stepkit-shadow":"0 24px 70px rgba(0, 0, 0, .34)","--stepkit-accent":"#057052","--stepkit-accent-strong":"#034b38","--stepkit-accent-soft":"#153f35","--stepkit-accent-on-soft":"#63d4b2","--stepkit-accent-text":"#ffffff","--stepkit-error":"#ff9ba9","--stepkit-placeholder":"#b8bac6","--stepkit-radius":"16px","--stepkit-space":"16px","--stepkit-control-height":"44px","--stepkit-font-sans":"Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif"} as CSSProperties;
export default function Example() {
return (
<div className="stepkit-theme" data-stepkit-preset="modern" style={themeStyle}>
<AIComposer
title={"What are you making room for?"}
description={"A focused prompt surface for the first useful pass."}
/>
</div>
);
}Dependencies
[email protected] [email protected]
/*
* Stepkit 0.3.0
* Copyright (c) 2026 Vladislav Stepanov
*
* Original Stepkit portions in this release are licensed under the MIT License. Adapted portions identify their upstream licence and attribution in the copied source and in the entry-specific upstream notice files.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
"use client";
import { useId, useState, type FormEvent } from "react";
import "./styles.css";
import "./showcase.css";
export interface AIComposerSubmitPayload { prompt: string; model: string; attachments: string[]; }
export interface AIComposerProps {
title?: string;
description?: string;
value?: string;
defaultValue?: string;
placeholder?: string;
models?: string[];
model?: string;
defaultModel?: string;
promptChips?: string[];
defaultAttachments?: string[];
onValueChange?: (value: string) => void;
onModelChange?: (model: string) => void;
onAttachmentChange?: (files: File[]) => void;
onSubmit?: (payload: AIComposerSubmitPayload) => void;
className?: string;
}
const defaultModels = ["Muse 2.1", "Muse fast", "Muse canvas"];
const defaultChips = ["Tighten the narrative", "Find the quiet detail", "Make a first draft"];
function SendIcon() {
return <svg viewBox="0 0 24 24" aria-hidden="true"><path d="m4 5 16 7-16 7 3.2-7L4 5Zm3.6 7H20" fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.7" /></svg>;
}
export function AIComposer({
title = "What are you making room for?",
description = "A focused prompt surface for the first useful pass.",
value,
defaultValue = "",
placeholder = "Describe the shape of the thing you want to make…",
models = defaultModels,
model,
defaultModel = models[0] ?? "Model",
promptChips = defaultChips,
defaultAttachments = [],
onValueChange,
onModelChange,
onAttachmentChange,
onSubmit,
className = ""
}: AIComposerProps) {
const fileInputId = useId();
const [internalValue, setInternalValue] = useState(defaultValue);
const [internalModel, setInternalModel] = useState(defaultModel);
const [files, setFiles] = useState<File[]>([]);
const [attachmentNames, setAttachmentNames] = useState(defaultAttachments);
const [sent, setSent] = useState(false);
const draft = value ?? internalValue;
const selectedModel = model ?? internalModel;
const setDraft = (next: string) => { if (value === undefined) setInternalValue(next); onValueChange?.(next); setSent(false); };
const setSelectedModel = (next: string) => { if (model === undefined) setInternalModel(next); onModelChange?.(next); };
const handleFiles = (nextFiles: File[]) => {
setFiles(nextFiles);
setAttachmentNames(nextFiles.map((file) => file.name));
onAttachmentChange?.(nextFiles);
};
const removeAttachment = (index: number) => {
setAttachmentNames((current) => current.filter((_, fileIndex) => fileIndex !== index));
const nextFiles = files.filter((_, fileIndex) => fileIndex !== index);
setFiles(nextFiles);
onAttachmentChange?.(nextFiles);
};
const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
const prompt = draft.trim();
if (!prompt) return;
setSent(true);
onSubmit?.({ prompt, model: selectedModel, attachments: attachmentNames });
};
return (
<section className={`stepkit stepkit-showcase stepkit-ai-composer${className ? ` ${className}` : ""}`} aria-labelledby={`${fileInputId}-title`}>
<div className="stepkit-ai-composer__inner">
<span className="stepkit-ai-composer__eyebrow">Compose / prompt</span>
<h2 className="stepkit-ai-composer__title" id={`${fileInputId}-title`}>{title}</h2>
<p className="stepkit-ai-composer__description">{description}</p>
<form className="stepkit-ai-composer__panel" onSubmit={handleSubmit}>
<div className="stepkit-ai-composer__panel-head"><label htmlFor={`${fileInputId}-model`} className="stepkit-sr-only">Model</label><select className="stepkit-ai-composer__model" id={`${fileInputId}-model`} value={selectedModel} onChange={(event) => setSelectedModel(event.target.value)}>{models.map((item) => <option value={item} key={item}>{item}</option>)}</select><small>Local draft</small></div>
<label className="stepkit-sr-only" htmlFor={`${fileInputId}-prompt`}>Prompt</label>
<textarea className="stepkit-ai-composer__textarea" id={`${fileInputId}-prompt`} value={draft} onChange={(event) => setDraft(event.target.value)} placeholder={placeholder} />
{attachmentNames.length ? <div className="stepkit-ai-composer__attachments" aria-label="Selected attachments">{attachmentNames.map((name, index) => <span className="stepkit-ai-composer__attachment" key={`${name}-${index}`}>↳ {name}<button type="button" aria-label={`Remove ${name}`} onClick={() => removeAttachment(index)}>{"×"}</button></span>)}</div> : null}
<div className="stepkit-ai-composer__panel-foot"><div className="stepkit-ai-composer__chips">{promptChips.slice(0, 3).map((chip) => <button className="stepkit-ai-composer__chip" type="button" key={chip} onClick={() => setDraft(chip)}>{chip}</button>)}<label className="stepkit-ai-composer__attach" htmlFor={fileInputId}>+ Attach<input className="stepkit-ai-composer__file" id={fileInputId} type="file" multiple onChange={(event) => handleFiles(Array.from(event.target.files ?? []))} /></label></div><button className="stepkit-ai-composer__send" type="submit" disabled={!draft.trim()}>{sent ? "Staged" : "Send prompt"}<SendIcon /></button></div>
{sent ? <p className="stepkit-ai-composer__sent" role="status">Draft saved for {selectedModel}.</p> : null}
</form>
</div>
</section>
);
}
export default AIComposer;
components/AIComposer.tsxcomponents/showcase.csscomponents/styles.cssInstall
Use the registry command or download the complete source archive.
npx [email protected] add https://vstepanov.com/stepkit/r/ai-composer.jsonIntegration prompt
Install Stepkit AI composer 0.3.0 from the registry and preserve the included MIT notice. npx [email protected] add https://vstepanov.com/stepkit/v0.3.0/r/ai-composer.json Install the tested runtime dependencies exactly: npm install [email protected] [email protected] Keep the component source, shared styles, NOTICE.md, and provenance files together. Apply the theme tokens on a wrapper rather than passing theme keys as component props. Usage: ```tsx "use client"; import type { CSSProperties } from "react"; import { AIComposer } from "./components/stepkit/AIComposer"; import "./components/stepkit/styles.css"; import "./components/stepkit/showcase.css"; const themeStyle = {"--stepkit-bg":"#f7f7f8","--stepkit-surface":"#ffffff","--stepkit-surface-muted":"#f0f0f3","--stepkit-fg":"#17181c","--stepkit-muted":"#626572","--stepkit-border":"#e3e4e9","--stepkit-border-strong":"#c8cad3","--stepkit-code":"#f1f1f4","--stepkit-shadow":"0 20px 60px rgba(31, 26, 64, .10)","--stepkit-accent":"#6950df","--stepkit-accent-strong":"#5137c7","--stepkit-accent-soft":"#eeeafd","--stepkit-accent-on-soft":"#5137c7","--stepkit-accent-text":"#ffffff","--stepkit-error":"#b43145","--stepkit-placeholder":"#626572","--stepkit-radius":"16px","--stepkit-space":"16px","--stepkit-control-height":"44px","--stepkit-font-sans":"Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif"} as CSSProperties; export default function Example() { return ( <div className="stepkit-theme" data-stepkit-preset="modern" style={themeStyle}> <AIComposer title={"What are you making room for?"} description={"A focused prompt surface for the first useful pass."} /> </div> ); } ``` Selected bounded settings: {"title":"What are you making room for?","description":"A focused prompt surface for the first useful pass."}. Selected theme: {"mode":"light","accent":"violet","radius":"soft","density":"comfortable","preset":"modern"}.
Props
| Name | Type | Default | Description |
|---|---|---|---|
value | string | undefined | Controlled prompt value. |
onValueChange | (value: string) => void | undefined | Called after prompt edits. |
models | string[] | default models | Model labels in the selector. |
onSubmit | (payload: AIComposerSubmitPayload) => void | undefined | Receives the prompt, model, and local attachment names. |
onAttachmentChange | (files: File[]) => void | undefined | Called after local file selection. |
title | string | "What are you making room for?" | Composer heading. |
Accessibility & limits
- Uses an associated label for the multiline prompt and model selector.
- Attachment input remains keyboard accessible despite its visual treatment.
- Submit is disabled for an empty prompt and status is announced politely.
Limitations
- The component does not call a backend; the submit callback belongs to the consuming application.
Provenance
Original source under the MIT licence.
Original Stepkit implementation.
Required notices: NOTICE.md, PROVENANCE-stepkit-ai-composer.md