feat: add a process analysis blocker and a progress bar

This commit is contained in:
Mahen 2026-03-20 16:59:30 +07:00
parent a5c3737524
commit fee2c94f78
8 changed files with 474 additions and 43 deletions

275
package-lock.json generated
View File

@ -33,6 +33,8 @@
"react-hook-form": "^7.71.1",
"react-icons": "^5.5.0",
"recharts": "^3.7.0",
"socket.io": "^4.8.3",
"socket.io-client": "^4.8.3",
"tailwind-merge": "^3.4.0",
"tailwindcss-animate": "^1.0.7",
"ts-node": "^10.9.2",
@ -5275,6 +5277,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/@socket.io/component-emitter": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
"license": "MIT"
},
"node_modules/@standard-schema/spec": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
@ -5608,6 +5616,15 @@
"tslib": "^2.4.0"
}
},
"node_modules/@types/cors": {
"version": "2.8.19",
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
"integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/d3-array": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
@ -5754,6 +5771,15 @@
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
"license": "MIT"
},
"node_modules/@types/ws": {
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/yauzl": {
"version": "2.10.3",
"resolved": "https://registry.npmmirror.com/@types/yauzl/-/yauzl-2.10.3.tgz",
@ -6302,6 +6328,19 @@
"win32"
]
},
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
"license": "MIT",
"dependencies": {
"mime-types": "~2.1.34",
"negotiator": "0.6.3"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/acorn": {
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
@ -6784,6 +6823,15 @@
"bare-path": "^3.0.0"
}
},
"node_modules/base64id": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
"integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
"license": "MIT",
"engines": {
"node": "^4.5.0 || >= 5.9"
}
},
"node_modules/baseline-browser-mapping": {
"version": "2.9.19",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz",
@ -7205,6 +7253,23 @@
"node": ">= 0.6"
}
},
"node_modules/cors": {
"version": "2.8.6",
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
"integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
"license": "MIT",
"dependencies": {
"object-assign": "^4",
"vary": "^1"
},
"engines": {
"node": ">= 0.10"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/cosmiconfig": {
"version": "9.0.0",
"resolved": "https://registry.npmmirror.com/cosmiconfig/-/cosmiconfig-9.0.0.tgz",
@ -7700,6 +7765,91 @@
"once": "^1.4.0"
}
},
"node_modules/engine.io": {
"version": "6.6.6",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.6.tgz",
"integrity": "sha512-U2SN0w3OpjFRVlrc17E6TMDmH58Xl9rai1MblNjAdwWp07Kk+llmzX0hjDpQdrDGzwmvOtgM5yI+meYX6iZ2xA==",
"license": "MIT",
"dependencies": {
"@types/cors": "^2.8.12",
"@types/node": ">=10.0.0",
"@types/ws": "^8.5.12",
"accepts": "~1.3.4",
"base64id": "2.0.0",
"cookie": "~0.7.2",
"cors": "~2.8.5",
"debug": "~4.4.1",
"engine.io-parser": "~5.2.1",
"ws": "~8.18.3"
},
"engines": {
"node": ">=10.2.0"
}
},
"node_modules/engine.io-client": {
"version": "6.6.4",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.4.tgz",
"integrity": "sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw==",
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.4.1",
"engine.io-parser": "~5.2.1",
"ws": "~8.18.3",
"xmlhttprequest-ssl": "~2.1.1"
}
},
"node_modules/engine.io-client/node_modules/ws": {
"version": "8.18.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/engine.io-parser": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/engine.io/node_modules/ws": {
"version": "8.18.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/enhanced-resolve": {
"version": "5.19.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz",
@ -10456,6 +10606,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",
@ -10602,6 +10773,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/netmask": {
"version": "2.0.2",
"resolved": "https://registry.npmmirror.com/netmask/-/netmask-2.0.2.tgz",
@ -10773,7 +10953,6 @@
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@ -12656,6 +12835,83 @@
"npm": ">= 3.0.0"
}
},
"node_modules/socket.io": {
"version": "4.8.3",
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.3.tgz",
"integrity": "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==",
"license": "MIT",
"dependencies": {
"accepts": "~1.3.4",
"base64id": "~2.0.0",
"cors": "~2.8.5",
"debug": "~4.4.1",
"engine.io": "~6.6.0",
"socket.io-adapter": "~2.5.2",
"socket.io-parser": "~4.2.4"
},
"engines": {
"node": ">=10.2.0"
}
},
"node_modules/socket.io-adapter": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.6.tgz",
"integrity": "sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ==",
"license": "MIT",
"dependencies": {
"debug": "~4.4.1",
"ws": "~8.18.3"
}
},
"node_modules/socket.io-adapter/node_modules/ws": {
"version": "8.18.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/socket.io-client": {
"version": "4.8.3",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz",
"integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==",
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.4.1",
"engine.io-client": "~6.6.1",
"socket.io-parser": "~4.2.4"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/socket.io-parser": {
"version": "4.2.6",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.6.tgz",
"integrity": "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==",
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.4.1"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/socks": {
"version": "2.8.7",
"resolved": "https://registry.npmmirror.com/socks/-/socks-2.8.7.tgz",
@ -13589,6 +13845,15 @@
}
}
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/victory-vendor": {
"version": "37.3.6",
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz",
@ -13815,6 +14080,14 @@
"node": ">=0.8"
}
},
"node_modules/xmlhttprequest-ssl": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
"integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",

