add llm config

This commit is contained in:
Chuck1sn
2025-05-24 13:29:09 +08:00
parent 43728ee733
commit c2d5fddcc0
22 changed files with 675 additions and 38 deletions

View File

@@ -2,6 +2,7 @@ package com.zl.mjga.controller;
import com.zl.mjga.dto.PageRequestDto;
import com.zl.mjga.dto.PageResponseDto;
import com.zl.mjga.dto.ai.LlmQueryDto;
import com.zl.mjga.dto.ai.LlmVm;
import com.zl.mjga.service.AiChatService;
import com.zl.mjga.service.LlmService;
@@ -52,7 +53,8 @@ public class AiController {
@PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).READ_LLM_CONFIG_PERMISSION)")
@GetMapping("/llm/page-query")
@ResponseStatus(HttpStatus.OK)
public PageResponseDto<List<LlmVm>> pageQueryLlm(@ModelAttribute PageRequestDto pageRequestDto) {
return llmService.pageQueryLlm(pageRequestDto);
public PageResponseDto<List<LlmVm>> pageQueryLlm(
@ModelAttribute PageRequestDto pageRequestDto, @ModelAttribute LlmQueryDto llmQueryDto) {
return llmService.pageQueryLlm(pageRequestDto, llmQueryDto);
}
}

View File

@@ -0,0 +1,3 @@
package com.zl.mjga.dto.ai;
public record LlmQueryDto(String name) {}

View File

@@ -1,8 +1,11 @@
package com.zl.mjga.repository;
import static org.jooq.generated.mjga.Tables.AI_LLM_CONFIG;
import static org.jooq.impl.DSL.noCondition;
import com.zl.mjga.dto.PageRequestDto;
import com.zl.mjga.dto.ai.LlmQueryDto;
import org.apache.commons.lang3.StringUtils;
import org.jooq.Configuration;
import org.jooq.Record;
import org.jooq.Result;
@@ -19,11 +22,15 @@ public class LlmRepository extends AiLlmConfigDao {
super(configuration);
}
public Result<Record> pageFetchBy(PageRequestDto pageRequestDto) {
public Result<Record> pageFetchBy(PageRequestDto pageRequestDto, LlmQueryDto llmQueryDto) {
return ctx()
.select(
AI_LLM_CONFIG.asterisk(), DSL.count().over().as("total_llm").convertFrom(Long::valueOf))
.from(AI_LLM_CONFIG)
.where(
StringUtils.isNotEmpty(llmQueryDto.name())
? AI_LLM_CONFIG.NAME.eq(llmQueryDto.name())
: noCondition())
.orderBy(pageRequestDto.getSortFields())
.limit(pageRequestDto.getSize())
.offset(pageRequestDto.getOffset())

View File

@@ -1,7 +1,9 @@
package com.zl.mjga.service;
import com.zl.mjga.config.ai.AiChatAssistant;
import com.zl.mjga.exception.BusinessException;
import dev.langchain4j.service.TokenStream;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.jooq.generated.mjga.enums.LlmCodeEnum;
@@ -26,8 +28,9 @@ public class AiChatService {
}
public TokenStream chatPrecedenceLlmWith(String sessionIdentifier, String userMessage) {
AiLlmConfig precedenceLlmBy = llmService.getPrecedenceLlmBy(true);
LlmCodeEnum code = precedenceLlmBy.getCode();
Optional<AiLlmConfig> precedenceLlmBy = llmService.getPrecedenceLlmBy(true);
AiLlmConfig aiLlmConfig = precedenceLlmBy.orElseThrow(() -> new BusinessException("没有开启的大模型"));
LlmCodeEnum code = aiLlmConfig.getCode();
return switch (code) {
case ZHI_PU -> zhiPuChatAssistant.chat(sessionIdentifier, userMessage);
case DEEP_SEEK -> deepSeekChatAssistant.chat(sessionIdentifier, userMessage);

View File

@@ -2,10 +2,12 @@ package com.zl.mjga.service;
import com.zl.mjga.dto.PageRequestDto;
import com.zl.mjga.dto.PageResponseDto;
import com.zl.mjga.dto.ai.LlmQueryDto;
import com.zl.mjga.dto.ai.LlmVm;
import com.zl.mjga.repository.LlmRepository;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.jooq.Record;
@@ -26,16 +28,14 @@ public class LlmService {
return llmRepository.fetchOneByCode(llmCodeEnum);
}
public AiLlmConfig getPrecedenceLlmBy(Boolean enable) {
public Optional<AiLlmConfig> getPrecedenceLlmBy(Boolean enable) {
List<AiLlmConfig> aiLlmConfigs = llmRepository.fetchByEnable(enable);
//noinspection OptionalGetWithoutIsPresent
return aiLlmConfigs.stream()
.max((o1, o2) -> o2.getPriority().compareTo(o1.getPriority()))
.get();
return aiLlmConfigs.stream().max((o1, o2) -> o2.getPriority().compareTo(o1.getPriority()));
}
public PageResponseDto<List<LlmVm>> pageQueryLlm(PageRequestDto pageRequestDto) {
Result<Record> records = llmRepository.pageFetchBy(pageRequestDto);
public PageResponseDto<List<LlmVm>> pageQueryLlm(
PageRequestDto pageRequestDto, LlmQueryDto llmQueryDto) {
Result<Record> records = llmRepository.pageFetchBy(pageRequestDto, llmQueryDto);
if (records.isEmpty()) {
return PageResponseDto.empty();
}

View File

@@ -57,8 +57,6 @@
"vue-tsc": "^2.2.8"
},
"msw": {
"workerDirectory": [
"public"
]
"workerDirectory": ["public"]
}
}

View File

@@ -8,4 +8,26 @@ export default [
});
return response;
}),
http.get("/ai/llm/page-query", () => {
const generateLlm = () => ({
id: faker.number.int({ min: 1, max: 100 }),
name: faker.lorem.word(),
modelName: faker.lorem.word(),
apiKey: faker.string.uuid(),
url: faker.internet.url(),
enable: faker.datatype.boolean(),
priority: faker.number.int({ min: 1, max: 10 }),
});
const mockData = {
data: faker.helpers.multiple(generateLlm, { count: 10 }),
total: 30,
};
return HttpResponse.json(mockData);
}),
http.put("/ai/llm", () => {
return HttpResponse.json({
message: "Llm updated successfully",
});
}),
];

View File

@@ -44,6 +44,29 @@
}
}
},
"/ai/llm": {
"put": {
"tags": [
"ai-controller"
],
"operationId": "updateLlm",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LlmVm"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/scheduler/trigger/resume": {
"post": {
"tags": [
@@ -1008,6 +1031,44 @@
}
}
}
},
"/ai/llm/page-query": {
"get": {
"tags": [
"ai-controller"
],
"operationId": "pageQueryLlm",
"parameters": [
{
"name": "pageRequestDto",
"in": "query",
"required": true,
"schema": {
"$ref": "#/components/schemas/PageRequestDto"
}
},
{
"name": "llmQueryDto",
"in": "query",
"required": true,
"schema": {
"$ref": "#/components/schemas/LlmQueryDto"
}
}
],
"responses": {
"200": {
"description": "OK",
"content": {
"*/*": {
"schema": {
"$ref": "#/components/schemas/PageResponseDtoListLlmVm"
}
}
}
}
}
}
}
},
"components": {
@@ -1027,6 +1088,43 @@
}
}
},
"LlmVm": {
"required": [
"apiKey",
"enable",
"id",
"modelName",
"name",
"priority",
"url"
],
"type": "object",
"properties": {
"id": {
"type": "integer",
"format": "int64"
},
"name": {
"type": "string"
},
"modelName": {
"type": "string"
},
"apiKey": {
"type": "string"
},
"url": {
"type": "string"
},
"enable": {
"type": "boolean"
},
"priority": {
"type": "integer",
"format": "int32"
}
}
},
"JobKeyDto": {
"required": [
"group",
@@ -1684,6 +1782,29 @@
}
}
}
},
"LlmQueryDto": {
"type": "object",
"properties": {
"name": {
"type": "string"
}
}
},
"PageResponseDtoListLlmVm": {
"type": "object",
"properties": {
"total": {
"type": "integer",
"format": "int64"
},
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/LlmVm"
}
}
}
}
}
}

