CzRger 1 週間 前
コミット
b7534fb8fe
共有8 個のファイルを変更した1162 個の追加9580 個の削除を含む
  1. 0 8497
      package-lock.json
  2. 1 0
      package.json
  3. BIN
      src/assets/images/avatar-default.png
  4. 0 0
      src/assets/images/chat/default-logo.png
  5. 95 195
      src/views/chat/answer/index.vue
  6. 18 22
      src/views/chat/ask/index.vue
  7. 153 4
      src/views/chat/index.vue
  8. 895 862
      yarn.lock

File diff suppressed because it is too large
+ 0 - 8497
package-lock.json


+ 1 - 0
package.json

@@ -21,6 +21,7 @@
     "@antv/x6-vue-shape": "^2.1.2",
     "@tailwindcss/vite": "^4.1.8",
     "@types/node": "^24.0.0",
+    "@tinymce/tinymce-vue": "^6.2.0",
     "ant-design-vue": "4.x",
     "axios": "^1.9.0",
     "dagre": "^0.8.5",

BIN
src/assets/images/avatar-default.png


src/assets/images/model/model-default-logo-2.png → src/assets/images/chat/default-logo.png


+ 95 - 195
src/views/chat/answer/index.vue

@@ -1,16 +1,19 @@
 <template>
-  <div class="answer">
-    <div class="answer-avatar">
-      <img src="@/views/smart-ask-answer/assistant-2/imgs/avatar.png"/>
+  <div class="flex gap-2">
+    <div class="h-11 w-11">
+      <img class="h-full w-full" src="@/assets/images/chat/default-logo.png" />
     </div>
-    <div class="answer-content">
-      <template v-if="item.welcome">
-        <div class="answer-content-text">{{ item.content }}</div>
-        <div class="answer-content-question" v-if="item.question?.length > 0">
-          <template v-for="ques in item.question">
-            <div class="ques-item __hover" @click="$emit('setText', {text: ques, send: true})">{{ques}}</div>
-          </template>
-        </div>
+    <div class="flex flex-1 overflow-hidden">
+      <template v-if="item.prologue">
+        <div
+          class="rounded-lg rounded-tl-none bg-[#EAF1FF] p-2.75 text-[#303133]"
+          style="line-height: 1.5"
+          v-html="item.prologue"
+        />
+        <template v-if="item.prologueQuestions?.length > 0">
+          <template v-if="item.prologueType === 'three'"></template>
+        </template>
+        <template v-for="pq in item.prologueQuestions"> </template>
       </template>
       <template v-else-if="item.loading">
         <div class="answer-loading">
@@ -20,147 +23,94 @@
         </div>
       </template>
       <template v-else>