View File

@ -35,6 +35,8 @@
"react-hook-form": "^7.71.1",
"react-icons": "^5.5.0",
"recharts": "^3.7.0",
"socket.io": "^4.8.3",
"socket.io-client": "^4.8.3",
"tailwind-merge": "^3.4.0",
"tailwindcss-animate": "^1.0.7",
"ts-node": "^10.9.2",

View File

@ -0,0 +1,40 @@
import { Server } from "socket.io";
import type { NextApiRequest, NextApiResponse } from "next";
export default function SocketHandler(req: NextApiRequest, res: any) {
if (res.socket.server.io) {
console.log("Socket sudah berjalan");
} else {
console.log("Socket baru diinisialisasi");
const io = new Server(res.socket.server);
res.socket.server.io = io;
io.on("connection", (socket) => {
console.log("User terhubung:", socket.id);
socket.on("start-analysis", (data) => {
setTimeout(
() =>
socket.emit("progress", {
status: "Scraping data...",
percent: 30,
}),
1000,
);
setTimeout(
() =>
socket.emit("progress", {
status: "Menganalisis dengan XGBoost...",
percent: 70,
}),
3000,
);
setTimeout(
() => socket.emit("analysis-finished", { result: "Selesai" }),
5000,
);
});
});
}
res.end();
}

View File

