fix(style): global theme settings in login

This commit is contained in:
jialin
2025-04-28 18:22:05 +08:00
parent bcfa80a2d5
commit beb395ce32
17 changed files with 165 additions and 188 deletions
+20
View File
@@ -0,0 +1,20 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1920" height="1080">
<filter id="noiseFilter">
<feTurbulence
type="fractalNoise"
baseFrequency="0.6"
numOctaves="2"
stitchTiles="stitch"
/>
<feColorMatrix
type="matrix"
values="1 0 0 0 0
0 1 0 0 0
0 0 1 0 0
0 0 0 0.2 0" />
<feGaussianBlur stdDeviation="0.6" />
</filter>
<rect width="100%" height="100%" fill="#181818"/>
<rect width="100%" height="100%" filter="url(#noiseFilter)"/>
</svg>

After

Width:  |  Height:  |  Size: 546 B

@@ -1,8 +0,0 @@
<?xml version="1.0" ?>
<svg xmlns="http://www.w3.org/2000/svg" width="500" height="500">
<filter id="n" x="0" y="0">
<feTurbulence type="fractalNoise" baseFrequency="0.65" numOctaves="5" stitchTiles="stitch"/>
</filter>
<rect width="500" height="500" fill="#1f1f1f" opacity="0.4"/>
<rect width="500" height="500" filter="url(#n)" opacity="0.09"/>
</svg>

Before

Width:  |  Height:  |  Size: 366 B

