refactor(chat-ui): compact layout
parent
d54cc49d66
commit
aab5c490dc
|
|
@ -17,7 +17,7 @@ struct ClawdisChatComposer: View {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
HStack {
|
HStack(spacing: 10) {
|
||||||
self.thinkingPicker
|
self.thinkingPicker
|
||||||
Spacer()
|
Spacer()
|
||||||
self.attachmentPicker
|
self.attachmentPicker
|
||||||
|
|
@ -29,24 +29,14 @@ struct ClawdisChatComposer: View {
|
||||||
|
|
||||||
self.editor
|
self.editor
|
||||||
|
|
||||||
HStack {
|
if let error = self.viewModel.errorText, !error.isEmpty {
|
||||||
if let error = self.viewModel.errorText {
|
Text(error)
|
||||||
Text(error)
|
.font(.footnote)
|
||||||
.font(.footnote)
|
.foregroundStyle(.red)
|
||||||
.foregroundStyle(.red)
|
.lineLimit(2)
|
||||||
}
|
|
||||||
Spacer()
|
|
||||||
Button {
|
|
||||||
self.viewModel.send()
|
|
||||||
} label: {
|
|
||||||
Label(self.viewModel.isSending ? "Sending…" : "Send", systemImage: "arrow.up.circle.fill")
|
|
||||||
.font(.headline)
|
|
||||||
}
|
|
||||||
.buttonStyle(.borderedProminent)
|
|
||||||
.disabled(!self.viewModel.canSend)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(14)
|
.padding(12)
|
||||||
.background(
|
.background(
|
||||||
RoundedRectangle(cornerRadius: 16, style: .continuous)
|
RoundedRectangle(cornerRadius: 16, style: .continuous)
|
||||||
.fill(ClawdisChatTheme.card)
|
.fill(ClawdisChatTheme.card)
|
||||||
|
|
@ -67,7 +57,8 @@ struct ClawdisChatComposer: View {
|
||||||
}
|
}
|
||||||
.labelsHidden()
|
.labelsHidden()
|
||||||
.pickerStyle(.menu)
|
.pickerStyle(.menu)
|
||||||
.frame(maxWidth: 200)
|
.controlSize(.small)
|
||||||
|
.frame(maxWidth: 140, alignment: .leading)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
|
|
@ -76,14 +67,18 @@ struct ClawdisChatComposer: View {
|
||||||
Button {
|
Button {
|
||||||
self.pickFilesMac()
|
self.pickFilesMac()
|
||||||
} label: {
|
} label: {
|
||||||
Label("Add Image", systemImage: "paperclip")
|
Image(systemName: "paperclip")
|
||||||
}
|
}
|
||||||
|
.help("Add Image")
|
||||||
.buttonStyle(.bordered)
|
.buttonStyle(.bordered)
|
||||||
|
.controlSize(.small)
|
||||||
#else
|
#else
|
||||||
PhotosPicker(selection: self.$pickerItems, maxSelectionCount: 8, matching: .images) {
|
PhotosPicker(selection: self.$pickerItems, maxSelectionCount: 8, matching: .images) {
|
||||||
Label("Add Image", systemImage: "paperclip")
|
Image(systemName: "paperclip")
|
||||||
}
|
}
|
||||||
|
.help("Add Image")
|
||||||
.buttonStyle(.bordered)
|
.buttonStyle(.bordered)
|
||||||
|
.controlSize(.small)
|
||||||
.onChange(of: self.pickerItems) { _, newItems in
|
.onChange(of: self.pickerItems) { _, newItems in
|
||||||
Task { await self.loadPhotosPickerItems(newItems) }
|
Task { await self.loadPhotosPickerItems(newItems) }
|
||||||
}
|
}
|
||||||
|
|
@ -135,7 +130,7 @@ struct ClawdisChatComposer: View {
|
||||||
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
||||||
.fill(ClawdisChatTheme.card))
|
.fill(ClawdisChatTheme.card))
|
||||||
.overlay(self.editorOverlay)
|
.overlay(self.editorOverlay)
|
||||||
.frame(maxHeight: 180)
|
.frame(maxHeight: 140)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var editorOverlay: some View {
|
private var editorOverlay: some View {
|
||||||
|
|
@ -143,17 +138,18 @@ struct ClawdisChatComposer: View {
|
||||||
if self.viewModel.input.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
if self.viewModel.input.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||||
Text("Message Clawd…")
|
Text("Message Clawd…")
|
||||||
.foregroundStyle(.tertiary)
|
.foregroundStyle(.tertiary)
|
||||||
.padding(.horizontal, 12)
|
.padding(.horizontal, 10)
|
||||||
.padding(.vertical, 10)
|
.padding(.vertical, 8)
|
||||||
}
|
}
|
||||||
|
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
ChatComposerTextView(text: self.$viewModel.input) {
|
ChatComposerTextView(text: self.$viewModel.input) {
|
||||||
self.viewModel.send()
|
self.viewModel.send()
|
||||||
}
|
}
|
||||||
.frame(minHeight: 54, maxHeight: 160)
|
.frame(minHeight: 44, maxHeight: 120)
|
||||||
.padding(.horizontal, 10)
|
.padding(.horizontal, 8)
|
||||||
.padding(.vertical, 8)
|
.padding(.vertical, 6)
|
||||||
|
.padding(.trailing, 44)
|
||||||
#else
|
#else
|
||||||
TextEditor(text: self.$viewModel.input)
|
TextEditor(text: self.$viewModel.input)
|
||||||
.font(.system(size: 15))
|
.font(.system(size: 15))
|
||||||
|
|
@ -162,6 +158,27 @@ struct ClawdisChatComposer: View {
|
||||||
.padding(.vertical, 8)
|
.padding(.vertical, 8)
|
||||||
.focused(self.$isFocused)
|
.focused(self.$isFocused)
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
VStack {
|
||||||
|
Spacer()
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
Button {
|
||||||
|
self.viewModel.send()
|
||||||
|
} label: {
|
||||||
|
if self.viewModel.isSending {
|
||||||
|
ProgressView().controlSize(.small)
|
||||||
|
} else {
|
||||||
|
Image(systemName: "arrow.up")
|
||||||
|
.font(.system(size: 13, weight: .semibold))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.buttonStyle(.borderedProminent)
|
||||||
|
.controlSize(.small)
|
||||||
|
.disabled(!self.viewModel.canSend)
|
||||||
|
.padding(8)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,40 +14,18 @@ public struct ClawdisChatView: View {
|
||||||
ClawdisChatTheme.surface
|
ClawdisChatTheme.surface
|
||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
|
|
||||||
VStack(spacing: 14) {
|
VStack(spacing: 10) {
|
||||||
self.header
|
|
||||||
self.messageList
|
self.messageList
|
||||||
ClawdisChatComposer(viewModel: self.viewModel)
|
ClawdisChatComposer(viewModel: self.viewModel)
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 18)
|
.padding(.horizontal, 12)
|
||||||
.padding(.vertical, 16)
|
.padding(.vertical, 12)
|
||||||
.frame(maxWidth: 1040)
|
.frame(maxWidth: .infinity)
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||||
.onAppear { self.viewModel.load() }
|
.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 {
|
private var messageList: some View {
|
||||||
ScrollViewReader { proxy in
|
ScrollViewReader { proxy in
|
||||||
ScrollView {
|
ScrollView {
|
||||||
|
|
@ -68,13 +46,31 @@ public struct ClawdisChatView: View {
|
||||||
.frame(height: 1)
|
.frame(height: 1)
|
||||||
.id(self.scrollerBottomID)
|
.id(self.scrollerBottomID)
|
||||||
}
|
}
|
||||||
.padding(.vertical, 10)
|
.padding(.top, 40)
|
||||||
|
.padding(.bottom, 10)
|
||||||
.padding(.horizontal, 12)
|
.padding(.horizontal, 12)
|
||||||
}
|
}
|
||||||
.background(
|
.background(
|
||||||
RoundedRectangle(cornerRadius: 16, style: .continuous)
|
RoundedRectangle(cornerRadius: 16, style: .continuous)
|
||||||
.fill(ClawdisChatTheme.card)
|
.fill(ClawdisChatTheme.card)
|
||||||
.shadow(color: .black.opacity(0.05), radius: 12, y: 6))
|
.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
|
.onChange(of: self.viewModel.messages.count) { _, _ in
|
||||||
withAnimation(.snappy(duration: 0.22)) {
|
withAnimation(.snappy(duration: 0.22)) {
|
||||||
proxy.scrollTo(self.scrollerBottomID, anchor: .bottom)
|
proxy.scrollTo(self.scrollerBottomID, anchor: .bottom)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue