tycoding 1 год назад
Родитель
Сommit
cecd8ca85d
21 измененных файлов с 518 добавлено и 104 удалено
  1. 2 0
      langchat-common/src/main/java/cn/tycoding/langchat/common/dto/ImageR.java
  2. 2 0
      langchat-common/src/main/java/cn/tycoding/langchat/common/dto/TextR.java
  3. 17 0
      langchat-core/src/main/java/cn/tycoding/langchat/core/autoconfig/GeminiAutoConfig.java
  4. 1 0
      langchat-core/src/main/java/cn/tycoding/langchat/core/enums/ModelConst.java
  5. 10 0
      langchat-core/src/main/java/cn/tycoding/langchat/core/service/LangChatService.java
  6. 11 2
      langchat-server/src/main/java/cn/tycoding/langchat/server/endpoint/ChatEndpoint.java
  7. 1 1
      langchat-server/src/main/java/cn/tycoding/langchat/server/endpoint/ModelEndpoint.java
  8. 10 1
      langchat-server/src/main/java/cn/tycoding/langchat/server/service/impl/ChatServiceImpl.java
  9. 3 0
      langchat-ui-client/package.json
  10. 136 0
      langchat-ui-client/pnpm-lock.yaml
  11. 11 0
      langchat-ui-client/src/locales/zh-CN.ts
  12. 18 18
      langchat-ui-client/src/router/index.ts
  13. 11 0
      langchat-ui-client/src/styles/global.less
  14. 71 0
      langchat-ui-client/src/utils/downloadFile.ts
  15. 30 40
      langchat-ui-client/src/views/modules/mermaid/components/Mermaid.vue
  16. 6 6
      langchat-ui-client/src/views/modules/mermaid/components/Sider.vue
  17. 1 8
      langchat-ui-client/src/views/modules/mermaid/index.vue
  18. 31 19
      langchat-ui-client/src/views/modules/mindmap/components/MindMap.vue
  19. 50 0
      langchat-ui-client/src/views/modules/ppt/components/PptGen.vue
  20. 68 9
      langchat-ui-client/src/views/modules/ppt/index.vue
  21. 28 0
      langchat-ui-client/src/views/modules/ppt/store/index.ts

+ 2 - 0
langchat-common/src/main/java/cn/tycoding/langchat/common/dto/ImageR.java

@@ -2,12 +2,14 @@ package cn.tycoding.langchat.common.dto;
 
 import dev.langchain4j.model.input.Prompt;
 import lombok.Data;
+import lombok.experimental.Accessors;
 
 /**
  * @author tycoding
  * @since 2024/1/6
  */
 @Data
+@Accessors(chain = true)
 public class ImageR {
 
     private Prompt prompt;

+ 2 - 0
langchat-common/src/main/java/cn/tycoding/langchat/common/dto/TextR.java

@@ -3,12 +3,14 @@ package cn.tycoding.langchat.common.dto;
 import cn.tycoding.langchat.common.utils.StreamEmitter;
 import dev.langchain4j.model.input.Prompt;
 import lombok.Data;
+import lombok.experimental.Accessors;
 
 /**
  * @author tycoding
  * @since 2024/1/4
  */
 @Data
+@Accessors(chain = true)
 public class TextR {
 
     private StreamEmitter emitter;

+ 17 - 0
langchat-core/src/main/java/cn/tycoding/langchat/core/autoconfig/GeminiAutoConfig.java

@@ -1,5 +1,6 @@
 package cn.tycoding.langchat.core.autoconfig;
 
+import cn.hutool.core.util.StrUtil;
 import cn.tycoding.langchat.core.enums.ModelConst;
 import cn.tycoding.langchat.core.properties.LangChatProps;
 import cn.tycoding.langchat.core.properties.chat.GeminiProps;
@@ -51,4 +52,20 @@ public class GeminiAutoConfig {
                 .topP(prop.getTopP())
                 .build();
     }
+
+    @Bean(ModelConst.GEMINI_IMAGE)
+    @ConditionalOnProperty(value = "langchat.gemini.project", matchIfMissing = false)
+    public VertexAiGeminiChatModel vertexAiGeminiImageChatModel() {
+        GeminiProps prop = props.getGemini();
+        return VertexAiGeminiChatModel.builder()
+                .project(prop.getProject())
+                .project(prop.getProject())
+                .location(prop.getLocation())
+                .modelName(StrUtil.isNotBlank(prop.getModelName()) ? prop.getModelName() : "gemini-pro-vision")
+                .temperature(prop.getTemperature())
+                .maxOutputTokens(prop.getMaxOutputTokens())
+                .topK(prop.getTopK())
+                .topP(prop.getTopP())
+                .build();
+    }
 }

+ 1 - 0
langchat-core/src/main/java/cn/tycoding/langchat/core/enums/ModelConst.java

@@ -24,6 +24,7 @@ public class ModelConst {
 
     public static final String GEMINI = "gemini";
     public static final String GEMINI_TEXT = GEMINI + TEXT_SUFFIX;
+    public static final String GEMINI_IMAGE = GEMINI + IMAGE_SUFFIX;
 
     public static final String AZUREOPENAI = "azureopenai";
     public static final String AZUREOPENAI_TEXT = AZUREOPENAI + TEXT_SUFFIX;

+ 10 - 0
langchat-core/src/main/java/cn/tycoding/langchat/core/service/LangChatService.java

@@ -67,4 +67,14 @@ public class LangChatService {
             return null;
         }
     }
+
+    public Response<AiMessage> textImage(TextR req) {
+        try {
+            ChatLanguageModel model = provider.text(req.getModel());
+            return model.generate(req.getPrompt().toUserMessage());
+        } catch (Exception e) {
+            e.printStackTrace();
+            return null;
+        }
+    }
 }

