@manta-eu/react 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +101 -35
- package/dist/components/channel-icon/index.d.ts +1 -2
- package/dist/components/channel-icon/index.d.ts.map +1 -1
- package/dist/components/channel-icon/index.js +0 -4
- package/dist/components/plan-row/index.d.ts +2 -1
- package/dist/components/plan-row/index.d.ts.map +1 -1
- package/dist/components/plan-row/index.js +2 -14
- package/dist/globals.css +146 -90
- package/dist/graph/nodes/AdMockupCard.js +1 -1
- package/dist/graph/nodes/CreativeMediaCard.js +1 -1
- package/dist/graph/nodes/EntityCardShell.js +1 -1
- package/dist/hooks/use-conversation-starters.d.ts +1 -1
- package/dist/hooks/useValidateActionPlan.d.ts.map +1 -1
- package/dist/routes/memories/components/ChangeScopeDialog.d.ts +12 -0
- package/dist/routes/memories/components/ChangeScopeDialog.d.ts.map +1 -0
- package/dist/routes/memories/components/ChangeScopeDialog.js +89 -0
- package/dist/routes/memories/components/ConflictMemoryCard.d.ts +13 -0
- package/dist/routes/memories/components/ConflictMemoryCard.d.ts.map +1 -0
- package/dist/routes/memories/components/ConflictMemoryCard.js +43 -0
- package/dist/routes/memories/components/ConflictTimeoutFooter.d.ts +7 -0
- package/dist/routes/memories/components/ConflictTimeoutFooter.d.ts.map +1 -0
- package/dist/routes/memories/components/ConflictTimeoutFooter.js +6 -0
- package/dist/routes/memories/components/ConsolidationReviewDialog.d.ts +61 -0
- package/dist/routes/memories/components/ConsolidationReviewDialog.d.ts.map +1 -0
- package/dist/routes/memories/components/ConsolidationReviewDialog.js +121 -0
- package/dist/routes/memories/components/EmptyStates.d.ts +18 -0
- package/dist/routes/memories/components/EmptyStates.d.ts.map +1 -0
- package/dist/routes/memories/components/EmptyStates.js +22 -0
- package/dist/routes/memories/components/ImportanceSlider.d.ts +11 -0
- package/dist/routes/memories/components/ImportanceSlider.d.ts.map +1 -0
- package/dist/routes/memories/components/ImportanceSlider.js +31 -0
- package/dist/routes/memories/components/LiveContextTab.d.ts +6 -0
- package/dist/routes/memories/components/LiveContextTab.d.ts.map +1 -0
- package/dist/routes/memories/components/LiveContextTab.js +37 -0
- package/dist/routes/memories/components/LoadingSkeleton.d.ts +2 -0
- package/dist/routes/memories/components/LoadingSkeleton.d.ts.map +1 -0
- package/dist/routes/memories/components/LoadingSkeleton.js +7 -0
- package/dist/routes/memories/components/MemoriesFilters.d.ts +31 -0
- package/dist/routes/memories/components/MemoriesFilters.d.ts.map +1 -0
- package/dist/routes/memories/components/MemoriesFilters.js +88 -0
- package/dist/routes/memories/components/MemoriesPanel.d.ts +6 -0
- package/dist/routes/memories/components/MemoriesPanel.d.ts.map +1 -0
- package/dist/routes/memories/components/MemoriesPanel.js +99 -0
- package/dist/routes/memories/components/MemoriesTabPanel.d.ts +30 -0
- package/dist/routes/memories/components/MemoriesTabPanel.d.ts.map +1 -0
- package/dist/routes/memories/components/MemoriesTabPanel.js +11 -0
- package/dist/routes/memories/components/MemoryActions.d.ts +12 -0
- package/dist/routes/memories/components/MemoryActions.d.ts.map +1 -0
- package/dist/routes/memories/components/MemoryActions.js +24 -0
- package/dist/routes/memories/components/MemoryFields.d.ts +32 -0
- package/dist/routes/memories/components/MemoryFields.d.ts.map +1 -0
- package/dist/routes/memories/components/MemoryFields.js +30 -0
- package/dist/routes/memories/components/MemoryFormDialog.d.ts +15 -0
- package/dist/routes/memories/components/MemoryFormDialog.d.ts.map +1 -0
- package/dist/routes/memories/components/MemoryFormDialog.js +299 -0
- package/dist/routes/memories/components/MemoryOnboardingWizard.d.ts +9 -0
- package/dist/routes/memories/components/MemoryOnboardingWizard.d.ts.map +1 -0
- package/dist/routes/memories/components/MemoryOnboardingWizard.js +99 -0
- package/dist/routes/memories/components/RestoreBlockedDialog.d.ts +8 -0
- package/dist/routes/memories/components/RestoreBlockedDialog.d.ts.map +1 -0
- package/dist/routes/memories/components/RestoreBlockedDialog.js +7 -0
- package/dist/routes/memories/components/SchemasTab.d.ts +6 -0
- package/dist/routes/memories/components/SchemasTab.d.ts.map +1 -0
- package/dist/routes/memories/components/SchemasTab.js +35 -0
- package/dist/routes/memories/components/SupersessionConflictDialog.d.ts +18 -0
- package/dist/routes/memories/components/SupersessionConflictDialog.d.ts.map +1 -0
- package/dist/routes/memories/components/SupersessionConflictDialog.js +78 -0
- package/dist/routes/memories/components/TeamPicker.d.ts +8 -0
- package/dist/routes/memories/components/TeamPicker.d.ts.map +1 -0
- package/dist/routes/memories/components/TeamPicker.js +25 -0
- package/dist/routes/memories/components/badges.d.ts +50 -0
- package/dist/routes/memories/components/badges.d.ts.map +1 -0
- package/dist/routes/memories/components/badges.js +106 -0
- package/dist/routes/memories/expiry-utils.d.ts +23 -0
- package/dist/routes/memories/expiry-utils.d.ts.map +1 -0
- package/dist/routes/memories/expiry-utils.js +97 -0
- package/dist/routes/memories/hooks/useConflictCheck.d.ts +18 -0
- package/dist/routes/memories/hooks/useConflictCheck.d.ts.map +1 -0
- package/dist/routes/memories/hooks/useConflictCheck.js +35 -0
- package/dist/routes/memories/hooks/useDuplicateDetection.d.ts +11 -0
- package/dist/routes/memories/hooks/useDuplicateDetection.d.ts.map +1 -0
- package/dist/routes/memories/hooks/useDuplicateDetection.js +73 -0
- package/dist/routes/memories/hooks/useLiveContext.d.ts +23 -0
- package/dist/routes/memories/hooks/useLiveContext.d.ts.map +1 -0
- package/dist/routes/memories/hooks/useLiveContext.js +102 -0
- package/dist/routes/memories/hooks/useMemoryColumns.d.ts +33 -0
- package/dist/routes/memories/hooks/useMemoryColumns.d.ts.map +1 -0
- package/dist/routes/memories/hooks/useMemoryColumns.js +188 -0
- package/dist/routes/memories/hooks/useMemoryTable.d.ts +33 -0
- package/dist/routes/memories/hooks/useMemoryTable.d.ts.map +1 -0
- package/dist/routes/memories/hooks/useMemoryTable.js +121 -0
- package/dist/routes/memories/index.d.ts +11 -0
- package/dist/routes/memories/index.d.ts.map +1 -0
- package/dist/routes/memories/index.js +472 -0
- package/dist/routes/memories/memory-form-validation.d.ts +8 -0
- package/dist/routes/memories/memory-form-validation.d.ts.map +1 -0
- package/dist/routes/memories/memory-form-validation.js +33 -0
- package/dist/routes/memories/memory-mutation-input.d.ts +6 -0
- package/dist/routes/memories/memory-mutation-input.d.ts.map +1 -0
- package/dist/routes/memories/memory-mutation-input.js +41 -0
- package/dist/routes/plans/components/actionPlansTableConfig.d.ts +3 -3
- package/package.json +12 -7
- package/src/globals.css +13 -18
- package/src/themes/manta-dark.css +0 -4
- package/src/themes/standard.css +11 -11
- package/dist/components/channel-icon/marketer-icon.d.ts +0 -4
- package/dist/components/channel-icon/marketer-icon.d.ts.map +0 -1
- package/dist/components/channel-icon/marketer-icon.js +0 -6
- package/src/themes/marketer-dark.css +0 -70
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { isMemoryPreloaded, isNeverUsed, needsTrustReview, parseMemoryDate, } from "@manta-eu/client/shared/memory-preload-gate";
|
|
2
|
+
import { useReactTable } from "@tanstack/react-table";
|
|
3
|
+
import { differenceInCalendarDays } from "date-fns";
|
|
4
|
+
import { useCallback, useMemo, useState } from "react";
|
|
5
|
+
import { match } from "ts-pattern";
|
|
6
|
+
import { useDebounceValue, useLocalStorage } from "usehooks-ts";
|
|
7
|
+
import { isTrustExpired } from "../expiry-utils.js";
|
|
8
|
+
import { memoryColumnIds } from "./useMemoryColumns.js";
|
|
9
|
+
import { createTableConfig, useTableState } from "#components/table/index";
|
|
10
|
+
const defaultStatusFilters = {
|
|
11
|
+
autoCaptured: false,
|
|
12
|
+
hideStale: false,
|
|
13
|
+
promotion: false,
|
|
14
|
+
deadWeight: false,
|
|
15
|
+
unused: false,
|
|
16
|
+
needsReview: false,
|
|
17
|
+
};
|
|
18
|
+
const noColumnVisibility = {};
|
|
19
|
+
const isAutoCaptured = (memory) => memory.confidence != null && memory.confidence < 1;
|
|
20
|
+
const deadWeightUntouchedDays = 90;
|
|
21
|
+
const isDeadWeight = (memory, now) => {
|
|
22
|
+
const lastTouched = parseMemoryDate(memory.memoryLastUsedAt) ?? parseMemoryDate(memory.extractedAt);
|
|
23
|
+
return (isMemoryPreloaded(memory, now) &&
|
|
24
|
+
lastTouched != null &&
|
|
25
|
+
differenceInCalendarDays(now, lastTouched) >= deadWeightUntouchedDays);
|
|
26
|
+
};
|
|
27
|
+
export function filterMemories(data, criteria, now = new Date()) {
|
|
28
|
+
let result = data;
|
|
29
|
+
if (criteria.search.trim()) {
|
|
30
|
+
const q = criteria.search.toLowerCase();
|
|
31
|
+
result = result.filter((m) => m.label.toLowerCase().includes(q) || (m.description ?? "").toLowerCase().includes(q));
|
|
32
|
+
}
|
|
33
|
+
if (criteria.importance !== 0) {
|
|
34
|
+
result = result.filter((m) => m.importance === criteria.importance);
|
|
35
|
+
}
|
|
36
|
+
if (criteria.autoCaptured) {
|
|
37
|
+
result = result.filter(isAutoCaptured);
|
|
38
|
+
}
|
|
39
|
+
if (criteria.hideStale) {
|
|
40
|
+
result = result.filter((m) => !isTrustExpired(m.memoryTrustedUntil, now));
|
|
41
|
+
}
|
|
42
|
+
// Promoting a stale memory would push an unconfirmed fact onto everyone in the company.
|
|
43
|
+
if (criteria.promotion) {
|
|
44
|
+
result = result.filter((m) => isAutoCaptured(m) && !isTrustExpired(m.memoryTrustedUntil, now));
|
|
45
|
+
}
|
|
46
|
+
if (criteria.deadWeight) {
|
|
47
|
+
result = result.filter((m) => isDeadWeight(m, now));
|
|
48
|
+
}
|
|
49
|
+
if (criteria.unused) {
|
|
50
|
+
result = result.filter((m) => isNeverUsed(m, now));
|
|
51
|
+
}
|
|
52
|
+
if (criteria.needsReview) {
|
|
53
|
+
result = result.filter((m) => needsTrustReview(m, now));
|
|
54
|
+
}
|
|
55
|
+
return match(criteria.schema)
|
|
56
|
+
.with("ALL", () => result)
|
|
57
|
+
.with("NONE", () => result.filter((m) => m.memorySubclass == null))
|
|
58
|
+
.otherwise((iri) => result.filter((m) => m.memorySubclass === iri));
|
|
59
|
+
}
|
|
60
|
+
export function useMemoryTable(tableKey, data, columns, initialSorting = [], initialColumnVisibility) {
|
|
61
|
+
const [searchQuery, setSearchQuery] = useState("");
|
|
62
|
+
const [debouncedSearch] = useDebounceValue(searchQuery, 200);
|
|
63
|
+
const [importanceFilter, setImportanceFilter] = useState(0);
|
|
64
|
+
const [statusFilters, setStatusFilters] = useState(defaultStatusFilters);
|
|
65
|
+
const [schemaFilter, setSchemaFilter] = useState("ALL");
|
|
66
|
+
const onStatusFilterChange = useCallback((key, value) => {
|
|
67
|
+
setStatusFilters((prev) => ({ ...prev, [key]: value }));
|
|
68
|
+
}, []);
|
|
69
|
+
const filteredData = useMemo(() => filterMemories(data, {
|
|
70
|
+
search: debouncedSearch,
|
|
71
|
+
importance: importanceFilter,
|
|
72
|
+
autoCaptured: statusFilters.autoCaptured,
|
|
73
|
+
hideStale: statusFilters.hideStale,
|
|
74
|
+
promotion: statusFilters.promotion,
|
|
75
|
+
deadWeight: statusFilters.deadWeight,
|
|
76
|
+
unused: statusFilters.unused,
|
|
77
|
+
needsReview: statusFilters.needsReview,
|
|
78
|
+
schema: schemaFilter,
|
|
79
|
+
}), [data, debouncedSearch, importanceFilter, statusFilters, schemaFilter]);
|
|
80
|
+
const { sorting, setSorting, columnFilters, setColumnFilters, columnOrder, setColumnOrder, rowSelection, setRowSelection, pagination, setPagination, } = useTableState(tableKey, initialSorting, { useLocalState: true, defaultPageSize: 12 });
|
|
81
|
+
// Keyed distinctly from useTableState so they don't collide in localStorage.
|
|
82
|
+
const [storedVisibility, setColumnVisibility] = useLocalStorage(`${tableKey}-visibility`, initialColumnVisibility ?? noColumnVisibility);
|
|
83
|
+
const columnVisibility = useMemo(() => ({ ...initialColumnVisibility, ...storedVisibility }), [initialColumnVisibility, storedVisibility]);
|
|
84
|
+
const table = useReactTable(createTableConfig({
|
|
85
|
+
data: filteredData,
|
|
86
|
+
columns,
|
|
87
|
+
columnPinning: { left: [memoryColumnIds.label], right: [memoryColumnIds.actions] },
|
|
88
|
+
sorting,
|
|
89
|
+
setSorting,
|
|
90
|
+
columnFilters,
|
|
91
|
+
setColumnFilters,
|
|
92
|
+
columnVisibility,
|
|
93
|
+
setColumnVisibility,
|
|
94
|
+
columnOrder,
|
|
95
|
+
setColumnOrder,
|
|
96
|
+
rowSelection,
|
|
97
|
+
setRowSelection,
|
|
98
|
+
pagination,
|
|
99
|
+
setPagination,
|
|
100
|
+
}));
|
|
101
|
+
const kindFilter = columnFilters.find((f) => f.id === memoryColumnIds.kind)?.value ?? "ALL";
|
|
102
|
+
const handleKindFilterChange = useCallback((kind) => {
|
|
103
|
+
setColumnFilters((prev) => [
|
|
104
|
+
...prev.filter((f) => f.id !== memoryColumnIds.kind),
|
|
105
|
+
...(kind !== "ALL" ? [{ id: memoryColumnIds.kind, value: kind }] : []),
|
|
106
|
+
]);
|
|
107
|
+
}, [setColumnFilters]);
|
|
108
|
+
return {
|
|
109
|
+
table,
|
|
110
|
+
kindFilter,
|
|
111
|
+
onKindFilterChange: handleKindFilterChange,
|
|
112
|
+
schemaFilter,
|
|
113
|
+
onSchemaFilterChange: setSchemaFilter,
|
|
114
|
+
searchQuery,
|
|
115
|
+
onSearchQueryChange: setSearchQuery,
|
|
116
|
+
importanceFilter,
|
|
117
|
+
onImportanceFilterChange: setImportanceFilter,
|
|
118
|
+
statusFilters,
|
|
119
|
+
onStatusFilterChange,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { deriveLabel } from "./memory-mutation-input.js";
|
|
2
|
+
export { deriveLabel };
|
|
3
|
+
export { KindBadge, ScopeBadge, preloadBadgeContent, type KindBadgeProps, type PreloadBadgeContent, type PreloadBadgeTone, type ScopeBadgeProps, } from "./components/badges.js";
|
|
4
|
+
export { formatHorizonDate, formatHorizonRelative, memoryHorizon, type MemoryHorizon } from "./expiry-utils.js";
|
|
5
|
+
export { filterMemories, type ImportanceFilter, type MemoryFilterCriteria, type SchemaFilter, } from "./hooks/useMemoryTable.js";
|
|
6
|
+
export { GroupMemoriesEmptyState, MemoriesEmptyState, type GroupMemoriesEmptyStateProps, type MemoriesEmptyStateProps, } from "./components/EmptyStates.js";
|
|
7
|
+
export { LoadingSkeleton } from "./components/LoadingSkeleton.js";
|
|
8
|
+
export { MemoryFormDialog, type MemoryFormDialogProps } from "./components/MemoryFormDialog.js";
|
|
9
|
+
export type { MemoryFormState } from "./components/MemoryFields.js";
|
|
10
|
+
export declare function MemoriesRoute(): import("react/jsx-runtime").JSX.Element;
|
|
11
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/routes/memories/index.tsx"],"names":[],"mappings":"AAuCA,OAAO,EAAkD,WAAW,EAAE,MAAM,yBAAyB,CAAC;AA8FtG,OAAO,EAAE,WAAW,EAAE,CAAC;AACvB,OAAO,EACL,SAAS,EACT,UAAU,EACV,mBAAmB,EACnB,KAAK,cAAc,EACnB,KAAK,mBAAmB,EACxB,KAAK,gBAAgB,EACrB,KAAK,eAAe,GACrB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,aAAa,EAAE,KAAK,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC7G,OAAO,EACL,cAAc,EACd,KAAK,gBAAgB,EACrB,KAAK,oBAAoB,EACzB,KAAK,YAAY,GAClB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACL,uBAAuB,EACvB,kBAAkB,EAClB,KAAK,4BAA4B,EACjC,KAAK,uBAAuB,GAC7B,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAC/D,OAAO,EAAE,gBAAgB,EAAE,KAAK,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AAC7F,YAAY,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAiCjE,wBAAgB,aAAa,4CAylB5B"}
|
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { GQLMemoryScope, useAvailableTeamsQuery, useCreateMemoryMutation, useDeleteMemoryMutation, useConsolidateMemoriesMutation, useMemoriesQuery, useMemoryRedundancyReviewQuery, useMemorySchemasQuery, useReconfirmMemoryMutation, useRestoreMemoryMutation, useUpdateMemoryMutation, } from "@manta-eu/client";
|
|
3
|
+
import { parseError } from "@manta-eu/client/shared/error-utils";
|
|
4
|
+
import { isPresent } from "@manta-eu/lib/present";
|
|
5
|
+
import { useQueryClient } from "@tanstack/react-query";
|
|
6
|
+
import { Archive, Brain, CalendarX, CopyCheck, EyeOff, Plus, RefreshCcw } from "lucide-react";
|
|
7
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
8
|
+
import { useSearchParams } from "react-router";
|
|
9
|
+
import { toast } from "sonner";
|
|
10
|
+
import { match } from "ts-pattern";
|
|
11
|
+
import { ChangeScopeDialog } from "./components/ChangeScopeDialog.js";
|
|
12
|
+
import { ConsolidationReviewDialog } from "./components/ConsolidationReviewDialog.js";
|
|
13
|
+
import { GroupMemoriesEmptyState, MemoriesEmptyState, CompanyMemoriesEmptyState } from "./components/EmptyStates.js";
|
|
14
|
+
import { LiveContextTab } from "./components/LiveContextTab.js";
|
|
15
|
+
import { MemoriesTabPanel } from "./components/MemoriesTabPanel.js";
|
|
16
|
+
import { MemoryFormDialog } from "./components/MemoryFormDialog.js";
|
|
17
|
+
import { MemoryOnboardingWizard } from "./components/MemoryOnboardingWizard.js";
|
|
18
|
+
import { RestoreBlockedDialog } from "./components/RestoreBlockedDialog.js";
|
|
19
|
+
import { SchemasTab } from "./components/SchemasTab.js";
|
|
20
|
+
import { useDuplicateDetection } from "./hooks/useDuplicateDetection.js";
|
|
21
|
+
import { liveContextQueryKey } from "./hooks/useLiveContext.js";
|
|
22
|
+
import { useMemoryColumns } from "./hooks/useMemoryColumns.js";
|
|
23
|
+
import { useMemoryTable } from "./hooks/useMemoryTable.js";
|
|
24
|
+
import { buildCreateMemoryInput, buildUpdateMemoryInput, deriveLabel } from "./memory-mutation-input.js";
|
|
25
|
+
import { Input } from "#components/input/index";
|
|
26
|
+
import { PageHeader, PageHeaderActions } from "#components/layouts/page/index";
|
|
27
|
+
import { RouteLayout } from "#components/layouts/RouteLayout";
|
|
28
|
+
import { ScopeTabs } from "#components/scope-tabs/index";
|
|
29
|
+
import { Tabs, TabsContent } from "#components/tabs/index";
|
|
30
|
+
import { useAuthContext } from "#providers/AuthContext";
|
|
31
|
+
import { useMantaClient } from "#providers/index";
|
|
32
|
+
function reportMutationError(e, fallbackMessage) {
|
|
33
|
+
const err = parseError(e, fallbackMessage);
|
|
34
|
+
toast.error(err.message, { description: err.description });
|
|
35
|
+
}
|
|
36
|
+
function addMemoryButtonConfig(tab) {
|
|
37
|
+
return match(tab)
|
|
38
|
+
.with("personal", () => ({ scope: GQLMemoryScope.User, label: "Add memory" }))
|
|
39
|
+
.with("company", () => ({ scope: GQLMemoryScope.Company, label: "Add company memory" }))
|
|
40
|
+
.with("team", () => ({ scope: GQLMemoryScope.Team, label: "Add team memory" }))
|
|
41
|
+
.otherwise(() => null);
|
|
42
|
+
}
|
|
43
|
+
const alwaysVisibleStatusFilters = ["hideStale", "deadWeight", "unused", "needsReview"];
|
|
44
|
+
function ScopeMemoriesTab(props) {
|
|
45
|
+
const { value, tableState, memories, emptyState, visibleStatusFilters = alwaysVisibleStatusFilters, ...panelProps } = props;
|
|
46
|
+
return (_jsx(TabsContent, { value: value, className: "flex flex-col flex-1 min-h-0 overflow-hidden", children: _jsx(MemoriesTabPanel, { ...panelProps, ...tableState, memories: memories, emptyState: emptyState, visibleStatusFilters: visibleStatusFilters }) }));
|
|
47
|
+
}
|
|
48
|
+
function FilterableTabContent(props) {
|
|
49
|
+
const { value, tabs, placeholder, query, onQueryChange, children } = props;
|
|
50
|
+
return (_jsxs(TabsContent, { value: value, className: "flex flex-col flex-1 min-h-0 overflow-hidden", children: [_jsx("div", { className: "border-b border-muted-border px-5 py-3 shrink-0", children: _jsxs("div", { className: "flex min-w-0 flex-col gap-3 2xl:flex-row 2xl:items-center", children: [_jsx("div", { className: "min-w-0 2xl:shrink", children: tabs }), _jsx(Input, { containerClassName: "w-auto min-w-48 sm:w-64", type: "search", variant: "search", placeholder: placeholder, value: query, onChange: (e) => onQueryChange(e.target.value) })] }) }), _jsx("div", { className: "flex-1 overflow-auto p-5", children: children })] }));
|
|
51
|
+
}
|
|
52
|
+
export { deriveLabel };
|
|
53
|
+
export { KindBadge, ScopeBadge, preloadBadgeContent, } from "./components/badges.js";
|
|
54
|
+
export { formatHorizonDate, formatHorizonRelative, memoryHorizon } from "./expiry-utils.js";
|
|
55
|
+
export { filterMemories, } from "./hooks/useMemoryTable.js";
|
|
56
|
+
export { GroupMemoriesEmptyState, MemoriesEmptyState, } from "./components/EmptyStates.js";
|
|
57
|
+
export { LoadingSkeleton } from "./components/LoadingSkeleton.js";
|
|
58
|
+
export { MemoryFormDialog } from "./components/MemoryFormDialog.js";
|
|
59
|
+
const tableKeyPersonal = "memories-table-personal";
|
|
60
|
+
const tableKeyCompany = "memories-table-company";
|
|
61
|
+
const tableKeyGroup = "memories-table-group";
|
|
62
|
+
const emptyMemories = [];
|
|
63
|
+
const personalColumnVisibility = { createdBy: false, teams: false };
|
|
64
|
+
const companyColumnVisibility = { teams: false };
|
|
65
|
+
const teamColumnVisibility = { teams: true };
|
|
66
|
+
// Drops a whole group once any of its memories was merged, since the rest of it no longer describes what is stored.
|
|
67
|
+
function unmergedGroups(groups, merged) {
|
|
68
|
+
if (merged.size === 0)
|
|
69
|
+
return [...groups];
|
|
70
|
+
return groups.filter((group) => !group.memories.some((memory) => merged.has(memory.iri)));
|
|
71
|
+
}
|
|
72
|
+
const tabLabels = {
|
|
73
|
+
company: "Company",
|
|
74
|
+
team: "Team",
|
|
75
|
+
personal: "Personal",
|
|
76
|
+
schemas: "Schemas",
|
|
77
|
+
"live-context": "Live Context",
|
|
78
|
+
};
|
|
79
|
+
export function MemoriesRoute() {
|
|
80
|
+
const { datastore, datastores, user, logout, isOperator, canSwitchDatastore } = useAuthContext();
|
|
81
|
+
const { mantaClient } = useMantaClient();
|
|
82
|
+
const queryClient = useQueryClient();
|
|
83
|
+
const [showArchived, setShowArchived] = useState(false);
|
|
84
|
+
// Server-side like showArchived — these rows are absent from the response unless requested.
|
|
85
|
+
const [showOutOfWindow, setShowOutOfWindow] = useState(false);
|
|
86
|
+
const { data, isLoading, isFetching, refetch, error: queriedError, } = useMemoriesQuery(mantaClient, {
|
|
87
|
+
includeArchived: showArchived,
|
|
88
|
+
includeOutOfWindow: showOutOfWindow,
|
|
89
|
+
});
|
|
90
|
+
const { data: schemasData } = useMemorySchemasQuery(mantaClient);
|
|
91
|
+
const schemas = schemasData?.memorySchemas.schemas ?? [];
|
|
92
|
+
const allMemories = data?.memories.memories ?? emptyMemories;
|
|
93
|
+
const personalMemories = useMemo(() => allMemories.filter((m) => m.scope === GQLMemoryScope.User), [allMemories]);
|
|
94
|
+
const companyMemories = useMemo(() => allMemories.filter((m) => m.scope === GQLMemoryScope.Company), [allMemories]);
|
|
95
|
+
const teamMemories = useMemo(() => allMemories.filter((m) => m.scope === GQLMemoryScope.Team), [allMemories]);
|
|
96
|
+
const unreviewedPersonalMemories = useMemo(() => personalMemories.filter((m) => m.confidence != null && m.confidence < 1), [personalMemories]);
|
|
97
|
+
const errorMessage = queriedError ? parseError(queriedError, "Failed to load memories").message : null;
|
|
98
|
+
const personalCount = personalMemories.length;
|
|
99
|
+
const companyCount = companyMemories.length;
|
|
100
|
+
const teamCount = teamMemories.length;
|
|
101
|
+
const unreviewedCount = unreviewedPersonalMemories.length;
|
|
102
|
+
const createMutation = useCreateMemoryMutation(mantaClient);
|
|
103
|
+
const updateMutation = useUpdateMemoryMutation(mantaClient);
|
|
104
|
+
const deleteMutation = useDeleteMemoryMutation(mantaClient);
|
|
105
|
+
const restoreMutation = useRestoreMemoryMutation(mantaClient);
|
|
106
|
+
const reconfirmMutation = useReconfirmMemoryMutation(mantaClient);
|
|
107
|
+
const { data: teamsData } = useAvailableTeamsQuery(mantaClient, undefined, { enabled: !!isOperator });
|
|
108
|
+
const hasTeams = isPresent(teamsData?.availableTeams.teams);
|
|
109
|
+
const availableTabs = useMemo(() => [
|
|
110
|
+
...(isOperator ? ["company"] : []),
|
|
111
|
+
...(isOperator && hasTeams ? ["team"] : []),
|
|
112
|
+
"personal",
|
|
113
|
+
...(isOperator ? ["schemas"] : []),
|
|
114
|
+
"live-context",
|
|
115
|
+
], [isOperator, hasTeams]);
|
|
116
|
+
const [searchParams, setSearchParams] = useSearchParams();
|
|
117
|
+
const tabParam = searchParams.get("tab");
|
|
118
|
+
const activeTab = useMemo(() => {
|
|
119
|
+
if (tabParam && availableTabs.includes(tabParam))
|
|
120
|
+
return tabParam;
|
|
121
|
+
return availableTabs[0] ?? "personal";
|
|
122
|
+
}, [tabParam, availableTabs]);
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
if (tabParam !== activeTab) {
|
|
125
|
+
setSearchParams({ tab: activeTab }, { replace: true });
|
|
126
|
+
}
|
|
127
|
+
}, [tabParam, activeTab, setSearchParams]);
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
if (unreviewedCount === 0)
|
|
130
|
+
setUnreviewedOnly(false);
|
|
131
|
+
}, [unreviewedCount]);
|
|
132
|
+
function handleTabChange(tab) {
|
|
133
|
+
setSearchParams({ tab }, { replace: true });
|
|
134
|
+
}
|
|
135
|
+
const [schemasQuery, setSchemasQuery] = useState("");
|
|
136
|
+
const [liveContextQuery, setLiveContextQuery] = useState("");
|
|
137
|
+
const [addDialog, setAddDialog] = useState(null);
|
|
138
|
+
const [editingMemory, setEditingMemory] = useState(null);
|
|
139
|
+
const [movingMemory, setMovingMemory] = useState(null);
|
|
140
|
+
const [restoreBlocked, setRestoreBlocked] = useState(null);
|
|
141
|
+
const [unreviewedOnly, setUnreviewedOnly] = useState(false);
|
|
142
|
+
const [showCompanyOnboarding, setShowOrgOnboarding] = useState(false);
|
|
143
|
+
const invalidate = useCallback(() => {
|
|
144
|
+
void refetch();
|
|
145
|
+
void queryClient.invalidateQueries({ queryKey: liveContextQueryKey });
|
|
146
|
+
}, [refetch, queryClient]);
|
|
147
|
+
const handleRefresh = useCallback(() => {
|
|
148
|
+
void refetch();
|
|
149
|
+
void queryClient.refetchQueries({ queryKey: liveContextQueryKey });
|
|
150
|
+
}, [refetch, queryClient]);
|
|
151
|
+
const [consolidating, setConsolidating] = useState(false);
|
|
152
|
+
const consolidationScope = match(activeTab)
|
|
153
|
+
.with("company", () => GQLMemoryScope.Company)
|
|
154
|
+
.with("team", () => GQLMemoryScope.Team)
|
|
155
|
+
.otherwise(() => GQLMemoryScope.User);
|
|
156
|
+
const consolidationVariables = { input: { scope: consolidationScope, wantToStopAfterMs: 60_000 } };
|
|
157
|
+
const { data: reviewData, isFetching: isFindingRedundancy, error: reviewError, } = useMemoryRedundancyReviewQuery(mantaClient, consolidationVariables, { enabled: consolidating });
|
|
158
|
+
const consolidateMutation = useConsolidateMemoriesMutation(mantaClient);
|
|
159
|
+
const consolidationCandidates = match(consolidationScope)
|
|
160
|
+
.with(GQLMemoryScope.Company, () => companyMemories)
|
|
161
|
+
.with(GQLMemoryScope.Team, () => teamMemories)
|
|
162
|
+
.otherwise(() => personalMemories)
|
|
163
|
+
.filter((m) => !isPresent(m.supersededBy));
|
|
164
|
+
const [mergedIris, setMergedIris] = useState(() => new Set());
|
|
165
|
+
const applyMerge = useCallback(async (decision) => {
|
|
166
|
+
const result = await consolidateMutation.mutateAsync({
|
|
167
|
+
input: {
|
|
168
|
+
memoryIris: decision.memoryIris,
|
|
169
|
+
label: decision.label,
|
|
170
|
+
description: decision.description,
|
|
171
|
+
scope: consolidationScope,
|
|
172
|
+
derived: decision.kind === "pattern",
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
if (result.consolidateMemories.error)
|
|
176
|
+
throw new Error(result.consolidateMemories.error);
|
|
177
|
+
setMergedIris((seen) => new Set([...seen, ...decision.memoryIris]));
|
|
178
|
+
invalidate();
|
|
179
|
+
}, [consolidateMutation, consolidationScope, invalidate]);
|
|
180
|
+
const reviewProposals = useMemo(() => {
|
|
181
|
+
const review = reviewData?.memoryRedundancyReview;
|
|
182
|
+
return [
|
|
183
|
+
...(review?.clusters ?? []).map((c) => ({ ...c, kind: "duplicate" })),
|
|
184
|
+
...(review?.abstractions ?? []).map((a) => ({ ...a, kind: "pattern" })),
|
|
185
|
+
];
|
|
186
|
+
}, [reviewData]);
|
|
187
|
+
const reviewGroups = unmergedGroups(reviewProposals, mergedIris);
|
|
188
|
+
const handleConsolidate = useCallback(async (decision) => {
|
|
189
|
+
const isPattern = decision.kind === "pattern";
|
|
190
|
+
try {
|
|
191
|
+
await applyMerge(decision);
|
|
192
|
+
toast.success(isPattern
|
|
193
|
+
? `Replaced ${decision.memoryIris.length} memories with one general memory`
|
|
194
|
+
: `Merged ${decision.memoryIris.length} memories`);
|
|
195
|
+
if (reviewGroups.length <= 1)
|
|
196
|
+
setConsolidating(false);
|
|
197
|
+
}
|
|
198
|
+
catch (e) {
|
|
199
|
+
reportMutationError(e, isPattern ? "Failed to generalize memories" : "Failed to merge memories");
|
|
200
|
+
}
|
|
201
|
+
}, [applyMerge, reviewGroups.length]);
|
|
202
|
+
const kindFilterResetRef = useRef(null);
|
|
203
|
+
const handleCreate = useCallback(async (form, supersedesMemoryIris) => {
|
|
204
|
+
if (!addDialog)
|
|
205
|
+
return;
|
|
206
|
+
if (!isOperator && addDialog.scope !== GQLMemoryScope.User)
|
|
207
|
+
return;
|
|
208
|
+
try {
|
|
209
|
+
const result = await createMutation.mutateAsync({
|
|
210
|
+
input: buildCreateMemoryInput(form, addDialog.scope, supersedesMemoryIris),
|
|
211
|
+
});
|
|
212
|
+
if (result.createMemory.error)
|
|
213
|
+
throw new Error(result.createMemory.error);
|
|
214
|
+
toast.success("Memory added");
|
|
215
|
+
setAddDialog(null);
|
|
216
|
+
kindFilterResetRef.current?.("ALL");
|
|
217
|
+
invalidate();
|
|
218
|
+
}
|
|
219
|
+
catch (e) {
|
|
220
|
+
reportMutationError(e, "Failed to add memory");
|
|
221
|
+
}
|
|
222
|
+
}, [addDialog, createMutation, invalidate, isOperator]);
|
|
223
|
+
const handleUpdate = useCallback(async (form, supersedesMemoryIris) => {
|
|
224
|
+
if (!editingMemory)
|
|
225
|
+
return;
|
|
226
|
+
if (!isOperator && editingMemory.scope !== GQLMemoryScope.User)
|
|
227
|
+
return;
|
|
228
|
+
try {
|
|
229
|
+
const result = await updateMutation.mutateAsync({
|
|
230
|
+
input: buildUpdateMemoryInput(editingMemory.iri, form, editingMemory.scope, supersedesMemoryIris),
|
|
231
|
+
});
|
|
232
|
+
if (result.updateMemory.error)
|
|
233
|
+
throw new Error(result.updateMemory.error);
|
|
234
|
+
toast.success("Memory updated");
|
|
235
|
+
setEditingMemory(null);
|
|
236
|
+
kindFilterResetRef.current?.("ALL");
|
|
237
|
+
invalidate();
|
|
238
|
+
}
|
|
239
|
+
catch (e) {
|
|
240
|
+
reportMutationError(e, "Failed to update memory");
|
|
241
|
+
}
|
|
242
|
+
}, [editingMemory, updateMutation, invalidate, isOperator]);
|
|
243
|
+
const handleDelete = useCallback(async (iri) => {
|
|
244
|
+
const target = allMemories.find((m) => m.iri === iri);
|
|
245
|
+
if (!isOperator && target && target.scope !== GQLMemoryScope.User)
|
|
246
|
+
return;
|
|
247
|
+
try {
|
|
248
|
+
await deleteMutation.mutateAsync({ memoryIri: iri });
|
|
249
|
+
toast.success("Memory deleted");
|
|
250
|
+
invalidate();
|
|
251
|
+
}
|
|
252
|
+
catch (e) {
|
|
253
|
+
reportMutationError(e, "Failed to delete memory");
|
|
254
|
+
}
|
|
255
|
+
}, [deleteMutation, invalidate, isOperator, allMemories]);
|
|
256
|
+
const handleRestore = useCallback(async (iri) => {
|
|
257
|
+
try {
|
|
258
|
+
const result = await restoreMutation.mutateAsync({ memoryIri: iri });
|
|
259
|
+
const { error, blockedByIri, blockedByLabel } = result.restoreMemory;
|
|
260
|
+
if (error) {
|
|
261
|
+
// Restore is blocked while the superseding memory is still active.
|
|
262
|
+
if (blockedByIri) {
|
|
263
|
+
setRestoreBlocked({ iri, blockedByIri, blockedByLabel: blockedByLabel ?? null });
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
throw new Error(error);
|
|
267
|
+
}
|
|
268
|
+
toast.success("Memory restored");
|
|
269
|
+
invalidate();
|
|
270
|
+
}
|
|
271
|
+
catch (e) {
|
|
272
|
+
reportMutationError(e, "Failed to restore memory");
|
|
273
|
+
}
|
|
274
|
+
}, [restoreMutation, invalidate]);
|
|
275
|
+
const handleReconfirm = useCallback(async (iri) => {
|
|
276
|
+
try {
|
|
277
|
+
const result = await reconfirmMutation.mutateAsync({ memoryIri: iri });
|
|
278
|
+
if (result.reconfirmMemory.error)
|
|
279
|
+
throw new Error(result.reconfirmMemory.error);
|
|
280
|
+
toast.success("Memory reconfirmed");
|
|
281
|
+
invalidate();
|
|
282
|
+
}
|
|
283
|
+
catch (e) {
|
|
284
|
+
reportMutationError(e, "Failed to reconfirm memory");
|
|
285
|
+
}
|
|
286
|
+
}, [reconfirmMutation, invalidate]);
|
|
287
|
+
const handleConfirmRestoreBlocked = useCallback(() => {
|
|
288
|
+
if (!restoreBlocked)
|
|
289
|
+
return;
|
|
290
|
+
const { iri, blockedByIri, blockedByLabel } = restoreBlocked;
|
|
291
|
+
void (async () => {
|
|
292
|
+
try {
|
|
293
|
+
await deleteMutation.mutateAsync({ memoryIri: blockedByIri });
|
|
294
|
+
const retry = await restoreMutation.mutateAsync({ memoryIri: iri });
|
|
295
|
+
if (retry.restoreMemory.error)
|
|
296
|
+
throw new Error(retry.restoreMemory.error);
|
|
297
|
+
toast.success(`Restored - replaced "${blockedByLabel ?? "the newer memory"}"`);
|
|
298
|
+
invalidate();
|
|
299
|
+
}
|
|
300
|
+
catch (e) {
|
|
301
|
+
reportMutationError(e, "Failed to restore memory");
|
|
302
|
+
}
|
|
303
|
+
finally {
|
|
304
|
+
setRestoreBlocked(null);
|
|
305
|
+
}
|
|
306
|
+
})();
|
|
307
|
+
}, [restoreBlocked, deleteMutation, restoreMutation, invalidate]);
|
|
308
|
+
const handleMove = useCallback(async (newScope, newTeams, supersedesMemoryIris) => {
|
|
309
|
+
if (!movingMemory)
|
|
310
|
+
return;
|
|
311
|
+
if (!isOperator)
|
|
312
|
+
return;
|
|
313
|
+
try {
|
|
314
|
+
const result = await updateMutation.mutateAsync({
|
|
315
|
+
input: {
|
|
316
|
+
iri: movingMemory.iri,
|
|
317
|
+
label: movingMemory.label,
|
|
318
|
+
description: movingMemory.description,
|
|
319
|
+
importance: movingMemory.importance,
|
|
320
|
+
// updateMemoryRecord overwrites kind/confidence/memorySubclass; re-send them on move.
|
|
321
|
+
kind: movingMemory.kind,
|
|
322
|
+
confidence: movingMemory.confidence,
|
|
323
|
+
scope: newScope,
|
|
324
|
+
teams: newTeams,
|
|
325
|
+
...(movingMemory.memorySubclass != null ? { memorySubclass: movingMemory.memorySubclass } : {}),
|
|
326
|
+
...(isPresent(supersedesMemoryIris) ? { supersedesMemoryIris } : {}),
|
|
327
|
+
},
|
|
328
|
+
});
|
|
329
|
+
if (result.updateMemory.error)
|
|
330
|
+
throw new Error(result.updateMemory.error);
|
|
331
|
+
toast.success("Memory moved");
|
|
332
|
+
setMovingMemory(null);
|
|
333
|
+
invalidate();
|
|
334
|
+
}
|
|
335
|
+
catch (e) {
|
|
336
|
+
reportMutationError(e, "Failed to move memory");
|
|
337
|
+
}
|
|
338
|
+
}, [movingMemory, updateMutation, invalidate, isOperator]);
|
|
339
|
+
const { duplicates } = useDuplicateDetection(unreviewedPersonalMemories, {
|
|
340
|
+
enabled: activeTab === "personal",
|
|
341
|
+
});
|
|
342
|
+
const columns = useMemoryColumns({
|
|
343
|
+
isOperator,
|
|
344
|
+
userId: user.id,
|
|
345
|
+
schemas,
|
|
346
|
+
onEdit: setEditingMemory,
|
|
347
|
+
onDelete: handleDelete,
|
|
348
|
+
onMove: isOperator ? setMovingMemory : undefined,
|
|
349
|
+
onRestore: handleRestore,
|
|
350
|
+
onReconfirm: handleReconfirm,
|
|
351
|
+
duplicates,
|
|
352
|
+
});
|
|
353
|
+
const displayedPersonalMemories = unreviewedOnly ? unreviewedPersonalMemories : personalMemories;
|
|
354
|
+
const personalTableState = useMemoryTable(tableKeyPersonal, displayedPersonalMemories, columns, [], personalColumnVisibility);
|
|
355
|
+
const companyTableState = useMemoryTable(tableKeyCompany, companyMemories, columns, [], companyColumnVisibility);
|
|
356
|
+
const teamTableState = useMemoryTable(tableKeyGroup, teamMemories, columns, [], teamColumnVisibility);
|
|
357
|
+
kindFilterResetRef.current = personalTableState.onKindFilterChange;
|
|
358
|
+
const handleRowClick = useCallback((m) => {
|
|
359
|
+
if (m.supersededBy == null)
|
|
360
|
+
setEditingMemory(m);
|
|
361
|
+
}, []);
|
|
362
|
+
const tabCounts = {
|
|
363
|
+
company: { count: companyCount },
|
|
364
|
+
team: { count: teamCount },
|
|
365
|
+
personal: { count: personalCount, secondaryCount: unreviewedCount },
|
|
366
|
+
};
|
|
367
|
+
const scopeTabs = availableTabs.map((tab) => ({
|
|
368
|
+
value: tab,
|
|
369
|
+
label: tabLabels[tab],
|
|
370
|
+
...tabCounts[tab],
|
|
371
|
+
}));
|
|
372
|
+
const tabsList = _jsx(ScopeTabs, { tabs: scopeTabs, activeValue: activeTab, isLoading: isLoading });
|
|
373
|
+
const addMemoryButton = addMemoryButtonConfig(activeTab);
|
|
374
|
+
const canShowArchived = activeTab === "personal" || (isOperator && (activeTab === "company" || activeTab === "team"));
|
|
375
|
+
const headerActions = [
|
|
376
|
+
{
|
|
377
|
+
key: "refresh",
|
|
378
|
+
label: "Refresh",
|
|
379
|
+
icon: RefreshCcw,
|
|
380
|
+
variant: "tertiary",
|
|
381
|
+
iconOnly: true,
|
|
382
|
+
disabled: isFetching,
|
|
383
|
+
onClick: handleRefresh,
|
|
384
|
+
},
|
|
385
|
+
...(canShowArchived
|
|
386
|
+
? [
|
|
387
|
+
{
|
|
388
|
+
key: "archived",
|
|
389
|
+
label: showArchived ? "Hide archived" : "Show archived",
|
|
390
|
+
icon: showArchived ? EyeOff : Archive,
|
|
391
|
+
variant: "secondary",
|
|
392
|
+
active: showArchived,
|
|
393
|
+
onClick: () => setShowArchived((v) => !v),
|
|
394
|
+
},
|
|
395
|
+
{
|
|
396
|
+
key: "out-of-window",
|
|
397
|
+
label: showOutOfWindow ? "Hide expired" : "Show expired",
|
|
398
|
+
icon: showOutOfWindow ? EyeOff : CalendarX,
|
|
399
|
+
variant: "secondary",
|
|
400
|
+
active: showOutOfWindow,
|
|
401
|
+
onClick: () => setShowOutOfWindow((v) => !v),
|
|
402
|
+
},
|
|
403
|
+
]
|
|
404
|
+
: []),
|
|
405
|
+
...(canShowArchived
|
|
406
|
+
? [
|
|
407
|
+
{
|
|
408
|
+
key: "consolidate",
|
|
409
|
+
label: "Review redundancy",
|
|
410
|
+
icon: CopyCheck,
|
|
411
|
+
variant: "secondary",
|
|
412
|
+
onClick: () => {
|
|
413
|
+
setMergedIris(new Set());
|
|
414
|
+
setConsolidating(true);
|
|
415
|
+
},
|
|
416
|
+
},
|
|
417
|
+
]
|
|
418
|
+
: []),
|
|
419
|
+
...(activeTab === "personal" && unreviewedCount > 0
|
|
420
|
+
? [
|
|
421
|
+
{
|
|
422
|
+
key: "unreviewed",
|
|
423
|
+
label: unreviewedOnly ? "All memories" : `Unreviewed (${unreviewedCount})`,
|
|
424
|
+
variant: unreviewedOnly ? "secondary" : "ghost",
|
|
425
|
+
active: unreviewedOnly,
|
|
426
|
+
onClick: () => setUnreviewedOnly((v) => !v),
|
|
427
|
+
},
|
|
428
|
+
]
|
|
429
|
+
: []),
|
|
430
|
+
...(addMemoryButton
|
|
431
|
+
? [
|
|
432
|
+
{
|
|
433
|
+
key: "add",
|
|
434
|
+
label: addMemoryButton.label,
|
|
435
|
+
icon: Plus,
|
|
436
|
+
onClick: () => setAddDialog({ scope: addMemoryButton.scope }),
|
|
437
|
+
},
|
|
438
|
+
]
|
|
439
|
+
: []),
|
|
440
|
+
];
|
|
441
|
+
const formDialogState = addDialog
|
|
442
|
+
? {
|
|
443
|
+
scope: addDialog.scope,
|
|
444
|
+
initial: undefined,
|
|
445
|
+
onSave: handleCreate,
|
|
446
|
+
isSaving: createMutation.isPending,
|
|
447
|
+
onClose: () => setAddDialog(null),
|
|
448
|
+
}
|
|
449
|
+
: editingMemory
|
|
450
|
+
? {
|
|
451
|
+
scope: editingMemory.scope,
|
|
452
|
+
initial: editingMemory,
|
|
453
|
+
onSave: handleUpdate,
|
|
454
|
+
isSaving: updateMutation.isPending,
|
|
455
|
+
onClose: () => setEditingMemory(null),
|
|
456
|
+
}
|
|
457
|
+
: null;
|
|
458
|
+
return (_jsxs(RouteLayout, { user: user, datastore: datastore, datastores: datastores, isOperator: isOperator, canSwitchDatastore: canSwitchDatastore, logout: logout, children: [_jsx(PageHeader, { title: "Memories", icon: Brain, actions: _jsx(PageHeaderActions, { actions: headerActions }) }), _jsxs(Tabs, { value: activeTab, onValueChange: handleTabChange, className: "flex flex-col flex-1 min-h-0 overflow-hidden", children: [_jsx(ScopeMemoriesTab, { value: "personal", tableState: personalTableState, memories: displayedPersonalMemories, tabs: tabsList, isLoading: isLoading, errorMessage: errorMessage, schemas: schemas, onRowClick: handleRowClick, visibleStatusFilters: [
|
|
459
|
+
...alwaysVisibleStatusFilters,
|
|
460
|
+
"autoCaptured",
|
|
461
|
+
...(isOperator ? ["promotion"] : []),
|
|
462
|
+
], emptyState: _jsx(MemoriesEmptyState, { isOperator: isOperator, onAdd: () => setAddDialog({ scope: GQLMemoryScope.User }) }) }), isOperator && (_jsx(ScopeMemoriesTab, { value: "company", tableState: companyTableState, memories: companyMemories, tabs: tabsList, isLoading: isLoading, errorMessage: errorMessage, schemas: schemas, onRowClick: handleRowClick, emptyState: showCompanyOnboarding ? (_jsx("div", { className: "flex items-center justify-center h-full min-h-64 p-5", children: _jsx(MemoryOnboardingWizard, { scope: GQLMemoryScope.Company, onComplete: () => setShowOrgOnboarding(false), onSkip: () => setShowOrgOnboarding(false) }) })) : (_jsx(CompanyMemoriesEmptyState, { onAdd: () => setAddDialog({ scope: GQLMemoryScope.Company }), onSetUp: () => setShowOrgOnboarding(true) })) })), isOperator && hasTeams && (_jsx(ScopeMemoriesTab, { value: "team", tableState: teamTableState, memories: teamMemories, tabs: tabsList, isLoading: isLoading, errorMessage: errorMessage, schemas: schemas, onRowClick: handleRowClick, emptyState: _jsx(GroupMemoriesEmptyState, { isOperator: true, onAdd: () => setAddDialog({ scope: GQLMemoryScope.Team }) }) })), isOperator && (_jsx(FilterableTabContent, { value: "schemas", tabs: tabsList, placeholder: "Filter schemas\u2026", query: schemasQuery, onQueryChange: setSchemasQuery, children: _jsx(SchemasTab, { query: schemasQuery }) })), _jsx(FilterableTabContent, { value: "live-context", tabs: tabsList, placeholder: "Filter live context\u2026", query: liveContextQuery, onQueryChange: setLiveContextQuery, children: _jsx(LiveContextTab, { query: liveContextQuery }) })] }), formDialogState && (_jsx(MemoryFormDialog, { open: true, onOpenChange: (open) => {
|
|
463
|
+
if (!open)
|
|
464
|
+
formDialogState.onClose();
|
|
465
|
+
}, scope: formDialogState.scope, initial: formDialogState.initial, onSave: formDialogState.onSave, isSaving: formDialogState.isSaving, isOperator: isOperator, schemas: schemas, currentUserId: user.id })), movingMemory && (_jsx(ChangeScopeDialog, { open: true, onOpenChange: (open) => {
|
|
466
|
+
if (!open)
|
|
467
|
+
setMovingMemory(null);
|
|
468
|
+
}, memory: movingMemory, isOperator: isOperator, onSave: handleMove, isSaving: updateMutation.isPending, currentUserId: user.id })), _jsx(RestoreBlockedDialog, { open: restoreBlocked != null, onOpenChange: (open) => {
|
|
469
|
+
if (!open)
|
|
470
|
+
setRestoreBlocked(null);
|
|
471
|
+
}, blockedByLabel: restoreBlocked?.blockedByLabel ?? null, onConfirm: handleConfirmRestoreBlocked }), _jsx(ConsolidationReviewDialog, { open: consolidating, clusters: reviewGroups, isLoading: isFindingRedundancy, isSaving: consolidateMutation.isPending, candidateCount: consolidationCandidates.length, comparedCount: reviewData?.memoryRedundancyReview.comparedCount ?? null, timedOut: reviewData?.memoryRedundancyReview.timedOut ?? false, errorMessage: reviewData?.memoryRedundancyReview.error ?? (reviewError instanceof Error ? reviewError.message : null), onMerge: handleConsolidate, onClose: () => setConsolidating(false) })] }));
|
|
472
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { FormErrors, MemoryFormState } from "./components/MemoryFields.js";
|
|
3
|
+
export declare const descriptionSchema: z.ZodString;
|
|
4
|
+
export declare const labelRequiredSchema: z.ZodString;
|
|
5
|
+
export declare const labelOptionalSchema: z.ZodString;
|
|
6
|
+
export declare const teamsRequiredSchema: z.ZodArray<z.ZodString>;
|
|
7
|
+
export declare function validateMemoryForm(form: MemoryFormState, isEdit: boolean, isTeam: boolean): FormErrors;
|
|
8
|
+
//# sourceMappingURL=memory-form-validation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memory-form-validation.d.ts","sourceRoot":"","sources":["../../../src/routes/memories/memory-form-validation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAE7E,eAAO,MAAM,iBAAiB,aAO3B,CAAC;AACJ,eAAO,MAAM,mBAAmB,aAIoB,CAAC;AACrD,eAAO,MAAM,mBAAmB,aAA+D,CAAC;AAChG,eAAO,MAAM,mBAAmB,yBAAyD,CAAC;AAE1F,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,eAAe,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,GAAG,UAAU,CAuBtG"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const descriptionSchema = z
|
|
3
|
+
.string()
|
|
4
|
+
.trim()
|
|
5
|
+
.min(1, "Description is required")
|
|
6
|
+
.max(1000, "Description must be 1000 characters or fewer - memories load into every conversation, so keep them focused. Split longer context into separate memories.");
|
|
7
|
+
export const labelRequiredSchema = z
|
|
8
|
+
.string()
|
|
9
|
+
.trim()
|
|
10
|
+
.min(1, "Label is required")
|
|
11
|
+
.max(100, "Label must be 100 characters or fewer");
|
|
12
|
+
export const labelOptionalSchema = z.string().max(100, "Label must be 100 characters or fewer");
|
|
13
|
+
export const teamsRequiredSchema = z.array(z.string()).min(1, "Select at least one team");
|
|
14
|
+
export function validateMemoryForm(form, isEdit, isTeam) {
|
|
15
|
+
const errors = {};
|
|
16
|
+
const descResult = descriptionSchema.safeParse(form.description ?? "");
|
|
17
|
+
if (!descResult.success)
|
|
18
|
+
errors.description = descResult.error.issues[0]?.message;
|
|
19
|
+
const labelResult = (isEdit ? labelRequiredSchema : labelOptionalSchema).safeParse(form.label);
|
|
20
|
+
if (!labelResult.success)
|
|
21
|
+
errors.label = labelResult.error.issues[0]?.message;
|
|
22
|
+
if (isTeam) {
|
|
23
|
+
const teamsResult = teamsRequiredSchema.safeParse(form.teams);
|
|
24
|
+
if (!teamsResult.success)
|
|
25
|
+
errors.teams = teamsResult.error.issues[0]?.message;
|
|
26
|
+
}
|
|
27
|
+
if (form.memoryTrustedUntil != null &&
|
|
28
|
+
form.memoryValidUntil != null &&
|
|
29
|
+
new Date(form.memoryTrustedUntil) > new Date(form.memoryValidUntil)) {
|
|
30
|
+
errors.memoryTrustedUntil = "Trusted until cannot be later than Valid until.";
|
|
31
|
+
}
|
|
32
|
+
return errors;
|
|
33
|
+
}
|