mirror of
https://github.com/lobehub/sd-webui-lobe-theme.git
synced 2026-01-09 06:23:44 +08:00
✨ feat(prompt): add prompt syntax highlighting
This commit is contained in:
parent
9ed6da2554
commit
b51301df08
@ -25,7 +25,7 @@
|
||||
|
||||
</div>
|
||||
|
||||

|
||||

|
||||
|
||||
<details>
|
||||
<summary><kbd>文档目录</kbd></summary>
|
||||
@ -57,8 +57,7 @@
|
||||
- [x] 🖼️ 可调节画板比例,使生成图像始终置顶
|
||||
- [x] 📱 移动端友好,针对手机屏幕完成部分优化
|
||||
- [x] 🇨🇳 支持 i18n 并欢迎提交 [PR](https://github.com/canisminor1990/sd-webui-lobe-theme/tree/main/src/i18n/lang) 贡献
|
||||
- [ ] 📝 语法高亮的 Prompt 输入框
|
||||
- [ ] 🆗 i18n 多语言支持
|
||||
- [x] 📝 语法高亮的 Prompt 输入框
|
||||
|
||||
<div align="right">
|
||||
|
||||
@ -92,7 +91,7 @@ git clone "https://github.com/canisminor1990/sd-webui-lobe-theme" extensions/lob
|
||||
|
||||
## 🤯 使用说明
|
||||
|
||||

|
||||

|
||||
|
||||
#### 亮暗色主题
|
||||
|
||||
@ -117,7 +116,7 @@ http://localhost:7860/?__theme=dark
|
||||
|
||||
</div>
|
||||
|
||||

|
||||

|
||||
|
||||
#### 主体定制
|
||||
|
||||
@ -136,7 +135,21 @@ http://localhost:7860/?__theme=dark
|
||||
|
||||
</div>
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||
|
||||
#### Prompt 语法高亮
|
||||
|
||||
按 Stable Diffusion 语法规则,自动染色 prompt 显示
|
||||
|
||||
<div align="right">
|
||||
|
||||
[![][back-to-top]](#readme-top)
|
||||
|
||||
</div>
|
||||
|
||||

|
||||
|
||||
#### 侧边栏定制
|
||||
|
||||
@ -180,7 +193,7 @@ sd_model_checkpoint, sd_vae, CLIP_stop_at_last_layers, img2img_background_color,
|
||||
|
||||
</div>
|
||||
|
||||

|
||||

|
||||
|
||||
#### 移动端适配
|
||||
|
||||
|
||||
25
README.md
25
README.md
@ -25,7 +25,7 @@ English · [简体中文](./README-zh_CN.md) · [Changelog](./CHANGELOG.md) · [
|
||||
|
||||
</div>
|
||||
|
||||

|
||||

|
||||
|
||||
> 📦 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] 📱 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
|
||||
- [ ] 📝 Syntax highlighting in the prompt input box
|
||||
- [ ] 🆗 Multilingual support with i18n
|
||||
- [x] 📝 Syntax highlighting in the prompt input box
|
||||
|
||||
<div align="right">
|
||||
|
||||
@ -92,7 +91,7 @@ git clone "https://github.com/canisminor1990/sd-webui-lobe-theme" extensions/lob
|
||||
|
||||
## 🤯 Usage
|
||||
|
||||

|
||||

|
||||
|
||||
#### Light and Dark Themes
|
||||
|
||||
@ -117,7 +116,7 @@ http://localhost:7860/?__theme=dark
|
||||
|
||||
</div>
|
||||
|
||||

|
||||

|
||||
|
||||
#### Theme Customization
|
||||
|
||||
@ -136,7 +135,19 @@ http://localhost:7860/?__theme=dark
|
||||
|
||||
</div>
|
||||
|
||||

|
||||

|
||||
|
||||
#### Prompt Syntax Highlighting
|
||||
|
||||
Automatically colorize prompt display according to the Stable Diffusion syntax rules
|
||||
|
||||
<div align="right">
|
||||
|
||||
[![][back-to-top]](#readme-top)
|
||||
|
||||
</div>
|
||||
|
||||

|
||||
|
||||
#### Sidebar Customization
|
||||
|
||||
@ -180,7 +191,7 @@ sd_model_checkpoint, sd_vae, CLIP_stop_at_last_layers, img2img_background_color,
|
||||
|
||||
</div>
|
||||
|
||||

|
||||

|
||||
|
||||
#### Mobile Adaptation
|
||||
|
||||
|
||||
BIN
docs/feat_highlight.webp
Normal file
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
@ -58,7 +58,6 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ant-design/icons": "^5",
|
||||
"@babel/plugin-syntax-import-assertions": "^7",
|
||||
"@commitlint/cli": "^17",
|
||||
"@giscus/react": "^2",
|
||||
@ -75,21 +74,16 @@
|
||||
"ahooks": "^3",
|
||||
"antd": "^5",
|
||||
"antd-style": "latest",
|
||||
"babel-plugin-styled-components": "^2",
|
||||
"browserslist": "^4",
|
||||
"commitlint": "^17",
|
||||
"concurrently": "^8",
|
||||
"css-minimizer-webpack-plugin": "^5",
|
||||
"eslint": "^8",
|
||||
"fast-deep-equal": "^3",
|
||||
"husky": "^8",
|
||||
"i18next": "^23",
|
||||
"lightningcss": "^1",
|
||||
"lint-staged": "^13",
|
||||
"lodash-es": "^4",
|
||||
"lucide-react": "latest",
|
||||
"lucide-static": "latest",
|
||||
"object-to-css-variables": "^0",
|
||||
"polished": "^4",
|
||||
"prettier": "^2",
|
||||
"query-string": "^8",
|
||||
@ -99,11 +93,13 @@
|
||||
"react-i18next": "^13",
|
||||
"react-layout-kit": "^1",
|
||||
"react-rnd": "^10",
|
||||
"react-simple-code-editor": "^0",
|
||||
"react-tag-input": "^6",
|
||||
"remark": "^14",
|
||||
"remark-cli": "^11",
|
||||
"rollup-plugin-terser": "^7",
|
||||
"semantic-release": "^21",
|
||||
"shiki-es": "^0",
|
||||
"styled-components": "latest",
|
||||
"stylelint": "^15",
|
||||
"typescript": "^5",
|
||||
|
||||
@ -19,8 +19,8 @@ const App = memo(() => {
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
onInit();
|
||||
console.time('🤯 Lobe Theme loading');
|
||||
onInit();
|
||||
onUiLoaded(() => {
|
||||
setLoading(false);
|
||||
console.timeEnd('🤯 Lobe Theme loading');
|
||||
@ -64,6 +64,7 @@ const App = memo(() => {
|
||||
<meta content="#000000" name="msapplication-TileColor" />
|
||||
<meta content="#000000" name="theme-color" />
|
||||
</Helmet>
|
||||
|
||||
{!storeLoading && <Layout>{loading ? <Loading /> : <Index />}</Layout>}
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
48
src/components/SyntaxHighlighter/index.tsx
Normal file
48
src/components/SyntaxHighlighter/index.tsx
Normal 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;
|
||||
50
src/components/SyntaxHighlighter/prompt.tmLanguage.json
Normal file
50
src/components/SyntaxHighlighter/prompt.tmLanguage.json
Normal 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"
|
||||
}
|
||||
72
src/components/SyntaxHighlighter/promptTheme.ts
Normal file
72
src/components/SyntaxHighlighter/promptTheme.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
45
src/components/SyntaxHighlighter/style.ts
Normal file
45
src/components/SyntaxHighlighter/style.ts
Normal 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;
|
||||
}
|
||||
`,
|
||||
),
|
||||
};
|
||||
});
|
||||
@ -5,3 +5,4 @@ export { default as SidebarBody } from './Sidebar/SidebarBody';
|
||||
export { default as SidebarContainer } from './Sidebar/SidebarContainer';
|
||||
export { default as SidebarFooter } from './Sidebar/SidebarFooter';
|
||||
export { default as SidebarHeader, type SidebarHeaderProps } from './Sidebar/SidebarHeader';
|
||||
export { default as SyntaxHighlighter } from './SyntaxHighlighter';
|
||||
|
||||
37
src/hooks/useExternalTextareaObserver.ts
Normal file
37
src/hooks/useExternalTextareaObserver.ts
Normal 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
48
src/hooks/useHighlight.ts
Normal 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 });
|
||||
}
|
||||
},
|
||||
}));
|
||||
@ -1,14 +1,14 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
function checkIsDarkMode() {
|
||||
const checkIsDarkMode = () => {
|
||||
try {
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export function useIsDarkMode() {
|
||||
export const useIsDarkMode = () => {
|
||||
const [isDarkMode, setIsDarkMode] = useState(checkIsDarkMode());
|
||||
|
||||
useEffect(() => {
|
||||
@ -26,4 +26,4 @@ export function useIsDarkMode() {
|
||||
}, []);
|
||||
|
||||
return isDarkMode;
|
||||
}
|
||||
};
|
||||
|
||||
@ -62,6 +62,9 @@ const translation = {
|
||||
settingPromptDisplayModeDesc: 'Fixed height or auto height with draggable resize support',
|
||||
settingPromptEditor: 'Prompt Editor',
|
||||
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',
|
||||
settingQuickSettingSidebarDefaultExpandDesc:
|
||||
'Whether to expand the sidebar by default when starting',
|
||||
|
||||
@ -62,6 +62,9 @@ const translation: Translation = {
|
||||
settingPromptDisplayModeDesc: '固定の高さまたはドラッグリサイズをサポートする自動の高さ',
|
||||
settingPromptEditor: 'プロンプトエディタ',
|
||||
settingPromptEditorDesc: 'クイック設定サイドバーの上部に簡単なプロンプトエディタを提供します',
|
||||
settingPromptHighlight: 'Promptのシンタックスハイライト',
|
||||
settingPromptHighlightDesc:
|
||||
'Stable Diffusionのシンタックスルールに基づいて、promptの表示を自動的にハイライトします',
|
||||
settingQuickSettingSidebarDefaultExpand: 'デフォルトで展開',
|
||||
settingQuickSettingSidebarDefaultExpandDesc: '起動時にサイドバーをデフォルトで展開しますか?',
|
||||
settingQuickSettingSidebarDefaultWidth: 'デフォルト幅',
|
||||
|
||||
@ -61,6 +61,9 @@ const translation: Translation = {
|
||||
settingPromptDisplayModeDesc: '고정 높이 또는 자동 높이 및 드래그 조절 지원',
|
||||
settingPromptEditor: '프롬프트 편집기',
|
||||
settingPromptEditorDesc: '빠른 설정 사이드바 상단에 간단한 프롬프트 편집기 제공',
|
||||
settingPromptHighlight: 'Prompt 구문 강조',
|
||||
settingPromptHighlightDesc:
|
||||
'Stable Diffusion 구문 규칙에 따라 자동으로 prompt를 강조하여 표시합니다',
|
||||
settingQuickSettingSidebarDefaultExpand: '기본 확장',
|
||||
settingQuickSettingSidebarDefaultExpandDesc: '시작시 사이드바 기본 확장 여부',
|
||||
settingQuickSettingSidebarDefaultWidth: '기본 너비',
|
||||
|
||||
@ -59,6 +59,8 @@ const translation: Translation = {
|
||||
settingPromptDisplayModeDesc: '固定高度或自动高度并支持拖拽拉伸',
|
||||
settingPromptEditor: '提示词编辑器',
|
||||
settingPromptEditorDesc: '提供简易的提示词编辑器位于快捷设置侧边栏顶部',
|
||||
settingPromptHighlight: 'Prompt 语法高亮',
|
||||
settingPromptHighlightDesc: '按 Stable Diffusion 语法规则,自动染色 prompt 显示',
|
||||
settingQuickSettingSidebarDefaultExpand: '默认展开',
|
||||
settingQuickSettingSidebarDefaultExpandDesc: '是否在启动时将侧边栏默认展开',
|
||||
settingQuickSettingSidebarDefaultWidth: '默认宽度',
|
||||
|
||||
@ -59,6 +59,8 @@ const translation: Translation = {
|
||||
settingPromptDisplayModeDesc: '固定高度或自動高度並支持拖拽拉伸',
|
||||
settingPromptEditor: '提示詞編輯器',
|
||||
settingPromptEditorDesc: '提供簡易的提示詞編輯器位於快捷設置側邊欄頂部',
|
||||
settingPromptHighlight: 'Prompt 語法高亮',
|
||||
settingPromptHighlightDesc: '按照 Stable Diffusion 語法規則,自動著色 prompt 顯示',
|
||||
settingQuickSettingSidebarDefaultExpand: '默認展開',
|
||||
settingQuickSettingSidebarDefaultExpandDesc: '是否在啟動時將側邊欄默認展開',
|
||||
settingQuickSettingSidebarDefaultWidth: '默認寬度',
|
||||
|
||||
@ -10,7 +10,6 @@ import { shallow } from 'zustand/shallow';
|
||||
|
||||
import { useIsDarkMode } from '@/hooks/useIsDarkMode';
|
||||
import { useAppStore } from '@/store';
|
||||
import GlobalStyle from '@/styles/index';
|
||||
import { kitchenNeutral, kitchenPrimary } from '@/styles/kitchenColors';
|
||||
import { neutralColorScales } from '@/styles/neutralColors';
|
||||
|
||||
@ -67,7 +66,6 @@ const Layout = memo<DivProps>(({ children }) => {
|
||||
['https://npm.elemecdn.com/normalize.css/normalize.css']
|
||||
}
|
||||
>
|
||||
<GlobalStyle />
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
)
|
||||
|
||||
66
src/modules/PromptHighlight/App.tsx
Normal file
66
src/modules/PromptHighlight/App.tsx
Normal 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;
|
||||
26
src/modules/PromptHighlight/index.tsx
Normal file
26
src/modules/PromptHighlight/index.tsx
Normal 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>,
|
||||
);
|
||||
};
|
||||
62
src/modules/PromptHighlight/style.ts
Normal file
62
src/modules/PromptHighlight/style.ts
Normal 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;
|
||||
}
|
||||
`,
|
||||
}));
|
||||
@ -272,6 +272,7 @@ export const useStyles = createStyles(
|
||||
}
|
||||
|
||||
.block.token-counter {
|
||||
top: -12px;
|
||||
right: 4px;
|
||||
scale: 0.8;
|
||||
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_neg_prompt {
|
||||
textarea {
|
||||
|
||||
@ -5,8 +5,10 @@ import { memo, useEffect } from 'react';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
|
||||
import '@/i18n/config';
|
||||
import { PromptHighlight } from '@/modules/PromptHighlight';
|
||||
import replaceIcon from '@/script/replaceIcon';
|
||||
import { useAppStore } from '@/store';
|
||||
import GlobalStyle from '@/styles';
|
||||
|
||||
import Content from './Content';
|
||||
import ExtraNetworkSidebar from './ExtraNetworkSidebar';
|
||||
@ -28,11 +30,16 @@ const Index = memo(() => {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (setting.enableHighlight) {
|
||||
PromptHighlight('#txt2img_prompt', '#lobe_txt2img_prompt');
|
||||
PromptHighlight('#img2img_prompt', '#lobe_img2img_prompt');
|
||||
}
|
||||
if (setting.svgIcon) replaceIcon();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<GlobalStyle />
|
||||
<LayoutHeader headerHeight={HEADER_HEIGHT}>
|
||||
<Header />
|
||||
</LayoutHeader>
|
||||
|
||||
@ -165,6 +165,15 @@ const SettingForm = memo(() => {
|
||||
]}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
desc={t('settingPromptHighlightDesc')}
|
||||
divider
|
||||
label={t('settingPromptHighlight')}
|
||||
name="enableHighlight"
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</FormItem>
|
||||
<FormItem
|
||||
desc={t('settingPromptEditorDesc')}
|
||||
divider
|
||||
|
||||
@ -24,6 +24,7 @@ export type NeutralColor = 'mauve' | 'slate' | 'sage' | 'olive' | 'sand' | 'kitc
|
||||
|
||||
export interface WebuiSetting {
|
||||
enableExtraNetworkSidebar: boolean;
|
||||
enableHighlight: boolean;
|
||||
enableSidebar: boolean;
|
||||
enableWebFont: boolean;
|
||||
extraNetworkCardSize: number;
|
||||
@ -49,6 +50,7 @@ export interface WebuiSetting {
|
||||
|
||||
export const defaultSetting: WebuiSetting = {
|
||||
enableExtraNetworkSidebar: true,
|
||||
enableHighlight: true,
|
||||
enableSidebar: true,
|
||||
enableWebFont: true,
|
||||
extraNetworkCardSize: 86,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user