Ver código fonte

fix chat page

tycoding 1 ano atrás
pai
commit
fa177af414
26 arquivos alterados com 307 adições e 241 exclusões
  1. 22 0
      langchat-common/src/main/java/cn/tycoding/langchat/common/dto/ChatData.java
  2. 4 1
      langchat-core/src/main/java/cn/tycoding/langchat/core/service/LangChatService.java
  3. 21 47
      langchat-core/src/main/java/cn/tycoding/langchat/core/service/impl/LangChatServiceImpl.java
  4. 0 3
      langchat-core/src/main/java/cn/tycoding/langchat/core/utils/ChatReq.java
  5. 6 5
      langchat-server/src/main/java/cn/tycoding/langchat/server/controller/ChatController.java
  6. 9 3
      langchat-server/src/main/java/cn/tycoding/langchat/server/controller/ConversationController.java
  7. 2 2
      langchat-server/src/main/java/cn/tycoding/langchat/server/controller/OssFileController.java
  8. 0 5
      langchat-server/src/main/java/cn/tycoding/langchat/server/entity/LcMessage.java
  9. 2 2
      langchat-server/src/main/java/cn/tycoding/langchat/server/listener/MessageListener.java
  10. 5 2
      langchat-server/src/main/java/cn/tycoding/langchat/server/service/ChatService.java
  11. 2 2
      langchat-server/src/main/java/cn/tycoding/langchat/server/service/ClientFileService.java
  12. 3 1
      langchat-server/src/main/java/cn/tycoding/langchat/server/service/MessageService.java
  13. 13 2
      langchat-server/src/main/java/cn/tycoding/langchat/server/service/impl/ChatServiceImpl.java
  14. 2 2
      langchat-server/src/main/java/cn/tycoding/langchat/server/service/impl/ClientFileServiceImpl.java
  15. 11 2
      langchat-server/src/main/java/cn/tycoding/langchat/server/service/impl/MessageServiceImpl.java
  16. 6 32
      langchat-server/src/main/java/cn/tycoding/langchat/server/utils/ChatR.java
  17. 44 0
      langchat-server/src/main/java/cn/tycoding/langchat/server/utils/TextR.java
  18. 8 2
      langchat-ui/src/api/conversation.ts
  19. 5 3
      langchat-ui/src/locales/zh-CN.ts
  20. 75 2
      langchat-ui/src/views/modules/chat/Header.vue
  21. 28 69
      langchat-ui/src/views/modules/chat/index.vue
  22. 7 30
      langchat-ui/src/views/modules/chat/message/Message.vue
  23. 3 2
      langchat-ui/src/views/modules/chat/message/TextComponent.vue
  24. 4 4
      langchat-ui/src/views/modules/chat/sider/List.vue
  25. 19 5
      langchat-ui/src/views/modules/chat/sider/index.vue
  26. 6 13
      langchat-ui/src/views/modules/chat/store/useChatStore.ts

+ 22 - 0
langchat-common/src/main/java/cn/tycoding/langchat/common/dto/ChatData.java

@@ -0,0 +1,22 @@
+package cn.tycoding.langchat.common.dto;
+
+import java.io.Serializable;
+import lombok.Data;
+
+/**
+ * @author tycoding
+ * @since 2024/2/20
+ */
+@Data
+public class ChatData implements Serializable {
+
+    private static final long serialVersionUID = -2299910927285482191L;
+
+    private String conversationId;
+
+    private String chatId;
+
+    private String promptId;
+
+    private String content;
+}

+ 4 - 1
langchat-core/src/main/java/cn/tycoding/langchat/core/service/LangChatService.java

@@ -1,7 +1,10 @@
 package cn.tycoding.langchat.core.service;
 
+import cn.tycoding.langchat.common.dto.ChatData;
 import cn.tycoding.langchat.core.utils.ChatReq;
 import cn.tycoding.langchat.core.utils.OssR;
