@modelnex/sdk 0.5.26 → 0.5.28

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
@@ -1827,8 +1827,153 @@ async function captureScreenshot(selector) {
1827
1827
  return canvas.toDataURL("image/png");
1828
1828
  }
1829
1829
 
1830
+ // src/utils/experience-tool-bridge.ts
1831
+ var activeBridge = null;
1832
+ function registerExperienceToolBridge(bridge) {
1833
+ activeBridge = bridge;
1834
+ return () => {
1835
+ if (activeBridge === bridge) {
1836
+ activeBridge = null;
1837
+ }
1838
+ };
1839
+ }
1840
+ function getExperienceToolBridge() {
1841
+ return activeBridge;
1842
+ }
1843
+
1844
+ // src/utils/tourDiscovery.ts
1845
+ async function fetchTours(serverUrl, toursApiBase, websiteId, userType, userId, experienceType = "tour") {
1846
+ try {
1847
+ const params = new URLSearchParams({
1848
+ websiteId,
1849
+ userType
1850
+ });
1851
+ if (userId) params.set("userId", userId);
1852
+ if (experienceType !== "tour") params.set("type", experienceType);
1853
+ const res = await fetch(getTourApiUrl(serverUrl, toursApiBase, `/tours?${params.toString()}`));
1854
+ if (!res.ok) return [];
1855
+ const data = await res.json();
1856
+ return data.tours ?? [];
1857
+ } catch {
1858
+ return [];
1859
+ }
1860
+ }
1861
+ async function fetchTourById(serverUrl, toursApiBase, tourId, experienceType = "tour", websiteId) {
1862
+ try {
1863
+ const url = new URL(withWebsiteId(getTourApiUrl(serverUrl, toursApiBase, `/tours/${tourId}`), websiteId));
1864
+ if (experienceType !== "tour") {
1865
+ url.searchParams.set("type", experienceType);
1866
+ }
1867
+ const res = await fetch(url.toString());
1868
+ if (!res.ok) {
1869
+ return null;
1870
+ }
1871
+ const data = await res.json();
1872
+ return data.tour ?? null;
1873
+ } catch {
1874
+ return null;
1875
+ }
1876
+ }
1877
+ async function markTourComplete(serverUrl, toursApiBase, tourId, userId, experienceType = "tour", websiteId) {
1878
+ try {
1879
+ const url = new URL(withWebsiteId(getTourApiUrl(serverUrl, toursApiBase, `/tours/${tourId}/complete`), websiteId));
1880
+ if (experienceType !== "tour") {
1881
+ url.searchParams.set("type", experienceType);
1882
+ }
1883
+ await fetch(url.toString(), {
1884
+ method: "POST",
1885
+ headers: { "Content-Type": "application/json" },
1886
+ body: JSON.stringify({ userId })
1887
+ });
1888
+ } catch {
1889
+ }
1890
+ }
1891
+ async function markTourDismissed(serverUrl, toursApiBase, tourId, userId, experienceType = "tour", websiteId) {
1892
+ try {
1893
+ const url = new URL(withWebsiteId(getTourApiUrl(serverUrl, toursApiBase, `/tours/${tourId}/dismiss`), websiteId));
1894
+ if (experienceType !== "tour") {
1895
+ url.searchParams.set("type", experienceType);
1896
+ }
1897
+ await fetch(url.toString(), {
1898
+ method: "POST",
1899
+ headers: { "Content-Type": "application/json" },
1900
+ body: JSON.stringify({ userId })
1901
+ });
1902
+ } catch {
1903
+ }
1904
+ }
1905
+ async function recordTourEvent(serverUrl, toursApiBase, tourId, userId, eventType, websiteId) {
1906
+ try {
1907
+ const url = withWebsiteId(getTourApiUrl(serverUrl, toursApiBase, `/tours/${tourId}/events`), websiteId);
1908
+ await fetch(url, {
1909
+ method: "POST",
1910
+ headers: { "Content-Type": "application/json" },
1911
+ body: JSON.stringify({ userId, eventType })
1912
+ });
1913
+ } catch {
1914
+ }
1915
+ }
1916
+ function getToursBaseUrl(serverUrl, toursApiBase) {
1917
+ if (toursApiBase?.startsWith("/")) {
1918
+ return `${typeof window !== "undefined" ? window.location.origin : ""}${toursApiBase.replace(/\/$/, "")}`;
1919
+ }
1920
+ return serverUrl.replace(/\/$/, "");
1921
+ }
1922
+ function getTourApiUrl(serverUrl, toursApiBase, path) {
1923
+ const base = getToursBaseUrl(serverUrl, toursApiBase);
1924
+ return toursApiBase ? `${base}${path}` : `${base}/api${path}`;
1925
+ }
1926
+ function withWebsiteId(url, websiteId) {
1927
+ if (!websiteId) {
1928
+ return url;
1929
+ }
1930
+ const baseOrigin = typeof window !== "undefined" ? window.location.origin : "http://localhost";
1931
+ const resolved = new URL(url, baseOrigin);
1932
+ resolved.searchParams.set("websiteId", websiteId);
1933
+ return resolved.toString();
1934
+ }
1935
+ function normalizeTrigger(trigger) {
1936
+ if (trigger === "feature_launch" || trigger === "feature_unlocked" || trigger === "feature_unlock" || trigger === "new_feature") {
1937
+ return "feature_launch";
1938
+ }
1939
+ if (trigger === "return_visit") {
1940
+ return "return_visit";
1941
+ }
1942
+ return trigger ?? "first_visit";
1943
+ }
1944
+ function getStartPolicy(tour) {
1945
+ return tour.startPolicy ?? (normalizeTrigger(tour.trigger) === "manual" ? "immediate_start" : "prompt_only");
1946
+ }
1947
+ function getNotificationType(tour) {
1948
+ return tour.notificationType ?? "bubble_card";
1949
+ }
1950
+ function getUserProfileFeatures(userProfile) {
1951
+ const directFeatures = Array.isArray(userProfile.features) ? userProfile.features : [];
1952
+ const nestedFeatures = Array.isArray(userProfile.tourFacts?.features) ? userProfile.tourFacts.features : [];
1953
+ return [...new Set([...directFeatures, ...nestedFeatures].filter((value) => Boolean(value)))];
1954
+ }
1955
+ function isTourEligible(tour, userProfile) {
1956
+ switch (normalizeTrigger(tour.trigger)) {
1957
+ case "first_visit":
1958
+ return !!userProfile.isNewUser;
1959
+ case "return_visit":
1960
+ return userProfile.isNewUser === false;
1961
+ case "feature_launch": {
1962
+ const featureKey = tour.featureKey?.trim();
1963
+ if (!featureKey) return false;
1964
+ return getUserProfileFeatures(userProfile).includes(featureKey);
1965
+ }
1966
+ case "manual":
1967
+ return false;
1968
+ default:
1969
+ return false;
1970
+ }
1971
+ }
1972
+
1830
1973
  // src/hooks/useBuiltinActions.ts
