Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.voicetypr.com/llms.txt

Use this file to discover all available pages before exploring further.

Overview

Consistent code style makes the codebase easier to read, review, and maintain. Follow these guidelines for all contributions.

Frontend Style (React + TypeScript)

TypeScript

Strict Type Safety

// ✅ Good - Explicit types
interface RecordingState {
  isRecording: boolean;
  duration: number;
  audioData: Float32Array | null;
}

const startRecording = async (): Promise<void> => {
  // Implementation
};

// ❌ Bad - Using 'any'
const processData = (data: any) => {
  // Avoid 'any' type
};

Type Imports

// ✅ Good - Type-only imports
import type { AppState } from '@/types';
import { useState } from 'react';

// ❌ Bad - Mixed imports
import { AppState } from '@/types';

React Components

Function Components

// ✅ Good - Named function component
export function RecordButton({ onClick }: RecordButtonProps) {
  const [isRecording, setIsRecording] = useState(false);
  
  return (
    <Button onClick={onClick} variant="destructive">
      {isRecording ? 'Stop' : 'Record'}
    </Button>
  );
}

// ❌ Bad - Arrow function default export
export default ({ onClick }) => {
  // Harder to debug and find
};

Props Interface

// ✅ Good - Clear interface
interface ModelCardProps {
  name: string;
  size: number;
  isDownloaded: boolean;
  onDownload: () => void;
}

export function ModelCard({ name, size, isDownloaded, onDownload }: ModelCardProps) {
  // Implementation
}

Hooks

Custom Hooks

// ✅ Good - Descriptive hook name
export function useTranscription() {
  const [text, setText] = useState('');
  const [isProcessing, setIsProcessing] = useState(false);
  
  const transcribe = async (audioPath: string) => {
    setIsProcessing(true);
    try {
      const result = await invoke('transcribe_audio', { audioPath });
      setText(result);
    } finally {
      setIsProcessing(false);
    }
  };
  
  return { text, isProcessing, transcribe };
}

Hook Dependencies

// ✅ Good - Explicit dependencies
useEffect(() => {
  const loadModel = async () => {
    await invoke('load_model', { name: modelName });
  };
  loadModel();
}, [modelName]); // Clear dependency

// ❌ Bad - Missing or wrong dependencies
useEffect(() => {
  loadModel(); // modelName used but not listed
}, []); // ESLint will warn

Import Conventions

Path Aliases

