@modelnex/sdk 0.5.17 → 0.5.19
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 +114 -8
- package/dist/index.mjs +114 -8
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2718,6 +2718,51 @@ function createTourSocketPool({
|
|
|
2718
2718
|
}
|
|
2719
2719
|
var tourSocketPool = createTourSocketPool();
|
|
2720
2720
|
|
|
2721
|
+
// src/utils/tour-playback-guards.ts
|
|
2722
|
+
var playbackOwners = /* @__PURE__ */ new Map();
|
|
2723
|
+
function shouldExecuteTourCommandBatch(isPlaybackActive) {
|
|
2724
|
+
return isPlaybackActive;
|
|
2725
|
+
}
|
|
2726
|
+
function shouldAcceptTourStart(state) {
|
|
2727
|
+
return !state.isPlaybackActive && !state.startRequested;
|
|
2728
|
+
}
|
|
2729
|
+
function shouldRunTourAutoDiscovery(state) {
|
|
2730
|
+
return state.enableAutoDiscovery && !state.disabled && !state.isPlaybackActive && !state.startRequested && !state.hasPendingTour;
|
|
2731
|
+
}
|
|
2732
|
+
function shouldLogTourDebugEntry(state) {
|
|
2733
|
+
if (!state.isPlaybackActive) {
|
|
2734
|
+
if (state.entryType !== "tour_start") {
|
|
2735
|
+
return false;
|
|
2736
|
+
}
|
|
2737
|
+
if (state.entryTourType && state.entryTourType !== state.experienceType) {
|
|
2738
|
+
return false;
|
|
2739
|
+
}
|
|
2740
|
+
}
|
|
2741
|
+
if (state.entryTourType && state.entryTourType !== state.experienceType) {
|
|
2742
|
+
return false;
|
|
2743
|
+
}
|
|
2744
|
+
return true;
|
|
2745
|
+
}
|
|
2746
|
+
function createTourPlaybackOwnerKey(serverUrl, websiteId, experienceType) {
|
|
2747
|
+
return `${serverUrl}::${websiteId || "__default__"}::${experienceType}`;
|
|
2748
|
+
}
|
|
2749
|
+
function claimTourPlaybackOwnership(key, ownerId) {
|
|
2750
|
+
const currentOwner = playbackOwners.get(key);
|
|
2751
|
+
if (currentOwner && currentOwner !== ownerId) {
|
|
2752
|
+
return false;
|
|
2753
|
+
}
|
|
2754
|
+
playbackOwners.set(key, ownerId);
|
|
2755
|
+
return true;
|
|
2756
|
+
}
|
|
2757
|
+
function hasTourPlaybackOwnership(key, ownerId) {
|
|
2758
|
+
return playbackOwners.get(key) === ownerId;
|
|
2759
|
+
}
|
|
2760
|
+
function releaseTourPlaybackOwnership(key, ownerId) {
|
|
2761
|
+
if (playbackOwners.get(key) === ownerId) {
|
|
2762
|
+
playbackOwners.delete(key);
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2765
|
+
|
|
2721
2766
|
// src/hooks/useTourPlayback.ts
|
|
2722
2767
|
function resolveElement(step) {
|
|
2723
2768
|
const el = step.element;
|
|
@@ -3251,6 +3296,9 @@ function useTourPlayback({
|
|
|
3251
3296
|
const showCaptionsRef = (0, import_react12.useRef)(showCaptions);
|
|
3252
3297
|
const runIdRef = (0, import_react12.useRef)(null);
|
|
3253
3298
|
const turnIdRef = (0, import_react12.useRef)(null);
|
|
3299
|
+
const startRequestedRef = (0, import_react12.useRef)(false);
|
|
3300
|
+
const playbackOwnerIdRef = (0, import_react12.useRef)(`playback-owner-${Math.random().toString(36).slice(2, 10)}`);
|
|
3301
|
+
const claimedPlaybackOwnerKeyRef = (0, import_react12.useRef)(null);
|
|
3254
3302
|
const socketRef = (0, import_react12.useRef)(null);
|
|
3255
3303
|
const socketIdRef = (0, import_react12.useRef)(socketId);
|
|
3256
3304
|
const commandUrlRef = (0, import_react12.useRef)(commandUrl);
|
|
@@ -3268,6 +3316,12 @@ function useTourPlayback({
|
|
|
3268
3316
|
toursApiBaseRef.current = toursApiBase;
|
|
3269
3317
|
pendingTourRef.current = pendingTour;
|
|
3270
3318
|
showCaptionsRef.current = showCaptions;
|
|
3319
|
+
const releasePlaybackOwnership = (0, import_react12.useCallback)(() => {
|
|
3320
|
+
const claimedKey = claimedPlaybackOwnerKeyRef.current;
|
|
3321
|
+
if (!claimedKey) return;
|
|
3322
|
+
releaseTourPlaybackOwnership(claimedKey, playbackOwnerIdRef.current);
|
|
3323
|
+
claimedPlaybackOwnerKeyRef.current = null;
|
|
3324
|
+
}, []);
|
|
3271
3325
|
(0, import_react12.useEffect)(() => {
|
|
3272
3326
|
if (disabled) return;
|
|
3273
3327
|
if (typeof window === "undefined") return;
|
|
@@ -3303,6 +3357,12 @@ function useTourPlayback({
|
|
|
3303
3357
|
}
|
|
3304
3358
|
};
|
|
3305
3359
|
const handleCommand = async (payload) => {
|
|
3360
|
+
const ownerKey = createTourPlaybackOwnerKey(serverUrl, websiteIdRef.current, experienceTypeRef.current);
|
|
3361
|
+
if (!shouldExecuteTourCommandBatch(isActiveRef.current) || !hasTourPlaybackOwnership(ownerKey, playbackOwnerIdRef.current)) {
|
|
3362
|
+
const activeType = experienceTypeRef.current;
|
|
3363
|
+
console.log("[TourClient] Ignoring command batch for inactive playback hook:", activeType, payload.stepIndex);
|
|
3364
|
+
return;
|
|
3365
|
+
}
|
|
3306
3366
|
const emitIfOpen = (ev, data) => {
|
|
3307
3367
|
if (socketRef.current !== socket) return;
|
|
3308
3368
|
emitSocketEvent(socket, ev, data);
|
|
@@ -3808,7 +3868,14 @@ function useTourPlayback({
|
|
|
3808
3868
|
console.log(`[TourClient] Ignoring ${tour.type} start (this hook is for ${expType})`);
|
|
3809
3869
|
return;
|
|
3810
3870
|
}
|
|
3871
|
+
const ownerKey = createTourPlaybackOwnerKey(serverUrl, websiteIdRef.current, expType);
|
|
3872
|
+
if (!claimTourPlaybackOwnership(ownerKey, playbackOwnerIdRef.current)) {
|
|
3873
|
+
console.log(`[TourClient] Ignoring ${expType} start because another hook already owns playback`);
|
|
3874
|
+
return;
|
|
3875
|
+
}
|
|
3876
|
+
claimedPlaybackOwnerKeyRef.current = ownerKey;
|
|
3811
3877
|
skipRequestedRef.current = false;
|
|
3878
|
+
startRequestedRef.current = false;
|
|
3812
3879
|
const total = tourData.totalSteps ?? tour?.steps?.length ?? 0;
|
|
3813
3880
|
isActiveRef.current = true;
|
|
3814
3881
|
setIsActive(true);
|
|
@@ -3867,6 +3934,16 @@ function useTourPlayback({
|
|
|
3867
3934
|
};
|
|
3868
3935
|
const handleDebugLog = (entry) => {
|
|
3869
3936
|
const isDev = devModeRef.current || process.env.NODE_ENV === "development" || typeof window !== "undefined" && window.MODELNEX_DEBUG;
|
|
3937
|
+
const entryTourType = entry?.data?.tourContext?.type ?? tourRef.current?.type ?? null;
|
|
3938
|
+
const ownerKey = createTourPlaybackOwnerKey(serverUrl, websiteIdRef.current, experienceTypeRef.current);
|
|
3939
|
+
if (!shouldLogTourDebugEntry({
|
|
3940
|
+
isPlaybackActive: isActiveRef.current,
|
|
3941
|
+
entryType: entry?.type,
|
|
3942
|
+
entryTourType,
|
|
3943
|
+
experienceType: experienceTypeRef.current
|
|
3944
|
+
}) || isActiveRef.current && !hasTourPlaybackOwnership(ownerKey, playbackOwnerIdRef.current)) {
|
|
3945
|
+
return;
|
|
3946
|
+
}
|
|
3870
3947
|
if (isDev) {
|
|
3871
3948
|
console.log(`%c[ModelNex Tour] ${entry.type}`, "color: #3b82f6; font-weight: bold", entry);
|
|
3872
3949
|
if (typeof window !== "undefined") {
|
|
@@ -3899,9 +3976,10 @@ function useTourPlayback({
|
|
|
3899
3976
|
setServerState(null);
|
|
3900
3977
|
runIdRef.current = null;
|
|
3901
3978
|
turnIdRef.current = null;
|
|
3979
|
+
releasePlaybackOwnership();
|
|
3902
3980
|
tourSocketPool.release(serverUrl, toClose);
|
|
3903
3981
|
};
|
|
3904
|
-
}, [serverUrl, disabled]);
|
|
3982
|
+
}, [serverUrl, disabled, releasePlaybackOwnership]);
|
|
3905
3983
|
(0, import_react12.useEffect)(() => {
|
|
3906
3984
|
if (disabled) return;
|
|
3907
3985
|
const s = socketRef.current;
|
|
@@ -3972,6 +4050,8 @@ function useTourPlayback({
|
|
|
3972
4050
|
const stopTour = (0, import_react12.useCallback)(() => {
|
|
3973
4051
|
skipRequestedRef.current = true;
|
|
3974
4052
|
isActiveRef.current = false;
|
|
4053
|
+
startRequestedRef.current = false;
|
|
4054
|
+
releasePlaybackOwnership();
|
|
3975
4055
|
activeExecutionTokenRef.current += 1;
|
|
3976
4056
|
commandInFlightRef.current = false;
|
|
3977
4057
|
activeCommandBatchIdRef.current = null;
|
|
@@ -4007,12 +4087,14 @@ function useTourPlayback({
|
|
|
4007
4087
|
setIsReviewMode(false);
|
|
4008
4088
|
pendingInputBufRef.current = null;
|
|
4009
4089
|
onTourEnd?.();
|
|
4010
|
-
}, [voice, onTourEnd, serverUrl, websiteId]);
|
|
4090
|
+
}, [voice, onTourEnd, serverUrl, websiteId, releasePlaybackOwnership]);
|
|
4011
4091
|
const handleTourEnd = (0, import_react12.useCallback)(() => {
|
|
4012
4092
|
const endingTourId = tourRef.current?.id;
|
|
4013
4093
|
const endingPreviewRunId = previewRunIdRef.current;
|
|
4014
4094
|
const endingStepOrder = stepIndexRef.current;
|
|
4015
4095
|
isActiveRef.current = false;
|
|
4096
|
+
startRequestedRef.current = false;
|
|
4097
|
+
releasePlaybackOwnership();
|
|
4016
4098
|
setPlaybackState("complete");
|
|
4017
4099
|
removeHighlight();
|
|
4018
4100
|
removeCaption();
|
|
@@ -4044,8 +4126,15 @@ function useTourPlayback({
|
|
|
4044
4126
|
clearActiveDraftPreview(experienceType);
|
|
4045
4127
|
}
|
|
4046
4128
|
onTourEnd?.();
|
|
4047
|
-
}, [experienceType, userProfile, serverUrl, voice, onTourEnd, websiteId]);
|
|
4129
|
+
}, [experienceType, userProfile, serverUrl, voice, onTourEnd, websiteId, releasePlaybackOwnership]);
|
|
4048
4130
|
const runTour = (0, import_react12.useCallback)(async (tour, options) => {
|
|
4131
|
+
if (!shouldAcceptTourStart({
|
|
4132
|
+
isPlaybackActive: isActiveRef.current,
|
|
4133
|
+
startRequested: startRequestedRef.current
|
|
4134
|
+
})) {
|
|
4135
|
+
console.log("[TourClient] Ignoring duplicate start request while playback is already active or starting:", tour.id);
|
|
4136
|
+
return;
|
|
4137
|
+
}
|
|
4049
4138
|
setPendingTour(null);
|
|
4050
4139
|
pendingTourRef.current = null;
|
|
4051
4140
|
let retries = 0;
|
|
@@ -4057,6 +4146,13 @@ function useTourPlayback({
|
|
|
4057
4146
|
console.warn("[TourClient] Cannot run tour, socket not connected.");
|
|
4058
4147
|
return;
|
|
4059
4148
|
}
|
|
4149
|
+
const ownerKey = createTourPlaybackOwnerKey(serverUrl, websiteIdRef.current, tour.type ?? experienceTypeRef.current);
|
|
4150
|
+
if (!claimTourPlaybackOwnership(ownerKey, playbackOwnerIdRef.current)) {
|
|
4151
|
+
console.log("[TourClient] Ignoring duplicate start request because another hook already owns this experience:", tour.id);
|
|
4152
|
+
return;
|
|
4153
|
+
}
|
|
4154
|
+
claimedPlaybackOwnerKeyRef.current = ownerKey;
|
|
4155
|
+
startRequestedRef.current = true;
|
|
4060
4156
|
const shouldReview = Boolean(options?.reviewMode);
|
|
4061
4157
|
resetCaptionSuppression();
|
|
4062
4158
|
setReviewStatusMessage(null);
|
|
@@ -4088,8 +4184,13 @@ function useTourPlayback({
|
|
|
4088
4184
|
});
|
|
4089
4185
|
}, [serverUrl, websiteId]);
|
|
4090
4186
|
(0, import_react12.useEffect)(() => {
|
|
4091
|
-
if (!
|
|
4092
|
-
|
|
4187
|
+
if (!shouldRunTourAutoDiscovery({
|
|
4188
|
+
enableAutoDiscovery,
|
|
4189
|
+
disabled,
|
|
4190
|
+
isPlaybackActive: isActiveRef.current,
|
|
4191
|
+
startRequested: startRequestedRef.current,
|
|
4192
|
+
hasPendingTour: Boolean(pendingTourRef.current)
|
|
4193
|
+
})) return;
|
|
4093
4194
|
if (typeof window === "undefined") return;
|
|
4094
4195
|
const params = new URLSearchParams(window.location.search);
|
|
4095
4196
|
const queryParam = experienceType === "onboarding" ? "modelnex_test_workflow" : "modelnex_test_tour";
|
|
@@ -4137,8 +4238,13 @@ function useTourPlayback({
|
|
|
4137
4238
|
};
|
|
4138
4239
|
}, [serverUrl, toursApiBase, disabled, websiteId, experienceType, enableAutoDiscovery]);
|
|
4139
4240
|
(0, import_react12.useEffect)(() => {
|
|
4140
|
-
if (!
|
|
4141
|
-
|
|
4241
|
+
if (!shouldRunTourAutoDiscovery({
|
|
4242
|
+
enableAutoDiscovery,
|
|
4243
|
+
disabled,
|
|
4244
|
+
isPlaybackActive: isActiveRef.current,
|
|
4245
|
+
startRequested: startRequestedRef.current,
|
|
4246
|
+
hasPendingTour: Boolean(pendingTourRef.current)
|
|
4247
|
+
})) return;
|
|
4142
4248
|
if (!websiteId || !userProfile) return;
|
|
4143
4249
|
if (typeof window !== "undefined") {
|
|
4144
4250
|
const params = new URLSearchParams(window.location.search);
|
|
@@ -4177,7 +4283,7 @@ function useTourPlayback({
|
|
|
4177
4283
|
cancelled = true;
|
|
4178
4284
|
clearTimeout(timer);
|
|
4179
4285
|
};
|
|
4180
|
-
}, [websiteId, serverUrl, toursApiBase, disabled, experienceType, userProfile, enableAutoDiscovery]);
|
|
4286
|
+
}, [websiteId, serverUrl, toursApiBase, disabled, experienceType, userProfile?.userId, userProfile?.type, userProfile?.isNewUser, enableAutoDiscovery]);
|
|
4181
4287
|
(0, import_react12.useEffect)(() => {
|
|
4182
4288
|
if (!disabled || !isActiveRef.current) return;
|
|
4183
4289
|
stopTour();
|
package/dist/index.mjs
CHANGED
|
@@ -2508,6 +2508,51 @@ function createTourSocketPool({
|
|
|
2508
2508
|
}
|
|
2509
2509
|
var tourSocketPool = createTourSocketPool();
|
|
2510
2510
|
|
|
2511
|
+
// src/utils/tour-playback-guards.ts
|
|
2512
|
+
var playbackOwners = /* @__PURE__ */ new Map();
|
|
2513
|
+
function shouldExecuteTourCommandBatch(isPlaybackActive) {
|
|
2514
|
+
return isPlaybackActive;
|
|
2515
|
+
}
|
|
2516
|
+
function shouldAcceptTourStart(state) {
|
|
2517
|
+
return !state.isPlaybackActive && !state.startRequested;
|
|
2518
|
+
}
|
|
2519
|
+
function shouldRunTourAutoDiscovery(state) {
|
|
2520
|
+
return state.enableAutoDiscovery && !state.disabled && !state.isPlaybackActive && !state.startRequested && !state.hasPendingTour;
|
|
2521
|
+
}
|
|
2522
|
+
function shouldLogTourDebugEntry(state) {
|
|
2523
|
+
if (!state.isPlaybackActive) {
|
|
2524
|
+
if (state.entryType !== "tour_start") {
|
|
2525
|
+
return false;
|
|
2526
|
+
}
|
|
2527
|
+
if (state.entryTourType && state.entryTourType !== state.experienceType) {
|
|
2528
|
+
return false;
|
|
2529
|
+
}
|
|
2530
|
+
}
|
|
2531
|
+
if (state.entryTourType && state.entryTourType !== state.experienceType) {
|
|
2532
|
+
return false;
|
|
2533
|
+
}
|
|
2534
|
+
return true;
|
|
2535
|
+
}
|
|
2536
|
+
function createTourPlaybackOwnerKey(serverUrl, websiteId, experienceType) {
|
|
2537
|
+
return `${serverUrl}::${websiteId || "__default__"}::${experienceType}`;
|
|
2538
|
+
}
|
|
2539
|
+
function claimTourPlaybackOwnership(key, ownerId) {
|
|
2540
|
+
const currentOwner = playbackOwners.get(key);
|
|
2541
|
+
if (currentOwner && currentOwner !== ownerId) {
|
|
2542
|
+
return false;
|
|
2543
|
+
}
|
|
2544
|
+
playbackOwners.set(key, ownerId);
|
|
2545
|
+
return true;
|
|
2546
|
+
}
|
|
2547
|
+
function hasTourPlaybackOwnership(key, ownerId) {
|
|
2548
|
+
return playbackOwners.get(key) === ownerId;
|
|
2549
|
+
}
|
|
2550
|
+
function releaseTourPlaybackOwnership(key, ownerId) {
|
|
2551
|
+
if (playbackOwners.get(key) === ownerId) {
|
|
2552
|
+
playbackOwners.delete(key);
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
|
|
2511
2556
|
// src/hooks/useTourPlayback.ts
|
|
2512
2557
|
function resolveElement(step) {
|
|
2513
2558
|
const el = step.element;
|
|
@@ -3041,6 +3086,9 @@ function useTourPlayback({
|
|
|
3041
3086
|
const showCaptionsRef = useRef8(showCaptions);
|
|
3042
3087
|
const runIdRef = useRef8(null);
|
|
3043
3088
|
const turnIdRef = useRef8(null);
|
|
3089
|
+
const startRequestedRef = useRef8(false);
|
|
3090
|
+
const playbackOwnerIdRef = useRef8(`playback-owner-${Math.random().toString(36).slice(2, 10)}`);
|
|
3091
|
+
const claimedPlaybackOwnerKeyRef = useRef8(null);
|
|
3044
3092
|
const socketRef = useRef8(null);
|
|
3045
3093
|
const socketIdRef = useRef8(socketId);
|
|
3046
3094
|
const commandUrlRef = useRef8(commandUrl);
|
|
@@ -3058,6 +3106,12 @@ function useTourPlayback({
|
|
|
3058
3106
|
toursApiBaseRef.current = toursApiBase;
|
|
3059
3107
|
pendingTourRef.current = pendingTour;
|
|
3060
3108
|
showCaptionsRef.current = showCaptions;
|
|
3109
|
+
const releasePlaybackOwnership = useCallback7(() => {
|
|
3110
|
+
const claimedKey = claimedPlaybackOwnerKeyRef.current;
|
|
3111
|
+
if (!claimedKey) return;
|
|
3112
|
+
releaseTourPlaybackOwnership(claimedKey, playbackOwnerIdRef.current);
|
|
3113
|
+
claimedPlaybackOwnerKeyRef.current = null;
|
|
3114
|
+
}, []);
|
|
3061
3115
|
useEffect11(() => {
|
|
3062
3116
|
if (disabled) return;
|
|
3063
3117
|
if (typeof window === "undefined") return;
|
|
@@ -3093,6 +3147,12 @@ function useTourPlayback({
|
|
|
3093
3147
|
}
|
|
3094
3148
|
};
|
|
3095
3149
|
const handleCommand = async (payload) => {
|
|
3150
|
+
const ownerKey = createTourPlaybackOwnerKey(serverUrl, websiteIdRef.current, experienceTypeRef.current);
|
|
3151
|
+
if (!shouldExecuteTourCommandBatch(isActiveRef.current) || !hasTourPlaybackOwnership(ownerKey, playbackOwnerIdRef.current)) {
|
|
3152
|
+
const activeType = experienceTypeRef.current;
|
|
3153
|
+
console.log("[TourClient] Ignoring command batch for inactive playback hook:", activeType, payload.stepIndex);
|
|
3154
|
+
return;
|
|
3155
|
+
}
|
|
3096
3156
|
const emitIfOpen = (ev, data) => {
|
|
3097
3157
|
if (socketRef.current !== socket) return;
|
|
3098
3158
|
emitSocketEvent(socket, ev, data);
|
|
@@ -3598,7 +3658,14 @@ function useTourPlayback({
|
|
|
3598
3658
|
console.log(`[TourClient] Ignoring ${tour.type} start (this hook is for ${expType})`);
|
|
3599
3659
|
return;
|
|
3600
3660
|
}
|
|
3661
|
+
const ownerKey = createTourPlaybackOwnerKey(serverUrl, websiteIdRef.current, expType);
|
|
3662
|
+
if (!claimTourPlaybackOwnership(ownerKey, playbackOwnerIdRef.current)) {
|
|
3663
|
+
console.log(`[TourClient] Ignoring ${expType} start because another hook already owns playback`);
|
|
3664
|
+
return;
|
|
3665
|
+
}
|
|
3666
|
+
claimedPlaybackOwnerKeyRef.current = ownerKey;
|
|
3601
3667
|
skipRequestedRef.current = false;
|
|
3668
|
+
startRequestedRef.current = false;
|
|
3602
3669
|
const total = tourData.totalSteps ?? tour?.steps?.length ?? 0;
|
|
3603
3670
|
isActiveRef.current = true;
|
|
3604
3671
|
setIsActive(true);
|
|
@@ -3657,6 +3724,16 @@ function useTourPlayback({
|
|
|
3657
3724
|
};
|
|
3658
3725
|
const handleDebugLog = (entry) => {
|
|
3659
3726
|
const isDev = devModeRef.current || process.env.NODE_ENV === "development" || typeof window !== "undefined" && window.MODELNEX_DEBUG;
|
|
3727
|
+
const entryTourType = entry?.data?.tourContext?.type ?? tourRef.current?.type ?? null;
|
|
3728
|
+
const ownerKey = createTourPlaybackOwnerKey(serverUrl, websiteIdRef.current, experienceTypeRef.current);
|
|
3729
|
+
if (!shouldLogTourDebugEntry({
|
|
3730
|
+
isPlaybackActive: isActiveRef.current,
|
|
3731
|
+
entryType: entry?.type,
|
|
3732
|
+
entryTourType,
|
|
3733
|
+
experienceType: experienceTypeRef.current
|
|
3734
|
+
}) || isActiveRef.current && !hasTourPlaybackOwnership(ownerKey, playbackOwnerIdRef.current)) {
|
|
3735
|
+
return;
|
|
3736
|
+
}
|
|
3660
3737
|
if (isDev) {
|
|
3661
3738
|
console.log(`%c[ModelNex Tour] ${entry.type}`, "color: #3b82f6; font-weight: bold", entry);
|
|
3662
3739
|
if (typeof window !== "undefined") {
|
|
@@ -3689,9 +3766,10 @@ function useTourPlayback({
|
|
|
3689
3766
|
setServerState(null);
|
|
3690
3767
|
runIdRef.current = null;
|
|
3691
3768
|
turnIdRef.current = null;
|
|
3769
|
+
releasePlaybackOwnership();
|
|
3692
3770
|
tourSocketPool.release(serverUrl, toClose);
|
|
3693
3771
|
};
|
|
3694
|
-
}, [serverUrl, disabled]);
|
|
3772
|
+
}, [serverUrl, disabled, releasePlaybackOwnership]);
|
|
3695
3773
|
useEffect11(() => {
|
|
3696
3774
|
if (disabled) return;
|
|
3697
3775
|
const s = socketRef.current;
|
|
@@ -3762,6 +3840,8 @@ function useTourPlayback({
|
|
|
3762
3840
|
const stopTour = useCallback7(() => {
|
|
3763
3841
|
skipRequestedRef.current = true;
|
|
3764
3842
|
isActiveRef.current = false;
|
|
3843
|
+
startRequestedRef.current = false;
|
|
3844
|
+
releasePlaybackOwnership();
|
|
3765
3845
|
activeExecutionTokenRef.current += 1;
|
|
3766
3846
|
commandInFlightRef.current = false;
|
|
3767
3847
|
activeCommandBatchIdRef.current = null;
|
|
@@ -3797,12 +3877,14 @@ function useTourPlayback({
|
|
|
3797
3877
|
setIsReviewMode(false);
|
|
3798
3878
|
pendingInputBufRef.current = null;
|
|
3799
3879
|
onTourEnd?.();
|
|
3800
|
-
}, [voice, onTourEnd, serverUrl, websiteId]);
|
|
3880
|
+
}, [voice, onTourEnd, serverUrl, websiteId, releasePlaybackOwnership]);
|
|
3801
3881
|
const handleTourEnd = useCallback7(() => {
|
|
3802
3882
|
const endingTourId = tourRef.current?.id;
|
|
3803
3883
|
const endingPreviewRunId = previewRunIdRef.current;
|
|
3804
3884
|
const endingStepOrder = stepIndexRef.current;
|
|
3805
3885
|
isActiveRef.current = false;
|
|
3886
|
+
startRequestedRef.current = false;
|
|
3887
|
+
releasePlaybackOwnership();
|
|
3806
3888
|
setPlaybackState("complete");
|
|
3807
3889
|
removeHighlight();
|
|
3808
3890
|
removeCaption();
|
|
@@ -3834,8 +3916,15 @@ function useTourPlayback({
|
|
|
3834
3916
|
clearActiveDraftPreview(experienceType);
|
|
3835
3917
|
}
|
|
3836
3918
|
onTourEnd?.();
|
|
3837
|
-
}, [experienceType, userProfile, serverUrl, voice, onTourEnd, websiteId]);
|
|
3919
|
+
}, [experienceType, userProfile, serverUrl, voice, onTourEnd, websiteId, releasePlaybackOwnership]);
|
|
3838
3920
|
const runTour = useCallback7(async (tour, options) => {
|
|
3921
|
+
if (!shouldAcceptTourStart({
|
|
3922
|
+
isPlaybackActive: isActiveRef.current,
|
|
3923
|
+
startRequested: startRequestedRef.current
|
|
3924
|
+
})) {
|
|
3925
|
+
console.log("[TourClient] Ignoring duplicate start request while playback is already active or starting:", tour.id);
|
|
3926
|
+
return;
|
|
3927
|
+
}
|
|
3839
3928
|
setPendingTour(null);
|
|
3840
3929
|
pendingTourRef.current = null;
|
|
3841
3930
|
let retries = 0;
|
|
@@ -3847,6 +3936,13 @@ function useTourPlayback({
|
|
|
3847
3936
|
console.warn("[TourClient] Cannot run tour, socket not connected.");
|
|
3848
3937
|
return;
|
|
3849
3938
|
}
|
|
3939
|
+
const ownerKey = createTourPlaybackOwnerKey(serverUrl, websiteIdRef.current, tour.type ?? experienceTypeRef.current);
|
|
3940
|
+
if (!claimTourPlaybackOwnership(ownerKey, playbackOwnerIdRef.current)) {
|
|
3941
|
+
console.log("[TourClient] Ignoring duplicate start request because another hook already owns this experience:", tour.id);
|
|
3942
|
+
return;
|
|
3943
|
+
}
|
|
3944
|
+
claimedPlaybackOwnerKeyRef.current = ownerKey;
|
|
3945
|
+
startRequestedRef.current = true;
|
|
3850
3946
|
const shouldReview = Boolean(options?.reviewMode);
|
|
3851
3947
|
resetCaptionSuppression();
|
|
3852
3948
|
setReviewStatusMessage(null);
|
|
@@ -3878,8 +3974,13 @@ function useTourPlayback({
|
|
|
3878
3974
|
});
|
|
3879
3975
|
}, [serverUrl, websiteId]);
|
|
3880
3976
|
useEffect11(() => {
|
|
3881
|
-
if (!
|
|
3882
|
-
|
|
3977
|
+
if (!shouldRunTourAutoDiscovery({
|
|
3978
|
+
enableAutoDiscovery,
|
|
3979
|
+
disabled,
|
|
3980
|
+
isPlaybackActive: isActiveRef.current,
|
|
3981
|
+
startRequested: startRequestedRef.current,
|
|
3982
|
+
hasPendingTour: Boolean(pendingTourRef.current)
|
|
3983
|
+
})) return;
|
|
3883
3984
|
if (typeof window === "undefined") return;
|
|
3884
3985
|
const params = new URLSearchParams(window.location.search);
|
|
3885
3986
|
const queryParam = experienceType === "onboarding" ? "modelnex_test_workflow" : "modelnex_test_tour";
|
|
@@ -3927,8 +4028,13 @@ function useTourPlayback({
|
|
|
3927
4028
|
};
|
|
3928
4029
|
}, [serverUrl, toursApiBase, disabled, websiteId, experienceType, enableAutoDiscovery]);
|
|
3929
4030
|
useEffect11(() => {
|
|
3930
|
-
if (!
|
|
3931
|
-
|
|
4031
|
+
if (!shouldRunTourAutoDiscovery({
|
|
4032
|
+
enableAutoDiscovery,
|
|
4033
|
+
disabled,
|
|
4034
|
+
isPlaybackActive: isActiveRef.current,
|
|
4035
|
+
startRequested: startRequestedRef.current,
|
|
4036
|
+
hasPendingTour: Boolean(pendingTourRef.current)
|
|
4037
|
+
})) return;
|
|
3932
4038
|
if (!websiteId || !userProfile) return;
|
|
3933
4039
|
if (typeof window !== "undefined") {
|
|
3934
4040
|
const params = new URLSearchParams(window.location.search);
|
|
@@ -3967,7 +4073,7 @@ function useTourPlayback({
|
|
|
3967
4073
|
cancelled = true;
|
|
3968
4074
|
clearTimeout(timer);
|
|
3969
4075
|
};
|
|
3970
|
-
}, [websiteId, serverUrl, toursApiBase, disabled, experienceType, userProfile, enableAutoDiscovery]);
|
|
4076
|
+
}, [websiteId, serverUrl, toursApiBase, disabled, experienceType, userProfile?.userId, userProfile?.type, userProfile?.isNewUser, enableAutoDiscovery]);
|
|
3971
4077
|
useEffect11(() => {
|
|
3972
4078
|
if (!disabled || !isActiveRef.current) return;
|
|
3973
4079
|
stopTour();
|