feat(chat): share SwiftUI chat across macOS+iOS
This commit is contained in:
100
apps/shared/ClawdisKit/Sources/ClawdisChatUI/ChatView.swift
Normal file
100
apps/shared/ClawdisKit/Sources/ClawdisChatUI/ChatView.swift
Normal file
@@ -0,0 +1,100 @@
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
public struct ClawdisChatView: View {
|
||||
@StateObject private var viewModel: ClawdisChatViewModel
|
||||
@State private var scrollerBottomID = UUID()
|
||||
|
||||
public init(viewModel: ClawdisChatViewModel) {
|
||||
self._viewModel = StateObject(wrappedValue: viewModel)
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
ZStack {
|
||||
ClawdisChatTheme.surface
|
||||
.ignoresSafeArea()
|
||||
|
||||
VStack(spacing: 14) {
|
||||
self.header
|
||||
self.messageList
|
||||
ClawdisChatComposer(viewModel: self.viewModel)
|
||||
}
|
||||
.padding(.horizontal, 18)
|
||||
.padding(.vertical, 16)
|
||||
.frame(maxWidth: 1040)
|
||||
}
|
||||
.background(
|
||||
LinearGradient(
|
||||
colors: [
|
||||
Color(red: 0.96, green: 0.97, blue: 1.0),
|
||||
Color(red: 0.93, green: 0.94, blue: 0.98),
|
||||
],
|
||||
startPoint: .top,
|
||||
endPoint: .bottom)
|
||||
.opacity(0.35)
|
||||
.ignoresSafeArea())
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||
.onAppear { self.viewModel.load() }
|
||||
}
|
||||
|
||||
private var header: some View {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Clawd Chat")
|
||||
.font(.title2.weight(.semibold))
|
||||
Text("Session \(self.viewModel.sessionKey) · \(self.viewModel.healthOK ? "Connected" : "Connecting…")")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Button {
|
||||
self.viewModel.refresh()
|
||||
} label: {
|
||||
Label("Refresh", systemImage: "arrow.clockwise")
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
}
|
||||
}
|
||||
|
||||
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(.vertical, 10)
|
||||
.padding(.horizontal, 12)
|
||||
}
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 16, style: .continuous)
|
||||
.fill(ClawdisChatTheme.card)
|
||||
.shadow(color: .black.opacity(0.05), radius: 12, y: 6))
|
||||
.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user