@semiont/react-ui 0.5.1 → 0.5.3

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.
Files changed (177) hide show
  1. package/README.md +13 -0
  2. package/dist/{ar-3W37O3R3.mjs → ar-UUMMNQKF.mjs} +2 -17
  3. package/dist/ar-UUMMNQKF.mjs.map +1 -0
  4. package/dist/{bn-JZTJLMVE.mjs → bn-AL5BJSR3.mjs} +2 -17
  5. package/dist/bn-AL5BJSR3.mjs.map +1 -0
  6. package/dist/{chunk-4NOUO3W6.mjs → chunk-EBBL3VJI.mjs} +5062 -2906
  7. package/dist/chunk-EBBL3VJI.mjs.map +1 -0
  8. package/dist/{chunk-NOD3NCXE.mjs → chunk-OJSRLEER.mjs} +2 -17
  9. package/dist/chunk-OJSRLEER.mjs.map +1 -0
  10. package/dist/{cs-XYHH7HNE.mjs → cs-UMINALSU.mjs} +2 -17
  11. package/dist/cs-UMINALSU.mjs.map +1 -0
  12. package/dist/{da-MZKIECVT.mjs → da-FKUX6CDL.mjs} +2 -17
  13. package/dist/da-FKUX6CDL.mjs.map +1 -0
  14. package/dist/{de-AYXTMRQW.mjs → de-XSJ3E25S.mjs} +2 -17
  15. package/dist/de-XSJ3E25S.mjs.map +1 -0
  16. package/dist/{el-A6CVQWAW.mjs → el-UJXNRCBP.mjs} +2 -17
  17. package/dist/el-UJXNRCBP.mjs.map +1 -0
  18. package/dist/{en-YPQQBI4T.mjs → en-J5DHKLQ5.mjs} +2 -2
  19. package/dist/{es-M2HXLJGT.mjs → es-VURP62BU.mjs} +2 -17
  20. package/dist/es-VURP62BU.mjs.map +1 -0
  21. package/dist/{fa-V6JZJDYP.mjs → fa-TIT5ZPZY.mjs} +2 -17
  22. package/dist/fa-TIT5ZPZY.mjs.map +1 -0
  23. package/dist/{fi-ONDTZ5H7.mjs → fi-F7VTGT4H.mjs} +2 -17
  24. package/dist/fi-F7VTGT4H.mjs.map +1 -0
  25. package/dist/{fr-PAPV4H4G.mjs → fr-2ZR26VF7.mjs} +2 -17
  26. package/dist/fr-2ZR26VF7.mjs.map +1 -0
  27. package/dist/{he-F6VTLJLW.mjs → he-BXP2KYVZ.mjs} +2 -17
  28. package/dist/he-BXP2KYVZ.mjs.map +1 -0
  29. package/dist/{hi-CFUAV4BF.mjs → hi-PSWTP3NC.mjs} +2 -17
  30. package/dist/hi-PSWTP3NC.mjs.map +1 -0
  31. package/dist/{id-NBKLCCI7.mjs → id-HO6TXGTO.mjs} +2 -17
  32. package/dist/id-HO6TXGTO.mjs.map +1 -0
  33. package/dist/index.d.mts +292 -27
  34. package/dist/index.mjs +1134 -592
  35. package/dist/index.mjs.map +1 -1
  36. package/dist/{it-SLSOWVVU.mjs → it-AGTDMBL3.mjs} +2 -17
  37. package/dist/it-AGTDMBL3.mjs.map +1 -0
  38. package/dist/{ja-L5IG4ECE.mjs → ja-TTGOVF5K.mjs} +2 -17
  39. package/dist/ja-TTGOVF5K.mjs.map +1 -0
  40. package/dist/{ko-QYMTULKK.mjs → ko-FF77IQ7N.mjs} +2 -17
  41. package/dist/ko-FF77IQ7N.mjs.map +1 -0
  42. package/dist/{ms-5DGSFKM2.mjs → ms-UPQWWIL4.mjs} +2 -17
  43. package/dist/ms-UPQWWIL4.mjs.map +1 -0
  44. package/dist/{nl-VZPCGONO.mjs → nl-W75HEPFL.mjs} +2 -17
  45. package/dist/nl-W75HEPFL.mjs.map +1 -0
  46. package/dist/{no-MF6F352I.mjs → no-R4W7W7ZU.mjs} +2 -17
  47. package/dist/no-R4W7W7ZU.mjs.map +1 -0
  48. package/dist/{pl-WIK72JUO.mjs → pl-GQC2ELWO.mjs} +2 -17
  49. package/dist/pl-GQC2ELWO.mjs.map +1 -0
  50. package/dist/{pt-RRP5ZF6A.mjs → pt-YGVT62RU.mjs} +2 -17
  51. package/dist/pt-YGVT62RU.mjs.map +1 -0
  52. package/dist/{ro-XHQLC3T7.mjs → ro-TST6XS6X.mjs} +2 -17
  53. package/dist/ro-TST6XS6X.mjs.map +1 -0
  54. package/dist/{sv-EWULDN6E.mjs → sv-TQLF6HV7.mjs} +2 -17
  55. package/dist/sv-TQLF6HV7.mjs.map +1 -0
  56. package/dist/test-utils.d.mts +1 -1
  57. package/dist/test-utils.mjs +5 -2353
  58. package/dist/test-utils.mjs.map +1 -1
  59. package/dist/{th-TGOBHFG4.mjs → th-HJUIETVR.mjs} +2 -17
  60. package/dist/th-HJUIETVR.mjs.map +1 -0
  61. package/dist/{tr-LMMPBMV7.mjs → tr-CW3C46TW.mjs} +2 -17
  62. package/dist/tr-CW3C46TW.mjs.map +1 -0
  63. package/dist/{uk-IPGRRJY6.mjs → uk-WTHZQB2U.mjs} +2 -17
  64. package/dist/uk-WTHZQB2U.mjs.map +1 -0
  65. package/dist/{vi-Q676OJQS.mjs → vi-PHWHJLKP.mjs} +2 -17
  66. package/dist/vi-PHWHJLKP.mjs.map +1 -0
  67. package/dist/{zh-F3MTWQDX.mjs → zh-MO3FCUD6.mjs} +2 -17
  68. package/dist/zh-MO3FCUD6.mjs.map +1 -0
  69. package/package.json +1 -1
  70. package/src/components/StatusDisplay.tsx +1 -1
  71. package/src/components/modals/PermissionDeniedModal.tsx +2 -2
  72. package/src/components/modals/SessionExpiredModal.tsx +4 -4
  73. package/src/components/resource/panels/AssessmentPanel.tsx +4 -0
  74. package/src/components/resource/panels/AssistSection.tsx +10 -1
  75. package/src/components/resource/panels/CollaborationPanel.tsx +1 -1
  76. package/src/components/resource/panels/CommentsPanel.tsx +4 -0
  77. package/src/components/resource/panels/HighlightPanel.tsx +4 -0
  78. package/src/components/resource/panels/ReferencesPanel.tsx +11 -0
  79. package/src/components/resource/panels/TagEntry.tsx +13 -2
  80. package/src/components/resource/panels/TaggingPanel.tsx +93 -41
  81. package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +11 -1
  82. package/src/components/resource/panels/__tests__/ReferencesPanel.observable-flow.test.tsx +2 -2
  83. package/src/components/resource/panels/__tests__/TagEntry.test.tsx +26 -19
  84. package/src/components/resource/panels/__tests__/TaggingPanel.test.tsx +128 -38
  85. package/src/features/admin-devops/components/AdminDevOpsPage.tsx +1 -1
  86. package/src/features/admin-exchange/components/AdminExchangePage.tsx +1 -1
  87. package/src/features/admin-exchange/components/ImportCard.tsx +1 -1
  88. package/src/features/admin-exchange/state/__tests__/exchange-state-unit.test.ts +171 -0
  89. package/src/features/admin-exchange/state/exchange-state-unit.ts +131 -0
  90. package/src/features/admin-security/components/AdminSecurityPage.tsx +1 -1
  91. package/src/features/admin-security/state/__tests__/admin-security-state-unit.test.ts +68 -0
  92. package/src/features/admin-security/state/admin-security-state-unit.ts +46 -0
  93. package/src/features/admin-users/components/AdminUsersPage.tsx +1 -1
  94. package/src/features/admin-users/state/__tests__/admin-users-state-unit.test.ts +86 -0
  95. package/src/features/admin-users/state/admin-users-state-unit.ts +73 -0
  96. package/src/features/auth-welcome/state/__tests__/welcome-state-unit.test.ts +86 -0
  97. package/src/features/auth-welcome/state/welcome-state-unit.ts +44 -0
  98. package/src/features/moderate-entity-tags/components/EntityTagsPage.tsx +1 -1
  99. package/src/features/moderate-entity-tags/state/__tests__/entity-tags-state-unit.test.ts +102 -0
  100. package/src/features/moderate-entity-tags/state/entity-tags-state-unit.ts +64 -0
  101. package/src/features/moderate-recent/components/RecentDocumentsPage.tsx +1 -1
  102. package/src/features/moderate-tag-schemas/components/TagSchemasPage.tsx +4 -4
  103. package/src/features/moderation-linked-data/components/LinkedDataPage.tsx +1 -1
  104. package/src/features/resource-compose/__tests__/UploadProgressBar.test.tsx +225 -0
  105. package/src/features/resource-compose/components/ResourceComposePage.tsx +19 -4
  106. package/src/features/resource-compose/components/UploadProgressBar.tsx +94 -0
  107. package/src/features/resource-compose/state/__tests__/compose-page-state-unit.test.ts +187 -0
  108. package/src/features/resource-compose/state/compose-page-state-unit.ts +209 -0
  109. package/src/features/resource-discovery/components/ResourceDiscoveryPage.tsx +1 -1
  110. package/src/features/resource-discovery/state/__tests__/discover-state-unit.test.ts +76 -0
  111. package/src/features/resource-discovery/state/discover-state-unit.ts +54 -0
  112. package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +4 -2
  113. package/src/features/resource-viewer/components/ResourceViewerPage.tsx +36 -32
  114. package/src/features/resource-viewer/state/__tests__/resource-loader-state-unit.test.ts +46 -0
  115. package/src/features/resource-viewer/state/__tests__/resource-viewer-page-state-unit.test.ts +203 -0
  116. package/src/features/resource-viewer/state/resource-loader-state-unit.ts +26 -0
  117. package/src/features/resource-viewer/state/resource-viewer-page-state-unit.ts +180 -0
  118. package/translations/ar.json +1 -16
  119. package/translations/bn.json +1 -16
  120. package/translations/cs.json +1 -16
  121. package/translations/da.json +1 -16
  122. package/translations/de.json +1 -16
  123. package/translations/el.json +1 -16
  124. package/translations/en.json +1 -16
  125. package/translations/es.json +1 -16
  126. package/translations/fa.json +1 -16
  127. package/translations/fi.json +1 -16
  128. package/translations/fr.json +1 -16
  129. package/translations/he.json +1 -16
  130. package/translations/hi.json +1 -16
  131. package/translations/id.json +1 -16
  132. package/translations/it.json +1 -16
  133. package/translations/ja.json +1 -16
  134. package/translations/ko.json +1 -16
  135. package/translations/ms.json +1 -16
  136. package/translations/nl.json +1 -16
  137. package/translations/no.json +1 -16
  138. package/translations/pl.json +1 -16
  139. package/translations/pt.json +1 -16
  140. package/translations/ro.json +1 -16
  141. package/translations/sv.json +1 -16
  142. package/translations/th.json +1 -16
  143. package/translations/tr.json +1 -16
  144. package/translations/uk.json +1 -16
  145. package/translations/vi.json +1 -16
  146. package/translations/zh.json +1 -16
  147. package/dist/ar-3W37O3R3.mjs.map +0 -1
  148. package/dist/bn-JZTJLMVE.mjs.map +0 -1
  149. package/dist/chunk-4NOUO3W6.mjs.map +0 -1
  150. package/dist/chunk-NOD3NCXE.mjs.map +0 -1
  151. package/dist/cs-XYHH7HNE.mjs.map +0 -1
  152. package/dist/da-MZKIECVT.mjs.map +0 -1
  153. package/dist/de-AYXTMRQW.mjs.map +0 -1
  154. package/dist/el-A6CVQWAW.mjs.map +0 -1
  155. package/dist/es-M2HXLJGT.mjs.map +0 -1
  156. package/dist/fa-V6JZJDYP.mjs.map +0 -1
  157. package/dist/fi-ONDTZ5H7.mjs.map +0 -1
  158. package/dist/fr-PAPV4H4G.mjs.map +0 -1
  159. package/dist/he-F6VTLJLW.mjs.map +0 -1
  160. package/dist/hi-CFUAV4BF.mjs.map +0 -1
  161. package/dist/id-NBKLCCI7.mjs.map +0 -1
  162. package/dist/it-SLSOWVVU.mjs.map +0 -1
  163. package/dist/ja-L5IG4ECE.mjs.map +0 -1
  164. package/dist/ko-QYMTULKK.mjs.map +0 -1
  165. package/dist/ms-5DGSFKM2.mjs.map +0 -1
  166. package/dist/nl-VZPCGONO.mjs.map +0 -1
  167. package/dist/no-MF6F352I.mjs.map +0 -1
  168. package/dist/pl-WIK72JUO.mjs.map +0 -1
  169. package/dist/pt-RRP5ZF6A.mjs.map +0 -1
  170. package/dist/ro-XHQLC3T7.mjs.map +0 -1
  171. package/dist/sv-EWULDN6E.mjs.map +0 -1
  172. package/dist/th-TGOBHFG4.mjs.map +0 -1
  173. package/dist/tr-LMMPBMV7.mjs.map +0 -1
  174. package/dist/uk-IPGRRJY6.mjs.map +0 -1
  175. package/dist/vi-Q676OJQS.mjs.map +0 -1
  176. package/dist/zh-F3MTWQDX.mjs.map +0 -1
  177. /package/dist/{en-YPQQBI4T.mjs.map → en-J5DHKLQ5.mjs.map} +0 -0
