1- import React , { useState , useCallback , useRef , DragEvent , useEffect } from "react" ;
1+ import React , { useState , useCallback , useRef , DragEvent , useEffect , useMemo } from "react" ;
22import { Upload , GithubLogo , CircleNotch } from "@phosphor-icons/react" ;
33import { UserSquare , ChevronRight , Shield } from "lucide-react" ;
44import { toast , Toaster } from "sonner" ;
@@ -25,6 +25,7 @@ import {
2525 ExceededRequestDetail ,
2626 ProjectedUserData ,
2727 MonthOption ,
28+ UserAnalysisData ,
2829 aggregateDataByDay ,
2930 parseCSV ,
3031 getModelUsageSummary ,
@@ -43,9 +44,11 @@ import {
4344 getProjectedUsersExceedingQuotaDetails ,
4445 getAvailableMonths ,
4546 filterDataByMonth ,
47+ getUserAnalysisData ,
4648 EXCESS_REQUEST_COST
4749} from "@/lib/utils" ;
4850import { MonthSelector } from "@/components/MonthSelector" ;
51+ import { UserSearch } from "@/components/UserSearch" ;
4952
5053function App ( ) {
5154 const [ showPrivacyBanner , setShowPrivacyBanner ] = useState ( true ) ;
@@ -73,6 +76,8 @@ function App() {
7376 const [ showPotentialCostDetails , setShowPotentialCostDetails ] = useState ( false ) ;
7477 const [ showProjectedUsersDialog , setShowProjectedUsersDialog ] = useState ( false ) ;
7578 const [ projectedUsersData , setProjectedUsersData ] = useState < ProjectedUserData [ ] > ( [ ] ) ;
79+ const [ selectedSearchUser , setSelectedSearchUser ] = useState < string | null > ( null ) ;
80+ const [ userAnalysisData , setUserAnalysisData ] = useState < UserAnalysisData | null > ( null ) ;
7681 const fileInputRef = useRef < HTMLInputElement > ( null ) ;
7782
7883 // Recalculate users exceeding quota when plan selection changes
@@ -86,15 +91,74 @@ function App() {
8691
8792 const projectedDetails = getProjectedUsersExceedingQuotaDetails ( data , selectedPlan ) ;
8893 setProjectedUsersData ( projectedDetails ) ;
94+
95+ // Update user analysis data if a user is selected
96+ if ( selectedSearchUser ) {
97+ const analysisData = getUserAnalysisData ( data , selectedSearchUser ) ;
98+ setUserAnalysisData ( analysisData ) ;
99+ }
89100 }
90- } , [ selectedPlan , data ] ) ;
101+ } , [ selectedPlan , data , selectedSearchUser ] ) ;
102+
103+ // Get display data - either filtered by user or all data
104+ const displayData = useMemo ( ( ) => {
105+ if ( ! data ) return null ;
106+ if ( ! selectedSearchUser ) return data ;
107+ return data . filter ( item => item . user === selectedSearchUser ) ;
108+ } , [ data , selectedSearchUser ] ) ;
91109
92110 // Reprocess data when month selection changes
93111 useEffect ( ( ) => {
94112 if ( rawData && selectedMonth ) {
95113 processDataForMonth ( rawData , selectedMonth ) ;
96114 }
97115 } , [ selectedMonth , rawData , selectedPlan ] ) ;
116+
117+ // Reprocess display data when user selection changes
118+ useEffect ( ( ) => {
119+ if ( displayData && displayData . length > 0 ) {
120+ // Get unique models from display data
121+ const models = Array . from ( new Set ( displayData . map ( item => item . model ) ) ) ;
122+ setUniqueModels ( models ) ;
123+
124+ // Aggregate data by day and model for display data
125+ const aggregated = aggregateDataByDay ( displayData ) ;
126+ setAggregatedData ( aggregated ) ;
127+
128+ // Get model usage summary for display data
129+ const summary = getModelUsageSummary ( displayData ) ;
130+ setModelSummary ( summary ) ;
131+
132+ // Get daily model data for bar chart for display data
133+ const dailyData = getDailyModelData ( displayData ) ;
134+ setDailyModelData ( dailyData ) ;
135+
136+ // Get power users data for display data
137+ const powerUsers = getPowerUsers ( displayData ) ;
138+ setPowerUserSummary ( powerUsers ) ;
139+
140+ // Get power user daily breakdown for display data
141+ const powerUserNames = powerUsers . powerUsers . map ( user => user . user ) ;
142+ const powerUserBreakdown = getPowerUserDailyBreakdown ( displayData , powerUserNames ) ;
143+ setPowerUserDailyBreakdown ( powerUserBreakdown ) ;
144+
145+ // Get count of users exceeding quota for display data
146+ const exceedingUsersCount = getUniqueUsersExceedingQuota ( displayData , selectedPlan ) ;
147+ setUsersExceedingQuota ( exceedingUsersCount ) ;
148+
149+ // Get projected count of users who will exceed quota by month-end for display data
150+ const projectedExceedingUsersCount = getProjectedUsersExceedingQuota ( displayData , selectedPlan ) ;
151+ setProjectedUsersExceedingQuota ( projectedExceedingUsersCount ) ;
152+
153+ // Get projected users details for display data
154+ const projectedDetails = getProjectedUsersExceedingQuotaDetails ( displayData , selectedPlan ) ;
155+ setProjectedUsersData ( projectedDetails ) ;
156+
157+ // Get the last date available in the display data
158+ const lastDate = getLastDateFromData ( displayData ) ;
159+ setLastDateAvailable ( lastDate ) ;
160+ }
161+ } , [ displayData , selectedPlan ] ) ;
98162
99163 /**
100164 * Process data for a specific month and update all derived state
@@ -148,12 +212,28 @@ function App() {
148212
149213 // Reset selected power user when month changes
150214 setSelectedPowerUser ( null ) ;
151- } , [ selectedPlan ] ) ;
215+
216+ // Reset selected search user when month changes and recalculate analysis
217+ if ( selectedSearchUser ) {
218+ const analysisData = getUserAnalysisData ( filteredData , selectedSearchUser ) ;
219+ setUserAnalysisData ( analysisData ) ;
220+ }
221+ } , [ selectedPlan , selectedSearchUser ] ) ;
152222
153223 const handlePowerUserSelect = useCallback ( ( userName : string | null ) => {
154224 setSelectedPowerUser ( userName ) ;
155225 } , [ ] ) ;
156226
227+ const handleSearchUserSelect = useCallback ( ( userName : string | null ) => {
228+ setSelectedSearchUser ( userName ) ;
229+ if ( userName && data ) {
230+ const analysisData = getUserAnalysisData ( data , userName ) ;
231+ setUserAnalysisData ( analysisData ) ;
232+ } else {
233+ setUserAnalysisData ( null ) ;
234+ }
235+ } , [ data ] ) ;
236+
157237 // Generate filtered power user daily breakdown based on selected user
158238 const getFilteredPowerUserBreakdown = useCallback ( ( ) => {
159239 if ( ! selectedPowerUser || ! data ) {
@@ -596,12 +676,24 @@ function App() {
596676 </ Card >
597677 ) }
598678
599- { data && data . length > 0 && (
679+ { displayData && displayData . length > 0 && (
600680 < div className = "space-y-8" >
601681 < div >
602682 < div className = "flex items-center justify-between mb-4" >
603683 < div >
604- < h2 className = "text-2xl font-semibold mb-2" > Usage Statistics</ h2 >
684+ < h2 className = "text-2xl font-semibold mb-2" >
685+ Usage Statistics
686+ { selectedSearchUser && (
687+ < span className = "ml-2 text-lg font-medium text-blue-600" >
688+ - { selectedSearchUser }
689+ </ span >
690+ ) }
691+ </ h2 >
692+ { selectedSearchUser && (
693+ < p className = "text-sm text-muted-foreground" >
694+ Showing data filtered for selected user. All panels below reflect this user's activity only.
695+ </ p >
696+ ) }
605697 </ div >
606698 < div className = "flex items-center gap-4" >
607699 < MonthSelector
@@ -613,6 +705,135 @@ function App() {
613705 />
614706 </ div >
615707 </ div >
708+ < Separator className = "mb-4" />
709+
710+ { /* User Analysis Section */ }
711+ < div className = "mb-6" >
712+ < div className = "flex items-center justify-between mb-4" >
713+ < div >
714+ < h2 className = "text-2xl font-semibold mb-2" > User Analysis</ h2 >
715+ < p className = "text-muted-foreground" >
716+ { selectedSearchUser
717+ ? `Currently viewing data for ${ selectedSearchUser } . All panels are filtered to show only this user's activity.`
718+ : "Search for a specific user to view their detailed usage statistics"
719+ }
720+ </ p >
721+ </ div >
722+ { selectedSearchUser && (
723+ < Button
724+ variant = "outline"
725+ onClick = { ( ) => handleSearchUserSelect ( null ) }
726+ className = "ml-4"
727+ >
728+ View All Users
729+ </ Button >
730+ ) }
731+ </ div >
732+ < div className = "mb-4" >
733+ < UserSearch
734+ data = { data }
735+ selectedUser = { selectedSearchUser }
736+ onUserChange = { handleSearchUserSelect }
737+ disabled = { isProcessing }
738+ />
739+ </ div >
740+
741+ { userAnalysisData && (
742+ < Card >
743+ < div className = "p-5" >
744+ < div className = "flex items-center justify-between mb-4" >
745+ < h3 className = "text-lg font-semibold" > Analysis for { userAnalysisData . user } </ h3 >
746+ </ div >
747+
748+ { /* User Statistics Summary */ }
749+ < div className = "grid grid-cols-2 md:grid-cols-4 gap-4 mb-6" >
750+ < div >
751+ < div className = "text-sm text-muted-foreground" > Total Requests</ div >
752+ < div className = "text-lg font-bold" > { userAnalysisData . totalRequests . toLocaleString ( ) } </ div >
753+ </ div >
754+ < div >
755+ < div className = "text-sm text-muted-foreground" > Daily Average</ div >
756+ < div className = "text-lg font-bold" > { userAnalysisData . dailyAverage . toLocaleString ( undefined , { maximumFractionDigits : 1 } ) } </ div >
757+ </ div >
758+ < div >
759+ < div className = "text-sm text-muted-foreground" > Models Used</ div >
760+ < div className = "text-lg font-bold" > { userAnalysisData . uniqueModels . length } </ div >
761+ </ div >
762+ < div >
763+ < div className = "text-sm text-muted-foreground" > Exceeds Free Budget</ div >
764+ < div className = { `text-lg font-bold ${ userAnalysisData . exceedsFreeBudget ? 'text-red-600' : 'text-green-600' } ` } >
765+ { userAnalysisData . exceedsFreeBudget ? 'Yes' : 'No' }
766+ </ div >
767+ </ div >
768+ </ div >
769+
770+ { /* Weekly Breakdown */ }
771+ < div className = "mb-4" >
772+ < h4 className = "text-md font-medium mb-3" > Weekly Breakdown (ISO weeks)</ h4 >
773+ < div className = "overflow-auto max-h-60" >
774+ < Table >
775+ < TableHeader >
776+ < TableRow >
777+ < TableHead > Week</ TableHead >
778+ < TableHead > Date Range</ TableHead >
779+ < TableHead className = "text-right" > Compliant Requests</ TableHead >
780+ < TableHead className = "text-right" > Exceeding Requests</ TableHead >
781+ < TableHead className = "text-right" > Total Requests</ TableHead >
782+ < TableHead > Models Used</ TableHead >
783+ </ TableRow >
784+ </ TableHeader >
785+ < TableBody >
786+ { userAnalysisData . weeklyBreakdown . map ( ( week ) => (
787+ < TableRow key = { `${ week . year } -W${ week . week } ` } >
788+ < TableCell className = "font-medium" >
789+ { week . year } -W{ week . week . toString ( ) . padStart ( 2 , '0' ) }
790+ </ TableCell >
791+ < TableCell >
792+ { week . startDate } to { week . endDate }
793+ </ TableCell >
794+ < TableCell className = "text-right" >
795+ { week . compliantRequests . toLocaleString ( ) }
796+ </ TableCell >
797+ < TableCell className = "text-right" >
798+ < span className = { week . exceedingRequests > 0 ? 'text-red-600 font-medium' : '' } >
799+ { week . exceedingRequests . toLocaleString ( ) }
800+ </ span >
801+ </ TableCell >
802+ < TableCell className = "text-right font-medium" >
803+ { week . totalRequests . toLocaleString ( ) }
804+ </ TableCell >
805+ < TableCell >
806+ < div className = "flex flex-wrap gap-1" >
807+ { week . modelsUsed . map ( ( model ) => (
808+ < span key = { model } className = "px-1.5 py-0.5 bg-secondary text-xs rounded" >
809+ { model }
810+ </ span >
811+ ) ) }
812+ </ div >
813+ </ TableCell >
814+ </ TableRow >
815+ ) ) }
816+ </ TableBody >
817+ </ Table >
818+ </ div >
819+ </ div >
820+
821+ { /* Models Used */ }
822+ < div >
823+ < h4 className = "text-md font-medium mb-3" > All Models Used</ h4 >
824+ < div className = "flex flex-wrap gap-2" >
825+ { userAnalysisData . uniqueModels . map ( ( model ) => (
826+ < span key = { model } className = "px-3 py-1 bg-secondary text-sm rounded-md" >
827+ { model }
828+ </ span >
829+ ) ) }
830+ </ div >
831+ </ div >
832+ </ div >
833+ </ Card >
834+ ) }
835+ </ div >
836+
616837 < Separator className = "mb-4" />
617838 < div className = "mb-4" >
618839 < Card >
@@ -621,13 +842,13 @@ function App() {
621842 < div className = "flex items-center gap-2" >
622843 < span className = "text-sm text-muted-foreground" > Total Requests:</ span >
623844 < span className = "text-lg font-bold" >
624- { data . reduce ( ( sum , item ) => sum + item . requestsUsed , 0 ) . toLocaleString ( undefined , { maximumFractionDigits : 2 , minimumFractionDigits : 0 } ) }
845+ { displayData . reduce ( ( sum , item ) => sum + item . requestsUsed , 0 ) . toLocaleString ( undefined , { maximumFractionDigits : 2 , minimumFractionDigits : 0 } ) }
625846 </ span >
626847 </ div >
627848 < div className = "flex items-center gap-2" >
628849 < span className = "text-sm text-muted-foreground" > Unique Users:</ span >
629850 < span className = "text-lg font-bold" >
630- { new Set ( data . map ( item => item . user ) ) . size . toLocaleString ( ) }
851+ { new Set ( displayData . map ( item => item . user ) ) . size . toLocaleString ( ) }
631852 </ span >
632853 </ div >
633854 < div className = "flex items-center gap-2" >
@@ -660,7 +881,7 @@ function App() {
660881 >
661882 < span className = "text-sm text-muted-foreground" > Potential Cost:</ span >
662883 < span className = "value" >
663- ${ ( data . reduce ( ( sum , item ) => sum + item . requestsUsed , 0 ) * EXCESS_REQUEST_COST ) . toLocaleString ( undefined , { minimumFractionDigits : 2 , maximumFractionDigits : 2 } ) }
884+ ${ ( displayData . reduce ( ( sum , item ) => sum + item . requestsUsed , 0 ) * EXCESS_REQUEST_COST ) . toLocaleString ( undefined , { minimumFractionDigits : 2 , maximumFractionDigits : 2 } ) }
664885 </ span >
665886 < ChevronRight className = "icon" />
666887 </ div >
@@ -1004,7 +1225,14 @@ function App() {
10041225 < div className = "mb-6" >
10051226 < Card className = "p-5" >
10061227 < div className = "flex items-center justify-between mb-3" >
1007- < h3 className = "text-md font-medium" > Requests per Model</ h3 >
1228+ < h3 className = "text-md font-medium" >
1229+ Requests per Model
1230+ { selectedSearchUser && (
1231+ < span className = "ml-2 text-sm font-normal text-blue-600" >
1232+ - { selectedSearchUser }
1233+ </ span >
1234+ ) }
1235+ </ h3 >
10081236 < div className = "flex items-center gap-2" >
10091237 < span className = "text-sm text-muted-foreground" > Plan Type:</ span >
10101238 < Select value = { selectedPlan } onValueChange = { setSelectedPlan } >
@@ -1072,7 +1300,14 @@ function App() {
10721300
10731301 < div >
10741302 < div className = "flex justify-between items-center mb-2" >
1075- < h2 className = "text-2xl font-semibold" > Daily Usage Overview</ h2 >
1303+ < h2 className = "text-2xl font-semibold" >
1304+ Daily Usage Overview
1305+ { selectedSearchUser && (
1306+ < span className = "ml-2 text-lg font-medium text-blue-600" >
1307+ - { selectedSearchUser }
1308+ </ span >
1309+ ) }
1310+ </ h2 >
10761311 { lastDateAvailable && (
10771312 < div className = "text-sm text-muted-foreground" >
10781313 Data available through: < span className = "font-medium" > { lastDateAvailable } </ span >
@@ -1159,7 +1394,14 @@ function App() {
11591394
11601395 { /* Bar Chart - Requests per Model per Day */ }
11611396 < div className = "flex justify-between items-center mb-2" >
1162- < h2 className = "text-2xl font-semibold" > Requests per Model per Day</ h2 >
1397+ < h2 className = "text-2xl font-semibold" >
1398+ Requests per Model per Day
1399+ { selectedSearchUser && (
1400+ < span className = "ml-2 text-lg font-medium text-blue-600" >
1401+ - { selectedSearchUser }
1402+ </ span >
1403+ ) }
1404+ </ h2 >
11631405 { lastDateAvailable && (
11641406 < div className = "text-sm text-muted-foreground" >
11651407 Data available through: < span className = "font-medium" > { lastDateAvailable } </ span >
@@ -1245,11 +1487,11 @@ function App() {
12451487 { exceededDetailsData . length > 0 ? (
12461488 < >
12471489 { /* Summary Card */ }
1248- { selectedPowerUser && data && (
1490+ { selectedPowerUser && displayData && (
12491491 < Card className = "p-4" >
12501492 < h3 className = "text-md font-medium mb-3" > User Summary</ h3 >
12511493 { ( ( ) => {
1252- const userSummary = getUserExceededRequestSummary ( data , selectedPowerUser ) ;
1494+ const userSummary = getUserExceededRequestSummary ( displayData , selectedPowerUser ) ;
12531495 return (
12541496 < div className = "grid grid-cols-2 md:grid-cols-4 gap-4" >
12551497 < div >
0 commit comments