Compare commits

..
40 Commits
Author SHA1 Message Date
jialin f730548c8f fix(style): tooltip shown when no content 2024-10-25 22:36:26 +08:00
jialin a185bd9463 style: viewcode scrollbar color 2024-10-25 20:52:15 +08:00
jialin abdc78e123 fix(style): text overflow 2024-10-25 18:36:18 +08:00
jialin ba49fa6c88 chore: completion payload format 2024-10-25 12:15:26 +08:00
jialin 870ffa9dc1 chore: add button for copy logs 2024-10-24 23:23:47 +08:00
jialin 8afaf1a60e chore: update playground payload format 2024-10-24 22:40:38 +08:00
jialin 6974ac8e14 chore: parse bibtex 2024-10-24 22:40:38 +08:00
jialin 9f274ee17e chore: remove metadata of readme 2024-10-24 17:25:31 +08:00
jialin 673bcde719 fix: playground error message field 2024-10-24 14:52:57 +08:00
jialin 6eaf5bb463 style: comparsion model card title 2024-10-24 11:54:21 +08:00
jialin b463eedb3f fix: remove invalid parameter 2024-10-24 10:25:18 +08:00
jialin 90350f99ad style: table column size 2024-10-23 19:36:29 +08:00
jialin 8576015633 fix: chart resize 2024-10-23 18:04:34 +08:00
jialin f5edf39027 chore: worker ip 2024-10-23 14:47:55 +08:00
jialin 751781e3e5 fix: distributed across workers 2024-10-23 14:27:49 +08:00
jialin bc9129bcbe fix(style): body scroll 2024-10-22 18:37:45 +08:00
jialin c442a33d5e fix: stream_options spelling 2024-10-22 15:46:35 +08:00
jialin 902bb06353 fix: logs viewer do not scroll after opened 2024-10-22 15:36:53 +08:00
jialin a0a1b8b3e8 chore: openai api error message 2024-10-22 10:17:29 +08:00
jialin eb5cf9da8d fix: logs scrollbar ux 2024-10-22 09:29:03 +08:00
jialin 57386eeb55 style: body and side menu scrollbar style 2024-10-21 11:36:54 +08:00
jialin 33a21f2aaa fix: scrollbar ux 2024-10-20 12:47:14 +08:00
jialin d6fd498489 chore: logs style 2024-10-20 10:40:17 +08:00
jialin 386f2969eb chore: upgrade check 2024-10-18 19:35:47 +08:00
jialin 2fea3a4b40 chore: parse ansi 2024-10-18 15:20:55 +08:00
jialin b9c9fe7a97 fix: model instances limit 2024-10-18 12:09:09 +08:00
jialin 594c4cd670 fix: select same model will clear filename 2024-10-18 10:59:01 +08:00
jialin e10c5e28b5 fix: upload a image can clear other message 2024-10-17 16:06:38 +08:00
jialin fbefb2014d fix: do not clear system message, on clear button 2024-10-17 15:49:41 +08:00
jialin 909da7edb5 style: model tag style 2024-10-17 14:59:42 +08:00
jialin 349676d908 fix: adjust image size when paste 2024-10-17 13:32:38 +08:00
jialin a47f7cc0ce fix: no request, search model 2024-10-15 17:52:02 +08:00
jialin 8fbdb884c7 chore: hide rerank tab 2024-10-14 10:46:32 +08:00
jialin ec68ce082e fix(style): document list 2024-10-14 10:40:13 +08:00
jialin 2b61e3dfd3 fix(style): input text hover 2024-10-14 10:33:00 +08:00
jialin 4c3dddc486 feat: playground rerank 2024-10-14 10:27:46 +08:00
jialin ecc683a6bd fix: cpu_offload is not shown when editting model 2024-10-09 19:23:29 +08:00
jialin 7b2098cf33 fix: playground params do not update after being changed 2024-10-09 15:07:12 +08:00
jialin 3784d86344 chore: update antd 2024-10-09 14:08:27 +08:00
jialin 2b22481443 style: change select to radio group 2024-10-09 14:08:27 +08:00
106 changed files with 5043 additions and 2014 deletions
+9 -1
View File
@@ -1,4 +1,12 @@
const proxyTableList = ['cli', 'v1', 'auth', 'v1-openai', 'version', 'proxy']; const proxyTableList = [
'cli',
'v1',
'auth',
'v1-openai',
'version',
'proxy',
'update'
];
// @ts-ingore // @ts-ingore
export default function createProxyTable(target?: string) { export default function createProxyTable(target?: string) {
+12 -4
View File
@@ -7,23 +7,28 @@
"format": "prettier --cache --write .", "format": "prettier --cache --write .",
"postinstall": "max setup", "postinstall": "max setup",
"prepare": "husky", "prepare": "husky",
"preview": "max preview",
"setup": "max setup", "setup": "max setup",
"start": "npm run dev" "start": "npm run dev"
}, },
"resolutions": {
"immer": "^9.0.6"
},
"dependencies": { "dependencies": {
"@ant-design/icons": "^5.3.7", "@ant-design/icons": "^5.5.1",
"@ant-design/pro-components": "^2.7.1", "@ant-design/pro-components": "^2.7.18",
"@huggingface/gguf": "^0.1.7", "@huggingface/gguf": "^0.1.7",
"@huggingface/hub": "^0.15.1", "@huggingface/hub": "^0.15.1",
"@huggingface/tasks": "^0.11.6", "@huggingface/tasks": "^0.11.6",
"@monaco-editor/react": "^4.6.0", "@monaco-editor/react": "^4.6.0",
"@orcid/bibtex-parse-js": "^0.0.25",
"@react-hook/resize-observer": "^2.0.2", "@react-hook/resize-observer": "^2.0.2",
"@types/lodash": "^4.17.4", "@types/lodash": "^4.17.4",
"@umijs/max": "^4.2.11", "@umijs/max": "^4.2.11",
"@xterm/addon-fit": "^0.10.0", "@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.5.0", "@xterm/xterm": "^5.5.0",
"ansi-to-html": "^0.7.2", "ansi-to-html": "^0.7.2",
"antd": "^5.18.3", "antd": "^5.20.6",
"antd-style": "^3.6.2", "antd-style": "^3.6.2",
"axios": "^1.7.2", "axios": "^1.7.2",
"classnames": "^2.5.1", "classnames": "^2.5.1",
@@ -36,16 +41,19 @@
"jotai": "^2.8.4", "jotai": "^2.8.4",
"localforage": "^1.10.0", "localforage": "^1.10.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mammoth": "^1.8.0",
"marked": "^14.1.0", "marked": "^14.1.0",
"numeral": "^2.0.6", "numeral": "^2.0.6",
"overlayscrollbars": "^2.10.0", "overlayscrollbars": "^2.10.0",
"overlayscrollbars-react": "^0.5.6", "overlayscrollbars-react": "^0.5.6",
"query-string": "^9.0.0", "query-string": "^9.0.0",
"rc-virtual-list": "^3.14.8",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-hotkeys-hook": "^4.5.0", "react-hotkeys-hook": "^4.5.0",
"simplebar-react": "^3.2.6", "simplebar-react": "^3.2.6",
"umi-presets-pro": "^2.0.3" "umi-presets-pro": "^2.0.3",
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz"
}, },
"devDependencies": { "devDependencies": {
"@types/marked": "^6.0.0", "@types/marked": "^6.0.0",
+4 -3
View File
@@ -4,11 +4,12 @@ export default (api: IApi) => {
api.modifyHTML(($) => { api.modifyHTML(($) => {
const info = JSON.parse(process.env.VERSION || '{}'); const info = JSON.parse(process.env.VERSION || '{}');
const env = process.env.NODE_ENV; const env = process.env.NODE_ENV;
$('html').attr('data-env', env);
$('html').attr( $('html').attr(
'data-version', 'data-version',
env === 'production' env === 'production' ? info.version || info.commitId : `${info.commitId}`
? info.version || info.commitId
: `dev-${info.commitId}`
); );
if (env === 'production') { if (env === 'production') {
$('script[src^="/js/umi"]').first?.().remove?.(); $('script[src^="/js/umi"]').first?.().remove?.();
+1589 -1031
View File
File diff suppressed because it is too large Load Diff
+16 -2
View File
@@ -1,9 +1,10 @@
import { GPUStackVersionAtom } from '@/atoms/user'; import { GPUStackVersionAtom, UpdateCheckAtom } from '@/atoms/user';
import { setAtomStorage } from '@/atoms/utils'; import { setAtomStorage } from '@/atoms/utils';
import { requestConfig } from '@/request-config'; import { requestConfig } from '@/request-config';
import { import {
queryCurrentUserState, queryCurrentUserState,
queryVersionInfo queryVersionInfo,
updateCheck
} from '@/services/profile/apis'; } from '@/services/profile/apis';
import { RequestConfig, history } from '@umijs/max'; import { RequestConfig, history } from '@umijs/max';
@@ -16,11 +17,24 @@ export async function getInitialState(): Promise<{
}> { }> {
const { location } = history; const { location } = history;
const getUpdateCheck = async () => {
try {
const data = await updateCheck();
setAtomStorage(UpdateCheckAtom, data);
return data;
} catch (error) {
console.error('updateCheck error', error);
}
};
const fetchUserInfo = async (): Promise<Global.UserInfo> => { const fetchUserInfo = async (): Promise<Global.UserInfo> => {
try { try {
const data = await queryCurrentUserState({ const data = await queryCurrentUserState({
skipErrorHandler: true skipErrorHandler: true
}); });
if (data.is_admin) {
getUpdateCheck();
}
return data; return data;
} catch (error) { } catch (error) {
history.push(loginPath); history.push(loginPath);
+8
View File
@@ -136,6 +136,10 @@
position: relative; position: relative;
} }
.font-size-0 {
font-size: 0;
}
.font-size-12 { .font-size-12 {
font-size: var(--font-size-base); font-size: var(--font-size-base);
} }
@@ -148,6 +152,10 @@
font-size: var(--font-size-large); font-size: var(--font-size-large);
} }
.font-size-20 {
font-size: 20px;
}
.font-400 { .font-400 {
font-weight: var(--font-weight-normal); font-weight: var(--font-weight-normal);
} }
+6
View File
@@ -11,6 +11,12 @@ export const GPUStackVersionAtom = atom<{
git_commit: '' git_commit: ''
}); });
export const UpdateCheckAtom = atom<{
latest_version: string;
}>({
latest_version: ''
});
export const initialPasswordAtom = atomWithStorage<string>( export const initialPasswordAtom = atomWithStorage<string>(
'initialPassword', 'initialPassword',
'' ''
+34
View File
@@ -0,0 +1,34 @@
import { Image as AntImage, ImageProps } from 'antd';
import { round } from 'lodash';
import React, { useCallback, useEffect, useState } from 'react';
const AutoImage: React.FC<ImageProps & { height: number }> = (props) => {
const { height = 100, ...rest } = props;
const [width, setWidth] = useState(0);
const getImgRatio = useCallback((url: string): Promise<{ ratio: number }> => {
return new Promise((resolve) => {
const img = new Image();
img.onload = () => {
resolve({ ratio: round(img.width / img.height, 2) });
};
img.onerror = () => {
resolve({ ratio: 1 });
};
img.src = url;
});
}, []);
const handleOnLoad = useCallback(async () => {
const { ratio } = await getImgRatio(props.src || '');
setWidth(height * ratio);
}, [getImgRatio, height, props.src]);
useEffect(() => {
handleOnLoad();
}, [handleOnLoad]);
return <AntImage {...rest} height={height} width={width} />;
};
export default AutoImage;
+23 -9
View File
@@ -1,5 +1,5 @@
import { Tag, Tooltip, type TagProps } from 'antd'; import { Tag, Tooltip, type TagProps } from 'antd';
import debounce from 'lodash/debounce'; import { throttle } from 'lodash';
import React, { import React, {
useCallback, useCallback,
useEffect, useEffect,
@@ -7,12 +7,14 @@ import React, {
useRef, useRef,
useState useState
} from 'react'; } from 'react';
import TitleTip from './title-tip';
// type TagProps = React.ComponentProps<typeof Tag>; // type TagProps = React.ComponentProps<typeof Tag>;
interface AutoTooltipProps extends Omit<TagProps, 'title'> { interface AutoTooltipProps extends Omit<TagProps, 'title'> {
children: React.ReactNode; children: React.ReactNode;
maxWidth?: number | string; maxWidth?: number | string;
minWidth?: number | string;
color?: string; color?: string;
style?: React.CSSProperties; style?: React.CSSProperties;
ghost?: boolean; ghost?: boolean;
@@ -24,6 +26,7 @@ interface AutoTooltipProps extends Omit<TagProps, 'title'> {
const AutoTooltip: React.FC<AutoTooltipProps> = ({ const AutoTooltip: React.FC<AutoTooltipProps> = ({
children, children,
maxWidth = '100%', maxWidth = '100%',
minWidth,
ghost = false, ghost = false,
title, title,
showTitle = false, showTitle = false,
@@ -32,6 +35,7 @@ const AutoTooltip: React.FC<AutoTooltipProps> = ({
}) => { }) => {
const contentRef = useRef<HTMLDivElement>(null); const contentRef = useRef<HTMLDivElement>(null);
const [isOverflowing, setIsOverflowing] = useState(false); const [isOverflowing, setIsOverflowing] = useState(false);
const resizeObserver = useRef<ResizeObserver>();
const checkOverflow = useCallback(() => { const checkOverflow = useCallback(() => {
if (contentRef.current) { if (contentRef.current) {
@@ -40,19 +44,14 @@ const AutoTooltip: React.FC<AutoTooltipProps> = ({
} }
}, [contentRef.current]); }, [contentRef.current]);
const debouncedCheckOverflow = useMemo(
() => debounce(checkOverflow, 200),
[checkOverflow]
);
useEffect(() => { useEffect(() => {
checkOverflow(); const debouncedCheckOverflow = throttle(checkOverflow, 200);
window.addEventListener('resize', debouncedCheckOverflow); window.addEventListener('resize', debouncedCheckOverflow);
return () => { return () => {
window.removeEventListener('resize', debouncedCheckOverflow); window.removeEventListener('resize', debouncedCheckOverflow);
debouncedCheckOverflow.cancel(); debouncedCheckOverflow.cancel();
}; };
}, [checkOverflow, debouncedCheckOverflow]); }, [checkOverflow]);
useEffect(() => { useEffect(() => {
checkOverflow(); checkOverflow();
@@ -61,6 +60,7 @@ const AutoTooltip: React.FC<AutoTooltipProps> = ({
const tagStyle = useMemo( const tagStyle = useMemo(
() => ({ () => ({
maxWidth, maxWidth,
minWidth,
overflow: 'hidden', overflow: 'hidden',
textOverflow: 'ellipsis', textOverflow: 'ellipsis',
whiteSpace: 'nowrap' as const, whiteSpace: 'nowrap' as const,
@@ -71,7 +71,21 @@ const AutoTooltip: React.FC<AutoTooltipProps> = ({
return ( return (
<Tooltip <Tooltip
title={isOverflowing || showTitle ? title || children : ''} overlayInnerStyle={{ paddingInline: 0 }}
destroyTooltipOnHide={false}
title={
isOverflowing || showTitle ? (
<TitleTip
isOverflowing={isOverflowing}
title={title}
showTitle={showTitle}
>
{children}
</TitleTip>
) : (
''
)
}
{...tooltipProps} {...tooltipProps}
> >
{ghost ? ( {ghost ? (
+31
View File
@@ -0,0 +1,31 @@
import useOverlayScroller from '@/hooks/use-overlay-scroller';
import React from 'react';
interface TitleTipProps {
isOverflowing: boolean;
showTitle: boolean;
title: React.ReactNode;
children: React.ReactNode;
}
const TitleTip: React.FC<TitleTipProps> = (props) => {
const { isOverflowing, showTitle, title, children } = props;
const { initialize } = useOverlayScroller();
const scrollRef = React.useRef<any>(null);
React.useEffect(() => {
if (scrollRef.current) {
initialize(scrollRef.current);
}
}, [initialize, scrollRef.current]);
return (
<div style={{ maxHeight: 200, overflowY: 'auto' }} ref={scrollRef}>
<div style={{ width: 'max-content', maxWidth: 250, paddingInline: 10 }}>
{isOverflowing || showTitle ? title || children : ''}
</div>
</div>
);
};
export default React.memo(TitleTip);
+47
View File
@@ -0,0 +1,47 @@
import bibtexParse from '@orcid/bibtex-parse-js';
import { Typography } from 'antd';
import React from 'react';
/*
@inproceedings{Lysenko:2010:GMC:1839778.1839781,\
author = {Lysenko, Mikola and Nelaturi, Saigopal and Shapiro, Vadim},\
title = {Group morphology with convolution algebras},\
booktitle = {Proceedings of the 14th ACM Symposium on Solid and Physical Modeling},\
series = {SPM '10},\
year = {2010},\
isbn = {978-1-60558-984-8},\
location = {Haifa, Israel},\
pages = {11--22},\
numpages = {12},\
url = {http://doi.acm.org/10.1145/1839778.1839781},\
doi = {10.1145/1839778.1839781},\
acmid = {1839781},\
publisher = {ACM},\
address = {New York, NY, USA},\
}
*/
const BibTeXViewer: React.FC<{ data: string }> = ({ data }) => {
if (!data) {
return null;
}
const dataList = bibtexParse.toJSON(data);
return (
<ol>
{dataList.map((item: any, index: number) => (
<li key={index} style={{ lineHeight: 2 }}>
<Typography.Link href={item.entryTags?.url} target="_blank">
{item.entryTags?.title}.{' '}
</Typography.Link>
<Typography.Text>{item.entryTags?.author}. </Typography.Text>
<Typography.Text>[{item.entryTags?.year}] </Typography.Text>
{item.entryTags?.journal && (
<Typography.Text>.({item.entryTags?.journal})</Typography.Text>
)}
</li>
))}
</ol>
);
};
export default BibTeXViewer;
+29 -6
View File
@@ -1,4 +1,4 @@
import { throttle } from 'echarts/core'; import { throttle } from 'lodash';
import { useCallback, useEffect, useRef } from 'react'; import { useCallback, useEffect, useRef } from 'react';
import echarts, { ECOption } from '.'; import echarts, { ECOption } from '.';
@@ -9,6 +9,8 @@ const Chart: React.FC<{
}> = ({ options, width, height }) => { }> = ({ options, width, height }) => {
const container = useRef<HTMLDivElement>(null); const container = useRef<HTMLDivElement>(null);
const chart = useRef<echarts.EChartsType>(); const chart = useRef<echarts.EChartsType>();
const resizeable = useRef(false);
const resizeObserver = useRef<ResizeObserver>();
const init = useCallback(() => { const init = useCallback(() => {
if (container.current) { if (container.current) {
@@ -30,27 +32,48 @@ const Chart: React.FC<{
}, []); }, []);
useEffect(() => { useEffect(() => {
if (container.current) {
init(); init();
}
return () => { return () => {
chart.current?.dispose(); chart.current?.dispose();
}; };
}, [init]); }, [init]);
useEffect(() => { useEffect(() => {
resizeable.current = false;
resize(); resize();
setOption(options); setOption(options);
}, [options]); }, [options]);
// resize on window resize useEffect(() => {
let timer: any = null;
timer = setTimeout(() => {
resizeable.current = true;
}, 300);
return () => {
clearTimeout(timer);
};
}, []);
useEffect(() => { useEffect(() => {
const handleResize = throttle(() => { const handleResize = throttle(() => {
resize(); if (resizeable.current) {
chart.current?.resize();
}
}, 100); }, 100);
window.addEventListener('resize', handleResize);
if (container.current) {
resizeObserver.current = new ResizeObserver(handleResize);
resizeObserver.current.observe(container.current);
}
return () => { return () => {
window.removeEventListener('resize', handleResize); if (container.current) {
resizeObserver.current?.unobserve(container.current);
}
}; };
}, [resize]); }, []);
return <div ref={container} style={{ width: width, height }}></div>; return <div ref={container} style={{ width: width, height }}></div>;
}; };
+2 -6
View File
@@ -1,5 +1,4 @@
import type { import type {
// 系列类型的定义后缀都为 SeriesOption
BarSeriesOption, BarSeriesOption,
GaugeSeriesOption, GaugeSeriesOption,
LineSeriesOption LineSeriesOption
@@ -8,18 +7,16 @@ import { BarChart, GaugeChart, LineChart } from 'echarts/charts';
import type { import type {
DatasetComponentOption, DatasetComponentOption,
GridComponentOption, GridComponentOption,
// 组件类型的定义后缀都为 ComponentOption
TitleComponentOption, TitleComponentOption,
TooltipComponentOption TooltipComponentOption
} from 'echarts/components'; } from 'echarts/components';
import { import {
// 数据集组件
DatasetComponent, DatasetComponent,
GridComponent, GridComponent,
LegendComponent, LegendComponent,
TitleComponent, TitleComponent,
TooltipComponent, TooltipComponent,
// 内置数据转换器组件 (filter, sort) // (filter, sort)
TransformComponent TransformComponent
} from 'echarts/components'; } from 'echarts/components';
import type { ComposeOption } from 'echarts/core'; import type { ComposeOption } from 'echarts/core';
@@ -27,7 +24,6 @@ import * as echarts from 'echarts/core';
import { LabelLayout, UniversalTransition } from 'echarts/features'; import { LabelLayout, UniversalTransition } from 'echarts/features';
import { CanvasRenderer } from 'echarts/renderers'; import { CanvasRenderer } from 'echarts/renderers';
// 通过 ComposeOption 来组合出一个只有必须组件和图表的 Option 类型
type ECOption = ComposeOption< type ECOption = ComposeOption<
| BarSeriesOption | BarSeriesOption
| LineSeriesOption | LineSeriesOption
@@ -38,7 +34,7 @@ type ECOption = ComposeOption<
| GaugeSeriesOption | GaugeSeriesOption
>; >;
// 注册必须的组件 // register components and charts
echarts.use([ echarts.use([
LegendComponent, LegendComponent,
TitleComponent, TitleComponent,
+1
View File
@@ -12,6 +12,7 @@ const Footer: React.FC = () => {
const showVersion = () => { const showVersion = () => {
Modal.info({ Modal.info({
...modalConfig, ...modalConfig,
width: 460,
content: <VersionInfo intl={intl} /> content: <VersionInfo intl={intl} />
}); });
}; };
@@ -73,7 +73,11 @@ const CodeViewer: React.FC<CodeViewerProps> = (props) => {
}} }}
> >
<code <code
className={highlightedCode.className} style={{ minHeight: height }}
className={classNames(highlightedCode.className, {
dark: props.theme === 'dark',
light: props.theme === 'light'
})}
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: highlightedCode.value __html: highlightedCode.value
}} }}
@@ -20,6 +20,7 @@
background-color: transparent; background-color: transparent;
} }
&.light {
&:hover { &:hover {
&::-webkit-scrollbar-thumb { &::-webkit-scrollbar-thumb {
background-color: var(--color-scrollbar-thumb); background-color: var(--color-scrollbar-thumb);
@@ -28,6 +29,16 @@
} }
} }
&.dark {
&:hover {
&::-webkit-scrollbar-thumb {
background-color: var(--scrollbar-handle-light-bg);
border-radius: 4px;
}
}
}
}
.code-pre { .code-pre {
padding-inline: 12px 12px; padding-inline: 12px 12px;
position: relative; position: relative;
+1 -1
View File
@@ -1,7 +1,7 @@
import { createFromIconfontCN } from '@ant-design/icons'; import { createFromIconfontCN } from '@ant-design/icons';
const IconFont = createFromIconfontCN({ const IconFont = createFromIconfontCN({
scriptUrl: '//at.alicdn.com/t/c/font_4613488_4jkdkc8jcf7.js' scriptUrl: '//at.alicdn.com/t/c/font_4613488_l9554igzbh.js'
}); });
export default IconFont; export default IconFont;
+4 -2
View File
@@ -6,7 +6,7 @@ const Wrapper: React.FC<{
label?: string; label?: string;
description?: React.ReactNode; description?: React.ReactNode;
children: React.ReactNode; children: React.ReactNode;
}> = ({ children, label, description }) => { }> = ({ children, label, description, ...rest }) => {
return ( return (
<div className={styles['wrapper']}> <div className={styles['wrapper']}>
{label && ( {label && (
@@ -14,7 +14,9 @@ const Wrapper: React.FC<{
<LabelInfo label={label} description={description}></LabelInfo> <LabelInfo label={label} description={description}></LabelInfo>
</span> </span>
)} )}
{children} {React.isValidElement(children)
? React.cloneElement(children, { ...rest })
: children}
</div> </div>
); );
}; };
+1 -1
View File
@@ -9,7 +9,7 @@ import ListItem from './list-item';
interface ListInputProps { interface ListInputProps {
dataList: string[]; dataList: string[];
label: string; label: string;
description?: string; description?: React.ReactNode;
btnText?: string; btnText?: string;
options?: Global.HintOptions[]; options?: Global.HintOptions[];
placeholder?: string; placeholder?: string;
+23 -4
View File
@@ -1,9 +1,30 @@
.logs-viewer-wrap-w2 { .logs-viewer-wrap-w2 {
position: relative;
.copy {
position: absolute;
top: 10px;
right: 10px;
z-index: 100;
button {
color: rgba(255, 255, 255, 70%);
background-color: rgba(71, 71, 71, 100%);
&:hover {
color: rgba(255, 255, 255, 90%) !important;
background-color: rgba(71, 71, 71, 100%) !important;
}
}
}
.wrap { .wrap {
padding: 5px 0 5px 10px; padding: 5px 0 5px 10px;
background-color: var(--color-logs-bg); background-color: var(--color-logs-bg);
border-radius: var(--border-radius-mini); border-radius: var(--border-radius-mini);
overflow: hidden; overflow: auto;
font-family: monospace, Menlo, Courier, 'Courier New', Consolas, Monaco,
'Liberation Mono' !important;
.content { .content {
word-wrap: break-word; word-wrap: break-word;
@@ -14,7 +35,7 @@
} }
.text { .text {
height: 100%; min-height: 22px;
} }
color: var(--color-logs-text); color: var(--color-logs-text);
@@ -26,8 +47,6 @@
} }
.xterm { .xterm {
// height: 100% !important;
.xterm-viewport { .xterm-viewport {
overflow-y: auto !important; overflow-y: auto !important;
+98 -76
View File
@@ -1,12 +1,11 @@
import useSetChunkRequest from '@/hooks/use-chunk-request'; import useSetChunkFetch from '@/hooks/use-chunk-fetch';
import { FitAddon } from '@xterm/addon-fit'; import useOverlayScroller from '@/hooks/use-overlay-scroller';
import { Terminal } from '@xterm/xterm';
import '@xterm/xterm/css/xterm.css'; import '@xterm/xterm/css/xterm.css';
import classNames from 'classnames'; import classNames from 'classnames';
import _ from 'lodash'; import _ from 'lodash';
import { memo, useCallback, useEffect, useRef, useState } from 'react'; import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import './index.less'; import './index.less';
import useSize from './use-size'; import useParseAnsi from './parse-ansi';
interface LogsViewerProps { interface LogsViewerProps {
height: number; height: number;
@@ -15,39 +14,47 @@ interface LogsViewerProps {
params?: object; params?: object;
} }
const LogsViewer: React.FC<LogsViewerProps> = (props) => { const LogsViewer: React.FC<LogsViewerProps> = (props) => {
const { height, content, url } = props; const { height, url } = props;
const { setChunkRequest } = useSetChunkRequest(); const {
initialize,
updateScrollerPosition,
generateInstance,
scrollEventElement,
instance,
initialized
} = useOverlayScroller({
theme: 'os-theme-light'
});
const { isClean, parseAnsi } = useParseAnsi();
const { setChunkFetch } = useSetChunkFetch();
const chunkRequedtRef = useRef<any>(null); const chunkRequedtRef = useRef<any>(null);
const scroller = useRef<any>({}); const scroller = useRef<any>({});
const termRef = useRef<any>({}); const cacheDataRef = useRef<any>('');
const termwrapRef = useRef<any>({}); const uidRef = useRef<any>(0);
const fitAddonRef = useRef<any>({}); const [logs, setLogs] = useState<any[]>([]);
const cacheDataRef = useRef<any>(null); const stopScroll = useRef(false);
const [logs, setLogs] = useState('');
const size = useSize(scroller);
const throttleScroll = _.throttle(() => { const setId = () => {
termRef.current?.scrollToBottom?.(); uidRef.current += 1;
}, 100); return uidRef.current;
const updateContent = useCallback(
_.throttle((data: string) => {
cacheDataRef.current = '';
termRef.current?.clear?.();
cacheDataRef.current = data;
termRef.current?.write?.(data);
setLogs(data);
}, 100),
[]
);
const fitTerm = () => {
fitAddonRef.current?.fit?.();
}; };
const updateContent = useCallback(
(data: string) => {
if (isClean(data)) {
cacheDataRef.current = data;
} else {
cacheDataRef.current += data;
}
const res = parseAnsi(cacheDataRef.current, setId);
setLogs(res);
},
[setLogs, setId]
);
const createChunkConnection = async () => { const createChunkConnection = async () => {
chunkRequedtRef.current?.current?.cancel?.(); chunkRequedtRef.current?.current?.abort?.();
chunkRequedtRef.current = setChunkRequest({ chunkRequedtRef.current = setChunkFetch({
url, url,
params: { params: {
...props.params, ...props.params,
@@ -58,69 +65,84 @@ const LogsViewer: React.FC<LogsViewerProps> = (props) => {
}); });
}; };
const initTerm = () => { const debounceResetStopScroll = _.debounce(() => {
termRef.current?.dispose?.(); stopScroll.current = false;
termRef.current = new Terminal({ }, 30000);
lineHeight: 1.2,
fontSize: 13, const handleOnWheel = useCallback(
fontFamily: (e: any) => {
"monospace,Menlo,Courier,'Courier New',Consolas,Monaco, 'Liberation Mono'", const scrollTop = scrollEventElement?.scrollTop;
disableStdin: true, const scrollHeight = scrollEventElement?.scrollHeight;
convertEol: true, const clientHeight = scrollEventElement?.clientHeight;
theme: { if (scrollTop + clientHeight >= scrollHeight) {
background: '#1e1e1e', stopScroll.current = false;
foreground: 'rgba(255,255,255,0.8)' } else {
stopScroll.current = true;
}
debounceResetStopScroll();
}, },
cursorInactiveStyle: 'none', [debounceResetStopScroll, scrollEventElement]
smoothScrollDuration: 0 );
});
fitAddonRef.current = new FitAddon();
termRef.current.loadAddon(fitAddonRef.current);
termRef.current.open(termwrapRef.current);
// add event const debounceUpdateScrollerPosition = _.debounce(() => {
// termRef.current.onLineFeed((e: any) => { generateInstance();
// if (cacheDataRef.current) { updateScrollerPosition(0);
// throttleScroll(); }, 200);
// }
// });
};
const handleResize = _.throttle(() => { const copyText = useMemo(() => {
fitTerm(); if (!logs.length) {
}, 100); return '';
}
return logs?.map((item) => item.content).join('\n');
}, [logs]);
useEffect(() => { useEffect(() => {
createChunkConnection(); createChunkConnection();
return () => { return () => {
chunkRequedtRef.current?.current?.cancel?.(); chunkRequedtRef.current?.current?.abort?.();
}; };
}, [url, props.params]); }, [url, props.params]);
useEffect(() => { useEffect(() => {
if (termwrapRef.current) { if (scroller.current) {
initTerm(); initialize(scroller.current);
} }
return () => { }, [scroller.current, initialize]);
termRef.current?.dispose?.();
};
}, [termwrapRef.current]);
useEffect(() => { useEffect(() => {
if (size) { if (logs.length && !stopScroll.current && instance) {
handleResize(); updateScrollerPosition(0);
} else if (logs.length && !stopScroll.current && scroller.current) {
if (!initialized) {
initialize(scroller.current);
} }
}, [size]); if (!instance) {
debounceUpdateScrollerPosition();
useEffect(() => { } else {
throttleScroll(); updateScrollerPosition(0);
}, [logs]); }
}
}, [logs, stopScroll.current, instance, scroller.current]);
return ( return (
<div className="logs-viewer-wrap-w2"> <div className="logs-viewer-wrap-w2">
<div className="wrap" style={{ height: height }} ref={scroller}> {/* <span className="copy">
<CopyButton text={copyText} type="text" size="small"></CopyButton>
</span> */}
<div
className="wrap"
style={{ height: height }}
ref={scroller}
onWheel={handleOnWheel}
>
<div className={classNames('content')}> <div className={classNames('content')}>
<div className="text" ref={termwrapRef}></div> {_.map(logs, (item: any, index: number) => {
return (
<div key={item.uid} className="text">
{item.content}
</div>
);
})}
</div> </div>
</div> </div>
</div> </div>
+153
View File
@@ -0,0 +1,153 @@
import { useCallback, useRef } from 'react';
const controlSeqRegex = /\x1b\[(\d*);?(\d*)?([A-DJKHfm])/g;
const useParseAnsi = () => {
const lastIndex = useRef(0);
const removeBrackets = useCallback((str: string) => {
return str?.replace?.(/^\(…\)/, '');
}, []);
const isClean = useCallback((ansiStr: string) => {
let input = ansiStr.replace(/\r\n/g, '\n');
let match = controlSeqRegex.exec(input) || [];
const command = match?.[3];
const n = parseInt(match?.[1], 10) || 1;
return command === 'J' && n === 2;
}, []);
const parseAnsi = useCallback((inputStr: string, setId: () => number) => {
let cursorRow = 0; // current row
let cursorCol = 0; // current column
// screen content array
let screen = [['']];
// replace carriage return and newline characters in the text
let input = inputStr.replace(/\r\n/g, '\n');
lastIndex.current = 0;
// handle the \r and \n characters in the text
const handleText = (text: string) => {
let processed = '';
for (let char of text) {
if (char === '\r') {
cursorCol = 0; // move to the beginning of the line
} else if (char === '\n') {
cursorRow++; // move to the next line
cursorCol = 0; // move to the beginning of the line
screen[cursorRow] = screen[cursorRow] || ['']; // create a new line if it does not exist
} else {
// add the character to the screen content array
screen[cursorRow][cursorCol] = char;
cursorCol++;
}
}
return processed;
};
let output = ''; // output text
let match;
// ANSI color map
const colorMap: Record<string, string> = {
'30': 'black',
'31': 'red',
'32': 'green',
'33': 'yellow',
'34': 'blue',
'35': 'magenta',
'36': 'cyan',
'37': 'white'
};
let currentStyle = ''; // current text style
// match ANSI control characters
while ((match = controlSeqRegex.exec(input)) !== null) {
// handle text before the control character
let textBeforeControl = input.slice(lastIndex.current, match.index);
output += handleText(textBeforeControl); // add the processed text to the output
lastIndex.current = controlSeqRegex.lastIndex; // update the last index
const n = parseInt(match[1], 10) || 1;
const m = parseInt(match[2], 10) || 1;
const command = match[3];
console.log('command', {
command,
cursorRow,
n
});
// handle ANSI control characters
switch (command) {
case 'A': // up
cursorRow = Math.max(0, cursorRow - n);
if (cursorRow === 0) {
// screen = [['']];
// cursorCol = 0;
}
break;
case 'B': // down
cursorRow += n;
break;
case 'C': // right
cursorCol += n;
break;
case 'D': // left
cursorCol = Math.max(0, cursorCol - n);
break;
case 'H': // move the cursor to the specified position (n, m)
cursorRow = Math.max(0, n - 1);
cursorCol = Math.max(0, m - 1);
break;
case 'J': // clear the screen
if (n === 2) {
console.log('clear====');
screen = [['']];
cursorRow = 0;
cursorCol = 0;
}
break;
case 'm': // color
if (match[1] === '0') {
currentStyle = '';
} else if (colorMap[match[1]]) {
currentStyle = `color: ${colorMap[match[1]]};`;
}
break;
}
// check if the row and column are within the screen content array
while (screen.length <= cursorRow) {
screen.push(['']);
}
while (screen[cursorRow].length <= cursorCol) {
screen[cursorRow].push('');
}
}
// handle the remaining text
output += handleText(input.slice(lastIndex.current));
let result = [];
for (let row = 0; row < screen.length; row++) {
let rowContent = screen[row].join('');
result.push({
content: removeBrackets(rowContent),
uid: setId()
});
}
result.push({
content: output,
uid: setId()
});
return result;
}, []);
return { parseAnsi, isClean };
};
export default useParseAnsi;
@@ -0,0 +1,139 @@
import { useRef, useState } from 'react';
const usePaseByLine = () => {
const [result, setResult] = useState<any[]>([]);
const uidRef = useRef(0);
const cursorRow = useRef(0); // current row
const cursorCol = useRef(0); // current column
// screen.current content array
const screen = useRef([['']]);
// replace carriage return and newline characters in the text
// let input = inputStr.replace(/\r\n/g, '\n')
const removeBrackets = (str: string) => {
return str?.replace?.(/^\(\.*?\)/, '');
};
const setId = () => {
uidRef.current += 1;
return uidRef.current;
};
// handle the \r and \n characters in the text
const handleText = (text: string) => {
for (let char of text) {
if (char === '\r') {
cursorCol.current = 0; // move to the beginning of the line
} else if (char === '\n') {
cursorRow.current += 1; // move to the next line
cursorCol.current = 0; // move to the beginning of the line
screen.current[cursorRow.current] = screen.current[
cursorRow.current
] || ['']; // create a new line if it does not exist
} else {
// add the character to the screen.current content array
screen.current[cursorRow.current][cursorCol.current] = char;
cursorCol.current += 1;
}
}
};
// ANSI
const controlSeqRegex = /\x1b\[(\d*);?(\d*)?([A-DJKHfm])/g;
// ANSI color map
const colorMap: Record<string, string> = {
'30': 'black',
'31': 'red',
'32': 'green',
'33': 'yellow',
'34': 'blue',
'35': 'magenta',
'36': 'cyan',
'37': 'white'
};
const updateResult = () => {
let res = [];
for (let row = 0; row < screen.current.length; row++) {
console.log('screen.current[row]', screen.current[row]);
let rowContent = screen.current[row].join('');
res.push({
content: removeBrackets(rowContent),
uid: setId()
});
}
console.log('res', res);
setResult(res);
};
const handleLine = (line: string) => {
let match;
let lastIndex = 0;
let currentStyle = ''; // current text style
// match ANSI control characters
while ((match = controlSeqRegex.exec(line)) !== null) {
// handle text before the control character
let textBeforeControl = line.slice(lastIndex, match.index);
handleText(textBeforeControl); // add the processed text to the output
lastIndex = controlSeqRegex.lastIndex; // update the last index
const n = parseInt(match[1], 10) || 1;
const m = parseInt(match[2], 10) || 1;
const command = match[3];
// handle ANSI control characters
switch (command) {
case 'A': // up
cursorRow.current = Math.max(0, cursorRow.current - n);
break;
case 'B': // down
cursorRow.current += n;
break;
case 'C': // right
cursorCol.current += n;
break;
case 'D': // left
cursorCol.current = Math.max(0, cursorCol.current - n);
break;
case 'H': // move the cursor to the specified position (n, m)
cursorRow.current = Math.max(0, n - 1);
cursorCol.current = Math.max(0, m - 1);
break;
case 'J': // clear the screen.current
if (n === 2) {
screen.current = [['']];
cursorRow.current = 0;
cursorCol.current = 0;
}
break;
case 'm': // color
if (match[1] === '0') {
currentStyle = '';
} else if (colorMap[match[1]]) {
currentStyle = `color: ${colorMap[match[1]]};`;
}
break;
}
// check if the row and column are within the screen.current content array
while (screen.current.length <= cursorRow.current) {
screen.current.push(['']);
}
while (screen.current[cursorRow.current].length <= cursorCol.current) {
screen.current[cursorRow.current].push('');
}
}
handleText(line.slice(lastIndex));
updateResult();
};
return {
handleLine,
result
};
};
export default usePaseByLine;
@@ -46,6 +46,7 @@
table { table {
width: 100%; width: 100%;
margin-bottom: 1.2em;
th { th {
font-weight: var(--font-weight-bold); font-weight: var(--font-weight-bold);
+41 -14
View File
@@ -1,5 +1,5 @@
import { EyeOutlined } from '@ant-design/icons'; import { EyeOutlined } from '@ant-design/icons';
import { Image, Typography } from 'antd'; import { Checkbox, Image, Typography } from 'antd';
import { unescape } from 'lodash'; import { unescape } from 'lodash';
import { TokensList, marked } from 'marked'; import { TokensList, marked } from 'marked';
import React, { Fragment, useCallback, useEffect } from 'react'; import React, { Fragment, useCallback, useEffect } from 'react';
@@ -38,12 +38,16 @@ const MarkdownViewer: React.FC<MarkdownViewerProps> = ({
'list_item', 'list_item',
'br', 'br',
'html', 'html',
'escape' 'escape',
'del',
'blockquote',
'checkbox'
// 'bibtex'
]; ];
renderer.link = ({ href, title, text }) => { // renderer.link = ({ href, title, text }) => {
return `<a href="${href}" title="${title || ''}" target="_blank" rel="noopener noreferrer">${text}</a>`; // return `<a href="${href}" title="${title || ''}" target="_blank" rel="noopener noreferrer">${text}</a>`;
}; // };
const isValidURL = useCallback((url: string) => { const isValidURL = useCallback((url: string) => {
const pattern = /^(https?:\/\/|\/\/)([^\s/$.?#].[^\s]*)$/; const pattern = /^(https?:\/\/|\/\/)([^\s/$.?#].[^\s]*)$/;
@@ -51,8 +55,22 @@ const MarkdownViewer: React.FC<MarkdownViewerProps> = ({
return pattern.test(url); return pattern.test(url);
}, []); }, []);
const generateImgSrc = useCallback(
(src: string | null) => {
if (!src) {
return '';
}
if (generateImgLink) {
return isValidURL(src) ? src : generateImgLink(src);
}
return src;
},
[generateImgLink]
);
const renderItem = useCallback( const renderItem = useCallback(
(token: any, render: any) => { (token: any, render: any) => {
console.log('token====', token);
if (!reDefineTypes.includes(token.type)) { if (!reDefineTypes.includes(token.type)) {
return ( return (
<span <span
@@ -88,6 +106,18 @@ const MarkdownViewer: React.FC<MarkdownViewerProps> = ({
htmlstr = <li>{text}</li>; htmlstr = <li>{text}</li>;
} }
if (token.type === 'del') {
htmlstr = <del>{text}</del>;
}
if (token.type === 'blockquote') {
htmlstr = <blockquote>{text}</blockquote>;
}
if (token.type === 'checkbox') {
htmlstr = <Checkbox value={token.checked} />;
}
if (token.type === 'br') { if (token.type === 'br') {
htmlstr = <br />; htmlstr = <br />;
} }
@@ -97,10 +127,7 @@ const MarkdownViewer: React.FC<MarkdownViewerProps> = ({
} }
if (token.type === 'image') { if (token.type === 'image') {
let href = token.href; let href = generateImgSrc(token.href);
if (!isValidURL(token.href)) {
href = generateImgLink ? generateImgLink(token.href) : token.href;
}
htmlstr = ( htmlstr = (
<Image <Image
src={href} src={href}
@@ -126,11 +153,13 @@ const MarkdownViewer: React.FC<MarkdownViewerProps> = ({
if (token.type === 'paragraph') { if (token.type === 'paragraph') {
htmlstr = <Paragraph> {text}</Paragraph>; htmlstr = <Paragraph> {text}</Paragraph>;
} }
if (token.type === 'code') { if (token.type === 'code') {
htmlstr = ( htmlstr = (
<HighlightCode theme={theme} code={token.text} lang={token.lang} /> <HighlightCode theme={theme} code={token.text} lang={token.lang} />
); );
} }
if (token.type === 'link') { if (token.type === 'link') {
htmlstr = ( htmlstr = (
<Link <Link
@@ -149,7 +178,7 @@ const MarkdownViewer: React.FC<MarkdownViewerProps> = ({
return htmlstr; return htmlstr;
}, },
[generateImgLink] [generateImgSrc]
); );
const renderTokens = (tokens: TokensList): any => { const renderTokens = (tokens: TokensList): any => {
return tokens?.map((token: any, index: number) => { return tokens?.map((token: any, index: number) => {
@@ -164,11 +193,9 @@ const MarkdownViewer: React.FC<MarkdownViewerProps> = ({
const imgs = document.querySelectorAll('.markdown-viewer img'); const imgs = document.querySelectorAll('.markdown-viewer img');
imgs.forEach((img) => { imgs.forEach((img) => {
const src = img.getAttribute('src'); const src = img.getAttribute('src');
if (src && !isValidURL(src)) { img.setAttribute('src', generateImgSrc(src));
img.setAttribute('src', generateImgLink ? generateImgLink(src) : src);
}
}); });
}, [content, generateImgLink]); }, [content, generateImgSrc]);
return ( return (
<div <div
+1 -1
View File
@@ -9,7 +9,7 @@ interface WrapperProps {
className?: string; className?: string;
disabled?: boolean; disabled?: boolean;
required?: boolean; required?: boolean;
description?: string; description?: React.ReactNode;
variant?: string; variant?: string;
style?: React.CSSProperties; style?: React.CSSProperties;
onClick?: () => void; onClick?: () => void;
+115
View File
@@ -0,0 +1,115 @@
import { CloseOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Button, Input, Tooltip } from 'antd';
import classNames from 'classnames';
import React, { useState } from 'react';
import './styles/row-textarea.less';
interface SystemMessageProps {
style?: React.CSSProperties;
value: string;
placeholder?: string;
label?: React.ReactNode;
onChange: (e: any) => void;
}
const RowTextarea: React.FC<SystemMessageProps> = (props) => {
const { value, onChange, style, label, placeholder } = props;
const intl = useIntl();
const rowTextAreaRef = React.useRef<any>(null);
const [autoSize, setAutoSize] = useState<{
minRows: number;
maxRows: number;
focus: boolean;
}>({ minRows: 1, maxRows: 1, focus: false });
const handleFocus = () => {
setAutoSize({
minRows: 3,
maxRows: 3,
focus: true
});
setTimeout(() => {
rowTextAreaRef.current?.focus?.({
cursor: 'end'
});
}, 50);
};
const handleBlur = (e: any) => {
setAutoSize({
minRows: 2,
maxRows: 2,
focus: false
});
};
const handleOnChange = (e: any) => {
onChange?.(e);
};
const handleClear = () => {
onChange?.({ target: { value: '' } });
};
return (
<div
className={classNames('row-textarea', {
focus: autoSize.focus
})}
style={{ ...style }}
>
{
<div
style={{ display: autoSize.focus ? 'block' : 'none' }}
className="textarea-wrapper"
>
<span className="textarea-label">{label}</span>
<Input.TextArea
className="custome-scrollbar"
ref={rowTextAreaRef}
placeholder={placeholder}
style={{
borderRadius: '0',
border: 'none'
}}
value={value}
autoSize={{
minRows: autoSize.minRows,
maxRows: autoSize.maxRows
}}
onFocus={handleFocus}
onBlur={handleBlur}
allowClear={false}
onChange={handleOnChange}
></Input.TextArea>
</div>
}
{!autoSize.focus && (
<div className="content-wrap" onClick={handleFocus}>
<div className="content">
<span className="title">{label}</span>
{value || (
<span style={{ color: 'var(--ant-color-text-tertiary)' }}>
{placeholder}
</span>
)}
</div>
{value && (
<Tooltip title={intl.formatMessage({ id: 'common.button.clear' })}>
<Button
className="clear-btn"
type="text"
icon={<CloseOutlined />}
size="small"
onClick={handleClear}
></Button>
</Tooltip>
)}
</div>
)}
</div>
);
};
export default React.memo(RowTextarea);
@@ -0,0 +1,60 @@
.row-textarea {
position: relative;
&.focus {
padding-top: 9px;
}
.content-wrap {
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
padding-right: 20px;
cursor: pointer;
&:hover {
.clear-btn {
display: block;
}
}
}
textarea.ant-input {
background-color: transparent;
box-shadow: none;
}
.clear-btn {
display: none;
position: absolute;
right: 6px;
top: 6px;
}
.textarea-label {
position: relative;
top: 1px;
font-weight: var(--font-weight-bold);
padding-left: 14px;
}
.content {
flex: 1;
width: 100px;
height: 40px;
line-height: 24px;
padding: 8px 14px;
padding-right: 4px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
color: var(--ant-color-text-secondary);
.title {
font-weight: var(--font-weight-normal);
padding-right: 10px;
color: var(--ant-color-text);
}
}
}
@@ -46,6 +46,7 @@ const TableRow: React.FC<
const { updateChunkedList, cacheDataListRef } = useUpdateChunkedList({ const { updateChunkedList, cacheDataListRef } = useUpdateChunkedList({
dataList: childrenData, dataList: childrenData,
limit: 100,
setDataList: setChildrenData setDataList: setChildrenData
// callback: (list) => renderChildren?.(list) // callback: (list) => renderChildren?.(list)
}); });
@@ -118,7 +119,7 @@ const TableRow: React.FC<
const updateChildrenHandler = (list: any) => { const updateChildrenHandler = (list: any) => {
_.each(list, (data: any) => { _.each(list, (data: any) => {
updateChunkedList(data, childrenDataRef.current); updateChunkedList(data);
}); });
}; };
+5 -2
View File
@@ -46,7 +46,10 @@ const SealTable: React.FC<SealTableProps & { pagination: PaginationProps }> = (
if (selectedRowKeys?.length === 0) { if (selectedRowKeys?.length === 0) {
setSelectAll(false); setSelectAll(false);
setIndeterminate(false); setIndeterminate(false);
} else if (selectedRowKeys?.length === props.dataSource.length) { } else if (
selectedRowKeys?.length === props.dataSource.length &&
selectedRowKeys.length > 0
) {
setSelectAll(true); setSelectAll(true);
setIndeterminate(false); setIndeterminate(false);
} else { } else {
@@ -54,7 +57,7 @@ const SealTable: React.FC<SealTableProps & { pagination: PaginationProps }> = (
setIndeterminate(true); setIndeterminate(true);
} }
} }
}, [rowSelection]); }, [rowSelection, props.dataSource]);
const handleSelectAllChange = (e: any) => { const handleSelectAllChange = (e: any) => {
if (e.target.checked) { if (e.target.checked) {
+2 -1
View File
@@ -4,13 +4,14 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: flex-start; justify-content: flex-start;
height: 68px; min-height: 68px;
word-break: break-word; word-break: break-word;
min-width: 20px; min-width: 20px;
overflow: hidden; overflow: hidden;
.cell-content { .cell-content {
max-width: 100%; max-width: 100%;
line-height: 18px;
} }
&-left { &-left {
@@ -63,6 +63,8 @@
} }
.row-prefix-wrapper { .row-prefix-wrapper {
display: flex;
align-items: center;
padding-left: var(--ant-table-cell-padding-inline); padding-left: var(--ant-table-cell-padding-inline);
} }
} }
+26 -5
View File
@@ -1,6 +1,5 @@
.version-box { .version-box {
display: flex; display: flex;
margin-bottom: 40px;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
@@ -23,20 +22,42 @@
} }
.ver { .ver {
margin-bottom: 20px;
line-height: 32px; line-height: 32px;
display: flex; display: flex;
justify-content: center;
font-size: var(--font-size-middle); font-size: var(--font-size-middle);
color: var(--ant-color-text-secondary);
gap: 10px;
.label { .label {
display: flex; display: flex;
justify-content: flex-start; justify-content: flex-start;
width: 60px;
font-size: var(--font-size-middle); font-size: var(--font-size-middle);
font-weight: var(--font-weight-medium);
} }
.val { .dev.val {
color: var(--ant-color-text-secondary); display: flex;
flex-direction: column;
.item {
display: flex;
}
.tl {
display: flex;
justify-content: flex-start;
width: 60px;
} }
} }
} }
.upgrade {
display: flex;
justify-content: space-between;
padding-block: 20px 4px;
align-items: center;
width: 100%;
border-top: 1px solid var(--ant-color-split);
}
}
+56 -10
View File
@@ -1,34 +1,80 @@
import Logo from '@/assets/images/gpustack-logo.png'; import Logo from '@/assets/images/gpustack-logo.png';
import { GPUStackVersionAtom } from '@/atoms/user'; import { GPUStackVersionAtom, UpdateCheckAtom, userAtom } from '@/atoms/user';
import { getAtomStorage } from '@/atoms/utils'; import { getAtomStorage } from '@/atoms/utils';
import externalLinks from '@/constants/external-links';
import { Button } from 'antd';
import React from 'react'; import React from 'react';
import './index.less'; import './index.less';
const VersionInfo: React.FC<{ intl: any }> = ({ intl }) => { const VersionInfo: React.FC<{ intl: any }> = ({ intl }) => {
// get the data attr from html const latestVersion = getAtomStorage(UpdateCheckAtom).latest_version;
const version = document.documentElement.getAttribute('data-version'); const currentVersion = getAtomStorage(GPUStackVersionAtom)?.version;
const isProd =
currentVersion !== '0.0.0' && currentVersion?.indexOf('rc') === -1;
const uiVersion = document.documentElement.getAttribute('data-version');
return ( return (
<div className="version-box"> <div className="version-box">
<div className="img"> <div className="img">
<img src={Logo} alt="logo" /> <img src={Logo} alt="logo" />
</div> </div>
<div>
<div className="ver"> <div className="ver">
{isProd && (
<span className="label"> <span className="label">
{' '} {intl.formatMessage({ id: 'common.footer.version' })}
{intl.formatMessage({ id: 'common.footer.version.server' })}
</span> </span>
)}
{isProd ? (
<span className="val"> <span className="val">
{getAtomStorage(GPUStackVersionAtom)?.version || {getAtomStorage(GPUStackVersionAtom)?.version ||
getAtomStorage(GPUStackVersionAtom)?.git_commit} getAtomStorage(GPUStackVersionAtom)?.git_commit}
</span> </span>
) : (
<span className="val dev">
<span className="item">
<span className="tl">
{' '}
{intl.formatMessage({ id: 'common.footer.version.server' })}
</span>
{currentVersion === '0.0.0'
? getAtomStorage(GPUStackVersionAtom)?.git_commit
: getAtomStorage(GPUStackVersionAtom)?.version}
</span>
<span className="item">
<span className="tl">UI</span>
{uiVersion}
</span>
</span>
)}
</div> </div>
<div className="ver"> {getAtomStorage(userAtom)?.is_admin && isProd && (
<span className="label">UI </span> <div className="upgrade">
<span className="val"> {version}</span> <span className="m-l-5">
</div> {latestVersion &&
latestVersion !== currentVersion &&
latestVersion !== '0.0.0'
? intl.formatMessage(
{ id: 'users.version.update' },
{ version: latestVersion }
)
: intl.formatMessage(
{ id: 'users.version.islatest' },
{ version: currentVersion }
)}
</span>
<Button
type="link"
href={externalLinks.release}
target="_blank"
style={{ paddingInline: 0 }}
>
{intl.formatMessage({ id: 'common.text.changelog' })}
</Button>
</div> </div>
)}
</div> </div>
); );
}; };
+2 -1
View File
@@ -2,5 +2,6 @@ export default {
documentation: 'https://docs.gpustack.ai/', documentation: 'https://docs.gpustack.ai/',
github: 'https://github.com/gpustack/gpustack', github: 'https://github.com/gpustack/gpustack',
discord: 'https://discord.gg/VXYJzuaqwD', discord: 'https://discord.gg/VXYJzuaqwD',
site: 'https://gpustack.ai/' site: 'https://gpustack.ai/',
release: 'https://github.com/gpustack/gpustack/releases'
}; };
+64
View File
@@ -15,6 +15,7 @@ html {
--color-scrollbar-thumb: rgba(193, 193, 193, 80%); --color-scrollbar-thumb: rgba(193, 193, 193, 80%);
--scrollbar-size: 6px; --scrollbar-size: 6px;
--scrollbar-handle-bg: rgba(0, 0, 0, 44%); --scrollbar-handle-bg: rgba(0, 0, 0, 44%);
--scrollbar-handle-light-bg: rgba(255, 255, 255, 44%);
--scrollbar-handle-hover-bg: rgba(0, 0, 0, 55%); --scrollbar-handle-hover-bg: rgba(0, 0, 0, 55%);
--color-editor-dark: #282c34; --color-editor-dark: #282c34;
--color-editor-light: #fafafa; --color-editor-light: #fafafa;
@@ -64,6 +65,8 @@ html {
--seal-transition-func: cubic-bezier(0, 0, 1, 1); --seal-transition-func: cubic-bezier(0, 0, 1, 1);
--color-progress-green: rgba(84, 204, 152, 100%); --color-progress-green: rgba(84, 204, 152, 100%);
--color-border-1: rgba(217, 217, 217, 100%); --color-border-1: rgba(217, 217, 217, 100%);
--ant-rate-star-color: #fadb14;
--color-fill-mask: rgba(255, 255, 255, 60%);
// ======== input ============ // ======== input ============
--ant-input-active-shadow: 0 0 0 2px rgba(5, 255, 105, 6%); --ant-input-active-shadow: 0 0 0 2px rgba(5, 255, 105, 6%);
--ant-input-active-border-color: #007bff; --ant-input-active-border-color: #007bff;
@@ -169,6 +172,10 @@ body {
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
&::-webkit-scrollbar {
width: 0;
}
.ant-modal-confirm-title { .ant-modal-confirm-title {
display: none; display: none;
} }
@@ -381,6 +388,7 @@ body {
} }
.ant-pro-layout-container { .ant-pro-layout-container {
overflow-x: auto;
min-height: 100vh; min-height: 100vh;
// background-color: var(--color-fill-2); // background-color: var(--color-fill-2);
} }
@@ -432,6 +440,26 @@ body {
font-size: var(--font-size-middle); font-size: var(--font-size-middle);
} }
} }
.new-icon {
position: relative;
bottom: 3px;
display: flex;
color: #fff;
width: 36px;
height: 15px;
margin-left: 5px;
justify-content: center;
align-items: center;
background-color: var(--ant-orange-5);
border-radius: 8px 8px 8px 0;
transform: scale(0.9);
.text {
transform: scale(0.8);
line-height: 1em;
}
}
} }
} }
@@ -444,6 +472,16 @@ body {
} }
&.user-menu-collapsed { &.user-menu-collapsed {
.ant-menu-submenu-title {
display: flex;
justify-content: center;
align-items: center;
color: var(--color-text-1) !important;
padding-inline: unset !important;
width: 100%;
margin-inline: 0 !important;
}
.user-avatar.ant-menu-submenu { .user-avatar.ant-menu-submenu {
.ant-menu-submenu-title { .ant-menu-submenu-title {
padding: 0 !important; padding: 0 !important;
@@ -477,6 +515,28 @@ body {
border-radius: 8px; border-radius: 8px;
} }
.ant-modal-centered.ant-modal-wrap {
&::-webkit-scrollbar {
width: var(--scrollbar-size);
}
&::-webkit-scrollbar-thumb {
background-color: transparent;
border-radius: 4px;
}
&::-webkit-scrollbar-track {
background-color: transparent;
}
&:hover {
&::-webkit-scrollbar-thumb {
background-color: var(--scrollbar-handle-bg);
border-radius: 4px;
}
}
}
.custome-scrollbar { .custome-scrollbar {
&::-webkit-scrollbar { &::-webkit-scrollbar {
width: var(--scrollbar-size); width: var(--scrollbar-size);
@@ -649,6 +709,10 @@ body {
.ant-pro-page-container { .ant-pro-page-container {
background: transparent; background: transparent;
.ant-page-header-heading {
min-width: max-content;
}
} }
@keyframes skeleton-loading { @keyframes skeleton-loading {
+137
View File
@@ -0,0 +1,137 @@
import { split } from 'lodash';
import qs from 'query-string';
import { useEffect, useRef } from 'react';
interface RequestConfig {
url: string;
handler: (data: any) => any;
beforeReconnect?: () => void;
params?: object;
byLine?: boolean;
contentType?: 'json' | 'text';
}
const useSetChunkFetch = () => {
const axiosToken = useRef<any>(null);
const requestConfig = useRef<any>({});
const chunkDataRef = useRef<any>([]);
const bufferCacheRef = useRef<any>('');
const readTextEventStreamData = async (
reader: ReadableStreamDefaultReader<Uint8Array>,
decoder: TextDecoder,
callback: (data: any) => void
) => {
const { done, value } = await reader.read();
if (done) {
return;
}
const chunk = decoder.decode(value, { stream: true });
callback(chunk);
// console.log('chunkDataRef.current===2', chunkDataRef.current);
await readTextEventStreamData(reader, decoder, callback);
};
const readTextEventStreamDataByLine = async (
reader: ReadableStreamDefaultReader<Uint8Array>,
decoder: TextDecoder,
callback: (data: any) => void
) => {
const { done, value } = await reader.read();
if (done) {
return;
}
bufferCacheRef.current += decoder.decode(value, { stream: true });
const lines = split(bufferCacheRef.current, /\r?\n/);
bufferCacheRef.current = lines.pop();
for (const line of lines) {
callback(line);
}
await readTextEventStreamDataByLine(reader, decoder, callback);
};
const readTextEventStreamDataByLineWithBuffer = async (
reader: ReadableStreamDefaultReader<Uint8Array>,
decoder: TextDecoder,
callback: (data: any) => void
) => {
await readTextEventStreamDataByLine(reader, decoder, callback);
if (bufferCacheRef.current.length > 0) {
callback(bufferCacheRef.current);
}
};
const fetchChunkRequest = async ({
url,
handler,
byLine = false,
params = {}
}: RequestConfig) => {
axiosToken.current?.abort?.();
axiosToken.current = new AbortController();
try {
const response = await fetch(
`v1${url}?${qs.stringify({
...params,
watch: true
})}`,
{
method: 'GET',
body: null,
headers: {
'Content-Type': 'application/octet-stream'
},
signal: axiosToken.current.signal
}
);
if (!response.ok) {
return;
}
const reader =
response?.body?.getReader() as ReadableStreamDefaultReader<Uint8Array>;
const decoder = new TextDecoder('utf-8');
await readTextEventStreamData(reader, decoder, handler);
console.log('chunkDataRef.current===1', chunkDataRef.current);
} catch (error) {
// handle error
console.log('error============', error);
}
return axiosToken.current;
};
const setChunkFetch = (config: RequestConfig) => {
requestConfig.current = { ...config };
fetchChunkRequest(requestConfig.current);
return axiosToken;
};
useEffect(() => {
const handleUnload = () => {
axiosToken.current?.abort?.();
};
window.addEventListener('beforeunload', handleUnload);
return () => {
axiosToken.current?.abort?.();
window.removeEventListener('beforeunload', handleUnload);
};
}, []);
return {
setChunkFetch
};
};
export default useSetChunkFetch;
+1 -2
View File
@@ -172,7 +172,7 @@ const useSetChunkRequest = () => {
let result = response; let result = response;
let cres = ''; let cres = '';
console.log('chunkrequest============e==', e); console.log('chunkrequest============e==', result);
if (contentType === 'json') { if (contentType === 'json') {
const currentRes = sliceData(response, e.loaded, loadedSize); const currentRes = sliceData(response, e.loaded, loadedSize);
result = parseData(currentRes); result = parseData(currentRes);
@@ -194,7 +194,6 @@ const useSetChunkRequest = () => {
retryCount.current -= 1; retryCount.current -= 1;
} }
} catch (error) { } catch (error) {
console.log('error=============', error);
if (!axios.isCancel(error)) { if (!axios.isCancel(error)) {
setRequestReadyState(4); setRequestReadyState(4);
if (retryCount.current > 0) { if (retryCount.current > 0) {
+72 -25
View File
@@ -1,7 +1,7 @@
import { throttle } from 'lodash'; import { throttle } from 'lodash';
import { import {
useOverlayScrollbars, UseOverlayScrollbarsParams,
UseOverlayScrollbarsParams useOverlayScrollbars
} from 'overlayscrollbars-react'; } from 'overlayscrollbars-react';
import React from 'react'; import React from 'react';
@@ -14,6 +14,7 @@ export const overlaySollerOptions: UseOverlayScrollbarsParams = {
x: 'hidden' x: 'hidden'
}, },
scrollbars: { scrollbars: {
theme: 'os-theme-light',
autoHide: 'scroll', autoHide: 'scroll',
autoHideDelay: 600, autoHideDelay: 600,
clickScroll: 'instant' clickScroll: 'instant'
@@ -22,51 +23,97 @@ export const overlaySollerOptions: UseOverlayScrollbarsParams = {
defer: true defer: true
}; };
export default function useOverlayScroller() { export default function useOverlayScroller(options?: any) {
const scrollEventElement = React.useRef<any>(null); const scrollEventElement = React.useRef<any>(null);
const instanceRef = React.useRef<any>(null); const instanceRef = React.useRef<any>(null);
const initialized = React.useRef(false);
const [initialize, instance] = useOverlayScrollbars({ const [initialize, instance] = useOverlayScrollbars({
...overlaySollerOptions options: {
update: {
debounce: 0
},
overflow: {
x: 'hidden'
},
scrollbars: {
theme: options?.theme || 'os-theme-dark',
autoHide: 'scroll',
autoHideDelay: 600,
clickScroll: 'instant'
}
},
defer: true
}); });
instanceRef.current = instance?.(); instanceRef.current = instance?.();
scrollEventElement.current = scrollEventElement.current =
instanceRef.current?.elements()?.scrollEventElement; instanceRef.current?.elements()?.scrollEventElement;
const throttledScroll = React.useMemo( const throttledScroll = React.useCallback(
() =>
throttle(() => { throttle(() => {
scrollEventElement.current?.scrollTo?.({ scrollEventElement.current?.scrollTo?.({
top: scrollEventElement.current.scrollHeight, top: scrollEventElement.current.scrollHeight,
behavior: 'smooth' behavior: 'smooth'
}); });
instanceRef.current?.update?.(); instanceRef.current?.update?.();
}, 300), }, 100),
[scrollEventElement, instanceRef] [(scrollEventElement.current, instanceRef.current)]
); );
const throttledUpdateScrollerPosition = React.useCallback(() => { const scrollauto = React.useCallback(() => {
scrollEventElement.current?.scrollTo?.({
top: scrollEventElement.current.scrollHeight,
behavior: 'auto'
});
instanceRef.current?.update?.();
}, [scrollEventElement.current, instanceRef.current]);
const throttledUpdateScrollerPosition = React.useCallback(
(delay?: number) => {
if (delay === 0) {
scrollauto();
} else {
throttledScroll(); throttledScroll();
}, [throttledScroll]); }
},
[throttledScroll, scrollauto]
);
// const createInstance = React.useCallback((el: any) => { const generateInstance = () => {
// if (el) { instanceRef.current = instance?.();
// instanceRef.current?.destroy?.(); scrollEventElement.current =
// initialize(el); instanceRef.current?.elements()?.scrollEventElement;
// instanceRef.current = instance?.();
// scrollEventElement.current =
// instanceRef.current?.elements()?.scrollEventElement;
// }
// }, []);
React.useEffect(() => {
return () => {
instanceRef.current?.destroy?.();
}; };
}, []);
const createInstance = React.useCallback(
(el: any) => {
if (instanceRef.current) {
return;
}
if (el) {
initialize(el);
initialized.current = true;
console.log(
'createInstance===2',
initialized.current,
instanceRef.current,
instance?.(),
instance
);
instanceRef.current = instance?.();
scrollEventElement.current =
instanceRef.current?.elements()?.scrollEventElement;
}
},
[initialize, instance]
);
return { return {
initialize, initialize: createInstance,
instance: instanceRef.current, instance: instanceRef.current,
scrollEventElement: scrollEventElement.current,
initialized: initialized.current,
generateInstance,
updateScrollerPosition: throttledUpdateScrollerPosition updateScrollerPosition: throttledUpdateScrollerPosition
}; };
} }
+6 -1
View File
@@ -11,10 +11,15 @@ export default function useTableRowSelection() {
setSelectedRowKeys([]); setSelectedRowKeys([]);
}; };
const removeSelectedKey = (rowKey: React.Key) => {
setSelectedRowKeys((keys) => keys.filter((key) => key !== rowKey));
};
const rowSelection = { const rowSelection = {
selectedRowKeys, selectedRowKeys,
clearSelections, clearSelections,
onChange: onSelectChange onChange: onSelectChange,
removeSelectedKey
}; };
return rowSelection; return rowSelection;
+4 -2
View File
@@ -10,6 +10,7 @@ interface ChunkedCollection {
// Only used to update lists without nested state // Only used to update lists without nested state
export function useUpdateChunkedList(options: { export function useUpdateChunkedList(options: {
dataList?: any[]; dataList?: any[];
limit?: number;
setDataList: (args: any) => void; setDataList: (args: any) => void;
callback?: (args: any) => void; callback?: (args: any) => void;
filterFun?: (args: any) => boolean; filterFun?: (args: any) => boolean;
@@ -19,6 +20,7 @@ export function useUpdateChunkedList(options: {
const cacheDataListRef = useRef<any[]>(options.dataList || []); const cacheDataListRef = useRef<any[]>(options.dataList || []);
const timerRef = useRef<any>(null); const timerRef = useRef<any>(null);
const countRef = useRef<number>(0); const countRef = useRef<number>(0);
const limit = options.limit || 10;
useEffect(() => { useEffect(() => {
cacheDataListRef.current = [...(options.dataList || [])]; cacheDataListRef.current = [...(options.dataList || [])];
@@ -68,7 +70,7 @@ export function useUpdateChunkedList(options: {
cacheDataListRef.current = [ cacheDataListRef.current = [
...newDataList, ...newDataList,
...cacheDataListRef.current ...cacheDataListRef.current
].slice(0, 10); ].slice(0, limit);
} }
// DELETE // DELETE
if (data?.type === WatchEventType.DELETE) { if (data?.type === WatchEventType.DELETE) {
@@ -91,7 +93,7 @@ export function useUpdateChunkedList(options: {
} else if (updateIndex === -1) { } else if (updateIndex === -1) {
cacheDataListRef.current = [ cacheDataListRef.current = [
updateItem, updateItem,
...cacheDataListRef.current.slice(0, 9) ...cacheDataListRef.current.slice(0, limit - 1)
]; ];
} }
}); });
+40 -4
View File
@@ -1,10 +1,11 @@
// @ts-nocheck // @ts-nocheck
import { userAtom } from '@/atoms/user'; import { GPUStackVersionAtom, UpdateCheckAtom, userAtom } from '@/atoms/user';
import ShortCuts, { import ShortCuts, {
modalConfig as ShortCutsConfig modalConfig as ShortCutsConfig
} from '@/components/short-cuts'; } from '@/components/short-cuts';
import VersionInfo, { modalConfig } from '@/components/version-info'; import VersionInfo, { modalConfig } from '@/components/version-info';
import useOverlayScroller from '@/hooks/use-overlay-scroller';
import { logout } from '@/pages/login/apis'; import { logout } from '@/pages/login/apis';
import { useAccessMarkedRoutes } from '@@/plugin-access'; import { useAccessMarkedRoutes } from '@@/plugin-access';
import { useModel } from '@@/plugin-model'; import { useModel } from '@@/plugin-model';
@@ -23,12 +24,14 @@ import {
} from '@umijs/max'; } from '@umijs/max';
import { Button, Modal } from 'antd'; import { Button, Modal } from 'antd';
import { useAtom } from 'jotai'; import { useAtom } from 'jotai';
import { useCallback, useMemo, useState } from 'react'; import 'overlayscrollbars/overlayscrollbars.css';
import { useCallback, useEffect, useMemo, useState } from 'react';
import Exception from './Exception'; import Exception from './Exception';
import './Layout.css'; import './Layout.css';
import { LogoIcon, SLogoIcon } from './Logo'; import { LogoIcon, SLogoIcon } from './Logo';
import { getRightRenderContent } from './rightRender'; import { getRightRenderContent } from './rightRender';
import { patchRoutes } from './runtime'; import { patchRoutes } from './runtime';
const loginPath = '/login'; const loginPath = '/login';
type InitialStateType = { type InitialStateType = {
@@ -87,7 +90,11 @@ const mapRoutes = (routes: IRoute[], role: string) => {
}; };
export default (props: any) => { export default (props: any) => {
const { initialize: initialize } = useOverlayScroller();
const { initialize: initializeMenu } = useOverlayScroller();
const [userInfo] = useAtom(userAtom); const [userInfo] = useAtom(userAtom);
const [version] = useAtom(GPUStackVersionAtom);
const [updateCheck] = useAtom(UpdateCheckAtom);
const location = useLocation(); const location = useLocation();
const navigate = useNavigate(); const navigate = useNavigate();
const intl = useIntl(); const intl = useIntl();
@@ -115,6 +122,7 @@ export default (props: any) => {
const showVersion = () => { const showVersion = () => {
Modal.info({ Modal.info({
...modalConfig, ...modalConfig,
width: 460,
content: <VersionInfo intl={intl} /> content: <VersionInfo intl={intl} />
}); });
}; };
@@ -169,6 +177,30 @@ export default (props: any) => {
[location.pathname] [location.pathname]
); );
const showUpgrade = useMemo(() => {
return (
initialState?.currentUser?.is_admin &&
updateCheck.latest_version &&
updateCheck.latest_version !== version?.version
);
}, [updateCheck, version, initialState]);
useEffect(() => {
const body = document.querySelector('body');
if (body) {
initialize(body);
}
}, [initialize]);
useEffect(() => {
const menuWrap = document.querySelector(
'.ant-menu.ant-menu-root'
)?.parentElement;
if (menuWrap) {
initializeMenu(menuWrap);
}
}, [initializeMenu]);
const renderMenuHeader = useCallback( const renderMenuHeader = useCallback(
(logo, title) => { (logo, title) => {
return ( return (
@@ -206,12 +238,13 @@ export default (props: any) => {
setInitialState, setInitialState,
intl, intl,
siderWidth: layoutProps.siderWidth, siderWidth: layoutProps.siderWidth,
collapsed: layoutProps.collapsed collapsed: layoutProps.collapsed,
showUpgrade
}); });
return dom; return dom;
}, },
[intl] [intl, version, updateCheck]
); );
return ( return (
@@ -225,6 +258,9 @@ export default (props: any) => {
layout="side" layout="side"
disableMobile={true} disableMobile={true}
siderWidth={220} siderWidth={220}
onCollapse={(collapsed) => {
setCollapsed(collapsed);
}}
onMenuHeaderClick={(e) => { onMenuHeaderClick={(e) => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
+31 -13
View File
@@ -26,7 +26,7 @@ export const getRightRenderContent = (opts: {
siderWidth: number; siderWidth: number;
intl: any; intl: any;
}) => { }) => {
const { intl, collapsed, siderWidth } = opts; const { intl, collapsed, siderWidth, showUpgrade } = opts;
const allLocals = getAllLocales(); const allLocals = getAllLocales();
if (opts.runtimeConfig.rightRender) { if (opts.runtimeConfig.rightRender) {
@@ -99,11 +99,6 @@ export const getRightRenderContent = (opts: {
icon: <InfoCircleOutlined />, icon: <InfoCircleOutlined />,
label: intl.formatMessage({ id: 'common.button.version' }) label: intl.formatMessage({ id: 'common.button.version' })
} }
// {
// key: 'shortcuts',
// icon: <IconFont type="icon-keyboard"></IconFont>,
// label: intl.formatMessage({ id: 'common.button.shortcut' })
// }
]; ];
const helpMenu = { const helpMenu = {
@@ -113,7 +108,7 @@ export const getRightRenderContent = (opts: {
: 'user-menu-container', : 'user-menu-container',
mode: 'vertical', mode: 'vertical',
expandIcon: false, expandIcon: false,
inlineCollapsed: collapsed, // inlineCollapsed: collapsed,
triggerSubMenuAction: 'hover', triggerSubMenuAction: 'hover',
items: [ items: [
{ {
@@ -121,7 +116,21 @@ export const getRightRenderContent = (opts: {
icon: <QuestionCircleOutlined />, icon: <QuestionCircleOutlined />,
label: ( label: (
<span className="sub-title "> <span className="sub-title ">
{intl?.formatMessage?.({ id: 'common.button.help' })} <span className="flex-center">
<span>{intl?.formatMessage?.({ id: 'common.button.help' })}</span>
{showUpgrade && (
<span
className="m-l-5"
style={{
display: 'flex',
width: 8,
height: 8,
borderRadius: 4,
backgroundColor: 'var(--ant-orange-5)'
}}
></span>
)}
</span>
</span> </span>
), ),
children: helpList.map((item) => ({ children: helpList.map((item) => ({
@@ -130,7 +139,16 @@ export const getRightRenderContent = (opts: {
<span className="flex flex-center"> <span className="flex flex-center">
{item.icon} {item.icon}
{item.key === 'version' ? ( {item.key === 'version' ? (
<>
<a className="m-l-8">{item.label}</a> <a className="m-l-8">{item.label}</a>
{showUpgrade && (
<span className="new-icon">
<span className="text">
{intl.formatMessage({ id: 'common.text.new' })}
</span>
</span>
)}
</>
) : ( ) : (
<a <a
className="m-l-8" className="m-l-8"
@@ -163,7 +181,7 @@ export const getRightRenderContent = (opts: {
: 'user-menu-container', : 'user-menu-container',
mode: 'vertical', mode: 'vertical',
expandIcon: false, expandIcon: false,
inlineCollapsed: collapsed, // inlineCollapsed: collapsed,
triggerSubMenuAction: 'hover', triggerSubMenuAction: 'hover',
items: [ items: [
{ {
@@ -196,7 +214,7 @@ export const getRightRenderContent = (opts: {
: 'user-menu-container', : 'user-menu-container',
mode: 'vertical', mode: 'vertical',
expandIcon: false, expandIcon: false,
inlineCollapsed: collapsed, // inlineCollapsed: collapsed,
triggerSubMenuAction: 'hover', triggerSubMenuAction: 'hover',
items: [ items: [
{ {
@@ -263,19 +281,19 @@ export const getRightRenderContent = (opts: {
<Menu <Menu
{...helpMenu} {...helpMenu}
style={{ style={{
width: collapsed ? 64 : `calc(${siderWidth}px - 16px)` width: collapsed ? 40 : `calc(${siderWidth}px - 16px)`
}} }}
></Menu> ></Menu>
<Menu <Menu
{...langMenu} {...langMenu}
style={{ style={{
width: collapsed ? 64 : `calc(${siderWidth}px - 16px)` width: collapsed ? 40 : `calc(${siderWidth}px - 16px)`
}} }}
></Menu> ></Menu>
<Menu <Menu
{...userMenu} {...userMenu}
style={{ style={{
width: collapsed ? 64 : `calc(${siderWidth}px - 16px)`, width: collapsed ? 40 : `calc(${siderWidth}px - 16px)`,
marginTop: 20 marginTop: 20
}} }}
></Menu> ></Menu>
+6 -2
View File
@@ -88,7 +88,7 @@ export default {
'common.tips.title': 'Tips:', 'common.tips.title': 'Tips:',
'common.input.range.start': 'start value', 'common.input.range.start': 'start value',
'common.input.range.end': 'end value', 'common.input.range.end': 'end value',
'common.footer.version': 'Versions', 'common.footer.version': 'Version',
'common.footer.version.title': 'Version Information', 'common.footer.version.title': 'Version Information',
'common.footer.version.web': 'UI', 'common.footer.version.web': 'UI',
'common.footer.version.server': 'Server', 'common.footer.version.server': 'Server',
@@ -206,5 +206,9 @@ export default {
'common.title.delete.confirm': 'Confirm delete', 'common.title.delete.confirm': 'Confirm delete',
'common.button.addLabel': 'Add Labels', 'common.button.addLabel': 'Add Labels',
'common.button.addSelector': 'Add Selectors', 'common.button.addSelector': 'Add Selectors',
'common.button.addParams': 'Add Parameter' 'common.button.addParams': 'Add Parameter',
'common.text.here': 'here',
'common.text.latest': 'Latest',
'common.text.new': 'New',
'common.text.changelog': 'Release Notes'
}; };
+3 -2
View File
@@ -53,7 +53,7 @@ export default {
'models.form.manual.schedule': 'Manual Schedule', 'models.form.manual.schedule': 'Manual Schedule',
'models.table.gpuindex': 'GPU Index', 'models.table.gpuindex': 'GPU Index',
'models.table.backend': 'Backends', 'models.table.backend': 'Backends',
'models.table.acrossworker': 'Distribution Across Workers', 'models.table.acrossworker': 'Distributed Across Workers',
'models.table.cpuoffload': 'CPU Offload', 'models.table.cpuoffload': 'CPU Offload',
'models.table.layers': 'Layers', 'models.table.layers': 'Layers',
'models.form.backend': 'Backend', 'models.form.backend': 'Backend',
@@ -64,5 +64,6 @@ export default {
'models.form.backend_parameters.llamabox.placeholder': 'models.form.backend_parameters.llamabox.placeholder':
'e.g., --ctx-size=8192', 'e.g., --ctx-size=8192',
'models.form.backend_parameters.vllm.placeholder': 'models.form.backend_parameters.vllm.placeholder':
'e.g., --max-model-len=8192' 'e.g., --max-model-len=8192',
'models.form.backend_parameters.vllm.tips': 'More {backend} parameter details'
}; };
+1
View File
@@ -42,6 +42,7 @@ export default {
'playground.toolbar.compare4Model': '4-Model Comparison', 'playground.toolbar.compare4Model': '4-Model Comparison',
'playground.toolbar.compare6Model': '6-Model Comparison', 'playground.toolbar.compare6Model': '6-Model Comparison',
'playground.input.holder': 'Type <kbd>/</kbd> to input message', 'playground.input.holder': 'Type <kbd>/</kbd> to input message',
'playground.input.keyword.holder': 'Type <kbd>/</kbd> to input your query',
'playground.compare.apply': 'Apply', 'playground.compare.apply': 'Apply',
'playground.compare.applytoall': 'Apply to all models', 'playground.compare.applytoall': 'Apply to all models',
'playground.model.noavailable': 'No available models', 'playground.model.noavailable': 'No available models',
+1
View File
@@ -37,6 +37,7 @@ export default {
'resources.table.vramutilization': 'VRAM Utilization', 'resources.table.vramutilization': 'VRAM Utilization',
'resources.table.total': 'Total', 'resources.table.total': 'Total',
'resources.table.used': 'Used', 'resources.table.used': 'Used',
'resources.table.allocated': 'Allocated',
'resources.table.wokers': 'workers', 'resources.table.wokers': 'workers',
'resources.worker.linuxormaxos': 'Linux or MacOS', 'resources.worker.linuxormaxos': 'Linux or MacOS',
'resources.table.unified': 'Unified Memory', 'resources.table.unified': 'Unified Memory',
+3 -1
View File
@@ -25,5 +25,7 @@ export default {
'users.password.confirm': 'Confirm New Password', 'users.password.confirm': 'Confirm New Password',
'users.password.confirm.empty': 'Please confirm the new password.', 'users.password.confirm.empty': 'Please confirm the new password.',
'users.password.confirm.error': 'The two passwords entered do not match.', 'users.password.confirm.error': 'The two passwords entered do not match.',
'users.login.title': 'Log in to {name}' 'users.login.title': 'Log in to {name}',
'users.version.islatest': 'GPUStack {version} is the latest version',
'users.version.update': 'GPUStack {version} is available'
}; };
+5 -1
View File
@@ -199,5 +199,9 @@ export default {
'common.title.delete.confirm': '确认删除', 'common.title.delete.confirm': '确认删除',
'common.button.addLabel': '添加标签', 'common.button.addLabel': '添加标签',
'common.button.addSelector': '添加选择器', 'common.button.addSelector': '添加选择器',
'common.button.addParams': '添加参数' 'common.button.addParams': '添加参数',
'common.text.here': '这里',
'common.text.latest': '最新',
'common.text.new': '新',
'common.text.changelog': '更新日志'
}; };
+2 -1
View File
@@ -63,5 +63,6 @@ export default {
'models.form.backend_parameters.llamabox.placeholder': 'models.form.backend_parameters.llamabox.placeholder':
'例如,--ctx-size=8192', '例如,--ctx-size=8192',
'models.form.backend_parameters.vllm.placeholder': 'models.form.backend_parameters.vllm.placeholder':
'例如,--max-model-len=8192' '例如,--max-model-len=8192',
'models.form.backend_parameters.vllm.tips': '更多 {backend} 参数说明查看'
}; };
+1
View File
@@ -42,6 +42,7 @@ export default {
'playground.toolbar.compare4Model': '4 模型对比', 'playground.toolbar.compare4Model': '4 模型对比',
'playground.toolbar.compare6Model': '6 模型对比', 'playground.toolbar.compare6Model': '6 模型对比',
'playground.input.holder': '按 <kbd>/</kbd> 开始输入', 'playground.input.holder': '按 <kbd>/</kbd> 开始输入',
'playground.input.keyword.holder': '按 <kbd>/</kbd> 输入你的查询',
'playground.compare.apply': '应用', 'playground.compare.apply': '应用',
'playground.compare.applytoall': '应用到所有模型', 'playground.compare.applytoall': '应用到所有模型',
'playground.model.noavailable': '无可用模型', 'playground.model.noavailable': '无可用模型',
+1
View File
@@ -37,6 +37,7 @@ export default {
'resources.table.utilization': '利用率', 'resources.table.utilization': '利用率',
'resources.table.total': '总量', 'resources.table.total': '总量',
'resources.table.used': '已用', 'resources.table.used': '已用',
'resources.table.allocated': '已分配',
'resources.table.wokers': 'workers', 'resources.table.wokers': 'workers',
'resources.table.unified': '统一内存', 'resources.table.unified': '统一内存',
'resources.worker.linuxormaxos': 'Linux 或 MacOS', 'resources.worker.linuxormaxos': 'Linux 或 MacOS',
+3 -1
View File
@@ -24,5 +24,7 @@ export default {
'users.password.confirm': '确认新密码', 'users.password.confirm': '确认新密码',
'users.password.confirm.empty': '请确认新密码', 'users.password.confirm.empty': '请确认新密码',
'users.password.confirm.error': '两次输入的密码不一致', 'users.password.confirm.error': '两次输入的密码不一致',
'users.login.title': '登录 {name}' 'users.login.title': '登录 {name}',
'users.version.islatest': 'GPUStack {version} 已是最新版本',
'users.version.update': 'GPUStack {version} 版本可供更新'
}; };
+35 -6
View File
@@ -1,3 +1,4 @@
import AutoTooltip from '@/components/auto-tooltip';
import DeleteModal from '@/components/delete-modal'; import DeleteModal from '@/components/delete-modal';
import PageTools from '@/components/page-tools'; import PageTools from '@/components/page-tools';
import { PageAction } from '@/config'; import { PageAction } from '@/config';
@@ -218,9 +219,15 @@ const APIKeys: React.FC = () => {
title={intl.formatMessage({ id: 'common.table.name' })} title={intl.formatMessage({ id: 'common.table.name' })}
dataIndex="name" dataIndex="name"
key="name" key="name"
width={400}
ellipsis={{ ellipsis={{
showTitle: true showTitle: false
}}
render={(text, record) => {
return (
<AutoTooltip ghost style={{ maxWidth: 400 }}>
{text}
</AutoTooltip>
);
}} }}
/> />
@@ -228,10 +235,19 @@ const APIKeys: React.FC = () => {
title={intl.formatMessage({ id: 'apikeys.form.expiretime' })} title={intl.formatMessage({ id: 'apikeys.form.expiretime' })}
dataIndex="expires_at" dataIndex="expires_at"
key="expiration" key="expiration"
ellipsis={{
showTitle: false
}}
render={(text, record) => { render={(text, record) => {
return text return (
<AutoTooltip ghost>
{text
? dayjs(text).format('YYYY-MM-DD HH:mm:ss') ? dayjs(text).format('YYYY-MM-DD HH:mm:ss')
: intl.formatMessage({ id: 'apikeys.form.expiration.never' }); : intl.formatMessage({
id: 'apikeys.form.expiration.never'
})}
</AutoTooltip>
);
}} }}
/> />
<Column <Column
@@ -239,7 +255,10 @@ const APIKeys: React.FC = () => {
dataIndex="description" dataIndex="description"
key="description" key="description"
ellipsis={{ ellipsis={{
showTitle: true showTitle: false
}}
render={(text, record) => {
return <AutoTooltip ghost>{text}</AutoTooltip>;
}} }}
/> />
<Column <Column
@@ -250,13 +269,23 @@ const APIKeys: React.FC = () => {
sortOrder={sortOrder} sortOrder={sortOrder}
showSorterTooltip={false} showSorterTooltip={false}
sorter={false} sorter={false}
ellipsis={{
showTitle: false
}}
render={(text, record) => { render={(text, record) => {
return dayjs(text).format('YYYY-MM-DD HH:mm:ss'); return (
<AutoTooltip ghost>
{dayjs(text).format('YYYY-MM-DD HH:mm:ss')}
</AutoTooltip>
);
}} }}
/> />
<Column <Column
title={intl.formatMessage({ id: 'common.table.operation' })} title={intl.formatMessage({ id: 'common.table.operation' })}
key="operation" key="operation"
ellipsis={{
showTitle: false
}}
render={(text, record: ListItem) => { render={(text, record: ListItem) => {
return ( return (
<Space size={20}> <Space size={20}>
@@ -1,3 +1,4 @@
import AutoTooltip from '@/components/auto-tooltip';
import PageTools from '@/components/page-tools'; import PageTools from '@/components/page-tools';
import { convertFileSize } from '@/utils'; import { convertFileSize } from '@/utils';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
@@ -12,18 +13,27 @@ const ActiveTable = () => {
{ {
title: intl.formatMessage({ id: 'common.table.name' }), title: intl.formatMessage({ id: 'common.table.name' }),
dataIndex: 'name', dataIndex: 'name',
key: 'name' key: 'name',
ellipsis: true,
render: (text: any, record: any) => {
return (
<AutoTooltip ghost>
<span>{text}</span>
</AutoTooltip>
);
}
}, },
{ {
title: intl.formatMessage({ id: 'dashboard.allocatevram' }), title: intl.formatMessage({ id: 'dashboard.allocatevram' }),
dataIndex: 'resource_claim.memory', dataIndex: 'resource_claim.memory',
key: 'vram', key: 'vram',
ellipsis: true,
render: (text: any, record: any) => { render: (text: any, record: any) => {
return ( return (
<span> <AutoTooltip ghost>
{convertFileSize(record.resource_claim?.vram || 0)} /{' '} {convertFileSize(record.resource_claim?.vram || 0)} /{' '}
{convertFileSize(record.resource_claim?.ram || 0)} {convertFileSize(record.resource_claim?.ram || 0)}
</span> </AutoTooltip>
); );
} }
}, },
@@ -35,7 +45,15 @@ const ActiveTable = () => {
{ {
title: intl.formatMessage({ id: 'dashboard.tokens' }), title: intl.formatMessage({ id: 'dashboard.tokens' }),
dataIndex: 'token_count', dataIndex: 'token_count',
key: 'token_count' key: 'token_count',
ellipsis: true,
render: (text: any, record: any) => {
return (
<AutoTooltip ghost>
<span>{text}</span>
</AutoTooltip>
);
}
} }
]; ];
return ( return (
@@ -5,11 +5,6 @@ import _ from 'lodash';
import { memo, useContext, useMemo } from 'react'; import { memo, useContext, useMemo } from 'react';
import { DashboardContext } from '../config/dashboard-context'; import { DashboardContext } from '../config/dashboard-context';
const chartColorMap = {
tickLineColor: 'rgba(217,217,217,0.5)',
axislabelColor: 'rgba(0, 0, 0, 0.4)'
};
const TypeKeyMap = { const TypeKeyMap = {
cpu: { cpu: {
label: 'CPU', label: 'CPU',
@@ -37,78 +32,6 @@ const TypeKeyMap = {
} }
}; };
const option = {
title: {
text: ''
},
legend: {
itemWidth: 8,
itemHeight: 8,
data: []
},
grid: {
left: 0,
right: 20,
bottom: 20,
containLabel: true
},
tooltip: {
trigger: 'axis',
formatter(params: any) {
let result = `<span class="tooltip-x-name">${params[0].axisValue}</span>`;
params.forEach((item: any) => {
result += `<span class="tooltip-item">
<span class="tooltip-item-name">
<span style="display:inline-block;margin-right:5px;border-radius:8px;width:8px;height:8px;background-color:${item.color};"></span>
<span class="tooltip-title">${item.seriesName}</span>:
</span>
<span class="tooltip-value">${item.data.value}</span>
</span>`;
});
return `<div class="tooltip-wrapper">${result}</div>`;
}
},
xAxis: {
type: 'category',
boundaryGap: true,
axisTick: {
show: true,
lineStyle: {
color: chartColorMap.tickLineColor
}
},
axisLabel: {
color: chartColorMap.axislabelColor,
// fontFamily: 'unset',
fontSize: 12
},
axisLine: {
show: false
},
data: []
},
yAxis: {
max: 100,
min: 0,
splitLine: {
show: true,
lineStyle: {
type: 'dashed'
}
},
axisLabel: {
color: chartColorMap.axislabelColor,
// fontFamily: 'unset',
fontSize: 12
},
axisTick: {
show: false
},
type: 'value'
},
series: []
};
const UtilizationOvertime: React.FC = () => { const UtilizationOvertime: React.FC = () => {
const intl = useIntl(); const intl = useIntl();
const data = useContext(DashboardContext)?.system_load?.history || {}; const data = useContext(DashboardContext)?.system_load?.history || {};
@@ -5,6 +5,7 @@ import { PageAction } from '@/config';
import { PageActionType } from '@/config/types'; import { PageActionType } from '@/config/types';
import { InfoCircleOutlined, RightOutlined } from '@ant-design/icons'; import { InfoCircleOutlined, RightOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { import {
Checkbox, Checkbox,
Collapse, Collapse,
@@ -76,6 +77,19 @@ const AdvanceConfig: React.FC<AdvanceConfigProps> = (props) => {
return backend === backendOptionsMap.llamaBox ? llamaConfig : vllmConfig; return backend === backendOptionsMap.llamaBox ? llamaConfig : vllmConfig;
}, [backend]); }, [backend]);
const backendParamsTips = useMemo(() => {
if (backend === backendOptionsMap.llamaBox) {
return {
backend: 'llama-box',
link: 'https://github.com/gpustack/llama-box?tab=readme-ov-file#usage'
};
}
return {
backend: 'vLLM',
link: 'https://docs.vllm.ai/en/stable/serving/openai_compatible_server.html#command-line-arguments-for-the-server'
};
}, [backend]);
const renderSelectTips = (list: Array<{ title: string; tips: string }>) => { const renderSelectTips = (list: Array<{ title: string; tips: string }>) => {
return ( return (
<div> <div>
@@ -252,6 +266,21 @@ const AdvanceConfig: React.FC<AdvanceConfigProps> = (props) => {
dataList={form.getFieldValue('backend_parameters') || []} dataList={form.getFieldValue('backend_parameters') || []}
onChange={handleBackendParametersChange} onChange={handleBackendParametersChange}
options={paramsConfig} options={paramsConfig}
description={
<span>
{intl.formatMessage(
{ id: 'models.form.backend_parameters.vllm.tips' },
{ backend: backendParamsTips.backend }
)}{' '}
<Typography.Link
style={{ color: 'var(--ant-blue-4)' }}
href={backendParamsTips.link}
target="_blank"
>
{intl.formatMessage({ id: 'common.text.here' })}
</Typography.Link>
</span>
}
></ListInput> ></ListInput>
</Form.Item> </Form.Item>
{isGGUF && ( {isGGUF && (
+10 -3
View File
@@ -3,6 +3,7 @@ import { PageActionType } from '@/config/types';
import { CloseOutlined } from '@ant-design/icons'; import { CloseOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Button, Drawer } from 'antd'; import { Button, Drawer } from 'antd';
import { debounce } from 'lodash';
import { memo, useCallback, useEffect, useRef, useState } from 'react'; import { memo, useCallback, useEffect, useRef, useState } from 'react';
import { modelSourceMap } from '../config'; import { modelSourceMap } from '../config';
import { FormData, ListItem } from '../config/types'; import { FormData, ListItem } from '../config/types';
@@ -60,15 +61,21 @@ const AddModal: React.FC<AddModalProps> = (props) => {
form.current?.submit?.(); form.current?.submit?.();
}; };
const debounceFetchModelFiles = debounce(() => {
modelFileRef.current?.fetchModelFiles?.();
}, 300);
const handleSetIsGGUF = (flag: boolean) => { const handleSetIsGGUF = (flag: boolean) => {
setIsGGUF(flag); setIsGGUF(flag);
if (flag) { if (flag) {
setTimeout(() => { debounceFetchModelFiles();
modelFileRef.current?.fetchModelFiles?.();
}, 50);
} }
}; };
useEffect(() => {
handleSelectModelFile({ fakeName: '' });
}, [selectedModel]);
useEffect(() => { useEffect(() => {
return () => { return () => {
setSelectedModel({}); setSelectedModel({});
@@ -261,7 +261,7 @@ const HFModelFile: React.FC<HFModelFileProps> = forwardRef((props, ref) => {
}, []); }, []);
return ( return (
<div> <div className="files-wrap">
<TitleWrapper> <TitleWrapper>
<span className="title"> <span className="title">
{intl.formatMessage({ id: 'models.available.files' })} ( {intl.formatMessage({ id: 'models.available.files' })} (
@@ -282,13 +282,20 @@ const HFModelFile: React.FC<HFModelFileProps> = forwardRef((props, ref) => {
style={{ width: '120px' }} style={{ width: '120px' }}
></Select> ></Select>
</TitleWrapper> </TitleWrapper>
{dataSource.loading && (
<div className="spin-wrapper">
<Spin
spinning={dataSource.loading}
style={{ height: '100%', width: '100%' }}
></Spin>
</div>
)}
<SimpleBar <SimpleBar
style={{ style={{
height: collapsed ? 'max-content' : 'calc(100vh - 300px)' height: collapsed ? 'max-content' : 'calc(100vh - 300px)'
}} }}
> >
<div style={{ padding: '16px 24px' }}> <div style={{ padding: '16px 24px' }}>
<Spin spinning={dataSource.loading} style={{ minHeight: 100 }}>
{dataSource.fileList.length ? ( {dataSource.fileList.length ? (
<Row gutter={[16, 24]}> <Row gutter={[16, 24]}>
{_.map(dataSource.fileList, (item: any) => { {_.map(dataSource.fileList, (item: any) => {
@@ -358,7 +365,6 @@ const HFModelFile: React.FC<HFModelFileProps> = forwardRef((props, ref) => {
/> />
) )
)} )}
</Spin>
</div> </div>
</SimpleBar> </SimpleBar>
</div> </div>
@@ -64,27 +64,16 @@ const HFModelItem: React.FC<HFModelItemProps> = (props) => {
<div className="info"> <div className="info">
{SUPPORTEDSOURCE.includes(props.source || '') ? ( {SUPPORTEDSOURCE.includes(props.source || '') ? (
<div className="info-item"> <div className="info-item">
{/* {props.task && (
<Tag
className="tag-item"
color="gold"
style={{
marginRight: 0
}}
>
<span style={{ opacity: 0.65 }}>{props.task}</span>
</Tag>
)} */}
<span> <span>
{dayjs().to( {dayjs().to(
dayjs(dayjs(props.updatedAt).format('YYYY-MM-DD HH:mm:ss')) dayjs(dayjs(props.updatedAt).format('YYYY-MM-DD HH:mm:ss'))
)} )}
</span> </span>
<span> <span className="flex-center">
<HeartOutlined className="m-r-5" /> <HeartOutlined className="m-r-5" />
{props.likes} {props.likes}
</span> </span>
<span> <span className="flex-center">
<DownloadOutlined className="m-r-5" /> <DownloadOutlined className="m-r-5" />
{formatNumber(props.downloads)} {formatNumber(props.downloads)}
</span> </span>
@@ -116,7 +116,7 @@ const InstanceItem: React.FC<InstanceItemProps> = ({
return { return {
worker_name: data?.name, worker_name: data?.name,
worker_ip: data?.ip, worker_ip: data?.ip,
port: data?.port, port: '',
gpu_index: item.gpu_index gpu_index: item.gpu_index
}; };
}); });
@@ -125,7 +125,7 @@ const InstanceItem: React.FC<InstanceItemProps> = ({
{ {
worker_name: `${row.worker_name}`, worker_name: `${row.worker_name}`,
worker_ip: `${row.worker_ip}`, worker_ip: `${row.worker_ip}`,
port: row.port, port: '',
gpu_index: `${row.gpu_indexes?.sort?.()} (main)` gpu_index: `${row.gpu_indexes?.sort?.()} (main)`
} }
]; ];
+21 -1
View File
@@ -60,6 +60,23 @@ const ModelCard: React.FC<{
} }
}; };
const removeMetadata = useCallback((str: string) => {
let indexes = [];
let index = str.indexOf('---');
while (index !== -1) {
indexes.push(index);
if (indexes.length >= 2) {
break;
}
index = str.indexOf('---', index + 1);
}
if (indexes.length >= 2) {
return str.slice(indexes[1] + 3);
}
return str;
}, []);
// huggingface model card data // huggingface model card data
const getHuggingfaceModelDetail = async () => { const getHuggingfaceModelDetail = async () => {
try { try {
@@ -74,7 +91,10 @@ const ModelCard: React.FC<{
]); ]);
setModelData(modelcard); setModelData(modelcard);
setReadmeText(readme); // remove the meta data from readme
const newReadme = removeMetadata(readme);
setReadmeText(newReadme);
const isGGUF = modelcard.tags?.includes('gguf'); const isGGUF = modelcard.tags?.includes('gguf');
setIsGGUF(isGGUF); setIsGGUF(isGGUF);
setIsGGUFModel(isGGUF); setIsGGUFModel(isGGUF);
@@ -8,9 +8,10 @@ import { modelSourceMap, modelSourceValueMap } from '../config';
const SearchInput: React.FC<{ const SearchInput: React.FC<{
modelSource: string; modelSource: string;
onChange: (e: any) => void;
onSearch: (e: any) => void; onSearch: (e: any) => void;
}> = (props) => { }> = (props) => {
const { onSearch, modelSource } = props; const { onSearch, onChange, modelSource } = props;
const intl = useIntl(); const intl = useIntl();
const inputRef = useRef<any>(null); const inputRef = useRef<any>(null);
@@ -23,6 +24,7 @@ const SearchInput: React.FC<{
<Input <Input
ref={inputRef} ref={inputRef}
onPressEnter={onSearch} onPressEnter={onSearch}
onChange={onChange}
allowClear allowClear
placeholder={intl.formatMessage( placeholder={intl.formatMessage(
{ {
@@ -136,7 +136,6 @@ const SearchModel: React.FC<SearchInputProps> = (props) => {
} }
axiosTokenRef.current?.abort?.(); axiosTokenRef.current?.abort?.();
axiosTokenRef.current = new AbortController(); axiosTokenRef.current = new AbortController();
if (dataSource.loading) return;
const sort = sortType ?? dataSource.sortType; const sort = sortType ?? dataSource.sortType;
try { try {
setDataSource((pre) => { setDataSource((pre) => {
@@ -174,7 +173,9 @@ const SearchModel: React.FC<SearchInputProps> = (props) => {
}, },
[dataSource] [dataSource]
); );
const handleSearchInputChange = useCallback((e: any) => {
searchInputRef.current = e.target.value;
}, []);
const handlerSearchModels = useCallback( const handlerSearchModels = useCallback(
async (e: any) => { async (e: any) => {
searchInputRef.current = e.target.value; searchInputRef.current = e.target.value;
@@ -208,7 +209,6 @@ const SearchModel: React.FC<SearchInputProps> = (props) => {
}; };
const handleFilterGGUFChange = (e: any) => { const handleFilterGGUFChange = (e: any) => {
console.log('filterggufChange:', e.target.checked);
filterGGUFRef.current = e.target.checked; filterGGUFRef.current = e.target.checked;
handleOnSearchRepo(); handleOnSearchRepo();
}; };
@@ -218,6 +218,7 @@ const SearchModel: React.FC<SearchInputProps> = (props) => {
<> <>
<SearchInput <SearchInput
onSearch={handlerSearchModels} onSearch={handlerSearchModels}
onChange={handleSearchInputChange}
modelSource={modelSource} modelSource={modelSource}
></SearchInput> ></SearchInput>
<div className={SearchStyle.filter}> <div className={SearchStyle.filter}>
+62 -23
View File
@@ -96,7 +96,13 @@ const Models: React.FC<ModelsProps> = ({
source: modelSourceMap.huggingface_value source: modelSourceMap.huggingface_value
}); });
const [currentData, setCurrentData] = useState<ListItem>({} as ListItem); const [currentData, setCurrentData] = useState<ListItem>({} as ListItem);
const [currentInstanceUrl, setCurrentInstanceUrl] = useState<string>(''); const [currentInstance, setCurrentInstance] = useState<{
url: string;
status: string;
}>({
url: '',
status: ''
});
const modalRef = useRef<any>(null); const modalRef = useRef<any>(null);
useHotkeys( useHotkeys(
@@ -199,11 +205,11 @@ const Models: React.FC<ModelsProps> = ({
key: 'chat', key: 'chat',
icon: <WechatWorkOutlined /> icon: <WechatWorkOutlined />
}, },
{ // {
label: 'common.button.viewcode', // label: 'common.button.viewcode',
key: 'embedding', // key: 'embedding',
icon: <IconFont type="icon-code" /> // icon: <IconFont type="icon-code" />
}, // },
{ {
label: 'common.button.delete', label: 'common.button.delete',
key: 'delete', key: 'delete',
@@ -220,7 +226,10 @@ const Models: React.FC<ModelsProps> = ({
return record.ready_replicas > 0 && !record.embedding_only; return record.ready_replicas > 0 && !record.embedding_only;
} }
if (action.key === 'embedding') { if (action.key === 'embedding') {
return record.embedding_only && record.ready_replicas > 0; return (
(record.embedding_only || record.reranker) &&
record.ready_replicas > 0
);
} }
return true; return true;
}); });
@@ -308,6 +317,7 @@ const Models: React.FC<ModelsProps> = ({
async onOk() { async onOk() {
await deleteModel(row.id); await deleteModel(row.id);
updateExpandedRowKeys([row.id]); updateExpandedRowKeys([row.id]);
rowSelection.removeSelectedKey(row.id);
} }
}); });
}; };
@@ -329,7 +339,10 @@ const Models: React.FC<ModelsProps> = ({
const handleViewLogs = async (row: any) => { const handleViewLogs = async (row: any) => {
try { try {
setCurrentInstanceUrl(`${MODEL_INSTANCE_API}/${row.id}/logs`); setCurrentInstance({
url: `${MODEL_INSTANCE_API}/${row.id}/logs`,
status: row.status
});
setOpenLogModal(true); setOpenLogModal(true);
} catch (error) { } catch (error) {
console.log('error:', error); console.log('error:', error);
@@ -367,7 +380,8 @@ const Models: React.FC<ModelsProps> = ({
setOpenAddModal(true); setOpenAddModal(true);
}; };
const handleSelect = useCallback((val: any, row: ListItem) => { const handleSelect = useCallback(
(val: any, row: ListItem) => {
if (val === 'edit') { if (val === 'edit') {
handleEdit(row); handleEdit(row);
} }
@@ -386,7 +400,9 @@ const Models: React.FC<ModelsProps> = ({
show: true show: true
}); });
} }
}, []); },
[handleEdit, handleOpenPlayGround, handleDelete, setEmbeddingParams]
);
const handleChildSelect = useCallback( const handleChildSelect = useCallback(
(val: any, row: ModelInstanceListItem, list: ModelInstanceListItem[]) => { (val: any, row: ModelInstanceListItem, list: ModelInstanceListItem[]) => {
@@ -417,10 +433,10 @@ const Models: React.FC<ModelsProps> = ({
const generateSource = useCallback((record: ListItem) => { const generateSource = useCallback((record: ListItem) => {
if (record.source === modelSourceMap.modelscope_value) { if (record.source === modelSourceMap.modelscope_value) {
return `${modelSourceMap.modelScope}/${record.model_scope_file_path || record.model_scope_model_id}`; return `${modelSourceMap.modelScope}/${record.model_scope_model_id}`;
} }
if (record.source === modelSourceMap.huggingface_value) { if (record.source === modelSourceMap.huggingface_value) {
return `${modelSourceMap.huggingface}/${record.huggingface_filename || record.huggingface_repo_id}`; return `${modelSourceMap.huggingface}/${record.huggingface_repo_id}`;
} }
return `${modelSourceMap.ollama_library}/${record.ollama_library_model_name}`; return `${modelSourceMap.ollama_library}/${record.ollama_library_model_name}`;
}, []); }, []);
@@ -518,18 +534,37 @@ const Models: React.FC<ModelsProps> = ({
render={(text, record: ListItem) => { render={(text, record: ListItem) => {
return ( return (
<span <span
className="flex-center flex-wrap" className="flex-center"
style={{ maxWidth: '100%' }} style={{
maxWidth: '100%'
}}
> >
<AutoTooltip ghost> <AutoTooltip ghost>
<span className="m-r-5">{text}</span> <span className="m-r-5">{text}</span>
</AutoTooltip> </AutoTooltip>
{record.embedding_only && ( {record.reranker && (
<span> <Tag
<Tag style={{ margin: 0 }} color="geekblue"> style={{
margin: 0,
opacity: 0.8,
transform: 'scale(0.9)'
}}
color="geekblue"
>
Reranker
</Tag>
)}
{record.embedding_only && !record.reranker && (
<Tag
style={{
margin: 0,
opacity: 0.8,
transform: 'scale(0.9)'
}}
color="geekblue"
>
Embedding Only Embedding Only
</Tag> </Tag>
</span>
)} )}
</span> </span>
); );
@@ -542,8 +577,8 @@ const Models: React.FC<ModelsProps> = ({
span={6} span={6}
render={(text, record: ListItem) => { render={(text, record: ListItem) => {
return ( return (
<span className="flex flex-column"> <span className="flex flex-column" style={{ width: '100%' }}>
<span>{generateSource(record)}</span> <AutoTooltip ghost>{generateSource(record)}</AutoTooltip>
</span> </span>
); );
}} }}
@@ -560,7 +595,7 @@ const Models: React.FC<ModelsProps> = ({
}} }}
render={(text, record: ListItem) => { render={(text, record: ListItem) => {
return ( return (
<span style={{ paddingLeft: 7 }}> <span style={{ paddingLeft: 10, minWidth: '33px' }}>
{record.ready_replicas} / {record.replicas} {record.ready_replicas} / {record.replicas}
</span> </span>
); );
@@ -575,7 +610,11 @@ const Models: React.FC<ModelsProps> = ({
sortOrder={sortOrder} sortOrder={sortOrder}
sorter={false} sorter={false}
render={(text, row) => { render={(text, row) => {
return dayjs(text).format('YYYY-MM-DD HH:mm:ss'); return (
<AutoTooltip ghost>
{dayjs(text).format('YYYY-MM-DD HH:mm:ss')}
</AutoTooltip>
);
}} }}
/> />
<SealColumn <SealColumn
@@ -613,7 +652,7 @@ const Models: React.FC<ModelsProps> = ({
onOk={handleCreateModel} onOk={handleCreateModel}
></DeployModal> ></DeployModal>
<ViewLogsModal <ViewLogsModal
url={currentInstanceUrl} url={currentInstance.url}
open={openLogModal} open={openLogModal}
onCancel={handleLogModalCancel} onCancel={handleLogModalCancel}
></ViewLogsModal> ></ViewLogsModal>
+10 -16
View File
@@ -72,32 +72,23 @@ const UpdateModal: React.FC<AddModalProps> = (props) => {
setGpuOptions(list); setGpuOptions(list);
}; };
const initFormValue = () => { useEffect(() => {
if (action === PageAction.CREATE && open) {
form.setFieldsValue({
source: modelSourceMap.huggingface_value,
replicas: 1
});
}
if (action === PageAction.EDIT && open) { if (action === PageAction.EDIT && open) {
const result = setSourceRepoConfigValue( const result = setSourceRepoConfigValue(
props.data?.source || '', props.data?.source || '',
props.data props.data
); );
form.setFieldsValue({ const formData = {
...result.values, ...result.values,
..._.omit(props.data, result.omits), ..._.omit(props.data, result.omits),
scheduleType: props.data?.gpu_selector ? 'manual' : 'auto', scheduleType: props.data?.gpu_selector ? 'manual' : 'auto',
gpu_selector: props.data?.gpu_selector gpu_selector: props.data?.gpu_selector
? `${props.data?.gpu_selector.worker_name}-${props.data?.gpu_selector.gpu_name}-${props.data?.gpu_selector.gpu_index}` ? `${props.data?.gpu_selector.worker_name}-${props.data?.gpu_selector.gpu_name}-${props.data?.gpu_selector.gpu_index}`
: null : null
});
}
}; };
form.setFieldsValue(formData);
useEffect(() => { }
initFormValue();
}, [open]); }, [open]);
useEffect(() => { useEffect(() => {
@@ -257,6 +248,10 @@ const UpdateModal: React.FC<AddModalProps> = (props) => {
} }
}; };
const handleOnClose = () => {
onCancel?.();
};
useEffect(() => { useEffect(() => {
getGPUList(); getGPUList();
}, []); }, []);
@@ -268,6 +263,7 @@ const UpdateModal: React.FC<AddModalProps> = (props) => {
centered={true} centered={true}
onOk={handleSumit} onOk={handleSumit}
onCancel={onCancel} onCancel={onCancel}
onClose={handleOnClose}
destroyOnClose={true} destroyOnClose={true}
closeIcon={true} closeIcon={true}
maskClosable={false} maskClosable={false}
@@ -391,9 +387,7 @@ const UpdateModal: React.FC<AddModalProps> = (props) => {
form={form} form={form}
gpuOptions={gpuOptions} gpuOptions={gpuOptions}
action={PageAction.EDIT} action={PageAction.EDIT}
isGGUF={ isGGUF={props.data?.backend === backendOptionsMap.llamaBox}
form.getFieldValue('backend') === backendOptionsMap.llamaBox
}
></AdvanceConfig> ></AdvanceConfig>
</Form> </Form>
</SimpleBar> </SimpleBar>
@@ -1,4 +1,4 @@
import LogsViewer from '@/components/logs-viewer'; import LogsViewer from '@/components/logs-viewer/index';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Modal } from 'antd'; import { Modal } from 'antd';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
@@ -6,6 +6,7 @@ import React, { useEffect, useState } from 'react';
type ViewModalProps = { type ViewModalProps = {
open: boolean; open: boolean;
url: string; url: string;
autoScroll?: boolean;
onCancel: () => void; onCancel: () => void;
}; };
@@ -41,7 +42,10 @@ const ViewCodeModal: React.FC<ViewModalProps> = (props) => {
<Modal <Modal
title={ title={
<span className="flex flex-center"> <span className="flex flex-center">
<span> {intl.formatMessage({ id: 'common.button.viewlog' })}</span> <span style={{ fontWeight: 'var(--font-weight-bold)' }}>
{' '}
{intl.formatMessage({ id: 'common.button.viewlog' })}
</span>
</span> </span>
} }
open={open} open={open}
+1
View File
@@ -1,6 +1,7 @@
export interface ListItem { export interface ListItem {
source: string; source: string;
backend: string; backend: string;
reranker: boolean;
huggingface_repo_id: string; huggingface_repo_id: string;
huggingface_file_name: string; huggingface_file_name: string;
huggingface_filename: string; huggingface_filename: string;
@@ -45,6 +45,23 @@
} }
} }
.files-wrap {
position: relative;
.spin-wrapper {
position: absolute;
left: 0;
right: 0;
height: 100%;
z-index: 100;
bottom: 0;
top: 0;
padding-top: 150px;
text-align: center;
background-color: var(--color-fill-mask);
}
}
.wrapper { .wrapper {
padding: 16px @padding; padding: 16px @padding;
} }
+18
View File
@@ -4,6 +4,8 @@ export const CHAT_API = '/v1-openai/chat/completions';
export const OPENAI_MODELS = '/v1-openai/models'; export const OPENAI_MODELS = '/v1-openai/models';
export const RERANKER_API = '/rerank';
export async function execChatCompletions(params: any) { export async function execChatCompletions(params: any) {
return request(`${CHAT_API}`, { return request(`${CHAT_API}`, {
method: 'POST', method: 'POST',
@@ -17,3 +19,19 @@ export const queryModelsList = async (params: any) => {
params params
}); });
}; };
export const rerankerQuery = async (
params: {
model: string;
query: string;
top_n: number;
documents: string[];
},
options?: any
) => {
return request(`${RERANKER_API}`, {
method: 'POST',
data: params,
cancelToken: options?.cancelToken
});
};
@@ -0,0 +1,51 @@
import AutoTooltip from '@/components/auto-tooltip';
import { DeleteOutlined, PaperClipOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Button, Tooltip } from 'antd';
import classNames from 'classnames';
import React from 'react';
import '../style/file-list.less';
interface FileListProps {
fileList: { text: string; name: string; uid: number | string }[];
ghost?: boolean;
showIcon?: boolean;
textListCount: number;
onDelete?: (uid: number | string) => void;
}
const FileList: React.FC<FileListProps> = (props) => {
const { textListCount, fileList, ghost, showIcon = true, onDelete } = props;
const intl = useIntl();
return (
<div className="file-list">
{fileList.map((file, index) => {
return (
<div key={file.uid} className={classNames('file-item', { ghost })}>
<span className="title">
<span className="m-r-5">{index + 1 + textListCount}.</span>
{showIcon && <PaperClipOutlined className="m-r-5" />}
<AutoTooltip ghost> {file.name}</AutoTooltip>
</span>
{onDelete && (
<Tooltip
title={intl.formatMessage({ id: 'common.button.delete' })}
>
<Button
danger
size="small"
type="text"
className="delete-btn"
icon={<DeleteOutlined></DeleteOutlined>}
onClick={() => onDelete(file.uid)}
></Button>
</Tooltip>
)}
</div>
);
})}
</div>
);
};
export default React.memo(FileList);
+36 -45
View File
@@ -10,11 +10,12 @@ import {
memo, memo,
useEffect, useEffect,
useImperativeHandle, useImperativeHandle,
useMemo,
useRef, useRef,
useState useState
} from 'react'; } from 'react';
import { CHAT_API } from '../apis'; import { CHAT_API } from '../apis';
import { Roles } from '../config'; import { Roles, generateMessages } from '../config';
import { MessageItem } from '../config/types'; import { MessageItem } from '../config/types';
import '../style/ground-left.less'; import '../style/ground-left.less';
import '../style/system-message-wrap.less'; import '../style/system-message-wrap.less';
@@ -62,10 +63,18 @@ const GroundLeft: React.FC<MessageProps> = forwardRef((props, ref) => {
}, },
setCollapse() { setCollapse() {
setCollapse(!collapse); setCollapse(!collapse);
} },
collapse: collapse
}; };
}); });
const viewCodeMessage = useMemo(() => {
return generateMessages([
{ role: Roles.System, content: systemMessage },
...messageList
]);
}, [messageList, systemMessage]);
const setMessageId = () => { const setMessageId = () => {
messageId.current = messageId.current + 1; messageId.current = messageId.current + 1;
}; };
@@ -132,48 +141,22 @@ const GroundLeft: React.FC<MessageProps> = forwardRef((props, ref) => {
setMessageList((pre) => { setMessageList((pre) => {
return [...pre, ...currentMessageRef.current]; return [...pre, ...currentMessageRef.current];
}); });
const formatMessages = _.map(
[...messageList, ...currentMessageRef.current], const messageParams = [
(item: MessageItem) => { { role: Roles.System, content: systemMessage },
return { ...messageList,
role: item.role, ...currentMessageRef.current
content: [ ];
{
type: 'text', const messages = generateMessages(messageParams);
text: item.content
},
..._.map(
item.imgs,
(img: { uid: string | number; dataUrl: string }) => {
return {
type: 'image_url',
image_url: {
url: img.dataUrl
}
};
}
)
]
};
}
);
const chatParams = { const chatParams = {
messages: systemMessage messages: messages,
? [
{
role: Roles.System,
content: [
{
type: 'text',
text: systemMessage
}
]
},
...formatMessages
]
: [...formatMessages],
...parameters, ...parameters,
stream: true stream: true,
stream_options: {
include_usage: true
}
}; };
const result: any = await fetchChunkedData({ const result: any = await fetchChunkedData({
data: chatParams, data: chatParams,
@@ -184,13 +167,21 @@ const GroundLeft: React.FC<MessageProps> = forwardRef((props, ref) => {
if (result?.error) { if (result?.error) {
setTokenResult({ setTokenResult({
error: true, error: true,
errorMessage: result?.data?.message errorMessage:
result?.data?.error?.message || result?.data?.message || ''
}); });
return; return;
} }
setMessageId(); setMessageId();
const { reader, decoder } = result; const { reader, decoder } = result;
await readStreamData(reader, decoder, (chunk: any) => { await readStreamData(reader, decoder, (chunk: any) => {
if (chunk?.error) {
setTokenResult({
error: true,
errorMessage: chunk?.error?.message || chunk?.message || ''
});
return;
}
joinMessage(chunk); joinMessage(chunk);
}); });
} catch (error) { } catch (error) {
@@ -330,6 +321,7 @@ const GroundLeft: React.FC<MessageProps> = forwardRef((props, ref) => {
<div className="box"> <div className="box">
<ParamsSettings <ParamsSettings
setParams={setParams} setParams={setParams}
params={parameters}
selectedModel={selectModel} selectedModel={selectModel}
modelList={modelList} modelList={modelList}
/> />
@@ -338,8 +330,7 @@ const GroundLeft: React.FC<MessageProps> = forwardRef((props, ref) => {
<ViewCodeModal <ViewCodeModal
open={show} open={show}
systemMessage={systemMessage} messageList={viewCodeMessage}
messageList={messageList}
parameters={parameters} parameters={parameters}
onCancel={handleCloseViewCode} onCancel={handleCloseViewCode}
title={intl.formatMessage({ id: 'playground.viewcode' })} title={intl.formatMessage({ id: 'playground.viewcode' })}
@@ -0,0 +1,355 @@
import useOverlayScroller from '@/hooks/use-overlay-scroller';
import useRequestToken from '@/hooks/use-request-token';
import { ClearOutlined, InboxOutlined } from '@ant-design/icons';
import { useIntl, useSearchParams } from '@umijs/max';
import { Button, Spin } from 'antd';
import classNames from 'classnames';
import 'overlayscrollbars/overlayscrollbars.css';
import {
forwardRef,
memo,
useEffect,
useImperativeHandle,
useRef,
useState
} from 'react';
import { rerankerQuery } from '../apis';
import { MessageItem } from '../config/types';
import '../style/ground-left.less';
import '../style/system-message-wrap.less';
import FileList from './file-list';
import InputList from './input-list';
import MessageInput from './message-input';
import ReferenceParams from './reference-params';
import RerankMessage from './rerank-message';
import RerankerParams from './reranker-params';
import UploadFile from './upload-file';
import ViewCodeModal from './view-code-modal';
interface MessageProps {
modelList: Global.BaseOption<string>[];
loaded?: boolean;
ref?: any;
}
const GroundReranker: React.FC<MessageProps> = forwardRef((props, ref) => {
const { modelList } = props;
const messageId = useRef<number>(0);
const [messageList, setMessageList] = useState<MessageItem[]>([]);
const intl = useIntl();
const requestSource = useRequestToken();
const [searchParams] = useSearchParams();
const selectModel = searchParams.get('model') || '';
const [parameters, setParams] = useState<any>({});
const [systemMessage, setSystemMessage] = useState('');
const [show, setShow] = useState(false);
const [loading, setLoading] = useState(false);
const [tokenResult, setTokenResult] = useState<any>(null);
const [collapse, setCollapse] = useState(false);
const contentRef = useRef<any>('');
const controllerRef = useRef<any>(null);
const scroller = useRef<any>(null);
const currentMessageRef = useRef<any>(null);
const paramsRef = useRef<any>(null);
const messageListLengthCache = useRef<number>(0);
const requestToken = useRef<any>(null);
const [fileList, setFileList] = useState<
{ text: string; name: string; uid: number | string }[]
>([]);
const [textList, setTextList] = useState<
{ text: string; uid: number | string; name: string }[]
>([]);
const { initialize, updateScrollerPosition } = useOverlayScroller();
const { initialize: innitializeParams } = useOverlayScroller();
useImperativeHandle(ref, () => {
return {
viewCode() {
setShow(true);
},
setCollapse() {
setCollapse(!collapse);
}
};
});
const setMessageId = () => {
messageId.current = messageId.current + 1;
};
const handleStopConversation = () => {
requestToken.current?.cancel?.();
setLoading(false);
};
const submitMessage = async (current?: { role: string; content: string }) => {
if (!parameters.model) return;
try {
setLoading(true);
setMessageId();
setTokenResult(null);
requestToken.current?.cancel?.();
requestToken.current = requestSource();
controllerRef.current?.abort?.();
controllerRef.current = new AbortController();
const signal = controllerRef.current.signal;
currentMessageRef.current = current
? [
{
content: current.content,
title: 'Query',
uid: messageId.current
}
]
: [];
contentRef.current = '';
setMessageList((pre) => {
return [...currentMessageRef.current];
});
const documentList: any[] = [...textList, ...fileList];
console.log('documentList:', documentList);
const result: any = await rerankerQuery(
{
model: parameters.model,
top_n: parameters.top_n,
query: current?.content || '',
documents: [
...textList.map((item) => item.text),
...fileList.map((item) => item.text)
]
},
{
token: requestToken.current.token
}
);
console.log('result:', result);
setMessageId();
setTokenResult(result.usage);
setMessageList([
...currentMessageRef.current,
{
title: 'Results',
content: result.results?.map((item: any) => {
return {
uid: item.index,
text: `${item.document?.text?.slice(0, 100) || ''}`,
docIndex: item.index,
title: documentList[item.index]?.name || '',
score: item.relevance_score
};
}),
uid: messageId.current
}
]);
} catch (error: any) {
setTokenResult({
error: true,
errorMessage: error.response?.data?.error?.message
});
} finally {
setLoading(false);
}
};
const handleClear = () => {
if (!messageList.length) {
return;
}
setMessageId();
setMessageList([]);
setTokenResult(null);
};
const handleSendMessage = (message: Omit<MessageItem, 'uid'>) => {
const currentMessage =
message.content || message.imgs?.length ? message : undefined;
submitMessage(currentMessage);
};
const handleCloseViewCode = () => {
setShow(false);
};
const handleUpdateFileList = (
files: { text: string; name: string; uid: number | string }[]
) => {
console.log('files:', files);
setFileList((preList) => {
return [...preList, ...files];
});
};
const handleDeleteFile = (uid: number | string) => {
setFileList((preList) => {
return preList.filter((item) => item.uid !== uid);
});
};
const handleTextListChange = (
list: { text: string; uid: number | string; name: string }[]
) => {
setTextList(list);
};
const handleClearDocuments = () => {
setTextList([]);
setFileList([]);
};
useEffect(() => {
setMessageId();
setMessageList([]);
setTokenResult(null);
}, [parameters.model]);
useEffect(() => {
if (scroller.current) {
initialize(scroller.current);
}
}, [scroller.current, initialize]);
useEffect(() => {
if (paramsRef.current) {
innitializeParams(paramsRef.current);
}
}, [paramsRef.current, innitializeParams]);
useEffect(() => {
if (loading) {
updateScrollerPosition();
}
}, [messageList, loading]);
useEffect(() => {
if (messageList.length > messageListLengthCache.current) {
updateScrollerPosition();
}
messageListLengthCache.current = messageList.length;
}, [messageList.length]);
return (
<div className="ground-left-wrapper">
<div className="ground-left">
<div className="message-list-wrap" ref={scroller}>
<>
<div className="content">
<RerankMessage dataList={messageList} />
{loading && (
<Spin size="small">
<div style={{ height: '46px' }}></div>
</Spin>
)}
</div>
</>
</div>
{tokenResult && (
<div style={{ height: 40 }}>
<ReferenceParams
usage={tokenResult}
showOutput={false}
></ReferenceParams>
</div>
)}
<div className="ground-left-footer">
<MessageInput
scope="reranker"
loading={loading}
disabled={!parameters.model}
isEmpty={true}
handleSubmit={handleSendMessage}
handleAbortFetch={handleStopConversation}
clearAll={handleClear}
modelList={modelList}
placeholer={intl.formatMessage({
id: 'playground.input.keyword.holder'
})}
/>
</div>
</div>
<div
className={classNames('params-wrapper', {
collapsed: collapse
})}
style={{
paddingBottom: 80
}}
ref={paramsRef}
>
<div className="box" style={{ padding: collapse ? 0 : '0 16px' }}>
<RerankerParams
setParams={setParams}
params={parameters}
selectedModel={selectModel}
modelList={modelList}
/>
<h3 className="m-b-20 m-l-10 flex-between flex-center font-size-14">
<span>Documents</span>
<Button
type="text"
icon={<ClearOutlined />}
size="middle"
onClick={handleClearDocuments}
></Button>
</h3>
<InputList
textList={textList}
onChange={handleTextListChange}
></InputList>
<div style={{ marginTop: 8 }}>
<FileList
fileList={fileList}
textListCount={textList.length || 0}
onDelete={handleDeleteFile}
></FileList>
</div>
</div>
<div
style={{
height: collapse ? 0 : 60,
padding: '0 10px',
position: 'absolute',
overflow: 'hidden',
bottom: 20,
left: 0,
right: 0
}}
>
<UploadFile
handleUpdateFileList={handleUpdateFileList}
accept=".txt, .doc, .docx, .xls, .xlsx"
>
<div style={{ backgroundColor: 'var(--color-fill-sider)' }}>
<InboxOutlined className="font-size-16" />
<span className="m-l-10">
Click or drag file to this area to upload
</span>
</div>
<span className="text-tertiary">
support .txt, .doc, .docx, .xls, .xlsx
</span>
</UploadFile>
</div>
</div>
<ViewCodeModal
open={show}
systemMessage={systemMessage}
messageList={messageList}
parameters={parameters}
onCancel={handleCloseViewCode}
title={intl.formatMessage({ id: 'playground.viewcode' })}
></ViewCodeModal>
</div>
);
});
export default memo(GroundReranker);
@@ -0,0 +1,110 @@
import RowTextarea from '@/components/seal-form/row-textarea';
import {
MinusCircleOutlined,
PlusCircleOutlined,
PlusOutlined
} from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Button, Tooltip } from 'antd';
import React, { useRef } from 'react';
import '../style/input-list.less';
interface InputListProps {
textList: { text: string; uid: number | string; name: string }[];
onChange?: (
textList: { text: string; uid: number | string; name: string }[]
) => void;
}
const InputList: React.FC<InputListProps> = ({ textList, onChange }) => {
const intl = useIntl();
const messageId = useRef(0);
const setMessageId = () => {
messageId.current = messageId.current + 1;
};
const handleAdd = () => {
setMessageId();
const dataList = [...textList];
dataList.push({
text: '',
uid: messageId.current,
name: `Text ${dataList.length + 1}`
});
onChange?.(dataList);
};
const handleDelete = (text: { text: string; uid: number | string }) => {
const dataList = [...textList];
const index = dataList.findIndex((item) => item.uid === text.uid);
dataList.splice(index, 1);
onChange?.(dataList);
};
const handleTextChange = (
value: string,
text: { text: string; uid: number | string }
) => {
const dataList = [...textList];
const index = dataList.findIndex((item) => item.uid === text.uid);
dataList[index].text = value;
onChange?.(dataList);
};
return (
<div className="input-list">
{textList.length === 0 && (
<Button
block
onClick={handleAdd}
type="text"
style={{ backgroundColor: 'var(--ant-color-fill-secondary)' }}
>
<PlusOutlined />
Add Text
</Button>
)}
{textList.map((text, index) => {
return (
<div key={text.uid} className="input-item">
<div className="input-wrap">
<RowTextarea
label={`${index + 1}.`}
value={text.text}
placeholder="Input your text"
onChange={(e) => handleTextChange(e.target.value, text)}
></RowTextarea>
</div>
<span className="btn-group">
<Tooltip
title={intl.formatMessage({ id: 'common.button.delete' })}
>
<Button
size="small"
type="text"
icon={<MinusCircleOutlined />}
onClick={() => handleDelete(text)}
></Button>
</Tooltip>
{index === textList.length - 1 && (
<Tooltip
title={intl.formatMessage({ id: 'common.button.add' })}
>
<Button
size="small"
type="text"
icon={<PlusCircleOutlined />}
onClick={handleAdd}
></Button>
</Tooltip>
)}
</span>
</div>
);
})}
</div>
);
};
export default React.memo(InputList);
@@ -1,6 +1,5 @@
import IconFont from '@/components/icon-font'; import IconFont from '@/components/icon-font';
import HotKeys, { KeyMap } from '@/config/hotkeys'; import HotKeys, { KeyMap } from '@/config/hotkeys';
import { platformCall } from '@/utils';
import { ClearOutlined, SendOutlined, SwapOutlined } from '@ant-design/icons'; import { ClearOutlined, SendOutlined, SwapOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Button, Divider, Input, Select, Tooltip } from 'antd'; import { Button, Divider, Input, Select, Tooltip } from 'antd';
@@ -61,18 +60,19 @@ interface MessageInputProps {
handleAbortFetch: () => void; handleAbortFetch: () => void;
updateLayout?: (value: { span: number; count: number }) => void; updateLayout?: (value: { span: number; count: number }) => void;
clearAll: () => void; clearAll: () => void;
setModelSelections: ( setModelSelections?: (
modelList: (Global.BaseOption<string> & { modelList: (Global.BaseOption<string> & {
instanceId: symbol; instanceId: symbol;
})[] })[]
) => void; ) => void;
presetPrompt: (list: CurrentMessage[]) => void; presetPrompt?: (list: CurrentMessage[]) => void;
addMessage: (message: CurrentMessage) => void; addMessage?: (message: CurrentMessage) => void;
loading: boolean; loading: boolean;
showModelSelection?: boolean; showModelSelection?: boolean;
disabled: boolean; disabled: boolean;
isEmpty?: boolean; isEmpty?: boolean;
scope: string; scope: string;
placeholer?: string;
} }
const MessageInput: React.FC<MessageInputProps> = ({ const MessageInput: React.FC<MessageInputProps> = ({
@@ -88,12 +88,11 @@ const MessageInput: React.FC<MessageInputProps> = ({
showModelSelection, showModelSelection,
disabled, disabled,
isEmpty, isEmpty,
scope scope,
placeholer
}) => { }) => {
const { TextArea } = Input; const { TextArea } = Input;
const intl = useIntl(); const intl = useIntl();
const platform = platformCall();
// const [disabled, setDisabled] = useState(false);
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [focused, setFocused] = useState(false); const [focused, setFocused] = useState(false);
const [message, setMessage] = useState<CurrentMessage>({ const [message, setMessage] = useState<CurrentMessage>({
@@ -130,11 +129,9 @@ const MessageInput: React.FC<MessageInputProps> = ({
resetMessage(); resetMessage();
}; };
const onStop = () => { const onStop = () => {
// setDisabled(false);
handleAbortFetch(); handleAbortFetch();
}; };
const handleLayoutChange = (value: { span: number; count: number }) => { const handleLayoutChange = (value: { span: number; count: number }) => {
console.log('layout change:', value);
updateLayout?.(value); updateLayout?.(value);
}; };
@@ -151,7 +148,6 @@ const MessageInput: React.FC<MessageInputProps> = ({
}; };
const handleUpdateModelSelections = (value: string[]) => { const handleUpdateModelSelections = (value: string[]) => {
console.log('update model selections:', value);
const list = value?.map?.((val) => { const list = value?.map?.((val) => {
return { return {
value: val, value: val,
@@ -159,17 +155,21 @@ const MessageInput: React.FC<MessageInputProps> = ({
instanceId: Symbol(val) instanceId: Symbol(val)
}; };
}); });
setModelSelections(list); setModelSelections?.(list);
}; };
const handleOpenPrompt = () => { const handleOpenPrompt = () => {
setOpen(true); setOpen(true);
}; };
const handleAddMessage = () => { const handleAddMessage = (e?: any) => {
console.log('add message=====1'); e?.preventDefault();
addMessage({ ...message }); addMessage?.({ ...message });
resetMessage(); resetMessage();
setFocused(true);
setTimeout(() => {
inputRef.current?.focus?.();
}, 100);
}; };
const getPasteContent = useCallback( const getPasteContent = useCallback(
@@ -246,14 +246,26 @@ const MessageInput: React.FC<MessageInputProps> = ({
}; };
const handleOnPaste = (e: any) => { const handleOnPaste = (e: any) => {
e.preventDefault(); // e.preventDefault();
const text = e.clipboardData.getData('text'); const text = e.clipboardData.getData('text');
if (text) { if (text) {
setMessage?.({ // const startPos = e.target.selectionStart;
...message, // const endPos = e.target.selectionEnd;
content: message.content + text // setMessage?.({
}); // ...message,
// content:
// message.content.slice(0, startPos) +
// text +
// message.content.slice(endPos)
// });
// if (endPos !== startPos) {
// setTimeout(() => {
// e.target.setSelectionRange(endPos, endPos);
// }, 0);
// }
} else { } else {
e.preventDefault();
getPasteContent(e); getPasteContent(e);
} }
}; };
@@ -284,6 +296,10 @@ const MessageInput: React.FC<MessageInputProps> = ({
[message, handleDeleteLastImage] [message, handleDeleteLastImage]
); );
const handleSelectPrompt = (list: CurrentMessage[]) => {
presetPrompt?.(list);
};
useHotkeys( useHotkeys(
HotKeys.SUBMIT, HotKeys.SUBMIT,
(e: any) => { (e: any) => {
@@ -325,6 +341,8 @@ const MessageInput: React.FC<MessageInputProps> = ({
<div className="messageInput"> <div className="messageInput">
<div className="tool-bar"> <div className="tool-bar">
<div className="actions"> <div className="actions">
{scope !== 'reranker' && (
<>
<Button <Button
type="text" type="text"
size="middle" size="middle"
@@ -334,7 +352,14 @@ const MessageInput: React.FC<MessageInputProps> = ({
{intl.formatMessage({ id: `playground.${message.role}` })} {intl.formatMessage({ id: `playground.${message.role}` })}
</Button> </Button>
<Divider type="vertical" style={{ margin: 0 }} /> <Divider type="vertical" style={{ margin: 0 }} />
<UploadImg handleUpdateImgList={handleUpdateImgList}></UploadImg> {message.role === Roles.User && (
<UploadImg
handleUpdateImgList={handleUpdateImgList}
size="middle"
></UploadImg>
)}
</>
)}
<Tooltip <Tooltip
title={intl.formatMessage({ id: 'playground.toolbar.clearmsg' })} title={intl.formatMessage({ id: 'playground.toolbar.clearmsg' })}
> >
@@ -342,20 +367,10 @@ const MessageInput: React.FC<MessageInputProps> = ({
type="text" type="text"
icon={<ClearOutlined />} icon={<ClearOutlined />}
size="middle" size="middle"
disabled={loading}
onClick={handleClearAll} onClick={handleClearAll}
></Button> ></Button>
</Tooltip> </Tooltip>
{/* <Tooltip
title={intl.formatMessage({ id: 'playground.toolbar.prompts' })}
>
<Button
type="text"
icon={<ControlOutlined />}
size="middle"
onClick={handleOpenPrompt}
></Button>
</Tooltip> */}
{updateLayout && ( {updateLayout && (
<> <>
<Divider type="vertical" style={{ margin: 0 }} /> <Divider type="vertical" style={{ margin: 0 }} />
@@ -391,6 +406,7 @@ const MessageInput: React.FC<MessageInputProps> = ({
></Select> ></Select>
)} )}
{scope !== 'reranker' && (
<Tooltip <Tooltip
title={ title={
<span> <span>
@@ -403,6 +419,7 @@ const MessageInput: React.FC<MessageInputProps> = ({
{intl.formatMessage({ id: 'common.button.add' })} {intl.formatMessage({ id: 'common.button.add' })}
</Button> </Button>
</Tooltip> </Tooltip>
)}
{!loading ? ( {!loading ? (
<Tooltip <Tooltip
title={ title={
@@ -440,6 +457,7 @@ const MessageInput: React.FC<MessageInputProps> = ({
onDelete={handleDeleteImg} onDelete={handleDeleteImg}
></ThumbImg> ></ThumbImg>
<div className="input-box"> <div className="input-box">
{scope !== 'reranker' ? (
<TextArea <TextArea
ref={inputRef} ref={inputRef}
autoSize={{ minRows: 3, maxRows: 8 }} autoSize={{ minRows: 3, maxRows: 8 }}
@@ -452,11 +470,26 @@ const MessageInput: React.FC<MessageInputProps> = ({
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
onPaste={handleOnPaste} onPaste={handleOnPaste}
></TextArea> ></TextArea>
) : (
<TextArea
ref={inputRef}
autoSize={{ minRows: 3, maxRows: 8 }}
onChange={handleInputChange}
value={message.content}
size="large"
variant="borderless"
onFocus={() => setFocused(true)}
onBlur={() => setFocused(false)}
onKeyDown={handleKeyDown}
></TextArea>
)}
{!message.content && !focused && ( {!message.content && !focused && (
<span <span
className="holder" className="holder"
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: intl.formatMessage({ id: 'playground.input.holder' }) __html:
placeholer ??
intl.formatMessage({ id: 'playground.input.holder' })
}} }}
></span> ></span>
)} )}
@@ -464,7 +497,7 @@ const MessageInput: React.FC<MessageInputProps> = ({
<PromptModal <PromptModal
open={open} open={open}
onCancel={() => setOpen(false)} onCancel={() => setOpen(false)}
onSelect={presetPrompt} onSelect={handleSelectPrompt}
></PromptModal> ></PromptModal>
</div> </div>
); );
@@ -15,6 +15,7 @@ interface MessageItemProps {
data: MessageItem; data: MessageItem;
editable?: boolean; editable?: boolean;
loading?: boolean; loading?: boolean;
actions?: string[];
updateMessage?: (message: MessageItem) => void; updateMessage?: (message: MessageItem) => void;
onDelete?: () => void; onDelete?: () => void;
} }
@@ -24,7 +25,8 @@ const ContentItem: React.FC<MessageItemProps> = ({
onDelete, onDelete,
loading, loading,
data, data,
editable editable,
actions = ['upload', 'delete', 'copy']
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
const inputRef = useRef<any>(null); const inputRef = useRef<any>(null);
@@ -56,7 +58,6 @@ const ContentItem: React.FC<MessageItemProps> = ({
for (let i = 0; i < items.length; i++) { for (let i = 0; i < items.length; i++) {
let item = items[i]; let item = items[i];
console.log('item===========', item);
if (item.kind === 'file' && item.type.indexOf('image') !== -1) { if (item.kind === 'file' && item.type.indexOf('image') !== -1) {
const file = item.getAsFile(); const file = item.getAsFile();
@@ -100,26 +101,34 @@ const ContentItem: React.FC<MessageItemProps> = ({
console.error('Error processing images:', error); console.error('Error processing images:', error);
} }
}, },
[data] [data, updateMessage]
); );
const handleOnPaste = (e: any) => { const handleOnPaste = (e: any) => {
e.preventDefault(); // e.preventDefault();
const text = e.clipboardData.getData('text'); const text = e.clipboardData.getData('text');
if (text) { if (text) {
updateMessage?.({ // const startPos = e.target.selectionStart;
role: data.role, // const endPos = e.target.selectionEnd;
content: data.content + text, // updateMessage?.({
uid: data.uid // role: data.role,
}); // content:
// data.content.slice(0, startPos) + text + data.content.slice(endPos),
// uid: data.uid
// });
// if (endPos !== startPos) {
// setTimeout(() => {
// e.target.setSelectionRange(endPos, endPos);
// }, 0);
// }
} else { } else {
e.preventDefault();
getPasteContent(e); getPasteContent(e);
} }
}; };
const handleUpdateImgList = useCallback( const handleUpdateImgList = useCallback(
(list: { uid: number | string; dataUrl: string }[]) => { (list: { uid: number | string; dataUrl: string }[]) => {
console.log('list===========', data.imgs, list);
updateMessage?.({ updateMessage?.({
role: data.role, role: data.role,
content: data.content, content: data.content,
@@ -127,7 +136,7 @@ const ContentItem: React.FC<MessageItemProps> = ({
imgs: [...(data.imgs || []), ...list] imgs: [...(data.imgs || []), ...list]
}); });
}, },
[data] [data, updateMessage]
); );
const handleDeleteImg = (uid: number | string) => { const handleDeleteImg = (uid: number | string) => {
@@ -177,13 +186,13 @@ const ContentItem: React.FC<MessageItemProps> = ({
<div className="content-item"> <div className="content-item">
<div className="content-item-role"> <div className="content-item-role">
<div className="role"> <div className="role">
{intl.formatMessage({ id: `playground.${data.role}` })} {data.title ?? intl.formatMessage({ id: `playground.${data.role}` })}
</div> </div>
<div className="actions"> <div className="actions">
{editable && ( {actions.includes('upload') && data.role === Roles.User && (
<UploadImg handleUpdateImgList={handleUpdateImgList}></UploadImg> <UploadImg handleUpdateImgList={handleUpdateImgList}></UploadImg>
)} )}
{data.content && ( {data.content && actions.includes('copy') && (
<CopyButton <CopyButton
text={data.content} text={data.content}
size="small" size="small"
@@ -192,7 +201,7 @@ const ContentItem: React.FC<MessageItemProps> = ({
fontSize="12px" fontSize="12px"
></CopyButton> ></CopyButton>
)} )}
{editable && ( {actions.includes('delete') && (
<Tooltip title={intl.formatMessage({ id: 'common.button.delete' })}> <Tooltip title={intl.formatMessage({ id: 'common.button.delete' })}>
<Button <Button
size="small" size="small"
@@ -230,9 +239,7 @@ const ContentItem: React.FC<MessageItemProps> = ({
></Input.TextArea> ></Input.TextArea>
</div> </div>
) : ( ) : (
<div className="content-item-content"> <div className="content-item-content">{data.content}</div>
<span>{data.content}</span>
</div>
)} )}
</div> </div>
); );
@@ -5,10 +5,11 @@ import ContentItem from './content-item';
interface MessageContentProps { interface MessageContentProps {
loading?: boolean; loading?: boolean;
spans: { spans?: {
span: number; span: number;
count: number; count: number;
}; };
actions?: string[];
editable?: boolean; editable?: boolean;
messageList: MessageItem[]; messageList: MessageItem[];
setMessageList?: (list: any) => void; setMessageList?: (list: any) => void;
@@ -17,8 +18,8 @@ interface MessageContentProps {
const MessageContent: React.FC<MessageContentProps> = ({ const MessageContent: React.FC<MessageContentProps> = ({
setMessageList, setMessageList,
messageList, messageList,
spans, editable,
editable actions = ['upload', 'delete', 'copy']
}) => { }) => {
const updateMessage = (index: number, message: MessageItem) => { const updateMessage = (index: number, message: MessageItem) => {
const newMessageList = [...messageList]; const newMessageList = [...messageList];
@@ -41,6 +42,7 @@ const MessageContent: React.FC<MessageContentProps> = ({
key={item.uid} key={item.uid}
data={item} data={item}
editable={editable} editable={editable}
actions={actions}
onDelete={() => handleDelete(index)} onDelete={() => handleDelete(index)}
updateMessage={(data) => updateMessage(index, data)} updateMessage={(data) => updateMessage(index, data)}
/> />
@@ -14,6 +14,7 @@ import _ from 'lodash';
import 'overlayscrollbars/overlayscrollbars.css'; import 'overlayscrollbars/overlayscrollbars.css';
import React, { import React, {
forwardRef, forwardRef,
useCallback,
useContext, useContext,
useEffect, useEffect,
useImperativeHandle, useImperativeHandle,
@@ -23,7 +24,7 @@ import React, {
} from 'react'; } from 'react';
import 'simplebar-react/dist/simplebar.min.css'; import 'simplebar-react/dist/simplebar.min.css';
import { CHAT_API } from '../../apis'; import { CHAT_API } from '../../apis';
import { Roles } from '../../config'; import { Roles, generateMessages } from '../../config';
import CompareContext from '../../config/compare-context'; import CompareContext from '../../config/compare-context';
import { MessageItem, ModelSelectionItem } from '../../config/types'; import { MessageItem, ModelSelectionItem } from '../../config/types';
import '../../style/model-item.less'; import '../../style/model-item.less';
@@ -69,6 +70,13 @@ const ModelItem: React.FC<ModelItemProps> = forwardRef(
const { initialize, updateScrollerPosition } = useOverlayScroller(); const { initialize, updateScrollerPosition } = useOverlayScroller();
const viewCodeMessage = useMemo(() => {
return generateMessages([
{ role: Roles.System, content: systemMessage },
...messageList
]);
}, [messageList, systemMessage]);
const setMessageId = () => { const setMessageId = () => {
messageId.current = messageId.current + 1; messageId.current = messageId.current + 1;
}; };
@@ -121,58 +129,25 @@ const ModelItem: React.FC<ModelItemProps> = forwardRef(
setMessageList((preList) => { setMessageList((preList) => {
return [...preList, ...currentMessageRef.current]; return [...preList, ...currentMessageRef.current];
}); });
const messages = _.map(
[...messageList, ...currentMessageRef.current],
(item: MessageItem) => {
return {
role: item.role,
content: item.content,
imgs: item.imgs || []
};
}
);
contentRef.current = ''; contentRef.current = '';
// ====== payload ================= // ====== payload =================
const formatMessages = _.map(messages, (item: MessageItem) => {
return { const messageParams = [
role: item.role, { role: Roles.System, content: systemMessage },
content: [ ...messageList,
{ ...currentMessageRef.current
type: 'text', ];
text: item.content
}, const messages = generateMessages(messageParams);
..._.map(
item.imgs,
(img: { uid: string | number; dataUrl: string }) => {
return {
type: 'image_url',
image_url: {
url: img.dataUrl
}
};
}
)
]
};
});
const chatParams = { const chatParams = {
messages: systemMessage messages: messages,
? [
{
role: Roles.System,
content: [
{
type: 'text',
text: systemMessage
}
]
},
...formatMessages
]
: [...formatMessages],
...params, ...params,
stream: true stream: true,
stream_options: {
include_usage: true
}
}; };
// ============== payload end ================ // ============== payload end ================
const result: any = await fetchChunkedData({ const result: any = await fetchChunkedData({
@@ -184,13 +159,21 @@ const ModelItem: React.FC<ModelItemProps> = forwardRef(
if (result?.error) { if (result?.error) {
setTokenResult({ setTokenResult({
error: true, error: true,
errorMessage: result?.data?.message errorMessage:
result?.data?.error?.message || result?.data?.message || ''
}); });
return; return;
} }
setMessageId(); setMessageId();
const { reader, decoder } = result; const { reader, decoder } = result;
await readStreamData(reader, decoder, (chunk: any) => { await readStreamData(reader, decoder, (chunk: any) => {
if (chunk?.error) {
setTokenResult({
error: true,
errorMessage: chunk?.error?.message || chunk?.message || ''
});
return;
}
joinMessage(chunk); joinMessage(chunk);
}); });
} catch (error) { } catch (error) {
@@ -213,7 +196,6 @@ const ModelItem: React.FC<ModelItemProps> = forwardRef(
}; };
const handleApplyToAllModels = (e: any) => { const handleApplyToAllModels = (e: any) => {
console.log('checkbox change:', e.target.checked);
isApplyToAllModels.current = e.target.checked; isApplyToAllModels.current = e.target.checked;
if (e.target.checked) { if (e.target.checked) {
setGlobalParams({ setGlobalParams({
@@ -222,10 +204,8 @@ const ModelItem: React.FC<ModelItemProps> = forwardRef(
} }
}; };
const handleOnValuesChange = ( const handleOnValuesChange = useCallback(
changeValues: any, (changeValues: any, allValues: Record<string, any>) => {
allValues: Record<string, any>
) => {
if (isApplyToAllModels.current) { if (isApplyToAllModels.current) {
setParams({ setParams({
...params, ...params,
@@ -240,12 +220,13 @@ const ModelItem: React.FC<ModelItemProps> = forwardRef(
...changeValues ...changeValues
}); });
} }
}; },
[params, isApplyToAllModels.current]
);
const handleClearMessage = () => { const handleClearMessage = () => {
setMessageList([]); setMessageList([]);
setTokenResult(null); setTokenResult(null);
setSystemMessage('');
currentMessageRef.current = []; currentMessageRef.current = [];
}; };
@@ -331,7 +312,6 @@ const ModelItem: React.FC<ModelItemProps> = forwardRef(
}, [modelList, intl]); }, [modelList, intl]);
useEffect(() => { useEffect(() => {
console.log('globalParams:', globalParams.model, globalParams);
setParams({ setParams({
...params, ...params,
model: model, model: model,
@@ -381,19 +361,34 @@ const ModelItem: React.FC<ModelItemProps> = forwardRef(
<div className="header"> <div className="header">
<span className="title"> <span className="title">
<Select <Select
style={{ minWidth: '120px', maxWidth: '200px' }} style={{ width: '100%' }}
variant="borderless" variant="borderless"
options={modelFullList} options={modelFullList}
onChange={handleModelChange} onChange={handleModelChange}
value={params.model} value={params.model}
optionRender={(data) => { labelRender={(data) => {
return ( return (
<AutoTooltip <AutoTooltip
title={data.label}
ghost ghost
tooltipProps={{ tooltipProps={{
placement: 'right' placement: 'right'
}} }}
minWidth={60}
maxWidth={180}
>
{data.label}
</AutoTooltip>
);
}}
optionRender={(data) => {
return (
<AutoTooltip
ghost
tooltipProps={{
placement: 'right'
}}
minWidth={60}
maxWidth={180}
> >
{data.label} {data.label}
</AutoTooltip> </AutoTooltip>
@@ -401,7 +396,7 @@ const ModelItem: React.FC<ModelItemProps> = forwardRef(
}} }}
></Select> ></Select>
</span> </span>
<ReferenceParams usage={tokenResult}></ReferenceParams> <ReferenceParams usage={tokenResult} scaleable></ReferenceParams>
<span className="action"> <span className="action">
<Dropdown <Dropdown
menu={{ menu={{
@@ -471,8 +466,7 @@ const ModelItem: React.FC<ModelItemProps> = forwardRef(
</div> </div>
<ViewCodeModal <ViewCodeModal
open={show} open={show}
systemMessage={systemMessage} messageList={viewCodeMessage}
messageList={messageList}
parameters={params} parameters={params}
onCancel={handleCloseViewCode} onCancel={handleCloseViewCode}
title={intl.formatMessage({ id: 'playground.viewcode' })} title={intl.formatMessage({ id: 'playground.viewcode' })}
@@ -6,7 +6,7 @@ import { InfoCircleOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Form, InputNumber, Slider, Tooltip } from 'antd'; import { Form, InputNumber, Slider, Tooltip } from 'antd';
import _ from 'lodash'; import _ from 'lodash';
import { useEffect, useId, useState } from 'react'; import { memo, useCallback, useEffect, useId } from 'react';
import CustomLabelStyles from '../style/custom-label.less'; import CustomLabelStyles from '../style/custom-label.less';
type ParamsSettingsFormProps = { type ParamsSettingsFormProps = {
@@ -34,12 +34,10 @@ const ParamsSettings: React.FC<ParamsSettingsProps> = ({
setParams, setParams,
globalParams, globalParams,
onValuesChange, onValuesChange,
model,
modelList, modelList,
showModelSelector = true showModelSelector = true
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
const [ModelList, setModelList] = useState([]);
const initialValues = { const initialValues = {
seed: null, seed: null,
stop: null, stop: null,
@@ -81,12 +79,15 @@ const ParamsSettings: React.FC<ParamsSettingsProps> = ({
console.log('handleOnFinishFailed', errorInfo); console.log('handleOnFinishFailed', errorInfo);
}; };
const handleValuesChange = (changedValues: any, allValues: any) => { const handleValuesChange = useCallback(
console.log('changedValues===', changedValues); (changedValues: any, allValues: any) => {
setParams?.(allValues); setParams?.(allValues);
onValuesChange?.(changedValues, allValues); onValuesChange?.(changedValues, allValues);
}; },
const handleFieldValueChange = (val: any, field: string) => { [onValuesChange, setParams]
);
const handleFieldValueChange = useCallback(
(val: any, field: string) => {
const values = form.getFieldsValue(); const values = form.getFieldsValue();
form.setFieldsValue({ form.setFieldsValue({
...values, ...values,
@@ -103,7 +104,9 @@ const ParamsSettings: React.FC<ParamsSettingsProps> = ({
[field]: val [field]: val
} }
); );
}; },
[form, setParams, onValuesChange]
);
const handleResetParams = () => { const handleResetParams = () => {
form.setFieldsValue(initialValues); form.setFieldsValue(initialValues);
@@ -177,7 +180,7 @@ const ParamsSettings: React.FC<ParamsSettingsProps> = ({
} }
]} ]}
> >
<SealSelect showSearch options={modelList}></SealSelect> <SealSelect showSearch={true} options={modelList}></SealSelect>
</Form.Item> </Form.Item>
</> </>
)} )}
@@ -296,4 +299,4 @@ const ParamsSettings: React.FC<ParamsSettingsProps> = ({
); );
}; };
export default ParamsSettings; export default memo(ParamsSettings);
@@ -1,10 +1,13 @@
import { WarningOutlined } from '@ant-design/icons'; import { WarningOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Alert, Space, Tooltip } from 'antd'; import { Space, Tooltip, Typography } from 'antd';
import classNames from 'classnames';
import _ from 'lodash'; import _ from 'lodash';
import '../style/reference-params.less'; import '../style/reference-params.less';
interface ReferenceParamsProps { interface ReferenceParamsProps {
showOutput?: boolean;
scaleable?: boolean;
usage: { usage: {
error?: boolean; error?: boolean;
errorMessage?: string; errorMessage?: string;
@@ -19,29 +22,41 @@ interface ReferenceParamsProps {
const ReferenceParams = (props: ReferenceParamsProps) => { const ReferenceParams = (props: ReferenceParamsProps) => {
const intl = useIntl(); const intl = useIntl();
const { usage } = props; const { usage, showOutput = true, scaleable } = props;
if (!usage || _.isEmpty(usage)) { if (!usage || _.isEmpty(usage)) {
return null; return null;
} }
if (usage.error) { if (usage.error) {
return ( return (
<Alert <Typography.Paragraph
type="error" type="danger"
style={{ textAlign: 'center', paddingBlock: 0 }} ellipsis={{
message={ rows: 2,
<span style={{ color: 'var(--ant-color-error)' }}> tooltip: usage?.errorMessage
}}
style={{
textAlign: 'center',
paddingBlock: 0,
margin: 0,
backgroundColor: 'var(--ant-color-error-bg)'
}}
>
<WarningOutlined className="m-r-8" /> <WarningOutlined className="m-r-8" />
{usage?.errorMessage} {usage?.errorMessage}
</span> </Typography.Paragraph>
}
banner
showIcon={false}
/>
); );
} }
return ( return (
<div className="reference-params"> <div
<span className="usage"> className={classNames('reference-params', {
scaleable: scaleable
})}
>
<span
className={classNames('usage', {
scaleable: scaleable
})}
>
<Tooltip <Tooltip
title={ title={
<Space> <Space>
@@ -63,7 +78,12 @@ const ReferenceParams = (props: ReferenceParamsProps) => {
</Tooltip> </Tooltip>
</span> </span>
<span className="usage"> {showOutput && (
<span
className={classNames('usage', {
scaleable: scaleable
})}
>
<Tooltip <Tooltip
title={ title={
<Space> <Space>
@@ -82,6 +102,7 @@ const ReferenceParams = (props: ReferenceParamsProps) => {
</span> </span>
</Tooltip> </Tooltip>
</span> </span>
)}
</div> </div>
); );
}; };
@@ -0,0 +1,56 @@
import { StarFilled } from '@ant-design/icons';
import { Tooltip } from 'antd';
import _ from 'lodash';
import React from 'react';
import '../style/content-item.less';
import '../style/rerank-message.less';
interface RerankMessageProps {
dataList: { title?: string; content: any; uid: number | string }[];
}
const RerankMessage: React.FC<RerankMessageProps> = ({ dataList }) => {
return (
<div className="rerank-message">
{dataList.map((item) => {
return (
<div className="content-item" key={item.uid}>
<div className="content-item-role">
<span className="role">{item.title}</span>
</div>
<div className="content-item-content">
{Array.isArray(item.content) ? (
<div className="result">
{item.content.map((sItem, sIndex) => {
return (
<dl className="content-item-text" key={sItem.uid}>
<dt className="rank">
<span>[{sItem.docIndex + 1}]</span>
<span className="score">
<Tooltip
title={
<span>Score: {_.round(sItem.score, 2)}</span>
}
>
<StarFilled className="m-r-5" />
{_.round(sItem.score, 2)}
</Tooltip>
</span>
</dt>
<dd className="text">{sItem.text}</dd>
{/* <dd className="doc-name">《{sItem.title}》</dd> */}
</dl>
);
})}
</div>
) : (
<div className="content-item-text">{item.content}</div>
)}
</div>
</div>
);
})}
</div>
);
};
export default React.memo(RerankMessage);
@@ -0,0 +1,188 @@
import SealInput from '@/components/seal-form/seal-input';
import SealSelect from '@/components/seal-form/seal-select';
import { INPUT_WIDTH } from '@/constants';
import { InfoCircleOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Form, InputNumber, Tooltip } from 'antd';
import _ from 'lodash';
import { memo, useCallback, useEffect, useId } from 'react';
import CustomLabelStyles from '../style/custom-label.less';
type ParamsSettingsFormProps = {
top_n?: number;
model?: string;
};
type ParamsSettingsProps = {
selectedModel?: string;
showModelSelector?: boolean;
params?: ParamsSettingsFormProps;
model?: string;
modelList: Global.BaseOption<string>[];
onValuesChange?: (changeValues: any, value: Record<string, any>) => void;
setParams: (params: any) => void;
globalParams?: ParamsSettingsFormProps;
};
const ParamsSettings: React.FC<ParamsSettingsProps> = ({
selectedModel,
setParams,
globalParams,
onValuesChange,
modelList,
showModelSelector = true
}) => {
const intl = useIntl();
const initialValues = {
top_n: 1
};
const [form] = Form.useForm();
const formId = useId();
useEffect(() => {
if (showModelSelector) {
form.setFieldsValue({
model: selectedModel || _.get(modelList, '[0].value'),
...initialValues
});
setParams({
model: selectedModel || _.get(modelList, '[0].value'),
...initialValues
});
} else {
form.setFieldsValue({
model: selectedModel || '',
...initialValues
});
setParams({
model: selectedModel || '',
...initialValues
});
}
}, [modelList, showModelSelector, selectedModel]);
const handleOnFinish = (values: any) => {
console.log('handleOnFinish', values);
};
const handleOnFinishFailed = (errorInfo: any) => {
console.log('handleOnFinishFailed', errorInfo);
};
const handleValuesChange = useCallback(
(changedValues: any, allValues: any) => {
setParams?.(allValues);
onValuesChange?.(changedValues, allValues);
},
[onValuesChange, setParams]
);
const handleFieldValueChange = useCallback(
(val: any, field: string) => {
const values = form.getFieldsValue();
form.setFieldsValue({
...values,
[field]: val
});
setParams({
...values,
[field]: val
});
onValuesChange?.(
{ [field]: val },
{
...values,
[field]: val
}
);
},
[form, setParams, onValuesChange]
);
useEffect(() => {
form.setFieldsValue(globalParams);
}, [globalParams]);
const renderLabel = (args: {
field: string;
label: string;
description: string;
}) => {
return (
<span
className={CustomLabelStyles.label}
style={{ width: INPUT_WIDTH.mini }}
>
<span className="text">
{args.description ? (
<Tooltip title={args.description}>
<span> {args.label}</span>
<span className="m-l-5">
<InfoCircleOutlined />
</span>
</Tooltip>
) : (
<span>{args.label}</span>
)}
</span>
<InputNumber
className="label-val"
variant="outlined"
size="small"
value={form.getFieldValue(args.field)}
controls={false}
onChange={(val) => handleFieldValueChange(val, args.field)}
></InputNumber>
</span>
);
};
return (
<Form
name={formId}
form={form}
onValuesChange={handleValuesChange}
onFinish={handleOnFinish}
onFinishFailed={handleOnFinishFailed}
>
<div>
{
<>
<Form.Item<ParamsSettingsFormProps>
name="model"
rules={[
{
required: true,
message: intl.formatMessage(
{
id: 'common.form.rule.select'
},
{ name: intl.formatMessage({ id: 'playground.model' }) }
)
}
]}
>
<SealSelect
showSearch={true}
options={modelList}
label={intl.formatMessage({ id: 'playground.model' })}
></SealSelect>
</Form.Item>
</>
}
<Form.Item<ParamsSettingsFormProps>
name="top_n"
rules={[{ required: true }]}
>
<SealInput.Number
style={{ width: '100%' }}
label="Top N"
min={1}
></SealInput.Number>
</Form.Item>
</div>
</Form>
);
};
export default memo(ParamsSettings);
+11 -5
View File
@@ -1,5 +1,5 @@
import AutoImage from '@/components/auto-image';
import { CloseCircleOutlined, EyeOutlined } from '@ant-design/icons'; import { CloseCircleOutlined, EyeOutlined } from '@ant-design/icons';
import { Image } from 'antd';
import _ from 'lodash'; import _ from 'lodash';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import '../style/thumb-img.less'; import '../style/thumb-img.less';
@@ -23,12 +23,18 @@ const ThumbImg: React.FC<{
<div className="thumb-list-wrap"> <div className="thumb-list-wrap">
{_.map(dataList, (item: any) => { {_.map(dataList, (item: any) => {
return ( return (
<span key={item.uid} className="thumb-img"> <span
key={item.uid}
className="thumb-img"
style={{
width: item.width,
height: item.height
}}
>
<span className="img"> <span className="img">
<Image <AutoImage
src={item.dataUrl} src={item.dataUrl}
width={56} height={100}
height={56}
preview={{ preview={{
mask: <EyeOutlined /> mask: <EyeOutlined />
}} }}
@@ -0,0 +1,118 @@
import { readBlob, readExcelContent, readWordContent } from '@/utils';
import { PaperClipOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Button, Tooltip, Upload } from 'antd';
import type { UploadFile } from 'antd/es/upload';
import { RcFile } from 'antd/es/upload';
import { debounce } from 'lodash';
import React, { useCallback, useRef } from 'react';
interface UploadImgProps {
size?: 'small' | 'middle' | 'large';
accept?: string;
children?: React.ReactNode;
handleUpdateFileList: (
fileList: { text: string; name: string; uid: number | string }[]
) => void;
}
const UploadImg: React.FC<UploadImgProps> = ({
handleUpdateFileList,
size = 'small',
accept = '.txt,.doc,.docx',
children
}) => {
const intl = useIntl();
const uploadRef = useRef<any>(null);
const wordReg = /\.(doc|docx)$/;
const excelReg = /\.(xls|xlsx)$/;
const getBase64 = useCallback((file: RcFile): Promise<string> => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result as string);
reader.onerror = (error) => reject(error);
});
}, []);
const debouncedUpdate = useCallback(
debounce(
(files: { text: string; name: string; uid: number | string }[]) => {
handleUpdateFileList(files);
},
300
),
[handleUpdateFileList, intl]
);
const handleChange = useCallback(
async (info: any) => {
try {
const { fileList } = info;
const newFileList = await Promise.all(
fileList.map(async (item: UploadFile) => {
if (wordReg.test(item.name)) {
const context = await readWordContent(
item.originFileObj as RcFile
);
item.url = context;
} else if (excelReg.test(item.name)) {
const context = await readExcelContent(
item.originFileObj as RcFile
);
item.url = context;
} else {
const context = await readBlob(item.originFileObj as RcFile);
item.url = context;
}
return item;
})
);
if (newFileList.length > 0) {
const files = newFileList
.filter((sitem) => sitem.url)
.map((item: UploadFile) => {
return {
text: item.url as string,
name: item.name as string,
uid: item.uid
};
});
debouncedUpdate(files);
}
} catch (error) {
// console.log('error', error);
}
},
[debouncedUpdate, getBase64]
);
return (
<>
<Upload.Dragger
ref={uploadRef}
multiple
action="/"
accept={accept}
fileList={[]}
beforeUpload={(file) => false}
onChange={handleChange}
>
{children ? (
children
) : (
<Tooltip title={intl.formatMessage({ id: 'playground.upload' })}>
<Button size={size} icon={<PaperClipOutlined />} />
</Tooltip>
)}
</Upload.Dragger>
</>
);
};
export default React.memo(UploadImg);
+14 -6
View File
@@ -7,23 +7,27 @@ import { debounce } from 'lodash';
import React, { useCallback, useRef } from 'react'; import React, { useCallback, useRef } from 'react';
interface UploadImgProps { interface UploadImgProps {
size?: 'small' | 'middle' | 'large';
handleUpdateImgList: ( handleUpdateImgList: (
imgList: { dataUrl: string; uid: number | string }[] imgList: { dataUrl: string; uid: number | string }[]
) => void; ) => void;
} }
const UploadImg: React.FC<UploadImgProps> = ({ handleUpdateImgList }) => { const UploadImg: React.FC<UploadImgProps> = ({
handleUpdateImgList,
size = 'small'
}) => {
const intl = useIntl(); const intl = useIntl();
const uploadRef = useRef<any>(null); const uploadRef = useRef<any>(null);
const getBase64 = (file: RcFile): Promise<string> => { const getBase64 = useCallback((file: RcFile): Promise<string> => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const reader = new FileReader(); const reader = new FileReader();
reader.readAsDataURL(file); reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result as string); reader.onload = () => resolve(reader.result as string);
reader.onerror = (error) => reject(error); reader.onerror = (error) => reject(error);
}); });
}; }, []);
const debouncedUpdate = useCallback( const debouncedUpdate = useCallback(
debounce((base64List: { dataUrl: string; uid: number | string }[]) => { debounce((base64List: { dataUrl: string; uid: number | string }[]) => {
@@ -32,13 +36,15 @@ const UploadImg: React.FC<UploadImgProps> = ({ handleUpdateImgList }) => {
[handleUpdateImgList, intl] [handleUpdateImgList, intl]
); );
const handleChange = async (info: any) => { const handleChange = useCallback(
async (info: any) => {
const { fileList } = info; const { fileList } = info;
const newFileList = await Promise.all( const newFileList = await Promise.all(
fileList.map(async (item: UploadFile) => { fileList.map(async (item: UploadFile) => {
if (item.originFileObj && !item.url) { if (item.originFileObj && !item.url) {
const base64 = await getBase64(item.originFileObj as RcFile); const base64 = await getBase64(item.originFileObj as RcFile);
item.url = base64; item.url = base64;
} }
return item; return item;
@@ -57,7 +63,9 @@ const UploadImg: React.FC<UploadImgProps> = ({ handleUpdateImgList }) => {
debouncedUpdate(base64List); debouncedUpdate(base64List);
} }
}; },
[debouncedUpdate, getBase64]
);
return ( return (
<> <>
@@ -71,7 +79,7 @@ const UploadImg: React.FC<UploadImgProps> = ({ handleUpdateImgList }) => {
onChange={handleChange} onChange={handleChange}
> >
<Tooltip title={intl.formatMessage({ id: 'playground.img.upload' })}> <Tooltip title={intl.formatMessage({ id: 'playground.img.upload' })}>
<Button size="small" type="text" icon={<PictureOutlined />}></Button> <Button size={size} type="text" icon={<PictureOutlined />}></Button>
</Tooltip> </Tooltip>
</Upload> </Upload>
</> </>
@@ -4,7 +4,7 @@ import { BulbOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Button, Modal } from 'antd'; import { Button, Modal } from 'antd';
import _ from 'lodash'; import _ from 'lodash';
import React, { useEffect, useRef, useState } from 'react'; import React, { useMemo, useState } from 'react';
type ViewModalProps = { type ViewModalProps = {
systemMessage?: string; systemMessage?: string;
@@ -33,16 +33,12 @@ const ViewCodeModal: React.FC<ViewModalProps> = (props) => {
title, title,
open, open,
onCancel, onCancel,
systemMessage,
messageList, messageList,
parameters = {}, parameters = {},
apiType = 'chat' apiType = 'chat'
} = props || {}; } = props || {};
const intl = useIntl(); const intl = useIntl();
const editorRef = useRef(null);
const [loaded, setLoaded] = useState(false);
const [codeValue, setCodeValue] = useState('');
const [lang, setLang] = useState(langMap.shell); const [lang, setLang] = useState(langMap.shell);
const BaseURL = `${window.location.origin}/v1-openai`; const BaseURL = `${window.location.origin}/v1-openai`;
@@ -51,42 +47,9 @@ const ViewCodeModal: React.FC<ViewModalProps> = (props) => {
const logcommand = const logcommand =
apiType === 'chat' ? 'choices[0].message.content' : 'data[0].embedding'; apiType === 'chat' ? 'choices[0].message.content' : 'data[0].embedding';
const generateCode = () => { const codeValue = useMemo(() => {
const systemList = systemMessage
? [
{
role: 'system',
content: [
{
type: 'text',
text: systemMessage
}
]
}
]
: [];
const formatMessageList = _.map(messageList, (item: any) => {
return {
role: item.role,
content: [
{
type: 'text',
text: item.content
},
..._.map(item.imgs, (img: any) => {
return {
type: 'image_url',
image_url: {
url: img.dataUrl
}
};
})
]
};
});
if (lang === langMap.shell) { if (lang === langMap.shell) {
const messages = [...systemList, ...formatMessageList]; const messages = messageList;
const code = `curl ${window.location.origin}/v1-openai/${api} \\\n-H "Content-Type: application/json" \\\n-H "Authorization: Bearer $\{YOUR_GPUSTACK_API_KEY}" \\\n-d '${JSON.stringify( const code = `curl ${window.location.origin}/v1-openai/${api} \\\n-H "Content-Type: application/json" \\\n-H "Authorization: Bearer $\{YOUR_GPUSTACK_API_KEY}" \\\n-d '${JSON.stringify(
{ {
...parameters, ...parameters,
@@ -95,9 +58,10 @@ const ViewCodeModal: React.FC<ViewModalProps> = (props) => {
null, null,
2 2
)}'`; )}'`;
setCodeValue(code); return code;
} else if (lang === langMap.javascript) { }
const messages = [...systemList, ...formatMessageList]; if (lang === langMap.javascript) {
const messages = messageList;
const code = `const OpenAI = require("openai");\n\nconst openai = new OpenAI({\n "apiKey": "YOUR_GPUSTACK_API_KEY",\n "baseURL": "${BaseURL}"\n});\n\nasync function main(){\n const params = ${JSON.stringify( const code = `const OpenAI = require("openai");\n\nconst openai = new OpenAI({\n "apiKey": "YOUR_GPUSTACK_API_KEY",\n "baseURL": "${BaseURL}"\n});\n\nasync function main(){\n const params = ${JSON.stringify(
{ {
...parameters, ...parameters,
@@ -106,8 +70,10 @@ const ViewCodeModal: React.FC<ViewModalProps> = (props) => {
null, null,
4 4
)};\nconst response = await openai.${ClientType}.create(params);\n console.log(response.${logcommand});\n}\nmain();`; )};\nconst response = await openai.${ClientType}.create(params);\n console.log(response.${logcommand});\n}\nmain();`;
setCodeValue(code);
} else if (lang === langMap.python) { return code;
}
if (lang === langMap.python) {
const formattedParams = _.keys(parameters).reduce( const formattedParams = _.keys(parameters).reduce(
(acc: string, key: string) => { (acc: string, key: string) => {
if (parameters[key] === null) { if (parameters[key] === null) {
@@ -123,16 +89,13 @@ const ViewCodeModal: React.FC<ViewModalProps> = (props) => {
); );
const messages = const messages =
apiType === 'chat' apiType === 'chat'
? `messages=${JSON.stringify( ? `messages=${JSON.stringify(messageList, null, 2)}`
[...systemList, ...formatMessageList],
null,
2
)}`
: ''; : '';
const code = `from openai import OpenAI\n\nclient = OpenAI(\n base_url="${BaseURL}", \n api_key="YOUR_GPUSTACK_API_KEY"\n)\n\nresponse = client.${ClientType}.create(\n${formattedParams} ${messages})\nprint(response.${logcommand})`; const code = `from openai import OpenAI\n\nclient = OpenAI(\n base_url="${BaseURL}", \n api_key="YOUR_GPUSTACK_API_KEY"\n)\n\nresponse = client.${ClientType}.create(\n${formattedParams} ${messages})\nprint(response.${logcommand})`;
setCodeValue(code); return code;
} }
}; return '';
}, [lang, messageList, parameters]);
const handleOnChangeLang = (value: string) => { const handleOnChangeLang = (value: string) => {
setLang(value); setLang(value);
@@ -142,25 +105,6 @@ const ViewCodeModal: React.FC<ViewModalProps> = (props) => {
setLang(langMap.shell); setLang(langMap.shell);
onCancel(); onCancel();
}; };
const editorConfig = {
minimap: {
enabled: false
},
hover: {
enabled: false
},
readOnly: true,
formatOnType: true,
formatOnPaste: true,
fontWeight: 'bold',
scrollbar: {
verticalSliderSize: 8
}
};
useEffect(() => {
generateCode();
}, [lang, systemMessage, messageList, parameters]);
return ( return (
<> <>
@@ -191,6 +135,12 @@ const ViewCodeModal: React.FC<ViewModalProps> = (props) => {
backgroundColor: 'var(--color-editor-dark)' backgroundColor: 'var(--color-editor-dark)'
} }
}} }}
>
<div
style={{
paddingRight: 2,
paddingBottom: 2
}}
> >
<HighlightCode <HighlightCode
height={380} height={380}
@@ -199,6 +149,7 @@ const ViewCodeModal: React.FC<ViewModalProps> = (props) => {
lang={lang} lang={lang}
copyable={false} copyable={false}
></HighlightCode> ></HighlightCode>
</div>
</EditorWrap> </EditorWrap>
<div <div
style={{ marginTop: 10, display: 'flex', alignItems: 'baseline' }} style={{ marginTop: 10, display: 'flex', alignItems: 'baseline' }}
@@ -234,4 +185,4 @@ const ViewCodeModal: React.FC<ViewModalProps> = (props) => {
); );
}; };
export default ViewCodeModal; export default React.memo(ViewCodeModal);
+90
View File
@@ -1,3 +1,6 @@
import { map } from 'lodash';
import { MessageItem } from './types';
export const Roles = { export const Roles = {
User: 'user', User: 'user',
Assistant: 'assistant', Assistant: 'assistant',
@@ -13,3 +16,90 @@ export const playGroundRoles = [
label: 'playground.assitant' label: 'playground.assitant'
} }
]; ];
export const formatMessageParams = (messageList: any[]) => {
const result: any[] = [];
messageList.forEach((item) => {
const { role, content, title, imgs, uid } = item;
content?.forEach((contentItem: any) => {
if (contentItem.type === 'text') {
result.push({
role,
content: contentItem.text
});
}
if (contentItem.type === 'image_url') {
result.push({
role,
content: [
{
...contentItem
}
]
});
}
});
});
return result;
};
export const generateMessagesByListContent = (messageList: any[]) => {
if (!messageList.length) return [];
return messageList.map((item: MessageItem) => {
const content = map(
item.imgs,
(img: { uid: string | number; dataUrl: string }) => {
return {
type: 'image_url',
image_url: {
url: img.dataUrl
}
};
}
);
if (item.content) {
content.push({
type: 'text',
text: item.content
});
}
return {
role: item.role,
content: content
};
});
};
export const generateMessages = (messageList: Omit<MessageItem, 'uid'>[]) => {
if (!messageList.length) return [];
const result: any[] = [];
messageList.forEach((item: Omit<MessageItem, 'uid'>) => {
if (item.imgs?.length) {
const imgList = item.imgs.map((img) => {
return {
type: 'image_url',
image_url: {
url: img.dataUrl
}
};
});
result.push({
role: item.role,
content: imgList
});
}
if (item.content) {
result.push({
role: item.role,
content: item.content
});
}
});
return result;
};
+1
View File
@@ -7,6 +7,7 @@ export interface ModelSelectionItem extends Global.BaseOption<string> {
export interface MessageItem { export interface MessageItem {
role: string; role: string;
content: string; content: string;
title?: string;
imgs?: { uid: string | number; dataUrl: string }[]; imgs?: { uid: string | number; dataUrl: string }[];
uid: number; uid: number;
} }
@@ -1,110 +0,0 @@
import { fetchChunkedData, readStreamData } from '@/utils/fetch-chunk-data';
import _ from 'lodash';
import { useRef, useState } from 'react';
import { CHAT_API } from '../apis';
import { Roles } from '../config';
interface MessageItemProps {
role: string | number;
content: string;
uid: number;
}
const useChatCompletion = () => {
const [loading, setLoading] = useState(false);
const messageId = useRef<number>(0);
const [messageList, setMessageList] = useState<MessageItemProps[]>([]);
const contentRef = useRef<any>('');
const controllerRef = useRef<any>(null);
const setMessageId = () => {
messageId.current = messageId.current + 1;
};
const abortFetch = () => {
controllerRef.current?.abort?.();
};
const joinMessage = (chunk: any) => {
if (!chunk) {
return;
}
if (_.get(chunk, 'choices.0.finish_reason')) {
return;
}
contentRef.current =
contentRef.current + _.get(chunk, 'choices.0.delta.content', '');
setMessageList([
...messageList,
{
role: Roles.Assistant,
content: contentRef.current,
uid: messageId.current
}
]);
};
const submitMessage = async (pramas: {
parameters: Record<string, any>;
systemMessage: string;
}) => {
const { parameters, systemMessage } = pramas;
if (!parameters.model) return;
try {
setLoading(true);
setMessageId();
console.log('messagelist=========2=', messageList);
controllerRef.current?.abort?.();
controllerRef.current = new AbortController();
const signal = controllerRef.current.signal;
const messages = _.map(messageList, (item: MessageItemProps) => {
return {
role: item.role,
content: item.content
};
});
contentRef.current = '';
const chatParams = {
messages: systemMessage
? [
{
role: Roles.System,
content: systemMessage
},
...messages
]
: [...messages],
...parameters,
stream: true
};
const result = await fetchChunkedData({
data: chatParams,
url: CHAT_API,
signal
});
if (!result) {
return;
}
const { reader, decoder } = result;
await readStreamData(reader, decoder, (chunk: any) => {
joinMessage(chunk);
});
setLoading(false);
} catch (error) {
console.log('error=====', error);
setLoading(false);
}
};
return {
loading,
messageList,
setMessageList,
submitMessage,
abortFetch
};
};
export default useChatCompletion;
+70 -5
View File
@@ -1,5 +1,7 @@
import IconFont from '@/components/icon-font'; import IconFont from '@/components/icon-font';
import breakpoints from '@/config/breakpoints';
import HotKeys from '@/config/hotkeys'; import HotKeys from '@/config/hotkeys';
import useWindowResize from '@/hooks/use-window-resize';
import { MessageOutlined, OneToOneOutlined } from '@ant-design/icons'; import { MessageOutlined, OneToOneOutlined } from '@ant-design/icons';
import { PageContainer } from '@ant-design/pro-components'; import { PageContainer } from '@ant-design/pro-components';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
@@ -10,14 +12,20 @@ import { useCallback, useEffect, useRef, useState } from 'react';
import { useHotkeys } from 'react-hotkeys-hook'; import { useHotkeys } from 'react-hotkeys-hook';
import { queryModelsList } from './apis'; import { queryModelsList } from './apis';
import GroundLeft from './components/ground-left'; import GroundLeft from './components/ground-left';
import GroundReranker from './components/ground-reranker';
import MultipleChat from './components/multiple-chat'; import MultipleChat from './components/multiple-chat';
import './style/play-ground.less'; import './style/play-ground.less';
const Playground: React.FC = () => { const Playground: React.FC = () => {
const intl = useIntl(); const intl = useIntl();
const { size } = useWindowResize();
const [activeKey, setActiveKey] = useState('chat'); const [activeKey, setActiveKey] = useState('chat');
const groundLeftRef = useRef<any>(null); const groundLeftRef = useRef<any>(null);
const groundRerankerRef = useRef<any>(null);
const [modelList, setModelList] = useState<Global.BaseOption<string>[]>([]); const [modelList, setModelList] = useState<Global.BaseOption<string>[]>([]);
const [rerankerModelList, setRerankerModelList] = useState<
Global.BaseOption<string>[]
>([]);
const [loaded, setLoaded] = useState(false); const [loaded, setLoaded] = useState(false);
const optionsList = [ const optionsList = [
{ {
@@ -30,6 +38,11 @@ const Playground: React.FC = () => {
value: 'compare', value: 'compare',
icon: <OneToOneOutlined /> icon: <OneToOneOutlined />
} }
// {
// label: 'Rerank',
// value: 'reranker',
// icon: <FileSearchOutlined />
// }
]; ];
const handleViewCode = useCallback(() => { const handleViewCode = useCallback(() => {
@@ -37,8 +50,12 @@ const Playground: React.FC = () => {
}, [groundLeftRef]); }, [groundLeftRef]);
const handleToggleCollapse = useCallback(() => { const handleToggleCollapse = useCallback(() => {
if (activeKey === 'reranker') {
groundRerankerRef.current?.setCollapse?.();
return;
}
groundLeftRef.current?.setCollapse?.(); groundLeftRef.current?.setCollapse?.();
}, [groundLeftRef]); }, [groundLeftRef, groundRerankerRef, activeKey]);
const items: TabsProps['items'] = [ const items: TabsProps['items'] = [
{ {
@@ -52,9 +69,28 @@ const Playground: React.FC = () => {
key: 'compare', key: 'compare',
label: 'Compare', label: 'Compare',
children: <MultipleChat modelList={modelList} loaded={loaded} /> children: <MultipleChat modelList={modelList} loaded={loaded} />
},
{
key: 'reranker',
label: 'Reranker',
children: (
<GroundReranker
ref={groundRerankerRef}
modelList={rerankerModelList}
loaded={loaded}
></GroundReranker>
)
} }
]; ];
useEffect(() => {
if (size.width < breakpoints.lg) {
if (!groundLeftRef.current?.collapse) {
groundLeftRef.current?.setCollapse?.();
}
}
}, [size.width]);
useEffect(() => { useEffect(() => {
const getModelList = async () => { const getModelList = async () => {
try { try {
@@ -68,14 +104,43 @@ const Playground: React.FC = () => {
label: item.id label: item.id
}; };
}) as Global.BaseOption<string>[]; }) as Global.BaseOption<string>[];
setModelList(list); return list;
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} finally { return [];
}
};
const getModelListByReranker = async () => {
try {
const params = {
reranker: true
};
const res = await queryModelsList(params);
const list = _.map(res.data || [], (item: any) => {
return {
value: item.id,
label: item.id
};
}) as Global.BaseOption<string>[];
return list;
} catch (error) {
console.error(error);
return [];
}
};
const fetchData = async () => {
try {
const [modelist, rerankerModelList] = await Promise.all([
getModelList(),
getModelListByReranker()
]);
setModelList(modelist);
setRerankerModelList(rerankerModelList);
} catch (error) {
setLoaded(true); setLoaded(true);
} }
}; };
getModelList(); fetchData();
}, []); }, []);
const renderExtra = () => { const renderExtra = () => {
@@ -136,7 +201,7 @@ const Playground: React.FC = () => {
extra={renderExtra()} extra={renderExtra()}
className={classNames('playground-container', { className={classNames('playground-container', {
compare: activeKey === 'compare', compare: activeKey === 'compare',
chat: activeKey === 'chat' chat: activeKey !== 'compare'
})} })}
> >
<div className="play-ground"> <div className="play-ground">
+1 -2
View File
@@ -12,7 +12,6 @@
.role { .role {
position: relative; position: relative;
cursor: pointer;
padding: 0 4px; padding: 0 4px;
border-radius: var(--border-radius-mini); border-radius: var(--border-radius-mini);
} }
@@ -32,7 +31,7 @@
&-content { &-content {
word-break: break-word; word-break: break-word;
padding: 8px; padding: 8px 14px;
min-height: 38px; min-height: 38px;
border-radius: var(--border-radius-mini); border-radius: var(--border-radius-mini);
background-color: var(--ant-color-fill-tertiary); background-color: var(--ant-color-fill-tertiary);
+38
View File
@@ -0,0 +1,38 @@
.file-list {
display: flex;
flex-direction: column;
gap: 8px;
.file-item {
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid var(--ant-color-split);
padding: 8px 14px;
padding-right: 6px;
cursor: pointer;
height: 41px;
transition: background-color 0.3s ease;
.delete-btn {
display: none;
}
&:hover {
background-color: var(--ant-color-fill-tertiary);
.delete-btn {
display: block;
}
}
&.ghost {
background-color: transparent;
}
.title {
display: flex;
align-items: center;
}
}
}
@@ -2,6 +2,7 @@
display: flex; display: flex;
.params-wrapper { .params-wrapper {
position: relative;
overflow-x: hidden; overflow-x: hidden;
width: 390px; width: 390px;
border-left: 1px solid var(--ant-color-split); border-left: 1px solid var(--ant-color-split);
@@ -10,6 +11,10 @@
padding-top: 16px; padding-top: 16px;
overflow-y: auto; overflow-y: auto;
&::-webkit-scrollbar {
width: 0;
}
.box { .box {
width: 390px; width: 390px;
padding-inline: var(--layout-content-inlinepadding); padding-inline: var(--layout-content-inlinepadding);
@@ -50,4 +55,11 @@
} }
} }
} }
.ant-upload-wrapper .ant-upload-drag {
background-color: var(--color-fill-sider);
display: flex;
flex-direction: column;
justify-content: center;
}
} }
@@ -0,0 +1,39 @@
.input-list {
display: flex;
flex-direction: column;
gap: 8px;
.input-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0;
cursor: pointer;
transition: background-color 0.3s ease;
border-bottom: 1px solid var(--ant-color-split);
&:hover {
background-color: var(--ant-color-fill-tertiary);
.btn-group {
display: flex;
gap: 8px;
}
}
&:focus-within {
background-color: transparent;
}
.input-wrap {
flex: 1;
}
.btn-group {
width: 60px;
margin-left: 8px;
display: flex;
gap: 8px;
}
}
}
@@ -4,6 +4,7 @@
border: 1px solid var(--ant-color-border); border: 1px solid var(--ant-color-border);
border-radius: var(--border-radius-base); border-radius: var(--border-radius-base);
height: 100%; height: 100%;
background-color: var(--color-white-1);
.header { .header {
padding-inline: 2px 16px; padding-inline: 2px 16px;
@@ -12,6 +13,12 @@
justify-content: space-between; justify-content: space-between;
height: 46px; height: 46px;
border-bottom: 1px solid var(--ant-color-border); border-bottom: 1px solid var(--ant-color-border);
width: 100%;
.title {
max-width: min(100%, 180px);
min-width: min(120px, 32%);
}
} }
.action { .action {
@@ -6,7 +6,16 @@
color: var(--ant-orange); color: var(--ant-orange);
gap: 20px; gap: 20px;
&.scaleable {
gap: 15px;
}
.usage { .usage {
cursor: pointer; cursor: pointer;
line-height: 16px;
&.scaleable {
transform: scale(0.9);
}
} }
} }
@@ -0,0 +1,45 @@
.rerank-message {
display: flex;
flex-direction: column;
.rank {
margin-right: 10px;
max-width: 75px;
min-width: 55px;
display: flex;
align-items: center;
justify-content: space-between;
.score {
margin-left: 5px;
color: var(--ant-rate-star-color);
}
}
.result {
display: flex;
flex-direction: column;
gap: 10px;
}
.content-item-text {
margin-bottom: 0;
display: flex;
align-items: center;
}
.text {
flex: 1;
width: 300px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-bottom: 0;
}
.doc-name {
margin-bottom: 0;
font-weight: var(--font-weight-bold);
font-style: italic;
}
}
@@ -1,8 +1,6 @@
.thumb-img { .thumb-img {
position: relative; position: relative;
display: flex; display: flex;
width: 56px;
height: 56px;
.img { .img {
display: flex; display: flex;
+39 -5
View File
@@ -1,3 +1,4 @@
import AutoTooltip from '@/components/auto-tooltip';
import PageTools from '@/components/page-tools'; import PageTools from '@/components/page-tools';
import ProgressBar from '@/components/progress-bar'; import ProgressBar from '@/components/progress-bar';
import useTableSort from '@/hooks/use-table-sort'; import useTableSort from '@/hooks/use-table-sort';
@@ -127,6 +128,13 @@ const GPUList: React.FC = () => {
title={intl.formatMessage({ id: 'common.table.name' })} title={intl.formatMessage({ id: 'common.table.name' })}
dataIndex="name" dataIndex="name"
key="name" key="name"
render={(text, record) => {
return (
<AutoTooltip ghost style={{ width: '100%' }}>
{text}
</AutoTooltip>
);
}}
/> />
<Column <Column
title={intl.formatMessage({ id: 'resources.table.index' })} title={intl.formatMessage({ id: 'resources.table.index' })}
@@ -140,6 +148,16 @@ const GPUList: React.FC = () => {
title={intl.formatMessage({ id: 'resources.table.workername' })} title={intl.formatMessage({ id: 'resources.table.workername' })}
dataIndex="worker_name" dataIndex="worker_name"
key="worker_name" key="worker_name"
width={200}
render={(text, record: GPUDeviceItem) => {
return (
<span style={{ display: 'flex', width: '100%' }}>
<AutoTooltip ghost maxWidth={340}>
{text}
</AutoTooltip>
</span>
);
}}
/> />
<Column <Column
title={intl.formatMessage({ id: 'resources.table.vender' })} title={intl.formatMessage({ id: 'resources.table.vender' })}
@@ -155,23 +173,29 @@ const GPUList: React.FC = () => {
return <span>{text ? _.round(text, 1) : '-'}</span>; return <span>{text ? _.round(text, 1) : '-'}</span>;
}} }}
/> />
<Column {/* <Column
title={intl.formatMessage({ id: 'resources.table.core' })} title={intl.formatMessage({ id: 'resources.table.core' })}
dataIndex="core" dataIndex="core"
key="Core" key="Core"
render={(text, record: GPUDeviceItem) => { render={(text, record: GPUDeviceItem) => {
return <span>{record.core?.total}</span>; return <>{record.core ? <span>{record.core?.total}</span> : '-'}</>;
}} }}
/> /> */}
<Column <Column
title={intl.formatMessage({ id: 'resources.table.gpuutilization' })} title={intl.formatMessage({ id: 'resources.table.gpuutilization' })}
dataIndex="gpuUtil" dataIndex="gpuUtil"
key="gpuUtil" key="gpuUtil"
render={(text, record: GPUDeviceItem) => { render={(text, record: GPUDeviceItem) => {
return ( return (
<>
{record.core ? (
<ProgressBar <ProgressBar
percent={_.round(record.core?.utilization_rate, 2)} percent={_.round(record.core?.utilization_rate, 2)}
></ProgressBar> ></ProgressBar>
) : (
'-'
)}
</>
); );
}} }}
/> />
@@ -183,7 +207,14 @@ const GPUList: React.FC = () => {
render={(text, record: GPUDeviceItem) => { render={(text, record: GPUDeviceItem) => {
return ( return (
<ProgressBar <ProgressBar
percent={_.round(record.memory?.utilization_rate, 0)} percent={
record.memory?.used
? _.round(record.memory?.utilization_rate, 0)
: _.round(
record.memory?.allocated / record.memory?.total,
0
)
}
label={ label={
<span className="flex-column"> <span className="flex-column">
<span> <span>
@@ -192,7 +223,10 @@ const GPUList: React.FC = () => {
</span> </span>
<span> <span>
{intl.formatMessage({ id: 'resources.table.used' })}:{' '} {intl.formatMessage({ id: 'resources.table.used' })}:{' '}
{convertFileSize(record.memory?.used, 0)} {convertFileSize(
record.memory?.used || record.memory?.allocated,
0
)}
</span> </span>
</span> </span>
} }
+27 -2
View File
@@ -307,6 +307,14 @@ const Resources: React.FC = () => {
title={intl.formatMessage({ id: 'common.table.name' })} title={intl.formatMessage({ id: 'common.table.name' })}
dataIndex="name" dataIndex="name"
key="name" key="name"
width={100}
render={(text, record: ListItem) => {
return (
<AutoTooltip ghost maxWidth={240}>
<span>{record.name}</span>
</AutoTooltip>
);
}}
/> />
<Column <Column
title={intl.formatMessage({ id: 'resources.table.labels' })} title={intl.formatMessage({ id: 'resources.table.labels' })}
@@ -408,10 +416,14 @@ const Resources: React.FC = () => {
> >
[{item.index}] [{item.index}]
</span> </span>
{item.core ? (
<ProgressBar <ProgressBar
key={index} key={index}
percent={_.round(item.core?.utilization_rate, 0)} percent={_.round(item.core?.utilization_rate, 0)}
></ProgressBar> ></ProgressBar>
) : (
'-'
)}
</span> </span>
); );
} }
@@ -442,7 +454,16 @@ const Resources: React.FC = () => {
</span> </span>
<ProgressBar <ProgressBar
key={index} key={index}
percent={_.round(item.memory?.utilization_rate, 0)} percent={
item.memory?.used
? _.round(item.memory?.utilization_rate, 0)
: _.round(
(item.memory?.allocated /
item.memory?.total) *
100,
0
)
}
label={ label={
<span className="flex-column"> <span className="flex-column">
<span> <span>
@@ -455,7 +476,11 @@ const Resources: React.FC = () => {
{intl.formatMessage({ {intl.formatMessage({
id: 'resources.table.used' id: 'resources.table.used'
})} })}
: {convertFileSize(item.memory?.used, 0)} :{' '}
{convertFileSize(
item.memory?.used || item.memory?.allocated,
0
)}
</span> </span>
</span> </span>
} }

Some files were not shown because too many files have changed in this diff Show More