@modelnex/sdk 0.5.27 → 0.5.29

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/dist/index.js CHANGED
@@ -121,7 +121,6 @@ function waitForDomSettle(options = {}) {
121
121
  if (!resolved) {
122
122
  resolved = true;
123
123
  cleanup();
124
- console.log("[DOM Sync] Forced resolution by max timeout");
125
124
  resolve();
126
125
  }
127
126
  }, Math.max(timeoutMs, minWaitMs));
@@ -237,6 +236,60 @@ function resolveSocketIoTransports(serverUrl, order) {
237
236
 
238
237
  // src/auto-extract.ts
239
238
  var import_react2 = require("react");
239
+
240
+ // src/utils/dev-logging.ts
241
+ function isSdkDebugEnabled(devMode) {
242
+ if (devMode) return true;
243
+ if (typeof window !== "undefined" && Boolean(window.MODELNEX_DEBUG)) {
244
+ return true;
245
+ }
246
+ return process.env.NODE_ENV === "development";
247
+ }
248
+ function emitSdkDebugLog(message, payload, options) {
249
+ if (!isSdkDebugEnabled(options?.devMode)) return;
250
+ if (payload && Object.keys(payload).length > 0) {
251
+ console.log(message, payload);
252
+ } else {
253
+ console.log(message);
254
+ }
255
+ if (options?.dispatchEvent && typeof window !== "undefined") {
256
+ window.dispatchEvent(new CustomEvent("modelnex-debug", { detail: { msg: message, data: payload } }));
257
+ }
258
+ }
259
+ function sanitizeActionList(actions) {
260
+ if (!Array.isArray(actions) || actions.length === 0) return void 0;
261
+ return actions.map(({ actionId }) => ({ actionId }));
262
+ }
263
+ function sanitizeAgentDebug(debug) {
264
+ if (!debug) return void 0;
265
+ const actions = sanitizeActionList(debug.actions);
266
+ const traces = Array.isArray(debug.traces) && debug.traces.length > 0 ? debug.traces.map((trace) => ({
267
+ step: trace.step,
268
+ actions: sanitizeActionList(trace.actions) ?? [],
269
+ results: Array.isArray(trace.results) && trace.results.length > 0 ? trace.results.map(({ actionId, success }) => ({ actionId, success })) : void 0
270
+ })) : void 0;
271
+ if ((!actions || actions.length === 0) && (!traces || traces.length === 0)) {
272
+ return void 0;
273
+ }
274
+ return {
275
+ ...actions ? { actions } : {},
276
+ ...traces ? { traces } : {}
277
+ };
278
+ }
279
+ function sanitizeChatMessages(messages, devMode) {
280
+ const includeDebug = isSdkDebugEnabled(devMode);
281
+ return messages.map((message) => {
282
+ if (message.role !== "assistant") {
283
+ return message;
284
+ }
285
+ return {
286
+ ...message,
287
+ debug: includeDebug ? sanitizeAgentDebug(message.debug) : void 0
288
+ };
289
+ });
290
+ }
291
+
292
+ // src/auto-extract.ts
240
293
  function simpleHash(str) {
241
294
  let hash = 0;
242
295
  for (let i = 0; i < str.length; i++) {
@@ -500,7 +553,7 @@ function extractInteractiveElements() {
500
553
  }
501
554
  return elements;
502
555
  }
503
- function useAutoExtract() {
556
+ function useAutoExtract(devMode) {
504
557
  const [elements, setElements] = (0, import_react2.useState)([]);
505
558
  const timerRef = (0, import_react2.useRef)(null);
506
559
  const lastSnapshotRef = (0, import_react2.useRef)("");
@@ -518,9 +571,11 @@ function useAutoExtract() {
518
571
  })));
519
572
  if (snapshot === lastSnapshotRef.current) return;
520
573
  lastSnapshotRef.current = snapshot;
521
- console.log(`[ModelNex AutoExtract] Found ${extracted.length} interactive elements`, extracted.map((e) => ({ fp: e.fingerprint, role: e.role, text: e.text.slice(0, 30) })));
574
+ emitSdkDebugLog("[ModelNex AutoExtract] Scan complete", {
575
+ elementCount: extracted.length
576
+ }, { devMode });
522
577
  setElements(extracted);
523
- }, []);
578
+ }, [devMode]);
524
579
  (0, import_react2.useEffect)(() => {
525
580
  const initialTimer = setTimeout(scan, 300);
526
581
  const observer = new MutationObserver((mutations) => {
@@ -835,7 +890,8 @@ function useModelNexSocket({
835
890
  setStagingFields,
836
891
  setExecutedFields,
837
892
  onSocketId,
838
- websiteId
893
+ websiteId,
894
+ devMode
839
895
  }) {
840
896
  const socketRef = (0, import_react3.useRef)(null);
841
897
  const actionsRef = (0, import_react3.useRef)(actions);
@@ -868,12 +924,14 @@ function useModelNexSocket({
868
924
  allTaggedElements: tagsRef.current ? Array.from(tagsRef.current.values()) : void 0
869
925
  });
870
926
  socket.on("connect", () => {
871
- console.log("ModelNex SDK: Connected to agent server", socket.id);
927
+ emitSdkDebugLog("[ModelNex SDK] Connected to agent server", {
928
+ socketId: socket.id ?? null
929
+ }, { devMode });
872
930
  onSocketId?.(socket.id || null);
873
931
  socket.emit("client:sync", buildSyncPayload());
874
932
  });
875
933
  socket.on("disconnect", () => {
876
- console.log("ModelNex SDK: Disconnected from agent server");
934
+ emitSdkDebugLog("[ModelNex SDK] Disconnected from agent server", void 0, { devMode });
877
935
  onSocketId?.(null);
878
936
  });
879
937
  socket.on("agent:request_context", () => {
@@ -899,12 +957,11 @@ function useModelNexSocket({
899
957
  reasoning
900
958
  }) => {
901
959
  const log = (msg, data) => {
902
- console.log(msg, data ?? "");
903
- window.dispatchEvent(new CustomEvent("modelnex-debug", { detail: { msg, data } }));
960
+ emitSdkDebugLog(msg, data, { devMode, dispatchEvent: true });
904
961
  };
905
962
  if (reasoning || params?.fingerprint) {
906
963
  window.dispatchEvent(new CustomEvent("modelnex-action-start", {
907
- detail: { actionId, fingerprint: params?.fingerprint ?? null, reasoning: reasoning ?? "" }
964
+ detail: { actionId, fingerprint: params?.fingerprint ?? null }
908
965
  }));
909
966
  }
910
967
  const sendResult = (success, result, error) => {
@@ -915,17 +972,18 @@ function useModelNexSocket({
915
972
  const currentActions = actionsRef.current;
916
973
  log("[SDK] agent:execute received", {
917
974
  actionId,
918
- params,
919
- registeredActions: Array.from(currentActions.keys())
975
+ executionId: executionId ?? null,
976
+ fieldId: fieldId ?? null,
977
+ hasParams: params != null
920
978
  });
921
979
  const NAV_ACTION_IDS = ["navigate_to_documents", "navigate_to_templates", "navigate_to_inbox", "navigate_to_settings", "navigate_to_template", "navigate_to_document", "navigate_to_folder", "navigate_editor_step"];
922
980
  const action = currentActions.get(actionId);
923
981
  if (action) {
924
982
  try {
925
983
  const validatedParams = action.schema.parse(params);
926
- log("[SDK] Executing", { actionId, validatedParams });
984
+ log("[SDK] Executing action", { actionId });
927
985
  const execResult = await action.execute(validatedParams);
928
- log("[SDK] Execute completed", { actionId });
986
+ log("[SDK] Action completed", { actionId });
929
987
  sendResult(true, execResult);
930
988
  if (NAV_ACTION_IDS.includes(actionId)) {
931
989
  requestAnimationFrame(() => {
@@ -949,25 +1007,29 @@ function useModelNexSocket({
949
1007
  }
950
1008
  } catch (err) {
951
1009
  const errMsg = err instanceof Error ? err.message : String(err);
952
- console.error(`[ModelNex SDK] Execution failed for ${actionId}`, err);
1010
+ console.error(`[ModelNex SDK] Execution failed for ${actionId}: ${errMsg}`);
1011
+ if (isSdkDebugEnabled(devMode)) {
1012
+ window.dispatchEvent(
1013
+ new CustomEvent("modelnex-debug", {
1014
+ detail: { msg: "[SDK] Execution failed", data: { actionId, error: errMsg } }
1015
+ })
1016
+ );
1017
+ }
1018
+ sendResult(false, void 0, errMsg);
1019
+ }
1020
+ } else {
1021
+ const errMsg = `Action not found: ${actionId}`;
1022
+ console.warn(`[ModelNex SDK] ${errMsg}`);
1023
+ if (isSdkDebugEnabled(devMode)) {
953
1024
  window.dispatchEvent(
954
1025
  new CustomEvent("modelnex-debug", {
955
- detail: { msg: "[SDK] Execution failed", data: { actionId, error: errMsg } }
1026
+ detail: {
1027
+ msg: "[SDK] Action not found",
1028
+ data: { actionId }
1029
+ }
956
1030
  })
957
1031
  );
958
- sendResult(false, void 0, errMsg);
959
1032
  }
960
- } else {
961
- const errMsg = `Action not found: ${actionId}`;
962
- console.warn("[ModelNex SDK]", errMsg, "Available:", Array.from(currentActions.keys()));
963
- window.dispatchEvent(
964
- new CustomEvent("modelnex-debug", {
965
- detail: {
966
- msg: "[SDK] Action not found",
967
- data: { actionId, available: Array.from(actions.keys()) }
968
- }
969
- })
970
- );
971
1033
  sendResult(false, void 0, errMsg);
972
1034
  }
973
1035
  }
@@ -1827,8 +1889,153 @@ async function captureScreenshot(selector) {
1827
1889
  return canvas.toDataURL("image/png");
1828
1890
  }
1829
1891
 
1892
+ // src/utils/experience-tool-bridge.ts
1893
+ var activeBridge = null;
1894
+ function registerExperienceToolBridge(bridge) {
1895
+ activeBridge = bridge;
1896
+ return () => {
1897
+ if (activeBridge === bridge) {
1898
+ activeBridge = null;
1899
+ }
1900
+ };
1901
+ }
1902
+ function getExperienceToolBridge() {
1903
+ return activeBridge;
1904
+ }
1905
+
1906
+ // src/utils/tourDiscovery.ts
1907
+ async function fetchTours(serverUrl, toursApiBase, websiteId, userType, userId, experienceType = "tour") {
1908
+ try {
1909
+ const params = new URLSearchParams({
1910
+ websiteId,
1911
+ userType
1912
+ });
1913
+ if (userId) params.set("userId", userId);
1914
+ if (experienceType !== "tour") params.set("type", experienceType);
1915
+ const res = await fetch(getTourApiUrl(serverUrl, toursApiBase, `/tours?${params.toString()}`));
1916
+ if (!res.ok) return [];
1917
+ const data = await res.json();
1918
+ return data.tours ?? [];
1919
+ } catch {
1920
+ return [];
1921
+ }
1922
+ }
1923
+ async function fetchTourById(serverUrl, toursApiBase, tourId, experienceType = "tour", websiteId) {
1924
+ try {
1925
+ const url = new URL(withWebsiteId(getTourApiUrl(serverUrl, toursApiBase, `/tours/${tourId}`), websiteId));
1926
+ if (experienceType !== "tour") {
1927
+ url.searchParams.set("type", experienceType);
1928
+ }
1929
+ const res = await fetch(url.toString());
1930
+ if (!res.ok) {
1931
+ return null;
1932
+ }
1933
+ const data = await res.json();
1934
+ return data.tour ?? null;
1935
+ } catch {
1936
+ return null;
1937
+ }
1938
+ }
1939
+ async function markTourComplete(serverUrl, toursApiBase, tourId, userId, experienceType = "tour", websiteId) {
1940
+ try {
1941
+ const url = new URL(withWebsiteId(getTourApiUrl(serverUrl, toursApiBase, `/tours/${tourId}/complete`), websiteId));
1942
+ if (experienceType !== "tour") {
1943
+ url.searchParams.set("type", experienceType);
1944
+ }
1945
+ await fetch(url.toString(), {
1946
+ method: "POST",
1947
+ headers: { "Content-Type": "application/json" },
1948
+ body: JSON.stringify({ userId })
1949
+ });
1950
+ } catch {
1951
+ }
1952
+ }
1953
+ async function markTourDismissed(serverUrl, toursApiBase, tourId, userId, experienceType = "tour", websiteId) {
1954
+ try {
1955
+ const url = new URL(withWebsiteId(getTourApiUrl(serverUrl, toursApiBase, `/tours/${tourId}/dismiss`), websiteId));
1956
+ if (experienceType !== "tour") {
1957
+ url.searchParams.set("type", experienceType);
1958
+ }
1959
+ await fetch(url.toString(), {
1960
+ method: "POST",
1961
+ headers: { "Content-Type": "application/json" },
1962
+ body: JSON.stringify({ userId })
1963
+ });
1964
+ } catch {
1965
+ }
1966
+ }
1967
+ async function recordTourEvent(serverUrl, toursApiBase, tourId, userId, eventType, websiteId) {
1968
+ try {
1969
+ const url = withWebsiteId(getTourApiUrl(serverUrl, toursApiBase, `/tours/${tourId}/events`), websiteId);
1970
+ await fetch(url, {
1971
+ method: "POST",
1972
+ headers: { "Content-Type": "application/json" },
1973
+ body: JSON.stringify({ userId, eventType })
1974
+ });
1975
+ } catch {
1976
+ }
1977
+ }
1978
+ function getToursBaseUrl(serverUrl, toursApiBase) {
1979
+ if (toursApiBase?.startsWith("/")) {
1980
+ return `${typeof window !== "undefined" ? window.location.origin : ""}${toursApiBase.replace(/\/$/, "")}`;
1981
+ }
1982
+ return serverUrl.replace(/\/$/, "");
1983
+ }
1984
+ function getTourApiUrl(serverUrl, toursApiBase, path) {
1985
+ const base = getToursBaseUrl(serverUrl, toursApiBase);
1986
+ return toursApiBase ? `${base}${path}` : `${base}/api${path}`;
1987
+ }
1988
+ function withWebsiteId(url, websiteId) {
1989
+ if (!websiteId) {
1990
+ return url;
1991
+ }
1992
+ const baseOrigin = typeof window !== "undefined" ? window.location.origin : "http://localhost";
1993
+ const resolved = new URL(url, baseOrigin);
1994
+ resolved.searchParams.set("websiteId", websiteId);
1995
+ return resolved.toString();
1996
+ }
1997
+ function normalizeTrigger(trigger) {
1998
+ if (trigger === "feature_launch" || trigger === "feature_unlocked" || trigger === "feature_unlock" || trigger === "new_feature") {
1999
+ return "feature_launch";
2000
+ }
2001
+ if (trigger === "return_visit") {
2002
+ return "return_visit";
2003
+ }
2004
+ return trigger ?? "first_visit";
2005
+ }
2006
+ function getStartPolicy(tour) {
2007
+ return tour.startPolicy ?? (normalizeTrigger(tour.trigger) === "manual" ? "immediate_start" : "prompt_only");
2008
+ }
2009
+ function getNotificationType(tour) {
2010
+ return tour.notificationType ?? "bubble_card";
2011
+ }
2012
+ function getUserProfileFeatures(userProfile) {
2013
+ const directFeatures = Array.isArray(userProfile.features) ? userProfile.features : [];
2014
+ const nestedFeatures = Array.isArray(userProfile.tourFacts?.features) ? userProfile.tourFacts.features : [];
2015
+ return [...new Set([...directFeatures, ...nestedFeatures].filter((value) => Boolean(value)))];
2016
+ }
2017
+ function isTourEligible(tour, userProfile) {
2018
+ switch (normalizeTrigger(tour.trigger)) {
2019
+ case "first_visit":
2020
+ return !!userProfile.isNewUser;
2021
+ case "return_visit":
2022
+ return userProfile.isNewUser === false;
2023
+ case "feature_launch": {
2024
+ const featureKey = tour.featureKey?.trim();
2025
+ if (!featureKey) return false;
2026
+ return getUserProfileFeatures(userProfile).includes(featureKey);
2027
+ }
2028
+ case "manual":
2029
+ return false;
2030
+ default:
2031
+ return false;
2032
+ }
2033
+ }
2034
+
1830
2035
  // src/hooks/useBuiltinActions.ts
1831
2036
  var resolutionCache = /* @__PURE__ */ new Map();
2037
+ var DEFAULT_WORKFLOW_SEARCH_LIMIT = 5;
2038
+ var WORKFLOW_TYPES = ["onboarding", "tour"];
1832
2039
  function safeQueryAll2(selector) {
1833
2040
  try {
1834
2041
  return Array.from(document.querySelectorAll(selector)).filter(
@@ -2003,6 +2210,137 @@ function lastResortScan(fingerprint, options, elements) {
2003
2210
  function cacheResolution(originalFp, selector, resolvedFp) {
2004
2211
  resolutionCache.set(originalFp, { selector, resolvedFingerprint: resolvedFp });
2005
2212
  }
2213
+ function normalizeWorkflowQuery(value) {
2214
+ return value.trim().toLowerCase();
2215
+ }
2216
+ function getWorkflowSearchText(tour) {
2217
+ const firstSteps = (tour.steps || []).slice(0, 6).flatMap((step) => [step.goal, step.narration, step.ask]).filter(Boolean).join(" ");
2218
+ return [
2219
+ tour.name,
2220
+ tour.type,
2221
+ tour.trigger,
2222
+ tour.startPolicy,
2223
+ tour.featureKey,
2224
+ tour.goal?.primaryAction,
2225
+ tour.goal?.successMetric,
2226
+ ...tour.targetUserTypes,
2227
+ firstSteps
2228
+ ].filter(Boolean).join(" ").toLowerCase();
2229
+ }
2230
+ function scoreWorkflowMatch(tour, query) {
2231
+ const normalizedQuery = normalizeWorkflowQuery(query);
2232
+ if (!normalizedQuery) return 1;
2233
+ const name = tour.name.toLowerCase();
2234
+ const featureKey = (tour.featureKey || "").toLowerCase();
2235
+ const haystack = getWorkflowSearchText(tour);
2236
+ if (name === normalizedQuery) return 140;
2237
+ if (featureKey && featureKey === normalizedQuery) return 125;
2238
+ if (name.includes(normalizedQuery)) return 115;
2239
+ if (featureKey && featureKey.includes(normalizedQuery)) return 100;
2240
+ if (haystack.includes(normalizedQuery)) return 85;
2241
+ const words = normalizedQuery.split(/\s+/).filter((word) => word.length > 1);
2242
+ if (words.length === 0) return 0;
2243
+ const matched = words.filter((word) => haystack.includes(word));
2244
+ if (matched.length === 0) return 0;
2245
+ const ratio = matched.length / words.length;
2246
+ return Math.round(ratio * 70);
2247
+ }
2248
+ function summarizeWorkflowStep(step, index) {
2249
+ const detail = step.goal || step.ask || step.narration || step.rawNarration || "No description";
2250
+ return `${index + 1}. [${step.type}] ${detail}`;
2251
+ }
2252
+ function formatWorkflowSummary(tour, options) {
2253
+ const stepLimit = options?.stepLimit ?? 4;
2254
+ const parts = [
2255
+ `**${tour.name}**`,
2256
+ `ID: ${tour.id}`,
2257
+ `Type: ${tour.type || "tour"}`,
2258
+ `Trigger: ${tour.trigger}`,
2259
+ `Start policy: ${tour.startPolicy || "prompt_only"}`,
2260
+ tour.featureKey ? `Feature key: ${tour.featureKey}` : null,
2261
+ tour.targetUserTypes?.length ? `Target users: ${tour.targetUserTypes.join(", ")}` : null,
2262
+ `Steps: ${tour.steps?.length ?? 0}`,
2263
+ tour.goal?.primaryAction ? `Primary action: ${tour.goal.primaryAction}` : null
2264
+ ].filter(Boolean);
2265
+ if (options?.includeSteps) {
2266
+ const stepPreview = (tour.steps || []).slice(0, stepLimit).map((step, index) => summarizeWorkflowStep(step, index)).join("\n");
2267
+ if (stepPreview) {
2268
+ parts.push(`Steps preview:
2269
+ ${stepPreview}`);
2270
+ }
2271
+ }
2272
+ return parts.join("\n");
2273
+ }
2274
+ function getRequestedWorkflowTypes(experienceType) {
2275
+ if (!experienceType || experienceType === "all") {
2276
+ return WORKFLOW_TYPES;
2277
+ }
2278
+ return [experienceType];
2279
+ }
2280
+ async function fetchAvailableWorkflows(getters, experienceType) {
2281
+ const serverUrl = getters.serverUrl();
2282
+ const websiteId = getters.websiteId();
2283
+ if (!serverUrl) {
2284
+ throw new Error("Server URL is not configured.");
2285
+ }
2286
+ if (!websiteId) {
2287
+ throw new Error("websiteId is required to search workflows.");
2288
+ }
2289
+ const userProfile = getters.userProfile();
2290
+ const userType = userProfile?.type || "";
2291
+ const userId = userProfile?.userId;
2292
+ const toursApiBase = getters.toursApiBase();
2293
+ const requestedTypes = getRequestedWorkflowTypes(experienceType);
2294
+ const lists = await Promise.all(
2295
+ requestedTypes.map(
2296
+ (type) => fetchTours(serverUrl, toursApiBase, websiteId, userType, userId, type)
2297
+ )
2298
+ );
2299
+ return lists.flatMap(
2300
+ (list, index) => list.map((tour) => ({
2301
+ ...tour,
2302
+ type: tour.type || requestedTypes[index]
2303
+ }))
2304
+ );
2305
+ }
2306
+ async function resolveWorkflowFromInput(getters, params) {
2307
+ const serverUrl = getters.serverUrl();
2308
+ const websiteId = getters.websiteId();
2309
+ const toursApiBase = getters.toursApiBase();
2310
+ if (!serverUrl) {
2311
+ throw new Error("Server URL is not configured.");
2312
+ }
2313
+ if (params.workflowId) {
2314
+ const requestedTypes = getRequestedWorkflowTypes(params.experienceType);
2315
+ for (const type of requestedTypes) {
2316
+ const workflow = await fetchTourById(serverUrl, toursApiBase, params.workflowId, type, websiteId);
2317
+ if (workflow) {
2318
+ return { workflow: { ...workflow, type: workflow.type || type } };
2319
+ }
2320
+ }
2321
+ }
2322
+ const query = params.name?.trim();
2323
+ if (!query) {
2324
+ return { workflow: null, reason: "Provide either workflowId or name." };
2325
+ }
2326
+ const workflows = await fetchAvailableWorkflows(getters, params.experienceType);
2327
+ const ranked = workflows.map((workflow) => ({ workflow, score: scoreWorkflowMatch(workflow, query) })).filter(({ score }) => score > 0).sort((a, b) => b.score - a.score);
2328
+ if (ranked.length === 0) {
2329
+ return {
2330
+ workflow: null,
2331
+ reason: `No workflow matched "${query}". Try search_workflows first for available options.`
2332
+ };
2333
+ }
2334
+ if (ranked.length > 1 && ranked[0].score === ranked[1].score) {
2335
+ return {
2336
+ workflow: null,
2337
+ reason: `Multiple workflows matched "${query}". Try view_workflow with an exact workflowId.
2338
+
2339
+ ` + ranked.slice(0, 3).map(({ workflow }) => formatWorkflowSummary(workflow)).join("\n\n---\n\n")
2340
+ };
2341
+ }
2342
+ return { workflow: ranked[0].workflow };
2343
+ }
2006
2344
  var screenshotSchema = import_zod2.z.object({
2007
2345
  selector: import_zod2.z.string().optional().describe("Optional CSS selector to capture a specific element. Omit to capture the full viewport.")
2008
2346
  });
@@ -2283,15 +2621,91 @@ function createSearchTaggedElementsAction(getTagStore) {
2283
2621
  }
2284
2622
  };
2285
2623
  }
2624
+ var workflowExperienceTypeSchema = import_zod2.z.enum(["onboarding", "tour", "all"]);
2625
+ var searchWorkflowsSchema = import_zod2.z.object({
2626
+ query: import_zod2.z.string().optional().describe("Optional workflow name, feature, or task phrase to search for. Omit to list available workflows."),
2627
+ experienceType: workflowExperienceTypeSchema.optional().describe("Which workflow type to search: onboarding, tour, or all. Default: onboarding."),
2628
+ limit: import_zod2.z.number().optional().describe(`Maximum number of workflows to return (default: ${DEFAULT_WORKFLOW_SEARCH_LIMIT}).`)
2629
+ });
2630
+ function createSearchWorkflowsAction(getters) {
2631
+ return {
2632
+ id: "search_workflows",
2633
+ description: "Search available workflows and tours that can be launched for the current website/user. Use this when the user asks for onboarding, a walkthrough, a tutorial, a guided setup, or a named workflow/tour.",
2634
+ schema: searchWorkflowsSchema,
2635
+ execute: async (params) => {
2636
+ const workflows = await fetchAvailableWorkflows(getters, params.experienceType ?? "onboarding");
2637
+ if (workflows.length === 0) {
2638
+ return "No workflows are currently available for this website/user.";
2639
+ }
2640
+ const query = params.query?.trim();
2641
+ const limit = params.limit ?? DEFAULT_WORKFLOW_SEARCH_LIMIT;
2642
+ const ranked = workflows.map((workflow) => ({
2643
+ workflow,
2644
+ score: query ? scoreWorkflowMatch(workflow, query) : 1
2645
+ })).filter(({ score }) => score > 0).sort((a, b) => b.score - a.score).slice(0, limit);
2646
+ if (ranked.length === 0) {
2647
+ return `No workflows matched "${query}".`;
2648
+ }
2649
+ return ranked.map(({ workflow }) => formatWorkflowSummary(workflow)).join("\n\n---\n\n");
2650
+ }
2651
+ };
2652
+ }
2653
+ var resolveWorkflowSchema = import_zod2.z.object({
2654
+ workflowId: import_zod2.z.string().optional().describe("Exact workflow id when known."),
2655
+ name: import_zod2.z.string().optional().describe("Workflow name or a close name match when the id is unknown."),
2656
+ experienceType: workflowExperienceTypeSchema.optional().describe("Which workflow type to search: onboarding, tour, or all. Default: onboarding.")
2657
+ });
2658
+ function createViewWorkflowAction(getters) {
2659
+ return {
2660
+ id: "view_workflow",
2661
+ description: "View a specific workflow or tour in more detail, including its id, trigger, start policy, and the first few steps. Use after search_workflows when you need to inspect a candidate before starting it.",
2662
+ schema: resolveWorkflowSchema,
2663
+ execute: async (params) => {
2664
+ const { workflow, reason } = await resolveWorkflowFromInput(getters, params);
2665
+ if (!workflow) {
2666
+ return reason || "Workflow not found.";
2667
+ }
2668
+ return formatWorkflowSummary(workflow, { includeSteps: true, stepLimit: 5 });
2669
+ }
2670
+ };
2671
+ }
2672
+ var startWorkflowSchema = resolveWorkflowSchema.extend({
2673
+ reviewMode: import_zod2.z.boolean().optional().describe("Optional. Start in review mode instead of normal playback. Default: false.")
2674
+ });
2675
+ function createStartWorkflowAction(getters) {
2676
+ return {
2677
+ id: "start_workflow",
2678
+ description: "Start a specific workflow or tour in the chat experience. Use this after search_workflows or view_workflow when the user wants to begin a named guided workflow.",
2679
+ schema: startWorkflowSchema,
2680
+ execute: async (params) => {
2681
+ const { workflow, reason } = await resolveWorkflowFromInput(getters, params);
2682
+ if (!workflow) {
2683
+ return reason || "Workflow not found.";
2684
+ }
2685
+ const bridge = getExperienceToolBridge();
2686
+ if (!bridge) {
2687
+ return "Workflow launch is unavailable because no chat playback controller is mounted.";
2688
+ }
2689
+ bridge.startExperience(workflow, workflow.type, params.reviewMode ? {
2690
+ reviewMode: true,
2691
+ reviewMetadata: { trigger: "agent_tool" }
2692
+ } : void 0);
2693
+ return `Started workflow "${workflow.name}" (${workflow.id}).`;
2694
+ }
2695
+ };
2696
+ }
2286
2697
  var BUILTIN_ACTION_IDS = {
2287
2698
  screenshot: "take_screenshot",
2288
2699
  click: "click_element",
2289
2700
  fill: "fill_input",
2290
2701
  wait: "request_user_action",
2291
2702
  searchDocs: "search_docs",
2292
- searchTaggedElements: "search_tagged_elements"
2703
+ searchTaggedElements: "search_tagged_elements",
2704
+ searchWorkflows: "search_workflows",
2705
+ viewWorkflow: "view_workflow",
2706
+ startWorkflow: "start_workflow"
2293
2707
  };
2294
- function useBuiltinActions(registerAction, unregisterAction, tagStore, serverUrl, websiteId) {
2708
+ function useBuiltinActions(registerAction, unregisterAction, tagStore, serverUrl, websiteId, toursApiBase, userProfile) {
2295
2709
  const registeredRef = (0, import_react8.useRef)(false);
2296
2710
  const tagStoreRef = (0, import_react8.useRef)(tagStore);
2297
2711
  tagStoreRef.current = tagStore;
@@ -2299,18 +2713,33 @@ function useBuiltinActions(registerAction, unregisterAction, tagStore, serverUrl
2299
2713
  serverUrlRef.current = serverUrl;
2300
2714
  const websiteIdRef = (0, import_react8.useRef)(websiteId);
2301
2715
  websiteIdRef.current = websiteId;
2716
+ const toursApiBaseRef = (0, import_react8.useRef)(toursApiBase);
2717
+ toursApiBaseRef.current = toursApiBase;
2718
+ const userProfileRef = (0, import_react8.useRef)(userProfile);
2719
+ userProfileRef.current = userProfile;
2302
2720
  (0, import_react8.useEffect)(() => {
2303
2721
  if (registeredRef.current) return;
2304
2722
  registeredRef.current = true;
2305
2723
  const getTagStore = () => tagStoreRef.current;
2306
2724
  const getServerUrl = () => serverUrlRef.current;
2307
2725
  const getWebsiteId = () => websiteIdRef.current;
2726
+ const getToursApiBase = () => toursApiBaseRef.current;
2727
+ const getUserProfile = () => userProfileRef.current;
2728
+ const workflowToolGetters = {
2729
+ serverUrl: getServerUrl,
2730
+ websiteId: getWebsiteId,
2731
+ toursApiBase: getToursApiBase,
2732
+ userProfile: getUserProfile
2733
+ };
2308
2734
  registerAction(BUILTIN_SCREENSHOT_ACTION);
2309
2735
  registerAction(createClickAction(getTagStore));
2310
2736
  registerAction(createFillAction(getTagStore));
2311
2737
  registerAction(createWaitAction(getTagStore));
2312
2738
  registerAction(createSearchDocsAction(getServerUrl, getWebsiteId));
2313
2739
  registerAction(createSearchTaggedElementsAction(getTagStore));
2740
+ registerAction(createSearchWorkflowsAction(workflowToolGetters));
2741
+ registerAction(createViewWorkflowAction(workflowToolGetters));
2742
+ registerAction(createStartWorkflowAction(workflowToolGetters));
2314
2743
  return () => {
2315
2744
  unregisterAction(BUILTIN_SCREENSHOT_ACTION.id);
2316
2745
  unregisterAction(BUILTIN_ACTION_IDS.click);
@@ -2318,6 +2747,9 @@ function useBuiltinActions(registerAction, unregisterAction, tagStore, serverUrl
2318
2747
  unregisterAction(BUILTIN_ACTION_IDS.wait);
2319
2748
  unregisterAction(BUILTIN_ACTION_IDS.searchDocs);
2320
2749
  unregisterAction(BUILTIN_ACTION_IDS.searchTaggedElements);
2750
+ unregisterAction(BUILTIN_ACTION_IDS.searchWorkflows);
2751
+ unregisterAction(BUILTIN_ACTION_IDS.viewWorkflow);
2752
+ unregisterAction(BUILTIN_ACTION_IDS.startWorkflow);
2321
2753
  registeredRef.current = false;
2322
2754
  };
2323
2755
  }, [registerAction, unregisterAction]);
@@ -2326,6 +2758,56 @@ function useBuiltinActions(registerAction, unregisterAction, tagStore, serverUrl
2326
2758
  // src/constants.ts
2327
2759
  var DEFAULT_MODELNEX_SERVER_URL = "https://api.modelnex.com";
2328
2760
 
2761
+ // src/utils/dev-mode.ts
2762
+ var DEV_MODE_KEY_GLOBAL_NAMES = ["__MODELNEX_DEV_MODE_KEY__", "MODELNEX_DEV_MODE_KEY"];
2763
+ var devModeValidationCache = /* @__PURE__ */ new Map();
2764
+ function normalizeDevModeKey(value) {
2765
+ if (typeof value !== "string") return void 0;
2766
+ const normalized = value.trim();
2767
+ return normalized || void 0;
2768
+ }
2769
+ function resolveInjectedDevModeKey(explicitDevModeKey) {
2770
+ const normalizedExplicitKey = normalizeDevModeKey(explicitDevModeKey);
2771
+ if (normalizedExplicitKey) return normalizedExplicitKey;
2772
+ if (typeof window === "undefined") return void 0;
2773
+ const browserWindow = window;
2774
+ for (const globalName of DEV_MODE_KEY_GLOBAL_NAMES) {
2775
+ const normalizedGlobalKey = normalizeDevModeKey(browserWindow[globalName]);
2776
+ if (normalizedGlobalKey) return normalizedGlobalKey;
2777
+ }
2778
+ const scriptInjectedKey = typeof document !== "undefined" ? normalizeDevModeKey(
2779
+ document.querySelector("script[data-modelnex-dev-mode-key]")?.dataset.modelnexDevModeKey
2780
+ ) : void 0;
2781
+ if (scriptInjectedKey) return scriptInjectedKey;
2782
+ return void 0;
2783
+ }
2784
+ async function validateInjectedDevModeKey(serverUrl, websiteId, devModeKey) {
2785
+ const normalizedWebsiteId = websiteId.trim();
2786
+ const normalizedDevModeKey = devModeKey.trim();
2787
+ if (!normalizedWebsiteId || !normalizedDevModeKey) return false;
2788
+ const cacheKey = `${serverUrl}::${normalizedWebsiteId}::${normalizedDevModeKey}`;
2789
+ const cached = devModeValidationCache.get(cacheKey);
2790
+ if (cached) {
2791
+ return cached;
2792
+ }
2793
+ const validationPromise = fetch(
2794
+ `${serverUrl.replace(/\/$/, "")}/api/websites/${encodeURIComponent(normalizedWebsiteId)}/dev-mode/verify`,
2795
+ {
2796
+ method: "POST",
2797
+ headers: {
2798
+ "Content-Type": "application/json"
2799
+ },
2800
+ body: JSON.stringify({ key: normalizedDevModeKey })
2801
+ }
2802
+ ).then(async (response) => {
2803
+ if (!response.ok) return false;
2804
+ const payload = await response.json().catch(() => null);
2805
+ return payload?.enabled === true;
2806
+ }).catch(() => false);
2807
+ devModeValidationCache.set(cacheKey, validationPromise);
2808
+ return validationPromise;
2809
+ }
2810
+
2329
2811
  // src/hooks/useRunCommand.ts
2330
2812
  var import_react9 = require("react");
2331
2813
  function searchTaggedElementsForQuery(store, query, limit = 8) {
@@ -2368,7 +2850,10 @@ function useRunCommand(serverUrlOverride) {
2368
2850
  if (!res.ok) {
2369
2851
  throw new Error(data?.error ?? `Request failed: ${res.status}`);
2370
2852
  }
2371
- return data;
2853
+ return {
2854
+ ...data,
2855
+ debug: sanitizeAgentDebug(data?.debug)
2856
+ };
2372
2857
  },
2373
2858
  [baseUrl, tagStore]
2374
2859
  );
@@ -2572,135 +3057,6 @@ function readPreviewSessionSuppression() {
2572
3057
  }
2573
3058
  }
2574
3059
 
2575
- // src/utils/tourDiscovery.ts
2576
- async function fetchTours(serverUrl, toursApiBase, websiteId, userType, userId, experienceType = "tour") {
2577
- try {
2578
- const params = new URLSearchParams({
2579
- websiteId,
2580
- userType
2581
- });
2582
- if (userId) params.set("userId", userId);
2583
- if (experienceType !== "tour") params.set("type", experienceType);
2584
- const res = await fetch(getTourApiUrl(serverUrl, toursApiBase, `/tours?${params.toString()}`));
2585
- if (!res.ok) return [];
2586
- const data = await res.json();
2587
- return data.tours ?? [];
2588
- } catch {
2589
- return [];
2590
- }
2591
- }
2592
- async function fetchTourById(serverUrl, toursApiBase, tourId, experienceType = "tour", websiteId) {
2593
- try {
2594
- const url = new URL(withWebsiteId(getTourApiUrl(serverUrl, toursApiBase, `/tours/${tourId}`), websiteId));
2595
- if (experienceType !== "tour") {
2596
- url.searchParams.set("type", experienceType);
2597
- }
2598
- const res = await fetch(url.toString());
2599
- if (!res.ok) {
2600
- return null;
2601
- }
2602
- const data = await res.json();
2603
- return data.tour ?? null;
2604
- } catch {
2605
- return null;
2606
- }
2607
- }
2608
- async function markTourComplete(serverUrl, toursApiBase, tourId, userId, experienceType = "tour", websiteId) {
2609
- try {
2610
- const url = new URL(withWebsiteId(getTourApiUrl(serverUrl, toursApiBase, `/tours/${tourId}/complete`), websiteId));
2611
- if (experienceType !== "tour") {
2612
- url.searchParams.set("type", experienceType);
2613
- }
2614
- await fetch(url.toString(), {
2615
- method: "POST",
2616
- headers: { "Content-Type": "application/json" },
2617
- body: JSON.stringify({ userId })
2618
- });
2619
- } catch {
2620
- }
2621
- }
2622
- async function markTourDismissed(serverUrl, toursApiBase, tourId, userId, experienceType = "tour", websiteId) {
2623
- try {
2624
- const url = new URL(withWebsiteId(getTourApiUrl(serverUrl, toursApiBase, `/tours/${tourId}/dismiss`), websiteId));
2625
- if (experienceType !== "tour") {
2626
- url.searchParams.set("type", experienceType);
2627
- }
2628
- await fetch(url.toString(), {
2629
- method: "POST",
2630
- headers: { "Content-Type": "application/json" },
2631
- body: JSON.stringify({ userId })
2632
- });
2633
- } catch {
2634
- }
2635
- }
2636
- async function recordTourEvent(serverUrl, toursApiBase, tourId, userId, eventType, websiteId) {
2637
- try {
2638
- const url = withWebsiteId(getTourApiUrl(serverUrl, toursApiBase, `/tours/${tourId}/events`), websiteId);
2639
- await fetch(url, {
2640
- method: "POST",
2641
- headers: { "Content-Type": "application/json" },
2642
- body: JSON.stringify({ userId, eventType })
2643
- });
2644
- } catch {
2645
- }
2646
- }
2647
- function getToursBaseUrl(serverUrl, toursApiBase) {
2648
- if (toursApiBase?.startsWith("/")) {
2649
- return `${typeof window !== "undefined" ? window.location.origin : ""}${toursApiBase.replace(/\/$/, "")}`;
2650
- }
2651
- return serverUrl.replace(/\/$/, "");
2652
- }
2653
- function getTourApiUrl(serverUrl, toursApiBase, path) {
2654
- const base = getToursBaseUrl(serverUrl, toursApiBase);
2655
- return toursApiBase ? `${base}${path}` : `${base}/api${path}`;
2656
- }
2657
- function withWebsiteId(url, websiteId) {
2658
- if (!websiteId) {
2659
- return url;
2660
- }
2661
- const baseOrigin = typeof window !== "undefined" ? window.location.origin : "http://localhost";
2662
- const resolved = new URL(url, baseOrigin);
2663
- resolved.searchParams.set("websiteId", websiteId);
2664
- return resolved.toString();
2665
- }
2666
- function normalizeTrigger(trigger) {
2667
- if (trigger === "feature_launch" || trigger === "feature_unlocked" || trigger === "feature_unlock" || trigger === "new_feature") {
2668
- return "feature_launch";
2669
- }
2670
- if (trigger === "return_visit") {
2671
- return "return_visit";
2672
- }
2673
- return trigger ?? "first_visit";
2674
- }
2675
- function getStartPolicy(tour) {
2676
- return tour.startPolicy ?? (normalizeTrigger(tour.trigger) === "manual" ? "immediate_start" : "prompt_only");
2677
- }
2678
- function getNotificationType(tour) {
2679
- return tour.notificationType ?? "bubble_card";
2680
- }
2681
- function getUserProfileFeatures(userProfile) {
2682
- const directFeatures = Array.isArray(userProfile.features) ? userProfile.features : [];
2683
- const nestedFeatures = Array.isArray(userProfile.tourFacts?.features) ? userProfile.tourFacts.features : [];
2684
- return [...new Set([...directFeatures, ...nestedFeatures].filter((value) => Boolean(value)))];
2685
- }
2686
- function isTourEligible(tour, userProfile) {
2687
- switch (normalizeTrigger(tour.trigger)) {
2688
- case "first_visit":
2689
- return !!userProfile.isNewUser;
2690
- case "return_visit":
2691
- return userProfile.isNewUser === false;
2692
- case "feature_launch": {
2693
- const featureKey = tour.featureKey?.trim();
2694
- if (!featureKey) return false;
2695
- return getUserProfileFeatures(userProfile).includes(featureKey);
2696
- }
2697
- case "manual":
2698
- return false;
2699
- default:
2700
- return false;
2701
- }
2702
- }
2703
-
2704
3060
  // src/hooks/useTourPlayback.ts
2705
3061
  var import_react12 = require("react");
2706
3062
 
@@ -3405,7 +3761,9 @@ function useTourPlayback({
3405
3761
  const socket = tourSocketPool.acquire(serverUrl);
3406
3762
  socketRef.current = socket;
3407
3763
  const handleConnect = () => {
3408
- console.log("[TourClient] Connected to tour agent server:", socket.id);
3764
+ emitSdkDebugLog("[TourClient] Connected to tour agent server", {
3765
+ socketId: socket.id ?? null
3766
+ }, { devMode: devModeRef.current });
3409
3767
  const profile = userProfileRef.current;
3410
3768
  const currentWebsiteId = websiteIdRef.current;
3411
3769
  if (currentWebsiteId && profile?.userId) {
@@ -3422,7 +3780,9 @@ function useTourPlayback({
3422
3780
  setServerState(payload);
3423
3781
  };
3424
3782
  const handleCommandCancel = (payload) => {
3425
- console.log("[TourClient] Received command_cancel:", payload);
3783
+ emitSdkDebugLog("[TourClient] Received command cancel", {
3784
+ commandBatchId: payload.commandBatchId ?? null
3785
+ }, { devMode: devModeRef.current });
3426
3786
  if (payload.commandBatchId && activeCommandBatchIdRef.current === payload.commandBatchId) {
3427
3787
  activeCommandBatchIdRef.current = null;
3428
3788
  activeExecutionTokenRef.current++;
@@ -3437,14 +3797,22 @@ function useTourPlayback({
3437
3797
  const ownerKey = createTourPlaybackOwnerKey(serverUrl, websiteIdRef.current, experienceTypeRef.current);
3438
3798
  if (!shouldExecuteTourCommandBatch(isActiveRef.current) || !hasTourPlaybackOwnership(ownerKey, playbackOwnerIdRef.current)) {
3439
3799
  const activeType = experienceTypeRef.current;
3440
- console.log("[TourClient] Ignoring command batch for inactive playback hook:", activeType, payload.stepIndex);
3800
+ emitSdkDebugLog("[TourClient] Ignoring command batch for inactive playback hook", {
3801
+ experienceType: activeType,
3802
+ stepIndex: payload.stepIndex
3803
+ }, { devMode: devModeRef.current });
3441
3804
  return;
3442
3805
  }
3443
3806
  const emitIfOpen = (ev, data) => {
3444
3807
  if (socketRef.current !== socket) return;
3445
3808
  emitSocketEvent(socket, ev, data);
3446
3809
  };
3447
- console.log("[TourClient] Received command batch:", payload.stepIndex, payload.commands);
3810
+ const commandBatchId = payload.commandBatchId ?? null;
3811
+ emitSdkDebugLog("[TourClient] Received command batch", {
3812
+ stepIndex: payload.stepIndex,
3813
+ commandCount: Array.isArray(payload.commands) ? payload.commands.length : 0,
3814
+ commandBatchId
3815
+ }, { devMode: devModeRef.current });
3448
3816
  runCleanup(pendingManualWaitCleanupRef.current);
3449
3817
  pendingManualWaitCleanupRef.current = null;
3450
3818
  if (voiceInputResolveRef.current) {
@@ -3454,7 +3822,6 @@ function useTourPlayback({
3454
3822
  }
3455
3823
  setPlaybackState("executing");
3456
3824
  commandInFlightRef.current = true;
3457
- const commandBatchId = payload.commandBatchId ?? null;
3458
3825
  turnIdRef.current = payload.turnId ?? turnIdRef.current;
3459
3826
  const clearCommandBatchId = () => {
3460
3827
  if (activeCommandBatchIdRef.current === commandBatchId) {
@@ -3500,7 +3867,7 @@ function useTourPlayback({
3500
3867
  }
3501
3868
  }
3502
3869
  if (!payload.commands || !Array.isArray(payload.commands)) {
3503
- console.warn("[TourClient] Payload commands is not an array:", payload);
3870
+ console.warn("[TourClient] Command batch payload was invalid.");
3504
3871
  commandInFlightRef.current = false;
3505
3872
  emitIfOpen("tour:action_result", {
3506
3873
  success: false,
@@ -3549,7 +3916,9 @@ function useTourPlayback({
3549
3916
  };
3550
3917
  const executeOne = async (action) => {
3551
3918
  assertNotInterrupted();
3552
- console.log("[TourClient] Executing action:", action?.type, action?.params ? JSON.stringify(action.params).slice(0, 120) : "");
3919
+ emitSdkDebugLog("[TourClient] Executing action", {
3920
+ actionType: action?.type ?? "unknown"
3921
+ }, { devMode: devModeRef.current });
3553
3922
  const currentStep = tourRef.current?.steps?.[stepIndexRef.current] ?? null;
3554
3923
  const executeTimeline = async (params = {}) => {
3555
3924
  const segments = Array.isArray(params.segments) ? params.segments : [];
@@ -3736,7 +4105,7 @@ function useTourPlayback({
3736
4105
  handleTourEnd();
3737
4106
  return { result: "ended" };
3738
4107
  }
3739
- console.warn("[TourClient] Unknown action type:", action?.type, "- skipping");
4108
+ console.warn(`[TourClient] Unknown action type: ${String(action?.type ?? "unknown")}. Skipping.`);
3740
4109
  return { result: "unknown_action_skipped" };
3741
4110
  };
3742
4111
  try {
@@ -3792,14 +4161,15 @@ function useTourPlayback({
3792
4161
  clearCommandBatchId();
3793
4162
  return;
3794
4163
  }
3795
- console.error("[TourClient] Command batch execution failed:", err);
4164
+ const errMsg = err instanceof Error ? err.message : String(err);
4165
+ console.error(`[TourClient] Command batch execution failed: ${errMsg}`);
3796
4166
  if (reviewModeRef.current && activeTourId && activePreviewRunId) {
3797
4167
  void logPreviewEvent(serverUrl, toursApiBaseRef.current, activeTourId, activePreviewRunId, websiteId, {
3798
4168
  stepOrder: stepIndexRef.current,
3799
4169
  eventType: "command_batch_failed",
3800
4170
  payload: {
3801
4171
  commandBatchId,
3802
- error: String(err),
4172
+ error: errMsg,
3803
4173
  partialResults: results
3804
4174
  },
3805
4175
  status: "active",
@@ -3809,7 +4179,7 @@ function useTourPlayback({
3809
4179
  emitIfOpen("tour:action_result", {
3810
4180
  success: false,
3811
4181
  reason: "execution_error",
3812
- error: String(err),
4182
+ error: errMsg,
3813
4183
  results,
3814
4184
  commandBatchId,
3815
4185
  runId: runIdRef.current,
@@ -3835,7 +4205,10 @@ function useTourPlayback({
3835
4205
  let manualWaitTarget = await resolveTargetElement2(waitTargetHints, currentStep);
3836
4206
  if (inputLikeWait && preferredWaitTarget && manualWaitTarget && manualWaitTarget !== preferredWaitTarget && !isEditableWaitTarget(manualWaitTarget) && isEditableWaitTarget(preferredWaitTarget)) {
3837
4207
  manualWaitTarget = preferredWaitTarget;
3838
- console.log("[TourClient] wait_for_input: preferring current editable target over hinted step target", manualWaitTarget);
4208
+ emitSdkDebugLog("[TourClient] wait_for_input preferred highlighted editable target", {
4209
+ stepIndex: stepIndexRef.current,
4210
+ event: waitEvent
4211
+ }, { devMode: devModeRef.current });
3839
4212
  }
3840
4213
  if (manualWaitTarget) {
3841
4214
  const manualWait = createManualWaitForTarget(manualWaitTarget, waitEvent, currentStep);
@@ -3849,7 +4222,10 @@ function useTourPlayback({
3849
4222
  manualWaitPromise = manualWait.promise;
3850
4223
  manualWaitKind = manualWait.kind;
3851
4224
  pendingManualWaitCleanupRef.current = manualWait.cleanup;
3852
- console.log("[TourClient] wait_for_input: using current editable target as fallback wait target", preferredWaitTarget);
4225
+ emitSdkDebugLog("[TourClient] wait_for_input using fallback editable target", {
4226
+ stepIndex: stepIndexRef.current,
4227
+ event: waitEvent
4228
+ }, { devMode: devModeRef.current });
3853
4229
  }
3854
4230
  if (!manualWaitPromise && inputLikeWait) {
3855
4231
  const firstInput = document.querySelector(
@@ -3860,7 +4236,10 @@ function useTourPlayback({
3860
4236
  manualWaitPromise = manualWait.promise;
3861
4237
  manualWaitKind = manualWait.kind;
3862
4238
  pendingManualWaitCleanupRef.current = manualWait.cleanup;
3863
- console.log("[TourClient] wait_for_input: no target found, falling back to first visible editable element", firstInput);
4239
+ emitSdkDebugLog("[TourClient] wait_for_input falling back to first editable element", {
4240
+ stepIndex: stepIndexRef.current,
4241
+ event: waitEvent
4242
+ }, { devMode: devModeRef.current });
3864
4243
  }
3865
4244
  }
3866
4245
  setPlaybackState(manualWaitKind ? "waiting_input" : "waiting_voice");
@@ -3942,12 +4321,17 @@ function useTourPlayback({
3942
4321
  const tour = tourData.tourContext ?? tourRef.current;
3943
4322
  const expType = experienceTypeRef.current;
3944
4323
  if (tour?.type && tour.type !== expType) {
3945
- console.log(`[TourClient] Ignoring ${tour.type} start (this hook is for ${expType})`);
4324
+ emitSdkDebugLog("[TourClient] Ignoring tour start for mismatched experience type", {
4325
+ incomingType: tour.type,
4326
+ hookType: expType
4327
+ }, { devMode: devModeRef.current });
3946
4328
  return;
3947
4329
  }
3948
4330
  const ownerKey = createTourPlaybackOwnerKey(serverUrl, websiteIdRef.current, expType);
3949
4331
  if (!claimTourPlaybackOwnership(ownerKey, playbackOwnerIdRef.current)) {
3950
- console.log(`[TourClient] Ignoring ${expType} start because another hook already owns playback`);
4332
+ emitSdkDebugLog("[TourClient] Ignoring tour start because playback ownership is already claimed", {
4333
+ experienceType: expType
4334
+ }, { devMode: devModeRef.current });
3951
4335
  return;
3952
4336
  }
3953
4337
  claimedPlaybackOwnerKeyRef.current = ownerKey;
@@ -3972,6 +4356,8 @@ function useTourPlayback({
3972
4356
  },
3973
4357
  currentStepOrder: 0
3974
4358
  });
4359
+ } else if (tour?.id && userProfile?.userId) {
4360
+ void recordTourEvent(serverUrl, toursApiBaseRef.current, tour.id, userProfile.userId, "started", websiteId);
3975
4361
  }
3976
4362
  try {
3977
4363
  const { generateMinifiedAOM: generateMinifiedAOM2 } = await Promise.resolve().then(() => (init_aom(), aom_exports));
@@ -3984,7 +4370,8 @@ function useTourPlayback({
3984
4370
  });
3985
4371
  }
3986
4372
  } catch (e) {
3987
- console.warn("[TourClient] Initial DOM sync failed:", e);
4373
+ const errMsg = e instanceof Error ? e.message : String(e);
4374
+ console.warn(`[TourClient] Initial DOM sync failed: ${errMsg}`);
3988
4375
  }
3989
4376
  };
3990
4377
  const handleTourUpdate = (payload) => {
@@ -4022,12 +4409,11 @@ function useTourPlayback({
4022
4409
  return;
4023
4410
  }
4024
4411
  if (isDev) {
4025
- console.log(`%c[ModelNex Tour] ${entry.type}`, "color: #3b82f6; font-weight: bold", entry);
4026
- if (typeof window !== "undefined") {
4027
- window.dispatchEvent(new CustomEvent("modelnex-debug", {
4028
- detail: { msg: `[Tour Timeline] ${entry.type}`, data: entry }
4029
- }));
4030
- }
4412
+ emitSdkDebugLog(`[Tour Timeline] ${entry.type}`, {
4413
+ stepIndex: entry?.data?.stepIndex ?? null,
4414
+ runId: entry?.data?.runId ?? null,
4415
+ turnId: entry?.data?.turnId ?? null
4416
+ }, { devMode: devModeRef.current, dispatchEvent: true });
4031
4417
  }
4032
4418
  };
4033
4419
  socket.on("connect", handleConnect);
@@ -4038,7 +4424,7 @@ function useTourPlayback({
4038
4424
  socket.on("tour:update", handleTourUpdate);
4039
4425
  socket.on("tour:end", handleTourEndEvent);
4040
4426
  socket.on("tour:debug_log", handleDebugLog);
4041
- console.log("[ModelNex SDK] Tour playback initialized. Debug logs enabled:", devModeRef.current || process.env.NODE_ENV === "development");
4427
+ emitSdkDebugLog("[ModelNex SDK] Tour playback initialized", void 0, { devMode: devModeRef.current });
4042
4428
  return () => {
4043
4429
  socket.off("connect", handleConnect);
4044
4430
  socket.off("tour:server_state", handleServerState);
@@ -4149,6 +4535,8 @@ function useTourPlayback({
4149
4535
  status: "stopped",
4150
4536
  currentStepOrder: stepIndexRef.current
4151
4537
  });
4538
+ } else if (tourRef.current?.id && userProfile?.userId) {
4539
+ void recordTourEvent(serverUrl, toursApiBaseRef.current, tourRef.current.id, userProfile.userId, "cancelled", websiteId);
4152
4540
  }
4153
4541
  if (reviewModeRef.current) {
4154
4542
  if (tourRef.current?.id) {
@@ -4221,7 +4609,9 @@ function useTourPlayback({
4221
4609
  isPlaybackActive: isActiveRef.current,
4222
4610
  startRequested: startRequestedRef.current
4223
4611
  })) {
4224
- console.log("[TourClient] Ignoring duplicate start request while playback is already active or starting:", tour.id);
4612
+ emitSdkDebugLog("[TourClient] Ignoring duplicate start request while playback is already active or starting", {
4613
+ tourId: tour.id
4614
+ }, { devMode: devModeRef.current });
4225
4615
  return;
4226
4616
  }
4227
4617
  setPendingTour(null);
@@ -4237,7 +4627,9 @@ function useTourPlayback({
4237
4627
  }
4238
4628
  const ownerKey = createTourPlaybackOwnerKey(serverUrl, websiteIdRef.current, tour.type ?? experienceTypeRef.current);
4239
4629
  if (!claimTourPlaybackOwnership(ownerKey, playbackOwnerIdRef.current)) {
4240
- console.log("[TourClient] Ignoring duplicate start request because another hook already owns this experience:", tour.id);
4630
+ emitSdkDebugLog("[TourClient] Ignoring duplicate start request because another hook already owns this experience", {
4631
+ tourId: tour.id
4632
+ }, { devMode: devModeRef.current });
4241
4633
  return;
4242
4634
  }
4243
4635
  claimedPlaybackOwnerKeyRef.current = ownerKey;
@@ -4260,7 +4652,8 @@ function useTourPlayback({
4260
4652
  setPreviewRunId(previewRun.id);
4261
4653
  previewRunIdRef.current = previewRun.id;
4262
4654
  } catch (err) {
4263
- console.warn("[TourClient] Failed to create preview run:", err);
4655
+ const errMsg = err instanceof Error ? err.message : String(err);
4656
+ console.warn(`[TourClient] Failed to create preview run: ${errMsg}`);
4264
4657
  setReviewStatusMessage("Preview review logging is unavailable.");
4265
4658
  setPreviewRunId(null);
4266
4659
  previewRunIdRef.current = null;
@@ -4309,7 +4702,7 @@ function useTourPlayback({
4309
4702
  if (!tour) {
4310
4703
  clearActiveDraftPreview(experienceType);
4311
4704
  persistSuppressedDraftPreview(draftPreview);
4312
- console.warn("[ModelNex] Tour fetch failed:", tourId, experienceType, "(Check ModelNex server is running and CORS allows this origin)");
4705
+ console.warn(`[ModelNex] Tour fetch failed for ${experienceType}:${tourId}. Check the ModelNex server and CORS configuration.`);
4313
4706
  return;
4314
4707
  }
4315
4708
  if (cancelled) {
@@ -4332,7 +4725,8 @@ function useTourPlayback({
4332
4725
  await runTour(tour, previewOptions);
4333
4726
  }
4334
4727
  } catch (err) {
4335
- console.warn("[ModelNex] Tour test failed:", err);
4728
+ const errMsg = err instanceof Error ? err.message : String(err);
4729
+ console.warn(`[ModelNex] Tour test failed: ${errMsg}`);
4336
4730
  }
4337
4731
  })();
4338
4732
  return () => {
@@ -4442,7 +4836,8 @@ function useTourPlayback({
4442
4836
  setPlaybackState("executing");
4443
4837
  }
4444
4838
  } catch (err) {
4445
- console.warn("[TourClient] Failed to submit review feedback:", err);
4839
+ const errMsg = err instanceof Error ? err.message : String(err);
4840
+ console.warn(`[TourClient] Failed to submit review feedback: ${errMsg}`);
4446
4841
  setReviewStatusMessage("Unable to save correction.");
4447
4842
  } finally {
4448
4843
  setReviewSubmitting(false);
@@ -4483,23 +4878,29 @@ function useTourPlayback({
4483
4878
  }, [voice]);
4484
4879
  const handleVoiceInput = (0, import_react12.useCallback)((transcript) => {
4485
4880
  const text = transcript.trim();
4486
- console.log("[TourAgent] handleVoiceInput called with text:", text);
4881
+ emitSdkDebugLog("[TourAgent] Voice input received", {
4882
+ textLength: text.length
4883
+ }, { devMode: devModeRef.current });
4487
4884
  if (!text) return;
4488
4885
  if (interruptExecution(text)) {
4489
4886
  return;
4490
4887
  }
4491
4888
  if (voiceInputResolveRef.current) {
4492
- console.log("[TourAgent] Resolving loop waiting_voice with text:", text);
4889
+ emitSdkDebugLog("[TourAgent] Resolving waiting voice input", void 0, { devMode: devModeRef.current });
4493
4890
  voiceInputResolveRef.current(text);
4494
4891
  } else if (isSocketWritable(socketRef.current)) {
4495
- console.log("[TourAgent] Forwarding ambient voice to server:", text);
4892
+ emitSdkDebugLog("[TourAgent] Forwarding ambient voice input to server", {
4893
+ textLength: text.length
4894
+ }, { devMode: devModeRef.current });
4496
4895
  emitSocketEvent(socketRef.current, "tour:user_input", {
4497
4896
  transcript: text,
4498
4897
  runId: runIdRef.current,
4499
4898
  turnId: turnIdRef.current
4500
4899
  });
4501
4900
  } else {
4502
- console.log("[TourAgent] buffering voice input because socket is not ready:", text);
4901
+ emitSdkDebugLog("[TourAgent] Buffering voice input until socket is ready", {
4902
+ textLength: text.length
4903
+ }, { devMode: devModeRef.current });
4503
4904
  pendingInputBufRef.current = text;
4504
4905
  }
4505
4906
  }, [interruptExecution]);
@@ -5654,7 +6055,6 @@ function useVoice(serverUrl) {
5654
6055
  setIsListening(false);
5655
6056
  }, [stopLiveSttTransport]);
5656
6057
  const startListening = (0, import_react14.useCallback)((onResult, onInterruption, onError, options = {}) => {
5657
- console.log("[Voice] startListening called, options:", options);
5658
6058
  stopListening();
5659
6059
  listeningSessionIdRef.current = createVoiceDebugId("stt");
5660
6060
  listeningStartedAtRef.current = performance.now();
@@ -5868,7 +6268,9 @@ function useVoice(serverUrl) {
5868
6268
  recorder.start(LIVE_STT_TIMESLICE_MS);
5869
6269
  }
5870
6270
  setIsListening(true);
5871
- console.log("[Voice] Live STT pipeline active (Deepgram streaming + WebRTC loopback)");
6271
+ emitVoiceDebug("stt_live_pipeline_active", {
6272
+ listeningSessionId: listeningSessionIdRef.current
6273
+ });
5872
6274
  } catch (err) {
5873
6275
  const normalizedError = normalizeSttError(err);
5874
6276
  console.warn("[Voice] Failed to start live STT recorder:", normalizedError);
@@ -8616,6 +9018,7 @@ function Tooltip({ children, title }) {
8616
9018
  );
8617
9019
  }
8618
9020
  var BUBBLE_EXPANDED_STORAGE_KEY = "modelnex-chat-bubble-expanded";
9021
+ var BUBBLE_DOCKED_STORAGE_KEY = "modelnex-chat-bubble-docked";
8619
9022
  var TOUR_MINIMIZED_STORAGE_KEY = "modelnex-tour-bubble-minimized";
8620
9023
  var BotIcon = () => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("svg", { width: "var(--modelnex-bubble-icon-size, 20px)", height: "var(--modelnex-bubble-icon-size, 20px)", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
8621
9024
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z" }),
@@ -8629,6 +9032,18 @@ var CloseIcon = () => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("svg", { wid
8629
9032
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
8630
9033
  ] });
8631
9034
  var MinimizeIcon = () => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "M8 18h8" }) });
9035
+ var DockIcon = () => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
9036
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "M12 3v10" }),
9037
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "m8 9 4 4 4-4" }),
9038
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "M4 17h16" }),
9039
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "M6 21h12" })
9040
+ ] });
9041
+ var UndockIcon = () => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
9042
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "M12 21V11" }),
9043
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "m8 15 4-4 4 4" }),
9044
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "M4 7h16" }),
9045
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "M6 3h12" })
9046
+ ] });
8632
9047
  var TrashIcon = () => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
8633
9048
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "M3 6h18" }),
8634
9049
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" }),
@@ -8677,7 +9092,7 @@ function AgentTraces({ debug, command, defaultExpanded = true }) {
8677
9092
  },
8678
9093
  children: [
8679
9094
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { children: [
8680
- "Agent traces (",
9095
+ "Execution summary (",
8681
9096
  traces.length,
8682
9097
  " step",
8683
9098
  traces.length !== 1 ? "s" : "",
@@ -8693,21 +9108,9 @@ function AgentTraces({ debug, command, defaultExpanded = true }) {
8693
9108
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("pre", { style: { margin: 0, padding: "8px", background: "#f4f4f5", borderRadius: "4px", whiteSpace: "pre-wrap", wordBreak: "break-word" }, children: command })
8694
9109
  ] }),
8695
9110
  !hasTraceContent ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { marginBottom: "12px", padding: "8px", background: "#fef3c7", borderRadius: "4px", borderLeft: "3px solid #f59e0b" }, children: [
8696
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { fontWeight: 600, color: "#92400e", marginBottom: "4px" }, children: "No trace data" }),
8697
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { fontSize: "10px", color: "#71717a", marginBottom: "6px" }, children: "The server may not have OPENROUTER_API_KEY set, or the request failed before the agent ran." }),
8698
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("pre", { style: { margin: 0, padding: "8px", background: "#fff", borderRadius: "4px", fontSize: "10px", overflow: "auto", maxHeight: "120px", color: "#27272a" }, children: JSON.stringify(debug, null, 2) })
9111
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { fontWeight: 600, color: "#92400e", marginBottom: "4px" }, children: "No safe debug summary" }),
9112
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { fontSize: "10px", color: "#71717a" }, children: "Verbose prompts, reasoning, and raw tool payloads are intentionally hidden by the SDK." })
8699
9113
  ] }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
8700
- debug.llmInput && traces.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { marginBottom: "12px" }, children: [
8701
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { fontWeight: 600, color: "#3f3f46", marginBottom: "4px" }, children: "Input \u2192 agent" }),
8702
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { marginBottom: "6px" }, children: [
8703
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { fontSize: "10px", color: "#a1a1aa", marginBottom: "2px" }, children: "System" }),
8704
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("pre", { style: { margin: 0, padding: "8px", background: "#f4f4f5", borderRadius: "4px", whiteSpace: "pre-wrap", wordBreak: "break-word", maxHeight: "80px", overflow: "auto" }, children: debug.llmInput.systemPrompt ?? "(empty)" })
8705
- ] }),
8706
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { children: [
8707
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { fontSize: "10px", color: "#a1a1aa", marginBottom: "2px" }, children: "User" }),
8708
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("pre", { style: { margin: 0, padding: "8px", background: "#f4f4f5", borderRadius: "4px", whiteSpace: "pre-wrap", wordBreak: "break-word", maxHeight: "80px", overflow: "auto" }, children: debug.llmInput.userMessage ?? "(empty)" })
8709
- ] })
8710
- ] }),
8711
9114
  traces.map((t) => {
8712
9115
  const isStepExpanded = expandedSteps.has(t.step);
8713
9116
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { marginBottom: "12px", paddingBottom: "12px", borderBottom: "1px solid #e4e4e7" }, children: [
@@ -8740,31 +9143,6 @@ function AgentTraces({ debug, command, defaultExpanded = true }) {
8740
9143
  }
8741
9144
  ),
8742
9145
  isStepExpanded && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
8743
- t.reasoning && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { marginBottom: "6px" }, children: [
8744
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { color: "#71717a", marginBottom: "2px" }, children: "Reasoning" }),
8745
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("pre", { style: { margin: 0, padding: "6px", background: "#fef3c7", borderRadius: "4px", whiteSpace: "pre-wrap", wordBreak: "break-word", maxHeight: "100px", overflow: "auto", borderLeft: "3px solid #f59e0b", color: "#451a03" }, children: t.reasoning })
8746
- ] }),
8747
- t.llmInput && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
8748
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { marginBottom: "6px" }, children: [
8749
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { color: "#71717a", marginBottom: "2px" }, children: "Input \u2192 agent" }),
8750
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { marginBottom: "4px" }, children: [
8751
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { fontSize: "10px", color: "#a1a1aa", marginBottom: "2px" }, children: "System" }),
8752
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("pre", { style: { margin: 0, padding: "6px", background: "#f4f4f5", borderRadius: "4px", whiteSpace: "pre-wrap", wordBreak: "break-word", maxHeight: "80px", overflow: "auto" }, children: t.llmInput.systemPrompt ?? "(empty)" })
8753
- ] }),
8754
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { children: [
8755
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { fontSize: "10px", color: "#a1a1aa", marginBottom: "2px" }, children: "User" }),
8756
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("pre", { style: { margin: 0, padding: "6px", background: "#f4f4f5", borderRadius: "4px", whiteSpace: "pre-wrap", wordBreak: "break-word", maxHeight: "60px", overflow: "auto" }, children: t.llmInput.userMessage ?? "(empty)" })
8757
- ] })
8758
- ] }),
8759
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { marginBottom: "6px" }, children: [
8760
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { color: "#71717a", marginBottom: "2px" }, children: "Output \u2190 agent" }),
8761
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("pre", { style: { margin: 0, padding: "6px", background: "#f4f4f5", borderRadius: "4px", whiteSpace: "pre-wrap", wordBreak: "break-word", maxHeight: "60px", overflow: "auto" }, children: t.llmOutput ?? "(empty)" })
8762
- ] })
8763
- ] }),
8764
- !t.llmInput && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { marginBottom: "6px" }, children: [
8765
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { color: "#71717a", marginBottom: "2px" }, children: "LLM output" }),
8766
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("pre", { style: { margin: 0, padding: "6px", background: "#f4f4f5", borderRadius: "4px", whiteSpace: "pre-wrap", wordBreak: "break-word", maxHeight: "60px", overflow: "auto" }, children: t.llmOutput ?? "(empty)" })
8767
- ] }),
8768
9146
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { marginBottom: "6px" }, children: [
8769
9147
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { color: "#71717a", marginBottom: "2px" }, children: "Actions" }),
8770
9148
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("pre", { style: { margin: 0, padding: "6px", background: "#f4f4f5", borderRadius: "4px", whiteSpace: "pre-wrap", wordBreak: "break-word" }, children: JSON.stringify(t.actions, null, 2) })
@@ -8776,10 +9154,6 @@ function AgentTraces({ debug, command, defaultExpanded = true }) {
8776
9154
  ] })
