mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-02 12:05:54 +00:00
chore: rename web_ui dir to web
This commit is contained in:
BIN
web/src/app/favicon.ico
Normal file
BIN
web/src/app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.4 KiB |
37
web/src/app/global.css
Normal file
37
web/src/app/global.css
Normal file
@@ -0,0 +1,37 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
:root {
|
||||
/* 适用于 Firefox 的滚动条 */
|
||||
scrollbar-color: rgba(0, 0, 0, 0.2) transparent; /* 滑块颜色 + 轨道颜色 */
|
||||
scrollbar-width: thin; /* auto | thin | none */
|
||||
}
|
||||
|
||||
/* WebKit 内核浏览器定制 */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px; /* 垂直滚动条宽度 */
|
||||
height: 6px; /* 水平滚动条高度 */
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent; /* 隐藏轨道背景 */
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 0, 0, 0.2); /* 半透明黑色 */
|
||||
border-radius: 3px;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(0, 0, 0, 0.35); /* 悬停加深 */
|
||||
}
|
||||
|
||||
/* 兼容 Edge */
|
||||
@supports (-ms-ime-align:auto) {
|
||||
body {
|
||||
-ms-overflow-style: -ms-autohiding-scrollbar; /* 自动隐藏滚动条 */
|
||||
}
|
||||
}
|
||||
0
web/src/app/home/bots/ICreateBotField.ts
Normal file
0
web/src/app/home/bots/ICreateBotField.ts
Normal file
23
web/src/app/home/bots/botConfig.module.css
Normal file
23
web/src/app/home/bots/botConfig.module.css
Normal file
@@ -0,0 +1,23 @@
|
||||
.configPageContainer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.cardContainer {
|
||||
width: 420px;
|
||||
height: 220px;
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
.botListContainer {
|
||||
align-self: flex-start;
|
||||
justify-self: flex-start;
|
||||
width: calc(100% - 60px);
|
||||
margin: auto;
|
||||
display: grid;
|
||||
grid-template-rows: repeat(auto-fill, minmax(220px, 1fr));
|
||||
grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
|
||||
gap: 15px;
|
||||
justify-items: center;
|
||||
align-items: center;
|
||||
}
|
||||
39
web/src/app/home/bots/components/bot-card/BotCard.tsx
Normal file
39
web/src/app/home/bots/components/bot-card/BotCard.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import {BotCardVO} from "@/app/home/bots/components/bot-card/BotCardVO";
|
||||
import styles from "./botCard.module.css";
|
||||
|
||||
export default function BotCard({
|
||||
botCardVO
|
||||
}: {
|
||||
botCardVO: BotCardVO;
|
||||
}) {
|
||||
return (
|
||||
<div className={`${styles.cardContainer}`}>
|
||||
{/* icon和基本信息 */}
|
||||
<div className={`${styles.iconBasicInfoContainer}`}>
|
||||
{/* icon */}
|
||||
<div className={`${styles.icon}`}>
|
||||
ICO
|
||||
</div>
|
||||
{/* bot基本信息 */}
|
||||
<div className={`${styles.basicInfoContainer}`}>
|
||||
<div className={`${styles.basicInfoText} ${styles.bigText}`}>
|
||||
{botCardVO.name}
|
||||
</div>
|
||||
<div className={`${styles.basicInfoText}`}>
|
||||
平台:{botCardVO.adapter}
|
||||
</div>
|
||||
<div className={`${styles.basicInfoText}`}>
|
||||
绑定流水线:{botCardVO.pipelineName}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* 描述和创建时间 */}
|
||||
<div className={`${styles.urlAndUpdateText}`}>
|
||||
描述:{botCardVO.description}
|
||||
</div>
|
||||
{/* <div className={`${styles.urlAndUpdateText}`}>
|
||||
更新时间:{botCardVO.updateTime}
|
||||
</div> */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
28
web/src/app/home/bots/components/bot-card/BotCardVO.ts
Normal file
28
web/src/app/home/bots/components/bot-card/BotCardVO.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
export interface IBotCardVO {
|
||||
id: string;
|
||||
name: string;
|
||||
adapter: string;
|
||||
description: string;
|
||||
updateTime: string;
|
||||
pipelineName: string;
|
||||
}
|
||||
|
||||
export class BotCardVO implements IBotCardVO {
|
||||
id: string;
|
||||
adapter: string;
|
||||
description: string;
|
||||
name: string;
|
||||
updateTime: string;
|
||||
pipelineName: string;
|
||||
|
||||
|
||||
constructor(props: IBotCardVO) {
|
||||
this.id = props.id;
|
||||
this.name = props.name;
|
||||
this.adapter = props.adapter;
|
||||
this.description = props.description;
|
||||
this.updateTime = props.updateTime;
|
||||
this.pipelineName = props.pipelineName;
|
||||
}
|
||||
|
||||
}
|
||||
67
web/src/app/home/bots/components/bot-card/botCard.module.css
Normal file
67
web/src/app/home/bots/components/bot-card/botCard.module.css
Normal file
@@ -0,0 +1,67 @@
|
||||
.iconBasicInfoContainer {
|
||||
width: 300px;
|
||||
height: 100px;
|
||||
margin-left: 20px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.cardContainer {
|
||||
width: 360px;
|
||||
height: 200px;
|
||||
background-color: #FFF;
|
||||
border-radius: 9px;
|
||||
box-shadow: rgba(0, 0, 0, 0.4) 0 1px 1px -1px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
.iconBasicInfoContainer {
|
||||
width: 300px;
|
||||
height: 100px;
|
||||
margin-left: 20px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
border-radius: 5px;
|
||||
font-size: 40px;
|
||||
line-height: 90px;
|
||||
text-align: center;
|
||||
color: #ffffff;
|
||||
background: rgba(96, 149, 209, 0.31);
|
||||
border: 1px solid rgba(96, 149, 209, 0.31);
|
||||
}
|
||||
|
||||
.basicInfoContainer {
|
||||
width: 200px;
|
||||
height: 90px;
|
||||
padding-left: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.basicInfoText {
|
||||
|
||||
}
|
||||
|
||||
.bigText {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.urlAndUpdateText {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.createCardContainer {
|
||||
font-size: 90px;
|
||||
background: #6062E7;
|
||||
color: white;
|
||||
}
|
||||
292
web/src/app/home/bots/components/bot-form/BotForm.tsx
Normal file
292
web/src/app/home/bots/components/bot-form/BotForm.tsx
Normal file
@@ -0,0 +1,292 @@
|
||||
import {
|
||||
BotFormEntity,
|
||||
IBotFormEntity
|
||||
} from "@/app/home/bots/components/bot-form/BotFormEntity";
|
||||
import { Button, Form, Input, notification, Select, Space } from "antd";
|
||||
import { useEffect, useState } from "react";
|
||||
import { IChooseAdapterEntity } from "@/app/home/bots/components/bot-form/ChooseAdapterEntity";
|
||||
import {
|
||||
DynamicFormItemConfig,
|
||||
IDynamicFormItemConfig,
|
||||
parseDynamicFormItemType
|
||||
} from "@/app/home/components/dynamic-form/DynamicFormItemConfig";
|
||||
import { UUID } from "uuidjs";
|
||||
import DynamicFormComponent from "@/app/home/components/dynamic-form/DynamicFormComponent";
|
||||
import { httpClient } from "@/app/infra/http/HttpClient";
|
||||
import { Bot } from "@/app/infra/api/api-types";
|
||||
|
||||
export default function BotForm({
|
||||
initBotId,
|
||||
onFormSubmit,
|
||||
onFormCancel
|
||||
}: {
|
||||
initBotId?: string;
|
||||
onFormSubmit: (value: IBotFormEntity) => void;
|
||||
onFormCancel: (value: IBotFormEntity) => void;
|
||||
}) {
|
||||
const [adapterNameToDynamicConfigMap, setAdapterNameToDynamicConfigMap] =
|
||||
useState(new Map<string, IDynamicFormItemConfig[]>());
|
||||
const [form] = Form.useForm<IBotFormEntity>();
|
||||
const [showDynamicForm, setShowDynamicForm] = useState<boolean>(false);
|
||||
const [dynamicForm] = Form.useForm();
|
||||
const [adapterNameList, setAdapterNameList] = useState<
|
||||
IChooseAdapterEntity[]
|
||||
>([]);
|
||||
const [dynamicFormConfigList, setDynamicFormConfigList] = useState<
|
||||
IDynamicFormItemConfig[]
|
||||
>([]);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
initBotFormComponent();
|
||||
if (initBotId) {
|
||||
onEditMode();
|
||||
} else {
|
||||
onCreateMode();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
async function initBotFormComponent() {
|
||||
// 拉取adapter
|
||||
const rawAdapterList = await httpClient.getAdapters();
|
||||
// 初始化适配器选择列表
|
||||
setAdapterNameList(
|
||||
rawAdapterList.adapters.map((item) => {
|
||||
return {
|
||||
label: item.label.zh_CN,
|
||||
value: item.name
|
||||
};
|
||||
})
|
||||
);
|
||||
// 初始化适配器表单map
|
||||
rawAdapterList.adapters.forEach((rawAdapter) => {
|
||||
adapterNameToDynamicConfigMap.set(
|
||||
rawAdapter.name,
|
||||
rawAdapter.spec.config.map(
|
||||
(item) =>
|
||||
new DynamicFormItemConfig({
|
||||
default: item.default,
|
||||
id: UUID.generate(),
|
||||
label: item.label,
|
||||
name: item.name,
|
||||
required: item.required,
|
||||
type: parseDynamicFormItemType(item.type)
|
||||
})
|
||||
)
|
||||
);
|
||||
});
|
||||
// 拉取初始化表单信息
|
||||
if (initBotId) {
|
||||
getBotFieldById(initBotId).then((val) => {
|
||||
form.setFieldsValue(val);
|
||||
handleAdapterSelect(val.adapter);
|
||||
dynamicForm.setFieldsValue(val.adapter_config);
|
||||
});
|
||||
} else {
|
||||
form.resetFields();
|
||||
}
|
||||
setAdapterNameToDynamicConfigMap(adapterNameToDynamicConfigMap);
|
||||
}
|
||||
|
||||
async function onCreateMode() {}
|
||||
|
||||
function onEditMode() {}
|
||||
|
||||
async function getBotFieldById(botId: string): Promise<IBotFormEntity> {
|
||||
const bot = (await httpClient.getBot(botId)).bot;
|
||||
return new BotFormEntity({
|
||||
adapter: bot.adapter,
|
||||
description: bot.description,
|
||||
name: bot.name,
|
||||
adapter_config: bot.adapter_config
|
||||
});
|
||||
}
|
||||
|
||||
function handleAdapterSelect(adapterName: string) {
|
||||
console.log("Select adapter: ", adapterName);
|
||||
if (adapterName) {
|
||||
const dynamicFormConfigList =
|
||||
adapterNameToDynamicConfigMap.get(adapterName);
|
||||
console.log(dynamicFormConfigList);
|
||||
if (dynamicFormConfigList) {
|
||||
setDynamicFormConfigList(dynamicFormConfigList);
|
||||
}
|
||||
setShowDynamicForm(true);
|
||||
} else {
|
||||
setShowDynamicForm(false);
|
||||
}
|
||||
}
|
||||
|
||||
function handleSubmitButton() {
|
||||
form.submit();
|
||||
}
|
||||
|
||||
function handleFormFinish() {
|
||||
dynamicForm.submit();
|
||||
}
|
||||
|
||||
// 只有通过外层固定表单验证才会走到这里,真正的提交逻辑在这里
|
||||
function onDynamicFormSubmit(value: object) {
|
||||
setIsLoading(true);
|
||||
console.log("set loading", true);
|
||||
if (initBotId) {
|
||||
// 编辑提交
|
||||
console.log("submit edit", form.getFieldsValue(), value);
|
||||
const updateBot: Bot = {
|
||||
uuid: initBotId,
|
||||
name: form.getFieldsValue().name,
|
||||
description: form.getFieldsValue().description,
|
||||
adapter: form.getFieldsValue().adapter,
|
||||
adapter_config: value
|
||||
};
|
||||
httpClient
|
||||
.updateBot(initBotId, updateBot)
|
||||
.then((res) => {
|
||||
// TODO success toast
|
||||
console.log("update bot success", res);
|
||||
onFormSubmit(form.getFieldsValue());
|
||||
notification.success({
|
||||
message: "更新成功",
|
||||
description: "机器人更新成功"
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
// TODO error toast
|
||||
notification.error({
|
||||
message: "更新失败",
|
||||
description: "机器人更新失败"
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
form.resetFields();
|
||||
dynamicForm.resetFields();
|
||||
});
|
||||
} else {
|
||||
// 创建提交
|
||||
console.log("submit create", form.getFieldsValue(), value);
|
||||
const newBot: Bot = {
|
||||
name: form.getFieldsValue().name,
|
||||
description: form.getFieldsValue().description,
|
||||
adapter: form.getFieldsValue().adapter,
|
||||
adapter_config: value
|
||||
};
|
||||
httpClient
|
||||
.createBot(newBot)
|
||||
.then((res) => {
|
||||
// TODO success toast
|
||||
notification.success({
|
||||
message: "创建成功",
|
||||
description: "机器人创建成功"
|
||||
});
|
||||
console.log(res);
|
||||
onFormSubmit(form.getFieldsValue());
|
||||
})
|
||||
.catch(() => {
|
||||
// TODO error toast
|
||||
notification.error({
|
||||
message: "创建失败",
|
||||
description: "机器人创建失败"
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
form.resetFields();
|
||||
dynamicForm.resetFields();
|
||||
});
|
||||
}
|
||||
setShowDynamicForm(false);
|
||||
console.log("set loading", false);
|
||||
// TODO 刷新bot列表
|
||||
// TODO 关闭当前弹窗 Already closed @setShowDynamicForm(false)?
|
||||
}
|
||||
|
||||
function handleSaveButton() {
|
||||
form.submit();
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Form
|
||||
form={form}
|
||||
labelCol={{ span: 5 }}
|
||||
wrapperCol={{ span: 18 }}
|
||||
layout="vertical"
|
||||
onFinish={handleFormFinish}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<Form.Item<IBotFormEntity>
|
||||
label={"机器人名称"}
|
||||
name={"name"}
|
||||
rules={[{ required: true, message: "该项为必填项哦~" }]}
|
||||
>
|
||||
<Input
|
||||
placeholder="为机器人取个好听的名字吧~"
|
||||
style={{ width: 260 }}
|
||||
></Input>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<IBotFormEntity>
|
||||
label={"描述"}
|
||||
name={"description"}
|
||||
rules={[{ required: true, message: "该项为必填项哦~" }]}
|
||||
>
|
||||
<Input placeholder="简单描述一下这个机器人"></Input>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<IBotFormEntity>
|
||||
label={"平台/适配器选择"}
|
||||
name={"adapter"}
|
||||
rules={[{ required: true, message: "该项为必填项哦~" }]}
|
||||
>
|
||||
<Select
|
||||
style={{ width: 220 }}
|
||||
onChange={(value) => {
|
||||
handleAdapterSelect(value);
|
||||
}}
|
||||
options={adapterNameList}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
{showDynamicForm && (
|
||||
<DynamicFormComponent
|
||||
form={dynamicForm}
|
||||
itemConfigList={dynamicFormConfigList}
|
||||
onSubmit={onDynamicFormSubmit}
|
||||
/>
|
||||
)}
|
||||
<Space>
|
||||
{!initBotId && (
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="button"
|
||||
onClick={handleSubmitButton}
|
||||
loading={isLoading}
|
||||
>
|
||||
提交
|
||||
</Button>
|
||||
)}
|
||||
{initBotId && (
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
onClick={handleSaveButton}
|
||||
loading={isLoading}
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
htmlType="button"
|
||||
onClick={() => {
|
||||
onFormCancel(form.getFieldsValue());
|
||||
}}
|
||||
disabled={isLoading}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
20
web/src/app/home/bots/components/bot-form/BotFormEntity.ts
Normal file
20
web/src/app/home/bots/components/bot-form/BotFormEntity.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
export interface IBotFormEntity {
|
||||
name: string,
|
||||
description: string,
|
||||
adapter: string,
|
||||
adapter_config: object;
|
||||
}
|
||||
|
||||
export class BotFormEntity implements IBotFormEntity {
|
||||
adapter: string;
|
||||
description: string;
|
||||
name: string;
|
||||
adapter_config: object;
|
||||
|
||||
constructor(props: IBotFormEntity) {
|
||||
this.adapter = props.adapter;
|
||||
this.description = props.description;
|
||||
this.name = props.name;
|
||||
this.adapter_config = props.adapter_config;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface IChooseAdapterEntity {
|
||||
label: string
|
||||
value: string
|
||||
}
|
||||
162
web/src/app/home/bots/page.tsx
Normal file
162
web/src/app/home/bots/page.tsx
Normal file
@@ -0,0 +1,162 @@
|
||||
"use client"
|
||||
|
||||
import {useEffect, useState} from "react";
|
||||
import styles from "./botConfig.module.css";
|
||||
import EmptyAndCreateComponent from "@/app/home/components/empty-and-create-component/EmptyAndCreateComponent";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {BotCardVO} from "@/app/home/bots/components/bot-card/BotCardVO";
|
||||
import {Modal, notification, Spin} from "antd";
|
||||
import BotForm from "@/app/home/bots/components/bot-form/BotForm";
|
||||
import BotCard from "@/app/home/bots/components/bot-card/BotCard";
|
||||
import CreateCardComponent from "@/app/infra/basic-component/create-card-component/CreateCardComponent"
|
||||
import {httpClient} from "@/app/infra/http/HttpClient";
|
||||
import { Bot } from "@/app/infra/api/api-types";
|
||||
|
||||
export default function BotConfigPage() {
|
||||
const router = useRouter();
|
||||
const [pageShowRule, setPageShowRule] = useState<BotConfigPageShowRule>(BotConfigPageShowRule.NO_BOT)
|
||||
const [modalOpen, setModalOpen] = useState<boolean>(false);
|
||||
const [botList, setBotList] = useState<BotCardVO[]>([])
|
||||
const [isEditForm, setIsEditForm] = useState(false)
|
||||
const [nowSelectedBotCard, setNowSelectedBotCard] = useState<BotCardVO>()
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
// TODO:补齐加载转圈逻辑
|
||||
setIsLoading(true)
|
||||
checkHasLLM().then((hasLLM) => {
|
||||
if (hasLLM) {
|
||||
getBotList()
|
||||
} else {
|
||||
setPageShowRule(BotConfigPageShowRule.NO_LLM)
|
||||
setIsLoading(false)
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
||||
async function checkHasLLM(): Promise<boolean> {
|
||||
// NOT IMPL
|
||||
return true
|
||||
}
|
||||
|
||||
function getBotList() {
|
||||
httpClient.getBots().then((resp) => {
|
||||
const botList: BotCardVO[] = resp.bots.map((bot: Bot) => {
|
||||
return new BotCardVO({
|
||||
adapter: bot.adapter,
|
||||
description: bot.description,
|
||||
id: bot.uuid || "",
|
||||
name: bot.name,
|
||||
updateTime: bot.updated_at || "",
|
||||
pipelineName: bot.use_pipeline_name || "",
|
||||
})
|
||||
})
|
||||
if (botList.length === 0) {
|
||||
setPageShowRule(BotConfigPageShowRule.NO_BOT)
|
||||
} else {
|
||||
setPageShowRule(BotConfigPageShowRule.HAVE_BOT)
|
||||
}
|
||||
setBotList(botList)
|
||||
}).catch((err) => {
|
||||
console.error("get bot list error", err)
|
||||
// TODO HACK: need refactor to hook mode Notification, but it's not working under render
|
||||
notification.error({
|
||||
message: "获取机器人列表失败",
|
||||
description: err.message,
|
||||
placement: "bottomRight",
|
||||
})
|
||||
}).finally(() => {
|
||||
setIsLoading(false)
|
||||
})
|
||||
}
|
||||
|
||||
function handleCreateBotClick() {
|
||||
setIsEditForm(false)
|
||||
setNowSelectedCard(undefined)
|
||||
setModalOpen(true);
|
||||
}
|
||||
|
||||
function setNowSelectedCard(cardVO: BotCardVO | undefined) {
|
||||
setNowSelectedBotCard(cardVO)
|
||||
}
|
||||
|
||||
function selectBot(cardVO: BotCardVO) {
|
||||
setIsEditForm(true)
|
||||
setNowSelectedCard(cardVO)
|
||||
console.log("set now vo", cardVO)
|
||||
setModalOpen(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<Spin spinning={isLoading}>
|
||||
<div className={styles.configPageContainer}>
|
||||
<Modal
|
||||
title={isEditForm ? "编辑机器人" : "创建机器人"}
|
||||
centered
|
||||
open={modalOpen}
|
||||
onOk={() => setModalOpen(false)}
|
||||
onCancel={() => setModalOpen(false)}
|
||||
width={700}
|
||||
footer={null}
|
||||
destroyOnClose={true}
|
||||
>
|
||||
<BotForm
|
||||
initBotId={nowSelectedBotCard?.id}
|
||||
onFormSubmit={() => {
|
||||
getBotList()
|
||||
setModalOpen(false)
|
||||
}}
|
||||
onFormCancel={() => setModalOpen(false)}
|
||||
/>
|
||||
</Modal>
|
||||
{pageShowRule === BotConfigPageShowRule.NO_LLM &&
|
||||
<EmptyAndCreateComponent
|
||||
title={"需要先创建大模型才能配置机器人哦~"}
|
||||
subTitle={"快去创建一个吧!"}
|
||||
buttonText={"创建大模型 GO!"}
|
||||
onButtonClick={() => {
|
||||
router.push("/home/models");
|
||||
}}
|
||||
/>
|
||||
}
|
||||
|
||||
{pageShowRule === BotConfigPageShowRule.NO_BOT &&
|
||||
<EmptyAndCreateComponent
|
||||
title={"您还未配置机器人哦~"}
|
||||
subTitle={"快去创建一个吧!"}
|
||||
buttonText={"创建机器人 +"}
|
||||
onButtonClick={handleCreateBotClick}
|
||||
/>
|
||||
}
|
||||
|
||||
{pageShowRule === BotConfigPageShowRule.HAVE_BOT &&
|
||||
<div className={`${styles.botListContainer}`}
|
||||
>
|
||||
{botList.map(cardVO => {
|
||||
return (
|
||||
<div
|
||||
key={cardVO.id}
|
||||
onClick={() => {selectBot(cardVO)}}
|
||||
>
|
||||
<BotCard botCardVO={cardVO} />
|
||||
</div>)
|
||||
})}
|
||||
<CreateCardComponent
|
||||
width={360}
|
||||
height={200}
|
||||
plusSize={90}
|
||||
onClick={handleCreateBotClick}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</Spin>
|
||||
)
|
||||
}
|
||||
|
||||
enum BotConfigPageShowRule {
|
||||
NO_LLM,
|
||||
NO_BOT,
|
||||
HAVE_BOT,
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import {IDynamicFormItemConfig} from "@/app/home/components/dynamic-form/DynamicFormItemConfig";
|
||||
import {Form, FormInstance} from "antd";
|
||||
import DynamicFormItemComponent from "@/app/home/components/dynamic-form/DynamicFormItemComponent";
|
||||
|
||||
export default function DynamicFormComponent({
|
||||
form,
|
||||
itemConfigList,
|
||||
onSubmit,
|
||||
}: {
|
||||
form: FormInstance<object>
|
||||
itemConfigList: IDynamicFormItemConfig[]
|
||||
onSubmit?: (val: object) => unknown
|
||||
}) {
|
||||
return (
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={onSubmit}
|
||||
layout={"vertical"}
|
||||
>
|
||||
{
|
||||
itemConfigList.map(config =>
|
||||
<DynamicFormItemComponent
|
||||
key={config.id}
|
||||
config={config}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import {Form, Input, InputNumber, Select, Switch} from "antd";
|
||||
import {DynamicFormItemType, IDynamicFormItemConfig} from "@/app/home/components/dynamic-form/DynamicFormItemConfig";
|
||||
|
||||
export default function DynamicFormItemComponent({
|
||||
config
|
||||
}: {
|
||||
config: IDynamicFormItemConfig
|
||||
}) {
|
||||
return (
|
||||
<Form.Item
|
||||
label={config.label.zh_CN}
|
||||
name={config.name}
|
||||
rules={[{required: config.required, message: "该项为必填项哦~"}]}
|
||||
initialValue={config.default}
|
||||
>
|
||||
{
|
||||
config.type === DynamicFormItemType.INT &&
|
||||
<InputNumber/>
|
||||
}
|
||||
|
||||
{
|
||||
config.type === DynamicFormItemType.STRING &&
|
||||
<Input/>
|
||||
}
|
||||
|
||||
{
|
||||
config.type === DynamicFormItemType.BOOLEAN &&
|
||||
<Switch defaultChecked/>
|
||||
}
|
||||
|
||||
{
|
||||
config.type === DynamicFormItemType.STRING_ARRAY &&
|
||||
<Select options={[]}/>
|
||||
}
|
||||
</Form.Item>
|
||||
)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
export interface IDynamicFormItemConfig {
|
||||
id: string;
|
||||
default: string | number | boolean | Array<unknown>;
|
||||
label: IDynamicFormItemLabel;
|
||||
name: string;
|
||||
required: boolean;
|
||||
type: DynamicFormItemType
|
||||
description?: IDynamicFormItemLabel;
|
||||
}
|
||||
|
||||
export class DynamicFormItemConfig implements IDynamicFormItemConfig {
|
||||
id: string;
|
||||
name: string;
|
||||
default: string | number | boolean | Array<unknown>;
|
||||
label: IDynamicFormItemLabel;
|
||||
required: boolean;
|
||||
type: DynamicFormItemType;
|
||||
description?: IDynamicFormItemLabel;
|
||||
|
||||
constructor(params: IDynamicFormItemConfig) {
|
||||
this.id = params.id;
|
||||
this.name = params.name;
|
||||
this.default = params.default;
|
||||
this.label = params.label;
|
||||
this.required = params.required;
|
||||
this.type = params.type;
|
||||
this.description = params.description;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export interface IDynamicFormItemLabel {
|
||||
en_US: string,
|
||||
zh_CN: string,
|
||||
}
|
||||
|
||||
export enum DynamicFormItemType {
|
||||
INT = "integer",
|
||||
STRING = "string",
|
||||
BOOLEAN = "boolean",
|
||||
STRING_ARRAY = "array[string]",
|
||||
UNKNOWN = "unknown",
|
||||
}
|
||||
|
||||
export function isDynamicFormItemType(value: string): value is DynamicFormItemType {
|
||||
return Object.values(DynamicFormItemType).includes(value as DynamicFormItemType);
|
||||
}
|
||||
|
||||
export function parseDynamicFormItemType(value: string): DynamicFormItemType {
|
||||
return isDynamicFormItemType(value) ? value : DynamicFormItemType.UNKNOWN;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import {
|
||||
DynamicFormItemConfig,
|
||||
DynamicFormItemType,
|
||||
IDynamicFormItemConfig
|
||||
} from "@/app/home/components/dynamic-form/DynamicFormItemConfig";
|
||||
|
||||
export const testDynamicConfigList: IDynamicFormItemConfig[] = [
|
||||
new DynamicFormItemConfig({
|
||||
default: "",
|
||||
id: "111",
|
||||
label: {
|
||||
zh_CN: "测试字段string",
|
||||
en_US: "eng test"
|
||||
},
|
||||
name: "string_test",
|
||||
required: false,
|
||||
type: DynamicFormItemType.STRING
|
||||
}),
|
||||
new DynamicFormItemConfig({
|
||||
default: "",
|
||||
id: "222",
|
||||
label: {
|
||||
zh_CN: "测试字段int",
|
||||
en_US: "int eng test"
|
||||
},
|
||||
name: "int_test",
|
||||
required: true,
|
||||
type: DynamicFormItemType.INT
|
||||
}),
|
||||
new DynamicFormItemConfig({
|
||||
default: "",
|
||||
id: "333",
|
||||
label: {
|
||||
zh_CN: "测试字段boolean",
|
||||
en_US: "boolean eng test"
|
||||
},
|
||||
name: "boolean_test",
|
||||
required: false,
|
||||
type: DynamicFormItemType.BOOLEAN
|
||||
}),
|
||||
]
|
||||
@@ -0,0 +1,35 @@
|
||||
import styles from "./emptyAndCreate.module.css";
|
||||
|
||||
export default function EmptyAndCreateComponent({
|
||||
title,
|
||||
subTitle,
|
||||
buttonText,
|
||||
onButtonClick,
|
||||
}: {
|
||||
title: string,
|
||||
subTitle: string,
|
||||
buttonText: string,
|
||||
onButtonClick: () => void,
|
||||
}) {
|
||||
|
||||
return (
|
||||
<div className={`${styles.emptyPageContainer}`}>
|
||||
<div className={`${styles.emptyContainer}`}>
|
||||
<div className={`${styles.emptyInfoContainer}`}>
|
||||
<div className={`${styles.emptyInfoText}`}>
|
||||
{title}
|
||||
</div>
|
||||
<div className={`${styles.emptyInfoSubText}`}>
|
||||
{subTitle}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`${styles.emptyCreateButton}`}
|
||||
onClick={onButtonClick}
|
||||
>
|
||||
{buttonText}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
.emptyPageContainer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #FFF;
|
||||
border: 1px solid #c5c5c5;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.emptyContainer {
|
||||
width: 100%;
|
||||
height: 50%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
.emptyCreateButton {
|
||||
width: 200px;
|
||||
height: 50px;
|
||||
border-radius: 20px;
|
||||
background-color: #2288ee;
|
||||
color: #FFF;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
line-height: 50px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.emptyCreateButton:hover {
|
||||
background-color: #1b77d2;
|
||||
}
|
||||
|
||||
.emptyInfoContainer {
|
||||
width: 100%;
|
||||
height: 60px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
color: #353535;
|
||||
}
|
||||
|
||||
.emptyInfoText {
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
.emptyInfoSubText {
|
||||
font-size: 28px;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
.sidebarContainer {
|
||||
box-sizing: border-box;
|
||||
width: 200px;
|
||||
height: 100vh;
|
||||
background-color: #FFF;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.langbotIconContainer {
|
||||
width: 200px;
|
||||
height: 70px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-evenly;
|
||||
|
||||
.langbotIcon {
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
border-radius: 12px;
|
||||
background: #2288ee;
|
||||
color: #fbfbfb;
|
||||
font-weight: 600;
|
||||
font-size: 36px;
|
||||
line-height: 54px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.langbotText {
|
||||
font-size: 26px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.sidebarChildContainer {
|
||||
box-sizing: border-box;
|
||||
width: 160px;
|
||||
height: 48px;
|
||||
margin: 12px 0;
|
||||
font-size: 16px;
|
||||
background-color: #fff;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sidebarSelected {
|
||||
background-color: #2288ee;
|
||||
color: white;
|
||||
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.sidebarUnselected {
|
||||
background-color: white;
|
||||
color: #6C6C6C;
|
||||
}
|
||||
|
||||
.sidebarChildIcon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
margin-left: 16px;
|
||||
margin-right: 6px;
|
||||
background-color: rgba(96, 149, 209, 0);
|
||||
}
|
||||
100
web/src/app/home/components/home-sidebar/HomeSidebar.tsx
Normal file
100
web/src/app/home/components/home-sidebar/HomeSidebar.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
"use client";
|
||||
|
||||
import styles from "./HomeSidebar.module.css";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
SidebarChild,
|
||||
SidebarChildVO
|
||||
} from "@/app/home/components/home-sidebar/HomeSidebarChild";
|
||||
import { useRouter, usePathname } from "next/navigation";
|
||||
import { sidebarConfigList } from "@/app/home/components/home-sidebar/sidbarConfigList";
|
||||
|
||||
// TODO 侧边导航栏要加动画
|
||||
export default function HomeSidebar({
|
||||
onSelectedChangeAction
|
||||
}: {
|
||||
onSelectedChangeAction: (sidebarChild: SidebarChildVO) => void;
|
||||
}) {
|
||||
// 路由相关
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
// 路由被动变化时处理
|
||||
useEffect(() => {
|
||||
handleRouteChange(pathname);
|
||||
}, [pathname]);
|
||||
|
||||
const [selectedChild, setSelectedChild] = useState<SidebarChildVO>(
|
||||
sidebarConfigList[0]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
console.log("HomeSidebar挂载完成");
|
||||
initSelect();
|
||||
return () => console.log("HomeSidebar卸载");
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
function handleChildClick(child: SidebarChildVO) {
|
||||
setSelectedChild(child);
|
||||
handleRoute(child);
|
||||
onSelectedChangeAction(child);
|
||||
}
|
||||
|
||||
function initSelect() {
|
||||
handleChildClick(sidebarConfigList[0]);
|
||||
}
|
||||
|
||||
function handleRoute(child: SidebarChildVO) {
|
||||
console.log(child);
|
||||
router.push(`${child.route}`);
|
||||
}
|
||||
|
||||
function handleRouteChange(pathname: string) {
|
||||
// TODO 这段逻辑并不好,未来router封装好后改掉
|
||||
// 判断在home下,并且路由更改的是自己的路由子组件则更新UI
|
||||
const routeList = pathname.split("/");
|
||||
if (
|
||||
routeList[1] === "home" &&
|
||||
sidebarConfigList.find((childConfig) => childConfig.route === pathname)
|
||||
) {
|
||||
console.log("find success");
|
||||
const routeSelectChild = sidebarConfigList.find(
|
||||
(childConfig) => childConfig.route === pathname
|
||||
);
|
||||
if (routeSelectChild) {
|
||||
setSelectedChild(routeSelectChild);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${styles.sidebarContainer}`}>
|
||||
{/* LangBot、ICON区域 */}
|
||||
<div className={`${styles.langbotIconContainer}`}>
|
||||
{/* icon */}
|
||||
<div className={`${styles.langbotIcon}`}>L</div>
|
||||
<div className={`${styles.langbotText}`}>Langbot</div>
|
||||
</div>
|
||||
{/* 菜单列表,后期可升级成配置驱动 */}
|
||||
<div>
|
||||
{sidebarConfigList.map((config) => {
|
||||
return (
|
||||
<div
|
||||
key={config.id}
|
||||
onClick={() => {
|
||||
console.log("click:", config.id);
|
||||
handleChildClick(config);
|
||||
}}
|
||||
>
|
||||
<SidebarChild
|
||||
isSelected={selectedChild.id === config.id}
|
||||
icon={config.icon}
|
||||
name={config.name}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import styles from "./HomeSidebar.module.css";
|
||||
|
||||
export interface ISidebarChildVO {
|
||||
id: string;
|
||||
icon: string;
|
||||
name: string;
|
||||
route: string;
|
||||
}
|
||||
|
||||
export class SidebarChildVO {
|
||||
id: string;
|
||||
icon: string;
|
||||
name: string;
|
||||
route: string;
|
||||
|
||||
constructor(props: ISidebarChildVO) {
|
||||
this.id = props.id;
|
||||
this.icon = props.icon;
|
||||
this.name = props.name;
|
||||
this.route = props.route;
|
||||
}
|
||||
}
|
||||
|
||||
export function SidebarChild({
|
||||
icon,
|
||||
name,
|
||||
isSelected
|
||||
}: {
|
||||
icon: string;
|
||||
name: string;
|
||||
isSelected: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={`${styles.sidebarChildContainer} ${isSelected ? styles.sidebarSelected : styles.sidebarUnselected}`}
|
||||
>
|
||||
<div className={`${styles.sidebarChildIcon}`} />
|
||||
<div>
|
||||
{icon}
|
||||
{name}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
28
web/src/app/home/components/home-sidebar/sidbarConfigList.ts
Normal file
28
web/src/app/home/components/home-sidebar/sidbarConfigList.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import {SidebarChildVO} from "@/app/home/components/home-sidebar/HomeSidebarChild";
|
||||
|
||||
export const sidebarConfigList = [
|
||||
new SidebarChildVO({
|
||||
id: "models",
|
||||
name: "模型配置",
|
||||
icon: "",
|
||||
route: "/home/models",
|
||||
}),
|
||||
new SidebarChildVO({
|
||||
id: "bots",
|
||||
name: "机器人",
|
||||
icon: "",
|
||||
route: "/home/bots",
|
||||
}),
|
||||
new SidebarChildVO({
|
||||
id: "pipelines",
|
||||
name: "流水线",
|
||||
icon: "",
|
||||
route: "/home/pipelines",
|
||||
}),
|
||||
new SidebarChildVO({
|
||||
id: "plugins",
|
||||
name: "插件管理",
|
||||
icon: "",
|
||||
route: "/home/plugins",
|
||||
}),
|
||||
]
|
||||
16
web/src/app/home/components/home-titlebar/HomeTitleBar.tsx
Normal file
16
web/src/app/home/components/home-titlebar/HomeTitleBar.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import styles from "./HomeTittleBar.module.css"
|
||||
|
||||
|
||||
export default function HomeTitleBar({
|
||||
title,
|
||||
}: {
|
||||
title: string
|
||||
}) {
|
||||
return (
|
||||
<div className={`${styles.titleBarContainer}`}>
|
||||
<div
|
||||
className={`${styles.titleText}`}
|
||||
>{title}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
.titleBarContainer {
|
||||
width: 100%;
|
||||
height: 70px;
|
||||
background-color: #FFF;
|
||||
font-size: 20px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.titleText {
|
||||
margin-left: 10px;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
18
web/src/app/home/layout.module.css
Normal file
18
web/src/app/home/layout.module.css
Normal file
@@ -0,0 +1,18 @@
|
||||
.homeLayoutContainer {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.main {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #FAFBFB;
|
||||
}
|
||||
|
||||
.mainContent {
|
||||
width: calc(100% - 40px);
|
||||
height: calc(100% - 110px);
|
||||
margin: 20px;
|
||||
}
|
||||
30
web/src/app/home/layout.tsx
Normal file
30
web/src/app/home/layout.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
"use client";
|
||||
|
||||
import "@ant-design/v5-patch-for-react-19";
|
||||
import styles from "./layout.module.css";
|
||||
import HomeSidebar from "@/app/home/components/home-sidebar/HomeSidebar";
|
||||
import HomeTitleBar from "@/app/home/components/home-titlebar/HomeTitleBar";
|
||||
import React, { useState } from "react";
|
||||
import { SidebarChildVO } from "@/app/home/components/home-sidebar/HomeSidebarChild";
|
||||
|
||||
export default function HomeLayout({
|
||||
children
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
const [title, setTitle] = useState<string>("");
|
||||
const onSelectedChange = (child: SidebarChildVO) => {
|
||||
setTitle(child.name);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`${styles.homeLayoutContainer}`}>
|
||||
<HomeSidebar onSelectedChangeAction={onSelectedChange} />
|
||||
<div className={`${styles.main}`}>
|
||||
<HomeTitleBar title={title} />
|
||||
{/* 主页面 */}
|
||||
<div className={`${styles.mainContent}`}>{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
733
web/src/app/home/mock-api/index.ts
Normal file
733
web/src/app/home/mock-api/index.ts
Normal file
@@ -0,0 +1,733 @@
|
||||
import {GetMetaDataResponse} from "@/app/infra/api/api-types/pipelines/GetMetaDataResponse";
|
||||
import {ApiResponse} from "@/app/infra/api/api-types";
|
||||
|
||||
export async function fetchPipelineMetaData(): Promise<ApiResponse<GetMetaDataResponse>> {
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"configs": [
|
||||
{
|
||||
"label": {
|
||||
"en_US": "Trigger",
|
||||
"zh_CN": "触发条件"
|
||||
},
|
||||
"name": "trigger",
|
||||
"stages": [
|
||||
{
|
||||
"config": [
|
||||
{
|
||||
"default": false,
|
||||
"description": {
|
||||
"en_US": "Whether to trigger when the message mentions the bot",
|
||||
"zh_CN": "是否在消息@机器人时触发"
|
||||
},
|
||||
"label": {
|
||||
"en_US": "At",
|
||||
"zh_CN": "@"
|
||||
},
|
||||
"name": "at",
|
||||
"required": true,
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"default": [],
|
||||
"description": {
|
||||
"en_US": "The prefix of the message",
|
||||
"zh_CN": "消息前缀"
|
||||
},
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"label": {
|
||||
"en_US": "Prefix",
|
||||
"zh_CN": "前缀"
|
||||
},
|
||||
"name": "prefix",
|
||||
"required": true,
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"default": [],
|
||||
"description": {
|
||||
"en_US": "The regexp of the message",
|
||||
"zh_CN": "消息正则表达式"
|
||||
},
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"label": {
|
||||
"en_US": "Regexp",
|
||||
"zh_CN": "正则表达式"
|
||||
},
|
||||
"name": "regexp",
|
||||
"required": true,
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"default": 0,
|
||||
"description": {
|
||||
"en_US": "The probability of the random response, range from 0.0 to 1.0",
|
||||
"zh_CN": "随机响应概率,范围为 0.0-1.0"
|
||||
},
|
||||
"label": {
|
||||
"en_US": "Random",
|
||||
"zh_CN": "随机"
|
||||
},
|
||||
"name": "random",
|
||||
"required": false,
|
||||
"type": "float"
|
||||
}
|
||||
],
|
||||
"description": {
|
||||
"en_US": "The group respond rule of the pipeline",
|
||||
"zh_CN": "群响应规则"
|
||||
},
|
||||
"label": {
|
||||
"en_US": "Group Respond Rule",
|
||||
"zh_CN": "群响应规则"
|
||||
},
|
||||
"name": "group-respond-rules"
|
||||
},
|
||||
{
|
||||
"config": [
|
||||
{
|
||||
"default": "blacklist",
|
||||
"description": {
|
||||
"en_US": "The mode of the access control",
|
||||
"zh_CN": "访问控制模式"
|
||||
},
|
||||
"label": {
|
||||
"en_US": "Mode",
|
||||
"zh_CN": "模式"
|
||||
},
|
||||
"name": "mode",
|
||||
"options": [
|
||||
{
|
||||
"label": {
|
||||
"en_US": "Blacklist",
|
||||
"zh_CN": "黑名单"
|
||||
},
|
||||
"name": "blacklist"
|
||||
},
|
||||
{
|
||||
"label": {
|
||||
"en_US": "Whitelist",
|
||||
"zh_CN": "白名单"
|
||||
},
|
||||
"name": "whitelist"
|
||||
}
|
||||
],
|
||||
"required": true,
|
||||
"type": "select"
|
||||
},
|
||||
{
|
||||
"default": [],
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"label": {
|
||||
"en_US": "Blacklist",
|
||||
"zh_CN": "黑名单"
|
||||
},
|
||||
"name": "blacklist",
|
||||
"required": true,
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"default": [],
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"label": {
|
||||
"en_US": "Whitelist",
|
||||
"zh_CN": "白名单"
|
||||
},
|
||||
"name": "whitelist",
|
||||
"required": true,
|
||||
"type": "array"
|
||||
}
|
||||
],
|
||||
"label": {
|
||||
"en_US": "Access Control",
|
||||
"zh_CN": "访问控制"
|
||||
},
|
||||
"name": "access-control"
|
||||
},
|
||||
{
|
||||
"config": [
|
||||
{
|
||||
"default": [],
|
||||
"description": {
|
||||
"en_US": "The prefix of the message",
|
||||
"zh_CN": "消息前缀"
|
||||
},
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"label": {
|
||||
"en_US": "Prefix",
|
||||
"zh_CN": "前缀"
|
||||
},
|
||||
"name": "prefix",
|
||||
"required": true,
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"default": [],
|
||||
"description": {
|
||||
"en_US": "The regexp of the message",
|
||||
"zh_CN": "消息正则表达式"
|
||||
},
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"label": {
|
||||
"en_US": "Regexp",
|
||||
"zh_CN": "正则表达式"
|
||||
},
|
||||
"name": "regexp",
|
||||
"required": true,
|
||||
"type": "array"
|
||||
}
|
||||
],
|
||||
"label": {
|
||||
"en_US": "Ignore Rules",
|
||||
"zh_CN": "消息忽略规则"
|
||||
},
|
||||
"name": "ignore-rules"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": {
|
||||
"en_US": "Safety Control",
|
||||
"zh_CN": "安全控制"
|
||||
},
|
||||
"name": "safety",
|
||||
"stages": [
|
||||
{
|
||||
"config": [
|
||||
{
|
||||
"default": "all",
|
||||
"label": {
|
||||
"en_US": "Scope",
|
||||
"zh_CN": "检查范围"
|
||||
},
|
||||
"name": "scope",
|
||||
"options": [
|
||||
{
|
||||
"label": {
|
||||
"en_US": "All",
|
||||
"zh_CN": "全部"
|
||||
},
|
||||
"name": "all"
|
||||
},
|
||||
{
|
||||
"label": {
|
||||
"en_US": "Income Message",
|
||||
"zh_CN": "传入消息(用户消息)"
|
||||
},
|
||||
"name": "income-msg"
|
||||
},
|
||||
{
|
||||
"label": {
|
||||
"en_US": "Output Message",
|
||||
"zh_CN": "传出消息(机器人消息)"
|
||||
},
|
||||
"name": "output-msg"
|
||||
}
|
||||
],
|
||||
"required": true,
|
||||
"type": "select"
|
||||
},
|
||||
{
|
||||
"default": false,
|
||||
"label": {
|
||||
"en_US": "Check Sensitive Words",
|
||||
"zh_CN": "检查敏感词"
|
||||
},
|
||||
"name": "check-sensitive-words",
|
||||
"required": true,
|
||||
"type": "boolean"
|
||||
}
|
||||
],
|
||||
"label": {
|
||||
"en_US": "Content Filter",
|
||||
"zh_CN": "内容过滤"
|
||||
},
|
||||
"name": "content-filter"
|
||||
},
|
||||
{
|
||||
"config": [
|
||||
{
|
||||
"default": 60,
|
||||
"label": {
|
||||
"en_US": "Window Length",
|
||||
"zh_CN": "窗口长度(秒)"
|
||||
},
|
||||
"name": "window-length",
|
||||
"required": true,
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"default": 60,
|
||||
"label": {
|
||||
"en_US": "Limitation",
|
||||
"zh_CN": "限制次数"
|
||||
},
|
||||
"name": "limitation",
|
||||
"required": true,
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"default": "drop",
|
||||
"label": {
|
||||
"en_US": "Strategy",
|
||||
"zh_CN": "策略"
|
||||
},
|
||||
"name": "strategy",
|
||||
"options": [
|
||||
{
|
||||
"label": {
|
||||
"en_US": "Drop",
|
||||
"zh_CN": "丢弃"
|
||||
},
|
||||
"name": "drop"
|
||||
},
|
||||
{
|
||||
"label": {
|
||||
"en_US": "Wait",
|
||||
"zh_CN": "等待"
|
||||
},
|
||||
"name": "wait"
|
||||
}
|
||||
],
|
||||
"required": true,
|
||||
"type": "select"
|
||||
}
|
||||
],
|
||||
"label": {
|
||||
"en_US": "Rate Limit",
|
||||
"zh_CN": "速率限制"
|
||||
},
|
||||
"name": "rate-limit"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": {
|
||||
"en_US": "AI Feature",
|
||||
"zh_CN": "AI 能力"
|
||||
},
|
||||
"name": "ai",
|
||||
"stages": [
|
||||
{
|
||||
"config": [
|
||||
{
|
||||
"default": "local-agent",
|
||||
"label": {
|
||||
"en_US": "Runner",
|
||||
"zh_CN": "运行器"
|
||||
},
|
||||
"name": "runner",
|
||||
"options": [
|
||||
{
|
||||
"label": {
|
||||
"en_US": "Embedded Agent",
|
||||
"zh_CN": "内置 Agent"
|
||||
},
|
||||
"name": "local-agent"
|
||||
},
|
||||
{
|
||||
"label": {
|
||||
"en_US": "Dify Service API",
|
||||
"zh_CN": "Dify 服务 API"
|
||||
},
|
||||
"name": "dify-service-api"
|
||||
},
|
||||
{
|
||||
"label": {
|
||||
"en_US": "Aliyun Dashscope App API",
|
||||
"zh_CN": "阿里云百炼平台 API"
|
||||
},
|
||||
"name": "dashscope-app-api"
|
||||
}
|
||||
],
|
||||
"required": true,
|
||||
"type": "select"
|
||||
}
|
||||
],
|
||||
"label": {
|
||||
"en_US": "Runner",
|
||||
"zh_CN": "运行方式"
|
||||
},
|
||||
"name": "runner"
|
||||
},
|
||||
{
|
||||
"config": [
|
||||
{
|
||||
"label": {
|
||||
"en_US": "Model",
|
||||
"zh_CN": "模型"
|
||||
},
|
||||
"name": "model",
|
||||
"required": true,
|
||||
"scope": "/provider/models/llm",
|
||||
"type": "select"
|
||||
},
|
||||
{
|
||||
"default": 10,
|
||||
"label": {
|
||||
"en_US": "Max Round",
|
||||
"zh_CN": "最大回合数"
|
||||
},
|
||||
"name": "max-round",
|
||||
"required": true,
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"items": {
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "string"
|
||||
},
|
||||
"role": {
|
||||
"default": "user",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"label": {
|
||||
"en_US": "Prompt",
|
||||
"zh_CN": "提示词"
|
||||
},
|
||||
"name": "prompt",
|
||||
"required": true,
|
||||
"type": "array"
|
||||
}
|
||||
],
|
||||
"description": {
|
||||
"en_US": "Configure the embedded agent of the pipeline",
|
||||
"zh_CN": "配置内置 Agent"
|
||||
},
|
||||
"label": {
|
||||
"en_US": "Embedded Agent",
|
||||
"zh_CN": "内置 Agent"
|
||||
},
|
||||
"name": "local-agent"
|
||||
},
|
||||
{
|
||||
"config": [
|
||||
{
|
||||
"label": {
|
||||
"en_US": "Base URL",
|
||||
"zh_CN": "基础 URL"
|
||||
},
|
||||
"name": "base-url",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"default": "chat",
|
||||
"label": {
|
||||
"en_US": "App Type",
|
||||
"zh_CN": "应用类型"
|
||||
},
|
||||
"name": "app-type",
|
||||
"options": [
|
||||
{
|
||||
"label": {
|
||||
"en_US": "Chat",
|
||||
"zh_CN": "聊天(包括Chatflow)"
|
||||
},
|
||||
"name": "chat"
|
||||
},
|
||||
{
|
||||
"label": {
|
||||
"en_US": "Agent",
|
||||
"zh_CN": "Agent"
|
||||
},
|
||||
"name": "agent"
|
||||
},
|
||||
{
|
||||
"label": {
|
||||
"en_US": "Workflow",
|
||||
"zh_CN": "工作流"
|
||||
},
|
||||
"name": "workflow"
|
||||
}
|
||||
],
|
||||
"required": true,
|
||||
"type": "select"
|
||||
},
|
||||
{
|
||||
"label": {
|
||||
"en_US": "API Key",
|
||||
"zh_CN": "API 密钥"
|
||||
},
|
||||
"name": "api-key",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"default": "plain",
|
||||
"label": {
|
||||
"en_US": "CoT Convert",
|
||||
"zh_CN": "思维链转换策略"
|
||||
},
|
||||
"name": "thinking-convert",
|
||||
"options": [
|
||||
{
|
||||
"label": {
|
||||
"en_US": "Convert to <think>...</think>",
|
||||
"zh_CN": "转换成 <think>...</think>"
|
||||
},
|
||||
"name": "plain"
|
||||
},
|
||||
{
|
||||
"label": {
|
||||
"en_US": "Original",
|
||||
"zh_CN": "原始"
|
||||
},
|
||||
"name": "original"
|
||||
},
|
||||
{
|
||||
"label": {
|
||||
"en_US": "Remove",
|
||||
"zh_CN": "移除"
|
||||
},
|
||||
"name": "remove"
|
||||
}
|
||||
],
|
||||
"required": true,
|
||||
"type": "select"
|
||||
}
|
||||
],
|
||||
"description": {
|
||||
"en_US": "Configure the Dify service API of the pipeline",
|
||||
"zh_CN": "配置 Dify 服务 API"
|
||||
},
|
||||
"label": {
|
||||
"en_US": "Dify Service API",
|
||||
"zh_CN": "Dify 服务 API"
|
||||
},
|
||||
"name": "dify-service-api"
|
||||
},
|
||||
{
|
||||
"config": [
|
||||
{
|
||||
"default": "agent",
|
||||
"label": {
|
||||
"en_US": "App Type",
|
||||
"zh_CN": "应用类型"
|
||||
},
|
||||
"name": "app-type",
|
||||
"options": [
|
||||
{
|
||||
"label": {
|
||||
"en_US": "Agent",
|
||||
"zh_CN": "Agent"
|
||||
},
|
||||
"name": "agent"
|
||||
},
|
||||
{
|
||||
"label": {
|
||||
"en_US": "Workflow",
|
||||
"zh_CN": "工作流"
|
||||
},
|
||||
"name": "workflow"
|
||||
}
|
||||
],
|
||||
"required": true,
|
||||
"type": "select"
|
||||
},
|
||||
{
|
||||
"label": {
|
||||
"en_US": "API Key",
|
||||
"zh_CN": "API 密钥"
|
||||
},
|
||||
"name": "api-key",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"label": {
|
||||
"en_US": "App ID",
|
||||
"zh_CN": "应用 ID"
|
||||
},
|
||||
"name": "app-id",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"default": "参考资料来自:",
|
||||
"label": {
|
||||
"en_US": "References Quote",
|
||||
"zh_CN": "引用文本"
|
||||
},
|
||||
"name": "references_quote",
|
||||
"required": false,
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"description": {
|
||||
"en_US": "Configure the Aliyun Dashscope App API of the pipeline",
|
||||
"zh_CN": "配置阿里云百炼平台 API"
|
||||
},
|
||||
"label": {
|
||||
"en_US": "Aliyun Dashscope App API",
|
||||
"zh_CN": "阿里云百炼平台 API"
|
||||
},
|
||||
"name": "dashscope-app-api"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": {
|
||||
"en_US": "Output Processing",
|
||||
"zh_CN": "输出处理"
|
||||
},
|
||||
"name": "output",
|
||||
"stages": [
|
||||
{
|
||||
"config": [
|
||||
{
|
||||
"default": 1000,
|
||||
"label": {
|
||||
"en_US": "Threshold",
|
||||
"zh_CN": "阈值"
|
||||
},
|
||||
"name": "threshold",
|
||||
"required": true,
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"default": "forward",
|
||||
"label": {
|
||||
"en_US": "Strategy",
|
||||
"zh_CN": "策略"
|
||||
},
|
||||
"name": "strategy",
|
||||
"options": [
|
||||
{
|
||||
"label": {
|
||||
"en_US": "Forward Message Component",
|
||||
"zh_CN": "转发消息组件"
|
||||
},
|
||||
"name": "forward"
|
||||
},
|
||||
{
|
||||
"label": {
|
||||
"en_US": "Convert to Image",
|
||||
"zh_CN": "转换为图片"
|
||||
},
|
||||
"name": "image"
|
||||
}
|
||||
],
|
||||
"required": true,
|
||||
"type": "select"
|
||||
},
|
||||
{
|
||||
"default": "",
|
||||
"label": {
|
||||
"en_US": "Font Path",
|
||||
"zh_CN": "字体路径"
|
||||
},
|
||||
"name": "font-path",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"label": {
|
||||
"en_US": "Long Text Processing",
|
||||
"zh_CN": "长文本处理"
|
||||
},
|
||||
"name": "long-text-processing"
|
||||
},
|
||||
{
|
||||
"config": [
|
||||
{
|
||||
"default": 0,
|
||||
"label": {
|
||||
"en_US": "Min",
|
||||
"zh_CN": "最小秒数"
|
||||
},
|
||||
"name": "min",
|
||||
"required": true,
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"default": 0,
|
||||
"label": {
|
||||
"en_US": "Max",
|
||||
"zh_CN": "最大秒数"
|
||||
},
|
||||
"name": "max",
|
||||
"required": true,
|
||||
"type": "integer"
|
||||
}
|
||||
],
|
||||
"label": {
|
||||
"en_US": "Force Delay",
|
||||
"zh_CN": "强制延迟"
|
||||
},
|
||||
"name": "force-delay"
|
||||
},
|
||||
{
|
||||
"config": [
|
||||
{
|
||||
"default": true,
|
||||
"label": {
|
||||
"en_US": "Hide Exception",
|
||||
"zh_CN": "不输出异常信息给用户"
|
||||
},
|
||||
"name": "hide-exception",
|
||||
"required": true,
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"default": true,
|
||||
"label": {
|
||||
"en_US": "At Sender",
|
||||
"zh_CN": "在回复中@发送者"
|
||||
},
|
||||
"name": "at-sender",
|
||||
"required": true,
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"default": false,
|
||||
"label": {
|
||||
"en_US": "Quote Origin",
|
||||
"zh_CN": "引用原文"
|
||||
},
|
||||
"name": "quote-origin",
|
||||
"required": true,
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"default": true,
|
||||
"label": {
|
||||
"en_US": "Track Function Calls",
|
||||
"zh_CN": "跟踪函数调用"
|
||||
},
|
||||
"name": "track-function-calls",
|
||||
"required": true,
|
||||
"type": "boolean"
|
||||
}
|
||||
],
|
||||
"label": {
|
||||
"en_US": "Misc",
|
||||
"zh_CN": "杂项"
|
||||
},
|
||||
"name": "misc"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"msg": "ok"
|
||||
}
|
||||
}
|
||||
|
||||
8
web/src/app/home/models/ICreateLLMField.ts
Normal file
8
web/src/app/home/models/ICreateLLMField.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export interface ICreateLLMField {
|
||||
name: string;
|
||||
model_provider: string;
|
||||
url: string;
|
||||
api_key: string;
|
||||
abilities: string[];
|
||||
extra_args: string[];
|
||||
}
|
||||
90
web/src/app/home/models/LLMConfig.module.css
Normal file
90
web/src/app/home/models/LLMConfig.module.css
Normal file
@@ -0,0 +1,90 @@
|
||||
.configPageContainer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.modalContainer {
|
||||
width: 100%;
|
||||
/*height: calc(100vh - 200px);*/
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.modelListContainer {
|
||||
align-self: flex-start;
|
||||
justify-self: flex-start;
|
||||
width: calc(100% - 60px);
|
||||
margin: auto;
|
||||
display: grid;
|
||||
grid-template-rows: repeat(auto-fill, minmax(220px, 1fr));
|
||||
grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
|
||||
gap: 15px;
|
||||
justify-items: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.emptyContainer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.cardContainer {
|
||||
width: 360px;
|
||||
height: 200px;
|
||||
background-color: #FFF;
|
||||
border-radius: 9px;
|
||||
box-shadow: rgba(0, 0, 0, 0.4) 0 1px 1px -1px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
.iconBasicInfoContainer {
|
||||
width: 300px;
|
||||
height: 100px;
|
||||
margin-left: 20px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
border-radius: 5px;
|
||||
font-size: 40px;
|
||||
line-height: 90px;
|
||||
text-align: center;
|
||||
color: #ffffff;
|
||||
background: rgba(96, 149, 209, 0.31);
|
||||
border: 1px solid rgba(96, 149, 209, 0.31);
|
||||
}
|
||||
|
||||
.basicInfoContainer {
|
||||
width: 200px;
|
||||
height: 90px;
|
||||
padding-left: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.basicInfoText {
|
||||
|
||||
}
|
||||
|
||||
.bigText {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.urlAndUpdateText {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
33
web/src/app/home/models/component/llm-card/LLMCard.tsx
Normal file
33
web/src/app/home/models/component/llm-card/LLMCard.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import styles from "../../LLMConfig.module.css"
|
||||
import {LLMCardVO} from "@/app/home/models/component/llm-card/LLMCardVO";
|
||||
|
||||
export default function LLMCard({
|
||||
cardVO
|
||||
}: {
|
||||
cardVO: LLMCardVO
|
||||
}) {
|
||||
return (
|
||||
<div className={`${styles.cardContainer}`}>
|
||||
{/* icon和基本信息 */}
|
||||
<div className={`${styles.iconBasicInfoContainer}`}>
|
||||
{/* icon */}
|
||||
<div className={`${styles.icon}`}>
|
||||
ICO
|
||||
</div>
|
||||
{/* bot基本信息 */}
|
||||
<div className={`${styles.basicInfoContainer}`}>
|
||||
<div className={`${styles.basicInfoText} ${styles.bigText}`}>
|
||||
{cardVO.name}
|
||||
</div>
|
||||
<div className={`${styles.basicInfoText}`}>
|
||||
厂商:{cardVO.company}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* URL和创建时间 */}
|
||||
<div className={`${styles.urlAndUpdateText}`}>
|
||||
URL:{cardVO.URL}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
21
web/src/app/home/models/component/llm-card/LLMCardVO.ts
Normal file
21
web/src/app/home/models/component/llm-card/LLMCardVO.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export interface ILLMCardVO {
|
||||
id: string;
|
||||
name: string;
|
||||
company: string;
|
||||
URL: string;
|
||||
}
|
||||
|
||||
export class LLMCardVO implements ILLMCardVO {
|
||||
id: string;
|
||||
name: string;
|
||||
company: string;
|
||||
URL: string;
|
||||
|
||||
constructor(props: ILLMCardVO) {
|
||||
this.id = props.id;
|
||||
this.name = props.name;
|
||||
this.company = props.company;
|
||||
this.URL = props.URL;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface IChooseRequesterEntity {
|
||||
label: string
|
||||
value: string
|
||||
}
|
||||
287
web/src/app/home/models/component/llm-form/LLMForm.tsx
Normal file
287
web/src/app/home/models/component/llm-form/LLMForm.tsx
Normal file
@@ -0,0 +1,287 @@
|
||||
import styles from "@/app/home/models/LLMConfig.module.css";
|
||||
import { Button, Form, Input, Select, SelectProps, Space, Modal } from "antd";
|
||||
import { ICreateLLMField } from "@/app/home/models/ICreateLLMField";
|
||||
import { useEffect, useState } from "react";
|
||||
import { IChooseRequesterEntity } from "@/app/home/models/component/llm-form/ChooseAdapterEntity";
|
||||
import { httpClient } from "@/app/infra/http/HttpClient";
|
||||
import { LLMModel } from "@/app/infra/api/api-types";
|
||||
import { UUID } from "uuidjs";
|
||||
|
||||
export default function LLMForm({
|
||||
editMode,
|
||||
initLLMId,
|
||||
onFormSubmit,
|
||||
onFormCancel,
|
||||
onLLMDeleted
|
||||
}: {
|
||||
editMode: boolean;
|
||||
initLLMId?: string;
|
||||
onFormSubmit: (value: ICreateLLMField) => void;
|
||||
onFormCancel: (value: ICreateLLMField) => void;
|
||||
onLLMDeleted: () => void;
|
||||
}) {
|
||||
const [form] = Form.useForm<ICreateLLMField>();
|
||||
const extraOptions: SelectProps["options"] = [];
|
||||
const [initValue] = useState<ICreateLLMField>();
|
||||
const [showDeleteConfirmModal, setShowDeleteConfirmModal] = useState(false);
|
||||
const abilityOptions: SelectProps["options"] = [
|
||||
{
|
||||
label: "函数调用",
|
||||
value: "func_call"
|
||||
},
|
||||
{
|
||||
label: "图像识别",
|
||||
value: "vision"
|
||||
}
|
||||
];
|
||||
const [requesterNameList, setRequesterNameList] = useState<
|
||||
IChooseRequesterEntity[]
|
||||
>([]);
|
||||
|
||||
useEffect(() => {
|
||||
initLLMModelFormComponent();
|
||||
if (editMode && initLLMId) {
|
||||
getLLMConfig(initLLMId).then((val) => {
|
||||
form.setFieldsValue(val);
|
||||
});
|
||||
} else {
|
||||
form.resetFields();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
async function initLLMModelFormComponent() {
|
||||
const requesterNameList = await httpClient.getProviderRequesters();
|
||||
setRequesterNameList(
|
||||
requesterNameList.requesters.map((item) => {
|
||||
return {
|
||||
label: item.label.zh_CN,
|
||||
value: item.name
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async function getLLMConfig(id: string): Promise<ICreateLLMField> {
|
||||
const llmModel = await httpClient.getProviderLLMModel(id);
|
||||
|
||||
const fakeExtraArgs = [];
|
||||
const extraArgs = llmModel.model.extra_args as Record<string, string>;
|
||||
for (const key in extraArgs) {
|
||||
fakeExtraArgs.push(`${key}:${extraArgs[key]}`);
|
||||
}
|
||||
return {
|
||||
name: llmModel.model.name,
|
||||
model_provider: llmModel.model.requester,
|
||||
url: llmModel.model.requester_config?.base_url,
|
||||
api_key: llmModel.model.api_keys[0],
|
||||
abilities: llmModel.model.abilities,
|
||||
extra_args: fakeExtraArgs
|
||||
};
|
||||
}
|
||||
|
||||
function handleFormSubmit(value: ICreateLLMField) {
|
||||
if (editMode) {
|
||||
// 暂不支持更改模型
|
||||
// onSaveEdit(value)
|
||||
} else {
|
||||
onCreateLLM(value);
|
||||
}
|
||||
form.resetFields();
|
||||
}
|
||||
|
||||
// function onSaveEdit(value: ICreateLLMField) {
|
||||
// const requestParam: LLMModel = {
|
||||
// uuid: UUID.generate(),
|
||||
// name: value.name,
|
||||
// description: "",
|
||||
// requester: value.model_provider,
|
||||
// requester_config: {
|
||||
// "base_url": value.url,
|
||||
// "timeout": 120
|
||||
// },
|
||||
// extra_args: value.extra_args,
|
||||
// api_keys: [value.api_key],
|
||||
// abilities: value.abilities,
|
||||
// // created_at: 'Sun Apr 27 2025 21:56:35 GMT+0800',
|
||||
// // updated_at: 'Sun Apr 27 2025 21:56:35 GMT+0800',
|
||||
// };
|
||||
// httpClient.createProviderLLMModel(requestParam).then(r => console.log(r))
|
||||
// }
|
||||
|
||||
function onCreateLLM(value: ICreateLLMField) {
|
||||
console.log("create llm", value);
|
||||
const requestParam: LLMModel = {
|
||||
uuid: UUID.generate(),
|
||||
name: value.name,
|
||||
description: "",
|
||||
requester: value.model_provider,
|
||||
requester_config: {
|
||||
base_url: value.url,
|
||||
timeout: 120
|
||||
},
|
||||
extra_args: value.extra_args,
|
||||
api_keys: [value.api_key],
|
||||
abilities: value.abilities
|
||||
// created_at: 'Sun Apr 27 2025 21:56:35 GMT+0800',
|
||||
// updated_at: 'Sun Apr 27 2025 21:56:35 GMT+0800',
|
||||
};
|
||||
httpClient.createProviderLLMModel(requestParam).then(() => {
|
||||
onFormSubmit(value);
|
||||
});
|
||||
}
|
||||
|
||||
function handleAbilitiesChange() {}
|
||||
|
||||
function deleteModel() {
|
||||
if (initLLMId) {
|
||||
httpClient.deleteProviderLLMModel(initLLMId).then(() => {
|
||||
onLLMDeleted();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.modalContainer}>
|
||||
<Modal
|
||||
open={showDeleteConfirmModal}
|
||||
title={"删除确认"}
|
||||
onCancel={() => setShowDeleteConfirmModal(false)}
|
||||
footer={
|
||||
<div
|
||||
style={{
|
||||
width: "170px",
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between"
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
danger
|
||||
onClick={() => {
|
||||
deleteModel();
|
||||
setShowDeleteConfirmModal(false);
|
||||
}}
|
||||
>
|
||||
确认删除
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShowDeleteConfirmModal(false);
|
||||
}}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
你确定要删除这个模型吗?
|
||||
</Modal>
|
||||
<Form
|
||||
form={form}
|
||||
labelCol={{ span: 4 }}
|
||||
wrapperCol={{ span: 14 }}
|
||||
layout="horizontal"
|
||||
initialValues={{
|
||||
...initValue
|
||||
}}
|
||||
onFinish={handleFormSubmit}
|
||||
clearOnDestroy={true}
|
||||
disabled={editMode}
|
||||
>
|
||||
<Form.Item<ICreateLLMField>
|
||||
label={"模型名称"}
|
||||
name={"name"}
|
||||
rules={[{ required: true, message: "该项为必填项哦~" }]}
|
||||
>
|
||||
<Input
|
||||
placeholder={"为自己的大模型取个好听的名字~"}
|
||||
style={{ width: 260 }}
|
||||
></Input>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<ICreateLLMField>
|
||||
label={"模型供应商"}
|
||||
name={"model_provider"}
|
||||
rules={[{ required: true, message: "该项为必填项哦~" }]}
|
||||
>
|
||||
<Select
|
||||
style={{ width: 120 }}
|
||||
onChange={() => {}}
|
||||
options={requesterNameList}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<ICreateLLMField>
|
||||
label={"请求URL"}
|
||||
name={"url"}
|
||||
rules={[{ required: true, message: "该项为必填项哦~" }]}
|
||||
>
|
||||
<Input
|
||||
placeholder="请求地址,一般是API提供商提供的URL"
|
||||
style={{ width: 500 }}
|
||||
></Input>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<ICreateLLMField>
|
||||
label={"API Key"}
|
||||
name={"api_key"}
|
||||
rules={[{ required: true, message: "该项为必填项哦~" }]}
|
||||
>
|
||||
<Input placeholder="你的API Key" style={{ width: 500 }}></Input>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<ICreateLLMField> label={"开启能力"} name={"abilities"}>
|
||||
<Select
|
||||
mode="tags"
|
||||
style={{ width: 500 }}
|
||||
placeholder="选择模型能力,输入回车可自定义能力"
|
||||
onChange={handleAbilitiesChange}
|
||||
options={abilityOptions}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<ICreateLLMField> label={"其他参数"} name={"extra_args"}>
|
||||
<Select
|
||||
mode="tags"
|
||||
style={{ width: 500 }}
|
||||
placeholder="输入后回车可自定义其他参数,例 key:value"
|
||||
onChange={handleAbilitiesChange}
|
||||
options={extraOptions}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item wrapperCol={{ offset: 4, span: 14 }}>
|
||||
<Space>
|
||||
{!editMode && (
|
||||
<Button type="primary" htmlType="submit">
|
||||
提交
|
||||
</Button>
|
||||
)}
|
||||
{editMode && (
|
||||
<Button
|
||||
color="danger"
|
||||
variant="solid"
|
||||
onClick={() => {
|
||||
setShowDeleteConfirmModal(true);
|
||||
}}
|
||||
disabled={false}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
htmlType="button"
|
||||
onClick={() => {
|
||||
onFormCancel(form.getFieldsValue());
|
||||
}}
|
||||
disabled={false}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
116
web/src/app/home/models/page.tsx
Normal file
116
web/src/app/home/models/page.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
"use client"
|
||||
|
||||
import {useState, useEffect} from "react";
|
||||
import {LLMCardVO} from "@/app/home/models/component/llm-card/LLMCardVO";
|
||||
import styles from "./LLMConfig.module.css"
|
||||
import EmptyAndCreateComponent from "@/app/home/components/empty-and-create-component/EmptyAndCreateComponent";
|
||||
import {Modal} from "antd";
|
||||
import LLMCard from "@/app/home/models/component/llm-card/LLMCard";
|
||||
import LLMForm from "@/app/home/models/component/llm-form/LLMForm";
|
||||
import CreateCardComponent from "@/app/infra/basic-component/create-card-component/CreateCardComponent";
|
||||
import { httpClient } from "@/app/infra/http/HttpClient";
|
||||
import { LLMModel } from "@/app/infra/api/api-types";
|
||||
|
||||
export default function LLMConfigPage() {
|
||||
const [cardList, setCardList] = useState<LLMCardVO[]>([])
|
||||
const [modalOpen, setModalOpen] = useState<boolean>(false);
|
||||
const [isEditForm, setIsEditForm] = useState(false)
|
||||
const [nowSelectedLLM, setNowSelectedLLM] = useState<LLMCardVO | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
getLLMModelList()
|
||||
}, [])
|
||||
|
||||
function getLLMModelList() {
|
||||
httpClient.getProviderLLMModels().then((resp) => {
|
||||
const llmModelList: LLMCardVO[] = resp.models.map((model: LLMModel) => {
|
||||
console.log("model", model)
|
||||
return new LLMCardVO({
|
||||
id: model.uuid,
|
||||
name: model.name,
|
||||
company: model.requester,
|
||||
URL: model.requester_config?.base_url,
|
||||
})
|
||||
})
|
||||
console.log("get llmModelList", llmModelList)
|
||||
setCardList(llmModelList)
|
||||
}).catch((err) => {
|
||||
// TODO error toast
|
||||
console.error("get LLM model list error", err)
|
||||
})
|
||||
}
|
||||
|
||||
function selectLLM(cardVO: LLMCardVO) {
|
||||
setIsEditForm(true)
|
||||
setNowSelectedLLM(cardVO)
|
||||
console.log("set now vo", cardVO)
|
||||
setModalOpen(true)
|
||||
}
|
||||
function handleCreateModelClick() {
|
||||
setIsEditForm(false)
|
||||
setNowSelectedLLM(null)
|
||||
setModalOpen(true);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.configPageContainer}>
|
||||
<Modal
|
||||
title={isEditForm ? "预览模型" : "创建模型"}
|
||||
centered
|
||||
open={modalOpen}
|
||||
destroyOnClose={true}
|
||||
onOk={() => setModalOpen(false)}
|
||||
onCancel={() => setModalOpen(false)}
|
||||
width={700}
|
||||
footer={null}
|
||||
>
|
||||
<LLMForm
|
||||
editMode={isEditForm}
|
||||
initLLMId={nowSelectedLLM?.id}
|
||||
onFormSubmit={() => {
|
||||
setModalOpen(false);
|
||||
getLLMModelList()
|
||||
}}
|
||||
onFormCancel={() => {
|
||||
setModalOpen(false);
|
||||
}}
|
||||
onLLMDeleted={() => {
|
||||
setModalOpen(false)
|
||||
getLLMModelList()
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
{
|
||||
cardList.length > 0 &&
|
||||
<div className={`${styles.modelListContainer}`}
|
||||
>
|
||||
{cardList.map(cardVO => {
|
||||
return <div key={cardVO.id} onClick={() => {selectLLM(cardVO)}}>
|
||||
<LLMCard cardVO={cardVO}></LLMCard>
|
||||
</div>
|
||||
})}
|
||||
<CreateCardComponent
|
||||
width={360}
|
||||
height={200}
|
||||
plusSize={90}
|
||||
onClick={handleCreateModelClick}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
cardList.length === 0 &&
|
||||
<div className={`${styles.emptyContainer}`}>
|
||||
<EmptyAndCreateComponent
|
||||
title={"模型列表空空如也~"}
|
||||
subTitle={"快去创建一个吧!"}
|
||||
buttonText={"创建模型 +"}
|
||||
onButtonClick={() => {
|
||||
handleCreateModelClick()
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
6
web/src/app/home/page.tsx
Normal file
6
web/src/app/home/page.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className={``}>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import styles from "./pipelineCard.module.css";
|
||||
import {PipelineCardVO} from "@/app/home/pipelines/components/pipeline-card/PipelineCardVO";
|
||||
|
||||
export default function PipelineCardComponent({
|
||||
cardVO
|
||||
}: {
|
||||
cardVO: PipelineCardVO
|
||||
}) {
|
||||
return (
|
||||
<div className={`${styles.cardContainer}`}>
|
||||
{/* icon和基本信息 */}
|
||||
<div className={`${styles.iconBasicInfoContainer}`}>
|
||||
{/* icon */}
|
||||
<div className={`${styles.icon}`}>
|
||||
ICO
|
||||
</div>
|
||||
{/* 基本信息 */}
|
||||
<div className={`${styles.basicInfoContainer}`}>
|
||||
<div className={`${styles.basicInfoText} ${styles.bigText}`}>
|
||||
{cardVO.name}
|
||||
</div>
|
||||
<div className={`${styles.basicInfoText}`}>
|
||||
描述:{cardVO.description}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* URL和创建时间 */}
|
||||
<div className={`${styles.urlAndUpdateText}`}>
|
||||
版本:{cardVO.version}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
export interface IPipelineCardVO {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
createTime: string;
|
||||
version: string;
|
||||
}
|
||||
|
||||
export class PipelineCardVO implements IPipelineCardVO {
|
||||
createTime: string;
|
||||
description: string;
|
||||
id: string;
|
||||
name: string;
|
||||
version: string;
|
||||
|
||||
constructor(props: IPipelineCardVO) {
|
||||
this.id = props.id;
|
||||
this.name = props.name;
|
||||
this.description = props.description;
|
||||
this.createTime = props.createTime;
|
||||
this.version = props.version;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
.iconBasicInfoContainer {
|
||||
width: 300px;
|
||||
height: 100px;
|
||||
margin-left: 20px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.cardContainer {
|
||||
width: 360px;
|
||||
height: 200px;
|
||||
background-color: #FFF;
|
||||
border-radius: 9px;
|
||||
box-shadow: rgba(0, 0, 0, 0.4) 0 1px 1px -1px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
border-radius: 5px;
|
||||
font-size: 40px;
|
||||
line-height: 90px;
|
||||
text-align: center;
|
||||
color: #ffffff;
|
||||
background: rgba(96, 149, 209, 0.31);
|
||||
border: 1px solid rgba(96, 149, 209, 0.31);
|
||||
}
|
||||
|
||||
.basicInfoContainer {
|
||||
width: 200px;
|
||||
height: 90px;
|
||||
padding-left: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.basicInfoText {
|
||||
|
||||
}
|
||||
|
||||
.bigText {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.urlAndUpdateText {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.createCardContainer {
|
||||
font-size: 90px;
|
||||
background: #6062E7;
|
||||
color: white;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { DynamicFormItemConfig } from "@/app/home/components/dynamic-form/DynamicFormItemConfig";
|
||||
|
||||
export interface IPipelineChildFormEntity {
|
||||
name: string;
|
||||
label: string;
|
||||
formItems: DynamicFormItemConfig[];
|
||||
}
|
||||
|
||||
export class PipelineChildFormEntity implements IPipelineChildFormEntity {
|
||||
formItems: DynamicFormItemConfig[];
|
||||
label: string;
|
||||
name: string;
|
||||
|
||||
constructor(props: IPipelineChildFormEntity) {
|
||||
this.label = props.label;
|
||||
this.name = props.name;
|
||||
this.formItems = props.formItems;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,604 @@
|
||||
import {
|
||||
Form,
|
||||
Button,
|
||||
Switch,
|
||||
Select,
|
||||
Input,
|
||||
InputNumber,
|
||||
SelectProps
|
||||
} from "antd";
|
||||
import { CaretLeftOutlined, CaretRightOutlined } from "@ant-design/icons";
|
||||
import { useEffect, useState } from "react";
|
||||
import styles from "./pipelineFormStyle.module.css";
|
||||
import { httpClient } from "@/app/infra/http/HttpClient";
|
||||
import { LLMModel, Pipeline } from "@/app/infra/api/api-types";
|
||||
import { UUID } from "uuidjs";
|
||||
|
||||
export default function PipelineFormComponent({
|
||||
onFinish
|
||||
}: {
|
||||
onFinish: () => void;
|
||||
}) {
|
||||
const [nowFormIndex, setNowFormIndex] = useState<number>(0);
|
||||
const [nowAIRunner, setNowAIRunner] = useState("");
|
||||
const [llmModelList, setLlmModelList] = useState<SelectProps["options"]>([]);
|
||||
// 这里不好,可以改成enum等
|
||||
const formLabelList: FormLabel[] = [
|
||||
{ label: "基础", name: "basic" },
|
||||
{ label: "AI能力", name: "ai" },
|
||||
{ label: "触发条件", name: "trigger" },
|
||||
{ label: "安全能力", name: "safety" },
|
||||
{ label: "输出处理", name: "output" }
|
||||
];
|
||||
const [basicForm] = Form.useForm();
|
||||
const [aiForm] = Form.useForm();
|
||||
const [triggerForm] = Form.useForm();
|
||||
const [safetyForm] = Form.useForm();
|
||||
const [outputForm] = Form.useForm();
|
||||
|
||||
useEffect(() => {
|
||||
getLLMModelList();
|
||||
}, []);
|
||||
|
||||
function getLLMModelList() {
|
||||
httpClient
|
||||
.getProviderLLMModels()
|
||||
.then((resp) => {
|
||||
setLlmModelList(
|
||||
resp.models.map((model: LLMModel) => {
|
||||
return {
|
||||
value: model.uuid,
|
||||
label: model.name
|
||||
};
|
||||
})
|
||||
);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("get LLM model list error", err);
|
||||
});
|
||||
}
|
||||
|
||||
function getNowFormLabel() {
|
||||
return formLabelList[nowFormIndex];
|
||||
}
|
||||
|
||||
function getPreFormLabel(): undefined | FormLabel {
|
||||
if (nowFormIndex !== undefined && nowFormIndex > 0) {
|
||||
return formLabelList[nowFormIndex - 1];
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function getNextFormLabel(): undefined | FormLabel {
|
||||
if (nowFormIndex !== undefined && nowFormIndex < formLabelList.length - 1) {
|
||||
return formLabelList[nowFormIndex + 1];
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function addFormLabelIndex() {
|
||||
if (nowFormIndex < formLabelList.length - 1) {
|
||||
setNowFormIndex(nowFormIndex + 1);
|
||||
}
|
||||
}
|
||||
|
||||
function reduceFormLabelIndex() {
|
||||
if (nowFormIndex > 0) {
|
||||
setNowFormIndex(nowFormIndex - 1);
|
||||
}
|
||||
}
|
||||
|
||||
function handleCommit() {
|
||||
Promise.all([
|
||||
basicForm.validateFields(),
|
||||
aiForm.validateFields(),
|
||||
triggerForm.validateFields(),
|
||||
safetyForm.validateFields(),
|
||||
outputForm.validateFields()
|
||||
])
|
||||
.then(() => {
|
||||
const pipeline = assembleForm();
|
||||
httpClient.createPipeline(pipeline).then(() => onFinish());
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
});
|
||||
}
|
||||
|
||||
// TODO 类型混乱,需要优化
|
||||
function assembleForm(): Pipeline {
|
||||
console.log("basicForm:", basicForm.getFieldsValue());
|
||||
console.log("aiForm:", aiForm.getFieldsValue());
|
||||
console.log("triggerForm:", triggerForm.getFieldsValue());
|
||||
console.log("safetyForm:", safetyForm.getFieldsValue());
|
||||
console.log("outputForm:", outputForm.getFieldsValue());
|
||||
const config: object = {
|
||||
ai: aiForm.getFieldsValue(),
|
||||
trigger: triggerForm.getFieldsValue(),
|
||||
safety: safetyForm.getFieldsValue(),
|
||||
output: outputForm.getFieldsValue()
|
||||
};
|
||||
|
||||
return {
|
||||
config,
|
||||
created_at: "",
|
||||
description: basicForm.getFieldsValue().description,
|
||||
for_version: "",
|
||||
name: basicForm.getFieldsValue().name,
|
||||
stages: [],
|
||||
updated_at: "",
|
||||
uuid: UUID.generate()
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ maxHeight: "70vh", overflowY: "auto" }}>
|
||||
<h1>{getNowFormLabel().label}</h1>
|
||||
<Form
|
||||
layout={"vertical"}
|
||||
style={{
|
||||
display: getNowFormLabel().name === "basic" ? "block" : "none"
|
||||
}}
|
||||
form={basicForm}
|
||||
>
|
||||
<Form.Item
|
||||
label="流水线名称"
|
||||
name={"name"}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="流水线描述"
|
||||
name={"description"}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
{/* AI能力表单 ai */}
|
||||
<Form
|
||||
layout={"vertical"}
|
||||
style={{ display: getNowFormLabel().name === "ai" ? "block" : "none" }}
|
||||
form={aiForm}
|
||||
>
|
||||
{/* Runner 配置区块 */}
|
||||
<div className={`${styles.formItemSubtitle}`}>运行器</div>
|
||||
<Form.Item
|
||||
label="运行器"
|
||||
name={["runner", "runner"]}
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Select
|
||||
options={[
|
||||
{ label: "内置 Agent", value: "local-agent" },
|
||||
{ label: "Dify 服务 API", value: "dify-service-api" },
|
||||
{ label: "阿里云百炼平台 API", value: "dashscope-app-api" }
|
||||
]}
|
||||
onChange={(value) => setNowAIRunner(value)}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{/* 内置 Agent 配置区块 */}
|
||||
{nowAIRunner === "local-agent" && (
|
||||
<>
|
||||
<div className={`${styles.formItemSubtitle}`}>配置内置Agent</div>
|
||||
<Form.Item
|
||||
label="模型"
|
||||
name={["local-agent", "model"]}
|
||||
rules={[{ required: true }]}
|
||||
tooltip="从模型库中选择"
|
||||
>
|
||||
<Select
|
||||
options={llmModelList}
|
||||
placeholder="请选择语言模型"
|
||||
showSearch
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="最大回合数"
|
||||
name={["local-agent", "max-round"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber precision={0} />
|
||||
</Form.Item>
|
||||
{/* TODO 这里要做转换处理 */}
|
||||
<Form.Item
|
||||
label="提示词"
|
||||
name={["local-agent", "prompt"]}
|
||||
rules={[{ required: true }]}
|
||||
tooltip="按JSON格式输入"
|
||||
>
|
||||
<Input.TextArea
|
||||
rows={4}
|
||||
placeholder={`示例结构:{ "role": "user", "content": "你好" } `}
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
{/* Dify 服务 API 区块 */}
|
||||
{nowAIRunner === "dify-service-api" && (
|
||||
<>
|
||||
<div className={`${styles.formItemSubtitle}`}>配置Dify服务API</div>
|
||||
<Form.Item
|
||||
label="基础 URL"
|
||||
name={["dify-service-api", "base-url"]}
|
||||
rules={[
|
||||
{ required: true },
|
||||
{ type: "url", message: "请输入有效的URL地址" }
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="应用类型"
|
||||
name={["dify-service-api", "app-type"]}
|
||||
initialValue={"chat"}
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Select
|
||||
options={[
|
||||
{ label: "聊天(包括Chatflow)", value: "chat" },
|
||||
{ label: "Agent", value: "agent" },
|
||||
{ label: "工作流", value: "workflow" }
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="API 密钥"
|
||||
name={["dify-service-api", "api-key"]}
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Input.Password visibilityToggle={false} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="思维链转换"
|
||||
name={["dify-service-api", "thinking-convert"]}
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Select
|
||||
options={[
|
||||
{ label: "转换成 \<think\>...\<\/think\>", value: "plain" },
|
||||
{ label: "原始", value: "original" },
|
||||
{ label: "移除", value: "remove" }
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
{/* 阿里云百炼区块 */}
|
||||
{nowAIRunner === "dashscope-app-api" && (
|
||||
<>
|
||||
<div className={`${styles.formItemSubtitle}`}>
|
||||
配置阿里云百炼平台 API
|
||||
</div>
|
||||
<Form.Item
|
||||
label="应用类型"
|
||||
name={["dashscope-app-api", "app-type"]}
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Select
|
||||
options={[
|
||||
{ label: "Agent", value: "agent" },
|
||||
{ label: "工作流", value: "workflow" }
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="API 密钥"
|
||||
name={["dashscope-app-api", "api-key"]}
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Input.Password visibilityToggle={false} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="应用 ID"
|
||||
name={["dashscope-app-api", "app-id"]}
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="引用文本"
|
||||
name={["dashscope-app-api", "references_quote"]}
|
||||
initialValue={"参考资料来自:"}
|
||||
>
|
||||
<Input.TextArea rows={2} />
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
|
||||
{/* 触发条件表单 trigger */}
|
||||
<Form
|
||||
layout={"vertical"}
|
||||
style={{
|
||||
display: getNowFormLabel().name === "trigger" ? "block" : "none"
|
||||
}}
|
||||
form={triggerForm}
|
||||
>
|
||||
{/* 群响应规则块 */}
|
||||
<div className={`${styles.formItemSubtitle}`}> 群响应规则</div>
|
||||
<Form.Item
|
||||
label={"是否在消息@机器人时触发"}
|
||||
name={["group-respond-rules", "at"]}
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={"消息前缀"}
|
||||
name={["group-respond-rules", "prefix"]}
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Select
|
||||
options={[{ value: '"type": "string"', label: '"type": "string"' }]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={"正则表达式"}
|
||||
name={["group-respond-rules", "regexp"]}
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Select mode="tags" options={[]} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={"随机"}
|
||||
name={["group-respond-rules", "random"]}
|
||||
rules={[{ required: false }]}
|
||||
>
|
||||
<InputNumber max={1} min={0} step={0.05} />
|
||||
</Form.Item>
|
||||
<div className={`${styles.formItemSubtitle}`}> 访问控制 </div>
|
||||
<Form.Item
|
||||
label={"模式"}
|
||||
name={["access-control", "mode"]}
|
||||
rules={[{ required: true }]}
|
||||
tooltip={"访问控制模式"}
|
||||
>
|
||||
<Select
|
||||
options={[
|
||||
{ label: "黑名单", value: "blacklist" },
|
||||
{ label: "白名单", value: "Whitelist" }
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={"黑名单"}
|
||||
name={["access-control", "blacklist"]}
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Select mode={"tags"} options={[]} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={"白名单"}
|
||||
name={["access-control", "whitelist"]}
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Select mode={"tags"} options={[]} />
|
||||
</Form.Item>
|
||||
|
||||
<div className={`${styles.formItemSubtitle}`}> 消息忽略规则 </div>
|
||||
|
||||
<Form.Item
|
||||
label={"前缀"}
|
||||
name={["ignore-rules", "whitelist"]}
|
||||
rules={[{ required: true }]}
|
||||
tooltip={"消息前缀"}
|
||||
>
|
||||
<Select mode={"tags"} options={[]} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={"正则表达式"}
|
||||
name={["ignore-rules", "regexp"]}
|
||||
rules={[{ required: true }]}
|
||||
tooltip={"消息正则表达式"}
|
||||
>
|
||||
<Select mode={"tags"} options={[]} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
{/* 安全控制表单 safety */}
|
||||
<Form
|
||||
layout={"vertical"}
|
||||
style={{
|
||||
display: getNowFormLabel().name === "safety" ? "block" : "none"
|
||||
}}
|
||||
form={safetyForm}
|
||||
>
|
||||
{/* 内容过滤块 content-filter */}
|
||||
<div className={`${styles.formItemSubtitle}`}> 内容过滤 </div>
|
||||
<Form.Item
|
||||
label={"检查范围"}
|
||||
name={["content-filter", "scope"]}
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Select
|
||||
options={[
|
||||
{ label: "全部", value: "all" },
|
||||
{ label: "传入消息(用户消息)", value: "income-msg" },
|
||||
{ label: "传出消息(机器人消息)", value: "output-msg" }
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={"检查敏感词"}
|
||||
name={["content-filter", "check-sensitive-words"]}
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
|
||||
{/* 速率限制块 rate-limit */}
|
||||
<div className={`${styles.formItemSubtitle}`}> 速率限制 </div>
|
||||
<Form.Item
|
||||
label={"窗口长度(秒)"}
|
||||
name={["rate-limit", "window-length"]}
|
||||
rules={[{ required: true }]}
|
||||
initialValue={60}
|
||||
>
|
||||
<InputNumber></InputNumber>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={"限制次数"}
|
||||
name={["rate-limit", "limitation"]}
|
||||
rules={[{ required: true }]}
|
||||
initialValue={60}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={"策略"}
|
||||
name={["rate-limit", "strategy"]}
|
||||
rules={[{ required: true }]}
|
||||
initialValue={"drop"}
|
||||
>
|
||||
<Select
|
||||
options={[
|
||||
{ label: "丢弃", value: "drop" },
|
||||
{ label: "等待", value: "wait" }
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
{/* 输出处理控制表单 output */}
|
||||
<Form
|
||||
layout={"vertical"}
|
||||
style={{
|
||||
display: getNowFormLabel().name === "output" ? "block" : "none"
|
||||
}}
|
||||
form={outputForm}
|
||||
>
|
||||
{/* 长文本处理区块 */}
|
||||
<div className={`${styles.formItemSubtitle}`}> 长文本处理 </div>
|
||||
<Form.Item
|
||||
label="阈值"
|
||||
name={["long-text-processing", "threshold"]}
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="策略"
|
||||
name={["long-text-processing", "strategy"]}
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Select
|
||||
options={[
|
||||
{ label: "转发消息组件", value: "forward" },
|
||||
{ label: "转换为图片", value: "image" }
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="字体路径"
|
||||
name={["long-text-processing", "font-path"]}
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
{/* 强制延迟区块 */}
|
||||
<div className={`${styles.formItemSubtitle}`}> 强制延迟 </div>
|
||||
<Form.Item
|
||||
label="最小秒数"
|
||||
name={["force-delay", "min"]}
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="最大秒数"
|
||||
name={["force-delay", "max"]}
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
|
||||
{/* 杂项区块 */}
|
||||
<div className={`${styles.formItemSubtitle}`}> 杂项 </div>
|
||||
<Form.Item
|
||||
label="不输出异常信息给用户"
|
||||
name={["misc", "hide-exception"]}
|
||||
rules={[{ required: true }]}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="在回复中@发送者"
|
||||
name={["misc", "at-sender"]}
|
||||
rules={[{ required: true }]}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="引用原文"
|
||||
name={["misc", "quote-origin"]}
|
||||
rules={[{ required: true }]}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="跟踪函数调用"
|
||||
name={["misc", "track-function-calls"]}
|
||||
rules={[{ required: true }]}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
<div className={`${styles.changeFormButtonGroupContainer}`}>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<CaretLeftOutlined />}
|
||||
onClick={reduceFormLabelIndex}
|
||||
disabled={!getPreFormLabel()}
|
||||
>
|
||||
{getPreFormLabel()?.label || "暂无更多"}
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<CaretRightOutlined />}
|
||||
onClick={addFormLabelIndex}
|
||||
disabled={!getNextFormLabel()}
|
||||
iconPosition={"end"}
|
||||
>
|
||||
{getNextFormLabel()?.label || "暂无更多"}
|
||||
</Button>
|
||||
|
||||
<Button type="primary" onClick={handleCommit}>
|
||||
提交
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface FormLabel {
|
||||
label: string;
|
||||
name: string;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
.formItemSubtitle {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.changeFormButtonGroupContainer {
|
||||
width: 320px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
78
web/src/app/home/pipelines/page.tsx
Normal file
78
web/src/app/home/pipelines/page.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
"use client";
|
||||
import { Modal } from "antd";
|
||||
import { useState, useEffect } from "react";
|
||||
import CreateCardComponent from "@/app/infra/basic-component/create-card-component/CreateCardComponent";
|
||||
import PipelineFormComponent from "./components/pipeline-form/PipelineFormComponent";
|
||||
import { httpClient } from "@/app/infra/http/HttpClient";
|
||||
import { PipelineCardVO } from "@/app/home/pipelines/components/pipeline-card/PipelineCardVO";
|
||||
import PipelineCardComponent from "@/app/home/pipelines/components/pipeline-card/PipelineCardComponent";
|
||||
|
||||
export default function PluginConfigPage() {
|
||||
const [modalOpen, setModalOpen] = useState<boolean>(false);
|
||||
const [isEditForm] = useState(false);
|
||||
const [pipelineList, setPipelineList] = useState<PipelineCardVO[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
getPipelines();
|
||||
}, []);
|
||||
|
||||
function getPipelines() {
|
||||
httpClient
|
||||
.getPipelines()
|
||||
.then((value) => {
|
||||
const pipelineList = value.pipelines.map((pipeline) => {
|
||||
return new PipelineCardVO({
|
||||
createTime: pipeline.created_at,
|
||||
description: pipeline.description,
|
||||
id: pipeline.uuid,
|
||||
name: pipeline.name,
|
||||
version: pipeline.for_version
|
||||
});
|
||||
});
|
||||
setPipelineList(pipelineList);
|
||||
})
|
||||
.catch((error) => {
|
||||
// TODO toast
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={``}>
|
||||
<Modal
|
||||
title={isEditForm ? "编辑流水线" : "创建流水线"}
|
||||
centered
|
||||
open={modalOpen}
|
||||
onOk={() => setModalOpen(false)}
|
||||
onCancel={() => setModalOpen(false)}
|
||||
width={700}
|
||||
footer={null}
|
||||
>
|
||||
<PipelineFormComponent
|
||||
onFinish={() => {
|
||||
getPipelines();
|
||||
setModalOpen(false);
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
{pipelineList.length > 0 && (
|
||||
<div className={``}>
|
||||
{pipelineList.map((pipeline) => {
|
||||
return (
|
||||
<PipelineCardComponent key={pipeline.id} cardVO={pipeline} />
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
<CreateCardComponent
|
||||
width={360}
|
||||
height={200}
|
||||
plusSize={90}
|
||||
onClick={() => {
|
||||
setModalOpen(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
4
web/src/app/home/pipelines/pipelineConfig.module.css
Normal file
4
web/src/app/home/pipelines/pipelineConfig.module.css
Normal file
@@ -0,0 +1,4 @@
|
||||
.formItemSubTitle {
|
||||
font-size: 30px;
|
||||
font-weight: bold;
|
||||
}
|
||||
46
web/src/app/home/plugins/page.tsx
Normal file
46
web/src/app/home/plugins/page.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
"use client"
|
||||
import { Radio } from 'antd';
|
||||
import {useState} from "react";
|
||||
import PluginInstalledComponent from "@/app/home/plugins/plugin-installed/PluginInstalledComponent";
|
||||
import PluginMarketComponent from "@/app/home/plugins/plugin-market/PluginMarketComponent";
|
||||
import styles from './plugins.module.css'
|
||||
|
||||
export default function PluginConfigPage() {
|
||||
enum PageType {
|
||||
INSTALLED = "installed",
|
||||
MARKET = 'market'
|
||||
}
|
||||
|
||||
const [nowPageType, setNowPageType] = useState(PageType.INSTALLED)
|
||||
|
||||
return (
|
||||
<div className={`${styles.pageContainer}`}>
|
||||
<div>
|
||||
<Radio.Group
|
||||
block
|
||||
options={[
|
||||
{ label: '已安装', value: PageType.INSTALLED },
|
||||
{ label: '插件市场', value: PageType.MARKET },
|
||||
]}
|
||||
defaultValue={PageType.INSTALLED}
|
||||
value={nowPageType}
|
||||
optionType="button"
|
||||
buttonStyle="solid"
|
||||
onChange={(e) => {
|
||||
// 这里静态类型检测有问题
|
||||
setNowPageType(e.target.value)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className={`${styles.pageContainer}`}>
|
||||
{
|
||||
nowPageType === PageType.INSTALLED && <PluginInstalledComponent/>
|
||||
}
|
||||
{
|
||||
nowPageType === PageType.MARKET && <PluginMarketComponent/>
|
||||
}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
27
web/src/app/home/plugins/plugin-installed/PluginCardVO.ts
Normal file
27
web/src/app/home/plugins/plugin-installed/PluginCardVO.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
export interface IPluginCardVO {
|
||||
author: string,
|
||||
version: string,
|
||||
name: string,
|
||||
description: string,
|
||||
handlerCount: number,
|
||||
isInitialized: boolean,
|
||||
}
|
||||
|
||||
export class PluginCardVO implements IPluginCardVO {
|
||||
description: string;
|
||||
handlerCount: number;
|
||||
name: string;
|
||||
author: string;
|
||||
version: string;
|
||||
isInitialized: boolean;
|
||||
|
||||
constructor(prop: IPluginCardVO) {
|
||||
this.description = prop.description
|
||||
this.handlerCount = prop.handlerCount
|
||||
this.name = prop.name
|
||||
this.author = prop.author
|
||||
this.version = prop.version
|
||||
this.isInitialized = prop.isInitialized
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
"use client";
|
||||
|
||||
import CreateCardComponent from "@/app/infra/basic-component/create-card-component/CreateCardComponent";
|
||||
import { PluginCardVO } from "@/app/home/plugins/plugin-installed/PluginCardVO";
|
||||
import { useEffect, useState } from "react";
|
||||
import PluginCardComponent from "@/app/home/plugins/plugin-installed/plugin-card/PluginCardComponent";
|
||||
import styles from "@/app/home/plugins/plugins.module.css";
|
||||
import { Modal, Input } from "antd";
|
||||
import { GithubOutlined } from "@ant-design/icons";
|
||||
import { httpClient } from "@/app/infra/http/HttpClient";
|
||||
|
||||
export default function PluginInstalledComponent() {
|
||||
const [pluginList, setPluginList] = useState<PluginCardVO[]>([]);
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [githubURL, setGithubURL] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
initData();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
function initData() {
|
||||
getPluginList();
|
||||
}
|
||||
|
||||
function getPluginList() {
|
||||
httpClient.getPlugins().then((value) => {
|
||||
setPluginList(
|
||||
value.plugins.map((plugin) => {
|
||||
return new PluginCardVO({
|
||||
author: plugin.author,
|
||||
description: plugin.description.zh_CN,
|
||||
handlerCount: 0,
|
||||
name: plugin.name,
|
||||
version: plugin.version,
|
||||
isInitialized: plugin.status === "initialized"
|
||||
});
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function handleModalConfirm() {
|
||||
installPlugin(githubURL);
|
||||
setModalOpen(false);
|
||||
}
|
||||
|
||||
function installPlugin(url: string) {
|
||||
httpClient
|
||||
.installPluginFromGithub(url)
|
||||
.then(() => {
|
||||
// 安装后重新拉取
|
||||
getPluginList();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log("error when install plugin:", err);
|
||||
});
|
||||
}
|
||||
return (
|
||||
<div className={`${styles.pluginListContainer}`}>
|
||||
<Modal
|
||||
title={
|
||||
<div className={`${styles.modalTitle}`}>
|
||||
<GithubOutlined
|
||||
style={{
|
||||
fontSize: "30px",
|
||||
marginRight: "20px"
|
||||
}}
|
||||
type="setting"
|
||||
/>
|
||||
<span>从 GitHub 安装插件</span>
|
||||
</div>
|
||||
}
|
||||
centered
|
||||
open={modalOpen}
|
||||
onOk={() => handleModalConfirm()}
|
||||
onCancel={() => setModalOpen(false)}
|
||||
width={500}
|
||||
destroyOnClose={true}
|
||||
>
|
||||
<div className={`${styles.modalBody}`}>
|
||||
<div>目前仅支持从 GitHub 安装</div>
|
||||
<Input
|
||||
placeholder="请输入插件的Github链接"
|
||||
value={githubURL}
|
||||
onChange={(e) => setGithubURL(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
{pluginList.map((vo, index) => {
|
||||
return (
|
||||
<div key={index}>
|
||||
<PluginCardComponent cardVO={vo} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<CreateCardComponent
|
||||
width={360}
|
||||
height={140}
|
||||
plusSize={90}
|
||||
onClick={() => {
|
||||
setModalOpen(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import styles from "./pluginCard.module.css";
|
||||
import { PluginCardVO } from "@/app/home/plugins/plugin-installed/PluginCardVO";
|
||||
import { GithubOutlined, LinkOutlined, ToolOutlined } from "@ant-design/icons";
|
||||
import { Switch, Tag } from "antd";
|
||||
import { useState } from "react";
|
||||
import { httpClient } from "@/app/infra/http/HttpClient";
|
||||
|
||||
export default function PluginCardComponent({
|
||||
cardVO
|
||||
}: {
|
||||
cardVO: PluginCardVO;
|
||||
}) {
|
||||
const [initialized, setInitialized] = useState(cardVO.isInitialized);
|
||||
const [switchEnable, setSwitchEnable] = useState(true);
|
||||
|
||||
function handleEnable() {
|
||||
setSwitchEnable(false);
|
||||
httpClient
|
||||
.togglePlugin(cardVO.author, cardVO.name, !initialized)
|
||||
.then(() => {
|
||||
setInitialized(!initialized);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log("error: ", err);
|
||||
})
|
||||
.finally(() => {
|
||||
setSwitchEnable(true);
|
||||
});
|
||||
}
|
||||
return (
|
||||
<div className={`${styles.cardContainer}`}>
|
||||
{/* header */}
|
||||
<div className={`${styles.cardHeader}`}>
|
||||
{/* left author */}
|
||||
<div className={`${styles.fontGray}`}>{cardVO.author}</div>
|
||||
{/* right icon & version */}
|
||||
<div className={`${styles.iconVersionContainer}`}>
|
||||
<GithubOutlined style={{ fontSize: "26px" }} type="setting" />
|
||||
<Tag color="#108ee9">v{cardVO.version}</Tag>
|
||||
</div>
|
||||
</div>
|
||||
{/* content */}
|
||||
<div className={`${styles.cardContent}`}>
|
||||
<div className={`${styles.boldFont}`}>{cardVO.name}</div>
|
||||
<div className={`${styles.fontGray}`}>{cardVO.description}</div>
|
||||
</div>
|
||||
{/* footer */}
|
||||
<div className={`${styles.cardFooter}`}>
|
||||
<div className={`${styles.linkSettingContainer}`}>
|
||||
<div className={`${styles.link}`}>
|
||||
<LinkOutlined style={{ fontSize: "22px" }} />
|
||||
<span>1</span>
|
||||
</div>
|
||||
<ToolOutlined style={{ fontSize: "22px" }} />
|
||||
</div>
|
||||
|
||||
<Switch
|
||||
value={initialized}
|
||||
onClick={handleEnable}
|
||||
disabled={!switchEnable}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
.cardContainer {
|
||||
width: 360px;
|
||||
height: 140px;
|
||||
box-sizing: border-box;
|
||||
background-color: #FFF;
|
||||
border-radius: 9px;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
box-shadow: rgba(0, 0, 0, 0.4) 0 1px 1px -1px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
.cardHeader {
|
||||
width: 90%;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.iconVersionContainer {
|
||||
width: 90px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
.cardContent {
|
||||
width: 90%;
|
||||
|
||||
height: 70px;
|
||||
}
|
||||
|
||||
.cardFooter {
|
||||
width: 90%;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
|
||||
.fontGray {
|
||||
color: #6C6C6C;
|
||||
}
|
||||
|
||||
.boldFont {
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.linkSettingContainer {
|
||||
width: 80px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.link {
|
||||
width: 32px;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-self: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import styles from "@/app/home/plugins/plugins.module.css";
|
||||
import { PluginMarketCardVO } from "@/app/home/plugins/plugin-market/plugin-market-card/PluginMarketCardVO";
|
||||
import PluginMarketCardComponent from "@/app/home/plugins/plugin-market/plugin-market-card/PluginMarketCardComponent";
|
||||
import { Input, Pagination } from "antd";
|
||||
import { spaceClient } from "@/app/infra/http/HttpClient";
|
||||
|
||||
export default function PluginMarketComponent() {
|
||||
const [marketPluginList, setMarketPluginList] = useState<
|
||||
PluginMarketCardVO[]
|
||||
>([]);
|
||||
const [totalCount, setTotalCount] = useState(0);
|
||||
const [nowPage, setNowPage] = useState(1);
|
||||
const [searchKeyword, setSearchKeyword] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
initData();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
function initData() {
|
||||
getPluginList();
|
||||
}
|
||||
|
||||
function onInputSearchKeyword(keyword: string) {
|
||||
// 这里记得加防抖,暂时没加
|
||||
setSearchKeyword(keyword);
|
||||
setNowPage(1);
|
||||
getPluginList(1, keyword);
|
||||
}
|
||||
|
||||
function getPluginList(
|
||||
page: number = nowPage,
|
||||
keyword: string = searchKeyword
|
||||
) {
|
||||
spaceClient.getMarketPlugins(page, 10, keyword).then((res) => {
|
||||
setMarketPluginList(
|
||||
res.plugins.map(
|
||||
(marketPlugin) =>
|
||||
new PluginMarketCardVO({
|
||||
author: marketPlugin.author,
|
||||
description: marketPlugin.description,
|
||||
githubURL: marketPlugin.repository,
|
||||
name: marketPlugin.name,
|
||||
pluginId: String(marketPlugin.ID),
|
||||
starCount: marketPlugin.stars
|
||||
})
|
||||
)
|
||||
);
|
||||
setTotalCount(res.total);
|
||||
console.log("market plugins:", res);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${styles.marketComponentBody}`}>
|
||||
<Input
|
||||
style={{
|
||||
width: "300px",
|
||||
marginTop: "10px"
|
||||
}}
|
||||
value={searchKeyword}
|
||||
placeholder="搜索插件"
|
||||
onChange={(e) => onInputSearchKeyword(e.target.value)}
|
||||
/>
|
||||
<div className={`${styles.pluginListContainer}`}>
|
||||
{marketPluginList.map((vo, index) => {
|
||||
return (
|
||||
<div key={index}>
|
||||
<PluginMarketCardComponent cardVO={vo} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<Pagination
|
||||
defaultCurrent={1}
|
||||
total={totalCount}
|
||||
onChange={(pageNumber) => {
|
||||
setNowPage(pageNumber);
|
||||
getPluginList(pageNumber);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import styles from "./pluginMarketCard.module.css"
|
||||
import {GithubOutlined, StarOutlined} from '@ant-design/icons';
|
||||
import {PluginMarketCardVO} from "@/app/home/plugins/plugin-market/plugin-market-card/PluginMarketCardVO";
|
||||
import {Button} from "antd";
|
||||
|
||||
export default function PluginMarketCardComponent({
|
||||
cardVO
|
||||
}: {
|
||||
cardVO: PluginMarketCardVO
|
||||
}) {
|
||||
|
||||
|
||||
function handleInstallClick (pluginId: string) {
|
||||
console.log("Install plugin: ", pluginId)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${styles.cardContainer}`}>
|
||||
{/* header */}
|
||||
<div className={`${styles.cardHeader}`}>
|
||||
{/* left author */}
|
||||
<div className={`${styles.fontGray}`}>{cardVO.author}</div>
|
||||
{/* right icon */}
|
||||
<GithubOutlined
|
||||
style={{fontSize: '26px'}}
|
||||
type="setting"
|
||||
/>
|
||||
</div>
|
||||
{/* content */}
|
||||
<div className={`${styles.cardContent}`}>
|
||||
<div className={`${styles.boldFont}`}>{cardVO.name}</div>
|
||||
<div className={`${styles.fontGray}`}>{cardVO.description}</div>
|
||||
</div>
|
||||
{/* footer */}
|
||||
<div className={`${styles.cardFooter}`}>
|
||||
<div className={`${styles.linkSettingContainer}`}>
|
||||
<div className={`${styles.link}`}>
|
||||
<StarOutlined
|
||||
style={{fontSize: '22px'}}
|
||||
/>
|
||||
<span>{cardVO.starCount}</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type="primary"
|
||||
size={"small"}
|
||||
onClick={() => {
|
||||
handleInstallClick(cardVO.pluginId)
|
||||
}}
|
||||
>
|
||||
安装
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
export interface IPluginMarketCardVO {
|
||||
pluginId: string;
|
||||
author: string,
|
||||
name: string,
|
||||
description: string,
|
||||
starCount: number,
|
||||
githubURL: string,
|
||||
}
|
||||
|
||||
export class PluginMarketCardVO implements IPluginMarketCardVO {
|
||||
pluginId: string;
|
||||
description: string;
|
||||
name: string;
|
||||
author: string;
|
||||
githubURL: string;
|
||||
starCount: number;
|
||||
|
||||
constructor(prop: IPluginMarketCardVO) {
|
||||
this.description = prop.description
|
||||
this.name = prop.name
|
||||
this.author = prop.author
|
||||
this.githubURL = prop.githubURL
|
||||
this.starCount = prop.starCount
|
||||
this.pluginId = prop.pluginId
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
.cardContainer {
|
||||
width: 360px;
|
||||
height: 140px;
|
||||
box-sizing: border-box;
|
||||
background-color: #FFF;
|
||||
border-radius: 9px;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
box-shadow: rgba(0, 0, 0, 0.4) 0 1px 1px -1px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
.cardHeader {
|
||||
width: 90%;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.iconVersionContainer {
|
||||
width: 90px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
.cardContent {
|
||||
width: 90%;
|
||||
|
||||
height: 70px;
|
||||
}
|
||||
|
||||
.cardFooter {
|
||||
width: 90%;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
|
||||
.fontGray {
|
||||
color: #6C6C6C;
|
||||
}
|
||||
|
||||
.boldFont {
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.linkSettingContainer {
|
||||
width: 80px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.link {
|
||||
width: 32px;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
color: #6062E7;
|
||||
align-self: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
38
web/src/app/home/plugins/plugins.module.css
Normal file
38
web/src/app/home/plugins/plugins.module.css
Normal file
@@ -0,0 +1,38 @@
|
||||
.pageContainer {
|
||||
width: 100%;
|
||||
height: calc(100% - 30px);
|
||||
}
|
||||
|
||||
.marketComponentBody {
|
||||
width: 100%;
|
||||
height: calc(100% - 60px);
|
||||
}
|
||||
|
||||
.pluginListContainer {
|
||||
align-self: flex-start;
|
||||
justify-self: flex-start;
|
||||
width: calc(100% - 60px);
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
margin: auto;
|
||||
display: grid;
|
||||
grid-template-rows: repeat(auto-fill, minmax(160px, 1fr));
|
||||
grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
|
||||
gap: 15px;
|
||||
justify-items: center;
|
||||
align-items: center;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.modalTitle {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modalBody {
|
||||
height: 80px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
}
|
||||
217
web/src/app/infra/api/api-types/index.ts
Normal file
217
web/src/app/infra/api/api-types/index.ts
Normal file
@@ -0,0 +1,217 @@
|
||||
export interface ApiResponse<T> {
|
||||
code: number;
|
||||
data: T;
|
||||
msg: string;
|
||||
}
|
||||
|
||||
export interface I18nText {
|
||||
en_US: string;
|
||||
zh_CN: string;
|
||||
}
|
||||
|
||||
export interface AsyncTaskCreatedResp {
|
||||
task_id: number;
|
||||
}
|
||||
|
||||
export interface ApiRespProviderRequesters {
|
||||
requesters: Requester[];
|
||||
}
|
||||
|
||||
export interface ApiRespProviderRequester {
|
||||
requester: Requester;
|
||||
}
|
||||
|
||||
export interface Requester {
|
||||
name: string;
|
||||
label: I18nText;
|
||||
description: I18nText;
|
||||
icon?: string;
|
||||
spec: object;
|
||||
}
|
||||
|
||||
export interface ApiRespProviderLLMModels {
|
||||
models: LLMModel[];
|
||||
}
|
||||
|
||||
export interface ApiRespProviderLLMModel {
|
||||
model: LLMModel;
|
||||
}
|
||||
|
||||
export interface LLMModel {
|
||||
name: string;
|
||||
description: string;
|
||||
uuid: string;
|
||||
requester: string;
|
||||
requester_config: {
|
||||
base_url: string;
|
||||
timeout: number;
|
||||
};
|
||||
extra_args: object;
|
||||
api_keys: string[];
|
||||
abilities: string[];
|
||||
// created_at: string;
|
||||
// updated_at: string;
|
||||
}
|
||||
|
||||
export interface ApiRespPipelines {
|
||||
pipelines: Pipeline[];
|
||||
}
|
||||
|
||||
export interface ApiRespPipeline {
|
||||
pipeline: Pipeline;
|
||||
}
|
||||
|
||||
export interface Pipeline {
|
||||
uuid: string;
|
||||
name: string;
|
||||
description: string;
|
||||
for_version: string;
|
||||
config: object;
|
||||
stages: string[];
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface ApiRespPlatformAdapters {
|
||||
adapters: Adapter[];
|
||||
}
|
||||
|
||||
export interface ApiRespPlatformAdapter {
|
||||
adapter: Adapter;
|
||||
}
|
||||
|
||||
export interface Adapter {
|
||||
name: string;
|
||||
label: I18nText;
|
||||
description: I18nText;
|
||||
icon?: string;
|
||||
spec: {
|
||||
config: AdapterSpecConfig[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface AdapterSpecConfig {
|
||||
default: string | number | boolean | Array<unknown>;
|
||||
label: I18nText;
|
||||
name: string;
|
||||
required: boolean;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface ApiRespPlatformBots {
|
||||
bots: Bot[];
|
||||
}
|
||||
|
||||
export interface ApiRespPlatformBot {
|
||||
bot: Bot;
|
||||
}
|
||||
|
||||
export interface Bot {
|
||||
uuid?: string;
|
||||
name: string;
|
||||
description: string;
|
||||
enable?: boolean;
|
||||
adapter: string;
|
||||
adapter_config: object;
|
||||
use_pipeline_name?: string;
|
||||
use_pipeline_uuid?: string;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
// plugins
|
||||
export interface ApiRespPlugins {
|
||||
plugins: Plugin[];
|
||||
}
|
||||
|
||||
export interface ApiRespPlugin {
|
||||
plugin: Plugin;
|
||||
}
|
||||
|
||||
export interface Plugin {
|
||||
author: string;
|
||||
name: string;
|
||||
description: I18nText;
|
||||
label: I18nText;
|
||||
version: string;
|
||||
enabled: boolean;
|
||||
priority: number;
|
||||
status: string;
|
||||
tools: object[];
|
||||
event_handlers: object;
|
||||
main_file: string;
|
||||
pkg_path: string;
|
||||
repository: string;
|
||||
config_schema: object;
|
||||
}
|
||||
|
||||
export interface ApiRespPluginConfig {
|
||||
config: object;
|
||||
}
|
||||
|
||||
export interface PluginReorderElement {
|
||||
author: string;
|
||||
name: string;
|
||||
priority: number;
|
||||
}
|
||||
|
||||
// system
|
||||
export interface ApiRespSystemInfo {
|
||||
debug: boolean;
|
||||
version: string;
|
||||
}
|
||||
|
||||
export interface ApiRespAsyncTasks {
|
||||
tasks: AsyncTask[];
|
||||
}
|
||||
|
||||
export interface ApiRespAsyncTask {
|
||||
task: AsyncTask;
|
||||
}
|
||||
|
||||
export interface AsyncTaskRuntimeInfo {
|
||||
done: boolean;
|
||||
exception?: string;
|
||||
result?: object;
|
||||
state: string;
|
||||
}
|
||||
|
||||
export interface AsyncTaskTaskContext {
|
||||
current_action: string;
|
||||
log: string;
|
||||
}
|
||||
|
||||
export interface AsyncTask {
|
||||
id: number;
|
||||
kind: string;
|
||||
name: string;
|
||||
task_type: string; // system or user
|
||||
runtime: AsyncTaskRuntimeInfo;
|
||||
task_context: AsyncTaskTaskContext;
|
||||
}
|
||||
|
||||
export interface ApiRespUserToken {
|
||||
token: string;
|
||||
}
|
||||
|
||||
export interface MarketPlugin {
|
||||
ID: number;
|
||||
CreatedAt: string; // ISO 8601 格式日期
|
||||
UpdatedAt: string;
|
||||
DeletedAt: string | null;
|
||||
name: string;
|
||||
author: string;
|
||||
description: string;
|
||||
repository: string; // GitHub 仓库路径
|
||||
artifacts_path: string;
|
||||
stars: number;
|
||||
downloads: number;
|
||||
status: "initialized" | "mounted"; // 可根据实际状态值扩展联合类型
|
||||
synced_at: string;
|
||||
pushed_at: string; // 最后一次代码推送时间
|
||||
}
|
||||
|
||||
export interface MarketPluginResponse {
|
||||
plugins: MarketPlugin[];
|
||||
total: number;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
export interface GetMetaDataResponse {
|
||||
configs: Config[];
|
||||
}
|
||||
|
||||
interface Label {
|
||||
en_US: string;
|
||||
zh_CN: string;
|
||||
}
|
||||
|
||||
interface Option {
|
||||
label: Label;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface ConfigItem {
|
||||
default?: boolean | Array<unknown> | number | string;
|
||||
description?: Label;
|
||||
items?: {
|
||||
type?: string;
|
||||
properties?: {
|
||||
[key: string]: {
|
||||
type: string;
|
||||
default?: object | string;
|
||||
};
|
||||
};
|
||||
};
|
||||
label: Label;
|
||||
name: string;
|
||||
options?: Option[];
|
||||
required: boolean;
|
||||
scope?: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
interface Stage {
|
||||
config: ConfigItem[];
|
||||
description?: Label;
|
||||
label: Label;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface Config {
|
||||
label: Label;
|
||||
name: string;
|
||||
stages: Stage[];
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import styles from "./createCartComponent.module.css";
|
||||
|
||||
export default function CreateCardComponent({
|
||||
width,
|
||||
height,
|
||||
plusSize,
|
||||
onClick,
|
||||
}: {
|
||||
width: number;
|
||||
height: number;
|
||||
plusSize: number;
|
||||
onClick: () => void
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={`${styles.cardContainer} ${styles.createCardContainer} `}
|
||||
style={{
|
||||
width: `${width}px`,
|
||||
height: `${height}px`,
|
||||
fontSize: `${plusSize}px`
|
||||
}}
|
||||
onClick={onClick}
|
||||
>
|
||||
+
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
.cardContainer {
|
||||
width: 360px;
|
||||
height: 200px;
|
||||
background-color: #FFF;
|
||||
border-radius: 9px;
|
||||
box-shadow: rgba(0, 0, 0, 0.4) 0 1px 1px -1px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
.createCardContainer {
|
||||
font-size: 90px;
|
||||
color: #acacac;
|
||||
}
|
||||
4
web/src/app/infra/basic-types/I18N.ts
Normal file
4
web/src/app/infra/basic-types/I18N.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface I18NText {
|
||||
en_US: string;
|
||||
zh_CN: string;
|
||||
}
|
||||
418
web/src/app/infra/http/HttpClient.ts
Normal file
418
web/src/app/infra/http/HttpClient.ts
Normal file
@@ -0,0 +1,418 @@
|
||||
import axios, {
|
||||
AxiosInstance,
|
||||
AxiosRequestConfig,
|
||||
AxiosResponse,
|
||||
AxiosError
|
||||
} from "axios";
|
||||
import {
|
||||
ApiRespProviderRequesters,
|
||||
ApiRespProviderRequester,
|
||||
ApiRespProviderLLMModels,
|
||||
ApiRespProviderLLMModel,
|
||||
LLMModel,
|
||||
ApiRespPipelines,
|
||||
ApiRespPipeline,
|
||||
Pipeline,
|
||||
ApiRespPlatformAdapters,
|
||||
ApiRespPlatformAdapter,
|
||||
ApiRespPlatformBots,
|
||||
ApiRespPlatformBot,
|
||||
Bot,
|
||||
ApiRespPlugins,
|
||||
ApiRespPlugin,
|
||||
ApiRespPluginConfig,
|
||||
PluginReorderElement,
|
||||
AsyncTaskCreatedResp,
|
||||
ApiRespSystemInfo,
|
||||
ApiRespAsyncTasks,
|
||||
ApiRespAsyncTask,
|
||||
ApiRespUserToken, MarketPluginResponse
|
||||
} from "../api/api-types";
|
||||
import { notification } from "antd";
|
||||
|
||||
type JSONValue = string | number | boolean | JSONObject | JSONArray | null;
|
||||
interface JSONObject {
|
||||
[key: string]: JSONValue;
|
||||
}
|
||||
type JSONArray = Array<JSONValue>;
|
||||
|
||||
export interface ResponseData<T = unknown> {
|
||||
code: number;
|
||||
message: string;
|
||||
data: T;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export interface RequestConfig extends AxiosRequestConfig {
|
||||
isSSR?: boolean; // 服务端渲染标识
|
||||
retry?: number; // 重试次数
|
||||
}
|
||||
|
||||
class HttpClient {
|
||||
private instance: AxiosInstance;
|
||||
private disableToken: boolean = false
|
||||
// 暂不需要SSR
|
||||
// private ssrInstance: AxiosInstance | null = null
|
||||
|
||||
constructor(
|
||||
baseURL?: string,
|
||||
disableToken?: boolean
|
||||
) {
|
||||
this.instance = axios.create({
|
||||
baseURL: baseURL || this.getBaseUrl(),
|
||||
timeout: 15000,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
});
|
||||
this.disableToken = disableToken || false
|
||||
this.initInterceptors();
|
||||
}
|
||||
|
||||
// 兜底URL,如果使用未配置会走到这里
|
||||
private getBaseUrl(): string {
|
||||
return "http://localhost:5300";
|
||||
// NOT IMPLEMENT
|
||||
if (typeof window === "undefined") {
|
||||
// 服务端环境
|
||||
return "";
|
||||
}
|
||||
// 客户端环境
|
||||
return "";
|
||||
}
|
||||
|
||||
// 获取Session
|
||||
private async getSession() {
|
||||
// NOT IMPLEMENT
|
||||
return "";
|
||||
}
|
||||
|
||||
// 同步获取Session
|
||||
private getSessionSync() {
|
||||
// NOT IMPLEMENT
|
||||
return localStorage.getItem("token");
|
||||
}
|
||||
|
||||
// 拦截器配置
|
||||
private initInterceptors() {
|
||||
// 请求拦截
|
||||
this.instance.interceptors.request.use(
|
||||
async (config) => {
|
||||
// 服务端请求自动携带 cookie, Langbot暂时用不到SSR相关
|
||||
// if (typeof window === 'undefined' && config.isSSR) { }
|
||||
// cookie not required
|
||||
// const { cookies } = await import('next/headers')
|
||||
// config.headers.Cookie = cookies().toString()
|
||||
|
||||
// 客户端添加认证头
|
||||
if (typeof window !== "undefined" && !this.disableToken) {
|
||||
const session = this.getSessionSync();
|
||||
config.headers.Authorization = `Bearer ${session}`;
|
||||
}
|
||||
|
||||
return config;
|
||||
},
|
||||
(error) => Promise.reject(error)
|
||||
);
|
||||
|
||||
// 响应拦截
|
||||
this.instance.interceptors.response.use(
|
||||
(response: AxiosResponse<ResponseData>) => {
|
||||
// 响应拦截处理写在这里,暂无业务需要
|
||||
|
||||
return response;
|
||||
},
|
||||
(error: AxiosError<ResponseData>) => {
|
||||
// 统一错误处理
|
||||
if (error.response) {
|
||||
const { status, data } = error.response;
|
||||
const errMessage = data?.message || error.message;
|
||||
|
||||
switch (status) {
|
||||
case 401:
|
||||
window.location.href = "/login";
|
||||
break;
|
||||
case 403:
|
||||
console.error("Permission denied:", errMessage);
|
||||
break;
|
||||
case 500:
|
||||
// TODO 弹Toast窗
|
||||
// NOTE: move to component layer for customized message?
|
||||
notification.error({
|
||||
message: "服务器错误",
|
||||
description: errMessage,
|
||||
placement: "bottomRight"
|
||||
});
|
||||
console.error("Server error:", errMessage);
|
||||
break;
|
||||
}
|
||||
|
||||
return Promise.reject({
|
||||
code: data?.code || status,
|
||||
message: errMessage,
|
||||
data: data?.data || null
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.reject({
|
||||
code: -1,
|
||||
message: error.message || "Network Error",
|
||||
data: null
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// 转换下划线为驼峰
|
||||
private convertKeysToCamel(obj: JSONValue): JSONValue {
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map((v) => this.convertKeysToCamel(v));
|
||||
} else if (obj !== null && typeof obj === "object") {
|
||||
return Object.keys(obj).reduce((acc, key) => {
|
||||
const camelKey = key.replace(/_([a-z])/g, (_, letter) =>
|
||||
letter.toUpperCase()
|
||||
);
|
||||
acc[camelKey] = this.convertKeysToCamel((obj as JSONObject)[key]);
|
||||
return acc;
|
||||
}, {} as JSONObject);
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
// 核心请求方法
|
||||
public async request<T = unknown>(config: RequestConfig): Promise<T> {
|
||||
try {
|
||||
// 这里未来如果需要SSR可以将前面替换为SSR的instance
|
||||
const instance = config.isSSR ? this.instance : this.instance;
|
||||
const response = await instance.request<ResponseData<T>>(config);
|
||||
return response.data.data;
|
||||
} catch (error) {
|
||||
return this.handleError(error as object);
|
||||
}
|
||||
}
|
||||
|
||||
private handleError(error: object): never {
|
||||
if (axios.isCancel(error)) {
|
||||
throw { code: -2, message: "Request canceled", data: null };
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
// 快捷方法
|
||||
public get<T = unknown>(
|
||||
url: string,
|
||||
params?: object,
|
||||
config?: RequestConfig
|
||||
) {
|
||||
return this.request<T>({ method: "get", url, params, ...config });
|
||||
}
|
||||
|
||||
public post<T = unknown>(url: string, data?: object, config?: RequestConfig) {
|
||||
return this.request<T>({ method: "post", url, data, ...config });
|
||||
}
|
||||
|
||||
public put<T = unknown>(url: string, data?: object, config?: RequestConfig) {
|
||||
return this.request<T>({ method: "put", url, data, ...config });
|
||||
}
|
||||
|
||||
public delete<T = unknown>(url: string, config?: RequestConfig) {
|
||||
return this.request<T>({ method: "delete", url, ...config });
|
||||
}
|
||||
|
||||
// real api request implementation
|
||||
// ============ Provider API ============
|
||||
public getProviderRequesters(): Promise<ApiRespProviderRequesters> {
|
||||
return this.get("/api/v1/provider/requesters");
|
||||
}
|
||||
|
||||
public getProviderRequester(name: string): Promise<ApiRespProviderRequester> {
|
||||
return this.get(`/api/v1/provider/requesters/${name}`);
|
||||
}
|
||||
|
||||
public getProviderRequesterIconURL(name: string): string {
|
||||
return `/api/v1/provider/requesters/${name}/icon`;
|
||||
}
|
||||
|
||||
// ============ Provider Model LLM ============
|
||||
public getProviderLLMModels(): Promise<ApiRespProviderLLMModels> {
|
||||
return this.get("/api/v1/provider/models/llm");
|
||||
}
|
||||
|
||||
public getProviderLLMModel(uuid: string): Promise<ApiRespProviderLLMModel> {
|
||||
return this.get(`/api/v1/provider/models/llm/${uuid}`);
|
||||
}
|
||||
|
||||
public createProviderLLMModel(model: LLMModel): Promise<object> {
|
||||
return this.post("/api/v1/provider/models/llm", model);
|
||||
}
|
||||
|
||||
public deleteProviderLLMModel(uuid: string): Promise<object> {
|
||||
return this.delete(`/api/v1/provider/models/llm/${uuid}`);
|
||||
}
|
||||
|
||||
// ============ Pipeline API ============
|
||||
public getGeneralPipelineMetadata(): Promise<object> {
|
||||
// as designed, this method will be deprecated, and only for developer to check the prefered config schema
|
||||
return this.get("/api/v1/pipelines/_/metadata");
|
||||
}
|
||||
|
||||
public getPipelines(): Promise<ApiRespPipelines> {
|
||||
return this.get("/api/v1/pipelines");
|
||||
}
|
||||
|
||||
public getPipeline(uuid: string): Promise<ApiRespPipeline> {
|
||||
return this.get(`/api/v1/pipelines/${uuid}`);
|
||||
}
|
||||
|
||||
public createPipeline(pipeline: Pipeline): Promise<object> {
|
||||
return this.post("/api/v1/pipelines", pipeline);
|
||||
}
|
||||
|
||||
public updatePipeline(uuid: string, pipeline: Pipeline): Promise<object> {
|
||||
return this.put(`/api/v1/pipelines/${uuid}`, pipeline);
|
||||
}
|
||||
|
||||
public deletePipeline(uuid: string): Promise<object> {
|
||||
return this.delete(`/api/v1/pipelines/${uuid}`);
|
||||
}
|
||||
|
||||
// ============ Platform API ============
|
||||
public getAdapters(): Promise<ApiRespPlatformAdapters> {
|
||||
return this.get("/api/v1/platform/adapters");
|
||||
}
|
||||
|
||||
public getAdapter(name: string): Promise<ApiRespPlatformAdapter> {
|
||||
return this.get(`/api/v1/platform/adapters/${name}`);
|
||||
}
|
||||
|
||||
public getAdapterIconURL(name: string): string {
|
||||
return `/api/v1/platform/adapters/${name}/icon`;
|
||||
}
|
||||
|
||||
// ============ Platform Bots ============
|
||||
public getBots(): Promise<ApiRespPlatformBots> {
|
||||
return this.get("/api/v1/platform/bots");
|
||||
}
|
||||
|
||||
public getBot(uuid: string): Promise<ApiRespPlatformBot> {
|
||||
return this.get(`/api/v1/platform/bots/${uuid}`);
|
||||
}
|
||||
|
||||
public createBot(bot: Bot): Promise<object> {
|
||||
return this.post("/api/v1/platform/bots", bot);
|
||||
}
|
||||
|
||||
public updateBot(uuid: string, bot: Bot): Promise<object> {
|
||||
return this.put(`/api/v1/platform/bots/${uuid}`, bot);
|
||||
}
|
||||
|
||||
public deleteBot(uuid: string): Promise<object> {
|
||||
return this.delete(`/api/v1/platform/bots/${uuid}`);
|
||||
}
|
||||
|
||||
// ============ Plugins API ============
|
||||
public getPlugins(): Promise<ApiRespPlugins> {
|
||||
return this.get("/api/v1/plugins");
|
||||
}
|
||||
|
||||
public getPlugin(author: string, name: string): Promise<ApiRespPlugin> {
|
||||
return this.get(`/api/v1/plugins/${author}/${name}`);
|
||||
}
|
||||
|
||||
public getPluginConfig(
|
||||
author: string,
|
||||
name: string
|
||||
): Promise<ApiRespPluginConfig> {
|
||||
return this.get(`/api/v1/plugins/${author}/${name}/config`);
|
||||
}
|
||||
|
||||
public updatePluginConfig(
|
||||
author: string,
|
||||
name: string,
|
||||
config: object
|
||||
): Promise<object> {
|
||||
return this.put(`/api/v1/plugins/${author}/${name}/config`, config);
|
||||
}
|
||||
|
||||
public togglePlugin(
|
||||
author: string,
|
||||
name: string,
|
||||
target_enabled: boolean
|
||||
): Promise<object> {
|
||||
return this.put(`/api/v1/plugins/${author}/${name}/toggle`, {
|
||||
target_enabled
|
||||
});
|
||||
}
|
||||
|
||||
public reorderPlugins(plugins: PluginReorderElement[]): Promise<object> {
|
||||
return this.post("/api/v1/plugins/reorder", plugins);
|
||||
}
|
||||
|
||||
public updatePlugin(
|
||||
author: string,
|
||||
name: string
|
||||
): Promise<AsyncTaskCreatedResp> {
|
||||
return this.post(`/api/v1/plugins/${author}/${name}/update`);
|
||||
}
|
||||
|
||||
public getMarketPlugins(
|
||||
page: number,
|
||||
page_size: number,
|
||||
query: string,
|
||||
): Promise<MarketPluginResponse> {
|
||||
return this.post(`/api/v1/market/plugins`, {
|
||||
page,
|
||||
page_size,
|
||||
query,
|
||||
sort_by: "stars",
|
||||
sort_order: "DESC"
|
||||
})
|
||||
}
|
||||
public installPluginFromGithub(
|
||||
source: string
|
||||
): Promise<AsyncTaskCreatedResp> {
|
||||
return this.post("/api/v1/plugins/install/github", { source });
|
||||
}
|
||||
|
||||
public removePlugin(
|
||||
author: string,
|
||||
name: string
|
||||
): Promise<AsyncTaskCreatedResp> {
|
||||
return this.delete(`/api/v1/plugins/${author}/${name}`);
|
||||
}
|
||||
|
||||
// ============ System API ============
|
||||
public getSystemInfo(): Promise<ApiRespSystemInfo> {
|
||||
return this.get("/api/v1/system/info");
|
||||
}
|
||||
|
||||
public getAsyncTasks(): Promise<ApiRespAsyncTasks> {
|
||||
return this.get("/api/v1/system/tasks");
|
||||
}
|
||||
|
||||
public getAsyncTask(id: number): Promise<ApiRespAsyncTask> {
|
||||
return this.get(`/api/v1/system/tasks/${id}`);
|
||||
}
|
||||
|
||||
// ============ User API ============
|
||||
public checkIfInited(): Promise<{ initialized: boolean }> {
|
||||
return this.get("/api/v1/user/init");
|
||||
}
|
||||
|
||||
public initUser(user: string, password: string): Promise<object> {
|
||||
return this.post("/api/v1/user/init", { user, password });
|
||||
}
|
||||
|
||||
public authUser(user: string, password: string): Promise<ApiRespUserToken> {
|
||||
return this.post("/api/v1/user/auth", { user, password });
|
||||
}
|
||||
|
||||
public checkUserToken(): Promise<ApiRespUserToken> {
|
||||
return this.get("/api/v1/user/check-token");
|
||||
}
|
||||
}
|
||||
|
||||
export const httpClient = new HttpClient("https://version-4.langbot.dev");
|
||||
|
||||
// 临时写法,未来两种Client都继承自HttpClient父类,不允许共享方法
|
||||
export const spaceClient = new HttpClient("https://space.langbot.app")
|
||||
22
web/src/app/layout.tsx
Normal file
22
web/src/app/layout.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import "./global.css"
|
||||
import type { Metadata } from "next";
|
||||
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={``}>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
26
web/src/app/login/layout.tsx
Normal file
26
web/src/app/login/layout.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
'use client';
|
||||
|
||||
import React from "react";
|
||||
import { ConfigProvider, theme } from 'antd';
|
||||
|
||||
export default function LoginLayout({
|
||||
children
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
token: {
|
||||
colorPrimary: '#2288ee',
|
||||
borderRadius: 6,
|
||||
},
|
||||
algorithm: theme.defaultAlgorithm,
|
||||
}}
|
||||
>
|
||||
<div style={{ width: '100%', height: '100%' }}>
|
||||
<main style={{ width: '100%', height: '100%' }}>{children}</main>
|
||||
</div>
|
||||
</ConfigProvider>
|
||||
)
|
||||
}
|
||||
98
web/src/app/login/login.module.css
Normal file
98
web/src/app/login/login.module.css
Normal file
@@ -0,0 +1,98 @@
|
||||
.container {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.login {
|
||||
display: flex;
|
||||
width: 30%;
|
||||
height: 80vh;
|
||||
background-color: white;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.left {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
|
||||
.loginForm {
|
||||
width: 100%;
|
||||
max-width: 360px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 2rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.loginButton {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.divider {
|
||||
margin: 1.5rem 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.socialLogin {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.socialButton {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.rememberMe {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
.forgetPassword {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* 修改Logo样式,调整位置确保正确对齐 */
|
||||
.logoContainer {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
204
web/src/app/login/page.tsx
Normal file
204
web/src/app/login/page.tsx
Normal file
@@ -0,0 +1,204 @@
|
||||
"use client";
|
||||
import { Button, Input, Form, Checkbox, Divider } from "antd";
|
||||
import {
|
||||
GoogleOutlined,
|
||||
LockOutlined,
|
||||
UserOutlined,
|
||||
QqOutlined
|
||||
} from "@ant-design/icons";
|
||||
import styles from "./login.module.css";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { httpClient } from "@/app/infra/http/HttpClient";
|
||||
import "@ant-design/v5-patch-for-react-19";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export default function Home() {
|
||||
const router = useRouter();
|
||||
const [form] = Form.useForm<LoginField>();
|
||||
const [rememberMe, setRememberMe] = useState(false);
|
||||
const [isRegisterMode, setIsRegisterMode] = useState(false);
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
getIsInitialized();
|
||||
}, []);
|
||||
|
||||
// 检查是否为首次启动项目,只为首次启动的用户提供注册资格
|
||||
function getIsInitialized() {
|
||||
httpClient
|
||||
.checkIfInited()
|
||||
.then((res) => {
|
||||
setIsInitialized(res.initialized);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log("error at getIsInitialized: ", err);
|
||||
});
|
||||
}
|
||||
|
||||
function handleFormSubmit(formField: LoginField) {
|
||||
if (isRegisterMode) {
|
||||
handleRegister(formField.email, formField.password);
|
||||
} else {
|
||||
handleLogin(formField.email, formField.password);
|
||||
}
|
||||
}
|
||||
|
||||
function handleRegister(username: string, password: string) {
|
||||
httpClient
|
||||
.initUser(username, password)
|
||||
.then((res) => {
|
||||
console.log("init user success: ", res);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log("init user error: ", err);
|
||||
});
|
||||
}
|
||||
|
||||
function handleLogin(username: string, password: string) {
|
||||
httpClient
|
||||
.authUser(username, password)
|
||||
.then((res) => {
|
||||
localStorage.setItem("token", res.token);
|
||||
console.log("login success: ", res);
|
||||
router.push("/home");
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log("login error: ", err);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
// 使用 Ant Design 的组件库,使用 antd 的样式
|
||||
// 仅前端样式,无交互功能。
|
||||
|
||||
<div className={styles.container}>
|
||||
{/* login 类是整个 container,使用 flex 左右布局 */}
|
||||
<div className={styles.login}>
|
||||
{/* left 为注册的表单,需要填入的内容有:邮箱,密码 */}
|
||||
<div className={styles.left}>
|
||||
<div className={styles.loginForm}>
|
||||
{isRegisterMode && (
|
||||
<h1 className={styles.title}>注册 LangBot 账号</h1>
|
||||
)}
|
||||
{!isRegisterMode && (
|
||||
<h1 className={styles.title}>欢迎回到 LangBot</h1>
|
||||
)}
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={(values) => {
|
||||
handleFormSubmit(values);
|
||||
}}
|
||||
>
|
||||
<Form.Item
|
||||
name="email"
|
||||
rules={[
|
||||
{ required: true, message: "请输入邮箱!" },
|
||||
{ type: "email", message: "请输入有效的邮箱地址!" }
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
placeholder="输入邮箱地址"
|
||||
size="large"
|
||||
prefix={<UserOutlined />}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="password"
|
||||
rules={[{ required: true, message: "请输入密码!" }]}
|
||||
>
|
||||
<Input.Password
|
||||
placeholder="输入密码"
|
||||
size="large"
|
||||
prefix={<LockOutlined />}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<div className={styles.rememberMe}>
|
||||
<Checkbox
|
||||
checked={rememberMe}
|
||||
onChange={(e) => setRememberMe(e.target.checked)}
|
||||
>
|
||||
30天内自动登录
|
||||
</Checkbox>
|
||||
<span>
|
||||
<a href="#" className={`${styles.forgetPassword}`}>
|
||||
忘记密码?
|
||||
</a>
|
||||
{!isRegisterMode && (
|
||||
<a
|
||||
href=""
|
||||
onClick={(event) => {
|
||||
setIsRegisterMode(true);
|
||||
event.preventDefault();
|
||||
}}
|
||||
>
|
||||
去注册?
|
||||
</a>
|
||||
)}
|
||||
{isRegisterMode && (
|
||||
<a
|
||||
href=""
|
||||
onClick={(event) => {
|
||||
setIsRegisterMode(false);
|
||||
event.preventDefault();
|
||||
}}
|
||||
>
|
||||
去登录
|
||||
</a>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
className={styles.loginButton}
|
||||
block
|
||||
htmlType="submit"
|
||||
disabled={isRegisterMode && isInitialized}
|
||||
>
|
||||
{isRegisterMode
|
||||
? isInitialized
|
||||
? "暂不提供注册"
|
||||
: "注册"
|
||||
: "登录"}
|
||||
</Button>
|
||||
|
||||
<Divider className={styles.divider}>或</Divider>
|
||||
|
||||
<div className={styles.socialLogin}>
|
||||
<Button
|
||||
className={styles.socialButton}
|
||||
icon={<GoogleOutlined />}
|
||||
size="large"
|
||||
disabled={true}
|
||||
>
|
||||
使用谷歌账号登录
|
||||
</Button>
|
||||
</div>
|
||||
<div style={{ height: "10px" }}></div>
|
||||
<div className={styles.socialLogin}>
|
||||
<Button
|
||||
className={styles.socialButton}
|
||||
icon={<QqOutlined />}
|
||||
size="large"
|
||||
disabled={true}
|
||||
>
|
||||
使用QQ账号登录
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface LoginField {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
8
web/src/app/not-found.tsx
Normal file
8
web/src/app/not-found.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<div className={``}>
|
||||
{/* TODO: @qidongrui 这里404页面有时间要更新*/}
|
||||
<h1>Langbot没有找到该页面喔~</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
7
web/src/app/page.tsx
Normal file
7
web/src/app/page.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className={``}>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user