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 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,8 +42,8 @@ enum PromptTemplates {
- Output only the cleaned text.
- Don't add any information not available in the <TRANSCRIPT> text ever.
""",
icon: .sealedFill,
description: "Default system prompt for improving clarity and accuracy of transcriptions"
icon: "checkmark.seal.fill",
description: "Default system prompt"
),
TemplatePrompt(
id: UUID(),
@ -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,8 +76,8 @@ 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,
description: "Template for converting casual messages into professional email format"
icon: "envelope.fill",
description: "Professional email formatting"
),
TemplatePrompt(
id: UUID(),
@ -95,8 +95,8 @@ enum PromptTemplates {
- Output only the rewritten text.
- Don't add any information not available in the <TRANSCRIPT> text ever.
""",
icon: .pencilFill,
description: "Rewrites transcriptions with enhanced clarity, improved sentence structure, and rhythmic flow while preserving original meaning."
icon: "pencil.circle.fill",
description: "Rewrites with better clarity."
)
]
}

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

@ -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 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)
@ -218,36 +215,32 @@ struct PromptEditorView: View {
.padding(.horizontal)
if case .add = mode {
// Templates Section with modern styling
VStack(alignment: .leading, spacing: 16) {
Text("Start with a Predefined Template")
.font(.title2)
.fontWeight(.semibold)
.foregroundColor(.primary)
let columns = [
GridItem(.flexible(), spacing: 16),
GridItem(.flexible(), spacing: 16)
]
LazyVGrid(columns: columns, spacing: 16) {
ForEach(PromptTemplates.all) { template in
CleanTemplateButton(prompt: template) {
title = template.title
promptText = template.promptText
selectedIcon = template.icon
description = template.description
}
}
// Popover keeps templates accessible without taking space in the layout
Button("Start with a Predefined Template") {
showingPredefinedPrompts.toggle()
}
.font(.headline)
.padding(.horizontal, 24)
.padding(.vertical, 12)
.background(
Capsule()
.fill(Color(.windowBackgroundColor).opacity(0.9))
)
.overlay(
Capsule()
.stroke(Color.secondary.opacity(0.2), lineWidth: 1)
)
.buttonStyle(.plain)
.padding(.horizontal)
.popover(isPresented: $showingPredefinedPrompts, arrowEdge: .bottom) {
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
struct TriggerWordsEditor: View {
@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 {
let word: String
@ -503,4 +374,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
) {