feat: community backends page

This commit is contained in:
jialin
2026-01-30 20:48:55 +08:00
committed by Lawrence Li
parent 2dab5093dc
commit b6bd9cd7dd
22 changed files with 783 additions and 124 deletions
@@ -0,0 +1,94 @@
import GSDrawer from '@/components/scroller-modal/gs-drawer';
import ColumnWrapper from '@/pages/_components/column-wrapper';
import Separator from '@/pages/llmodels/components/separator';
import { useIntl } from '@umijs/max';
import React from 'react';
import styled from 'styled-components';
import { ListItem } from '../config/types';
import useEnableBackend from '../services/use-enable-backend';
import BackendDetail from './backend-detail';
import CommunityBackends from './commnity-backends';
const Container = styled.div`
display: grid;
grid-template-columns: 1fr 1fr;
height: 100%;
`;
const AddCommunityModal: React.FC<{
open: boolean;
onClose: () => void;
onRefresh?: () => void;
}> = ({ open, onClose, onRefresh }) => {
const intl = useIntl();
const { handleEnableBackend } = useEnableBackend();
const [currentData, setCurrentData] = React.useState<any>(null);
const handleOnEnable = async (checked: boolean, data: ListItem) => {
await handleEnableBackend({
id: data.id,
data: {
...data,
enabled: checked
}
});
onRefresh?.();
};
const handleOnClick = (data: ListItem) => {
setCurrentData(data);
};
return (
<GSDrawer
open={open}
title={intl.formatMessage({ id: 'backend.community.title' })}
footer={false}
onClose={onClose}
destroyOnHidden={true}
closeIcon={false}
maskClosable={false}
keyboard={false}
styles={{
wrapper: { width: 'max(50vw, 900px)' }
}}
>
<Container>
<div
style={{
display: 'flex'
}}
>
<ColumnWrapper
maxHeight={'calc(100vh - 90px)'}
styles={{
container: {
paddingTop: 0
}
}}
>
<CommunityBackends
onClick={handleOnClick}
onEnable={handleOnEnable}
></CommunityBackends>
</ColumnWrapper>
<Separator showArrow={false}></Separator>
</div>
<ColumnWrapper
maxHeight={'calc(100vh - 90px)'}
styles={{
container: {
paddingTop: 0
}
}}
>
<BackendDetail
onEnable={handleOnEnable}
currentData={currentData}
></BackendDetail>
</ColumnWrapper>
</Container>
</GSDrawer>
);
};
export default AddCommunityModal;
@@ -0,0 +1,163 @@
import AutoTooltip from '@/components/auto-tooltip';
import { BulbOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Button, Tag, Typography } from 'antd';
import _ from 'lodash';
import React from 'react';
import styled from 'styled-components';
import { generateIcon } from '../components/backend-card';
const Title = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
.text {
display: flex;
align-items: center;
gap: 8px;
font-weight: 700;
}
`;
const Subtitle = styled.div`
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: var(--ant-color-text);
font-weight: 500;
margin-top: 24px;
margin-bottom: 16px;
border-bottom: 1px solid var(--ant-color-split);
padding-bottom: 8px;
.icon {
color: var(--ant-color-text-tertiary);
}
`;
const Section = styled.div`
display: flex;
flex-direction: column;
`;
const Content = styled.div`
padding: 0px 8px;
border-radius: var(--ant-border-radius);
`;
const UL = styled.ul`
margin: 0;
padding-left: 16px;
list-style-type: disc;
display: flex;
flex-direction: column;
gap: 12px;
.label {
font-weight: 500;
font-size: 14px;
}
`;
const BackendDetail: React.FC<{
onEnable: (checked: boolean, data: any) => void;
onDisable?: () => void;
currentData?: any;
}> = ({ onEnable, onDisable, currentData }) => {
const intl = useIntl();
const renderSupportedFrameworks = () => {
const frameworks = currentData?.framework_index_map || {};
if (Object.keys(frameworks).length === 0) {
return null;
}
return (
<UL>
{Object.entries(frameworks as Record<string, string[]>).map(
([key, value]) => {
return (
<li key={key}>
<div className="label">{_.upperCase(key)}</div>
{/* <Flex gap={8} wrap="wrap" style={{ marginBlock: 8 }}>
{value?.map?.((v: string, index: number) => (
<Tag key={index} style={{ margin: 0 }}>
{v}
</Tag>
))}
</Flex> */}
</li>
);
}
)}
</UL>
);
};
return (
<div>
<Title>
<span className="text">
{generateIcon(currentData || {}, 32)}
<AutoTooltip ghost>{currentData?.backend_name}</AutoTooltip>
</span>
{currentData && (
<span>
{currentData?.enabled ? (
<Tag color="geekblue" variant="filled">
{intl.formatMessage({ id: 'common.status.enabled' })}
</Tag>
) : (
<Button
type="primary"
size="middle"
onClick={() => onEnable(true, currentData)}
>
{intl.formatMessage({ id: 'common.button.enable' })}
</Button>
)}
</span>
)}
</Title>
{Object.keys(currentData?.framework_index_map || {}).length > 0 && (
<Section>
<Subtitle>
<BulbOutlined className="icon" />
<span>
{intl.formatMessage({ id: 'backend.availableFrameworks' })}
</span>
</Subtitle>
<Content>{renderSupportedFrameworks()}</Content>
</Section>
)}
{/* <Section>
<Subtitle>
<BulbOutlined className="icon" />
<span>
{intl.formatMessage({
id: 'backend.form.defaultExecuteCommand'
})}
</span>
</Subtitle>
<Content style={{ padding: 0, border: 'none' }}>
<HighlightCode
code={currentData?.default_run_command}
></HighlightCode>
</Content>
</Section> */}
{currentData?.description && (
<Section>
<Subtitle>
<BulbOutlined className="icon" />
<span>
{intl.formatMessage({ id: 'common.table.description' })}
</span>
</Subtitle>
<Content>
<Typography.Text>{currentData?.description}</Typography.Text>
</Content>
</Section>
)}
</div>
);
};
export default BackendDetail;
@@ -0,0 +1,153 @@
import IconFont from '@/components/icon-font';
import ThemeTag from '@/components/tags-wrapper/theme-tag';
import useTableFetch from '@/hooks/use-table-fetch';
import { SearchOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Input, Switch, Tooltip } from 'antd';
import _ from 'lodash';
import React, { useEffect } from 'react';
import styled from 'styled-components';
import { ScrollerContext } from '../../_components/infinite-scroller/use-scroller-context';
import NoResult from '../../_components/no-result';
import { INFERENCE_BACKEND_API, queryBackendsList } from '../apis';
import BackendCard from '../components/backend-card';
import BackendCardList from '../components/backend-list';
import { ListItem } from '../config/types';
const FilterBox = styled.div`
position: sticky;
top: 0;
width: 100%;
z-index: 100;
margin-bottom: 16px;
background-color: var(--ant-color-bg-container);
`;
const CommunityBackends: React.FC<{
onEnable: (checked: boolean, data: ListItem) => void;
onClick: (data: ListItem) => void;
}> = ({ onEnable, onClick }) => {
const intl = useIntl();
const [currentData, setCurrentData] = React.useState<ListItem | null>(null);
const { dataSource, loadMore, queryParams, handleSearch, handleNameChange } =
useTableFetch<ListItem>({
fetchAPI: queryBackendsList,
API: INFERENCE_BACKEND_API,
watch: false,
isInfiniteScroll: true,
contentForDelete: 'backends.title',
defaultQueryParams: {
perPage: 24,
community: 1
}
});
const handleOnClickItem = (data: ListItem) => {
onClick(data);
setCurrentData(data);
};
const handleOnEnable = async (checked: boolean, data: ListItem) => {
await onEnable(checked, data);
await handleSearch();
if (data.id === currentData?.id) {
handleOnClickItem({
...data,
enabled: checked
});
}
};
const actionsRenderer = (data: ListItem) => {
return (
<Tooltip
title={
data.enabled
? false
: intl.formatMessage({ id: 'common.button.enable' })
}
>
<>
{data.enabled ? (
<ThemeTag style={{ margin: 0 }} color="geekblue" variant="filled">
{intl.formatMessage({ id: 'common.status.enabled' })}
</ThemeTag>
) : (
<span onClick={(e) => e.stopPropagation()}>
<Switch
onChange={(checked) => handleOnEnable(checked, data)}
size="small"
></Switch>
</span>
)}
</>
</Tooltip>
);
};
useEffect(() => {
if (dataSource.dataList.length > 0 && dataSource.loadend && !currentData) {
handleOnClickItem(dataSource.dataList[0]);
}
}, [dataSource.dataList, dataSource.loadend]);
const renderItem = (item: ListItem) => {
return (
<BackendCard
active={currentData?.id === item.id}
data={item}
layout="community"
onClick={handleOnClickItem}
actionsRenderer={actionsRenderer}
/>
);
};
return (
<div>
<FilterBox>
<Input
prefix={
<SearchOutlined
style={{ color: 'var(--ant-color-text-placeholder)' }}
></SearchOutlined>
}
placeholder={intl.formatMessage({
id: 'common.filter.name'
})}
style={{ width: '100%' }}
allowClear
onChange={handleNameChange}
></Input>
</FilterBox>
<ScrollerContext.Provider
value={{
total: dataSource.totalPage,
current: queryParams.page!,
loading: dataSource.loading,
refresh: loadMore,
throttleDelay: 300
}}
>
<BackendCardList
dataList={dataSource.dataList}
loading={dataSource.loading}
activeId={false}
isFirst={!dataSource.loadend}
renderItem={renderItem}
></BackendCardList>
<NoResult
loading={dataSource.loading}
loadend={dataSource.loadend}
dataSource={dataSource.dataList}
image={<IconFont type="icon-models" />}
filters={_.omit(queryParams, ['sort_by'])}
noFoundText={intl.formatMessage({
id: 'noresult.backend.nofound'
})}
></NoResult>
</ScrollerContext.Provider>
</div>
);
};
export default CommunityBackends;