新增 ChatDto 数据传输对象,更新聊天接口以支持知识库功能,优化聊天服务逻辑,调整前端组件以提升用户体验。

This commit is contained in:
ccmjga
2025-06-28 22:31:20 +08:00
parent 3e1d7e6fee
commit b6ecc929b0
30 changed files with 268 additions and 176 deletions

View File

@@ -1,5 +1,7 @@
package com.zl.mjga.config.ai; package com.zl.mjga.config.ai;
import static dev.langchain4j.store.embedding.filter.MetadataFilterBuilder.metadataKey;
import com.zl.mjga.component.PromptConfiguration; import com.zl.mjga.component.PromptConfiguration;
import com.zl.mjga.service.LlmService; import com.zl.mjga.service.LlmService;
import dev.langchain4j.community.model.zhipu.ZhipuAiStreamingChatModel; import dev.langchain4j.community.model.zhipu.ZhipuAiStreamingChatModel;
@@ -72,6 +74,11 @@ public class ChatModelInitializer {
.embeddingModel(zhipuEmbeddingModel) .embeddingModel(zhipuEmbeddingModel)
.minScore(0.75) .minScore(0.75)
.maxResults(5) .maxResults(5)
.dynamicFilter(
query -> {
String libraryId = (String) query.metadata().chatMemoryId();
return metadataKey("libraryId").isEqualTo(libraryId);
})
.build()) .build())
.build(); .build();
} }

View File