1831
1974
  var resolutionCache = /* @__PURE__ */ new Map();
1975
+ var DEFAULT_WORKFLOW_SEARCH_LIMIT = 5;
1976
+ var WORKFLOW_TYPES = ["onboarding", "tour"];
1832
1977
  function safeQueryAll2(selector) {
1833
1978
  try {
1834
1979
  return Array.from(document.querySelectorAll(selector)).filter(
@@ -2003,6 +2148,137 @@ function lastResortScan(fingerprint, options, elements) {
2003
2148
  function cacheResolution(originalFp, selector, resolvedFp) {
2004
2149
  resolutionCache.set(originalFp, { selector, resolvedFingerprint: resolvedFp });
2005
2150
  }
2151
+ function normalizeWorkflowQuery(value) {
2152
+ return value.trim().toLowerCase();
2153
+ }
2154
+ function getWorkflowSearchText(tour) {
2155
+ const firstSteps = (tour.steps || []).slice(0, 6).flatMap((step) => [step.goal, step.narration, step.ask]).filter(Boolean).join(" ");
2156
+ return [
2157
+ tour.name,
2158
+ tour.type,
2159
+ tour.trigger,
2160
+ tour.startPolicy,
2161
+ tour.featureKey,
2162
+ tour.goal?.primaryAction,
2163
+ tour.goal?.successMetric,
2164
+ ...tour.targetUserTypes,
2165
+ firstSteps
2166
+ ].filter(Boolean).join(" ").toLowerCase();
2167
+ }
2168
+ function scoreWorkflowMatch(tour, query) {
2169
+ const normalizedQuery = normalizeWorkflowQuery(query);
2170
+ if (!normalizedQuery) return 1;
2171
+ const name = tour.name.toLowerCase();
2172
+ const featureKey = (tour.featureKey || "").toLowerCase();
2173
+ const haystack = getWorkflowSearchText(tour);
2174
+ if (name === normalizedQuery) return 140;
2175
+ if (featureKey && featureKey === normalizedQuery) return 125;
2176
+ if (name.includes(normalizedQuery)) return 115;
2177
+ if (featureKey && featureKey.includes(normalizedQuery)) return 100;
2178
+ if (haystack.includes(normalizedQuery)) return 85;
2179
+ const words = normalizedQuery.split(/\s+/).filter((word) => word.length > 1);
2180
+ if (words.length === 0) return 0;
2181
+ const matched = words.filter((word) => haystack.includes(word));
2182
+ if (matched.length === 0) return 0;
2183
+ const ratio = matched.length / words.length;
2184
+ return Math.round(ratio * 70);
2185
+ }
2186
+ function summarizeWorkflowStep(step, index) {
2187
+ const detail = step.goal || step.ask || step.narration || step.rawNarration || "No description";
2188
+ return `${index + 1}. [${step.type}] ${detail}`;
2189
+ }
2190
+ function formatWorkflowSummary(tour, options) {
2191
+ const stepLimit = options?.stepLimit ?? 4;
2192
+ const parts = [
2193
+ `**${tour.name}**`,
2194
+ `ID: ${tour.id}`,
2195
+ `Type: ${tour.type || "tour"}`,
2196
+ `Trigger: ${tour.trigger}`,
2197
+ `Start policy: ${tour.startPolicy || "prompt_only"}`,
2198
+ tour.featureKey ? `Feature key: ${tour.featureKey}` : null,
2199
+ tour.targetUserTypes?.length ? `Target users: ${tour.targetUserTypes.join(", ")}` : null,
2200
+ `Steps: ${tour.steps?.length ?? 0}`,
2201
+ tour.goal?.primaryAction ? `Primary action: ${tour.goal.primaryAction}` : null
2202
+ ].filter(Boolean);
2203
+ if (options?.includeSteps) {
2204
+ const stepPreview = (tour.steps || []).slice(0, stepLimit).map((step, index) => summarizeWorkflowStep(step, index)).join("\n");
2205
+ if (stepPreview) {
2206
+ parts.push(`Steps preview:
2207
+ ${stepPreview}`);
2208
+ }
2209
+ }
2210
+ return parts.join("\n");
2211
+ }
2212
+ function getRequestedWorkflowTypes(experienceType) {
2213
+ if (!experienceType || experienceType === "all") {
2214
+ return WORKFLOW_TYPES;
2215
+ }
2216
+ return [experienceType];
2217
+ }
2218
+ async function fetchAvailableWorkflows(getters, experienceType) {
2219
+ const serverUrl = getters.serverUrl();
2220
+ const websiteId = getters.websiteId();
2221
+ if (!serverUrl) {
2222
+ throw new Error("Server URL is not configured.");
2223
+ }
2224
+ if (!websiteId) {
2225
+ throw new Error("websiteId is required to search workflows.");
2226
+ }
2227
+ const userProfile = getters.userProfile();
2228
+ const userType = userProfile?.type || "";
2229
+ const userId = userProfile?.userId;
2230
+ const toursApiBase = getters.toursApiBase();
2231
+ const requestedTypes = getRequestedWorkflowTypes(experienceType);
2232
+ const lists = await Promise.all(
2233
+ requestedTypes.map(
2234
+ (type) => fetchTours(serverUrl, toursApiBase, websiteId, userType, userId, type)
2235
+ )
2236
+ );
2237
+ return lists.flatMap(
2238
+ (list, index) => list.map((tour) => ({
2239
+ ...tour,
2240
+ type: tour.type || requestedTypes[index]
2241
+ }))
2242
+ );
2243
+ }
2244
+ async function resolveWorkflowFromInput(getters, params) {
2245
+ const serverUrl = getters.serverUrl();
2246
+ const websiteId = getters.websiteId();
2247
+ const toursApiBase = getters.toursApiBase();
2248
+ if (!serverUrl) {
2249
+ throw new Error("Server URL is not configured.");
2250
+ }
2251
+ if (params.workflowId) {
2252
+ const requestedTypes = getRequestedWorkflowTypes(params.experienceType);
2253
+ for (const type of requestedTypes) {
2254
+ const workflow = await fetchTourById(serverUrl, toursApiBase, params.workflowId, type, websiteId);
2255
+ if (workflow) {
2256
+ return { workflow: { ...workflow, type: workflow.type || type } };
2257
+ }
2258
+ }
2259
+ }
2260
+ const query = params.name?.trim();
2261
+ if (!query) {
2262
+ return { workflow: null, reason: "Provide either workflowId or name." };
2263
+ }
2264
+ const workflows = await fetchAvailableWorkflows(getters, params.experienceType);
2265
+ const ranked = workflows.map((workflow) => ({ workflow, score: scoreWorkflowMatch(workflow, query) })).filter(({ score }) => score > 0).sort((a, b) => b.score - a.score);
2266
+ if (ranked.length === 0) {
2267
+ return {
2268
+ workflow: null,
2269
+ reason: `No workflow matched "${query}". Try search_workflows first for available options.`
2270
+ };
2271
+ }
2272
+ if (ranked.length > 1 && ranked[0].score === ranked[1].score) {
2273
+ return {
2274
+ workflow: null,
2275
+ reason: `Multiple workflows matched "${query}". Try view_workflow with an exact workflowId.
2276
+
2277
+ ` + ranked.slice(0, 3).map(({ workflow }) => formatWorkflowSummary(workflow)).join("\n\n---\n\n")
2278
+ };
2279
+ }
2280
+ return { workflow: ranked[0].workflow };
2281
+ }
2006
2282
  var screenshotSchema = import_zod2.z.object({
2007
2283
  selector: import_zod2.z.string().optional().describe("Optional CSS selector to capture a specific element. Omit to capture the full viewport.")
2008
2284
  });
@@ -2283,15 +2559,91 @@ function createSearchTaggedElementsAction(getTagStore) {
2283
2559
  }
2284
2560
  };
2285
2561
  }
2562
+ var workflowExperienceTypeSchema = import_zod2.z.enum(["onboarding", "tour", "all"]);
2563
+ var searchWorkflowsSchema = import_zod2.z.object({
2564
+ query: import_zod2.z.string().optional().describe("Optional workflow name, feature, or task phrase to search for. Omit to list available workflows."),
2565
+ experienceType: workflowExperienceTypeSchema.optional().describe("Which workflow type to search: onboarding, tour, or all. Default: onboarding."),
2566
+ limit: import_zod2.z.number().optional().describe(`Maximum number of workflows to return (default: ${DEFAULT_WORKFLOW_SEARCH_LIMIT}).`)
2567
+ });
2568
+ function createSearchWorkflowsAction(getters) {
2569
+ return {
2570
+ id: "search_workflows",
2571
+ 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.",
2572
+ schema: searchWorkflowsSchema,
2573
+ execute: async (params) => {
2574
+ const workflows = await fetchAvailableWorkflows(getters, params.experienceType ?? "onboarding");
2575
+ if (workflows.length === 0) {
2576
+ return "No workflows are currently available for this website/user.";
2577
+ }
2578
+ const query = params.query?.trim();
2579
+ const limit = params.limit ?? DEFAULT_WORKFLOW_SEARCH_LIMIT;
2580
+ const ranked = workflows.map((workflow) => ({
2581
+ workflow,
2582
+ score: query ? scoreWorkflowMatch(workflow, query) : 1
2583
+ })).filter(({ score }) => score > 0).sort((a, b) => b.score - a.score).slice(0, limit);
2584
+ if (ranked.length === 0) {
2585
+ return `No workflows matched "${query}".`;
2586
+ }
2587
+ return ranked.map(({ workflow }) => formatWorkflowSummary(workflow)).join("\n\n---\n\n");
2588
+ }
2589
+ };
2590
+ }
2591
+ var resolveWorkflowSchema = import_zod2.z.object({
2592
+ workflowId: import_zod2.z.string().optional().describe("Exact workflow id when known."),
2593
+ name: import_zod2.z.string().optional().describe("Workflow name or a close name match when the id is unknown."),
2594
+ experienceType: workflowExperienceTypeSchema.optional().describe("Which workflow type to search: onboarding, tour, or all. Default: onboarding.")
2595
+ });
2596
+ function createViewWorkflowAction(getters) {
2597
+ return {
2598
+ id: "view_workflow",
2599
+ 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.",
2600
+ schema: resolveWorkflowSchema,
2601
+ execute: async (params) => {
2602
+ const { workflow, reason } = await resolveWorkflowFromInput(getters, params);
2603
+ if (!workflow) {
2604
+ return reason || "Workflow not found.";
2605
+ }
2606
+ return formatWorkflowSummary(workflow, { includeSteps: true, stepLimit: 5 });
2607
+ }
2608
+ };
2609
+ }
2610
+ var startWorkflowSchema = resolveWorkflowSchema.extend({
2611
+ reviewMode: import_zod2.z.boolean().optional().describe("Optional. Start in review mode instead of normal playback. Default: false.")
2612
+ });
2613
+ function createStartWorkflowAction(getters) {
2614
+ return {
2615
+ id: "start_workflow",
2616
+ 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.",
2617
+ schema: startWorkflowSchema,
2618
+ execute: async (params) => {
2619
+ const { workflow, reason } = await resolveWorkflowFromInput(getters, params);
2620
+ if (!workflow) {
2621
+ return reason || "Workflow not found.";
2622
+ }
2623
+ const bridge = getExperienceToolBridge();
2624
+ if (!bridge) {
2625
+ return "Workflow launch is unavailable because no chat playback controller is mounted.";
2626
+ }
2627
+ bridge.startExperience(workflow, workflow.type, params.reviewMode ? {
2628
+ reviewMode: true,
2629
+ reviewMetadata: { trigger: "agent_tool" }
2630
+ } : void 0);
2631
+ return `Started workflow "${workflow.name}" (${workflow.id}).`;
2632
+ }
2633
+ };
2634
+ }
2286
2635
  var BUILTIN_ACTION_IDS = {
2287
2636
  screenshot: "take_screenshot",
2288
2637
  click: "click_element",
2289
2638
  fill: "fill_input",
2290
2639
  wait: "request_user_action",
2291
2640
  searchDocs: "search_docs",
2292
- searchTaggedElements: "search_tagged_elements"
2641
+ searchTaggedElements: "search_tagged_elements",
2642
+ searchWorkflows: "search_workflows",
2643
+ viewWorkflow: "view_workflow",
2644
+ startWorkflow: "start_workflow"
2293
2645
  };
2294
- function useBuiltinActions(registerAction, unregisterAction, tagStore, serverUrl, websiteId) {
2646
+ function useBuiltinActions(registerAction, unregisterAction, tagStore, serverUrl, websiteId, toursApiBase, userProfile) {
2295
2647
  const registeredRef = (0, import_react8.useRef)(false);
2296
2648
  const tagStoreRef = (0, import_react8.useRef)(tagStore);
2297
2649
  tagStoreRef.current = tagStore;
@@ -2299,18 +2651,33 @@ function useBuiltinActions(registerAction, unregisterAction, tagStore, serverUrl
2299
2651
  serverUrlRef.current = serverUrl;
2300
2652
  const websiteIdRef = (0, import_react8.useRef)(websiteId);
2301
2653
  websiteIdRef.current = websiteId;
2654
+ const toursApiBaseRef = (0, import_react8.useRef)(toursApiBase);
2655
+ toursApiBaseRef.current = toursApiBase;
2656
+ const userProfileRef = (0, import_react8.useRef)(userProfile);
2657
+ userProfileRef.current = userProfile;
2302
2658
  (0, import_react8.useEffect)(() => {
2303
2659
  if (registeredRef.current) return;
2304
2660
  registeredRef.current = true;
2305
2661
  const getTagStore = () => tagStoreRef.current;
2306
2662
  const getServerUrl = () => serverUrlRef.current;
2307
2663
  const getWebsiteId = () => websiteIdRef.current;
2664
+ const getToursApiBase = () => toursApiBaseRef.current;
2665
+ const getUserProfile = () => userProfileRef.current;
2666
+ const workflowToolGetters = {
2667
+ serverUrl: getServerUrl,
2668
+ websiteId: getWebsiteId,
2669
+ toursApiBase: getToursApiBase,
2670
+ userProfile: getUserProfile
2671
+ };
2308
2672
  registerAction(BUILTIN_SCREENSHOT_ACTION);
2309
2673
  registerAction(createClickAction(getTagStore));
2310
2674
  registerAction(createFillAction(getTagStore));
2311
2675
  registerAction(createWaitAction(getTagStore));
2312
2676
  registerAction(createSearchDocsAction(getServerUrl, getWebsiteId));
2313
2677
  registerAction(createSearchTaggedElementsAction(getTagStore));
2678
+ registerAction(createSearchWorkflowsAction(workflowToolGetters));
2679
+ registerAction(createViewWorkflowAction(workflowToolGetters));
2680
+ registerAction(createStartWorkflowAction(workflowToolGetters));
2314
2681
  return () => {
2315
2682
  unregisterAction(BUILTIN_SCREENSHOT_ACTION.id);
2316
2683
  unregisterAction(BUILTIN_ACTION_IDS.click);
@@ -2318,6 +2685,9 @@ function useBuiltinActions(registerAction, unregisterAction, tagStore, serverUrl
2318
2685
  unregisterAction(BUILTIN_ACTION_IDS.wait);
2319
2686
  unregisterAction(BUILTIN_ACTION_IDS.searchDocs);
2320
2687
  unregisterAction(BUILTIN_ACTION_IDS.searchTaggedElements);
2688
+ unregisterAction(BUILTIN_ACTION_IDS.searchWorkflows);
2689
+ unregisterAction(BUILTIN_ACTION_IDS.viewWorkflow);
2690
+ unregisterAction(BUILTIN_ACTION_IDS.startWorkflow);
2321
2691
  registeredRef.current = false;
2322
2692
  };
2323
2693
  }, [registerAction, unregisterAction]);
@@ -2572,120 +2942,6 @@ function readPreviewSessionSuppression() {
2572
2942
  }
2573
2943
  }
2574
2944
 
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
- return trigger ?? "first_visit";
2671
- }
2672
- function getStartPolicy(tour) {
2673
- return tour.startPolicy ?? (normalizeTrigger(tour.trigger) === "manual" ? "immediate_start" : "prompt_only");
2674
- }
2675
- function getNotificationType(tour) {
2676
- return tour.notificationType ?? "bubble_card";
2677
- }
2678
- function isTourEligible(tour, userProfile) {
2679
- switch (normalizeTrigger(tour.trigger)) {
2680
- case "first_visit":
2681
- return !!userProfile.isNewUser;
2682
- case "manual":
2683
- return false;
2684
- default:
2685
- return false;
2686
- }
2687
- }
2688
-
2689
2945
  // src/hooks/useTourPlayback.ts
2690
2946
  var import_react12 = require("react");
2691
2947
 
@@ -8196,6 +8452,58 @@ function buildTranscriptPreviewLines(transcript, options = {}) {
8196
8452
  return lines.slice(-maxLines);
8197
8453
  }
8198
8454
 
8455
+ // src/utils/pendingPromptUi.ts
8456
+ function isPendingReviewPrompt(pendingPrompt) {
8457
+ return Boolean(pendingPrompt?.options?.reviewMode);
8458
+ }
8459
+ function shouldRenderPendingPromptInline({
8460
+ pendingPrompt,
8461
+ isPlaybackActive,
8462
+ recordingMode
8463
+ }) {
8464
+ return Boolean(
8465
+ pendingPrompt && !isPlaybackActive && !recordingMode && !isPendingReviewPrompt(pendingPrompt)
8466
+ );
8467
+ }
8468
+ function shouldRenderPendingPromptModal({
8469
+ pendingPrompt,
8470
+ isPlaybackActive,
8471
+ recordingMode,
8472
+ pendingNotificationType
8473
+ }) {
8474
+ return Boolean(
8475
+ pendingPrompt && !isPlaybackActive && !recordingMode && isPendingReviewPrompt(pendingPrompt) && pendingNotificationType === "modal"
8476
+ );
8477
+ }
8478
+ function shouldAutoExpandForPendingPrompt({
8479
+ pendingPrompt,
8480
+ isPlaybackActive,
8481
+ recordingMode,
8482
+ pendingNotificationType
8483
+ }) {
8484
+ if (!pendingPrompt || isPlaybackActive || recordingMode) return false;
8485
+ return pendingNotificationType === "bubble_card" || shouldRenderPendingPromptInline({
8486
+ pendingPrompt,
8487
+ isPlaybackActive,
8488
+ recordingMode
8489
+ });
8490
+ }
8491
+ function getPendingPromptTitle(pendingPrompt) {
8492
+ return `I found a ${pendingPrompt.experienceType === "onboarding" ? "workflow" : "tour"} called "${pendingPrompt.tour.name}". Would you like to start it now?`;
8493
+ }
8494
+ function getPendingPromptReason(pendingPrompt) {
8495
+ if (pendingPrompt.tour.trigger === "first_visit") {
8496
+ return "This was surfaced because the current user matches the configured first-visit trigger.";
8497
+ }
8498
+ if (pendingPrompt.tour.trigger === "return_visit") {
8499
+ return "This was surfaced because the current user matches the configured return-visit trigger.";
8500
+ }
8501
+ if (pendingPrompt.tour.featureKey) {
8502
+ return `This was surfaced because the "${pendingPrompt.tour.featureKey}" trigger condition matched.`;
8503
+ }
8504
+ return "This was surfaced because the configured trigger condition matched.";
8505
+ }
8506
+
8199
8507
  // src/utils/floatingLiveTranscript.ts
8200
8508
  var floatingTranscriptEl = null;
8201
8509
  var liveTranscriptSuppressed = false;
@@ -8549,6 +8857,7 @@ function Tooltip({ children, title }) {
8549
8857
  );
8550
8858
  }
8551
8859
  var BUBBLE_EXPANDED_STORAGE_KEY = "modelnex-chat-bubble-expanded";
8860
+ var BUBBLE_DOCKED_STORAGE_KEY = "modelnex-chat-bubble-docked";
8552
8861
  var TOUR_MINIMIZED_STORAGE_KEY = "modelnex-tour-bubble-minimized";
8553
8862
  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: [
8554
8863
  /* @__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" }),
@@ -8562,6 +8871,18 @@ var CloseIcon = () => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("svg", { wid
8562
8871
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
8563
8872
  ] });
8564
8873
  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" }) });
8874
+ 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: [
8875
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "M12 3v10" }),
8876
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "m8 9 4 4 4-4" }),
8877
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "M4 17h16" }),
8878
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "M6 21h12" })
8879
+ ] });
8880
+ 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: [
8881
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "M12 21V11" }),
8882
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "m8 15 4-4 4 4" }),
8883
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "M4 7h16" }),
8884
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "M6 3h12" })
8885
+ ] });
8565
8886
  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: [
8566
8887
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "M3 6h18" }),
8567
8888
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" }),
@@ -8782,6 +9103,7 @@ function ModelNexChatBubble({
8782
9103
  const ctx = (0, import_react18.useContext)(ModelNexContext);
8783
9104
  const [hydrated, setHydrated] = (0, import_react18.useState)(false);
8784
9105
  const [expanded, setExpanded] = (0, import_react18.useState)(false);
9106
+ const [docked, setDocked] = (0, import_react18.useState)(false);
8785
9107
  const [input, setInput] = (0, import_react18.useState)("");
8786
9108
  const messages = ctx?.chatMessages ?? [];
8787
9109
  const setMessages = ctx?.setChatMessages ?? (() => {
@@ -8836,6 +9158,11 @@ function ModelNexChatBubble({
8836
9158
  const activePlayback = playbackController.playback;
8837
9159
  const activeExperienceType = playbackController.activeExperienceType;
8838
9160
  const startingExperienceType = playbackController.startingExperienceType;
9161
+ (0, import_react18.useEffect)(() => {
9162
+ return registerExperienceToolBridge({
9163
+ startExperience: playbackController.startExperience
9164
+ });
9165
+ }, [playbackController.startExperience]);
8839
9166
  const createPlaybackView = (0, import_react18.useCallback)((experienceType) => {
8840
9167
  const isActiveExperience = activePlayback.isActive && activeExperienceType === experienceType;
8841
9168
  const pendingTour = playbackController.pendingPrompt?.experienceType === experienceType ? playbackController.pendingPrompt.tour : null;
@@ -8915,13 +9242,28 @@ function ModelNexChatBubble({
8915
9242
  }
8916
9243
  }, [devMode, handleAutoTag, ctx?.extractedElements.length, window.location.pathname]);
8917
9244
  const onboardingReviewToggle = getReviewModeToggleConfig(onboardingPlayback.playbackState);
9245
+ const pendingPrompt = playbackController.pendingPrompt;
8918
9246
  const pendingNotificationType = (onboardingPlayback.pendingTour || tourPlayback.pendingTour)?.notificationType ?? "bubble_card";
9247
+ const isPlaybackActive = tourPlayback.isActive || onboardingPlayback.isActive;
9248
+ const renderPendingPromptInline = shouldRenderPendingPromptInline({
9249
+ pendingPrompt,
9250
+ isPlaybackActive,
9251
+ recordingMode
9252
+ });
9253
+ const renderPendingPromptModal = shouldRenderPendingPromptModal({
9254
+ pendingPrompt,
9255
+ isPlaybackActive,
9256
+ recordingMode,
9257
+ pendingNotificationType
9258
+ });
8919
9259
  (0, import_react18.useEffect)(() => {
8920
9260
  setHydrated(true);
8921
9261
  try {
8922
9262
  setExpanded(sessionStorage.getItem(BUBBLE_EXPANDED_STORAGE_KEY) === "true");
9263
+ setDocked(sessionStorage.getItem(BUBBLE_DOCKED_STORAGE_KEY) === "true");
8923
9264
  } catch {
8924
9265
  setExpanded(false);
9266
+ setDocked(false);
8925
9267
  }
8926
9268
  }, []);
8927
9269
  const sttActiveRef = (0, import_react18.useRef)(false);
@@ -8981,11 +9323,23 @@ function ModelNexChatBubble({
8981
9323
  } catch {
8982
9324
  }
8983
9325
  }, [tourPlayback.isActive, onboardingPlayback.isActive]);
9326
+ const setDockedState = (0, import_react18.useCallback)((next) => {
9327
+ setDocked(next);
9328
+ try {
9329
+ sessionStorage.setItem(BUBBLE_DOCKED_STORAGE_KEY, String(next));
9330
+ } catch {
9331
+ }
9332
+ }, []);
8984
9333
  (0, import_react18.useEffect)(() => {
8985
- if ((onboardingPlayback.pendingTour || tourPlayback.pendingTour) && !recordingMode && pendingNotificationType === "bubble_card") {
9334
+ if (shouldAutoExpandForPendingPrompt({
9335
+ pendingPrompt,
9336
+ isPlaybackActive,
9337
+ recordingMode,
9338
+ pendingNotificationType
9339
+ })) {
8986
9340
  setExpandedState(true);
8987
9341
  }
8988
- }, [onboardingPlayback.pendingTour, tourPlayback.pendingTour, recordingMode, pendingNotificationType, setExpandedState]);
9342
+ }, [isPlaybackActive, pendingNotificationType, pendingPrompt, recordingMode, setExpandedState]);
8989
9343
  const preferredListeningExperienceRef = (0, import_react18.useRef)(null);
8990
9344
  const playbackVoiceRoutingRef = (0, import_react18.useRef)({
8991
9345
  activeExperienceType,
@@ -9086,13 +9440,13 @@ function ModelNexChatBubble({
9086
9440
  updateTourSttError
9087
9441
  ]);
9088
9442
  (0, import_react18.useEffect)(() => {
9089
- const isPlaybackActive = isTourListeningSessionActive({
9443
+ const isPlaybackActive2 = isTourListeningSessionActive({
9090
9444
  isTourActive: tourPlayback.isActive,
9091
9445
  isOnboardingActive: onboardingPlayback.isActive,
9092
9446
  startingExperienceType
9093
9447
  });
9094
- const becameActive = isPlaybackActive && !previousTourActiveRef.current;
9095
- previousTourActiveRef.current = isPlaybackActive;
9448
+ const becameActive = isPlaybackActive2 && !previousTourActiveRef.current;
9449
+ previousTourActiveRef.current = isPlaybackActive2;
9096
9450
  if (becameActive) {
9097
9451
  try {
9098
9452
  sessionStorage.setItem(TOUR_MINIMIZED_STORAGE_KEY, "true");
@@ -9101,7 +9455,7 @@ function ModelNexChatBubble({
9101
9455
  setExpandedState(false, { rememberTourMinimize: true });
9102
9456
  updateTourSttError(null);
9103
9457
  }
9104
- if (!isPlaybackActive) {
9458
+ if (!isPlaybackActive2) {
9105
9459
  try {
9106
9460
  sessionStorage.removeItem(TOUR_MINIMIZED_STORAGE_KEY);
9107
9461
  } catch {
@@ -9128,12 +9482,12 @@ function ModelNexChatBubble({
9128
9482
  }
9129
9483
  }, [recordingMode, setExpandedState]);
9130
9484
  (0, import_react18.useEffect)(() => {
9131
- const isPlaybackActive = isTourListeningSessionActive({
9485
+ const isPlaybackActive2 = isTourListeningSessionActive({
9132
9486
  isTourActive: tourPlayback.isActive,
9133
9487
  isOnboardingActive: onboardingPlayback.isActive,
9134
9488
  startingExperienceType
9135
9489
  });
9136
- if (!isPlaybackActive) {
9490
+ if (!isPlaybackActive2) {
9137
9491
  updateTourListenReady(false);
9138
9492
  setTourLiveTranscript("");
9139
9493
  hideFloatingLiveTranscript();
@@ -9142,12 +9496,12 @@ function ModelNexChatBubble({
9142
9496
  updateTourListenReady(Boolean(voice.isListening && sttActiveRef.current));
9143
9497
  }, [tourPlayback.isActive, onboardingPlayback.isActive, voice.isListening, startingExperienceType, updateTourListenReady]);
9144
9498
  (0, import_react18.useEffect)(() => {
9145
- const isPlaybackActive = isTourListeningSessionActive({
9499
+ const isPlaybackActive2 = isTourListeningSessionActive({
9146
9500
  isTourActive: tourPlayback.isActive,
9147
9501
  isOnboardingActive: onboardingPlayback.isActive,
9148
9502
  startingExperienceType
9149
9503
  });
9150
- if (!isPlaybackActive && sttActiveRef.current) {
9504
+ if (!isPlaybackActive2 && sttActiveRef.current) {
9151
9505
  sttActiveRef.current = false;
9152
9506
  updateTourListenReady(false);
9153
9507
  updateTourSttError(null);
@@ -9157,12 +9511,12 @@ function ModelNexChatBubble({
9157
9511
  }
9158
9512
  }, [tourPlayback.isActive, onboardingPlayback.isActive, voice, startingExperienceType, updateTourListenReady, updateTourSttError]);
9159
9513
  (0, import_react18.useEffect)(() => {
9160
- const isPlaybackActive = isTourListeningSessionActive({
9514
+ const isPlaybackActive2 = isTourListeningSessionActive({
9161
9515
  isTourActive: tourPlayback.isActive,
9162
9516
  isOnboardingActive: onboardingPlayback.isActive,
9163
9517
  startingExperienceType
9164
9518
  });
9165
- if (!isPlaybackActive || tourListenReady || !voice.sttSupported || tourSttError === "not-allowed") {
9519
+ if (!isPlaybackActive2 || tourListenReady || !voice.sttSupported || tourSttError === "not-allowed") {
9166
9520
  return;
9167
9521
  }
9168
9522
  const enableTourListeningFromGesture = (event) => {
@@ -9337,11 +9691,13 @@ function ModelNexChatBubble({
9337
9691
  fontFamily: "var(--modelnex-font)",
9338
9692
  ...themeStyles
9339
9693
  };
9694
+ const desktopPanelHeight = docked ? "min(calc(100vh - 48px), calc(var(--modelnex-panel-max-height, 600px) + 180px))" : "var(--modelnex-panel-max-height, 600px)";
9695
+ const desktopPanelMaxHeight = docked ? "calc(100vh - 48px)" : "calc(100vh - 120px)";
9340
9696
  const panelStyle = {
9341
9697
  width: isMobile ? "100%" : "var(--modelnex-panel-width, 380px)",
9342
9698
  maxWidth: "calc(100vw - 32px)",
9343
- height: isMobile ? "100%" : "var(--modelnex-panel-max-height, 600px)",
9344
- maxHeight: "calc(100vh - 120px)",
9699
+ height: isMobile ? "100%" : desktopPanelHeight,
9700
+ maxHeight: isMobile ? "100%" : desktopPanelMaxHeight,
9345
9701
  borderRadius: isMobile ? "0" : "var(--modelnex-radius-panel, 20px)",
9346
9702
  border: isMobile ? "none" : "1px solid var(--modelnex-border, #e4e4e7)",
9347
9703
  background: "var(--modelnex-bg, #ffffff)",
@@ -9369,8 +9725,8 @@ function ModelNexChatBubble({
9369
9725
  return (0, import_react_dom.createPortal)(
9370
9726
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: containerStyle, className, "data-modelnex-internal": "true", onMouseDown: stopEventPropagation, onClick: stopEventPropagation, children: [
9371
9727
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("style", { children: GLOBAL_STYLES }),
9372
- !(tourPlayback.isActive || onboardingPlayback.isActive) && (tourPlayback.pendingTour || onboardingPlayback.pendingTour) && pendingNotificationType === "modal" && !recordingMode && (() => {
9373
- const pt = onboardingPlayback.pendingTour || tourPlayback.pendingTour;
9728
+ renderPendingPromptModal && (() => {
9729
+ const pt = pendingPrompt?.tour;
9374
9730
  const mc = pt?.presentation?.modalConfig || {};
9375
9731
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
9376
9732
  "div",
@@ -9432,8 +9788,8 @@ function ModelNexChatBubble({
9432
9788
  "button",
9433
9789
  {
9434
9790
  onClick: () => {
9435
- const preferredExperience = onboardingPlayback.pendingTour ? "onboarding" : "tour";
9436
- if (onboardingPlayback.pendingTour) {
9791
+ const preferredExperience = pendingPrompt?.experienceType === "onboarding" ? "onboarding" : "tour";
9792
+ if (preferredExperience === "onboarding") {
9437
9793
  onboardingPlayback.acceptPendingTour();
9438
9794
  } else {
9439
9795
  tourPlayback.acceptPendingTour();
@@ -9493,6 +9849,31 @@ function ModelNexChatBubble({
9493
9849
  ] })
9494
9850
  ] }) }),
9495
9851
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: "4px" }, children: [
9852
+ !isMobile && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Tooltip, { title: docked ? "Undock" : "Dock", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
9853
+ "button",
9854
+ {
9855
+ onClick: () => setDockedState(!docked),
9856
+ style: {
9857
+ padding: "8px 10px",
9858
+ borderRadius: "10px",
9859
+ border: "none",
9860
+ background: docked ? "rgba(79,70,229,0.08)" : "transparent",
9861
+ cursor: "pointer",
9862
+ color: docked ? "var(--modelnex-accent, #4f46e5)" : "#71717a",
9863
+ transition: "all 0.2s",
9864
+ display: "flex",
9865
+ alignItems: "center",
9866
+ gap: "6px",
9867
+ fontSize: "12px",
9868
+ fontWeight: 700
9869
+ },
9870
+ "aria-label": docked ? "Undock chat bubble" : "Dock chat bubble",
9871
+ children: [
9872
+ docked ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(UndockIcon, {}) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(DockIcon, {}),
9873
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: docked ? "Undock" : "Dock" })
9874
+ ]
9875
+ }
9876
+ ) }),
9496
9877
  (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)(
9497
9878
  "button",
9498
9879
  {
@@ -9596,24 +9977,28 @@ function ModelNexChatBubble({
9596
9977
  children: "Microphone access is blocked. Use the input box below, or allow microphone permission and tap the mic again."
9597
9978
  }
9598
9979
  ),
9599
- !(tourPlayback.isActive || onboardingPlayback.isActive) && (tourPlayback.pendingTour || onboardingPlayback.pendingTour) && pendingNotificationType === "bubble_card" && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
9980
+ renderPendingPromptInline && pendingPrompt && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { display: "flex", justifyContent: "flex-start" }, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
9600
9981
  "div",
9601
9982
  {
9602
9983
  style: {
9603
- margin: "12px",
9604
- padding: "14px",
9605
- borderRadius: "12px",
9606
- border: "1px solid rgba(59,130,246,0.18)",
9607
- background: "linear-gradient(135deg, rgba(59,130,246,0.08) 0%, rgba(59,130,246,0.03) 100%)"
9984
+ maxWidth: "85%",
9985
+ padding: "12px 16px",
9986
+ borderRadius: "var(--modelnex-radius-inner, 16px)",
9987
+ borderBottomLeftRadius: 4,
9988
+ fontSize: "14px",
9989
+ lineHeight: 1.55,
9990
+ background: "var(--modelnex-bg, #fff)",
9991
+ color: "var(--modelnex-fg, #18181b)",
9992
+ border: "1px solid var(--modelnex-border, #e4e4e7)",
9993
+ boxShadow: "0 2px 4px rgba(0,0,0,0.02)"
9608
9994
  },
9609
9995
  children: [
9610
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { fontSize: "13px", fontWeight: 600, color: "var(--modelnex-fg, #18181b)", marginBottom: "6px" }, children: onboardingPlayback.pendingTour ? "Suggested workflow" : "Suggested tour" }),
9611
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { fontSize: "14px", color: "var(--modelnex-fg, #27272a)", marginBottom: "6px" }, children: (onboardingPlayback.pendingTour || tourPlayback.pendingTour)?.name }),
9612
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { fontSize: "12px", color: "#52525b", lineHeight: 1.45, marginBottom: "12px" }, children: (onboardingPlayback.pendingTour || tourPlayback.pendingTour)?.trigger === "first_visit" ? "Start a short walkthrough for this user now?" : (onboardingPlayback.pendingTour || tourPlayback.pendingTour)?.featureKey ? `A walkthrough is available for ${(onboardingPlayback.pendingTour || tourPlayback.pendingTour)?.featureKey}.` : "A walkthrough is available for this user." }),
9996
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { fontWeight: 500, marginBottom: "8px" }, children: getPendingPromptTitle(pendingPrompt) }),
9997
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { fontSize: "12px", color: "#52525b", marginBottom: "12px" }, children: getPendingPromptReason(pendingPrompt) }),
9613
9998
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { display: "flex", gap: "8px" }, children: [
9614
9999
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("button", { className: "btn btn-primary btn-sm", onClick: () => {
9615
- const preferredExperience = onboardingPlayback.pendingTour ? "onboarding" : "tour";
9616
- if (onboardingPlayback.pendingTour) {
10000
+ const preferredExperience = pendingPrompt.experienceType === "onboarding" ? "onboarding" : "tour";
10001
+ if (preferredExperience === "onboarding") {
9617
10002
  onboardingPlayback.acceptPendingTour();
9618
10003
  } else {
9619
10004
  tourPlayback.acceptPendingTour();
@@ -9621,13 +10006,13 @@ function ModelNexChatBubble({
9621
10006
  startTourListening(preferredExperience);
9622
10007
  }, children: [
9623
10008
  "Start ",
9624
- onboardingPlayback.pendingTour ? "workflow" : "tour"
10009
+ pendingPrompt.experienceType === "onboarding" ? "workflow" : "tour"
9625
10010
  ] }),
9626
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { className: "btn btn-secondary btn-sm", onClick: onboardingPlayback.pendingTour ? onboardingPlayback.dismissPendingTour : tourPlayback.dismissPendingTour, children: "Not now" })
10011
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { className: "btn btn-secondary btn-sm", onClick: pendingPrompt.experienceType === "onboarding" ? onboardingPlayback.dismissPendingTour : tourPlayback.dismissPendingTour, children: "Not now" })
9627
10012
  ] })
9628
10013
  ]
9629
10014
  }
9630
- ),
10015
+ ) }),
9631
10016
  tourPlayback.isActive && tourPlayback.isReviewMode && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
9632
10017
  "div",
9633
10018
  {
@@ -10206,7 +10591,7 @@ function ModelNexChatBubble({
10206
10591
  )
10207
10592
  ] })
10208
10593
  ] }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
10209
- messages.length === 0 && !loading && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
10594
+ messages.length === 0 && !loading && !renderPendingPromptInline && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
10210
10595
  "div",
10211
10596
  {
10212
10597
  style: {
@@ -11279,7 +11664,7 @@ var ModelNexProvider = ({
11279
11664
  }, []);
11280
11665
  const extractedElements = useAutoExtract();
11281
11666
  const tagStore = useTagStore({ serverUrl, websiteId });
11282
- useBuiltinActions(registerAction, unregisterAction, tagStore, serverUrl, websiteId);
11667
+ useBuiltinActions(registerAction, unregisterAction, tagStore, serverUrl, websiteId, toursApiBase, userProfile);
11283
11668
  const CHAT_STORAGE_KEY = "modelnex-chat-messages";
11284
11669
  const [chatMessages, setChatMessagesRaw] = (0, import_react21.useState)([]);
11285
11670
  (0, import_react21.useEffect)(() => {