✨ 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,
|
||||
'object-curly-spacing': 0,
|
||||
'unicorn/prefer-add-event-listener': 0,
|
||||
'unused-imports/no-unused-imports': 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
|
|||
|
|
@ -42,6 +42,4 @@ test-output
|
|||
# add other ignore file below
|
||||
__pycache__
|
||||
/lobe_theme_config.json
|
||||
/javascript/**/*
|
||||
!/javascript/main.js
|
||||
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"
|
||||
}
|
||||
},
|
||||
"tab": {
|
||||
"appearance": "Appearance",
|
||||
"sidebar": "Sidebar",
|
||||
"layout": "Layout",
|
||||
"experimental": "Experimental"
|
||||
},
|
||||
"group": {
|
||||
"extraNetworkSidebar": "Extra Network Sidebar",
|
||||
"layout": "Layout Settings",
|
||||
"promptTextarea": "Prompt Textbox",
|
||||
"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": {
|
||||
"title": "Hide Footer",
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@
|
|||
"ahooks": "^3",
|
||||
"antd": "^5",
|
||||
"antd-style": "latest",
|
||||
"consola": "^3.2.3",
|
||||
"consola": "^3",
|
||||
"i18next": "^23",
|
||||
"i18next-http-backend": "^2",
|
||||
"lodash-es": "^4",
|
||||
|
|
@ -87,14 +87,13 @@
|
|||
"react-rnd": "^10",
|
||||
"react-tag-input": "^6",
|
||||
"semver": "^7",
|
||||
"shiki-es": "^0.14",
|
||||
"shikiji": "^0.7",
|
||||
"swr": "^2",
|
||||
"zustand": "^4.4.1",
|
||||
"zustand-utils": "^1.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^18",
|
||||
"@lobehub/i18n-cli": "latest",
|
||||
"@lobehub/lint": "latest",
|
||||
"@types/lodash-es": "^4",
|
||||
"@types/node": "^20",
|
||||
|
|
@ -106,6 +105,7 @@
|
|||
"@vitejs/plugin-react-swc": "^3",
|
||||
"@vitest/coverage-v8": "latest",
|
||||
"commitlint": "^18",
|
||||
"dotenv": "^16",
|
||||
"eslint": "^8",
|
||||
"fast-deep-equal": "^3",
|
||||
"husky": "^8",
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ import isEqual from 'fast-deep-equal';
|
|||
import { memo, useEffect } from 'react';
|
||||
|
||||
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 { selectors, useAppStore } from '@/store';
|
||||
import GlobalStyle from '@/styles';
|
||||
|
|
@ -25,10 +26,8 @@ const Index = memo(() => {
|
|||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (setting.enableHighlight) {
|
||||
PromptHighlight('#txt2img_prompt', '#lobe_txt2img_prompt');
|
||||
PromptHighlight('#img2img_prompt', '#lobe_img2img_prompt');
|
||||
}
|
||||
if (setting.enableHighlight) PromptHighlight();
|
||||
if (setting.enableImageInfo) ImageInfo();
|
||||
if (setting.svgIcon) replaceIcon();
|
||||
}, []);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,24 +1,20 @@
|
|||
import { consola } from 'consola';
|
||||
import { PropsWithChildren, Suspense, memo, useEffect, useState } from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
|
||||
import { Loading } from '@/components';
|
||||
import Layout from '@/layouts';
|
||||
import GlobalLayout from '@/layouts';
|
||||
import { useAppStore } from '@/store';
|
||||
|
||||
import manifest from './manifest';
|
||||
|
||||
export const Layouts = memo<PropsWithChildren>(({ children }) => {
|
||||
export const Layout = memo<PropsWithChildren>(({ children }) => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const { setCurrentTab, onInit, storeLoading } = useAppStore(
|
||||
(st) => ({
|
||||
onInit: st.onInit,
|
||||
setCurrentTab: st.setCurrentTab,
|
||||
storeLoading: st.loading,
|
||||
}),
|
||||
shallow,
|
||||
);
|
||||
const { setCurrentTab, onInit, storeLoading } = useAppStore((st) => ({
|
||||
onInit: st.onInit,
|
||||
setCurrentTab: st.setCurrentTab,
|
||||
storeLoading: st.loading,
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
onInit();
|
||||
|
|
@ -61,9 +57,11 @@ export const Layouts = memo<PropsWithChildren>(({ children }) => {
|
|||
<meta content="#000000" name="theme-color" />
|
||||
<link href={manifest} rel="manifest" />
|
||||
</Helmet>
|
||||
<Layout>{storeLoading === false && loading === false ? children : <Loading />}</Layout>
|
||||
<GlobalLayout>
|
||||
{storeLoading === false && loading === false ? children : <Loading />}
|
||||
</GlobalLayout>
|
||||
</Suspense>
|
||||
);
|
||||
});
|
||||
|
||||
export default Layouts;
|
||||
export default Layout;
|
||||
|
|
|
|||
|
|
@ -1,16 +1,15 @@
|
|||
import { Tag, TagProps } from 'antd';
|
||||
import { memo } from 'react';
|
||||
import semver from 'semver';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
|
||||
import { homepage } from '@/../package.json';
|
||||
import { useAppStore } from '@/store';
|
||||
|
||||
const VersionTag = memo<TagProps>((props) => {
|
||||
const { version, latestVersion } = useAppStore(
|
||||
(st) => ({ latestVersion: st.latestVersion, version: st.version }),
|
||||
shallow,
|
||||
);
|
||||
const { version, latestVersion } = useAppStore((st) => ({
|
||||
latestVersion: st.latestVersion,
|
||||
version: st.version,
|
||||
}));
|
||||
|
||||
const isLatest = semver.gte(version, latestVersion);
|
||||
|
||||
|
|
|
|||
|
|
@ -304,7 +304,7 @@ export const useStyles = createStyles(
|
|||
}
|
||||
|
||||
#img2img_toprow .interrogate-col {
|
||||
flex-flow: column wrap;
|
||||
flex-direction: row !important;
|
||||
min-width: 100% !important;
|
||||
}
|
||||
|
||||
|
|
@ -374,7 +374,8 @@ export const useStyles = createStyles(
|
|||
|
||||
padding: 8px !important;
|
||||
|
||||
font-family: var(--font);
|
||||
font-family: ${token.fontFamilyCode} !important;
|
||||
font-size: 13px !important;
|
||||
line-height: 1.5 !important;
|
||||
word-wrap: break-word !important;
|
||||
white-space: pre-wrap !important;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { Header as H, Tooltip } from '@lobehub/ui';
|
||||
import { useTheme } from 'antd-style';
|
||||
import { memo } from 'react';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
|
||||
import { Logo } from '@/components';
|
||||
import { useAppStore } from '@/store';
|
||||
|
|
@ -12,10 +11,10 @@ import Actions from './Actions';
|
|||
import Nav from './Nav';
|
||||
|
||||
const Header = memo<DivProps>(({ children }) => {
|
||||
const { themeMode, version } = useAppStore(
|
||||
(st) => ({ themeMode: st.themeMode, version: st.version }),
|
||||
shallow,
|
||||
);
|
||||
const { themeMode, version } = useAppStore((st) => ({
|
||||
themeMode: st.themeMode,
|
||||
version: st.version,
|
||||
}));
|
||||
const theme = useTheme();
|
||||
|
||||
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 { memo, useCallback, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
|
||||
import { CustomLogo } from '@/components';
|
||||
import {
|
||||
|
|
@ -24,7 +23,6 @@ import {
|
|||
primaryColors,
|
||||
primaryColorsSwatches,
|
||||
} from './data';
|
||||
import { useStyles } from './style';
|
||||
|
||||
type SettingItemGroup = ItemGroup & {
|
||||
children: {
|
||||
|
|
@ -34,10 +32,10 @@ type SettingItemGroup = ItemGroup & {
|
|||
|
||||
const SettingForm = memo(() => {
|
||||
const setting = useAppStore(selectors.currentSetting, isEqual);
|
||||
const { onSetSetting, localeOptions } = useAppStore(
|
||||
(st) => ({ localeOptions: st.localeOptions, onSetSetting: st.onSetSetting }),
|
||||
shallow,
|
||||
);
|
||||
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,
|
||||
|
|
@ -45,7 +43,7 @@ const SettingForm = memo(() => {
|
|||
const [neutralColor, setNeutralColor] = useState<NeutralColor | undefined>(
|
||||
setting.neutralColor || undefined,
|
||||
);
|
||||
const { styles } = useStyles();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const onReset = useCallback(() => {
|
||||
|
|
@ -363,7 +361,6 @@ const SettingForm = memo(() => {
|
|||
|
||||
return (
|
||||
<Form
|
||||
className={styles}
|
||||
footer={
|
||||
<>
|
||||
<Button htmlType="button" onClick={onReset} style={{ borderRadius: 4 }}>
|
||||
|
|
@ -378,6 +375,7 @@ const SettingForm = memo(() => {
|
|||
items={[theme, promptTextarea, layout, quickSettingSidebar, extraNetworkSidebar]}
|
||||
onFinish={onFinish}
|
||||
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 { Space } from 'antd';
|
||||
import { Book } from 'lucide-react';
|
||||
import { memo } from 'react';
|
||||
import { memo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import VersionTag from '@/components/VersionTag';
|
||||
|
||||
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 {
|
||||
onCancel?: ModalProps['onCancel'];
|
||||
|
|
@ -16,6 +19,7 @@ export interface SettingProps {
|
|||
}
|
||||
|
||||
const Setting = memo<SettingProps>(({ open, onCancel }) => {
|
||||
const [tab, setTab] = useState<SettingsTabs>(SettingsTabs.Appearance);
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Modal
|
||||
|
|
@ -23,18 +27,26 @@ const Setting = memo<SettingProps>(({ open, onCancel }) => {
|
|||
onCancel={onCancel}
|
||||
open={open}
|
||||
title={
|
||||
<Flexbox align={'center'} gap={4} horizontal>
|
||||
<a href={homepage} rel="noreferrer" target="_blank">
|
||||
<ActionIcon icon={Book} title="Setting Documents" />
|
||||
</a>
|
||||
<Space>
|
||||
<Flexbox align={'center'} gap={4}>
|
||||
<Flexbox align={'center'} gap={4} horizontal>
|
||||
<a href={homepage} rel="noreferrer" target="_blank">
|
||||
<ActionIcon icon={Book} title="Setting Documents" />
|
||||
</a>
|
||||
|
||||
{t('modal.themeSetting.title')}
|
||||
<VersionTag />
|
||||
</Space>
|
||||
</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>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 qs from 'query-string';
|
||||
import { memo, useCallback, useEffect } from 'react';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
|
||||
import { useIsDarkMode } from '@/hooks/useIsDarkMode';
|
||||
import { selectors, useAppStore } from '@/store';
|
||||
import { kitchenNeutral, kitchenPrimary } from '@/styles/kitchenColors';
|
||||
|
||||
const Layout = memo<DivProps>(({ children }) => {
|
||||
const { onSetThemeMode, themeMode } = useAppStore(
|
||||
(st) => ({ onInit: st.onInit, onSetThemeMode: st.onSetThemeMode, themeMode: st.themeMode }),
|
||||
shallow,
|
||||
);
|
||||
const GlobalLayout = memo<DivProps>(({ children }) => {
|
||||
const { onSetThemeMode, themeMode } = useAppStore((st) => ({
|
||||
onInit: st.onInit,
|
||||
onSetThemeMode: st.onSetThemeMode,
|
||||
themeMode: st.themeMode,
|
||||
}));
|
||||
const setting = useAppStore(selectors.currentSetting, isEqual);
|
||||
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 { useThemeMode } from 'antd-style';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { memo, useEffect } from 'react';
|
||||
import { PropsWithChildren, memo } from 'react';
|
||||
import { Center } from 'react-layout-kit';
|
||||
import { type HighlighterOptions } from 'shiki-es';
|
||||
|
||||
import { useStyles } from './style';
|
||||
import { useHighlight } from './useHighlight';
|
||||
import { useHighlight } from '@/hooks/useHighlight';
|
||||
|
||||
export interface SyntaxHighlighterProps {
|
||||
children: string;
|
||||
language: string;
|
||||
options?: HighlighterOptions;
|
||||
}
|
||||
import { useStyles } from '../style';
|
||||
|
||||
const SyntaxHighlighter = memo<SyntaxHighlighterProps>(({ children, language, options }) => {
|
||||
const SyntaxHighlighter = memo<PropsWithChildren>(({ children }) => {
|
||||
const { styles } = useStyles();
|
||||
const { isDarkMode } = useThemeMode();
|
||||
const [codeToHtml, isLoading] = useHighlight((s) => [s.codeToHtml, !s.highlighter]);
|
||||
|
||||
useEffect(() => {
|
||||
useHighlight.getState().initHighlighter(options);
|
||||
}, [options]);
|
||||
const { data: codeToHtml, isLoading } = useHighlight(children as string, isDarkMode);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -31,7 +21,7 @@ const SyntaxHighlighter = memo<SyntaxHighlighterProps>(({ children, language, op
|
|||
<div
|
||||
className={styles.shiki}
|
||||
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',
|
||||
settings: {
|
||||
foreground: colorGreen,
|
||||
foreground: colorScales.gray[type][6],
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -1,32 +1,71 @@
|
|||
import { StrictMode, Suspense, memo } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { useScroll, useSize } from 'ahooks';
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import Layout from '@/layouts';
|
||||
import { useAppStore } from '@/store';
|
||||
import { useExternalTextareaObserver } from '@/hooks/useExternalTextareaObserver';
|
||||
|
||||
import App from './App';
|
||||
import SyntaxHighlighter from './features/SyntaxHighlighter';
|
||||
import { useStyles } from './style';
|
||||
|
||||
const Main = memo<{ parentId: string }>(({ parentId }) => {
|
||||
const loading = useAppStore((st) => st.loading);
|
||||
interface AppProps {
|
||||
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) => {
|
||||
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>,
|
||||
);
|
||||
};
|
||||
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 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));
|
||||
|
||||
pre {
|
||||
font-family: var(--font) !important;
|
||||
font-size: var(--input-text-size) !important;
|
||||
font-family: ${token.fontFamilyCode} !important;
|
||||
font-size: 13px !important;
|
||||
line-height: 1.5 !important;
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word !important;
|
||||
white-space: pre-wrap !important;
|
||||
vertical-align: bottom !important;
|
||||
}
|
||||
`,
|
||||
|
|
@ -51,7 +51,7 @@ export const useStyles = createStyles(({ css, token, cx, stylish, prefixCls }) =
|
|||
|
||||
code,
|
||||
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;
|
||||
enableExtraNetworkSidebar: boolean;
|
||||
enableHighlight: boolean;
|
||||
enableImageInfo: boolean;
|
||||
enableSidebar: boolean;
|
||||
enableWebFont: boolean;
|
||||
extraNetworkCardSize: number;
|
||||
|
|
@ -37,6 +38,7 @@ export const DEFAULT_SETTING: WebuiSetting = {
|
|||
confirmPageUnload: false,
|
||||
enableExtraNetworkSidebar: true,
|
||||
enableHighlight: false,
|
||||
enableImageInfo: true,
|
||||
enableSidebar: true,
|
||||
enableWebFont: true,
|
||||
extraNetworkCardSize: 86,
|
||||
|
|
|
|||
|
|
@ -107,6 +107,8 @@ export default (token: Theme) => css`
|
|||
&#deepbooru {
|
||||
height: auto !important;
|
||||
max-height: fit-content !important;
|
||||
|
||||
font-size: 14px;
|
||||
line-height: inherit;
|
||||
white-space: break-spaces;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,17 @@
|
|||
import react from '@vitejs/plugin-react-swc';
|
||||
import dotenv from 'dotenv';
|
||||
import { resolve } from 'node:path';
|
||||
import * as process from 'node:process';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
|
||||
const SD_HOST = '127.0.0.1';
|
||||
const SD_PORT = 7860;
|
||||
const SD_HOST = process.env.SD_HOST || '127.0.0.1';
|
||||
const SD_PORT = process.env.SD_PORT || 7860;
|
||||
|
||||
console.log(SD_HOST, SD_PORT);
|
||||
export default defineConfig({
|
||||
base: '/dev',
|
||||
build: {
|
||||
|
|
|
|||
Loading…
Reference in New Issue