From ed9a13c16b434411f4e71da56c9f8d5124fa7e77 Mon Sep 17 00:00:00 2001 From: Beingpax Date: Fri, 31 Oct 2025 12:10:57 +0545 Subject: [PATCH] Improve prompt icon picker: grid popover UI, 30+ productivity icons, system colors --- VoiceInk/Models/CustomPrompt.swift | 136 +++++++++--------- VoiceInk/Models/PredefinedPrompts.swift | 4 +- VoiceInk/Models/PromptTemplates.swift | 8 +- VoiceInk/Services/AIEnhancementService.swift | 2 +- VoiceInk/Views/MenuBarView.swift | 2 +- VoiceInk/Views/PromptEditorView.swift | 131 ++++++++--------- .../Recorder/EnhancementPromptPopover.swift | 2 +- .../Views/Recorder/RecorderComponents.swift | 2 +- 8 files changed, 145 insertions(+), 142 deletions(-) diff --git a/VoiceInk/Models/CustomPrompt.swift b/VoiceInk/Models/CustomPrompt.swift index 6c7e226..2260457 100644 --- a/VoiceInk/Models/CustomPrompt.swift +++ b/VoiceInk/Models/CustomPrompt.swift @@ -1,78 +1,78 @@ import Foundation import SwiftUI -enum PromptIcon: String, Codable, CaseIterable { - // Document & Text - case documentFill = "doc.text.fill" - case textbox = "textbox" - case sealedFill = "checkmark.seal.fill" - - // Communication - case chatFill = "bubble.left.and.bubble.right.fill" - case messageFill = "message.fill" - case emailFill = "envelope.fill" - - // Professional - case meetingFill = "person.2.fill" - case presentationFill = "person.wave.2.fill" - case briefcaseFill = "briefcase.fill" - - // Technical - case codeFill = "curlybraces" - case terminalFill = "terminal.fill" - case gearFill = "gearshape.fill" - - // Content - case blogFill = "doc.text.image.fill" - case notesFill = "note" - case bookFill = "book.fill" - case bookmarkFill = "bookmark.fill" - case pencilFill = "pencil.circle.fill" - - // Media & Creative - case videoFill = "video.fill" - case micFill = "mic.fill" - case musicFill = "music.note" - case photoFill = "photo.fill" - case brushFill = "paintbrush.fill" - - var title: String { - switch self { +typealias PromptIcon = String + +extension PromptIcon { + static let allCases: [PromptIcon] = [ // Document & Text - case .documentFill: return "Document" - case .textbox: return "Textbox" - case .sealedFill: return "Sealed" - + "doc.text.fill", + "textbox", + "checkmark.seal.fill", + // Communication - case .chatFill: return "Chat" - case .messageFill: return "Message" - case .emailFill: return "Email" - + "bubble.left.and.bubble.right.fill", + "message.fill", + "envelope.fill", + // Professional - case .meetingFill: return "Meeting" - case .presentationFill: return "Presentation" - case .briefcaseFill: return "Briefcase" - + "person.2.fill", + "person.wave.2.fill", + "briefcase.fill", + // Technical - case .codeFill: return "Code" - case .terminalFill: return "Terminal" - case .gearFill: return "Settings" - + "curlybraces", + "terminal.fill", + "gearshape.fill", + // Content - case .blogFill: return "Blog" - case .notesFill: return "Notes" - case .bookFill: return "Book" - case .bookmarkFill: return "Bookmark" - case .pencilFill: return "Edit" - + "doc.text.image.fill", + "note", + "book.fill", + "bookmark.fill", + "pencil.circle.fill", + // Media & Creative - case .videoFill: return "Video" - case .micFill: return "Audio" - case .musicFill: return "Music" - case .photoFill: return "Photo" - case .brushFill: return "Design" - } - } + "video.fill", + "mic.fill", + "music.note", + "photo.fill", + "paintbrush.fill", + + // Productivity & Time + "clock.fill", + "calendar", + "list.bullet", + "checkmark.circle.fill", + "timer", + "hourglass", + "star.fill", + "flag.fill", + "tag.fill", + "folder.fill", + "paperclip", + "tray.fill", + "chart.bar.fill", + "flame.fill", + "target", + "list.clipboard.fill", + "brain.head.profile", + "lightbulb.fill", + "megaphone.fill", + "heart.fill", + "map.fill", + "house.fill", + "camera.fill", + "figure.walk", + "dumbbell.fill", + "cart.fill", + "creditcard.fill", + "graduationcap.fill", + "airplane", + "leaf.fill", + "hand.raised.fill", + "hand.thumbsup.fill" + ] } struct CustomPrompt: Identifiable, Codable, Equatable { @@ -91,7 +91,7 @@ struct CustomPrompt: Identifiable, Codable, Equatable { title: String, promptText: String, isActive: Bool = false, - icon: PromptIcon = .documentFill, + icon: PromptIcon = "doc.text.fill", description: String? = nil, isPredefined: Bool = false, triggerWords: [String] = [], @@ -199,7 +199,7 @@ extension CustomPrompt { .blur(radius: 2) // Icon with enhanced effects - Image(systemName: icon.rawValue) + Image(systemName: icon) .font(.system(size: 20, weight: .medium)) .foregroundStyle( LinearGradient( diff --git a/VoiceInk/Models/PredefinedPrompts.swift b/VoiceInk/Models/PredefinedPrompts.swift index 69a86a2..15718c1 100644 --- a/VoiceInk/Models/PredefinedPrompts.swift +++ b/VoiceInk/Models/PredefinedPrompts.swift @@ -19,7 +19,7 @@ enum PredefinedPrompts { id: defaultPromptId, title: "Default", promptText: PromptTemplates.all.first { $0.title == "System Default" }?.promptText ?? "", - icon: .sealedFill, + icon: "checkmark.seal.fill", description: "Default mode to improved clarity and accuracy of the transcription", isPredefined: true, useSystemInstructions: true @@ -29,7 +29,7 @@ enum PredefinedPrompts { id: assistantPromptId, title: "Assistant", promptText: AIPrompts.assistantMode, - icon: .chatFill, + icon: "bubble.left.and.bubble.right.fill", description: "AI assistant that provides direct answers to queries", isPredefined: true, useSystemInstructions: false diff --git a/VoiceInk/Models/PromptTemplates.swift b/VoiceInk/Models/PromptTemplates.swift index ec0e04c..47eb348 100644 --- a/VoiceInk/Models/PromptTemplates.swift +++ b/VoiceInk/Models/PromptTemplates.swift @@ -42,7 +42,7 @@ enum PromptTemplates { - Output only the cleaned text. - Don't add any information not available in the text ever. """, - icon: .sealedFill, + icon: "checkmark.seal.fill", description: "Default system prompt for improving clarity and accuracy of transcriptions" ), TemplatePrompt( @@ -60,7 +60,7 @@ enum PromptTemplates { - Output only the chat message. - Don't add any information not available in the text ever. """, - icon: .chatFill, + icon: "bubble.left.and.bubble.right.fill", description: "Casual chat-style formatting" ), @@ -76,7 +76,7 @@ enum PromptTemplates { - Do not invent new content, but structure it as a proper email format. - Don't add any information not available in the text ever. """, - icon: .emailFill, + icon: "envelope.fill", description: "Template for converting casual messages into professional email format" ), TemplatePrompt( @@ -95,7 +95,7 @@ enum PromptTemplates { - Output only the rewritten text. - Don't add any information not available in the text ever. """, - icon: .pencilFill, + icon: "pencil.circle.fill", description: "Rewrites transcriptions with enhanced clarity, improved sentence structure, and rhythmic flow while preserving original meaning." ) ] diff --git a/VoiceInk/Services/AIEnhancementService.swift b/VoiceInk/Services/AIEnhancementService.swift index 0ab70f6..d5042c5 100644 --- a/VoiceInk/Services/AIEnhancementService.swift +++ b/VoiceInk/Services/AIEnhancementService.swift @@ -415,7 +415,7 @@ class AIEnhancementService: ObservableObject { screenCaptureService.lastCapturedText = nil } - func addPrompt(title: String, promptText: String, icon: PromptIcon = .documentFill, description: String? = nil, triggerWords: [String] = [], useSystemInstructions: Bool = true) { + func addPrompt(title: String, promptText: String, icon: PromptIcon = "doc.text.fill", description: String? = nil, triggerWords: [String] = [], useSystemInstructions: Bool = true) { let newPrompt = CustomPrompt(title: title, promptText: promptText, icon: icon, description: description, isPredefined: false, triggerWords: triggerWords, useSystemInstructions: useSystemInstructions) customPrompts.append(newPrompt) if customPrompts.count == 1 { diff --git a/VoiceInk/Views/MenuBarView.swift b/VoiceInk/Views/MenuBarView.swift index 8067481..bf7543d 100644 --- a/VoiceInk/Views/MenuBarView.swift +++ b/VoiceInk/Views/MenuBarView.swift @@ -53,7 +53,7 @@ struct MenuBarView: View { enhancementService.setActivePrompt(prompt) } label: { HStack { - Image(systemName: prompt.icon.rawValue) + Image(systemName: prompt.icon) .foregroundColor(.accentColor) Text(prompt.title) if enhancementService.selectedPromptId == prompt.id { diff --git a/VoiceInk/Views/PromptEditorView.swift b/VoiceInk/Views/PromptEditorView.swift index 31498fc..46c497e 100644 --- a/VoiceInk/Views/PromptEditorView.swift +++ b/VoiceInk/Views/PromptEditorView.swift @@ -27,6 +27,7 @@ struct PromptEditorView: View { @State private var triggerWords: [String] @State private var showingPredefinedPrompts = false @State private var useSystemInstructions: Bool + @State private var showingIconPicker = false private var isEditingPredefinedPrompt: Bool { if case .edit(let prompt) = mode { @@ -41,7 +42,7 @@ struct PromptEditorView: View { case .add: _title = State(initialValue: "") _promptText = State(initialValue: "") - _selectedIcon = State(initialValue: .documentFill) + _selectedIcon = State(initialValue: "doc.text.fill") _description = State(initialValue: "") _triggerWords = State(initialValue: []) _useSystemInstructions = State(initialValue: true) @@ -132,29 +133,25 @@ struct PromptEditorView: View { .font(.headline) .foregroundColor(.secondary) - Menu { - IconMenuContent(selectedIcon: $selectedIcon) - } label: { - HStack { - Image(systemName: selectedIcon.rawValue) - .font(.system(size: 16)) - .foregroundColor(.accentColor) - .frame(width: 24) - - Text(selectedIcon.title) - .foregroundColor(.primary) - - Spacer() - - Image(systemName: "chevron.up.chevron.down") - .font(.system(size: 12)) - .foregroundColor(.secondary) - } - .padding(8) - .background(Color(NSColor.controlBackgroundColor)) - .cornerRadius(8) + // Preview of selected icon - clickable to open popover (square button) + Button(action: { + showingIconPicker = true + }) { + Image(systemName: selectedIcon) + .font(.system(size: 20)) + .foregroundColor(.primary) + .frame(width: 48, height: 48) + .background(Color(NSColor.controlBackgroundColor)) + .cornerRadius(8) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(Color.secondary.opacity(0.2), lineWidth: 1) + ) } - .frame(width: 180) + .buttonStyle(.plain) + } + .popover(isPresented: $showingIconPicker, arrowEdge: .bottom) { + IconPickerPopover(selectedIcon: $selectedIcon, isPresented: $showingIconPicker) } } .padding(.horizontal) @@ -299,7 +296,7 @@ struct CleanTemplateButton: View { .fill(Color.accentColor.opacity(0.15)) .frame(width: 44, height: 44) - Image(systemName: prompt.icon.rawValue) + Image(systemName: prompt.icon) .font(.system(size: 20, weight: .semibold)) .foregroundColor(.accentColor) } @@ -342,7 +339,7 @@ struct TemplateButton: View { var body: some View { Button(action: action) { HStack(alignment: .center, spacing: 12) { - Image(systemName: prompt.icon.rawValue) + Image(systemName: prompt.icon) .font(.system(size: 20, weight: .medium)) .foregroundColor(.accentColor) .frame(width: 28, height: 28) @@ -425,44 +422,6 @@ struct TriggerWordsEditor: View { } } -// Icon menu content for better organization -struct IconMenuContent: View { - @Binding var selectedIcon: PromptIcon - - var body: some View { - Group { - IconMenuSection(title: "Document & Text", icons: [.documentFill, .textbox, .sealedFill], selectedIcon: $selectedIcon) - IconMenuSection(title: "Communication", icons: [.chatFill, .messageFill, .emailFill], selectedIcon: $selectedIcon) - IconMenuSection(title: "Professional", icons: [.meetingFill, .presentationFill, .briefcaseFill], selectedIcon: $selectedIcon) - IconMenuSection(title: "Technical", icons: [.codeFill, .terminalFill, .gearFill], selectedIcon: $selectedIcon) - IconMenuSection(title: "Content", icons: [.blogFill, .notesFill, .bookFill, .bookmarkFill, .pencilFill], selectedIcon: $selectedIcon) - IconMenuSection(title: "Media & Creative", icons: [.videoFill, .micFill, .musicFill, .photoFill, .brushFill], selectedIcon: $selectedIcon) - } - } -} - -// Icon menu section for better organization -struct IconMenuSection: View { - let title: String - let icons: [PromptIcon] - @Binding var selectedIcon: PromptIcon - - var body: some View { - Group { - Text(title) - .font(.caption) - .foregroundColor(.secondary) - ForEach(icons, id: \.self) { icon in - Button(action: { selectedIcon = icon }) { - Label(icon.title, systemImage: icon.rawValue) - } - } - if title != "Media & Creative" { - Divider() - } - } - } -} struct TriggerWordItemView: View { let word: String @@ -503,4 +462,48 @@ struct TriggerWordItemView: View { .stroke(Color.secondary.opacity(0.2), lineWidth: 1) } } -} \ No newline at end of file +} + +// Icon Picker Popover - shows icons in a grid format without category labels +struct IconPickerPopover: View { + @Binding var selectedIcon: PromptIcon + @Binding var isPresented: Bool + + var body: some View { + let columns = [ + GridItem(.adaptive(minimum: 45, maximum: 52), spacing: 14) + ] + + ScrollView { + LazyVGrid(columns: columns, spacing: 14) { + ForEach(PromptIcon.allCases, id: \.self) { icon in + Button(action: { + withAnimation(.spring(response: 0.2, dampingFraction: 0.7)) { + selectedIcon = icon + isPresented = false + } + }) { + ZStack { + RoundedRectangle(cornerRadius: 12) + .fill(selectedIcon == icon ? Color(NSColor.windowBackgroundColor) : Color(NSColor.controlBackgroundColor)) + .frame(width: 52, height: 52) + .overlay( + RoundedRectangle(cornerRadius: 12) + .stroke(selectedIcon == icon ? Color(NSColor.separatorColor) : Color.secondary.opacity(0.2), lineWidth: selectedIcon == icon ? 2 : 1) + ) + + Image(systemName: icon) + .font(.system(size: 24, weight: .medium)) + .foregroundColor(.primary) + } + .scaleEffect(selectedIcon == icon ? 1.1 : 1.0) + .animation(.spring(response: 0.2, dampingFraction: 0.7), value: selectedIcon == icon) + } + .buttonStyle(.plain) + } + } + .padding(20) + } + .frame(width: 400, height: 400) + } +} diff --git a/VoiceInk/Views/Recorder/EnhancementPromptPopover.swift b/VoiceInk/Views/Recorder/EnhancementPromptPopover.swift index 7ff4ae0..df51ee9 100644 --- a/VoiceInk/Views/Recorder/EnhancementPromptPopover.swift +++ b/VoiceInk/Views/Recorder/EnhancementPromptPopover.swift @@ -70,7 +70,7 @@ struct EnhancementPromptRow: View { Button(action: action) { HStack(spacing: 8) { // Use the icon from the prompt - Image(systemName: prompt.icon.rawValue) + Image(systemName: prompt.icon) .font(.system(size: 14)) .foregroundColor(isDisabled ? .white.opacity(0.4) : .white.opacity(0.7)) diff --git a/VoiceInk/Views/Recorder/RecorderComponents.swift b/VoiceInk/Views/Recorder/RecorderComponents.swift index 6137cc1..b78e335 100644 --- a/VoiceInk/Views/Recorder/RecorderComponents.swift +++ b/VoiceInk/Views/Recorder/RecorderComponents.swift @@ -167,7 +167,7 @@ struct RecorderPromptButton: View { var body: some View { RecorderToggleButton( isEnabled: enhancementService.isEnhancementEnabled, - icon: enhancementService.activePrompt?.icon.rawValue ?? enhancementService.allPrompts.first(where: { $0.id == PredefinedPrompts.defaultPromptId })?.icon.rawValue ?? "checkmark.seal.fill", + icon: enhancementService.activePrompt?.icon ?? enhancementService.allPrompts.first(where: { $0.id == PredefinedPrompts.defaultPromptId })?.icon ?? "checkmark.seal.fill", color: .blue, disabled: false ) {