Skip to content

Commit db0b8f7

Browse files
KevenWMarkhamclaude
andcommitted
feat: add monthly billing breakdown and AI speaker name detection
Features: - AI-powered speaker name detection using Gemini 2.5 Flash - Detects when speakers introduce themselves in transcripts - Returns confidence levels (high/medium/low) with evidence quotes - UI component for reviewing and accepting/rejecting suggestions - Real-time token usage tracking with cost calculation - Tracks actual token usage from Gemini API responses - Captures both Video Transcription and Speaker Name Detection operations - Persists usage data to localStorage for accumulative tracking - Monthly billing breakdown in Cost Summary Modal - Current month billing card with tokens, cost, and operations - Historical monthly billing list sorted newest first - All data formatted with human-readable dates (e.g., "December 2024") - Speaker name propagation throughout UI - Custom speaker names now appear in transcript entries - Inline editing in Speaker Analytics sidebar - Names sync across all components Components Added: - SpeakerNameSuggestions.tsx: AI name suggestion review UI - speakerNameDetection.ts: Service for AI-powered name detection Components Modified: - CostSummaryModal.tsx: Added monthly billing sections - App.tsx: Pass monthly stats to cost modal - TranscriptList.tsx: Speaker name mapping and propagation - TranscriptEntry.tsx: Display custom speaker names - TranscriptView.tsx: AI detection trigger and state management - SpeakerAnalytics.tsx: Inline speaker renaming - geminiClient.ts: Token usage tracking integration - usageTracker.ts: localStorage persistence and monthly breakdown 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 3a58fa3 commit db0b8f7

File tree

10 files changed

+975
-139
lines changed

10 files changed

+975
-139
lines changed

src/App.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,12 @@ function App() {
354354
stats={usageTracker.getUserUsage(
355355
apiClient.getCurrentUser()?.id || 1
356356
)}
357+
monthlyStats={usageTracker.getMonthlyUsage(
358+
apiClient.getCurrentUser()?.id || 1
359+
)}
360+
currentMonthStats={usageTracker.getCurrentMonthUsage(
361+
apiClient.getCurrentUser()?.id || 1
362+
)}
357363
/>
358364
)}
359365

src/components/CostSummaryModal.tsx

Lines changed: 166 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import { X, TrendingUp, DollarSign, Activity, BarChart3 } from 'lucide-react'
1+
import {
2+
X,
3+
TrendingUp,
4+
DollarSign,
5+
Activity,
6+
BarChart3,
7+
Calendar,
8+
} from 'lucide-react'
29
import { Dialog, DialogContent } from '@/components/ui/dialog'
310
import { Card } from '@/components/ui/card'
411
import { Button } from '@/components/ui/button'
@@ -14,9 +21,20 @@ interface CostSummaryModalProps {
1421
byModel: Record<string, { tokens: number; cost: number; count: number }>
1522
byOperation: Record<string, { tokens: number; cost: number; count: number }>
1623
}
24+
monthlyStats?: Record<
25+
string,
26+
{ tokens: number; cost: number; operations: number }
27+
>
28+
currentMonthStats?: { tokens: number; cost: number; operations: number }
1729
}
1830