+ 11 - 2
langchat-server/src/main/java/cn/tycoding/langchat/server/endpoint/ChatEndpoint.java

@@ -1,5 +1,6 @@
 package cn.tycoding.langchat.server.endpoint;
 
+import cn.tycoding.langchat.biz.entity.SysOss;
 import cn.tycoding.langchat.common.utils.R;
 import cn.tycoding.langchat.common.dto.ChatReq;
 import cn.tycoding.langchat.common.dto.ChatRes;
@@ -8,6 +9,7 @@ import cn.tycoding.langchat.common.dto.TextR;
 import cn.tycoding.langchat.common.dto.PromptConst;
 import cn.tycoding.langchat.common.utils.PromptUtil;
 import cn.tycoding.langchat.common.utils.StreamEmitter;
+import cn.tycoding.langchat.core.enums.ModelConst;
 import cn.tycoding.langchat.server.service.ChatService;
 import lombok.AllArgsConstructor;
 import org.springframework.web.bind.annotation.PostMapping;
@@ -28,11 +30,18 @@ public class ChatEndpoint {
     private final ChatService chatService;
 
     @PostMapping
-    public SseEmitter chat(@RequestBody ChatReq req) {
+    public Object chat(@RequestBody ChatReq req) {
         StreamEmitter emitter = new StreamEmitter();
         req.setEmitter(emitter);
         req.setPrompt(PromptUtil.build(req.getMessage()));
-        chatService.chat(req);
+
+        if (req.getModel().endsWith(ModelConst.IMAGE_SUFFIX)) {
+            SysOss oss = chatService.image(new ImageR().setPrompt(req.getPrompt()).setModel(req.getModel()));
+            emitter.send("Image:" + oss);
+            emitter.complete();
+        } else {
+            chatService.chat(req);
+        }
         return emitter.get();
     }
 

+ 1 - 1
langchat-server/src/main/java/cn/tycoding/langchat/server/endpoint/ModelEndpoint.java

@@ -6,7 +6,6 @@ import cn.tycoding.langchat.core.enums.ModelConst;
 import java.util.Arrays;
 import java.util.List;
 import lombok.AllArgsConstructor;
-import org.springframework.context.ApplicationContext;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
@@ -26,6 +25,7 @@ public class ModelEndpoint {
                 set("ChatGPT", ModelConst.OPENAI),
                 set("Ollama", ModelConst.OLLAMA),
                 set("Google Gemini", ModelConst.GEMINI),
+                set("Google Gemini Image", ModelConst.GEMINI_IMAGE),
                 set("Azure OpenAI", ModelConst.AZUREOPENAI),
                 set("ChatGLM", ModelConst.CHATGLM),
                 set("千帆大模型", ModelConst.QIANFAN)

+ 10 - 1
langchat-server/src/main/java/cn/tycoding/langchat/server/service/impl/ChatServiceImpl.java

@@ -11,9 +11,11 @@ import cn.tycoding.langchat.common.dto.ChatRes;
 import cn.tycoding.langchat.common.dto.ImageR;
 import cn.tycoding.langchat.common.dto.TextR;
 import cn.tycoding.langchat.common.utils.StreamEmitter;
+import cn.tycoding.langchat.core.enums.ModelConst;
 import cn.tycoding.langchat.core.service.LangChatService;
 import cn.tycoding.langchat.server.service.ChatService;
 import dev.langchain4j.data.image.Image;
+import dev.langchain4j.data.message.AiMessage;
 import dev.langchain4j.model.output.Response;
 import dev.langchain4j.model.output.TokenUsage;
 import lombok.AllArgsConstructor;
@@ -87,7 +89,14 @@ public class ChatServiceImpl implements ChatService {
 
     @Override
     public SysOss image(ImageR req) {
-        Response<Image> image = langChatService.image(req);
+        if (req.getModel().equals(ModelConst.GEMINI_IMAGE)) {
+            Response<AiMessage> text = langChatService.textImage(
+                    new TextR().setPrompt(req.getPrompt()).setModel(req.getModel()));
+            log.info("生成图片:{}", text);
+        } else {
+            Response<Image> image = langChatService.image(req);
+            log.info("生成图片:{}", image);
+        }
 
         SysOss oss = new SysOss();
 //        ossMapper.insert(oss);

+ 3 - 0
langchat-ui-client/package.json

@@ -29,10 +29,12 @@
     "@vueuse/core": "^9.13.0",
     "codemirror": "^6.0.1",
     "date-fns": "^2.30.0",
+    "docxtemplater": "^3.46.2",
     "echarts": "^5.4.3",
     "highlight.js": "^11.7.0",
     "html2canvas": "^1.4.1",
     "js-beautify": "^1.14.11",
+    "jspdf": "^2.5.1",
     "katex": "^0.16.4",
     "markdown-it": "^13.0.1",
     "markmap-common": "^0.15.6",
@@ -40,6 +42,7 @@
     "markmap-view": "^0.15.6",
     "naive-ui": "^2.34.3",
     "pinia": "^2.0.33",
+    "pizzip": "^3.1.6",
     "pptxgenjs": "^3.12.0",
     "typed.js": "^2.1.0",
     "uuid": "^9.0.1",

+ 136 - 0
langchat-ui-client/pnpm-lock.yaml

@@ -26,6 +26,9 @@ dependencies:
   date-fns:
     specifier: ^2.30.0
     version: 2.30.0
+  docxtemplater:
+    specifier: ^3.46.2
+    version: 3.46.2
   echarts:
     specifier: ^5.4.3
     version: 5.5.0
@@ -38,6 +41,9 @@ dependencies:
   js-beautify:
     specifier: ^1.14.11
     version: 1.15.1
+  jspdf:
+    specifier: ^2.5.1
+    version: 2.5.1
   katex:
     specifier: ^0.16.4
     version: 0.16.10
@@ -59,6 +65,9 @@ dependencies:
   pinia:
     specifier: ^2.0.33
     version: 2.1.7(typescript@4.9.5)(vue@3.4.21)
+  pizzip:
+    specifier: ^3.1.6
+    version: 3.1.6
   pptxgenjs:
     specifier: ^3.12.0
     version: 3.12.0
@@ -2590,6 +2599,12 @@ packages:
     resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==}
     dev: true
 
+  /@types/raf@3.4.3:
+    resolution: {integrity: sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==}
+    requiresBuild: true
+    dev: false
+    optional: true
+
   /@types/resolve@1.17.1:
     resolution: {integrity: sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==}
     dependencies:
@@ -2892,6 +2907,11 @@ packages:
       - vue
     dev: false
 
+  /@xmldom/xmldom@0.8.10:
+    resolution: {integrity: sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==}
+    engines: {node: '>=10.0.0'}
+    dev: false
+
   /JSONStream@1.3.5:
     resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==}
     hasBin: true
@@ -3105,6 +3125,12 @@ packages:
     engines: {node: '>= 4.0.0'}
     dev: true
 
+  /atob@2.1.2:
+    resolution: {integrity: sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==}
+    engines: {node: '>= 4.5.0'}
+    hasBin: true
+    dev: false
+
   /autolinker@3.16.2:
     resolution: {integrity: sha512-JiYl7j2Z19F9NdTmirENSUUIIL/9MytEWtmzhfmsKPCp9E+G35Y0UNCMoM9tFigxT59qSc8Ml2dlZXOCVTYwuA==}
     dependencies:
@@ -3231,6 +3257,12 @@ packages:
       update-browserslist-db: 1.0.13(browserslist@4.23.0)
     dev: true
 
+  /btoa@1.2.1:
+    resolution: {integrity: sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==}
+    engines: {node: '>= 0.4.0'}
+    hasBin: true
+    dev: false
+
   /buffer-from@1.1.2:
     resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
     dev: true
@@ -3285,6 +3317,22 @@ packages:
     resolution: {integrity: sha512-iL2iSS0eDILMb9n5yKQoTBim9jMZ0Yrk8g0N9K7UzYyWnfIKzXBZD5ngpM37ZcL/cv0Mli8XtVMRYMQAfFpi5Q==}
     dev: true
 
+  /canvg@3.0.10:
+    resolution: {integrity: sha512-qwR2FRNO9NlzTeKIPIKpnTY6fqwuYSequ8Ru8c0YkYU7U0oW+hLUvWadLvAu1Rl72OMNiFhoLu4f8eUjQ7l/+Q==}
+    engines: {node: '>=10.0.0'}
+    requiresBuild: true
+    dependencies:
+      '@babel/runtime': 7.24.1
+      '@types/raf': 3.4.3
+      core-js: 3.36.1
+      raf: 3.4.1
+      regenerator-runtime: 0.13.11
+      rgbcolor: 1.0.1
+      stackblur-canvas: 2.7.0
+      svg-pathdata: 6.0.3
+    dev: false
+    optional: true
+
   /chalk@2.4.2:
     resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==}
     engines: {node: '>=4'}
@@ -3519,6 +3567,12 @@ packages:
       browserslist: 4.23.0
     dev: true
 
+  /core-js@3.36.1:
+    resolution: {integrity: sha512-BTvUrwxVBezj5SZ3f10ImnX2oRByMxql3EimVqMysepbC9EeMUOpLwdy6Eoili2x6E4kf+ZUB5k/+Jv55alPfA==}
+    requiresBuild: true
+    dev: false
+    optional: true
+
   /core-util-is@1.0.3:
     resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
     dev: false
@@ -4121,6 +4175,13 @@ packages:
       esutils: 2.0.3
     dev: true
 
+  /docxtemplater@3.46.2:
+    resolution: {integrity: sha512-IG5o+675VzQDW6/saYmBP1aaJHJ78r846fVhTUOVjrcaqrj/mHIR7a43YSkDd5bxa+M76i0SdAIKfBnXC6bbTQ==}
+    engines: {node: '>=0.10'}
+    dependencies:
+      '@xmldom/xmldom': 0.8.10
+    dev: false
+
   /dom-serializer@2.0.0:
     resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
     dependencies:
@@ -4140,6 +4201,12 @@ packages:
       domelementtype: 2.3.0
     dev: true
 
+  /dompurify@2.4.9:
+    resolution: {integrity: sha512-iHtnxYMotKgOTvxIqq677JsKHvCOkAFqj9x8Mek2zdeHW1XjuFKwjpmZeMaXQRQ8AbJZDbcRz/+r1QhwvFtmQg==}
+    requiresBuild: true
+    dev: false
+    optional: true
+
   /dompurify@3.0.11:
     resolution: {integrity: sha512-Fan4uMuyB26gFV3ovPoEoQbxRRPfTu3CvImyZnhGq5fsIEO+gEFLp45ISFt+kQBWsK5ulDdT0oV28jS1UrwQLg==}
     dev: false
@@ -4895,6 +4962,10 @@ packages:
       reusify: 1.0.4
     dev: true
 
+  /fflate@0.4.8:
+    resolution: {integrity: sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==}
+    dev: false
+
   /file-entry-cache@6.0.1:
     resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
     engines: {node: ^10.12.0 || >=12.0.0}
@@ -5779,6 +5850,20 @@ packages:
     engines: {node: '>=0.10.0'}
     dev: true
 
+  /jspdf@2.5.1:
+    resolution: {integrity: sha512-hXObxz7ZqoyhxET78+XR34Xu2qFGrJJ2I2bE5w4SM8eFaFEkW2xcGRVUss360fYelwRSid/jT078kbNvmoW0QA==}
+    dependencies:
+      '@babel/runtime': 7.24.1
+      atob: 2.1.2
+      btoa: 1.2.1
+      fflate: 0.4.8
+    optionalDependencies:
+      canvg: 3.0.10
+      core-js: 3.36.1
+      dompurify: 2.4.9
+      html2canvas: 1.4.1
+    dev: false
+
   /jszip@3.10.1:
     resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==}
     dependencies:
@@ -6816,6 +6901,10 @@ packages:
     resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
     dev: false
 
+  /pako@2.1.0:
+    resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==}
+    dev: false
+
   /parent-module@1.0.1:
     resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
     engines: {node: '>=6'}
