组件
AI 提示输入 AI Prompt Input
AI 输入框:复用共享 field textarea,状态机的 autoresize 让高度自动增长,发送按钮位于最后一行。主体周围的插槽对应输入区结构:`header` 放上方附件,`leading` 放文本左侧工具,`trailing` 放按钮本身,`footer` 放文本下方工具,`footer-end` 放 footer 行末尾的控件;发送按钮默认落在 `footer-end`,因此加入模型选择器后会与发送按钮同排。插槽为空时不渲染对应部件,空白输入框仍保持一行。组件受控:绑定 `v-model`,在 `submit` 时获取文本。Enter 发送,Shift+Enter 换行。`busy` 时发送按钮变为停止按钮,Enter 暂不生效。
#基础用法
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>
#属性
#AiPromptInput
AI 输入框:复用共享 field textarea,状态机的 autoresize 让高度自动增长,发送按钮位于最后一行。主体周围的插槽对应输入区结构:header 放上方附件,leading 放文本左侧工具,trailing 放按钮本身,footer 放文本下方工具,footer-end 放 footer 行末尾的控件;发送按钮默认落在 footer-end,因此加入模型选择器后会与发送按钮同排。插槽为空时不渲染对应部件,空白输入框仍保持一行。组件受控:绑定 v-model,在 submit 时获取文本。Enter 发送,Shift+Enter 换行。busy 时发送按钮变为停止按钮,Enter 暂不生效。
| 属性 | 类型 | 默认值 | 描述 |
|---|---|---|---|
modelValue | String | "" | 输入框中的草稿,绑定 v-model;提交成功后自动清空 |
placeholder | String | "Send a message" | 输入前的占位提示 |
disabled | Boolean | false | 禁用时文字仍留在输入区域,但提交按钮不生效,Enter 只换行 |
busy | Boolean | false | 状态机处理中;提交按钮变为停止按钮,Enter 暂不提交 |
mentions | Object as PropType<{ items: MentionEntry[]; trigger?: string }> | undefined | 可提及的候选项;传入候选列表和触发字符(默认 @),组件会附着到 textarea 上;候选列表打开时,Enter 插入候选值,发送暂缓 |