const TEMP_STORAGE_KEY = "wa-direct-chat-temps-v2";
const MAX_TEMPS = 2;
const EMPTY_SLOTS = {
systemPrompt: "",
loreBefore: "",
seedHistory: "",
loreAfter: "",
postTurn: "",
};
const $ = (id) => document.getElementById(id);
const els = {
pathList: $("chat-path-list"),
railList: $("chat-rail-list"),
room: $("chat-room"),
roomTitle: $("chat-room-title"),
roomSub: $("chat-room-sub"),
arena: $("chat-arena"),
input: $("chat-input"),
status: $("chat-status"),
shellEditor: $("shell-editor"),
contextSub: $("context-sub"),
btnNewShell: $("btn-new-shell"),
btnSaveTemp: $("btn-save-temp"),
btnNewConv: $("btn-new-conv"),
btnAddWindow: $("btn-add-window"),
btnSend: $("btn-send"),
btnSaveShell: $("btn-save-shell"),
shellName: $("shell-name"),
shellSystem: $("shell-system"),
shellLoreBefore: $("shell-lore-before"),
shellSeedHistory: $("shell-seed-history"),
shellLoreAfter: $("shell-lore-after"),
shellPostTurn: $("shell-post-turn"),
dialogPromote: $("dialog-promote"),
formPromote: $("form-promote"),
promoteShellName: $("promote-shell-name"),
promoteConvName: $("promote-conv-name"),
dialogAddWindow: $("dialog-add-window"),
formAddWindow: $("form-add-window"),
addWindowProfile: $("add-window-profile"),
};
/** @type {"root"|"shell"|"conversation"} */
let navLevel = "root";
/** @type {string|null} */
let activeShellId = null;
/** @type {string|null} */
let activeConversationId = null;
/** @type {boolean} */
let activeIsTemp = false;
/** @type {any} */
let activeShell = null;
/** @type {any[]} */
let activeWindows = [];
/** @type {any[]} */
let activeTurns = [];
/** @type {any[]} */
let shells = [];
/** @type {any[]} */
let conversations = [];
/** @type {any[]} */
let profileList = [];
let busy = false;
async function api(path, options = {}) {
const res = await fetch(path, {
headers: { "Content-Type": "application/json", ...(options.headers || {}) },
...options,
});
const text = await res.text();
let data = null;
try {
data = text ? JSON.parse(text) : null;
} catch {
data = { error: text || res.statusText };
}
if (!res.ok) {
throw new Error(data?.error || res.statusText || `HTTP ${res.status}`);
}
return data;
}
function loadTemps() {
try {
const raw = localStorage.getItem(TEMP_STORAGE_KEY);
if (!raw) return [];
const parsed = JSON.parse(raw);
return Array.isArray(parsed) ? parsed.map(normalizeTemp).filter(Boolean) : [];
} catch {
return [];
}
}
function normalizeTemp(t) {
if (!t || typeof t !== "object") return null;
const windows = Array.isArray(t.windows) ? t.windows : [];
let turns = Array.isArray(t.turns) ? t.turns : [];
if (!turns.length && Array.isArray(t.messages) && t.messages.length) {
turns = migrateFlatMessages(t.messages, windows[0]?.id || "default");
}
return {
id: t.id,
name: t.name || "临时会话",
slots: { ...EMPTY_SLOTS, ...(t.slots || {}) },
windows,
turns,
createdAt: t.createdAt || nowIso(),
updatedAt: t.updatedAt || nowIso(),
};
}
function migrateFlatMessages(messages, windowId) {
const turns = [];
let pending = null;
for (const m of messages) {
if (m.role === "user") {
pending = { content: m.content, createdAt: m.createdAt };
} else if (m.role === "assistant" && pending) {
turns.push({
id: m.id || newId(),
user: pending,
replies: [
{
windowId,
content: m.content,
thinking: m.thinking,
model: m.model,
contextTrace: m.contextTrace,
},
],
createdAt: m.createdAt,
});
pending = null;
}
}
return turns;
}
function saveTemps(list) {
const trimmed = list
.slice()
.sort((a, b) => String(b.updatedAt).localeCompare(String(a.updatedAt)))
.slice(0, MAX_TEMPS);
localStorage.setItem(TEMP_STORAGE_KEY, JSON.stringify(trimmed));
return trimmed;
}
function upsertTemp(temp) {
const list = loadTemps().filter((t) => t.id !== temp.id);
list.unshift(temp);
return saveTemps(list);
}
function deleteTemp(id) {
return saveTemps(loadTemps().filter((t) => t.id !== id));
}
function newId() {
return crypto.randomUUID();
}
function nowIso() {
return new Date().toISOString();
}
function setStatus(text, isError = false) {
if (!text) {
els.status.hidden = true;
els.status.textContent = "";
return;
}
els.status.hidden = false;
els.status.textContent = text;
els.status.classList.toggle("error", isError);
}
function escapeHtml(s) {
return String(s)
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/"/g, """);
}
function slotsFromEditor() {
return {
systemPrompt: els.shellSystem.value,
loreBefore: els.shellLoreBefore.value,
seedHistory: els.shellSeedHistory.value,
loreAfter: els.shellLoreAfter.value,
postTurn: els.shellPostTurn.value,
};
}
function fillEditor(shellLike) {
const s = shellLike || { name: "", ...EMPTY_SLOTS };
els.shellName.value = s.name || "";
els.shellSystem.value = s.systemPrompt || "";
els.shellLoreBefore.value = s.loreBefore || "";
els.shellSeedHistory.value = s.seedHistory || "";
els.shellLoreAfter.value = s.loreAfter || "";
els.shellPostTurn.value = s.postTurn || "";
}
function setContextEditorEnabled(enabled, subtitle) {
const fields = [
els.shellName,
els.shellSystem,
els.shellLoreBefore,
els.shellSeedHistory,
els.shellLoreAfter,
els.shellPostTurn,
els.btnSaveShell,
];
for (const el of fields) el.disabled = !enabled;
if (els.contextSub) {
els.contextSub.textContent =
subtitle || (enabled ? "改完点应用" : "选择或新建后编辑");
}
}
function syncContextPanel() {
if (activeIsTemp) {
fillEditor(activeShell || { name: "临时会话", ...EMPTY_SLOTS });
setContextEditorEnabled(true, "临时上下文 · 仅浏览器");
return;
}
if (activeShell) {
fillEditor(activeShell);
setContextEditorEnabled(true, "持久上下文 · 点应用写入本机");
return;
}
fillEditor({ name: "", ...EMPTY_SLOTS });
setContextEditorEnabled(false, "选择或新建后编辑");
}
function windowLabel(w) {
return w.profileName || w.model || w.profileId || "窗口";
}
function ensureDefaultWindow() {
if (activeWindows.length) return;
const p = profileList.find((x) => x.hasKey) || profileList[0];
if (!p) return;
activeWindows = [
{
id: newId(),
profileId: p.id,
profileName: p.name,
model: p.model,
},
];
}
function persistActiveSessionMeta() {
if (activeIsTemp && activeConversationId) {
const temp = loadTemps().find((t) => t.id === activeConversationId);
if (!temp) return;
temp.windows = activeWindows;
temp.turns = activeTurns;
temp.slots = { ...EMPTY_SLOTS, ...(temp.slots || {}), ...slotsFromEditor() };
temp.updatedAt = nowIso();
upsertTemp(temp);
return;
}
if (!activeIsTemp && activeConversationId) {
api(
`/api/direct-chat/conversations/${encodeURIComponent(activeConversationId)}`,
{
method: "PATCH",
body: JSON.stringify({
windows: activeWindows,
turns: activeTurns,
}),
},
).catch((err) =>
setStatus(err instanceof Error ? err.message : String(err), true),
);
}
}
function renderPath() {
const items = [];
items.push(
navLevel === "root" && !activeIsTemp
? `
直聊`
: ``,
);
if (navLevel === "shell" || (navLevel === "conversation" && !activeIsTemp)) {
const shellLabel = activeShell?.name || "上下文";
if (navLevel === "shell") {
items.push(
`${escapeHtml(shellLabel)}`,
);
} else {
items.push(
``,
);
const convLabel =
conversations.find((c) => c.id === activeConversationId)?.name || "会话";
items.push(
`${escapeHtml(convLabel)}`,
);
}
} else if (activeIsTemp) {
items.push(
`临时`,
);
}
els.pathList.innerHTML = items.join("");
}
function renderRootRail() {
const temps = loadTemps();
const parts = [];
if (temps.length) {
parts.push(`快速对话(浏览器)
`);
for (const t of temps) {
parts.push(
railItemHtml({
id: t.id,
title: t.name || "临时会话",
meta: `${t.turns?.length || 0} 轮 · 临时`,
temp: true,
active: activeIsTemp && activeConversationId === t.id,
}),
);
}
}
parts.push(`已保存上下文
`);
if (!shells.length) {
parts.push(`还没有上下文壳
`);
}
for (const s of shells) {
parts.push(
railItemHtml({
id: s.id,
title: s.name,
meta: `${s.conversationCount} 个会话`,
temp: false,
active: !activeIsTemp && activeShellId === s.id,
}),
);
}
els.railList.innerHTML = parts.join("");
}
function renderRail() {
if (navLevel === "root" || activeIsTemp) {
renderRootRail();
return;
}
if (navLevel === "shell") {
const parts = [];
if (!conversations.length) {
parts.push(`还没有会话
`);
}
for (const c of conversations) {
parts.push(
railItemHtml({
id: c.id,
title: c.name,
meta: `${c.messageCount} 条消息`,
temp: false,
active: activeConversationId === c.id,
}),
);
}
els.railList.innerHTML = parts.join("");
return;
}
els.railList.innerHTML = conversations
.map((c) =>
railItemHtml({
id: c.id,
title: c.name,
meta: `${c.messageCount} 条消息`,
temp: false,
active: activeConversationId === c.id,
}),
)
.join("");
}
function railItemHtml({ id, title, meta, temp, active }) {
return `
`;
}
function renderArena() {
if (!els.arena) return;
if (!activeWindows.length) {
els.arena.innerHTML = `还没有对照窗。点右上「新增窗口」加入模型;发送后各列同时回复。
`;
return;
}
const multi = activeWindows.length > 1;
els.arena.innerHTML = activeWindows
.map((w) => {
const hasSelection = activeTurns.some((t) => t.selectedWindowId === w.id);
const messagesHtml = activeTurns.length
? activeTurns
.map((turn) => {
const reply = turn.replies.find((r) => r.windowId === w.id);
const selected = turn.selectedWindowId === w.id;
const userHtml = `
你
${escapeHtml(turn.user.content)}
`;
let assistantInner;
if (!reply) {
assistantInner = `本轮无回复
`;
} else if (reply.error) {
assistantInner = `${escapeHtml(reply.error)}
`;
} else {
assistantInner = `${
reply.thinking
? `${escapeHtml(reply.thinking)}
`
: ""
}${escapeHtml(reply.content || "")}
`;
}
const selectBtn =
multi && reply && !reply.error
? ``
: selected
? ``
: "";
return `${userHtml}
助手${reply?.model ? `${escapeHtml(reply.model)}` : ""}
${assistantInner}
${selectBtn}
`;
})
.join("")
: `在下方输入,各列会同时回复同一条消息。
`;
return `
${escapeHtml(windowLabel(w))}
${escapeHtml(w.model || w.profileId || "")}
${
activeWindows.length > 1
? ``
: ""
}
${messagesHtml}
`;
})
.join("");
// 各列滚到底
for (const sc of els.arena.querySelectorAll(".chat-arena-col-scroll")) {
sc.scrollTop = sc.scrollHeight;
}
}
function setComposerEnabled(enabled) {
els.input.disabled = !enabled;
els.btnSend.disabled = !enabled;
els.btnAddWindow.disabled = !enabled;
}
function ensureDraftTemp() {
const temps = loadTemps();
if (temps.length) return temps[0];
ensureDefaultWindow();
const temp = {
id: newId(),
name: "临时会话",
slots: { ...EMPTY_SLOTS },
windows: activeWindows.length
? activeWindows
: profileList[0]
? [
{
id: newId(),
profileId: profileList[0].id,
profileName: profileList[0].name,
model: profileList[0].model,
},
]
: [],
turns: [],
createdAt: nowIso(),
updatedAt: nowIso(),
};
upsertTemp(temp);
return temp;
}
function openDraftTemp() {
const temp = ensureDraftTemp();
activeIsTemp = true;
activeConversationId = temp.id;
activeShellId = null;
activeShell = { name: temp.name || "临时会话", ...EMPTY_SLOTS, ...temp.slots };
activeWindows = temp.windows?.length ? temp.windows : [];
ensureDefaultWindow();
if (!temp.windows?.length && activeWindows.length) {
temp.windows = activeWindows;
upsertTemp(temp);
}
activeTurns = temp.turns || [];
navLevel = "conversation";
els.roomTitle.textContent = temp.name || "临时会话";
els.roomSub.textContent = "未选存档 · 快速对话(浏览器缓存,最多 2 条)";
els.btnSaveTemp.hidden = false;
els.btnNewConv.hidden = true;
setComposerEnabled(true);
syncContextPanel();
renderArena();
}
function syncRoomChrome() {
els.btnNewConv.hidden = !(
navLevel === "shell" ||
(navLevel === "conversation" && !activeIsTemp)
);
syncContextPanel();
els.room.hidden = false;
if (navLevel === "shell" && !activeIsTemp) {
els.roomTitle.textContent = activeShell?.name || "上下文";
els.roomSub.textContent = "从左侧选会话,或点「新建会话」。";
els.btnSaveTemp.hidden = true;
setComposerEnabled(false);
activeWindows = [];
activeTurns = [];
if (els.arena) {
els.arena.innerHTML =
`尚未打开会话。从左侧选会话,或点「新建会话」。
`;
}
return;
}
if (navLevel !== "conversation") {
openDraftTemp();
return;
}
const title = activeIsTemp
? loadTemps().find((t) => t.id === activeConversationId)?.name || "临时会话"
: conversations.find((c) => c.id === activeConversationId)?.name || "会话";
els.roomTitle.textContent = title;
els.roomSub.textContent = activeIsTemp
? "未选存档 · 快速对话(浏览器缓存,最多 2 条)"
: `上下文:${activeShell?.name || "—"}`;
els.btnSaveTemp.hidden = !activeIsTemp;
setComposerEnabled(true);
renderArena();
}
async function refreshShells() {
const data = await api("/api/direct-chat/shells");
shells = data.shells || [];
}
async function refreshConversations(shellId) {
const data = await api(
`/api/direct-chat/shells/${encodeURIComponent(shellId)}`,
);
activeShell = data.shell;
conversations = data.conversations || [];
fillEditor(activeShell);
}
async function loadProfiles() {
try {
const data = await api("/api/profiles");
profileList = (data.profiles || []).map((p) => ({
id: p.id,
name: p.name,
model: p.model,
hasKey: Boolean(String(p.apiKey ?? "").trim()),
}));
} catch {
profileList = [];
}
}
async function goRoot() {
activeShellId = null;
conversations = [];
await refreshShells();
openDraftTemp();
renderPath();
renderRail();
}
async function openShell(shellId) {
activeIsTemp = false;
activeShellId = shellId;
activeConversationId = null;
activeTurns = [];
activeWindows = [];
navLevel = "shell";
await refreshConversations(shellId);
renderPath();
renderRail();
syncRoomChrome();
}
async function openConversation(conversationId) {
const data = await api(
`/api/direct-chat/conversations/${encodeURIComponent(conversationId)}`,
);
activeIsTemp = false;
activeConversationId = conversationId;
activeShellId = data.conversation.shellId;
activeShell = data.shell;
activeWindows = data.conversation.windows || [];
activeTurns = data.conversation.turns || [];
ensureDefaultWindow();
navLevel = "conversation";
if (activeShellId) {
const shellData = await api(
`/api/direct-chat/shells/${encodeURIComponent(activeShellId)}`,
);
conversations = shellData.conversations || [];
activeShell = shellData.shell || activeShell;
}
renderPath();
renderRail();
syncRoomChrome();
}
function openTemp(tempId) {
const temp = loadTemps().find((t) => t.id === tempId);
if (!temp) return;
activeIsTemp = true;
activeConversationId = temp.id;
activeShellId = null;
activeShell = { name: temp.name || "临时会话", ...EMPTY_SLOTS, ...temp.slots };
activeWindows = temp.windows || [];
activeTurns = temp.turns || [];
ensureDefaultWindow();
navLevel = "conversation";
renderPath();
renderRail();
syncRoomChrome();
}
async function createShellAndOpen() {
const name = window.prompt("上下文名称", "新上下文");
if (name == null) return;
const data = await api("/api/direct-chat/shells", {
method: "POST",
body: JSON.stringify({ name: name.trim() || "新上下文", ...EMPTY_SLOTS }),
});
await refreshShells();
await openShell(data.shell.id);
}
async function createConversationUnderShell() {
if (!activeShellId) return;
const name = window.prompt("会话名称", "新会话");
if (name == null) return;
ensureDefaultWindow();
const data = await api(
`/api/direct-chat/shells/${encodeURIComponent(activeShellId)}/conversations`,
{
method: "POST",
body: JSON.stringify({
name: name.trim() || "新会话",
windows: activeWindows,
turns: [],
}),
},
);
await openConversation(data.conversation.id);
}
async function saveShellEditor() {
const slots = slotsFromEditor();
const name = els.shellName.value.trim() || "未命名上下文";
if (activeIsTemp && activeConversationId) {
const temps = loadTemps();
const temp = temps.find((t) => t.id === activeConversationId);
if (!temp) return;
temp.name = name === "临时" ? temp.name : name;
temp.slots = slots;
temp.windows = activeWindows;
temp.turns = activeTurns;
temp.updatedAt = nowIso();
upsertTemp(temp);
activeShell = { name: temp.name, ...slots };
if (els.contextSub) els.contextSub.textContent = "已更新(仅浏览器)";
setStatus("已更新临时上下文(仅浏览器)");
return;
}
if (!activeShellId) return;
const data = await api(
`/api/direct-chat/shells/${encodeURIComponent(activeShellId)}`,
{
method: "PATCH",
body: JSON.stringify({ name, ...slots }),
},
);
activeShell = data.shell;
fillEditor(activeShell);
await refreshShells();
renderPath();
renderRail();
if (els.contextSub) els.contextSub.textContent = "已保存";
setStatus("上下文已保存");
}
async function sendMessage() {
if (busy) return;
const text = els.input.value.trim();
if (!text) return;
if (navLevel !== "conversation" || !activeConversationId) {
setStatus("请先打开一个会话", true);
return;
}
ensureDefaultWindow();
if (!activeWindows.length) {
setStatus("请先新增至少一个模型窗口", true);
return;
}
busy = true;
els.btnSend.disabled = true;
setStatus(
activeWindows.length > 1
? `发送中(${activeWindows.length} 个窗口并行)…`
: "发送中…",
);
try {
if (activeIsTemp) {
const temp = loadTemps().find((t) => t.id === activeConversationId);
if (!temp) throw new Error("临时会话不存在");
const slots = { ...EMPTY_SLOTS, ...temp.slots };
const data = await api("/api/direct-chat/ephemeral/complete", {
method: "POST",
body: JSON.stringify({
text,
...slots,
windows: activeWindows,
turns: activeTurns,
}),
});
const multi = data.multi;
if (!multi?.turn) throw new Error("未返回回合结果");
activeTurns = [...activeTurns, multi.turn];
temp.turns = activeTurns;
temp.windows = activeWindows;
temp.slots = slots;
temp.updatedAt = nowIso();
upsertTemp(temp);
els.input.value = "";
const failed = (multi.results || []).filter((r) => r.error);
setStatus(
failed.length
? `${failed.length} 个窗口失败,其余已写入`
: "",
failed.length > 0,
);
renderArena();
renderRail();
return;
}
const data = await api(
`/api/direct-chat/conversations/${encodeURIComponent(activeConversationId)}/messages`,
{
method: "POST",
body: JSON.stringify({ text, windows: activeWindows }),
},
);
activeTurns = data.conversation.turns || [];
activeWindows = data.conversation.windows || activeWindows;
activeShell = data.shell;
els.input.value = "";
const failed = (data.multi?.results || []).filter((r) => r.error);
setStatus(
failed.length ? `${failed.length} 个窗口失败,其余已写入` : "",
failed.length > 0,
);
renderArena();
if (activeShellId) {
const shellData = await api(
`/api/direct-chat/shells/${encodeURIComponent(activeShellId)}`,
);
conversations = shellData.conversations || [];
renderRail();
}
} catch (err) {
setStatus(err instanceof Error ? err.message : String(err), true);
} finally {
busy = false;
els.btnSend.disabled = false;
}
}
async function selectReply(turnId, windowId) {
const turn = activeTurns.find((t) => t.id === turnId);
if (!turn) return;
const next =
turn.selectedWindowId === windowId ? null : windowId;
turn.selectedWindowId = next;
activeTurns = activeTurns.map((t) =>
t.id === turnId ? { ...t, selectedWindowId: next } : t,
);
renderArena();
if (activeIsTemp) {
persistActiveSessionMeta();
setStatus(
next
? `已保留「${windowLabel(activeWindows.find((w) => w.id === next) || {})}」为后续共用基准`
: "已取消保留,各窗口继续用各自回复",
);
return;
}
try {
const data = await api(
`/api/direct-chat/conversations/${encodeURIComponent(activeConversationId)}/turns/${encodeURIComponent(turnId)}/select`,
{
method: "POST",
body: JSON.stringify({ windowId: next }),
},
);
activeTurns = data.conversation.turns || activeTurns;
renderArena();
setStatus(
next
? `已保留「${windowLabel(activeWindows.find((w) => w.id === next) || {})}」为后续共用基准`
: "已取消保留,各窗口继续用各自回复",
);
} catch (err) {
setStatus(err instanceof Error ? err.message : String(err), true);
}
}
function openAddWindowDialog() {
const used = new Set(activeWindows.map((w) => w.profileId));
const available = profileList.filter((p) => p.hasKey && !used.has(p.id));
if (!available.length) {
setStatus(
used.size
? "没有更多可用的 API 配置"
: "请先在设置里配置带 Key 的 API",
true,
);
return;
}
els.addWindowProfile.innerHTML = available
.map(
(p) =>
``,
)
.join("");
els.dialogAddWindow.showModal();
}
els.formAddWindow.addEventListener("submit", (e) => {
const submitter = e.submitter;
if (submitter?.value === "cancel") return;
e.preventDefault();
const profileId = els.addWindowProfile.value;
const p = profileList.find((x) => x.id === profileId);
if (!p) return;
if (activeWindows.some((w) => w.profileId === profileId)) {
setStatus("该配置已在窗口中", true);
return;
}
activeWindows = [
...activeWindows,
{
id: newId(),
profileId: p.id,
profileName: p.name,
model: p.model,
},
];
persistActiveSessionMeta();
els.dialogAddWindow.close();
renderArena();
setStatus(`已新增窗口:${p.name}`);
});
function removeWindow(windowId) {
if (activeWindows.length <= 1) {
setStatus("至少保留一个窗口", true);
return;
}
activeWindows = activeWindows.filter((w) => w.id !== windowId);
activeTurns = activeTurns.map((t) => ({
...t,
replies: t.replies.filter((r) => r.windowId !== windowId),
selectedWindowId:
t.selectedWindowId === windowId ? null : t.selectedWindowId,
}));
persistActiveSessionMeta();
renderArena();
}
els.formPromote.addEventListener("submit", async (e) => {
const submitter = e.submitter;
if (submitter?.value === "cancel") return;
e.preventDefault();
if (!activeIsTemp || !activeConversationId) return;
const temp = loadTemps().find((t) => t.id === activeConversationId);
if (!temp) return;
try {
const data = await api("/api/direct-chat/promote", {
method: "POST",
body: JSON.stringify({
shell: {
name: els.promoteShellName.value.trim() || "临时上下文",
...(temp.slots || EMPTY_SLOTS),
},
conversationName: els.promoteConvName.value.trim() || "会话",
windows: temp.windows || activeWindows,
turns: temp.turns || activeTurns,
}),
});
deleteTemp(temp.id);
els.dialogPromote.close();
await openConversation(data.conversation.id);
setStatus("已保存为持久会话");
} catch (err) {
setStatus(err instanceof Error ? err.message : String(err), true);
}
});
async function deleteRailItem(id, isTemp) {
if (!window.confirm("确定删除?")) return;
try {
if (isTemp) {
deleteTemp(id);
if (activeConversationId === id) await goRoot();
else {
renderRail();
renderPath();
}
return;
}
if (navLevel === "root" || activeIsTemp) {
await api(`/api/direct-chat/shells/${encodeURIComponent(id)}`, {
method: "DELETE",
});
if (activeShellId === id) await goRoot();
else
await refreshShells().then(() => {
renderRail();
});
return;
}
await api(`/api/direct-chat/conversations/${encodeURIComponent(id)}`, {
method: "DELETE",
});
if (activeConversationId === id) {
if (activeShellId) await openShell(activeShellId);
else await goRoot();
} else if (activeShellId) {
await refreshConversations(activeShellId);
renderRail();
}
} catch (err) {
setStatus(err instanceof Error ? err.message : String(err), true);
}
}
// events
els.btnNewShell.addEventListener("click", () => {
createShellAndOpen().catch((err) =>
setStatus(err instanceof Error ? err.message : String(err), true),
);
});
els.btnNewConv.addEventListener("click", () => {
createConversationUnderShell().catch((err) =>
setStatus(err instanceof Error ? err.message : String(err), true),
);
});
els.btnSaveShell.addEventListener("click", () => {
saveShellEditor().catch((err) =>
setStatus(err instanceof Error ? err.message : String(err), true),
);
});
els.btnAddWindow.addEventListener("click", () => openAddWindowDialog());
els.btnSaveTemp.addEventListener("click", () => {
if (!activeIsTemp || !activeConversationId) return;
const temp = loadTemps().find((t) => t.id === activeConversationId);
if (!temp) return;
els.promoteShellName.value = "临时上下文";
els.promoteConvName.value = temp.name || "会话";
els.dialogPromote.showModal();
});
els.btnSend.addEventListener("click", () => {
sendMessage().catch((err) =>
setStatus(err instanceof Error ? err.message : String(err), true),
);
});
els.input.addEventListener("keydown", (e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
sendMessage().catch((err) =>
setStatus(err instanceof Error ? err.message : String(err), true),
);
}
});
els.pathList.addEventListener("click", (e) => {
const btn = e.target.closest("button[data-nav]");
if (!btn) return;
const nav = btn.getAttribute("data-nav");
if (nav === "root") {
goRoot().catch((err) =>
setStatus(err instanceof Error ? err.message : String(err), true),
);
} else if (nav === "shell" && activeShellId) {
openShell(activeShellId).catch((err) =>
setStatus(err instanceof Error ? err.message : String(err), true),
);
}
});
els.railList.addEventListener("click", (e) => {
const del = e.target.closest("[data-action='delete']");
const open = e.target.closest("[data-action='open']");
const row = e.target.closest(".chat-rail-item");
if (!row) return;
const id = row.getAttribute("data-id");
const isTemp = row.getAttribute("data-temp") === "1";
if (del) {
deleteRailItem(id, isTemp).catch((err) =>
setStatus(err instanceof Error ? err.message : String(err), true),
);
return;
}
if (!open) return;
if (isTemp) {
openTemp(id);
return;
}
if (activeIsTemp || navLevel === "root") {
openShell(id).catch((err) =>
setStatus(err instanceof Error ? err.message : String(err), true),
);
return;
}
openConversation(id).catch((err) =>
setStatus(err instanceof Error ? err.message : String(err), true),
);
});
els.arena?.addEventListener("click", (e) => {
const removeBtn = e.target.closest("[data-action='remove-window']");
if (removeBtn) {
const col = removeBtn.closest("[data-window-id]");
const id = col?.getAttribute("data-window-id");
if (id) removeWindow(id);
return;
}
const selectBtn = e.target.closest("[data-action='select-reply']");
if (!selectBtn) return;
const turnId = selectBtn.getAttribute("data-turn-id");
const windowId = selectBtn.getAttribute("data-window-id");
if (turnId && windowId) {
selectReply(turnId, windowId).catch((err) =>
setStatus(err instanceof Error ? err.message : String(err), true),
);
}
});
loadProfiles()
.then(() => goRoot())
.catch((err) =>
setStatus(err instanceof Error ? err.message : String(err), true),
);