@@ -5,11 +5,11 @@ import { useSemiont } from '../../session/SemiontProvider';
5
5
  import { useObservable } from '../../hooks/useObservable';
6
6
 
7
7
  /**
8
- * Modal that surfaces when the active KB's session expires (a 401 from
9
- * either the session's own JWT validation or from any React Query call
10
- * via the QueryCache.onError handler).
8
+ * Modal that surfaces when the active KB's session expires (a 401
9
+ * surfaced by the session's own JWT validation or by the host's
10
+ * error-routing path).
11
11
  *
12
- * Reads `sessionExpiredAt$` from the active `FrontendSessionSignals`.
12
+ * Reads `sessionExpiredAt$` from the active `SessionSignals`.
13
13
  * When the user dismisses the modal, the signals instance clears the
14
14
  * flag. Modal state lives on signals (not the session itself) so
15
15
  * headless sessions (workers/CLIs) don't carry dead observables.
@@ -45,6 +45,8 @@ interface AssessmentPanelProps {
45
45
  isAssisting?: boolean;
46
46
  progress?: JobProgress | null;
47
47
  locale?: string;
48
+ /** BCP-47 tag of the resource being analyzed — forwarded to the assist call. */
49
+ sourceLanguage?: string;
48
50
  annotateMode?: boolean;
