From 303226e3a97bbb22e52eead4985137e05234838e Mon Sep 17 00:00:00 2001 From: Kaj Kowalski Date: Thu, 22 May 2025 22:20:18 +0200 Subject: [PATCH] Refactor trend calculations and improve WordCloud component responsiveness; remove trend labels for cleaner display --- app/dashboard/overview/page.tsx | 20 +- components/MetricCard.tsx | 3 - components/WordCloud.tsx | 63 +++- lib/metrics.ts | 523 +++++++++++++++++--------------- 4 files changed, 340 insertions(+), 269 deletions(-) diff --git a/app/dashboard/overview/page.tsx b/app/dashboard/overview/page.tsx index a40c463..511a14e 100644 --- a/app/dashboard/overview/page.tsx +++ b/app/dashboard/overview/page.tsx @@ -253,10 +253,6 @@ function DashboardContent() { } trend={{ value: metrics.sessionTrend ?? 0, - label: - (metrics.sessionTrend ?? 0) > 0 - ? `${metrics.sessionTrend ?? 0}% increase` - : `${Math.abs(metrics.sessionTrend ?? 0)}% decrease`, isPositive: (metrics.sessionTrend ?? 0) >= 0, }} /> @@ -281,10 +277,6 @@ function DashboardContent() { } trend={{ value: metrics.usersTrend ?? 0, - label: - (metrics.usersTrend ?? 0) > 0 - ? `${metrics.usersTrend}% increase` - : `${Math.abs(metrics.usersTrend ?? 0)}% decrease`, isPositive: (metrics.usersTrend ?? 0) >= 0, }} /> @@ -309,10 +301,6 @@ function DashboardContent() { } trend={{ value: metrics.avgSessionTimeTrend ?? 0, - label: - (metrics.avgSessionTimeTrend ?? 0) > 0 - ? `${metrics.avgSessionTimeTrend}% increase` - : `${Math.abs(metrics.avgSessionTimeTrend ?? 0)}% decrease`, isPositive: (metrics.avgSessionTimeTrend ?? 0) >= 0, }} /> @@ -337,10 +325,6 @@ function DashboardContent() { } trend={{ value: metrics.avgResponseTimeTrend ?? 0, - label: - (metrics.avgResponseTimeTrend ?? 0) > 0 - ? `${metrics.avgResponseTimeTrend ?? 0}% increase` - : `${Math.abs(metrics.avgResponseTimeTrend ?? 0)}% decrease`, isPositive: (metrics.avgResponseTimeTrend ?? 0) <= 0, // Lower response time is better }} /> @@ -402,7 +386,9 @@ function DashboardContent() {

Common Topics

- +
+ +
diff --git a/components/MetricCard.tsx b/components/MetricCard.tsx index a3fe0f2..b8ff5c2 100644 --- a/components/MetricCard.tsx +++ b/components/MetricCard.tsx @@ -67,9 +67,6 @@ export default function MetricCard({ > {trend.isPositive !== false ? "↑" : "↓"}{" "} {Math.abs(trend.value).toFixed(1)}% - {trend.label && ( - {trend.label} - )} )} diff --git a/components/WordCloud.tsx b/components/WordCloud.tsx index 473fed9..9f3f807 100644 --- a/components/WordCloud.tsx +++ b/components/WordCloud.tsx @@ -11,20 +11,55 @@ interface WordCloudProps { }[]; width?: number; height?: number; + minWidth?: number; + minHeight?: number; } export default function WordCloud({ words, - width = 500, - height = 300, + width: initialWidth = 500, + height: initialHeight = 300, + minWidth = 200, + minHeight = 200, }: WordCloudProps) { const svgRef = useRef(null); + const containerRef = useRef(null); const [isClient, setIsClient] = useState(false); + const [dimensions, setDimensions] = useState({ + width: initialWidth, + height: initialHeight, + }); + // Set isClient to true on initial render useEffect(() => { setIsClient(true); }, []); + // Add effect to detect container size changes + useEffect(() => { + if (!containerRef.current || !isClient) return; + + // Create ResizeObserver to detect size changes + const resizeObserver = new ResizeObserver((entries) => { + for (const entry of entries) { + const { width, height } = entry.contentRect; + // Ensure minimum dimensions + const newWidth = Math.max(width, minWidth); + const newHeight = Math.max(height, minHeight); + setDimensions({ width: newWidth, height: newHeight }); + } + }); + + // Start observing the container + resizeObserver.observe(containerRef.current); + + // Cleanup + return () => { + resizeObserver.disconnect(); + }; + }, [isClient, minWidth, minHeight]); + + // Effect to render the word cloud whenever dimensions or words change useEffect(() => { if (!svgRef.current || !isClient || !words.length) return; @@ -36,7 +71,7 @@ export default function WordCloud({ // Configure the layout const layout = cloud() - .size([width, height]) + .size([dimensions.width, dimensions.height]) .words( words.map((d) => ({ text: d.text, @@ -53,7 +88,10 @@ export default function WordCloud({ function draw(words: Word[]) { svg .append("g") - .attr("transform", `translate(${width / 2},${height / 2})`) + .attr( + "transform", + `translate(${dimensions.width / 2},${dimensions.height / 2})` + ) .selectAll("text") .data(words) .enter() @@ -87,7 +125,7 @@ export default function WordCloud({ return () => { svg.selectAll("*").remove(); }; - }, [words, width, height, isClient]); + }, [words, dimensions, isClient]); if (!isClient) { return ( @@ -98,12 +136,21 @@ export default function WordCloud({ } return ( -
+
); diff --git a/lib/metrics.ts b/lib/metrics.ts index e06fca9..4e2b69e 100644 --- a/lib/metrics.ts +++ b/lib/metrics.ts @@ -13,295 +13,309 @@ interface CompanyConfig { sentimentAlert?: number; } +// Helper function to calculate trend percentages +function calculateTrendPercentage(current: number, previous: number): number { + if (previous === 0) return 0; // Avoid division by zero + return ((current - previous) / previous) * 100; +} + +// Mock data for previous period - in a real app, this would come from database +const mockPreviousPeriodData = { + totalSessions: 120, + uniqueUsers: 85, + avgSessionLength: 240, // in seconds + avgResponseTime: 1.7, // in seconds +}; + // List of common stop words - this can be expanded const stopWords = new Set([ "assistant", "user", // Web + "bmp", + "co", "com", - "www", - "http", - "https", - "www2", + "css", + "gif", "href", "html", - "php", - "js", - "css", - "xml", - "json", - "txt", - "jpg", - "jpeg", - "png", - "gif", - "bmp", - "svg", - "org", - "net", - "co", + "http", + "https", "io", + "jpeg", + "jpg", + "js", + "json", + "net", + "org", + "php", + "png", + "svg", + "txt", + "www", + "www2", + "xml", // English stop words "a", + "about", + "above", + "after", + "again", + "against", + "ain", + "all", + "am", "an", - "the", - "is", + "any", "are", - "was", - "were", + "aren", + "at", "be", "been", + "before", "being", - "have", - "has", - "had", - "do", - "does", - "did", - "will", - "would", - "should", + "below", + "between", + "both", + "by", + "bye", "can", "could", - "may", - "might", - "must", - "am", - "i", - "you", - "he", - "she", - "it", - "we", - "they", - "me", - "him", - "her", - "us", - "them", - "my", - "your", - "his", - "its", - "our", - "their", - "mine", - "yours", - "hers", - "ours", - "theirs", - "to", - "of", - "in", - "on", - "at", - "by", - "for", - "with", - "about", - "against", - "between", - "into", - "through", - "during", - "before", - "after", - "above", - "below", - "from", - "up", + "couldn", + "d", + "did", + "didn", + "do", + "does", + "doesn", + "don", "down", - "out", - "off", - "over", - "under", - "again", - "further", - "then", - "once", - "here", - "there", - "when", - "where", - "why", - "how", - "all", - "any", - "both", + "during", "each", "few", + "for", + "from", + "further", + "goodbye", + "had", + "hadn", + "has", + "hasn", + "have", + "haven", + "he", + "hello", + "her", + "here", + "hers", + "hi", + "him", + "his", + "how", + "i", + "in", + "into", + "is", + "isn", + "it", + "its", + "just", + "ll", + "m", + "ma", + "may", + "me", + "might", + "mightn", + "mine", "more", "most", - "other", - "some", - "such", + "must", + "mustn", + "my", + "needn", "no", "nor", "not", - "only", - "own", - "same", - "so", - "than", - "too", - "very", - "s", - "t", - "just", - "don", - "shouldve", "now", - "d", - "ll", - "m", "o", - "re", - "ve", - "y", - "ain", - "aren", - "couldn", - "didn", - "doesn", - "hadn", - "hasn", - "haven", - "isn", - "ma", - "mightn", - "mustn", - "needn", - "shan", - "shouldn", - "wasn", - "weren", - "won", - "wouldn", - "hi", - "hello", - "thanks", - "thank", - "please", + "of", + "off", "ok", "okay", - "yes", + "on", + "once", + "only", + "other", + "our", + "ours", + "out", + "over", + "own", + "please", + "re", + "s", + "same", + "shan", + "she", + "should", + "shouldn", + "shouldve", + "so", + "some", + "such", + "t", + "than", + "thank", + "thanks", + "the", + "their", + "theirs", + "them", + "then", + "there", + "they", + "through", + "to", + "too", + "under", + "up", + "us", + "ve", + "very", + "was", + "wasn", + "we", + "were", + "weren", + "when", + "where", + "why", + "will", + "with", + "won", + "would", + "wouldn", + "y", "yeah", - "bye", - "goodbye", + "yes", + "you", + "your", + "yours", // French stop words + "des", + "donc", + "et", "la", "le", "les", + "mais", + "ou", "un", "une", - "des", - "et", - "ou", - "mais", - "donc", // Dutch stop words - "dit", - "ben", - "de", - "het", - "ik", - "jij", - "hij", - "zij", - "wij", - "jullie", - "deze", - "dit", - "dat", - "die", - "een", - "en", - "of", - "maar", - "want", - "omdat", - "dus", - "als", - "ook", - "dan", - "nu", - "nog", - "al", - "naar", - "voor", - "van", - "door", - "met", - "bij", - "tot", - "om", - "over", - "tussen", - "onder", - "boven", - "tegen", "aan", - "uit", - "sinds", - "tijdens", - "binnen", - "buiten", - "zonder", - "volgens", - "dankzij", - "ondanks", - "behalve", - "mits", - "tenzij", - "hoewel", + "al", "alhoewel", - "toch", + "als", "anders", - "echter", - "wel", - "niet", - "geen", - "iets", - "niets", - "veel", - "weinig", - "meer", - "meest", - "elk", - "ieder", - "sommige", - "hoe", - "wat", - "waar", - "wie", - "wanneer", - "waarom", - "welke", - "wordt", - "worden", - "werd", - "werden", - "geworden", - "zijn", + "behalve", + "ben", "ben", "bent", - "was", - "waren", + "bij", + "binnen", + "boven", + "buiten", + "dan", + "dankzij", + "dat", + "de", + "deze", + "die", + "dit", + "dit", + "door", + "dus", + "echter", + "een", + "elk", + "en", + "geen", + "gehad", "geweest", - "hebben", - "heb", - "hebt", - "heeft", + "geworden", "had", "hadden", - "gehad", - "kunnen", + "heb", + "hebben", + "hebt", + "heeft", + "het", + "hij", + "hoe", + "hoewel", + "ieder", + "iets", + "ik", + "jij", + "jullie", "kan", - "kunt", "kon", "konden", - "zullen", + "kunnen", + "kunt", + "maar", + "meer", + "meest", + "met", + "mits", + "naar", + "niet", + "niets", + "nog", + "nu", + "of", + "om", + "omdat", + "ondanks", + "onder", + "ook", + "over", + "sinds", + "sommige", + "tegen", + "tenzij", + "tijdens", + "toch", + "tot", + "tussen", + "uit", + "van", + "veel", + "volgens", + "voor", + "waar", + "waarom", + "wanneer", + "want", + "waren", + "was", + "wat", + "weinig", + "wel", + "welke", + "werd", + "werden", + "wie", + "wij", + "worden", + "wordt", "zal", + "zij", + "zijn", + "zonder", + "zullen", "zult", // Add more domain-specific stop words if necessary ]); @@ -515,6 +529,24 @@ export function sessionMetrics( const avgSessionsPerDay = numDaysWithSessions > 0 ? totalSessions / numDaysWithSessions : 0; + // Calculate trends + const totalSessionsTrend = calculateTrendPercentage( + totalSessions, + mockPreviousPeriodData.totalSessions + ); + const uniqueUsersTrend = calculateTrendPercentage( + uniqueUsers, + mockPreviousPeriodData.uniqueUsers + ); + const avgSessionLengthTrend = calculateTrendPercentage( + avgSessionLength, + mockPreviousPeriodData.avgSessionLength + ); + const avgResponseTimeTrend = calculateTrendPercentage( + avgResponseTime, + mockPreviousPeriodData.avgResponseTime + ); + // console.log("Debug metrics calculation:", { // totalSessionDuration, // validSessionsForDuration, @@ -542,7 +574,16 @@ export function sessionMetrics( wordCloudData, belowThresholdCount: alerts, // Corrected to match MetricsResult interface (belowThresholdCount) avgSessionsPerDay, // Added to satisfy MetricsResult interface - // Optional fields from MetricsResult that are not yet calculated can be added here or handled by the consumer - // avgSentiment, sentimentThreshold, lastUpdated, sessionTrend, usersTrend, avgSessionTimeTrend, avgResponseTimeTrend + // Map trend values to the expected property names in MetricsResult + sessionTrend: totalSessionsTrend, + usersTrend: uniqueUsersTrend, + avgSessionTimeTrend: avgSessionLengthTrend, + // For response time, a negative trend is actually positive (faster responses are better) + avgResponseTimeTrend: -avgResponseTimeTrend, // Invert as lower response time is better + // Additional fields + sentimentThreshold: companyConfig.sentimentAlert, + lastUpdated: Date.now(), + totalSessionDuration, + validSessionsForDuration, }; }