@modelnex/sdk 0.5.16 → 0.5.18
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 +215 -88
- package/dist/index.mjs +215 -88
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2626,7 +2626,6 @@ function isTourEligible(tour, userProfile) {
|
|
|
2626
2626
|
|
|
2627
2627
|
// src/hooks/useTourPlayback.ts
|
|
2628
2628
|
var import_react12 = require("react");
|
|
2629
|
-
var import_socket2 = require("socket.io-client");
|
|
2630
2629
|
|
|
2631
2630
|
// src/utils/retryLookup.ts
|
|
2632
2631
|
var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -2649,6 +2648,101 @@ async function retryLookup({
|
|
|
2649
2648
|
}
|
|
2650
2649
|
}
|
|
2651
2650
|
|
|
2651
|
+
// src/utils/tour-socket-pool.ts
|
|
2652
|
+
var import_socket2 = require("socket.io-client");
|
|
2653
|
+
function isSocketWritable(socket) {
|
|
2654
|
+
if (!socket?.connected) return false;
|
|
2655
|
+
const engine = socket.io?.engine;
|
|
2656
|
+
if (!engine) return true;
|
|
2657
|
+
if (typeof engine.readyState === "string" && engine.readyState !== "open") return false;
|
|
2658
|
+
if (engine.transport && "writable" in engine.transport && engine.transport.writable === false) return false;
|
|
2659
|
+
return true;
|
|
2660
|
+
}
|
|
2661
|
+
function emitSocketEvent(socket, event, payload) {
|
|
2662
|
+
if (!isSocketWritable(socket)) return false;
|
|
2663
|
+
socket.emit(event, payload);
|
|
2664
|
+
return true;
|
|
2665
|
+
}
|
|
2666
|
+
function createTourSocketPool({
|
|
2667
|
+
createSocket = (serverUrl) => (0, import_socket2.io)(serverUrl, {
|
|
2668
|
+
path: "/socket.io",
|
|
2669
|
+
transports: resolveSocketIoTransports(serverUrl, "polling-first"),
|
|
2670
|
+
autoConnect: true,
|
|
2671
|
+
reconnection: true,
|
|
2672
|
+
reconnectionAttempts: 10,
|
|
2673
|
+
reconnectionDelay: 1e3
|
|
2674
|
+
}),
|
|
2675
|
+
releaseDelayMs = 2500,
|
|
2676
|
+
scheduleRelease = (callback, delayMs) => setTimeout(callback, delayMs),
|
|
2677
|
+
cancelRelease = (timer) => clearTimeout(timer)
|
|
2678
|
+
} = {}) {
|
|
2679
|
+
const pooledSockets = /* @__PURE__ */ new Map();
|
|
2680
|
+
return {
|
|
2681
|
+
acquire(serverUrl) {
|
|
2682
|
+
const pooled = pooledSockets.get(serverUrl);
|
|
2683
|
+
if (pooled) {
|
|
2684
|
+
if (pooled.releaseTimer) {
|
|
2685
|
+
cancelRelease(pooled.releaseTimer);
|
|
2686
|
+
pooled.releaseTimer = null;
|
|
2687
|
+
}
|
|
2688
|
+
pooled.leases += 1;
|
|
2689
|
+
return pooled.socket;
|
|
2690
|
+
}
|
|
2691
|
+
const socket = createSocket(serverUrl);
|
|
2692
|
+
pooledSockets.set(serverUrl, {
|
|
2693
|
+
socket,
|
|
2694
|
+
leases: 1,
|
|
2695
|
+
releaseTimer: null
|
|
2696
|
+
});
|
|
2697
|
+
return socket;
|
|
2698
|
+
},
|
|
2699
|
+
release(serverUrl, socket) {
|
|
2700
|
+
const pooled = pooledSockets.get(serverUrl);
|
|
2701
|
+
if (!pooled || pooled.socket !== socket) return;
|
|
2702
|
+
pooled.leases = Math.max(0, pooled.leases - 1);
|
|
2703
|
+
if (pooled.leases > 0) return;
|
|
2704
|
+
pooled.releaseTimer = scheduleRelease(() => {
|
|
2705
|
+
const latest = pooledSockets.get(serverUrl);
|
|
2706
|
+
if (!latest || latest !== pooled || latest.leases > 0) return;
|
|
2707
|
+
latest.socket.disconnect();
|
|
2708
|
+
pooledSockets.delete(serverUrl);
|
|
2709
|
+
}, releaseDelayMs);
|
|
2710
|
+
},
|
|
2711
|
+
// Test-only introspection
|
|
2712
|
+
getSnapshot(serverUrl) {
|
|
2713
|
+
const pooled = pooledSockets.get(serverUrl);
|
|
2714
|
+
if (!pooled) return null;
|
|
2715
|
+
return { leases: pooled.leases, socket: pooled.socket };
|
|
2716
|
+
}
|
|
2717
|
+
};
|
|
2718
|
+
}
|
|
2719
|
+
var tourSocketPool = createTourSocketPool();
|
|
2720
|
+
|
|
2721
|
+
// src/utils/tour-playback-guards.ts
|
|
2722
|
+
function shouldExecuteTourCommandBatch(isPlaybackActive) {
|
|
2723
|
+
return isPlaybackActive;
|
|
2724
|
+
}
|
|
2725
|
+
function shouldAcceptTourStart(state) {
|
|
2726
|
+
return !state.isPlaybackActive && !state.startRequested;
|
|
2727
|
+
}
|
|
2728
|
+
function shouldRunTourAutoDiscovery(state) {
|
|
2729
|
+
return state.enableAutoDiscovery && !state.disabled && !state.isPlaybackActive && !state.startRequested && !state.hasPendingTour;
|
|
2730
|
+
}
|
|
2731
|
+
function shouldLogTourDebugEntry(state) {
|
|
2732
|
+
if (!state.isPlaybackActive) {
|
|
2733
|
+
if (state.entryType !== "tour_start") {
|
|
2734
|
+
return false;
|
|
2735
|
+
}
|
|
2736
|
+
if (state.entryTourType && state.entryTourType !== state.experienceType) {
|
|
2737
|
+
return false;
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
if (state.entryTourType && state.entryTourType !== state.experienceType) {
|
|
2741
|
+
return false;
|
|
2742
|
+
}
|
|
2743
|
+
return true;
|
|
2744
|
+
}
|
|
2745
|
+
|
|
2652
2746
|
// src/hooks/useTourPlayback.ts
|
|
2653
2747
|
function resolveElement(step) {
|
|
2654
2748
|
const el = step.element;
|
|
@@ -3182,14 +3276,17 @@ function useTourPlayback({
|
|
|
3182
3276
|
const showCaptionsRef = (0, import_react12.useRef)(showCaptions);
|
|
3183
3277
|
const runIdRef = (0, import_react12.useRef)(null);
|
|
3184
3278
|
const turnIdRef = (0, import_react12.useRef)(null);
|
|
3279
|
+
const startRequestedRef = (0, import_react12.useRef)(false);
|
|
3185
3280
|
const socketRef = (0, import_react12.useRef)(null);
|
|
3186
3281
|
const socketIdRef = (0, import_react12.useRef)(socketId);
|
|
3187
3282
|
const commandUrlRef = (0, import_react12.useRef)(commandUrl);
|
|
3283
|
+
const websiteIdRef = (0, import_react12.useRef)(websiteId);
|
|
3188
3284
|
const onStepChangeRef = (0, import_react12.useRef)(onStepChange);
|
|
3189
3285
|
const isActiveRef = (0, import_react12.useRef)(false);
|
|
3190
3286
|
const activeCommandBatchIdRef = (0, import_react12.useRef)(null);
|
|
3191
3287
|
socketIdRef.current = socketId;
|
|
3192
3288
|
commandUrlRef.current = commandUrl;
|
|
3289
|
+
websiteIdRef.current = websiteId;
|
|
3193
3290
|
onStepChangeRef.current = onStepChange;
|
|
3194
3291
|
isActiveRef.current = isActive;
|
|
3195
3292
|
reviewModeRef.current = isReviewMode;
|
|
@@ -3200,25 +3297,17 @@ function useTourPlayback({
|
|
|
3200
3297
|
(0, import_react12.useEffect)(() => {
|
|
3201
3298
|
if (disabled) return;
|
|
3202
3299
|
if (typeof window === "undefined") return;
|
|
3203
|
-
|
|
3204
|
-
const socket = (0, import_socket2.io)(serverUrl, {
|
|
3205
|
-
path: "/socket.io",
|
|
3206
|
-
transports: resolveSocketIoTransports(serverUrl, "polling-first"),
|
|
3207
|
-
autoConnect: true,
|
|
3208
|
-
reconnection: true,
|
|
3209
|
-
reconnectionAttempts: 10,
|
|
3210
|
-
reconnectionDelay: 1e3
|
|
3211
|
-
});
|
|
3212
|
-
createdSocket = socket;
|
|
3300
|
+
const socket = tourSocketPool.acquire(serverUrl);
|
|
3213
3301
|
socketRef.current = socket;
|
|
3214
|
-
|
|
3302
|
+
const handleConnect = () => {
|
|
3215
3303
|
console.log("[TourClient] Connected to tour agent server:", socket.id);
|
|
3216
3304
|
const profile = userProfileRef.current;
|
|
3217
|
-
|
|
3218
|
-
|
|
3305
|
+
const currentWebsiteId = websiteIdRef.current;
|
|
3306
|
+
if (currentWebsiteId && profile?.userId) {
|
|
3307
|
+
emitSocketEvent(socket, "tour:init", { websiteId: currentWebsiteId, userId: profile.userId, userType: profile.type });
|
|
3219
3308
|
}
|
|
3220
|
-
}
|
|
3221
|
-
|
|
3309
|
+
};
|
|
3310
|
+
const handleServerState = (payload) => {
|
|
3222
3311
|
if (typeof payload?.runId === "number") {
|
|
3223
3312
|
runIdRef.current = payload.runId;
|
|
3224
3313
|
}
|
|
@@ -3226,8 +3315,8 @@ function useTourPlayback({
|
|
|
3226
3315
|
turnIdRef.current = payload.turnId ?? null;
|
|
3227
3316
|
}
|
|
3228
3317
|
setServerState(payload);
|
|
3229
|
-
}
|
|
3230
|
-
|
|
3318
|
+
};
|
|
3319
|
+
const handleCommandCancel = (payload) => {
|
|
3231
3320
|
console.log("[TourClient] Received command_cancel:", payload);
|
|
3232
3321
|
if (payload.commandBatchId && activeCommandBatchIdRef.current === payload.commandBatchId) {
|
|
3233
3322
|
activeCommandBatchIdRef.current = null;
|
|
@@ -3238,12 +3327,16 @@ function useTourPlayback({
|
|
|
3238
3327
|
window.speechSynthesis.cancel();
|
|
3239
3328
|
}
|
|
3240
3329
|
}
|
|
3241
|
-
}
|
|
3242
|
-
|
|
3330
|
+
};
|
|
3331
|
+
const handleCommand = async (payload) => {
|
|
3332
|
+
if (!shouldExecuteTourCommandBatch(isActiveRef.current)) {
|
|
3333
|
+
const activeType = experienceTypeRef.current;
|
|
3334
|
+
console.log("[TourClient] Ignoring command batch for inactive playback hook:", activeType, payload.stepIndex);
|
|
3335
|
+
return;
|
|
3336
|
+
}
|
|
3243
3337
|
const emitIfOpen = (ev, data) => {
|
|
3244
3338
|
if (socketRef.current !== socket) return;
|
|
3245
|
-
|
|
3246
|
-
socket.emit(ev, data);
|
|
3339
|
+
emitSocketEvent(socket, ev, data);
|
|
3247
3340
|
};
|
|
3248
3341
|
console.log("[TourClient] Received command batch:", payload.stepIndex, payload.commands);
|
|
3249
3342
|
runCleanup(pendingManualWaitCleanupRef.current);
|
|
@@ -3736,8 +3829,8 @@ function useTourPlayback({
|
|
|
3736
3829
|
turnId: turnIdRef.current
|
|
3737
3830
|
});
|
|
3738
3831
|
clearCommandBatchId();
|
|
3739
|
-
}
|
|
3740
|
-
|
|
3832
|
+
};
|
|
3833
|
+
const handleTourStart = async (tourData) => {
|
|
3741
3834
|
if (isActiveRef.current) return;
|
|
3742
3835
|
runIdRef.current = typeof tourData.runId === "number" ? tourData.runId : runIdRef.current;
|
|
3743
3836
|
const tour = tourData.tourContext ?? tourRef.current;
|
|
@@ -3747,6 +3840,7 @@ function useTourPlayback({
|
|
|
3747
3840
|
return;
|
|
3748
3841
|
}
|
|
3749
3842
|
skipRequestedRef.current = false;
|
|
3843
|
+
startRequestedRef.current = false;
|
|
3750
3844
|
const total = tourData.totalSteps ?? tour?.steps?.length ?? 0;
|
|
3751
3845
|
isActiveRef.current = true;
|
|
3752
3846
|
setIsActive(true);
|
|
@@ -3770,8 +3864,8 @@ function useTourPlayback({
|
|
|
3770
3864
|
try {
|
|
3771
3865
|
const { generateMinifiedAOM: generateMinifiedAOM2 } = await Promise.resolve().then(() => (init_aom(), aom_exports));
|
|
3772
3866
|
const aom = generateMinifiedAOM2();
|
|
3773
|
-
if (socketRef.current === socket
|
|
3774
|
-
socket
|
|
3867
|
+
if (socketRef.current === socket) {
|
|
3868
|
+
emitSocketEvent(socket, "tour:sync_dom", {
|
|
3775
3869
|
url: window.location.pathname + window.location.search + window.location.hash,
|
|
3776
3870
|
aom: aom.nodes,
|
|
3777
3871
|
domSummary: captureDomSummary()
|
|
@@ -3780,8 +3874,8 @@ function useTourPlayback({
|
|
|
3780
3874
|
} catch (e) {
|
|
3781
3875
|
console.warn("[TourClient] Initial DOM sync failed:", e);
|
|
3782
3876
|
}
|
|
3783
|
-
}
|
|
3784
|
-
|
|
3877
|
+
};
|
|
3878
|
+
const handleTourUpdate = (payload) => {
|
|
3785
3879
|
const updatedTour = payload?.tourContext;
|
|
3786
3880
|
if (!updatedTour?.id || updatedTour.id !== tourRef.current?.id) {
|
|
3787
3881
|
return;
|
|
@@ -3798,13 +3892,22 @@ function useTourPlayback({
|
|
|
3798
3892
|
onStepChangeRef.current?.(clampedStepIndex, nextTotal, updatedTour);
|
|
3799
3893
|
}
|
|
3800
3894
|
}
|
|
3801
|
-
}
|
|
3802
|
-
|
|
3895
|
+
};
|
|
3896
|
+
const handleTourEndEvent = () => {
|
|
3803
3897
|
setServerState((prev) => prev ? { ...prev, isActive: false, phase: "completed" } : prev);
|
|
3804
3898
|
handleTourEnd();
|
|
3805
|
-
}
|
|
3806
|
-
|
|
3899
|
+
};
|
|
3900
|
+
const handleDebugLog = (entry) => {
|
|
3807
3901
|
const isDev = devModeRef.current || process.env.NODE_ENV === "development" || typeof window !== "undefined" && window.MODELNEX_DEBUG;
|
|
3902
|
+
const entryTourType = entry?.data?.tourContext?.type ?? tourRef.current?.type ?? null;
|
|
3903
|
+
if (!shouldLogTourDebugEntry({
|
|
3904
|
+
isPlaybackActive: isActiveRef.current,
|
|
3905
|
+
entryType: entry?.type,
|
|
3906
|
+
entryTourType,
|
|
3907
|
+
experienceType: experienceTypeRef.current
|
|
3908
|
+
})) {
|
|
3909
|
+
return;
|
|
3910
|
+
}
|
|
3808
3911
|
if (isDev) {
|
|
3809
3912
|
console.log(`%c[ModelNex Tour] ${entry.type}`, "color: #3b82f6; font-weight: bold", entry);
|
|
3810
3913
|
if (typeof window !== "undefined") {
|
|
@@ -3813,29 +3916,40 @@ function useTourPlayback({
|
|
|
3813
3916
|
}));
|
|
3814
3917
|
}
|
|
3815
3918
|
}
|
|
3816
|
-
}
|
|
3919
|
+
};
|
|
3920
|
+
socket.on("connect", handleConnect);
|
|
3921
|
+
socket.on("tour:server_state", handleServerState);
|
|
3922
|
+
socket.on("tour:command_cancel", handleCommandCancel);
|
|
3923
|
+
socket.on("tour:command", handleCommand);
|
|
3924
|
+
socket.on("tour:start", handleTourStart);
|
|
3925
|
+
socket.on("tour:update", handleTourUpdate);
|
|
3926
|
+
socket.on("tour:end", handleTourEndEvent);
|
|
3927
|
+
socket.on("tour:debug_log", handleDebugLog);
|
|
3817
3928
|
console.log("[ModelNex SDK] Tour playback initialized. Debug logs enabled:", devModeRef.current || process.env.NODE_ENV === "development");
|
|
3818
3929
|
return () => {
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3930
|
+
socket.off("connect", handleConnect);
|
|
3931
|
+
socket.off("tour:server_state", handleServerState);
|
|
3932
|
+
socket.off("tour:command_cancel", handleCommandCancel);
|
|
3933
|
+
socket.off("tour:command", handleCommand);
|
|
3934
|
+
socket.off("tour:start", handleTourStart);
|
|
3935
|
+
socket.off("tour:update", handleTourUpdate);
|
|
3936
|
+
socket.off("tour:end", handleTourEndEvent);
|
|
3937
|
+
socket.off("tour:debug_log", handleDebugLog);
|
|
3938
|
+
const toClose = socket;
|
|
3824
3939
|
socketRef.current = null;
|
|
3825
3940
|
setServerState(null);
|
|
3826
3941
|
runIdRef.current = null;
|
|
3827
3942
|
turnIdRef.current = null;
|
|
3943
|
+
tourSocketPool.release(serverUrl, toClose);
|
|
3828
3944
|
};
|
|
3829
|
-
}, [serverUrl,
|
|
3945
|
+
}, [serverUrl, disabled]);
|
|
3830
3946
|
(0, import_react12.useEffect)(() => {
|
|
3831
3947
|
if (disabled) return;
|
|
3832
3948
|
const s = socketRef.current;
|
|
3833
3949
|
const profile = userProfile;
|
|
3834
|
-
if (!
|
|
3950
|
+
if (!websiteId || !profile?.userId) return;
|
|
3835
3951
|
const timer = setTimeout(() => {
|
|
3836
|
-
|
|
3837
|
-
s.emit("tour:init", { websiteId, userId: profile.userId, userType: profile.type });
|
|
3838
|
-
}
|
|
3952
|
+
emitSocketEvent(s, "tour:init", { websiteId, userId: profile.userId, userType: profile.type });
|
|
3839
3953
|
}, 150);
|
|
3840
3954
|
return () => clearTimeout(timer);
|
|
3841
3955
|
}, [disabled, websiteId, userProfile?.userId, userProfile?.type]);
|
|
@@ -3845,8 +3959,8 @@ function useTourPlayback({
|
|
|
3845
3959
|
}
|
|
3846
3960
|
}, [showCaptions, isReviewMode]);
|
|
3847
3961
|
(0, import_react12.useEffect)(() => {
|
|
3848
|
-
if (!
|
|
3849
|
-
socketRef.current
|
|
3962
|
+
if (!isActiveRef.current) return;
|
|
3963
|
+
emitSocketEvent(socketRef.current, "tour:client_state", {
|
|
3850
3964
|
runId: runIdRef.current,
|
|
3851
3965
|
turnId: turnIdRef.current,
|
|
3852
3966
|
commandBatchId: activeCommandBatchIdRef.current,
|
|
@@ -3859,19 +3973,17 @@ function useTourPlayback({
|
|
|
3859
3973
|
});
|
|
3860
3974
|
}, [isActive, playbackState, voice.isListening, voice.isSpeaking]);
|
|
3861
3975
|
const syncAOM = (0, import_react12.useCallback)(async () => {
|
|
3862
|
-
if (!
|
|
3976
|
+
if (!isActiveRef.current) return;
|
|
3863
3977
|
const { generateMinifiedAOM: generateMinifiedAOM2 } = await Promise.resolve().then(() => (init_aom(), aom_exports));
|
|
3864
3978
|
const aom = generateMinifiedAOM2();
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
});
|
|
3871
|
-
}
|
|
3979
|
+
emitSocketEvent(socketRef.current, "tour:sync_dom", {
|
|
3980
|
+
url: window.location.pathname + window.location.search + window.location.hash,
|
|
3981
|
+
aom: aom.nodes,
|
|
3982
|
+
domSummary: captureDomSummary()
|
|
3983
|
+
});
|
|
3872
3984
|
}, []);
|
|
3873
3985
|
const interruptExecution = (0, import_react12.useCallback)((transcript) => {
|
|
3874
|
-
if (!socketRef.current
|
|
3986
|
+
if (!isSocketWritable(socketRef.current) || !isActiveRef.current) return false;
|
|
3875
3987
|
if (!commandInFlightRef.current && !voice.isSpeaking) return false;
|
|
3876
3988
|
interruptedForQuestionRef.current = true;
|
|
3877
3989
|
activeExecutionTokenRef.current += 1;
|
|
@@ -3879,17 +3991,16 @@ function useTourPlayback({
|
|
|
3879
3991
|
removeHighlight();
|
|
3880
3992
|
removeCaption();
|
|
3881
3993
|
voice.stopSpeaking();
|
|
3882
|
-
if (socketRef.current
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
});
|
|
3994
|
+
if (emitSocketEvent(socketRef.current, "tour:action_result", {
|
|
3995
|
+
success: true,
|
|
3996
|
+
interrupted: true,
|
|
3997
|
+
transcript,
|
|
3998
|
+
commandBatchId: activeCommandBatchIdRef.current,
|
|
3999
|
+
runId: runIdRef.current,
|
|
4000
|
+
turnId: turnIdRef.current
|
|
4001
|
+
})) {
|
|
3891
4002
|
activeCommandBatchIdRef.current = null;
|
|
3892
|
-
socketRef.current
|
|
4003
|
+
emitSocketEvent(socketRef.current, "tour:user_input", {
|
|
3893
4004
|
transcript,
|
|
3894
4005
|
interrupted: true,
|
|
3895
4006
|
runId: runIdRef.current,
|
|
@@ -3902,6 +4013,7 @@ function useTourPlayback({
|
|
|
3902
4013
|
const stopTour = (0, import_react12.useCallback)(() => {
|
|
3903
4014
|
skipRequestedRef.current = true;
|
|
3904
4015
|
isActiveRef.current = false;
|
|
4016
|
+
startRequestedRef.current = false;
|
|
3905
4017
|
activeExecutionTokenRef.current += 1;
|
|
3906
4018
|
commandInFlightRef.current = false;
|
|
3907
4019
|
activeCommandBatchIdRef.current = null;
|
|
@@ -3911,9 +4023,7 @@ function useTourPlayback({
|
|
|
3911
4023
|
removeCaption();
|
|
3912
4024
|
voice.stopSpeaking();
|
|
3913
4025
|
voice.stopListening();
|
|
3914
|
-
|
|
3915
|
-
socketRef.current.emit("tour:abort");
|
|
3916
|
-
}
|
|
4026
|
+
emitSocketEvent(socketRef.current, "tour:abort");
|
|
3917
4027
|
if (reviewModeRef.current && tourRef.current?.id && previewRunIdRef.current) {
|
|
3918
4028
|
void logPreviewEvent(serverUrl, toursApiBaseRef.current, tourRef.current.id, previewRunIdRef.current, websiteId, {
|
|
3919
4029
|
stepOrder: stepIndexRef.current,
|
|
@@ -3945,6 +4055,7 @@ function useTourPlayback({
|
|
|
3945
4055
|
const endingPreviewRunId = previewRunIdRef.current;
|
|
3946
4056
|
const endingStepOrder = stepIndexRef.current;
|
|
3947
4057
|
isActiveRef.current = false;
|
|
4058
|
+
startRequestedRef.current = false;
|
|
3948
4059
|
setPlaybackState("complete");
|
|
3949
4060
|
removeHighlight();
|
|
3950
4061
|
removeCaption();
|
|
@@ -3978,6 +4089,13 @@ function useTourPlayback({
|
|
|
3978
4089
|
onTourEnd?.();
|
|
3979
4090
|
}, [experienceType, userProfile, serverUrl, voice, onTourEnd, websiteId]);
|
|
3980
4091
|
const runTour = (0, import_react12.useCallback)(async (tour, options) => {
|
|
4092
|
+
if (!shouldAcceptTourStart({
|
|
4093
|
+
isPlaybackActive: isActiveRef.current,
|
|
4094
|
+
startRequested: startRequestedRef.current
|
|
4095
|
+
})) {
|
|
4096
|
+
console.log("[TourClient] Ignoring duplicate start request while playback is already active or starting:", tour.id);
|
|
4097
|
+
return;
|
|
4098
|
+
}
|
|
3981
4099
|
setPendingTour(null);
|
|
3982
4100
|
pendingTourRef.current = null;
|
|
3983
4101
|
let retries = 0;
|
|
@@ -3985,10 +4103,11 @@ function useTourPlayback({
|
|
|
3985
4103
|
await new Promise((r) => setTimeout(r, 200));
|
|
3986
4104
|
retries++;
|
|
3987
4105
|
}
|
|
3988
|
-
if (!socketRef.current
|
|
4106
|
+
if (!isSocketWritable(socketRef.current)) {
|
|
3989
4107
|
console.warn("[TourClient] Cannot run tour, socket not connected.");
|
|
3990
4108
|
return;
|
|
3991
4109
|
}
|
|
4110
|
+
startRequestedRef.current = true;
|
|
3992
4111
|
const shouldReview = Boolean(options?.reviewMode);
|
|
3993
4112
|
resetCaptionSuppression();
|
|
3994
4113
|
setReviewStatusMessage(null);
|
|
@@ -4013,17 +4132,20 @@ function useTourPlayback({
|
|
|
4013
4132
|
previewRunIdRef.current = null;
|
|
4014
4133
|
}
|
|
4015
4134
|
tourRef.current = tour;
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
});
|
|
4022
|
-
}
|
|
4135
|
+
emitSocketEvent(socketRef.current, "tour:request_start", {
|
|
4136
|
+
tourId: tour.id,
|
|
4137
|
+
previewRunId: previewRunIdRef.current,
|
|
4138
|
+
tourContext: tour
|
|
4139
|
+
});
|
|
4023
4140
|
}, [serverUrl, websiteId]);
|
|
4024
4141
|
(0, import_react12.useEffect)(() => {
|
|
4025
|
-
if (!
|
|
4026
|
-
|
|
4142
|
+
if (!shouldRunTourAutoDiscovery({
|
|
4143
|
+
enableAutoDiscovery,
|
|
4144
|
+
disabled,
|
|
4145
|
+
isPlaybackActive: isActiveRef.current,
|
|
4146
|
+
startRequested: startRequestedRef.current,
|
|
4147
|
+
hasPendingTour: Boolean(pendingTourRef.current)
|
|
4148
|
+
})) return;
|
|
4027
4149
|
if (typeof window === "undefined") return;
|
|
4028
4150
|
const params = new URLSearchParams(window.location.search);
|
|
4029
4151
|
const queryParam = experienceType === "onboarding" ? "modelnex_test_workflow" : "modelnex_test_tour";
|
|
@@ -4071,8 +4193,13 @@ function useTourPlayback({
|
|
|
4071
4193
|
};
|
|
4072
4194
|
}, [serverUrl, toursApiBase, disabled, websiteId, experienceType, enableAutoDiscovery]);
|
|
4073
4195
|
(0, import_react12.useEffect)(() => {
|
|
4074
|
-
if (!
|
|
4075
|
-
|
|
4196
|
+
if (!shouldRunTourAutoDiscovery({
|
|
4197
|
+
enableAutoDiscovery,
|
|
4198
|
+
disabled,
|
|
4199
|
+
isPlaybackActive: isActiveRef.current,
|
|
4200
|
+
startRequested: startRequestedRef.current,
|
|
4201
|
+
hasPendingTour: Boolean(pendingTourRef.current)
|
|
4202
|
+
})) return;
|
|
4076
4203
|
if (!websiteId || !userProfile) return;
|
|
4077
4204
|
if (typeof window !== "undefined") {
|
|
4078
4205
|
const params = new URLSearchParams(window.location.search);
|
|
@@ -4111,7 +4238,7 @@ function useTourPlayback({
|
|
|
4111
4238
|
cancelled = true;
|
|
4112
4239
|
clearTimeout(timer);
|
|
4113
4240
|
};
|
|
4114
|
-
}, [websiteId, serverUrl, toursApiBase, disabled, experienceType, userProfile, enableAutoDiscovery]);
|
|
4241
|
+
}, [websiteId, serverUrl, toursApiBase, disabled, experienceType, userProfile?.userId, userProfile?.type, userProfile?.isNewUser, enableAutoDiscovery]);
|
|
4115
4242
|
(0, import_react12.useEffect)(() => {
|
|
4116
4243
|
if (!disabled || !isActiveRef.current) return;
|
|
4117
4244
|
stopTour();
|
|
@@ -4163,8 +4290,8 @@ function useTourPlayback({
|
|
|
4163
4290
|
const revisionVersion = Number(response?.revision?.versionNumber);
|
|
4164
4291
|
const appliedMessage = Number.isFinite(revisionVersion) && revisionVersion > 0 ? `Applied to the draft as version ${revisionVersion}.` : "Correction applied to the draft.";
|
|
4165
4292
|
setReviewStatusMessage(apply ? appliedMessage : "Correction saved for review.");
|
|
4166
|
-
if (apply && playbackState === "paused" &&
|
|
4167
|
-
socketRef.current
|
|
4293
|
+
if (apply && playbackState === "paused" && isActiveRef.current) {
|
|
4294
|
+
emitSocketEvent(socketRef.current, "tour:resume");
|
|
4168
4295
|
setPlaybackState("executing");
|
|
4169
4296
|
}
|
|
4170
4297
|
} catch (err) {
|
|
@@ -4188,14 +4315,14 @@ function useTourPlayback({
|
|
|
4188
4315
|
stopTour();
|
|
4189
4316
|
}, [stopTour]);
|
|
4190
4317
|
const pauseTour = (0, import_react12.useCallback)(() => {
|
|
4191
|
-
if (socketRef.current
|
|
4192
|
-
socketRef.current
|
|
4318
|
+
if (isSocketWritable(socketRef.current) && isActiveRef.current) {
|
|
4319
|
+
emitSocketEvent(socketRef.current, "tour:pause");
|
|
4193
4320
|
setPlaybackState("paused");
|
|
4194
4321
|
}
|
|
4195
4322
|
}, []);
|
|
4196
4323
|
const resumeTour = (0, import_react12.useCallback)(() => {
|
|
4197
|
-
if (socketRef.current
|
|
4198
|
-
socketRef.current
|
|
4324
|
+
if (isSocketWritable(socketRef.current) && isActiveRef.current) {
|
|
4325
|
+
emitSocketEvent(socketRef.current, "tour:resume");
|
|
4199
4326
|
setPlaybackState("executing");
|
|
4200
4327
|
}
|
|
4201
4328
|
}, []);
|
|
@@ -4217,9 +4344,9 @@ function useTourPlayback({
|
|
|
4217
4344
|
if (voiceInputResolveRef.current) {
|
|
4218
4345
|
console.log("[TourAgent] Resolving loop waiting_voice with text:", text);
|
|
4219
4346
|
voiceInputResolveRef.current(text);
|
|
4220
|
-
} else if (socketRef.current
|
|
4347
|
+
} else if (isSocketWritable(socketRef.current)) {
|
|
4221
4348
|
console.log("[TourAgent] Forwarding ambient voice to server:", text);
|
|
4222
|
-
socketRef.current
|
|
4349
|
+
emitSocketEvent(socketRef.current, "tour:user_input", {
|
|
4223
4350
|
transcript: text,
|
|
4224
4351
|
runId: runIdRef.current,
|
|
4225
4352
|
turnId: turnIdRef.current
|
package/dist/index.mjs
CHANGED
|
@@ -2416,7 +2416,6 @@ function isTourEligible(tour, userProfile) {
|
|
|
2416
2416
|
|
|
2417
2417
|
// src/hooks/useTourPlayback.ts
|
|
2418
2418
|
import { useState as useState7, useRef as useRef8, useCallback as useCallback7, useEffect as useEffect11, useContext as useContext4 } from "react";
|
|
2419
|
-
import { io as io2 } from "socket.io-client";
|
|
2420
2419
|
|
|
2421
2420
|
// src/utils/retryLookup.ts
|
|
2422
2421
|
var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -2439,6 +2438,101 @@ async function retryLookup({
|
|
|
2439
2438
|
}
|
|
2440
2439
|
}
|
|
2441
2440
|
|
|
2441
|
+
// src/utils/tour-socket-pool.ts
|
|
2442
|
+
import { io as io2 } from "socket.io-client";
|
|
2443
|
+
function isSocketWritable(socket) {
|
|
2444
|
+
if (!socket?.connected) return false;
|
|
2445
|
+
const engine = socket.io?.engine;
|
|
2446
|
+
if (!engine) return true;
|
|
2447
|
+
if (typeof engine.readyState === "string" && engine.readyState !== "open") return false;
|
|
2448
|
+
if (engine.transport && "writable" in engine.transport && engine.transport.writable === false) return false;
|
|
2449
|
+
return true;
|
|
2450
|
+
}
|
|
2451
|
+
function emitSocketEvent(socket, event, payload) {
|
|
2452
|
+
if (!isSocketWritable(socket)) return false;
|
|
2453
|
+
socket.emit(event, payload);
|
|
2454
|
+
return true;
|
|
2455
|
+
}
|
|
2456
|
+
function createTourSocketPool({
|
|
2457
|
+
createSocket = (serverUrl) => io2(serverUrl, {
|
|
2458
|
+
path: "/socket.io",
|
|
2459
|
+
transports: resolveSocketIoTransports(serverUrl, "polling-first"),
|
|
2460
|
+
autoConnect: true,
|
|
2461
|
+
reconnection: true,
|
|
2462
|
+
reconnectionAttempts: 10,
|
|
2463
|
+
reconnectionDelay: 1e3
|
|
2464
|
+
}),
|
|
2465
|
+
releaseDelayMs = 2500,
|
|
2466
|
+
scheduleRelease = (callback, delayMs) => setTimeout(callback, delayMs),
|
|
2467
|
+
cancelRelease = (timer) => clearTimeout(timer)
|
|
2468
|
+
} = {}) {
|
|
2469
|
+
const pooledSockets = /* @__PURE__ */ new Map();
|
|
2470
|
+
return {
|
|
2471
|
+
acquire(serverUrl) {
|
|
2472
|
+
const pooled = pooledSockets.get(serverUrl);
|
|
2473
|
+
if (pooled) {
|
|
2474
|
+
if (pooled.releaseTimer) {
|
|
2475
|
+
cancelRelease(pooled.releaseTimer);
|
|
2476
|
+
pooled.releaseTimer = null;
|
|
2477
|
+
}
|
|
2478
|
+
pooled.leases += 1;
|
|
2479
|
+
return pooled.socket;
|
|
2480
|
+
}
|
|
2481
|
+
const socket = createSocket(serverUrl);
|
|
2482
|
+
pooledSockets.set(serverUrl, {
|
|
2483
|
+
socket,
|
|
2484
|
+
leases: 1,
|
|
2485
|
+
releaseTimer: null
|
|
2486
|
+
});
|
|
2487
|
+
return socket;
|
|
2488
|
+
},
|
|
2489
|
+
release(serverUrl, socket) {
|
|
2490
|
+
const pooled = pooledSockets.get(serverUrl);
|
|
2491
|
+
if (!pooled || pooled.socket !== socket) return;
|
|
2492
|
+
pooled.leases = Math.max(0, pooled.leases - 1);
|
|
2493
|
+
if (pooled.leases > 0) return;
|
|
2494
|
+
pooled.releaseTimer = scheduleRelease(() => {
|
|
2495
|
+
const latest = pooledSockets.get(serverUrl);
|
|
2496
|
+
if (!latest || latest !== pooled || latest.leases > 0) return;
|
|
2497
|
+
latest.socket.disconnect();
|
|
2498
|
+
pooledSockets.delete(serverUrl);
|
|
2499
|
+
}, releaseDelayMs);
|
|
2500
|
+
},
|
|
2501
|
+
// Test-only introspection
|
|
2502
|
+
getSnapshot(serverUrl) {
|
|
2503
|
+
const pooled = pooledSockets.get(serverUrl);
|
|
2504
|
+
if (!pooled) return null;
|
|
2505
|
+
return { leases: pooled.leases, socket: pooled.socket };
|
|
2506
|
+
}
|
|
2507
|
+
};
|
|
2508
|
+
}
|
|
2509
|
+
var tourSocketPool = createTourSocketPool();
|
|
2510
|
+
|
|
2511
|
+
// src/utils/tour-playback-guards.ts
|
|
2512
|
+
function shouldExecuteTourCommandBatch(isPlaybackActive) {
|
|
2513
|
+
return isPlaybackActive;
|
|
2514
|
+
}
|
|
2515
|
+
function shouldAcceptTourStart(state) {
|
|
2516
|
+
return !state.isPlaybackActive && !state.startRequested;
|
|
2517
|
+
}
|
|
2518
|
+
function shouldRunTourAutoDiscovery(state) {
|
|
2519
|
+
return state.enableAutoDiscovery && !state.disabled && !state.isPlaybackActive && !state.startRequested && !state.hasPendingTour;
|
|
2520
|
+
}
|
|
2521
|
+
function shouldLogTourDebugEntry(state) {
|
|
2522
|
+
if (!state.isPlaybackActive) {
|
|
2523
|
+
if (state.entryType !== "tour_start") {
|
|
2524
|
+
return false;
|
|
2525
|
+
}
|
|
2526
|
+
if (state.entryTourType && state.entryTourType !== state.experienceType) {
|
|
2527
|
+
return false;
|
|
2528
|
+
}
|
|
2529
|
+
}
|
|
2530
|
+
if (state.entryTourType && state.entryTourType !== state.experienceType) {
|
|
2531
|
+
return false;
|
|
2532
|
+
}
|
|
2533
|
+
return true;
|
|
2534
|
+
}
|
|
2535
|
+
|
|
2442
2536
|
// src/hooks/useTourPlayback.ts
|
|
2443
2537
|
function resolveElement(step) {
|
|
2444
2538
|
const el = step.element;
|
|
@@ -2972,14 +3066,17 @@ function useTourPlayback({
|
|
|
2972
3066
|
const showCaptionsRef = useRef8(showCaptions);
|
|
2973
3067
|
const runIdRef = useRef8(null);
|
|
2974
3068
|
const turnIdRef = useRef8(null);
|
|
3069
|
+
const startRequestedRef = useRef8(false);
|
|
2975
3070
|
const socketRef = useRef8(null);
|
|
2976
3071
|
const socketIdRef = useRef8(socketId);
|
|
2977
3072
|
const commandUrlRef = useRef8(commandUrl);
|
|
3073
|
+
const websiteIdRef = useRef8(websiteId);
|
|
2978
3074
|
const onStepChangeRef = useRef8(onStepChange);
|
|
2979
3075
|
const isActiveRef = useRef8(false);
|
|
2980
3076
|
const activeCommandBatchIdRef = useRef8(null);
|
|
2981
3077
|
socketIdRef.current = socketId;
|
|
2982
3078
|
commandUrlRef.current = commandUrl;
|
|
3079
|
+
websiteIdRef.current = websiteId;
|
|
2983
3080
|
onStepChangeRef.current = onStepChange;
|
|
2984
3081
|
isActiveRef.current = isActive;
|
|
2985
3082
|
reviewModeRef.current = isReviewMode;
|
|
@@ -2990,25 +3087,17 @@ function useTourPlayback({
|
|
|
2990
3087
|
useEffect11(() => {
|
|
2991
3088
|
if (disabled) return;
|
|
2992
3089
|
if (typeof window === "undefined") return;
|
|
2993
|
-
|
|
2994
|
-
const socket = io2(serverUrl, {
|
|
2995
|
-
path: "/socket.io",
|
|
2996
|
-
transports: resolveSocketIoTransports(serverUrl, "polling-first"),
|
|
2997
|
-
autoConnect: true,
|
|
2998
|
-
reconnection: true,
|
|
2999
|
-
reconnectionAttempts: 10,
|
|
3000
|
-
reconnectionDelay: 1e3
|
|
3001
|
-
});
|
|
3002
|
-
createdSocket = socket;
|
|
3090
|
+
const socket = tourSocketPool.acquire(serverUrl);
|
|
3003
3091
|
socketRef.current = socket;
|
|
3004
|
-
|
|
3092
|
+
const handleConnect = () => {
|
|
3005
3093
|
console.log("[TourClient] Connected to tour agent server:", socket.id);
|
|
3006
3094
|
const profile = userProfileRef.current;
|
|
3007
|
-
|
|
3008
|
-
|
|
3095
|
+
const currentWebsiteId = websiteIdRef.current;
|
|
3096
|
+
if (currentWebsiteId && profile?.userId) {
|
|
3097
|
+
emitSocketEvent(socket, "tour:init", { websiteId: currentWebsiteId, userId: profile.userId, userType: profile.type });
|
|
3009
3098
|
}
|
|
3010
|
-
}
|
|
3011
|
-
|
|
3099
|
+
};
|
|
3100
|
+
const handleServerState = (payload) => {
|
|
3012
3101
|
if (typeof payload?.runId === "number") {
|
|
3013
3102
|
runIdRef.current = payload.runId;
|
|
3014
3103
|
}
|
|
@@ -3016,8 +3105,8 @@ function useTourPlayback({
|
|
|
3016
3105
|
turnIdRef.current = payload.turnId ?? null;
|
|
3017
3106
|
}
|
|
3018
3107
|
setServerState(payload);
|
|
3019
|
-
}
|
|
3020
|
-
|
|
3108
|
+
};
|
|
3109
|
+
const handleCommandCancel = (payload) => {
|
|
3021
3110
|
console.log("[TourClient] Received command_cancel:", payload);
|
|
3022
3111
|
if (payload.commandBatchId && activeCommandBatchIdRef.current === payload.commandBatchId) {
|
|
3023
3112
|
activeCommandBatchIdRef.current = null;
|
|
@@ -3028,12 +3117,16 @@ function useTourPlayback({
|
|
|
3028
3117
|
window.speechSynthesis.cancel();
|
|
3029
3118
|
}
|
|
3030
3119
|
}
|
|
3031
|
-
}
|
|
3032
|
-
|
|
3120
|
+
};
|
|
3121
|
+
const handleCommand = async (payload) => {
|
|
3122
|
+
if (!shouldExecuteTourCommandBatch(isActiveRef.current)) {
|
|
3123
|
+
const activeType = experienceTypeRef.current;
|
|
3124
|
+
console.log("[TourClient] Ignoring command batch for inactive playback hook:", activeType, payload.stepIndex);
|
|
3125
|
+
return;
|
|
3126
|
+
}
|
|
3033
3127
|
const emitIfOpen = (ev, data) => {
|
|
3034
3128
|
if (socketRef.current !== socket) return;
|
|
3035
|
-
|
|
3036
|
-
socket.emit(ev, data);
|
|
3129
|
+
emitSocketEvent(socket, ev, data);
|
|
3037
3130
|
};
|
|
3038
3131
|
console.log("[TourClient] Received command batch:", payload.stepIndex, payload.commands);
|
|
3039
3132
|
runCleanup(pendingManualWaitCleanupRef.current);
|
|
@@ -3526,8 +3619,8 @@ function useTourPlayback({
|
|
|
3526
3619
|
turnId: turnIdRef.current
|
|
3527
3620
|
});
|
|
3528
3621
|
clearCommandBatchId();
|
|
3529
|
-
}
|
|
3530
|
-
|
|
3622
|
+
};
|
|
3623
|
+
const handleTourStart = async (tourData) => {
|
|
3531
3624
|
if (isActiveRef.current) return;
|
|
3532
3625
|
runIdRef.current = typeof tourData.runId === "number" ? tourData.runId : runIdRef.current;
|
|
3533
3626
|
const tour = tourData.tourContext ?? tourRef.current;
|
|
@@ -3537,6 +3630,7 @@ function useTourPlayback({
|
|
|
3537
3630
|
return;
|
|
3538
3631
|
}
|
|
3539
3632
|
skipRequestedRef.current = false;
|
|
3633
|
+
startRequestedRef.current = false;
|
|
3540
3634
|
const total = tourData.totalSteps ?? tour?.steps?.length ?? 0;
|
|
3541
3635
|
isActiveRef.current = true;
|
|
3542
3636
|
setIsActive(true);
|
|
@@ -3560,8 +3654,8 @@ function useTourPlayback({
|
|
|
3560
3654
|
try {
|
|
3561
3655
|
const { generateMinifiedAOM: generateMinifiedAOM2 } = await import("./aom-HDYNCIOY.mjs");
|
|
3562
3656
|
const aom = generateMinifiedAOM2();
|
|
3563
|
-
if (socketRef.current === socket
|
|
3564
|
-
socket
|
|
3657
|
+
if (socketRef.current === socket) {
|
|
3658
|
+
emitSocketEvent(socket, "tour:sync_dom", {
|
|
3565
3659
|
url: window.location.pathname + window.location.search + window.location.hash,
|
|
3566
3660
|
aom: aom.nodes,
|
|
3567
3661
|
domSummary: captureDomSummary()
|
|
@@ -3570,8 +3664,8 @@ function useTourPlayback({
|
|
|
3570
3664
|
} catch (e) {
|
|
3571
3665
|
console.warn("[TourClient] Initial DOM sync failed:", e);
|
|
3572
3666
|
}
|
|
3573
|
-
}
|
|
3574
|
-
|
|
3667
|
+
};
|
|
3668
|
+
const handleTourUpdate = (payload) => {
|
|
3575
3669
|
const updatedTour = payload?.tourContext;
|
|
3576
3670
|
if (!updatedTour?.id || updatedTour.id !== tourRef.current?.id) {
|
|
3577
3671
|
return;
|
|
@@ -3588,13 +3682,22 @@ function useTourPlayback({
|
|
|
3588
3682
|
onStepChangeRef.current?.(clampedStepIndex, nextTotal, updatedTour);
|
|
3589
3683
|
}
|
|
3590
3684
|
}
|
|
3591
|
-
}
|
|
3592
|
-
|
|
3685
|
+
};
|
|
3686
|
+
const handleTourEndEvent = () => {
|
|
3593
3687
|
setServerState((prev) => prev ? { ...prev, isActive: false, phase: "completed" } : prev);
|
|
3594
3688
|
handleTourEnd();
|
|
3595
|
-
}
|
|
3596
|
-
|
|
3689
|
+
};
|
|
3690
|
+
const handleDebugLog = (entry) => {
|
|
3597
3691
|
const isDev = devModeRef.current || process.env.NODE_ENV === "development" || typeof window !== "undefined" && window.MODELNEX_DEBUG;
|
|
3692
|
+
const entryTourType = entry?.data?.tourContext?.type ?? tourRef.current?.type ?? null;
|
|
3693
|
+
if (!shouldLogTourDebugEntry({
|
|
3694
|
+
isPlaybackActive: isActiveRef.current,
|
|
3695
|
+
entryType: entry?.type,
|
|
3696
|
+
entryTourType,
|
|
3697
|
+
experienceType: experienceTypeRef.current
|
|
3698
|
+
})) {
|
|
3699
|
+
return;
|
|
3700
|
+
}
|
|
3598
3701
|
if (isDev) {
|
|
3599
3702
|
console.log(`%c[ModelNex Tour] ${entry.type}`, "color: #3b82f6; font-weight: bold", entry);
|
|
3600
3703
|
if (typeof window !== "undefined") {
|
|
@@ -3603,29 +3706,40 @@ function useTourPlayback({
|
|
|
3603
3706
|
}));
|
|
3604
3707
|
}
|
|
3605
3708
|
}
|
|
3606
|
-
}
|
|
3709
|
+
};
|
|
3710
|
+
socket.on("connect", handleConnect);
|
|
3711
|
+
socket.on("tour:server_state", handleServerState);
|
|
3712
|
+
socket.on("tour:command_cancel", handleCommandCancel);
|
|
3713
|
+
socket.on("tour:command", handleCommand);
|
|
3714
|
+
socket.on("tour:start", handleTourStart);
|
|
3715
|
+
socket.on("tour:update", handleTourUpdate);
|
|
3716
|
+
socket.on("tour:end", handleTourEndEvent);
|
|
3717
|
+
socket.on("tour:debug_log", handleDebugLog);
|
|
3607
3718
|
console.log("[ModelNex SDK] Tour playback initialized. Debug logs enabled:", devModeRef.current || process.env.NODE_ENV === "development");
|
|
3608
3719
|
return () => {
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3720
|
+
socket.off("connect", handleConnect);
|
|
3721
|
+
socket.off("tour:server_state", handleServerState);
|
|
3722
|
+
socket.off("tour:command_cancel", handleCommandCancel);
|
|
3723
|
+
socket.off("tour:command", handleCommand);
|
|
3724
|
+
socket.off("tour:start", handleTourStart);
|
|
3725
|
+
socket.off("tour:update", handleTourUpdate);
|
|
3726
|
+
socket.off("tour:end", handleTourEndEvent);
|
|
3727
|
+
socket.off("tour:debug_log", handleDebugLog);
|
|
3728
|
+
const toClose = socket;
|
|
3614
3729
|
socketRef.current = null;
|
|
3615
3730
|
setServerState(null);
|
|
3616
3731
|
runIdRef.current = null;
|
|
3617
3732
|
turnIdRef.current = null;
|
|
3733
|
+
tourSocketPool.release(serverUrl, toClose);
|
|
3618
3734
|
};
|
|
3619
|
-
}, [serverUrl,
|
|
3735
|
+
}, [serverUrl, disabled]);
|
|
3620
3736
|
useEffect11(() => {
|
|
3621
3737
|
if (disabled) return;
|
|
3622
3738
|
const s = socketRef.current;
|
|
3623
3739
|
const profile = userProfile;
|
|
3624
|
-
if (!
|
|
3740
|
+
if (!websiteId || !profile?.userId) return;
|
|
3625
3741
|
const timer = setTimeout(() => {
|
|
3626
|
-
|
|
3627
|
-
s.emit("tour:init", { websiteId, userId: profile.userId, userType: profile.type });
|
|
3628
|
-
}
|
|
3742
|
+
emitSocketEvent(s, "tour:init", { websiteId, userId: profile.userId, userType: profile.type });
|
|
3629
3743
|
}, 150);
|
|
3630
3744
|
return () => clearTimeout(timer);
|
|
3631
3745
|
}, [disabled, websiteId, userProfile?.userId, userProfile?.type]);
|
|
@@ -3635,8 +3749,8 @@ function useTourPlayback({
|
|
|
3635
3749
|
}
|
|
3636
3750
|
}, [showCaptions, isReviewMode]);
|
|
3637
3751
|
useEffect11(() => {
|
|
3638
|
-
if (!
|
|
3639
|
-
socketRef.current
|
|
3752
|
+
if (!isActiveRef.current) return;
|
|
3753
|
+
emitSocketEvent(socketRef.current, "tour:client_state", {
|
|
3640
3754
|
runId: runIdRef.current,
|
|
3641
3755
|
turnId: turnIdRef.current,
|
|
3642
3756
|
commandBatchId: activeCommandBatchIdRef.current,
|
|
@@ -3649,19 +3763,17 @@ function useTourPlayback({
|
|
|
3649
3763
|
});
|
|
3650
3764
|
}, [isActive, playbackState, voice.isListening, voice.isSpeaking]);
|
|
3651
3765
|
const syncAOM = useCallback7(async () => {
|
|
3652
|
-
if (!
|
|
3766
|
+
if (!isActiveRef.current) return;
|
|
3653
3767
|
const { generateMinifiedAOM: generateMinifiedAOM2 } = await import("./aom-HDYNCIOY.mjs");
|
|
3654
3768
|
const aom = generateMinifiedAOM2();
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
});
|
|
3661
|
-
}
|
|
3769
|
+
emitSocketEvent(socketRef.current, "tour:sync_dom", {
|
|
3770
|
+
url: window.location.pathname + window.location.search + window.location.hash,
|
|
3771
|
+
aom: aom.nodes,
|
|
3772
|
+
domSummary: captureDomSummary()
|
|
3773
|
+
});
|
|
3662
3774
|
}, []);
|
|
3663
3775
|
const interruptExecution = useCallback7((transcript) => {
|
|
3664
|
-
if (!socketRef.current
|
|
3776
|
+
if (!isSocketWritable(socketRef.current) || !isActiveRef.current) return false;
|
|
3665
3777
|
if (!commandInFlightRef.current && !voice.isSpeaking) return false;
|
|
3666
3778
|
interruptedForQuestionRef.current = true;
|
|
3667
3779
|
activeExecutionTokenRef.current += 1;
|
|
@@ -3669,17 +3781,16 @@ function useTourPlayback({
|
|
|
3669
3781
|
removeHighlight();
|
|
3670
3782
|
removeCaption();
|
|
3671
3783
|
voice.stopSpeaking();
|
|
3672
|
-
if (socketRef.current
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
});
|
|
3784
|
+
if (emitSocketEvent(socketRef.current, "tour:action_result", {
|
|
3785
|
+
success: true,
|
|
3786
|
+
interrupted: true,
|
|
3787
|
+
transcript,
|
|
3788
|
+
commandBatchId: activeCommandBatchIdRef.current,
|
|
3789
|
+
runId: runIdRef.current,
|
|
3790
|
+
turnId: turnIdRef.current
|
|
3791
|
+
})) {
|
|
3681
3792
|
activeCommandBatchIdRef.current = null;
|
|
3682
|
-
socketRef.current
|
|
3793
|
+
emitSocketEvent(socketRef.current, "tour:user_input", {
|
|
3683
3794
|
transcript,
|
|
3684
3795
|
interrupted: true,
|
|
3685
3796
|
runId: runIdRef.current,
|
|
@@ -3692,6 +3803,7 @@ function useTourPlayback({
|
|
|
3692
3803
|
const stopTour = useCallback7(() => {
|
|
3693
3804
|
skipRequestedRef.current = true;
|
|
3694
3805
|
isActiveRef.current = false;
|
|
3806
|
+
startRequestedRef.current = false;
|
|
3695
3807
|
activeExecutionTokenRef.current += 1;
|
|
3696
3808
|
commandInFlightRef.current = false;
|
|
3697
3809
|
activeCommandBatchIdRef.current = null;
|
|
@@ -3701,9 +3813,7 @@ function useTourPlayback({
|
|
|
3701
3813
|
removeCaption();
|
|
3702
3814
|
voice.stopSpeaking();
|
|
3703
3815
|
voice.stopListening();
|
|
3704
|
-
|
|
3705
|
-
socketRef.current.emit("tour:abort");
|
|
3706
|
-
}
|
|
3816
|
+
emitSocketEvent(socketRef.current, "tour:abort");
|
|
3707
3817
|
if (reviewModeRef.current && tourRef.current?.id && previewRunIdRef.current) {
|
|
3708
3818
|
void logPreviewEvent(serverUrl, toursApiBaseRef.current, tourRef.current.id, previewRunIdRef.current, websiteId, {
|
|
3709
3819
|
stepOrder: stepIndexRef.current,
|
|
@@ -3735,6 +3845,7 @@ function useTourPlayback({
|
|
|
3735
3845
|
const endingPreviewRunId = previewRunIdRef.current;
|
|
3736
3846
|
const endingStepOrder = stepIndexRef.current;
|
|
3737
3847
|
isActiveRef.current = false;
|
|
3848
|
+
startRequestedRef.current = false;
|
|
3738
3849
|
setPlaybackState("complete");
|
|
3739
3850
|
removeHighlight();
|
|
3740
3851
|
removeCaption();
|
|
@@ -3768,6 +3879,13 @@ function useTourPlayback({
|
|
|
3768
3879
|
onTourEnd?.();
|
|
3769
3880
|
}, [experienceType, userProfile, serverUrl, voice, onTourEnd, websiteId]);
|
|
3770
3881
|
const runTour = useCallback7(async (tour, options) => {
|
|
3882
|
+
if (!shouldAcceptTourStart({
|
|
3883
|
+
isPlaybackActive: isActiveRef.current,
|
|
3884
|
+
startRequested: startRequestedRef.current
|
|
3885
|
+
})) {
|
|
3886
|
+
console.log("[TourClient] Ignoring duplicate start request while playback is already active or starting:", tour.id);
|
|
3887
|
+
return;
|
|
3888
|
+
}
|
|
3771
3889
|
setPendingTour(null);
|
|
3772
3890
|
pendingTourRef.current = null;
|
|
3773
3891
|
let retries = 0;
|
|
@@ -3775,10 +3893,11 @@ function useTourPlayback({
|
|
|
3775
3893
|
await new Promise((r) => setTimeout(r, 200));
|
|
3776
3894
|
retries++;
|
|
3777
3895
|
}
|
|
3778
|
-
if (!socketRef.current
|
|
3896
|
+
if (!isSocketWritable(socketRef.current)) {
|
|
3779
3897
|
console.warn("[TourClient] Cannot run tour, socket not connected.");
|
|
3780
3898
|
return;
|
|
3781
3899
|
}
|
|
3900
|
+
startRequestedRef.current = true;
|
|
3782
3901
|
const shouldReview = Boolean(options?.reviewMode);
|
|
3783
3902
|
resetCaptionSuppression();
|
|
3784
3903
|
setReviewStatusMessage(null);
|
|
@@ -3803,17 +3922,20 @@ function useTourPlayback({
|
|
|
3803
3922
|
previewRunIdRef.current = null;
|
|
3804
3923
|
}
|
|
3805
3924
|
tourRef.current = tour;
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
});
|
|
3812
|
-
}
|
|
3925
|
+
emitSocketEvent(socketRef.current, "tour:request_start", {
|
|
3926
|
+
tourId: tour.id,
|
|
3927
|
+
previewRunId: previewRunIdRef.current,
|
|
3928
|
+
tourContext: tour
|
|
3929
|
+
});
|
|
3813
3930
|
}, [serverUrl, websiteId]);
|
|
3814
3931
|
useEffect11(() => {
|
|
3815
|
-
if (!
|
|
3816
|
-
|
|
3932
|
+
if (!shouldRunTourAutoDiscovery({
|
|
3933
|
+
enableAutoDiscovery,
|
|
3934
|
+
disabled,
|
|
3935
|
+
isPlaybackActive: isActiveRef.current,
|
|
3936
|
+
startRequested: startRequestedRef.current,
|
|
3937
|
+
hasPendingTour: Boolean(pendingTourRef.current)
|
|
3938
|
+
})) return;
|
|
3817
3939
|
if (typeof window === "undefined") return;
|
|
3818
3940
|
const params = new URLSearchParams(window.location.search);
|
|
3819
3941
|
const queryParam = experienceType === "onboarding" ? "modelnex_test_workflow" : "modelnex_test_tour";
|
|
@@ -3861,8 +3983,13 @@ function useTourPlayback({
|
|
|
3861
3983
|
};
|
|
3862
3984
|
}, [serverUrl, toursApiBase, disabled, websiteId, experienceType, enableAutoDiscovery]);
|
|
3863
3985
|
useEffect11(() => {
|
|
3864
|
-
if (!
|
|
3865
|
-
|
|
3986
|
+
if (!shouldRunTourAutoDiscovery({
|
|
3987
|
+
enableAutoDiscovery,
|
|
3988
|
+
disabled,
|
|
3989
|
+
isPlaybackActive: isActiveRef.current,
|
|
3990
|
+
startRequested: startRequestedRef.current,
|
|
3991
|
+
hasPendingTour: Boolean(pendingTourRef.current)
|
|
3992
|
+
})) return;
|
|
3866
3993
|
if (!websiteId || !userProfile) return;
|
|
3867
3994
|
if (typeof window !== "undefined") {
|
|
3868
3995
|
const params = new URLSearchParams(window.location.search);
|
|
@@ -3901,7 +4028,7 @@ function useTourPlayback({
|
|
|
3901
4028
|
cancelled = true;
|
|
3902
4029
|
clearTimeout(timer);
|
|
3903
4030
|
};
|
|
3904
|
-
}, [websiteId, serverUrl, toursApiBase, disabled, experienceType, userProfile, enableAutoDiscovery]);
|
|
4031
|
+
}, [websiteId, serverUrl, toursApiBase, disabled, experienceType, userProfile?.userId, userProfile?.type, userProfile?.isNewUser, enableAutoDiscovery]);
|
|
3905
4032
|
useEffect11(() => {
|
|
3906
4033
|
if (!disabled || !isActiveRef.current) return;
|
|
3907
4034
|
stopTour();
|
|
@@ -3953,8 +4080,8 @@ function useTourPlayback({
|
|
|
3953
4080
|
const revisionVersion = Number(response?.revision?.versionNumber);
|
|
3954
4081
|
const appliedMessage = Number.isFinite(revisionVersion) && revisionVersion > 0 ? `Applied to the draft as version ${revisionVersion}.` : "Correction applied to the draft.";
|
|
3955
4082
|
setReviewStatusMessage(apply ? appliedMessage : "Correction saved for review.");
|
|
3956
|
-
if (apply && playbackState === "paused" &&
|
|
3957
|
-
socketRef.current
|
|
4083
|
+
if (apply && playbackState === "paused" && isActiveRef.current) {
|
|
4084
|
+
emitSocketEvent(socketRef.current, "tour:resume");
|
|
3958
4085
|
setPlaybackState("executing");
|
|
3959
4086
|
}
|
|
3960
4087
|
} catch (err) {
|
|
@@ -3978,14 +4105,14 @@ function useTourPlayback({
|
|
|
3978
4105
|
stopTour();
|
|
3979
4106
|
}, [stopTour]);
|
|
3980
4107
|
const pauseTour = useCallback7(() => {
|
|
3981
|
-
if (socketRef.current
|
|
3982
|
-
socketRef.current
|
|
4108
|
+
if (isSocketWritable(socketRef.current) && isActiveRef.current) {
|
|
4109
|
+
emitSocketEvent(socketRef.current, "tour:pause");
|
|
3983
4110
|
setPlaybackState("paused");
|
|
3984
4111
|
}
|
|
3985
4112
|
}, []);
|
|
3986
4113
|
const resumeTour = useCallback7(() => {
|
|
3987
|
-
if (socketRef.current
|
|
3988
|
-
socketRef.current
|
|
4114
|
+
if (isSocketWritable(socketRef.current) && isActiveRef.current) {
|
|
4115
|
+
emitSocketEvent(socketRef.current, "tour:resume");
|
|
3989
4116
|
setPlaybackState("executing");
|
|
3990
4117
|
}
|
|
3991
4118
|
}, []);
|
|
@@ -4007,9 +4134,9 @@ function useTourPlayback({
|
|
|
4007
4134
|
if (voiceInputResolveRef.current) {
|
|
4008
4135
|
console.log("[TourAgent] Resolving loop waiting_voice with text:", text);
|
|
4009
4136
|
voiceInputResolveRef.current(text);
|
|
4010
|
-
} else if (socketRef.current
|
|
4137
|
+
} else if (isSocketWritable(socketRef.current)) {
|
|
4011
4138
|
console.log("[TourAgent] Forwarding ambient voice to server:", text);
|
|
4012
|
-
socketRef.current
|
|
4139
|
+
emitSocketEvent(socketRef.current, "tour:user_input", {
|
|
4013
4140
|
transcript: text,
|
|
4014
4141
|
runId: runIdRef.current,
|
|
4015
4142
|
turnId: turnIdRef.current
|