118 lines
4.4 KiB
Swift
118 lines
4.4 KiB
Swift
import SwiftUI
|
|
|
|
@MainActor
|
|
public struct ClawdisChatView: View {
|
|
@State private var viewModel: ClawdisChatViewModel
|
|
@State private var scrollerBottomID = UUID()
|
|
@State private var showSessions = false
|
|
|
|
public init(viewModel: ClawdisChatViewModel) {
|
|
self._viewModel = State(initialValue: viewModel)
|
|
}
|
|
|
|
public var body: some View {
|
|
ZStack {
|
|
ClawdisChatTheme.surface
|
|
.ignoresSafeArea()
|
|
|
|
VStack(spacing: 10) {
|
|
self.messageList
|
|
ClawdisChatComposer(viewModel: self.viewModel)
|
|
}
|
|
.padding(.horizontal, 12)
|
|
.padding(.vertical, 12)
|
|
.frame(maxWidth: .infinity)
|
|
}
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
|
.onAppear { self.viewModel.load() }
|
|
.sheet(isPresented: self.$showSessions) {
|
|
ChatSessionsSheet(viewModel: self.viewModel)
|
|
}
|
|
}
|
|
|
|
private var messageList: some View {
|
|
ScrollViewReader { proxy in
|
|
ScrollView {
|
|
LazyVStack(spacing: 14) {
|
|
ForEach(self.viewModel.messages) { msg in
|
|
ChatMessageBubble(message: msg)
|
|
.frame(
|
|
maxWidth: .infinity,
|
|
alignment: msg.role.lowercased() == "user" ? .trailing : .leading)
|
|
}
|
|
|
|
if self.viewModel.pendingRunCount > 0 {
|
|
ChatTypingIndicatorBubble()
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
}
|
|
|
|
if !self.viewModel.pendingToolCalls.isEmpty {
|
|
ChatPendingToolsBubble(toolCalls: self.viewModel.pendingToolCalls)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
}
|
|
|
|
if let text = self.viewModel.streamingAssistantText, !text.isEmpty {
|
|
ChatStreamingAssistantBubble(text: text)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
}
|
|
|
|
Color.clear
|
|
.frame(height: 1)
|
|
.id(self.scrollerBottomID)
|
|
}
|
|
.padding(.top, 40)
|
|
.padding(.bottom, 10)
|
|
.padding(.horizontal, 12)
|
|
}
|
|
.background(
|
|
RoundedRectangle(cornerRadius: 16, style: .continuous)
|
|
.fill(ClawdisChatTheme.card)
|
|
.shadow(color: .black.opacity(0.05), radius: 12, y: 6))
|
|
.overlay(alignment: .topLeading) {
|
|
HStack(spacing: 8) {
|
|
Circle()
|
|
.fill(self.viewModel.healthOK ? .green : .orange)
|
|
.frame(width: 7, height: 7)
|
|
Text(self.viewModel.sessionKey)
|
|
.font(.caption.weight(.semibold))
|
|
Text(self.viewModel.healthOK ? "Connected" : "Connecting…")
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
Spacer(minLength: 0)
|
|
|
|
Button {
|
|
self.showSessions = true
|
|
} label: {
|
|
Image(systemName: "tray.full")
|
|
}
|
|
.buttonStyle(.borderless)
|
|
.help("Sessions")
|
|
|
|
Button {
|
|
self.viewModel.refresh()
|
|
} label: {
|
|
Image(systemName: "arrow.clockwise")
|
|
}
|
|
.buttonStyle(.borderless)
|
|
.help("Refresh")
|
|
}
|
|
.padding(.horizontal, 10)
|
|
.padding(.vertical, 6)
|
|
.background(ClawdisChatTheme.subtleCard)
|
|
.clipShape(Capsule())
|
|
.padding(10)
|
|
}
|
|
.onChange(of: self.viewModel.messages.count) { _, _ in
|
|
withAnimation(.snappy(duration: 0.22)) {
|
|
proxy.scrollTo(self.scrollerBottomID, anchor: .bottom)
|
|
}
|
|
}
|
|
.onChange(of: self.viewModel.pendingRunCount) { _, _ in
|
|
withAnimation(.snappy(duration: 0.22)) {
|
|
proxy.scrollTo(self.scrollerBottomID, anchor: .bottom)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|