@open-mercato/ui 0.6.2-develop.3461.1.605f31c2c9 → 0.6.2
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/.turbo/turbo-build.log +1 -1
- package/dist/ai/AiChat.js +130 -22
- package/dist/ai/AiChat.js.map +2 -2
- package/dist/ai/AiChatSessions.js +73 -3
- package/dist/ai/AiChatSessions.js.map +2 -2
- package/dist/ai/conversation-store.js +149 -0
- package/dist/ai/conversation-store.js.map +7 -0
- package/dist/ai/parts/MutationPreviewCard.js +4 -3
- package/dist/ai/parts/MutationPreviewCard.js.map +2 -2
- package/dist/ai/parts/MutationResultCard.js +9 -16
- package/dist/ai/parts/MutationResultCard.js.map +2 -2
- package/dist/ai/useAiChat.js +80 -4
- package/dist/ai/useAiChat.js.map +2 -2
- package/dist/backend/AuthSessionGuard.js +99 -0
- package/dist/backend/AuthSessionGuard.js.map +7 -0
- package/dist/backend/CrudForm.js +2 -1
- package/dist/backend/CrudForm.js.map +2 -2
- package/dist/backend/FilterOverlay.js +1 -1
- package/dist/backend/FilterOverlay.js.map +1 -1
- package/dist/backend/dashboard/widgetRegistry.js +2 -1
- package/dist/backend/dashboard/widgetRegistry.js.map +2 -2
- package/dist/backend/date-range/dateRanges.js +16 -18
- package/dist/backend/date-range/dateRanges.js.map +2 -2
- package/dist/backend/detail/AttachmentMetadataDialog.js +3 -1
- package/dist/backend/detail/AttachmentMetadataDialog.js.map +2 -2
- package/dist/backend/detail/InlineEditors.js +1 -1
- package/dist/backend/detail/InlineEditors.js.map +1 -1
- package/dist/backend/injection/useAppEvent.js +2 -7
- package/dist/backend/injection/useAppEvent.js.map +2 -2
- package/dist/backend/injection/widgetRegistry.js +2 -1
- package/dist/backend/injection/widgetRegistry.js.map +2 -2
- package/dist/backend/messages/useMessageCompose.js +62 -5
- package/dist/backend/messages/useMessageCompose.js.map +2 -2
- package/dist/backend/messages/useMessageComposeOperations.js +64 -5
- package/dist/backend/messages/useMessageComposeOperations.js.map +2 -2
- package/dist/backend/schedule/ScheduleCalendar.js +11 -1
- package/dist/backend/schedule/ScheduleCalendar.js.map +2 -2
- package/dist/backend/schedule/ScheduleToolbar.js +11 -1
- package/dist/backend/schedule/ScheduleToolbar.js.map +2 -2
- package/dist/primitives/calendar.js +1 -1
- package/dist/primitives/calendar.js.map +1 -1
- package/dist/primitives/date-format.js +1 -1
- package/dist/primitives/date-format.js.map +1 -1
- package/dist/primitives/date-picker-helpers.js +7 -9
- package/dist/primitives/date-picker-helpers.js.map +2 -2
- package/dist/primitives/date-picker.js +1 -1
- package/dist/primitives/date-picker.js.map +2 -2
- package/dist/primitives/date-range-picker.js +1 -1
- package/dist/primitives/date-range-picker.js.map +2 -2
- package/jest.config.cjs +4 -1
- package/package.json +6 -7
- package/src/ai/AiChat.tsx +138 -22
- package/src/ai/AiChatSessions.tsx +89 -9
- package/src/ai/__tests__/AiAssistantLauncher.test.tsx +1 -1
- package/src/ai/__tests__/AiChat.conversation.test.tsx +86 -0
- package/src/ai/__tests__/AiChat.test.tsx +17 -2
- package/src/ai/__tests__/conversation-store.test.ts +29 -0
- package/src/ai/conversation-store.ts +246 -0
- package/src/ai/parts/MutationPreviewCard.tsx +25 -20
- package/src/ai/parts/MutationResultCard.tsx +15 -27
- package/src/ai/parts/__tests__/MutationPreviewCard.test.tsx +26 -1
- package/src/ai/parts/__tests__/MutationResultCard.test.tsx +16 -1
- package/src/ai/useAiChat.ts +97 -8
- package/src/backend/AuthSessionGuard.tsx +114 -0
- package/src/backend/CrudForm.tsx +3 -2
- package/src/backend/FilterOverlay.tsx +1 -1
- package/src/backend/__tests__/AppShell.test.tsx +1 -1
- package/src/backend/__tests__/AuthSessionGuard.test.tsx +150 -0
- package/src/backend/__tests__/SidebarCustomizationEditor.test.tsx +8 -1
- package/src/backend/dashboard/widgetRegistry.ts +2 -1
- package/src/backend/date-range/dateRanges.ts +16 -18
- package/src/backend/detail/AttachmentMetadataDialog.tsx +1 -1
- package/src/backend/detail/InlineEditors.tsx +1 -1
- package/src/backend/injection/__tests__/useAppEvent.test.ts +16 -0
- package/src/backend/injection/useAppEvent.ts +2 -9
- package/src/backend/injection/widgetRegistry.ts +2 -1
- package/src/backend/messages/__tests__/MessageComposer.test.tsx +77 -0
- package/src/backend/messages/useMessageCompose.ts +64 -5
- package/src/backend/messages/useMessageComposeOperations.ts +70 -4
- package/src/backend/schedule/ScheduleCalendar.tsx +11 -1
- package/src/backend/schedule/ScheduleToolbar.tsx +11 -1
- package/src/primitives/calendar.tsx +2 -2
- package/src/primitives/date-format.ts +2 -2
- package/src/primitives/date-picker-helpers.ts +7 -9
- package/src/primitives/date-picker.tsx +2 -2
- package/src/primitives/date-range-picker.tsx +2 -2
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
|
+
import {
|
|
5
|
+
createAiServerConversation,
|
|
6
|
+
listAiServerConversations,
|
|
7
|
+
updateAiServerConversation
|
|
8
|
+
} from "./conversation-store.js";
|
|
4
9
|
const STORAGE_KEY = "om-ai-chat-sessions-v1";
|
|
5
10
|
const HISTORY_LIMIT = 50;
|
|
6
11
|
const AiChatSessionsContext = React.createContext(null);
|
|
@@ -50,11 +55,54 @@ function writePersisted(state) {
|
|
|
50
55
|
} catch {
|
|
51
56
|
}
|
|
52
57
|
}
|
|
58
|
+
function serverConversationToSession(conversation, existing) {
|
|
59
|
+
const createdAt = Date.parse(conversation.createdAt);
|
|
60
|
+
const updatedAt = Date.parse(conversation.lastMessageAt ?? conversation.updatedAt);
|
|
61
|
+
return {
|
|
62
|
+
id: existing?.id ?? conversation.conversationId,
|
|
63
|
+
agentId: conversation.agentId,
|
|
64
|
+
conversationId: conversation.conversationId,
|
|
65
|
+
name: conversation.title ?? existing?.name,
|
|
66
|
+
createdAt: Number.isFinite(createdAt) ? createdAt : existing?.createdAt ?? Date.now(),
|
|
67
|
+
lastUsedAt: Number.isFinite(updatedAt) ? updatedAt : existing?.lastUsedAt ?? Date.now(),
|
|
68
|
+
status: conversation.status
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function mergeServerConversations(prev, conversations) {
|
|
72
|
+
const byConversationId = new Map(prev.sessions.map((session) => [session.conversationId, session]));
|
|
73
|
+
const serverSessions = conversations.map(
|
|
74
|
+
(conversation) => serverConversationToSession(conversation, byConversationId.get(conversation.conversationId))
|
|
75
|
+
);
|
|
76
|
+
const serverIds = new Set(serverSessions.map((session) => session.conversationId));
|
|
77
|
+
const localOnly = prev.sessions.filter((session) => !serverIds.has(session.conversationId));
|
|
78
|
+
const sessions = [...serverSessions, ...localOnly];
|
|
79
|
+
const activeByAgent = { ...prev.activeByAgent };
|
|
80
|
+
const agentIds = new Set(sessions.map((session) => session.agentId));
|
|
81
|
+
for (const agentId of agentIds) {
|
|
82
|
+
const activeId = activeByAgent[agentId];
|
|
83
|
+
const active = sessions.find((session) => session.id === activeId);
|
|
84
|
+
if (active && active.status === "open") continue;
|
|
85
|
+
const nextOpen = sessions.filter((session) => session.agentId === agentId && session.status === "open").sort((a, b) => b.lastUsedAt - a.lastUsedAt)[0];
|
|
86
|
+
if (nextOpen) activeByAgent[agentId] = nextOpen.id;
|
|
87
|
+
else delete activeByAgent[agentId];
|
|
88
|
+
}
|
|
89
|
+
return { sessions, activeByAgent };
|
|
90
|
+
}
|
|
53
91
|
function AiChatSessionsProvider({ children }) {
|
|
54
92
|
const [state, setState] = React.useState(() => readPersisted());
|
|
55
93
|
React.useEffect(() => {
|
|
56
94
|
writePersisted(state);
|
|
57
95
|
}, [state]);
|
|
96
|
+
React.useEffect(() => {
|
|
97
|
+
let cancelled = false;
|
|
98
|
+
void listAiServerConversations({ limit: 100 }).then((conversations) => {
|
|
99
|
+
if (cancelled || !conversations) return;
|
|
100
|
+
setState((prev) => mergeServerConversations(prev, conversations));
|
|
101
|
+
});
|
|
102
|
+
return () => {
|
|
103
|
+
cancelled = true;
|
|
104
|
+
};
|
|
105
|
+
}, []);
|
|
58
106
|
const update = React.useCallback(
|
|
59
107
|
(mutator) => {
|
|
60
108
|
setState((prev) => mutator(prev));
|
|
@@ -88,6 +136,10 @@ function AiChatSessionsProvider({ children }) {
|
|
|
88
136
|
lastUsedAt: Date.now(),
|
|
89
137
|
status: "open"
|
|
90
138
|
};
|
|
139
|
+
void createAiServerConversation({
|
|
140
|
+
agentId: session.agentId,
|
|
141
|
+
conversationId: session.conversationId
|
|
142
|
+
});
|
|
91
143
|
update((prev) => ({
|
|
92
144
|
sessions: [...prev.sessions, session],
|
|
93
145
|
activeByAgent: { ...prev.activeByAgent, [agentId]: session.id }
|
|
@@ -96,6 +148,10 @@ function AiChatSessionsProvider({ children }) {
|
|
|
96
148
|
}, [update]);
|
|
97
149
|
const closeSession = React.useCallback(
|
|
98
150
|
(sessionId) => {
|
|
151
|
+
const serverTarget = state.sessions.find((s) => s.id === sessionId);
|
|
152
|
+
if (serverTarget) {
|
|
153
|
+
void updateAiServerConversation(serverTarget.conversationId, { status: "closed" });
|
|
154
|
+
}
|
|
99
155
|
update((prev) => {
|
|
100
156
|
const target = prev.sessions.find((s) => s.id === sessionId);
|
|
101
157
|
if (!target) return prev;
|
|
@@ -114,10 +170,14 @@ function AiChatSessionsProvider({ children }) {
|
|
|
114
170
|
return { sessions, activeByAgent };
|
|
115
171
|
});
|
|
116
172
|
},
|
|
117
|
-
[update]
|
|
173
|
+
[state.sessions, update]
|
|
118
174
|
);
|
|
119
175
|
const reopenSession = React.useCallback(
|
|
120
176
|
(sessionId) => {
|
|
177
|
+
const serverTarget = state.sessions.find((s) => s.id === sessionId);
|
|
178
|
+
if (serverTarget) {
|
|
179
|
+
void updateAiServerConversation(serverTarget.conversationId, { status: "open" });
|
|
180
|
+
}
|
|
121
181
|
update((prev) => {
|
|
122
182
|
const target = prev.sessions.find((s) => s.id === sessionId);
|
|
123
183
|
if (!target) return prev;
|
|
@@ -130,7 +190,7 @@ function AiChatSessionsProvider({ children }) {
|
|
|
130
190
|
};
|
|
131
191
|
});
|
|
132
192
|
},
|
|
133
|
-
[update]
|
|
193
|
+
[state.sessions, update]
|
|
134
194
|
);
|
|
135
195
|
const setActiveSession = React.useCallback(
|
|
136
196
|
(sessionId) => {
|
|
@@ -151,6 +211,12 @@ function AiChatSessionsProvider({ children }) {
|
|
|
151
211
|
const renameSession = React.useCallback(
|
|
152
212
|
(sessionId, name) => {
|
|
153
213
|
const trimmed = name.trim();
|
|
214
|
+
const serverTarget = state.sessions.find((s) => s.id === sessionId);
|
|
215
|
+
if (serverTarget) {
|
|
216
|
+
void updateAiServerConversation(serverTarget.conversationId, {
|
|
217
|
+
title: trimmed.length > 0 ? trimmed : null
|
|
218
|
+
});
|
|
219
|
+
}
|
|
154
220
|
update((prev) => ({
|
|
155
221
|
...prev,
|
|
156
222
|
sessions: prev.sessions.map(
|
|
@@ -158,7 +224,7 @@ function AiChatSessionsProvider({ children }) {
|
|
|
158
224
|
)
|
|
159
225
|
}));
|
|
160
226
|
},
|
|
161
|
-
[update]
|
|
227
|
+
[state.sessions, update]
|
|
162
228
|
);
|
|
163
229
|
const touchSession = React.useCallback(
|
|
164
230
|
(sessionId) => {
|
|
@@ -201,6 +267,10 @@ function AiChatSessionsProvider({ children }) {
|
|
|
201
267
|
lastUsedAt: Date.now(),
|
|
202
268
|
status: "open"
|
|
203
269
|
};
|
|
270
|
+
void createAiServerConversation({
|
|
271
|
+
agentId: mintedSession.agentId,
|
|
272
|
+
conversationId: mintedSession.conversationId
|
|
273
|
+
});
|
|
204
274
|
}
|
|
205
275
|
resolved = mintedSession;
|
|
206
276
|
return {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/ai/AiChatSessions.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\n/**\n * Multi-tab AI chat sessions.\n *\n * Each agent can have several concurrent conversation threads (sessions).\n * The provider stores their *metadata* \u2014 id, conversationId, optional\n * user-given name, timestamps, status \u2014 in localStorage, so closing and\n * re-opening the chat pane (or refreshing the page) restores the tabs and\n * their history. The actual messages live in `useAiChat`'s per-conversation\n * persistence slot keyed by `(agent, conversationId)`, which is why every\n * session gets its own UUID even when it shares an agent with the next tab.\n *\n * Sessions are partitioned into:\n * - `open` \u2192 currently shown in the tab strip\n * - `closed` \u2192 hidden but kept for the history dropdown\n *\n * Closing a tab moves it to `closed`. Re-opening from history flips it back\n * to `open` and surfaces it as the active tab. Renaming is a single\n * `name` field; falling back to the formatted creation date when the user\n * never named the session keeps the picker scannable.\n */\n\nimport * as React from 'react'\n\nconst STORAGE_KEY = 'om-ai-chat-sessions-v1'\nconst HISTORY_LIMIT = 50\n\nexport type AiChatSessionStatus = 'open' | 'closed'\n\nexport interface AiChatSession {\n id: string\n agentId: string\n conversationId: string\n name?: string\n createdAt: number\n lastUsedAt: number\n status: AiChatSessionStatus\n}\n\ninterface AiChatSessionsState {\n sessions: AiChatSession[]\n activeByAgent: Record<string, string>\n}\n\ninterface AiChatSessionsApi {\n state: AiChatSessionsState\n /** Returns open sessions for `agentId`, ordered by creation. */\n getOpenSessions: (agentId: string) => AiChatSession[]\n /** Returns closed sessions (history) for `agentId`, newest first. */\n getClosedSessions: (agentId: string, limit?: number) => AiChatSession[]\n /** Returns the active session for `agentId`, or null if none. */\n getActiveSession: (agentId: string) => AiChatSession | null\n /** Creates and activates a fresh open session for `agentId`. */\n createSession: (agentId: string) => AiChatSession\n /** Closes a session (moves to history). If it was active, picks another. */\n closeSession: (sessionId: string) => void\n /** Re-opens a closed session and activates it. */\n reopenSession: (sessionId: string) => void\n /** Activates an open session. */\n setActiveSession: (sessionId: string) => void\n /** Renames a session \u2014 empty / whitespace clears the custom name. */\n renameSession: (sessionId: string, name: string) => void\n /** Bumps `lastUsedAt` on a session (call when activity happens). */\n touchSession: (sessionId: string) => void\n /** Ensures `agentId` has at least one open session and returns its id. */\n ensureSession: (agentId: string) => AiChatSession\n}\n\nconst AiChatSessionsContext = React.createContext<AiChatSessionsApi | null>(null)\n\nfunction makeId(): string {\n const g = globalThis as unknown as { crypto?: { randomUUID?: () => string } }\n if (g.crypto && typeof g.crypto.randomUUID === 'function') {\n try {\n return g.crypto.randomUUID()\n } catch {\n /* fall through */\n }\n }\n const rand = () => Math.random().toString(16).slice(2, 10)\n return `${Date.now().toString(16)}-${rand()}-${rand()}`\n}\n\nfunction readPersisted(): AiChatSessionsState {\n if (typeof window === 'undefined') return { sessions: [], activeByAgent: {} }\n try {\n const raw = window.localStorage.getItem(STORAGE_KEY)\n if (!raw) return { sessions: [], activeByAgent: {} }\n const parsed = JSON.parse(raw) as Partial<AiChatSessionsState> | null\n const sessions = Array.isArray(parsed?.sessions)\n ? (parsed!.sessions as unknown[])\n .filter((entry): entry is AiChatSession => {\n if (!entry || typeof entry !== 'object') return false\n const value = entry as Record<string, unknown>\n return (\n typeof value.id === 'string' &&\n typeof value.agentId === 'string' &&\n typeof value.conversationId === 'string' &&\n typeof value.createdAt === 'number' &&\n typeof value.lastUsedAt === 'number' &&\n (value.status === 'open' || value.status === 'closed')\n )\n })\n .map((entry) => {\n const value = entry as unknown as Record<string, unknown>\n const candidate = value.name\n return {\n ...entry,\n name: typeof candidate === 'string' ? candidate : undefined,\n }\n })\n : []\n const activeByAgent =\n parsed?.activeByAgent && typeof parsed.activeByAgent === 'object'\n ? Object.fromEntries(\n Object.entries(parsed.activeByAgent as Record<string, unknown>).filter(\n (entry): entry is [string, string] =>\n typeof entry[0] === 'string' && typeof entry[1] === 'string',\n ),\n )\n : {}\n return { sessions, activeByAgent }\n } catch {\n return { sessions: [], activeByAgent: {} }\n }\n}\n\nfunction writePersisted(state: AiChatSessionsState): void {\n if (typeof window === 'undefined') return\n try {\n window.localStorage.setItem(STORAGE_KEY, JSON.stringify(state))\n } catch {\n /* quota / privacy mode \u2014 drop silently */\n }\n}\n\nexport function AiChatSessionsProvider({ children }: { children: React.ReactNode }) {\n // Hydrate synchronously via a lazy initializer. The previous \"empty\n // state + post-mount load effect\" pattern had a window where the\n // persistence effect ran with the empty closure value (because the\n // hydrate effect's queued setState had not committed yet) and clobbered\n // localStorage with `[]` before the loaded state's re-render wrote it\n // back. The lazy initializer puts the loaded state into the very first\n // render, so the persistence effect always sees the real data.\n // `readPersisted` already short-circuits to an empty object on the\n // server (no `window`), so SSR stays consistent with the bare-bones\n // shell \u2014 actual session-dependent UI only renders after a user\n // interaction opens a chat surface.\n const [state, setState] = React.useState<AiChatSessionsState>(() => readPersisted())\n\n React.useEffect(() => {\n writePersisted(state)\n }, [state])\n\n const update = React.useCallback(\n (mutator: (prev: AiChatSessionsState) => AiChatSessionsState) => {\n setState((prev) => mutator(prev))\n },\n [],\n )\n\n const getOpenSessions = React.useCallback(\n (agentId: string) =>\n state.sessions\n .filter((s) => s.agentId === agentId && s.status === 'open')\n .sort((a, b) => a.createdAt - b.createdAt),\n [state.sessions],\n )\n\n const getClosedSessions = React.useCallback(\n (agentId: string, limit = HISTORY_LIMIT) =>\n state.sessions\n .filter((s) => s.agentId === agentId && s.status === 'closed')\n .sort((a, b) => b.lastUsedAt - a.lastUsedAt)\n .slice(0, limit),\n [state.sessions],\n )\n\n const getActiveSession = React.useCallback(\n (agentId: string) => {\n const id = state.activeByAgent[agentId]\n if (!id) return null\n const session = state.sessions.find((s) => s.id === id)\n if (!session || session.status !== 'open') return null\n return session\n },\n [state.activeByAgent, state.sessions],\n )\n\n const createSession = React.useCallback((agentId: string): AiChatSession => {\n const session: AiChatSession = {\n id: makeId(),\n agentId,\n conversationId: makeId(),\n createdAt: Date.now(),\n lastUsedAt: Date.now(),\n status: 'open',\n }\n update((prev) => ({\n sessions: [...prev.sessions, session],\n activeByAgent: { ...prev.activeByAgent, [agentId]: session.id },\n }))\n return session\n }, [update])\n\n const closeSession = React.useCallback(\n (sessionId: string) => {\n update((prev) => {\n const target = prev.sessions.find((s) => s.id === sessionId)\n if (!target) return prev\n const sessions = prev.sessions.map((s) =>\n s.id === sessionId ? { ...s, status: 'closed' as const, lastUsedAt: Date.now() } : s,\n )\n const activeByAgent = { ...prev.activeByAgent }\n if (activeByAgent[target.agentId] === sessionId) {\n // Pick the next open session for this agent (most-recent first).\n const fallback = sessions\n .filter((s) => s.agentId === target.agentId && s.status === 'open')\n .sort((a, b) => b.lastUsedAt - a.lastUsedAt)[0]\n if (fallback) {\n activeByAgent[target.agentId] = fallback.id\n } else {\n delete activeByAgent[target.agentId]\n }\n }\n return { sessions, activeByAgent }\n })\n },\n [update],\n )\n\n const reopenSession = React.useCallback(\n (sessionId: string) => {\n update((prev) => {\n const target = prev.sessions.find((s) => s.id === sessionId)\n if (!target) return prev\n const sessions = prev.sessions.map((s) =>\n s.id === sessionId ? { ...s, status: 'open' as const, lastUsedAt: Date.now() } : s,\n )\n return {\n sessions,\n activeByAgent: { ...prev.activeByAgent, [target.agentId]: sessionId },\n }\n })\n },\n [update],\n )\n\n const setActiveSession = React.useCallback(\n (sessionId: string) => {\n update((prev) => {\n const target = prev.sessions.find((s) => s.id === sessionId)\n if (!target || target.status !== 'open') return prev\n const sessions = prev.sessions.map((s) =>\n s.id === sessionId ? { ...s, lastUsedAt: Date.now() } : s,\n )\n return {\n sessions,\n activeByAgent: { ...prev.activeByAgent, [target.agentId]: sessionId },\n }\n })\n },\n [update],\n )\n\n const renameSession = React.useCallback(\n (sessionId: string, name: string) => {\n const trimmed = name.trim()\n update((prev) => ({\n ...prev,\n sessions: prev.sessions.map((s) =>\n s.id === sessionId\n ? { ...s, name: trimmed.length > 0 ? trimmed : undefined }\n : s,\n ),\n }))\n },\n [update],\n )\n\n const touchSession = React.useCallback(\n (sessionId: string) => {\n update((prev) => ({\n ...prev,\n sessions: prev.sessions.map((s) =>\n s.id === sessionId ? { ...s, lastUsedAt: Date.now() } : s,\n ),\n }))\n },\n [update],\n )\n\n const ensureSession = React.useCallback(\n (agentId: string): AiChatSession => {\n // Everything happens inside a single functional setState so we always\n // see the latest pending state (not a stale closure). React Strict\n // Mode double-invokes both effects AND setState updaters in dev,\n // which previously caused the auto-bootstrap path to mint two\n // sessions on first chat-pane open. The `mintedSession` closure\n // cache makes the updater itself idempotent across double-invokes\n // (one fresh id per `ensureSession` call, regardless of how many\n // times React replays the updater for purity testing); the\n // functional `prev` lookup handles the case where two queued\n // ensureSession calls resolve back-to-back \u2014 the second one sees\n // the first's pending append and short-circuits.\n let resolved: AiChatSession | null = null\n let mintedSession: AiChatSession | null = null\n setState((prev) => {\n const activeId = prev.activeByAgent[agentId]\n if (activeId) {\n const active = prev.sessions.find((s) => s.id === activeId)\n if (active && active.status === 'open') {\n resolved = active\n return prev\n }\n }\n const anyOpen = prev.sessions\n .filter((s) => s.agentId === agentId && s.status === 'open')\n .sort((a, b) => b.lastUsedAt - a.lastUsedAt)[0]\n if (anyOpen) {\n resolved = anyOpen\n return {\n ...prev,\n activeByAgent: { ...prev.activeByAgent, [agentId]: anyOpen.id },\n }\n }\n if (!mintedSession) {\n mintedSession = {\n id: makeId(),\n agentId,\n conversationId: makeId(),\n createdAt: Date.now(),\n lastUsedAt: Date.now(),\n status: 'open',\n }\n }\n resolved = mintedSession\n return {\n sessions: [...prev.sessions, mintedSession],\n activeByAgent: { ...prev.activeByAgent, [agentId]: mintedSession.id },\n }\n })\n // `setState` returns synchronously after invoking the updater (twice\n // in Strict Mode dev), so `resolved` is guaranteed to be set here.\n return resolved as unknown as AiChatSession\n },\n [],\n )\n\n const api = React.useMemo<AiChatSessionsApi>(\n () => ({\n state,\n getOpenSessions,\n getClosedSessions,\n getActiveSession,\n createSession,\n closeSession,\n reopenSession,\n setActiveSession,\n renameSession,\n touchSession,\n ensureSession,\n }),\n [\n state,\n getOpenSessions,\n getClosedSessions,\n getActiveSession,\n createSession,\n closeSession,\n reopenSession,\n setActiveSession,\n renameSession,\n touchSession,\n ensureSession,\n ],\n )\n\n return <AiChatSessionsContext.Provider value={api}>{children}</AiChatSessionsContext.Provider>\n}\n\nexport function useAiChatSessions(): AiChatSessionsApi {\n const ctx = React.useContext(AiChatSessionsContext)\n if (ctx) return ctx\n // Fallback no-op API \u2014 keeps consumers safe when the provider is absent\n // (legacy code paths, isolated unit tests). Every method is a no-op so a\n // chat surface without the provider behaves like a single anonymous\n // session (the behavior shipped before the multi-tab work).\n return {\n state: { sessions: [], activeByAgent: {} },\n getOpenSessions: () => [],\n getClosedSessions: () => [],\n getActiveSession: () => null,\n createSession: (agentId) => ({\n id: 'noop',\n agentId,\n conversationId: 'noop',\n createdAt: Date.now(),\n lastUsedAt: Date.now(),\n status: 'open',\n }),\n closeSession: () => {},\n reopenSession: () => {},\n setActiveSession: () => {},\n renameSession: () => {},\n touchSession: () => {},\n ensureSession: (agentId) => ({\n id: 'noop',\n agentId,\n conversationId: 'noop',\n createdAt: Date.now(),\n lastUsedAt: Date.now(),\n status: 'open',\n }),\n }\n}\n\nexport function defaultSessionLabel(session: AiChatSession): string {\n if (session.name && session.name.trim().length > 0) return session.name.trim()\n const date = new Date(session.createdAt)\n if (Number.isNaN(date.getTime())) return 'Session'\n return date.toLocaleString(undefined, {\n month: 'short',\n day: 'numeric',\n hour: '2-digit',\n minute: '2-digit',\n })\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\n/**\n * Multi-tab AI chat sessions.\n *\n * Each agent can have several concurrent conversation threads (sessions).\n * The provider mirrors session metadata from the server-side conversation\n * store. localStorage remains a backward-compatible cache so existing browser\n * sessions can be imported rather than orphaned, but the server is the source\n * of truth whenever it is reachable.\n *\n * Sessions are partitioned into:\n * - `open` \u2192 currently shown in the tab strip\n * - `closed` \u2192 hidden but kept for the history dropdown\n *\n * Closing a tab moves it to `closed`. Re-opening from history flips it back\n * to `open` and surfaces it as the active tab. Renaming is a single\n * `name` field; falling back to the formatted creation date when the user\n * never named the session keeps the picker scannable.\n */\n\nimport * as React from 'react'\nimport {\n createAiServerConversation,\n listAiServerConversations,\n updateAiServerConversation,\n type AiServerConversation,\n} from './conversation-store'\n\nconst STORAGE_KEY = 'om-ai-chat-sessions-v1'\nconst HISTORY_LIMIT = 50\n\nexport type AiChatSessionStatus = 'open' | 'closed'\n\nexport interface AiChatSession {\n id: string\n agentId: string\n conversationId: string\n name?: string\n createdAt: number\n lastUsedAt: number\n status: AiChatSessionStatus\n}\n\ninterface AiChatSessionsState {\n sessions: AiChatSession[]\n activeByAgent: Record<string, string>\n}\n\ninterface AiChatSessionsApi {\n state: AiChatSessionsState\n /** Returns open sessions for `agentId`, ordered by creation. */\n getOpenSessions: (agentId: string) => AiChatSession[]\n /** Returns closed sessions (history) for `agentId`, newest first. */\n getClosedSessions: (agentId: string, limit?: number) => AiChatSession[]\n /** Returns the active session for `agentId`, or null if none. */\n getActiveSession: (agentId: string) => AiChatSession | null\n /** Creates and activates a fresh open session for `agentId`. */\n createSession: (agentId: string) => AiChatSession\n /** Closes a session (moves to history). If it was active, picks another. */\n closeSession: (sessionId: string) => void\n /** Re-opens a closed session and activates it. */\n reopenSession: (sessionId: string) => void\n /** Activates an open session. */\n setActiveSession: (sessionId: string) => void\n /** Renames a session \u2014 empty / whitespace clears the custom name. */\n renameSession: (sessionId: string, name: string) => void\n /** Bumps `lastUsedAt` on a session (call when activity happens). */\n touchSession: (sessionId: string) => void\n /** Ensures `agentId` has at least one open session and returns its id. */\n ensureSession: (agentId: string) => AiChatSession\n}\n\nconst AiChatSessionsContext = React.createContext<AiChatSessionsApi | null>(null)\n\nfunction makeId(): string {\n const g = globalThis as unknown as { crypto?: { randomUUID?: () => string } }\n if (g.crypto && typeof g.crypto.randomUUID === 'function') {\n try {\n return g.crypto.randomUUID()\n } catch {\n /* fall through */\n }\n }\n const rand = () => Math.random().toString(16).slice(2, 10)\n return `${Date.now().toString(16)}-${rand()}-${rand()}`\n}\n\nfunction readPersisted(): AiChatSessionsState {\n if (typeof window === 'undefined') return { sessions: [], activeByAgent: {} }\n try {\n const raw = window.localStorage.getItem(STORAGE_KEY)\n if (!raw) return { sessions: [], activeByAgent: {} }\n const parsed = JSON.parse(raw) as Partial<AiChatSessionsState> | null\n const sessions = Array.isArray(parsed?.sessions)\n ? (parsed!.sessions as unknown[])\n .filter((entry): entry is AiChatSession => {\n if (!entry || typeof entry !== 'object') return false\n const value = entry as Record<string, unknown>\n return (\n typeof value.id === 'string' &&\n typeof value.agentId === 'string' &&\n typeof value.conversationId === 'string' &&\n typeof value.createdAt === 'number' &&\n typeof value.lastUsedAt === 'number' &&\n (value.status === 'open' || value.status === 'closed')\n )\n })\n .map((entry) => {\n const value = entry as unknown as Record<string, unknown>\n const candidate = value.name\n return {\n ...entry,\n name: typeof candidate === 'string' ? candidate : undefined,\n }\n })\n : []\n const activeByAgent =\n parsed?.activeByAgent && typeof parsed.activeByAgent === 'object'\n ? Object.fromEntries(\n Object.entries(parsed.activeByAgent as Record<string, unknown>).filter(\n (entry): entry is [string, string] =>\n typeof entry[0] === 'string' && typeof entry[1] === 'string',\n ),\n )\n : {}\n return { sessions, activeByAgent }\n } catch {\n return { sessions: [], activeByAgent: {} }\n }\n}\n\nfunction writePersisted(state: AiChatSessionsState): void {\n if (typeof window === 'undefined') return\n try {\n window.localStorage.setItem(STORAGE_KEY, JSON.stringify(state))\n } catch {\n /* quota / privacy mode \u2014 drop silently */\n }\n}\n\nfunction serverConversationToSession(\n conversation: AiServerConversation,\n existing?: AiChatSession,\n): AiChatSession {\n const createdAt = Date.parse(conversation.createdAt)\n const updatedAt = Date.parse(conversation.lastMessageAt ?? conversation.updatedAt)\n return {\n id: existing?.id ?? conversation.conversationId,\n agentId: conversation.agentId,\n conversationId: conversation.conversationId,\n name: conversation.title ?? existing?.name,\n createdAt: Number.isFinite(createdAt) ? createdAt : existing?.createdAt ?? Date.now(),\n lastUsedAt: Number.isFinite(updatedAt) ? updatedAt : existing?.lastUsedAt ?? Date.now(),\n status: conversation.status,\n }\n}\n\nfunction mergeServerConversations(\n prev: AiChatSessionsState,\n conversations: AiServerConversation[],\n): AiChatSessionsState {\n const byConversationId = new Map(prev.sessions.map((session) => [session.conversationId, session]))\n const serverSessions = conversations.map((conversation) =>\n serverConversationToSession(conversation, byConversationId.get(conversation.conversationId)),\n )\n const serverIds = new Set(serverSessions.map((session) => session.conversationId))\n const localOnly = prev.sessions.filter((session) => !serverIds.has(session.conversationId))\n const sessions = [...serverSessions, ...localOnly]\n const activeByAgent = { ...prev.activeByAgent }\n const agentIds = new Set(sessions.map((session) => session.agentId))\n for (const agentId of agentIds) {\n const activeId = activeByAgent[agentId]\n const active = sessions.find((session) => session.id === activeId)\n if (active && active.status === 'open') continue\n const nextOpen = sessions\n .filter((session) => session.agentId === agentId && session.status === 'open')\n .sort((a, b) => b.lastUsedAt - a.lastUsedAt)[0]\n if (nextOpen) activeByAgent[agentId] = nextOpen.id\n else delete activeByAgent[agentId]\n }\n return { sessions, activeByAgent }\n}\n\nexport function AiChatSessionsProvider({ children }: { children: React.ReactNode }) {\n // Hydrate synchronously via a lazy initializer. The previous \"empty\n // state + post-mount load effect\" pattern had a window where the\n // persistence effect ran with the empty closure value (because the\n // hydrate effect's queued setState had not committed yet) and clobbered\n // localStorage with `[]` before the loaded state's re-render wrote it\n // back. The lazy initializer puts the loaded state into the very first\n // render, so the persistence effect always sees the real data.\n // `readPersisted` already short-circuits to an empty object on the\n // server (no `window`), so SSR stays consistent with the bare-bones\n // shell \u2014 actual session-dependent UI only renders after a user\n // interaction opens a chat surface.\n const [state, setState] = React.useState<AiChatSessionsState>(() => readPersisted())\n\n React.useEffect(() => {\n writePersisted(state)\n }, [state])\n\n React.useEffect(() => {\n let cancelled = false\n void listAiServerConversations({ limit: 100 }).then((conversations) => {\n if (cancelled || !conversations) return\n setState((prev) => mergeServerConversations(prev, conversations))\n })\n return () => {\n cancelled = true\n }\n }, [])\n\n const update = React.useCallback(\n (mutator: (prev: AiChatSessionsState) => AiChatSessionsState) => {\n setState((prev) => mutator(prev))\n },\n [],\n )\n\n const getOpenSessions = React.useCallback(\n (agentId: string) =>\n state.sessions\n .filter((s) => s.agentId === agentId && s.status === 'open')\n .sort((a, b) => a.createdAt - b.createdAt),\n [state.sessions],\n )\n\n const getClosedSessions = React.useCallback(\n (agentId: string, limit = HISTORY_LIMIT) =>\n state.sessions\n .filter((s) => s.agentId === agentId && s.status === 'closed')\n .sort((a, b) => b.lastUsedAt - a.lastUsedAt)\n .slice(0, limit),\n [state.sessions],\n )\n\n const getActiveSession = React.useCallback(\n (agentId: string) => {\n const id = state.activeByAgent[agentId]\n if (!id) return null\n const session = state.sessions.find((s) => s.id === id)\n if (!session || session.status !== 'open') return null\n return session\n },\n [state.activeByAgent, state.sessions],\n )\n\n const createSession = React.useCallback((agentId: string): AiChatSession => {\n const session: AiChatSession = {\n id: makeId(),\n agentId,\n conversationId: makeId(),\n createdAt: Date.now(),\n lastUsedAt: Date.now(),\n status: 'open',\n }\n void createAiServerConversation({\n agentId: session.agentId,\n conversationId: session.conversationId,\n })\n update((prev) => ({\n sessions: [...prev.sessions, session],\n activeByAgent: { ...prev.activeByAgent, [agentId]: session.id },\n }))\n return session\n }, [update])\n\n const closeSession = React.useCallback(\n (sessionId: string) => {\n const serverTarget = state.sessions.find((s) => s.id === sessionId)\n if (serverTarget) {\n void updateAiServerConversation(serverTarget.conversationId, { status: 'closed' })\n }\n update((prev) => {\n const target = prev.sessions.find((s) => s.id === sessionId)\n if (!target) return prev\n const sessions = prev.sessions.map((s) =>\n s.id === sessionId ? { ...s, status: 'closed' as const, lastUsedAt: Date.now() } : s,\n )\n const activeByAgent = { ...prev.activeByAgent }\n if (activeByAgent[target.agentId] === sessionId) {\n // Pick the next open session for this agent (most-recent first).\n const fallback = sessions\n .filter((s) => s.agentId === target.agentId && s.status === 'open')\n .sort((a, b) => b.lastUsedAt - a.lastUsedAt)[0]\n if (fallback) {\n activeByAgent[target.agentId] = fallback.id\n } else {\n delete activeByAgent[target.agentId]\n }\n }\n return { sessions, activeByAgent }\n })\n },\n [state.sessions, update],\n )\n\n const reopenSession = React.useCallback(\n (sessionId: string) => {\n const serverTarget = state.sessions.find((s) => s.id === sessionId)\n if (serverTarget) {\n void updateAiServerConversation(serverTarget.conversationId, { status: 'open' })\n }\n update((prev) => {\n const target = prev.sessions.find((s) => s.id === sessionId)\n if (!target) return prev\n const sessions = prev.sessions.map((s) =>\n s.id === sessionId ? { ...s, status: 'open' as const, lastUsedAt: Date.now() } : s,\n )\n return {\n sessions,\n activeByAgent: { ...prev.activeByAgent, [target.agentId]: sessionId },\n }\n })\n },\n [state.sessions, update],\n )\n\n const setActiveSession = React.useCallback(\n (sessionId: string) => {\n update((prev) => {\n const target = prev.sessions.find((s) => s.id === sessionId)\n if (!target || target.status !== 'open') return prev\n const sessions = prev.sessions.map((s) =>\n s.id === sessionId ? { ...s, lastUsedAt: Date.now() } : s,\n )\n return {\n sessions,\n activeByAgent: { ...prev.activeByAgent, [target.agentId]: sessionId },\n }\n })\n },\n [update],\n )\n\n const renameSession = React.useCallback(\n (sessionId: string, name: string) => {\n const trimmed = name.trim()\n const serverTarget = state.sessions.find((s) => s.id === sessionId)\n if (serverTarget) {\n void updateAiServerConversation(serverTarget.conversationId, {\n title: trimmed.length > 0 ? trimmed : null,\n })\n }\n update((prev) => ({\n ...prev,\n sessions: prev.sessions.map((s) =>\n s.id === sessionId\n ? { ...s, name: trimmed.length > 0 ? trimmed : undefined }\n : s,\n ),\n }))\n },\n [state.sessions, update],\n )\n\n const touchSession = React.useCallback(\n (sessionId: string) => {\n update((prev) => ({\n ...prev,\n sessions: prev.sessions.map((s) =>\n s.id === sessionId ? { ...s, lastUsedAt: Date.now() } : s,\n ),\n }))\n },\n [update],\n )\n\n const ensureSession = React.useCallback(\n (agentId: string): AiChatSession => {\n // Everything happens inside a single functional setState so we always\n // see the latest pending state (not a stale closure). React Strict\n // Mode double-invokes both effects AND setState updaters in dev,\n // which previously caused the auto-bootstrap path to mint two\n // sessions on first chat-pane open. The `mintedSession` closure\n // cache makes the updater itself idempotent across double-invokes\n // (one fresh id per `ensureSession` call, regardless of how many\n // times React replays the updater for purity testing); the\n // functional `prev` lookup handles the case where two queued\n // ensureSession calls resolve back-to-back \u2014 the second one sees\n // the first's pending append and short-circuits.\n let resolved: AiChatSession | null = null\n let mintedSession: AiChatSession | null = null\n setState((prev) => {\n const activeId = prev.activeByAgent[agentId]\n if (activeId) {\n const active = prev.sessions.find((s) => s.id === activeId)\n if (active && active.status === 'open') {\n resolved = active\n return prev\n }\n }\n const anyOpen = prev.sessions\n .filter((s) => s.agentId === agentId && s.status === 'open')\n .sort((a, b) => b.lastUsedAt - a.lastUsedAt)[0]\n if (anyOpen) {\n resolved = anyOpen\n return {\n ...prev,\n activeByAgent: { ...prev.activeByAgent, [agentId]: anyOpen.id },\n }\n }\n if (!mintedSession) {\n mintedSession = {\n id: makeId(),\n agentId,\n conversationId: makeId(),\n createdAt: Date.now(),\n lastUsedAt: Date.now(),\n status: 'open',\n }\n void createAiServerConversation({\n agentId: mintedSession.agentId,\n conversationId: mintedSession.conversationId,\n })\n }\n resolved = mintedSession\n return {\n sessions: [...prev.sessions, mintedSession],\n activeByAgent: { ...prev.activeByAgent, [agentId]: mintedSession.id },\n }\n })\n // `setState` returns synchronously after invoking the updater (twice\n // in Strict Mode dev), so `resolved` is guaranteed to be set here.\n return resolved as unknown as AiChatSession\n },\n [],\n )\n\n const api = React.useMemo<AiChatSessionsApi>(\n () => ({\n state,\n getOpenSessions,\n getClosedSessions,\n getActiveSession,\n createSession,\n closeSession,\n reopenSession,\n setActiveSession,\n renameSession,\n touchSession,\n ensureSession,\n }),\n [\n state,\n getOpenSessions,\n getClosedSessions,\n getActiveSession,\n createSession,\n closeSession,\n reopenSession,\n setActiveSession,\n renameSession,\n touchSession,\n ensureSession,\n ],\n )\n\n return <AiChatSessionsContext.Provider value={api}>{children}</AiChatSessionsContext.Provider>\n}\n\nexport function useAiChatSessions(): AiChatSessionsApi {\n const ctx = React.useContext(AiChatSessionsContext)\n if (ctx) return ctx\n // Fallback no-op API \u2014 keeps consumers safe when the provider is absent\n // (legacy code paths, isolated unit tests). Every method is a no-op so a\n // chat surface without the provider behaves like a single anonymous\n // session (the behavior shipped before the multi-tab work).\n return {\n state: { sessions: [], activeByAgent: {} },\n getOpenSessions: () => [],\n getClosedSessions: () => [],\n getActiveSession: () => null,\n createSession: (agentId) => ({\n id: 'noop',\n agentId,\n conversationId: 'noop',\n createdAt: Date.now(),\n lastUsedAt: Date.now(),\n status: 'open',\n }),\n closeSession: () => {},\n reopenSession: () => {},\n setActiveSession: () => {},\n renameSession: () => {},\n touchSession: () => {},\n ensureSession: (agentId) => ({\n id: 'noop',\n agentId,\n conversationId: 'noop',\n createdAt: Date.now(),\n lastUsedAt: Date.now(),\n status: 'open',\n }),\n }\n}\n\nexport function defaultSessionLabel(session: AiChatSession): string {\n if (session.name && session.name.trim().length > 0) return session.name.trim()\n const date = new Date(session.createdAt)\n if (Number.isNaN(date.getTime())) return 'Session'\n return date.toLocaleString(undefined, {\n month: 'short',\n day: 'numeric',\n hour: '2-digit',\n minute: '2-digit',\n })\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
const CONVERSATIONS_ENDPOINT = "/api/ai_assistant/ai/conversations";
|
|
3
|
+
function readString(value) {
|
|
4
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
5
|
+
}
|
|
6
|
+
function buildAttachmentImagePreviewUrl(attachmentId) {
|
|
7
|
+
const params = new URLSearchParams({
|
|
8
|
+
width: "320",
|
|
9
|
+
height: "320",
|
|
10
|
+
cropType: "contain"
|
|
11
|
+
});
|
|
12
|
+
return `/api/attachments/image/${encodeURIComponent(attachmentId)}?${params.toString()}`;
|
|
13
|
+
}
|
|
14
|
+
function getFetch() {
|
|
15
|
+
const fetchImpl = globalThis.fetch;
|
|
16
|
+
return typeof fetchImpl === "function" ? fetchImpl.bind(globalThis) : null;
|
|
17
|
+
}
|
|
18
|
+
async function requestJson(input, init) {
|
|
19
|
+
const fetchImpl = getFetch();
|
|
20
|
+
if (!fetchImpl) return { ok: false, status: 0, data: null };
|
|
21
|
+
try {
|
|
22
|
+
const response = await fetchImpl(input, {
|
|
23
|
+
credentials: "include",
|
|
24
|
+
...init,
|
|
25
|
+
headers: {
|
|
26
|
+
Accept: "application/json",
|
|
27
|
+
"x-om-forbidden-redirect": "0",
|
|
28
|
+
"x-om-unauthorized-redirect": "0",
|
|
29
|
+
...init?.body ? { "Content-Type": "application/json" } : {},
|
|
30
|
+
...init?.headers ?? {}
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
if (!response.ok) return { ok: false, status: response.status, data: null };
|
|
34
|
+
return { ok: true, status: response.status, data: await response.json() };
|
|
35
|
+
} catch {
|
|
36
|
+
return { ok: false, status: 0, data: null };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
async function listAiServerConversations(options = {}) {
|
|
40
|
+
const params = new URLSearchParams();
|
|
41
|
+
if (options.agentId) params.set("agent", options.agentId);
|
|
42
|
+
if (options.status) params.set("status", options.status);
|
|
43
|
+
params.set("limit", String(options.limit ?? 100));
|
|
44
|
+
const result = await requestJson(
|
|
45
|
+
`${CONVERSATIONS_ENDPOINT}?${params.toString()}`
|
|
46
|
+
);
|
|
47
|
+
return result.ok ? result.data.items : null;
|
|
48
|
+
}
|
|
49
|
+
async function createAiServerConversation(input) {
|
|
50
|
+
const result = await requestJson(CONVERSATIONS_ENDPOINT, {
|
|
51
|
+
method: "POST",
|
|
52
|
+
body: JSON.stringify({
|
|
53
|
+
agentId: input.agentId,
|
|
54
|
+
conversationId: input.conversationId,
|
|
55
|
+
title: input.title ?? void 0,
|
|
56
|
+
pageContext: input.pageContext ?? void 0
|
|
57
|
+
})
|
|
58
|
+
});
|
|
59
|
+
return result.ok ? result.data : null;
|
|
60
|
+
}
|
|
61
|
+
async function updateAiServerConversation(conversationId, patch) {
|
|
62
|
+
const result = await requestJson(
|
|
63
|
+
`${CONVERSATIONS_ENDPOINT}/${encodeURIComponent(conversationId)}`,
|
|
64
|
+
{
|
|
65
|
+
method: "PATCH",
|
|
66
|
+
body: JSON.stringify(patch)
|
|
67
|
+
}
|
|
68
|
+
);
|
|
69
|
+
return result.ok ? result.data : null;
|
|
70
|
+
}
|
|
71
|
+
async function loadAiServerTranscript(conversationId, options = {}) {
|
|
72
|
+
const params = new URLSearchParams();
|
|
73
|
+
params.set("limit", String(options.limit ?? 100));
|
|
74
|
+
const result = await requestJson(
|
|
75
|
+
`${CONVERSATIONS_ENDPOINT}/${encodeURIComponent(conversationId)}?${params.toString()}`
|
|
76
|
+
);
|
|
77
|
+
return result.ok ? result.data : null;
|
|
78
|
+
}
|
|
79
|
+
async function importAiLocalConversation(input) {
|
|
80
|
+
const result = await requestJson(`${CONVERSATIONS_ENDPOINT}/import`, {
|
|
81
|
+
method: "POST",
|
|
82
|
+
body: JSON.stringify({
|
|
83
|
+
conversation: {
|
|
84
|
+
conversationId: input.conversationId,
|
|
85
|
+
agentId: input.agentId,
|
|
86
|
+
title: input.title ?? void 0,
|
|
87
|
+
status: input.status ?? "open",
|
|
88
|
+
pageContext: input.pageContext ?? void 0
|
|
89
|
+
},
|
|
90
|
+
messages: input.messages.slice(-100).map((message) => ({
|
|
91
|
+
clientMessageId: message.id,
|
|
92
|
+
role: message.role,
|
|
93
|
+
content: message.content,
|
|
94
|
+
uiParts: message.uiParts ?? [],
|
|
95
|
+
files: (message.files ?? []).map((file) => ({
|
|
96
|
+
id: file.id,
|
|
97
|
+
name: file.name,
|
|
98
|
+
mimeType: file.type
|
|
99
|
+
}))
|
|
100
|
+
}))
|
|
101
|
+
})
|
|
102
|
+
});
|
|
103
|
+
return result.ok ? result.data : null;
|
|
104
|
+
}
|
|
105
|
+
function serverMessageToChatMessage(message) {
|
|
106
|
+
if (message.role !== "user" && message.role !== "assistant") return null;
|
|
107
|
+
const uiParts = [];
|
|
108
|
+
message.uiParts.forEach((part, index) => {
|
|
109
|
+
if (!part || typeof part !== "object") return;
|
|
110
|
+
const value = part;
|
|
111
|
+
const componentId = typeof value.componentId === "string" ? value.componentId : typeof value.type === "string" ? value.type : null;
|
|
112
|
+
if (!componentId) return;
|
|
113
|
+
uiParts.push({
|
|
114
|
+
componentId,
|
|
115
|
+
payload: value.payload ?? value.props,
|
|
116
|
+
pendingActionId: typeof value.pendingActionId === "string" ? value.pendingActionId : void 0,
|
|
117
|
+
key: typeof value.key === "string" ? value.key : `${message.id}:${index}:${componentId}`
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
return {
|
|
121
|
+
id: message.clientMessageId ?? message.id,
|
|
122
|
+
role: message.role,
|
|
123
|
+
content: message.content,
|
|
124
|
+
files: message.files.map((file, index) => {
|
|
125
|
+
const name = readString(file.name);
|
|
126
|
+
if (!name) return null;
|
|
127
|
+
const type = typeof file.mimeType === "string" ? file.mimeType : typeof file.type === "string" ? file.type : "application/octet-stream";
|
|
128
|
+
const id = readString(file.id) ?? readString(message.attachmentIds[index]);
|
|
129
|
+
const rawPreviewUrl = readString(file.previewUrl) ?? readString(file.thumbnailUrl) ?? readString(file.url);
|
|
130
|
+
const previewUrl = type.startsWith("image/") ? rawPreviewUrl ?? (id ? buildAttachmentImagePreviewUrl(id) : void 0) : void 0;
|
|
131
|
+
return {
|
|
132
|
+
...id ? { id } : {},
|
|
133
|
+
name,
|
|
134
|
+
type,
|
|
135
|
+
...previewUrl ? { previewUrl } : {}
|
|
136
|
+
};
|
|
137
|
+
}).filter((file) => file !== null),
|
|
138
|
+
uiParts
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
export {
|
|
142
|
+
createAiServerConversation,
|
|
143
|
+
importAiLocalConversation,
|
|
144
|
+
listAiServerConversations,
|
|
145
|
+
loadAiServerTranscript,
|
|
146
|
+
serverMessageToChatMessage,
|
|
147
|
+
updateAiServerConversation
|
|
148
|
+
};
|
|
149
|
+
//# sourceMappingURL=conversation-store.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/ai/conversation-store.ts"],
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport type { AiChatMessage, AiChatMessageUiPart } from './useAiChat'\n\nconst CONVERSATIONS_ENDPOINT = '/api/ai_assistant/ai/conversations'\n\nexport interface AiServerConversation {\n conversationId: string\n agentId: string\n title: string | null\n status: 'open' | 'closed'\n visibility: 'private' | 'shared' | 'organization'\n pageContext: Record<string, unknown> | null\n createdAt: string\n updatedAt: string\n lastMessageAt: string | null\n importedFromLocalAt: string | null\n}\n\nexport interface AiServerMessage {\n id: string\n clientMessageId: string | null\n role: 'user' | 'assistant' | 'system'\n content: string\n uiParts: unknown[]\n attachmentIds: string[]\n files: Array<Record<string, unknown>>\n model: string | null\n metadata: Record<string, unknown> | null\n createdAt: string\n}\n\nexport interface AiServerConversationListResponse {\n items: AiServerConversation[]\n nextCursor: string | null\n}\n\nexport interface AiServerTranscriptResponse {\n conversation: AiServerConversation\n messages: AiServerMessage[]\n nextCursor: string | null\n}\n\nexport interface AiServerImportResponse {\n conversation: AiServerConversation\n importedMessageCount: number\n skippedMessageCount: number\n}\n\nfunction readString(value: unknown): string | null {\n return typeof value === 'string' && value.length > 0 ? value : null\n}\n\nfunction buildAttachmentImagePreviewUrl(attachmentId: string): string {\n const params = new URLSearchParams({\n width: '320',\n height: '320',\n cropType: 'contain',\n })\n return `/api/attachments/image/${encodeURIComponent(attachmentId)}?${params.toString()}`\n}\n\nfunction getFetch(): typeof fetch | null {\n const fetchImpl = (globalThis as typeof globalThis & { fetch?: typeof fetch }).fetch\n return typeof fetchImpl === 'function' ? fetchImpl.bind(globalThis) : null\n}\n\nasync function requestJson<T>(\n input: string,\n init?: RequestInit,\n): Promise<{ ok: true; status: number; data: T } | { ok: false; status: number; data: null }> {\n const fetchImpl = getFetch()\n if (!fetchImpl) return { ok: false, status: 0, data: null }\n try {\n const response = await fetchImpl(input, {\n credentials: 'include',\n ...init,\n headers: {\n Accept: 'application/json',\n 'x-om-forbidden-redirect': '0',\n 'x-om-unauthorized-redirect': '0',\n ...(init?.body ? { 'Content-Type': 'application/json' } : {}),\n ...(init?.headers ?? {}),\n },\n })\n if (!response.ok) return { ok: false, status: response.status, data: null }\n return { ok: true, status: response.status, data: (await response.json()) as T }\n } catch {\n return { ok: false, status: 0, data: null }\n }\n}\n\nexport async function listAiServerConversations(options: {\n agentId?: string | null\n status?: 'open' | 'closed' | null\n limit?: number\n} = {}): Promise<AiServerConversation[] | null> {\n const params = new URLSearchParams()\n if (options.agentId) params.set('agent', options.agentId)\n if (options.status) params.set('status', options.status)\n params.set('limit', String(options.limit ?? 100))\n const result = await requestJson<AiServerConversationListResponse>(\n `${CONVERSATIONS_ENDPOINT}?${params.toString()}`,\n )\n return result.ok ? result.data.items : null\n}\n\nexport async function createAiServerConversation(input: {\n agentId: string\n conversationId: string\n title?: string | null\n pageContext?: Record<string, unknown> | null\n}): Promise<AiServerConversation | null> {\n const result = await requestJson<AiServerConversation>(CONVERSATIONS_ENDPOINT, {\n method: 'POST',\n body: JSON.stringify({\n agentId: input.agentId,\n conversationId: input.conversationId,\n title: input.title ?? undefined,\n pageContext: input.pageContext ?? undefined,\n }),\n })\n return result.ok ? result.data : null\n}\n\nexport async function updateAiServerConversation(\n conversationId: string,\n patch: {\n title?: string | null\n status?: 'open' | 'closed'\n pageContext?: Record<string, unknown> | null\n },\n): Promise<AiServerConversation | null> {\n const result = await requestJson<AiServerConversation>(\n `${CONVERSATIONS_ENDPOINT}/${encodeURIComponent(conversationId)}`,\n {\n method: 'PATCH',\n body: JSON.stringify(patch),\n },\n )\n return result.ok ? result.data : null\n}\n\nexport async function loadAiServerTranscript(\n conversationId: string,\n options: { limit?: number } = {},\n): Promise<AiServerTranscriptResponse | null> {\n const params = new URLSearchParams()\n params.set('limit', String(options.limit ?? 100))\n const result = await requestJson<AiServerTranscriptResponse>(\n `${CONVERSATIONS_ENDPOINT}/${encodeURIComponent(conversationId)}?${params.toString()}`,\n )\n return result.ok ? result.data : null\n}\n\nexport async function importAiLocalConversation(input: {\n agentId: string\n conversationId: string\n title?: string | null\n status?: 'open' | 'closed'\n pageContext?: Record<string, unknown> | null\n messages: AiChatMessage[]\n}): Promise<AiServerImportResponse | null> {\n const result = await requestJson<AiServerImportResponse>(`${CONVERSATIONS_ENDPOINT}/import`, {\n method: 'POST',\n body: JSON.stringify({\n conversation: {\n conversationId: input.conversationId,\n agentId: input.agentId,\n title: input.title ?? undefined,\n status: input.status ?? 'open',\n pageContext: input.pageContext ?? undefined,\n },\n messages: input.messages.slice(-100).map((message) => ({\n clientMessageId: message.id,\n role: message.role,\n content: message.content,\n uiParts: message.uiParts ?? [],\n files: (message.files ?? []).map((file) => ({\n id: file.id,\n name: file.name,\n mimeType: file.type,\n })),\n })),\n }),\n })\n return result.ok ? result.data : null\n}\n\nexport function serverMessageToChatMessage(message: AiServerMessage): AiChatMessage | null {\n if (message.role !== 'user' && message.role !== 'assistant') return null\n const uiParts: AiChatMessageUiPart[] = []\n message.uiParts.forEach((part, index) => {\n if (!part || typeof part !== 'object') return\n const value = part as Record<string, unknown>\n const componentId =\n typeof value.componentId === 'string'\n ? value.componentId\n : typeof value.type === 'string'\n ? value.type\n : null\n if (!componentId) return\n uiParts.push({\n componentId,\n payload: value.payload ?? value.props,\n pendingActionId:\n typeof value.pendingActionId === 'string' ? value.pendingActionId : undefined,\n key:\n typeof value.key === 'string'\n ? value.key\n : `${message.id}:${index}:${componentId}`,\n })\n })\n return {\n id: message.clientMessageId ?? message.id,\n role: message.role,\n content: message.content,\n files: message.files\n .map((file, index) => {\n const name = readString(file.name)\n if (!name) return null\n const type =\n typeof file.mimeType === 'string'\n ? file.mimeType\n : typeof file.type === 'string'\n ? file.type\n : 'application/octet-stream'\n const id = readString(file.id) ?? readString(message.attachmentIds[index])\n const rawPreviewUrl =\n readString(file.previewUrl) ??\n readString(file.thumbnailUrl) ??\n readString(file.url)\n const previewUrl = type.startsWith('image/')\n ? rawPreviewUrl ?? (id ? buildAttachmentImagePreviewUrl(id) : undefined)\n : undefined\n return {\n ...(id ? { id } : {}),\n name,\n type,\n ...(previewUrl ? { previewUrl } : {}),\n }\n })\n .filter((file): file is NonNullable<AiChatMessage['files']>[number] => file !== null),\n uiParts,\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -107,6 +107,7 @@ function MutationPreviewCard(props) {
|
|
|
107
107
|
}
|
|
108
108
|
const batch = Array.isArray(action?.records) && action.records.length > 0 ? action.records : null;
|
|
109
109
|
const summary = batch ? summarizeBatch(batch) : null;
|
|
110
|
+
const hasReviewDetails = Boolean(batch);
|
|
110
111
|
return /* @__PURE__ */ jsxs(
|
|
111
112
|
"section",
|
|
112
113
|
{
|
|
@@ -152,7 +153,7 @@ function MutationPreviewCard(props) {
|
|
|
152
153
|
]
|
|
153
154
|
}
|
|
154
155
|
) : /* @__PURE__ */ jsx(FieldDiffCard, { fieldDiff: action?.fieldDiff ?? null }) }),
|
|
155
|
-
expanded ? /* @__PURE__ */ jsx("div", { className: "mt-3 rounded-md border border-border bg-muted/20 p-3", "data-ai-mutation-preview-details": true, children: /* @__PURE__ */ jsx(
|
|
156
|
+
expanded && hasReviewDetails ? /* @__PURE__ */ jsx("div", { className: "mt-3 rounded-md border border-border bg-muted/20 p-3", "data-ai-mutation-preview-details": true, children: /* @__PURE__ */ jsx(
|
|
156
157
|
FieldDiffCard,
|
|
157
158
|
{
|
|
158
159
|
fieldDiff: action?.fieldDiff ?? null,
|
|
@@ -160,7 +161,7 @@ function MutationPreviewCard(props) {
|
|
|
160
161
|
}
|
|
161
162
|
) }) : null,
|
|
162
163
|
/* @__PURE__ */ jsxs("div", { className: "mt-3 flex items-center justify-between gap-2", children: [
|
|
163
|
-
/* @__PURE__ */ jsxs(
|
|
164
|
+
hasReviewDetails ? /* @__PURE__ */ jsxs(
|
|
164
165
|
Button,
|
|
165
166
|
{
|
|
166
167
|
type: "button",
|
|
@@ -183,7 +184,7 @@ function MutationPreviewCard(props) {
|
|
|
183
184
|
)
|
|
184
185
|
]
|
|
185
186
|
}
|
|
186
|
-
),
|
|
187
|
+
) : /* @__PURE__ */ jsx("span", { "aria-hidden": true }),
|
|
187
188
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
188
189
|
/* @__PURE__ */ jsx(
|
|
189
190
|
Button,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/ai/parts/MutationPreviewCard.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { ChevronDown, Eye, ShieldAlert } from 'lucide-react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '../../primitives/button'\nimport { useAiShortcuts } from '../useAiShortcuts'\nimport type { AiUiPartProps } from '../ui-part-registry'\nimport { confirmPendingAction, cancelPendingAction } from './pending-action-api'\nimport { useAiPendingActionPolling } from './useAiPendingActionPolling'\nimport { FieldDiffCard } from './FieldDiffCard'\nimport { ConfirmationCard } from './ConfirmationCard'\nimport { MutationResultCard } from './MutationResultCard'\nimport type { AiPendingActionCardAction } from './types'\n\n/**\n * Mutation-approval preview card. Rendered by the server-emitted\n * `mutation-preview-card` UI part (spec \u00A79.2).\n *\n * Responsibilities:\n * - Fetch the current pending-action row via the shared polling hook so the\n * card recovers state on page reload (reconnect behavior).\n * - Render the top-level `fieldDiff` OR a per-record `records[]` summary\n * with a drill-in link.\n * - Provide `Confirm` / `Cancel` / `Review Details` actions with shared\n * keyboard shortcuts (`Cmd/Ctrl+Enter` \u2192 confirm, `Escape` \u2192 cancel).\n * - Flip to the {@link ConfirmationCard} once the user confirms, and\n * further to the {@link MutationResultCard} once the row becomes\n * terminal.\n */\nexport interface MutationPreviewCardPayload {\n /** Optional server-serialized pending action snapshot for the initial render. */\n pendingAction?: AiPendingActionCardAction\n}\n\nexport interface MutationPreviewCardProps extends AiUiPartProps {\n /** Optional injected action for tests \u2014 bypasses the polling fetch. */\n initialAction?: AiPendingActionCardAction\n /** Endpoint base override for tests. */\n endpoint?: string\n}\n\nfunction summarizeBatch(\n records: NonNullable<AiPendingActionCardAction['records']>,\n): { count: number; labels: string[] } {\n const labels = records.slice(0, 3).map((record) => record.label)\n return { count: records.length, labels }\n}\n\nexport function MutationPreviewCard(props: MutationPreviewCardProps) {\n const t = useT()\n const pendingActionId = props.pendingActionId ?? ''\n const payload = (props.payload as MutationPreviewCardPayload | undefined) ?? {}\n const injected = props.initialAction ?? payload.pendingAction ?? null\n\n const { action: polled, refresh } = useAiPendingActionPolling({\n pendingActionId,\n endpoint: props.endpoint,\n disabled: !pendingActionId,\n })\n const action = polled ?? injected\n\n const [expanded, setExpanded] = React.useState(false)\n const [phase, setPhase] = React.useState<'preview' | 'confirming'>('preview')\n const [confirmError, setConfirmError] = React.useState<{\n status: number\n code?: string\n message: string\n extra?: Record<string, unknown>\n } | null>(null)\n\n const handleConfirm = React.useCallback(async () => {\n if (!pendingActionId) return\n if (phase !== 'preview') return\n setPhase('confirming')\n setConfirmError(null)\n const result = await confirmPendingAction(pendingActionId, {\n endpoint: props.endpoint,\n })\n if (!result.ok) {\n // Network / timeout / 4xx / 5xx \u2014 surface the envelope and rewind the\n // card to the preview phase so the operator can read the error,\n // edit the proposal upstream, or retry.\n setConfirmError(result.error)\n setPhase('preview')\n await refresh()\n return\n }\n // HTTP 200 path. The dispatcher returns `ok: false` AND a populated\n // `mutationResult.error` when the wrapped tool handler failed inside\n // the confirm route \u2014 the row is already in a terminal state but the\n // overall HTTP call succeeded. Treat that as a confirm error too so\n // the alert renders inline instead of leaving the card on the\n // generic \"applying\u2026\" spinner forever.\n const handlerError = result.data?.mutationResult?.error\n if (result.data?.ok === false || handlerError) {\n const mappedCode =\n typeof handlerError?.code === 'string' && handlerError.code.length > 0\n ? handlerError.code\n : 'execution_failed'\n setConfirmError({\n status: 200,\n code: mappedCode,\n message:\n handlerError?.message ??\n t(\n 'ai_assistant.chat.mutation_cards.preview.handlerError',\n 'The mutation handler reported an error. Review the details and re-propose if needed.',\n ),\n })\n }\n await refresh()\n }, [pendingActionId, phase, props.endpoint, refresh, t])\n\n const handleCancel = React.useCallback(async () => {\n if (!pendingActionId) return\n const result = await cancelPendingAction(pendingActionId, {\n endpoint: props.endpoint,\n })\n if (!result.ok) {\n setConfirmError(result.error)\n }\n await refresh()\n }, [pendingActionId, props.endpoint, refresh])\n\n const currentStatus = action?.status ?? null\n const executionError = action?.executionResult?.error\n // Treat any captured handler error as terminal too \u2014 the dispatcher\n // sometimes returns the action before the row has fully transitioned\n // out of `executing`, and we must never leave the spinner masking a\n // real failure. The MutationResultCard's failure path renders the\n // error envelope as long as `executionResult.error` is set.\n const isTerminal =\n currentStatus === 'confirmed' ||\n currentStatus === 'failed' ||\n currentStatus === 'cancelled' ||\n currentStatus === 'expired' ||\n Boolean(executionError)\n\n const { handleKeyDown } = useAiShortcuts({\n onSubmit: () => {\n void handleConfirm()\n },\n onCancel: () => {\n void handleCancel()\n },\n enabled: phase === 'preview' && !isTerminal,\n })\n\n // Terminal \u2014 short-circuit into the result card.\n if (isTerminal && action) {\n return (\n <MutationResultCard\n componentId=\"mutation-result-card\"\n pendingActionId={pendingActionId}\n initialAction={action}\n endpoint={props.endpoint}\n />\n )\n }\n\n // Confirming \u2014 flip to the spinner card. Propagate the confirmError so\n // the user sees the structured envelope even though the confirm call has\n // already resolved.\n if (phase === 'confirming') {\n return (\n <ConfirmationCard\n componentId=\"confirmation-card\"\n pendingActionId={pendingActionId}\n initialAction={action ?? undefined}\n endpoint={props.endpoint}\n payload={{\n sideEffectsSummary: action?.sideEffectsSummary ?? null,\n pendingAction: action ?? undefined,\n confirmError: confirmError ?? undefined,\n }}\n />\n )\n }\n\n const batch = Array.isArray(action?.records) && action!.records!.length > 0 ? action!.records! : null\n const summary = batch ? summarizeBatch(batch) : null\n\n return (\n <section\n className=\"rounded-md border border-border bg-background p-4 text-sm outline-none\"\n tabIndex={0}\n onKeyDown={handleKeyDown}\n data-ai-mutation-preview\n data-ai-mutation-preview-mode={batch ? 'batch' : 'single'}\n >\n <header className=\"flex items-start justify-between gap-3\">\n <div className=\"flex items-start gap-2\">\n <ShieldAlert className=\"mt-0.5 size-4 text-status-warning-icon\" aria-hidden />\n <div>\n <h4 className=\"text-sm font-semibold\">\n {t(\n 'ai_assistant.chat.mutation_cards.preview.title',\n 'Review proposed changes',\n )}\n </h4>\n {action?.sideEffectsSummary ? (\n <p className=\"mt-1 text-sm text-muted-foreground\">\n {action.sideEffectsSummary}\n </p>\n ) : null}\n </div>\n </div>\n </header>\n\n <div className=\"mt-3\" data-ai-mutation-preview-body>\n {summary ? (\n <div\n className=\"rounded-md border border-border bg-muted/30 p-3 text-sm\"\n data-ai-mutation-preview-batch-summary\n >\n <p className=\"font-medium\">\n {t(\n 'ai_assistant.chat.mutation_cards.preview.batchSummary',\n 'Batch update',\n )}\n {': '}\n <span data-ai-mutation-preview-count>{summary.count}</span>{' '}\n <span>\n {t(\n 'ai_assistant.chat.mutation_cards.preview.batchRecords',\n 'records',\n )}\n </span>\n </p>\n {summary.labels.length > 0 ? (\n <p className=\"mt-1 text-xs text-muted-foreground\">\n {summary.labels.join(', ')}\n {summary.count > summary.labels.length\n ? ` +${summary.count - summary.labels.length}`\n : ''}\n </p>\n ) : null}\n </div>\n ) : (\n <FieldDiffCard fieldDiff={action?.fieldDiff ?? null} />\n )}\n </div>\n\n {expanded ? (\n <div className=\"mt-3 rounded-md border border-border bg-muted/20 p-3\" data-ai-mutation-preview-details>\n <FieldDiffCard\n fieldDiff={action?.fieldDiff ?? null}\n records={action?.records ?? null}\n />\n </div>\n ) : null}\n\n <div className=\"mt-3 flex items-center justify-between gap-2\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => setExpanded((value) => !value)}\n data-ai-mutation-preview-review\n >\n <Eye className=\"size-4\" aria-hidden />\n <span>\n {t(\n 'ai_assistant.chat.mutation_cards.preview.reviewDetails',\n 'Review details',\n )}\n </span>\n <ChevronDown\n className={`size-4 transition-transform ${expanded ? 'rotate-180' : ''}`}\n aria-hidden\n />\n </Button>\n <div className=\"flex items-center gap-2\">\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={() => {\n void handleCancel()\n }}\n data-ai-mutation-preview-cancel\n >\n {t('ai_assistant.chat.mutation_cards.preview.cancel', 'Cancel')}\n </Button>\n <Button\n type=\"button\"\n size=\"sm\"\n onClick={() => {\n void handleConfirm()\n }}\n data-ai-mutation-preview-confirm\n >\n {t('ai_assistant.chat.mutation_cards.preview.confirm', 'Confirm')}\n </Button>\n </div>\n </div>\n </section>\n )\n}\n\nexport default MutationPreviewCard\n"],
|
|
5
|
-
"mappings": ";AAwJM,
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { ChevronDown, Eye, ShieldAlert } from 'lucide-react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '../../primitives/button'\nimport { useAiShortcuts } from '../useAiShortcuts'\nimport type { AiUiPartProps } from '../ui-part-registry'\nimport { confirmPendingAction, cancelPendingAction } from './pending-action-api'\nimport { useAiPendingActionPolling } from './useAiPendingActionPolling'\nimport { FieldDiffCard } from './FieldDiffCard'\nimport { ConfirmationCard } from './ConfirmationCard'\nimport { MutationResultCard } from './MutationResultCard'\nimport type { AiPendingActionCardAction } from './types'\n\n/**\n * Mutation-approval preview card. Rendered by the server-emitted\n * `mutation-preview-card` UI part (spec \u00A79.2).\n *\n * Responsibilities:\n * - Fetch the current pending-action row via the shared polling hook so the\n * card recovers state on page reload (reconnect behavior).\n * - Render the top-level `fieldDiff` OR a per-record `records[]` summary\n * with a drill-in link.\n * - Provide `Confirm` / `Cancel` / `Review Details` actions with shared\n * keyboard shortcuts (`Cmd/Ctrl+Enter` \u2192 confirm, `Escape` \u2192 cancel).\n * - Flip to the {@link ConfirmationCard} once the user confirms, and\n * further to the {@link MutationResultCard} once the row becomes\n * terminal.\n */\nexport interface MutationPreviewCardPayload {\n /** Optional server-serialized pending action snapshot for the initial render. */\n pendingAction?: AiPendingActionCardAction\n}\n\nexport interface MutationPreviewCardProps extends AiUiPartProps {\n /** Optional injected action for tests \u2014 bypasses the polling fetch. */\n initialAction?: AiPendingActionCardAction\n /** Endpoint base override for tests. */\n endpoint?: string\n}\n\nfunction summarizeBatch(\n records: NonNullable<AiPendingActionCardAction['records']>,\n): { count: number; labels: string[] } {\n const labels = records.slice(0, 3).map((record) => record.label)\n return { count: records.length, labels }\n}\n\nexport function MutationPreviewCard(props: MutationPreviewCardProps) {\n const t = useT()\n const pendingActionId = props.pendingActionId ?? ''\n const payload = (props.payload as MutationPreviewCardPayload | undefined) ?? {}\n const injected = props.initialAction ?? payload.pendingAction ?? null\n\n const { action: polled, refresh } = useAiPendingActionPolling({\n pendingActionId,\n endpoint: props.endpoint,\n disabled: !pendingActionId,\n })\n const action = polled ?? injected\n\n const [expanded, setExpanded] = React.useState(false)\n const [phase, setPhase] = React.useState<'preview' | 'confirming'>('preview')\n const [confirmError, setConfirmError] = React.useState<{\n status: number\n code?: string\n message: string\n extra?: Record<string, unknown>\n } | null>(null)\n\n const handleConfirm = React.useCallback(async () => {\n if (!pendingActionId) return\n if (phase !== 'preview') return\n setPhase('confirming')\n setConfirmError(null)\n const result = await confirmPendingAction(pendingActionId, {\n endpoint: props.endpoint,\n })\n if (!result.ok) {\n // Network / timeout / 4xx / 5xx \u2014 surface the envelope and rewind the\n // card to the preview phase so the operator can read the error,\n // edit the proposal upstream, or retry.\n setConfirmError(result.error)\n setPhase('preview')\n await refresh()\n return\n }\n // HTTP 200 path. The dispatcher returns `ok: false` AND a populated\n // `mutationResult.error` when the wrapped tool handler failed inside\n // the confirm route \u2014 the row is already in a terminal state but the\n // overall HTTP call succeeded. Treat that as a confirm error too so\n // the alert renders inline instead of leaving the card on the\n // generic \"applying\u2026\" spinner forever.\n const handlerError = result.data?.mutationResult?.error\n if (result.data?.ok === false || handlerError) {\n const mappedCode =\n typeof handlerError?.code === 'string' && handlerError.code.length > 0\n ? handlerError.code\n : 'execution_failed'\n setConfirmError({\n status: 200,\n code: mappedCode,\n message:\n handlerError?.message ??\n t(\n 'ai_assistant.chat.mutation_cards.preview.handlerError',\n 'The mutation handler reported an error. Review the details and re-propose if needed.',\n ),\n })\n }\n await refresh()\n }, [pendingActionId, phase, props.endpoint, refresh, t])\n\n const handleCancel = React.useCallback(async () => {\n if (!pendingActionId) return\n const result = await cancelPendingAction(pendingActionId, {\n endpoint: props.endpoint,\n })\n if (!result.ok) {\n setConfirmError(result.error)\n }\n await refresh()\n }, [pendingActionId, props.endpoint, refresh])\n\n const currentStatus = action?.status ?? null\n const executionError = action?.executionResult?.error\n // Treat any captured handler error as terminal too \u2014 the dispatcher\n // sometimes returns the action before the row has fully transitioned\n // out of `executing`, and we must never leave the spinner masking a\n // real failure. The MutationResultCard's failure path renders the\n // error envelope as long as `executionResult.error` is set.\n const isTerminal =\n currentStatus === 'confirmed' ||\n currentStatus === 'failed' ||\n currentStatus === 'cancelled' ||\n currentStatus === 'expired' ||\n Boolean(executionError)\n\n const { handleKeyDown } = useAiShortcuts({\n onSubmit: () => {\n void handleConfirm()\n },\n onCancel: () => {\n void handleCancel()\n },\n enabled: phase === 'preview' && !isTerminal,\n })\n\n // Terminal \u2014 short-circuit into the result card.\n if (isTerminal && action) {\n return (\n <MutationResultCard\n componentId=\"mutation-result-card\"\n pendingActionId={pendingActionId}\n initialAction={action}\n endpoint={props.endpoint}\n />\n )\n }\n\n // Confirming \u2014 flip to the spinner card. Propagate the confirmError so\n // the user sees the structured envelope even though the confirm call has\n // already resolved.\n if (phase === 'confirming') {\n return (\n <ConfirmationCard\n componentId=\"confirmation-card\"\n pendingActionId={pendingActionId}\n initialAction={action ?? undefined}\n endpoint={props.endpoint}\n payload={{\n sideEffectsSummary: action?.sideEffectsSummary ?? null,\n pendingAction: action ?? undefined,\n confirmError: confirmError ?? undefined,\n }}\n />\n )\n }\n\n const batch = Array.isArray(action?.records) && action!.records!.length > 0 ? action!.records! : null\n const summary = batch ? summarizeBatch(batch) : null\n const hasReviewDetails = Boolean(batch)\n\n return (\n <section\n className=\"rounded-md border border-border bg-background p-4 text-sm outline-none\"\n tabIndex={0}\n onKeyDown={handleKeyDown}\n data-ai-mutation-preview\n data-ai-mutation-preview-mode={batch ? 'batch' : 'single'}\n >\n <header className=\"flex items-start justify-between gap-3\">\n <div className=\"flex items-start gap-2\">\n <ShieldAlert className=\"mt-0.5 size-4 text-status-warning-icon\" aria-hidden />\n <div>\n <h4 className=\"text-sm font-semibold\">\n {t(\n 'ai_assistant.chat.mutation_cards.preview.title',\n 'Review proposed changes',\n )}\n </h4>\n {action?.sideEffectsSummary ? (\n <p className=\"mt-1 text-sm text-muted-foreground\">\n {action.sideEffectsSummary}\n </p>\n ) : null}\n </div>\n </div>\n </header>\n\n <div className=\"mt-3\" data-ai-mutation-preview-body>\n {summary ? (\n <div\n className=\"rounded-md border border-border bg-muted/30 p-3 text-sm\"\n data-ai-mutation-preview-batch-summary\n >\n <p className=\"font-medium\">\n {t(\n 'ai_assistant.chat.mutation_cards.preview.batchSummary',\n 'Batch update',\n )}\n {': '}\n <span data-ai-mutation-preview-count>{summary.count}</span>{' '}\n <span>\n {t(\n 'ai_assistant.chat.mutation_cards.preview.batchRecords',\n 'records',\n )}\n </span>\n </p>\n {summary.labels.length > 0 ? (\n <p className=\"mt-1 text-xs text-muted-foreground\">\n {summary.labels.join(', ')}\n {summary.count > summary.labels.length\n ? ` +${summary.count - summary.labels.length}`\n : ''}\n </p>\n ) : null}\n </div>\n ) : (\n <FieldDiffCard fieldDiff={action?.fieldDiff ?? null} />\n )}\n </div>\n\n {expanded && hasReviewDetails ? (\n <div className=\"mt-3 rounded-md border border-border bg-muted/20 p-3\" data-ai-mutation-preview-details>\n <FieldDiffCard\n fieldDiff={action?.fieldDiff ?? null}\n records={action?.records ?? null}\n />\n </div>\n ) : null}\n\n <div className=\"mt-3 flex items-center justify-between gap-2\">\n {hasReviewDetails ? (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => setExpanded((value) => !value)}\n data-ai-mutation-preview-review\n >\n <Eye className=\"size-4\" aria-hidden />\n <span>\n {t(\n 'ai_assistant.chat.mutation_cards.preview.reviewDetails',\n 'Review details',\n )}\n </span>\n <ChevronDown\n className={`size-4 transition-transform ${expanded ? 'rotate-180' : ''}`}\n aria-hidden\n />\n </Button>\n ) : (\n <span aria-hidden />\n )}\n <div className=\"flex items-center gap-2\">\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={() => {\n void handleCancel()\n }}\n data-ai-mutation-preview-cancel\n >\n {t('ai_assistant.chat.mutation_cards.preview.cancel', 'Cancel')}\n </Button>\n <Button\n type=\"button\"\n size=\"sm\"\n onClick={() => {\n void handleConfirm()\n }}\n data-ai-mutation-preview-confirm\n >\n {t('ai_assistant.chat.mutation_cards.preview.confirm', 'Confirm')}\n </Button>\n </div>\n </div>\n </section>\n )\n}\n\nexport default MutationPreviewCard\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -67,29 +67,22 @@ function MutationResultCard(props) {
|
|
|
67
67
|
return /* @__PURE__ */ jsxs(Alert, { variant: "success", "data-ai-mutation-result": "success", children: [
|
|
68
68
|
/* @__PURE__ */ jsx(AlertTitle, { children: t("ai_assistant.chat.mutation_cards.result.successTitle", "Action applied") }),
|
|
69
69
|
/* @__PURE__ */ jsxs("div", { className: "text-sm leading-relaxed", children: [
|
|
70
|
-
/* @__PURE__ */ jsx("p", { children:
|
|
71
|
-
"ai_assistant.chat.mutation_cards.result.successWithCommand",
|
|
72
|
-
"Completed"
|
|
73
|
-
) + `: ${result.commandName}` : t(
|
|
70
|
+
/* @__PURE__ */ jsx("p", { children: t(
|
|
74
71
|
"ai_assistant.chat.mutation_cards.result.successBody",
|
|
75
|
-
"The
|
|
72
|
+
"The requested change was saved successfully."
|
|
76
73
|
) }),
|
|
77
|
-
recordId ? /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs", children:
|
|
74
|
+
recordId && href ? /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs", children: /* @__PURE__ */ jsx(
|
|
78
75
|
"a",
|
|
79
76
|
{
|
|
80
|
-
className: "
|
|
77
|
+
className: "text-primary underline",
|
|
81
78
|
href,
|
|
82
79
|
"data-ai-mutation-result-link": true,
|
|
83
|
-
children:
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
),
|
|
88
|
-
": ",
|
|
89
|
-
recordId
|
|
90
|
-
]
|
|
80
|
+
children: t(
|
|
81
|
+
"ai_assistant.chat.mutation_cards.result.viewRecord",
|
|
82
|
+
"View record"
|
|
83
|
+
)
|
|
91
84
|
}
|
|
92
|
-
)
|
|
85
|
+
) }) : null
|
|
93
86
|
] })
|
|
94
87
|
] });
|
|
95
88
|
}
|