@ -1,7 +1,7 @@
"use client";
import { useAnalyseText } from "@/src/hooks/useAnalyzeText";
import { Sparkles } from "lucide-react";
import { Sparkles, X } from "lucide-react";
import { Input } from "../ui/input";
import { Button } from "../ui/button";
import ResultSection from "./ResultSection";
@ -14,10 +14,12 @@ export default function AnalysisClient() {
result,
showField,
resultRef,
progress,
register,
handleSubmit,
onSubmit,
setShowField,
handleCancel
} = useAnalyseText();
return (
@ -166,14 +168,42 @@ export default function AnalysisClient() {
</div>
</div>
<Button
type="submit"
disabled={!isValid || loading}
className="w-full md:w-max bg-primary text-white px-6 py-3 mt-6 rounded-md transition-colors disabled:bg-gray-400"
>
<Sparkles className="h-4 w-4" />
{loading ? "Menganalisis..." : "Analisis Sekarang"}
</Button>
{loading && (
<div className="mt-8 p-4 border rounded-lg bg-transparent">
<div className="flex justify-between mb-2">
<span className="text-sm font-medium">{progress.status}</span>
<span className="text-sm font-medium">{progress.percent}%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2.5">
<div
className="bg-blue-600 h-2.5 rounded-full transition-all duration-500"
style={{ width: `${progress.percent}%` }}
></div>
</div>
</div>
)}
<div className="flex gap-2">
{loading && (
<Button
type="button"
variant="destructive"
onClick={handleCancel}
className="w-full bg-sentiment-negative text-white md:w-max px-6 py-3 mt-6 rounded-md transition-colors disabled:bg-gray-400"
>
<X className="h-4 w-4" />
<span>Cancel</span>
</Button>
)}
<Button
type="submit"
disabled={!isValid || loading}
className="w-full md:w-max bg-primary text-white px-6 py-3 mt-6 rounded-md transition-colors disabled:bg-gray-400"
>
<Sparkles className="h-4 w-4" />
{loading ? "Menganalisis..." : "Analisis Sekarang"}
</Button>
</div>
</form>
<div ref={resultRef} id="result-section" className="scroll-mt-28">

View File

@ -1,9 +1,9 @@
import { useEffect, useRef, useState } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import { useSession } from "next-auth/react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { AnalysisResults } from "../types";
import { AnalysisResults, AnalyzeFormData } from "../types";
import {
scrapeProduct,
getAIRecommendation,
@ -13,21 +13,14 @@ import { getAnotherUserData } from "../app/profile/lib/action";
import prisma from "@/lib/prisma";
import { getMetricId } from "../services/metric.service";
export type AnalyzeFormData = z.infer<typeof analyzeSchema>;
export interface AnalysisWithMetric {
metric: {
metricId: number;
name: string;
} | null;
}
export const useAnalyseText = () => {
const { data: session } = useSession();
const [loading, setLoading] = useState(false);
const [result, setResult] = useState<AnalysisResults | null>(null);
const [showField, setShowField] = useState(false);
const resultRef = useRef<HTMLDivElement>(null);
const [progress, setProgress] = useState({ status: "", percent: 0 });
const abortControllerRef = useRef<AbortController | null>(null);
const {
control,
@ -107,6 +100,15 @@ export const useAnalyseText = () => {
// }
// };
const handleCancel = useCallback(() => {
if (abortControllerRef.current) {
abortControllerRef.current.abort("User cancelled the analysis");
abortControllerRef.current = null;
}
setLoading(false);
setProgress({ status: "Analisis Dibatalkan", percent: 0 });
}, []);
const onSubmit = async (data: AnalyzeFormData) => {
if (!session?.user?.email) {
alert("Anda harus login terlebih dahulu.");
@ -114,24 +116,39 @@ export const useAnalyseText = () => {
}
setLoading(true);
setProgress({ status: "Memulai scraping...", percent: 10 });
setResult(null);
abortControllerRef.current = new AbortController();
const signal = abortControllerRef.current.signal;
try {
const urlsToScrape = [data.url1, data.url2, data.url3, data.url4].filter(
(url) => url && url.trim() !== "",
) as string[];
const scrapePromises = urlsToScrape.map((url) => scrapeProduct(url));
const scrapeResults = await Promise.all(scrapePromises);
const scrapeResults = await Promise.all(
urlsToScrape.map(async (url) => {
return await scrapeProduct(url, {
signal: signal,
});
}),
);
const candidates = scrapeResults.map((res) => ({
name: res.data.name,
url: res.data.url,
reviews: res.data.reviews,
}));
const candidates = scrapeResults
.filter((res) => res && res.success)
.map((res) => ({
name: res.data.name,
url: res.data.url,
reviews: res.data.reviews,
}));
if (candidates.length === 0) {
throw new Error("Tidak ada data produk yang berhasil diambil.");
}
const metricIdValue = await getMetricId();
console.log("Payload to AI:", {
user_email: session.user.email,
metric_id: metricIdValue,
@ -139,13 +156,23 @@ export const useAnalyseText = () => {
totalReviews: candidates.reduce((acc, c) => acc + c.reviews.length, 0),
});
const aiResult = await getAIRecommendation({
user_email: session.user.email as string,
candidates: candidates,
metric_id: metricIdValue,
});
setProgress({ status: "AI sedang menganalisis ulasan...", percent: 70 });
const aiResult = await getAIRecommendation(
{
user_email: session.user.email as string,
candidates: candidates,
metric_id: metricIdValue,
},
{ signal: abortControllerRef.current?.signal },
);
if (!aiResult) {
console.log("Server menghentikan proses karena pembatalan.");
return;
}
setResult(aiResult);
setProgress({ status: "Selesai", percent: 100 });
setTimeout(() => {
document
@ -153,12 +180,18 @@ export const useAnalyseText = () => {
?.scrollIntoView({ behavior: "smooth" });
}, 100);
} catch (error: any) {
if (error.name === "AbortError" || signal.aborted) {
console.log("🛠️ Request dibatalkan secara aman.");
return; // Keluar dari fungsi tanpa memunculkan alert error
}
console.error("Analysis Error:", error);
alert(
"Terjadi kesalahan: " + (error.message || "Gagal menganalisis ulasan."),
);
} finally {
setLoading(false);
abortControllerRef.current = null;
}
};
@ -183,10 +216,12 @@ export const useAnalyseText = () => {
result,
showField,
resultRef,
progress,
register,
handleSubmit,
setValue,
onSubmit,
setShowField,
handleCancel,
};
};

33
src/hooks/useSocket.ts Normal file
View File

@ -0,0 +1,33 @@
// src/hooks/useSocket.ts
import { useEffect, useState } from "react";
import { io, Socket } from "socket.io-client";
export const useSocket = () => {
const [socket, setSocket] = useState<Socket | null>(null);
const [progress, setProgress] = useState({ status: "", percent: 0 });
useEffect(() => {
const socketInitializer = async () => {
await fetch("/api/socket"); // Panggil API untuk menyalakan server socket
const newSocket = io();
newSocket.on("progress", (data) => {
setProgress(data);
});
setSocket(newSocket);
};
socketInitializer();
return () => {
if (socket) socket.disconnect();
};
}, []);
const startAnalysis = (payload: any) => {
if (socket) socket.emit("start-analysis", payload);
};
return { progress, startAnalysis };
};

View File

@ -1,13 +1,17 @@
import prisma from "@/lib/prisma";
import { AIRecommendationResponse } from "../types";
export const scrapeProduct = async (url: string) => {
export const scrapeProduct = async (
url: string,
options?: { signal?: AbortSignal },
) => {
const res = await fetch("/api/scrape", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ url }),
signal: options?.signal,
});
if (!res.ok) throw new Error(`Gagal scraping: ${url}`);
@ -33,9 +37,9 @@ export const getAnalysisData = async (email: string) => {
select: {
productId: true,
brand: {
select:{
name: true
}
select: {
name: true,
},
},
_count: {
select: {
@ -57,16 +61,20 @@ export const getAnalysisData = async (email: string) => {
return userAnalyses;
};
export const getAIRecommendation = async (payload: {
user_email: string;
metric_id: number | 1;
candidates: { name: string; url: string; reviews: string[] }[];
}): Promise<AIRecommendationResponse> => {
export const getAIRecommendation = async (
payload: {
user_email: string;
metric_id: number | 1;
candidates: { name: string; url: string; reviews: string[] }[];
},
options?: { signal?: AbortSignal },
): Promise<AIRecommendationResponse> => {
console.log("Fetching to FastAPI...");
const aiRes = await fetch("http://localhost:8000/recommend", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
signal: options?.signal,
});
if (!aiRes.ok) {

View File

@ -4,6 +4,7 @@ import z from "zod";
import { profileSchema } from "../app/validation/profile.schema";
import { Session } from "next-auth";
import { NextResponse } from "next/server";
import { analyzeSchema } from "../app/validation/analyze.schema";
export interface ModelDB {
modelName: string;
@ -362,3 +363,12 @@ export interface VisiblePageProps {
export interface RadarProps {
data: any[];
}
export type AnalyzeFormData = z.infer<typeof analyzeSchema>;
export interface AnalysisWithMetric {
metric: {
metricId: number;
name: string;
} | null;
}