-        <template v-for="part in contentCpt">
-          <template v-if="part.type === 'think'">
-            <thinkCom :content="part.content"/>
-          </template>
-          <template v-else>
-            <div class="answer-markdown" v-html="md.render(part.content.replace(/^<think>/, ''))"/>
-            <div class="answer-operation">
-              <el-tooltip content="播放" placement="top">
-                <div class="answer-operation-item __hover" @click="speak(part.content)">
-                  <img src="@/views/smart-ask-answer/assistant-2/imgs/play.png"/>
-                </div>
-              </el-tooltip>
-              <el-tooltip content="复制" placement="top">
-                <div class="answer-operation-item __hover" @click="onCopy(part.content)">
-                  <img src="@/views/smart-ask-answer/assistant-2/imgs/copy.png"/>
-                </div>
-              </el-tooltip>
-              <template v-if="item.messageId">
-                <template v-if="!badMap.has(item.messageId)">
-                  <template v-if="goodMap.has(item.messageId)">
-                    <el-tooltip content="取消点赞" placement="top">
-                      <div class="answer-operation-item __hover" @click="$emit('onNormal', item)">
-                        <SvgIcon name="good" color="#2264f0" size="20"/>
-                      </div>
-                    </el-tooltip>
-                  </template>
-                  <template v-else>
-                    <el-tooltip content="点赞" placement="top">
-                      <div class="answer-operation-item __hover" @click="$emit('onGood', item)">
-                        <SvgIcon name="good" color="#a6a6a6" size="20"/>
-                      </div>
-                    </el-tooltip>
-                  </template>
-                </template>
-                <template v-if="!goodMap.has(item.messageId)">
-                  <template v-if="badMap.has(item.messageId)">
-                    <el-tooltip content="取消点踩" placement="top">
-                      <div class="answer-operation-item __hover" @click="$emit('onNormal', item)">
-                        <SvgIcon name="good" color="#d92d20" size="20" rotate="180"/>
-                      </div>
-                    </el-tooltip>
-                  </template>
-                  <template v-else>
-                    <el-tooltip content="点踩" placement="top">
-                      <div class="answer-operation-item __hover" @click="$emit('onBad', item)">
-                        <SvgIcon name="good" color="#a6a6a6" size="20" rotate="180"/>
-                      </div>
-                    </el-tooltip>
-                  </template>
-                </template>
-              </template>
-            </div>
-            <template v-if="item.suggest?.length > 0">
-              <div class="answer-suggest">
-                相似问题
-                <template v-for="sug in item.suggest">
-                  <div class="__hover" @click="$emit('setText', {text: sug, send: true})">{{sug}}</div>
-                </template>
-              </div>
+        <div
+          class="rounded-lg rounded-tl-none bg-[#EAF1FF] p-2.75 text-[#303133]"
+        >
+          <template v-for="part in textCpt">
+            <template v-if="part.type === 'think'">
+              <thinkCom :text="part.text" />
+            </template>
+            <template v-else>
+              <div
+                class="answer-markdown"
+                v-html="md.render(part.text.replace(/^<think>/, ''))"
+              />
             </template>
-<!--            <div class="answer-markdown" v-html="DOMPurify.sanitize(marked.parse(part.content))"/>-->
           </template>
-        </template>
+        </div>
+        <div></div>
+        <div></div>
       </template>
     </div>
   </div>
-
 </template>
 
 <script setup lang="ts">
-import {computed, getCurrentInstance, onMounted, reactive, watch} from "vue";
-import MarkdownIt from 'markdown-it';
-import hljs from 'highlight.js';
-import { marked } from 'marked';
-import DOMPurify from 'dompurify';
+import { computed, getCurrentInstance, onMounted, reactive, watch } from 'vue'
+import MarkdownIt from 'markdown-it'
+import hljs from 'highlight.js'
+import { marked } from 'marked'
+import DOMPurify from 'dompurify'
 import thinkCom from './think.vue'
-import {copy} from "@/utils/czr-util";
-import {ElMessage} from "element-plus";
-import useTextToSpeech from './useTextToSpeech';
+import { copy } from '@/utils/czr-util'
+import { ElMessage } from 'element-plus'
+import useTextToSpeech from './useTextToSpeech'
 
-const { speak, stop } = useTextToSpeech();
+const { speak, stop } = useTextToSpeech()
 const md = new MarkdownIt({
-  html: false,        // 在源码中启用 HTML 标签
+  html: false, // 在源码中启用 HTML 标签
   highlight: (str, lang) => {
     if (lang && hljs.getLanguage(lang)) {
       try {
-        return `<pre class="hljs"><code>${hljs.highlight(str, { language: lang }).value}</code></pre>`;
+        return `<pre class="hljs"><code>${hljs.highlight(str, { language: lang }).value}</code></pre>`
       } catch (__) {}
     }
-    return `<pre class="hljs"><code>${md.utils.escapeHtml(str)}</code></pre>`;
-  }
-});
+    return `<pre class="hljs"><code>${md.utils.escapeHtml(str)}</code></pre>`
+  },
+})
 const props = defineProps({
   item: {} as any,
   goodMap: {} as any,
   badMap: {} as any,
 })
-const state: any = reactive({
-})
+const state: any = reactive({})
 
