Files
clawdbot/apps/shared/ClawdisKit/Sources/ClawdisChatUI/ChatView.swift
2025-12-14 05:06:34 +00:00

87 lines
3.1 KiB
Swift

import SwiftUI
@MainActor
public struct ClawdisChatView: View {
@State private var viewModel: ClawdisChatViewModel
@State private var scrollerBottomID = UUID()
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() }
}
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)
}
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)
}
.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)
}
}
}
}
}