@@ -2,6 +2,7 @@ package com.zl.mjga.controller;
import com.zl.mjga.dto.PageRequestDto; import com.zl.mjga.dto.PageRequestDto;
import com.zl.mjga.dto.PageResponseDto; import com.zl.mjga.dto.PageResponseDto;
import com.zl.mjga.dto.ai.ChatDto;
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.exception.BusinessException; import com.zl.mjga.exception.BusinessException;
@@ -72,9 +73,9 @@ public class AiController {
} }
@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 ChatDto chatDto) {
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.chat(principal.getName(), chatDto);
chat.onPartialResponse( chat.onPartialResponse(
text -> text ->
sink.tryEmitNext( sink.tryEmitNext(

View File

@@ -8,7 +8,6 @@ import com.zl.mjga.repository.LibraryRepository;
import com.zl.mjga.service.RagService; import com.zl.mjga.service.RagService;
import com.zl.mjga.service.UploadService; import com.zl.mjga.service.UploadService;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@@ -34,16 +33,16 @@ public class LibraryController {
@GetMapping("/libraries") @GetMapping("/libraries")
public List<Library> queryLibraries() { public List<Library> queryLibraries() {
return libraryRepository.findAll().stream().sorted( return libraryRepository.findAll().stream()
Comparator.comparing(Library::getId).reversed() .sorted(Comparator.comparing(Library::getId).reversed())
).toList(); .toList();
} }
@GetMapping("/docs") @GetMapping("/docs")
public List<LibraryDoc> queryLibraryDocs(@RequestParam Long libraryId) { public List<LibraryDoc> queryLibraryDocs(@RequestParam Long libraryId) {
return libraryDocRepository.fetchByLibId(libraryId).stream().sorted( return libraryDocRepository.fetchByLibId(libraryId).stream()
Comparator.comparing(LibraryDoc::getId).reversed() .sorted(Comparator.comparing(LibraryDoc::getId).reversed())
).toList(); .toList();
} }
@GetMapping("/segments") @GetMapping("/segments")

View File

@@ -0,0 +1,7 @@
package com.zl.mjga.dto.ai;
import com.zl.mjga.model.urp.ChatMode;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
public record ChatDto(@NotNull ChatMode mode, Long libraryId, @NotEmpty String message) {}

View File

@@ -0,0 +1,6 @@
package com.zl.mjga.model.urp;
public enum ChatMode {
NORMAL,
WITH_LIBRARY
}

View File

@@ -2,6 +2,7 @@ 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.config.ai.SystemToolAssistant;
import com.zl.mjga.dto.ai.ChatDto;
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;
@@ -39,8 +40,20 @@ public class AiChatService {
}; };
} }
public TokenStream chatPrecedenceLlmWith(String sessionIdentifier, String userMessage) { public TokenStream chat(String sessionIdentifier, ChatDto chatDto) {
return switch (chatDto.mode()) {
case NORMAL -> chatWithPrecedenceLlm(sessionIdentifier, chatDto);
case WITH_LIBRARY -> chatWithLibrary(chatDto.libraryId(), chatDto);
};
}
public TokenStream chatWithLibrary(Long libraryId, ChatDto chatDto) {
return zhiPuChatAssistant.chat(String.valueOf(libraryId), chatDto.message());
}
public TokenStream chatWithPrecedenceLlm(String sessionIdentifier, ChatDto chatDto) {
LlmCodeEnum code = getPrecedenceLlmCode(); LlmCodeEnum code = getPrecedenceLlmCode();
String userMessage = chatDto.message();
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);

View File

@@ -71,10 +71,6 @@ public class UploadService {
if (size > 1024 * 1024) { if (size > 1024 * 1024) {
throw new BusinessException("知识库文档大小不能超过1MB"); throw new BusinessException("知识库文档大小不能超过1MB");
} }
String contentType = multipartFile.getContentType();
if (!StringUtils.startsWith(contentType, "text/")) {
throw new BusinessException("非法的上传文件");
}
minioClient.putObject( minioClient.putObject(
PutObjectArgs.builder().bucket(minIoConfig.getDefaultBucket()).object(objectName).stream( PutObjectArgs.builder().bucket(minIoConfig.getDefaultBucket()).object(objectName).stream(
multipartFile.getInputStream(), size, -1) multipartFile.getInputStream(), size, -1)

View File

@@ -894,7 +894,7 @@
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"type": "string" "$ref": "#/components/schemas/ChatDto"
} }
} }
}, },
@@ -1580,7 +1580,8 @@
"DocUpdateDto": { "DocUpdateDto": {
"required": [ "required": [
"enable", "enable",
"id" "id",
"libId"
], ],
"type": "object", "type": "object",
"properties": { "properties": {
@@ -1588,6 +1589,10 @@
"type": "integer", "type": "integer",
"format": "int64" "format": "int64"
}, },
"libId": {
"type": "integer",
"format": "int64"
},
"enable": { "enable": {
"type": "boolean" "type": "boolean"
} }
@@ -1868,6 +1873,29 @@
} }
} }
}, },
"ChatDto": {
"required": [
"message",
"mode"
],
"type": "object",
"properties": {
"mode": {
"type": "string",
"enum": [
"NORMAL",
"WITH_LIBRARY"
]
},
"libraryId": {
"type": "integer",
"format": "int64"
},
"message": {
"type": "string"
}
}
},
"PageRequestDto": { "PageRequestDto": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@@ -783,6 +783,8 @@ export interface components {
DocUpdateDto: { DocUpdateDto: {
/** Format: int64 */ /** Format: int64 */
id: number; id: number;
/** Format: int64 */
libId: number;
enable: boolean; enable: boolean;
}; };
LlmVm: { LlmVm: {
@@ -867,6 +869,13 @@ export interface components {
username: string; username: string;
password: string; password: string;
}; };
ChatDto: {
/** @enum {string} */
mode: "NORMAL" | "WITH_LIBRARY";
/** Format: int64 */
libraryId?: number;
message: string;
};
PageRequestDto: { PageRequestDto: {
/** Format: int64 */ /** Format: int64 */
page?: number; page?: number;
@@ -1888,7 +1897,7 @@ export interface operations {
}; };
requestBody: { requestBody: {
content: { content: {
"application/json": string; "application/json": components["schemas"]["ChatDto"];
}; };
}; };
responses: { responses: {

View File

@@ -5,14 +5,18 @@
<div class="flex flex-col gap-y-5 flex-1 pb-2"> <div class="flex flex-col gap-y-5 flex-1 pb-2">
<li v-for="chatElement in messages" :key="chatElement.content" <li v-for="chatElement in messages" :key="chatElement.content"
:class="['flex items-start gap-2.5', chatElement.isUser ? 'flex-row-reverse' : 'flex-row']"> :class="['flex items-start gap-2.5', chatElement.isUser ? 'flex-row-reverse' : 'flex-row']">
<Avatar :src="chatElement.isUser ? user.avatar : '/trump.jpg'" size="sm" <Avatar :src="chatElement.isUser ? user.avatar : undefined" size="sm"
:alt="chatElement.isUser ? '用户头像' : 'AI头像'" /> :alt="chatElement.isUser ? '用户头像' : 'AI头像'" />
<div <div
:class="['flex flex-col leading-1.5 p-4 border-gray-200 rounded-e-xl rounded-es-xl max-w-[calc(100%-40px)]', chatElement.isUser ? 'bg-blue-100' : 'bg-gray-100']"> :class="['flex flex-col leading-1.5 p-4 border-gray-200 max-w-[calc(100%-40px)]', chatElement.isUser ? 'bg-blue-100 rounded-tl-xl rounded-bl-xl rounded-br-xl' : 'bg-gray-100 rounded-e-xl rounded-es-xl']">
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<span class="text-sm font-semibold text-gray-900 ">{{ chatElement.username }}</span> <span class="text-sm font-semibold text-gray-900 ">{{ chatElement.username }}</span>
<LoadingIcon :textColor="'text-gray-900'" <LoadingIcon :textColor="'text-gray-900'"
v-if="isLoading && !chatElement.isUser && chatElement.content === ''" /> v-if="isLoading && !chatElement.isUser && chatElement.content === ''" />
<span v-if="!chatElement.isUser && chatElement.withLibrary"
class="text-xs bg-blue-100 text-blue-800 px-2 py-0.5 rounded-full">
{{ chatElement.libraryName }}
</span>
</div> </div>
<div> <div>
<div class="markdown-content markdown-body text-base font-normal py-2.5 text-gray-900 break-words" <div class="markdown-content markdown-body text-base font-normal py-2.5 text-gray-900 break-words"
@@ -34,14 +38,24 @@
</div> </div>
<form class="sticky"> <form class="sticky">
<button @click.prevent="clearConversation" <div class="flex items-center justify-between gap-2 mb-2">
class="relative inline-flex items-center justify-center p-0.5 mb-2 me-2 <button @click.prevent="clearConversation"
overflow-hidden text-sm font-medium text-gray-900 rounded-lg group bg-gradient-to-br from-purple-600 to-blue-500 group-hover:from-purple-600 group-hover:to-blue-500 hover:text-white focus:ring-4 focus:outline-none focus:ring-blue-300 "> class="relative inline-flex items-center justify-center p-0.5
<span overflow-hidden text-sm font-medium text-gray-900 rounded-lg group bg-gradient-to-br from-purple-600 to-blue-500 group-hover:from-purple-600 group-hover:to-blue-500 hover:text-white focus:ring-4 focus:outline-none focus:ring-blue-300 ">
class="relative px-3 py-2 text-xs font-medium transition-all ease-in duration-75 bg-white rounded-md group-hover:bg-transparent"> <span
开启新对话 class="relative px-3 py-2 text-xs font-medium transition-all ease-in duration-75 bg-white rounded-md group-hover:bg-transparent">
</span> 开启新对话
</button> </span>
</button>
<select v-if="commandMode === 'chat'" v-model="selectedLibraryId"
class="bg-white border border-gray-300 text-gray-900 text-xs rounded-lg py-2 px-2 flex-1 max-w-48">
<option :value="undefined">不使用知识库</option>
<option v-for="library in libraries" :key="library.id" :value="library.id">
{{ library.name }}
</option>
</select>
</div>
<div class="w-full border border-gray-200 rounded-lg bg-gray-50"> <div class="w-full border border-gray-200 rounded-lg bg-gray-50">
<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>
@@ -51,7 +65,7 @@
" required></textarea> " 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">
<select id="countries" v-model="commandMode" <select id="commandMode" 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="'execute'">指令模式</option> <option selected :value="'execute'">指令模式</option>
<option :value="'search'">搜索模式</option> <option :value="'search'">搜索模式</option>
@@ -62,7 +76,6 @@
{{ isLoading ? '中止' : '发送' }} {{ isLoading ? '中止' : '发送' }}
</TableButton> </TableButton>
</div> </div>
</div> </div>
</form> </form>
</div> </div>
@@ -144,6 +157,9 @@ import PositionDeleteModal from "@/components/modals/ConfirmationDialog.vue";
import PositionFormDialog from "@/components/modals/PositionFormDialog.vue"; import PositionFormDialog from "@/components/modals/PositionFormDialog.vue";
import RoleDeleteModal from "@/components/modals/ConfirmationDialog.vue"; import RoleDeleteModal from "@/components/modals/ConfirmationDialog.vue";
import RoleFormDialog from "@/components/modals/RoleFormDialog.vue"; import RoleFormDialog from "@/components/modals/RoleFormDialog.vue";
import { useKnowledgeQuery } from "@/composables/knowledge/useKnowledgeQuery";
import { UserFormDialog } from "../modals";
import { computed } from "vue";
const { const {
messages, messages,
@@ -190,6 +206,15 @@ const actionExcStore = useActionExcStore();
const { availableDepartments, fetchAvailableDepartments } = const { availableDepartments, fetchAvailableDepartments } =
useDepartmentQuery(); useDepartmentQuery();
// 知识库相关
const { libraries, fetchLibraries } = useKnowledgeQuery();
const selectedLibraryId = ref<number | null | undefined>(undefined);
const selectedLibraryName = computed(() => {
return libraries.value.find(
(library) => library.id === selectedLibraryId.value,
)?.name;
});
const commandPlaceholderMap: Record<string, string> = { const commandPlaceholderMap: Record<string, string> = {
chat: "随便聊聊", chat: "随便聊聊",
search: "输入「创建用户、删除部门、创建岗位、创建角色、创建权限」试试看", search: "输入「创建用户、删除部门、创建岗位、创建角色、创建权限」试试看",
@@ -235,31 +260,6 @@ const renderMarkdown = (content: string | undefined) => {
// console.log('处理后HTML:', renderMarkdown(newVal[newVal.length - 1])); // console.log('处理后HTML:', renderMarkdown(newVal[newVal.length - 1]));
// }, { deep: true }); // }, { deep: true });
const handleDeleteUserClick = (input: string) => {
currentDeleteUsername.value = input;
userDeleteModal.value?.show();
};
const handleDeleteDepartmentClick = (input: string) => {
currentDeleteDepartmentName.value = input;
departmentDeleteModal.value?.show();
};
const handleDeletePositionClick = (input: string) => {
currentDeletePositionName.value = input;
positionDeleteModal.value?.show();
};
const handleDeleteRoleClick = (input: string) => {
currentDeleteRoleName.value = input;
roleDeleteModal.value?.show();
};
const handleDeletePermissionClick = (input: string) => {
currentDeletePermissionName.value = input;
permissionDeleteModal.value?.show();
};
const handleUpsertUserSubmit = async (data: UserUpsertSubmitModel) => { const handleUpsertUserSubmit = async (data: UserUpsertSubmitModel) => {
await userUpsert.upsertUser(data); await userUpsert.upsertUser(data);
userUpsertModal.value?.hide(); userUpsertModal.value?.hide();
@@ -402,7 +402,12 @@ const chatByMode = async (
await executeAction(message); await executeAction(message);
actionExcStore.notify(true); actionExcStore.notify(true);
} else { } else {
await chat(message); // 聊天模式,判断是否使用知识库
if (selectedLibraryId.value !== undefined) {
await chat(message, selectedLibraryId.value);
} else {
await chat(message);
}
} }
}; };
@@ -427,6 +432,9 @@ onUnmounted(() => {
onMounted(async () => { onMounted(async () => {
initFlowbite(); initFlowbite();
// 加载知识库列表
await fetchLibraries();
const $upsertModalElement: HTMLElement | null = const $upsertModalElement: HTMLElement | null =
document.querySelector("#user-upsert-modal"); document.querySelector("#user-upsert-modal");
if ($upsertModalElement) { if ($upsertModalElement) {

View File

@@ -36,9 +36,9 @@
<script setup lang="ts"> <script setup lang="ts">
defineProps({ defineProps({
titleClass: { titleClass: {
type: String, type: String,
default: '' default: "",
} },
}); });
</script> </script>

View File

@@ -1,4 +1,4 @@
import CardBase from "./CardBase.vue"; import CardBase from "./CardBase.vue";
import PromotionBanner from "./PromotionBanner.vue"; import PromotionBanner from "./PromotionBanner.vue";
export { CardBase, PromotionBanner }; export { CardBase, PromotionBanner };

View File

@@ -10,7 +10,7 @@
</template> </template>
<template #footer-left> <template #footer-left>
<span class="text-xs text-gray-500"> <span class="text-xs text-gray-500">
上传时间: {{ formatDateString(doc.createTime) }} {{ formatDateString(doc.createTime) }}
</span> </span>
</template> </template>
<template #footer-actions> <template #footer-actions>
@@ -20,12 +20,12 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import CardBase from '@/components/common/CardBase.vue'; import CardBase from "@/components/common/CardBase.vue";
import { KnowledgeStatusBadge } from '@/components/common/knowledge'; import { KnowledgeStatusBadge } from "@/components/common/knowledge";
import type { LibraryDoc } from "@/types/KnowledgeTypes"; import type { LibraryDoc } from "@/types/KnowledgeTypes";
import { formatDateString } from '@/utils/dateUtil'; import { formatDateString } from "@/utils/dateUtil";
const props = defineProps<{ const props = defineProps<{
doc: LibraryDoc; doc: LibraryDoc;
}>(); }>();
</script> </script>

View File

@@ -21,11 +21,11 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import CardBase from '@/components/common/CardBase.vue'; import CardBase from "@/components/common/CardBase.vue";
import type { Library } from "@/types/KnowledgeTypes"; import type { Library } from "@/types/KnowledgeTypes";
import { formatDateString } from '@/utils/dateUtil'; import { formatDateString } from "@/utils/dateUtil";
const props = defineProps<{ const props = defineProps<{
library: Library; library: Library;
}>(); }>();
</script> </script>

View File

@@ -10,28 +10,28 @@
import { DocStatus } from "@/types/KnowledgeTypes"; import { DocStatus } from "@/types/KnowledgeTypes";
const props = defineProps<{ const props = defineProps<{
status?: string; status?: string;
enabled?: boolean; enabled?: boolean;
type: 'status' | 'enabled'; type: "status" | "enabled";
}>(); }>();
const getStatusClass = () => { const getStatusClass = () => {
if (props.type === 'status') { if (props.type === "status") {
return props.status === DocStatus.SUCCESS return props.status === DocStatus.SUCCESS
? 'bg-green-100 text-green-800' ? "bg-green-100 text-green-800"
: 'bg-yellow-100 text-yellow-800'; : "bg-yellow-100 text-yellow-800";
} }
return props.enabled return props.enabled
? 'bg-blue-100 text-blue-800' ? "bg-blue-100 text-blue-800"
: 'bg-gray-100 text-gray-800'; : "bg-gray-100 text-gray-800";
}; };
const getStatusText = () => { const getStatusText = () => {
if (props.type === 'status') { if (props.type === "status") {
return props.status === DocStatus.SUCCESS ? '解析完成' : '解析中'; return props.status === DocStatus.SUCCESS ? "解析完成" : "解析中";
} }
return props.enabled ? '已启用' : '已禁用'; return props.enabled ? "已启用" : "已禁用";
}; };
</script> </script>

View File

@@ -27,11 +27,11 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import CardBase from '@/components/common/CardBase.vue'; import CardBase from "@/components/common/CardBase.vue";
import type { LibraryDocSegment } from "@/types/KnowledgeTypes"; import type { LibraryDocSegment } from "@/types/KnowledgeTypes";
const props = defineProps<{ const props = defineProps<{
segment: LibraryDocSegment; segment: LibraryDocSegment;
index: number; index: number;
}>(); }>();
</script> </script>

View File

@@ -8,4 +8,4 @@ export {
KnowledgeDocCard, KnowledgeDocCard,
KnowledgeLibraryCard, KnowledgeLibraryCard,
SegmentCard, SegmentCard,
}; };

View File

@@ -1,7 +1,4 @@
import FormInput from './FormInput.vue'; import FormInput from "./FormInput.vue";
import FormSelect from './FormSelect.vue'; import FormSelect from "./FormSelect.vue";
export { export { FormInput, FormSelect };
FormInput,
FormSelect
};

View File

@@ -29,7 +29,18 @@
import { Modal, initFlowbite } from "flowbite"; import { Modal, initFlowbite } from "flowbite";
import { computed, onMounted } from "vue"; import { computed, onMounted } from "vue";
export type ModalSize = "xs" | "sm" | "md" | "lg" | "xl" | "2xl" | "3xl" | "4xl" | "5xl" | "6xl" | "7xl"; export type ModalSize =
| "xs"
| "sm"
| "md"
| "lg"
| "xl"
| "2xl"
| "3xl"
| "4xl"
| "5xl"
| "6xl"
| "7xl";
const props = defineProps<{ const props = defineProps<{
/** 对话框标题 */ /** 对话框标题 */

View File

@@ -20,4 +20,4 @@ export {
RoleFormDialog, RoleFormDialog,
SchedulerFormDialog, SchedulerFormDialog,
UserFormDialog, UserFormDialog,
}; };

View File

@@ -12,4 +12,4 @@ export {
TableFilterForm, TableFilterForm,
TableFormLayout, TableFormLayout,
TablePagination, TablePagination,
}; };

View File

@@ -25,7 +25,7 @@ const sizeClass = computed(() => {
sm: "w-8 h-8", sm: "w-8 h-8",
md: "w-10 h-10", md: "w-10 h-10",
lg: "w-12 h-12", lg: "w-12 h-12",
xl: "w-16 h-16" xl: "w-16 h-16",
}; };
return sizes[props.size || "md"]; return sizes[props.size || "md"];
}); });

View File

@@ -28,95 +28,95 @@ import { StopIcon } from "@/components/icons";
import { computed } from "vue"; import { computed } from "vue";
export type ButtonVariant = export type ButtonVariant =
| "primary" | "primary"
| "secondary" | "secondary"
| "success" | "success"
| "danger" | "danger"
| "warning" | "warning"
| "info"; | "info";
export type ButtonSize = "xs" | "sm" | "md" | "lg"; export type ButtonSize = "xs" | "sm" | "md" | "lg";
export type ButtonType = "button" | "submit" | "reset"; export type ButtonType = "button" | "submit" | "reset";
const props = defineProps<{ const props = defineProps<{
/** 按钮变体类型 */ /** 按钮变体类型 */
variant?: ButtonVariant; variant?: ButtonVariant;
/** 按钮尺寸 */ /** 按钮尺寸 */
size?: ButtonSize; size?: ButtonSize;
/** 是否禁用 */ /** 是否禁用 */
disabled?: boolean; disabled?: boolean;
/** 自定义CSS类名 */ /** 自定义CSS类名 */
className?: string; className?: string;
/** 是否为移动端尺寸 */ /** 是否为移动端尺寸 */
isMobile?: boolean; isMobile?: boolean;
/** 是否处于加载状态 */ /** 是否处于加载状态 */
isLoading?: boolean; isLoading?: boolean;
/** 是否可中止 */ /** 是否可中止 */
abortable?: boolean; abortable?: boolean;
/** 按钮类型 */ /** 按钮类型 */
type?: ButtonType; type?: ButtonType;
/** 是否占满宽度 */ /** 是否占满宽度 */
fullWidth?: boolean; fullWidth?: boolean;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
click: [event: MouseEvent]; click: [event: MouseEvent];
}>(); }>();
/** 按钮颜色样式映射 */ /** 按钮颜色样式映射 */
const colorClasses = computed(() => { const colorClasses = computed(() => {
const variants: Record<ButtonVariant, string> = { const variants: Record<ButtonVariant, string> = {
primary: "text-white bg-blue-700 hover:bg-blue-800 focus:ring-blue-300", primary: "text-white bg-blue-700 hover:bg-blue-800 focus:ring-blue-300",
secondary: secondary:
"text-gray-900 bg-white border border-gray-300 hover:bg-gray-100 focus:ring-gray-100", "text-gray-900 bg-white border border-gray-300 hover:bg-gray-100 focus:ring-gray-100",
success: "text-white bg-green-700 hover:bg-green-800 focus:ring-green-300", success: "text-white bg-green-700 hover:bg-green-800 focus:ring-green-300",
danger: "text-white bg-red-700 hover:bg-red-800 focus:ring-red-300", danger: "text-white bg-red-700 hover:bg-red-800 focus:ring-red-300",
warning: warning:
"text-gray-900 bg-yellow-400 hover:bg-yellow-500 focus:ring-yellow-300", "text-gray-900 bg-yellow-400 hover:bg-yellow-500 focus:ring-yellow-300",
info: "text-white bg-cyan-700 hover:bg-cyan-800 focus:ring-cyan-300", info: "text-white bg-cyan-700 hover:bg-cyan-800 focus:ring-cyan-300",
}; };
return variants[props.variant || "primary"]; return variants[props.variant || "primary"];
}); });
/** 按钮尺寸样式映射 */ /** 按钮尺寸样式映射 */
const sizeClasses = computed(() => { const sizeClasses = computed(() => {
// 移动端尺寸 // 移动端尺寸
if (props.isMobile) { if (props.isMobile) {
const sizes: Record<ButtonSize, string> = { const sizes: Record<ButtonSize, string> = {
xs: "text-xs px-2 py-1", xs: "text-xs px-2 py-1",
sm: "text-xs px-3 py-1.5", sm: "text-xs px-3 py-1.5",
md: "text-sm px-3 py-2", md: "text-sm px-3 py-2",
lg: "text-sm px-4 py-2.5", lg: "text-sm px-4 py-2.5",
}; };
return sizes[props.size || "sm"]; return sizes[props.size || "sm"];
} }
// PC端尺寸 // PC端尺寸
const sizes: Record<ButtonSize, string> = { const sizes: Record<ButtonSize, string> = {
xs: "text-xs px-3 py-1.5", xs: "text-xs px-3 py-1.5",
sm: "text-sm px-3 py-2", sm: "text-sm px-3 py-2",
md: "text-sm px-4 py-2.5", md: "text-sm px-4 py-2.5",
lg: "text-base px-5 py-3", lg: "text-base px-5 py-3",
}; };
return sizes[props.size || "md"]; return sizes[props.size || "md"];
}); });
/** 图标尺寸样式映射 */ /** 图标尺寸样式映射 */
const iconSizeClasses = computed(() => { const iconSizeClasses = computed(() => {
const sizes: Record<ButtonSize, string> = { const sizes: Record<ButtonSize, string> = {
xs: "w-3.5 h-3.5", xs: "w-3.5 h-3.5",
sm: "w-4 h-4", sm: "w-4 h-4",
md: "w-4.5 h-4.5", md: "w-4.5 h-4.5",
lg: "w-5 h-5", lg: "w-5 h-5",
}; };
return sizes[props.size || "md"]; return sizes[props.size || "md"];
}); });
/** 处理点击事件 */ /** 处理点击事件 */
const handleClick = (event: MouseEvent) => { const handleClick = (event: MouseEvent) => {
if (!props.disabled && !(props.isLoading && !props.abortable)) { if (!props.disabled && !(props.isLoading && !props.abortable)) {
emit("click", event); emit("click", event);
} }
}; };
</script> </script>

View File

@@ -4,4 +4,4 @@ import Button from "./Button.vue";
import InputButton from "./InputButton.vue"; import InputButton from "./InputButton.vue";
import SortIcon from "./SortIcon.vue"; import SortIcon from "./SortIcon.vue";
export { Alert, Avatar, Button, InputButton, SortIcon }; export { Alert, Avatar, Button, InputButton, SortIcon };

View File

@@ -11,13 +11,19 @@ export const useAiChat = () => {
isUser: boolean; isUser: boolean;
username: string; username: string;
command?: string; command?: string;
withLibrary?: boolean;
libraryName?: string;
}[] }[]
>([]); >([]);
const isLoading = ref(false); const isLoading = ref(false);
let currentController: AbortController | null = null; let currentController: AbortController | null = null;
const chat = async (message: string) => { const chat = async (
message: string,
libraryId?: number | null,
libraryName?: string,
) => {
isLoading.value = true; isLoading.value = true;
const authStore = useAuthStore(); const authStore = useAuthStore();
const ctrl = new AbortController(); const ctrl = new AbortController();
@@ -27,6 +33,8 @@ export const useAiChat = () => {
type: "chat", type: "chat",
isUser: false, isUser: false,
username: "知路智能体", username: "知路智能体",
withLibrary: libraryId !== undefined,
libraryName: libraryName,
}); });
try { try {
const baseUrl = `${import.meta.env.VITE_BASE_URL}`; const baseUrl = `${import.meta.env.VITE_BASE_URL}`;
@@ -36,7 +44,11 @@ export const useAiChat = () => {
Authorization: authStore.get(), Authorization: authStore.get(),
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: message, body: JSON.stringify({
mode: libraryId !== undefined ? "WITH_LIBRARY" : "NORMAL",
libraryId: libraryId,
message: message,
}),
signal: ctrl.signal, signal: ctrl.signal,
onmessage(ev) { onmessage(ev) {
messages.value[messages.value.length - 1].content += ev.data; messages.value[messages.value.length - 1].content += ev.data;

View File

@@ -3,4 +3,4 @@ import { usePagination } from "./usePagination";
import { useSorting } from "./useSorting"; import { useSorting } from "./useSorting";
import { useStyleSystem } from "./useStyleSystem"; import { useStyleSystem } from "./useStyleSystem";
export { useErrorHandling, usePagination, useSorting, useStyleSystem }; export { useErrorHandling, usePagination, useSorting, useStyleSystem };

View File

@@ -26,9 +26,9 @@ export interface DocQueryParams {
} }
export interface SegmentQueryParams { export interface SegmentQueryParams {
libraryDocId?: number; libraryDocId?: number;
docId?: number; docId?: number;
} }
export enum DocStatus { export enum DocStatus {
SUCCESS = "SUCCESS", SUCCESS = "SUCCESS",

View File

@@ -1,4 +1,4 @@
import { getUserAvatarUrl } from "./avatarUtil"; import { getUserAvatarUrl } from "./avatarUtil";
import { dayjs, formatDate, formatDateString } from "./dateUtil"; import { dayjs, formatDate, formatDateString } from "./dateUtil";
export { getUserAvatarUrl, dayjs, formatDate, formatDateString }; export { getUserAvatarUrl, dayjs, formatDate, formatDateString };

View File

@@ -68,7 +68,7 @@ import { useKnowledgeQuery } from "@/composables/knowledge/useKnowledgeQuery";
import { useKnowledgeUpsert } from "@/composables/knowledge/useKnowledgeUpsert"; import { useKnowledgeUpsert } from "@/composables/knowledge/useKnowledgeUpsert";
import useAlertStore from "@/composables/store/useAlertStore"; import useAlertStore from "@/composables/store/useAlertStore";
import { Routes } from "@/router/constants"; import { Routes } from "@/router/constants";
import { formatDateString } from '@/utils/dateUtil'; import { formatDateString } from "@/utils/dateUtil";
import type { Library, LibraryDoc } from "@/types/KnowledgeTypes"; import type { Library, LibraryDoc } from "@/types/KnowledgeTypes";
import { DocStatus } from "@/types/KnowledgeTypes"; import { DocStatus } from "@/types/KnowledgeTypes";

View File

@@ -28,9 +28,6 @@
<!-- 空状态 --> <!-- 空状态 -->
<div v-else class="flex flex-col items-center justify-center py-10"> <div v-else class="flex flex-col items-center justify-center py-10">
<div class="text-gray-500 text-lg mb-4">暂无分段内容</div> <div class="text-gray-500 text-lg mb-4">暂无分段内容</div>
<Button variant="secondary" @click="navigateBack">
返回文档列表
</Button>
</div> </div>
</div> </div>
</template> </template>
@@ -55,7 +52,8 @@ const libraryId = Number.parseInt(route.params.libraryId as string, 10);
const docId = Number.parseInt(route.params.docId as string, 10); const docId = Number.parseInt(route.params.docId as string, 10);
// 获取文档信息和分段列表 // 获取文档信息和分段列表
const { docs, segments, fetchLibraryDocs, fetchDocSegments } = useKnowledgeQuery(); const { docs, segments, fetchLibraryDocs, fetchDocSegments } =
useKnowledgeQuery();
const currentDoc = ref<LibraryDoc | undefined>(); const currentDoc = ref<LibraryDoc | undefined>();
// 导航回文档列表 // 导航回文档列表