@consilioweb/spellcheck 0.10.1

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.
@@ -0,0 +1,1711 @@
1
+ "use client";
2
+ 'use strict';
3
+
4
+ var React3 = require('react');
5
+ var ui = require('@payloadcms/ui');
6
+ var jsxRuntime = require('react/jsx-runtime');
7
+
8
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
9
+
10
+ var React3__default = /*#__PURE__*/_interopDefault(React3);
11
+
12
+ // src/components/SpellCheckField.tsx
13
+ var styles = {
14
+ card: {
15
+ border: "1px solid var(--theme-elevation-150)",
16
+ borderRadius: "4px",
17
+ padding: "12px",
18
+ marginBottom: "8px",
19
+ backgroundColor: "var(--theme-elevation-50)"
20
+ },
21
+ header: {
22
+ display: "flex",
23
+ justifyContent: "space-between",
24
+ alignItems: "flex-start",
25
+ gap: "8px",
26
+ marginBottom: "8px"
27
+ },
28
+ message: {
29
+ fontSize: "13px",
30
+ lineHeight: "1.4",
31
+ color: "var(--theme-text)",
32
+ flex: 1
33
+ },
34
+ badge: {
35
+ fontSize: "10px",
36
+ padding: "2px 6px",
37
+ borderRadius: "3px",
38
+ backgroundColor: "var(--theme-elevation-200)",
39
+ color: "var(--theme-elevation-800)",
40
+ whiteSpace: "nowrap",
41
+ flexShrink: 0
42
+ },
43
+ context: {
44
+ fontSize: "12px",
45
+ color: "var(--theme-elevation-500)",
46
+ backgroundColor: "var(--theme-elevation-100)",
47
+ padding: "6px 8px",
48
+ borderRadius: "3px",
49
+ fontFamily: "monospace",
50
+ marginBottom: "8px",
51
+ wordBreak: "break-word"
52
+ },
53
+ original: {
54
+ textDecoration: "line-through",
55
+ color: "var(--theme-error-500)",
56
+ fontWeight: 600
57
+ },
58
+ suggestion: {
59
+ display: "flex",
60
+ alignItems: "center",
61
+ gap: "6px",
62
+ marginBottom: "8px"
63
+ },
64
+ suggestionLabel: {
65
+ fontSize: "11px",
66
+ color: "var(--theme-elevation-500)"
67
+ },
68
+ suggestionText: {
69
+ fontSize: "13px",
70
+ fontWeight: 600,
71
+ color: "var(--theme-success-500)"
72
+ },
73
+ actions: {
74
+ display: "flex",
75
+ gap: "6px"
76
+ },
77
+ btn: {
78
+ fontSize: "11px",
79
+ padding: "4px 10px",
80
+ borderRadius: "3px",
81
+ border: "1px solid var(--theme-elevation-200)",
82
+ backgroundColor: "var(--theme-elevation-0)",
83
+ color: "var(--theme-text)",
84
+ cursor: "pointer"
85
+ },
86
+ btnFix: {
87
+ fontSize: "11px",
88
+ padding: "4px 10px",
89
+ borderRadius: "3px",
90
+ border: "none",
91
+ backgroundColor: "var(--theme-success-500)",
92
+ color: "#fff",
93
+ cursor: "pointer",
94
+ fontWeight: 600
95
+ },
96
+ fixed: {
97
+ opacity: 0.5,
98
+ pointerEvents: "none"
99
+ },
100
+ diff: {
101
+ padding: "8px 10px",
102
+ borderRadius: "3px",
103
+ backgroundColor: "var(--theme-elevation-100)",
104
+ marginBottom: "8px",
105
+ fontSize: "13px",
106
+ lineHeight: "1.6",
107
+ fontFamily: "monospace",
108
+ wordBreak: "break-word"
109
+ },
110
+ diffBefore: {
111
+ textDecoration: "line-through",
112
+ backgroundColor: "rgba(239, 68, 68, 0.15)",
113
+ color: "var(--theme-error-500)",
114
+ padding: "1px 3px",
115
+ borderRadius: "2px"
116
+ },
117
+ diffAfter: {
118
+ backgroundColor: "rgba(34, 197, 94, 0.15)",
119
+ color: "var(--theme-success-500)",
120
+ fontWeight: 600,
121
+ padding: "1px 3px",
122
+ borderRadius: "2px"
123
+ },
124
+ diffArrow: {
125
+ display: "inline-block",
126
+ margin: "0 6px",
127
+ color: "var(--theme-elevation-400)",
128
+ fontSize: "12px"
129
+ }
130
+ };
131
+ var IssueCard = ({
132
+ issue,
133
+ onFix,
134
+ onIgnore,
135
+ onAddToDict,
136
+ isFixed = false
137
+ }) => {
138
+ const [showDiff, setShowDiff] = React3.useState(false);
139
+ const [selectedReplacement, setSelectedReplacement] = React3.useState(0);
140
+ const [manualEdit, setManualEdit] = React3.useState(false);
141
+ const [manualValue, setManualValue] = React3.useState("");
142
+ const [addedToDict, setAddedToDict] = React3.useState(false);
143
+ const getContextIdx = (ctx, original) => {
144
+ if (typeof issue.contextOffset === "number" && issue.contextOffset >= 0) {
145
+ return issue.contextOffset;
146
+ }
147
+ return ctx.indexOf(original);
148
+ };
149
+ const highlightContext = (ctx, original) => {
150
+ const idx = getContextIdx(ctx, original);
151
+ if (!original || idx < 0) return ctx;
152
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
153
+ ctx.slice(0, idx),
154
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles.original, children: original }),
155
+ ctx.slice(idx + original.length)
156
+ ] });
157
+ };
158
+ const renderDiff = (ctx, original, replacement) => {
159
+ const idx = getContextIdx(ctx, original);
160
+ if (!original || idx < 0) {
161
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles.diff, children: [
162
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles.diffBefore, children: original }),
163
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles.diffArrow, children: "\u2192" }),
164
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles.diffAfter, children: replacement })
165
+ ] });
166
+ }
167
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles.diff, children: [
168
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { marginBottom: "4px", color: "var(--theme-elevation-400)", fontSize: "10px", textTransform: "uppercase", letterSpacing: "0.5px" }, children: "Avant / Apr\xE8s" }),
169
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
170
+ ctx.slice(0, idx),
171
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles.diffBefore, children: original }),
172
+ ctx.slice(idx + original.length)
173
+ ] }),
174
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { marginTop: "4px" }, children: [
175
+ ctx.slice(0, idx),
176
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles.diffAfter, children: replacement }),
177
+ ctx.slice(idx + original.length)
178
+ ] })
179
+ ] });
180
+ };
181
+ const currentReplacement = manualEdit && manualValue ? manualValue : issue.replacements[selectedReplacement] || "";
182
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { ...styles.card, ...isFixed ? styles.fixed : {} }, children: [
183
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles.header, children: [
184
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles.message, children: issue.message }),
185
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles.badge, children: issue.source === "claude" ? "IA" : issue.category })
186
+ ] }),
187
+ showDiff && issue.context && currentReplacement ? renderDiff(issue.context, issue.original, currentReplacement) : issue.context ? /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles.context, children: highlightContext(issue.context, issue.original) }) : null,
188
+ !manualEdit && issue.replacements.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles.suggestion, children: [
189
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles.suggestionLabel, children: "Suggestion :" }),
190
+ issue.replacements.length > 1 ? /* @__PURE__ */ jsxRuntime.jsx(
191
+ "select",
192
+ {
193
+ value: selectedReplacement,
194
+ onChange: (e) => setSelectedReplacement(Number(e.target.value)),
195
+ style: {
196
+ fontSize: "13px",
197
+ fontWeight: 600,
198
+ color: "var(--theme-success-500)",
199
+ border: "1px solid var(--theme-elevation-200)",
200
+ borderRadius: "3px",
201
+ padding: "2px 4px",
202
+ backgroundColor: "var(--theme-elevation-0)"
203
+ },
204
+ children: issue.replacements.map((r, i) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: i, children: r }, i))
205
+ }
206
+ ) : /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles.suggestionText, children: currentReplacement })
207
+ ] }),
208
+ manualEdit && !isFixed && /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { marginBottom: "8px" }, children: [
209
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: "11px", color: "var(--theme-elevation-500)", marginBottom: "4px" }, children: "Correction manuelle :" }),
210
+ /* @__PURE__ */ jsxRuntime.jsx(
211
+ "input",
212
+ {
213
+ type: "text",
214
+ value: manualValue,
215
+ onChange: (e) => setManualValue(e.target.value),
216
+ placeholder: issue.original,
217
+ style: {
218
+ width: "100%",
219
+ padding: "6px 8px",
220
+ fontSize: "13px",
221
+ border: "1px solid var(--theme-elevation-200)",
222
+ borderRadius: "3px",
223
+ backgroundColor: "var(--theme-elevation-0)",
224
+ color: "var(--theme-text)",
225
+ boxSizing: "border-box"
226
+ }
227
+ }
228
+ )
229
+ ] }),
230
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles.actions, children: [
231
+ issue.context && currentReplacement && !isFixed && /* @__PURE__ */ jsxRuntime.jsx(
232
+ "button",
233
+ {
234
+ type: "button",
235
+ style: { ...styles.btn, fontSize: "10px" },
236
+ onClick: () => setShowDiff(!showDiff),
237
+ children: showDiff ? "Masquer" : "Avant/Apr\xE8s"
238
+ }
239
+ ),
240
+ !isFixed && /* @__PURE__ */ jsxRuntime.jsx(
241
+ "button",
242
+ {
243
+ type: "button",
244
+ style: { ...styles.btn, fontSize: "10px" },
245
+ onClick: () => {
246
+ setManualEdit(!manualEdit);
247
+ if (!manualEdit) setManualValue(issue.original);
248
+ },
249
+ children: manualEdit ? "Suggestion" : "Manuel"
250
+ }
251
+ ),
252
+ currentReplacement && onFix && !isFixed && /* @__PURE__ */ jsxRuntime.jsx(
253
+ "button",
254
+ {
255
+ type: "button",
256
+ style: styles.btnFix,
257
+ onClick: () => onFix(issue.original, currentReplacement, issue.offset, issue.length),
258
+ children: "Corriger"
259
+ }
260
+ ),
261
+ onAddToDict && !isFixed && !addedToDict && issue.original && /* @__PURE__ */ jsxRuntime.jsx(
262
+ "button",
263
+ {
264
+ type: "button",
265
+ style: { ...styles.btn, fontSize: "10px" },
266
+ onClick: () => {
267
+ onAddToDict(issue.original);
268
+ setAddedToDict(true);
269
+ },
270
+ title: "Ajouter au dictionnaire",
271
+ children: "+ Dico"
272
+ }
273
+ ),
274
+ addedToDict && /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: "11px", color: "var(--theme-success-500)" }, children: "Ajout\xE9 au dico" }),
275
+ onIgnore && !isFixed && /* @__PURE__ */ jsxRuntime.jsx(
276
+ "button",
277
+ {
278
+ type: "button",
279
+ style: styles.btn,
280
+ onClick: () => onIgnore(issue.ruleId),
281
+ children: "Ignorer"
282
+ }
283
+ ),
284
+ isFixed && /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: "11px", color: "var(--theme-success-500)" }, children: "Corrige" })
285
+ ] })
286
+ ] });
287
+ };
288
+ var styles2 = {
289
+ container: {
290
+ padding: "12px 0"
291
+ },
292
+ header: {
293
+ display: "flex",
294
+ justifyContent: "space-between",
295
+ alignItems: "center",
296
+ marginBottom: "12px"
297
+ },
298
+ title: {
299
+ fontSize: "13px",
300
+ fontWeight: 600,
301
+ color: "var(--theme-text)",
302
+ textTransform: "uppercase",
303
+ letterSpacing: "0.5px"
304
+ },
305
+ scoreBadge: (score) => ({
306
+ fontSize: "14px",
307
+ fontWeight: 700,
308
+ padding: "3px 10px",
309
+ borderRadius: "12px",
310
+ color: "#fff",
311
+ backgroundColor: score >= 95 ? "var(--theme-success-500)" : score >= 80 ? "#f59e0b" : "var(--theme-error-500)"
312
+ }),
313
+ checkBtn: {
314
+ display: "block",
315
+ width: "100%",
316
+ padding: "8px 12px",
317
+ fontSize: "13px",
318
+ fontWeight: 600,
319
+ border: "1px solid var(--theme-elevation-200)",
320
+ borderRadius: "4px",
321
+ backgroundColor: "var(--theme-elevation-0)",
322
+ color: "var(--theme-text)",
323
+ cursor: "pointer",
324
+ textAlign: "center",
325
+ marginBottom: "12px"
326
+ },
327
+ checkBtnDisabled: {
328
+ opacity: 0.6,
329
+ cursor: "not-allowed"
330
+ },
331
+ stats: {
332
+ display: "flex",
333
+ justifyContent: "space-between",
334
+ fontSize: "12px",
335
+ color: "var(--theme-elevation-500)",
336
+ marginBottom: "12px",
337
+ padding: "6px 8px",
338
+ backgroundColor: "var(--theme-elevation-50)",
339
+ borderRadius: "4px"
340
+ },
341
+ issueList: {
342
+ maxHeight: "400px",
343
+ overflowY: "auto"
344
+ },
345
+ empty: {
346
+ textAlign: "center",
347
+ padding: "20px 0",
348
+ fontSize: "13px",
349
+ color: "var(--theme-elevation-500)"
350
+ },
351
+ error: {
352
+ padding: "8px 12px",
353
+ fontSize: "12px",
354
+ color: "var(--theme-error-500)",
355
+ backgroundColor: "var(--theme-error-100)",
356
+ borderRadius: "4px",
357
+ marginBottom: "12px"
358
+ }
359
+ };
360
+ var SpellCheckField = () => {
361
+ const { id, collectionSlug } = ui.useDocumentInfo();
362
+ const [result, setResult] = React3.useState(null);
363
+ const [resultDbId, setResultDbId] = React3.useState(null);
364
+ const [loading, setLoading] = React3.useState(false);
365
+ const [error, setError] = React3.useState(null);
366
+ const resultRef = React3.useRef(result);
367
+ resultRef.current = result;
368
+ const resultDbIdRef = React3.useRef(resultDbId);
369
+ resultDbIdRef.current = resultDbId;
370
+ React3.useEffect(() => {
371
+ if (!id || !collectionSlug) return;
372
+ fetch(`/api/spellcheck-results?where[docId][equals]=${id}&where[collection][equals]=${collectionSlug}&limit=1`).then((res) => res.json()).then((data) => {
373
+ if (data.docs?.[0]) {
374
+ setResultDbId(data.docs[0].id);
375
+ setResult({
376
+ docId: String(id),
377
+ collection: collectionSlug,
378
+ score: data.docs[0].score,
379
+ issueCount: data.docs[0].issueCount,
380
+ wordCount: data.docs[0].wordCount || 0,
381
+ issues: data.docs[0].issues || [],
382
+ lastChecked: data.docs[0].lastChecked
383
+ });
384
+ }
385
+ }).catch(() => {
386
+ });
387
+ }, [id, collectionSlug]);
388
+ const handleCheck = React3.useCallback(async () => {
389
+ if (!id || !collectionSlug || loading) return;
390
+ setLoading(true);
391
+ setError(null);
392
+ try {
393
+ const res = await fetch("/api/spellcheck/validate", {
394
+ method: "POST",
395
+ headers: { "Content-Type": "application/json" },
396
+ body: JSON.stringify({ id, collection: collectionSlug })
397
+ });
398
+ if (!res.ok) {
399
+ const err = await res.json();
400
+ throw new Error(err.error || `HTTP ${res.status}`);
401
+ }
402
+ const data = await res.json();
403
+ setResult(data);
404
+ } catch (err) {
405
+ setError(err.message);
406
+ } finally {
407
+ setLoading(false);
408
+ }
409
+ }, [id, collectionSlug, loading]);
410
+ const removeIssue = React3.useCallback((matcher) => {
411
+ const prev = resultRef.current;
412
+ if (!prev) return;
413
+ const updatedIssues = prev.issues.filter((issue) => {
414
+ if (typeof matcher.offset === "number" && issue.offset === matcher.offset) {
415
+ if (matcher.original && issue.original !== matcher.original) return true;
416
+ if (matcher.ruleId && issue.ruleId !== matcher.ruleId) return true;
417
+ return false;
418
+ }
419
+ return true;
420
+ });
421
+ const updatedCount = updatedIssues.length;
422
+ const updatedScore = prev.wordCount > 0 ? Math.min(100, Math.max(0, Math.round(100 - updatedCount / prev.wordCount * 1e3))) : prev.score;
423
+ setResult({ ...prev, issues: updatedIssues, issueCount: updatedCount, score: updatedScore });
424
+ if (resultDbIdRef.current) {
425
+ fetch(`/api/spellcheck-results/${resultDbIdRef.current}`, {
426
+ method: "PATCH",
427
+ headers: { "Content-Type": "application/json" },
428
+ body: JSON.stringify({ issues: updatedIssues, issueCount: updatedCount, score: updatedScore })
429
+ }).catch(() => {
430
+ });
431
+ }
432
+ }, []);
433
+ const handleFix = React3.useCallback(async (original, replacement, offset, length) => {
434
+ if (!id || !collectionSlug) return;
435
+ try {
436
+ const res = await fetch("/api/spellcheck/fix", {
437
+ method: "POST",
438
+ headers: { "Content-Type": "application/json" },
439
+ body: JSON.stringify({ id, collection: collectionSlug, original, replacement, offset, length })
440
+ });
441
+ if (res.ok) {
442
+ removeIssue({ offset, original });
443
+ }
444
+ } catch {
445
+ }
446
+ }, [id, collectionSlug, removeIssue]);
447
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles2.container, children: [
448
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles2.header, children: [
449
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles2.title, children: "Orthographe" }),
450
+ result && /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles2.scoreBadge(result.score), children: result.score })
451
+ ] }),
452
+ /* @__PURE__ */ jsxRuntime.jsx(
453
+ "button",
454
+ {
455
+ type: "button",
456
+ style: {
457
+ ...styles2.checkBtn,
458
+ ...loading ? styles2.checkBtnDisabled : {}
459
+ },
460
+ onClick: handleCheck,
461
+ disabled: loading,
462
+ children: loading ? "V\xE9rification..." : "V\xE9rifier"
463
+ }
464
+ ),
465
+ error && /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles2.error, children: error }),
466
+ result && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
467
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles2.stats, children: [
468
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
469
+ result.wordCount,
470
+ " mots"
471
+ ] }),
472
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
473
+ result.issueCount,
474
+ " probl\xE8me",
475
+ result.issueCount !== 1 ? "s" : ""
476
+ ] }),
477
+ result.lastChecked && /* @__PURE__ */ jsxRuntime.jsx("span", { children: new Date(result.lastChecked).toLocaleTimeString("fr-FR", {
478
+ hour: "2-digit",
479
+ minute: "2-digit"
480
+ }) })
481
+ ] }),
482
+ result.issues.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles2.issueList, children: result.issues.map((issue, i) => /* @__PURE__ */ jsxRuntime.jsx(
483
+ IssueCard,
484
+ {
485
+ issue,
486
+ onFix: handleFix,
487
+ onIgnore: () => {
488
+ removeIssue({ offset: issue.offset, ruleId: issue.ruleId });
489
+ if (resultDbIdRef.current) {
490
+ fetch(`/api/spellcheck-results/${resultDbIdRef.current}?depth=0`).then((res) => res.json()).then((data) => {
491
+ const existing = Array.isArray(data.ignoredIssues) ? data.ignoredIssues : [];
492
+ const alreadyIgnored = existing.some((e) => e.ruleId === issue.ruleId && e.original === issue.original);
493
+ if (!alreadyIgnored) {
494
+ fetch(`/api/spellcheck-results/${resultDbIdRef.current}`, {
495
+ method: "PATCH",
496
+ headers: { "Content-Type": "application/json" },
497
+ body: JSON.stringify({ ignoredIssues: [...existing, { ruleId: issue.ruleId, original: issue.original }] })
498
+ }).catch(() => {
499
+ });
500
+ }
501
+ }).catch(() => {
502
+ });
503
+ }
504
+ },
505
+ onAddToDict: (word) => {
506
+ fetch("/api/spellcheck/dictionary", {
507
+ method: "POST",
508
+ headers: { "Content-Type": "application/json" },
509
+ body: JSON.stringify({ word })
510
+ }).catch(() => {
511
+ });
512
+ removeIssue({ offset: issue.offset, ruleId: issue.ruleId });
513
+ }
514
+ },
515
+ `${issue.ruleId}-${issue.offset}-${i}`
516
+ )) }) : /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles2.empty, children: "Aucun probl\xE8me d\xE9tect\xE9 \u2713" })
517
+ ] }),
518
+ !result && !loading && /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles2.empty, children: 'Cliquez sur "V\xE9rifier" pour analyser le contenu' })
519
+ ] });
520
+ };
521
+ var styles3 = {
522
+ container: {
523
+ padding: "24px",
524
+ maxWidth: "1200px"
525
+ },
526
+ header: {
527
+ display: "flex",
528
+ justifyContent: "space-between",
529
+ alignItems: "center",
530
+ marginBottom: "24px"
531
+ },
532
+ title: {
533
+ fontSize: "24px",
534
+ fontWeight: 700,
535
+ color: "var(--theme-text)",
536
+ margin: 0
537
+ },
538
+ description: {
539
+ fontSize: "14px",
540
+ color: "var(--theme-elevation-500)",
541
+ marginTop: "4px"
542
+ },
543
+ scanBtn: {
544
+ padding: "10px 20px",
545
+ fontSize: "14px",
546
+ fontWeight: 600,
547
+ border: "none",
548
+ borderRadius: "4px",
549
+ backgroundColor: "var(--theme-elevation-900)",
550
+ color: "var(--theme-elevation-0)",
551
+ cursor: "pointer"
552
+ },
553
+ scanBtnDisabled: {
554
+ opacity: 0.6,
555
+ cursor: "not-allowed"
556
+ },
557
+ summary: {
558
+ display: "grid",
559
+ gridTemplateColumns: "repeat(auto-fit, minmax(180px, 1fr))",
560
+ gap: "16px",
561
+ marginBottom: "24px"
562
+ },
563
+ summaryCard: {
564
+ padding: "16px",
565
+ borderRadius: "6px",
566
+ backgroundColor: "var(--theme-elevation-50)",
567
+ border: "1px solid var(--theme-elevation-150)"
568
+ },
569
+ summaryLabel: {
570
+ fontSize: "12px",
571
+ color: "var(--theme-elevation-500)",
572
+ textTransform: "uppercase",
573
+ letterSpacing: "0.5px",
574
+ marginBottom: "4px"
575
+ },
576
+ summaryValue: {
577
+ fontSize: "24px",
578
+ fontWeight: 700,
579
+ color: "var(--theme-text)"
580
+ },
581
+ progress: {
582
+ padding: "12px 16px",
583
+ marginBottom: "16px",
584
+ borderRadius: "4px",
585
+ backgroundColor: "var(--theme-elevation-50)",
586
+ border: "1px solid var(--theme-elevation-150)",
587
+ fontSize: "13px",
588
+ color: "var(--theme-text)"
589
+ },
590
+ progressBar: {
591
+ height: "4px",
592
+ backgroundColor: "var(--theme-elevation-150)",
593
+ borderRadius: "2px",
594
+ marginTop: "8px",
595
+ overflow: "hidden"
596
+ },
597
+ progressFill: (pct) => ({
598
+ height: "100%",
599
+ width: `${pct}%`,
600
+ backgroundColor: "var(--theme-success-500)",
601
+ borderRadius: "2px",
602
+ transition: "width 0.3s"
603
+ }),
604
+ table: {
605
+ width: "100%",
606
+ borderCollapse: "collapse",
607
+ fontSize: "13px"
608
+ },
609
+ th: {
610
+ textAlign: "left",
611
+ padding: "10px 12px",
612
+ borderBottom: "2px solid var(--theme-elevation-200)",
613
+ fontSize: "11px",
614
+ fontWeight: 600,
615
+ textTransform: "uppercase",
616
+ letterSpacing: "0.5px",
617
+ color: "var(--theme-elevation-500)",
618
+ cursor: "pointer"
619
+ },
620
+ td: {
621
+ padding: "10px 12px",
622
+ borderBottom: "1px solid var(--theme-elevation-100)",
623
+ verticalAlign: "middle"
624
+ },
625
+ tr: {
626
+ cursor: "pointer"
627
+ },
628
+ trHover: {
629
+ backgroundColor: "var(--theme-elevation-50)"
630
+ },
631
+ scoreBadge: (score) => ({
632
+ display: "inline-block",
633
+ fontSize: "12px",
634
+ fontWeight: 700,
635
+ padding: "2px 8px",
636
+ borderRadius: "10px",
637
+ color: "#fff",
638
+ backgroundColor: score >= 95 ? "var(--theme-success-500)" : score >= 80 ? "#f59e0b" : "var(--theme-error-500)"
639
+ }),
640
+ expandedRow: {
641
+ padding: "16px",
642
+ backgroundColor: "var(--theme-elevation-50)",
643
+ borderBottom: "1px solid var(--theme-elevation-150)"
644
+ },
645
+ empty: {
646
+ textAlign: "center",
647
+ padding: "40px 0",
648
+ color: "var(--theme-elevation-500)",
649
+ fontSize: "14px"
650
+ },
651
+ link: {
652
+ color: "var(--theme-text)",
653
+ textDecoration: "none",
654
+ fontWeight: 500
655
+ },
656
+ // Tab styles
657
+ tabs: {
658
+ display: "flex",
659
+ gap: "0",
660
+ marginBottom: "24px",
661
+ borderBottom: "2px solid var(--theme-elevation-150)"
662
+ },
663
+ tab: (active) => ({
664
+ padding: "10px 20px",
665
+ fontSize: "14px",
666
+ fontWeight: active ? 600 : 400,
667
+ color: active ? "var(--theme-text)" : "var(--theme-elevation-500)",
668
+ backgroundColor: "transparent",
669
+ border: "none",
670
+ borderBottom: active ? "2px solid var(--theme-text)" : "2px solid transparent",
671
+ marginBottom: "-2px",
672
+ cursor: "pointer"
673
+ })
674
+ };
675
+ var SpellCheckDashboard = () => {
676
+ const [activeTab, setActiveTab] = React3.useState("results");
677
+ const [results, setResults] = React3.useState([]);
678
+ const [loading, setLoading] = React3.useState(true);
679
+ const [scanning, setScanning] = React3.useState(false);
680
+ const [scanProgress, setScanProgress] = React3.useState("");
681
+ const [expandedId, setExpandedId] = React3.useState(null);
682
+ const [sortKey, setSortKey] = React3.useState("score");
683
+ const [sortDir, setSortDir] = React3.useState("asc");
684
+ const [hoveredRow, setHoveredRow] = React3.useState(null);
685
+ const [selectedIds, setSelectedIds] = React3.useState(/* @__PURE__ */ new Set());
686
+ const [filterCollection, setFilterCollection] = React3.useState("");
687
+ const [allDocs, setAllDocs] = React3.useState([]);
688
+ const [loadingDocs, setLoadingDocs] = React3.useState(true);
689
+ const [dictWords, setDictWords] = React3.useState([]);
690
+ const [dictLoading, setDictLoading] = React3.useState(false);
691
+ const [dictSearch, setDictSearch] = React3.useState("");
692
+ const [dictInput, setDictInput] = React3.useState("");
693
+ const [dictSelectedIds, setDictSelectedIds] = React3.useState(/* @__PURE__ */ new Set());
694
+ const [dictToast, setDictToast] = React3.useState("");
695
+ const [showImport, setShowImport] = React3.useState(false);
696
+ const [importText, setImportText] = React3.useState("");
697
+ const resultsRef = React3.useRef(results);
698
+ resultsRef.current = results;
699
+ const loadResults = React3.useCallback(async () => {
700
+ setLoading(true);
701
+ try {
702
+ const res = await fetch("/api/spellcheck-results?limit=0&sort=-lastChecked");
703
+ const data = await res.json();
704
+ setResults(data.docs || []);
705
+ } catch {
706
+ } finally {
707
+ setLoading(false);
708
+ }
709
+ }, []);
710
+ const loadAllDocs = React3.useCallback(async () => {
711
+ setLoadingDocs(true);
712
+ try {
713
+ let collections2 = ["pages", "posts"];
714
+ try {
715
+ const configRes = await fetch("/api/spellcheck/collections");
716
+ if (configRes.ok) {
717
+ const configData = await configRes.json();
718
+ if (Array.isArray(configData.collections)) {
719
+ collections2 = configData.collections;
720
+ }
721
+ }
722
+ } catch {
723
+ }
724
+ const docs = [];
725
+ for (const col of collections2) {
726
+ try {
727
+ const res = await fetch(`/api/${col}?limit=0&depth=0&where[_status][equals]=published`);
728
+ const data = await res.json();
729
+ if (data.docs) {
730
+ for (const doc of data.docs) {
731
+ docs.push({
732
+ id: String(doc.id),
733
+ collection: col,
734
+ title: doc.title || doc.slug || String(doc.id),
735
+ slug: doc.slug || ""
736
+ });
737
+ }
738
+ }
739
+ } catch {
740
+ }
741
+ }
742
+ setAllDocs(docs);
743
+ } finally {
744
+ setLoadingDocs(false);
745
+ }
746
+ }, []);
747
+ const loadDictionary = React3.useCallback(async () => {
748
+ setDictLoading(true);
749
+ try {
750
+ const res = await fetch("/api/spellcheck/dictionary");
751
+ const data = await res.json();
752
+ setDictWords(data.words || []);
753
+ } catch {
754
+ } finally {
755
+ setDictLoading(false);
756
+ }
757
+ }, []);
758
+ React3.useEffect(() => {
759
+ loadResults();
760
+ loadAllDocs();
761
+ }, [loadResults, loadAllDocs]);
762
+ React3.useEffect(() => {
763
+ if (activeTab === "dictionary") {
764
+ loadDictionary();
765
+ }
766
+ }, [activeTab, loadDictionary]);
767
+ React3.useEffect(() => {
768
+ if (!dictToast) return;
769
+ const t = setTimeout(() => setDictToast(""), 3e3);
770
+ return () => clearTimeout(t);
771
+ }, [dictToast]);
772
+ const toggleSelect = React3.useCallback((docId, collection) => {
773
+ const key = `${collection}:${docId}`;
774
+ setSelectedIds((prev) => {
775
+ const next = new Set(prev);
776
+ if (next.has(key)) {
777
+ next.delete(key);
778
+ } else {
779
+ next.add(key);
780
+ }
781
+ return next;
782
+ });
783
+ }, []);
784
+ const [scanCurrent, setScanCurrent] = React3.useState(0);
785
+ const [scanTotal, setScanTotal] = React3.useState(0);
786
+ const [scanCurrentDoc, setScanCurrentDoc] = React3.useState("");
787
+ React3.useEffect(() => {
788
+ if (!scanning) return;
789
+ const interval = setInterval(async () => {
790
+ try {
791
+ const res = await fetch("/api/spellcheck/status");
792
+ if (!res.ok) return;
793
+ const data = await res.json();
794
+ if (data.status === "running") {
795
+ setScanCurrent(data.current || 0);
796
+ setScanTotal(data.total || 0);
797
+ setScanCurrentDoc(data.currentDoc || "");
798
+ setScanProgress(
799
+ `Analyse en cours... ${data.current}/${data.total} \u2014 ${data.currentDoc || ""}`
800
+ );
801
+ } else if (data.status === "completed") {
802
+ setScanProgress(
803
+ `Termin\xE9 \u2014 ${data.totalDocuments} documents, ${data.totalIssues} probl\xE8mes, score moyen ${data.averageScore}`
804
+ );
805
+ setScanning(false);
806
+ setScanCurrent(0);
807
+ setScanTotal(0);
808
+ setScanCurrentDoc("");
809
+ loadResults();
810
+ } else if (data.status === "error") {
811
+ setScanProgress(`Erreur : ${data.error || "Erreur inconnue"}`);
812
+ setScanning(false);
813
+ loadResults();
814
+ } else if (data.status === "idle") {
815
+ setScanProgress("");
816
+ setScanning(false);
817
+ loadResults();
818
+ }
819
+ } catch {
820
+ }
821
+ }, 2e3);
822
+ return () => clearInterval(interval);
823
+ }, [scanning, loadResults]);
824
+ React3.useEffect(() => {
825
+ fetch("/api/spellcheck/status").then((res) => res.json()).then((data) => {
826
+ if (data.status === "running") {
827
+ setScanning(true);
828
+ setScanCurrent(data.current || 0);
829
+ setScanTotal(data.total || 0);
830
+ setScanCurrentDoc(data.currentDoc || "");
831
+ setScanProgress(
832
+ `Analyse en cours... ${data.current}/${data.total} \u2014 ${data.currentDoc || ""}`
833
+ );
834
+ }
835
+ }).catch(() => {
836
+ });
837
+ }, []);
838
+ const handleForceReset = React3.useCallback(async () => {
839
+ try {
840
+ const res = await fetch("/api/spellcheck/bulk", {
841
+ method: "POST",
842
+ headers: { "Content-Type": "application/json" },
843
+ body: JSON.stringify({ force: true })
844
+ });
845
+ if (res.ok) {
846
+ setScanning(true);
847
+ setScanCurrent(0);
848
+ setScanTotal(0);
849
+ setScanProgress("Scan pr\xE9c\xE9dent r\xE9initialis\xE9, red\xE9marrage...");
850
+ }
851
+ } catch {
852
+ setScanProgress("Erreur r\xE9seau");
853
+ }
854
+ }, []);
855
+ const handleScan = React3.useCallback(async (onlySelected = false) => {
856
+ if (scanning) return;
857
+ setScanning(true);
858
+ setSelectedIds(/* @__PURE__ */ new Set());
859
+ setScanCurrent(0);
860
+ setScanTotal(0);
861
+ setScanProgress("D\xE9marrage de l'analyse...");
862
+ const idsToScan = onlySelected ? [...selectedIds].map((key) => {
863
+ const [collection, id] = key.split(":");
864
+ return { id, collection };
865
+ }) : void 0;
866
+ try {
867
+ const res = await fetch("/api/spellcheck/bulk", {
868
+ method: "POST",
869
+ headers: { "Content-Type": "application/json" },
870
+ body: JSON.stringify(idsToScan ? { ids: idsToScan } : {})
871
+ });
872
+ if (res.ok) {
873
+ setScanProgress("Analyse en cours... 0/? \u2014 d\xE9marrage");
874
+ } else if (res.status === 409) {
875
+ setScanProgress("Une analyse est d\xE9j\xE0 en cours...");
876
+ setScanning(false);
877
+ } else {
878
+ setScanProgress("Erreur lors du lancement de l'analyse");
879
+ setScanning(false);
880
+ }
881
+ } catch {
882
+ setScanProgress("Erreur r\xE9seau");
883
+ setScanning(false);
884
+ }
885
+ }, [scanning, selectedIds]);
886
+ const removeIssueFromResults = React3.useCallback((docId, collection, issueIdentifier) => {
887
+ const target = resultsRef.current.find(
888
+ (r) => r.docId === docId && r.collection === collection
889
+ );
890
+ if (!target) return;
891
+ const updatedIssues = target.issues.filter((issue) => {
892
+ if (typeof issueIdentifier.offset !== "number") return true;
893
+ if (issue.offset !== issueIdentifier.offset) return true;
894
+ if (issueIdentifier.original && issue.original !== issueIdentifier.original) return true;
895
+ if (issueIdentifier.ruleId && issue.ruleId !== issueIdentifier.ruleId) return true;
896
+ return false;
897
+ });
898
+ const updatedCount = updatedIssues.length;
899
+ const updatedScore = target.wordCount > 0 ? Math.min(100, Math.max(0, Math.round(100 - updatedCount / target.wordCount * 1e3))) : target.score;
900
+ setResults((prev) => prev.map((r) => {
901
+ if (r.id !== target.id) return r;
902
+ return { ...r, issues: updatedIssues, issueCount: updatedCount, score: updatedScore };
903
+ }));
904
+ if (target.id) {
905
+ fetch(`/api/spellcheck-results/${target.id}`, {
906
+ method: "PATCH",
907
+ headers: { "Content-Type": "application/json" },
908
+ body: JSON.stringify({ issues: updatedIssues, issueCount: updatedCount, score: updatedScore })
909
+ }).catch(() => {
910
+ });
911
+ }
912
+ }, []);
913
+ const handleFix = React3.useCallback(async (docId, collection, original, replacement, offset, length) => {
914
+ try {
915
+ const res = await fetch("/api/spellcheck/fix", {
916
+ method: "POST",
917
+ headers: { "Content-Type": "application/json" },
918
+ body: JSON.stringify({ id: docId, collection, original, replacement, offset, length })
919
+ });
920
+ if (res.ok) {
921
+ removeIssueFromResults(docId, collection, { offset, original });
922
+ }
923
+ } catch {
924
+ }
925
+ }, [removeIssueFromResults]);
926
+ const handleIgnore = React3.useCallback((docId, collection, ruleId, offset, original) => {
927
+ removeIssueFromResults(docId, collection, { offset, ruleId });
928
+ const target = resultsRef.current.find(
929
+ (r) => r.docId === docId && r.collection === collection
930
+ );
931
+ if (target?.id) {
932
+ fetch(`/api/spellcheck-results/${target.id}?depth=0`).then((res) => res.json()).then((data) => {
933
+ const existing = Array.isArray(data.ignoredIssues) ? data.ignoredIssues : [];
934
+ const alreadyIgnored = existing.some((e) => e.ruleId === ruleId && e.original === original);
935
+ if (!alreadyIgnored) {
936
+ fetch(`/api/spellcheck-results/${target.id}`, {
937
+ method: "PATCH",
938
+ headers: { "Content-Type": "application/json" },
939
+ body: JSON.stringify({ ignoredIssues: [...existing, { ruleId, original }] })
940
+ }).catch(() => {
941
+ });
942
+ }
943
+ }).catch(() => {
944
+ });
945
+ }
946
+ }, [removeIssueFromResults]);
947
+ const handleAddToDict = React3.useCallback(async (word) => {
948
+ try {
949
+ const res = await fetch("/api/spellcheck/dictionary", {
950
+ method: "POST",
951
+ headers: { "Content-Type": "application/json" },
952
+ body: JSON.stringify({ word })
953
+ });
954
+ if (res.ok) {
955
+ setDictToast(`"${word}" ajout\xE9 au dictionnaire`);
956
+ if (activeTab === "dictionary") loadDictionary();
957
+ }
958
+ } catch {
959
+ }
960
+ }, [activeTab, loadDictionary]);
961
+ const handleDictAdd = React3.useCallback(async () => {
962
+ if (!dictInput.trim()) return;
963
+ const words = dictInput.split(/[,;\n]+/).map((w) => w.trim()).filter(Boolean);
964
+ if (words.length === 0) return;
965
+ try {
966
+ const res = await fetch("/api/spellcheck/dictionary", {
967
+ method: "POST",
968
+ headers: { "Content-Type": "application/json" },
969
+ body: JSON.stringify({ words })
970
+ });
971
+ if (res.ok) {
972
+ const data = await res.json();
973
+ setDictToast(`${data.count} mot(s) ajout\xE9(s)${data.skipped.length > 0 ? `, ${data.skipped.length} doublon(s)` : ""}`);
974
+ setDictInput("");
975
+ loadDictionary();
976
+ }
977
+ } catch {
978
+ setDictToast("Erreur lors de l'ajout");
979
+ }
980
+ }, [dictInput, loadDictionary]);
981
+ const handleDictDelete = React3.useCallback(async (ids) => {
982
+ try {
983
+ const res = await fetch("/api/spellcheck/dictionary", {
984
+ method: "DELETE",
985
+ headers: { "Content-Type": "application/json" },
986
+ body: JSON.stringify({ ids })
987
+ });
988
+ if (res.ok) {
989
+ const data = await res.json();
990
+ setDictToast(`${data.deleted} mot(s) supprim\xE9(s)`);
991
+ setDictSelectedIds(/* @__PURE__ */ new Set());
992
+ loadDictionary();
993
+ }
994
+ } catch {
995
+ setDictToast("Erreur lors de la suppression");
996
+ }
997
+ }, [loadDictionary]);
998
+ const handleDictImport = React3.useCallback(async () => {
999
+ const words = importText.split(/[,;\n]+/).map((w) => w.trim()).filter(Boolean);
1000
+ if (words.length === 0) return;
1001
+ try {
1002
+ const res = await fetch("/api/spellcheck/dictionary", {
1003
+ method: "POST",
1004
+ headers: { "Content-Type": "application/json" },
1005
+ body: JSON.stringify({ words })
1006
+ });
1007
+ if (res.ok) {
1008
+ const data = await res.json();
1009
+ setDictToast(`${data.count} mot(s) import\xE9(s)${data.skipped.length > 0 ? `, ${data.skipped.length} doublon(s)` : ""}`);
1010
+ setImportText("");
1011
+ setShowImport(false);
1012
+ loadDictionary();
1013
+ }
1014
+ } catch {
1015
+ setDictToast("Erreur lors de l'import");
1016
+ }
1017
+ }, [importText, loadDictionary]);
1018
+ const handleDictExport = React3.useCallback(() => {
1019
+ const content = dictWords.map((w) => w.word).join("\n");
1020
+ const blob = new Blob([content], { type: "text/plain" });
1021
+ const url = URL.createObjectURL(blob);
1022
+ const a = document.createElement("a");
1023
+ a.href = url;
1024
+ a.download = "spellcheck-dictionary.txt";
1025
+ a.click();
1026
+ URL.revokeObjectURL(url);
1027
+ setDictToast("Dictionnaire export\xE9");
1028
+ }, [dictWords]);
1029
+ const handleSort = (key) => {
1030
+ if (sortKey === key) {
1031
+ setSortDir(sortDir === "asc" ? "desc" : "asc");
1032
+ } else {
1033
+ setSortKey(key);
1034
+ setSortDir(key === "score" ? "asc" : "desc");
1035
+ }
1036
+ };
1037
+ const mergedDocs = React3__default.default.useMemo(() => {
1038
+ const resultMap = /* @__PURE__ */ new Map();
1039
+ for (const r of results) {
1040
+ resultMap.set(`${r.collection}:${r.docId}`, r);
1041
+ }
1042
+ const seen = /* @__PURE__ */ new Set();
1043
+ const merged = [];
1044
+ for (const doc of allDocs) {
1045
+ const key = `${doc.collection}:${doc.id}`;
1046
+ seen.add(key);
1047
+ const existing = resultMap.get(key);
1048
+ merged.push({
1049
+ docId: doc.id,
1050
+ collection: doc.collection,
1051
+ title: doc.title,
1052
+ slug: doc.slug,
1053
+ score: existing?.score ?? null,
1054
+ issueCount: existing?.issueCount ?? 0,
1055
+ wordCount: existing?.wordCount ?? 0,
1056
+ issues: existing?.issues ?? [],
1057
+ lastChecked: existing?.lastChecked ?? null,
1058
+ resultId: existing?.id ?? null
1059
+ });
1060
+ }
1061
+ for (const r of results) {
1062
+ const key = `${r.collection}:${r.docId}`;
1063
+ if (!seen.has(key)) {
1064
+ merged.push({
1065
+ docId: r.docId,
1066
+ collection: r.collection,
1067
+ title: r.title,
1068
+ slug: r.slug,
1069
+ score: r.score,
1070
+ issueCount: r.issueCount,
1071
+ wordCount: r.wordCount,
1072
+ issues: r.issues,
1073
+ lastChecked: r.lastChecked,
1074
+ resultId: r.id
1075
+ });
1076
+ }
1077
+ }
1078
+ return merged;
1079
+ }, [results, allDocs]);
1080
+ const collections = React3__default.default.useMemo(() => {
1081
+ return [...new Set(mergedDocs.map((d) => d.collection))].sort();
1082
+ }, [mergedDocs]);
1083
+ const filteredMergedDocs = React3__default.default.useMemo(() => {
1084
+ return filterCollection ? mergedDocs.filter((d) => d.collection === filterCollection) : mergedDocs;
1085
+ }, [mergedDocs, filterCollection]);
1086
+ const sortedResults = [...filteredMergedDocs].sort((a, b) => {
1087
+ const dir = sortDir === "asc" ? 1 : -1;
1088
+ switch (sortKey) {
1089
+ case "title":
1090
+ return dir * (a.title || "").localeCompare(b.title || "");
1091
+ case "score":
1092
+ return dir * ((a.score ?? 101) - (b.score ?? 101));
1093
+ case "issueCount":
1094
+ return dir * (a.issueCount - b.issueCount);
1095
+ case "wordCount":
1096
+ return dir * ((a.wordCount || 0) - (b.wordCount || 0));
1097
+ case "lastChecked": {
1098
+ const aTime = a.lastChecked ? new Date(a.lastChecked).getTime() : 0;
1099
+ const bTime = b.lastChecked ? new Date(b.lastChecked).getTime() : 0;
1100
+ return dir * (aTime - bTime);
1101
+ }
1102
+ default:
1103
+ return 0;
1104
+ }
1105
+ });
1106
+ const scannedResults = results.filter((r) => !filterCollection || r.collection === filterCollection);
1107
+ const totalIssues = scannedResults.reduce((sum, r) => sum + r.issueCount, 0);
1108
+ const avgScore = scannedResults.length > 0 ? Math.round(scannedResults.reduce((sum, r) => sum + r.score, 0) / scannedResults.length) : 0;
1109
+ const sortIndicator = (key) => {
1110
+ if (sortKey !== key) return "";
1111
+ return sortDir === "asc" ? " \u2191" : " \u2193";
1112
+ };
1113
+ const docsWithErrors = React3__default.default.useMemo(() => {
1114
+ return filteredMergedDocs.filter((d) => d.issueCount > 0 && d.lastChecked);
1115
+ }, [filteredMergedDocs]);
1116
+ const handleToggleSelectAll = () => {
1117
+ if (selectedIds.size === filteredMergedDocs.length && filteredMergedDocs.length > 0) {
1118
+ setSelectedIds(/* @__PURE__ */ new Set());
1119
+ } else {
1120
+ setSelectedIds(new Set(filteredMergedDocs.map((d) => `${d.collection}:${d.docId}`)));
1121
+ }
1122
+ };
1123
+ const handleScanErrors = React3.useCallback(() => {
1124
+ if (scanning || docsWithErrors.length === 0) return;
1125
+ const errorIds = new Set(docsWithErrors.map((d) => `${d.collection}:${d.docId}`));
1126
+ setSelectedIds(errorIds);
1127
+ setTimeout(() => {
1128
+ setScanning(true);
1129
+ setScanCurrent(0);
1130
+ setScanTotal(0);
1131
+ setScanProgress("D\xE9marrage de l'analyse des pages avec erreurs...");
1132
+ const idsToScan = docsWithErrors.map((d) => ({ id: d.docId, collection: d.collection }));
1133
+ fetch("/api/spellcheck/bulk", {
1134
+ method: "POST",
1135
+ headers: { "Content-Type": "application/json" },
1136
+ body: JSON.stringify({ ids: idsToScan })
1137
+ }).then((res) => {
1138
+ if (res.ok) {
1139
+ setScanProgress(`Analyse en cours... 0/${idsToScan.length} \u2014 d\xE9marrage`);
1140
+ } else if (res.status === 409) {
1141
+ setScanProgress("Une analyse est d\xE9j\xE0 en cours...");
1142
+ } else {
1143
+ setScanProgress("Erreur lors du lancement");
1144
+ setScanning(false);
1145
+ }
1146
+ }).catch(() => {
1147
+ setScanProgress("Erreur r\xE9seau");
1148
+ setScanning(false);
1149
+ });
1150
+ }, 0);
1151
+ }, [scanning, docsWithErrors]);
1152
+ const filteredDictWords = React3__default.default.useMemo(() => {
1153
+ if (!dictSearch.trim()) return dictWords;
1154
+ const q = dictSearch.toLowerCase();
1155
+ return dictWords.filter((w) => w.word.toLowerCase().includes(q));
1156
+ }, [dictWords, dictSearch]);
1157
+ const handleDictToggleSelectAll = () => {
1158
+ if (dictSelectedIds.size === filteredDictWords.length && filteredDictWords.length > 0) {
1159
+ setDictSelectedIds(/* @__PURE__ */ new Set());
1160
+ } else {
1161
+ setDictSelectedIds(new Set(filteredDictWords.map((w) => w.id)));
1162
+ }
1163
+ };
1164
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles3.container, children: [
1165
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles3.header, children: [
1166
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1167
+ /* @__PURE__ */ jsxRuntime.jsx("h1", { style: styles3.title, children: "Correcteur orthographique" }),
1168
+ /* @__PURE__ */ jsxRuntime.jsx("p", { style: styles3.description, children: "Analyse orthographique et grammaticale du contenu via LanguageTool" })
1169
+ ] }),
1170
+ activeTab === "results" && /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", gap: "8px", alignItems: "center", flexWrap: "wrap" }, children: [
1171
+ selectedIds.size > 0 && /* @__PURE__ */ jsxRuntime.jsx(
1172
+ "button",
1173
+ {
1174
+ type: "button",
1175
+ style: {
1176
+ ...styles3.scanBtn,
1177
+ backgroundColor: "var(--theme-success-500)",
1178
+ ...scanning ? styles3.scanBtnDisabled : {}
1179
+ },
1180
+ onClick: () => handleScan(true),
1181
+ disabled: scanning,
1182
+ children: scanning ? "Analyse..." : `Scanner (${selectedIds.size})`
1183
+ }
1184
+ ),
1185
+ docsWithErrors.length > 0 && selectedIds.size === 0 && /* @__PURE__ */ jsxRuntime.jsx(
1186
+ "button",
1187
+ {
1188
+ type: "button",
1189
+ style: {
1190
+ ...styles3.scanBtn,
1191
+ backgroundColor: "var(--theme-error-500)",
1192
+ ...scanning ? styles3.scanBtnDisabled : {}
1193
+ },
1194
+ onClick: handleScanErrors,
1195
+ disabled: scanning,
1196
+ children: scanning ? "Analyse..." : `Rescanner erreurs (${docsWithErrors.length})`
1197
+ }
1198
+ ),
1199
+ /* @__PURE__ */ jsxRuntime.jsx(
1200
+ "button",
1201
+ {
1202
+ type: "button",
1203
+ style: {
1204
+ ...styles3.scanBtn,
1205
+ ...scanning ? styles3.scanBtnDisabled : {}
1206
+ },
1207
+ onClick: () => handleScan(false),
1208
+ disabled: scanning,
1209
+ children: scanning ? "Analyse..." : "Scanner tout"
1210
+ }
1211
+ )
1212
+ ] })
1213
+ ] }),
1214
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles3.tabs, children: [
1215
+ /* @__PURE__ */ jsxRuntime.jsx(
1216
+ "button",
1217
+ {
1218
+ type: "button",
1219
+ style: styles3.tab(activeTab === "results"),
1220
+ onClick: () => setActiveTab("results"),
1221
+ children: "R\xE9sultats"
1222
+ }
1223
+ ),
1224
+ /* @__PURE__ */ jsxRuntime.jsxs(
1225
+ "button",
1226
+ {
1227
+ type: "button",
1228
+ style: styles3.tab(activeTab === "dictionary"),
1229
+ onClick: () => setActiveTab("dictionary"),
1230
+ children: [
1231
+ "Dictionnaire ",
1232
+ dictWords.length > 0 && `(${dictWords.length})`
1233
+ ]
1234
+ }
1235
+ )
1236
+ ] }),
1237
+ dictToast && /* @__PURE__ */ jsxRuntime.jsx("div", { style: {
1238
+ padding: "8px 16px",
1239
+ marginBottom: "16px",
1240
+ borderRadius: "4px",
1241
+ backgroundColor: "var(--theme-success-100, #dcfce7)",
1242
+ border: "1px solid var(--theme-success-500)",
1243
+ color: "var(--theme-success-700, #15803d)",
1244
+ fontSize: "13px"
1245
+ }, children: dictToast }),
1246
+ activeTab === "results" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1247
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", gap: "12px", alignItems: "center", marginBottom: "16px", flexWrap: "wrap" }, children: [
1248
+ collections.length > 1 && /* @__PURE__ */ jsxRuntime.jsxs(
1249
+ "select",
1250
+ {
1251
+ value: filterCollection,
1252
+ onChange: (e) => {
1253
+ setFilterCollection(e.target.value);
1254
+ setSelectedIds(/* @__PURE__ */ new Set());
1255
+ },
1256
+ style: {
1257
+ padding: "6px 10px",
1258
+ fontSize: "13px",
1259
+ border: "1px solid var(--theme-elevation-200)",
1260
+ borderRadius: "4px",
1261
+ backgroundColor: "var(--theme-elevation-0)",
1262
+ color: "var(--theme-text)"
1263
+ },
1264
+ children: [
1265
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "", children: "Toutes les collections" }),
1266
+ collections.map((c) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: c, children: c }, c))
1267
+ ]
1268
+ }
1269
+ ),
1270
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { style: { fontSize: "12px", color: "var(--theme-elevation-500)" }, children: [
1271
+ filteredMergedDocs.length,
1272
+ " document(s)",
1273
+ selectedIds.size > 0 && ` \u2014 ${selectedIds.size} s\xE9lectionn\xE9(s)`
1274
+ ] })
1275
+ ] }),
1276
+ scanProgress && /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles3.progress, children: [
1277
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center" }, children: [
1278
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: scanProgress }),
1279
+ scanProgress.includes("d\xE9j\xE0 en cours") && !scanning && /* @__PURE__ */ jsxRuntime.jsx(
1280
+ "button",
1281
+ {
1282
+ type: "button",
1283
+ onClick: handleForceReset,
1284
+ style: {
1285
+ padding: "4px 12px",
1286
+ fontSize: "12px",
1287
+ fontWeight: 600,
1288
+ border: "none",
1289
+ borderRadius: "4px",
1290
+ backgroundColor: "var(--theme-error-500)",
1291
+ color: "#fff",
1292
+ cursor: "pointer",
1293
+ marginLeft: "12px"
1294
+ },
1295
+ children: "Forcer r\xE9initialisation"
1296
+ }
1297
+ )
1298
+ ] }),
1299
+ scanning && scanTotal > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles3.progressBar, children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles3.progressFill(Math.round(scanCurrent / scanTotal * 100)) }) })
1300
+ ] }),
1301
+ results.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles3.summary, children: [
1302
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles3.summaryCard, children: [
1303
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles3.summaryLabel, children: "Documents" }),
1304
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles3.summaryValue, children: results.length })
1305
+ ] }),
1306
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles3.summaryCard, children: [
1307
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles3.summaryLabel, children: "Score moyen" }),
1308
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles3.summaryValue, children: avgScore })
1309
+ ] }),
1310
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles3.summaryCard, children: [
1311
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles3.summaryLabel, children: "Probl\xE8mes" }),
1312
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles3.summaryValue, children: totalIssues })
1313
+ ] }),
1314
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles3.summaryCard, children: [
1315
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles3.summaryLabel, children: "Sans erreurs" }),
1316
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles3.summaryValue, children: results.filter((r) => r.issueCount === 0).length })
1317
+ ] })
1318
+ ] }),
1319
+ loading && loadingDocs ? /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles3.empty, children: "Chargement..." }) : sortedResults.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles3.empty, children: "Aucun document trouv\xE9. V\xE9rifiez la configuration des collections." }) : /* @__PURE__ */ jsxRuntime.jsxs("table", { style: styles3.table, children: [
1320
+ /* @__PURE__ */ jsxRuntime.jsx("thead", { children: /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
1321
+ /* @__PURE__ */ jsxRuntime.jsx("th", { style: { ...styles3.th, width: "36px" }, children: /* @__PURE__ */ jsxRuntime.jsx(
1322
+ "input",
1323
+ {
1324
+ type: "checkbox",
1325
+ checked: selectedIds.size === filteredMergedDocs.length && filteredMergedDocs.length > 0,
1326
+ onChange: handleToggleSelectAll,
1327
+ style: { cursor: "pointer" }
1328
+ }
1329
+ ) }),
1330
+ /* @__PURE__ */ jsxRuntime.jsxs("th", { style: styles3.th, onClick: () => handleSort("title"), children: [
1331
+ "Document",
1332
+ sortIndicator("title")
1333
+ ] }),
1334
+ /* @__PURE__ */ jsxRuntime.jsx("th", { style: styles3.th, children: "Collection" }),
1335
+ /* @__PURE__ */ jsxRuntime.jsxs("th", { style: styles3.th, onClick: () => handleSort("score"), children: [
1336
+ "Score",
1337
+ sortIndicator("score")
1338
+ ] }),
1339
+ /* @__PURE__ */ jsxRuntime.jsxs("th", { style: styles3.th, onClick: () => handleSort("issueCount"), children: [
1340
+ "Probl\xE8mes",
1341
+ sortIndicator("issueCount")
1342
+ ] }),
1343
+ /* @__PURE__ */ jsxRuntime.jsxs("th", { style: styles3.th, onClick: () => handleSort("wordCount"), children: [
1344
+ "Mots",
1345
+ sortIndicator("wordCount")
1346
+ ] }),
1347
+ /* @__PURE__ */ jsxRuntime.jsxs("th", { style: styles3.th, onClick: () => handleSort("lastChecked"), children: [
1348
+ "V\xE9rifi\xE9",
1349
+ sortIndicator("lastChecked")
1350
+ ] })
1351
+ ] }) }),
1352
+ /* @__PURE__ */ jsxRuntime.jsx("tbody", { children: sortedResults.map((r) => {
1353
+ const rowKey = `${r.collection}:${r.docId}`;
1354
+ const isSelected = selectedIds.has(rowKey);
1355
+ return /* @__PURE__ */ jsxRuntime.jsxs(React3__default.default.Fragment, { children: [
1356
+ /* @__PURE__ */ jsxRuntime.jsxs(
1357
+ "tr",
1358
+ {
1359
+ style: {
1360
+ ...styles3.tr,
1361
+ ...hoveredRow === rowKey ? styles3.trHover : {},
1362
+ ...isSelected ? { backgroundColor: "var(--theme-elevation-100)" } : {}
1363
+ },
1364
+ onClick: () => {
1365
+ if (r.issues && r.issues.length > 0) {
1366
+ setExpandedId(expandedId === rowKey ? null : rowKey);
1367
+ }
1368
+ },
1369
+ onMouseEnter: () => setHoveredRow(rowKey),
1370
+ onMouseLeave: () => setHoveredRow(null),
1371
+ children: [
1372
+ /* @__PURE__ */ jsxRuntime.jsx("td", { style: styles3.td, children: /* @__PURE__ */ jsxRuntime.jsx(
1373
+ "input",
1374
+ {
1375
+ type: "checkbox",
1376
+ checked: isSelected,
1377
+ onChange: () => toggleSelect(r.docId, r.collection),
1378
+ onClick: (e) => e.stopPropagation(),
1379
+ style: { cursor: "pointer" }
1380
+ }
1381
+ ) }),
1382
+ /* @__PURE__ */ jsxRuntime.jsx("td", { style: styles3.td, children: /* @__PURE__ */ jsxRuntime.jsx(
1383
+ "a",
1384
+ {
1385
+ href: `/admin/collections/${r.collection}/${r.docId}`,
1386
+ style: styles3.link,
1387
+ onClick: (e) => e.stopPropagation(),
1388
+ children: r.title || r.slug || r.docId
1389
+ }
1390
+ ) }),
1391
+ /* @__PURE__ */ jsxRuntime.jsx("td", { style: styles3.td, children: r.collection }),
1392
+ /* @__PURE__ */ jsxRuntime.jsx("td", { style: styles3.td, children: r.score !== null ? /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles3.scoreBadge(r.score), children: r.score }) : /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: "11px", color: "var(--theme-elevation-400)" }, children: "\u2014" }) }),
1393
+ /* @__PURE__ */ jsxRuntime.jsx("td", { style: styles3.td, children: r.lastChecked ? r.issueCount : "\u2014" }),
1394
+ /* @__PURE__ */ jsxRuntime.jsx("td", { style: styles3.td, children: r.wordCount || "\u2014" }),
1395
+ /* @__PURE__ */ jsxRuntime.jsx("td", { style: styles3.td, children: r.lastChecked ? new Date(r.lastChecked).toLocaleString("fr-FR", {
1396
+ day: "2-digit",
1397
+ month: "2-digit",
1398
+ hour: "2-digit",
1399
+ minute: "2-digit",
1400
+ second: "2-digit"
1401
+ }) : "\u2014" })
1402
+ ]
1403
+ }
1404
+ ),
1405
+ expandedId === rowKey && r.issues && r.issues.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("tr", { children: /* @__PURE__ */ jsxRuntime.jsx("td", { colSpan: 7, style: styles3.expandedRow, children: r.issues.map((issue, i) => /* @__PURE__ */ jsxRuntime.jsx(
1406
+ IssueCard,
1407
+ {
1408
+ issue,
1409
+ onFix: (original, replacement, offset, length) => handleFix(r.docId, r.collection, original, replacement, offset, length),
1410
+ onIgnore: () => handleIgnore(r.docId, r.collection, issue.ruleId, issue.offset, issue.original),
1411
+ onAddToDict: handleAddToDict
1412
+ },
1413
+ `${issue.ruleId}-${issue.offset}-${i}`
1414
+ )) }) })
1415
+ ] }, rowKey);
1416
+ }) })
1417
+ ] })
1418
+ ] }),
1419
+ activeTab === "dictionary" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1420
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", gap: "8px", marginBottom: "16px", alignItems: "center" }, children: [
1421
+ /* @__PURE__ */ jsxRuntime.jsx(
1422
+ "input",
1423
+ {
1424
+ type: "text",
1425
+ value: dictInput,
1426
+ onChange: (e) => setDictInput(e.target.value),
1427
+ onKeyDown: (e) => {
1428
+ if (e.key === "Enter") handleDictAdd();
1429
+ },
1430
+ placeholder: "Ajouter un mot (virgules pour plusieurs)...",
1431
+ style: {
1432
+ flex: 1,
1433
+ padding: "8px 12px",
1434
+ fontSize: "13px",
1435
+ border: "1px solid var(--theme-elevation-200)",
1436
+ borderRadius: "4px",
1437
+ backgroundColor: "var(--theme-elevation-0)",
1438
+ color: "var(--theme-text)"
1439
+ }
1440
+ }
1441
+ ),
1442
+ /* @__PURE__ */ jsxRuntime.jsx(
1443
+ "button",
1444
+ {
1445
+ type: "button",
1446
+ onClick: handleDictAdd,
1447
+ disabled: !dictInput.trim(),
1448
+ style: {
1449
+ ...styles3.scanBtn,
1450
+ padding: "8px 16px",
1451
+ fontSize: "13px",
1452
+ ...!dictInput.trim() ? styles3.scanBtnDisabled : {}
1453
+ },
1454
+ children: "Ajouter"
1455
+ }
1456
+ )
1457
+ ] }),
1458
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", gap: "8px", marginBottom: "16px", alignItems: "center", flexWrap: "wrap" }, children: [
1459
+ /* @__PURE__ */ jsxRuntime.jsx(
1460
+ "input",
1461
+ {
1462
+ type: "text",
1463
+ value: dictSearch,
1464
+ onChange: (e) => setDictSearch(e.target.value),
1465
+ placeholder: "Rechercher...",
1466
+ style: {
1467
+ padding: "6px 10px",
1468
+ fontSize: "13px",
1469
+ border: "1px solid var(--theme-elevation-200)",
1470
+ borderRadius: "4px",
1471
+ backgroundColor: "var(--theme-elevation-0)",
1472
+ color: "var(--theme-text)",
1473
+ width: "200px"
1474
+ }
1475
+ }
1476
+ ),
1477
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { style: { fontSize: "12px", color: "var(--theme-elevation-500)" }, children: [
1478
+ filteredDictWords.length,
1479
+ " mot(s)",
1480
+ dictSelectedIds.size > 0 && ` \u2014 ${dictSelectedIds.size} s\xE9lectionn\xE9(s)`
1481
+ ] }),
1482
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { flex: 1 } }),
1483
+ dictSelectedIds.size > 0 && /* @__PURE__ */ jsxRuntime.jsxs(
1484
+ "button",
1485
+ {
1486
+ type: "button",
1487
+ onClick: () => handleDictDelete([...dictSelectedIds]),
1488
+ style: {
1489
+ ...styles3.scanBtn,
1490
+ padding: "6px 14px",
1491
+ fontSize: "12px",
1492
+ backgroundColor: "var(--theme-error-500)"
1493
+ },
1494
+ children: [
1495
+ "Supprimer (",
1496
+ dictSelectedIds.size,
1497
+ ")"
1498
+ ]
1499
+ }
1500
+ ),
1501
+ /* @__PURE__ */ jsxRuntime.jsx(
1502
+ "button",
1503
+ {
1504
+ type: "button",
1505
+ onClick: () => setShowImport(!showImport),
1506
+ style: {
1507
+ ...styles3.scanBtn,
1508
+ padding: "6px 14px",
1509
+ fontSize: "12px",
1510
+ backgroundColor: "var(--theme-elevation-600)"
1511
+ },
1512
+ children: showImport ? "Annuler" : "Importer"
1513
+ }
1514
+ ),
1515
+ /* @__PURE__ */ jsxRuntime.jsx(
1516
+ "button",
1517
+ {
1518
+ type: "button",
1519
+ onClick: handleDictExport,
1520
+ disabled: dictWords.length === 0,
1521
+ style: {
1522
+ ...styles3.scanBtn,
1523
+ padding: "6px 14px",
1524
+ fontSize: "12px",
1525
+ backgroundColor: "var(--theme-elevation-600)",
1526
+ ...dictWords.length === 0 ? styles3.scanBtnDisabled : {}
1527
+ },
1528
+ children: "Exporter"
1529
+ }
1530
+ )
1531
+ ] }),
1532
+ showImport && /* @__PURE__ */ jsxRuntime.jsxs("div", { style: {
1533
+ marginBottom: "16px",
1534
+ padding: "16px",
1535
+ borderRadius: "6px",
1536
+ backgroundColor: "var(--theme-elevation-50)",
1537
+ border: "1px solid var(--theme-elevation-150)"
1538
+ }, children: [
1539
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: "13px", marginBottom: "8px", color: "var(--theme-text)" }, children: "Un mot par ligne, ou s\xE9par\xE9s par des virgules :" }),
1540
+ /* @__PURE__ */ jsxRuntime.jsx(
1541
+ "textarea",
1542
+ {
1543
+ value: importText,
1544
+ onChange: (e) => setImportText(e.target.value),
1545
+ rows: 6,
1546
+ style: {
1547
+ width: "100%",
1548
+ padding: "8px",
1549
+ fontSize: "13px",
1550
+ border: "1px solid var(--theme-elevation-200)",
1551
+ borderRadius: "4px",
1552
+ backgroundColor: "var(--theme-elevation-0)",
1553
+ color: "var(--theme-text)",
1554
+ fontFamily: "monospace",
1555
+ boxSizing: "border-box",
1556
+ resize: "vertical"
1557
+ },
1558
+ placeholder: "mot1\nmot2\nmot3, mot4, mot5"
1559
+ }
1560
+ ),
1561
+ /* @__PURE__ */ jsxRuntime.jsxs(
1562
+ "button",
1563
+ {
1564
+ type: "button",
1565
+ onClick: handleDictImport,
1566
+ disabled: !importText.trim(),
1567
+ style: {
1568
+ ...styles3.scanBtn,
1569
+ marginTop: "8px",
1570
+ padding: "8px 16px",
1571
+ fontSize: "13px",
1572
+ ...!importText.trim() ? styles3.scanBtnDisabled : {}
1573
+ },
1574
+ children: [
1575
+ "Importer ",
1576
+ importText.trim() ? `(${importText.split(/[,;\n]+/).filter((w) => w.trim()).length} mots)` : ""
1577
+ ]
1578
+ }
1579
+ )
1580
+ ] }),
1581
+ dictLoading ? /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles3.empty, children: "Chargement du dictionnaire..." }) : filteredDictWords.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles3.empty, children: dictSearch ? "Aucun mot trouv\xE9." : "Dictionnaire vide. Ajoutez des mots ci-dessus." }) : /* @__PURE__ */ jsxRuntime.jsxs("table", { style: styles3.table, children: [
1582
+ /* @__PURE__ */ jsxRuntime.jsx("thead", { children: /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
1583
+ /* @__PURE__ */ jsxRuntime.jsx("th", { style: { ...styles3.th, width: "36px", cursor: "default" }, children: /* @__PURE__ */ jsxRuntime.jsx(
1584
+ "input",
1585
+ {
1586
+ type: "checkbox",
1587
+ checked: dictSelectedIds.size === filteredDictWords.length && filteredDictWords.length > 0,
1588
+ onChange: handleDictToggleSelectAll,
1589
+ style: { cursor: "pointer" }
1590
+ }
1591
+ ) }),
1592
+ /* @__PURE__ */ jsxRuntime.jsx("th", { style: { ...styles3.th, cursor: "default" }, children: "Mot" }),
1593
+ /* @__PURE__ */ jsxRuntime.jsx("th", { style: { ...styles3.th, cursor: "default" }, children: "Ajout\xE9 par" }),
1594
+ /* @__PURE__ */ jsxRuntime.jsx("th", { style: { ...styles3.th, cursor: "default" }, children: "Date" }),
1595
+ /* @__PURE__ */ jsxRuntime.jsx("th", { style: { ...styles3.th, width: "60px", cursor: "default" } })
1596
+ ] }) }),
1597
+ /* @__PURE__ */ jsxRuntime.jsx("tbody", { children: filteredDictWords.map((w) => {
1598
+ const isSelected = dictSelectedIds.has(w.id);
1599
+ const addedByName = w.addedBy && typeof w.addedBy === "object" ? w.addedBy.name || w.addedBy.email || "\u2014" : "\u2014";
1600
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1601
+ "tr",
1602
+ {
1603
+ style: {
1604
+ ...isSelected ? { backgroundColor: "var(--theme-elevation-100)" } : {}
1605
+ },
1606
+ children: [
1607
+ /* @__PURE__ */ jsxRuntime.jsx("td", { style: styles3.td, children: /* @__PURE__ */ jsxRuntime.jsx(
1608
+ "input",
1609
+ {
1610
+ type: "checkbox",
1611
+ checked: isSelected,
1612
+ onChange: () => {
1613
+ setDictSelectedIds((prev) => {
1614
+ const next = new Set(prev);
1615
+ if (next.has(w.id)) next.delete(w.id);
1616
+ else next.add(w.id);
1617
+ return next;
1618
+ });
1619
+ },
1620
+ style: { cursor: "pointer" }
1621
+ }
1622
+ ) }),
1623
+ /* @__PURE__ */ jsxRuntime.jsx("td", { style: { ...styles3.td, fontFamily: "monospace", fontWeight: 500 }, children: w.word }),
1624
+ /* @__PURE__ */ jsxRuntime.jsx("td", { style: { ...styles3.td, fontSize: "12px", color: "var(--theme-elevation-500)" }, children: addedByName }),
1625
+ /* @__PURE__ */ jsxRuntime.jsx("td", { style: { ...styles3.td, fontSize: "12px", color: "var(--theme-elevation-500)" }, children: w.createdAt ? new Date(w.createdAt).toLocaleString("fr-FR", {
1626
+ day: "2-digit",
1627
+ month: "2-digit",
1628
+ year: "2-digit"
1629
+ }) : "\u2014" }),
1630
+ /* @__PURE__ */ jsxRuntime.jsx("td", { style: styles3.td, children: /* @__PURE__ */ jsxRuntime.jsx(
1631
+ "button",
1632
+ {
1633
+ type: "button",
1634
+ onClick: () => handleDictDelete([w.id]),
1635
+ style: {
1636
+ background: "none",
1637
+ border: "none",
1638
+ cursor: "pointer",
1639
+ color: "var(--theme-error-500)",
1640
+ fontSize: "14px",
1641
+ padding: "2px 6px"
1642
+ },
1643
+ title: "Supprimer",
1644
+ children: "\u2715"
1645
+ }
1646
+ ) })
1647
+ ]
1648
+ },
1649
+ w.id
1650
+ );
1651
+ }) })
1652
+ ] })
1653
+ ] })
1654
+ ] });
1655
+ };
1656
+ var SpellCheckScoreCell = ({
1657
+ rowData,
1658
+ collectionSlug
1659
+ }) => {
1660
+ const [score, setScore] = React3.useState(null);
1661
+ const [issueCount, setIssueCount] = React3.useState(null);
1662
+ React3.useEffect(() => {
1663
+ if (!rowData?.id || !collectionSlug) return;
1664
+ fetch(
1665
+ `/api/spellcheck-results?where[docId][equals]=${rowData.id}&where[collection][equals]=${collectionSlug}&limit=1&depth=0`
1666
+ ).then((res) => res.json()).then((data) => {
1667
+ if (data.docs?.[0]) {
1668
+ setScore(data.docs[0].score);
1669
+ setIssueCount(data.docs[0].issueCount ?? 0);
1670
+ }
1671
+ }).catch(() => {
1672
+ });
1673
+ }, [rowData?.id, collectionSlug]);
1674
+ if (score === null) {
1675
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: "12px", color: "var(--theme-elevation-400)" }, children: "\u2014" });
1676
+ }
1677
+ const bgColor = score >= 95 ? "var(--theme-success-500)" : score >= 80 ? "#f59e0b" : "var(--theme-error-500)";
1678
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1679
+ "span",
1680
+ {
1681
+ style: {
1682
+ display: "inline-flex",
1683
+ alignItems: "center",
1684
+ gap: "6px"
1685
+ },
1686
+ children: [
1687
+ /* @__PURE__ */ jsxRuntime.jsx(
1688
+ "span",
1689
+ {
1690
+ style: {
1691
+ display: "inline-block",
1692
+ padding: "2px 8px",
1693
+ borderRadius: "10px",
1694
+ fontSize: "12px",
1695
+ fontWeight: 700,
1696
+ color: "#fff",
1697
+ backgroundColor: bgColor
1698
+ },
1699
+ children: score
1700
+ }
1701
+ ),
1702
+ issueCount !== null && issueCount > 0 && /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: "11px", color: "var(--theme-error-500)" }, children: issueCount })
1703
+ ]
1704
+ }
1705
+ );
1706
+ };
1707
+
1708
+ exports.IssueCard = IssueCard;
1709
+ exports.SpellCheckDashboard = SpellCheckDashboard;
1710
+ exports.SpellCheckField = SpellCheckField;
1711
+ exports.SpellCheckScoreCell = SpellCheckScoreCell;