mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-25 14:06:18 +00:00
* feat(plugins): support manifest-backed browser assets * fix(plugins): validate asset roots and clarify module timeouts * fix(plugins): sandbox asset documents and support Turbo imports * docs(gateway): keep guidance within the merged size budget --------- Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
217 lines
6.8 KiB
JavaScript
217 lines
6.8 KiB
JavaScript
// Original web adaptation of Pi's bookmark concept. No React/DeerFlow imports.
|
|
export function mountBookmarks(root, context) {
|
|
const zh = context.locale.startsWith("zh");
|
|
const words = zh
|
|
? {
|
|
heading: "留住值得再看的回答",
|
|
description:
|
|
"从会话菜单收藏回答,再回到这里查找。只有你可以访问这些书签。",
|
|
search: "搜索书签",
|
|
submit: "搜索",
|
|
empty: "还没有匹配的书签",
|
|
open: "返回会话",
|
|
rename: "保存名称",
|
|
remove: "删除",
|
|
confirm: "确认删除",
|
|
cancel: "取消",
|
|
expand: "查看完整内容",
|
|
loading: "正在加载…",
|
|
error: "操作未完成,请重试。",
|
|
label: "书签名称",
|
|
}
|
|
: {
|
|
heading: "Keep answers worth returning to",
|
|
description:
|
|
"Save an answer from the conversation menu. Your bookmarks are private to your account.",
|
|
search: "Search bookmarks",
|
|
submit: "Search",
|
|
empty: "No matching bookmarks",
|
|
open: "Open conversation",
|
|
rename: "Save name",
|
|
remove: "Delete",
|
|
confirm: "Confirm delete",
|
|
cancel: "Cancel",
|
|
expand: "Read full answer",
|
|
loading: "Loading…",
|
|
error: "Could not complete the action. Please retry.",
|
|
label: "Bookmark name",
|
|
};
|
|
const make = (tag, content, attributes = {}) => {
|
|
const element = document.createElement(tag);
|
|
if (content) element.textContent = content;
|
|
for (const [key, value] of Object.entries(attributes))
|
|
element.setAttribute(key, value);
|
|
return element;
|
|
};
|
|
const style = make("link", "", {
|
|
rel: "stylesheet",
|
|
crossorigin: "use-credentials",
|
|
href: new URL("../styles.css", import.meta.url).href,
|
|
});
|
|
const intro = make("div", "", { class: "intro" });
|
|
intro.append(
|
|
make("img", "", {
|
|
crossorigin: "use-credentials",
|
|
src: new URL("../bookmark.svg", import.meta.url).href,
|
|
alt: "",
|
|
width: "28",
|
|
height: "28",
|
|
}),
|
|
make("h3", words.heading),
|
|
make("p", words.description),
|
|
);
|
|
const form = make("form");
|
|
const query = make("input", "", {
|
|
type: "search",
|
|
placeholder: words.search,
|
|
"aria-label": words.search,
|
|
maxlength: "200",
|
|
});
|
|
const search = make("button", words.submit, {
|
|
type: "submit",
|
|
class: "search-button",
|
|
});
|
|
form.append(query, search);
|
|
const status = make("p", "", { role: "status" });
|
|
const error = make("p", "", { role: "alert" });
|
|
const list = make("div");
|
|
root.append(style, intro, form, status, error, list);
|
|
let generation = 0;
|
|
let disposed = false;
|
|
const active = () => !disposed && !context.signal.aborted;
|
|
async function run(button, operation) {
|
|
button.disabled = true;
|
|
error.textContent = "";
|
|
try {
|
|
await operation();
|
|
} catch {
|
|
if (active()) error.textContent = words.error;
|
|
} finally {
|
|
if (active()) button.disabled = false;
|
|
}
|
|
}
|
|
async function load() {
|
|
const current = ++generation;
|
|
status.textContent = words.loading;
|
|
try {
|
|
const data = await context.callBackend("search", { query: query.value });
|
|
if (!active() || current !== generation) return;
|
|
status.textContent = data.total
|
|
? zh
|
|
? `共 ${data.total} 条 · 显示前 10 条,可搜索缩小范围`
|
|
: `${data.total} saved · Showing up to 10; search to narrow results`
|
|
: words.empty;
|
|
list.replaceChildren();
|
|
for (const item of data.items) {
|
|
const card = make("article");
|
|
const row = make("div", "", { class: "name" });
|
|
const label = make("input", "", {
|
|
"aria-label": words.label,
|
|
maxlength: "120",
|
|
});
|
|
label.value = item.label;
|
|
const rename = make("button", words.rename, { type: "button" });
|
|
rename.addEventListener(
|
|
"click",
|
|
() =>
|
|
void run(rename, async () => {
|
|
await context.callBackend("rename", {
|
|
id: item.id,
|
|
label: label.value,
|
|
});
|
|
await load();
|
|
}),
|
|
{ signal: context.signal },
|
|
);
|
|
row.append(label, rename);
|
|
const content = make("pre", item.text);
|
|
const actions = make("div", "", { class: "actions" });
|
|
// The host resolves current routing metadata, including for old bookmarks.
|
|
const open = make("button", words.open, { type: "button" });
|
|
open.addEventListener(
|
|
"click",
|
|
() =>
|
|
void run(open, async () => {
|
|
if (!context.openConversation)
|
|
throw new Error("Host conversation navigation is unavailable");
|
|
await context.openConversation(item.thread_id);
|
|
}),
|
|
{ signal: context.signal },
|
|
);
|
|
actions.append(open);
|
|
if (item.truncated) {
|
|
const expand = make("button", words.expand, { type: "button" });
|
|
expand.addEventListener(
|
|
"click",
|
|
() =>
|
|
void run(expand, async () => {
|
|
const answer = await context.callBackend("get", {
|
|
id: item.id,
|
|
});
|
|
if (active()) {
|
|
content.textContent = answer.text;
|
|
expand.remove();
|
|
}
|
|
}),
|
|
{ signal: context.signal },
|
|
);
|
|
actions.append(expand);
|
|
}
|
|
const remove = make("button", words.remove, { type: "button" });
|
|
const cancel = make("button", words.cancel, { type: "button" });
|
|
cancel.hidden = true;
|
|
cancel.addEventListener(
|
|
"click",
|
|
() => {
|
|
remove.textContent = words.remove;
|
|
cancel.hidden = true;
|
|
},
|
|
{ signal: context.signal },
|
|
);
|
|
remove.addEventListener(
|
|
"click",
|
|
() => {
|
|
if (cancel.hidden) {
|
|
remove.textContent = words.confirm;
|
|
cancel.hidden = false;
|
|
return;
|
|
}
|
|
void run(remove, async () => {
|
|
await context.callBackend("delete", { id: item.id });
|
|
await load();
|
|
});
|
|
},
|
|
{ signal: context.signal },
|
|
);
|
|
actions.append(remove, cancel);
|
|
card.append(
|
|
row,
|
|
make("div", item.created_at, { class: "meta" }),
|
|
content,
|
|
actions,
|
|
);
|
|
list.append(card);
|
|
}
|
|
} catch (cause) {
|
|
if (active() && current === generation) status.textContent = "";
|
|
throw cause;
|
|
}
|
|
}
|
|
form.addEventListener(
|
|
"submit",
|
|
(event) => {
|
|
event.preventDefault();
|
|
void run(search, load);
|
|
},
|
|
{ signal: context.signal },
|
|
);
|
|
void run(search, load);
|
|
return {
|
|
dispose() {
|
|
disposed = true;
|
|
generation++;
|
|
root.replaceChildren();
|
|
},
|
|
};
|
|
}
|