diff --git a/build/isGame.ts b/build/isGame.ts index 5bd4825..f6be86d 100644 --- a/build/isGame.ts +++ b/build/isGame.ts @@ -21,5 +21,5 @@ export async function getGames() { if (!stat.isDirectory()) return []; const list = await fs.readdir(dir); - return list.filter(d => d !== 'index'); + return list.filter(d => d !== 'index').sort(); } \ No newline at end of file diff --git a/src/common/components/modal/modal.module.css b/src/common/components/modal/modal.module.css index 58df899..0dc6ad7 100644 --- a/src/common/components/modal/modal.module.css +++ b/src/common/components/modal/modal.module.css @@ -10,7 +10,7 @@ height: fit-content; max-height: 80dvh; overflow: hidden; - background-color: var(--backgroundColorDark, #111); + background-color: var(--backgroundColor, #333333); color: var(--color, white); border: var(--border, 1px solid white); outline: none; diff --git a/src/common/hooks/useBool.ts b/src/common/hooks/useBool.ts index 9ca862d..bc34179 100644 --- a/src/common/hooks/useBool.ts +++ b/src/common/hooks/useBool.ts @@ -1,6 +1,6 @@ import { useCallback, useState } from "preact/hooks"; -export const useBool = (initialValue: boolean) => { +export const useBool = (initialValue: boolean = false) => { const [value, setValue] = useState(initialValue); const setTrue = useCallback(() => setValue(true), []); diff --git a/src/common/hooks/useInputState.ts b/src/common/hooks/useInputState.ts new file mode 100644 index 0000000..5e0b121 --- /dev/null +++ b/src/common/hooks/useInputState.ts @@ -0,0 +1,18 @@ +import { useCallback, useState } from "preact/hooks"; + +export const useInputState = (defaultValue = ''): [string, (e: Event | string) => void] => { + const [value, setValue] = useState(defaultValue); + + const handleInput = useCallback((e: Event | string) => { + if (typeof e === 'string') { + setValue(e); + } else { + const { target } = e; + if (target && 'value' in target && typeof target.value === 'string') { + setValue(target.value); + } + } + }, []); + + return [value, handleInput]; +} \ No newline at end of file diff --git a/src/games/ai/assets/favicon.ico b/src/games/ai/assets/favicon.ico index c355512..057ea8e 100644 Binary files a/src/games/ai/assets/favicon.ico and b/src/games/ai/assets/favicon.ico differ diff --git a/src/games/ai/assets/style.css b/src/games/ai/assets/style.css index da4c5be..90c921f 100644 --- a/src/games/ai/assets/style.css +++ b/src/games/ai/assets/style.css @@ -24,28 +24,20 @@ textarea, input { color: var(--color); - background-color: var(--backgroundColor); + border: var(--border); + background-color: var(--backgroundColorDark); font-size: 1em; font-family: sans-serif; - background-color: transparent; appearance: none; outline: none; } -input { - border: var(--border); -} - textarea { - border: none; - resize: none; + resize: vertical; width: 100%; min-height: 100px; padding: 4px; - - &.border { - border: var(--border); - } + line-height: 1.5; } button { @@ -59,6 +51,18 @@ button { pointer-events: none; opacity: 0.5; } + + &.icon { + font-family: var(--emojiFont); + font-size: 20px; + border: none; + background: none; + padding: 0; + + &.color { + font-family: var(--emojiColorFont); + } + } } body { @@ -71,6 +75,7 @@ body { flex-direction: row; justify-content: center; font-size: 16px; + line-height: 1.5; .app { display: flex; @@ -81,24 +86,6 @@ body { height: 100%; max-height: 100dvh; - >.header { - display: flex; - flex-direction: row; - height: 36px; - width: 100%; - border: var(--border); - - >input { - &.valid { - background-color: var(--green); - } - - &.invalid { - background-color: var(--red); - } - } - } - >.chat { display: flex; flex-direction: column; @@ -112,108 +99,18 @@ body { border: var(--border); border-bottom: none; border-top: none; - - >.message { - width: 100%; - padding: 12px; - - &.role-user { - background-color: var(--shadeColor); - - &:not(.last-user) .content .text { - opacity: 0.5; - font-size: 12px; - } - } - - &.role-assistant { - border-top: 1px solid var(--backgroundColorDark); - } - - >.content { - white-space: pre-wrap; - line-height: 1.5; - display: flex; - flex-direction: column; - width: 100%; - gap: 8px; - - >textarea { - background-color: var(--backgroundColorDark); - border: var(--border); - min-height: 100px; - height: unset; - resize: vertical; - line-height: 1.5; - padding: 5px; - border-radius: var(--border-radius); - } - - >.text { - flex-grow: 1; - width: 100%; - animation-duration: 300ms; - - >.bold { - font-weight: bold; - } - - >.italic { - font-style: italic; - color: var(--italicColor); - } - - >.quote { - color: var(--quoteColor); - } - } - - >.buttons { - display: flex; - flex-direction: row; - align-items: center; - justify-content: flex-end; - gap: 8px; - - >.icon { - font-family: var(--emojiFont); - font-weight: bold; - font-size: 20px; - border: none; - background: transparent; - padding: 0; - color: var(--color); - cursor: pointer; - - &.color { - font-family: var(--emojiColorFont); - } - } - - >.swipes { - display: flex; - width: 100%; - flex-direction: row; - justify-content: space-between; - gap: 8px; - - >div { - cursor: pointer; - font-size: 20px; - } - } - } - } - } } >.chat-input { display: flex; flex-direction: row; height: auto; - min-height: 48px; width: 100%; - border: var(--border); + > textarea { + min-height: 48px; + resize: none; + background-color: var(--backgroundColor); + } } } } diff --git a/src/games/ai/components/app.tsx b/src/games/ai/components/app.tsx index e0e62cb..97b1265 100644 --- a/src/games/ai/components/app.tsx +++ b/src/games/ai/components/app.tsx @@ -1,4 +1,4 @@ -import { Header } from "./header"; +import { Header } from "./header/header"; import { Chat } from "./chat"; import { Input } from "./input"; diff --git a/src/games/ai/components/chat.tsx b/src/games/ai/components/chat.tsx index a7ba117..75ec7bb 100644 --- a/src/games/ai/components/chat.tsx +++ b/src/games/ai/components/chat.tsx @@ -1,6 +1,6 @@ import { useContext, useEffect, useRef } from "preact/hooks"; import { StateContext } from "../contexts/state"; -import { Message } from "./message"; +import { Message } from "./message/message"; import { MessageTools } from "../messages"; import { DOMTools } from "../dom"; diff --git a/src/games/ai/components/header.tsx b/src/games/ai/components/header.tsx deleted file mode 100644 index ffd405e..0000000 --- a/src/games/ai/components/header.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { useCallback, useContext, useEffect, useState } from "preact/hooks"; -import { StateContext } from "../contexts/state"; -import { LLMContext } from "../contexts/llm"; - -export const Header = () => { - const llm = useContext(LLMContext); - const { connectionUrl, setConnectionUrl } = useContext(StateContext); - const [urlValid, setUrlValid] = useState(false); - const [urlEditing, setUrlEditing] = useState(false); - - const handleEditUrl = useCallback((e: InputEvent) => { - if (e.target instanceof HTMLInputElement) { - setConnectionUrl(e.target.value.trim()); - } - }, [setConnectionUrl]); - - const handleFocusUrl = useCallback(() => setUrlEditing(true), []); - - const handleBlurUrl = useCallback(() => { - const regex = /^(?:http(s?):\/\/)?(.*?)\/?$/i - const normalizedConnectionUrl = connectionUrl.replace(regex, 'http$1://$2'); - console.log({ connectionUrl, normalizedConnectionUrl }) - setConnectionUrl(normalizedConnectionUrl); - setUrlEditing(false); - setUrlValid(false); - }, [connectionUrl, setConnectionUrl]); - - useEffect(() => { - if (!urlEditing) { - llm.getContextLength().then(length => { - setUrlValid(length > 0); - }); - } - }, [connectionUrl, urlEditing]); - - return ( -
- -
- ); -} \ No newline at end of file diff --git a/src/games/ai/components/header/header.module.css b/src/games/ai/components/header/header.module.css new file mode 100644 index 0000000..865fd77 --- /dev/null +++ b/src/games/ai/components/header/header.module.css @@ -0,0 +1,29 @@ +.header { + display: flex; + flex-direction: row; + justify-content: space-between; + height: 36px; + width: 100%; + border: var(--border); + + >input { + &.valid { + background-color: var(--green); + } + + &.invalid { + background-color: var(--red); + } + } + + .buttons { + display: flex; + flex-direction: row; + gap: 8px; + padding: 0 8px; + } +} + +.modalTitle { + margin-top: 0; +} \ No newline at end of file diff --git a/src/games/ai/components/header/header.tsx b/src/games/ai/components/header/header.tsx new file mode 100644 index 0000000..567eded --- /dev/null +++ b/src/games/ai/components/header/header.tsx @@ -0,0 +1,79 @@ +import { useCallback, useContext, useEffect, useRef, useState } from "preact/hooks"; +import { useBool } from "@common/hooks/useBool"; +import { Modal } from "@common/components/modal/modal"; + +import { StateContext } from "../../contexts/state"; +import { LLMContext } from "../../contexts/llm"; +import { MiniChat } from "../minichat/minichat"; + +import styles from './header.module.css'; +import { DOMTools } from "../../dom"; + +export const Header = () => { + const llm = useContext(LLMContext); + const { messages, connectionUrl, lore, setConnectionUrl, setLore, addSwipe } = useContext(StateContext); + const [urlValid, setUrlValid] = useState(false); + const [urlEditing, setUrlEditing] = useState(false); + + const loreAreaRef = useRef(null); + + const loreOpen = useBool(); + const assistantOpen = useBool(); + + const handleFocusUrl = useCallback(() => setUrlEditing(true), []); + + const handleBlurUrl = useCallback(() => { + const regex = /^(?:http(s?):\/\/)?(.*?)\/?$/i + const normalizedConnectionUrl = connectionUrl.replace(regex, 'http$1://$2'); + console.log({ connectionUrl, normalizedConnectionUrl }) + setConnectionUrl(normalizedConnectionUrl); + setUrlEditing(false); + setUrlValid(false); + }, [connectionUrl, setConnectionUrl]); + + useEffect(() => { + if (!urlEditing) { + llm.getContextLength().then(length => { + setUrlValid(length > 0); + }); + } + }, [connectionUrl, urlEditing]); + + useEffect(() => { + DOMTools.fixHeight(loreAreaRef.current); + }, [lore, loreOpen.value]); + + const handleAssistantAddSwipe = useCallback((answer: string) => { + const index = messages.findLastIndex(m => m.role === 'assistant'); + addSwipe(index, answer); + assistantOpen.setFalse(); + }, [addSwipe, messages]); + + return ( +
+ +
+ + +
+ +

Lore Editor

+