adds the ability to rename chats
This commit is contained in:
@@ -74,6 +74,16 @@ actor SybilAPIClient: SybilAPIClienting {
|
||||
return response.chat
|
||||
}
|
||||
|
||||
func updateChatTitle(chatID: String, title: String) async throws -> ChatSummary {
|
||||
let response = try await request(
|
||||
"/v1/chats/\(chatID)",
|
||||
method: "PATCH",
|
||||
body: AnyEncodable(ChatTitleUpdateBody(title: title)),
|
||||
responseType: ChatCreateResponse.self
|
||||
)
|
||||
return response.chat
|
||||
}
|
||||
|
||||
func deleteChat(chatID: String) async throws {
|
||||
_ = try await request("/v1/chats/\(chatID)", method: "DELETE", responseType: DeleteResponse.self)
|
||||
}
|
||||
@@ -640,6 +650,10 @@ private struct ChatCreateBody: Encodable {
|
||||
var messages: [CompletionRequestMessage]?
|
||||
}
|
||||
|
||||
private struct ChatTitleUpdateBody: Encodable {
|
||||
var title: String
|
||||
}
|
||||
|
||||
private struct SearchCreateBody: Encodable {
|
||||
var title: String?
|
||||
var query: String?
|
||||
|
||||
@@ -11,6 +11,7 @@ protocol SybilAPIClienting: Sendable {
|
||||
messages: [CompletionRequestMessage]?
|
||||
) async throws -> ChatSummary
|
||||
func getChat(chatID: String) async throws -> ChatDetail
|
||||
func updateChatTitle(chatID: String, title: String) async throws -> ChatSummary
|
||||
func deleteChat(chatID: String) async throws
|
||||
func suggestChatTitle(chatID: String, content: String) async throws -> ChatSummary
|
||||
func listSearches() async throws -> [SearchSummary]
|
||||
|
||||
@@ -111,56 +111,100 @@ struct SybilSidebarItemList: View {
|
||||
@Bindable var viewModel: SybilViewModel
|
||||
var isSelected: (SidebarItem) -> Bool
|
||||
var onSelect: (SidebarItem) -> Void
|
||||
@State private var renameTarget: SidebarItem?
|
||||
@State private var renameTitle = ""
|
||||
|
||||
private var isRenameAlertPresented: Binding<Bool> {
|
||||
Binding {
|
||||
renameTarget != nil
|
||||
} set: { isPresented in
|
||||
if !isPresented {
|
||||
renameTarget = nil
|
||||
renameTitle = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if viewModel.isLoadingCollections && viewModel.sidebarItems.isEmpty {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
ProgressView()
|
||||
.tint(SybilTheme.primary)
|
||||
Text("Loading conversations…")
|
||||
.font(.sybil(.footnote))
|
||||
.foregroundStyle(SybilTheme.textMuted)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
||||
.padding(16)
|
||||
} else if viewModel.sidebarItems.isEmpty {
|
||||
VStack(spacing: 10) {
|
||||
Image(systemName: "message.badge")
|
||||
.font(.system(size: 20, weight: .medium))
|
||||
.foregroundStyle(SybilTheme.textMuted)
|
||||
Text("Start a chat or run your first search.")
|
||||
.font(.sybil(.footnote))
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundStyle(SybilTheme.textMuted)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.padding(16)
|
||||
} else {
|
||||
ScrollView {
|
||||
LazyVStack(alignment: .leading, spacing: 8) {
|
||||
ForEach(viewModel.sidebarItems) { item in
|
||||
Button {
|
||||
onSelect(item)
|
||||
} label: {
|
||||
SybilSidebarRow(item: item, isSelected: isSelected(item))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.contextMenu {
|
||||
Button(role: .destructive) {
|
||||
Task {
|
||||
await viewModel.deleteItem(item.selection)
|
||||
}
|
||||
Group {
|
||||
if viewModel.isLoadingCollections && viewModel.sidebarItems.isEmpty {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
ProgressView()
|
||||
.tint(SybilTheme.primary)
|
||||
Text("Loading conversations…")
|
||||
.font(.sybil(.footnote))
|
||||
.foregroundStyle(SybilTheme.textMuted)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
||||
.padding(16)
|
||||
} else if viewModel.sidebarItems.isEmpty {
|
||||
VStack(spacing: 10) {
|
||||
Image(systemName: "message.badge")
|
||||
.font(.system(size: 20, weight: .medium))
|
||||
.foregroundStyle(SybilTheme.textMuted)
|
||||
Text("Start a chat or run your first search.")
|
||||
.font(.sybil(.footnote))
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundStyle(SybilTheme.textMuted)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.padding(16)
|
||||
} else {
|
||||
ScrollView {
|
||||
LazyVStack(alignment: .leading, spacing: 8) {
|
||||
ForEach(viewModel.sidebarItems) { item in
|
||||
Button {
|
||||
onSelect(item)
|
||||
} label: {
|
||||
Label("Delete", systemImage: "trash")
|
||||
SybilSidebarRow(item: item, isSelected: isSelected(item))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.contextMenu {
|
||||
if item.kind == .chat {
|
||||
Button {
|
||||
renameTarget = item
|
||||
renameTitle = item.title
|
||||
} label: {
|
||||
Label("Rename", systemImage: "pencil")
|
||||
}
|
||||
}
|
||||
|
||||
Button(role: .destructive) {
|
||||
Task {
|
||||
await viewModel.deleteItem(item.selection)
|
||||
}
|
||||
} label: {
|
||||
Label("Delete", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(10)
|
||||
}
|
||||
.refreshable {
|
||||
await viewModel.refreshSidebarCollectionsFromPullToRefresh()
|
||||
}
|
||||
.padding(10)
|
||||
}
|
||||
.refreshable {
|
||||
await viewModel.refreshSidebarCollectionsFromPullToRefresh()
|
||||
}
|
||||
.alert("Rename Chat", isPresented: isRenameAlertPresented) {
|
||||
TextField("Title", text: $renameTitle)
|
||||
Button("Cancel", role: .cancel) {
|
||||
renameTarget = nil
|
||||
renameTitle = ""
|
||||
}
|
||||
Button("Save") {
|
||||
let target = renameTarget
|
||||
let title = renameTitle
|
||||
renameTarget = nil
|
||||
renameTitle = ""
|
||||
|
||||
if let target, case let .chat(chatID) = target.selection {
|
||||
Task {
|
||||
await viewModel.renameChat(chatID: chatID, title: title)
|
||||
}
|
||||
}
|
||||
}
|
||||
.disabled(renameTitle.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -851,6 +851,40 @@ final class SybilViewModel {
|
||||
}
|
||||
}
|
||||
|
||||
func renameChat(chatID: String, title: String) async {
|
||||
guard isAuthenticated else {
|
||||
return
|
||||
}
|
||||
|
||||
let trimmedTitle = title.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmedTitle.isEmpty else {
|
||||
errorMessage = "Enter a chat title."
|
||||
return
|
||||
}
|
||||
|
||||
SybilLog.info(SybilLog.ui, "Renaming chat \(chatID)")
|
||||
errorMessage = nil
|
||||
|
||||
do {
|
||||
let updated = try await client().updateChatTitle(chatID: chatID, title: trimmedTitle)
|
||||
chats.removeAll(where: { $0.id == updated.id })
|
||||
chats.insert(updated, at: 0)
|
||||
upsertWorkspaceChat(updated)
|
||||
|
||||
if selectedChat?.id == updated.id {
|
||||
selectedChat?.title = updated.title
|
||||
selectedChat?.updatedAt = updated.updatedAt
|
||||
selectedChat?.initiatedProvider = updated.initiatedProvider
|
||||
selectedChat?.initiatedModel = updated.initiatedModel
|
||||
selectedChat?.lastUsedProvider = updated.lastUsedProvider
|
||||
selectedChat?.lastUsedModel = updated.lastUsedModel
|
||||
}
|
||||
} catch {
|
||||
errorMessage = normalizeAPIError(error)
|
||||
SybilLog.error(SybilLog.ui, "Rename failed", error: error)
|
||||
}
|
||||
}
|
||||
|
||||
func refreshAfterSettingsChange() async {
|
||||
SybilLog.info(SybilLog.ui, "Settings changed, reconnecting")
|
||||
settings.persist()
|
||||
|
||||
Reference in New Issue
Block a user