diff --git a/bun.lockb b/bun.lockb index 1fd6ee6..cc5a448 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/src/common/components/modal/modal.module.css b/src/common/components/modal/modal.module.css new file mode 100644 index 0000000..58df899 --- /dev/null +++ b/src/common/components/modal/modal.module.css @@ -0,0 +1,42 @@ +.dialog { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + margin: auto; + width: 100%; + max-width: 1000px; + height: fit-content; + max-height: 80dvh; + overflow: hidden; + background-color: var(--backgroundColorDark, #111); + color: var(--color, white); + border: var(--border, 1px solid white); + outline: none; + padding: 0; + border-radius: var(--border-radius, 0); + + &::backdrop { + background-color: var(--shadeColor, rgba(0, 0, 0, 0.2)); + } + + >.content { + display: flex; + flex-direction: column; + max-height: 80dvh; + overflow: hidden; + + padding: 30px; + + >.close { + font-family: var(--emojiFont, sans-serif); + font-size: 12px; + position: absolute; + top: 10px; + right: 10px; + cursor: pointer; + user-select: none; + } + } +} \ No newline at end of file diff --git a/src/common/components/modal/modal.tsx b/src/common/components/modal/modal.tsx new file mode 100644 index 0000000..9ae6f9d --- /dev/null +++ b/src/common/components/modal/modal.tsx @@ -0,0 +1,40 @@ +import type { ComponentChildren } from "preact"; +import { useCallback, useEffect, useRef } from "preact/hooks"; + +import styles from './modal.module.css'; + +interface IProps { + open: boolean; + class?: string; + className?: string; + onClose?: () => void; + children?: ComponentChildren; +} + +export const Modal = ({ children, open, onClose, ['class']: cls, className }: IProps) => { + const ref = useRef(null); + const handleClickWrapper = useCallback((e: MouseEvent) => { + if (e.currentTarget instanceof HTMLDialogElement) { + onClose?.(); + } else { + e.stopPropagation(); + } + }, [onClose]); + + useEffect(() => { + if (open) { + ref.current?.showModal(); + } else { + ref.current?.close(); + } + }, [open]); + + return ( + +
+
+ {children} +
+
+ ); +}; diff --git a/src/common/hooks/useBool.ts b/src/common/hooks/useBool.ts new file mode 100644 index 0000000..9ca862d --- /dev/null +++ b/src/common/hooks/useBool.ts @@ -0,0 +1,11 @@ +import { useCallback, useState } from "preact/hooks"; + +export const useBool = (initialValue: boolean) => { + const [value, setValue] = useState(initialValue); + + const setTrue = useCallback(() => setValue(true), []); + const setFalse = useCallback(() => setValue(false), []); + const toggle = useCallback(() => setValue(v => !v), []); + + return { value, setTrue, setFalse, toggle }; +}; diff --git a/src/games/ai/assets/style.css b/src/games/ai/assets/style.css index 9abe603..890c9ff 100644 --- a/src/games/ai/assets/style.css +++ b/src/games/ai/assets/style.css @@ -10,11 +10,17 @@ --shadeColor: rgba(0, 128, 128, 0.3); --border: 1px solid var(--color); + --border-radius: 4px; --emojiFont: "Noto Emoji", sans-serif; --emojiColorFont: "Noto Color Emoji", sans-serif; } +* { + scrollbar-width: thin; + scrollbar-color: var(--color) transparent; +} + textarea, input { color: var(--color); @@ -34,9 +40,19 @@ textarea { border: none; resize: none; width: 100%; - scrollbar-width: thin; - scrollbar-color: var(--color) transparent; + min-height: 100px; padding: 4px; + + &.border { + border: var(--border); + } +} + +button { + border: var(--border); + background-color: var(--backgroundColor); + color: var(--color); + cursor: pointer; } body { @@ -124,7 +140,7 @@ body { resize: vertical; line-height: 1.5; padding: 5px; - border-radius: 3px; + border-radius: var(--border-radius); } >.text { @@ -207,6 +223,7 @@ body { left: 0; } } + @keyframes swipe-from-right { 0% { position: relative; diff --git a/src/games/ai/components/chat.tsx b/src/games/ai/components/chat.tsx index c9a787e..a7ba117 100644 --- a/src/games/ai/components/chat.tsx +++ b/src/games/ai/components/chat.tsx @@ -1,30 +1,27 @@ import { useContext, useEffect, useRef } from "preact/hooks"; -import { GlobalContext } from "../context"; +import { StateContext } from "../contexts/state"; import { Message } from "./message"; +import { MessageTools } from "../messages"; +import { DOMTools } from "../dom"; export const Chat = () => { - const { messages } = useContext(GlobalContext); + const { messages } = useContext(StateContext); const chatRef = useRef(null); const lastMessage = messages.at(-1); - const lastMessageSwipe = lastMessage?.swipes[lastMessage.currentSwipe]; - const lastMessageContent = lastMessageSwipe?.displayContent ?? lastMessageSwipe?.content; + const lastMessageSwipe = MessageTools.getSwipe(lastMessage); + const lastMessageContent = lastMessageSwipe?.content; const lastUserId = messages.findLastIndex(m => m.role === 'user'); const lastAssistantId = messages.findLastIndex(m => m.role === 'assistant'); useEffect(() => { - if (chatRef.current) { - chatRef.current.scrollTo({ - top: chatRef.current.scrollHeight, - behavior: 'smooth', - }); - } + DOMTools.scrollDown(chatRef.current); }, [messages.length, lastMessageContent]); return (
{messages.map((m, i) => ( - { - const { connectionUrl, setConnectionUrl } = useContext(GlobalContext); + const llm = useContext(LLMContext); + const { connectionUrl, setConnectionUrl } = useContext(StateContext); const [urlValid, setUrlValid] = useState(false); const [urlEditing, setUrlEditing] = useState(false); @@ -26,7 +27,7 @@ export const Header = () => { useEffect(() => { if (!urlEditing) { - LLM.getContextLength().then(length => { + llm.getContextLength().then(length => { setUrlValid(length > 0); }); } diff --git a/src/games/ai/components/input.tsx b/src/games/ai/components/input.tsx index 69f121f..9094c38 100644 --- a/src/games/ai/components/input.tsx +++ b/src/games/ai/components/input.tsx @@ -1,24 +1,30 @@ import { useCallback, useContext } from "preact/hooks"; -import { GlobalContext } from "../context"; +import { DOMTools } from "../dom"; +import { StateContext } from "../contexts/state"; +import { LLMContext } from "../contexts/llm"; export const Input = () => { - const { input, setInput, addMessage, continueMessage } = useContext(GlobalContext); + const { input, setInput, addMessage, continueMessage } = useContext(StateContext); + const { generating } = useContext(LLMContext); const handleChange = useCallback((e: Event) => { if (e.target instanceof HTMLTextAreaElement) { setInput(e.target.value); } - }, []); + DOMTools.fixHeight(e.target); + }, [setInput]); const handleSend = useCallback(async () => { - const newInput = input.trim(); - if (newInput) { - addMessage(newInput, 'user', true); - setInput(''); - } else { - continueMessage(); + if (!generating) { + const newInput = input.trim(); + if (newInput) { + addMessage(newInput, 'user', true); + setInput(''); + } else { + continueMessage(); + } } - }, [input]); + }, [input, setInput, generating]); const handleKeyDown = useCallback((e: KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { @@ -30,7 +36,7 @@ export const Input = () => { return (