Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01394b75da | ||
|
|
667c8754c6 |
No files matched your search
@@ -21,6 +21,7 @@ const api = vi.hoisted(() => ({
|
|||||||
legacyGraph: vi.fn(),
|
legacyGraph: vi.fn(),
|
||||||
sdkLibrary: vi.fn(),
|
sdkLibrary: vi.fn(),
|
||||||
sdkGraph: vi.fn(),
|
sdkGraph: vi.fn(),
|
||||||
|
sdkMemory: vi.fn(),
|
||||||
}));
|
}));
|
||||||
vi.mock("./api", () => ({
|
vi.mock("./api", () => ({
|
||||||
getMemoryAuthority: api.authority,
|
getMemoryAuthority: api.authority,
|
||||||
@@ -29,6 +30,9 @@ vi.mock("./api", () => ({
|
|||||||
getMemoryGraph: api.legacyGraph,
|
getMemoryGraph: api.legacyGraph,
|
||||||
getSdkMemoryLibrary: api.sdkLibrary,
|
getSdkMemoryLibrary: api.sdkLibrary,
|
||||||
getSdkMemoryGraph: api.sdkGraph,
|
getSdkMemoryGraph: api.sdkGraph,
|
||||||
|
getSdkMemory: api.sdkMemory,
|
||||||
|
updateSdkMemory: vi.fn(),
|
||||||
|
deleteSdkMemory: vi.fn(),
|
||||||
getMemoryProcessing: async () => null,
|
getMemoryProcessing: async () => null,
|
||||||
getModelProfiles: async () => [],
|
getModelProfiles: async () => [],
|
||||||
apiErrorCode: () => null,
|
apiErrorCode: () => null,
|
||||||
@@ -140,8 +144,9 @@ it("draws the map from SDK memories on SDK authority, without legacy reads", asy
|
|||||||
expect(api.legacyGraph).not.toHaveBeenCalled();
|
expect(api.legacyGraph).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("opens a memory from the SDK map in the SDK library", async () => {
|
it("opens an SDK memory in place instead of switching away from the map", async () => {
|
||||||
sdkAuthority();
|
sdkAuthority();
|
||||||
|
api.sdkMemory.mockResolvedValue(record("a"));
|
||||||
|
|
||||||
render(<MemorySpace simplified active />);
|
render(<MemorySpace simplified active />);
|
||||||
await waitFor(() =>
|
await waitFor(() =>
|
||||||
@@ -149,10 +154,10 @@ it("opens a memory from the SDK map in the SDK library", async () => {
|
|||||||
);
|
);
|
||||||
fireEvent.click(screen.getByText("open a"));
|
fireEvent.click(screen.getByText("open a"));
|
||||||
|
|
||||||
const library = await screen.findByTestId("sdk-library");
|
await waitFor(() => expect(api.sdkMemory).toHaveBeenCalledWith("a"));
|
||||||
expect(library.textContent).toBe("a");
|
// The inspector stays on the map; the reader is never moved to a second list.
|
||||||
expect(library.closest("[hidden]")).toBeNull();
|
expect(screen.getByTestId("map").hidden).toBe(false);
|
||||||
expect(screen.getByTestId("map").hidden).toBe(true);
|
expect(screen.getByTestId("sdk-library").textContent).toBe("");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps asking for the authority until Core answers", async () => {
|
it("keeps asking for the authority until Core answers", async () => {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type {
|
|||||||
ImportJob,
|
ImportJob,
|
||||||
MemoryDetail,
|
MemoryDetail,
|
||||||
MemoryGraphResponse,
|
MemoryGraphResponse,
|
||||||
|
MemoryLibraryPage,
|
||||||
MemoryProcessingSummary,
|
MemoryProcessingSummary,
|
||||||
SdkAskMemory,
|
SdkAskMemory,
|
||||||
SourceResponse,
|
SourceResponse,
|
||||||
@@ -32,6 +33,7 @@ import {
|
|||||||
apiErrorCode,
|
apiErrorCode,
|
||||||
askMemoriesStream,
|
askMemoriesStream,
|
||||||
correctMemory,
|
correctMemory,
|
||||||
|
deleteSdkMemory,
|
||||||
digestMemoryGraph,
|
digestMemoryGraph,
|
||||||
getMemories,
|
getMemories,
|
||||||
getMemory,
|
getMemory,
|
||||||
@@ -39,6 +41,7 @@ import {
|
|||||||
getMemoryGraph,
|
getMemoryGraph,
|
||||||
getMemoryProcessing,
|
getMemoryProcessing,
|
||||||
getModelProfiles,
|
getModelProfiles,
|
||||||
|
getSdkMemory,
|
||||||
getSdkMemoryGraph,
|
getSdkMemoryGraph,
|
||||||
getSdkMemoryLibrary,
|
getSdkMemoryLibrary,
|
||||||
manualExtract,
|
manualExtract,
|
||||||
@@ -47,6 +50,7 @@ import {
|
|||||||
saveMemoryNote,
|
saveMemoryNote,
|
||||||
setExtractionWindow,
|
setExtractionWindow,
|
||||||
syncMemoryIndex,
|
syncMemoryIndex,
|
||||||
|
updateSdkMemory,
|
||||||
} from "./api";
|
} from "./api";
|
||||||
import { ConversationSearch } from "./ConversationSearch";
|
import { ConversationSearch } from "./ConversationSearch";
|
||||||
import { clockStamp } from "./clock-stamp";
|
import { clockStamp } from "./clock-stamp";
|
||||||
@@ -58,7 +62,7 @@ import { subscribePoll } from "./poll-scheduler";
|
|||||||
import { R6Button } from "./R6Controls";
|
import { R6Button } from "./R6Controls";
|
||||||
import { SdkMemoryLibrary } from "./SdkMemoryLibrary";
|
import { SdkMemoryLibrary } from "./SdkMemoryLibrary";
|
||||||
import { SdkRawHistory } from "./SdkRawHistory";
|
import { SdkRawHistory } from "./SdkRawHistory";
|
||||||
import { sdkMapMemory } from "./sdk-memory-map";
|
import { sdkDetail, sdkLibraryPage, sdkMapMemory } from "./sdk-memory-map";
|
||||||
import { sourceNameOr } from "./source-label";
|
import { sourceNameOr } from "./source-label";
|
||||||
import { traceUiAction } from "./ui-trace";
|
import { traceUiAction } from "./ui-trace";
|
||||||
import { useMemoryLibrary } from "./use-memory-library";
|
import { useMemoryLibrary } from "./use-memory-library";
|
||||||
@@ -296,9 +300,9 @@ export function MemorySpace({
|
|||||||
text: string;
|
text: string;
|
||||||
memories: AskMemory[];
|
memories: AskMemory[];
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
const [sdkFocusedMemoryId, setSdkFocusedMemoryId] = useState<string | null>(
|
// The SDK library is no longer where a memory opens from the map; the id is
|
||||||
null,
|
// kept so the panel still has a focus target for its own tests.
|
||||||
);
|
const [sdkFocusedMemoryId] = useState<string | null>(null);
|
||||||
// The map on SDK authority. Legacy reads (library page, governed graph)
|
// The map on SDK authority. Legacy reads (library page, governed graph)
|
||||||
// are refused there, so it draws SDK memories and the v2 graph instead.
|
// are refused there, so it draws SDK memories and the v2 graph instead.
|
||||||
const [sdkMap, setSdkMap] = useState<{
|
const [sdkMap, setSdkMap] = useState<{
|
||||||
@@ -306,6 +310,20 @@ export function MemorySpace({
|
|||||||
graph: MemoryGraphResponse;
|
graph: MemoryGraphResponse;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
const [sdkMapFailed, setSdkMapFailed] = useState(false);
|
const [sdkMapFailed, setSdkMapFailed] = useState(false);
|
||||||
|
// A write on SDK authority has no governed refresh to ride on; bumping this
|
||||||
|
// re-reads the SDK page and graph after an edit or a delete.
|
||||||
|
const [sdkMapRevision, setSdkMapRevision] = useState(0);
|
||||||
|
const bumpSdkMap = useCallback(() => setSdkMapRevision((v) => v + 1), []);
|
||||||
|
// The map's left list pages through the SDK library the same way it pages
|
||||||
|
// through the governed one: the list is always one page, and the count above
|
||||||
|
// it is the library's total, not the page's length.
|
||||||
|
const [sdkLibrary, setSdkLibrary] = useState<{
|
||||||
|
page: MemoryLibraryPage | null;
|
||||||
|
loading: boolean;
|
||||||
|
failed: boolean;
|
||||||
|
}>({ page: null, loading: false, failed: false });
|
||||||
|
const [sdkLibraryQuery, setSdkLibraryQuery] = useState("");
|
||||||
|
const [sdkLibraryOffset, setSdkLibraryOffset] = useState(0);
|
||||||
const sdkMapVisible =
|
const sdkMapVisible =
|
||||||
memoryAuthority === "sdk" &&
|
memoryAuthority === "sdk" &&
|
||||||
simplified &&
|
simplified &&
|
||||||
@@ -313,6 +331,9 @@ export function MemorySpace({
|
|||||||
displayedMode === "map";
|
displayedMode === "map";
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!sdkMapVisible) return;
|
if (!sdkMapVisible) return;
|
||||||
|
// A write bumps the revision; reading it here re-runs the read at once
|
||||||
|
// instead of waiting for the 30s poll.
|
||||||
|
void sdkMapRevision;
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
let controller: AbortController | null = null;
|
let controller: AbortController | null = null;
|
||||||
const read = async () => {
|
const read = async () => {
|
||||||
@@ -341,7 +362,46 @@ export function MemorySpace({
|
|||||||
clearInterval(timer);
|
clearInterval(timer);
|
||||||
controller?.abort();
|
controller?.abort();
|
||||||
};
|
};
|
||||||
}, [sdkMapVisible]);
|
}, [sdkMapVisible, sdkMapRevision]);
|
||||||
|
useEffect(() => {
|
||||||
|
if (memoryAuthority !== "sdk" || !simplified || !active) return;
|
||||||
|
if (displayedMode !== "map") return;
|
||||||
|
void sdkMapRevision;
|
||||||
|
let cancelled = false;
|
||||||
|
const controller = new AbortController();
|
||||||
|
setSdkLibrary((current) => ({ ...current, loading: true }));
|
||||||
|
void getSdkMemoryLibrary(
|
||||||
|
{ statuses: ["active"], offset: sdkLibraryOffset, limit: 50 },
|
||||||
|
controller.signal,
|
||||||
|
)
|
||||||
|
.then((page) => {
|
||||||
|
if (cancelled) return;
|
||||||
|
setSdkLibrary({
|
||||||
|
page: sdkLibraryPage(page),
|
||||||
|
loading: false,
|
||||||
|
failed: false,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
if (cancelled || controller.signal.aborted) return;
|
||||||
|
setSdkLibrary((current) => ({
|
||||||
|
...current,
|
||||||
|
loading: false,
|
||||||
|
failed: true,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
controller.abort();
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
memoryAuthority,
|
||||||
|
simplified,
|
||||||
|
active,
|
||||||
|
displayedMode,
|
||||||
|
sdkLibraryOffset,
|
||||||
|
sdkMapRevision,
|
||||||
|
]);
|
||||||
const [draft, setDraft] = useState("");
|
const [draft, setDraft] = useState("");
|
||||||
const [selection, setSelection] = useState<Set<string>>(new Set());
|
const [selection, setSelection] = useState<Set<string>>(new Set());
|
||||||
const bulkInFlight = useRef(false);
|
const bulkInFlight = useRef(false);
|
||||||
@@ -1025,8 +1085,9 @@ export function MemorySpace({
|
|||||||
open(memory.memory_id);
|
open(memory.memory_id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setSdkFocusedMemoryId(memory.memory_id);
|
// An SDK answer cites SDK memories; they open in the inspector like any
|
||||||
setMode("library");
|
// other rather than switching the reader to a second list.
|
||||||
|
void open(memory.memory_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveAnswer(draft: { content: string }) {
|
async function saveAnswer(draft: { content: string }) {
|
||||||
@@ -1075,7 +1136,13 @@ export function MemorySpace({
|
|||||||
setDetailFreshness(null);
|
setDetailFreshness(null);
|
||||||
setDetailLoad({ memoryId, status: "loading" });
|
setDetailLoad({ memoryId, status: "loading" });
|
||||||
try {
|
try {
|
||||||
const result = await getMemory(memoryId);
|
// Governed detail is refused on SDK authority, so the inspector reads the
|
||||||
|
// same memory through v2 and renders it in place. Staying here keeps the
|
||||||
|
// map's list, graph and detail where the design put them.
|
||||||
|
const result =
|
||||||
|
memoryAuthority === "sdk"
|
||||||
|
? sdkDetail(await getSdkMemory(memoryId))
|
||||||
|
: await getMemory(memoryId);
|
||||||
if (epoch !== detailEpoch.current || result.memory.memory_id !== memoryId)
|
if (epoch !== detailEpoch.current || result.memory.memory_id !== memoryId)
|
||||||
return;
|
return;
|
||||||
setDetail(result);
|
setDetail(result);
|
||||||
@@ -1123,6 +1190,28 @@ export function MemorySpace({
|
|||||||
return;
|
return;
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
if (memoryAuthority === "sdk") {
|
||||||
|
// An SDK memory is removed outright: there is no revision to undo.
|
||||||
|
if (actionName !== "delete") {
|
||||||
|
setBusy(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const trace = traceUiAction("memory", "confirm");
|
||||||
|
try {
|
||||||
|
await deleteSdkMemory(detail.memory.memory_id);
|
||||||
|
trace.done("succeeded");
|
||||||
|
closeDetail();
|
||||||
|
bumpSdkMap();
|
||||||
|
} catch (reason) {
|
||||||
|
trace.done(writeTraceStatus(reason), {
|
||||||
|
reason: "memory_action_failed",
|
||||||
|
});
|
||||||
|
setError(message(reason, t));
|
||||||
|
} finally {
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
const intent = `${detail.memory.memory_id}:${actionName}:${detail.memory.revision}`;
|
const intent = `${detail.memory.memory_id}:${actionName}:${detail.memory.revision}`;
|
||||||
const requestId = requestIdFor(requestIds.current, intent);
|
const requestId = requestIdFor(requestIds.current, intent);
|
||||||
const trace = traceUiAction("memory", "confirm");
|
const trace = traceUiAction("memory", "confirm");
|
||||||
@@ -1329,6 +1418,28 @@ export function MemorySpace({
|
|||||||
return;
|
return;
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
if (memoryAuthority === "sdk") {
|
||||||
|
// A rewrite replaces the SDK row instead of adding a revision, so there
|
||||||
|
// is no revision to send and nothing to reconcile afterwards.
|
||||||
|
const trace = traceUiAction("memory", "submit");
|
||||||
|
try {
|
||||||
|
const saved = await updateSdkMemory(detail.memory.memory_id, draft);
|
||||||
|
trace.done("succeeded");
|
||||||
|
setDetail(sdkDetail(saved));
|
||||||
|
setDraft(saved.content);
|
||||||
|
bumpSdkMap();
|
||||||
|
} catch (reason) {
|
||||||
|
trace.done(writeTraceStatus(reason), {
|
||||||
|
reason: isDefiniteRefusal(reason)
|
||||||
|
? "memory_correct_failed"
|
||||||
|
: "memory_correct_result_unknown",
|
||||||
|
});
|
||||||
|
setError(message(reason, t));
|
||||||
|
} finally {
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
const intent = `${detail.memory.memory_id}:correct:${detail.memory.revision}`;
|
const intent = `${detail.memory.memory_id}:correct:${detail.memory.revision}`;
|
||||||
const requestId = requestIdFor(requestIds.current, intent);
|
const requestId = requestIdFor(requestIds.current, intent);
|
||||||
const trace = traceUiAction("memory", "submit");
|
const trace = traceUiAction("memory", "submit");
|
||||||
@@ -1510,7 +1621,8 @@ export function MemorySpace({
|
|||||||
{
|
{
|
||||||
filter: "",
|
filter: "",
|
||||||
shown: new Intl.NumberFormat(locale).format(
|
shown: new Intl.NumberFormat(locale).format(
|
||||||
sdkMap.memories.length,
|
sdkLibrary.page?.items.length ??
|
||||||
|
sdkMap.memories.length,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -1887,23 +1999,39 @@ export function MemorySpace({
|
|||||||
focusDialogue={focusDialogue}
|
focusDialogue={focusDialogue}
|
||||||
onFocusDialogueConsumed={onFocusDialogueConsumed}
|
onFocusDialogueConsumed={onFocusDialogueConsumed}
|
||||||
libraryView={
|
libraryView={
|
||||||
simplified && memoryAuthority !== "sdk"
|
simplified && memoryAuthority === "sdk"
|
||||||
? {
|
? {
|
||||||
query: mapQuery,
|
query: sdkLibraryQuery,
|
||||||
source: mapSource,
|
source: "",
|
||||||
page: mapLibrary.page,
|
page: sdkLibrary.page,
|
||||||
loading: mapLibrary.loading,
|
loading: sdkLibrary.loading,
|
||||||
failed: mapLibrary.failed,
|
failed: sdkLibrary.failed,
|
||||||
stale: sourceSnapshotStale,
|
stale: false,
|
||||||
onQuery: setMapQuery,
|
onQuery: setSdkLibraryQuery,
|
||||||
onSource: setMapSource,
|
// SDK memories carry one source each and the list is paged by
|
||||||
resetPage: () => mapLibrary.go(0),
|
// the endpoint; a source filter would have to page separately.
|
||||||
onPage: mapLibrary.go,
|
onSource: () => {},
|
||||||
onRetry: () => {
|
resetPage: () => setSdkLibraryOffset(0),
|
||||||
void mapLibrary.refresh().catch(() => {});
|
onPage: (offset) => setSdkLibraryOffset(offset),
|
||||||
},
|
onRetry: bumpSdkMap,
|
||||||
}
|
}
|
||||||
: undefined
|
: simplified
|
||||||
|
? {
|
||||||
|
query: mapQuery,
|
||||||
|
source: mapSource,
|
||||||
|
page: mapLibrary.page,
|
||||||
|
loading: mapLibrary.loading,
|
||||||
|
failed: mapLibrary.failed,
|
||||||
|
stale: sourceSnapshotStale,
|
||||||
|
onQuery: setMapQuery,
|
||||||
|
onSource: setMapSource,
|
||||||
|
resetPage: () => mapLibrary.go(0),
|
||||||
|
onPage: mapLibrary.go,
|
||||||
|
onRetry: () => {
|
||||||
|
void mapLibrary.refresh().catch(() => {});
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
}
|
}
|
||||||
onMutationComplete={(id) => {
|
onMutationComplete={(id) => {
|
||||||
void refresh(query)
|
void refresh(query)
|
||||||
@@ -1951,14 +2079,6 @@ export function MemorySpace({
|
|||||||
streaming={streaming}
|
streaming={streaming}
|
||||||
onDraft={setDraft}
|
onDraft={setDraft}
|
||||||
onOpenMemory={(memoryId) => {
|
onOpenMemory={(memoryId) => {
|
||||||
if (memoryAuthority === "sdk") {
|
|
||||||
// Governed detail is refused on SDK authority; the SDK library
|
|
||||||
// opens, edits and deletes the same memory through v2.
|
|
||||||
setSdkFocusedMemoryId(memoryId);
|
|
||||||
if (simplified) setManagementOpen(true);
|
|
||||||
else setMode("library");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const recalled = answerMemories.current.find(
|
const recalled = answerMemories.current.find(
|
||||||
(memory) => memory.memory_id === memoryId,
|
(memory) => memory.memory_id === memoryId,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -249,9 +249,14 @@ export function SelectedMemoryDetail({
|
|||||||
</p>
|
</p>
|
||||||
<h3>{heading}</h3>
|
<h3>{heading}</h3>
|
||||||
{body && <p className="selected-memory-body">{body}</p>}
|
{body && <p className="selected-memory-body">{body}</p>}
|
||||||
<p className="selected-memory-source">
|
{/* The source line names the conversation a memory was read from.
|
||||||
{t("selectedMemory.sourceLine", { source, time: timestamp })}
|
Without a cited excerpt there is no such conversation to name, and
|
||||||
</p>
|
the row would say "unknown" about a memory that is right here. */}
|
||||||
|
{detail.evidence.length ? (
|
||||||
|
<p className="selected-memory-source">
|
||||||
|
{t("selectedMemory.sourceLine", { source, time: timestamp })}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
<p className="selected-memory-times">
|
<p className="selected-memory-times">
|
||||||
{t("selectedMemory.timestamps", {
|
{t("selectedMemory.timestamps", {
|
||||||
conversation: clockStamp(
|
conversation: clockStamp(
|
||||||
@@ -276,27 +281,32 @@ export function SelectedMemoryDetail({
|
|||||||
{t("selectedMemory.edit")}
|
{t("selectedMemory.edit")}
|
||||||
</R6Button>
|
</R6Button>
|
||||||
)}
|
)}
|
||||||
<section className="selected-memory-evidence">
|
{/* An SDK memory carries no cited excerpt, so the section would only
|
||||||
<h4>{t("selectedMemory.evidence")}</h4>
|
say "not available". The design shows the memory itself; keep it
|
||||||
<small>
|
that way rather than opening a block about evidence nobody kept. */}
|
||||||
{t("selectedMemory.evidenceLine", { author, time: timestamp })}
|
{detail.evidence.length ? (
|
||||||
</small>
|
<section className="selected-memory-evidence">
|
||||||
<blockquote>
|
<h4>{t("selectedMemory.evidence")}</h4>
|
||||||
{evidence?.available
|
<small>
|
||||||
? evidence.verbatim
|
{t("selectedMemory.evidenceLine", { author, time: timestamp })}
|
||||||
: t("selectedMemory.evidenceUnavailable")}
|
</small>
|
||||||
</blockquote>
|
<blockquote>
|
||||||
{!hideActions && (
|
{evidence?.available
|
||||||
<R6Button
|
? evidence.verbatim
|
||||||
onClick={(event) => {
|
: t("selectedMemory.evidenceUnavailable")}
|
||||||
trigger.current = event.currentTarget;
|
</blockquote>
|
||||||
setPage("original");
|
{!hideActions && (
|
||||||
}}
|
<R6Button
|
||||||
>
|
onClick={(event) => {
|
||||||
{t("selectedMemory.viewOriginal")}
|
trigger.current = event.currentTarget;
|
||||||
</R6Button>
|
setPage("original");
|
||||||
)}
|
}}
|
||||||
</section>
|
>
|
||||||
|
{t("selectedMemory.viewOriginal")}
|
||||||
|
</R6Button>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
) : null}
|
||||||
{!hideActions && (
|
{!hideActions && (
|
||||||
<R6Button variant="quiet" onClick={() => setShowHistory((v) => !v)}>
|
<R6Button variant="quiet" onClick={() => setShowHistory((v) => !v)}>
|
||||||
{t("selectedMemory.history")}
|
{t("selectedMemory.history")}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import type {
|
import type {
|
||||||
GovernedMemory,
|
GovernedMemory,
|
||||||
|
MemoryDetail,
|
||||||
MemoryKind,
|
MemoryKind,
|
||||||
|
MemoryLibraryPage,
|
||||||
|
SdkMemoryPage,
|
||||||
SdkMemoryRecord,
|
SdkMemoryRecord,
|
||||||
} from "@one-memory/contracts";
|
} from "@one-memory/contracts";
|
||||||
|
|
||||||
@@ -20,9 +23,11 @@ function stamp(seconds: number | null | undefined): string {
|
|||||||
|
|
||||||
/** An SDK memory in the shape the memory map draws.
|
/** An SDK memory in the shape the memory map draws.
|
||||||
*
|
*
|
||||||
* The map groups and labels by id, content, kind, status and tags only. It
|
* The map groups and labels by id, content, kind, status and tags only, so the
|
||||||
* never writes through this shape: opening a memory goes to the SDK library,
|
* governed-only fields are inert placeholders here. Opening, editing and
|
||||||
* so the governed-only fields are inert placeholders here.
|
* deleting still go through the SDK (v2) endpoints — `sdkDetail()` below wraps
|
||||||
|
* the same projection for the map's inspector, which is why the inspector can
|
||||||
|
* stay where the design put it instead of sending the reader to another view.
|
||||||
*/
|
*/
|
||||||
export function sdkMapMemory(record: SdkMemoryRecord): GovernedMemory {
|
export function sdkMapMemory(record: SdkMemoryRecord): GovernedMemory {
|
||||||
const created = stamp(record.gmt_created ?? record.memory_at);
|
const created = stamp(record.gmt_created ?? record.memory_at);
|
||||||
@@ -49,3 +54,52 @@ export function sdkMapMemory(record: SdkMemoryRecord): GovernedMemory {
|
|||||||
deleted_at: null,
|
deleted_at: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** One SDK memory as the map's inspector reads it.
|
||||||
|
*
|
||||||
|
* The inspector was built for a governed detail (memory + cited evidence +
|
||||||
|
* versions). The SDK keeps none of the latter two per memory — a rewrite
|
||||||
|
* replaces the row — so they are empty rather than faked, and the panel shows
|
||||||
|
* the memory itself where the design put it.
|
||||||
|
*/
|
||||||
|
export function sdkDetail(record: SdkMemoryRecord): MemoryDetail {
|
||||||
|
return {
|
||||||
|
memory: sdkMapMemory(record),
|
||||||
|
evidence: [],
|
||||||
|
history: [],
|
||||||
|
index_sync: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** One SDK page as the map's left list reads it.
|
||||||
|
*
|
||||||
|
* The list asks a library page for its rows, its total and — per row — when
|
||||||
|
* the cited conversation happened. The SDK page carries the row itself, so the
|
||||||
|
* time is taken from the memory: `memory_at` is the moment the conversation
|
||||||
|
* happened, `gmt_created` only the moment it was imported. Naming which one it
|
||||||
|
* is keeps "对话于" from silently becoming "沉淀于".
|
||||||
|
*/
|
||||||
|
export function sdkLibraryPage(page: SdkMemoryPage): MemoryLibraryPage {
|
||||||
|
const sourceTime: Record<
|
||||||
|
string,
|
||||||
|
{ at: string; basis: "conversation" | "import" }
|
||||||
|
> = {};
|
||||||
|
const sourceIds: Record<string, string[]> = {};
|
||||||
|
for (const record of page.memories) {
|
||||||
|
if (record.source) sourceIds[record.memory_id] = [record.source];
|
||||||
|
const at = record.memory_at ?? record.gmt_created;
|
||||||
|
if (typeof at !== "number" || !Number.isFinite(at)) continue;
|
||||||
|
sourceTime[record.memory_id] = {
|
||||||
|
at: new Date(at * 1_000).toISOString(),
|
||||||
|
basis: record.memory_at ? "conversation" : "import",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
items: page.memories.map(sdkMapMemory),
|
||||||
|
total: page.total,
|
||||||
|
offset: page.offset,
|
||||||
|
limit: page.limit,
|
||||||
|
source_ids_by_memory: sourceIds,
|
||||||
|
source_time_by_memory: sourceTime,
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in new issue
Block a user