Pārlūkot izejas kodu

AI生成章节内容

Kevin Jiang 2 gadi atpakaļ
vecāks
revīzija
ea51f8ed14

+ 18 - 0
package-lock.json

@@ -13,6 +13,7 @@
         "@types/lodash": "^4.14.188",
         "ant-design-vue": "^3.2.13",
         "axios": "^1.1.3",
+        "marked": "^5.0.1",
         "nzh": "^1.0.8",
         "pinia": "^2.0.21",
         "tinymce": "^6.3.1",
@@ -5847,6 +5848,18 @@
       "integrity": "sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==",
       "dev": true
     },
+    "node_modules/marked": {
+      "version": "5.0.1",
+      "resolved": "https://repo.huaweicloud.com/repository/npm/marked/-/marked-5.0.1.tgz",
+      "integrity": "sha512-Nn9peC4lvIZdcfp8Uze6xk4ZYowkcj/K6+e/6rLHadhtjqeip/bYRxMgt3124IGGJ3RPs2uX5YVmAGbUutY18g==",
+      "license": "MIT",
+      "bin": {
+        "marked": "bin/marked.js"
+      },
+      "engines": {
+        "node": ">= 18"
+      }
+    },
     "node_modules/memorystream": {
       "version": "0.3.1",
       "resolved": "https://registry.npmmirror.com/memorystream/-/memorystream-0.3.1.tgz",
@@ -13167,6 +13180,11 @@
       "integrity": "sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==",
       "dev": true
     },
