format everything (#323)

This commit is contained in:
CJ Pais 2025-11-10 11:39:14 +08:00 committed by GitHub
parent a80c37097b
commit 70ec82285a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
45 changed files with 528 additions and 387 deletions

View file

@ -1,9 +1,9 @@
---
name: Bug Report
about: Create a report to help us improve Handy
title: '[BUG] '
labels: ['bug']
assignees: ''
title: "[BUG] "
labels: ["bug"]
assignees: ""
---
## Before You Submit
@ -11,17 +11,23 @@ assignees: ''
**Please search [existing issues](https://github.com/cjpais/Handy/issues) to avoid duplicates.** Your bug may already be reported! Right now it's just me maintaining this project so many issues can be overwhelming! Help me out by checking first.
## Bug Description
A clear and concise description of what the bug is.
## System Information
**App Version:**
<!-- You can find this in the app settings or about section -->
**Operating System:**
<!-- e.g., macOS 14.1, Windows 11, Ubuntu 22.04 -->
**CPU:**
<!-- e.g., Apple M2, Intel i7-12700K, AMD Ryzen 7 5800X -->
**GPU:**
<!-- e.g., Apple M2 GPU, NVIDIA RTX 4080, AMD RX 6800 XT, Intel UHD Graphics -->

View file

@ -1,9 +1,9 @@
---
name: Feature Request
about: Suggest an idea or new feature for Handy
title: ''
title: ""
labels: []
assignees: ''
assignees: ""
---
## 🎯 Feature Requests Go in Discussions!
@ -13,6 +13,7 @@ Thanks for your interest in improving Handy!
**Please post feature requests in our [Discussions tab](https://github.com/cjpais/Handy/discussions) instead of opening an issue.**
This helps us:
- Keep issues focused on bugs and actionable tasks
- Have more open-ended conversations about features
- Gather community feedback and input

20
.prettierignore Normal file
View file

@ -0,0 +1,20 @@
# Dependencies
node_modules
bun.lock
package-lock.json
# Build outputs
dist
target
*.bundle.*
# Tauri
src-tauri/target
src-tauri/gen
# Generated files
src/bindings.ts
# Misc
.DS_Store
*.log

3
.prettierrc Normal file
View file

@ -0,0 +1,3 @@
{
"endOfLine": "lf"
}

View file

@ -1,3 +1,7 @@
{
"recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"]
"recommendations": [
"tauri-apps.tauri-vscode",
"rust-lang.rust-analyzer",
"esbenp.prettier-vscode"
]
}

View file

@ -5,10 +5,12 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Development Commands
**Prerequisites:**
- [Rust](https://rustup.rs/) (latest stable)
- [Bun](https://bun.sh/) package manager
**Core Development:**
```bash
# Install dependencies
bun install
@ -28,6 +30,7 @@ bun run preview # Preview built frontend
```
**Model Setup (Required for Development):**
```bash
# Create models directory
mkdir -p src-tauri/resources/models
@ -43,6 +46,7 @@ Handy is a cross-platform desktop speech-to-text application built with Tauri (R
### Core Components
**Backend (Rust - src-tauri/src/):**
- `lib.rs` - Main application entry point with Tauri setup, tray menu, and managers
- `managers/` - Core business logic managers:
- `audio.rs` - Audio recording and device management
@ -56,6 +60,7 @@ Handy is a cross-platform desktop speech-to-text application built with Tauri (R
- `settings.rs` - Application settings management
**Frontend (React/TypeScript - src/):**
- `App.tsx` - Main application component with onboarding flow
- `components/settings/` - Settings UI components
- `components/model-selector/` - Model management interface
@ -73,6 +78,7 @@ Handy is a cross-platform desktop speech-to-text application built with Tauri (R
### Technology Stack
**Core Libraries:**
- `whisper-rs` - Local Whisper inference with GPU acceleration
- `cpal` - Cross-platform audio I/O
- `vad-rs` - Voice Activity Detection
@ -81,6 +87,7 @@ Handy is a cross-platform desktop speech-to-text application built with Tauri (R
- `rodio` - Audio playback for feedback sounds
**Platform-Specific Features:**
- macOS: Metal acceleration for Whisper, accessibility permissions
- Windows: Vulkan acceleration, code signing
- Linux: OpenBLAS + Vulkan acceleration
@ -96,6 +103,7 @@ Handy is a cross-platform desktop speech-to-text application built with Tauri (R
### Settings System
Settings are stored using Tauri's store plugin with reactive updates:
- Keyboard shortcuts (configurable, supports push-to-talk)
- Audio devices (microphone/output selection)
- Model preferences (Small/Medium/Turbo/Large Whisper variants)

View file

@ -5,6 +5,7 @@ This guide covers how to set up the development environment and build Handy from
## Prerequisites
### All Platforms
- [Rust](https://rustup.rs/) (latest stable)
- [Bun](https://bun.sh/) package manager
- [Tauri Prerequisites](https://tauri.app/start/prerequisites/)
@ -12,18 +13,22 @@ This guide covers how to set up the development environment and build Handy from
### Platform-Specific Requirements
#### macOS
- Xcode Command Line Tools
- Install with: `xcode-select --install`
#### Windows
- Microsoft C++ Build Tools
- Visual Studio 2019/2022 with C++ development tools
- Or Visual Studio Build Tools 2019/2022
#### Linux
- Build essentials
- ALSA development libraries
- Install with:
```bash
# Ubuntu/Debian
sudo apt update
@ -42,15 +47,18 @@ This guide covers how to set up the development environment and build Handy from
## Setup Instructions
### 1. Clone the Repository
```bash
git clone git@github.com:cjpais/Handy.git
cd Handy
```
### 2. Install Dependencies
```bash
bun install
```
### 3. Download Required Models
Handy requires a VAD (Voice Activity Detection) model to function

View file

@ -3,38 +3,45 @@
## [0.3.0] - 2025-07-11
### Added
- **Translate to English** setting: Added automatic translation of speech to English
- Settings refactored into React hooks for better state management
- Audio device switching capability
- Hysteresis to VAD (Voice Activity Detection) for more stable recording
### Changed
- Major audio backend refactor for improved performance and reliability
- Moved audio toolkit into src-tauri directory for better permissions handling
- Model files no longer need to be downloaded separately for releases
- Updated settings components and transcription logic
### Fixed
- Audio toolkit permissions issues
- Various stability improvements
## [0.2.3] - 2025-07-03
### Fixed
- Keycode bug that was causing input issues
- Whisper model optimization: switched to unquantized Whisper Turbo, updated Whisper Medium quantization to 4_1
## [0.2.2] - 2025-07-02
### Fixed
- Removed 50ms delay feature flag for Windows (now applies to all platforms for consistency)
## [0.2.1] - 2025-07-01
### Added
- Ctrl+Space key binding for Windows platform
### Fixed
- Windows crash issue
- Model loading on startup when available
- Windows paste functionality bug
@ -42,54 +49,64 @@
## [0.2.0] - 2025-06-30
### Added
- **Microphone activation on demand**: More efficient resource usage
- Less permissive VAD settings for better accuracy
### Changed
- Improved microphone management and activation system
## [0.1.6] - 2025-06-30
### Added
- **Multiple models support**: Users can now select from different transcription models
- Model selection onboarding flow
- Cleanup and refactoring of model management
### Changed
- Enhanced user experience with model selection interface
- Better language and UI tweaks
## [0.1.5] - 2025-06-27
### Added
- **Different start and stop recording sounds**: Enhanced audio feedback
- Recording sound samples for better user experience
## [0.1.4] - 2025-06-27
### Fixed
- Build issues
- Auto-update functionality improvements
## [0.1.3] - 2025-06-26
### Fixed
- Paste functionality using enigo library for better cross-platform compatibility
## [0.1.2] - 2025-06-26
### Added
- **Auto-update functionality**: Application can now automatically update itself
- Footer displaying current version
- Improved menu system
### Changed
- Better user interface for version management
- Enhanced update workflow
## [0.1.1] - 2025-06-25
### Added
- **Comprehensive build system**: Support for Windows, macOS, and Linux
- Windows code signing for trusted installation
- Ubuntu/Linux build support with Vulkan
@ -97,15 +114,18 @@
- GitHub Actions CI/CD workflow
### Changed
- Improved build process and release workflow
- Better cross-platform compatibility
### Fixed
- Various build-related issues across platforms
## [0.1.0] - 2025-05-16
### Added
- **Initial release** of Handy
- Basic speech-to-text transcription functionality
- Voice Activity Detection (VAD) for automatic recording
@ -128,6 +148,7 @@
- **MIT License** for open-source distribution
### Technical Implementation
- Built with Tauri (Rust backend) and React (TypeScript frontend)
- Audio processing with cpal and whisper-rs
- Real-time transcription with performance optimizations

View file

@ -26,22 +26,26 @@ Before you begin, ensure you have the following installed:
1. **Fork the repository** on GitHub
2. **Clone your fork**:
```bash
git clone git@github.com:YOUR_USERNAME/Handy.git
cd Handy
```
3. **Add upstream remote**:
```bash
git remote add upstream git@github.com:cjpais/Handy.git
```
4. **Install dependencies**:
```bash
bun install
```
5. **Download required models**:
```bash
mkdir -p src-tauri/resources/models
curl -o src-tauri/resources/models/silero_vad_v4.onnx https://blob.handy.computer/silero_vad_v4.onnx
@ -61,6 +65,7 @@ For detailed platform-specific setup instructions, see [BUILD.md](BUILD.md).
Handy follows a clean architecture pattern:
**Backend (Rust - `src-tauri/src/`):**
- `lib.rs` - Main application entry point with Tauri setup
- `managers/` - Core business logic (audio, model, transcription)
- `audio_toolkit/` - Low-level audio processing (recording, VAD)
@ -69,6 +74,7 @@ Handy follows a clean architecture pattern:
- `settings.rs` - Application settings management
**Frontend (React/TypeScript - `src/`):**
- `App.tsx` - Main application component
- `components/` - React UI components
- `hooks/` - Reusable React hooks
@ -90,12 +96,14 @@ For more details, see the Architecture section in [README.md](README.md) or [AGE
When creating a bug report, please include:
**System Information:**
- App version (found in settings or about section)
- Operating System (e.g., macOS 14.1, Windows 11, Ubuntu 22.04)
- CPU (e.g., Apple M2, Intel i7-12700K, AMD Ryzen 7 5800X)
- GPU (e.g., Apple M2 GPU, NVIDIA RTX 4080, Intel UHD Graphics)
**Bug Details:**
- Clear description of the bug
- Steps to reproduce
- Expected behavior
@ -132,6 +140,7 @@ We use GitHub Discussions for feature requests rather than issues. This keeps is
### Development Workflow
1. **Create a feature branch**:
```bash
git checkout -b feature/your-feature-name
# or
@ -151,6 +160,7 @@ We use GitHub Discussions for feature requests rather than issues. This keeps is
- Use debug mode to verify audio/transcription behavior
4. **Commit your changes**:
```bash
git add .
git commit -m "feat: add your feature description"
@ -167,12 +177,14 @@ We use GitHub Discussions for feature requests rather than issues. This keeps is
- `chore:` for maintenance tasks
5. **Keep your fork updated**:
```bash
git fetch upstream
git rebase upstream/main
```
6. **Push to your fork**:
```bash
git push origin feature/your-feature-name
```
@ -191,6 +203,7 @@ We use GitHub Discussions for feature requests rather than issues. This keeps is
### Code Style Guidelines
**Rust:**
- Follow standard Rust formatting (`cargo fmt`)
- Run `cargo clippy` and address warnings
- Use descriptive variable and function names
@ -198,6 +211,7 @@ We use GitHub Discussions for feature requests rather than issues. This keeps is
- Handle errors explicitly (avoid unwrap in production code)
**TypeScript/React:**
- Use TypeScript strictly, avoid `any` types
- Follow React hooks best practices
- Use functional components
@ -205,6 +219,7 @@ We use GitHub Discussions for feature requests rather than issues. This keeps is
- Use Tailwind CSS for styling
**General:**
- Write self-documenting code
- Add comments for non-obvious logic
- Keep functions small and single-purpose
@ -213,6 +228,7 @@ We use GitHub Discussions for feature requests rather than issues. This keeps is
### Testing Your Changes
**Manual Testing:**
- Run the app in development mode: `bun run tauri dev`
- Test your changes with debug mode enabled
- Verify on multiple platforms if possible
@ -220,6 +236,7 @@ We use GitHub Discussions for feature requests rather than issues. This keeps is
- Try various transcription scenarios
**Building for Production:**
```bash
bun run tauri build
```
@ -247,6 +264,7 @@ Documentation improvements are highly valued! You can contribute by:
## 🎯 Good First Issues
Look for issues labeled `good first issue` or `help wanted` if you're new to the project. These are typically:
- Well-defined and scoped
- Good for learning the codebase
- Mentor support available

View file

@ -1,6 +1,7 @@
# Development Commands
**Environment Setup:**
```bash
bun install # Install dependencies
mkdir -p src-tauri/resources/models
@ -8,6 +9,7 @@ curl -o src-tauri/resources/models/silero_vad_v4.onnx https://blob.handy.compute
```
**Development:**
```bash
bun run tauri dev # Full app development
CMAKE_POLICY_VERSION_MINIMUM=3.5 bun run tauri dev # macOS with cmake fix
@ -17,6 +19,7 @@ bun run tauri build # Production build
```
**Type Check & Build:**
```bash
bunx tsc --noEmit # Type checking
bun run build # Build and validate
@ -25,6 +28,7 @@ bun run build # Build and validate
# Code Style Guidelines
**Rust (Backend):**
- Use `anyhow::Error` for error handling with descriptive messages
- Prefer `Arc<Mutex<T>>` for shared state in managers
- Log with appropriate levels: `debug!`, `info!`, `eprintln!` for errors
@ -33,6 +37,7 @@ bun run build # Build and validate
- Separate logical sections with comment blocks: `/* ─────────── */`
**TypeScript/React (Frontend):**
- Functional components with TypeScript interfaces
- Zod schemas for type validation and inference
- `useCallback` hooks for stable function references
@ -42,16 +47,19 @@ bun run build # Build and validate
- PascalCase for components, camelCase for variables/functions
**Imports:**
- Group imports: external libs, internal modules, relative imports
- Use type imports for TypeScript: `import type { Settings }`
- Named imports preferred over default exports
**Error Handling:**
- Frontend: Try/catch with user feedback, rollback optimistic updates
- Backend: `?` operator with anyhow context messages
- Log errors appropriately for debugging level
**Component Patterns:**
- Container component pattern for layout
- Composition over inheritance
- Prop drilling minimized with context where appropriate

View file

@ -25,6 +25,7 @@ Handy isn't trying to be the best speech-to-text app—it's trying to be the mos
4. **Get** your transcribed text pasted directly into whatever app you're using
The process is entirely local:
- Silence is filtered using VAD (Voice Activity Detection) with Silero
- Transcription uses your choice of models:
- **Whisper models** (Small/Medium/Turbo/Large) with GPU acceleration when available
@ -62,6 +63,7 @@ Handy is built as a Tauri application combining:
### Debug Mode
Handy includes an advanced debug mode for development and troubleshooting. Access it by pressing:
- **macOS**: `Cmd+Shift+D`
- **Windows/Linux**: `Ctrl+Shift+D`
@ -72,14 +74,17 @@ This project is actively being developed and has some [known issues](https://git
### Major Issues (Help Wanted)
**Whisper Model Crashes:**
- Whisper models crash on certain system configurations (Windows and Linux)
- Does not affect all systems - issue is configuration-dependent
- If you experience crashes and are a developer, please help to fix and provide debug logs!
**Wayland Support (Linux):**
- Limited or no support for Wayland display server
### Platform Support
- **macOS (both Intel and Apple Silicon)**
- **x64 Windows**
- **x64 Linux**
@ -89,12 +94,14 @@ This project is actively being developed and has some [known issues](https://git
The following are recommendations for running Handy on your own machine. If you don't meet the system requirements, the performance of the application may be degraded. We are working on improving the performance across all kinds of computers and hardware.
**For Whisper Models:**
- **macOS**: M series Mac, Intel Mac
- **Windows**: Intel, AMD, or NVIDIA GPU
- **Linux**: Intel, AMD, or NVIDIA GPU
* Ubuntu 22.04, 24.04
- Ubuntu 22.04, 24.04
**For Parakeet V3 Model:**
- **CPU-only operation** - runs on a wide variety of hardware
- **Minimum**: Intel Skylake (6th gen) or equivalent AMD processors
- **Performance**: ~5x real-time speed on mid-range hardware (tested on i5)
@ -107,25 +114,29 @@ We're actively working on several features and improvements. Contributions and f
### In Progress
**Debug Logging:**
- Adding debug logging to a file to help diagnose issues
**macOS Keyboard Improvements:**
- Support for Globe key as transcription trigger
- A rewrite of global shortcut handling for MacOS, and potentially other OS's too.
**Opt-in Analytics:**
- Collect anonymous usage data to help improve Handy
- Privacy-first approach with clear opt-in
**Settings Refactoring:**
- Cleanup and refactor settings system which is becoming bloated and messy
- Implement better abstractions for settings management
**Tauri Commands Cleanup:**
- Abstract and organize Tauri command patterns
- Investigate tauri-specta for improved type safety and organization
## Troubleshooting
### Manual Model Installation (For Proxy Users or Network Restrictions)
@ -141,6 +152,7 @@ If you're behind a proxy, firewall, or in a restricted network environment where
- **Windows/Linux**: `Ctrl+Shift+D` to open debug menu
The typical paths are:
- **macOS**: `~/Library/Application Support/com.pais.handy/`
- **Windows**: `C:\Users\{username}\AppData\Roaming\com.pais.handy\`
- **Linux**: `~/.config/com.pais.handy/`
@ -162,12 +174,14 @@ New-Item -ItemType Directory -Force -Path "$env:APPDATA\com.pais.handy\models"
Download the models you want from below
**Whisper Models (single .bin files):**
- Small (487 MB): `https://blob.handy.computer/ggml-small.bin`
- Medium (492 MB): `https://blob.handy.computer/whisper-medium-q4_1.bin`
- Turbo (1600 MB): `https://blob.handy.computer/ggml-large-v3-turbo.bin`
- Large (1100 MB): `https://blob.handy.computer/ggml-large-v3-q5_0.bin`
**Parakeet Models (compressed archives):**
- V2 (473 MB): `https://blob.handy.computer/parakeet-v2-int8.tar.gz`
- V3 (478 MB): `https://blob.handy.computer/parakeet-v3-int8.tar.gz`
@ -206,6 +220,7 @@ Final structure should look like:
```
**Important Notes:**
- For Parakeet models, the extracted directory name **must** match exactly as shown above
- Do not rename the `.bin` files for Whisper models—use the exact filenames from the download URLs
- After placing the files, restart Handy to detect the new models
@ -260,4 +275,4 @@ MIT License - see [LICENSE](LICENSE) file for details.
---
*"Your search for the right speech-to-text tool can end here—not because Handy is perfect, but because you can make it perfect for you."*
_"Your search for the right speech-to-text tool can end here—not because Handy is perfect, but because you can make it perfect for you."_

View file

@ -33,6 +33,7 @@
"@types/react-dom": "^18.3.7",
"@types/react-select": "^5.0.1",
"@vitejs/plugin-react": "^4.7.0",
"prettier": "^3.6.2",
"typescript": "~5.6.3",
"vite": "^6.4.1",
},
@ -441,6 +442,8 @@
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
"prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="],
"prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
"react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="],

View file

@ -7,7 +7,11 @@
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"tauri": "tauri"
"tauri": "tauri",
"format": "prettier --write . && cd src-tauri && cargo fmt",
"format:check": "prettier --check . && cd src-tauri && cargo fmt -- --check",
"format:frontend": "prettier --write .",
"format:backend": "cd src-tauri && cargo fmt"
},
"dependencies": {
"@tailwindcss/vite": "^4.1.16",
@ -39,6 +43,7 @@
"@types/react-dom": "^18.3.7",
"@types/react-select": "^5.0.1",
"@vitejs/plugin-react": "^4.7.0",
"prettier": "^3.6.2",
"typescript": "~5.6.3",
"vite": "^6.4.1"
}

View file

@ -1,13 +1,7 @@
{
"identifier": "desktop-capability",
"platforms": [
"macOS",
"windows",
"linux"
],
"windows": [
"main"
],
"platforms": ["macOS", "windows", "linux"],
"windows": ["main"],
"permissions": [
"autostart:default",
"global-shortcut:default",

1
src-tauri/rustfmt.toml Normal file
View file

@ -0,0 +1 @@
edition = "2021"

View file

@ -1,8 +1,11 @@
use async_openai::{config::OpenAIConfig, Client};
use crate::settings::PostProcessProvider;
use async_openai::{config::OpenAIConfig, Client};
/// Create an OpenAI-compatible client configured for the given provider
pub fn create_client(provider: &PostProcessProvider, api_key: String) -> Result<Client<OpenAIConfig>, String> {
pub fn create_client(
provider: &PostProcessProvider,
api_key: String,
) -> Result<Client<OpenAIConfig>, String> {
let base_url = provider.base_url.trim_end_matches('/');
let config = OpenAIConfig::new()
.with_api_base(base_url)

View file

@ -3,8 +3,8 @@
@theme {
--color-text: #0f0f0f;
--color-background: #fbfbfb;
--color-background-ui: #DA5893;
--color-logo-primary: #FAA2CA;
--color-background-ui: #da5893;
--color-logo-primary: #faa2ca;
--color-logo-stroke: #382731;
--color-text-stroke: #f6f6f6;
--color-mid-gray: #808080;
@ -49,8 +49,8 @@
/* Colors - Dark Theme */
--color-text: #fbfbfb;
--color-background: #2c2b29;
--color-logo-primary: #F28CBB;
--color-logo-stroke: #FAD1ED;
--color-logo-primary: #f28cbb;
--color-logo-stroke: #fad1ed;
}
}

View file

@ -1 +1 @@
export { default } from './Footer';
export { default } from "./Footer";

View file

@ -1,3 +1,3 @@
export { default as MicrophoneIcon } from './MicrophoneIcon';
export { default as TranscriptionIcon } from './TranscriptionIcon';
export { default as CancelIcon } from './CancelIcon';
export { default as MicrophoneIcon } from "./MicrophoneIcon";
export { default as TranscriptionIcon } from "./TranscriptionIcon";
export { default as CancelIcon } from "./CancelIcon";

View file

@ -30,7 +30,9 @@ const DownloadProgressDisplay: React.FC<DownloadProgressDisplayProps> = ({
return null;
}
const progressData: ProgressData[] = Array.from(downloadProgress.values()).map((progress) => {
const progressData: ProgressData[] = Array.from(
downloadProgress.values(),
).map((progress) => {
const stats = downloadStats.get(progress.model_id);
return {
id: progress.model_id,

View file

@ -184,22 +184,22 @@ const ModelDropdown: React.FC<ModelDropdownProps> = ({
</div>
</div>
<div className="text-xs text-logo-primary tabular-nums">
{isDownloading && progress ? (
`${Math.max(0, Math.min(100, Math.round(progress.percentage)))}%`
) : (
"Download"
)}
{isDownloading && progress
? `${Math.max(0, Math.min(100, Math.round(progress.percentage)))}%`
: "Download"}
</div>
</div>
{isDownloading && progress && (
<div className="mt-2">
<ProgressBar
progress={[{
progress={[
{
id: model.id,
percentage: progress.percentage,
label: model.name
}]}
label: model.name,
},
]}
size="small"
/>
</div>

View file

@ -199,9 +199,10 @@ const ModelSelector: React.FC<ModelSelectorProps> = ({ onError }) => {
},
);
const extractionFailedUnlisten = listen<{model_id: string, error: string}>(
"model-extraction-failed",
(event) => {
const extractionFailedUnlisten = listen<{
model_id: string;
error: string;
}>("model-extraction-failed", (event) => {
const modelId = event.payload.model_id;
setExtractingModels((prev) => {
const next = new Set(prev);
@ -210,8 +211,7 @@ const ModelSelector: React.FC<ModelSelectorProps> = ({ onError }) => {
});
setModelError(`Failed to extract model: ${event.payload.error}`);
setModelStatus("error");
},
);
});
// Click outside to close dropdown
const handleClickOutside = (event: MouseEvent) => {
@ -304,8 +304,8 @@ const ModelSelector: React.FC<ModelSelectorProps> = ({ onError }) => {
if (extractingModels.size > 0) {
if (extractingModels.size === 1) {
const [modelId] = Array.from(extractingModels);
const model = models.find(m => m.id === modelId);
return `Extracting ${model?.name || 'Model'}...`;
const model = models.find((m) => m.id === modelId);
return `Extracting ${model?.name || "Model"}...`;
} else {
return `Extracting ${extractingModels.size} models...`;
}
@ -332,7 +332,9 @@ const ModelSelector: React.FC<ModelSelectorProps> = ({ onError }) => {
case "loading":
return currentModel ? `Loading ${currentModel.name}...` : "Loading...";
case "extracting":
return currentModel ? `Extracting ${currentModel.name}...` : "Extracting...";
return currentModel
? `Extracting ${currentModel.name}...`
: "Extracting...";
case "error":
return modelError || "Model Error";
case "unloaded":

View file

@ -1 +1 @@
export { default } from './Onboarding';
export { default } from "./Onboarding";

View file

@ -7,10 +7,8 @@ interface AlwaysOnMicrophoneProps {
grouped?: boolean;
}
export const AlwaysOnMicrophone: React.FC<AlwaysOnMicrophoneProps> = React.memo(({
descriptionMode = "tooltip",
grouped = false,
}) => {
export const AlwaysOnMicrophone: React.FC<AlwaysOnMicrophoneProps> = React.memo(
({ descriptionMode = "tooltip", grouped = false }) => {
const { getSetting, updateSetting, isUpdating } = useSettings();
const alwaysOnMode = getSetting("always_on_microphone") || false;
@ -26,4 +24,5 @@ export const AlwaysOnMicrophone: React.FC<AlwaysOnMicrophoneProps> = React.memo(
grouped={grouped}
/>
);
});
},
);

View file

@ -14,10 +14,8 @@ const clipboardHandlingOptions = [
{ value: "copy_to_clipboard", label: "Copy to Clipboard" },
];
export const ClipboardHandlingSetting: React.FC<ClipboardHandlingProps> = React.memo(({
descriptionMode = "tooltip",
grouped = false,
}) => {
export const ClipboardHandlingSetting: React.FC<ClipboardHandlingProps> =
React.memo(({ descriptionMode = "tooltip", grouped = false }) => {
const { getSetting, updateSetting, isUpdating } = useSettings();
const selectedHandling = (getSetting("clipboard_handling") ||

View file

@ -9,10 +9,8 @@ interface MicrophoneSelectorProps {
grouped?: boolean;
}
export const MicrophoneSelector: React.FC<MicrophoneSelectorProps> = React.memo(({
descriptionMode = "tooltip",
grouped = false,
}) => {
export const MicrophoneSelector: React.FC<MicrophoneSelectorProps> = React.memo(
({ descriptionMode = "tooltip", grouped = false }) => {
const {
getSetting,
updateSetting,
@ -23,8 +21,10 @@ export const MicrophoneSelector: React.FC<MicrophoneSelectorProps> = React.memo(
refreshAudioDevices,
} = useSettings();
const selectedMicrophone = getSetting("selected_microphone") === "default" ? "Default" : getSetting("selected_microphone") || "Default";
const selectedMicrophone =
getSetting("selected_microphone") === "default"
? "Default"
: getSetting("selected_microphone") || "Default";
const handleMicrophoneSelect = async (deviceName: string) => {
await updateSetting("selected_microphone", deviceName);
@ -34,9 +34,9 @@ export const MicrophoneSelector: React.FC<MicrophoneSelectorProps> = React.memo(
await resetSetting("selected_microphone");
};
const microphoneOptions = audioDevices.map(device => ({
const microphoneOptions = audioDevices.map((device) => ({
value: device.name,
label: device.name
label: device.name,
}));
return (
@ -51,8 +51,16 @@ export const MicrophoneSelector: React.FC<MicrophoneSelectorProps> = React.memo(
options={microphoneOptions}
selectedValue={selectedMicrophone}
onSelect={handleMicrophoneSelect}
placeholder={isLoading || audioDevices.length === 0 ? "Loading..." : "Select microphone..."}
disabled={isUpdating("selected_microphone") || isLoading || audioDevices.length === 0}
placeholder={
isLoading || audioDevices.length === 0
? "Loading..."
: "Select microphone..."
}
disabled={
isUpdating("selected_microphone") ||
isLoading ||
audioDevices.length === 0
}
onRefresh={refreshAudioDevices}
/>
<ResetButton
@ -62,4 +70,5 @@ export const MicrophoneSelector: React.FC<MicrophoneSelectorProps> = React.memo(
</div>
</SettingContainer>
);
});
},
);

View file

@ -7,8 +7,8 @@ interface PostProcessingToggleProps {
grouped?: boolean;
}
export const PostProcessingToggle: React.FC<PostProcessingToggleProps> = React.memo(
({ descriptionMode = "tooltip", grouped = false }) => {
export const PostProcessingToggle: React.FC<PostProcessingToggleProps> =
React.memo(({ descriptionMode = "tooltip", grouped = false }) => {
const { getSetting, updateSetting, isUpdating } = useSettings();
const enabled = getSetting("post_process_enabled") || false;
@ -24,5 +24,4 @@ export const PostProcessingToggle: React.FC<PostProcessingToggleProps> = React.m
grouped={grouped}
/>
);
},
);
});

View file

@ -7,10 +7,8 @@ interface PushToTalkProps {
grouped?: boolean;
}
export const PushToTalk: React.FC<PushToTalkProps> = React.memo(({
descriptionMode = "tooltip",
grouped = false,
}) => {
export const PushToTalk: React.FC<PushToTalkProps> = React.memo(
({ descriptionMode = "tooltip", grouped = false }) => {
const { getSetting, updateSetting, isUpdating } = useSettings();
const pttEnabled = getSetting("push_to_talk") || false;
@ -26,4 +24,5 @@ export const PushToTalk: React.FC<PushToTalkProps> = React.memo(({
grouped={grouped}
/>
);
});
},
);

View file

@ -15,10 +15,8 @@ const overlayOptions = [
{ value: "top", label: "Top" },
];
export const ShowOverlay: React.FC<ShowOverlayProps> = React.memo(({
descriptionMode = "tooltip",
grouped = false,
}) => {
export const ShowOverlay: React.FC<ShowOverlayProps> = React.memo(
({ descriptionMode = "tooltip", grouped = false }) => {
const { getSetting, updateSetting, isUpdating } = useSettings();
const selectedPosition = (getSetting("overlay_position") ||
@ -41,4 +39,5 @@ export const ShowOverlay: React.FC<ShowOverlayProps> = React.memo(({
/>
</SettingContainer>
);
});
},
);

View file

@ -7,10 +7,8 @@ interface StartHiddenProps {
grouped?: boolean;
}
export const StartHidden: React.FC<StartHiddenProps> = React.memo(({
descriptionMode = "tooltip",
grouped = false,
}) => {
export const StartHidden: React.FC<StartHiddenProps> = React.memo(
({ descriptionMode = "tooltip", grouped = false }) => {
const { getSetting, updateSetting, isUpdating } = useSettings();
const startHidden = getSetting("start_hidden") ?? false;
@ -27,4 +25,5 @@ export const StartHidden: React.FC<StartHiddenProps> = React.memo(({
tooltipPosition="bottom"
/>
);
});
},
);

View file

@ -28,7 +28,9 @@ export const DebugPaths: React.FC<DebugPathsProps> = ({
</div>
<div>
<span className="font-medium">Settings:</span>{" "}
<span className="font-mono text-xs">%APPDATA%/handy/settings_store.json</span>
<span className="font-mono text-xs">
%APPDATA%/handy/settings_store.json
</span>
</div>
</div>
</SettingContainer>

View file

@ -1,2 +1,2 @@
export { default as ProgressBar } from './ProgressBar';
export type { ProgressData } from './ProgressBar';
export { default as ProgressBar } from "./ProgressBar";
export type { ProgressData } from "./ProgressBar";

View file

@ -10,13 +10,7 @@ interface ResetButtonProps {
}
export const ResetButton: React.FC<ResetButtonProps> = React.memo(
({
onClick,
disabled = false,
className = "",
ariaLabel,
children,
}) => (
({ onClick, disabled = false, className = "", ariaLabel, children }) => (
<button
type="button"
aria-label={ariaLabel}

View file

@ -39,10 +39,14 @@ type NonCreatableProps = {
export type SelectProps = BaseProps & (CreatableProps | NonCreatableProps);
const baseBackground = "color-mix(in srgb, var(--color-mid-gray) 10%, transparent)";
const hoverBackground = "color-mix(in srgb, var(--color-logo-primary) 12%, transparent)";
const focusBackground = "color-mix(in srgb, var(--color-logo-primary) 20%, transparent)";
const neutralBorder = "color-mix(in srgb, var(--color-mid-gray) 80%, transparent)";
const baseBackground =
"color-mix(in srgb, var(--color-mid-gray) 10%, transparent)";
const hoverBackground =
"color-mix(in srgb, var(--color-logo-primary) 12%, transparent)";
const focusBackground =
"color-mix(in srgb, var(--color-logo-primary) 20%, transparent)";
const neutralBorder =
"color-mix(in srgb, var(--color-mid-gray) 80%, transparent)";
const selectStyles: StylesConfig<SelectOption, false> = {
control: (base, state) => ({
@ -94,7 +98,8 @@ const selectStyles: StylesConfig<SelectOption, false> = {
zIndex: 30,
backgroundColor: "var(--color-background)",
color: "var(--color-text)",
border: "1px solid color-mix(in srgb, var(--color-mid-gray) 30%, transparent)",
border:
"1px solid color-mix(in srgb, var(--color-mid-gray) 30%, transparent)",
boxShadow: "0 10px 30px rgba(15, 15, 15, 0.2)",
}),
option: (base, state) => ({

View file

@ -164,11 +164,15 @@ export const SettingContainer: React.FC<SettingContainerProps> = ({
/>
</svg>
{showTooltip && (
<div className={`absolute ${tooltipPosition === "top" ? "bottom-full" : "top-[150%]"} left-1/2 transform -translate-x-1/2 mb-2 px-3 py-2 bg-background border border-mid-gray/80 rounded-lg shadow-lg z-50 max-w-xs min-w-[200px] whitespace-normal animate-in fade-in-0 zoom-in-95 duration-200`}>
<div
className={`absolute ${tooltipPosition === "top" ? "bottom-full" : "top-[150%]"} left-1/2 transform -translate-x-1/2 mb-2 px-3 py-2 bg-background border border-mid-gray/80 rounded-lg shadow-lg z-50 max-w-xs min-w-[200px] whitespace-normal animate-in fade-in-0 zoom-in-95 duration-200`}
>
<p className="text-sm text-center leading-relaxed">
{description}
</p>
<div className={`absolute ${tooltipPosition === "top" ? "top-full" : "bottom-full rotate-180"} left-1/2 transform -translate-x-1/2 w-0 h-0 border-l-[6px] border-r-[6px] border-t-[6px] border-l-transparent border-r-transparent border-t-mid-gray/80`}></div>
<div
className={`absolute ${tooltipPosition === "top" ? "top-full" : "bottom-full rotate-180"} left-1/2 transform -translate-x-1/2 w-0 h-0 border-l-[6px] border-r-[6px] border-t-[6px] border-l-transparent border-r-transparent border-t-mid-gray/80`}
></div>
</div>
)}
</div>

View file

@ -1 +1 @@
export { default } from './UpdateChecker';
export { default } from "./UpdateChecker";

View file

@ -193,9 +193,10 @@ export const useModels = () => {
},
);
const extractionFailedUnlisten = listen<{model_id: string, error: string}>(
"model-extraction-failed",
(event) => {
const extractionFailedUnlisten = listen<{
model_id: string;
error: string;
}>("model-extraction-failed", (event) => {
const modelId = event.payload.model_id;
setExtractingModels((prev) => {
const next = new Set(prev);
@ -203,8 +204,7 @@ export const useModels = () => {
return next;
});
setError(`Failed to extract model: ${event.payload.error}`);
},
);
});
return () => {
progressUnlisten.then((fn) => fn());

View file

@ -31,8 +31,14 @@ interface UseSettingsReturn {
// Post-processing helpers
setPostProcessProvider: (providerId: string) => Promise<void>;
updatePostProcessBaseUrl: (providerId: string, baseUrl: string) => Promise<void>;
updatePostProcessApiKey: (providerId: string, apiKey: string) => Promise<void>;
updatePostProcessBaseUrl: (
providerId: string,
baseUrl: string,
) => Promise<void>;
updatePostProcessApiKey: (
providerId: string,
apiKey: string,
) => Promise<void>;
updatePostProcessModel: (providerId: string, model: string) => Promise<void>;
fetchPostProcessModels: (providerId: string) => Promise<string[]>;
}

View file

@ -37,7 +37,10 @@ export type ModelUnloadTimeout = z.infer<typeof ModelUnloadTimeoutSchema>;
export const PasteMethodSchema = z.enum(["ctrl_v", "direct", "shift_insert"]);
export type PasteMethod = z.infer<typeof PasteMethodSchema>;
export const ClipboardHandlingSchema = z.enum(["dont_modify", "copy_to_clipboard"]);
export const ClipboardHandlingSchema = z.enum([
"dont_modify",
"copy_to_clipboard",
]);
export type ClipboardHandling = z.infer<typeof ClipboardHandlingSchema>;
export const LLMPromptSchema = z.object({
@ -93,14 +96,8 @@ export const SettingsSchema = z.object({
.array(PostProcessProviderSchema)
.optional()
.default([]),
post_process_api_keys: z
.record(z.string())
.optional()
.default({}),
post_process_models: z
.record(z.string())
.optional()
.default({}),
post_process_api_keys: z.record(z.string()).optional().default({}),
post_process_models: z.record(z.string()).optional().default({}),
post_process_prompts: z.array(LLMPromptSchema).optional().default([]),
post_process_selected_prompt_id: z.string().nullable().optional(),
mute_while_recording: z.boolean().optional().default(false),

View file

@ -41,7 +41,7 @@
.bar {
width: 6px;
background: #FFE5EE;
background: #ffe5ee;
max-height: 20px;
border-radius: 2px;
transition: height 80ms linear;
@ -86,7 +86,7 @@
}
.cancel-button:hover {
background: #FAA2CA33;
background: #faa2ca33;
transform: scale(1.05);
}

View file

@ -29,9 +29,19 @@ interface SettingsStore {
playTestSound: (soundType: "start" | "stop") => Promise<void>;
checkCustomSounds: () => Promise<void>;
setPostProcessProvider: (providerId: string) => Promise<void>;
updatePostProcessSetting: (settingType: 'base_url' | 'api_key' | 'model', providerId: string, value: string) => Promise<void>;
updatePostProcessBaseUrl: (providerId: string, baseUrl: string) => Promise<void>;
updatePostProcessApiKey: (providerId: string, apiKey: string) => Promise<void>;
updatePostProcessSetting: (
settingType: "base_url" | "api_key" | "model",
providerId: string,
value: string,
) => Promise<void>;
updatePostProcessBaseUrl: (
providerId: string,
baseUrl: string,
) => Promise<void>;
updatePostProcessApiKey: (
providerId: string,
apiKey: string,
) => Promise<void>;
updatePostProcessModel: (providerId: string, model: string) => Promise<void>;
fetchPostProcessModels: (providerId: string) => Promise<string[]>;
setPostProcessModelOptions: (providerId: string, models: string[]) => void;
@ -151,7 +161,7 @@ export const useSettingsStore = create<SettingsStore>()(
const { load } = await import("@tauri-apps/plugin-store");
const store = await load("settings_store.json", {
defaults: DEFAULT_SETTINGS,
autoSave: false
autoSave: false,
});
const settings = (await store.get("settings")) as Settings;
@ -234,7 +244,6 @@ export const useSettingsStore = create<SettingsStore>()(
}
},
checkCustomSounds: async () => {
try {
const sounds = await invoke("check_custom_sounds");
@ -386,25 +395,25 @@ export const useSettingsStore = create<SettingsStore>()(
// Generic updater for post-processing provider settings
updatePostProcessSetting: async (
settingType: 'base_url' | 'api_key' | 'model',
settingType: "base_url" | "api_key" | "model",
providerId: string,
value: string
value: string,
) => {
const { setUpdating, refreshSettings } = get();
const updateKey = `post_process_${settingType}:${providerId}`;
// Map setting types to command names
const commandMap = {
base_url: 'change_post_process_base_url_setting',
api_key: 'change_post_process_api_key_setting',
model: 'change_post_process_model_setting',
base_url: "change_post_process_base_url_setting",
api_key: "change_post_process_api_key_setting",
model: "change_post_process_model_setting",
};
// Map setting types to param names
const paramMap = {
base_url: 'baseUrl',
api_key: 'apiKey',
model: 'model',
base_url: "baseUrl",
api_key: "apiKey",
model: "model",
};
setUpdating(updateKey, true);
@ -416,14 +425,17 @@ export const useSettingsStore = create<SettingsStore>()(
});
await refreshSettings();
} catch (error) {
console.error(`Failed to update post-process ${settingType.replace('_', ' ')}:`, error);
console.error(
`Failed to update post-process ${settingType.replace("_", " ")}:`,
error,
);
} finally {
setUpdating(updateKey, false);
}
},
updatePostProcessBaseUrl: async (providerId, baseUrl) => {
return get().updatePostProcessSetting('base_url', providerId, baseUrl);
return get().updatePostProcessSetting("base_url", providerId, baseUrl);
},
updatePostProcessApiKey: async (providerId, apiKey) => {
@ -434,11 +446,11 @@ export const useSettingsStore = create<SettingsStore>()(
[providerId]: [],
},
}));
return get().updatePostProcessSetting('api_key', providerId, apiKey);
return get().updatePostProcessSetting("api_key", providerId, apiKey);
},
updatePostProcessModel: async (providerId, model) => {
return get().updatePostProcessSetting('model', providerId, model);
return get().updatePostProcessSetting("model", providerId, model);
},
fetchPostProcessModels: async (providerId) => {

View file

@ -1,19 +1,16 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
theme: {
extend: {
colors: {
text: 'var(--color-text)',
background: 'var(--color-background)',
'logo-primary': 'var(--color-logo-primary)',
'logo-stroke': 'var(--color-logo-stroke)',
'text-stroke': 'var(--color-text-stroke)',
text: "var(--color-text)",
background: "var(--color-background)",
"logo-primary": "var(--color-logo-primary)",
"logo-stroke": "var(--color-logo-stroke)",
"text-stroke": "var(--color-text-stroke)",
},
},
},
plugins: [],
}
};