Skip to content

Commit 5a41d65

Browse files
committed
refactor(conversation): streamline conversation management and enhance demo examples
- Removed the '存储策略' sidebar item from the theme configuration to simplify navigation. - Added mock response and storage strategies to the conversation demo, allowing for a complete offline experience. - Updated the Basic.vue demo to include a message sender and delete conversation functionality, improving user interaction. - Introduced new utility files for mock response and storage strategies, enhancing the flexibility of the conversation management system. - Enhanced documentation for conversation management, providing clearer examples and usage instructions.
1 parent 7406f4c commit 5a41d65

26 files changed

+1382
-566
lines changed

docs/.vitepress/themeConfig.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ const sharedSidebarItems = [
3636
{ text: 'AI模型交互工具类', link: 'ai-client' },
3737
{ text: '消息数据管理', link: 'message' },
3838
{ text: '会话数据管理', link: 'conversation' },
39-
{ text: '存储策略', link: 'storage' },
4039
{ text: '工具函数', link: 'utils' },
4140
],
4241
},
Lines changed: 50 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
<template>
22
<div>
3-
<h1>会话</h1>
43
<tr-bubble-list :messages="messages" :role-configs="roles"></tr-bubble-list>
4+
<tr-sender
5+
v-model="inputMessage"
6+
:placeholder="isProcessing ? '模拟回复中...' : '请输入您的问题'"
7+
:clearable="true"
8+
:loading="isProcessing"
9+
@submit="handleSubmit"
10+
@cancel="abortActiveRequest"
11+
></tr-sender>
512
<div class="actions">
613
<span><b>切换会话</b></span>
714
<tiny-select
@@ -10,98 +17,56 @@
1017
@change="switchConversation($event)"
1118
></tiny-select>
1219
<tiny-button type="info" @click="createConversation()">创建新对话</tiny-button>
20+
<tiny-button type="danger" :disabled="!activeConversationId" @click="handleDeleteConversation">
21+
删除当前会话
22+
</tiny-button>
1323
</div>
1424
</div>
1525
</template>
1626