8777
9155
  ] }, t.step);
8778
9156
  }),
8779
- traces.length === 0 && debug.llmOutput && debug.llmOutput.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { marginBottom: "12px" }, children: [
8780
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { fontWeight: 600, color: "#3f3f46", marginBottom: "4px" }, children: "LLM output" }),
8781
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("pre", { style: { margin: 0, padding: "8px", background: "#f4f4f5", borderRadius: "4px", whiteSpace: "pre-wrap", wordBreak: "break-word" }, children: debug.llmOutput.join("\n\n") })
8782
- ] }),
8783
9157
  traces.length === 0 && (debug.actions?.length ?? 0) > 0 && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { marginBottom: "12px" }, children: [
8784
9158
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { fontWeight: 600, color: "#3f3f46", marginBottom: "4px" }, children: "Executed actions" }),
8785
9159
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("pre", { style: { margin: 0, padding: "8px", background: "#f4f4f5", borderRadius: "4px", whiteSpace: "pre-wrap", wordBreak: "break-word" }, children: JSON.stringify(debug.actions, null, 2) })
@@ -8849,6 +9223,7 @@ function ModelNexChatBubble({
8849
9223
  const ctx = (0, import_react18.useContext)(ModelNexContext);
8850
9224
  const [hydrated, setHydrated] = (0, import_react18.useState)(false);
8851
9225
  const [expanded, setExpanded] = (0, import_react18.useState)(false);
9226
+ const [docked, setDocked] = (0, import_react18.useState)(false);
8852
9227
  const [input, setInput] = (0, import_react18.useState)("");
8853
9228
  const messages = ctx?.chatMessages ?? [];
8854
9229
  const setMessages = ctx?.setChatMessages ?? (() => {
@@ -8903,6 +9278,11 @@ function ModelNexChatBubble({
8903
9278
  const activePlayback = playbackController.playback;
8904
9279
  const activeExperienceType = playbackController.activeExperienceType;
8905
9280
  const startingExperienceType = playbackController.startingExperienceType;
9281
+ (0, import_react18.useEffect)(() => {
9282
+ return registerExperienceToolBridge({
9283
+ startExperience: playbackController.startExperience
9284
+ });
9285
+ }, [playbackController.startExperience]);
8906
9286
  const createPlaybackView = (0, import_react18.useCallback)((experienceType) => {
8907
9287
  const isActiveExperience = activePlayback.isActive && activeExperienceType === experienceType;
8908
9288
  const pendingTour = playbackController.pendingPrompt?.experienceType === experienceType ? playbackController.pendingPrompt.tour : null;
@@ -8969,7 +9349,6 @@ function ModelNexChatBubble({
8969
9349
  tagStore.setTagsBatch(data.tags, true);
8970
9350
  lastAutoTaggedUrlRef.current = currentUrl;
8971
9351
  localStorage.setItem(storageKey, "true");
8972
- console.log(`[ModelNex] Auto-tagged ${data.tags.length} elements for ${currentUrl}`);
8973
9352
  }
8974
9353
  } catch (err) {
8975
9354
  console.warn("[ModelNex] Auto-tag error:", err);
@@ -9000,8 +9379,10 @@ function ModelNexChatBubble({
9000
9379
  setHydrated(true);
9001
9380
  try {
9002
9381
  setExpanded(sessionStorage.getItem(BUBBLE_EXPANDED_STORAGE_KEY) === "true");
9382
+ setDocked(sessionStorage.getItem(BUBBLE_DOCKED_STORAGE_KEY) === "true");
9003
9383
  } catch {
9004
9384
  setExpanded(false);
9385
+ setDocked(false);
9005
9386
  }
9006
9387
  }, []);
9007
9388
  const sttActiveRef = (0, import_react18.useRef)(false);
@@ -9061,6 +9442,13 @@ function ModelNexChatBubble({
9061
9442
  } catch {
9062
9443
  }
9063
9444
  }, [tourPlayback.isActive, onboardingPlayback.isActive]);
9445
+ const setDockedState = (0, import_react18.useCallback)((next) => {
9446
+ setDocked(next);
9447
+ try {
9448
+ sessionStorage.setItem(BUBBLE_DOCKED_STORAGE_KEY, String(next));
9449
+ } catch {
9450
+ }
9451
+ }, []);
9064
9452
  (0, import_react18.useEffect)(() => {
9065
9453
  if (shouldAutoExpandForPendingPrompt({
9066
9454
  pendingPrompt,
@@ -9124,12 +9512,9 @@ function ModelNexChatBubble({
9124
9512
  };
9125
9513
  const listeningExperience = resolveTourListeningExperience(preferredExperience, listeningState);
9126
9514
  preferredListeningExperienceRef.current = listeningExperience;
9127
- console.log("[ChatBubble] startTourListening called. listeningState:", listeningState, "preferredExperience:", preferredExperience, "listeningExperience:", listeningExperience);
9128
9515
  if (!canStartTourListening(listeningState)) {
9129
- console.log("[ChatBubble] startTourListening bailed out early.");
9130
9516
  return;
9131
9517
  }
9132
- console.log("[ChatBubble] Proceeding to startTourListening...");
9133
9518
  sttActiveRef.current = true;
9134
9519
  updateTourSttError(null);
9135
9520
  resetFloatingLiveTranscriptSuppression();
@@ -9335,7 +9720,7 @@ function ModelNexChatBubble({
9335
9720
  content: summary || fallbackContent,
9336
9721
  summary: summary ?? void 0,
9337
9722
  nextSteps: nextSteps ?? void 0,
9338
- debug: data?.debug ?? void 0
9723
+ debug: devMode ? data?.debug ?? void 0 : void 0
9339
9724
  }
9340
9725
  ]);
9341
9726
  } catch (err) {
@@ -9422,11 +9807,13 @@ function ModelNexChatBubble({
9422
9807
  fontFamily: "var(--modelnex-font)",
9423
9808
  ...themeStyles
9424
9809
  };
9810
+ const desktopPanelHeight = docked ? "min(calc(100vh - 48px), calc(var(--modelnex-panel-max-height, 600px) + 180px))" : "var(--modelnex-panel-max-height, 600px)";
9811
+ const desktopPanelMaxHeight = docked ? "calc(100vh - 48px)" : "calc(100vh - 120px)";
9425
9812
  const panelStyle = {
9426
9813
  width: isMobile ? "100%" : "var(--modelnex-panel-width, 380px)",
9427
9814
  maxWidth: "calc(100vw - 32px)",
9428
- height: isMobile ? "100%" : "var(--modelnex-panel-max-height, 600px)",
9429
- maxHeight: "calc(100vh - 120px)",
9815
+ height: isMobile ? "100%" : desktopPanelHeight,
9816
+ maxHeight: isMobile ? "100%" : desktopPanelMaxHeight,
9430
9817
  borderRadius: isMobile ? "0" : "var(--modelnex-radius-panel, 20px)",
9431
9818
  border: isMobile ? "none" : "1px solid var(--modelnex-border, #e4e4e7)",
9432
9819
  background: "var(--modelnex-bg, #ffffff)",
@@ -9578,6 +9965,31 @@ function ModelNexChatBubble({
9578
9965
  ] })
9579
9966
  ] }) }),
9580
9967
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: "4px" }, children: [
9968
+ !isMobile && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Tooltip, { title: docked ? "Undock" : "Dock", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
9969
+ "button",
9970
+ {
9971
+ onClick: () => setDockedState(!docked),
9972
+ style: {
9973
+ padding: "8px 10px",
9974
+ borderRadius: "10px",
9975
+ border: "none",
9976
+ background: docked ? "rgba(79,70,229,0.08)" : "transparent",
9977
+ cursor: "pointer",
9978
+ color: docked ? "var(--modelnex-accent, #4f46e5)" : "#71717a",
9979
+ transition: "all 0.2s",
9980
+ display: "flex",
9981
+ alignItems: "center",
9982
+ gap: "6px",
9983
+ fontSize: "12px",
9984
+ fontWeight: 700
9985
+ },
9986
+ "aria-label": docked ? "Undock chat bubble" : "Dock chat bubble",
9987
+ children: [
9988
+ docked ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(UndockIcon, {}) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(DockIcon, {}),
9989
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: docked ? "Undock" : "Dock" })
9990
+ ]
9991
+ }
9992
+ ) }),
9581
9993
  (tourPlayback.isActive || onboardingPlayback.isActive || voice.isSpeaking) && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Tooltip, { title: voice.isMuted ? "Unmute" : "Mute", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
9582
9994
  "button",
9583
9995
  {
@@ -10351,7 +10763,7 @@ function ModelNexChatBubble({
10351
10763
  )
10352
10764
  }
10353
10765
  ),
10354
- msg.role === "assistant" && msg.debug && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(AgentTraces, { debug: msg.debug, command: messages[i - 1]?.content ?? "" })
10766
+ msg.role === "assistant" && devMode && msg.debug && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(AgentTraces, { debug: msg.debug, command: messages[i - 1]?.content ?? "" })
10355
10767
  ] }, i)),
10356
10768
  loading && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { display: "flex", justifyContent: "flex-start" }, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
10357
10769
  "div",
@@ -11330,6 +11742,7 @@ var ModelNexProvider = ({
11330
11742
  userProfile,
11331
11743
  toursApiBase,
11332
11744
  devMode,
11745
+ devModeKey,
11333
11746
  serverUrl: serverUrlProp
11334
11747
  }) => {
11335
11748
  const serverUrl = serverUrlProp ?? DEFAULT_MODELNEX_SERVER_URL;
@@ -11352,6 +11765,25 @@ var ModelNexProvider = ({
11352
11765
  const [voiceMuted, setVoiceMuted] = (0, import_react21.useState)(false);
11353
11766
  const [socketId, setSocketId] = (0, import_react21.useState)(null);
11354
11767
  const [actions, setActions] = (0, import_react21.useState)(/* @__PURE__ */ new Map());
11768
+ const [validatedBrowserDevMode, setValidatedBrowserDevMode] = (0, import_react21.useState)(false);
11769
+ const resolvedDevModeKey = (0, import_react21.useMemo)(() => resolveInjectedDevModeKey(devModeKey), [devModeKey]);
11770
+ (0, import_react21.useEffect)(() => {
11771
+ let cancelled = false;
11772
+ if (!websiteId || !resolvedDevModeKey) {
11773
+ setValidatedBrowserDevMode(false);
11774
+ return () => {
11775
+ cancelled = true;
11776
+ };
11777
+ }
11778
+ void validateInjectedDevModeKey(serverUrl, websiteId, resolvedDevModeKey).then((enabled) => {
11779
+ if (cancelled) return;
11780
+ setValidatedBrowserDevMode(enabled);
11781
+ });
11782
+ return () => {
11783
+ cancelled = true;
11784
+ };
11785
+ }, [resolvedDevModeKey, serverUrl, websiteId]);
11786
+ const effectiveDevMode = Boolean(devMode) || validatedBrowserDevMode;
11355
11787
  const registerAction = (0, import_react21.useCallback)((action) => {
11356
11788
  setActions((prev) => {
11357
11789
  const next = new Map(prev);
@@ -11366,30 +11798,41 @@ var ModelNexProvider = ({
11366
11798
  return next;
11367
11799
  });
11368
11800
  }, []);
11369
- const extractedElements = useAutoExtract();
11801
+ const extractedElements = useAutoExtract(effectiveDevMode);
11370
11802
  const tagStore = useTagStore({ serverUrl, websiteId });
11371
- useBuiltinActions(registerAction, unregisterAction, tagStore, serverUrl, websiteId);
11803
+ useBuiltinActions(registerAction, unregisterAction, tagStore, serverUrl, websiteId, toursApiBase, userProfile);
11372
11804
  const CHAT_STORAGE_KEY = "modelnex-chat-messages";
11373
11805
  const [chatMessages, setChatMessagesRaw] = (0, import_react21.useState)([]);
11374
11806
  (0, import_react21.useEffect)(() => {
11375
11807
  try {
11376
11808
  const stored = sessionStorage.getItem(CHAT_STORAGE_KEY);
11377
11809
  if (stored) {
11378
- setChatMessagesRaw(JSON.parse(stored));
11810
+ setChatMessagesRaw(sanitizeChatMessages(JSON.parse(stored), effectiveDevMode));
11379
11811
  }
11380
11812
  } catch {
11381
11813
  }
11382
- }, []);
11814
+ }, [effectiveDevMode]);
11815
+ (0, import_react21.useEffect)(() => {
11816
+ setChatMessagesRaw((prev) => {
11817
+ const next = sanitizeChatMessages(prev, effectiveDevMode);
11818
+ try {
11819
+ sessionStorage.setItem(CHAT_STORAGE_KEY, JSON.stringify(next));
11820
+ } catch {
11821
+ }
11822
+ return next;
11823
+ });
11824
+ }, [effectiveDevMode]);
11383
11825
  const setChatMessages = (0, import_react21.useCallback)((action) => {
11384
11826
  setChatMessagesRaw((prev) => {
11385
- const next = typeof action === "function" ? action(prev) : action;
11827
+ const resolved = typeof action === "function" ? action(prev) : action;
11828
+ const next = sanitizeChatMessages(resolved, effectiveDevMode);
11386
11829
  try {
11387
11830
  sessionStorage.setItem(CHAT_STORAGE_KEY, JSON.stringify(next));
11388
11831
  } catch {
11389
11832
  }
11390
11833
  return next;
11391
11834
  });
11392
- }, []);
11835
+ }, [effectiveDevMode]);
11393
11836
  useModelNexSocket({
11394
11837
  serverUrl,
11395
11838
  actions,
@@ -11401,7 +11844,8 @@ var ModelNexProvider = ({
11401
11844
  setStagingFields,
11402
11845
  setExecutedFields,
11403
11846
  onSocketId: setSocketId,
11404
- websiteId
11847
+ websiteId,
11848
+ devMode: effectiveDevMode
11405
11849
  });
11406
11850
  useFieldHighlight(stagingFields, executedFields, setExecutedFields);
11407
11851
  (0, import_react21.useEffect)(() => {
@@ -11436,9 +11880,9 @@ var ModelNexProvider = ({
11436
11880
  voiceMuted,
11437
11881
  setVoiceMuted,
11438
11882
  socketId,
11439
- devMode
11883
+ devMode: effectiveDevMode
11440
11884
  }),
11441
- [serverUrl, commandUrl, registerAction, unregisterAction, activeAgentActions, stagingFields, highlightActions, studioMode, recordingMode, extractedElements, tagStore, chatMessages, websiteId, userProfile?.userId, userProfile?.type, userProfile?.isNewUser, toursApiBase, voiceMuted, socketId, devMode]
11885
+ [serverUrl, commandUrl, registerAction, unregisterAction, activeAgentActions, stagingFields, highlightActions, studioMode, recordingMode, extractedElements, tagStore, chatMessages, websiteId, userProfile?.userId, userProfile?.type, userProfile?.isNewUser, toursApiBase, voiceMuted, socketId, effectiveDevMode]
11442
11886
  );
11443
11887
  return import_react21.default.createElement(
11444
11888
  ModelNexContext.Provider,