Skip to content
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
59 changes: 39 additions & 20 deletions app/components/home/contents/HomeContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { EXTERNAL_LINKS } from '@/lib/constants/external-links'
import { useSettingsStore } from '../../../store/useSettingsStore'
import { Tooltip, TooltipTrigger, TooltipContent } from '../../ui/tooltip'
import { useAuthStore } from '@/app/store/useAuthStore'
import { Interaction } from '@/lib/main/sqlite/models'
import { InteractionSummary } from '@/lib/main/sqlite/models'
import { TotalWordsIcon } from '../../icons/TotalWordsIcon'
import { SpeedIcon } from '../../icons/SpeedIcon'
import {
Expand Down Expand Up @@ -77,7 +77,7 @@ export default function HomeContent({
const { user } = useAuthStore()
const firstName = user?.name?.split(' ')[0]
const platform = usePlatform()
const [interactions, setInteractions] = useState<Interaction[]>([])
const [interactions, setInteractions] = useState<InteractionSummary[]>([])
const [loading, setLoading] = useState(true)
const [playingAudio, setPlayingAudio] = useState<string | null>(null)
const [audioInstances, setAudioInstances] = useState<
Expand Down Expand Up @@ -177,7 +177,7 @@ export default function HomeContent({

// Calculate statistics from interactions
const calculateStats = useCallback(
(interactions: Interaction[]): InteractionStats => {
(interactions: InteractionSummary[]): InteractionStats => {
if (interactions.length === 0) {
return { streakDays: 0, totalWords: 0, averageWPM: 0 }
}
Expand All @@ -196,11 +196,11 @@ export default function HomeContent({
[],
)

const calculateStreak = (interactions: Interaction[]): number => {
const calculateStreak = (interactions: InteractionSummary[]): number => {
if (interactions.length === 0) return 0

// Group interactions by date
const dateGroups = new Map<string, Interaction[]>()
const dateGroups = new Map<string, InteractionSummary[]>()
interactions.forEach(interaction => {
const date = new Date(interaction.created_at).toDateString()
if (!dateGroups.has(date)) {
Expand Down Expand Up @@ -233,7 +233,7 @@ export default function HomeContent({
return streak
}

const calculateTotalWords = (interactions: Interaction[]): number => {
const calculateTotalWords = (interactions: InteractionSummary[]): number => {
return interactions.reduce((total, interaction) => {
const transcript = interaction.asr_output?.transcript?.trim()
if (transcript) {
Expand All @@ -245,7 +245,7 @@ export default function HomeContent({
}, 0)
}

const calculateAverageWPM = (interactions: Interaction[]): number => {
const calculateAverageWPM = (interactions: InteractionSummary[]): number => {
const validInteractions = interactions.filter(
interaction =>
interaction.asr_output?.transcript?.trim() && interaction.duration_ms,
Expand Down Expand Up @@ -292,7 +292,7 @@ export default function HomeContent({

// Sort by creation date (newest first) - remove the slice(0, 10) to show all interactions
const sortedInteractions = allInteractions.sort(
(a: Interaction, b: Interaction) =>
(a: InteractionSummary, b: InteractionSummary) =>
new Date(b.created_at).getTime() - new Date(a.created_at).getTime(),
)
setInteractions(sortedInteractions)
Expand Down Expand Up @@ -372,8 +372,8 @@ export default function HomeContent({
.toUpperCase()
}

const groupInteractionsByDate = (interactions: Interaction[]) => {
const groups: { [key: string]: Interaction[] } = {}
const groupInteractionsByDate = (interactions: InteractionSummary[]) => {
const groups: { [key: string]: InteractionSummary[] } = {}

interactions.forEach(interaction => {
const dateKey = formatDate(interaction.created_at)
Expand All @@ -386,7 +386,7 @@ export default function HomeContent({
return groups
}

const getDisplayText = (interaction: Interaction) => {
const getDisplayText = (interaction: InteractionSummary) => {
// Check for errors first
if (interaction.asr_output?.error) {
// Prefer precise error code mapping when available
Expand Down Expand Up @@ -435,7 +435,7 @@ export default function HomeContent({
}
}

const handleAudioPlayStop = async (interaction: Interaction) => {
const handleAudioPlayStop = async (interaction: InteractionSummary) => {
try {
// If this interaction is currently playing, stop it
if (playingAudio === interaction.id) {
Expand Down Expand Up @@ -463,7 +463,7 @@ export default function HomeContent({
}
}

if (!interaction.raw_audio) {
if (!interaction.has_audio) {
console.warn('No audio data available for this interaction')
return
}
Expand All @@ -475,7 +475,17 @@ export default function HomeContent({
let audio = audioInstances.get(interaction.id)

if (!audio) {
const pcmData = new Uint8Array(interaction.raw_audio)
// The list rows don't carry audio (it can be megabytes per row);
// fetch the full record on demand.
const fullInteraction = await window.api.interactions.getById(
interaction.id,
)
if (!fullInteraction?.raw_audio) {
console.warn('No audio data available for this interaction')
setPlayingAudio(null)
return
}
const pcmData = new Uint8Array(fullInteraction.raw_audio)
try {
// Convert raw PCM (mono, typically 16 kHz) to 48 kHz stereo WAV for smoother playback
const wavBuffer = createStereo48kWavFromMonoPCM(
Expand Down Expand Up @@ -546,14 +556,23 @@ export default function HomeContent({
}
}

const handleAudioDownload = async (interaction: Interaction) => {
const handleAudioDownload = async (interaction: InteractionSummary) => {
try {
if (!interaction.raw_audio) {
if (!interaction.has_audio) {
console.warn('No audio data available for download')
return
}

// Fetch the full record on demand — list rows don't carry audio.
const fullInteraction = await window.api.interactions.getById(
interaction.id,
)
if (!fullInteraction?.raw_audio) {
console.warn('No audio data available for download')
return
}

const pcmData = new Uint8Array(interaction.raw_audio)
const pcmData = new Uint8Array(fullInteraction.raw_audio)
// Convert raw PCM to WAV format
const wavBuffer = createStereo48kWavFromMonoPCM(
pcmData,
Expand Down Expand Up @@ -787,7 +806,7 @@ export default function HomeContent({
)}

{/* Download button */}
{interaction.raw_audio && (
{interaction.has_audio && (
<Tooltip
open={
openTooltipKey === `download:${interaction.id}`
Expand Down Expand Up @@ -831,7 +850,7 @@ export default function HomeContent({
: 'text-gray-600'
}`}
onClick={() => handleAudioPlayStop(interaction)}
disabled={!interaction.raw_audio}
disabled={!interaction.has_audio}
>
{playingAudio === interaction.id ? (
<Stop className="w-4 h-4" />
Expand All @@ -841,7 +860,7 @@ export default function HomeContent({
</button>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={5}>
{!interaction.raw_audio
{!interaction.has_audio
? 'No audio available'
: playingAudio === interaction.id
? 'Stop'
Expand Down
33 changes: 20 additions & 13 deletions app/components/home/contents/settings/AudioSettingsContent.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Switch } from '@/app/components/ui/switch'
import { MicrophoneSelector } from '@/app/components/ui/microphone-selector'
import { useSettingsStore } from '@/app/store/useSettingsStore'
import { usePlatform } from '@/app/hooks/usePlatform'

export default function AudioSettingsContent() {
const {
Expand All @@ -12,6 +13,7 @@ export default function AudioSettingsContent() {
// setInteractionSounds,
setMuteAudioWhenDictating,
} = useSettingsStore()
const platform = usePlatform()

return (
<div className="space-y-8">
Expand All @@ -30,28 +32,33 @@ export default function AudioSettingsContent() {
/>
</div> */}

<div className="flex items-center justify-between">
<div>
<div className="text-sm font-medium">
Mute audio when dictating
</div>
<div className="text-xs text-gray-600 mt-1">
Automatically silence other active audio during dictation.
{/* System-audio mute is implemented via osascript (macOS only);
hide the toggle elsewhere instead of showing a silent no-op. */}
{platform === 'darwin' && (
<div className="flex items-center justify-between">
<div>
<div className="text-sm font-medium">
Mute audio when dictating
</div>
<div className="text-xs text-gray-600 mt-1">
Automatically silence other active audio during dictation.
</div>
</div>
<Switch
checked={muteAudioWhenDictating}
onCheckedChange={setMuteAudioWhenDictating}
/>
</div>
<Switch
checked={muteAudioWhenDictating}
onCheckedChange={setMuteAudioWhenDictating}
/>
</div>
)}

<div className="flex justify-between">
<div>
<div className="text-sm font-medium mb-2">
Select default microphone
</div>
<div className="text-xs text-gray-600 mt-1">
Select the microphone Scriba will use by default for audio input.
Select the microphone Scriba will use by default for audio
input.
</div>
</div>
<MicrophoneSelector
Expand Down
12 changes: 12 additions & 0 deletions build-binaries.sh
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,18 @@ build_native_workspace() {
# Cross-compile from macOS/Linux using GNU toolchain
print_info "Cross-compiling with GNU toolchain..."
cargo build --release --target x86_64-pc-windows-gnu

# electron-builder and the dev-mode binary resolver only look in
# the MSVC target dir (CI builds MSVC on Windows runners). Mirror
# the GNU output there so the local cross-compile flow produces a
# packageable layout too.
gnu_dir="target/x86_64-pc-windows-gnu/release"
msvc_dir="target/x86_64-pc-windows-msvc/release"
mkdir -p "$msvc_dir"
for exe in "$gnu_dir"/*.exe; do
[ -e "$exe" ] && cp -f "$exe" "$msvc_dir/"
done
print_info "Mirrored GNU-built .exe files into $msvc_dir for packaging"
fi
fi

Expand Down
21 changes: 17 additions & 4 deletions lib/main/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ let pillWindow: BrowserWindow | null = null
// Keep a reference to the main window
export let mainWindow: BrowserWindow | null = null

// Distinguishes a real quit from a dashboard-close, so Windows can hide to
// tray on close (like macOS) instead of killing background dictation.
let appIsQuitting = false
app.on('before-quit', () => {
appIsQuitting = true
})

export function getPillWindow(): BrowserWindow | null {
return pillWindow
}
Expand Down Expand Up @@ -61,13 +68,19 @@ export function createAppWindow(): BrowserWindow {
},
)

// On Windows, closing the dashboard hides it to the tray — the app (pill,
// hotkeys, dictation) keeps running, mirroring the macOS behavior. A real
// quit is still available from the tray menu.
mainWindow.on('close', event => {
if (process.platform === 'win32' && !appIsQuitting) {
event.preventDefault()
mainWindow?.hide()
}
})

// Clean up the reference when the window is closed.
mainWindow.on('closed', () => {
mainWindow = null
// On Windows, closing the main window should quit the entire app
if (process.platform === 'win32') {
app.quit()
}
})

// HMR for renderer base on electron-vite cli.
Expand Down
Loading
Loading