Added Assistant Mode in Prompt + Cleaner Code seperation

This commit is contained in:
Beingpax 2025-03-22 13:52:43 +05:45
parent 4b79d200ff
commit 6a376dd5ad
5 changed files with 388 additions and 326 deletions

View File

@ -5,8 +5,7 @@ enum PredefinedPrompts {
// Static UUIDs for predefined prompts
private static let defaultPromptId = UUID(uuidString: "00000000-0000-0000-0000-000000000001")!
private static let chatStylePromptId = UUID(uuidString: "00000000-0000-0000-0000-000000000002")!
private static let emailPromptId = UUID(uuidString: "00000000-0000-0000-0000-000000000003")!
private static let assistantPromptId = UUID(uuidString: "00000000-0000-0000-0000-000000000002")!
static var all: [CustomPrompt] {
// Always return the latest predefined prompts from source code
@ -71,54 +70,38 @@ enum PredefinedPrompts {
),
CustomPrompt(
id: chatStylePromptId,
title: "Chat",
id: assistantPromptId,
title: "Assistant",
promptText: """
Primary Rules:
We are in a causual chat conversation.
1. Focus on clarity while preserving the speaker's personality:
- Keep personality markers that show intent or style (e.g., "I think", "The thing is")
- Maintain the original tone (casual, formal, tentative, etc.)
2. Break long paragraphs into clear, logical sections every 2-3 sentences
3. Fix grammar and punctuation errors based on context
4. Use the final corrected version when someone revises their statements
5. Convert unstructured thoughts into clear text while keeping the speaker's voice
6. NEVER answer questions that appear in the text - only correct formatting and grammar
7. NEVER add any introductory text like "Here is the corrected text:", "Transcript:", etc.
8. NEVER add content not present in the source text
9. NEVER add sign-offs or acknowledgments
10. Correct speech-to-text transcription errors based on context.
Provide a direct clear, and concise reply to the user's query. Use the available context if directly related to the user's query.
Remember to:
1. Be helpful and informative
2. Be accurate and precise
3. Don't add meta commentary or anything extra other than the actual answer
6. Maintain a friendly, casual tone
Examples:
Use the following information if provided:
1. Active Window Context:
IMPORTANT: Only use window content when directly relevant to input
- Use application name and window title for understanding the context
- Reference captured text from the window
- Preserve application-specific terms and formatting
- Help resolve unclear terms or phrases
Input: "so like i tried this new restaurant yesterday you know the one near the mall and um the pasta was really good i think i'll go back there soon"
2. Available Clipboard Content:
IMPORTANT: Only use when directly relevant to input
- Use for additional context
- Help resolve unclear references
- Ignore unrelated clipboard content
Output: "I tried this new restaurant near the mall yesterday! 🍽️
The pasta was really good. I think I'll go back there soon! 😊"
Input: "we need to finish the project by friday no wait thursday because the client meeting is on friday morning and we still need to test everything"
Output: "We need to finish the project by Thursday (not Friday) ⏰ because the client meeting is on Friday morning.
We still need to test everything! "
Input: "my phone is like three years old now and the battery is terrible i have to charge it like twice a day i think i need a new one"
Output: "My phone is three years old now and the battery is terrible. 📱
I have to charge it twice a day. I think I need a new one! 🔋"
Input: "went for a run yesterday it was nice weather and i saw this cute dog in the park wish i took a picture"
Output: "Went for a run yesterday! 🏃‍♀️
It was nice weather and I saw this cute dog in the park. 🐶
Wish I took a picture! 📸"
3. Examples:
- Follow the correction patterns shown in examples
- Match the formatting style of similar texts
- Use consistent terminology with examples
- Learn from previous corrections
""",
icon: .chatFill,
description: "Casual chat-style formatting",
description: "AI assistant that provides direct answers to queries",
isPredefined: true
)
]

View File

@ -26,6 +26,57 @@ enum PromptTemplates {
static func createTemplatePrompts() -> [TemplatePrompt] {
[
TemplatePrompt(
id: UUID(),
title: "AI Assistant",
promptText: """
Primary Rules:
We are in a causual chat conversation.
1. Focus on clarity while preserving the speaker's personality:
- Keep personality markers that show intent or style (e.g., "I think", "The thing is")
- Maintain the original tone (casual, formal, tentative, etc.)
2. Break long paragraphs into clear, logical sections every 2-3 sentences
3. Fix grammar and punctuation errors based on context
4. Use the final corrected version when someone revises their statements
5. Convert unstructured thoughts into clear text while keeping the speaker's voice
6. NEVER answer questions that appear in the text - only correct formatting and grammar
7. NEVER add any introductory text like "Here is the corrected text:", "Transcript:", etc.
8. NEVER add content not present in the source text
9. NEVER add sign-offs or acknowledgments
10. Correct speech-to-text transcription errors based on context.
Examples:
Input: "so like i tried this new restaurant yesterday you know the one near the mall and um the pasta was really good i think i'll go back there soon"
Output: "I tried this new restaurant near the mall yesterday! 🍽️
The pasta was really good. I think I'll go back there soon! 😊"
Input: "we need to finish the project by friday no wait thursday because the client meeting is on friday morning and we still need to test everything"
Output: "We need to finish the project by Thursday (not Friday) ⏰ because the client meeting is on Friday morning.
We still need to test everything! "
Input: "my phone is like three years old now and the battery is terrible i have to charge it like twice a day i think i need a new one"
Output: "My phone is three years old now and the battery is terrible. 📱
I have to charge it twice a day. I think I need a new one! 🔋"
Input: "went for a run yesterday it was nice weather and i saw this cute dog in the park wish i took a picture"
Output: "Went for a run yesterday! 🏃‍♀️
It was nice weather and I saw this cute dog in the park. 🐶
Wish I took a picture! 📸"
""",
icon: .chatFill,
description: "Casual chat-style formatting"
),
TemplatePrompt(
id: UUID(),
title: "Email",

View File

@ -0,0 +1,160 @@
import Foundation
import os
// MARK: - Model Management Extension
extension WhisperState {
// MARK: - Model Directory Management
func createModelsDirectoryIfNeeded() {
do {
try FileManager.default.createDirectory(at: modelsDirectory, withIntermediateDirectories: true, attributes: nil)
} catch {
messageLog += "Error creating models directory: \(error.localizedDescription)\n"
}
}
func loadAvailableModels() {
do {
let fileURLs = try FileManager.default.contentsOfDirectory(at: modelsDirectory, includingPropertiesForKeys: nil)
availableModels = fileURLs.compactMap { url in
guard url.pathExtension == "bin" else { return nil }
return WhisperModel(name: url.deletingPathExtension().lastPathComponent, url: url)
}
} catch {
messageLog += "Error loading available models: \(error.localizedDescription)\n"
}
}
// MARK: - Model Loading
func loadModel(_ model: WhisperModel) async throws {
guard whisperContext == nil else { return }
logger.notice("🔄 Loading Whisper model: \(model.name)")
isModelLoading = true
defer { isModelLoading = false }
do {
whisperContext = try await WhisperContext.createContext(path: model.url.path)
isModelLoaded = true
currentModel = model
logger.notice("✅ Successfully loaded model: \(model.name)")
} catch {
logger.error("❌ Failed to load model: \(model.name) - \(error.localizedDescription)")
throw WhisperStateError.modelLoadFailed
}
}
func setDefaultModel(_ model: WhisperModel) async {
do {
currentModel = model
UserDefaults.standard.set(model.name, forKey: "CurrentModel")
canTranscribe = true
} catch {
currentError = error as? WhisperStateError ?? .unknownError
canTranscribe = false
}
}
// MARK: - Model Download & Management
func downloadModel(_ model: PredefinedModel) async {
guard let url = URL(string: model.downloadURL) else { return }
logger.notice("🔽 Downloading model: \(model.name)")
do {
let (data, response) = try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<(Data, URLResponse), Error>) in
let task = URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error {
continuation.resume(throwing: error)
return
}
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode),
let data = data else {
continuation.resume(throwing: URLError(.badServerResponse))
return
}
continuation.resume(returning: (data, httpResponse))
}
task.resume()
let observation = task.progress.observe(\.fractionCompleted) { progress, _ in
DispatchQueue.main.async {
self.downloadProgress[model.name] = progress.fractionCompleted
}
}
Task {
await withTaskCancellationHandler {
observation.invalidate()
} operation: {
await withCheckedContinuation { (_: CheckedContinuation<Void, Never>) in }
}
}
}
let destinationURL = modelsDirectory.appendingPathComponent(model.filename)
try data.write(to: destinationURL)
availableModels.append(WhisperModel(name: model.name, url: destinationURL))
self.downloadProgress.removeValue(forKey: model.name)
logger.notice("✅ Successfully downloaded model: \(model.name)")
} catch {
logger.error("❌ Failed to download model: \(model.name) - \(error.localizedDescription)")
currentError = .modelDownloadFailed
self.downloadProgress.removeValue(forKey: model.name)
}
}
func deleteModel(_ model: WhisperModel) async {
do {
try FileManager.default.removeItem(at: model.url)
availableModels.removeAll { $0.id == model.id }
if currentModel?.id == model.id {
currentModel = nil
canTranscribe = false
}
} catch {
messageLog += "Error deleting model: \(error.localizedDescription)\n"
currentError = .modelDeletionFailed
}
}
func unloadModel() {
Task {
await whisperContext?.releaseResources()
whisperContext = nil
isModelLoaded = false
if let recordedFile = recordedFile {
try? FileManager.default.removeItem(at: recordedFile)
self.recordedFile = nil
}
}
}
func clearDownloadedModels() async {
for model in availableModels {
do {
try FileManager.default.removeItem(at: model.url)
} catch {
messageLog += "Error deleting model: \(error.localizedDescription)\n"
}
}
availableModels.removeAll()
}
// MARK: - Resource Management
func cleanupModelResources() async {
if !isRecording && !isProcessing {
logger.notice("🧹 Cleaning up Whisper resources")
await whisperContext?.releaseResources()
whisperContext = nil
isModelLoaded = false
}
}
}

View File

@ -0,0 +1,120 @@
import Foundation
import SwiftUI
import os
// MARK: - UI Management Extension
extension WhisperState {
// MARK: - Recorder Panel Management
func showRecorderPanel() {
logger.notice("📱 Showing \(self.recorderType) recorder")
if recorderType == "notch" {
if notchWindowManager == nil {
notchWindowManager = NotchWindowManager(whisperState: self, recorder: recorder)
logger.info("Created new notch window manager")
}
notchWindowManager?.show()
} else {
if miniWindowManager == nil {
miniWindowManager = MiniWindowManager(whisperState: self, recorder: recorder)
logger.info("Created new mini window manager")
}
miniWindowManager?.show()
}
}
func hideRecorderPanel() {
if isRecording {
Task {
await toggleRecord()
}
}
}
// MARK: - Mini Recorder Management
func toggleMiniRecorder() async {
if isMiniRecorderVisible {
await dismissMiniRecorder()
} else {
Task {
await toggleRecord()
SoundManager.shared.playStartSound()
await MainActor.run {
showRecorderPanel()
isMiniRecorderVisible = true
}
}
}
}
func dismissMiniRecorder() async {
logger.notice("📱 Dismissing \(self.recorderType) recorder")
shouldCancelRecording = true
if isRecording {
await recorder.stopRecording()
}
if recorderType == "notch" {
notchWindowManager?.hide()
} else {
miniWindowManager?.hide()
}
await MainActor.run {
isRecording = false
isVisualizerActive = false
isProcessing = false
isTranscribing = false
canTranscribe = true
isMiniRecorderVisible = false
shouldCancelRecording = false
}
try? await Task.sleep(nanoseconds: 150_000_000)
await cleanupModelResources()
}
func cancelRecording() async {
shouldCancelRecording = true
SoundManager.shared.playEscSound()
if isRecording {
await recorder.stopRecording()
}
await dismissMiniRecorder()
}
// MARK: - Notification Handling
func setupNotifications() {
NotificationCenter.default.addObserver(self, selector: #selector(handleToggleMiniRecorder), name: .toggleMiniRecorder, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(handleLicenseStatusChanged), name: .licenseStatusChanged, object: nil)
}
@objc public func handleToggleMiniRecorder() {
if isMiniRecorderVisible {
Task {
await toggleRecord()
}
} else {
Task {
await toggleRecord()
SoundManager.shared.playStartSound()
await MainActor.run {
showRecorderPanel()
isMiniRecorderVisible = true
}
}
}
}
@objc func handleLicenseStatusChanged() {
// This will refresh the license state when it changes elsewhere in the app
self.licenseViewModel = LicenseViewModel()
}
}

View File

@ -32,9 +32,21 @@ class WhisperState: NSObject, ObservableObject, AVAudioRecorderDelegate {
}
}
private var whisperContext: WhisperContext?
private let recorder = Recorder()
private var recordedFile: URL? = nil
@Published var isVisualizerActive = false
@Published var isMiniRecorderVisible = false {
didSet {
if isMiniRecorderVisible {
showRecorderPanel()
} else {
hideRecorderPanel()
}
}
}
var whisperContext: WhisperContext?
let recorder = Recorder()
var recordedFile: URL? = nil
let whisperPrompt = WhisperPrompt()
let modelContext: ModelContext
@ -61,11 +73,14 @@ class WhisperState: NSObject, ObservableObject, AVAudioRecorderDelegate {
let modelsDirectory: URL
let recordingsDirectory: URL
let enhancementService: AIEnhancementService?
private var licenseViewModel: LicenseViewModel
private let logger = Logger(subsystem: "com.prakashjoshipax.voiceink", category: "WhisperState")
var licenseViewModel: LicenseViewModel
let logger = Logger(subsystem: "com.prakashjoshipax.voiceink", category: "WhisperState")
private var transcriptionStartTime: Date?
private var notchWindowManager: NotchWindowManager?
private var miniWindowManager: MiniWindowManager?
var notchWindowManager: NotchWindowManager?
var miniWindowManager: MiniWindowManager?
// For model progress tracking
@Published var downloadProgress: [String: Double] = [:]
init(modelContext: ModelContext, enhancementService: AIEnhancementService? = nil) {
self.modelContext = modelContext
@ -91,14 +106,6 @@ class WhisperState: NSObject, ObservableObject, AVAudioRecorderDelegate {
}
}
private func createModelsDirectoryIfNeeded() {
do {
try FileManager.default.createDirectory(at: modelsDirectory, withIntermediateDirectories: true, attributes: nil)
} catch {
messageLog += "Error creating models directory: \(error.localizedDescription)\n"
}
}
private func createRecordingsDirectoryIfNeeded() {
do {
try FileManager.default.createDirectory(at: recordingsDirectory, withIntermediateDirectories: true, attributes: nil)
@ -107,47 +114,6 @@ class WhisperState: NSObject, ObservableObject, AVAudioRecorderDelegate {
}
}
private func loadAvailableModels() {
do {
let fileURLs = try FileManager.default.contentsOfDirectory(at: modelsDirectory, includingPropertiesForKeys: nil)
availableModels = fileURLs.compactMap { url in
guard url.pathExtension == "bin" else { return nil }
return WhisperModel(name: url.deletingPathExtension().lastPathComponent, url: url)
}
} catch {
messageLog += "Error loading available models: \(error.localizedDescription)\n"
}
}
private func loadModel(_ model: WhisperModel) async throws {
guard whisperContext == nil else { return }
logger.notice("🔄 Loading Whisper model: \(model.name)")
isModelLoading = true
defer { isModelLoading = false }
do {
whisperContext = try await WhisperContext.createContext(path: model.url.path)
isModelLoaded = true
currentModel = model
logger.notice("✅ Successfully loaded model: \(model.name)")
} catch {
logger.error("❌ Failed to load model: \(model.name) - \(error.localizedDescription)")
throw WhisperStateError.modelLoadFailed
}
}
func setDefaultModel(_ model: WhisperModel) async {
do {
currentModel = model
UserDefaults.standard.set(model.name, forKey: "CurrentModel")
canTranscribe = true
} catch {
currentError = error as? WhisperStateError ?? .unknownError
canTranscribe = false
}
}
func toggleRecord() async {
if isRecording {
logger.notice("🛑 Stopping recording")
@ -257,58 +223,6 @@ class WhisperState: NSObject, ObservableObject, AVAudioRecorderDelegate {
private func onDidFinishRecording(success: Bool) {
isRecording = false
}
@Published var downloadProgress: [String: Double] = [:]
func downloadModel(_ model: PredefinedModel) async {
guard let url = URL(string: model.downloadURL) else { return }
logger.notice("🔽 Downloading model: \(model.name)")
do {
let (data, response) = try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<(Data, URLResponse), Error>) in
let task = URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error {
continuation.resume(throwing: error)
return
}
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode),
let data = data else {
continuation.resume(throwing: URLError(.badServerResponse))
return
}
continuation.resume(returning: (data, httpResponse))
}
task.resume()
let observation = task.progress.observe(\.fractionCompleted) { progress, _ in
DispatchQueue.main.async {
self.downloadProgress[model.name] = progress.fractionCompleted
}
}
Task {
await withTaskCancellationHandler {
observation.invalidate()
} operation: {
await withCheckedContinuation { (_: CheckedContinuation<Void, Never>) in }
}
}
}
let destinationURL = modelsDirectory.appendingPathComponent(model.filename)
try data.write(to: destinationURL)
availableModels.append(WhisperModel(name: model.name, url: destinationURL))
self.downloadProgress.removeValue(forKey: model.name)
logger.notice("✅ Successfully downloaded model: \(model.name)")
} catch {
logger.error("❌ Failed to download model: \(model.name) - \(error.localizedDescription)")
currentError = .modelDownloadFailed
self.downloadProgress.removeValue(forKey: model.name)
}
}
private func transcribeAudio(_ url: URL, duration: TimeInterval) async {
if shouldCancelRecording { return }
@ -328,7 +242,7 @@ class WhisperState: NSObject, ObservableObject, AVAudioRecorderDelegate {
logger.error("❌ Failed to load model: \(currentModel.name) - \(error.localizedDescription)")
messageLog += "Failed to load transcription model. Please try again.\n"
currentError = .modelLoadFailed
await cleanupResources()
await cleanupModelResources()
return
}
}
@ -350,7 +264,7 @@ class WhisperState: NSObject, ObservableObject, AVAudioRecorderDelegate {
let permanentURLString = permanentURL.absoluteString
if shouldCancelRecording {
await cleanupResources()
await cleanupModelResources()
return
}
@ -358,7 +272,7 @@ class WhisperState: NSObject, ObservableObject, AVAudioRecorderDelegate {
let data = try readAudioSamples(url)
if shouldCancelRecording {
await cleanupResources()
await cleanupModelResources()
return
}
@ -367,14 +281,14 @@ class WhisperState: NSObject, ObservableObject, AVAudioRecorderDelegate {
await whisperContext.setPrompt(whisperPrompt.transcriptionPrompt)
if shouldCancelRecording {
await cleanupResources()
await cleanupModelResources()
return
}
await whisperContext.fullTranscribe(samples: data)
if shouldCancelRecording {
await cleanupResources()
await cleanupModelResources()
return
}
@ -393,7 +307,7 @@ class WhisperState: NSObject, ObservableObject, AVAudioRecorderDelegate {
enhancementService.isConfigured {
do {
if shouldCancelRecording {
await cleanupResources()
await cleanupModelResources()
return
}
@ -461,14 +375,14 @@ class WhisperState: NSObject, ObservableObject, AVAudioRecorderDelegate {
}
}
await cleanupResources()
await cleanupModelResources()
await dismissMiniRecorder()
} catch {
messageLog += "\(error.localizedDescription)\n"
currentError = .transcriptionFailed
await cleanupResources()
await cleanupModelResources()
await dismissMiniRecorder()
}
}
@ -488,174 +402,8 @@ class WhisperState: NSObject, ObservableObject, AVAudioRecorderDelegate {
return floats
}
func deleteModel(_ model: WhisperModel) async {
do {
try FileManager.default.removeItem(at: model.url)
availableModels.removeAll { $0.id == model.id }
if currentModel?.id == model.id {
currentModel = nil
canTranscribe = false
}
} catch {
messageLog += "Error deleting model: \(error.localizedDescription)\n"
currentError = .modelDeletionFailed
}
}
@Published var isVisualizerActive = false
@Published var isMiniRecorderVisible = false {
didSet {
if isMiniRecorderVisible {
showRecorderPanel()
} else {
hideRecorderPanel()
}
}
}
private func setupNotifications() {
NotificationCenter.default.addObserver(self, selector: #selector(handleToggleMiniRecorder), name: .toggleMiniRecorder, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(handleLicenseStatusChanged), name: .licenseStatusChanged, object: nil)
}
@objc public func handleToggleMiniRecorder() {
if isMiniRecorderVisible {
Task {
await toggleRecord()
}
} else {
Task {
await toggleRecord()
SoundManager.shared.playStartSound()
await MainActor.run {
showRecorderPanel()
isMiniRecorderVisible = true
}
}
}
}
@objc private func handleLicenseStatusChanged() {
// This will refresh the license state when it changes elsewhere in the app
self.licenseViewModel = LicenseViewModel()
}
private func showRecorderPanel() {
logger.notice("📱 Showing \(self.recorderType) recorder")
if recorderType == "notch" {
if notchWindowManager == nil {
notchWindowManager = NotchWindowManager(whisperState: self, recorder: recorder)
logger.info("Created new notch window manager")
}
notchWindowManager?.show()
} else {
if miniWindowManager == nil {
miniWindowManager = MiniWindowManager(whisperState: self, recorder: recorder)
logger.info("Created new mini window manager")
}
miniWindowManager?.show()
}
}
private func hideRecorderPanel() {
if isRecording {
Task {
await toggleRecord()
}
}
}
func toggleMiniRecorder() async {
if isMiniRecorderVisible {
await dismissMiniRecorder()
} else {
Task {
await toggleRecord()
SoundManager.shared.playStartSound()
await MainActor.run {
showRecorderPanel()
isMiniRecorderVisible = true
}
}
}
}
private func cleanupResources() async {
if !isRecording && !isProcessing {
logger.notice("🧹 Cleaning up Whisper resources")
await whisperContext?.releaseResources()
whisperContext = nil
isModelLoaded = false
}
}
func dismissMiniRecorder() async {
logger.notice("📱 Dismissing \(self.recorderType) recorder")
shouldCancelRecording = true
if isRecording {
await recorder.stopRecording()
}
if recorderType == "notch" {
notchWindowManager?.hide()
} else {
miniWindowManager?.hide()
}
await MainActor.run {
isRecording = false
isVisualizerActive = false
isProcessing = false
isTranscribing = false
canTranscribe = true
isMiniRecorderVisible = false
shouldCancelRecording = false
}
try? await Task.sleep(nanoseconds: 150_000_000)
await cleanupResources()
}
func cancelRecording() async {
shouldCancelRecording = true
SoundManager.shared.playEscSound()
if isRecording {
await recorder.stopRecording()
}
await dismissMiniRecorder()
}
@Published var currentError: WhisperStateError?
func unloadModel() {
Task {
await whisperContext?.releaseResources()
whisperContext = nil
isModelLoaded = false
if let recordedFile = recordedFile {
try? FileManager.default.removeItem(at: recordedFile)
self.recordedFile = nil
}
}
}
private func clearDownloadedModels() async {
for model in availableModels {
do {
try FileManager.default.removeItem(at: model.url)
} catch {
messageLog += "Error deleting model: \(error.localizedDescription)\n"
}
}
availableModels.removeAll()
}
func getEnhancementService() -> AIEnhancementService? {
return enhancementService
}