1727
<script setup lang="ts">
18-
import { BubbleRoleConfig, TrBubbleList } from '@opentiny/tiny-robot'
19-
import {
20-
ChatMessage,
21-
ConversationInfo,
22-
ConversationStorageStrategy,
23-
sseStreamToGenerator,
24-
useConversation,
25-
} from '@opentiny/tiny-robot-kit'
28+
import { BubbleRoleConfig, TrBubbleList, TrSender } from '@opentiny/tiny-robot'
29+
import type { UseMessageOptions } from '@opentiny/tiny-robot-kit'
30+
import { useConversation } from '@opentiny/tiny-robot-kit'
2631
import { IconAi, IconUser } from '@opentiny/tiny-robot-svgs'
2732
import { TinyButton, TinySelect } from '@opentiny/vue'
28-
import { computed, h } from 'vue'
29-
30-
class MockStorageStrategy implements ConversationStorageStrategy {
31-
private conversations: ConversationInfo[] = [
32-
{
33-
id: 'm9zfbomexdm9pza',
34-
title: '安排日程',
35-
createdAt: 1745744706662,
36-
updatedAt: 1745744717297,
37-
metadata: {},
38-
},
39-
{
40-
id: 'm9zefqta1rihhpj',
41-
title: '写段文案',
42-
createdAt: 1745743216510,
43-
updatedAt: 1745744704671,
44-
metadata: {},
45-
},
46-
]
47-
48-
private messagesMap: Map<string, ChatMessage[]> = new Map([
49-
[
50-
'm9zfbomexdm9pza',
51-
[
52-
{
53-
role: 'user',
54-
content: '今天需要我帮你安排日程,规划旅行,还是起草一封邮件?',
55-
},
56-
{
57-
role: 'assistant',
58-
content: '这是对 "今天需要我帮你安排日程,规划旅行,还是起草一封邮件?" 的模拟回复。',
59-
},
60-
],
61-
],
62-
[
63-
'm9zefqta1rihhpj',
64-
[
65-
{
66-
role: 'user',
67-
content: '想写段文案、起个名字,还是来点灵感?',
68-
},
69-
{
70-
role: 'assistant',
71-
content: '这是对 "想写段文案、起个名字,还是来点灵感?" 的模拟回复。',
72-
},
73-
{
74-
role: 'user',
75-
content: 'hello',
76-
},
77-
{
78-
role: 'assistant',
79-
content: '你好!我是TinyRobot搭建的AI助手。你可以问我任何问题,我会尽力回答。',
80-
},
81-
],
82-
],
83-
])
33+
import { computed, h, ref } from 'vue'
34+
import { mockResponseProvider } from './mockResponseProvider'
35+
import { MockStorageStrategy } from './mockStorageStrategy'
36+
37+
// useConversation basic usage: useMessageOptions.responseProvider + storage
38+
const {
39+
activeConversation,
40+
activeConversationId,
41+
conversations,
42+
createConversation,
43+
switchConversation,
44+
deleteConversation,
45+
abortActiveRequest,
46+
} = useConversation({
47+
useMessageOptions: {
48+
responseProvider: mockResponseProvider as UseMessageOptions['responseProvider'],
49+
},
50+
storage: new MockStorageStrategy(),
51+
})
8452
85-
async loadConversations(): Promise<ConversationInfo[]> {
86-
return this.conversations || []
87-
}
53+
const messages = computed(() => activeConversation.value?.engine?.messages.value || [])
54+
const isProcessing = computed(() => activeConversation.value?.engine?.isProcessing.value ?? false)
55+
const options = computed(() => conversations.value.map((c) => ({ label: c.title, value: c.id })))
8856
89-
async loadMessages(conversationId: string): Promise<ChatMessage[]> {
90-
return this.messagesMap.get(conversationId) || []
91-
}
57+
const inputMessage = ref('')
9258
93-
async saveConversation(conversation: ConversationInfo): Promise<void> {
94-
const index = this.conversations.findIndex((c) => c.id === conversation.id)
95-
if (index >= 0) {
96-
this.conversations[index] = conversation
97-
} else {
98-
this.conversations.push(conversation)
99-
}
100-
}
59+
function handleSubmit(content: string) {
60+
// Auto-create conversation if none exists
61+
const conversation = activeConversation.value ?? createConversation()
62+
conversation?.engine?.sendMessage(content)
63+
inputMessage.value = ''
64+
}
10165
102-
async saveMessages(conversationId: string, messages: ChatMessage[]): Promise<void> {
103-
this.messagesMap.set(conversationId, messages)
104-
}
66+
async function handleDeleteConversation() {
67+
const id = activeConversationId.value
68+
if (!id) return
69+
await deleteConversation(id)
10570
}
10671
10772
const aiAvatar = h(IconAi, { style: { fontSize: '32px' } })
@@ -117,42 +82,6 @@ const roles: Record<string, BubbleRoleConfig> = {
11782
avatar: userAvatar,
11883
},
11984
}
120-
121-
// Get BASE_URL from import.meta if available, otherwise use empty string
122-
interface ImportMetaEnv {
123-
BASE_URL?: string
124-
}
125-
interface ImportMetaWithEnv extends ImportMeta {
126-
env?: ImportMetaEnv
127-
}
128-
const meta = typeof import.meta !== 'undefined' ? (import.meta as ImportMetaWithEnv) : null
129-
const baseUrl = meta?.env?.BASE_URL || ''
130-
const apiUrl = window.parent?.location.origin || location.origin + baseUrl
131-
132-
const storage = new MockStorageStrategy()
133-
const { activeConversation, activeConversationId, conversations, createConversation, switchConversation } =
134-
useConversation({
135-
useMessageOptions: {
136-
responseProvider: async (requestBody, abortSignal) => {
137-
const response = await fetch(`${apiUrl}/api/chat/completions`, {
138-
method: 'POST',
139-
body: JSON.stringify({ ...requestBody, stream: true }),
140-
signal: abortSignal,
141-
})
142-
return sseStreamToGenerator(response, { signal: abortSignal })
143-
},
144-
},
145-
storage,
146-
})
147-
148-
const messages = computed(() => activeConversation.value?.engine?.messages.value || [])
149-
150-
const options = computed(() =>
151-
conversations.value.map((conversation) => ({
152-
label: conversation.title,
153-
value: conversation.id,
154-
})),
155-
)
15685
</script>
15786

