Merge pull request #352 from Beingpax/improve-enhancement-prompt-view

Improve enhancement prompt view
This commit is contained in:
Prakash Joshi Pax 2025-10-31 18:39:05 +05:45 committed by GitHub
commit b02a22cb95
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 260 additions and 255 deletions

View File

@ -1,78 +1,78 @@
import Foundation import Foundation
import SwiftUI import SwiftUI
enum PromptIcon: String, Codable, CaseIterable { typealias PromptIcon = String
// Document & Text
case documentFill = "doc.text.fill" extension PromptIcon {
case textbox = "textbox" static let allCases: [PromptIcon] = [
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 {
// Document & Text // Document & Text
case .documentFill: return "Document" "doc.text.fill",
case .textbox: return "Textbox" "textbox",
case .sealedFill: return "Sealed" "checkmark.seal.fill",
// Communication // Communication
case .chatFill: return "Chat" "bubble.left.and.bubble.right.fill",
case .messageFill: return "Message" "message.fill",
case .emailFill: return "Email" "envelope.fill",
// Professional // Professional
case .meetingFill: return "Meeting" "person.2.fill",
case .presentationFill: return "Presentation" "person.wave.2.fill",
case .briefcaseFill: return "Briefcase" "briefcase.fill",
// Technical // Technical
case .codeFill: return "Code" "curlybraces",
case .terminalFill: return "Terminal" "terminal.fill",
case .gearFill: return "Settings" "gearshape.fill",
// Content // Content
case .blogFill: return "Blog" "doc.text.image.fill",
case .notesFill: return "Notes" "note",
case .bookFill: return "Book" "book.fill",
case .bookmarkFill: return "Bookmark" "bookmark.fill",
case .pencilFill: return "Edit" "pencil.circle.fill",
// Media & Creative // Media & Creative
case .videoFill: return "Video" "video.fill",
case .micFill: return "Audio" "mic.fill",
case .musicFill: return "Music" "music.note",
case .photoFill: return "Photo" "photo.fill",
case .brushFill: return "Design" "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 { 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,8 +42,8 @@ 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"
), ),
TemplatePrompt( TemplatePrompt(
id: UUID(), id: UUID(),
@ -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,8 +76,8 @@ 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: "Professional email formatting"
), ),
TemplatePrompt( TemplatePrompt(
id: UUID(), id: UUID(),
@ -95,8 +95,8 @@ 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 with better clarity."
) )
] ]
} }

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

