diff --git a/app/components/home/SubscriptionStatusWidget.tsx b/app/components/home/SubscriptionStatusWidget.tsx index 5589325f..cb2640a4 100644 --- a/app/components/home/SubscriptionStatusWidget.tsx +++ b/app/components/home/SubscriptionStatusWidget.tsx @@ -1,22 +1,39 @@ -import useBillingState, { - BillingState, - ProStatus, -} from '@/app/hooks/useBillingState' +import { useCallback, useEffect, useState } from 'react' +import useBillingState, { ProStatus } from '@/app/hooks/useBillingState' import { useMainStore } from '@/app/store/useMainStore' +import { calculateWeeklyWordCount } from '@/app/utils/userMetrics' interface SubscriptionStatusWidgetProps { - wordsUsed?: number navExpanded?: boolean } const FREE_TIER_WORD_LIMIT = 5000 export function SubscriptionStatusWidget({ - wordsUsed = 1000, navExpanded = true, }: SubscriptionStatusWidgetProps) { const billingState = useBillingState() const { setCurrentPage, setSettingsPage } = useMainStore() + const [weeklyWords, setWeeklyWords] = useState(0) + + const loadWeeklyWords = useCallback(async () => { + try { + const allInteractions = await window.api.interactions.getAll() + const weeklyCount = calculateWeeklyWordCount(allInteractions) + setWeeklyWords(weeklyCount) + } catch (error) { + console.error('Failed to load weekly word count:', error) + } + }, []) + + useEffect(() => { + loadWeeklyWords() + + // Listen for new interactions to update count + const unsubscribe = window.api.on('interaction-created', loadWeeklyWords) + + return unsubscribe + }, [loadWeeklyWords]) const handleUpgradeClick = () => { setCurrentPage('settings') @@ -81,7 +98,7 @@ export function SubscriptionStatusWidget({ // Show free tier status (Ito Starter) const totalWords = FREE_TIER_WORD_LIMIT - const usagePercentage = Math.min(100, (wordsUsed / totalWords) * 100) + const usagePercentage = Math.min(100, (weeklyWords / totalWords) * 100) return (
@@ -101,7 +118,7 @@ export function SubscriptionStatusWidget({
You've used{' '} - {wordsUsed.toLocaleString()} of {totalWords.toLocaleString()} + {weeklyWords.toLocaleString()} of {totalWords.toLocaleString()} {' '} words this week
diff --git a/app/components/home/contents/HomeContent.tsx b/app/components/home/contents/HomeContent.tsx index c10d9948..6e11df52 100644 --- a/app/components/home/contents/HomeContent.tsx +++ b/app/components/home/contents/HomeContent.tsx @@ -30,13 +30,7 @@ import { createStereo48kWavFromMonoPCM } from '@/app/utils/audioUtils' import { KeyName } from '@/lib/types/keyboard' import { usePlatform } from '@/app/hooks/usePlatform' import { BillingModals } from './BillingModals' - -// Interface for interaction statistics -interface InteractionStats { - streakDays: number - totalWords: number - averageWPM: number -} +import { calculateAllStats, InteractionStats } from '@/app/utils/userMetrics' const StatCard = ({ title, @@ -80,114 +74,10 @@ export default function HomeContent() { const [stats, setStats] = useState({ streakDays: 0, totalWords: 0, + weeklyWords: 0, averageWPM: 0, }) - // Calculate statistics from interactions - const calculateStats = useCallback( - (interactions: Interaction[]): InteractionStats => { - if (interactions.length === 0) { - return { streakDays: 0, totalWords: 0, averageWPM: 0 } - } - - // Calculate streak (consecutive days with interactions) - const streakDays = calculateStreak(interactions) - - // Calculate total words from transcripts - const totalWords = calculateTotalWords(interactions) - - // Calculate average WPM (estimate based on average speaking rate) - const averageWPM = calculateAverageWPM(interactions) - - return { streakDays, totalWords, averageWPM } - }, - [], - ) - - const calculateStreak = (interactions: Interaction[]): number => { - if (interactions.length === 0) return 0 - - // Group interactions by date - const dateGroups = new Map() - interactions.forEach(interaction => { - const date = new Date(interaction.created_at).toDateString() - if (!dateGroups.has(date)) { - dateGroups.set(date, []) - } - dateGroups.get(date)!.push(interaction) - }) - - // Sort dates in descending order (most recent first) - const sortedDates = Array.from(dateGroups.keys()).sort( - (a, b) => new Date(b).getTime() - new Date(a).getTime(), - ) - - let streak = 0 - const today = new Date() - - for (let i = 0; i < sortedDates.length; i++) { - const currentDate = new Date(sortedDates[i]) - const expectedDate = new Date(today) - expectedDate.setDate(today.getDate() - i) - - // Check if current date matches expected date (allowing for today or previous consecutive days) - if (currentDate.toDateString() === expectedDate.toDateString()) { - streak++ - } else { - break - } - } - - return streak - } - - const calculateTotalWords = (interactions: Interaction[]): number => { - return interactions.reduce((total, interaction) => { - const transcript = interaction.asr_output?.transcript?.trim() - if (transcript) { - // Count words by splitting on whitespace and filtering out empty strings - const words = transcript - .split(/\s+/) - .filter((word: string) => word.length > 0) - return total + words.length - } - return total - }, 0) - } - - const calculateAverageWPM = (interactions: Interaction[]): number => { - const validInteractions = interactions.filter( - interaction => - interaction.asr_output?.transcript?.trim() && interaction.duration_ms, - ) - - if (validInteractions.length === 0) return 0 - - let totalWords = 0 - let totalDurationMs = 0 - - validInteractions.forEach(interaction => { - const transcript = interaction.asr_output?.transcript?.trim() - if (transcript && interaction.duration_ms) { - // Count words by splitting on whitespace and filtering out empty strings - const words = transcript - .split(/\s+/) - .filter((word: string) => word.length > 0) - totalWords += words.length - totalDurationMs += interaction.duration_ms - } - }) - - if (totalDurationMs === 0) return 0 - - // Calculate WPM: (total words / total duration in minutes) - const totalMinutes = totalDurationMs / (1000 * 60) - const wpm = totalWords / totalMinutes - - // Round to nearest integer and ensure it's reasonable - return Math.round(Math.max(1, wpm)) - } - const formatStreakText = (days: number): string => { if (days === 0) return '0 days' if (days === 1) return '1 day' @@ -210,14 +100,14 @@ export default function HomeContent() { setInteractions(sortedInteractions) // Calculate and set statistics - const calculatedStats = calculateStats(sortedInteractions) + const calculatedStats = calculateAllStats(sortedInteractions) setStats(calculatedStats) } catch (error) { console.error('Failed to load interactions:', error) } finally { setLoading(false) } - }, [calculateStats]) + }, []) useEffect(() => { loadInteractions() diff --git a/app/components/home/contents/settings/PricingBillingSettingsContent.tsx b/app/components/home/contents/settings/PricingBillingSettingsContent.tsx index 2eb31863..5e38ca2f 100644 --- a/app/components/home/contents/settings/PricingBillingSettingsContent.tsx +++ b/app/components/home/contents/settings/PricingBillingSettingsContent.tsx @@ -3,10 +3,7 @@ import { Button } from '@/app/components/ui/button' import { Check } from '@mynaui/icons-react' import useBillingState, { ProStatus } from '@/app/hooks/useBillingState' -type BillingPeriod = 'monthly' | 'annual' - export default function PricingBillingSettingsContent() { - const [billingPeriod, setBillingPeriod] = useState('annual') const billingState = useBillingState() const [checkoutLoading, setCheckoutLoading] = useState(false) const [checkoutError, setCheckoutError] = useState(null) diff --git a/app/utils/userMetrics.ts b/app/utils/userMetrics.ts new file mode 100644 index 00000000..b56edb94 --- /dev/null +++ b/app/utils/userMetrics.ts @@ -0,0 +1,148 @@ +import { Interaction } from '@/lib/main/sqlite/models' + +/** + * Get the start of the current week (Monday at 00:00:00) + */ +export const getStartOfWeek = (date: Date = new Date()): Date => { + const start = new Date(date) + const day = start.getDay() + // Convert Sunday (0) to 7, then subtract to get to Monday + const diff = day === 0 ? -6 : 1 - day + start.setDate(start.getDate() + diff) + start.setHours(0, 0, 0, 0) + return start +} + +/** + * Calculate total words from all interactions + */ +export const calculateTotalWords = (interactions: Interaction[]): number => { + return interactions.reduce((total, interaction) => { + const transcript = interaction.asr_output?.transcript?.trim() + if (transcript) { + // Count words by splitting on whitespace and filtering out empty strings + const words = transcript + .split(/\s+/) + .filter((word: string) => word.length > 0) + return total + words.length + } + return total + }, 0) +} + +/** + * Calculate word count for the current week (Monday - Sunday) + */ +export const calculateWeeklyWordCount = ( + interactions: Interaction[], +): number => { + const weekStart = getStartOfWeek() + + const weeklyInteractions = interactions.filter(interaction => { + const interactionDate = new Date(interaction.created_at) + return interactionDate >= weekStart + }) + + return calculateTotalWords(weeklyInteractions) +} + +/** + * Calculate average words per minute across all interactions + */ +export const calculateAverageWPM = (interactions: Interaction[]): number => { + const validInteractions = interactions.filter( + interaction => + interaction.asr_output?.transcript?.trim() && interaction.duration_ms, + ) + + if (validInteractions.length === 0) return 0 + + let totalWords = 0 + let totalDurationMs = 0 + + validInteractions.forEach(interaction => { + const transcript = interaction.asr_output?.transcript?.trim() + if (transcript && interaction.duration_ms) { + // Count words by splitting on whitespace and filtering out empty strings + const words = transcript + .split(/\s+/) + .filter((word: string) => word.length > 0) + totalWords += words.length + totalDurationMs += interaction.duration_ms + } + }) + + if (totalDurationMs === 0) return 0 + + // Calculate WPM: (total words / total duration in minutes) + const totalMinutes = totalDurationMs / (1000 * 60) + const wpm = totalWords / totalMinutes + + // Round to nearest integer and ensure it's reasonable + return Math.round(Math.max(1, wpm)) +} + +/** + * Calculate consecutive days streak (from most recent day backwards) + */ +export const calculateStreak = (interactions: Interaction[]): number => { + if (interactions.length === 0) return 0 + + // Group interactions by date + const dateGroups = new Map() + interactions.forEach(interaction => { + const date = new Date(interaction.created_at).toDateString() + if (!dateGroups.has(date)) { + dateGroups.set(date, []) + } + dateGroups.get(date)!.push(interaction) + }) + + // Sort dates in descending order (most recent first) + const sortedDates = Array.from(dateGroups.keys()).sort( + (a, b) => new Date(b).getTime() - new Date(a).getTime(), + ) + + let streak = 0 + const today = new Date() + + for (let i = 0; i < sortedDates.length; i++) { + const currentDate = new Date(sortedDates[i]) + const expectedDate = new Date(today) + expectedDate.setDate(today.getDate() - i) + + // Check if current date matches expected date (allowing for today or previous consecutive days) + if (currentDate.toDateString() === expectedDate.toDateString()) { + streak++ + } else { + break + } + } + + return streak +} + +/** + * Calculate all interaction statistics at once + */ +export interface InteractionStats { + streakDays: number + totalWords: number + weeklyWords: number + averageWPM: number +} + +export const calculateAllStats = ( + interactions: Interaction[], +): InteractionStats => { + if (interactions.length === 0) { + return { streakDays: 0, totalWords: 0, weeklyWords: 0, averageWPM: 0 } + } + + return { + streakDays: calculateStreak(interactions), + totalWords: calculateTotalWords(interactions), + weeklyWords: calculateWeeklyWordCount(interactions), + averageWPM: calculateAverageWPM(interactions), + } +} diff --git a/server/src/services/ito/transcribeStreamV2Handler.ts b/server/src/services/ito/transcribeStreamV2Handler.ts index 70c85b7c..8888d038 100644 --- a/server/src/services/ito/transcribeStreamV2Handler.ts +++ b/server/src/services/ito/transcribeStreamV2Handler.ts @@ -309,7 +309,10 @@ export class TranscribeStreamV2Handler { noSpeechThreshold: number, ) { return { - asrModel: this.resolveOrDefault(asrModel, DEFAULT_ADVANCED_SETTINGS.asrModel), + asrModel: this.resolveOrDefault( + asrModel, + DEFAULT_ADVANCED_SETTINGS.asrModel, + ), asrProvider: this.resolveOrDefault( asrProvider, DEFAULT_ADVANCED_SETTINGS.asrProvider,