Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions docs/.vitepress/themeConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ const sharedSidebarItems = [
{ text: 'CLI 接入', link: 'cli-integration' },
],
},
{
text: '套件',
base: '/suites/',
items: [
{ text: 'Chat 聊天界面', link: 'chat' },
{ text: 'Chat 配置与操作', link: 'chat-runtime' },
],
},
{
text: '组件',
base: '/components/',
Expand Down Expand Up @@ -56,14 +64,6 @@ const sharedSidebarItems = [
text: '图标',
items: [{ text: 'SVG 图标', link: '/icons/' }],
},
{
text: '套件',
base: '/suites/',
items: [
{ text: 'Chat 聊天界面', link: 'chat' },
{ text: 'Chat 运行时', link: 'chat-runtime' },
],
},
]

const nav = [
Expand Down
102 changes: 102 additions & 0 deletions docs/demos/chat/bubble-options.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<script setup lang="ts">
import { computed, h, shallowRef } from 'vue'
import { TrChat, type ChatUIOptions } from '@opentiny/tiny-robot-chat'
import '@opentiny/tiny-robot-chat/dist/style.css'
import { useChatCaseRuntime } from './shared/createChatRuntime'

const customAppearance = shallowRef(true)
const userAvatar = h('span', { class: 'bubble-options-demo__avatar bubble-options-demo__avatar--user' }, '我')
const assistantAvatar = h('span', { class: 'bubble-options-demo__avatar bubble-options-demo__avatar--assistant' }, 'AI')
const runtime = useChatCaseRuntime({
storageKey: 'tiny-robot-demo-bubble-options-v1',
initialConversations: [
{
title: '消息外观配置',
messages: [
{ role: 'user', content: '如何定制聊天消息的外观?' },
{
role: 'assistant',
content:
'**可以按角色分别配置:**\n\n- 用户消息显示在右侧,使用圆角气泡。\n- AI 回答显示在左侧,不使用气泡框。\n\n切换上方选项,可比较默认外观。',
},
],
},
],
})

const ui = computed<ChatUIOptions>(() => ({
layout: { leftAside: false },
bubble: customAppearance.value
? {
bubbleList: {
roleConfigs: {
user: { placement: 'end', avatar: userAvatar, shape: 'rounded' },
assistant: { placement: 'start', avatar: assistantAvatar, shape: 'none' },
},
},
}
: {},
}))
</script>

<template>
<section class="bubble-options-demo">
<div class="bubble-options-demo__controls">
<label><input v-model="customAppearance" type="radio" :value="false" /> 默认外观</label>
<label><input v-model="customAppearance" type="radio" :value="true" /> 自定义外观</label>
</div>
<p class="bubble-options-demo__tip">比较头像和气泡外观;继续发送消息,新消息会沿用当前配置。</p>
<div class="bubble-options-demo__chat" :class="{ 'bubble-options-demo__chat--custom': customAppearance }">
<TrChat :runtime="runtime" :ui="ui" />
</div>
</section>
</template>

<style scoped>
.bubble-options-demo__controls {
display: flex;
flex-wrap: wrap;
gap: 12px;
}

.bubble-options-demo__tip {
margin: 12px 0;
color: var(--tr-text-secondary);
font-size: 13px;
}

.bubble-options-demo__chat {
--tr-layout-height: 100%;
height: 600px;
}

.bubble-options-demo__chat :deep(.tr-chat-ui) {
height: 100%;
min-height: 0;
}

.bubble-options-demo__chat :deep(.bubble-options-demo__avatar) {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border-radius: 50%;
font-size: 12px;
font-weight: 600;
}

.bubble-options-demo__chat :deep(.bubble-options-demo__avatar--user) {
color: #3964fe;
background: #e4edfd;
}

.bubble-options-demo__chat :deep(.bubble-options-demo__avatar--assistant) {
color: #087f5b;
background: #d3f9d8;
}

.bubble-options-demo__chat--custom :deep([data-box-type='box'][data-role='assistant']) {
--tr-bubble-box-bg: transparent;
}
</style>
30 changes: 23 additions & 7 deletions docs/demos/chat/controlled-ui.vue
Original file line number Diff line number Diff line change
@@ -1,34 +1,50 @@
<script setup lang="ts">
import { computed, shallowRef } from 'vue'
import { computed, onUnmounted, shallowRef } from 'vue'
import { TrChatUI, type ChatMessageItem, type ChatSendPayload, type ChatUIData } from '@opentiny/tiny-robot-chat'
import '@opentiny/tiny-robot-chat/dist/style.css'

const inputValue = shallowRef('')
const messages = shallowRef<ChatMessageItem[]>([])
const sending = shallowRef(false)
let replyTimer: ReturnType<typeof setTimeout> | undefined

const data = computed<ChatUIData>(() => ({
conversation: { activeId: 'controlled-demo', title: '受控数据' },
conversation: { activeId: 'controlled-demo', title: '应用助手' },
bubble: { messages: messages.value },
sender: { loading: sending.value },
request: { state: sending.value ? 'processing' : 'idle' },
}))

async function handleSubmit(payload: ChatSendPayload) {
function handleSubmit(payload: ChatSendPayload) {
if (!payload.text.trim() || sending.value) return

sending.value = true
messages.value = [...messages.value, { role: 'user', content: payload.text }]
inputValue.value = ''
await Promise.resolve()
messages.value = [...messages.value, { role: 'assistant', content: `已收到:${payload.text}` }]
replyTimer = setTimeout(() => {
messages.value = [...messages.value, { role: 'assistant', content: `已收到:${payload.text}` }]
replyTimer = undefined
sending.value = false
}, 1500)
}

function handleCancel() {
clearTimeout(replyTimer)
replyTimer = undefined
sending.value = false
}

onUnmounted(handleCancel)
</script>

<template>
<div class="controlled-ui-demo">
<TrChatUI :data="data" :input-value="inputValue" @update:input-value="inputValue = $event" @submit="handleSubmit" />
<TrChatUI
:data="data"
:ui="{ layout: { leftAside: false, rightAside: false } }"
v-model:input-value="inputValue"
@submit="handleSubmit"
@cancel="handleCancel"
/>
</div>
</template>

Expand Down
80 changes: 80 additions & 0 deletions docs/demos/chat/history-groups.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<script setup lang="ts">
import { computed } from 'vue'
import { TrChat, type ChatHistoryData, type ChatUIOptions } from '@opentiny/tiny-robot-chat'
import '@opentiny/tiny-robot-chat/dist/style.css'
import { useChatCaseRuntime } from './shared/createChatRuntime'

const groups = ['置顶', '昨天', '30天内']
const initialConversations = [
{ title: '初次问候与自我介绍', group: '置顶' },
{ title: 'square-pen 含义解释', group: '昨天' },
{ title: '浏览器 Agent 视觉操作优化', group: '30天内' },
{ title: 'VSCode 重启 TS 服务快捷键', group: '30天内' },
{ title: '如何制定英语学习计划', group: '30天内' },
]
const runtime = useChatCaseRuntime({
storageKey: 'tiny-robot-demo-history-groups-v1',
initialConversations: initialConversations.map(({ title, group }) => ({
title,
metadata: { group },
messages: [
{ role: 'user', content: title },
{ role: 'assistant', content: `这是“${title}”的示例回答。切换左侧会话,可查看对应的消息。` },
],
})),
})

const historyData = computed<ChatHistoryData>(() =>
groups
.map((group) => ({
group,
items: runtime.conversations.value.filter((item) => (item.metadata?.group ?? '30天内') === group),
}))
.filter(({ items }) => items.length > 0),
)

const ui: ChatUIOptions = {
layout: { leftAside: { width: 260, defaultOpen: true } },
history: { showRenameControls: true },
}
</script>

<template>
<section class="history-groups-demo">
<p class="history-groups-demo__tip">
展开会话列表后,可切换、重命名或删除会话。分组为预设示例,不包含自动日期归类或置顶操作;新会话显示在“30天内”。
</p>
<div class="history-groups-demo__chat">
<TrChat :runtime="runtime" :ui="ui" :history-data="historyData" />
</div>
</section>
</template>

<style scoped>
.history-groups-demo__tip {
margin: 0 0 12px;
color: var(--tr-text-secondary);
font-size: 13px;
}

.history-groups-demo__chat {
--tr-layout-height: 100%;
--tr-history-group-space-y: 14px;
--tr-history-group-title-font-size: 12px;
--tr-history-group-title-line-height: 18px;
--tr-history-group-title-padding: 0 8px 6px;
--tr-history-group-title-color: var(--tr-text-tertiary);
--tr-history-item-padding: 7px 8px;
--tr-history-item-padding-editing: 7px 8px;
--tr-history-item-border-radius: 8px;
--tr-history-item-space-y: 2px;
--tr-history-item-selected-bg: #e4edfd;
--tr-history-item-selected-color: #3964fe;
height: 600px;
}

.history-groups-demo__chat :deep(.tr-chat-ui) {
height: 100%;
min-height: 0;
}
</style>
47 changes: 43 additions & 4 deletions docs/demos/chat/layout-presets.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { computed, shallowRef } from 'vue'
import { TrChatUI, type ChatUIData, type ChatUIOptions } from '@opentiny/tiny-robot-chat'
import '@opentiny/tiny-robot-chat/dist/style.css'

type LayoutPreset = 'default' | 'compact' | 'focus'
type LayoutPreset = 'default' | 'compact' | 'focus' | 'welcome-footer' | 'welcome-center'

const preset = shallowRef<LayoutPreset>('default')
const inputValue = shallowRef('')
Expand All @@ -30,6 +30,12 @@ const data: ChatUIData = {
sender: { submitDisabled: true },
}

const emptyData: ChatUIData = {
conversation: { items: data.conversation?.items, activeId: null, title: '新对话' },
bubble: { messages: [] },
sender: { submitDisabled: true },
}

const presets: Record<LayoutPreset, { label: string; description: string; ui: ChatUIOptions }> = {
default: {
label: '默认布局',
Expand All @@ -38,7 +44,7 @@ const presets: Record<LayoutPreset, { label: string; description: string; ui: Ch
},
compact: {
label: '紧凑内容',
description: '收窄消息和输入区,并让会话列表默认展开。',
description: '内容最大宽度为 640px;左侧栏默认展开,展开宽度为 240px,收起宽度为 48px。',
ui: {
layout: {
contentMaxWidth: 640,
Expand All @@ -60,10 +66,31 @@ const presets: Record<LayoutPreset, { label: string; description: string; ui: Ch
},
},
},
'welcome-footer': {
label: '空会话:底部输入',
description: '没有消息时,输入区位于页面底部。',
ui: {
layout: {
composer: { welcome: 'footer' },
},
},
},
'welcome-center': {
label: '空会话:居中输入',
description: '没有消息时,输入区位于欢迎区中央;已有消息时仍位于页面底部。',
ui: {
layout: {
composer: { welcome: 'center' },
},
},
},
}

const presetOptions: LayoutPreset[] = ['default', 'compact', 'focus']
const presetOptions: LayoutPreset[] = ['default', 'compact', 'focus', 'welcome-footer', 'welcome-center']
const activePreset = computed(() => presets[preset.value])
const activeData = computed(() =>
preset.value === 'welcome-footer' || preset.value === 'welcome-center' ? emptyData : data,
)
</script>

<template>
Expand All @@ -82,7 +109,13 @@ const activePreset = computed(() => presets[preset.value])
<span>{{ activePreset.description }}</span>
</div>

<TrChatUI :data="data" :ui="activePreset.ui" :input-value="inputValue" @update:input-value="inputValue = $event" />
<TrChatUI
:key="preset"
:data="activeData"
:ui="activePreset.ui"
:input-value="inputValue"
@update:input-value="inputValue = $event"
/>
</section>
</template>

Expand Down Expand Up @@ -137,4 +170,10 @@ const activePreset = computed(() => presets[preset.value])
flex: 1;
min-height: 0;
}

.chat-layout-demo :deep(.tr-welcome__title-wrapper) {
display: flex;
align-items: center;
justify-content: center;
}
</style>
Loading