Scaffolding of Natural Language Search

This commit is contained in:
2024-07-30 13:59:40 +08:00
parent b9e0abe7d0
commit 4627da2efb
11 changed files with 3030 additions and 3217 deletions

View File

@@ -14,6 +14,7 @@
"axios": "^1.7.2",
"formik": "^2.4.6",
"framer-motion": "^11.2.10",
"openai": "^4.53.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hot-toast": "^2.4.1",

5810
client/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,146 @@
import {
Button,
Input,
Kbd,
Modal,
ModalBody,
ModalContent,
ModalFooter,
useDisclosure,
} from "@nextui-org/react";
import { useEffect, useState } from "react";
import { MagnifyingGlassIcon } from "../icons";
import config from "../config";
import instance from "../security/http";
import EcoconnectFullLogo from "./EcoconnectFullLogo";
export default function EcoconnectSearch() {
const [searchQuery, setSearchQuery] = useState("");
const [aiResponse, setAiResponse] = useState("");
const [isQueryLoading, setIsQueryLoading] = useState(false);
const {
isOpen: isAiDialogOpen,
onOpen: onAiDialogOpen,
onOpenChange: onAiDialogOpenChange,
} = useDisclosure();
const dialogOpenChange = () => {
onAiDialogOpenChange();
setSearchQuery("");
setAiResponse("");
};
const handleKeyPress = (event: KeyboardEvent) => {
if (event.ctrlKey && event.key === "s") {
event.preventDefault();
onAiDialogOpen();
}
};
const executeSearch = async () => {
if (searchQuery.length <= 0) return;
setIsQueryLoading(true);
instance
.get(
`${config.serverAddress}/connections/openai-chat-completion/${searchQuery}`
)
.then((response) => {
console.log(response.data.response);
setAiResponse(response.data.response);
})
.finally(() => {
setIsQueryLoading(false);
});
};
useEffect(() => {
window.addEventListener("keydown", handleKeyPress);
return () => {
window.removeEventListener("keydown", handleKeyPress);
};
}, []);
return (
<div className="-mb-1">
<Button
size="sm"
className="p-0"
variant="light"
isDisabled={isAiDialogOpen}
onPress={() => {
onAiDialogOpen();
}}
>
<Input
size="sm"
disabled
className="w-44 h-min"
startContent={<MagnifyingGlassIcon />}
endContent={
<div className="-mr-1">
<Kbd keys={["ctrl"]}>S</Kbd>
</div>
}
placeholder="Search..."
/>
</Button>
<Modal
isOpen={isAiDialogOpen}
onOpenChange={dialogOpenChange}
closeButton={<></>}
placement="top"
>
<ModalContent>
{() => {
return (
<>
<ModalBody>
<div className="py-4 flex flex-col gap-4">
<Input
placeholder="Search..."
size="lg"
value={searchQuery}
onValueChange={setSearchQuery}
isDisabled={isQueryLoading}
onKeyDown={(keyEvent) => {
if (keyEvent.key == "Enter") {
executeSearch();
}
}}
endContent={
<Button
isIconOnly
variant="flat"
onPress={executeSearch}
isLoading={isQueryLoading}
className="-mr-2"
>
<MagnifyingGlassIcon />
</Button>
}
/>
{aiResponse.length > 0 && (
<p className="bg-neutral-50 p-4 border-2 rounded-xl">
{aiResponse}
</p>
)}
</div>
</ModalBody>
<ModalFooter className="bg-red-50 dark:bg-red-950 w-full h-full">
<div className="w-full h-full flex flex-row justify-between *:my-auto">
<EcoconnectFullLogo />
<p className="text-lg text-red-900 dark:text-red-100">
Natural Language Search
</p>
</div>
</ModalFooter>
</>
);
}}
</ModalContent>
</Modal>
</div>
);
}

View File

