-
Notifications
You must be signed in to change notification settings - Fork 253
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2417 from Agenta-AI/feat/AGE-1430/-new-playgroud-…
…text-editor [Feat]: New-playground Text Editor
- Loading branch information
Showing
42 changed files
with
3,525 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
import {LexicalComposer} from "@lexical/react/LexicalComposer" | ||
import {EditorState, LexicalEditor} from "lexical" | ||
import {$getRoot} from "lexical" | ||
import {useEditorResize} from "./hooks/useEditorResize" | ||
import {useEditorInvariant} from "./hooks/useEditorInvariant" | ||
import {useEditorConfig} from "./hooks/useEditorConfig" | ||
import EditorPlugins from "./plugins" | ||
|
||
import type {EditorProps} from "./types" | ||
import {useCallback} from "react" | ||
/** | ||
* Editor component | ||
* | ||
* @param {string} id - Unique identifier for the editor instance. | ||
* @param {string} initialValue - Initial value of the editor content. | ||
* @param {function} onChange - Callback function to handle content changes. | ||
* @param {string} placeholder - Placeholder text for the editor. | ||
* @param {boolean} singleLine - If true, the editor will be single-line. | ||
* @param {boolean} codeOnly - If true, the editor will be in code-only mode. | ||
* @param {string} language - Programming language for code highlighting. | ||
* @param {boolean} showToolbar - If true, the toolbar will be shown. | ||
* @param {boolean} enableTokens - If true, token functionality will be enabled. | ||
* @param {boolean} enableResize - If true, the editor will be resizable. | ||
* @param {boolean} boundWidth - If true, the editor width will be bounded to the parent width. | ||
* @param {boolean} boundHeight - If true, the editor height will be bounded to the parent height. | ||
* @param {boolean} debug - If true, debug information will be shown. | ||
*/ | ||
export function Editor({ | ||
id = crypto.randomUUID(), | ||
initialValue = "", | ||
onChange, | ||
placeholder = "Enter some text...", | ||
singleLine = false, | ||
codeOnly = false, | ||
language, | ||
showToolbar = true, | ||
enableTokens = false, | ||
debug = false, | ||
enableResize = false, // New prop | ||
boundWidth = true, // New prop | ||
boundHeight, // New prop | ||
}: EditorProps) { | ||
useEditorInvariant({ | ||
singleLine, | ||
enableResize, | ||
codeOnly, | ||
enableTokens, | ||
showToolbar, | ||
language, | ||
}) | ||
|
||
const {containerRef, dimensions} = useEditorResize({ | ||
singleLine, | ||
enableResize, | ||
boundWidth, | ||
boundHeight, | ||
}) | ||
|
||
const config = useEditorConfig({ | ||
id, | ||
initialValue, | ||
codeOnly, | ||
enableTokens, | ||
}) | ||
|
||
const handleUpdate = useCallback( | ||
(editorState: EditorState, _editor: LexicalEditor) => { | ||
editorState.read(() => { | ||
const root = $getRoot() | ||
const textContent = root.getTextContent() | ||
const tokens: unknown[] = [] // Extract tokens if needed | ||
|
||
const result = { | ||
value: "", // Omit this for now | ||
textContent, | ||
tokens, | ||
} | ||
|
||
if (onChange) { | ||
onChange(result) | ||
} | ||
}) | ||
}, | ||
[onChange], | ||
) | ||
|
||
if (!config) { | ||
return ( | ||
<div | ||
className="bg-white relative flex flex-col p-2 border rounded-lg" | ||
style={ | ||
dimensions.width | ||
? { | ||
width: dimensions.width, | ||
height: dimensions.height, | ||
} | ||
: undefined | ||
} | ||
> | ||
<div className="editor-placeholder">{placeholder}</div> | ||
</div> | ||
) | ||
} | ||
|
||
return ( | ||
<div className="bg-white relative flex flex-col p-2"> | ||
<LexicalComposer initialConfig={config}> | ||
<div className="editor-container overflow-hidden relative"> | ||
<div | ||
ref={containerRef} | ||
className={`editor-inner border rounded-lg ${singleLine ? "single-line" : ""}`} | ||
style={ | ||
dimensions.width | ||
? { | ||
width: dimensions.width, | ||
height: dimensions.height, | ||
} | ||
: undefined | ||
} | ||
> | ||
<EditorPlugins | ||
showToolbar={showToolbar} | ||
singleLine={singleLine} | ||
codeOnly={codeOnly} | ||
enableTokens={enableTokens} | ||
debug={debug} | ||
language={language} | ||
placeholder={placeholder} | ||
handleUpdate={handleUpdate} | ||
/> | ||
{!singleLine && enableResize && <div className="resize-handle" />} | ||
</div> | ||
</div> | ||
</LexicalComposer> | ||
</div> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
export const theme = { | ||
code: "editor-code", | ||
codeHighlight: { | ||
atrule: "editor-tokenAttr", | ||
attr: "editor-tokenAttr", | ||
boolean: "editor-tokenProperty", | ||
builtin: "editor-tokenSelector", | ||
cdata: "editor-tokenComment", | ||
char: "editor-tokenSelector", | ||
class: "editor-tokenFunction", | ||
"class-name": "editor-tokenFunction", | ||
comment: "editor-tokenComment", | ||
constant: "editor-tokenProperty", | ||
deleted: "editor-tokenProperty", | ||
doctype: "editor-tokenComment", | ||
entity: "editor-tokenOperator", | ||
function: "editor-tokenFunction", | ||
important: "editor-tokenVariable", | ||
inserted: "editor-tokenSelector", | ||
keyword: "editor-tokenAttr", | ||
namespace: "editor-tokenVariable", | ||
number: "editor-tokenProperty", | ||
operator: "editor-tokenOperator", | ||
prolog: "editor-tokenComment", | ||
property: "editor-tokenProperty", | ||
punctuation: "editor-tokenPunctuation", | ||
regex: "editor-tokenVariable", | ||
selector: "editor-tokenSelector", | ||
string: "editor-tokenSelector", | ||
symbol: "editor-tokenProperty", | ||
tag: "editor-tokenProperty", | ||
url: "editor-tokenOperator", | ||
variable: "editor-tokenVariable", | ||
}, | ||
} |
51 changes: 51 additions & 0 deletions
51
agenta-web/src/components/Editor/hooks/useEditorConfig/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
import {useEffect, useState, useRef} from "react" | ||
import {theme} from "../../assets/theme" | ||
import {TokenNode} from "../../plugins/token/TokenNode" | ||
import {TokenInputNode} from "../../plugins/token/TokenInputNode" | ||
import {LexicalComposer} from "@lexical/react/LexicalComposer" | ||
import {ComponentProps} from "react" | ||
import type {EditorProps} from "../../types" | ||
|
||
type LexicalComposerProps = ComponentProps<typeof LexicalComposer> | ||
|
||
export function useEditorConfig({ | ||
id, | ||
initialValue, | ||
codeOnly, | ||
enableTokens, | ||
}: Pick<EditorProps, "id" | "initialValue" | "codeOnly" | "enableTokens">): | ||
| LexicalComposerProps["initialConfig"] | ||
| null { | ||
const [config, setConfig] = useState<LexicalComposerProps["initialConfig"] | null>(null) | ||
const configRef = useRef<LexicalComposerProps["initialConfig"] | null>(null) | ||
|
||
useEffect(() => { | ||
const loadConfig = async () => { | ||
if (configRef.current) return | ||
|
||
const nodes = codeOnly | ||
? [ | ||
(await import("../../plugins/code/CodeNode/CodeNode")).CodeNode, | ||
(await import("../../plugins/code/CodeNode/CodeHighlightNode")) | ||
.CodeHighlightNode, | ||
(await import("../../plugins/code/CodeNode/CodeLineNode")).CodeLineNode, | ||
] | ||
: [...(enableTokens ? [TokenNode, TokenInputNode] : [])] | ||
|
||
const newConfig = { | ||
namespace: `editor-${id}`, | ||
onError: console.error, | ||
nodes, | ||
editorState: initialValue || undefined, | ||
theme, | ||
} | ||
|
||
configRef.current = newConfig | ||
setConfig(newConfig) | ||
} | ||
|
||
loadConfig() | ||
}, [codeOnly, enableTokens, id, initialValue]) | ||
|
||
return config | ||
} |
45 changes: 45 additions & 0 deletions
45
agenta-web/src/components/Editor/hooks/useEditorInvariant.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import {useEffect} from "react" | ||
import type {EditorProps} from "../types" | ||
|
||
export function useEditorInvariant({ | ||
singleLine, | ||
enableResize, | ||
codeOnly, | ||
enableTokens, | ||
showToolbar, | ||
language, | ||
}: Pick< | ||
EditorProps, | ||
"singleLine" | "enableResize" | "codeOnly" | "enableTokens" | "showToolbar" | "language" | ||
>) { | ||
useEffect(() => { | ||
if (singleLine && enableResize) { | ||
throw new Error( | ||
"Invalid configuration: 'singleLine' and 'enableResize' cannot be used together.", | ||
) | ||
} | ||
if (singleLine && codeOnly) { | ||
throw new Error( | ||
"Invalid configuration: 'singleLine' and 'codeOnly' cannot be used together.", | ||
) | ||
} | ||
if (codeOnly && enableTokens) { | ||
throw new Error( | ||
"Invalid configuration: 'codeOnly' and 'enableTokens' cannot be used together.", | ||
) | ||
} | ||
if (codeOnly && showToolbar) { | ||
throw new Error( | ||
"Invalid configuration: 'codeOnly' and 'showToolbar' cannot be used together.", | ||
) | ||
} | ||
if (singleLine && showToolbar) { | ||
throw new Error( | ||
"Invalid configuration: 'singleLine' and 'showToolbar' cannot be used together.", | ||
) | ||
} | ||
if (language && !codeOnly) { | ||
throw new Error("Invalid configuration: 'language' prop is only valid with 'codeOnly'.") | ||
} | ||
}, [singleLine, enableResize, codeOnly, enableTokens, showToolbar, language]) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
import {useEffect, useRef, useState} from "react" | ||
import type {EditorProps} from "../types" | ||
|
||
export function useEditorResize({ | ||
singleLine, | ||
enableResize, | ||
boundWidth, | ||
boundHeight, | ||
}: Pick<EditorProps, "singleLine" | "enableResize" | "boundWidth" | "boundHeight">) { | ||
const containerRef = useRef<HTMLDivElement>(null) | ||
const isResizing = useRef(false) | ||
const [dimensions, setDimensions] = useState({width: 0, height: 0}) | ||
|
||
useEffect(() => { | ||
if (!containerRef.current || singleLine || !enableResize) { | ||
return | ||
} | ||
|
||
const container = containerRef.current | ||
const handle = container.querySelector(".resize-handle") as HTMLElement | ||
if (!handle) { | ||
return | ||
} | ||
|
||
const startResize = (e: MouseEvent) => { | ||
e.preventDefault() | ||
isResizing.current = true | ||
} | ||
|
||
const stopResize = () => { | ||
isResizing.current = false | ||
} | ||
|
||
const resize = (e: MouseEvent) => { | ||
if (!isResizing.current || !container.parentElement) return | ||
|
||
const parentRect = container.parentElement.getBoundingClientRect() | ||
let width = e.clientX - parentRect.left | ||
let height = e.clientY - parentRect.top | ||
|
||
if (boundWidth) { | ||
width = Math.max(200, Math.min(width, parentRect.width)) | ||
} else { | ||
width = Math.max(200, width) | ||
} | ||
|
||
if (boundHeight) { | ||
height = Math.max(100, Math.min(height, parentRect.height)) | ||
} else { | ||
height = Math.max(100, height) | ||
} | ||
|
||
setDimensions({width, height}) | ||
} | ||
|
||
const throttledResize = (e: MouseEvent) => { | ||
requestAnimationFrame(() => resize(e)) | ||
} | ||
|
||
handle.addEventListener("mousedown", startResize) | ||
document.addEventListener("mousemove", throttledResize) | ||
document.addEventListener("mouseup", stopResize) | ||
|
||
return () => { | ||
handle.removeEventListener("mousedown", startResize) | ||
document.removeEventListener("mousemove", throttledResize) | ||
document.removeEventListener("mouseup", stopResize) | ||
} | ||
}, [singleLine, enableResize, boundWidth, boundHeight, containerRef.current]) | ||
|
||
return {containerRef, dimensions} | ||
} |
Oops, something went wrong.