View File

@@ -20,6 +20,22 @@ export interface paths {
patch?: never;
trace?: never;
};
"/ai/llm": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put: operations["updateLlm"];
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/scheduler/trigger/resume": {
parameters: {
query?: never;
@@ -484,6 +500,22 @@ export interface paths {
patch?: never;
trace?: never;
};
"/ai/llm/page-query": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get: operations["pageQueryLlm"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
}
export type webhooks = Record<string, never>;
export interface components {
@@ -492,6 +524,17 @@ export interface components {
name: string;
group: string;
};
LlmVm: {
/** Format: int64 */
id: number;
name: string;
modelName: string;
apiKey: string;
url: string;
enable: boolean;
/** Format: int32 */
priority: number;
};
JobKeyDto: {
name: string;
group: string;
@@ -714,6 +757,14 @@ export interface components {
total?: number;
data?: components["schemas"]["DepartmentRespDto"][];
};
LlmQueryDto: {
name?: string;
};
PageResponseDtoListLlmVm: {
/** Format: int64 */
total?: number;
data?: components["schemas"]["LlmVm"][];
};
};
responses: never;
parameters: never;
@@ -747,6 +798,28 @@ export interface operations {
};
};
};
updateLlm: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["LlmVm"];
};
};
responses: {
/** @description OK */
200: {
headers: {
[name: string]: unknown;
};
content?: never;
};
};
};
resumeTrigger: {
parameters: {
query?: never;
@@ -1553,4 +1626,27 @@ export interface operations {
};
};
};
pageQueryLlm: {
parameters: {
query: {
pageRequestDto: components["schemas"]["PageRequestDto"];
llmQueryDto: components["schemas"]["LlmQueryDto"];
};
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description OK */
200: {
headers: {
[name: string]: unknown;
};
content: {
"*/*": components["schemas"]["PageResponseDtoListLlmVm"];
};
};
};
};
}

View File

