diff --git a/src/components/icon-font/index.tsx b/src/components/icon-font/index.tsx index 61a8c717..8d60a63d 100644 --- a/src/components/icon-font/index.tsx +++ b/src/components/icon-font/index.tsx @@ -1,7 +1,7 @@ import { createFromIconfontCN } from '@ant-design/icons'; const IconFont = createFromIconfontCN({ - scriptUrl: '//at.alicdn.com/t/c/font_4613488_m179wc500g.js' + scriptUrl: '//at.alicdn.com/t/c/font_4613488_3n3aubqzylv.js' }); export default IconFont; diff --git a/src/components/short-cuts/index.less b/src/components/short-cuts/index.less new file mode 100644 index 00000000..78f569e2 --- /dev/null +++ b/src/components/short-cuts/index.less @@ -0,0 +1,42 @@ +.short-cuts { + margin-bottom: 10px; + + .ant-table-container .ant-table-content table tr > td { + height: auto; + border-bottom: var(--ant-line-width) var(--ant-line-type) + var(--ant-table-border-color); + } + + .ant-table-container .ant-table-content table { + border-spacing: 0; + + .ant-table-tbody .ant-table-row { + background: transparent; + } + + tr > td:first-child { + border-radius: 0; + } + + tr > td:last-child { + border-radius: 0; + } + + .ant-table-thead > tr > th { + background-color: var(--color-fill-sider); + height: 36px; + padding-block: 0; + } + } + + .ant-table-fixed-header { + // .ant-table-body { + // overflow-y: auto !important; + // } + .ant-table-thead > tr > th { + background-color: var(--color-fill-sider); + height: 36px; + padding-block: 0; + } + } +} diff --git a/src/components/short-cuts/index.tsx b/src/components/short-cuts/index.tsx new file mode 100644 index 00000000..4a2c5a03 --- /dev/null +++ b/src/components/short-cuts/index.tsx @@ -0,0 +1,144 @@ +import { platformCall } from '@/utils'; +import { SearchOutlined } from '@ant-design/icons'; +import { Input, Table, Tag } from 'antd'; +import _ from 'lodash'; +import React from 'react'; +import IconFont from '../icon-font'; +import './index.less'; + +const dataSource = [ + { + scope: 'playground', + command: 'New Message', + keybindingWin: 'Ctrl + N', + keybindingMac: 'N' + }, + { + scope: 'models', + span: { + rowSpan: 3, + colSpan: 1 + }, + command: 'deploy model from Hugging Face', + keybindingWin: 'Ctrl + 1', + keybindingMac: '1' + }, + { + scope: 'models', + command: 'deploy model from Ollama Library', + keybindingWin: 'Ctrl + 2', + keybindingMac: '2', + span: { + rowSpan: 0, + colSpan: 0 + } + }, + { + scope: 'models', + command: '从 Hugging Face 搜索模型', + keybindingWin: 'Ctrl + K', + keybindingMac: 'K', + span: { + rowSpan: 0, + colSpan: 0 + } + } +]; + +const ShortCuts: React.FC<{ intl: any }> = ({ intl }) => { + const platform = platformCall(); + + const [dataList, setDataList] = React.useState(dataSource); + + const columns = [ + { + title: 'Scope', + dataIndex: 'scope', + key: 'scope', + width: 120, + onCell: (row: any, index: number) => { + if (row.span) { + return row.span; + } + return { + rowSpan: 1, + colSpan: 1 + }; + } + }, + { + title: 'Command', + dataIndex: 'command', + key: 'command' + }, + { + title: 'Keybinding', + dataIndex: 'keybinding', + key: 'keybinding', + width: 180, + render: (text: string, row: any) => { + if (platform.isMac) { + return ( + + + {row.keybindingMac} + + ); + } + return {row.keybindingWin}; + } + } + ]; + + const handleInputChange = (e: any) => { + const value = e.target.value; + const list = _.filter(dataSource, (item: any) => { + return ( + item.command.toLowerCase().includes(value.toLowerCase()) || + item.scope.toLowerCase().includes(value.toLowerCase()) + ); + }); + setDataList(list); + }; + + const debounceHandleInputChange = _.debounce(handleInputChange, 300); + return ( +
+

GPUStack 快捷方式

+ + + + } + > + record.command} + columns={columns} + dataSource={dataList} + pagination={false} + >
+
+ ); +}; + +export const modalConfig = { + icon: null, + centered: false, + maskClosable: true, + footer: null, + style: { + top: '10%' + }, + width: 660 +}; + +export default React.memo(ShortCuts); diff --git a/src/config/hotkeys.ts b/src/config/hotkeys.ts index e05010a8..5c6a6714 100644 --- a/src/config/hotkeys.ts +++ b/src/config/hotkeys.ts @@ -1,5 +1,5 @@ export default { - CREATE: ['ctrl+shift+n', 'meta+shift+n'], + CREATE: ['ctrl+alt+n', 'option+meta+n'], SAVE: ['ctrl+s', 'meta+s'], SUBMIT: ['ctrl+enter', 'meta+enter'], SAVEAS: ['ctrl+shift+s', 'meta+shift+s'], diff --git a/src/layouts/index.tsx b/src/layouts/index.tsx index ae66044b..7fd1b115 100644 --- a/src/layouts/index.tsx +++ b/src/layouts/index.tsx @@ -1,6 +1,9 @@ // @ts-nocheck import { userAtom } from '@/atoms/user'; +import ShortCuts, { + modalConfig as ShortCutsConfig +} from '@/components/short-cuts'; import VersionInfo, { modalConfig } from '@/components/version-info'; import { logout } from '@/pages/login/apis'; import { useAccessMarkedRoutes } from '@@/plugin-access'; @@ -55,13 +58,11 @@ const filterRoutes = ( return newRoutes; }; -// 格式化路由 处理因 wrapper 导致的 菜单 path 不一致 const mapRoutes = (routes: IRoute[], role: string) => { if (routes.length === 0) { return []; } return routes.map((route) => { - // 需要 copy 一份, 否则会污染原始数据 const newRoute = { ...route, role }; if (route.originPath) { newRoute.path = route.originPath; @@ -110,6 +111,13 @@ export default (props: any) => { }); }; + const showShortcuts = () => { + Modal.info({ + ...ShortCutsConfig, + content: + }); + }; + const runtimeConfig = { ...initialInfo, logout: async (userInfo) => { @@ -120,10 +128,12 @@ export default (props: any) => { showVersion: () => { return showVersion(); }, + showShortcuts: () => { + return showShortcuts(); + }, notFound: 404 not found }; - // 现在的 layout 及 wrapper 实现是通过父路由的形式实现的, 会导致路由数据多了冗余层级, proLayout 消费时, 无法正确展示菜单, 这里对冗余数据进行过滤操作 const newRoutes = filterRoutes( clientRoutes.filter((route) => route.id === '@@/global-layout'), (route) => { diff --git a/src/layouts/rightRender.tsx b/src/layouts/rightRender.tsx index 5ccb928e..d863e587 100644 --- a/src/layouts/rightRender.tsx +++ b/src/layouts/rightRender.tsx @@ -100,6 +100,11 @@ export function getRightRenderContent(opts: { icon: , label: intl.formatMessage({ id: 'common.button.version' }) } + // { + // key: 'shortcuts', + // icon: , + // label: intl.formatMessage({ id: 'common.button.shortcut' }) + // } ]; const helpMenu = { @@ -142,6 +147,10 @@ export function getRightRenderContent(opts: { // opts.runtimeConfig.showVersion(); opts.runtimeConfig.showVersion(); } + if (item.key === 'shortcuts') { + // opts.runtimeConfig.showShortcuts(); + opts.runtimeConfig.showShortcuts(); + } } })) } diff --git a/src/locales/en-US/common.ts b/src/locales/en-US/common.ts index 88d7821c..bbe1bb31 100644 --- a/src/locales/en-US/common.ts +++ b/src/locales/en-US/common.ts @@ -3,6 +3,7 @@ export default { 'common.button.perview': 'Preview', 'common.button.readonly': 'Readonly', 'common.button.editmode': 'Edit mode', + 'common.button.shortcut': 'Keyboard Shortcut', 'common.button.add': 'Add', 'common.button.login': 'Log In', 'common.button.select': 'Select', diff --git a/src/locales/en-US/users.ts b/src/locales/en-US/users.ts index ddca26d1..0f473e82 100644 --- a/src/locales/en-US/users.ts +++ b/src/locales/en-US/users.ts @@ -1,8 +1,8 @@ export default { 'users.title': 'Users', - 'users.button.create': 'Create User', + 'users.button.create': 'New User', 'users.form.edit': 'Edit User', - 'users.form.create': 'Create User', + 'users.form.create': 'New User', 'users.table.username': 'User Name', 'users.table.role': 'Role', 'users.form.fullname': 'Full Name', @@ -13,7 +13,7 @@ export default { 'users.form.currentpassword': 'Current Password', 'users.form.updatepassword': 'Modify Password', 'users.form.rule.password': - 'Contains uppercase and lowercase letters, numbers, and !@#$%^&*_+, 6 to 12 characters in length, no spaces allowed.', + 'Contains uppercase and lowercase letters, numbers, and special characters(!@#$%^&*_+), 6 to 12 characters in length, no spaces allowed.', 'users.password.uppcase': 'At least one uppercase letter', 'users.password.lowercase': 'At least one lowercase letter', 'users.password.number': 'At least one number', diff --git a/src/locales/zh-CN/common.ts b/src/locales/zh-CN/common.ts index afd69f0b..daeeb424 100644 --- a/src/locales/zh-CN/common.ts +++ b/src/locales/zh-CN/common.ts @@ -3,6 +3,7 @@ export default { 'common.button.perview': '预览', 'common.button.readonly': '只读', 'common.button.editmode': '编辑模式', + 'common.button.shortcut': '快捷键', 'common.button.add': '添加', 'common.button.login': '登录', 'common.button.select': '选择', diff --git a/src/locales/zh-CN/users.ts b/src/locales/zh-CN/users.ts index 97ce2d63..32af2305 100644 --- a/src/locales/zh-CN/users.ts +++ b/src/locales/zh-CN/users.ts @@ -13,7 +13,7 @@ export default { 'users.form.currentpassword': '当前密码', 'users.form.updatepassword': '修改密码', 'users.form.rule.password': - '包含大小写字母、数字和!@#$%^&*_+,6至12个字符,不允许有空格', + '包含大小写字母、数字和特殊字符(!@#$%^&*_+),6至12个字符,不允许有空格', 'users.password.uppcase': '至少包含一个大写字母', 'users.password.lowercase': '至少包含一个小写字母', 'users.password.number': '至少包含一个数字', diff --git a/src/pages/llmodels/components/deploy-modal.tsx b/src/pages/llmodels/components/deploy-modal.tsx index bb3b0c1b..7dcb8ec6 100644 --- a/src/pages/llmodels/components/deploy-modal.tsx +++ b/src/pages/llmodels/components/deploy-modal.tsx @@ -11,6 +11,7 @@ import DataForm from './data-form'; import HFModelFile from './hf-model-file'; import ModelCard from './model-card'; import SearchModel from './search-model'; +import Separator from './separator'; import TitleWrapper from './title-wrapper'; type AddModalProps = { @@ -98,27 +99,33 @@ const AddModal: React.FC = (props) => { >
{props.source === modelSourceMap.huggingface_value && ( - - - +
+ + + + +
)} {props.source === modelSourceMap.huggingface_value && ( - - - - +
+ + + + + +
)} = (props) => { {source === modelSourceMap.huggingface_value && ( {intl.formatMessage({ id: 'models.form.configurations' })} + )} { + return ( +
+ + + {/* */} +
+ ); +}; + +export default Separator; diff --git a/src/pages/llmodels/style/column-wrapper.less b/src/pages/llmodels/style/column-wrapper.less index 8b364a6e..5ba7364c 100644 --- a/src/pages/llmodels/style/column-wrapper.less +++ b/src/pages/llmodels/style/column-wrapper.less @@ -1,6 +1,7 @@ .column-wrapper { flex: 1; - border-left: 1px solid var(--ant-color-split); + position: relative; + // border-left: 1px solid var(--ant-color-split); } .simplebar-scrollbar::before { @@ -11,7 +12,7 @@ flex: 1; display: flex; position: relative; - border-left: 1px solid var(--ant-color-split); + // border-left: 1px solid var(--ant-color-split); .column-wrapper { border-left: none; diff --git a/src/pages/llmodels/style/separator.less b/src/pages/llmodels/style/separator.less new file mode 100644 index 00000000..47d4e304 --- /dev/null +++ b/src/pages/llmodels/style/separator.less @@ -0,0 +1,28 @@ +.separator { + position: relative; + + .shape { + position: absolute; + top: 10px; + left: -10px; + width: 22px; + height: 22px; + border: 2px solid var(--ant-color-split); + transform: rotate(45deg); + border-left: none; + border-bottom: none; + background-color: var(--color-white-1); + z-index: 100; + } + + .shape-s { + position: absolute; + top: 50%; + left: 4px; + width: 30px; + height: 30px; + border: 16px solid transparent; + border-left: 16px solid var(--color-fill-sider); + z-index: 100; + } +} diff --git a/src/pages/login/components/login-form.tsx b/src/pages/login/components/login-form.tsx index f7bf71b7..127c265c 100644 --- a/src/pages/login/components/login-form.tsx +++ b/src/pages/login/components/login-form.tsx @@ -125,7 +125,6 @@ const LoginForm = () => { }); const userInfo = await fetchUserInfo(); setUserInfo(userInfo); - setInitialPassword(values.password); if (values.autoLogin) { await callRememberMe(values); } else { @@ -133,6 +132,8 @@ const LoginForm = () => { } if (!userInfo?.require_password_change) { gotoDefaultPage(userInfo); + } else { + setInitialPassword(encryptPassword(values.password)); } } catch (error) { // to do something diff --git a/src/pages/login/components/password-form.tsx b/src/pages/login/components/password-form.tsx index 2da6499b..6507cebb 100644 --- a/src/pages/login/components/password-form.tsx +++ b/src/pages/login/components/password-form.tsx @@ -4,9 +4,12 @@ import { PasswordReg } from '@/config'; import { GlobalOutlined, LockOutlined } from '@ant-design/icons'; import { SelectLang, history, useIntl } from '@umijs/max'; import { Button, Form, message } from 'antd'; +import CryptoJS from 'crypto-js'; import { useAtom } from 'jotai'; import { updatePassword } from '../apis'; +const CRYPT_TEXT = 'seal'; + const PasswordForm: React.FC = () => { const intl = useIntl(); const [form] = Form.useForm(); @@ -19,12 +22,18 @@ const PasswordForm: React.FC = () => { history.push(pathname); }; + const decryptPassword = (password: string) => { + const bytes = CryptoJS.AES?.decrypt?.(password, CRYPT_TEXT); + const res = bytes.toString(CryptoJS.enc.Utf8); + return res; + }; + const handleSubmit = async (values: any) => { console.log('values', values, form); try { await updatePassword({ new_password: values.new_password, - current_password: initialPassword + current_password: decryptPassword(initialPassword) }); await setUserInfo({