49
51
  scrollToAnnotationId?: string | null;
50
52
  onScrollCompleted?: () => void;
@@ -64,6 +66,7 @@ export function AssessmentPanel({
64
66
  isAssisting = false,
65
67
  progress,
66
68
  locale,
69
+ sourceLanguage,
67
70
  annotateMode = true,
68
71
  scrollToAnnotationId,
69
72
  onScrollCompleted,
@@ -243,6 +246,7 @@ export function AssessmentPanel({
243
246
  annotationType="assessment"
244
247
  isAssisting={isAssisting}
245
248
  locale={locale}
249
+ sourceLanguage={sourceLanguage}
246
250
  progress={progress}
247
251
  />
248
252
  )}
@@ -12,7 +12,10 @@ type JobProgress = components['schemas']['JobProgress'];
12
12
  interface AssistSectionProps {
13
13
  annotationType: 'highlight' | 'assessment' | 'comment';
14
14
  isAssisting: boolean;
15
+ /** User UI locale — written into the annotation body's `language` field for comment/assessment. */
15
16
  locale?: string;
17
+ /** BCP-47 tag of the resource being analyzed. Forwarded to the prompt so the LLM analyzes non-English source correctly. */
18
+ sourceLanguage?: string;
16
19
  progress?: JobProgress | null | undefined;
17
20
  }
18
21
 
@@ -34,6 +37,7 @@ export function AssistSection({
34
37
  annotationType,
35
38
  isAssisting,
36
39
  locale,
40
+ sourceLanguage,
37
41
  progress,
38
42
  }: AssistSectionProps) {
39
43
 
@@ -74,13 +78,18 @@ export function AssistSection({
74
78
  instructions: instructions.trim() || undefined,
75
79
  tone: (annotationType === 'comment' || annotationType === 'assessment') && tone ? tone : undefined,
76
80
  density: (annotationType === 'comment' || annotationType === 'assessment' || annotationType === 'highlight') && useDensity ? density : undefined,
81
+ // Body locale only applies where the LLM writes natural-language text:
82
+ // comment/assessment have a body, highlight does not.
77
83
  language: (annotationType === 'comment' || annotationType === 'assessment') ? locale : undefined,
84
+ // Source locale applies to all three — affects analysis quality on
85
+ // non-English source, regardless of whether a body is produced.
86
+ sourceLanguage,
78
87
  });
79
88
 
80
89
  setInstructions('');
81
90
  setTone('');
82
91
  // Don't reset density/useDensity - persist across assists
83
- }, [annotationType, instructions, tone, useDensity, density, locale, session]);
92
+ }, [annotationType, instructions, tone, useDensity, density, locale, sourceLanguage, session]);
84
93
 
