Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions apps/web/app/(app)/[emailAccountId]/stats/BarListCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,19 @@ import { cn } from "@/utils";

interface BarListCardProps {
tabs: { id: string; label: string; data: any }[]; // TODO: add type
icon: React.ReactNode;
icon?: React.ReactNode;
title: string;
onItemClick?: (item: any) => void;
hideIcons?: boolean;
}

export function BarListCard({ tabs, icon, title }: BarListCardProps) {
export function BarListCard({
tabs,
icon,
title,
onItemClick,
hideIcons,
}: BarListCardProps) {
const [selected, setSelected] = useState<string | null>(
tabs?.length > 0 ? tabs[0]?.id : null,
);
Expand All @@ -35,7 +43,7 @@ export function BarListCard({ tabs, icon, title }: BarListCardProps) {
selected={selected}
/>
<div className="flex items-center gap-2">
{icon}
{icon && icon}
<p className="text-xs text-neutral-500">{title.toUpperCase()}</p>
</div>
</div>
Expand All @@ -49,6 +57,8 @@ export function BarListCard({ tabs, icon, title }: BarListCardProps) {
/>
<HorizontalBarChart
data={tabs.find((d) => d.id === selected)?.data || []}
onItemClick={onItemClick}
hideIcon={hideIcons}
/>
<div className="absolute w-full left-0 bottom-0 pb-6 z-30">
{tabs.find((d) => d.id === selected)?.data.length > 0 && (
Expand All @@ -71,6 +81,8 @@ export function BarListCard({ tabs, icon, title }: BarListCardProps) {
<div className="max-h-[60vh] overflow-y-auto p-6">
<HorizontalBarChart
data={tabs.find((d) => d.id === selected)?.data || []}
onItemClick={onItemClick}
hideIcon={hideIcons}
/>
</div>
</DialogContent>
Expand Down
4 changes: 4 additions & 0 deletions apps/web/app/(app)/[emailAccountId]/stats/Stats.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { ActionBar } from "@/app/(app)/[emailAccountId]/stats/ActionBar";
import { DetailedStatsFilter } from "@/app/(app)/[emailAccountId]/stats/DetailedStatsFilter";
import { LayoutGrid } from "lucide-react";
import { DatePickerWithRange } from "@/components/DatePickerWithRange";
import { TopicDistribution } from "@/app/(app)/[emailAccountId]/stats/TopicDistribution";

const selectOptions = [
{ label: "Last week", value: "7" },
Expand Down Expand Up @@ -122,6 +123,9 @@ export function Stats() {
dateRange={dateRange}
refreshInterval={refreshInterval}
/>
<div className="grid grid-cols-2 gap-4">
<TopicDistribution />
</div>
<RuleStatsChart
dateRange={dateRange}
title="Assistant processed emails"
Expand Down
154 changes: 154 additions & 0 deletions apps/web/app/(app)/[emailAccountId]/stats/TopicDistribution.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
"use client";

import { useState } from "react";
import { MessageSquare } from "lucide-react";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { NewBarChart } from "@/app/(app)/[emailAccountId]/stats/NewBarChart";
import { format, subDays } from "date-fns";
import { COLORS } from "@/utils/colors";
import { BarListCard } from "@/app/(app)/[emailAccountId]/stats/BarListCard";

// Mock data for demo
const MOCK_TOPICS = [
{
topic: "Customer Support",
count: 342,
chartColor: COLORS.analytics.blue,
emoji: "💬",
},
{
topic: "Product Updates",
count: 256,
chartColor: COLORS.analytics.purple,
emoji: "🚀",
},
{
topic: "Billing & Payments",
count: 189,
chartColor: COLORS.analytics.green,
emoji: "💰",
},
{
topic: "Feature Requests",
count: 167,
chartColor: COLORS.analytics.lightGreen,
emoji: "✨",
},
{
topic: "Bug Reports",
count: 134,
chartColor: COLORS.analytics.pink,
emoji: "🐛",
},
{
topic: "Sales Inquiries",
count: 89,
chartColor: COLORS.analytics.lightPink,
emoji: "📈",
},
{ topic: "General Questions", count: 45, chartColor: "#94A3B8", emoji: "❓" },
];

// Generate mock daily data for past 30 days
function generateDailyData(topicCount: number) {
const data = [];
const now = new Date();

for (let i = 29; i >= 0; i--) {
const date = subDays(now, i);
// Generate semi-realistic data with some variance
const baseCount = topicCount / 30;
const variance = Math.random() * baseCount * 0.8;
const count = Math.max(
0,
Math.round(baseCount + variance - baseCount * 0.4),
);

data.push({
date: format(date, "yyyy-MM-dd"),
count: count,
});
}

return data;
}

export function TopicDistribution() {
const [selectedTopic, setSelectedTopic] = useState<
(typeof MOCK_TOPICS)[0] | null
>(null);

const handleTopicClick = (item: { name: string; value: number }) => {
const topic = MOCK_TOPICS.find((t) => t.topic === item.name);
if (topic) {
setSelectedTopic(topic);
}
};

const tabs = [
{
id: "common-topics",
label: "Common Topics",
data: MOCK_TOPICS.map((topic) => ({
name: topic.topic,
value: topic.count,
icon: topic.emoji,
})),
},
];

return (
<>
<BarListCard
tabs={tabs}
icon={<MessageSquare className="h-4 w-4 text-neutral-500" />}
title="Topics"
onItemClick={handleTopicClick}
/>

<Dialog
open={!!selectedTopic}
onOpenChange={() => setSelectedTopic(null)}
>
<DialogContent className="max-w-3xl">
{selectedTopic && (
<>
<DialogHeader>
<DialogTitle>{selectedTopic.topic}</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div className="flex items-center gap-6">
<div>
<p className="text-sm text-gray-500">Total (30 days)</p>
<p className="text-2xl font-bold">
{selectedTopic.count.toLocaleString()}
</p>
</div>
<div>
<p className="text-sm text-gray-500">Average per day</p>
<p className="text-2xl font-bold">
{Math.round(selectedTopic.count / 30).toLocaleString()}
</p>
</div>
</div>
<NewBarChart
data={generateDailyData(selectedTopic.count)}
config={{
count: { label: "Emails", color: selectedTopic.chartColor },
}}
xAxisKey="date"
period="day"
/>
</div>
</>
)}
</DialogContent>
</Dialog>
</>
);
}
82 changes: 62 additions & 20 deletions apps/web/components/charts/HorizontalBarChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,26 @@
import { DomainIcon } from "@/components/charts/DomainIcon";
import { cn } from "@/utils";

export interface HorizontalBarChartItem {
name: string;
value: number;
href?: string;
target?: string;
icon?: string;
}

interface HorizontalBarChartProps {
data: Array<{
name: string;
value: number;
href?: string;
target?: string;
}>;
data: Array<HorizontalBarChartItem>;
className?: string;
onItemClick?: (item: HorizontalBarChartItem) => void;
hideIcon?: boolean;
}

export function HorizontalBarChart({
data,
className,
onItemClick,
hideIcon,
}: HorizontalBarChartProps) {
const maxValue = Math.max(...data.map((item) => item.value), 1);

Expand All @@ -27,26 +34,39 @@ export function HorizontalBarChart({
? item.name.split("@")[1]
: item.name;

return (
<div
key={item.name}
className="flex items-center justify-between gap-4"
>
const content = (
<>
<div className="flex-1 min-w-0">
<div className="px-3 py-2 relative">
<div
className="absolute top-0 left-0 bg-gradient-to-r from-blue-100 to-blue-50 h-full rounded-md"
style={{ width: `${widthPercentage}%` }}
/>
<div className="flex items-center gap-2">
<DomainIcon domain={domain} />
<a
href={item.href}
target={item.target}
className="text-sm text-gray-900 truncate block z-10 relative hover:underline"
>
{item.name}
</a>
<div className="flex items-center gap-2 relative z-10">
{!hideIcon &&
(item.icon ? (
<span className="text-base">{item.icon}</span>
) : (
<DomainIcon domain={domain} />
))}
{item.href ? (
<a
href={item.href}
target={item.target}
className="text-sm text-gray-900 truncate block z-10 relative hover:underline"
>
{item.name}
</a>
) : (
<span
className={cn(
"text-sm text-gray-900 truncate block z-10 relative",
onItemClick && "group-hover:underline",
)}
>
{item.name}
</span>
)}
</div>
</div>
</div>
Expand All @@ -55,6 +75,28 @@ export function HorizontalBarChart({
{item.value.toLocaleString()}
</span>
</div>
</>
);

if (onItemClick) {
return (
<button
key={item.name}
type="button"
className="w-full flex items-center justify-between gap-4 group"
onClick={() => onItemClick(item)}
>
{content}
</button>
);
}

return (
<div
key={item.name}
className="flex items-center justify-between gap-4"
>
{content}
</div>
);
})}
Expand Down