CzRger недель назад: 2
Родитель
Сommit
5f7ef3ef40

BIN
src/assets/images/workflow/icon-start.png


Разница между файлами не показана из-за своего большого размера
+ 1 - 0
src/assets/svg/failed.svg


+ 2 - 0
src/types/chat.ts

@@ -18,4 +18,6 @@ export type AnswerStruct = {
   mock?: boolean //  是否为自己模拟的回答,不需要回答后的额外功能
   slot?: any //  自定义组件的插槽名称
   slotProps?: any //  自定义组件的插槽参数
+  workflows?: Array<any> //  工作流步骤
+  workflowStatus?: 'succeeded' | 'failed' //  工作流步骤状态
 }

+ 135 - 3
src/views/chat/answer/index.vue

@@ -5,7 +5,11 @@
     </div>
     <div
       class="flex flex-1 flex-col overflow-hidden"
-      :class="item.slot ? '' : 'max-w-fit'"
+      :class="
+        (item.slot ? '' : 'max-w-fit') +
+        ' ' +
+        (item.workflows?.length > 0 ? 'w-full max-w-full' : '')
+      "
     >
       <template v-if="item.prologue">
         <div
@@ -52,13 +56,138 @@
       </template>
       <template v-else>
         <div
-          class="w-fit max-w-full rounded-lg rounded-tl-none bg-[#EAF1FF] p-2.75 text-[#303133]"
+          class="max-w-full rounded-lg rounded-tl-none bg-[#EAF1FF] p-2.75 text-[#303133]"
+          :class="item.workflows?.length > 0 ? 'w-full' : 'w-fit'"
         >
           <template v-for="part in textCpt">
             <template v-if="part.type === 'think'">
               <thinkCom :text="part.text" />
             </template>
             <template v-else>
+              <template v-if="item.workflows?.length > 0">
+                <div
+                  class="flex flex-col rounded-sm p-2 text-xs"
+                  :class="
+                    item.workflowStatus === 'succeeded'
+                      ? 'bg-[var(--czr-success-color)]/15'
+                      : item.workflowStatus === 'failed'
+                        ? 'bg-[var(--czr-error-color)]/15'
+                        : 'bg-[#ffffff]/50'
+                  "
+                >
+                  <div
+                    class="__hover flex items-center"
+                    @click="state.workflowExpend = !state.workflowExpend"
+                  >
+                    <template v-if="item.workflowStatus === 'succeeded'">
+                      <SvgIcon
+                        name="success"
+                        size="14"
+                        color="var(--czr-success-color)"
+                      />
+                    </template>
+                    <template v-else-if="item.workflowStatus === 'failed'">
+                      <SvgIcon
+                        name="failed"
+                        size="14"
+                        color="var(--czr-error-color)"
+                      />
+                    </template>
+                    <template v-else>
+                      <SvgIcon name="wait" size="14" />
+                    </template>
+                    <div class="ml-0.5">工作流</div>
+                    <template v-if="state.workflowExpend">
+                      <SvgIcon
+                        name="czr_arrow"
+                        size="8"
+                        rotate="90"
+                        class="ml-auto"
+                      />
+                    </template>
+                    <template v-else>
+                      <SvgIcon name="czr_arrow" size="8" class="ml-1" />
+                    </template>
+                  </div>
+                  <div class="mt-2 flex flex-col gap-1">
+                    <template v-for="wf in item.workflows">
+                      <div class="rounded-sm bg-[#ffffff] p-1">
+                        <div
+                          class="flex items-center"
+                          :class="wf.status ? '__hover' : 'cursor-no-drop'"
+                          @click="
+                            wf.status ? (wf.__expend = !wf.__expend) : undefined
+                          "
+                        >
+                          <template v-if="wf.__expend">
+                            <SvgIcon name="czr_arrow" size="8" rotate="90" />
+                          </template>
+                          <template v-else>
+                            <SvgIcon name="czr_arrow" size="8" />
+                          </template>
+                          <img
+                            :src="nodeSources[wf.nodeType].icon"
+                            class="ml-1 size-4"
+                          />
+                          <div class="ml-0.5 flex-1 font-bold" v-title>
+                            {{ wf.title }}
+                          </div>
+                          <div class="ml-auto flex items-center">
+                            <template v-if="wf.status === 'succeeded'">
+                              <SvgIcon
+                                name="success"
+                                size="12"
+                                color="var(--czr-success-color)"
+                              />
+                            </template>
+                            <template v-else-if="wf.status === 'failed'">
+                              <SvgIcon
+                                name="failed"
+                                size="12"
+                                color="var(--czr-error-color)"
+                              />
+                            </template>
+                            <template v-else>
+                              <div class="mr-1 text-[var(--czr-main-color)]/70">
+                                Running
+                              </div>
+                              <SvgIcon name="wait" size="12" />
+                            </template>
+                          </div>
+                        </div>
+                        <div
+                          v-if="wf.__expend"
+                          class="mt-1 flex flex-col gap-1"
+                        >
+                          <div
+                            v-if="wf.error"
+                            class="rounded-sm border-1 border-[var(--czr-error-color)] p-1 text-[var(--czr-error-color)] shadow"
+                          >
+                            <div>错误信息</div>
+                            <div class="mt-1 break-words">
+                              {{ wf.error }}
+                            </div>
+                          </div>
+                          <div
+                            v-if="wf.inputData"
+                            class="rounded-sm border-1 border-[#000000]/5 p-1 shadow"
+                          >
+                            <div>输入</div>
+                            <div class="mt-1">{{ wf.inputData }}</div>
+                          </div>
+                          <div
+                            v-if="wf.outputData"
+                            class="rounded-sm border-1 border-[#000000]/5 p-1 shadow"
+                          >
+                            <div>输出</div>
+                            <div class="mt-1">{{ wf.outputData }}</div>
+                          </div>
+                        </div>
+                      </div>
+                    </template>
+                  </div>
+                </div>
+              </template>
               <div
                 class="answer-markdown"
                 :class="{ error: item.error }"
