@@ -18,126 +18,187 @@ struct ChatView: View {
18
18
viewModel. selectedThread? . messages. sorted { $0. timestamp < $1. timestamp } ?? [ ]
19
19
}
20
20
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
+ }
31
49
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
+ }
64
120
121
+ if viewModel. isAwaitingResponse {
122
+ LoadingAnimationView ( )
123
+ . padding ( . vertical, 8 )
65
124
}
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 " )
68
134
}
69
- . frame ( maxWidth: . infinity, maxHeight: . infinity)
135
+ . padding ( )
136
+ . frame ( maxWidth: 960 )
70
137
}
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
75
144
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)
79
147
}
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 }
82
151
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)
87
155
}
88
156
}
89
157
}
90
158
}
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
101
169
}
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
+ }
117
171
}
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 )
119
186
}
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
122
200
} 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
141
202
}
142
203
}
143
204
0 commit comments