refactor: clean up spaghetti code
This commit is contained in:
parent
169b5050a3
commit
e30e126e0d
|
|
@ -28,10 +28,10 @@ async function main() {
|
|||
modelName: "Model XGBoost (Optimized)",
|
||||
description:
|
||||
"Model final menggunakan teknik SMOTE untuk menyeimbangkan kelas, seleksi fitur Chi-Square, dan optimasi Grid Search.",
|
||||
accuracy: 0.72,
|
||||
macroF1: 0.66,
|
||||
accuracy: 0.73,
|
||||
macroF1: 0.67,
|
||||
f1Negative: 0.68,
|
||||
f1Neutral: 0.43,
|
||||
f1Neutral: 0.45,
|
||||
isActive: true,
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -2,15 +2,12 @@ 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, AnalyzeFormData } from "../types";
|
||||
import {
|
||||
scrapeProduct,
|
||||
getAIRecommendation,
|
||||
} from "../services/analyze.service";
|
||||
import { analyzeSchema } from "../app/validation/analyze.schema"; // Sesuaikan path-nya
|
||||
import { getAnotherUserData } from "../app/profile/lib/action";
|
||||
import prisma from "@/lib/prisma";
|
||||
import { analyzeSchema } from "../app/validation/analyze.schema";
|
||||
import { getMetricId } from "../services/metric.service";
|
||||
|
||||
export const useAnalyseText = () => {
|
||||
|
|
@ -182,7 +179,7 @@ export const useAnalyseText = () => {
|
|||
} catch (error: any) {
|
||||
if (error.name === "AbortError" || signal.aborted) {
|
||||
console.log("🛠️ Request dibatalkan secara aman.");
|
||||
return; // Keluar dari fungsi tanpa memunculkan alert error
|
||||
return;
|
||||
}
|
||||
|
||||
console.error("Analysis Error:", error);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import { useState, useEffect, useMemo } from "react";
|
||||
import { ModelDB, Review, StatCounts } from "@/src/types";
|
||||
import { getClassificationReport } from "../app/dashboard/lib/actions";
|
||||
import { sentimentStatsPath } from "../utils/const";
|
||||
|
||||
export const useDashboards = () => {
|
||||
const [selectedBrand, setSelectedBrand] = useState<string | null>(null);
|
||||
|
|
@ -20,7 +21,7 @@ export const useDashboards = () => {
|
|||
async function fetchStats() {
|
||||
setLoading(true);
|
||||
|
||||
const res = await fetch("/api/review/sentiment-stats");
|
||||
const res = await fetch(sentimentStatsPath);
|
||||
const json = await res.json();
|
||||
|
||||
const statsData = json.data;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { useSession } from "next-auth/react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { productPath } from "../utils/const";
|
||||
|
||||
export const useHeader = () => {
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
|
|
@ -17,7 +18,7 @@ export const useHeader = () => {
|
|||
setMounted(true);
|
||||
const getProductCount = async () => {
|
||||
try {
|
||||
const res = await fetch("/api/product");
|
||||
const res = await fetch(productPath);
|
||||
if (!res.ok) throw new Error("Failed to fetch product count");
|
||||
|
||||
const data = await res.json();
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { useState, useEffect } from "react";
|
|||
import { useSession } from "next-auth/react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { ProfileClientProps, ProfileFormData, ProfileState } from "@/src/types";
|
||||
import { Brand, OS, Profession } from "@prisma/client";
|
||||
import { OS, Profession } from "@prisma/client";
|
||||
import { brandFormat } from "../utils/datas";
|
||||
|
||||
export const useProfileClient = (props: ProfileClientProps) => {
|
||||
|
|
@ -47,7 +47,7 @@ export const useProfileClient = (props: ProfileClientProps) => {
|
|||
(newData.profession as Profession) || prev.preference.profession,
|
||||
|
||||
preferredBrand:
|
||||
(newData.preferredBrand ) || prev.preference.preferredBrand,
|
||||
newData.preferredBrand || prev.preference.preferredBrand,
|
||||
|
||||
preferredOS: (newData.preferredOS as OS) || prev.preference.preferredOS,
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { useEffect, useState, useMemo } from "react";
|
||||
import { ApiResponse, ReviewItem } from "../types";
|
||||
import { PaginationService } from "../services/review.service";
|
||||
import { reviewPath } from "../utils/const";
|
||||
|
||||
export const useReviewTable = (
|
||||
itemsPerPage: number = 10,
|
||||
|
|
@ -14,7 +15,7 @@ export const useReviewTable = (
|
|||
const getReviewData = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const req = await fetch("/api/review");
|
||||
const req = await fetch(reviewPath);
|
||||
const res: ApiResponse = await req.json();
|
||||
|
||||
if (res.data && Array.isArray(res.data)) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { useMemo, useState } from "react";
|
||||
import { AnalysisResult } from "../types";
|
||||
import { Minus, ThumbsDown, ThumbsUp } from "lucide-react";
|
||||
import { configDisplay } from "../utils/datas";
|
||||
import { models, negativeWords, positiveWords } from "../utils/const";
|
||||
|
||||
export const useSentiment = () => {
|
||||
const [text, setText] = useState("");
|
||||
|
|
@ -23,32 +24,6 @@ export const useSentiment = () => {
|
|||
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||
|
||||
const positiveWords = [
|
||||
"bagus",
|
||||
"cepat",
|
||||
"aman",
|
||||
"baik",
|
||||
"mulus",
|
||||
"moga",
|
||||
"awet",
|
||||
"mantap",
|
||||
"sangat",
|
||||
"fungsi",
|
||||
];
|
||||
|
||||
const negativeWords = [
|
||||
"lebih",
|
||||
"jual",
|
||||
"baru",
|
||||
"lalu",
|
||||
"tahun",
|
||||
"masalah",
|
||||
"rusak",
|
||||
"garansi",
|
||||
"layar",
|
||||
"kecewa",
|
||||
];
|
||||
|
||||
const lowerText = text.toLowerCase();
|
||||
let positiveScore = 0;
|
||||
let negativeScore = 0;
|
||||
|
|
@ -91,53 +66,10 @@ export const useSentiment = () => {
|
|||
};
|
||||
|
||||
const getSentimentDisplay = (sentiment: AnalysisResult["sentiment"]) => {
|
||||
const config = {
|
||||
POSITIVE: {
|
||||
icon: ThumbsUp,
|
||||
label: "Positif",
|
||||
bgClass: "bg-sentiment-positive-light",
|
||||
textClass: "text-sentiment-positive",
|
||||
borderClass: "border-sentiment-positive/30",
|
||||
},
|
||||
NEGATIVE: {
|
||||
icon: ThumbsDown,
|
||||
label: "Negatif",
|
||||
bgClass: "bg-sentiment-negative-light",
|
||||
textClass: "text-sentiment-negative",
|
||||
borderClass: "border-sentiment-negative/30",
|
||||
},
|
||||
NEUTRAL: {
|
||||
icon: Minus,
|
||||
label: "Netral",
|
||||
bgClass: "bg-sentiment-neutral-light",
|
||||
textClass: "text-sentiment-neutral",
|
||||
borderClass: "border-sentiment-neutral/30",
|
||||
},
|
||||
};
|
||||
return config[sentiment];
|
||||
const config = configDisplay(sentiment);
|
||||
return config;
|
||||
};
|
||||
|
||||
const models = [
|
||||
{
|
||||
code: "none",
|
||||
value: "xgboost",
|
||||
label: "XGBoost (Baseline)",
|
||||
desc: "Model 1",
|
||||
},
|
||||
{
|
||||
code: "Grid Search",
|
||||
value: "xgboost",
|
||||
label: "XGBoost (Tuned)",
|
||||
desc: "Model 2",
|
||||
},
|
||||
{
|
||||
code: "recommended",
|
||||
value: "xgboost",
|
||||
label: "XGBoost (Fully Optimized)",
|
||||
desc: "Model 3",
|
||||
},
|
||||
];
|
||||
|
||||
const filteredItems = useMemo(() => {
|
||||
if (!searchQuery) return models;
|
||||
return models.filter(
|
||||
|
|
@ -149,18 +81,18 @@ export const useSentiment = () => {
|
|||
|
||||
return {
|
||||
selectedModel,
|
||||
setSelectedModel,
|
||||
text,
|
||||
setText,
|
||||
laptopName,
|
||||
setLaptopName,
|
||||
isAnalyzing,
|
||||
analyzeText,
|
||||
result,
|
||||
getSentimentDisplay,
|
||||
searchQuery,
|
||||
setSearchQuery,
|
||||
filteredItems,
|
||||
isFormValid,
|
||||
setSelectedModel,
|
||||
setText,
|
||||
setLaptopName,
|
||||
analyzeText,
|
||||
getSentimentDisplay,
|
||||
setSearchQuery,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { MODEL_OPTIONS } from "../utils/const";
|
||||
import { MODEL_OPTIONS, predictPath } from "../utils/const";
|
||||
|
||||
export const useSentimentForm = () => {
|
||||
const [selectedModel, setSelectedModel] = useState(MODEL_OPTIONS[2]);
|
||||
|
|
@ -31,7 +31,7 @@ export const useSentimentForm = () => {
|
|||
setResult(null);
|
||||
|
||||
try {
|
||||
const response = await fetch("http://127.0.0.1:8000/predict", {
|
||||
const response = await fetch(predictPath, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// src/hooks/useSocket.ts
|
||||
import { useEffect, useState } from "react";
|
||||
import { io, Socket } from "socket.io-client";
|
||||
import { socketPath } from "../utils/const";
|
||||
|
||||
export const useSocket = () => {
|
||||
const [socket, setSocket] = useState<Socket | null>(null);
|
||||
|
|
@ -8,7 +8,7 @@ export const useSocket = () => {
|
|||
|
||||
useEffect(() => {
|
||||
const socketInitializer = async () => {
|
||||
await fetch("/api/socket"); // Panggil API untuk menyalakan server socket
|
||||
await fetch(socketPath);
|
||||
const newSocket = io();
|
||||
|
||||
newSocket.on("progress", (data) => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { KeywordStats, WordCloudReview, WordItem } from "@/src/types";
|
||||
import { WORD_LIMIT } from "../utils/const";
|
||||
import { WORD_LIMIT, wordCloudPath } from "../utils/const";
|
||||
|
||||
export const useWordCloud = () => {
|
||||
const [words, setWords] = useState<WordItem[]>([]);
|
||||
|
|
@ -11,11 +11,11 @@ export const useWordCloud = () => {
|
|||
useEffect(() => {
|
||||
const fetchWords = async () => {
|
||||
try {
|
||||
const res = await fetch("/api/word-cloud");
|
||||
const res = await fetch(wordCloudPath);
|
||||
const json = await res.json();
|
||||
|
||||
if (!json?.success || !Array.isArray(json.data)) {
|
||||
console.error("Invalid response from /api/word-cloud");
|
||||
console.error(`Invalid response from ${wordCloudPath}`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import prisma from "@/lib/prisma";
|
||||
import { AIRecommendationResponse } from "../types";
|
||||
import { aiRecommendPath, scrapePath } from "../utils/const";
|
||||
|
||||
export const scrapeProduct = async (
|
||||
url: string,
|
||||
options?: { signal?: AbortSignal },
|
||||
) => {
|
||||
const res = await fetch("/api/scrape", {
|
||||
const res = await fetch(scrapePath, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
|
@ -70,7 +71,7 @@ export const getAIRecommendation = async (
|
|||
options?: { signal?: AbortSignal },
|
||||
): Promise<AIRecommendationResponse> => {
|
||||
console.log("Fetching to FastAPI...");
|
||||
const aiRes = await fetch("https://citot123-tokped-scraper.hf.space/recommend", {
|
||||
const aiRes = await fetch(aiRecommendPath, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
|
|
@ -79,13 +80,7 @@ export const getAIRecommendation = async (
|
|||
|
||||
if (!aiRes.ok) {
|
||||
const errorData = await aiRes.json();
|
||||
// DEBUG: Munculkan di console agar bisa dibaca strukturnya
|
||||
console.error(
|
||||
"DETAILED VALIDATION ERROR:",
|
||||
JSON.stringify(errorData, null, 2),
|
||||
);
|
||||
|
||||
// Ambil pesan error pertama dari list validation FastAPI
|
||||
const errorMessage =
|
||||
errorData.detail?.[0]?.msg || "Gagal melakukan analisis AI";
|
||||
throw new Error(errorMessage);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import { userMetricPath } from "../utils/const";
|
||||
|
||||
export const getMetricId = async () => {
|
||||
const response = await fetch("/api/user-metric");
|
||||
const response = await fetch(userMetricPath);
|
||||
if (!response.ok) return null;
|
||||
|
||||
const data = await response.json();
|
||||
return data.metricId;
|
||||
return data.metricId;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import prisma from "@/lib/prisma";
|
||||
import { ProfileFormData } from "../types";
|
||||
import { profilePath } from "../utils/const";
|
||||
|
||||
export const updateProfileService = async (formData: ProfileFormData) => {
|
||||
const response = await fetch("/api/profile", {
|
||||
const response = await fetch(profilePath, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(formData),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import puppeteer from "puppeteer-core";
|
|||
import chromium from "@sparticuz/chromium-min";
|
||||
import { ScrapeResult } from "../types";
|
||||
import { getFallbackData } from "../utils/datas";
|
||||
import { chromiumUrl } from "../utils/const";
|
||||
|
||||
function normalizeToReviewUrl(rawUrl: string): string {
|
||||
try {
|
||||
|
|
@ -28,8 +29,6 @@ export async function scrapeTokopediaProduct(
|
|||
): Promise<ScrapeResult> {
|
||||
const targetUrl = normalizeToReviewUrl(url);
|
||||
let browser;
|
||||
const CHROMIUM_URL =
|
||||
"https://github.com/Sparticuz/chromium/releases/download/v131.0.1/chromium-v131.0.1-pack.tar";
|
||||
|
||||
try {
|
||||
browser = await puppeteer.launch({
|
||||
|
|
@ -39,7 +38,7 @@ export async function scrapeTokopediaProduct(
|
|||
"--disable-setuid-sandbox",
|
||||
"--disable-blink-features=AutomationControlled",
|
||||
],
|
||||
executablePath: await chromium.executablePath(CHROMIUM_URL),
|
||||
executablePath: await chromium.executablePath(chromiumUrl),
|
||||
headless: true,
|
||||
defaultViewport: { width: 1280, height: 800 },
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,9 +8,8 @@ import {
|
|||
import { SiAcer, SiAsus, SiLenovo, SiLinux, SiMacos } from "react-icons/si";
|
||||
import { FaWindows } from "react-icons/fa";
|
||||
import { Sentiment } from "@prisma/client";
|
||||
import { useAnalyseText } from "../hooks/useAnalyzeText";
|
||||
|
||||
export const MODEL_OPTIONS = [
|
||||
const MODEL_OPTIONS = [
|
||||
{
|
||||
label: "Model XGBoost (Baseline)",
|
||||
code: "baseline",
|
||||
|
|
@ -28,9 +27,9 @@ export const MODEL_OPTIONS = [
|
|||
},
|
||||
];
|
||||
|
||||
export const WORD_LIMIT = 30;
|
||||
const WORD_LIMIT = 30;
|
||||
|
||||
export const professionItems = [
|
||||
const professionItems = [
|
||||
{ value: "PROGRAMMER", label: "Programmer", icon: Code },
|
||||
{ value: "DESIGNER", label: "Designer", icon: Palette },
|
||||
{ value: "STUDENT", label: "Student", icon: Book },
|
||||
|
|
@ -38,21 +37,21 @@ export const professionItems = [
|
|||
{ value: "OTHER", label: "Other", icon: LucideCircleEllipsis },
|
||||
];
|
||||
|
||||
export const brandItems = [
|
||||
const brandItems = [
|
||||
{ value: "ASUS", label: "Asus", icon: SiAsus },
|
||||
{ value: "ACER", label: "Acer", icon: SiAcer },
|
||||
{ value: "LENOVO", label: "Lenovo", icon: SiLenovo },
|
||||
{ value: "OTHER", label: "Other", icon: LucideCircleEllipsis },
|
||||
];
|
||||
|
||||
export const OSItems = [
|
||||
const OSItems = [
|
||||
{ value: "WINDOWS", label: "Windows", icon: FaWindows },
|
||||
{ value: "MACOS", label: "Macos", icon: SiMacos },
|
||||
{ value: "LINUX", label: "Linux", icon: SiLinux },
|
||||
{ value: "OTHER", label: "Other", icon: LucideCircleEllipsis },
|
||||
];
|
||||
|
||||
export const reviewDatas = [
|
||||
const reviewDatas = [
|
||||
{
|
||||
productId: 2,
|
||||
modelId: 1,
|
||||
|
|
@ -79,3 +78,87 @@ export const reviewDatas = [
|
|||
confidenceScore: 0.88,
|
||||
},
|
||||
];
|
||||
|
||||
const scrapePath = "/api/scrape";
|
||||
const backendUrl = process.env.BACKEND_URL || "http://localhost:8000";
|
||||
const aiRecommendPath = `${backendUrl}/recommend`;
|
||||
const userMetricPath = "/api/user-metric";
|
||||
const profilePath = "/api/profile";
|
||||
const chromiumUrl =
|
||||
"https://github.com/Sparticuz/chromium/releases/download/v131.0.1/chromium-v131.0.1-pack.tar";
|
||||
const sentimentStatsPath = "/api/review/sentiment-stats";
|
||||
const productPath = "/api/product";
|
||||
const reviewPath = "/api/review";
|
||||
const positiveWords = [
|
||||
"bagus",
|
||||
"cepat",
|
||||
"aman",
|
||||
"baik",
|
||||
"mulus",
|
||||
"moga",
|
||||
"awet",
|
||||
"mantap",
|
||||
"sangat",
|
||||
"fungsi",
|
||||
];
|
||||
|
||||
const negativeWords = [
|
||||
"lebih",
|
||||
"jual",
|
||||
"baru",
|
||||
"lalu",
|
||||
"tahun",
|
||||
"masalah",
|
||||
"rusak",
|
||||
"garansi",
|
||||
"layar",
|
||||
"kecewa",
|
||||
];
|
||||
|
||||
const models = [
|
||||
{
|
||||
code: "none",
|
||||
value: "xgboost",
|
||||
label: "XGBoost (Baseline)",
|
||||
desc: "Model 1",
|
||||
},
|
||||
{
|
||||
code: "Grid Search",
|
||||
value: "xgboost",
|
||||
label: "XGBoost (Tuned)",
|
||||
desc: "Model 2",
|
||||
},
|
||||
{
|
||||
code: "recommended",
|
||||
value: "xgboost",
|
||||
label: "XGBoost (Fully Optimized)",
|
||||
desc: "Model 3",
|
||||
},
|
||||
];
|
||||
|
||||
const predictPath = `${backendUrl}/predict`;
|
||||
const socketPath = "/api/socket";
|
||||
const wordCloudPath = "/api/word-cloud";
|
||||
|
||||
export {
|
||||
scrapePath,
|
||||
aiRecommendPath,
|
||||
userMetricPath,
|
||||
profilePath,
|
||||
chromiumUrl,
|
||||
sentimentStatsPath,
|
||||
productPath,
|
||||
reviewPath,
|
||||
positiveWords,
|
||||
negativeWords,
|
||||
models,
|
||||
predictPath,
|
||||
MODEL_OPTIONS,
|
||||
WORD_LIMIT,
|
||||
professionItems,
|
||||
brandItems,
|
||||
OSItems,
|
||||
reviewDatas,
|
||||
socketPath,
|
||||
wordCloudPath,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { useReviewTable } from "../hooks/useReviewTable";
|
||||
import { Minus, ThumbsDown, ThumbsUp } from "lucide-react";
|
||||
import {
|
||||
AnalysisResult,
|
||||
RadarProps,
|
||||
ScrapeResult,
|
||||
VisiblePageProps,
|
||||
|
|
@ -142,3 +143,30 @@ export const radarFormat = ({ data }: RadarProps) => {
|
|||
|
||||
return { chartData, colors };
|
||||
};
|
||||
|
||||
export const configDisplay = (sentiment: AnalysisResult["sentiment"]) => {
|
||||
const config = {
|
||||
POSITIVE: {
|
||||
icon: ThumbsUp,
|
||||
label: "Positif",
|
||||
bgClass: "bg-sentiment-positive-light",
|
||||
textClass: "text-sentiment-positive",
|
||||
borderClass: "border-sentiment-positive/30",
|
||||
},
|
||||
NEGATIVE: {
|
||||
icon: ThumbsDown,
|
||||
label: "Negatif",
|
||||
bgClass: "bg-sentiment-negative-light",
|
||||
textClass: "text-sentiment-negative",
|
||||
borderClass: "border-sentiment-negative/30",
|
||||
},
|
||||
NEUTRAL: {
|
||||
icon: Minus,
|
||||
label: "Netral",
|
||||
bgClass: "bg-sentiment-neutral-light",
|
||||
textClass: "text-sentiment-neutral",
|
||||
borderClass: "border-sentiment-neutral/30",
|
||||
},
|
||||
};
|
||||
return config[sentiment];
|
||||
};
|
||||
|
|
|
|||
|
|
@ -12,5 +12,4 @@ const iconStyles = {
|
|||
neutral: "bg-sentiment-neutral/10 text-sentiment-neutral",
|
||||
};
|
||||
|
||||
|
||||
export { variantStyles, iconStyles };
|
||||
|
|
|
|||
Loading…
Reference in New Issue