@eshal-bot/chat-widget 0.1.47 → 0.1.49
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/README.md +99 -99
- package/dist/chat-widget.esm.js +2 -2
- package/dist/chat-widget.esm.js.map +1 -0
- package/dist/chat-widget.js +293 -139
- package/dist/chat-widget.js.map +1 -0
- package/dist/chat-widget.min.js +2 -2
- package/dist/chat-widget.umd.js +2 -2
- package/dist/chat-widget.umd.js.map +1 -0
- package/package.json +2 -2
package/dist/chat-widget.js
CHANGED
|
@@ -8454,9 +8454,9 @@
|
|
|
8454
8454
|
}
|
|
8455
8455
|
};
|
|
8456
8456
|
|
|
8457
|
-
/**
|
|
8458
|
-
* Initializes the PCM audio player worklet that streams inline PCM chunks
|
|
8459
|
-
* coming from the ADK websocket events.
|
|
8457
|
+
/**
|
|
8458
|
+
* Initializes the PCM audio player worklet that streams inline PCM chunks
|
|
8459
|
+
* coming from the ADK websocket events.
|
|
8460
8460
|
*/
|
|
8461
8461
|
async function startAudioPlayerWorklet() {
|
|
8462
8462
|
if (typeof window === 'undefined') {
|
|
@@ -8480,9 +8480,9 @@
|
|
|
8480
8480
|
return [audioPlayerNode, audioContext];
|
|
8481
8481
|
}
|
|
8482
8482
|
|
|
8483
|
-
/**
|
|
8484
|
-
* Bootstraps the PCM recorder audio worklet, captures mic audio at 16kHz
|
|
8485
|
-
* mono, and forwards buffers through the provided handler.
|
|
8483
|
+
/**
|
|
8484
|
+
* Bootstraps the PCM recorder audio worklet, captures mic audio at 16kHz
|
|
8485
|
+
* mono, and forwards buffers through the provided handler.
|
|
8486
8486
|
*/
|
|
8487
8487
|
async function startAudioRecorderWorklet(onPcmChunk) {
|
|
8488
8488
|
if (typeof window === 'undefined') {
|
|
@@ -8519,8 +8519,8 @@
|
|
|
8519
8519
|
return [audioRecorderNode, audioRecorderContext, micStream];
|
|
8520
8520
|
}
|
|
8521
8521
|
|
|
8522
|
-
/**
|
|
8523
|
-
* Stops all microphone tracks.
|
|
8522
|
+
/**
|
|
8523
|
+
* Stops all microphone tracks.
|
|
8524
8524
|
*/
|
|
8525
8525
|
function stopMicrophone(stream) {
|
|
8526
8526
|
if (!stream) {
|
|
@@ -8537,24 +8537,24 @@
|
|
|
8537
8537
|
return pcm16Array.buffer;
|
|
8538
8538
|
}
|
|
8539
8539
|
|
|
8540
|
-
/**
|
|
8541
|
-
* Client-side Voice-Activity gate for the widget's voice (Bidi) session.
|
|
8542
|
-
*
|
|
8543
|
-
* Ported from packages/agent-sdk/src/voice/audio/vad.ts (`createSpeechGate`),
|
|
8544
|
-
* which was itself originally derived from this widget. The gate sits between
|
|
8545
|
-
* the mic worklet and the WebSocket: it forwards PCM ONLY inside a confirmed
|
|
8546
|
-
* speech turn and fires `onSpeechEnd` when the turn closes, so the caller can
|
|
8547
|
-
* tell the backend the utterance is done (an `audio_end` signal).
|
|
8548
|
-
*
|
|
8549
|
-
* Why the widget needs this: knowledge-api's non-streaming voice path only
|
|
8550
|
-
* transcribes a turn when it receives `audio_end` (or its flaky energy-VAD
|
|
8551
|
-
* backstop trips). Without a client gate the widget streamed mic audio forever
|
|
8552
|
-
* and the backend just buffered it ("Collecting Audio…") without ever
|
|
8553
|
-
* transcribing — so voice connected but never replied. The agent-sdk already
|
|
8554
|
-
* gates this way; this brings the widget to parity.
|
|
8555
|
-
*
|
|
8556
|
-
* Framework-free and side-effect-free (only calls the two callbacks) so it can
|
|
8557
|
-
* be reasoned about / tested in isolation.
|
|
8540
|
+
/**
|
|
8541
|
+
* Client-side Voice-Activity gate for the widget's voice (Bidi) session.
|
|
8542
|
+
*
|
|
8543
|
+
* Ported from packages/agent-sdk/src/voice/audio/vad.ts (`createSpeechGate`),
|
|
8544
|
+
* which was itself originally derived from this widget. The gate sits between
|
|
8545
|
+
* the mic worklet and the WebSocket: it forwards PCM ONLY inside a confirmed
|
|
8546
|
+
* speech turn and fires `onSpeechEnd` when the turn closes, so the caller can
|
|
8547
|
+
* tell the backend the utterance is done (an `audio_end` signal).
|
|
8548
|
+
*
|
|
8549
|
+
* Why the widget needs this: knowledge-api's non-streaming voice path only
|
|
8550
|
+
* transcribes a turn when it receives `audio_end` (or its flaky energy-VAD
|
|
8551
|
+
* backstop trips). Without a client gate the widget streamed mic audio forever
|
|
8552
|
+
* and the backend just buffered it ("Collecting Audio…") without ever
|
|
8553
|
+
* transcribing — so voice connected but never replied. The agent-sdk already
|
|
8554
|
+
* gates this way; this brings the widget to parity.
|
|
8555
|
+
*
|
|
8556
|
+
* Framework-free and side-effect-free (only calls the two callbacks) so it can
|
|
8557
|
+
* be reasoned about / tested in isolation.
|
|
8558
8558
|
*/
|
|
8559
8559
|
|
|
8560
8560
|
const DEFAULT_SPEECH_GATE_CONFIG = {
|
|
@@ -8583,16 +8583,16 @@
|
|
|
8583
8583
|
return Math.sqrt(sumSquares / samples.length);
|
|
8584
8584
|
}
|
|
8585
8585
|
|
|
8586
|
-
/**
|
|
8587
|
-
* Create a stateful speech gate. Returns `{ process, reset, isSpeaking }`.
|
|
8588
|
-
* `config.sampleRate` is required; everything else falls back to
|
|
8589
|
-
* DEFAULT_SPEECH_GATE_CONFIG.
|
|
8590
|
-
*
|
|
8591
|
-
* @param {{ sampleRate: number, energyThreshold?: number, speechStartMs?: number, hangoverMs?: number, preRollMs?: number }} config
|
|
8592
|
-
* @param {{ onSpeechStart?: () => void, onVoiceFrame: (pcm: ArrayBuffer) => void, onSpeechEnd: () => void }} callbacks
|
|
8593
|
-
* onSpeechStart fires the instant a turn opens — use it for barge-in
|
|
8594
|
-
* (stop any assistant TTS still playing). onVoiceFrame emits each voiced
|
|
8595
|
-
* PCM frame; onSpeechEnd fires when the turn closes after the hangover.
|
|
8586
|
+
/**
|
|
8587
|
+
* Create a stateful speech gate. Returns `{ process, reset, isSpeaking }`.
|
|
8588
|
+
* `config.sampleRate` is required; everything else falls back to
|
|
8589
|
+
* DEFAULT_SPEECH_GATE_CONFIG.
|
|
8590
|
+
*
|
|
8591
|
+
* @param {{ sampleRate: number, energyThreshold?: number, speechStartMs?: number, hangoverMs?: number, preRollMs?: number }} config
|
|
8592
|
+
* @param {{ onSpeechStart?: () => void, onVoiceFrame: (pcm: ArrayBuffer) => void, onSpeechEnd: () => void }} callbacks
|
|
8593
|
+
* onSpeechStart fires the instant a turn opens — use it for barge-in
|
|
8594
|
+
* (stop any assistant TTS still playing). onVoiceFrame emits each voiced
|
|
8595
|
+
* PCM frame; onSpeechEnd fires when the turn closes after the hangover.
|
|
8596
8596
|
*/
|
|
8597
8597
|
function createSpeechGate(config, callbacks) {
|
|
8598
8598
|
var _config$energyThresho, _config$speechStartMs, _config$hangoverMs, _config$preRollMs;
|
|
@@ -9288,6 +9288,14 @@
|
|
|
9288
9288
|
} = await Promise.resolve().then(function () { return index; });
|
|
9289
9289
|
const tracer = new UserTrace({
|
|
9290
9290
|
cache: true,
|
|
9291
|
+
// Capture the visitor's precise location via the browser Geolocation
|
|
9292
|
+
// API (behind the native permission prompt) and reverse-geocode it
|
|
9293
|
+
// into profile.geo, so "Traffic by location" reflects the real city
|
|
9294
|
+
// instead of the coarse/wrong IP-gateway city. Granted → accurate
|
|
9295
|
+
// GPS; denied/dismissed/plain-HTTP → graceful IP fallback. Set
|
|
9296
|
+
// explicitly (not relying on the SDK default) so the behavior is
|
|
9297
|
+
// durable even if @eshal-tracker/sdk changes its default later.
|
|
9298
|
+
geoFromBrowser: true,
|
|
9291
9299
|
onError: () => {/* swallow — fingerprint is best-effort */}
|
|
9292
9300
|
});
|
|
9293
9301
|
const profile = await tracer.init();
|
|
@@ -58861,88 +58869,6 @@
|
|
|
58861
58869
|
});
|
|
58862
58870
|
};
|
|
58863
58871
|
|
|
58864
|
-
const LOGO_MAX_HEIGHT = "3.5rem";
|
|
58865
|
-
const LOGO_MAX_HEIGHT_COMPACT = "2.5rem";
|
|
58866
|
-
const LOGO_MAX_WIDTH_DEFAULT = "4.5rem";
|
|
58867
|
-
const LOGO_MAX_WIDTH_DEFAULT_COMPACT = "3rem";
|
|
58868
|
-
const LOGO_MAX_WIDTH_WIDE = "9rem";
|
|
58869
|
-
const LOGO_MAX_WIDTH_WIDE_COMPACT = "5.5rem";
|
|
58870
|
-
const STATUS_DOT_SIZE = "0.8rem";
|
|
58871
|
-
const STATUS_DOT_SIZE_COMPACT = "0.65rem";
|
|
58872
|
-
const STATUS_DOT_BORDER_WIDTH = "2px";
|
|
58873
|
-
const STATUS_DOT_BORDER_WIDTH_COMPACT = "1.5px";
|
|
58874
|
-
const STATUS_DOT_DEFAULT_COLOR = "#34d399";
|
|
58875
|
-
const STATUS_DOT_OFFSET = "38%";
|
|
58876
|
-
const STATUS_DOT_OFFSET_COMPACT = "22%";
|
|
58877
|
-
const LogoImage = _ref => {
|
|
58878
|
-
let {
|
|
58879
|
-
src,
|
|
58880
|
-
showStatusDot = false,
|
|
58881
|
-
statusColor = STATUS_DOT_DEFAULT_COLOR,
|
|
58882
|
-
size = "default"
|
|
58883
|
-
} = _ref;
|
|
58884
|
-
const [aspect, setAspect] = reactExports.useState("square");
|
|
58885
|
-
const isCompact = size === "compact";
|
|
58886
|
-
reactExports.useEffect(() => {
|
|
58887
|
-
if (typeof window === "undefined" || !src) {
|
|
58888
|
-
return;
|
|
58889
|
-
}
|
|
58890
|
-
const img = new Image();
|
|
58891
|
-
img.src = src;
|
|
58892
|
-
img.onload = () => {
|
|
58893
|
-
if (img.width > img.height) {
|
|
58894
|
-
setAspect("wide");
|
|
58895
|
-
} else if (img.height > img.width) {
|
|
58896
|
-
setAspect("tall");
|
|
58897
|
-
} else {
|
|
58898
|
-
setAspect("square");
|
|
58899
|
-
}
|
|
58900
|
-
};
|
|
58901
|
-
}, [src]);
|
|
58902
|
-
const containerStyle = {
|
|
58903
|
-
position: "relative",
|
|
58904
|
-
display: "inline-flex"
|
|
58905
|
-
};
|
|
58906
|
-
const imageStyle = {
|
|
58907
|
-
display: "block",
|
|
58908
|
-
objectFit: "cover",
|
|
58909
|
-
height: "100%",
|
|
58910
|
-
width: "100%",
|
|
58911
|
-
// borderRadius:'50%',
|
|
58912
|
-
maxHeight: isCompact ? LOGO_MAX_HEIGHT_COMPACT : LOGO_MAX_HEIGHT,
|
|
58913
|
-
maxWidth: isCompact ? LOGO_MAX_WIDTH_DEFAULT_COMPACT : LOGO_MAX_WIDTH_DEFAULT
|
|
58914
|
-
};
|
|
58915
|
-
if (aspect === "wide") {
|
|
58916
|
-
imageStyle.maxWidth = isCompact ? LOGO_MAX_WIDTH_WIDE_COMPACT : LOGO_MAX_WIDTH_WIDE;
|
|
58917
|
-
}
|
|
58918
|
-
if (aspect === "tall") {
|
|
58919
|
-
imageStyle.maxHeight = isCompact ? "3rem" : "4rem";
|
|
58920
|
-
imageStyle.maxWidth = isCompact ? "3rem" : "4rem";
|
|
58921
|
-
}
|
|
58922
|
-
return /*#__PURE__*/jsxRuntimeExports.jsxs("div", {
|
|
58923
|
-
style: containerStyle,
|
|
58924
|
-
children: [/*#__PURE__*/jsxRuntimeExports.jsx("img", {
|
|
58925
|
-
src: src,
|
|
58926
|
-
alt: "Logo",
|
|
58927
|
-
style: imageStyle,
|
|
58928
|
-
loading: "lazy"
|
|
58929
|
-
}), showStatusDot && /*#__PURE__*/jsxRuntimeExports.jsx("span", {
|
|
58930
|
-
style: {
|
|
58931
|
-
position: "absolute",
|
|
58932
|
-
bottom: 0,
|
|
58933
|
-
right: 0,
|
|
58934
|
-
width: isCompact ? STATUS_DOT_SIZE_COMPACT : STATUS_DOT_SIZE,
|
|
58935
|
-
height: isCompact ? STATUS_DOT_SIZE_COMPACT : STATUS_DOT_SIZE,
|
|
58936
|
-
borderRadius: "9999px",
|
|
58937
|
-
backgroundColor: statusColor,
|
|
58938
|
-
border: "".concat(isCompact ? STATUS_DOT_BORDER_WIDTH_COMPACT : STATUS_DOT_BORDER_WIDTH, " solid #ffffff"),
|
|
58939
|
-
transform: "translate(".concat(isCompact ? STATUS_DOT_OFFSET_COMPACT : STATUS_DOT_OFFSET, ", ").concat(isCompact ? STATUS_DOT_OFFSET_COMPACT : STATUS_DOT_OFFSET, ")"),
|
|
58940
|
-
pointerEvents: "none"
|
|
58941
|
-
}
|
|
58942
|
-
})]
|
|
58943
|
-
});
|
|
58944
|
-
};
|
|
58945
|
-
|
|
58946
58872
|
const ChatHeader = _ref => {
|
|
58947
58873
|
let {
|
|
58948
58874
|
conciergeName,
|
|
@@ -59001,8 +58927,16 @@
|
|
|
59001
58927
|
className: "absolute inset-0 bg-gradient-to-br from-white/10 to-transparent pointer-events-none"
|
|
59002
58928
|
}), companyLogo && /*#__PURE__*/jsxRuntimeExports.jsx("div", {
|
|
59003
58929
|
className: "w-[48px] h-[48px] rounded-full overflow-hidden flex items-center justify-center bg-white/10",
|
|
59004
|
-
children: /*#__PURE__*/jsxRuntimeExports.jsx(
|
|
59005
|
-
src: companyLogo
|
|
58930
|
+
children: /*#__PURE__*/jsxRuntimeExports.jsx("img", {
|
|
58931
|
+
src: companyLogo,
|
|
58932
|
+
alt: "Logo",
|
|
58933
|
+
style: {
|
|
58934
|
+
display: "block",
|
|
58935
|
+
width: "100%",
|
|
58936
|
+
height: "100%",
|
|
58937
|
+
objectFit: "cover"
|
|
58938
|
+
},
|
|
58939
|
+
loading: "lazy"
|
|
59006
58940
|
})
|
|
59007
58941
|
}), /*#__PURE__*/jsxRuntimeExports.jsx("div", {
|
|
59008
58942
|
className: "flex items-center gap-2 ".concat(!companyLogo ? "px-3" : ""),
|
|
@@ -59479,8 +59413,8 @@
|
|
|
59479
59413
|
});
|
|
59480
59414
|
};
|
|
59481
59415
|
|
|
59482
|
-
/**
|
|
59483
|
-
* Auto-scroll hook that scrolls to bottom when dependencies change
|
|
59416
|
+
/**
|
|
59417
|
+
* Auto-scroll hook that scrolls to bottom when dependencies change
|
|
59484
59418
|
*/
|
|
59485
59419
|
function useAutoScroll(containerRef) {
|
|
59486
59420
|
let dependencies = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : [];
|
|
@@ -61939,6 +61873,16 @@
|
|
|
61939
61873
|
})
|
|
61940
61874
|
}];
|
|
61941
61875
|
|
|
61876
|
+
// Reorder the keyless cascade: freeipapi.com is currently the only one
|
|
61877
|
+
// reliably reachable from a browser — ip-api.com's free tier is HTTP-only
|
|
61878
|
+
// (→ HTTP 403 over HTTPS), ipwho.is → 403, ipapi.co → 429. Trying freeipapi
|
|
61879
|
+
// first avoids three doomed round-trips on every lookup. The rest stay as
|
|
61880
|
+
// fallback in case they recover or behave differently from another network.
|
|
61881
|
+
// NOTE: all IP providers locate the ISP gateway, not the device — for an
|
|
61882
|
+
// accurate end-user position use getBrowserLocation() (GPS + reverseGeocode).
|
|
61883
|
+
const FREE_ORDER = ["freeipapi.com", "ipwho.is", "ipapi.co", "ip-api.com"];
|
|
61884
|
+
freeProviders.sort((a, b) => FREE_ORDER.indexOf(a.name) - FREE_ORDER.indexOf(b.name));
|
|
61885
|
+
|
|
61942
61886
|
// Key-based providers go first (more reliable, higher limits)
|
|
61943
61887
|
return [...keyedProviders, ...freeProviders];
|
|
61944
61888
|
}
|
|
@@ -62036,22 +61980,130 @@
|
|
|
62036
61980
|
}));
|
|
62037
61981
|
}
|
|
62038
61982
|
|
|
61983
|
+
// Default options for navigator.geolocation.getCurrentPosition.
|
|
61984
|
+
// enableHighAccuracy → prefer real GPS over coarse network positioning;
|
|
61985
|
+
// timeout 8s → don't hang forever waiting for a fix;
|
|
61986
|
+
// maximumAge 5 min → accept a recently-cached fix so a silent refresh
|
|
61987
|
+
// (permission already granted) resolves instantly.
|
|
61988
|
+
const DEFAULT_GEO_OPTIONS = {
|
|
61989
|
+
enableHighAccuracy: true,
|
|
61990
|
+
timeout: 8000,
|
|
61991
|
+
maximumAge: 5 * 60 * 1000
|
|
61992
|
+
};
|
|
61993
|
+
|
|
62039
61994
|
/**
|
|
62040
|
-
* Get browser-native geolocation (GPS / network-based
|
|
61995
|
+
* Get browser-native geolocation (GPS / network-based).
|
|
61996
|
+
* Triggers the native permission prompt on first use for an origin (unless the
|
|
61997
|
+
* user already granted/denied). Resolves null — never rejects — on error,
|
|
61998
|
+
* denial, unsupported, or insecure (plain-HTTP) context, so callers can simply
|
|
61999
|
+
* fall back to IP geo.
|
|
62000
|
+
*
|
|
62001
|
+
* @param {PositionOptions} [options] - merged over DEFAULT_GEO_OPTIONS
|
|
62002
|
+
* @returns {Promise<{latitude:number, longitude:number, accuracy:number}|null>}
|
|
62041
62003
|
*/
|
|
62042
|
-
function getBrowserGeolocation$1(options = {
|
|
62043
|
-
timeout: 8000
|
|
62044
|
-
}) {
|
|
62004
|
+
function getBrowserGeolocation$1(options = {}) {
|
|
62045
62005
|
return new Promise(resolve => {
|
|
62046
|
-
if (!navigator
|
|
62006
|
+
if (typeof navigator === "undefined" || !navigator.geolocation) return resolve(null);
|
|
62007
|
+
// getCurrentPosition only resolves in a secure context (HTTPS or
|
|
62008
|
+
// localhost). Bail early on plain HTTP rather than attempt a doomed prompt.
|
|
62009
|
+
if (typeof window !== "undefined" && window.isSecureContext === false) return resolve(null);
|
|
62047
62010
|
navigator.geolocation.getCurrentPosition(pos => resolve({
|
|
62048
62011
|
latitude: pos.coords.latitude,
|
|
62049
62012
|
longitude: pos.coords.longitude,
|
|
62050
62013
|
accuracy: pos.coords.accuracy
|
|
62051
|
-
}), () => resolve(null),
|
|
62014
|
+
}), () => resolve(null), {
|
|
62015
|
+
...DEFAULT_GEO_OPTIONS,
|
|
62016
|
+
...options
|
|
62017
|
+
});
|
|
62052
62018
|
});
|
|
62053
62019
|
}
|
|
62054
62020
|
|
|
62021
|
+
/**
|
|
62022
|
+
* Query the current geolocation permission WITHOUT triggering a prompt.
|
|
62023
|
+
* Lets the host decide whether to ask: "granted" → fetch silently, "denied" →
|
|
62024
|
+
* skip (IP fallback), "prompt" → show your own pre-ask before getCurrentPosition.
|
|
62025
|
+
* Returns "unsupported" where the Permissions API is unavailable (e.g. older
|
|
62026
|
+
* Safari) so callers can treat it as "not known to be granted".
|
|
62027
|
+
* @returns {Promise<"granted"|"denied"|"prompt"|"unsupported">}
|
|
62028
|
+
*/
|
|
62029
|
+
async function getGeolocationPermission$1() {
|
|
62030
|
+
try {
|
|
62031
|
+
if (typeof navigator === "undefined" || !navigator.permissions || !navigator.permissions.query) {
|
|
62032
|
+
return "unsupported";
|
|
62033
|
+
}
|
|
62034
|
+
const status = await navigator.permissions.query({
|
|
62035
|
+
name: "geolocation"
|
|
62036
|
+
});
|
|
62037
|
+
return status.state; // "granted" | "denied" | "prompt"
|
|
62038
|
+
} catch {
|
|
62039
|
+
return "unsupported";
|
|
62040
|
+
}
|
|
62041
|
+
}
|
|
62042
|
+
|
|
62043
|
+
/**
|
|
62044
|
+
* Reverse-geocode latitude/longitude → city / region / country.
|
|
62045
|
+
* Uses BigDataCloud's keyless, CORS-enabled *client* endpoint (no API key,
|
|
62046
|
+
* accurate to the city/state level). Resolves null on network failure or an
|
|
62047
|
+
* unusable response so the caller can keep any prior (IP-based) values.
|
|
62048
|
+
*
|
|
62049
|
+
* @param {number} latitude
|
|
62050
|
+
* @param {number} longitude
|
|
62051
|
+
* @param {Object} [options]
|
|
62052
|
+
* @param {number} [options.timeoutMs]
|
|
62053
|
+
* @param {string} [options.localityLanguage="en"]
|
|
62054
|
+
* @returns {Promise<{city,region,regionCode,country,countryCode,latitude,longitude}|null>}
|
|
62055
|
+
*/
|
|
62056
|
+
async function reverseGeocode$1(latitude, longitude, options = {}) {
|
|
62057
|
+
if (typeof latitude !== "number" || typeof longitude !== "number") return null;
|
|
62058
|
+
const timeoutMs = options.timeoutMs || _globalConfig.timeoutMs || 5000;
|
|
62059
|
+
const lang = options.localityLanguage || "en";
|
|
62060
|
+
const url = "https://api.bigdatacloud.net/data/reverse-geocode-client" + `?latitude=${latitude}&longitude=${longitude}&localityLanguage=${lang}`;
|
|
62061
|
+
try {
|
|
62062
|
+
const d = await fetchWithTimeout(url, timeoutMs);
|
|
62063
|
+
const city = d.city || d.locality || "";
|
|
62064
|
+
// Treat an empty body (e.g. mid-ocean coords) as a miss, not a result.
|
|
62065
|
+
if (!city && !d.countryName) return null;
|
|
62066
|
+
return {
|
|
62067
|
+
city,
|
|
62068
|
+
region: d.principalSubdivision || "",
|
|
62069
|
+
regionCode: (d.principalSubdivisionCode || "").split("-").pop() || "",
|
|
62070
|
+
country: d.countryName || "",
|
|
62071
|
+
countryCode: d.countryCode || "",
|
|
62072
|
+
latitude,
|
|
62073
|
+
longitude
|
|
62074
|
+
};
|
|
62075
|
+
} catch {
|
|
62076
|
+
return null;
|
|
62077
|
+
}
|
|
62078
|
+
}
|
|
62079
|
+
|
|
62080
|
+
/**
|
|
62081
|
+
* High-level "exact location" helper. Gets the device GPS fix (prompting if
|
|
62082
|
+
* needed) and reverse-geocodes it. Returns a geo-shaped object tagged
|
|
62083
|
+
* source:"gps" when the place resolved; the bare coords (no source tag) when
|
|
62084
|
+
* only the fix succeeded but reverse-geocoding failed; or null when there is no
|
|
62085
|
+
* fix at all (denied / unsupported / insecure). This is the accurate-location
|
|
62086
|
+
* primitive — prefer it over IP geo for the real end-user position.
|
|
62087
|
+
*
|
|
62088
|
+
* @param {PositionOptions} [options]
|
|
62089
|
+
* @returns {Promise<Object|null>}
|
|
62090
|
+
*/
|
|
62091
|
+
async function getBrowserLocation$1(options = {}) {
|
|
62092
|
+
const coords = await getBrowserGeolocation$1(options);
|
|
62093
|
+
if (!coords) return null;
|
|
62094
|
+
const place = await reverseGeocode$1(coords.latitude, coords.longitude, options);
|
|
62095
|
+
const hasPlace = !!(place && (place.city || place.country));
|
|
62096
|
+
return {
|
|
62097
|
+
...(hasPlace ? place : {}),
|
|
62098
|
+
latitude: coords.latitude,
|
|
62099
|
+
longitude: coords.longitude,
|
|
62100
|
+
accuracy: coords.accuracy,
|
|
62101
|
+
...(hasPlace ? {
|
|
62102
|
+
source: "gps"
|
|
62103
|
+
} : {})
|
|
62104
|
+
};
|
|
62105
|
+
}
|
|
62106
|
+
|
|
62055
62107
|
/** Build a Google Maps URL from coordinates */
|
|
62056
62108
|
function toMapsUrl$1(lat, lon) {
|
|
62057
62109
|
return `https://www.google.com/maps?q=${lat},${lon}`;
|
|
@@ -62061,6 +62113,9 @@
|
|
|
62061
62113
|
resolveLocation: resolveLocation$1,
|
|
62062
62114
|
listProviders: listProviders$1,
|
|
62063
62115
|
getBrowserGeolocation: getBrowserGeolocation$1,
|
|
62116
|
+
getGeolocationPermission: getGeolocationPermission$1,
|
|
62117
|
+
reverseGeocode: reverseGeocode$1,
|
|
62118
|
+
getBrowserLocation: getBrowserLocation$1,
|
|
62064
62119
|
toMapsUrl: toMapsUrl$1
|
|
62065
62120
|
};
|
|
62066
62121
|
|
|
@@ -62244,6 +62299,9 @@
|
|
|
62244
62299
|
resolveLocation,
|
|
62245
62300
|
listProviders,
|
|
62246
62301
|
getBrowserGeolocation,
|
|
62302
|
+
getGeolocationPermission,
|
|
62303
|
+
reverseGeocode,
|
|
62304
|
+
getBrowserLocation,
|
|
62247
62305
|
toMapsUrl
|
|
62248
62306
|
} = location;
|
|
62249
62307
|
const {
|
|
@@ -62276,7 +62334,13 @@
|
|
|
62276
62334
|
* @param {Object} [options]
|
|
62277
62335
|
* @param {Function} [options.onReady] - Callback when profile is ready
|
|
62278
62336
|
* @param {Function} [options.onError] - Callback on error
|
|
62279
|
-
* @param {boolean}
|
|
62337
|
+
* @param {boolean|"auto"} [options.geoFromBrowser=true] - Capture precise GPS
|
|
62338
|
+
* and reverse-geocode it into `geo`. DEFAULT true = request on init()
|
|
62339
|
+
* (shows the native permission prompt unless already granted/denied);
|
|
62340
|
+
* false = never (IP only); "auto" = use GPS only when permission was
|
|
62341
|
+
* ALREADY granted (silent, never prompts).
|
|
62342
|
+
* @param {Object} [options.geoApiKeys] - API keys for keyed geo providers
|
|
62343
|
+
* @param {PositionOptions} [options.geoOptions] - Override getCurrentPosition opts
|
|
62280
62344
|
* @param {boolean} [options.cache=true] - Use cached profile if < maxAgeMs old
|
|
62281
62345
|
* @param {number} [options.maxAgeMs=300000] - Max cache age (default 5 min)
|
|
62282
62346
|
* @param {boolean} [options.autoInit=false] - Auto-init on construction
|
|
@@ -62286,7 +62350,13 @@
|
|
|
62286
62350
|
this._options = {
|
|
62287
62351
|
onReady: options.onReady || null,
|
|
62288
62352
|
onError: options.onError || null,
|
|
62289
|
-
|
|
62353
|
+
// Default ON: capture precise GPS on init() behind the browser's native
|
|
62354
|
+
// permission prompt. Use `=== undefined` (not `|| `) so an explicit
|
|
62355
|
+
// `false` opt-out is honored; pass "auto" to use an already-granted
|
|
62356
|
+
// permission silently without ever prompting.
|
|
62357
|
+
geoFromBrowser: options.geoFromBrowser === undefined ? true : options.geoFromBrowser,
|
|
62358
|
+
geoApiKeys: options.geoApiKeys || null,
|
|
62359
|
+
geoOptions: options.geoOptions || null,
|
|
62290
62360
|
cache: options.cache !== false,
|
|
62291
62361
|
maxAgeMs: options.maxAgeMs || 5 * 60 * 1000,
|
|
62292
62362
|
autoInit: options.autoInit || false,
|
|
@@ -62336,15 +62406,46 @@
|
|
|
62336
62406
|
confidence
|
|
62337
62407
|
} = generateFingerprint();
|
|
62338
62408
|
|
|
62339
|
-
// 3. IP
|
|
62340
|
-
|
|
62341
|
-
|
|
62342
|
-
const
|
|
62343
|
-
|
|
62344
|
-
|
|
62345
|
-
|
|
62346
|
-
|
|
62347
|
-
|
|
62409
|
+
// 3. IP→geo chain and (optional) precise browser GPS run concurrently.
|
|
62410
|
+
// GPS is the slow path (permission prompt + satellite fix) so we never
|
|
62411
|
+
// block the IP lookup on it.
|
|
62412
|
+
const ipApiKeys = this._options.geoApiKeys ? {
|
|
62413
|
+
apiKeys: this._options.geoApiKeys
|
|
62414
|
+
} : {};
|
|
62415
|
+
const [ipBundle, gpsLoc] = await Promise.all([(async () => {
|
|
62416
|
+
const ipResult = await detectIP();
|
|
62417
|
+
const ipAddr = ipResult?.ip || "unknown";
|
|
62418
|
+
const ipGeo = ipAddr !== "unknown" ? await resolveLocation(ipAddr, ipApiKeys) : null;
|
|
62419
|
+
return {
|
|
62420
|
+
ip: ipAddr,
|
|
62421
|
+
ipVersion: ipResult ? getIPVersion(ipAddr) : "unknown",
|
|
62422
|
+
geo: ipGeo
|
|
62423
|
+
};
|
|
62424
|
+
})(), this._collectBrowserLocation()]);
|
|
62425
|
+
const {
|
|
62426
|
+
ip,
|
|
62427
|
+
ipVersion
|
|
62428
|
+
} = ipBundle;
|
|
62429
|
+
|
|
62430
|
+
// 4. Geo: start from the (coarse) IP location, then — when we have a
|
|
62431
|
+
// precise GPS fix — OVERRIDE city/region/country with the
|
|
62432
|
+
// reverse-geocoded values. IP geo only locates the ISP gateway, so a
|
|
62433
|
+
// real device fix always wins. Denied/unavailable → IP value stays.
|
|
62434
|
+
let geo = ipBundle.geo;
|
|
62435
|
+
let browserGeo = null;
|
|
62436
|
+
if (gpsLoc) {
|
|
62437
|
+
browserGeo = {
|
|
62438
|
+
latitude: gpsLoc.latitude,
|
|
62439
|
+
longitude: gpsLoc.longitude,
|
|
62440
|
+
accuracy: gpsLoc.accuracy
|
|
62441
|
+
};
|
|
62442
|
+
if (gpsLoc.source === "gps") {
|
|
62443
|
+
// gpsLoc carries city/region/regionCode/country/countryCode/lat/lng/source
|
|
62444
|
+
geo = {
|
|
62445
|
+
...(geo || {}),
|
|
62446
|
+
...gpsLoc
|
|
62447
|
+
};
|
|
62448
|
+
}
|
|
62348
62449
|
}
|
|
62349
62450
|
|
|
62350
62451
|
// 5. Browser context
|
|
@@ -62386,6 +62487,56 @@
|
|
|
62386
62487
|
}
|
|
62387
62488
|
}
|
|
62388
62489
|
|
|
62490
|
+
/**
|
|
62491
|
+
* Decide whether to capture browser GPS during init(), honoring geoFromBrowser:
|
|
62492
|
+
* false → null (skip)
|
|
62493
|
+
* "auto" → only when permission is ALREADY granted (silent, never prompts)
|
|
62494
|
+
* true → attempt (getCurrentPosition shows the prompt unless denied)
|
|
62495
|
+
* @private
|
|
62496
|
+
* @returns {Promise<Object|null>}
|
|
62497
|
+
*/
|
|
62498
|
+
async _collectBrowserLocation() {
|
|
62499
|
+
const mode = this._options.geoFromBrowser;
|
|
62500
|
+
if (!mode) return null;
|
|
62501
|
+
if (mode === "auto") {
|
|
62502
|
+
const perm = await getGeolocationPermission();
|
|
62503
|
+
if (perm !== "granted") return null; // never prompt during a silent init
|
|
62504
|
+
}
|
|
62505
|
+
return getBrowserLocation(this._options.geoOptions || {});
|
|
62506
|
+
}
|
|
62507
|
+
|
|
62508
|
+
/**
|
|
62509
|
+
* Capture precise browser location AFTER your UI has obtained user consent
|
|
62510
|
+
* (e.g. an in-widget "Share your location?" pre-ask). Triggers the native
|
|
62511
|
+
* permission prompt, reverse-geocodes the fix, merges it into the current
|
|
62512
|
+
* profile's `geo` (source:"gps"), re-persists, and fires onReady. No-ops
|
|
62513
|
+
* gracefully when denied/unavailable (keeps the IP fallback) or before init().
|
|
62514
|
+
* @param {PositionOptions} [options]
|
|
62515
|
+
* @returns {Promise<UserProfile|null>}
|
|
62516
|
+
*/
|
|
62517
|
+
async captureBrowserLocation(options = {}) {
|
|
62518
|
+
const loc = await getBrowserLocation({
|
|
62519
|
+
...(this._options.geoOptions || {}),
|
|
62520
|
+
...options
|
|
62521
|
+
});
|
|
62522
|
+
if (!loc || !this.profile) return this.profile; // denied/unavailable or not init'd yet
|
|
62523
|
+
this.profile.browserGeo = {
|
|
62524
|
+
latitude: loc.latitude,
|
|
62525
|
+
longitude: loc.longitude,
|
|
62526
|
+
accuracy: loc.accuracy
|
|
62527
|
+
};
|
|
62528
|
+
this.profile.mapsUrl = toMapsUrl(loc.latitude, loc.longitude);
|
|
62529
|
+
if (loc.source === "gps") {
|
|
62530
|
+
this.profile.geo = {
|
|
62531
|
+
...(this.profile.geo || {}),
|
|
62532
|
+
...loc
|
|
62533
|
+
};
|
|
62534
|
+
}
|
|
62535
|
+
saveSessionMeta(this.profile);
|
|
62536
|
+
if (this._options.onReady) this._options.onReady(this.profile);
|
|
62537
|
+
return this.profile;
|
|
62538
|
+
}
|
|
62539
|
+
|
|
62389
62540
|
/**
|
|
62390
62541
|
* Get the current profile (must call init() first)
|
|
62391
62542
|
* @returns {UserProfile|null}
|
|
@@ -62480,6 +62631,9 @@
|
|
|
62480
62631
|
src.exports.resolveLocation = resolveLocation;
|
|
62481
62632
|
src.exports.listProviders = listProviders;
|
|
62482
62633
|
src.exports.getBrowserGeolocation = getBrowserGeolocation;
|
|
62634
|
+
src.exports.getGeolocationPermission = getGeolocationPermission;
|
|
62635
|
+
src.exports.reverseGeocode = reverseGeocode;
|
|
62636
|
+
src.exports.getBrowserLocation = getBrowserLocation;
|
|
62483
62637
|
src.exports.getOrCreateUserID = getOrCreateUserID;
|
|
62484
62638
|
src.exports.getOrCreateSessionID = getOrCreateSessionID;
|
|
62485
62639
|
src.exports.clearSession = clearSession;
|