Skip to content
This repository was archived by the owner on Jan 13, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 25 additions & 8 deletions app/components/home/SubscriptionStatusWidget.tsx
Original file line number Diff line number Diff line change
@@ -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')
Expand Down Expand Up @@ -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 (
<div className={cardClassName}>
Expand All @@ -101,7 +118,7 @@ export function SubscriptionStatusWidget({
<div className="text-xs">
You've used{' '}
<span className="font-medium">
{wordsUsed.toLocaleString()} of {totalWords.toLocaleString()}
{weeklyWords.toLocaleString()} of {totalWords.toLocaleString()}
</span>{' '}
words this week
</div>
Expand Down
118 changes: 4 additions & 114 deletions app/components/home/contents/HomeContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -80,114 +74,10 @@ export default function HomeContent() {
const [stats, setStats] = useState<InteractionStats>({
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<string, Interaction[]>()
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'
Expand All @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<BillingPeriod>('annual')
const billingState = useBillingState()
const [checkoutLoading, setCheckoutLoading] = useState(false)
const [checkoutError, setCheckoutError] = useState<string | null>(null)
Expand Down
148 changes: 148 additions & 0 deletions app/utils/userMetrics.ts
Original file line number Diff line number Diff line change
@@ -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<string, Interaction[]>()
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),
}
}
5 changes: 4 additions & 1 deletion server/src/services/ito/transcribeStreamV2Handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down