158 lines
7.1 KiB
TypeScript
158 lines
7.1 KiB
TypeScript
import { ItemView, WorkspaceLeaf } from "obsidian";
|
|
import { fetchMemories, fetchDistillStatus, fetchDistillQuota, type MemoryRecord } from "./api";
|
|
|
|
export const MEMORY_VIEW_TYPE = "zhiyi-memory-view";
|
|
|
|
export class MemoryView extends ItemView {
|
|
private container: HTMLElement;
|
|
private memories: MemoryRecord[] = [];
|
|
private currentPage = 0;
|
|
private pageSize = 20;
|
|
private totalMemories = 0;
|
|
private status_interval: number | null = null;
|
|
|
|
constructor(leaf: WorkspaceLeaf) {
|
|
super(leaf);
|
|
this.container = this.contentEl;
|
|
}
|
|
|
|
getViewType() { return MEMORY_VIEW_TYPE; }
|
|
getDisplayText() { return "织忆记忆"; }
|
|
|
|
async onOpen() {
|
|
this.render();
|
|
this.startStatusPoll();
|
|
}
|
|
|
|
async onClose() {
|
|
if (this.status_interval !== null) {
|
|
(window as unknown as { clearInterval: (n: number) => void }).clearInterval(this.status_interval);
|
|
}
|
|
}
|
|
|
|
private startStatusPoll() {
|
|
// 轮询蒸馏状态(每 30 秒)
|
|
const id = (window as unknown as { setInterval: (fn: () => void, ms: number) => number }).setInterval(
|
|
() => this.renderDistillBanner(),
|
|
30000
|
|
);
|
|
this.status_interval = id;
|
|
}
|
|
|
|
private async renderDistillBanner() {
|
|
const banner = this.container.querySelector(".zhiyi-distill-banner") as HTMLElement;
|
|
if (!banner) return;
|
|
try {
|
|
const [status, quota] = await Promise.all([fetchDistillStatus(), fetchDistillQuota()]);
|
|
if (status.queue_len > 0) {
|
|
banner.innerHTML = `<span class="zhiyi-warn">⚠️ 蒸馏队列: ${status.queue_len} 条</span>`;
|
|
banner.style.display = "block";
|
|
} else {
|
|
banner.style.display = "none";
|
|
}
|
|
} catch (_) {
|
|
// ignore
|
|
}
|
|
}
|
|
|
|
async render() {
|
|
this.container.empty();
|
|
this.container.createEl("style", {
|
|
text: `
|
|
.zhiyi-memory-view { height: 100%; display: flex; flex-direction: column; font-size: 13px; }
|
|
.zhiyi-header { padding: 10px 12px 6px; border-bottom: 1px solid var(--background-modifier-border); }
|
|
.zhiyi-header h1 { font-size: 14px; font-weight: 600; margin: 0 0 4px; }
|
|
.zhiyi-stats { color: var(--text-muted); font-size: 11px; margin: 0; }
|
|
.zhiyi-distill-banner {
|
|
display: none; background: #c44; color: white; padding: 4px 12px;
|
|
font-size: 12px; font-weight: 600; cursor: pointer;
|
|
}
|
|
.zhiyi-body { flex: 1; overflow-y: auto; }
|
|
.zhiyi-item {
|
|
padding: 8px 12px; border-bottom: 1px solid var(--background-modifier-border);
|
|
cursor: pointer;
|
|
}
|
|
.zhiyi-item:hover { background: var(--background-modifier-hover); }
|
|
.zhiyi-item-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 2px; }
|
|
.zhiyi-item-cat { font-size: 10px; color: var(--text-muted); background: var(--background-secondary); padding: 1px 4px; border-radius: 2px; }
|
|
.zhiyi-item-score { font-size: 10px; color: var(--text-muted); }
|
|
.zhiyi-item-content { font-size: 12px; color: var(--text-normal); line-height: 1.4; }
|
|
.zhiyi-item-time { font-size: 10px; color: var(--text-muted); margin-top: 2px; }
|
|
.zhiyi-item-id { font-size: 10px; color: var(--text-faint); font-family: monospace; margin-top: 2px; overflow: hidden; text-overflow: ellipsis; }
|
|
.zhiyi-pagination { display: flex; gap: 6px; padding: 8px 12px; border-top: 1px solid var(--background-modifier-border); }
|
|
.zhiyi-pagination button { flex: 1; padding: 4px; background: var(--background-secondary); border: 1px solid var(--background-modifier-border); border-radius: 4px; cursor: pointer; font-size: 11px; }
|
|
.zhiyi-pagination button:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
.zhiyi-loading { padding: 20px; text-align: center; color: var(--text-muted); }
|
|
.zhiyi-empty { padding: 20px; text-align: center; color: var(--text-muted); }
|
|
`
|
|
});
|
|
this.container.createEl("div", { cls: "zhiyi-memory-view" }).append(
|
|
this.container.createEl("div", { cls: "zhiyi-distill-banner" }),
|
|
this.container.createEl("div", { cls: "zhiyi-header" }).append(
|
|
this.container.createEl("h1", { text: "🧠 织忆记忆" }),
|
|
this.container.createEl("p", { cls: "zhiyi-stats", text: "加载中..." })
|
|
),
|
|
this.container.createEl("div", { cls: "zhiyi-body" }),
|
|
this.container.createEl("div", { cls: "zhiyi-pagination" })
|
|
);
|
|
await this.loadMemories();
|
|
await this.renderDistillBanner();
|
|
}
|
|
|
|
async loadMemories() {
|
|
const body = this.container.querySelector(".zhiyi-body") as HTMLElement;
|
|
const stats = this.container.querySelector(".zhiyi-stats") as HTMLElement;
|
|
body.innerHTML = '<div class="zhiyi-loading">加载中…</div>';
|
|
try {
|
|
const data = await fetchMemories({ namespace: "hermes-main", limit: this.pageSize, offset: this.currentPage * this.pageSize });
|
|
this.memories = data.memories || [];
|
|
this.totalMemories = data.total || 0;
|
|
stats.setText(`${this.totalMemories} 条记忆 · 第 ${this.currentPage + 1} 页`);
|
|
this.renderList();
|
|
} catch (e) {
|
|
body.innerHTML = `<div class="zhiyi-empty">加载失败: ${(e as Error).message}</div>`;
|
|
}
|
|
}
|
|
|
|
private renderList() {
|
|
const body = this.container.querySelector(".zhiyi-body") as HTMLElement;
|
|
const pag = this.container.querySelector(".zhiyi-pagination") as HTMLElement;
|
|
body.empty();
|
|
if (this.memories.length === 0) {
|
|
body.createEl("div", { cls: "zhiyi-empty", text: "暂无记忆" });
|
|
}
|
|
for (const mem of this.memories) {
|
|
const item = body.createEl("div", { cls: "zhiyi-item" });
|
|
const header = item.createEl("div", { cls: "zhiyi-item-header" });
|
|
header.createEl("span", { cls: "zhiyi-item-cat", text: mem.category || "normal" });
|
|
header.createEl("span", { cls: "zhiyi-item-score", text: `score: ${(mem.quality_score ?? 0).toFixed(2)}` });
|
|
item.createEl("div", { cls: "zhiyi-item-content", text: this.truncate(mem.content, 120) });
|
|
item.createEl("div", { cls: "zhiyi-item-time", text: this.formatTime(mem.created_at) });
|
|
item.createEl("div", { cls: "zhiyi-item-id", text: mem.id });
|
|
}
|
|
const totalPages = Math.ceil(this.totalMemories / this.pageSize);
|
|
pag.empty();
|
|
if (totalPages > 1) {
|
|
const prev = pag.createEl("button", { text: "◀ 上一页" });
|
|
prev.setAttr("disabled", this.currentPage === 0 ? "true" : "");
|
|
prev.onclick = () => { this.currentPage--; this.loadMemories(); };
|
|
pag.createEl("span", { text: `${this.currentPage + 1}/${totalPages}`, cls: "zhiyi-item-cat" });
|
|
const next = pag.createEl("button", { text: "下一页 ▶" });
|
|
next.setAttr("disabled", this.currentPage >= totalPages - 1 ? "true" : "");
|
|
next.onclick = () => { this.currentPage++; this.loadMemories(); };
|
|
}
|
|
}
|
|
|
|
private truncate(s: string, max: number) {
|
|
if (!s) return "";
|
|
return s.length > max ? s.slice(0, max) + "…" : s;
|
|
}
|
|
|
|
private formatTime(iso: string) {
|
|
if (!iso) return "";
|
|
try {
|
|
const d = new Date(iso);
|
|
return d.toLocaleString("zh-CN", { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" });
|
|
} catch { return iso; }
|
|
}
|
|
} |