Selaa lähdekoodia

add chat page

tycoding 1 vuosi sitten
vanhempi
commit
bbf1e5b6bd

+ 4 - 0
langchat-ui/package.json

@@ -29,6 +29,7 @@
   },
   "dependencies": {
     "@iconify/vue": "^4.1.1",
+    "@traptitech/markdown-it-katex": "^3.6.0",
     "@types/uuid": "^9.0.2",
     "@vicons/antd": "^0.12.0",
     "@vicons/ionicons5": "^0.12.0",
@@ -38,7 +39,10 @@
     "date-fns": "^2.30.0",
     "echarts": "^5.4.3",
     "element-resize-detector": "^1.2.4",
+    "highlight.js": "^11.9.0",
     "lodash-es": "^4.17.21",
+    "markdown-it": "^14.1.0",
+    "markdown-it-link-attributes": "^4.0.1",
     "mitt": "^3.0.1",
     "mockjs": "^1.1.0",
     "naive-ui": "^2.36.0",

+ 68 - 5
langchat-ui/pnpm-lock.yaml

@@ -8,6 +8,9 @@ dependencies:
   '@iconify/vue':
     specifier: ^4.1.1
     version: 4.1.1(vue@3.3.4)
+  '@traptitech/markdown-it-katex':
+    specifier: ^3.6.0
+    version: 3.6.0
   '@types/uuid':
     specifier: ^9.0.2
     version: 9.0.8
@@ -35,9 +38,18 @@ dependencies:
   element-resize-detector:
     specifier: ^1.2.4
     version: 1.2.4
+  highlight.js:
+    specifier: ^11.9.0
+    version: 11.9.0
   lodash-es:
     specifier: ^4.17.21
     version: 4.17.21
+  markdown-it:
+    specifier: ^14.1.0
+    version: 14.1.0
+  markdown-it-link-attributes:
+    specifier: ^4.0.1
+    version: 4.0.1
   mitt:
     specifier: ^3.0.1
     version: 3.0.1
@@ -1487,6 +1499,12 @@ packages:
       '@sinonjs/commons': 3.0.0
     dev: true
 
+  /@traptitech/markdown-it-katex@3.6.0:
+    resolution: {integrity: sha512-CnJzTWxsgLGXFdSrWRaGz7GZ1kUUi8g3E9HzJmeveX1YwVJavrKYqysktfHZQsujdnRqV5O7g8FPKEA/aeTkOQ==}
+    dependencies:
+      katex: 0.16.10
+    dev: false
+
   /@types/babel__core@7.20.1:
     resolution: {integrity: sha512-aACu/U/omhdk15O4Nfb+fHgH/z3QsfQzpnvRZhYhThms83ZnAOZz7zZAWO7mn2yyNQaA4xTO8GLK3uqFU4bYYw==}
     dependencies:
@@ -2182,7 +2200,6 @@ packages:
 
   /argparse@2.0.1:
     resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
-    dev: true
 
   /array-differ@3.0.0:
     resolution: {integrity: sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg==}
@@ -2701,7 +2718,6 @@ packages:
   /commander@8.3.0:
     resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==}
     engines: {node: '>= 12'}
-    dev: true
 
   /commitizen@4.3.0(@types/node@18.17.4)(typescript@4.9.5):
     resolution: {integrity: sha512-H0iNtClNEhT0fotHvGV3E9tDejDeS04sN1veIebsKYGMuGscFaswRoYJKmT3eW85eIJAs0F28bG2+a/9wCOfPw==}
@@ -3207,6 +3223,11 @@ packages:
     resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==}
     dev: true
 
+  /entities@4.5.0:
+    resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
+    engines: {node: '>=0.12'}
+    dev: false
+
   /errno@0.1.8:
     resolution: {integrity: sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==}
     hasBin: true
@@ -4456,8 +4477,8 @@ packages:
       tslib: 2.6.1
     dev: true
 
-  /highlight.js@11.8.0:
-    resolution: {integrity: sha512-MedQhoqVdr0U6SSnWPzfiadUcDHfN/Wzq25AkXiQv9oiOO/sG0S7XkvpFIqWBl9Yq1UYyYOOVORs5UW2XlPyzg==}
+  /highlight.js@11.9.0:
+    resolution: {integrity: sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw==}
     engines: {node: '>=12.0.0'}
     dev: false
 
@@ -5283,6 +5304,13 @@ packages:
     engines: {'0': node >= 0.2.0}
     dev: true
 
+  /katex@0.16.10:
+    resolution: {integrity: sha512-ZiqaC04tp2O5utMsl2TEZTXxa6WSC4yo0fv5ML++D3QZv/vx2Mct0mTlRx3O+uUkjfuAgOkzsCmq5MiUEsDDdA==}
+    hasBin: true
+    dependencies:
+      commander: 8.3.0
+    dev: false
+
   /kind-of@6.0.3:
     resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==}
     engines: {node: '>=0.10.0'}
@@ -5350,6 +5378,12 @@ packages:
     resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
     dev: true
 
+  /linkify-it@5.0.0:
+    resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==}
+    dependencies:
+      uc.micro: 2.1.0
+    dev: false
+
   /lint-staged@13.2.3:
     resolution: {integrity: sha512-zVVEXLuQIhr1Y7R7YAWx4TZLdvuzk7DnmrsTNL0fax6Z3jrpFcas+vKbzxhhvp6TA55m1SQuWkpzI1qbfDZbAg==}
     engines: {node: ^14.13.1 || >=16.0.0}
@@ -5572,10 +5606,30 @@ packages:
     engines: {node: '>=8'}
     dev: true
 
+  /markdown-it-link-attributes@4.0.1:
+    resolution: {integrity: sha512-pg5OK0jPLg62H4k7M9mRJLT61gUp9nvG0XveKYHMOOluASo9OEF13WlXrpAp2aj35LbedAy3QOCgQCw0tkLKAQ==}
+    dev: false
+
+  /markdown-it@14.1.0:
+    resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==}
+    hasBin: true
+    dependencies:
+      argparse: 2.0.1
+      entities: 4.5.0
+      linkify-it: 5.0.0
+      mdurl: 2.0.0
+      punycode.js: 2.3.1
+      uc.micro: 2.1.0
+    dev: false
+
   /mathml-tag-names@2.1.3:
     resolution: {integrity: sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==}
     dev: true
 
+  /mdurl@2.0.0:
+    resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==}
+    dev: false
+
   /meow@8.1.2:
     resolution: {integrity: sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q==}
     engines: {node: '>=10'}
@@ -5763,7 +5817,7 @@ packages:
       date-fns: 2.30.0
       date-fns-tz: 2.0.1(date-fns@2.30.0)
       evtd: 0.2.4
-      highlight.js: 11.8.0
+      highlight.js: 11.9.0
       lodash: 4.17.21
       lodash-es: 4.17.21
       seemly: 0.3.8
@@ -6308,6 +6362,11 @@ packages:
       once: 1.4.0
     dev: true
 
+  /punycode.js@2.3.1:
+    resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==}
+    engines: {node: '>=6'}
+    dev: false
+
   /punycode@2.3.0:
     resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==}
     engines: {node: '>=6'}
@@ -7188,6 +7247,10 @@ packages:
     engines: {node: '>=4.2.0'}
     hasBin: true
 
+  /uc.micro@2.1.0:
+    resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==}
+    dev: false
+
   /universalify@0.1.2:
     resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==}
     engines: {node: '>= 4.0.0'}

+ 181 - 0
langchat-ui/src/views/aigc/chat/components/Chat.vue

@@ -0,0 +1,181 @@
+<script setup lang="ts">
+  import Message from './message/Message.vue';
+  import { useProjectSetting } from '@/hooks/setting/useProjectSetting';
+  import { computed, ref } from 'vue';
+  import { useChatStore } from './store/useChatStore';
+  import { useScroll } from './store/useScroll';
+  import { useDialog } from 'naive-ui';
+  import SvgIcon from '@/components/SvgIcon/index.vue';
+
+  const dialog = useDialog();
+  const chatStore = useChatStore();
+  const { scrollRef, contentRef, scrollToBottom } = useScroll();
+  const { isMobile } = useProjectSetting();
+  const loading = ref<boolean>(false);
+  const message = ref('');
+  let controller = new AbortController();
+
+  const footerClass = computed(() => {
+    let classes = ['p-4'];
+    if (isMobile.value) {
+      classes = ['sticky', 'left-0', 'bottom-0', 'right-0', 'p-2', 'pr-3', 'overflow-hidden'];
+    }
+    return classes;
+  });
+  const chatIsLoading = computed(() => {
+    return chatStore.chatIsLoading;
+  });
+  const buttonDisabled = computed(() => {
+    return loading.value;
+  });
+
+  // 初始化加载数据
+  chatStore.loadData();
+  const dataSources = computed(() => {
+    // 获取当前聊天窗口的数据
+    scrollToBottom();
+    return chatStore.messages;
+  });
+
+  function handleEnter(event: KeyboardEvent) {
+    if (!isMobile.value) {
+      if (event.key === 'Enter' && !event.shiftKey) {
+        event.preventDefault();
+        handleSubmit();
+      }
+    } else {
+      if (event.key === 'Enter' && event.ctrlKey) {
+        event.preventDefault();
+        handleSubmit();
+      }
+    }
+  }
+  async function handleSubmit() {}
+
+  function handleStop() {
+    if (loading.value) {
+      controller.abort();
+      loading.value = false;
+    }
+  }
+
+  // 删除
+  function handleDelete(item: any) {
+    if (loading.value) {
+      return;
+    }
+
+    dialog.warning({
+      title: '删除消息',
+      content: '确认删除消息',
+      positiveText: '是',
+      negativeText: '否',
+      onPositiveClick: () => {
+        chatStore.delMessage(item);
+      },
+    });
+  }
+
+  // 清除
+  function handleClear() {
+    if (loading.value) {
+      return;
+    }
+    dialog.warning({
+      title: '清除聊天',
+      content: '确认清除聊天',
+      positiveText: '是',
+      negativeText: '否',
+      onPositiveClick: async () => {
+        console.log('清除聊天');
+      },
+    });
+  }
+</script>
+
+<template>
+  <div class="flex flex-col w-full h-full">
+    <!-- 聊天记录窗口 -->
+    <main class="flex-1 overflow-hidden">
+      <div ref="contentRef" class="h-full overflow-hidden overflow-y-auto">
+        <div v-if="chatIsLoading" class="w-full h-full flex items-center justify-center">
+          <n-spin :show="chatIsLoading" size="large" />
+        </div>
+        <div
+          v-else
+          ref="scrollRef"
+          class="w-full max-w-screen-xl m-auto"
+          :class="[isMobile ? 'p-2' : 'p-5']"
+        >
+          <Message
+            v-for="(item, index) of dataSources"
+            :key="index"
+            :date-time="item.createTime"
+            :text="item.message"
+            :inversion="item.role !== 'assistant'"
+            :error="item.isError"
+            :loading="loading"
+            @delete="handleDelete(item)"
+          />
+          <div class="sticky bottom-0 left-0 flex justify-center">
+            <NButton v-if="loading" type="warning" @click="handleStop">
+              <template #icon>
+                <SvgIcon icon="ri:stop-circle-line" />
+              </template>
+              Stop Responding
+            </NButton>
+          </div>
+        </div>
+      </div>
+    </main>
+
+    <!-- 底部 -->
+    <footer :class="footerClass">
+      <div class="w-full max-w-screen-xl m-auto pl-8 pr-8 pb-0">
+        <div class="flex items-center justify-between space-x-2">
+          <NInput
+            ref="inputRef"
+            v-model:value="message"
+            type="textarea"
+            :autosize="{ minRows: 3, maxRows: isMobile ? 4 : 8 }"
+            @keypress="handleEnter"
+            class="custom-input"
+          >
+            <template #suffix>
+              <div class="flex items-center gap-2">
+                <NButton
+                  type="default"
+                  size="small"
+                  :disabled="buttonDisabled"
+                  @click="handleSubmit"
+                >
+                  <SvgIcon icon="ph:file-plus-duotone" />
+                </NButton>
+                <NButton
+                  type="primary"
+                  size="small"
+                  :disabled="buttonDisabled"
+                  @click="handleSubmit"
+                >
+                  <SvgIcon icon="ri:send-plane-fill" />
+                </NButton>
+              </div>
+            </template>
+          </NInput>
+        </div>
+      </div>
+    </footer>
+  </div>
+</template>
+
+<style scoped lang="less">
+  ::v-deep(.custom-input) {
+    .n-input-wrapper {
+      padding-right: 10px;
+    }
+    .n-input__suffix {
+      align-items: end;
+      padding-bottom: 6px;
+    }
+  }
+</style>