19-
export function CostSummaryModal({ isOpen, onClose, stats }: CostSummaryModalProps) {
31+
export function CostSummaryModal({
32+
isOpen,
33+
onClose,
34+
stats,
35+
monthlyStats,
36+
currentMonthStats,
37+
}: CostSummaryModalProps) {
2038
const avgCost = stats.operations > 0 ? stats.totalCost / stats.operations : 0
2139

2240
const formatTokens = (tokens: number) => {
@@ -30,6 +48,17 @@ export function CostSummaryModal({ isOpen, onClose, stats }: CostSummaryModalPro
3048
return `$${cost.toFixed(3)}m`
3149
}
3250

51+
const formatMonthLabel = (monthKey: string) => {
52+
const [year, month] = monthKey.split('-')
53+
const date = new Date(parseInt(year), parseInt(month) - 1)
54+
return date.toLocaleDateString('en-US', { month: 'long', year: 'numeric' })
55+
}
56+
57+
// Sort monthly stats by date (newest first)
58+
const sortedMonthlyStats = monthlyStats
59+
? Object.entries(monthlyStats).sort(([a], [b]) => b.localeCompare(a))
60+
: []
61+
3362
return (
3463
<Dialog open={isOpen} onOpenChange={onClose}>
3564
<DialogContent className="max-w-2xl p-0 gap-0 bg-white/95 backdrop-blur-xl border-white/20">
@@ -44,8 +73,12 @@ export function CostSummaryModal({ isOpen, onClose, stats }: CostSummaryModalPro
4473
</div>
4574
</div>
4675
<div>
47-
<h2 className="text-2xl font-bold text-slate-800">Token Usage & Cost Summary</h2>
48-
<p className="text-sm text-slate-600">Real-time API usage tracking</p>
76+
<h2 className="text-2xl font-bold text-slate-800">
77+
Token Usage & Cost Summary
78+
</h2>
79+
<p className="text-sm text-slate-600">
80+
Real-time API usage tracking
81+
</p>
4982
</div>
5083
</div>
5184
<Button
@@ -73,9 +106,13 @@ export function CostSummaryModal({ isOpen, onClose, stats }: CostSummaryModalPro
73106
<div className="w-10 h-10 rounded-xl bg-blue-500 flex items-center justify-center shadow-md">
74107
<TrendingUp className="w-5 h-5 text-white" />
75108
</div>
76-
<span className="text-sm font-medium text-slate-600">Total Tokens</span>
109+
<span className="text-sm font-medium text-slate-600">
110+
Total Tokens
111+
</span>
77112
</div>
78-
<p className="text-3xl font-bold text-slate-800">{formatTokens(stats.totalTokens)}</p>
113+
<p className="text-3xl font-bold text-slate-800">
114+
{formatTokens(stats.totalTokens)}
115+
</p>
79116
</Card>
80117
</motion.div>
81118

@@ -89,9 +126,13 @@ export function CostSummaryModal({ isOpen, onClose, stats }: CostSummaryModalPro
89126
<div className="w-10 h-10 rounded-xl bg-purple-500 flex items-center justify-center shadow-md">
90127
<DollarSign className="w-5 h-5 text-white" />
91128
</div>
92-
<span className="text-sm font-medium text-slate-600">Total Cost</span>
129+
<span className="text-sm font-medium text-slate-600">
130+
Total Cost
131+
</span>
93132
</div>
94-
<p className="text-3xl font-bold text-slate-800">{formatCost(stats.totalCost)}</p>
133+
<p className="text-3xl font-bold text-slate-800">
134+
{formatCost(stats.totalCost)}
135+
</p>
95136
</Card>
96137
</motion.div>
97138

@@ -105,9 +146,13 @@ export function CostSummaryModal({ isOpen, onClose, stats }: CostSummaryModalPro
105146
<div className="w-10 h-10 rounded-xl bg-emerald-500 flex items-center justify-center shadow-md">
106147
<Activity className="w-5 h-5 text-white" />
107148
</div>
108-
<span className="text-sm font-medium text-slate-600">Operations</span>
149+
<span className="text-sm font-medium text-slate-600">
150+
Operations
151+
</span>
109152
</div>
110-
<p className="text-3xl font-bold text-slate-800">{stats.operations}</p>
153+
<p className="text-3xl font-bold text-slate-800">
154+
{stats.operations}
155+
</p>
111156
</Card>
112157
</motion.div>
113158

@@ -121,9 +166,13 @@ export function CostSummaryModal({ isOpen, onClose, stats }: CostSummaryModalPro
121166
<div className="w-10 h-10 rounded-xl bg-orange-500 flex items-center justify-center shadow-md">
122167
<DollarSign className="w-5 h-5 text-white" />
123168
</div>
124-
<span className="text-sm font-medium text-slate-600">Avg Cost</span>
169+
<span className="text-sm font-medium text-slate-600">
170+
Avg Cost
171+
</span>
125172
</div>
126-
<p className="text-3xl font-bold text-slate-800">{formatCost(avgCost)}</p>
173+
<p className="text-3xl font-bold text-slate-800">
174+
{formatCost(avgCost)}
175+
</p>
127176
</Card>
128177
</motion.div>
129178
</div>
@@ -135,7 +184,9 @@ export function CostSummaryModal({ isOpen, onClose, stats }: CostSummaryModalPro
135184
transition={{ delay: 0.3 }}
136185
>
137186
<Card className="p-5">
138-
<h3 className="text-lg font-semibold text-slate-800 mb-4">By Model</h3>
187+
<h3 className="text-lg font-semibold text-slate-800 mb-4">
188+
By Model
189+
</h3>
139190
<div className="space-y-3">
140191
{Object.entries(stats.byModel).map(([model, data]) => (
141192
<div
@@ -149,8 +200,12 @@ export function CostSummaryModal({ isOpen, onClose, stats }: CostSummaryModalPro
149200
</p>
150201
</div>
151202
<div className="text-right">
152-
<p className="font-semibold text-slate-800">{formatTokens(data.tokens)}</p>
153-
<p className="text-sm text-slate-600">{formatCost(data.cost)}</p>
203+
<p className="font-semibold text-slate-800">
204+
{formatTokens(data.tokens)}
205+
</p>
206+
<p className="text-sm text-slate-600">
207+
{formatCost(data.cost)}
208+
</p>
154209
</div>
155210
</div>
156211
))}
@@ -165,7 +220,9 @@ export function CostSummaryModal({ isOpen, onClose, stats }: CostSummaryModalPro
165220
transition={{ delay: 0.35 }}
166221
>
167222
<Card className="p-5">
168-
<h3 className="text-lg font-semibold text-slate-800 mb-4">By Operation</h3>
223+
<h3 className="text-lg font-semibold text-slate-800 mb-4">
224+
By Operation
225+
</h3>
169226
<div className="space-y-3">
170227
{Object.entries(stats.byOperation).map(([operation, data]) => (
171228
<div
@@ -175,18 +232,108 @@ export function CostSummaryModal({ isOpen, onClose, stats }: CostSummaryModalPro
175232
<div>
176233
<p className="font-medium text-slate-800">{operation}</p>
177234
<p className="text-sm text-slate-600">
178-
{data.count} {data.count === 1 ? 'operation' : 'operations'}
235+
{data.count}{' '}
236+
{data.count === 1 ? 'operation' : 'operations'}
179237
</p>
180238
</div>
181239
<div className="text-right">
182-
<p className="font-semibold text-slate-800">{formatTokens(data.tokens)}</p>
183-
<p className="text-sm text-slate-600">{formatCost(data.cost)}</p>
240+
<p className="font-semibold text-slate-800">
241+
{formatTokens(data.tokens)}
242+
</p>
243+
<p className="text-sm text-slate-600">
244+
{formatCost(data.cost)}
245+
</p>
184246
</div>
185247
</div>
186248
))}
187249
</div>
188250
</Card>
189251
</motion.div>
252+
253+
{/* Current Month Billing */}
254+
{currentMonthStats && currentMonthStats.operations > 0 && (
255+
<motion.div
256+
initial={{ opacity: 0, y: 20 }}
257+
animate={{ opacity: 1, y: 0 }}
258+
transition={{ delay: 0.4 }}
259+
>
260+
<Card className="p-5 bg-gradient-to-br from-amber-50 to-amber-100/50 border-amber-200/50">
261+
<div className="flex items-center gap-3 mb-4">
262+
<div className="w-10 h-10 rounded-xl bg-amber-500 flex items-center justify-center shadow-md">
263+
<Calendar className="w-5 h-5 text-white" />
264+
</div>
265+
<h3 className="text-lg font-semibold text-slate-800">
266+
Current Month (
267+
{formatMonthLabel(
268+
`${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, '0')}`
269+
)}
270+
)
271+
</h3>
272+
</div>
273+
<div className="grid grid-cols-3 gap-4">
274+
<div className="text-center p-3 rounded-xl bg-white/60 border border-amber-200">
275+
<p className="text-sm text-slate-600 mb-1">Tokens</p>
276+
<p className="text-xl font-bold text-slate-800">
277+
{formatTokens(currentMonthStats.tokens)}
278+
</p>
279+
</div>
280+
<div className="text-center p-3 rounded-xl bg-white/60 border border-amber-200">
281+
<p className="text-sm text-slate-600 mb-1">Cost</p>
282+
<p className="text-xl font-bold text-slate-800">
283+
{formatCost(currentMonthStats.cost)}
284+
</p>
285+
</div>
286+
<div className="text-center p-3 rounded-xl bg-white/60 border border-amber-200">
287+
<p className="text-sm text-slate-600 mb-1">Operations</p>
288+
<p className="text-xl font-bold text-slate-800">
289+
{currentMonthStats.operations}
290+
</p>
291+
</div>
292+
</div>
293+
</Card>
294+
</motion.div>
295+
)}
296+
297+
{/* Monthly Billing Breakdown */}
298+
{sortedMonthlyStats.length > 0 && (
299+
<motion.div
300+
initial={{ opacity: 0, y: 20 }}
301+
animate={{ opacity: 1, y: 0 }}
302+
transition={{ delay: 0.45 }}
303+
>
304+
<Card className="p-5">
305+
<h3 className="text-lg font-semibold text-slate-800 mb-4">
306+
Monthly Billing History
307+
</h3>
308+
<div className="space-y-3 max-h-[300px] overflow-y-auto">
309+
{sortedMonthlyStats.map(([monthKey, data]) => (
310+
<div
311+
key={monthKey}
312+
className="flex items-center justify-between p-4 rounded-xl bg-slate-50 border border-slate-200 hover:bg-slate-100 transition-colors"
313+
>
314+
<div>
315+
<p className="font-medium text-slate-800">
316+
{formatMonthLabel(monthKey)}
317+
</p>
318+
<p className="text-sm text-slate-600">
319+
{data.operations}{' '}
320+
{data.operations === 1 ? 'operation' : 'operations'}
321+
</p>
322+
</div>
323+
<div className="text-right">
324+
<p className="font-semibold text-slate-800">
325+
{formatCost(data.cost)}
326+
</p>
327+
<p className="text-sm text-slate-600">
328+
{formatTokens(data.tokens)} tokens
329+
</p>
330+
</div>
331+
</div>
332+
))}
333+
</div>
334+
</Card>
335+
</motion.div>
336+
)}
190337
</div>
191338

192339
{/* Footer */}

0 commit comments

Comments
 (0)