@@ -176,6 +305,7 @@ import thinkCom from './think.vue'
 import { copy } from '@/utils/czr-util'
 import { ElMessage } from 'element-plus'
 import useTextToSpeech from '../audio/useTextToSpeech'
+import { nodeSources } from '@/views/workflow/config'
 
 const { speak, stop } = useTextToSpeech()
 const md = new MarkdownIt({
@@ -195,7 +325,9 @@ const props = defineProps({
   goodMap: {} as any,
   badMap: {} as any,
 })
-const state: any = reactive({})
+const state: any = reactive({
+  workflowExpend: false,
+})
 const textCpt = computed(() => {
   const segments: any = []
   const rawContent = props.item.text

+ 19 - 5
src/views/chat/chat.ts

@@ -20,9 +20,13 @@ export const chatMessage = (params, funcs) => {
 
 const handleStream = ({
   response,
+  onInit,
   onData,
   onMessageEnd,
-  onStart,
+  onWorkflowStart,
+  onWorkflowEnd,
+  onNodeStart,
+  onNodeEnd,
   onEnd,
   onError,
 }) => {
@@ -30,12 +34,12 @@ const handleStream = ({
     onError?.(`${response.status}:${response.statusText}`, response)
     throw new Error('Network response was not ok')
   }
-
   const reader = response.body?.getReader()
   const decoder = new TextDecoder('utf-8')
   let buffer = ''
   let bufferObj: any = {}
   const dataFlag = 'data:'
+  let isInit = false
   const read = () => {
     let hasError = false
     reader?.read().then((result: any) => {
@@ -54,12 +58,22 @@ const handleStream = ({
               return
             }
             const { event } = bufferObj
-            if (event === 'chatStart') {
-              onStart?.(bufferObj)
-            } else if (event === 'message') {
+            if (!isInit) {
+              onInit(bufferObj)
+              isInit = true
+            }
+            if (event === 'message') {
               onData(unicodeToChar(bufferObj.answer), bufferObj)
             } else if (event === 'messageEnd') {
               onMessageEnd(bufferObj)
+            } else if (event === 'workflowStarted') {
+              onWorkflowStart(bufferObj)
+            } else if (event === 'workflowFinished') {
+              onWorkflowEnd(bufferObj)
+            } else if (event === 'nodeStarted') {
+              onNodeStart(bufferObj)
+            } else if (event === 'nodeFinished') {
+              onNodeEnd(bufferObj)
             } else if (event === 'error') {
               ElNotification({
                 title: bufferObj.errorType,

+ 18 - 1
src/views/chat/normal.vue

@@ -254,6 +254,7 @@ const onSend = (text = '', isSet = false) => {
       finished: false,
       time: 0,
       tokens: 0,
+      workflows: [],
     })
     state.chats.push(answer)
     scrollToEnd()
@@ -266,12 +267,14 @@ const onSend = (text = '', isSet = false) => {
       }
     }
     chatMessage(p, {
-      onData: (text, data) => {
+      onInit: (data) => {
         state.isWaiting = false
         state.params.conversationId = data.conversationId
         answer.messageId = data.messageId
         answer.taskId = data.taskId
         answer.loading = false
+      },
+      onData: (text, data) => {
         answer.text += text
         scrollToEnd()
       },
@@ -296,6 +299,20 @@ const onSend = (text = '', isSet = false) => {
           }
         }
       },
+      onWorkflowStart: (data) => {},
+      onWorkflowEnd: (data) => {
+        answer.workflowStatus = data.data.status
+      },
+      onNodeStart: (data) => {
+        answer.workflows?.push(data.data)
+      },
+      onNodeEnd: (data) => {
+        answer.workflows?.forEach((v) => {
+          if (v.nodeId === data.data.nodeId) {
+            Object.assign(v, data.data)
+          }
+        })
+      },
       onError: (text, data) => {
         state.isWaiting = false
         answer.loading = false

+ 3 - 0
src/views/workflow/config.ts

@@ -8,6 +8,8 @@ import {
 } from '@/views/workflow/types'
 import startNodeDefault from '@/views/workflow/instance/start/default'
 // @ts-ignore
+import startImg from '@/assets/images/workflow/icon-start.png'
+// @ts-ignore
 import answerImg from '@/assets/images/workflow/icon-answer.png'
 import answerNodeDefault from '@/views/workflow/instance/answer/default'
 // @ts-ignore
@@ -49,6 +51,7 @@ export const nodeSources = {
           ...startNodeDefault.defaultValue(),
         },
       },
+    icon: startImg,
     nodeCom: defineAsyncComponent(
       () => import('@/views/workflow/instance/start/node/index.vue'),
     ),