@semiont/react-ui 0.5.24 → 0.5.25
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/{PdfAnnotationCanvas.client-V7Q3BEEQ.js → PdfAnnotationCanvas.client-EHQWF6BR.js} +45 -10
- package/dist/PdfAnnotationCanvas.client-EHQWF6BR.js.map +1 -0
- package/dist/{ar-JRTKCMTF.js → ar-CULRHLNG.js} +20 -6
- package/dist/ar-CULRHLNG.js.map +1 -0
- package/dist/{bn-LO6EDSCC.js → bn-HYR7CEL2.js} +20 -6
- package/dist/bn-HYR7CEL2.js.map +1 -0
- package/dist/{chunk-5Y5JDCKC.js → chunk-NH6L2GIL.js} +20 -6
- package/dist/chunk-NH6L2GIL.js.map +1 -0
- package/dist/{chunk-4QMTEPQ3.js → chunk-TG2XQEC4.js} +31 -31
- package/dist/{cs-LCHHOR4O.js → cs-YBRWY7NH.js} +20 -6
- package/dist/cs-YBRWY7NH.js.map +1 -0
- package/dist/{da-TARGPMJR.js → da-PHDEOBV3.js} +20 -6
- package/dist/da-PHDEOBV3.js.map +1 -0
- package/dist/{de-SBH4QBPJ.js → de-O4UCCVLJ.js} +20 -6
- package/dist/de-O4UCCVLJ.js.map +1 -0
- package/dist/{el-2D2OLGWB.js → el-MVGO6WZV.js} +20 -6
- package/dist/el-MVGO6WZV.js.map +1 -0
- package/dist/{en-QFIEG3CA.js → en-2KQILFKQ.js} +4 -2
- package/dist/{es-U6YK2LX6.js → es-USSLXQY7.js} +20 -6
- package/dist/es-USSLXQY7.js.map +1 -0
- package/dist/{fa-S7SZE37M.js → fa-WMAREFWY.js} +20 -6
- package/dist/fa-WMAREFWY.js.map +1 -0
- package/dist/{fi-CBL3NIXN.js → fi-B3NBNOTS.js} +20 -6
- package/dist/fi-B3NBNOTS.js.map +1 -0
- package/dist/{fr-EABUHV6S.js → fr-TXYK5T5W.js} +20 -6
- package/dist/fr-TXYK5T5W.js.map +1 -0
- package/dist/{he-THWD244Y.js → he-JCJACW5D.js} +20 -6
- package/dist/he-JCJACW5D.js.map +1 -0
- package/dist/{hi-GHGFZJ44.js → hi-VOQLZNJU.js} +20 -6
- package/dist/hi-VOQLZNJU.js.map +1 -0
- package/dist/{id-POZOUMDW.js → id-BFN5RZNZ.js} +20 -6
- package/dist/id-BFN5RZNZ.js.map +1 -0
- package/dist/index.css +0 -36
- package/dist/index.css.map +1 -1
- package/dist/index.d.ts +85 -24
- package/dist/index.js +1763 -1690
- package/dist/index.js.map +1 -1
- package/dist/{it-KDYJADSH.js → it-TWWDHKKM.js} +20 -6
- package/dist/it-TWWDHKKM.js.map +1 -0
- package/dist/{ja-Z5Z6ZF3P.js → ja-YRQLEBFM.js} +20 -6
- package/dist/ja-YRQLEBFM.js.map +1 -0
- package/dist/{ko-7X3XUXRN.js → ko-PG6B3CRB.js} +20 -6
- package/dist/ko-PG6B3CRB.js.map +1 -0
- package/dist/{ms-BTWEIW43.js → ms-7EZECVKM.js} +20 -6
- package/dist/ms-7EZECVKM.js.map +1 -0
- package/dist/{nl-IS2CGF5I.js → nl-HP753YAP.js} +20 -6
- package/dist/nl-HP753YAP.js.map +1 -0
- package/dist/{no-AAMEVA26.js → no-JUD5WX3N.js} +20 -6
- package/dist/no-JUD5WX3N.js.map +1 -0
- package/dist/{pl-PVXUQYFT.js → pl-RVUL4CBM.js} +20 -6
- package/dist/pl-RVUL4CBM.js.map +1 -0
- package/dist/{pt-3ITQ76TL.js → pt-RPOVD4QM.js} +20 -6
- package/dist/pt-RPOVD4QM.js.map +1 -0
- package/dist/{ro-FRB4IIVS.js → ro-DMYWQTRD.js} +20 -6
- package/dist/ro-DMYWQTRD.js.map +1 -0
- package/dist/{sv-DS3RO24M.js → sv-DMZ4FGXD.js} +20 -6
- package/dist/sv-DMZ4FGXD.js.map +1 -0
- package/dist/test-utils.js +2 -2
- package/dist/{th-AX66MWWB.js → th-4HVBJBQ2.js} +20 -6
- package/dist/th-4HVBJBQ2.js.map +1 -0
- package/dist/{tr-7KTB67TT.js → tr-6BDPU5ML.js} +20 -6
- package/dist/tr-6BDPU5ML.js.map +1 -0
- package/dist/{uk-SMQS2MAQ.js → uk-TBZBT2TL.js} +20 -6
- package/dist/uk-TBZBT2TL.js.map +1 -0
- package/dist/{vi-D3JXL2EZ.js → vi-UMKZKZL3.js} +20 -6
- package/dist/vi-UMKZKZL3.js.map +1 -0
- package/dist/{zh-S754AWFS.js → zh-YBJWJJYB.js} +20 -6
- package/dist/zh-YBJWJJYB.js.map +1 -0
- package/package.json +8 -8
- package/src/components/AssistProgress.tsx +42 -30
- package/src/components/EntityFoundLog.tsx +38 -0
- package/src/components/__tests__/AssistProgress.test.tsx +86 -28
- package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +112 -12
- package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +247 -1
- package/src/components/resource/AnnotateView.tsx +14 -7
- package/src/components/resource/BrowseView.tsx +2 -7
- package/src/components/resource/DownloadFileLink.tsx +49 -0
- package/src/components/resource/__tests__/AnnotateView.focus.test.tsx +58 -0
- package/src/components/resource/__tests__/download-link.test.tsx +129 -0
- package/src/components/resource/panels/AssistSection.css +0 -43
- package/src/components/resource/panels/AssistSection.tsx +11 -2
- package/src/components/resource/panels/AssistShell.tsx +5 -1
- package/src/components/resource/panels/ReferencesPanel.tsx +12 -13
- package/src/components/resource/panels/TaggingPanel.tsx +21 -1
- package/src/components/resource/panels/__tests__/AssistSection.test.tsx +2 -2
- package/src/components/resource/panels/__tests__/AssistShell.test.tsx +9 -4
- package/src/components/resource/panels/__tests__/HighlightPanel.annotationProgress.test.tsx +1 -1
- package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +26 -0
- package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +32 -3
- package/src/features/resource-viewer/components/ResourceViewerPage.tsx +17 -6
- package/translations/ar.json +17 -5
- package/translations/bn.json +17 -5
- package/translations/cs.json +17 -5
- package/translations/da.json +17 -5
- package/translations/de.json +17 -5
- package/translations/el.json +17 -5
- package/translations/en.json +17 -5
- package/translations/es.json +17 -5
- package/translations/fa.json +17 -5
- package/translations/fi.json +17 -5
- package/translations/fr.json +17 -5
- package/translations/he.json +17 -5
- package/translations/hi.json +17 -5
- package/translations/id.json +17 -5
- package/translations/it.json +17 -5
- package/translations/ja.json +17 -5
- package/translations/ko.json +17 -5
- package/translations/ms.json +17 -5
- package/translations/nl.json +17 -5
- package/translations/no.json +17 -5
- package/translations/pl.json +17 -5
- package/translations/pt.json +17 -5
- package/translations/ro.json +17 -5
- package/translations/sv.json +17 -5
- package/translations/th.json +17 -5
- package/translations/tr.json +17 -5
- package/translations/uk.json +17 -5
- package/translations/vi.json +17 -5
- package/translations/zh.json +17 -5
- package/dist/PdfAnnotationCanvas.client-V7Q3BEEQ.js.map +0 -1
- package/dist/ar-JRTKCMTF.js.map +0 -1
- package/dist/bn-LO6EDSCC.js.map +0 -1
- package/dist/chunk-5Y5JDCKC.js.map +0 -1
- package/dist/cs-LCHHOR4O.js.map +0 -1
- package/dist/da-TARGPMJR.js.map +0 -1
- package/dist/de-SBH4QBPJ.js.map +0 -1
- package/dist/el-2D2OLGWB.js.map +0 -1
- package/dist/es-U6YK2LX6.js.map +0 -1
- package/dist/fa-S7SZE37M.js.map +0 -1
- package/dist/fi-CBL3NIXN.js.map +0 -1
- package/dist/fr-EABUHV6S.js.map +0 -1
- package/dist/he-THWD244Y.js.map +0 -1
- package/dist/hi-GHGFZJ44.js.map +0 -1
- package/dist/id-POZOUMDW.js.map +0 -1
- package/dist/it-KDYJADSH.js.map +0 -1
- package/dist/ja-Z5Z6ZF3P.js.map +0 -1
- package/dist/ko-7X3XUXRN.js.map +0 -1
- package/dist/ms-BTWEIW43.js.map +0 -1
- package/dist/nl-IS2CGF5I.js.map +0 -1
- package/dist/no-AAMEVA26.js.map +0 -1
- package/dist/pl-PVXUQYFT.js.map +0 -1
- package/dist/pt-3ITQ76TL.js.map +0 -1
- package/dist/ro-FRB4IIVS.js.map +0 -1
- package/dist/sv-DS3RO24M.js.map +0 -1
- package/dist/th-AX66MWWB.js.map +0 -1
- package/dist/tr-7KTB67TT.js.map +0 -1
- package/dist/uk-SMQS2MAQ.js.map +0 -1
- package/dist/vi-D3JXL2EZ.js.map +0 -1
- package/dist/zh-S754AWFS.js.map +0 -1
- /package/dist/{chunk-4QMTEPQ3.js.map → chunk-TG2XQEC4.js.map} +0 -0
- /package/dist/{en-QFIEG3CA.js.map → en-2KQILFKQ.js.map} +0 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The "Download File" affordance on the unsupported-media fallback, in BOTH
|
|
3
|
+
* resource views.
|
|
4
|
+
*
|
|
5
|
+
* `GET /api/resources/:id` is the browser-facing alias of the pipe, and it
|
|
6
|
+
* exists only as an auth affordance for `<img>`, PDF.js and download links: a
|
|
7
|
+
* plain `<a download href>` sends neither an Authorization header nor a cookie,
|
|
8
|
+
* so the alias takes bearer + `?token=` only. Both views shipped a bare
|
|
9
|
+
* `/api/resources/${id}` — tokenless, so a 401, AND relative, so under
|
|
10
|
+
* bring-your-own-session embedding it resolves against the HOST app's origin
|
|
11
|
+
* rather than the backend's.
|
|
12
|
+
*
|
|
13
|
+
* Started RED (both hrefs were `/api/resources/res-1`).
|
|
14
|
+
*/
|
|
15
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
16
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
17
|
+
import '@testing-library/jest-dom';
|
|
18
|
+
import type { SemiontSession } from '@semiont/sdk';
|
|
19
|
+
import type { AnnotationUIState } from '../../../types/annotation-props';
|
|
20
|
+
import { AnnotateView } from '../AnnotateView';
|
|
21
|
+
import { BrowseView } from '../BrowseView';
|
|
22
|
+
|
|
23
|
+
vi.mock('../../annotation/AnnotateToolbar', () => ({ AnnotateToolbar: () => null }));
|
|
24
|
+
vi.mock('../../CodeMirrorRenderer', () => ({ CodeMirrorRenderer: () => null }));
|
|
25
|
+
vi.mock('../../image-annotation/SvgDrawingCanvas', () => ({ SvgDrawingCanvas: () => null }));
|
|
26
|
+
vi.mock('react-markdown', () => ({ default: ({ children }: { children: string }) => <div>{children}</div> }));
|
|
27
|
+
vi.mock('remark-gfm', () => ({ default: () => ({}) }));
|
|
28
|
+
|
|
29
|
+
const BACKEND = 'http://backend.test:4000';
|
|
30
|
+
const UNSUPPORTED = 'application/octet-stream';
|
|
31
|
+
|
|
32
|
+
const emptyAnnotations = { highlights: [], references: [], assessments: [], comments: [], tags: [] };
|
|
33
|
+
const uiState: AnnotationUIState = {
|
|
34
|
+
selectedMotivation: null,
|
|
35
|
+
selectedClick: 'detail',
|
|
36
|
+
selectedShape: 'rectangle',
|
|
37
|
+
hoveredAnnotationId: null,
|
|
38
|
+
scrollToAnnotationId: null,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** A session whose client mints `token` — the batteries-included case. */
|
|
42
|
+
function sessionMinting(token: string): SemiontSession {
|
|
43
|
+
return fakeSession({ auth: { mediaToken: vi.fn(async () => ({ token })) } });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** A session whose token request never settles — the in-flight case. */
|
|
47
|
+
function sessionPending(): SemiontSession {
|
|
48
|
+
return fakeSession({ auth: { mediaToken: vi.fn(() => new Promise<never>(() => {})) } });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* A transport-only client: `SemiontClient.auth` is `AuthNamespace | undefined`,
|
|
53
|
+
* and a host wiring a bare transport (no `IBackendOperations`) has no `auth`.
|
|
54
|
+
*/
|
|
55
|
+
function sessionWithoutAuth(): SemiontSession {
|
|
56
|
+
return fakeSession({});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function fakeSession(clientExtras: Record<string, unknown>): SemiontSession {
|
|
60
|
+
return {
|
|
61
|
+
client: {
|
|
62
|
+
baseUrl: BACKEND,
|
|
63
|
+
mark: { request: vi.fn() },
|
|
64
|
+
browse: { click: vi.fn() },
|
|
65
|
+
beckon: { hover: vi.fn() },
|
|
66
|
+
...clientExtras,
|
|
67
|
+
},
|
|
68
|
+
subscribe: () => () => {},
|
|
69
|
+
} as unknown as SemiontSession;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const annotateProps = (session: SemiontSession) => ({
|
|
73
|
+
content: '',
|
|
74
|
+
mimeType: UNSUPPORTED,
|
|
75
|
+
resourceUri: 'res-1',
|
|
76
|
+
annotations: emptyAnnotations,
|
|
77
|
+
uiState,
|
|
78
|
+
annotateMode: false,
|
|
79
|
+
session,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const browseProps = (session: SemiontSession) => ({
|
|
83
|
+
content: '',
|
|
84
|
+
mimeType: UNSUPPORTED,
|
|
85
|
+
resourceUri: 'res-1',
|
|
86
|
+
annotations: emptyAnnotations,
|
|
87
|
+
annotateMode: false,
|
|
88
|
+
session,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const VIEWS = [
|
|
92
|
+
{ name: 'AnnotateView', render: (s: SemiontSession) => render(<AnnotateView {...annotateProps(s)} />) },
|
|
93
|
+
{ name: 'BrowseView', render: (s: SemiontSession) => render(<BrowseView {...browseProps(s)} />) },
|
|
94
|
+
] as const;
|
|
95
|
+
|
|
96
|
+
describe.each(VIEWS)('$name — unsupported-media download link', ({ render: renderView }) => {
|
|
97
|
+
it('points at the backend origin and carries the media token', async () => {
|
|
98
|
+
renderView(sessionMinting('tok-abc'));
|
|
99
|
+
|
|
100
|
+
const link = await screen.findByRole('link', { name: 'Download File' });
|
|
101
|
+
expect(link).toHaveAttribute('href', `${BACKEND}/api/resources/res-1?token=tok-abc`);
|
|
102
|
+
expect(link).toHaveAttribute('download');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('renders no href while the token is still in flight', async () => {
|
|
106
|
+
renderView(sessionPending());
|
|
107
|
+
|
|
108
|
+
// The affordance stays on screen — it is just not yet actionable. What it
|
|
109
|
+
// must NOT do is offer a URL already known to fail.
|
|
110
|
+
expect(await screen.findByText('Download File')).toBeInTheDocument();
|
|
111
|
+
expect(screen.queryByRole('link', { name: 'Download File' })).not.toBeInTheDocument();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('renders no href when the client has no auth namespace', async () => {
|
|
115
|
+
renderView(sessionWithoutAuth());
|
|
116
|
+
|
|
117
|
+
expect(await screen.findByText('Download File')).toBeInTheDocument();
|
|
118
|
+
await waitFor(() => {
|
|
119
|
+
expect(screen.queryByRole('link', { name: 'Download File' })).not.toBeInTheDocument();
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('never builds a host-relative URL', async () => {
|
|
124
|
+
renderView(sessionMinting('tok-abc'));
|
|
125
|
+
|
|
126
|
+
const link = await screen.findByRole('link', { name: 'Download File' });
|
|
127
|
+
expect(link.getAttribute('href')).not.toMatch(/^\//);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
@@ -126,49 +126,6 @@
|
|
|
126
126
|
Detection Log (Completed Results)
|
|
127
127
|
============================================ */
|
|
128
128
|
|
|
129
|
-
.semiont-assist-widget__log {
|
|
130
|
-
background-color: var(--semiont-bg-primary);
|
|
131
|
-
border-radius: 0.5rem;
|
|
132
|
-
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
|
133
|
-
padding: 1rem;
|
|
134
|
-
display: flex;
|
|
135
|
-
flex-direction: column;
|
|
136
|
-
gap: 0.75rem;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
[data-theme="dark"] .semiont-assist-widget__log {
|
|
140
|
-
background-color: var(--semiont-color-gray-800);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
.semiont-assist-widget__log-items {
|
|
144
|
-
display: flex;
|
|
145
|
-
flex-direction: column;
|
|
146
|
-
gap: 0.25rem;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
.semiont-assist-widget__log-item {
|
|
150
|
-
font-size: 0.75rem;
|
|
151
|
-
color: var(--semiont-color-gray-600);
|
|
152
|
-
display: flex;
|
|
153
|
-
align-items: center;
|
|
154
|
-
gap: 0.5rem;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
[data-theme="dark"] .semiont-assist-widget__log-item {
|
|
158
|
-
color: var(--semiont-color-gray-400);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
.semiont-assist-widget__log-check {
|
|
162
|
-
color: var(--semiont-color-green-600);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
[data-theme="dark"] .semiont-assist-widget__log-check {
|
|
166
|
-
color: var(--semiont-color-green-400);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
.semiont-assist-widget__log-type {
|
|
170
|
-
font-weight: 500;
|
|
171
|
-
}
|
|
172
129
|
|
|
173
130
|
/* ============================================
|
|
174
131
|
Detection Progress Display
|
|
@@ -43,6 +43,7 @@ export function AssistSection({
|
|
|
43
43
|
annotationType === 'assessment' ? 'AssessmentPanel' :
|
|
44
44
|
'CommentsPanel';
|
|
45
45
|
const t = useTranslations(panelName);
|
|
46
|
+
const ta = useTranslations('AssistProgress');
|
|
46
47
|
const [instructions, setInstructions] = useState('');
|
|
47
48
|
type ToneValue = 'scholarly' | 'explanatory' | 'conversational' | 'technical' | 'analytical' | 'critical' | 'balanced' | 'constructive' | '';
|
|
48
49
|
const [tone, setTone] = useState<ToneValue>('');
|
|
@@ -89,7 +90,15 @@ export function AssistSection({
|
|
|
89
90
|
progress={progress}
|
|
90
91
|
progressProps={{
|
|
91
92
|
onDismiss: handleDismissProgress,
|
|
92
|
-
translations: {
|
|
93
|
+
translations: {
|
|
94
|
+
cancel: t('cancel'),
|
|
95
|
+
inProgress: t('annotating'),
|
|
96
|
+
complete: t('complete'),
|
|
97
|
+
failed: t('failed'),
|
|
98
|
+
paramsTitle: ta('paramsTitle'),
|
|
99
|
+
processing: (label) => ta('processing', { label }),
|
|
100
|
+
close: ta('close'),
|
|
101
|
+
},
|
|
93
102
|
}}
|
|
94
103
|
form={
|
|
95
104
|
<>
|
|
@@ -157,7 +166,7 @@ export function AssistSection({
|
|
|
157
166
|
<span>{t('densityLabel')}</span>
|
|
158
167
|
</label>
|
|
159
168
|
{useDensity && (
|
|
160
|
-
<span className="semiont-form-field__info">{
|
|
169
|
+
<span className="semiont-form-field__info">{t('densityPerWords', { density })}</span>
|
|
161
170
|
)}
|
|
162
171
|
</div>
|
|
163
172
|
|
|
@@ -19,8 +19,12 @@ export interface AssistShellProps {
|
|
|
19
19
|
* Pass-through config for the progress renderer (cancel/dismiss wiring,
|
|
20
20
|
* translations, percent bar). Dismiss policy lives HERE: the shell forwards
|
|
21
21
|
* `onDismiss` only once the assist is no longer running.
|
|
22
|
+
*
|
|
23
|
+
* Required because `translations` is: the shell renders the progress display
|
|
24
|
+
* itself, so a caller that omitted this would render untranslated chrome —
|
|
25
|
+
* the failure Lane A exists to make impossible.
|
|
22
26
|
*/
|
|
23
|
-
progressProps
|
|
27
|
+
progressProps: Omit<AssistProgressProps, 'progress' | 'dataType'>;
|
|
24
28
|
}
|
|
25
29
|
|
|
26
30
|
/**
|
|
@@ -6,6 +6,7 @@ import type { SemiontSession } from '@semiont/sdk';
|
|
|
6
6
|
import { useSessionEventSubscriptions } from '../../../hooks/useSessionEventSubscriptions';
|
|
7
7
|
import type { RouteBuilder, LinkComponentProps } from '../../../contexts/RoutingContext';
|
|
8
8
|
import { AssistShell } from './AssistShell';
|
|
9
|
+
import { EntityFoundLog } from '../../EntityFoundLog';
|
|
9
10
|
import { ReferenceEntry } from './ReferenceEntry';
|
|
10
11
|
import type { components, Selector } from '@semiont/core';
|
|
11
12
|
import { getTextPositionSelector, getTargetSelector } from '@semiont/core';
|
|
@@ -121,6 +122,9 @@ export function ReferencesPanel({
|
|
|
121
122
|
sourceLanguage,
|
|
122
123
|
}: Props) {
|
|
123
124
|
const t = useTranslations('ReferencesPanel');
|
|
125
|
+
// Chrome shared by every assist surface (heading, current-work line, dismiss)
|
|
126
|
+
// lives in one namespace so the same words aren't re-translated per panel.
|
|
127
|
+
const ta = useTranslations('AssistProgress');
|
|
124
128
|
const [selectedEntityTypes, setSelectedEntityTypes] = useState<string[]>([]);
|
|
125
129
|
const [lastAnnotationLog, setLastDetectionLog] = useState<Array<{ entityType: string; foundCount: number }> | null>(null);
|
|
126
130
|
const [pendingEntityTypes, setPendingEntityTypes] = useState<string[]>([]);
|
|
@@ -375,24 +379,19 @@ export function ReferencesPanel({
|
|
|
375
379
|
failed: t('failed'),
|
|
376
380
|
found: (count) => t('found', { count }),
|
|
377
381
|
current: (entityType) => t('current', { entityType }),
|
|
378
|
-
|
|
382
|
+
paramsTitle: ta('paramsTitle'),
|
|
383
|
+
processing: (label) => ta('processing', { label }),
|
|
384
|
+
close: ta('close'),
|
|
379
385
|
},
|
|
380
386
|
}}
|
|
381
387
|
form={
|
|
382
388
|
<>
|
|
383
389
|
{/* Completed annotation log - shown after completion */}
|
|
384
|
-
{lastAnnotationLog &&
|
|
385
|
-
<
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
<span className="semiont-assist-widget__log-check">✓</span>
|
|
390
|
-
<span className="semiont-assist-widget__log-type">{item.entityType}:</span>
|
|
391
|
-
<span>{t('found', { count: item.foundCount })}</span>
|
|
392
|
-
</div>
|
|
393
|
-
))}
|
|
394
|
-
</div>
|
|
395
|
-
</div>
|
|
390
|
+
{lastAnnotationLog && (
|
|
391
|
+
<EntityFoundLog
|
|
392
|
+
entries={lastAnnotationLog}
|
|
393
|
+
formatFound={(count) => t('found', { count })}
|
|
394
|
+
/>
|
|
396
395
|
)}
|
|
397
396
|
|
|
398
397
|
{/* Entity Types Selection */}
|
|
@@ -82,6 +82,14 @@ export function TaggingPanel({
|
|
|
82
82
|
sourceLanguage,
|
|
83
83
|
}: TaggingPanelProps) {
|
|
84
84
|
const t = useTranslations('TaggingPanel');
|
|
85
|
+
const ta = useTranslations('AssistProgress');
|
|
86
|
+
|
|
87
|
+
// Dismiss parity: tag was the one assist surface with no way to clear a
|
|
88
|
+
// finished progress display, because it had no `closeProgress` key to
|
|
89
|
+
// label the control. Same wiring as every other surface.
|
|
90
|
+
const handleDismissProgress = useCallback(() => {
|
|
91
|
+
session?.client.mark.dismissProgress();
|
|
92
|
+
}, [session]);
|
|
85
93
|
|
|
86
94
|
// Subscribe to the per-KB tag-schema registry. Schemas are runtime-
|
|
87
95
|
// registered by the KB at session start (see frame.addTagSchema).
|
|
@@ -355,7 +363,19 @@ export function TaggingPanel({
|
|
|
355
363
|
title={t('annotateTags')}
|
|
356
364
|
isAssisting={isAssisting}
|
|
357
365
|
progress={progress}
|
|
358
|
-
progressProps={{
|
|
366
|
+
progressProps={{
|
|
367
|
+
showPercentBar: true,
|
|
368
|
+
onDismiss: handleDismissProgress,
|
|
369
|
+
translations: {
|
|
370
|
+
cancel: t('cancel'),
|
|
371
|
+
inProgress: t('annotating'),
|
|
372
|
+
complete: t('complete'),
|
|
373
|
+
failed: t('failed'),
|
|
374
|
+
paramsTitle: ta('paramsTitle'),
|
|
375
|
+
processing: (label) => ta('processing', { label }),
|
|
376
|
+
close: ta('close'),
|
|
377
|
+
},
|
|
378
|
+
}}
|
|
359
379
|
form={
|
|
360
380
|
<>
|
|
361
381
|
{/* Empty-state — registry has resolved with no schemas. */}
|
|
@@ -126,7 +126,7 @@ describe('AssistSection', () => {
|
|
|
126
126
|
/>
|
|
127
127
|
);
|
|
128
128
|
|
|
129
|
-
expect(screen.getByText('
|
|
129
|
+
expect(screen.getByText('paramsTitle')).toBeInTheDocument();
|
|
130
130
|
expect(screen.getByText('Find important points')).toBeInTheDocument();
|
|
131
131
|
expect(screen.getByText(/Instructions:/)).toBeInTheDocument();
|
|
132
132
|
expect(screen.getByText('5')).toBeInTheDocument();
|
|
@@ -506,7 +506,7 @@ describe('AssistSection', () => {
|
|
|
506
506
|
/>
|
|
507
507
|
);
|
|
508
508
|
|
|
509
|
-
expect(screen.queryByText('
|
|
509
|
+
expect(screen.queryByText('paramsTitle')).not.toBeInTheDocument();
|
|
510
510
|
});
|
|
511
511
|
});
|
|
512
512
|
});
|
|
@@ -13,6 +13,11 @@ import { AssistShell } from '../AssistShell';
|
|
|
13
13
|
|
|
14
14
|
const progress = { stage: 'analyzing', percentage: 50, message: 'working' };
|
|
15
15
|
|
|
16
|
+
/** AssistProgress requires a full translation set; the shell just passes it through. */
|
|
17
|
+
const TR = { cancel: 'tr.cancel', inProgress: 'tr.inProgress', complete: 'tr.complete',
|
|
18
|
+
failed: 'tr.failed', close: 'tr.close', paramsTitle: 'tr.paramsTitle',
|
|
19
|
+
processing: (l: string) => `tr.processing(${l})` };
|
|
20
|
+
|
|
16
21
|
describe('AssistShell', () => {
|
|
17
22
|
beforeEach(() => {
|
|
18
23
|
localStorage.clear();
|
|
@@ -21,12 +26,12 @@ describe('AssistShell', () => {
|
|
|
21
26
|
it('renders the form when there is no progress, the progress when there is', () => {
|
|
22
27
|
const { rerender } = render(
|
|
23
28
|
<AssistShell assistType="tag" title="Annotate Tags" isAssisting={false} progress={null}
|
|
24
|
-
form={<button type="button">the form</button>} />,
|
|
29
|
+
form={<button type="button">the form</button>} progressProps={{ translations: TR }} />,
|
|
25
30
|
);
|
|
26
31
|
expect(screen.getByText('the form')).toBeInTheDocument();
|
|
27
32
|
rerender(
|
|
28
33
|
<AssistShell assistType="tag" title="Annotate Tags" isAssisting={true} progress={progress}
|
|
29
|
-
form={<button type="button">the form</button>} />,
|
|
34
|
+
form={<button type="button">the form</button>} progressProps={{ translations: TR }} />,
|
|
30
35
|
);
|
|
31
36
|
expect(screen.queryByText('the form')).not.toBeInTheDocument();
|
|
32
37
|
expect(screen.getByText('working')).toBeInTheDocument();
|
|
@@ -37,7 +42,7 @@ describe('AssistShell', () => {
|
|
|
37
42
|
const props = {
|
|
38
43
|
assistType: 'highlight', title: 'Annotate Highlights', progress,
|
|
39
44
|
form: <span>form</span>,
|
|
40
|
-
progressProps: { onDismiss, translations: { close: 'Close' } },
|
|
45
|
+
progressProps: { onDismiss, translations: { ...TR, close: 'Close' } },
|
|
41
46
|
};
|
|
42
47
|
const { rerender } = render(<AssistShell {...props} isAssisting={true} />);
|
|
43
48
|
expect(screen.queryByLabelText('Close')).not.toBeInTheDocument();
|
|
@@ -50,7 +55,7 @@ describe('AssistShell', () => {
|
|
|
50
55
|
it('persists the expand state per assist type', async () => {
|
|
51
56
|
render(
|
|
52
57
|
<AssistShell assistType="reference" title="Annotate References" isAssisting={false} progress={null}
|
|
53
|
-
form={<span>form</span>} />,
|
|
58
|
+
form={<span>form</span>} progressProps={{ translations: TR }} />,
|
|
54
59
|
);
|
|
55
60
|
await userEvent.click(screen.getByRole('button', { name: /Annotate References/ }));
|
|
56
61
|
expect(screen.queryByText('form')).not.toBeInTheDocument();
|
|
@@ -169,7 +169,7 @@ describe('HighlightPanel + AssistSection Integration', () => {
|
|
|
169
169
|
/>
|
|
170
170
|
);
|
|
171
171
|
|
|
172
|
-
expect(screen.getByText('
|
|
172
|
+
expect(screen.getByText('paramsTitle')).toBeInTheDocument();
|
|
173
173
|
expect(screen.getByText('Find important points')).toBeInTheDocument();
|
|
174
174
|
expect(screen.getByText('5')).toBeInTheDocument();
|
|
175
175
|
});
|
|
@@ -409,6 +409,32 @@ describe('ReferencesPanel Component', () => {
|
|
|
409
409
|
expect(screen.getByText('Organization:')).toBeInTheDocument();
|
|
410
410
|
});
|
|
411
411
|
|
|
412
|
+
it('renders the entity log with the SAME markup the progress display uses', () => {
|
|
413
|
+
// ASSIST-SURFACE-WARTS Lane B: this form-side log and AssistProgress's
|
|
414
|
+
// completed-entity log are the same concept. They had two class families
|
|
415
|
+
// (semiont-assist-widget__log* here, semiont-annotation-log* there) one
|
|
416
|
+
// panel apart — one concept, one markup.
|
|
417
|
+
const { container, rerender } = renderWithEventBus(
|
|
418
|
+
<ReferencesPanel
|
|
419
|
+
{...panelProps()}
|
|
420
|
+
isAssisting={false}
|
|
421
|
+
progress={{
|
|
422
|
+
stage: 'complete',
|
|
423
|
+
percentage: 100,
|
|
424
|
+
message: 'Annotation complete',
|
|
425
|
+
completedEntityTypes: [{ entityType: 'Person', foundCount: 5 }],
|
|
426
|
+
}}
|
|
427
|
+
/>
|
|
428
|
+
);
|
|
429
|
+
rerender(
|
|
430
|
+
<ReferencesPanel {...panelProps()} isAssisting={false} progress={null} />
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
expect(container.querySelector('.semiont-annotation-log')).toBeInTheDocument();
|
|
434
|
+
expect(container.querySelector('.semiont-annotation-log-item')).toBeInTheDocument();
|
|
435
|
+
expect(container.querySelector('.semiont-assist-widget__log')).not.toBeInTheDocument();
|
|
436
|
+
});
|
|
437
|
+
|
|
412
438
|
it('should hide entity type selection during detection', () => {
|
|
413
439
|
renderWithEventBus(
|
|
414
440
|
<ReferencesPanel
|
|
@@ -41,7 +41,7 @@ vi.mock('../../../hooks/useResourceContent', () => ({
|
|
|
41
41
|
// SemiontClient (wired to a dummy baseUrl). The real client surface lets
|
|
42
42
|
// createResourceViewerPageStateUnit run against the full namespace API without us
|
|
43
43
|
// hand-stubbing every method it touches.
|
|
44
|
-
const { stubBrowser } = vi.hoisted(() => {
|
|
44
|
+
const { stubBrowser, stubClient } = vi.hoisted(() => {
|
|
45
45
|
const { BehaviorSubject } = require('rxjs');
|
|
46
46
|
const { SemiontClient, HttpTransport, HttpContentTransport } = require('@semiont/sdk');
|
|
47
47
|
const { baseUrl } = require('@semiont/core');
|
|
@@ -63,7 +63,7 @@ const { stubBrowser } = vi.hoisted(() => {
|
|
|
63
63
|
on: vi.fn(() => () => {}),
|
|
64
64
|
stream: vi.fn(() => ({ subscribe: () => ({ unsubscribe: () => {} }) })),
|
|
65
65
|
};
|
|
66
|
-
return { stubBrowser };
|
|
66
|
+
return { stubBrowser, stubClient: client };
|
|
67
67
|
});
|
|
68
68
|
|
|
69
69
|
vi.mock('../../../session/SemiontProvider', async () => {
|
|
@@ -76,6 +76,8 @@ vi.mock('../../../session/SemiontProvider', async () => {
|
|
|
76
76
|
};
|
|
77
77
|
});
|
|
78
78
|
|
|
79
|
+
const capturedHistory = vi.hoisted(() => ({ props: null as any }));
|
|
80
|
+
|
|
79
81
|
vi.mock('@semiont/react-ui', async () => {
|
|
80
82
|
const actual = await vi.importActual('@semiont/react-ui');
|
|
81
83
|
return {
|
|
@@ -84,7 +86,10 @@ vi.mock('@semiont/react-ui', async () => {
|
|
|
84
86
|
Toolbar: () => <div data-testid="toolbar">Toolbar</div>,
|
|
85
87
|
ToolbarPanels: ({ children }: any) => <div data-testid="toolbar-panels">{children}</div>,
|
|
86
88
|
UnifiedAnnotationsPanel: () => <div data-testid="annotations-panel">Annotations</div>,
|
|
87
|
-
AnnotationHistory: () =>
|
|
89
|
+
AnnotationHistory: (props: any) => {
|
|
90
|
+
capturedHistory.props = props;
|
|
91
|
+
return <div data-testid="history-panel">History</div>;
|
|
92
|
+
},
|
|
88
93
|
ResourceInfoPanel: () => <div data-testid="info-panel">Info</div>,
|
|
89
94
|
CollaborationPanel: () => <div data-testid="collaboration-panel">Collaboration</div>,
|
|
90
95
|
JsonLdPanel: () => <div data-testid="jsonld-panel">JSON-LD</div>,
|
|
@@ -228,6 +233,30 @@ describe('ResourceViewerPage', () => {
|
|
|
228
233
|
localStorage.clear();
|
|
229
234
|
});
|
|
230
235
|
|
|
236
|
+
it('clicking a history event focuses that annotation in the content', () => {
|
|
237
|
+
// ASSIST-SURFACE-WARTS Lane D. `handleEventClick` used to be a no-op with
|
|
238
|
+
// a stale comment, while HistoryEvent still rendered a focusable button
|
|
239
|
+
// labelled "View annotation" — a promise to screen-reader users that
|
|
240
|
+
// nothing kept. `beckon:focus` is the existing "scroll to and highlight"
|
|
241
|
+
// contract (BrowseView already subscribes); this makes the page a producer.
|
|
242
|
+
localStorage.setItem('activeToolbarPanel', 'history');
|
|
243
|
+
capturedHistory.props = null;
|
|
244
|
+
const props = createMockProps();
|
|
245
|
+
renderWithProviders(<ResourceViewerPage {...props} />);
|
|
246
|
+
|
|
247
|
+
expect(capturedHistory.props).not.toBeNull();
|
|
248
|
+
|
|
249
|
+
const focused: Array<string | null> = [];
|
|
250
|
+
const sub = stubClient.bus.get('beckon:focus').subscribe(({ annotationId }: any) => focused.push(annotationId));
|
|
251
|
+
|
|
252
|
+
expect(capturedHistory.props?.onEventClick).toBeTypeOf('function');
|
|
253
|
+
capturedHistory.props!.onEventClick('ann-42');
|
|
254
|
+
expect(focused).toEqual(['ann-42']);
|
|
255
|
+
|
|
256
|
+
sub.unsubscribe();
|
|
257
|
+
localStorage.clear();
|
|
258
|
+
});
|
|
259
|
+
|
|
231
260
|
it('shows history panel when activePanel is history', () => {
|
|
232
261
|
localStorage.setItem('activeToolbarPanel', 'history');
|
|
233
262
|
const props = createMockProps();
|
|
@@ -24,6 +24,7 @@ import type { BrowseMediaRenderers, AnnotateMediaRenderers } from '@semiont/reac
|
|
|
24
24
|
import { useObservable } from '@semiont/react-ui';
|
|
25
25
|
import { useResourceContent } from '../../../hooks/useResourceContent';
|
|
26
26
|
import { useMediaToken } from '../../../hooks/useMediaToken';
|
|
27
|
+
import { mediaUrl } from '../../../lib/media-url';
|
|
27
28
|
import { useToast } from '../../../components/Toast';
|
|
28
29
|
import { useOutcomeToasts } from '../../../hooks/useOutcomeToasts';
|
|
29
30
|
import { useTheme } from '../../../contexts/ThemeContext';
|
|
@@ -139,6 +140,7 @@ export function ResourceViewerPage({
|
|
|
139
140
|
// Translations
|
|
140
141
|
const tw = useTranslations('ReferenceWizard');
|
|
141
142
|
const tg = useTranslations('ResourceGenerate');
|
|
143
|
+
const ta = useTranslations('AssistProgress');
|
|
142
144
|
|
|
143
145
|
const browser = useSemiont();
|
|
144
146
|
const session = useObservable(browser.activeSession$);
|
|
@@ -185,9 +187,7 @@ export function ResourceViewerPage({
|
|
|
185
187
|
|
|
186
188
|
// Binary path: fetch short-lived media token, construct URL
|
|
187
189
|
const { token: mediaToken, loading: mediaTokenLoading } = useMediaToken(semiont ?? null, rUri);
|
|
188
|
-
const binaryContent = (isBinary
|
|
189
|
-
? `${semiont.baseUrl}/api/resources/${rUri}?token=${mediaToken}`
|
|
190
|
-
: '';
|
|
190
|
+
const binaryContent = (isBinary ? mediaUrl(semiont, rUri, mediaToken) : undefined) ?? '';
|
|
191
191
|
|
|
192
192
|
const content = isBinary ? binaryContent : textContent;
|
|
193
193
|
const contentLoading = isBinary ? mediaTokenLoading : textLoading;
|
|
@@ -444,9 +444,17 @@ export function ResourceViewerPage({
|
|
|
444
444
|
}
|
|
445
445
|
}, [session]);
|
|
446
446
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
447
|
+
// Clicking a history row reveals that annotation in the content. HistoryEvent
|
|
448
|
+
// renders these rows as buttons labelled "View annotation", so this used to be
|
|
449
|
+
// a focusable, screen-reader-announced control wired to a no-op.
|
|
450
|
+
// `beckon:focus` is the existing "scroll to and highlight" contract rather
|
|
451
|
+
// than a new prop chain — BrowseView already subscribed to it, AnnotateView
|
|
452
|
+
// now does too. See .plans/ASSIST-SURFACE-WARTS.md Lane D.
|
|
453
|
+
const handleEventClick = useCallback((id: string | null) => {
|
|
454
|
+
if (id) {
|
|
455
|
+
stateUnit?.beckon.focus(annotationId(id));
|
|
456
|
+
}
|
|
457
|
+
}, [stateUnit]);
|
|
450
458
|
|
|
451
459
|
// Document rendering
|
|
452
460
|
return (
|
|
@@ -475,6 +483,9 @@ export function ResourceViewerPage({
|
|
|
475
483
|
inProgress: tg('progressInProgress'),
|
|
476
484
|
complete: tg('progressComplete'),
|
|
477
485
|
failed: tg('progressFailed'),
|
|
486
|
+
paramsTitle: ta('paramsTitle'),
|
|
487
|
+
processing: (label) => ta('processing', { label }),
|
|
488
|
+
close: ta('close'),
|
|
478
489
|
}}
|
|
479
490
|
/>
|
|
480
491
|
)}
|
package/translations/ar.json
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
+
"AssistProgress": {
|
|
3
|
+
"close": "إغلاق",
|
|
4
|
+
"paramsTitle": "معاملات الطلب:",
|
|
5
|
+
"processing": "الحالي: {{label}}"
|
|
6
|
+
},
|
|
2
7
|
"Toolbar": {
|
|
3
8
|
"annotations": "التعليقات التوضيحية",
|
|
4
9
|
"history": "السجل",
|
|
@@ -105,7 +110,9 @@
|
|
|
105
110
|
"densityDense": "كثيف",
|
|
106
111
|
"fragmentSelected": "تم تحديد جزء",
|
|
107
112
|
"imageRegionSelected": "تم تحديد منطقة من الصورة",
|
|
108
|
-
"
|
|
113
|
+
"complete": "اكتمل التوضيح!",
|
|
114
|
+
"failed": "فشل التوضيح",
|
|
115
|
+
"densityPerWords": "{{density}} لكل 2000 كلمة"
|
|
109
116
|
},
|
|
110
117
|
"HighlightPanel": {
|
|
111
118
|
"title": "التمييزات",
|
|
@@ -120,7 +127,9 @@
|
|
|
120
127
|
"densityLabel": "الكثافة",
|
|
121
128
|
"densitySparse": "متفرق",
|
|
122
129
|
"densityDense": "كثيف",
|
|
123
|
-
"
|
|
130
|
+
"complete": "اكتمل التوضيح!",
|
|
131
|
+
"failed": "فشل التوضيح",
|
|
132
|
+
"densityPerWords": "{{density}} لكل 2000 كلمة"
|
|
124
133
|
},
|
|
125
134
|
"AssessmentPanel": {
|
|
126
135
|
"title": "التقييمات",
|
|
@@ -145,7 +154,9 @@
|
|
|
145
154
|
"densityDense": "كثيف",
|
|
146
155
|
"fragmentSelected": "تم تحديد جزء",
|
|
147
156
|
"imageRegionSelected": "تم تحديد منطقة من الصورة",
|
|
148
|
-
"
|
|
157
|
+
"complete": "اكتمل التوضيح!",
|
|
158
|
+
"failed": "فشل التوضيح",
|
|
159
|
+
"densityPerWords": "{{density}} لكل 2000 كلمة"
|
|
149
160
|
},
|
|
150
161
|
"TaggingPanel": {
|
|
151
162
|
"title": "العلامات",
|
|
@@ -164,7 +175,9 @@
|
|
|
164
175
|
"selectCategory": "اختيار الفئة",
|
|
165
176
|
"chooseCategory": "اختر فئة...",
|
|
166
177
|
"fragmentSelected": "تم تحديد جزء",
|
|
167
|
-
"imageRegionSelected": "تم تحديد منطقة من الصورة"
|
|
178
|
+
"imageRegionSelected": "تم تحديد منطقة من الصورة",
|
|
179
|
+
"complete": "اكتمل التوضيح!",
|
|
180
|
+
"failed": "فشل التوضيح"
|
|
168
181
|
},
|
|
169
182
|
"UnifiedAnnotationsPanel": {
|
|
170
183
|
"title": "التعليقات التوضيحية",
|
|
@@ -220,7 +233,6 @@
|
|
|
220
233
|
"complete": "اكتمل التوضيح!",
|
|
221
234
|
"failed": "فشل التوضيح",
|
|
222
235
|
"current": "الحالي: {{entityType}}",
|
|
223
|
-
"closeProgress": "إغلاق",
|
|
224
236
|
"loading": "جارٍ التحميل...",
|
|
225
237
|
"loadingEllipsis": "جارٍ التحميل...",
|
|
226
238
|
"untitledResource": "مورد بدون عنوان",
|