diff --git a/frontend/packages/common/src/components/aoplatform/LanguageSetting.tsx b/frontend/packages/common/src/components/aoplatform/LanguageSetting.tsx index c54bc0a4..90a20efa 100644 --- a/frontend/packages/common/src/components/aoplatform/LanguageSetting.tsx +++ b/frontend/packages/common/src/components/aoplatform/LanguageSetting.tsx @@ -1,8 +1,8 @@ -import { Dropdown, Button } from 'antd' -import i18n from '@common/locales' -import { memo, useEffect, useMemo } from 'react' import { useGlobalContext } from '@common/contexts/GlobalStateContext' +import i18n from '@common/locales' import { Icon } from '@iconify/react/dist/iconify.js' +import { Button, Dropdown } from 'antd' +import { memo, useEffect, useMemo } from 'react' const LanguageSetting = ({ mode = 'light' }: { mode?: 'dark' | 'light' }) => { const { dispatch, state } = useGlobalContext() @@ -48,12 +48,17 @@ const LanguageSetting = ({ mode = 'light' }: { mode?: 'dark' | 'light' }) => { const langLabel = useMemo(() => items.find((item) => item?.key === state.language)?.title, [state.language]) useEffect(() => { - const savedLang = sessionStorage.getItem('i18nextLng') - const browserLang = navigator.language || navigator.userLanguage - if (savedLang) return - - dispatch({ type: 'UPDATE_LANGUAGE', language: browserLang }) + const savedLang = i18n.language || sessionStorage.getItem('i18nextLng') + if (savedLang && state.language !== savedLang) { + dispatch({ type: 'UPDATE_LANGUAGE', language: savedLang }) + } else if (!savedLang) { + const browserLang = navigator.language + const supportedLang = items.find((item) => item.key === browserLang) ? browserLang : 'zh-CN' + dispatch({ type: 'UPDATE_LANGUAGE', language: supportedLang }) + i18n.changeLanguage(supportedLang) + } }, []) + return ( { const { key } = e dispatch({ type: 'UPDATE_LANGUAGE', language: key }) i18n.changeLanguage(key) + sessionStorage.setItem('i18nextLng', key) } }} > diff --git a/frontend/packages/common/src/components/aoplatform/TableBtnWithPermission.tsx b/frontend/packages/common/src/components/aoplatform/TableBtnWithPermission.tsx index 7c938271..b951a7c5 100644 --- a/frontend/packages/common/src/components/aoplatform/TableBtnWithPermission.tsx +++ b/frontend/packages/common/src/components/aoplatform/TableBtnWithPermission.tsx @@ -1,10 +1,10 @@ -import { Button, Tooltip } from 'antd' -import { useState, useMemo, useEffect, useCallback } from 'react' -import { useGlobalContext } from '@common/contexts/GlobalStateContext' -import { useNavigate } from 'react-router-dom' import { PERMISSION_DEFINITION } from '@common/const/permissions' -import { Icon } from '@iconify/react/dist/iconify.js' +import { useGlobalContext } from '@common/contexts/GlobalStateContext' import { $t } from '@common/locales' +import { Icon } from '@iconify/react/dist/iconify.js' +import { Button, Tooltip } from 'antd' +import { useCallback, useEffect, useMemo, useState } from 'react' +import { useNavigate } from 'react-router-dom' type TableBtnWithPermissionProps = { btnTitle: string @@ -25,6 +25,7 @@ const TableIconName = { copy: 'ic:baseline-file-copy', view: 'ic:baseline-remove-red-eye', publish: 'ic:baseline-publish', + offline: 'ic:baseline-file-download-off', approval: 'ic:baseline-approval', stop: 'ic:baseline-stop-circle', online: 'ic:baseline-check-circle', @@ -86,7 +87,7 @@ const TableBtnWithPermission = ({ + + ) : ( + + )} ) } 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..a81a4c7d 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 $t('保存后供应商状态变为【停用】,使用本供应商的 API 将临时使用负载优先级最高的正常供应商。') + } + return $t('保存后供应商状态变为【正常】,恢复调用本供应商的 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 && ( + +
+
+ {$t('当前调用状态:')} + {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..35d585f5 100644 --- a/frontend/packages/core/src/pages/aiSetting/components/CustomEdge.tsx +++ b/frontend/packages/core/src/pages/aiSetting/components/CustomEdge.tsx @@ -1,4 +1,4 @@ -import { BaseEdge, EdgeLabelRenderer, EdgeProps, getSmoothStepPath } from '@xyflow/react' +import { BaseEdge, EdgeLabelRenderer, EdgeProps, getSmoothStepPath, useStore } from '@xyflow/react' export default function CustomEdge({ id, @@ -11,14 +11,26 @@ export default function CustomEdge({ style = {}, markerEnd, label, - data + data, + source, + target }: EdgeProps) { - const [edgePath, labelX, labelY] = getSmoothStepPath({ + // Get all edges to check for duplicates + const edges = useStore((state) => state.edges) + + // Find duplicate edges between the same source and target + const duplicateEdges = edges.filter((edge) => edge.source === source && edge.target === target) + const edgeIndex = duplicateEdges.findIndex((edge) => edge.id === id) + + // Adjust the path if this is a duplicate edge + const offset = edgeIndex * 20 // 20px offset for each duplicate edge + + const [edgePath] = getSmoothStepPath({ sourceX, - sourceY, + sourceY: sourceY, sourcePosition, targetX, - targetY, + targetY: targetY + offset, targetPosition, borderRadius: 16 }) @@ -27,24 +39,29 @@ export default function CustomEdge({ return ( <> - + {label && ( {label} diff --git a/frontend/packages/core/src/pages/aiSetting/components/KeyStatusNode.tsx b/frontend/packages/core/src/pages/aiSetting/components/KeyStatusNode.tsx index 5f56e782..5420e9fe 100644 --- a/frontend/packages/core/src/pages/aiSetting/components/KeyStatusNode.tsx +++ b/frontend/packages/core/src/pages/aiSetting/components/KeyStatusNode.tsx @@ -22,7 +22,7 @@ export const KeyStatusNode: React.FC<{ data: KeyStatusNodeData }> = ({ data }) = style={{ border: '1px solid var(--border-color)' }} > -
+
{title}
= ({ data }) => { - const { title, status, defaultModel, logo } = data + const { name, status, defaultLlm, logo, alternativeModel } = 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 +38,14 @@ export const ModelCardNode: React.FC<{ data: ModelCardNodeData }> = ({ data }) =
-
+
- {title} - + {name} +
{/* Action buttons */} @@ -46,14 +53,21 @@ export const ModelCardNode: React.FC<{ data: ModelCardNodeData }> = ({ data }) = console.log('Default:', data.id)} + onClick={() => { + openConfigModal({ id: data.id, defaultLlm: defaultLlm } as AiSettingListItem) + }} />
- {t('默认:')} - {defaultModel} + {$t('默认:')} + {defaultLlm}
+ {status !== 'enabled' && alternativeModel && ( +
+ {$t('关联 API 已转用')} {alternativeModel.name}/{alternativeModel.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..52ec18d8 100644 --- a/frontend/packages/core/src/pages/aiSetting/constants.ts +++ b/frontend/packages/core/src/pages/aiSetting/constants.ts @@ -1,7 +1,7 @@ export const LAYOUT = { - SERVICE_NODE_X: 50, + SERVICE_NODE_X: 0, NODE_START_Y: 20, NODE_GAP: 120, - MODEL_NODE_X: 500, - KEY_NODE_X: 900, + MODEL_NODE_X: 700, + KEY_NODE_X: 1200, } as const; 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/components/ApiKeyContent.tsx b/frontend/packages/core/src/pages/keySettings/components/ApiKeyContent.tsx index 0ce3c4a4..a303ce36 100644 --- a/frontend/packages/core/src/pages/keySettings/components/ApiKeyContent.tsx +++ b/frontend/packages/core/src/pages/keySettings/components/ApiKeyContent.tsx @@ -25,7 +25,7 @@ const ApiKeyContent: React.FC = forwardRef(({ provider, enti setNeverExpire(isNeverExpire) form.setFieldsValue({ name: entity.name, - expire_time: isNeverExpire ? undefined : dayjs(entity.expire_time), + expire_time: isNeverExpire ? undefined : dayjs(entity.expire_time * 1000), config: entity.config }) } catch (e) { @@ -41,7 +41,7 @@ const ApiKeyContent: React.FC = forwardRef(({ provider, enti try { const values = await form.validateFields() const { expire_time, ...restValues } = values - const expireTime = neverExpire ? 0 : expire_time.valueOf() + const expireTime = neverExpire ? 0 : Math.trunc(expire_time.valueOf() / 1000) const response = await fetchData>('ai/resource/key', { method: entity.id ? 'PUT' : 'POST', diff --git a/frontend/packages/core/src/pages/keySettings/components/StatusFilter.tsx b/frontend/packages/core/src/pages/keySettings/components/StatusFilter.tsx index 64cc18e9..3663e12a 100644 --- a/frontend/packages/core/src/pages/keySettings/components/StatusFilter.tsx +++ b/frontend/packages/core/src/pages/keySettings/components/StatusFilter.tsx @@ -11,22 +11,22 @@ 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 } + { label: $t('正常'), value: 'normal', color: token.colorSuccess }, + { label: $t('超额'), value: 'exceeded', color: token.colorError }, + { label: $t('过期'), value: 'expired', color: token.colorWarning }, + { label: $t('停用'), value: 'disabled', color: token.colorTextDisabled }, + { label: $t('错误'), value: 'error', color: token.colorError } ] return ( - {$t('Status')}: + {$t('状态')}: { + return { label: $t(value), value: key } + })} + /> + ) + } } - return (form.setFieldsValue({})) - }, []); - - - const translatedMatchConfig = useMemo(()=>{ - return MATCH_CONFIG.map((item)=>{ - if(item.key === 'position'){ - return ({...item,component:{ - return { label:$t(value), value:key} - })}/>}) - } - return {...item} - }) + if (item.key === 'matchType') { + return { + ...item, + component: ( + - - - - - - { - if((e.target.value as string).endsWith('/*')){ - form.setFieldValue('path',e.target.value.slice(0,-2)) - form.setFieldValue('pathMatch','prefix') - } - }}/> - - - + + +
+ } + > + } spinning={loading} className=""> +
+
+ + {' '} + + {$t('API 基础信息')} + + + + label={$t('拦截该接口的请求')} + name="disable" + extra={$t('开启拦截后,网关会拦截所有该路径的请求,相当于防火墙禁用了特定路径的访问。')} + > + + - - label={$t("请求方式")} - name="methods" - rules={[{ required: true }]} - > - - + label={$t('请求协议')} name="protocols" rules={[{ required: true }]}> + + + + + + { + if ((e.target.value as string).endsWith('/*')) { + form.setFieldValue('path', e.target.value.slice(0, -2)) + form.setFieldValue('pathMatch', 'prefix') + } + }} + /> + + + - - label={$t("描述")} - name="description" - > - - + label={$t('请求方式')} name="methods" rules={[{ required: true }]}> + + - - label={$t("高级匹配")} - name="match" - > - - configFields={translatedMatchConfig} - /> - + label={$t('描述')} name="description"> + + - {$t('转发规则设置')} - - className="mb-0 bg-transparent border-none p-0" - name="proxy" - > - - -
-
-
- + label={$t('高级匹配')} name="match"> + configFields={translatedMatchConfig} /> + + + + + {$t('转发规则设置')} + + + className="p-0 mb-0 bg-transparent border-none" name="proxy"> + + +
+ + + ) -}) -export default SystemInsideRouterCreate \ No newline at end of file + } +) +export default SystemInsideRouterCreate 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: '' })} /> */}