@@ -6908,6 +6997,12 @@ packages:
     engines: {node: '>=8'}
     dev: true
 
+  /performance-now@2.1.0:
+    resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==}
+    requiresBuild: true
+    dev: false
+    optional: true
+
   /picocolors@1.0.0:
     resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
 
@@ -6968,6 +7063,12 @@ packages:
     engines: {node: '>= 6'}
     dev: true
 
+  /pizzip@3.1.6:
+    resolution: {integrity: sha512-FCG2lSMVlrt2jB1iokujjXexanfszV/Y04t4mu1icdSEC/vb/2qDISr2kgENzdkThd1jkRNjvipWitU4gpbM/g==}
+    dependencies:
+      pako: 2.1.0
+    dev: false
+
   /pluralize@8.0.0:
     resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==}
     engines: {node: '>=4'}
@@ -7145,6 +7246,14 @@ packages:
     engines: {node: '>=8'}
     dev: true
 
+  /raf@3.4.1:
+    resolution: {integrity: sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==}
+    requiresBuild: true
+    dependencies:
+      performance-now: 2.1.0
+    dev: false
+    optional: true
+
   /randombytes@2.1.0:
     resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
     dependencies:
@@ -7232,6 +7341,12 @@ packages:
     resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==}
     dev: true
 
