Components
AI Prompt Input
The prompt vessel: the shared field textarea — self-growing on the machine's autoresize — with the submit seal riding its last line. Around the body, slots answer the composer's anatomy: `header` for attachments riding above, `leading` for the tools at the text's left, `trailing` for the seal itself, `footer` for the tools beneath, and `footer-end` for the controls that close the footer row — the seal falls there by default, so a model picker dropped in rides the send's shoulder. An empty slot renders no part, so the bare vessel stays one quiet line. Controlled — bind `v-model` and take the text on `submit`. Enter sends; Shift+Enter breaks the line. While `busy` the seal becomes a stop seal and Enter holds its breath.
#Basic usage
Type and press Enter.
<script setup lang="ts">
import { AiPromptInput } from "@bysages/vue";
import { ref } from "vue";
const prompt = ref("");
const sent = ref("");
</script>
<template>
<!-- The prompt vessel alone: controlled, self-growing, Enter to send. -->
<div class="grid gap-3 w-full">
<AiPromptInput
v-model="prompt"
@submit="
sent = $event;
prompt = '';
"
/>
<p class="m-0 text-tertiary text-sm">
{{ sent ? `Sent: ${sent}` : "Type and press Enter." }}
</p>
</div>
</template>
Attach, pick a model, type and press Enter.
<script setup lang="ts">
import { createListCollection } from "@ark-ui/vue/select";
import { globe, plus } from "@bysages/icons";
import {
AiAttachment,
AiAttachments,
AiPromptInput,
Button,
Icon,
Menu,
Select,
Toggle,
} from "@bysages/vue";
import { ref } from "vue";
// The composer at full dress: attachments above, and one tool row
// beneath the text — the plus menu, the web-search toggle and the
// model picker at the left, the seal closing the row at the right.
// Only the seal submits the form — every other trigger opts out.
const prompt = ref("");
const busy = ref(false);
const webSearch = ref(false);
const sent = ref("");
const files = ref([
{ name: "colophon.png", size: 48213, status: "ready" as "ready" | "uploading" | "error" },
{ name: "notes.md", size: 1024, status: "uploading" as "ready" | "uploading" | "error" },
]);
const models = createListCollection({
items: [
{ label: "Hunyuan", value: "hunyuan" },
{ label: "Qinghua", value: "qinghua" },
{ label: "Celadon", value: "celadon" },
],
});
const picker = ref<HTMLInputElement>();
const onPicked = (event: Event) => {
const input = event.target as HTMLInputElement;
for (const file of input.files ?? []) {
files.value.push({ name: file.name, size: file.size, status: "uploading" });
setTimeout(() => {
const found = files.value.find((f) => f.name === file.name);
if (found) found.status = "ready";
}, 1000);
}
input.value = "";
};
const remove = (name: string) => {
files.value = files.value.filter((f) => f.name !== name);
};
const send = (value: string) => {
sent.value = value;
busy.value = true;
setTimeout(() => {
busy.value = false;
}, 2000);
};
</script>
<template>
<div class="grid gap-3 w-full">
<input ref="picker" type="file" multiple hidden @change="onPicked" />
<AiPromptInput v-model="prompt" :busy="busy" @submit="send" @stop="busy = false">
<template #header>
<AiAttachments>
<AiAttachment
v-for="file in files"
:key="file.name"
:name="file.name"
:size="file.size"
:status="file.status"
@remove="remove(file.name)"
/>
</AiAttachments>
</template>
<template #footer>
<Menu.Root>
<Menu.Trigger as-child>
<Button variant="ghost" square aria-label="Attach">
<Icon :glyph="plus" />
</Button>
</Menu.Trigger>
<Teleport to="body">
<Menu.Positioner>
<Menu.Content>
<Menu.Item value="image" @select="picker?.click()">Upload image</Menu.Item>
<Menu.Item value="file" @select="picker?.click()">Upload file</Menu.Item>
</Menu.Content>
</Menu.Positioner>
</Teleport>
</Menu.Root>
<Toggle.Root v-model:pressed="webSearch" type="button">
<Icon :glyph="globe" />
<span>Web search</span>
</Toggle.Root>
</template>
<template #footerEnd>
<Select.Root :collection="models" default-value="hunyuan">
<Select.Trigger type="button" aria-label="Model">
<Select.ValueText placeholder="Model" />
<Select.Indicator>
<svg
width="14"
height="14"
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="square"
aria-hidden="true"
>
<path d="m4 6 4 4 4-4" />
</svg>
</Select.Indicator>
</Select.Trigger>
<Teleport to="body">
<Select.Positioner>
<Select.Content>
<Select.Item v-for="item in models.items" :key="item.value" :item="item">
<Select.ItemText>{{ item.label }}</Select.ItemText>
<Select.ItemIndicator>✓</Select.ItemIndicator>
</Select.Item>
</Select.Content>
</Select.Positioner>
</Teleport>
</Select.Root>
</template>
</AiPromptInput>
<p class="m-0 text-tertiary text-sm">
{{
busy
? "Working — the seal is a stop now; Enter holds its breath."
: sent
? `Sent: ${sent}`
: "Attach, pick a model, type and press Enter."
}}
</p>
</div>
</template>
#Props
#AiPromptInput
The prompt vessel: the shared field textarea — self-growing on the
machine's autoresize — with the submit seal riding its last line.
Around the body, slots answer the composer's anatomy: header for
attachments riding above, leading for the tools at the text's
left, trailing for the seal itself, footer for the tools beneath,
and footer-end for the controls that close the footer row — the
seal falls there by default, so a model picker dropped in rides the
send's shoulder. An empty slot renders no part, so the bare vessel
stays one quiet line. Controlled — bind v-model and take the text
on submit. Enter sends; Shift+Enter breaks the line. While busy
the seal becomes a stop seal and Enter holds its breath.
| Prop | Type | Default | Description |
|---|---|---|---|
modelValue | String | "" | The draft in the vessel — bind v-model; it clears itself on a successful submit. |
placeholder | String | "Send a message" | The quiet invitation before the reader types. |
disabled | Boolean | false | Still the words on the canvas, but the submit seal does nothing and Enter stays a line break. |
busy | Boolean | false | The machine is working — the seal becomes a stop seal and Enter holds its breath. |
mentions | Object as PropType<{ items: MentionEntry[]; trigger?: string }> | undefined | Mention candidates for the field: pass the roster and the summon character (@ unless told otherwise) and the vessel rides the textarea. While candidates are up, Enter inserts and the send waits. |