feat: Add ImageInfo modules

pull/472/head
canisminor1990 2023-11-28 14:29:21 +08:00
parent c701c9f429
commit d1d079b9d5
45 changed files with 1453 additions and 399 deletions

4
.env Normal file
View File

@ -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

View File

@ -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,
}, },
}, },
], ],

2
.gitignore vendored
View File

@ -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

1
javascript/onig.js Normal file

File diff suppressed because one or more lines are too long

View File

@ -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",

View File

@ -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",

View File

@ -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();
}, []); }, []);

View File

@ -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;

View File

@ -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);

View File

@ -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;

View File

@ -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 (

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -0,0 +1,9 @@
import { ItemGroup } from '@lobehub/ui';
import { WebuiSettingKeys } from '@/store';
export type SettingItemGroup = ItemGroup & {
children: {
name?: WebuiSettingKeys | string;
}[];
};

View File

@ -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 }}
/> />
); );
}); });

View File

@ -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;

View File

@ -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;

View File

@ -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} horizontal> <Flexbox align={'center'} gap={4}>
<a href={homepage} rel="noreferrer" target="_blank"> <Flexbox align={'center'} gap={4} horizontal>
<ActionIcon icon={Book} title="Setting Documents" /> <a href={homepage} rel="noreferrer" target="_blank">
</a> <ActionIcon icon={Book} title="Setting Documents" />
<Space> </a>
{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>
); );
}); });

39
src/hooks/useHighlight.ts Normal file
View File

@ -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;
}
});

43
src/hooks/useObserver.ts Normal file
View File

@ -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);
};

View File

@ -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;

View File

@ -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;

View File

@ -0,0 +1,31 @@
import { Converter } from '@/scripts/formatPrompt';
const formatPrompt = (prompt: string) => {
let newPrompt = prompt.replaceAll('&lt;', '<').replaceAll('&gt;', '>');
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])),
};
};

View File

@ -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;

View File

@ -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;

View File

@ -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);
}
};

View File

@ -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;
}
`,
};
});

View File

@ -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;

View File

@ -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,
}} }}
/> />
)} )}

View File

@ -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;

View File

@ -19,7 +19,7 @@ export const themeConfig: any = (isDarkMode: ThemeAppearance) => {
{ {
scope: 'comma', scope: 'comma',
settings: { settings: {
foreground: colorGreen, foreground: colorScales.gray[type][6],
}, },
}, },
{ {

View File

@ -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>,
);
};

View File

@ -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;

View File

@ -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);
}
};

View File

@ -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"
}

View File

@ -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;
} }
} }
`, `,

View File

@ -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,
);

View File

@ -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,

View File

@ -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;
} }

View File

@ -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: {