15887
<style scoped>
@@ -168,6 +97,8 @@ const options = computed(() =>
16897
.actions {
16998
display: flex;
17099
align-items: center;
171-
margin-top: 10px;
100+
margin-top: 12px;
101+
flex-wrap: wrap;
102+
gap: 8px;
172103
}
173104
</style>
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import type { ChatCompletion, MessageRequestBody } from '@opentiny/tiny-robot-kit'
2+
3+
/**
4+
* Mock stream: simulates AI response without real API
5+
*/
6+
export async function* mockResponseProvider(
7+
_requestBody: MessageRequestBody,
8+
abortSignal: AbortSignal,
9+
): AsyncGenerator<ChatCompletion> {
10+
const reply = '这是模拟回复,无需真实 API。你可以切换会话、创建新对话体验完整流程。'
11+
const id = 'mock-' + Date.now()
12+
for (let i = 0; i < reply.length && !abortSignal.aborted; i++) {
13+
await new Promise((r) => setTimeout(r, 150))
14+
const deltaContent = reply[i]
15+
yield {
16+
id,
17+
object: 'chat.completion.chunk',
18+
created: Math.floor(Date.now() / 1000),
19+
model: 'mock',
20+
system_fingerprint: null,
21+
choices: [
22+
{
23+
index: 0,
24+
message: undefined,
25+
delta: i === 0 ? { role: 'assistant', content: deltaContent } : { content: deltaContent },
26+
finish_reason: i === reply.length - 1 ? 'stop' : null,
27+
logprobs: null,
28+
},
29+
],
30+
}
31+
}
32+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import type { ChatMessage, ConversationInfo, ConversationStorageStrategy } from '@opentiny/tiny-robot-kit'
2+
3+
/**
4+
* Mock storage: pre-loaded conversations and messages, no real persistence
5+
*/
6+
export class MockStorageStrategy implements ConversationStorageStrategy {
7+
private conversations: ConversationInfo[] = [
8+
{
9+
id: 'm9zfbomexdm9pza',
10+
title: '安排日程',
11+
createdAt: 1745744706662,
12+
updatedAt: 1745744717297,
13+
metadata: {},
14+
},
15+
{
16+
id: 'm9zefqta1rihhpj',
17+
title: '写段文案',
18+
createdAt: 1745743216510,
19+
updatedAt: 1745744704671,
20+
metadata: {},
21+
},
22+
]
23+
24+
private messagesMap: Map<string, ChatMessage[]> = new Map([
25+
[
26+
'm9zfbomexdm9pza',
27+
[
28+
{
29+
role: 'user',
30+
content: '今天需要我帮你安排日程,规划旅行,还是起草一封邮件?',
31+
},
32+
{
33+
role: 'assistant',
34+
content: '这是对 "今天需要我帮你安排日程,规划旅行,还是起草一封邮件?" 的模拟回复。',
35+
},
36+
],
37+
],
38+
[
39+
'm9zefqta1rihhpj',
40+
[
41+
{
42+
role: 'user',
43+
content: '想写段文案、起个名字,还是来点灵感?',
44+
},
45+
{
46+
role: 'assistant',
47+
content: '这是对 "想写段文案、起个名字,还是来点灵感?" 的模拟回复。',
48+
},
49+
{
50+
role: 'user',
51+
content: 'hello',
52+
},
53+
{
54+
role: 'assistant',
55+
content: '你好!我是TinyRobot搭建的AI助手。你可以问我任何问题,我会尽力回答。',
56+
},
57+
],
58+
],
59+
])
60+
61+
async loadConversations(): Promise<ConversationInfo[]> {
62+
return this.conversations || []
63+
}
64+
65+
async loadMessages(conversationId: string): Promise<ChatMessage[]> {
66+
return this.messagesMap.get(conversationId) || []
67+
}
68+
69+
async saveConversation(conversation: ConversationInfo): Promise<void> {
70+
const index = this.conversations.findIndex((c) => c.id === conversation.id)
71+
if (index >= 0) {
72+
this.conversations[index] = conversation
73+
} else {
74+
this.conversations.push(conversation)
75+
}
76+
}
77+
78+
async saveMessages(conversationId: string, messages: ChatMessage[]): Promise<void> {
79+
this.messagesMap.set(conversationId, messages)
80+
}
81+
82+
async deleteConversation(conversationId: string): Promise<void> {
83+
const index = this.conversations.findIndex((c) => c.id === conversationId)
84+
if (index >= 0) {
85+
this.conversations.splice(index, 1)
86+
}
87+
this.messagesMap.delete(conversationId)
88+
}
89+
}

docs/demos/tools/message/Basic.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { useMessage, sseStreamToGenerator } from '@opentiny/tiny-robot-kit'
2+
3+
// 若有 import.meta 则取 BASE_URL,否则为空字符串
4+
interface ImportMetaEnv {
5+
BASE_URL?: string
6+
}
7+
interface ImportMetaWithEnv extends ImportMeta {
8+
env?: ImportMetaEnv
9+
}
10+
const meta = typeof import.meta !== 'undefined' ? (import.meta as ImportMetaWithEnv) : null
11+
const baseUrl = meta?.env?.BASE_URL || ''
12+
const apiUrl = window.parent?.location.origin || location.origin + baseUrl
13+
14+
/**
15+
* useMessage 基础用法:responseProvider 发起流式请求,initialMessages 展示欢迎语
16+
*/
17+
export function useMessageBasic() {
18+
return useMessage({
19+
responseProvider: async (requestBody, abortSignal) => {
20+
const response = await fetch(`${apiUrl}/api/chat/completions`, {
21+
method: 'POST',
22+
body: JSON.stringify({ ...requestBody, stream: true }),
23+
signal: abortSignal,
24+
})
25+
return sseStreamToGenerator(response, { signal: abortSignal })
26+
},
27+
initialMessages: [
28+
{
29+
content: '你好!我是AI助手,有什么可以帮助你的吗?',
30+
role: 'assistant',
31+
},
32+
],
33+
})
34+
}

0 commit comments

Comments
 (0)