1+ import { useState , useCallback } from "react" ;
2+ import { Upload } from "@phosphor-icons/react" ;
3+ import { toast } from "sonner" ;
4+ import {
5+ LineChart , Line , XAxis , YAxis , CartesianGrid , Tooltip , Legend , ResponsiveContainer
6+ } from "recharts" ;
7+ import { Card } from "@/components/ui/card" ;
8+ import { Button } from "@/components/ui/button" ;
9+ import { Separator } from "@/components/ui/separator" ;
10+ import { ChartContainer , ChartTooltip , ChartTooltipContent } from "@/components/ui/chart" ;
11+ import { AggregatedData , CopilotUsageData , aggregateDataByDay , parseCSV } from "@/lib/utils" ;
12+
113function App ( ) {
2- return < div > </ div >
14+ const [ data , setData ] = useState < CopilotUsageData [ ] | null > ( null ) ;
15+ const [ aggregatedData , setAggregatedData ] = useState < AggregatedData [ ] > ( [ ] ) ;
16+ const [ uniqueModels , setUniqueModels ] = useState < string [ ] > ( [ ] ) ;
17+
18+ const handleFileUpload = useCallback ( ( event : React . ChangeEvent < HTMLInputElement > ) => {
19+ const file = event . target . files ?. [ 0 ] ;
20+ if ( ! file ) return ;
21+
22+ const reader = new FileReader ( ) ;
23+ reader . onload = ( e ) => {
24+ try {
25+ const csvContent = e . target ?. result as string ;
26+ if ( ! csvContent ) throw new Error ( "Failed to read file" ) ;
27+
28+ const parsedData = parseCSV ( csvContent ) ;
29+ setData ( parsedData ) ;
30+
31+ // Get unique models
32+ const models = Array . from ( new Set ( parsedData . map ( item => item . model ) ) ) ;
33+ setUniqueModels ( models ) ;
34+
35+ // Aggregate data by day and model
36+ const aggregated = aggregateDataByDay ( parsedData ) ;
37+ setAggregatedData ( aggregated ) ;
38+
39+ toast . success ( `Loaded ${ parsedData . length } records successfully` ) ;
40+ } catch ( error ) {
41+ console . error ( "Error parsing CSV:" , error ) ;
42+ toast . error ( "Failed to parse CSV file. Please check the format." ) ;
43+ setData ( null ) ;
44+ setAggregatedData ( [ ] ) ;
45+ }
46+ } ;
47+
48+ reader . readAsText ( file ) ;
49+ } , [ ] ) ;
50+
51+ // Generate chart data grouped by date with a stacked entry for each model
52+ const chartData = useCallback ( ( ) => {
53+ if ( ! aggregatedData . length ) return [ ] ;
54+
55+ // Group by date first
56+ const groupedByDate : Record < string , any > = { } ;
57+
58+ aggregatedData . forEach ( item => {
59+ if ( ! groupedByDate [ item . date ] ) {
60+ groupedByDate [ item . date ] = { date : item . date } ;
61+ }
62+
63+ // Add compliant and exceeding requests for this model
64+ groupedByDate [ item . date ] [ `${ item . model } _compliant` ] = item . compliantRequests ;
65+ groupedByDate [ item . date ] [ `${ item . model } _exceeding` ] = item . exceedingRequests ;
66+ } ) ;
67+
68+ // Convert to array sorted by date
69+ return Object . values ( groupedByDate ) . sort ( ( a , b ) =>
70+ a . date . localeCompare ( b . date )
71+ ) ;
72+ } , [ aggregatedData ] ) ;
73+
74+ return (
75+ < div className = "container max-w-7xl mx-auto py-8 px-4 min-h-screen" >
76+ < header className = "mb-8" >
77+ < h1 className = "text-3xl font-bold tracking-tight text-foreground mb-2" >
78+ GitHub Copilot Usage Analyzer
79+ </ h1 >
80+ < p className = "text-muted-foreground" >
81+ Upload your Copilot usage CSV export to visualize request patterns
82+ </ p >
83+ </ header >
84+
85+ < Card className = "mb-8" >
86+ < div className = "p-6 text-center" >
87+ < div className = "mb-4" >
88+ < Upload size = { 48 } weight = "thin" className = "mx-auto text-muted-foreground" />
89+ </ div >
90+
91+ < h2 className = "text-xl font-medium mb-2" > Upload CSV File</ h2 >
92+ < p className = "text-muted-foreground mb-4 max-w-md mx-auto" >
93+ Upload your GitHub Copilot premium requests usage CSV export to visualize the data
94+ </ p >
95+
96+ < label htmlFor = "csv-upload" >
97+ < Button as = "div" className = "cursor-pointer" >
98+ Select CSV File
99+ </ Button >
100+ < input
101+ id = "csv-upload"
102+ type = "file"
103+ accept = ".csv"
104+ onChange = { handleFileUpload }
105+ className = "hidden"
106+ />
107+ </ label >
108+ </ div >
109+ </ Card >
110+
111+ { data && data . length > 0 && (
112+ < div className = "space-y-8" >
113+ < div >
114+ < h2 className = "text-2xl font-semibold mb-2" > Usage Statistics</ h2 >
115+ < Separator className = "mb-4" />
116+ < div className = "grid grid-cols-1 md:grid-cols-3 gap-4" >
117+ < Card >
118+ < div className = "p-5" >
119+ < h3 className = "text-sm font-medium text-muted-foreground mb-1" > Total Requests</ h3 >
120+ < p className = "text-2xl font-bold" >
121+ { data . reduce ( ( sum , item ) => sum + item . requestsUsed , 0 ) . toLocaleString ( ) }
122+ </ p >
123+ </ div >
124+ </ Card >
125+ < Card >
126+ < div className = "p-5" >
127+ < h3 className = "text-sm font-medium text-muted-foreground mb-1" > Unique Users</ h3 >
128+ < p className = "text-2xl font-bold" >
129+ { new Set ( data . map ( item => item . user ) ) . size . toLocaleString ( ) }
130+ </ p >
131+ </ div >
132+ </ Card >
133+ < Card >
134+ < div className = "p-5" >
135+ < h3 className = "text-sm font-medium text-muted-foreground mb-1" > Models Used</ h3 >
136+ < p className = "text-2xl font-bold" >
137+ { uniqueModels . length }
138+ </ p >
139+ </ div >
140+ </ Card >
141+ </ div >
142+ </ div >
143+
144+ < div >
145+ < h2 className = "text-2xl font-semibold mb-2" > Daily Usage by Model</ h2 >
146+ < Separator className = "mb-6" />
147+ < div className = "bg-card p-4 rounded-lg border" >
148+ < ChartContainer
149+ config = { {
150+ compliant : { color : "#10b981" } , // green
151+ exceeding : { color : "#ef4444" } , // red
152+ } }
153+ className = "h-[400px] w-full"
154+ >
155+ < LineChart data = { chartData ( ) } >
156+ < CartesianGrid strokeDasharray = "3 3" opacity = { 0.2 } />
157+ < XAxis
158+ dataKey = "date"
159+ tick = { { fill : 'var(--foreground)' } }
160+ tickLine = { { stroke : 'var(--border)' } }
161+ />
162+ < YAxis
163+ tick = { { fill : 'var(--foreground)' } }
164+ tickLine = { { stroke : 'var(--border)' } }
165+ />
166+ < ChartTooltip
167+ content = { ( { active, payload, label } ) => {
168+ if ( active && payload && payload . length ) {
169+ const modelsData : Record < string , { compliant : number , exceeding : number } > = { } ;
170+
171+ payload . forEach ( item => {
172+ const keyParts = String ( item . dataKey ) . split ( '_' ) ;
173+ const model = keyParts [ 0 ] ;
174+ const type = keyParts [ 1 ] ;
175+
176+ if ( ! modelsData [ model ] ) {
177+ modelsData [ model ] = { compliant : 0 , exceeding : 0 } ;
178+ }
179+
180+ modelsData [ model ] [ type as 'compliant' | 'exceeding' ] = Number ( item . value || 0 ) ;
181+ } ) ;
182+
183+ return (
184+ < div className = "border rounded-lg bg-background shadow-lg p-3 text-xs" >
185+ < div className = "font-medium mb-2" > { label } </ div >
186+ < div className = "space-y-2" >
187+ { Object . entries ( modelsData ) . map ( ( [ model , stats ] ) => (
188+ < div key = { model } className = "grid grid-cols-2 gap-2" >
189+ < div className = "font-medium" > { model } </ div >
190+ < div />
191+ < div className = "flex items-center gap-1.5" >
192+ < div className = "w-2 h-2 rounded-full bg-[#10b981]" />
193+ < span > Compliant:</ span >
194+ </ div >
195+ < div className = "text-right" > { stats . compliant } </ div >
196+ < div className = "flex items-center gap-1.5" >
197+ < div className = "w-2 h-2 rounded-full bg-[#ef4444]" />
198+ < span > Exceeding:</ span >
199+ </ div >
200+ < div className = "text-right" > { stats . exceeding } </ div >
201+ </ div >
202+ ) ) }
203+ </ div >
204+ </ div >
205+ ) ;
206+ }
207+ return null ;
208+ } }
209+ />
210+ < Legend />
211+
212+ { /* Create lines for each model */ }
213+ { uniqueModels . flatMap ( model => [
214+ < Line
215+ key = { `${ model } _compliant` }
216+ type = "monotone"
217+ dataKey = { `${ model } _compliant` }
218+ name = { `${ model } (Compliant)` }
219+ stroke = "#10b981"
220+ strokeWidth = { 2 }
221+ activeDot = { { r : 6 } }
222+ stackId = "1"
223+ /> ,
224+ < Line
225+ key = { `${ model } _exceeding` }
226+ type = "monotone"
227+ dataKey = { `${ model } _exceeding` }
228+ name = { `${ model } (Exceeding)` }
229+ stroke = "#ef4444"
230+ strokeWidth = { 2 }
231+ activeDot = { { r : 6 } }
232+ stackId = "1"
233+ />
234+ ] ) }
235+ </ LineChart >
236+ </ ChartContainer >
237+ </ div >
238+ </ div >
239+ </ div >
240+ ) }
241+ </ div >
242+ ) ;
3243}
4244
5- export default App
245+ export default App ;
0 commit comments