@ -0,0 +1,90 @@
import SwiftUI
struct PredefinedPromptsView: View {
let onSelect: (TemplatePrompt) -> Void
private let columns: [GridItem] = Array(repeating: GridItem(.flexible(), spacing: 18), count: 2)
var body: some View {
ScrollView {
LazyVGrid(columns: columns, spacing: 16) {
ForEach(PromptTemplates.all, id: \.title) { template in
PredefinedTemplateButton(prompt: template) {
onSelect(template)
}
}
}
.padding(.horizontal, 24)
.padding(.vertical, 20)
}
.frame(minWidth: 410, idealWidth: 520, maxWidth: 570, maxHeight: 440)
}
}
struct PredefinedTemplateButton: View {
let prompt: TemplatePrompt
let action: () -> Void
var body: some View {
Button(action: action) {
VStack(alignment: .leading, spacing: 12) {
HStack(alignment: .center, spacing: 12) {
RoundedRectangle(cornerRadius: 10, style: .continuous)
.fill(Color(NSColor.unemphasizedSelectedTextBackgroundColor))
.frame(width: 42, height: 42)
.overlay(
Image(systemName: prompt.icon)
.font(.system(size: 19, weight: .medium))
.foregroundColor(Color(NSColor.labelColor))
)
Text(prompt.title)
.font(.system(size: 15, weight: .semibold))
.foregroundColor(.primary)
.lineLimit(1)
Spacer(minLength: 0)
}
Text(prompt.description)
.font(.system(size: 12))
.foregroundColor(Color(NSColor.secondaryLabelColor))
.lineLimit(1)
.truncationMode(.tail)
.frame(maxWidth: .infinity, alignment: .leading)
}
.frame(maxWidth: .infinity, alignment: .topLeading)
.padding(.horizontal, 18)
.padding(.vertical, 12)
.background(cardBackground)
.overlay(cardStroke)
.contentShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
.shadow(color: cardShadowColor, radius: 6, x: 0, y: 4)
}
.buttonStyle(.plain)
}
private var cardBackground: some View {
RoundedRectangle(cornerRadius: 16, style: .continuous)
.fill(Color(NSColor.controlBackgroundColor))
}
private var cardStroke: some View {
RoundedRectangle(cornerRadius: 16, style: .continuous)
.stroke(
LinearGradient(
colors: [
Color(NSColor.separatorColor).opacity(0.35),
Color(NSColor.separatorColor).opacity(0.15)
],
startPoint: .topLeading,
endPoint: .bottomTrailing
),
lineWidth: 1
)
}
private var cardShadowColor: Color {
Color(NSColor.shadowColor).opacity(0.25)
}
}

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) .foregroundColor(.primary)
.frame(width: 24) .frame(width: 48, height: 48)
.background(Color(NSColor.controlBackgroundColor))
Text(selectedIcon.title) .cornerRadius(8)
.foregroundColor(.primary) .overlay(
RoundedRectangle(cornerRadius: 8)
Spacer() .stroke(Color.secondary.opacity(0.2), lineWidth: 1)
)
Image(systemName: "chevron.up.chevron.down")
.font(.system(size: 12))
.foregroundColor(.secondary)
}
.padding(8)
.background(Color(NSColor.controlBackgroundColor))
.cornerRadius(8)
} }
.frame(width: 180) .buttonStyle(.plain)
}
.popover(isPresented: $showingIconPicker, arrowEdge: .bottom) {
IconPickerPopover(selectedIcon: $selectedIcon, isPresented: $showingIconPicker)
} }
} }
.padding(.horizontal) .padding(.horizontal)
@ -218,36 +215,32 @@ struct PromptEditorView: View {
.padding(.horizontal) .padding(.horizontal)
if case .add = mode { if case .add = mode {
// Templates Section with modern styling // Popover keeps templates accessible without taking space in the layout
VStack(alignment: .leading, spacing: 16) { Button("Start with a Predefined Template") {
Text("Start with a Predefined Template") showingPredefinedPrompts.toggle()
.font(.title2) }
.fontWeight(.semibold) .font(.headline)
.foregroundColor(.primary) .padding(.horizontal, 24)
.padding(.vertical, 12)
let columns = [ .background(
GridItem(.flexible(), spacing: 16), Capsule()
GridItem(.flexible(), spacing: 16) .fill(Color(.windowBackgroundColor).opacity(0.9))
] )
.overlay(
LazyVGrid(columns: columns, spacing: 16) { Capsule()
ForEach(PromptTemplates.all) { template in .stroke(Color.secondary.opacity(0.2), lineWidth: 1)
CleanTemplateButton(prompt: template) { )
title = template.title .buttonStyle(.plain)
promptText = template.promptText .padding(.horizontal)
selectedIcon = template.icon .popover(isPresented: $showingPredefinedPrompts, arrowEdge: .bottom) {
description = template.description PredefinedPromptsView { template in
} title = template.title
} promptText = template.promptText
selectedIcon = template.icon
description = template.description
showingPredefinedPrompts = false
} }
} }
.padding(.horizontal)
.padding(.vertical, 16)
.background(
RoundedRectangle(cornerRadius: 16)
.fill(Color(.windowBackgroundColor).opacity(0.6))
)
.padding(.horizontal)
} }
} }
} }
@ -285,90 +278,6 @@ struct PromptEditorView: View {
} }
} }
// Clean template button with minimal styling
struct CleanTemplateButton: View {
let prompt: TemplatePrompt
let action: () -> Void
var body: some View {
Button(action: action) {
HStack(alignment: .top, spacing: 12) {
// Clean icon design
ZStack {
RoundedRectangle(cornerRadius: 12)
.fill(Color.accentColor.opacity(0.15))
.frame(width: 44, height: 44)
Image(systemName: prompt.icon.rawValue)
.font(.system(size: 20, weight: .semibold))
.foregroundColor(.accentColor)
}
VStack(alignment: .leading, spacing: 4) {
Text(prompt.title)
.font(.system(size: 16, weight: .semibold))
.foregroundColor(.primary)
.lineLimit(1)
Text(prompt.description)
.font(.system(size: 13))
.foregroundColor(.secondary)
.lineLimit(2)
.multilineTextAlignment(.leading)
}
Spacer(minLength: 0)
}
.padding(16)
.frame(maxWidth: .infinity, alignment: .leading)
.background(
RoundedRectangle(cornerRadius: 12)
.fill(Color(.controlBackgroundColor))
)
.overlay(
RoundedRectangle(cornerRadius: 12)
.stroke(Color.secondary.opacity(0.2), lineWidth: 1)
)
}
.buttonStyle(.plain)
}
}
// Keep the old TemplateButton for backward compatibility if needed elsewhere
struct TemplateButton: View {
let prompt: TemplatePrompt
let action: () -> Void
var body: some View {
Button(action: action) {
HStack(alignment: .center, spacing: 12) {
Image(systemName: prompt.icon.rawValue)
.font(.system(size: 20, weight: .medium))
.foregroundColor(.accentColor)
.frame(width: 28, height: 28)
.background(Color.accentColor.opacity(0.12))
.clipShape(RoundedRectangle(cornerRadius: 6))
VStack(alignment: .leading, spacing: 4) {
Text(prompt.title)
.font(.system(size: 15, weight: .semibold))
.foregroundColor(.primary)
.lineLimit(1)
}
Spacer(minLength: 0)
}
.padding(12)
.frame(height: 60)
.background(Color(NSColor.controlBackgroundColor))
.cornerRadius(10)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.secondary.opacity(0.18), lineWidth: 1)
)
}
.buttonStyle(.plain)
}
}
// Reusable Trigger Words Editor Component // Reusable Trigger Words Editor Component
struct TriggerWordsEditor: View { struct TriggerWordsEditor: View {
@Binding var triggerWords: [String] @Binding var triggerWords: [String]
@ -425,44 +334,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
@ -503,4 +374,48 @@ struct TriggerWordItemView: View {
.stroke(Color.secondary.opacity(0.2), lineWidth: 1) .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) { 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
) { ) {