ios: unify attachment controls and message typography
This commit is contained in:
@@ -32,8 +32,9 @@ struct AgentPickedPhoto: Transferable, Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
struct AgentAttachmentComposer: View {
|
||||
struct AgentAttachmentComposer<Prompt: View>: View {
|
||||
@Bindable var room: SybilAgentRoom
|
||||
@ViewBuilder var prompt: () -> Prompt
|
||||
@State private var modalID = UUID()
|
||||
@State private var pickerSession: UUID?
|
||||
@State private var showChoices = false
|
||||
@@ -78,10 +79,17 @@ struct AgentAttachmentComposer: View {
|
||||
if !room.attachmentDraft.supportsAttachments && !room.attachmentDraft.items.isEmpty {
|
||||
Text(AgentAttachmentIssue.unavailable.localizedDescription).font(.sybil(.caption)).foregroundStyle(SybilTheme.danger)
|
||||
}
|
||||
if room.attachmentDraft.supportsAttachments {
|
||||
Button { pickerSession = room.attachmentSessionID; showChoices = true } label: { Label("Attach", systemImage: "paperclip") }
|
||||
.font(.sybil(.caption, weight: .semibold)).accessibilityIdentifier("agent-attach")
|
||||
.disabled(room.attachmentDraft.items.count >= (room.attachmentDraft.capability?.countLimit ?? 0))
|
||||
HStack(alignment: .bottom, spacing: 10) {
|
||||
if room.attachmentDraft.supportsAttachments {
|
||||
SybilPromptAttachmentButton(
|
||||
isEnabled: room.attachmentDraft.items.count < (room.attachmentDraft.capability?.countLimit ?? 0)
|
||||
) {
|
||||
pickerSession = room.attachmentSessionID
|
||||
showChoices = true
|
||||
}
|
||||
.accessibilityIdentifier("agent-attach")
|
||||
}
|
||||
prompt()
|
||||
}
|
||||
}
|
||||
.confirmationDialog("Add attachment", isPresented: $showChoices) {
|
||||
|
||||
@@ -205,19 +205,16 @@ struct SybilAgentView: View {
|
||||
}
|
||||
|
||||
private var composer: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
AgentAttachmentComposer(room: room)
|
||||
HStack(alignment: .bottom, spacing: 10) {
|
||||
SybilPromptField(
|
||||
text: $room.composer,
|
||||
focused: $composerFocused,
|
||||
submit: submitComposer
|
||||
)
|
||||
.accessibilityIdentifier("agent-composer")
|
||||
AgentAttachmentComposer(room: room) {
|
||||
SybilPromptField(
|
||||
text: $room.composer,
|
||||
focused: $composerFocused,
|
||||
submit: submitComposer
|
||||
)
|
||||
.accessibilityIdentifier("agent-composer")
|
||||
|
||||
SybilPromptSendButton(canSend: canSendComposer, submit: submitComposer)
|
||||
.accessibilityIdentifier("agent-send")
|
||||
}
|
||||
SybilPromptSendButton(canSend: canSendComposer, submit: submitComposer)
|
||||
.accessibilityIdentifier("agent-send")
|
||||
}
|
||||
.padding(.horizontal, 14)
|
||||
.padding(.bottom, 12)
|
||||
|
||||
@@ -230,15 +230,7 @@ struct MessageBubble: View {
|
||||
presentation: matchingStreamingAssistantPresentation
|
||||
)
|
||||
} else if !message.content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
if isUser {
|
||||
Text(message.content)
|
||||
.font(.sybil(.body))
|
||||
} else {
|
||||
Markdown(message.content)
|
||||
.tint(SybilTheme.primary)
|
||||
.foregroundStyle(SybilTheme.text.opacity(0.95))
|
||||
.markdownTheme(.sybilReadable)
|
||||
}
|
||||
SybilMessageText(content: message.content, format: isUser ? .literal : .markdown)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, isUser || agentStyle ? 14 : 2)
|
||||
@@ -371,10 +363,7 @@ struct StreamingAssistantContentView: View {
|
||||
}
|
||||
|
||||
if hasAnswer {
|
||||
Markdown(content)
|
||||
.tint(SybilTheme.primary)
|
||||
.foregroundStyle(SybilTheme.text.opacity(0.95))
|
||||
.markdownTheme(.sybilReadable)
|
||||
SybilMessageText(content: content, format: .markdown)
|
||||
.textSelection(.enabled)
|
||||
.id("streaming-assistant-answer")
|
||||
.transition(answerTransition)
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import MarkdownUI
|
||||
import SwiftUI
|
||||
|
||||
/// Both roles and both workspaces use normal chat's readable, Dynamic Type-aware theme.
|
||||
/// Literal input is built from text nodes, never passed through the Markdown parser.
|
||||
struct SybilMessageText: View {
|
||||
enum Format { case literal, markdown }
|
||||
var content: String
|
||||
var format: Format
|
||||
|
||||
static func literalContent(_ text: String) -> MarkdownContent {
|
||||
// Pasted CRLF/CR and blank lines containing spaces or tabs have the same
|
||||
// paragraph boundaries as Markdown. Keep the newline count so deliberate
|
||||
// extra blank lines survive; nonblank lines and Markdown syntax stay literal.
|
||||
let normalized = text.replacingOccurrences(of: "\r\n", with: "\n")
|
||||
.replacingOccurrences(of: "\r", with: "\n")
|
||||
.components(separatedBy: "\n")
|
||||
.map { line in line.allSatisfy { $0 == " " || $0 == "\t" } ? "" : line }
|
||||
.joined(separator: "\n")
|
||||
return MarkdownContent {
|
||||
for paragraph in normalized.components(separatedBy: "\n\n") {
|
||||
Paragraph {
|
||||
// Keep extra blank paragraphs visible, including leading/trailing ones.
|
||||
paragraph.isEmpty ? "\u{00a0}" : paragraph
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Markdown(format == .literal ? Self.literalContent(content) : MarkdownContent(content))
|
||||
.tint(SybilTheme.primary)
|
||||
.foregroundStyle(format == .literal ? SybilTheme.text : SybilTheme.text.opacity(0.95))
|
||||
.markdownTheme(.sybilReadable)
|
||||
}
|
||||
}
|
||||
@@ -60,3 +60,26 @@ struct SybilPromptSendButton: View {
|
||||
.accessibilityLabel("Send message")
|
||||
}
|
||||
}
|
||||
|
||||
/// The normal chat attachment control, shared with Agent's composer.
|
||||
/// Picker ownership and availability stay with each composer.
|
||||
struct SybilPromptAttachmentButton: View {
|
||||
var isEnabled: Bool
|
||||
var action: () -> Void
|
||||
|
||||
var body: some View {
|
||||
Button(action: action) {
|
||||
Image(systemName: "paperclip")
|
||||
.font(.system(size: 17, weight: .semibold))
|
||||
.frame(width: 40, height: 40)
|
||||
.background(Circle().fill(SybilTheme.surface))
|
||||
.overlay(Circle().stroke(SybilTheme.border.opacity(0.82), lineWidth: 1))
|
||||
.foregroundStyle(isEnabled ? SybilTheme.text : SybilTheme.textMuted)
|
||||
.frame(width: 44, height: 44)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.disabled(!isEnabled)
|
||||
.accessibilityLabel("Attach file")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -592,25 +592,10 @@ struct SybilWorkspaceView: View {
|
||||
|
||||
HStack(alignment: .bottom, spacing: 10) {
|
||||
if !viewModel.isSearchMode {
|
||||
Button {
|
||||
SybilPromptAttachmentButton(isEnabled: !viewModel.isActiveSelectionSending) {
|
||||
isShowingAttachmentOptions = true
|
||||
} label: {
|
||||
Image(systemName: "paperclip")
|
||||
.font(.system(size: 17, weight: .semibold))
|
||||
.frame(width: 40, height: 40)
|
||||
.background(
|
||||
Circle()
|
||||
.fill(SybilTheme.surface)
|
||||
)
|
||||
.overlay(
|
||||
Circle()
|
||||
.stroke(SybilTheme.border.opacity(0.82), lineWidth: 1)
|
||||
)
|
||||
.foregroundStyle(viewModel.isActiveSelectionSending ? SybilTheme.textMuted : SybilTheme.text)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.disabled(viewModel.isActiveSelectionSending)
|
||||
.accessibilityLabel("Attach file")
|
||||
.accessibilityIdentifier("chat-attach")
|
||||
}
|
||||
|
||||
SybilPromptField(
|
||||
|
||||
Reference in New Issue
Block a user