main
vue 158 lines 3.78 KB
Raw
1 <template>
2 <div class="flex h-full flex-col overflow-hidden">
3 <n-scrollbar ref="scrollbar" class="grow">
4 <div v-if="messages.length" class="flex flex-col gap-6 pb-20">
5 <TalonChatBubble v-for="msg of messages" :key="msg.id" :msg class="animate-fade" />
6
7 <div v-if="streaming" class="group animate-fade flex flex-col gap-0.5">
8 <div class="text-secondary inline-flex items-center gap-1 text-sm font-semibold">
9 <span>Talon:</span>
10 </div>
11 <div v-if="streamBuffer" class="**:text-default **:text-sm [&_*:last-child]:mb-0!">
12 <Markdown :source="streamBuffer" class="overflow-hidden" />
13 </div>
14 <div v-else class="animate-fade flex items-center gap-2">
15 <Icon name="svg-spinners:pulse-rings-3" :size="20" />
16 <span class="text-tertiary text-sm">Thinking...</span>
17 </div>
18 </div>
19 </div>
20 <div v-else class="animate-fade flex flex-col items-center justify-center py-12">
21 <TalonChatExample @example="useExample" />
22 </div>
23 </n-scrollbar>
24
25 <TalonChatQuery
26 v-model:input="input"
27 :loading="streaming"
28 @message="sendMessage"
29 @stop="stopStream()"
30 @clear-chat="clearChat()"
31 />
32 </div>
33 </template>
34
35 <script setup lang="ts">
36 import type { ScrollbarInst } from "naive-ui"
37 import type { Message } from "./TalonChatQuery.vue"
38 import { useStorage } from "@vueuse/core"
39 import { NScrollbar, useMessage } from "naive-ui"
40 import { nanoid } from "nanoid"
41 import { nextTick, ref } from "vue"
42 import Api from "@/api"
43 import Icon from "@/components/common/Icon.vue"
44 import Markdown from "@/components/common/Markdown.vue"
45 import { secureLocalStorage } from "@/utils/secure-storage"
46 import TalonChatBubble from "./TalonChatBubble.vue"
47 import TalonChatExample from "./TalonChatExample.vue"
48 import TalonChatQuery from "./TalonChatQuery.vue"
49
50 interface TalonMessage {
51 id: string
52 datetime: Date
53 body: string
54 sender: "user" | "server"
55 }
56
57 const message = useMessage()
58
59 const messages = useStorage<TalonMessage[]>("talon-chat-messages", [], secureLocalStorage({ session: true }))
60 const input = ref("")
61 const streaming = ref(false)
62 const streamBuffer = ref("")
63 const scrollbar = ref<ScrollbarInst | null>(null)
64
65 let abortController: AbortController | null = null
66
67 function useExample(example: string) {
68 input.value = example
69 sendMessage({ input: example })
70 }
71
72 function clearChat() {
73 messages.value = []
74 }
75
76 function scrollChat() {
77 nextTick(() => {
78 scrollbar.value?.scrollTo({ top: 99999999999999, behavior: "smooth" })
79 })
80 }
81
82 async function sendMessage(payload: Message) {
83 const text = payload.input.trim()
84 if (!text || streaming.value) return
85
86 messages.value.push({
87 id: nanoid(),
88 datetime: new Date(),
89 body: text,
90 sender: "user"
91 })
92
93 input.value = ""
94 streaming.value = true
95 streamBuffer.value = ""
96 scrollChat()
97
98 abortController = new AbortController()
99
100 try {
101 await Api.talon.streamMessage(
102 text,
103 (chunk: string) => {
104 streamBuffer.value += chunk
105 scrollChat()
106 },
107 () => {
108 if (streamBuffer.value) {
109 messages.value.push({
110 id: nanoid(),
111 datetime: new Date(),
112 body: streamBuffer.value,
113 sender: "server"
114 })
115 }
116 streaming.value = false
117 streamBuffer.value = ""
118 scrollChat()
119 },
120 (err: string) => {
121 if (err !== "AbortError") {
122 message.error("An error occurred. Please try again.")
123 }
124 streaming.value = false
125 streamBuffer.value = ""
126 scrollChat()
127 },
128 abortController.signal
129 )
130 } catch {
131 streaming.value = false
132 streamBuffer.value = ""
133 }
134 }
135
136 function stopStream() {
137 abortController?.abort()
138
139 if (streamBuffer.value) {
140 messages.value.push({
141 id: nanoid(),
142 datetime: new Date(),
143 body: streamBuffer.value,
144 sender: "server"
145 })
146 }
147
148 streaming.value = false
149 streamBuffer.value = ""
150 scrollChat()
151 }
152
153 defineExpose({
154 clearHistory() {
155 messages.value = []
156 }
157 })
158 </script>