@manta-eu/react 0.4.0 → 0.5.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 +1 -0
- package/dist/components/chat-view/Chat.d.ts +27 -1
- package/dist/components/chat-view/Chat.d.ts.map +1 -1
- package/dist/components/chat-view/Chat.js +76 -18
- package/dist/components/chat-view/Chat.js.map +1 -1
- package/dist/components/embedded-chat/index.d.ts +1 -1
- package/dist/components/embedded-chat/index.d.ts.map +1 -1
- package/dist/components/embedded-chat/index.js +1 -1
- package/dist/components/embedded-chat/index.js.map +1 -1
- package/dist/components/layouts/Sidebar.d.ts.map +1 -1
- package/dist/components/layouts/Sidebar.js +4 -2
- package/dist/components/layouts/Sidebar.js.map +1 -1
- package/dist/components/planning/ActioningView.d.ts.map +1 -1
- package/dist/components/planning/ActioningView.js +1 -1
- package/dist/components/planning/ActioningView.js.map +1 -1
- package/dist/components/planning/ExecutionTopBar.d.ts +1 -0
- package/dist/components/planning/ExecutionTopBar.d.ts.map +1 -1
- package/dist/components/planning/ExecutionTopBar.js +3 -2
- package/dist/components/planning/ExecutionTopBar.js.map +1 -1
- package/dist/components/planning/plan-detail-utils.d.ts +1 -0
- package/dist/components/planning/plan-detail-utils.d.ts.map +1 -1
- package/dist/components/planning/plan-detail-utils.js +5 -2
- package/dist/components/planning/plan-detail-utils.js.map +1 -1
- package/dist/components/sidebar/nav-user.js +1 -1
- package/dist/components/sidebar/nav-user.js.map +1 -1
- package/dist/components/tool-invocation-card/ToolOutputRenderer.d.ts.map +1 -1
- package/dist/components/tool-invocation-card/ToolOutputRenderer.js +5 -2
- package/dist/components/tool-invocation-card/ToolOutputRenderer.js.map +1 -1
- package/dist/explain-fact/rule-presentation.d.ts.map +1 -1
- package/dist/explain-fact/rule-presentation.js +1 -6
- package/dist/explain-fact/rule-presentation.js.map +1 -1
- package/dist/explain-fact/tree-model.d.ts +2 -1
- package/dist/explain-fact/tree-model.d.ts.map +1 -1
- package/dist/explain-fact/tree-model.js +17 -2
- package/dist/explain-fact/tree-model.js.map +1 -1
- package/dist/hooks/use-connection-problems.d.ts.map +1 -1
- package/dist/hooks/use-manual-actions.js +1 -1
- package/dist/hooks/use-manual-actions.js.map +1 -1
- package/dist/hooks/useAuth.d.ts +1 -0
- package/dist/hooks/useAuth.d.ts.map +1 -1
- package/dist/hooks/useAuth.js +2 -1
- package/dist/hooks/useAuth.js.map +1 -1
- package/dist/hooks/useValidateActionPlan.d.ts.map +1 -1
- package/dist/lib/chat-types.d.ts +17 -0
- package/dist/lib/chat-types.d.ts.map +1 -1
- package/dist/lib/chat-types.js.map +1 -1
- package/dist/lib/datastore-route-table.d.ts +1 -0
- package/dist/lib/datastore-route-table.d.ts.map +1 -1
- package/dist/lib/datastore-route-table.js +1 -0
- package/dist/lib/datastore-route-table.js.map +1 -1
- package/dist/providers/AuthContext.d.ts +1 -0
- package/dist/providers/AuthContext.d.ts.map +1 -1
- package/dist/providers/AuthContext.js.map +1 -1
- package/dist/providers/ChatManagerProvider.d.ts +7 -0
- package/dist/providers/ChatManagerProvider.d.ts.map +1 -1
- package/dist/providers/ChatManagerProvider.js +129 -16
- package/dist/providers/ChatManagerProvider.js.map +1 -1
- package/dist/providers/chat-connection-context.d.ts +4 -1
- package/dist/providers/chat-connection-context.d.ts.map +1 -1
- package/dist/providers/chat-connection-context.js.map +1 -1
- package/dist/providers/chat-connection-lifecycle.d.ts +1 -0
- package/dist/providers/chat-connection-lifecycle.d.ts.map +1 -1
- package/dist/providers/chat-connection-lifecycle.js.map +1 -1
- package/dist/providers/chat-socket-auth.d.ts +3 -1
- package/dist/providers/chat-socket-auth.d.ts.map +1 -1
- package/dist/providers/chat-socket-auth.js +27 -9
- package/dist/providers/chat-socket-auth.js.map +1 -1
- package/dist/providers/chat-startup-timing.d.ts +14 -0
- package/dist/providers/chat-startup-timing.d.ts.map +1 -0
- package/dist/providers/chat-startup-timing.js +72 -0
- package/dist/providers/chat-startup-timing.js.map +1 -0
- package/dist/routes/plans/components/ActionPlansTableColumns.d.ts.map +1 -1
- package/dist/routes/plans/components/ActionPlansTableColumns.js +3 -3
- package/dist/routes/plans/components/ActionPlansTableColumns.js.map +1 -1
- package/package.json +7 -7
- package/src/components/chat-view/Chat.tsx +122 -20
- package/src/components/embedded-chat/index.tsx +9 -2
- package/src/components/layouts/Sidebar.tsx +3 -2
- package/src/components/planning/ActioningView.tsx +1 -0
- package/src/components/planning/ExecutionTopBar.tsx +14 -2
- package/src/components/planning/plan-detail-utils.tsx +6 -2
- package/src/components/sidebar/nav-user.tsx +1 -1
- package/src/components/tool-invocation-card/ToolOutputRenderer.tsx +5 -2
- package/src/explain-fact/rule-presentation.ts +1 -6
- package/src/explain-fact/tree-model.ts +23 -1
- package/src/hooks/use-manual-actions.ts +1 -1
- package/src/hooks/useAuth.ts +3 -1
- package/src/lib/chat-types.ts +18 -0
- package/src/lib/datastore-route-table.ts +2 -0
- package/src/providers/AuthContext.ts +1 -0
- package/src/providers/ChatManagerProvider.tsx +166 -15
- package/src/providers/chat-connection-context.ts +4 -0
- package/src/providers/chat-connection-lifecycle.ts +1 -0
- package/src/providers/chat-socket-auth.ts +31 -12
- package/src/providers/chat-startup-timing.ts +89 -0
- package/src/routes/plans/components/ActionPlansTableColumns.tsx +3 -2
|
@@ -6,6 +6,7 @@ import { cn } from "#lib/utils";
|
|
|
6
6
|
type ExecutionTopBarProps = {
|
|
7
7
|
planName: string;
|
|
8
8
|
status: ExecutionStatus;
|
|
9
|
+
pendingManualActionCount: number;
|
|
9
10
|
workflowId: string | null;
|
|
10
11
|
triggerError: string | null;
|
|
11
12
|
onExecute?: (dryRun: boolean) => void;
|
|
@@ -38,8 +39,19 @@ const statusConfig: Record<
|
|
|
38
39
|
};
|
|
39
40
|
|
|
40
41
|
export function ExecutionTopBar(props: ExecutionTopBarProps) {
|
|
41
|
-
const {
|
|
42
|
-
|
|
42
|
+
const {
|
|
43
|
+
planName,
|
|
44
|
+
status,
|
|
45
|
+
pendingManualActionCount,
|
|
46
|
+
onExecute,
|
|
47
|
+
onRerun,
|
|
48
|
+
isExecuteDialogOpen,
|
|
49
|
+
onExecuteDialogOpenChange,
|
|
50
|
+
isDryRun,
|
|
51
|
+
} = props;
|
|
52
|
+
|
|
53
|
+
const shownStatus = status === "completed" && !isDryRun && pendingManualActionCount > 0 ? "needs-approval" : status;
|
|
54
|
+
const config = statusConfig[shownStatus];
|
|
43
55
|
const isTerminal = status === "completed" || status === "failed";
|
|
44
56
|
const displayLabel = (isDryRun && config.dryRunLabel) || config.label;
|
|
45
57
|
|
|
@@ -77,10 +77,14 @@ export function statusLabel(status: ActionPlanStatus): string {
|
|
|
77
77
|
.exhaustive();
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
export function needsApproval(state: ActionPlanStatus, pendingApprovalCount: number): boolean {
|
|
81
|
+
return pendingApprovalCount > 0 && (state === GQLActionPlanState.Running || state === GQLActionPlanState.Completed);
|
|
82
|
+
}
|
|
83
|
+
|
|
80
84
|
export function planStateVariant(state: ActionPlanStatus, pendingApprovalCount = 0): BadgeVariant {
|
|
81
|
-
return match({ state,
|
|
85
|
+
return match({ state, needsApproval: needsApproval(state, pendingApprovalCount) })
|
|
82
86
|
.returnType<BadgeVariant>()
|
|
83
|
-
.with({
|
|
87
|
+
.with({ needsApproval: true }, () => "warning")
|
|
84
88
|
.with({ state: GQLActionPlanState.Running }, () => "info")
|
|
85
89
|
.with({ state: GQLActionPlanState.Completed }, () => "success")
|
|
86
90
|
.with({ state: GQLActionPlanState.Failed }, () => "destructive")
|
|
@@ -83,7 +83,7 @@ export function NavUser(args: NavUserArgs) {
|
|
|
83
83
|
{pickerSlot}
|
|
84
84
|
{topSlot}
|
|
85
85
|
{(pickerSlot || topSlot) && <DropdownSeparator />}
|
|
86
|
-
<DropdownGroup className="p-2">
|
|
86
|
+
<DropdownGroup className={children ? "px-2 pt-2" : "p-2"}>
|
|
87
87
|
<DropdownItem
|
|
88
88
|
className="p-1.5 flex flex-row items-center gap-1.5 typography-caption h-11"
|
|
89
89
|
onClick={onNavigateToAccount}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { withChatModeQueryParam } from "@manta-eu/client/shared/chat-manager";
|
|
1
2
|
import { withDatastoreQueryParam } from "@manta-eu/client/shared/datastore-path";
|
|
2
3
|
import { isShowEntitiesResult } from "@manta-eu/client/shared/tools";
|
|
3
4
|
import {
|
|
@@ -5,7 +6,7 @@ import {
|
|
|
5
6
|
type WorkspaceArtifact,
|
|
6
7
|
} from "@manta-eu/client/shared/workspace-artifacts";
|
|
7
8
|
import type { ToolUIPart } from "ai";
|
|
8
|
-
import { useCallback } from "react";
|
|
9
|
+
import { useCallback, useContext } from "react";
|
|
9
10
|
import { match } from "ts-pattern";
|
|
10
11
|
import { FileMessagePart } from "../chat-view/FileMessagePart";
|
|
11
12
|
import { ActionPlanShowcaseCard } from "./ActionPlanShowcaseCard";
|
|
@@ -45,6 +46,7 @@ import {
|
|
|
45
46
|
} from "#components/chart/index";
|
|
46
47
|
import { decodeChartEntities } from "#lib/decode-chart-entities";
|
|
47
48
|
import { cn } from "#lib/utils";
|
|
49
|
+
import { ChatManagerContext } from "#providers/chat-connection-context";
|
|
48
50
|
import { useChatManagerConnection } from "#providers/ChatManagerProvider";
|
|
49
51
|
import { useMantaClient } from "#providers/MantaClientProvider";
|
|
50
52
|
|
|
@@ -129,12 +131,13 @@ function WorkspaceArtifactResultDisplay(props: WorkspaceArtifactResultDisplayPro
|
|
|
129
131
|
const { result } = props;
|
|
130
132
|
|
|
131
133
|
const { datastoreId } = useMantaClient();
|
|
134
|
+
const mode = useContext(ChatManagerContext)?.managerConnection.mode;
|
|
132
135
|
|
|
133
136
|
return (
|
|
134
137
|
<FileMessagePart
|
|
135
138
|
part={{
|
|
136
139
|
type: "file",
|
|
137
|
-
url: withDatastoreQueryParam(result.url, datastoreId),
|
|
140
|
+
url: withChatModeQueryParam(withDatastoreQueryParam(result.url, datastoreId), mode),
|
|
138
141
|
mediaType: result.mediaType,
|
|
139
142
|
filename: result.filename,
|
|
140
143
|
}}
|
|
@@ -20,6 +20,7 @@ import { isPresent } from "@manta-eu/lib/present";
|
|
|
20
20
|
import { match, P } from "ts-pattern";
|
|
21
21
|
import { parseDatalogRuleCached, parseGroundedDatalogRuleCached } from "../rules/datalog-cache";
|
|
22
22
|
import {
|
|
23
|
+
explainRuleAst,
|
|
23
24
|
getRuleBodyTerms,
|
|
24
25
|
kindForBodyFormula,
|
|
25
26
|
kindForSyntheticTerm,
|
|
@@ -362,12 +363,6 @@ function ruleNameTermFromHeadText(headText: string): string {
|
|
|
362
363
|
return predicate.trim();
|
|
363
364
|
}
|
|
364
365
|
|
|
365
|
-
// Explain payloads echo rules as the datastore serializes them (bare ON, atom-free aggregates),
|
|
366
|
-
// so authored text falls through to the expanded grammar when the strict parse fails.
|
|
367
|
-
function explainRuleAst(rule: string): DatalogRule | undefined {
|
|
368
|
-
return parseDatalogRuleCached(rule).ast ?? parseGroundedDatalogRuleCached(rule).ast ?? undefined;
|
|
369
|
-
}
|
|
370
|
-
|
|
371
366
|
function collectBindings(ast: DatalogRule, groundedAst: DatalogRule): VariableBinding[] {
|
|
372
367
|
const bindings = new Map<string, GroundedBinding>();
|
|
373
368
|
const predicates = new Map<string, string>();
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
classAssertionFromAtom,
|
|
3
|
+
formulaSourceTexts,
|
|
3
4
|
isDatalogAtom,
|
|
4
5
|
isLiteralTerm,
|
|
5
6
|
isVariableTerm,
|
|
@@ -11,6 +12,7 @@ import {
|
|
|
11
12
|
type DatalogAtom,
|
|
12
13
|
type DatalogBodyFormula,
|
|
13
14
|
type DatalogExpression,
|
|
15
|
+
type DatalogRule,
|
|
14
16
|
} from "@manta-eu/datalog-parser";
|
|
15
17
|
import { isPresent } from "@manta-eu/lib/present";
|
|
16
18
|
import { countBy } from "ramda";
|
|
@@ -472,13 +474,33 @@ export function ruleHeadText(rule: string): string {
|
|
|
472
474
|
);
|
|
473
475
|
}
|
|
474
476
|
|
|
477
|
+
// Explain payloads echo rules as the datastore serializes them (bare ON, atom-free aggregates),
|
|
478
|
+
// so authored text falls through to the expanded grammar when the strict parse fails.
|
|
479
|
+
export function explainRuleAst(rule: string): DatalogRule | undefined {
|
|
480
|
+
return parseDatalogRuleCached(rule).ast ?? parseGroundedDatalogRuleCached(rule).ast ?? undefined;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function parsedSourceTexts(rule: string) {
|
|
484
|
+
const parsed = explainRuleAst(rule);
|
|
485
|
+
|
|
486
|
+
return parsed ? formulaSourceTexts(parsed) : undefined;
|
|
487
|
+
}
|
|
488
|
+
|
|
475
489
|
export function ruleHeadTerms(rule: string): string[] {
|
|
490
|
+
const parsed = parsedSourceTexts(rule);
|
|
491
|
+
|
|
492
|
+
if (parsed) return parsed.head;
|
|
493
|
+
|
|
476
494
|
return splitTopLevel(ruleHeadText(rule), ",")
|
|
477
495
|
.map((part) => part.trim())
|
|
478
496
|
.filter(isPresent);
|
|
479
497
|
}
|
|
480
498
|
|
|
481
499
|
export function getRuleBodyTerms(rule: string): string[] {
|
|
500
|
+
const parsed = parsedSourceTexts(rule);
|
|
501
|
+
|
|
502
|
+
if (parsed) return parsed.body;
|
|
503
|
+
|
|
482
504
|
const body = bodyText(rule);
|
|
483
505
|
|
|
484
506
|
if (!body) return [];
|
|
@@ -624,7 +646,7 @@ function aggregateOutputsOf(
|
|
|
624
646
|
}
|
|
625
647
|
|
|
626
648
|
function ruleBodyFormulas(rule: string, expectedCount: number): DatalogBodyFormula[] | undefined {
|
|
627
|
-
const ast =
|
|
649
|
+
const ast = explainRuleAst(rule);
|
|
628
650
|
|
|
629
651
|
if (!ast || ast.body.length !== expectedCount) return undefined;
|
|
630
652
|
|
|
@@ -74,7 +74,7 @@ export function toManualAction(entity: ManualActionEntity, options: ManualAction
|
|
|
74
74
|
assignees: entity.assignee.map(String),
|
|
75
75
|
label: entity.label || null,
|
|
76
76
|
description: entity.description || null,
|
|
77
|
-
observedValue: entity.observedValue || null,
|
|
77
|
+
observedValue: typeof entity.observedValue === "string" ? entity.observedValue || null : null,
|
|
78
78
|
aboutIri: entity.about ?? null,
|
|
79
79
|
retryable: String(entity.retryable) === "true",
|
|
80
80
|
defaultResolution: normalizeResolution(entity.defaultResolution ?? undefined),
|
package/src/hooks/useAuth.ts
CHANGED
|
@@ -65,6 +65,8 @@ const orgRoleSchema = z.object({
|
|
|
65
65
|
capabilities: z.array(z.string()).transform((values) => values.filter(isCapability)),
|
|
66
66
|
});
|
|
67
67
|
|
|
68
|
+
export const userContextQueryKey = ["user-context"] as const;
|
|
69
|
+
|
|
68
70
|
export const contextResponseSchema = z.object({
|
|
69
71
|
datastoreAccessGranted: z.boolean().default(true),
|
|
70
72
|
user: z.object({
|
|
@@ -197,7 +199,7 @@ export function useAuth(): UseAuthResult {
|
|
|
197
199
|
isLoading: isContextLoading,
|
|
198
200
|
error: contextError,
|
|
199
201
|
} = useQuery({
|
|
200
|
-
queryKey: [
|
|
202
|
+
queryKey: [...userContextQueryKey, datastoreId],
|
|
201
203
|
queryFn: async ({ signal }) => {
|
|
202
204
|
const res = await authorizedFetch(new URL("/graphql", mantaBaseUrl), {
|
|
203
205
|
method: "POST",
|
package/src/lib/chat-types.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/* eslint-disable import/order -- each type is imported beside the re-export it exists for, which is
|
|
2
2
|
how this file is organized; they are type-only, so the positions have no runtime meaning. */
|
|
3
|
+
import type { ConversationMode } from "@manta-eu/client/shared/chat-manager";
|
|
3
4
|
import type { MantaModelId } from "@manta-eu/client/shared/model";
|
|
4
5
|
import type { ReactNode } from "react";
|
|
5
6
|
import type { Datastore } from "#ldo-ui";
|
|
@@ -26,6 +27,21 @@ export type CopilotMenuDefaults = {
|
|
|
26
27
|
|
|
27
28
|
export type ChatDraft = { id: string; text: string };
|
|
28
29
|
|
|
30
|
+
export type ChatStartupReport = {
|
|
31
|
+
surface: "embedded" | "app";
|
|
32
|
+
mode: "ticket" | "cookie";
|
|
33
|
+
openIndex: number;
|
|
34
|
+
hostTokenMs?: number;
|
|
35
|
+
ticketMs?: number;
|
|
36
|
+
ticketAttempts?: number;
|
|
37
|
+
socketMs: number;
|
|
38
|
+
socketAttempts: number;
|
|
39
|
+
chatMountMs: number;
|
|
40
|
+
socketReadyAtOpen: boolean;
|
|
41
|
+
firstPaintMs: number;
|
|
42
|
+
interactiveMs: number;
|
|
43
|
+
};
|
|
44
|
+
|
|
29
45
|
// NOTE: Adding to this is a breaking change to library clients
|
|
30
46
|
export type AuthenticatedChatProps = {
|
|
31
47
|
datastore?: Datastore;
|
|
@@ -39,6 +55,7 @@ export type AuthenticatedChatProps = {
|
|
|
39
55
|
collapsible?: boolean;
|
|
40
56
|
isCollapsed?: boolean;
|
|
41
57
|
pageContext?: PageContext;
|
|
58
|
+
mode?: ConversationMode;
|
|
42
59
|
allowedModels?: MantaModelId[];
|
|
43
60
|
initialPrompt?: string;
|
|
44
61
|
initialDraft?: ChatDraft;
|
|
@@ -48,6 +65,7 @@ export type AuthenticatedChatProps = {
|
|
|
48
65
|
assistantIcon?: AssistantIcon;
|
|
49
66
|
mark?: ReactNode;
|
|
50
67
|
renderMenuItems?: (defaults: CopilotMenuDefaults) => ReactNode[];
|
|
68
|
+
onStartupTiming?: (report: ChatStartupReport) => void;
|
|
51
69
|
};
|
|
52
70
|
|
|
53
71
|
export type ResolvedChatProps = AuthenticatedChatProps & {
|
|
@@ -19,6 +19,7 @@ export type RouteElements = {
|
|
|
19
19
|
Team?: ReactNode;
|
|
20
20
|
CustomApps?: ReactNode;
|
|
21
21
|
CustomAppUsers?: ReactNode;
|
|
22
|
+
Profile?: ReactNode;
|
|
22
23
|
DataExplorerLayout?: ReactNode;
|
|
23
24
|
DataExplorerIndex?: ReactNode;
|
|
24
25
|
DataExplorerRecent?: ReactNode;
|
|
@@ -48,6 +49,7 @@ export const globalRoutes: RouteDefinition[] = [
|
|
|
48
49
|
{ path: "/shopify/connect", render: (e) => e.ShopifyConnect || notFoundElement(e) },
|
|
49
50
|
{ path: "/custom-apps", render: (e) => e.CustomApps || notFoundElement(e) },
|
|
50
51
|
{ path: "/custom-apps/:clientId/users", render: (e) => e.CustomAppUsers || notFoundElement(e) },
|
|
52
|
+
{ path: "/profile", render: (e) => e.Profile || notFoundElement(e) },
|
|
51
53
|
];
|
|
52
54
|
|
|
53
55
|
export const datastoreRoutes: RouteDefinition[] = [
|
|
@@ -20,6 +20,7 @@ export type AuthContextValue = Pick<
|
|
|
20
20
|
user: User;
|
|
21
21
|
// Only Manta's own web app may list the apps connected to someone; any other host's token is refused.
|
|
22
22
|
canDisconnectCustomApps?: boolean;
|
|
23
|
+
hasProfilePage?: boolean;
|
|
23
24
|
};
|
|
24
25
|
|
|
25
26
|
export const AuthContext = createContext<AuthContextValue | undefined>(undefined);
|
|
@@ -3,35 +3,55 @@ import { ChatContextKind, type ChatContext } from "@manta-eu/client/shared/agent
|
|
|
3
3
|
import {
|
|
4
4
|
chatIdSchema,
|
|
5
5
|
chatManagerBasePath,
|
|
6
|
+
chatModeQuery,
|
|
6
7
|
type ChatManagerClient,
|
|
8
|
+
type ConversationMode,
|
|
7
9
|
type ConversationSummary,
|
|
8
10
|
type DeleteConversationResult,
|
|
9
11
|
type ListConversationsInput,
|
|
10
12
|
} from "@manta-eu/client/shared/chat-manager";
|
|
11
13
|
import { datastoreSocketQuery } from "@manta-eu/client/shared/datastore-path";
|
|
12
14
|
import { chatIdToPath, newChatPath, type ChatUIMessage } from "@manta-eu/client/shared/index";
|
|
15
|
+
import { DEFAULT_CALL_TIMEOUT_MS } from "agents/client";
|
|
13
16
|
import { useAgent } from "agents/react";
|
|
14
|
-
import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
|
|
17
|
+
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, type ReactNode } from "react";
|
|
15
18
|
import { matchPath, useLocation, useMatch, useNavigate } from "react-router";
|
|
16
19
|
import { toast } from "sonner";
|
|
17
20
|
import { match } from "ts-pattern";
|
|
21
|
+
import type { ChatStartupReport } from "../lib/chat-types";
|
|
18
22
|
import { ChatManagerContext, type ChatManagerContextValue, type MessageHandoff } from "./chat-connection-context";
|
|
19
23
|
import {
|
|
20
24
|
createBlankConversationLifecycle,
|
|
21
25
|
mapManagerConnectionState,
|
|
22
26
|
type BlankConversationLifecycle,
|
|
23
27
|
} from "./chat-connection-lifecycle";
|
|
24
|
-
import { useChatSocketAuth } from "./chat-socket-auth";
|
|
28
|
+
import { isRequestedClose, useChatSocketAuth } from "./chat-socket-auth";
|
|
29
|
+
import {
|
|
30
|
+
chatStartupReport,
|
|
31
|
+
createChatStartupTimeline,
|
|
32
|
+
markChatStartup,
|
|
33
|
+
type ChatViewStartupPhase,
|
|
34
|
+
} from "./chat-startup-timing";
|
|
25
35
|
import { useMantaClient } from "./index";
|
|
26
36
|
|
|
27
37
|
export { useChatManagerConnection, type ChatManagerConnectionContextValue } from "./chat-connection-context";
|
|
28
38
|
|
|
39
|
+
export type { ChatStartupReport } from "../lib/chat-types";
|
|
40
|
+
|
|
41
|
+
export type { ChatViewStartupPhase } from "./chat-startup-timing";
|
|
42
|
+
|
|
29
43
|
export type ChatManagerProviderProps = {
|
|
30
44
|
onSelectPlan?: (planIri: string) => void;
|
|
31
45
|
datastoreId: string | undefined;
|
|
46
|
+
// Fixed for the provider's lifetime: it picks the manager, so key the provider on it.
|
|
47
|
+
mode?: ConversationMode;
|
|
48
|
+
surface?: "embedded" | "app";
|
|
49
|
+
onStartupTiming?: (report: ChatStartupReport) => void;
|
|
32
50
|
children: ReactNode;
|
|
33
51
|
};
|
|
34
52
|
|
|
53
|
+
let chatStartupReports = 0;
|
|
54
|
+
|
|
35
55
|
type ContextTransition =
|
|
36
56
|
| { kind: "select"; context: ChatContext }
|
|
37
57
|
| { kind: "start"; context: ChatContext; message: ChatUIMessage }
|
|
@@ -45,6 +65,20 @@ type ExpectedConversationClose = {
|
|
|
45
65
|
conversationId: string;
|
|
46
66
|
};
|
|
47
67
|
|
|
68
|
+
type ManagerOpenWaiter = {
|
|
69
|
+
resolve: () => void;
|
|
70
|
+
reject: (error: Error) => void;
|
|
71
|
+
timer: ReturnType<typeof setTimeout>;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
function settleManagerOpenWaiters(waiters: Set<ManagerOpenWaiter>, settle: (waiter: ManagerOpenWaiter) => void) {
|
|
75
|
+
for (const waiter of waiters) {
|
|
76
|
+
clearTimeout(waiter.timer);
|
|
77
|
+
settle(waiter);
|
|
78
|
+
}
|
|
79
|
+
waiters.clear();
|
|
80
|
+
}
|
|
81
|
+
|
|
48
82
|
function activeConversationFromSelection(selection: ConversationSelection | undefined) {
|
|
49
83
|
return selection?.kind === "active" ? selection.conversation : undefined;
|
|
50
84
|
}
|
|
@@ -57,9 +91,13 @@ function clearExpectedConversationClose(
|
|
|
57
91
|
}
|
|
58
92
|
|
|
59
93
|
export function ChatManagerProvider(props: ChatManagerProviderProps) {
|
|
60
|
-
const { onSelectPlan, datastoreId, children } = props;
|
|
94
|
+
const { onSelectPlan, datastoreId, surface = "app", onStartupTiming, children } = props;
|
|
95
|
+
const [mode] = useState<ConversationMode>(props.mode ?? "datastore");
|
|
61
96
|
const { mantaBaseUrl } = useMantaClient();
|
|
62
|
-
const datastoreQuery = useMemo(
|
|
97
|
+
const datastoreQuery = useMemo(
|
|
98
|
+
() => ({ ...datastoreSocketQuery(datastoreId), ...chatModeQuery(mode) }),
|
|
99
|
+
[datastoreId, mode],
|
|
100
|
+
);
|
|
63
101
|
const navigate = useNavigate();
|
|
64
102
|
const location = useLocation();
|
|
65
103
|
const chatMatch = useMatch("/chat/:chatId");
|
|
@@ -91,7 +129,35 @@ export function ChatManagerProvider(props: ChatManagerProviderProps) {
|
|
|
91
129
|
pathnameRef.current = location.pathname;
|
|
92
130
|
routedConversationIdRef.current = routedConversationId;
|
|
93
131
|
|
|
94
|
-
const
|
|
132
|
+
const [timeline] = useState(createChatStartupTimeline);
|
|
133
|
+
const onStartupTimingRef = useRef(onStartupTiming);
|
|
134
|
+
onStartupTimingRef.current = onStartupTiming;
|
|
135
|
+
const socketAuth = useChatSocketAuth(datastoreQuery, (phase) => markChatStartup(timeline, phase));
|
|
136
|
+
const authMode = "enabled" in socketAuth ? "ticket" : "cookie";
|
|
137
|
+
const socketKey = "enabled" in socketAuth && !socketAuth.enabled ? undefined : JSON.stringify(socketAuth.query);
|
|
138
|
+
const enabledSocketKeyRef = useRef<string>(undefined);
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
if (socketKey === undefined || enabledSocketKeyRef.current === socketKey) return;
|
|
141
|
+
enabledSocketKeyRef.current = socketKey;
|
|
142
|
+
markChatStartup(timeline, "socket-enabled");
|
|
143
|
+
}, [socketKey, timeline]);
|
|
144
|
+
|
|
145
|
+
const reportedStartupRef = useRef(false);
|
|
146
|
+
const reportStartup = useCallback(() => {
|
|
147
|
+
if (reportedStartupRef.current) return;
|
|
148
|
+
const report = chatStartupReport(timeline, { surface, mode: authMode, openIndex: chatStartupReports + 1 });
|
|
149
|
+
if (!report) return;
|
|
150
|
+
reportedStartupRef.current = true;
|
|
151
|
+
chatStartupReports += 1;
|
|
152
|
+
try {
|
|
153
|
+
onStartupTimingRef.current?.(report);
|
|
154
|
+
} catch (error) {
|
|
155
|
+
console.error("onStartupTiming threw", error);
|
|
156
|
+
}
|
|
157
|
+
}, [surface, authMode, timeline]);
|
|
158
|
+
|
|
159
|
+
const managerOpenWaitersRef = useRef(new Set<ManagerOpenWaiter>());
|
|
160
|
+
|
|
95
161
|
const managerAgent = useAgent<ChatManagerClient, ChatManagerClient["state"]>({
|
|
96
162
|
agent: "ChatManager",
|
|
97
163
|
basePath: chatManagerBasePath,
|
|
@@ -100,8 +166,56 @@ export function ChatManagerProvider(props: ChatManagerProviderProps) {
|
|
|
100
166
|
...("enabled" in socketAuth
|
|
101
167
|
? { enabled: socketAuth.enabled, shouldReconnectOnClose: socketAuth.shouldReconnectOnClose }
|
|
102
168
|
: {}),
|
|
169
|
+
onOpen: () => {
|
|
170
|
+
markChatStartup(timeline, "socket-open");
|
|
171
|
+
reportStartup();
|
|
172
|
+
settleManagerOpenWaiters(managerOpenWaitersRef.current, (waiter) => waiter.resolve());
|
|
173
|
+
},
|
|
174
|
+
onClose: (event) => {
|
|
175
|
+
if (authMode === "cookie" && !isRequestedClose(event) && timeline.marks["socket-open"] === undefined) {
|
|
176
|
+
timeline.socketAttempts += 1;
|
|
177
|
+
}
|
|
178
|
+
},
|
|
103
179
|
});
|
|
104
180
|
|
|
181
|
+
const managerAgentRef = useRef(managerAgent);
|
|
182
|
+
managerAgentRef.current = managerAgent;
|
|
183
|
+
|
|
184
|
+
const disposedRef = useRef(false);
|
|
185
|
+
useEffect(() => {
|
|
186
|
+
disposedRef.current = false;
|
|
187
|
+
return () => {
|
|
188
|
+
disposedRef.current = true;
|
|
189
|
+
};
|
|
190
|
+
}, []);
|
|
191
|
+
|
|
192
|
+
const waitForManagerOpen = useCallback((): Promise<void> => {
|
|
193
|
+
const agent = managerAgentRef.current;
|
|
194
|
+
if (agent.readyState === WebSocket.OPEN) return Promise.resolve();
|
|
195
|
+
if (agent.connectionError !== null && agent.readyState === WebSocket.CLOSED) {
|
|
196
|
+
return Promise.reject(new Error("The chat connection closed."));
|
|
197
|
+
}
|
|
198
|
+
return new Promise((resolve, reject) => {
|
|
199
|
+
const waiters = managerOpenWaitersRef.current;
|
|
200
|
+
const waiter: ManagerOpenWaiter = {
|
|
201
|
+
resolve,
|
|
202
|
+
reject,
|
|
203
|
+
timer: setTimeout(() => {
|
|
204
|
+
waiters.delete(waiter);
|
|
205
|
+
if (!disposedRef.current) reject(new Error("The chat connection did not open."));
|
|
206
|
+
}, DEFAULT_CALL_TIMEOUT_MS),
|
|
207
|
+
};
|
|
208
|
+
waiters.add(waiter);
|
|
209
|
+
});
|
|
210
|
+
}, []);
|
|
211
|
+
|
|
212
|
+
const markStartup = useCallback(
|
|
213
|
+
(phase: ChatViewStartupPhase) => {
|
|
214
|
+
if (markChatStartup(timeline, phase) && phase === "interactive") reportStartup();
|
|
215
|
+
},
|
|
216
|
+
[reportStartup, timeline],
|
|
217
|
+
);
|
|
218
|
+
|
|
105
219
|
const listConversations = useCallback(
|
|
106
220
|
(input: ListConversationsInput = {}): Promise<ConversationSummary[]> => managerAgent.stub.listConversations(input),
|
|
107
221
|
[managerAgent],
|
|
@@ -166,10 +280,14 @@ export function ChatManagerProvider(props: ChatManagerProviderProps) {
|
|
|
166
280
|
|
|
167
281
|
const sendBlankConversationMessage = useCallback(
|
|
168
282
|
(message: ChatUIMessage): Promise<void> => {
|
|
169
|
-
|
|
170
|
-
if (lifecycle.
|
|
283
|
+
let lifecycle = blankConversationRef.current;
|
|
284
|
+
if (lifecycle.conversationId || lifecycle.creating) {
|
|
171
285
|
return Promise.reject(new Error("A new chat is already starting."));
|
|
172
286
|
}
|
|
287
|
+
if (lifecycle.materialization) {
|
|
288
|
+
lifecycle = createBlankConversationLifecycle();
|
|
289
|
+
blankConversationRef.current = lifecycle;
|
|
290
|
+
}
|
|
173
291
|
|
|
174
292
|
setMessageHandoff({ message });
|
|
175
293
|
function clearMessageHandoff() {
|
|
@@ -187,13 +305,23 @@ export function ChatManagerProvider(props: ChatManagerProviderProps) {
|
|
|
187
305
|
}
|
|
188
306
|
const materialization = (async () => {
|
|
189
307
|
try {
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
308
|
+
const deadline = performance.now() + DEFAULT_CALL_TIMEOUT_MS;
|
|
309
|
+
await waitForManagerOpen();
|
|
310
|
+
if (!isCurrentLifecycle()) {
|
|
311
|
+
clearMessageHandoff();
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
lifecycle.creating = true;
|
|
316
|
+
const conversation = await managerAgentRef.current.call(
|
|
317
|
+
"createConversation",
|
|
318
|
+
[{ idempotencyKey: lifecycle.idempotencyKey }],
|
|
319
|
+
{ timeout: Math.max(deadline - performance.now(), 1) },
|
|
320
|
+
);
|
|
193
321
|
|
|
194
322
|
if (!isCurrentLifecycle()) {
|
|
195
323
|
clearMessageHandoff();
|
|
196
|
-
await
|
|
324
|
+
await managerAgentRef.current.stub.deleteConversation({ id: conversation.id });
|
|
197
325
|
return;
|
|
198
326
|
}
|
|
199
327
|
|
|
@@ -213,13 +341,14 @@ export function ChatManagerProvider(props: ChatManagerProviderProps) {
|
|
|
213
341
|
throw error;
|
|
214
342
|
} finally {
|
|
215
343
|
lifecycle.materialization = undefined;
|
|
344
|
+
lifecycle.creating = false;
|
|
216
345
|
}
|
|
217
346
|
})();
|
|
218
347
|
|
|
219
348
|
lifecycle.materialization = materialization;
|
|
220
349
|
return materialization;
|
|
221
350
|
},
|
|
222
|
-
[
|
|
351
|
+
[navigate, waitForManagerOpen],
|
|
223
352
|
);
|
|
224
353
|
|
|
225
354
|
const startNewChat = useCallback(() => {
|
|
@@ -234,8 +363,13 @@ export function ChatManagerProvider(props: ChatManagerProviderProps) {
|
|
|
234
363
|
}
|
|
235
364
|
}, [onNewChatPath, navigate]);
|
|
236
365
|
|
|
366
|
+
const handledPathnameRef = useRef(location.pathname);
|
|
367
|
+
|
|
237
368
|
// `/` is still a blank chat in an app that gives its datastores no home page.
|
|
238
|
-
|
|
369
|
+
useLayoutEffect(() => {
|
|
370
|
+
if (handledPathnameRef.current === location.pathname) return;
|
|
371
|
+
handledPathnameRef.current = location.pathname;
|
|
372
|
+
|
|
239
373
|
if (onNewChatPath || location.pathname === "/") {
|
|
240
374
|
const preserveBlankLifecycle = preserveBlankLifecycleOnNextBlankChatRef.current;
|
|
241
375
|
if (!preserveBlankLifecycle) {
|
|
@@ -247,6 +381,7 @@ export function ChatManagerProvider(props: ChatManagerProviderProps) {
|
|
|
247
381
|
setConversationSelection(undefined);
|
|
248
382
|
} else {
|
|
249
383
|
preserveBlankLifecycleOnNextBlankChatRef.current = false;
|
|
384
|
+
setMessageHandoff((current) => (current?.conversationId ? current : undefined));
|
|
250
385
|
}
|
|
251
386
|
}, [location.pathname, onNewChatPath]);
|
|
252
387
|
|
|
@@ -394,6 +529,10 @@ export function ChatManagerProvider(props: ChatManagerProviderProps) {
|
|
|
394
529
|
onSelectPlan(planIri);
|
|
395
530
|
return;
|
|
396
531
|
}
|
|
532
|
+
if (mode === "group") {
|
|
533
|
+
toast.info("Open this plan from its own datastore.");
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
397
536
|
|
|
398
537
|
const context = { kind: ChatContextKind.Plan, planGraphIri: planIri } as const;
|
|
399
538
|
const source = activeConversationFromSelection(conversationSelectionRef.current);
|
|
@@ -412,12 +551,20 @@ export function ChatManagerProvider(props: ChatManagerProviderProps) {
|
|
|
412
551
|
if (pendingPlanNavigationRef.current === navigation) pendingPlanNavigationRef.current = undefined;
|
|
413
552
|
}
|
|
414
553
|
},
|
|
415
|
-
[navigate, onSelectPlan, reassignCurrentConversation],
|
|
554
|
+
[mode, navigate, onSelectPlan, reassignCurrentConversation],
|
|
416
555
|
);
|
|
417
556
|
|
|
418
557
|
const managerConnectionState = mapManagerConnectionState(managerAgent);
|
|
558
|
+
|
|
559
|
+
useEffect(() => {
|
|
560
|
+
if (managerAgent.connectionError === null) return;
|
|
561
|
+
const error = new Error("The chat connection closed.");
|
|
562
|
+
settleManagerOpenWaiters(managerOpenWaitersRef.current, (waiter) => waiter.reject(error));
|
|
563
|
+
}, [managerAgent.connectionError]);
|
|
564
|
+
|
|
419
565
|
const selectedContext = activeConversation?.context ?? pendingContext;
|
|
420
|
-
const isConversationLoading =
|
|
566
|
+
const isConversationLoading =
|
|
567
|
+
invalidConversationRoute || (routedConversationId !== undefined && activeConversation?.id !== routedConversationId);
|
|
421
568
|
const context = useMemo<ChatManagerContextValue>(
|
|
422
569
|
() => ({
|
|
423
570
|
managerConnection: {
|
|
@@ -428,6 +575,7 @@ export function ChatManagerProvider(props: ChatManagerProviderProps) {
|
|
|
428
575
|
pendingMessage: messageHandoff?.message,
|
|
429
576
|
isConversationLoading,
|
|
430
577
|
connectionState: managerConnectionState,
|
|
578
|
+
mode,
|
|
431
579
|
listConversations,
|
|
432
580
|
deleteConversation,
|
|
433
581
|
openConversation,
|
|
@@ -437,6 +585,7 @@ export function ChatManagerProvider(props: ChatManagerProviderProps) {
|
|
|
437
585
|
startContextConversation,
|
|
438
586
|
reassignCurrentConversation,
|
|
439
587
|
handleSelectPlan,
|
|
588
|
+
markStartup,
|
|
440
589
|
},
|
|
441
590
|
session: {
|
|
442
591
|
activeConversationId: activeConversation?.id,
|
|
@@ -451,11 +600,13 @@ export function ChatManagerProvider(props: ChatManagerProviderProps) {
|
|
|
451
600
|
[
|
|
452
601
|
activeConversation,
|
|
453
602
|
deleteConversation,
|
|
603
|
+
mode,
|
|
454
604
|
handleSelectPlan,
|
|
455
605
|
isConversationLoading,
|
|
456
606
|
listConversations,
|
|
457
607
|
managerAgent,
|
|
458
608
|
managerAgent.state,
|
|
609
|
+
markStartup,
|
|
459
610
|
managerConnectionState,
|
|
460
611
|
mantaBaseUrl.host,
|
|
461
612
|
datastoreQuery,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ChatContext, SendAgentMessage } from "@manta-eu/client/shared/agent-types";
|
|
2
2
|
import type {
|
|
3
3
|
ChatManagerState,
|
|
4
|
+
ConversationMode,
|
|
4
5
|
ConversationSummary,
|
|
5
6
|
DeleteConversationResult,
|
|
6
7
|
ListConversationsInput,
|
|
@@ -8,6 +9,7 @@ import type {
|
|
|
8
9
|
import type { ChatUIMessage } from "@manta-eu/client/shared/index";
|
|
9
10
|
import { createContext, useContext } from "react";
|
|
10
11
|
import type { ConnectionState } from "../lib/chat-types";
|
|
12
|
+
import type { ChatViewStartupPhase } from "./chat-startup-timing";
|
|
11
13
|
|
|
12
14
|
export type ChatManagerConnectionContextValue = {
|
|
13
15
|
agent: { readyState: number; reconnect: () => void };
|
|
@@ -17,6 +19,7 @@ export type ChatManagerConnectionContextValue = {
|
|
|
17
19
|
pendingMessage?: ChatUIMessage;
|
|
18
20
|
isConversationLoading: boolean;
|
|
19
21
|
connectionState: ConnectionState;
|
|
22
|
+
mode: ConversationMode;
|
|
20
23
|
listConversations(input?: ListConversationsInput): Promise<ConversationSummary[]>;
|
|
21
24
|
deleteConversation(id: string): Promise<DeleteConversationResult>;
|
|
22
25
|
openConversation(id: string): void;
|
|
@@ -26,6 +29,7 @@ export type ChatManagerConnectionContextValue = {
|
|
|
26
29
|
startContextConversation: (context: ChatContext, message: ChatUIMessage) => Promise<ConversationSummary | undefined>;
|
|
27
30
|
reassignCurrentConversation: (context: ChatContext) => Promise<ConversationSummary | undefined>;
|
|
28
31
|
handleSelectPlan: (planIri: string) => Promise<void>;
|
|
32
|
+
markStartup: (phase: ChatViewStartupPhase) => void;
|
|
29
33
|
};
|
|
30
34
|
|
|
31
35
|
export type MessageHandoff = {
|