-const parsedContent = computed(() => {
-  // 先解析Markdown,再净化HTML
-  return DOMPurify.sanitize(marked.parse(props.item.content));
-});
-
-const contentCpt = computed(() => {
-  const segments: any = [];
-  const rawContent = props.item.content
+const textCpt = computed(() => {
+  const segments: any = []
+  const rawContent = props.item.text
 
   // 正则表达式匹配<think>标签及其内容
-  const thinkRegex = /<think>([\s\S]*?)<\/think>/g;
+  const thinkRegex = /<think>([\s\S]*?)<\/think>/g
 
-  let match;
-  let lastIndex = 0;
+  let match
+  let lastIndex = 0
 
   while ((match = thinkRegex.exec(rawContent)) !== null) {
     // 添加think标签前的普通内容
     if (match.index > lastIndex) {
       segments.push({
         type: 'response',
-        content: rawContent.substring(lastIndex, match.index)
-      });
+        text: rawContent.substring(lastIndex, match.index),
+      })
     }
 
     // 添加think内容
     segments.push({
       type: 'think',
-      content: match[1].trim()
-    });
+      text: match[1].trim(),
+    })
 
-    lastIndex = thinkRegex.lastIndex;
+    lastIndex = thinkRegex.lastIndex
   }
 
   // 添加剩余内容
   if (lastIndex < rawContent.length) {
     segments.push({
       type: 'response',
-      content: rawContent.substring(lastIndex).trim()
-    });
+      text: rawContent.substring(lastIndex).trim(),
+    })
   }