+ 43 - 0
langchat-ui/src/views/aigc/chat/components/message/Avatar.vue

@@ -0,0 +1,43 @@
+<script setup lang="ts">
+  import { computed } from 'vue';
+  import { NAvatar } from 'naive-ui';
+  import { isString } from '@/utils/is';
+  import { useUserStore } from '@/store/modules/user';
+  import defaultAvatar from '@/assets/avatar.jpg';
+
+  interface Props {
+    image?: boolean;
+  }
+  defineProps<Props>();
+
+  const userStore = useUserStore();
+
+  const avatar = computed(() => userStore.getUserInfo.headerImg);
+</script>
+
+<template>
+  <template v-if="image">
+    <NAvatar
+      v-if="isString(avatar) && avatar.length > 0"
+      :src="avatar"
+      :fallback-src="defaultAvatar"
+    />
+    <NAvatar v-else round :src="defaultAvatar" />
+  </template>
+  <span v-else class="text-[28px]">
+    <svg
+      xmlns="http://www.w3.org/2000/svg"
+      viewBox="0 0 32 32"
+      aria-hidden="true"
+      width="1em"
+      height="1em"
+    >
+      <path
+        d="M29.71,13.09A8.09,8.09,0,0,0,20.34,2.68a8.08,8.08,0,0,0-13.7,2.9A8.08,8.08,0,0,0,2.3,18.9,8,8,0,0,0,3,25.45a8.08,8.08,0,0,0,8.69,3.87,8,8,0,0,0,6,2.68,8.09,8.09,0,0,0,7.7-5.61,8,8,0,0,0,5.33-3.86A8.09,8.09,0,0,0,29.71,13.09Zm-12,16.82a6,6,0,0,1-3.84-1.39l.19-.11,6.37-3.68a1,1,0,0,0,.53-.91v-9l2.69,1.56a.08.08,0,0,1,.05.07v7.44A6,6,0,0,1,17.68,29.91ZM4.8,24.41a6,6,0,0,1-.71-4l.19.11,6.37,3.68a1,1,0,0,0,1,0l7.79-4.49V22.8a.09.09,0,0,1,0,.08L13,26.6A6,6,0,0,1,4.8,24.41ZM3.12,10.53A6,6,0,0,1,6.28,7.9v7.57a1,1,0,0,0,.51.9l7.75,4.47L11.85,22.4a.14.14,0,0,1-.09,0L5.32,18.68a6,6,0,0,1-2.2-8.18Zm22.13,5.14-7.78-4.52L20.16,9.6a.08.08,0,0,1,.09,0l6.44,3.72a6,6,0,0,1-.9,10.81V16.56A1.06,1.06,0,0,0,25.25,15.67Zm2.68-4-.19-.12-6.36-3.7a1,1,0,0,0-1.05,0l-7.78,4.49V9.2a.09.09,0,0,1,0-.09L19,5.4a6,6,0,0,1,8.91,6.21ZM11.08,17.15,8.38,15.6a.14.14,0,0,1-.05-.08V8.1a6,6,0,0,1,9.84-4.61L18,3.6,11.61,7.28a1,1,0,0,0-.53.91ZM12.54,14,16,12l3.47,2v4L16,20l-3.47-2Z"
+        fill="currentColor"
+      />
+    </svg>
+  </span>
+</template>
+
+<style scoped lang="less"></style>

+ 129 - 0
langchat-ui/src/views/aigc/chat/components/message/Message.vue

@@ -0,0 +1,129 @@
+<script setup lang="ts">
+  import { computed, ref } from 'vue';
+  import { useMessage } from 'naive-ui';
+  import TextComponent from './TextComponent.vue';
+  import AvatarComponent from './Avatar.vue';
+  import SvgIcon from '@/components/SvgIcon/index.vue';
+  import { useBasicLayout } from '../store/useBasicLayout';
+  import { useIconRender } from '../store/useIconRender';
+  import { copyToClip } from '@/utils/copy';
+
+  interface Props {
+    dateTime?: string;
+    text?: string;
+    inversion?: boolean;
+    error?: boolean;
+    loading?: boolean;
+  }
+
+  interface Emit {
+    (ev: 'delete'): void;
+  }
+
+  const props = defineProps<Props>();
+
+  const emit = defineEmits<Emit>();
+
+  const { isMobile } = useBasicLayout();
+
+  const { iconRender } = useIconRender();
+
+  const message = useMessage();
+
+  const textRef = ref<HTMLElement>();
+
+  const asRawText = ref(props.inversion);
+
+  const messageRef = ref<HTMLElement>();
+
+  const options = computed(() => {
+    const common = [
+      {
+        label: '复制',
+        key: 'copyText',
+        icon: iconRender({ icon: 'ri:file-copy-2-line' }),
+      },
+      {
+        label: '删除',
+        key: 'delete',
+        icon: iconRender({ icon: 'ri:delete-bin-line' }),
+      },
+    ];
+
+    if (!props.inversion) {
+      common.unshift({
+        label: asRawText.value ? '预览' : '显示原文',
+        key: 'toggleRenderType',
+        icon: iconRender({ icon: asRawText.value ? 'ic:outline-code-off' : 'ic:outline-code' }),
+      });
+    }
+
+    return common;
+  });
+
+  function handleSelect(key: 'copyText' | 'delete' | 'toggleRenderType') {
+    switch (key) {
+      case 'copyText':
+        handleCopy();
+        return;
+      case 'toggleRenderType':
+        asRawText.value = !asRawText.value;
+        return;
+      case 'delete':
+        emit('delete');
+    }
+  }
+
+  async function handleCopy() {
+    try {
+      await copyToClip(props.text || '');
+      message.success('复制成功');
+    } catch {
+      message.error('复制失败');
+    }
+  }
+</script>
+
+<template>
+  <div
+    ref="messageRef"
+    class="flex w-full mb-6 overflow-hidden"
+    :class="[{ 'flex-row-reverse': inversion }]"
+  >
+    <div
+      class="flex items-center justify-center flex-shrink-0 h-8 overflow-hidden rounded-full basis-8"
+      :class="[inversion ? 'ml-2' : 'mr-2']"
+    >
+      <AvatarComponent :image="inversion" />
+    </div>
+    <div class="overflow-hidden text-sm" :class="[inversion ? 'items-end' : 'items-start']">
+      <p class="text-xs text-[#b4bbc4]" :class="[inversion ? 'text-right' : 'text-left']">
+        {{ dateTime }}
+      </p>
+      <div class="flex items-end gap-1 mt-2" :class="[inversion ? 'flex-row-reverse' : 'flex-row']">
+        <TextComponent
+          ref="textRef"
+          :inversion="inversion"
+          :error="error"
+          :text="text"
+          :loading="loading"
+          :as-raw-text="asRawText"
+        />
+        <div class="flex flex-col">
+          <NDropdown
+            :trigger="isMobile ? 'click' : 'hover'"
+            :placement="!inversion ? 'right' : 'left'"
+            :options="options"
+            @select="handleSelect"
+          >
+            <button class="transition text-neutral-300 hover:text-neutral-800">
+              <SvgIcon icon="ri:more-2-fill" />
+            </button>
+          </NDropdown>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style scoped lang="less"></style>

+ 124 - 0
langchat-ui/src/views/aigc/chat/components/message/TextComponent.vue

