@open-mercato/ui 0.6.2-develop.3446.1.bd060c6017 → 0.6.2-develop.3467.1.2a1818709d

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.
@@ -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": "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",
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,147 @@
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
+ ...init?.body ? { "Content-Type": "application/json" } : {},
28
+ ...init?.headers ?? {}
29
+ }
30
+ });
31
+ if (!response.ok) return { ok: false, status: response.status, data: null };
32
+ return { ok: true, status: response.status, data: await response.json() };
33
+ } catch {
34
+ return { ok: false, status: 0, data: null };
35
+ }
36
+ }
37
+ async function listAiServerConversations(options = {}) {
38
+ const params = new URLSearchParams();
39
+ if (options.agentId) params.set("agent", options.agentId);
40
+ if (options.status) params.set("status", options.status);
41
+ params.set("limit", String(options.limit ?? 100));
42
+ const result = await requestJson(
43
+ `${CONVERSATIONS_ENDPOINT}?${params.toString()}`
44
+ );
45
+ return result.ok ? result.data.items : null;
46
+ }
47
+ async function createAiServerConversation(input) {
48
+ const result = await requestJson(CONVERSATIONS_ENDPOINT, {
49
+ method: "POST",
50
+ body: JSON.stringify({
51
+ agentId: input.agentId,
52
+ conversationId: input.conversationId,
53
+ title: input.title ?? void 0,
54
+ pageContext: input.pageContext ?? void 0
55
+ })
56
+ });
57
+ return result.ok ? result.data : null;
58
+ }
59
+ async function updateAiServerConversation(conversationId, patch) {
60
+ const result = await requestJson(
61
+ `${CONVERSATIONS_ENDPOINT}/${encodeURIComponent(conversationId)}`,
62
+ {
63
+ method: "PATCH",
64
+ body: JSON.stringify(patch)
65
+ }
66
+ );
67
+ return result.ok ? result.data : null;
68
+ }
69
+ async function loadAiServerTranscript(conversationId, options = {}) {
70
+ const params = new URLSearchParams();
71
+ params.set("limit", String(options.limit ?? 100));
72
+ const result = await requestJson(
73
+ `${CONVERSATIONS_ENDPOINT}/${encodeURIComponent(conversationId)}?${params.toString()}`
74
+ );
75
+ return result.ok ? result.data : null;
76
+ }
77
+ async function importAiLocalConversation(input) {
78
+ const result = await requestJson(`${CONVERSATIONS_ENDPOINT}/import`, {
79
+ method: "POST",
80
+ body: JSON.stringify({
81
+ conversation: {
82
+ conversationId: input.conversationId,
83
+ agentId: input.agentId,
84
+ title: input.title ?? void 0,
85
+ status: input.status ?? "open",
86
+ pageContext: input.pageContext ?? void 0
87
+ },
88
+ messages: input.messages.slice(-100).map((message) => ({
89
+ clientMessageId: message.id,
90
+ role: message.role,
91
+ content: message.content,
92
+ uiParts: message.uiParts ?? [],
93
+ files: (message.files ?? []).map((file) => ({
94
+ id: file.id,
95
+ name: file.name,
96
+ mimeType: file.type
97
+ }))
98
+ }))
99
+ })
100
+ });
101
+ return result.ok ? result.data : null;
102
+ }
103
+ function serverMessageToChatMessage(message) {
104
+ if (message.role !== "user" && message.role !== "assistant") return null;
105
+ const uiParts = [];
106
+ message.uiParts.forEach((part, index) => {
107
+ if (!part || typeof part !== "object") return;
108
+ const value = part;
109
+ const componentId = typeof value.componentId === "string" ? value.componentId : typeof value.type === "string" ? value.type : null;
110
+ if (!componentId) return;
111
+ uiParts.push({
112
+ componentId,
113
+ payload: value.payload ?? value.props,
114
+ pendingActionId: typeof value.pendingActionId === "string" ? value.pendingActionId : void 0,
115
+ key: typeof value.key === "string" ? value.key : `${message.id}:${index}:${componentId}`
116
+ });
117
+ });
118
+ return {
119
+ id: message.clientMessageId ?? message.id,
120
+ role: message.role,
121
+ content: message.content,
122
+ files: message.files.map((file, index) => {
123
+ const name = readString(file.name);
124
+ if (!name) return null;
125
+ const type = typeof file.mimeType === "string" ? file.mimeType : typeof file.type === "string" ? file.type : "application/octet-stream";
126
+ const id = readString(file.id) ?? readString(message.attachmentIds[index]);
127
+ const rawPreviewUrl = readString(file.previewUrl) ?? readString(file.thumbnailUrl) ?? readString(file.url);
128
+ const previewUrl = type.startsWith("image/") ? rawPreviewUrl ?? (id ? buildAttachmentImagePreviewUrl(id) : void 0) : void 0;
129
+ return {
130
+ ...id ? { id } : {},
131
+ name,
132
+ type,
133
+ ...previewUrl ? { previewUrl } : {}
134
+ };
135
+ }).filter((file) => file !== null),
136
+ uiParts
137
+ };
138
+ }
139
+ export {
140
+ createAiServerConversation,
141
+ importAiLocalConversation,
142
+ listAiServerConversations,
143
+ loadAiServerTranscript,
144
+ serverMessageToChatMessage,
145
+ updateAiServerConversation
146
+ };
147
+ //# 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 ...(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
+ }
@@ -2,6 +2,12 @@
2
2
  import * as React from "react";
