@narumitw/pi-webui 0.20.2 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,11 +1,17 @@
1
1
  import { renderMarkdown } from "./markdown.js";
2
2
 
3
- export function createTranscriptRenderer({ documentRef = document, list }) {
3
+ export function createTranscriptRenderer({
4
+ documentRef = document,
5
+ list,
6
+ onReattach = () => undefined,
7
+ onForget = () => undefined,
8
+ }) {
4
9
  const messages = new Map();
5
10
 
6
11
  return {
7
- render(nextMessages, tools) {
12
+ render(nextMessages, tools, sentImages = { items: [] }) {
8
13
  const toolById = new Map(tools.map((tool) => [tool.id, tool]));
14
+ const retainedImageIds = new Set((sentImages.items ?? []).map((item) => item.id));
9
15
  const retained = new Set();
10
16
  const changed = [];
11
17
  let cursor = list.firstChild;
@@ -16,7 +22,18 @@ export function createTranscriptRenderer({ documentRef = document, list }) {
16
22
  view = createMessageView(message.role, documentRef);
17
23
  messages.set(message.id, view);
18
24
  }
19
- if (updateMessageView(view, message, toolById, documentRef)) changed.push(message.id);
25
+ if (
26
+ updateMessageView(
27
+ view,
28
+ message,
29
+ toolById,
30
+ retainedImageIds,
31
+ { onReattach, onForget },
32
+ documentRef,
33
+ )
34
+ ) {
35
+ changed.push(message.id);
36
+ }
20
37
  if (view.node !== cursor) list.insertBefore(view.node, cursor);
21
38
  cursor = view.node.nextSibling;
22
39
  }
@@ -41,6 +58,11 @@ export function toolPhaseLabel(tool) {
41
58
  return "Requested";
42
59
  }
43
60
 
61
+ export function retainedImageStatus(block, retainedImageIds) {
62
+ if (!block?.retainedImageId) return "none";
63
+ return retainedImageIds?.has(block.retainedImageId) ? "eligible" : "expired";
64
+ }
65
+
44
66
  export function toolCommandPreview(tool) {
45
67
  const command = tool?.args?.command;
46
68
  if (typeof command !== "string") return "";
@@ -67,7 +89,7 @@ function createMessageView(role, documentRef) {
67
89
  return { node, heading, body, blocks: new Map(), role: "", final: undefined };
68
90
  }
69
91
 
70
- function updateMessageView(view, message, toolById, documentRef) {
92
+ function updateMessageView(view, message, toolById, retainedImageIds, actions, documentRef) {
71
93
  let changed = false;
72
94
  const role = knownRole(message.role);
73
95
  if (view.role !== role) {
@@ -93,7 +115,18 @@ function updateMessageView(view, message, toolById, documentRef) {
93
115
  view.blocks.set(key, blockView);
94
116
  changed = true;
95
117
  }
96
- if (updateBlockView(blockView, block, toolById.get(block.id), documentRef)) changed = true;
118
+ if (
119
+ updateBlockView(
120
+ blockView,
121
+ block,
122
+ toolById.get(block.id),
123
+ retainedImageIds,
124
+ actions,
125
+ documentRef,
126
+ )
127
+ ) {
128
+ changed = true;
129
+ }
97
130
  if (blockView.node !== cursor) view.body.insertBefore(blockView.node, cursor);
98
131
  cursor = blockView.node.nextSibling;
99
132
  }
@@ -141,7 +174,7 @@ function createBlockView(block, documentRef) {
141
174
  return { type: block.type, node, value: undefined };
142
175
  }
143
176
 
144
- function updateBlockView(view, block, tool, documentRef) {
177
+ function updateBlockView(view, block, tool, retainedImageIds, actions, documentRef) {
145
178
  if (block.type === "text") {
146
179
  if (view.value === block.text) return false;
147
180
  view.value = block.text;
@@ -155,10 +188,34 @@ function updateBlockView(view, block, tool, documentRef) {
155
188
  return true;
156
189
  }
157
190
  if (block.type === "image") {
191
+ const status = retainedImageStatus(block, retainedImageIds);
158
192
  const label = `Image${block.mimeType ? ` · ${block.mimeType}` : ""}`;
159
- if (view.value === label) return false;
160
- view.value = label;
161
- view.node.textContent = label;
193
+ const value = `${label}:${status}`;
194
+ if (view.value === value) return false;
195
+ view.value = value;
196
+ const text = documentRef.createElement("span");
197
+ text.textContent = label;
198
+ view.node.replaceChildren(text);
199
+ if (status === "eligible") {
200
+ const attach = documentRef.createElement("button");
201
+ attach.type = "button";
202
+ attach.className = "message-image-action";
203
+ attach.textContent = "Attach again";
204
+ attach.setAttribute("aria-label", `Attach image again: ${label}`);
205
+ attach.addEventListener("click", () => actions.onReattach(block.retainedImageId));
206
+ const forget = documentRef.createElement("button");
207
+ forget.type = "button";
208
+ forget.className = "message-image-action subtle";
209
+ forget.textContent = "Forget";
210
+ forget.setAttribute("aria-label", `Forget retained image: ${label}`);
211
+ forget.addEventListener("click", () => actions.onForget(block.retainedImageId));
212
+ view.node.append(attach, forget);
213
+ } else if (status === "expired") {
214
+ const expired = documentRef.createElement("span");
215
+ expired.className = "message-image-expired";
216
+ expired.textContent = "Expired";
217
+ view.node.append(expired);
218
+ }
162
219
  return true;
163
220
  }
164
221
  if (block.type === "toolCall") return updateToolView(view, block, tool);