Skip to content

Commit 7050a2e

Browse files
committed
Add chat model and embedding model settings view
1 parent 012d188 commit 7050a2e

24 files changed

+1799
-557
lines changed
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import ComposableArchitecture
2+
import SwiftUI
3+
4+
struct APIKeyManagementView: View {
5+
let store: StoreOf<APIKeyManagement>
6+
7+
var body: some View {
8+
VStack(spacing: 0) {
9+
HStack {
10+
Button(action: {
11+
store.send(.closeButtonClicked)
12+
}) {
13+
Image(systemName: "xmark.circle.fill")
14+
.foregroundStyle(.secondary)
15+
.padding()
16+
}
17+
.buttonStyle(.plain)
18+
Text("API Keys")
19+
Spacer()
20+
Button(action: {
21+
store.send(.addButtonClicked)
22+
}) {
23+
Image(systemName: "plus.circle.fill")
24+
.foregroundStyle(.secondary)
25+
.padding()
26+
}
27+
.buttonStyle(.plain)
28+
}
29+
.background(Color(nsColor: .separatorColor))
30+
31+
List {
32+
WithViewStore(store, observe: { $0.availableAPIKeyNames }) { viewStore in
33+
ForEach(viewStore.state, id: \.self) { name in
34+
HStack {
35+
Text(name)
36+
.contextMenu {
37+
Button("Remove") {
38+
viewStore.send(.deleteButtonClicked(name: name))
39+
}
40+
}
41+
Spacer()
42+
43+
Button(action: {
44+
viewStore.send(.deleteButtonClicked(name: name))
45+
}) {
46+
Image(systemName: "trash.fill")
47+
.foregroundStyle(.secondary)
48+
}
49+
.buttonStyle(.plain)
50+
}
51+
}
52+
}
53+
}
54+
.removeBackground()
55+
.overlay {
56+
WithViewStore(store, observe: { $0.availableAPIKeyNames }) { viewStore in
57+
if viewStore.state.isEmpty {
58+
Text("""
59+
Empty
60+
Add a new key by clicking the add button
61+
""")
62+
.multilineTextAlignment(.center)
63+
.padding()
64+
}
65+
}
66+
}
67+
}
68+
.focusable(false)
69+
.frame(width: 300, height: 400)
70+
.background(.thickMaterial)
71+
.onAppear {
72+
store.send(.appear)
73+
}
74+
.sheet(store: store.scope(
75+
state: \.$apiKeySubmission,
76+
action: APIKeyManagement.Action.apiKeySubmission
77+
)) { store in
78+
APIKeySubmissionView(store: store)
79+
.frame(minWidth: 400)
80+
}
81+
}
82+
}
83+
84+
struct APIKeySubmissionView: View {
85+
let store: StoreOf<APIKeySubmission>
86+
87+
var body: some View {
88+
ScrollView {
89+
VStack(spacing: 0) {
90+
Form {
91+
WithViewStore(store, removeDuplicates: { $0.name == $1.name }) { viewStore in
92+
TextField("Name", text: viewStore.$name)
93+
}
94+
WithViewStore(store, removeDuplicates: { $0.key == $1.key }) { viewStore in
95+
SecureField("Key", text: viewStore.$key)
96+
}
97+
}.padding()
98+
99+
Divider()
100+
101+
HStack {
102+
Spacer()
103+
104+
Button("Cancel") { store.send(.cancelButtonClicked) }
105+
.keyboardShortcut(.cancelAction)
106+
107+
Button("Save", action: { store.send(.saveButtonClicked) })
108+
.keyboardShortcut(.defaultAction)
109+
}.padding()
110+
}
111+
}
112+
.textFieldStyle(.roundedBorder)
113+
}
114+
}
115+
116+
class APIKeyManagementView_Preview: PreviewProvider {
117+
static var previews: some View {
118+
APIKeyManagementView(
119+
store: .init(
120+
initialState: .init(
121+
availableAPIKeyNames: ["test1", "test2"]
122+
),
123+
reducer: APIKeyManagement()
124+
)
125+
)
126+
}
127+
}
128+
129+
class APIKeySubmissionView_Preview: PreviewProvider {
130+
static var previews: some View {
131+
APIKeySubmissionView(
132+
store: .init(
133+
initialState: .init(),
134+
reducer: APIKeySubmission()
135+
)
136+
)
137+
}
138+
}
139+
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import ComposableArchitecture
2+
import Foundation
3+
4+
struct APIKeyManagement: ReducerProtocol {
5+
struct State: Equatable {
6+
var availableAPIKeyNames: [String] = []
7+
@PresentationState var apiKeySubmission: APIKeySubmission.State?
8+
}
9+
10+
enum Action: Equatable {
11+
case appear
12+
case closeButtonClicked
13+
case addButtonClicked
14+
case deleteButtonClicked(name: String)
15+
case refreshAvailableAPIKeyNames
16+
17+
case apiKeySubmission(PresentationAction<APIKeySubmission.Action>)
18+
}
19+
20+
@Dependency(\.toast) var toast
21+
@Dependency(\.apiKeyKeychain) var keychain
22+
23+
var body: some ReducerProtocol<State, Action> {
24+
Reduce { state, action in
25+
switch action {
26+
case .appear:
27+
if isPreview { return .none }
28+
29+
return .run { send in
30+
await send(.refreshAvailableAPIKeyNames)
31+
}
32+
case .closeButtonClicked:
33+
return .none
34+
35+
case .addButtonClicked:
36+
state.apiKeySubmission = .init()
37+
38+
return .none
39+
40+
case let .deleteButtonClicked(name):
41+
do {
42+
try keychain.remove(name)
43+
return .run { send in
44+
await send(.refreshAvailableAPIKeyNames)
45+
}
46+
} catch {
47+
toast(error.localizedDescription, .error)
48+
return .none
49+
}
50+
51+
case .refreshAvailableAPIKeyNames:
52+
do {
53+
let pairs = try keychain.getAll()
54+
state.availableAPIKeyNames = Array(pairs.keys)
55+
} catch {
56+
toast(error.localizedDescription, .error)
57+
}
58+
59+
return .none
60+
61+
case .apiKeySubmission(.presented(.saveFinished)):
62+
state.apiKeySubmission = nil
63+
return .run { send in
64+
await send(.refreshAvailableAPIKeyNames)
65+
}
66+
67+
case .apiKeySubmission(.presented(.cancelButtonClicked)):
68+
state.apiKeySubmission = nil
69+
return .none
70+
71+
case .apiKeySubmission:
72+
return .none
73+
}
74+
}
75+
.ifLet(\.$apiKeySubmission, action: /Action.apiKeySubmission) {
76+
APIKeySubmission()
77+
}
78+
}
79+
}
80+
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import ComposableArchitecture
2+
import SwiftUI
3+
4+
struct APIKeyPicker: View {
5+
let store: StoreOf<APIKeySelection>
6+
7+
var body: some View {
8+
WithViewStore(store) { viewStore in
9+
HStack {
10+
Picker(
11+
selection: viewStore.$apiKeyName,
12+
content: {
13+
Text("No API Key").tag("")
14+
if viewStore.state.availableAPIKeyNames.isEmpty {
15+
Text("No API key found, please add a new one →")
16+
}
17+
ForEach(viewStore.state.availableAPIKeyNames, id: \.self) { name in
18+
Text(name).tag(name)
19+
}
20+
21+
},
22+
label: { Text("API Key") }
23+
)
24+
}
25+
26+
Button(action: { store.send(.manageAPIKeysButtonClicked) }) {
27+
Text(Image(systemName: "key"))
28+
}
29+
.sheet(isPresented: viewStore.$isAPIKeyManagementPresented) {
30+
APIKeyManagementView(store: store.scope(
31+
state: \.apiKeyManagement,
32+
action: APIKeySelection.Action.apiKeyManagement
33+
))
34+
}
35+
}
36+
.onAppear {
37+
store.send(.appear)
38+
}
39+
}
40+
}
41+
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import Foundation
2+
import SwiftUI
3+
import ComposableArchitecture
4+
5+
struct APIKeySelection: ReducerProtocol {
6+
struct State: Equatable {
7+
@BindingState var apiKeyName: String = ""
8+
var availableAPIKeyNames: [String] {
9+
apiKeyManagement.availableAPIKeyNames
10+
}
11+
var apiKeyManagement: APIKeyManagement.State = .init()
12+
@BindingState var isAPIKeyManagementPresented: Bool = false
13+
}
14+
15+
enum Action: Equatable, BindableAction {
16+
case appear
17+
case manageAPIKeysButtonClicked
18+
19+
case binding(BindingAction<State>)
20+
case apiKeyManagement(APIKeyManagement.Action)
21+
}
22+
23+
@Dependency(\.toast) var toast
24+
@Dependency(\.apiKeyKeychain) var keychain
25+
26+
var body: some ReducerProtocol<State, Action> {
27+
BindingReducer()
28+
29+
Scope(state: \.apiKeyManagement, action: /Action.apiKeyManagement) {
30+
APIKeyManagement()
31+
}
32+
33+
Reduce { state, action in
34+
switch action {
35+
case .appear:
36+
return .run { send in
37+
await send(.apiKeyManagement(.refreshAvailableAPIKeyNames))
38+
}
39+
40+
case .manageAPIKeysButtonClicked:
41+
state.isAPIKeyManagementPresented = true
42+
return .none
43+
44+
case .binding:
45+
return .none
46+
47+
case .apiKeyManagement(.closeButtonClicked):
48+
state.isAPIKeyManagementPresented = false
49+
return .none
50+
51+
case .apiKeyManagement:
52+
return .none
53+
}
54+
}
55+
}
56+
}