@@ -0,0 +1,142 @@
<template>
<!-- Main modal -->
<div id="user-upsert-modal" tabindex="-1" aria-hidden="true"
class="bg-gray-900/50 /80 hidden overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full">
<div class="relative p-4 w-full max-w-md max-h-full">
<!-- Modal content -->
<div class="relative bg-white rounded-lg shadow-sm ">
<!-- Modal header -->
<div class="flex items-center justify-between p-4 md:p-5 border-b rounded-t border-gray-200">
<h3 class="text-lg font-semibold text-gray-900 ">
大模型管理
</h3>
<button type="button" @click="closeModal"
class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center ">
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" />
</svg>
<span class="sr-only">Close modal</span>
</button>
</div>
<!-- Modal body -->
<form class="p-4 md:p-5">
<div class="grid gap-4 mb-4 grid-cols-2">
<div class="col-span-2">
<label for="name" class="block mb-2 text-sm font-medium text-gray-900 ">名称</label>
<input type="text" name="名称" id="name" v-model="formData.name"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 "
required="true">
</div>
<div class="col-span-2">
<label for="modelName" class="block mb-2 text-sm font-medium autocomplete text-gray-900 ">模型名称</label>
<input type="text" id="modelName" autocomplete="new-password" v-model="formData.modelName"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 "
required />
</div>
<div class="col-span-2">
<label for="apiKey" class="block mb-2 text-sm font-medium autocomplete text-gray-900 ">apiKey</label>
<input type="text" id="apiKey" autocomplete="new-password" v-model="formData.apiKey"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
required />
</div>
<div class="col-span-2">
<label for="url" class="block mb-2 text-sm font-medium text-gray-900 ">url</label>
<input type="text" id="url" autocomplete="new-password" v-model="formData.url"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 "
required />
</div>
<div class="col-span-2 sm:col-span-1">
<label for="status" class="block mb-2 text-sm font-medium text-gray-900 ">状态</label>
<select id="status" v-model="formData.enable"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full p-2.5">
<option :value=true>启用</option>
<option :value=false>禁用</option>
</select>
</div>
<div class="col-span-2">
<label for="priority" class="block mb-2 text-sm font-medium autocomplete text-gray-900 ">优先级</label>
<input type="text" id="priority" autocomplete="new-password" v-model="formData.priority"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
required />
</div>
</div>
<button type="submit" @click.prevent="handleSubmit"
class="text-white flex items-center bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center self-start mt-5">
保存
</button>
</form>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import useAlertStore from "@/composables/store/useAlertStore";
import { initFlowbite } from "flowbite";
import { onMounted, ref, watch } from "vue";
import { z } from "zod";
import type { components } from "../api/types/schema";
const alertStore = useAlertStore();
const { llm, onSubmit } = defineProps<{
llm?: components["schemas"]["LlmVm"];
closeModal: () => void;
onSubmit: (data: components["schemas"]["LlmVm"]) => Promise<void>;
}>();
const formData = ref();
const updateFormData = (newLlm: typeof llm) => {
formData.value = {
...newLlm,
};
};
watch(() => llm, updateFormData, {
immediate: true,
});
const handleSubmit = async () => {
try {
const llmSchema = z.object({
id: z.number({
message: "id不能为空",
}),
name: z.string({
message: "名称不能为空",
}),
modelName: z.string({
message: "模型名称不能为空",
}),
apiKey: z.string({
message: "apiKey不能为空",
}),
url: z.string({
message: "url不能为空",
}),
enable: z.boolean({
message: "状态不能为空",
}),
priority: z.number({
message: "优先级必须为数字",
}),
});
const validatedData = llmSchema.parse(formData.value);
await onSubmit(validatedData);
updateFormData(undefined);
} catch (error) {
if (error instanceof z.ZodError) {
alertStore.showAlert({
level: "error",
content: error.errors[0].message,
});
}
throw error;
}
};
onMounted(() => {
initFlowbite();
});
</script>

View File

@@ -32,6 +32,7 @@ import SchedulerIcon from "./icons/SchedulerIcon.vue";
import SettingsIcon from "./icons/SettingsIcon.vue";
import UsersIcon from "./icons/UsersIcon.vue";
import AiChatIcon from "./icons/AiChatIcon.vue";
import LlmConfigIcon from "./icons/LlmConfigIcon.vue";
// 菜单配置
const menuItems = [
@@ -75,6 +76,11 @@ const menuItems = [
path: `${RoutePath.DASHBOARD}/${RoutePath.AICHATVIEW}`,
icon: AiChatIcon,
},
{
title: "大模型管理",
path: `${RoutePath.DASHBOARD}/${RoutePath.LLMCONFIGVIEW}`,
icon: LlmConfigIcon,
},
];
const route = useRoute();

View File

@@ -42,8 +42,8 @@
required placeholder="编辑时非必填" />
</div>
<div class="col-span-2 sm:col-span-1">
<label for="category" class="block mb-2 text-sm font-medium text-gray-900 ">状态</label>
<select id="category" v-model="formData.enable"
<label for="status" class="block mb-2 text-sm font-medium text-gray-900 ">状态</label>
<select id="status" v-model="formData.enable"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full p-2.5">
<option :value=true>启用</option>
<option :value=false>禁用</option>

View File

