diff --git a/hooks/useApiKey.ts b/hooks/useApiKey.ts
new file mode 100644
index 0000000..4623855
--- /dev/null
+++ b/hooks/useApiKey.ts
@@ -0,0 +1,20 @@
+import { useState, useEffect } from "react";
+
+export function useApiKey() {
+ const [apiKey, setApiKey] = useState("");
+
+ useEffect(() => {
+ // Load API key from localStorage on component mount
+ const storedApiKey = localStorage.getItem("openaiApiKey");
+ if (storedApiKey) {
+ setApiKey(storedApiKey);
+ }
+ }, []);
+
+ const updateApiKey = (newApiKey: string) => {
+ setApiKey(newApiKey);
+ localStorage.setItem("openaiApiKey", newApiKey);
+ };
+
+ return [apiKey, updateApiKey] as const;
+}
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index e75b202..d0b0009 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14,6 +14,7 @@
"clsx": "^2.1.1",
"lucide-react": "^0.447.0",
"next": "14.2.14",
+ "openai": "^4.67.3",
"react": "^18",
"react-dom": "^18",
"tailwind-merge": "^2.5.3",
@@ -493,12 +494,21 @@
"version": "20.16.10",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.10.tgz",
"integrity": "sha512-vQUKgWTjEIRFCvK6CyriPH3MZYiYlNy0fKiEYHWbcoWLEgs4opurGGKlebrTLqdSMIbXImH6XExNiIyNUv3WpA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.19.2"
}
},
+ "node_modules/@types/node-fetch": {
+ "version": "2.6.11",
+ "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz",
+ "integrity": "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "form-data": "^4.0.0"
+ }
+ },
"node_modules/@types/prop-types": {
"version": "15.7.13",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz",
@@ -763,6 +773,18 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/abort-controller": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
+ "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
+ "license": "MIT",
+ "dependencies": {
+ "event-target-shim": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=6.5"
+ }
+ },
"node_modules/acorn": {
"version": "8.12.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
@@ -786,6 +808,18 @@
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
+ "node_modules/agentkeepalive": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz",
+ "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==",
+ "license": "MIT",
+ "dependencies": {
+ "humanize-ms": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 8.0.0"
+ }
+ },
"node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@@ -1034,6 +1068,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "license": "MIT"
+ },
"node_modules/available-typed-arrays": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
@@ -1276,6 +1316,18 @@
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT"
},
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/commander": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
@@ -1480,6 +1532,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
"node_modules/didyoumean": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
@@ -2180,6 +2241,15 @@
"node": ">=0.10.0"
}
},
+ "node_modules/event-target-shim": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
+ "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -2316,6 +2386,39 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/form-data": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
+ "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/form-data-encoder": {
+ "version": "1.7.2",
+ "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz",
+ "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==",
+ "license": "MIT"
+ },
+ "node_modules/formdata-node": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz",
+ "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==",
+ "license": "MIT",
+ "dependencies": {
+ "node-domexception": "1.0.0",
+ "web-streams-polyfill": "4.0.0-beta.3"
+ },
+ "engines": {
+ "node": ">= 12.20"
+ }
+ },
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -2632,6 +2735,15 @@
"node": ">= 0.4"
}
},
+ "node_modules/humanize-ms": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
+ "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.0.0"
+ }
+ },
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -3389,6 +3501,27 @@
"node": ">=8.6"
}
},
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -3425,7 +3558,6 @@
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "dev": true,
"license": "MIT"
},
"node_modules/mz": {
@@ -3542,6 +3674,45 @@
"node": "^10 || ^12 || >=14"
}
},
+ "node_modules/node-domexception": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
+ "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/jimmywarting"
+ },
+ {
+ "type": "github",
+ "url": "https://paypal.me/jimmywarting"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.5.0"
+ }
+ },
+ "node_modules/node-fetch": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
+ "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-url": "^5.0.0"
+ },
+ "engines": {
+ "node": "4.x || >=6.0.0"
+ },
+ "peerDependencies": {
+ "encoding": "^0.1.0"
+ },
+ "peerDependenciesMeta": {
+ "encoding": {
+ "optional": true
+ }
+ }
+ },
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
@@ -3705,6 +3876,47 @@
"wrappy": "1"
}
},
+ "node_modules/openai": {
+ "version": "4.67.3",
+ "resolved": "https://registry.npmjs.org/openai/-/openai-4.67.3.tgz",
+ "integrity": "sha512-HT2tZgjLgRqbLQNKmYtjdF/4TQuiBvg1oGvTDhwpSEQzxo6/oM1us8VQ53vBK2BiKvCxFuq6gKGG70qfwrNhKg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/node": "^18.11.18",
+ "@types/node-fetch": "^2.6.4",
+ "abort-controller": "^3.0.0",
+ "agentkeepalive": "^4.2.1",
+ "form-data-encoder": "1.7.2",
+ "formdata-node": "^4.3.2",
+ "node-fetch": "^2.6.7"
+ },
+ "bin": {
+ "openai": "bin/cli"
+ },
+ "peerDependencies": {
+ "zod": "^3.23.8"
+ },
+ "peerDependenciesMeta": {
+ "zod": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/openai/node_modules/@types/node": {
+ "version": "18.19.55",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.55.tgz",
+ "integrity": "sha512-zzw5Vw52205Zr/nmErSEkN5FLqXPuKX/k5d1D7RKHATGqU7y6YfX9QxZraUzUrFGqH6XzOzG196BC35ltJC4Cw==",
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~5.26.4"
+ }
+ },
+ "node_modules/openai/node_modules/undici-types": {
+ "version": "5.26.5",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
+ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
+ "license": "MIT"
+ },
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -4912,6 +5124,12 @@
"node": ">=8.0"
}
},
+ "node_modules/tr46": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
+ "license": "MIT"
+ },
"node_modules/ts-api-utils": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz",
@@ -5087,7 +5305,6 @@
"version": "6.19.8",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
- "dev": true,
"license": "MIT"
},
"node_modules/uri-js": {
@@ -5106,6 +5323,31 @@
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
+ "node_modules/web-streams-polyfill": {
+ "version": "4.0.0-beta.3",
+ "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz",
+ "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/webidl-conversions": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/whatwg-url": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+ "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "~0.0.3",
+ "webidl-conversions": "^3.0.0"
+ }
+ },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
diff --git a/package.json b/package.json
index b0e3792..26dacd8 100644
--- a/package.json
+++ b/package.json
@@ -19,6 +19,7 @@
"clsx": "^2.1.1",
"lucide-react": "^0.447.0",
"next": "14.2.14",
+ "openai": "^4.67.3",
"react": "^18",
"react-dom": "^18",
"tailwind-merge": "^2.5.3",
diff --git a/yarn.lock b/yarn.lock
index 91f2fd4..e370121 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -114,10 +114,10 @@
dependencies:
glob "10.3.10"
-"@next/swc-win32-x64-msvc@14.2.14":
+"@next/swc-darwin-arm64@14.2.14":
version "14.2.14"
- resolved "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.14.tgz"
- integrity sha512-MZom+OvZ1NZxuRovKt1ApevjiUJTcU2PmdJKL66xUPaJeRywnbGGRWUlaAOwunD6dX+pm83vj979NTC8QXjGWg==
+ resolved "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.14.tgz"
+ integrity sha512-bsxbSAUodM1cjYeA4o6y7sp9wslvwjSkWw57t8DtC8Zig8aG8V6r+Yc05/9mDzLKcybb6EN85k1rJDnMKBd9Gw==
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
@@ -195,13 +195,28 @@
resolved "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz"
integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==
-"@types/node@^20":
+"@types/node-fetch@^2.6.4":
+ version "2.6.11"
+ resolved "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz"
+ integrity sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==
+ dependencies:
+ "@types/node" "*"
+ form-data "^4.0.0"
+
+"@types/node@*", "@types/node@^20":
version "20.16.10"
resolved "https://registry.npmjs.org/@types/node/-/node-20.16.10.tgz"
integrity sha512-vQUKgWTjEIRFCvK6CyriPH3MZYiYlNy0fKiEYHWbcoWLEgs4opurGGKlebrTLqdSMIbXImH6XExNiIyNUv3WpA==
dependencies:
undici-types "~6.19.2"
+"@types/node@^18.11.18":
+ version "18.19.55"
+ resolved "https://registry.npmjs.org/@types/node/-/node-18.19.55.tgz"
+ integrity sha512-zzw5Vw52205Zr/nmErSEkN5FLqXPuKX/k5d1D7RKHATGqU7y6YfX9QxZraUzUrFGqH6XzOzG196BC35ltJC4Cw==
+ dependencies:
+ undici-types "~5.26.4"
+
"@types/prop-types@*":
version "15.7.13"
resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz"
@@ -308,6 +323,13 @@
resolved "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz"
integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==
+abort-controller@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz"
+ integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==
+ dependencies:
+ event-target-shim "^5.0.0"
+
acorn-jsx@^5.3.2:
version "5.3.2"
resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz"
@@ -318,6 +340,13 @@ acorn-jsx@^5.3.2:
resolved "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz"
integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==
+agentkeepalive@^4.2.1:
+ version "4.5.0"
+ resolved "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz"
+ integrity sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==
+ dependencies:
+ humanize-ms "^1.2.1"
+
ajv@^6.12.4:
version "6.12.6"
resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz"
@@ -474,6 +503,11 @@ ast-types-flow@^0.0.8:
resolved "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz"
integrity sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==
+asynckit@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz"
+ integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
+
available-typed-arrays@^1.0.7:
version "1.0.7"
resolved "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz"
@@ -613,6 +647,13 @@ color-name@~1.1.4:
resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
+combined-stream@^1.0.8:
+ version "1.0.8"
+ resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz"
+ integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
+ dependencies:
+ delayed-stream "~1.0.0"
+
commander@^4.0.0:
version "4.1.1"
resolved "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz"
@@ -735,6 +776,11 @@ define-properties@^1.1.3, define-properties@^1.2.0, define-properties@^1.2.1:
has-property-descriptors "^1.0.0"
object-keys "^1.1.1"
+delayed-stream@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz"
+ integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
+
didyoumean@^1.2.2:
version "1.2.2"
resolved "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz"
@@ -1130,6 +1176,11 @@ esutils@^2.0.2:
resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz"
integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
+event-target-shim@^5.0.0:
+ version "5.0.1"
+ resolved "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz"
+ integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==
+
fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
version "3.1.3"
resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz"
@@ -1214,11 +1265,38 @@ foreground-child@^3.1.0:
cross-spawn "^7.0.0"
signal-exit "^4.0.1"
+form-data-encoder@1.7.2:
+ version "1.7.2"
+ resolved "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz"
+ integrity sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==
+
+form-data@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz"
+ integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==
+ dependencies:
+ asynckit "^0.4.0"
+ combined-stream "^1.0.8"
+ mime-types "^2.1.12"
+
+formdata-node@^4.3.2:
+ version "4.4.1"
+ resolved "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz"
+ integrity sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==
+ dependencies:
+ node-domexception "1.0.0"
+ web-streams-polyfill "4.0.0-beta.3"
+
fs.realpath@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz"
integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
+fsevents@~2.3.2:
+ version "2.3.3"
+ resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz"
+ integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
+
function-bind@^1.1.2:
version "1.1.2"
resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz"
@@ -1388,6 +1466,13 @@ hasown@^2.0.0, hasown@^2.0.1, hasown@^2.0.2:
dependencies:
function-bind "^1.1.2"
+humanize-ms@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz"
+ integrity sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==
+ dependencies:
+ ms "^2.0.0"
+
ignore@^5.2.0, ignore@^5.3.1:
version "5.3.2"
resolved "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz"
@@ -1797,6 +1882,18 @@ micromatch@^4.0.4, micromatch@^4.0.5:
braces "^3.0.3"
picomatch "^2.3.1"
+mime-db@1.52.0:
+ version "1.52.0"
+ resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz"
+ integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
+
+mime-types@^2.1.12:
+ version "2.1.35"
+ resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz"
+ integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
+ dependencies:
+ mime-db "1.52.0"
+
minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2:
version "3.1.2"
resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz"
@@ -1828,7 +1925,7 @@ minimist@^1.2.0, minimist@^1.2.6:
resolved "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz"
integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==
-ms@^2.1.1, ms@^2.1.3:
+ms@^2.0.0, ms@^2.1.1, ms@^2.1.3:
version "2.1.3"
resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
@@ -1875,6 +1972,18 @@ next@14.2.14:
"@next/swc-win32-ia32-msvc" "14.2.14"
"@next/swc-win32-x64-msvc" "14.2.14"
+node-domexception@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz"
+ integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==
+
+node-fetch@^2.6.7:
+ version "2.7.0"
+ resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz"
+ integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==
+ dependencies:
+ whatwg-url "^5.0.0"
+
normalize-path@^3.0.0, normalize-path@~3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz"
@@ -1962,6 +2071,19 @@ once@^1.3.0:
dependencies:
wrappy "1"
+openai@^4.67.3:
+ version "4.67.3"
+ resolved "https://registry.npmjs.org/openai/-/openai-4.67.3.tgz"
+ integrity sha512-HT2tZgjLgRqbLQNKmYtjdF/4TQuiBvg1oGvTDhwpSEQzxo6/oM1us8VQ53vBK2BiKvCxFuq6gKGG70qfwrNhKg==
+ dependencies:
+ "@types/node" "^18.11.18"
+ "@types/node-fetch" "^2.6.4"
+ abort-controller "^3.0.0"
+ agentkeepalive "^4.2.1"
+ form-data-encoder "1.7.2"
+ formdata-node "^4.3.2"
+ node-fetch "^2.6.7"
+
optionator@^0.9.3:
version "0.9.4"
resolved "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz"
@@ -2571,6 +2693,11 @@ to-regex-range@^5.0.1:
dependencies:
is-number "^7.0.0"
+tr46@~0.0.3:
+ version "0.0.3"
+ resolved "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz"
+ integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
+
ts-api-utils@^1.3.0:
version "1.3.0"
resolved "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz"
@@ -2667,6 +2794,11 @@ unbox-primitive@^1.0.2:
has-symbols "^1.0.3"
which-boxed-primitive "^1.0.2"
+undici-types@~5.26.4:
+ version "5.26.5"
+ resolved "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz"
+ integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==
+
undici-types@~6.19.2:
version "6.19.8"
resolved "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz"
@@ -2684,6 +2816,24 @@ util-deprecate@^1.0.2:
resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz"
integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
+web-streams-polyfill@4.0.0-beta.3:
+ version "4.0.0-beta.3"
+ resolved "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz"
+ integrity sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==
+
+webidl-conversions@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz"
+ integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==
+
+whatwg-url@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz"
+ integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==
+ dependencies:
+ tr46 "~0.0.3"
+ webidl-conversions "^3.0.0"
+
which-boxed-primitive@^1.0.2:
version "1.0.2"
resolved "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz"
From e079881fc653cc9705acc4327ff465e57f2ac4cc Mon Sep 17 00:00:00 2001
From: ilham
Date: Wed, 9 Oct 2024 21:54:19 +0700
Subject: [PATCH 3/3] complete risk score generator
---
.env.example | 1 +
app/api/generate-risk-score/route.ts | 237 +-
app/dashboard/page.tsx | 6 +-
app/layout.tsx | 5 +-
components/masked-api-key.tsx | 8 +-
components/risk-score-card.tsx | 60 +-
components/ui/input.tsx | 36 +-
hooks/ApiKeyContext.tsx | 30 +
hooks/useApiKey.ts | 22 +-
package-lock.json | 3159 +++++++++++++++++++++++++-
package.json | 5 +
yarn.lock | 1733 +++++++++++++-
12 files changed, 5070 insertions(+), 232 deletions(-)
create mode 100644 .env.example
create mode 100644 hooks/ApiKeyContext.tsx
diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..9847a1d
--- /dev/null
+++ b/.env.example
@@ -0,0 +1 @@
+OPENAI_API_KEY=
\ No newline at end of file
diff --git a/app/api/generate-risk-score/route.ts b/app/api/generate-risk-score/route.ts
index a33368e..19eddc5 100644
--- a/app/api/generate-risk-score/route.ts
+++ b/app/api/generate-risk-score/route.ts
@@ -1,76 +1,195 @@
import { NextResponse } from 'next/server';
-import OpenAI from 'openai';
+import { openai } from '@ai-sdk/openai'; // Assuming this is part of some SDK you're using
+import { generateText } from 'ai';
+import puppeteer, { Page } from 'puppeteer';
+
+interface TwitterProfile {
+ handle: string;
+ userDescription: string;
+ createdAt: string;
+ followers: number;
+ following: number;
+ verified: boolean;
+ locked: boolean;
+ website: string;
+ recentTweets: { text: string }[];
+}
+
export async function POST(request: Request) {
- const { twitterHandle, apiKey } = await request.json();
+ const { twitterHandle } = await request.json();
if (!twitterHandle) {
return NextResponse.json({ error: 'Twitter handle is required' }, { status: 400 });
}
- if (!apiKey) {
- return NextResponse.json({ error: 'OpenAI API key is required' }, { status: 400 });
- }
-
- // Initialize OpenAI client with the provided API key
- const openai = new OpenAI({
- apiKey: apiKey,
- });
-
try {
- // Fetch Twitter profile data (mock implementation)
+ // Fetch Twitter profile data
const twitterProfile = await fetchTwitterProfile(twitterHandle);
+ console.log('Twitter Profile:', JSON.stringify(twitterProfile, null, 2));
+
+ // Check if the account is locked
+ if (twitterProfile.locked) {
+ return NextResponse.json({ error: 'Unable to verify the account as the account is locked' }, { status: 403 });
+ }
// Generate analysis prompt
const prompt = generateAnalysisPrompt(twitterProfile);
+ console.log('Generated Analysis Prompt:', prompt);
// Call OpenAI API for analysis
- const completion = await openai.chat.completions.create({
- model: "gpt-3.5-turbo",
- messages: [
- { role: "system", content: "You are an AI assistant that analyzes Twitter profiles for Web3 project risk assessment." },
- { role: "user", content: prompt }
- ],
+ const { text } = await generateText({
+ model: openai('gpt-4-turbo'),
+ system: "You are an AI assistant that analyzes Twitter profiles for Web3 project risk assessment. Your output for the reasoning will always be in markdown.",
+ prompt: prompt,
});
- const analysis = completion.choices[0].message.content;
+ console.log('result by openAI:', text);
- // Parse the analysis to extract scores
- const scores = parseAnalysis(analysis);
+ // Parse the analysis to extract scores and overall risk score
+ const medianScore = getMedian(text);
- // Calculate overall risk score
- const overallScore = calculateOverallScore(scores);
-
- return NextResponse.json({ riskScore: overallScore, detailedScores: scores });
+ // Return the analysis result and overall risk score
+ return NextResponse.json({ result: text, riskScore: medianScore });
} catch (error) {
console.error('Error generating risk score:', error);
- return NextResponse.json({ error: 'Failed to generate risk score' }, { status: 500 });
+ return NextResponse.json({ error: error }, { status: 500 });
}
}
async function fetchTwitterProfile(handle: string) {
- // Mock implementation - replace with actual Twitter API call
- return {
- handle,
- createdAt: '2020-01-01',
- followers: 15000,
- verified: true,
- tweetCount: 1000,
- // Add more fields as needed
- };
+ try {
+ const browser = await puppeteer.launch({ headless: false });
+ const page = await browser.newPage();
+ await page.goto(`https://twitter.com/${handle}`, { waitUntil: 'networkidle0' });
+
+ // Scroll down to load more tweets
+ await autoScroll(page);
+
+
+ const profileData = await page.evaluate(() => {
+ const followingElement = document.querySelector('a[href$="/following"] span');
+ const userDescriptionElement = document.querySelector('div[data-testid="UserDescription"]');
+ const followersElement = document.querySelector('a[href$="/verified_followers"] span');
+ const joinDateElement = document.querySelector('span[data-testid="UserJoinDate"]');
+ const verifiedElement = document.querySelector('svg[aria-label="Verified account"]');
+ const lockedElement = document.querySelector('svg[aria-label="Protected account"]');
+ const websiteElement = document.querySelector('a[data-testid="UserUrl"] span');
+
+ // Updated tweet extraction
+ const tweetElements = document.querySelectorAll('div[data-testid="tweetText"]');
+
+ const tweets = Array.from(tweetElements)
+ .map(tweet => ({
+ text: tweet.textContent?.replace(/\n/g, ' ').trim() ?? ''
+ }))
+ .filter(tweet => tweet.text !== ''); // Filter out any empty tweets
+
+ console.log(tweets);
+
+
+
+ return {
+ userDescription: userDescriptionElement
+ ? Array.from(userDescriptionElement.childNodes)
+ .map(node => node.textContent?.trim() ?? '')
+ .filter(text => text !== '')
+ .join(' ')
+ : '',
+ following: followingElement?.textContent?.trim() ?? '0',
+ followers: followersElement?.textContent?.trim() ?? '0',
+ joinDate: joinDateElement?.textContent?.trim() ?? '',
+ verified: !!verifiedElement,
+ locked: !!lockedElement,
+ website: websiteElement?.textContent?.trim() ?? '',
+ recentTweets: tweets,
+ };
+ });
+
+ await browser.close();
+
+ const cleanedTweets = profileData.recentTweets.map(tweet => ({
+ text: cleanTweetText(tweet.text)
+ })).filter(tweet => tweet.text !== '');
+
+ return {
+ handle,
+ userDescription: profileData.userDescription,
+ createdAt: profileData.joinDate,
+ followers: parseTwitterNumber(profileData.followers),
+ following: parseTwitterNumber(profileData.following),
+ verified: profileData.verified,
+ locked: profileData.locked,
+ website: profileData.website,
+ recentTweets: cleanedTweets,
+ };
+ } catch (error) {
+ console.error('Error fetching Twitter profile:', error);
+ throw new Error('Failed to fetch Twitter profile');
+ }
}
-function generateAnalysisPrompt(profile: any) {
+async function autoScroll(page: Page) {
+ await page.evaluate(async () => {
+ await new Promise((resolve) => {
+ const startTime = Date.now(); // Track the start time
+ const duration = 15000; // Minimum 10 seconds (in ms)
+ let totalHeight = 0;
+ const distance = 1000; // Scroll 100px on each step
+
+ const timer = setInterval(() => {
+ const now = Date.now();
+ const scrollHeight = document.documentElement.scrollHeight;
+ window.scrollBy(0, distance);
+ totalHeight += distance;
+
+ // Stop scrolling if we've reached the end of the page or if 10 seconds have passed
+ if (
+ totalHeight >= scrollHeight - window.innerHeight || // Scroll till the bottom of the page
+ now - startTime >= duration // Ensure 10 seconds duration
+ ) {
+ clearInterval(timer);
+ resolve();
+ }
+ }, 100); // Scroll every 100 milliseconds for stability
+ });
+ });
+}
+
+
+function cleanTweetText(text: string): string {
+ let cleaned = text.replace(/^.*?ยท.*?(\n|$)/, '');
+ cleaned = cleaned.replace(/Show more$/, '');
+ return cleaned.trim();
+}
+
+function parseTwitterNumber(str: string): number {
+ const multipliers: { [key: string]: number } = { K: 1000, M: 1000000, B: 1000000000 };
+ const match = str.match(/^(\d+(?:\.\d+)?)\s*([KMB])?$/);
+ if (match) {
+ const [, num, unit] = match;
+ return Math.round(parseFloat(num) * (unit ? multipliers[unit] : 1));
+ }
+ return 0;
+}
+
+
+function generateAnalysisPrompt(profile: TwitterProfile) {
return `
Analyze the following Twitter profile for a Web3 project risk assessment:
Twitter Handle: ${profile.handle}
+User Description: ${profile.userDescription}
Account Created: ${profile.createdAt}
+Website: ${profile.website}
Followers: ${profile.followers}
+Following: ${profile.following}
Verified: ${profile.verified}
-Tweet Count: ${profile.tweetCount}
-Please provide a score from 1 to 10 for each of the following criteria, where 1 is the lowest (highest risk) and 10 is the highest (lowest risk):
+Recent Tweets:
+${profile.recentTweets.map((tweet, index) => `${index + 1}. ${tweet.text}`).join('\n')}
+
+Please provide a score from 1 to 10 for each of the following criteria, where 0 is the lowest (highest risk) and 10 is the highest (lowest risk):
1. Account Longevity: Established accounts (>6 months) indicate more stability
2. Follower Base: A substantial following (>10,000) suggests broader recognition
@@ -83,35 +202,39 @@ Please provide a score from 1 to 10 for each of the following criteria, where 1
9. Team Visibility: Clear information about team members builds trust
10. Industry Connections: Interactions with reputable projects suggest legitimacy
-Provide your scores in the following format:
-1. Account Longevity: [SCORE]
-2. Follower Base: [SCORE]
-...
-10. Industry Connections: [SCORE]
-
-Follow each score with a brief explanation of your reasoning.
+Provide your scores and reasoning for each criterion. Make sure your output is in markdown style.
`;
}
-function parseAnalysis(analysis: string | null): Record {
- if (!analysis) return {};
+function getMedian(analysis: string | null): number {
+ if (!analysis) return 0;
- const scores: Record = {};
+ const scores: number[] = [];
const lines = analysis.split('\n');
for (const line of lines) {
- const match = line.match(/^(\d+)\.\s+(.+):\s+(\d+)/);
- if (match) {
- const [, , category, score] = match;
- scores[category] = parseInt(score, 10);
+ if (line.includes('Score:')) {
+ const scoreMatch = line.match(/Score:\s*(\d+)/);
+ if (scoreMatch) {
+ const score = parseInt(scoreMatch[1], 10);
+ scores.push(score);
+ }
}
}
- return scores;
+ return calculateMedian(scores);
}
-function calculateOverallScore(scores: Record): number {
- const totalScore = Object.values(scores).reduce((sum, score) => sum + score, 0);
- const averageScore = totalScore / Object.keys(scores).length;
- return Math.round(averageScore * 10) / 10; // Round to one decimal place
+// Helper function to calculate the median
+function calculateMedian(numbers: number[]): number {
+ if (numbers.length === 0) return 0;
+
+ const sorted = [...numbers].sort((a, b) => a - b);
+ const middle = Math.floor(sorted.length / 2);
+
+ if (sorted.length % 2 === 0) {
+ return (sorted[middle - 1] + sorted[middle]) / 2;
+ }
+
+ return sorted[middle];
}
\ No newline at end of file
diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx
index b1af7eb..0cea000 100644
--- a/app/dashboard/page.tsx
+++ b/app/dashboard/page.tsx
@@ -1,7 +1,7 @@
'use client'
import { HeaderComponent } from "@/components/header";
-import MaskedAPIKey from "@/components/masked-api-key";
+// import MaskedAPIKey from "@/components/masked-api-key";
import RiskScoreCard from "@/components/risk-score-card";
export default function DashboardPage() {
@@ -10,8 +10,8 @@ export default function DashboardPage() {
<>
-
-
+ {/* */}
+
>
);
diff --git a/app/layout.tsx b/app/layout.tsx
index 9a292df..665b416 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -1,6 +1,7 @@
import type { Metadata } from "next";
import localFont from "next/font/local";
import "./globals.css";
+import { ApiKeyProvider } from "@/hooks/ApiKeyContext";
const geistSans = localFont({
src: "./fonts/GeistVF.woff",
@@ -28,7 +29,9 @@ export default function RootLayout({
- {children}
+
+ {children}
+