Core/Sources/HostApp/AccountSettings/APIKeySubmission.swift renamed to Core/Sources/HostApp/AccountSettings/APIKeyManagement/APIKeySubmission.swift

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,27 +6,51 @@ struct APIKeySubmission: ReducerProtocol {
66
@BindingState var name: String = ""
77
@BindingState var key: String = ""
88
}
9-
9+
1010
enum Action: Equatable, BindableAction {
1111
case binding(BindingAction<State>)
1212
case saveButtonClicked
1313
case cancelButtonClicked
14+
case saveFinished
15+
}
16+
17+
@Dependency(\.toast) var toast
18+
@Dependency(\.apiKeyKeychain) var keychain
19+
20+
enum E: Error, LocalizedError {
21+
case nameIsEmpty
22+
case keyIsEmpty
1423
}
15-
24+
1625
var body: some ReducerProtocol<State, Action> {
1726
BindingReducer()
18-
27+
1928
Reduce { state, action in
2029
switch action {
2130
case .saveButtonClicked:
22-
return .none
23-
31+
do {
32+
guard !state.name.isEmpty else { throw E.nameIsEmpty }
33+
guard !state.key.isEmpty else { throw E.keyIsEmpty }
34+
35+
try keychain.update(state.name, key: state.key)
36+
return .run { send in
37+
await send(.saveFinished)
38+
}
39+
} catch {
40+
toast(error.localizedDescription, .error)
41+
return .none
42+
}
43+
2444
case .cancelButtonClicked:
2545
return .none
26-
46+
47+
case .saveFinished:
48+
return .none
49+
2750
case .binding:
2851
return .none
2952
}
3053
}
3154
}
3255
}
56+

0 commit comments

Comments
 (0)