ios: adds file uploading
This commit is contained in:
@@ -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"
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user