From eed4f492cc69e7e48857a39a4bdc7ccd8a122a05 Mon Sep 17 00:00:00 2001 From: Pabloader Date: Sat, 2 Nov 2024 14:53:35 +0000 Subject: [PATCH] AIStory: ace template editor --- bun.lockb | Bin 29916 -> 30275 bytes package.json | 1 + src/common/hooks/useIsVisible.ts | 22 +++++++ src/games/ai/assets/style.css | 5 ++ src/games/ai/components/ace.tsx | 59 ++++++++++++++++++ src/games/ai/components/autoTextarea.tsx | 19 ++---- .../ai/components/header/header.module.css | 4 -- src/games/ai/components/header/header.tsx | 8 +-- src/games/ai/contexts/llm.tsx | 10 ++- src/games/ai/contexts/state.tsx | 16 +---- 10 files changed, 106 insertions(+), 38 deletions(-) create mode 100644 src/common/hooks/useIsVisible.ts create mode 100644 src/games/ai/components/ace.tsx diff --git a/bun.lockb b/bun.lockb index cc5a448565eb35397152aa096981c87aab69709a..6ebbd7a561e199a2402f8320b2badd58edbeb577 100755 GIT binary patch delta 4961 zcmeHLdvH|M8NcVkCcB$#2;{X(2%9I#=CR3cvU!kXFZck$N+7&84{&Uf}bzj*1K*k-p)ekAXg9SIi>J^X3s;O67c{p^q38~3j~b8<&^i@EWY zhY$R2pj}ea!fvm%gDL}zB(3t@-mu=cR$8~dsiAR$B<(?kQIa~J2;zglNbtvnBuxSy z0)_)`lO@Rj+z&ni=m#GLd^;t#{gV@8psom0=fSPkQX=0TiIJHLkJVu1})0<$-@H8!*~tZwq7977m8k0BF{ z1|!rU7a1Ol3Fv-^cFESvGw8{RcsGcz29X71QNF^AkfyWHOlRfkdfD}MPxfm+pEjVM zuu~TMs86Se)8yAFrX-lI91Q_dDm`b&qMbUuxS4Dr+`<>4$XPP=hq%QC@}p=U*+Lb= zMcCiXoycJ*lARN%Kh!POlRs3E!zWUv-Yx3Mr&kQSVcs&d)6pIJBJm;l^@==ON0|n< zVLLp4htTs7;u!TA6hk7ce;vwXwRsEKjEZ=gd`3n0IcjE7tFcs045hP1x9%1gem3P8 zO2sbng(>0;^@S;NhMvxbxkW44!WHo%`SAUY`oa}MDRjXjPgG~yOE!}tJ|~|^F(gA< zh3cHog%ruFjFf3~8+M>D8-pn!j2u0R~QGbM6RFFSH(cK3_mZ{UfPQFQsAruC6 zqQe9l(-#>^z|8|Esoi_X7OCit!=yEo6IrUu#SU7j<{m<>QO%7Z=TmdF*mn(T?l;KQ zskvBKI?$sXxq7wi_sDtGTshptP0xmwio3}_1s0e>nNe=T2DlaLUa&F^zW`UPF7S?! zBJlr=2ON%#e@cI& z;h1#v@W&|Pr(}y$s7Rpe>0)F0~>H<2Gjo5>cZh;PUjr^qF7)F0>O#iHl{+02Ug zEBVZdteC0aJTnPm^WmeWYoU%z7HbvzD|}u}S0mIcrGRiiNHaj(E*->)k;^lgLAn|R z^;+VfN!9QS>;RHPdo0!t9>dNY(zOu2D7n4xc?O+r&F=AIqGXw0^ z*r8Uymb|m2Il%_sCt{mQ^Fgef1;O#h@%li?c)>W&5>y5|FtP-Tg8bDe)8(mgK5o4% zSj))MRfD*^2E>VxN2vpG`HCQabC3t9-f=C@(CYs0A*_k-)l>dA9#Z~adC0(ELaLb6 z@aj76Dt+^#Pd;>G=iDVX-15P^m(ANZUi@hIZo_wfPHV3HbcO5Eea5wAbuZsJc42he z(#;nJKQ_O*F(%T|5$@?e>7WFwncko75jwJ2y>!BAqODes2&GYQU5O?tPxJ@_ZAtV} zagvGt04|J5lDzZFp(eJrZkU;qnb1?>1UYe05}U7W_amEaIG^uBAy1pHK&^>A>AVq z$d~RVbB2iygR_z)!%MG$+mzuENi+iPPMe8xY#xz98*N_7$TZRW;L^yJ3HiWn&Gd+L z8U@#t1^Kc(!bV%NAYV4*1D8c5*cKaU#_)Aeh21Nhv(nkpn2oE-1SZ9j<&ANW3%oC}mgI4l#}A zfz2RJJOH)Tg zlp^JWcKD_v@VfC^~5xl8asU8>tLMSTn z9gZOJALWSP)wQsE+y?Qq8gFo$7)k2j5S_z#wKFS_70P97AVyX!E0`6_N?;|ia4ak< zAPvL{U?nAjc(KVKRtyI!oM)wQ;J~2>2NW()0VrRh9gC)Rlp_n9sCH(1u+7;f+(iLh z2bv9<1G)jkcH?ERP1zP~lL`<|$HesuK`ru2X_aMQ zQPnIFB?q4uvT)e*3+?c)J+x(UzUZX`i!JfmEAiGvpS+d&&N-JXZg$x7xYd3dTWmpm z9k#?Gj?*(sE#fv(mRMZc`!eg{C!}YSfAmtY2YmQHj1|8Zc7`=NE9c&SF>ZRqV)V?z zTu|B&4P%xOI)!gw#!;HI)Z)^fn6Z!6gttAg;SKaCuw#9a^zT=3p6apIOO&(B60f~^ zm%V*1^{%y%HRxYxcXEGKqIm6zTbcXYr!0qFidM(Rny_W*L(H$el@pRbPB~~u9#s3Q z_3G;xveb&7oSO5^lj~69f-Km@>db}OBl_Ix+GX#T9lQbc#i+-~+B19aTF3J(=NosR zCO=RU6+-tcGsmxjOB*rt(dzyq6&Ig8EsM<#2n{uzqJ7IOVu&8BvBYcd;qtKUZ)Zlo zabSFy2k0_dZKJs5`J#tB%PnFzH7#E-O?zp-SkzPqX&Y*o&l-w9soQ)E>@7HNG`tyX^Lf@Ghr|2h*O`9TOL) zeB0f1bi5|kL_56Zc-Mizhc9XT* z;?iFJP0gccw|w@&JLB~u*xlIto%xS6-Zk92bCWFgqXgT&^BA>OTjGOn_|9LMUwQAy zkt?4;G`NJ_Q6POyyQ=eZv^Rg-mU&m6-;;WeD!OV~?Os{?_*;`Ucipr-5?eyl@K$ow zl#0jbj+&r#glZ`os+sE29{-2ChYFlOK7Upgdz4wYAr79 zmygfR{d(%b#LGvqjy!u|z+M3l`Ij0vm+BkSv>zDmKKFyeT~oeH=Btn*KCS4;nk1UO(m1fRzANtU1A@Gl delta 4777 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 EpO{&7DF6Tf 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