feat(prompt): add prompt syntax highlighting

This commit is contained in:
canisminor1990 2023-06-30 22:53:15 +08:00
parent 9ed6da2554
commit b51301df08
27 changed files with 907 additions and 264 deletions

View File

@ -25,7 +25,7 @@
</div> </div>
![cover.webp](https://github.com/canisminor1990/sd-webui-lobe-theme/blob/main/docs/cover.webp) ![cover](https://raw.githubusercontent.com/canisminor1990/sd-webui-lobe-theme/main/docs/cover.webp)
<details> <details>
<summary><kbd>文档目录</kbd></summary> <summary><kbd>文档目录</kbd></summary>
@ -57,8 +57,7 @@
- [x] 🖼️ 可调节画板比例,使生成图像始终置顶 - [x] 🖼️ 可调节画板比例,使生成图像始终置顶
- [x] 📱 移动端友好,针对手机屏幕完成部分优化 - [x] 📱 移动端友好,针对手机屏幕完成部分优化
- [x] 🇨🇳 支持 i18n 并欢迎提交 [PR](https://github.com/canisminor1990/sd-webui-lobe-theme/tree/main/src/i18n/lang) 贡献 - [x] 🇨🇳 支持 i18n 并欢迎提交 [PR](https://github.com/canisminor1990/sd-webui-lobe-theme/tree/main/src/i18n/lang) 贡献
- [ ] 📝 语法高亮的 Prompt 输入框 - [x] 📝 语法高亮的 Prompt 输入框
- [ ] 🆗 i18n 多语言支持
<div align="right"> <div align="right">
@ -92,7 +91,7 @@ git clone "https://github.com/canisminor1990/sd-webui-lobe-theme" extensions/lob
## 🤯 使用说明 ## 🤯 使用说明
![feat_thememode.webp](https://github.com/canisminor1990/sd-webui-lobe-theme/blob/main/docs/feat_thememode.webp) ![feat_thememode](https://raw.githubusercontent.com/canisminor1990/sd-webui-lobe-theme/main/docs/feat_thememode.webp)
#### 亮暗色主题 #### 亮暗色主题
@ -117,7 +116,7 @@ http://localhost:7860/?__theme=dark
</div> </div>
![feat_theme_modify.webp](https://github.com/canisminor1990/sd-webui-lobe-theme/blob/main/docs/feat_theme_modify.webp) ![feat_theme_modify](https://raw.githubusercontent.com/canisminor1990/sd-webui-lobe-theme/main/docs/feat_theme_modify.webp)
#### 主体定制 #### 主体定制
@ -136,7 +135,21 @@ http://localhost:7860/?__theme=dark
</div> </div>
![feat_sidebar.webp](https://github.com/canisminor1990/sd-webui-lobe-theme/blob/main/docs/feat_sidebar.webp) ![feat_highlight](https://raw.githubusercontent.com/canisminor1990/sd-webui-lobe-theme/main/docs/feat_highlight.webp)
![feat_highlight](https://raw.githubusercontent.com/canisminor1990/sd-webui-lobe-theme/main/docs/feat_highlight.webp)
#### Prompt 语法高亮
按 Stable Diffusion 语法规则,自动染色 prompt 显示
<div align="right">
[![][back-to-top]](#readme-top)
</div>
![feat_sidebar](https://raw.githubusercontent.com/canisminor1990/sd-webui-lobe-theme/main/docs/feat_sidebar.webp)
#### 侧边栏定制 #### 侧边栏定制
@ -180,7 +193,7 @@ sd_model_checkpoint, sd_vae, CLIP_stop_at_last_layers, img2img_background_color,
</div> </div>
![feat_mobile_friendly.webp](https://github.com/canisminor1990/sd-webui-lobe-theme/blob/main/docs/feat_mobile_friendly.webp) ![feat_mobile_friendly](https://raw.githubusercontent.com/canisminor1990/sd-webui-lobe-theme/main/docs/feat_mobile_friendly.webp)
#### 移动端适配 #### 移动端适配

View File

@ -25,7 +25,7 @@ English · [简体中文](./README-zh_CN.md) · [Changelog](./CHANGELOG.md) · [
</div> </div>
![cover.webp](https://github.com/canisminor1990/sd-webui-lobe-theme/blob/main/docs/cover.webp) ![cover](https://raw.githubusercontent.com/canisminor1990/sd-webui-lobe-theme/main/docs/cover.webp)
> 📦 After **Version 2.0.0** Kitchen theme was renamed to **Lobe Theme**. The legacy version can be accessed at [sd-webui-kitchen-theme-legacy](https://github.com/canisminor1990/sd-webui-kitchen-theme-legacy) > 📦 After **Version 2.0.0** Kitchen theme was renamed to **Lobe Theme**. The legacy version can be accessed at [sd-webui-kitchen-theme-legacy](https://github.com/canisminor1990/sd-webui-kitchen-theme-legacy)
@ -57,8 +57,7 @@ English · [简体中文](./README-zh_CN.md) · [Changelog](./CHANGELOG.md) · [
- [x] 🖼️ Adjustable canvas ratio, ensuring that generated images are always displayed at the top - [x] 🖼️ Adjustable canvas ratio, ensuring that generated images are always displayed at the top
- [x] 📱 Mobile-friendly, with partial optimization for mobile screens - [x] 📱 Mobile-friendly, with partial optimization for mobile screens
- [x] 🇨🇳 Support i18n and welcome [PR](https://github.com/canisminor1990/sd-webui-lobe-theme/tree/main/src/i18n/lang) contributions - [x] 🇨🇳 Support i18n and welcome [PR](https://github.com/canisminor1990/sd-webui-lobe-theme/tree/main/src/i18n/lang) contributions
- [ ] 📝 Syntax highlighting in the prompt input box - [x] 📝 Syntax highlighting in the prompt input box
- [ ] 🆗 Multilingual support with i18n
<div align="right"> <div align="right">
@ -92,7 +91,7 @@ git clone "https://github.com/canisminor1990/sd-webui-lobe-theme" extensions/lob
## 🤯 Usage ## 🤯 Usage
![feat_thememode.webp](https://github.com/canisminor1990/sd-webui-lobe-theme/blob/main/docs/feat_thememode.webp) ![feat_thememode](https://raw.githubusercontent.com/canisminor1990/sd-webui-lobe-theme/main/docs/feat_thememode.webp)
#### Light and Dark Themes #### Light and Dark Themes
@ -117,7 +116,7 @@ http://localhost:7860/?__theme=dark
</div> </div>
![feat_theme_modify.webp](https://github.com/canisminor1990/sd-webui-lobe-theme/blob/main/docs/feat_theme_modify.webp) ![feat_theme_modify](https://raw.githubusercontent.com/canisminor1990/sd-webui-lobe-theme/main/docs/feat_theme_modify.webp)
#### Theme Customization #### Theme Customization
@ -136,7 +135,19 @@ http://localhost:7860/?__theme=dark
</div> </div>
![feat_sidebar.webp](https://github.com/canisminor1990/sd-webui-lobe-theme/blob/main/docs/feat_sidebar.webp) ![feat_highlight](https://raw.githubusercontent.com/canisminor1990/sd-webui-lobe-theme/main/docs/feat_highlight.webp)
#### Prompt Syntax Highlighting
Automatically colorize prompt display according to the Stable Diffusion syntax rules
<div align="right">
[![][back-to-top]](#readme-top)
</div>
![feat_sidebar](https://raw.githubusercontent.com/canisminor1990/sd-webui-lobe-theme/main/docs/feat_sidebar.webp)
#### Sidebar Customization #### Sidebar Customization
@ -180,7 +191,7 @@ sd_model_checkpoint, sd_vae, CLIP_stop_at_last_layers, img2img_background_color,
</div> </div>
![feat_mobile_friendly.webp](https://github.com/canisminor1990/sd-webui-lobe-theme/blob/main/docs/feat_mobile_friendly.webp) ![feat_mobile_friendly](https://raw.githubusercontent.com/canisminor1990/sd-webui-lobe-theme/main/docs/feat_mobile_friendly.webp)
#### Mobile Adaptation #### Mobile Adaptation

BIN
docs/feat_highlight.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

File diff suppressed because one or more lines are too long

View File

@ -58,7 +58,6 @@
] ]
}, },
"devDependencies": { "devDependencies": {
"@ant-design/icons": "^5",
"@babel/plugin-syntax-import-assertions": "^7", "@babel/plugin-syntax-import-assertions": "^7",
"@commitlint/cli": "^17", "@commitlint/cli": "^17",
"@giscus/react": "^2", "@giscus/react": "^2",
@ -75,21 +74,16 @@
"ahooks": "^3", "ahooks": "^3",
"antd": "^5", "antd": "^5",
"antd-style": "latest", "antd-style": "latest",
"babel-plugin-styled-components": "^2",
"browserslist": "^4",
"commitlint": "^17", "commitlint": "^17",
"concurrently": "^8", "concurrently": "^8",
"css-minimizer-webpack-plugin": "^5",
"eslint": "^8", "eslint": "^8",
"fast-deep-equal": "^3", "fast-deep-equal": "^3",
"husky": "^8", "husky": "^8",
"i18next": "^23", "i18next": "^23",
"lightningcss": "^1",
"lint-staged": "^13", "lint-staged": "^13",
"lodash-es": "^4", "lodash-es": "^4",
"lucide-react": "latest", "lucide-react": "latest",
"lucide-static": "latest", "lucide-static": "latest",
"object-to-css-variables": "^0",
"polished": "^4", "polished": "^4",
"prettier": "^2", "prettier": "^2",
"query-string": "^8", "query-string": "^8",
@ -99,11 +93,13 @@
"react-i18next": "^13", "react-i18next": "^13",
"react-layout-kit": "^1", "react-layout-kit": "^1",
"react-rnd": "^10", "react-rnd": "^10",
"react-simple-code-editor": "^0",
"react-tag-input": "^6", "react-tag-input": "^6",
"remark": "^14", "remark": "^14",
"remark-cli": "^11", "remark-cli": "^11",
"rollup-plugin-terser": "^7", "rollup-plugin-terser": "^7",
"semantic-release": "^21", "semantic-release": "^21",
"shiki-es": "^0",
"styled-components": "latest", "styled-components": "latest",
"stylelint": "^15", "stylelint": "^15",
"typescript": "^5", "typescript": "^5",

View File

@ -19,8 +19,8 @@ const App = memo(() => {
); );
useEffect(() => { useEffect(() => {
onInit();
console.time('🤯 Lobe Theme loading'); console.time('🤯 Lobe Theme loading');
onInit();
onUiLoaded(() => { onUiLoaded(() => {
setLoading(false); setLoading(false);
console.timeEnd('🤯 Lobe Theme loading'); console.timeEnd('🤯 Lobe Theme loading');
@ -64,6 +64,7 @@ const App = memo(() => {
<meta content="#000000" name="msapplication-TileColor" /> <meta content="#000000" name="msapplication-TileColor" />
<meta content="#000000" name="theme-color" /> <meta content="#000000" name="theme-color" />
</Helmet> </Helmet>
{!storeLoading && <Layout>{loading ? <Loading /> : <Index />}</Layout>} {!storeLoading && <Layout>{loading ? <Loading /> : <Index />}</Layout>}
</Suspense> </Suspense>
); );

View File

@ -0,0 +1,48 @@
import { type HighlighterProps, Icon } from '@lobehub/ui';
import { Loader2 } from 'lucide-react';
import { memo, useEffect } from 'react';
import { Center } from 'react-layout-kit';
import { shallow } from 'zustand/shallow';
import { useHighlight } from '@/hooks/useHighlight';
import { useStyles } from './style';
export type SyntaxHighlighterProps = Pick<HighlighterProps, 'children' | 'theme'>;
const SyntaxHighlighter = memo<SyntaxHighlighterProps>(({ theme, children }) => {
const { styles } = useStyles();
const [codeToHtml, isLoading] = useHighlight((s) => [s.codeToHtml, !s.highlighter], shallow);
useEffect(() => {
useHighlight.getState().initHighlighter();
}, []);
return (
<>
{isLoading ? (
<div className={styles.shiki}>
<pre>
<code>{children}</code>
</pre>
</div>
) : (
<div
className={styles.shiki}
dangerouslySetInnerHTML={{
__html: codeToHtml(children, 'prompt', theme === 'dark') || '',
}}
/>
)}
{isLoading && (
<Center className={styles.loading} gap={8} horizontal>
<Icon icon={Loader2} spin />
Highlighting...
</Center>
)}
</>
);
});
export default SyntaxHighlighter;

View File

@ -0,0 +1,50 @@
{
"$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": "<([^:]+):([^:]+):([^>]+)>",
"captures": {
"0": {
"name": "model-bracket"
},
"1": {
"name": "model-type"
},
"2": {
"name": "model-name"
},
"3": {
"name": "number"
}
}
},
{
"match": "[<|>]",
"name": "model-bracket"
},
{
"match": "[(|)|\\[|\\]|{|}]",
"name": "bracket"
},
{
"match": "\\d+(\\.\\d+)?",
"name": "number"
}
],
"scopeName": "source.prompt"
}

View File

@ -0,0 +1,72 @@
import { colors as colorScales } from '@lobehub/ui';
import { ThemeAppearance } from 'antd-style';
export const themeConfig: any = (isDarkMode: ThemeAppearance) => {
const type = isDarkMode ? 'dark' : 'light';
const colorTextTertiary = isDarkMode ? colorScales.gray[type][6] : colorScales.gray[type][7];
const colorOrange = isDarkMode ? colorScales.gold[type][9] : colorScales.orange[type][9];
const colorGreen = isDarkMode ? colorScales.lime[type][9] : colorScales.green[type][10];
const colorBlue = isDarkMode ? colorScales.blue[type][9] : colorScales.geekblue[type][8];
const colorPurple = isDarkMode ? colorScales.purple[type][10] : colorScales.purple[type][9];
return {
colors: {
'editor.foreground': colorGreen,
},
name: type,
tokenColors: [
{
scope: 'comma',
settings: {
foreground: colorTextTertiary,
},
},
{
scope: 'func',
settings: {
foreground: colorBlue,
},
},
{
scope: 'and',
settings: {
fontStyle: 'bold',
foreground: colorBlue,
},
},
{
scope: 'bracket',
settings: {
foreground: colorBlue,
},
},
{
scope: 'model-type',
settings: {
fontStyle: 'italic',
foreground: colorOrange,
},
},
{
scope: 'model-name',
settings: {
fontStyle: 'bold',
foreground: colorOrange,
},
},
{
scope: 'model-bracket',
settings: {
foreground: colorOrange,
},
},
{
scope: 'number',
settings: {
foreground: colorPurple,
},
},
],
type,
};
};

View File

@ -0,0 +1,45 @@
import { createStyles } from 'antd-style';
export const useStyles = createStyles(({ css, token, cx, prefixCls, stylish }) => {
const prefix = `${prefixCls}-highlighter`;
return {
loading: cx(
stylish.blur,
css`
position: absolute;
z-index: 10;
top: 0;
right: 0;
display: flex;
align-items: center;
justify-content: center;
height: 34px;
padding: 0 8px;
font-family: ${token.fontFamilyCode};
color: ${token.colorTextTertiary};
border-radius: ${token.borderRadius};
`,
),
prism: css`
pre {
overflow: auto;
font-family: ${token.fontFamilyCode} !important;
}
`,
shiki: cx(
`${prefix}-shiki`,
css`
.shiki {
overflow-x: auto;
background: none !important;
}
`,
),
};
});

View File

@ -5,3 +5,4 @@ export { default as SidebarBody } from './Sidebar/SidebarBody';
export { default as SidebarContainer } from './Sidebar/SidebarContainer'; export { default as SidebarContainer } from './Sidebar/SidebarContainer';
export { default as SidebarFooter } from './Sidebar/SidebarFooter'; export { default as SidebarFooter } from './Sidebar/SidebarFooter';
export { default as SidebarHeader, type SidebarHeaderProps } from './Sidebar/SidebarHeader'; export { default as SidebarHeader, type SidebarHeaderProps } from './Sidebar/SidebarHeader';
export { default as SyntaxHighlighter } from './SyntaxHighlighter';

View File

@ -0,0 +1,37 @@
import { useEffect, useState } from 'react';
export const useExternalTextareaObserver = (textareaSelector: string) => {
const [value, setValue] = useState('');
useEffect(() => {
const observerCallback: MutationCallback = (mutationsList) => {
for (const mutation of mutationsList) {
if (mutation.type === 'attributes') {
const externalTextarea = document.querySelector(textareaSelector) as HTMLTextAreaElement;
setValue(externalTextarea.value);
}
}
};
const observerOptions: MutationObserverInit = {
attributes: true,
characterData: true,
childList: true,
subtree: true,
};
const observer = new MutationObserver(observerCallback);
const externalTextarea = document.querySelector(textareaSelector) as HTMLTextAreaElement | null;
if (externalTextarea) {
observer.observe(externalTextarea, observerOptions);
setValue(externalTextarea.value);
}
return () => {
observer.disconnect();
};
}, [textareaSelector]);
return value;
};

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

@ -0,0 +1,48 @@
// @ts-ignore
import { type Highlighter, getHighlighter } from 'shiki-es';
import { create } from 'zustand';
import grammar from '@/components/SyntaxHighlighter/prompt.tmLanguage.json';
import { themeConfig } from '@/components/SyntaxHighlighter/promptTheme';
interface Store {
codeToHtml: (text: string, language: string, isDarkMode: boolean) => string;
highlighter?: Highlighter;
initHighlighter: () => Promise<void>;
}
export const useHighlight = create<Store>((set, get) => ({
codeToHtml: (text, language = 'prompt', 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() => {
if (!get().highlighter) {
const highlighter = await getHighlighter({
langs: [
{
aliases: ['prompt'],
grammar: grammar,
id: 'prompt',
scopeName: 'source.prompt',
},
],
themes: [themeConfig(true), themeConfig(false)],
});
set({ highlighter });
}
},
}));

View File

@ -1,14 +1,14 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
function checkIsDarkMode() { const checkIsDarkMode = () => {
try { try {
return window.matchMedia('(prefers-color-scheme: dark)').matches; return window.matchMedia('(prefers-color-scheme: dark)').matches;
} catch { } catch {
return false; return false;
} }
} };
export function useIsDarkMode() { export const useIsDarkMode = () => {
const [isDarkMode, setIsDarkMode] = useState(checkIsDarkMode()); const [isDarkMode, setIsDarkMode] = useState(checkIsDarkMode());
useEffect(() => { useEffect(() => {
@ -26,4 +26,4 @@ export function useIsDarkMode() {
}, []); }, []);
return isDarkMode; return isDarkMode;
} };

View File

@ -62,6 +62,9 @@ const translation = {
settingPromptDisplayModeDesc: 'Fixed height or auto height with draggable resize support', settingPromptDisplayModeDesc: 'Fixed height or auto height with draggable resize support',
settingPromptEditor: 'Prompt Editor', settingPromptEditor: 'Prompt Editor',
settingPromptEditorDesc: 'Provide a simple prompt editor at the top of the quick setting sidebar', settingPromptEditorDesc: 'Provide a simple prompt editor at the top of the quick setting sidebar',
settingPromptHighlight: 'Prompt Syntax Highlighting',
settingPromptHighlightDesc:
'Automatically colorize prompt display according to the Stable Diffusion syntax rules',
settingQuickSettingSidebarDefaultExpand: 'Default Expand', settingQuickSettingSidebarDefaultExpand: 'Default Expand',
settingQuickSettingSidebarDefaultExpandDesc: settingQuickSettingSidebarDefaultExpandDesc:
'Whether to expand the sidebar by default when starting', 'Whether to expand the sidebar by default when starting',

View File

@ -62,6 +62,9 @@ const translation: Translation = {
settingPromptDisplayModeDesc: '固定の高さまたはドラッグリサイズをサポートする自動の高さ', settingPromptDisplayModeDesc: '固定の高さまたはドラッグリサイズをサポートする自動の高さ',
settingPromptEditor: 'プロンプトエディタ', settingPromptEditor: 'プロンプトエディタ',
settingPromptEditorDesc: 'クイック設定サイドバーの上部に簡単なプロンプトエディタを提供します', settingPromptEditorDesc: 'クイック設定サイドバーの上部に簡単なプロンプトエディタを提供します',
settingPromptHighlight: 'Promptのシンタックスハイライト',
settingPromptHighlightDesc:
'Stable Diffusionのシンタックスルールに基づいて、promptの表示を自動的にハイライトします',
settingQuickSettingSidebarDefaultExpand: 'デフォルトで展開', settingQuickSettingSidebarDefaultExpand: 'デフォルトで展開',
settingQuickSettingSidebarDefaultExpandDesc: '起動時にサイドバーをデフォルトで展開しますか?', settingQuickSettingSidebarDefaultExpandDesc: '起動時にサイドバーをデフォルトで展開しますか?',
settingQuickSettingSidebarDefaultWidth: 'デフォルト幅', settingQuickSettingSidebarDefaultWidth: 'デフォルト幅',

View File

@ -61,6 +61,9 @@ const translation: Translation = {
settingPromptDisplayModeDesc: '고정 높이 또는 자동 높이 및 드래그 조절 지원', settingPromptDisplayModeDesc: '고정 높이 또는 자동 높이 및 드래그 조절 지원',
settingPromptEditor: '프롬프트 편집기', settingPromptEditor: '프롬프트 편집기',
settingPromptEditorDesc: '빠른 설정 사이드바 상단에 간단한 프롬프트 편집기 제공', settingPromptEditorDesc: '빠른 설정 사이드바 상단에 간단한 프롬프트 편집기 제공',
settingPromptHighlight: 'Prompt 구문 강조',
settingPromptHighlightDesc:
'Stable Diffusion 구문 규칙에 따라 자동으로 prompt를 강조하여 표시합니다',
settingQuickSettingSidebarDefaultExpand: '기본 확장', settingQuickSettingSidebarDefaultExpand: '기본 확장',
settingQuickSettingSidebarDefaultExpandDesc: '시작시 사이드바 기본 확장 여부', settingQuickSettingSidebarDefaultExpandDesc: '시작시 사이드바 기본 확장 여부',
settingQuickSettingSidebarDefaultWidth: '기본 너비', settingQuickSettingSidebarDefaultWidth: '기본 너비',

View File

@ -59,6 +59,8 @@ const translation: Translation = {
settingPromptDisplayModeDesc: '固定高度或自动高度并支持拖拽拉伸', settingPromptDisplayModeDesc: '固定高度或自动高度并支持拖拽拉伸',
settingPromptEditor: '提示词编辑器', settingPromptEditor: '提示词编辑器',
settingPromptEditorDesc: '提供简易的提示词编辑器位于快捷设置侧边栏顶部', settingPromptEditorDesc: '提供简易的提示词编辑器位于快捷设置侧边栏顶部',
settingPromptHighlight: 'Prompt 语法高亮',
settingPromptHighlightDesc: '按 Stable Diffusion 语法规则,自动染色 prompt 显示',
settingQuickSettingSidebarDefaultExpand: '默认展开', settingQuickSettingSidebarDefaultExpand: '默认展开',
settingQuickSettingSidebarDefaultExpandDesc: '是否在启动时将侧边栏默认展开', settingQuickSettingSidebarDefaultExpandDesc: '是否在启动时将侧边栏默认展开',
settingQuickSettingSidebarDefaultWidth: '默认宽度', settingQuickSettingSidebarDefaultWidth: '默认宽度',

View File

@ -59,6 +59,8 @@ const translation: Translation = {
settingPromptDisplayModeDesc: '固定高度或自動高度並支持拖拽拉伸', settingPromptDisplayModeDesc: '固定高度或自動高度並支持拖拽拉伸',
settingPromptEditor: '提示詞編輯器', settingPromptEditor: '提示詞編輯器',
settingPromptEditorDesc: '提供簡易的提示詞編輯器位於快捷設置側邊欄頂部', settingPromptEditorDesc: '提供簡易的提示詞編輯器位於快捷設置側邊欄頂部',
settingPromptHighlight: 'Prompt 語法高亮',
settingPromptHighlightDesc: '按照 Stable Diffusion 語法規則,自動著色 prompt 顯示',
settingQuickSettingSidebarDefaultExpand: '默認展開', settingQuickSettingSidebarDefaultExpand: '默認展開',
settingQuickSettingSidebarDefaultExpandDesc: '是否在啟動時將側邊欄默認展開', settingQuickSettingSidebarDefaultExpandDesc: '是否在啟動時將側邊欄默認展開',
settingQuickSettingSidebarDefaultWidth: '默認寬度', settingQuickSettingSidebarDefaultWidth: '默認寬度',

View File

@ -10,7 +10,6 @@ import { shallow } from 'zustand/shallow';
import { useIsDarkMode } from '@/hooks/useIsDarkMode'; import { useIsDarkMode } from '@/hooks/useIsDarkMode';
import { useAppStore } from '@/store'; import { useAppStore } from '@/store';
import GlobalStyle from '@/styles/index';
import { kitchenNeutral, kitchenPrimary } from '@/styles/kitchenColors'; import { kitchenNeutral, kitchenPrimary } from '@/styles/kitchenColors';
import { neutralColorScales } from '@/styles/neutralColors'; import { neutralColorScales } from '@/styles/neutralColors';
@ -67,7 +66,6 @@ const Layout = memo<DivProps>(({ children }) => {
['https://npm.elemecdn.com/normalize.css/normalize.css'] ['https://npm.elemecdn.com/normalize.css/normalize.css']
} }
> >
<GlobalStyle />
{children} {children}
</ThemeProvider> </ThemeProvider>
) )

View File

@ -0,0 +1,66 @@
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import Editor from 'react-simple-code-editor';
import { shallow } from 'zustand/shallow';
import { SyntaxHighlighter } from '@/components';
import { useExternalTextareaObserver } from '@/hooks/useExternalTextareaObserver';
import { useAppStore } from '@/store';
import { useStyles } from './style';
interface AppProps {
parentId: string;
}
const App = memo<AppProps>(({ parentId }) => {
const reference = useRef(null);
const [prompt, setPrompt] = useState<string>('');
const themeMode = useAppStore((s) => s.themeMode, shallow);
const { styles, cx } = useStyles();
const nativeTextareaValue = useExternalTextareaObserver(`${parentId} label textarea`);
const nativeTextarea = useMemo(
() => gradioApp().querySelector(`${parentId} label textarea`) as HTMLTextAreaElement,
[parentId],
);
const handlePromptChange = useCallback((event: any) => {
setPrompt(event.target.value);
}, []);
useEffect(() => {
nativeTextarea.style.display = 'none';
nativeTextarea.addEventListener('change', handlePromptChange);
return () => {
nativeTextarea.removeEventListener('change', handlePromptChange);
};
}, []);
useEffect(() => {
setPrompt(nativeTextareaValue);
}, [nativeTextareaValue]);
const onBlur = useCallback(() => {
nativeTextarea.value = prompt;
const event = new Event('input');
nativeTextarea.dispatchEvent(event);
}, [prompt]);
return (
<Editor
className={cx(styles.editor, 'prompt_editor')}
highlight={(code) => <SyntaxHighlighter theme={themeMode}>{code}</SyntaxHighlighter>}
onBlur={onBlur}
onValueChange={setPrompt}
padding={8}
placeholder={nativeTextarea.placeholder}
ref={reference}
textareaClassName={cx(styles.textarea)}
value={prompt}
/>
);
});
export default App;

View File

@ -0,0 +1,26 @@
import { StrictMode, Suspense } from 'react';
import { createRoot } from 'react-dom/client';
import Layout from '@/layouts';
import App from './App';
export const PromptHighlight = (parentId: string, containerId: string) => {
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>
<App parentId={parentId} />
</Layout>
</Suspense>
</StrictMode>,
);
};

View File

@ -0,0 +1,62 @@
import { createStyles } from 'antd-style';
export const useStyles = createStyles(({ css, token }) => ({
container: css`
display: flex;
flex-direction: column;
width: 100%;
min-height: 100px;
max-height: 800px;
`,
editor: css`
resize: vertical;
font-family: ${token.fontFamilyCode} !important;
font-size: 13px;
line-height: 18.2px;
background: ${token.colorFillTertiary};
border: 1px solid ${token.colorBorderSecondary};
border-radius: ${token.borderRadius}px;
&:focus,
&:active,
&:hover {
border: 1px solid ${token.colorBorder};
}
pre {
word-wrap: break-word;
white-space: pre-wrap;
}
`,
handle: css`
cursor: row-resize;
width: 100%;
height: 2px;
background-color: transparent;
&:hover {
background: ${token.colorPrimary};
}
`,
textarea: css`
height: 100% !important;
&::placeholder {
color: ${token.colorTextQuaternary};
}
&::selection {
color: #000;
background: ${token.yellow3A};
}
&:focus {
border: none !important;
outline: none !important;
box-shadow: none !important;
}
`,
}));

View File

@ -272,6 +272,7 @@ export const useStyles = createStyles(
} }
.block.token-counter { .block.token-counter {
top: -12px;
right: 4px; right: 4px;
scale: 0.8; scale: 0.8;
background: ${token.colorBgContainer} !important; background: ${token.colorBgContainer} !important;
@ -286,6 +287,16 @@ export const useStyles = createStyles(
} }
} }
#lobe_txt2img_prompt .prompt_editor {
min-height: ${TEXT2IMG_PROMPT_HEIGHT}px;
max-height: ${isPromptResizable ? 'unset' : `${TEXT2IMG_PROMPT_HEIGHT}px`};
}
#lobe_img2img_prompt .prompt_editor {
min-height: ${IMG2IMG_PROMPT_HEIGHT}px;
max-height: ${isPromptResizable ? 'unset' : `${IMG2IMG_PROMPT_HEIGHT}px`};
}
#text2img_prompt, #text2img_prompt,
#text2img_neg_prompt { #text2img_neg_prompt {
textarea { textarea {

View File

@ -5,8 +5,10 @@ import { memo, useEffect } from 'react';
import { shallow } from 'zustand/shallow'; import { shallow } from 'zustand/shallow';
import '@/i18n/config'; import '@/i18n/config';
import { PromptHighlight } from '@/modules/PromptHighlight';
import replaceIcon from '@/script/replaceIcon'; import replaceIcon from '@/script/replaceIcon';
import { useAppStore } from '@/store'; import { useAppStore } from '@/store';
import GlobalStyle from '@/styles';
import Content from './Content'; import Content from './Content';
import ExtraNetworkSidebar from './ExtraNetworkSidebar'; import ExtraNetworkSidebar from './ExtraNetworkSidebar';
@ -28,11 +30,16 @@ const Index = memo(() => {
}); });
useEffect(() => { useEffect(() => {
if (setting.enableHighlight) {
PromptHighlight('#txt2img_prompt', '#lobe_txt2img_prompt');
PromptHighlight('#img2img_prompt', '#lobe_img2img_prompt');
}
if (setting.svgIcon) replaceIcon(); if (setting.svgIcon) replaceIcon();
}, []); }, []);
return ( return (
<> <>
<GlobalStyle />
<LayoutHeader headerHeight={HEADER_HEIGHT}> <LayoutHeader headerHeight={HEADER_HEIGHT}>
<Header /> <Header />
</LayoutHeader> </LayoutHeader>

View File

@ -165,6 +165,15 @@ const SettingForm = memo(() => {
]} ]}
/> />
</FormItem> </FormItem>
<FormItem
desc={t('settingPromptHighlightDesc')}
divider
label={t('settingPromptHighlight')}
name="enableHighlight"
valuePropName="checked"
>
<Switch />
</FormItem>
<FormItem <FormItem
desc={t('settingPromptEditorDesc')} desc={t('settingPromptEditorDesc')}
divider divider

View File

@ -24,6 +24,7 @@ export type NeutralColor = 'mauve' | 'slate' | 'sage' | 'olive' | 'sand' | 'kitc
export interface WebuiSetting { export interface WebuiSetting {
enableExtraNetworkSidebar: boolean; enableExtraNetworkSidebar: boolean;
enableHighlight: boolean;
enableSidebar: boolean; enableSidebar: boolean;
enableWebFont: boolean; enableWebFont: boolean;
extraNetworkCardSize: number; extraNetworkCardSize: number;
@ -49,6 +50,7 @@ export interface WebuiSetting {
export const defaultSetting: WebuiSetting = { export const defaultSetting: WebuiSetting = {
enableExtraNetworkSidebar: true, enableExtraNetworkSidebar: true,
enableHighlight: true,
enableSidebar: true, enableSidebar: true,
enableWebFont: true, enableWebFont: true,
extraNetworkCardSize: 86, extraNetworkCardSize: 86,