-
Notifications
You must be signed in to change notification settings - Fork 0
Dashboard: Analytics Dashboard Page #183
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
bc3986f
feat(*): analytics dashboard page
Ayush8923 221a34c
fix(metrics): added the totals summary
Ayush8923 48032df
fix(analytics): implement the changes as per the suggestion
Ayush8923 23aaf45
fix(analytics): cleanups
Ayush8923 2131a90
fix(analytics): cleanups
Ayush8923 4cf661d
fix(analytics): move the types inside the analytics types files
Ayush8923 84dea98
Merge branch 'main' of https://github.com/ProjectTech4DevAI/kaapi-fro…
Ayush8923 535c59d
fix(dashboard): few cleanups
Ayush8923 dc840b8
feat(analytics): UI improvement and added the pie chart graph
Ayush8923 1f32bf7
fix(analytics): formatting
Ayush8923 efee2ef
fix(analtics): change the page header title
Ayush8923 355aefc
fix(analytics): cleanups and added the info tooltip
Ayush8923 9254913
fix(analytics): define types
Ayush8923 3351a5e
fix(guardrails): few updates and ui alignment
Ayush8923 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,251 @@ | ||
| "use client"; | ||
|
|
||
| import { useMemo, useState } from "react"; | ||
| import { PageHeader, Sidebar } from "@/app/components"; | ||
| import { InfoTooltip, Select } from "@/app/components/ui"; | ||
| import { toSelectOptions } from "@/app/lib/utils/selectOptions"; | ||
| import { useApp } from "@/app/lib/context/AppContext"; | ||
| import { useAuth } from "@/app/lib/context/AuthContext"; | ||
| import { useAnalyticsChart } from "@/app/hooks/useAnalyticsChart"; | ||
| import { useAnalyticsTotals } from "@/app/hooks/useAnalyticsTotals"; | ||
| import { | ||
| AnalyticsChartCard, | ||
| AnalyticsTotalsRow, | ||
| MonthYearPicker, | ||
| } from "@/app/components/analytics"; | ||
| import { | ||
| ANALYTICS_GROUP_BY_OPTIONS, | ||
| ANALYTICS_METRIC_OPTIONS, | ||
| ANALYTICS_MODALITY_OPTIONS, | ||
| PROVIDES_OPTIONS, | ||
| } from "@/app/lib/constants"; | ||
| import { | ||
| AnalyticsChartFilters, | ||
| AnalyticsGroupBy, | ||
| AnalyticsMetric, | ||
| AnalyticsModality, | ||
| } from "@/app/lib/types/analytics"; | ||
|
|
||
| function FilterField({ | ||
| label, | ||
| className, | ||
| info, | ||
| children, | ||
| }: { | ||
| label: string; | ||
| className?: string; | ||
| info?: React.ReactNode; | ||
| children: React.ReactNode; | ||
| }) { | ||
| return ( | ||
| <div className={className}> | ||
| <label className="flex items-center text-[12px] font-medium mb-1 text-text-secondary"> | ||
| <span>{label}</span> | ||
| {info && <InfoTooltip text={info} />} | ||
| </label> | ||
| {children} | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| export default function AnalyticsPage() { | ||
| const { sidebarCollapsed } = useApp(); | ||
| const { isAuthenticated, isHydrated } = useAuth(); | ||
| const [metric, setMetric] = useState<AnalyticsMetric>("cost_all"); | ||
| const [groupBy, setGroupBy] = useState<AnalyticsGroupBy>("provider"); | ||
| const [modality, setModality] = useState<AnalyticsModality | "">(""); | ||
| const [provider, setProvider] = useState(""); | ||
| const [fromMonth, setFromMonth] = useState(""); | ||
| const [toMonth, setToMonth] = useState(""); | ||
|
|
||
| const filters: AnalyticsChartFilters = useMemo( | ||
| () => ({ | ||
| metric, | ||
| group_by: groupBy, | ||
| modality: modality || undefined, | ||
| provider: provider || undefined, | ||
| from_month: fromMonth || undefined, | ||
| to_month: toMonth || undefined, | ||
| }), | ||
| [metric, groupBy, modality, provider, fromMonth, toMonth], | ||
| ); | ||
|
|
||
| const { data, isLoading, error } = useAnalyticsChart(filters); | ||
|
|
||
| const totalsFilters = useMemo( | ||
| () => ({ | ||
| modality: modality || undefined, | ||
| provider: provider || undefined, | ||
| from_month: fromMonth || undefined, | ||
| to_month: toMonth || undefined, | ||
| }), | ||
| [modality, provider, fromMonth, toMonth], | ||
| ); | ||
| const { | ||
| totals, | ||
| isLoading: isTotalsLoading, | ||
| error: totalsError, | ||
| } = useAnalyticsTotals(totalsFilters); | ||
|
|
||
| const metricLabel = | ||
| ANALYTICS_METRIC_OPTIONS.find((m) => m.value === metric)?.label ?? metric; | ||
|
|
||
| const isReady = isHydrated; | ||
|
|
||
| return ( | ||
| <div className="w-full h-screen flex flex-col bg-bg-secondary"> | ||
| <div className="flex flex-1 overflow-hidden"> | ||
| <Sidebar collapsed={sidebarCollapsed} activeRoute="/analytics" /> | ||
|
|
||
| <div className="flex-1 flex flex-col overflow-hidden bg-bg-primary"> | ||
| <PageHeader | ||
| title="Analytics" | ||
| subtitle="Track cost, request volume and token usage across providers, modalities and projects" | ||
| /> | ||
|
|
||
| {!isReady ? null : !isAuthenticated ? ( | ||
| <div className="flex-1 flex items-center justify-center px-6"> | ||
| <p className="text-sm text-text-secondary"> | ||
| Log in to view analytics. | ||
| </p> | ||
| </div> | ||
| ) : ( | ||
| <div className="flex-1 overflow-y-auto"> | ||
| <div className="sticky top-0 z-10 bg-[#FAFAFA] border-b border-border px-4 sm:px-6 py-3"> | ||
| <div className="flex flex-wrap lg:flex-nowrap items-end gap-x-3 gap-y-2"> | ||
| <FilterField | ||
| label="Metric" | ||
| className="flex-1 basis-40 min-w-40" | ||
| info={ | ||
| <> | ||
| What the chart shows. | ||
| <br /> | ||
| <strong>Cost</strong> = money spent on AI (real users + | ||
| quality checks combined). <strong>Volume</strong> = how | ||
| busy your AI was (number of requests). | ||
| </> | ||
| } | ||
| > | ||
| <Select | ||
| value={metric} | ||
| onChange={(e) => | ||
| setMetric(e.target.value as AnalyticsMetric) | ||
| } | ||
| options={toSelectOptions(ANALYTICS_METRIC_OPTIONS)} | ||
| /> | ||
| </FilterField> | ||
| <FilterField | ||
| label="Group by" | ||
| className="flex-1 basis-40 min-w-40" | ||
| info={ | ||
| <> | ||
| How the chart is split up. | ||
| <br /> | ||
| <strong>Total</strong> = everything as one line. Pick{" "} | ||
| <strong>Provider</strong> (who supplies the AI) or{" "} | ||
| <strong>Request type</strong> (what the AI was used for) | ||
| to see one line per group. | ||
| </> | ||
| } | ||
| > | ||
| <Select | ||
| value={groupBy} | ||
| onChange={(e) => | ||
| setGroupBy(e.target.value as AnalyticsGroupBy) | ||
| } | ||
| options={toSelectOptions(ANALYTICS_GROUP_BY_OPTIONS)} | ||
| /> | ||
| </FilterField> | ||
| <FilterField | ||
| label="Request type" | ||
| className="flex-1 basis-40 min-w-40" | ||
| info={ | ||
| <> | ||
| What the AI was used for. Pick one to see numbers for | ||
| just that kind of work: | ||
| <br /> | ||
| <strong>Text → Text</strong> (chat / written replies),{" "} | ||
| <strong>Speech → Speech</strong> (voice conversations),{" "} | ||
| <strong>Speech → Text</strong> (turning audio into | ||
| written text), <strong>Text → Speech</strong> (reading | ||
| text aloud), or <strong>Other</strong>. Leave as{" "} | ||
| <em>All</em> to count everything. | ||
| </> | ||
| } | ||
| > | ||
| <Select | ||
| value={modality} | ||
| onChange={(e) => | ||
| setModality(e.target.value as AnalyticsModality | "") | ||
| } | ||
| options={toSelectOptions(ANALYTICS_MODALITY_OPTIONS)} | ||
| placeholder="All" | ||
| /> | ||
| </FilterField> | ||
| <FilterField | ||
| label="Provider" | ||
| className="flex-1 basis-36 min-w-36" | ||
| > | ||
| <Select | ||
| value={provider} | ||
| onChange={(e) => setProvider(e.target.value)} | ||
| options={PROVIDES_OPTIONS} | ||
| placeholder="All" | ||
| /> | ||
| </FilterField> | ||
| <FilterField | ||
| label="From" | ||
| className="flex-[1.25] basis-48 min-w-44" | ||
| > | ||
| <MonthYearPicker | ||
| value={fromMonth} | ||
| onChange={setFromMonth} | ||
| placeholder="Any start" | ||
| /> | ||
| </FilterField> | ||
| <FilterField | ||
| label="To" | ||
| className="flex-[1.25] basis-48 min-w-44" | ||
| > | ||
| <MonthYearPicker | ||
| value={toMonth} | ||
| onChange={setToMonth} | ||
| placeholder="Any end" | ||
| /> | ||
| </FilterField> | ||
| {(fromMonth || toMonth) && ( | ||
| <button | ||
| type="button" | ||
| onClick={() => { | ||
| setFromMonth(""); | ||
| setToMonth(""); | ||
| }} | ||
| className="text-[12px] font-medium text-status-error-text hover:underline cursor-pointer pb-1.5 shrink-0" | ||
| > | ||
| Clear dates | ||
| </button> | ||
| )} | ||
| </div> | ||
| </div> | ||
|
|
||
| <div className="px-4 sm:px-6 py-6 space-y-6"> | ||
| <AnalyticsTotalsRow | ||
| totals={totals} | ||
| isLoading={isTotalsLoading} | ||
| error={totalsError} | ||
| /> | ||
|
|
||
| <AnalyticsChartCard | ||
| data={data} | ||
| isLoading={isLoading} | ||
| error={error} | ||
| metricLabel={metricLabel} | ||
| /> | ||
| </div> | ||
| </div> | ||
| )} | ||
| </div> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| import { NextResponse } from "next/server"; | ||
| import { apiClient } from "@/app/lib/apiClient"; | ||
|
|
||
| export async function GET(request: Request) { | ||
| try { | ||
| const { searchParams } = new URL(request.url); | ||
| const qs = searchParams.toString(); | ||
| const endpoint = `/api/v1/analytics/monthly/chart${qs ? `?${qs}` : ""}`; | ||
| const { status, data } = await apiClient(request, endpoint); | ||
| return NextResponse.json(data, { status }); | ||
| } catch (error: unknown) { | ||
| return NextResponse.json( | ||
| { | ||
| success: false, | ||
| error: error instanceof Error ? error.message : String(error), | ||
| data: null, | ||
| }, | ||
| { status: 500 }, | ||
| ); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.