Improve chat UX: paste images and language-aware TTS

- Remove clipboard button, add ⌘V paste support for images
- Add automatic language detection for TTS (French/English)
- Use appropriate voice based on message language
- Simplify TTS to use system default voices

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Mathias Beaulieu-Duncan 2025-12-31 03:38:36 -05:00
parent b754945923
commit 7655f1f0b8
2 changed files with 44 additions and 17 deletions

View File

@ -429,12 +429,10 @@ final class ChatViewModel {
utterance.pitchMultiplier = 1.0 utterance.pitchMultiplier = 1.0
utterance.volume = 1.0 utterance.volume = 1.0
// Use voice matching current speech recognition language // Detect message language and use appropriate voice
if detectedLanguage == "fr-CA" { let isFrench = Self.detectFrench(message.content)
utterance.voice = AVSpeechSynthesisVoice(language: "fr-CA") let language = isFrench ? "fr-CA" : "en-US"
} else { utterance.voice = AVSpeechSynthesisVoice(language: language)
utterance.voice = AVSpeechSynthesisVoice(language: "en-CA")
}
// Create synthesizer and delegate // Create synthesizer and delegate
let synthesizer = AVSpeechSynthesizer() let synthesizer = AVSpeechSynthesizer()
@ -460,6 +458,25 @@ final class ChatViewModel {
isSpeaking = false isSpeaking = false
speakingMessageId = nil speakingMessageId = nil
} }
/// Detect if text is likely French based on common words
private static func detectFrench(_ text: String) -> Bool {
let lowercased = text.lowercased()
let frenchIndicators = [
" le ", " la ", " les ", " un ", " une ", " des ",
" je ", " tu ", " il ", " elle ", " nous ", " vous ", " ils ", " elles ",
" est ", " sont ", " avoir ", " être ", " fait ", " faire ",
" que ", " qui ", " quoi ", " dans ", " pour ", " avec ", " sur ",
" ce ", " cette ", " ces ", " mon ", " ma ", " mes ",
" pas ", " plus ", " très ", " bien ", " aussi ",
"bonjour", "merci", "salut", "oui", "non", "peut",
" et ", " ou ", " mais ", " donc ", " car ",
"c'est", "j'ai", "qu'est", "n'est", "d'un", "l'on"
]
let frenchCount = frenchIndicators.filter { lowercased.contains($0) }.count
return frenchCount >= 2
}
} }
// MARK: - Speech Synthesizer Delegate // MARK: - Speech Synthesizer Delegate

View File

@ -278,17 +278,7 @@ struct ChatView: View {
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.help("Add image") .help("Add image (or paste with ⌘V)")
Button {
viewModel.addImageFromPasteboard()
} label: {
Image(systemName: "doc.on.clipboard")
.font(.title3)
.foregroundStyle(.secondary)
}
.buttonStyle(.plain)
.help("Paste image from clipboard")
// Language toggle for speech recognition // Language toggle for speech recognition
Button { Button {
@ -361,6 +351,26 @@ struct ChatView: View {
} }
} }
.padding() .padding()
.onPasteCommand(of: [.image, .png, .jpeg, .tiff]) { providers in
for provider in providers {
// Try to load as image
if provider.hasItemConformingToTypeIdentifier(UTType.image.identifier) {
provider.loadDataRepresentation(forTypeIdentifier: UTType.image.identifier) { data, _ in
if let data = data {
DispatchQueue.main.async {
let attachment = ImageAttachment(data: data, filename: "pasted_image.png")
if viewModel.pendingImages.count < 5 {
viewModel.pendingImages.append(attachment)
}
}
}
}
return
}
}
// Fallback to pasteboard check
viewModel.addImageFromPasteboard()
}
} }
} }