Bladeren bron

播放、点赞

CzRger 4 dagen geleden
bovenliggende
commit
85e226e052

+ 3 - 0
.gitignore

@@ -22,3 +22,6 @@ dist-ssr
 *.njsproj
 *.sln
 *.sw?
+smart-ask-answer-web/
+smart-ask-answer-web.zip
+stats.html

File diff suppressed because it is too large
+ 8 - 0
src/assets/svg/good.svg


+ 46 - 7
src/views/smart-ask-answer/assistant/chat.vue

@@ -6,7 +6,14 @@
           <askCom :item="item"/>
         </template>
         <template v-else>
-          <answerCom :item="item"/>
+          <answerCom
+            :item="item"
+            :goodMap="state.goodMap"
+            :badMap="state.badMap"
+            @onGood="onGood"
+            @onBad="onBad"
+            @onNormal="onNormal"
+          />
         </template>
       </template>
     </div>
@@ -43,6 +50,7 @@ import audioCom from './component/audio/index.vue'
 import {get, post, ssePost} from './dify/base'
 import {useRouter} from "vue-router";
 import {YMDHms} from "@/utils/czr-util";
+import {updateFeedback} from "@/views/smart-ask-answer/assistant/dify/share";
 
 const emit = defineEmits(['getText'])
 const router = useRouter()
@@ -62,7 +70,9 @@ const state: any = reactive({
       type: 'answer',
       welcome: true,
     },
-  ]
+  ],
+  goodMap: new Map(),
+  badMap: new Map(),
 })
 const ref_text = ref()
 const ref_chatMsg = ref()
@@ -78,7 +88,8 @@ const onSend = () => {
   state.chats.push(ask)
   const answer = reactive({
     type: 'answer',
-    content: ''
+    content: '',
+    messageId: ''
   })
   state.chats.push(answer)
   ssePost(`/installed-apps/${window.czrConfig.dify.appId}/chat-messages`, {
@@ -100,6 +111,7 @@ const onSend = () => {
         state.params.conversation_id = messageEnd.conversation_id
         post(`/installed-apps/${window.czrConfig.dify.appId}/conversations/${state.params.conversation_id}/name`, {body: {auto_generate: false, name: YMDHms(new Date())}})
       }
+      answer.messageId = messageEnd.message_id
       state.params.parent_message_id = messageEnd.message_id
       state.loading = false
     },
@@ -140,10 +152,6 @@ const initChat = () => {
           type: 'answer',
           welcome: true,
         },
-        {
-          type: 'answer',
-          content: 'asdasd',
-        },
       ]
     }
   })
@@ -151,6 +159,37 @@ const initChat = () => {
 const setText = (text: string) => {
   state.text = text
 }
+const onGood = (item) => {
+  updateFeedback({
+    url: `/messages/${item.messageId}/feedbacks`,
+    body: {rating: 'like'},
+  }, true, window.czrConfig.dify.appId).then((res: any) => {
+    if (res.result === 'success') {
+      state.goodMap.set(item.messageId, item)
+    }
+  })
+}
+const onBad = (item) => {
+  updateFeedback({
+    url: `/messages/${item.messageId}/feedbacks`,
+    body: {rating: 'dislike'},
+  }, true, window.czrConfig.dify.appId).then((res: any) => {
+    if (res.result === 'success') {
+      state.badMap.set(item.messageId, item)
+    }
+  })
+}
+const onNormal = (item) => {
+  updateFeedback({
+    url: `/messages/${item.messageId}/feedbacks`,
+    body: {rating: null},
+  }, true, window.czrConfig.dify.appId).then((res: any) => {
+    if (res.result === 'success') {
+      state.badMap.delete(item.messageId)
+      state.goodMap.delete(item.messageId)
+    }
+  })
+}
 onMounted(() => {
   initChat()
   ref_text.value.addEventListener('input', (e) => {

+ 67 - 0
src/views/smart-ask-answer/assistant/component/answer/index.vue

@@ -12,6 +12,52 @@
           </template>
           <template v-else>
             <div class="answer-markdown" v-html="md.render(part.content)"/>
+            <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/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/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>
 <!--            <div class="answer-markdown" v-html="DOMPurify.sanitize(marked.parse(part.content))"/>-->
           </template>
         </template>
@@ -28,7 +74,11 @@ 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';
 
+const { speak, stop } = useTextToSpeech();
 const md = new MarkdownIt({
   html: false,        // 在源码中启用 HTML 标签
   highlight: (str, lang) => {
@@ -42,6 +92,8 @@ const md = new MarkdownIt({
 });
 const props = defineProps({
   item: {} as any,
+  goodMap: {} as any,
+  badMap: {} as any,
 })
 const state: any = reactive({
 })
@@ -88,6 +140,10 @@ const contentCpt = computed(() => {
   }
   return segments;
 })
+const onCopy = (text) => {
+  copy(text)
+  ElMessage.success('复制成功!')
+}
 </script>
 
 <style lang="scss" scoped>
@@ -107,5 +163,16 @@ const contentCpt = computed(() => {
     .answer-markdown {
     }
   }
+  .answer-operation {
+    margin-top: 16px;
+    display: flex;
+    align-items: center;
+    gap: 12px;
+    .answer-operation-item {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+    }
+  }
 }
 </style>

+ 56 - 0
src/views/smart-ask-answer/assistant/component/answer/useTextToSpeech.ts

@@ -0,0 +1,56 @@
+// src/composables/useTextToSpeech.ts
+import { onUnmounted } from 'vue';
+
+export default function useTextToSpeech() {
+  const speech = new SpeechSynthesisUtterance();
+  let isPlaying = false;
+
+  // 过滤HTML标签
+  const stripHtml = (html: string): string => {
+    const doc = new DOMParser().parseFromString(html, 'text/html');
+    return doc.body.textContent || '';
+  };
+
+  // 播放文本
+  const speak = (text: string) => {
+    stop(); // 停止当前播放
+
+    const cleanText = stripHtml(text).trim();
+    if (!cleanText) {
+      console.warn('No valid text to speak');
+      return;
+    }
+
+    speech.text = cleanText;
+    isPlaying = true;
+
+    speech.onend = () => {
+      isPlaying = false;
+    };
+
+    speech.onerror = (event) => {
+      console.error('Speech error:', event);
+      isPlaying = false;
+    };
+
+    window.speechSynthesis.speak(speech);
+  };
+
+  // 停止播放
+  const stop = () => {
+    if (isPlaying) {
+      window.speechSynthesis.cancel();
+      isPlaying = false;
+    }
+  };
+
+  // 组件卸载时自动停止
+  onUnmounted(() => {
+    stop();
+  });
+
+  return {
+    speak,
+    stop
+  };
+}