ChatPalette
A chat palette to create a chatbot interface inside an overlay.
Usage
The ChatPalette component is a structured layout wrapper that organizes ChatMessages in a scrollable content area and ChatPrompt in a fixed bottom section, creating cohesive chatbot interfaces for modals, slideovers, or drawers.
<template>
<PChatPalette>
<PChatMessages />
<template #prompt>
<PChatPrompt />
</template>
</PChatPalette>
</template>
Examples
Within a Modal
You can use the ChatPalette component inside a Modal's content.
<script setup lang="ts">
import type { UIMessage } from 'ai';
import { useChat } from '@ai-sdk/vue';
import { Markdown } from '@comark/vue';
import security from '@comark/vue/plugins/security';
import shiki from '@comark/vue/plugins/shiki';
import { isTextUIPart } from 'ai';
import { isPartStreaming } from 'pohon-ui/utils/ai';
const plugins = [shiki(), security({ blockedTags: ['script', 'style', 'iframe', 'object', 'embed', 'form'] })];
const initialMessages: Array<UIMessage> = [{
id: '1',
role: 'user',
parts: [{ type: 'text', text: 'What is Pohon UI?' }],
}, {
id: '2',
role: 'assistant',
parts: [{ type: 'text', text: 'Pohon UI is a Vue component library built on Akar, UnoCSS, and UnoCSS Variants. It provides 125+ accessible components for building modern web apps.' }],
}];
const input = ref('');
const { messages, status, error, sendMessage } = useChat({
messages: initialMessages,
});
function onSubmit() {
if (!input.value.trim()) {
return;
}
sendMessage({ text: input.value });
input.value = '';
}
const ui = {
prose: {
p: { base: 'my-2 leading-6' },
li: { base: 'my-0.5 leading-6' },
ul: { base: 'my-2' },
ol: { base: 'my-2' },
h1: { base: 'text-xl my-2' },
h2: { base: 'text-lg my-2' },
h3: { base: 'text-base my-2' },
h4: { base: 'text-sm my-2' },
pre: { root: 'my-2' },
table: { root: 'my-2' },
hr: { base: 'my-2' },
},
};
</script>
<template>
<PModal
open
:ui="{ content: 'sm:max-w-3xl sm:h-[28rem]' }"
>
<template #content>
<PTheme :ui="ui">
<PChatPalette>
<PChatMessages
:messages="messages"
:status="status"
:user="{ side: 'left', variant: 'naked', avatar: { src: 'https://github.com/benjamincanac.png', loading: 'lazy' as const } }"
:assistant="{ icon: 'i-lucide-bot' }"
>
<template #content="{ message }">
<template
v-for="(part, index) in message.parts"
:key="`${message.id}-${part.type}-${index}`"
>
<template v-if="isTextUIPart(part)">
<Markdown
v-if="message.role === 'assistant'"
:value="part.text"
:streaming="isPartStreaming(part)"
:plugins="plugins"
class="*:first:mt-0 *:last:mb-0"
/>
<p
v-else-if="message.role === 'user'"
class="leading-6 whitespace-pre-wrap"
>
{{ part.text }}
</p>
</template>
</template>
</template>
</PChatMessages>
<template #prompt>
<PChatPrompt
v-model="input"
icon="i-lucide-search"
variant="naked"
:error="error"
@submit="onSubmit"
/>
</template>
</PChatPalette>
</PTheme>
</template>
</PModal>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { UIMessage } from 'ai';
import { useChat } from '@ai-sdk/vue';
import { Markdown } from '@comark/vue';
import security from '@comark/vue/plugins/security';
import shiki from '@comark/vue/plugins/shiki';
import { isTextUIPart } from 'ai';
import { isPartStreaming } from 'pohon-ui/utils/ai';
const plugins = [shiki(), security({ blockedTags: ['script', 'style', 'iframe', 'object', 'embed', 'form'] })];
const initialMessages: Array<UIMessage> = [{
id: '1',
role: 'user',
parts: [{ type: 'text', text: 'What is Pohon UI?' }],
}, {
id: '2',
role: 'assistant',
parts: [{ type: 'text', text: 'Pohon UI is a Vue component library built on Akar, UnoCSS, and UnoCSS Variants. It provides 125+ accessible components for building modern web apps.' }],
}];
const input = ref('');
const { messages, status, error, sendMessage } = useChat({
messages: initialMessages,
});
function onSubmit() {
if (!input.value.trim()) {
return;
}
sendMessage({ text: input.value });
input.value = '';
}
const ui = {
prose: {
p: { base: 'my-2 leading-6' },
li: { base: 'my-0.5 leading-6' },
ul: { base: 'my-2' },
ol: { base: 'my-2' },
h1: { base: 'text-xl my-2' },
h2: { base: 'text-lg my-2' },
h3: { base: 'text-base my-2' },
h4: { base: 'text-sm my-2' },
pre: { root: 'my-2' },
table: { root: 'my-2' },
hr: { base: 'my-2' },
},
};
</script>
<template>
<PModal
open
:ui="{ content: 'sm:max-w-3xl sm:h-[28rem]' }"
>
<template #content>
<PTheme :ui="ui">
<PChatPalette>
<PChatMessages
:messages="messages"
:status="status"
:user="{ side: 'left', variant: 'naked', avatar: { src: 'https://github.com/benjamincanac.png', loading: 'lazy' as const } }"
:assistant="{ icon: 'i-lucide-bot' }"
>
<template #content="{ message }">
<template
v-for="(part, index) in message.parts"
:key="`${message.id}-${part.type}-${index}`"
>
<template v-if="isTextUIPart(part)">
<Markdown
v-if="message.role === 'assistant'"
:value="part.text"
:streaming="isPartStreaming(part)"
:plugins="plugins"
class="*:first:mt-0 *:last:mb-0"
/>
<p
v-else-if="message.role === 'user'"
class="leading-6 whitespace-pre-wrap"
>
{{ part.text }}
</p>
</template>
</template>
</template>
</PChatMessages>
<template #prompt>
<PChatPrompt
v-model="input"
icon="i-lucide-search"
variant="naked"
:error="error"
@submit="onSubmit"
/>
</template>
</PChatPalette>
</PTheme>
</template>
</PModal>
</template>
Within ContentSearch
You can use the ChatPalette component conditionally inside ContentSearch's content to display a chatbot interface when a user selects an item.
<script setup lang="ts">
import { useChat } from '@ai-sdk/vue';
import { Markdown } from '@comark/vue';
import security from '@comark/vue/plugins/security';
import shiki from '@comark/vue/plugins/shiki';
import { isTextUIPart } from 'ai';
import { isPartStreaming } from 'pohon-ui/utils/ai';
const plugins = [shiki(), security({ blockedTags: ['script', 'style', 'iframe', 'object', 'embed', 'form'] })];
const input = ref('');
const { messages, status, error, sendMessage, regenerate } = useChat();
const groups = computed(() => [{
id: 'ai',
ignoreFilter: true,
items: [{
label: searchTerm.value ? `Ask AI for “${searchTerm.value}”` : 'Ask AI',
icon: 'i-lucide-bot',
onSelect: (e: any) => {
e.preventDefault();
ai.value = true;
if (searchTerm.value) {
messages.value = [...messages.value, {
id: '1',
role: 'user',
parts: [{ type: 'text', text: searchTerm.value }],
}];
regenerate();
}
},
}],
}]);
const ai = ref(false);
const searchTerm = ref('');
function onSubmit() {
if (!input.value.trim()) {
return;
}
sendMessage({ text: input.value });
input.value = '';
}
function onClose(e: Event) {
e.preventDefault();
ai.value = false;
}
const ui = {
prose: {
p: { base: 'my-2 leading-6' },
li: { base: 'my-0.5 leading-6' },
ul: { base: 'my-2' },
ol: { base: 'my-2' },
h1: { base: 'text-xl my-2' },
h2: { base: 'text-lg my-2' },
h3: { base: 'text-base my-2' },
h4: { base: 'text-sm my-2' },
pre: { root: 'my-2' },
table: { root: 'my-2' },
hr: { base: 'my-2' },
},
};
</script>
<template>
<PContentSearch
v-model:search-term="searchTerm"
open
:groups="groups"
>
<template
v-if="ai"
#content
>
<PTheme :ui="ui">
<PChatPalette>
<PChatMessages
:messages="messages"
:status="status"
:user="{ side: 'left', variant: 'naked', avatar: { src: 'https://github.com/benjamincanac.png', loading: 'lazy' as const } }"
:assistant="{ icon: 'i-lucide-bot' }"
>
<template #content="{ message }">
<template
v-for="(part, index) in message.parts"
:key="`${message.id}-${part.type}-${index}`"
>
<template v-if="isTextUIPart(part)">
<Markdown
v-if="message.role === 'assistant'"
:value="part.text"
:streaming="isPartStreaming(part)"
:plugins="plugins"
class="*:first:mt-0 *:last:mb-0"
/>
<p
v-else-if="message.role === 'user'"
class="leading-6 whitespace-pre-wrap"
>
{{ part.text }}
</p>
</template>
</template>
</template>
</PChatMessages>
<template #prompt>
<PChatPrompt
v-model="input"
icon="i-lucide-search"
variant="naked"
:error="error"
@submit="onSubmit"
@close="onClose"
/>
</template>
</PChatPalette>
</PTheme>
</template>
</PContentSearch>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useChat } from '@ai-sdk/vue';
import { Markdown } from '@comark/vue';
import security from '@comark/vue/plugins/security';
import shiki from '@comark/vue/plugins/shiki';
import { isTextUIPart } from 'ai';
import { isPartStreaming } from 'pohon-ui/utils/ai';
const plugins = [shiki(), security({ blockedTags: ['script', 'style', 'iframe', 'object', 'embed', 'form'] })];
const input = ref('');
const { messages, status, error, sendMessage, regenerate } = useChat();
const groups = computed(() => [{
id: 'ai',
ignoreFilter: true,
items: [{
label: searchTerm.value ? `Ask AI for “${searchTerm.value}”` : 'Ask AI',
icon: 'i-lucide-bot',
onSelect: (e: any) => {
e.preventDefault();
ai.value = true;
if (searchTerm.value) {
messages.value = [...messages.value, {
id: '1',
role: 'user',
parts: [{ type: 'text', text: searchTerm.value }],
}];
regenerate();
}
},
}],
}]);
const ai = ref(false);
const searchTerm = ref('');
function onSubmit() {
if (!input.value.trim()) {
return;
}
sendMessage({ text: input.value });
input.value = '';
}
function onClose(e: Event) {
e.preventDefault();
ai.value = false;
}
const ui = {
prose: {
p: { base: 'my-2 leading-6' },
li: { base: 'my-0.5 leading-6' },
ul: { base: 'my-2' },
ol: { base: 'my-2' },
h1: { base: 'text-xl my-2' },
h2: { base: 'text-lg my-2' },
h3: { base: 'text-base my-2' },
h4: { base: 'text-sm my-2' },
pre: { root: 'my-2' },
table: { root: 'my-2' },
hr: { base: 'my-2' },
},
};
</script>
<template>
<PContentSearch
v-model:search-term="searchTerm"
open
:groups="groups"
>
<template
v-if="ai"
#content
>
<PTheme :ui="ui">
<PChatPalette>
<PChatMessages
:messages="messages"
:status="status"
:user="{ side: 'left', variant: 'naked', avatar: { src: 'https://github.com/benjamincanac.png', loading: 'lazy' as const } }"
:assistant="{ icon: 'i-lucide-bot' }"
>
<template #content="{ message }">
<template
v-for="(part, index) in message.parts"
:key="`${message.id}-${part.type}-${index}`"
>
<template v-if="isTextUIPart(part)">
<Markdown
v-if="message.role === 'assistant'"
:value="part.text"
:streaming="isPartStreaming(part)"
:plugins="plugins"
class="*:first:mt-0 *:last:mb-0"
/>
<p
v-else-if="message.role === 'user'"
class="leading-6 whitespace-pre-wrap"
>
{{ part.text }}
</p>
</template>
</template>
</template>
</PChatMessages>
<template #prompt>
<PChatPrompt
v-model="input"
icon="i-lucide-search"
variant="naked"
:error="error"
@submit="onSubmit"
@close="onClose"
/>
</template>
</PChatPalette>
</PTheme>
</template>
</PContentSearch>
</template>
API
Props
| Prop | Default | Type |
|---|---|---|
as | 'div' | anyThe element or component this component should render as. |
ui | { root?: SlotClass; prompt?: SlotClass; close?: SlotClass; content?: SlotClass; } |
Slots
| Slot | Type |
|---|---|
default | {} |
prompt | {} |
Theme
app.config.ts
export default defineAppConfig({
ui: {
chatPalette: {
slots: {
root: 'relative flex-1 flex flex-col min-h-0 min-w-0',
prompt: 'px-0 rounded-t-none border-t border-border',
close: '',
content: 'overflow-y-auto flex-1 flex flex-col py-3'
}
}
}
})vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from 'pohon-ui/vite'
export default defineConfig({
plugins: [
vue(),
ui({
ui: {
chatPalette: {
slots: {
root: 'relative flex-1 flex flex-col min-h-0 min-w-0',
prompt: 'px-0 rounded-t-none border-t border-border',
close: '',
content: 'overflow-y-auto flex-1 flex flex-col py-3'
}
}
}
})
]
})Changelog
No recent changes