+  /regenerator-runtime@0.13.11:
+    resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==}
+    requiresBuild: true
+    dev: false
+    optional: true
+
   /regenerator-runtime@0.14.1:
     resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
 
@@ -7350,6 +7465,13 @@ packages:
     resolution: {integrity: sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==}
     dev: true
 
+  /rgbcolor@1.0.1:
+    resolution: {integrity: sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==}
+    engines: {node: '>= 0.8.15'}
+    requiresBuild: true
+    dev: false
+    optional: true
+
   /rimraf@3.0.2:
     resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
     hasBin: true
@@ -7645,6 +7767,13 @@ packages:
     resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
     dev: false
 
+  /stackblur-canvas@2.7.0:
+    resolution: {integrity: sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==}
+    engines: {node: '>=0.1.14'}
+    requiresBuild: true
+    dev: false
+    optional: true
+
   /string-argv@0.3.2:
     resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==}
     engines: {node: '>=0.6.19'}
@@ -7896,6 +8025,13 @@ packages:
     engines: {node: '>= 0.4'}
     dev: true
 
+  /svg-pathdata@6.0.3:
+    resolution: {integrity: sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==}
+    engines: {node: '>=12.0.0'}
+    requiresBuild: true
+    dev: false
+    optional: true
+
   /svg-tags@1.0.0:
     resolution: {integrity: sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==}
     dev: true

+ 11 - 0
langchat-ui-client/src/locales/zh-CN.ts

@@ -158,4 +158,15 @@ export default {
     titleDes: '在左侧输入内容点击生成思维导图,或者点击左侧查看示例',
     begin: '立即开始',
   },
