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:
parent
b754945923
commit
7655f1f0b8
@ -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
|
||||||
|
|||||||
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user