85
94
  const handleDismissProgress = useCallback(() => {
86
95
  session?.client.mark.dismissProgress();
@@ -7,7 +7,7 @@ import './CollaborationPanel.css';
7
7
  interface Props {
8
8
  /**
9
9
  * Connection state from `client.actor.state$`. See
10
- * `packages/api-client/src/view-models/domain/actor-vm.ts`.
10
+ * `packages/api-client/src/state/domain/actor-state-unit.ts`.
11
11
  *
12
12
  * UI mapping:
13
13
  * `open` | `reconnecting` | `initial` | `connecting`
@@ -46,6 +46,8 @@ interface CommentsPanelProps {
46
46
  isAssisting?: boolean;
47
47
  progress?: JobProgress | null;
48
48
  locale?: string;
49
+ /** BCP-47 tag of the resource being analyzed — forwarded to the assist call. */
50
+ sourceLanguage?: string;
49
51
  scrollToAnnotationId?: string | null;
50
52
  onScrollCompleted?: () => void;
51
53
  hoveredAnnotationId?: string | null;
@@ -65,6 +67,7 @@ export function CommentsPanel({
65
67
  isAssisting = false,
66
68
  progress,
67
69
  locale,
70
+ sourceLanguage,
68
71
  scrollToAnnotationId,
69
72
  onScrollCompleted,
70
73
  hoveredAnnotationId,
@@ -253,6 +256,7 @@ export function CommentsPanel({
253
256
  annotationType="comment"
254
257
  isAssisting={isAssisting}
255
258
  locale={locale}
259
+ sourceLanguage={sourceLanguage}
256
260
  progress={progress}
257
261
  />
258
262
  )}
@@ -31,6 +31,8 @@ interface HighlightPanelProps {
31
31
  scrollToAnnotationId?: string | null;
32
32
  onScrollCompleted?: () => void;
33
33
  hoveredAnnotationId?: string | null;
34
+ /** BCP-47 tag of the resource being analyzed — forwarded to the assist call so the LLM analyzes non-English source correctly. */
35
+ sourceLanguage?: string;
34
36
  }
35
37
 
36
38
  /**
@@ -48,6 +50,7 @@ export function HighlightPanel({
48
50
  scrollToAnnotationId,
49
51
  onScrollCompleted,
50
52
  hoveredAnnotationId,
53
+ sourceLanguage,
51
54
  }: HighlightPanelProps) {
52
55
 
53
56
  const t = useTranslations('HighlightPanel');
@@ -151,6 +154,7 @@ export function HighlightPanel({
151
154
  annotationType="highlight"
152
155
  isAssisting={isAssisting}
153
156
  progress={progress}
157
+ sourceLanguage={sourceLanguage}
154
158
  />
155
159
  )}
156
160
 
@@ -60,6 +60,11 @@ interface Props {
60
60
  scrollToAnnotationId?: string | null;
61
61
  onScrollCompleted?: () => void;
62
62
  hoveredAnnotationId?: string | null;
63
+
64
+ /** User UI locale — stamped on the unresolved-reference body's `language` field. */
65
+ locale?: string;
66
+ /** BCP-47 tag of the resource being analyzed — fed into the prompt for source-aware analysis. */
67
+ sourceLanguage?: string;
63
68
  }
64
69
 
65
70
  /**
@@ -85,6 +90,8 @@ export function ReferencesPanel({
85
90
  scrollToAnnotationId,
86
91
  onScrollCompleted,
87
92
  hoveredAnnotationId,
93
+ locale,
94
+ sourceLanguage,
88
95
  }: Props) {
89
96
  const t = useTranslations('ReferencesPanel');
90
97
  const session = useObservable(useSemiont().activeSession$);
@@ -206,6 +213,10 @@ export function ReferencesPanel({
206
213
  session?.client.mark.requestAssist('linking', {
207
214
  entityTypes: selectedEntityTypes,
208
215
  includeDescriptiveReferences,
216
+ // Body locale stamps the unresolved-reference body's `language`;
217
+ // sourceLanguage tunes the prompt for non-English source content.
218
+ language: locale,
219
+ sourceLanguage,
209
220
  });
210
221
  };
211
222
 
@@ -1,10 +1,10 @@
1
1
  'use client';
2
2
 
3
+ import { useMemo } from 'react';
3
4
  import type { Ref } from 'react';
4
5
  import type { Annotation } from '@semiont/core';
5
6
  import { getAnnotationExactText } from '@semiont/core';
6
7
  import { getTagCategory, getTagSchemaId } from '@semiont/ontology';
7
- import { getTagSchema } from '../../../lib/tag-schemas';
8
8
  import { useSemiont } from '../../../session/SemiontProvider';
9
9
  import { useObservable } from '../../../hooks/useObservable';
10
10
  import { useHoverEmitter } from '../../../hooks/useHoverEmitter';
@@ -28,7 +28,18 @@ export function TagEntry({
28
28
  const selectedText = getAnnotationExactText(tag);
29
29
  const category = getTagCategory(tag);
30
30
  const schemaId = getTagSchemaId(tag);
31
- const schema = schemaId ? getTagSchema(schemaId) : null;
31
+
32
+ // Resolve the schema's display name from the per-KB tag-schema registry.
33
+ // The registry is runtime-populated (frame.addTagSchema); during the
34
+ // initial fetch the observable yields `undefined`, which we treat as
35
+ // "no schema name available yet" — render the category badge alone
36
+ // until the registry resolves.
37
+ const tagSchemas$ = useMemo(
38
+ () => session?.client.browse.tagSchemas() ?? null,
39
+ [session],
40
+ );
41
+ const schemas = useObservable(tagSchemas$);
42
+ const schema = schemaId && schemas ? schemas.find((s) => s.id === schemaId) ?? null : null;
32
43
 
33
44
  return (
34
45
  <div
@@ -9,7 +9,6 @@ import type { components, Selector } from '@semiont/core';
9
9
  import { getTextPositionSelector, getTargetSelector } from '@semiont/core';
10
10
  import { TagEntry } from './TagEntry';
11
11
  import { PanelHeader } from './PanelHeader';
12
- import { getAllTagSchemas } from '../../../lib/tag-schemas';
13
12
  import './TaggingPanel.css';
14
13
 
15
14
  import type { Annotation } from '@semiont/core';
@@ -48,6 +47,10 @@ interface TaggingPanelProps {
48
47
  scrollToAnnotationId?: string | null;
49
48
  onScrollCompleted?: () => void;
50
49
  hoveredAnnotationId?: string | null;
50
+ /** User UI locale — stamped on the tagging body's `language` field. */
51
+ locale?: string;
52
+ /** BCP-47 tag of the resource being analyzed — fed into the prompt for source-aware analysis. */
53
+ sourceLanguage?: string;
51
54
  }
52
55
 
53
56
  /**
@@ -67,11 +70,39 @@ export function TaggingPanel({
67
70
  scrollToAnnotationId,
68
71
  onScrollCompleted,
69
72
  hoveredAnnotationId,
73
+ locale,
74
+ sourceLanguage,
70
75
  }: TaggingPanelProps) {
71
76
  const t = useTranslations('TaggingPanel');
72
77
  const session = useObservable(useSemiont().activeSession$);
73
- const [selectedSchemaId, setSelectedSchemaId] = useState<string>('legal-irac');
78
+
79
+ // Subscribe to the per-KB tag-schema registry. Schemas are runtime-
80
+ // registered by the KB at session start (see frame.addTagSchema).
81
+ // During the initial load the observable yields `undefined` — render an
82
+ // empty schemas list and let the picker render no options until the
83
+ // first emission lands.
84
+ const tagSchemas$ = useMemo(
85
+ () => session?.client.browse.tagSchemas() ?? null,
86
+ [session],
87
+ );
88
+ const schemasObserved = useObservable(tagSchemas$);
89
+ const schemas = schemasObserved ?? [];
90
+ // True only AFTER the registry has resolved AND it's empty — distinct
91
+ // from the initial-loading state (`schemasObserved === undefined`),
92
+ // which renders nothing rather than an empty-state message.
93
+ const noSchemasRegistered = schemasObserved !== undefined && schemasObserved.length === 0;
94
+
95
+ const [selectedSchemaId, setSelectedSchemaId] = useState<string>('');
74
96
  const [selectedCategories, setSelectedCategories] = useState<Set<string>>(new Set());
97
+
98
+ // Default the schema selection to the first registered schema once
99
+ // the registry resolves. We don't reset on schemas changing to avoid
100
+ // clobbering an explicit user choice.
101
+ useEffect(() => {
102
+ if (!selectedSchemaId && schemas.length > 0) {
103
+ setSelectedSchemaId(schemas[0]!.id);
104
+ }
105
+ }, [schemas, selectedSchemaId]);
75
106
  const [focusedAnnotationId, setFocusedAnnotationId] = useState<string | null>(null);
76
107
  const containerRef = useRef<HTMLDivElement>(null);
77
108
 
@@ -159,7 +190,6 @@ export function TaggingPanel({
159
190
  // Pulse effect is handled by isHovered prop on TagEntry
160
191
  }, [hoveredAnnotationId]);
161
192
 
162
- const schemas = getAllTagSchemas();
163
193
  const selectedSchema = schemas.find(s => s.id === selectedSchemaId);
164
194
 
165
195
  const handleSchemaChange = (schemaId: string) => {
@@ -192,6 +222,10 @@ export function TaggingPanel({
192
222
  session?.client.mark.requestAssist('tagging', {
193
223
  schemaId: selectedSchemaId,
194
224
  categories: Array.from(selectedCategories),
225
+ // Body locale stamps the tagging body's `language`; sourceLanguage
226
+ // tunes the prompt for non-English source content.
227
+ language: locale,
228
+ sourceLanguage,
195
229
  });
196
230
  setSelectedCategories(new Set()); // Reset after annotation
197
231
  }
@@ -238,23 +272,32 @@ export function TaggingPanel({
238
272
  </p>
239
273
  </div>
240
274
 
275
+ {/* Empty-state — registry has resolved with no schemas. */}
276
+ {noSchemasRegistered && (
277
+ <p className="semiont-form__help" data-type="tag-no-schemas">
278
+ {t('noSchemas')}
279
+ </p>
280
+ )}
281
+
241
282
  {/* Schema and Category Selection for Manual Tag */}
242
- <div className="semiont-form-field">
243
- <label className="semiont-form-field__label">
244
- {t('selectSchema')}
245
- </label>
246
- <select
247
- value={selectedSchemaId}
248
- onChange={(e) => handleSchemaChange(e.target.value)}
249
- className="semiont-select"
250
- >
251
- {schemas.map(schema => (
252
- <option key={schema.id} value={schema.id}>
253
- {t(`schema${schema.id === 'legal-irac' ? 'Legal' : schema.id === 'scientific-imrad' ? 'Scientific' : 'Argument'}`)}
254
- </option>
255
- ))}
256
- </select>
257
- </div>
283
+ {!noSchemasRegistered && (
284
+ <div className="semiont-form-field">
285
+ <label className="semiont-form-field__label">
286
+ {t('selectSchema')}
287
+ </label>
288
+ <select
289
+ value={selectedSchemaId}
290
+ onChange={(e) => handleSchemaChange(e.target.value)}
291
+ className="semiont-select"
292
+ >
293
+ {schemas.map(schema => (
294
+ <option key={schema.id} value={schema.id}>
295
+ {schema.name}
296
+ </option>
297
+ ))}
298
+ </select>
299
+ </div>
300
+ )}
258
301
 
259
302
  {selectedSchema && (
260
303
  <div className="semiont-form-field">
@@ -324,28 +367,37 @@ export function TaggingPanel({
324
367
  <div className="semiont-assist-widget" data-assisting={isAssisting && progress ? 'true' : 'false'} data-type="tag">
325
368
  {!isAssisting && !progress && (
326
369
  <>
370
+ {/* Empty-state — registry has resolved with no schemas. */}
371
+ {noSchemasRegistered && (
372
+ <p className="semiont-form__help" data-type="tag-no-schemas">
373
+ {t('noSchemas')}
374
+ </p>
375
+ )}
376
+
327
377
  {/* Schema Selector */}
328
- <div className="semiont-form-field">
329
- <label className="semiont-form-field__label">
330
- {t('selectSchema')}
331
- </label>
332
- <select
333
- value={selectedSchemaId}
334
- onChange={(e) => handleSchemaChange(e.target.value)}
335
- className="semiont-select"
336
- >
337
- {schemas.map(schema => (
338
- <option key={schema.id} value={schema.id}>
339
- {t(`schema${schema.id === 'legal-irac' ? 'Legal' : schema.id === 'scientific-imrad' ? 'Scientific' : 'Argument'}`)}
340
- </option>
341
- ))}
342
- </select>
343
- {selectedSchema && (
344
- <p className="semiont-form__help">
345
- {selectedSchema.description}
346
- </p>
347
- )}
348
- </div>
378
+ {!noSchemasRegistered && (
379
+ <div className="semiont-form-field">
380
+ <label className="semiont-form-field__label">
381
+ {t('selectSchema')}
382
+ </label>
383
+ <select
384
+ value={selectedSchemaId}
385
+ onChange={(e) => handleSchemaChange(e.target.value)}
386
+ className="semiont-select"
387
+ >
388
+ {schemas.map(schema => (
389
+ <option key={schema.id} value={schema.id}>
390
+ {schema.name}
391
+ </option>
392
+ ))}
393
+ </select>
394
+ {selectedSchema && (
395
+ <p className="semiont-form__help">
396
+ {selectedSchema.description}
397
+ </p>
398
+ )}
399
+ </div>
400
+ )}
349
401
 
350
402
  {/* Category Selector */}
351
403
  {selectedSchema && (
@@ -388,7 +440,7 @@ export function TaggingPanel({
388
440
  style={{ display: 'flex', flexDirection: 'column', gap: '0.25rem' }}
389
441
  >
390
442
  <span style={{ fontWeight: 500 }}>
391
- {t(`category${category.name.replace(/\s+/g, '')}`)}
443
+ {category.name}
392
444
  </span>
393
445
  <span style={{ fontSize: 'var(--semiont-text-xs)', color: 'var(--semiont-text-secondary)' }}>
394
446
  {category.description}
@@ -71,9 +71,16 @@ interface UnifiedAnnotationsPanelProps {
71
71
  // Hover coordination (for bidirectional hover highlighting)
72
72
  hoveredAnnotationId?: string | null;
73
73
 
74
- // Locale for AI-generated text language
74
+ // Locale for AI-generated text language (annotation body locale)
75
75
  locale?: string;
76
76
 
77
+ /**
78
+ * BCP-47 tag of the resource being analyzed (source-resource locale).
79
+ * Independent from `locale` — a German user can analyze a French source
80
+ * and get German bodies back. Fed into detection prompts.
81
+ */
82
+ sourceLanguage?: string;
83
+
77
84
  // Routing
78
85
  Link: React.ComponentType<LinkComponentProps>;
79
86
  routes: RouteBuilder;
@@ -243,6 +250,7 @@ export function UnifiedAnnotationsPanel(props: UnifiedAnnotationsPanelProps) {
243
250
  progress,
244
251
  annotateMode: props.annotateMode,
245
252
  locale: props.locale,
253
+ sourceLanguage: props.sourceLanguage,
246
254
  scrollToAnnotationId: props.scrollToAnnotationId,
247
255
  onScrollCompleted: props.onScrollCompleted,
248
256
  hoveredAnnotationId: props.hoveredAnnotationId
@@ -268,6 +276,8 @@ export function UnifiedAnnotationsPanel(props: UnifiedAnnotationsPanelProps) {
268
276
  scrollToAnnotationId={commonProps.scrollToAnnotationId}
269
277
  onScrollCompleted={commonProps.onScrollCompleted}
270
278
  hoveredAnnotationId={commonProps.hoveredAnnotationId}
279
+ locale={commonProps.locale}
280
+ sourceLanguage={commonProps.sourceLanguage}
271
281
  allEntityTypes={props.allEntityTypes || []}
272
282
  generatingReferenceId={props.generatingReferenceId}
273
283
  referencedBy={props.referencedBy}
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Triangulation test for the VM → useObservable → prop → chip render chain.
2
+ * Triangulation test for the state unit → useObservable → prop → chip render chain.
3
3
  *
4
4
  * Written after an e2e failure (test 05) where `ReferencesPanel` rendered
5
5
  * "No entity types available" even though the client provably received a
@@ -90,7 +90,7 @@ const renderWithBus = (ui: React.ReactElement) => {
90
90
  return render(<SemiontWrapper>{ui}</SemiontWrapper>);
91
91
  };
92
92
 
93
- describe('Layer 5-6 — VM observable → useObservable → ReferencesPanel chips', () => {
93
+ describe('Layer 5-6 — state-unit observable → useObservable → ReferencesPanel chips', () => {
94
94
  it('an observable seeded with [9 strings] renders 9 pending-reference chips', async () => {
95
95
  const source$ = new BehaviorSubject<string[]>(NINE_TYPES);
96
96
  renderWithBus(<ObservableHarness source$={source$} />);
@@ -1,10 +1,12 @@
1
1
  import { describe, it, expect, beforeEach, vi } from 'vitest';
2
2
  import React from 'react';
3
- import { screen } from '@testing-library/react';
3
+ import { render, screen } from '@testing-library/react';
4
4
  import '@testing-library/jest-dom';
5
- import { renderWithProviders } from '../../../../test-utils';
5
+ import { of } from 'rxjs';
6
+ import { CacheObservable } from '@semiont/sdk';
7
+ import { renderWithProviders, createTestSemiontWrapper } from '../../../../test-utils';
6
8
  import userEvent from '@testing-library/user-event';
7
- import type { components } from '@semiont/core';
9
+ import type { components, TagSchema } from '@semiont/core';
8
10
 
9
11
  import type { Annotation } from '@semiont/core';
10
12
 
@@ -23,21 +25,15 @@ vi.mock('@semiont/ontology', () => ({
23
25
  getTagSchemaId: vi.fn(),
24
26
  }));
25
27
 
26
- // Mock tag-schemas
27
- vi.mock('../../../../lib/tag-schemas', () => ({
28
- getTagSchema: vi.fn(),
29
- }));
30
-
31
28
  import { getAnnotationExactText } from '@semiont/core';
32
29
  import { getTagCategory, getTagSchemaId } from '@semiont/ontology';
33
- import { getTagSchema } from '../../../../lib/tag-schemas';
34
30
  import type { MockedFunction } from 'vitest';
35
31
  import { TagEntry } from '../TagEntry';
36
32
 
37
33
  const mockGetAnnotationExactText = getAnnotationExactText as MockedFunction<typeof getAnnotationExactText>;
38
34
  const mockGetTagCategory = getTagCategory as MockedFunction<typeof getTagCategory>;
39
35
  const mockGetTagSchemaId = getTagSchemaId as MockedFunction<typeof getTagSchemaId>;
40
- const mockGetTagSchema = getTagSchema as MockedFunction<typeof getTagSchema>;
36
+
41
37
 
42
38
  const createMockTag = (overrides?: Partial<Annotation>): Annotation => ({
43
39
  '@context': 'http://www.w3.org/ns/anno.jsonld',
@@ -76,7 +72,6 @@ describe('TagEntry', () => {
76
72
  mockGetAnnotationExactText.mockReturnValue('Tagged text content');
77
73
  mockGetTagCategory.mockReturnValue('Entity');
78
74
  mockGetTagSchemaId.mockReturnValue(null);
79
- mockGetTagSchema.mockReturnValue(null);
80
75
  });
81
76
 
82
77
  describe('Rendering', () => {
@@ -115,24 +110,36 @@ describe('TagEntry', () => {
115
110
 
116
111
  it('should render schema name when available', () => {
117
112
  mockGetTagSchemaId.mockReturnValue('schema-ner-v1');
118
- mockGetTagSchema.mockReturnValue({
113
+ const NER_SCHEMA: TagSchema = {
119
114
  id: 'schema-ner-v1',
120
115
  name: 'Named Entity Recognition',
116
+ description: 'NER',
121
117
  domain: 'nlp',
122
- version: '1.0',
123
- categories: [],
124
- });
125
-
126
- renderWithProviders(<TagEntry {...defaultProps} />);
118
+ tags: [],
119
+ };
120
+
121
+ // Stub the cache to resolve immediately with the test schema —
122
+ // exercises the rendering path without round-tripping through the
123
+ // transport's HTTP plumbing.
124
+ const { SemiontWrapper, client } = createTestSemiontWrapper();
125
+ vi.spyOn(client.browse, 'tagSchemas').mockReturnValue(
126
+ CacheObservable.from(of([NER_SCHEMA]))
127
+ );
128
+ render(<TagEntry {...defaultProps} />, { wrapper: SemiontWrapper });
127
129
 
128
130
  expect(screen.getByText('Named Entity Recognition')).toBeInTheDocument();
129
131
  });
130
132
 
131
133
  it('should not render schema name when schema is not found', () => {
132
134
  mockGetTagSchemaId.mockReturnValue('unknown-schema');
133
- mockGetTagSchema.mockReturnValue(null);
134
135
 
135
- const { container } = renderWithProviders(<TagEntry {...defaultProps} />);
136
+ // Stub the cache to resolve to an empty list — the schema lookup
137
+ // misses, the schema-name `<span>` is not rendered.
138
+ const { SemiontWrapper, client } = createTestSemiontWrapper();
139
+ vi.spyOn(client.browse, 'tagSchemas').mockReturnValue(
140
+ CacheObservable.from(of([]))
141
+ );
142
+ const { container } = render(<TagEntry {...defaultProps} />, { wrapper: SemiontWrapper });
136
143
 
137
144
  expect(container.querySelector('.semiont-annotation-entry__meta')).not.toBeInTheDocument();
138
145
  });