-  return segments;
+  return segments
 })
 const onCopy = (text) => {
   copy(text)
@@ -169,100 +119,50 @@ const onCopy = (text) => {
 </script>
 
 <style lang="scss" scoped>
-.answer {
-  width: 100%;
-  display: flex;
-  .answer-avatar {
-    margin-right: 16px;
+.answer-loading {
+  width: 50px;
+  height: 100%;
+  display: inline-flex;
+  gap: 6px;
+  align-items: center;
+  justify-content: center;
+
+  span {
+    display: inline-block;
+    width: 8px;
+    height: 8px;
+    border-radius: 50%;
+    background-color: #6c9eff;
+    animation: ellipsis-bounce 1.4s infinite ease-in-out;
   }
-  .answer-content {
-    flex: 1;
-    overflow: hidden;
-    font-weight: 400;
-    font-size: 16px;
-    color: #111111;
-    .answer-loading {
-      width: 50px;
-      height: 100%;
-      display: inline-flex;
-      gap: 6px;
-      align-items: center;
-      justify-content: center;
-
-      span {
-        display: inline-block;
-        width: 8px;
-        height: 8px;
-        border-radius: 50%;
-        background-color: #1d64fd;
-        animation: ellipsis-bounce 1.4s infinite ease-in-out;
-      }
-
-      span:nth-child(1) {
-        animation-delay: -0.32s;
-      }
 
-      span:nth-child(2) {
-        animation-delay: -0.16s;
-      }
+  span:nth-child(1) {
+    animation-delay: -0.32s;
+  }
 
-      @keyframes ellipsis-bounce {
-        0%, 80%, 100% {
-          transform: translateY(0);
-        }
-        40% {
-          transform: translateY(-10px);
-        }
-      }
-    }
-    .answer-content-text {
-      margin-top: 16px;
-    }
-    .answer-content-question {
-      margin-top: 10px;
-      display: flex;
-      flex-wrap: wrap;
-      gap: 4px;
-      .ques-item {
-        padding: 6px 14px;
-        border-radius: 8px;
-        border: 1px solid #10182824;
-        font-size: 14px;
-        color: #1D64FD;
-      }
-    }
+  span:nth-child(2) {
+    animation-delay: -0.16s;
   }
-  .answer-markdown {
-    line-height: 1.8;
-    >* {
-      margin-bottom: 12px;
-      &:last-child {
-        margin-bottom: 0;
-      }
+
+  @keyframes ellipsis-bounce {
+    0%,
+    80%,
+    100% {
+      transform: translateY(0);
     }
-  }
-  .answer-operation {
-    margin-top: 16px;
-    display: flex;
-    align-items: center;
-    gap: 12px;
-    .answer-operation-item {
-      display: flex;
-      align-items: center;
-      justify-content: center;
+    40% {
+      transform: translateY(-10px);
     }
   }
-  .answer-suggest {
-    margin-top: 14px;
-    padding-top: 14px;
-    border-top: 1px solid #D8DAE5;
-    display: flex;
-    flex-direction: column;
-    gap: 2px;
-    font-size: 14px;
-    color: #9A9CA6;
-    >div {
-      color: #1D64FD;
+}
+.answer-markdown {
+  line-height: 1.5;
+  > * {
+    text-align: justify;
+    line-height: inherit;
+    margin-bottom: 12px;
+    &:last-child {
+      margin-bottom: 0;
     }
   }
 }

+ 18 - 22
src/views/chat/ask/index.vue

@@ -1,34 +1,30 @@
 <template>
-  <div class="ask">
-    {{contentCpt}}
+  <div class="flex gap-2">
+    <div class="flex flex-1 justify-end overflow-hidden">
+      <div
+        class="rounded-lg rounded-tr-none bg-[#6C9EFF] p-2.75 text-justify text-[#ffffff]"
+        style="line-height: 1.5"
+      >
+        {{ textCpt }}
+      </div>
+    </div>
+    <div class="h-11 w-11">
+      <img class="h-full w-full" src="@/assets/images/avatar-default.png" />
+    </div>
   </div>
 </template>
 
 <script setup lang="ts">
-import {computed, getCurrentInstance, onMounted, reactive, watch} from "vue";
+import { computed, getCurrentInstance, onMounted, reactive, watch } from 'vue'
 
 const props = defineProps({
   item: {} as any,
 })
-const contentCpt = computed(() => {
-  return props.item.content
-})
-const state: any = reactive({
-})
-onMounted(() => {
+const textCpt = computed(() => {
+  return props.item.text
 })
+const state: any = reactive({})
+onMounted(() => {})
 </script>
 
-<style lang="scss" scoped>
-.ask {
-  background: #F2F2F5;
-  border-radius: 8px;
-  padding: 16px;
-  font-family: Microsoft YaHei;
-  font-weight: 400;
-  font-size: 16px;
-  color: #111111;
-  width: auto;
-  margin-left: auto;
-}
-</style>
+<style lang="scss" scoped></style>

+ 153 - 4
src/views/chat/index.vue

@@ -1,13 +1,162 @@
 <template>
-  <div class="h-full w-full bg-[#ffffff] p-4">
-    <div>聊天内容</div>
+  <div class="flex h-full w-full flex-col bg-[#ffffff] p-4">
+    <div class="flex flex-1 flex-col gap-4" ref="ref_chatMsg">
+      <template v-for="(item, index) in state.chats">
+        <template v-if="item.type === 'ask'">
+          <askCom :item="item" />
+        </template>
+        <template v-else>
+          <answerCom
+            :item="item"
+            :goodMap="state.goodMap"
+            :badMap="state.badMap"
+            @onGood="onGood"
+            @onBad="onBad"
+            @onNormal="onNormal"
+            @setText="(val) => setText(val.text, val.send)"
+          />
+        </template>
+      </template>
+    </div>
+    <!--    <div class="chat-input">-->
+    <!--      <div class="chat-input-block" v-loading="state.loading">-->
+    <!--        <div class="chat-input-block-main">-->
+    <!--          <div class="chat-input-block-main-auto" ref="ref_auto">-->
+    <!--            <div class="chat-input-block-main-auto-list">-->
+    <!--              <template v-for="item in state.autoList">-->
+    <!--                <div-->
+    <!--                  class="chat-input-block-main-auto-item __hover"-->
+    <!--                  @click="setText(item)"-->
+    <!--                  v-html="-->
+    <!--                    item.replace(-->
+    <!--                      new RegExp(state.text, 'g'),-->
+    <!--                      `<span style='color: #d32520;'>$&</span>`,-->
+    <!--                    )-->
+    <!--                  "-->
+    <!--                />-->
+    <!--              </template>-->
+    <!--            </div>-->
+    <!--          </div>-->
+    <!--          <textarea-->
+    <!--            ref="ref_text"-->
+    <!--            placeholder="请输入您的问题"-->
+    <!--            :rows="3"-->
+    <!--            v-model="state.text"-->
+    <!--          />-->
+    <!--        </div>-->
+    <!--        <div class="chat-input-block-operations">-->
+    <!--          <div class="cibo-audio">-->
+    <!--            <audioCom @onLoading="onLoading" @onAudio="onAudio" />-->
+    <!--          </div>-->
+    <!--          <div class="cibo-split" />-->
+    <!--          <template-->
+    <!--            v-if="state.waiting && state.chats[state.chats.length - 1]?.taskId"-->
+    <!--          >-->
+    <!--            <el-tooltip content="停止生成" placement="top">-->
+    <!--              <div class="cibo-send __hover" @click="onStop()">-->
+    <!--                <div-->
+    <!--                  style="-->
+    <!--                    width: 13px;-->
+    <!--                    height: 13px;-->
+    <!--                    border-radius: 2px;-->
+    <!--                    background-color: #ffffff;-->
+    <!--                  "-->
+    <!--                />-->
+    <!--              </div>-->
+    <!--            </el-tooltip>-->
+    <!--          </template>-->
+    <!--          <template v-else>-->
+    <!--            <el-tooltip content="发送" placement="top">-->
+    <!--              <div class="cibo-send __hover" @click="onSend()">-->
+    <!--                <img src="@/views/smart-ask-answer/assistant-2/imgs/send.png" />-->
+    <!--              </div>-->
+    <!--            </el-tooltip>-->
+    <!--          </template>-->
+    <!--        </div>-->
+    <!--      </div>-->
+    <!--    </div>-->
   </div>
 </template>
 
 <script setup lang="ts">
-import { reactive } from 'vue'
+import { onMounted, reactive } from 'vue'
+import askCom from './ask/index.vue'
+import answerCom from './answer/index.vue'
 
-const state: any = reactive({})
+const state: any = reactive({
+  chats: [],
+  chatConfig: {},
+})
+const onGood = () => {}
+const onBad = () => {}
+const onNormal = () => {}
+const setText = (text: string, send = false) => {
+  // if (send) {
+  //   onSend(text)
+  // } else {
+  //   state.text = text
+  // }
+}
+const initChat = () => {
+  state.chatConfig = {}
+  state.chats = [
+    {
+      type: 'answer',
+      prologue: '您好,请不要问我问题!',
+      prologueType: 'three',
+      prologueQuestions: ['马什么每', '马东什么', '什么冬梅'],
+    },
+    {
+      type: 'answer',
+      prologue: '您好,请不要问我问题!',
+      prologueType: 'all',
+      prologueQuestions: [
+        '马什么每',
+        '马东什么',
+        '什么冬梅',
+        '马什么每',
+        '马东什么',
+        '什么冬梅',
+        '马什么每',
+        '马东什么',
+        '什么冬梅',
+        '马什么每',
+        '马东什么',
+        '什么冬梅',
+      ],
+    },
+    {
+      type: 'ask',
+      text: '现在几点了?',
+    },
+    {
+      type: 'answer',
+      text: '现在是5025年88月99日 77:55:44,星期九。',
+      advise: [],
+    },
+    {
+      type: 'answer',
+      text: '',
+      advise: [],
+      loading: true,
+    },
+    {
+      type: 'ask',
+      text: '还有别的问题建议吗?还有别的问题建议吗?还有别的问题建议吗?还有别的问题建议吗?还有别的问题建议吗?还有别的问题建议吗?还有别的问题建议吗?还有别的问题建议吗?还有别的问题建议吗?还有别的问题建议吗?还有别的问题建议吗?还有别的问题建议吗?还有别的问题建议吗?还有别的问题建议吗?还有别的问题建议吗?',
+    },
+    {
+      type: 'answer',
+      text: '还有一些问题建议',
+      advise: ['这个时间牛*不', '再问我一个时间吧'],
+    },
+  ]
+  initHistory()
+}
+const initHistory = () => {}
+onMounted(() => {
+  initChat()
+  // initTextHandle()
+})
 </script>
 
 <style lang="scss" scoped></style>

File diff suppressed because it is too large
+ 895 - 862
yarn.lock