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 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(

View File

@ -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

View File

@ -42,7 +42,7 @@ enum PromptTemplates {
- Output only the cleaned text.
- 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"
),
TemplatePrompt(
@ -60,7 +60,7 @@ enum PromptTemplates {
- Output only the chat message.
- 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"
),
@ -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 <TRANSCRIPT> 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 <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."
)
]

View File

@ -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 {

View File

@ -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 {

View File

@ -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)
}
}
}
}
// 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) {
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))

View File

@ -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
) {