3
3
  import { createAiAgentTransport } from "@open-mercato/ai-assistant/modules/ai_assistant/lib/agent-transport";
4
4
  import { apiFetch } from "../backend/utils/api.js";
5
+ import {
6
+ createAiServerConversation,
7
+ importAiLocalConversation,
8
+ loadAiServerTranscript,
9
+ serverMessageToChatMessage
10
+ } from "./conversation-store.js";
5
11
  function makeMessageId() {
6
12
  const random = Math.random().toString(36).slice(2, 10);
7
13
  const time = Date.now().toString(36);
@@ -19,6 +25,7 @@ function makeConversationId() {
19
25
  return `conv_${Date.now().toString(16)}_${rand()}${rand()}`;
20
26
  }
21
27
  const SESSION_STORAGE_PREFIX = "om-ai-chat:";
28
+ const SESSION_IMPORT_MARKER_PREFIX = "om-ai-chat-imported:";
22
29
  const SESSION_STORAGE_VERSION = 1;
23
30
  const SESSION_UPDATED_EVENT = "om-ai-chat-session-updated";
24
31
  const SESSION_STREAM_STATE_EVENT = "om-ai-chat-session-stream-state";
@@ -70,9 +77,10 @@ function writePersistedSession(agent, session, conversationId, options) {
70
77
  try {
71
78
  const messages = session.messages.map((message) => {
72
79
  if (!message.files || message.files.length === 0) return message;
73
- const safeFiles = message.files.map(({ name, type, previewUrl }) => {
74
- const durable = typeof previewUrl === "string" && previewUrl.startsWith("data:") ? previewUrl : void 0;
75
- return durable ? { name, type, previewUrl: durable } : { name, type };
80
+ const safeFiles = message.files.map(({ id, name, type, previewUrl }) => {
81
+ const durable = typeof previewUrl === "string" && (previewUrl.startsWith("data:") || previewUrl.startsWith("/api/attachments/image/") || previewUrl.startsWith("/api/attachments/file/")) ? previewUrl : void 0;
82
+ const base = id ? { id, name, type } : { name, type };
83
+ return durable ? { ...base, previewUrl: durable } : base;
76
84
  });
77
85
  return { ...message, files: safeFiles };
78
86
  });
@@ -95,6 +103,24 @@ function clearPersistedSession(agent, conversationId) {
95
103
  } catch {
96
104
  }
97
105
  }
106
+ function getImportMarkerKey(agent, conversationId) {
107
+ return `${SESSION_IMPORT_MARKER_PREFIX}${agent}:${conversationId}`;
108
+ }
109
+ function hasImportMarker(agent, conversationId) {
110
+ if (typeof window === "undefined") return false;
111
+ try {
112
+ return window.localStorage.getItem(getImportMarkerKey(agent, conversationId)) === "1";
113
+ } catch {
114
+ return false;
115
+ }
116
+ }
117
+ function writeImportMarker(agent, conversationId) {
118
+ if (typeof window === "undefined") return;
119
+ try {
120
+ window.localStorage.setItem(getImportMarkerKey(agent, conversationId), "1");
121
+ } catch {
122
+ }
123
+ }
98
124
  function getTransportEndpoint(agent, apiPath, providerOverride, modelOverride) {
99
125
  const transport = createAiAgentTransport({ agentId: agent, endpoint: apiPath });
100
126
  void transport;
@@ -438,20 +464,28 @@ function useAiChat(input) {
438
464
  () => isSessionStreamActive(sessionStorageKey) ? "streaming" : "idle"
439
465
  );
440
466
  const latestMessagesRef = React.useRef(messages);
467
+ const latestStatusRef = React.useRef(status);
441
468
  const persistKeyRef = React.useRef(null);
442
469
  const agentRef = React.useRef(agent);
470
+ const pageContextRef = React.useRef(pageContext);
443
471
  const effectiveConversationIdRef = React.useRef(effectiveConversationId);
444
472
  const sessionStorageKeyRef = React.useRef(sessionStorageKey);
445
473
  const mountedRef = React.useRef(true);
446
474
  React.useEffect(() => {
447
475
  latestMessagesRef.current = messages;
448
476
  }, [messages]);
477
+ React.useEffect(() => {
478
+ latestStatusRef.current = status;
479
+ }, [status]);
449
480
  React.useEffect(() => {
450
481
  persistKeyRef.current = typeof conversationIdInput === "string" && conversationIdInput.length > 0 ? conversationIdInput : null;
451
482
  }, [conversationIdInput]);
452
483
  React.useEffect(() => {
453
484
  agentRef.current = agent;
454
485
  }, [agent]);
486
+ React.useEffect(() => {
487
+ pageContextRef.current = pageContext;
488
+ }, [pageContext]);
455
489
  React.useEffect(() => {
456
490
  effectiveConversationIdRef.current = effectiveConversationId;
457
491
  }, [effectiveConversationId]);
@@ -500,6 +534,45 @@ function useAiChat(input) {
500
534
  },
501
535
  [persistSnapshot]
502
536
  );
537
+ React.useEffect(() => {
538
+ let cancelled = false;
539
+ const localCandidate = readPersistedSession(agent, persistKey);
540
+ async function hydrateFromServer() {
541
+ const transcript = await loadAiServerTranscript(effectiveConversationId, { limit: 100 });
542
+ if (cancelled || latestStatusRef.current !== "idle") return;
543
+ if (transcript) {
544
+ const serverMessages = transcript.messages.map(serverMessageToChatMessage).filter((message) => message !== null);
545
+ if (serverMessages.length > 0 || !localCandidate || localCandidate.messages.length === 0) {
546
+ updateMessages(serverMessages);
547
+ if (serverMessages.length > 0) persistSnapshot(serverMessages, true);
548
+ return;
549
+ }
550
+ }
551
+ if (!localCandidate || localCandidate.messages.length === 0) {
552
+ if (!transcript) {
553
+ void createAiServerConversation({
554
+ agentId: agent,
555
+ conversationId: effectiveConversationId,
556
+ pageContext: pageContextRef.current ?? null
557
+ });
558
+ }
559
+ return;
560
+ }
561
+ if (hasImportMarker(agent, effectiveConversationId)) return;
562
+ const imported = await importAiLocalConversation({
563
+ agentId: agent,
564
+ conversationId: effectiveConversationId,
565
+ pageContext: pageContextRef.current ?? null,
566
+ messages: localCandidate.messages
567
+ });
568
+ if (cancelled || !imported) return;
569
+ writeImportMarker(agent, effectiveConversationId);
570
+ }
571
+ void hydrateFromServer();
572
+ return () => {
573
+ cancelled = true;
574
+ };
575
+ }, [agent, effectiveConversationId, persistKey, persistSnapshot, updateMessages]);
503
576
  const setStatus = React.useCallback((next) => {
504
577
  if (mountedRef.current) {
505
578
  setStatusInternal(next);
@@ -570,8 +643,11 @@ function useAiChat(input) {
570
643
  const url = getTransportEndpoint(agent, apiPath, providerOverride, modelOverride);
571
644
  const body = {
572
645
  messages: outgoingHistory.map((message) => ({
646
+ id: message.id,
573
647
  role: message.role,
574
- content: message.content
648
+ content: message.content,
649
+ files: message.files,
650
+ uiParts: message.uiParts
575
651
  })),
576
652
  pageContext,
577
653
  attachmentIds,