feat: Add new components, modify import statements, and update CSS styles

The code changes involve modifications to multiple files, including the removal and replacement of import statements, the addition of new components and files, and changes to initial state, selectors, and CSS styles. The changes are related to settings, themes, components, and store functionality.
This commit is contained in:
canisminor1990
2023-07-18 18:28:12 +08:00
parent 24b6b635d5
commit 9b261db080
22 changed files with 367 additions and 124 deletions

View File

@@ -1,97 +1,109 @@
{
"danger": {
"reset": {
"title": "Reset All Settings",
"desc": "Reset all settings to default values",
"action": "Reset Now",
"confirm": "Confirm reset all settings?",
"currentVersion": "Current Version"
},
"clear": {
"title": "Clear All Data",
"desc": "Clear all chat and settings data",
"action": "Clear Now",
"confirm": "Confirm clear all chat and settings data?"
"confirm": "Are you sure you want to clear all chat and settings data?",
"desc": "Clear all chat and settings data",
"title": "Clear All Data"
},
"reset": {
"action": "Reset Now",
"confirm": "Are you sure you want to reset all settings?",
"currentVersion": "Current Version",
"desc": "Reset all settings to default values",
"title": "Reset All Settings"
}
},
"header": "Settings",
"settingChat": {
"title": "Chat Settings",
"inputTemplate": {
"title": "User Input Template",
"desc": "The latest user message will be filled into this template"
},
"compressThreshold": {
"title": "History Message Length Compression Threshold",
"desc": "When the uncompressed history message exceeds this value, it will be compressed"
"desc": "When the uncompressed chat history exceeds this value, it will be compressed",
"title": "Chat History Compression Threshold"
},
"historyCount": {
"title": "Number of History Messages",
"desc": "Number of history messages carried in each request"
"desc": "Number of chat history messages carried in each request",
"title": "History Message Count"
},
"inputTemplate": {
"desc": "The latest user message will be filled into this template",
"title": "User Input Preprocessing"
},
"maxTokens": {
"title": "Max Tokens per Response",
"desc": "The maximum number of tokens used for each interaction"
"desc": "Maximum number of tokens used for each interaction",
"title": "Reply Limit (max_tokens)"
},
"sendKey": {
"title": "Send Key"
}
},
"title": "Chat Settings"
},
"settingModel": {
"title": "Model Settings",
"frequencyPenalty": {
"desc": "The higher the value, the more likely it is to reduce repeated words",
"title": "Frequency Penalty (frequency_penalty)"
},
"model": {
"title": "Model"
},
"temperature": {
"title": "Randomness (temperature)",
"desc": "The higher the value, the more random the response"
},
"topP": {
"title": "Nucleus Sampling (top_p)",
"desc": "Similar to randomness, but do not change it together with randomness"
},
"presencePenalty": {
"title": "Topic Freshness (presence_penalty)",
"desc": "The higher the value, the more likely it is to expand to new topics"
"desc": "The higher the value, the more likely it is to expand to new topics",
"title": "Topic Freshness (presence_penalty)"
},
"frequencyPenalty": {
"title": "Frequency Penalty (frequency_penalty)",
"desc": "The higher the value, the more likely it is to reduce repeated words"
"temperature": {
"desc": "The higher the value, the more random the reply",
"title": "Randomness (temperature)"
},
"title": "Model Settings",
"topP": {
"desc": "Similar to randomness, but do not change together with randomness",
"title": "Nucleus Sampling (top_p)"
}
},
"settingOpenAI": {
"endpoint": {
"desc": "Must include http(s)://, in addition to the default address",
"title": "API Endpoint"
},
"title": "OpenAI Settings",
"token": {
"title": "API Key",
"desc": "Use your own key to bypass password access restrictions",
"placeholder": "OpenAI API Key"
},
"endpoint": {
"title": "API Endpoint",
"desc": "In addition to the default address, it must include http(s)://"
"desc": "Use your own Key to bypass password access restrictions",
"placeholder": "OpenAI API Key",
"title": "API Key"
}
},
"settingSystem": {
"title": "System Settings",
"accessCode": {
"title": "Access Code",
"desc": "Encryption access has been enabled by the administrator",
"placeholder": "Please enter the access code"
}
"placeholder": "Please enter the access password",
"title": "Access Password"
},
"title": "System Settings"
},
"settingTheme": {
"title": "Theme Settings",
"avatar": {
"title": "Avatar",
"desc": "Supports URL / Base64 / Emoji"
"title": "Avatar"
},
"fontSize": {
"title": "Font Size",
"desc": "Font size of chat content"
"desc": "Font size of chat content",
"title": "Font Size"
},
"lang": {
"name": "Language Settings",
"all": "All Languages"
}
"title": "Language Settings"
},
"neutralColor": {
"desc": "Custom grayscale for different color tendencies",
"title": "Neutral Color"
},
"primaryColor": {
"desc": "Custom theme color",
"title": "Theme Color"
},
"themeMode": {
"auto": "Auto",
"dark": "Dark",
"light": "Light",
"title": "Theme"
},
"title": "Theme Settings"
}
}