+import cn.tycoding.langchat.core.utils.StreamEmitter;
+import dev.langchain4j.model.input.Prompt;
 
 /**
  * @author tycoding
@@ -9,7 +12,7 @@ import cn.tycoding.langchat.core.utils.OssR;
  */
 public interface LangChatService {
 
-    void chat(ChatReq req);
+    void chat(StreamEmitter emitter, Prompt prompt, ChatData data);
 
     void stream(ChatReq req);
 

+ 21 - 47
langchat-core/src/main/java/cn/tycoding/langchat/core/service/impl/LangChatServiceImpl.java

@@ -4,8 +4,8 @@ import static dev.ai4j.openai4j.image.ImageModel.DALL_E_QUALITY_HD;
 import static java.net.Proxy.Type.HTTP;
 
 import cn.hutool.core.io.FileUtil;
-import cn.hutool.core.lang.Dict;
 import cn.tycoding.langchat.common.component.SpringContextHolder;
+import cn.tycoding.langchat.common.dto.ChatData;
 import cn.tycoding.langchat.common.event.MessageEvent;
 import cn.tycoding.langchat.core.properties.LangChatProps;
 import cn.tycoding.langchat.core.properties.OssProps;
@@ -14,13 +14,13 @@ import cn.tycoding.langchat.core.utils.ChatReq;
 import cn.tycoding.langchat.core.utils.ChatRes;
 import cn.tycoding.langchat.core.utils.OssR;
 import cn.tycoding.langchat.core.utils.OssUtil;
+import cn.tycoding.langchat.core.utils.StreamEmitter;
 import dev.langchain4j.data.image.Image;
 import dev.langchain4j.data.message.AiMessage;
 import dev.langchain4j.model.StreamingResponseHandler;
 import dev.langchain4j.model.chat.ChatLanguageModel;
 import dev.langchain4j.model.chat.StreamingChatLanguageModel;
 import dev.langchain4j.model.input.Prompt;
-import dev.langchain4j.model.input.PromptTemplate;
 import dev.langchain4j.model.openai.OpenAiChatModel;
 import dev.langchain4j.model.openai.OpenAiImageModel;
 import dev.langchain4j.model.openai.OpenAiStreamingChatModel;
@@ -45,45 +45,11 @@ public class LangChatServiceImpl implements LangChatService {
     private final OssProps ossProps;
 
     @Override
-    public void chat(ChatReq req) {
-        long startTime = System.currentTimeMillis();
-        StreamingChatLanguageModel model = OpenAiStreamingChatModel.builder()
-                .apiKey(props.getApiKey())
-                .modelName("gpt-4")
-                .proxy(new Proxy(HTTP, new InetSocketAddress("127.0.0.1", 7890)))
-                .build();
-
-        PromptTemplate promptTemplate = PromptTemplate.from(
-                req.getPrompt() + "\n\n Here's what you need to deal with: {{input_text}}");
-        Prompt prompt = promptTemplate.apply(Dict.create().set("input_text", req.getContent()));
-
-        StringBuilder res = new StringBuilder();
-        model.generate(prompt.toUserMessage().text(), new StreamingResponseHandler<AiMessage>() {
-            @Override
-            public void onNext(String s) {
-                res.append(s);
-                req.getEmitter().send(new ChatRes(s));
-            }
-
-            @Override
-            public void onError(Throwable throwable) {
-                req.getEmitter().complete();
-            }
-
-            @Override
-            public void onComplete(Response<AiMessage> response) {
-                TokenUsage tokenUsage = response.tokenUsage();
-                req.getEmitter().send(new ChatRes(tokenUsage.totalTokenCount(), startTime));
-                req.getEmitter().complete();
-
-                req.setContent(res.toString());
-                SpringContextHolder.publishEvent(new MessageEvent(req));
-            }
-        });
+    public void chat(StreamEmitter emitter, Prompt prompt, ChatData data) {
+        stream(emitter, prompt, data);
     }
 
-    @Override
-    public void stream(ChatReq req) {
+    private void stream(StreamEmitter emitter, Prompt prompt, ChatData data) {
         long startTime = System.currentTimeMillis();
         StreamingChatLanguageModel model = OpenAiStreamingChatModel.builder()
                 .apiKey(props.getApiKey())
@@ -97,32 +63,40 @@ public class LangChatServiceImpl implements LangChatService {
 //                .build();
 
         StringBuilder res = new StringBuilder();
-        model.generate(req.getPrompt().toUserMessage().text(),
+        model.generate(prompt.toUserMessage().text(),
                 new StreamingResponseHandler<AiMessage>() {
                     @Override
                     public void onNext(String s) {
                         res.append(s);
-                        req.getEmitter().send(new ChatRes(s));
+                        emitter.send(new ChatRes(s));
                     }
 
                     @Override
                     public void onError(Throwable throwable) {
                         throwable.printStackTrace();
-                        req.getEmitter().complete();
+                        emitter.complete();
                     }
 
                     @Override
                     public void onComplete(Response<AiMessage> response) {
                         TokenUsage tokenUsage = response.tokenUsage();
-                        req.getEmitter().send(new ChatRes(tokenUsage.totalTokenCount(), startTime));
-                        req.getEmitter().complete();
-
-                        req.setContent(res.toString());
-                        SpringContextHolder.publishEvent(new MessageEvent(req));
+                        emitter.send(new ChatRes(tokenUsage.totalTokenCount(), startTime));
+                        emitter.complete();
+
+                        // save message
+                        if (data != null) {
+                            data.setContent(res.toString());
+                            SpringContextHolder.publishEvent(new MessageEvent(data));
+                        }
                     }
                 });
     }
 
+    @Override
+    public void stream(ChatReq req) {
+        stream(req.getEmitter(), req.getPrompt(), null);
+    }
+
     @Override
     public String text(ChatReq req) {
         ChatLanguageModel model = OpenAiChatModel.builder()

+ 0 - 3
langchat-core/src/main/java/cn/tycoding/langchat/core/utils/ChatReq.java

@@ -12,9 +12,6 @@ import lombok.experimental.Accessors;
 @Accessors(chain = true)
 public class ChatReq {
 
-    private String conversationId;
-    private String botId;
-
     private String content;
 
     private String promptText;

+ 6 - 5
langchat-server/src/main/java/cn/tycoding/langchat/server/controller/ChatController.java

@@ -4,6 +4,7 @@ import cn.tycoding.langchat.core.utils.ChatRes;
 import cn.tycoding.langchat.core.utils.StreamEmitter;
 import cn.tycoding.langchat.common.constant.PromptConst;
 import cn.tycoding.langchat.server.utils.ChatR;
+import cn.tycoding.langchat.server.utils.TextR;
 import cn.tycoding.langchat.server.utils.ImageR;
 import cn.tycoding.langchat.server.utils.R;
 import cn.tycoding.langchat.server.service.ChatService;
@@ -29,12 +30,12 @@ public class ChatController {
     public SseEmitter chat(@RequestBody ChatR req) {
         StreamEmitter emitter = new StreamEmitter();
         req.setEmitter(emitter);
-        chatService.stream(req, PromptConst.CHAT);
+        chatService.chat(req);
         return emitter.get();
     }
 
     @PostMapping("/translate")
-    public SseEmitter translate(@RequestBody ChatR req) {
+    public SseEmitter translate(@RequestBody TextR req) {
         StreamEmitter emitter = new StreamEmitter();
         req.setEmitter(emitter);
         chatService.stream(req, PromptConst.TRANSLATE);
@@ -42,7 +43,7 @@ public class ChatController {
     }
 
     @PostMapping("/write")
-    public SseEmitter write(@RequestBody ChatR req) {
+    public SseEmitter write(@RequestBody TextR req) {
         StreamEmitter emitter = new StreamEmitter();
         req.setEmitter(emitter);
         chatService.stream(req, PromptConst.WRITE);
@@ -50,12 +51,12 @@ public class ChatController {
     }
 
     @PostMapping("/mindmap")
-    public R mindmap(@RequestBody ChatR req) {
+    public R mindmap(@RequestBody TextR req) {
         return R.ok(new ChatRes(chatService.text(req, PromptConst.MIND_MAP)));
     }
 
     @PostMapping("/chart")
-    public R chart(@RequestBody ChatR req) {
+    public R chart(@RequestBody TextR req) {
         return R.ok(new ChatRes(chatService.text(req, PromptConst.LINE_CHART)));
     }
 

+ 9 - 3
langchat-server/src/main/java/cn/tycoding/langchat/server/controller/ConversationController.java

@@ -71,9 +71,15 @@ public class ConversationController {
     /**
      * 删除会话
      */
-    @DeleteMapping("/{id}")
-    public R delConversation(@PathVariable Long id) {
-        messageService.delConversation(id);
+    @DeleteMapping("/{conversationId}")
+    public R delConversation(@PathVariable String conversationId) {
+        messageService.delConversation(conversationId);
+        return R.ok();
+    }
+
+    @DeleteMapping("/message/{conversationId}")
+    public R clearMessage(@PathVariable String conversationId) {
+        messageService.clearMessage(conversationId);
         return R.ok();
     }
 

+ 2 - 2
langchat-server/src/main/java/cn/tycoding/langchat/server/controller/OssFileController.java

@@ -2,7 +2,7 @@ package cn.tycoding.langchat.server.controller;
 
 import cn.tycoding.langchat.core.utils.StreamEmitter;
 import cn.tycoding.langchat.common.constant.PromptConst;
-import cn.tycoding.langchat.server.utils.ChatR;
+import cn.tycoding.langchat.server.utils.TextR;
 import cn.tycoding.langchat.server.utils.R;
 import cn.tycoding.langchat.server.entity.LcOss;
 import cn.tycoding.langchat.server.service.ClientFileService;
@@ -50,7 +50,7 @@ public class OssFileController {
     }
 
     @PostMapping("/chat")
-    public SseEmitter chat(@RequestBody ChatR req) {
+    public SseEmitter chat(@RequestBody TextR req) {
         StreamEmitter emitter = new StreamEmitter();
         req.setEmitter(emitter);
         clientFileService.chat(req, PromptConst.DOCUMENT);

+ 0 - 5
langchat-server/src/main/java/cn/tycoding/langchat/server/entity/LcMessage.java

@@ -28,11 +28,6 @@ public class LcMessage implements Serializable {
      */
     private String chatId;
 
-    /**
-     * 上级消息ID
-     */
-    private String parentChatId;
-
     /**
      * 会话ID
      */

+ 2 - 2
langchat-server/src/main/java/cn/tycoding/langchat/server/listener/MessageListener.java

@@ -1,8 +1,8 @@
 package cn.tycoding.langchat.server.listener;
 
 import cn.tycoding.langchat.common.constant.RoleEnum;
+import cn.tycoding.langchat.common.dto.ChatData;
 import cn.tycoding.langchat.common.event.MessageEvent;
-import cn.tycoding.langchat.core.utils.ChatReq;
 import cn.tycoding.langchat.server.entity.LcMessage;
 import cn.tycoding.langchat.server.service.MessageService;
 import lombok.RequiredArgsConstructor;
@@ -26,7 +26,7 @@ public class MessageListener {
     @Order
     @EventListener(MessageEvent.class)
     public void handler(MessageEvent event) {
-        ChatReq data = (ChatReq) event.getSource();
+        ChatData data = (ChatData) event.getSource();
         LcMessage message = new LcMessage();
         BeanUtils.copyProperties(data, message);
         message.setRole(RoleEnum.ASSISTANT.getName());

+ 5 - 2
langchat-server/src/main/java/cn/tycoding/langchat/server/service/ChatService.java

@@ -2,6 +2,7 @@ package cn.tycoding.langchat.server.service;
 
 import cn.tycoding.langchat.common.constant.PromptConst;
 import cn.tycoding.langchat.server.utils.ChatR;
+import cn.tycoding.langchat.server.utils.TextR;
 import cn.tycoding.langchat.server.utils.ImageR;
 import cn.tycoding.langchat.server.entity.LcOss;
 
@@ -14,12 +15,14 @@ public interface ChatService {
     /**
      * 流式响应
      */
-    void stream(ChatR req, PromptConst promptConst);
+    void chat(ChatR req);
+
+    void stream(TextR req, PromptConst promptConst);
 
     /**
      * 文本请求
      */
-    String text(ChatR req, PromptConst promptConst);
+    String text(TextR req, PromptConst promptConst);
 
     /**
      * 文生图

+ 2 - 2
langchat-server/src/main/java/cn/tycoding/langchat/server/service/ClientFileService.java

@@ -1,7 +1,7 @@
 package cn.tycoding.langchat.server.service;
 
 import cn.tycoding.langchat.common.constant.PromptConst;
-import cn.tycoding.langchat.server.utils.ChatR;
+import cn.tycoding.langchat.server.utils.TextR;
 import cn.tycoding.langchat.server.entity.LcOss;
 import org.springframework.web.multipart.MultipartFile;
 
@@ -14,7 +14,7 @@ public interface ClientFileService {
     /**
      * 流式响应
      */
-    void chat(ChatR req, PromptConst promptConst);
+    void chat(TextR req, PromptConst promptConst);
 
     /**
      * 上传文件

+ 3 - 1
langchat-server/src/main/java/cn/tycoding/langchat/server/service/MessageService.java

@@ -36,8 +36,10 @@ public interface MessageService extends IService<LcMessage> {
     /**
      * 删除会话
      */
-    void delConversation(Long id);
+    void delConversation(String conversationId);
 
     void addMessage(LcMessage message);
+
+    void clearMessage(String conversationId);
 }
 

+ 13 - 2
langchat-server/src/main/java/cn/tycoding/langchat/server/service/impl/ChatServiceImpl.java

@@ -1,12 +1,14 @@
 package cn.tycoding.langchat.server.service.impl;
 
 import cn.hutool.core.bean.BeanUtil;
+import cn.tycoding.langchat.common.dto.ChatData;
 import cn.tycoding.langchat.core.service.LangChatService;
 import cn.tycoding.langchat.core.utils.ChatReq;
 import cn.tycoding.langchat.core.utils.FileEnum;
 import cn.tycoding.langchat.core.utils.OssR;
 import cn.tycoding.langchat.common.constant.PromptConst;
 import cn.tycoding.langchat.server.utils.ChatR;
+import cn.tycoding.langchat.server.utils.TextR;
 import cn.tycoding.langchat.server.utils.ImageR;
 import cn.tycoding.langchat.server.component.PromptStore;
 import cn.tycoding.langchat.server.entity.LcOss;
@@ -32,14 +34,23 @@ public class ChatServiceImpl implements ChatService {
     private final OssMapper ossMapper;
 
     @Override
-    public void stream(ChatR req, PromptConst promptConst) {
+    public void chat(ChatR req) {
+        PromptTemplate promptTemplate = PromptTemplate.from(PromptStore.get(PromptConst.CHAT));
+        Prompt prompt = promptTemplate.apply(BeanUtil.beanToMap(req, false, true));
+        ChatData data = new ChatData();
+        BeanUtils.copyProperties(req, data);
+        langChatService.chat(req.getEmitter(), prompt, data);
+    }
+
+    @Override
+    public void stream(TextR req, PromptConst promptConst) {
         PromptTemplate promptTemplate = PromptTemplate.from(PromptStore.get(promptConst));
         Prompt prompt = promptTemplate.apply(BeanUtil.beanToMap(req, false, true));
         langChatService.stream(new ChatReq(prompt, req.getEmitter()));
     }
 
     @Override
-    public String text(ChatR req, PromptConst promptConst) {
+    public String text(TextR req, PromptConst promptConst) {
         PromptTemplate promptTemplate = PromptTemplate.from(PromptStore.get(promptConst));
         Prompt prompt = promptTemplate.apply(BeanUtil.beanToMap(req, false, true));
         return langChatService.text(new ChatReq(prompt));

+ 2 - 2
langchat-server/src/main/java/cn/tycoding/langchat/server/service/impl/ClientFileServiceImpl.java

@@ -5,7 +5,7 @@ import cn.tycoding.langchat.core.service.LangDocService;
 import cn.tycoding.langchat.core.utils.OssR;
 import cn.tycoding.langchat.core.utils.OssUtil;
 import cn.tycoding.langchat.common.constant.PromptConst;
-import cn.tycoding.langchat.server.utils.ChatR;
+import cn.tycoding.langchat.server.utils.TextR;
 import cn.tycoding.langchat.server.entity.LcOss;
 import cn.tycoding.langchat.server.service.ClientFileService;
 import lombok.AllArgsConstructor;
@@ -28,7 +28,7 @@ public class ClientFileServiceImpl implements ClientFileService {
     private final LangDocService langDocService;
 
     @Override
-    public void chat(ChatR req, PromptConst promptConst) {
+    public void chat(TextR req, PromptConst promptConst) {
 //        langDocService.stream(new ChatReq(req.getContent(), PromptStore.get(promptConst), req.getEmitter()));
     }
 

+ 11 - 2
langchat-server/src/main/java/cn/tycoding/langchat/server/service/impl/MessageServiceImpl.java

@@ -60,8 +60,10 @@ public class MessageServiceImpl extends ServiceImpl<MessageMapper, LcMessage> im
     }
 
     @Override
-    public void delConversation(Long id) {
-        conversationMapper.deleteById(id);
+    public void delConversation(String conversationId) {
+        conversationMapper.deleteById(conversationId);
+        baseMapper.delete(
+                Wrappers.<LcMessage>lambdaQuery().eq(LcMessage::getConversationId, conversationId));
     }
 
     @Override
@@ -74,7 +76,14 @@ public class MessageServiceImpl extends ServiceImpl<MessageMapper, LcMessage> im
             message.setConversationId(conversation.getId());
         }
 
+        message.setCreateTime(new Date());
         baseMapper.insert(message);
     }
+
+    @Override
+    public void clearMessage(String conversationId) {
+        baseMapper.delete(
+                Wrappers.<LcMessage>lambdaQuery().eq(LcMessage::getConversationId, conversationId));
+    }
 }
 

+ 6 - 32
langchat-server/src/main/java/cn/tycoding/langchat/server/utils/ChatR.java

@@ -1,44 +1,18 @@
 package cn.tycoding.langchat.server.utils;
 
 import cn.tycoding.langchat.core.utils.StreamEmitter;
+import cn.tycoding.langchat.server.entity.LcMessage;
 import lombok.Data;
+import lombok.EqualsAndHashCode;
 
 /**
  * @author tycoding
- * @since 2024/1/4
+ * @since 2024/2/20
  */
 @Data
-public class ChatR {
+@EqualsAndHashCode(callSuper = true)
+public class ChatR extends LcMessage {
+    private static final long serialVersionUID = 2838308353711307727L;
 
     private StreamEmitter emitter;
-
-    /**
-     * 输入内容
-     */
-    private String content;
-
-    /**
-     * 角色
-     */
-    private String role;
-
-    /**
-     * 输出内容类型
-     */
-    private String type;
-
-    /**
-     * 输出内容语言
-     */
-    private String language;
-
-    /**
-     * 输出内容语气
-     */
-    private String tone;
-
-    /**
-     * 输出内容长度
-     */
-    private String length;
 }

+ 44 - 0
langchat-server/src/main/java/cn/tycoding/langchat/server/utils/TextR.java

@@ -0,0 +1,44 @@
+package cn.tycoding.langchat.server.utils;
+
+import cn.tycoding.langchat.core.utils.StreamEmitter;
+import lombok.Data;
+
+/**
+ * @author tycoding
+ * @since 2024/1/4
+ */
+@Data
+public class TextR {
+
+    private StreamEmitter emitter;
+
+    /**
+     * 输入内容
+     */
+    private String content;
+
+    /**
+     * 角色
+     */
+    private String role;
+
+    /**
+     * 输出内容类型
+     */
+    private String type;
+
+    /**
+     * 输出内容语言
+     */
+    private String language;
+
+    /**
+     * 输出内容语气
+     */
+    private String tone;
+
+    /**
+     * 输出内容长度
+     */
+    private String length;
+}

+ 8 - 2
langchat-ui/src/api/conversation.ts

@@ -29,9 +29,15 @@ export function update(params: Partial<Conversation>) {
   });
 }
 
-export function del(id: string) {
+export function del(conversationId: string) {
   return http.delete({
-    url: `/langchat/conversation/${id}`,
+    url: `/langchat/conversation/${conversationId}`,
+  });
+}
+
+export function clearMessage(conversationId: string | undefined) {
+  return http.delete({
+    url: `/langchat/conversation/message/${conversationId}`,
   });
 }
 

+ 5 - 3
langchat-ui/src/locales/zh-CN.ts

@@ -74,8 +74,7 @@ export default {
     user: '用户中心',
   },
   chat: {
-    newChatButton: '新建聊天',
-    placeholder: '来说点什么吧...(Shift + Enter = 换行,"/" 触发提示词)',
+    placeholder: '请输入您的问题...(Shift + Enter 换行,按下 Enter 发送)',
     placeholderMobile: '来说点什么...',
     copy: '复制',
     copied: '复制成功',
@@ -92,9 +91,12 @@ export default {
     deleteMessage: '删除消息',
     deleteMessageConfirm: '是否删除此消息?',
     deleteHistoryConfirm: '确定删除此记录?',
-    clearHistoryConfirm: '确定清空记录?',
+    deleteConversationConfirm: '确定删除会话内容?',
     preview: '预览',
     showRawText: '显示原文',
+    filePlaceholder: '上传图片或者文件内容',
+    searchPlaceholder: '搜索会话',
+    newChatButton: '新建会话',
   },
   store: {
     siderButton: '提示词商店',

+ 75 - 2
langchat-ui/src/views/modules/chat/Header.vue

@@ -4,18 +4,74 @@
   import { useChatStore } from '@/views/modules/chat/store/useChatStore';
   import { ModelList } from '@/api/chat';
   import { computed } from 'vue';
+  import { useDialog, useMessage } from 'naive-ui';
+  import { t } from '@/locales';
+  import { clearMessage } from '@/api/conversation';
+  import html2canvas from 'html2canvas';
 
   const { isMobile } = useBasicLayout();
   const chatStore = useChatStore();
+  const dialog = useDialog();
+  const ms = useMessage();
 
   const chatModel = computed(() => {
     return ModelList.filter((i) => i.value == chatStore.model)[0].label;
   });
+
+  function onClear() {
+    dialog.warning({
+      title: t('chat.clearChat'),
+      content: t('chat.clearChatConfirm'),
+      positiveText: t('common.yes'),
+      negativeText: t('common.no'),
+      onPositiveClick: async () => {
+        await clearMessage(chatStore.curConversation?.id);
+      },
+    });
+  }
+
+  function handleExport() {
+    const d = dialog.warning({
+      title: t('chat.exportImage'),
+      content: t('chat.exportImageConfirm'),
+      positiveText: t('common.yes'),
+      negativeText: t('common.no'),
+      onPositiveClick: async () => {
+        try {
+          d.loading = true;
+          const ele = document.getElementById('image-wrapper');
+          const canvas = await html2canvas(ele as HTMLDivElement, {
+            useCORS: true,
+            height: ele?.scrollHeight,
+            windowHeight: ele?.scrollHeight,
+          });
+          const imgUrl = canvas.toDataURL('image/png');
+          const tempLink = document.createElement('a');
+          tempLink.style.display = 'none';
+          tempLink.href = imgUrl;
+          tempLink.setAttribute('download', 'chat-shot.png');
+          if (typeof tempLink.download === 'undefined') tempLink.setAttribute('target', '_blank');
+
+          document.body.appendChild(tempLink);
+          tempLink.click();
+          document.body.removeChild(tempLink);
+          window.URL.revokeObjectURL(imgUrl);
+          d.loading = false;
+          ms.success(t('chat.exportSuccess'));
+          Promise.resolve();
+        } catch (error: any) {
+          ms.error(t('chat.exportFailed'));
+        } finally {
+          d.loading = false;
+        }
+      },
+    });
+  }
 </script>
 
 <template>
   <header
-    class="sticky pl-2 pr-2 z-30 border-b dark:border-neutral-800 bg-white/80 dark:bg-black/20 backdrop-blur"
+    class="sticky pl-4 pr-6 z-30 border-b dark:border-neutral-800 bg-white/80 dark:bg-black/20 backdrop-blur"
   >
     <div class="relative flex items-center justify-between min-w-0 overflow-hidden h-14 ml-2 mr-2">
       <div class="flex items-center">
@@ -32,8 +88,25 @@
           </n-button>
         </n-popselect>
       </div>
+
       <div class="flex items-center space-x-2">
-        <n-button text><SvgIcon class="text-lg" icon="material-symbols:download" /></n-button>
+        <n-popover trigger="hover">
+          <template #trigger>
+            <n-button @click="onClear" text>
+              <SvgIcon class="text-lg" icon="fluent:delete-28-regular" />
+            </n-button>
+          </template>
+          <span>{{ t('chat.clearChat') }}</span>
+        </n-popover>
+
+        <n-popover trigger="hover">
+          <template #trigger>
+            <n-button @click="handleExport" text>
+              <SvgIcon class="text-xl" icon="material-symbols:download" />
+            </n-button>
+          </template>
+          <span>{{ t('chat.exportImage') }}</span>
+        </n-popover>
       </div>
     </div>
   </header>

+ 28 - 69
langchat-ui/src/views/modules/chat/index.vue

@@ -1,5 +1,5 @@
 <script lang="ts" setup>
-  import { computed, onMounted, onUnmounted, Ref, ref } from 'vue';
+  import { computed, onMounted, onUnmounted, onUpdated, Ref, ref } from 'vue';
   import { SvgIcon } from '@/components/common';
   import { v4 as uuidv4 } from 'uuid';
   import { chat } from '@/api/chat';
@@ -13,6 +13,7 @@
   import { useRouter } from 'vue-router';
   import Header from './Header.vue';
   import { addMessage } from '@/api/conversation';
+  import { t } from '@/locales';
 
   const router = useRouter();
   const dialog = useDialog();
@@ -25,7 +26,7 @@
   const loading = ref<boolean>(false);
   const prompt = ref<string>('');
   const chatId = ref<string>('');
-  const parentChatId = ref<string>('');
+  const aiChatId = ref<string>('');
   const inputRef = ref<Ref | null>(null);
 
   // 初始化加载数据
@@ -48,13 +49,7 @@
 
     // user
     chatId.value = uuidv4();
-    parentChatId.value = uuidv4();
-    const messageData = await chatStore.addMessage(
-      message,
-      'user',
-      chatId.value,
-      parentChatId.value
-    );
+    const messageData = await chatStore.addMessage(message, 'user', chatId.value);
     await addMessage(messageData);
 
     loading.value = true;
@@ -65,24 +60,19 @@
     }
 
     // ai
-    await chatStore.addMessage('', 'assistant', parentChatId.value, chatId.value);
+    aiChatId.value = uuidv4();
+    await chatStore.addMessage('', 'assistant', aiChatId.value);
     await scrollToBottom();
-    await onConversation(message, false, chatId.value, parentChatId.value);
+    await onChat(message);
   }
 
-  async function onConversation(
-    message: string,
-    isRegenerate: boolean,
-    chatId?: string,
-    parentChatId?: string
-  ) {
+  async function onChat(message: string) {
     try {
       // 定义接口
       const fetchChatAPIOnce = async () => {
         await chat(
           {
-            chatId,
-            parentChatId,
+            chatId: chatId.value,
             content: message,
             role: 'user',
             conversationId: chatStore.curConversation?.id,
@@ -102,11 +92,13 @@
               }
               text += content;
             });
-            // 只更新AI回答,promptId要反转
-            chatStore.updateMessage(parentChatId, text);
+            chatStore.updateMessage(aiChatId.value, text, false);
             scrollToBottomIfAtBottom();
           }
-        ).catch(() => {});
+        ).catch((err: any) => {
+          console.error(err);
+          chatStore.updateMessage(aiChatId.value, err, true);
+        });
       };
 
       // 调用接口
@@ -116,24 +108,6 @@
     }
   }
 
-  // 重新生成
-  async function onRegenerate(item: AiMessage) {
-    if (loading.value) {
-      return;
-    }
-    const index = chatStore.messages.findIndex((i) => i.chatId == item.parentChatId);
-    if (index === -1) {
-      ms.warning('数据异常,无法重新生成');
-      return;
-    }
-    loading.value = true;
-    await chatStore.updateMessage(item.chatId, '');
-
-    const message = String(chatStore.messages[index].content);
-    // 对于AI的回答重新生成,promptId要反转设置
-    await onConversation(message, true, item.parentChatId, item.chatId);
-  }
-
   // 删除
   function handleDelete(item: AiMessage) {
     if (loading.value) {
@@ -141,10 +115,10 @@
     }
 
     dialog.warning({
-      title: '删除消息',
-      content: '确认删除消息',
-      positiveText: '是',
-      negativeText: '否',
+      title: t('chat.deleteMessage'),
+      content: t('chat.deleteMessageConfirm'),
+      positiveText: t('common.yes'),
+      negativeText: t('common.no'),
       onPositiveClick: () => {
         chatStore.delMessage(item);
       },
@@ -152,23 +126,6 @@
     chatId.value = '';
   }
 
-  // 清除
-  function handleClear() {
-    if (loading.value) {
-      return;
-    }
-    dialog.warning({
-      title: '清除聊天',
-      content: '确认清除聊天',
-      positiveText: '是',
-      negativeText: '否',
-      onPositiveClick: async () => {
-        console.log('清除聊天');
-      },
-    });
-    chatId.value = '';
-  }
-
   function handleEnter(event: KeyboardEvent) {
     if (!isMobile.value) {
       if (event.key === 'Enter' && !event.shiftKey) {
@@ -195,7 +152,7 @@
   });
 
   const footerClass = computed(() => {
-    let classes = ['p-4 pt-0'];
+    let classes = ['p-8 pt-0'];
     if (isMobile.value) {
       classes = ['sticky', 'left-0', 'bottom-0', 'right-0', 'p-2', 'pr-3', 'overflow-hidden'];
     }
@@ -223,6 +180,10 @@
       controller.abort();
     }
   });
+
+  onUpdated(() => {
+    scrollToBottomIfAtBottom();
+  });
 </script>
 
 <template>
@@ -236,13 +197,13 @@
         <div class="flex flex-col w-full h-full">
           <Header />
 
-          <!-- 聊天记录窗口 -->
+          <!-- chat -->
           <main class="flex-1 overflow-hidden">
-            <div ref="contentRef" class="h-full overflow-hidden overflow-y-auto">
+            <div ref="contentRef" class="h-full overflow-hidden overflow-y-auto" id="image-wrapper">
               <div v-if="chatIsLoading" class="w-full h-full flex items-center justify-center">
                 <n-spin :show="chatIsLoading" size="large" />
               </div>
-              <div v-else ref="scrollRef" class="w-full m-auto" :class="[isMobile ? 'p-2' : 'p-5']">
+              <div v-else ref="scrollRef" class="w-full m-auto" :class="[isMobile ? 'p-2' : 'p-8']">
                 <Message
                   v-for="(item, index) of dataSources"
                   :key="index"
@@ -251,7 +212,6 @@
                   :inversion="item.role !== 'assistant'"
                   :error="item.isError"
                   :loading="loading"
-                  @regenerate="onRegenerate(item)"
                   @delete="handleDelete(item)"
                 />
                 <div class="sticky bottom-0 left-0 flex justify-center">
@@ -266,7 +226,6 @@
             </div>
           </main>
 
-          <!-- 底部 -->
           <footer :class="footerClass">
             <div class="w-full m-auto">
               <div class="flex items-center justify-between space-x-2 w-full">
@@ -276,7 +235,7 @@
                   type="textarea"
                   :autosize="{ minRows: 3, maxRows: isMobile ? 4 : 8 }"
                   @keypress="handleEnter"
-                  placeholder="请输入您的问题...(Shift + Enter 换行,按下 Enter 发送)"
+                  :placeholder="t('chat.placeholder')"
                 >
                   <template #suffix>
                     <div
@@ -297,7 +256,7 @@
                             </template>
                           </n-button>
                         </template>
-                        <span>上传图片或者文件信息</span>
+                        <span>{{ t('chat.filePlaceholder') }}</span>
                       </n-popover>
 
                       <n-button

+ 7 - 30
langchat-ui/src/views/modules/chat/message/Message.vue

@@ -3,10 +3,10 @@
   import { useMessage } from 'naive-ui';
   import TextComponent from './TextComponent.vue';
   import AvatarComponent from './Avatar.vue';
-  import { SvgIcon } from '@/components/common';
   import { useBasicLayout } from '../store/useBasicLayout';
   import { useIconRender } from '../store/useIconRender';
   import { copyToClip } from '@/utils/copy';
+  import { t } from '@/locales';
 
   interface Props {
     dateTime?: string;
@@ -17,35 +17,27 @@
   }
 
   interface Emit {
-    (ev: 'regenerate'): void;
     (ev: 'delete'): void;
   }
-
   const props = defineProps<Props>();
-
   const emit = defineEmits<Emit>();
 
   const { isMobile } = useBasicLayout();
-
   const { iconRender } = useIconRender();
-
   const message = useMessage();
-
   const textRef = ref<HTMLElement>();
-
   const asRawText = ref(props.inversion);
-
   const messageRef = ref<HTMLElement>();
 
   const options = computed(() => {
     const common = [
       {
-        label: '删除',
+        label: t('chat.deleteMessage'),
         key: 'delete',
         icon: iconRender({ icon: 'ri:delete-bin-line' }),
       },
       {
-        label: '复制',
+        label: t('chat.copy'),
         key: 'copyText',
         icon: iconRender({ icon: 'ri:file-copy-2-line' }),
       },
@@ -53,7 +45,7 @@
 
     if (!props.inversion) {
       common.unshift({
-        label: asRawText.value ? '预览' : '显示原文',
+        label: asRawText.value ? t('chat.preview') : t('chat.showRawText'),
         key: 'toggleRenderType',
         icon: iconRender({ icon: asRawText.value ? 'ic:outline-code-off' : 'ic:outline-code' }),
       });
@@ -63,7 +55,6 @@
   });
 
   function handleSelect(key: any) {
-    console.log('点击', key);
     switch (key) {
       case 'copyText':
         handleCopy();
@@ -76,17 +67,12 @@
     }
   }
 
-  function handleRegenerate() {
-    messageRef.value?.scrollIntoView();
-    emit('regenerate');
-  }
-
   async function handleCopy() {
     try {
       await copyToClip(props.text || '');
-      message.success('复制成功');
-    } catch {
-      message.error('复制失败');
+      message.success(t('chat.copied'));
+    } catch (e: any) {
+      console.error(e);
     }
   }
 </script>
@@ -120,15 +106,6 @@
           :as-raw-text="asRawText"
         />
         <div class="flex flex-row justify-start items-start gap-1">
-          <button
-            v-if="!inversion"
-            :disabled="loading"
-            class="mb-2 transition text-neutral-300 hover:text-neutral-800"
-            @click="handleRegenerate"
-          >
-            <SvgIcon icon="ri:restart-line" />
-          </button>
-
           <n-popover
             v-for="item in options"
             :key="item"

+ 3 - 2
langchat-ui/src/views/modules/chat/message/TextComponent.vue

@@ -6,6 +6,7 @@
   import hljs from 'highlight.js';
   import { useBasicLayout } from '../store/useBasicLayout';
   import { copyToClip } from '@/utils/copy';
+  import { t } from '@/locales';
 
   interface Props {
     inversion?: boolean;
@@ -71,9 +72,9 @@
           const code = btn.parentElement?.nextElementSibling?.textContent;
           if (code) {
             copyToClip(code).then(() => {
-              btn.textContent = '复制成功';
+              btn.textContent = t('chat.copied');
               setTimeout(() => {
-                btn.textContent = '复制代码';
+                btn.textContent = t('chat.copyCode');
               }, 1000);
             });
           }

+ 4 - 4
langchat-ui/src/views/modules/chat/sider/List.vue

@@ -1,18 +1,18 @@
 <script setup lang="ts">
-  import { computed } from 'vue';
+  import { computed, nextTick } from 'vue';
   import { NInput, NPopconfirm, NScrollbar } from 'naive-ui';
   import { SvgIcon } from '@/components/common';
   import { useChatStore } from '../store/useChatStore';
   import { debounce } from '@/utils/debounce';
   import { Conversation } from '@/typings/chat';
-  const chatStore = useChatStore();
+  import { t } from '@/locales';
 
+  const chatStore = useChatStore();
   const dataSources = computed(() => {
     return chatStore.conversations;
   });
 
   async function handleSelect(item: Conversation) {
-    console.log(item);
     if (isActive(item.id)) return;
     await chatStore.selectConversation(item);
   }
@@ -90,7 +90,7 @@
                       <SvgIcon icon="ri:delete-bin-line" />
                     </button>
                   </template>
-                  确认删除?
+                  {{ t('chat.deleteConversationConfirm') }}
                 </NPopconfirm>
               </template>
             </div>

+ 19 - 5
langchat-ui/src/views/modules/chat/sider/index.vue

@@ -3,9 +3,12 @@
   import { computed, watch } from 'vue';
   import { SvgIcon } from '@/components/common';
   import { NButton, NLayoutSider, useMessage } from 'naive-ui';
-  import List from './List.vue';
   import { useChatStore } from '../store/useChatStore';
   import { useBasicLayout } from '../store/useBasicLayout';
+  import List from './List.vue';
+  import { add as addConversation } from '@/api/conversation';
+  import { t } from '@/locales';
+
   const chatStore = useChatStore();
   const { isMobile } = useBasicLayout();
   const ms = useMessage();
@@ -48,6 +51,11 @@
       flush: 'post',
     }
   );
+
+  async function onAddConversation() {
+    await addConversation({});
+    await chatStore.loadData();
+  }
 </script>
 
 <template>
@@ -68,12 +76,18 @@
       </div>
       <main v-else class="flex flex-col flex-1 min-h-0">
         <div class="p-4 pt-3 flex justify-between items-center gap-2">
-          <n-input size="small" placeholder="搜索">
+          <n-input size="small" :placeholder="t('chat.searchPlaceholder')">
             <template #prefix> <SvgIcon icon="carbon:search" /> </template>
           </n-input>
-          <n-button size="small" type="success" secondary>
-            <SvgIcon icon="ic:round-plus" />
-          </n-button>
+
+          <n-popover trigger="hover">
+            <template #trigger>
+              <n-button @click="onAddConversation" size="small" type="success" secondary>
+                <SvgIcon icon="ic:round-plus" />
+              </n-button>
+            </template>
+            <span>{{ t('chat.newChatButton') }}</span>
+          </n-popover>
         </div>
         <div class="flex-1 min-h-0 pb-4 overflow-hidden">
           <List />

+ 6 - 13
langchat-ui/src/views/modules/chat/store/useChatStore.ts

@@ -90,6 +90,7 @@ export const useChatStore = defineStore('chat-store', {
       await this.setEdit('');
       this.curConversation = params;
       this.messages = await getMessages(params.id);
+      await this.selectPath(params.id);
     },
 
     /**
@@ -116,23 +117,15 @@ export const useChatStore = defineStore('chat-store', {
       await delConversations(id);
       await this.setActive('');
       await this.loadData();
-      this.messages = [];
     },
 
     /**
      * 新增消息
      */
-    async addMessage(
-      message: string,
-      role: 'user' | 'assistant' | 'system',
-      chatId: string,
-      parentChatId: string
-    ) {
+    async addMessage(message: string, role: 'user' | 'assistant' | 'system', chatId: string) {
       const data = {
         chatId,
-        parentRefId: parentChatId,
         conversationId: this.curConversation?.id,
-        chatModel: '',
         role: role,
         content: message,
         createTime: formatToDateTime(new Date()),
@@ -145,10 +138,10 @@ export const useChatStore = defineStore('chat-store', {
      * 更新消息
      */
     async updateMessage(chatId: string | undefined, content: string, isError?: boolean) {
-      const promptIndex = this.messages.findIndex((item) => item?.chatId == chatId);
-      if (promptIndex !== -1) {
-        this.messages[promptIndex].content = content;
-        this.messages[promptIndex].isError = isError;
+      const index = this.messages.findIndex((item) => item?.chatId == chatId);
+      if (index !== -1) {
+        this.messages[index].content = content;
+        this.messages[index].isError = isError;
       }
     },