feat(memory): S3 3a minnes-yta + impact-endpoint
This commit is contained in:
@@ -76,6 +76,18 @@ export async function deriveMemoryUpdates(
|
||||
}));
|
||||
}
|
||||
|
||||
export interface MemoryOverviewItem extends MemoryItem {
|
||||
summary: string;
|
||||
/** Lokaliserad etikett för ursprung (user_stated/observed/ai_inferred). */
|
||||
originLabel: string;
|
||||
/** Konfidens 0–100 %. */
|
||||
confidencePercent: number;
|
||||
/** Synlig etikett om posten är pausad. */
|
||||
pausedLabel?: string;
|
||||
/** Tydlig gissningsmarkering för ai_inferred (R1). */
|
||||
guessLabel?: string;
|
||||
}
|
||||
|
||||
/** Gruppera minnesposter för "Vad plattformen vet om mig"-vyn. */
|
||||
export interface MemoryOverview {
|
||||
language: string;
|
||||
@@ -84,7 +96,7 @@ export interface MemoryOverview {
|
||||
/** Rubrik på begärt språk (i18n M10). titleSv behålls för bakåtkompatibilitet. */
|
||||
title: string;
|
||||
titleSv: string;
|
||||
items: Array<MemoryItem & { summary: string }>;
|
||||
items: MemoryOverviewItem[];
|
||||
}>;
|
||||
totalCount: number;
|
||||
pausedCount: number;
|
||||
@@ -177,6 +189,76 @@ const KIND_TITLES: Record<string, Record<MemoryKind, string>> = {
|
||||
},
|
||||
};
|
||||
|
||||
const UI_LABELS: Record<
|
||||
string,
|
||||
{
|
||||
origin: Record<SignalOrigin, string>;
|
||||
paused: string;
|
||||
guess: string;
|
||||
}
|
||||
> = {
|
||||
sv: {
|
||||
origin: { user_stated: "Du har sagt", observed: "Vi har sett", ai_inferred: "Gissning" },
|
||||
paused: "Pausad",
|
||||
guess: "Detta är en gissning – bekräfta eller ändra om det stämmer.",
|
||||
},
|
||||
en: {
|
||||
origin: { user_stated: "You said", observed: "We noticed", ai_inferred: "Guess" },
|
||||
paused: "Paused",
|
||||
guess: "This is a guess — confirm or change it if it fits.",
|
||||
},
|
||||
es: {
|
||||
origin: { user_stated: "Dijiste", observed: "Notamos", ai_inferred: "Suposición" },
|
||||
paused: "Pausado",
|
||||
guess: "Esto es una suposición: confírmala o cámbiala si encaja.",
|
||||
},
|
||||
it: {
|
||||
origin: { user_stated: "Hai detto", observed: "Abbiamo notato", ai_inferred: "Ipotesi" },
|
||||
paused: "In pausa",
|
||||
guess: "Questa è un'ipotesi: confermala o modificala se è corretta.",
|
||||
},
|
||||
de: {
|
||||
origin: { user_stated: "Du hast gesagt", observed: "Wir haben bemerkt", ai_inferred: "Vermutung" },
|
||||
paused: "Pausiert",
|
||||
guess: "Dies ist eine Vermutung: bestätige oder ändere sie, wenn sie stimmt.",
|
||||
},
|
||||
fr: {
|
||||
origin: { user_stated: "Vous avez dit", observed: "Nous avons remarqué", ai_inferred: "Hypothèse" },
|
||||
paused: "En pause",
|
||||
guess: "Ceci est une hypothèse : confirmez ou modifiez si cela vous convient.",
|
||||
},
|
||||
da: {
|
||||
origin: { user_stated: "Du har sagt", observed: "Vi har set", ai_inferred: "Gæt" },
|
||||
paused: "Pauset",
|
||||
guess: "Dette er et gæt — bekræft eller ændr det, hvis det passer.",
|
||||
},
|
||||
nb: {
|
||||
origin: { user_stated: "Du har sagt", observed: "Vi har sett", ai_inferred: "Gjettning" },
|
||||
paused: "Pauset",
|
||||
guess: "Dette er en gjetning — bekreft eller endre hvis det stemmer.",
|
||||
},
|
||||
fi: {
|
||||
origin: { user_stated: "Olet sanonut", observed: "Olemme huomanneet", ai_inferred: "Arvaus" },
|
||||
paused: "Tauolla",
|
||||
guess: "Tämä on arvaus: vahvista tai muuta se, jos se sopii.",
|
||||
},
|
||||
nl: {
|
||||
origin: { user_stated: "Jij zei", observed: "We hebben gezien", ai_inferred: "Gok" },
|
||||
paused: "Gepauzeerd",
|
||||
guess: "Dit is een gok: bevestig of pas aan als het klopt.",
|
||||
},
|
||||
pl: {
|
||||
origin: { user_stated: "Powiedziałeś", observed: "Zauważyliśmy", ai_inferred: "Domysł" },
|
||||
paused: "Wstrzymane",
|
||||
guess: "To jest przypuszczenie — potwierdź lub zmień, jeśli się zgadza.",
|
||||
},
|
||||
pt: {
|
||||
origin: { user_stated: "Disseste", observed: "Reparámos", ai_inferred: "Suposição" },
|
||||
paused: "Pausado",
|
||||
guess: "Isto é uma suposição: confirma ou altera se fizer sentido.",
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Deterministisk rendering av summary ur strukturerad value (i18n-spec §23, M10).
|
||||
* Kända value-former renderas per språk; okända faller tillbaka på summarySv
|
||||
@@ -209,6 +291,7 @@ export function renderMemorySummary(
|
||||
export function buildMemoryOverview(items: MemoryItem[], languageTag = "sv"): MemoryOverview {
|
||||
const lang = languageTag.split("-")[0] ?? "sv";
|
||||
const titles = KIND_TITLES[lang] ?? KIND_TITLES.sv!;
|
||||
const labels = UI_LABELS[lang] ?? UI_LABELS.sv!;
|
||||
const byKind = new Map<MemoryKind, MemoryItem[]>();
|
||||
for (const item of items) {
|
||||
const list = byKind.get(item.kind) ?? [];
|
||||
@@ -221,7 +304,21 @@ export function buildMemoryOverview(items: MemoryItem[], languageTag = "sv"): Me
|
||||
titleSv: KIND_TITLES.sv![kind],
|
||||
items: list
|
||||
.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))
|
||||
.map((i) => ({ ...i, summary: renderMemorySummary(i, languageTag) })),
|
||||
.map((i) => {
|
||||
const overviewItem: MemoryOverviewItem = {
|
||||
...i,
|
||||
summary: renderMemorySummary(i, languageTag),
|
||||
originLabel: labels.origin[i.origin] ?? labels.origin.ai_inferred,
|
||||
confidencePercent: Math.round(i.confidence * 100),
|
||||
};
|
||||
if (i.paused) {
|
||||
overviewItem.pausedLabel = labels.paused;
|
||||
}
|
||||
if (i.origin === "ai_inferred") {
|
||||
overviewItem.guessLabel = labels.guess;
|
||||
}
|
||||
return overviewItem;
|
||||
}),
|
||||
}));
|
||||
return {
|
||||
language: lang,
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildMemoryOverview } from "../src/index.js";
|
||||
import type { MemoryItem } from "@app/shared-types";
|
||||
|
||||
function makeItem(overrides: Partial<MemoryItem> = {}): MemoryItem {
|
||||
return {
|
||||
id: "m1",
|
||||
userId: "u1",
|
||||
kind: "structured_fact",
|
||||
key: "favorite_cuisine_swedish",
|
||||
summarySv: "Gillar svensk mat",
|
||||
value: { favoriteCuisine: "swedish" },
|
||||
origin: "user_stated",
|
||||
confidence: 1,
|
||||
verifiedByUser: true,
|
||||
paused: false,
|
||||
createdAt: "2026-08-01T00:00:00.000Z",
|
||||
updatedAt: "2026-08-01T00:00:00.000Z",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("buildMemoryOverview", () => {
|
||||
it("grupperar minnen efter kind", () => {
|
||||
const items: MemoryItem[] = [
|
||||
makeItem({ id: "a", kind: "structured_fact", summarySv: "Fakta A" }),
|
||||
makeItem({ id: "b", kind: "recipe_memory", summarySv: "Recept B" }),
|
||||
makeItem({ id: "c", kind: "structured_fact", summarySv: "Fakta C" }),
|
||||
];
|
||||
const overview = buildMemoryOverview(items, "sv-SE");
|
||||
const kinds = overview.sections.map((s) => s.kind);
|
||||
expect(kinds).toContain("structured_fact");
|
||||
expect(kinds).toContain("recipe_memory");
|
||||
const factSection = overview.sections.find((s) => s.kind === "structured_fact")!;
|
||||
expect(factSection.items).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("visar origin och confidence tydligt", () => {
|
||||
const items: MemoryItem[] = [
|
||||
makeItem({ origin: "observed", confidence: 0.75 }),
|
||||
];
|
||||
const overview = buildMemoryOverview(items, "sv-SE");
|
||||
const item = overview.sections[0]!.items[0]!;
|
||||
expect(item.originLabel).toBe("Vi har sett");
|
||||
expect(item.confidencePercent).toBe(75);
|
||||
});
|
||||
|
||||
it("markerar pausade poster synligt", () => {
|
||||
const items: MemoryItem[] = [makeItem({ paused: true })];
|
||||
const overview = buildMemoryOverview(items, "sv-SE");
|
||||
const item = overview.sections[0]!.items[0]!;
|
||||
expect(item.pausedLabel).toBe("Pausad");
|
||||
expect(overview.pausedCount).toBe(1);
|
||||
});
|
||||
|
||||
it("markerar ai_inferred som gissning", () => {
|
||||
const items: MemoryItem[] = [makeItem({ origin: "ai_inferred", confidence: 0.5 })];
|
||||
const overview = buildMemoryOverview(items, "sv-SE");
|
||||
const item = overview.sections[0]!.items[0]!;
|
||||
expect(item.originLabel).toBe("Gissning");
|
||||
expect(item.guessLabel).toBe("Detta är en gissning – bekräfta eller ändra om det stämmer.");
|
||||
});
|
||||
|
||||
it("sorterar senast uppdaterade överst", () => {
|
||||
const items: MemoryItem[] = [
|
||||
makeItem({ id: "old", updatedAt: "2026-08-01T00:00:00.000Z" }),
|
||||
makeItem({ id: "new", updatedAt: "2026-08-10T00:00:00.000Z" }),
|
||||
];
|
||||
const overview = buildMemoryOverview(items, "sv-SE");
|
||||
expect(overview.sections[0]!.items[0]!.id).toBe("new");
|
||||
expect(overview.sections[0]!.items[1]!.id).toBe("old");
|
||||
});
|
||||
|
||||
it("stödjer engelska texter", () => {
|
||||
const items: MemoryItem[] = [makeItem({ origin: "user_stated", paused: true })];
|
||||
const overview = buildMemoryOverview(items, "en-US");
|
||||
const item = overview.sections[0]!.items[0]!;
|
||||
expect(item.originLabel).toBe("You said");
|
||||
expect(item.pausedLabel).toBe("Paused");
|
||||
expect(item.summary).toBe("Favorite cuisine: swedish");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user