Installation
Complete project setup with vue/reka, then install these components:
pnpm exec frontfriend download button message bubble input-group icon -f vue --primitive reka The row scrolls horizontally when its buttons do not fit. Buttons stay in the tab order, and focus brings each one into view. Connect the selected prompt to your send action.
Usage
Complete example
<script setup lang="ts">
import { ref } from "vue";
import { Message } from "@/components/ui/message";
import { MessageContent } from "@/components/ui/message";
import { Bubble } from "@/components/ui/bubble";
import { BubbleContent } from "@/components/ui/bubble";
import { Button } from "@/components/ui/button";
import { InputGroup } from "@/components/ui/input-group";
import { InputGroupTextarea } from "@/components/ui/input-group";
import { InputGroupAddon } from "@/components/ui/input-group";
import { InputGroupButton } from "@/components/ui/input-group";
import { Icon } from "@/components/ui/icon";
const args = {
"disabled": false,
};
const prompt = ref("");
const text = ref("");
const status = ref("");
const fileInput = ref<HTMLInputElement | null>(null);
const suggestions = [
"Add a dark mode toggle",
"Check the form",
"Explain this change",
"Write a test",
];
function send(value: string) {
if (args.disabled || !value.trim()) return;
prompt.value = value.trim();
text.value = "";
status.value = "Reply added.";
}
function keydown(event: KeyboardEvent) {
if (event.key === "Enter" && !event.shiftKey && !event.isComposing) {
event.preventDefault();
send(text.value);
}
}
function selectFiles(event: Event) {
const input = event.target as HTMLInputElement;
const count = input.files?.length ?? 0;
status.value = `${count} ${count === 1 ? "file" : "files"} attached.`;
input.value = "";
}
</script>
<template>
<div
style="
width: 100%;
max-width: 560px;
min-width: 0;
margin-inline: auto;
display: flex;
flex-direction: column;
gap: 24px;
padding-block: 16px;
font-size: var(--text-sm);
line-height: 1.5;
"
>
<div v-if="prompt" style="display: grid; gap: 16px">
<Message align="end"
><MessageContent
><Bubble align="end" variant="muted"
><BubbleContent>{{ prompt }}</BubbleContent></Bubble
></MessageContent
></Message
><Message
><MessageContent
><p>
I will help you with “{{ prompt }}”. Start with a small change, then
check the result.
</p></MessageContent
></Message
>
</div>
<div style="display: grid; gap: 8px">
<div
role="group"
aria-label="Suggested prompts"
style="display: flex; gap: 8px; overflow-x: auto; padding-block: 4px"
>
<span
v-for="suggestion in suggestions"
:key="suggestion"
style="flex-shrink: 0"
><Button
variant="outline"
size="sm"
:disabled="args.disabled"
@click="send(suggestion)"
>{{ suggestion }}</Button
></span
>
</div>
<form @submit.prevent="send(text)">
<input
ref="fileInput"
type="file"
multiple
hidden
aria-label="Attach files"
@change="selectFiles"
/><InputGroup
><InputGroupTextarea
v-model="text"
aria-label="Prompt"
placeholder="Ask the agent to change your code..."
:disabled="args.disabled"
@keydown="keydown" /><InputGroupAddon align="block-end"
><InputGroupButton
type="button"
size="icon-sm"
variant="outline"
aria-label="Attach files"
:disabled="args.disabled"
@click="fileInput?.click()"
><Icon name="Plus" /></InputGroupButton
><span style="flex: 1" /><InputGroupButton
type="submit"
size="icon-sm"
:variant="text.trim() && !args.disabled ? 'default' : 'secondary'"
aria-label="Send"
:aria-disabled="args.disabled || !text.trim()"
><Icon name="ArrowUp" /></InputGroupButton></InputGroupAddon
></InputGroup>
</form>
</div>
<p
role="status"
style="
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
border: 0;
"
>
{{ status }}
</p>
</div>
</template> Components and API
This composition has no separate component API. Use the APIs for its installed components.
| Component | Target reference |
|---|---|
button | Reka UI API |
message | Reka UI API |
bubble | Reka UI API |
input-group | Reka UI API |
icon | Reka UI API |