feat: integrate word cloud get data endpoint

This commit is contained in:
Mahen 2026-02-10 07:55:38 +07:00
parent 71f2ed0a6c
commit e5a3262091
6 changed files with 154 additions and 43 deletions

View File

@ -1,4 +1,3 @@
// app/api/reviews/sentiment-stats/route.ts
import prisma from "@/lib/prisma";
import { NextResponse } from "next/server";

View File

@ -1,13 +1,10 @@
"use client";
import { WordCloudProps } from "@/src/types";
import WordCloudItem from "./WordCloudItem";
import { useWordCloud } from "@/src/hooks/useWordCloud";
import WordCloudItem from "./WordCloudItem";
export function WordCloud({ words }: WordCloudProps) {
const { mounted, maxValue, minValue, shuffledWords } = useWordCloud({
words,
});
export function WordCloud() {
const { mounted, maxValue, minValue, shuffledWords } = useWordCloud();
if (!mounted) {
return (

View File

@ -1,30 +1,14 @@
"use client";
import { cn } from "@/lib/utils";
import { WordCloudItemProps, WordItem } from "@/src/types";
import { setWordCloud } from "@/src/utils/datas";
const WordCloudItem: React.FC<WordCloudItemProps> = ({
word,
index,
maxValue,
minValue,
maxValue,
}) => {
const getSize = (value: number) => {
if (maxValue === minValue) return 1.5;
const normalized = (value - minValue) / (maxValue - minValue);
return 0.75 + normalized * 1.5;
};
const getColor = (sentiment: WordItem["sentiment"]) => {
switch (sentiment) {
case "positive":
return "text-sentiment-positive hover:bg-sentiment-positive-light";
case "negative":
return "text-sentiment-negative hover:bg-sentiment-negative-light";
default:
return "text-sentiment-neutral hover:bg-sentiment-neutral-light";
}
};
const { getSize, getColor } = setWordCloud({ minValue, maxValue });
return (
<span

View File

@ -1,11 +1,90 @@
import { useEffect, useMemo, useState } from "react";
import { WordCloudProps } from "../types";
import {
KeywordStats,
Review,
ReviewResponse,
Sentiment,
WordItem,
} from "@/src/types";
import { WORD_LIMIT } from "../utils/datas";
export const useWordCloud = ({ words }: WordCloudProps) => {
export const useWordCloud = () => {
const [mounted, setMounted] = useState(false);
const [words, setWords] = useState<WordItem[]>([]);
useEffect(() => {
setMounted(true);
const fetchWords = async () => {
try {
const res = await fetch("/api/review");
const result: unknown = await res.json();
if (
typeof result !== "object" ||
result === null ||
!("data" in result)
) {
console.error("Invalid response from /api/review");
return;
}
const reviewsData = (result as ReviewResponse).data || [];
const reviews: Review[] = reviewsData;
const keywordMap: Record<string, KeywordStats> = reviews.reduce(
(acc, review) => {
const sentiment: Sentiment = [
"positive",
"negative",
"neutral",
].includes(review.sentiment)
? review.sentiment
: "neutral";
(review.keywords || []).forEach((keyword) => {
const key = keyword.toLowerCase();
if (!acc[key]) {
acc[key] = { count: 0, positive: 0, negative: 0, neutral: 0 };
}
acc[key].count += 1;
acc[key][sentiment] += 1;
});
return acc;
},
{} as Record<string, KeywordStats>,
);
const wordItems: WordItem[] = Object.entries(keywordMap)
.map(([text, data]) => {
let sentiment: Sentiment = "neutral";
if (
data.positive >= data.negative &&
data.positive >= data.neutral
) {
sentiment = "positive";
} else if (
data.negative >= data.positive &&
data.negative >= data.neutral
) {
sentiment = "negative";
}
return { text, value: data.count, sentiment };
})
.sort((a, b) => b.value - a.value)
.slice(0, WORD_LIMIT);
setWords(wordItems);
} catch (error) {
console.error("Failed to fetch wordcloud data", error);
}
};
fetchWords();
}, []);
const maxValue = Math.max(...words.map((w) => w.value), 1);
@ -15,5 +94,10 @@ export const useWordCloud = ({ words }: WordCloudProps) => {
return [...words].sort(() => Math.random() - 0.5);
}, [words]);
return { mounted, maxValue, minValue, shuffledWords };
return {
mounted,
maxValue,
minValue,
shuffledWords,
};
};

View File

@ -1,4 +1,4 @@
import { Sentiment, UserGender } from "@prisma/client";
import { UserGender } from "@prisma/client";
import { LucideIcon } from "lucide-react";
export interface ModelDB {
@ -27,16 +27,17 @@ export interface BrandFilterProps {
onSelect: (brand: string | null) => void;
}
export interface Review {
export type Review = {
id: number;
product: string;
brand: string;
review: string;
rating?: number | null;
createdAt: string;
sentiment: Sentiment;
date: string;
confidence: number;
}
keywords: string[];
content: string;
product: {
name: string;
brand: string;
};
};
export interface ReviewTableProps {
reviews: Review[];
@ -94,11 +95,11 @@ export interface TrendChartTooltipProps {
label?: string;
}
export interface WordItem {
text: string;
value: number;
sentiment: "positive" | "negative" | "neutral";
}
// export type WordItem = {
// text: string;
// value: number;
// sentiment: "positive" | "negative" | "neutral";
// };
export interface WordCloudProps {
words: WordItem[];
@ -140,3 +141,25 @@ export interface ApiResponse {
message: string;
data: ReviewItem[];
}
export type Sentiment = "positive" | "negative" | "neutral";
export type WordItem = {
text: string;
value: number;
sentiment: Sentiment;
};
export type KeywordStats = {
count: number;
} & Record<Sentiment, number>;
export type ReviewResponse = {
message: string;
data: Review[];
};
export type WordCloudConfig = {
minValue: number;
maxValue: number;
};

View File

@ -1,4 +1,5 @@
import { Frown, Meh, Smile } from "lucide-react";
import { WordCloudConfig, WordCloudItemProps, WordItem } from "../types";
export const MODEL_OPTIONS = [
{
@ -49,3 +50,26 @@ export const getSentimentDisplay = (sentiment: string) => {
};
}
};
export const WORD_LIMIT = 20;
export const setWordCloud = ({ maxValue, minValue }: WordCloudConfig) => {
const getSize = (value: number) => {
if (maxValue === minValue) return 1.5;
const normalized = (value - minValue) / (maxValue - minValue);
return 0.75 + normalized * 1.5;
};
const getColor = (sentiment: WordItem["sentiment"]) => {
switch (sentiment) {
case "positive":
return "text-sentiment-positive hover:bg-sentiment-positive-light";
case "negative":
return "text-sentiment-negative hover:bg-sentiment-negative-light";
default:
return "text-sentiment-neutral hover:bg-sentiment-neutral-light";
}
};
return { getSize, getColor };
};