From fee2c94f78bcb6623e13a00711ef9c7e8bdde979 Mon Sep 17 00:00:00 2001 From: Mahen Date: Fri, 20 Mar 2026 16:59:30 +0700 Subject: [PATCH] feat: add a process analysis blocker and a progress bar --- package-lock.json | 275 ++++++++++++++++++- package.json | 2 + src/app/api/socket/route.ts | 40 +++ src/components/dashboards/AnalysisClient.tsx | 48 +++- src/hooks/useAnalyzeText.ts | 83 ++++-- src/hooks/useSocket.ts | 33 +++ src/services/analyze.service.ts | 26 +- src/types/index.ts | 10 + 8 files changed, 474 insertions(+), 43 deletions(-) create mode 100644 src/app/api/socket/route.ts create mode 100644 src/hooks/useSocket.ts diff --git a/package-lock.json b/package-lock.json index 5ae4529..4b08922 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 6f3f6c8..dde2278 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/app/api/socket/route.ts b/src/app/api/socket/route.ts new file mode 100644 index 0000000..2eecea1 --- /dev/null +++ b/src/app/api/socket/route.ts @@ -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(); +} diff --git a/src/components/dashboards/AnalysisClient.tsx b/src/components/dashboards/AnalysisClient.tsx index 50e4cc3..3648066 100644 --- a/src/components/dashboards/AnalysisClient.tsx +++ b/src/components/dashboards/AnalysisClient.tsx @@ -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() { - + {loading && ( +
+
+ {progress.status} + {progress.percent}% +
+
+
+
+
+ )} + +
+ {loading && ( + + )} + +
diff --git a/src/hooks/useAnalyzeText.ts b/src/hooks/useAnalyzeText.ts index 15598d3..932c528 100644 --- a/src/hooks/useAnalyzeText.ts +++ b/src/hooks/useAnalyzeText.ts @@ -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; - -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(null); const [showField, setShowField] = useState(false); const resultRef = useRef(null); + const [progress, setProgress] = useState({ status: "", percent: 0 }); + const abortControllerRef = useRef(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, }; }; diff --git a/src/hooks/useSocket.ts b/src/hooks/useSocket.ts new file mode 100644 index 0000000..114279e --- /dev/null +++ b/src/hooks/useSocket.ts @@ -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(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 }; +}; diff --git a/src/services/analyze.service.ts b/src/services/analyze.service.ts index b4f5433..58265c2 100644 --- a/src/services/analyze.service.ts +++ b/src/services/analyze.service.ts @@ -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 => { +export const getAIRecommendation = async ( + payload: { + user_email: string; + metric_id: number | 1; + candidates: { name: string; url: string; reviews: string[] }[]; + }, + options?: { signal?: AbortSignal }, +): Promise => { 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) { diff --git a/src/types/index.ts b/src/types/index.ts index 7cfade7..defd41c 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -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; + +export interface AnalysisWithMetric { + metric: { + metricId: number; + name: string; + } | null; +} \ No newline at end of file