Always use @/* instead of relative paths:
// ✅ Good - Path alias
import { Button } from '@/components/ui/button';
import { useSettings } from '@/hooks/useSettings';
import type { AppState } from '@/types';

// ❌ Bad - Relative paths
import { Button } from '../../../components/ui/button';
import { useSettings } from '../../hooks/useSettings';

Import Order

// 1. React imports
import { useState, useEffect } from 'react';

// 2. Third-party imports
import { invoke } from '@tauri-apps/api/core';
import { listen } from '@tauri-apps/api/event';

// 3. Local imports (components, hooks, utils)
import { Button } from '@/components/ui/button';
import { useRecording } from '@/hooks/useRecording';
import { formatDuration } from '@/utils/time';

// 4. Type imports
import type { RecordingState } from '@/types';

// 5. Styles
import './styles.css';

Tailwind CSS

Class Ordering

Use a consistent order:
// Layout → Spacing → Sizing → Typography → Visual → Effects
<div className="
  flex items-center justify-between  // Layout
  p-4 gap-2                          // Spacing
  w-full h-12                        // Sizing
  text-sm font-medium               // Typography
  bg-primary text-white             // Visual
  rounded-lg shadow-sm              // Effects
  hover:bg-primary/90               // States
">

Conditional Classes

// ✅ Good - Use clsx or cn helper
import { cn } from '@/lib/utils';

<Button className={cn(
  "base-classes",
  isActive && "active-classes",
  variant === "destructive" && "destructive-classes"
)}>

// ❌ Bad - Template strings
<Button className={`base-classes ${isActive ? 'active-classes' : ''}`}>

Backend Style (Rust)

Formatting

Always run cargo fmt before committing:
cd src-tauri
cargo fmt
Configuration (.rustfmt.toml):
max_width = 100
tab_spaces = 4
use_field_init_shorthand = true
use_try_shorthand = true

Clippy Lints

Run cargo clippy to catch common issues:
cd src-tauri
cargo clippy -- -D warnings
Fix suggestions:
// ✅ Good - Following clippy suggestions
if let Some(value) = option {
    process(value);
}

// ❌ Bad - Clippy will suggest if-let
match option {
    Some(value) => process(value),
    None => {},
}

Error Handling

Result Types

// ✅ Good - Descriptive error types
pub async fn start_recording() -> Result<(), RecordingError> {
    // Implementation
}

// ❌ Bad - Generic error
pub async fn start_recording() -> Result<(), String> {
    // Less type-safe
}

Custom Errors

use thiserror::Error;

#[derive(Error, Debug)]
pub enum RecordingError {
    #[error("Failed to start audio device: {0}")]
    DeviceError(String),
    
    #[error("Recording already in progress")]
    AlreadyRecording,
    
    #[error("No audio data available")]
    NoData,
}

Async Code

Tauri Commands

// ✅ Good - Async command with proper error handling
#[tauri::command]
async fn download_model(
    name: String,
    app: tauri::AppHandle,
    state: State<'_, AppState>,
) -> Result<(), String> {
    let mut downloader = state.downloader.lock().await;
    downloader.download(&name, &app).await
        .map_err(|e| e.to_string())
}

Tokio Best Practices

// ✅ Good - Spawn tasks for concurrent work
tokio::spawn(async move {
    let _ = preload_model("base").await;
});

// ✅ Good - Use timeout for network operations
let result = tokio::time::timeout(
    Duration::from_secs(30),
    download_file(url),
).await??;

State Management

// ✅ Good - Thread-safe state
pub struct AppState {
    pub recording: Arc<Mutex<Option<RecordingState>>>,
    pub whisper: Arc<Mutex<Option<WhisperEngine>>>,
}

// Access in commands
#[tauri::command]
async fn get_status(state: State<'_, AppState>) -> Result<String, String> {
    let recording = state.recording.lock().await;
    Ok(format!("Status: {:?}", *recording))
}

Naming Conventions

// Modules: snake_case
mod audio_processor;
mod whisper_engine;

// Types: PascalCase
struct RecordingState;
enum TranscriptionEngine;

// Functions: snake_case
fn start_recording() {}
async fn transcribe_audio() {}

// Constants: SCREAMING_SNAKE_CASE
const MAX_RECORDING_DURATION: u64 = 300;
const DEFAULT_SAMPLE_RATE: u32 = 16000;

Testing Patterns

Frontend Tests

import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

test('user can start recording', async () => {
  const user = userEvent.setup();
  render(<App />);
  
  // Use accessible queries
  const recordButton = screen.getByRole('button', { name: /record/i });
  await user.click(recordButton);
  
  // Wait for async updates
  await waitFor(() => {
    expect(screen.getByText(/recording/i)).toBeInTheDocument();
  });
});

Backend Tests

#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    async fn test_recording_lifecycle() {
        let recorder = AudioRecorder::new();
        
        // Start recording
        recorder.start().await.unwrap();
        assert!(recorder.is_recording());
        
        // Wait and stop
        tokio::time::sleep(Duration::from_millis(100)).await;
        let data = recorder.stop().await.unwrap();
        
        // Verify
        assert!(!data.is_empty());
    }
}

Documentation

TypeScript JSDoc

/**
 * Download a Whisper model from remote server
 * 
 * @param modelName - Name of the model (tiny, base, small, medium, large)
 * @param onProgress - Optional progress callback (0-1)
 * @returns Promise that resolves when download completes
 * 
 * @example
 * await downloadModel('base', (progress) => {
 *   console.log(`Progress: ${progress * 100}%`);
 * });
 */
export async function downloadModel(
  modelName: string,
  onProgress?: (progress: number) => void
): Promise<void> {
  // Implementation
}

Rust Doc Comments

/// Starts recording audio from the default input device
/// 
/// # Returns
/// 
/// Returns `Ok(())` if recording started successfully, or an error if:
/// - No audio input device is available
/// - Recording is already in progress
/// - Permissions are not granted
/// 
/// # Examples
/// 
/// ```
/// let recorder = AudioRecorder::new();
/// recorder.start().await?;
/// ```
pub async fn start(&mut self) -> Result<(), RecordingError> {
    // Implementation
}

Pre-Commit Checks

Before every commit, run:
pnpm quality-gate
This runs:
  1. TypeScript - pnpm typecheck
  2. ESLint - pnpm lint
  3. Frontend tests - pnpm test run
  4. Backend tests - pnpm test:backend

Manual Checks

# Format frontend code
pnpm format

# Format backend code
cd src-tauri && cargo fmt

# Run clippy
cd src-tauri && cargo clippy

Editor Configuration

VS Code Settings

.vscode/settings.json:
{
  "editor.formatOnSave": true,
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
  },
  "[rust]": {
    "editor.defaultFormatter": "rust-lang.rust-analyzer",
    "editor.formatOnSave": true
  },
  "[typescript]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "tailwindCSS.experimental.classRegex": [
    ["cn\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]
  ]
}

Common Pitfalls

TypeScript

// ❌ Don't use 'any'
const data: any = await invoke('get_data');

// ✅ Define proper types
interface ApiResponse {
  status: string;
  data: unknown;
}
const data: ApiResponse = await invoke('get_data');

// ❌ Don't ignore null/undefined
const value = data.field.nested; // Can crash

// ✅ Use optional chaining
const value = data?.field?.nested;

Rust

// ❌ Don't unwrap in production code
let value = option.unwrap();

// ✅ Handle errors properly
let value = option.ok_or("Value not found")?;

// ❌ Don't clone unnecessarily
let copy = expensive_data.clone();

// ✅ Use references
let reference = &expensive_data;

Next Steps