diff --git a/frontend/package.json b/frontend/package.json index 16a7b590..373992ce 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -44,6 +44,7 @@ "@tanstack/react-query-devtools": "5.59.13", "axios": "1.6.2", "date-fns": "2.30.0", + "eliza-as-promised": "git+https://npm@github.com/Hypfer/eliza-as-promised#0.0.3", "notistack": "3.0.1", "react": "18.3.1", "react-div-100vh": "0.7.0", diff --git a/frontend/src/components/ValetudoAppBar.tsx b/frontend/src/components/ValetudoAppBar.tsx index 1bde49a7..1ac198b7 100644 --- a/frontend/src/components/ValetudoAppBar.tsx +++ b/frontend/src/components/ValetudoAppBar.tsx @@ -35,6 +35,7 @@ import { Wysiwyg as SystemInformationIcon, Info as AboutIcon, Help as HelpIcon, + SmartToy as AiIcon, SvgIconComponent } from "@mui/icons-material"; import {Link, useLocation} from "react-router-dom"; @@ -269,6 +270,13 @@ const menuTree: Array = [ menuIcon: SystemInformationIcon, menuText: "System Information" }, + { + kind: "MenuEntry", + route: "/valetudo/ai", + title: "AI Assistant", + menuIcon: AiIcon, + menuText: "AI Assistant" + }, { kind: "MenuEntry", route: "/valetudo/help", diff --git a/frontend/src/valetudo/Help.tsx b/frontend/src/valetudo/Help.tsx index 50455baf..020018f8 100644 --- a/frontend/src/valetudo/Help.tsx +++ b/frontend/src/valetudo/Help.tsx @@ -9,6 +9,15 @@ import style from "./Help.module.css"; import {HelpText} from "./res/HelpText"; import DetailPageHeaderRow from "../components/DetailPageHeaderRow"; +// Taken from stackoverflow: https://stackoverflow.com/a/69120400 +function LinkRenderer(props: any) { + return ( + + {props.children} + + ); +} + const Help = (): React.ReactElement => { return ( @@ -20,6 +29,7 @@ const Help = (): React.ReactElement => { /> { + const [messages, setMessages] = useState([]); + const [inputValue, setInputValue] = useState(""); + const [elizaInstance, setElizaInstance] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [isFinished, setIsFinished] = useState(false); + + const messagesEndRef = useRef(null); + const inputRef = useRef(null); + + useEffect(() => { + const eliza = new ElizaBot(); + setElizaInstance(eliza); + + setTimeout(() => { + setMessages([{ sender: "ai", text: eliza.getInitial() }]); + setIsLoading(false); + + setTimeout(() => inputRef.current?.focus(), 0); + }, 800); + }, []); + + useEffect(() => { + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); + }, [messages]); + + + const handleSend = async () => { + if (!inputValue.trim() || !elizaInstance || isLoading || isFinished) { + return; + } + + const userMessage: AiChatMessage = { sender: "user", text: inputValue }; + setMessages(prev => [...prev, userMessage]); + setInputValue(""); + setIsLoading(true); + + try { + const response: ElizaBot.ElizaResponse = await elizaInstance.getResponse(inputValue.replace(/\n/g, " ").trim()); + const aiResponseText = response.reply || response.final || "I seem to be at a loss for words."; + + setTimeout(() => { + const aiMessage: AiChatMessage = { sender: "ai", text: aiResponseText }; + setMessages(prev => [...prev, aiMessage]); + setIsLoading(false); + + if (response.final) { + setIsFinished(true); + } else { + setTimeout(() => inputRef.current?.focus(), 0); + } + }, Math.random() * 800 + 400); + + } catch (error) { + const errorMessage: AiChatMessage = { + sender: "ai", + text: "I'm sorry, I'm afraid I can't do that." + }; + setMessages(prev => [...prev, errorMessage]); + + setIsLoading(false); + setTimeout(() => inputRef.current?.focus(), 0); + } + }; + + const handleReset = () => { + if (!elizaInstance) { + return; + } + + setMessages([]); + setIsLoading(true); + setIsFinished(false); + setInputValue(""); + elizaInstance.reset(); + + setTimeout(() => { + setMessages([{ sender: "ai", text: elizaInstance.getInitial() }]); + setIsLoading(false); + + setTimeout(() => inputRef.current?.focus(), 0); + }, 1000); + }; + + const handleKeyPress = (event: React.KeyboardEvent) => { + if (event.key === "Enter" && !event.shiftKey && !isFinished) { + event.preventDefault(); + + handleSend().catch(e => { + /* intentional */ + }); + } + }; + + return ( + + + } + /> + + {messages.map((msg, index) => ( + + {msg.sender === "ai" && } + + + {msg.text} + + + {msg.sender === "user" && } + + ))} + {isLoading && ( + + + + + {messages.length === 0 ? "Initializing..." : "Thinking..."} + + + + + )} + +
+ + + + setInputValue(e.target.value)} + onKeyDown={handleKeyPress} + disabled={isLoading || isFinished} + multiline + maxRows={4} + /> + + + {isFinished ? : } + + + + + + ); +}; + +export default ValetudoAI; diff --git a/frontend/src/valetudo/ValetudoRouter.tsx b/frontend/src/valetudo/ValetudoRouter.tsx index 848d408a..84702618 100644 --- a/frontend/src/valetudo/ValetudoRouter.tsx +++ b/frontend/src/valetudo/ValetudoRouter.tsx @@ -6,6 +6,7 @@ import Log from "./Log"; import Updater from "./Updater"; import About from "./About"; import Help from "./Help"; +import ValetudoAI from "./ValetudoAI"; import React from "react"; const ValetudoRouter = (): React.ReactElement => { @@ -16,6 +17,7 @@ const ValetudoRouter = (): React.ReactElement => { }/> }/> }/> + }/> }/> } /> diff --git a/frontend/src/valetudo/res/HelpText.ts b/frontend/src/valetudo/res/HelpText.ts index 7fa5614a..fc920c82 100644 --- a/frontend/src/valetudo/res/HelpText.ts +++ b/frontend/src/valetudo/res/HelpText.ts @@ -21,4 +21,19 @@ Just tap on the bar at the bottom of the screen. Simply configure/select the segments/zones/go-to locations like you'd normally do and then long-press the button that would start the action. This will bring up a dialog providing you with everything you'll need. +### What's up with the AI Assistant? + +The "AI Assistant" feature is a joke about the joke that is our industry, featuring a JavaScript implementation of ELIZA. +Created in 1966 by Joseph Weizenbaum, ELIZA was one of the very first chatbots and is famous for being one of the earliest programs to fool people into thinking that it passed the Turing test. + +Fittingly, Weizenbaum himself also had some opinions about this industry.
+Quoting Wikipedia:
+> His belief was that the computer, at its most base level, is a fundamentally conservative force and that despite being a technological innovation, it would end up hindering social progress. +> Weizenbaum used his experience working with Bank of America as justification for his reasoning, saying that the computer allowed banks to deal with an ever-expanding number of checks in play that otherwise would have forced drastic changes to banking organization such as decentralization. +> As such, although the computer allowed the industry to become more efficient, it prevented a fundamental re-haul of the system. +> +> Weizenbaum also worried about the negative effects computers would have with regards to the military, calling the computer "a child of the military." + +You can read more about ELIZA here: https://en.wikipedia.org/wiki/ELIZA + `; diff --git a/package-lock.json b/package-lock.json index 7b667b56..66e77935 100644 --- a/package-lock.json +++ b/package-lock.json @@ -115,6 +115,7 @@ "@tanstack/react-query-devtools": "5.59.13", "axios": "1.6.2", "date-fns": "2.30.0", + "eliza-as-promised": "git+https://npm@github.com/Hypfer/eliza-as-promised#0.0.3", "notistack": "3.0.1", "react": "18.3.1", "react-div-100vh": "0.7.0", @@ -8509,6 +8510,14 @@ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.594.tgz", "integrity": "sha512-xT1HVAu5xFn7bDfkjGQi9dNpMqGchUkebwf1GL7cZN32NSwwlHRPMSDJ1KN6HkS0bWUtndbSQZqvpQftKG2uFQ==" }, + "node_modules/eliza-as-promised": { + "version": "0.0.3", + "resolved": "git+https://npm@github.com/Hypfer/eliza-as-promised.git#0cc5aea257b742ba069015fba5e6a4123147cbae", + "license": "MIT", + "engines": { + "node": ">=7" + } + }, "node_modules/emittery": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.8.1.tgz", @@ -27780,6 +27789,10 @@ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.594.tgz", "integrity": "sha512-xT1HVAu5xFn7bDfkjGQi9dNpMqGchUkebwf1GL7cZN32NSwwlHRPMSDJ1KN6HkS0bWUtndbSQZqvpQftKG2uFQ==" }, + "eliza-as-promised": { + "version": "git+https://npm@github.com/Hypfer/eliza-as-promised.git#0cc5aea257b742ba069015fba5e6a4123147cbae", + "from": "eliza-as-promised@git+https://npm@github.com/Hypfer/eliza-as-promised#0.0.3" + }, "emittery": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.8.1.tgz", @@ -36595,6 +36608,7 @@ "axios": "1.6.2", "cra-build-watch": "git+https://npm@github.com/Hypfer/cra-build-watch.git#5.0.0", "date-fns": "2.30.0", + "eliza-as-promised": "git+https://npm@github.com/Hypfer/eliza-as-promised#0.0.3", "notistack": "3.0.1", "react": "18.3.1", "react-div-100vh": "0.7.0",