+1 -1
View File
@@ -5,7 +5,7 @@ import { atomWithStorage } from 'jotai/utils';
type UserSettings = { type UserSettings = {
theme: 'light' | 'realDark' | 'auto'; theme: 'light' | 'realDark' | 'auto';
colorPrimary: string; colorPrimary: string;
isDarkTheme?: boolean; isDarkTheme: boolean;
}; };
export const userSettingsAtom = atomWithStorage<UserSettings>('userSettings', { export const userSettingsAtom = atomWithStorage<UserSettings>('userSettings', {
+21 -3
View File
@@ -4,10 +4,28 @@ import VersionInfo, { modalConfig } from '@/components/version-info';
import externalLinks from '@/constants/external-links'; import externalLinks from '@/constants/external-links';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Button, Modal, Space } from 'antd'; import { Button, Modal, Space } from 'antd';
import './index.less'; import { createStyles } from 'antd-style';
const useStyles = createStyles(({ token, css }) => ({
footer: css`
bottom: 0;
width: 100%;
background-color: transparent;
padding: 20px 0;
text-align: center;
font-size: var(--font-size-middle);
color: ${token.colorTextTertiary};
`,
'footer-content-left-text': {
display: 'flex',
justifyContent: 'center',
alignItems: 'center'
}
}));
const Footer: React.FC = () => { const Footer: React.FC = () => {
const intl = useIntl(); const intl = useIntl();
const { styles } = useStyles();
const showVersion = () => { const showVersion = () => {
Modal.info({ Modal.info({
@@ -18,10 +36,10 @@ const Footer: React.FC = () => {
}; };
return ( return (
<div className="footer"> <div className={styles.footer}>
<div className="footer-content"> <div className="footer-content">
<div className="footer-content-left"> <div className="footer-content-left">
<div className="footer-content-left-text"> <div className={styles['footer-content-left-text']}>
<Space size={4}> <Space size={4}>
<span>&copy;</span> <span>&copy;</span>
<span> {new Date().getFullYear()}</span> <span> {new Date().getFullYear()}</span>
+42
View File
@@ -0,0 +1,42 @@
import useUserSettings from '@/hooks/use-user-settings';
import { MoonOutlined, SunOutlined } from '@ant-design/icons';
import { createStyles } from 'antd-style';
import React from 'react';
const useStyles = createStyles(({ token, css }) => ({
wrapper: css`
display: inline-flex;
justify-content: center;
align-items: center;
cursor: pointer;
color: ${token.colorText};
font-size: var(--font-size-base);
padding: 0 8px;
gap: 8px;
&:hover {
color: ${token.colorTextTertiary};
}
.anticon {
font-size: 16px;
}
`
}));
const ThemeToggle: React.FC = () => {
const { setTheme, userSettings } = useUserSettings();
const { styles } = useStyles();
const handleChangeTheme = () => {
const currentTheme =
userSettings.theme === 'realDark' ? 'light' : 'realDark';
setTheme(currentTheme);
};
return (
<div className={styles.wrapper} onClick={handleChangeTheme}>
{userSettings.theme === 'realDark' ? <MoonOutlined /> : <SunOutlined />}
</div>
);
};
export default ThemeToggle;
+1
View File
@@ -2,6 +2,7 @@ import dark from './dark';
import light from './light'; import light from './light';
export const colorPrimary = '#007BFF'; export const colorPrimary = '#007BFF';
export default { export default {
light, light,
dark, dark,
+2
View File
@@ -74,6 +74,7 @@ html {
--color-fill-spin-bg: rgba(255, 255, 255, 60%); --color-fill-spin-bg: rgba(255, 255, 255, 60%);
--width-tooltip-max: 300px; --width-tooltip-max: 300px;
--color-bg-tooltip: '#fff'; --color-bg-tooltip: '#fff';
--color-modal-content-bg: rgba(255, 255, 255, 90%);
// ======== input ============ // ======== input ============
--box-shadow-base: 0 4px 6px rgba(227, 232, 240, 70%); --box-shadow-base: 0 4px 6px rgba(227, 232, 240, 70%);
--ant-border-radius-lg: 4px; --ant-border-radius-lg: 4px;
@@ -93,6 +94,7 @@ html[data-theme='realDark'] {
--color-bg-tooltip: #424242; --color-bg-tooltip: #424242;
--color-editor-header-bg: #292929; --color-editor-header-bg: #292929;
--color-progress-text: rgba(255, 255, 255, 80%); --color-progress-text: rgba(255, 255, 255, 80%);
--color-modal-content-bg: #1f1f1f;
background: #141414; background: #141414;
} }
+18 -3
View File
@@ -132,6 +132,7 @@ const AddModal: FC<AddModalProps> = (props) => {
} }
return categories || null; return categories || null;
}; };
const handleSelectModelFile = (item: any, evaluate?: boolean) => { const handleSelectModelFile = (item: any, evaluate?: boolean) => {
form.current?.form?.resetFields(resetFields); form.current?.form?.resetFields(resetFields);
const modelInfo = onSelectModel(selectedModel, props.source); const modelInfo = onSelectModel(selectedModel, props.source);
@@ -142,7 +143,11 @@ const AddModal: FC<AddModalProps> = (props) => {
categories: getCategory(item) categories: getCategory(item)
}); });
if (item.fakeName) { if (
item.fakeName &&
!isHolderRef.current.model &&
!isHolderRef.current.file
) {
handleShowCompatibleAlert(item.evaluateResult); handleShowCompatibleAlert(item.evaluateResult);
} }
}; };
@@ -153,7 +158,9 @@ const AddModal: FC<AddModalProps> = (props) => {
setIsGGUF(false); setIsGGUF(false);
form.current?.form?.resetFields(resetFields); form.current?.form?.resetFields(resetFields);
const modelInfo = onSelectModel(item, props.source); const modelInfo = onSelectModel(item, props.source);
handleShowCompatibleAlert(item.evaluateResult); if (!isHolderRef.current.model) {
handleShowCompatibleAlert(item.evaluateResult);
}
form.current?.setFieldsValue?.({ form.current?.setFieldsValue?.({
...getDefaultSpec(item), ...getDefaultSpec(item),
...modelInfo, ...modelInfo,
@@ -256,7 +263,15 @@ const AddModal: FC<AddModalProps> = (props) => {
const displayEvaluateStatus = (data: { const displayEvaluateStatus = (data: {
show?: boolean; show?: boolean;
flag: Record<string, boolean>; flag: Record<string, boolean>;
}) => {}; }) => {
setIsHolderRef(data.flag);
setWarningStatus({
show: isHolderRef.current.model || isHolderRef.current.file,
title: '',
type: 'transition',
message: intl.formatMessage({ id: 'models.form.evaluating' })
});
};
useEffect(() => { useEffect(() => {
if (open) { if (open) {
-1
View File
@@ -28,7 +28,6 @@
.mkd-title { .mkd-title {
cursor: pointer; cursor: pointer;
background-color: var(--ant-color-fill-tertiary); background-color: var(--ant-color-fill-tertiary);
border-bottom: 1px solid rgba(255, 255, 255, 10%);
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
+23 -8
View File
@@ -2,6 +2,7 @@ import LogoIcon from '@/assets/images/gpustack-logo.png';
import { initialPasswordAtom, userAtom } from '@/atoms/user'; import { initialPasswordAtom, userAtom } from '@/atoms/user';
import LangSelect from '@/components/lang-select'; import LangSelect from '@/components/lang-select';
import SealInput from '@/components/seal-form/seal-input'; import SealInput from '@/components/seal-form/seal-input';
import ThemeToggle from '@/components/theme-toggle';
import { import {
CRYPT_TEXT, CRYPT_TEXT,
REMEMBER_ME_KEY, REMEMBER_ME_KEY,
@@ -12,6 +13,7 @@ import {
import { LockOutlined, UserOutlined } from '@ant-design/icons'; import { LockOutlined, UserOutlined } from '@ant-design/icons';
import { useIntl, useModel } from '@umijs/max'; import { useIntl, useModel } from '@umijs/max';
import { Button, Checkbox, Form } from 'antd'; import { Button, Checkbox, Form } from 'antd';
import { createStyles } from 'antd-style';
import CryptoJS from 'crypto-js'; import CryptoJS from 'crypto-js';
import { useAtom } from 'jotai'; import { useAtom } from 'jotai';
import { useEffect, useMemo } from 'react'; import { useEffect, useMemo } from 'react';
@@ -19,7 +21,26 @@ import { flushSync } from 'react-dom';
import { login } from '../apis'; import { login } from '../apis';
import { checkDefaultPage } from '../utils'; import { checkDefaultPage } from '../utils';
const useStyles = createStyles(({ token, css }) => ({
header: css`
display: flex;
align-items: center;
gap: 8px;
position: fixed;
right: 0;
top: 0;
padding: 20px;
.anticon-global {
color: ${token.colorText};
}
.anticon:hover {
color: ${token.colorTextTertiary};
}
`
}));
const LoginForm = () => { const LoginForm = () => {
const { styles } = useStyles();
const [userInfo, setUserInfo] = useAtom(userAtom); const [userInfo, setUserInfo] = useAtom(userAtom);
const [initialPassword, setInitialPassword] = useAtom(initialPasswordAtom); const [initialPassword, setInitialPassword] = useAtom(initialPasswordAtom);
const { initialState, setInitialState } = useModel('@@initialState') || {}; const { initialState, setInitialState } = useModel('@@initialState') || {};
@@ -130,14 +151,8 @@ const LoginForm = () => {
return ( return (
<div> <div>
<div <div className={styles.header}>
style={{ <ThemeToggle></ThemeToggle>
position: 'fixed',
right: 0,
top: 0,
padding: '20px'
}}
>
<LangSelect /> <LangSelect />
</div> </div>
<div> <div>
+12 -13
View File
@@ -11,7 +11,7 @@ import LoginForm from './components/login-form';
import PasswordForm from './components/password-form'; import PasswordForm from './components/password-form';
import { checkDefaultPage } from './utils'; import { checkDefaultPage } from './utils';
const Wrapper = styled.div` const Wrapper = styled.div<{ $isDarkTheme: boolean }>`
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
@@ -19,9 +19,11 @@ const Wrapper = styled.div`
right: 0; right: 0;
min-height: 100vh; min-height: 100vh;
z-index: -1; z-index: -1;
background: url(${Bg2}) center center no-repeat; background: ${({ $isDarkTheme }) =>
background-size: cover; $isDarkTheme
opacity: 0.6; ? `repeating-linear-gradient(45deg, #000, transparent 120px)`
: `url(${Bg2}) center center /cover no-repeat`};
opacity: ${({ $isDarkTheme }) => ($isDarkTheme ? 1 : 0.6)};
`; `;
const Box = styled.div` const Box = styled.div`
@@ -38,7 +40,7 @@ const FormWrapper = styled.div`
width: max-content; width: max-content;
height: max-content; height: max-content;
padding: 32px; padding: 32px;
background-color: var(--ant-modal-content-bg); background-color: var(--color-modal-content-bg);
.field-wrapper { .field-wrapper {
background-color: transparent !important; background-color: transparent !important;
@@ -50,15 +52,10 @@ const FormWrapper = styled.div`
`; `;
const Login = () => { const Login = () => {
const { themeData, userSettings } = useUserSettings(); const { themeData, userSettings, isDarkTheme } = useUserSettings();
const { token } = theme.useToken();
const [userInfo, setUserInfo] = useAtom(userAtom); const [userInfo, setUserInfo] = useAtom(userAtom);
const { initialState, setInitialState } = useModel('@@initialState') || {}; const { initialState, setInitialState } = useModel('@@initialState') || {};
const globalCssvars: Record<string, any> = {
'--ant-modal-content-bg': 'rgba(255, 255, 255, 0.9)'
};
const gotoDefaultPage = async (userInfo: any) => { const gotoDefaultPage = async (userInfo: any) => {
if (!userInfo || userInfo?.require_password_change) { if (!userInfo || userInfo?.require_password_change) {
return; return;
@@ -72,9 +69,11 @@ const Login = () => {
})); }));
} }
}; };
useEffect(() => { useEffect(() => {
gotoDefaultPage(userInfo); gotoDefaultPage(userInfo);
}, [userInfo]); }, [userInfo]);
useEffect(() => { useEffect(() => {
document.title = 'GPUStack'; document.title = 'GPUStack';
}, []); }, []);
@@ -90,8 +89,8 @@ const Login = () => {
...themeData ...themeData
}} }}
> >
<div style={globalCssvars}> <div>
<Wrapper></Wrapper> <Wrapper $isDarkTheme={isDarkTheme}></Wrapper>
<Box> <Box>
<FormWrapper> <FormWrapper>
{userInfo?.require_password_change ? ( {userInfo?.require_password_change ? (
@@ -158,15 +158,7 @@ const GroundImages: React.FC<MessageProps> = forwardRef((props, ref) => {
} }
const params = { const params = {
..._.omitBy( ..._.omitBy(finalParameters, (value: string) => !value),
parameters,
(value: string, key: string) =>
!value || ['width', 'height', 'seed'].includes(key)
),
size:
parameters.size === 'custom'
? `${parameters.width}x${parameters.height}`
: parameters.size,
seed: parameters.random_seed ? generateRandomNumber() : parameters.seed, seed: parameters.random_seed ? generateRandomNumber() : parameters.seed,
stream: true, stream: true,
stream_options: { stream_options: {
@@ -154,7 +154,7 @@ const GroundImages: React.FC<MessageProps> = forwardRef((props, ref) => {
prompt: currentPrompt prompt: currentPrompt
} }
}); });
}, [finalParameters, currentPrompt, parameters.size]); }, [finalParameters, currentPrompt, isOpenaiCompatible]);
const handleClear = () => { const handleClear = () => {
setCurrentPrompt(''); setCurrentPrompt('');
-135
View File
@@ -1,135 +0,0 @@
import FormButtons from '@/components/form-buttons';
import SealInput from '@/components/seal-form/seal-input';
import { PasswordReg } from '@/config';
import { INPUT_WIDTH } from '@/constants';
import { updatePassword } from '@/pages/login/apis';
import { PageContainer } from '@ant-design/pro-components';
import { useIntl } from '@umijs/max';
import { Form, message } from 'antd';
import React from 'react';
interface ProfileProps {
username?: string;
new_password: string;
current_password: string;
}
const Profile: React.FC = () => {
const [form] = Form.useForm();
const intl = useIntl();
const handleOnFinish = async (values: any) => {
try {
await updatePassword({
new_password: values.new_password,
current_password: values.current_password
});
message.success(intl.formatMessage({ id: 'common.message.success' }));
} catch (error) {}
};
const handleOnFinishFailed = (errorInfo: any) => {
console.log('handleOnFinishFailed', errorInfo);
};
const handleCancel = () => {
form.resetFields();
};
return (
<PageContainer
ghost
header={{
title: intl.formatMessage({ id: 'users.form.updatepassword' }),
style: {
paddingInline: 'var(--layout-content-header-inlinepadding)'
}
}}
extra={[]}
>
<Form
style={{ width: '524px' }}
name="profileForm"
form={form}
onFinish={handleOnFinish}
onFinishFailed={handleOnFinishFailed}
>
<Form.Item<ProfileProps>
name="current_password"
rules={[
{
required: true,
message: intl.formatMessage(
{ id: 'common.form.rule.input' },
{
name: intl.formatMessage({
id: 'users.form.currentpassword'
})
}
)
}
]}
>
<SealInput.Password
label={intl.formatMessage({ id: 'users.form.currentpassword' })}
required
style={{ width: INPUT_WIDTH.default }}
></SealInput.Password>
</Form.Item>
<Form.Item<ProfileProps>
name="new_password"
rules={[
{
required: true,
pattern: PasswordReg,
message: intl.formatMessage({
id: 'users.form.rule.password'
})
}
]}
>
<SealInput.Password
label={intl.formatMessage({ id: 'users.form.newpassword' })}
required
style={{ width: INPUT_WIDTH.default }}
></SealInput.Password>
</Form.Item>
<Form.Item
name="confirm_password"
dependencies={['new_password']}
rules={[
{
required: true,
message: intl.formatMessage({
id: 'users.password.confirm.empty'
})
},
({ getFieldValue }) => ({
validator(_, value) {
if (!value || getFieldValue('new_password') === value) {
return Promise.resolve();
}
return Promise.reject(
new Error(
intl.formatMessage({ id: 'users.password.confirm.error' })
)
);
}
})
]}
>
<SealInput.Password
required={true}
style={{ width: INPUT_WIDTH.default }}
label={intl.formatMessage({ id: 'users.password.confirm' })}
/>
</Form.Item>
<FormButtons
htmlType="submit"
onCancel={handleCancel}
showCancel={false}
></FormButtons>
</Form>
</PageContainer>
);
};
export default Profile;
+6 -2
View File
@@ -17,14 +17,18 @@ const ThemeOptions = [
} }
]; ];
const Appearance: React.FC = () => { const Appearance: React.FC = () => {
const { setTheme } = useUserSettings(); const { setTheme, userSettings } = useUserSettings();
const intl = useIntl(); const intl = useIntl();
const handleOnChange = (e: any) => { const handleOnChange = (e: any) => {
setTheme(e.target.value); setTheme(e.target.value);
}; };
return ( return (
<Radio.Group onChange={handleOnChange} style={{ marginTop: 16 }}> <Radio.Group
onChange={handleOnChange}
style={{ marginTop: 16 }}
value={userSettings.theme}
>
{ThemeOptions.map((item) => ( {ThemeOptions.map((item) => (
<Radio key={item.key} value={item.key}> <Radio key={item.key} value={item.key}>
{intl.formatMessage({ id: item.label })} {intl.formatMessage({ id: item.label })}
+17 -4
View File
@@ -546,6 +546,9 @@ const ModelFiles = () => {
{ {
title: intl.formatMessage({ id: 'models.form.source' }), title: intl.formatMessage({ id: 'models.form.source' }),
dataIndex: 'source', dataIndex: 'source',
ellipsis: {
showTitle: false
},
render: (text: string, record: ListItem) => { render: (text: string, record: ListItem) => {
const modelInfo = getModelInfo(record); const modelInfo = getModelInfo(record);
const { repo_id } = modelInfo; const { repo_id } = modelInfo;
@@ -561,6 +564,9 @@ const ModelFiles = () => {
{ {
title: 'Worker', title: 'Worker',
dataIndex: 'worker_name', dataIndex: 'worker_name',
ellipsis: {
showTitle: false
},
render: (text: string, record: ListItem) => { render: (text: string, record: ListItem) => {
return ( return (
<AutoTooltip ghost> <AutoTooltip ghost>
@@ -581,6 +587,9 @@ const ModelFiles = () => {
title: intl.formatMessage({ id: 'resources.modelfiles.form.path' }), title: intl.formatMessage({ id: 'resources.modelfiles.form.path' }),
dataIndex: 'resolved_paths', dataIndex: 'resolved_paths',
width: '35%', width: '35%',
ellipsis: {
showTitle: false
},
render: (text: string, record: ListItem) => ( render: (text: string, record: ListItem) => (
<ResolvedPathColumn record={record} /> <ResolvedPathColumn record={record} />
) )
@@ -588,10 +597,12 @@ const ModelFiles = () => {
{ {
title: intl.formatMessage({ id: 'resources.modelfiles.size' }), title: intl.formatMessage({ id: 'resources.modelfiles.size' }),
dataIndex: 'size', dataIndex: 'size',
width: 100, ellipsis: {
showTitle: false
},
render: (text: string, record: ListItem) => { render: (text: string, record: ListItem) => {
return ( return (
<AutoTooltip ghost maxWidth={100}> <AutoTooltip ghost>
<span>{convertFileSize(record.size, 1, true)}</span> <span>{convertFileSize(record.size, 1, true)}</span>
</AutoTooltip> </AutoTooltip>
); );
@@ -601,9 +612,11 @@ const ModelFiles = () => {
title: intl.formatMessage({ id: 'common.table.createTime' }), title: intl.formatMessage({ id: 'common.table.createTime' }),
dataIndex: 'created_at', dataIndex: 'created_at',
sorter: false, sorter: false,
width: 180, ellipsis: {
showTitle: false
},
render: (text: number) => ( render: (text: number) => (
<AutoTooltip ghost> <AutoTooltip ghost minWidth={20}>
{dayjs(text).format('YYYY-MM-DD HH:mm:ss')} {dayjs(text).format('YYYY-MM-DD HH:mm:ss')}
</AutoTooltip> </AutoTooltip>
) )