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) => (
+
+ ))}
+
+
+
+ >
+ );
+}
diff --git a/src/features/Dashboard/Chatbot/components/TournamentPreview.tsx b/src/features/Dashboard/Chatbot/components/TournamentPreview.tsx
new file mode 100644
index 00000000..72906e52
--- /dev/null
+++ b/src/features/Dashboard/Chatbot/components/TournamentPreview.tsx
@@ -0,0 +1,142 @@
+import { marked } from "marked";
+import React from "react";
+import { IoLocationOutline } from "react-icons/io5";
+import Card from "src/Components/Card/Card";
+import FAQsSection from "src/features/Tournaments/pages/OverviewPage/FAQsSection/FAQsSection";
+import JudgesSection from "src/features/Tournaments/pages/OverviewPage/JudgesSection/JudgesSection";
+import PrizesSection from "src/features/Tournaments/pages/OverviewPage/PrizesSection/PrizesSection";
+import RegisterCard from "src/features/Tournaments/pages/OverviewPage/RegisterCard/RegisterCard";
+import { purifyHtml } from "src/utils/validation";
+import { useTournament } from "../contexts/tournament.context";
+
+export default function TournamentPreview() {
+ const { tournament } = useTournament();
+
+ const loading = !tournament;
+
+ return (
+
+ {loading &&
Loading...
}
+ {tournament && (
+ <>
+
+
+
+
+
+
+
+
TOURNAMENT 🏆
+
+ {tournament.title}
+
+
+ {new Date(tournament.start_date).toDateString()} -{" "}
+ {new Date(tournament.end_date).toDateString()}
+
+
+ {tournament.location}
+
+
+
+
+
+
+
+
+
+ {true &&
+ tournament.makers_deals &&
+ tournament.makers_deals?.length > 0 && (
+
+
+ Hacker perks from our partners 🎁
+
+
+
+ )}
+
+
+
+
+
+ {tournament.judges && tournament.judges?.length > 0 && (
+
+ )}
+ {tournament.faqs && tournament.faqs.length > 0 && (
+
+ )}
+
+ >
+ )}
+
+ );
+}
diff --git a/src/features/Dashboard/Chatbot/components/useSubmitMessage.tsx b/src/features/Dashboard/Chatbot/components/useSubmitMessage.tsx
new file mode 100644
index 00000000..7f45b320
--- /dev/null
+++ b/src/features/Dashboard/Chatbot/components/useSubmitMessage.tsx
@@ -0,0 +1,31 @@
+import { useState } from "react";
+import { useChat } from "../contexts/chat.context";
+
+export const useSubmitMessage = () => {
+ const { submitMessage } = useChat();
+ const [currentState, setCurrentState] =
+ useState<"idle" | "fetching-invoice" | "getting-response" | "error">(
+ "idle"
+ );
+ const [error, setError] = useState(null);
+
+ const submit = async (prompt: string) => {
+ try {
+ await submitMessage(prompt, {
+ onStatusUpdate: (status) => {
+ if (status === "fetching-invoice")
+ setCurrentState("fetching-invoice");
+ if (status === "fetching-response")
+ setCurrentState("getting-response");
+ },
+ });
+ setCurrentState("idle");
+ } catch (error) {
+ setCurrentState("error");
+ setError(error);
+ } finally {
+ }
+ };
+
+ return { submit, currentState, error };
+};
diff --git a/src/features/Dashboard/Chatbot/contexts/chat.context.tsx b/src/features/Dashboard/Chatbot/contexts/chat.context.tsx
new file mode 100644
index 00000000..44ce6486
--- /dev/null
+++ b/src/features/Dashboard/Chatbot/contexts/chat.context.tsx
@@ -0,0 +1,134 @@
+import {
+ ChatCompletionFunctions,
+ ChatCompletionRequestMessage,
+ ChatCompletionRequestMessageFunctionCall,
+ ChatCompletionResponseMessage,
+} from "openai";
+import { createContext, useState, useContext, useCallback } from "react";
+import YAML from "yaml";
+import { sendCommand } from "../lib/open-ai.service";
+
+export type Message = {
+ id: string;
+ content: string;
+ role: "user" | "assistant";
+};
+
+interface ChatContext {
+ messages: Message[];
+ submitMessage: (
+ message: string,
+ options?: Partial<{
+ onStatusUpdate: (
+ status:
+ | "fetching-invoice"
+ | "invoice-paid"
+ | "fetching-response"
+ | "response-fetched"
+ ) => void;
+ }>
+ ) => Promise;
+}
+
+const context = createContext(null!);
+
+export const ChatContextProvider = ({
+ children,
+ systemMessage,
+ getContextMessage,
+ functionsTemplates,
+ functions,
+}: {
+ children: React.ReactNode;
+ systemMessage: string;
+ getContextMessage: (options: { input: string }) => Promise;
+ functionsTemplates: ChatCompletionFunctions[];
+ functions: Record;
+}) => {
+ const [messages, setMessages] = useState([]);
+
+ const submitMessage = useCallback(
+ async (message: string, options) => {
+ const onStatusUpdate = options?.onStatusUpdate || (() => {});
+
+ const oldMessages = messages;
+
+ const newResponses = [
+ { content: message, role: "user" },
+ ] as (ChatCompletionRequestMessage & { internal?: boolean })[];
+
+ onStatusUpdate("fetching-response");
+
+ let finishedCallingFunctions = false;
+
+ const contextMessage = await getContextMessage({
+ input: message,
+ });
+
+ while (!finishedCallingFunctions) {
+ const response = await sendCommand({
+ messages: [...oldMessages, ...newResponses],
+ availableFunctions: functionsTemplates,
+ systemMessage:
+ systemMessage +
+ (contextMessage ? `\nContext: ${contextMessage}` : ""),
+ });
+
+ if (response?.function_call) {
+ const fnResponse = execFunction(response.function_call);
+ newResponses.push({ ...response, internal: true });
+
+ newResponses.push({
+ content: YAML.stringify(fnResponse ?? "Success"),
+ role: "function",
+ name: response.function_call.name,
+ internal: true,
+ });
+ } else if (response?.content) {
+ newResponses.push(response);
+ finishedCallingFunctions = true;
+ }
+ }
+
+ function execFunction(f: ChatCompletionRequestMessageFunctionCall) {
+ const fn = functions[f.name as string];
+ if (!fn) throw new Error(`Function ${f.name} not found`);
+ return fn(JSON.parse(f?.arguments as any));
+ }
+
+ onStatusUpdate("response-fetched");
+
+ setMessages((prev) => [
+ ...prev,
+ {
+ id: Math.random().toString(),
+ content: message,
+ role: "user",
+ },
+ ...newResponses
+ .filter((r) => !r.internal)
+ .filter((r) => r.role === "assistant")
+ .map((r) => ({
+ id: Math.random().toString(),
+ content: r.content ?? "",
+ role: "assistant" as const,
+ })),
+ ]);
+ },
+ [functions, functionsTemplates, messages, systemMessage]
+ );
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const useChat = () => {
+ const ctx = useContext(context);
+ if (!ctx) {
+ throw new Error("useChat must be used within a ChatContextProvider");
+ }
+ return ctx;
+};
diff --git a/src/features/Dashboard/Chatbot/contexts/tournament.context.tsx b/src/features/Dashboard/Chatbot/contexts/tournament.context.tsx
new file mode 100644
index 00000000..047adabf
--- /dev/null
+++ b/src/features/Dashboard/Chatbot/contexts/tournament.context.tsx
@@ -0,0 +1,52 @@
+import { createContext, useState, useContext, useCallback } from "react";
+import {
+ Tournament,
+ TournamentMakerDeal,
+ useGetTournamentByIdQuery,
+} from "src/graphql";
+
+interface TournamentContext {
+ tournament?: Tournament;
+ updateTournament: (newTournament: Partial) => void;
+}
+
+const context = createContext(null!);
+
+export const TournamentContextProvider = ({
+ children,
+ idOrSlug,
+}: {
+ children: React.ReactNode;
+ idOrSlug: string;
+}) => {
+ const [tournament, setTournament] = useState();
+
+ useGetTournamentByIdQuery({
+ variables: {
+ idOrSlug,
+ },
+ onCompleted: (data) => {
+ setTournament(data.getTournamentById);
+ },
+ });
+
+ const updateTournament = useCallback((newTournament: Partial) => {
+ setTournament((prev) => ({ ...prev, ...(newTournament as any) }));
+ }, []);
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const useTournament = () => {
+ const ctx = useContext(context);
+ if (!ctx) {
+ throw new Error(
+ "useTournament must be used within a TournamentContextProvider"
+ );
+ }
+ return ctx;
+};
diff --git a/src/features/Dashboard/Chatbot/contexts/tournamentChatbot.context.tsx b/src/features/Dashboard/Chatbot/contexts/tournamentChatbot.context.tsx
new file mode 100644
index 00000000..b8b95969
--- /dev/null
+++ b/src/features/Dashboard/Chatbot/contexts/tournamentChatbot.context.tsx
@@ -0,0 +1,345 @@
+import { ChatCompletionFunctions } from "openai";
+import {
+ createContext,
+ useCallback,
+ useContext,
+ useEffect,
+ useMemo,
+ useRef,
+} from "react";
+import { Tournament, TournamentMakerDeal } from "src/graphql";
+import YAML from "yaml";
+import { getEmbeddings } from "../lib";
+import { ChatContextProvider } from "./chat.context";
+import { useTournament } from "./tournament.context";
+
+interface TournamentChatbotContext {}
+
+const context = createContext(null!);
+
+export const TournamentChatbotContextProvider = ({
+ children,
+}: {
+ children: React.ReactNode;
+}) => {
+ const { tournament, updateTournament } = useTournament();
+
+ const tournamentRef = useRef(tournament);
+
+ const getContextMessage = useCallback(
+ async ({ input }: { input: string }) => {
+ if (!tournamentRef.current) return null;
+
+ const relatedData = await getRelatedData(input);
+ const dataToInclude = {} as any;
+ console.log(relatedData);
+ relatedData.slice(0, 2).forEach(({ field }) => {
+ if (field === "title")
+ dataToInclude["Tournament Title"] = tournamentRef.current?.title;
+ if (field === "description")
+ dataToInclude["TournamentDescription"] =
+ tournamentRef.current?.description;
+ if (field === "start_date")
+ dataToInclude["Start Date"] = tournamentRef.current?.start_date;
+ if (field === "end_date")
+ dataToInclude["End Date"] = tournamentRef.current?.end_date;
+ if (field === "makers_deals")
+ dataToInclude["Makers Deals"] =
+ tournamentRef.current?.makers_deals.map((d) => ({
+ title: d.title,
+ description: d.description,
+ url: d.url,
+ }));
+ });
+
+ return YAML.stringify(dataToInclude);
+ },
+ []
+ );
+
+ useEffect(() => {
+ tournamentRef.current = tournament;
+ }, [tournament]);
+
+ const functions: Functions = useMemo(() => {
+ const set_tournament_info: Functions["set_tournament_info"] = (
+ new_data
+ ) => {
+ const newData = {
+ ...tournamentRef.current,
+ ...new_data,
+ } as Tournament;
+ tournamentRef.current = newData;
+ updateTournament(new_data);
+ };
+
+ const set_tournament_deals: Functions["set_tournament_deals"] = ({
+ deals,
+ }) => {
+ set_tournament_info({
+ makers_deals: deals.map((d) => ({
+ ...d,
+ __typename: "TournamentMakerDeal",
+ })),
+ });
+ };
+
+ return {
+ set_tournament_info,
+ set_tournament_deals,
+
+ // get_current_tournament_state: (select) => {
+ // const tournament = tournamentRef.current;
+
+ // if (!tournament) throw new Error("No tournament selected");
+
+ // let result: Partial = {};
+
+ // for (const [key, include] of Object.entries(select)) {
+ // if (include)
+ // result[key as keyof Tournament] =
+ // tournament[key as keyof Tournament];
+ // }
+
+ // if (result.makers_deals)
+ // result.makers_deals = result.makers_deals?.map(
+ // ({ __typename, ...data }) => data
+ // );
+
+ // return {
+ // tournament_data: result,
+ // };
+ // },
+ };
+ }, [updateTournament]);
+
+ return (
+
+
+ {children}
+
+
+ );
+};
+
+export const useTournamentChatbot = () => {
+ const ctx = useContext(context);
+ if (!ctx) {
+ throw new Error(
+ "useTournament must be used within a TournamentChatbotContextProvider"
+ );
+ }
+ return ctx;
+};
+
+const SYSTEM_MESSAGE = `
+You are an assisstant chatbot whose sole job is to help the user in updating his tournament data.
+
+The user will provide you with a prompt that can contain one or more commands from the user.
+
+& you will have to decide which functions to use & what parameters to pass to them to update the tournament data.
+
+RULES:
+- Never invent new functions. Only use the functions provided to you.
+- Never make assumptions about the values to plug into functions. If not clear, ask user for clarification.
+- Always Use the existing data in the tournament to make decisions.
+- If you don't know how to do something, tell the user that you can't & suggest to him contacting the admins.
+- Don't answer questions or queries not related to updating the tournament data.
+- Don't call the same function twice with the same parameters.
+- functions that start with "set" will replace old data with new data. So make sure to include all the data you want to keep.
+`;
+
+type SelectTournamentFields = {
+ [key in keyof Tournament]?: boolean;
+};
+
+type Functions = {
+ set_tournament_info: (parameters: Partial) => void;
+ set_tournament_deals: (parameters: { deals: TournamentMakerDeal[] }) => void;
+ // get_current_tournament_state: (parameters: SelectTournamentFields) => {
+ // tournament_data: Partial;
+ // };
+};
+
+const availableFunctions: ChatCompletionFunctions[] = [
+ {
+ name: "set_tournament_info",
+ description: "set the new tournament info",
+ parameters: {
+ type: "object",
+ properties: {
+ title: {
+ type: "string",
+ description: "The new title of the tournament",
+ },
+ description: {
+ type: "string",
+ description: "The new description of the tournament in markdown",
+ },
+ start_date: {
+ type: "string",
+ description: "The new start date of the tournament in ISO format",
+ },
+ end_date: {
+ type: "string",
+ description: "The new end date of the tournament in ISO format",
+ },
+ },
+ },
+ },
+ {
+ name: "set_tournament_deals",
+ description: "set the tournament makers deals",
+ parameters: {
+ type: "object",
+ properties: {
+ deals: {
+ type: "array",
+ description: "the list of deals to set",
+ items: {
+ type: "object",
+ properties: {
+ title: {
+ type: "string",
+ description: "The new title of the deal",
+ },
+ description: {
+ type: "string",
+ description: "The new description of the deal in markdown",
+ },
+ url: {
+ type: "string",
+ description: "The new url of the deal",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ // {
+ // name: "get_current_tournament_state",
+ // description:
+ // "Get the current state of the tournament selectivly. You will only get the fields you ask for.",
+ // parameters: {
+ // type: "object",
+ // properties: {
+ // title: {
+ // type: "boolean",
+ // description: "Get title or not",
+ // },
+ // description: {
+ // type: "boolean",
+ // description: "Get description or not",
+ // },
+ // start_date: {
+ // type: "boolean",
+ // description: "Get start date or not",
+ // },
+ // end_date: {
+ // type: "boolean",
+ // description: "Get end date or not",
+ // },
+
+ // makers_deals: {
+ // type: "boolean",
+ // description: "Get makers deals or not",
+ // },
+ // },
+ // },
+ // },
+];
+
+async function getRelatedData(input: string) {
+ // const input1 = [
+ // "add or update title",
+ // "add or update description",
+ // "add or update start and end dates",
+ // "add or update or delete makers deals",
+ // // "add or update or delete organizers",
+ // // "add or update or delete prizes",
+ // // "add or update or delete schedule",
+ // ];
+
+ const fieldsEmbeddings = await getFieldsEmbeddings();
+
+ const inputEmbedding = await getEmbeddings([input]).then(
+ (res) => res.data[0].embedding
+ );
+
+ const similarities = getSimilarities(
+ inputEmbedding,
+ fieldsEmbeddings.map((f) => f.embeddings)
+ );
+
+ const fieldsSimilarities = fieldsEmbeddings.map((f, i) => ({
+ field: f.field,
+ similarity: similarities[i],
+ }));
+
+ const sortedFields = fieldsSimilarities.sort(
+ (a, b) => b.similarity - a.similarity
+ );
+
+ return sortedFields;
+}
+
+const fields = {
+ title: "add or update title",
+ description: "add or update description",
+ start_date: "add or update start date",
+ end_date: "add or update end date",
+ makers_deals: "add or update makers deals",
+} as const;
+
+let cachedFieldsEmbeddings:
+ | { field: keyof typeof fields; embeddings: number[] }[]
+ | null = null;
+
+async function getFieldsEmbeddings() {
+ if (cachedFieldsEmbeddings === null) {
+ const embeddings = await getEmbeddings(Object.values(fields)).then((res) =>
+ res.data.map((d) => d.embedding)
+ );
+ cachedFieldsEmbeddings = embeddings.map((embedding, i) => ({
+ field: Object.keys(fields)[i] as keyof typeof fields,
+ embeddings: embedding,
+ }));
+ }
+
+ return cachedFieldsEmbeddings;
+}
+
+type Vector = number[];
+
+function getSimilarities(test: Vector, options: Vector[]) {
+ function dotproduct(a: Vector, b: Vector) {
+ let n = 0,
+ lim = Math.min(a.length, b.length);
+ for (let i = 0; i < lim; i++) n += a[i] * b[i];
+ return n;
+ }
+ function norm2(a: Vector) {
+ let sumsqr = 0;
+ for (let i = 0; i < a.length; i++) sumsqr += a[i] * a[i];
+ return Math.sqrt(sumsqr);
+ }
+ function similarity(a: Vector, b: Vector) {
+ return dotproduct(a, b) / norm2(a) / norm2(b);
+ }
+
+ let similarities = [];
+ for (let i = 0; i < options.length; i++) {
+ similarities.push(similarity(test, options[i]));
+ }
+ return similarities;
+}
+
+// getRelatedData(`
+// Make the end date month september
+// `).then((res) => console.log(res));
diff --git a/src/features/Dashboard/Chatbot/index.ts b/src/features/Dashboard/Chatbot/index.ts
new file mode 100644
index 00000000..6c926be2
--- /dev/null
+++ b/src/features/Dashboard/Chatbot/index.ts
@@ -0,0 +1 @@
+export * from "./Chatbot";
diff --git a/src/features/Dashboard/Chatbot/lib/index.ts b/src/features/Dashboard/Chatbot/lib/index.ts
new file mode 100644
index 00000000..6b6c9a85
--- /dev/null
+++ b/src/features/Dashboard/Chatbot/lib/index.ts
@@ -0,0 +1 @@
+export * from "./open-ai.service";
diff --git a/src/features/Dashboard/Chatbot/lib/open-ai.service.ts b/src/features/Dashboard/Chatbot/lib/open-ai.service.ts
new file mode 100644
index 00000000..9157272c
--- /dev/null
+++ b/src/features/Dashboard/Chatbot/lib/open-ai.service.ts
@@ -0,0 +1,83 @@
+import {
+ ChatCompletionFunctions,
+ ChatCompletionRequestMessage,
+ ChatCompletionResponseMessage,
+ Configuration,
+ CreateEmbeddingRequest,
+ OpenAIApi,
+} from "openai";
+import { CONSTS } from "src/utils";
+
+let openai: OpenAIApi;
+
+export function getOpenAIApi() {
+ if (openai) return openai;
+
+ let apiKey: string | null = CONSTS.OPENAI_API_KEY;
+
+ if (!apiKey) apiKey = prompt("Please enter an OpenAI API key");
+
+ if (!apiKey) throw new Error("No OpenAI API key provided");
+
+ const configuration = new Configuration({
+ apiKey,
+ });
+
+ openai = new OpenAIApi(configuration);
+
+ return openai;
+}
+
+export async function sendCommand({
+ messages: _messages,
+ systemMessage,
+ availableFunctions = [],
+}: {
+ systemMessage?: string;
+ availableFunctions?: ChatCompletionFunctions[];
+ messages: ChatCompletionRequestMessage[];
+}) {
+ const openai = getOpenAIApi();
+
+ let messages: ChatCompletionRequestMessage[] = [];
+
+ if (systemMessage) {
+ messages.push({
+ role: "system",
+ content: systemMessage,
+ });
+
+ messages.push(
+ ..._messages.map((m) => ({
+ role: m.role,
+ name: m.name,
+ content: m.content,
+ function_call: m.function_call,
+ }))
+ );
+
+ const response = await openai.createChatCompletion({
+ model: "gpt-3.5-turbo",
+ messages,
+ ...(availableFunctions.length > 0 && { functions: availableFunctions }),
+ });
+
+ const finish_reason = response.data.choices[0].finish_reason;
+
+ if (finish_reason !== "function_call" && finish_reason !== "stop")
+ throw new Error(`Unexpected finish reason: ${finish_reason}`);
+
+ return response.data.choices[0].message;
+ }
+}
+
+export async function getEmbeddings(input: string | string[]) {
+ const openai = getOpenAIApi();
+
+ const response = await openai.createEmbedding({
+ model: "text-embedding-ada-002",
+ input,
+ });
+
+ return response.data;
+}
diff --git a/src/features/Dashboard/Chatbot/lib/updateTournament.graphql b/src/features/Dashboard/Chatbot/lib/updateTournament.graphql
new file mode 100644
index 00000000..abe4fc56
--- /dev/null
+++ b/src/features/Dashboard/Chatbot/lib/updateTournament.graphql
@@ -0,0 +1,92 @@
+mutation UpdateTournament($data: UpdateTournamentInput) {
+ updateTournament(data: $data) {
+ id
+ title
+ description
+ thumbnail_image
+ cover_image
+ start_date
+ end_date
+ location
+ website
+ events_count
+ makers_count
+ projects_count
+ tracks {
+ id
+ title
+ icon
+ }
+ prizes {
+ title
+ description
+ image
+ positions {
+ position
+ reward
+ project
+ }
+ additional_prizes {
+ text
+ url
+ }
+ }
+ judges {
+ name
+ company
+ avatar
+ }
+ faqs {
+ question
+ answer
+ }
+ events {
+ id
+ title
+ image
+ description
+ starts_at
+ ends_at
+ location
+ website
+ type
+ links
+ }
+ contacts {
+ type
+ url
+ }
+ partners {
+ title
+ items {
+ image
+ url
+ isBigImage
+ }
+ }
+ schedule {
+ date
+ events {
+ title
+ time
+ timezone
+ url
+ type
+ location
+ }
+ }
+ makers_deals {
+ title
+ description
+ url
+ }
+ config {
+ registerationOpen
+ projectsSubmissionOpen
+ ideasRootNostrEventId
+ showFeed
+ mainFeedHashtag
+ feedFilters
+ }
+ }
+}
diff --git a/src/features/Tournaments/pages/OverviewPage/AI4ALLOverviewPage/AI4ALLOverviewPage.tsx b/src/features/Tournaments/pages/OverviewPage/AI4ALLOverviewPage/AI4ALLOverviewPage.tsx
index dafbfb7a..2bbaf554 100644
--- a/src/features/Tournaments/pages/OverviewPage/AI4ALLOverviewPage/AI4ALLOverviewPage.tsx
+++ b/src/features/Tournaments/pages/OverviewPage/AI4ALLOverviewPage/AI4ALLOverviewPage.tsx
@@ -79,6 +79,7 @@ export default function LegendsOfLightningOverviewPage() {
)}
m.user.avatar)}
diff --git a/src/features/Tournaments/pages/OverviewPage/LegendsOfLightningOverviewPage/LegendsOfLightningOverviewPage.tsx b/src/features/Tournaments/pages/OverviewPage/LegendsOfLightningOverviewPage/LegendsOfLightningOverviewPage.tsx
index 7f757d12..898db147 100644
--- a/src/features/Tournaments/pages/OverviewPage/LegendsOfLightningOverviewPage/LegendsOfLightningOverviewPage.tsx
+++ b/src/features/Tournaments/pages/OverviewPage/LegendsOfLightningOverviewPage/LegendsOfLightningOverviewPage.tsx
@@ -79,6 +79,7 @@ export default function LegendsOfLightningOverviewPage() {
)}
m.user.avatar)}
diff --git a/src/features/Tournaments/pages/OverviewPage/NostrHackWeekOverviewPage/NostrHackWeekOverviewPage.tsx b/src/features/Tournaments/pages/OverviewPage/NostrHackWeekOverviewPage/NostrHackWeekOverviewPage.tsx
index c80bcd65..41633b0e 100644
--- a/src/features/Tournaments/pages/OverviewPage/NostrHackWeekOverviewPage/NostrHackWeekOverviewPage.tsx
+++ b/src/features/Tournaments/pages/OverviewPage/NostrHackWeekOverviewPage/NostrHackWeekOverviewPage.tsx
@@ -79,6 +79,7 @@ export default function NostrHackWeekOverviewPage() {
)}
m.user.avatar)}
diff --git a/src/features/Tournaments/pages/OverviewPage/OverviewPage.tsx b/src/features/Tournaments/pages/OverviewPage/OverviewPage.tsx
index 83749b17..29945c25 100644
--- a/src/features/Tournaments/pages/OverviewPage/OverviewPage.tsx
+++ b/src/features/Tournaments/pages/OverviewPage/OverviewPage.tsx
@@ -85,6 +85,7 @@ export default function OverviewPage() {
isRegistrationOpen={tournamentDetails.config.registerationOpen}
partnersList={tournamentDetails.partners}
contacts={tournamentDetails.contacts}
+ tournament={tournamentDetails}
/>
diff --git a/src/features/Tournaments/pages/OverviewPage/RegisterCard/RegisterCard.tsx b/src/features/Tournaments/pages/OverviewPage/RegisterCard/RegisterCard.tsx
index 1b39cb3c..1fa74825 100644
--- a/src/features/Tournaments/pages/OverviewPage/RegisterCard/RegisterCard.tsx
+++ b/src/features/Tournaments/pages/OverviewPage/RegisterCard/RegisterCard.tsx
@@ -4,7 +4,7 @@ import { GiOstrich } from "react-icons/gi";
import Button from "src/Components/Button/Button";
import Card from "src/Components/Card/Card";
import Avatar from "src/features/Profiles/Components/Avatar/Avatar";
-import { TournamentContact, TournamentPartner } from "src/graphql";
+import { Tournament, TournamentContact, TournamentPartner } from "src/graphql";
import { openModal } from "src/redux/features/modals.slice";
import { useCountdown } from "src/utils/hooks";
import { useAppDispatch, useAppSelector } from "src/utils/hooks";
@@ -12,6 +12,7 @@ import { twMerge } from "tailwind-merge";
import { useTournament } from "../../TournamentDetailsPage/TournamentDetailsContext";
interface Props {
+ tournament: Pick;
start_date: string;
makers_count: number;
avatars: string[];
@@ -29,24 +30,22 @@ export default function RegisterCard({
isRegistrationOpen,
partnersList,
contacts,
+ tournament,
}: Props) {
const counter = useCountdown(start_date);
- const {
- tournamentDetails: { id: tournamentId, end_date },
- } = useTournament();
const isLoggedIn = useAppSelector((state) => !!state.user.me);
const dispatch = useAppDispatch();
const onRegister = () => {
- if (!tournamentId) return;
+ if (!tournament.id) return;
if (isLoggedIn)
dispatch(
openModal({
Modal: "RegisterTournamet_ConfrimAccount",
props: {
- tournamentId: Number(tournamentId),
+ tournamentId: Number(tournament.id),
},
})
);
@@ -55,7 +54,7 @@ export default function RegisterCard({
openModal({
Modal: "RegisterTournamet_Login",
props: {
- tournamentId: Number(tournamentId),
+ tournamentId: Number(tournament.id),
},
})
);
@@ -149,7 +148,7 @@ export default function RegisterCard({
•
Live
- entries close {dayjs(end_date).format("Do MMM")}
+ entries close {dayjs(tournament.end_date).format("Do MMM")}
) : (
diff --git a/src/features/Tournaments/pages/TournamentDetailsPage/tournamentDetails.graphql b/src/features/Tournaments/pages/TournamentDetailsPage/tournamentDetails.graphql
index 0949cd2e..9f713b82 100644
--- a/src/features/Tournaments/pages/TournamentDetailsPage/tournamentDetails.graphql
+++ b/src/features/Tournaments/pages/TournamentDetailsPage/tournamentDetails.graphql
@@ -51,6 +51,7 @@ query GetTournamentById($idOrSlug: String!) {
links
}
faqs {
+ id
question
answer
}
diff --git a/src/graphql/index.tsx b/src/graphql/index.tsx
index a69cd270..01f93b01 100644
--- a/src/graphql/index.tsx
+++ b/src/graphql/index.tsx
@@ -151,6 +151,43 @@ export type CreateProjectResponse = {
project: Project;
};
+export type CreateTournamentFaqInput = {
+ answer: Scalars['String'];
+ question: Scalars['String'];
+};
+
+export type CreateTournamentInput = {
+ config: TournamentConfigInput;
+ contacts: Array;
+ cover_image: ImageInput;
+ description: Scalars['String'];
+ end_date: Scalars['Date'];
+ faqs: Array;
+ judges: Array;
+ location: Scalars['String'];
+ makers_deals: Array;
+ partners: Array;
+ prizes: Array;
+ schedule: Array;
+ slug: Scalars['String'];
+ start_date: Scalars['Date'];
+ thumbnail_image: ImageInput;
+ title: Scalars['String'];
+ tracks: Array;
+ website: Scalars['String'];
+};
+
+export type CreateTournamentJudgeInput = {
+ avatar: ImageInput;
+ company: Scalars['String'];
+ name: Scalars['String'];
+};
+
+export type CreateTournamentTrackInput = {
+ icon: Scalars['String'];
+ title: Scalars['String'];
+};
+
export type Donation = {
__typename?: 'Donation';
amount: Scalars['Int'];
@@ -234,6 +271,7 @@ export type Mutation = {
confirmVote: Vote;
createProject: Maybe;
createStory: Maybe;
+ createTournament: Maybe;
deleteProject: Maybe;
deleteStory: Maybe;
donate: Donation;
@@ -245,6 +283,7 @@ export type Mutation = {
updateProfileDetails: Maybe;
updateProfileRoles: Maybe;
updateProject: Maybe;
+ updateTournament: Maybe;
updateTournamentRegistration: Maybe;
updateUserPreferences: User;
vote: Vote;
@@ -278,6 +317,11 @@ export type MutationCreateStoryArgs = {
};
+export type MutationCreateTournamentArgs = {
+ data: InputMaybe;
+};
+
+
export type MutationDeleteProjectArgs = {
id: Scalars['Int'];
};
@@ -334,6 +378,11 @@ export type MutationUpdateProjectArgs = {
};
+export type MutationUpdateTournamentArgs = {
+ data: InputMaybe;
+};
+
+
export type MutationUpdateTournamentRegistrationArgs = {
data: InputMaybe;
tournament_id: Scalars['Int'];
@@ -828,7 +877,16 @@ export type TournamentConfig = {
mainFeedHashtag: Maybe;
projectsSubmissionOpen: Scalars['Boolean'];
registerationOpen: Scalars['Boolean'];
- showFeed: Scalars['Boolean'];
+ showFeed: Maybe;
+};
+
+export type TournamentConfigInput = {
+ feedFilters?: InputMaybe>;
+ ideasRootNostrEventId?: InputMaybe;
+ mainFeedHashtag?: InputMaybe;
+ projectsSubmissionOpen: Scalars['Boolean'];
+ registerationOpen: Scalars['Boolean'];
+ showFeed?: InputMaybe;
};
export type TournamentContact = {
@@ -837,6 +895,11 @@ export type TournamentContact = {
url: Scalars['String'];
};
+export type TournamentContactInput = {
+ type: Scalars['String'];
+ url: Scalars['String'];
+};
+
export type TournamentEvent = {
__typename?: 'TournamentEvent';
description: Scalars['String'];
@@ -861,6 +924,7 @@ export enum TournamentEventTypeEnum {
export type TournamentFaq = {
__typename?: 'TournamentFAQ';
answer: Scalars['String'];
+ id: Scalars['Int'];
question: Scalars['String'];
};
@@ -878,6 +942,12 @@ export type TournamentMakerDeal = {
url: Maybe;
};
+export type TournamentMakerDealInput = {
+ description: Scalars['String'];
+ title: Scalars['String'];
+ url?: InputMaybe;
+};
+
export enum TournamentMakerHackingStatusEnum {
OpenToConnect = 'OpenToConnect',
Solo = 'Solo'
@@ -903,6 +973,11 @@ export type TournamentPartner = {
title: Scalars['String'];
};
+export type TournamentPartnerInput = {
+ items: Array;
+ title: Scalars['String'];
+};
+
export type TournamentPartnerItem = {
__typename?: 'TournamentPartnerItem';
image: Scalars['String'];
@@ -910,6 +985,12 @@ export type TournamentPartnerItem = {
url: Scalars['String'];
};
+export type TournamentPartnerItemInput = {
+ image: Scalars['String'];
+ isBigImage?: InputMaybe;
+ url: Scalars['String'];
+};
+
export type TournamentPrize = {
__typename?: 'TournamentPrize';
additional_prizes: Maybe>;
@@ -925,6 +1006,19 @@ export type TournamentPrizeAdditionalPrize = {
url: Maybe;
};
+export type TournamentPrizeAdditionalPrizeInput = {
+ text: Scalars['String'];
+ url?: InputMaybe;
+};
+
+export type TournamentPrizeInput = {
+ additional_prizes?: InputMaybe>;
+ description: Scalars['String'];
+ image: Scalars['String'];
+ positions: Array;
+ title: Scalars['String'];
+};
+
export type TournamentPrizePosition = {
__typename?: 'TournamentPrizePosition';
position: Scalars['String'];
@@ -932,6 +1026,12 @@ export type TournamentPrizePosition = {
reward: Scalars['String'];
};
+export type TournamentPrizePositionInput = {
+ position: Scalars['String'];
+ project?: InputMaybe;
+ reward: Scalars['String'];
+};
+
export type TournamentProjectsResponse = {
__typename?: 'TournamentProjectsResponse';
allItemsCount: Maybe;
@@ -956,6 +1056,20 @@ export type TournamentScheduleEvent = {
url: Maybe;
};
+export type TournamentScheduleEventInput = {
+ location?: InputMaybe;
+ time?: InputMaybe;
+ timezone?: InputMaybe;
+ title: Scalars['String'];
+ type?: InputMaybe;
+ url?: InputMaybe;
+};
+
+export type TournamentScheduleInput = {
+ date: Scalars['String'];
+ events: Array;
+};
+
export type TournamentTrack = {
__typename?: 'TournamentTrack';
icon: Scalars['String'];
@@ -990,6 +1104,22 @@ export type UpdateProjectInput = {
website: Scalars['String'];
};
+export type UpdateTournamentInput = {
+ config: TournamentConfigInput;
+ contacts: Array;
+ description: Scalars['String'];
+ end_date: Scalars['Date'];
+ id?: InputMaybe;
+ location: Scalars['String'];
+ makers_deals: Array;
+ partners: Array;
+ prizes: Array;
+ schedule: Array;
+ start_date: Scalars['Date'];
+ title: Scalars['String'];
+ website: Scalars['String'];
+};
+
export type UpdateTournamentRegistrationInput = {
email?: InputMaybe;
hacking_status?: InputMaybe;
@@ -1107,6 +1237,13 @@ export type MeQueryVariables = Exact<{ [key: string]: never; }>;
export type MeQuery = { __typename?: 'Query', me: { __typename?: 'User', id: number, name: string, avatar: string, jobTitle: string | null, bio: string | null, primary_nostr_key: string | null, last_seen_notification_time: any } | null };
+export type UpdateTournamentMutationVariables = Exact<{
+ data: InputMaybe;
+}>;
+
+
+export type UpdateTournamentMutation = { __typename?: 'Mutation', updateTournament: { __typename?: 'Tournament', id: number, title: string, description: string, thumbnail_image: string, cover_image: string, start_date: any, end_date: any, location: string, website: string, events_count: number, makers_count: number, projects_count: number, tracks: Array<{ __typename?: 'TournamentTrack', id: number, title: string, icon: string }>, prizes: Array<{ __typename?: 'TournamentPrize', title: string, description: string, image: string, positions: Array<{ __typename?: 'TournamentPrizePosition', position: string, reward: string, project: string | null }>, additional_prizes: Array<{ __typename?: 'TournamentPrizeAdditionalPrize', text: string, url: string | null }> | null }>, judges: Array<{ __typename?: 'TournamentJudge', name: string, company: string, avatar: string }>, faqs: Array<{ __typename?: 'TournamentFAQ', question: string, answer: string }>, events: Array<{ __typename?: 'TournamentEvent', id: number, title: string, image: string, description: string, starts_at: any, ends_at: any, location: string, website: string, type: TournamentEventTypeEnum, links: Array }>, contacts: Array<{ __typename?: 'TournamentContact', type: string, url: string }>, partners: Array<{ __typename?: 'TournamentPartner', title: string, items: Array<{ __typename?: 'TournamentPartnerItem', image: string, url: string, isBigImage: boolean | null }> }>, schedule: Array<{ __typename?: 'TournamentSchedule', date: string, events: Array<{ __typename?: 'TournamentScheduleEvent', title: string, time: string | null, timezone: string | null, url: string | null, type: string | null, location: string | null }> }>, makers_deals: Array<{ __typename?: 'TournamentMakerDeal', title: string, description: string, url: string | null }>, config: { __typename?: 'TournamentConfig', registerationOpen: boolean, projectsSubmissionOpen: boolean, ideasRootNostrEventId: string | null, showFeed: boolean | null, mainFeedHashtag: string | null, feedFilters: Array | null } } | null };
+
export type DonationsStatsQueryVariables = Exact<{ [key: string]: never; }>;
@@ -1454,7 +1591,7 @@ export type GetTournamentByIdQueryVariables = Exact<{
}>;
-export type GetTournamentByIdQuery = { __typename?: 'Query', pubkeysOfMakersInTournament: Array, pubkeysOfProjectsInTournament: Array, getTournamentById: { __typename?: 'Tournament', id: number, title: string, description: string, thumbnail_image: string, cover_image: string, start_date: any, end_date: any, location: string, website: string, events_count: number, makers_count: number, projects_count: number, prizes: Array<{ __typename?: 'TournamentPrize', title: string, description: string, image: string, positions: Array<{ __typename?: 'TournamentPrizePosition', position: string, reward: string, project: string | null }>, additional_prizes: Array<{ __typename?: 'TournamentPrizeAdditionalPrize', text: string, url: string | null }> | null }>, tracks: Array<{ __typename?: 'TournamentTrack', id: number, title: string, icon: string }>, judges: Array<{ __typename?: 'TournamentJudge', name: string, company: string, avatar: string }>, events: Array<{ __typename?: 'TournamentEvent', id: number, title: string, image: string, description: string, starts_at: any, ends_at: any, location: string, website: string, type: TournamentEventTypeEnum, links: Array }>, faqs: Array<{ __typename?: 'TournamentFAQ', question: string, answer: string }>, contacts: Array<{ __typename?: 'TournamentContact', type: string, url: string }>, partners: Array<{ __typename?: 'TournamentPartner', title: string, items: Array<{ __typename?: 'TournamentPartnerItem', image: string, url: string, isBigImage: boolean | null }> }>, schedule: Array<{ __typename?: 'TournamentSchedule', date: string, events: Array<{ __typename?: 'TournamentScheduleEvent', title: string, time: string | null, timezone: string | null, url: string | null, type: string | null, location: string | null }> }>, makers_deals: Array<{ __typename?: 'TournamentMakerDeal', title: string, description: string, url: string | null }>, config: { __typename?: 'TournamentConfig', registerationOpen: boolean, projectsSubmissionOpen: boolean, ideasRootNostrEventId: string | null, showFeed: boolean, mainFeedHashtag: string | null, feedFilters: Array | null } }, getMakersInTournament: { __typename?: 'TournamentMakersResponse', makers: Array<{ __typename?: 'TournamentParticipant', user: { __typename?: 'User', id: number, avatar: string } }> } };
+export type GetTournamentByIdQuery = { __typename?: 'Query', pubkeysOfMakersInTournament: Array, pubkeysOfProjectsInTournament: Array, getTournamentById: { __typename?: 'Tournament', id: number, title: string, description: string, thumbnail_image: string, cover_image: string, start_date: any, end_date: any, location: string, website: string, events_count: number, makers_count: number, projects_count: number, prizes: Array<{ __typename?: 'TournamentPrize', title: string, description: string, image: string, positions: Array<{ __typename?: 'TournamentPrizePosition', position: string, reward: string, project: string | null }>, additional_prizes: Array<{ __typename?: 'TournamentPrizeAdditionalPrize', text: string, url: string | null }> | null }>, tracks: Array<{ __typename?: 'TournamentTrack', id: number, title: string, icon: string }>, judges: Array<{ __typename?: 'TournamentJudge', name: string, company: string, avatar: string }>, events: Array<{ __typename?: 'TournamentEvent', id: number, title: string, image: string, description: string, starts_at: any, ends_at: any, location: string, website: string, type: TournamentEventTypeEnum, links: Array }>, faqs: Array<{ __typename?: 'TournamentFAQ', id: number, question: string, answer: string }>, contacts: Array<{ __typename?: 'TournamentContact', type: string, url: string }>, partners: Array<{ __typename?: 'TournamentPartner', title: string, items: Array<{ __typename?: 'TournamentPartnerItem', image: string, url: string, isBigImage: boolean | null }> }>, schedule: Array<{ __typename?: 'TournamentSchedule', date: string, events: Array<{ __typename?: 'TournamentScheduleEvent', title: string, time: string | null, timezone: string | null, url: string | null, type: string | null, location: string | null }> }>, makers_deals: Array<{ __typename?: 'TournamentMakerDeal', title: string, description: string, url: string | null }>, config: { __typename?: 'TournamentConfig', registerationOpen: boolean, projectsSubmissionOpen: boolean, ideasRootNostrEventId: string | null, showFeed: boolean | null, mainFeedHashtag: string | null, feedFilters: Array | null } }, getMakersInTournament: { __typename?: 'TournamentMakersResponse', makers: Array<{ __typename?: 'TournamentParticipant', user: { __typename?: 'User', id: number, avatar: string } }> } };
export type NostrKeysMetadataQueryVariables = Exact<{
keys: Array | Scalars['String'];
@@ -1801,6 +1938,126 @@ export function useMeLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions;
export type MeLazyQueryHookResult = ReturnType;
export type MeQueryResult = Apollo.QueryResult;
+export const UpdateTournamentDocument = gql`
+ mutation UpdateTournament($data: UpdateTournamentInput) {
+ updateTournament(data: $data) {
+ id
+ title
+ description
+ thumbnail_image
+ cover_image
+ start_date
+ end_date
+ location
+ website
+ events_count
+ makers_count
+ projects_count
+ tracks {
+ id
+ title
+ icon
+ }
+ prizes {
+ title
+ description
+ image
+ positions {
+ position
+ reward
+ project
+ }
+ additional_prizes {
+ text
+ url
+ }
+ }
+ judges {
+ name
+ company
+ avatar
+ }
+ faqs {
+ question
+ answer
+ }
+ events {
+ id
+ title
+ image
+ description
+ starts_at
+ ends_at
+ location
+ website
+ type
+ links
+ }
+ contacts {
+ type
+ url
+ }
+ partners {
+ title
+ items {
+ image
+ url
+ isBigImage
+ }
+ }
+ schedule {
+ date
+ events {
+ title
+ time
+ timezone
+ url
+ type
+ location
+ }
+ }
+ makers_deals {
+ title
+ description
+ url
+ }
+ config {
+ registerationOpen
+ projectsSubmissionOpen
+ ideasRootNostrEventId
+ showFeed
+ mainFeedHashtag
+ feedFilters
+ }
+ }
+}
+ `;
+export type UpdateTournamentMutationFn = Apollo.MutationFunction;
+
+/**
+ * __useUpdateTournamentMutation__
+ *
+ * To run a mutation, you first call `useUpdateTournamentMutation` within a React component and pass it any options that fit your needs.
+ * When your component renders, `useUpdateTournamentMutation` returns a tuple that includes:
+ * - A mutate function that you can call at any time to execute the mutation
+ * - An object with fields that represent the current status of the mutation's execution
+ *
+ * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
+ *
+ * @example
+ * const [updateTournamentMutation, { data, loading, error }] = useUpdateTournamentMutation({
+ * variables: {
+ * data: // value for 'data'
+ * },
+ * });
+ */
+export function useUpdateTournamentMutation(baseOptions?: Apollo.MutationHookOptions) {
+ const options = {...defaultOptions, ...baseOptions}
+ return Apollo.useMutation(UpdateTournamentDocument, options);
+ }
+export type UpdateTournamentMutationHookResult = ReturnType;
+export type UpdateTournamentMutationResult = Apollo.MutationResult;
+export type UpdateTournamentMutationOptions = Apollo.BaseMutationOptions;
export const DonationsStatsDocument = gql`
query DonationsStats {
getDonationsStats {
@@ -4317,6 +4574,7 @@ export const GetTournamentByIdDocument = gql`
links
}
faqs {
+ id
question
answer
}
diff --git a/src/mocks/data/tournament.ts b/src/mocks/data/tournament.ts
index e0d86ef5..b0d15d0a 100644
--- a/src/mocks/data/tournament.ts
+++ b/src/mocks/data/tournament.ts
@@ -240,24 +240,28 @@ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Semper turpis est, ac e
faqs: [
{
+ id: 1,
question: "What is Shock the Web?",
answer: `Shock the Web is a virtual hackathon to promote, explore, build and design web applications that can interact with WebLN enabled wallets and browsers. We want to make building on bitcoin more accessible to the masses of web developers out there.
Bitcoin development can seem scary for new developers coming in, but it doesn't have to be. With the lightning network's toolkit and libraries a bunch of new opportunities are waiting to be explored. We hope these hackathons can be a chance for you to preview what is possible on bitcoin and the lightning network by fostering collaboration, hopefully shortening (or easing) any developer onboarding time, and helping you connect with other bitcoiners in a fun and friendly space.`,
},
{
+ id: 2,
question: "When and where will it take place?",
answer: `Shock the Web is a virtual hackathon to promote, explore, build and design web applications that can interact with WebLN enabled wallets and browsers. We want to make building on bitcoin more accessible to the masses of web developers out there.
Bitcoin development can seem scary for new developers coming in, but it doesn't have to be. With the lightning network's toolkit and libraries a bunch of new opportunities are waiting to be explored. We hope these hackathons can be a chance for you to preview what is possible on bitcoin and the lightning network by fostering collaboration, hopefully shortening (or easing) any developer onboarding time, and helping you connect with other bitcoiners in a fun and friendly space.`,
},
{
+ id: 3,
question: "What will we be doing?",
answer: `Shock the Web is a virtual hackathon to promote, explore, build and design web applications that can interact with WebLN enabled wallets and browsers. We want to make building on bitcoin more accessible to the masses of web developers out there.
Bitcoin development can seem scary for new developers coming in, but it doesn't have to be. With the lightning network's toolkit and libraries a bunch of new opportunities are waiting to be explored. We hope these hackathons can be a chance for you to preview what is possible on bitcoin and the lightning network by fostering collaboration, hopefully shortening (or easing) any developer onboarding time, and helping you connect with other bitcoiners in a fun and friendly space.`,
},
{
+ id: 4,
question:
"This is my first time hacking on lightning, will there be help?",
answer: `Shock the Web is a virtual hackathon to promote, explore, build and design web applications that can interact with WebLN enabled wallets and browsers. We want to make building on bitcoin more accessible to the masses of web developers out there.
@@ -265,6 +269,7 @@ Bitcoin development can seem scary for new developers coming in, but it doesn't
Bitcoin development can seem scary for new developers coming in, but it doesn't have to be. With the lightning network's toolkit and libraries a bunch of new opportunities are waiting to be explored. We hope these hackathons can be a chance for you to preview what is possible on bitcoin and the lightning network by fostering collaboration, hopefully shortening (or easing) any developer onboarding time, and helping you connect with other bitcoiners in a fun and friendly space.`,
},
{
+ id: 5,
question:
"This is my first time hacking on lightning, will there be help?",
answer: `Shock the Web is a virtual hackathon to promote, explore, build and design web applications that can interact with WebLN enabled wallets and browsers. We want to make building on bitcoin more accessible to the masses of web developers out there.
@@ -272,12 +277,14 @@ Bitcoin development can seem scary for new developers coming in, but it doesn't
Bitcoin development can seem scary for new developers coming in, but it doesn't have to be. With the lightning network's toolkit and libraries a bunch of new opportunities are waiting to be explored. We hope these hackathons can be a chance for you to preview what is possible on bitcoin and the lightning network by fostering collaboration, hopefully shortening (or easing) any developer onboarding time, and helping you connect with other bitcoiners in a fun and friendly space.`,
},
{
+ id: 6,
question: "How many members can I have on my team?",
answer: `Shock the Web is a virtual hackathon to promote, explore, build and design web applications that can interact with WebLN enabled wallets and browsers. We want to make building on bitcoin more accessible to the masses of web developers out there.
Bitcoin development can seem scary for new developers coming in, but it doesn't have to be. With the lightning network's toolkit and libraries a bunch of new opportunities are waiting to be explored. We hope these hackathons can be a chance for you to preview what is possible on bitcoin and the lightning network by fostering collaboration, hopefully shortening (or easing) any developer onboarding time, and helping you connect with other bitcoiners in a fun and friendly space.`,
},
{
+ id: 7,
question: "Who will choose the winners?",
answer: `Shock the Web is a virtual hackathon to promote, explore, build and design web applications that can interact with WebLN enabled wallets and browsers. We want to make building on bitcoin more accessible to the masses of web developers out there.
diff --git a/src/utils/consts/consts.ts b/src/utils/consts/consts.ts
index ff91f12e..19c994e1 100644
--- a/src/utils/consts/consts.ts
+++ b/src/utils/consts/consts.ts
@@ -11,6 +11,7 @@ const CONSTS = {
DEFAULT_RELAYS,
BF_NOSTR_PUBKEY:
"4f260791d78a93d13e09f1965f4ba1e1f96d1fcb812123a26d95737c9d54802b",
+ OPENAI_API_KEY: process.env.REACT_APP_OPENAI_API_KEY ?? "",
};
export default CONSTS;
diff --git a/src/utils/routing/rootRouter.tsx b/src/utils/routing/rootRouter.tsx
index e98937b3..bbdf88fe 100644
--- a/src/utils/routing/rootRouter.tsx
+++ b/src/utils/routing/rootRouter.tsx
@@ -206,6 +206,15 @@ const TermsAndConditionsPage = Loadable(
)
);
+const ChatbotPage = Loadable(
+ React.lazy(
+ () =>
+ import(
+ /* webpackChunkName: "terms_conditions_page" */ "../../features/Dashboard/Chatbot/Chatbot"
+ )
+ )
+);
+
const createRoutes = (queryClient: ApolloClient