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 = `⚠️ 蒸馏队列: ${status.queue_len} 条`; 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 = '
加载中…
'; 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 = `
加载失败: ${(e as Error).message}
`; } } 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; } } }