Improve prompt icon picker: grid popover UI, 30+ productivity icons, system colors

This commit is contained in:
Beingpax 2025-10-31 12:10:57 +05:45
parent 417adba2bc
commit ed9a13c16b
8 changed files with 145 additions and 142 deletions

View File

@ -1,78 +1,78 @@
import Foundation import Foundation
import SwiftUI import SwiftUI
enum PromptIcon: String, Codable, CaseIterable { typealias PromptIcon = String
extension PromptIcon {
static let allCases: [PromptIcon] = [
// Document & Text // Document & Text
case documentFill = "doc.text.fill" "doc.text.fill",
case textbox = "textbox" "textbox",
case sealedFill = "checkmark.seal.fill" "checkmark.seal.fill",
// Communication // Communication
case chatFill = "bubble.left.and.bubble.right.fill" "bubble.left.and.bubble.right.fill",
case messageFill = "message.fill" "message.fill",
case emailFill = "envelope.fill" "envelope.fill",
// Professional // Professional
case meetingFill = "person.2.fill" "person.2.fill",
case presentationFill = "person.wave.2.fill" "person.wave.2.fill",
case briefcaseFill = "briefcase.fill" "briefcase.fill",
// Technical // Technical
case codeFill = "curlybraces" "curlybraces",
case terminalFill = "terminal.fill" "terminal.fill",
case gearFill = "gearshape.fill" "gearshape.fill",
// Content // Content
case blogFill = "doc.text.image.fill" "doc.text.image.fill",
case notesFill = "note" "note",
case bookFill = "book.fill" "book.fill",
case bookmarkFill = "bookmark.fill" "bookmark.fill",
case pencilFill = "pencil.circle.fill" "pencil.circle.fill",
// Media & Creative // Media & Creative
case videoFill = "video.fill" "video.fill",
case micFill = "mic.fill" "mic.fill",
case musicFill = "music.note" "music.note",
case photoFill = "photo.fill" "photo.fill",
case brushFill = "paintbrush.fill" "paintbrush.fill",
var title: String { // Productivity & Time
switch self { "clock.fill",
// Document & Text "calendar",
case .documentFill: return "Document" "list.bullet",
case .textbox: return "Textbox" "checkmark.circle.fill",
case .sealedFill: return "Sealed" "timer",
"hourglass",
// Communication "star.fill",
case .chatFill: return "Chat" "flag.fill",
case .messageFill: return "Message" "tag.fill",
case .emailFill: return "Email" "folder.fill",
"paperclip",
// Professional "tray.fill",
case .meetingFill: return "Meeting" "chart.bar.fill",
case .presentationFill: return "Presentation" "flame.fill",
case .briefcaseFill: return "Briefcase" "target",
"list.clipboard.fill",
// Technical "brain.head.profile",
case .codeFill: return "Code" "lightbulb.fill",
case .terminalFill: return "Terminal" "megaphone.fill",
case .gearFill: return "Settings" "heart.fill",
"map.fill",
// Content "house.fill",
case .blogFill: return "Blog" "camera.fill",
case .notesFill: return "Notes" "figure.walk",
case .bookFill: return "Book" "dumbbell.fill",
case .bookmarkFill: return "Bookmark" "cart.fill",
case .pencilFill: return "Edit" "creditcard.fill",
"graduationcap.fill",
// Media & Creative "airplane",
case .videoFill: return "Video" "leaf.fill",
case .micFill: return "Audio" "hand.raised.fill",
case .musicFill: return "Music" "hand.thumbsup.fill"
case .photoFill: return "Photo" ]
case .brushFill: return "Design"
}
}
} }
struct CustomPrompt: Identifiable, Codable, Equatable { struct CustomPrompt: Identifiable, Codable, Equatable {
@ -91,7 +91,7 @@ struct CustomPrompt: Identifiable, Codable, Equatable {
title: String, title: String,
promptText: String, promptText: String,
isActive: Bool = false, isActive: Bool = false,
icon: PromptIcon = .documentFill, icon: PromptIcon = "doc.text.fill",
description: String? = nil, description: String? = nil,
isPredefined: Bool = false, isPredefined: Bool = false,
triggerWords: [String] = [], triggerWords: [String] = [],
@ -199,7 +199,7 @@ extension CustomPrompt {
.blur(radius: 2) .blur(radius: 2)
// Icon with enhanced effects // Icon with enhanced effects
Image(systemName: icon.rawValue) Image(systemName: icon)
.font(.system(size: 20, weight: .medium)) .font(.system(size: 20, weight: .medium))
.foregroundStyle( .foregroundStyle(
LinearGradient( LinearGradient(

View File

@ -19,7 +19,7 @@ enum PredefinedPrompts {
id: defaultPromptId, id: defaultPromptId,
title: "Default", title: "Default",
promptText: PromptTemplates.all.first { $0.title == "System Default" }?.promptText ?? "", 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", description: "Default mode to improved clarity and accuracy of the transcription",
isPredefined: true, isPredefined: true,
useSystemInstructions: true useSystemInstructions: true
@ -29,7 +29,7 @@ enum PredefinedPrompts {
id: assistantPromptId, id: assistantPromptId,
title: "Assistant", title: "Assistant",
promptText: AIPrompts.assistantMode, promptText: AIPrompts.assistantMode,
icon: .chatFill, icon: "bubble.left.and.bubble.right.fill",
description: "AI assistant that provides direct answers to queries", description: "AI assistant that provides direct answers to queries",
isPredefined: true, isPredefined: true,
useSystemInstructions: false useSystemInstructions: false

View File

@ -42,7 +42,7 @@ enum PromptTemplates {
- Output only the cleaned text. - Output only the cleaned text.
- Don't add any information not available in the <TRANSCRIPT> text ever. - Don't add any information not available in the <TRANSCRIPT> text ever.
""", """,
icon: .sealedFill, icon: "checkmark.seal.fill",
description: "Default system prompt for improving clarity and accuracy of transcriptions" description: "Default system prompt for improving clarity and accuracy of transcriptions"
), ),
TemplatePrompt( TemplatePrompt(
@ -60,7 +60,7 @@ enum PromptTemplates {
- Output only the chat message. - Output only the chat message.
- Don't add any information not available in the <TRANSCRIPT> text ever. - Don't add any information not available in the <TRANSCRIPT> text ever.
""", """,
icon: .chatFill, icon: "bubble.left.and.bubble.right.fill",
description: "Casual chat-style formatting" description: "Casual chat-style formatting"
), ),
@ -76,7 +76,7 @@ enum PromptTemplates {
- Do not invent new content, but structure it as a proper email format. - Do not invent new content, but structure it as a proper email format.
- Don't add any information not available in the <TRANSCRIPT> text ever. - Don't add any information not available in the <TRANSCRIPT> text ever.
""", """,
icon: .emailFill, icon: "envelope.fill",
description: "Template for converting casual messages into professional email format" description: "Template for converting casual messages into professional email format"
), ),
TemplatePrompt( TemplatePrompt(
@ -95,7 +95,7 @@ enum PromptTemplates {
- Output only the rewritten text. - Output only the rewritten text.
- Don't add any information not available in the <TRANSCRIPT> text ever. - Don't add any information not available in the <TRANSCRIPT> text ever.
""", """,
icon: .pencilFill, icon: "pencil.circle.fill",
description: "Rewrites transcriptions with enhanced clarity, improved sentence structure, and rhythmic flow while preserving original meaning." description: "Rewrites transcriptions with enhanced clarity, improved sentence structure, and rhythmic flow while preserving original meaning."
) )
] ]

View File

@ -415,7 +415,7 @@ class AIEnhancementService: ObservableObject {
screenCaptureService.lastCapturedText = nil 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) let newPrompt = CustomPrompt(title: title, promptText: promptText, icon: icon, description: description, isPredefined: false, triggerWords: triggerWords, useSystemInstructions: useSystemInstructions)
customPrompts.append(newPrompt) customPrompts.append(newPrompt)
if customPrompts.count == 1 { if customPrompts.count == 1 {

View File

@ -53,7 +53,7 @@ struct MenuBarView: View {
enhancementService.setActivePrompt(prompt) enhancementService.setActivePrompt(prompt)
} label: { } label: {
HStack { HStack {
Image(systemName: prompt.icon.rawValue) Image(systemName: prompt.icon)
.foregroundColor(.accentColor) .foregroundColor(.accentColor)
Text(prompt.title) Text(prompt.title)
if enhancementService.selectedPromptId == prompt.id { if enhancementService.selectedPromptId == prompt.id {

View File

@ -27,6 +27,7 @@ struct PromptEditorView: View {
@State private var triggerWords: [String] @State private var triggerWords: [String]
@State private var showingPredefinedPrompts = false @State private var showingPredefinedPrompts = false
@State private var useSystemInstructions: Bool @State private var useSystemInstructions: Bool
@State private var showingIconPicker = false
private var isEditingPredefinedPrompt: Bool { private var isEditingPredefinedPrompt: Bool {
if case .edit(let prompt) = mode { if case .edit(let prompt) = mode {
@ -41,7 +42,7 @@ struct PromptEditorView: View {
case .add: case .add:
_title = State(initialValue: "") _title = State(initialValue: "")
_promptText = State(initialValue: "") _promptText = State(initialValue: "")
_selectedIcon = State(initialValue: .documentFill) _selectedIcon = State(initialValue: "doc.text.fill")
_description = State(initialValue: "") _description = State(initialValue: "")
_triggerWords = State(initialValue: []) _triggerWords = State(initialValue: [])
_useSystemInstructions = State(initialValue: true) _useSystemInstructions = State(initialValue: true)
@ -132,29 +133,25 @@ struct PromptEditorView: View {
.font(.headline) .font(.headline)
.foregroundColor(.secondary) .foregroundColor(.secondary)
Menu { // Preview of selected icon - clickable to open popover (square button)
IconMenuContent(selectedIcon: $selectedIcon) Button(action: {
} label: { showingIconPicker = true
HStack { }) {
Image(systemName: selectedIcon.rawValue) Image(systemName: selectedIcon)
.font(.system(size: 16)) .font(.system(size: 20))
.foregroundColor(.accentColor)
.frame(width: 24)
Text(selectedIcon.title)
.foregroundColor(.primary) .foregroundColor(.primary)
.frame(width: 48, height: 48)
Spacer()
Image(systemName: "chevron.up.chevron.down")
.font(.system(size: 12))
.foregroundColor(.secondary)
}
.padding(8)
.background(Color(NSColor.controlBackgroundColor)) .background(Color(NSColor.controlBackgroundColor))
.cornerRadius(8) .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) .padding(.horizontal)
@ -299,7 +296,7 @@ struct CleanTemplateButton: View {
.fill(Color.accentColor.opacity(0.15)) .fill(Color.accentColor.opacity(0.15))
.frame(width: 44, height: 44) .frame(width: 44, height: 44)
Image(systemName: prompt.icon.rawValue) Image(systemName: prompt.icon)
.font(.system(size: 20, weight: .semibold)) .font(.system(size: 20, weight: .semibold))
.foregroundColor(.accentColor) .foregroundColor(.accentColor)
} }
@ -342,7 +339,7 @@ struct TemplateButton: View {
var body: some View { var body: some View {
Button(action: action) { Button(action: action) {
HStack(alignment: .center, spacing: 12) { HStack(alignment: .center, spacing: 12) {
Image(systemName: prompt.icon.rawValue) Image(systemName: prompt.icon)
.font(.system(size: 20, weight: .medium)) .font(.system(size: 20, weight: .medium))
.foregroundColor(.accentColor) .foregroundColor(.accentColor)
.frame(width: 28, height: 28) .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 { struct TriggerWordItemView: View {
let word: String let word: String
@ -504,3 +463,47 @@ struct TriggerWordItemView: View {
} }
} }
} }
// 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)
}
}

View File

@ -70,7 +70,7 @@ struct EnhancementPromptRow: View {
Button(action: action) { Button(action: action) {
HStack(spacing: 8) { HStack(spacing: 8) {
// Use the icon from the prompt // Use the icon from the prompt
Image(systemName: prompt.icon.rawValue) Image(systemName: prompt.icon)
.font(.system(size: 14)) .font(.system(size: 14))
.foregroundColor(isDisabled ? .white.opacity(0.4) : .white.opacity(0.7)) .foregroundColor(isDisabled ? .white.opacity(0.4) : .white.opacity(0.7))

View File

@ -167,7 +167,7 @@ struct RecorderPromptButton: View {
var body: some View { var body: some View {
RecorderToggleButton( RecorderToggleButton(
isEnabled: enhancementService.isEnhancementEnabled, 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, color: .blue,
disabled: false disabled: false
) { ) {