Skip to content

Commit 21dd338

Browse files
Merge pull request #5 from dennisimoo/custom-prompts
Add custom prompt template functionality
2 parents 16306f2 + f6535d6 commit 21dd338

6 files changed

Lines changed: 187 additions & 15 deletions

File tree

Recap/UIComponents/Buttons/PillButton.swift

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,3 @@
1-
//
2-
// PillButton.swift
3-
// Recap
4-
//
5-
// Created by Rawand Ahmad on 25/07/2025.
6-
//
7-
81
import SwiftUI
92

103
struct PillButton: View {
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import SwiftUI
2+
3+
struct CustomTextEditor: View {
4+
let title: String
5+
let textBinding: Binding<String>
6+
let placeholder: String
7+
let height: CGFloat
8+
9+
@State private var isEditing = false
10+
@FocusState private var isFocused: Bool
11+
12+
init(
13+
title: String,
14+
text: Binding<String>,
15+
placeholder: String = "",
16+
height: CGFloat = 100
17+
) {
18+
self.title = title
19+
self.textBinding = text
20+
self.placeholder = placeholder
21+
self.height = height
22+
}
23+
24+
var body: some View {
25+
VStack(alignment: .leading, spacing: 8) {
26+
Text(title)
27+
.font(.system(size: 11, weight: .medium))
28+
.foregroundColor(UIConstants.Colors.textSecondary)
29+
30+
ZStack(alignment: .topLeading) {
31+
RoundedRectangle(cornerRadius: 8)
32+
.fill(Color(hex: "2A2A2A").opacity(0.3))
33+
.overlay(
34+
RoundedRectangle(cornerRadius: 8)
35+
.stroke(
36+
LinearGradient(
37+
gradient: Gradient(stops: [
38+
.init(color: Color(hex: "979797").opacity(isFocused ? 0.4 : 0.2), location: 0),
39+
.init(color: Color(hex: "979797").opacity(isFocused ? 0.3 : 0.1), location: 1)
40+
]),
41+
startPoint: .top,
42+
endPoint: .bottom
43+
),
44+
lineWidth: 0.8
45+
)
46+
)
47+
.frame(height: height)
48+
49+
if textBinding.wrappedValue.isEmpty && !isFocused {
50+
Text(placeholder)
51+
.font(.system(size: 12, weight: .medium))
52+
.foregroundColor(UIConstants.Colors.textSecondary.opacity(0.6))
53+
.padding(.horizontal, 12)
54+
.padding(.vertical, 8)
55+
.allowsHitTesting(false)
56+
}
57+
58+
TextEditor(text: textBinding)
59+
.font(.system(size: 12, weight: .medium))
60+
.foregroundColor(UIConstants.Colors.textPrimary)
61+
.background(Color.clear)
62+
.scrollContentBackground(.hidden)
63+
.padding(.horizontal, 8)
64+
.padding(.vertical, 4)
65+
.focused($isFocused)
66+
.lineLimit(nil)
67+
.textSelection(.enabled)
68+
.onChange(of: isFocused) { _, focused in
69+
withAnimation(.easeInOut(duration: 0.2)) {
70+
isEditing = focused
71+
}
72+
}
73+
}
74+
}
75+
}
76+
}
77+
78+
#Preview {
79+
VStack(spacing: 20) {
80+
CustomTextEditor(
81+
title: "Custom Prompt",
82+
text: .constant(""),
83+
placeholder: "Enter your custom prompt template here...",
84+
height: 120
85+
)
86+
87+
CustomTextEditor(
88+
title: "With Content",
89+
text: .constant(UserPreferencesInfo.defaultPromptTemplate),
90+
placeholder: "Enter text...",
91+
height: 80
92+
)
93+
}
94+
.frame(width: 400, height: 300)
95+
.padding(20)
96+
.background(Color.black)
97+
}

Recap/UseCases/Settings/Components/TabViews/GeneralSettingsView.swift

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,31 @@ struct GeneralSettingsView<ViewModel: GeneralSettingsViewModelType>: View {
8686
}
8787
}
8888

