ios: adds file uploading

This commit is contained in:
2026-05-02 19:47:38 -07:00
parent fd9ee455fb
commit 01ee807991
8 changed files with 1070 additions and 55 deletions

View File

@@ -91,6 +91,7 @@ final class SybilViewModel {
var errorMessage: String?
var composer = ""
var composerAttachments: [ChatAttachment] = []
var provider: Provider
var modelCatalog: [Provider: ProviderModelInfo] = [:]
var model: String
@@ -202,6 +203,19 @@ final class SybilViewModel {
return draftKind != nil || selectedItem != nil
}
var canSendComposer: Bool {
if isSending {
return false
}
let content = composer.trimmingCharacters(in: .whitespacesAndNewlines)
if isSearchMode {
return !content.isEmpty
}
return !content.isEmpty || !composerAttachments.isEmpty
}
var displayedMessages: [Message] {
let canonical = displayableMessages(selectedChat?.messages ?? [])
guard let pending = pendingChatState else {
@@ -282,6 +296,7 @@ final class SybilViewModel {
authError = nil
errorMessage = nil
pendingChatState = nil
composerAttachments = []
settings.persist()
SybilLog.info(
@@ -358,6 +373,7 @@ final class SybilViewModel {
selectedSearch = nil
errorMessage = nil
composer = ""
composerAttachments = []
}
func startNewSearch() {
@@ -368,6 +384,7 @@ final class SybilViewModel {
selectedSearch = nil
errorMessage = nil
composer = ""
composerAttachments = []
}
func openSettings() {
@@ -377,6 +394,7 @@ final class SybilViewModel {
selectedChat = nil
selectedSearch = nil
errorMessage = nil
composerAttachments = []
}
func select(_ selection: SidebarSelection) {
@@ -384,6 +402,9 @@ final class SybilViewModel {
draftKind = nil
selectedItem = selection
errorMessage = nil
if case .search = selection {
composerAttachments = []
}
if case .settings = selection {
selectedChat = nil
@@ -430,11 +451,20 @@ final class SybilViewModel {
func sendComposer() async {
let content = composer.trimmingCharacters(in: .whitespacesAndNewlines)
guard !content.isEmpty, !isSending else {
let attachments = composerAttachments
guard !isSending else {
return
}
if isSearchMode {
guard !content.isEmpty else { return }
} else if content.isEmpty && attachments.isEmpty {
return
}
composer = ""
composerAttachments = []
errorMessage = nil
isSending = true
@@ -444,7 +474,7 @@ final class SybilViewModel {
try await sendSearch(query: content)
} else {
SybilLog.info(SybilLog.ui, "Sending chat prompt")
try await sendChat(content: content)
try await sendChat(content: content, attachments: attachments)
}
} catch {
errorMessage = normalizeAPIError(error)
@@ -468,12 +498,38 @@ final class SybilViewModel {
}
}
pendingChatState = nil
if !isSearchMode {
composer = content
composerAttachments = attachments
pendingChatState = nil
}
}
isSending = false
}
func appendComposerAttachments(_ attachments: [ChatAttachment]) throws {
guard !attachments.isEmpty else {
return
}
guard !isSearchMode else {
errorMessage = "Attachments are only available in chat mode."
return
}
if composerAttachments.count + attachments.count > SybilChatAttachmentSupport.maxAttachmentsPerMessage {
throw ChatAttachmentError.tooManyAttachments(SybilChatAttachmentSupport.maxAttachmentsPerMessage)
}
composerAttachments += attachments
errorMessage = nil
}
func removeComposerAttachment(id: String) {
composerAttachments.removeAll { $0.id == id }
}
func startChatFromSelectedSearch() async {
guard let search = selectedSearch, !isCreatingSearchChat, !isSending else {
return
@@ -488,6 +544,7 @@ final class SybilViewModel {
draftKind = nil
pendingChatState = nil
composer = ""
composerAttachments = []
chats.removeAll(where: { $0.id == chat.id })
chats.insert(chat, at: 0)
@@ -668,13 +725,14 @@ final class SybilViewModel {
selectedSearch = nil
}
private func sendChat(content: String) async throws {
private func sendChat(content: String, attachments: [ChatAttachment]) async throws {
let optimisticUser = Message(
id: "temp-user-\(UUID().uuidString)",
createdAt: Date(),
role: .user,
content: content,
name: nil
name: nil,
metadata: SybilChatAttachmentSupport.metadataValue(for: attachments)
)
let optimisticAssistant = Message(
@@ -740,8 +798,8 @@ final class SybilViewModel {
baseChat.messages
.filter { !$0.isToolCallLog }
.map {
CompletionRequestMessage(role: $0.role, content: $0.content, name: $0.name)
} + [CompletionRequestMessage(role: .user, content: content)]
CompletionRequestMessage(role: $0.role, content: $0.content, name: $0.name, attachments: $0.attachments.isEmpty ? nil : $0.attachments)
} + [CompletionRequestMessage(role: .user, content: content, attachments: attachments.isEmpty ? nil : attachments)]
let streamStatus = CompletionStreamStatus()
@@ -749,7 +807,8 @@ final class SybilViewModel {
Task { [weak self] in
guard let self else { return }
do {
let updated = try await client.suggestChatTitle(chatID: chatID, content: content)
let titleSeed = !content.isEmpty ? content : SybilChatAttachmentSupport.attachmentSummary(attachments)
let updated = try await client.suggestChatTitle(chatID: chatID, content: titleSeed.isEmpty ? "Uploaded files" : titleSeed)
await MainActor.run {
self.chats = self.chats.map { existing in
if existing.id == updated.id {
@@ -1019,6 +1078,13 @@ final class SybilViewModel {
return String(firstUserMessage.prefix(48))
}
if let firstUserMessage = messages?.first(where: { $0.role == .user }) {
let attachmentSummary = SybilChatAttachmentSupport.attachmentSummary(firstUserMessage.attachments)
if !attachmentSummary.isEmpty {
return String(attachmentSummary.prefix(48))
}
}
return "New chat"
}