@semiont/react-ui 0.5.28 → 0.5.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/{PdfAnnotationCanvas.client-C2IGG4SW.js → PdfAnnotationCanvas.client-LSH3PZPY.js} +4 -4
- package/dist/{ar-FABPTJVB.js → ar-AZBRADEB.js} +5 -1
- package/dist/{bn-LKP6BDMH.js → bn-CCEW6AOS.js} +5 -1
- package/dist/{chunk-VXASXU4K.js → chunk-3ZBKCWUZ.js} +1 -1
- package/dist/{chunk-2YHEE63S.js → chunk-G4VHL4UA.js} +31 -31
- package/dist/{chunk-2GOHTTHW.js → chunk-SDKFZLEX.js} +5 -1
- package/dist/{cs-YQYNWHRG.js → cs-HXPF2CTE.js} +5 -1
- package/dist/{da-QHZOYKRY.js → da-K5NTK7LH.js} +5 -1
- package/dist/{de-GKA7RD7J.js → de-GMRQEDZI.js} +5 -1
- package/dist/{el-45T257AL.js → el-EUB4CI4G.js} +5 -1
- package/dist/{en-RVW5EQ66.js → en-R5XA26NF.js} +2 -2
- package/dist/{es-AVER7RU4.js → es-Z32DAZ6W.js} +5 -1
- package/dist/{fa-K3VPK3O2.js → fa-G7HN6B32.js} +5 -1
- package/dist/{fi-5NEKF7EB.js → fi-3YBLZO7I.js} +5 -1
- package/dist/{fr-FUMNDLBZ.js → fr-VUBH7BBS.js} +5 -1
- package/dist/{he-3JGNA3PR.js → he-WQQEY26B.js} +5 -1
- package/dist/{hi-IJY375DF.js → hi-HJZ6W4JB.js} +5 -1
- package/dist/{id-LWHKJ7ZD.js → id-ZOGZ37MI.js} +5 -1
- package/dist/index.d.ts +157 -257
- package/dist/index.js +763 -1209
- package/dist/{it-EBVOV4RW.js → it-DT6UW66F.js} +5 -1
- package/dist/{ja-KWRIX2NF.js → ja-RUKQCOLO.js} +5 -1
- package/dist/{ko-L5CJMFDS.js → ko-ZH5ADKZM.js} +5 -1
- package/dist/{ms-LSNMJDR2.js → ms-Y2AFPZTU.js} +5 -1
- package/dist/{nl-ZB32MB7U.js → nl-OQEWIXNA.js} +5 -1
- package/dist/{no-4GTNY7M5.js → no-KPAYJZWY.js} +5 -1
- package/dist/{pl-LUHBLTHB.js → pl-WSXK2UUD.js} +5 -1
- package/dist/{pt-C4H3UNQ6.js → pt-NWRJRUCK.js} +5 -1
- package/dist/{ro-GGITS5V4.js → ro-3CGVMOC6.js} +5 -1
- package/dist/{sv-T56RTLBP.js → sv-HGSEBA7S.js} +5 -1
- package/dist/test-utils.js +16 -7
- package/dist/{th-22X2IWCB.js → th-NZ4G352Z.js} +5 -1
- package/dist/{tr-77TFG62H.js → tr-LGGS4DLW.js} +5 -1
- package/dist/{uk-MIIL6NXT.js → uk-AIX536L4.js} +5 -1
- package/dist/{vi-7JYOIGNV.js → vi-V6CVQZLQ.js} +5 -1
- package/dist/{zh-ID6DRYU4.js → zh-AGKPGI3L.js} +5 -1
- package/package.json +13 -13
- package/src/components/StatusDisplay.tsx +13 -13
- package/src/components/__tests__/StatusDisplay.test.tsx +9 -9
- package/src/components/modals/ComposeStep.tsx +19 -4
- package/src/components/modals/ConfigureGenerationStep.tsx +143 -15
- package/src/components/modals/ReferenceWizardModal.tsx +14 -9
- package/src/components/modals/ResourceGenerateModal.tsx +10 -5
- package/src/components/modals/__tests__/ConfigureGenerationStep.test.tsx +171 -8
- package/src/components/modals/__tests__/ReferenceWizardModal.test.tsx +25 -1
- package/src/components/modals/__tests__/ResourceGenerateModal.test.tsx +24 -1
- package/src/components/modals/__tests__/WizardFooter.test.tsx +3 -5
- package/src/components/resource/BrowseView.tsx +3 -3
- package/src/components/resource/DownloadFileLink.tsx +1 -1
- package/src/components/resource/__tests__/BrowseView.reference-hover.test.tsx +1 -1
- package/src/components/resource/__tests__/BrowseView.test.tsx +2 -2
- package/src/components/resource/__tests__/download-link.test.tsx +6 -6
- package/src/components/resource/panels/ReferenceEntry.tsx +2 -2
- package/src/components/resource/panels/ReferencesPanel.tsx +5 -1
- package/src/components/resource/panels/__tests__/ReferencesPanel.observable-flow.test.tsx +1 -1
- package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +51 -2
- package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +1 -1
- package/src/features/auth/__tests__/SignInForm.a11y.test.tsx +12 -12
- package/src/features/auth/components/SignInForm.tsx +53 -53
- package/src/features/auth-welcome/state/__tests__/welcome-state-unit.test.ts +2 -2
- package/src/features/moderate-recent/__tests__/RecentDocumentsPage.test.tsx +2 -2
- package/src/features/resource-compose/components/ResourceComposePage.tsx +18 -3
- package/src/features/resource-discovery/__tests__/ResourceDiscoveryPage.test.tsx +1 -1
- package/src/features/resource-discovery/components/ResourceDiscoveryPage.tsx +2 -2
- package/src/features/resource-discovery/state/discover-state-unit.ts +1 -1
- package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +2 -2
- package/src/features/resource-viewer/__tests__/generation-options.test.ts +53 -0
- package/src/features/resource-viewer/components/ResourceViewerPage.tsx +16 -25
- package/src/features/resource-viewer/generation-options.ts +29 -0
- package/src/styles/index.css +0 -1
- package/translations/ar.json +4 -0
- package/translations/bn.json +4 -0
- package/translations/cs.json +4 -0
- package/translations/da.json +4 -0
- package/translations/de.json +4 -0
- package/translations/el.json +4 -0
- package/translations/en.json +4 -0
- package/translations/es.json +4 -0
- package/translations/fa.json +4 -0
- package/translations/fi.json +4 -0
- package/translations/fr.json +4 -0
- package/translations/he.json +4 -0
- package/translations/hi.json +4 -0
- package/translations/id.json +4 -0
- package/translations/it.json +4 -0
- package/translations/ja.json +4 -0
- package/translations/ko.json +4 -0
- package/translations/ms.json +4 -0
- package/translations/nl.json +4 -0
- package/translations/no.json +4 -0
- package/translations/pl.json +4 -0
- package/translations/pt.json +4 -0
- package/translations/ro.json +4 -0
- package/translations/sv.json +4 -0
- package/translations/th.json +4 -0
- package/translations/tr.json +4 -0
- package/translations/uk.json +4 -0
- package/translations/vi.json +4 -0
- package/translations/zh.json +4 -0
- package/src/features/admin-exchange/__tests__/AdminExchangePage.test.tsx +0 -137
- package/src/features/admin-exchange/__tests__/ExportCard.test.tsx +0 -41
- package/src/features/admin-exchange/__tests__/ImportCard.test.tsx +0 -148
- package/src/features/admin-exchange/__tests__/ImportProgress.test.tsx +0 -117
- package/src/features/admin-exchange/components/AdminExchangePage.tsx +0 -117
- package/src/features/admin-exchange/components/ExportCard.tsx +0 -35
- package/src/features/admin-exchange/components/ImportCard.tsx +0 -183
- package/src/features/admin-exchange/components/ImportProgress.tsx +0 -80
- package/src/features/admin-exchange/state/__tests__/exchange-state-unit.test.ts +0 -185
- package/src/features/admin-exchange/state/exchange-state-unit.ts +0 -133
- package/src/features/moderation-linked-data/__tests__/LinkedDataPage.test.tsx +0 -118
- package/src/features/moderation-linked-data/components/LinkedDataPage.tsx +0 -118
- package/src/styles/features/exchange.css +0 -374
|
@@ -29,8 +29,8 @@ const mockTranslations = {
|
|
|
29
29
|
welcomeBack: 'Welcome back to Semiont',
|
|
30
30
|
signInPrompt: 'Sign in to your knowledge workspace',
|
|
31
31
|
continueWithGoogle: 'Continue with Google',
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
gatewayUrlLabel: 'Gateway URL',
|
|
33
|
+
gatewayUrlPlaceholder: 'http://localhost:4000',
|
|
34
34
|
emailLabel: 'Email',
|
|
35
35
|
emailPlaceholder: 'your@email.com',
|
|
36
36
|
passwordLabel: 'Password',
|
|
@@ -44,9 +44,9 @@ const mockTranslations = {
|
|
|
44
44
|
signUpInstead: 'Sign Up Instead',
|
|
45
45
|
errorEmailRequired: 'Email is required',
|
|
46
46
|
errorPasswordRequired: 'Password is required',
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
errorGatewayUrlRequired: 'Gateway URL is required',
|
|
48
|
+
errorGatewayUrlInvalid: 'Gateway URL is not valid',
|
|
49
|
+
errorGatewayUrlUnreachable: 'Gateway URL is unreachable',
|
|
50
50
|
tagline: 'make meaning',
|
|
51
51
|
};
|
|
52
52
|
|
|
@@ -58,7 +58,7 @@ describe('SignInForm - Accessibility', () => {
|
|
|
58
58
|
const { container } = render(
|
|
59
59
|
<SignInForm
|
|
60
60
|
onGoogleSignIn={onGoogleSignIn}
|
|
61
|
-
|
|
61
|
+
gatewayUrl="http://localhost:4000"
|
|
62
62
|
Link={MockLink}
|
|
63
63
|
translations={mockTranslations}
|
|
64
64
|
/>
|
|
@@ -206,7 +206,7 @@ describe('SignInForm - Accessibility', () => {
|
|
|
206
206
|
render(
|
|
207
207
|
<SignInForm
|
|
208
208
|
onGoogleSignIn={onGoogleSignIn}
|
|
209
|
-
|
|
209
|
+
gatewayUrl="http://localhost:4000"
|
|
210
210
|
Link={MockLink}
|
|
211
211
|
translations={mockTranslations}
|
|
212
212
|
/>
|
|
@@ -244,7 +244,7 @@ describe('SignInForm - Accessibility', () => {
|
|
|
244
244
|
render(
|
|
245
245
|
<SignInForm
|
|
246
246
|
onGoogleSignIn={onGoogleSignIn}
|
|
247
|
-
|
|
247
|
+
gatewayUrl="http://localhost:4000"
|
|
248
248
|
Link={MockLink}
|
|
249
249
|
translations={mockTranslations}
|
|
250
250
|
/>
|
|
@@ -261,7 +261,7 @@ describe('SignInForm - Accessibility', () => {
|
|
|
261
261
|
render(
|
|
262
262
|
<SignInForm
|
|
263
263
|
onGoogleSignIn={onGoogleSignIn}
|
|
264
|
-
|
|
264
|
+
gatewayUrl="http://localhost:4000"
|
|
265
265
|
Link={MockLink}
|
|
266
266
|
translations={mockTranslations}
|
|
267
267
|
/>
|
|
@@ -361,7 +361,7 @@ describe('SignInForm - Accessibility', () => {
|
|
|
361
361
|
render(
|
|
362
362
|
<SignInForm
|
|
363
363
|
onGoogleSignIn={onGoogleSignIn}
|
|
364
|
-
|
|
364
|
+
gatewayUrl="http://localhost:4000"
|
|
365
365
|
Link={MockLink}
|
|
366
366
|
translations={mockTranslations}
|
|
367
367
|
/>
|
|
@@ -379,7 +379,7 @@ describe('SignInForm - Accessibility', () => {
|
|
|
379
379
|
const { container } = render(
|
|
380
380
|
<SignInForm
|
|
381
381
|
onGoogleSignIn={onGoogleSignIn}
|
|
382
|
-
|
|
382
|
+
gatewayUrl="http://localhost:4000"
|
|
383
383
|
Link={MockLink}
|
|
384
384
|
translations={mockTranslations}
|
|
385
385
|
/>
|
|
@@ -397,7 +397,7 @@ describe('SignInForm - Accessibility', () => {
|
|
|
397
397
|
const { container } = render(
|
|
398
398
|
<SignInForm
|
|
399
399
|
onGoogleSignIn={onGoogleSignIn}
|
|
400
|
-
|
|
400
|
+
gatewayUrl="http://localhost:4000"
|
|
401
401
|
Link={MockLink}
|
|
402
402
|
translations={mockTranslations}
|
|
403
403
|
/>
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* Supports both Google OAuth and email/password credentials.
|
|
5
5
|
* No Next.js dependencies - all data via props.
|
|
6
6
|
*
|
|
7
|
-
* When
|
|
8
|
-
* When
|
|
7
|
+
* When gatewayUrl is provided it is shown as a locked read-only field.
|
|
8
|
+
* When gatewayUrl is omitted the user must enter a gateway URL to connect to.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import React from 'react';
|
|
@@ -15,21 +15,21 @@ import '../auth.css';
|
|
|
15
15
|
export interface SignInFormProps {
|
|
16
16
|
/**
|
|
17
17
|
* Callback when user clicks Google sign-in.
|
|
18
|
-
* Receives the
|
|
18
|
+
* Receives the gateway URL (either the locked one or what the user typed).
|
|
19
19
|
*/
|
|
20
|
-
onGoogleSignIn: (
|
|
20
|
+
onGoogleSignIn: (gatewayUrl: string) => Promise<void>;
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* Callback when user submits email/password credentials.
|
|
24
|
-
* Receives the
|
|
24
|
+
* Receives the gateway URL along with email and password.
|
|
25
25
|
*/
|
|
26
|
-
onCredentialsSignIn?: ((
|
|
26
|
+
onCredentialsSignIn?: ((gatewayUrl: string, email: string, password: string) => Promise<void>) | undefined;
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
|
-
* Pre-filled
|
|
29
|
+
* Pre-filled gateway URL. When provided the field is read-only (re-auth to known workspace).
|
|
30
30
|
* When omitted the user enters the URL themselves (new connection).
|
|
31
31
|
*/
|
|
32
|
-
|
|
32
|
+
gatewayUrl?: string;
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
35
|
* Error message to display (if any)
|
|
@@ -59,8 +59,8 @@ export interface SignInFormProps {
|
|
|
59
59
|
welcomeBack: string;
|
|
60
60
|
signInPrompt: string;
|
|
61
61
|
continueWithGoogle: string;
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
gatewayUrlLabel: string;
|
|
63
|
+
gatewayUrlPlaceholder: string;
|
|
64
64
|
emailLabel: string;
|
|
65
65
|
emailPlaceholder: string;
|
|
66
66
|
passwordLabel: string;
|
|
@@ -72,9 +72,9 @@ export interface SignInFormProps {
|
|
|
72
72
|
backToHome: string;
|
|
73
73
|
learnMore: string;
|
|
74
74
|
signUpInstead: string;
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
75
|
+
errorGatewayUrlRequired: string;
|
|
76
|
+
errorGatewayUrlInvalid: string;
|
|
77
|
+
errorGatewayUrlUnreachable: string;
|
|
78
78
|
errorEmailRequired: string;
|
|
79
79
|
errorPasswordRequired: string;
|
|
80
80
|
tagline: string;
|
|
@@ -117,23 +117,23 @@ function normalizeUrlForProbe(raw: string): string | null {
|
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
/**
|
|
120
|
-
* CredentialsAuthForm -
|
|
121
|
-
* When
|
|
120
|
+
* CredentialsAuthForm - Gateway URL + email/password form.
|
|
121
|
+
* When lockedGatewayUrl is provided, the URL field is shown read-only.
|
|
122
122
|
*/
|
|
123
123
|
function CredentialsAuthForm({
|
|
124
|
-
|
|
124
|
+
lockedGatewayUrl,
|
|
125
125
|
onSubmit,
|
|
126
126
|
translations: t,
|
|
127
127
|
}: {
|
|
128
|
-
|
|
129
|
-
onSubmit: (
|
|
128
|
+
lockedGatewayUrl: string | undefined;
|
|
129
|
+
onSubmit: (gatewayUrl: string, email: string, password: string) => Promise<void>;
|
|
130
130
|
translations: SignInFormProps['translations'];
|
|
131
131
|
}) {
|
|
132
|
-
const [
|
|
132
|
+
const [gatewayUrl, setGatewayUrl] = React.useState(lockedGatewayUrl ?? '');
|
|
133
133
|
const [email, setEmail] = React.useState('');
|
|
134
134
|
const [password, setPassword] = React.useState('');
|
|
135
135
|
const [validationError, setValidationError] = React.useState<string | null>(null);
|
|
136
|
-
const [fieldErrors, setFieldErrors] = React.useState<{
|
|
136
|
+
const [fieldErrors, setFieldErrors] = React.useState<{ gatewayUrl?: string; email?: string; password?: string }>({});
|
|
137
137
|
const [urlProbeStatus, setUrlProbeStatus] = React.useState<UrlProbeStatus>('idle');
|
|
138
138
|
|
|
139
139
|
const probeUrl = async (raw: string) => {
|
|
@@ -141,7 +141,7 @@ function CredentialsAuthForm({
|
|
|
141
141
|
if (!url) return;
|
|
142
142
|
// Validate format
|
|
143
143
|
try { new URL(url); } catch {
|
|
144
|
-
setFieldErrors(prev => ({ ...prev,
|
|
144
|
+
setFieldErrors(prev => ({ ...prev, gatewayUrl: t.errorGatewayUrlInvalid }));
|
|
145
145
|
setUrlProbeStatus('error');
|
|
146
146
|
return;
|
|
147
147
|
}
|
|
@@ -150,19 +150,19 @@ function CredentialsAuthForm({
|
|
|
150
150
|
const res = await fetch(`${url}/api/health`, { signal: AbortSignal.timeout(5000) });
|
|
151
151
|
setUrlProbeStatus(res.ok ? 'ok' : 'error');
|
|
152
152
|
if (!res.ok) {
|
|
153
|
-
setFieldErrors(prev => ({ ...prev,
|
|
153
|
+
setFieldErrors(prev => ({ ...prev, gatewayUrl: t.errorGatewayUrlUnreachable }));
|
|
154
154
|
}
|
|
155
155
|
} catch {
|
|
156
156
|
setUrlProbeStatus('error');
|
|
157
|
-
setFieldErrors(prev => ({ ...prev,
|
|
157
|
+
setFieldErrors(prev => ({ ...prev, gatewayUrl: t.errorGatewayUrlUnreachable }));
|
|
158
158
|
}
|
|
159
159
|
};
|
|
160
160
|
|
|
161
161
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
162
162
|
e.preventDefault();
|
|
163
|
-
const errors: {
|
|
163
|
+
const errors: { gatewayUrl?: string; email?: string; password?: string } = {};
|
|
164
164
|
|
|
165
|
-
if (!
|
|
165
|
+
if (!gatewayUrl.trim()) errors.gatewayUrl = t.errorGatewayUrlRequired;
|
|
166
166
|
if (!email) errors.email = t.errorEmailRequired;
|
|
167
167
|
if (!password) errors.password = t.errorPasswordRequired;
|
|
168
168
|
|
|
@@ -174,10 +174,10 @@ function CredentialsAuthForm({
|
|
|
174
174
|
|
|
175
175
|
setFieldErrors({});
|
|
176
176
|
setValidationError(null);
|
|
177
|
-
await onSubmit(
|
|
177
|
+
await onSubmit(gatewayUrl.trim(), email, password);
|
|
178
178
|
};
|
|
179
179
|
|
|
180
|
-
const probeIndicator = !
|
|
180
|
+
const probeIndicator = !lockedGatewayUrl && urlProbeStatus !== 'idle' ? (
|
|
181
181
|
<span
|
|
182
182
|
className={`semiont-form__url-status semiont-form__url-status--${urlProbeStatus}`}
|
|
183
183
|
aria-live="polite"
|
|
@@ -198,33 +198,33 @@ function CredentialsAuthForm({
|
|
|
198
198
|
|
|
199
199
|
<form onSubmit={handleSubmit} className="semiont-auth__form" noValidate>
|
|
200
200
|
<div className="semiont-form__field">
|
|
201
|
-
<label htmlFor="
|
|
202
|
-
{t.
|
|
201
|
+
<label htmlFor="gateway-url" className="semiont-form__label">
|
|
202
|
+
{t.gatewayUrlLabel}
|
|
203
203
|
</label>
|
|
204
204
|
<div className="semiont-form__input-row">
|
|
205
205
|
<input
|
|
206
|
-
id="
|
|
206
|
+
id="gateway-url"
|
|
207
207
|
type="url"
|
|
208
|
-
value={
|
|
208
|
+
value={gatewayUrl}
|
|
209
209
|
onChange={(e) => {
|
|
210
|
-
if (
|
|
211
|
-
|
|
210
|
+
if (lockedGatewayUrl) return;
|
|
211
|
+
setGatewayUrl(e.target.value);
|
|
212
212
|
setUrlProbeStatus('idle');
|
|
213
|
-
if (fieldErrors.
|
|
213
|
+
if (fieldErrors.gatewayUrl) setFieldErrors({ ...fieldErrors, gatewayUrl: undefined });
|
|
214
214
|
}}
|
|
215
|
-
onBlur={() => { if (!
|
|
216
|
-
readOnly={!!
|
|
217
|
-
placeholder={t.
|
|
218
|
-
className={`semiont-input${
|
|
219
|
-
aria-invalid={!!fieldErrors.
|
|
220
|
-
aria-describedby={fieldErrors.
|
|
215
|
+
onBlur={() => { if (!lockedGatewayUrl) probeUrl(gatewayUrl); }}
|
|
216
|
+
readOnly={!!lockedGatewayUrl}
|
|
217
|
+
placeholder={t.gatewayUrlPlaceholder}
|
|
218
|
+
className={`semiont-input${lockedGatewayUrl ? ' semiont-input--readonly' : ''}`}
|
|
219
|
+
aria-invalid={!!fieldErrors.gatewayUrl}
|
|
220
|
+
aria-describedby={fieldErrors.gatewayUrl ? 'gateway-url-error' : undefined}
|
|
221
221
|
required
|
|
222
222
|
/>
|
|
223
223
|
{probeIndicator}
|
|
224
224
|
</div>
|
|
225
|
-
{fieldErrors.
|
|
226
|
-
<span id="
|
|
227
|
-
{fieldErrors.
|
|
225
|
+
{fieldErrors.gatewayUrl && (
|
|
226
|
+
<span id="gateway-url-error" className="semiont-form__error" role="alert">
|
|
227
|
+
{fieldErrors.gatewayUrl}
|
|
228
228
|
</span>
|
|
229
229
|
)}
|
|
230
230
|
</div>
|
|
@@ -295,23 +295,23 @@ function CredentialsAuthForm({
|
|
|
295
295
|
/**
|
|
296
296
|
* SignInForm - Main sign-in / connect component.
|
|
297
297
|
*
|
|
298
|
-
* When
|
|
299
|
-
* When
|
|
298
|
+
* When gatewayUrl is provided (re-auth to known workspace) the URL field is locked.
|
|
299
|
+
* When gatewayUrl is omitted (new connection) the user enters the URL themselves.
|
|
300
300
|
*/
|
|
301
301
|
export function SignInForm({
|
|
302
302
|
onGoogleSignIn,
|
|
303
303
|
onCredentialsSignIn,
|
|
304
|
-
|
|
304
|
+
gatewayUrl,
|
|
305
305
|
error,
|
|
306
306
|
showCredentialsAuth = false,
|
|
307
307
|
isLoading = false,
|
|
308
308
|
translations: t,
|
|
309
309
|
}: SignInFormProps) {
|
|
310
310
|
const handleGoogleClick = () => {
|
|
311
|
-
// For Google auth we need the
|
|
312
|
-
// The caller is responsible for ensuring
|
|
313
|
-
if (
|
|
314
|
-
onGoogleSignIn(
|
|
311
|
+
// For Google auth we need the gateway URL from the locked prop or we cannot proceed.
|
|
312
|
+
// The caller is responsible for ensuring gatewayUrl is set when Google sign-in is offered.
|
|
313
|
+
if (gatewayUrl) {
|
|
314
|
+
onGoogleSignIn(gatewayUrl);
|
|
315
315
|
}
|
|
316
316
|
};
|
|
317
317
|
|
|
@@ -348,14 +348,14 @@ export function SignInForm({
|
|
|
348
348
|
<>
|
|
349
349
|
{showCredentialsAuth && onCredentialsSignIn && (
|
|
350
350
|
<CredentialsAuthForm
|
|
351
|
-
|
|
351
|
+
lockedGatewayUrl={gatewayUrl}
|
|
352
352
|
onSubmit={onCredentialsSignIn}
|
|
353
353
|
translations={t}
|
|
354
354
|
/>
|
|
355
355
|
)}
|
|
356
356
|
|
|
357
|
-
{/* Google sign-in only shown when a
|
|
358
|
-
{
|
|
357
|
+
{/* Google sign-in only shown when a gateway URL is known */}
|
|
358
|
+
{gatewayUrl && (
|
|
359
359
|
<button onClick={handleGoogleClick} className={`${buttonStyles.primary.base} semiont-button--full-width`}>
|
|
360
360
|
<GoogleIcon />
|
|
361
361
|
{t.continueWithGoogle}
|
|
@@ -3,7 +3,7 @@ import { firstValueFrom } from 'rxjs';
|
|
|
3
3
|
import { filter } from 'rxjs/operators';
|
|
4
4
|
import type { SemiontSession } from '@semiont/sdk';
|
|
5
5
|
import { AuthNamespace } from '@semiont/sdk';
|
|
6
|
-
import { createTestSession,
|
|
6
|
+
import { createTestSession, stubGateway } from '@semiont/sdk/testing';
|
|
7
7
|
import { createWelcomeStateUnit } from '../welcome-state-unit';
|
|
8
8
|
import { assertStateUnitAxioms } from '@semiont/core/testing/axioms';
|
|
9
9
|
|
|
@@ -25,7 +25,7 @@ afterEach(() => {
|
|
|
25
25
|
});
|
|
26
26
|
|
|
27
27
|
function makeSession(): SemiontSession {
|
|
28
|
-
return createTestSession({
|
|
28
|
+
return createTestSession({ gateway: stubGateway() }).session;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
describe('createWelcomeStateUnit', () => {
|
|
@@ -368,8 +368,8 @@ describe('RecentDocumentsPage', () => {
|
|
|
368
368
|
const props = createMockProps();
|
|
369
369
|
const { container } = render(<RecentDocumentsPage {...props} />);
|
|
370
370
|
|
|
371
|
-
const svg = container.querySelector('svg[viewBox
|
|
372
|
-
expect(svg).
|
|
371
|
+
const svg = container.querySelector('svg[viewBox]');
|
|
372
|
+
expect(svg).toHaveAttribute('viewBox', '0 0 24 24');
|
|
373
373
|
});
|
|
374
374
|
|
|
375
375
|
it('renders SVG path with correct attributes', () => {
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import React, { useState, useEffect } from 'react';
|
|
11
|
-
import { capabilitiesOf, AUTHORABLE_MEDIA_TYPES, MEDIA_TYPES, mediaTypeForExtension, isSupportedMediaType, LOCALES } from '@semiont/core';
|
|
11
|
+
import { capabilitiesOf, AUTHORABLE_MEDIA_TYPES, MEDIA_TYPES, mediaTypeForExtension, isSupportedMediaType, LOCALES, folderOf, proposeStoragePath, getStorageUri } from '@semiont/core';
|
|
12
|
+
import type { SupportedMediaType } from '@semiont/core';
|
|
12
13
|
import type { UploadProgress } from '@semiont/sdk';
|
|
13
14
|
import { type CloneData } from '../state/compose-page-state-unit';
|
|
14
15
|
import { COMMON_PANELS, type ToolbarPanelType } from '../../../state/shell-state-unit';
|
|
@@ -168,6 +169,20 @@ export function ResourceComposePage({
|
|
|
168
169
|
// Archive original checkbox (for clones only)
|
|
169
170
|
const [archiveOriginal, setArchiveOriginal] = useState(true);
|
|
170
171
|
|
|
172
|
+
// D11 — the Save location starts filled and follows the title until the user
|
|
173
|
+
// takes it over; `pathTouched` is derived, so clearing restores the proposal.
|
|
174
|
+
// A clone proposes beside its source; a new resource proposes at the root.
|
|
175
|
+
const composeFormat: SupportedMediaType =
|
|
176
|
+
isSupportedMediaType(selectedFormat) ? selectedFormat : 'text/markdown';
|
|
177
|
+
const pathTouched = storagePath !== '';
|
|
178
|
+
const effectivePath = pathTouched
|
|
179
|
+
? storagePath
|
|
180
|
+
: proposeStoragePath(
|
|
181
|
+
mode === 'clone' && cloneData ? folderOf(getStorageUri(cloneData.sourceResource)) : '',
|
|
182
|
+
newResourceName,
|
|
183
|
+
composeFormat,
|
|
184
|
+
);
|
|
185
|
+
|
|
171
186
|
// Initialize form data based on mode
|
|
172
187
|
useEffect(() => {
|
|
173
188
|
if (mode === 'clone' && cloneData) {
|
|
@@ -234,7 +249,7 @@ export function ResourceComposePage({
|
|
|
234
249
|
const params: SaveResourceParams = {
|
|
235
250
|
mode,
|
|
236
251
|
name: newResourceName,
|
|
237
|
-
storageUri: `file://${
|
|
252
|
+
storageUri: `file://${effectivePath}`,
|
|
238
253
|
content: newResourceContent,
|
|
239
254
|
format: uploadedFile ? fileMimeType : selectedFormat,
|
|
240
255
|
entityTypes: selectedEntityTypes,
|
|
@@ -316,7 +331,7 @@ export function ResourceComposePage({
|
|
|
316
331
|
<input
|
|
317
332
|
id="storagePath"
|
|
318
333
|
type="text"
|
|
319
|
-
value={
|
|
334
|
+
value={effectivePath}
|
|
320
335
|
onChange={(e) => setStoragePath(e.target.value)}
|
|
321
336
|
placeholder="docs/my-resource.md"
|
|
322
337
|
required
|
|
@@ -354,7 +354,7 @@ describe('ResourceDiscoveryPage', () => {
|
|
|
354
354
|
});
|
|
355
355
|
|
|
356
356
|
it('renders the recentDocuments prop as-is without applying any post-filter', () => {
|
|
357
|
-
// The component is now controlled —
|
|
357
|
+
// The component is now controlled — gateway filtering means
|
|
358
358
|
// `recentDocuments` already contains only the resources matching the
|
|
359
359
|
// active `selectedEntityType`. The component must not re-filter.
|
|
360
360
|
const props = createMockProps({
|
|
@@ -44,7 +44,7 @@ export interface ResourceDiscoveryPageProps {
|
|
|
44
44
|
onSearchQueryChange: (query: string) => void;
|
|
45
45
|
|
|
46
46
|
// Controlled entity-type filter — owned by the state unit so filtering
|
|
47
|
-
// pushes to the
|
|
47
|
+
// pushes to the gateway rather than running as a post-fetch array filter.
|
|
48
48
|
selectedEntityType: string;
|
|
49
49
|
onSelectedEntityTypeChange: (entityType: string) => void;
|
|
50
50
|
|
|
@@ -106,7 +106,7 @@ export function ResourceDiscoveryPage({
|
|
|
106
106
|
const hasSearchQuery = searchQuery.trim() !== '';
|
|
107
107
|
|
|
108
108
|
// When searching, render search results; otherwise render recent.
|
|
109
|
-
// Both already arrive entity-type-filtered from the
|
|
109
|
+
// Both already arrive entity-type-filtered from the gateway — no post-filter here.
|
|
110
110
|
const filteredResources = hasSearchQuery ? searchDocuments : recentDocuments;
|
|
111
111
|
|
|
112
112
|
// Roving tabindex for entity type filters
|
|
@@ -50,7 +50,7 @@ export function createDiscoverStateUnit(
|
|
|
50
50
|
|
|
51
51
|
// Selected entity-type chip on the Discover page. Drives both the
|
|
52
52
|
// `recent` list and the search results — filtering happens on the
|
|
53
|
-
//
|
|
53
|
+
// gateway, not via post-fetch array filtering.
|
|
54
54
|
const selectedEntityType$ = new BehaviorSubject<string>('');
|
|
55
55
|
disposer.add(() => selectedEntityType$.complete());
|
|
56
56
|
|
|
@@ -47,8 +47,8 @@ const { stubBrowser, stubClient } = vi.hoisted(() => {
|
|
|
47
47
|
const { SemiontClient, HttpTransport, HttpContentTransport } = require('@semiont/sdk');
|
|
48
48
|
const { baseUrl } = require('@semiont/core');
|
|
49
49
|
const transport = new HttpTransport({ baseUrl: baseUrl('http://localhost:4000') });
|
|
50
|
-
// HttpTransport implements both ITransport and
|
|
51
|
-
// as
|
|
50
|
+
// HttpTransport implements both ITransport and IGatewayOperations; pass it
|
|
51
|
+
// as gateway so `client.auth` (used by useMediaToken) is wired.
|
|
52
52
|
const client = new SemiontClient(transport, new HttpContentTransport(transport), transport);
|
|
53
53
|
const stubActiveSession$ = new BehaviorSubject({ client });
|
|
54
54
|
const stubOpenResources$ = new BehaviorSubject([]);
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GENERATION-OUTPUT-FORMAT D8 — the submitted config becomes generation
|
|
3
|
+
* options in ONE place, by spread.
|
|
4
|
+
*
|
|
5
|
+
* Both page handlers used to build the options object field-by-field, so any
|
|
6
|
+
* field not explicitly listed was silently dropped. That is precisely what
|
|
7
|
+
* stranded `outputMediaType` in the SDK one layer down (P1), relocated one
|
|
8
|
+
* layer up. These pins state the PROPERTY — everything the config carries is
|
|
9
|
+
* forwarded — rather than enumerating today's fields, so a knob added later
|
|
10
|
+
* cannot regress the mode back into existence.
|
|
11
|
+
*/
|
|
12
|
+
import { describe, it, expect } from 'vitest';
|
|
13
|
+
import type { GatheredContext } from '@semiont/core';
|
|
14
|
+
import { toGenerationOptions } from '../generation-options';
|
|
15
|
+
import type { GenerationConfig } from '../../../components/modals/ConfigureGenerationStep';
|
|
16
|
+
|
|
17
|
+
const context = { focus: { kind: 'resource' } } as unknown as GatheredContext;
|
|
18
|
+
|
|
19
|
+
const config: GenerationConfig = {
|
|
20
|
+
title: 'Cedar County',
|
|
21
|
+
storageUri: 'file://research/notes.pdf',
|
|
22
|
+
prompt: 'lead with the treaty',
|
|
23
|
+
language: 'en',
|
|
24
|
+
temperature: 0.7,
|
|
25
|
+
maxTokens: 500,
|
|
26
|
+
outputMediaType: 'application/pdf',
|
|
27
|
+
context,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
describe('toGenerationOptions', () => {
|
|
31
|
+
it('forwards EVERY field the config carries except the context', () => {
|
|
32
|
+
// Stated structurally on purpose: the expectation is derived from the
|
|
33
|
+
// same object, so adding a field to GenerationConfig extends this pin
|
|
34
|
+
// automatically. A field-by-field assertion would have passed happily
|
|
35
|
+
// while the real handler dropped the new one.
|
|
36
|
+
const { context: _positional, ...forwarded } = config;
|
|
37
|
+
expect(toGenerationOptions(config, 'fr')).toMatchObject(forwarded);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('does not pass the context as an option — it is the positional argument', () => {
|
|
41
|
+
expect(toGenerationOptions(config, 'fr')).not.toHaveProperty('context');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('injects the sourceLanguage, which comes from the viewed resource and not the form', () => {
|
|
45
|
+
expect(toGenerationOptions(config, 'fr').sourceLanguage).toBe('fr');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('omits sourceLanguage entirely when the resource has no language', () => {
|
|
49
|
+
// Absence must stay absence: a manufactured '' would tell the prompt the
|
|
50
|
+
// source is in a language named empty string.
|
|
51
|
+
expect(toGenerationOptions(config, undefined)).not.toHaveProperty('sourceLanguage');
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -9,9 +9,9 @@ import React, { useState, useEffect, useCallback } from 'react';
|
|
|
9
9
|
import { useResourceViewedReport } from '../hooks/useResourceViewedReport';
|
|
10
10
|
import type { components, ResourceDescriptor, ResourceId, EventMap } from '@semiont/core';
|
|
11
11
|
import type { ConnectionState } from '@semiont/core';
|
|
12
|
-
import { annotationId } from '@semiont/core';
|
|
12
|
+
import { annotationId, folderOf } from '@semiont/core';
|
|
13
13
|
import type { ComposeParams } from '../../../components/modals/ComposeStep';
|
|
14
|
-
import { getLanguage, getPrimaryRepresentation, getPrimaryMediaType, capabilitiesOf, extensionForMediaType } from '@semiont/core';
|
|
14
|
+
import { getLanguage, getPrimaryRepresentation, getPrimaryMediaType, getStorageUri, capabilitiesOf, extensionForMediaType } from '@semiont/core';
|
|
15
15
|
import { ANNOTATORS } from '@semiont/react-ui';
|
|
16
16
|
import { ErrorBoundary } from '@semiont/react-ui';
|
|
17
17
|
import { AnnotationHistory } from '@semiont/react-ui';
|
|
@@ -47,6 +47,7 @@ import { useTranslations } from '../../../contexts/TranslationContext';
|
|
|
47
47
|
import { ReferenceWizardModal } from '../../../components/modals/ReferenceWizardModal';
|
|
48
48
|
import { ResourceGenerateModal } from '../../../components/modals/ResourceGenerateModal';
|
|
49
49
|
import type { GenerationConfig } from '../../../components/modals/ConfigureGenerationStep';
|
|
50
|
+
import { toGenerationOptions } from '../generation-options';
|
|
50
51
|
|
|
51
52
|
type SemiontResource = ResourceDescriptor;
|
|
52
53
|
|
|
@@ -275,18 +276,10 @@ export function ResourceViewerPage({
|
|
|
275
276
|
|
|
276
277
|
const handleWizardGenerateSubmit = useCallback((referenceId: string, config: GenerationConfig) => {
|
|
277
278
|
clearSparkle(annotationId(referenceId));
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
language: config.language,
|
|
283
|
-
// The source resource is the one the user is viewing — fed into the
|
|
284
|
-
// prompt so the LLM understands the embedded context (selected
|
|
285
|
-
// passage, surrounding text) regardless of UI/target language.
|
|
286
|
-
sourceLanguage: getLanguage(resource),
|
|
287
|
-
temperature: config.temperature,
|
|
288
|
-
maxTokens: config.maxTokens,
|
|
289
|
-
});
|
|
279
|
+
// D8: forwarded by spread in ONE place, so a knob added to the form is
|
|
280
|
+
// never dropped on the way to the wire. `sourceLanguage` is the viewed
|
|
281
|
+
// resource's language — a page fact the form cannot know.
|
|
282
|
+
stateUnit?.yield.generate(config.context, toGenerationOptions(config, getLanguage(resource)));
|
|
290
283
|
}, [stateUnit, clearSparkle, resource]);
|
|
291
284
|
|
|
292
285
|
// Resource-generate flow (GENERATE-FROM-BUTTON): drive the SAME yield progress$
|
|
@@ -294,15 +287,7 @@ export function ResourceViewerPage({
|
|
|
294
287
|
// toast. Both paths are one `generate(context, options)` now: the context's
|
|
295
288
|
// focus.kind (resource here, annotation above) decides the shape.
|
|
296
289
|
const handleResourceGenerateSubmit = useCallback((_resourceId: string, config: GenerationConfig) => {
|
|
297
|
-
stateUnit?.yield.generate(config.context,
|
|
298
|
-
title: config.title,
|
|
299
|
-
storageUri: config.storagePath,
|
|
300
|
-
...(config.prompt ? { prompt: config.prompt } : {}),
|
|
301
|
-
language: config.language,
|
|
302
|
-
sourceLanguage: getLanguage(resource),
|
|
303
|
-
temperature: config.temperature,
|
|
304
|
-
maxTokens: config.maxTokens,
|
|
305
|
-
});
|
|
290
|
+
stateUnit?.yield.generate(config.context, toGenerationOptions(config, getLanguage(resource)));
|
|
306
291
|
}, [stateUnit, resource]);
|
|
307
292
|
|
|
308
293
|
const handleWizardLinkResource = useCallback(async (referenceId: string, targetResourceId: string) => {
|
|
@@ -364,7 +349,7 @@ export function ResourceViewerPage({
|
|
|
364
349
|
useEffect(() => {
|
|
365
350
|
if (resource && rUri) {
|
|
366
351
|
const mediaType = getPrimaryMediaType(resource);
|
|
367
|
-
browser.addOpenResource(rUri, resource.name, mediaType || undefined, resource
|
|
352
|
+
browser.addOpenResource(rUri, resource.name, mediaType || undefined, getStorageUri(resource));
|
|
368
353
|
browser.setLastViewedResource(rUri);
|
|
369
354
|
}
|
|
370
355
|
}, [resource, rUri, browser]);
|
|
@@ -671,7 +656,7 @@ export function ResourceViewerPage({
|
|
|
671
656
|
documentLocale={getLanguage(resource)}
|
|
672
657
|
primaryMediaType={primaryMediaType}
|
|
673
658
|
primaryByteSize={primaryByteSize}
|
|
674
|
-
storageUri={resource
|
|
659
|
+
storageUri={getStorageUri(resource)}
|
|
675
660
|
isArchived={resource.archived ?? false}
|
|
676
661
|
dateCreated={resource.dateCreated}
|
|
677
662
|
dateModified={resource.dateModified}
|
|
@@ -724,6 +709,7 @@ export function ResourceViewerPage({
|
|
|
724
709
|
annotationId={wizardAnnotationId}
|
|
725
710
|
resourceId={wizardResourceId}
|
|
726
711
|
defaultTitle={wizardDefaultTitle}
|
|
712
|
+
defaultFolder={folderOf(getStorageUri(resource))}
|
|
727
713
|
entityTypes={wizardEntityTypes}
|
|
728
714
|
resourceName={resource.name}
|
|
729
715
|
locale={locale}
|
|
@@ -774,6 +760,8 @@ export function ResourceViewerPage({
|
|
|
774
760
|
maxLength: tw('maxLength'),
|
|
775
761
|
maxLengthHelp: tw('maxLengthHelp'),
|
|
776
762
|
maxLengthCeiling: tw('maxLengthCeiling'),
|
|
763
|
+
outputFormat: tw('outputFormat'),
|
|
764
|
+
formatExtensionMismatch: tw('formatExtensionMismatch'),
|
|
777
765
|
maxResults: tw('maxResults'),
|
|
778
766
|
semanticScoring: tw('semanticScoring'),
|
|
779
767
|
semanticScoringHelp: tw('semanticScoringHelp'),
|
|
@@ -797,6 +785,7 @@ export function ResourceViewerPage({
|
|
|
797
785
|
// Seed the proposed title from the source resource's name (GFR D4/A4);
|
|
798
786
|
// the field stays editable and required.
|
|
799
787
|
defaultTitle={resource.name}
|
|
788
|
+
defaultFolder={folderOf(getStorageUri(resource))}
|
|
800
789
|
locale={locale}
|
|
801
790
|
entityTypeOptions={allEntityTypes}
|
|
802
791
|
onGenerateSubmit={handleResourceGenerateSubmit}
|
|
@@ -840,6 +829,8 @@ export function ResourceViewerPage({
|
|
|
840
829
|
maxLength: tg('maxLength'),
|
|
841
830
|
maxLengthHelp: tg('maxLengthHelp'),
|
|
842
831
|
maxLengthCeiling: tg('maxLengthCeiling'),
|
|
832
|
+
outputFormat: tg('outputFormat'),
|
|
833
|
+
formatExtensionMismatch: tg('formatExtensionMismatch'),
|
|
843
834
|
generate: tg('generate'),
|
|
844
835
|
discardDraftPrompt: tg('discardDraftPrompt'),
|
|
845
836
|
discardDraft: tg('discardDraft'),
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { GenerationOptions } from '@semiont/sdk';
|
|
2
|
+
import type { GenerationConfig } from '../../components/modals/ConfigureGenerationStep';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The submitted form config becomes generation options — in ONE place, by
|
|
6
|
+
* spread (GENERATION-OUTPUT-FORMAT D8).
|
|
7
|
+
*
|
|
8
|
+
* Both page handlers previously built this object field-by-field, so a knob
|
|
9
|
+
* the list didn't mention was silently dropped on the way to the wire. That
|
|
10
|
+
* is exactly how `outputMediaType` came to be unreachable one layer down
|
|
11
|
+
* (P1's finding), and copying the pattern here would have re-created it for
|
|
12
|
+
* every future knob. Spreading means the default is "forwarded"; only the two
|
|
13
|
+
* genuine transformations are spelled out:
|
|
14
|
+
*
|
|
15
|
+
* - `context` leaves the bag — it is `fromContext`'s positional argument.
|
|
16
|
+
* - `sourceLanguage` joins it — the language of the resource being VIEWED,
|
|
17
|
+
* which the form cannot know and the page can. Omitted entirely when
|
|
18
|
+
* unknown, so absence stays absence rather than becoming `''`.
|
|
19
|
+
*/
|
|
20
|
+
export function toGenerationOptions(
|
|
21
|
+
config: GenerationConfig,
|
|
22
|
+
sourceLanguage: string | undefined,
|
|
23
|
+
): GenerationOptions {
|
|
24
|
+
const { context: _positional, ...options } = config;
|
|
25
|
+
return {
|
|
26
|
+
...options,
|
|
27
|
+
...(sourceLanguage ? { sourceLanguage } : {}),
|
|
28
|
+
};
|
|
29
|
+
}
|
package/src/styles/index.css
CHANGED
|
@@ -43,7 +43,6 @@
|
|
|
43
43
|
@import './features/admin.css';
|
|
44
44
|
@import './features/compose.css';
|
|
45
45
|
@import './features/devops.css';
|
|
46
|
-
@import './features/exchange.css';
|
|
47
46
|
@import './features/entity-tags.css';
|
|
48
47
|
@import './features/recent-docs.css';
|
|
49
48
|
@import './features/resource.css';
|