feat: add a process analysis blocker and a progress bar
This commit is contained in:
parent
a5c3737524
commit
fee2c94f78
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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,21 +116,36 @@ 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();
|
||||
|
||||
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
};
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
Loading…
Reference in New Issue