✨ feat: Add ImageInfo modules
parent
c701c9f429
commit
d1d079b9d5
|
|
@ -0,0 +1,4 @@
|
||||||
|
OPENAI_API='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'
|
||||||
|
OPENAI_API_URL='https://openai.canisminor.cc'
|
||||||
|
#SD_HOST='30.183.88.36'
|
||||||
|
#SD_PORT=80
|
||||||
|
|
@ -26,7 +26,6 @@ module.exports = {
|
||||||
'no-undef': 0,
|
'no-undef': 0,
|
||||||
'object-curly-spacing': 0,
|
'object-curly-spacing': 0,
|
||||||
'unicorn/prefer-add-event-listener': 0,
|
'unicorn/prefer-add-event-listener': 0,
|
||||||
'unused-imports/no-unused-imports': 0,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,4 @@ test-output
|
||||||
# add other ignore file below
|
# add other ignore file below
|
||||||
__pycache__
|
__pycache__
|
||||||
/lobe_theme_config.json
|
/lobe_theme_config.json
|
||||||
/javascript/**/*
|
|
||||||
!/javascript/main.js
|
|
||||||
bun.lockb
|
bun.lockb
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -75,12 +75,23 @@
|
||||||
"desc": "Enable the extra network sidebar on the right side"
|
"desc": "Enable the extra network sidebar on the right side"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"tab": {
|
||||||
|
"appearance": "Appearance",
|
||||||
|
"sidebar": "Sidebar",
|
||||||
|
"layout": "Layout",
|
||||||
|
"experimental": "Experimental"
|
||||||
|
},
|
||||||
"group": {
|
"group": {
|
||||||
"extraNetworkSidebar": "Extra Network Sidebar",
|
"extraNetworkSidebar": "Extra Network Sidebar",
|
||||||
"layout": "Layout Settings",
|
"layout": "Layout Settings",
|
||||||
"promptTextarea": "Prompt Textbox",
|
"promptTextarea": "Prompt Textbox",
|
||||||
"quickSettingSidebar": "Quick Setting Sidebar",
|
"quickSettingSidebar": "Quick Setting Sidebar",
|
||||||
"theme": "Theme Settings"
|
"theme": "Theme Settings",
|
||||||
|
"experimental": "Experimental Features"
|
||||||
|
},
|
||||||
|
"imageInfo": {
|
||||||
|
"title": "Image Info Alternative",
|
||||||
|
"desc": "Display better image information in the generated image"
|
||||||
},
|
},
|
||||||
"hideFooter": {
|
"hideFooter": {
|
||||||
"title": "Hide Footer",
|
"title": "Hide Footer",
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@
|
||||||
"ahooks": "^3",
|
"ahooks": "^3",
|
||||||
"antd": "^5",
|
"antd": "^5",
|
||||||
"antd-style": "latest",
|
"antd-style": "latest",
|
||||||
"consola": "^3.2.3",
|
"consola": "^3",
|
||||||
"i18next": "^23",
|
"i18next": "^23",
|
||||||
"i18next-http-backend": "^2",
|
"i18next-http-backend": "^2",
|
||||||
"lodash-es": "^4",
|
"lodash-es": "^4",
|
||||||
|
|
@ -87,14 +87,13 @@
|
||||||
"react-rnd": "^10",
|
"react-rnd": "^10",
|
||||||
"react-tag-input": "^6",
|
"react-tag-input": "^6",
|
||||||
"semver": "^7",
|
"semver": "^7",
|
||||||
"shiki-es": "^0.14",
|
"shikiji": "^0.7",
|
||||||
"swr": "^2",
|
"swr": "^2",
|
||||||
"zustand": "^4.4.1",
|
"zustand": "^4.4.1",
|
||||||
"zustand-utils": "^1.3.1"
|
"zustand-utils": "^1.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/cli": "^18",
|
"@commitlint/cli": "^18",
|
||||||
"@lobehub/i18n-cli": "latest",
|
|
||||||
"@lobehub/lint": "latest",
|
"@lobehub/lint": "latest",
|
||||||
"@types/lodash-es": "^4",
|
"@types/lodash-es": "^4",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
|
|
@ -106,6 +105,7 @@
|
||||||
"@vitejs/plugin-react-swc": "^3",
|
"@vitejs/plugin-react-swc": "^3",
|
||||||
"@vitest/coverage-v8": "latest",
|
"@vitest/coverage-v8": "latest",
|
||||||
"commitlint": "^18",
|
"commitlint": "^18",
|
||||||
|
"dotenv": "^16",
|
||||||
"eslint": "^8",
|
"eslint": "^8",
|
||||||
"fast-deep-equal": "^3",
|
"fast-deep-equal": "^3",
|
||||||
"husky": "^8",
|
"husky": "^8",
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,8 @@ import isEqual from 'fast-deep-equal';
|
||||||
import { memo, useEffect } from 'react';
|
import { memo, useEffect } from 'react';
|
||||||
|
|
||||||
import '@/locales/config';
|
import '@/locales/config';
|
||||||
import { PromptHighlight } from '@/modules/PromptHighlight';
|
import ImageInfo from '@/modules/ImageInfo/page';
|
||||||
|
import PromptHighlight from '@/modules/PromptHighlight/page';
|
||||||
import replaceIcon from '@/scripts/replaceIcon';
|
import replaceIcon from '@/scripts/replaceIcon';
|
||||||
import { selectors, useAppStore } from '@/store';
|
import { selectors, useAppStore } from '@/store';
|
||||||
import GlobalStyle from '@/styles';
|
import GlobalStyle from '@/styles';
|
||||||
|
|
@ -25,10 +26,8 @@ const Index = memo(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (setting.enableHighlight) {
|
if (setting.enableHighlight) PromptHighlight();
|
||||||
PromptHighlight('#txt2img_prompt', '#lobe_txt2img_prompt');
|
if (setting.enableImageInfo) ImageInfo();
|
||||||
PromptHighlight('#img2img_prompt', '#lobe_img2img_prompt');
|
|
||||||
}
|
|
||||||
if (setting.svgIcon) replaceIcon();
|
if (setting.svgIcon) replaceIcon();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,20 @@
|
||||||
import { consola } from 'consola';
|
import { consola } from 'consola';
|
||||||
import { PropsWithChildren, Suspense, memo, useEffect, useState } from 'react';
|
import { PropsWithChildren, Suspense, memo, useEffect, useState } from 'react';
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import { shallow } from 'zustand/shallow';
|
|
||||||
|
|
||||||
import { Loading } from '@/components';
|
import { Loading } from '@/components';
|
||||||
import Layout from '@/layouts';
|
import GlobalLayout from '@/layouts';
|
||||||
import { useAppStore } from '@/store';
|
import { useAppStore } from '@/store';
|
||||||
|
|
||||||
import manifest from './manifest';
|
import manifest from './manifest';
|
||||||
|
|
||||||
export const Layouts = memo<PropsWithChildren>(({ children }) => {
|
export const Layout = memo<PropsWithChildren>(({ children }) => {
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const { setCurrentTab, onInit, storeLoading } = useAppStore(
|
const { setCurrentTab, onInit, storeLoading } = useAppStore((st) => ({
|
||||||
(st) => ({
|
|
||||||
onInit: st.onInit,
|
onInit: st.onInit,
|
||||||
setCurrentTab: st.setCurrentTab,
|
setCurrentTab: st.setCurrentTab,
|
||||||
storeLoading: st.loading,
|
storeLoading: st.loading,
|
||||||
}),
|
}));
|
||||||
shallow,
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onInit();
|
onInit();
|
||||||
|
|
@ -61,9 +57,11 @@ export const Layouts = memo<PropsWithChildren>(({ children }) => {
|
||||||
<meta content="#000000" name="theme-color" />
|
<meta content="#000000" name="theme-color" />
|
||||||
<link href={manifest} rel="manifest" />
|
<link href={manifest} rel="manifest" />
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<Layout>{storeLoading === false && loading === false ? children : <Loading />}</Layout>
|
<GlobalLayout>
|
||||||
|
{storeLoading === false && loading === false ? children : <Loading />}
|
||||||
|
</GlobalLayout>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
export default Layouts;
|
export default Layout;
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,15 @@
|
||||||
import { Tag, TagProps } from 'antd';
|
import { Tag, TagProps } from 'antd';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import semver from 'semver';
|
import semver from 'semver';
|
||||||
import { shallow } from 'zustand/shallow';
|
|
||||||
|
|
||||||
import { homepage } from '@/../package.json';
|
import { homepage } from '@/../package.json';
|
||||||
import { useAppStore } from '@/store';
|
import { useAppStore } from '@/store';
|
||||||
|
|
||||||
const VersionTag = memo<TagProps>((props) => {
|
const VersionTag = memo<TagProps>((props) => {
|
||||||
const { version, latestVersion } = useAppStore(
|
const { version, latestVersion } = useAppStore((st) => ({
|
||||||
(st) => ({ latestVersion: st.latestVersion, version: st.version }),
|
latestVersion: st.latestVersion,
|
||||||
shallow,
|
version: st.version,
|
||||||
);
|
}));
|
||||||
|
|
||||||
const isLatest = semver.gte(version, latestVersion);
|
const isLatest = semver.gte(version, latestVersion);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -304,7 +304,7 @@ export const useStyles = createStyles(
|
||||||
}
|
}
|
||||||
|
|
||||||
#img2img_toprow .interrogate-col {
|
#img2img_toprow .interrogate-col {
|
||||||
flex-flow: column wrap;
|
flex-direction: row !important;
|
||||||
min-width: 100% !important;
|
min-width: 100% !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -374,7 +374,8 @@ export const useStyles = createStyles(
|
||||||
|
|
||||||
padding: 8px !important;
|
padding: 8px !important;
|
||||||
|
|
||||||
font-family: var(--font);
|
font-family: ${token.fontFamilyCode} !important;
|
||||||
|
font-size: 13px !important;
|
||||||
line-height: 1.5 !important;
|
line-height: 1.5 !important;
|
||||||
word-wrap: break-word !important;
|
word-wrap: break-word !important;
|
||||||
white-space: pre-wrap !important;
|
white-space: pre-wrap !important;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { Header as H, Tooltip } from '@lobehub/ui';
|
import { Header as H, Tooltip } from '@lobehub/ui';
|
||||||
import { useTheme } from 'antd-style';
|
import { useTheme } from 'antd-style';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { shallow } from 'zustand/shallow';
|
|
||||||
|
|
||||||
import { Logo } from '@/components';
|
import { Logo } from '@/components';
|
||||||
import { useAppStore } from '@/store';
|
import { useAppStore } from '@/store';
|
||||||
|
|
@ -12,10 +11,10 @@ import Actions from './Actions';
|
||||||
import Nav from './Nav';
|
import Nav from './Nav';
|
||||||
|
|
||||||
const Header = memo<DivProps>(({ children }) => {
|
const Header = memo<DivProps>(({ children }) => {
|
||||||
const { themeMode, version } = useAppStore(
|
const { themeMode, version } = useAppStore((st) => ({
|
||||||
(st) => ({ themeMode: st.themeMode, version: st.version }),
|
themeMode: st.themeMode,
|
||||||
shallow,
|
version: st.version,
|
||||||
);
|
}));
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,181 @@
|
||||||
|
import { Form, Swatches } from '@lobehub/ui';
|
||||||
|
import { Input, Segmented, Select, Switch } from 'antd';
|
||||||
|
import isEqual from 'fast-deep-equal';
|
||||||
|
import { Palette } from 'lucide-react';
|
||||||
|
import { memo, useCallback, useMemo, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { CustomLogo } from '@/components';
|
||||||
|
import { SettingItemGroup } from '@/features/Setting/Form/types';
|
||||||
|
import { type WebuiSetting, selectors, useAppStore } from '@/store';
|
||||||
|
|
||||||
|
import Footer from './Footer';
|
||||||
|
import {
|
||||||
|
type NeutralColor,
|
||||||
|
type PrimaryColor,
|
||||||
|
findCustomThemeName,
|
||||||
|
neutralColors,
|
||||||
|
neutralColorsSwatches,
|
||||||
|
primaryColors,
|
||||||
|
primaryColorsSwatches,
|
||||||
|
} from './data';
|
||||||
|
|
||||||
|
const SettingForm = memo(() => {
|
||||||
|
const setting = useAppStore(selectors.currentSetting, isEqual);
|
||||||
|
const { onSetSetting, localeOptions } = useAppStore((st) => ({
|
||||||
|
localeOptions: st.localeOptions,
|
||||||
|
onSetSetting: st.onSetSetting,
|
||||||
|
}));
|
||||||
|
const [rawSetting, setRawSetting] = useState<WebuiSetting>(setting);
|
||||||
|
const [primaryColor, setPrimaryColor] = useState<PrimaryColor | undefined>(
|
||||||
|
setting.primaryColor || undefined,
|
||||||
|
);
|
||||||
|
const [neutralColor, setNeutralColor] = useState<NeutralColor | undefined>(
|
||||||
|
setting.neutralColor || undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const onFinish = useCallback(
|
||||||
|
(value: WebuiSetting) => {
|
||||||
|
onSetSetting({ ...value, neutralColor, primaryColor });
|
||||||
|
location.reload();
|
||||||
|
},
|
||||||
|
[primaryColor, neutralColor],
|
||||||
|
);
|
||||||
|
|
||||||
|
const theme: SettingItemGroup = useMemo(
|
||||||
|
() => ({
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
children: <Select options={localeOptions} />,
|
||||||
|
desc: t('setting.language.desc'),
|
||||||
|
label: t('setting.language.title'),
|
||||||
|
name: 'i18n',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
children: <Switch />,
|
||||||
|
desc: t('setting.reduceAnimation.desc'),
|
||||||
|
label: t('setting.reduceAnimation.title'),
|
||||||
|
name: 'liteAnimation',
|
||||||
|
valuePropName: 'checked',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
children: (
|
||||||
|
<Swatches
|
||||||
|
activeColor={primaryColor ? primaryColors[primaryColor] : undefined}
|
||||||
|
colors={primaryColorsSwatches}
|
||||||
|
onSelect={(c) => setPrimaryColor(findCustomThemeName('primary', c))}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
desc: t('setting.primaryColor.desc'),
|
||||||
|
label: t('setting.primaryColor.title'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
children: (
|
||||||
|
<Swatches
|
||||||
|
activeColor={neutralColor ? neutralColors[neutralColor] : undefined}
|
||||||
|
colors={neutralColorsSwatches}
|
||||||
|
onSelect={(c) => setNeutralColor(findCustomThemeName('neutral', c))}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
desc: t('setting.neutralColor.desc'),
|
||||||
|
label: t('setting.neutralColor.title'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
children: (
|
||||||
|
<Segmented
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
label: t('brand.lobe'),
|
||||||
|
value: 'lobe',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('brand.kitchen'),
|
||||||
|
value: 'kitchen',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('brand.custom'),
|
||||||
|
value: 'custom',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
desc: t('setting.logoType.desc'),
|
||||||
|
label: t('setting.logoType.title'),
|
||||||
|
name: 'logoType',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
children: <Input />,
|
||||||
|
desc: t('setting.customLogo.desc'),
|
||||||
|
divider: false,
|
||||||
|
hidden: rawSetting.logoType !== 'custom',
|
||||||
|
label: t('setting.customLogo.title'),
|
||||||
|
name: 'logoCustomUrl',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
children: <Input />,
|
||||||
|
desc: t('setting.customTitle.desc'),
|
||||||
|
divider: false,
|
||||||
|
hidden: rawSetting.logoType !== 'custom',
|
||||||
|
label: t('setting.customTitle.title'),
|
||||||
|
name: 'logoCustomTitle',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
children: (
|
||||||
|
<CustomLogo
|
||||||
|
logoCustomTitle={rawSetting.logoCustomTitle}
|
||||||
|
logoCustomUrl={rawSetting.logoCustomUrl}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
divider: false,
|
||||||
|
hidden: rawSetting.logoType !== 'custom',
|
||||||
|
label: t('setting.logoType.preview'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
children: <Switch />,
|
||||||
|
desc: t('setting.svgIcons.desc'),
|
||||||
|
label: t('setting.svgIcons.title'),
|
||||||
|
name: 'svgIcon',
|
||||||
|
valuePropName: 'checked',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
children: <Switch />,
|
||||||
|
desc: t('setting.customFont.desc'),
|
||||||
|
label: t('setting.customFont.title'),
|
||||||
|
name: 'enableWebFont',
|
||||||
|
valuePropName: 'checked',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
children: <Switch />,
|
||||||
|
desc: t('setting.confirmPageUnload.desc'),
|
||||||
|
label: t('setting.confirmPageUnload.title'),
|
||||||
|
name: 'confirmPageUnload',
|
||||||
|
valuePropName: 'checked',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
icon: Palette,
|
||||||
|
title: t('setting.group.theme'),
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
primaryColor,
|
||||||
|
neutralColor,
|
||||||
|
rawSetting.logoType,
|
||||||
|
rawSetting.logoCustomTitle,
|
||||||
|
rawSetting.logoCustomUrl,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form
|
||||||
|
footer={<Footer />}
|
||||||
|
initialValues={setting}
|
||||||
|
items={[theme]}
|
||||||
|
onFinish={onFinish}
|
||||||
|
onValuesChange={(_, v) => setRawSetting(v)}
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default SettingForm;
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
import { Form } from '@lobehub/ui';
|
||||||
|
import { Switch } from 'antd';
|
||||||
|
import isEqual from 'fast-deep-equal';
|
||||||
|
import { Puzzle, TextCursorInput } from 'lucide-react';
|
||||||
|
import { memo, useMemo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import Footer from '@/features/Setting/Form/Footer';
|
||||||
|
import { SettingItemGroup } from '@/features/Setting/Form/types';
|
||||||
|
import { selectors, useAppStore } from '@/store';
|
||||||
|
|
||||||
|
const SettingForm = memo(() => {
|
||||||
|
const setting = useAppStore(selectors.currentSetting, isEqual);
|
||||||
|
const onSetSetting = useAppStore((st) => st.onSetSetting);
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const experimental: SettingItemGroup = useMemo(
|
||||||
|
() => ({
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
children: <Switch />,
|
||||||
|
desc: t('setting.imageInfo.desc'),
|
||||||
|
label: t('setting.imageInfo.title'),
|
||||||
|
name: 'enableImageInfo',
|
||||||
|
valuePropName: 'checked',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
icon: Puzzle,
|
||||||
|
title: t('setting.group.experimental'),
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const promptTextarea: SettingItemGroup = useMemo(
|
||||||
|
() => ({
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
children: <Switch />,
|
||||||
|
desc: t('setting.promptHighlight.desc'),
|
||||||
|
label: t('setting.promptHighlight.title'),
|
||||||
|
name: 'enableHighlight',
|
||||||
|
valuePropName: 'checked',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
children: <Switch />,
|
||||||
|
desc: t('setting.promptEditor.desc'),
|
||||||
|
label: t('setting.promptEditor.title'),
|
||||||
|
name: 'promptEditor',
|
||||||
|
valuePropName: 'checked',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
icon: TextCursorInput,
|
||||||
|
title: t('setting.group.promptTextarea'),
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form
|
||||||
|
footer={<Footer />}
|
||||||
|
initialValues={setting}
|
||||||
|
items={[experimental, promptTextarea]}
|
||||||
|
onFinish={onSetSetting}
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default SettingForm;
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { Button } from 'antd';
|
||||||
|
import { memo, useCallback } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { DEFAULT_SETTING, useAppStore } from '@/store';
|
||||||
|
|
||||||
|
const Footer = memo(() => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const onSetSetting = useAppStore((st) => st.onSetSetting);
|
||||||
|
|
||||||
|
const onReset = useCallback(() => {
|
||||||
|
onSetSetting(DEFAULT_SETTING);
|
||||||
|
location.reload();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button htmlType="button" onClick={onReset} style={{ borderRadius: 4 }}>
|
||||||
|
{t('setting.button.reset')}
|
||||||
|
</Button>
|
||||||
|
<Button htmlType="submit" style={{ borderRadius: 4 }} type="primary">
|
||||||
|
{t('setting.button.submit')}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
export default Footer;
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
import { Form } from '@lobehub/ui';
|
||||||
|
import { Segmented, Switch } from 'antd';
|
||||||
|
import isEqual from 'fast-deep-equal';
|
||||||
|
import { Layout, TextCursorInput } from 'lucide-react';
|
||||||
|
import { memo, useMemo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import Footer from '@/features/Setting/Form/Footer';
|
||||||
|
import { SettingItemGroup } from '@/features/Setting/Form/types';
|
||||||
|
import { selectors, useAppStore } from '@/store';
|
||||||
|
|
||||||
|
const SettingForm = memo(() => {
|
||||||
|
const setting = useAppStore(selectors.currentSetting, isEqual);
|
||||||
|
const onSetSetting = useAppStore((st) => st.onSetSetting);
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const layout: SettingItemGroup = useMemo(
|
||||||
|
() => ({
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
children: <Switch />,
|
||||||
|
desc: t('setting.splitPreviewer.desc'),
|
||||||
|
label: t('setting.splitPreviewer.title'),
|
||||||
|
name: 'layoutSplitPreview',
|
||||||
|
valuePropName: 'checked',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
children: <Switch />,
|
||||||
|
desc: t('setting.hideFooter.desc'),
|
||||||
|
label: t('setting.hideFooter.title'),
|
||||||
|
name: 'layoutHideFooter',
|
||||||
|
valuePropName: 'checked',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
icon: Layout,
|
||||||
|
title: t('setting.group.layout'),
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const promptTextarea: SettingItemGroup = useMemo(
|
||||||
|
() => ({
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
children: (
|
||||||
|
<Segmented
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
label: t('setting.promptDisplayMode.scroll'),
|
||||||
|
value: 'scroll',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('setting.promptDisplayMode.resizable'),
|
||||||
|
value: 'resizable',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
desc: t('setting.promptDisplayMode.desc'),
|
||||||
|
label: t('setting.promptDisplayMode.title'),
|
||||||
|
name: 'promptTextareaType',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
icon: TextCursorInput,
|
||||||
|
title: t('setting.group.promptTextarea'),
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form
|
||||||
|
footer={<Footer />}
|
||||||
|
initialValues={setting}
|
||||||
|
items={[layout, promptTextarea]}
|
||||||
|
onFinish={onSetSetting}
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default SettingForm;
|
||||||
|
|
@ -0,0 +1,142 @@
|
||||||
|
import { Form } from '@lobehub/ui';
|
||||||
|
import { InputNumber, Segmented, Switch } from 'antd';
|
||||||
|
import isEqual from 'fast-deep-equal';
|
||||||
|
import { PanelLeftClose, PanelRightClose } from 'lucide-react';
|
||||||
|
import { memo, useMemo, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import Footer from '@/features/Setting/Form/Footer';
|
||||||
|
import { SettingItemGroup } from '@/features/Setting/Form/types';
|
||||||
|
import { type WebuiSetting, selectors, useAppStore } from '@/store';
|
||||||
|
|
||||||
|
const SettingForm = memo(() => {
|
||||||
|
const setting = useAppStore(selectors.currentSetting, isEqual);
|
||||||
|
const onSetSetting = useAppStore((st) => st.onSetSetting);
|
||||||
|
const [rawSetting, setRawSetting] = useState<WebuiSetting>(setting);
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const quickSettingSidebar: SettingItemGroup = useMemo(
|
||||||
|
() => ({
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
children: <Switch />,
|
||||||
|
desc: t('setting.quickSettingSidebar.enable.desc'),
|
||||||
|
label: t('setting.quickSettingSidebar.enable.title'),
|
||||||
|
name: 'enableSidebar',
|
||||||
|
valuePropName: 'checked',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
children: <Switch />,
|
||||||
|
desc: t('setting.quickSettingSidebar.defaultExpand.desc'),
|
||||||
|
hidden: !rawSetting.enableSidebar,
|
||||||
|
label: t('setting.quickSettingSidebar.defaultExpand.title'),
|
||||||
|
name: 'sidebarExpand',
|
||||||
|
valuePropName: 'checked',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
children: (
|
||||||
|
<Segmented
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
label: t('sidebar.mode.fixed'),
|
||||||
|
value: 'fixed',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('sidebar.mode.float'),
|
||||||
|
value: 'float',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
desc: t('setting.quickSettingSidebar.displayMode.desc'),
|
||||||
|
hidden: !rawSetting.enableSidebar,
|
||||||
|
label: t('setting.quickSettingSidebar.displayMode.title'),
|
||||||
|
name: 'sidebarFixedMode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
children: <InputNumber />,
|
||||||
|
desc: t('setting.quickSettingSidebar.defaultWidth.desc'),
|
||||||
|
hidden: !rawSetting.enableSidebar,
|
||||||
|
label: t('setting.quickSettingSidebar.defaultWidth.title'),
|
||||||
|
name: 'sidebarWidth',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
icon: PanelLeftClose,
|
||||||
|
title: t('setting.group.quickSettingSidebar'),
|
||||||
|
}),
|
||||||
|
[rawSetting.enableSidebar],
|
||||||
|
);
|
||||||
|
|
||||||
|
const extraNetworkSidebar: SettingItemGroup = useMemo(
|
||||||
|
() => ({
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
children: <Switch />,
|
||||||
|
desc: t('setting.extraNetworkSidebar.enable.desc'),
|
||||||
|
label: t('setting.extraNetworkSidebar.enable.title'),
|
||||||
|
name: 'enableExtraNetworkSidebar',
|
||||||
|
valuePropName: 'checked',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
children: (
|
||||||
|
<Segmented
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
label: t('sidebar.mode.fixed'),
|
||||||
|
value: 'fixed',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('sidebar.mode.float'),
|
||||||
|
value: 'float',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
desc: t('setting.extraNetworkSidebar.displayMode.desc'),
|
||||||
|
hidden: !rawSetting.enableExtraNetworkSidebar,
|
||||||
|
label: t('setting.extraNetworkSidebar.displayMode.title'),
|
||||||
|
name: 'extraNetworkFixedMode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
children: <Switch />,
|
||||||
|
desc: t('setting.extraNetworkSidebar.defaultExpand.desc'),
|
||||||
|
hidden: !rawSetting.enableExtraNetworkSidebar,
|
||||||
|
label: t('setting.extraNetworkSidebar.defaultExpand.title'),
|
||||||
|
name: 'extraNetworkSidebarExpand',
|
||||||
|
valuePropName: 'checked',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
children: <InputNumber />,
|
||||||
|
desc: t('setting.extraNetworkSidebar.defaultWidth.desc'),
|
||||||
|
hidden: !rawSetting.enableExtraNetworkSidebar,
|
||||||
|
label: t('setting.extraNetworkSidebar.defaultWidth.title'),
|
||||||
|
name: 'extraNetworkSidebarWidth',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
children: <InputNumber />,
|
||||||
|
desc: t('setting.extraNetworkSidebar.defaultCardSize.desc'),
|
||||||
|
hidden: !rawSetting.enableExtraNetworkSidebar,
|
||||||
|
label: t('setting.extraNetworkSidebar.defaultCardSize.title'),
|
||||||
|
name: 'extraNetworkCardSize',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
icon: PanelRightClose,
|
||||||
|
title: t('setting.group.extraNetworkSidebar'),
|
||||||
|
}),
|
||||||
|
[rawSetting.enableExtraNetworkSidebar],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form
|
||||||
|
footer={<Footer />}
|
||||||
|
initialValues={setting}
|
||||||
|
items={[quickSettingSidebar, extraNetworkSidebar]}
|
||||||
|
onFinish={onSetSetting}
|
||||||
|
onValuesChange={(_, v) => setRawSetting(v)}
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default SettingForm;
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
import {
|
||||||
|
neutralColors as nc,
|
||||||
|
neutralColorsSwatches as ncs,
|
||||||
|
primaryColorsSwatches as pcs,
|
||||||
|
primaryColors as ps,
|
||||||
|
} from '@lobehub/ui';
|
||||||
|
|
||||||
|
import { kitchenNeutral, kitchenPrimary } from '@/styles/kitchenColors';
|
||||||
|
|
||||||
|
export const primaryColors = {
|
||||||
|
kitchen: kitchenPrimary.dark.colorPrimary,
|
||||||
|
...ps,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const primaryColorsSwatches = [primaryColors.kitchen, ...pcs];
|
||||||
|
|
||||||
|
export const neutralColors = {
|
||||||
|
kitchen: kitchenNeutral.dark.colorNeutral,
|
||||||
|
...nc,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const neutralColorsSwatches = [neutralColors.kitchen, ...ncs];
|
||||||
|
|
||||||
|
export const findCustomThemeName = (type: 'primary' | 'neutral', value?: string): any => {
|
||||||
|
if (!value) return '';
|
||||||
|
let res = type === 'primary' ? primaryColors : neutralColors;
|
||||||
|
let result = Object.entries(res).find((item) => {
|
||||||
|
return item[1] === value;
|
||||||
|
});
|
||||||
|
return result === null || result === void 0 ? void 0 : result[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PrimaryColor = keyof typeof primaryColors;
|
||||||
|
|
||||||
|
export type NeutralColor = keyof typeof neutralColors;
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { ItemGroup } from '@lobehub/ui';
|
||||||
|
|
||||||
|
import { WebuiSettingKeys } from '@/store';
|
||||||
|
|
||||||
|
export type SettingItemGroup = ItemGroup & {
|
||||||
|
children: {
|
||||||
|
name?: WebuiSettingKeys | string;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
@ -4,7 +4,6 @@ import isEqual from 'fast-deep-equal';
|
||||||
import { Layout, Palette, PanelLeftClose, PanelRightClose, TextCursorInput } from 'lucide-react';
|
import { Layout, Palette, PanelLeftClose, PanelRightClose, TextCursorInput } from 'lucide-react';
|
||||||
import { memo, useCallback, useMemo, useState } from 'react';
|
import { memo, useCallback, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { shallow } from 'zustand/shallow';
|
|
||||||
|
|
||||||
import { CustomLogo } from '@/components';
|
import { CustomLogo } from '@/components';
|
||||||
import {
|
import {
|
||||||
|
|
@ -24,7 +23,6 @@ import {
|
||||||
primaryColors,
|
primaryColors,
|
||||||
primaryColorsSwatches,
|
primaryColorsSwatches,
|
||||||
} from './data';
|
} from './data';
|
||||||
import { useStyles } from './style';
|
|
||||||
|
|
||||||
type SettingItemGroup = ItemGroup & {
|
type SettingItemGroup = ItemGroup & {
|
||||||
children: {
|
children: {
|
||||||
|
|
@ -34,10 +32,10 @@ type SettingItemGroup = ItemGroup & {
|
||||||
|
|
||||||
const SettingForm = memo(() => {
|
const SettingForm = memo(() => {
|
||||||
const setting = useAppStore(selectors.currentSetting, isEqual);
|
const setting = useAppStore(selectors.currentSetting, isEqual);
|
||||||
const { onSetSetting, localeOptions } = useAppStore(
|
const { onSetSetting, localeOptions } = useAppStore((st) => ({
|
||||||
(st) => ({ localeOptions: st.localeOptions, onSetSetting: st.onSetSetting }),
|
localeOptions: st.localeOptions,
|
||||||
shallow,
|
onSetSetting: st.onSetSetting,
|
||||||
);
|
}));
|
||||||
const [rawSetting, setRawSetting] = useState<WebuiSetting>(setting);
|
const [rawSetting, setRawSetting] = useState<WebuiSetting>(setting);
|
||||||
const [primaryColor, setPrimaryColor] = useState<PrimaryColor | undefined>(
|
const [primaryColor, setPrimaryColor] = useState<PrimaryColor | undefined>(
|
||||||
setting.primaryColor || undefined,
|
setting.primaryColor || undefined,
|
||||||
|
|
@ -45,7 +43,7 @@ const SettingForm = memo(() => {
|
||||||
const [neutralColor, setNeutralColor] = useState<NeutralColor | undefined>(
|
const [neutralColor, setNeutralColor] = useState<NeutralColor | undefined>(
|
||||||
setting.neutralColor || undefined,
|
setting.neutralColor || undefined,
|
||||||
);
|
);
|
||||||
const { styles } = useStyles();
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const onReset = useCallback(() => {
|
const onReset = useCallback(() => {
|
||||||
|
|
@ -363,7 +361,6 @@ const SettingForm = memo(() => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form
|
<Form
|
||||||
className={styles}
|
|
||||||
footer={
|
footer={
|
||||||
<>
|
<>
|
||||||
<Button htmlType="button" onClick={onReset} style={{ borderRadius: 4 }}>
|
<Button htmlType="button" onClick={onReset} style={{ borderRadius: 4 }}>
|
||||||
|
|
@ -378,6 +375,7 @@ const SettingForm = memo(() => {
|
||||||
items={[theme, promptTextarea, layout, quickSettingSidebar, extraNetworkSidebar]}
|
items={[theme, promptTextarea, layout, quickSettingSidebar, extraNetworkSidebar]}
|
||||||
onFinish={onFinish}
|
onFinish={onFinish}
|
||||||
onValuesChange={(_, v) => setRawSetting(v)}
|
onValuesChange={(_, v) => setRawSetting(v)}
|
||||||
|
style={{ flex: 1 }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
import { Icon, List } from '@lobehub/ui';
|
||||||
|
import { createStyles } from 'antd-style';
|
||||||
|
import { type LucideIcon } from 'lucide-react';
|
||||||
|
import { CSSProperties, ReactNode, memo } from 'react';
|
||||||
|
|
||||||
|
const { Item } = List;
|
||||||
|
|
||||||
|
const useStyles = createStyles(({ css, token }) => ({
|
||||||
|
container: css`
|
||||||
|
position: relative;
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: ${token.borderRadius}px;
|
||||||
|
|
||||||
|
div {
|
||||||
|
overflow: visible;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export interface ItemProps {
|
||||||
|
active?: boolean;
|
||||||
|
className?: string;
|
||||||
|
icon: LucideIcon;
|
||||||
|
label: ReactNode;
|
||||||
|
onClick?: () => void;
|
||||||
|
style?: CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SettingItem = memo<ItemProps>(
|
||||||
|
({ label, icon, active = false, style, className, onClick }) => {
|
||||||
|
const { cx, styles } = useStyles();
|
||||||
|
return (
|
||||||
|
<Item
|
||||||
|
active={active}
|
||||||
|
avatar={<Icon icon={icon} size={{ fontSize: 16 }} />}
|
||||||
|
className={cx(styles.container, className)}
|
||||||
|
onClick={onClick}
|
||||||
|
style={style}
|
||||||
|
title={label as string}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export default SettingItem;
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { Brush, FlaskConical, Layout, PanelRight } from 'lucide-react';
|
||||||
|
import { memo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Flexbox } from 'react-layout-kit';
|
||||||
|
|
||||||
|
import Item from './Item';
|
||||||
|
|
||||||
|
export enum SettingsTabs {
|
||||||
|
Appearance = 'appearance',
|
||||||
|
Experimental = 'experimental',
|
||||||
|
Layout = 'layout',
|
||||||
|
Sidebar = 'sidebar',
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SidebarProps {
|
||||||
|
setTab: (tab: SettingsTabs) => void;
|
||||||
|
tab: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Sidebar = memo<SidebarProps>(({ tab, setTab }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const items = [
|
||||||
|
{ icon: Brush, label: t('setting.tab.appearance'), value: SettingsTabs.Appearance },
|
||||||
|
{ icon: Layout, label: t('setting.tab.layout'), value: SettingsTabs.Layout },
|
||||||
|
{ icon: PanelRight, label: t('setting.tab.sidebar'), value: SettingsTabs.Sidebar },
|
||||||
|
{ icon: FlaskConical, label: t('setting.tab.experimental'), value: SettingsTabs.Experimental },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flexbox gap={4}>
|
||||||
|
{items.map(({ value, icon, label }) => (
|
||||||
|
<Item
|
||||||
|
active={tab === value}
|
||||||
|
icon={icon}
|
||||||
|
key={value}
|
||||||
|
label={label}
|
||||||
|
onClick={() => setTab(value)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Flexbox>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default Sidebar;
|
||||||
|
|
@ -1,14 +1,17 @@
|
||||||
import { ActionIcon, Modal, type ModalProps } from '@lobehub/ui';
|
import { ActionIcon, Modal, type ModalProps } from '@lobehub/ui';
|
||||||
import { Space } from 'antd';
|
|
||||||
import { Book } from 'lucide-react';
|
import { Book } from 'lucide-react';
|
||||||
import { memo } from 'react';
|
import { memo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Flexbox } from 'react-layout-kit';
|
import { Flexbox } from 'react-layout-kit';
|
||||||
|
|
||||||
import VersionTag from '@/components/VersionTag';
|
import VersionTag from '@/components/VersionTag';
|
||||||
|
|
||||||
import { homepage } from '../../../package.json';
|
import { homepage } from '../../../package.json';
|
||||||
import SettingForm from './SettingForm';
|
import FormAppearance from './Form/Appearance';
|
||||||
|
import FormExperimental from './Form/Experimental';
|
||||||
|
import FormLayout from './Form/Layout';
|
||||||
|
import FormSidebar from './Form/Sidebar';
|
||||||
|
import Sidebar, { SettingsTabs } from './Sidebar';
|
||||||
|
|
||||||
export interface SettingProps {
|
export interface SettingProps {
|
||||||
onCancel?: ModalProps['onCancel'];
|
onCancel?: ModalProps['onCancel'];
|
||||||
|
|
@ -16,6 +19,7 @@ export interface SettingProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const Setting = memo<SettingProps>(({ open, onCancel }) => {
|
const Setting = memo<SettingProps>(({ open, onCancel }) => {
|
||||||
|
const [tab, setTab] = useState<SettingsTabs>(SettingsTabs.Appearance);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
|
|
@ -23,18 +27,26 @@ const Setting = memo<SettingProps>(({ open, onCancel }) => {
|
||||||
onCancel={onCancel}
|
onCancel={onCancel}
|
||||||
open={open}
|
open={open}
|
||||||
title={
|
title={
|
||||||
|
<Flexbox align={'center'} gap={4}>
|
||||||
<Flexbox align={'center'} gap={4} horizontal>
|
<Flexbox align={'center'} gap={4} horizontal>
|
||||||
<a href={homepage} rel="noreferrer" target="_blank">
|
<a href={homepage} rel="noreferrer" target="_blank">
|
||||||
<ActionIcon icon={Book} title="Setting Documents" />
|
<ActionIcon icon={Book} title="Setting Documents" />
|
||||||
</a>
|
</a>
|
||||||
<Space>
|
|
||||||
{t('modal.themeSetting.title')}
|
{t('modal.themeSetting.title')}
|
||||||
<VersionTag />
|
<VersionTag />
|
||||||
</Space>
|
</Flexbox>
|
||||||
</Flexbox>
|
</Flexbox>
|
||||||
}
|
}
|
||||||
|
width={960}
|
||||||
>
|
>
|
||||||
<SettingForm />
|
<Flexbox gap={16} horizontal>
|
||||||
|
<Sidebar setTab={setTab} tab={tab} />
|
||||||
|
{tab === SettingsTabs.Appearance && <FormAppearance />}
|
||||||
|
{tab === SettingsTabs.Layout && <FormLayout />}
|
||||||
|
{tab === SettingsTabs.Sidebar && <FormSidebar />}
|
||||||
|
{tab === SettingsTabs.Experimental && <FormExperimental />}
|
||||||
|
</Flexbox>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { HighlighterCore, getHighlighterCore } from 'shikiji/core';
|
||||||
|
import { getWasmInlined } from 'shikiji/wasm';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
import prompt from '@/modules/PromptHighlight/features/grammar';
|
||||||
|
import { themeConfig } from '@/modules/PromptHighlight/features/promptTheme';
|
||||||
|
|
||||||
|
let cacheHighlighter: HighlighterCore;
|
||||||
|
|
||||||
|
const initHighlighter = async(): Promise<HighlighterCore> => {
|
||||||
|
let highlighter = cacheHighlighter;
|
||||||
|
|
||||||
|
if (highlighter) return highlighter;
|
||||||
|
|
||||||
|
highlighter = await getHighlighterCore({
|
||||||
|
// @ts-ignore
|
||||||
|
langs: [prompt],
|
||||||
|
loadWasm: getWasmInlined,
|
||||||
|
themes: [themeConfig(true), themeConfig(false)],
|
||||||
|
});
|
||||||
|
|
||||||
|
cacheHighlighter = highlighter;
|
||||||
|
|
||||||
|
return highlighter;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useHighlight = (text: string, isDarkMode: boolean) =>
|
||||||
|
useSWR([isDarkMode ? 'dark' : 'light', text].join('-'), async() => {
|
||||||
|
try {
|
||||||
|
const highlighter = await initHighlighter();
|
||||||
|
const html = highlighter?.codeToHtml(text, {
|
||||||
|
lang: 'prompt',
|
||||||
|
theme: isDarkMode ? 'dark' : 'light',
|
||||||
|
});
|
||||||
|
return html;
|
||||||
|
} catch {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
const observerOptions = {
|
||||||
|
attributes: true,
|
||||||
|
characterData: true,
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useObserver = (
|
||||||
|
selector: string,
|
||||||
|
{ subSelector, valueProp = 'innerHTML' }: { subSelector?: string; valueProp?: string } = {},
|
||||||
|
) => {
|
||||||
|
const [value, setValue] = useState<string>('');
|
||||||
|
useEffect(() => {
|
||||||
|
const observer = new MutationObserver((mutationsList) => {
|
||||||
|
for (const mutation of mutationsList) {
|
||||||
|
if (mutation.type === 'childList' || mutation.type === 'characterData') {
|
||||||
|
if (subSelector) {
|
||||||
|
const info = (mutation.target as any).querySelector(subSelector);
|
||||||
|
setValue(String(info[valueProp]));
|
||||||
|
} else {
|
||||||
|
setValue(String((mutation.target as any)?.innerHTML));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const infoContainer = gradioApp().querySelector(selector);
|
||||||
|
|
||||||
|
if (infoContainer) {
|
||||||
|
observer.observe(infoContainer, observerOptions);
|
||||||
|
const info = subSelector ? infoContainer.querySelector(subSelector) : infoContainer;
|
||||||
|
setValue(String((info as any)?.[valueProp]));
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
observer.disconnect();
|
||||||
|
};
|
||||||
|
}, [selector, subSelector, valueProp]);
|
||||||
|
|
||||||
|
return String(value);
|
||||||
|
};
|
||||||
|
|
@ -9,17 +9,17 @@ import {
|
||||||
import isEqual from 'fast-deep-equal';
|
import isEqual from 'fast-deep-equal';
|
||||||
import qs from 'query-string';
|
import qs from 'query-string';
|
||||||
import { memo, useCallback, useEffect } from 'react';
|
import { memo, useCallback, useEffect } from 'react';
|
||||||
import { shallow } from 'zustand/shallow';
|
|
||||||
|
|
||||||
import { useIsDarkMode } from '@/hooks/useIsDarkMode';
|
import { useIsDarkMode } from '@/hooks/useIsDarkMode';
|
||||||
import { selectors, useAppStore } from '@/store';
|
import { selectors, useAppStore } from '@/store';
|
||||||
import { kitchenNeutral, kitchenPrimary } from '@/styles/kitchenColors';
|
import { kitchenNeutral, kitchenPrimary } from '@/styles/kitchenColors';
|
||||||
|
|
||||||
const Layout = memo<DivProps>(({ children }) => {
|
const GlobalLayout = memo<DivProps>(({ children }) => {
|
||||||
const { onSetThemeMode, themeMode } = useAppStore(
|
const { onSetThemeMode, themeMode } = useAppStore((st) => ({
|
||||||
(st) => ({ onInit: st.onInit, onSetThemeMode: st.onSetThemeMode, themeMode: st.themeMode }),
|
onInit: st.onInit,
|
||||||
shallow,
|
onSetThemeMode: st.onSetThemeMode,
|
||||||
);
|
themeMode: st.themeMode,
|
||||||
|
}));
|
||||||
const setting = useAppStore(selectors.currentSetting, isEqual);
|
const setting = useAppStore(selectors.currentSetting, isEqual);
|
||||||
const isDarkMode = useIsDarkMode();
|
const isDarkMode = useIsDarkMode();
|
||||||
|
|
||||||
|
|
@ -70,4 +70,4 @@ const Layout = memo<DivProps>(({ children }) => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
export default Layout;
|
export default GlobalLayout;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
import { ActionIcon, CopyButton } from '@lobehub/ui';
|
||||||
|
import { createStyles } from 'antd-style';
|
||||||
|
import { ChevronDown, ChevronRight } from 'lucide-react';
|
||||||
|
import { memo, useState } from 'react';
|
||||||
|
import { Flexbox } from 'react-layout-kit';
|
||||||
|
|
||||||
|
import SyntaxHighlighter from '@/modules/PromptHighlight/features/SyntaxHighlighter';
|
||||||
|
import { DivProps } from '@/types';
|
||||||
|
|
||||||
|
const useStyles = createStyles(
|
||||||
|
({ token, css, cx, prefixCls }, type: 'ghost' | 'block' | 'pure') => {
|
||||||
|
const prefix = `${prefixCls}-highlighter`;
|
||||||
|
const buttonHoverCls = `${prefix}-hover-btn`;
|
||||||
|
const langHoverCls = `${prefix}-hover-lang`;
|
||||||
|
|
||||||
|
const typeStylish = css`
|
||||||
|
background-color: ${type === 'block' ? token.colorFillTertiary : 'transparent'};
|
||||||
|
border: 1px solid ${type === 'block' ? 'transparent' : token.colorBorder};
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: ${type === 'block' ? token.colorFillTertiary : token.colorFillQuaternary};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
container: cx(
|
||||||
|
prefix,
|
||||||
|
type !== 'pure' && typeStylish,
|
||||||
|
css`
|
||||||
|
position: relative;
|
||||||
|
overflow: auto;
|
||||||
|
border-radius: ${token.borderRadius}px;
|
||||||
|
transition: background-color 100ms ${token.motionEaseOut};
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.${buttonHoverCls} {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.${langHoverCls} {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.prism-code {
|
||||||
|
background: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: ${type === 'pure' ? 0 : `16px 24px`} !important;
|
||||||
|
background: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
),
|
||||||
|
header: css`
|
||||||
|
padding: 4px 8px;
|
||||||
|
background: ${token.colorFillQuaternary};
|
||||||
|
`,
|
||||||
|
select: css`
|
||||||
|
.${prefixCls}-select-selection-item {
|
||||||
|
min-width: 100px;
|
||||||
|
padding-inline-end: 0 !important;
|
||||||
|
color: ${token.colorTextDescription};
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export interface HighlighterProps extends DivProps {
|
||||||
|
/**
|
||||||
|
* @description The code content to be highlighted
|
||||||
|
*/
|
||||||
|
children: string;
|
||||||
|
/**
|
||||||
|
* @description The language of the code content
|
||||||
|
*/
|
||||||
|
title?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Highlighter = memo<HighlighterProps>(
|
||||||
|
({ children, title = 'Prompt', className, style, ...rest }) => {
|
||||||
|
const [expand, setExpand] = useState(true);
|
||||||
|
const { styles, cx } = useStyles('block');
|
||||||
|
const container = cx(styles.container, className);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={container} data-code-type="highlighter" style={style} {...rest}>
|
||||||
|
<Flexbox align={'center'} className={styles.header} horizontal justify={'space-between'}>
|
||||||
|
<ActionIcon
|
||||||
|
icon={expand ? ChevronDown : ChevronRight}
|
||||||
|
onClick={() => setExpand(!expand)}
|
||||||
|
size={{ blockSize: 24, fontSize: 14, strokeWidth: 3 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ActionIcon size={{ blockSize: 24 }} style={{ width: 'unset' }}>
|
||||||
|
{title}
|
||||||
|
</ActionIcon>
|
||||||
|
<CopyButton
|
||||||
|
content={children}
|
||||||
|
placement="left"
|
||||||
|
size={{ blockSize: 24, fontSize: 14, strokeWidth: 2 }}
|
||||||
|
/>
|
||||||
|
</Flexbox>
|
||||||
|
<div style={expand ? {} : { height: 0, overflow: 'hidden' }}>
|
||||||
|
<SyntaxHighlighter>{children}</SyntaxHighlighter>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Highlighter;
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { Converter } from '@/scripts/formatPrompt';
|
||||||
|
|
||||||
|
const formatPrompt = (prompt: string) => {
|
||||||
|
let newPrompt = prompt.replaceAll('<', '<').replaceAll('>', '>');
|
||||||
|
return Converter.convert(newPrompt);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatInfo = (info: string) => {
|
||||||
|
if (!info) return;
|
||||||
|
if (!info.includes('<br>')) return;
|
||||||
|
const data = info.split('<br>').filter(Boolean);
|
||||||
|
const config = data[2] || data[1];
|
||||||
|
if (!config.includes(',')) return;
|
||||||
|
const clearConfigs = config
|
||||||
|
.split(',')
|
||||||
|
.map((item) => item.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
const configs: any = {};
|
||||||
|
|
||||||
|
for (const item of clearConfigs) {
|
||||||
|
const items = item.split(':');
|
||||||
|
configs[items[0].trim()] = items[1].trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
config: configs,
|
||||||
|
negative: formatPrompt(data[2] ? decodeURI(data[1]).split('Negative prompt: ')[1] : ''),
|
||||||
|
positive: formatPrompt(decodeURI(data[0])),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { CopyButton } from '@lobehub/ui';
|
||||||
|
import { memo, useEffect } from 'react';
|
||||||
|
import { Flexbox } from 'react-layout-kit';
|
||||||
|
|
||||||
|
import { useObserver } from '@/hooks/useObserver';
|
||||||
|
import { formatInfo } from '@/modules/ImageInfo/features/formatInfo';
|
||||||
|
|
||||||
|
import Highlighter from './features/Highlighter';
|
||||||
|
import { useStyles } from './style';
|
||||||
|
|
||||||
|
const Index = memo<{ parentId: string }>(({ parentId }) => {
|
||||||
|
const value = useObserver(`${parentId} .infotext`, { subSelector: 'p' });
|
||||||
|
const { styles, cx } = useStyles();
|
||||||
|
|
||||||
|
const data = formatInfo(value);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const infoContainer = gradioApp().querySelector(`${parentId} .infotext`) as HTMLDivElement;
|
||||||
|
infoContainer.style.display = 'none';
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flexbox gap={4}>
|
||||||
|
{data?.positive && (
|
||||||
|
<Highlighter className={styles.highlight} title={'Positive Prompt'}>
|
||||||
|
{data.positive}
|
||||||
|
</Highlighter>
|
||||||
|
)}
|
||||||
|
{data?.negative && (
|
||||||
|
<Highlighter className={cx(styles.highlight, styles.negative)} title={'Negative Prompt'}>
|
||||||
|
{data.negative}
|
||||||
|
</Highlighter>
|
||||||
|
)}
|
||||||
|
{data?.config && (
|
||||||
|
<Flexbox className={styles.container}>
|
||||||
|
{Object.entries(data.config).map(([key, value]) => (
|
||||||
|
<Flexbox gap={4} horizontal justify={'space-between'} key={key}>
|
||||||
|
<Flexbox className={styles.configTitle} horizontal>
|
||||||
|
{key}:
|
||||||
|
</Flexbox>
|
||||||
|
<Flexbox className={styles.configValue} gap={4} horizontal>
|
||||||
|
{value as string}
|
||||||
|
<CopyButton content={value as string} size={'small'} />
|
||||||
|
</Flexbox>
|
||||||
|
</Flexbox>
|
||||||
|
))}
|
||||||
|
</Flexbox>
|
||||||
|
)}
|
||||||
|
</Flexbox>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default Index;
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { PropsWithChildren, memo } from 'react';
|
||||||
|
|
||||||
|
import GlobalLayout from '@/layouts';
|
||||||
|
import { useAppStore } from '@/store';
|
||||||
|
|
||||||
|
const Layout = memo<PropsWithChildren>(({ children }) => {
|
||||||
|
const loading = useAppStore((st) => st.loading);
|
||||||
|
|
||||||
|
return <GlobalLayout>{loading === false && children}</GlobalLayout>;
|
||||||
|
});
|
||||||
|
|
||||||
|
export default Layout;
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { consola } from 'consola';
|
||||||
|
import { StrictMode, Suspense } from 'react';
|
||||||
|
import { createRoot } from 'react-dom/client';
|
||||||
|
|
||||||
|
import Inedx from './index';
|
||||||
|
import Layout from './layout';
|
||||||
|
|
||||||
|
const ImageInfo = (parentId: string, containerId: string) => {
|
||||||
|
if (document.querySelector(containerId)) return;
|
||||||
|
const settingsDiv = document.createElement('div') as HTMLDivElement;
|
||||||
|
settingsDiv.id = containerId.replace('#', '');
|
||||||
|
|
||||||
|
(gradioApp().querySelector(parentId) as HTMLDivElement).insertBefore(
|
||||||
|
settingsDiv,
|
||||||
|
(gradioApp().querySelector(parentId) as HTMLDivElement).firstChild,
|
||||||
|
);
|
||||||
|
|
||||||
|
createRoot(settingsDiv).render(
|
||||||
|
<StrictMode>
|
||||||
|
<Suspense fallback="loading...">
|
||||||
|
<Layout>
|
||||||
|
<Inedx parentId={parentId} />
|
||||||
|
</Layout>
|
||||||
|
</Suspense>
|
||||||
|
</StrictMode>,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
try {
|
||||||
|
ImageInfo('#html_info_txt2img', '#lobe_html_info_txt2img');
|
||||||
|
ImageInfo('#html_info_img2img', '#lobe_html_info_img2img');
|
||||||
|
consola.success('🤯 [module] inject - ImageInfo');
|
||||||
|
} catch (error) {
|
||||||
|
consola.error('🤯 [module] inject - ImageInfo', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { colors as colorScales } from '@lobehub/ui';
|
||||||
|
import { createStyles } from 'antd-style';
|
||||||
|
|
||||||
|
export const useStyles = createStyles(({ css, token, isDarkMode }) => {
|
||||||
|
const type = isDarkMode ? 'dark' : 'light';
|
||||||
|
const color = isDarkMode ? colorScales.lime[type][9] : colorScales.green[type][10];
|
||||||
|
|
||||||
|
return {
|
||||||
|
configTitle: css`
|
||||||
|
color: ${token.colorTextSecondary};
|
||||||
|
`,
|
||||||
|
configValue: css`
|
||||||
|
color: ${token.colorInfoText};
|
||||||
|
text-align: right;
|
||||||
|
`,
|
||||||
|
container: css`
|
||||||
|
padding: 16px 10px 16px 24px;
|
||||||
|
|
||||||
|
font-family: ${token.fontFamilyCode};
|
||||||
|
font-size: 13px;
|
||||||
|
|
||||||
|
background: ${token.colorFillTertiary};
|
||||||
|
border-radius: ${token.borderRadius}px;
|
||||||
|
`,
|
||||||
|
highlight: css`
|
||||||
|
pre {
|
||||||
|
font-family: ${token.fontFamilyCode} !important;
|
||||||
|
font-size: 13px !important;
|
||||||
|
line-height: 1.5 !important;
|
||||||
|
word-wrap: break-word !important;
|
||||||
|
white-space: pre-wrap !important;
|
||||||
|
vertical-align: bottom !important;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
negative: css`
|
||||||
|
span[style='color:${color.toUpperCase()}'] {
|
||||||
|
color: ${token.colorErrorTextHover} !important;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
@ -1,87 +0,0 @@
|
||||||
import { useScroll, useSize } from 'ahooks';
|
|
||||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
||||||
|
|
||||||
import { useExternalTextareaObserver } from '@/hooks/useExternalTextareaObserver';
|
|
||||||
|
|
||||||
import SyntaxHighlighter from './SyntaxHighlighter';
|
|
||||||
import grammar from './prompt.tmLanguage.json';
|
|
||||||
import { themeConfig } from './promptTheme';
|
|
||||||
import { useStyles } from './style';
|
|
||||||
|
|
||||||
const options: any = {
|
|
||||||
langs: [
|
|
||||||
{
|
|
||||||
aliases: ['prompt'],
|
|
||||||
grammar,
|
|
||||||
id: 'prompt',
|
|
||||||
scopeName: 'source.prompt',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
themes: [themeConfig(true), themeConfig(false)],
|
|
||||||
};
|
|
||||||
|
|
||||||
interface AppProps {
|
|
||||||
parentId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const App = memo<AppProps>(({ parentId }) => {
|
|
||||||
const ref: any = useRef(null);
|
|
||||||
const [prompt, setPrompt] = useState<string>('');
|
|
||||||
const { styles, theme } = useStyles();
|
|
||||||
const nativeTextareaValue = useExternalTextareaObserver(`${parentId} label textarea`);
|
|
||||||
const nativeTextarea = useMemo(
|
|
||||||
() => gradioApp().querySelector(`${parentId} label textarea`) as HTMLTextAreaElement,
|
|
||||||
[parentId],
|
|
||||||
);
|
|
||||||
const size = useSize(nativeTextarea);
|
|
||||||
const scroll = useScroll(nativeTextarea);
|
|
||||||
|
|
||||||
const handlePromptChange = useCallback((event: any) => {
|
|
||||||
setPrompt(event.target.value);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handlePromptResize = useCallback(() => {
|
|
||||||
if (nativeTextarea.clientHeight < nativeTextarea.scrollHeight) {
|
|
||||||
return size?.width === undefined ? '' : size?.width + 6;
|
|
||||||
} else {
|
|
||||||
return size?.width === undefined ? '' : size?.width + 2;
|
|
||||||
}
|
|
||||||
}, [nativeTextarea.clientWidth]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
ref.current.scroll(0, scroll?.top || 0);
|
|
||||||
}, [scroll?.top]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
nativeTextarea.addEventListener('change', handlePromptChange);
|
|
||||||
return () => {
|
|
||||||
nativeTextarea.removeEventListener('change', handlePromptChange);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (theme) {
|
|
||||||
nativeTextarea.style.color = 'transparent';
|
|
||||||
nativeTextarea.style.caretColor = theme.colorSuccess;
|
|
||||||
}
|
|
||||||
}, [theme]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setPrompt(nativeTextareaValue);
|
|
||||||
}, [nativeTextareaValue]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={styles.container}
|
|
||||||
data-code-type="highlighter"
|
|
||||||
ref={ref}
|
|
||||||
style={{ height: size?.height, width: handlePromptResize() }}
|
|
||||||
>
|
|
||||||
<SyntaxHighlighter language="prompt" options={options}>
|
|
||||||
{prompt}
|
|
||||||
</SyntaxHighlighter>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default App;
|
|
||||||
|
|
@ -1,27 +1,17 @@
|
||||||
import { Icon } from '@lobehub/ui';
|
import { Icon } from '@lobehub/ui';
|
||||||
import { useThemeMode } from 'antd-style';
|
import { useThemeMode } from 'antd-style';
|
||||||
import { Loader2 } from 'lucide-react';
|
import { Loader2 } from 'lucide-react';
|
||||||
import { memo, useEffect } from 'react';
|
import { PropsWithChildren, memo } from 'react';
|
||||||
import { Center } from 'react-layout-kit';
|
import { Center } from 'react-layout-kit';
|
||||||
import { type HighlighterOptions } from 'shiki-es';
|
|
||||||
|
|
||||||
import { useStyles } from './style';
|
import { useHighlight } from '@/hooks/useHighlight';
|
||||||
import { useHighlight } from './useHighlight';
|
|
||||||
|
|
||||||
export interface SyntaxHighlighterProps {
|
import { useStyles } from '../style';
|
||||||
children: string;
|
|
||||||
language: string;
|
|
||||||
options?: HighlighterOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SyntaxHighlighter = memo<SyntaxHighlighterProps>(({ children, language, options }) => {
|
const SyntaxHighlighter = memo<PropsWithChildren>(({ children }) => {
|
||||||
const { styles } = useStyles();
|
const { styles } = useStyles();
|
||||||
const { isDarkMode } = useThemeMode();
|
const { isDarkMode } = useThemeMode();
|
||||||
const [codeToHtml, isLoading] = useHighlight((s) => [s.codeToHtml, !s.highlighter]);
|
const { data: codeToHtml, isLoading } = useHighlight(children as string, isDarkMode);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
useHighlight.getState().initHighlighter(options);
|
|
||||||
}, [options]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -31,7 +21,7 @@ const SyntaxHighlighter = memo<SyntaxHighlighterProps>(({ children, language, op
|
||||||
<div
|
<div
|
||||||
className={styles.shiki}
|
className={styles.shiki}
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: codeToHtml(children, language, isDarkMode) || '',
|
__html: codeToHtml as any,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
export const lang = {
|
||||||
|
$schema: 'https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json',
|
||||||
|
fileTypes: ['prompt'],
|
||||||
|
name: 'prompt',
|
||||||
|
patterns: [
|
||||||
|
{
|
||||||
|
match: '[,]',
|
||||||
|
name: 'comma',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
match: '[:|]',
|
||||||
|
name: 'func',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
match: 'AND',
|
||||||
|
name: 'and',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
match: 'BREAK',
|
||||||
|
name: 'break',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
captures: {
|
||||||
|
0: {
|
||||||
|
name: 'model-bracket',
|
||||||
|
},
|
||||||
|
1: {
|
||||||
|
name: 'model-type',
|
||||||
|
},
|
||||||
|
2: {
|
||||||
|
name: 'model-name',
|
||||||
|
},
|
||||||
|
3: {
|
||||||
|
name: 'number',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
match: '<([^:]+):([^:]+):([^>]+)>',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
match: '[<|>]',
|
||||||
|
name: 'model-bracket',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
match: '[(|)|\\[|\\]|{|}]',
|
||||||
|
name: 'bracket',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
match: '(?<!\\w)(\\d*\\.?\\d+|\\.\\d+)(?!\\w)',
|
||||||
|
name: 'number',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
match: '__.*__',
|
||||||
|
name: 'wildcards',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
scopeName: 'source.prompt',
|
||||||
|
};
|
||||||
|
|
||||||
|
const prompt = [lang];
|
||||||
|
|
||||||
|
export default prompt;
|
||||||
|
|
@ -19,7 +19,7 @@ export const themeConfig: any = (isDarkMode: ThemeAppearance) => {
|
||||||
{
|
{
|
||||||
scope: 'comma',
|
scope: 'comma',
|
||||||
settings: {
|
settings: {
|
||||||
foreground: colorGreen,
|
foreground: colorScales.gray[type][6],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -1,32 +1,71 @@
|
||||||
import { StrictMode, Suspense, memo } from 'react';
|
import { useScroll, useSize } from 'ahooks';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
import Layout from '@/layouts';
|
import { useExternalTextareaObserver } from '@/hooks/useExternalTextareaObserver';
|
||||||
import { useAppStore } from '@/store';
|
|
||||||
|
|
||||||
import App from './App';
|
import SyntaxHighlighter from './features/SyntaxHighlighter';
|
||||||
|
import { useStyles } from './style';
|
||||||
|
|
||||||
const Main = memo<{ parentId: string }>(({ parentId }) => {
|
interface AppProps {
|
||||||
const loading = useAppStore((st) => st.loading);
|
parentId: string;
|
||||||
|
}
|
||||||
|
|
||||||
return <Layout>{loading === false && <App parentId={parentId} />}</Layout>;
|
const Index = memo<AppProps>(({ parentId }) => {
|
||||||
|
const ref: any = useRef(null);
|
||||||
|
const [prompt, setPrompt] = useState<string>('');
|
||||||
|
const { styles, theme } = useStyles();
|
||||||
|
const nativeTextareaValue = useExternalTextareaObserver(`${parentId} label textarea`);
|
||||||
|
const nativeTextarea = useMemo(
|
||||||
|
() => gradioApp().querySelector(`${parentId} label textarea`) as HTMLTextAreaElement,
|
||||||
|
[parentId],
|
||||||
|
);
|
||||||
|
const size = useSize(nativeTextarea);
|
||||||
|
const scroll = useScroll(nativeTextarea);
|
||||||
|
|
||||||
|
const handlePromptChange = useCallback((event: any) => {
|
||||||
|
setPrompt(event.target.value);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handlePromptResize = useCallback(() => {
|
||||||
|
if (nativeTextarea.clientHeight < nativeTextarea.scrollHeight) {
|
||||||
|
return size?.width === undefined ? '' : size?.width + 6;
|
||||||
|
} else {
|
||||||
|
return size?.width === undefined ? '' : size?.width + 2;
|
||||||
|
}
|
||||||
|
}, [nativeTextarea.clientWidth]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
ref.current.scroll(0, scroll?.top || 0);
|
||||||
|
}, [scroll?.top]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
nativeTextarea.addEventListener('change', handlePromptChange);
|
||||||
|
return () => {
|
||||||
|
nativeTextarea.removeEventListener('change', handlePromptChange);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (theme) {
|
||||||
|
nativeTextarea.style.color = 'transparent';
|
||||||
|
nativeTextarea.style.caretColor = theme.colorSuccess;
|
||||||
|
}
|
||||||
|
}, [theme]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setPrompt(nativeTextareaValue);
|
||||||
|
}, [nativeTextareaValue]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={styles.container}
|
||||||
|
data-code-type="highlighter"
|
||||||
|
ref={ref}
|
||||||
|
style={{ height: size?.height, width: handlePromptResize() }}
|
||||||
|
>
|
||||||
|
<SyntaxHighlighter>{prompt}</SyntaxHighlighter>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
export const PromptHighlight = (parentId: string, containerId: string) => {
|
export default Index;
|
||||||
if (document.querySelector(containerId)) return;
|
|
||||||
const settingsDiv = document.createElement('div') as HTMLDivElement;
|
|
||||||
settingsDiv.id = containerId.replace('#', '');
|
|
||||||
|
|
||||||
(gradioApp().querySelector(parentId) as HTMLDivElement).insertBefore(
|
|
||||||
settingsDiv,
|
|
||||||
(gradioApp().querySelector(parentId) as HTMLDivElement).firstChild,
|
|
||||||
);
|
|
||||||
|
|
||||||
createRoot(settingsDiv).render(
|
|
||||||
<StrictMode>
|
|
||||||
<Suspense fallback="loading...">
|
|
||||||
<Main parentId={parentId} />
|
|
||||||
</Suspense>
|
|
||||||
</StrictMode>,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { PropsWithChildren, memo } from 'react';
|
||||||
|
|
||||||
|
import GlobalLayout from '@/layouts';
|
||||||
|
import { useAppStore } from '@/store';
|
||||||
|
|
||||||
|
const Layout = memo<PropsWithChildren>(({ children }) => {
|
||||||
|
const loading = useAppStore((st) => st.loading);
|
||||||
|
|
||||||
|
return <GlobalLayout>{loading === false && children}</GlobalLayout>;
|
||||||
|
});
|
||||||
|
|
||||||
|
export default Layout;
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { consola } from 'consola';
|
||||||
|
import { StrictMode, Suspense } from 'react';
|
||||||
|
import { createRoot } from 'react-dom/client';
|
||||||
|
|
||||||
|
import Inedx from './index';
|
||||||
|
import Layout from './layout';
|
||||||
|
|
||||||
|
const PromptHighlight = (parentId: string, containerId: string) => {
|
||||||
|
if (document.querySelector(containerId)) return;
|
||||||
|
const settingsDiv = document.createElement('div') as HTMLDivElement;
|
||||||
|
settingsDiv.id = containerId.replace('#', '');
|
||||||
|
|
||||||
|
(gradioApp().querySelector(parentId) as HTMLDivElement).insertBefore(
|
||||||
|
settingsDiv,
|
||||||
|
(gradioApp().querySelector(parentId) as HTMLDivElement).firstChild,
|
||||||
|
);
|
||||||
|
|
||||||
|
createRoot(settingsDiv).render(
|
||||||
|
<StrictMode>
|
||||||
|
<Suspense fallback="loading...">
|
||||||
|
<Layout>
|
||||||
|
<Inedx parentId={parentId} />
|
||||||
|
</Layout>
|
||||||
|
</Suspense>
|
||||||
|
</StrictMode>,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
try {
|
||||||
|
PromptHighlight('#txt2img_prompt', '#lobe_txt2img_prompt');
|
||||||
|
PromptHighlight('#img2img_prompt', '#lobe_img2img_prompt');
|
||||||
|
consola.success('🤯 [module] inject - PromptHighlight');
|
||||||
|
} catch (error) {
|
||||||
|
consola.error('🤯 [module] inject - PromptHighlight', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -1,58 +0,0 @@
|
||||||
{
|
|
||||||
"$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
|
|
||||||
"fileTypes": ["prompt"],
|
|
||||||
"name": "prompt",
|
|
||||||
"patterns": [
|
|
||||||
{
|
|
||||||
"match": "[,]",
|
|
||||||
"name": "comma"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match": "[:|]",
|
|
||||||
"name": "func"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match": "AND",
|
|
||||||
"name": "and"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match": "BREAK",
|
|
||||||
"name": "break"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match": "<([^:]+):([^:]+):([^>]+)>",
|
|
||||||
"captures": {
|
|
||||||
"0": {
|
|
||||||
"name": "model-bracket"
|
|
||||||
},
|
|
||||||
"1": {
|
|
||||||
"name": "model-type"
|
|
||||||
},
|
|
||||||
"2": {
|
|
||||||
"name": "model-name"
|
|
||||||
},
|
|
||||||
"3": {
|
|
||||||
"name": "number"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match": "[<|>]",
|
|
||||||
"name": "model-bracket"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match": "[(|)|\\[|\\]|{|}]",
|
|
||||||
"name": "bracket"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match": "(?<!\\w)(\\d*\\.?\\d+|\\.\\d+)(?!\\w)",
|
|
||||||
"name": "number"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match": "__.*__",
|
|
||||||
"name": "wildcards"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
|
|
||||||
"scopeName": "source.prompt"
|
|
||||||
}
|
|
||||||
|
|
@ -10,11 +10,11 @@ export const useStyles = createStyles(({ css, token, cx, stylish, prefixCls }) =
|
||||||
padding: calc(8px + var(--input-border-width));
|
padding: calc(8px + var(--input-border-width));
|
||||||
|
|
||||||
pre {
|
pre {
|
||||||
font-family: var(--font) !important;
|
font-family: ${token.fontFamilyCode} !important;
|
||||||
font-size: var(--input-text-size) !important;
|
font-size: 13px !important;
|
||||||
line-height: 1.5 !important;
|
line-height: 1.5 !important;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word !important;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap !important;
|
||||||
vertical-align: bottom !important;
|
vertical-align: bottom !important;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
|
@ -51,7 +51,7 @@ export const useStyles = createStyles(({ css, token, cx, stylish, prefixCls }) =
|
||||||
|
|
||||||
code,
|
code,
|
||||||
code span {
|
code span {
|
||||||
font-family: var(--font) !important;
|
font-family: ${token.fontFamilyCode} !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
|
|
||||||
|
|
@ -1,60 +0,0 @@
|
||||||
import { type Highlighter, type HighlighterOptions, getHighlighter } from 'shiki-es';
|
|
||||||
import { shallow } from 'zustand/shallow';
|
|
||||||
import { createWithEqualityFn } from 'zustand/traditional';
|
|
||||||
|
|
||||||
export const languageMap = [] as const;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @title 代码高亮的存储对象
|
|
||||||
*/
|
|
||||||
interface Store {
|
|
||||||
/**
|
|
||||||
* @title Convert code to HTML string
|
|
||||||
* @param text - The code text
|
|
||||||
* @param language - The language of the code
|
|
||||||
* @param isDarkMode - Whether it's in dark mode or not
|
|
||||||
* @returns HTML string
|
|
||||||
*/
|
|
||||||
codeToHtml: (text: string, language: string, isDarkMode: boolean) => string;
|
|
||||||
/**
|
|
||||||
* @title Highlighter object
|
|
||||||
*/
|
|
||||||
highlighter?: Highlighter;
|
|
||||||
/**
|
|
||||||
* @title Initialize the highlighter object
|
|
||||||
* @returns Initialization promise object
|
|
||||||
*/
|
|
||||||
initHighlighter: (options?: HighlighterOptions) => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useHighlight = createWithEqualityFn<Store>(
|
|
||||||
(set, get) => ({
|
|
||||||
codeToHtml: (text, language, isDarkMode) => {
|
|
||||||
const { highlighter } = get();
|
|
||||||
|
|
||||||
if (!highlighter) return '';
|
|
||||||
|
|
||||||
try {
|
|
||||||
return highlighter?.codeToHtml(text, {
|
|
||||||
lang: language,
|
|
||||||
theme: isDarkMode ? 'dark' : 'light',
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
highlighter: undefined,
|
|
||||||
|
|
||||||
initHighlighter: async(options) => {
|
|
||||||
if (!get().highlighter) {
|
|
||||||
const highlighter = await getHighlighter({
|
|
||||||
langs: options?.langs,
|
|
||||||
themes: options?.themes,
|
|
||||||
});
|
|
||||||
|
|
||||||
set({ highlighter });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
shallow,
|
|
||||||
);
|
|
||||||
|
|
@ -8,6 +8,7 @@ export interface WebuiSetting {
|
||||||
confirmPageUnload: boolean;
|
confirmPageUnload: boolean;
|
||||||
enableExtraNetworkSidebar: boolean;
|
enableExtraNetworkSidebar: boolean;
|
||||||
enableHighlight: boolean;
|
enableHighlight: boolean;
|
||||||
|
enableImageInfo: boolean;
|
||||||
enableSidebar: boolean;
|
enableSidebar: boolean;
|
||||||
enableWebFont: boolean;
|
enableWebFont: boolean;
|
||||||
extraNetworkCardSize: number;
|
extraNetworkCardSize: number;
|
||||||
|
|
@ -37,6 +38,7 @@ export const DEFAULT_SETTING: WebuiSetting = {
|
||||||
confirmPageUnload: false,
|
confirmPageUnload: false,
|
||||||
enableExtraNetworkSidebar: true,
|
enableExtraNetworkSidebar: true,
|
||||||
enableHighlight: false,
|
enableHighlight: false,
|
||||||
|
enableImageInfo: true,
|
||||||
enableSidebar: true,
|
enableSidebar: true,
|
||||||
enableWebFont: true,
|
enableWebFont: true,
|
||||||
extraNetworkCardSize: 86,
|
extraNetworkCardSize: 86,
|
||||||
|
|
|
||||||
|
|
@ -107,6 +107,8 @@ export default (token: Theme) => css`
|
||||||
&#deepbooru {
|
&#deepbooru {
|
||||||
height: auto !important;
|
height: auto !important;
|
||||||
max-height: fit-content !important;
|
max-height: fit-content !important;
|
||||||
|
|
||||||
|
font-size: 14px;
|
||||||
line-height: inherit;
|
line-height: inherit;
|
||||||
white-space: break-spaces;
|
white-space: break-spaces;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,17 @@
|
||||||
import react from '@vitejs/plugin-react-swc';
|
import react from '@vitejs/plugin-react-swc';
|
||||||
|
import dotenv from 'dotenv';
|
||||||
import { resolve } from 'node:path';
|
import { resolve } from 'node:path';
|
||||||
import * as process from 'node:process';
|
import * as process from 'node:process';
|
||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
const isProduction = process.env.NODE_ENV === 'production';
|
const isProduction = process.env.NODE_ENV === 'production';
|
||||||
|
|
||||||
const SD_HOST = '127.0.0.1';
|
const SD_HOST = process.env.SD_HOST || '127.0.0.1';
|
||||||
const SD_PORT = 7860;
|
const SD_PORT = process.env.SD_PORT || 7860;
|
||||||
|
|
||||||
|
console.log(SD_HOST, SD_PORT);
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
base: '/dev',
|
base: '/dev',
|
||||||
build: {
|
build: {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue