@modelnex/sdk 0.5.27 → 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.
Files changed (3) hide show
  1. package/dist/index.js +430 -134
  2. package/dist/index.mjs +430 -134
  3. package/package.json +1 -1
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,135 +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
- 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
2945
  // src/hooks/useTourPlayback.ts
2705
2946
  var import_react12 = require("react");
2706
2947
 
@@ -8616,6 +8857,7 @@ function Tooltip({ children, title }) {
8616
8857
  );
8617
8858
  }
8618
8859
  var BUBBLE_EXPANDED_STORAGE_KEY = "modelnex-chat-bubble-expanded";
8860
+ var BUBBLE_DOCKED_STORAGE_KEY = "modelnex-chat-bubble-docked";
8619
8861
  var TOUR_MINIMIZED_STORAGE_KEY = "modelnex-tour-bubble-minimized";
8620
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: [
8621
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" }),
@@ -8629,6 +8871,18 @@ var CloseIcon = () => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("svg", { wid
8629
8871
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
8630
8872
  ] });
8631
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
+ ] });
8632
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: [
8633
8887
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "M3 6h18" }),
8634
8888
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" }),
@@ -8849,6 +9103,7 @@ function ModelNexChatBubble({
8849
9103
  const ctx = (0, import_react18.useContext)(ModelNexContext);
8850
9104
  const [hydrated, setHydrated] = (0, import_react18.useState)(false);
8851
9105
  const [expanded, setExpanded] = (0, import_react18.useState)(false);
9106
+ const [docked, setDocked] = (0, import_react18.useState)(false);
8852
9107
  const [input, setInput] = (0, import_react18.useState)("");
8853
9108
  const messages = ctx?.chatMessages ?? [];
8854
9109
  const setMessages = ctx?.setChatMessages ?? (() => {
@@ -8903,6 +9158,11 @@ function ModelNexChatBubble({
8903
9158
  const activePlayback = playbackController.playback;
8904
9159
  const activeExperienceType = playbackController.activeExperienceType;
8905
9160
  const startingExperienceType = playbackController.startingExperienceType;
9161
+ (0, import_react18.useEffect)(() => {
9162
+ return registerExperienceToolBridge({
9163
+ startExperience: playbackController.startExperience
9164
+ });
9165
+ }, [playbackController.startExperience]);
8906
9166
  const createPlaybackView = (0, import_react18.useCallback)((experienceType) => {
8907
9167
  const isActiveExperience = activePlayback.isActive && activeExperienceType === experienceType;
8908
9168
  const pendingTour = playbackController.pendingPrompt?.experienceType === experienceType ? playbackController.pendingPrompt.tour : null;
@@ -9000,8 +9260,10 @@ function ModelNexChatBubble({
9000
9260
  setHydrated(true);
9001
9261
  try {
9002
9262
  setExpanded(sessionStorage.getItem(BUBBLE_EXPANDED_STORAGE_KEY) === "true");
9263
+ setDocked(sessionStorage.getItem(BUBBLE_DOCKED_STORAGE_KEY) === "true");
9003
9264
  } catch {
9004
9265
  setExpanded(false);
9266
+ setDocked(false);
9005
9267
  }
9006
9268
  }, []);
9007
9269
  const sttActiveRef = (0, import_react18.useRef)(false);
@@ -9061,6 +9323,13 @@ function ModelNexChatBubble({
9061
9323
  } catch {
9062
9324
  }
9063
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
+ }, []);
9064
9333
  (0, import_react18.useEffect)(() => {
9065
9334
  if (shouldAutoExpandForPendingPrompt({
9066
9335
  pendingPrompt,
@@ -9422,11 +9691,13 @@ function ModelNexChatBubble({
9422
9691
  fontFamily: "var(--modelnex-font)",
9423
9692
  ...themeStyles
9424
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)";
9425
9696
  const panelStyle = {
9426
9697
  width: isMobile ? "100%" : "var(--modelnex-panel-width, 380px)",
9427
9698
  maxWidth: "calc(100vw - 32px)",
9428
- height: isMobile ? "100%" : "var(--modelnex-panel-max-height, 600px)",
9429
- maxHeight: "calc(100vh - 120px)",
9699
+ height: isMobile ? "100%" : desktopPanelHeight,
9700
+ maxHeight: isMobile ? "100%" : desktopPanelMaxHeight,
9430
9701
  borderRadius: isMobile ? "0" : "var(--modelnex-radius-panel, 20px)",
9431
9702
  border: isMobile ? "none" : "1px solid var(--modelnex-border, #e4e4e7)",
9432
9703
  background: "var(--modelnex-bg, #ffffff)",
@@ -9578,6 +9849,31 @@ function ModelNexChatBubble({
9578
9849
  ] })
9579
9850
  ] }) }),
9580
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
+ ) }),
9581
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)(
9582
9878
  "button",
9583
9879
  {
@@ -11368,7 +11664,7 @@ var ModelNexProvider = ({
11368
11664
  }, []);
11369
11665
  const extractedElements = useAutoExtract();
11370
11666
  const tagStore = useTagStore({ serverUrl, websiteId });
11371
- useBuiltinActions(registerAction, unregisterAction, tagStore, serverUrl, websiteId);
11667
+ useBuiltinActions(registerAction, unregisterAction, tagStore, serverUrl, websiteId, toursApiBase, userProfile);
11372
11668
  const CHAT_STORAGE_KEY = "modelnex-chat-messages";
11373
11669
  const [chatMessages, setChatMessagesRaw] = (0, import_react21.useState)([]);
11374
11670
  (0, import_react21.useEffect)(() => {