View File

@@ -81,7 +81,6 @@
},
"settingTheme": {
"avatar": {
"desc": "支持 URL / Base64 / Emoji 表情符号",
"title": "头像"
},
"fontSize": {
@@ -89,8 +88,21 @@
"title": "字体大小"
},
"lang": {
"all": "所有语言",
"name": "语言设置"
"title": "语言设置"
},
"neutralColor": {
"desc": "不同色彩倾向的灰阶自定义",
"title": "中性色"
},
"primaryColor": {
"desc": "自定义主题色",
"title": "主题色"
},
"themeMode": {
"auto": "自动",
"dark": "深色",
"light": "浅色",
"title": "主题"
},
"title": "主题设置"
}

View File

@@ -0,0 +1,27 @@
import { Avatar, Logo } from '@lobehub/ui';
import { Upload } from 'antd';
import { memo } from 'react';
import { shallow } from 'zustand/shallow';
import { useSettings } from '@/store/settings';
import { createUploadImageHandler } from '@/utils/uploadFIle';
interface AvatarWithUploadProps {
size?: number;
}
export default memo<AvatarWithUploadProps>(({ size = 40 }) => {
const [avatar, setSettings] = useSettings((st) => [st.settings.avatar, st.setSettings], shallow);
const handleUploadAvatar = createUploadImageHandler((avatar) => {
setSettings({ avatar });
});
return (
<div style={{ maxHeight: size, maxWidth: size }}>
<Upload beforeUpload={handleUploadAvatar} itemRender={() => void 0} maxCount={1}>
{avatar ? <Avatar avatar={avatar} size={size} /> : <Logo size={size} />}
</Upload>
</div>
);
});

View File

