PageSourceSearch

https://vip.xiaoxiaodong.ai/image-conversation-directory.js

js xiaoxiaodong.ai collected 2026-10-03 23:20:37 UTC 16,830 bytes, 414 lines download raw bytes

1// Generated from src/app/image-generator/conversation-directory.js. Do not edit directly.
2
3// src/app/loading-state.js
4var SKELETON_WATERFALL_RATIOS = Object.freeze(["4 / 5", "1 / 1", "3 / 4", "4 / 5", "2 / 3", "1 / 1"]);
5var CONTENT_SKELETON_COUNTS = Object.freeze({ cards: 6, list: 6, text: 7 });
6function createContentSkeleton(document, { layout = "cards", count, ratio = "" } = {}) {
7  const shape = ["cards", "list", "text"].includes(layout) ? layout : "cards";
8  const skeleton = document.createElement("div");
9  skeleton.className = `ui-loading-skeleton content-skeleton content-skeleton--${shape}`;
10  skeleton.setAttribute("aria-hidden", "true");
11  if (shape === "cards" && ratio) skeleton.style.setProperty("--content-skeleton-ratio", String(ratio));
12  const total = Math.max(1, Number(count) || CONTENT_SKELETON_COUNTS[shape]);
13  for (let index = 0; index < total; index += 1) {
14    if (shape === "text") {
15      skeleton.append(document.createElement("b"));
16      continue;
17    }
18    const item = document.createElement("span");
19    item.className = "content-skeleton-item";
20    item.append(document.createElement("i"), document.createElement("b"), document.createElement("b"));
21    skeleton.append(item);
22  }
23  return skeleton;
24}
25
26// src/app/image-generator/conversation-directory.js
27function createConversationDirectory({
28  document,
29  window,
30  state,
31  host,
32  apiClient,
33  t,
34  currentId = () => "",
35  open = () => {
36  },
37  allWorksHref = "/image-generator/general",
38  notify = () => {
39  },
40  previewLegacy = () => {
41  },
42  metadataChanged = () => {
43  },
44  pendingConversations = () => []
45}) {
46  const accountScope = () => String(state.me?.user_id || state.me?.id || state.me?.email || "");
47  const active = () => state.page === "image-generator" && state.generatedImageMode === "general";
48  const localePrefix = () => window.location.pathname.match(/^\/(en|zh-hant)(?:\/|$)/)?.[0]?.replace(/\/$/, "") || "";
49  let account = "", epoch = 0, rows = [], cursor = "", query = "", archived = false, legacy = false;
50  let loading = false, loaded = false, error = "", entered = false;
51  let pane, body, list, search, status, more, archiveToggle, legacyToggle, allWorks;
52  let debounce = 0, refreshTimer = 0, invalidationTimer = 0, lastRead = 0;
53  const nodes = /* @__PURE__ */ new Map(), pending = /* @__PURE__ */ new Set();
54  const button = (label, handler, className = "") => {
55    const el = document.createElement("button");
56    el.type = "button";
57    el.textContent = t(label);
58    el.className = className;
59    el.addEventListener("click", handler);
60    return el;
61  };
62  function restart() {
63    epoch++;
64    loading = loaded = false;
65    rows = [];
66    cursor = "";
67  }
68  function mount() {
69    if (pane) return;
70    pane = document.createElement("div");
71    pane.className = "image-conversation-directory";
72    pane.id = "imageConversationDirectory";
73    pane.setAttribute("aria-label", t("作图记录"));
74    const head = document.createElement("div");
75    head.className = "image-conversation-directory-head";
76    search = document.createElement("input");
77    search.type = "search";
78    search.placeholder = t("搜索作图记录");
79    search.setAttribute("aria-label", t("搜索对话标题"));
80    search.maxLength = 160;
81    search.addEventListener("input", () => {
82      query = search.value.trim();
83      restart();
84      window.clearTimeout(debounce);
85      debounce = window.setTimeout(() => void load(), 250);
86    });
87    archiveToggle = button("已归档", () => {
88      archived = !archived;
89      legacy = false;
90      legacyToggle.setAttribute("aria-pressed", "false");
91      archiveToggle.setAttribute("aria-pressed", String(archived));
92      restart();
93      void load();
94    });
95    archiveToggle.setAttribute("aria-pressed", "false");
96    legacyToggle = button("较早记录", () => {
97      legacy = !legacy;
98      archived = false;
99      archiveToggle.setAttribute("aria-pressed", "false");
100      legacyToggle.setAttribute("aria-pressed", String(legacy));
101      restart();
102      void load();
103    });
104    legacyToggle.setAttribute("aria-pressed", "false");
105    legacyToggle.hidden = !apiClient.getLegacyImageConversations;
106    const filters = document.createElement("div");
107    filters.className = "image-conversation-directory-tools";
108    filters.append(archiveToggle, legacyToggle);
109    head.append(search, filters);
110    body = document.createElement("div");
111    body.className = "image-conversation-directory-body";
112    allWorks = document.createElement("a");
113    allWorks.className = "image-conversation-directory-all";
114    allWorks.textContent = t("全部作品");
115    status = document.createElement("p");
116    status.className = "image-conversation-directory-status";
117    status.setAttribute("role", "status");
118    list = document.createElement("div");
119    list.className = "image-conversation-directory-list";
120    more = button("加载更多", () => void load(Boolean(cursor)), "image-conversation-directory-more");
121    body.append(allWorks, status, list, more);
122    pane.append(head, body);
123    host.append(pane);
124  }
125  async function load(append = false) {
126    if (!account || !active() || loading) return;
127    const stamp = ++epoch, owner = account;
128    loading = true;
129    error = "";
130    render();
131    try {
132      const fetch = legacy ? apiClient.getLegacyImageConversations : apiClient.getImageConversations;
133      const data = await fetch({ limit: 30, q: query, archived: archived ? "1" : "0", ...append && cursor ? { cursor } : {} });
134      if (stamp !== epoch || owner !== accountScope()) return;
135      const unique = new Map((append ? rows : []).map((row) => [row.id, row]));
136      for (const row of data.conversations || []) unique.set(row.id, row);
137      rows = [...unique.values()];
138      cursor = data.nextCursor || "";
139      loaded = true;
140      lastRead = Date.now();
141    } catch {
142      if (stamp === epoch && owner === accountScope()) error = t("作图记录暂时无法加载,请重试。");
143    } finally {
144      if (stamp === epoch && owner === accountScope()) {
145        loading = false;
146        render();
147        window.clearTimeout(refreshTimer);
148        refreshTimer = window.setTimeout(() => {
149          if (active() && document.visibilityState !== "hidden" && rows.length <= 30 && !pending.size) void load();
150        }, 3e4);
151      }
152    }
153  }
154  async function mutate(row, patch, controls) {
155    if (pending.has(row.id)) return;
156    const owner = account, stamp = epoch;
157    pending.add(row.id);
158    controls.forEach((el) => {
159      el.disabled = true;
160    });
161    try {
162      const result = await apiClient.updateImageConversation(row.id, { ...patch, revision: row.revision });
163      if (owner !== accountScope() || account !== owner) return;
164      Object.assign(row, result.conversation || result);
165      const live = rows.find((item) => item.id === row.id);
166      if (live && live !== row) Object.assign(live, row);
167      metadataChanged(row);
168      if (Object.hasOwn(patch, "archived") && row.archived !== archived) rows = rows.filter((item) => item.id !== row.id);
169      const restoreFocus = nodes.get(row.id)?.root.contains(document.activeElement);
170      nodes.get(row.id)?.root.remove();
171      nodes.delete(row.id);
172      render();
173      if (restoreFocus) (nodes.get(row.id)?.link || search).focus();
174    } catch {
175      if (owner !== accountScope()) return;
176      notify(t("修改结果暂未确认,正在重新读取记录。"));
177      if (stamp === epoch) await load();
178    } finally {
179      if (owner === account) {
180        pending.delete(row.id);
181        controls.forEach((el) => {
182          el.disabled = false;
183        });
184      }
185    }
186  }
187  async function continueLegacy(row, control) {
188    if (pending.has(row.id)) return;
189    const owner = account;
190    const receiptKey = `xxd:image-conversation:legacy:${owner}:${row.id}`;
191    let id;
192    try {
193      id = window.localStorage.getItem(receiptKey);
194      if (!/^[0-9a-f-]{36}$/i.test(id || "")) {
195        id = window.crypto.randomUUID();
196        window.localStorage.setItem(receiptKey, id);
197      }
198    } catch {
199      notify(t("无法保存恢复凭据,请允许本地存储后重试。"));
200      return;
201    }
202    pending.add(row.id);
203    control.disabled = true;
204    try {
205      const result = await apiClient.attachImageConversationTask(id, row.taskId);
206      if (owner !== accountScope()) return;
207      rows = rows.filter((item) => item.id !== row.id);
208      render();
209      open(result.id || id);
210    } catch (error2) {
211      if (owner !== accountScope()) return;
212      if (error2?.payload?.conversationId) {
213        rows = rows.filter((item) => item.id !== row.id);
214        render();
215        open(error2.payload.conversationId);
216      } else notify(t("记录恢复结果尚未确认,请再次点击继续核对;不会自动生成。"));
217    } finally {
218      if (owner === account) {
219        pending.delete(row.id);
220        control.disabled = false;
221      }
222    }
223  }
224  function rowNode(row) {
225    const root = document.createElement("article");
226    root.className = "image-conversation-directory-row";
227    root.dataset.conversationId = row.id;
228    const link = row.legacy ? button("", () => previewLegacy(row.taskId)) : document.createElement("a");
229    if (!row.legacy) {
230      link.href = `${localePrefix()}/image-generator/c/${row.id}`;
231      link.addEventListener("click", (event) => {
232        if (event.button || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
233        event.preventDefault();
234        open(row.id);
235      });
236    }
237    link.className = "image-conversation-directory-link";
238    const name = document.createElement("span");
239    name.className = "image-conversation-directory-title";
240    name.setAttribute("data-locale-original", "");
241    const note = document.createElement("span");
242    note.className = "image-conversation-directory-note";
243    link.append(name, note);
244    if (row.legacy) {
245      const resume = button("ç»§ç»­", () => void continueLegacy(row, resume));
246      resume.title = t("继续这次创作");
247      root.classList.add("is-legacy");
248      root.append(link, resume);
249      return { root, link, name, note, row };
250    }
251    if (row.optimistic) {
252      root.append(link);
253      return { root, link, name, note, row };
254    }
255    const menu = document.createElement("details");
256    menu.className = "image-conversation-directory-menu";
257    const summary = document.createElement("summary");
258    summary.textContent = "…";
259    summary.setAttribute("aria-label", t("对话操作"));
260    const edit = button("重命名", () => {
261      menu.open = false;
262      if (root.querySelector("form")) return;
263      const form = document.createElement("form");
264      form.className = "image-conversation-directory-rename";
265      const input = document.createElement("input");
266      input.value = row.title;
267      input.maxLength = 160;
268      input.setAttribute("aria-label", t("对话标题"));
269      const cancel = button("取消", () => {
270        form.remove();
271        summary.focus();
272      }, "ui-dialog-cancel");
273      const save = button("保存", () => {
274      });
275      save.type = "submit";
276      form.addEventListener("submit", (event) => {
277        event.preventDefault();
278        if (input.value.trim()) void mutate(row, { title: input.value.trim() }, [input, save, cancel]);
279        else input.focus();
280      });
281      form.append(input, cancel, save);
282      root.append(form);
283      input.focus();
284      input.select();
285    });
286    const archive = button(row.archived ? "恢复记录" : "归档", () => void mutate(row, { archived: !row.archived }, [archive, edit]));
287    menu.append(summary, edit, archive);
288    root.append(link, menu);
289    return { root, link, name, note, row };
290  }
291  function render() {
292    if (!pane) return;
293    allWorks.href = `${localePrefix()}${allWorksHref}`;
294    allWorks.setAttribute("aria-current", active() ? "page" : "false");
295    const temporary = !archived && !legacy ? pendingConversations().filter((row) => !rows.some((item) => item.id === row.id) && (!query || row.title.includes(query))) : [];
296    const visibleRows = [...temporary, ...rows];
297    status.textContent = error || (loading && !visibleRows.length ? t("正在加载作图记录…") : !visibleRows.length ? t(query ? "没有匹配的对话" : legacy ? "暂无较早记录。" : archived ? "暂无归档记录" : "首次发送后,创作对话会出现在这里。") : "");
298    status.hidden = !status.textContent;
299    const firstPagePending = loading && !visibleRows.length && !error;
300    status.classList.toggle("sr-only", firstPagePending);
301    if (firstPagePending && !status.nextElementSibling?.classList?.contains("content-skeleton")) {
302      status.after(createContentSkeleton(document, { layout: "list", count: 6 }));
303    } else if (!firstPagePending && status.nextElementSibling?.classList?.contains("content-skeleton")) {
304      status.nextElementSibling.remove();
305    }
306    more.hidden = !cursor && !error;
307    more.disabled = loading;
308    more.textContent = t(error ? "重试" : "加载更多");
309    const wanted = new Set(visibleRows.map((row) => row.id));
310    for (const [id, node] of nodes) if (!wanted.has(id)) {
311      node.root.remove();
312      nodes.delete(id);
313    }
314    const headers = new Map([...list.querySelectorAll(".image-conversation-directory-date")].map((el) => [el.dataset.group, el]));
315    const ordered = [];
316    let previousGroup = "";
317    for (const row of visibleRows) {
318      let node = nodes.get(row.id);
319      if (!node) {
320        node = rowNode(row);
321        nodes.set(row.id, node);
322      }
323      if (node.row.optimistic && !row.optimistic) {
324        node.root.remove();
325        node = rowNode(row);
326        nodes.set(row.id, node);
327      }
328      Object.assign(node.row, row);
329      node.name.textContent = row.title || t("未命名创作");
330      node.link.title = node.name.textContent;
331      const progress = row.optimistic ? t("正在确认提交") : row.activeCount ? `${t("生成中")} · ${row.activeCount}` : `${row.taskCount} ${t("轮作图")}`;
332      node.note.textContent = [
333        progress,
334        row.attentionCount ? t("有任务需处理") : "",
335        new Date(row.lastActivityAt).toLocaleTimeString(void 0, { hour: "2-digit", minute: "2-digit" })
336      ].filter(Boolean).join(" · ");
337      node.root.classList.toggle("is-running", Boolean(row.activeCount && !row.optimistic));
338      node.link.setAttribute("aria-current", row.id === currentId() ? "page" : "false");
339      const date = new Date(row.lastActivityAt), today = /* @__PURE__ */ new Date();
340      today.setHours(0, 0, 0, 0);
341      const yesterday = new Date(today);
342      yesterday.setDate(yesterday.getDate() - 1);
343      const group = date >= today ? "今天" : date >= yesterday ? "昨天" : "更早";
344      if (group !== previousGroup) {
345        const label = headers.get(group) || document.createElement("h3");
346        label.className = "image-conversation-directory-date";
347        label.dataset.group = group;
348        label.textContent = t(group);
349        ordered.push(label);
350        previousGroup = group;
351      }
352      ordered.push(node.root);
353    }
354    const keep = new Set(ordered);
355    for (const child of [...list.children]) if (!keep.has(child)) child.remove();
356    ordered.forEach((child, index) => {
357      if (list.children[index] !== child) list.insertBefore(child, list.children[index] || null);
358    });
359  }
360  function sync() {
361    const next = accountScope();
362    if (next !== account) {
363      account = next;
364      restart();
365      query = error = "";
366      entered = false;
367      pending.clear();
368      nodes.clear();
369      list?.replaceChildren();
370      if (search) search.value = "";
371      archived = legacy = false;
372      legacyToggle?.setAttribute("aria-pressed", "false");
373      archiveToggle?.setAttribute("aria-pressed", "false");
374    }
375    if (!host || !apiClient?.getImageConversations || !account) {
376      if (pane) pane.hidden = true;
377      return;
378    }
379    mount();
380    pane.hidden = !active();
381    if (!active()) {
382      if (entered) {
383        epoch++;
384        loading = false;
385        window.clearTimeout(refreshTimer);
386      }
387      entered = false;
388      return;
389    }
390    if (!entered) {
391      entered = true;
392      if (!loaded || rows.length <= 30 && Date.now() - lastRead > 3e4) void load();
393    }
394    render();
395  }
396  window.addEventListener("focus", () => {
397    if (active() && pane && Date.now() - lastRead > 3e4 && rows.length <= 30) void load();
398  });
399  return {
400    sync,
401    isOpen: () => Boolean(pane && !pane.hidden && active()),
402    invalidate: () => {
403      if (invalidationTimer) return;
404      invalidationTimer = window.setTimeout(() => {
405        invalidationTimer = 0;
406        lastRead = 0;
407        if (active() && rows.length <= 30 && !pending.size) void load();
408      }, 500);
409    }
410  };
411}
412export {
413  createConversationDirectory
414};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.