iCloud Backup, better chatview exp. bugfixes++
This commit is contained in:
@@ -33,6 +33,8 @@ struct MessageRow: View {
|
||||
let viewModel: ChatViewModel?
|
||||
private let settings = SettingsService.shared
|
||||
|
||||
@State private var isExpanded = false
|
||||
|
||||
#if os(macOS)
|
||||
@State private var isHovering = false
|
||||
@State private var showCopied = false
|
||||
@@ -82,8 +84,8 @@ struct MessageRow: View {
|
||||
.help("Star this message to always include it in context")
|
||||
}
|
||||
|
||||
// Copy button (assistant messages only, visible on hover)
|
||||
if message.role == .assistant && isHovering && !message.content.isEmpty {
|
||||
// Copy button (user + assistant messages, visible on hover)
|
||||
if (message.role == .assistant || message.role == .user) && isHovering && !message.content.isEmpty {
|
||||
Button(action: copyContent) {
|
||||
HStack(spacing: 3) {
|
||||
Image(systemName: showCopied ? "checkmark" : "doc.on.doc")
|
||||
@@ -188,27 +190,126 @@ struct MessageRow: View {
|
||||
|
||||
@ViewBuilder
|
||||
private var compactSystemMessage: some View {
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "wrench.and.screwdriver")
|
||||
.font(.system(size: 11))
|
||||
.foregroundColor(.secondary)
|
||||
let expandable = message.toolCalls != nil
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
Button(action: {
|
||||
if expandable {
|
||||
withAnimation(.easeInOut(duration: 0.18)) { isExpanded.toggle() }
|
||||
}
|
||||
}) {
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "wrench.and.screwdriver")
|
||||
.font(.system(size: 11))
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
Text(message.content)
|
||||
.font(.system(size: 11))
|
||||
.foregroundColor(.secondary)
|
||||
Text(message.content)
|
||||
.font(.system(size: 11))
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
Spacer()
|
||||
Spacer()
|
||||
|
||||
Text(message.timestamp, style: .time)
|
||||
.font(.system(size: 10))
|
||||
.foregroundColor(.secondary.opacity(0.7))
|
||||
if expandable {
|
||||
Image(systemName: isExpanded ? "chevron.up" : "chevron.down")
|
||||
.font(.system(size: 9, weight: .medium))
|
||||
.foregroundColor(.secondary.opacity(0.5))
|
||||
}
|
||||
|
||||
Text(message.timestamp, style: .time)
|
||||
.font(.system(size: 10))
|
||||
.foregroundColor(.secondary.opacity(0.7))
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 6)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
if isExpanded, let calls = message.toolCalls {
|
||||
Divider()
|
||||
.padding(.horizontal, 8)
|
||||
toolCallsDetailView(calls)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 6)
|
||||
.background(Color.secondary.opacity(0.08))
|
||||
.cornerRadius(6)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func toolCallsDetailView(_ calls: [ToolCallDetail]) -> some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
ForEach(calls.indices, id: \.self) { i in
|
||||
let call = calls[i]
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
// Tool name + status
|
||||
HStack(spacing: 6) {
|
||||
Image(systemName: "function")
|
||||
.font(.system(size: 10, weight: .semibold))
|
||||
.foregroundColor(.secondary)
|
||||
Text(call.name)
|
||||
.font(.system(size: 11, weight: .semibold))
|
||||
.foregroundColor(.secondary)
|
||||
Spacer()
|
||||
if call.result == nil {
|
||||
ProgressView()
|
||||
.scaleEffect(0.5)
|
||||
.frame(width: 12, height: 12)
|
||||
} else {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.font(.system(size: 10))
|
||||
.foregroundColor(.green.opacity(0.8))
|
||||
}
|
||||
}
|
||||
|
||||
// Input
|
||||
if !call.input.isEmpty && call.input != "{}" {
|
||||
toolDetailSection(label: "Input", text: prettyJSON(call.input), maxHeight: 100)
|
||||
}
|
||||
|
||||
// Result
|
||||
if let result = call.result {
|
||||
toolDetailSection(label: "Result", text: prettyJSON(result), maxHeight: 180)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 8)
|
||||
|
||||
if i < calls.count - 1 {
|
||||
Divider().padding(.horizontal, 12)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func toolDetailSection(label: String, text: String, maxHeight: CGFloat) -> some View {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(label.uppercased())
|
||||
.font(.system(size: 9, weight: .semibold))
|
||||
.foregroundColor(.secondary.opacity(0.6))
|
||||
ScrollView([.vertical, .horizontal]) {
|
||||
Text(text)
|
||||
.font(.system(size: 10, design: .monospaced))
|
||||
.foregroundColor(.secondary)
|
||||
.textSelection(.enabled)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
.frame(maxHeight: maxHeight)
|
||||
.background(Color.secondary.opacity(0.06))
|
||||
.cornerRadius(4)
|
||||
}
|
||||
}
|
||||
|
||||
private func prettyJSON(_ raw: String) -> String {
|
||||
guard let data = raw.data(using: .utf8),
|
||||
let obj = try? JSONSerialization.jsonObject(with: data),
|
||||
let pretty = try? JSONSerialization.data(withJSONObject: obj, options: [.prettyPrinted, .sortedKeys]),
|
||||
let str = String(data: pretty, encoding: .utf8) else {
|
||||
return raw
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
// MARK: - Message Content
|
||||
|
||||
@ViewBuilder
|
||||
|
||||
Reference in New Issue
Block a user