89+
SettingsCard(title: "Custom Prompt") {
90+
VStack(alignment: .leading, spacing: 12) {
91+
CustomTextEditor(
92+
title: "Prompt Template",
93+
text: viewModel.customPromptTemplate,
94+
placeholder: "Enter your custom prompt template here...",
95+
height: 120
96+
)
97+
98+
HStack {
99+
Text("Customize how AI summarizes your meeting transcripts")
100+
.font(.system(size: 11, weight: .regular))
101+
.foregroundColor(UIConstants.Colors.textSecondary)
102+
103+
Spacer()
104+
105+
PillButton(text: "Reset to Default") {
106+
Task {
107+
await viewModel.resetToDefaultPrompt()
108+
}
109+
}
110+
}
111+
}
112+
}
113+
89114
}
90115
.padding(.horizontal, 20)
91116
.padding(.vertical, 20)
@@ -121,12 +146,15 @@ struct GeneralSettingsView<ViewModel: GeneralSettingsViewModelType>: View {
121146
}
122147

123148
#Preview {
124-
GeneralSettingsView<PreviewGeneralSettingsViewModel>(viewModel: PreviewGeneralSettingsViewModel())
149+
GeneralSettingsView(viewModel: PreviewGeneralSettingsViewModel())
125150
.frame(width: 550, height: 500)
126151
.background(Color.black)
127152
}
128153

