@solhun/feedback-kit-core 0.4.0 → 0.6.0
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.cjs +623 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +332 -4
- package/dist/index.d.ts +332 -4
- package/dist/index.js +603 -9
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -33,6 +33,16 @@ __export(index_exports, {
|
|
|
33
33
|
FLOATING_BUTTON_ID: () => FLOATING_BUTTON_ID,
|
|
34
34
|
FeedbackQueue: () => FeedbackQueue,
|
|
35
35
|
FocusRing: () => FocusRing,
|
|
36
|
+
HINT_DISPLAY_DEFAULT: () => HINT_DISPLAY_DEFAULT,
|
|
37
|
+
HINT_DISPLAY_MAX: () => HINT_DISPLAY_MAX,
|
|
38
|
+
HINT_DISPLAY_MIN: () => HINT_DISPLAY_MIN,
|
|
39
|
+
HINT_DRAFT_MAX_CHARS: () => HINT_DRAFT_MAX_CHARS,
|
|
40
|
+
HINT_FAIL_COOLDOWN_MS: () => HINT_FAIL_COOLDOWN_MS,
|
|
41
|
+
HINT_LABEL_MAX_CHARS: () => HINT_LABEL_MAX_CHARS,
|
|
42
|
+
HINT_MAX_ITEMS: () => HINT_MAX_ITEMS,
|
|
43
|
+
HINT_TIMEOUT_MS: () => HINT_TIMEOUT_MS,
|
|
44
|
+
HINT_TTL_MS: () => HINT_TTL_MS,
|
|
45
|
+
HintProvider: () => HintProvider,
|
|
36
46
|
KEEPALIVE_BODY_LIMIT_BYTES: () => KEEPALIVE_BODY_LIMIT_BYTES,
|
|
37
47
|
LASSO_DEFAULT_ENDPOINT: () => LASSO_DEFAULT_ENDPOINT,
|
|
38
48
|
LOG_BUFFER_LIMIT: () => LOG_BUFFER_LIMIT,
|
|
@@ -41,6 +51,7 @@ __export(index_exports, {
|
|
|
41
51
|
MAX_QUEUE_ITEMS: () => MAX_QUEUE_ITEMS,
|
|
42
52
|
MODAL_ACTION_ATTACH: () => MODAL_ACTION_ATTACH,
|
|
43
53
|
MODAL_ACTION_CANCEL: () => MODAL_ACTION_CANCEL,
|
|
54
|
+
MODAL_ACTION_HINT_TOGGLE: () => MODAL_ACTION_HINT_TOGGLE,
|
|
44
55
|
MODAL_ACTION_PICK: () => MODAL_ACTION_PICK,
|
|
45
56
|
MODAL_ACTION_PIN: () => MODAL_ACTION_PIN,
|
|
46
57
|
MODAL_ACTION_REMOVE_SCREENSHOT: () => MODAL_ACTION_REMOVE_SCREENSHOT,
|
|
@@ -54,6 +65,9 @@ __export(index_exports, {
|
|
|
54
65
|
REDACTED: () => REDACTED,
|
|
55
66
|
ReportModalController: () => ReportModalController,
|
|
56
67
|
RingBuffer: () => RingBuffer,
|
|
68
|
+
SCORE_ELEMENT: () => SCORE_ELEMENT,
|
|
69
|
+
SCORE_PATH_EXACT: () => SCORE_PATH_EXACT,
|
|
70
|
+
SCORE_PATH_PATTERN: () => SCORE_PATH_PATTERN,
|
|
57
71
|
SCREENSHOT_FAILED_MESSAGE: () => SCREENSHOT_FAILED_MESSAGE,
|
|
58
72
|
SCREENSHOT_MAX_BASE64_BYTES: () => SCREENSHOT_MAX_BASE64_BYTES,
|
|
59
73
|
SCREENSHOT_QUALITY_STEPS: () => SCREENSHOT_QUALITY_STEPS,
|
|
@@ -63,6 +77,7 @@ __export(index_exports, {
|
|
|
63
77
|
SUBMIT_PENDING_MESSAGE: () => SUBMIT_PENDING_MESSAGE,
|
|
64
78
|
TITLE_MAX_CHARS: () => TITLE_MAX_CHARS,
|
|
65
79
|
WidgetController: () => WidgetController,
|
|
80
|
+
appPathFromNav: () => appPathFromNav,
|
|
66
81
|
buildContext: () => buildContext,
|
|
67
82
|
buildLassoContext: () => buildLassoContext,
|
|
68
83
|
buildLassoEnvelope: () => buildLassoEnvelope,
|
|
@@ -80,10 +95,15 @@ __export(index_exports, {
|
|
|
80
95
|
diagnosticsProviderFor: () => diagnosticsProviderFor,
|
|
81
96
|
getOrCreateGuestId: () => getOrCreateGuestId,
|
|
82
97
|
guestUser: () => guestUser,
|
|
98
|
+
hintCharCount: () => hintCharCount,
|
|
83
99
|
isInternalUser: () => isInternalUser,
|
|
100
|
+
matchElement: () => matchElement,
|
|
101
|
+
matchPath: () => matchPath,
|
|
84
102
|
normalizePin: () => normalizePin,
|
|
85
103
|
normalizeUser: () => normalizeUser,
|
|
104
|
+
parseHintCatalog: () => parseHintCatalog,
|
|
86
105
|
parseSourceAttr: () => parseSourceAttr,
|
|
106
|
+
rankHints: () => rankHints,
|
|
87
107
|
renderReportBody: () => renderReportBody,
|
|
88
108
|
resolveConfig: () => resolveConfig,
|
|
89
109
|
resolveContextUser: () => resolveContextUser,
|
|
@@ -314,7 +334,9 @@ async function buildReport(parts, opts) {
|
|
|
314
334
|
pin: parts.pin,
|
|
315
335
|
element: parts.element,
|
|
316
336
|
context,
|
|
317
|
-
createdAt: iso(now())
|
|
337
|
+
createdAt: iso(now()),
|
|
338
|
+
// 생략과 "정말 아무 것도 안 눌렀음"을 굳이 구분하지 않는다 — 둘 다 빈 배열로 싣는다.
|
|
339
|
+
hintIds: parts.hintIds ?? []
|
|
318
340
|
};
|
|
319
341
|
}
|
|
320
342
|
|
|
@@ -1437,6 +1459,7 @@ var COMMENT_TOO_LONG_MESSAGE = "4,000\uC790 \uC774\uD558\uB85C \uC785\uB825\uD57
|
|
|
1437
1459
|
var SUBMIT_DONE_MESSAGE = "\uBCF4\uB0C8\uC2B5\uB2C8\uB2E4";
|
|
1438
1460
|
var SUBMIT_PENDING_MESSAGE = "\uB300\uAE30 \uC911";
|
|
1439
1461
|
var SUBMIT_FAILED_MESSAGE = "\uBCF4\uB0B4\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4";
|
|
1462
|
+
var MODAL_ACTION_HINT_TOGGLE = "hint-toggle";
|
|
1440
1463
|
var MODAL_FIELD_COMMENT = "comment";
|
|
1441
1464
|
var MODAL_FIELD_PRIORITY = "priority";
|
|
1442
1465
|
var MODAL_ACTION_ATTACH = "attach";
|
|
@@ -1458,6 +1481,12 @@ var ReportModalController = class {
|
|
|
1458
1481
|
this.captureGeneration = 0;
|
|
1459
1482
|
/** 지금 들고 있는 핀의 기준. 핀이 없으면 의미 없다(기본 `picture`). */
|
|
1460
1483
|
this.pinAnchor = "picture";
|
|
1484
|
+
/**
|
|
1485
|
+
* 사용자가 [제안 ▾] 를 직접 건드렸는가(방향 무관). 한 번이라도 건드리면 이후
|
|
1486
|
+
* 타이핑에 의한 자동 접힘/펼침이 멈춘다 — "사람이 편 상태는 모달이 닫힐 때까지 유지"를
|
|
1487
|
+
* 이렇게 구현한다. `open()` 에서 매번 리셋된다.
|
|
1488
|
+
*/
|
|
1489
|
+
this.userToggledHints = false;
|
|
1461
1490
|
this.state = {
|
|
1462
1491
|
open: false,
|
|
1463
1492
|
element: null,
|
|
@@ -1478,7 +1507,11 @@ var ReportModalController = class {
|
|
|
1478
1507
|
actions: [],
|
|
1479
1508
|
focusOrder: [],
|
|
1480
1509
|
focused: null,
|
|
1481
|
-
restoreFocusTo: null
|
|
1510
|
+
restoreFocusTo: null,
|
|
1511
|
+
hints: [],
|
|
1512
|
+
hintsExpanded: false,
|
|
1513
|
+
hintsAvailable: false,
|
|
1514
|
+
usedHintIds: []
|
|
1482
1515
|
};
|
|
1483
1516
|
this.queue = opts.queue;
|
|
1484
1517
|
this.createReport = opts.createReport;
|
|
@@ -1538,9 +1571,11 @@ var ReportModalController = class {
|
|
|
1538
1571
|
this.touched = false;
|
|
1539
1572
|
this.submitAttempted = false;
|
|
1540
1573
|
this.pinAnchor = opts.pin && !pendingReport ? "viewport" : "picture";
|
|
1574
|
+
this.userToggledHints = false;
|
|
1575
|
+
const resolvedComment = pendingReport?.comment ?? "";
|
|
1541
1576
|
this.patch({
|
|
1542
1577
|
open: true,
|
|
1543
|
-
comment:
|
|
1578
|
+
comment: resolvedComment,
|
|
1544
1579
|
// 요소를 지목해서 연 경우엔 그 요소를 물고 간다. 대기 항목 복원이 우선이다.
|
|
1545
1580
|
element: pendingReport?.element ?? opts.element ?? null,
|
|
1546
1581
|
priority: pendingReport?.priority ?? "unset",
|
|
@@ -1551,7 +1586,10 @@ var ReportModalController = class {
|
|
|
1551
1586
|
submitStatus: wasPending ? "pending" : "idle",
|
|
1552
1587
|
submitMessage: wasPending ? SUBMIT_PENDING_MESSAGE : null,
|
|
1553
1588
|
closeConfirmVisible: false,
|
|
1554
|
-
restoreFocusTo: null
|
|
1589
|
+
restoreFocusTo: null,
|
|
1590
|
+
// 코멘트가 비어 있을 때만 펼쳐진 채로 연다. 대기 항목을 복원해도 규칙은 같다(계약 원문).
|
|
1591
|
+
hintsExpanded: resolvedComment === "",
|
|
1592
|
+
usedHintIds: []
|
|
1555
1593
|
});
|
|
1556
1594
|
this.ring.focus(MODAL_FIELD_COMMENT);
|
|
1557
1595
|
this.patch({ focused: this.ring.current });
|
|
@@ -1610,7 +1648,22 @@ var ReportModalController = class {
|
|
|
1610
1648
|
setComment(value) {
|
|
1611
1649
|
if (this.isDraftLocked()) return;
|
|
1612
1650
|
this.touched = true;
|
|
1613
|
-
this.patch({ comment: value });
|
|
1651
|
+
this.patch({ comment: value, ...this.hintsPatchFor(value) });
|
|
1652
|
+
}
|
|
1653
|
+
/**
|
|
1654
|
+
* 코멘트가 비었는지에 따라 칩 줄 상태를 갱신하는 조각을 만든다. `setComment`·`applyHint`
|
|
1655
|
+
* 양쪽에서 쓴다.
|
|
1656
|
+
*
|
|
1657
|
+
* - 완전히 비워지면(trim 기준) 눌린 칩 기록을 버리고 다시 펼친다 — "지우고 처음부터 다시
|
|
1658
|
+
* 쓴 글"에 이전 제안을 귀속시키지 않기 위해서다. 사용자가 직접 편 상태여도 예외 없다.
|
|
1659
|
+
* - 비어 있지 않게 되면, 사용자가 직접 편 게 아닌 이상 접는다(좁은 화면에서 입력란을
|
|
1660
|
+
* 밀어내지 않으려고). 직접 편 상태(`userToggledHints`)면 손대지 않는다.
|
|
1661
|
+
*/
|
|
1662
|
+
hintsPatchFor(nextComment) {
|
|
1663
|
+
if (nextComment.trim().length === 0) {
|
|
1664
|
+
return { hintsExpanded: true, usedHintIds: [] };
|
|
1665
|
+
}
|
|
1666
|
+
return this.userToggledHints ? {} : { hintsExpanded: false };
|
|
1614
1667
|
}
|
|
1615
1668
|
setPriority(priority) {
|
|
1616
1669
|
if (this.isDraftLocked()) return;
|
|
@@ -1683,6 +1736,41 @@ var ReportModalController = class {
|
|
|
1683
1736
|
...dropPin
|
|
1684
1737
|
});
|
|
1685
1738
|
}
|
|
1739
|
+
// ── 제안 힌트 ─────────────────────────────────────────────────────────────
|
|
1740
|
+
/**
|
|
1741
|
+
* 컨트롤러가 랭킹 결과를 밀어넣는다. **모달이 열려 있는 동안에는 무시한다** — 지목 대상을
|
|
1742
|
+
* 바꿔 새로 열기 직전에 호출되는 흐름이 정상이고, 열려 있는 모달의 칩을 도중에 바꾸면
|
|
1743
|
+
* 누르려던 칩이 손가락 밑에서 바뀐다(캐시 백그라운드 갱신도 이 경로로 들어올 수 있다).
|
|
1744
|
+
*/
|
|
1745
|
+
setHints(hints) {
|
|
1746
|
+
if (this.state.open) return;
|
|
1747
|
+
this.patch({ hints });
|
|
1748
|
+
}
|
|
1749
|
+
/**
|
|
1750
|
+
* 칩 누름. 이미 눌린 칩이면 아무 일도 하지 않는다(같은 문구가 두 번 이어붙는 걸 막는다).
|
|
1751
|
+
* 코멘트가 비어 있으면 초안으로 치환, 아니면 줄바꿈 뒤 이어붙인다 — 어느 쪽도 사용자가
|
|
1752
|
+
* 쓴 글을 지우지 않는다.
|
|
1753
|
+
*/
|
|
1754
|
+
applyHint(hintId) {
|
|
1755
|
+
if (this.isDraftLocked()) return;
|
|
1756
|
+
if (this.state.usedHintIds.includes(hintId)) return;
|
|
1757
|
+
const hint = this.state.hints.find((h) => h.id === hintId);
|
|
1758
|
+
if (!hint) return;
|
|
1759
|
+
this.touched = true;
|
|
1760
|
+
const comment = this.state.comment.length === 0 ? hint.draft : `${this.state.comment}
|
|
1761
|
+
${hint.draft}`;
|
|
1762
|
+
this.patch({
|
|
1763
|
+
comment,
|
|
1764
|
+
usedHintIds: [...this.state.usedHintIds, hintId],
|
|
1765
|
+
// 방금 채워 넣었으니 비어 있지 않다 — setComment 와 같은 접힘 규칙을 그대로 쓴다.
|
|
1766
|
+
...this.hintsPatchFor(comment)
|
|
1767
|
+
});
|
|
1768
|
+
}
|
|
1769
|
+
/** [제안 ▾] 토글. 방향과 무관하게 이후 자동 접힘/펼침을 멈춘다(`userToggledHints`). */
|
|
1770
|
+
toggleHints() {
|
|
1771
|
+
this.userToggledHints = true;
|
|
1772
|
+
this.patch({ hintsExpanded: !this.state.hintsExpanded });
|
|
1773
|
+
}
|
|
1686
1774
|
// ── 포커스 ────────────────────────────────────────────────────────────────
|
|
1687
1775
|
tabNext() {
|
|
1688
1776
|
const next = this.ring.next();
|
|
@@ -1720,7 +1808,9 @@ var ReportModalController = class {
|
|
|
1720
1808
|
priority: this.state.priority,
|
|
1721
1809
|
screenshot: this.state.screenshot,
|
|
1722
1810
|
pin: this.state.pin,
|
|
1723
|
-
element: this.state.element
|
|
1811
|
+
element: this.state.element,
|
|
1812
|
+
// 눌린 칩 기록. 문구를 고쳐 보내도 "이 제안을 눌렀다"는 사실은 그대로 남는다.
|
|
1813
|
+
hintIds: this.state.usedHintIds
|
|
1724
1814
|
};
|
|
1725
1815
|
let outcome;
|
|
1726
1816
|
try {
|
|
@@ -1786,8 +1876,15 @@ var ReportModalController = class {
|
|
|
1786
1876
|
actionsFor() {
|
|
1787
1877
|
return this.platform === "web" ? [MODAL_ACTION_PICK] : [MODAL_ACTION_PIN];
|
|
1788
1878
|
}
|
|
1789
|
-
focusOrderFor() {
|
|
1790
|
-
const order = [
|
|
1879
|
+
focusOrderFor(hintsAvailable) {
|
|
1880
|
+
const order = [];
|
|
1881
|
+
if (hintsAvailable) {
|
|
1882
|
+
if (this.state.hintsExpanded) {
|
|
1883
|
+
for (const hint of this.state.hints) order.push(`hint:${hint.id}`);
|
|
1884
|
+
}
|
|
1885
|
+
order.push(MODAL_ACTION_HINT_TOGGLE);
|
|
1886
|
+
}
|
|
1887
|
+
order.push(MODAL_FIELD_COMMENT, MODAL_FIELD_PRIORITY);
|
|
1791
1888
|
if (this.state.screenshot) order.push(MODAL_ACTION_REMOVE_SCREENSHOT);
|
|
1792
1889
|
else order.push(MODAL_ACTION_ATTACH);
|
|
1793
1890
|
order.push(...this.actionsFor());
|
|
@@ -1800,7 +1897,8 @@ var ReportModalController = class {
|
|
|
1800
1897
|
const canSubmit = this.isCommentValid() && this.state.screenshotStatus !== "capturing" && (this.state.submitStatus === "idle" || this.state.submitStatus === "failed");
|
|
1801
1898
|
const canPin = this.platform === "native" && this.state.screenshot !== null && this.state.screenshotStatus !== "capturing" && !this.isDraftLocked() && this.state.open;
|
|
1802
1899
|
const actions = this.actionsFor();
|
|
1803
|
-
const
|
|
1900
|
+
const hintsAvailable = this.state.hints.length > 0;
|
|
1901
|
+
const focusOrder = this.state.open ? this.focusOrderFor(hintsAvailable) : [];
|
|
1804
1902
|
this.ring.setItems(focusOrder);
|
|
1805
1903
|
this.state = {
|
|
1806
1904
|
...this.state,
|
|
@@ -1809,6 +1907,7 @@ var ReportModalController = class {
|
|
|
1809
1907
|
canPin,
|
|
1810
1908
|
showRetry: this.state.submitStatus === "pending" || this.state.pending > 0,
|
|
1811
1909
|
actions,
|
|
1910
|
+
hintsAvailable,
|
|
1812
1911
|
focusOrder,
|
|
1813
1912
|
focused: this.state.open ? this.ring.current : null
|
|
1814
1913
|
};
|
|
@@ -2199,6 +2298,13 @@ function absoluteEndpoint(endpoint) {
|
|
|
2199
2298
|
if (typeof origin !== "string" || origin.length === 0) return null;
|
|
2200
2299
|
return endpoint.startsWith("/") ? `${origin}${endpoint}` : `${origin}/${endpoint}`;
|
|
2201
2300
|
}
|
|
2301
|
+
function hintsCacheKeyFrom(endpoint) {
|
|
2302
|
+
const path = endpoint.split(/[?#]/)[0] ?? "";
|
|
2303
|
+
const segments = path.split("/").filter((s) => s.length > 0);
|
|
2304
|
+
const last = segments.at(-1);
|
|
2305
|
+
if (!last || last === "verify") return void 0;
|
|
2306
|
+
return last;
|
|
2307
|
+
}
|
|
2202
2308
|
function parseRetryAfterMs(res) {
|
|
2203
2309
|
const raw = res.headers?.get("Retry-After") ?? null;
|
|
2204
2310
|
if (raw === null) return null;
|
|
@@ -2263,6 +2369,8 @@ function buildLassoContext(report) {
|
|
|
2263
2369
|
} : null,
|
|
2264
2370
|
// 속성이 하나도 없으면 빈 객체를 보내지 않는다 — 화면에 빈 항목이 늘어난다.
|
|
2265
2371
|
element: el && (el.id !== null || Object.keys(el.attributes).length > 0) ? { id: el.id, attributes: el.attributes } : null,
|
|
2372
|
+
// 생략과 "정말 하나도 안 눌렀음"을 구분하지 않는다 — 둘 다 빈 배열.
|
|
2373
|
+
hintIds: [...report.hintIds ?? []],
|
|
2266
2374
|
native: c.native ? {
|
|
2267
2375
|
screenPath: c.native.screenPath,
|
|
2268
2376
|
navPath: c.native.navPath,
|
|
@@ -2322,6 +2430,7 @@ function createLassoAdapter(opts = {}) {
|
|
|
2322
2430
|
const token = opts.token ?? null;
|
|
2323
2431
|
const endpoint = opts.endpoint && opts.endpoint.trim().length > 0 ? opts.endpoint.trim() : LASSO_DEFAULT_ENDPOINT;
|
|
2324
2432
|
const warn = opts.warn ?? null;
|
|
2433
|
+
const hintsCacheKey = hintsCacheKeyFrom(endpoint);
|
|
2325
2434
|
const emit = (message) => {
|
|
2326
2435
|
if (!warn) return;
|
|
2327
2436
|
try {
|
|
@@ -2330,6 +2439,7 @@ function createLassoAdapter(opts = {}) {
|
|
|
2330
2439
|
}
|
|
2331
2440
|
};
|
|
2332
2441
|
return {
|
|
2442
|
+
hintsCacheKey,
|
|
2333
2443
|
async submit(report) {
|
|
2334
2444
|
const url = absoluteEndpoint(endpoint);
|
|
2335
2445
|
if (url === null) {
|
|
@@ -2400,7 +2510,491 @@ function createLassoAdapter(opts = {}) {
|
|
|
2400
2510
|
return fail(false);
|
|
2401
2511
|
}
|
|
2402
2512
|
return { ok: true, id: idFrom(parsed), retryable: false, retryAfterMs: null };
|
|
2513
|
+
},
|
|
2514
|
+
/**
|
|
2515
|
+
* 힌트 카탈로그 조회. `submit` 과 같은 절대 규칙이 그대로 적용된다 — **던지지 않는다**,
|
|
2516
|
+
* **본문·헤더를 로그에 남기지 않는다.** 다만 여기는 `warn` 을 아예 부르지 않는다.
|
|
2517
|
+
* "실패하면 조용히 포기"·"개발 빌드에서만 1회 경고"의 판단(쿨다운·환경 분기)은
|
|
2518
|
+
* `HintProvider`(hints/ 소유자) 몫이라, 어댑터가 매 실패마다 경고를 찍으면 그 판단과
|
|
2519
|
+
* 어긋난다.
|
|
2520
|
+
*/
|
|
2521
|
+
async fetchHints(etag) {
|
|
2522
|
+
const url = absoluteEndpoint(`${endpoint.replace(/\/+$/, "")}/hints`);
|
|
2523
|
+
if (url === null) return { kind: "fail" };
|
|
2524
|
+
const sender = resolveFetch(opts.fetch);
|
|
2525
|
+
if (sender === null) return { kind: "fail" };
|
|
2526
|
+
const headers = {};
|
|
2527
|
+
if (token !== null) headers.Authorization = `Bearer ${token}`;
|
|
2528
|
+
if (etag !== null) headers["If-None-Match"] = etag;
|
|
2529
|
+
let res;
|
|
2530
|
+
try {
|
|
2531
|
+
res = await sender(url, { method: "GET", headers });
|
|
2532
|
+
} catch {
|
|
2533
|
+
return { kind: "fail" };
|
|
2534
|
+
}
|
|
2535
|
+
if (res.status === 304) return { kind: "not-modified" };
|
|
2536
|
+
if (res.status === 401 || res.status === 403) {
|
|
2537
|
+
return { kind: "fail", permanent: true };
|
|
2538
|
+
}
|
|
2539
|
+
if (res.status < 200 || res.status >= 300) return { kind: "fail" };
|
|
2540
|
+
let text;
|
|
2541
|
+
try {
|
|
2542
|
+
text = await res.text();
|
|
2543
|
+
} catch {
|
|
2544
|
+
return { kind: "fail" };
|
|
2545
|
+
}
|
|
2546
|
+
try {
|
|
2547
|
+
const raw = JSON.parse(text);
|
|
2548
|
+
return { kind: "ok", raw, etag: res.headers?.get("ETag") ?? null };
|
|
2549
|
+
} catch {
|
|
2550
|
+
return { kind: "fail" };
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
};
|
|
2554
|
+
}
|
|
2555
|
+
|
|
2556
|
+
// src/hints/constants.ts
|
|
2557
|
+
var HINT_LABEL_MAX_CHARS = 12;
|
|
2558
|
+
var HINT_DRAFT_MAX_CHARS = 200;
|
|
2559
|
+
var HINT_DISPLAY_DEFAULT = 4;
|
|
2560
|
+
var HINT_DISPLAY_MIN = 3;
|
|
2561
|
+
var HINT_DISPLAY_MAX = 6;
|
|
2562
|
+
var HINT_MAX_ITEMS = 300;
|
|
2563
|
+
var HINT_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
2564
|
+
var HINT_TIMEOUT_MS = 5e3;
|
|
2565
|
+
var HINT_FAIL_COOLDOWN_MS = 5 * 60 * 1e3;
|
|
2566
|
+
var SCORE_PATH_EXACT = 40;
|
|
2567
|
+
var SCORE_PATH_PATTERN = 25;
|
|
2568
|
+
var SCORE_ELEMENT = 30;
|
|
2569
|
+
|
|
2570
|
+
// src/hints/chars.ts
|
|
2571
|
+
function hintCharCount(value) {
|
|
2572
|
+
return [...value.normalize("NFC")].length;
|
|
2573
|
+
}
|
|
2574
|
+
|
|
2575
|
+
// src/hints/path.ts
|
|
2576
|
+
function appPathFromNav(navPath) {
|
|
2577
|
+
if (navPath === null || navPath === void 0 || navPath.length === 0) return null;
|
|
2578
|
+
return "/" + navPath.join("/");
|
|
2579
|
+
}
|
|
2580
|
+
function matchPath(pattern, path) {
|
|
2581
|
+
const patternSegs = splitSegments(pattern);
|
|
2582
|
+
const pathSegs = splitSegments(path);
|
|
2583
|
+
if (patternSegs === null || pathSegs === null) return null;
|
|
2584
|
+
const hasWildcard = patternSegs.some((seg) => seg === "*" || seg === "**");
|
|
2585
|
+
if (!hasWildcard) {
|
|
2586
|
+
if (patternSegs.length !== pathSegs.length) return null;
|
|
2587
|
+
for (let i = 0; i < patternSegs.length; i++) {
|
|
2588
|
+
if (patternSegs[i] !== pathSegs[i]) return null;
|
|
2589
|
+
}
|
|
2590
|
+
return "exact";
|
|
2591
|
+
}
|
|
2592
|
+
return matchesFrom(patternSegs, 0, pathSegs, 0) ? "pattern" : null;
|
|
2593
|
+
}
|
|
2594
|
+
function splitSegments(value) {
|
|
2595
|
+
if (typeof value !== "string" || value.length === 0) return null;
|
|
2596
|
+
if (!value.startsWith("/")) return null;
|
|
2597
|
+
if (value === "/") return [];
|
|
2598
|
+
const trimmed = value.endsWith("/") ? value.slice(0, -1) : value;
|
|
2599
|
+
const segments = trimmed.split("/").slice(1);
|
|
2600
|
+
if (segments.some((seg) => seg.length === 0)) return null;
|
|
2601
|
+
return segments;
|
|
2602
|
+
}
|
|
2603
|
+
function matchesFrom(pattern, patternIndex, path, pathIndex) {
|
|
2604
|
+
if (patternIndex === pattern.length) return pathIndex === path.length;
|
|
2605
|
+
const seg = pattern[patternIndex];
|
|
2606
|
+
if (seg === "**") {
|
|
2607
|
+
for (let consume = 0; pathIndex + consume <= path.length; consume++) {
|
|
2608
|
+
if (matchesFrom(pattern, patternIndex + 1, path, pathIndex + consume)) return true;
|
|
2609
|
+
}
|
|
2610
|
+
return false;
|
|
2611
|
+
}
|
|
2612
|
+
if (pathIndex >= path.length) return false;
|
|
2613
|
+
if (seg === "*") return matchesFrom(pattern, patternIndex + 1, path, pathIndex + 1);
|
|
2614
|
+
return path[pathIndex] === seg && matchesFrom(pattern, patternIndex + 1, path, pathIndex + 1);
|
|
2615
|
+
}
|
|
2616
|
+
|
|
2617
|
+
// src/hints/element.ts
|
|
2618
|
+
function matchElement(rule, element, components) {
|
|
2619
|
+
if (rule === null) return true;
|
|
2620
|
+
try {
|
|
2621
|
+
const axes = [rule.tags, rule.roles, rule.classIncludes, rule.componentIncludes, rule.testIds];
|
|
2622
|
+
const hasAnyAxis = axes.some((axis) => Array.isArray(axis) && axis.length > 0);
|
|
2623
|
+
if (!hasAnyAxis) return true;
|
|
2624
|
+
if (element === null) return false;
|
|
2625
|
+
return matchesTags(rule.tags, element) || matchesRoles(rule.roles, element) || matchesClassIncludes(rule.classIncludes, element) || matchesComponentIncludes(rule.componentIncludes, components) || matchesTestIds(rule.testIds, element);
|
|
2626
|
+
} catch {
|
|
2627
|
+
return false;
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
function matchesTags(tags, element) {
|
|
2631
|
+
if (!Array.isArray(tags)) return false;
|
|
2632
|
+
const tag = safeLower(element.tag);
|
|
2633
|
+
if (tag === null) return false;
|
|
2634
|
+
return tags.some((t) => typeof t === "string" && t.toLowerCase() === tag);
|
|
2635
|
+
}
|
|
2636
|
+
function matchesRoles(roles, element) {
|
|
2637
|
+
if (!Array.isArray(roles)) return false;
|
|
2638
|
+
const role = safeLower(element.attributes?.role ?? null);
|
|
2639
|
+
const type = safeLower(element.attributes?.type ?? null);
|
|
2640
|
+
return roles.some((r) => {
|
|
2641
|
+
if (typeof r !== "string") return false;
|
|
2642
|
+
const lowered = r.toLowerCase();
|
|
2643
|
+
return lowered === role || lowered === type;
|
|
2644
|
+
});
|
|
2645
|
+
}
|
|
2646
|
+
function matchesClassIncludes(classIncludes, element) {
|
|
2647
|
+
if (!Array.isArray(classIncludes)) return false;
|
|
2648
|
+
const className = safeLower(element.className);
|
|
2649
|
+
if (className === null) return false;
|
|
2650
|
+
return classIncludes.some((c) => typeof c === "string" && className.includes(c.toLowerCase()));
|
|
2651
|
+
}
|
|
2652
|
+
function matchesComponentIncludes(componentIncludes, components) {
|
|
2653
|
+
if (!Array.isArray(componentIncludes)) return false;
|
|
2654
|
+
if (components === null) return false;
|
|
2655
|
+
return componentIncludes.some((c) => typeof c === "string" && components.includes(c));
|
|
2656
|
+
}
|
|
2657
|
+
function matchesTestIds(testIds, element) {
|
|
2658
|
+
if (!Array.isArray(testIds)) return false;
|
|
2659
|
+
const testId = element.attributes?.["data-testid"];
|
|
2660
|
+
if (typeof testId !== "string") return false;
|
|
2661
|
+
return testIds.some((t) => t === testId);
|
|
2662
|
+
}
|
|
2663
|
+
function safeLower(value) {
|
|
2664
|
+
return typeof value === "string" ? value.toLowerCase() : null;
|
|
2665
|
+
}
|
|
2666
|
+
|
|
2667
|
+
// src/hints/parse.ts
|
|
2668
|
+
var SCOPES = ["screen", "element", "both"];
|
|
2669
|
+
var PLATFORMS = ["web", "app"];
|
|
2670
|
+
function parseHintCatalog(raw) {
|
|
2671
|
+
if (typeof raw !== "object" || raw === null) return null;
|
|
2672
|
+
const obj = raw;
|
|
2673
|
+
if (obj.ok !== true) return null;
|
|
2674
|
+
if (!Array.isArray(obj.hints)) return null;
|
|
2675
|
+
const hints = [];
|
|
2676
|
+
for (const item of obj.hints) {
|
|
2677
|
+
if (hints.length >= HINT_MAX_ITEMS) break;
|
|
2678
|
+
const parsed = parseHintItem(item);
|
|
2679
|
+
if (parsed !== null) hints.push(parsed);
|
|
2680
|
+
}
|
|
2681
|
+
return {
|
|
2682
|
+
version: typeof obj.version === "number" && Number.isInteger(obj.version) ? obj.version : null,
|
|
2683
|
+
maxDisplay: parseMaxDisplay(obj.maxDisplay),
|
|
2684
|
+
hints
|
|
2685
|
+
};
|
|
2686
|
+
}
|
|
2687
|
+
function parseMaxDisplay(value) {
|
|
2688
|
+
if (typeof value === "number" && Number.isInteger(value) && value >= HINT_DISPLAY_MIN && value <= HINT_DISPLAY_MAX) {
|
|
2689
|
+
return value;
|
|
2690
|
+
}
|
|
2691
|
+
return HINT_DISPLAY_DEFAULT;
|
|
2692
|
+
}
|
|
2693
|
+
function parseHintItem(item) {
|
|
2694
|
+
if (typeof item !== "object" || item === null) return null;
|
|
2695
|
+
const obj = item;
|
|
2696
|
+
const id = nonEmptyString(obj.id);
|
|
2697
|
+
const label = nonEmptyString(obj.label);
|
|
2698
|
+
const draft = nonEmptyString(obj.draft);
|
|
2699
|
+
if (id === null || label === null || draft === null) return null;
|
|
2700
|
+
if (typeof obj.scope !== "string" || !SCOPES.includes(obj.scope)) return null;
|
|
2701
|
+
const scope = obj.scope;
|
|
2702
|
+
const platforms = parsePlatforms(obj.platforms);
|
|
2703
|
+
if (platforms === null) return null;
|
|
2704
|
+
return {
|
|
2705
|
+
id,
|
|
2706
|
+
label,
|
|
2707
|
+
draft,
|
|
2708
|
+
scope,
|
|
2709
|
+
platforms,
|
|
2710
|
+
paths: parsePaths(obj.paths),
|
|
2711
|
+
element: parseElementRule(obj.element),
|
|
2712
|
+
active: parseActive(obj.active)
|
|
2713
|
+
};
|
|
2714
|
+
}
|
|
2715
|
+
function parseActive(value) {
|
|
2716
|
+
return value !== false;
|
|
2717
|
+
}
|
|
2718
|
+
function parsePlatforms(value) {
|
|
2719
|
+
if (!Array.isArray(value) || value.length === 0) return null;
|
|
2720
|
+
const result = [];
|
|
2721
|
+
for (const p of value) {
|
|
2722
|
+
if (typeof p !== "string" || !PLATFORMS.includes(p)) return null;
|
|
2723
|
+
result.push(p);
|
|
2724
|
+
}
|
|
2725
|
+
return result;
|
|
2726
|
+
}
|
|
2727
|
+
function parsePaths(value) {
|
|
2728
|
+
if (!Array.isArray(value)) return [];
|
|
2729
|
+
return value.filter((p) => typeof p === "string");
|
|
2730
|
+
}
|
|
2731
|
+
function parseElementRule(value) {
|
|
2732
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
2733
|
+
const obj = value;
|
|
2734
|
+
const rule = {};
|
|
2735
|
+
if (Array.isArray(obj.tags)) rule.tags = obj.tags.filter((v) => typeof v === "string");
|
|
2736
|
+
if (Array.isArray(obj.roles)) rule.roles = obj.roles.filter((v) => typeof v === "string");
|
|
2737
|
+
if (Array.isArray(obj.classIncludes)) {
|
|
2738
|
+
rule.classIncludes = obj.classIncludes.filter((v) => typeof v === "string");
|
|
2739
|
+
}
|
|
2740
|
+
if (Array.isArray(obj.componentIncludes)) {
|
|
2741
|
+
rule.componentIncludes = obj.componentIncludes.filter((v) => typeof v === "string");
|
|
2742
|
+
}
|
|
2743
|
+
if (Array.isArray(obj.testIds)) rule.testIds = obj.testIds.filter((v) => typeof v === "string");
|
|
2744
|
+
return rule;
|
|
2745
|
+
}
|
|
2746
|
+
function nonEmptyString(value) {
|
|
2747
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
2748
|
+
}
|
|
2749
|
+
|
|
2750
|
+
// src/hints/rank.ts
|
|
2751
|
+
function rankHints(hints, input, limit) {
|
|
2752
|
+
const scored = [];
|
|
2753
|
+
for (let index = 0; index < hints.length; index++) {
|
|
2754
|
+
const hint = hints[index];
|
|
2755
|
+
if (!passesHardFilter(hint, input)) continue;
|
|
2756
|
+
scored.push({ hint, score: scoreHint(hint, input), index });
|
|
2757
|
+
}
|
|
2758
|
+
const ordered = orderByScoreThenRotatedGroup(scored, input.path);
|
|
2759
|
+
return ordered.slice(0, Math.max(0, limit)).map((s) => s.hint);
|
|
2760
|
+
}
|
|
2761
|
+
function orderByScoreThenRotatedGroup(scored, path) {
|
|
2762
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2763
|
+
for (const s of scored) {
|
|
2764
|
+
const group = groups.get(s.score);
|
|
2765
|
+
if (group) group.push(s);
|
|
2766
|
+
else groups.set(s.score, [s]);
|
|
2767
|
+
}
|
|
2768
|
+
const scoresDesc = [...groups.keys()].sort((a, b) => b - a);
|
|
2769
|
+
const result = [];
|
|
2770
|
+
for (const score of scoresDesc) {
|
|
2771
|
+
result.push(...rotateGroup(groups.get(score), path));
|
|
2772
|
+
}
|
|
2773
|
+
return result;
|
|
2774
|
+
}
|
|
2775
|
+
function rotateGroup(group, path) {
|
|
2776
|
+
if (group.length <= 1 || path === null) return group;
|
|
2777
|
+
const offset = fnv1a32(path) % group.length;
|
|
2778
|
+
if (offset === 0) return group;
|
|
2779
|
+
return [...group.slice(offset), ...group.slice(0, offset)];
|
|
2780
|
+
}
|
|
2781
|
+
function fnv1a32(value) {
|
|
2782
|
+
let hash = 2166136261;
|
|
2783
|
+
for (let i = 0; i < value.length; i++) {
|
|
2784
|
+
hash ^= value.charCodeAt(i);
|
|
2785
|
+
hash = Math.imul(hash, 16777619);
|
|
2786
|
+
}
|
|
2787
|
+
return hash >>> 0;
|
|
2788
|
+
}
|
|
2789
|
+
function passesHardFilter(hint, input) {
|
|
2790
|
+
if (!hint.platforms.includes(input.platform)) return false;
|
|
2791
|
+
const wantsElementScope = input.element !== null;
|
|
2792
|
+
if (wantsElementScope) {
|
|
2793
|
+
if (hint.scope !== "element" && hint.scope !== "both") return false;
|
|
2794
|
+
} else if (hint.scope !== "screen" && hint.scope !== "both") {
|
|
2795
|
+
return false;
|
|
2796
|
+
}
|
|
2797
|
+
if (hint.paths.length > 0 && !matchesAnyPath(hint.paths, input.path)) return false;
|
|
2798
|
+
if (hint.active === false) return false;
|
|
2799
|
+
if (hintCharCount(hint.label) > HINT_LABEL_MAX_CHARS) return false;
|
|
2800
|
+
if (hintCharCount(hint.draft) > HINT_DRAFT_MAX_CHARS) return false;
|
|
2801
|
+
return true;
|
|
2802
|
+
}
|
|
2803
|
+
function matchesAnyPath(paths, path) {
|
|
2804
|
+
if (path === null) return false;
|
|
2805
|
+
return paths.some((pattern) => matchPath(pattern, path) !== null);
|
|
2806
|
+
}
|
|
2807
|
+
function scoreHint(hint, input) {
|
|
2808
|
+
let score = bestPathScore(hint.paths, input.path);
|
|
2809
|
+
if (input.element !== null && matchElement(hint.element, input.element, input.components)) {
|
|
2810
|
+
score += SCORE_ELEMENT;
|
|
2811
|
+
}
|
|
2812
|
+
return score;
|
|
2813
|
+
}
|
|
2814
|
+
function bestPathScore(paths, path) {
|
|
2815
|
+
if (paths.length === 0 || path === null) return 0;
|
|
2816
|
+
let best = 0;
|
|
2817
|
+
for (const pattern of paths) {
|
|
2818
|
+
const result = matchPath(pattern, path);
|
|
2819
|
+
if (result === "exact") return SCORE_PATH_EXACT;
|
|
2820
|
+
if (result === "pattern") best = SCORE_PATH_PATTERN;
|
|
2821
|
+
}
|
|
2822
|
+
return best;
|
|
2823
|
+
}
|
|
2824
|
+
|
|
2825
|
+
// src/hints/provider.ts
|
|
2826
|
+
var CACHE_KEY_PREFIX = "feedback-kit:hints:";
|
|
2827
|
+
var HintProvider = class {
|
|
2828
|
+
constructor(opts) {
|
|
2829
|
+
this.entry = null;
|
|
2830
|
+
/** refresh() 동시 호출을 한 번의 요청으로 합친다. */
|
|
2831
|
+
this.inflight = null;
|
|
2832
|
+
/**
|
|
2833
|
+
* 401/403 등 설정 문제로 영구 실패했는가 — 이 인스턴스가 살아 있는 동안 재요청하지
|
|
2834
|
+
* 않는다. **저장소에는 남기지 않는다**(메모리만): 토큰을 고치고 재배포하면 그건 새
|
|
2835
|
+
* 페이지 로드 = 새 인스턴스이므로 자연히 풀린다. 캐시에 박아두면 사용자가 캐시를
|
|
2836
|
+
* 지우기 전까지 죽은 채로 남는다.
|
|
2837
|
+
*/
|
|
2838
|
+
this.permanentlyFailed = false;
|
|
2839
|
+
/**
|
|
2840
|
+
* 실패(네트워크·타임아웃·5xx·401/403 전부 포함) 경고를 이미 냈는가 — 인스턴스당 1회로
|
|
2841
|
+
* 제한한다. 5분 쿨다운마다 매번 경고를 찍으면 콘솔이 "실패했다"만 반복하는 노이즈가
|
|
2842
|
+
* 된다("개발 빌드에서만 경고를 1회 남긴다" — 계약 원문).
|
|
2843
|
+
*/
|
|
2844
|
+
this.warnedFailure = false;
|
|
2845
|
+
this.source = opts.source;
|
|
2846
|
+
this.storage = opts.storage ?? null;
|
|
2847
|
+
this.storageKey = CACHE_KEY_PREFIX + opts.cacheKey;
|
|
2848
|
+
this.now = opts.now ?? (() => Date.now());
|
|
2849
|
+
this.warn = opts.warn ?? (() => void 0);
|
|
2850
|
+
this.staticCatalog = opts.staticHints ? { version: null, maxDisplay: HINT_DISPLAY_DEFAULT, hints: opts.staticHints } : null;
|
|
2851
|
+
}
|
|
2852
|
+
/** 캐시 복원. 마운트 직후 1회. 던지지 않는다. */
|
|
2853
|
+
async load() {
|
|
2854
|
+
if (this.staticCatalog !== null) return;
|
|
2855
|
+
if (this.storage === null) return;
|
|
2856
|
+
try {
|
|
2857
|
+
const raw = await this.storage.get(this.storageKey);
|
|
2858
|
+
if (raw === null) return;
|
|
2859
|
+
const parsed = parseCacheEntry(JSON.parse(raw));
|
|
2860
|
+
if (parsed !== null) this.entry = parsed;
|
|
2861
|
+
} catch (err) {
|
|
2862
|
+
this.warn(`feedback-kit: \uD78C\uD2B8 \uCE90\uC2DC\uB97C \uC77D\uC9C0 \uBABB\uD588\uB2E4(${String(err)})`);
|
|
2403
2863
|
}
|
|
2864
|
+
}
|
|
2865
|
+
/** 필요하면 네트워크. TTL 안이거나 쿨다운 중이면 아무것도 안 한다. 던지지 않는다. */
|
|
2866
|
+
refresh() {
|
|
2867
|
+
if (this.staticCatalog !== null) return Promise.resolve();
|
|
2868
|
+
if (this.source === null) return Promise.resolve();
|
|
2869
|
+
if (this.inflight !== null) return this.inflight;
|
|
2870
|
+
const p = this.doRefresh().finally(() => {
|
|
2871
|
+
this.inflight = null;
|
|
2872
|
+
});
|
|
2873
|
+
this.inflight = p;
|
|
2874
|
+
return p;
|
|
2875
|
+
}
|
|
2876
|
+
/** 지금 쓸 수 있는 카탈로그. 없으면 null. 동기 — 요청을 기다리지 않는다. */
|
|
2877
|
+
catalog() {
|
|
2878
|
+
if (this.staticCatalog !== null) return this.staticCatalog;
|
|
2879
|
+
if (this.entry === null || this.entry.hints.length === 0) return null;
|
|
2880
|
+
return { version: this.entry.version, maxDisplay: this.entry.maxDisplay, hints: this.entry.hints };
|
|
2881
|
+
}
|
|
2882
|
+
async doRefresh() {
|
|
2883
|
+
if (this.permanentlyFailed) return;
|
|
2884
|
+
const now = this.now();
|
|
2885
|
+
if (this.entry !== null) {
|
|
2886
|
+
if (now - this.entry.fetchedAt < HINT_TTL_MS) return;
|
|
2887
|
+
if (this.entry.failedAt !== null && now - this.entry.failedAt < HINT_FAIL_COOLDOWN_MS) return;
|
|
2888
|
+
}
|
|
2889
|
+
const etag = this.entry?.etag ?? null;
|
|
2890
|
+
const result = await this.fetchWithTimeout(etag);
|
|
2891
|
+
if (result.kind === "ok") {
|
|
2892
|
+
const parsed = parseHintCatalog(result.raw);
|
|
2893
|
+
if (parsed === null) {
|
|
2894
|
+
this.warn("feedback-kit: \uD78C\uD2B8 \uC751\uB2F5\uC774 \uC2A4\uD0A4\uB9C8\uB97C \uBC97\uC5B4\uB098 \uAE30\uC874 \uCE90\uC2DC\uB97C \uC720\uC9C0\uD55C\uB2E4");
|
|
2895
|
+
return;
|
|
2896
|
+
}
|
|
2897
|
+
await this.setEntry({
|
|
2898
|
+
version: parsed.version,
|
|
2899
|
+
etag: result.etag,
|
|
2900
|
+
fetchedAt: now,
|
|
2901
|
+
maxDisplay: parsed.maxDisplay,
|
|
2902
|
+
hints: parsed.hints,
|
|
2903
|
+
failedAt: null
|
|
2904
|
+
});
|
|
2905
|
+
return;
|
|
2906
|
+
}
|
|
2907
|
+
if (result.kind === "not-modified") {
|
|
2908
|
+
if (this.entry !== null) {
|
|
2909
|
+
await this.setEntry({ ...this.entry, fetchedAt: now, failedAt: null });
|
|
2910
|
+
}
|
|
2911
|
+
return;
|
|
2912
|
+
}
|
|
2913
|
+
if (result.permanent === true) {
|
|
2914
|
+
this.permanentlyFailed = true;
|
|
2915
|
+
}
|
|
2916
|
+
if (!this.warnedFailure && detectDevBuild()) {
|
|
2917
|
+
this.warnedFailure = true;
|
|
2918
|
+
this.warn(
|
|
2919
|
+
result.permanent === true ? "feedback-kit: \uD78C\uD2B8 \uC694\uCCAD\uC774 \uAC70\uBD80\uB410\uB2E4(401/403) \u2014 \uC774 \uC138\uC158\uC5D0\uC11C\uB294 \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uC9C0 \uC54A\uB294\uB2E4" : "feedback-kit: \uD78C\uD2B8 \uC694\uCCAD\uC774 \uC2E4\uD328\uD588\uB2E4 \u2014 \uC7A0\uC2DC \uB4A4 \uB2E4\uC2DC \uC2DC\uB3C4\uD55C\uB2E4"
|
|
2920
|
+
);
|
|
2921
|
+
}
|
|
2922
|
+
const base = this.entry ?? {
|
|
2923
|
+
version: null,
|
|
2924
|
+
etag: null,
|
|
2925
|
+
fetchedAt: 0,
|
|
2926
|
+
maxDisplay: HINT_DISPLAY_DEFAULT,
|
|
2927
|
+
hints: [],
|
|
2928
|
+
failedAt: null
|
|
2929
|
+
};
|
|
2930
|
+
await this.setEntry({ ...base, failedAt: now });
|
|
2931
|
+
}
|
|
2932
|
+
async fetchWithTimeout(etag) {
|
|
2933
|
+
if (this.source === null) return { kind: "fail" };
|
|
2934
|
+
const g = globalThis;
|
|
2935
|
+
const controller = typeof g.AbortController === "function" ? new g.AbortController() : null;
|
|
2936
|
+
try {
|
|
2937
|
+
return await withTimeout(
|
|
2938
|
+
this.source.fetchHints(etag, controller?.signal),
|
|
2939
|
+
HINT_TIMEOUT_MS,
|
|
2940
|
+
() => controller?.abort()
|
|
2941
|
+
);
|
|
2942
|
+
} catch {
|
|
2943
|
+
return { kind: "fail" };
|
|
2944
|
+
}
|
|
2945
|
+
}
|
|
2946
|
+
async setEntry(entry) {
|
|
2947
|
+
this.entry = entry;
|
|
2948
|
+
if (this.storage === null) return;
|
|
2949
|
+
try {
|
|
2950
|
+
await this.storage.set(this.storageKey, JSON.stringify(entry));
|
|
2951
|
+
} catch (err) {
|
|
2952
|
+
this.warn(`feedback-kit: \uD78C\uD2B8 \uCE90\uC2DC\uB97C \uC800\uC7A5\uD558\uC9C0 \uBABB\uD588\uB2E4(${String(err)})`);
|
|
2953
|
+
}
|
|
2954
|
+
}
|
|
2955
|
+
};
|
|
2956
|
+
function withTimeout(promise, ms, onTimeout) {
|
|
2957
|
+
return new Promise((resolve, reject) => {
|
|
2958
|
+
const g = globalThis;
|
|
2959
|
+
let settled = false;
|
|
2960
|
+
const timer = typeof g.setTimeout === "function" ? g.setTimeout(() => {
|
|
2961
|
+
if (settled) return;
|
|
2962
|
+
settled = true;
|
|
2963
|
+
onTimeout?.();
|
|
2964
|
+
reject(new Error("feedback-kit: hint fetch timed out"));
|
|
2965
|
+
}, ms) : null;
|
|
2966
|
+
const clear = () => {
|
|
2967
|
+
if (timer !== null && typeof g.clearTimeout === "function") g.clearTimeout(timer);
|
|
2968
|
+
};
|
|
2969
|
+
promise.then(
|
|
2970
|
+
(value) => {
|
|
2971
|
+
if (settled) return;
|
|
2972
|
+
settled = true;
|
|
2973
|
+
clear();
|
|
2974
|
+
resolve(value);
|
|
2975
|
+
},
|
|
2976
|
+
(err) => {
|
|
2977
|
+
if (settled) return;
|
|
2978
|
+
settled = true;
|
|
2979
|
+
clear();
|
|
2980
|
+
reject(err);
|
|
2981
|
+
}
|
|
2982
|
+
);
|
|
2983
|
+
});
|
|
2984
|
+
}
|
|
2985
|
+
function parseCacheEntry(value) {
|
|
2986
|
+
if (typeof value !== "object" || value === null) return null;
|
|
2987
|
+
const obj = value;
|
|
2988
|
+
if (!Array.isArray(obj.hints)) return null;
|
|
2989
|
+
if (typeof obj.fetchedAt !== "number") return null;
|
|
2990
|
+
if (typeof obj.maxDisplay !== "number") return null;
|
|
2991
|
+
return {
|
|
2992
|
+
version: typeof obj.version === "number" ? obj.version : null,
|
|
2993
|
+
etag: typeof obj.etag === "string" ? obj.etag : null,
|
|
2994
|
+
fetchedAt: obj.fetchedAt,
|
|
2995
|
+
maxDisplay: obj.maxDisplay,
|
|
2996
|
+
hints: obj.hints,
|
|
2997
|
+
failedAt: typeof obj.failedAt === "number" ? obj.failedAt : null
|
|
2404
2998
|
};
|
|
2405
2999
|
}
|
|
2406
3000
|
// Annotate the CommonJS export names for ESM import in node:
|
|
@@ -2418,6 +3012,16 @@ function createLassoAdapter(opts = {}) {
|
|
|
2418
3012
|
FLOATING_BUTTON_ID,
|
|
2419
3013
|
FeedbackQueue,
|
|
2420
3014
|
FocusRing,
|
|
3015
|
+
HINT_DISPLAY_DEFAULT,
|
|
3016
|
+
HINT_DISPLAY_MAX,
|
|
3017
|
+
HINT_DISPLAY_MIN,
|
|
3018
|
+
HINT_DRAFT_MAX_CHARS,
|
|
3019
|
+
HINT_FAIL_COOLDOWN_MS,
|
|
3020
|
+
HINT_LABEL_MAX_CHARS,
|
|
3021
|
+
HINT_MAX_ITEMS,
|
|
3022
|
+
HINT_TIMEOUT_MS,
|
|
3023
|
+
HINT_TTL_MS,
|
|
3024
|
+
HintProvider,
|
|
2421
3025
|
KEEPALIVE_BODY_LIMIT_BYTES,
|
|
2422
3026
|
LASSO_DEFAULT_ENDPOINT,
|
|
2423
3027
|
LOG_BUFFER_LIMIT,
|
|
@@ -2426,6 +3030,7 @@ function createLassoAdapter(opts = {}) {
|
|
|
2426
3030
|
MAX_QUEUE_ITEMS,
|
|
2427
3031
|
MODAL_ACTION_ATTACH,
|
|
2428
3032
|
MODAL_ACTION_CANCEL,
|
|
3033
|
+
MODAL_ACTION_HINT_TOGGLE,
|
|
2429
3034
|
MODAL_ACTION_PICK,
|
|
2430
3035
|
MODAL_ACTION_PIN,
|
|
2431
3036
|
MODAL_ACTION_REMOVE_SCREENSHOT,
|
|
@@ -2439,6 +3044,9 @@ function createLassoAdapter(opts = {}) {
|
|
|
2439
3044
|
REDACTED,
|
|
2440
3045
|
ReportModalController,
|
|
2441
3046
|
RingBuffer,
|
|
3047
|
+
SCORE_ELEMENT,
|
|
3048
|
+
SCORE_PATH_EXACT,
|
|
3049
|
+
SCORE_PATH_PATTERN,
|
|
2442
3050
|
SCREENSHOT_FAILED_MESSAGE,
|
|
2443
3051
|
SCREENSHOT_MAX_BASE64_BYTES,
|
|
2444
3052
|
SCREENSHOT_QUALITY_STEPS,
|
|
@@ -2448,6 +3056,7 @@ function createLassoAdapter(opts = {}) {
|
|
|
2448
3056
|
SUBMIT_PENDING_MESSAGE,
|
|
2449
3057
|
TITLE_MAX_CHARS,
|
|
2450
3058
|
WidgetController,
|
|
3059
|
+
appPathFromNav,
|
|
2451
3060
|
buildContext,
|
|
2452
3061
|
buildLassoContext,
|
|
2453
3062
|
buildLassoEnvelope,
|
|
@@ -2465,10 +3074,15 @@ function createLassoAdapter(opts = {}) {
|
|
|
2465
3074
|
diagnosticsProviderFor,
|
|
2466
3075
|
getOrCreateGuestId,
|
|
2467
3076
|
guestUser,
|
|
3077
|
+
hintCharCount,
|
|
2468
3078
|
isInternalUser,
|
|
3079
|
+
matchElement,
|
|
3080
|
+
matchPath,
|
|
2469
3081
|
normalizePin,
|
|
2470
3082
|
normalizeUser,
|
|
3083
|
+
parseHintCatalog,
|
|
2471
3084
|
parseSourceAttr,
|
|
3085
|
+
rankHints,
|
|
2472
3086
|
renderReportBody,
|
|
2473
3087
|
resolveConfig,
|
|
2474
3088
|
resolveContextUser,
|