@@ -1,7 +1,7 @@
import { DraggablePanel } from '@lobehub/ui';
import { createStyles } from 'antd-style';
import isEqual from 'fast-deep-equal';
import { PropsWithChildren, useState } from 'react';
import { PropsWithChildren, memo, useState } from 'react';
import { shallow } from 'zustand/shallow';
import { useSettings } from '@/store/settings';
@@ -14,7 +14,7 @@ export const useStyles = createStyles(({ css, token }) => ({
`,
}));
export default ({ children }: PropsWithChildren) => {
export default memo<PropsWithChildren>(({ children }) => {
const { styles } = useStyles();
const [sessionsWidth, sessionExpandable] = useSettings(
(s) => [s.sessionsWidth, s.sessionExpandable],
@@ -52,4 +52,4 @@ export default ({ children }: PropsWithChildren) => {
{children}
</DraggablePanel>
);
};
});

View File

@@ -1,16 +1,17 @@
import { ActionIcon, Logo, SideNav } from '@lobehub/ui';
import { ActionIcon, SideNav } from '@lobehub/ui';
import { MessageSquare, Settings2, Sticker } from 'lucide-react';
import Router from 'next/router';
import { memo } from 'react';
import { shallow } from 'zustand/shallow';
import AvatarWithUpload from '@/features/AvatarWithUpload';
import { useSettings } from '@/store/settings';
const Sidebar = memo(() => {
export default memo(() => {
const [tab, setTab] = useSettings((s) => [s.sidebarKey, s.switchSideBar], shallow);
return (
<SideNav
avatar={<Logo size={40} />}
avatar={<AvatarWithUpload />}
bottomActions={<ActionIcon icon={Settings2} onClick={() => Router.push('/setting')} />}
style={{ height: '100vh' }}
topActions={
@@ -32,5 +33,3 @@ const Sidebar = memo(() => {
/>
);
});
export default Sidebar;

View File

@@ -1,8 +1,10 @@
import { ThemeProvider } from '@lobehub/ui';
import { ThemeProvider, lobeCustomTheme } from '@lobehub/ui';
import { App, ConfigProvider } from 'antd';
import { useThemeMode } from 'antd-style';
import 'antd/dist/reset.css';
import Zh_CN from 'antd/locale/zh_CN';
import { PropsWithChildren, useEffect } from 'react';
import { PropsWithChildren, useCallback, useEffect } from 'react';
import { shallow } from 'zustand/shallow';
import { useSessionStore } from '@/store/session';
import { useSettings } from '@/store/settings';
@@ -27,14 +29,27 @@ const Layout = ({ children }: PropsWithChildren) => {
};
export default ({ children }: PropsWithChildren) => {
const themeMode = useSettings((s) => s.settings.themeMode, shallow);
const { primaryColor, neutralColor } = useSettings(
(s) => ({ neutralColor: s.settings.neutralColor, primaryColor: s.settings.primaryColor }),
shallow,
);
const { browserPrefers } = useThemeMode();
const isDarkMode = themeMode === 'auto' ? browserPrefers === 'dark' : themeMode === 'dark';
useEffect(() => {
// refs: https://github.com/pmndrs/zustand/blob/main/docs/integrations/persisting-store-data.md#hashydrated
useSessionStore.persist.rehydrate();
useSettings.persist.rehydrate();
}, []);
const genCustomToken: any = useCallback(
() => lobeCustomTheme({ isDarkMode, neutralColor, primaryColor }),
[primaryColor, neutralColor, isDarkMode],
);
return (
<ThemeProvider themeMode={'auto'}>
<ThemeProvider customToken={genCustomToken || {}} themeMode={themeMode}>
<GlobalStyle />
<Layout>{children}</Layout>
</ThemeProvider>

View File

@@ -3,20 +3,20 @@ import LanguageDetector from 'i18next-browser-languagedetector';
import { isArray } from 'lodash-es';
import { initReactI18next } from 'react-i18next';
import type { Namespaces, Resources } from '@/types/locale';
import type { Namespaces } from '@/types/locale';
import resources from './resources';
const getRes = (res: Resources, namespace: Namespaces[]) => {
const newRes: any = {};
for (const [locale, value] of Object.entries(res)) {
newRes[locale] = {};
for (const ns of namespace) {
newRes[locale][ns] = value[ns];
}
}
return newRes;
};
// const getRes = (res: Resources, namespace: Namespaces[]) => {
// const newRes: any = {};
// for (const [locale, value] of Object.entries(res)) {
// newRes[locale] = {};
// for (const ns of namespace) {
// newRes[locale][ns] = value[ns];
// }
// }
// return newRes;
// };
export const createI18nNext = (namespace?: Namespaces[] | Namespaces) => {
const ns: Namespaces[] = namespace
@@ -42,7 +42,8 @@ export const createI18nNext = (namespace?: Namespaces[] | Namespaces) => {
escapeValue: false, // not needed for react as it escapes by default
},
ns,
resources: getRes(resources, ns),
// resources: getRes(resources, ns),
resources,
})
);
};

View File

@@ -81,7 +81,6 @@ export default {
},
settingTheme: {
avatar: {
desc: '支持 URL / Base64 / Emoji 表情符号',
title: '头像',
},
fontSize: {
@@ -89,8 +88,21 @@ export default {
title: '字体大小',
},
lang: {
all: '所有语言',
name: '语言设置',
title: '语言设置',
},
neutralColor: {
desc: '不同色彩倾向的灰阶自定义',
title: '中性色',
},
primaryColor: {
desc: '自定义主题色',
title: '主题色',
},
themeMode: {
auto: '自动',
dark: '深色',
light: '浅色',
title: '主题',
},
title: '主题设置',
},

19
src/locales/options.ts Normal file
View File

@@ -0,0 +1,19 @@
import type { SelectProps } from 'antd';
import type { Locales } from '@/types/locale';
type LocaleOptions = SelectProps['options'] &
{
value: Locales;
}[];
export const options: LocaleOptions = [
{
label: '简体中文',
value: 'zh-CN',
},
{
label: 'English',
value: 'en-US',
},
] as LocaleOptions;

View File

@@ -1,9 +1,7 @@
import common from '../../../locales/en_US/common.json';
import setting from '../../../locales/en_US/setting.json';
const resources = {
common,
setting,
} as const;
export default resources;

View File

@@ -3,11 +3,11 @@ import { PropsWithChildren, memo, useEffect } from 'react';
import { Flexbox } from 'react-layout-kit';
import { shallow } from 'zustand/shallow';
import SideBar from '@/features/SideBar';
import { createI18nNext } from '@/locales/create';
import { useSessionStore } from '@/store/session';
import { useSettings } from '@/store/settings';
import Sidebar from '../Sidebar';
import { Sessions } from './SessionList';
const initI18n = createI18nNext();
@@ -41,7 +41,7 @@ const ChatLayout = memo<PropsWithChildren>(({ children }) => {
return (
<Flexbox horizontal width={'100%'}>
<Sidebar />
<SideBar />
<Sessions />
{children}
</Flexbox>

View File

@@ -1,20 +1,31 @@
import { Form, Input, type ItemGroup } from '@lobehub/ui';
import { Form, type ItemGroup, ThemeSwitch } from '@lobehub/ui';
import { Select, Slider } from 'antd';
import isEqual from 'fast-deep-equal';
import { debounce } from 'lodash-es';
import { Palette } from 'lucide-react';
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { shallow } from 'zustand/shallow';
import AvatarWithUpload from '@/features/AvatarWithUpload';
import { options } from '@/locales/options';
import { settingsSelectors, useSettings } from '@/store/settings';
import { ConfigKeys } from '@/types/exportConfig';
import { ThemeSwatchesNeutral, ThemeSwatchesPrimary } from './ThemeSwatches';
type SettingItemGroup = ItemGroup & {
children: {
name: ConfigKeys;
name?: ConfigKeys;
}[];
};
const SettingForm = memo(() => {
const settings = useSettings(settingsSelectors.currentSettings, isEqual);
const { setThemeMode, setSettings } = useSettings(
(s) => ({ setSettings: s.setSettings, setThemeMode: s.setThemeMode }),
shallow,
);
const { t } = useTranslation('setting');
@@ -22,10 +33,47 @@ const SettingForm = memo(() => {
() => ({
children: [
{
children: <Input />,
desc: t('settingTheme.avatar.desc'),
children: <AvatarWithUpload />,
label: t('settingTheme.avatar.title'),
name: 'avatar',
minWidth: undefined,
},
{
children: (
<ThemeSwitch
labels={{
auto: t('settingTheme.themeMode.auto'),
dark: t('settingTheme.themeMode.dark'),
light: t('settingTheme.themeMode.light'),
}}
onThemeSwitch={setThemeMode}
themeMode={settings.themeMode}
type={'select'}
/>
),
label: t('settingTheme.themeMode.title'),
},
{
children: <Select options={options} />,
label: t('settingTheme.lang.title'),
name: 'language',
},
{
children: <Slider max={18} min={12} />,
desc: t('settingTheme.fontSize.desc'),
label: t('settingTheme.fontSize.title'),
name: 'fontSize',
},
{
children: <ThemeSwatchesPrimary />,
desc: t('settingTheme.primaryColor.desc'),
label: t('settingTheme.primaryColor.title'),
minWidth: undefined,
},
{
children: <ThemeSwatchesNeutral />,
desc: t('settingTheme.neutralColor.desc'),
label: t('settingTheme.neutralColor.title'),
minWidth: undefined,
},
],
icon: Palette,
@@ -35,7 +83,13 @@ const SettingForm = memo(() => {
);
return (
<Form initialValues={settings} items={[theme]} style={{ maxWidth: 1024, width: '100%' }} />
<Form
initialValues={settings}
itemMinWidth="min(30%,200px)"
items={[theme]}
onValuesChange={debounce(setSettings, 100)}
style={{ maxWidth: 1024, width: '100%' }}
/>
);
});

View File

@@ -0,0 +1,33 @@
import {
NeutralColors,
Swatches,
findCustomThemeName,
neutralColors,
neutralColorsSwatches,
} from '@lobehub/ui';
import { memo } from 'react';
import { shallow } from 'zustand/shallow';
import { useSettings } from '@/store/settings';
const ThemeSwatchesNeutral = memo(() => {
const { neutralColor, setSettings } = useSettings(
(s) => ({ neutralColor: s.settings.neutralColor, setSettings: s.setSettings }),
shallow,
);
const handleSelect = (v: any) => {
const name = findCustomThemeName('neutral', v) as NeutralColors;
setSettings({ neutralColor: name || '' });
};
return (
<Swatches
activeColor={neutralColor ? neutralColors[neutralColor] : undefined}
colors={neutralColorsSwatches}
onSelect={handleSelect}
/>
);
});
export default ThemeSwatchesNeutral;

View File

@@ -0,0 +1,33 @@
import {
PrimaryColors,
Swatches,
findCustomThemeName,
primaryColors,
primaryColorsSwatches,
} from '@lobehub/ui';
import { memo } from 'react';
import { shallow } from 'zustand/shallow';
import { useSettings } from '@/store/settings';
const ThemeSwatchesPrimary = memo(() => {
const { primaryColor, setSettings } = useSettings(
(s) => ({ primaryColor: s.settings.primaryColor, setSettings: s.setSettings }),
shallow,
);
const handleSelect = (v: any) => {
const name = findCustomThemeName('primary', v) as PrimaryColors;
setSettings({ primaryColor: name || '' });
};
return (
<Swatches
activeColor={primaryColor ? primaryColors[primaryColor] : undefined}
colors={primaryColorsSwatches}
onSelect={handleSelect}
/>
);
});
export default ThemeSwatchesPrimary;

View File

@@ -0,0 +1,2 @@
export { default as ThemeSwatchesNeutral } from './ThemeSwatchesNeutral';
export { default as ThemeSwatchesPrimary } from './ThemeSwatchesPrimary';

View File

@@ -3,10 +3,10 @@ import { memo, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { Flexbox } from 'react-layout-kit';
import SideBar from '@/features/SideBar';
import { createI18nNext } from '@/locales/create';
import { Sessions } from '@/pages/chat/SessionList';
import Sidebar from '../Sidebar';
import Header from './Header';
import SettingForm from './SettingForm';
@@ -25,7 +25,7 @@ const SettingLayout = memo(() => {
<title>{pageTitle}</title>
</Head>
<Flexbox horizontal width={'100%'}>
<Sidebar />
<SideBar />
<Sessions />
<Flexbox flex={1}>
<Header />

View File

@@ -1,15 +1,17 @@
import { ThemeMode } from 'antd-style';
import { merge } from 'lodash-es';
import type { StateCreator } from 'zustand/vanilla';
import type { ConfigSettings } from '@/types/exportConfig';
import type { SidebarTabKey } from './initialState';
import { SettingsState } from './initialState';
import { GlobalSettingsState } from './initialState';
import type { SettingsStore } from './store';
export interface SettingsAction {
importSettings: (settings: SettingsState) => void;
saveSettings: (settings: ConfigSettings) => void;
setGlobalSettings: (settings: GlobalSettingsState) => void;
setSettings: (settings: { [keys in keyof ConfigSettings]?: any }) => void;
setThemeMode: (themeMode: ThemeMode) => void;
switchSideBar: (key: SidebarTabKey) => void;
}
@@ -19,12 +21,15 @@ export const createSettings: StateCreator<
[],
SettingsAction
> = (set, get) => ({
importSettings: (settings) => {
setGlobalSettings: (settings) => {
set({ ...settings });
},
saveSettings: (settings) => {
setSettings: (settings) => {
set({ settings: merge(get().settings, settings) });
},
setThemeMode: (themeMode) => {
get().setSettings({ themeMode });
},
switchSideBar: (key) => {
set({ sidebarKey: key });
},

View File

@@ -1,22 +1,24 @@
import type { ThemeMode } from 'antd-style';
import type { ConfigSettings } from '@/types/exportConfig';
export type SidebarTabKey = 'chat' | 'market';
export const DEFAULT_SETTINGS: ConfigSettings = {
avatar: '',
fontSize: 14,
language: 'zh-CN',
neutralColor: '',
primaryColor: '',
themeMode: 'auto',
};
export interface SettingsState {
export interface GlobalSettingsState {
inputHeight: number;
sessionExpandable?: boolean;
sessionsWidth: number;
settings: ConfigSettings;
sidebarKey: SidebarTabKey;
themeMode?: ThemeMode;
}
export const initialState: SettingsState = {
export const initialState: GlobalSettingsState = {
inputHeight: 200,
sessionExpandable: true,
sessionsWidth: 320,

View File

@@ -1,9 +1,17 @@
import { merge } from 'lodash-es';
import { DEFAULT_SETTINGS } from '@/store/settings/initialState';
import { SettingsStore } from './store';
const currentSettings = (s: SettingsStore) => s.settings || DEFAULT_SETTINGS;
const currentSettings = (s: SettingsStore) => merge(DEFAULT_SETTINGS, s.settings);
const selecThemeMode = (s: SettingsStore) => ({
setThemeMode: s.setThemeMode,
themeMode: s.settings.themeMode,
});
export const settingsSelectors = {
currentSettings,
selecThemeMode,
};

View File

@@ -1,9 +1,9 @@
import { StateCreator } from 'zustand/vanilla';
import { type SettingsAction, createSettings } from './action';
import { type SettingsState, initialState } from './initialState';
import { type GlobalSettingsState, initialState } from './initialState';
export type SettingsStore = SettingsAction & SettingsState;
export type SettingsStore = SettingsAction & GlobalSettingsState;
export const createStore: StateCreator<SettingsStore, [['zustand/devtools', never]]> = (
...parameters

View File

@@ -1,5 +1,5 @@
import { Theme, css } from 'antd-style';
import { rgba } from 'polished';
import { readableColor } from 'polished';
export default (token: Theme) => css`
.ant-btn {
@@ -10,20 +10,24 @@ export default (token: Theme) => css`
z-index: 1100;
}
.ant-notification .ant-notification-notice.notification-primary-info {
background: ${token.colorPrimary};
box-shadow: 0 6px 16px 0 ${rgba(token.colorPrimary, 0.1)},
0 3px 6px -4px ${rgba(token.colorPrimary, 0.2)},
0 9px 28px 8px ${rgba(token.colorPrimary, 0.1)};
.ant-slider-track,
.ant-tabs-ink-bar,
.ant-switch-checked {
background: ${token.colorPrimary} !important;
}
.anticon {
color: ${token.colorTextLightSolid};
.ant-btn-primary {
color: ${readableColor(token.colorPrimary)};
background: ${token.colorPrimary};
&:hover {
color: ${readableColor(token.colorPrimary)} !important;
background: ${token.colorPrimaryHover} !important;
}
.ant-notification-notice-message {
margin-bottom: 0;
padding-right: 0;
color: ${token.colorTextLightSolid};
&:active {
color: ${readableColor(token.colorPrimaryActive)} !important;
background: ${token.colorPrimaryActive} !important;
}
}
`;

View File

@@ -1,11 +1,18 @@
import type { NeutralColors, PrimaryColors } from '@lobehub/ui';
import { ThemeMode } from 'antd-style';
import { Locales } from './locale';
/**
* 配置设置
*/
export interface ConfigSettings {
/**
* 头像链接
*/
avatar?: string;
fontSize: number;
language: Locales;
neutralColor?: NeutralColors | '';
primaryColor?: PrimaryColors | '';
themeMode: ThemeMode;
}
export type ConfigKeys = keyof ConfigSettings;