Removed separate handling of AI assistant trigger word
This commit is contained in:
parent
7781ecc19b
commit
8a1870934d
@ -108,7 +108,7 @@ struct CustomPrompt: Identifiable, Codable, Equatable {
|
||||
|
||||
// MARK: - UI Extensions
|
||||
extension CustomPrompt {
|
||||
func promptIcon(isSelected: Bool, onTap: @escaping () -> Void, onEdit: ((CustomPrompt) -> Void)? = nil, onDelete: ((CustomPrompt) -> Void)? = nil, assistantTriggerWord: String? = nil) -> some View {
|
||||
func promptIcon(isSelected: Bool, onTap: @escaping () -> Void, onEdit: ((CustomPrompt) -> Void)? = nil, onDelete: ((CustomPrompt) -> Void)? = nil) -> some View {
|
||||
VStack(spacing: 8) {
|
||||
ZStack {
|
||||
// Dynamic background with blur effect
|
||||
@ -206,21 +206,7 @@ extension CustomPrompt {
|
||||
|
||||
// Trigger word section with consistent height
|
||||
ZStack(alignment: .center) {
|
||||
if id == PredefinedPrompts.assistantPromptId, let assistantTriggerWord = assistantTriggerWord, !assistantTriggerWord.isEmpty {
|
||||
// Show the global assistant trigger word for the Assistant Mode
|
||||
HStack(spacing: 2) {
|
||||
Image(systemName: "mic.fill")
|
||||
.font(.system(size: 7))
|
||||
.foregroundColor(isSelected ? .accentColor.opacity(0.9) : .secondary.opacity(0.7))
|
||||
|
||||
Text("\"\(assistantTriggerWord)...\"")
|
||||
.font(.system(size: 8, weight: .regular))
|
||||
.foregroundColor(isSelected ? .primary.opacity(0.8) : .secondary.opacity(0.7))
|
||||
.lineLimit(1)
|
||||
}
|
||||
.frame(maxWidth: 70)
|
||||
} else if let triggerWord = triggerWord, !triggerWord.isEmpty {
|
||||
// Show custom trigger words for Enhancement Modes
|
||||
if let triggerWord = triggerWord, !triggerWord.isEmpty {
|
||||
HStack(spacing: 2) {
|
||||
Image(systemName: "mic.fill")
|
||||
.font(.system(size: 7))
|
||||
@ -234,7 +220,7 @@ extension CustomPrompt {
|
||||
.frame(maxWidth: 70)
|
||||
}
|
||||
}
|
||||
.frame(height: 16) // Fixed height for all modes, with or without trigger words
|
||||
.frame(height: 16)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 4)
|
||||
@ -243,7 +229,7 @@ extension CustomPrompt {
|
||||
.scaleEffect(isSelected ? 1.05 : 1.0)
|
||||
.onTapGesture(perform: onTap)
|
||||
.contextMenu {
|
||||
if !isPredefined && (onEdit != nil || onDelete != nil) {
|
||||
if onEdit != nil || onDelete != nil {
|
||||
if let onEdit = onEdit {
|
||||
Button {
|
||||
onEdit(self)
|
||||
@ -252,7 +238,7 @@ extension CustomPrompt {
|
||||
}
|
||||
}
|
||||
|
||||
if let onDelete = onDelete {
|
||||
if let onDelete = onDelete, !isPredefined {
|
||||
Button(role: .destructive) {
|
||||
onDelete(self)
|
||||
} label: {
|
||||
|
||||
@ -5,7 +5,7 @@ enum PredefinedPrompts {
|
||||
private static let predefinedPromptsKey = "PredefinedPrompts"
|
||||
|
||||
// Static UUIDs for predefined prompts
|
||||
private static let defaultPromptId = UUID(uuidString: "00000000-0000-0000-0000-000000000001")!
|
||||
static let defaultPromptId = UUID(uuidString: "00000000-0000-0000-0000-000000000001")!
|
||||
static let assistantPromptId = UUID(uuidString: "00000000-0000-0000-0000-000000000002")!
|
||||
|
||||
static var all: [CustomPrompt] {
|
||||
|
||||
@ -564,20 +564,20 @@ struct ConfigurationView: View {
|
||||
.foregroundColor(.primary)
|
||||
|
||||
PromptSelectionGrid(
|
||||
prompts: enhancementService.allPrompts,
|
||||
selectedPromptId: selectedPromptId,
|
||||
onPromptTap: { prompt in
|
||||
onPromptSelected: { prompt in
|
||||
selectedPromptId = prompt.id
|
||||
},
|
||||
onPromptEdit: { prompt in
|
||||
onEditPrompt: { prompt in
|
||||
selectedPromptForEdit = prompt
|
||||
},
|
||||
onPromptDelete: { prompt in
|
||||
onDeletePrompt: { prompt in
|
||||
enhancementService.deletePrompt(prompt)
|
||||
},
|
||||
onAddNew: {
|
||||
onAddNewPrompt: {
|
||||
isEditingPrompt = true
|
||||
},
|
||||
assistantTriggerWord: enhancementService.assistantTriggerWord
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -42,15 +42,9 @@ class AIEnhancementService: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
@Published var assistantTriggerWord: String {
|
||||
didSet {
|
||||
UserDefaults.standard.set(assistantTriggerWord, forKey: "assistantTriggerWord")
|
||||
}
|
||||
}
|
||||
|
||||
@Published var customPrompts: [CustomPrompt] {
|
||||
didSet {
|
||||
if let encoded = try? JSONEncoder().encode(customPrompts.filter { !$0.isPredefined }) {
|
||||
if let encoded = try? JSONEncoder().encode(customPrompts) {
|
||||
UserDefaults.standard.set(encoded, forKey: "customPrompts")
|
||||
}
|
||||
}
|
||||
@ -67,7 +61,7 @@ class AIEnhancementService: ObservableObject {
|
||||
}
|
||||
|
||||
var allPrompts: [CustomPrompt] {
|
||||
PredefinedPrompts.createDefaultPrompts() + customPrompts.filter { !$0.isPredefined }
|
||||
return customPrompts
|
||||
}
|
||||
|
||||
private let aiService: AIService
|
||||
@ -87,7 +81,6 @@ class AIEnhancementService: ObservableObject {
|
||||
self.isEnhancementEnabled = UserDefaults.standard.bool(forKey: "isAIEnhancementEnabled")
|
||||
self.useClipboardContext = UserDefaults.standard.bool(forKey: "useClipboardContext")
|
||||
self.useScreenCaptureContext = UserDefaults.standard.bool(forKey: "useScreenCaptureContext")
|
||||
self.assistantTriggerWord = UserDefaults.standard.string(forKey: "assistantTriggerWord") ?? "hey"
|
||||
|
||||
if let savedPromptsData = UserDefaults.standard.data(forKey: "customPrompts"),
|
||||
let decodedPrompts = try? JSONDecoder().decode([CustomPrompt].self, from: savedPromptsData) {
|
||||
@ -110,6 +103,8 @@ class AIEnhancementService: ObservableObject {
|
||||
name: .aiProviderKeyChanged,
|
||||
object: nil
|
||||
)
|
||||
|
||||
initializePredefinedPrompts()
|
||||
}
|
||||
|
||||
deinit {
|
||||
@ -166,20 +161,17 @@ class AIEnhancementService: ObservableObject {
|
||||
""
|
||||
}
|
||||
|
||||
switch mode {
|
||||
case .transcriptionEnhancement:
|
||||
if let activePrompt = activePrompt,
|
||||
activePrompt.id == PredefinedPrompts.assistantPromptId {
|
||||
return AIPrompts.assistantMode + contextSection
|
||||
}
|
||||
|
||||
var systemMessage = String(format: AIPrompts.customPromptTemplate, activePrompt!.promptText)
|
||||
systemMessage += contextSection
|
||||
return systemMessage
|
||||
|
||||
case .aiAssistant:
|
||||
guard let activePrompt = activePrompt else {
|
||||
return AIPrompts.assistantMode + contextSection
|
||||
}
|
||||
|
||||
if activePrompt.id == PredefinedPrompts.assistantPromptId {
|
||||
return activePrompt.promptText + contextSection
|
||||
}
|
||||
|
||||
var systemMessage = String(format: AIPrompts.customPromptTemplate, activePrompt.promptText)
|
||||
systemMessage += contextSection
|
||||
return systemMessage
|
||||
}
|
||||
|
||||
private func makeRequest(text: String, mode: EnhancementPrompt, retryCount: Int = 0) async throws -> String {
|
||||
@ -418,12 +410,7 @@ class AIEnhancementService: ObservableObject {
|
||||
func enhance(_ text: String) async throws -> String {
|
||||
logger.notice("🚀 Starting AI enhancement for text (\(text.count) characters)")
|
||||
|
||||
let enhancementPrompt: EnhancementPrompt = {
|
||||
if let activePrompt = activePrompt, activePrompt.id == PredefinedPrompts.assistantPromptId {
|
||||
return .aiAssistant
|
||||
}
|
||||
return .transcriptionEnhancement
|
||||
}()
|
||||
let enhancementPrompt: EnhancementPrompt = .transcriptionEnhancement
|
||||
|
||||
var retryCount = 0
|
||||
while retryCount < maxRetries {
|
||||
@ -477,16 +464,12 @@ class AIEnhancementService: ObservableObject {
|
||||
}
|
||||
|
||||
func updatePrompt(_ prompt: CustomPrompt) {
|
||||
if prompt.isPredefined { return }
|
||||
|
||||
if let index = customPrompts.firstIndex(where: { $0.id == prompt.id }) {
|
||||
customPrompts[index] = prompt
|
||||
}
|
||||
}
|
||||
|
||||
func deletePrompt(_ prompt: CustomPrompt) {
|
||||
if prompt.isPredefined { return }
|
||||
|
||||
customPrompts.removeAll { $0.id == prompt.id }
|
||||
if selectedPromptId == prompt.id {
|
||||
selectedPromptId = allPrompts.first?.id
|
||||
@ -511,6 +494,31 @@ class AIEnhancementService: ObservableObject {
|
||||
private func getRetryDelay(for retryCount: Int) -> TimeInterval {
|
||||
return retryCount == 1 ? 1.0 : 2.0
|
||||
}
|
||||
|
||||
private func initializePredefinedPrompts() {
|
||||
let predefinedTemplates = PredefinedPrompts.createDefaultPrompts()
|
||||
|
||||
for template in predefinedTemplates {
|
||||
if let existingIndex = customPrompts.firstIndex(where: { $0.id == template.id }) {
|
||||
// Update existing predefined prompt: only update prompt text, preserve trigger word
|
||||
var updatedPrompt = customPrompts[existingIndex]
|
||||
updatedPrompt = CustomPrompt(
|
||||
id: updatedPrompt.id,
|
||||
title: template.title,
|
||||
promptText: template.promptText, // Update from template
|
||||
isActive: updatedPrompt.isActive,
|
||||
icon: template.icon,
|
||||
description: template.description,
|
||||
isPredefined: true,
|
||||
triggerWord: updatedPrompt.triggerWord // Preserve user's trigger word
|
||||
)
|
||||
customPrompts[existingIndex] = updatedPrompt
|
||||
} else {
|
||||
// Add new predefined prompt (no default trigger word)
|
||||
customPrompts.append(template)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum EnhancementError: Error {
|
||||
|
||||
@ -20,21 +20,10 @@ class PromptDetectionService {
|
||||
let originalEnhancementState = enhancementService.isEnhancementEnabled
|
||||
let originalPromptId = enhancementService.selectedPromptId
|
||||
|
||||
if let result = checkAssistantTrigger(text: text, triggerWord: enhancementService.assistantTriggerWord) {
|
||||
return PromptDetectionResult(
|
||||
shouldEnableAI: true,
|
||||
selectedPromptId: PredefinedPrompts.assistantPromptId,
|
||||
processedText: result,
|
||||
detectedTriggerWord: enhancementService.assistantTriggerWord,
|
||||
originalEnhancementState: originalEnhancementState,
|
||||
originalPromptId: originalPromptId
|
||||
)
|
||||
}
|
||||
|
||||
for prompt in enhancementService.allPrompts {
|
||||
if let triggerWord = prompt.triggerWord?.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
!triggerWord.isEmpty,
|
||||
let result = checkCustomTrigger(text: text, triggerWord: triggerWord) {
|
||||
let result = removeTriggerWord(from: text, triggerWord: triggerWord) {
|
||||
|
||||
return PromptDetectionResult(
|
||||
shouldEnableAI: true,
|
||||
@ -87,14 +76,6 @@ class PromptDetectionService {
|
||||
}
|
||||
}
|
||||
|
||||
private func checkAssistantTrigger(text: String, triggerWord: String) -> String? {
|
||||
return removeTriggerWord(from: text, triggerWord: triggerWord)
|
||||
}
|
||||
|
||||
private func checkCustomTrigger(text: String, triggerWord: String) -> String? {
|
||||
return removeTriggerWord(from: text, triggerWord: triggerWord)
|
||||
}
|
||||
|
||||
private func removeTriggerWord(from text: String, triggerWord: String) -> String? {
|
||||
let trimmedText = text.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let lowerText = trimmedText.lowercased()
|
||||
|
||||
@ -4,32 +4,63 @@ import SwiftUI
|
||||
struct PromptSelectionGrid: View {
|
||||
@EnvironmentObject private var enhancementService: AIEnhancementService
|
||||
|
||||
let prompts: [CustomPrompt]
|
||||
let selectedPromptId: UUID?
|
||||
let onPromptTap: (CustomPrompt) -> Void
|
||||
let onPromptEdit: (CustomPrompt) -> Void
|
||||
let onPromptDelete: (CustomPrompt) -> Void
|
||||
let onAddNew: () -> Void
|
||||
let assistantTriggerWord: String?
|
||||
let onPromptSelected: (CustomPrompt) -> Void
|
||||
let onEditPrompt: ((CustomPrompt) -> Void)?
|
||||
let onDeletePrompt: ((CustomPrompt) -> Void)?
|
||||
let onAddNewPrompt: (() -> Void)?
|
||||
|
||||
init(
|
||||
prompts: [CustomPrompt],
|
||||
selectedPromptId: UUID?,
|
||||
onPromptTap: @escaping (CustomPrompt) -> Void,
|
||||
onPromptEdit: @escaping (CustomPrompt) -> Void = { _ in },
|
||||
onPromptDelete: @escaping (CustomPrompt) -> Void = { _ in },
|
||||
onAddNew: @escaping () -> Void,
|
||||
assistantTriggerWord: String? = nil
|
||||
onPromptSelected: @escaping (CustomPrompt) -> Void,
|
||||
onEditPrompt: ((CustomPrompt) -> Void)? = nil,
|
||||
onDeletePrompt: ((CustomPrompt) -> Void)? = nil,
|
||||
onAddNewPrompt: (() -> Void)? = nil
|
||||
) {
|
||||
self.prompts = prompts
|
||||
self.selectedPromptId = selectedPromptId
|
||||
self.onPromptTap = onPromptTap
|
||||
self.onPromptEdit = onPromptEdit
|
||||
self.onPromptDelete = onPromptDelete
|
||||
self.onAddNew = onAddNew
|
||||
self.assistantTriggerWord = assistantTriggerWord
|
||||
self.onPromptSelected = onPromptSelected
|
||||
self.onEditPrompt = onEditPrompt
|
||||
self.onDeletePrompt = onDeletePrompt
|
||||
self.onAddNewPrompt = onAddNewPrompt
|
||||
}
|
||||
|
||||
private var sortedPrompts: [CustomPrompt] {
|
||||
prompts.sorted { prompt1, prompt2 in
|
||||
// Predefined prompts come first
|
||||
if prompt1.isPredefined && !prompt2.isPredefined {
|
||||
return true
|
||||
}
|
||||
if !prompt1.isPredefined && prompt2.isPredefined {
|
||||
return false
|
||||
}
|
||||
|
||||
// Among predefined prompts: Default first, then Assistant
|
||||
if prompt1.isPredefined && prompt2.isPredefined {
|
||||
if prompt1.id == PredefinedPrompts.defaultPromptId {
|
||||
return true
|
||||
}
|
||||
if prompt2.id == PredefinedPrompts.defaultPromptId {
|
||||
return false
|
||||
}
|
||||
if prompt1.id == PredefinedPrompts.assistantPromptId {
|
||||
return true
|
||||
}
|
||||
if prompt2.id == PredefinedPrompts.assistantPromptId {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Custom prompts: sort alphabetically by title
|
||||
return prompt1.title.localizedCaseInsensitiveCompare(prompt2.title) == .orderedAscending
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
if enhancementService.allPrompts.isEmpty {
|
||||
if sortedPrompts.isEmpty {
|
||||
Text("No prompts available")
|
||||
.foregroundColor(.secondary)
|
||||
.font(.caption)
|
||||
@ -38,29 +69,42 @@ struct PromptSelectionGrid: View {
|
||||
GridItem(.adaptive(minimum: 80, maximum: 100), spacing: 36)
|
||||
]
|
||||
|
||||
LazyVGrid(columns: columns, spacing: 24) {
|
||||
ForEach(enhancementService.allPrompts) { prompt in
|
||||
LazyVGrid(columns: columns, spacing: 16) {
|
||||
ForEach(sortedPrompts) { prompt in
|
||||
prompt.promptIcon(
|
||||
isSelected: selectedPromptId == prompt.id,
|
||||
onTap: {
|
||||
withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) {
|
||||
onPromptTap(prompt)
|
||||
onPromptSelected(prompt)
|
||||
}
|
||||
},
|
||||
onEdit: onPromptEdit,
|
||||
onDelete: onPromptDelete,
|
||||
assistantTriggerWord: assistantTriggerWord
|
||||
onEdit: onEditPrompt,
|
||||
onDelete: onDeletePrompt
|
||||
)
|
||||
}
|
||||
|
||||
// Plus icon using the same styling as prompt icons
|
||||
CustomPrompt.addNewButton {
|
||||
onAddNew()
|
||||
if let onAddNewPrompt = onAddNewPrompt {
|
||||
CustomPrompt.addNewButton {
|
||||
onAddNewPrompt()
|
||||
}
|
||||
.help("Add new prompt")
|
||||
}
|
||||
.help("Add new prompt")
|
||||
}
|
||||
.padding(.vertical, 12)
|
||||
.padding(.horizontal, 16)
|
||||
|
||||
// Helpful tip for users
|
||||
HStack {
|
||||
Image(systemName: "info.circle")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
Text("Right-click on prompts to edit or delete")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.padding(.top, 8)
|
||||
.padding(.horizontal, 16)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,8 +5,6 @@ struct EnhancementSettingsView: View {
|
||||
@State private var isEditingPrompt = false
|
||||
@State private var isSettingsExpanded = true
|
||||
@State private var selectedPromptForEdit: CustomPrompt?
|
||||
@State private var isEditingTriggerWord = false
|
||||
@State private var tempTriggerWord = ""
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
@ -92,88 +90,22 @@ struct EnhancementSettingsView: View {
|
||||
// Prompts Section
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
PromptSelectionGrid(
|
||||
prompts: enhancementService.allPrompts,
|
||||
selectedPromptId: enhancementService.selectedPromptId,
|
||||
onPromptTap: { prompt in
|
||||
onPromptSelected: { prompt in
|
||||
enhancementService.setActivePrompt(prompt)
|
||||
},
|
||||
onPromptEdit: { prompt in
|
||||
onEditPrompt: { prompt in
|
||||
selectedPromptForEdit = prompt
|
||||
},
|
||||
onPromptDelete: { prompt in
|
||||
onDeletePrompt: { prompt in
|
||||
enhancementService.deletePrompt(prompt)
|
||||
},
|
||||
onAddNew: {
|
||||
onAddNewPrompt: {
|
||||
isEditingPrompt = true
|
||||
},
|
||||
assistantTriggerWord: enhancementService.assistantTriggerWord
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
// Assistant Mode Section
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
HStack {
|
||||
Text("Assistant Prompt")
|
||||
.font(.subheadline)
|
||||
Image(systemName: "sparkles")
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
|
||||
Text("Configure how to trigger the AI assistant mode")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
HStack {
|
||||
Text("Current Trigger:")
|
||||
.font(.subheadline)
|
||||
Text("\"\(enhancementService.assistantTriggerWord)\"")
|
||||
.font(.system(.subheadline, design: .monospaced))
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
|
||||
if isEditingTriggerWord {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
HStack {
|
||||
TextField("New trigger word", text: $tempTriggerWord)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.frame(maxWidth: 200)
|
||||
|
||||
Button("Save") {
|
||||
enhancementService.assistantTriggerWord = tempTriggerWord
|
||||
isEditingTriggerWord = false
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.disabled(tempTriggerWord.isEmpty)
|
||||
|
||||
Button("Cancel") {
|
||||
isEditingTriggerWord = false
|
||||
tempTriggerWord = enhancementService.assistantTriggerWord
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
}
|
||||
|
||||
Text("Default: \"hey\"")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
} else {
|
||||
Button("Change Trigger Word") {
|
||||
tempTriggerWord = enhancementService.assistantTriggerWord
|
||||
isEditingTriggerWord = true
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
}
|
||||
}
|
||||
|
||||
Text("Start with \"\(enhancementService.assistantTriggerWord), \" to use AI assistant mode")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
Text("Instead of enhancing the text, VoiceInk will respond like a conversational AI assistant")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(Color(.windowBackgroundColor).opacity(0.4))
|
||||
|
||||
@ -27,6 +27,13 @@ struct PromptEditorView: View {
|
||||
@State private var triggerWord: String
|
||||
@State private var showingPredefinedPrompts = false
|
||||
|
||||
private var isEditingPredefinedPrompt: Bool {
|
||||
if case .edit(let prompt) = mode {
|
||||
return prompt.isPredefined
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
init(mode: Mode) {
|
||||
self.mode = mode
|
||||
switch mode {
|
||||
@ -49,7 +56,7 @@ struct PromptEditorView: View {
|
||||
VStack(spacing: 0) {
|
||||
// Header with modern styling
|
||||
HStack {
|
||||
Text(mode == .add ? "New Prompt" : "Edit Prompt")
|
||||
Text(isEditingPredefinedPrompt ? "Edit Trigger Word" : (mode == .add ? "New Prompt" : "Edit Prompt"))
|
||||
.font(.title2)
|
||||
.fontWeight(.bold)
|
||||
Spacer()
|
||||
@ -68,7 +75,7 @@ struct PromptEditorView: View {
|
||||
.fontWeight(.medium)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.disabled(title.isEmpty || promptText.isEmpty)
|
||||
.disabled(isEditingPredefinedPrompt ? false : (title.isEmpty || promptText.isEmpty))
|
||||
.keyboardShortcut(.return, modifiers: .command)
|
||||
}
|
||||
}
|
||||
@ -80,141 +87,157 @@ struct PromptEditorView: View {
|
||||
|
||||
ScrollView {
|
||||
VStack(spacing: 24) {
|
||||
// Title and Icon Section with improved layout
|
||||
HStack(spacing: 20) {
|
||||
// Title Field
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Title")
|
||||
.font(.headline)
|
||||
.foregroundColor(.secondary)
|
||||
TextField("Enter a short, descriptive title", text: $title)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.font(.body)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
|
||||
// Icon Selector with preview
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Icon")
|
||||
.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)
|
||||
}
|
||||
.frame(width: 180)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.padding(.top, 8)
|
||||
|
||||
// Description Field
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Description")
|
||||
.font(.headline)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
Text("Add a brief description of what this prompt does")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
TextField("Enter a description", text: $description)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.font(.body)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
|
||||
// Trigger Word Field
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Trigger Word")
|
||||
.font(.headline)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
Text("Add a custom word to activate this prompt by voice (optional)")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
TextField("Enter a trigger word", text: $triggerWord)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.font(.body)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
|
||||
// Prompt Text Section with improved styling
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Prompt Instructions")
|
||||
.font(.headline)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
Text("Define how AI should enhance your transcriptions")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
TextEditor(text: $promptText)
|
||||
.font(.system(.body, design: .monospaced))
|
||||
.frame(minHeight: 200)
|
||||
.padding(12)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.fill(Color(NSColor.textBackgroundColor))
|
||||
)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.stroke(Color.secondary.opacity(0.2), lineWidth: 1)
|
||||
)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
|
||||
if case .add = mode {
|
||||
// Templates Section with modern styling
|
||||
if isEditingPredefinedPrompt {
|
||||
// Simplified view for predefined prompts - only trigger word editing
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
Text("Start with a Predefined Template")
|
||||
Text("Editing: \(title)")
|
||||
.font(.title2)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(.primary)
|
||||
.padding(.horizontal)
|
||||
.padding(.top, 8)
|
||||
|
||||
let columns = [
|
||||
GridItem(.flexible(), spacing: 16),
|
||||
GridItem(.flexible(), spacing: 16)
|
||||
]
|
||||
Text("You can only customize the trigger word for system prompts.")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
.padding(.horizontal)
|
||||
|
||||
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
|
||||
// Trigger Word Field with same styling as custom prompts
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Trigger Word")
|
||||
.font(.headline)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
TextField("Enter a trigger word (optional)", text: $triggerWord)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.font(.body)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
}
|
||||
.padding(.vertical, 20)
|
||||
|
||||
} else {
|
||||
// Full editing interface for custom prompts
|
||||
// Title and Icon Section with improved layout
|
||||
HStack(spacing: 20) {
|
||||
// Title Field
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Title")
|
||||
.font(.headline)
|
||||
.foregroundColor(.secondary)
|
||||
TextField("Enter a short, descriptive title", text: $title)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.font(.body)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
|
||||
// Icon Selector with preview
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Icon")
|
||||
.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)
|
||||
}
|
||||
.frame(width: 180)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.padding(.vertical, 16)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 16)
|
||||
.fill(Color(.windowBackgroundColor).opacity(0.6))
|
||||
)
|
||||
.padding(.top, 8)
|
||||
|
||||
// Description Field
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Description")
|
||||
.font(.headline)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
Text("Add a brief description of what this prompt does")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
TextField("Enter a description", text: $description)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.font(.body)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
|
||||
// Prompt Text Section with improved styling
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Prompt Instructions")
|
||||
.font(.headline)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
Text("Define how AI should enhance your transcriptions")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
TextEditor(text: $promptText)
|
||||
.font(.system(.body, design: .monospaced))
|
||||
.frame(minHeight: 200)
|
||||
.padding(12)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.fill(Color(NSColor.textBackgroundColor))
|
||||
)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.stroke(Color.secondary.opacity(0.2), lineWidth: 1)
|
||||
)
|
||||
}
|
||||
.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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.padding(.vertical, 16)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 16)
|
||||
.fill(Color(.windowBackgroundColor).opacity(0.6))
|
||||
)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 20)
|
||||
@ -236,11 +259,12 @@ struct PromptEditorView: View {
|
||||
case .edit(let prompt):
|
||||
let updatedPrompt = CustomPrompt(
|
||||
id: prompt.id,
|
||||
title: title,
|
||||
promptText: promptText,
|
||||
title: prompt.isPredefined ? prompt.title : title,
|
||||
promptText: prompt.isPredefined ? prompt.promptText : promptText,
|
||||
isActive: prompt.isActive,
|
||||
icon: selectedIcon,
|
||||
description: description.isEmpty ? nil : description,
|
||||
icon: prompt.isPredefined ? prompt.icon : selectedIcon,
|
||||
description: prompt.isPredefined ? prompt.description : (description.isEmpty ? nil : description),
|
||||
isPredefined: prompt.isPredefined,
|
||||
triggerWord: triggerWord.isEmpty ? nil : triggerWord
|
||||
)
|
||||
enhancementService.updatePrompt(updatedPrompt)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user