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,