+  mermaid: {
+    example: '示例',
+    des: '内容描述',
+    output: '输出内容',
+    confirm: '生成序列图',
+    inputTips: '请描述要生成的序列图Mermaid',
+    outputTips: '根据描述生成的序列图Mermaid脚本内容',
+    title: '序列图',
+    titleDes: '在左侧输入内容点击生成序列图Mermaid,或者点击左侧查看示例',
+    begin: '立即开始',
+  },
 };

+ 18 - 18
langchat-ui-client/src/router/index.ts

@@ -48,6 +48,24 @@ const routes: RouteRecordRaw[] = [
         },
         component: () => import('@/views/modules/write/index.vue'),
       },
+      {
+        path: '/image',
+        name: 'Image',
+        meta: {
+          label: t('menu.image'),
+          icon: 'radix-icons:image',
+        },
+        component: () => import('@/views/modules/image/index.vue'),
+      },
+      {
+        path: '/chart',
+        name: 'Chart',
+        meta: {
+          label: t('menu.chart'),
+          icon: 'fluent:data-area-24-regular',
+        },
+        component: () => import('@/views/modules/chart/index.vue'),
+      },
       {
         path: '/ppt',
         name: 'PPT',
@@ -66,24 +84,6 @@ const routes: RouteRecordRaw[] = [
         },
         component: () => import('@/views/modules/mermaid/index.vue'),
       },
-      {
-        path: '/chart',
-        name: 'Chart',
-        meta: {
-          label: t('menu.chart'),
-          icon: 'fluent:data-area-24-regular',
-        },
-        component: () => import('@/views/modules/chart/index.vue'),
-      },
-      {
-        path: '/image',
-        name: 'Image',
-        meta: {
-          label: t('menu.image'),
-          icon: 'radix-icons:image',
-        },
-        component: () => import('@/views/modules/image/index.vue'),
-      },
       {
         path: '/mindmap',
         name: 'MindMap',

+ 11 - 0
langchat-ui-client/src/styles/global.less

@@ -31,3 +31,14 @@ body {
 		font-weight: bold;
 	}
 }
+
+.dot-bg {
+	height: 100%;
+	background-image: radial-gradient(circle at center, rgba(0, 0, 0, 0.13) 1.2px, transparent 0);
+	background-size: 15px 15px;
+	background-repeat: round;
+}
+
+.n-tabs .n-tabs-rail {
+	//position: relative;
+}

+ 71 - 0
langchat-ui-client/src/utils/downloadFile.ts

