Skip to content

Commit e81ad0c

Browse files
committed
✨ Clean up empty states & first onboarding views
1 parent 81710c4 commit e81ad0c

File tree

4 files changed

+169
-115
lines changed

4 files changed

+169
-115
lines changed

DraftPatch/RootView.swift

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -84,13 +84,6 @@ struct RootView: View {
8484
if viewModel.availableModels.count > 0 {
8585
ModelPickerPopoverView()
8686
.environmentObject(viewModel)
87-
} else {
88-
Button {
89-
viewModel.showSettings.toggle()
90-
} label: {
91-
Text("Enable an LLM provider")
92-
Image(systemName: "arrowshape.right.fill")
93-
}
9487
}
9588
}
9689

DraftPatch/Views/ChatView.swift

Lines changed: 165 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -18,126 +18,187 @@ struct ChatView: View {
1818
viewModel.selectedThread?.messages.sorted { $0.timestamp < $1.timestamp } ?? []
1919
}
2020

21-
var body: some View {
22-
if let thread = viewModel.selectedThread {
23-
VStack {
24-
VStack(spacing: 0) {
25-
if thread.messages.isEmpty {
26-
Text("No messages yet")
27-
.foregroundColor(.gray).opacity(0.01)
28-
.padding()
29-
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
30-
}
21+
@ViewBuilder
22+
private var noLLMConfiguredView: some View {
23+
VStack(spacing: 16) {
24+
Image(systemName: "gear")
25+
.resizable()
26+
.scaledToFit()
27+
.frame(width: 80, height: 80)
28+
.foregroundStyle(.secondary)
29+
Text("LLM Not Configured")
30+
.font(.title2)
31+
.bold()
32+
Text("Please configure an LLM to start using DraftPatch.")
33+
.multilineTextAlignment(.center)
34+
.foregroundStyle(.secondary)
35+
.padding(.horizontal, 24)
36+
Button(action: {
37+
viewModel.showSettings.toggle()
38+
}) {
39+
HStack(spacing: 4) {
40+
Text("Enable an LLM provider")
41+
Image(systemName: "arrowshape.right.fill")
42+
}
43+
}
44+
.buttonStyle(.borderedProminent)
45+
}
46+
.frame(maxWidth: .infinity, maxHeight: .infinity)
47+
.background(.black.opacity(0.1))
48+
}
3149

32-
GeometryReader { geometry in
33-
Color.clear
34-
.onAppear {
35-
currentViewportHeight = geometry.size.height
36-
}
37-
.onChange(of: geometry.size.height) { _, newHeight in
38-
currentViewportHeight = newHeight
39-
}
40-
ScrollViewReader { scrollProxy in
41-
ScrollView {
42-
VStack(spacing: 0) {
43-
VStack(spacing: 8) {
44-
ForEach(sortedMessages, id: \.id) { msg in
45-
ChatMessageRow(message: msg)
46-
.id(msg.id)
47-
.environmentObject(viewModel)
48-
.frame(maxWidth: .infinity, alignment: .leading)
49-
}
50-
51-
if viewModel.isAwaitingResponse {
52-
LoadingAnimationView()
53-
.padding(.vertical, 8)
54-
}
55-
56-
if sentMessage && (sortedMessages.filter { $0.role == .user }.count > 1) {
57-
Spacer(minLength: currentViewportHeight - 150)
58-
.id("bottomSpacer")
59-
.accessibilityIdentifier("dynamicSpacer")
60-
}
61-
62-
Color.clear.frame(height: 1)
63-
.id("bottomAnchor")
50+
@ViewBuilder
51+
private var noThreadsView: some View {
52+
VStack(spacing: 16) {
53+
Image(systemName: "flag.checkered")
54+
.resizable()
55+
.scaledToFit()
56+
.frame(width: 80, height: 80)
57+
.foregroundStyle(.secondary)
58+
Text("No Threads Available")
59+
.font(.title2)
60+
.bold()
61+
Text("Create a new thread to start drafting!")
62+
.multilineTextAlignment(.center)
63+
.foregroundStyle(.secondary)
64+
.padding(.horizontal, 24)
65+
}
66+
.frame(maxWidth: .infinity, maxHeight: .infinity)
67+
.background(.black.opacity(0.1))
68+
}
69+
70+
@ViewBuilder
71+
private var noChatSelectedView: some View {
72+
VStack(spacing: 16) {
73+
Image(systemName: "questionmark")
74+
.resizable()
75+
.scaledToFit()
76+
.frame(width: 80, height: 80)
77+
.foregroundStyle(.secondary)
78+
Text("No Chat Selected")
79+
.font(.title2)
80+
.bold()
81+
Text("Please select a chat from the list or create a new one.")
82+
.multilineTextAlignment(.center)
83+
.foregroundStyle(.secondary)
84+
.padding(.horizontal, 24)
85+
}
86+
.frame(maxWidth: .infinity, maxHeight: .infinity)
87+
.background(.black.opacity(0.1))
88+
}
89+
90+
private var chatView: some View {
91+
// Force unwrapping is safe here because this view is only used when a thread is selected
92+
let thread = viewModel.selectedThread!
93+
return VStack {
94+
VStack(spacing: 0) {
95+
if thread.messages.isEmpty {
96+
Text("No messages yet")
97+
.foregroundColor(.gray).opacity(0.01)
98+
.padding()
99+
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
100+
}
101+
102+
GeometryReader { geometry in
103+
Color.clear
104+
.onAppear {
105+
currentViewportHeight = geometry.size.height
106+
}
107+
.onChange(of: geometry.size.height) { _, newHeight in
108+
currentViewportHeight = newHeight
109+
}
110+
ScrollViewReader { scrollProxy in
111+
ScrollView {
112+
VStack(spacing: 0) {
113+
VStack(spacing: 8) {
114+
ForEach(sortedMessages, id: \.id) { msg in
115+
ChatMessageRow(message: msg)
116+
.id(msg.id)
117+
.environmentObject(viewModel)
118+
.frame(maxWidth: .infinity, alignment: .leading)
119+
}
64120

121+
if viewModel.isAwaitingResponse {
122+
LoadingAnimationView()
123+
.padding(.vertical, 8)
65124
}
66-
.padding()
67-
.frame(maxWidth: 960)
125+
126+
if sentMessage && (sortedMessages.filter { $0.role == .user }.count > 1) {
127+
Spacer(minLength: currentViewportHeight - 150)
128+
.id("bottomSpacer")
129+
.accessibilityIdentifier("dynamicSpacer")
130+
}
131+
132+
Color.clear.frame(height: 1)
133+
.id("bottomAnchor")
68134
}
69-
.frame(maxWidth: .infinity, maxHeight: .infinity)
135+
.padding()
136+
.frame(maxWidth: 960)
70137
}
71-
.defaultScrollAnchor(.bottom)
72-
.onAppear {
73-
scrollViewProxy = scrollProxy
74-
sentMessage = false
138+
.frame(maxWidth: .infinity, maxHeight: .infinity)
139+
}
140+
.defaultScrollAnchor(.bottom)
141+
.onAppear {
142+
scrollViewProxy = scrollProxy
143+
sentMessage = false
75144

76-
DispatchQueue.main.async {
77-
scrollProxy.scrollTo(sentMessage ? "bottomSpacer" : "bottomAnchor", anchor: .bottom)
78-
}
145+
DispatchQueue.main.async {
146+
scrollProxy.scrollTo(sentMessage ? "bottomSpacer" : "bottomAnchor", anchor: .bottom)
79147
}
80-
.onChange(of: viewModel.lastUserMessageID) { _, newID in
81-
guard let newID else { return }
148+
}
149+
.onChange(of: viewModel.lastUserMessageID) { _, newID in
150+
guard let newID else { return }
82151

83-
DispatchQueue.main.async {
84-
withAnimation(.smooth) {
85-
scrollViewProxy?.scrollTo(newID, anchor: .top)
86-
}
152+
DispatchQueue.main.async {
153+
withAnimation(.smooth) {
154+
scrollViewProxy?.scrollTo(newID, anchor: .top)
87155
}
88156
}
89157
}
90158
}
91-
.frame(maxHeight: .infinity)
92-
93-
if let error = viewModel.errorMessage {
94-
Text(error)
95-
.foregroundColor(.red)
96-
.padding(.vertical)
97-
.onAppear {
98-
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
99-
viewModel.errorMessage = nil
100-
}
159+
}
160+
.frame(maxHeight: .infinity)
161+
162+
if let error = viewModel.errorMessage {
163+
Text(error)
164+
.foregroundColor(.red)
165+
.padding(.vertical)
166+
.onAppear {
167+
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
168+
viewModel.errorMessage = nil
101169
}
102-
}
103-
104-
ChatBoxView(
105-
userMessage: $userMessage,
106-
selectedDraftApp: $viewModel.selectedDraftApp,
107-
isTextFieldFocused: $viewModel.chatBoxFocused,
108-
thinking: viewModel.thinking,
109-
onSubmit: sendMessage,
110-
onCancel: {
111-
viewModel.cancelStreamingMessage()
112-
},
113-
draftWithLastApp: viewModel.toggleDraftWithLastApp
114-
)
115-
.padding(.horizontal)
116-
.frame(maxWidth: 960)
170+
}
117171
}
118-
.id(thread.id)
172+
173+
ChatBoxView(
174+
userMessage: $userMessage,
175+
selectedDraftApp: $viewModel.selectedDraftApp,
176+
isTextFieldFocused: $viewModel.chatBoxFocused,
177+
thinking: viewModel.thinking,
178+
onSubmit: sendMessage,
179+
onCancel: {
180+
viewModel.cancelStreamingMessage()
181+
},
182+
draftWithLastApp: viewModel.toggleDraftWithLastApp
183+
)
184+
.padding(.horizontal)
185+
.frame(maxWidth: 960)
119186
}
120-
.padding(.bottom, 12)
121-
.background(.black.opacity(0.2))
187+
.id(thread.id)
188+
}
189+
.padding(.bottom, 12)
190+
.background(.black.opacity(0.2))
191+
}
192+
193+
var body: some View {
194+
if viewModel.availableModels.isEmpty {
195+
noLLMConfiguredView
196+
} else if viewModel.chatThreads.isEmpty && viewModel.selectedThread == nil {
197+
noThreadsView
198+
} else if viewModel.selectedThread == nil {
199+
noChatSelectedView
122200
} else {
123-
VStack(spacing: 16) {
124-
Image(systemName: "flag.checkered")
125-
.resizable()
126-
.scaledToFit()
127-
.frame(width: 80, height: 80)
128-
.foregroundStyle(.secondary)
129-
130-
Text("No chat selected")
131-
.font(.title2)
132-
.bold()
133-
134-
Text("Select a chat and start drafting!")
135-
.multilineTextAlignment(.center)
136-
.foregroundStyle(.secondary)
137-
.padding(.horizontal, 24)
138-
}
139-
.frame(maxWidth: .infinity, maxHeight: .infinity)
140-
.background(.black.opacity(0.1))
201+
chatView
141202
}
142203
}
143204

DraftPatch/Views/SettingsView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ struct SettingsView: View {
4141

4242
Divider()
4343

44-
if isAnyLLMEnabled {
44+
if isAnyLLMEnabled && viewModel.availableModels.isEmpty == false {
4545
Picker("Select Default Model", selection: $selectedDefaultModel) {
4646
Text("None").tag(nil as ChatModel?)
4747
ForEach(viewModel.availableModels) { model in

DraftPatchUITests/DraftPatchUITests.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,11 @@ final class DraftPatchUITests: XCTestCase {
4040

4141
XCTAssertTrue(app.wait(for: .runningForeground, timeout: 5))
4242

43-
let noChatSelectedText = app.staticTexts["No chat selected"]
43+
let noChatSelectedText = app.staticTexts["No Threads Available"]
4444
XCTAssertTrue(
45-
noChatSelectedText.waitForExistence(timeout: 2), "The 'No chat selected' text is not visible")
45+
noChatSelectedText.waitForExistence(timeout: 2), "The 'No threads available' text is not visible")
4646

47-
let startDraftingText = app.staticTexts["Select a chat and start drafting!"]
47+
let startDraftingText = app.staticTexts["Create a new thread to start drafting!"]
4848
XCTAssertTrue(
4949
startDraftingText.waitForExistence(timeout: 2),
5050
"The 'Select a chat and start drafting!' text is not visible")

0 commit comments

Comments
 (0)