feat(chat): restyle onboarding chat UI
parent
6b56f7d643
commit
5936ed7941
|
|
@ -10,6 +10,7 @@ import UniformTypeIdentifiers
|
||||||
@MainActor
|
@MainActor
|
||||||
struct ClawdisChatComposer: View {
|
struct ClawdisChatComposer: View {
|
||||||
@Bindable var viewModel: ClawdisChatViewModel
|
@Bindable var viewModel: ClawdisChatViewModel
|
||||||
|
let style: ClawdisChatView.Style
|
||||||
|
|
||||||
#if !os(macOS)
|
#if !os(macOS)
|
||||||
@State private var pickerItems: [PhotosPickerItem] = []
|
@State private var pickerItems: [PhotosPickerItem] = []
|
||||||
|
|
@ -18,14 +19,16 @@ struct ClawdisChatComposer: View {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 6) {
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
HStack(spacing: 8) {
|
if self.showsToolbar {
|
||||||
self.thinkingPicker
|
HStack(spacing: 8) {
|
||||||
Spacer()
|
self.thinkingPicker
|
||||||
self.refreshButton
|
Spacer()
|
||||||
self.attachmentPicker
|
self.refreshButton
|
||||||
|
self.attachmentPicker
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !self.viewModel.attachments.isEmpty {
|
if self.showsAttachments && !self.viewModel.attachments.isEmpty {
|
||||||
self.attachmentsStrip
|
self.attachmentsStrip
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -40,9 +43,9 @@ struct ClawdisChatComposer: View {
|
||||||
}
|
}
|
||||||
.padding(8)
|
.padding(8)
|
||||||
.background(
|
.background(
|
||||||
RoundedRectangle(cornerRadius: 16, style: .continuous)
|
RoundedRectangle(cornerRadius: 18, style: .continuous)
|
||||||
.fill(ClawdisChatTheme.card)
|
.fill(ClawdisChatTheme.composerBackground)
|
||||||
.shadow(color: .black.opacity(0.06), radius: 12, y: 6))
|
.shadow(color: .black.opacity(0.08), radius: 10, y: 4))
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
.onDrop(of: [.fileURL], isTargeted: nil) { providers in
|
.onDrop(of: [.fileURL], isTargeted: nil) { providers in
|
||||||
self.handleDrop(providers)
|
self.handleDrop(providers)
|
||||||
|
|
@ -126,15 +129,17 @@ struct ClawdisChatComposer: View {
|
||||||
|
|
||||||
private var editor: some View {
|
private var editor: some View {
|
||||||
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
||||||
.strokeBorder(ClawdisChatTheme.divider)
|
.strokeBorder(ClawdisChatTheme.composerBorder)
|
||||||
.background(
|
.background(
|
||||||
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
||||||
.fill(ClawdisChatTheme.card))
|
.fill(ClawdisChatTheme.composerField))
|
||||||
.overlay {
|
.overlay {
|
||||||
VStack(alignment: .leading, spacing: 6) {
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
self.editorOverlay
|
self.editorOverlay
|
||||||
HStack(alignment: .bottom, spacing: 8) {
|
HStack(alignment: .bottom, spacing: 8) {
|
||||||
self.connectionPill
|
if self.showsConnectionPill {
|
||||||
|
self.connectionPill
|
||||||
|
}
|
||||||
Spacer(minLength: 0)
|
Spacer(minLength: 0)
|
||||||
self.sendButton
|
self.sendButton
|
||||||
}
|
}
|
||||||
|
|
@ -195,29 +200,32 @@ struct ClawdisChatComposer: View {
|
||||||
self.viewModel.abort()
|
self.viewModel.abort()
|
||||||
} label: {
|
} label: {
|
||||||
if self.viewModel.isAborting {
|
if self.viewModel.isAborting {
|
||||||
ProgressView().controlSize(.small)
|
ProgressView().controlSize(.mini)
|
||||||
} else {
|
} else {
|
||||||
Image(systemName: "stop.fill")
|
Image(systemName: "stop.fill")
|
||||||
.font(.system(size: 13, weight: .semibold))
|
.font(.system(size: 13, weight: .semibold))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.buttonStyle(.bordered)
|
.buttonStyle(.plain)
|
||||||
.tint(.red)
|
.foregroundStyle(.white)
|
||||||
.controlSize(.small)
|
.padding(8)
|
||||||
|
.background(Circle().fill(Color.red))
|
||||||
.disabled(self.viewModel.isAborting)
|
.disabled(self.viewModel.isAborting)
|
||||||
} else {
|
} else {
|
||||||
Button {
|
Button {
|
||||||
self.viewModel.send()
|
self.viewModel.send()
|
||||||
} label: {
|
} label: {
|
||||||
if self.viewModel.isSending {
|
if self.viewModel.isSending {
|
||||||
ProgressView().controlSize(.small)
|
ProgressView().controlSize(.mini)
|
||||||
} else {
|
} else {
|
||||||
Image(systemName: "arrow.up")
|
Image(systemName: "arrow.up")
|
||||||
.font(.system(size: 13, weight: .semibold))
|
.font(.system(size: 13, weight: .semibold))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.buttonStyle(.borderedProminent)
|
.buttonStyle(.plain)
|
||||||
.controlSize(.small)
|
.foregroundStyle(.white)
|
||||||
|
.padding(8)
|
||||||
|
.background(Circle().fill(Color.accentColor))
|
||||||
.disabled(!self.viewModel.canSend)
|
.disabled(!self.viewModel.canSend)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -234,6 +242,18 @@ struct ClawdisChatComposer: View {
|
||||||
.help("Refresh")
|
.help("Refresh")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var showsToolbar: Bool {
|
||||||
|
self.style == .standard
|
||||||
|
}
|
||||||
|
|
||||||
|
private var showsAttachments: Bool {
|
||||||
|
self.style == .standard
|
||||||
|
}
|
||||||
|
|
||||||
|
private var showsConnectionPill: Bool {
|
||||||
|
self.style == .standard
|
||||||
|
}
|
||||||
|
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
private func pickFilesMac() {
|
private func pickFilesMac() {
|
||||||
let panel = NSOpenPanel()
|
let panel = NSOpenPanel()
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@ import Foundation
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
private enum ChatUIConstants {
|
private enum ChatUIConstants {
|
||||||
static let bubbleMaxWidth: CGFloat = 760
|
static let bubbleMaxWidth: CGFloat = 560
|
||||||
static let bubbleCorner: CGFloat = 16
|
static let bubbleCorner: CGFloat = 18
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
|
|
@ -11,27 +11,10 @@ struct ChatMessageBubble: View {
|
||||||
let message: ClawdisChatMessage
|
let message: ClawdisChatMessage
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: self.isUser ? .trailing : .leading, spacing: 8) {
|
ChatMessageBody(message: self.message, isUser: self.isUser)
|
||||||
HStack(spacing: 8) {
|
.frame(maxWidth: ChatUIConstants.bubbleMaxWidth, alignment: self.isUser ? .trailing : .leading)
|
||||||
if !self.isUser {
|
.frame(maxWidth: .infinity, alignment: self.isUser ? .trailing : .leading)
|
||||||
Label("Assistant", systemImage: "sparkles")
|
.padding(.horizontal, 2)
|
||||||
.labelStyle(.titleAndIcon)
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
}
|
|
||||||
Spacer(minLength: 0)
|
|
||||||
if self.isUser {
|
|
||||||
Label("You", systemImage: "person.fill")
|
|
||||||
.labelStyle(.titleAndIcon)
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ChatMessageBody(message: self.message, isUser: self.isUser)
|
|
||||||
.frame(maxWidth: ChatUIConstants.bubbleMaxWidth, alignment: self.isUser ? .trailing : .leading)
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 2)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var isUser: Bool { self.message.role.lowercased() == "user" }
|
private var isUser: Bool { self.message.role.lowercased() == "user" }
|
||||||
|
|
@ -45,14 +28,15 @@ private struct ChatMessageBody: View {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
let text = self.primaryText
|
let text = self.primaryText
|
||||||
let split = ChatMarkdownSplitter.split(markdown: text)
|
let split = ChatMarkdownSplitter.split(markdown: text)
|
||||||
|
let textColor = self.isUser ? ClawdisChatTheme.userText : ClawdisChatTheme.assistantText
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 10) {
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
ForEach(split.blocks) { block in
|
ForEach(split.blocks) { block in
|
||||||
switch block.kind {
|
switch block.kind {
|
||||||
case .text:
|
case .text:
|
||||||
MarkdownTextView(text: block.text)
|
MarkdownTextView(text: block.text, textColor: textColor)
|
||||||
case let .code(language):
|
case let .code(language):
|
||||||
CodeBlockView(code: block.text, language: language)
|
CodeBlockView(code: block.text, language: language, isUser: self.isUser)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -80,12 +64,14 @@ private struct ChatMessageBody: View {
|
||||||
|
|
||||||
if !self.inlineAttachments.isEmpty {
|
if !self.inlineAttachments.isEmpty {
|
||||||
ForEach(self.inlineAttachments.indices, id: \.self) { idx in
|
ForEach(self.inlineAttachments.indices, id: \.self) { idx in
|
||||||
AttachmentRow(att: self.inlineAttachments[idx])
|
AttachmentRow(att: self.inlineAttachments[idx], isUser: self.isUser)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.textSelection(.enabled)
|
.textSelection(.enabled)
|
||||||
.padding(12)
|
.padding(.vertical, 10)
|
||||||
|
.padding(.horizontal, 12)
|
||||||
|
.foregroundStyle(textColor)
|
||||||
.background(self.bubbleBackground)
|
.background(self.bubbleBackground)
|
||||||
.overlay(self.bubbleBorder)
|
.overlay(self.bubbleBorder)
|
||||||
.clipShape(RoundedRectangle(cornerRadius: ChatUIConstants.bubbleCorner, style: .continuous))
|
.clipShape(RoundedRectangle(cornerRadius: ChatUIConstants.bubbleCorner, style: .continuous))
|
||||||
|
|
@ -101,27 +87,21 @@ private struct ChatMessageBody: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private var bubbleBackground: AnyShapeStyle {
|
private var bubbleBackground: AnyShapeStyle {
|
||||||
if self.isUser {
|
let fill = self.isUser ? ClawdisChatTheme.userBubble : ClawdisChatTheme.assistantBubble
|
||||||
return AnyShapeStyle(
|
return AnyShapeStyle(fill)
|
||||||
LinearGradient(
|
|
||||||
colors: [
|
|
||||||
Color.orange.opacity(0.22),
|
|
||||||
Color.accentColor.opacity(0.18),
|
|
||||||
],
|
|
||||||
startPoint: .topLeading,
|
|
||||||
endPoint: .bottomTrailing))
|
|
||||||
}
|
|
||||||
return AnyShapeStyle(ClawdisChatTheme.subtleCard)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var bubbleBorder: some View {
|
private var bubbleBorder: some View {
|
||||||
RoundedRectangle(cornerRadius: ChatUIConstants.bubbleCorner, style: .continuous)
|
RoundedRectangle(cornerRadius: ChatUIConstants.bubbleCorner, style: .continuous)
|
||||||
.strokeBorder(self.isUser ? Color.orange.opacity(0.35) : Color.white.opacity(0.10), lineWidth: 1)
|
.strokeBorder(
|
||||||
|
self.isUser ? Color.white.opacity(0.12) : Color.black.opacity(0.08),
|
||||||
|
lineWidth: self.isUser ? 0.5 : 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct AttachmentRow: View {
|
private struct AttachmentRow: View {
|
||||||
let att: ClawdisChatMessageContent
|
let att: ClawdisChatMessageContent
|
||||||
|
let isUser: Bool
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
|
|
@ -129,10 +109,11 @@ private struct AttachmentRow: View {
|
||||||
Text(self.att.fileName ?? "Attachment")
|
Text(self.att.fileName ?? "Attachment")
|
||||||
.font(.footnote)
|
.font(.footnote)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
|
.foregroundStyle(self.isUser ? ClawdisChatTheme.userText : ClawdisChatTheme.assistantText)
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
.padding(10)
|
.padding(10)
|
||||||
.background(Color.white.opacity(0.06))
|
.background(self.isUser ? Color.white.opacity(0.2) : Color.black.opacity(0.04))
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
|
.clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -150,10 +131,10 @@ struct ChatTypingIndicatorBubble: View {
|
||||||
.padding(12)
|
.padding(12)
|
||||||
.background(
|
.background(
|
||||||
RoundedRectangle(cornerRadius: 16, style: .continuous)
|
RoundedRectangle(cornerRadius: 16, style: .continuous)
|
||||||
.fill(ClawdisChatTheme.subtleCard))
|
.fill(ClawdisChatTheme.assistantBubble))
|
||||||
.overlay(
|
.overlay(
|
||||||
RoundedRectangle(cornerRadius: 16, style: .continuous)
|
RoundedRectangle(cornerRadius: 16, style: .continuous)
|
||||||
.strokeBorder(Color.white.opacity(0.10), lineWidth: 1))
|
.strokeBorder(Color.black.opacity(0.08), lineWidth: 1))
|
||||||
.frame(maxWidth: ChatUIConstants.bubbleMaxWidth, alignment: .leading)
|
.frame(maxWidth: ChatUIConstants.bubbleMaxWidth, alignment: .leading)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -164,19 +145,15 @@ struct ChatStreamingAssistantBubble: View {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 10) {
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
Label("Assistant (streaming)", systemImage: "sparkles")
|
ChatMarkdownBody(text: self.text, textColor: ClawdisChatTheme.assistantText)
|
||||||
.font(.caption)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
|
|
||||||
ChatMarkdownBody(text: self.text)
|
|
||||||
}
|
}
|
||||||
.padding(12)
|
.padding(12)
|
||||||
.background(
|
.background(
|
||||||
RoundedRectangle(cornerRadius: 16, style: .continuous)
|
RoundedRectangle(cornerRadius: 16, style: .continuous)
|
||||||
.fill(ClawdisChatTheme.subtleCard))
|
.fill(ClawdisChatTheme.assistantBubble))
|
||||||
.overlay(
|
.overlay(
|
||||||
RoundedRectangle(cornerRadius: 16, style: .continuous)
|
RoundedRectangle(cornerRadius: 16, style: .continuous)
|
||||||
.strokeBorder(Color.white.opacity(0.10), lineWidth: 1))
|
.strokeBorder(Color.black.opacity(0.08), lineWidth: 1))
|
||||||
.frame(maxWidth: ChatUIConstants.bubbleMaxWidth, alignment: .leading)
|
.frame(maxWidth: ChatUIConstants.bubbleMaxWidth, alignment: .leading)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -207,10 +184,10 @@ struct ChatPendingToolsBubble: View {
|
||||||
.padding(12)
|
.padding(12)
|
||||||
.background(
|
.background(
|
||||||
RoundedRectangle(cornerRadius: 16, style: .continuous)
|
RoundedRectangle(cornerRadius: 16, style: .continuous)
|
||||||
.fill(ClawdisChatTheme.subtleCard))
|
.fill(ClawdisChatTheme.assistantBubble))
|
||||||
.overlay(
|
.overlay(
|
||||||
RoundedRectangle(cornerRadius: 16, style: .continuous)
|
RoundedRectangle(cornerRadius: 16, style: .continuous)
|
||||||
.strokeBorder(Color.white.opacity(0.10), lineWidth: 1))
|
.strokeBorder(Color.black.opacity(0.08), lineWidth: 1))
|
||||||
.frame(maxWidth: ChatUIConstants.bubbleMaxWidth, alignment: .leading)
|
.frame(maxWidth: ChatUIConstants.bubbleMaxWidth, alignment: .leading)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -245,16 +222,17 @@ private struct TypingDots: View {
|
||||||
@MainActor
|
@MainActor
|
||||||
private struct MarkdownTextView: View {
|
private struct MarkdownTextView: View {
|
||||||
let text: String
|
let text: String
|
||||||
|
let textColor: Color
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
if let attributed = try? AttributedString(markdown: self.text) {
|
if let attributed = try? AttributedString(markdown: self.text) {
|
||||||
Text(attributed)
|
Text(attributed)
|
||||||
.font(.system(size: 14))
|
.font(.system(size: 14))
|
||||||
.foregroundStyle(.primary)
|
.foregroundStyle(self.textColor)
|
||||||
} else {
|
} else {
|
||||||
Text(self.text)
|
Text(self.text)
|
||||||
.font(.system(size: 14))
|
.font(.system(size: 14))
|
||||||
.foregroundStyle(.primary)
|
.foregroundStyle(self.textColor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -262,6 +240,7 @@ private struct MarkdownTextView: View {
|
||||||
@MainActor
|
@MainActor
|
||||||
private struct ChatMarkdownBody: View {
|
private struct ChatMarkdownBody: View {
|
||||||
let text: String
|
let text: String
|
||||||
|
let textColor: Color
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
let split = ChatMarkdownSplitter.split(markdown: self.text)
|
let split = ChatMarkdownSplitter.split(markdown: self.text)
|
||||||
|
|
@ -269,9 +248,9 @@ private struct ChatMarkdownBody: View {
|
||||||
ForEach(split.blocks) { block in
|
ForEach(split.blocks) { block in
|
||||||
switch block.kind {
|
switch block.kind {
|
||||||
case .text:
|
case .text:
|
||||||
MarkdownTextView(text: block.text)
|
MarkdownTextView(text: block.text, textColor: self.textColor)
|
||||||
case let .code(language):
|
case let .code(language):
|
||||||
CodeBlockView(code: block.text, language: language)
|
CodeBlockView(code: block.text, language: language, isUser: false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -305,6 +284,7 @@ private struct ChatMarkdownBody: View {
|
||||||
private struct CodeBlockView: View {
|
private struct CodeBlockView: View {
|
||||||
let code: String
|
let code: String
|
||||||
let language: String?
|
let language: String?
|
||||||
|
let isUser: Bool
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
|
@ -315,15 +295,15 @@ private struct CodeBlockView: View {
|
||||||
}
|
}
|
||||||
Text(self.code)
|
Text(self.code)
|
||||||
.font(.system(size: 13, weight: .regular, design: .monospaced))
|
.font(.system(size: 13, weight: .regular, design: .monospaced))
|
||||||
.foregroundStyle(.primary)
|
.foregroundStyle(self.isUser ? .white : .primary)
|
||||||
.textSelection(.enabled)
|
.textSelection(.enabled)
|
||||||
}
|
}
|
||||||
.padding(12)
|
.padding(12)
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
.background(Color.black.opacity(0.06))
|
.background(self.isUser ? Color.white.opacity(0.16) : Color.black.opacity(0.06))
|
||||||
.overlay(
|
.overlay(
|
||||||
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
||||||
.strokeBorder(Color.white.opacity(0.10), lineWidth: 1))
|
.strokeBorder(Color.black.opacity(0.08), lineWidth: 1))
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
|
.clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,52 @@ enum ClawdisChatTheme {
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static var userBubble: Color {
|
||||||
|
#if os(macOS)
|
||||||
|
Color(nsColor: .systemBlue)
|
||||||
|
#else
|
||||||
|
Color(uiColor: .systemBlue)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
static var assistantBubble: Color {
|
||||||
|
#if os(macOS)
|
||||||
|
Color(nsColor: .controlBackgroundColor)
|
||||||
|
#else
|
||||||
|
Color(uiColor: .secondarySystemBackground)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
static var userText: Color { .white }
|
||||||
|
|
||||||
|
static var assistantText: Color {
|
||||||
|
#if os(macOS)
|
||||||
|
Color(nsColor: .labelColor)
|
||||||
|
#else
|
||||||
|
Color(uiColor: .label)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
static var composerBackground: Color {
|
||||||
|
#if os(macOS)
|
||||||
|
Color(nsColor: .windowBackgroundColor)
|
||||||
|
#else
|
||||||
|
Color(uiColor: .systemBackground)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
static var composerField: Color {
|
||||||
|
#if os(macOS)
|
||||||
|
Color(nsColor: .textBackgroundColor)
|
||||||
|
#else
|
||||||
|
Color(uiColor: .secondarySystemBackground)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
static var composerBorder: Color {
|
||||||
|
Color.secondary.opacity(0.2)
|
||||||
|
}
|
||||||
|
|
||||||
static var divider: Color {
|
static var divider: Color {
|
||||||
Color.secondary.opacity(0.2)
|
Color.secondary.opacity(0.2)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,19 +2,25 @@ import SwiftUI
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
public struct ClawdisChatView: View {
|
public struct ClawdisChatView: View {
|
||||||
|
public enum Style {
|
||||||
|
case standard
|
||||||
|
case onboarding
|
||||||
|
}
|
||||||
|
|
||||||
@State private var viewModel: ClawdisChatViewModel
|
@State private var viewModel: ClawdisChatViewModel
|
||||||
@State private var scrollerBottomID = UUID()
|
@State private var scrollerBottomID = UUID()
|
||||||
@State private var showSessions = false
|
@State private var showSessions = false
|
||||||
private let showsSessionSwitcher: Bool
|
private let showsSessionSwitcher: Bool
|
||||||
|
private let style: Style
|
||||||
|
|
||||||
private enum Layout {
|
private enum Layout {
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
static let outerPadding: CGFloat = 2
|
static let outerPadding: CGFloat = 6
|
||||||
static let stackSpacing: CGFloat = 3
|
static let stackSpacing: CGFloat = 6
|
||||||
static let messageSpacing: CGFloat = 8
|
static let messageSpacing: CGFloat = 6
|
||||||
static let messageListPaddingTop: CGFloat = 0
|
static let messageListPaddingTop: CGFloat = 2
|
||||||
static let messageListPaddingBottom: CGFloat = 2
|
static let messageListPaddingBottom: CGFloat = 4
|
||||||
static let messageListPaddingHorizontal: CGFloat = 4
|
static let messageListPaddingHorizontal: CGFloat = 6
|
||||||
#else
|
#else
|
||||||
static let outerPadding: CGFloat = 6
|
static let outerPadding: CGFloat = 6
|
||||||
static let stackSpacing: CGFloat = 6
|
static let stackSpacing: CGFloat = 6
|
||||||
|
|
@ -25,9 +31,14 @@ public struct ClawdisChatView: View {
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
public init(viewModel: ClawdisChatViewModel, showsSessionSwitcher: Bool = false) {
|
public init(
|
||||||
|
viewModel: ClawdisChatViewModel,
|
||||||
|
showsSessionSwitcher: Bool = false,
|
||||||
|
style: Style = .standard)
|
||||||
|
{
|
||||||
self._viewModel = State(initialValue: viewModel)
|
self._viewModel = State(initialValue: viewModel)
|
||||||
self.showsSessionSwitcher = showsSessionSwitcher
|
self.showsSessionSwitcher = showsSessionSwitcher
|
||||||
|
self.style = style
|
||||||
}
|
}
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
|
|
@ -37,7 +48,7 @@ public struct ClawdisChatView: View {
|
||||||
|
|
||||||
VStack(spacing: Layout.stackSpacing) {
|
VStack(spacing: Layout.stackSpacing) {
|
||||||
self.messageList
|
self.messageList
|
||||||
ClawdisChatComposer(viewModel: self.viewModel)
|
ClawdisChatComposer(viewModel: self.viewModel, style: self.style)
|
||||||
}
|
}
|
||||||
.padding(.horizontal, Layout.outerPadding)
|
.padding(.horizontal, Layout.outerPadding)
|
||||||
.padding(.vertical, Layout.outerPadding)
|
.padding(.vertical, Layout.outerPadding)
|
||||||
|
|
@ -88,10 +99,6 @@ public struct ClawdisChatView: View {
|
||||||
.padding(.bottom, Layout.messageListPaddingBottom)
|
.padding(.bottom, Layout.messageListPaddingBottom)
|
||||||
.padding(.horizontal, Layout.messageListPaddingHorizontal)
|
.padding(.horizontal, Layout.messageListPaddingHorizontal)
|
||||||
}
|
}
|
||||||
.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
|
.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