Chat.vue 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. <!--
  2. - Copyright (c) 2024 LangChat. TyCoding All Rights Reserved.
  3. -
  4. - Licensed under the GNU Affero General Public License, Version 3 (the "License");
  5. - you may not use this file except in compliance with the License.
  6. - You may obtain a copy of the License at
  7. -
  8. - https://www.gnu.org/licenses/agpl-3.0.html
  9. -
  10. - Unless required by applicable law or agreed to in writing, software
  11. - distributed under the License is distributed on an "AS IS" BASIS,
  12. - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. - See the License for the specific language governing permissions and
  14. - limitations under the License.
  15. -->
  16. <script lang="ts" setup>
  17. import { ref } from 'vue';
  18. import { docsChat } from '@/api/chat';
  19. import { v4 as uuid } from 'uuid';
  20. import MarkdownIt from 'markdown-it';
  21. import hljs from 'highlight.js';
  22. import mila from 'markdown-it-link-attributes';
  23. import mdKatex from '@traptitech/markdown-it-katex';
  24. import Message from './Message.vue';
  25. import { SvgIcon } from '@/components/common';
  26. import { useDocStore } from '@/views/modules/doc/store';
  27. import { t } from '@/locales';
  28. import Header from '@/views/modules/chat/Header.vue';
  29. import { useBasicLayout } from '@/hooks/useBasicLayout';
  30. import { useChatStore } from '@/views/modules/chat/store/useChatStore';
  31. import ModelProvider from '@/views/modules/common/ModelProvider.vue';
  32. const { isMobile } = useBasicLayout();
  33. const emits = defineEmits(['focus-active']);
  34. const messageRef = ref();
  35. const message = ref('');
  36. const loading = ref(false);
  37. const docStore = useDocStore();
  38. const chatStore = useChatStore();
  39. function init() {
  40. messages.value = docStore.messages as any;
  41. }
  42. function handleFocus() {
  43. emits('focus-active');
  44. }
  45. const mdi = new MarkdownIt({
  46. html: false,
  47. linkify: true,
  48. highlight(code, language) {
  49. const validLang = !!(language && hljs.getLanguage(language));
  50. if (validLang) {
  51. const lang = language ?? '';
  52. return highlightBlock(hljs.highlight(code, { language: lang }).value, lang);
  53. }
  54. return highlightBlock(hljs.highlightAuto(code).value, '');
  55. },
  56. });
  57. mdi.use(mila, { attrs: { target: '_blank', rel: 'noopener' } });
  58. mdi.use(mdKatex, { blockClass: 'katexmath-block rounded-md p-[10px]', errorColor: ' #cc0000' });
  59. function highlightBlock(str: string, lang?: string) {
  60. return `<pre class="code-block-wrapper"><div class="code-block-header"><span class="code-block-header__lang">${lang}</span><span class="code-block-header__copy">复制</span></div><code class="hljs code-block-body ${lang}">${str}</code></pre>`;
  61. }
  62. const messages = ref<
  63. {
  64. id: any;
  65. role: 'user' | 'assistant';
  66. error: boolean;
  67. message: string;
  68. createTime?: any;
  69. tokens?: number;
  70. }[]
  71. >([]);
  72. async function handleSubmit() {
  73. if (docStore.file.id === undefined) {
  74. window.$message?.error('请先选择文档');
  75. return;
  76. }
  77. loading.value = true;
  78. messageRef.value.scrollToBottom();
  79. try {
  80. let id = uuid();
  81. const userChat = {
  82. id: uuid(),
  83. error: false,
  84. role: 'user',
  85. message: message.value,
  86. };
  87. docStore.addMessage(userChat);
  88. messages.value.push(userChat, {
  89. id: id,
  90. error: false,
  91. role: 'assistant',
  92. message: '',
  93. tokens: 0,
  94. createTime: 0,
  95. });
  96. const items = messages.value.filter((i) => i.id == id);
  97. await docsChat(
  98. docStore.file?.id,
  99. {
  100. conversationId: docStore.file?.id,
  101. message: message.value,
  102. modelId: chatStore.modelId,
  103. modelName: chatStore.modelName,
  104. modelProvider: chatStore.modelProvider,
  105. },
  106. ({ event }) => {
  107. const list = event.target.responseText.split('\n\n');
  108. let text = '';
  109. list.forEach((i: any) => {
  110. if (!i.startsWith('data:{')) {
  111. return;
  112. }
  113. const { usedToken, done, message, time } = JSON.parse(i.substring(5, i.length));
  114. if (done) {
  115. items[0].tokens = usedToken;
  116. items[0].createTime = time;
  117. docStore.addMessage(items[0]);
  118. } else {
  119. text += message;
  120. items[0].message = mdi.render(text);
  121. messageRef.value.scrollToBottom();
  122. }
  123. });
  124. }
  125. )
  126. .catch((err: any) => {
  127. if (err.message !== undefined) {
  128. items[0].error = true;
  129. items[0].message = err.message;
  130. }
  131. loading.value = false;
  132. })
  133. .finally(() => {
  134. message.value = '';
  135. loading.value = false;
  136. });
  137. } finally {
  138. loading.value = false;
  139. messageRef.value.scrollToBottom();
  140. }
  141. }
  142. function handleEnter(event: KeyboardEvent) {
  143. if (event.key === 'Enter' && event.ctrlKey) {
  144. } else if (event.key === 'Enter') {
  145. event.preventDefault();
  146. handleSubmit();
  147. }
  148. }
  149. defineExpose({ init });
  150. </script>
  151. <template>
  152. <div class="container relative h-full card-shadow rounded-xl mb-2 flex flex-col">
  153. <header
  154. :class="isMobile ? 'px-1' : 'px-2'"
  155. class="sticky z-30 border-b dark:border-neutral-800 border-l-0 bg-white/80 dark:bg-black/20 backdrop-blur"
  156. >
  157. <div
  158. class="relative flex items-center justify-between min-w-0 overflow-hidden h-12 ml-2 mr-2 gap-2"
  159. >
  160. <ModelProvider />
  161. </div>
  162. </header>
  163. <Message ref="messageRef" :messages="messages" />
  164. <div
  165. v-if="docStore.file.id"
  166. :class="isMobile ? 'mb-2' : 'mb-6'"
  167. class="pt-2 left-0 w-full z-10"
  168. >
  169. <div class="px-8 flex justify-center items-center space-x-2 w-full">
  170. <n-input
  171. v-model:value="message"
  172. :autosize="{ minRows: 1, maxRows: 3 }"
  173. :disabled="loading"
  174. :placeholder="t('chat.placeholder')"
  175. class="!rounded-full px-2 py-1"
  176. type="textarea"
  177. @focus="handleFocus"
  178. @keypress="handleEnter"
  179. >
  180. <template #suffix>
  181. <n-button :loading="loading" text @click="handleSubmit">
  182. <template #icon>
  183. <SvgIcon icon="mdi:sparkles-outline" />
  184. </template>
  185. </n-button>
  186. </template>
  187. </n-input>
  188. </div>
  189. </div>
  190. </div>
  191. </template>
  192. <style lang="less" scoped>
  193. ::v-deep(.markdown-body) {
  194. background-color: transparent !important;
  195. font-size: inherit;
  196. }
  197. </style>