Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
81ed1d816f |
No files matched your search
@@ -5,7 +5,6 @@ import type {
|
|||||||
MemoryGraphNode,
|
MemoryGraphNode,
|
||||||
MemoryGraphResponse,
|
MemoryGraphResponse,
|
||||||
MemoryProcessingSummary,
|
MemoryProcessingSummary,
|
||||||
SourceImportReceipt as Receipt,
|
|
||||||
SourceResponse,
|
SourceResponse,
|
||||||
} from "@one-memory/contracts";
|
} from "@one-memory/contracts";
|
||||||
import {
|
import {
|
||||||
@@ -40,12 +39,6 @@ import { afterEach, expect, it, vi } from "vitest";
|
|||||||
import { MemoryMap } from "./MemoryMap";
|
import { MemoryMap } from "./MemoryMap";
|
||||||
import type { MemoryArrival } from "./memory-arrival-state";
|
import type { MemoryArrival } from "./memory-arrival-state";
|
||||||
|
|
||||||
const { sourceReceipt } = vi.hoisted(() => ({
|
|
||||||
sourceReceipt: vi.fn(),
|
|
||||||
}));
|
|
||||||
vi.mock("./api", () => ({
|
|
||||||
getSourceImportReceipt: sourceReceipt,
|
|
||||||
}));
|
|
||||||
vi.mock("./MemoryGraph3D", () => ({
|
vi.mock("./MemoryGraph3D", () => ({
|
||||||
MemoryGraph3D: ({
|
MemoryGraph3D: ({
|
||||||
graph,
|
graph,
|
||||||
@@ -982,35 +975,6 @@ const importedSources = [
|
|||||||
imported_conversations: 1,
|
imported_conversations: 1,
|
||||||
},
|
},
|
||||||
] as SourceResponse[];
|
] as SourceResponse[];
|
||||||
const importedReceipt: Receipt = {
|
|
||||||
source_id: "codex",
|
|
||||||
measured_at: "2026-09-14T00:00:00Z",
|
|
||||||
messages: 5,
|
|
||||||
conversations: 2,
|
|
||||||
person_messages: 2,
|
|
||||||
agent_messages: 2,
|
|
||||||
tool_messages: 0,
|
|
||||||
unknown_author_messages: 1,
|
|
||||||
unknown_time_messages: 1,
|
|
||||||
examples: [
|
|
||||||
{
|
|
||||||
evidence_id: "raw-codex",
|
|
||||||
role: "user",
|
|
||||||
written_by: "person",
|
|
||||||
created_at: null,
|
|
||||||
excerpt: "合成来源原文,不是治理记忆。",
|
|
||||||
truncated: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
async function openSourceReceipt() {
|
|
||||||
await userEvent.click(
|
|
||||||
screen.getByRole("button", { name: "图中选择 Codex · 2 段" }),
|
|
||||||
);
|
|
||||||
await userEvent.click(screen.getByText("查看已读内容"));
|
|
||||||
await waitFor(() => expect(sourceReceipt).toHaveBeenCalledWith("codex"));
|
|
||||||
}
|
|
||||||
|
|
||||||
it("REV09: selecting an imported source retains the canvas and only its authorized node", async () => {
|
it("REV09: selecting an imported source retains the canvas and only its authorized node", async () => {
|
||||||
render(<Host items={[]} sources={importedSources} />);
|
render(<Host items={[]} sources={importedSources} />);
|
||||||
const canvas = screen.getByTestId("canvas");
|
const canvas = screen.getByTestId("canvas");
|
||||||
@@ -1025,7 +989,8 @@ it("REV09: selecting an imported source retains the canvas and only its authoriz
|
|||||||
expect(
|
expect(
|
||||||
screen.getByRole("list", { name: "地图记忆列表" }).children,
|
screen.getByRole("list", { name: "地图记忆列表" }).children,
|
||||||
).toHaveLength(0);
|
).toHaveLength(0);
|
||||||
expect(sourceReceipt).not.toHaveBeenCalled();
|
expect(screen.getByRole("heading", { name: "记忆详情" })).toBeTruthy();
|
||||||
|
expect(screen.getByText("还没有已生成的记忆")).toBeTruthy();
|
||||||
await userEvent.click(screen.getByRole("button", { name: /全部记忆/ }));
|
await userEvent.click(screen.getByRole("button", { name: /全部记忆/ }));
|
||||||
expect(within(canvas).getAllByRole("button")).toHaveLength(2);
|
expect(within(canvas).getAllByRole("button")).toHaveLength(2);
|
||||||
expect(canvas.dataset.selected ?? "").toBe("");
|
expect(canvas.dataset.selected ?? "").toBe("");
|
||||||
@@ -1047,83 +1012,247 @@ it("REV09: fallback search matches source names and IDs without bypassing the se
|
|||||||
);
|
);
|
||||||
await userEvent.type(search, "cOdEx");
|
await userEvent.type(search, "cOdEx");
|
||||||
expect(screen.getByTestId("canvas").dataset.selected).toBe("source:codex");
|
expect(screen.getByTestId("canvas").dataset.selected).toBe("source:codex");
|
||||||
expect(screen.getByText("查看已读内容")).toBeTruthy();
|
expect(screen.getByText("还没有已生成的记忆")).toBeTruthy();
|
||||||
await userEvent.clear(search);
|
await userEvent.clear(search);
|
||||||
await userEvent.type(search, "Cursor");
|
await userEvent.type(search, "Cursor");
|
||||||
expect(screen.queryByTestId("canvas")).toBeNull();
|
expect(screen.queryByTestId("canvas")).toBeNull();
|
||||||
expect(screen.queryByText("查看已读内容")).toBeNull();
|
expect(screen.queryByText("正在读取记忆详情…")).toBeNull();
|
||||||
await userEvent.clear(search);
|
await userEvent.clear(search);
|
||||||
expect(screen.getByTestId("canvas").dataset.selected ?? "").toBe("");
|
expect(screen.getByTestId("canvas").dataset.selected ?? "").toBe("");
|
||||||
expect(screen.queryByText("查看已读内容")).toBeNull();
|
expect(screen.queryByText("正在读取记忆详情…")).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("REV09: source selection uses the actual permission-checked receipt API", async () => {
|
it("selecting a source node waits for its settled page, then opens the first memory", async () => {
|
||||||
sourceReceipt.mockResolvedValue(importedReceipt);
|
const onQuery = vi.fn();
|
||||||
render(<Host items={[]} sources={importedSources} />);
|
const onSource = vi.fn();
|
||||||
await openSourceReceipt();
|
const resetPage = vi.fn();
|
||||||
expect(
|
const oldPage = {
|
||||||
await screen.findByText(importedReceipt.examples[0].excerpt),
|
items: memories,
|
||||||
).toBeTruthy();
|
total: 2,
|
||||||
expect(screen.getByText("用户 · 时间未知")).toBeTruthy();
|
offset: 0,
|
||||||
expect(screen.getByText("原始消息").nextElementSibling?.textContent).toBe(
|
limit: 20,
|
||||||
"5",
|
source_ids_by_memory: { m0: ["codex"], m1: ["codex"] },
|
||||||
|
};
|
||||||
|
const view = render(
|
||||||
|
<Host
|
||||||
|
simplified
|
||||||
|
sources={importedSources}
|
||||||
|
libraryView={{
|
||||||
|
...pagedView(oldPage),
|
||||||
|
query: "旧查询",
|
||||||
|
settledQuery: "旧查询",
|
||||||
|
onQuery,
|
||||||
|
onSource,
|
||||||
|
resetPage,
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
);
|
);
|
||||||
// No raw conversation is read by id on the SDK data plane.
|
await act(async () => {});
|
||||||
expect(screen.queryByRole("button", { name: "查看完整对话" })).toBeNull();
|
await userEvent.click(screen.getByRole("button", { name: "图中选择 Codex" }));
|
||||||
|
expect(onQuery).toHaveBeenCalledWith("");
|
||||||
|
expect(onSource).toHaveBeenCalledWith("codex");
|
||||||
|
expect(resetPage).toHaveBeenCalledOnce();
|
||||||
|
|
||||||
|
view.rerender(
|
||||||
|
<Host
|
||||||
|
simplified
|
||||||
|
sources={importedSources}
|
||||||
|
libraryView={{
|
||||||
|
...pagedView(oldPage),
|
||||||
|
source: "codex",
|
||||||
|
settledSource: "",
|
||||||
|
loading: true,
|
||||||
|
onQuery,
|
||||||
|
onSource,
|
||||||
|
resetPage,
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
screen
|
||||||
|
.getAllByRole("status")
|
||||||
|
.some((node) => node.textContent === "正在读取记忆详情…"),
|
||||||
|
).toBe(true);
|
||||||
|
|
||||||
|
const settledItems = [memories[1], memories[0]];
|
||||||
|
view.rerender(
|
||||||
|
<Host
|
||||||
|
simplified
|
||||||
|
items={settledItems}
|
||||||
|
sources={importedSources}
|
||||||
|
libraryView={{
|
||||||
|
...pagedView({
|
||||||
|
...oldPage,
|
||||||
|
items: settledItems,
|
||||||
|
source_ids_by_memory: { m1: ["codex"], m0: ["codex"] },
|
||||||
|
}),
|
||||||
|
source: "codex",
|
||||||
|
settledSource: "codex",
|
||||||
|
settledQuery: "",
|
||||||
|
onQuery,
|
||||||
|
onSource,
|
||||||
|
resetPage,
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(await screen.findByRole("heading", { name: "第二条" })).toBeTruthy();
|
||||||
|
expect(screen.queryByText("查看已读内容")).toBeNull();
|
||||||
|
expect(document.querySelector(".source-import-receipt")).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each(["revoked", "removed", "revoked-hidden"])(
|
it("does not replace a newer memory choice when a source page settles", async () => {
|
||||||
"REV09: %s source drops fallback, preview and a late receipt without reopening on reauthorization",
|
const page = {
|
||||||
async (state) => {
|
items: memories,
|
||||||
let finish: (value: Receipt) => void = () => {};
|
total: 2,
|
||||||
sourceReceipt.mockImplementation(
|
offset: 0,
|
||||||
() =>
|
limit: 20,
|
||||||
new Promise<Receipt>((resolve) => {
|
source_ids_by_memory: { m0: ["codex"], m1: ["codex"] },
|
||||||
finish = resolve;
|
};
|
||||||
}),
|
const view = render(
|
||||||
);
|
<Host simplified sources={importedSources} libraryView={pagedView(page)} />,
|
||||||
const view = render(<Host items={[]} sources={[importedSources[0]]} />);
|
);
|
||||||
await openSourceReceipt();
|
await act(async () => {});
|
||||||
view.rerender(
|
await userEvent.click(screen.getByRole("button", { name: "图中选择 Codex" }));
|
||||||
<Host
|
await userEvent.click(
|
||||||
items={[]}
|
screen.getByRole("button", { name: "图中选择 第二条" }),
|
||||||
hidden={state === "revoked-hidden"}
|
);
|
||||||
sources={
|
expect(await screen.findByRole("heading", { name: "第二条" })).toBeTruthy();
|
||||||
state === "removed"
|
|
||||||
? []
|
|
||||||
: [{ ...importedSources[0], authorized: false }]
|
|
||||||
}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
expect(screen.queryByTestId("canvas")).toBeNull();
|
|
||||||
expect(document.querySelector(".source-import-receipt")).toBeNull();
|
|
||||||
await act(async () => finish(importedReceipt));
|
|
||||||
view.rerender(<Host items={[]} sources={[importedSources[0]]} />);
|
|
||||||
expect(screen.getByTestId("canvas").dataset.selected ?? "").toBe("");
|
|
||||||
expect(screen.queryByText(importedReceipt.examples[0].excerpt)).toBeNull();
|
|
||||||
expect(screen.queryByText("查看已读内容")).toBeNull();
|
|
||||||
expect(sourceReceipt).toHaveBeenCalledTimes(1);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
it("REV09: compact preview close keeps source selection for reopening", async () => {
|
view.rerender(
|
||||||
|
<Host
|
||||||
|
simplified
|
||||||
|
sources={importedSources}
|
||||||
|
libraryView={{
|
||||||
|
...pagedView(page),
|
||||||
|
source: "codex",
|
||||||
|
settledSource: "codex",
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(screen.getByRole("heading", { name: "第二条" })).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("closing the compact source inspector cancels its pending auto-open", async () => {
|
||||||
vi.stubGlobal("matchMedia", (query: string) => ({
|
vi.stubGlobal("matchMedia", (query: string) => ({
|
||||||
matches: query.includes("max-width"),
|
matches: query.includes("max-width"),
|
||||||
addEventListener() {},
|
addEventListener() {},
|
||||||
removeEventListener() {},
|
removeEventListener() {},
|
||||||
}));
|
}));
|
||||||
sourceReceipt.mockResolvedValue(importedReceipt);
|
const page = {
|
||||||
render(<Host items={[]} sources={importedSources} />);
|
items: memories,
|
||||||
await openSourceReceipt();
|
total: 2,
|
||||||
await screen.findByText(importedReceipt.examples[0].excerpt);
|
offset: 0,
|
||||||
|
limit: 20,
|
||||||
|
source_ids_by_memory: { m0: ["codex"], m1: ["codex"] },
|
||||||
|
};
|
||||||
|
const view = render(
|
||||||
|
<Host simplified sources={importedSources} libraryView={pagedView(page)} />,
|
||||||
|
);
|
||||||
|
await act(async () => {});
|
||||||
|
await userEvent.click(screen.getByRole("button", { name: "图中选择 Codex" }));
|
||||||
await userEvent.click(screen.getByRole("button", { name: "关闭记忆详情" }));
|
await userEvent.click(screen.getByRole("button", { name: "关闭记忆详情" }));
|
||||||
expect(document.querySelector(".source-import-receipt")).toBeNull();
|
|
||||||
|
view.rerender(
|
||||||
|
<Host
|
||||||
|
simplified
|
||||||
|
sources={importedSources}
|
||||||
|
libraryView={{
|
||||||
|
...pagedView(page),
|
||||||
|
source: "codex",
|
||||||
|
settledSource: "codex",
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(screen.queryByRole("dialog", { name: "记忆与原文" })).toBeNull();
|
||||||
|
expect(screen.queryByRole("heading", { name: "第一条" })).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps the source selection and offers retry when its page fails", async () => {
|
||||||
|
const onRetry = vi.fn();
|
||||||
|
const page = {
|
||||||
|
items: memories,
|
||||||
|
total: 2,
|
||||||
|
offset: 0,
|
||||||
|
limit: 20,
|
||||||
|
source_ids_by_memory: { m0: ["codex"], m1: ["codex"] },
|
||||||
|
};
|
||||||
|
const view = render(
|
||||||
|
<Host simplified sources={importedSources} libraryView={pagedView(page)} />,
|
||||||
|
);
|
||||||
|
await act(async () => {});
|
||||||
|
await userEvent.click(screen.getByRole("button", { name: "图中选择 Codex" }));
|
||||||
|
view.rerender(
|
||||||
|
<Host
|
||||||
|
simplified
|
||||||
|
sources={importedSources}
|
||||||
|
libraryView={{
|
||||||
|
...pagedView(page),
|
||||||
|
source: "codex",
|
||||||
|
settledSource: "codex",
|
||||||
|
failed: true,
|
||||||
|
onRetry,
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
screen
|
||||||
|
.getAllByRole("alert")
|
||||||
|
.some((node) => node.textContent === "记忆暂时无法读取"),
|
||||||
|
).toBe(true);
|
||||||
|
await userEvent.click(screen.getByRole("button", { name: "重试" }));
|
||||||
|
expect(onRetry).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("drops a pending source selection when that source is revoked", async () => {
|
||||||
|
const page = {
|
||||||
|
items: memories,
|
||||||
|
total: 2,
|
||||||
|
offset: 0,
|
||||||
|
limit: 20,
|
||||||
|
source_ids_by_memory: { m0: ["codex"], m1: ["codex"] },
|
||||||
|
};
|
||||||
|
const view = render(
|
||||||
|
<Host
|
||||||
|
simplified
|
||||||
|
sources={[importedSources[0]]}
|
||||||
|
libraryView={pagedView(page)}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
await act(async () => {});
|
||||||
|
await userEvent.click(screen.getByRole("button", { name: "图中选择 Codex" }));
|
||||||
expect(screen.getByTestId("canvas").dataset.selected).toBe("source:codex");
|
expect(screen.getByTestId("canvas").dataset.selected).toBe("source:codex");
|
||||||
await userEvent.click(
|
|
||||||
screen.getByRole("button", { name: "查看选中来源原文" }),
|
view.rerender(
|
||||||
|
<Host
|
||||||
|
simplified
|
||||||
|
sources={[{ ...importedSources[0], authorized: false }]}
|
||||||
|
libraryView={{
|
||||||
|
...pagedView(page),
|
||||||
|
source: "codex",
|
||||||
|
settledSource: "codex",
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(screen.queryByRole("button", { name: "图中选择 Codex" })).toBeNull();
|
||||||
|
expect(screen.getByTestId("canvas").dataset.selected).not.toBe(
|
||||||
|
"source:codex",
|
||||||
|
);
|
||||||
|
expect(screen.queryByText("正在读取记忆详情…")).toBeNull();
|
||||||
|
|
||||||
|
view.rerender(
|
||||||
|
<Host
|
||||||
|
simplified
|
||||||
|
sources={[importedSources[0]]}
|
||||||
|
libraryView={{
|
||||||
|
...pagedView(page),
|
||||||
|
source: "codex",
|
||||||
|
settledSource: "codex",
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(screen.getByTestId("canvas").dataset.selected).not.toBe(
|
||||||
|
"source:codex",
|
||||||
);
|
);
|
||||||
expect(screen.getByRole("dialog", { name: "记忆与原文" })).toBeTruthy();
|
|
||||||
expect(screen.getByText("查看已读内容")).toBeTruthy();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("REV09: first governed memories replace fallback without remounting the graph or breaking memory IDs", async () => {
|
it("REV09: first governed memories replace fallback without remounting the graph or breaking memory IDs", async () => {
|
||||||
@@ -1162,5 +1291,4 @@ it.each([
|
|||||||
render(<Host items={[]} sources={sources} />);
|
render(<Host items={[]} sources={sources} />);
|
||||||
expect(screen.queryByTestId("canvas")).toBeNull();
|
expect(screen.queryByTestId("canvas")).toBeNull();
|
||||||
expect(document.querySelector(".source-import-receipt")).toBeNull();
|
expect(document.querySelector(".source-import-receipt")).toBeNull();
|
||||||
expect(sourceReceipt).not.toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
@@ -48,7 +48,6 @@ import {
|
|||||||
import { OptionMenu } from "./OptionMenu";
|
import { OptionMenu } from "./OptionMenu";
|
||||||
import { R6Button } from "./R6Controls";
|
import { R6Button } from "./R6Controls";
|
||||||
import { SelectedMemoryDetail } from "./SelectedMemoryDetail";
|
import { SelectedMemoryDetail } from "./SelectedMemoryDetail";
|
||||||
import { SourceImportReceipt } from "./SourceImportReceipt";
|
|
||||||
import { sourceNameOr } from "./source-label";
|
import { sourceNameOr } from "./source-label";
|
||||||
import { useMediaPreference } from "./use-media-preference";
|
import { useMediaPreference } from "./use-media-preference";
|
||||||
import { Answer, NodeDetail, type SavedAnswer } from "./Workbench";
|
import { Answer, NodeDetail, type SavedAnswer } from "./Workbench";
|
||||||
@@ -97,6 +96,8 @@ type Props = {
|
|||||||
loading: boolean;
|
loading: boolean;
|
||||||
failed: boolean;
|
failed: boolean;
|
||||||
stale: boolean;
|
stale: boolean;
|
||||||
|
settledSource?: string;
|
||||||
|
settledQuery?: string;
|
||||||
/** The source id the SDK uses for "no source given" (shown as 未识别来源). */
|
/** The source id the SDK uses for "no source given" (shown as 未识别来源). */
|
||||||
unattributedSource?: string;
|
unattributedSource?: string;
|
||||||
onQuery(value: string): void;
|
onQuery(value: string): void;
|
||||||
@@ -213,6 +214,9 @@ export function MemoryMap(props: Props) {
|
|||||||
}, [submittedQuery]);
|
}, [submittedQuery]);
|
||||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||||
const [groupNode, setGroupNode] = useState<MemoryGraphNode | null>(null);
|
const [groupNode, setGroupNode] = useState<MemoryGraphNode | null>(null);
|
||||||
|
const [pendingSourceSelection, setPendingSourceSelection] = useState<
|
||||||
|
string | null
|
||||||
|
>(null);
|
||||||
const [question, setQuestion] = useState("");
|
const [question, setQuestion] = useState("");
|
||||||
const [inspectorOpen, setInspectorOpen] = useState(false);
|
const [inspectorOpen, setInspectorOpen] = useState(false);
|
||||||
const [filterNotice, setFilterNotice] = useState("");
|
const [filterNotice, setFilterNotice] = useState("");
|
||||||
@@ -363,6 +367,7 @@ export function MemoryMap(props: Props) {
|
|||||||
// Source revocation also clears a suspended preview, without closing shared memory detail.
|
// Source revocation also clears a suspended preview, without closing shared memory detail.
|
||||||
if (groupNode && !visibleGroupNode) {
|
if (groupNode && !visibleGroupNode) {
|
||||||
setGroupNode(null);
|
setGroupNode(null);
|
||||||
|
setPendingSourceSelection(null);
|
||||||
setInspectorOpen(false);
|
setInspectorOpen(false);
|
||||||
}
|
}
|
||||||
}, [groupNode, visibleGroupNode]);
|
}, [groupNode, visibleGroupNode]);
|
||||||
@@ -389,6 +394,7 @@ export function MemoryMap(props: Props) {
|
|||||||
const pick = useCallback(
|
const pick = useCallback(
|
||||||
(memoryId: string, scroll = true) => {
|
(memoryId: string, scroll = true) => {
|
||||||
if (!active.some((memory) => memory.memory_id === memoryId)) return;
|
if (!active.some((memory) => memory.memory_id === memoryId)) return;
|
||||||
|
setPendingSourceSelection(null);
|
||||||
scrollSelection.current = scroll;
|
scrollSelection.current = scroll;
|
||||||
if (!filtered.some((memory) => memory.memory_id === memoryId)) {
|
if (!filtered.some((memory) => memory.memory_id === memoryId)) {
|
||||||
setGroupKey("");
|
setGroupKey("");
|
||||||
@@ -402,6 +408,32 @@ export function MemoryMap(props: Props) {
|
|||||||
},
|
},
|
||||||
[active, filtered, onOpenMemory],
|
[active, filtered, onOpenMemory],
|
||||||
);
|
);
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
!library ||
|
||||||
|
!pendingSourceSelection ||
|
||||||
|
overlaysSuspended ||
|
||||||
|
library.loading ||
|
||||||
|
library.failed ||
|
||||||
|
!selectedSource ||
|
||||||
|
selectedSource.source_id !== pendingSourceSelection ||
|
||||||
|
library.source !== pendingSourceSelection ||
|
||||||
|
(library.settledSource ?? library.source) !== pendingSourceSelection ||
|
||||||
|
(library.settledQuery ?? library.query).trim() ||
|
||||||
|
library.page?.offset !== 0
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
setPendingSourceSelection(null);
|
||||||
|
const first = active[0];
|
||||||
|
if (first) pick(first.memory_id, false);
|
||||||
|
}, [
|
||||||
|
active,
|
||||||
|
library,
|
||||||
|
overlaysSuspended,
|
||||||
|
pendingSourceSelection,
|
||||||
|
pick,
|
||||||
|
selectedSource,
|
||||||
|
]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!props.simplified || openedDefault.current || overlaysSuspended) return;
|
if (!props.simplified || openedDefault.current || overlaysSuspended) return;
|
||||||
if (library?.loading && filtered.length === 0) return;
|
if (library?.loading && filtered.length === 0) return;
|
||||||
@@ -452,6 +484,7 @@ export function MemoryMap(props: Props) {
|
|||||||
const clear = useCallback(() => {
|
const clear = useCallback(() => {
|
||||||
setSelectedId(null);
|
setSelectedId(null);
|
||||||
setGroupNode(null);
|
setGroupNode(null);
|
||||||
|
setPendingSourceSelection(null);
|
||||||
setInspectorOpen(false);
|
setInspectorOpen(false);
|
||||||
onCloseDetail();
|
onCloseDetail();
|
||||||
}, [onCloseDetail]);
|
}, [onCloseDetail]);
|
||||||
@@ -483,6 +516,7 @@ export function MemoryMap(props: Props) {
|
|||||||
clear();
|
clear();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
setPendingSourceSelection(null);
|
||||||
setInspectorOpen(false);
|
setInspectorOpen(false);
|
||||||
}
|
}
|
||||||
function chooseGroup(key: string) {
|
function chooseGroup(key: string) {
|
||||||
@@ -509,8 +543,17 @@ export function MemoryMap(props: Props) {
|
|||||||
setAxis("source");
|
setAxis("source");
|
||||||
setGroupKey(`source:${node.source_id}`);
|
setGroupKey(`source:${node.source_id}`);
|
||||||
if (library) {
|
if (library) {
|
||||||
|
setPendingSourceSelection(node.source_id);
|
||||||
|
library.onQuery("");
|
||||||
library.onSource(node.source_id);
|
library.onSource(node.source_id);
|
||||||
library.resetPage();
|
library.resetPage();
|
||||||
|
} else {
|
||||||
|
const first = sourceGroups
|
||||||
|
.find((entry) => entry.key === `source:${node.source_id}`)
|
||||||
|
?.memoryIds.find((memoryId) =>
|
||||||
|
active.some((memory) => memory.memory_id === memoryId),
|
||||||
|
);
|
||||||
|
if (first) pick(first, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (node.node_type === "topic") {
|
if (node.node_type === "topic") {
|
||||||
@@ -987,11 +1030,13 @@ export function MemoryMap(props: Props) {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="quiet map-tool"
|
className="quiet map-tool"
|
||||||
aria-label={text(
|
aria-label={text("记忆详情")}
|
||||||
selectedSource ? "查看选中来源原文" : "查看选中记忆原文",
|
|
||||||
)}
|
|
||||||
title={text("查看原文")}
|
title={text("查看原文")}
|
||||||
onClick={() => setInspectorOpen(true)}
|
onClick={() => {
|
||||||
|
setInspectorOpen(true);
|
||||||
|
if (selectedSource && library)
|
||||||
|
setPendingSourceSelection(selectedSource.source_id);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<FileText size={17} />
|
<FileText size={17} />
|
||||||
</button>
|
</button>
|
||||||
@@ -1126,7 +1171,7 @@ export function MemoryMap(props: Props) {
|
|||||||
{inspectorDetail
|
{inspectorDetail
|
||||||
? text("记忆详情")
|
? text("记忆详情")
|
||||||
: selectedSource
|
: selectedSource
|
||||||
? text("来源原文")
|
? text("记忆详情")
|
||||||
: visibleGroupNode
|
: visibleGroupNode
|
||||||
? text("关联节点")
|
? text("关联节点")
|
||||||
: text("记忆详情")}
|
: text("记忆详情")}
|
||||||
@@ -1145,13 +1190,33 @@ export function MemoryMap(props: Props) {
|
|||||||
</header>
|
</header>
|
||||||
<div className="map-inspector-body" ref={inspectorBody}>
|
<div className="map-inspector-body" ref={inspectorBody}>
|
||||||
{selectedSource ? (
|
{selectedSource ? (
|
||||||
inspectorOpen &&
|
<div className="map-inspector-empty map-inspector-loading">
|
||||||
!overlaysSuspended && (
|
{library?.failed &&
|
||||||
<SourceImportReceipt
|
library.source === selectedSource.source_id ? (
|
||||||
key={selectedSource.source_id}
|
<>
|
||||||
source={selectedSource}
|
<p role="alert">{text("记忆暂时无法读取")}</p>
|
||||||
/>
|
<button
|
||||||
)
|
type="button"
|
||||||
|
className="secondary"
|
||||||
|
onClick={library.onRetry}
|
||||||
|
>
|
||||||
|
{text("重试")}
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
) : pendingSourceSelection || library?.loading ? (
|
||||||
|
<>
|
||||||
|
<div className="map-detail-skeleton" aria-hidden="true">
|
||||||
|
<i />
|
||||||
|
<i />
|
||||||
|
<i />
|
||||||
|
<i />
|
||||||
|
</div>
|
||||||
|
<p role="status">{text("正在读取记忆详情…")}</p>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<p>{text("还没有已生成的记忆")}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
) : props.simplified && inspectorDetail ? (
|
) : props.simplified && inspectorDetail ? (
|
||||||
<div
|
<div
|
||||||
aria-busy={
|
aria-busy={
|
||||||
|
|||||||
@@ -1027,6 +1027,8 @@ export function MemorySpace({
|
|||||||
loading: sdkLibrary.loading,
|
loading: sdkLibrary.loading,
|
||||||
failed: sdkLibrary.failed,
|
failed: sdkLibrary.failed,
|
||||||
stale: false,
|
stale: false,
|
||||||
|
settledSource: sdkLibrary.settledSource,
|
||||||
|
settledQuery: sdkLibrary.settledQuery,
|
||||||
unattributedSource: UNATTRIBUTED_SOURCE_ID,
|
unattributedSource: UNATTRIBUTED_SOURCE_ID,
|
||||||
onQuery: setSdkLibraryQuery,
|
onQuery: setSdkLibraryQuery,
|
||||||
onSource: setSdkLibrarySource,
|
onSource: setSdkLibrarySource,
|
||||||
|
|||||||
Reference in new issue
Block a user