+    "marked": {
+      "version": "5.0.1",
+      "resolved": "https://repo.huaweicloud.com/repository/npm/marked/-/marked-5.0.1.tgz",
+      "integrity": "sha512-Nn9peC4lvIZdcfp8Uze6xk4ZYowkcj/K6+e/6rLHadhtjqeip/bYRxMgt3124IGGJ3RPs2uX5YVmAGbUutY18g=="
+    },
     "memorystream": {
       "version": "0.3.1",
       "resolved": "https://registry.npmmirror.com/memorystream/-/memorystream-0.3.1.tgz",

+ 1 - 0
package.json

@@ -18,6 +18,7 @@
     "@types/lodash": "^4.14.188",
     "ant-design-vue": "^3.2.13",
     "axios": "^1.1.3",
+    "marked": "^5.0.1",
     "nzh": "^1.0.8",
     "pinia": "^2.0.21",
     "tinymce": "^6.3.1",

+ 11 - 0
src/client/ai.client.ts

@@ -0,0 +1,11 @@
+import httpClient from "@/services/httpClient";
+import type { Response } from "@/types/response.types";
+import * as urlHelper from "@/libs/url.lib"
+
+const _url = urlHelper.withPrefix("/gw/user")
+
+export async function generate(projectName: string, keywords: string, chapterTitle: string) {
+  const params = { projectName, keywords, chapterTitle }
+  const resp = await httpClient.post<Response<string>>(_url("ai/generate"), params);
+  return resp.data.data;
+}

+ 0 - 10
src/client/test.client.ts

@@ -1,10 +0,0 @@
-import httpClient from "@/services/httpClient";
-import type { Response } from "@/types/response.types";
-import * as urlHelper from "@/libs/url.lib"
-
-const _url = urlHelper.withPrefix("/gw/user/test")
-
-export async function test1(text: string) {
-  const resp = await httpClient.post<Response<string>>(_url("1"), { text });
-  return resp.data.data;
-}

+ 0 - 5
src/router/index.ts

@@ -21,11 +21,6 @@ export const router = createRouter({
           name: "ReportList",
           component: () => import("../views/report/ReportListView.vue"),
         },
-        // {
-        //   path: "create",
-        //   name: "ReportCreate",
-        //   component: () => import("../views/report/ReportCreateView.vue"),
-        // },
         {
           path: "edit/:id",
           name: "ReportEditor",

+ 5 - 0
src/services/ai.service.ts

@@ -0,0 +1,5 @@
+import * as aiClient from "@/client/ai.client";
+
+export async function generate(projectName: string, keywords: string, chapterTitle: string) {
+  return aiClient.generate(projectName, keywords, chapterTitle)
+}

+ 1 - 1
src/services/httpClient.ts

@@ -7,7 +7,7 @@ const urlPrefix = "/";
 
 const httpClient = axios.create({
   baseURL: urlPrefix,
-  timeout: 60000,
+  timeout: 300000,
 });
 
 

+ 0 - 5
src/services/test.service.ts

@@ -1,5 +0,0 @@
-import * as testClient from "@/client/test.client";
-
-export async function test1(text: string) {
-  return testClient.test1(text)
-}

+ 27 - 54
src/views/report/components/ChapterEditor.vue

@@ -1,13 +1,14 @@
 <!-- 章节编辑器 -->
 
 <script setup lang="ts">
-import { computed, ref, watch, type PropType } from 'vue';
+import { ref, watch, type PropType } from 'vue';
 import { EditOutlined, DownOutlined } from '@ant-design/icons-vue';
+import { marked } from "marked";
 import type { Chapter } from '@/types/report.types';
 import ContentEditor from "../components/ContentEditor.vue";
 import ReferenceDraw from "../components/reference/ReferenceDraw.vue";
-import { useRoute } from 'vue-router';
-import * as testService from "@/services/test.service"
+import AIContentDraw from "../components/reference/AIContentDraw.vue";
+import * as aiService from "@/services/ai.service"
 import { message } from 'ant-design-vue';
 
 const props = defineProps({
@@ -30,10 +31,6 @@ const props = defineProps({
   }
 });
 
-const route = useRoute()
-
-const isAI = computed(() => route.query.ai)
-
 const titleEditor = ref<HTMLInputElement | null>(null);
 
 const titleEditing = ref(false);
@@ -51,6 +48,8 @@ const fullTitle = ref('');
 const referenceDraw = ref<InstanceType<typeof ReferenceDraw> | null>(null);
 // 内容编辑器组件ref
 const contentEditor = ref<InstanceType<typeof ContentEditor> | null>(null)
+// AI内容展示组件ref
+const aiContentDraw = ref<InstanceType<typeof ReferenceDraw> | null>(null);
 
 const emits = defineEmits<{
   (e: "update:data", data: Chapter): void;
@@ -87,12 +86,12 @@ function onEditKeywords() {
 
 function onTitleChange() {
   titleEditing.value = false;
-  emits("change", {...props.data, title: fullTitle.value})
+  emits("change", { ...props.data, title: fullTitle.value })
 }
 
 function onKeywordsChange() {
   keywordsEditing.value = false;
-  emits("change", {...props.data, keywords: keywordsInChapter.value.split(",")})
+  emits("change", { ...props.data, keywords: keywordsInChapter.value.split(",") })
 }
 
 function onTitleChangeCancel() {
@@ -100,13 +99,13 @@ function onTitleChangeCancel() {
   titleEditing.value = false;
 }
 
-function onKeywordsChangeCancel(){
+function onKeywordsChangeCancel() {
   keywordsInChapter.value = props.data.keywords.join(",");
   keywordsEditing.value = false;
 }
 
 function onContentChange(value: string) {
-  emits("change", {...props.data, content: value});
+  emits("change", { ...props.data, content: value });
 }
 
 function onMenuClick(e: { key: string }) {
@@ -139,17 +138,14 @@ function onSectionRecordUse(sectionId: number) {
   emits("sectionRecordUse", sectionId)
 }
 
-function notifyRepetitiveWarning(data: boolean){
+function notifyRepetitiveWarning(data: boolean) {
   repetitiveWarning.value = data;
 }
 
 function test1() {
-  const loadingHandler = message.loading({content: 'AI正在撰写章节内容,请稍等……', duration: 0})
-  const prompt = `根据R&D报告的标题、章节标题和关键词续写章节内容:\n` +
-            `标题:${props.title}\n`+
-            `章节标题:${props.data.title}\n关键词:${props.data.keywords}\n续写:`
-  testService.test1(prompt).then((resp) => {
-    onContentChange(resp)
+  const loadingHandler = message.loading({ content: 'AI正在撰写章节内容,请稍等……', duration: 0 })
+  aiService.generate(props.title, props.reportKeywords || '', props.data.title).then((resp) => {
+    onContentChange(marked.parse(resp))
     if (loadingHandler) {
       loadingHandler()
     }
@@ -171,15 +167,8 @@ defineExpose({
     <a-row type="flex" justify="space-between">
       <a-col>
         <a-space>
-          <a-input
-            v-if="titleEditing"
-            v-model:value="fullTitle"
-            @blur="onTitleChange"
-            @pressEnter="onTitleChange"
-            @keyup.esc="onTitleChangeCancel"
-            ref="titleEditor"
-            class="title-editor"
-          />
+          <a-input v-if="titleEditing" v-model:value="fullTitle" @blur="onTitleChange" @pressEnter="onTitleChange"
+            @keyup.esc="onTitleChangeCancel" ref="titleEditor" class="title-editor" />
           <h1 v-else>
             <span>{{ data.fullTitle }}</span>
           </h1>
@@ -192,20 +181,10 @@ defineExpose({
           <h1>
             <span>章节关键词:</span><span v-if="!keywordsEditing">{{ data.keywords.join(', ') }}</span>
           </h1>
-          <a-input
-            v-if="keywordsEditing"
-            v-model:value="keywordsInChapter"
-            @blur="onKeywordsChange"
-            @pressEnter="onKeywordsChange"
-            @keyup.esc="onKeywordsChangeCancel"
-            ref="keywordsEditor"
-            class="keywords-editor"
-          />
-          <a-button
-            type="text"
-            @click="onEditKeywords"
-            class="edit-btn"
-          ><edit-outlined /></a-button>
+          <a-input v-if="keywordsEditing" v-model:value="keywordsInChapter" @blur="onKeywordsChange"
+            @pressEnter="onKeywordsChange" @keyup.esc="onKeywordsChangeCancel" ref="keywordsEditor"
+            class="keywords-editor" />
+          <a-button type="text" @click="onEditKeywords" class="edit-btn"><edit-outlined /></a-button>
         </a-space>
       </a-col>
       <a-col>
@@ -224,25 +203,19 @@ defineExpose({
         </a-dropdown>
       </a-col>
     </a-row>
-    <p v-if="contentEditorVisible" :class="{'repetitiveWarning': repetitiveWarning}">
+    <p v-if="contentEditorVisible" :class="{ 'repetitiveWarning': repetitiveWarning }">
       <ContentEditor :data="data.content || ''" @change="onContentChange" ref="contentEditor" />
     </p>
     <div class="chapter-button-group" v-if="contentEditorVisible">
       <a-space>
-        <a-button @click="searchReference">查找参考文献</a-button>
-        <a-button :disabled="!isAI" @click="test1">自动生成(二期)</a-button>
+        <a-button @click="test1">AI生成</a-button>
+        <a-button @click="searchReference">匹配文献</a-button>
       </a-space>
     </div>
   </div>
-  <ReferenceDraw
-    :report-name="title"
-    :reportKeywords="reportKeywords || ''"
-    :chapter-keywords="data.keywords || []"
-    ref="referenceDraw"
-    @insert="onInsert"
-    @close="onReferenceDrawClose"
-    @recordUse="onSectionRecordUse"
-  />
+  <ReferenceDraw :report-name="title" :reportKeywords="reportKeywords || ''" :chapter-keywords="data.keywords || []"
+    ref="referenceDraw" @insert="onInsert" @close="onReferenceDrawClose" @recordUse="onSectionRecordUse" />
+  <AIContentDraw content="''" ref="aiContentDraw" />
   <!--删除章节 start -->
   <a-modal v-model:visible="visible" title="删除章节" @ok="handleOk">
     <p>确认删除章节</p>
@@ -277,7 +250,7 @@ defineExpose({
     text-align: right;
   }
 
-  .repetitiveWarning{
+  .repetitiveWarning {
     border: 1px solid red;
     border-radius: 8px;
   }

+ 97 - 0
src/views/report/components/reference/AIContentDraw.vue

@@ -0,0 +1,97 @@
+<!-- AI生成内容展示的抽屉组件 -->
+
+<script setup lang="ts">
+import { message } from 'ant-design-vue';
+import { ref } from 'vue';
+import { useClipboard } from "@/libs/clipboard.lib";
+
+const props = defineProps({
+  content: {
+    type: String,
+    required: true
+  }
+})
+
+const clipboard = useClipboard()
+
+// 组件可见状态
+const visible = ref<boolean>(false);
+
+const emits = defineEmits<{
+  (e: "insert", text: string): void;
+  (e: "close"): void;
+}>();
+
+function getSelectedText() {
+  if (window.getSelection) {
+    const range = window.getSelection()
+    return range?.toString()
+  } else {
+    message.warning("暂不支持此版本浏览器选择复制")
+  }
+  return null
+}
+
+/**
+ * 清理文本当中的em标签
+ * @param {string} text 文本
+ * @returns {string} 清理后的文本
+ */
+function purnText(text: string): string {
+  if (!text) {
+    return text
+  }
+  const re = new RegExp("</?em>", "gi");
+  return text.replace(re, '')
+}
+
+function onCopy() {
+  const selectedText = getSelectedText()
+  const text = purnText(selectedText || '')
+  const msg = selectedText && "复制所选内容成功" || "复制内容成功"
+  clipboard.copy(text).then(() => {
+    message.success(msg)
+  })
+}
+
+function onInsertion() {
+  const text = purnText(getSelectedText() || props.content)
+  emits("insert", text)
+}
+
+const onClose = () => {
+  visible.value = false;
+  emits("close")
+};
+
+const show = () => {
+  visible.value = true;
+};
+
+defineExpose({
+  show
+});
+</script>
+
+<template>
+  <a-drawer
+    :width="'60%'"
+    title="检索参考文献"
+    placement="right"
+    :visible="visible"
+    @close="onClose"
+    class="reference-drawer"
+  >
+    <h1>AI生成的参考内容</h1>
+    <p>{{ content }}</p>
+    <div class="btn-group">
+      <a-radio-group>
+        <a-radio-button value="default" @mousedown="onCopy">复制</a-radio-button>
+        <a-radio-button value="small" @mousedown="onInsertion">插入</a-radio-button>
+      </a-radio-group>
+    </div>
+  </a-drawer>
+</template>
+
+<style scoped lang="scss">
+</style>

+ 6 - 6
src/views/search/SearchView.vue

@@ -62,8 +62,8 @@ function getHoyKeyword(size: number){
         var expression = item.expression;
         var _expression = expression.match(/\((.+)\)/g);
         var $1 = RegExp.$1
-        var index = hotWords.value.indexOf($1); 
-        
+        var index = hotWords.value.indexOf($1);
+
         if (hotWords.value.length == 5) {
             break;
         }
@@ -96,7 +96,7 @@ function recommendsearch() {
 function search(kwd: string){
   routeToSearch({ type: 'simple', kw: kwd, field: 'TP' });
 }
- 
+
 </script>
 
 <template>
@@ -135,7 +135,7 @@ function search(kwd: string){
          <div v-for="(doc, index) in searchResult.docs" :key="index" v-if="searchResult" class="search-result-content">
           <SearchResultItem :data="doc"/>
         </div>
-        </a-spin> 
+        </a-spin>
     </a-row>
   </div>
 </template>
@@ -144,7 +144,7 @@ function search(kwd: string){
 .search-wrap {
   width: 80%;
   margin: 0 auto;
-  margin-top: 10%;
+  margin-top: 20px;
 
   h1 {
     font-size: 2rem;
@@ -157,7 +157,7 @@ function search(kwd: string){
       magin-left: 20px;
     }
   }
-  
+
   .search-result{
     margin-top: 30px;
   }