diff --git a/src/games/ai/assets/message.mp3 b/src/games/ai/assets/message.mp3 new file mode 100644 index 0000000..673180f Binary files /dev/null and b/src/games/ai/assets/message.mp3 differ diff --git a/src/games/ai/assets/style.css b/src/games/ai/assets/style.css index 44f0dcf..cba440e 100644 --- a/src/games/ai/assets/style.css +++ b/src/games/ai/assets/style.css @@ -4,21 +4,35 @@ --color: #DCDCD2; --italicColor: #AFAFAF; --quoteColor: #D4E5FF; + --green: #AFAFAF; + --red: #7F0000; + --green: #007F00; + --shadeColor: rgba(0, 128, 128, 0.3); + + --border: 1px solid var(--color); --emojiFont: "Noto Emoji", sans-serif; --emojiColorFont: "Noto Color Emoji", sans-serif; } -textarea { +textarea, +input { color: var(--color); background-color: var(--backgroundColor); font-size: 1em; font-family: sans-serif; background-color: transparent; - resize: none; appearance: none; outline: none; +} + +input { + border: var(--border); +} + +textarea { border: none; + resize: none; width: 100%; scrollbar-width: thin; scrollbar-color: var(--color) transparent; @@ -48,8 +62,18 @@ body { display: flex; flex-direction: row; height: 36px; - background-color: yellow; width: 100%; + border: var(--border); + + >input { + &.valid { + background-color: var(--green); + } + + &.invalid { + background-color: var(--red); + } + } } >.chat { @@ -62,26 +86,68 @@ body { overflow-y: auto; scrollbar-width: thin; scrollbar-color: var(--color) transparent; - border: 1px solid var(--color); + border: var(--border); + border-bottom: none; + border-top: none; >.message { width: 100%; padding: 12px; - >.header { + &.role-user { + background-color: var(--shadeColor); + + :not(.last-user) .content .text { + opacity: 0.5; + } + } + + >.content { + white-space: pre-wrap; + line-height: 1.5; display: flex; flex-direction: row; - justify-content: space-between; - margin-bottom: 6px; + width: 100%; + gap: 8px; - >.name { - font-weight: bold; + >textarea { + background-color: var(--backgroundColorDark); + border: var(--border); + min-height: 100px; + height: unset; + resize: vertical; + line-height: 1.25; + padding: 5px; + border-radius: 3px; + } + + >.text { + flex-grow: 1; + width: 100%; + + >.bold { + font-weight: bold; + } + + >.italic { + font-style: italic; + color: var(--italicColor); + } + + >.quote { + color: var(--quoteColor); + } } >.buttons { + display: flex; + flex-direction: column; + gap: 8px; + >.icon { font-family: var(--emojiFont); font-weight: bold; + font-size: 20px; border: none; background: transparent; padding: 0; @@ -94,22 +160,6 @@ body { } } } - - >.content { - white-space: pre-wrap; - line-height: 1.5; - - >textarea { - background-color: var(--backgroundColorDark); - border: 1px solid var(--italicColor); - min-height: 100px; - height: unset; - resize: vertical; - line-height: 1.25; - padding: 5px; - border-radius: 3px; - } - } } } @@ -119,7 +169,7 @@ body { height: auto; min-height: 48px; width: 100%; - border: 1px solid var(--color); + border: var(--border); } } } \ No newline at end of file diff --git a/src/games/ai/components/chat.tsx b/src/games/ai/components/chat.tsx index 6011f13..2ebc2f2 100644 --- a/src/games/ai/components/chat.tsx +++ b/src/games/ai/components/chat.tsx @@ -9,6 +9,7 @@ export const Chat = () => { const lastMessage = messages.at(-1); const lastMessageSwipe = lastMessage?.swipes[lastMessage.currentSwipe]; const lastMessageContent = lastMessageSwipe?.displayContent ?? lastMessageSwipe.content; + const lastUserId = messages.findLastIndex(m => m.role === 'user'); useEffect(() => { if (chatRef.current) { @@ -22,7 +23,7 @@ export const Chat = () => { return (
{messages.map((m, i) => ( - + ))}
); diff --git a/src/games/ai/components/header.tsx b/src/games/ai/components/header.tsx index 8aa3adb..e6f1ff4 100644 --- a/src/games/ai/components/header.tsx +++ b/src/games/ai/components/header.tsx @@ -1,7 +1,44 @@ +import { useCallback, useContext, useEffect, useState } from "preact/hooks"; +import { GlobalContext } from "../context"; +import { LLM } from "../llm"; + export const Header = () => { + const { connectionUrl, setConnectionUrl } = useContext(GlobalContext); + 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 (
- Header +
); } \ No newline at end of file diff --git a/src/games/ai/components/input.tsx b/src/games/ai/components/input.tsx index 7279273..69f121f 100644 --- a/src/games/ai/components/input.tsx +++ b/src/games/ai/components/input.tsx @@ -2,7 +2,7 @@ import { useCallback, useContext } from "preact/hooks"; import { GlobalContext } from "../context"; export const Input = () => { - const { input, setInput, addMessage } = useContext(GlobalContext); + const { input, setInput, addMessage, continueMessage } = useContext(GlobalContext); const handleChange = useCallback((e: Event) => { if (e.target instanceof HTMLTextAreaElement) { @@ -15,6 +15,8 @@ export const Input = () => { if (newInput) { addMessage(newInput, 'user', true); setInput(''); + } else { + continueMessage(); } }, [input]); @@ -28,7 +30,7 @@ export const Input = () => { return (