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) } } } } }