@@ -0,0 +1,15 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-brain-icon lucide-brain">
<path d="M12 5a3 3 0 1 0-5.997.125 4 4 0 0 0-2.526 5.77 4 4 0 0 0 .556 6.588A4 4 0 1 0 12 18Z" />
<path d="M12 5a3 3 0 1 1 5.997.125 4 4 0 0 1 2.526 5.77 4 4 0 0 1-.556 6.588A4 4 0 1 1 12 18Z" />
<path d="M15 13a4.5 4.5 0 0 1-3-4 4.5 4.5 0 0 1-3 4" />
<path d="M17.599 6.5a3 3 0 0 0 .399-1.375" />
<path d="M6.003 5.125A3 3 0 0 0 6.401 6.5" />
<path d="M3.477 10.896a4 4 0 0 1 .585-.396" />
<path d="M19.938 10.5a4 4 0 0 1 .585.396" />
<path d="M6 18a4 4 0 0 1-1.967-.516" />
<path d="M19.967 17.484A4 4 0 0 1 18 18" />
</svg>
</template>

View File

@@ -1,6 +1,7 @@
import { fetchEventSource } from "@microsoft/fetch-event-source";
import { ref } from "vue";
import useAuthStore from "../store/useAuthStore";
import useAlertStore from "../store/useAlertStore";
const authStore = useAuthStore();

View File

@@ -0,0 +1,32 @@
import { ref } from "vue";
import type { components } from "../../api/types/schema";
import client from "../../api/client";
export const useLlmQuery = () => {
const total = ref<number>(0);
const llms = ref<components["schemas"]["LlmVm"][]>([]);
const fetchLlmConfigs = async (page = 1, size = 10, name?: string) => {
const { data } = await client.GET("/ai/llm/page-query", {
params: {
query: {
pageRequestDto: {
page,
size,
},
llmQueryDto: {
name,
},
},
},
});
llms.value = data?.data ?? [];
total.value = !data || !data.total ? 0 : data.total;
};
return {
llms,
total,
fetchLlmConfigs,
};
};

View File

@@ -0,0 +1,13 @@
import type { components } from "../../api/types/schema";
import client from "../../api/client";
export const useLlmUpdate = () => {
const updateLlmConfig = async (llm: components["schemas"]["LlmVm"]) => {
await client.PUT("/ai/llm", {
body: llm,
});
};
return {
updateLlmConfig,
};
};

View File

