Files
Sybil-2/ios/Packages/Sybil/Sources/Sybil/SybilAttachmentViews.swift

223 lines
7.5 KiB
Swift

import SwiftUI
enum SybilAttachmentTone {
case composer
case user
case assistant
}
struct SybilAttachmentListView: View {
var attachments: [ChatAttachment]
var tone: SybilAttachmentTone
var onRemove: ((String) -> Void)? = nil
var body: some View {
VStack(alignment: .leading, spacing: 8) {
ForEach(attachments) { attachment in
Group {
if attachment.kind == .image {
imageCard(attachment)
} else {
textCard(attachment)
}
}
}
}
}
@ViewBuilder
private func imageCard(_ attachment: ChatAttachment) -> some View {
VStack(alignment: .leading, spacing: 0) {
if let image = SybilChatAttachmentSupport.image(for: attachment) {
Image(uiImage: image)
.resizable()
.scaledToFill()
.frame(maxWidth: .infinity)
.frame(height: 180)
.clipped()
} else {
ZStack {
RoundedRectangle(cornerRadius: 14)
.fill(Color.black.opacity(0.18))
Image(systemName: "photo")
.font(.system(size: 22, weight: .medium))
.foregroundStyle(SybilTheme.textMuted)
}
.frame(height: 140)
}
HStack(alignment: .top, spacing: 10) {
Image(systemName: "photo")
.font(.system(size: 13, weight: .semibold))
.foregroundStyle(titleColor.opacity(0.92))
.frame(width: 26, height: 26)
.background(
RoundedRectangle(cornerRadius: 8)
.fill(Color.white.opacity(0.06))
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(Color.white.opacity(0.08), lineWidth: 1)
)
)
VStack(alignment: .leading, spacing: 2) {
Text(attachment.filename)
.font(.sybil(.footnote, weight: .medium))
.foregroundStyle(titleColor)
.lineLimit(1)
Text(attachment.mimeType)
.font(.sybil(.caption2))
.foregroundStyle(SybilTheme.textMuted)
.lineLimit(1)
}
Spacer(minLength: 0)
if let onRemove {
removeButton(for: attachment.id, onRemove: onRemove)
}
}
.padding(12)
}
.background(cardBackground)
.clipShape(RoundedRectangle(cornerRadius: 14))
.overlay(
RoundedRectangle(cornerRadius: 14)
.stroke(cardBorder, lineWidth: 1)
)
}
@ViewBuilder
private func textCard(_ attachment: ChatAttachment) -> some View {
VStack(alignment: .leading, spacing: 10) {
HStack(alignment: .top, spacing: 10) {
Image(systemName: "doc.text")
.font(.system(size: 13, weight: .semibold))
.foregroundStyle(titleColor.opacity(0.92))
.frame(width: 26, height: 26)
.background(
RoundedRectangle(cornerRadius: 8)
.fill(Color.white.opacity(0.06))
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(Color.white.opacity(0.08), lineWidth: 1)
)
)
VStack(alignment: .leading, spacing: 2) {
Text(attachment.filename)
.font(.sybil(.footnote, weight: .medium))
.foregroundStyle(titleColor)
.lineLimit(1)
Text(
attachment.truncated == true
? "\(attachment.mimeType) • truncated"
: attachment.mimeType
)
.font(.sybil(.caption2))
.foregroundStyle(SybilTheme.textMuted)
.lineLimit(1)
}
Spacer(minLength: 0)
if let onRemove {
removeButton(for: attachment.id, onRemove: onRemove)
}
}
Text(SybilChatAttachmentSupport.previewText(for: attachment))
.font(.system(.caption, design: .monospaced))
.foregroundStyle(bodyColor)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(10)
.background(
RoundedRectangle(cornerRadius: 10)
.fill(Color.black.opacity(0.16))
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.white.opacity(0.05), lineWidth: 1)
)
)
}
.padding(12)
.background(cardBackground)
.clipShape(RoundedRectangle(cornerRadius: 14))
.overlay(
RoundedRectangle(cornerRadius: 14)
.stroke(cardBorder, lineWidth: 1)
)
}
private func removeButton(for attachmentID: String, onRemove: @escaping (String) -> Void) -> some View {
Button {
onRemove(attachmentID)
} label: {
Image(systemName: "xmark")
.font(.system(size: 11, weight: .bold))
.foregroundStyle(SybilTheme.textMuted)
.frame(width: 24, height: 24)
.background(
Circle()
.fill(Color.white.opacity(0.06))
)
}
.buttonStyle(.plain)
.accessibilityLabel("Remove attachment")
}
private var cardBackground: some ShapeStyle {
switch tone {
case .composer:
return AnyShapeStyle(
LinearGradient(
colors: [SybilTheme.surface.opacity(0.86), SybilTheme.surfaceStrong.opacity(0.78)],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
case .user:
return AnyShapeStyle(Color.black.opacity(0.14))
case .assistant:
return AnyShapeStyle(
LinearGradient(
colors: [SybilTheme.surface.opacity(0.58), SybilTheme.surfaceStrong.opacity(0.42)],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
}
}
private var cardBorder: Color {
switch tone {
case .composer:
return SybilTheme.border.opacity(0.82)
case .user:
return Color.white.opacity(0.12)
case .assistant:
return SybilTheme.border.opacity(0.58)
}
}
private var titleColor: Color {
switch tone {
case .composer, .assistant:
return SybilTheme.text
case .user:
return SybilTheme.text
}
}
private var bodyColor: Color {
switch tone {
case .composer, .assistant:
return SybilTheme.text.opacity(0.94)
case .user:
return SybilTheme.text.opacity(0.96)
}
}
}