mirror of
https://github.com/ccmjga/zhilu-admin
synced 2026-04-04 19:27:32 +00:00
init command execute mode
This commit is contained in:
@@ -0,0 +1,38 @@
|
|||||||
|
package com.zl.mjga.config.ai;
|
||||||
|
|
||||||
|
import com.zl.mjga.exception.BusinessException;
|
||||||
|
import com.zl.mjga.repository.DepartmentRepository;
|
||||||
|
import com.zl.mjga.service.DepartmentService;
|
||||||
|
import dev.langchain4j.agent.tool.P;
|
||||||
|
import dev.langchain4j.agent.tool.Tool;
|
||||||
|
import dev.langchain4j.model.output.structured.Description;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.jooq.generated.mjga.tables.pojos.Department;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Description("和部门管理有关的操作工具")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Component
|
||||||
|
public class DepartmentOperatorTool {
|
||||||
|
|
||||||
|
private final DepartmentService departmentService;
|
||||||
|
private final DepartmentRepository departmentRepository;
|
||||||
|
|
||||||
|
@Tool(value = "创建部门")
|
||||||
|
void createDepartment(
|
||||||
|
@P(value = "部门名称") String departmentName,
|
||||||
|
@P(value = "上级部门名称", required = false) String parentDepartmentName) {
|
||||||
|
Department exist = departmentRepository.fetchOneByName(departmentName);
|
||||||
|
if (exist != null) {
|
||||||
|
throw new BusinessException("当前部门已存在");
|
||||||
|
}
|
||||||
|
if (StringUtils.isNotEmpty(parentDepartmentName)) {
|
||||||
|
Department parent = departmentRepository.fetchOneByName(parentDepartmentName);
|
||||||
|
if (parent == null) {
|
||||||
|
throw new BusinessException("上级部门不存在");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
departmentService.upsertDepartment(new Department(null, departmentName, null));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.zl.mjga.config.ai;
|
||||||
|
|
||||||
|
import dev.langchain4j.service.MemoryId;
|
||||||
|
import dev.langchain4j.service.TokenStream;
|
||||||
|
import dev.langchain4j.service.UserMessage;
|
||||||
|
import dev.langchain4j.service.memory.ChatMemoryAccess;
|
||||||
|
|
||||||
|
public interface SystemToolAssistant extends ChatMemoryAccess {
|
||||||
|
TokenStream ask(@MemoryId String memoryId, @UserMessage String question);
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package com.zl.mjga.config.ai;
|
||||||
|
|
||||||
|
import dev.langchain4j.community.model.zhipu.ZhipuAiStreamingChatModel;
|
||||||
|
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
|
||||||
|
import dev.langchain4j.service.AiServices;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.context.annotation.DependsOn;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ToolsInitializer {
|
||||||
|
|
||||||
|
private final UserOperatorTool userOperatorTool;
|
||||||
|
private final DepartmentOperatorTool departmentOperatorTool;
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@DependsOn("flywayInitializer")
|
||||||
|
public SystemToolAssistant zhiPuToolAssistant(ZhipuAiStreamingChatModel zhipuChatModel) {
|
||||||
|
return AiServices.builder(SystemToolAssistant.class)
|
||||||
|
.streamingChatModel(zhipuChatModel)
|
||||||
|
.chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(10))
|
||||||
|
.tools(userOperatorTool, departmentOperatorTool)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package com.zl.mjga.config.ai;
|
||||||
|
|
||||||
|
import com.zl.mjga.dto.urp.UserUpsertDto;
|
||||||
|
import com.zl.mjga.exception.BusinessException;
|
||||||
|
import com.zl.mjga.repository.UserRepository;
|
||||||
|
import com.zl.mjga.service.IdentityAccessService;
|
||||||
|
import dev.langchain4j.agent.tool.P;
|
||||||
|
import dev.langchain4j.agent.tool.Tool;
|
||||||
|
import dev.langchain4j.model.output.structured.Description;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.jooq.generated.mjga.tables.pojos.User;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Description("和用户管理有关的操作工具")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Component
|
||||||
|
public class UserOperatorTool {
|
||||||
|
|
||||||
|
private final IdentityAccessService identityAccessService;
|
||||||
|
private final UserRepository userRepository;
|
||||||
|
|
||||||
|
@Tool(value = "创建用户或注册用户")
|
||||||
|
void createUser(@P(value = "用户名") String username) {
|
||||||
|
User user = userRepository.fetchOneByUsername(username);
|
||||||
|
if (user != null) {
|
||||||
|
throw new BusinessException("用户已存在");
|
||||||
|
}
|
||||||
|
identityAccessService.upsertUser(new UserUpsertDto(null, username, username, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Tool(value = "删除用户")
|
||||||
|
void deleteUser(@P(value = "用户名") String username) {
|
||||||
|
userRepository.deleteByUsername(username);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Tool(value = "编辑/更新/更改用户")
|
||||||
|
void updateUser(
|
||||||
|
@P(value = "用户名") String username,
|
||||||
|
@P(value = "密码", required = false) String password,
|
||||||
|
@P(value = "是否开启", required = false) Boolean enable) {
|
||||||
|
identityAccessService.upsertUser(new UserUpsertDto(null, username, password, enable));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -42,11 +42,34 @@ public class AiController {
|
|||||||
private final UserRepository userRepository;
|
private final UserRepository userRepository;
|
||||||
private final DepartmentRepository departmentRepository;
|
private final DepartmentRepository departmentRepository;
|
||||||
|
|
||||||
|
@PostMapping(value = "/action/execute", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
||||||
|
public Flux<String> actionExecute(Principal principal, @RequestBody String userMessage) {
|
||||||
|
Sinks.Many<String> sink = Sinks.many().unicast().onBackpressureBuffer();
|
||||||
|
TokenStream chat = aiChatService.actionExecuteWithZhiPu(principal.getName(), userMessage);
|
||||||
|
chat.onPartialResponse(
|
||||||
|
text ->
|
||||||
|
sink.tryEmitNext(
|
||||||
|
StringUtils.isNotEmpty(text) ? text.replace(" ", "␣").replace("\t", "⇥") : ""))
|
||||||
|
.onToolExecuted(
|
||||||
|
toolExecution -> log.debug("当前请求 {} 成功执行函数调用: {}", userMessage, toolExecution))
|
||||||
|
.onCompleteResponse(
|
||||||
|
r -> {
|
||||||
|
sink.tryEmitComplete();
|
||||||
|
sink.emitComplete(Sinks.EmitFailureHandler.FAIL_FAST);
|
||||||
|
})
|
||||||
|
.onError(sink::tryEmitError)
|
||||||
|
.start();
|
||||||
|
return sink.asFlux().timeout(Duration.ofSeconds(120));
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping(value = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
@PostMapping(value = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
||||||
public Flux<String> chat(Principal principal, @RequestBody String userMessage) {
|
public Flux<String> chat(Principal principal, @RequestBody String userMessage) {
|
||||||
Sinks.Many<String> sink = Sinks.many().unicast().onBackpressureBuffer();
|
Sinks.Many<String> sink = Sinks.many().unicast().onBackpressureBuffer();
|
||||||
TokenStream chat = aiChatService.chatPrecedenceLlmWith(principal.getName(), userMessage);
|
TokenStream chat = aiChatService.chatPrecedenceLlmWith(principal.getName(), userMessage);
|
||||||
chat.onPartialResponse(text -> sink.tryEmitNext(text.replace(" ", "␣").replace("\t", "⇥")))
|
chat.onPartialResponse(
|
||||||
|
text ->
|
||||||
|
sink.tryEmitNext(
|
||||||
|
StringUtils.isNotEmpty(text) ? text.replace(" ", "␣").replace("\t", "⇥") : ""))
|
||||||
.onCompleteResponse(
|
.onCompleteResponse(
|
||||||
r -> {
|
r -> {
|
||||||
sink.tryEmitComplete();
|
sink.tryEmitComplete();
|
||||||
@@ -71,8 +94,8 @@ public class AiController {
|
|||||||
return llmService.pageQueryLlm(pageRequestDto, llmQueryDto);
|
return llmService.pageQueryLlm(pageRequestDto, llmQueryDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/action/chat")
|
@PostMapping("/action/search")
|
||||||
public Map<String, String> actionChat(@RequestBody String message) {
|
public Map<String, String> searchAction(@RequestBody String message) {
|
||||||
AiLlmConfig aiLlmConfig = llmService.loadConfig(LlmCodeEnum.ZHI_PU);
|
AiLlmConfig aiLlmConfig = llmService.loadConfig(LlmCodeEnum.ZHI_PU);
|
||||||
if (!aiLlmConfig.getEnable()) {
|
if (!aiLlmConfig.getEnable()) {
|
||||||
throw new BusinessException("命令模型未启用,请开启后再试。");
|
throw new BusinessException("命令模型未启用,请开启后再试。");
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.zl.mjga.service;
|
package com.zl.mjga.service;
|
||||||
|
|
||||||
import com.zl.mjga.config.ai.AiChatAssistant;
|
import com.zl.mjga.config.ai.AiChatAssistant;
|
||||||
|
import com.zl.mjga.config.ai.SystemToolAssistant;
|
||||||
import com.zl.mjga.exception.BusinessException;
|
import com.zl.mjga.exception.BusinessException;
|
||||||
import dev.langchain4j.service.TokenStream;
|
import dev.langchain4j.service.TokenStream;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
@@ -17,6 +18,7 @@ public class AiChatService {
|
|||||||
|
|
||||||
private final AiChatAssistant deepSeekChatAssistant;
|
private final AiChatAssistant deepSeekChatAssistant;
|
||||||
private final AiChatAssistant zhiPuChatAssistant;
|
private final AiChatAssistant zhiPuChatAssistant;
|
||||||
|
private final SystemToolAssistant zhiPuToolAssistant;
|
||||||
private final LlmService llmService;
|
private final LlmService llmService;
|
||||||
|
|
||||||
public TokenStream chatWithDeepSeek(String sessionIdentifier, String userMessage) {
|
public TokenStream chatWithDeepSeek(String sessionIdentifier, String userMessage) {
|
||||||
@@ -27,14 +29,22 @@ public class AiChatService {
|
|||||||
return zhiPuChatAssistant.chat(sessionIdentifier, userMessage);
|
return zhiPuChatAssistant.chat(sessionIdentifier, userMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public TokenStream actionExecuteWithZhiPu(String sessionIdentifier, String userMessage) {
|
||||||
|
return zhiPuToolAssistant.ask(sessionIdentifier, userMessage);
|
||||||
|
}
|
||||||
|
|
||||||
public TokenStream chatPrecedenceLlmWith(String sessionIdentifier, String userMessage) {
|
public TokenStream chatPrecedenceLlmWith(String sessionIdentifier, String userMessage) {
|
||||||
Optional<AiLlmConfig> precedenceLlmBy = llmService.getPrecedenceChatLlmBy(true);
|
LlmCodeEnum code = getPrecedenceLlmCode();
|
||||||
AiLlmConfig aiLlmConfig = precedenceLlmBy.orElseThrow(() -> new BusinessException("没有开启的大模型"));
|
|
||||||
LlmCodeEnum code = aiLlmConfig.getCode();
|
|
||||||
return switch (code) {
|
return switch (code) {
|
||||||
case ZHI_PU -> zhiPuChatAssistant.chat(sessionIdentifier, userMessage);
|
case ZHI_PU -> zhiPuChatAssistant.chat(sessionIdentifier, userMessage);
|
||||||
case DEEP_SEEK -> deepSeekChatAssistant.chat(sessionIdentifier, userMessage);
|
case DEEP_SEEK -> deepSeekChatAssistant.chat(sessionIdentifier, userMessage);
|
||||||
default -> throw new BusinessException(String.format("无效的模型代码 %s", code));
|
default -> throw new BusinessException(String.format("无效的模型代码 %s", code));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private LlmCodeEnum getPrecedenceLlmCode() {
|
||||||
|
Optional<AiLlmConfig> precedenceLlmBy = llmService.getPrecedenceChatLlmBy(true);
|
||||||
|
AiLlmConfig aiLlmConfig = precedenceLlmBy.orElseThrow(() -> new BusinessException("没有开启的大模型"));
|
||||||
|
return aiLlmConfig.getCode();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import com.zl.mjga.dto.PageResponseDto;
|
|||||||
import com.zl.mjga.dto.ai.LlmQueryDto;
|
import com.zl.mjga.dto.ai.LlmQueryDto;
|
||||||
import com.zl.mjga.dto.ai.LlmVm;
|
import com.zl.mjga.dto.ai.LlmVm;
|
||||||
import com.zl.mjga.repository.LlmRepository;
|
import com.zl.mjga.repository.LlmRepository;
|
||||||
|
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
@@ -37,7 +36,7 @@ public class LlmService {
|
|||||||
List<AiLlmConfig> aiLlmConfigs = llmRepository.fetchByEnable(enable);
|
List<AiLlmConfig> aiLlmConfigs = llmRepository.fetchByEnable(enable);
|
||||||
return aiLlmConfigs.stream()
|
return aiLlmConfigs.stream()
|
||||||
.filter(aiLlmConfig -> LlmTypeEnum.CHAT.equals(aiLlmConfig.getType()))
|
.filter(aiLlmConfig -> LlmTypeEnum.CHAT.equals(aiLlmConfig.getType()))
|
||||||
.max(Comparator.comparingInt(AiLlmConfig::getPriority));
|
.max(Comparator.comparingInt(AiLlmConfig::getPriority));
|
||||||
}
|
}
|
||||||
|
|
||||||
public PageResponseDto<List<LlmVm>> pageQueryLlm(
|
public PageResponseDto<List<LlmVm>> pageQueryLlm(
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ logging:
|
|||||||
file:
|
file:
|
||||||
path: /var/log
|
path: /var/log
|
||||||
level:
|
level:
|
||||||
|
dev:
|
||||||
|
langchain4j: debug
|
||||||
|
com:
|
||||||
|
zl: debug
|
||||||
org:
|
org:
|
||||||
springframework:
|
springframework:
|
||||||
security: debug
|
security: debug
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export default [
|
|||||||
);
|
);
|
||||||
return response;
|
return response;
|
||||||
}),
|
}),
|
||||||
http.post("/ai/action/chat", () => {
|
http.post("/ai/action/search", () => {
|
||||||
const response = HttpResponse.json({
|
const response = HttpResponse.json({
|
||||||
action: "CREATE_USER",
|
action: "CREATE_USER",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -747,12 +747,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/ai/action/chat": {
|
"/ai/action/search": {
|
||||||
"post": {
|
"post": {
|
||||||
"tags": [
|
"tags": [
|
||||||
"ai-controller"
|
"ai-controller"
|
||||||
],
|
],
|
||||||
"operationId": "actionChat",
|
"operationId": "searchAction",
|
||||||
"requestBody": {
|
"requestBody": {
|
||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
@@ -780,6 +780,39 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/ai/action/execute": {
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"ai-controller"
|
||||||
|
],
|
||||||
|
"operationId": "actionExecute",
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"content": {
|
||||||
|
"text/event-stream": {
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/scheduler/page-query": {
|
"/scheduler/page-query": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
|
|||||||
3550
frontend/src/api/types/schema.d.ts
vendored
3550
frontend/src/api/types/schema.d.ts
vendored
File diff suppressed because it is too large
Load Diff
@@ -42,19 +42,21 @@
|
|||||||
<div class="px-4 py-2 bg-white rounded-t-lg">
|
<div class="px-4 py-2 bg-white rounded-t-lg">
|
||||||
<label for="comment" class="sr-only"></label>
|
<label for="comment" class="sr-only"></label>
|
||||||
<textarea id="comment" rows="3" v-model="inputMessage"
|
<textarea id="comment" rows="3" v-model="inputMessage"
|
||||||
class="w-full px-0 text-gray-900 bg-white border-0 focus:ring-0"
|
class="w-full px-0 text-gray-900 bg-white border-0 focus:ring-0" :placeholder="
|
||||||
:placeholder="isCommandMode ? '输入创建用户/删除用户试试看' : '随便聊聊'" required></textarea>
|
commandPlaceholderMap[commandMode]
|
||||||
|
" required></textarea>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between px-2 py-2 border-t border-gray-200">
|
<div class="flex justify-between px-2 py-2 border-t border-gray-200">
|
||||||
<form>
|
<form>
|
||||||
<select id="countries" v-model="isCommandMode"
|
<select id="countries" v-model="commandMode"
|
||||||
class="bg-white border border-gray-300 text-gray-900 text-sm rounded-lg block">
|
class="bg-white border border-gray-300 text-gray-900 text-sm rounded-lg block">
|
||||||
<option selected :value="false">询问模式</option>
|
<option selected :value="'chat'">询问模式</option>
|
||||||
<option :value="true">指令模式</option>
|
<option :value="'search'">搜索模式</option>
|
||||||
|
<option :value="'execute'">指令模式</option>
|
||||||
</select>
|
</select>
|
||||||
</form>
|
</form>
|
||||||
<Button :abortable="true" :isLoading="isLoading" :loadingContent="'中止'" :submitContent="'发送'"
|
<Button :abortable="true" :isLoading="isLoading" :loadingContent="'中止'" :submitContent="'发送'"
|
||||||
:handleClick="() => handleSendClick(inputMessage, isCommandMode)" />
|
:handleClick="() => handleSendClick(inputMessage, commandMode)" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -102,14 +104,14 @@ import { useAiAction } from "@/composables/ai/useAiAction";
|
|||||||
import DepartmentDeleteModal from "@/components/PopupModal.vue";
|
import DepartmentDeleteModal from "@/components/PopupModal.vue";
|
||||||
import InputButton from "@/components/InputButton.vue";
|
import InputButton from "@/components/InputButton.vue";
|
||||||
|
|
||||||
const { messages, chat, isLoading, cancel, actionChat } = useAiChat();
|
const { messages, chat, isLoading, cancel, searchAction, executeAction } = useAiChat();
|
||||||
const { user } = useUserStore();
|
const { user } = useUserStore();
|
||||||
const userUpsertModal = ref<ModalInterface>();
|
const userUpsertModal = ref<ModalInterface>();
|
||||||
const departmentUpsertModal = ref<ModalInterface>();
|
const departmentUpsertModal = ref<ModalInterface>();
|
||||||
const inputMessage = ref("");
|
const inputMessage = ref("");
|
||||||
const chatContainer = ref<HTMLElement | null>(null);
|
const chatContainer = ref<HTMLElement | null>(null);
|
||||||
const alertStore = useAlertStore();
|
const alertStore = useAlertStore();
|
||||||
const isCommandMode = ref(false);
|
const commandMode = ref<"chat" | "search" | "execute">("chat");
|
||||||
const userUpsert = useUserUpsert();
|
const userUpsert = useUserUpsert();
|
||||||
const departmentUpsert = useDepartmentUpsert();
|
const departmentUpsert = useDepartmentUpsert();
|
||||||
const userDeleteModal = ref<ModalInterface>();
|
const userDeleteModal = ref<ModalInterface>();
|
||||||
@@ -121,6 +123,12 @@ const currentDeleteDepartmentName = ref<string>();
|
|||||||
const { availableDepartments, fetchAvailableDepartments } =
|
const { availableDepartments, fetchAvailableDepartments } =
|
||||||
useDepartmentQuery();
|
useDepartmentQuery();
|
||||||
|
|
||||||
|
const commandPlaceholderMap: Record<string, string> = {
|
||||||
|
chat: "随便聊聊",
|
||||||
|
search: "搜索创建用户、删除部门等功能",
|
||||||
|
execute: "帮我创建一个名为 mjga 的用户",
|
||||||
|
};
|
||||||
|
|
||||||
const commandActionMap: Record<string, () => void> = {
|
const commandActionMap: Record<string, () => void> = {
|
||||||
CREATE_USER: () => {
|
CREATE_USER: () => {
|
||||||
userUpsertModal.value?.show();
|
userUpsertModal.value?.show();
|
||||||
@@ -241,7 +249,7 @@ const abortChat = () => {
|
|||||||
cancel();
|
cancel();
|
||||||
};
|
};
|
||||||
|
|
||||||
const chatByMode = async (message: string, mode: boolean) => {
|
const chatByMode = async (message: string, mode: "chat" | "search" | "execute") => {
|
||||||
inputMessage.value = "";
|
inputMessage.value = "";
|
||||||
messages.value.push({
|
messages.value.push({
|
||||||
content: message,
|
content: message,
|
||||||
@@ -249,14 +257,16 @@ const chatByMode = async (message: string, mode: boolean) => {
|
|||||||
isUser: true,
|
isUser: true,
|
||||||
username: user.username!,
|
username: user.username!,
|
||||||
});
|
});
|
||||||
if (mode) {
|
if (mode === "search") {
|
||||||
await actionChat(message);
|
await searchAction(message);
|
||||||
|
} else if (mode === "execute") {
|
||||||
|
await executeAction(message);
|
||||||
} else {
|
} else {
|
||||||
await chat(message);
|
await chat(message);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSendClick = async (message: string, mode: boolean) => {
|
const handleSendClick = async (message: string, mode: "chat" | "search" | "execute") => {
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
if (isLoading.value) {
|
if (isLoading.value) {
|
||||||
abortChat();
|
abortChat();
|
||||||
|
|||||||
@@ -56,16 +56,55 @@ export const useAiChat = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const actionChat = async (message: string) => {
|
const executeAction = async (message: string) => {
|
||||||
|
isLoading.value = true;
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
const ctrl = new AbortController();
|
||||||
|
currentController = ctrl;
|
||||||
|
messages.value.push({
|
||||||
|
content: "",
|
||||||
|
type: "chat",
|
||||||
|
isUser: false,
|
||||||
|
username: "知路智能体",
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const baseUrl = `${import.meta.env.VITE_BASE_URL}`;
|
||||||
|
await fetchEventSource(`${baseUrl}/ai/action/execute`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: authStore.get(),
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: message,
|
||||||
|
signal: ctrl.signal,
|
||||||
|
onmessage(ev) {
|
||||||
|
messages.value[messages.value.length - 1].content += ev.data;
|
||||||
|
},
|
||||||
|
onclose() {
|
||||||
|
console.log("onclose");
|
||||||
|
},
|
||||||
|
onerror(err) {
|
||||||
|
throw err;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
messages.value.pop();
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const searchAction = async (message: string) => {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
try {
|
try {
|
||||||
const { data } = await client.POST("/ai/action/chat", {
|
const { data } = await client.POST("/ai/action/search", {
|
||||||
body: message,
|
body: message,
|
||||||
});
|
});
|
||||||
messages.value.push({
|
messages.value.push({
|
||||||
content: data?.action
|
content: data?.action
|
||||||
? "接收到指令,请您执行。"
|
? "搜索到功能,请您执行。"
|
||||||
: "未找到有效指令,请告诉我更加准确的信息。",
|
: "未搜索到指定功能,请告诉我更加准确的信息。",
|
||||||
type: "action",
|
type: "action",
|
||||||
isUser: false,
|
isUser: false,
|
||||||
username: "知路智能体",
|
username: "知路智能体",
|
||||||
@@ -84,5 +123,5 @@ export const useAiChat = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return { messages, chat, isLoading, cancel, actionChat };
|
return { messages, chat, isLoading, cancel, searchAction, executeAction };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ export enum RoutePath {
|
|||||||
DEPARTMENTVIEW = "departments",
|
DEPARTMENTVIEW = "departments",
|
||||||
POSITIONVIEW = "positions",
|
POSITIONVIEW = "positions",
|
||||||
CREATEUSERVIEW = "create-user",
|
CREATEUSERVIEW = "create-user",
|
||||||
AICHATVIEW = "ai/chat",
|
|
||||||
LLMCONFIGVIEW = "llm/config",
|
LLMCONFIGVIEW = "llm/config",
|
||||||
SCHEDULERVIEW = "scheduler",
|
SCHEDULERVIEW = "scheduler",
|
||||||
UPSERTUSERVIEW = "upsert-user",
|
UPSERTUSERVIEW = "upsert-user",
|
||||||
@@ -41,7 +40,6 @@ export enum RouteName {
|
|||||||
DEPARTMENTVIEW = "departments",
|
DEPARTMENTVIEW = "departments",
|
||||||
POSITIONVIEW = "positions",
|
POSITIONVIEW = "positions",
|
||||||
CREATEUSERVIEW = "create-user",
|
CREATEUSERVIEW = "create-user",
|
||||||
AICHATVIEW = "ai/chat",
|
|
||||||
LLMCONFIGVIEW = "llm/config",
|
LLMCONFIGVIEW = "llm/config",
|
||||||
SCHEDULERVIEW = "scheduler",
|
SCHEDULERVIEW = "scheduler",
|
||||||
UPSERTUSERVIEW = "upsert-user",
|
UPSERTUSERVIEW = "upsert-user",
|
||||||
|
|||||||
@@ -2,14 +2,6 @@ import type { RouteRecordRaw } from "vue-router";
|
|||||||
import { EPermission, RouteName, RoutePath } from "../constants";
|
import { EPermission, RouteName, RoutePath } from "../constants";
|
||||||
|
|
||||||
const aiRoutes: RouteRecordRaw[] = [
|
const aiRoutes: RouteRecordRaw[] = [
|
||||||
{
|
|
||||||
path: RoutePath.AICHATVIEW,
|
|
||||||
name: RouteName.AICHATVIEW,
|
|
||||||
component: () => import("@/views/AiChatView.vue"),
|
|
||||||
meta: {
|
|
||||||
requiresAuth: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: RoutePath.LLMCONFIGVIEW,
|
path: RoutePath.LLMCONFIGVIEW,
|
||||||
name: RouteName.LLMCONFIGVIEW,
|
name: RouteName.LLMCONFIGVIEW,
|
||||||
|
|||||||
@@ -1,398 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div
|
|
||||||
class="flex flex-col px-4 sm:px-8 md:px-16 lg:px-32 xl:px-48 2xl:px-72 box-border pt-14 min-h-screen max-h-screen overflow-auto"
|
|
||||||
ref="chatContainer">
|
|
||||||
<div class="flex flex-col gap-y-5 flex-1 pt-14">
|
|
||||||
<li v-for="chatElement in messages" :key="chatElement.content"
|
|
||||||
:class="['flex items-start gap-2.5', chatElement.isUser ? 'flex-row-reverse' : 'flex-row']">
|
|
||||||
<img class="w-8 h-8 rounded-full" :src="chatElement.isUser ? '/java.svg' : '/trump.jpg'" alt="avatar">
|
|
||||||
<div
|
|
||||||
:class="['flex flex-col leading-1.5 p-4 border-gray-200 rounded-e-xl rounded-es-xl ', chatElement.isUser ? 'bg-blue-100' : 'bg-gray-100']">
|
|
||||||
<div class="flex items-center space-x-2 rtl:space-x-reverse">
|
|
||||||
<span class="text-sm font-semibold text-gray-900 ">{{ chatElement.username }}</span>
|
|
||||||
<LoadingIcon :textColor="'text-gray-900'"
|
|
||||||
v-if="isLoading && !chatElement.isUser && chatElement.content === ''" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="markdown-content markdown-body text-base font-normal py-2.5 text-gray-900 "
|
|
||||||
v-html="renderMarkdown(chatElement.content)">
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
v-if="chatElement.type === 'action' && (chatElement.command === 'CREATE_USER' || chatElement.command === 'CREATE_DEPARTMENT')"
|
|
||||||
type="button" @click="commandActionMap[chatElement.command!]"
|
|
||||||
class="px-3 py-2 text-sm font-medium text-center text-white bg-blue-700 rounded-lg hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">
|
|
||||||
{{
|
|
||||||
commandContentMap[chatElement.command!]
|
|
||||||
}}</button>
|
|
||||||
<InputButton
|
|
||||||
bgColor="bg-red-700 hover:bg-red-800 focus:ring-red-300 text-white focus:ring-4 focus:outline-none"
|
|
||||||
size="sm" :content="commandContentMap[chatElement.command!]" :handleSubmit="handleDeleteUserClick"
|
|
||||||
v-if="chatElement.command === 'DELETE_USER'" />
|
|
||||||
<InputButton
|
|
||||||
bgColor="bg-red-700 hover:bg-red-800 focus:ring-red-300 text-white focus:ring-4 focus:outline-none"
|
|
||||||
size="sm" :content="commandContentMap[chatElement.command!]" :handleSubmit="handleDeleteDepartmentClick"
|
|
||||||
v-if="chatElement.command === 'DELETE_DEPARTMENT'" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form class="sticky bottom-4 pt-14">
|
|
||||||
<button @click.prevent="toggleMode"
|
|
||||||
class="absolute left-1 top-2 inline-flex items-center justify-center p-0.5 mb-2 me-2 overflow-hidden text-sm font-medium rounded-lg group focus:ring-4 focus:outline-none focus:ring-cyan-200"
|
|
||||||
:class="[
|
|
||||||
isCommandMode
|
|
||||||
? 'bg-gradient-to-br from-cyan-500 to-blue-500 text-white'
|
|
||||||
: 'bg-gradient-to-br from-cyan-500 to-blue-500 group-hover:from-cyan-500 group-hover:to-blue-500 text-gray-900'
|
|
||||||
]">
|
|
||||||
<span class="relative px-3 py-2 transition-all ease-in duration-75 rounded-md hover:text-white" :class="[
|
|
||||||
isCommandMode
|
|
||||||
? 'bg-transparent'
|
|
||||||
: 'bg-white group-hover:bg-transparent'
|
|
||||||
]">
|
|
||||||
指令模式
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
<div class="absolute right-1 top-2">
|
|
||||||
<button @click.prevent="() => handleSendClick('请帮我创建用户', true)" class=" inline-flex items-center justify-center p-0.5
|
|
||||||
mb-2 me-2 overflow-hidden font-medium text-gray-900 rounded-lg group bg-gradient-to-br from-purple-500
|
|
||||||
to-pink-500 group-hover:from-purple-500 group-hover:to-pink-500 hover:text-white dark:text-white focus:ring-4
|
|
||||||
focus:outline-none focus:ring-purple-200 cursor-pointer dark:focus:ring-purple-800">
|
|
||||||
<span
|
|
||||||
class="px-3 py-2 text-xs transition-all ease-in duration-75 bg-white dark:bg-gray-900 rounded-md group-hover:bg-transparent group-hover:dark:bg-transparent">
|
|
||||||
帮我创建用户?
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
<button @click.prevent="() => handleSendClick('删除用户', true)" class=" inline-flex items-center justify-center p-0.5
|
|
||||||
mb-2 me-2 overflow-hidden font-medium text-gray-900 rounded-lg group bg-gradient-to-br from-purple-500
|
|
||||||
to-pink-500 group-hover:from-purple-500 group-hover:to-pink-500 hover:text-white dark:text-white focus:ring-4
|
|
||||||
focus:outline-none focus:ring-purple-200 cursor-pointer dark:focus:ring-purple-800">
|
|
||||||
<span
|
|
||||||
class="px-3 py-2 text-xs transition-all ease-in duration-75 bg-white dark:bg-gray-900 rounded-md group-hover:bg-transparent group-hover:dark:bg-transparent">
|
|
||||||
删除用户
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
<button @click.prevent="() => handleSendClick('删除部门', true)" class=" inline-flex items-center justify-center p-0.5
|
|
||||||
mb-2 me-2 overflow-hidden font-medium text-gray-900 rounded-lg group bg-gradient-to-br from-purple-500
|
|
||||||
to-pink-500 group-hover:from-purple-500 group-hover:to-pink-500 hover:text-white dark:text-white focus:ring-4
|
|
||||||
focus:outline-none focus:ring-purple-200 cursor-pointer dark:focus:ring-purple-800">
|
|
||||||
<span
|
|
||||||
class="px-3 py-2 text-xs transition-all ease-in duration-75 bg-white dark:bg-gray-900 rounded-md group-hover:bg-transparent group-hover:dark:bg-transparent">
|
|
||||||
删除部门
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
<button @click.prevent="() => handleSendClick('请帮我创建部门', true)" class="inline-flex items-center justify-center p-0.5 mb-2 me-2 overflow-hidden font-medium text-gray-900 rounded-lg group bg-gradient-to-br from-purple-500 to-pink-500 group-hover:from-purple-500 group-hover:to-pink-500 hover:text-white dark:text-white focus:ring-4 focus:outline-none focus:ring-purple-200
|
|
||||||
cursor-pointer dark:focus:ring-purple-800">
|
|
||||||
<span
|
|
||||||
class="px-3 py-2 text-xs transition-all ease-in duration-75 bg-white dark:bg-gray-900 rounded-md group-hover:bg-transparent group-hover:dark:bg-transparent">
|
|
||||||
帮我创建部门?
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="w-full border border-gray-200 rounded-lg bg-gray-50">
|
|
||||||
<div class="px-4 py-2 bg-white rounded-t-lg">
|
|
||||||
<label for="comment" class="sr-only"></label>
|
|
||||||
<textarea id="comment" rows="3" v-model="inputMessage"
|
|
||||||
class="w-full px-0 text-gray-900 bg-white border-0 focus:ring-0 " placeholder="发送消息" required></textarea>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center justify-between px-3 py-2 border-t border-gray-200">
|
|
||||||
<Button :abortable="true" :isLoading="isLoading" :loadingContent="'中止'" :submitContent="'发送'"
|
|
||||||
:handleClick="() => handleSendClick(inputMessage, isCommandMode)" />
|
|
||||||
<div class="flex ps-0 space-x-1 rtl:space-x-reverse sm:ps-2">
|
|
||||||
<button type="button"
|
|
||||||
class="inline-flex justify-center items-center p-2 text-gray-500 rounded-sm cursor-pointer hover:text-gray-900 hover:bg-gray-100 ">
|
|
||||||
<svg class="w-4 h-4" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none"
|
|
||||||
viewBox="0 0 12 20">
|
|
||||||
<path stroke="currentColor" stroke-linejoin="round" stroke-width="2"
|
|
||||||
d="M1 6v8a5 5 0 1 0 10 0V4.5a3.5 3.5 0 1 0-7 0V13a2 2 0 0 0 4 0V6" />
|
|
||||||
</svg>
|
|
||||||
<span class="sr-only">Attach file</span>
|
|
||||||
</button>
|
|
||||||
<button type="button"
|
|
||||||
class="inline-flex justify-center items-center p-2 text-gray-500 rounded-sm cursor-pointer hover:text-gray-900 hover:bg-gray-100 ">
|
|
||||||
<svg class="w-4 h-4" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor"
|
|
||||||
viewBox="0 0 20 18">
|
|
||||||
<path
|
|
||||||
d="M18 0H2a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2Zm-5.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3Zm4.376 10.481A1 1 0 0 1 16 15H4a1 1 0 0 1-.895-1.447l3.5-7A1 1 0 0 1 7.468 6a.965.965 0 0 1 .9.5l2.775 4.757 1.546-1.887a1 1 0 0 1 1.618.1l2.541 4a1 1 0 0 1 .028 1.011Z" />
|
|
||||||
</svg>
|
|
||||||
<span class="sr-only">Upload image</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<UserUpsertModal :id="'user-upsert-modal'" :onSubmit="handleUpsertUserSubmit" :closeModal="() => {
|
|
||||||
userUpsertModal!.hide();
|
|
||||||
}">
|
|
||||||
</UserUpsertModal>
|
|
||||||
<UserDeleteModal :id="'user-delete-modal'" :closeModal="() => {
|
|
||||||
currentDeleteUsername = undefined
|
|
||||||
userDeleteModal!.hide();
|
|
||||||
}" :onSubmit="handleDeleteUserSubmit" title="确定删除该用户吗" content="删除用户"></UserDeleteModal>
|
|
||||||
<DepartmentUpsertModal :id="'department-upsert-modal'" :onSubmit="handleUpsertDepartmentSubmit" :closeModal="() => {
|
|
||||||
availableDepartments = undefined
|
|
||||||
departmentUpsertModal!.hide();
|
|
||||||
}" :availableDepartments="availableDepartments">
|
|
||||||
</DepartmentUpsertModal>
|
|
||||||
<DepartmentDeleteModal :id="'department-delete-modal'" :closeModal="() => {
|
|
||||||
currentDeleteDepartmentName = undefined
|
|
||||||
departmentDeleteModal!.hide();
|
|
||||||
}" :onSubmit="handleDeleteDepartmentSubmit" title="确定删除该部门吗" content="删除部门"></DepartmentDeleteModal>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import LoadingIcon from "@/components/icons/LoadingIcon.vue";
|
|
||||||
import useAlertStore from "@/composables/store/useAlertStore";
|
|
||||||
import DOMPurify from "dompurify";
|
|
||||||
import { Modal, type ModalInterface, initFlowbite } from "flowbite";
|
|
||||||
import { marked } from "marked";
|
|
||||||
import { nextTick, onMounted, onUnmounted, ref, watch } from "vue";
|
|
||||||
import { z } from "zod";
|
|
||||||
import Button from "../components/Button.vue";
|
|
||||||
import DepartmentUpsertModal from "../components/DepartmentUpsertModal.vue";
|
|
||||||
import UserUpsertModal from "../components/UserUpsertModal.vue";
|
|
||||||
import { useAiChat } from "../composables/ai/useAiChat";
|
|
||||||
import useUserStore from "../composables/store/useUserStore";
|
|
||||||
import { useUserUpsert } from "../composables/user/useUserUpsert";
|
|
||||||
import type { UserUpsertSubmitModel } from "../types/user";
|
|
||||||
import { useDepartmentQuery } from "@/composables/department/useDepartmentQuery";
|
|
||||||
import { useDepartmentUpsert } from "@/composables/department/useDepartmentUpsert";
|
|
||||||
import type { DepartmentUpsertModel } from "@/types/department";
|
|
||||||
import UserDeleteModal from "@/components/PopupModal.vue";
|
|
||||||
import { useAiAction } from "@/composables/ai/useAiAction";
|
|
||||||
import DepartmentDeleteModal from "@/components/PopupModal.vue";
|
|
||||||
import InputButton from "@/components/InputButton.vue";
|
|
||||||
|
|
||||||
const { messages, chat, isLoading, cancel, actionChat } = useAiChat();
|
|
||||||
const { user } = useUserStore();
|
|
||||||
const userUpsertModal = ref<ModalInterface>();
|
|
||||||
const departmentUpsertModal = ref<ModalInterface>();
|
|
||||||
const inputMessage = ref("");
|
|
||||||
const chatContainer = ref<HTMLElement | null>(null);
|
|
||||||
const alertStore = useAlertStore();
|
|
||||||
const isCommandMode = ref(false);
|
|
||||||
const userUpsert = useUserUpsert();
|
|
||||||
const departmentUpsert = useDepartmentUpsert();
|
|
||||||
const userDeleteModal = ref<ModalInterface>();
|
|
||||||
const { deleteUserByUsername, deleteDepartmentByName } = useAiAction();
|
|
||||||
const departmentDeleteModal = ref<ModalInterface>();
|
|
||||||
const currentDeleteUsername = ref<string>();
|
|
||||||
const currentDeleteDepartmentName = ref<string>();
|
|
||||||
|
|
||||||
const { availableDepartments, fetchAvailableDepartments } =
|
|
||||||
useDepartmentQuery();
|
|
||||||
|
|
||||||
const commandActionMap: Record<string, () => void> = {
|
|
||||||
CREATE_USER: () => {
|
|
||||||
userUpsertModal.value?.show();
|
|
||||||
},
|
|
||||||
CREATE_DEPARTMENT: () => {
|
|
||||||
fetchAvailableDepartments();
|
|
||||||
departmentUpsertModal.value?.show();
|
|
||||||
},
|
|
||||||
DELETE_USER: () => {
|
|
||||||
userDeleteModal.value?.show();
|
|
||||||
},
|
|
||||||
DELETE_DEPARTMENT: () => {
|
|
||||||
departmentDeleteModal.value?.show();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const commandContentMap: Record<string, string> = {
|
|
||||||
CREATE_USER: "创建用户",
|
|
||||||
CREATE_DEPARTMENT: "创建部门",
|
|
||||||
DELETE_USER: "删除用户",
|
|
||||||
DELETE_DEPARTMENT: "删除部门",
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleMode = () => {
|
|
||||||
isCommandMode.value = !isCommandMode.value;
|
|
||||||
};
|
|
||||||
|
|
||||||
marked.setOptions({
|
|
||||||
gfm: true,
|
|
||||||
breaks: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const renderMarkdown = (content: string | undefined) => {
|
|
||||||
if (!content) return "";
|
|
||||||
|
|
||||||
const restoredContent = content
|
|
||||||
.replace(/␣/g, " ")
|
|
||||||
.replace(/⇥/g, "\t")
|
|
||||||
.replace(//g, "\n");
|
|
||||||
|
|
||||||
const processedContent = restoredContent
|
|
||||||
.replace(/^(\s*)(`{3,})/gm, "$1$2")
|
|
||||||
.replace(/(\s+)`/g, "$1`");
|
|
||||||
|
|
||||||
const rawHtml = marked(processedContent);
|
|
||||||
return DOMPurify.sanitize(rawHtml as string);
|
|
||||||
};
|
|
||||||
|
|
||||||
// watch(messages, (newVal) => {
|
|
||||||
// console.log('原始消息:', newVal[newVal.length - 1]);
|
|
||||||
// console.log('处理后HTML:', renderMarkdown(newVal[newVal.length - 1]));
|
|
||||||
// }, { deep: true });
|
|
||||||
|
|
||||||
const handleDeleteUserClick = (input: string) => {
|
|
||||||
currentDeleteUsername.value = input;
|
|
||||||
nextTick(() => {
|
|
||||||
userDeleteModal.value?.show();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeleteDepartmentClick = (input: string) => {
|
|
||||||
currentDeleteDepartmentName.value = input;
|
|
||||||
nextTick(() => {
|
|
||||||
departmentDeleteModal.value?.show();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUpsertUserSubmit = async (data: UserUpsertSubmitModel) => {
|
|
||||||
await userUpsert.upsertUser(data);
|
|
||||||
userUpsertModal.value?.hide();
|
|
||||||
alertStore.showAlert({
|
|
||||||
content: "操作成功",
|
|
||||||
level: "success",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUpsertDepartmentSubmit = async (
|
|
||||||
department: DepartmentUpsertModel,
|
|
||||||
) => {
|
|
||||||
await departmentUpsert.upsertDepartment(department);
|
|
||||||
departmentUpsertModal.value?.hide();
|
|
||||||
alertStore.showAlert({
|
|
||||||
content: "操作成功",
|
|
||||||
level: "success",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeleteUserSubmit = async () => {
|
|
||||||
await deleteUserByUsername(currentDeleteUsername.value!);
|
|
||||||
userDeleteModal.value?.hide();
|
|
||||||
alertStore.showAlert({
|
|
||||||
content: "操作成功",
|
|
||||||
level: "success",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeleteDepartmentSubmit = async () => {
|
|
||||||
await deleteDepartmentByName(currentDeleteDepartmentName.value!);
|
|
||||||
departmentDeleteModal.value?.hide();
|
|
||||||
alertStore.showAlert({
|
|
||||||
content: "操作成功",
|
|
||||||
level: "success",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
watch(
|
|
||||||
messages,
|
|
||||||
async () => {
|
|
||||||
await nextTick();
|
|
||||||
scrollToBottom();
|
|
||||||
},
|
|
||||||
{ deep: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
const scrollToBottom = () => {
|
|
||||||
if (chatContainer.value) {
|
|
||||||
chatContainer.value.scrollTop = chatContainer.value.scrollHeight;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const abortChat = () => {
|
|
||||||
cancel();
|
|
||||||
};
|
|
||||||
|
|
||||||
const chatByMode = async (message: string, mode: boolean) => {
|
|
||||||
inputMessage.value = "";
|
|
||||||
messages.value.push({
|
|
||||||
content: message,
|
|
||||||
type: "chat",
|
|
||||||
isUser: true,
|
|
||||||
username: user.username!,
|
|
||||||
});
|
|
||||||
if (mode) {
|
|
||||||
await actionChat(message);
|
|
||||||
} else {
|
|
||||||
await chat(message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSendClick = async (message: string, mode: boolean) => {
|
|
||||||
scrollToBottom();
|
|
||||||
if (isLoading.value) {
|
|
||||||
abortChat();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const validInputMessage = z
|
|
||||||
.string({ message: "消息不能为空" })
|
|
||||||
.min(1, "消息不能为空")
|
|
||||||
.parse(message);
|
|
||||||
await chatByMode(validInputMessage, mode);
|
|
||||||
};
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
cancel();
|
|
||||||
});
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
initFlowbite();
|
|
||||||
const $upsertModalElement: HTMLElement | null =
|
|
||||||
document.querySelector("#user-upsert-modal");
|
|
||||||
userUpsertModal.value = new Modal(
|
|
||||||
$upsertModalElement,
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
id: "user-upsert-modal",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const $userDeleteModalElement: HTMLElement | null =
|
|
||||||
document.querySelector("#user-delete-modal");
|
|
||||||
userDeleteModal.value = new Modal(
|
|
||||||
$userDeleteModalElement,
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
id: "user-delete-modal",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const $departmentDeleteModalElement: HTMLElement | null =
|
|
||||||
document.querySelector("#department-delete-modal");
|
|
||||||
departmentDeleteModal.value = new Modal(
|
|
||||||
$departmentDeleteModalElement,
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
id: "department-delete-modal",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const $departmentUpsertModalElement: HTMLElement | null =
|
|
||||||
document.querySelector("#department-upsert-modal");
|
|
||||||
departmentUpsertModal.value = new Modal(
|
|
||||||
$departmentUpsertModalElement,
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
id: "department-upsert-modal",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="css">
|
|
||||||
@import "github-markdown-css/github-markdown-light.css";
|
|
||||||
|
|
||||||
.markdown-body {
|
|
||||||
background: transparent !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-body pre code {
|
|
||||||
white-space: pre !important;
|
|
||||||
tab-size: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-body p {
|
|
||||||
white-space: pre-wrap;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
Reference in New Issue
Block a user