feat: integrate word cloud get data endpoint
This commit is contained in:
parent
71f2ed0a6c
commit
e5a3262091
|
|
@ -1,4 +1,3 @@
|
|||
// app/api/reviews/sentiment-stats/route.ts
|
||||
import prisma from "@/lib/prisma";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue