Compare commits

..
31 Commits
Author SHA1 Message Date
jialin f9cf111a86 chore: vllm docs link 2025-08-20 17:21:01 +08:00
jialin 99f8914830 chore: update vllm arguments 2025-08-20 16:43:48 +08:00
jialin 6da877709d style: display title when sso loading 2025-08-20 16:43:48 +08:00
jialin c4948dc11f fix: sso message 2025-08-19 11:08:15 +08:00
jialin 3f243129a4 fix: do not extract the error message from chat chunk 2025-08-18 18:22:46 +08:00
jialin eb2cae7a77 fix: break early while reading stream data 2025-08-15 19:12:12 +08:00
jialin a2bbcd3939 style: side menu 2025-08-15 15:37:51 +08:00
jialin 0650c4bc26 chore: some sso adjustments 2025-08-15 10:26:24 +08:00
jialin 7c32bf97ae chore: auth-config api 2025-08-14 21:50:59 +08:00
jialin 70aecc1f0c fix: keep storing avatar url when edit external user 2025-08-14 20:28:21 +08:00
jialin 5bd5c7d2c5 feat: add user avatar 2025-08-14 16:39:25 +08:00
jialin d6dce4021d fix: filter non-normal gguf files 2025-08-14 11:03:55 +08:00
jialin ccfdb99348 fix: miss global user info 2025-08-13 20:13:46 +08:00
jialin 2ca712435a fix: markdown view \n do not work 2025-08-13 19:50:54 +08:00
jialin 0af8e34a8f fix: external redirect issue 2025-08-13 19:50:54 +08:00
jialin f2583e152a style: login form style 2025-08-13 16:25:26 +08:00
jialin cf70bd2dc1 refactor: login form based on sso auth 2025-08-13 12:15:16 +08:00
forrestzhouandjialin 8f50af917f feat: Change get_config to auth_config to make it more specific 2025-08-12 17:19:04 +08:00
forrestzhouandjialin 4bdde365cf add sso login logic 2025-08-12 17:19:04 +08:00
forrestzhouandjialin 7f1bc33eed first push 0.7 2025-08-12 17:19:04 +08:00
jialin d0d24aba4e fix: markdown viewer white-space: pre-line 2025-08-12 16:45:50 +08:00
EVGENY Mandjialin 0d68c0d683 Update menu.ts 2025-08-12 00:15:14 +08:00
jialin e1ab735823 perf: toggle theme too much render in catalog 2025-08-11 09:35:01 +08:00
jialin bb67a8cbbb chore: display downloading progress in model instance log 2025-08-08 15:51:09 +08:00
jialin c8a7ac7dc6 style: keep text wrap for evalution result 2025-08-05 15:57:35 +08:00
EVGENY Mandjialin 788a294200 Update menu.ts 2025-07-31 09:38:00 +08:00
EVGENY Mandjialin f34935a634 Update models.ts 2025-07-31 09:36:50 +08:00
EVGENY Mandjialin a7331256fc Update dashboard.ts 2025-07-31 09:36:21 +08:00
jialin 8fa3941db9 fix: do not evaluate when local path is null 2025-07-30 11:12:13 +08:00
jialin ada120c1b9 fix: the theme align with the system 2025-07-30 11:12:13 +08:00
jialin 72f7168893 fix: adjust precision for big size 2025-07-29 18:40:29 +08:00
65 changed files with 1143 additions and 423 deletions
+3 -3
View File
@@ -100,9 +100,9 @@ export default [
name: 'deployment',
path: '/models/deployments',
key: 'modelDeployments',
icon: 'icon-model',
selectedIcon: 'icon-model-filled',
defaultIcon: 'icon-model',
icon: 'icon-rocket-launch1',
selectedIcon: 'icon-rocket-launch-fill',
defaultIcon: 'icon-rocket-launch1',
access: 'canSeeAdmin',
component: './llmodels/index'
}
+1
View File
@@ -68,6 +68,7 @@
"react-hotkeys-hook": "^4.5.0",
"react-markdown": "^9.0.3",
"rehype-katex": "^7.0.1",
"remark-breaks": "^4.0.0",
"remark-gfm": "^4.0.1",
"remark-math": "^6.0.0",
"simplebar-react": "^3.2.6",
+18
View File
@@ -165,6 +165,9 @@ dependencies:
rehype-katex:
specifier: ^7.0.1
version: 7.0.1
remark-breaks:
specifier: ^4.0.0
version: 4.0.0
remark-gfm:
specifier: ^4.0.1
version: 4.0.1
@@ -13121,6 +13124,13 @@ packages:
- supports-color
dev: false
/mdast-util-newline-to-break@2.0.0:
resolution: {integrity: sha512-MbgeFca0hLYIEx/2zGsszCSEJJ1JSCdiY5xQxRcLDDGa8EPvlLPupJ4DSajbMPAnC0je8jfb9TiUATnxxrHUog==, tarball: https://registry.npmjs.org/mdast-util-newline-to-break/-/mdast-util-newline-to-break-2.0.0.tgz}
dependencies:
'@types/mdast': 4.0.4
mdast-util-find-and-replace: 3.0.2
dev: false
/mdast-util-phrasing@4.1.0:
resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==, tarball: https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz}
dependencies:
@@ -17456,6 +17466,14 @@ packages:
engines: {node: '>= 0.10'}
dev: false
/remark-breaks@4.0.0:
resolution: {integrity: sha512-IjEjJOkH4FuJvHZVIW0QCDWxcG96kCq7An/KVH2NfJe6rKZU2AsHeB3OEjPNRxi4QC34Xdx7I2KGYn6IpT7gxQ==, tarball: https://registry.npmjs.org/remark-breaks/-/remark-breaks-4.0.0.tgz}
dependencies:
'@types/mdast': 4.0.4
mdast-util-newline-to-break: 2.0.0
unified: 11.0.5
dev: false
/remark-gfm@4.0.1:
resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==, tarball: https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz}
dependencies:
+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1755020187776" class="icon" viewBox="0 0 1105 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7392" xmlns:xlink="http://www.w3.org/1999/xlink" width="138.125" height="128"><path d="M507.2896 133.44768l130.98496-71.4496v820.9408l-130.97984 69.71392V133.44768z" fill="#F38019" p-id="7393"></path><path d="M479.39072 323.968a724.30592 724.30592 0 0 0-77.056 16.6144C118.35904 417.01376 37.77536 603.8016 67.95776 701.696c30.11584 97.8944 161.03424 263.2192 439.2704 250.19904 0.13824-27.41248 0.13824-57.73824 0-90.89536-200.49408-26.37824-302.67904-88.82688-306.55488-187.40736-5.74464-147.74272 134.1696-216.90368 217.87136-229.0176 16.82432-2.49344 37.66272-5.67808 61.19936-8.6528l-0.27648-111.9488h-0.07168z m185.12384 107.65312c52.1984 5.6832 103.57248 18.69312 147.0464 44.44672-10.65984 5.19168-34.26816 19.73248-70.82496 43.47904l290.49344 66.39104-16.54272-206.58176-78.37184 44.30336c-116.10112-57.8048-205.19936-91.0336-267.29984-99.6864l-4.50048 107.648z" fill="#ADAFB3" p-id="7394"></path></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M140-180v-88.92q0-29.39 15.96-54.43 15.96-25.03 42.66-38.49 59.3-29.08 119.65-43.62Q378.62-420 440-420q21.06 0 42.12 1.89 21.05 1.88 42.11 5.65-1.69 51.46 20.81 96.81Q567.54-270.31 609-240v60H140ZM753.85-72.31l-53.35-53.14v-164.73q-39.11-11.51-63.85-43.97-24.73-32.46-24.73-74.7 0-51.46 36.38-87.84t87.85-36.38q51.46 0 87.66 36.39Q860-460.28 860-408.79q0 39.94-22.42 70.71-22.43 30.77-57.2 44.23l44.23 44.23-53.07 53.2 53.07 53.19-70.76 70.92ZM440-484.62q-57.75 0-98.87-41.12Q300-566.86 300-624.61q0-57.75 41.13-98.88 41.12-41.12 98.87-41.12 57.75 0 98.87 41.12Q580-682.36 580-624.61q0 57.75-41.13 98.87-41.12 41.12-98.87 41.12Zm296.15 93.47q14.7 0 25.04-10.54 10.35-10.54 10.35-25.23 0-14.69-10.35-25.04-10.34-10.35-25.04-10.35-14.69 0-25.23 10.35-10.54 10.35-10.54 25.04t10.54 25.23q10.54 10.54 25.23 10.54Z"/></svg>

After

Width:  |  Height:  |  Size: 934 B

+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="none">
<g fill="#C22E33">
<path d="M7.754 2l.463.41c.343.304.687.607 1.026.915C11.44 5.32 13.3 7.565 14.7 10.149c.072.132.137.268.202.403l.098.203-.108.057-.081-.115-.21-.299-.147-.214c-1.019-1.479-2.04-2.96-3.442-4.145a6.563 6.563 0 00-1.393-.904c-1.014-.485-1.916-.291-2.69.505-.736.757-1.118 1.697-1.463 2.653-.045.123-.092.245-.139.367l-.082.215-.172-.055c.1-.348.192-.698.284-1.049.21-.795.42-1.59.712-2.356.31-.816.702-1.603 1.093-2.39.169-.341.338-.682.5-1.025h.092z"/>
<path d="M8.448 11.822c-1.626.77-5.56 1.564-7.426 1.36C.717 11.576 3.71 4.05 5.18 2.91l-.095.218a4.638 4.638 0 01-.138.303l-.066.129c-.76 1.462-1.519 2.926-1.908 4.53a7.482 7.482 0 00-.228 1.689c-.01 1.34.824 2.252 2.217 2.309.67.027 1.347-.043 2.023-.114.294-.03.587-.061.88-.084.108-.008.214-.021.352-.039l.231-.028z"/>
<path d="M3.825 14.781c-.445.034-.89.068-1.333.108 4.097.39 8.03-.277 11.91-1.644-1.265-2.23-2.97-3.991-4.952-5.522.026.098.084.169.141.239l.048.06c.17.226.348.448.527.67.409.509.818 1.018 1.126 1.578.778 1.42.356 2.648-1.168 3.296-1.002.427-2.097.718-3.18.892-1.03.164-2.075.243-3.119.323z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