@@ -1,3 +1,6 @@
+import html2canvas from 'html2canvas';
+import jspdf from 'jspdf';
+
 /**
  * 根据文件url获取文件名
  * @param url 文件url
@@ -74,3 +77,71 @@ export function downloadByUrl({
     }
   });
 }
+
+export function downloadBlob(blob: any, filename: string) {
+  const url = URL.createObjectURL(blob);
+  const link = document.createElement('a');
+  link.href = url;
+  link.download = filename;
+  document.body.append(link);
+  link.click();
+  link.remove();
+  URL.revokeObjectURL(url);
+}
+
+export async function downloadPng(eleId: string, filename: string) {
+  const ele = document.getElementById(eleId);
+  const canvas = await html2canvas(ele as HTMLDivElement, {
+    useCORS: true,
+  });
+  const imgUrl = canvas.toDataURL('image/png');
+  if (imgUrl.length < 8) {
+    return;
+  }
+  const tempLink = document.createElement('a');
+  tempLink.style.display = 'none';
+  tempLink.href = imgUrl;
+  tempLink.setAttribute('download', filename + '.png');
+  if (typeof tempLink.download === 'undefined') tempLink.setAttribute('target', '_blank');
+  document.body.appendChild(tempLink);
+  tempLink.click();
+}
+
+export function downloadSvg(eleId: string, filename: string) {
+  const mermaidDiv = document.getElementById(eleId);
+  const svg = mermaidDiv!.querySelector('svg') as any;
+  if (svg == null) {
+    return;
+  }
+  const data = new XMLSerializer().serializeToString(svg);
+  const blob = new Blob([data], { type: 'image/svg+xml' });
+  const url = URL.createObjectURL(blob);
+  const link = document.createElement('a');
+  link.href = url;
+  link.download = filename + '.svg';
+  document.body.append(link);
+  link.click();
+  link.remove();
+  URL.revokeObjectURL(url);
+}
+
+export async function downloadPdf(eleId: string, filename: string) {
+  const ele = document.getElementById(eleId);
+  const canvas = await html2canvas(ele as HTMLDivElement, {
+    useCORS: true,
+  });
+  const imgUrl = canvas.toDataURL('image/png');
+  if (imgUrl.length < 8) {
+    return;
+  }
+  const pdf = new jspdf();
+  pdf.addImage({
+    imageData: imgUrl,
+    x: 10,
+    y: 10,
+    width: 100,
+    height: 100,
+    format: 'PNG',
+  });
+  pdf.save(filename + '.pdf');
+}

+ 30 - 40
langchat-ui-client/src/views/modules/mermaid/components/Mermaid.vue

@@ -4,6 +4,7 @@
   import html2canvas from 'html2canvas';
   import { t } from '@/locales';
   import { VueMermaidRender } from 'vue-mermaid-render';
+  import { downloadPdf, downloadPng, downloadSvg } from '@/utils/downloadFile';
 
   const props = defineProps<{
     genText: string;
@@ -27,44 +28,21 @@
   function onZoomFill() {
     width.value = 80;
   }
-  async function onDownload() {
-    const ele = document.getElementById('mermaid-view');
-    const canvas = await html2canvas(ele as HTMLDivElement, {
-      useCORS: true,
-    });
-    const imgUrl = canvas.toDataURL('image/png');
-    const tempLink = document.createElement('a');
-    tempLink.style.display = 'none';
-    tempLink.href = imgUrl;
-    tempLink.setAttribute('download', 'mermaid-shot.png');
-    if (typeof tempLink.download === 'undefined') tempLink.setAttribute('target', '_blank');
-    document.body.appendChild(tempLink);
-    tempLink.click();
-  }
 
-  function download() {
-    const mermaidDiv = document.getElementById('mermaid-view');
-    const svg = mermaidDiv!.querySelector('svg');
-    const url = svgToBlob(svg);
-    const link = document.createElement('a');
-    link.href = url;
-    link.download = 'my-svg-file.svg';
-    document.body.append(link);
-    link.click();
-    link.remove();
-    URL.revokeObjectURL(url);
+  function downPng() {
+    downloadPng('mermaid-view', 'mermaid-shot');
   }
-
-  function svgToBlob(svg) {
-    const data = new XMLSerializer().serializeToString(svg);
-    const blob = new Blob([data], { type: 'image/svg+xml' });
-    return URL.createObjectURL(blob);
+  function downSvg() {
+    downloadSvg('mermaid-view', 'mermaid-shot');
+  }
+  function downPdf() {
+    downloadPdf('mermaid-view', 'mermaid-shot');
   }
 </script>
 
 <template>
-  <div class="bg w-full h-full" :class="genText == '' ? 'overflow-hidden' : ''">
-    <div v-if="genText !== ''" class="absolute top-0 z-10 p-2 flex flex-wrap justify-center gap-2">
+  <div class="dot-bg w-full h-full" :class="genText == '' ? 'overflow-hidden' : ''">
+    <div class="absolute top-0 z-10 p-2 flex flex-wrap justify-center gap-2">
       <n-button @click="onZoomIn" text>
         <SvgIcon class="text-2xl" icon="basil:zoom-in-outline" />
       </n-button>
@@ -74,19 +52,31 @@
       <n-button @click="onZoomFill" text>
         <SvgIcon class="text-2xl" icon="fluent:full-screen-zoom-24-filled" />
       </n-button>
-      <n-button text>
-        <SvgIcon @click="onDownload" class="text-2xl" icon="material-symbols:download" />
+      <n-button round size="small" @click="downPng">
+        <template #icon>
+          <SvgIcon class="text-lg" icon="material-symbols:download" />
+        </template>
+        PNG
+      </n-button>
+      <n-button round size="small" @click="downSvg">
+        <template #icon>
+          <SvgIcon class="text-lg" icon="material-symbols:download" />
+        </template>
+        SVG
       </n-button>
-      <n-button>
-        <SvgIcon @click="download" class="text-2xl" icon="material-symbols:download" />
+      <n-button round size="small" @click="downPdf">
+        <template #icon>
+          <SvgIcon class="text-lg" icon="material-symbols:download" />
+        </template>
+        PDF
       </n-button>
     </div>
 
     <div class="h-full w-full flex flex-col justify-center items-center gap-3" v-if="genText == ''">
-      <SvgIcon class="text-6xl" icon="ri:mind-map" />
-      <div class="text-2xl font-bold">{{ t('mindmap.title') }}</div>
-      <div class="text-gray-400">{{ t('mindmap.titleDes') }}</div>
-      <n-button type="success">{{ t('mindmap.begin') }}</n-button>
+      <SvgIcon class="text-6xl" icon="flowbite:chart-mixed-outline" />
+      <div class="text-2xl font-bold">{{ t('mermaid.title') }}</div>
+      <div class="text-gray-400">{{ t('mermaid.titleDes') }}</div>
+      <n-button type="success">{{ t('mermaid.begin') }}</n-button>
     </div>
 
     <div class="h-full w-full flex justify-center items-center">

+ 6 - 6
langchat-ui-client/src/views/modules/mermaid/components/Sider.vue

@@ -45,11 +45,11 @@
 
 <template>
   <div class="p-4">
-    <div class="pb-2">{{ t('mindmap.des') }}</div>
+    <div class="pb-2">{{ t('mermaid.des') }}</div>
     <n-input
       :disabled="loading"
       v-model:value="text"
-      :placeholder="t('mindmap.inputTips')"
+      :placeholder="t('mermaid.inputTips')"
       type="textarea"
       :rows="6"
     />
@@ -58,19 +58,19 @@
         <template #icon>
           <SvgIcon class="text-lg" icon="ion:sparkles-outline" />
         </template>
-        {{ t('mindmap.confirm') }}
+        {{ t('mermaid.confirm') }}
       </n-button>
     </div>
 
     <div class="mt-6">
       <div class="flex flex-wrap justify-between items-center mb-2">
-        <div>{{ t('mindmap.output') }}</div>
-        <n-button @click="onCase" type="success" text>{{ t('mindmap.example') }}</n-button>
+        <div>{{ t('mermaid.output') }}</div>
+        <n-button @click="onCase" type="success" text>{{ t('mermaid.example') }}</n-button>
       </div>
     </div>
     <n-input
       v-model:value="gen"
-      :placeholder="t('mindmap.outputTips')"
+      :placeholder="t('mermaid.outputTips')"
       type="textarea"
       :rows="16"
     />

+ 1 - 8
langchat-ui-client/src/views/modules/mermaid/index.vue

@@ -46,11 +46,4 @@
   </n-layout>
 </template>
 
-<style scoped lang="less">
-  ::v-deep(.bg) {
-    height: 100%;
-    background-image: radial-gradient(circle at center, rgba(0, 0, 0, 0.13) 1.2px, transparent 0);
-    background-size: 15px 15px;
-    background-repeat: round;
-  }
-</style>
+<style scoped lang="less"></style>

+ 31 - 19
langchat-ui-client/src/views/modules/mindmap/components/MindMap.vue

@@ -5,6 +5,7 @@
   import { onMounted, ref, watch } from 'vue';
   import html2canvas from 'html2canvas';
   import { t } from '@/locales';
+  import { downloadPdf, downloadPng, downloadSvg } from '@/utils/downloadFile';
 
   const props = defineProps<{
     genText: string;
@@ -12,7 +13,7 @@
 
   let instance: Markmap | null = null;
   onMounted(() => {
-    const el = document.getElementById('mind-map') as any;
+    const el = document.getElementById('mindmap') as any;
     instance = Markmap.create(el);
   });
 
@@ -35,24 +36,20 @@
   function onZoomFill() {
     instance?.fit();
   }
-  async function onDownload() {
-    const ele = document.getElementById('mind-map-view');
-    const canvas = await html2canvas(ele as HTMLDivElement, {
-      useCORS: true,
-    });
-    const imgUrl = canvas.toDataURL('image/png');
-    const tempLink = document.createElement('a');
-    tempLink.style.display = 'none';
-    tempLink.href = imgUrl;
-    tempLink.setAttribute('download', 'mind-map-shot.png');
-    if (typeof tempLink.download === 'undefined') tempLink.setAttribute('target', '_blank');
-    document.body.appendChild(tempLink);
-    tempLink.click();
+
+  function downPng() {
+    downloadPng('mindmap-view', 'mindmap-shot');
+  }
+  function downSvg() {
+    downloadSvg('mindmap-view', 'mindmap-shot');
+  }
+  function downPdf() {
+    downloadPdf('mindmap-view', 'mindmap-shot');
   }
 </script>
 
 <template>
-  <div class="w-full h-full" :class="genText == '' ? 'overflow-hidden' : ''">
+  <div class="dot-bg w-full h-full" :class="genText == '' ? 'overflow-hidden' : ''">
     <div v-if="genText !== ''" class="absolute top-0 z-10 p-2 flex flex-wrap justify-center gap-2">
       <n-button @click="onZoomIn" text>
         <SvgIcon class="text-2xl" icon="basil:zoom-in-outline" />
@@ -63,8 +60,23 @@
       <n-button @click="onZoomFill" text>
         <SvgIcon class="text-2xl" icon="fluent:full-screen-zoom-24-filled" />
       </n-button>
-      <n-button text>
-        <SvgIcon @click="onDownload" class="text-2xl" icon="material-symbols:download" />
+      <n-button round size="small" @click="downPng">
+        <template #icon>
+          <SvgIcon class="text-lg" icon="material-symbols:download" />
+        </template>
+        PNG
+      </n-button>
+      <n-button round size="small" @click="downSvg">
+        <template #icon>
+          <SvgIcon class="text-lg" icon="material-symbols:download" />
+        </template>
+        SVG
+      </n-button>
+      <n-button round size="small" @click="downPdf">
+        <template #icon>
+          <SvgIcon class="text-lg" icon="material-symbols:download" />
+        </template>
+        PDF
       </n-button>
     </div>
 
@@ -75,8 +87,8 @@
       <n-button type="success">{{ t('mindmap.begin') }}</n-button>
     </div>
 
-    <div class="h-full w-full" id="mind-map-view">
-      <svg class="h-full w-full" id="mind-map" />
+    <div class="h-full w-full" id="mindmap-view">
+      <svg class="h-full w-full" id="mindmap" />
     </div>
   </div>
 </template>

+ 50 - 0
langchat-ui-client/src/views/modules/ppt/components/PptGen.vue

@@ -0,0 +1,50 @@
+<script setup lang="ts">
+  import Pptxgen from 'pptxgenjs';
+  import { onMounted, ref } from 'vue';
+  import Docxtemplater from 'docxtemplater';
+  import PizZipUtils from 'pizzip/utils';
+  import PizZip from 'pizzip';
+  import { downloadBlob } from '@/utils/downloadFile';
+  import { SvgIcon } from '@/components/common';
+  import ChartSelect from '@/views/modules/chart/components/ChartSelect.vue';
+  import ChartData from '@/views/modules/chart/components/ChartData.vue';
+  import ChartConfig from '@/views/modules/chart/components/ChartConfig.vue';
+  import ChartExport from '@/views/modules/chart/components/ChartExport.vue';
+  let pptx = new Pptxgen();
+  const pptxRef = ref();
+
+  onMounted(() => {
+    PizZipUtils.getBinaryContent('https://docxtemplater.com/tag-example.docx', (err, data) => {
+      const zip = new PizZip(data);
+      const doc = new Docxtemplater(zip, { paragraphLoop: true, linebreaks: true });
+      doc.render({
+        first_name: 'TyCoding',
+        last_name: 'Tumo',
+        phone: '0652455478',
+        description: 'New Website',
+      });
+      const out = doc.getZip().generate({
+        type: 'blob',
+        mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+      });
+      // downloadBlob(out, 'output.docx');
+    });
+  });
+</script>
+
+<template>
+  <div class="h-full w-full p-2">
+    <div id="pptxRef" ref="pptxRef">
+      <h1>H1</h1>
+      <h2>H2</h2>
+      <p>事实上司收拾收拾</p>
+    </div>
+    <iframe
+      width="100%"
+      height="100%"
+      src="https://view.officeapps.live.com/op/embed.aspx?src=https://djgurnpwsdoqjscwqbsj.supabase.co/storage/v1/object/public/presentations/3d68fd2ac262bdbd.pptx"
+    ></iframe>
+  </div>
+</template>
+
+<style scoped lang="less"></style>

+ 68 - 9
langchat-ui-client/src/views/modules/ppt/index.vue

@@ -1,25 +1,84 @@
 <script setup lang="ts">
-  import Pptxgen from 'pptxgenjs';
   import { onMounted, ref } from 'vue';
-  let pptx = new Pptxgen();
-  const pptxRef = ref();
+  import { SvgIcon } from '@/components/common';
+  import { usePptStore } from '@/views/modules/ppt/store';
+  import ChartSelect from '@/views/modules/chart/components/ChartSelect.vue';
+  import ChartData from '@/views/modules/chart/components/ChartData.vue';
+  import ChartConfig from '@/views/modules/chart/components/ChartConfig.vue';
+
+  const pptStore = usePptStore();
+  const steps = [
+    { key: '选择PPT模版', component: ChartSelect },
+    { key: '输入Prompt', component: ChartData },
+    { key: '生成PPT', component: ChartConfig },
+  ];
 
   onMounted(() => {});
 </script>
 
 <template>
+  <div class="w-full pt-3">
+    <div class="flex justify-between items-center pl-10 pr-10">
+      <n-button text type="primary" size="large">
+        <template #icon>
+          <SvgIcon icon="mingcute:ppt-fill" />
+        </template>
+        AI生成PPT
+      </n-button>
+      <n-button text type="primary"> 使用说明 </n-button>
+    </div>
+    <n-divider class="!mt-2 !mb-6" />
+    <div class="w-full flex justify-center items-center gap-4 flex-col pb-16">
+      <div class="!w-[90%]">
+        <n-tabs
+          v-model:value="pptStore.step"
+          type="segment"
+          animated
+          justify-content="space-evenly"
+        >
+          <n-tab-pane
+            v-for="(item, index) in steps"
+            :key="index"
+            :name="index"
+            display-directive="show"
+          >
+            <template #tab>
+              <div
+                :class="pptStore.step < index ? 'text-gray-400' : ''"
+                class="flex justify-center items-center w-full pr-2"
+              >
+                <div
+                  class="w-full flex justify-center items-center"
+                  :class="pptStore.step === index ? 'text-[#70c0e8]' : ''"
+                >
+                  <div class="w-1/2 justify-center flex">{{ item.key }}</div>
+                </div>
+              </div>
+              <SvgIcon icon="mingcute:right-fill" />
+            </template>
+            <component :is="item.component" />
+          </n-tab-pane>
+        </n-tabs>
+      </div>
+    </div>
+  </div>
+
   <div class="h-full w-full p-2">
     <div id="pptxRef" ref="pptxRef">
       <h1>H1</h1>
       <h2>H2</h2>
       <p>事实上司收拾收拾</p>
     </div>
-    <!--    <iframe-->
-    <!--      width="100%"-->
-    <!--      height="100%"-->
-    <!--      src="https://view.officeapps.live.com/op/embed.aspx?src=https://djgurnpwsdoqjscwqbsj.supabase.co/storage/v1/object/public/presentations/3d68fd2ac262bdbd.pptx"-->
-    <!--    ></iframe>-->
+    <iframe
+      width="100%"
+      height="100%"
+      src="https://view.officeapps.live.com/op/embed.aspx?src=https://djgurnpwsdoqjscwqbsj.supabase.co/storage/v1/object/public/presentations/3d68fd2ac262bdbd.pptx"
+    ></iframe>
   </div>
 </template>
 
-<style scoped lang="less"></style>
+<style scoped lang="less">
+  ::v-deep(.n-tabs .n-tabs-tab .n-tabs-tab__label) {
+    width: 100% !important;
+  }
+</style>

+ 28 - 0
langchat-ui-client/src/views/modules/ppt/store/index.ts

@@ -0,0 +1,28 @@
+import { defineStore } from 'pinia';
+
+export interface PptState {
+  step: number;
+  key: string;
+  data: Object;
+}
+
+export const usePptStore = defineStore({
+  id: 'ppt-store',
+  state: (): PptState => ({
+    step: 0,
+    key: '',
+    data: {},
+  }),
+
+  actions: {
+    setStep(step: number) {
+      this.step = step;
+    },
+    setKey(key: string) {
+      this.key = key;
+    },
+    setData(data: Object) {
+      this.data = data;
+    },
+  },
+});