hhhero 11 сар өмнө
parent
commit
f72e4f47f4

+ 13 - 2
src/api/ai/writer/index.ts

@@ -1,4 +1,3 @@
-import request from '@/config/axios'
 import { fetchEventSource } from '@microsoft/fetch-event-source'
 
 import { getAccessToken } from '@/utils/auth'
@@ -34,7 +33,19 @@ export interface WriteParams {
    */
   language: number
 }
-export const writeStream = (data: WriteParams, onMessage, onError, onClose, ctrl) => {
+export const writeStream = ({
+  data,
+  onClose,
+  onMessage,
+  onError,
+  ctrl
+}: {
+  data: WriteParams
+  onMessage?: (res: any) => void
+  onError?: (...args: any[]) => void
+  onClose?: (...args: any[]) => void
+  ctrl: AbortController
+}) => {
   // return request.post({ url: '/ai/write/generate-stream', data })
   const token = getAccessToken()
   return fetchEventSource(`${config.base_url}/ai/write/generate-stream`, {

+ 29 - 9
src/views/ai/writer/components/Left.vue

@@ -13,7 +13,11 @@
   <DefineLabel v-slot="{ label, hint, hintClick }">
     <h3 class="mt-5 mb-3 flex items-center justify-between text-[14px]">
       <span>{{ label }}</span>
-      <span @click="hintClick" v-if="hint" class="flex items-center text-[12px] text-[#846af7]">
+      <span
+        @click="hintClick"
+        v-if="hint"
+        class="flex items-center text-[12px] text-[#846af7] cursor-pointer select-none"
+      >
         <Icon icon="ep:question-filled" />
         {{ hint }}
       </span>
@@ -43,7 +47,7 @@
     >
       <div>
         <template v-if="selectedTab === 1">
-          <ReuseLabel label="写作内容" hint="示例" />
+          <ReuseLabel label="写作内容" hint="示例" :hint-click="() => example('write')" />
           <el-input
             type="textarea"
             :rows="5"
@@ -55,7 +59,7 @@
         </template>
 
         <template v-else>
-          <ReuseLabel label="原文" hint="示例" />
+          <ReuseLabel label="原文" hint="示例" :hint-click="() => example('reply')" />
           <el-input
             type="textarea"
             :rows="5"
@@ -86,8 +90,8 @@
         <Tag v-model="writeForm.language" :tags="writeTags.langTags" />
 
         <div class="flex items-center justify-center mt-3">
-          <el-button>重置</el-button>
-          <el-button @click="submit" color="#846af7">生成</el-button>
+          <el-button :disabled="isWriting">重置</el-button>
+          <el-button :loading="isWriting" @click="submit" color="#846af7">生成</el-button>
         </div>
       </div>
     </div>
@@ -97,18 +101,33 @@
 <script setup lang="ts">
   import { createReusableTemplate } from '@vueuse/core'
   import { ref } from 'vue'
-  import { ElMessage } from 'element-plus'
   import Tag from './Tag.vue'
   import { WriteParams } from '@/api/ai/writer'
   import { omit } from 'lodash-es'
   import { getIntDictOptions } from '@/utils/dict'
+  import dataJson from '../data.json'
 
   type TabType = WriteParams['type']
 
+  const message = useMessage()
+
+  defineProps<{
+    isWriting: boolean
+  }>()
+
   const emits = defineEmits<{
     (e: 'submit', params: Partial<WriteParams>)
+    (e: 'example', param: 'write' | 'reply')
   }>()
 
+  const example = (type: 'write' | 'reply') => {
+    writeForm.value = {
+      ...initData,
+      ...omit(dataJson[type], ['data'])
+    }
+    emits('example', type)
+  }
+
   const selectedTab = ref<TabType>(1)
   const tabs: {
     text: string
@@ -129,12 +148,13 @@
     originalContent: '',
     tone: 1,
     language: 1,
-    length: 100,
+    length: 1,
     format: 1
   }
   const writeForm = ref<WriteParams>({ ...initData })
 
   const writeTags = {
+    // 长度
     lenTags: getIntDictOptions('ai_write_length'),
     // 格式
 
@@ -161,10 +181,10 @@
 
   const submit = () => {
     if (selectedTab.value === 2 && !writeForm.value.originalContent) {
-      ElMessage.warning('请输入原文')
+      message.warning('请输入原文')
       return
     } else if (!writeForm.value.prompt) {
-      ElMessage.warning(`请输入${selectedTab.value === 1 ? '写作' : '回复'}内容`)
+      message.warning(`请输入${selectedTab.value === 1 ? '写作' : '回复'}内容`)
       return
     }
     emits('submit', {

+ 73 - 12
src/views/ai/writer/components/Right.vue

@@ -1,25 +1,86 @@
 <template>
   <div class="h-full box-border py-6 px-7">
-    <div class="w-full h-full bg-white box-border p-3 sm:p-16 overflow-y-auto">
-      <el-input
-        type="textarea"
-        :value="msg"
-        autosize
-        :input-style="{ boxShadow: 'none' }"
-        resize="none"
-        placeholder="生成的内容……"
-      />
+    <div class="w-full h-full relative bg-white box-border p-3 sm:p-16 pr-0">
+      <!-- 展示在右上角 -->
+      <el-button
+        color="#846af7"
+        v-show="showCopy"
+        @click="copyMsg"
+        class="absolute top-2 right-2 copy-btn"
+        :data-clipboard-target="inputId"
+      >
+        复制
+      </el-button>
+      <!-- 展示在下面中间的位置 -->
+      <el-button
+        v-show="isWriting"
+        class="absolute bottom-2 left-1/2 -translate-x-1/2"
+        @click="emits('stopStream')"
+      >
+        终止生成
+      </el-button>
+      <div ref="contentRef" class="w-full h-full pr-3 sm:pr-16 overflow-y-auto">
+        <el-input
+          id="inputId"
+          type="textarea"
+          v-model="compMsg"
+          autosize
+          :input-style="{ boxShadow: 'none' }"
+          resize="none"
+          placeholder="生成的内容……"
+        />
+      </div>
     </div>
   </div>
 </template>
 
 <script setup lang="ts">
-  defineProps({
+  import { useClipboard } from '@vueuse/core'
+  const message = useMessage()
+  const props = defineProps({
     msg: {
       type: String,
       default: ''
+    },
+    isWriting: {
+      type: Boolean,
+      default: false
     }
   })
-</script>
 
-<style scoped></style>
+  const emits = defineEmits(['update:msg', 'stopStream'])
+
+  const { copied, copy } = useClipboard()
+
+  const compMsg = computed({
+    get() {
+      return props.msg
+    },
+    set(val) {
+      emits('update:msg', val)
+    }
+  })
+
+  const showCopy = computed(() => props.msg && !props.isWriting)
+
+  const inputId = computed(() => getCurrentInstance()?.uid)
+
+  const contentRef = ref<HTMLDivElement>()
+  defineExpose({
+    scrollToBottom() {
+      contentRef.value?.scrollTo(0, contentRef.value?.scrollHeight)
+    }
+  })
+
+  // 点击复制的时候复制msg
+  const copyMsg = () => {
+    copy(props.msg)
+  }
+
+  watch(copied, (val) => {
+    console.log({ copied: val })
+    if (val) {
+      message.success('复制成功')
+    }
+  })
+</script>

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 3 - 0
src/views/ai/writer/data.json


+ 48 - 7
src/views/ai/writer/index.vue

@@ -1,7 +1,13 @@
 <template>
   <div class="h-[calc(100vh-var(--top-tool-height)-var(--app-footer-height)-40px)] -m-5 flex">
-    <Left class="h-full" @submit="submit" />
-    <Right class="flex-grow" :msg="msg" />
+    <Left :is-writing="isWriting" class="h-full" @submit="submit" @example="example" />
+    <Right
+      :is-writing="isWriting"
+      @stop-stream="stopStream"
+      ref="rightRef"
+      class="flex-grow"
+      v-model:msg="msgResult"
+    />
   </div>
 </template>
 
@@ -9,12 +15,47 @@
   import Left from './components/Left.vue'
   import Right from './components/Right.vue'
   import { writeStream } from '@/api/ai/writer'
+  import dataJson from './data.json'
 
-  const msg = ref('')
+  const message = useMessage()
+  const msgResult = ref('')
+  const isWriting = ref(false)
 
-  const submit = async (params) => {
-    const res = await writeStream(params)
+  const abortController = ref<AbortController>()
+
+  const stopStream = () => {
+    abortController.value?.abort()
+    isWriting.value = false
   }
-</script>
 
-<style scoped></style>
+  const rightRef = ref<InstanceType<typeof Right>>()
+
+  // 点击示例触发
+  const example = (type: keyof typeof dataJson) => {
+    msgResult.value = dataJson[type].data
+  }
+
+  const submit = async (data) => {
+    abortController.value = new AbortController()
+    msgResult.value = ''
+    isWriting.value = true
+    writeStream({
+      data,
+      onMessage: async (res) => {
+        const { code, data, msg } = JSON.parse(res.data)
+        if (code !== 0) {
+          message.alert(`写作异常! ${msg}`)
+          stopStream()
+          return
+        }
+        msgResult.value = msgResult.value + data
+        nextTick(() => {
+          rightRef.value?.scrollToBottom()
+        })
+      },
+      ctrl: abortController.value,
+      onClose: stopStream,
+      onError: stopStream
+    })
+  }
+</script>

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно