Add persona stats (#3282)

* Added a chart to display persona message stats

* polish

* k

* hope this works

* cleanup
This commit is contained in:
hagen-danswer
2024-12-05 09:15:56 -08:00
committed by GitHub
parent c81e704c95
commit 14772dee71
5 changed files with 442 additions and 3 deletions

View File

@@ -97,3 +97,69 @@ export function getDatesList(startDate: Date): string[] {
return datesList;
}
export interface PersonaMessageAnalytics {
total_messages: number;
date: string;
persona_id: number;
}
export interface PersonaSnapshot {
id: number;
name: string;
description: string;
is_visible: boolean;
is_public: boolean;
}
export const usePersonaMessages = (
personaId: number | undefined,
timeRange: DateRangePickerValue
) => {
const url = buildApiPath(`/api/analytics/admin/persona/messages`, {
persona_id: personaId?.toString(),
start: convertDateToStartOfDay(timeRange.from)?.toISOString(),
end: convertDateToEndOfDay(timeRange.to)?.toISOString(),
});
const { data, error, isLoading } = useSWR<PersonaMessageAnalytics[]>(
personaId !== undefined ? url : null,
errorHandlingFetcher
);
return {
data,
error,
isLoading,
refreshPersonaMessages: () => mutate(url),
};
};
export interface PersonaUniqueUserAnalytics {
unique_users: number;
date: string;
persona_id: number;
}
export const usePersonaUniqueUsers = (
personaId: number | undefined,
timeRange: DateRangePickerValue
) => {
const url = buildApiPath(`/api/analytics/admin/persona/unique-users`, {
persona_id: personaId?.toString(),
start: convertDateToStartOfDay(timeRange.from)?.toISOString(),
end: convertDateToEndOfDay(timeRange.to)?.toISOString(),
});
const { data, error, isLoading } = useSWR<PersonaUniqueUserAnalytics[]>(
personaId !== undefined ? url : null,
errorHandlingFetcher
);
return {
data,
error,
isLoading,
refreshPersonaUniqueUsers: () => mutate(url),
};
};

View File

@@ -0,0 +1,231 @@
import { ThreeDotsLoader } from "@/components/Loading";
import { X, Search } from "lucide-react";
import {
getDatesList,
usePersonaMessages,
usePersonaUniqueUsers,
} from "../lib";
import { useAssistants } from "@/components/context/AssistantsContext";
import { DateRangePickerValue } from "@/app/ee/admin/performance/DateRangeSelector";
import Text from "@/components/ui/text";
import Title from "@/components/ui/title";
import CardSection from "@/components/admin/CardSection";
import { AreaChartDisplay } from "@/components/ui/areaChart";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { useState, useMemo, useEffect } from "react";
export function PersonaMessagesChart({
timeRange,
}: {
timeRange: DateRangePickerValue;
}) {
const [selectedPersonaId, setSelectedPersonaId] = useState<
number | undefined
>(undefined);
const [searchQuery, setSearchQuery] = useState("");
const [highlightedIndex, setHighlightedIndex] = useState(-1);
const { allAssistants: personaList } = useAssistants();
const {
data: personaMessagesData,
isLoading: isPersonaMessagesLoading,
error: personaMessagesError,
} = usePersonaMessages(selectedPersonaId, timeRange);
const {
data: personaUniqueUsersData,
isLoading: isPersonaUniqueUsersLoading,
error: personaUniqueUsersError,
} = usePersonaUniqueUsers(selectedPersonaId, timeRange);
const isLoading = isPersonaMessagesLoading || isPersonaUniqueUsersLoading;
const hasError = personaMessagesError || personaUniqueUsersError;
const filteredPersonaList = useMemo(() => {
if (!personaList) return [];
return personaList.filter((persona) =>
persona.name.toLowerCase().includes(searchQuery.toLowerCase())
);
}, [personaList, searchQuery]);
const handleKeyDown = (e: React.KeyboardEvent) => {
e.stopPropagation();
switch (e.key) {
case "ArrowDown":
e.preventDefault();
setHighlightedIndex((prev) =>
prev < filteredPersonaList.length - 1 ? prev + 1 : prev
);
break;
case "ArrowUp":
e.preventDefault();
setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : prev));
break;
case "Enter":
if (
highlightedIndex >= 0 &&
highlightedIndex < filteredPersonaList.length
) {
setSelectedPersonaId(filteredPersonaList[highlightedIndex].id);
setSearchQuery("");
setHighlightedIndex(-1);
}
break;
case "Escape":
setSearchQuery("");
setHighlightedIndex(-1);
break;
}
};
// Reset highlight when search query changes
useEffect(() => {
setHighlightedIndex(-1);
}, [searchQuery]);
const chartData = useMemo(() => {
if (
!personaMessagesData?.length ||
!personaUniqueUsersData?.length ||
selectedPersonaId === undefined
) {
return null;
}
const initialDate =
timeRange.from ||
new Date(
Math.min(
...personaMessagesData.map((entry) => new Date(entry.date).getTime())
)
);
const dateRange = getDatesList(initialDate);
// Create maps for messages and unique users data
const messagesMap = new Map(
personaMessagesData.map((entry) => [entry.date, entry])
);
const uniqueUsersMap = new Map(
personaUniqueUsersData.map((entry) => [entry.date, entry])
);
return dateRange.map((dateStr) => {
const messageData = messagesMap.get(dateStr);
const uniqueUserData = uniqueUsersMap.get(dateStr);
return {
Day: dateStr,
Messages: messageData?.total_messages || 0,
"Unique Users": uniqueUserData?.unique_users || 0,
};
});
}, [
personaMessagesData,
personaUniqueUsersData,
timeRange.from,
selectedPersonaId,
]);
let content;
if (isLoading) {
content = (
<div className="h-80 flex flex-col">
<ThreeDotsLoader />
</div>
);
} else if (!personaList || hasError) {
content = (
<div className="h-80 text-red-600 text-bold flex flex-col">
<p className="m-auto">Failed to fetch data...</p>
</div>
);
} else if (selectedPersonaId === undefined) {
content = (
<div className="h-80 text-gray-500 flex flex-col">
<p className="m-auto">Select a persona to view analytics</p>
</div>
);
} else if (!personaMessagesData?.length) {
content = (
<div className="h-80 text-gray-500 flex flex-col">
<p className="m-auto">
No data found for selected persona in the selected time range
</p>
</div>
);
} else if (chartData) {
content = (
<AreaChartDisplay
className="mt-4"
data={chartData}
categories={["Messages", "Unique Users"]}
index="Day"
colors={["indigo", "fuchsia"]}
yAxisWidth={60}
/>
);
}
const selectedPersona = personaList?.find((p) => p.id === selectedPersonaId);
return (
<CardSection className="mt-8">
<Title>Persona Analytics</Title>
<div className="flex flex-col gap-4">
<Text>Messages and unique users per day for selected persona</Text>
<div className="flex items-center gap-4">
<Select
value={selectedPersonaId?.toString() ?? ""}
onValueChange={(value) => {
setSelectedPersonaId(parseInt(value));
}}
>
<SelectTrigger className="flex w-full max-w-xs">
<SelectValue placeholder="Select a persona to display" />
</SelectTrigger>
<SelectContent>
<div className="flex items-center px-2 pb-2 sticky top-0 bg-background border-b">
<Search className="h-4 w-4 mr-2 shrink-0 opacity-50" />
<input
className="flex h-8 w-full rounded-sm bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50"
placeholder="Search personas..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onClick={(e) => e.stopPropagation()}
onMouseDown={(e) => e.stopPropagation()}
onKeyDown={handleKeyDown}
/>
{searchQuery && (
<X
className="h-4 w-4 shrink-0 opacity-50 cursor-pointer hover:opacity-100"
onClick={() => {
setSearchQuery("");
setHighlightedIndex(-1);
}}
/>
)}
</div>
{filteredPersonaList.map((persona, index) => (
<SelectItem
key={persona.id}
value={persona.id.toString()}
className={`${highlightedIndex === index ? "hover" : ""}`}
onMouseEnter={() => setHighlightedIndex(index)}
>
{persona.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
{content}
</CardSection>
);
}

View File

@@ -4,6 +4,7 @@ import { DateRangeSelector } from "../DateRangeSelector";
import { DanswerBotChart } from "./DanswerBotChart";
import { FeedbackChart } from "./FeedbackChart";
import { QueryPerformanceChart } from "./QueryPerformanceChart";
import { PersonaMessagesChart } from "./PersonaMessagesChart";
import { useTimeRange } from "../lib";
import { AdminPageTitle } from "@/components/admin/Title";
import { FiActivity } from "react-icons/fi";
@@ -26,6 +27,7 @@ export default function AnalyticsPage() {
<QueryPerformanceChart timeRange={timeRange} />
<FeedbackChart timeRange={timeRange} />
<DanswerBotChart timeRange={timeRange} />
<PersonaMessagesChart timeRange={timeRange} />
<Separator />
<UsageReports />
</main>