feat(webui): editable memory page with delete confirmation
This commit is contained in:
parent
652a837e88
commit
f0ef14099d
@ -7,12 +7,23 @@
|
|||||||
let memories = $state([]);
|
let memories = $state([]);
|
||||||
let loading = $state(true);
|
let loading = $state(true);
|
||||||
let error = $state("");
|
let error = $state("");
|
||||||
|
let limit = $state(100);
|
||||||
|
let editingKey = $state(null);
|
||||||
|
let confirmingKey = $state(null);
|
||||||
|
let draftContent = $state("");
|
||||||
|
let draftImportance = $state(0);
|
||||||
|
let saving = $state(false);
|
||||||
|
let deleting = $state(false);
|
||||||
|
let actionError = $state("");
|
||||||
|
|
||||||
const knowledge = $derived(memories.filter((item) => item.category === "knowledge").length);
|
const knowledge = $derived(memories.filter((item) => item.category === "knowledge").length);
|
||||||
|
const timeline = $derived(memories.length - knowledge);
|
||||||
|
const hasMore = $derived(memories.length >= limit);
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
loading = true;
|
loading = true;
|
||||||
error = "";
|
error = "";
|
||||||
const params = new URLSearchParams({ limit: "200" });
|
const params = new URLSearchParams({ limit: String(limit) });
|
||||||
if (query.trim()) params.set("query", query.trim());
|
if (query.trim()) params.set("query", query.trim());
|
||||||
if (category) params.set("category", category);
|
if (category) params.set("category", category);
|
||||||
try { memories = (await api(`/api/memories?${params}`)).memories; }
|
try { memories = (await api(`/api/memories?${params}`)).memories; }
|
||||||
@ -20,19 +31,146 @@
|
|||||||
finally { loading = false; }
|
finally { loading = false; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applyFilters() {
|
||||||
|
limit = 100;
|
||||||
|
editingKey = null;
|
||||||
|
confirmingKey = null;
|
||||||
|
actionError = "";
|
||||||
|
load();
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadMore() {
|
||||||
|
limit += 100;
|
||||||
|
load();
|
||||||
|
}
|
||||||
|
|
||||||
|
function startEdit(memory) {
|
||||||
|
confirmingKey = null;
|
||||||
|
actionError = "";
|
||||||
|
editingKey = memory.key;
|
||||||
|
draftContent = memory.content;
|
||||||
|
draftImportance = Number(memory.importance);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelEdit() {
|
||||||
|
editingKey = null;
|
||||||
|
actionError = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveEdit(key) {
|
||||||
|
saving = true;
|
||||||
|
actionError = "";
|
||||||
|
try {
|
||||||
|
await api(`/api/memories/${encodeURIComponent(key)}`, {
|
||||||
|
method: "PUT",
|
||||||
|
body: JSON.stringify({ content: draftContent, importance: Number(draftImportance) })
|
||||||
|
});
|
||||||
|
editingKey = null;
|
||||||
|
await load();
|
||||||
|
} catch (caught) { actionError = caught.message; }
|
||||||
|
finally { saving = false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
function startConfirm(memory) {
|
||||||
|
editingKey = null;
|
||||||
|
actionError = "";
|
||||||
|
confirmingKey = memory.key;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelConfirm() {
|
||||||
|
confirmingKey = null;
|
||||||
|
actionError = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeMemory(key) {
|
||||||
|
deleting = true;
|
||||||
|
actionError = "";
|
||||||
|
try {
|
||||||
|
await api(`/api/memories/${encodeURIComponent(key)}`, { method: "DELETE" });
|
||||||
|
confirmingKey = null;
|
||||||
|
await load();
|
||||||
|
} catch (caught) { actionError = caught.message; }
|
||||||
|
finally { deleting = false; }
|
||||||
|
}
|
||||||
|
|
||||||
onMount(load);
|
onMount(load);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section class="page active content-page">
|
<section class="page active content-page">
|
||||||
<form class="toolbar filters" onsubmit={(event) => { event.preventDefault(); load(); }}>
|
<form class="toolbar filters" onsubmit={(event) => { event.preventDefault(); applyFilters(); }}>
|
||||||
<label class="search grow"><span>⌕</span><input bind:value={query} placeholder="搜索记忆内容或键" /></label>
|
<label class="search grow"><span>⌕</span><input bind:value={query} placeholder="搜索记忆内容或键" /></label>
|
||||||
<select bind:value={category}><option value="">全部类型</option><option value="knowledge">Knowledge</option><option value="timeline">Timeline</option></select>
|
<select bind:value={category}><option value="">全部</option><option value="knowledge">Knowledge</option><option value="timeline">Timeline</option></select>
|
||||||
<button class="secondary" type="submit">查询</button>
|
<button class="secondary" type="submit">查询</button>
|
||||||
</form>
|
</form>
|
||||||
<div class="metrics"><div class="metric"><b>{memories.length}</b><small>当前结果</small></div><div class="metric"><b>{knowledge}</b><small>Knowledge</small></div><div class="metric"><b>{memories.length - knowledge}</b><small>Timeline</small></div></div>
|
<div class="metrics"><div class="metric"><b>{memories.length}</b><small>总数</small></div><div class="metric"><b>{knowledge}</b><small>Knowledge</small></div><div class="metric"><b>{timeline}</b><small>Timeline</small></div></div>
|
||||||
<div class="cards">
|
<div class="cards">
|
||||||
{#if loading}<div class="loading">加载中…</div>
|
{#if loading}<div class="loading">加载中…</div>
|
||||||
{:else if error}<div class="empty-card error-text">{error}</div>
|
{:else if error}<div class="empty-card error-text">{error}</div>
|
||||||
{:else}{#each memories as memory (memory.id)}<article class="card"><div class="card-row"><div><h3 class="memory-key">{memory.key}</h3><p class="memory-content">{memory.content}</p><div class="meta"><span>{formatTime(memory.updated_at)}</span>{#if memory.session_id}<span>{memory.session_id}</span>{/if}<span>重要度 {Number(memory.importance).toFixed(2)}</span></div></div><span class:ok={memory.category === "knowledge"} class="badge">{memory.category}</span></div></article>{:else}<div class="empty-card">没有匹配的记忆</div>{/each}{/if}
|
{:else}
|
||||||
|
{#each memories as memory (memory.id)}
|
||||||
|
<article class="card">
|
||||||
|
{#if editingKey === memory.key}
|
||||||
|
<div class="card-row"><h3 class="memory-key">{memory.key}</h3><span class="badge" class:ok={memory.category === "knowledge"}>{memory.category}</span></div>
|
||||||
|
<label class="edit-field"><span>内容</span><textarea bind:value={draftContent} rows="4" disabled={saving}></textarea></label>
|
||||||
|
<label class="edit-field"><span>重要度 {Number(draftImportance).toFixed(2)}</span><input type="range" min="0" max="1" step="0.05" bind:value={draftImportance} disabled={saving} /></label>
|
||||||
|
{#if actionError}<p class="error-text action-error">{actionError}</p>{/if}
|
||||||
|
<div class="card-actions">
|
||||||
|
<button class="primary" onclick={() => saveEdit(memory.key)} disabled={saving}>保存</button>
|
||||||
|
<button class="secondary" onclick={cancelEdit} disabled={saving}>取消</button>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="card-row">
|
||||||
|
<div class="card-main">
|
||||||
|
<h3 class="memory-key">{memory.key}</h3>
|
||||||
|
<p class="memory-content">{memory.content}</p>
|
||||||
|
<div class="meta"><span>重要度 {Number(memory.importance).toFixed(2)}</span><span>更新 {formatTime(memory.updated_at)}</span>{#if memory.session_id}<span>{memory.session_id}</span>{/if}</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-side">
|
||||||
|
<span class="badge" class:ok={memory.category === "knowledge"}>{memory.category}</span>
|
||||||
|
<div class="card-actions">
|
||||||
|
<button class="secondary" onclick={() => startEdit(memory)}>编辑</button>
|
||||||
|
<button class="secondary danger-outline" onclick={() => startConfirm(memory)}>删除</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{#if confirmingKey === memory.key}
|
||||||
|
<div class="confirm" class:confirm-danger={memory.category === "timeline"}>
|
||||||
|
{#if memory.category === "timeline"}
|
||||||
|
<p class="confirm-warning">Timeline 是压缩后的历史上下文,删除后模型将永久失去该时段长期记忆且无法自动重建;原始消息仍保留在聊天历史,但不再进入模型上下文</p>
|
||||||
|
{:else}
|
||||||
|
<p class="confirm-text">删除后不可恢复,影响后续召回</p>
|
||||||
|
{/if}
|
||||||
|
{#if actionError}<p class="error-text action-error">{actionError}</p>{/if}
|
||||||
|
<div class="card-actions">
|
||||||
|
<button class="danger-solid" onclick={() => removeMemory(memory.key)} disabled={deleting}>{memory.category === "timeline" ? "我了解,确认删除" : "确认删除"}</button>
|
||||||
|
<button class="secondary" onclick={cancelConfirm} disabled={deleting}>取消</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</article>
|
||||||
|
{:else}<div class="empty-card">没有匹配的记忆</div>{/each}
|
||||||
|
{#if hasMore}<div class="load-more"><button class="secondary" onclick={loadMore}>加载更多</button></div>{/if}
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.card-main { min-width: 0; flex: 1; }
|
||||||
|
.card-side { display: flex; flex-direction: column; align-items: flex-end; gap: 10px; flex: 0 0 auto; }
|
||||||
|
.card-actions { display: flex; gap: 8px; flex-wrap: wrap; }
|
||||||
|
.card > .card-actions { margin-top: 14px; }
|
||||||
|
.danger-outline { border-color: var(--danger-border); color: var(--danger); }
|
||||||
|
.danger-solid { border-radius: 9px; padding: 9px 14px; font-weight: 650; cursor: pointer; border: 1px solid var(--danger); background: var(--danger); color: #fff; }
|
||||||
|
.danger-solid:hover { filter: brightness(1.08); }
|
||||||
|
.edit-field { display: grid; gap: 6px; margin-top: 12px; }
|
||||||
|
.edit-field > span { color: var(--muted); font-size: 11px; }
|
||||||
|
.edit-field textarea { width: 100%; min-height: 90px; resize: vertical; border: 1px solid var(--line); border-radius: 8px; background: var(--panel-2); color: var(--text); padding: 10px; font: 13px/1.55 var(--font-ui); }
|
||||||
|
.edit-field input[type="range"] { width: 100%; accent-color: var(--accent); }
|
||||||
|
.action-error { margin: 12px 0 0; font-size: 12px; }
|
||||||
|
.confirm { margin-top: 12px; border-top: 1px solid var(--line); padding-top: 12px; display: grid; gap: 10px; }
|
||||||
|
.confirm-text { margin: 0; color: var(--muted); font-size: 12px; }
|
||||||
|
.confirm-danger { border: 1px solid var(--danger-border); border-radius: 8px; background: var(--danger-soft); padding: 12px; }
|
||||||
|
.confirm-warning { margin: 0; color: var(--danger); font-size: 12px; line-height: 1.6; }
|
||||||
|
.load-more { display: flex; justify-content: center; padding: 6px 0; }
|
||||||
|
</style>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user