mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-10-09 12:47:13 +02:00
Add persona stats (#3282)
* Added a chart to display persona message stats * polish * k * hope this works * cleanup
This commit is contained in:
@@ -170,3 +170,67 @@ def fetch_danswerbot_analytics(
|
|||||||
)
|
)
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_persona_message_analytics(
|
||||||
|
db_session: Session,
|
||||||
|
persona_id: int,
|
||||||
|
start: datetime.datetime,
|
||||||
|
end: datetime.datetime,
|
||||||
|
) -> list[tuple[int, datetime.date]]:
|
||||||
|
"""Gets the daily message counts for a specific persona within the given time range."""
|
||||||
|
query = (
|
||||||
|
select(
|
||||||
|
func.count(ChatMessage.id),
|
||||||
|
cast(ChatMessage.time_sent, Date),
|
||||||
|
)
|
||||||
|
.join(
|
||||||
|
ChatSession,
|
||||||
|
ChatMessage.chat_session_id == ChatSession.id,
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
or_(
|
||||||
|
ChatMessage.alternate_assistant_id == persona_id,
|
||||||
|
ChatSession.persona_id == persona_id,
|
||||||
|
),
|
||||||
|
ChatMessage.time_sent >= start,
|
||||||
|
ChatMessage.time_sent <= end,
|
||||||
|
ChatMessage.message_type == MessageType.ASSISTANT,
|
||||||
|
)
|
||||||
|
.group_by(cast(ChatMessage.time_sent, Date))
|
||||||
|
.order_by(cast(ChatMessage.time_sent, Date))
|
||||||
|
)
|
||||||
|
|
||||||
|
return [tuple(row) for row in db_session.execute(query).all()]
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_persona_unique_users(
|
||||||
|
db_session: Session,
|
||||||
|
persona_id: int,
|
||||||
|
start: datetime.datetime,
|
||||||
|
end: datetime.datetime,
|
||||||
|
) -> list[tuple[int, datetime.date]]:
|
||||||
|
"""Gets the daily unique user counts for a specific persona within the given time range."""
|
||||||
|
query = (
|
||||||
|
select(
|
||||||
|
func.count(func.distinct(ChatSession.user_id)),
|
||||||
|
cast(ChatMessage.time_sent, Date),
|
||||||
|
)
|
||||||
|
.join(
|
||||||
|
ChatSession,
|
||||||
|
ChatMessage.chat_session_id == ChatSession.id,
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
or_(
|
||||||
|
ChatMessage.alternate_assistant_id == persona_id,
|
||||||
|
ChatSession.persona_id == persona_id,
|
||||||
|
),
|
||||||
|
ChatMessage.time_sent >= start,
|
||||||
|
ChatMessage.time_sent <= end,
|
||||||
|
ChatMessage.message_type == MessageType.ASSISTANT,
|
||||||
|
)
|
||||||
|
.group_by(cast(ChatMessage.time_sent, Date))
|
||||||
|
.order_by(cast(ChatMessage.time_sent, Date))
|
||||||
|
)
|
||||||
|
|
||||||
|
return [tuple(row) for row in db_session.execute(query).all()]
|
||||||
|
@@ -11,11 +11,16 @@ from danswer.db.engine import get_session
|
|||||||
from danswer.db.models import User
|
from danswer.db.models import User
|
||||||
from ee.danswer.db.analytics import fetch_danswerbot_analytics
|
from ee.danswer.db.analytics import fetch_danswerbot_analytics
|
||||||
from ee.danswer.db.analytics import fetch_per_user_query_analytics
|
from ee.danswer.db.analytics import fetch_per_user_query_analytics
|
||||||
|
from ee.danswer.db.analytics import fetch_persona_message_analytics
|
||||||
|
from ee.danswer.db.analytics import fetch_persona_unique_users
|
||||||
from ee.danswer.db.analytics import fetch_query_analytics
|
from ee.danswer.db.analytics import fetch_query_analytics
|
||||||
|
|
||||||
router = APIRouter(prefix="/analytics")
|
router = APIRouter(prefix="/analytics")
|
||||||
|
|
||||||
|
|
||||||
|
_DEFAULT_LOOKBACK_DAYS = 30
|
||||||
|
|
||||||
|
|
||||||
class QueryAnalyticsResponse(BaseModel):
|
class QueryAnalyticsResponse(BaseModel):
|
||||||
total_queries: int
|
total_queries: int
|
||||||
total_likes: int
|
total_likes: int
|
||||||
@@ -33,7 +38,7 @@ def get_query_analytics(
|
|||||||
daily_query_usage_info = fetch_query_analytics(
|
daily_query_usage_info = fetch_query_analytics(
|
||||||
start=start
|
start=start
|
||||||
or (
|
or (
|
||||||
datetime.datetime.utcnow() - datetime.timedelta(days=30)
|
datetime.datetime.utcnow() - datetime.timedelta(days=_DEFAULT_LOOKBACK_DAYS)
|
||||||
), # default is 30d lookback
|
), # default is 30d lookback
|
||||||
end=end or datetime.datetime.utcnow(),
|
end=end or datetime.datetime.utcnow(),
|
||||||
db_session=db_session,
|
db_session=db_session,
|
||||||
@@ -64,7 +69,7 @@ def get_user_analytics(
|
|||||||
daily_query_usage_info_per_user = fetch_per_user_query_analytics(
|
daily_query_usage_info_per_user = fetch_per_user_query_analytics(
|
||||||
start=start
|
start=start
|
||||||
or (
|
or (
|
||||||
datetime.datetime.utcnow() - datetime.timedelta(days=30)
|
datetime.datetime.utcnow() - datetime.timedelta(days=_DEFAULT_LOOKBACK_DAYS)
|
||||||
), # default is 30d lookback
|
), # default is 30d lookback
|
||||||
end=end or datetime.datetime.utcnow(),
|
end=end or datetime.datetime.utcnow(),
|
||||||
db_session=db_session,
|
db_session=db_session,
|
||||||
@@ -98,7 +103,7 @@ def get_danswerbot_analytics(
|
|||||||
daily_danswerbot_info = fetch_danswerbot_analytics(
|
daily_danswerbot_info = fetch_danswerbot_analytics(
|
||||||
start=start
|
start=start
|
||||||
or (
|
or (
|
||||||
datetime.datetime.utcnow() - datetime.timedelta(days=30)
|
datetime.datetime.utcnow() - datetime.timedelta(days=_DEFAULT_LOOKBACK_DAYS)
|
||||||
), # default is 30d lookback
|
), # default is 30d lookback
|
||||||
end=end or datetime.datetime.utcnow(),
|
end=end or datetime.datetime.utcnow(),
|
||||||
db_session=db_session,
|
db_session=db_session,
|
||||||
@@ -115,3 +120,74 @@ def get_danswerbot_analytics(
|
|||||||
]
|
]
|
||||||
|
|
||||||
return resolution_results
|
return resolution_results
|
||||||
|
|
||||||
|
|
||||||
|
class PersonaMessageAnalyticsResponse(BaseModel):
|
||||||
|
total_messages: int
|
||||||
|
date: datetime.date
|
||||||
|
persona_id: int
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/admin/persona/messages")
|
||||||
|
def get_persona_messages(
|
||||||
|
persona_id: int,
|
||||||
|
start: datetime.datetime | None = None,
|
||||||
|
end: datetime.datetime | None = None,
|
||||||
|
_: User | None = Depends(current_admin_user),
|
||||||
|
db_session: Session = Depends(get_session),
|
||||||
|
) -> list[PersonaMessageAnalyticsResponse]:
|
||||||
|
"""Fetch daily message counts for a single persona within the given time range."""
|
||||||
|
start = start or (
|
||||||
|
datetime.datetime.utcnow() - datetime.timedelta(days=_DEFAULT_LOOKBACK_DAYS)
|
||||||
|
)
|
||||||
|
end = end or datetime.datetime.utcnow()
|
||||||
|
|
||||||
|
persona_message_counts = []
|
||||||
|
for count, date in fetch_persona_message_analytics(
|
||||||
|
db_session=db_session,
|
||||||
|
persona_id=persona_id,
|
||||||
|
start=start,
|
||||||
|
end=end,
|
||||||
|
):
|
||||||
|
persona_message_counts.append(
|
||||||
|
PersonaMessageAnalyticsResponse(
|
||||||
|
total_messages=count,
|
||||||
|
date=date,
|
||||||
|
persona_id=persona_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return persona_message_counts
|
||||||
|
|
||||||
|
|
||||||
|
class PersonaUniqueUsersResponse(BaseModel):
|
||||||
|
unique_users: int
|
||||||
|
date: datetime.date
|
||||||
|
persona_id: int
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/admin/persona/unique-users")
|
||||||
|
def get_persona_unique_users(
|
||||||
|
persona_id: int,
|
||||||
|
start: datetime.datetime,
|
||||||
|
end: datetime.datetime,
|
||||||
|
_: User | None = Depends(current_admin_user),
|
||||||
|
db_session: Session = Depends(get_session),
|
||||||
|
) -> list[PersonaUniqueUsersResponse]:
|
||||||
|
"""Get unique users per day for a single persona."""
|
||||||
|
unique_user_counts = []
|
||||||
|
daily_counts = fetch_persona_unique_users(
|
||||||
|
db_session=db_session,
|
||||||
|
persona_id=persona_id,
|
||||||
|
start=start,
|
||||||
|
end=end,
|
||||||
|
)
|
||||||
|
for count, date in daily_counts:
|
||||||
|
unique_user_counts.append(
|
||||||
|
PersonaUniqueUsersResponse(
|
||||||
|
unique_users=count,
|
||||||
|
date=date,
|
||||||
|
persona_id=persona_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return unique_user_counts
|
||||||
|
@@ -97,3 +97,69 @@ export function getDatesList(startDate: Date): string[] {
|
|||||||
|
|
||||||
return datesList;
|
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),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
231
web/src/app/ee/admin/performance/usage/PersonaMessagesChart.tsx
Normal file
231
web/src/app/ee/admin/performance/usage/PersonaMessagesChart.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
@@ -4,6 +4,7 @@ import { DateRangeSelector } from "../DateRangeSelector";
|
|||||||
import { DanswerBotChart } from "./DanswerBotChart";
|
import { DanswerBotChart } from "./DanswerBotChart";
|
||||||
import { FeedbackChart } from "./FeedbackChart";
|
import { FeedbackChart } from "./FeedbackChart";
|
||||||
import { QueryPerformanceChart } from "./QueryPerformanceChart";
|
import { QueryPerformanceChart } from "./QueryPerformanceChart";
|
||||||
|
import { PersonaMessagesChart } from "./PersonaMessagesChart";
|
||||||
import { useTimeRange } from "../lib";
|
import { useTimeRange } from "../lib";
|
||||||
import { AdminPageTitle } from "@/components/admin/Title";
|
import { AdminPageTitle } from "@/components/admin/Title";
|
||||||
import { FiActivity } from "react-icons/fi";
|
import { FiActivity } from "react-icons/fi";
|
||||||
@@ -26,6 +27,7 @@ export default function AnalyticsPage() {
|
|||||||
<QueryPerformanceChart timeRange={timeRange} />
|
<QueryPerformanceChart timeRange={timeRange} />
|
||||||
<FeedbackChart timeRange={timeRange} />
|
<FeedbackChart timeRange={timeRange} />
|
||||||
<DanswerBotChart timeRange={timeRange} />
|
<DanswerBotChart timeRange={timeRange} />
|
||||||
|
<PersonaMessagesChart timeRange={timeRange} />
|
||||||
<Separator />
|
<Separator />
|
||||||
<UsageReports />
|
<UsageReports />
|
||||||
</main>
|
</main>
|
||||||
|
Reference in New Issue
Block a user