From f473f319da49f07a70788ad2f889b741d899855b Mon Sep 17 00:00:00 2001 From: Ivan <8611739+IRBorisov@users.noreply.github.com> Date: Tue, 22 Jul 2025 20:39:57 +0300 Subject: [PATCH] F: Implement prompt editor --- rsconcept/frontend/package.json | 2 +- .../ai/components/prompt-input/completion.tsx | 24 +++ .../ai/components/prompt-input/index.tsx | 1 + .../prompt-input/mark-variables.tsx | 66 ++++++++ .../components/prompt-input/no-spellcheck.tsx | 45 ++++++ .../prompt-input/parse/highlight.ts | 6 + .../ai/components/prompt-input/parse/index.ts | 8 + .../prompt-input/parse/parser.terms.ts | 5 + .../prompt-input/parse/parser.test.ts | 30 ++++ .../components/prompt-input/parse/parser.ts | 20 +++ .../prompt-input/parse/prompt-text.grammar | 39 +++++ .../components/prompt-input/prompt-input.tsx | 144 ++++++++++++++++++ .../ai/components/prompt-input/tooltip.ts | 87 +++++++++++ .../dialogs/dlg-ai-prompt/tab-prompt-edit.tsx | 12 +- .../src/features/ai/models/prompting.ts | 10 +- .../form-prompt-template.tsx | 29 +++- .../tab-view-variables.tsx | 6 +- .../refs-input/parse/parser.terms.ts | 5 +- .../components/refs-input/parse/parser.ts | 20 +-- .../components/refs-input/refs-input.tsx | 57 ++++--- .../rsform/components/rs-input/rs-input.tsx | 59 ++++--- rsconcept/frontend/src/styling/overrides.css | 10 ++ 22 files changed, 594 insertions(+), 91 deletions(-) create mode 100644 rsconcept/frontend/src/features/ai/components/prompt-input/completion.tsx create mode 100644 rsconcept/frontend/src/features/ai/components/prompt-input/index.tsx create mode 100644 rsconcept/frontend/src/features/ai/components/prompt-input/mark-variables.tsx create mode 100644 rsconcept/frontend/src/features/ai/components/prompt-input/no-spellcheck.tsx create mode 100644 rsconcept/frontend/src/features/ai/components/prompt-input/parse/highlight.ts create mode 100644 rsconcept/frontend/src/features/ai/components/prompt-input/parse/index.ts create mode 100644 rsconcept/frontend/src/features/ai/components/prompt-input/parse/parser.terms.ts create mode 100644 rsconcept/frontend/src/features/ai/components/prompt-input/parse/parser.test.ts create mode 100644 rsconcept/frontend/src/features/ai/components/prompt-input/parse/parser.ts create mode 100644 rsconcept/frontend/src/features/ai/components/prompt-input/parse/prompt-text.grammar create mode 100644 rsconcept/frontend/src/features/ai/components/prompt-input/prompt-input.tsx create mode 100644 rsconcept/frontend/src/features/ai/components/prompt-input/tooltip.ts diff --git a/rsconcept/frontend/package.json b/rsconcept/frontend/package.json index 0c833535..21761219 100644 --- a/rsconcept/frontend/package.json +++ b/rsconcept/frontend/package.json @@ -4,7 +4,7 @@ "version": "1.0.0", "type": "module", "scripts": { - "generate": "lezer-generator src/features/rsform/components/rs-input/rslang/rslang-fast.grammar -o src/features/rsform/components/rs-input/rslang/parser.ts && lezer-generator src/features/rsform/components/rs-input/rslang/rslang-ast.grammar -o src/features/rsform/components/rs-input/rslang/parser-ast.ts && lezer-generator src/features/rsform/components/refs-input/parse/refs-text.grammar -o src/features/rsform/components/refs-input/parse/parser.ts", + "generate": "lezer-generator src/features/rsform/components/rs-input/rslang/rslang-fast.grammar -o src/features/rsform/components/rs-input/rslang/parser.ts && lezer-generator src/features/rsform/components/rs-input/rslang/rslang-ast.grammar -o src/features/rsform/components/rs-input/rslang/parser-ast.ts && lezer-generator src/features/rsform/components/refs-input/parse/refs-text.grammar -o src/features/rsform/components/refs-input/parse/parser.ts && lezer-generator src/features/ai/components/prompt-input/parse/prompt-text.grammar -o src/features/ai/components/prompt-input/parse/parser.ts", "test": "jest", "test:e2e": "playwright test", "dev": "vite --host", diff --git a/rsconcept/frontend/src/features/ai/components/prompt-input/completion.tsx b/rsconcept/frontend/src/features/ai/components/prompt-input/completion.tsx new file mode 100644 index 00000000..0da01a01 --- /dev/null +++ b/rsconcept/frontend/src/features/ai/components/prompt-input/completion.tsx @@ -0,0 +1,24 @@ +import { type CompletionContext } from '@codemirror/autocomplete'; + +import { describePromptVariable } from '../../labels'; +import { type PromptVariableType } from '../../models/prompting'; + +export function variableCompletions(variables: string[]) { + return (context: CompletionContext) => { + let word = context.matchBefore(/\{\{[a-zA-Z.-]*/); + if (!word && context.explicit) { + word = { from: context.pos, to: context.pos, text: '' }; + } + if (!word || (word.from == word.to && !context.explicit)) { + return null; + } + return { + from: word.from, + to: word.to, + options: variables.map(name => ({ + label: `{{${name}}}`, + info: describePromptVariable(name as PromptVariableType) + })) + }; + }; +} diff --git a/rsconcept/frontend/src/features/ai/components/prompt-input/index.tsx b/rsconcept/frontend/src/features/ai/components/prompt-input/index.tsx new file mode 100644 index 00000000..e0f47da1 --- /dev/null +++ b/rsconcept/frontend/src/features/ai/components/prompt-input/index.tsx @@ -0,0 +1 @@ +export { PromptInput } from './prompt-input'; diff --git a/rsconcept/frontend/src/features/ai/components/prompt-input/mark-variables.tsx b/rsconcept/frontend/src/features/ai/components/prompt-input/mark-variables.tsx new file mode 100644 index 00000000..df2c211a --- /dev/null +++ b/rsconcept/frontend/src/features/ai/components/prompt-input/mark-variables.tsx @@ -0,0 +1,66 @@ +import { syntaxTree } from '@codemirror/language'; +import { RangeSetBuilder } from '@codemirror/state'; +import { Decoration, type DecorationSet, type EditorView, ViewPlugin, type ViewUpdate } from '@codemirror/view'; + +const invalidVarMark = Decoration.mark({ + class: 'text-destructive' +}); + +const validMark = Decoration.mark({ + class: 'text-(--acc-fg-purple)' +}); + +class MarkVariablesPlugin { + decorations: DecorationSet; + allowed: string[]; + + constructor(view: EditorView, allowed: string[]) { + this.allowed = allowed; + this.decorations = this.buildDecorations(view); + } + + update(update: ViewUpdate) { + if (update.docChanged || update.viewportChanged) { + this.decorations = this.buildDecorations(update.view); + } + } + + buildDecorations(view: EditorView): DecorationSet { + const builder = new RangeSetBuilder(); + const tree = syntaxTree(view.state); + const doc = view.state.doc; + + tree.iterate({ + enter: node => { + if (node.name === 'Variable') { + // Extract inner text from the Variable node ({{my_var}}) + const text = doc.sliceString(node.from, node.to); + const match = /\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/.exec(text); + const varName = match?.[1]; + + if (!varName || !this.allowed.includes(varName)) { + builder.add(node.from, node.to, invalidVarMark); + } else { + builder.add(node.from, node.to, validMark); + } + } + } + }); + + return builder.finish(); + } +} + +/** Returns a ViewPlugin that marks invalid variables in the editor. */ +export function markVariables(allowed: string[]) { + return ViewPlugin.fromClass( + class extends MarkVariablesPlugin { + constructor(view: EditorView) { + super(view, allowed); + } + }, + { + decorations: plugin => plugin.decorations + } + ); +} diff --git a/rsconcept/frontend/src/features/ai/components/prompt-input/no-spellcheck.tsx b/rsconcept/frontend/src/features/ai/components/prompt-input/no-spellcheck.tsx new file mode 100644 index 00000000..5257e5d6 --- /dev/null +++ b/rsconcept/frontend/src/features/ai/components/prompt-input/no-spellcheck.tsx @@ -0,0 +1,45 @@ +import { syntaxTree } from '@codemirror/language'; +import { RangeSetBuilder } from '@codemirror/state'; +import { Decoration, type DecorationSet, type EditorView, ViewPlugin, type ViewUpdate } from '@codemirror/view'; + +const noSpellcheckMark = Decoration.mark({ + attributes: { spellcheck: 'false' } +}); + +class NoSpellcheckPlugin { + decorations: DecorationSet; + + constructor(view: EditorView) { + this.decorations = this.buildDecorations(view); + } + + update(update: ViewUpdate) { + if (update.docChanged || update.viewportChanged) { + this.decorations = this.buildDecorations(update.view); + } + } + + buildDecorations(view: EditorView): DecorationSet { + const builder = new RangeSetBuilder(); + + const tree = syntaxTree(view.state); + for (const { from, to } of view.visibleRanges) { + tree.iterate({ + from, + to, + enter: node => { + if (node.name === 'Variable') { + builder.add(node.from, node.to, noSpellcheckMark); + } + } + }); + } + + return builder.finish(); + } +} + +/** Plugin that adds a no-spellcheck attribute to all variables in the editor. */ +export const noSpellcheckForVariables = ViewPlugin.fromClass(NoSpellcheckPlugin, { + decorations: (plugin: NoSpellcheckPlugin) => plugin.decorations +}); diff --git a/rsconcept/frontend/src/features/ai/components/prompt-input/parse/highlight.ts b/rsconcept/frontend/src/features/ai/components/prompt-input/parse/highlight.ts new file mode 100644 index 00000000..43cf1716 --- /dev/null +++ b/rsconcept/frontend/src/features/ai/components/prompt-input/parse/highlight.ts @@ -0,0 +1,6 @@ +import { styleTags, tags } from '@lezer/highlight'; + +export const highlighting = styleTags({ + Variable: tags.name, + Error: tags.comment +}); diff --git a/rsconcept/frontend/src/features/ai/components/prompt-input/parse/index.ts b/rsconcept/frontend/src/features/ai/components/prompt-input/parse/index.ts new file mode 100644 index 00000000..06dcaa87 --- /dev/null +++ b/rsconcept/frontend/src/features/ai/components/prompt-input/parse/index.ts @@ -0,0 +1,8 @@ +import { LRLanguage } from '@codemirror/language'; + +import { parser } from './parser'; + +export const PromptLanguage = LRLanguage.define({ + parser: parser, + languageData: {} +}); diff --git a/rsconcept/frontend/src/features/ai/components/prompt-input/parse/parser.terms.ts b/rsconcept/frontend/src/features/ai/components/prompt-input/parse/parser.terms.ts new file mode 100644 index 00000000..c4b9399b --- /dev/null +++ b/rsconcept/frontend/src/features/ai/components/prompt-input/parse/parser.terms.ts @@ -0,0 +1,5 @@ +// This file was generated by lezer-generator. You probably shouldn't edit it. +export const Text = 1, + Variable = 2, + Error = 3, + Filler = 4; diff --git a/rsconcept/frontend/src/features/ai/components/prompt-input/parse/parser.test.ts b/rsconcept/frontend/src/features/ai/components/prompt-input/parse/parser.test.ts new file mode 100644 index 00000000..16166413 --- /dev/null +++ b/rsconcept/frontend/src/features/ai/components/prompt-input/parse/parser.test.ts @@ -0,0 +1,30 @@ +import { printTree } from '@/utils/codemirror'; + +import { parser } from './parser'; + +const testData = [ + ['', '[Text]'], + ['тест русский', '[Text[Filler]]'], + ['test english', '[Text[Filler]]'], + ['test greek σσσ', '[Text[Filler]]'], + ['X1 раз два X2', '[Text[Filler]]'], + ['{{variable}}', '[Text[Variable]]'], + ['{{var_1}}', '[Text[Variable]]'], + ['{{user.name}}', '[Text[Variable]]'], + ['!error!', '[Text[Error]]'], + ['word !error! word', '[Text[Filler][Error][Filler]]'], + ['{{variable}} !error! word', '[Text[Variable][Error][Filler]]'], + ['word {{variable}}', '[Text[Filler][Variable]]'], + ['word {{variable}} !error!', '[Text[Filler][Variable][Error]]'], + ['{{variable}} word', '[Text[Variable][Filler]]'], + ['!err! {{variable}}', '[Text[Error][Variable]]'], + ['!err! {{variable}} word', '[Text[Error][Variable][Filler]]'] +] as const; + +/** Test prompt grammar parser with various prompt inputs */ +describe('Prompt grammar parser', () => { + it.each(testData)('Parse %p', (input: string, expectedTree: string) => { + const tree = parser.parse(input); + expect(printTree(tree)).toBe(expectedTree); + }); +}); diff --git a/rsconcept/frontend/src/features/ai/components/prompt-input/parse/parser.ts b/rsconcept/frontend/src/features/ai/components/prompt-input/parse/parser.ts new file mode 100644 index 00000000..1aec37da --- /dev/null +++ b/rsconcept/frontend/src/features/ai/components/prompt-input/parse/parser.ts @@ -0,0 +1,20 @@ +// This file was generated by lezer-generator. You probably shouldn't edit it. +import { LRParser } from '@lezer/lr'; +import { highlighting } from './highlight'; +export const parser = LRParser.deserialize({ + version: 14, + states: + "!vQVQPOOObQQO'#C^OgQPO'#CjO]QPO'#C_OOQO'#C`'#C`OOQO'#Ce'#CeOOQO'#Ca'#CaQVQPOOOuQPO,58xOOQO,59U,59UOzQPO,58yOOQO-E6_-E6_OOQO1G.d1G.dOOQO1G.e1G.e", + stateData: '!S~OWOS~OZPO]RO_QO~O[WO~O_QOU^XZ^X]^X~OY[O~O]]O~O_W~', + goto: 'x_PP```dPPPjPPPPnTTOVQVORZVTUOVSSOVQXQRYR', + nodeNames: '⚠ Text Variable Error Filler', + maxTerm: 15, + propSources: [highlighting], + skippedNodes: [0], + repeatNodeCount: 1, + tokenData: + "(^~RqOX#YXZ#zZ^$o^p#Ypq#zqr&hr!b#Y!c!}&m!}#R#Y#R#S&m#S#T#Y#T#o&m#o#p'v#q#r(R#r#y#Y#y#z$o#z$f#Y$f$g$o$g#BY#Y#BY#BZ$o#BZ$IS#Y$IS$I_$o$I_$I|#Y$I|$JO$o$JO$JT#Y$JT$JU$o$JU$KV#Y$KV$KW$o$KW&FU#Y&FU&FV$o&FV;'S#Y;'S;=`#t<%lO#YP#_V_POX#YZp#Yr!b#Y!c#o#Y#r;'S#Y;'S;=`#t<%lO#YP#wP;=`<%l#Y~$PYW~X^#zpq#z#y#z#z$f$g#z#BY#BZ#z$IS$I_#z$I|$JO#z$JT$JU#z$KV$KW#z&FU&FV#z~$vj_PW~OX#YXZ#zZ^$o^p#Ypq#zr!b#Y!c#o#Y#r#y#Y#y#z$o#z$f#Y$f$g$o$g#BY#Y#BY#BZ$o#BZ$IS#Y$IS$I_$o$I_$I|#Y$I|$JO$o$JO$JT#Y$JT$JU$o$JU$KV#Y$KV$KW$o$KW&FU#Y&FU&FV$o&FV;'S#Y;'S;=`#t<%lO#Y~&mO]~R&t`[Q_POX#YZp#Yr}#Y}!O&m!O!P&m!P!Q#Y!Q![&m![!b#Y!c!}&m!}#R#Y#R#S&m#S#T#Y#T#o&m#r;'S#Y;'S;=`#t<%lO#Y~'yP#o#p'|~(ROZ~~(UP#q#r(X~(^OY~", + tokenizers: [0, 1], + topRules: { Text: [0, 1] }, + tokenPrec: 47 +}); diff --git a/rsconcept/frontend/src/features/ai/components/prompt-input/parse/prompt-text.grammar b/rsconcept/frontend/src/features/ai/components/prompt-input/parse/prompt-text.grammar new file mode 100644 index 00000000..8b75a06d --- /dev/null +++ b/rsconcept/frontend/src/features/ai/components/prompt-input/parse/prompt-text.grammar @@ -0,0 +1,39 @@ +@precedence { + text @right + p1 + p2 + p3 +} + +@top Text { textItem* } + +@skip { space } + +@tokens { + space { @whitespace+ } + variable { $[a-zA-Z_]$[a-zA-Z0-9_.-]* } + word { ![@{|}! \t\n]+ } + + @precedence { word, space } +} + +textItem { + !p1 Variable | + !p2 Error | + !p3 Filler +} + +Filler { word_enum } +Error { "!" word_enum "!" } +word_enum { + word | + word !text word_enum +} + +Variable { + "{{" variable "}}" +} + +@detectDelim + +@external propSource highlighting from "./highlight" \ No newline at end of file diff --git a/rsconcept/frontend/src/features/ai/components/prompt-input/prompt-input.tsx b/rsconcept/frontend/src/features/ai/components/prompt-input/prompt-input.tsx new file mode 100644 index 00000000..450f19a4 --- /dev/null +++ b/rsconcept/frontend/src/features/ai/components/prompt-input/prompt-input.tsx @@ -0,0 +1,144 @@ +'use client'; + +import { forwardRef, useRef } from 'react'; +import { autocompletion } from '@codemirror/autocomplete'; +import { type Extension } from '@codemirror/state'; +import { tags } from '@lezer/highlight'; +import { createTheme } from '@uiw/codemirror-themes'; +import CodeMirror, { + type BasicSetupOptions, + type ReactCodeMirrorProps, + type ReactCodeMirrorRef +} from '@uiw/react-codemirror'; +import clsx from 'clsx'; +import { EditorView } from 'codemirror'; + +import { Label } from '@/components/input'; +import { usePreferencesStore } from '@/stores/preferences'; +import { APP_COLORS } from '@/styling/colors'; + +import { PromptVariableType } from '../../models/prompting'; + +import { variableCompletions } from './completion'; +import { markVariables } from './mark-variables'; +import { noSpellcheckForVariables } from './no-spellcheck'; +import { PromptLanguage } from './parse'; +import { variableHoverTooltip } from './tooltip'; + +const EDITOR_OPTIONS: BasicSetupOptions = { + highlightSpecialChars: false, + history: true, + drawSelection: false, + syntaxHighlighting: false, + defaultKeymap: true, + historyKeymap: true, + + lineNumbers: false, + highlightActiveLineGutter: false, + foldGutter: false, + dropCursor: true, + allowMultipleSelections: false, + indentOnInput: false, + bracketMatching: false, + closeBrackets: false, + autocompletion: false, + rectangularSelection: false, + crosshairCursor: false, + highlightActiveLine: false, + highlightSelectionMatches: false, + closeBracketsKeymap: false, + searchKeymap: false, + foldKeymap: false, + completionKeymap: false, + lintKeymap: false +}; + +interface PromptInputProps + extends Pick< + ReactCodeMirrorProps, + | 'id' // + | 'height' + | 'minHeight' + | 'maxHeight' + | 'value' + | 'onFocus' + | 'onBlur' + | 'placeholder' + | 'style' + | 'className' + > { + value: string; + onChange: (newValue: string) => void; + availableVariables?: string[]; + + label?: string; + disabled?: boolean; + initialValue?: string; +} + +export const PromptInput = forwardRef( + ( + { + id, // + label, + disabled, + onChange, + ...restProps + }, + ref + ) => { + const darkMode = usePreferencesStore(state => state.darkMode); + + const internalRef = useRef(null); + const thisRef = !ref || typeof ref === 'function' ? internalRef : ref; + + const cursor = !disabled ? 'cursor-text' : 'cursor-default'; + const customTheme: Extension = createTheme({ + theme: darkMode ? 'dark' : 'light', + settings: { + fontFamily: 'inherit', + background: !disabled ? APP_COLORS.bgInput : APP_COLORS.bgDefault, + foreground: APP_COLORS.fgDefault, + caret: APP_COLORS.fgDefault + }, + styles: [ + { tag: tags.name, cursor: 'default' }, // Variable + { tag: tags.comment, color: APP_COLORS.fgRed } // Error + ] + }); + + const variables = restProps.availableVariables ?? Object.values(PromptVariableType); + const autoCompleter = autocompletion({ + override: [variableCompletions(variables)], + activateOnTyping: true, + icons: false + }); + + const editorExtensions = [ + EditorView.lineWrapping, + EditorView.contentAttributes.of({ spellcheck: 'true' }), + PromptLanguage, + variableHoverTooltip(variables), + autoCompleter, + noSpellcheckForVariables, + markVariables(variables) + ]; + + return ( +
+
+ ); + } +); diff --git a/rsconcept/frontend/src/features/ai/components/prompt-input/tooltip.ts b/rsconcept/frontend/src/features/ai/components/prompt-input/tooltip.ts new file mode 100644 index 00000000..be1b1a7b --- /dev/null +++ b/rsconcept/frontend/src/features/ai/components/prompt-input/tooltip.ts @@ -0,0 +1,87 @@ +import { syntaxTree } from '@codemirror/language'; +import { type EditorState, type Extension } from '@codemirror/state'; +import { hoverTooltip, type TooltipView } from '@codemirror/view'; +import clsx from 'clsx'; + +import { findEnvelopingNodes } from '@/utils/codemirror'; + +import { describePromptVariable } from '../../labels'; +import { type PromptVariableType } from '../../models/prompting'; + +import { Variable } from './parse/parser.terms'; + +/** + * Retrieves variable from position in Editor. + */ +function findVariableAt(pos: number, state: EditorState) { + const nodes = findEnvelopingNodes(pos, pos, syntaxTree(state), [Variable]); + if (nodes.length !== 1) { + return undefined; + } + const start = nodes[0].from; + const end = nodes[0].to; + const text = state.doc.sliceString(start, end); + const match = /\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/.exec(text); + const varName = match?.[1]; + if (!varName) { + return undefined; + } + return { + varName: varName, + start: start, + end: end + }; +} + +const tooltipProducer = (available: string[]) => { + return hoverTooltip((view, pos) => { + const parse = findVariableAt(pos, view.state); + if (!parse) { + return null; + } + + const isAvailable = available.includes(parse.varName); + return { + pos: parse.start, + end: parse.end, + above: false, + create: () => domTooltipVariable(parse.varName, isAvailable) + }; + }); +}; + +export function variableHoverTooltip(available: string[]): Extension { + return [tooltipProducer(available)]; +} + +/** + * Create DOM tooltip for {@link PromptVariableType}. + */ +function domTooltipVariable(varName: string, isAvailable: boolean): TooltipView { + const dom = document.createElement('div'); + dom.className = clsx( + 'max-h-100 max-w-100 min-w-40', + 'dense', + 'px-2 py-1 flex flex-col', + 'rounded-md shadow-md', + 'cc-scroll-y', + 'text-sm bg-card', + 'select-none cursor-auto' + ); + + const header = document.createElement('p'); + header.innerHTML = `Переменная ${varName}`; + dom.appendChild(header); + + const status = document.createElement('p'); + status.className = isAvailable ? 'text-green-700' : 'text-red-700'; + status.innerText = isAvailable ? 'Доступна для использования' : 'Недоступна для использования'; + dom.appendChild(status); + + const desc = document.createElement('p'); + desc.className = ''; + desc.innerText = `Описание: ${describePromptVariable(varName as PromptVariableType)}`; + dom.appendChild(desc); + + return { dom: dom }; +} diff --git a/rsconcept/frontend/src/features/ai/dialogs/dlg-ai-prompt/tab-prompt-edit.tsx b/rsconcept/frontend/src/features/ai/dialogs/dlg-ai-prompt/tab-prompt-edit.tsx index 0a35906c..6fc70157 100644 --- a/rsconcept/frontend/src/features/ai/dialogs/dlg-ai-prompt/tab-prompt-edit.tsx +++ b/rsconcept/frontend/src/features/ai/dialogs/dlg-ai-prompt/tab-prompt-edit.tsx @@ -1,5 +1,8 @@ import { TextArea } from '@/components/input'; +import { PromptInput } from '../../components/prompt-input'; +import { useAvailableVariables } from '../../stores/use-available-variables'; + interface TabPromptEditProps { label: string; description: string; @@ -8,6 +11,7 @@ interface TabPromptEditProps { } export function TabPromptEdit({ label, description, text, setText }: TabPromptEditProps) { + const availableVariables = useAvailableVariables(); return (
@@ -20,13 +24,13 @@ export function TabPromptEdit({ label, description, text, setText }: TabPromptEd rows={1} />