diff --git a/bun.lockb b/bun.lockb index cc5a448..6ebbd7a 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 90f7aa3..13645a1 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "dependencies": { "@huggingface/jinja": "0.3.1", "@inquirer/select": "2.3.10", + "ace-builds": "1.36.3", "classnames": "2.5.1", "preact": "10.22.0" }, diff --git a/src/common/hooks/useIsVisible.ts b/src/common/hooks/useIsVisible.ts new file mode 100644 index 0000000..a5baa38 --- /dev/null +++ b/src/common/hooks/useIsVisible.ts @@ -0,0 +1,22 @@ +import { useEffect, useState, type Ref } from "preact/hooks"; + +export const useIsVisible = (ref: Ref, onlyFirst = false) => { + const [isVisible, setVisible] = useState(false); + + useEffect(() => { + if (ref.current) { + const observer = new IntersectionObserver(([entry]) => { + setVisible(entry.isIntersecting); + if (entry.isIntersecting && onlyFirst) { + observer.disconnect(); + } + }); + + observer.observe(ref.current); + + return () => observer.disconnect(); + } + }, [ref.current, onlyFirst]); + + return isVisible; +} \ No newline at end of file diff --git a/src/games/ai/assets/style.css b/src/games/ai/assets/style.css index 90c921f..f9477b2 100644 --- a/src/games/ai/assets/style.css +++ b/src/games/ai/assets/style.css @@ -115,6 +115,11 @@ body { } } +.ace_editor { + background-color: var(--backgroundColorDark) !important; + border: var(--border) !important; +} + @keyframes swipe-from-left { 0% { position: relative; diff --git a/src/games/ai/components/ace.tsx b/src/games/ai/components/ace.tsx new file mode 100644 index 0000000..cd82bd4 --- /dev/null +++ b/src/games/ai/components/ace.tsx @@ -0,0 +1,59 @@ + +import { useEffect, useMemo, useRef } from "preact/hooks"; +import ace from "ace-builds"; +import { useIsVisible } from "@common/hooks/useIsVisible"; + +import "ace-builds/src-noconflict/mode-django"; +import "ace-builds/src-noconflict/theme-terminal"; + +interface IAceProps { + value: string; + onInput: (e: InputEvent | string) => void; +} + +export const Ace = ({ value, onInput }: IAceProps) => { + const ref = useRef(null); + const isVisible = useIsVisible(ref); + + const editor = useMemo(() => { + if (ref.current) { + const e = ace.edit(ref.current, { + theme: 'ace/theme/terminal', + mode: 'ace/mode/django', + showGutter: false, + showPrintMargin: false, + highlightActiveLine: false, + displayIndentGuides: false, + fontSize: 16, + maxLines: Infinity, + wrap: "free", + }); + return e; + } + }, [isVisible]); + + useEffect(() => { + if (editor) { + if (editor.getValue() !== value) { + const pos = editor.getCursorPosition(); + editor.setValue(value); + editor.selection.clearSelection(); + editor.moveCursorToPosition(pos); + } + } + }, [editor, value]); + + useEffect(() => { + if (onInput && editor) { + const e = editor; + const handler = () => onInput(e.getValue()); + + e.on('input', handler); + return () => e.off('input', handler); + } + }, [editor, onInput]); + + return ( +
+ ); +} \ No newline at end of file diff --git a/src/games/ai/components/autoTextarea.tsx b/src/games/ai/components/autoTextarea.tsx index b56dcfc..feccd64 100644 --- a/src/games/ai/components/autoTextarea.tsx +++ b/src/games/ai/components/autoTextarea.tsx @@ -1,10 +1,12 @@ -import { useEffect, useRef, useState } from "preact/hooks"; +import { useEffect, useRef } from "preact/hooks"; import type { JSX } from "preact/jsx-runtime" +import { useIsVisible } from '@common/hooks/useIsVisible'; + export const AutoTextarea = (props: JSX.HTMLAttributes) => { const { value } = props; const ref = useRef(null); - const [isVisible, setVisible] = useState(false); + const isVisible = useIsVisible(ref, true); useEffect(() => { if (ref.current) { @@ -15,20 +17,7 @@ export const AutoTextarea = (props: JSX.HTMLAttributes) => } }, [value, isVisible]); - useEffect(() => { - if (ref.current) { - const observer = new IntersectionObserver(([entry]) => { - setVisible(entry.isIntersecting); - if (entry.isIntersecting) { - observer.disconnect(); - } - }); - observer.observe(ref.current); - - return () => observer.disconnect(); - } - }, [ref.current]); return