From 78964b23cc1359a4d35a4f91c6b8f1127bc223c2 Mon Sep 17 00:00:00 2001 From: Ivan <8611739+IRBorisov@users.noreply.github.com> Date: Tue, 22 Jul 2025 14:52:03 +0300 Subject: [PATCH] F: Prompt editor component pt1 --- rsconcept/frontend/package.json | 2 +- .../ai/components/prompt-input/index.tsx | 1 + .../prompt-input/parse/highlight.ts | 6 + .../ai/components/prompt-input/parse/index.ts | 8 + .../prompt-input/parse/parser.terms.ts | 6 + .../prompt-input/parse/parser.test.ts | 30 ++++ .../components/prompt-input/parse/parser.ts | 18 +++ .../prompt-input/parse/prompt-text.grammar | 39 +++++ .../components/prompt-input/prompt-input.tsx | 143 ++++++++++++++++++ .../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 ++++---- 13 files changed, 321 insertions(+), 73 deletions(-) create mode 100644 rsconcept/frontend/src/features/ai/components/prompt-input/index.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 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/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/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..bef36bc7 --- /dev/null +++ b/rsconcept/frontend/src/features/ai/components/prompt-input/parse/parser.terms.ts @@ -0,0 +1,6 @@ +// 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..73d181ee --- /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..d4363a4d --- /dev/null +++ b/rsconcept/frontend/src/features/ai/components/prompt-input/parse/parser.ts @@ -0,0 +1,18 @@ +// 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: "!vQVQPOOObQPO'#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: "!U~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: ")O~RrOX#]XZ$QZ^$u^p#]pq$Qqr&qr!O#]!P!b#]!c!}&v!}#R#]#R#S&v#S#T#]#T#o&v#o#p(h#q#r(s#r#y#]#y#z$u#z$f#]$f$g$u$g#BY#]#BY#BZ$u#BZ$IS#]$IS$I_$u$I_$I|#]$I|$JO$u$JO$JT#]$JT$JU$u$JU$KV#]$KV$KW$u$KW&FU#]&FU&FV$u&FV;'S#];'S;=`#z<%lO#]~#bW_~OX#]Zp#]r!O#]!P!b#]!c#o#]#r;'S#];'S;=`#z<%lO#]~#}P;=`<%l#]~$VYW~X^$Qpq$Q#y#z$Q$f$g$Q#BY#BZ$Q$IS$I_$Q$I|$JO$Q$JT$JU$Q$KV$KW$Q&FU&FV$Q~$|k_~W~OX#]XZ$QZ^$u^p#]pq$Qr!O#]!P!b#]!c#o#]#r#y#]#y#z$u#z$f#]$f$g$u$g#BY#]#BY#BZ$u#BZ$IS#]$IS$I_$u$I_$I|#]$I|$JO$u$JO$JT#]$JT$JU$u$JU$KV#]$KV$KW$u$KW&FU#]&FU&FV$u&FV;'S#];'S;=`#z<%lO#]~&vO]~~&}`[~_~OX#]Zp#]r}#]}!O&v!O!P(P!P!Q#]!Q![&v![!b#]!c!}&v!}#R#]#R#S&v#S#T#]#T#o&v#r;'S#];'S;=`#z<%lO#]~(UU[~}!O(P!O!P(P!Q![(P!c!}(P#R#S(P#T#o(P~(kP#o#p(n~(sOZ~~(vP#q#r(y~)OOY~", + tokenizers: [0], + 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..6bd4b32c --- /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 { variable, 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..ead3174e --- /dev/null +++ b/rsconcept/frontend/src/features/ai/components/prompt-input/prompt-input.tsx @@ -0,0 +1,143 @@ +'use client'; + +import { forwardRef, useRef } from 'react'; +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 { notImplemented } from '@/utils/utils'; + +import { useAvailableVariables } from '../../stores/use-available-variables'; + +import { PromptLanguage } from './parse'; + +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; + + label?: string; + disabled?: boolean; + initialValue?: string; +} + +export const PromptInput = forwardRef( + ( + { + id, // + label, + disabled, + onChange, + ...restProps + }, + ref + ) => { + const darkMode = usePreferencesStore(state => state.darkMode); + const availableVariables = useAvailableVariables(); + console.log(availableVariables); + + 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, color: APP_COLORS.fgPurple, cursor: 'default' }, // Variable + { tag: tags.comment, color: APP_COLORS.fgRed } // Error + ] + }); + + const editorExtensions = [ + EditorView.lineWrapping, + EditorView.contentAttributes.of({ spellcheck: 'true' }), + PromptLanguage + ]; + + function handleInput(event: React.KeyboardEvent) { + if (!thisRef.current?.view) { + event.preventDefault(); + event.stopPropagation(); + return; + } + if ((event.ctrlKey || event.metaKey) && event.code === 'Space') { + notImplemented(); + return; + } + } + + return ( +
+
+ ); + } +); diff --git a/rsconcept/frontend/src/features/rsform/components/refs-input/parse/parser.terms.ts b/rsconcept/frontend/src/features/rsform/components/refs-input/parse/parser.terms.ts index fa3d74ce..f00e4e4d 100644 --- a/rsconcept/frontend/src/features/rsform/components/refs-input/parse/parser.terms.ts +++ b/rsconcept/frontend/src/features/rsform/components/refs-input/parse/parser.terms.ts @@ -1,6 +1,5 @@ // This file was generated by lezer-generator. You probably shouldn't edit it. -export const - Text = 1, +export const Text = 1, RefEntity = 2, Global = 3, Grams = 4, @@ -8,4 +7,4 @@ export const Offset = 6, Nominal = 7, Error = 8, - Filler = 9 + Filler = 9; diff --git a/rsconcept/frontend/src/features/rsform/components/refs-input/parse/parser.ts b/rsconcept/frontend/src/features/rsform/components/refs-input/parse/parser.ts index e7d332b3..0aab7c64 100644 --- a/rsconcept/frontend/src/features/rsform/components/refs-input/parse/parser.ts +++ b/rsconcept/frontend/src/features/rsform/components/refs-input/parse/parser.ts @@ -1,18 +1,20 @@ // This file was generated by lezer-generator. You probably shouldn't edit it. -import {LRParser} from "@lezer/lr" -import {highlighting} from "./highlight" +import { LRParser } from '@lezer/lr'; +import { highlighting } from './highlight'; export const parser = LRParser.deserialize({ version: 14, - states: "$nQVQPOOObQQO'#C^OOQO'#Cl'#ClOjQPO'#CuOxQPO'#CdOOQO'#Ce'#CeOOQO'#Ck'#CkOOQO'#Cf'#CfQVQPOOO}QPO,58xO!SQPO,58{OOQO,59a,59aO!XQPO,59OOOQO-E6d-E6dO!^QSO1G.dO!cQPO1G.gOOQO1G.j1G.jO!hQQO'#CpOOQO'#C`'#C`O!pQPO7+$OOOQO'#Cg'#CgO!uQPO'#CcO!}QPO7+$RO!^QSO,59[OOQO<( ); } ); - -// ======= Internal ========== -const editorSetup: 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 -}; diff --git a/rsconcept/frontend/src/features/rsform/components/rs-input/rs-input.tsx b/rsconcept/frontend/src/features/rsform/components/rs-input/rs-input.tsx index cc57b112..4756cbcf 100644 --- a/rsconcept/frontend/src/features/rsform/components/rs-input/rs-input.tsx +++ b/rsconcept/frontend/src/features/rsform/components/rs-input/rs-input.tsx @@ -26,6 +26,34 @@ import { RSLanguage } from './rslang'; import { getSymbolSubstitute, RSTextWrapper } from './text-editing'; import { rsHoverTooltip } from './tooltip'; +const editorSetup: 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 RSInputProps extends Pick< ReactCodeMirrorProps, @@ -63,7 +91,6 @@ export const RSInput = forwardRef( className, style, - onChange, onAnalyze, ...restProps }, @@ -169,7 +196,6 @@ export const RSInput = forwardRef( theme={customTheme} extensions={editorExtensions} indentWithTab={false} - onChange={onChange} editable={!disabled} onKeyDown={handleInput} {...restProps} @@ -178,32 +204,3 @@ export const RSInput = forwardRef( ); } ); - -// ======= Internal ========== -const editorSetup: 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 -};