Skip to content

Commit ef106f9

Browse files
committed
Update ChatWindowView to support tab management
1 parent ed252e6 commit ef106f9

1 file changed

Lines changed: 196 additions & 14 deletions

File tree

Core/Sources/SuggestionWidget/ChatWindowView.swift

Lines changed: 196 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,26 +9,208 @@ private let r: Double = 8
99
struct ChatWindowView: View {
1010
let store: StoreOf<ChatPanelFeature>
1111

12+
struct OverallState: Equatable {
13+
var isPanelDisplayed: Bool
14+
var colorScheme: ColorScheme
15+
var selectedTabId: String?
16+
}
17+
1218
var body: some View {
13-
WithViewStore(store, observe: { $0 }) { viewStore in
14-
Group {
15-
if let chat = viewStore.chat {
16-
ChatPanel(chat: chat)
17-
.background {
18-
Button(action: {
19-
viewStore.send(.hideButtonClicked)
20-
}) {
21-
EmptyView()
22-
}
23-
.keyboardShortcut("M", modifiers: [.command])
24-
}
19+
WithViewStore(
20+
store,
21+
observe: {
22+
OverallState(
23+
isPanelDisplayed: $0.isPanelDisplayed,
24+
colorScheme: $0.colorScheme,
25+
selectedTabId: $0.chatTapGroup.selectedTabId
26+
)
27+
}
28+
) { viewStore in
29+
VStack(spacing: 0) {
30+
ChatTabBar(store: store)
31+
.frame(height: 32)
32+
33+
Divider()
34+
35+
ChatTabContainer(store: store)
36+
.frame(maxWidth: .infinity, maxHeight: .infinity)
37+
}
38+
.background {
39+
Button(action: {
40+
viewStore.send(.hideButtonClicked)
41+
}) {
42+
EmptyView()
2543
}
44+
.opacity(0)
45+
.keyboardShortcut("M", modifiers: [.command])
2646
}
47+
.background(.regularMaterial)
2748
.xcodeStyleFrame()
28-
.opacity(viewStore.isPanelDisplayed ? 1 : 0)
49+
.opacity(viewStore.state.isPanelDisplayed ? 1 : 0)
2950
.frame(minWidth: Style.panelWidth, minHeight: Style.panelHeight)
30-
.preferredColorScheme(viewStore.colorScheme)
51+
.preferredColorScheme(viewStore.state.colorScheme)
52+
}
53+
}
54+
}
55+
56+
struct ChatTabBar: View {
57+
let store: StoreOf<ChatPanelFeature>
58+
59+
struct TabBarState: Equatable {
60+
var tabInfo: [ChatTabInfo]
61+
var selectedTabId: String
62+
}
63+
64+
var body: some View {
65+
WithViewStore(
66+
store,
67+
observe: { TabBarState(
68+
tabInfo: $0.chatTapGroup.tabInfo,
69+
selectedTabId: $0.chatTapGroup.selectedTabId
70+
?? $0.chatTapGroup.tabInfo.first?.id ?? ""
71+
) }
72+
) { viewStore in
73+
HStack(spacing: 0) {
74+
ScrollView(.horizontal) {
75+
HStack(spacing: 0) {
76+
ForEach(viewStore.state.tabInfo, id: \.id) { info in
77+
ChatTabBarButton(
78+
store: store,
79+
info: info,
80+
isSelected: info.id == viewStore.state.selectedTabId
81+
)
82+
}
83+
}
84+
}
85+
86+
Divider()
87+
88+
Button(action: {
89+
store.send(.createNewTapButtonClicked(type: ""))
90+
}) {
91+
Image(systemName: "plus")
92+
.foregroundColor(.secondary)
93+
.padding(8)
94+
}.buttonStyle(.plain)
95+
}
96+
}
97+
}
98+
}
99+
100+
struct ChatTabBarButton: View {
101+
let store: StoreOf<ChatPanelFeature>
102+
let info: ChatTabInfo
103+
let isSelected: Bool
104+
@State var isHovered: Bool = false
105+
106+
var body: some View {
107+
HStack(spacing: 0) {
108+
Button(action: {
109+
store.send(.tabClicked(id: info.id))
110+
}) {
111+
Text(info.title)
112+
.lineLimit(1)
113+
.frame(maxWidth: 120)
114+
}
115+
.buttonStyle(PlainButtonStyle())
116+
.padding(.horizontal, 32)
117+
.frame(maxHeight: .infinity)
118+
119+
.overlay(alignment: .leading) {
120+
Button(action: {
121+
store.send(.closeTabButtonClicked(id: info.id))
122+
}) {
123+
Image(systemName: "xmark")
124+
.foregroundColor(.secondary)
125+
}
126+
.buttonStyle(.plain)
127+
.padding(2)
128+
.padding(.leading, 10)
129+
.opacity(isHovered ? 1 : 0)
130+
}
131+
.onHover { isHovered = $0 }
132+
.animation(.linear(duration: 0.1), value: isHovered)
133+
.animation(.linear(duration: 0.1), value: isSelected)
134+
135+
Divider().padding(.vertical, 16)
31136
}
137+
.background(isSelected ? Color(nsColor: .selectedControlColor) : Color.clear)
138+
}
139+
}
140+
141+
struct ChatTabContainer: View {
142+
let store: StoreOf<ChatPanelFeature>
143+
144+
struct TabContainerState: Equatable {
145+
var tabs: [BaseChatTab]
146+
var selectedTabId: String?
147+
}
148+
149+
var body: some View {
150+
WithViewStore(
151+
store,
152+
observe: {
153+
TabContainerState(
154+
tabs: $0.chatTapGroup.tabs,
155+
selectedTabId: $0.chatTapGroup.selectedTabId
156+
?? $0.chatTapGroup.tabInfo.first?.id ?? ""
157+
)
158+
}
159+
) { viewStore in
160+
ZStack {
161+
if viewStore.state.tabs.isEmpty {
162+
Text("Empty")
163+
} else {
164+
ForEach(viewStore.state.tabs, id: \.id) { tab in
165+
tab.body
166+
.opacity(tab.id == viewStore.state.selectedTabId ? 1 : 0)
167+
.frame(maxWidth: .infinity, maxHeight: .infinity)
168+
}
169+
}
170+
}
171+
}
172+
.onPreferenceChange(ChatTabInfoPreferenceKey.self) { items in
173+
store.send(.updateChatTabInfo(items))
174+
}
175+
}
176+
}
177+
178+
struct ChatWindowView_Previews: PreviewProvider {
179+
class FakeChatTab: ChatTab {
180+
func buildView() -> any View {
181+
ChatPanel(
182+
chat: .init(
183+
history: [
184+
.init(id: "1", role: .assistant, text: "Hello World"),
185+
],
186+
isReceivingMessage: false
187+
),
188+
typedMessage: "Hello World!"
189+
)
190+
}
191+
192+
override init(id: String, title: String) {
193+
super.init(id: id, title: title)
194+
}
195+
}
196+
197+
static var previews: some View {
198+
ChatWindowView(
199+
store: .init(
200+
initialState: .init(
201+
chatTapGroup: .init(
202+
tabs: [
203+
FakeChatTab(id: "1", title: "Hello I am a chatbot"),
204+
EmptyChatTab(id: "2"),
205+
],
206+
selectedTabId: "1"
207+
),
208+
isPanelDisplayed: true
209+
),
210+
reducer: ChatPanelFeature()
211+
)
212+
)
213+
.padding()
32214
}
33215
}
34216

0 commit comments

Comments
 (0)