tellme/Sources/App/HelpWindow.swift
Felipe M. 54c3b65d4a
Complete Phase 4: Comprehensive preferences, localization, and UX polish
- Rename application from MenuWhisper to Tell me with new domain com.fmartingr.tellme
- Implement comprehensive preferences window with 6 tabs (General, Models, Text Insertion, Interface, Advanced, Permissions)
- Add full English/Spanish localization for all UI elements
- Create functional onboarding flow with model download capability
- Implement preview dialog for transcription editing
- Add settings export/import functionality
- Fix HUD content display issues and add comprehensive permission checking
- Enhance build scripts and app bundle creation for proper localization support
2025-09-19 13:55:46 +02:00

683 lines
No EOL
22 KiB
Swift

import SwiftUI
class HelpWindowController: NSWindowController {
init() {
let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 700, height: 600),
styleMask: [.titled, .closable, .resizable],
backing: .buffered,
defer: false
)
super.init(window: window)
window.title = "Tell me Help"
window.center()
let helpView = HelpView()
window.contentView = NSHostingView(rootView: helpView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
struct HelpView: View {
@State private var selectedSection = 0
var body: some View {
HSplitView {
// Sidebar
VStack(alignment: .leading, spacing: 0) {
Text("Help Topics")
.font(.headline)
.padding(.horizontal, 16)
.padding(.vertical, 12)
List(selection: $selectedSection) {
HelpSectionItem(title: "Getting Started", systemImage: "play.circle", tag: 0)
HelpSectionItem(title: "Permissions", systemImage: "lock.shield", tag: 1)
HelpSectionItem(title: "Speech Models", systemImage: "brain.head.profile", tag: 2)
HelpSectionItem(title: "Hotkeys & Usage", systemImage: "keyboard", tag: 3)
HelpSectionItem(title: "Troubleshooting", systemImage: "wrench.and.screwdriver", tag: 4)
HelpSectionItem(title: "Privacy & Security", systemImage: "shield.checkered", tag: 5)
}
.listStyle(.sidebar)
}
.frame(minWidth: 200, maxWidth: 300)
// Content area
ScrollView {
VStack(alignment: .leading, spacing: 20) {
switch selectedSection {
case 0:
GettingStartedHelp()
case 1:
PermissionsHelp()
case 2:
ModelsHelp()
case 3:
HotkeysHelp()
case 4:
TroubleshootingHelp()
case 5:
PrivacyHelp()
default:
GettingStartedHelp()
}
}
.padding(24)
}
}
.frame(width: 700, height: 600)
}
}
struct HelpSectionItem: View {
let title: String
let systemImage: String
let tag: Int
var body: some View {
Label(title, systemImage: systemImage)
.tag(tag)
}
}
// MARK: - Help Content Views
struct GettingStartedHelp: View {
var body: some View {
VStack(alignment: .leading, spacing: 16) {
Text("Getting Started with Tell me")
.font(.largeTitle)
.fontWeight(.bold)
Text("Tell me is a privacy-focused speech-to-text application that works completely offline on your Mac.")
.font(.body)
VStack(alignment: .leading, spacing: 12) {
Text("Quick Setup")
.font(.headline)
HelpStep(
number: 1,
title: "Grant Permissions",
description: "Allow Microphone, Accessibility, and Input Monitoring access when prompted."
)
HelpStep(
number: 2,
title: "Download a Model",
description: "Go to Preferences → Models and download a speech recognition model (recommended: whisper-small)."
)
HelpStep(
number: 3,
title: "Start Dictating",
description: "Press ⌘⇧V anywhere to start dictation. The HUD will appear to show status."
)
HelpStep(
number: 4,
title: "Stop and Insert",
description: "Release the hotkey (push-to-talk mode) or press it again (toggle mode) to transcribe and insert text."
)
}
InfoBox(
title: "First Time Setup",
content: "If this is your first time, Tell me will guide you through the setup process automatically.",
type: .info
)
}
}
}
struct PermissionsHelp: View {
var body: some View {
VStack(alignment: .leading, spacing: 16) {
Text("Permissions")
.font(.largeTitle)
.fontWeight(.bold)
Text("Tell me requires specific system permissions to function properly.")
.font(.body)
VStack(alignment: .leading, spacing: 16) {
PermissionHelpItem(
title: "Microphone",
description: "Required to capture your speech for transcription.",
required: true,
howToGrant: "Grant when prompted, or go to System Settings → Privacy & Security → Microphone."
)
PermissionHelpItem(
title: "Accessibility",
description: "Required to insert transcribed text into other applications.",
required: true,
howToGrant: "Go to System Settings → Privacy & Security → Accessibility and add Tell me."
)
PermissionHelpItem(
title: "Input Monitoring",
description: "Required to register global keyboard shortcuts and send text insertion events.",
required: true,
howToGrant: "Go to System Settings → Privacy & Security → Input Monitoring and add Tell me."
)
}
InfoBox(
title: "Permission Issues",
content: "If permissions aren't working, try restarting Tell me after granting them. Some permissions may require logging out and back in.",
type: .warning
)
}
}
}
struct ModelsHelp: View {
var body: some View {
VStack(alignment: .leading, spacing: 16) {
Text("Speech Recognition Models")
.font(.largeTitle)
.fontWeight(.bold)
Text("Tell me uses OpenAI Whisper models for speech recognition. All models work entirely offline.")
.font(.body)
VStack(alignment: .leading, spacing: 12) {
Text("Available Models")
.font(.headline)
ModelHelpItem(
name: "whisper-tiny",
size: "39 MB",
ram: "~400 MB",
speed: "Very Fast",
accuracy: "Basic",
recommendation: "Good for testing or very low-end hardware"
)
ModelHelpItem(
name: "whisper-base",
size: "142 MB",
ram: "~500 MB",
speed: "Fast",
accuracy: "Good",
recommendation: "Recommended for most users"
)
ModelHelpItem(
name: "whisper-small",
size: "466 MB",
ram: "~1 GB",
speed: "Medium",
accuracy: "Very Good",
recommendation: "Best balance of speed and accuracy"
)
}
InfoBox(
title: "Model Storage",
content: "Models are stored in ~/Library/Application Support/Tell me/Models and can be deleted from Preferences to free up space.",
type: .info
)
InfoBox(
title: "No Internet Required",
content: "Once downloaded, models work completely offline. Your speech never leaves your device.",
type: .success
)
}
}
}
struct HotkeysHelp: View {
var body: some View {
VStack(alignment: .leading, spacing: 16) {
Text("Hotkeys & Usage")
.font(.largeTitle)
.fontWeight(.bold)
Text("Tell me supports global hotkeys to start dictation from anywhere on your system.")
.font(.body)
VStack(alignment: .leading, spacing: 12) {
Text("Activation Modes")
.font(.headline)
HelpModeItem(
title: "Push-to-Talk (Default)",
description: "Hold down the hotkey to dictate, release to stop and transcribe."
)
HelpModeItem(
title: "Toggle Mode",
description: "Press once to start dictation, press again to stop and transcribe."
)
}
VStack(alignment: .leading, spacing: 12) {
Text("Default Hotkey")
.font(.headline)
HStack {
Text("⌘⇧V")
.font(.title2)
.fontWeight(.bold)
.padding(.horizontal, 12)
.padding(.vertical, 6)
.background(Color.gray.opacity(0.2))
.cornerRadius(8)
VStack(alignment: .leading) {
Text("Command + Shift + V")
.font(.body)
.fontWeight(.medium)
Text("Can be changed in Preferences → General")
.font(.caption)
.foregroundColor(.secondary)
}
}
}
VStack(alignment: .leading, spacing: 8) {
Text("Other Controls")
.font(.headline)
HStack {
Text("⎋ Esc")
.font(.body)
.fontWeight(.bold)
.frame(width: 60, alignment: .leading)
Text("Cancel dictation at any time")
}
HStack {
Text("⌘↩ Cmd+Return")
.font(.body)
.fontWeight(.bold)
.frame(width: 120, alignment: .leading)
Text("Insert text (in preview mode)")
}
}
InfoBox(
title: "Hotkey Conflicts",
content: "If your hotkey conflicts with another app, change it in Preferences → General → Hotkey Combination.",
type: .warning
)
}
}
}
struct TroubleshootingHelp: View {
var body: some View {
VStack(alignment: .leading, spacing: 16) {
Text("Troubleshooting")
.font(.largeTitle)
.fontWeight(.bold)
VStack(alignment: .leading, spacing: 16) {
TroubleshootingItem(
issue: "Hotkey doesn't work",
solutions: [
"Check that Input Monitoring permission is granted",
"Try changing the hotkey in Preferences",
"Restart Tell me after granting permissions",
"Make sure no other app is using the same hotkey"
]
)
TroubleshootingItem(
issue: "Text doesn't insert",
solutions: [
"Grant Accessibility permission in System Settings",
"Try switching insertion method in Preferences → Text Insertion",
"Check if you're in a secure input field (password, etc.)",
"Restart Tell me after granting permissions"
]
)
TroubleshootingItem(
issue: "Poor transcription quality",
solutions: [
"Try a larger model (whisper-small or whisper-base)",
"Ensure you're in a quiet environment",
"Speak clearly and at normal pace",
"Check your microphone input level",
"Set the correct language in Preferences → Models"
]
)
TroubleshootingItem(
issue: "App crashes or hangs",
solutions: [
"Check available RAM (models need 400MB-1GB+)",
"Try reducing processing threads in Preferences → Advanced",
"Restart Tell me",
"Try a smaller model if using whisper-medium or larger"
]
)
TroubleshootingItem(
issue: "Microphone not working",
solutions: [
"Grant Microphone permission when prompted",
"Check System Settings → Privacy & Security → Microphone",
"Test microphone in other apps",
"Check input level in System Settings → Sound"
]
)
}
InfoBox(
title: "Still Having Issues?",
content: "Enable logging in Preferences → Advanced and check Console.app for Tell me logs, or try restarting your Mac if permission issues persist.",
type: .info
)
}
}
}
struct PrivacyHelp: View {
var body: some View {
VStack(alignment: .leading, spacing: 16) {
Text("Privacy & Security")
.font(.largeTitle)
.fontWeight(.bold)
VStack(alignment: .leading, spacing: 12) {
Text("100% Offline Operation")
.font(.headline)
Text("Tell me is designed with privacy as the top priority:")
.font(.body)
VStack(alignment: .leading, spacing: 8) {
PrivacyPoint(
icon: "shield.checkered",
text: "Your audio never leaves your device"
)
PrivacyPoint(
icon: "wifi.slash",
text: "No internet connection required for transcription"
)
PrivacyPoint(
icon: "eye.slash",
text: "No telemetry or usage tracking"
)
PrivacyPoint(
icon: "lock.doc",
text: "Transcribed text is not stored or logged"
)
}
}
VStack(alignment: .leading, spacing: 12) {
Text("Local Data Storage")
.font(.headline)
Text("Tell me only stores:")
.font(.body)
VStack(alignment: .leading, spacing: 6) {
Text("• Settings and preferences in UserDefaults")
.font(.body)
Text("• Downloaded models in ~/Library/Application Support/Tell me/")
.font(.body)
Text("• Optional local logs (if enabled, contains only timing and error data)")
.font(.body)
}
}
VStack(alignment: .leading, spacing: 12) {
Text("Secure Input Detection")
.font(.headline)
Text("Tell me automatically detects secure input contexts (like password fields) and disables text insertion to protect your security. In these cases, text is copied to clipboard instead.")
.font(.body)
}
InfoBox(
title: "Open Source",
content: "Tell me is open source software. You can review the code and build it yourself for complete transparency.",
type: .success
)
}
}
}
// MARK: - Helper Views
struct HelpStep: View {
let number: Int
let title: String
let description: String
var body: some View {
HStack(alignment: .top, spacing: 12) {
Text("\(number)")
.font(.title2)
.fontWeight(.bold)
.foregroundColor(.white)
.frame(width: 32, height: 32)
.background(Color.blue)
.clipShape(Circle())
VStack(alignment: .leading, spacing: 4) {
Text(title)
.font(.body)
.fontWeight(.semibold)
Text(description)
.font(.body)
.foregroundColor(.secondary)
}
}
}
}
struct PermissionHelpItem: View {
let title: String
let description: String
let required: Bool
let howToGrant: String
var body: some View {
VStack(alignment: .leading, spacing: 8) {
HStack {
Text(title)
.font(.body)
.fontWeight(.semibold)
if required {
Text("REQUIRED")
.font(.caption)
.fontWeight(.bold)
.foregroundColor(.red)
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background(Color.red.opacity(0.1))
.cornerRadius(4)
}
}
Text(description)
.font(.body)
.foregroundColor(.secondary)
Text("How to grant: \(howToGrant)")
.font(.caption)
.foregroundColor(.blue)
}
.padding(12)
.background(Color(NSColor.controlBackgroundColor))
.cornerRadius(8)
}
}
struct ModelHelpItem: View {
let name: String
let size: String
let ram: String
let speed: String
let accuracy: String
let recommendation: String
var body: some View {
VStack(alignment: .leading, spacing: 8) {
HStack {
Text(name)
.font(.body)
.fontWeight(.semibold)
Spacer()
Text(size)
.font(.caption)
.foregroundColor(.secondary)
}
HStack {
VStack(alignment: .leading, spacing: 2) {
Text("RAM: \(ram)")
.font(.caption)
Text("Speed: \(speed)")
.font(.caption)
Text("Accuracy: \(accuracy)")
.font(.caption)
}
Spacer()
}
Text(recommendation)
.font(.caption)
.foregroundColor(.blue)
.italic()
}
.padding(12)
.background(Color(NSColor.controlBackgroundColor))
.cornerRadius(8)
}
}
struct HelpModeItem: View {
let title: String
let description: String
var body: some View {
HStack(alignment: .top, spacing: 12) {
Circle()
.fill(Color.blue)
.frame(width: 8, height: 8)
.padding(.top, 6)
VStack(alignment: .leading, spacing: 2) {
Text(title)
.font(.body)
.fontWeight(.semibold)
Text(description)
.font(.body)
.foregroundColor(.secondary)
}
}
}
}
struct TroubleshootingItem: View {
let issue: String
let solutions: [String]
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text(issue)
.font(.body)
.fontWeight(.semibold)
VStack(alignment: .leading, spacing: 4) {
ForEach(solutions, id: \.self) { solution in
HStack(alignment: .top, spacing: 8) {
Text("")
.foregroundColor(.blue)
Text(solution)
.font(.body)
.foregroundColor(.secondary)
}
}
}
}
.padding(12)
.background(Color(NSColor.controlBackgroundColor))
.cornerRadius(8)
}
}
struct PrivacyPoint: View {
let icon: String
let text: String
var body: some View {
HStack(spacing: 12) {
Image(systemName: icon)
.foregroundColor(.green)
.frame(width: 20)
Text(text)
.font(.body)
}
}
}
struct InfoBox: View {
let title: String
let content: String
let type: InfoType
enum InfoType {
case info, warning, success, error
var color: Color {
switch self {
case .info: return .blue
case .warning: return .orange
case .success: return .green
case .error: return .red
}
}
var icon: String {
switch self {
case .info: return "info.circle"
case .warning: return "exclamationmark.triangle"
case .success: return "checkmark.circle"
case .error: return "xmark.circle"
}
}
}
var body: some View {
HStack(alignment: .top, spacing: 12) {
Image(systemName: type.icon)
.foregroundColor(type.color)
.font(.title3)
VStack(alignment: .leading, spacing: 4) {
Text(title)
.font(.body)
.fontWeight(.semibold)
Text(content)
.font(.body)
.foregroundColor(.secondary)
}
}
.padding(16)
.background(type.color.opacity(0.1))
.cornerRadius(12)
}
}