129-
private final class PreviewGeneralSettingsViewModel: ObservableObject, GeneralSettingsViewModelType {
154+
private final class PreviewGeneralSettingsViewModel: GeneralSettingsViewModelType {
155+
var customPromptTemplate: Binding<String> {
156+
.constant(UserPreferencesInfo.defaultPromptTemplate)
157+
}
130158
@Published var availableModels: [LLMModelInfo] = [
131159
LLMModelInfo(name: "llama3.2", provider: "ollama"),
132160
LLMModelInfo(name: "codellama", provider: "ollama")
@@ -170,4 +198,8 @@ private final class PreviewGeneralSettingsViewModel: ObservableObject, GeneralSe
170198
func toggleAutoStopRecording(_ enabled: Bool) async {
171199
isAutoStopRecording = enabled
172200
}
201+
202+
func updateCustomPromptTemplate(_ template: String) async {}
203+
204+
func resetToDefaultPrompt() async {}
173205
}

Recap/UseCases/Settings/SettingsView.swift

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -150,9 +150,11 @@ struct SettingsView<GeneralViewModel: GeneralSettingsViewModelType>: View {
150150
.frame(width: 550, height: 500)
151151
}
152152

153-
private final class PreviewGeneralSettingsViewModel: ObservableObject, GeneralSettingsViewModelType {
154-
var activeWarnings: [WarningItem] = []
155-
153+
154+
private final class PreviewGeneralSettingsViewModel: GeneralSettingsViewModelType {
155+
var customPromptTemplate: Binding<String> {
156+
.constant(UserPreferencesInfo.defaultPromptTemplate)
157+
}
156158
@Published var availableModels: [LLMModelInfo] = [
157159
LLMModelInfo(name: "llama3.2", provider: "ollama"),
158160
LLMModelInfo(name: "codellama", provider: "ollama")
@@ -165,6 +167,15 @@ private final class PreviewGeneralSettingsViewModel: ObservableObject, GeneralSe
165167
@Published var errorMessage: String?
166168
@Published var showToast = false
167169
@Published var toastMessage = ""
170+
@Published var activeWarnings: [WarningItem] = [
171+
WarningItem(
172+
id: "ollama",
173+
title: "Ollama Not Running",
174+
message: "Please start Ollama to use local AI models for summarization.",
175+
icon: "server.rack",
176+
severity: .warning
177+
)
178+
]
168179

169180
var hasModels: Bool {
170181
!availableModels.isEmpty
@@ -187,4 +198,8 @@ private final class PreviewGeneralSettingsViewModel: ObservableObject, GeneralSe
187198
func toggleAutoStopRecording(_ enabled: Bool) async {
188199
isAutoStopRecording = enabled
189200
}
201+
202+
func updateCustomPromptTemplate(_ template: String) async {}
203+
204+
func resetToDefaultPrompt() async {}
190205
}

Recap/UseCases/Settings/ViewModels/General/GeneralSettingsViewModel.swift

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,27 @@
11
import Foundation
22
import Combine
3+
import SwiftUI
34

45
@MainActor
5-
final class GeneralSettingsViewModel: ObservableObject, GeneralSettingsViewModelType {
6+
final class GeneralSettingsViewModel: GeneralSettingsViewModelType {
67
@Published private(set) var availableModels: [LLMModelInfo] = []
78
@Published private(set) var selectedModel: LLMModelInfo?
89
@Published private(set) var selectedProvider: LLMProvider = .default
910
@Published private(set) var autoDetectMeetings: Bool = false
1011
@Published private(set) var isAutoStopRecording: Bool = false
12+
@Published private var customPromptTemplateValue: String = ""
13+
14+
var customPromptTemplate: Binding<String> {
15+
Binding(
16+
get: { self.customPromptTemplateValue },
17+
set: { newValue in
18+
Task {
19+
await self.updateCustomPromptTemplate(newValue)
20+
}
21+
}
22+
)
23+
}
24+
1125
@Published private(set) var isLoading = false
1226
@Published private(set) var errorMessage: String?
1327
@Published private(set) var showToast = false
@@ -31,7 +45,7 @@ final class GeneralSettingsViewModel: ObservableObject, GeneralSettingsViewModel
3145
init(
3246
llmService: LLMServiceType,
3347
userPreferencesRepository: UserPreferencesRepositoryType,
34-
environmentValidator: EnvironmentValidatorType = EnvironmentValidator(),
48+
environmentValidator: EnvironmentValidatorType,
3549
warningManager: WarningManagerType
3650
) {
3751
self.llmService = llmService
@@ -58,10 +72,12 @@ final class GeneralSettingsViewModel: ObservableObject, GeneralSettingsViewModel
5872
selectedProvider = preferences.selectedProvider
5973
autoDetectMeetings = preferences.autoDetectMeetings
6074
isAutoStopRecording = preferences.autoStopRecording
75+
customPromptTemplateValue = preferences.summaryPromptTemplate ?? UserPreferencesInfo.defaultPromptTemplate
6176
} catch {
6277
selectedProvider = .default
6378
autoDetectMeetings = false
6479
isAutoStopRecording = false
80+
customPromptTemplateValue = UserPreferencesInfo.defaultPromptTemplate
6581
}
6682
await loadModels()
6783
}
@@ -167,4 +183,19 @@ final class GeneralSettingsViewModel: ObservableObject, GeneralSettingsViewModel
167183
isAutoStopRecording = !enabled
168184
}
169185
}
170-
}
186+
187+
func updateCustomPromptTemplate(_ template: String) async {
188+
customPromptTemplateValue = template
189+
190+
do {
191+
let templateToSave = template.isEmpty ? nil : template
192+
try await userPreferencesRepository.updateSummaryPromptTemplate(templateToSave)
193+
} catch {
194+
errorMessage = error.localizedDescription
195+
}
196+
}
197+
198+
func resetToDefaultPrompt() async {
199+
await updateCustomPromptTemplate(UserPreferencesInfo.defaultPromptTemplate)
200+
}
201+
}

Recap/UseCases/Settings/ViewModels/General/GeneralSettingsViewModelType.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import Foundation
22
import Combine
3+
import SwiftUI
34

45
@MainActor
56
protocol GeneralSettingsViewModelType: ObservableObject {
@@ -15,10 +16,13 @@ protocol GeneralSettingsViewModelType: ObservableObject {
1516
var showToast: Bool { get }
1617
var toastMessage: String { get }
1718
var activeWarnings: [WarningItem] { get }
19+
var customPromptTemplate: Binding<String> { get }
1820

1921
func loadModels() async
2022
func selectModel(_ model: LLMModelInfo) async
2123
func selectProvider(_ provider: LLMProvider) async
2224
func toggleAutoDetectMeetings(_ enabled: Bool) async
2325
func toggleAutoStopRecording(_ enabled: Bool) async
26+
func updateCustomPromptTemplate(_ template: String) async
27+
func resetToDefaultPrompt() async
2428
}

0 commit comments

Comments
 (0)