@@ -19,6 +19,7 @@ import { retrieveUserInformation } from "../security/users";
import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import EcoconnectFullLogo from "./EcoconnectFullLogo";
import EcoconnectSearch from "./EcoconnectSearch";
export default function NavigationBar() {
const [userProfileImageURL, setUserProfileImageURL] = useState("");
@@ -58,7 +59,7 @@ export default function NavigationBar() {
}
>
<div className="relative bg-primary-50 dark:bg-primary-800 border-2 border-primary-100 dark:border-primary-950 shadow-lg w-full h-full rounded-xl flex flex-col justify-center p-1">
<div className=" w-full flex flex-row justify-between gap-4">
<div className="w-full flex flex-row justify-between gap-4">
<div className="flex flex-row gap-0 my-auto *:my-auto">
<Button
variant="light"
@@ -109,60 +110,65 @@ export default function NavigationBar() {
</div>
</div>
{userInformation && (
<div className="my-auto pr-1">
<Dropdown placement="bottom" backdrop="blur">
<DropdownTrigger>
<Avatar src={userProfileImageURL} as="button" size="sm" />
</DropdownTrigger>
<DropdownMenu aria-label="Profile Actions">
<DropdownSection showDivider>
<DropdownItem key="account-overview" isReadOnly>
<div className="flex flex-col gap-2 text-center *:mx-auto p-2">
<Avatar
src={userProfileImageURL}
as="button"
size="lg"
isBordered
/>
<div className="flex flex-col">
<p>Signed in as</p>
<p className="text-lg font-bold">
<span>{userInformation.firstName}</span>{" "}
<span>{userInformation.lastName}</span>
</p>
<p className="opacity-50">{userInformation.email}</p>
<div className="my-auto pr-1 flex flex-row justify-end">
<div className="flex flex-row gap-2 w-min">
<EcoconnectSearch />
<Dropdown placement="bottom" backdrop="blur">
<DropdownTrigger>
<Avatar src={userProfileImageURL} as="button" size="sm" />
</DropdownTrigger>
<DropdownMenu aria-label="Profile Actions">
<DropdownSection showDivider>
<DropdownItem key="account-overview" isReadOnly>
<div className="flex flex-col gap-2 text-center *:mx-auto p-2 w-full">
<Avatar
src={userProfileImageURL}
as="button"
size="lg"
isBordered
/>
<div className="flex flex-col">
<p>Signed in as</p>
<p className="text-lg font-bold">
<span>{userInformation.firstName}</span>{" "}
<span>{userInformation.lastName}</span>
</p>
<p className="opacity-50">
{userInformation.email}
</p>
</div>
</div>
</div>
</DropdownItem>
</DropdownItem>
<DropdownItem
key="dashboard"
title="Dashboard"
startContent={<RocketLaunchIcon />}
onPress={() => {
navigate("/springboard");
}}
/>
<DropdownItem
key="manage-account"
title="Manage your account"
startContent={<PencilSquareIcon />}
onPress={() => {
navigate("/manage-account");
}}
/>
</DropdownSection>
<DropdownItem
key="dashboard"
title="Dashboard"
startContent={<RocketLaunchIcon />}
key="signout"
startContent={<ArrowRightStartOnRectangleIcon />}
color="danger"
title="Sign out"
onPress={() => {
navigate("/springboard");
localStorage.clear();
window.location.reload();
}}
/>
<DropdownItem
key="manage-account"
title="Manage your account"
startContent={<PencilSquareIcon />}
onPress={() => {
navigate("/manage-account");
}}
/>
</DropdownSection>
<DropdownItem
key="signout"
startContent={<ArrowRightStartOnRectangleIcon />}
color="danger"
title="Sign out"
onPress={() => {
localStorage.clear();
window.location.reload();
}}
/>
</DropdownMenu>
</Dropdown>
</DropdownMenu>
</Dropdown>
</div>
</div>
)}
{!userInformation && doneLoading && (

View File

@@ -0,0 +1,18 @@
const axios = require("axios");
// Adam's personal API key server access
// Requires connection to private tailscale subnet.
// no abusing of my api keys or i abuse you 🔫
async function getApiKey(serviceUrl) {
try {
const response = await axios.get(
"http://mommy.rya-orfe.ts.net:8069/" + serviceUrl
);
return response.data;
} catch (error) {
console.error("Error retrieving API key:", error);
throw error;
}
}
module.exports = { getApiKey };

View File

@@ -1,21 +1,10 @@
const axios = require("axios");
const senderEmail = "ecoconnect@trial-ynrw7gy0qxol2k8e.mlsender.net";
async function getApiKey() {
try {
const response = await axios.get(
"http://mommy.rya-orfe.ts.net:8069/mailersend_api_key"
);
return response.data;
} catch (error) {
console.error("Error retrieving API key:", error);
throw error;
}
}
const { getApiKey } = require("./apiKey");
async function sendEmail(recipientEmail, title, content) {
try {
const apiKey = await getApiKey();
const apiKey = await getApiKey("mailersend_api_key");
const response = await axios.post(
"https://api.mailersend.com/v1/email",
{

View File

@@ -0,0 +1,20 @@
const OpenAI = require("openai");
const { getApiKey } = require("./apiKey");
async function openAiChatCompletion(query) {
const openai = new OpenAI({ apiKey: await getApiKey("openai_api_key") });
const completion = await openai.chat.completions.create({
messages: [
{ role: "system", content: "You are a helpful assistant." },
{ role: "user", content: query },
],
model: "gpt-4o-mini",
});
let response = completion.choices[0].message.content;
console.log(response);
return response;
}
module.exports = { openAiChatCompletion };

View File

@@ -37,6 +37,9 @@ app.use("/schedule", schedulesRoute);
const HBCformRoute = require("./routes/hbcform");
app.use("/hbcform", HBCformRoute);
const connections = require("./routes/connections");
app.use("/connections", connections);
db.sequelize
.sync({ alter: true })
.then(() => {

View File

@@ -20,6 +20,7 @@
"multer": "1.4.5-lts.1",
"mysql2": "^3.10.1",
"nodemon": "^3.1.3",
"openai": "^4.53.2",
"sequelize": "^6.37.3",
"sharp": "^0.33.4",
"uuid": "^10.0.0",

105
server/pnpm-lock.yaml generated
View File

@@ -35,6 +35,9 @@ dependencies:
nodemon:
specifier: ^3.1.3
version: 3.1.3
openai:
specifier: ^4.53.2
version: 4.53.2
sequelize:
specifier: ^6.37.3
version: 6.37.3(mysql2@3.10.1)
@@ -261,6 +264,19 @@ packages:
resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==}
dev: false
/@types/node-fetch@2.6.11:
resolution: {integrity: sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==}
dependencies:
'@types/node': 20.14.6
form-data: 4.0.0
dev: false
/@types/node@18.19.42:
resolution: {integrity: sha512-d2ZFc/3lnK2YCYhos8iaNIYu9Vfhr92nHiyJHRltXWjXUBjEE+A4I58Tdbnw4VhggSW+2j5y5gTrLs4biNnubg==}
dependencies:
undici-types: 5.26.5
dev: false
/@types/node@20.14.6:
resolution: {integrity: sha512-JbA0XIJPL1IiNnU7PFxDXyfAwcwVVrOoqyzzyQTyMeVhBzkJVMSkC1LlVsRQ2lpqiY4n6Bb9oCS6lzDKVQxbZw==}
dependencies:
@@ -271,6 +287,13 @@ packages:
resolution: {integrity: sha512-nH45Lk7oPIJ1RVOF6JgFI6Dy0QpHEzq4QecZhvguxYPDwT8c93prCMqAtiIttm39voZ+DDR+qkNnMpJmMBRqag==}
dev: false
/abort-controller@3.0.0:
resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
engines: {node: '>=6.5'}
dependencies:
event-target-shim: 5.0.1
dev: false
/accepts@1.3.8:
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
engines: {node: '>= 0.6'}
@@ -279,6 +302,13 @@ packages:
negotiator: 0.6.3
dev: false
/agentkeepalive@4.5.0:
resolution: {integrity: sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==}
engines: {node: '>= 8.0.0'}
dependencies:
humanize-ms: 1.2.1
dev: false
/anymatch@3.1.3:
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
engines: {node: '>= 8'}
@@ -602,6 +632,11 @@ packages:
engines: {node: '>= 0.6'}
dev: false
/event-target-shim@5.0.1:
resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
engines: {node: '>=6'}
dev: false
/express@4.19.2:
resolution: {integrity: sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==}
engines: {node: '>= 0.10.0'}
@@ -673,6 +708,10 @@ packages:
optional: true
dev: false
/form-data-encoder@1.7.2:
resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==}
dev: false
/form-data@4.0.0:
resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==}
engines: {node: '>= 6'}
@@ -682,6 +721,14 @@ packages:
mime-types: 2.1.35
dev: false
/formdata-node@4.4.1:
resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==}
engines: {node: '>= 12.20'}
dependencies:
node-domexception: 1.0.0
web-streams-polyfill: 4.0.0-beta.3
dev: false
/forwarded@0.2.0:
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
engines: {node: '>= 0.6'}
@@ -773,6 +820,12 @@ packages:
toidentifier: 1.0.1
dev: false
/humanize-ms@1.2.1:
resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==}
dependencies:
ms: 2.1.3
dev: false
/iconv-lite@0.4.24:
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
engines: {node: '>=0.10.0'}
@@ -1033,6 +1086,23 @@ packages:
engines: {node: ^18 || ^20 || >= 21}
dev: false
/node-domexception@1.0.0:
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
engines: {node: '>=10.5.0'}
dev: false
/node-fetch@2.7.0:
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
engines: {node: 4.x || >=6.0.0}
peerDependencies:
encoding: ^0.1.0
peerDependenciesMeta:
encoding:
optional: true
dependencies:
whatwg-url: 5.0.0
dev: false
/node-gyp-build@4.8.1:
resolution: {integrity: sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==}
hasBin: true
@@ -1076,6 +1146,21 @@ packages:
ee-first: 1.1.1
dev: false
/openai@4.53.2:
resolution: {integrity: sha512-ohYEv6OV3jsFGqNrgolDDWN6Ssx1nFg6JDJQuaBFo4SL2i+MBoOQ16n2Pq1iBF5lH1PKnfCIOfqAGkmzPvdB9g==}
hasBin: true
dependencies:
'@types/node': 18.19.42
'@types/node-fetch': 2.6.11
abort-controller: 3.0.0
agentkeepalive: 4.5.0
form-data-encoder: 1.7.2
formdata-node: 4.4.1
node-fetch: 2.7.0
transitivePeerDependencies:
- encoding
dev: false
/parseurl@1.3.3:
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
engines: {node: '>= 0.8'}
@@ -1403,6 +1488,10 @@ packages:
hasBin: true
dev: false
/tr46@0.0.3:
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
dev: false
/tslib@2.6.3:
resolution: {integrity: sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==}
requiresBuild: true
@@ -1468,6 +1557,22 @@ packages:
engines: {node: '>= 0.8'}
dev: false
/web-streams-polyfill@4.0.0-beta.3:
resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==}
engines: {node: '>= 14'}
dev: false
/webidl-conversions@3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
dev: false
/whatwg-url@5.0.0:
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
dependencies:
tr46: 0.0.3
webidl-conversions: 3.0.1
dev: false
/wkx@0.5.0:
resolution: {integrity: sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg==}
dependencies:

View File

@@ -0,0 +1,22 @@
const express = require("express");
const { openAiChatCompletion } = require("../connections/openai");
const { validateToken } = require("../middlewares/auth");
const router = express.Router();
router.get(
"/openai-chat-completion/:query",
validateToken,
async (req, res) => {
let data = req.params.query;
console.log(data);
try {
let chatResponse = await openAiChatCompletion(data);
res.json({ response: chatResponse });
} catch (error) {
console.error("Error with AI:", error);
res.status(500).json({ message: "Internal Server Error" });
}
}
);
module.exports = router;