diff --git a/whitewash/biome.json b/whitewash/biome.json index 2fbf5e9..b3da5a9 100644 --- a/whitewash/biome.json +++ b/whitewash/biome.json @@ -20,6 +20,9 @@ "recommended": true, "suspicious": { "noUnknownAtRules": "off" + }, + "correctness": { + "useExhaustiveDependencies": "off" } }, "domains": { diff --git a/whitewash/src/app/components/chat/OddyChat.tsx b/whitewash/src/app/components/chat/OddyChat.tsx index 97a77dc..cb71ac8 100644 --- a/whitewash/src/app/components/chat/OddyChat.tsx +++ b/whitewash/src/app/components/chat/OddyChat.tsx @@ -1,55 +1,100 @@ "use client" -import { useStateArray } from "@/hooks/useStateArray" +import Image, { type StaticImageData } from "next/image" +import oddy from "@/assets/images/oddy.png" +import frog from "@/assets/images/frog.png" +import prideFrog from "@/assets/images/pride-froggy.png" +import styles from "../oddy/Oddy.module.css" import { useEffect, useRef, useState } from "react" +import { useStateArray } from "@/hooks/useStateArray" type ChatMsg = { id: string - role: "user" | "oddy" - message: string + role: "user" | "assistant" + content: string } -type OddyProps = { +type OddyChatProps = { + isFrog?: boolean + isPride?: boolean previousMessages?: ChatMsg[] } -export const OddyChat = ({ previousMessages }: OddyProps) => { +export const OddyChat = ({ + isFrog = false, + isPride = false, + previousMessages, +}: OddyChatProps) => { + const [avatar, setAvatar] = useState(oddy) + const [name, setName] = useState<"Froggy" | "Oddy">( + isFrog ? "Froggy" : "Oddy", + ) previousMessages = previousMessages !== undefined ? [...previousMessages] : [] const [messages, addMessage] = useStateArray(previousMessages) const [input, setInput] = useState("") - const [agent, setAgent] = useState<"oddy" | "froggy">("froggy") - const [isPride, setIsPride] = useState(false) const ws = useRef(null) + const messagesEndRef = useRef(null) + + const scrollToBottom = () => { + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }) + } + useEffect(() => { + requestAnimationFrame(() => { + messagesEndRef.current?.scrollIntoView({ + behavior: "smooth", + block: "end", + }) + }) + }, []) + + useEffect(() => { + scrollToBottom() + }, [scrollToBottom]) + + // pick correct avatar on mount or when props change useEffect(() => { + if (isFrog) { + setAvatar(isPride ? prideFrog : frog) + setName("Froggy") + } else { + setAvatar(oddy) + setName("Oddy") + } + }, [isFrog, isPride]) + + // connect websocket + useEffect(() => { + const agent = isFrog ? "froggy" : "oddy" const url = new URL("ws://localhost:4001") url.searchParams.set("agent", agent) url.searchParams.set("isPride", String(isPride)) + url.searchParams.set( + "prevMessages", + JSON.stringify( + previousMessages.map((p) => { + return { role: p.role, content: p.content } + }), + ), + ) ws.current?.close() ws.current = new WebSocket(url.toString()) - ws.current.onopen = () => { - console.log("✅ Connected to WebSocket") - } - ws.current.onmessage = (event) => { - if (event.data === "Successfully connected") return + if (event.data === "Successfully connected") + return () => ws.current?.close() addMessage({ id: crypto.randomUUID(), - role: "oddy", - message: String(event.data), + role: "assistant", + content: String(event.data), }) + scrollToBottom() } - ws.current.onerror = (err) => console.error("❌ WebSocket error:", err) - ws.current.onclose = () => console.log("🔌 WebSocket closed") - - return () => { - ws.current?.close() - } - }, [agent, isPride, addMessage]) + return () => ws.current?.close() + }, [isFrog, isPride, addMessage, previousMessages.map, ]) const handleSubmit = (e: React.FormEvent) => { e.preventDefault() @@ -59,7 +104,7 @@ export const OddyChat = ({ previousMessages }: OddyProps) => { const msg: ChatMsg = { id: crypto.randomUUID(), role: "user", - message: trimmed, + content: trimmed, } addMessage(msg) ws.current?.send(trimmed) @@ -67,61 +112,59 @@ export const OddyChat = ({ previousMessages }: OddyProps) => { } return ( -
-
- - -
)