@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.mjs CHANGED
@@ -1617,8 +1617,153 @@ async function captureScreenshot(selector) {
1617
1617
  return canvas.toDataURL("image/png");
1618
1618
  }
1619
1619
 
1620
+ // src/utils/experience-tool-bridge.ts
1621
+ var activeBridge = null;
1622
+ function registerExperienceToolBridge(bridge) {
1623
+ activeBridge = bridge;
1624
+ return () => {
1625
+ if (activeBridge === bridge) {
1626
+ activeBridge = null;
1627
+ }
1628
+ };
1629
+ }
1630
+ function getExperienceToolBridge() {
1631
+ return activeBridge;
1632
+ }
1633
+
1634
+ // src/utils/tourDiscovery.ts
1635
+ async function fetchTours(serverUrl, toursApiBase, websiteId, userType, userId, experienceType = "tour") {
1636
+ try {
1637
+ const params = new URLSearchParams({
1638
+ websiteId,
1639
+ userType
1640
+ });
1641
+ if (userId) params.set("userId", userId);
1642
+ if (experienceType !== "tour") params.set("type", experienceType);
1643
+ const res = await fetch(getTourApiUrl(serverUrl, toursApiBase, `/tours?${params.toString()}`));
1644
+ if (!res.ok) return [];
1645
+ const data = await res.json();
1646
+ return data.tours ?? [];
1647
+ } catch {
1648
+ return [];
1649
+ }
1650
+ }
1651
+ async function fetchTourById(serverUrl, toursApiBase, tourId, experienceType = "tour", websiteId) {
1652
+ try {
1653
+ const url = new URL(withWebsiteId(getTourApiUrl(serverUrl, toursApiBase, `/tours/${tourId}`), websiteId));
1654
+ if (experienceType !== "tour") {
1655
+ url.searchParams.set("type", experienceType);
1656
+ }
1657
+ const res = await fetch(url.toString());
1658
+ if (!res.ok) {
1659
+ return null;
1660
+ }
1661
+ const data = await res.json();
1662
+ return data.tour ?? null;
1663
+ } catch {
1664
+ return null;
1665
+ }
1666
+ }
1667
+ async function markTourComplete(serverUrl, toursApiBase, tourId, userId, experienceType = "tour", websiteId) {
1668
+ try {
1669
+ const url = new URL(withWebsiteId(getTourApiUrl(serverUrl, toursApiBase, `/tours/${tourId}/complete`), websiteId));
1670
+ if (experienceType !== "tour") {
1671
+ url.searchParams.set("type", experienceType);
1672
+ }
1673
+ await fetch(url.toString(), {
1674
+ method: "POST",
1675
+ headers: { "Content-Type": "application/json" },
1676
+ body: JSON.stringify({ userId })
1677
+ });
1678
+ } catch {
1679
+ }
1680
+ }
1681
+ async function markTourDismissed(serverUrl, toursApiBase, tourId, userId, experienceType = "tour", websiteId) {
1682
+ try {
1683
+ const url = new URL(withWebsiteId(getTourApiUrl(serverUrl, toursApiBase, `/tours/${tourId}/dismiss`), websiteId));
1684
+ if (experienceType !== "tour") {
1685
+ url.searchParams.set("type", experienceType);
1686
+ }
1687
+ await fetch(url.toString(), {
1688
+ method: "POST",
1689
+ headers: { "Content-Type": "application/json" },
1690
+ body: JSON.stringify({ userId })
1691
+ });
1692
+ } catch {
1693
+ }
1694
+ }
1695
+ async function recordTourEvent(serverUrl, toursApiBase, tourId, userId, eventType, websiteId) {
1696
+ try {
1697
+ const url = withWebsiteId(getTourApiUrl(serverUrl, toursApiBase, `/tours/${tourId}/events`), websiteId);
1698
+ await fetch(url, {
1699
+ method: "POST",
1700
+ headers: { "Content-Type": "application/json" },
1701
+ body: JSON.stringify({ userId, eventType })
1702
+ });
1703
+ } catch {
1704
+ }
1705
+ }
1706
+ function getToursBaseUrl(serverUrl, toursApiBase) {
1707
+ if (toursApiBase?.startsWith("/")) {
1708
+ return `${typeof window !== "undefined" ? window.location.origin : ""}${toursApiBase.replace(/\/$/, "")}`;
1709
+ }
1710
+ return serverUrl.replace(/\/$/, "");
1711
+ }
1712
+ function getTourApiUrl(serverUrl, toursApiBase, path) {
1713
+ const base = getToursBaseUrl(serverUrl, toursApiBase);
1714
+ return toursApiBase ? `${base}${path}` : `${base}/api${path}`;
1715
+ }
1716
+ function withWebsiteId(url, websiteId) {
1717
+ if (!websiteId) {
1718
+ return url;
1719
+ }
1720
+ const baseOrigin = typeof window !== "undefined" ? window.location.origin : "http://localhost";
1721
+ const resolved = new URL(url, baseOrigin);
1722
+ resolved.searchParams.set("websiteId", websiteId);
1723
+ return resolved.toString();
1724
+ }
1725
+ function normalizeTrigger(trigger) {
1726
+ if (trigger === "feature_launch" || trigger === "feature_unlocked" || trigger === "feature_unlock" || trigger === "new_feature") {
1727
+ return "feature_launch";
1728
+ }
1729
+ if (trigger === "return_visit") {
1730
+ return "return_visit";
1731
+ }
1732
+ return trigger ?? "first_visit";
1733
+ }
1734
+ function getStartPolicy(tour) {
1735
+ return tour.startPolicy ?? (normalizeTrigger(tour.trigger) === "manual" ? "immediate_start" : "prompt_only");
1736
+ }
1737
+ function getNotificationType(tour) {
1738
+ return tour.notificationType ?? "bubble_card";
1739
+ }
1740
+ function getUserProfileFeatures(userProfile) {
1741
+ const directFeatures = Array.isArray(userProfile.features) ? userProfile.features : [];
1742
+ const nestedFeatures = Array.isArray(userProfile.tourFacts?.features) ? userProfile.tourFacts.features : [];
1743
+ return [...new Set([...directFeatures, ...nestedFeatures].filter((value) => Boolean(value)))];
1744
+ }
1745
+ function isTourEligible(tour, userProfile) {
1746
+ switch (normalizeTrigger(tour.trigger)) {
1747
+ case "first_visit":
1748
+ return !!userProfile.isNewUser;
1749
+ case "return_visit":
1750
+ return userProfile.isNewUser === false;
1751
+ case "feature_launch": {
1752
+ const featureKey = tour.featureKey?.trim();
1753
+ if (!featureKey) return false;
1754
+ return getUserProfileFeatures(userProfile).includes(featureKey);
1755
+ }
1756
+ case "manual":
1757
+ return false;
1758
+ default:
1759
+ return false;
1760
+ }
1761
+ }
1762
+
1620
1763
  // src/hooks/useBuiltinActions.ts