@@ -16,6 +16,7 @@ export enum RoutePath {
POSITIONVIEW = "positions",
CREATEUSERVIEW = "create-user",
AICHATVIEW = "ai/chat",
LLMCONFIGVIEW = "llm/config",
SCHEDULERVIEW = "scheduler",
UPSERTUSERVIEW = "upsert-user",
UPSERTROLEVIEW = "upsert-role",
@@ -41,6 +42,7 @@ export enum RouteName {
POSITIONVIEW = "positions",
CREATEUSERVIEW = "create-user",
AICHATVIEW = "ai/chat",
LLMCONFIGVIEW = "llm/config",
SCHEDULERVIEW = "scheduler",
UPSERTUSERVIEW = "upsert-user",
UPSERTROLEVIEW = "upsert-role",
@@ -67,4 +69,6 @@ export enum EPermission {
WRITE_USER_ROLE_PERMISSION = "WRITE_USER_ROLE_PERMISSION",
DELETE_USER_ROLE_PERMISSION = "DELETE_USER_ROLE_PERMISSION",
READ_USER_ROLE_PERMISSION = "READ_USER_ROLE_PERMISSION",
READ_LLM_CONFIG_PERMISSION = "READ_LLM_CONFIG_PERMISSION",
WRITE_LLM_CONFIG_PERMISSION = "WRITE_LLM_CONFIG_PERMISSION",
}

View File

@@ -8,7 +8,15 @@ const aiRoutes: RouteRecordRaw[] = [
component: () => import("@/views/AiChatView.vue"),
meta: {
requiresAuth: true,
// hasPermission: EPermission.READ_USER_ROLE_PERMISSION,
},
},
{
path: RoutePath.LLMCONFIGVIEW,
name: RouteName.LLMCONFIGVIEW,
component: () => import("@/views/LlmConfigView.vue"),
meta: {
requiresAuth: true,
hasPermission: EPermission.READ_LLM_CONFIG_PERMISSION,
},
},
];

View File

@@ -21,7 +21,7 @@ const makeErrorHandler =
}) => void,
) =>
(err: unknown, instance: ComponentPublicInstance | null, info: string) => {
console.error(err);
console.error(err);
if (err instanceof UnAuthError) {
signOut();
router.push(RoutePath.LOGIN);

View File

@@ -57,8 +57,8 @@
<script setup lang="ts">
import LoadingIcon from "@/components/icons/LoadingIcon.vue";
import useAlertStore from "@/composables/store/useAlertStore";
import DOMPurify from 'dompurify';
import { marked } from 'marked';
import DOMPurify from "dompurify";
import { marked } from "marked";
import { computed, nextTick, onUnmounted, ref, watch } from "vue";
import { z } from "zod";
import Button from "../components/Button.vue";
@@ -72,26 +72,24 @@ const chatContainer = ref<HTMLElement | null>(null);
const alertStore = useAlertStore();
marked.setOptions({
gfm: true,
breaks: true,
gfm: true,
breaks: true,
});
const renderMarkdown = (content: string) => {
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`');
if (!content) return "";
const rawHtml = marked(processedContent);
return DOMPurify.sanitize(rawHtml as string);
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);
};
const chatElements = computed(() => {
return messages.value.map((message, index) => {

View File

@@ -25,7 +25,7 @@
</form>
<!-- Create Modal toggle -->
<button @click="handleUpsertDepartmentClick()"
class="flex items-center block text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center absolute right-5 bottom-2"
class="flex items-center text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center absolute right-5 bottom-2"
type="button">
新增部门
</button>

View File

@@ -0,0 +1,166 @@
<template>
<div class="px-4 pt-6 xl:grid-cols-3 xl:gap-4 sm:rounded-lg mt-14">
<div class="mb-4 col-span-full">
<Breadcrumbs :names="['大模型管理']" />
<h1 class="text-xl font-semibold text-gray-900 sm:text-2xl ">大模型管理</h1>
</div>
<div class="relative">
<form class="max-w-xs mb-4 ">
<label for="default-search" class="mb-2 text-sm font-medium text-gray-900 sr-only ">Search</label>
<div class="relative">
<div class="absolute inset-y-0 start-0 flex items-center ps-3 pointer-events-none">
<svg class="w-4 h-4 text-gray-500 " aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none"
viewBox="0 0 20 20">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z" />
</svg>
</div>
<input type="search" id="default-search" v-model="name"
class="block w-full p-4 ps-10 text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-blue-500 focus:border-blue-500 "
placeholder="模型名称" required />
<button type="submit"
class="text-white absolute end-2.5 bottom-2.5 bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2 "
@click.prevent="handleSearch">搜索</button>
</div>
</form>
<!-- Create Modal toggle -->
</div>
<div class="relative overflow-x-auto">
<table
class="w-full whitespace-nowrap text-sm text-left rtl:text-right shadow-lg rounded-lg text-gray-500 overflow-x-auto">
<thead class="text-xs uppercase bg-gray-50 ">
<tr>
<th scope="col" class="p-4">
<div class="flex items-center">
<input id="checkbox-all-search" disabled type="checkbox"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 focus:ring-2 ">
<label for="checkbox-all-search" class="sr-only">checkbox</label>
</div>
</th>
<th scope="col" class="px-6 py-3">名称</th>
<th scope="col" class="px-6 py-3">模型名称</th>
<th scope="col" class="px-6 py-3">apiKey</th>
<th scope="col" class="px-6 py-3">url</th>
<th scope="col" class="px-6 py-3">状态</th>
<th scope="col" class="px-6 py-3">优先级</th>
<th scope="col" class="px-6 py-3">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="llm in llms" :key="llm.id" class="bg-white border-b border-gray-200 hover:bg-gray-50 ">
<td class="w-4 p-4">
<div class="flex items-center">
<input :id="'checkbox-table-search-' + llm.id" type="checkbox" disabled
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 focus:ring-2 ">
<label :for="'checkbox-table-search-' + llm.id" class="sr-only">checkbox</label>
</div>
</td>
<td class="px-6 py-4 max-w-sm overflow-hidden text-ellipsis font-medium text-gray-900">
{{
`${llm.name}` }}</td>
<td class="px-6 py-4 max-w-sm overflow-hidden text-ellipsis">{{
`${llm.modelName}` }}
</td>
<td class="px-6 py-4 max-w-sm overflow-hidden text-ellipsis">{{
llm.apiKey }}
</td>
<td class="px-6 py-4 max-w-sm overflow-hidden text-ellipsis">{{ llm.url }}</td>
<td class="px-6 py-4 max-w-sm overflow-hidden text-ellipsis">
<div class="flex items-center">
<div class="h-2.5 w-2.5 rounded-full me-2" :class="llm.enable ? 'bg-blue-500' : 'bg-red-500'"></div> {{
llm.enable === true ? "启用" : "禁用" }}
</div>
</td>
<td class="px-6 py-4 max-w-sm overflow-hidden text-ellipsis">{{ llm.priority }}
</td>
<td class="px-6 py-4 ">
<div class="flex items-center gap-x-2">
<button @click="handleLlmUpdateClick(llm)"
:class="['flex items-center justify-center gap-x-1 text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 ']"
type="button">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z"></path>
<path fill-rule="evenodd"
d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"
clip-rule="evenodd"></path>
</svg>
<span>编辑</span>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<TablePagination :pageChange="handlePageChange" :total="total" />
</div>
<LlmUpdateModal :llm="selectedLlm" :id="'llm-update-modal'" :closeModal="() => {
llmUpdateModal!.hide();
}" :onSubmit="handleUpdateModalSubmit"></LlmUpdateModal>
</template>
<script setup lang="ts">
import Breadcrumbs from "@/components/Breadcrumbs.vue";
import TablePagination from "@/components/TablePagination.vue";
import useAlertStore from "@/composables/store/useAlertStore";
import { Modal, type ModalInterface, initFlowbite } from "flowbite";
import { nextTick, onMounted, ref } from "vue";
import type { components } from "../api/types/schema";
import { useLlmQuery } from "@/composables/ai/useLlmQuery";
import { useLlmUpdate } from "@/composables/ai/useLlmUpdate";
import LlmUpdateModal from "@/components/LlmUpdateModal.vue";
const llmUpdateModal = ref<ModalInterface>();
const selectedLlm = ref<components["schemas"]["LlmVm"]>();
const name = ref<string>("");
const { llms, fetchLlmConfigs, total } = useLlmQuery();
const { updateLlmConfig } = useLlmUpdate();
const alertStore = useAlertStore();
const handleUpdateModalSubmit = async (llm: components["schemas"]["LlmVm"]) => {
await updateLlmConfig(llm);
llmUpdateModal.value?.hide();
alertStore.showAlert({
level: "success",
content: "操作成功",
});
await fetchLlmConfigs();
};
const handleSearch = async () => {
await fetchLlmConfigs();
};
const handlePageChange = async (page: number, pageSize: number) => {
await fetchLlmConfigs(page, pageSize);
};
const handleLlmUpdateClick = async (llm: components["schemas"]["LlmVm"]) => {
selectedLlm.value = llm;
await nextTick(() => {
llmUpdateModal.value?.show();
});
};
onMounted(async () => {
await fetchLlmConfigs();
initFlowbite();
const $llmUpdateModalElement: HTMLElement | null =
document.querySelector("#llm-update-modal");
llmUpdateModal.value = new Modal(
$llmUpdateModalElement,
{},
{
id: "llm-update-modal",
},
);
});
</script>
<style scoped></style>