@memberjunction/ng-conversations 6.1.0-edge.2 → 6.1.0-edge.3
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/LICENSE +180 -4
- package/README.md +32 -1
- package/dist/lib/components/conversation/conversation-chat-area.component.d.ts +159 -9
- package/dist/lib/components/conversation/conversation-chat-area.component.d.ts.map +1 -1
- package/dist/lib/components/conversation/conversation-chat-area.component.js +497 -106
- package/dist/lib/components/conversation/conversation-chat-area.component.js.map +1 -1
- package/dist/lib/components/conversation/pinned-messages-panel.component.d.ts +14 -2
- package/dist/lib/components/conversation/pinned-messages-panel.component.d.ts.map +1 -1
- package/dist/lib/components/conversation/pinned-messages-panel.component.js +37 -19
- package/dist/lib/components/conversation/pinned-messages-panel.component.js.map +1 -1
- package/dist/lib/components/message/message-input.component.d.ts +7 -3
- package/dist/lib/components/message/message-input.component.d.ts.map +1 -1
- package/dist/lib/components/message/message-input.component.js +78 -159
- package/dist/lib/components/message/message-input.component.js.map +1 -1
- package/dist/lib/components/message/message-list.component.d.ts +287 -7
- package/dist/lib/components/message/message-list.component.d.ts.map +1 -1
- package/dist/lib/components/message/message-list.component.js +850 -56
- package/dist/lib/components/message/message-list.component.js.map +1 -1
- package/dist/lib/services/conversation-agent.service.d.ts +98 -16
- package/dist/lib/services/conversation-agent.service.d.ts.map +1 -1
- package/dist/lib/services/conversation-agent.service.js +218 -69
- package/dist/lib/services/conversation-agent.service.js.map +1 -1
- package/dist/lib/services/conversation-detail-window.store.d.ts +206 -0
- package/dist/lib/services/conversation-detail-window.store.d.ts.map +1 -0
- package/dist/lib/services/conversation-detail-window.store.js +503 -0
- package/dist/lib/services/conversation-detail-window.store.js.map +1 -0
- package/dist/lib/utils/agent-artifact-summary.d.ts +54 -0
- package/dist/lib/utils/agent-artifact-summary.d.ts.map +1 -0
- package/dist/lib/utils/agent-artifact-summary.js +49 -0
- package/dist/lib/utils/agent-artifact-summary.js.map +1 -0
- package/dist/lib/utils/conversation-detail-window.d.ts +49 -0
- package/dist/lib/utils/conversation-detail-window.d.ts.map +1 -0
- package/dist/lib/utils/conversation-detail-window.js +86 -0
- package/dist/lib/utils/conversation-detail-window.js.map +1 -0
- package/dist/lib/utils/date-jump.d.ts +65 -0
- package/dist/lib/utils/date-jump.d.ts.map +1 -0
- package/dist/lib/utils/date-jump.js +106 -0
- package/dist/lib/utils/date-jump.js.map +1 -0
- package/package.json +32 -32
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-artifact-summary.d.ts","sourceRoot":"","sources":["../../../src/lib/utils/agent-artifact-summary.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,+FAA+F;AAC/F,MAAM,WAAW,uBAAuB;IACpC,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB;AAED,6CAA6C;AAC7C,MAAM,WAAW,gBAAgB;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,oBAAoB;IACjC,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,oDAAoD;IACpD,QAAQ,EAAE,KAAK,CAAC;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,aAAa,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC,CAAC;CAC7F;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,uBAAuB,CACnC,QAAQ,EAAE,SAAS,uBAAuB,EAAE,EAC5C,aAAa,EAAE,WAAW,CAAC,MAAM,EAAE,gBAAgB,CAAC,GACrD,oBAAoB,EAAE,CA4BxB"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grouping for the "Prior Artifacts Created by This Agent" block fed to the Check Sage Intent
|
|
3
|
+
* prompt.
|
|
4
|
+
*
|
|
5
|
+
* PURE (no Angular, no entities, no provider) so the ordering contract below is unit-testable
|
|
6
|
+
* without the service's DI — same reason `conversation-detail-window.ts` and `date-jump.ts`
|
|
7
|
+
* live out here rather than on their consumers.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Folds versions into per-artifact summaries.
|
|
11
|
+
*
|
|
12
|
+
* NO SORT, and that is deliberate rather than an omission. The caller reads versions
|
|
13
|
+
* `__mj_CreatedAt DESC`, so inserting in arrival order leaves each artifact's versions
|
|
14
|
+
* newest-first AND leaves the Map ordered by whichever artifact was touched most recently —
|
|
15
|
+
* which is exactly what the explicit sort in the array-scan version used to compute. The
|
|
16
|
+
* assumption is load-bearing and invisible, so it is pinned by tests.
|
|
17
|
+
*
|
|
18
|
+
* @param versions - Artifact versions, newest first
|
|
19
|
+
* @param artifactsById - The artifact rows behind those versions
|
|
20
|
+
*/
|
|
21
|
+
export function GroupVersionsByArtifact(versions, artifactsById) {
|
|
22
|
+
const byArtifact = new Map();
|
|
23
|
+
for (const version of versions) {
|
|
24
|
+
const artifact = artifactsById.get(version.ArtifactID);
|
|
25
|
+
if (!artifact) {
|
|
26
|
+
// Mirrors the stored query's INNER JOIN semantics. A version can outlive the
|
|
27
|
+
// artifact row the caller can read; emitting a summary with an undefined name
|
|
28
|
+
// would put the string "undefined" straight into the classifier prompt.
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
let summary = byArtifact.get(version.ArtifactID);
|
|
32
|
+
if (!summary) {
|
|
33
|
+
summary = {
|
|
34
|
+
artifactId: version.ArtifactID,
|
|
35
|
+
artifactName: artifact.Name,
|
|
36
|
+
artifactType: artifact.Type,
|
|
37
|
+
versions: []
|
|
38
|
+
};
|
|
39
|
+
byArtifact.set(version.ArtifactID, summary);
|
|
40
|
+
}
|
|
41
|
+
summary.versions.push({
|
|
42
|
+
versionId: version.ID,
|
|
43
|
+
versionNumber: version.VersionNumber,
|
|
44
|
+
versionName: version.Name
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return [...byArtifact.values()];
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=agent-artifact-summary.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-artifact-summary.js","sourceRoot":"","sources":["../../../src/lib/utils/agent-artifact-summary.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAiCH;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,uBAAuB,CACnC,QAA4C,EAC5C,aAAoD;IAEpD,MAAM,UAAU,GAAG,IAAI,GAAG,EAAgC,CAAC;IAE3D,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC7B,MAAM,QAAQ,GAAG,aAAa,CAAC,GAAG,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;QACvD,IAAI,CAAC,QAAQ,EAAE,CAAC;YACZ,6EAA6E;YAC7E,8EAA8E;YAC9E,wEAAwE;YACxE,SAAS;QACb,CAAC;QACD,IAAI,OAAO,GAAG,UAAU,CAAC,GAAG,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;QACjD,IAAI,CAAC,OAAO,EAAE,CAAC;YACX,OAAO,GAAG;gBACN,UAAU,EAAE,OAAO,CAAC,UAAU;gBAC9B,YAAY,EAAE,QAAQ,CAAC,IAAI;gBAC3B,YAAY,EAAE,QAAQ,CAAC,IAAI;gBAC3B,QAAQ,EAAE,EAAE;aACf,CAAC;YACF,UAAU,CAAC,GAAG,CAAC,OAAO,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;QAChD,CAAC;QACD,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC;YAClB,SAAS,EAAE,OAAO,CAAC,EAAE;YACrB,aAAa,EAAE,OAAO,CAAC,aAAa;YACpC,WAAW,EAAE,OAAO,CAAC,IAAI;SAC5B,CAAC,CAAC;IACP,CAAC;IACD,OAAO,CAAC,GAAG,UAAU,CAAC,MAAM,EAAE,CAAC,CAAC;AACpC,CAAC","sourcesContent":["/**\n * Grouping for the \"Prior Artifacts Created by This Agent\" block fed to the Check Sage Intent\n * prompt.\n *\n * PURE (no Angular, no entities, no provider) so the ordering contract below is unit-testable\n * without the service's DI — same reason `conversation-detail-window.ts` and `date-jump.ts`\n * live out here rather than on their consumers.\n */\n\n/** The version fields the grouping needs. Structural, so the RunView row type satisfies it. */\nexport interface AgentArtifactVersionRow {\n ID: string;\n ArtifactID: string;\n VersionNumber: number;\n Name: string | null;\n}\n\n/** The artifact fields the prompt prints. */\nexport interface AgentArtifactRow {\n ID: string;\n Name: string;\n Type: string;\n}\n\n/**\n * One artifact an agent produced in this conversation, with its versions newest-first.\n *\n * Deliberately narrower than the array-scan shape it replaces, which also carried `runId`,\n * `artifactDescription`, `versionDescription` and `createdAt` — none of which the intent\n * prompt ever read. Dropping them is what lets the caller resolve this without joining back\n * to agent runs, turning a four-table reconstruction into two reads.\n */\nexport interface AgentArtifactSummary {\n artifactId: string;\n artifactName: string;\n artifactType: string;\n /** Newest first, so `versions[0]` is the latest. */\n versions: Array<{ versionId: string; versionNumber: number; versionName: string | null }>;\n}\n\n/**\n * Folds versions into per-artifact summaries.\n *\n * NO SORT, and that is deliberate rather than an omission. The caller reads versions\n * `__mj_CreatedAt DESC`, so inserting in arrival order leaves each artifact's versions\n * newest-first AND leaves the Map ordered by whichever artifact was touched most recently —\n * which is exactly what the explicit sort in the array-scan version used to compute. The\n * assumption is load-bearing and invisible, so it is pinned by tests.\n *\n * @param versions - Artifact versions, newest first\n * @param artifactsById - The artifact rows behind those versions\n */\nexport function GroupVersionsByArtifact(\n versions: readonly AgentArtifactVersionRow[],\n artifactsById: ReadonlyMap<string, AgentArtifactRow>\n): AgentArtifactSummary[] {\n const byArtifact = new Map<string, AgentArtifactSummary>();\n\n for (const version of versions) {\n const artifact = artifactsById.get(version.ArtifactID);\n if (!artifact) {\n // Mirrors the stored query's INNER JOIN semantics. A version can outlive the\n // artifact row the caller can read; emitting a summary with an undefined name\n // would put the string \"undefined\" straight into the classifier prompt.\n continue;\n }\n let summary = byArtifact.get(version.ArtifactID);\n if (!summary) {\n summary = {\n artifactId: version.ArtifactID,\n artifactName: artifact.Name,\n artifactType: artifact.Type,\n versions: []\n };\n byArtifact.set(version.ArtifactID, summary);\n }\n summary.versions.push({\n versionId: version.ID,\n versionNumber: version.VersionNumber,\n versionName: version.Name\n });\n }\n return [...byArtifact.values()];\n}\n"]}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type RealtimeTimelineSourceDetail } from './realtime-session-timeline';
|
|
2
|
+
export declare function SelectLatestTimelinePage<T extends RealtimeTimelineSourceDetail>(chronologicalDetails: readonly T[], pageSize: number): {
|
|
3
|
+
Page: T[];
|
|
4
|
+
OldestIncluded: T | null;
|
|
5
|
+
};
|
|
6
|
+
/** Default number of *timeline items* shown on first paint and per older page. */
|
|
7
|
+
export declare const DEFAULT_TRANSCRIPT_PAGE_SIZE = 10;
|
|
8
|
+
/** Raw-row over-read so session collapse still fills a page of timeline items. */
|
|
9
|
+
export declare const DEFAULT_RAW_OVERREAD: number;
|
|
10
|
+
/**
|
|
11
|
+
* How many times a short page may re-fetch with a wider over-read before settling.
|
|
12
|
+
*
|
|
13
|
+
* The over-read exists because raw rows are not display items — a realtime session folds
|
|
14
|
+
* many rows into ONE timeline card, so `3 × pageSize` rows can yield two or three items
|
|
15
|
+
* instead of ten. Growing the read is what keeps a page a page.
|
|
16
|
+
*
|
|
17
|
+
* Bounded, because the pathological case is a conversation that is almost entirely one long
|
|
18
|
+
* session: growing without limit there would walk the whole table, which is the exact cost
|
|
19
|
+
* windowing exists to avoid. Three attempts reaches 12 × pageSize, past which a short page is
|
|
20
|
+
* the honest answer and the sentinel simply fires again.
|
|
21
|
+
*/
|
|
22
|
+
export declare const MAX_OVERREAD_ATTEMPTS = 3;
|
|
23
|
+
/** Multiplier applied to the over-read on each retry. */
|
|
24
|
+
export declare const OVERREAD_GROWTH_FACTOR = 2;
|
|
25
|
+
/**
|
|
26
|
+
* Extra pages a tail refresh may read to bridge back to the rows it already holds.
|
|
27
|
+
*
|
|
28
|
+
* A refresh reads the NEWEST page. If the conversation grew by more than a page since the
|
|
29
|
+
* last read — a long agent burst, a tab left open — that page starts above the loaded tail
|
|
30
|
+
* and the rows in between belong to neither. Merging the two anyway produces a set that
|
|
31
|
+
* sorts into order and looks contiguous while silently missing its middle, which is the
|
|
32
|
+
* worst possible outcome: nothing about the transcript says anything is absent.
|
|
33
|
+
*
|
|
34
|
+
* So the refresh pages down until it reaches known rows. Bounded, because "the tail ran away
|
|
35
|
+
* from us" has no upper limit and chasing it without one is the unbounded read this whole
|
|
36
|
+
* feature exists to remove. Past the budget the refresh drops the disconnected older rows
|
|
37
|
+
* instead and lets the sentinel offer them back — a visible gap the reader can close, rather
|
|
38
|
+
* than an invisible one they cannot.
|
|
39
|
+
*/
|
|
40
|
+
export declare const MAX_REFRESH_BACKFILL_PAGES = 3;
|
|
41
|
+
export interface ConversationDetailWindowCursor {
|
|
42
|
+
/** Exclusive upper bound for the next older fetch. Null only when the window is empty. */
|
|
43
|
+
OldestSequence: number | null;
|
|
44
|
+
/** Inclusive high-water mark of the loaded tail. Used to detect live appends. */
|
|
45
|
+
NewestSequence: number | null;
|
|
46
|
+
/** True when at least one older raw row exists that is not in LoadedDetails. */
|
|
47
|
+
HasMoreAbove: boolean;
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=conversation-detail-window.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conversation-detail-window.d.ts","sourceRoot":"","sources":["../../../src/lib/utils/conversation-detail-window.ts"],"names":[],"mappings":"AACA,OAAO,EAEH,KAAK,4BAA4B,EACpC,MAAM,6BAA6B,CAAC;AAErC,wBAAgB,wBAAwB,CAAC,CAAC,SAAS,4BAA4B,EAC3E,oBAAoB,EAAE,SAAS,CAAC,EAAE,EAClC,QAAQ,EAAE,MAAM,GACjB;IAAE,IAAI,EAAE,CAAC,EAAE,CAAC;IAAC,cAAc,EAAE,CAAC,GAAG,IAAI,CAAA;CAAE,CA0BzC;AAyBD,kFAAkF;AAClF,eAAO,MAAM,4BAA4B,KAAK,CAAC;AAE/C,kFAAkF;AAClF,eAAO,MAAM,oBAAoB,QAAmC,CAAC;AAErE;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,qBAAqB,IAAI,CAAC;AAEvC,yDAAyD;AACzD,eAAO,MAAM,sBAAsB,IAAI,CAAC;AAExC;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,0BAA0B,IAAI,CAAC;AAE5C,MAAM,WAAW,8BAA8B;IAC3C,0FAA0F;IAC1F,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,iFAAiF;IACjF,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,gFAAgF;IAChF,YAAY,EAAE,OAAO,CAAC;CACzB"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { NormalizeUUID } from '@memberjunction/global';
|
|
2
|
+
import { BuildConversationTimeline } from './realtime-session-timeline';
|
|
3
|
+
export function SelectLatestTimelinePage(chronologicalDetails, pageSize) {
|
|
4
|
+
const timeline = BuildConversationTimeline(chronologicalDetails);
|
|
5
|
+
const pageItems = timeline.slice(Math.max(0, timeline.length - pageSize));
|
|
6
|
+
// Message items match by identity (the grouping pass passes the row through
|
|
7
|
+
// untouched); session items by normalized id, so every folded row comes along.
|
|
8
|
+
const includedMessages = new Set();
|
|
9
|
+
const includedSessionKeys = new Set();
|
|
10
|
+
for (const item of pageItems) {
|
|
11
|
+
if (item.Kind === 'message') {
|
|
12
|
+
includedMessages.add(item.Detail);
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
includedSessionKeys.add(NormalizeUUID(item.Group.SessionID));
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
// Filter the ORIGINAL row set rather than walking the page items, so the result keeps
|
|
19
|
+
// chronological order and picks up session rows interleaved after the card's position.
|
|
20
|
+
const page = chronologicalDetails.filter(detail => {
|
|
21
|
+
const sessionId = trimmedSessionId(detail.AgentSessionID);
|
|
22
|
+
return sessionId === null
|
|
23
|
+
? includedMessages.has(detail)
|
|
24
|
+
: includedSessionKeys.has(NormalizeUUID(sessionId));
|
|
25
|
+
});
|
|
26
|
+
return { Page: page, OldestIncluded: page.length > 0 ? page[0] : null };
|
|
27
|
+
}
|
|
28
|
+
// NOTE: the plan also called for a `NeedsSessionExpansion` helper here, shared between the
|
|
29
|
+
// store and the engine. It is not exported, because there is no such shared caller: deciding
|
|
30
|
+
// whether a fetched page landed mid-session is something only the fetcher can act on, and the
|
|
31
|
+
// fetcher is `ConversationEngine.expandOldestSession` in `@memberjunction/core-entities`.
|
|
32
|
+
// That package cannot import from this one — the dependency runs Angular → core-entities and
|
|
33
|
+
// must never run the other way — so a helper here would have been exported, tested, and
|
|
34
|
+
// called by nothing. The engine makes the same `trim()` check inline against the row it just
|
|
35
|
+
// read. `trimmedSessionId` below keeps that definition honest on THIS side of the boundary,
|
|
36
|
+
// where `SelectLatestTimelinePage` does need it.
|
|
37
|
+
/**
|
|
38
|
+
* Trims the stamped session id; empty/whitespace stamps count as unstamped — mirrors the
|
|
39
|
+
* grouping pass, so a row this module treats as a normal message is one the timeline
|
|
40
|
+
* treats as a normal message too.
|
|
41
|
+
*/
|
|
42
|
+
function trimmedSessionId(raw) {
|
|
43
|
+
const trimmed = raw?.trim() ?? '';
|
|
44
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
45
|
+
}
|
|
46
|
+
/** Default number of *timeline items* shown on first paint and per older page. */
|
|
47
|
+
export const DEFAULT_TRANSCRIPT_PAGE_SIZE = 10;
|
|
48
|
+
/** Raw-row over-read so session collapse still fills a page of timeline items. */
|
|
49
|
+
export const DEFAULT_RAW_OVERREAD = DEFAULT_TRANSCRIPT_PAGE_SIZE * 3;
|
|
50
|
+
/**
|
|
51
|
+
* How many times a short page may re-fetch with a wider over-read before settling.
|
|
52
|
+
*
|
|
53
|
+
* The over-read exists because raw rows are not display items — a realtime session folds
|
|
54
|
+
* many rows into ONE timeline card, so `3 × pageSize` rows can yield two or three items
|
|
55
|
+
* instead of ten. Growing the read is what keeps a page a page.
|
|
56
|
+
*
|
|
57
|
+
* Bounded, because the pathological case is a conversation that is almost entirely one long
|
|
58
|
+
* session: growing without limit there would walk the whole table, which is the exact cost
|
|
59
|
+
* windowing exists to avoid. Three attempts reaches 12 × pageSize, past which a short page is
|
|
60
|
+
* the honest answer and the sentinel simply fires again.
|
|
61
|
+
*/
|
|
62
|
+
export const MAX_OVERREAD_ATTEMPTS = 3;
|
|
63
|
+
/** Multiplier applied to the over-read on each retry. */
|
|
64
|
+
export const OVERREAD_GROWTH_FACTOR = 2;
|
|
65
|
+
/**
|
|
66
|
+
* Extra pages a tail refresh may read to bridge back to the rows it already holds.
|
|
67
|
+
*
|
|
68
|
+
* A refresh reads the NEWEST page. If the conversation grew by more than a page since the
|
|
69
|
+
* last read — a long agent burst, a tab left open — that page starts above the loaded tail
|
|
70
|
+
* and the rows in between belong to neither. Merging the two anyway produces a set that
|
|
71
|
+
* sorts into order and looks contiguous while silently missing its middle, which is the
|
|
72
|
+
* worst possible outcome: nothing about the transcript says anything is absent.
|
|
73
|
+
*
|
|
74
|
+
* So the refresh pages down until it reaches known rows. Bounded, because "the tail ran away
|
|
75
|
+
* from us" has no upper limit and chasing it without one is the unbounded read this whole
|
|
76
|
+
* feature exists to remove. Past the budget the refresh drops the disconnected older rows
|
|
77
|
+
* instead and lets the sentinel offer them back — a visible gap the reader can close, rather
|
|
78
|
+
* than an invisible one they cannot.
|
|
79
|
+
*/
|
|
80
|
+
export const MAX_REFRESH_BACKFILL_PAGES = 3;
|
|
81
|
+
// NOTE: the loaded-window and fetch-result shapes the plan sketched here live on the
|
|
82
|
+
// consumer instead — `ConversationDetailWindowSnapshot` in
|
|
83
|
+
// `../services/conversation-detail-window.store`, and `DetailWindowLoadResult` in
|
|
84
|
+
// `@memberjunction/core-entities`. Declaring unused duplicates here would give a future
|
|
85
|
+
// contributor two plausible contracts to pick between.
|
|
86
|
+
//# sourceMappingURL=conversation-detail-window.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conversation-detail-window.js","sourceRoot":"","sources":["../../../src/lib/utils/conversation-detail-window.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,EACH,yBAAyB,EAE5B,MAAM,6BAA6B,CAAC;AAErC,MAAM,UAAU,wBAAwB,CACpC,oBAAkC,EAClC,QAAgB;IAEhB,MAAM,QAAQ,GAAG,yBAAyB,CAAC,oBAAoB,CAAC,CAAC;IACjE,MAAM,SAAS,GAAG,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,QAAQ,CAAC,MAAM,GAAG,QAAQ,CAAC,CAAC,CAAC;IAE1E,4EAA4E;IAC5E,+EAA+E;IAC/E,MAAM,gBAAgB,GAAG,IAAI,GAAG,EAAK,CAAC;IACtC,MAAM,mBAAmB,GAAG,IAAI,GAAG,EAAU,CAAC;IAC9C,KAAK,MAAM,IAAI,IAAI,SAAS,EAAE,CAAC;QAC3B,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;YAC1B,gBAAgB,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACtC,CAAC;aAAM,CAAC;YACJ,mBAAmB,CAAC,GAAG,CAAC,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC;QACjE,CAAC;IACL,CAAC;IAED,sFAAsF;IACtF,uFAAuF;IACvF,MAAM,IAAI,GAAG,oBAAoB,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE;QAC9C,MAAM,SAAS,GAAG,gBAAgB,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC;QAC1D,OAAO,SAAS,KAAK,IAAI;YACrB,CAAC,CAAC,gBAAgB,CAAC,GAAG,CAAC,MAAM,CAAC;YAC9B,CAAC,CAAC,mBAAmB,CAAC,GAAG,CAAC,aAAa,CAAC,SAAS,CAAC,CAAC,CAAC;IAC5D,CAAC,CAAC,CAAC;IAEH,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,cAAc,EAAE,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;AAC5E,CAAC;AAID,2FAA2F;AAC3F,6FAA6F;AAC7F,8FAA8F;AAC9F,0FAA0F;AAC1F,6FAA6F;AAC7F,wFAAwF;AACxF,6FAA6F;AAC7F,4FAA4F;AAC5F,iDAAiD;AAEjD;;;;GAIG;AACH,SAAS,gBAAgB,CAAC,GAA8B;IACpD,MAAM,OAAO,GAAG,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IAClC,OAAO,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC;AAC/C,CAAC;AAGD,kFAAkF;AAClF,MAAM,CAAC,MAAM,4BAA4B,GAAG,EAAE,CAAC;AAE/C,kFAAkF;AAClF,MAAM,CAAC,MAAM,oBAAoB,GAAG,4BAA4B,GAAG,CAAC,CAAC;AAErE;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,CAAC;AAEvC,yDAAyD;AACzD,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,CAAC;AAExC;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAG,CAAC,CAAC;AAW5C,qFAAqF;AACrF,2DAA2D;AAC3D,kFAAkF;AAClF,wFAAwF;AACxF,uDAAuD","sourcesContent":["import { NormalizeUUID } from '@memberjunction/global';\nimport {\n BuildConversationTimeline,\n type RealtimeTimelineSourceDetail\n} from './realtime-session-timeline';\n\nexport function SelectLatestTimelinePage<T extends RealtimeTimelineSourceDetail>(\n chronologicalDetails: readonly T[],\n pageSize: number\n): { Page: T[]; OldestIncluded: T | null } {\n const timeline = BuildConversationTimeline(chronologicalDetails);\n const pageItems = timeline.slice(Math.max(0, timeline.length - pageSize));\n\n // Message items match by identity (the grouping pass passes the row through\n // untouched); session items by normalized id, so every folded row comes along.\n const includedMessages = new Set<T>();\n const includedSessionKeys = new Set<string>();\n for (const item of pageItems) {\n if (item.Kind === 'message') {\n includedMessages.add(item.Detail);\n } else {\n includedSessionKeys.add(NormalizeUUID(item.Group.SessionID));\n }\n }\n\n // Filter the ORIGINAL row set rather than walking the page items, so the result keeps\n // chronological order and picks up session rows interleaved after the card's position.\n const page = chronologicalDetails.filter(detail => {\n const sessionId = trimmedSessionId(detail.AgentSessionID);\n return sessionId === null\n ? includedMessages.has(detail)\n : includedSessionKeys.has(NormalizeUUID(sessionId));\n });\n\n return { Page: page, OldestIncluded: page.length > 0 ? page[0] : null };\n}\n\n\n\n// NOTE: the plan also called for a `NeedsSessionExpansion` helper here, shared between the\n// store and the engine. It is not exported, because there is no such shared caller: deciding\n// whether a fetched page landed mid-session is something only the fetcher can act on, and the\n// fetcher is `ConversationEngine.expandOldestSession` in `@memberjunction/core-entities`.\n// That package cannot import from this one — the dependency runs Angular → core-entities and\n// must never run the other way — so a helper here would have been exported, tested, and\n// called by nothing. The engine makes the same `trim()` check inline against the row it just\n// read. `trimmedSessionId` below keeps that definition honest on THIS side of the boundary,\n// where `SelectLatestTimelinePage` does need it.\n\n/**\n * Trims the stamped session id; empty/whitespace stamps count as unstamped — mirrors the\n * grouping pass, so a row this module treats as a normal message is one the timeline\n * treats as a normal message too.\n */\nfunction trimmedSessionId(raw: string | null | undefined): string | null {\n const trimmed = raw?.trim() ?? '';\n return trimmed.length > 0 ? trimmed : null;\n}\n\n\n/** Default number of *timeline items* shown on first paint and per older page. */\nexport const DEFAULT_TRANSCRIPT_PAGE_SIZE = 10;\n\n/** Raw-row over-read so session collapse still fills a page of timeline items. */\nexport const DEFAULT_RAW_OVERREAD = DEFAULT_TRANSCRIPT_PAGE_SIZE * 3;\n\n/**\n * How many times a short page may re-fetch with a wider over-read before settling.\n *\n * The over-read exists because raw rows are not display items — a realtime session folds\n * many rows into ONE timeline card, so `3 × pageSize` rows can yield two or three items\n * instead of ten. Growing the read is what keeps a page a page.\n *\n * Bounded, because the pathological case is a conversation that is almost entirely one long\n * session: growing without limit there would walk the whole table, which is the exact cost\n * windowing exists to avoid. Three attempts reaches 12 × pageSize, past which a short page is\n * the honest answer and the sentinel simply fires again.\n */\nexport const MAX_OVERREAD_ATTEMPTS = 3;\n\n/** Multiplier applied to the over-read on each retry. */\nexport const OVERREAD_GROWTH_FACTOR = 2;\n\n/**\n * Extra pages a tail refresh may read to bridge back to the rows it already holds.\n *\n * A refresh reads the NEWEST page. If the conversation grew by more than a page since the\n * last read — a long agent burst, a tab left open — that page starts above the loaded tail\n * and the rows in between belong to neither. Merging the two anyway produces a set that\n * sorts into order and looks contiguous while silently missing its middle, which is the\n * worst possible outcome: nothing about the transcript says anything is absent.\n *\n * So the refresh pages down until it reaches known rows. Bounded, because \"the tail ran away\n * from us\" has no upper limit and chasing it without one is the unbounded read this whole\n * feature exists to remove. Past the budget the refresh drops the disconnected older rows\n * instead and lets the sentinel offer them back — a visible gap the reader can close, rather\n * than an invisible one they cannot.\n */\nexport const MAX_REFRESH_BACKFILL_PAGES = 3;\n\nexport interface ConversationDetailWindowCursor {\n /** Exclusive upper bound for the next older fetch. Null only when the window is empty. */\n OldestSequence: number | null;\n /** Inclusive high-water mark of the loaded tail. Used to detect live appends. */\n NewestSequence: number | null;\n /** True when at least one older raw row exists that is not in LoadedDetails. */\n HasMoreAbove: boolean;\n}\n\n// NOTE: the loaded-window and fetch-result shapes the plan sketched here live on the\n// consumer instead — `ConversationDetailWindowSnapshot` in\n// `../services/conversation-detail-window.store`, and `DetailWindowLoadResult` in\n// `@memberjunction/core-entities`. Declaring unused duplicates here would give a future\n// contributor two plausible contracts to pick between.\n\n"]}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { RealtimeTimelineSourceDetail } from './realtime-session-timeline';
|
|
2
|
+
/**
|
|
3
|
+
* Periods the transcript's date navigator offers. Kept as a union rather than a string so a
|
|
4
|
+
* new dropdown entry cannot silently fall through to "no cutoff".
|
|
5
|
+
*/
|
|
6
|
+
export type DateJumpPeriod = 'today' | 'yesterday' | 'last-week' | 'last-month';
|
|
7
|
+
/**
|
|
8
|
+
* How far back a jump had to page before it stopped.
|
|
9
|
+
*
|
|
10
|
+
* The plan's requirement is that a jump never silently no-ops, so every terminal state here is
|
|
11
|
+
* something the caller can report: `reached` scrolled to a real message, `oldest` ran out of
|
|
12
|
+
* history, `capped` hit {@link DATE_JUMP_MAX_PAGES}, `empty` had nothing loaded at all.
|
|
13
|
+
*/
|
|
14
|
+
export type DateJumpOutcome = 'reached' | 'oldest' | 'capped' | 'empty';
|
|
15
|
+
/**
|
|
16
|
+
* Ceiling on older-page loads for one jump.
|
|
17
|
+
*
|
|
18
|
+
* A jump into a very old conversation would otherwise page the entire history back — the
|
|
19
|
+
* exact unbounded read windowing exists to prevent. On a miss the caller lands the user at
|
|
20
|
+
* the oldest loaded message instead, which is where paging left them anyway.
|
|
21
|
+
*/
|
|
22
|
+
export declare const DATE_JUMP_MAX_PAGES = 50;
|
|
23
|
+
/**
|
|
24
|
+
* Inclusive lower bound for a period — the oldest instant a message may carry and still count
|
|
25
|
+
* as "in" that period. `today` is midnight today; the rest reach further back.
|
|
26
|
+
*/
|
|
27
|
+
export declare function ResolveDateJumpCutoff(period: DateJumpPeriod, now: Date): Date;
|
|
28
|
+
export interface DateJumpTarget<T extends RealtimeTimelineSourceDetail> {
|
|
29
|
+
/** Oldest loaded detail at or after the cutoff — the message to scroll to. */
|
|
30
|
+
Detail: T | null;
|
|
31
|
+
/**
|
|
32
|
+
* True when the loaded set cannot yet answer the question: everything loaded is at or
|
|
33
|
+
* after the cutoff, so an older page might contain a still-earlier message in-period.
|
|
34
|
+
*/
|
|
35
|
+
NeedsOlder: boolean;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Resolves which loaded message a date jump should land on.
|
|
39
|
+
*
|
|
40
|
+
* The target is the OLDEST message at or after the cutoff — jumping to "last week" means the
|
|
41
|
+
* start of last week's conversation, not its end.
|
|
42
|
+
*
|
|
43
|
+
* `NeedsOlder` is the windowing-aware part. If every loaded message is in-period, the true
|
|
44
|
+
* start of the period may still be below the loaded window, so the caller pages and asks
|
|
45
|
+
* again. If any loaded message predates the cutoff, the boundary is already inside the loaded
|
|
46
|
+
* set and no amount of paging would improve the answer.
|
|
47
|
+
*/
|
|
48
|
+
export declare function ResolveDateJumpTarget<T extends RealtimeTimelineSourceDetail>(chronologicalDetails: readonly T[], period: DateJumpPeriod, now: Date): DateJumpTarget<T>;
|
|
49
|
+
/** Human-readable result, for the caller to surface instead of failing silently. */
|
|
50
|
+
export declare function DescribeDateJumpOutcome(outcome: DateJumpOutcome, period: DateJumpPeriod): string;
|
|
51
|
+
/**
|
|
52
|
+
* Final outcome for a date jump.
|
|
53
|
+
*
|
|
54
|
+
* The paging loop and the scroll answer DIFFERENT questions: the loop knows only whether it
|
|
55
|
+
* ran out of history (or pages) to load, while the scroll knows whether an in-period message
|
|
56
|
+
* was actually found. Letting the loop's verdict win was a bug — a conversation whose oldest
|
|
57
|
+
* message is itself in-period never sees a pre-cutoff row, so the loop reports "ran out"
|
|
58
|
+
* even though the jump landed on a perfectly good target and the user was told no such
|
|
59
|
+
* messages exist.
|
|
60
|
+
*
|
|
61
|
+
* So: a successful scroll always wins. The cap only surfaces when the scroll did NOT reach,
|
|
62
|
+
* because that is the only case where "we stopped early" explains the miss.
|
|
63
|
+
*/
|
|
64
|
+
export declare function CombineDateJumpOutcome(scrollOutcome: DateJumpOutcome, hitPageCap: boolean): DateJumpOutcome;
|
|
65
|
+
//# sourceMappingURL=date-jump.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"date-jump.d.ts","sourceRoot":"","sources":["../../../src/lib/utils/date-jump.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,6BAA6B,CAAC;AAEhF;;;GAGG;AACH,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG,WAAW,GAAG,WAAW,GAAG,YAAY,CAAC;AAEhF;;;;;;GAMG;AACH,MAAM,MAAM,eAAe,GAAG,SAAS,GAAG,QAAQ,GAAG,QAAQ,GAAG,OAAO,CAAC;AAExE;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,KAAK,CAAC;AAStC;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,cAAc,EAAE,GAAG,EAAE,IAAI,GAAG,IAAI,CAW7E;AAYD,MAAM,WAAW,cAAc,CAAC,CAAC,SAAS,4BAA4B;IAClE,8EAA8E;IAC9E,MAAM,EAAE,CAAC,GAAG,IAAI,CAAC;IACjB;;;OAGG;IACH,UAAU,EAAE,OAAO,CAAC;CACvB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CAAC,CAAC,SAAS,4BAA4B,EACxE,oBAAoB,EAAE,SAAS,CAAC,EAAE,EAClC,MAAM,EAAE,cAAc,EACtB,GAAG,EAAE,IAAI,GACV,cAAc,CAAC,CAAC,CAAC,CAqBnB;AAED,oFAAoF;AACpF,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,eAAe,EAAE,MAAM,EAAE,cAAc,GAAG,MAAM,CAchG;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,sBAAsB,CAClC,aAAa,EAAE,eAAe,EAC9B,UAAU,EAAE,OAAO,GACpB,eAAe,CAKjB"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ceiling on older-page loads for one jump.
|
|
3
|
+
*
|
|
4
|
+
* A jump into a very old conversation would otherwise page the entire history back — the
|
|
5
|
+
* exact unbounded read windowing exists to prevent. On a miss the caller lands the user at
|
|
6
|
+
* the oldest loaded message instead, which is where paging left them anyway.
|
|
7
|
+
*/
|
|
8
|
+
export const DATE_JUMP_MAX_PAGES = 50;
|
|
9
|
+
/** Local midnight `daysAgo` days before `now`. */
|
|
10
|
+
function startOfDayBefore(now, daysAgo) {
|
|
11
|
+
const d = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
12
|
+
d.setDate(d.getDate() - daysAgo);
|
|
13
|
+
return d;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Inclusive lower bound for a period — the oldest instant a message may carry and still count
|
|
17
|
+
* as "in" that period. `today` is midnight today; the rest reach further back.
|
|
18
|
+
*/
|
|
19
|
+
export function ResolveDateJumpCutoff(period, now) {
|
|
20
|
+
switch (period) {
|
|
21
|
+
case 'today':
|
|
22
|
+
return startOfDayBefore(now, 0);
|
|
23
|
+
case 'yesterday':
|
|
24
|
+
return startOfDayBefore(now, 1);
|
|
25
|
+
case 'last-week':
|
|
26
|
+
return startOfDayBefore(now, 7);
|
|
27
|
+
case 'last-month':
|
|
28
|
+
return startOfDayBefore(now, 30);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** The timestamp a jump sorts on. Falsy/invalid stamps are skipped rather than treated as epoch. */
|
|
32
|
+
function detailTime(detail) {
|
|
33
|
+
const raw = detail.__mj_CreatedAt;
|
|
34
|
+
if (!raw) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
const ms = raw instanceof Date ? raw.getTime() : new Date(raw).getTime();
|
|
38
|
+
return Number.isNaN(ms) ? null : ms;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Resolves which loaded message a date jump should land on.
|
|
42
|
+
*
|
|
43
|
+
* The target is the OLDEST message at or after the cutoff — jumping to "last week" means the
|
|
44
|
+
* start of last week's conversation, not its end.
|
|
45
|
+
*
|
|
46
|
+
* `NeedsOlder` is the windowing-aware part. If every loaded message is in-period, the true
|
|
47
|
+
* start of the period may still be below the loaded window, so the caller pages and asks
|
|
48
|
+
* again. If any loaded message predates the cutoff, the boundary is already inside the loaded
|
|
49
|
+
* set and no amount of paging would improve the answer.
|
|
50
|
+
*/
|
|
51
|
+
export function ResolveDateJumpTarget(chronologicalDetails, period, now) {
|
|
52
|
+
const cutoff = ResolveDateJumpCutoff(period, now).getTime();
|
|
53
|
+
let target = null;
|
|
54
|
+
let sawOlderThanCutoff = false;
|
|
55
|
+
for (const detail of chronologicalDetails) {
|
|
56
|
+
const ms = detailTime(detail);
|
|
57
|
+
if (ms === null) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (ms < cutoff) {
|
|
61
|
+
sawOlderThanCutoff = true;
|
|
62
|
+
}
|
|
63
|
+
else if (target === null) {
|
|
64
|
+
target = detail;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// Nothing in-period AND nothing older loaded => the whole window post-dates the cutoff
|
|
68
|
+
// only if we also found no target; that still means paging could help.
|
|
69
|
+
return { Detail: target, NeedsOlder: !sawOlderThanCutoff };
|
|
70
|
+
}
|
|
71
|
+
/** Human-readable result, for the caller to surface instead of failing silently. */
|
|
72
|
+
export function DescribeDateJumpOutcome(outcome, period) {
|
|
73
|
+
const label = period === 'last-week' ? 'last week'
|
|
74
|
+
: period === 'last-month' ? 'last month'
|
|
75
|
+
: period;
|
|
76
|
+
switch (outcome) {
|
|
77
|
+
case 'reached':
|
|
78
|
+
return `Jumped to ${label}.`;
|
|
79
|
+
case 'oldest':
|
|
80
|
+
return `No messages from ${label} — showing the start of the conversation.`;
|
|
81
|
+
case 'capped':
|
|
82
|
+
return `Stopped after loading ${DATE_JUMP_MAX_PAGES} pages — showing the oldest loaded message.`;
|
|
83
|
+
case 'empty':
|
|
84
|
+
return 'Nothing loaded to jump to.';
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Final outcome for a date jump.
|
|
89
|
+
*
|
|
90
|
+
* The paging loop and the scroll answer DIFFERENT questions: the loop knows only whether it
|
|
91
|
+
* ran out of history (or pages) to load, while the scroll knows whether an in-period message
|
|
92
|
+
* was actually found. Letting the loop's verdict win was a bug — a conversation whose oldest
|
|
93
|
+
* message is itself in-period never sees a pre-cutoff row, so the loop reports "ran out"
|
|
94
|
+
* even though the jump landed on a perfectly good target and the user was told no such
|
|
95
|
+
* messages exist.
|
|
96
|
+
*
|
|
97
|
+
* So: a successful scroll always wins. The cap only surfaces when the scroll did NOT reach,
|
|
98
|
+
* because that is the only case where "we stopped early" explains the miss.
|
|
99
|
+
*/
|
|
100
|
+
export function CombineDateJumpOutcome(scrollOutcome, hitPageCap) {
|
|
101
|
+
if (scrollOutcome === 'reached') {
|
|
102
|
+
return 'reached';
|
|
103
|
+
}
|
|
104
|
+
return hitPageCap ? 'capped' : scrollOutcome;
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=date-jump.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"date-jump.js","sourceRoot":"","sources":["../../../src/lib/utils/date-jump.ts"],"names":[],"mappings":"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","sourcesContent":["import type { RealtimeTimelineSourceDetail } from './realtime-session-timeline';\n\n/**\n * Periods the transcript's date navigator offers. Kept as a union rather than a string so a\n * new dropdown entry cannot silently fall through to \"no cutoff\".\n */\nexport type DateJumpPeriod = 'today' | 'yesterday' | 'last-week' | 'last-month';\n\n/**\n * How far back a jump had to page before it stopped.\n *\n * The plan's requirement is that a jump never silently no-ops, so every terminal state here is\n * something the caller can report: `reached` scrolled to a real message, `oldest` ran out of\n * history, `capped` hit {@link DATE_JUMP_MAX_PAGES}, `empty` had nothing loaded at all.\n */\nexport type DateJumpOutcome = 'reached' | 'oldest' | 'capped' | 'empty';\n\n/**\n * Ceiling on older-page loads for one jump.\n *\n * A jump into a very old conversation would otherwise page the entire history back — the\n * exact unbounded read windowing exists to prevent. On a miss the caller lands the user at\n * the oldest loaded message instead, which is where paging left them anyway.\n */\nexport const DATE_JUMP_MAX_PAGES = 50;\n\n/** Local midnight `daysAgo` days before `now`. */\nfunction startOfDayBefore(now: Date, daysAgo: number): Date {\n const d = new Date(now.getFullYear(), now.getMonth(), now.getDate());\n d.setDate(d.getDate() - daysAgo);\n return d;\n}\n\n/**\n * Inclusive lower bound for a period — the oldest instant a message may carry and still count\n * as \"in\" that period. `today` is midnight today; the rest reach further back.\n */\nexport function ResolveDateJumpCutoff(period: DateJumpPeriod, now: Date): Date {\n switch (period) {\n case 'today':\n return startOfDayBefore(now, 0);\n case 'yesterday':\n return startOfDayBefore(now, 1);\n case 'last-week':\n return startOfDayBefore(now, 7);\n case 'last-month':\n return startOfDayBefore(now, 30);\n }\n}\n\n/** The timestamp a jump sorts on. Falsy/invalid stamps are skipped rather than treated as epoch. */\nfunction detailTime(detail: RealtimeTimelineSourceDetail): number | null {\n const raw = (detail as { __mj_CreatedAt?: Date | string | null }).__mj_CreatedAt;\n if (!raw) {\n return null;\n }\n const ms = raw instanceof Date ? raw.getTime() : new Date(raw).getTime();\n return Number.isNaN(ms) ? null : ms;\n}\n\nexport interface DateJumpTarget<T extends RealtimeTimelineSourceDetail> {\n /** Oldest loaded detail at or after the cutoff — the message to scroll to. */\n Detail: T | null;\n /**\n * True when the loaded set cannot yet answer the question: everything loaded is at or\n * after the cutoff, so an older page might contain a still-earlier message in-period.\n */\n NeedsOlder: boolean;\n}\n\n/**\n * Resolves which loaded message a date jump should land on.\n *\n * The target is the OLDEST message at or after the cutoff — jumping to \"last week\" means the\n * start of last week's conversation, not its end.\n *\n * `NeedsOlder` is the windowing-aware part. If every loaded message is in-period, the true\n * start of the period may still be below the loaded window, so the caller pages and asks\n * again. If any loaded message predates the cutoff, the boundary is already inside the loaded\n * set and no amount of paging would improve the answer.\n */\nexport function ResolveDateJumpTarget<T extends RealtimeTimelineSourceDetail>(\n chronologicalDetails: readonly T[],\n period: DateJumpPeriod,\n now: Date\n): DateJumpTarget<T> {\n const cutoff = ResolveDateJumpCutoff(period, now).getTime();\n\n let target: T | null = null;\n let sawOlderThanCutoff = false;\n\n for (const detail of chronologicalDetails) {\n const ms = detailTime(detail);\n if (ms === null) {\n continue;\n }\n if (ms < cutoff) {\n sawOlderThanCutoff = true;\n } else if (target === null) {\n target = detail;\n }\n }\n\n // Nothing in-period AND nothing older loaded => the whole window post-dates the cutoff\n // only if we also found no target; that still means paging could help.\n return { Detail: target, NeedsOlder: !sawOlderThanCutoff };\n}\n\n/** Human-readable result, for the caller to surface instead of failing silently. */\nexport function DescribeDateJumpOutcome(outcome: DateJumpOutcome, period: DateJumpPeriod): string {\n const label = period === 'last-week' ? 'last week'\n : period === 'last-month' ? 'last month'\n : period;\n switch (outcome) {\n case 'reached':\n return `Jumped to ${label}.`;\n case 'oldest':\n return `No messages from ${label} — showing the start of the conversation.`;\n case 'capped':\n return `Stopped after loading ${DATE_JUMP_MAX_PAGES} pages — showing the oldest loaded message.`;\n case 'empty':\n return 'Nothing loaded to jump to.';\n }\n}\n\n/**\n * Final outcome for a date jump.\n *\n * The paging loop and the scroll answer DIFFERENT questions: the loop knows only whether it\n * ran out of history (or pages) to load, while the scroll knows whether an in-period message\n * was actually found. Letting the loop's verdict win was a bug — a conversation whose oldest\n * message is itself in-period never sees a pre-cutoff row, so the loop reports \"ran out\"\n * even though the jump landed on a perfectly good target and the user was told no such\n * messages exist.\n *\n * So: a successful scroll always wins. The cap only surfaces when the scroll did NOT reach,\n * because that is the only case where \"we stopped early\" explains the miss.\n */\nexport function CombineDateJumpOutcome(\n scrollOutcome: DateJumpOutcome,\n hitPageCap: boolean\n): DateJumpOutcome {\n if (scrollOutcome === 'reached') {\n return 'reached';\n }\n return hitPageCap ? 'capped' : scrollOutcome;\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@memberjunction/ng-conversations",
|
|
3
|
-
"version": "6.1.0-edge.
|
|
3
|
+
"version": "6.1.0-edge.3",
|
|
4
4
|
"description": "MemberJunction: Conversation, Collection, and Artifact management components for any Angular application",
|
|
5
5
|
"main": "./dist/public-api.js",
|
|
6
6
|
"typings": "./dist/public-api.d.ts",
|
|
@@ -9,13 +9,13 @@
|
|
|
9
9
|
],
|
|
10
10
|
"keywords": [],
|
|
11
11
|
"author": "",
|
|
12
|
-
"license": "
|
|
12
|
+
"license": "BUSL-1.1",
|
|
13
13
|
"devDependencies": {
|
|
14
14
|
"@angular/compiler": "21.1.3",
|
|
15
15
|
"@angular/compiler-cli": "21.1.3",
|
|
16
16
|
"sass": "^1.97.3",
|
|
17
17
|
"vitest": "^4.0.18",
|
|
18
|
-
"@memberjunction/ng-test-utils": "6.1.0-edge.
|
|
18
|
+
"@memberjunction/ng-test-utils": "6.1.0-edge.3"
|
|
19
19
|
},
|
|
20
20
|
"peerDependencies": {
|
|
21
21
|
"@angular/common": "^21.1.3",
|
|
@@ -26,35 +26,35 @@
|
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@angular/animations": "21.1.3",
|
|
28
28
|
"@angular/cdk": "21.1.3",
|
|
29
|
-
"@memberjunction/ai": "6.1.0-edge.
|
|
30
|
-
"@memberjunction/ai-agent-client": "6.1.0-edge.
|
|
31
|
-
"@memberjunction/ai-core-plus": "6.1.0-edge.
|
|
32
|
-
"@memberjunction/ai-engine-base": "6.1.0-edge.
|
|
33
|
-
"@memberjunction/ai-realtime-client": "6.1.0-edge.
|
|
34
|
-
"@memberjunction/conversations-runtime": "6.1.0-edge.
|
|
35
|
-
"@memberjunction/core": "6.1.0-edge.
|
|
36
|
-
"@memberjunction/core-entities": "6.1.0-edge.
|
|
37
|
-
"@memberjunction/global": "6.1.0-edge.
|
|
38
|
-
"@memberjunction/graphql-dataprovider": "6.1.0-edge.
|
|
39
|
-
"@memberjunction/interactive-component-types": "6.1.0-edge.
|
|
40
|
-
"@memberjunction/ng-agent-client": "6.1.0-edge.
|
|
41
|
-
"@memberjunction/ng-artifacts": "6.1.0-edge.
|
|
42
|
-
"@memberjunction/ng-base-types": "6.1.0-edge.
|
|
43
|
-
"@memberjunction/ng-code-editor": "6.1.0-edge.
|
|
44
|
-
"@memberjunction/ng-composer": "6.1.0-edge.
|
|
45
|
-
"@memberjunction/ng-container-directives": "6.1.0-edge.
|
|
46
|
-
"@memberjunction/ng-forms": "6.1.0-edge.
|
|
47
|
-
"@memberjunction/ng-markdown": "6.1.0-edge.
|
|
48
|
-
"@memberjunction/ng-media-player": "6.1.0-edge.
|
|
49
|
-
"@memberjunction/ng-notifications": "6.1.0-edge.
|
|
50
|
-
"@memberjunction/ng-resource-permissions": "6.1.0-edge.
|
|
51
|
-
"@memberjunction/ng-shared-generic": "6.1.0-edge.
|
|
52
|
-
"@memberjunction/ng-task-graph-editor": "6.1.0-edge.
|
|
53
|
-
"@memberjunction/ng-tasks": "6.1.0-edge.
|
|
54
|
-
"@memberjunction/ng-testing": "6.1.0-edge.
|
|
55
|
-
"@memberjunction/ng-ui-components": "6.1.0-edge.
|
|
56
|
-
"@memberjunction/ng-user-routines": "6.1.0-edge.
|
|
57
|
-
"@memberjunction/ng-whiteboard": "6.1.0-edge.
|
|
29
|
+
"@memberjunction/ai": "6.1.0-edge.3",
|
|
30
|
+
"@memberjunction/ai-agent-client": "6.1.0-edge.3",
|
|
31
|
+
"@memberjunction/ai-core-plus": "6.1.0-edge.3",
|
|
32
|
+
"@memberjunction/ai-engine-base": "6.1.0-edge.3",
|
|
33
|
+
"@memberjunction/ai-realtime-client": "6.1.0-edge.3",
|
|
34
|
+
"@memberjunction/conversations-runtime": "6.1.0-edge.3",
|
|
35
|
+
"@memberjunction/core": "6.1.0-edge.3",
|
|
36
|
+
"@memberjunction/core-entities": "6.1.0-edge.3",
|
|
37
|
+
"@memberjunction/global": "6.1.0-edge.3",
|
|
38
|
+
"@memberjunction/graphql-dataprovider": "6.1.0-edge.3",
|
|
39
|
+
"@memberjunction/interactive-component-types": "6.1.0-edge.3",
|
|
40
|
+
"@memberjunction/ng-agent-client": "6.1.0-edge.3",
|
|
41
|
+
"@memberjunction/ng-artifacts": "6.1.0-edge.3",
|
|
42
|
+
"@memberjunction/ng-base-types": "6.1.0-edge.3",
|
|
43
|
+
"@memberjunction/ng-code-editor": "6.1.0-edge.3",
|
|
44
|
+
"@memberjunction/ng-composer": "6.1.0-edge.3",
|
|
45
|
+
"@memberjunction/ng-container-directives": "6.1.0-edge.3",
|
|
46
|
+
"@memberjunction/ng-forms": "6.1.0-edge.3",
|
|
47
|
+
"@memberjunction/ng-markdown": "6.1.0-edge.3",
|
|
48
|
+
"@memberjunction/ng-media-player": "6.1.0-edge.3",
|
|
49
|
+
"@memberjunction/ng-notifications": "6.1.0-edge.3",
|
|
50
|
+
"@memberjunction/ng-resource-permissions": "6.1.0-edge.3",
|
|
51
|
+
"@memberjunction/ng-shared-generic": "6.1.0-edge.3",
|
|
52
|
+
"@memberjunction/ng-task-graph-editor": "6.1.0-edge.3",
|
|
53
|
+
"@memberjunction/ng-tasks": "6.1.0-edge.3",
|
|
54
|
+
"@memberjunction/ng-testing": "6.1.0-edge.3",
|
|
55
|
+
"@memberjunction/ng-ui-components": "6.1.0-edge.3",
|
|
56
|
+
"@memberjunction/ng-user-routines": "6.1.0-edge.3",
|
|
57
|
+
"@memberjunction/ng-whiteboard": "6.1.0-edge.3",
|
|
58
58
|
"angular-split": "^20.0.0",
|
|
59
59
|
"rxjs": "^7.8.2",
|
|
60
60
|
"tslib": "^2.8.1"
|