Improve prompt icon picker: grid popover UI, 30+ productivity icons, system colors
This commit is contained in:
parent
417adba2bc
commit
ed9a13c16b
@ -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(
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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."
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -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))
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
) {
|
) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user