Let a queued message be taken back into the composer
This commit is contained in:
@@ -264,6 +264,29 @@ final class ChatModel {
|
||||
}
|
||||
}
|
||||
|
||||
func withdraw(_ p: PendingSend, _ overlays: Overlays) async {
|
||||
guard let taskId, let inputId = p.inputId else {
|
||||
return
|
||||
}
|
||||
let key = "message:\(taskId)"
|
||||
do {
|
||||
var imgs: [PendingImage] = []
|
||||
for id in p.imageIds {
|
||||
let data = try await ImageCache.fetch(.blob(id), store.api)
|
||||
guard let image = NSImage(data: data) else {
|
||||
throw ApiError(status: 0, message: "not an image")
|
||||
}
|
||||
imgs.append(PendingImage(data: data, image: image))
|
||||
}
|
||||
_ = try await store.api.client.withdrawMessage(path: .init(taskId: taskId, inputId: Int64(inputId))).noContent
|
||||
builder.withdrawn(inputId)
|
||||
store.drafts?.restoreText(key, p.text)
|
||||
store.images.restore(key, imgs)
|
||||
} catch {
|
||||
overlays.showAlert(errorText(error))
|
||||
}
|
||||
}
|
||||
|
||||
func interrupt(_ sessionId: String, _ overlays: Overlays) {
|
||||
Task {
|
||||
do {
|
||||
@@ -525,7 +548,7 @@ private struct TranscriptScroll: View {
|
||||
RowView(row: row, model: model, nested: false)
|
||||
}
|
||||
ForEach(model.builder.pending) { p in
|
||||
PendingView(pending: p)
|
||||
PendingView(pending: p, model: model)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -647,13 +670,30 @@ private final class ScannerView: NSView {
|
||||
|
||||
private struct PendingView: View {
|
||||
let pending: PendingSend
|
||||
let model: ChatModel
|
||||
@Environment(Overlays.self) private var overlays
|
||||
@ViewState private var withdrawing = false
|
||||
|
||||
var body: some View {
|
||||
TrailingBubble {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(pending.queued ? "queued…" : "sending…")
|
||||
.font(Fonts.font(11, italic: true))
|
||||
.foregroundStyle(Palette.muted)
|
||||
HStack(spacing: 8) {
|
||||
Text(pending.queued ? "queued…" : "sending…")
|
||||
.font(Fonts.font(11, italic: true))
|
||||
.foregroundStyle(Palette.muted)
|
||||
if pending.inputId != nil {
|
||||
Button("×") {
|
||||
withdrawing = true
|
||||
Task {
|
||||
await model.withdraw(pending, overlays)
|
||||
withdrawing = false
|
||||
}
|
||||
}
|
||||
.buttonStyle(ThumbRemoveStyle())
|
||||
.disabled(withdrawing)
|
||||
.help("Take this message back into the composer")
|
||||
}
|
||||
}
|
||||
if !pending.images.isEmpty {
|
||||
FlowLayout(spacing: 6) {
|
||||
ForEach(pending.images) { img in
|
||||
|
||||
@@ -237,7 +237,7 @@ struct Thumbnails: View {
|
||||
}
|
||||
}
|
||||
|
||||
private struct ThumbRemoveStyle: ButtonStyle {
|
||||
struct ThumbRemoveStyle: ButtonStyle {
|
||||
func makeBody(configuration: ButtonStyleConfiguration) -> some View {
|
||||
ThumbRemove(configuration: configuration)
|
||||
}
|
||||
@@ -371,6 +371,11 @@ struct ComposerView: View {
|
||||
store.drafts?.setComposerImages(draftKey, store.images.images(draftKey).count)
|
||||
reportDraft()
|
||||
}
|
||||
.onChange(of: store.drafts?.composerText(draftKey)) { _, new in
|
||||
if let new, new != text {
|
||||
text = new
|
||||
}
|
||||
}
|
||||
.onChange(of: store.images.images(draftKey).count) {
|
||||
store.drafts?.setComposerImages(draftKey, store.images.images(draftKey).count)
|
||||
}
|
||||
|
||||
@@ -192,15 +192,20 @@ final class Drafts {
|
||||
if ok {
|
||||
lostImages[send.key] = nil
|
||||
} else {
|
||||
let text = [send.text, composerTexts[send.key] ?? ""].filter { !$0.isEmpty }.joined(separator: "\n\n")
|
||||
if !text.isEmpty {
|
||||
composerTexts[send.key] = text
|
||||
}
|
||||
restoreText(send.key, send.text)
|
||||
}
|
||||
dirty = true
|
||||
flush()
|
||||
}
|
||||
|
||||
func restoreText(_ key: String, _ text: String) {
|
||||
let joined = [text, composerTexts[key] ?? ""].filter { !$0.isEmpty }.joined(separator: "\n\n")
|
||||
if !joined.isEmpty {
|
||||
composerTexts[key] = joined
|
||||
}
|
||||
save()
|
||||
}
|
||||
|
||||
func checkedAnswer(_ id: String) -> [String] {
|
||||
checked[id] ?? []
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ final class ImageCache {
|
||||
return img
|
||||
}
|
||||
|
||||
private static func fetch(_ source: ImageSource, _ api: API) async throws -> Data {
|
||||
static func fetch(_ source: ImageSource, _ api: API) async throws -> Data {
|
||||
switch source {
|
||||
case .data(let data):
|
||||
return data
|
||||
|
||||
@@ -135,6 +135,7 @@ struct PendingSend: Identifiable {
|
||||
let local: Bool
|
||||
let queued: Bool
|
||||
let md: [MDBlock]
|
||||
var inputId: Int? = nil
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@@ -231,6 +232,10 @@ final class TranscriptBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
func withdrawn(_ inputId: Int) {
|
||||
pending.removeAll { $0.inputId == inputId }
|
||||
}
|
||||
|
||||
private func confirmPending(_ text: String, queued: Bool, local: Bool) {
|
||||
let i = queued ? pending.firstIndex { !$0.local && $0.text == text } : nil
|
||||
if let i = i ?? (local ? pending.firstIndex { $0.local && $0.text == text } : nil) {
|
||||
@@ -417,7 +422,13 @@ final class TranscriptBuilder {
|
||||
confirmPending(text, queued: false, local: true)
|
||||
pendingIds += 1
|
||||
let images = (p["images"] as? [(any Sendable)?] ?? []).map { jsString($0) }
|
||||
pending.append(PendingSend(id: pendingIds, text: text, images: [], imageIds: images, local: false, queued: true, md: trimmed(text).isEmpty ? [] : parseMarkdown(text)))
|
||||
pending.append(PendingSend(id: pendingIds, text: text, images: [], imageIds: images, local: false, queued: true, md: trimmed(text).isEmpty ? [] : parseMarkdown(text), inputId: int(p, "id")))
|
||||
}
|
||||
continue
|
||||
}
|
||||
if ev.kind == "user.unqueued" {
|
||||
if sink == nil, let id = int(p, "id") {
|
||||
withdrawn(id)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user