feat: ai model

This commit is contained in:
scarqin
2024-12-30 15:59:43 +08:00
parent f6d6920cfb
commit c7b16e0ea9
7 changed files with 189 additions and 203 deletions
@@ -1,4 +1,3 @@
import Icon from '@ant-design/icons'
import { ActionType } from '@ant-design/pro-components'
import InsidePage from '@common/components/aoplatform/InsidePage'
import PageList, { PageProColumns } from '@common/components/aoplatform/PageList'
@@ -7,14 +6,12 @@ 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 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'
import { APIKey } from './types'
const KeySettings: React.FC = () => {
const pageListRef = useRef<ActionType>(null)
@@ -33,80 +30,9 @@ const KeySettings: React.FC = () => {
pageListRef.current?.reload()
}, [selectedProvider])
const handleEdit = (record: APIKey) => {
openModal(record)
}
const handleEdit = (record: APIKey) => {}
const handleAdd = () => {
openModal()
}
const openModal = async (entity?: EditAPIKey) => {
if (!provider) return
const mode = entity ? 'edit' : 'add'
if (mode === 'edit') {
message.loading($t(RESPONSE_TIPS.loading))
const { code, data, msg } = await fetchData<BasicResponse<{ info: EditAPIKey }>>('ai/resource/key', {
method: 'GET',
eoParams: { provider: selectedProvider, id: entity!.id }
// eoApiPrefix: 'http://uat.apikit.com:11204/mockApi/aoplatform/api/v1/'
})
message.destroy()
if (code !== STATUS_CODE.SUCCESS) {
message.error(msg || $t(RESPONSE_TIPS.error))
return
}
entity = data?.info
} else {
entity = {
name: `key${total}`,
config: provider.default_config,
expire_time: 0
} as EditAPIKey
}
const newEntity = entity as EditAPIKey
modal.confirm({
title: mode === 'add' ? $t(`添加 ${provider?.name} APIKey`) : $t('编辑 APIKey'),
content: <ApiKeyContent ref={modalRef} entity={newEntity} provider={provider} />,
onOk: () => {
return new Promise((resolve, reject) => {
modalRef.current?.handleOk().then((res: boolean) => {
if (res === true) {
pageListRef.current?.reload()
resolve(res)
return
}
reject()
})
})
},
width: 600,
okText: $t('确认'),
footer: (_, { OkBtn, CancelBtn }) => {
return (
<div className="flex justify-between items-center">
<a
target="_blank"
rel="noopener noreferrer"
href={provider.getApikeyUrl}
className="flex items-center gap-[8px]"
>
<span>{$t('从 (0) 获取 API KEY', [provider.name])}</span>
<Icon icon="ic:baseline-open-in-new" width={16} height={16} />
</a>
<div>
<CancelBtn />
{checkAccess('system.settings.ai_key_resource.manager', accessData) ? <OkBtn /> : null}
</div>
</div>
)
},
cancelText: $t('取消'),
closable: true,
icon: <></>
})
}
const handleAdd = () => {}
const handleDelete = async (id: string) => {
try {
@@ -20,8 +20,9 @@ import { KeyStatusNode } from './components/KeyStatusNode'
import { ModelCardNode } from './components/ModelCardNode'
import { ServiceCardNode } from './components/NodeComponents'
import { LAYOUT } from './constants'
import { useAiSetting } from './contexts/AiSettingContext'
import './styles.css'
import { AiSettingListItem, ModelData } from './types'
import { ModelData } from './types'
export type ApiResponse = BasicResponse<{
backup: {
@@ -61,17 +62,16 @@ const edgeTypes: EdgeTypes = {
custom: CustomEdge
}
const AIFlowChart = ({ openModal }: { openModal: (entity: AiSettingListItem) => Promise<void> }) => {
const AIFlowChart = () => {
const [modelData, setModelData] = useState<ModelData[]>([])
const [nodes, setNodes, onNodesChange] = useNodesState<Node>([])
const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([])
const { fetchData } = useFetch()
const { openConfigModal } = useAiSetting()
useEffect(() => {
// Mock API call - replace with actual API call
fetchData<ApiResponse>('ai/providers/configured', {
method: 'GET'
// eoApiPrefix: 'http://uat.apikit.com:11204/mockApi/aoplatform/api/v1/'
}).then((response) => {
const mockApiResponse: ApiResponse = response as ApiResponse
setModelData(mockApiResponse.data.providers)
@@ -105,7 +105,7 @@ const AIFlowChart = ({ openModal }: { openModal: (entity: AiSettingListItem) =>
defaultLlm: model.default_llm,
logo: model.logo,
id: model.id,
openModal
openModal: openConfigModal
}
})),
...modelData.map((model) => ({
@@ -4,7 +4,8 @@ import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
import { useFetch } from '@common/hooks/http'
import { $t } from '@common/locales'
import { App, Button, Card, Empty, Spin, Tag } from 'antd'
import { FC, memo, useEffect, useState } from 'react'
import { memo, useEffect, useState } from 'react'
import { useAiSetting } from './contexts/AiSettingContext'
import { AiSettingListItem } from './types'
const CardBox = memo(
@@ -92,26 +93,23 @@ const ModelCardArea = ({
</>
)
}
interface AIUnConfigureProps {
openModal: (entity: AiSettingListItem) => Promise<void>
}
const AIUnConfigure: FC<AIUnConfigureProps> = ({ openModal }) => {
const { message } = App.useApp()
const AIUnConfigure = () => {
const [modelData, setModelData] = useState<AiSettingListItem[]>([])
const { fetchData } = useFetch()
const [aiSettingList, setAiSettingList] = useState<AiSettingListItem[]>([])
const { openConfigModal } = useAiSetting()
const [loading, setLoading] = useState<boolean>(false)
const getAiSettingList = () => {
useEffect(() => {
setLoading(true)
return fetchData<BasicResponse<{ providers: Omit<AiSettingListItem, 'availableLlms' | 'llmListStatus'>[] }>>(
fetchData<BasicResponse<{ providers: Omit<AiSettingListItem, 'availableLlms' | 'llmListStatus'>[] }>>(
`ai/providers/unconfigured`,
{ method: 'GET', eoTransformKeys: ['default_llm', 'default_llm_logo'] }
)
.then((response) => {
const { code, data, msg } = response
if (code === STATUS_CODE.SUCCESS) {
setAiSettingList(
setModelData(
data.providers?.map((x: AiSettingListItem) => ({
...x,
name: $t(x.name),
@@ -120,14 +118,11 @@ const AIUnConfigure: FC<AIUnConfigureProps> = ({ openModal }) => {
}))
)
} else {
const { message } = App.useApp()
message.error(msg || $t(RESPONSE_TIPS.error))
}
})
.finally(() => setLoading(false))
}
useEffect(() => {
getAiSettingList()
}, [])
return (
@@ -135,13 +130,16 @@ const AIUnConfigure: FC<AIUnConfigureProps> = ({ openModal }) => {
className="h-full"
wrapperClassName="h-full pr-PAGE_INSIDE_X"
indicator={<LoadingOutlined style={{ fontSize: 24 }} spin />}
spinning={loading}
spinning={modelData.length === 0}
>
{aiSettingList && aiSettingList.length > 0 ? (
{modelData && modelData.length > 0 ? (
<div>
{aiSettingList.filter((item) => !item.configured).length > 0 && (
{modelData.filter((item) => !item.configured).length > 0 && (
<>
<ModelCardArea openModal={openModal} modelList={aiSettingList.filter((item) => !item.configured) || []} />
<ModelCardArea
openModal={openConfigModal}
modelList={modelData.filter((item) => !item.configured) || []}
/>
</>
)}
</div>
@@ -151,5 +149,4 @@ const AIUnConfigure: FC<AIUnConfigureProps> = ({ openModal }) => {
</Spin>
)
}
export default AIUnConfigure
@@ -1,105 +1,50 @@
import InsidePage from '@common/components/aoplatform/InsidePage'
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, Tabs } from 'antd'
import { useRef } from 'react'
import { Tabs } from 'antd'
import AIFlowChart from './AIFlowChart'
import AiSettingModalContent, { AiSettingModalContentHandle } from './AiSettingModal'
import AIUnConfigure from './AIUnconfigure'
import { AiProviderConfig, AiSettingListItem } from './types'
const AiSettingList = () => {
const { modal, message } = App.useApp()
const { fetchData } = useFetch()
const modalRef = useRef<AiSettingModalContentHandle>()
const { setAiConfigFlushed, accessData } = useGlobalContext()
const openModal = async (entity: AiSettingListItem) => {
console.log(entity)
message.loading($t(RESPONSE_TIPS.loading))
const { code, data, msg } = await fetchData<BasicResponse<{ provider: AiProviderConfig }>>('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: (
<AiSettingModalContent
ref={modalRef}
entity={{ ...data.provider, defaultLlm: entity.defaultLlm }}
readOnly={!checkAccess('system.devops.ai_provider.edit', accessData)}
/>
),
onOk: () => {
return modalRef.current?.save().then((res) => {
if (res === true) setAiConfigFlushed(true)
})
},
width: 600,
okText: $t('确认'),
footer: (_, { OkBtn, CancelBtn }) => {
return (
<div className="flex justify-between items-center">
<a
target="_blank"
rel="noopener noreferrer"
href={data.provider.getApikeyUrl}
className="flex items-center gap-[8px]"
>
<span>{$t('从 (0) 获取 API KEY', [data.provider.name])}</span>
<Icon icon="ic:baseline-open-in-new" width={16} height={16} />
</a>
<div>
<CancelBtn />
{checkAccess('system.devops.ai_provider.edit', accessData) ? <OkBtn /> : null}
</div>
</div>
)
},
cancelText: $t('取消'),
closable: true,
icon: <></>
})
}
import { AiSettingProvider } from './contexts/AiSettingContext'
const AiSettingContent = () => {
return (
<>
<InsidePage
className="overflow-y-auto pb-PAGE_INSIDE_B"
pageTitle={$t('AI 模型')}
description={$t('配置好 AI 模型后,你可以使用对应的大模型来创建 AI 服务')}
showBorder={false}
scrollPage={false}
>
<div className="flex flex-col h-full">
<Tabs
className="flex-shrink-0"
items={[
{
key: 'flow',
label: $t('已设置'),
children: <AIFlowChart openModal={openModal} />
},
{
key: 'config',
label: $t('未设置'),
children: <div className="overflow-auto flex-grow">{<AIUnConfigure openModal={openModal} />}</div>
}
]}
/>
</div>
</InsidePage>
</>
<InsidePage
className="overflow-y-auto pb-PAGE_INSIDE_B"
pageTitle={$t('AI 模型')}
description={$t('配置好 AI 模型后,你可以使用对应的大模型来创建 AI 服务')}
showBorder={false}
scrollPage={false}
>
<div className="flex flex-col h-full">
<Tabs
className="flex-shrink-0"
items={[
{
key: 'flow',
label: $t('已设置'),
children: <AIFlowChart />
},
{
key: 'config',
label: $t('未设置'),
children: (
<div className="overflow-auto flex-grow">
<AIUnConfigure />
</div>
)
}
]}
/>
</div>
</InsidePage>
)
}
const AiSettingList = () => {
return (
<AiSettingProvider>
<AiSettingContent />
</AiSettingProvider>
)
}
export default AiSettingList
@@ -2,7 +2,7 @@ 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, Tag } from 'antd'
import { forwardRef, useEffect, useImperativeHandle, useState } from 'react'
import { AiProviderConfig, AiProviderLlmsItems } from './AiSettingList'
@@ -18,6 +18,7 @@ export type AiSettingModalContentHandle = {
type AiSettingModalContentField = {
config: string
defaultLlm: string
priority: number
}
const AiSettingModalContent = forwardRef<AiSettingModalContentHandle, AiSettingModalContentProps>((props, ref) => {
@@ -52,12 +53,14 @@ const AiSettingModalContent = forwardRef<AiSettingModalContentHandle, AiSettingM
try {
form.setFieldsValue({
defaultLlm: entity.defaultLlm,
config: entity!.config ? JSON.stringify(JSON.parse(entity!.config), null, 2) : ''
config: entity!.config ? JSON.stringify(JSON.parse(entity!.config), null, 2) : '',
priority: entity.priority || 1
})
} catch (e) {
form.setFieldsValue({
defaultLlm: entity.defaultLlm,
config: ''
config: '',
priority: 1
})
}
}, [])
@@ -67,10 +70,15 @@ const AiSettingModalContent = forwardRef<AiSettingModalContentHandle, AiSettingM
form
.validateFields()
.then((value) => {
const finalValue = {
...value,
priority: Math.max(1, value.priority)
}
fetchData<BasicResponse<null>>('ai/provider/config', {
method: 'PUT',
eoParams: { provider: entity?.id },
eoBody: value,
eoBody: finalValue,
eoTransformKeys: ['defaultLlm']
})
.then((response) => {
@@ -103,7 +111,7 @@ const AiSettingModalContent = forwardRef<AiSettingModalContentHandle, AiSettingM
name="aiServiceInsideRouterModalConfig"
autoComplete="off"
>
<Form.Item<AiSettingModalContentField> label={$t('模型')} name="defaultLlm" rules={[{ required: true }]}>
<Form.Item<AiSettingModalContentField> label={$t('默认模型')} name="defaultLlm" rules={[{ required: true }]}>
<Select
className="w-INPUT_NORMAL"
placeholder={$t(PLACEHOLDER.select)}
@@ -120,6 +128,25 @@ const AiSettingModalContent = forwardRef<AiSettingModalContentHandle, AiSettingM
></Select>
</Form.Item>
<Form.Item<AiSettingModalContentField>
label={$t('优先级')}
name="priority"
rules={[
{ required: true },
{
validator: async (_, value) => {
if (value <= 0) {
throw new Error($t('优先级必须大于0'))
}
return Promise.resolve()
}
}
]}
initialValue={1}
>
<InputNumber className="w-INPUT_NORMAL" min={1} placeholder={$t('请输入优先级')} />
</Form.Item>
<Form.Item<AiSettingModalContentField> label={$t('参数')} name="config">
<Codebox
editorTheme="vs-dark"
@@ -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 { AiProviderConfig, AiSettingListItem } from '../types'
interface AiSettingContextType {
openConfigModal: (entity: AiSettingListItem) => Promise<void>
}
const AiSettingContext = createContext<AiSettingContextType | undefined>(undefined)
export const AiSettingProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const { modal, message } = App.useApp()
const { fetchData } = useFetch()
const { setAiConfigFlushed, accessData } = useGlobalContext()
const modalRef = useRef<AiSettingModalContentHandle>()
const openConfigModal = async (entity: AiSettingListItem) => {
message.loading($t(RESPONSE_TIPS.loading))
const { code, data, msg } = await fetchData<BasicResponse<{ provider: AiProviderConfig }>>('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: (
<AiSettingModalContent
ref={modalRef}
entity={{ ...data.provider, defaultLlm: entity.defaultLlm }}
readOnly={!checkAccess('system.devops.ai_provider.edit', accessData)}
/>
),
onOk: () => {
return modalRef.current?.save().then((res) => {
if (res === true) {
setAiConfigFlushed(true)
}
})
},
width: 600,
okText: $t('确认'),
footer: (_, { OkBtn, CancelBtn }) => {
return (
<div className="flex justify-between items-center">
<a
target="_blank"
rel="noopener noreferrer"
href={data.provider.getApikeyUrl}
className="flex items-center gap-[8px]"
>
<span>{$t('从 (0) 获取 API KEY', [data.provider.name])}</span>
<Icon icon="ic:baseline-open-in-new" width={16} height={16} />
</a>
<div>
<CancelBtn />
{checkAccess('system.devops.ai_provider.edit', accessData) ? <OkBtn /> : null}
</div>
</div>
)
},
cancelText: $t('取消'),
closable: true,
icon: <></>
})
}
return <AiSettingContext.Provider value={{ openConfigModal }}>{children}</AiSettingContext.Provider>
}
export const useAiSetting = () => {
const context = useContext(AiSettingContext)
if (!context) {
throw new Error('useAiSetting must be used within an AiSettingProvider')
}
return context
}
@@ -27,6 +27,7 @@ export type AiSettingListItem = {
defaultLlmLogo: string
enable: boolean
configured: boolean
priority?: number
}
export type AiProviderLlmsItems = {
@@ -48,6 +49,7 @@ export type AiProviderDefaultConfig = {
export type AiProviderConfig = {
id: string
name: string
config: string
config?: string
getApikeyUrl: string
priority?: number
}