@@ -0,0 +1,124 @@
+<script setup lang="ts">
+  import { computed, onMounted, onUnmounted, onUpdated, ref } from 'vue';
+  import MarkdownIt from 'markdown-it';
+  import mdKatex from '@traptitech/markdown-it-katex';
+  import mila from 'markdown-it-link-attributes';
+  import hljs from 'highlight.js';
+  import { useBasicLayout } from '../store/useBasicLayout';
+  import { copyToClip } from '@/utils/copy';
+
+  interface Props {
+    inversion?: boolean;
+    error?: boolean;
+    text?: string;
+    loading?: boolean;
+    asRawText?: boolean;
+  }
+
+  const props = defineProps<Props>();
+
+  const { isMobile } = useBasicLayout();
+
+  const textRef = ref<HTMLElement>();
+
+  const mdi = new MarkdownIt({
+    html: false,
+    linkify: true,
+    highlight(code, language) {
+      const validLang = !!(language && hljs.getLanguage(language));
+      if (validLang) {
+        const lang = language ?? '';
+        return highlightBlock(hljs.highlight(code, { language: lang }).value, lang);
+      }
+      return highlightBlock(hljs.highlightAuto(code).value, '');
+    },
+  });
+
+  mdi.use(mila, { attrs: { target: '_blank', rel: 'noopener' } });
+  mdi.use(mdKatex, { blockClass: 'katexmath-block rounded-md p-[10px]', errorColor: ' #cc0000' });
+
+  const wrapClass = computed(() => {
+    return [
+      'text-wrap',
+      'min-w-[20px]',
+      'rounded-md',
+      isMobile.value ? 'p-2' : 'px-3 py-2',
+      props.inversion ? 'bg-[#d2f9d1]' : 'bg-[#f4f6f8]',
+      // 黑色模式下,左边是黑色1e1e20,右边是绿色a1dc95
+      // props.inversion ? 'dark:bg-[#a1dc95]' : 'dark:bg-[#1e1e20]',
+      props.inversion ? 'message-request' : 'message-reply',
+      { 'text-red-500': props.error },
+    ];
+  });
+
+  const text = computed(() => {
+    const value = props.text ?? '';
+    if (!props.asRawText) {
+      return mdi.render(value);
+    }
+    return value;
+  });
+
+  function highlightBlock(str: string, lang?: string) {
+    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>`;
+  }
+
+  function addCopyEvents() {
+    if (textRef.value) {
+      const copyBtn = textRef.value.querySelectorAll('.code-block-header__copy');
+      copyBtn.forEach((btn) => {
+        btn.addEventListener('click', () => {
+          const code = btn.parentElement?.nextElementSibling?.textContent;
+          if (code) {
+            copyToClip(code).then(() => {
+              btn.textContent = '复制成功';
+              setTimeout(() => {
+                btn.textContent = '复制代码';
+              }, 1000);
+            });
+          }
+        });
+      });
+    }
+  }
+
+  function removeCopyEvents() {
+    if (textRef.value) {
+      const copyBtn = textRef.value.querySelectorAll('.code-block-header__copy');
+      copyBtn.forEach((btn) => {
+        btn.removeEventListener('click', () => {});
+      });
+    }
+  }
+
+  onMounted(() => {
+    addCopyEvents();
+  });
+
+  onUpdated(() => {
+    addCopyEvents();
+  });
+
+  onUnmounted(() => {
+    removeCopyEvents();
+  });
+</script>
+
+<template>
+  <div class="text-black" :class="wrapClass">
+    <div ref="textRef" class="leading-relaxed break-words">
+      <div v-if="!inversion">
+        <div v-if="!asRawText" class="markdown-body" v-html="text"></div>
+        <div v-else class="whitespace-pre-wrap" v-text="text"></div>
+      </div>
+      <div v-else class="whitespace-pre-wrap" v-text="text"></div>
+      <template v-if="loading && !text">
+        <span class="dark:text-white w-[4px] h-[20px] block animate-blink"></span>
+      </template>
+    </div>
+  </div>
+</template>
+
+<style lang="less">
+  @import 'styles';
+</style>

+ 1102 - 0
langchat-ui/src/views/aigc/chat/components/message/styles/github-markdown.less

@@ -0,0 +1,1102 @@
+html.dark {
+  .markdown-body {
+    color-scheme: dark;
+    --color-prettylights-syntax-comment: #8b949e;
+    --color-prettylights-syntax-constant: #79c0ff;
+    --color-prettylights-syntax-entity: #d2a8ff;
+    --color-prettylights-syntax-storage-modifier-import: #c9d1d9;
+    --color-prettylights-syntax-entity-tag: #7ee787;
+    --color-prettylights-syntax-keyword: #ff7b72;
+    --color-prettylights-syntax-string: #a5d6ff;
+    --color-prettylights-syntax-variable: #ffa657;
+    --color-prettylights-syntax-brackethighlighter-unmatched: #f85149;
+    --color-prettylights-syntax-invalid-illegal-text: #f0f6fc;
+    --color-prettylights-syntax-invalid-illegal-bg: #8e1519;
+    --color-prettylights-syntax-carriage-return-text: #f0f6fc;
+    --color-prettylights-syntax-carriage-return-bg: #b62324;
+    --color-prettylights-syntax-string-regexp: #7ee787;
+    --color-prettylights-syntax-markup-list: #f2cc60;
+    --color-prettylights-syntax-markup-heading: #1f6feb;
+    --color-prettylights-syntax-markup-italic: #c9d1d9;
+    --color-prettylights-syntax-markup-bold: #c9d1d9;
+    --color-prettylights-syntax-markup-deleted-text: #ffdcd7;
+    --color-prettylights-syntax-markup-deleted-bg: #67060c;
+    --color-prettylights-syntax-markup-inserted-text: #aff5b4;
+    --color-prettylights-syntax-markup-inserted-bg: #033a16;
+    --color-prettylights-syntax-markup-changed-text: #ffdfb6;
+    --color-prettylights-syntax-markup-changed-bg: #5a1e02;
+    --color-prettylights-syntax-markup-ignored-text: #c9d1d9;
+    --color-prettylights-syntax-markup-ignored-bg: #1158c7;
+    --color-prettylights-syntax-meta-diff-range: #d2a8ff;
+    --color-prettylights-syntax-brackethighlighter-angle: #8b949e;
+    --color-prettylights-syntax-sublimelinter-gutter-mark: #484f58;
+    --color-prettylights-syntax-constant-other-reference-link: #a5d6ff;
+    --color-fg-default: #c9d1d9;
+    --color-fg-muted: #8b949e;
+    --color-fg-subtle: #6e7681;
+    --color-canvas-default: #0d1117;
+    --color-canvas-subtle: #161b22;
+    --color-border-default: #30363d;
+    --color-border-muted: #21262d;
+    --color-neutral-muted: rgba(110,118,129,0.4);
+    --color-accent-fg: #58a6ff;
+    --color-accent-emphasis: #1f6feb;
+    --color-attention-subtle: rgba(187,128,9,0.15);
+    --color-danger-fg: #f85149;
+  }
+}
+
+html {
+  .markdown-body {
+    color-scheme: light;
+    --color-prettylights-syntax-comment: #6e7781;
+    --color-prettylights-syntax-constant: #0550ae;
+    --color-prettylights-syntax-entity: #8250df;
+    --color-prettylights-syntax-storage-modifier-import: #24292f;
+    --color-prettylights-syntax-entity-tag: #116329;
+    --color-prettylights-syntax-keyword: #cf222e;
+    --color-prettylights-syntax-string: #0a3069;
+    --color-prettylights-syntax-variable: #953800;
+    --color-prettylights-syntax-brackethighlighter-unmatched: #82071e;
+    --color-prettylights-syntax-invalid-illegal-text: #f6f8fa;
+    --color-prettylights-syntax-invalid-illegal-bg: #82071e;
+    --color-prettylights-syntax-carriage-return-text: #f6f8fa;
+    --color-prettylights-syntax-carriage-return-bg: #cf222e;
+    --color-prettylights-syntax-string-regexp: #116329;
+    --color-prettylights-syntax-markup-list: #3b2300;
+    --color-prettylights-syntax-markup-heading: #0550ae;
+    --color-prettylights-syntax-markup-italic: #24292f;
+    --color-prettylights-syntax-markup-bold: #24292f;
+    --color-prettylights-syntax-markup-deleted-text: #82071e;
+    --color-prettylights-syntax-markup-deleted-bg: #ffebe9;
+    --color-prettylights-syntax-markup-inserted-text: #116329;
+    --color-prettylights-syntax-markup-inserted-bg: #dafbe1;
+    --color-prettylights-syntax-markup-changed-text: #953800;
+    --color-prettylights-syntax-markup-changed-bg: #ffd8b5;
+    --color-prettylights-syntax-markup-ignored-text: #eaeef2;
+    --color-prettylights-syntax-markup-ignored-bg: #0550ae;
+    --color-prettylights-syntax-meta-diff-range: #8250df;
+    --color-prettylights-syntax-brackethighlighter-angle: #57606a;
+    --color-prettylights-syntax-sublimelinter-gutter-mark: #8c959f;
+    --color-prettylights-syntax-constant-other-reference-link: #0a3069;
+    --color-fg-default: #24292f;
+    --color-fg-muted: #57606a;
+    --color-fg-subtle: #6e7781;
+    --color-canvas-default: #ffffff;
+    --color-canvas-subtle: #f6f8fa;
+    --color-border-default: #d0d7de;
+    --color-border-muted: hsla(210,18%,87%,1);
+    --color-neutral-muted: rgba(175,184,193,0.2);
+    --color-accent-fg: #0969da;
+    --color-accent-emphasis: #0969da;
+    --color-attention-subtle: #fff8c5;
+    --color-danger-fg: #cf222e;
+  }
+}
+
+.markdown-body {
+  -ms-text-size-adjust: 100%;
+  -webkit-text-size-adjust: 100%;
+  margin: 0;
+  //color: var(--color-fg-default);
+  background-color: var(--color-canvas-default);
+  font-family: -apple-system,BlinkMacSystemFont,"Segoe UI","Noto Sans",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji";
+  font-size: 16px;
+  line-height: 1.5;
+  word-wrap: break-word;
+}
+
+.markdown-body .octicon {
+  display: inline-block;
+  fill: currentColor;
+  vertical-align: text-bottom;
+}
+
+.markdown-body h1:hover .anchor .octicon-link:before,
+.markdown-body h2:hover .anchor .octicon-link:before,
+.markdown-body h3:hover .anchor .octicon-link:before,
+.markdown-body h4:hover .anchor .octicon-link:before,
+.markdown-body h5:hover .anchor .octicon-link:before,
+.markdown-body h6:hover .anchor .octicon-link:before {
+  width: 16px;
+  height: 16px;
+  content: ' ';
+  display: inline-block;
+  background-color: currentColor;
+  -webkit-mask-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' version='1.1' aria-hidden='true'><path fill-rule='evenodd' d='M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z'></path></svg>");
+  mask-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' version='1.1' aria-hidden='true'><path fill-rule='evenodd' d='M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z'></path></svg>");
+}
+
+.markdown-body details,
+.markdown-body figcaption,
+.markdown-body figure {
+  display: block;
+}
+
+.markdown-body summary {
+  display: list-item;
+}
+
+.markdown-body [hidden] {
+  display: none !important;
+}
+
+.markdown-body a {
+  background-color: transparent;
+  color: var(--color-accent-fg);
+  text-decoration: none;
+}
+
+.markdown-body abbr[title] {
+  border-bottom: none;
+  text-decoration: underline dotted;
+}
+
+.markdown-body b,
+.markdown-body strong {
+  font-weight: var(--base-text-weight-semibold, 600);
+}
+
+.markdown-body dfn {
+  font-style: italic;
+}
+
+.markdown-body h1 {
+  margin: .67em 0;
+  font-weight: var(--base-text-weight-semibold, 600);
+  padding-bottom: .3em;
+  font-size: 2em;
+  border-bottom: 1px solid var(--color-border-muted);
+}
+
+.markdown-body mark {
+  background-color: var(--color-attention-subtle);
+  color: var(--color-fg-default);
+}
+
+.markdown-body small {
+  font-size: 90%;
+}
+
+.markdown-body sub,
+.markdown-body sup {
+  font-size: 75%;
+  line-height: 0;
+  position: relative;
+  vertical-align: baseline;
+}
+
+.markdown-body sub {
+  bottom: -0.25em;
+}
+
+.markdown-body sup {
+  top: -0.5em;
+}
+
+.markdown-body img {
+  border-style: none;
+  max-width: 100%;
+  box-sizing: content-box;
+  background-color: var(--color-canvas-default);
+}
+
+.markdown-body code,
+.markdown-body kbd,
+.markdown-body pre,
+.markdown-body samp {
+  font-family: monospace;
+  font-size: 1em;
+}
+
+.markdown-body figure {
+  margin: 1em 40px;
+}
+
+.markdown-body hr {
+  box-sizing: content-box;
+  overflow: hidden;
+  background: transparent;
+  border-bottom: 1px solid var(--color-border-muted);
+  height: .25em;
+  padding: 0;
+  margin: 24px 0;
+  background-color: var(--color-border-default);
+  border: 0;
+}
+
+.markdown-body input {
+  font: inherit;
+  margin: 0;
+  overflow: visible;
+  font-family: inherit;
+  font-size: inherit;
+  line-height: inherit;
+}
+
+.markdown-body [type=button],
+.markdown-body [type=reset],
+.markdown-body [type=submit] {
+  -webkit-appearance: button;
+}
+
+.markdown-body [type=checkbox],
+.markdown-body [type=radio] {
+  box-sizing: border-box;
+  padding: 0;
+}
+
+.markdown-body [type=number]::-webkit-inner-spin-button,
+.markdown-body [type=number]::-webkit-outer-spin-button {
+  height: auto;
+}
+
+.markdown-body [type=search]::-webkit-search-cancel-button,
+.markdown-body [type=search]::-webkit-search-decoration {
+  -webkit-appearance: none;
+}
+
+.markdown-body ::-webkit-input-placeholder {
+  color: inherit;
+  opacity: .54;
+}
+
+.markdown-body ::-webkit-file-upload-button {
+  -webkit-appearance: button;
+  font: inherit;
+}
+
+.markdown-body a:hover {
+  text-decoration: underline;
+}
+
+.markdown-body ::placeholder {
+  color: var(--color-fg-subtle);
+  opacity: 1;
+}
+
+.markdown-body hr::before {
+  display: table;
+  content: "";
+}
+
+.markdown-body hr::after {
+  display: table;
+  clear: both;
+  content: "";
+}
+
+.markdown-body table {
+  border-spacing: 0;
+  border-collapse: collapse;
+  display: block;
+  width: max-content;
+  max-width: 100%;
+  overflow: auto;
+}
+
+.markdown-body td,
+.markdown-body th {
+  padding: 0;
+}
+
+.markdown-body details summary {
+  cursor: pointer;
+}
+
+.markdown-body details:not([open])>*:not(summary) {
+  display: none !important;
+}
+
+.markdown-body a:focus,
+.markdown-body [role=button]:focus,
+.markdown-body input[type=radio]:focus,
+.markdown-body input[type=checkbox]:focus {
+  outline: 2px solid var(--color-accent-fg);
+  outline-offset: -2px;
+  box-shadow: none;
+}
+
+.markdown-body a:focus:not(:focus-visible),
+.markdown-body [role=button]:focus:not(:focus-visible),
+.markdown-body input[type=radio]:focus:not(:focus-visible),
+.markdown-body input[type=checkbox]:focus:not(:focus-visible) {
+  outline: solid 1px transparent;
+}
+
+.markdown-body a:focus-visible,
+.markdown-body [role=button]:focus-visible,
+.markdown-body input[type=radio]:focus-visible,
+.markdown-body input[type=checkbox]:focus-visible {
+  outline: 2px solid var(--color-accent-fg);
+  outline-offset: -2px;
+  box-shadow: none;
+}
+
+.markdown-body a:not([class]):focus,
+.markdown-body a:not([class]):focus-visible,
+.markdown-body input[type=radio]:focus,
+.markdown-body input[type=radio]:focus-visible,
+.markdown-body input[type=checkbox]:focus,
+.markdown-body input[type=checkbox]:focus-visible {
+  outline-offset: 0;
+}
+
+.markdown-body kbd {
+  display: inline-block;
+  padding: 3px 5px;
+  font: 11px ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace;
+  line-height: 10px;
+  color: var(--color-fg-default);
+  vertical-align: middle;
+  background-color: var(--color-canvas-subtle);
+  border: solid 1px var(--color-neutral-muted);
+  border-bottom-color: var(--color-neutral-muted);
+  border-radius: 6px;
+  box-shadow: inset 0 -1px 0 var(--color-neutral-muted);
+}
+
+.markdown-body h1,
+.markdown-body h2,
+.markdown-body h3,
+.markdown-body h4,
+.markdown-body h5,
+.markdown-body h6 {
+  margin-top: 24px;
+  margin-bottom: 16px;
+  font-weight: var(--base-text-weight-semibold, 600);
+  line-height: 1.25;
+}
+
+.markdown-body h2 {
+  font-weight: var(--base-text-weight-semibold, 600);
+  padding-bottom: .3em;
+  font-size: 1.5em;
+  border-bottom: 1px solid var(--color-border-muted);
+}
+
+.markdown-body h3 {
+  font-weight: var(--base-text-weight-semibold, 600);
+  font-size: 1.25em;
+}
+
+.markdown-body h4 {
+  font-weight: var(--base-text-weight-semibold, 600);
+  font-size: 1em;
+}
+
+.markdown-body h5 {
+  font-weight: var(--base-text-weight-semibold, 600);
+  font-size: .875em;
+}
+
+.markdown-body h6 {
+  font-weight: var(--base-text-weight-semibold, 600);
+  font-size: .85em;
+  color: var(--color-fg-muted);
+}
+
+.markdown-body p {
+  margin-top: 0;
+  margin-bottom: 10px;
+}
+
+.markdown-body blockquote {
+  margin: 0;
+  padding: 0 1em;
+  color: var(--color-fg-muted);
+  border-left: .25em solid var(--color-border-default);
+}
+
+.markdown-body ul,
+.markdown-body ol {
+  margin-top: 0;
+  margin-bottom: 0;
+  padding-left: 2em;
+}
+
+.markdown-body ol ol,
+.markdown-body ul ol {
+  list-style-type: lower-roman;
+}
+
+.markdown-body ul ul ol,
+.markdown-body ul ol ol,
+.markdown-body ol ul ol,
+.markdown-body ol ol ol {
+  list-style-type: lower-alpha;
+}
+
+.markdown-body dd {
+  margin-left: 0;
+}
+
+.markdown-body tt,
+.markdown-body code,
+.markdown-body samp {
+  font-family: ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace;
+  font-size: 12px;
+}
+
+.markdown-body pre {
+  margin-top: 0;
+  margin-bottom: 0;
+  font-family: ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace;
+  font-size: 12px;
+  word-wrap: normal;
+}
+
+.markdown-body .octicon {
+  display: inline-block;
+  overflow: visible !important;
+  vertical-align: text-bottom;
+  fill: currentColor;
+}
+
+.markdown-body input::-webkit-outer-spin-button,
+.markdown-body input::-webkit-inner-spin-button {
+  margin: 0;
+  -webkit-appearance: none;
+  appearance: none;
+}
+
+.markdown-body::before {
+  display: table;
+  content: "";
+}
+
+.markdown-body::after {
+  display: table;
+  clear: both;
+  content: "";
+}
+
+.markdown-body>*:first-child {
+  margin-top: 0 !important;
+}
+
+.markdown-body>*:last-child {
+  margin-bottom: 0 !important;
+}
+
+.markdown-body a:not([href]) {
+  color: inherit;
+  text-decoration: none;
+}
+
+.markdown-body .absent {
+  color: var(--color-danger-fg);
+}
+
+.markdown-body .anchor {
+  float: left;
+  padding-right: 4px;
+  margin-left: -20px;
+  line-height: 1;
+}
+
+.markdown-body .anchor:focus {
+  outline: none;
+}
+
+.markdown-body p,
+.markdown-body blockquote,
+.markdown-body ul,
+.markdown-body ol,
+.markdown-body dl,
+.markdown-body table,
+.markdown-body pre,
+.markdown-body details {
+  margin-top: 0;
+  margin-bottom: 16px;
+}
+
+.markdown-body blockquote>:first-child {
+  margin-top: 0;
+}
+
+.markdown-body blockquote>:last-child {
+  margin-bottom: 0;
+}
+
+.markdown-body h1 .octicon-link,
+.markdown-body h2 .octicon-link,
+.markdown-body h3 .octicon-link,
+.markdown-body h4 .octicon-link,
+.markdown-body h5 .octicon-link,
+.markdown-body h6 .octicon-link {
+  color: var(--color-fg-default);
+  vertical-align: middle;
+  visibility: hidden;
+}
+
+.markdown-body h1:hover .anchor,
+.markdown-body h2:hover .anchor,
+.markdown-body h3:hover .anchor,
+.markdown-body h4:hover .anchor,
+.markdown-body h5:hover .anchor,
+.markdown-body h6:hover .anchor {
+  text-decoration: none;
+}
+
+.markdown-body h1:hover .anchor .octicon-link,
+.markdown-body h2:hover .anchor .octicon-link,
+.markdown-body h3:hover .anchor .octicon-link,
+.markdown-body h4:hover .anchor .octicon-link,
+.markdown-body h5:hover .anchor .octicon-link,
+.markdown-body h6:hover .anchor .octicon-link {
+  visibility: visible;
+}
+
+.markdown-body h1 tt,
+.markdown-body h1 code,
+.markdown-body h2 tt,
+.markdown-body h2 code,
+.markdown-body h3 tt,
+.markdown-body h3 code,
+.markdown-body h4 tt,
+.markdown-body h4 code,
+.markdown-body h5 tt,
+.markdown-body h5 code,
+.markdown-body h6 tt,
+.markdown-body h6 code {
+  padding: 0 .2em;
+  font-size: inherit;
+}
+
+.markdown-body summary h1,
+.markdown-body summary h2,
+.markdown-body summary h3,
+.markdown-body summary h4,
+.markdown-body summary h5,
+.markdown-body summary h6 {
+  display: inline-block;
+}
+
+.markdown-body summary h1 .anchor,
+.markdown-body summary h2 .anchor,
+.markdown-body summary h3 .anchor,
+.markdown-body summary h4 .anchor,
+.markdown-body summary h5 .anchor,
+.markdown-body summary h6 .anchor {
+  margin-left: -40px;
+}
+
+.markdown-body summary h1,
+.markdown-body summary h2 {
+  padding-bottom: 0;
+  border-bottom: 0;
+}
+
+.markdown-body ul.no-list,
+.markdown-body ol.no-list {
+  padding: 0;
+  list-style-type: none;
+}
+
+.markdown-body ol[type=a] {
+  list-style-type: lower-alpha;
+}
+
+.markdown-body ol[type=A] {
+  list-style-type: upper-alpha;
+}
+
+.markdown-body ol[type=i] {
+  list-style-type: lower-roman;
+}
+
+.markdown-body ol[type=I] {
+  list-style-type: upper-roman;
+}
+
+.markdown-body ol[type="1"] {
+  list-style-type: decimal;
+}
+
+.markdown-body div>ol:not([type]) {
+  list-style-type: decimal;
+}
+
+.markdown-body ul ul,
+.markdown-body ul ol,
+.markdown-body ol ol,
+.markdown-body ol ul {
+  margin-top: 0;
+  margin-bottom: 0;
+}
+
+.markdown-body li>p {
+  margin-top: 16px;
+}
+
+.markdown-body li+li {
+  margin-top: .25em;
+}
+
+.markdown-body dl {
+  padding: 0;
+}
+
+.markdown-body dl dt {
+  padding: 0;
+  margin-top: 16px;
+  font-size: 1em;
+  font-style: italic;
+  font-weight: var(--base-text-weight-semibold, 600);
+}
+
+.markdown-body dl dd {
+  padding: 0 16px;
+  margin-bottom: 16px;
+}
+
+.markdown-body table th {
+  font-weight: var(--base-text-weight-semibold, 600);
+}
+
+.markdown-body table th,
+.markdown-body table td {
+  padding: 6px 13px;
+  border: 1px solid var(--color-border-default);
+}
+
+.markdown-body table tr {
+  background-color: var(--color-canvas-default);
+  border-top: 1px solid var(--color-border-muted);
+}
+
+.markdown-body table tr:nth-child(2n) {
+  background-color: var(--color-canvas-subtle);
+}
+
+.markdown-body table img {
+  background-color: transparent;
+}
+
+.markdown-body img[align=right] {
+  padding-left: 20px;
+}
+
+.markdown-body img[align=left] {
+  padding-right: 20px;
+}
+
+.markdown-body .emoji {
+  max-width: none;
+  vertical-align: text-top;
+  background-color: transparent;
+}
+
+.markdown-body span.frame {
+  display: block;
+  overflow: hidden;
+}
+
+.markdown-body span.frame>span {
+  display: block;
+  float: left;
+  width: auto;
+  padding: 7px;
+  margin: 13px 0 0;
+  overflow: hidden;
+  border: 1px solid var(--color-border-default);
+}
+
+.markdown-body span.frame span img {
+  display: block;
+  float: left;
+}
+
+.markdown-body span.frame span span {
+  display: block;
+  padding: 5px 0 0;
+  clear: both;
+  color: var(--color-fg-default);
+}
+
+.markdown-body span.align-center {
+  display: block;
+  overflow: hidden;
+  clear: both;
+}
+
+.markdown-body span.align-center>span {
+  display: block;
+  margin: 13px auto 0;
+  overflow: hidden;
+  text-align: center;
+}
+
+.markdown-body span.align-center span img {
+  margin: 0 auto;
+  text-align: center;
+}
+
+.markdown-body span.align-right {
+  display: block;
+  overflow: hidden;
+  clear: both;
+}
+
+.markdown-body span.align-right>span {
+  display: block;
+  margin: 13px 0 0;
+  overflow: hidden;
+  text-align: right;
+}
+
+.markdown-body span.align-right span img {
+  margin: 0;
+  text-align: right;
+}
+
+.markdown-body span.float-left {
+  display: block;
+  float: left;
+  margin-right: 13px;
+  overflow: hidden;
+}
+
+.markdown-body span.float-left span {
+  margin: 13px 0 0;
+}
+
+.markdown-body span.float-right {
+  display: block;
+  float: right;
+  margin-left: 13px;
+  overflow: hidden;
+}
+
+.markdown-body span.float-right>span {
+  display: block;
+  margin: 13px auto 0;
+  overflow: hidden;
+  text-align: right;
+}
+
+.markdown-body code,
+.markdown-body tt {
+  padding: .2em .4em;
+  margin: 0;
+  font-size: 85%;
+  white-space: break-spaces;
+  background-color: var(--color-neutral-muted);
+  border-radius: 6px;
+}
+
+.markdown-body code br,
+.markdown-body tt br {
+  display: none;
+}
+
+.markdown-body del code {
+  text-decoration: inherit;
+}
+
+.markdown-body samp {
+  font-size: 85%;
+}
+
+.markdown-body pre code {
+  font-size: 100%;
+}
+
+.markdown-body pre>code {
+  padding: 0;
+  margin: 0;
+  word-break: normal;
+  white-space: pre;
+  background: transparent;
+  border: 0;
+}
+
+.markdown-body .highlight {
+  margin-bottom: 16px;
+}
+
+.markdown-body .highlight pre {
+  margin-bottom: 0;
+  word-break: normal;
+}
+
+.markdown-body .highlight pre,
+.markdown-body pre {
+  padding: 16px;
+  overflow: auto;
+  font-size: 85%;
+  line-height: 1.45;
+  background-color: var(--color-canvas-subtle);
+  border-radius: 6px;
+}
+
+.markdown-body pre code,
+.markdown-body pre tt {
+  display: inline;
+  max-width: auto;
+  padding: 0;
+  margin: 0;
+  overflow: visible;
+  line-height: inherit;
+  word-wrap: normal;
+  background-color: transparent;
+  border: 0;
+}
+
+.markdown-body .csv-data td,
+.markdown-body .csv-data th {
+  padding: 5px;
+  overflow: hidden;
+  font-size: 12px;
+  line-height: 1;
+  text-align: left;
+  white-space: nowrap;
+}
+
+.markdown-body .csv-data .blob-num {
+  padding: 10px 8px 9px;
+  text-align: right;
+  background: var(--color-canvas-default);
+  border: 0;
+}
+
+.markdown-body .csv-data tr {
+  border-top: 0;
+}
+
+.markdown-body .csv-data th {
+  font-weight: var(--base-text-weight-semibold, 600);
+  background: var(--color-canvas-subtle);
+  border-top: 0;
+}
+
+.markdown-body [data-footnote-ref]::before {
+  content: "[";
+}
+
+.markdown-body [data-footnote-ref]::after {
+  content: "]";
+}
+
+.markdown-body .footnotes {
+  font-size: 12px;
+  color: var(--color-fg-muted);
+  border-top: 1px solid var(--color-border-default);
+}
+
+.markdown-body .footnotes ol {
+  padding-left: 16px;
+}
+
+.markdown-body .footnotes ol ul {
+  display: inline-block;
+  padding-left: 16px;
+  margin-top: 16px;
+}
+
+.markdown-body .footnotes li {
+  position: relative;
+}
+
+.markdown-body .footnotes li:target::before {
+  position: absolute;
+  top: -8px;
+  right: -8px;
+  bottom: -8px;
+  left: -24px;
+  pointer-events: none;
+  content: "";
+  border: 2px solid var(--color-accent-emphasis);
+  border-radius: 6px;
+}
+
+.markdown-body .footnotes li:target {
+  color: var(--color-fg-default);
+}
+
+.markdown-body .footnotes .data-footnote-backref g-emoji {
+  font-family: monospace;
+}
+
+.markdown-body .pl-c {
+  color: var(--color-prettylights-syntax-comment);
+}
+
+.markdown-body .pl-c1,
+.markdown-body .pl-s .pl-v {
+  color: var(--color-prettylights-syntax-constant);
+}
+
+.markdown-body .pl-e,
+.markdown-body .pl-en {
+  color: var(--color-prettylights-syntax-entity);
+}
+
+.markdown-body .pl-smi,
+.markdown-body .pl-s .pl-s1 {
+  color: var(--color-prettylights-syntax-storage-modifier-import);
+}
+
+.markdown-body .pl-ent {
+  color: var(--color-prettylights-syntax-entity-tag);
+}
+
+.markdown-body .pl-k {
+  color: var(--color-prettylights-syntax-keyword);
+}
+
+.markdown-body .pl-s,
+.markdown-body .pl-pds,
+.markdown-body .pl-s .pl-pse .pl-s1,
+.markdown-body .pl-sr,
+.markdown-body .pl-sr .pl-cce,
+.markdown-body .pl-sr .pl-sre,
+.markdown-body .pl-sr .pl-sra {
+  color: var(--color-prettylights-syntax-string);
+}
+
+.markdown-body .pl-v,
+.markdown-body .pl-smw {
+  color: var(--color-prettylights-syntax-variable);
+}
+
+.markdown-body .pl-bu {
+  color: var(--color-prettylights-syntax-brackethighlighter-unmatched);
+}
+
+.markdown-body .pl-ii {
+  color: var(--color-prettylights-syntax-invalid-illegal-text);
+  background-color: var(--color-prettylights-syntax-invalid-illegal-bg);
+}
+
+.markdown-body .pl-c2 {
+  color: var(--color-prettylights-syntax-carriage-return-text);
+  background-color: var(--color-prettylights-syntax-carriage-return-bg);
+}
+
+.markdown-body .pl-sr .pl-cce {
+  font-weight: bold;
+  color: var(--color-prettylights-syntax-string-regexp);
+}
+
+.markdown-body .pl-ml {
+  color: var(--color-prettylights-syntax-markup-list);
+}
+
+.markdown-body .pl-mh,
+.markdown-body .pl-mh .pl-en,
+.markdown-body .pl-ms {
+  font-weight: bold;
+  color: var(--color-prettylights-syntax-markup-heading);
+}
+
+.markdown-body .pl-mi {
+  font-style: italic;
+  color: var(--color-prettylights-syntax-markup-italic);
+}
+
+.markdown-body .pl-mb {
+  font-weight: bold;
+  color: var(--color-prettylights-syntax-markup-bold);
+}
+
+.markdown-body .pl-md {
+  color: var(--color-prettylights-syntax-markup-deleted-text);
+  background-color: var(--color-prettylights-syntax-markup-deleted-bg);
+}
+
+.markdown-body .pl-mi1 {
+  color: var(--color-prettylights-syntax-markup-inserted-text);
+  background-color: var(--color-prettylights-syntax-markup-inserted-bg);
+}
+
+.markdown-body .pl-mc {
+  color: var(--color-prettylights-syntax-markup-changed-text);
+  background-color: var(--color-prettylights-syntax-markup-changed-bg);
+}
+
+.markdown-body .pl-mi2 {
+  color: var(--color-prettylights-syntax-markup-ignored-text);
+  background-color: var(--color-prettylights-syntax-markup-ignored-bg);
+}
+
+.markdown-body .pl-mdr {
+  font-weight: bold;
+  color: var(--color-prettylights-syntax-meta-diff-range);
+}
+
+.markdown-body .pl-ba {
+  color: var(--color-prettylights-syntax-brackethighlighter-angle);
+}
+
+.markdown-body .pl-sg {
+  color: var(--color-prettylights-syntax-sublimelinter-gutter-mark);
+}
+
+.markdown-body .pl-corl {
+  text-decoration: underline;
+  color: var(--color-prettylights-syntax-constant-other-reference-link);
+}
+
+.markdown-body g-emoji {
+  display: inline-block;
+  min-width: 1ch;
+  font-family: "Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";
+  font-size: 1em;
+  font-style: normal !important;
+  font-weight: var(--base-text-weight-normal, 400);
+  line-height: 1;
+  vertical-align: -0.075em;
+}
+
+.markdown-body g-emoji img {
+  width: 1em;
+  height: 1em;
+}
+
+.markdown-body .task-list-item {
+  list-style-type: none;
+}
+
+.markdown-body .task-list-item label {
+  font-weight: var(--base-text-weight-normal, 400);
+}
+
+.markdown-body .task-list-item.enabled label {
+  cursor: pointer;
+}
+
+.markdown-body .task-list-item+.task-list-item {
+  margin-top: 4px;
+}
+
+.markdown-body .task-list-item .handle {
+  display: none;
+}
+
+.markdown-body .task-list-item-checkbox {
+  margin: 0 .2em .25em -1.4em;
+  vertical-align: middle;
+}
+
+.markdown-body .contains-task-list:dir(rtl) .task-list-item-checkbox {
+  margin: 0 -1.6em .25em .2em;
+}
+
+.markdown-body .contains-task-list {
+  position: relative;
+}
+
+.markdown-body .contains-task-list:hover .task-list-item-convert-container,
+.markdown-body .contains-task-list:focus-within .task-list-item-convert-container {
+  display: block;
+  width: auto;
+  height: 24px;
+  overflow: visible;
+  clip: auto;
+}
+
+.markdown-body ::-webkit-calendar-picker-indicator {
+  filter: invert(50%);
+}

+ 206 - 0
langchat-ui/src/views/aigc/chat/components/message/styles/highlight.less

@@ -0,0 +1,206 @@
+html.dark {
+	pre code.hljs {
+		display: block;
+		overflow-x: auto;
+		padding: 1em
+	}
+
+	code.hljs {
+		padding: 3px 5px
+	}
+
+	.hljs {
+		color: #abb2bf;
+		background: #282c34
+	}
+
+	.hljs-keyword,
+	.hljs-operator,
+	.hljs-pattern-match {
+		color: #f92672
+	}
+
+	.hljs-function,
+	.hljs-pattern-match .hljs-constructor {
+		color: #61aeee
+	}
+
+	.hljs-function .hljs-params {
+		color: #a6e22e
+	}
+
+	.hljs-function .hljs-params .hljs-typing {
+		color: #fd971f
+	}
+
+	.hljs-module-access .hljs-module {
+		color: #7e57c2
+	}
+
+	.hljs-constructor {
+		color: #e2b93d
+	}
+
+	.hljs-constructor .hljs-string {
+		color: #9ccc65
+	}
+
+	.hljs-comment,
+	.hljs-quote {
+		color: #b18eb1;
+		font-style: italic
+	}
+
+	.hljs-doctag,
+	.hljs-formula {
+		color: #c678dd
+	}
+
+	.hljs-deletion,
+	.hljs-name,
+	.hljs-section,
+	.hljs-selector-tag,
+	.hljs-subst {
+		color: #e06c75
+	}
+
+	.hljs-literal {
+		color: #56b6c2
+	}
+
+	.hljs-addition,
+	.hljs-attribute,
+	.hljs-meta .hljs-string,
+	.hljs-regexp,
+	.hljs-string {
+		color: #98c379
+	}
+
+	.hljs-built_in,
+	.hljs-class .hljs-title,
+	.hljs-title.class_ {
+		color: #e6c07b
+	}
+
+	.hljs-attr,
+	.hljs-number,
+	.hljs-selector-attr,
+	.hljs-selector-class,
+	.hljs-selector-pseudo,
+	.hljs-template-variable,
+	.hljs-type,
+	.hljs-variable {
+		color: #d19a66
+	}
+
+	.hljs-bullet,
+	.hljs-link,
+	.hljs-meta,
+	.hljs-selector-id,
+	.hljs-symbol,
+	.hljs-title {
+		color: #61aeee
+	}
+
+	.hljs-emphasis {
+		font-style: italic
+	}
+
+	.hljs-strong {
+		font-weight: 700
+	}
+
+	.hljs-link {
+		text-decoration: underline
+	}
+}
+
+html {
+	pre code.hljs {
+		display: block;
+		overflow-x: auto;
+		padding: 1em
+	}
+
+	code.hljs {
+		padding: 3px 5px;
+		&::-webkit-scrollbar {
+			height: 4px;
+		}
+	}
+
+	.hljs {
+		color: #383a42;
+		background: #fafafa
+	}
+
+	.hljs-comment,
+	.hljs-quote {
+		color: #a0a1a7;
+		font-style: italic
+	}
+
+	.hljs-doctag,
+	.hljs-formula,
+	.hljs-keyword {
+		color: #a626a4
+	}
+
+	.hljs-deletion,
+	.hljs-name,
+	.hljs-section,
+	.hljs-selector-tag,
+	.hljs-subst {
+		color: #e45649
+	}
+
+	.hljs-literal {
+		color: #0184bb
+	}
+
+	.hljs-addition,
+	.hljs-attribute,
+	.hljs-meta .hljs-string,
+	.hljs-regexp,
+	.hljs-string {
+		color: #50a14f
+	}
+
+	.hljs-attr,
+	.hljs-number,
+	.hljs-selector-attr,
+	.hljs-selector-class,
+	.hljs-selector-pseudo,
+	.hljs-template-variable,
+	.hljs-type,
+	.hljs-variable {
+		color: #986801
+	}
+
+	.hljs-bullet,
+	.hljs-link,
+	.hljs-meta,
+	.hljs-selector-id,
+	.hljs-symbol,
+	.hljs-title {
+		color: #4078f2
+	}
+
+	.hljs-built_in,
+	.hljs-class .hljs-title,
+	.hljs-title.class_ {
+		color: #c18401
+	}
+
+	.hljs-emphasis {
+		font-style: italic
+	}
+
+	.hljs-strong {
+		font-weight: 700
+	}
+
+	.hljs-link {
+		text-decoration: underline
+	}
+}

+ 3 - 0
langchat-ui/src/views/aigc/chat/components/message/styles/index.less

@@ -0,0 +1,3 @@
+@import "github-markdown.less";
+@import "highlight.less";
+@import "style.less";

+ 89 - 0
langchat-ui/src/views/aigc/chat/components/message/styles/style.less

@@ -0,0 +1,89 @@
+.markdown-body {
+	background-color: transparent;
+	font-size: 14px;
+
+	p {
+		white-space: pre-wrap;
+	}
+
+	ol {
+		list-style-type: decimal;
+	}
+
+	ul {
+		list-style-type: disc;
+	}
+
+	pre code,
+	pre tt {
+		line-height: 1.65;
+	}
+
+	.highlight pre,
+	pre {
+		background-color: #fff;
+	}
+
+	code.hljs {
+		padding: 0;
+	}
+
+	.code-block {
+		&-wrapper {
+			position: relative;
+			padding-top: 24px;
+      margin-top: 10px;
+		}
+
+		&-header {
+			position: absolute;
+			top: 5px;
+			right: 0;
+			width: 100%;
+			padding: 0 1rem;
+			display: flex;
+			justify-content: flex-end;
+			align-items: center;
+			color: #b3b3b3;
+
+			&__copy {
+				cursor: pointer;
+				margin-left: 0.5rem;
+				user-select: none;
+
+				&:hover {
+					color: #65a665;
+				}
+			}
+		}
+	}
+
+}
+
+html.dark {
+
+	.message-reply {
+		.whitespace-pre-wrap {
+			white-space: pre-wrap;
+			color: var(--n-text-color);
+		}
+	}
+
+	.highlight pre,
+	pre {
+		background-color: #282c34;
+	}
+}
+
+@keyframes blink {
+	0%, to {
+		background-color: currentColor
+	}
+	50% {
+		background-color: transparent
+	}
+}
+
+.animate-blink {
+	animation: blink 1.2s infinite steps(1, start)
+}

+ 8 - 0
langchat-ui/src/views/aigc/chat/components/store/chat.d.ts

@@ -0,0 +1,8 @@
+export interface ChatState {
+  isEdit: string; //当前编辑的id
+  active: string; //当前激活的id
+  siderCollapsed: boolean; //侧边栏展开状态
+  sideIsLoading: boolean; //侧边栏加载状态
+  chatIsLoading: boolean; //会话窗口加载状态
+  messages: any[]; //当前选中的消息内容
+}

+ 7 - 0
langchat-ui/src/views/aigc/chat/components/store/useBasicLayout.ts

@@ -0,0 +1,7 @@
+import { breakpointsTailwind, useBreakpoints } from '@vueuse/core';
+
+export const useBasicLayout = () => {
+  const breakpoints = useBreakpoints(breakpointsTailwind);
+  const isMobile = breakpoints.smaller('sm');
+  return { isMobile };
+};

+ 94 - 0
langchat-ui/src/views/aigc/chat/components/store/useChatStore.ts

@@ -0,0 +1,94 @@
+import { defineStore } from 'pinia';
+import { ChatState } from './chat';
+import { formatToDateTime } from '@/utils/dateUtil';
+import { getMessages } from '@/api/aigc/conversation';
+
+export const useChatStore = defineStore('chat-store', {
+  state: (): ChatState =>
+    <ChatState>{
+      active: '',
+      isEdit: '',
+      siderCollapsed: true,
+      sideIsLoading: true,
+      chatIsLoading: true,
+      messages: [],
+    },
+
+  getters: {},
+
+  actions: {
+    setSiderCollapsed(collapsed: boolean) {
+      this.siderCollapsed = collapsed;
+    },
+    async setActive(id: string) {
+      this.active = id;
+    },
+    async setEdit(id: string) {
+      this.isEdit = id;
+    },
+
+    /**
+     * 加载会话窗口和聊天信息
+     */
+    async loadData() {
+      try {
+      } finally {
+        this.sideIsLoading = false;
+        this.chatIsLoading = false;
+      }
+    },
+
+    /**
+     * 选择会话窗口
+     */
+    async selectConversation(params: any) {
+      if (params.id == undefined) {
+        return;
+      }
+      console.log('set', params.id);
+      await this.setActive(params.id);
+      await this.setEdit('');
+      this.messages = await getMessages(params.id);
+      console.log(this.messages);
+    },
+
+    /**
+     * 新增消息
+     */
+    async addMessage(
+      message: string,
+      role: 'user' | 'assistant' | 'system',
+      promptId: string,
+      parentRefId: string
+    ): Promise<boolean> {
+      this.messages.push({
+        promptId: promptId,
+        parentRefId: parentRefId,
+        role: role,
+        content: message,
+        createTime: formatToDateTime(new Date()),
+      });
+      return true;
+    },
+
+    /**
+     * 更新消息
+     * promptId 仅仅用于更新流式消息内容
+     */
+    async updateMessage(promptId: string, content: string, isError?: boolean) {
+      const promptIndex = this.messages.findIndex((item) => item?.promptId == promptId);
+      if (promptIndex !== -1) {
+        this.messages[promptIndex].content = content;
+        this.messages[promptIndex].isError = isError;
+      }
+      console.log(this.messages);
+    },
+
+    /**
+     * 删除消息
+     */
+    async delMessage(item: any) {
+      this.messages = this.messages.filter((i) => i.promptId !== item.promptId);
+    },
+  },
+});

+ 39 - 0
langchat-ui/src/views/aigc/chat/components/store/useIconRender.ts

@@ -0,0 +1,39 @@
+import { h } from 'vue';
+import SvgIcon from '@/components/SvgIcon/index.vue';
+
+export const useIconRender = () => {
+  interface IconConfig {
+    icon?: string;
+    color?: string;
+    fontSize?: number;
+  }
+
+  interface IconStyle {
+    color?: string;
+    fontSize?: string;
+  }
+
+  const iconRender = (config: IconConfig) => {
+    const { color, fontSize, icon } = config;
+
+    const style: IconStyle = {};
+
+    if (color) {
+      style.color = color;
+    }
+
+    if (fontSize) {
+      style.fontSize = `${fontSize}px`;
+    }
+
+    if (!icon) {
+      window.console.warn('iconRender: icon is required');
+    }
+
+    return () => h(SvgIcon, { icon, style });
+  };
+
+  return {
+    iconRender,
+  };
+};

+ 52 - 0
langchat-ui/src/views/aigc/chat/components/store/useScroll.ts

@@ -0,0 +1,52 @@
+import type { Ref } from 'vue';
+import { nextTick, ref } from 'vue';
+import { LayoutInst } from 'naive-ui';
+
+type ScrollElement = HTMLDivElement | null;
+
+interface ScrollReturn {
+  contentRef: Ref<LayoutInst | null>;
+  scrollRef: Ref<ScrollElement>;
+  scrollToBottom: () => Promise<void>;
+  scrollToTop: () => Promise<void>;
+  scrollToBottomIfAtBottom: () => Promise<void>;
+}
+
+export function useScroll(): ScrollReturn {
+  const scrollRef = ref<ScrollElement>(null);
+  const contentRef = ref<LayoutInst | null>(null);
+
+  const scrollToBottom = async () => {
+    await nextTick();
+    if (contentRef.value && scrollRef.value) {
+      contentRef.value.scrollTo({ top: scrollRef.value.scrollHeight });
+    }
+  };
+
+  const scrollToTop = async () => {
+    await nextTick();
+    if (contentRef.value && scrollRef.value) {
+      contentRef.value.scrollTo({ top: 0 });
+    }
+  };
+
+  const scrollToBottomIfAtBottom = async () => {
+    await nextTick();
+    if (contentRef.value && scrollRef.value) {
+      const threshold = 100; // 阈值,表示滚动条到底部的距离阈值
+      const distanceToBottom =
+        scrollRef.value.scrollHeight - scrollRef.value.scrollTop - scrollRef.value.clientHeight;
+      if (distanceToBottom <= threshold) {
+        contentRef.value.scrollTo({ top: scrollRef.value.scrollHeight });
+      }
+    }
+  };
+
+  return {
+    scrollRef,
+    contentRef,
+    scrollToBottom,
+    scrollToTop,
+    scrollToBottomIfAtBottom,
+  };
+}

+ 110 - 2
langchat-ui/src/views/aigc/chat/index.vue

@@ -1,7 +1,115 @@
-<script setup lang="ts"></script>
+<script setup lang="ts">
+  import { ref } from 'vue';
+  import SvgIcon from '@/components/SvgIcon/index.vue';
+  import Chat from './components/Chat.vue';
+
+  const checked = ref();
+  const radios = [
+    {
+      name: 'Paypal',
+      description: "It's the faster, safer way to send and receive money.",
+      icon: `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+                <path d="M7.60676 23.1864L8.02271 20.5444L7.09617 20.5229H2.67188L5.74654 1.02757C5.75608 0.968712 5.7871 0.913836 5.83243 0.874866C5.87776 0.835896 5.93582 0.814423 5.99626 0.814423H13.4562C15.9328 0.814423 17.642 1.32978 18.5343 2.34698C18.9526 2.82417 19.219 3.32282 19.3479 3.87159C19.4831 4.44739 19.4855 5.13533 19.3535 5.97438L19.3439 6.03562V6.57325L19.7622 6.81025C20.1146 6.99715 20.3945 7.21108 20.6092 7.45604C20.9671 7.86403 21.1986 8.38257 21.2964 8.99734C21.3974 9.62961 21.364 10.382 21.1986 11.2338C21.0077 12.2136 20.6991 13.0669 20.2824 13.7652C19.899 14.4086 19.4107 14.9423 18.8309 15.3558C18.2774 15.7487 17.6197 16.047 16.8761 16.2378C16.1555 16.4255 15.334 16.5202 14.4329 16.5202H13.8523C13.4372 16.5202 13.0339 16.6697 12.7174 16.9377C12.4001 17.2113 12.1901 17.5851 12.1257 17.9939L12.082 18.2317L11.3471 22.8882L11.3137 23.0592C11.3049 23.1133 11.2898 23.1403 11.2676 23.1586C11.2477 23.1753 11.219 23.1864 11.1912 23.1864H7.60676Z" fill="#253B80" />
+                <path d="M20.1586 6.09761C20.1364 6.23997 20.1109 6.38551 20.0823 6.53503C19.0985 11.586 15.7327 13.3309 11.4341 13.3309H9.24541C8.71971 13.3309 8.27673 13.7127 8.19481 14.2312L7.07422 21.3381L6.75689 23.3526C6.70361 23.693 6.96606 24 7.30963 24H11.1915C11.6512 24 12.0417 23.666 12.1141 23.2126L12.1523 23.0154L12.8831 18.3772L12.9301 18.1227C13.0016 17.6678 13.3929 17.3337 13.8526 17.3337H14.4332C18.1942 17.3337 21.1384 15.8067 21.999 11.388C22.3584 9.54209 22.1723 8.00078 21.2212 6.91678C20.9333 6.58991 20.5762 6.31871 20.1586 6.09761Z" fill="#179BD7" />
+                <path d="M19.13 5.68728C18.9797 5.64354 18.8246 5.60378 18.6655 5.56799C18.5057 5.53299 18.3419 5.50198 18.1732 5.47494C17.5831 5.3795 16.9365 5.33417 16.2438 5.33417H10.3967C10.2528 5.33417 10.116 5.36678 9.9935 5.42563C9.72389 5.55526 9.52348 5.81056 9.47496 6.12311L8.2311 14.0014L8.19531 14.2313C8.27723 13.7127 8.72022 13.331 9.24591 13.331H11.4346C15.7332 13.331 19.099 11.5853 20.0828 6.53508C20.1122 6.38556 20.1369 6.24002 20.1591 6.09766C19.9102 5.96564 19.6406 5.85271 19.3503 5.75648C19.2787 5.73262 19.2048 5.70955 19.13 5.68728Z" fill="#222D65" />
+                <path d="M9.47421 6.12308C9.52272 5.81052 9.72314 5.55523 9.99275 5.42639C10.116 5.36753 10.252 5.33493 10.396 5.33493H16.2431C16.9358 5.33493 17.5824 5.38026 18.1725 5.4757C18.3411 5.50274 18.5049 5.53375 18.6648 5.56875C18.8238 5.60453 18.9789 5.6443 19.1292 5.68804C19.204 5.71031 19.278 5.73337 19.3503 5.75644C19.6406 5.85267 19.9102 5.9664 20.1592 6.09763C20.4518 4.23104 20.1568 2.96014 19.1475 1.80933C18.0349 0.5424 16.0267 0 13.4571 0H5.99712C5.47222 0 5.02446 0.381748 4.94334 0.901084L1.83607 20.5969C1.77483 20.9866 2.07546 21.3381 2.46834 21.3381H7.07397L8.23034 14.0014L9.47421 6.12308Z" fill="#253B80" />
+            </svg>`,
+    },
+    {
+      name: 'Master Card',
+      description: ' payment-processing corporation worldwide.',
+      icon: `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+                <path d="M15.2436 6.17905H8.75391V17.8412H15.2436V6.17905Z" fill="#FF5F00" />
+                <path d="M9.16737 12.0105C9.16635 10.8874 9.42086 9.77873 9.91165 8.76848C10.4024 7.75824 11.1166 6.87289 12.0002 6.17946C10.906 5.31945 9.59201 4.78462 8.20829 4.63611C6.82457 4.48759 5.42699 4.73138 4.17527 5.33961C2.92356 5.94784 1.86822 6.89597 1.12988 8.07562C0.391546 9.25528 0 10.6189 0 12.0105C0 13.4022 0.391546 14.7658 1.12988 15.9455C1.86822 17.1251 2.92356 18.0732 4.17527 18.6815C5.42699 19.2897 6.82457 19.5335 8.20829 19.385C9.59201 19.2365 10.906 18.7016 12.0002 17.8416C11.1166 17.1482 10.4024 16.2628 9.91165 15.2526C9.42087 14.2423 9.16635 13.1337 9.16737 12.0105Z" fill="#EB001B" />
+                <path d="M23.9998 12.0105C23.9998 13.4022 23.6083 14.7658 22.87 15.9454C22.1317 17.1251 21.0764 18.0732 19.8247 18.6814C18.5731 19.2897 17.1755 19.5335 15.7918 19.385C14.4081 19.2365 13.0941 18.7016 12 17.8416C12.8828 17.1475 13.5964 16.262 14.0871 15.2519C14.5778 14.2418 14.8328 13.1335 14.8328 12.0105C14.8328 10.8876 14.5778 9.77925 14.0871 8.76917C13.5964 7.75908 12.8828 6.87359 12 6.17946C13.0941 5.31945 14.4081 4.78462 15.7918 4.63611C17.1755 4.48759 18.5731 4.73139 19.8247 5.33962C21.0764 5.94786 22.1317 6.89599 22.87 8.07565C23.6083 9.25531 23.9998 10.6189 23.9998 12.0105Z" fill="#F79E1B" />
+                <path d="M23.2934 16.6062V16.3674H23.3897V16.3188H23.1445V16.3674H23.2408V16.6062H23.2934ZM23.7695 16.6062V16.3183H23.6943L23.6079 16.5163L23.5214 16.3183H23.4462V16.6062H23.4993V16.389L23.5803 16.5762H23.6354L23.7164 16.3886V16.6062H23.7695Z" fill="#F79E1B" />
+            </svg>`,
+    },
+    {
+      name: 'Visa',
+      description: ' Trusted world leader in digital payment technology',
+      icon: `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+                <path d="M11.8832 8.24628L10.2798 15.7425H8.34041L9.94398 8.24628H11.8832ZM20.0422 13.0867L21.063 10.2717L21.6504 13.0867H20.0422ZM22.2067 15.7425H24L22.4334 8.24628H20.7792C20.4064 8.24628 20.0921 8.46243 19.953 8.79575L17.0431 15.7425H19.0799L19.4842 14.623H21.9719L22.2067 15.7425ZM17.1441 13.2952C17.1526 11.3169 14.4092 11.2073 14.4276 10.3233C14.4335 10.0547 14.6898 9.76859 15.2499 9.69542C15.5276 9.65967 16.2939 9.63067 17.1625 10.0309L17.5022 8.44068C17.0357 8.27191 16.4353 8.10938 15.6883 8.10938C13.7711 8.10938 12.4224 9.12773 12.4116 10.5872C12.3993 11.6664 13.375 12.2681 14.1086 12.6276C14.865 12.995 15.1184 13.2305 15.1147 13.5588C15.1094 14.0617 14.5116 14.2844 13.9549 14.2929C12.9793 14.308 12.4138 14.0292 11.9632 13.8191L11.6111 15.4624C12.065 15.6702 12.9013 15.8509 13.7672 15.8602C15.8054 15.8602 17.1381 14.8538 17.1441 13.2952ZM9.1121 8.24628L5.96986 15.7425H3.92017L2.37375 9.75999C2.28001 9.3921 2.19823 9.25688 1.91313 9.10143C1.44678 8.84819 0.676937 8.6113 0 8.46395L0.0458603 8.24628H3.34574C3.76606 8.24628 4.14424 8.52599 4.24051 9.01022L5.05739 13.3483L7.07471 8.24628H9.1121Z" fill="#1434CB" />
+            </svg>`,
+    },
+  ];
+</script>
 
 <template>
-  <div>11</div>
+  <n-layout has-sider class="h-full w-full">
+    <n-layout-sider :width="350" bordered>
+      <div class="flex justify-center items-center m-4 rounded">
+        <ul class="mt-2 space-y-3">
+          <li v-for="(item, idx) in radios" :key="idx">
+            <label :for="item.name" class="block relative">
+              <input
+                :id="item.name"
+                type="radio"
+                :checked="idx == 1 ? true : false"
+                name="payment"
+                class="sr-only peer"
+              />
+              <div
+                class="w-full flex gap-x-3 items-start p-4 cursor-pointer rounded-lg border bg-white shadow-sm ring-indigo-600 peer-checked:ring-2 duration-200"
+              >
+                <div class="flex-none">
+                  <div v-html="item.icon"></div>
+                </div>
+                <div>
+                  <h3 class="leading-none text-gray-800 font-medium pr-3">
+                    {{ item.name }}
+                  </h3>
+                  <p class="mt-1 text-sm text-gray-600">
+                    {{ item.description }}
+                  </p>
+                </div>
+              </div>
+              <div
+                class="absolute top-4 right-4 flex-none flex items-center justify-center w-4 h-4 rounded-full border peer-checked:bg-indigo-600 text-white peer-checked:text-white duration-200"
+              >
+                <svg class="w-2.5 h-2.5" viewBox="0 0 12 10">
+                  <polyline
+                    fill="none"
+                    stroke-width="2px"
+                    stroke="currentColor"
+                    stroke-dasharray="16px"
+                    points="1.5 6 4.5 9 10.5 1"
+                  />
+                </svg>
+              </div>
+            </label>
+          </li>
+        </ul>
+      </div>
+    </n-layout-sider>
+
+    <div class="flex justify-center items-center w-full mt-0">
+      <div class="p-8 pt-6 w-full h-full mb-2">
+        <div class="mb-2 flex flex-wrap justify-between items-center">
+          <div class="font-bold flex justify-center items-center flex-wrap gap-2">
+            <SvgIcon class="text-lg" icon="ion:sparkles-outline" />
+            <span>AI对话</span>
+          </div>
+          <div>
+            <n-button size="small" type="success" secondary>
+              <template #icon>
+                <SvgIcon class="text-[14px]" icon="fluent:delete-12-regular" />
+              </template>
+              清空聊天
+            </n-button>
+          </div>
+        </div>
+        <div class="w-full h-full rounded-md p-2 flex items-center justify-center">
+          <Chat v-if="checked !== null" />
+          <n-empty v-else description="在左侧输入图片描述,开始生成图片吧!">
+            <template #extra>
+              <n-button size="small" type="success"> 立即开始 </n-button>
+            </template>
+          </n-empty>
+        </div>
+      </div>
+    </div>
+  </n-layout>
 </template>
 
 <style scoped lang="less"></style>

+ 4 - 4
langchat-ui/src/views/aigc/knowledge/components/DocList/index.vue

@@ -22,9 +22,9 @@
         </n-button>
       </template>
     </BasicTable>
-  </n-card>
 
-  <Edit ref="editRef" @reload="reloadTable" />
+    <Edit ref="editRef" @reload="reloadTable" />
+  </n-card>
 </template>
 
 <script lang="ts" setup>
@@ -86,11 +86,11 @@
   }
 
   function handleAdd() {
-    editRef.value.show(props.kbId);
+    editRef.value.show('');
   }
 
   function handleEdit(record: Recordable) {
-    editRef.value.show(props.kbId, record.id);
+    editRef.value.show('', record.id);
   }
 
   function handleDelete(record: Recordable) {

+ 2 - 3
langchat-ui/src/views/aigc/knowledge/components/FileList/index.vue

@@ -22,7 +22,6 @@
   import { DeleteOutlined, EditOutlined } from '@vicons/antd';
   import { NSwitch, useDialog, useMessage } from 'naive-ui';
   import { useRouter } from 'vue-router';
-  import { KbFile } from '@/api/models/flow';
 
   const router = useRouter();
   const message = useMessage();
@@ -71,7 +70,7 @@
     actionRef.value.reload();
   }
 
-  const embedColumn: BasicColumn<KbFile>[] = [
+  const embedColumn: BasicColumn[] = [
     {
       title: '是否Embed',
       key: 'isEmbed',
@@ -86,7 +85,7 @@
       },
     },
   ];
-  function embedHandler(val: boolean, row: KbFile) {
+  function embedHandler(val: boolean, row: any) {
     console.log('点击了', val, row);
   }
 

+ 10 - 5
langchat-ui/src/views/aigc/knowledge/index.vue

@@ -87,13 +87,18 @@
     </div>
 
     <n-spin :show="loading">
-      <ul class="mt-6 grid gap-8 sm:grid-cols-2 lg:grid-cols-3">
-        <li v-for="(item, idx) in list" :key="idx" class="rounded-lg shadow bg-gray-50">
+      <ul class="mt-6 grid gap-8 sm:grid-cols-3 lg:grid-cols-4">
+        <li v-for="(item, idx) in list" :key="idx" class="rounded-lg shadow-sm border">
           <div class="flex items-center justify-between p-4">
             <div class="flex items-center gap-2">
-              <n-avatar :size="48" round>
-                <PlusOutlined />
+              <n-avatar v-if="item.cover" :src="item.cover" :size="48" round>
+                <template #fallback>
+                  <div class="flex justify-center items-center w-full">
+                    {{ item.name.substring(0, 4) }}
+                  </div>
+                </template>
               </n-avatar>
+              <n-avatar v-else :size="48" round> {{ item.name.substring(0, 4) }} </n-avatar>
               <h2 class="text-gray-800 font-semibold">{{ item.name }}</h2>
             </div>
             <button
@@ -109,7 +114,7 @@
           <div class="p-4 pt-0">
             <p class="text-gray-600 text-sm">{{ item.des }}</p>
           </div>
-          <div class="py-5 px-4 border-t flex justify-between items-center">
+          <div class="py-3 px-4 border-t flex justify-between items-center">
             <div class="flex gap-1 items-center">
               <n-icon size="20"> <AntCloudOutlined /> </n-icon>
               <span>文件大小:20kb</span>

+ 4 - 3
langchat-ui/src/views/aigc/message/components/InfoList.vue

@@ -1,13 +1,14 @@
 <script lang="ts" setup>
   import { nextTick, ref } from 'vue';
   import { getMessages } from '@/api/aigc/conversation';
+  import Message from '@/views/aigc/chat/components/message/Message.vue';
 
   const messageRef = ref();
   const contentRef = ref();
   const loading = ref(true);
   const showModel = ref(false);
-  const info = ref({});
-  const messages = ref([]);
+  const info = ref<any>({});
+  const messages = ref<any>([]);
 
   async function show(row: any) {
     showModel.value = true;
@@ -37,7 +38,7 @@
             :error="false"
             :inversion="item.role !== 'assistant'"
             :loading="loading"
-            :text="item.content"
+            :text="item.message"
             @delete="handleDelete(item)"
           />
         </n-scrollbar>