diff --git a/package-lock.json b/package-lock.json index 15cd6102..a90f54b5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -66,6 +66,7 @@ "nexus": "^1.3.0", "nostr-relaypool": "^0.6.16", "nostr-tools": "^1.2.0", + "openai": "^3.3.0", "qrcode.react": "^3.0.2", "react": "^18.0.0", "react-accessible-accordion": "^5.0.0", @@ -101,6 +102,7 @@ "web-vitals": "^2.1.4", "webln": "^0.3.0", "websocket-polyfill": "^0.0.3", + "yaml": "^2.3.1", "yup": "^0.32.11" }, "devDependencies": { @@ -2995,6 +2997,15 @@ "node": ">=8" } }, + "node_modules/@graphql-codegen/cli/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/@graphql-codegen/cli/node_modules/yargs": { "version": "17.4.1", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.4.1.tgz", @@ -10295,6 +10306,15 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true }, + "node_modules/@storybook/builder-webpack4/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/@storybook/builder-webpack5": { "version": "6.4.22", "resolved": "https://registry.npmjs.org/@storybook/builder-webpack5/-/builder-webpack5-6.4.22.tgz", @@ -18937,6 +18957,15 @@ "node": ">=8" } }, + "node_modules/babel-plugin-emotion/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/babel-plugin-extract-import-names": { "version": "1.6.22", "resolved": "https://registry.npmjs.org/babel-plugin-extract-import-names/-/babel-plugin-extract-import-names-1.6.22.tgz", @@ -21505,6 +21534,14 @@ "@iarna/toml": "^2.2.5" } }, + "node_modules/cosmiconfig/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "engines": { + "node": ">= 6" + } + }, "node_modules/cp-file": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/cp-file/-/cp-file-7.0.0.tgz", @@ -22353,6 +22390,14 @@ "postcss": "^8.2.15" } }, + "node_modules/cssnano/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "engines": { + "node": ">= 6" + } + }, "node_modules/csso": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz", @@ -25927,6 +25972,14 @@ "node": ">=6" } }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "engines": { + "node": ">= 6" + } + }, "node_modules/form-data": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", @@ -58355,6 +58408,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/openai": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-3.3.0.tgz", + "integrity": "sha512-uqxI/Au+aPRnsaQRe8CojU0eCR7I0mBiKjD3sNMzY6DaC1ZVrc85u98mtJW6voDug8fgGN+DIZmTDxTthxb7dQ==", + "dependencies": { + "axios": "^0.26.0", + "form-data": "^4.0.0" + } + }, "node_modules/optimism": { "version": "0.16.1", "resolved": "https://registry.npmjs.org/optimism/-/optimism-0.16.1.tgz", @@ -59752,6 +59814,14 @@ } } }, + "node_modules/postcss-load-config/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "engines": { + "node": ">= 6" + } + }, "node_modules/postcss-loader": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-6.2.1.tgz", @@ -69759,11 +69829,11 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz", + "integrity": "sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==", "engines": { - "node": ">= 6" + "node": ">= 14" } }, "node_modules/yaml-ast-parser": { @@ -71917,6 +71987,12 @@ "has-flag": "^4.0.0" } }, + "yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true + }, "yargs": { "version": "17.4.1", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.4.1.tgz", @@ -77397,6 +77473,12 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true + }, + "yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true } } }, @@ -84357,6 +84439,12 @@ "path-type": "^4.0.0", "yaml": "^1.7.2" } + }, + "yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true } } }, @@ -86402,6 +86490,13 @@ "parse-json": "^5.0.0", "path-type": "^4.0.0", "yaml": "^1.10.0" + }, + "dependencies": { + "yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==" + } } }, "cosmiconfig-toml-loader": { @@ -87008,6 +87103,13 @@ "cssnano-preset-default": "^5.2.7", "lilconfig": "^2.0.3", "yaml": "^1.10.2" + }, + "dependencies": { + "yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==" + } } }, "cssnano-preset-default": { @@ -89859,6 +89961,11 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==" + }, + "yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==" } } }, @@ -114549,6 +114656,15 @@ "is-wsl": "^2.2.0" } }, + "openai": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-3.3.0.tgz", + "integrity": "sha512-uqxI/Au+aPRnsaQRe8CojU0eCR7I0mBiKjD3sNMzY6DaC1ZVrc85u98mtJW6voDug8fgGN+DIZmTDxTthxb7dQ==", + "requires": { + "axios": "^0.26.0", + "form-data": "^4.0.0" + } + }, "optimism": { "version": "0.16.1", "resolved": "https://registry.npmjs.org/optimism/-/optimism-0.16.1.tgz", @@ -115518,6 +115634,13 @@ "requires": { "lilconfig": "^2.0.5", "yaml": "^1.10.2" + }, + "dependencies": { + "yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==" + } } }, "postcss-loader": { @@ -123214,9 +123337,9 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==" + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz", + "integrity": "sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==" }, "yaml-ast-parser": { "version": "0.0.43", diff --git a/package.json b/package.json index fbff8571..2f1862b4 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "nexus": "^1.3.0", "nostr-relaypool": "^0.6.16", "nostr-tools": "^1.2.0", + "openai": "^3.3.0", "qrcode.react": "^3.0.2", "react": "^18.0.0", "react-accessible-accordion": "^5.0.0", @@ -96,6 +97,7 @@ "web-vitals": "^2.1.4", "webln": "^0.3.0", "websocket-polyfill": "^0.0.3", + "yaml": "^2.3.1", "yup": "^0.32.11" }, "scripts": { diff --git a/src/features/Dashboard/Chatbot/Chatbot.tsx b/src/features/Dashboard/Chatbot/Chatbot.tsx new file mode 100644 index 00000000..82498842 --- /dev/null +++ b/src/features/Dashboard/Chatbot/Chatbot.tsx @@ -0,0 +1,33 @@ +import { useSearchParams } from "react-router-dom"; +import OgTags from "src/Components/OgTags/OgTags"; +import Chat from "./components/Chat"; +import TournamentPreview from "./components/TournamentPreview"; +import { TournamentContextProvider } from "./contexts/tournament.context"; +import { TournamentChatbotContextProvider } from "./contexts/tournamentChatbot.context"; + +export default function ChatbotPage() { + const [searchParams] = useSearchParams(); + + const tournamentIdOrSlug = searchParams.get("tournament"); + + if (!tournamentIdOrSlug) return

No tournament selected

; + + return ( + <> + +
+
+ + + + + + +
+
+ + ); +} diff --git a/src/features/Dashboard/Chatbot/components/Chat.tsx b/src/features/Dashboard/Chatbot/components/Chat.tsx new file mode 100644 index 00000000..a2b3760f --- /dev/null +++ b/src/features/Dashboard/Chatbot/components/Chat.tsx @@ -0,0 +1,11 @@ +import MessagesContainer from "./MessagesContainer"; + +export default function Chat() { + return ( +
+
+ +
+
+ ); +} diff --git a/src/features/Dashboard/Chatbot/components/MessagesContainer.tsx b/src/features/Dashboard/Chatbot/components/MessagesContainer.tsx new file mode 100644 index 00000000..ebddd5b5 --- /dev/null +++ b/src/features/Dashboard/Chatbot/components/MessagesContainer.tsx @@ -0,0 +1,123 @@ +import React, { useCallback, useEffect, useState } from "react"; +import { Message, useChat } from "../contexts/chat.context"; +import { useSubmitMessage } from "./useSubmitMessage"; + +interface Props {} + +export default function MessagesContainer({}: Props) { + const inputRef = React.useRef(null!); + + const [msgInput, setMessageInput] = useState(""); + const [inputDisabled, setInputDisabled] = useState(false); + const inputWasDisabled = React.useRef(false); + + const messagesContainerRef = React.useRef(null!); + + const { messages: newMessages } = useChat(); + + const submitMessageMutation = useSubmitMessage(); + + const [messages, setMessages] = useState(newMessages); + const [shouldScroll, setShouldScroll] = useState(true); + + if (messages !== newMessages) { + const scrolledToBottom = + messagesContainerRef.current.scrollTop + + messagesContainerRef.current.clientHeight === + messagesContainerRef.current.scrollHeight; + setShouldScroll(shouldScroll || scrolledToBottom); + setMessages(newMessages); + } + + const scrollToBottom = useCallback(() => { + messagesContainerRef.current.scrollTop = + messagesContainerRef.current.scrollHeight; + }, []); + + useEffect(() => { + if (shouldScroll) { + scrollToBottom(); + setShouldScroll(false); + } + }, [scrollToBottom, shouldScroll]); + + useEffect(() => { + if (!inputDisabled && inputWasDisabled.current) { + inputRef.current.focus(); + inputWasDisabled.current = false; + } + }, [inputDisabled]); + + const onSubmitMessage = async (e: React.FormEvent) => { + e.preventDefault(); + + if (msgInput.trim() === "") return; + + try { + setInputDisabled(true); + await submitMessageMutation.submit(msgInput); + + setMessageInput(""); + setShouldScroll(true); + } catch (error) { + alert("Failed to submit message"); + } finally { + inputWasDisabled.current = true; + setInputDisabled(false); + } + }; + + return ( + <> +
+
+ {messages.map((message) => ( +
+

{message.content}

+
+ ))} +
+
+
+
+