refactor: change all about fastapi endpoint integration

This commit is contained in:
Mahen 2026-03-06 15:27:06 +07:00
parent 03fe41419e
commit 16d1312f63
27 changed files with 642 additions and 130 deletions

View File

@ -3,6 +3,9 @@ import { ApiHandler, ServerActionHandler } from "@/src/types";
import { getServerSession } from "next-auth";
import { NextResponse } from "next/server";
export const dynamic = "force-dynamic";
export const revalidate = 0;
export function withAuth(handler: ApiHandler) {
return async (req: Request, context: any) => {
const session = await getServerSession(authOptions);

View File

@ -0,0 +1,205 @@
/*
Warnings:
- The primary key for the `Account` table will be changed. If it partially fails, the table could be left without primary key constraint.
- You are about to drop the column `id` on the `Account` table. All the data in the column will be lost.
- The primary key for the `Analysis` table will be changed. If it partially fails, the table could be left without primary key constraint.
- You are about to drop the column `compatibilityScore` on the `Analysis` table. All the data in the column will be lost.
- You are about to drop the column `generalSentiment` on the `Analysis` table. All the data in the column will be lost.
- You are about to drop the column `id` on the `Analysis` table. All the data in the column will be lost.
- You are about to drop the column `modelId` on the `Analysis` table. All the data in the column will be lost.
- You are about to drop the column `productId` on the `Analysis` table. All the data in the column will be lost.
- You are about to drop the column `targetProfession` on the `Analysis` table. All the data in the column will be lost.
- You are about to drop the column `topKeywords` on the `Analysis` table. All the data in the column will be lost.
- You are about to drop the column `verdict` on the `Analysis` table. All the data in the column will be lost.
- The primary key for the `Model` table will be changed. If it partially fails, the table could be left without primary key constraint.
- You are about to drop the column `id` on the `Model` table. All the data in the column will be lost.
- The primary key for the `Review` table will be changed. If it partially fails, the table could be left without primary key constraint.
- You are about to drop the column `id` on the `Review` table. All the data in the column will be lost.
- The primary key for the `Session` table will be changed. If it partially fails, the table could be left without primary key constraint.
- You are about to drop the column `id` on the `Session` table. All the data in the column will be lost.
- The primary key for the `User` table will be changed. If it partially fails, the table could be left without primary key constraint.
- You are about to drop the column `id` on the `User` table. All the data in the column will be lost.
- You are about to drop the `Product` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `UserPreference` table. If the table is not empty, all the data it contains will be lost.
- Added the required column `updatedAt` to the `Analysis` table without a default value. This is not possible if the table is not empty.
*/
-- CreateEnum
CREATE TYPE "BrandName" AS ENUM ('APPLE', 'ASUS', 'ACER', 'LENOVO', 'HP', 'DELL', 'MSI', 'AXIOO', 'ADVAN', 'ZYREX', 'OTHER');
-- DropForeignKey
ALTER TABLE "Account" DROP CONSTRAINT "Account_userId_fkey";
-- DropForeignKey
ALTER TABLE "Analysis" DROP CONSTRAINT "Analysis_modelId_fkey";
-- DropForeignKey
ALTER TABLE "Analysis" DROP CONSTRAINT "Analysis_productId_fkey";
-- DropForeignKey
ALTER TABLE "Analysis" DROP CONSTRAINT "Analysis_userId_fkey";
-- DropForeignKey
ALTER TABLE "Review" DROP CONSTRAINT "Review_modelId_fkey";
-- DropForeignKey
ALTER TABLE "Review" DROP CONSTRAINT "Review_productId_fkey";
-- DropForeignKey
ALTER TABLE "Review" DROP CONSTRAINT "Review_userId_fkey";
-- DropForeignKey
ALTER TABLE "Session" DROP CONSTRAINT "Session_userId_fkey";
-- DropForeignKey
ALTER TABLE "UserPreference" DROP CONSTRAINT "UserPreference_userId_fkey";
-- AlterTable
ALTER TABLE "Account" DROP CONSTRAINT "Account_pkey",
DROP COLUMN "id",
ADD COLUMN "accountId" SERIAL NOT NULL,
ADD CONSTRAINT "Account_pkey" PRIMARY KEY ("accountId");
-- AlterTable
ALTER TABLE "Analysis" DROP CONSTRAINT "Analysis_pkey",
DROP COLUMN "compatibilityScore",
DROP COLUMN "generalSentiment",
DROP COLUMN "id",
DROP COLUMN "modelId",
DROP COLUMN "productId",
DROP COLUMN "targetProfession",
DROP COLUMN "topKeywords",
DROP COLUMN "verdict",
ADD COLUMN "analysisId" SERIAL NOT NULL,
ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL,
ADD CONSTRAINT "Analysis_pkey" PRIMARY KEY ("analysisId");
-- AlterTable
ALTER TABLE "Model" DROP CONSTRAINT "Model_pkey",
DROP COLUMN "id",
ADD COLUMN "modelId" SERIAL NOT NULL,
ADD CONSTRAINT "Model_pkey" PRIMARY KEY ("modelId");
-- AlterTable
ALTER TABLE "Review" DROP CONSTRAINT "Review_pkey",
DROP COLUMN "id",
ADD COLUMN "reviewId" SERIAL NOT NULL,
ADD CONSTRAINT "Review_pkey" PRIMARY KEY ("reviewId");
-- AlterTable
ALTER TABLE "Session" DROP CONSTRAINT "Session_pkey",
DROP COLUMN "id",
ADD COLUMN "sessionId" SERIAL NOT NULL,
ADD CONSTRAINT "Session_pkey" PRIMARY KEY ("sessionId");
-- AlterTable
ALTER TABLE "User" DROP CONSTRAINT "User_pkey",
DROP COLUMN "id",
ADD COLUMN "userId" SERIAL NOT NULL,
ADD CONSTRAINT "User_pkey" PRIMARY KEY ("userId");
-- DropTable
DROP TABLE "Product";
-- DropTable
DROP TABLE "UserPreference";
-- DropEnum
DROP TYPE "Brand";
-- CreateTable
CREATE TABLE "user_preferences" (
"userPreferenceId" SERIAL NOT NULL,
"profession" "Profession",
"preferredOS" "OS",
"budgetMin" INTEGER,
"budgetMax" INTEGER,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"userId" INTEGER NOT NULL,
"preferedBrandId" INTEGER,
CONSTRAINT "user_preferences_pkey" PRIMARY KEY ("userPreferenceId")
);
-- CreateTable
CREATE TABLE "products" (
"productId" SERIAL NOT NULL,
"name" TEXT NOT NULL,
"url" TEXT NOT NULL,
"image" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"brandId" INTEGER,
CONSTRAINT "products_pkey" PRIMARY KEY ("productId")
);
-- CreateTable
CREATE TABLE "Metric" (
"metricId" SERIAL NOT NULL,
"generalSentiment" DOUBLE PRECISION NOT NULL,
"compatibilityScore" DOUBLE PRECISION NOT NULL,
"verdict" TEXT NOT NULL,
"topKeywords" TEXT[],
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"analysisId" INTEGER NOT NULL,
"productId" INTEGER NOT NULL,
"modelId" INTEGER NOT NULL,
CONSTRAINT "Metric_pkey" PRIMARY KEY ("metricId")
);
-- CreateTable
CREATE TABLE "brands" (
"brandId" SERIAL NOT NULL,
"name" "BrandName",
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "brands_pkey" PRIMARY KEY ("brandId")
);
-- CreateIndex
CREATE UNIQUE INDEX "user_preferences_userId_key" ON "user_preferences"("userId");
-- CreateIndex
CREATE UNIQUE INDEX "products_url_key" ON "products"("url");
-- AddForeignKey
ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("userId") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("userId") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "user_preferences" ADD CONSTRAINT "user_preferences_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("userId") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "user_preferences" ADD CONSTRAINT "user_pref_brand_fkey" FOREIGN KEY ("preferedBrandId") REFERENCES "brands"("brandId") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "products" ADD CONSTRAINT "product_brand_fkey" FOREIGN KEY ("brandId") REFERENCES "brands"("brandId") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Review" ADD CONSTRAINT "Review_productId_fkey" FOREIGN KEY ("productId") REFERENCES "products"("productId") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Review" ADD CONSTRAINT "Review_modelId_fkey" FOREIGN KEY ("modelId") REFERENCES "Model"("modelId") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Review" ADD CONSTRAINT "Review_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("userId") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Analysis" ADD CONSTRAINT "Analysis_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("userId") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Metric" ADD CONSTRAINT "Metric_analysisId_fkey" FOREIGN KEY ("analysisId") REFERENCES "Analysis"("analysisId") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Metric" ADD CONSTRAINT "Metric_productId_fkey" FOREIGN KEY ("productId") REFERENCES "products"("productId") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Metric" ADD CONSTRAINT "Metric_modelId_fkey" FOREIGN KEY ("modelId") REFERENCES "Model"("modelId") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@ -0,0 +1,98 @@
/*
Warnings:
- You are about to drop the `Account` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `Session` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `User` table. If the table is not empty, all the data it contains will be lost.
*/
-- DropForeignKey
ALTER TABLE "Account" DROP CONSTRAINT "Account_userId_fkey";
-- DropForeignKey
ALTER TABLE "Analysis" DROP CONSTRAINT "Analysis_userId_fkey";
-- DropForeignKey
ALTER TABLE "Review" DROP CONSTRAINT "Review_userId_fkey";
-- DropForeignKey
ALTER TABLE "Session" DROP CONSTRAINT "Session_userId_fkey";
-- DropForeignKey
ALTER TABLE "user_preferences" DROP CONSTRAINT "user_preferences_userId_fkey";
-- DropTable
DROP TABLE "Account";
-- DropTable
DROP TABLE "Session";
-- DropTable
DROP TABLE "User";
-- CreateTable
CREATE TABLE "accounts" (
"id" SERIAL NOT NULL,
"type" TEXT NOT NULL,
"provider" TEXT NOT NULL,
"providerAccountId" TEXT NOT NULL,
"refresh_token" TEXT,
"access_token" TEXT,
"expires_at" INTEGER,
"token_type" TEXT,
"scope" TEXT,
"id_token" TEXT,
"session_state" TEXT,
"user_id" INTEGER NOT NULL,
CONSTRAINT "accounts_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "sessions" (
"id" SERIAL NOT NULL,
"session_token" TEXT NOT NULL,
"expires" TIMESTAMP(3) NOT NULL,
"user_id" INTEGER NOT NULL,
CONSTRAINT "sessions_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "users" (
"userId" SERIAL NOT NULL,
"name" TEXT,
"email" TEXT,
"emailVerified" TIMESTAMP(3),
"image" TEXT,
"password" TEXT,
"bio" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "users_pkey" PRIMARY KEY ("userId")
);
-- CreateIndex
CREATE UNIQUE INDEX "accounts_provider_providerAccountId_key" ON "accounts"("provider", "providerAccountId");
-- CreateIndex
CREATE UNIQUE INDEX "sessions_session_token_key" ON "sessions"("session_token");
-- CreateIndex
CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
-- AddForeignKey
ALTER TABLE "accounts" ADD CONSTRAINT "accounts_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("userId") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("userId") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "user_preferences" ADD CONSTRAINT "user_preferences_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("userId") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Review" ADD CONSTRAINT "Review_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("userId") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Analysis" ADD CONSTRAINT "Analysis_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("userId") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -0,0 +1,9 @@
/*
Warnings:
- The `name` column on the `brands` table would be dropped and recreated. This will lead to data loss if there is data in the column.
*/
-- AlterTable
ALTER TABLE "brands" DROP COLUMN "name",
ADD COLUMN "name" TEXT;

View File

@ -0,0 +1,12 @@
/*
Warnings:
- A unique constraint covering the columns `[name]` on the table `brands` will be added. If there are existing duplicate values, this will fail.
- Made the column `name` on table `brands` required. This step will fail if there are existing NULL values in that column.
*/
-- AlterTable
ALTER TABLE "brands" ALTER COLUMN "name" SET NOT NULL;
-- CreateIndex
CREATE UNIQUE INDEX "brands_name_key" ON "brands"("name");

View File

@ -26,7 +26,7 @@ enum OS {
OTHER
}
enum Brand {
enum BrandName {
APPLE
ASUS
ACER
@ -49,8 +49,7 @@ enum Profession {
}
model Account {
id Int @id @default(autoincrement())
userId Int
id Int @id @default(autoincrement())
type String
provider String
providerAccountId String
@ -61,17 +60,23 @@ model Account {
scope String?
id_token String?
session_state String?
userId Int @map("user_id")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
@@map("accounts")
}
model Session {
id Int @id @default(autoincrement())
sessionToken String @unique
userId Int
sessionToken String @unique @map("session_token")
expires DateTime
userId Int @map("user_id")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@map("sessions")
}
model VerificationToken {
@ -83,7 +88,7 @@ model VerificationToken {
}
model User {
id Int @id @default(autoincrement())
id Int @id @default(autoincrement()) @map("userId")
name String?
email String? @unique
emailVerified DateTime?
@ -99,40 +104,50 @@ model User {
analyses Analysis[]
preference UserPreference?
review Review[]
review Review[]
@@map("users")
}
model UserPreference {
id Int @id @default(autoincrement())
profession Profession?
preferredOS OS?
preferredBrand Brand?
budgetMin Int?
budgetMax Int?
userPreferenceId Int @id @default(autoincrement())
profession Profession?
preferredOS OS?
budgetMin Int?
budgetMax Int?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
userId Int @unique
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
preferedBrandId Int?
brand Brand? @relation(fields: [preferedBrandId], references: [brandId], map: "user_pref_brand_fkey")
@@map("user_preferences")
}
model Product {
id Int @id @default(autoincrement())
name String
brand String?
url String @unique
image String?
productId Int @id @default(autoincrement())
name String
url String @unique
image String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
reviews Review[]
analyses Analysis[]
reviews Review[]
metrics Metric[]
brandId Int?
brand Brand? @relation(fields: [brandId], references: [brandId], map: "product_brand_fkey")
@@map("products")
}
model Review {
id Int @id @default(autoincrement())
reviewId Int @id @default(autoincrement())
content String
sentiment Sentiment
confidenceScore Float
@ -142,53 +157,73 @@ model Review {
updatedAt DateTime @updatedAt
productId Int
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
product Product @relation(fields: [productId], references: [productId], onDelete: Cascade)
modelId Int?
model Model? @relation(fields: [modelId], references: [id])
model Model? @relation(fields: [modelId], references: [modelId])
userId Int?
user User? @relation(fields: [userId], references: [id])
}
model Analysis {
id Int @id @default(autoincrement())
targetProfession String
generalSentiment Float
compatibilityScore Float
verdict String
topKeywords String[]
createdAt DateTime @default(now())
userId Int
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
productId Int
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
modelId Int
model Model @relation(fields: [modelId], references: [id])
}
model Model {
id Int @id @default(autoincrement())
modelName String
description String?
accuracy Float
macroF1 Float
f1Negative Float
f1Neutral Float
isActive Boolean @default(true)
analysisId Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
analyses Analysis[]
reviews Review[]
userId Int
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
metric Metric[]
}
model Model {
modelId Int @id @default(autoincrement())
modelName String
description String?
accuracy Float
macroF1 Float
f1Negative Float
f1Neutral Float
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
metrics Metric[]
reviews Review[]
}
model Metric {
metricId Int @id @default(autoincrement())
generalSentiment Float
compatibilityScore Float
verdict String
topKeywords String[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
analysisId Int
analysis Analysis @relation(fields: [analysisId], references: [analysisId])
productId Int
product Product @relation(fields: [productId], references: [productId], onDelete: Cascade)
modelId Int
model Model @relation(fields: [modelId], references: [modelId])
}
model Brand {
brandId Int @id @default(autoincrement())
name String @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
products Product[]
userPreferences UserPreference[]
@@map("brands")
}

View File

@ -12,10 +12,19 @@ export const authOptions = {
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
allowDangerousEmailAccountLinking: true,
}),
],
debug: true,
session: { strategy: "database" as const },
callbacks: {
async session({ session, user }: any) {
if (session.user) {
session.user.id = user.id;
}
return session;
},
},
};
const handler = NextAuth(authOptions);

View File

@ -0,0 +1,31 @@
import { NextResponse } from "next/server";
import { getServerSession } from "next-auth";
import prisma from "@/lib/prisma";
import { authOptions } from "../auth/[...nextauth]/route";
import { AnalysisWithMetric } from "@/src/hooks/useAnalyzeText";
export async function GET() {
const session = await getServerSession(authOptions);
if (!session?.user?.email) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const userAnalysis = (await prisma.analysis.findFirst({
where: {
user: { email: session.user.email },
},
select: {
metric: {
select: {
metricId: true,
},
},
},
orderBy: {
createdAt: "desc",
},
})) as AnalysisWithMetric | null;
return NextResponse.json({ metricId: userAnalysis?.metric?.metricId });
}

View File

@ -4,6 +4,7 @@ import { withActionAuth } from "@/lib/withAuth";
import { getAnalysisData } from "@/src/services/analyze.service";
import { formatBrandStats } from "@/src/services/brand.service";
import { reportService } from "@/src/services/report.service";
import { AnalysisData } from "@/src/types";
export const getClassificationReport = async () => {
try {
@ -20,13 +21,29 @@ export const getTotalBrandAnalysis = withActionAuth(async (session) => {
try {
const email = session.user?.email as string;
const userAnalysis = await getAnalysisData(email);
const rawAnalysis = await getAnalysisData(email);
const userAnalysis: AnalysisData[] = rawAnalysis.map((item: any) => ({
analysisId: item.analysisId,
userId: item.userId,
createdAt: item.createdAt,
updatedAt: item.updatedAt,
product:
item.metric && item.metric[0]?.product
? {
productId: item.metric[0].product.productId, // Gunakan productId, bukan id
brandName: item.metric[0].product.brand?.name || "Generic", // Gunakan brandName, bukan brand
name: item.metric[0].product.name,
reviewCount: item.metric[0].product._count?.reviews || 0, // Gunakan reviewCount, bukan _count.reviews
}
: null,
}));
const formattedBrands = formatBrandStats(userAnalysis);
return { formattedBrands, userAnalysis };
} catch (error) {
console.error("Gagal mengambil data review:", error);
return [];
return { formattedBrands: [], userAnalysis: [] };
}
});

View File

@ -5,7 +5,12 @@ import { getAnotherUserDataService } from "@/src/services/users.service";
export const getAnotherUserData = withActionAuth(async (session) => {
try {
const email = (await session.user?.email) as string;
const email = session.user?.email;
if (!email) {
console.warn("No email found in session");
return null;
}
const userData = await getAnotherUserDataService(email);

View File

@ -28,7 +28,7 @@ export const profileSchema = z.object({
name: z.string().min(2, "Nama minimal 2 karakter"),
bio: z.string().min(10, "Bio minimal 10 karakter"),
profession: professionEnum,
preferredBrand: brandEnum,
preferredBrand: z.string().min(2, "Merek laptop minimal 2 karakter"),
preferredOS: osEnum,
budgetMin: z.coerce.number().min(0),
budgetMax: z.coerce.number().min(0),

View File

@ -9,7 +9,6 @@ import {
MessageSquareText,
Smile,
Sparkles,
TrendingUp,
} from "lucide-react";
import { StatCard } from "./StatCard";
import { ModelInfoSkeleton } from "../skeletons/ModelInfoSkeleton";

View File

@ -8,8 +8,8 @@ import { Button } from "../ui/button";
import { Separator } from "../ui/separator";
import { formatRupiah, toTitleCase } from "@/src/utils/datas";
import { brandItems, OSItems, professionItems } from "@/src/utils/const";
import { useProfileClient } from "@/src/hooks/useProfileClient";
import { ProfileModal } from "./ProfileModal";
import { useProfileClient } from "@/src/hooks/useProfileClient";
export default function ProfileCard(props: ProfileClientProps) {
const {

View File

@ -23,7 +23,7 @@ export default async function ProfileClient() {
name={user?.name || ""}
bio={user?.bio || "None"}
profession={user?.preference?.profession || ""}
preferenceBrand={user?.preference?.preferredBrand || ""}
preferenceBrand={user?.preference?.brand?.name || ""}
preferenceOS={user?.preference?.preferredOS || ""}
budgetMax={user?.preference?.budgetMax || 0}
budgetMin={user?.preference?.budgetMin || 0}

View File

@ -88,7 +88,8 @@ export function ReviewTable() {
<div className="flex flex-col gap-1.5">
<div className="flex items-center gap-2">
<span className="inline-flex items-center rounded-md bg-primary/10 px-2 py-0.5 text-xs font-medium text-primary ring-1 ring-inset ring-primary/20">
{review.product?.brand || "Generic"}
{/* Tambahkan .name di sini */}
{review.product?.brand?.name || "Generic"}
</span>
</div>
<span className="text-sm font-medium leading-tight text-foreground line-clamp-2">

View File

@ -10,9 +10,18 @@ import {
} 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 { 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);
@ -121,9 +130,19 @@ export const useAnalyseText = () => {
reviews: res.data.reviews,
}));
const aiResult = await getAIRecommendation({
const metricIdValue = await getMetricId();
console.log("Payload to AI:", {
user_email: session.user.email,
metric_id: metricIdValue,
candidateCount: candidates.length,
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,
});
setResult(aiResult);

View File

@ -47,7 +47,7 @@ export const useProfileClient = (props: ProfileClientProps) => {
(newData.profession as Profession) || prev.preference.profession,
preferredBrand:
(newData.preferredBrand as Brand) || prev.preference.preferredBrand,
(newData.preferredBrand ) || prev.preference.preferredBrand,
preferredOS: (newData.preferredOS as OS) || prev.preference.preferredOS,
@ -74,7 +74,9 @@ export const useProfileClient = (props: ProfileClientProps) => {
profession,
} = preference;
const { brands } = brandFormat({ preferenceBrand });
const { brands } = brandFormat({
preferenceBrand: preferenceBrand as any,
});
return {
session,

View File

@ -38,7 +38,7 @@ export const useReviewTable = (
const filteredData = selectedBrand
? data.filter(
(review) =>
review.product?.brand?.toLowerCase() ===
review.product?.brand?.name.toLowerCase() ===
selectedBrand.toLowerCase(),
)
: data;

View File

@ -72,13 +72,13 @@ export const useSentiment = () => {
let confidence: number;
if (positiveScore > negativeScore) {
sentiment = "positif";
sentiment = "POSITIVE";
confidence = 0.75 + Math.random() * 0.2;
} else if (negativeScore > positiveScore) {
sentiment = "negatif";
sentiment = "NEGATIVE";
confidence = 0.75 + Math.random() * 0.2;
} else {
sentiment = "netral";
sentiment = "NEUTRAL";
confidence = 0.6 + Math.random() * 0.2;
}
@ -92,21 +92,21 @@ export const useSentiment = () => {
const getSentimentDisplay = (sentiment: AnalysisResult["sentiment"]) => {
const config = {
positif: {
POSITIVE: {
icon: ThumbsUp,
label: "Positif",
bgClass: "bg-sentiment-positive-light",
textClass: "text-sentiment-positive",
borderClass: "border-sentiment-positive/30",
},
negatif: {
NEGATIVE: {
icon: ThumbsDown,
label: "Negatif",
bgClass: "bg-sentiment-negative-light",
textClass: "text-sentiment-negative",
borderClass: "border-sentiment-negative/30",
},
netral: {
NEUTRAL: {
icon: Minus,
label: "Netral",
bgClass: "bg-sentiment-neutral-light",

View File

@ -19,22 +19,6 @@ export const scrapeProduct = async (url: string) => {
return data;
};
// export const getAIRecommendation = async (payload: {
// user_email: string;
// // profession: string;
// candidates: { name: string; url: string; reviews: any[] }[];
// }) => {
// const aiRes = await fetch("http://localhost:8000/recommend", {
// method: "POST",
// headers: { "Content-Type": "application/json" },
// body: JSON.stringify(payload),
// });
// if (!aiRes.ok) throw new Error("Gagal melakukan analisis AI");
// return await aiRes.json();
// };
export const getAnalysisData = async (email: string) => {
const userAnalyses = await prisma.analysis.findMany({
where: {
@ -43,16 +27,24 @@ export const getAnalysisData = async (email: string) => {
},
},
include: {
product: {
metric: {
select: {
id: true,
brand: true,
_count: {
product: {
select: {
reviews: {
where: {
user: {
email: email,
productId: true,
brand: {
select:{
name: true
}
},
_count: {
select: {
reviews: {
where: {
user: {
email: email,
},
},
},
},
},
@ -67,17 +59,28 @@ export const getAnalysisData = async (email: string) => {
export const getAIRecommendation = async (payload: {
user_email: string;
metric_id: number | 1;
candidates: { name: string; url: string; reviews: string[] }[];
}): 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),
body: JSON.stringify(payload),
});
if (!aiRes.ok) {
const errorData = await aiRes.json();
throw new Error(errorData.detail || "Gagal melakukan analisis AI");
// 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);
}
return await aiRes.json();

View File

@ -5,9 +5,9 @@ export const formatBrandStats = (userAnalysis: AnalysisData[]) => {
const brandCounts = userAnalysis.reduce(
(acc: Record<string, number>, analysis) => {
const productId = analysis.product?.id;
const rawBrand = analysis.product?.brand || "Unknown";
const reviewCount = analysis.product?._count?.reviews || 0;
const productId = analysis.product?.productId;
const rawBrand = analysis.product?.brandName || "Unknown";
const reviewCount = analysis.product?.reviewCount || 0;
if (productId && countedProductIds.has(productId)) {
return acc;
@ -20,7 +20,7 @@ export const formatBrandStats = (userAnalysis: AnalysisData[]) => {
const formattedBrand = rawBrand
.trim()
.toLowerCase()
.replace(/\b\w/g, (char) => char.toUpperCase());
.replace(/\b\w/g, (char: any) => char.toUpperCase());
if (!acc[formattedBrand]) {
acc[formattedBrand] = 0;

View File

@ -0,0 +1,7 @@
export const getMetricId = async () => {
const response = await fetch("/api/user-metric");
if (!response.ok) return null;
const data = await response.json();
return data.metricId;
};

View File

@ -8,9 +8,11 @@ export const productService = async (email: string) => {
if (!user) return null;
const totalProducts = await prisma.analysis.count({
const totalProducts = await prisma.metric.count({
where: {
userId: user.id,
analysis: {
userId: user.id,
},
},
});

View File

@ -23,7 +23,11 @@ export const userService = {
include: {
preference: {
select: {
preferredBrand: true,
brand: {
select: {
name: true,
},
},
preferredOS: true,
profession: true,
budgetMax: true,
@ -54,25 +58,38 @@ export const userService = {
bio: data.bio,
preference: {
upsert: {
where: { userId: user.id },
update: {
profession: data.profession,
preferredBrand: data.preferredBrand,
preferredOS: data.preferredOS,
budgetMin,
budgetMax,
brand: {
connectOrCreate: {
where: { name: data.preferredBrand },
create: { name: data.preferredBrand },
},
},
},
create: {
profession: data.profession,
preferredBrand: data.preferredBrand,
preferredOS: data.preferredOS,
budgetMin,
budgetMax,
brand: {
connectOrCreate: {
where: { name: data.preferredBrand },
create: { name: data.preferredBrand },
},
},
},
},
},
},
include: {
preference: true,
preference: {
include: { brand: true },
},
},
});

View File

@ -23,7 +23,7 @@ export const getReviewService = async (email: string) => {
createdAt: "asc",
},
select: {
id: true,
reviewId: true,
createdAt: true,
confidenceScore: true,
sentiment: true,

View File

@ -10,9 +10,13 @@ export const getAnotherUserDataService = async (email: string) => {
bio: true,
preference: {
select: {
id: true,
userPreferenceId: true,
profession: true,
preferredBrand: true,
brand:{
select:{
name: true
}
},
preferredOS: true,
budgetMin: true,
budgetMax: true,
@ -20,6 +24,5 @@ export const getAnotherUserDataService = async (email: string) => {
},
},
});
return userData;
};

View File

@ -1,5 +1,5 @@
import { LucideIcon } from "lucide-react";
import { OS, Profession, Sentiment, Brand } from "@prisma/client";
import { OS, Profession, Sentiment } from "@prisma/client";
import z from "zod";
import { profileSchema } from "../app/validation/profile.schema";
import { Session } from "next-auth";
@ -18,7 +18,7 @@ export interface ModelDB {
export interface ProfileClientProps {
name: string;
bio?: string;
preferenceBrand: string;
preferenceBrand: string
preferenceOS: string;
budgetMin: number;
budgetMax: number;
@ -130,6 +130,33 @@ export interface UseStatCardProps {
delay?: number;
}
// export interface ReviewItem {
// reviewId: number;
// content: string;
// sentiment: Sentiment;
// confidenceScore: number;
// createdAt: string;
// keywords: string[];
// product: {
// name: string;
// brand?: Brand;
// } | null;
// }
export interface Brand {
brandId: number;
name: string;
createdAt?: string;
updatedAt?: string;
}
export interface Product {
productId: number; // Pastikan sesuai dengan schema.prisma (productId bukan id)
name: string;
url: string;
brand: Brand | null; // <--- Ubah dari string ke Brand object
}
export interface ReviewItem {
id: number;
content: string;
@ -137,10 +164,7 @@ export interface ReviewItem {
confidenceScore: number;
createdAt: string;
keywords: string[];
product: {
name: string;
brand?: string;
} | null;
product: Product | null;
}
export interface ApiResponse {
@ -307,14 +331,25 @@ export type ServerActionHandler<T, Args extends any[] = any[]> = (
...args: Args
) => Promise<T>;
// export type AnalysisData = {
// product?: {
// id: number;
// brand: string | null;
// _count?: {
// reviews: number;
// };
// };
// };
export type AnalysisData = {
product?: {
id: number;
brand: string | null;
_count?: {
reviews: number;
};
};
analysisId: number;
createdAt: string | Date;
product: {
productId: number;
brandName: string | null;
name: string;
reviewCount: number;
} | null;
};
export type BodyData = (req: Request, body: any) => Promise<NextResponse>;