This commit is contained in:
@@ -154,6 +154,8 @@ public struct ChatAttachment: Codable, Hashable, Identifiable, Sendable {
|
||||
public struct ChatSummary: Codable, Identifiable, Hashable, Sendable {
|
||||
public var id: String
|
||||
public var title: String?
|
||||
public var parentChatId: String? = nil
|
||||
public var titleGenerationPending = false
|
||||
public var createdAt: Date
|
||||
public var updatedAt: Date
|
||||
public var starred = false
|
||||
@@ -164,6 +166,39 @@ public struct ChatSummary: Codable, Identifiable, Hashable, Sendable {
|
||||
public var lastUsedModel: String?
|
||||
}
|
||||
|
||||
extension ChatSummary {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case title
|
||||
case parentChatId
|
||||
case titleGenerationPending
|
||||
case createdAt
|
||||
case updatedAt
|
||||
case starred
|
||||
case starredAt
|
||||
case initiatedProvider
|
||||
case initiatedModel
|
||||
case lastUsedProvider
|
||||
case lastUsedModel
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
id = try container.decode(String.self, forKey: .id)
|
||||
title = try container.decodeIfPresent(String.self, forKey: .title)
|
||||
parentChatId = try container.decodeIfPresent(String.self, forKey: .parentChatId)
|
||||
titleGenerationPending = try container.decodeIfPresent(Bool.self, forKey: .titleGenerationPending) ?? false
|
||||
createdAt = try container.decode(Date.self, forKey: .createdAt)
|
||||
updatedAt = try container.decode(Date.self, forKey: .updatedAt)
|
||||
starred = try container.decodeIfPresent(Bool.self, forKey: .starred) ?? false
|
||||
starredAt = try container.decodeIfPresent(Date.self, forKey: .starredAt)
|
||||
initiatedProvider = try container.decodeIfPresent(Provider.self, forKey: .initiatedProvider)
|
||||
initiatedModel = try container.decodeIfPresent(String.self, forKey: .initiatedModel)
|
||||
lastUsedProvider = try container.decodeIfPresent(Provider.self, forKey: .lastUsedProvider)
|
||||
lastUsedModel = try container.decodeIfPresent(String.self, forKey: .lastUsedModel)
|
||||
}
|
||||
}
|
||||
|
||||
public struct SearchSummary: Codable, Identifiable, Hashable, Sendable {
|
||||
public var id: String
|
||||
public var title: String?
|
||||
@@ -184,6 +219,8 @@ public struct WorkspaceItem: Codable, Identifiable, Hashable, Sendable {
|
||||
public var id: String
|
||||
public var title: String?
|
||||
public var query: String?
|
||||
public var parentChatId: String? = nil
|
||||
public var titleGenerationPending = false
|
||||
public var createdAt: Date
|
||||
public var updatedAt: Date
|
||||
public var starred = false
|
||||
@@ -198,6 +235,8 @@ public struct WorkspaceItem: Codable, Identifiable, Hashable, Sendable {
|
||||
self.id = chat.id
|
||||
self.title = chat.title
|
||||
self.query = nil
|
||||
self.parentChatId = chat.parentChatId
|
||||
self.titleGenerationPending = chat.titleGenerationPending
|
||||
self.createdAt = chat.createdAt
|
||||
self.updatedAt = chat.updatedAt
|
||||
self.starred = chat.starred
|
||||
@@ -213,6 +252,8 @@ public struct WorkspaceItem: Codable, Identifiable, Hashable, Sendable {
|
||||
self.id = search.id
|
||||
self.title = search.title
|
||||
self.query = search.query
|
||||
self.parentChatId = nil
|
||||
self.titleGenerationPending = false
|
||||
self.createdAt = search.createdAt
|
||||
self.updatedAt = search.updatedAt
|
||||
self.starred = search.starred
|
||||
@@ -228,6 +269,8 @@ public struct WorkspaceItem: Codable, Identifiable, Hashable, Sendable {
|
||||
return ChatSummary(
|
||||
id: id,
|
||||
title: title,
|
||||
parentChatId: parentChatId,
|
||||
titleGenerationPending: titleGenerationPending,
|
||||
createdAt: createdAt,
|
||||
updatedAt: updatedAt,
|
||||
starred: starred,
|
||||
@@ -253,6 +296,43 @@ public struct WorkspaceItem: Codable, Identifiable, Hashable, Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
extension WorkspaceItem {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case type
|
||||
case id
|
||||
case title
|
||||
case query
|
||||
case parentChatId
|
||||
case titleGenerationPending
|
||||
case createdAt
|
||||
case updatedAt
|
||||
case starred
|
||||
case starredAt
|
||||
case initiatedProvider
|
||||
case initiatedModel
|
||||
case lastUsedProvider
|
||||
case lastUsedModel
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
type = try container.decode(WorkspaceItemType.self, forKey: .type)
|
||||
id = try container.decode(String.self, forKey: .id)
|
||||
title = try container.decodeIfPresent(String.self, forKey: .title)
|
||||
query = try container.decodeIfPresent(String.self, forKey: .query)
|
||||
parentChatId = try container.decodeIfPresent(String.self, forKey: .parentChatId)
|
||||
titleGenerationPending = try container.decodeIfPresent(Bool.self, forKey: .titleGenerationPending) ?? false
|
||||
createdAt = try container.decode(Date.self, forKey: .createdAt)
|
||||
updatedAt = try container.decode(Date.self, forKey: .updatedAt)
|
||||
starred = try container.decodeIfPresent(Bool.self, forKey: .starred) ?? false
|
||||
starredAt = try container.decodeIfPresent(Date.self, forKey: .starredAt)
|
||||
initiatedProvider = try container.decodeIfPresent(Provider.self, forKey: .initiatedProvider)
|
||||
initiatedModel = try container.decodeIfPresent(String.self, forKey: .initiatedModel)
|
||||
lastUsedProvider = try container.decodeIfPresent(Provider.self, forKey: .lastUsedProvider)
|
||||
lastUsedModel = try container.decodeIfPresent(String.self, forKey: .lastUsedModel)
|
||||
}
|
||||
}
|
||||
|
||||
public struct Message: Codable, Identifiable, Hashable, Sendable {
|
||||
public var id: String
|
||||
public var createdAt: Date
|
||||
@@ -391,6 +471,8 @@ public enum JSONValue: Codable, Hashable, Sendable {
|
||||
public struct ChatDetail: Codable, Identifiable, Hashable, Sendable {
|
||||
public var id: String
|
||||
public var title: String?
|
||||
public var parentChatId: String? = nil
|
||||
public var titleGenerationPending = false
|
||||
public var createdAt: Date
|
||||
public var updatedAt: Date
|
||||
public var starred = false
|
||||
@@ -402,6 +484,41 @@ public struct ChatDetail: Codable, Identifiable, Hashable, Sendable {
|
||||
public var messages: [Message]
|
||||
}
|
||||
|
||||
extension ChatDetail {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case title
|
||||
case parentChatId
|
||||
case titleGenerationPending
|
||||
case createdAt
|
||||
case updatedAt
|
||||
case starred
|
||||
case starredAt
|
||||
case initiatedProvider
|
||||
case initiatedModel
|
||||
case lastUsedProvider
|
||||
case lastUsedModel
|
||||
case messages
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
id = try container.decode(String.self, forKey: .id)
|
||||
title = try container.decodeIfPresent(String.self, forKey: .title)
|
||||
parentChatId = try container.decodeIfPresent(String.self, forKey: .parentChatId)
|
||||
titleGenerationPending = try container.decodeIfPresent(Bool.self, forKey: .titleGenerationPending) ?? false
|
||||
createdAt = try container.decode(Date.self, forKey: .createdAt)
|
||||
updatedAt = try container.decode(Date.self, forKey: .updatedAt)
|
||||
starred = try container.decodeIfPresent(Bool.self, forKey: .starred) ?? false
|
||||
starredAt = try container.decodeIfPresent(Date.self, forKey: .starredAt)
|
||||
initiatedProvider = try container.decodeIfPresent(Provider.self, forKey: .initiatedProvider)
|
||||
initiatedModel = try container.decodeIfPresent(String.self, forKey: .initiatedModel)
|
||||
lastUsedProvider = try container.decodeIfPresent(Provider.self, forKey: .lastUsedProvider)
|
||||
lastUsedModel = try container.decodeIfPresent(String.self, forKey: .lastUsedModel)
|
||||
messages = try container.decode([Message].self, forKey: .messages)
|
||||
}
|
||||
}
|
||||
|
||||
public struct SearchResultItem: Codable, Identifiable, Hashable, Sendable {
|
||||
public var id: String
|
||||
public var createdAt: Date
|
||||
|
||||
@@ -406,14 +406,17 @@ final class SybilViewModel {
|
||||
} else {
|
||||
initiatedLabel = nil
|
||||
}
|
||||
let starOwner = item.parentChatId.flatMap { parentChatID in
|
||||
workspaceItems.first(where: { $0.type == .chat && $0.id == parentChatID })
|
||||
} ?? item
|
||||
|
||||
return SidebarItem(
|
||||
selection: .chat(item.id),
|
||||
kind: .chat,
|
||||
title: chatTitle(title: item.title, messages: nil),
|
||||
updatedAt: item.updatedAt,
|
||||
starred: item.starred,
|
||||
starredAt: item.starredAt,
|
||||
starred: starOwner.starred,
|
||||
starredAt: starOwner.starredAt,
|
||||
initiatedLabel: initiatedLabel,
|
||||
isRunning: isChatRowRunning(item.id)
|
||||
)
|
||||
@@ -687,6 +690,8 @@ final class SybilViewModel {
|
||||
selectedChat = ChatDetail(
|
||||
id: chat.id,
|
||||
title: chat.title,
|
||||
parentChatId: chat.parentChatId,
|
||||
titleGenerationPending: chat.titleGenerationPending,
|
||||
createdAt: chat.createdAt,
|
||||
updatedAt: chat.updatedAt,
|
||||
starred: chat.starred,
|
||||
@@ -896,7 +901,8 @@ final class SybilViewModel {
|
||||
let client = try client()
|
||||
switch selection {
|
||||
case let .chat(chatID):
|
||||
let updated = try await client.updateChatStar(chatID: chatID, starred: starred)
|
||||
let rootChatID = chatFamilyRootID(for: chatID)
|
||||
let updated = try await client.updateChatStar(chatID: rootChatID, starred: starred)
|
||||
applyChatSummary(updated, moveToFront: false)
|
||||
case let .search(searchID):
|
||||
let updated = try await client.updateSearchStar(searchID: searchID, starred: starred)
|
||||
@@ -1454,6 +1460,8 @@ final class SybilViewModel {
|
||||
|
||||
if selectedChat?.id == chat.id {
|
||||
selectedChat?.title = chat.title
|
||||
selectedChat?.parentChatId = chat.parentChatId
|
||||
selectedChat?.titleGenerationPending = chat.titleGenerationPending
|
||||
selectedChat?.updatedAt = chat.updatedAt
|
||||
selectedChat?.starred = chat.starred
|
||||
selectedChat?.starredAt = chat.starredAt
|
||||
@@ -1505,6 +1513,19 @@ final class SybilViewModel {
|
||||
workspaceItems.insert(item, at: 0)
|
||||
}
|
||||
|
||||
private func chatFamilyRootID(for chatID: String) -> String {
|
||||
if let selectedChat, selectedChat.id == chatID, let parentChatID = selectedChat.parentChatId {
|
||||
return parentChatID
|
||||
}
|
||||
if let parentChatID = chats.first(where: { $0.id == chatID })?.parentChatId {
|
||||
return parentChatID
|
||||
}
|
||||
if let parentChatID = workspaceItems.first(where: { $0.type == .chat && $0.id == chatID })?.parentChatId {
|
||||
return parentChatID
|
||||
}
|
||||
return chatID
|
||||
}
|
||||
|
||||
private func attachToVisibleActiveRunIfNeeded() {
|
||||
guard draftKind == nil else {
|
||||
return
|
||||
@@ -1854,6 +1875,8 @@ final class SybilViewModel {
|
||||
selectedChat = ChatDetail(
|
||||
id: created.id,
|
||||
title: created.title,
|
||||
parentChatId: created.parentChatId,
|
||||
titleGenerationPending: created.titleGenerationPending,
|
||||
createdAt: created.createdAt,
|
||||
updatedAt: created.updatedAt,
|
||||
starred: created.starred,
|
||||
@@ -1912,7 +1935,7 @@ final class SybilViewModel {
|
||||
let streamLifecycleGeneration = appLifecycleGeneration
|
||||
let streamStartedWhileInactive = !isAppActive
|
||||
|
||||
if isUntitledChat(chatID: chatID, detail: currentSelectedChat) {
|
||||
if shouldRequestChatTitle(baseChat) {
|
||||
Task { [weak self] in
|
||||
guard let self else { return }
|
||||
do {
|
||||
@@ -2623,20 +2646,10 @@ final class SybilViewModel {
|
||||
)
|
||||
}
|
||||
|
||||
private func isUntitledChat(chatID: String, detail: ChatDetail?) -> Bool {
|
||||
if let detail, detail.id == chatID {
|
||||
if let title = detail.title?.trimmingCharacters(in: .whitespacesAndNewlines), !title.isEmpty {
|
||||
return false
|
||||
}
|
||||
private func shouldRequestChatTitle(_ chat: ChatDetail) -> Bool {
|
||||
if chat.titleGenerationPending {
|
||||
return true
|
||||
}
|
||||
|
||||
if let summary = chats.first(where: { $0.id == chatID }) {
|
||||
if let title = summary.title?.trimmingCharacters(in: .whitespacesAndNewlines), !title.isEmpty {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
return chat.title?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ?? true
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user