Chat.vue 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. <script lang="ts" setup>
  2. import { onMounted, ref, watch } from 'vue';
  3. import { chat } from '@/api/docs';
  4. import { v4 as uuid } from 'uuid';
  5. import { useRouter } from 'vue-router';
  6. import MarkdownIt from 'markdown-it';
  7. import hljs from 'highlight.js';
  8. import mila from 'markdown-it-link-attributes';
  9. import mdKatex from '@traptitech/markdown-it-katex';
  10. import Message from './Message.vue';
  11. import { SvgIcon } from '@/components/common';
  12. import { useDocStore } from '@/views/modules/doc/store';
  13. const emits = defineEmits(['focus-active']);
  14. const messageRef = ref();
  15. const router = useRouter();
  16. const message = ref('');
  17. const loading = ref(false);
  18. const docStore = useDocStore();
  19. function init() {
  20. messages.value = docStore.curMessage;
  21. }
  22. function handleFocus() {
  23. emits('focus-active');
  24. }
  25. const mdi = new MarkdownIt({
  26. html: false,
  27. linkify: true,
  28. highlight(code, language) {
  29. const validLang = !!(language && hljs.getLanguage(language));
  30. if (validLang) {
  31. const lang = language ?? '';
  32. return highlightBlock(hljs.highlight(code, { language: lang }).value, lang);
  33. }
  34. return highlightBlock(hljs.highlightAuto(code).value, '');
  35. },
  36. });
  37. mdi.use(mila, { attrs: { target: '_blank', rel: 'noopener' } });
  38. mdi.use(mdKatex, { blockClass: 'katexmath-block rounded-md p-[10px]', errorColor: ' #cc0000' });
  39. function highlightBlock(str: string, lang?: string) {
  40. 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>`;
  41. }
  42. const messages = ref<
  43. {
  44. id: string;
  45. inversion: boolean;
  46. error: boolean;
  47. message: string;
  48. time?: number;
  49. usedToken?: number;
  50. }[]
  51. >([]);
  52. async function handleSubmit() {
  53. if (docStore.file.id === undefined) {
  54. window.$message?.error('请先选择文档');
  55. return;
  56. }
  57. loading.value = true;
  58. messageRef.value.scrollToBottom();
  59. try {
  60. let id = uuid();
  61. const userChat = {
  62. id: uuid(),
  63. error: false,
  64. inversion: false,
  65. message: message.value,
  66. };
  67. docStore.addMessage(userChat);
  68. messages.value.push(userChat, {
  69. id: id,
  70. error: false,
  71. inversion: true,
  72. message: '',
  73. usedToken: 0,
  74. time: 0,
  75. });
  76. const items = messages.value.filter((i) => i.id == id);
  77. await chat(
  78. {
  79. id: docStore.file?.id,
  80. message: message.value,
  81. },
  82. ({ event }) => {
  83. const list = event.target.responseText.split('\n\n');
  84. let text = '';
  85. list.forEach((i: any) => {
  86. if (!i.startsWith('data:{')) {
  87. return;
  88. }
  89. const { usedToken, done, message, time } = JSON.parse(i.substring(5, i.length));
  90. if (done) {
  91. items[0].usedToken = usedToken;
  92. items[0].time = time;
  93. docStore.addMessage(items[0]);
  94. } else {
  95. text += message;
  96. items[0].message = mdi.render(text);
  97. messageRef.value.scrollToBottom();
  98. }
  99. });
  100. }
  101. )
  102. .catch((err: any) => {
  103. if (err.message !== undefined) {
  104. items[0].error = true;
  105. items[0].message = err.message;
  106. }
  107. loading.value = false;
  108. })
  109. .finally(() => {
  110. message.value = '';
  111. loading.value = false;
  112. });
  113. } finally {
  114. loading.value = false;
  115. messageRef.value.scrollToBottom();
  116. }
  117. }
  118. function handleEnter(event: KeyboardEvent) {
  119. if (event.key === 'Enter' && event.ctrlKey) {
  120. } else if (event.key === 'Enter') {
  121. event.preventDefault();
  122. handleSubmit();
  123. }
  124. }
  125. defineExpose({ init });
  126. </script>
  127. <template>
  128. <div class="container relative h-full card-shadow rounded-xl mb-2">
  129. <Message ref="messageRef" :messages="messages" />
  130. <div class="bottom absolute bottom-2 pt-5 left-0 w-full h-[60px] z-10">
  131. <div class="px-8 flex justify-center items-center space-x-2 w-full">
  132. <n-input
  133. v-model:value="message"
  134. :autosize="{ minRows: 1, maxRows: 5 }"
  135. :disabled="loading"
  136. class="w-full ]text-xs rounded-md"
  137. type="textarea"
  138. @focus="handleFocus"
  139. @keypress="handleEnter"
  140. >
  141. <template #suffix>
  142. <n-button :loading="loading" size="small" text @click="handleSubmit">
  143. <template #icon>
  144. <n-icon color="#18a058">
  145. <SvgIcon icon="mingcute:send-line" />
  146. </n-icon>
  147. </template>
  148. </n-button>
  149. </template>
  150. </n-input>
  151. </div>
  152. </div>
  153. </div>
  154. </template>
  155. <style scoped lang="less">
  156. ::v-deep(.markdown-body) {
  157. background-color: transparent !important;
  158. font-size: inherit;
  159. }
  160. </style>