Skip to content

Commit adbe2b1

Browse files
authored
Clearify api sse (#40)
* add useChatBot * polish code
1 parent b5c3054 commit adbe2b1

File tree

14 files changed

+161
-138
lines changed

14 files changed

+161
-138
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
"@types/unist": "^2.0.6",
5858
"pinia": "^2.1.4",
5959
"rehype-highlight": "^6.0.0",
60+
"rehype-sanitize": "^5.0.1",
6061
"rehype-stringify": "^9.0.3",
6162
"remark-html": "^15.0.2",
6263
"style-to-object": "^0.4.1",

pnpm-lock.yaml

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/app.vue

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ body,
2020
height: 100vh;
2121
margin: 0;
2222
padding: 0;
23-
background: #f2f2f2;
2423
color: black;
2524
display: flex;
2625
justify-content: center;

src/components/MarkdownPreview.vue

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import remarkSqueezeParagraphs from "remark-squeeze-paragraphs"
88
import remarkRehype from "remark-rehype"
99
import rehypeHighlight from "rehype-highlight"
1010
import rehypeStringify from "rehype-stringify"
11+
import rehypeSanitize from "rehype-sanitize"
1112
1213
const processor = unified()
1314
.use(remarkParse)
@@ -16,25 +17,31 @@ const processor = unified()
1617
.use(remarkSqueezeParagraphs)
1718
.use(remarkGfm)
1819
.use(rehypeHighlight)
20+
.use(rehypeSanitize)
1921
.use(rehypeStringify)
2022
2123
const props = defineProps({
2224
md: {
2325
type: String,
2426
required: true,
2527
},
26-
cid: {
27-
type: String,
28-
default: "TODO-uid",
29-
},
3028
})
3129
3230
const parsedMarkdown = ref(props.md)
3331
32+
const throttleParse = useThrottleFn(
33+
(s) => {
34+
const a = processor.processSync(s)
35+
parsedMarkdown.value = String(a)
36+
},
37+
80,
38+
true,
39+
true
40+
)
41+
3442
watchEffect(async () => {
3543
try {
36-
const a = processor.processSync(props.md || "...")
37-
parsedMarkdown.value = String(a)
44+
await throttleParse(props.md || "...")
3845
} catch (err) {
3946
parsedMarkdown.value = ""
4047
}

src/components/VChatDetailHeader.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ const chatSession = useRoutedChatSession()
66
const { t } = useTrans()
77
</script>
88
<template>
9-
<div class="relative flex items-center justify-between border border-gray-200 px-5 py-3.5">
9+
<div class="relative flex items-center justify-between border-b border-gray-200 px-5 py-3.5">
1010
<div class="truncate">
1111
<div class="cursor-pointer truncate text-[1.25rem] font-bold">{{ chatSession.session.topic }}</div>
1212
<div class="mt-1 text-[0.88rem]">

src/components/VChatList.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script setup lang="ts">
2-
import { useRoutedChatSession, useSidebarChatSessions } from "~/composable/chat"
2+
import { useSidebarChatSessions } from "~/composable/chat"
33
44
const router = useRouter()
55

src/components/VComposeView.vue

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,11 @@ const composeNewMessage = () => {
3535
}
3636
</script>
3737
<template>
38-
<div class="relative flex-col rounded-xl border p-3">
38+
<div class="relative flex-col rounded-xl border-t p-3">
3939
<div class="flex flex-wrap">
40-
<div class="mb-3 mr-1 flex cursor-pointer items-center rounded-2xl border px-3 py-1 text-[0.75rem]">
40+
<div
41+
class="mb-3 mr-1 flex cursor-pointer items-center rounded-2xl border px-3 py-1 text-[0.75rem] hover:bg-gray-200"
42+
>
4143
<Icon name="bx:bot" size="1.4em" />
4244
</div>
4345
</div>

src/composable/chat.ts

Lines changed: 32 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,9 @@ import { defineStore } from "pinia"
22
import { ComputedRef, ref } from "vue"
33
import { useSettingStore } from "~/composable/settings"
44
import { DEFAULT_INPUT_TEMPLATE, StoreKey } from "~/constants"
5-
import { fetchStream } from "~/constants/api"
5+
import useChatBot from "~/composable/useChatBot"
66
import { TChatDirection, TChatSession, TMask } from "~/constants/typing"
77
import { getUtcNow } from "~/utils/date"
8-
import { debounce } from "~/utils/debounce"
98
import { loadFromLocalStorage, saveSessionToLocalStorage, saveToLocalStorage } from "./storage"
109

1110
function makeEmptySession(s: number, simple: TSimple, mask?: TMask): TChatSession {
@@ -84,12 +83,17 @@ export const useSidebarChatSessions = defineStore(StoreKey.Chat, () => {
8483
sessionGid.value = loaded.sessionGid
8584
}
8685

87-
const saveAll = debounce(() => {
88-
saveToLocalStorage(StoreKey.ChatSession, {
89-
sessions: sessions.value,
90-
sessionGid: sessionGid.value,
91-
})
92-
}, 1000)
86+
const saveAll = useThrottleFn(
87+
() => {
88+
saveToLocalStorage(StoreKey.ChatSession, {
89+
sessions: sessions.value,
90+
sessionGid: sessionGid.value,
91+
})
92+
},
93+
1000,
94+
true,
95+
true
96+
)
9397

9498
const clearSessions = () => {
9599
sessions.value = []
@@ -194,16 +198,17 @@ export const useChatSession = (sid: string): TUseChatSession => {
194198
})
195199
session.messages.push(...res.messages)
196200

197-
const debounceSave = debounce(
201+
const throttledSave = useThrottleFn(
198202
() => {
199-
console.log("toRaw message before saving", toRaw(session.messages))
200203
saveSessionToLocalStorage(toRaw(session))
201204
},
202205
1000,
203-
false
206+
true,
207+
true
204208
)
205209

206210
const onNewMessage = (message: string) => {
211+
const { chat, message: currentMessage } = useChatBot()
207212
// latest 4 messages
208213
const lastMessages = session.messages.slice(-4).map((message) => {
209214
return {
@@ -259,22 +264,23 @@ export const useChatSession = (sid: string): TUseChatSession => {
259264
}, 500)
260265
chatStore.refreshSession(session)
261266

262-
fetchStream(
263-
payload,
264-
(receivedData: string) => {
267+
chat(
268+
payload as TOpenApiChatCompletionMessage,
269+
() => {},
270+
(message) => {
265271
clearInterval(loadingInterval) // 清除定时器
266-
nMessage.content = receivedData
267-
debounceSave()
272+
nMessage.content = message.content
273+
throttledSave()
268274
},
269-
settings.serverUrl,
270-
settings.apiKey
271-
).catch((error) => {
272-
clearInterval(loadingInterval) // 清除定时器
273-
// 在聊天中展示 error message
274-
nMessage.isError = true
275-
nMessage.content = `Error occurred: ${error}`
276-
console.error("Error occurred:", error)
277-
})
275+
(message) => {
276+
clearInterval(loadingInterval) // 清除定时器
277+
// 在聊天中展示 error message
278+
nMessage.isError = true
279+
nMessage.content = `Error occurred: ${message.errorMessage}`
280+
console.error("Error occurred:", message.errorMessage)
281+
},
282+
() => {}
283+
).then((r) => {})
278284
}
279285

280286
const onUserInput = async (content: string) => {
@@ -302,7 +308,7 @@ export const useChatSession = (sid: string): TUseChatSession => {
302308
// chatStore.saveAll()
303309
}
304310

305-
const result = <TUseChatSession> {
311+
const result = <TUseChatSession>{
306312
session,
307313
onUserInput,
308314
rename,

src/composable/useChatBot.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { fetchEventSource } from "@fortaine/fetch-event-source"
2+
import { useSettingStore } from "~/composable/settings"
3+
4+
type TChatCompletionMessage = {
5+
content: string
6+
status: "pending" | "connected" | "client error" | "messaging" | "error" | "done"
7+
errorMessage: string
8+
}
9+
10+
export default function useChatBot() {
11+
const { settings } = useSettingStore()
12+
const message = reactive<TChatCompletionMessage>({
13+
content: "",
14+
status: "pending",
15+
errorMessage: "",
16+
})
17+
18+
async function chat(
19+
payload: TOpenApiChatCompletion,
20+
onStart: (message: TChatCompletionMessage) => void,
21+
onMessage: (message: TChatCompletionMessage) => void,
22+
onError: (message: TChatCompletionMessage) => void,
23+
onDone: (message: TChatCompletionMessage) => void
24+
) {
25+
const BASE_URL = settings.serverUrl
26+
const TOKEN = settings.apiKey
27+
const API_URL = `${BASE_URL}/v1/chat/completions`
28+
29+
await fetchEventSource(API_URL, {
30+
method: "POST",
31+
headers: {
32+
Accept: "text/event-stream",
33+
Authorization: `Bearer ${TOKEN}`,
34+
},
35+
body: JSON.stringify({
36+
...payload,
37+
}),
38+
async onopen(res) {
39+
if (res.ok && res.status === 200) {
40+
console.log("Connection made ", res)
41+
message.status = "connected"
42+
onStart(message)
43+
} else if (res.status >= 400 && res.status < 500 && res.status !== 429) {
44+
message.status = "client error"
45+
message.errorMessage = `Client-side error ${res.status || res.statusText} ${res.type}`
46+
onError(message)
47+
console.log("Client-side error ", res)
48+
}
49+
},
50+
onmessage(ev) {
51+
if ("[DONE]" == ev.data) {
52+
message.status = "done"
53+
onDone(message)
54+
return
55+
}
56+
if (!ev.data) {
57+
return
58+
}
59+
try {
60+
const parsedData = JSON.parse(ev.data)
61+
let content = parsedData.choices[0]?.delta?.content
62+
if (content) {
63+
message.content += content
64+
onMessage(message)
65+
}
66+
} catch (e) {
67+
message.content = String(s)
68+
onError(message)
69+
}
70+
},
71+
onerror(ev) {
72+
message.status = "error"
73+
message.errorMessage = `error ${ev.status || ev.statusText || ""} ${ev.type || ""}`
74+
onError(message)
75+
},
76+
})
77+
}
78+
79+
return {
80+
message,
81+
chat,
82+
}
83+
}

src/constants/api.ts

Lines changed: 0 additions & 52 deletions
This file was deleted.

0 commit comments

Comments
 (0)