1621
1764
  var resolutionCache = /* @__PURE__ */ new Map();
1765
+ var DEFAULT_WORKFLOW_SEARCH_LIMIT = 5;
1766
+ var WORKFLOW_TYPES = ["onboarding", "tour"];
1622
1767
  function safeQueryAll2(selector) {
1623
1768
  try {
1624
1769
  return Array.from(document.querySelectorAll(selector)).filter(
@@ -1793,6 +1938,137 @@ function lastResortScan(fingerprint, options, elements) {
1793
1938
  function cacheResolution(originalFp, selector, resolvedFp) {
1794
1939
  resolutionCache.set(originalFp, { selector, resolvedFingerprint: resolvedFp });
1795
1940
  }
1941
+ function normalizeWorkflowQuery(value) {
1942
+ return value.trim().toLowerCase();
1943
+ }
1944
+ function getWorkflowSearchText(tour) {
1945
+ const firstSteps = (tour.steps || []).slice(0, 6).flatMap((step) => [step.goal, step.narration, step.ask]).filter(Boolean).join(" ");
1946
+ return [
1947
+ tour.name,
1948
+ tour.type,
1949
+ tour.trigger,
1950
+ tour.startPolicy,
1951
+ tour.featureKey,
1952
+ tour.goal?.primaryAction,
1953
+ tour.goal?.successMetric,
1954
+ ...tour.targetUserTypes,
1955
+ firstSteps
1956
+ ].filter(Boolean).join(" ").toLowerCase();
1957
+ }
1958
+ function scoreWorkflowMatch(tour, query) {
1959
+ const normalizedQuery = normalizeWorkflowQuery(query);
1960
+ if (!normalizedQuery) return 1;
1961
+ const name = tour.name.toLowerCase();
1962
+ const featureKey = (tour.featureKey || "").toLowerCase();
1963
+ const haystack = getWorkflowSearchText(tour);
1964
+ if (name === normalizedQuery) return 140;
1965
+ if (featureKey && featureKey === normalizedQuery) return 125;
1966
+ if (name.includes(normalizedQuery)) return 115;
1967
+ if (featureKey && featureKey.includes(normalizedQuery)) return 100;
1968
+ if (haystack.includes(normalizedQuery)) return 85;
1969
+ const words = normalizedQuery.split(/\s+/).filter((word) => word.length > 1);
1970
+ if (words.length === 0) return 0;
1971
+ const matched = words.filter((word) => haystack.includes(word));
1972
+ if (matched.length === 0) return 0;
1973
+ const ratio = matched.length / words.length;
1974
+ return Math.round(ratio * 70);
1975
+ }
1976
+ function summarizeWorkflowStep(step, index) {
1977
+ const detail = step.goal || step.ask || step.narration || step.rawNarration || "No description";
1978
+ return `${index + 1}. [${step.type}] ${detail}`;
1979
+ }
1980
+ function formatWorkflowSummary(tour, options) {
1981
+ const stepLimit = options?.stepLimit ?? 4;
1982
+ const parts = [
1983
+ `**${tour.name}**`,
1984
+ `ID: ${tour.id}`,
1985
+ `Type: ${tour.type || "tour"}`,
1986
+ `Trigger: ${tour.trigger}`,
1987
+ `Start policy: ${tour.startPolicy || "prompt_only"}`,
1988
+ tour.featureKey ? `Feature key: ${tour.featureKey}` : null,
1989
+ tour.targetUserTypes?.length ? `Target users: ${tour.targetUserTypes.join(", ")}` : null,
1990
+ `Steps: ${tour.steps?.length ?? 0}`,
1991
+ tour.goal?.primaryAction ? `Primary action: ${tour.goal.primaryAction}` : null
1992
+ ].filter(Boolean);
1993
+ if (options?.includeSteps) {
1994
+ const stepPreview = (tour.steps || []).slice(0, stepLimit).map((step, index) => summarizeWorkflowStep(step, index)).join("\n");
1995
+ if (stepPreview) {
1996
+ parts.push(`Steps preview:
1997
+ ${stepPreview}`);
1998
+ }
1999
+ }
2000
+ return parts.join("\n");
2001
+ }
2002
+ function getRequestedWorkflowTypes(experienceType) {
2003
+ if (!experienceType || experienceType === "all") {
2004
+ return WORKFLOW_TYPES;
2005
+ }
2006
+ return [experienceType];
2007
+ }
2008
+ async function fetchAvailableWorkflows(getters, experienceType) {
2009
+ const serverUrl = getters.serverUrl();
2010
+ const websiteId = getters.websiteId();
2011
+ if (!serverUrl) {
2012
+ throw new Error("Server URL is not configured.");
2013
+ }
2014
+ if (!websiteId) {
2015
+ throw new Error("websiteId is required to search workflows.");
2016
+ }
2017
+ const userProfile = getters.userProfile();
2018
+ const userType = userProfile?.type || "";
2019
+ const userId = userProfile?.userId;
2020
+ const toursApiBase = getters.toursApiBase();
2021
+ const requestedTypes = getRequestedWorkflowTypes(experienceType);
2022
+ const lists = await Promise.all(
2023
+ requestedTypes.map(
2024
+ (type) => fetchTours(serverUrl, toursApiBase, websiteId, userType, userId, type)
2025
+ )
2026
+ );
2027
+ return lists.flatMap(
2028
+ (list, index) => list.map((tour) => ({
2029
+ ...tour,
2030
+ type: tour.type || requestedTypes[index]
2031
+ }))
2032
+ );
2033
+ }
2034
+ async function resolveWorkflowFromInput(getters, params) {
2035
+ const serverUrl = getters.serverUrl();
2036
+ const websiteId = getters.websiteId();
2037
+ const toursApiBase = getters.toursApiBase();
2038
+ if (!serverUrl) {
2039
+ throw new Error("Server URL is not configured.");
2040
+ }
2041
+ if (params.workflowId) {
2042
+ const requestedTypes = getRequestedWorkflowTypes(params.experienceType);
2043
+ for (const type of requestedTypes) {
2044
+ const workflow = await fetchTourById(serverUrl, toursApiBase, params.workflowId, type, websiteId);
2045
+ if (workflow) {
2046
+ return { workflow: { ...workflow, type: workflow.type || type } };
2047
+ }
2048
+ }
2049
+ }
2050
+ const query = params.name?.trim();
2051
+ if (!query) {
2052
+ return { workflow: null, reason: "Provide either workflowId or name." };
2053
+ }
2054
+ const workflows = await fetchAvailableWorkflows(getters, params.experienceType);
2055
+ const ranked = workflows.map((workflow) => ({ workflow, score: scoreWorkflowMatch(workflow, query) })).filter(({ score }) => score > 0).sort((a, b) => b.score - a.score);
2056
+ if (ranked.length === 0) {
2057
+ return {
2058
+ workflow: null,
2059
+ reason: `No workflow matched "${query}". Try search_workflows first for available options.`
2060
+ };
2061
+ }
2062
+ if (ranked.length > 1 && ranked[0].score === ranked[1].score) {
2063
+ return {
2064
+ workflow: null,
2065
+ reason: `Multiple workflows matched "${query}". Try view_workflow with an exact workflowId.
2066
+
2067
+ ` + ranked.slice(0, 3).map(({ workflow }) => formatWorkflowSummary(workflow)).join("\n\n---\n\n")
2068
+ };
2069
+ }
2070
+ return { workflow: ranked[0].workflow };
2071
+ }
1796
2072
  var screenshotSchema = z2.object({
1797
2073
  selector: z2.string().optional().describe("Optional CSS selector to capture a specific element. Omit to capture the full viewport.")
1798
2074
  });
@@ -2073,15 +2349,91 @@ function createSearchTaggedElementsAction(getTagStore) {
2073
2349
  }
2074
2350
  };
2075
2351
  }
2352
+ var workflowExperienceTypeSchema = z2.enum(["onboarding", "tour", "all"]);
2353
+ var searchWorkflowsSchema = z2.object({
2354
+ query: z2.string().optional().describe("Optional workflow name, feature, or task phrase to search for. Omit to list available workflows."),
2355
+ experienceType: workflowExperienceTypeSchema.optional().describe("Which workflow type to search: onboarding, tour, or all. Default: onboarding."),
2356
+ limit: z2.number().optional().describe(`Maximum number of workflows to return (default: ${DEFAULT_WORKFLOW_SEARCH_LIMIT}).`)
2357
+ });
2358
+ function createSearchWorkflowsAction(getters) {
2359
+ return {
2360
+ id: "search_workflows",
2361
+ 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.",
2362
+ schema: searchWorkflowsSchema,
2363
+ execute: async (params) => {
2364
+ const workflows = await fetchAvailableWorkflows(getters, params.experienceType ?? "onboarding");
2365
+ if (workflows.length === 0) {
2366
+ return "No workflows are currently available for this website/user.";
2367
+ }
2368
+ const query = params.query?.trim();
2369
+ const limit = params.limit ?? DEFAULT_WORKFLOW_SEARCH_LIMIT;
2370
+ const ranked = workflows.map((workflow) => ({
2371
+ workflow,
2372
+ score: query ? scoreWorkflowMatch(workflow, query) : 1
2373
+ })).filter(({ score }) => score > 0).sort((a, b) => b.score - a.score).slice(0, limit);
2374
+ if (ranked.length === 0) {
2375
+ return `No workflows matched "${query}".`;
2376
+ }
2377
+ return ranked.map(({ workflow }) => formatWorkflowSummary(workflow)).join("\n\n---\n\n");
2378
+ }
2379
+ };
2380
+ }
2381
+ var resolveWorkflowSchema = z2.object({
2382
+ workflowId: z2.string().optional().describe("Exact workflow id when known."),
2383
+ name: z2.string().optional().describe("Workflow name or a close name match when the id is unknown."),
2384
+ experienceType: workflowExperienceTypeSchema.optional().describe("Which workflow type to search: onboarding, tour, or all. Default: onboarding.")
2385
+ });
2386
+ function createViewWorkflowAction(getters) {
2387
+ return {
2388
+ id: "view_workflow",
2389
+ 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.",
2390
+ schema: resolveWorkflowSchema,
2391
+ execute: async (params) => {
2392
+ const { workflow, reason } = await resolveWorkflowFromInput(getters, params);
2393
+ if (!workflow) {
2394
+ return reason || "Workflow not found.";
2395
+ }
2396
+ return formatWorkflowSummary(workflow, { includeSteps: true, stepLimit: 5 });
2397
+ }
2398
+ };
2399
+ }
2400
+ var startWorkflowSchema = resolveWorkflowSchema.extend({
2401
+ reviewMode: z2.boolean().optional().describe("Optional. Start in review mode instead of normal playback. Default: false.")
2402
+ });
2403
+ function createStartWorkflowAction(getters) {
2404
+ return {
2405
+ id: "start_workflow",
2406
+ 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.",
2407
+ schema: startWorkflowSchema,
2408
+ execute: async (params) => {
2409
+ const { workflow, reason } = await resolveWorkflowFromInput(getters, params);
2410
+ if (!workflow) {
2411
+ return reason || "Workflow not found.";
2412
+ }
2413
+ const bridge = getExperienceToolBridge();
2414
+ if (!bridge) {
2415
+ return "Workflow launch is unavailable because no chat playback controller is mounted.";
2416
+ }
2417
+ bridge.startExperience(workflow, workflow.type, params.reviewMode ? {
2418
+ reviewMode: true,
2419
+ reviewMetadata: { trigger: "agent_tool" }
2420
+ } : void 0);
2421
+ return `Started workflow "${workflow.name}" (${workflow.id}).`;
2422
+ }
2423
+ };
2424
+ }
2076
2425
  var BUILTIN_ACTION_IDS = {
2077
2426
  screenshot: "take_screenshot",
2078
2427
  click: "click_element",
2079
2428
  fill: "fill_input",
2080
2429
  wait: "request_user_action",
2081
2430
  searchDocs: "search_docs",
2082
- searchTaggedElements: "search_tagged_elements"
2431
+ searchTaggedElements: "search_tagged_elements",
2432
+ searchWorkflows: "search_workflows",
2433
+ viewWorkflow: "view_workflow",
2434
+ startWorkflow: "start_workflow"
2083
2435
  };
2084
- function useBuiltinActions(registerAction, unregisterAction, tagStore, serverUrl, websiteId) {
2436
+ function useBuiltinActions(registerAction, unregisterAction, tagStore, serverUrl, websiteId, toursApiBase, userProfile) {
2085
2437
  const registeredRef = useRef6(false);
2086
2438
  const tagStoreRef = useRef6(tagStore);
2087
2439
  tagStoreRef.current = tagStore;
@@ -2089,18 +2441,33 @@ function useBuiltinActions(registerAction, unregisterAction, tagStore, serverUrl
2089
2441
  serverUrlRef.current = serverUrl;
2090
2442
  const websiteIdRef = useRef6(websiteId);
2091
2443
  websiteIdRef.current = websiteId;
2444
+ const toursApiBaseRef = useRef6(toursApiBase);
2445
+ toursApiBaseRef.current = toursApiBase;
2446
+ const userProfileRef = useRef6(userProfile);
2447
+ userProfileRef.current = userProfile;
2092
2448
  useEffect8(() => {
2093
2449
  if (registeredRef.current) return;
2094
2450
  registeredRef.current = true;
2095
2451
  const getTagStore = () => tagStoreRef.current;
2096
2452
  const getServerUrl = () => serverUrlRef.current;
2097
2453
  const getWebsiteId = () => websiteIdRef.current;
2454
+ const getToursApiBase = () => toursApiBaseRef.current;
2455
+ const getUserProfile = () => userProfileRef.current;
2456
+ const workflowToolGetters = {
2457
+ serverUrl: getServerUrl,
2458
+ websiteId: getWebsiteId,
2459
+ toursApiBase: getToursApiBase,
2460
+ userProfile: getUserProfile
2461
+ };
2098
2462
  registerAction(BUILTIN_SCREENSHOT_ACTION);
2099
2463
  registerAction(createClickAction(getTagStore));
2100
2464
  registerAction(createFillAction(getTagStore));
2101
2465
  registerAction(createWaitAction(getTagStore));
2102
2466
  registerAction(createSearchDocsAction(getServerUrl, getWebsiteId));
2103
2467
  registerAction(createSearchTaggedElementsAction(getTagStore));
2468
+ registerAction(createSearchWorkflowsAction(workflowToolGetters));
2469
+ registerAction(createViewWorkflowAction(workflowToolGetters));
2470
+ registerAction(createStartWorkflowAction(workflowToolGetters));
2104
2471
  return () => {
2105
2472
  unregisterAction(BUILTIN_SCREENSHOT_ACTION.id);
2106
2473
  unregisterAction(BUILTIN_ACTION_IDS.click);
@@ -2108,6 +2475,9 @@ function useBuiltinActions(registerAction, unregisterAction, tagStore, serverUrl
2108
2475
  unregisterAction(BUILTIN_ACTION_IDS.wait);
2109
2476
  unregisterAction(BUILTIN_ACTION_IDS.searchDocs);
2110
2477
  unregisterAction(BUILTIN_ACTION_IDS.searchTaggedElements);
2478
+ unregisterAction(BUILTIN_ACTION_IDS.searchWorkflows);
2479
+ unregisterAction(BUILTIN_ACTION_IDS.viewWorkflow);
2480
+ unregisterAction(BUILTIN_ACTION_IDS.startWorkflow);
2111
2481
  registeredRef.current = false;
2112
2482
  };
2113
2483
  }, [registerAction, unregisterAction]);
@@ -2362,135 +2732,6 @@ function readPreviewSessionSuppression() {
2362
2732
  }
2363
2733
  }
2364
2734
 
2365
- // src/utils/tourDiscovery.ts
2366
- async function fetchTours(serverUrl, toursApiBase, websiteId, userType, userId, experienceType = "tour") {
2367
- try {
2368
- const params = new URLSearchParams({
2369
- websiteId,
2370
- userType
2371
- });
2372
- if (userId) params.set("userId", userId);
2373
- if (experienceType !== "tour") params.set("type", experienceType);
2374
- const res = await fetch(getTourApiUrl(serverUrl, toursApiBase, `/tours?${params.toString()}`));
2375
- if (!res.ok) return [];
2376
- const data = await res.json();
2377
- return data.tours ?? [];
2378
- } catch {
2379
- return [];
2380
- }
2381
- }
2382
- async function fetchTourById(serverUrl, toursApiBase, tourId, experienceType = "tour", websiteId) {
2383
- try {
2384
- const url = new URL(withWebsiteId(getTourApiUrl(serverUrl, toursApiBase, `/tours/${tourId}`), websiteId));
2385
- if (experienceType !== "tour") {
2386
- url.searchParams.set("type", experienceType);
2387
- }
2388
- const res = await fetch(url.toString());
2389
- if (!res.ok) {
2390
- return null;
2391
- }
2392
- const data = await res.json();
2393
- return data.tour ?? null;
2394
- } catch {
2395
- return null;
2396
- }
2397
- }
2398
- async function markTourComplete(serverUrl, toursApiBase, tourId, userId, experienceType = "tour", websiteId) {
2399
- try {
2400
- const url = new URL(withWebsiteId(getTourApiUrl(serverUrl, toursApiBase, `/tours/${tourId}/complete`), websiteId));
2401
- if (experienceType !== "tour") {
2402
- url.searchParams.set("type", experienceType);
2403
- }
2404
- await fetch(url.toString(), {
2405
- method: "POST",
2406
- headers: { "Content-Type": "application/json" },
2407
- body: JSON.stringify({ userId })
2408
- });
2409
- } catch {
2410
- }
2411
- }
2412
- async function markTourDismissed(serverUrl, toursApiBase, tourId, userId, experienceType = "tour", websiteId) {
2413
- try {
2414
- const url = new URL(withWebsiteId(getTourApiUrl(serverUrl, toursApiBase, `/tours/${tourId}/dismiss`), websiteId));
2415
- if (experienceType !== "tour") {
2416
- url.searchParams.set("type", experienceType);
2417
- }
2418
- await fetch(url.toString(), {
2419
- method: "POST",
2420
- headers: { "Content-Type": "application/json" },
2421
- body: JSON.stringify({ userId })
2422
- });
2423
- } catch {
2424
- }
2425
- }
2426
- async function recordTourEvent(serverUrl, toursApiBase, tourId, userId, eventType, websiteId) {
2427
- try {
2428
- const url = withWebsiteId(getTourApiUrl(serverUrl, toursApiBase, `/tours/${tourId}/events`), websiteId);
2429
- await fetch(url, {
2430
- method: "POST",
2431
- headers: { "Content-Type": "application/json" },
2432
- body: JSON.stringify({ userId, eventType })
2433
- });
2434
- } catch {
2435
- }
2436
- }
2437
- function getToursBaseUrl(serverUrl, toursApiBase) {
2438
- if (toursApiBase?.startsWith("/")) {
2439
- return `${typeof window !== "undefined" ? window.location.origin : ""}${toursApiBase.replace(/\/$/, "")}`;
2440
- }
2441
- return serverUrl.replace(/\/$/, "");
2442
- }
2443
- function getTourApiUrl(serverUrl, toursApiBase, path) {
2444
- const base = getToursBaseUrl(serverUrl, toursApiBase);
2445
- return toursApiBase ? `${base}${path}` : `${base}/api${path}`;
2446
- }
2447
- function withWebsiteId(url, websiteId) {
2448
- if (!websiteId) {
2449
- return url;
2450
- }
2451
- const baseOrigin = typeof window !== "undefined" ? window.location.origin : "http://localhost";
2452
- const resolved = new URL(url, baseOrigin);
2453
- resolved.searchParams.set("websiteId", websiteId);
2454
- return resolved.toString();
2455
- }
2456
- function normalizeTrigger(trigger) {
2457
- if (trigger === "feature_launch" || trigger === "feature_unlocked" || trigger === "feature_unlock" || trigger === "new_feature") {
2458
- return "feature_launch";
2459
- }
2460
- if (trigger === "return_visit") {
2461
- return "return_visit";
2462
- }
2463
- return trigger ?? "first_visit";
2464
- }
2465
- function getStartPolicy(tour) {
2466
- return tour.startPolicy ?? (normalizeTrigger(tour.trigger) === "manual" ? "immediate_start" : "prompt_only");
2467
- }
2468
- function getNotificationType(tour) {
2469
- return tour.notificationType ?? "bubble_card";
2470
- }
2471
- function getUserProfileFeatures(userProfile) {
2472
- const directFeatures = Array.isArray(userProfile.features) ? userProfile.features : [];
2473
- const nestedFeatures = Array.isArray(userProfile.tourFacts?.features) ? userProfile.tourFacts.features : [];
2474
- return [...new Set([...directFeatures, ...nestedFeatures].filter((value) => Boolean(value)))];
2475
- }
2476
- function isTourEligible(tour, userProfile) {
2477
- switch (normalizeTrigger(tour.trigger)) {
2478
- case "first_visit":
2479
- return !!userProfile.isNewUser;
2480
- case "return_visit":
2481
- return userProfile.isNewUser === false;
2482
- case "feature_launch": {
2483
- const featureKey = tour.featureKey?.trim();
2484
- if (!featureKey) return false;
2485
- return getUserProfileFeatures(userProfile).includes(featureKey);
2486
- }
2487
- case "manual":
2488
- return false;
2489
- default:
2490
- return false;
2491
- }
2492
- }
2493
-
2494
2735
  // src/hooks/useTourPlayback.ts
2495
2736
  import { useState as useState7, useRef as useRef8, useCallback as useCallback7, useEffect as useEffect11, useContext as useContext4 } from "react";
2496
2737
 
@@ -8405,6 +8646,7 @@ function Tooltip({ children, title }) {
8405
8646
  );
8406
8647
  }
8407
8648
  var BUBBLE_EXPANDED_STORAGE_KEY = "modelnex-chat-bubble-expanded";
8649
+ var BUBBLE_DOCKED_STORAGE_KEY = "modelnex-chat-bubble-docked";
8408
8650
  var TOUR_MINIMIZED_STORAGE_KEY = "modelnex-tour-bubble-minimized";
8409
8651
  var BotIcon = () => /* @__PURE__ */ jsxs3("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: [
8410
8652
  /* @__PURE__ */ jsx4("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" }),
@@ -8418,6 +8660,18 @@ var CloseIcon = () => /* @__PURE__ */ jsxs3("svg", { width: "var(--modelnex-bubb
8418
8660
  /* @__PURE__ */ jsx4("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
8419
8661
  ] });
8420
8662
  var MinimizeIcon = () => /* @__PURE__ */ jsx4("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx4("path", { d: "M8 18h8" }) });
8663
+ var DockIcon = () => /* @__PURE__ */ jsxs3("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
8664
+ /* @__PURE__ */ jsx4("path", { d: "M12 3v10" }),
8665
+ /* @__PURE__ */ jsx4("path", { d: "m8 9 4 4 4-4" }),
8666
+ /* @__PURE__ */ jsx4("path", { d: "M4 17h16" }),
8667
+ /* @__PURE__ */ jsx4("path", { d: "M6 21h12" })
8668
+ ] });
8669
+ var UndockIcon = () => /* @__PURE__ */ jsxs3("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
8670
+ /* @__PURE__ */ jsx4("path", { d: "M12 21V11" }),
8671
+ /* @__PURE__ */ jsx4("path", { d: "m8 15 4-4 4 4" }),
8672
+ /* @__PURE__ */ jsx4("path", { d: "M4 7h16" }),
8673
+ /* @__PURE__ */ jsx4("path", { d: "M6 3h12" })
8674
+ ] });
8421
8675
  var TrashIcon = () => /* @__PURE__ */ jsxs3("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
8422
8676
  /* @__PURE__ */ jsx4("path", { d: "M3 6h18" }),
8423
8677
  /* @__PURE__ */ jsx4("path", { d: "M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" }),
@@ -8638,6 +8892,7 @@ function ModelNexChatBubble({
8638
8892
  const ctx = useContext5(ModelNexContext);
8639
8893
  const [hydrated, setHydrated] = useState13(false);
8640
8894
  const [expanded, setExpanded] = useState13(false);
8895
+ const [docked, setDocked] = useState13(false);
8641
8896
  const [input, setInput] = useState13("");
8642
8897
  const messages = ctx?.chatMessages ?? [];
8643
8898
  const setMessages = ctx?.setChatMessages ?? (() => {
@@ -8692,6 +8947,11 @@ function ModelNexChatBubble({
8692
8947
  const activePlayback = playbackController.playback;
8693
8948
  const activeExperienceType = playbackController.activeExperienceType;
8694
8949
  const startingExperienceType = playbackController.startingExperienceType;
8950
+ useEffect17(() => {
8951
+ return registerExperienceToolBridge({
8952
+ startExperience: playbackController.startExperience
8953
+ });
8954
+ }, [playbackController.startExperience]);
8695
8955
  const createPlaybackView = useCallback12((experienceType) => {
8696
8956
  const isActiveExperience = activePlayback.isActive && activeExperienceType === experienceType;
8697
8957
  const pendingTour = playbackController.pendingPrompt?.experienceType === experienceType ? playbackController.pendingPrompt.tour : null;
@@ -8789,8 +9049,10 @@ function ModelNexChatBubble({
8789
9049
  setHydrated(true);
8790
9050
  try {
8791
9051
  setExpanded(sessionStorage.getItem(BUBBLE_EXPANDED_STORAGE_KEY) === "true");
9052
+ setDocked(sessionStorage.getItem(BUBBLE_DOCKED_STORAGE_KEY) === "true");
8792
9053
  } catch {
8793
9054
  setExpanded(false);
9055
+ setDocked(false);
8794
9056
  }
8795
9057
  }, []);
8796
9058
  const sttActiveRef = useRef13(false);
@@ -8850,6 +9112,13 @@ function ModelNexChatBubble({
8850
9112
  } catch {
8851
9113
  }
8852
9114
  }, [tourPlayback.isActive, onboardingPlayback.isActive]);
9115
+ const setDockedState = useCallback12((next) => {
9116
+ setDocked(next);
9117
+ try {
9118
+ sessionStorage.setItem(BUBBLE_DOCKED_STORAGE_KEY, String(next));
9119
+ } catch {
9120
+ }
9121
+ }, []);
8853
9122
  useEffect17(() => {
8854
9123
  if (shouldAutoExpandForPendingPrompt({
8855
9124
  pendingPrompt,
@@ -9211,11 +9480,13 @@ function ModelNexChatBubble({
9211
9480
  fontFamily: "var(--modelnex-font)",
9212
9481
  ...themeStyles
9213
9482
  };
9483
+ const desktopPanelHeight = docked ? "min(calc(100vh - 48px), calc(var(--modelnex-panel-max-height, 600px) + 180px))" : "var(--modelnex-panel-max-height, 600px)";
9484
+ const desktopPanelMaxHeight = docked ? "calc(100vh - 48px)" : "calc(100vh - 120px)";
9214
9485
  const panelStyle = {
9215
9486
  width: isMobile ? "100%" : "var(--modelnex-panel-width, 380px)",
9216
9487
  maxWidth: "calc(100vw - 32px)",
9217
- height: isMobile ? "100%" : "var(--modelnex-panel-max-height, 600px)",
9218
- maxHeight: "calc(100vh - 120px)",
9488
+ height: isMobile ? "100%" : desktopPanelHeight,
9489
+ maxHeight: isMobile ? "100%" : desktopPanelMaxHeight,
9219
9490
  borderRadius: isMobile ? "0" : "var(--modelnex-radius-panel, 20px)",
9220
9491
  border: isMobile ? "none" : "1px solid var(--modelnex-border, #e4e4e7)",
9221
9492
  background: "var(--modelnex-bg, #ffffff)",
@@ -9367,6 +9638,31 @@ function ModelNexChatBubble({
9367
9638
  ] })
9368
9639
  ] }) }),
9369
9640
  /* @__PURE__ */ jsxs3("div", { style: { display: "flex", alignItems: "center", gap: "4px" }, children: [
9641
+ !isMobile && /* @__PURE__ */ jsx4(Tooltip, { title: docked ? "Undock" : "Dock", children: /* @__PURE__ */ jsxs3(
9642
+ "button",
9643
+ {
9644
+ onClick: () => setDockedState(!docked),
9645
+ style: {
9646
+ padding: "8px 10px",
9647
+ borderRadius: "10px",
9648
+ border: "none",
9649
+ background: docked ? "rgba(79,70,229,0.08)" : "transparent",
9650
+ cursor: "pointer",
9651
+ color: docked ? "var(--modelnex-accent, #4f46e5)" : "#71717a",
9652
+ transition: "all 0.2s",
9653
+ display: "flex",
9654
+ alignItems: "center",
9655
+ gap: "6px",
9656
+ fontSize: "12px",
9657
+ fontWeight: 700
9658
+ },
9659
+ "aria-label": docked ? "Undock chat bubble" : "Dock chat bubble",
9660
+ children: [
9661
+ docked ? /* @__PURE__ */ jsx4(UndockIcon, {}) : /* @__PURE__ */ jsx4(DockIcon, {}),
9662
+ /* @__PURE__ */ jsx4("span", { children: docked ? "Undock" : "Dock" })
9663
+ ]
9664
+ }
9665
+ ) }),
9370
9666
  (tourPlayback.isActive || onboardingPlayback.isActive || voice.isSpeaking) && /* @__PURE__ */ jsx4(Tooltip, { title: voice.isMuted ? "Unmute" : "Mute", children: /* @__PURE__ */ jsx4(
9371
9667
  "button",
9372
9668
  {
@@ -11157,7 +11453,7 @@ var ModelNexProvider = ({
11157
11453
  }, []);
11158
11454
  const extractedElements = useAutoExtract();
11159
11455
  const tagStore = useTagStore({ serverUrl, websiteId });
11160
- useBuiltinActions(registerAction, unregisterAction, tagStore, serverUrl, websiteId);
11456
+ useBuiltinActions(registerAction, unregisterAction, tagStore, serverUrl, websiteId, toursApiBase, userProfile);
11161
11457
  const CHAT_STORAGE_KEY = "modelnex-chat-messages";
11162
11458
  const [chatMessages, setChatMessagesRaw] = useState15([]);
11163
11459
  useEffect19(() => {