+3 -4
View File
@@ -33,10 +33,9 @@ export const getStorageUserSettings = () => {
}
};
export const userSettingsAtom = atomWithStorage<UserSettings>(
'userSettings',
defaultSettings
);
export const userSettingsAtom = atomWithStorage<UserSettings>('userSettings', {
...getStorageUserSettings()
});
export const userSettingsHelperAtom = atom(
(get) => get(userSettingsAtom),
-72
View File
@@ -1,72 +0,0 @@
.alert-info-block {
padding-block: 6px;
padding-inline: 10px 16px;
position: relative;
padding-left: 32px;
text-align: left;
border-radius: var(--border-radius-base);
margin: 0;
border: 1px solid transparent;
.ant-typography {
margin-bottom: 0;
}
&.danger {
border-color: var(--ant-color-error-border);
background-color: var(--ant-color-error-bg);
}
&.warning {
border-color: var(--ant-color-warning-border);
background-color: var(--ant-color-warning-bg);
}
&.transition {
color: var(--ant-geekblue-7);
background: var(--ant-geekblue-1);
border-color: var(--ant-geekblue-3);
}
&.success {
border: 1px solid var(--ant-color-success);
color: var(--ant-color-success);
background: var(--ant-color-success-bg);
.content.success {
font-weight: var(--font-weight-normal);
}
}
.title {
position: absolute;
left: 0;
top: 0;
display: flex;
height: 32px;
padding: 5px 10px;
border-radius: var(--border-radius-base) var(--border-radius-base) 0 0;
.info-icon {
&.danger {
color: var(--ant-color-error);
}
&.warning {
color: var(--ant-color-warning);
}
&.transition {
color: var(--ant-geekblue-7);
}
&.success {
color: var(--ant-color-success);
}
}
.text {
font-weight: var(--font-weight-bold);
}
}
}
+78 -3
View File
@@ -1,10 +1,10 @@
import { WarningFilled } from '@ant-design/icons';
import { Typography } from 'antd';
import { createStyles } from 'antd-style';
import classNames from 'classnames';
import React from 'react';
import styled from 'styled-components';
import OverlayScroller, { OverlayScrollerOptions } from '../overlay-scroller';
import './block.less';
interface AlertInfoProps {
type: Global.MessageType;
message: React.ReactNode;
@@ -18,6 +18,81 @@ interface AlertInfoProps {
overlayScrollerProps?: OverlayScrollerOptions;
}
const useStyles = createStyles(({ token, css }) => {
return {
alertBlockInfo: css`
padding-block: 6px;
padding-inline: 10px 16px;
position: relative;
padding-left: 32px;
text-align: left;
border-radius: ${token.borderRadius}px;
margin: 0;
border: 1px solid transparent;
.ant-typography {
margin-bottom: 0;
}
&.danger {
border-color: ${token.colorErrorBorder};
background-color: ${token.colorErrorBg};
}
&.warning {
border-color: ${token.colorWarningBorder};
background-color: ${token.colorWarningBg};
}
&.transition {
color: ${token.geekblue7};
background: ${token.geekblue1};
border-color: ${token.geekblue3};
}
&.success {
border: 1px solid ${token.colorSuccess};
color: ${token.colorSuccessText};
background: ${token.colorSuccessBg};
.content.success {
font-weight: var(--font-weight-normal);
}
}
.title {
position: absolute;
left: 0;
top: 0;
display: flex;
height: 32px;
padding: 5px 10px;
border-radius: ${token.borderRadius}px ${token.borderRadius}px 0 0;
.info-icon {
&.danger {
color: ${token.colorErrorText};
}
&.warning {
color: ${token.colorWarningText};
}
&.transition {
color: ${token.geekblue7};
}
&.success {
color: ${token.colorSuccessText};
}
}
.text {
font-weight: var(--font-weight-bold);
}
}
`
};
});
const TitleWrapper = styled.div`
font-weight: 700;
color: var(--ant-color-text);
@@ -46,12 +121,12 @@ const AlertInfo: React.FC<AlertInfoProps> = (props) => {
maxHeight = 86,
overlayScrollerProps = {}
} = props;
const { styles } = useStyles();
return (
<>
{message ? (
<div
className={classNames('alert-info-block', type)}
className={classNames(styles.alertBlockInfo, type)}
style={{ ...style }}
>
<Typography.Paragraph
+3 -3
View File
@@ -15,7 +15,7 @@ const fadeOut = keyframes`
}
`;
const Mask = styled.div<{ isHiding: boolean }>`
const Mask = styled.div<{ $isHiding: boolean }>`
display: flex;
justify-content: center;
align-items: center;
@@ -26,7 +26,7 @@ const Mask = styled.div<{ isHiding: boolean }>`
height: 100vh;
background-color: rgba(59, 59, 59, 1);
z-index: 9999;
animation: ${({ isHiding }) => (isHiding ? fadeOut : 'none')} ${duration}s
animation: ${({ $isHiding }) => ($isHiding ? fadeOut : 'none')} ${duration}s
ease forwards;
`;
@@ -79,7 +79,7 @@ const DarkModeMask = () => {
return () => el.removeEventListener('animationend', handleAnimationEnd);
}, [visible]);
return visible ? <Mask ref={maskRef} isHiding={isHiding}></Mask> : null;
return visible ? <Mask ref={maskRef} $isHiding={isHiding}></Mask> : null;
};
export default DarkModeMask;
+7 -1
View File
@@ -129,9 +129,15 @@ const BarChart: React.FC<ChartProps & { maxItems?: number }> = (props) => {
legend
]);
const isEmpty = useMemo(() => {
return seriesData?.every?.((item: any) => {
return !item?.data?.length;
});
}, [seriesData]);
return (
<>
{!seriesData.length ? (
{isEmpty ? (
<EmptyData height={height} title={title}></EmptyData>
) : (
<Chart
+1 -1
View File
@@ -56,4 +56,4 @@ const HighlightCode: React.FC<{
);
};
export default React.memo(HighlightCode);
export default HighlightCode;
+47 -3
View File
@@ -1,8 +1,8 @@
@font-face {
font-family: iconfont; /* Project id 4613488 */
src: url('iconfont.woff2?t=1752112673765') format('woff2'),
url('iconfont.woff?t=1752112673765') format('woff'),
url('iconfont.ttf?t=1752112673765') format('truetype');
src: url('iconfont.woff2?t=1755160439217') format('woff2'),
url('iconfont.woff?t=1755160439217') format('woff'),
url('iconfont.ttf?t=1755160439217') format('truetype');
}
.iconfont {
@@ -13,6 +13,50 @@
-moz-osx-font-smoothing: grayscale;
}
.icon-admin-user::before {
content: "\e69d";
}
.icon-user::before {
content: "\e69e";
}
.icon-quick_reference_24dp_1F1F1F_FILL0_wght300_GRAD0_op::before {
content: "\e69b";
}
.icon-docker::before {
content: "\e69c";
}
.icon-digitalocean::before {
content: "\eb79";
}
.icon-rocket-launch1::before {
content: "\e699";
}
.icon-rocket-launch-fill::before {
content: "\e69a";
}
.icon-credential-filled::before {
content: "\e697";
}
.icon-credential-outline::before {
content: "\e698";
}
.icon-k8s-filled::before {
content: "\e63e";
}
.icon-k8s-outline::before {
content: "\e64b";
}
.icon-huggingface1::before {
content: "\e605";
}
File diff suppressed because one or more lines are too long
@@ -5,6 +5,83 @@
"css_prefix_text": "icon-",
"description": "",
"glyphs": [
{
"icon_id": "45240470",
"name": "admin-user",
"font_class": "admin-user",
"unicode": "e69d",
"unicode_decimal": 59037
},
{
"icon_id": "45240469",
"name": "user",
"font_class": "user",
"unicode": "e69e",
"unicode_decimal": 59038
},
{
"icon_id": "45221182",
"name": "quick_reference_24dp_1F1F1F_FILL0_wght300_GRAD0_op",
"font_class": "quick_reference_24dp_1F1F1F_FILL0_wght300_GRAD0_op",
"unicode": "e69b",
"unicode_decimal": 59035
},
{
"icon_id": "45102077",
"name": "Docker",
"font_class": "docker",
"unicode": "e69c",
"unicode_decimal": 59036
},
{
"icon_id": "15378290",
"name": "digitalocean",
"font_class": "digitalocean",
"unicode": "eb79",
"unicode_decimal": 60281
},
{
"icon_id": "45040302",
"name": "rocket-launch",
"font_class": "rocket-launch1",
"unicode": "e699",
"unicode_decimal": 59033
},
{
"icon_id": "45040301",
"name": "rocket-launch-fill",
"font_class": "rocket-launch-fill",
"unicode": "e69a",
"unicode_decimal": 59034
},
{
"icon_id": "45028316",
"name": "credential",
"font_class": "credential-filled",
"unicode": "e697",
"unicode_decimal": 59031
},
{
"icon_id": "45028315",
"name": "credential",
"font_class": "credential-outline",
"unicode": "e698",
"unicode_decimal": 59032
},
{
"icon_id": "37601405",
"name": "k8s",
"font_class": "k8s-filled",
"unicode": "e63e",
"unicode_decimal": 58942
},
{
"icon_id": "41673174",
"name": "k8s",
"font_class": "k8s-outline",
"unicode": "e64b",
"unicode_decimal": 58955
},
{
"icon_id": "40865292",
"name": "huggingface",
Binary file not shown.
Binary file not shown.
Binary file not shown.
+32
View File
@@ -0,0 +1,32 @@
import IconFont from '@/components/icon-font';
import {
ApiOutlined,
DeleteOutlined,
DownloadOutlined,
EditOutlined,
ExperimentOutlined,
RetweetOutlined,
ThunderboltOutlined
} from '@ant-design/icons';
import React from 'react';
const icons = {
EditOutlined: React.createElement(EditOutlined),
ExperimentOutlined: React.createElement(ExperimentOutlined),
DeleteOutlined: React.createElement(DeleteOutlined),
ThunderboltOutlined: React.createElement(ThunderboltOutlined),
RetweetOutlined: React.createElement(RetweetOutlined),
DownloadOutlined: React.createElement(DownloadOutlined),
ApiOutlined: React.createElement(ApiOutlined),
Stop: React.createElement(IconFont, { type: 'icon-stop1' }),
Play: React.createElement(IconFont, { type: 'icon-outline-play' }),
Catalog: React.createElement(IconFont, { type: 'icon-catalog' }),
HF: React.createElement(IconFont, { type: 'icon-huggingface' }),
Ollama: React.createElement(IconFont, { type: 'icon-ollama' }),
ModelScope: React.createElement(IconFont, { type: 'icon-tu2' }),
LocalPath: React.createElement(IconFont, { type: 'icon-hard-disk' }),
Launch: React.createElement(IconFont, { type: 'icon-rocket-launch' }),
Deployment: React.createElement(IconFont, { type: 'icon-rocket-launch1' })
};
export default icons;
+19 -1
View File
@@ -2,9 +2,27 @@ import langConfigMap from '@/locales/lang-config-map';
import { GlobalOutlined } from '@ant-design/icons';
import { getAllLocales, setLocale } from '@umijs/max';
import { Dropdown } from 'antd';
import { createStyles } from 'antd-style';
import { get } from 'lodash';
const useStyles = createStyles(({ token, css }) => ({
button: css`
color: ${token.colorText};
padding: 0 12px;
cursor: pointer;
.anticon {
color: ${token.colorText};
}
&:hover {
.anticon {
color: ${token.colorTextTertiary};
}
}
`
}));
const LangSelect = () => {
const { styles } = useStyles();
const allLocals = getAllLocales();
const items = allLocals.map((key) => {
return {
@@ -25,7 +43,7 @@ const LangSelect = () => {
return (
<Dropdown menu={{ items }}>
<span style={{ padding: '0 12px', cursor: 'pointer' }}>
<span className={styles.button}>
<GlobalOutlined />
</span>
</Dropdown>
+32 -9
View File
@@ -16,6 +16,7 @@ interface MessageProps {
chunked?: boolean;
progress?: number;
percent?: number;
isDownloading?: boolean;
}
class AnsiParser {
private cursorRow: number = 0;
@@ -32,6 +33,7 @@ class AnsiParser {
private chunked: boolean = true; // true: send data in chunks, false: send all data at once
private reminder: string = '';
private lines: string[] = [];
isDownloading: boolean = false;
private pageSize: number = 500;
private colorMap = {
'30': 'black',
@@ -79,6 +81,10 @@ class AnsiParser {
this.chunked = chunked ?? true;
}
public setIsDownloading(isDownloading: boolean) {
this.isDownloading = isDownloading;
}
private setId() {
this.uid += 1;
return this.uid;
@@ -168,14 +174,18 @@ class AnsiParser {
const remainingText = input.slice(lastIndex);
this.handleText(remainingText);
const result = this.screen.map((row, index) => ({
content: removeBracketsFromLine(row.join('')),
uid: `${this.page}-${index}`
}));
// const result = this.screen.map((row, index) => ({
// content: removeBracketsFromLine(row.join('')),
// uid: `${this.page}-${index}`
// }));
const result = this.screen.map((row, index) =>
removeBracketsFromLine(row.join(''))
);
return {
data: result,
lines: this.rawDataRows
lines: this.rawDataRows,
remainder: ''
};
}
@@ -219,12 +229,23 @@ class AnsiParser {
this.isProcessing = true;
while (this.taskQueue.length > 0) {
const input = this.reminder + this.taskQueue.shift();
let input = '';
if (this.isDownloading) {
input = this.taskQueue.shift() || '';
} else {
input = this.reminder + this.taskQueue.shift();
}
if (input) {
try {
const result = this.processInputByLine(input);
this.reminder = result.remainder;
const result = this.isDownloading
? this.processInput(input)
: this.processInputByLine(input);
if (!this.isDownloading) {
this.reminder = result.remainder;
}
if (this.chunked) {
self.postMessage({ result: result.data, lines: result.lines });
} else if (!this.isComplete) {
@@ -273,9 +294,11 @@ self.onmessage = function (event: MessageEvent<MessageProps>) {
page,
isComplete = false,
chunked = true,
percent = 0
percent = 0,
isDownloading = false
} = event.data;
parser.setIsDownloading(isDownloading);
parser.setPage(page);
parser.setIsCompelete(isComplete);
parser.setChunked(chunked);
@@ -4,7 +4,6 @@ import classNames from 'classnames';
import _ from 'lodash';
import React, {
forwardRef,
memo,
useCallback,
useEffect,
useImperativeHandle,
@@ -25,10 +24,17 @@ interface LogsViewerProps {
tail?: number;
enableScorllLoad?: boolean;
diffHeight?: number;
isDownloading?: boolean;
}
const LogsViewer: React.FC<LogsViewerProps> = forwardRef((props, ref) => {
const { diffHeight, url, tail: defaultTail, enableScorllLoad = true } = props;
const {
diffHeight,
url,
tail: defaultTail,
enableScorllLoad = true,
isDownloading
} = props;
const { pageSize, page, setPage, setTotalPage, totalPage } =
useLogsPagination();
const { setChunkFetch } = useSetChunkFetch();
@@ -64,9 +70,10 @@ const LogsViewer: React.FC<LogsViewerProps> = forwardRef((props, ref) => {
};
const setCurrentData = (lines: string[]) => {
console.log('setCurrentData', lines);
const dataList = lines.map((line, index) => {
return {
content: removeBracketsFromLine(line),
content: line,
uid: `${pageRef.current}-${index}`
};
});
@@ -150,7 +157,8 @@ const LogsViewer: React.FC<LogsViewerProps> = forwardRef((props, ref) => {
logParseWorker.current.postMessage({
inputStr: data,
page: pageRef.current,
reset: clearScreen.current
reset: clearScreen.current,
isDownloading: isDownloading
});
clearScreen.current = false;
};
@@ -247,7 +255,7 @@ const LogsViewer: React.FC<LogsViewerProps> = forwardRef((props, ref) => {
return () => {
chunkRequedtRef.current?.current?.abort?.();
};
}, [url]);
}, [url, isDownloading]);
useEffect(() => {
debouncedScroll();
@@ -349,4 +357,4 @@ const LogsViewer: React.FC<LogsViewerProps> = forwardRef((props, ref) => {
);
});
export default memo(LogsViewer);
export default LogsViewer;
@@ -3,6 +3,7 @@ import 'katex/dist/katex.min.css';
import React, { useMemo } from 'react';
import ReactMarkdown from 'react-markdown';
import rehypeKatex from 'rehype-katex';
import remarkBreaks from 'remark-breaks';
import remarkGfm from 'remark-gfm';
import remarkMath from 'remark-math';
import styled from 'styled-components';
@@ -17,9 +18,7 @@ interface FullMarkdownProps {
const Wrapper = styled.div.attrs(() => ({
className: 'markdown-viewer'
}))`
line-height: 2;
`;
}))``;
const CodeViewer = (props: any) => {
const { children, className, node, theme, ...rest } = props;
@@ -41,7 +40,7 @@ const FullMarkdown: React.FC<FullMarkdownProps> = ({ content, theme }) => {
return (
<Wrapper>
<ReactMarkdown
remarkPlugins={[[remarkMath], remarkGfm]}
remarkPlugins={[[remarkMath], remarkGfm, remarkBreaks]}
rehypePlugins={[rehypeKatex]}
components={{
code(props) {
+1 -2
View File
@@ -1,5 +1,6 @@
.markdown-viewer {
word-break: break-word;
line-height: 2;
font-size: var(--font-size-base);
.hr {
@@ -60,13 +61,11 @@
ul {
margin-bottom: 0;
padding-left: 20px;
line-height: 2;
}
ol {
margin-bottom: 0;
padding-left: 20px;
line-height: 2;
}
a * {
+26
View File
@@ -0,0 +1,26 @@
import { CloseOutlined } from '@ant-design/icons';
import { createStyles } from 'antd-style/lib/functions';
const useStyles = createStyles(({ token, css }) => ({
errorIcon: css`
display: flex;
align-items: center;
justify-content: center;
width: 64px;
height: 64px;
border-radius: 50%;
background-color: ${token.colorErrorBgActive};
color: ${token.colorError};
`
}));
const ErrorIcon: React.FC<{ size?: number }> = ({ size }) => {
const { styles } = useStyles();
return (
<div className={styles.errorIcon}>
<CloseOutlined style={{ fontSize: size || 30 }} />
</div>
);
};
export default ErrorIcon;
@@ -15,9 +15,13 @@ const useStyles = createStyles(({ token, css }) => ({
padding: 0 8px;
&:hover {
color: ${token.colorTextTertiary};
.anticon {
color: ${token.colorTextTertiary};
}
}
.anticon {
font-size: 16px;
color: ${token.colorText};
}
.icon-auto {
font-size: 20px;
+2
View File
@@ -20,6 +20,8 @@ declare namespace Global {
full_name: string;
require_password_change: boolean;
id: number;
source: string;
avatar_url?: string;
}
interface BaseListItem<T> {
+1 -2
View File
@@ -570,7 +570,6 @@ body {
.ant-message-notice-content {
border-radius: var(--border-radius-base) !important;
line-height: 2;
max-width: 300px;
text-align: left;
word-break: break-word;
@@ -579,7 +578,7 @@ body {
overflow-y: auto;
.anticon {
padding-top: 5px;
padding-top: 3px;
}
}
+17 -5
View File
@@ -9,6 +9,8 @@ export default function useUserSettings() {
const { light, dark, colorPrimary } = themeConfig;
const [userSettings, setUserSettings] = useAtom(userSettingsHelperAtom);
console.log('userSettings===', userSettings);
const getCurrentTheme = (mode: Theme): 'light' | 'realDark' => {
if (mode === 'auto') {
return window.matchMedia('(prefers-color-scheme: dark)').matches
@@ -46,22 +48,32 @@ export default function useUserSettings() {
});
};
useEffect(() => {
setHtmlThemeAttr(userSettings.theme);
}, [userSettings.theme]);
const initTheme = () => {
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)')
.matches
? 'realDark'
: 'light';
if (userSettings.mode === 'auto' && systemTheme !== userSettings.theme) {
setTheme('auto');
}
};
useEffect(() => {
if (userSettings.mode !== 'auto') return;
initTheme();
}, [userSettings.theme, userSettings.mode]);
useEffect(() => {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleChange = () => {
// The theme follows the system preference, when the mode is set to 'auto'
if (userSettings.mode !== 'auto') return;
setTheme('auto');
};
mediaQuery.addEventListener('change', handleChange);
return () => {
mediaQuery.removeEventListener('change', handleChange);
};
}, [userSettings.mode]);
}, [userSettings.mode, userSettings.theme]);
return {
userSettings,
+1
View File
@@ -267,6 +267,7 @@ export const getRightRenderContent = (opts: {
<span className="avatar-container">
<Avatar
size={28}
src={opts.initialState?.currentUser?.avatar_url}
style={{
color: 'var(--ant-color-text)',
fontSize: 16
+5 -3
View File
@@ -17,6 +17,7 @@ interface MenuItem {
interface SiderMenuProps {
menuData: MenuItem[];
collapsed?: boolean;
initialState: Global.InitialState;
}
const useStyles = createStyles(({ css, token }) => {
@@ -122,7 +123,8 @@ const useStyles = createStyles(({ css, token }) => {
});
const SiderMenu: React.FC<SiderMenuProps> = (props) => {
const { menuData, collapsed } = props;
const { menuData, collapsed, initialState } = props;
const is_admin = initialState?.currentUser?.is_admin || false;
const { styles, cx } = useStyles();
const location = useLocation();
const [collapseKeys, setCollapseKeys] = useState<Set<string>>(new Set());
@@ -220,9 +222,9 @@ const SiderMenu: React.FC<SiderMenuProps> = (props) => {
rotate={collapseKeys.has(item.key) ? -90 : 0}
></CaretDownOutlined>
</span>
) : (
) : is_admin ? (
<Divider style={dividerStyles} />
)}
) : null}
</div>
<div
className={cx(styles.menuItemGroup, {
+8 -2
View File
@@ -5,7 +5,7 @@ export default {
'common.button.editmode': 'Edit mode',
'common.button.shortcut': 'Keyboard Shortcut',
'common.button.add': 'Add',
'common.button.login': 'Log In',
'common.button.login': 'Log in',
'common.button.select': 'Select',
'common.button.selected': 'Selected',
'common.button.continue': 'Continue',
@@ -251,5 +251,11 @@ export default {
'common.tips.escape.disable':
'Click Cancel or the X at the top right to close.',
'common.button.clearSelection': 'Clear Selection',
'common.select.count': '{count} selected'
'common.select.count': '{count} selected',
'common.login.auth': 'Authenticating...',
'common.login.auth.failed': 'Authentication failed',
'common.login.password': 'Log in with Password',
'common.external.login': 'Log in with {type}',
'common.sso.noConfig':
'Single sign-on is not enabled on this system. Please contact your administrator.'
};
+1
View File
@@ -6,6 +6,7 @@ export default {
'users.table.username': 'User Name',
'users.table.role': 'Role',
'users.form.fullname': 'Full Name',
'users.form.source': 'Source',
'users.table.user': 'users',
'users.form.admin': 'Admin',
'users.form.user': 'User',
+13 -2
View File
@@ -251,7 +251,13 @@ export default {
'common.tips.escape.disable':
'Click Cancel or the X at the top right to close.',
'common.button.clearSelection': 'Clear Selection',
'common.select.count': '{count} selected'
'common.select.count': '{count} selected',
'common.login.auth': 'Authenticating...',
'common.login.auth.failed': 'Authentication failed',
'common.login.password': 'Log in with Password',
'common.external.login': 'Log in with {type}',
'common.sso.noConfig':
'Single sign-on is not enabled on this system. Please contact your administrator.'
};
// ========== To-Do: Translate Keys (Remove After Translation) ==========
@@ -269,5 +275,10 @@ export default {
// 12. 'common.page.refresh.tips': 'Oops! Something went wrong. Try refreshing the page.'
// 13. 'common.tips.escape.disable': 'Click Cancel or the X at the top right to close.'
// 14. 'common.button.clearSelection': 'Clear Selection',
// 15. 'common.select.count': '{count} selected'
// 15. 'common.select.count': '{count} selected',
// 16. 'common.login.auth': 'Authenticating...',
// 17. 'common.login.auth.failed': 'Authentication failed',
// 18. 'common.external.login': 'Log in with {type}'
// 19. 'common.login.password': 'Log in with Password',
// 20. 'common.sso.noConfig': 'Single sign-on is not enabled on this system. Please contact your administrator.'
// ========== End of To-Do List ==========
+1
View File
@@ -6,6 +6,7 @@ export default {
'users.table.username': 'ユーザー名',
'users.table.role': '役割',
'users.form.fullname': 'フルネーム',
'users.form.source': 'ソース',
'users.table.user': 'ユーザー',
'users.form.admin': '管理者',
'users.form.user': '一般ユーザー',
+10 -2
View File
@@ -250,9 +250,17 @@ export default {
'common.tips.escape.disable':
'Чтобы закрыть, нажмите "Отмена" или крестик (X) в правом верхнем углу.',
'common.button.clearSelection': 'Сбросить выбор',
'common.select.count': '{count} Выбрано'
'common.select.count': '{count} Выбрано',
'common.login.auth': 'Аутентификация...',
'common.login.auth.failed': 'Ошибка аутентификации',
'common.login.password': 'Log in with Password',
'common.external.login': 'Log in with {type}',
'common.sso.noConfig':
'Single sign-on is not enabled on this system. Please contact your administrator.'
};
// ========== To-Do: Translate Keys (Remove After Translation) ==========
// 1. 'common.external.login': 'Log in with {type}'
// 2. 'common.login.password': 'Log in with Password'
// 3. 'common.sso.noConfig': 'Single sign-on is not enabled on this system. Please contact your administrator.'
// ========== End of To-Do List ==========
+4 -6
View File
@@ -28,13 +28,11 @@ export default {
'dashboard.usage.export.user': 'Пользователь',
'dashboard.usage.export.model': 'Модель',
'dashboard.usage.export.date': 'Дата',
'dashboard.usage.datePicker.last7days': 'Last 7 Days',
'dashboard.usage.datePicker.last30days': 'Last 30 Days',
'dashboard.usage.datePicker.last60days': 'Last 60 Days'
'dashboard.usage.datePicker.last7days': 'Последние 7 Days',
'dashboard.usage.datePicker.last30days': 'Последние 30 Days',
'dashboard.usage.datePicker.last60days': 'Последние 60 Days'
};
// ========== To-Do: Translate Keys (Remove After Translation) ==========
// 1. 'dashboard.usage.datePicker.last7days': 'Last 7 Days',
// 2. 'dashboard.usage.datePicker.last30days': 'Last 30 Days',
// 3. 'dashboard.usage.datePicker.last60days': 'Last 60 Days'
// ========== End of To-Do List ==========
+8 -14
View File
@@ -11,7 +11,7 @@ export default {
'menu.models.modelList': 'Развертывание и управление',
'menu.models.modelCatalog': 'Каталог',
'menu.models.catalog': 'Каталог моделей',
'menu.models.deployment': 'Deployment',
'menu.models.deployment': 'Запуск',
'menu.modelCatalog': 'Каталог',
'menu.resources': 'Ресурсы',
'menu.apikeys': 'API-ключи',
@@ -19,21 +19,15 @@ export default {
'menu.profile': 'Профиль',
'menu.login': 'Авторизация',
'menu.usage': 'Использование',
'menu.404': '404',
'menu.resources.workers': 'Workers',
'menu.404': 'Ошибка 404',
'menu.resources.workers': 'Воркеры',
'menu.resources.gpus': 'GPUs',
'menu.resources.modelfiles': 'Model Files',
'menu.accessControl': 'Access Control',
'menu.accessControl.apikeys': 'API Keys',
'menu.accessControl.users': 'Users'
'menu.resources.modelfiles': 'Файлы модлей',
'menu.accessControl': 'Управление доступом',
'menu.accessControl.apikeys': 'API Ключи',
'menu.accessControl.users': 'Пользователи'
};
// ========== To-Do: Translate Keys (Remove After Translation) ==========
// 1. 'menu.models.deployment': 'Deployment',
// 2. 'menu.resources.workers': 'Workers',
// 3. 'menu.resources.gpus': 'GPUs',
// 4. 'menu.resources.modelfiles': 'Model Files',
// 5. 'menu.accessControl': 'Access Control',
// 6. 'menu.accessControl.apikeys': 'API Keys',
// 7. 'menu.accessControl.users': 'Users',
// ========== End of To-Do List ==========
+4 -6
View File
@@ -150,7 +150,7 @@ export default {
'models.table.apiAccessInfo.gotoCreate': 'Перейти к созданию',
'models.search.parts': '{n} частей',
'models.search.evaluate.error': 'Возникла ошибка при вычислении: ',
'models.ollama.deprecated.title': 'Deprecation Notice',
'models.ollama.deprecated.title': 'Объявление об устаревании',
'models.ollama.deprecated.current':
'<span class="bold-text">Текущая версия (v0.6.1): </span>Модели Ollama в настоящее время доступны для использования.',
'models.ollama.deprecated.upcoming':
@@ -159,13 +159,11 @@ export default {
'<span class="bold-text">После обновления до версии (v0.7.0),</span> все ранее развёрнутые модели продолжат работать в обычном режиме.',
'models.ollama.deprecated.issue':
'См. связанную проблему: <a href="https://github.com/gpustack/gpustack/issues/1979" target="_blank">#1979 on GitHub</a>.',
'models.ollama.deprecated.notice': `The Ollama model source has been deprecated as of v0.6.1. For more information, see the <a href="https://github.com/gpustack/gpustack/issues/1979" target="_blank">related GitHub issue</a>.`,
'models.ollama.deprecated.notice': `Источник моделей Ollama объявлен устаревшим начиная с версии v0.6.1. Подробности см. в <a href="https://github.com/gpustack/gpustack/issues/1979" target="_blank">соответствующем issue на GitHub</a>.`,
'models.backend.mindie.310p':
'Ascend 310P only supports FP16, so you need to set --dtype=float16.'
'Ascend 310P поддерживает только FP16, поэтому необходимо установить --dtype=float16.'
};
// ========== To-Do: Translate Keys (Remove After Translation) ==========
// 1. 'models.ollama.deprecated.title': 'Deprecation Notice',
// 2. 'models.ollama.deprecated.notice': `The Ollama model source has been deprecated as of v0.6.1. For more information, see the <a href="https://github.com/gpustack/gpustack/issues/1979" target="_blank">related GitHub issue</a>.`
// 3. 'models.backend.mindie.310p':'Ascend 310P only supports FP16, so you need to set --dtype=float16.'
// ========== End of To-Do List ==========
+1
View File
@@ -6,6 +6,7 @@ export default {
'users.table.username': 'Имя пользователя',
'users.table.role': 'Роль',
'users.form.fullname': 'Полное имя',
'users.form.source': 'Источник',
'users.table.user': 'пользователи',
'users.form.admin': 'Администратор',
'users.form.user': 'Пользователь',
+6 -1
View File
@@ -244,5 +244,10 @@ export default {
'common.page.refresh.tips': '出了点问题,试试刷新页面吧!',
'common.tips.escape.disable': '请点击「取消」按钮或右上角 X 关闭窗口',
'common.button.clearSelection': '清除选择',
'common.select.count': '已选 {count} 项'
'common.select.count': '已选 {count} 项',
'common.login.auth': '认证中...',
'common.login.auth.failed': '认证失败',
'common.login.password': '使用密码登录',
'common.external.login': '使用 {type} 登录',
'common.sso.noConfig': '该系统未启用单点登录,请联系管理员。'
};
+1
View File
@@ -6,6 +6,7 @@ export default {
'users.table.username': '用户名',
'users.table.role': '角色',
'users.form.fullname': '全名',
'users.form.source': '来源',
'users.table.user': '用户',
'users.form.admin': '管理员',
'users.form.user': '普通用户',
@@ -161,6 +161,11 @@ const HFModelFile: React.FC<HFModelFileProps> = forwardRef((props, ref) => {
return filterRegGGUF.test(file.path) || _.includes(file.path, '.gguf');
};
const isNormalGGUFModelFile = (filename: string) => {
const file = filename?.toLowerCase() ?? '';
return file.indexOf('mmproj') === -1 && file.indexOf('imatrix') === -1;
};
// hugging face files
const getHuggingfaceFiles = async () => {
try {
@@ -175,7 +180,7 @@ const HFModelFile: React.FC<HFModelFileProps> = forwardRef((props, ref) => {
});
const list = _.filter(fileList, (file: any) => {
return hfFileFilter(file) && file.path.indexOf('mmproj') === -1;
return hfFileFilter(file) && isNormalGGUFModelFile(file.path);
});
return list;
@@ -188,7 +193,7 @@ const HFModelFile: React.FC<HFModelFileProps> = forwardRef((props, ref) => {
return (
filterRegGGUF.test(file.Path) &&
file.Type === 'blob' &&
file.Path.indexOf('mmproj') === -1
isNormalGGUFModelFile(file.Path)
);
};
@@ -35,6 +35,7 @@ const IncompatibleInfo = styled.div`
li {
position: relative;
line-height: 20px;
white-space: pre-wrap;
}
li.normal::before {
position: absolute;
@@ -877,6 +877,7 @@ const Models: React.FC<ModelsProps> = ({
onOk={handleCreateModel}
></DeployModal>
<ViewLogsModal
status={currentInstance.status}
url={currentInstance.url}
tail={currentInstance.tail}
id={currentInstance.id}
@@ -4,7 +4,7 @@ import { useIntl } from '@umijs/max';
import { Modal } from 'antd';
import React, { useCallback, useEffect, useState } from 'react';
import { MODELS_API } from '../apis';
import { InstanceRealtimeLogStatus } from '../config';
import { InstanceRealtimeLogStatus, InstanceStatusMap } from '../config';
type ViewModalProps = {
open: boolean;
@@ -12,14 +12,18 @@ type ViewModalProps = {
id?: number | string;
modelId?: number | string;
tail?: number;
status?: string;
onCancel: () => void;
};
const ViewLogsModal: React.FC<ViewModalProps> = (props) => {
const intl = useIntl();
const { setChunkRequest } = useSetChunkRequest();
const { open, url, onCancel, tail } = props || {};
const { open, url, onCancel, tail, status } = props || {};
const [enableScorllLoad, setEnableScorllLoad] = useState(true);
const [isDownloading, setIsDownloading] = useState<boolean>(
status === InstanceStatusMap.Downloading
);
const logsViewerRef = React.useRef<any>(null);
const requestRef = React.useRef<any>(null);
const contentRef = React.useRef<any>(null);
@@ -32,7 +36,9 @@ const ViewLogsModal: React.FC<ViewModalProps> = (props) => {
const updateHandler = (list: any) => {
const data = list?.find((item: any) => item.data?.id === props.id);
// state in InstanceRealtimeLogStatus will not enable scorll load, because it is in the trasisition state
if (data) {
setIsDownloading(data?.data?.state === InstanceStatusMap.Downloading);
setEnableScorllLoad(
() => !InstanceRealtimeLogStatus.includes(data?.data?.state)
);
@@ -70,6 +76,9 @@ const ViewLogsModal: React.FC<ViewModalProps> = (props) => {
url: `${MODELS_API}/${props.modelId}/instances`,
handler: updateHandler
});
} else {
logsViewerRef.current?.abort();
requestRef.current?.current?.cancel?.();
}
return () => {
@@ -111,6 +120,7 @@ const ViewLogsModal: React.FC<ViewModalProps> = (props) => {
url={url}
tail={tail}
enableScorllLoad={enableScorllLoad}
isDownloading={isDownloading}
params={{
follow: true
}}
+2 -29
View File
@@ -1,36 +1,9 @@
import IconFont from '@/components/icon-font';
import icons from '@/components/icon-font/icons';
import HotKeys from '@/config/hotkeys';
import {
ApiOutlined,
DeleteOutlined,
DownloadOutlined,
EditOutlined,
ExperimentOutlined,
RetweetOutlined,
ThunderboltOutlined
} from '@ant-design/icons';
import _ from 'lodash';
import React from 'react';
import { modelCategoriesMap, modelSourceMap } from './index';
const icons = {
EditOutlined: React.createElement(EditOutlined),
ExperimentOutlined: React.createElement(ExperimentOutlined),
DeleteOutlined: React.createElement(DeleteOutlined),
ThunderboltOutlined: React.createElement(ThunderboltOutlined),
RetweetOutlined: React.createElement(RetweetOutlined),
DownloadOutlined: React.createElement(DownloadOutlined),
ApiOutlined: React.createElement(ApiOutlined),
Stop: React.createElement(IconFont, { type: 'icon-stop1' }),
Play: React.createElement(IconFont, { type: 'icon-outline-play' }),
Catalog: React.createElement(IconFont, { type: 'icon-catalog' }),
HF: React.createElement(IconFont, { type: 'icon-huggingface' }),
Ollama: React.createElement(IconFont, { type: 'icon-ollama' }),
ModelScope: React.createElement(IconFont, { type: 'icon-tu2' }),
LocalPath: React.createElement(IconFont, { type: 'icon-hard-disk' }),
Launch: React.createElement(IconFont, { type: 'icon-rocket-launch' })
};
export const modalConfig: Record<
string,
{ show: boolean; width: string | number; source: any; isGGUF?: boolean }
@@ -196,7 +169,7 @@ export const modelFileActions = [
{
label: 'common.button.deploy',
key: 'deploy',
icon: icons.Launch
icon: icons.Deployment
},
{
label: 'resources.modelfiles.retry.download',
+1 -1
View File
@@ -517,7 +517,7 @@ export const getBackendParamsTips = (backend: string) => {
return {
backend: 'vLLM',
releases: 'https://github.com/vllm-project/vllm/releases',
link: 'https://docs.vllm.ai/en/latest/configuration/engine_args.html',
link: 'https://docs.vllm.ai/en/stable/cli/serve.html',
version: 'v0.8.5'
};
}
+27 -8
View File
@@ -113,15 +113,25 @@ const options = [
label: '--tool-call-parser',
value: '--tool-call-parser',
options: [
'mistral',
'hermes',
'internlm',
'jamba',
'llama3_json',
'granite-20b-fc',
'phi4_mini_json',
'llama3_json',
'llama4_json',
'pythonic',
'jamba',
'xlam',
'llama4_pythonic',
'hunyuan_a13b',
'mistral',
'deepseek_v3',
'kimi_k2',
'step3',
'qwen3_coder',
'hermes',
'glm45',
'granite',
'pythonic'
'minimax',
'granite-20b-fc',
'internlm'
]
},
{
@@ -229,7 +239,16 @@ const options = [
{
label: '--reasoning-parser',
value: '--reasoning-parser',
options: ['deepseek_r1']
options: [
'deepseek_r1',
'glm45',
'GptOss',
'granite',
'hunyuan_a13b',
'mistral',
'qwen3',
'step3'
]
},
{
label: '--config-format',
+10 -1
View File
@@ -535,13 +535,22 @@ export const useCheckCompatibility = () => {
cacheFormValuesRef.current = {};
};
const noLocalPathValue = (allValues: any) => {
return (
allValues.source === modelSourceMap.local_path_value &&
!allValues.local_path
);
};
const handleOnValuesChange = async (params: {
changedValues: any;
allValues: any;
source: string;
}) => {
const { allValues, source } = params;
if (_.isEqual(cacheFormValuesRef.current, allValues)) {
if (
_.isEqual(cacheFormValuesRef.current, allValues) ||
noLocalPathValue(allValues)
) {
console.log('No changes detected, skipping evaluation.');
return;
}
+9
View File
@@ -5,6 +5,11 @@ import qs from 'query-string';
export const AUTH_API = '/auth';
export const AUTH_CONFIG_API = '/auth-config';
export const AUTH_OIDC_LOGIN_API = '/auth/oidc/login';
export const AUTH_SAML_LOGIN_API = '/auth/saml/login';
export const login = async (
params: { username: string; password: string },
options?: any
@@ -38,3 +43,7 @@ export const updatePassword = async (params: any) => {
data: params
});
};
export const fetchAuthConfig = async () => {
return request(AUTH_CONFIG_API);
};
@@ -0,0 +1,96 @@
import SealInput from '@/components/seal-form/seal-input';
import externalLinks from '@/constants/external-links';
import { LockOutlined, UserOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Button, Checkbox, Form, FormInstance } from 'antd';
interface LocalUserFormProps {
handleLogin: (values: any) => void;
form: FormInstance;
}
const LocalUserForm: React.FC<LocalUserFormProps> = (props) => {
const { handleLogin, form } = props;
const intl = useIntl();
return (
<Form
form={form}
style={{ width: '360px', margin: '0 auto' }}
onFinish={handleLogin}
>
<Form.Item
name="username"
rules={[
{
required: true,
message: intl.formatMessage(
{ id: 'common.form.rule.input' },
{
name: intl.formatMessage({ id: 'common.form.username' })
}
)
}
]}
>
<SealInput.Input
label={intl.formatMessage({ id: 'common.form.username' })}
prefix={<UserOutlined />}
/>
</Form.Item>
<Form.Item
name="password"
rules={[
{
required: true,
message: intl.formatMessage(
{ id: 'common.form.rule.input' },
{
name: intl.formatMessage({ id: 'common.form.password' })
}
)
}
]}
>
<SealInput.Password
prefix={<LockOutlined />}
label={intl.formatMessage({ id: 'common.form.password' })}
/>
</Form.Item>
<div
className="flex-center flex-between"
style={{
marginBottom: 24
}}
>
<Form.Item noStyle name="autoLogin" valuePropName="checked">
<Checkbox style={{ marginLeft: 5 }}>
<span style={{ color: 'var(--ant-color-text-secondary)' }}>
{intl.formatMessage({ id: 'common.login.rember' })}
</span>
</Checkbox>
</Form.Item>
<Button
type="link"
size="small"
href={externalLinks.resetPassword}
target="_blank"
style={{ padding: 0 }}
>
{intl.formatMessage({ id: 'common.button.forgotpassword' })}
</Button>
</div>
<Button
htmlType="submit"
type="primary"
block
style={{ height: '48px', fontSize: '14px' }}
>
{intl.formatMessage({ id: 'common.button.login' })}
</Button>
</Form>
);
};
export default LocalUserForm;
+202 -170
View File
@@ -1,67 +1,108 @@
import LogoIcon from '@/assets/images/gpustack-logo.png';
import { initialPasswordAtom, userAtom } from '@/atoms/user';
import LangSelect from '@/components/lang-select';
import SealInput from '@/components/seal-form/seal-input';
import ThemeDropActions from '@/components/theme-toggle/theme-drop-actions';
import externalLinks from '@/constants/external-links';
import {
CRYPT_TEXT,
REMEMBER_ME_KEY,
getRememberMe,
rememberMe,
removeRememberMe
} from '@/utils/localstore/index';
import { LockOutlined, UserOutlined } from '@ant-design/icons';
import { userAtom } from '@/atoms/user';
import { useIntl, useModel } from '@umijs/max';
import { Button, Checkbox, Form } from 'antd';
import { Button, Divider, Form, Spin, message } from 'antd';
import { createStyles } from 'antd-style';
import CryptoJS from 'crypto-js';
import { useAtom } from 'jotai';
import { useEffect, useMemo } from 'react';
import { useMemo, useState } from 'react';
import { flushSync } from 'react-dom';
import { login } from '../apis';
import styled from 'styled-components';
import { useLocalAuth } from '../hooks/use-local-auth';
import { useSSOAuth } from '../hooks/use-sso-auth';
import { checkDefaultPage } from '../utils';
import LocalUserForm from './local-user-form';
const SpinContainer = styled.div`
display: flex;
flex-direction: column;
height: 300px;
.spin {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
}
`;
const Buttons = styled.div`
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
gap: 24px;
width: 360px;
margin-top: 52px;
`;
const BackButton = styled(Button).attrs({
type: 'link',
size: 'small',
block: true
})`
margin-top: 20px;
`;
const ButtonWrapper = styled(Button).attrs({
type: 'primary',
block: true
})`
height: 48px;
`;
const ButtonText = styled.span`
display: flex;
align-items: center;
gap: 8px;
`;
const DividerWrapper = styled(Divider)`
margin-block: 24px !important;
.ant-divider-inner-text {
color: var(--ant-color-text-secondary);
}
`;
const useStyles = createStyles(({ token, css }) => ({
header: css`
errorMessage: css`
display: flex;
align-items: center;
gap: 8px;
position: fixed;
right: 0;
top: 0;
height: 60px;
padding: 20px;
.anticon-global {
color: ${token.colorText};
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
color: ${token.colorText};
.title {
font-weight: bold;
}
.anticon:hover {
color: ${token.colorTextTertiary};
`,
welcome: css`
display: flex;
margin-bottom: 32px;
font-size: 20px;
justify-content: center;
align-items: center;
.text {
color: ${token.colorText};
}
`
}));
const LoginForm = () => {
const [messageApi, contextHolder] = message.useMessage();
const { styles } = useStyles();
const [userInfo, setUserInfo] = useAtom(userAtom);
const [initialPassword, setInitialPassword] = useAtom(initialPasswordAtom);
const { initialState, setInitialState } = useModel('@@initialState') || {};
const [authError, setAuthError] = useState<Error | null>(null);
const intl = useIntl();
const [form] = Form.useForm();
const [isPassword, setIsPassword] = useState(false);
const [loading, setLoading] = useState(false);
const renderWelCome = useMemo(() => {
const renderWelCome = () => {
return (
<div
style={{
display: 'flex',
marginBottom: 32,
fontSize: 20,
justifyContent: 'center',
alignItems: 'center'
}}
>
<div className={styles.welcome}>
<div className="flex-center">
<span>{intl?.formatMessage({ id: 'users.login.title' })}</span>
<span className="text">
{intl?.formatMessage({ id: 'users.login.title' })}
</span>
<img
src={LogoIcon}
alt="logo"
@@ -70,7 +111,7 @@ const LoginForm = () => {
</div>
</div>
);
}, [intl]);
};
const gotoDefaultPage = async (userInfo: any) => {
checkDefaultPage(userInfo, true);
@@ -90,147 +131,138 @@ const LoginForm = () => {
return userInfo;
};
const encryptPassword = (password: string) => {
const psw = CryptoJS.AES?.encrypt?.(password, CRYPT_TEXT).toString();
return psw;
};
const decryptPassword = (password: string) => {
const bytes = CryptoJS.AES?.decrypt?.(password, CRYPT_TEXT);
const res = bytes.toString(CryptoJS.enc.Utf8);
return res;
// error handling for authentication
const handleOnError = (error: Error) => {
setAuthError(error);
messageApi.error({
duration: 5,
content: (
<div className={styles.errorMessage}>
<div className="title">
{intl.formatMessage({ id: 'common.login.auth.failed' })}
</div>
<div className="message">{error?.message || 'Unknown error'}</div>
</div>
)
});
};
const callRememberMe = async (values: any) => {
const { autoLogin } = values;
if (autoLogin) {
await rememberMe(REMEMBER_ME_KEY, {
um: encryptPassword(values.username),
pw: encryptPassword(values.password),
f: true
});
} else {
await removeRememberMe(REMEMBER_ME_KEY);
}
};
const callGetRememberMe = async () => {
const rememberMe = await getRememberMe(REMEMBER_ME_KEY);
if (rememberMe?.f) {
const username = decryptPassword(rememberMe?.um);
const password = decryptPassword(rememberMe?.pw);
form.setFieldsValue({ username, password, autoLogin: true });
}
};
const handleLogin = async (values: any) => {
try {
await login({
username: values.username,
password: values.password
});
const userInfo = await fetchUserInfo();
// local user authentication
const { handleLogin } = useLocalAuth({
fetchUserInfo,
form,
onSuccess: async (userInfo) => {
setUserInfo(userInfo);
if (values.autoLogin) {
await callRememberMe(values);
} else {
await removeRememberMe(REMEMBER_ME_KEY);
}
if (!userInfo?.require_password_change) {
gotoDefaultPage(userInfo);
} else {
setInitialPassword(encryptPassword(values.password));
}
} catch (error) {
// to do something
},
onError: (error) => {
// gpustack handle in the interceptor
}
});
// SSO hook
const SSOAuth = useSSOAuth({
fetchUserInfo,
onSuccess: (userInfo) => {
setUserInfo(userInfo);
gotoDefaultPage({});
},
onLoading: (loading) => {
setLoading(loading);
},
onError: handleOnError
});
const handleLoginWithPassword = () => {
setIsPassword(true);
};
useEffect(() => {
callGetRememberMe();
}, []);
const handleLoginWithThirdParty = () => {
if (SSOAuth.options.oidc) {
SSOAuth.loginWithOIDC();
} else if (SSOAuth.options.saml) {
SSOAuth.loginWithSAML();
}
setLoading(true);
setAuthError(null);
};
const hasThirdPartyLogin = useMemo(() => {
return SSOAuth.options.oidc || SSOAuth.options.saml;
}, [SSOAuth.options]);
const renderLoginButtons = () => {
// do not render login buttons if using password login or no third-party login
if (!hasThirdPartyLogin || isPassword) return null;
return (
<Buttons>
{SSOAuth.options.oidc && (
<ButtonWrapper onClick={SSOAuth.loginWithOIDC}>
<ButtonText>
{intl.formatMessage(
{ id: 'common.external.login' },
{ type: 'SSO' }
)}
</ButtonText>
</ButtonWrapper>
)}
{SSOAuth.options.saml && (
<ButtonWrapper onClick={SSOAuth.loginWithSAML}>
<ButtonText>
{intl.formatMessage(
{ id: 'common.external.login' },
{ type: 'SSO' }
)}
</ButtonText>
</ButtonWrapper>
)}
<Button type="link" block onClick={handleLoginWithPassword}>
<ButtonText>
{intl.formatMessage({ id: 'common.login.password' })}
</ButtonText>
</Button>
</Buttons>
);
};
const isThirdPartyAuthHandling = useMemo(() => {
return loading && !authError;
}, [loading, authError]);
return (
<div>
<div className={styles.header}>
<ThemeDropActions></ThemeDropActions>
<LangSelect />
</div>
{contextHolder}
<div>
<Form
form={form}
style={{ width: '360px', margin: '0 auto' }}
onFinish={handleLogin}
>
{renderWelCome}
<Form.Item
name="username"
rules={[
{
required: true,
message: intl.formatMessage(
{ id: 'common.form.rule.input' },
{ name: intl.formatMessage({ id: 'common.form.username' }) }
)
}
]}
>
<SealInput.Input
label={intl.formatMessage({ id: 'common.form.username' })}
prefix={<UserOutlined />}
/>
</Form.Item>
<Form.Item
name="password"
rules={[
{
required: true,
message: intl.formatMessage(
{ id: 'common.form.rule.input' },
{ name: intl.formatMessage({ id: 'common.form.password' }) }
)
}
]}
>
<SealInput.Password
prefix={<LockOutlined />}
label={intl.formatMessage({ id: 'common.form.password' })}
/>
</Form.Item>
<div
className="flex-center flex-between"
style={{
marginBottom: 24
}}
>
<Form.Item noStyle name="autoLogin" valuePropName="checked">
<Checkbox style={{ marginLeft: 5 }}>
<span style={{ color: 'var(--ant-color-text-secondary)' }}>
{intl.formatMessage({ id: 'common.login.rember' })}
</span>
</Checkbox>
</Form.Item>
<Button
type="link"
size="small"
href={externalLinks.resetPassword}
target="_blank"
style={{ padding: 0 }}
>
{intl.formatMessage({ id: 'common.button.forgotpassword' })}
</Button>
</div>
<Button
htmlType="submit"
type="primary"
block
style={{ height: '48px', fontSize: '14px' }}
>
{intl.formatMessage({ id: 'common.button.login' })}
</Button>
</Form>
{isThirdPartyAuthHandling ? (
<SpinContainer>
{renderWelCome()}
<div className="spin">
<Spin tip={intl.formatMessage({ id: 'common.login.auth' })}>
<div style={{ width: 300 }}></div>
</Spin>
</div>
</SpinContainer>
) : (
<>
{renderWelCome()}
{renderLoginButtons()}
{(!hasThirdPartyLogin || isPassword) && (
<LocalUserForm handleLogin={handleLogin} form={form} />
)}
{hasThirdPartyLogin && isPassword && (
<BackButton onClick={handleLoginWithThirdParty}>
{intl.formatMessage(
{ id: 'common.external.login' },
{ type: 'SSO' }
)}
</BackButton>
)}
</>
)}
</div>
</div>
);
+2 -5
View File
@@ -2,8 +2,8 @@ import { initialPasswordAtom, userAtom } from '@/atoms/user';
import SealInput from '@/components/seal-form/seal-input';
import { PasswordReg } from '@/config';
import { CRYPT_TEXT } from '@/utils/localstore/index';
import { GlobalOutlined, LockOutlined } from '@ant-design/icons';
import { SelectLang, useIntl } from '@umijs/max';
import { LockOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Button, Form, message } from 'antd';
import CryptoJS from 'crypto-js';
import { useAtom } from 'jotai';
@@ -47,9 +47,6 @@ const PasswordForm: React.FC = () => {
return (
<div>
<div style={{ position: 'fixed', right: 0, top: 0, padding: '0 20px' }}>
<SelectLang icon={<GlobalOutlined />} reload={false} />
</div>
<Form
form={form}
style={{ width: '360px', margin: '0 auto' }}
+98
View File
@@ -0,0 +1,98 @@
import { initialPasswordAtom } from '@/atoms/user';
import {
CRYPT_TEXT,
REMEMBER_ME_KEY,
getRememberMe,
rememberMe,
removeRememberMe
} from '@/utils/localstore/index';
import { FormInstance } from 'antd';
import CryptoJS from 'crypto-js';
import { useAtom } from 'jotai';
import { useEffect } from 'react';
import { login } from '../apis';
interface UseLocalAuthOptions {
fetchUserInfo: () => Promise<any>;
onSuccess?: (userInfo: any) => void;
onError?: (error: Error) => void;
form: FormInstance;
}
export const useLocalAuth = ({
fetchUserInfo,
onSuccess,
onError,
form
}: UseLocalAuthOptions) => {
const [initialPassword, setInitialPassword] = useAtom(initialPasswordAtom);
// Encrypt password before storing
const encryptPassword = (password: string) => {
const psw = CryptoJS.AES?.encrypt?.(password, CRYPT_TEXT).toString();
return psw;
};
const decryptPassword = (password: string) => {
const bytes = CryptoJS.AES?.decrypt?.(password, CRYPT_TEXT);
const res = bytes.toString(CryptoJS.enc.Utf8);
return res;
};
const callGetRememberMe = async () => {
const rememberMe = await getRememberMe(REMEMBER_ME_KEY);
if (rememberMe?.f) {
const username = decryptPassword(rememberMe?.um);
const password = decryptPassword(rememberMe?.pw);
form.setFieldsValue({ username, password, autoLogin: true });
}
};
// remember me
const callRememberMe = async (values: any) => {
const { autoLogin } = values;
if (autoLogin) {
await rememberMe(REMEMBER_ME_KEY, {
um: encryptPassword(values.username),
pw: encryptPassword(values.password),
f: true
});
} else {
await removeRememberMe(REMEMBER_ME_KEY);
}
};
// click login button
const handleLogin = async (values: any) => {
try {
await login({
username: values.username,
password: values.password
});
const userInfo = await fetchUserInfo();
if (values.autoLogin) {
await callRememberMe(values);
} else {
await removeRememberMe(REMEMBER_ME_KEY);
}
if (userInfo?.require_password_change) {
setInitialPassword(encryptPassword(values.password));
}
onSuccess?.(userInfo);
} catch (error: any) {
onError?.(error);
}
};
useEffect(() => {
callGetRememberMe();
}, []);
return {
handleLogin
};
};
+80
View File
@@ -0,0 +1,80 @@
// hooks/useSSOAuth.ts
import { history, useIntl } from '@umijs/max';
import { useEffect, useState } from 'react';
import {
AUTH_OIDC_LOGIN_API,
AUTH_SAML_LOGIN_API,
fetchAuthConfig
} from '../apis';
type LoginOption = {
saml: boolean;
oidc: boolean;
};
export function useSSOAuth({
fetchUserInfo,
onSuccess,
onLoading,
onError
}: {
fetchUserInfo: () => Promise<any>;
onSuccess?: (userInfo: any) => void;
onError?: (err: Error) => void;
onLoading?: (loading: boolean) => void;
}) {
const [loginOption, setLoginOption] = useState<LoginOption>({
saml: false,
oidc: false
});
const intl = useIntl();
const { location } = history;
const params = new URLSearchParams(location.search);
const sso = params.get('sso');
const oidcLogin = () => {
window.location.href = AUTH_OIDC_LOGIN_API;
};
const samlLogin = () => {
window.location.href = AUTH_SAML_LOGIN_API;
};
const init = async () => {
try {
const authConfig = await fetchAuthConfig();
setLoginOption({
oidc: !!authConfig.is_oidc,
saml: !!authConfig.is_saml
});
if (sso) {
onLoading?.(true);
if (authConfig.is_oidc) {
oidcLogin();
} else if (authConfig.is_saml) {
samlLogin();
} else {
onError?.(
new Error(intl.formatMessage({ id: 'common.sso.noConfig' }))
);
}
}
} catch (error: any) {
setLoginOption({ oidc: false, saml: false });
onLoading?.(false);
}
};
useEffect(() => {
init();
}, []);
return {
isSSOLogin: !!sso,
options: loginOption,
loginWithOIDC: oidcLogin,
loginWithSAML: samlLogin
};
}
+43 -8
View File
@@ -2,9 +2,12 @@ import Bg2 from '@/assets/images/bg-2.png';
import { userAtom } from '@/atoms/user';
import DarkMask from '@/components/dark-mask';
import Footer from '@/components/footer';
import LangSelect from '@/components/lang-select';
import ThemeDropActions from '@/components/theme-toggle/theme-drop-actions';
import useUserSettings from '@/hooks/use-user-settings';
import { useModel } from '@umijs/max';
import { ConfigProvider, theme } from 'antd';
import { createStyles } from 'antd-style';
import { useAtom } from 'jotai';
import { useEffect } from 'react';
import styled from 'styled-components';
@@ -12,6 +15,29 @@ import LoginForm from './components/login-form';
import PasswordForm from './components/password-form';
import { checkDefaultPage } from './utils';
const useStyles = createStyles(({ token, css }) => ({
header: css`
display: flex;
align-items: center;
gap: 8px;
position: fixed;
right: 0;
top: 0;
height: 60px;
padding: 20px;
.anticon-global {
color: ${token.colorText};
}
.anticon:hover {
color: ${token.colorTextTertiary};
}
`,
formContainer: css`
height: calc(100vh - 64px);
width: 100%;
`
}));
const Wrapper = styled.div<{ $isDarkTheme: boolean }>`
position: fixed;
top: 0;
@@ -30,7 +56,6 @@ const Wrapper = styled.div<{ $isDarkTheme: boolean }>`
`;
const Box = styled.div`
padding-top: 10%;
display: flex;
flex-direction: column;
justify-content: space-between;
@@ -40,12 +65,17 @@ const Box = styled.div`
const FormWrapper = styled.div`
position: relative;
margin: 0 auto;
z-index: 999;
border-radius: var(--border-radius-modal);
width: max-content;
height: max-content;
max-width: 800px;
max-height: 600px;
padding: 40px;
background-color: var(--color-modal-content-bg);
box-shadow: var(--color-modal-box-shadow);
margin-top: 50vh;
transform: translateY(-50%);
.field-wrapper {
background-color: transparent !important;
}
@@ -56,6 +86,7 @@ const FormWrapper = styled.div`
`;
const Login = () => {
const { styles } = useStyles();
const { themeData, userSettings, isDarkTheme } = useUserSettings();
const [userInfo, setUserInfo] = useAtom(userAtom);
const { initialState, setInitialState } = useModel('@@initialState') || {};
@@ -93,10 +124,14 @@ const Login = () => {
...themeData
}}
>
<div>
<DarkMask></DarkMask>
<Wrapper $isDarkTheme={isDarkTheme}></Wrapper>
<Box>
<div className={styles.header}>
<ThemeDropActions></ThemeDropActions>
<LangSelect />
</div>
<DarkMask></DarkMask>
<Wrapper $isDarkTheme={isDarkTheme}></Wrapper>
<Box>
<div className={styles.formContainer}>
<FormWrapper>
{userInfo?.require_password_change ? (
<PasswordForm />
@@ -104,9 +139,9 @@ const Login = () => {
<LoginForm />
)}
</FormWrapper>
<Footer />
</Box>
</div>
</div>
<Footer />
</Box>
</ConfigProvider>
);
};
@@ -168,11 +168,11 @@ export default function useChatCompletion(
if (chunk?.error) {
setTokenResult({
error: true,
errorMessage: extractErrorMessage(chunk)
errorMessage: extractErrorMessage(chunk.error)
});
return;
} else {
joinMessage(chunk);
}
joinMessage(chunk);
});
} catch (error) {
console.log('error:', error);
+28 -14
View File
@@ -1,8 +1,8 @@
import useTabActive from '@/hooks/use-tab-active';
import { PageContainer } from '@ant-design/pro-components';
import { useIntl } from '@umijs/max';
import { useIntl, useModel } from '@umijs/max';
import { TabsProps } from 'antd';
import React, { useCallback, useState } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import styled from 'styled-components';
import Appearance from './components/appearance';
import ModifyPasswordn from './components/modify-password';
@@ -15,23 +15,37 @@ const Wrapper = styled.div`
const Profile: React.FC = () => {
const intl = useIntl();
const { initialState, setInitialState } = useModel('@@initialState') || {};
const { setTabActive, getTabActive, tabsMap } = useTabActive();
const [activeKey, setActiveKey] = useState(
getTabActive(tabsMap.userSettings) || 'modify-password'
initialState?.currentUser?.source === 'Local'
? 'modify-password'
: 'appearance'
);
const items: TabsProps['items'] = [
{
key: 'modify-password',
label: intl.formatMessage({ id: 'users.form.updatepassword' }),
children: <ModifyPasswordn />
},
{
key: 'appearance',
label: intl.formatMessage({ id: 'common.appearance' }),
children: <Appearance />
const items: TabsProps['items'] = useMemo(() => {
if (initialState?.currentUser?.source !== 'Local') {
return [
{
key: 'appearance',
label: intl.formatMessage({ id: 'common.appearance' }),
children: <Appearance />
}
];
}
];
return [
{
key: 'modify-password',
label: intl.formatMessage({ id: 'users.form.updatepassword' }),
children: <ModifyPasswordn />
},
{
key: 'appearance',
label: intl.formatMessage({ id: 'common.appearance' }),
children: <Appearance />
}
];
}, [initialState?.currentUser?.source]);
const handleChangeTab = useCallback((key: string) => {
setActiveKey(key);
+2
View File
@@ -3,6 +3,8 @@ export interface ListItem {
is_admin: boolean;
full_name: string;
id: number;
source: string;
avatar_url: string;
username: string;
created_at: string;
updated_at: string;
+23 -5
View File
@@ -1,6 +1,7 @@
import AutoTooltip from '@/components/auto-tooltip';
import DeleteModal from '@/components/delete-modal';
import DropdownButtons from '@/components/drop-down-buttons';
import IconFont from '@/components/icon-font';
import PageTools from '@/components/page-tools';
import { PageAction } from '@/config';
import HotKeys from '@/config/hotkeys';
@@ -10,9 +11,7 @@ import {
DeleteOutlined,
EditOutlined,
PlusOutlined,
SyncOutlined,
UserOutlined,
UserSwitchOutlined
SyncOutlined
} from '@ant-design/icons';
import { PageContainer } from '@ant-design/pro-components';
import { useIntl } from '@umijs/max';
@@ -86,6 +85,7 @@ const Users: React.FC = () => {
const handleModalOk = async (data: FormData) => {
const params = {
...currentData,
...data,
is_admin: data.is_admin === 'admin'
};
@@ -247,14 +247,17 @@ const Users: React.FC = () => {
render={(text, record: ListItem) => {
return record.is_admin ? (
<AutoTooltip ghost minWidth={50}>
<UserSwitchOutlined className="size-16" />
<IconFont
type="icon-admin-user"
className="size-16"
></IconFont>
<span className="m-l-5">
{intl.formatMessage({ id: 'users.form.admin' })}
</span>
</AutoTooltip>
) : (
<AutoTooltip ghost minWidth={50}>
<UserOutlined className="size-16" />
<IconFont type="icon-user" className="size-16"></IconFont>
<span className="m-l-5">
{intl.formatMessage({ id: 'users.form.user' })}
</span>
@@ -277,6 +280,21 @@ const Users: React.FC = () => {
);
}}
/>
<Column
title={intl.formatMessage({ id: 'users.form.source' })}
dataIndex="source"
key="source"
ellipsis={{
showTitle: false
}}
render={(text, record) => {
return (
<AutoTooltip ghost minWidth={20}>
{text}
</AutoTooltip>
);
}}
/>
<Column
title={intl.formatMessage({ id: 'common.table.createTime' })}
dataIndex="created_at"
+1
View File
@@ -3,6 +3,7 @@ import { clearAtomStorage } from '@/atoms/utils';
import { RequestConfig, history } from '@umijs/max';
import { message } from 'antd';
// no base URL APIs
const NoBaseURLAPIs = ['/auth', '/v1-openai', '/version', '/proxy', '/update'];
export const requestConfig: RequestConfig = {
+1 -1
View File
@@ -1,7 +1,7 @@
import { request } from '@umijs/max';
export async function queryCurrentUserState(opts?: Record<string, any>) {
return request(`/users/me`, {
return request<Global.UserInfo>(`/users/me`, {
method: 'GET',
...opts
});
+28 -16
View File
@@ -1,7 +1,7 @@
import { throttle } from 'lodash';
import qs from 'query-string';
const extractStreamRegx = /data:\s*({.*?})(?=\n|$)/g;
const extractStreamRegx = /(data|error):\s*({.*?})(?=\n|$)/g;
const extractJSON = (dataStr: string) => {
let match;
@@ -12,11 +12,16 @@ const extractJSON = (dataStr: string) => {
}
while ((match = extractStreamRegx.exec(dataStr)) !== null) {
try {
const jsonData = JSON.parse(match[1]);
results.push(jsonData);
} catch (error) {
console.error('JSON parse error:', error, 'for match:', match[1]);
const type = match[1]; // "data" or "error"
console.log('type========', type);
const jsonData = JSON.parse(match[2]);
if (type === 'error') {
results.push({ error: jsonData });
} else {
results.push(jsonData);
}
} catch (err) {
console.error('JSON parse error:', err, 'for match:', match[2]);
continue;
}
}
@@ -162,33 +167,40 @@ export const readStreamData = async (
}, throttleDelay);
let isReading = true;
let textBuffer = ''; // cache incomplete line
while (isReading) {
const { done, value } = await reader.read();
if (done) {
isReading = false;
bufferManager.flush();
break;
}
try {
const chunk = decoder.decode(value, { stream: true });
textBuffer += decoder.decode(value, { stream: true });
if (chunk.startsWith('error:')) {
const errorStr = chunk.slice(7).trim();
if (textBuffer.startsWith('error:')) {
const errorStr = textBuffer.slice(7).trim();
const jsonData = JSON.parse(errorStr);
bufferManager.add({ error: jsonData });
textBuffer = ''; // Clear buffer after processing error
} else {
extractJSON(chunk).forEach((data) => {
extractJSON(textBuffer).forEach((data) => {
bufferManager.add(data);
});
textBuffer = ''; // Clear buffer after processing
}
throttledCallback();
} catch (error) {
bufferManager.add({ error });
}
if (done) {
textBuffer += decoder.decode();
if (textBuffer) {
extractJSON(textBuffer).forEach((data) => bufferManager.add(data));
}
isReading = false;
bufferManager.flush();
break;
}
}
};
+3 -2
View File
@@ -22,7 +22,7 @@ export const handleBatchRequest = async (
};
export const convertFileSize = (
sizeInBytes?: number,
sizeInBytes: number,
prec = 1,
allowEmpty = false
): string | number => {
@@ -31,6 +31,7 @@ export const convertFileSize = (
const fmt = 1024;
const units = ['B', 'KiB', 'MiB', 'GiB', 'TiB'];
const precs = [0, 1, 1, 2, 2]; // precision for each unit
let size = sizeInBytes;
let unitIndex = 0;
@@ -39,7 +40,7 @@ export const convertFileSize = (
unitIndex++;
}
return `${_.round(size, prec)} ${units[unitIndex]}`;
return `${_.round(size, precs[unitIndex])} ${units[unitIndex]}`;
};
export const platformCall = () => {