diff --git a/frontend/packages/core/src/App.tsx b/frontend/packages/core/src/App.tsx index de83bc0f..aa5b4c40 100644 --- a/frontend/packages/core/src/App.tsx +++ b/frontend/packages/core/src/App.tsx @@ -1,47 +1,45 @@ - +import { StyleProvider } from '@ant-design/cssinjs' +import { BreadcrumbProvider } from '@common/contexts/BreadcrumbContext.tsx' +import { GlobalProvider } from '@common/contexts/GlobalStateContext' +import { useLocaleContext } from '@common/contexts/LocaleContext' +import { PluginEventHubProvider } from '@common/contexts/PluginEventHubContext' +import { PluginSlotHubProvider } from '@common/contexts/PluginSlotHubContext' +import useInitializeMonaco from '@common/hooks/useInitializeMonaco' +import { $t } from '@common/locales' +import RenderRoutes from '@core/components/aoplatform/RenderRoutes' +import { App as AppAntd, ConfigProvider } from 'antd' +import { useMemo } from 'react' import './App.css' -import { ConfigProvider, App as AppAntd } from 'antd'; -import RenderRoutes from '@core/components/aoplatform/RenderRoutes'; -import {BreadcrumbProvider} from "@common/contexts/BreadcrumbContext.tsx"; -import useInitializeMonaco from "@common/hooks/useInitializeMonaco"; -import { useMemo } from 'react'; -import { GlobalProvider } from '@common/contexts/GlobalStateContext'; -import { $t } from '@common/locales'; -import { PluginEventHubProvider } from '@common/contexts/PluginEventHubContext'; -import { PluginSlotHubProvider } from '@common/contexts/PluginSlotHubContext'; -import { useLocaleContext } from '@common/contexts/LocaleContext'; -import { StyleProvider } from '@ant-design/cssinjs'; - const antdComponentThemeToken = { token: { // Seed Token,影响范围大 colorPrimary: '#3D46F2', - colorLink:'#3D46F2', - colorBorder:'#ededed', - colorText:'#333', + colorLink: '#3D46F2', + colorBorder: '#ededed', + colorText: '#333', borderRadius: 4, // 派生变量,影响范围小 colorBgContainer: '#fff', - colorPrimaryBg:'#EBEEF2', - colorTextQuaternary:'#BBB', - colorTextTertiary:'#999' + colorPrimaryBg: '#EBEEF2', + colorTextQuaternary: '#BBB', + colorTextTertiary: '#999' }, - components:{ + components: { // 派生变量,影响范围小 - Input:{ - activeShadow:'none' + Input: { + activeShadow: 'none' }, - Select:{ - activeShadow:'none' + Select: { + activeShadow: 'none' }, - Checkbox:{ - activeShadow:'none' + Checkbox: { + activeShadow: 'none' }, - Cascader:{ - activeShadow:'none', - optionSelectedBg:'#EBEEF2', - optionHoverBg:'#EBEEF2' + Cascader: { + activeShadow: 'none', + optionSelectedBg: '#EBEEF2', + optionHoverBg: '#EBEEF2' }, Layout: { bodyBg: '#fff', @@ -50,122 +48,122 @@ const antdComponentThemeToken = { headerHeight: 50, headerPadding: '10 20px', lightSiderBg: '#fff', - siderBg: '#fff', + siderBg: '#fff' }, - Breadcrumb:{ - itemColor:'#666', - linkColor:'#666', - lastItemColor:'#333', + Breadcrumb: { + itemColor: '#666', + linkColor: '#666', + lastItemColor: '#333' }, - Table:{ - headerBorderRadius:0, - headerSplitColor:'#ededed', - borderColor:'#ededed', - cellPaddingBlockMD:'15px', - cellPaddingInlineMD:'12px', - cellPaddingBlockSM:'8px', - cellPaddingInlineSM:'12px', - headerFilterHoverBg:'#EBEEF2', - headerSortActiveBg:'#F7F8FA', - headerSortHoverBg:'#F7F8FA', - fixedHeaderSortActiveBg:'#F7F8FA', - headerBg:'#FAFAFA', - rowHoverBg:'#EBEEF2' - + Table: { + headerBorderRadius: 0, + headerSplitColor: '#ededed', + borderColor: '#ededed', + cellPaddingBlockMD: '15px', + cellPaddingInlineMD: '12px', + cellPaddingBlockSM: '8px', + cellPaddingInlineSM: '12px', + headerFilterHoverBg: '#EBEEF2', + headerSortActiveBg: '#F7F8FA', + headerSortHoverBg: '#F7F8FA', + fixedHeaderSortActiveBg: '#F7F8FA', + headerBg: '#FAFAFA', + rowHoverBg: '#EBEEF2' }, - Segmented:{ - itemColor:'#333', - itemSelectedColor:'#333', - trackBg:'#f7f8fa', - trackPadding:0, - // itemHoverColor:'#EBEEF2', - itemActiveBg:'#EBEEF2', - itemHoverBg:'#EBEEF2', - itemSelectedBg:'#EBEEF2', + Segmented: { + itemColor: '#333', + itemSelectedColor: '#333', + trackBg: '#f7f8fa', + trackPadding: 0, + // itemHoverColor:'#EBEEF2', + itemActiveBg: '#EBEEF2', + itemHoverBg: '#EBEEF2', + itemSelectedBg: '#EBEEF2' }, - Tree:{ - // titleHeight:30, - // fontSize:12, - directoryNodeSelectedBg:'#EBEEF2', - directoryNodeSelectedColor:'#333', - nodeSelectedBg:'#EBEEF2', - nodeHoverBg:'#EBEEF2' - }, - Collapse:{ - headerBg:'#f7f8fa', - headerPadding:"12px", - contentPadding:"0 10px 12px 10px" - }, - Button:{ - // paddingInline:8, - dangerShadow:'none', - defaultShadow:'none', - primaryShadow:'none' - }, - Tabs:{ - cardBg:'#EBEEF2', - cardHeight:42, - horizontalItemGutter:8, - horizontalItemPaddingSM:'12px 8px 8px 8px', - horizontalItemPadding:'12px 8px 8px 8px', - }, - Menu:{ - // itemBg:'#F7F8FA', - // subMenuItemBg:'#F7F8FA', - // itemMarginBlock:0, - // activeBarBorderWidth:0, - // itemSelectedColor:'#333', - // itemSelectedBg:'#EBEEF2', - // itemHoverBg:'#EBEEF2' - }, - List:{ - itemPadding:'8px 0' - }, - Form:{ - itemMarginBottom:10, - - }, - Alert:{ - defaultPadding:'12px 16px' - }, - Tag:{ - defaultBg:"#f7f8fa" - }, + Tree: { + // titleHeight:30, + // fontSize:12, + directoryNodeSelectedBg: '#EBEEF2', + directoryNodeSelectedColor: '#333', + nodeSelectedBg: '#EBEEF2', + nodeHoverBg: '#EBEEF2' + }, + Collapse: { + headerBg: '#f7f8fa', + headerPadding: '12px', + contentPadding: '0 10px 12px 10px' + }, + Button: { + // paddingInline:8, + dangerShadow: 'none', + defaultShadow: 'none', + primaryShadow: 'none' + }, + Tabs: { + cardBg: '#EBEEF2', + cardHeight: 42, + horizontalItemGutter: 8, + horizontalItemPaddingSM: '12px 8px 8px 8px', + horizontalItemPadding: '12px 8px 8px 8px' + }, + Menu: { + // itemBg:'#F7F8FA', + // subMenuItemBg:'#F7F8FA', + // itemMarginBlock:0, + // activeBarBorderWidth:0, + // itemSelectedColor:'#333', + // itemSelectedBg:'#EBEEF2', + // itemHoverBg:'#EBEEF2' + }, + List: { + itemPadding: '8px 0' + }, + Form: { + itemMarginBottom: 10 + }, + Alert: { + defaultPadding: '8px 12px' + }, + Tag: { + defaultBg: '#f7f8fa' + } } } - function App() { - const { locale } = useLocaleContext(); + const { locale } = useLocaleContext() useInitializeMonaco() - - - const validateMessages = useMemo(()=>({ - required: $t('必填项'), - email:$t('不是有效邮箱地址')} - ),[locale]) - + + const validateMessages = useMemo( + () => ({ + required: $t('必填项'), + email: $t('不是有效邮箱地址') + }), + [locale] + ) + return ( - - + - - - - - - - - - + form={{ validateMessages }} + > + + + + + + + + + - ); + ) } export default App diff --git a/frontend/packages/core/src/components/AIProviderSelect/index.tsx b/frontend/packages/core/src/components/AIProviderSelect/index.tsx index 3437fb1b..273d9117 100644 --- a/frontend/packages/core/src/components/AIProviderSelect/index.tsx +++ b/frontend/packages/core/src/components/AIProviderSelect/index.tsx @@ -1,17 +1,14 @@ import { STATUS_CODE } from '@common/const/const' import { useFetch } from '@common/hooks/http' +import { ModelDetailData } from '@core/pages/aiSetting/types' import { Select, Space, message } from 'antd' import React, { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' -export interface AIProvider { - id: string - name: string - logo: string - configured: boolean - getApikeyUrl: string - status: string +export interface AIProvider extends ModelDetailData { default_config: string + backupName: string + backupModel: string } interface AIProviderResponse { @@ -41,13 +38,18 @@ const AIProviderSelect: React.FC = ({ value, onChange, st const fetchProviders = async () => { if (isMounted) setLoading(true) try { - const response = await fetchData('simple/ai/providers/configured', { method: 'GET' }) + const endpoint = 'simple/ai/providers/configured' + const response = await fetchData(endpoint, { method: 'GET' }) const { code, data, msg } = response if (code === STATUS_CODE.SUCCESS) { - isMounted && setProviders(data.providers) + const providers = data.providers.map((val) => ({ + ...val, + backupName: data.backup?.name, + backupModel: data.backup?.model?.name + })) + isMounted && setProviders(providers) if (!data.providers?.length) return - - const selectedProvider: AIProvider = value ? providers.find((p) => p.id === value) : data.providers[0] + const selectedProvider: AIProvider = value ? providers.find((p) => p.id === value) : providers[0] onChange?.(selectedProvider.id, selectedProvider) } else { message.error(msg || t('Failed to fetch AI providers')) diff --git a/frontend/packages/core/src/const/ai-service/const.tsx b/frontend/packages/core/src/const/ai-service/const.tsx index bd101041..91112b48 100644 --- a/frontend/packages/core/src/const/ai-service/const.tsx +++ b/frontend/packages/core/src/const/ai-service/const.tsx @@ -1,127 +1,134 @@ -import { AiServiceRouterTableListItem, VariableItems } from "./type"; -import { TabsProps } from "antd"; -import { frontendTimeSorter } from "@common/utils/dataTransfer"; -import { COLUMNS_TITLE, PLACEHOLDER } from "@common/const/const"; - -import { PageProColumns } from "@common/components/aoplatform/PageList"; - +import { COLUMNS_TITLE, PLACEHOLDER } from '@common/const/const' +import { frontendTimeSorter } from '@common/utils/dataTransfer' +import { TabsProps } from 'antd' +import { AiServiceRouterTableListItem, VariableItems } from './type' +import { PageProColumns } from '@common/components/aoplatform/PageList' export const AI_SERVICE_ROUTER_TABLE_COLUMNS: PageProColumns[] = [ - { - title:('URL'), - dataIndex: 'requestPath', - ellipsis:true - }, - { - title:('名称'), - dataIndex: 'name', - ellipsis:true, - }, - { - title:('模型'), - dataIndex: ['model','logo'], - ellipsis:true, - render: (_: React.ReactNode, entity: AiServiceRouterTableListItem) =>
{entity.model.id}
- }, - { - title:('描述'), - dataIndex: 'description', - ellipsis:true - }, - { - title:('创建者'), - dataIndex: ['creator','name'], - ellipsis: true, - filters: true, - onFilter: true, - valueType: 'select', - filterSearch: true, - }, - { - title:('更新时间'), - dataIndex: 'updateTime', - ellipsis:true, - hideInSearch: true, - width:182, - sorter: (a,b)=>frontendTimeSorter(a,b,'updateTime') - }, -]; + { + title: 'URL', + dataIndex: 'requestPath', + ellipsis: true + }, + { + title: '名称', + dataIndex: 'name', + ellipsis: true + }, + { + title: '模型', + dataIndex: ['model', 'logo'], + ellipsis: true, + render: (_: React.ReactNode, entity: AiServiceRouterTableListItem) => ( +
+ {entity.model.id} +
+ ) + }, + { + title: '是否放行', + dataIndex: 'disabled', + ellipsis: true, + filters: true, + onFilter: true, + valueType: 'select' + }, + { + title: '描述', + dataIndex: 'description', + ellipsis: true + }, + { + title: '创建者', + dataIndex: ['creator', 'name'], + ellipsis: true, + filters: true, + onFilter: true, + valueType: 'select', + filterSearch: true + }, + { + title: '更新时间', + dataIndex: 'updateTime', + ellipsis: true, + hideInSearch: true, + width: 182, + sorter: (a, b) => frontendTimeSorter(a, b, 'updateTime') + } +] - -export const AI_SERVICE_VARIABLES_TABLE_COLUMNS: PageProColumns[] = [ - { - title:('Key'), - dataIndex: 'key', - key:'key', - width: '30%', - formItemProps: { - className:'p-0 bg-transparent border-none', - rules: [ - { - required: true, - whitespace: true - }, - { - pattern:/^[a-zA-Z][a-zA-Z0-9-_]*$/, - message: PLACEHOLDER.onlyAlphabet - } - ], - }, - ellipsis:true, - fieldProps:{ - allowClear:false - } - }, - { - title:('描述'), - dataIndex: 'description', - key:'description', - formItemProps: { - className:'p-0 bg-transparent border-none' +export const AI_SERVICE_VARIABLES_TABLE_COLUMNS: PageProColumns[] = [ + { + title: 'Key', + dataIndex: 'key', + key: 'key', + width: '30%', + formItemProps: { + className: 'p-0 bg-transparent border-none', + rules: [ + { + required: true, + whitespace: true }, - fieldProps:{ - allowClear:false + { + pattern: /^[a-zA-Z][a-zA-Z0-9-_]*$/, + message: PLACEHOLDER.onlyAlphabet } + ] }, - { - title:('必填'), - dataIndex: 'require', - key:'require', - valueType:'switch', - width:64, - formItemProps: { - className:'p-0 bg-transparent border-none'} - }, - { - title: COLUMNS_TITLE.operate, - valueType: 'option', - width:34, - render: ()=>null - }, - ]; - - -export const AiService_INSIDE_APPROVAL_TAB_ITEMS: TabsProps['items'] = [ - { - key: '0', - label:('待审核'), - }, - { - key: '1', - label: ('已审核'), + ellipsis: true, + fieldProps: { + allowClear: false } -]; - + }, + { + title: '描述', + dataIndex: 'description', + key: 'description', + formItemProps: { + className: 'p-0 bg-transparent border-none' + }, + fieldProps: { + allowClear: false + } + }, + { + title: '必填', + dataIndex: 'require', + key: 'require', + valueType: 'switch', + width: 64, + formItemProps: { + className: 'p-0 bg-transparent border-none' + } + }, + { + title: COLUMNS_TITLE.operate, + valueType: 'option', + width: 34, + render: () => null + } +] +export const AiService_INSIDE_APPROVAL_TAB_ITEMS: TabsProps['items'] = [ + { + key: '0', + label: '待审核' + }, + { + key: '1', + label: '已审核' + } +] export const AiService_PUBLISH_TAB_ITEMS: TabsProps['items'] = [ - { - key: '0', - label: ('发布版本'), - }, - { - key: '1', - label: ('发布申请记录'), - } -]; + { + key: '0', + label: '发布版本' + }, + { + key: '1', + label: '发布申请记录' + } +] diff --git a/frontend/packages/core/src/const/system/const.tsx b/frontend/packages/core/src/const/system/const.tsx index 2fdd5234..986f6590 100644 --- a/frontend/packages/core/src/const/system/const.tsx +++ b/frontend/packages/core/src/const/system/const.tsx @@ -258,7 +258,7 @@ export const SYSTEM_API_TABLE_COLUMNS: PageProColumns[] }, { title: '是否放行', - dataIndex: 'disable', + dataIndex: 'disabled', ellipsis: true, filters: true, onFilter: true, diff --git a/frontend/packages/core/src/pages/aiApis/components/ApiKeyContent.tsx b/frontend/packages/core/src/pages/aiApis/components/ApiKeyContent.tsx new file mode 100644 index 00000000..0ce3c4a4 --- /dev/null +++ b/frontend/packages/core/src/pages/aiApis/components/ApiKeyContent.tsx @@ -0,0 +1,111 @@ +import { Codebox } from '@common/components/postcat/api/Codebox' +import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const' +import { useFetch } from '@common/hooks/http' +import { $t } from '@common/locales' +import { AIProvider } from '@core/components/AIProviderSelect' +import { App, DatePicker, Form, Input, Switch } from 'antd' +import dayjs from 'dayjs' +import React, { forwardRef, useEffect, useImperativeHandle, useState } from 'react' +import { EditAPIKey } from '../types' + +interface ApiKeyContentProps { + provider?: AIProvider + entity: EditAPIKey +} + +const ApiKeyContent: React.FC = forwardRef(({ provider, entity }, ref) => { + const [form] = Form.useForm() + const [neverExpire, setNeverExpire] = useState(true) + const { fetchData } = useFetch() + const { message } = App.useApp() + + useEffect(() => { + try { + const isNeverExpire = entity.expire_time === 0 + setNeverExpire(isNeverExpire) + form.setFieldsValue({ + name: entity.name, + expire_time: isNeverExpire ? undefined : dayjs(entity.expire_time), + config: entity.config + }) + } catch (e) { + form.setFieldsValue({ + name: entity.name, + expire_time: undefined, + config: '' + }) + } + }, []) + + const handleOk = async () => { + try { + const values = await form.validateFields() + const { expire_time, ...restValues } = values + const expireTime = neverExpire ? 0 : expire_time.valueOf() + + const response = await fetchData>('ai/resource/key', { + method: entity.id ? 'PUT' : 'POST', + eoParams: { provider: provider?.id, id: entity.id }, + eoBody: { ...restValues, expire_time: expireTime }, + eoTransformKeys: ['config'] + }) + + const { code, msg } = response + if (code === STATUS_CODE.SUCCESS) { + message.success(msg || $t(RESPONSE_TIPS.success)) + return true + } else { + message.error(msg || $t(RESPONSE_TIPS.error)) + return false + } + } catch (error) { + console.error('Validation failed:', error) + } + } + useImperativeHandle(ref, () => ({ + handleOk + })) + const handleNeverExpireChange = (checked: boolean) => { + setNeverExpire(checked) + if (!checked) { + form.setFieldsValue({ + expire_time: dayjs().add(7, 'days') + }) + } + } + + return ( +
+ + + + + + + +
+ + {neverExpire ? $t('永不过期') : $t('设置过期时间')} +
+
+ {!neverExpire && ( + + + + )} +
+ ) +}) + +export default ApiKeyContent diff --git a/frontend/packages/core/src/pages/aiApis/components/StatusFilter.tsx b/frontend/packages/core/src/pages/aiApis/components/StatusFilter.tsx new file mode 100644 index 00000000..64cc18e9 --- /dev/null +++ b/frontend/packages/core/src/pages/aiApis/components/StatusFilter.tsx @@ -0,0 +1,40 @@ +import { $t } from '@common/locales' +import { Select, Space, theme } from 'antd' +import React from 'react' + +interface StatusFilterProps { + value: string[] + onChange: (value: string[]) => void +} + +const StatusFilter: React.FC = ({ value, onChange }) => { + const { token } = theme.useToken() + + const options = [ + { label: $t('Normal'), value: 'normal', color: token.colorSuccess }, + { label: $t('Exceeded'), value: 'exceeded', color: token.colorError }, + { label: $t('Expired'), value: 'expired', color: token.colorWarning }, + { label: $t('Disabled'), value: 'disabled', color: token.colorTextDisabled }, + { label: $t('Error'), value: 'error', color: token.colorError } + ] + + return ( + + {$t('Status')}: + + + + + + + { + if ((e.target.value as string).endsWith('/*')) { + form.setFieldValue('path', e.target.value.slice(0, -2)) + form.setFieldValue('pathMatch', 'prefix') + } + }} + /> + + + + + + label={$t('提示词')} name="prompt"> + + + + + label={ +
+ {$t('变量')} + + + New +
- }> - } spinning={loading} wrapperClassName=' pb-PAGE_INSIDE_B pr-PAGE_INSIDE_X'> -
-
- - - className="flex-1" - label={$t("路由名称")} - name="name" - rules={[{ required: true,whitespace:true }]} - > - - - - - - - - { - if((e.target.value as string).endsWith('/*')){ - form.setFieldValue('path',e.target.value.slice(0,-2)) - form.setFieldValue('pathMatch','prefix') - } - }}/> - - - + } + name="variables" + className="[&>.ant-row>.ant-col>label]:w-full" + > + + getFromRef={setVariablesTableRef} + configFields={AI_SERVICE_VARIABLES_TABLE_COLUMNS} + /> + - + label={$t('描述')} name="description"> + + - - label={$t("提示词")} - name="prompt" - > - - - - - label={
{$t("变量")}New
} - name="variables" - className="[&>.ant-row>.ant-col>label]:w-full" - > - - getFromRef={setVariablesTableRef} - configFields={AI_SERVICE_VARIABLES_TABLE_COLUMNS} - /> - - - - label={$t("描述")} - name="description" - > - - - - - - className="flex-1" - label={$t("请求超时时间")} - name={'timeout'} - rules={[{required: true}]} - > - - - - className="flex-1" - label={$t("重试次数")} - name={'retry'} - rules={[{required: true}]} - > - - - - - -
-
-
- handlerSubmit()} - > - - - - ) + + + className="flex-1" + label={$t('请求超时时间')} + name={'timeout'} + rules={[{ required: true }]} + > + + + + className="flex-1" + label={$t('重试次数')} + name={'retry'} + rules={[{ required: true }]} + > + + + + + label={$t('拦截接口')} + name="disabled" + extra={$t('开启拦截后,网关会拦截所有该路径的请求。')} + > + + + + + + handlerSubmit()}> + + + + ) } export default AiServiceInsideRouterCreate - - - \ No newline at end of file diff --git a/frontend/packages/core/src/pages/aiService/api/AiServiceInsideRouterList.tsx b/frontend/packages/core/src/pages/aiService/api/AiServiceInsideRouterList.tsx index 88a09f72..007f64f6 100644 --- a/frontend/packages/core/src/pages/aiService/api/AiServiceInsideRouterList.tsx +++ b/frontend/packages/core/src/pages/aiService/api/AiServiceInsideRouterList.tsx @@ -1,182 +1,224 @@ -import PageList, { PageProColumns } from "@common/components/aoplatform/PageList.tsx" -import {ActionType} from "@ant-design/pro-components"; -import {FC, useEffect, useMemo, useRef, useState} from "react"; -import {Link, useNavigate, useParams} from "react-router-dom"; -import {useBreadcrumb} from "@common/contexts/BreadcrumbContext.tsx"; -import {App, Divider} from "antd"; -import {BasicResponse, COLUMNS_TITLE, DELETE_TIPS, RESPONSE_TIPS, STATUS_CODE} from "@common/const/const.tsx"; -import { SimpleMemberItem} from '@common/const/type.ts' -import {useFetch} from "@common/hooks/http.ts"; -import {RouterParams} from "@core/components/aoplatform/RenderRoutes.tsx"; -import TableBtnWithPermission from "@common/components/aoplatform/TableBtnWithPermission.tsx"; -import { useGlobalContext } from "@common/contexts/GlobalStateContext.tsx"; -import { checkAccess } from "@common/utils/permission.ts"; -import { $t } from "@common/locales/index.ts"; -import { AiServiceRouterTableListItem } from "@core/const/ai-service/type.ts"; -import { AI_SERVICE_ROUTER_TABLE_COLUMNS } from "@core/const/ai-service/const.tsx"; +import { ActionType } from '@ant-design/pro-components' +import PageList, { PageProColumns } from '@common/components/aoplatform/PageList.tsx' +import TableBtnWithPermission from '@common/components/aoplatform/TableBtnWithPermission.tsx' +import { BasicResponse, COLUMNS_TITLE, DELETE_TIPS, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const.tsx' +import { SimpleMemberItem } from '@common/const/type.ts' +import { useBreadcrumb } from '@common/contexts/BreadcrumbContext.tsx' +import { useGlobalContext } from '@common/contexts/GlobalStateContext.tsx' +import { useFetch } from '@common/hooks/http.ts' +import { $t } from '@common/locales/index.ts' +import { checkAccess } from '@common/utils/permission.ts' +import { RouterParams } from '@core/components/aoplatform/RenderRoutes.tsx' +import { AI_SERVICE_ROUTER_TABLE_COLUMNS } from '@core/const/ai-service/const.tsx' +import { AiServiceRouterTableListItem } from '@core/const/ai-service/type.ts' +import { App, Divider, Typography } from 'antd' +import { FC, useEffect, useMemo, useRef, useState } from 'react' +import { Link, useNavigate, useParams } from 'react-router-dom' -const AiServiceInsideRouterList:FC = ()=>{ - const [searchWord, setSearchWord] = useState('') - const { setBreadcrumb } = useBreadcrumb() - const { modal,message } = App.useApp() - const [tableListDataSource, setTableListDataSource] = useState([]); - const [tableHttpReload, setTableHttpReload] = useState(true); - const {fetchData} = useFetch() - const pageListRef = useRef(null); - const [memberValueEnum, setMemberValueEnum] = useState([]) - const {accessData,state} = useGlobalContext() - const {serviceId, teamId} = useParams() - const navigator = useNavigate() +const AiServiceInsideRouterList: FC = () => { + const [searchWord, setSearchWord] = useState('') + const { setBreadcrumb } = useBreadcrumb() + const { modal, message } = App.useApp() + const [tableListDataSource, setTableListDataSource] = useState([]) + const [tableHttpReload, setTableHttpReload] = useState(true) + const { fetchData } = useFetch() + const pageListRef = useRef(null) + const [memberValueEnum, setMemberValueEnum] = useState([]) + const { accessData, state } = useGlobalContext() + const { serviceId, teamId } = useParams() + const navigator = useNavigate() - const getRoutesList = (): Promise<{ data: AiServiceRouterTableListItem[], success: boolean }>=> { - if(!tableHttpReload){ - setTableHttpReload(true) - return Promise.resolve({ - data: tableListDataSource, - success: true, - }); - } - - return fetchData>('service/ai-routers',{method:'GET',eoParams:{service:serviceId,team:teamId, keyword:searchWord},eoTransformKeys:['request_path','create_time','update_time','disable']}).then(response=>{ - const {code,data,msg} = response - if(code === STATUS_CODE.SUCCESS){ - setTableListDataSource(data.apis) - setTableHttpReload(false) - return {data:data.apis, success: true} - }else{ - message.error(msg || $t(RESPONSE_TIPS.error)) - return {data:[], success:false} - } - }).catch(() => { - return {data:[], success:false} - }) + const getRoutesList = (): Promise<{ data: AiServiceRouterTableListItem[]; success: boolean }> => { + if (!tableHttpReload) { + setTableHttpReload(true) + return Promise.resolve({ + data: tableListDataSource, + success: true + }) } - const deleteRoute = (entity:AiServiceRouterTableListItem)=>{ - return new Promise((resolve, reject)=>{ - fetchData>('service/ai-router',{method:'DELETE',eoParams:{service:serviceId,team:teamId, router:entity!.id}}).then(response=>{ - const {code,msg} = response - if(code === STATUS_CODE.SUCCESS){ - message.success(msg || $t(RESPONSE_TIPS.success)) - resolve(true) - }else{ - message.error(msg || $t(RESPONSE_TIPS.error)) - reject(msg || $t(RESPONSE_TIPS.error)) - } - }).catch((errorInfo)=> reject(errorInfo)) - }) - } - - const openModal = async (type: 'delete',entity:AiServiceRouterTableListItem) =>{ - let title:string = '' - let content:string|React.ReactNode = '' - switch (type){ - case 'delete': - title=$t('删除') - content=$t(DELETE_TIPS.default) - break; + return fetchData>('service/ai-routers', { + method: 'GET', + eoParams: { service: serviceId, team: teamId, keyword: searchWord }, + eoTransformKeys: ['request_path', 'create_time', 'update_time', 'disable'] + }) + .then((response) => { + const { code, data, msg } = response + if (code === STATUS_CODE.SUCCESS) { + setTableListDataSource(data.apis) + setTableHttpReload(false) + return { data: data.apis, success: true } + } else { + message.error(msg || $t(RESPONSE_TIPS.error)) + return { data: [], success: false } } + }) + .catch(() => { + return { data: [], success: false } + }) + } - modal.confirm({ - title, - content, - onOk:()=> { - switch (type){ - case 'delete': - return deleteRoute(entity).then((res)=>{if(res === true) manualReloadTable()}) - } - }, - width:600, - okText:$t('确认'), - okButtonProps:{ - disabled : !checkAccess( `team.service.router.${type}`, accessData ) - }, - cancelText:$t('取消'), - closable:true, - icon:<>, - }) - } - - const operation:PageProColumns[] =[ - { - title: COLUMNS_TITLE.operate, - key: 'option', - btnNums:2, - fixed:'right', - valueType: 'option', - render: (_: React.ReactNode, entity: AiServiceRouterTableListItem) => [ - {navigator(`/service/${teamId}/aiInside/${serviceId}/route/${entity.id}`)}} btnTitle="编辑"/>, - , - {openModal('delete',entity)}} btnTitle="删除"/>, - ], - } - ] - - const manualReloadTable = () => { - setTableHttpReload(true); // 表格数据需要从后端接口获取 - pageListRef.current?.reload() - }; - - const getMemberList = async ()=>{ - setMemberValueEnum([]) - const {code,data,msg} = await fetchData>('simple/member',{method:'GET'}) - if(code === STATUS_CODE.SUCCESS){ - setMemberValueEnum(data.members) - }else{ + const deleteRoute = (entity: AiServiceRouterTableListItem) => { + return new Promise((resolve, reject) => { + fetchData>('service/ai-router', { + method: 'DELETE', + eoParams: { service: serviceId, team: teamId, router: entity!.id } + }) + .then((response) => { + const { code, msg } = response + if (code === STATUS_CODE.SUCCESS) { + message.success(msg || $t(RESPONSE_TIPS.success)) + resolve(true) + } else { message.error(msg || $t(RESPONSE_TIPS.error)) - } + reject(msg || $t(RESPONSE_TIPS.error)) + } + }) + .catch((errorInfo) => reject(errorInfo)) + }) + } + + const openModal = async (type: 'delete', entity: AiServiceRouterTableListItem) => { + let title: string = '' + let content: string | React.ReactNode = '' + switch (type) { + case 'delete': + title = $t('删除') + content = $t(DELETE_TIPS.default) + break } - useEffect(() => { - setBreadcrumb([ - { - title:{$t('服务')} - }, - { - title:$t('路由') - } - ]) - getMemberList() - manualReloadTable() - }, [serviceId]); - - const columns = useMemo(()=>{ - return [...AI_SERVICE_ROUTER_TABLE_COLUMNS].map(x=>{ - if(x.filters &&((x.dataIndex as string[])?.indexOf('creator') !== -1) ){ - const tmpValueEnum:{[k:string]:{text:string}} = {} - memberValueEnum?.forEach((x:SimpleMemberItem)=>{ - tmpValueEnum[x.name] = {text:x.name} - }) - x.valueEnum = tmpValueEnum - } - - return {...x,title:typeof x.title === 'string' ? $t(x.title as string) : x.title}}) - },[memberValueEnum,state.language]) + modal.confirm({ + title, + content, + onOk: () => { + switch (type) { + case 'delete': + return deleteRoute(entity).then((res) => { + if (res === true) manualReloadTable() + }) + } + }, + width: 600, + okText: $t('确认'), + okButtonProps: { + disabled: !checkAccess(`team.service.router.${type}`, accessData) + }, + cancelText: $t('取消'), + closable: true, + icon: <> + }) + } + const operation: PageProColumns[] = [ + { + title: COLUMNS_TITLE.operate, + key: 'option', + btnNums: 2, + fixed: 'right', + valueType: 'option', + render: (_: React.ReactNode, entity: AiServiceRouterTableListItem) => [ + { + navigator(`/service/${teamId}/aiInside/${serviceId}/route/${entity.id}`) + }} + btnTitle="编辑" + />, + , + { + openModal('delete', entity) + }} + btnTitle="删除" + /> + ] + } + ] - return ( - <> - getRoutesList()} - dataSource={tableListDataSource} - addNewBtnTitle={$t('添加路由')} - searchPlaceholder={$t('输入 URL 查找路由')} - onAddNewBtnClick={()=>{navigator(`/service/${teamId}/aiInside/${serviceId}/route/create`)}} - addNewBtnAccess="team.service.router.add" - tableClickAccess="team.service.router.view" - manualReloadTable={manualReloadTable} - onSearchWordChange={(e)=>{setSearchWord(e.target.value)}} - onChange={() => { - setTableHttpReload(false) - }} - onRowClick={(row:AiServiceRouterTableListItem)=>navigator(`/service/${teamId}/aiInside/${serviceId}/route/${row.id}`)} - tableClass="mr-PAGE_INSIDE_X " - /> - - ) + const manualReloadTable = () => { + setTableHttpReload(true) // 表格数据需要从后端接口获取 + pageListRef.current?.reload() + } + const getMemberList = async () => { + setMemberValueEnum([]) + const { code, data, msg } = await fetchData>('simple/member', { + method: 'GET' + }) + if (code === STATUS_CODE.SUCCESS) { + setMemberValueEnum(data.members) + } else { + message.error(msg || $t(RESPONSE_TIPS.error)) + } + } + + useEffect(() => { + setBreadcrumb([ + { + title: {$t('服务')} + }, + { + title: $t('路由') + } + ]) + getMemberList() + manualReloadTable() + }, [serviceId]) + + const columns = useMemo(() => { + return [...AI_SERVICE_ROUTER_TABLE_COLUMNS].map((x) => { + if (x.filters && (x.dataIndex as string[])?.indexOf('creator') !== -1) { + const tmpValueEnum: { [k: string]: { text: string } } = {} + memberValueEnum?.forEach((x: SimpleMemberItem) => { + tmpValueEnum[x.name] = { text: x.name } + }) + x.valueEnum = tmpValueEnum + } + if (x.filters && (x.dataIndex as string[])?.indexOf('disabled') !== -1) { + x.valueEnum = { + true: { text: {$t('拦截')} }, + false: { text: {$t('放行')} } + } + } + + return { ...x, title: typeof x.title === 'string' ? $t(x.title as string) : x.title } + }) + }, [memberValueEnum, state.language]) + + return ( + <> + getRoutesList()} + dataSource={tableListDataSource} + addNewBtnTitle={$t('添加路由')} + searchPlaceholder={$t('输入 URL 查找路由')} + onAddNewBtnClick={() => { + navigator(`/service/${teamId}/aiInside/${serviceId}/route/create`) + }} + addNewBtnAccess="team.service.router.add" + tableClickAccess="team.service.router.view" + manualReloadTable={manualReloadTable} + onSearchWordChange={(e) => { + setSearchWord(e.target.value) + }} + onChange={() => { + setTableHttpReload(false) + }} + onRowClick={(row: AiServiceRouterTableListItem) => + navigator(`/service/${teamId}/aiInside/${serviceId}/route/${row.id}`) + } + tableClass="mr-PAGE_INSIDE_X " + /> + + ) } -export default AiServiceInsideRouterList \ No newline at end of file +export default AiServiceInsideRouterList diff --git a/frontend/packages/core/src/pages/aiSetting/AIFlowChart.tsx b/frontend/packages/core/src/pages/aiSetting/AIFlowChart.tsx index 06bf6549..79611d80 100644 --- a/frontend/packages/core/src/pages/aiSetting/AIFlowChart.tsx +++ b/frontend/packages/core/src/pages/aiSetting/AIFlowChart.tsx @@ -1,6 +1,9 @@ 'use client' +import { BasicResponse } from '@common/const/const' +import { useGlobalContext } from '@common/contexts/GlobalStateContext' import { useFetch } from '@common/hooks/http' +import { $t } from '@common/locales' import { CoordinateExtent, Edge, @@ -13,28 +16,26 @@ import { useNodesState } from '@xyflow/react' import '@xyflow/react/dist/style.css' +import { Button, Space, Spin } from 'antd' import { useCallback, useEffect, useState } from 'react' +import { useNavigate } from 'react-router-dom' import CustomEdge from './components/CustomEdge' import { KeyStatusNode } from './components/KeyStatusNode' import { ModelCardNode } from './components/ModelCardNode' import { ServiceCardNode } from './components/NodeComponents' import { LAYOUT } from './constants' import './styles.css' -import { ModelData } from './types' +import { ModelListData } from './types' -interface ApiResponse { - data: { - backup: { - id: string - name: string - } - providers: ModelData[] +export type ApiResponse = BasicResponse<{ + backup: { + id: string + name: string } - code: number - success: string -} + providers: ModelListData[] +}> -const calculateNodePositions = (models: ModelData[], startY = LAYOUT.NODE_START_Y, gap = LAYOUT.NODE_GAP) => { +const calculateNodePositions = (models: ModelListData[], startY = LAYOUT.NODE_START_Y, gap = LAYOUT.NODE_GAP) => { return models.reduce( (acc, model, index) => { const y = startY + index * gap @@ -46,7 +47,7 @@ const calculateNodePositions = (models: ModelData[], startY = LAYOUT.NODE_START_ }, [`${model.id}-keys`]: { x: LAYOUT.KEY_NODE_X, - y + y: y + 16 } } }, @@ -65,21 +66,29 @@ const edgeTypes: EdgeTypes = { } const AIFlowChart = () => { - const [modelData, setModelData] = useState([]) + const [modelData, setModelData] = useState([]) + const [loading, setLoading] = useState(false) const [nodes, setNodes, onNodesChange] = useNodesState([]) const [edges, setEdges, onEdgesChange] = useEdgesState([]) const { fetchData } = useFetch() + const { aiConfigFlushed } = useGlobalContext() + const navigate = useNavigate() useEffect(() => { - // Mock API call - replace with actual API call + setLoading(true) fetchData('ai/providers/configured', { - method: 'GET' + method: 'GET', + eoTransformKeys: ['default_llm'] // eoApiPrefix: 'http://uat.apikit.com:11204/mockApi/aoplatform/api/v1/' - }).then((response) => { - const mockApiResponse: ApiResponse = response as ApiResponse - setModelData(mockApiResponse.data.providers) }) - }, []) + .then((response) => { + const mockApiResponse: ApiResponse = response as ApiResponse + setModelData(mockApiResponse.data.providers) + }) + .finally(() => { + setLoading(false) + }) + }, [aiConfigFlushed]) useEffect(() => { if (!modelData.length) return @@ -87,14 +96,13 @@ const AIFlowChart = () => { const positions = calculateNodePositions(modelData) // subtract 5 to make sure the service node is aligned with the top model node const serviceY = positions[modelData[0].id].y - 5 - const newNodes = [ { id: 'apiService', type: 'serviceCard', position: { x: LAYOUT.SERVICE_NODE_X, y: serviceY }, data: { - title: 'API Service', + title: 'API Services', count: modelData.length } }, @@ -103,10 +111,11 @@ const AIFlowChart = () => { type: 'modelCard', position: positions[model.id], data: { - title: model.name, + name: model.name, status: model.status, - defaultModel: model.default_llm, - logo: model.logo + defaultLlm: model.defaultLlm, + logo: model.logo, + id: model.id } })), ...modelData.map((model) => ({ @@ -114,7 +123,7 @@ const AIFlowChart = () => { type: 'keyCard', position: positions[`${model.id}-keys`], data: { - title: 'API Keys', + title: '', keys: (model.keys || []).map((key, index) => ({ id: key.id, status: key.status, @@ -138,17 +147,17 @@ const AIFlowChart = () => { source: model.id, target: `${model.id}-keys`, label: `${model.key_count} keys`, + data: { id: model.id }, animated: true })) ] - setNodes(newNodes) setEdges(newEdges) }, [modelData]) const calculateExtent = useCallback(() => { - const left = LAYOUT.SERVICE_NODE_X - 100 - const right = LAYOUT.KEY_NODE_X + 100 + const left = LAYOUT.SERVICE_NODE_X + const right = LAYOUT.KEY_NODE_X const top = 0 // Allow slight negative scroll to reduce top padding const bottom = LAYOUT.NODE_START_Y + modelData.length * LAYOUT.NODE_GAP return [ @@ -157,91 +166,91 @@ const AIFlowChart = () => { ] as CoordinateExtent }, [modelData.length]) - const onNodeDrag: any = useCallback( - (_: MouseEvent, node: Node) => { - if (node.type !== 'modelCard') return + const updateProviderOrder = async (sortedProviderIds: string[]) => { + await fetchData('ai/provider/sort', { + method: 'PUT', + body: JSON.stringify({ + providers: sortedProviderIds + }) + }) + } - setNodes((nds) => { - return nds.map((n) => { - if (n.type === 'keyCard' && n.id === `${node.id}-keys`) { + const onNodeDragStop: any = useCallback((_: any, node: Node) => { + if (node.type !== 'modelCard') return + + setNodes((nds) => { + const modelNodes = nds.filter((n) => n.type === 'modelCard') + const sortedNodes = [...modelNodes].sort((a, b) => a.position.y - b.position.y) + const sortedProviderIds = sortedNodes.map((node) => node.id) + + // Update provider order outside of setNodes callback + updateProviderOrder(sortedProviderIds) + // Update all node positions in a single pass + return nds.map((n) => { + if (n.type === 'modelCard') { + const index = sortedNodes.findIndex((sn) => sn.id === n.id) + return { + ...n, + position: { + x: LAYOUT.MODEL_NODE_X, + y: LAYOUT.NODE_START_Y + index * LAYOUT.NODE_GAP + } + } + } + if (n.type === 'keyCard') { + const modelId = n.id.replace('-keys', '') + const modelNode = sortedNodes.find((mn) => mn.id === modelId) + if (modelNode) { + const index = sortedNodes.findIndex((sn) => sn.id === modelId) return { ...n, position: { x: LAYOUT.KEY_NODE_X, - y: node.position.y + y: LAYOUT.NODE_START_Y + index * LAYOUT.NODE_GAP + 16 } } } - return n - }) + } + return n }) - }, - [setNodes] - ) - - const onNodeDragStop: any = useCallback( - (_: any, node: Node) => { - if (node.type !== 'modelCard') return - - setNodes((nds) => { - const modelNodes = nds.filter((n) => n.type === 'modelCard') - const sortedNodes = [...modelNodes].sort((a, b) => a.position.y - b.position.y) - - return nds.map((n) => { - if (n.type === 'modelCard') { - const index = sortedNodes.findIndex((sn) => sn.id === n.id) - return { - ...n, - position: { - x: LAYOUT.MODEL_NODE_X, - y: LAYOUT.NODE_START_Y + index * LAYOUT.NODE_GAP - } - } - } - if (n.type === 'keyCard') { - const modelId = n.id.replace('-keys', '') - const modelNode = sortedNodes.find((mn) => mn.id === modelId) - if (modelNode) { - const index = sortedNodes.findIndex((sn) => sn.id === modelId) - return { - ...n, - position: { - x: LAYOUT.KEY_NODE_X, - y: LAYOUT.NODE_START_Y + index * LAYOUT.NODE_GAP - } - } - } - } - return n - }) - }) - }, - [setNodes] - ) + }) + }, []) return ( -
- +
+ {loading ? ( +
+ +
+ ) : modelData.length === 0 ? ( + +
{$t('未配置 AI 模型')}
+ +
+ ) : ( + + )}
) } diff --git a/frontend/packages/core/src/pages/aiSetting/AIUnconfigure.tsx b/frontend/packages/core/src/pages/aiSetting/AIUnconfigure.tsx new file mode 100644 index 00000000..ba1ae334 --- /dev/null +++ b/frontend/packages/core/src/pages/aiSetting/AIUnconfigure.tsx @@ -0,0 +1,136 @@ +import Icon, { LoadingOutlined } from '@ant-design/icons' +import WithPermission from '@common/components/aoplatform/WithPermission' +import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const' +import { useGlobalContext } from '@common/contexts/GlobalStateContext' +import { useFetch } from '@common/hooks/http' +import { $t } from '@common/locales' +import { App, Button, Card, Empty, Spin, Tag } from 'antd' +import { memo, useEffect, useState } from 'react' +import { useNavigate } from 'react-router-dom' +import { useAiSetting } from './contexts/AiSettingContext' +import { AiSettingListItem } from './types' + +const CardBox = memo(({ provider }: { provider: AiSettingListItem }) => { + const { openConfigModal } = useAiSetting() + const navigate = useNavigate() + + const handleOpenModal = async (provider: AiSettingListItem) => { + await openConfigModal(provider) + navigate('/aisetting?status=configure') + } + + return ( + +
+ + {provider.name} +
+ + {provider.configured ? $t('已配置') : $t('未配置')} + +
+ } + className="shadow-[0_5px_10px_0_rgba(0,0,0,0.05)] rounded-[10px] overflow-visible h-[156px] m-0 flex flex-col " + classNames={{ header: 'border-b-[0px] p-[20px] px-[24px]', body: 'pt-0 flex-1' }} + > +
+
+ {provider.configured && ( + <> + + {provider.defaultLlm} + + )} +
+ + + +
+ + ) +}) +const ModelCardArea = ({ modelList, className }: { modelList: AiSettingListItem[]; className?: string }) => { + return ( + <> + {modelList.length > 0 ? ( +
+ {modelList.map((provider: AiSettingListItem) => ( + + ))} +
+ ) : ( + + )} + + ) +} + +const AIUnConfigure = () => { + const [modelData, setModelData] = useState([]) + const { fetchData } = useFetch() + const [loading, setLoading] = useState(false) + const { aiConfigFlushed } = useGlobalContext() + + useEffect(() => { + setLoading(true) + fetchData[] }>>(`ai/providers/unconfigured`, { + method: 'GET', + eoTransformKeys: ['default_llm', 'default_llm_logo'] + }) + .then((response) => { + const { code, data, msg } = response + if (code === STATUS_CODE.SUCCESS) { + setModelData(data.providers) + } else { + const { message } = App.useApp() + message.error(msg || $t(RESPONSE_TIPS.error)) + } + }) + .finally(() => setLoading(false)) + }, [aiConfigFlushed]) + + return ( + } + spinning={loading} + > + {modelData && modelData.length > 0 ? ( +
+ {modelData.filter((item) => !item.configured).length > 0 && ( + <> + !item.configured) || []} /> + + )} +
+ ) : ( + + )} +
+ ) +} +export default AIUnConfigure diff --git a/frontend/packages/core/src/pages/aiSetting/AiSettingList.tsx b/frontend/packages/core/src/pages/aiSetting/AiSettingList.tsx index 5d2af89a..30694d5a 100644 --- a/frontend/packages/core/src/pages/aiSetting/AiSettingList.tsx +++ b/frontend/packages/core/src/pages/aiSetting/AiSettingList.tsx @@ -1,243 +1,71 @@ -import { LoadingOutlined } from '@ant-design/icons' import InsidePage from '@common/components/aoplatform/InsidePage' -import WithPermission from '@common/components/aoplatform/WithPermission' -import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const' -import { useGlobalContext } from '@common/contexts/GlobalStateContext' -import { useFetch } from '@common/hooks/http' import { $t } from '@common/locales' -import { checkAccess } from '@common/utils/permission' -import { Icon } from '@iconify/react/dist/iconify.js' -import { App, Button, Card, Divider, Empty, Spin, Tag } from 'antd' -import { memo, useEffect, useRef, useState } from 'react' +import { Tabs } from 'antd' +import { useEffect, useState } from 'react' +import { useSearchParams } from 'react-router-dom' import AIFlowChart from './AIFlowChart' -import AiSettingModalContent, { AiSettingModalContentHandle } from './AiSettingModal' +import AIUnConfigure from './AIUnconfigure' +import { AiSettingProvider } from './contexts/AiSettingContext' -export type AiSettingListItem = { - name: string - id: string - logo: string - defaultLlm: string - defaultLlmLogo: string - enable: boolean - configured: boolean -} +const CONTENT_STYLE = { height: 'calc(-300px + 100vh)' } as const -export type AiProviderLlmsItems = { - id: string - logo: string - scopes: ('chat' | 'completions')[] - config: string -} - -export type AiProviderDefaultConfig = { - id: string - provider: string - name: string - logo: string - defaultLlm: string - scopes: string[] -} - -export type AiProviderConfig = { - id: string - name: string - config: string - getApikeyUrl: string -} -const AiSettingList = () => { - const { modal, message } = App.useApp() - const { fetchData } = useFetch() - const [aiSettingList, setAiSettingList] = useState([]) - const [loading, setLoading] = useState(false) - const modalRef = useRef() - const { setAiConfigFlushed, accessData } = useGlobalContext() - - const getAiSettingList = () => { - setLoading(true) - return fetchData[] }>>( - `ai/providers/unconfigured`, - { method: 'GET', eoTransformKeys: ['default_llm', 'default_llm_logo'] } - // eoApiPrefix: 'http://uat.apikit.com:11204/mockApi/aoplatform/api/v1/' - ) - .then((response) => { - const { code, data, msg } = response - if (code === STATUS_CODE.SUCCESS) { - setAiSettingList( - data.providers?.map((x: AiSettingListItem) => ({ - ...x, - name: $t(x.name), - llmListStatus: 'unload', - availableLlms: [] - })) - ) - } else { - message.error(msg || $t(RESPONSE_TIPS.error)) - } - }) - .finally(() => setLoading(false)) - } - - const openModal = async (entity: AiSettingListItem) => { - message.loading($t(RESPONSE_TIPS.loading)) - const { code, data, msg } = await fetchData>('ai/provider/config', { - method: 'GET', - eoParams: { provider: entity!.id }, - eoTransformKeys: ['get_apikey_url'] - }) - message.destroy() - if (code !== STATUS_CODE.SUCCESS) { - message.error(msg || $t(RESPONSE_TIPS.error)) - return - } - modal.confirm({ - title: $t('模型配置'), - content: ( - - ), - onOk: () => { - return modalRef.current?.save().then((res) => { - if (res === true) setAiConfigFlushed(true) - getAiSettingList() - }) - }, - width: 600, - okText: $t('确认'), - footer: (_, { OkBtn, CancelBtn }) => { - return ( -
- - {$t('从 (0) 获取 API KEY', [data.provider.name])} - - -
- - {checkAccess('system.devops.ai_provider.edit', accessData) ? : null} -
-
- ) - }, - cancelText: $t('取消'), - closable: true, - icon: <> - }) - } +const AiSettingContent = () => { + const [searchParams, setSearchParams] = useSearchParams() + const [activeKey, setActiveKey] = useState(searchParams.get('status') === 'unconfigure' ? 'config' : 'flow') useEffect(() => { - getAiSettingList() - }, []) - - const CardBox = memo(({ provider }: { provider: AiSettingListItem }) => { - return ( - -
- - {provider.name} -
- - {provider.configured ? $t('已配置') : $t('未配置')} - - - } - className="shadow-[0_5px_10px_0_rgba(0,0,0,0.05)] rounded-[10px] overflow-visible h-[156px] m-0 flex flex-col " - classNames={{ header: 'border-b-[0px] p-[20px] px-[24px]', body: 'pt-0 flex-1' }} - > -
-
- {provider.configured && ( - <> - - {provider.defaultLlm} - - )} -
- - - -
-
- ) - }) - - const ModelCardArea = ({ modelList, className }: { modelList: AiSettingListItem[]; className?: string }) => { - return ( - <> - {modelList.length > 0 ? ( -
- {modelList.map((provider: AiSettingListItem) => ( - - ))} -
- ) : ( - - )} - - ) - } + const newActiveKey = searchParams.get('status') === 'unconfigure' ? 'config' : 'flow' + setActiveKey(newActiveKey) + }, [searchParams]) return ( - <> - - - } - spinning={loading} - > - {aiSettingList && aiSettingList.length > 0 ? ( -
- {aiSettingList.filter((item) => !item.configured).length > 0 && ( - <> - -

{$t('未配置')}

- !item.configured) || []} /> - - )} -
- ) : ( - - )} -
-
- + +
+ { + setActiveKey(key) + setSearchParams({ status: key === 'config' ? 'unconfigure' : 'configure' }) + }} + className="sticky top-0 flex-shrink-0" + items={[ + { + key: 'flow', + label: $t('已设置'), + children: ( +
+ +
+ ) + }, + { + key: 'config', + label: $t('未设置'), + children: ( +
+ +
+ ) + } + ]} + /> +
+
) } + +const AiSettingList = () => { + return ( + + + + ) +} + export default AiSettingList diff --git a/frontend/packages/core/src/pages/aiSetting/AiSettingModal.tsx b/frontend/packages/core/src/pages/aiSetting/AiSettingModal.tsx index 862a7d71..9527d009 100644 --- a/frontend/packages/core/src/pages/aiSetting/AiSettingModal.tsx +++ b/frontend/packages/core/src/pages/aiSetting/AiSettingModal.tsx @@ -1,13 +1,14 @@ +import { QuestionCircleOutlined } from '@ant-design/icons' import { Codebox } from '@common/components/postcat/api/Codebox' import { BasicResponse, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const' import { useFetch } from '@common/hooks/http' import { $t } from '@common/locales' -import { App, Form, Select, Tag } from 'antd' +import { App, Form, InputNumber, Select, Switch, Tag, Tooltip } from 'antd' import { forwardRef, useEffect, useImperativeHandle, useState } from 'react' -import { AiProviderConfig, AiProviderLlmsItems } from './AiSettingList' +import { AiProviderLlmsItems, ModelDetailData } from './types' export type AiSettingModalContentProps = { - entity: AiProviderConfig & { defaultLlm: string } + entity: ModelDetailData & { defaultLlm: string } readOnly: boolean } @@ -15,11 +16,6 @@ export type AiSettingModalContentHandle = { save: () => Promise } -type AiSettingModalContentField = { - config: string - defaultLlm: string -} - const AiSettingModalContent = forwardRef((props, ref) => { const [form] = Form.useForm() const { message } = App.useApp() @@ -27,7 +23,7 @@ const AiSettingModalContent = forwardRef() const [loading, setLoading] = useState(false) - + const [enableState, setEnableState] = useState(entity.status === 'enabled') const getLlmList = () => { setLoading(true) fetchData>(`ai/provider/llms`, { @@ -52,12 +48,16 @@ const AiSettingModalContent = forwardRef { + const finalValue = { + ...value, + priority: Math.max(1, value.priority) + } + fetchData>('ai/provider/config', { method: 'PUT', eoParams: { provider: entity?.id }, - eoBody: value, + eoBody: finalValue, eoTransformKeys: ['defaultLlm'] + // eoApiPrefix: 'http://uat.apikit.com:11204/mockApi/aoplatform/api/v1/' }) .then((response) => { const { code, msg } = response @@ -89,21 +95,29 @@ const AiSettingModalContent = forwardRef { + if (!isChecked) { + return '保存后供应商状态变为【停用】,使用本供应商的 API 将临时使用负载优先级最高的正常供应商。' + } + return '保存后供应商状态变为【正常】,恢复调用本供应商的 AI 能力。' + } + useImperativeHandle(ref, () => ({ save })) return (
- label={$t('模型')} name="defaultLlm" rules={[{ required: true }]}> + label={$t('默认模型')} name="defaultLlm" rules={[{ required: true }]}> - label={$t('参数')} name="config"> + + label={ + + {$t('负载优先级')} + + + + + } + name="priority" + rules={[ + { required: true }, + { + validator: async (_, value) => { + if (value <= 0) { + throw new Error($t('优先级必须大于 0')) + } + return Promise.resolve() + } + } + ]} + initialValue={1} + > + + + + label={$t('API Key(默认 Key)')} name="config"> + + {entity.configured && ( + +
+
+ 当前调用状态: + {entity.status === 'enabled' && {$t('正常')}} + {entity.status === 'disabled' && {$t('停用')}} + {entity.status === 'abnormal' && {$t('异常')}} +
+ + { + form.setFieldsValue({ enable: checked }) + setEnableState(checked) + }} + /> + +
+ {(entity.status === 'enabled' && !enableState) || (entity.status !== 'enabled' && enableState) ? ( +
* {getTooltipText(enableState)}
+ ) : null} +
+ )} ) }) diff --git a/frontend/packages/core/src/pages/aiSetting/components/CustomEdge.tsx b/frontend/packages/core/src/pages/aiSetting/components/CustomEdge.tsx index f3cbff89..c4afd894 100644 --- a/frontend/packages/core/src/pages/aiSetting/components/CustomEdge.tsx +++ b/frontend/packages/core/src/pages/aiSetting/components/CustomEdge.tsx @@ -31,7 +31,7 @@ export default function CustomEdge({ {label && ( = ({ data }) = style={{ border: '1px solid var(--border-color)' }} > -
+
{title}
= ({ data }) => { - const { title, status, defaultModel, logo } = data + const { name, status, defaultLlm, logo } = data + const { openConfigModal } = useAiSetting() + + const getStatusIcon = (status: ModelStatus) => { + switch (status) { + case 'enabled': + return { icon: 'mdi:check-circle', color: 'text-green-500' } + case 'disabled': + return { icon: 'mdi:pause-circle', color: 'text-gray-400' } + case 'abnormal': + return { icon: 'mdi:alert-circle', color: 'text-red-500' } + } + } + + const statusConfig = getStatusIcon(status) + return (
@@ -28,17 +37,14 @@ export const ModelCardNode: React.FC<{ data: ModelCardNodeData }> = ({ data }) =
-
+
- {title} - + {name} +
{/* Action buttons */} @@ -46,13 +52,15 @@ export const ModelCardNode: React.FC<{ data: ModelCardNodeData }> = ({ data }) = console.log('Default:', data.id)} + onClick={() => { + openConfigModal({ id: data.id, defaultLlm: defaultLlm } as AiSettingListItem) + }} />
{t('默认:')} - {defaultModel} + {defaultLlm}
diff --git a/frontend/packages/core/src/pages/aiSetting/components/ServiceCardNode.tsx b/frontend/packages/core/src/pages/aiSetting/components/ServiceCardNode.tsx index b2237fd3..176d80da 100644 --- a/frontend/packages/core/src/pages/aiSetting/components/ServiceCardNode.tsx +++ b/frontend/packages/core/src/pages/aiSetting/components/ServiceCardNode.tsx @@ -11,7 +11,7 @@ export const ServiceCardNode: React.FC = () => {
- AI Service + AI Services
) diff --git a/frontend/packages/core/src/pages/aiSetting/constants.ts b/frontend/packages/core/src/pages/aiSetting/constants.ts index 9da8d084..a6635ffe 100644 --- a/frontend/packages/core/src/pages/aiSetting/constants.ts +++ b/frontend/packages/core/src/pages/aiSetting/constants.ts @@ -1,5 +1,5 @@ export const LAYOUT = { - SERVICE_NODE_X: 50, + SERVICE_NODE_X: 0, NODE_START_Y: 20, NODE_GAP: 120, MODEL_NODE_X: 500, diff --git a/frontend/packages/core/src/pages/aiSetting/contexts/AiSettingContext.tsx b/frontend/packages/core/src/pages/aiSetting/contexts/AiSettingContext.tsx new file mode 100644 index 00000000..f8e078bd --- /dev/null +++ b/frontend/packages/core/src/pages/aiSetting/contexts/AiSettingContext.tsx @@ -0,0 +1,89 @@ +import Icon from '@ant-design/icons' +import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const' +import { useGlobalContext } from '@common/contexts/GlobalStateContext' +import { useFetch } from '@common/hooks/http' +import { $t } from '@common/locales' +import { checkAccess } from '@common/utils/permission' +import { App } from 'antd' +import { createContext, useContext, useRef } from 'react' +import AiSettingModalContent, { AiSettingModalContentHandle } from '../AiSettingModal' +import { AiSettingListItem, ModelDetailData } from '../types' + +interface AiSettingContextType { + openConfigModal: (entity: AiSettingListItem) => Promise +} + +const AiSettingContext = createContext(undefined) + +export const AiSettingProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const { modal, message } = App.useApp() + const { fetchData } = useFetch() + const { aiConfigFlushed, setAiConfigFlushed, accessData } = useGlobalContext() + const modalRef = useRef() + + const openConfigModal = async (entity: AiSettingListItem) => { + message.loading($t(RESPONSE_TIPS.loading)) + const { code, data, msg } = await fetchData>('ai/provider/config', { + method: 'GET', + eoParams: { provider: entity!.id }, + eoTransformKeys: ['get_apikey_url'] + }) + message.destroy() + if (code !== STATUS_CODE.SUCCESS) { + message.error(msg || $t(RESPONSE_TIPS.error)) + return + } + + modal.confirm({ + title: $t('模型配置'), + content: ( + + ), + onOk: () => { + return modalRef.current?.save().then((res) => { + if (res === true) { + setAiConfigFlushed(!aiConfigFlushed) + } + }) + }, + width: 600, + okText: $t('确认'), + footer: (_, { OkBtn, CancelBtn }) => { + return ( +
+ + {$t('从 (0) 获取 API KEY', [data.provider.name])} + + +
+ + {checkAccess('system.devops.ai_provider.edit', accessData) ? : null} +
+
+ ) + }, + cancelText: $t('取消'), + closable: true, + icon: <> + }) + } + + return {children} +} + +export const useAiSetting = () => { + const context = useContext(AiSettingContext) + if (!context) { + throw new Error('useAiSetting must be used within an AiSettingProvider') + } + return context +} diff --git a/frontend/packages/core/src/pages/aiSetting/styles.css b/frontend/packages/core/src/pages/aiSetting/styles.css index e12ed580..13d5b62b 100644 --- a/frontend/packages/core/src/pages/aiSetting/styles.css +++ b/frontend/packages/core/src/pages/aiSetting/styles.css @@ -1,4 +1,20 @@ /* Flow Chart Styles */ +.react-flow { + width: 100%; + height: 100%; + min-height: 500px; +} + +.react-flow__container { + width: 100%; + height: 100%; +} + +.react-flow__renderer { + width: 100%; + height: 100%; +} + .react-flow__node { padding: 0; border-radius: 8px; diff --git a/frontend/packages/core/src/pages/aiSetting/types.ts b/frontend/packages/core/src/pages/aiSetting/types.ts index 4935473e..6ba2ea7a 100644 --- a/frontend/packages/core/src/pages/aiSetting/types.ts +++ b/frontend/packages/core/src/pages/aiSetting/types.ts @@ -1,4 +1,4 @@ -export type ModelStatus = 'enable' | 'abnormal'|'disabled' +export type ModelStatus = 'enabled' | 'abnormal'|'disabled' export type KeyStatus ='normal' | 'abnormal'|'disabled' export interface KeyData { @@ -7,14 +7,51 @@ export interface KeyData { status: KeyStatus, } -export interface ModelData { +export interface ModelListData { id: string name: string logo: string - default_llm: string + defaultLlm: string status: ModelStatus api_count: number key_count: number keys: KeyData[] +} +export interface ModelDetailData extends ModelListData{ + enable:boolean + config: string, + priority?: number + getApikeyUrl: string + status: ModelStatus + configured: boolean +} + + +export type AiSettingListItem = { + name: string + id: string + logo: string + defaultLlm: string + defaultLlmLogo: string + enable: boolean + configured: boolean priority?: number } + +export type AiProviderLlmsItems = { + id: string + logo: string + scopes: ('chat' | 'completions')[] + config: string +} + +export type AiProviderDefaultConfig = { + id: string + provider: string + name: string + logo: string + defaultLlm: string + scopes: string[] +} + + diff --git a/frontend/packages/core/src/pages/keySettings/index.tsx b/frontend/packages/core/src/pages/keySettings/index.tsx index 58e8dc93..6ea97505 100644 --- a/frontend/packages/core/src/pages/keySettings/index.tsx +++ b/frontend/packages/core/src/pages/keySettings/index.tsx @@ -12,15 +12,16 @@ import AIProviderSelect, { AIProvider } from '@core/components/AIProviderSelect' import { App, Divider, Space, Typography } from 'antd' import dayjs from 'dayjs' import React, { useEffect, useRef, useState } from 'react' +import { useSearchParams } from 'react-router-dom' import ApiKeyContent from './components/ApiKeyContent' import { APIKey, EditAPIKey } from './types' const KeySettings: React.FC = () => { const pageListRef = useRef(null) const { modal, message } = App.useApp() - const [selectedProvider, setSelectedProvider] = useState() + const [searchParams] = useSearchParams() + const [selectedProvider, setSelectedProvider] = useState(searchParams.get('modelId') || '') const [provider, setProvider] = useState() - const [apiKeys, setApiKeys] = useState([]) const { fetchData } = useFetch() const [searchWord, setSearchWord] = useState('') const [total, setTotal] = useState(0) @@ -186,6 +187,8 @@ const KeySettings: React.FC = () => { page_size: params.pageSize, keyword: searchWord, page: params.current + //TODO API 筛选 + // statuses: params.statuses || [] } // eoApiPrefix: 'http://uat.apikit.com:11204/mockApi/aoplatform/api/v1/' }) @@ -306,7 +309,7 @@ const KeySettings: React.FC = () => { render: (dom: React.ReactNode, entity: APIKey) => { return entity.expire_time === 0 ? $t('永不过期') - : dayjs(Number(entity.expire_time)).format('YYYY-MM-DD HH:mm:ss') + : dayjs(Number(entity.expire_time) * 1000).format('YYYY-MM-DD HH:mm:ss') } }, ...operation diff --git a/frontend/packages/dashboard/src/component/MonitorApiPage.tsx b/frontend/packages/dashboard/src/component/MonitorApiPage.tsx index 3a824368..5bb5d2d4 100644 --- a/frontend/packages/dashboard/src/component/MonitorApiPage.tsx +++ b/frontend/packages/dashboard/src/component/MonitorApiPage.tsx @@ -1,23 +1,23 @@ import { CloseOutlined, ExpandOutlined, SearchOutlined } from '@ant-design/icons' -import { Select, Input, Button, App, Drawer } from 'antd' -import { debounce } from 'lodash-es' -import { useState, useEffect, useRef } from 'react' -import { MonitorApiData, SearchBody } from '@dashboard/const/type' -import { getTime } from '../utils/dashboard' import ScrollableSection from '@common/components/aoplatform/ScrollableSection' import TimeRangeSelector, { RangeValue, TimeRange, TimeRangeButton } from '@common/components/aoplatform/TimeRangeSelector' -import MonitorTable, { MonitorTableHandler } from './MonitorTable' import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const' -import { DefaultOptionType } from 'antd/es/select' -import { useExcelExport } from '@common/hooks/excel' -import { API_TABLE_GLOBAL_COLUMNS_CONFIG } from '@dashboard/const/const' -import { useFetch } from '@common/hooks/http' import { EntityItem } from '@common/const/type' +import { useExcelExport } from '@common/hooks/excel' +import { useFetch } from '@common/hooks/http' import { $t } from '@common/locales' +import { API_TABLE_GLOBAL_COLUMNS_CONFIG } from '@dashboard/const/const' +import { MonitorApiData, SearchBody } from '@dashboard/const/type' +import { App, Button, Drawer, Input, Select } from 'antd' +import { DefaultOptionType } from 'antd/es/select' +import { debounce } from 'lodash-es' +import { useEffect, useRef, useState } from 'react' +import { getTime } from '../utils/dashboard' +import MonitorTable, { MonitorTableHandler } from './MonitorTable' export type MonitorApiPageProps = { fetchTableData: (body: SearchBody) => Promise> detailDrawerContent: React.ReactNode @@ -100,6 +100,7 @@ export default function MonitorApiPage(props: MonitorApiPageProps) { const getMonitorData = () => { let query = queryData if (!queryData || queryData.start === undefined) { + console.log(timeButton, datePickerValue) const { startTime, endTime } = getTime(timeButton, datePickerValue || []) query = { ...query, start: startTime, end: endTime } } @@ -186,7 +187,7 @@ export default function MonitorApiPage(props: MonitorApiPageProps) { } return ( -
+
-
- +
+ ({ ...(prevData || {}), apis: value })) }} /> - +
{/* setQueryData({ ...queryData, path: '' })} /> */}