@semiont/react-ui 0.5.26 → 0.5.28
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 +28 -4
- package/dist/{PdfAnnotationCanvas.client-6ZSF6ZTJ.js → PdfAnnotationCanvas.client-C2IGG4SW.js} +33 -6
- package/dist/{PdfAnnotationCanvas.client-JCJMXWWJ.css → PdfAnnotationCanvas.client-TGDPPGXJ.css} +6 -1
- package/dist/{ar-YTUUGK2O.js → ar-FABPTJVB.js} +88 -26
- package/dist/{bn-ASGNGPMD.js → bn-LKP6BDMH.js} +88 -26
- package/dist/{chunk-L3WKRWP3.js → chunk-2GOHTTHW.js} +88 -26
- package/dist/{chunk-OBQOZCJE.js → chunk-2YHEE63S.js} +31 -31
- package/dist/{cs-WMYVAOZY.js → cs-YQYNWHRG.js} +88 -26
- package/dist/{da-FSHHN4D6.js → da-QHZOYKRY.js} +88 -26
- package/dist/{de-FJFO3TPT.js → de-GKA7RD7J.js} +88 -26
- package/dist/{el-XOH4QIAG.js → el-45T257AL.js} +88 -26
- package/dist/{en-3FCBW3YD.js → en-RVW5EQ66.js} +4 -2
- package/dist/{es-HDX4QI7E.js → es-AVER7RU4.js} +88 -26
- package/dist/{fa-DJ6WTFTJ.js → fa-K3VPK3O2.js} +88 -26
- package/dist/{fi-4S7H7W7U.js → fi-5NEKF7EB.js} +88 -26
- package/dist/{fr-6UJSYHXC.js → fr-FUMNDLBZ.js} +88 -26
- package/dist/{he-ZZJOHJX2.js → he-3JGNA3PR.js} +88 -26
- package/dist/{hi-HZJTAW7U.js → hi-IJY375DF.js} +88 -26
- package/dist/{id-E3I6ILEM.js → id-LWHKJ7ZD.js} +88 -26
- package/dist/index.css +380 -524
- package/dist/index.d.ts +318 -192
- package/dist/index.js +2960 -2421
- package/dist/integrations/css-modules-helper.js +1 -3
- package/dist/{it-ULAPAD7U.js → it-EBVOV4RW.js} +88 -26
- package/dist/{ja-SAHZQ4CZ.js → ja-KWRIX2NF.js} +88 -26
- package/dist/{ko-DFU2ADQ4.js → ko-L5CJMFDS.js} +88 -26
- package/dist/{ms-NWL76XNT.js → ms-LSNMJDR2.js} +88 -26
- package/dist/{nl-ATCM76WN.js → nl-ZB32MB7U.js} +88 -26
- package/dist/{no-UIAQ5RJF.js → no-4GTNY7M5.js} +88 -26
- package/dist/{pl-NOECQDXD.js → pl-LUHBLTHB.js} +88 -26
- package/dist/{pt-SVWIIUPC.js → pt-C4H3UNQ6.js} +88 -26
- package/dist/{ro-NR2DRTZG.js → ro-GGITS5V4.js} +88 -26
- package/dist/{sv-STVJ37JP.js → sv-T56RTLBP.js} +88 -26
- package/dist/test-utils.css +1 -1
- package/dist/test-utils.d.ts +1 -1
- package/dist/test-utils.js +2 -2
- package/dist/{th-UWQR4K54.js → th-22X2IWCB.js} +88 -26
- package/dist/{tr-WNXZSD27.js → tr-77TFG62H.js} +88 -26
- package/dist/{uk-SJRMB47Y.js → uk-MIIL6NXT.js} +88 -26
- package/dist/{vi-TMRQFES2.js → vi-7JYOIGNV.js} +88 -26
- package/dist/{zh-QOL3LYEM.js → zh-ID6DRYU4.js} +88 -26
- package/package.json +11 -10
- package/src/components/AssistProgress.tsx +178 -134
- package/src/components/CodeMirrorRenderer.tsx +8 -8
- package/src/components/{EntityFoundLog.tsx → ItemFoundLog.tsx} +12 -7
- package/src/components/ProtectedErrorBoundary.css +3 -1
- package/src/components/StatusDisplay.css +1 -1
- package/src/components/Toast.css +3 -1
- package/src/components/__tests__/AssistProgress.test.tsx +268 -141
- package/src/components/annotation/annotation-entries.css +0 -4
- package/src/components/annotation/references.css +0 -4
- package/src/components/image-annotation/AnnotationOverlay.tsx +6 -6
- package/src/components/image-annotation/SvgDrawingCanvas.tsx +1 -1
- package/src/components/image-annotation/__tests__/AnnotationOverlay.click.test.tsx +7 -9
- package/src/components/image-annotation/__tests__/SvgDrawingCanvas.click.test.tsx +1 -2
- package/src/components/layout/Header.css +0 -4
- package/src/components/layout/LeftSidebar.css +0 -4
- package/src/components/modals/ComposeStep.tsx +218 -0
- package/src/components/modals/ConfigureGatherStep.tsx +10 -18
- package/src/components/modals/ConfigureGenerationStep.tsx +131 -54
- package/src/components/modals/ConfigureSearchStep.tsx +42 -40
- package/src/components/modals/ContextSummary.tsx +185 -72
- package/src/components/modals/CorpusPane.tsx +75 -0
- package/src/components/modals/DiscardPrompt.tsx +38 -0
- package/src/components/modals/GatherContextStep.tsx +237 -107
- package/src/components/modals/ReferenceWizardModal.tsx +287 -60
- package/src/components/modals/ResourceGenerateModal.tsx +233 -109
- package/src/components/modals/SearchModal.css +335 -111
- package/src/components/modals/SearchModal.tsx +1 -1
- package/src/components/modals/SearchResultsStep.tsx +63 -88
- package/src/components/modals/WizardFooter.tsx +69 -0
- package/src/components/modals/__tests__/ComposeStep.test.tsx +152 -0
- package/src/components/modals/__tests__/ConfigureGatherStep.test.tsx +14 -26
- package/src/components/modals/__tests__/ConfigureGenerationStep.test.tsx +290 -0
- package/src/components/modals/__tests__/ConfigureSearchStep.test.tsx +95 -0
- package/src/components/modals/__tests__/GatherContextStep.resource.test.tsx +463 -25
- package/src/components/modals/__tests__/ReferenceWizardModal.test.tsx +635 -0
- package/src/components/modals/__tests__/ResourceGenerateModal.test.tsx +277 -85
- package/src/components/modals/__tests__/SearchModal.search-wiring.test.tsx +6 -2
- package/src/components/modals/__tests__/SearchResultsStep.test.tsx +61 -0
- package/src/components/modals/__tests__/WizardFooter.test.tsx +344 -0
- package/src/components/modals/modals.css +16 -42
- package/src/components/navigation/NavigationTabs.css +0 -20
- package/src/components/pdf-annotation/PdfAnnotationCanvas.css +21 -2
- package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +36 -3
- package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +41 -6
- package/src/components/resource/AnnotateView.tsx +19 -8
- package/src/components/resource/AnnotationHistory.tsx +2 -0
- package/src/components/resource/BrowseView.tsx +20 -9
- package/src/components/resource/ResourceViewer.tsx +27 -28
- package/src/components/resource/__tests__/AnnotateView.embeddable.test.tsx +1 -1
- package/src/components/resource/__tests__/AnnotateView.focus.test.tsx +50 -0
- package/src/components/resource/__tests__/AnnotationHistory.test.tsx +2 -0
- package/src/components/resource/__tests__/BrowseView.embeddable.test.tsx +1 -1
- package/src/components/resource/__tests__/BrowseView.test.tsx +38 -12
- package/src/components/resource/__tests__/ResourceViewer.embeddable.test.tsx +47 -3
- package/src/components/resource/annotate-renderers.tsx +3 -3
- package/src/components/resource/event-formatting.ts +0 -3
- package/src/components/resource/panels/AssessmentEntry.tsx +1 -1
- package/src/components/resource/panels/AssessmentPanel.css +5 -6
- package/src/components/resource/panels/AssistSection.css +95 -263
- package/src/components/resource/panels/AssistSection.tsx +4 -4
- package/src/components/resource/panels/AssistShell.tsx +12 -4
- package/src/components/resource/panels/CollaborationPanel.css +47 -22
- package/src/components/resource/panels/CollaborationPanel.tsx +93 -2
- package/src/components/resource/panels/CommentEntry.tsx +1 -1
- package/src/components/resource/panels/CommentsPanel.css +0 -16
- package/src/components/resource/panels/HighlightEntry.tsx +1 -1
- package/src/components/resource/panels/HighlightPanel.css +0 -7
- package/src/components/resource/panels/JsonLdPanel.css +0 -4
- package/src/components/resource/panels/ReferenceEntry.tsx +10 -3
- package/src/components/resource/panels/ReferencesPanel.css +0 -12
- package/src/components/resource/panels/ReferencesPanel.tsx +16 -20
- package/src/components/resource/panels/ResourceInfoPanel.css +0 -32
- package/src/components/resource/panels/ResourceInfoPanel.tsx +89 -20
- package/src/components/resource/panels/StatisticsPanel.css +0 -20
- package/src/components/resource/panels/TagEntry.tsx +1 -1
- package/src/components/resource/panels/TaggingPanel.css +0 -4
- package/src/components/resource/panels/TaggingPanel.tsx +2 -10
- package/src/components/resource/panels/UnifiedAnnotationsPanel.css +0 -4
- package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +7 -2
- package/src/components/resource/panels/__tests__/AssessmentEntry.test.tsx +0 -1
- package/src/components/resource/panels/__tests__/AssistSection.test.tsx +48 -50
- package/src/components/resource/panels/__tests__/AssistShell.test.tsx +11 -6
- package/src/components/resource/panels/__tests__/CollaborationPanel.test.tsx +89 -0
- package/src/components/resource/panels/__tests__/CommentEntry.test.tsx +1 -2
- package/src/components/resource/panels/__tests__/HighlightEntry.test.tsx +0 -1
- package/src/components/resource/panels/__tests__/HighlightPanel.annotationProgress.test.tsx +44 -36
- package/src/components/resource/panels/__tests__/ReferenceEntry.test.tsx +41 -1
- package/src/components/resource/panels/__tests__/ReferencesPanel.headless.test.tsx +1 -1
- package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +67 -54
- package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +185 -7
- package/src/components/resource/panels/__tests__/TagEntry.test.tsx +0 -1
- package/src/components/settings/SettingsPanel.css +0 -4
- package/src/features/auth/auth.css +2 -26
- package/src/features/resource-compose/__tests__/ResourceComposePage.test.tsx +0 -65
- package/src/features/resource-compose/components/ResourceComposePage.tsx +17 -130
- package/src/features/resource-compose/state/__tests__/compose-page-state-unit.test.ts +1 -76
- package/src/features/resource-compose/state/compose-page-state-unit.ts +5 -48
- package/src/features/resource-discovery/__tests__/ResourceDiscoveryPage.test.tsx +55 -0
- package/src/features/resource-discovery/components/ResourceDiscoveryPage.tsx +29 -0
- package/src/features/resource-discovery/state/__tests__/discover-state-unit.test.ts +53 -3
- package/src/features/resource-discovery/state/discover-state-unit.ts +29 -8
- package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +71 -15
- package/src/features/resource-viewer/components/ResourceViewerPage.tsx +166 -71
- package/src/features/resource-viewer/hooks/__tests__/useResourceViewedReport.test.tsx +63 -0
- package/src/features/resource-viewer/hooks/useResourceViewedReport.ts +33 -0
- package/src/features/resource-viewer/state/__tests__/resource-viewer-page-state-unit.test.ts +2 -2
- package/src/features/resource-viewer/state/resource-viewer-page-state-unit.ts +1 -1
- package/src/styles/core/buttons.css +3 -1
- package/src/styles/core/forms.css +35 -29
- package/src/styles/core/indicators.css +0 -4
- package/src/styles/core/inputs.css +0 -4
- package/src/styles/core/progress.css +2 -34
- package/src/styles/core/sliders.css +0 -4
- package/src/styles/core/toggles.css +0 -4
- package/src/styles/features/resource-discovery.css +13 -0
- package/src/styles/features/resource-viewer.css +0 -42
- package/src/styles/features/resource.css +0 -4
- package/src/styles/motivations/motivation-assessment.css +2 -2
- package/src/styles/panels/history-panel.css +0 -40
- package/src/styles/panels/user-panel.css +0 -3
- package/src/styles/patterns/panel-helpers.css +0 -27
- package/src/styles/patterns/panel-sections.css +0 -59
- package/src/styles/patterns/panels-base.css +0 -32
- package/translations/ar.json +84 -24
- package/translations/bn.json +84 -24
- package/translations/cs.json +84 -24
- package/translations/da.json +84 -24
- package/translations/de.json +84 -24
- package/translations/el.json +84 -24
- package/translations/en.json +84 -24
- package/translations/es.json +84 -24
- package/translations/fa.json +84 -24
- package/translations/fi.json +84 -24
- package/translations/fr.json +84 -24
- package/translations/he.json +84 -24
- package/translations/hi.json +84 -24
- package/translations/id.json +84 -24
- package/translations/it.json +84 -24
- package/translations/ja.json +84 -24
- package/translations/ko.json +84 -24
- package/translations/ms.json +84 -24
- package/translations/nl.json +84 -24
- package/translations/no.json +84 -24
- package/translations/pl.json +84 -24
- package/translations/pt.json +84 -24
- package/translations/ro.json +84 -24
- package/translations/sv.json +84 -24
- package/translations/th.json +84 -24
- package/translations/tr.json +84 -24
- package/translations/uk.json +84 -24
- package/translations/vi.json +84 -24
- package/translations/zh.json +84 -24
- package/dist/PdfAnnotationCanvas.client-6ZSF6ZTJ.js.map +0 -1
- package/dist/PdfAnnotationCanvas.client-JCJMXWWJ.css.map +0 -1
- package/dist/ar-YTUUGK2O.js.map +0 -1
- package/dist/bn-ASGNGPMD.js.map +0 -1
- package/dist/chunk-BTUG3VJJ.js.map +0 -1
- package/dist/chunk-L3WKRWP3.js.map +0 -1
- package/dist/chunk-O23TLLXW.js.map +0 -1
- package/dist/chunk-OBQOZCJE.js.map +0 -1
- package/dist/chunk-QT7LYM72.js.map +0 -1
- package/dist/chunk-VXASXU4K.js.map +0 -1
- package/dist/chunk-XUDKYAVC.js +0 -21
- package/dist/chunk-XUDKYAVC.js.map +0 -1
- package/dist/cs-WMYVAOZY.js.map +0 -1
- package/dist/da-FSHHN4D6.js.map +0 -1
- package/dist/de-FJFO3TPT.js.map +0 -1
- package/dist/el-XOH4QIAG.js.map +0 -1
- package/dist/en-3FCBW3YD.js.map +0 -1
- package/dist/es-HDX4QI7E.js.map +0 -1
- package/dist/fa-DJ6WTFTJ.js.map +0 -1
- package/dist/fi-4S7H7W7U.js.map +0 -1
- package/dist/fr-6UJSYHXC.js.map +0 -1
- package/dist/he-ZZJOHJX2.js.map +0 -1
- package/dist/hi-HZJTAW7U.js.map +0 -1
- package/dist/id-E3I6ILEM.js.map +0 -1
- package/dist/index.css.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/integrations/css-modules-helper.js.map +0 -1
- package/dist/integrations/styled-components-theme.js.map +0 -1
- package/dist/it-ULAPAD7U.js.map +0 -1
- package/dist/ja-SAHZQ4CZ.js.map +0 -1
- package/dist/ko-DFU2ADQ4.js.map +0 -1
- package/dist/ms-NWL76XNT.js.map +0 -1
- package/dist/nl-ATCM76WN.js.map +0 -1
- package/dist/no-UIAQ5RJF.js.map +0 -1
- package/dist/pl-NOECQDXD.js.map +0 -1
- package/dist/pt-SVWIIUPC.js.map +0 -1
- package/dist/ro-NR2DRTZG.js.map +0 -1
- package/dist/sv-STVJ37JP.js.map +0 -1
- package/dist/test-utils.css.map +0 -1
- package/dist/test-utils.js.map +0 -1
- package/dist/th-UWQR4K54.js.map +0 -1
- package/dist/tr-WNXZSD27.js.map +0 -1
- package/dist/uk-SJRMB47Y.js.map +0 -1
- package/dist/vi-TMRQFES2.js.map +0 -1
- package/dist/zh-QOL3LYEM.js.map +0 -1
- package/src/components/modals/ResourceSearchModal.tsx +0 -224
- package/src/components/modals/__tests__/ResourceSearchModal.test.tsx +0 -159
- package/src/integrations/tailwind-plugin.cjs +0 -224
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* INFERENCE-LIMITS-EXPOSURE P3b — the max-length control is bounded by the
|
|
3
|
+
* generation-serving agent's discovered output ceiling.
|
|
4
|
+
*
|
|
5
|
+
* The control was NOT previously unbounded: it shipped `min="100" max="4000"`.
|
|
6
|
+
* So this phase replaces a hardcoded 4000 with the real ceiling, which for any
|
|
7
|
+
* model above 4000 RAISES a cap users hit today, and for a smaller model
|
|
8
|
+
* lowers it to something the provider will actually honour.
|
|
9
|
+
*/
|
|
10
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
11
|
+
import { useState } from 'react';
|
|
12
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
13
|
+
import '@testing-library/jest-dom';
|
|
14
|
+
import type { CollaboratorEntry, GatheredContext } from '@semiont/core';
|
|
15
|
+
import { ConfigureGenerationStep } from '../ConfigureGenerationStep';
|
|
16
|
+
|
|
17
|
+
const translations = {
|
|
18
|
+
resourceTitle: 'Title',
|
|
19
|
+
resourceTitlePlaceholder: 'Title…',
|
|
20
|
+
saveLocation: 'Save location',
|
|
21
|
+
additionalInstructions: 'Instructions',
|
|
22
|
+
additionalInstructionsPlaceholder: 'Instructions…',
|
|
23
|
+
language: 'Language',
|
|
24
|
+
languageHelp: 'Language help',
|
|
25
|
+
creativity: 'Creativity',
|
|
26
|
+
creativityFocused: 'Focused',
|
|
27
|
+
creativityCreative: 'Creative',
|
|
28
|
+
maxLength: 'Max length',
|
|
29
|
+
maxLengthHelp: 'How long the generated resource may be.',
|
|
30
|
+
// Named model + ceiling, so the bound is legible rather than mysterious (D6).
|
|
31
|
+
maxLengthCeiling: 'Limited to {{maxOutputTokens}} tokens by {{model}}.',
|
|
32
|
+
back: 'Back',
|
|
33
|
+
generate: 'Generate',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const context = { resources: [], annotations: [] } as unknown as GatheredContext;
|
|
37
|
+
|
|
38
|
+
/** The wizard's initial draft (WIZARD-NAVIGATION D3). */
|
|
39
|
+
const DRAFT = {
|
|
40
|
+
title: 'Untitled', storagePath: '', prompt: '', language: 'en',
|
|
41
|
+
temperature: 0.7, maxTokensText: '500',
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const agentWithCeiling = (maxOutputTokens: number): CollaboratorEntry =>
|
|
45
|
+
({
|
|
46
|
+
agent: {
|
|
47
|
+
'@type': 'Software',
|
|
48
|
+
name: 'Claude Sonnet 5',
|
|
49
|
+
provider: 'anthropic',
|
|
50
|
+
model: 'claude-sonnet-5',
|
|
51
|
+
},
|
|
52
|
+
servesJobTypes: ['generation'],
|
|
53
|
+
limits: { contextTokens: 200_000, maxOutputTokens },
|
|
54
|
+
}) as unknown as CollaboratorEntry;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The step is CONTROLLED since WIZARD-NAVIGATION D3 — the wizard owns the draft so
|
|
58
|
+
* Back cannot discard it. These tests therefore mount a tiny stateful harness rather
|
|
59
|
+
* than the bare component: a `vi.fn()` for `onConfigChange` would swallow every edit
|
|
60
|
+
* and quietly turn each assertion below into a test of nothing.
|
|
61
|
+
*/
|
|
62
|
+
function Harness({ generationAgent }: { generationAgent?: CollaboratorEntry }) {
|
|
63
|
+
const [draft, setDraft] = useState(DRAFT);
|
|
64
|
+
return (
|
|
65
|
+
<ConfigureGenerationStep
|
|
66
|
+
config={draft}
|
|
67
|
+
onConfigChange={setDraft}
|
|
68
|
+
context={context}
|
|
69
|
+
onBack={vi.fn()}
|
|
70
|
+
onGenerate={onGenerateSpy}
|
|
71
|
+
translations={translations}
|
|
72
|
+
{...(generationAgent ? { generationAgent } : {})}
|
|
73
|
+
/>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let onGenerateSpy = vi.fn();
|
|
78
|
+
|
|
79
|
+
function renderStep(generationAgent?: CollaboratorEntry) {
|
|
80
|
+
onGenerateSpy = vi.fn();
|
|
81
|
+
const utils = render(<Harness {...(generationAgent ? { generationAgent } : {})} />);
|
|
82
|
+
const input = () => screen.getByLabelText('Max length') as HTMLInputElement;
|
|
83
|
+
return { ...utils, input, props: { onGenerate: onGenerateSpy } };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function fillRequired() {
|
|
87
|
+
fireEvent.change(screen.getByLabelText('Save location'), {
|
|
88
|
+
target: { value: 'generated/out.md' },
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
describe('ConfigureGenerationStep — ceiling awareness', () => {
|
|
93
|
+
it('bounds the control by the agent ceiling and names it in help text', () => {
|
|
94
|
+
const { input } = renderStep(agentWithCeiling(64_000));
|
|
95
|
+
|
|
96
|
+
expect(input().max).toBe('64000');
|
|
97
|
+
expect(screen.getByText(/Limited to 64000 tokens by claude-sonnet-5\./)).toBeInTheDocument();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('the ceiling sentence is a row-level footnote, never a column child', () => {
|
|
101
|
+
// A full sentence inside the 5.5rem Max Length column wraps one word per
|
|
102
|
+
// line and its height taxes the whole flex row — the void under the other
|
|
103
|
+
// controls IS this caption. It renders full-width under the row instead.
|
|
104
|
+
renderStep(agentWithCeiling(64_000));
|
|
105
|
+
|
|
106
|
+
const help = screen.getByText(/Limited to 64000 tokens/);
|
|
107
|
+
expect(help).toHaveClass('semiont-form__help--row');
|
|
108
|
+
expect(help.closest('.semiont-form__field--narrow')).toBeNull();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('will not commit a value above the ceiling', () => {
|
|
112
|
+
// `max` on a number input does not prevent TYPING an over-value — it only
|
|
113
|
+
// marks the field invalid. D6 says the value cannot be entered, so assert
|
|
114
|
+
// the committed state, never the attribute alone.
|
|
115
|
+
const { input } = renderStep(agentWithCeiling(2_000));
|
|
116
|
+
|
|
117
|
+
fireEvent.change(input(), { target: { value: '9999' } });
|
|
118
|
+
|
|
119
|
+
expect(input().value).toBe('2000');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('clamps the 500 default down when the ceiling is smaller', () => {
|
|
123
|
+
const { input } = renderStep(agentWithCeiling(300));
|
|
124
|
+
|
|
125
|
+
expect(input().value).toBe('300');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('re-bounds and clamps when the ceiling arrives AFTER mount', () => {
|
|
129
|
+
// The case that decides whether this feature works in the app at all:
|
|
130
|
+
// `browse.agents()` is asynchronous, so `generationAgent` is undefined on
|
|
131
|
+
// first render and arrives later. An implementation that only consults the
|
|
132
|
+
// ceiling in the `useState` initializer passes every test above and does
|
|
133
|
+
// nothing in production.
|
|
134
|
+
const { input, rerender } = renderStep(undefined);
|
|
135
|
+
|
|
136
|
+
expect(input().max).toBe('4000');
|
|
137
|
+
fireEvent.change(input(), { target: { value: '3000' } });
|
|
138
|
+
expect(input().value).toBe('3000');
|
|
139
|
+
|
|
140
|
+
rerender(<Harness generationAgent={agentWithCeiling(1_000)} />);
|
|
141
|
+
|
|
142
|
+
expect(input().max).toBe('1000');
|
|
143
|
+
// 3000 is now out of range and must not survive as a submittable value.
|
|
144
|
+
expect(input().value).toBe('1000');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('keeps a value that is still within a newly arrived ceiling', () => {
|
|
148
|
+
const { input, rerender } = renderStep(undefined);
|
|
149
|
+
fireEvent.change(input(), { target: { value: '800' } });
|
|
150
|
+
|
|
151
|
+
rerender(<Harness generationAgent={agentWithCeiling(4_000)} />);
|
|
152
|
+
|
|
153
|
+
expect(input().value).toBe('800');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('falls back to today\'s bounds and copy without an agent (D3/D6 degradation)', () => {
|
|
157
|
+
const { input } = renderStep(undefined);
|
|
158
|
+
|
|
159
|
+
expect(input().min).toBe('100');
|
|
160
|
+
expect(input().max).toBe('4000');
|
|
161
|
+
expect(screen.getByText('How long the generated resource may be.')).toBeInTheDocument();
|
|
162
|
+
expect(screen.queryByText(/Limited to/)).not.toBeInTheDocument();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('falls back when the agent is present but discovery could not answer', () => {
|
|
166
|
+
const noLimits = { ...agentWithCeiling(64_000) } as Record<string, unknown>;
|
|
167
|
+
delete noLimits.limits;
|
|
168
|
+
const { input } = renderStep(noLimits as unknown as CollaboratorEntry);
|
|
169
|
+
|
|
170
|
+
expect(input().max).toBe('4000');
|
|
171
|
+
expect(screen.queryByText(/Limited to/)).not.toBeInTheDocument();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('submits the clamped value, not the typed one', () => {
|
|
175
|
+
const { props } = renderStep(agentWithCeiling(1_500));
|
|
176
|
+
const onGenerate = props.onGenerate;
|
|
177
|
+
|
|
178
|
+
fillRequired();
|
|
179
|
+
const input = screen.getByLabelText('Max length') as HTMLInputElement;
|
|
180
|
+
fireEvent.change(input, { target: { value: '8000' } });
|
|
181
|
+
fireEvent.click(screen.getByText(/Generate/));
|
|
182
|
+
|
|
183
|
+
expect(onGenerate).toHaveBeenCalledTimes(1);
|
|
184
|
+
expect(onGenerate.mock.calls[0]![0].maxTokens).toBe(1_500);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('lets the field be cleared, and never submits NaN for it', () => {
|
|
188
|
+
// Pre-existing hazard the clamp must not inherit: the handler was
|
|
189
|
+
// `parseInt(e.target.value)` and `parseInt('')` is NaN, which React then
|
|
190
|
+
// warns about and which would travel into the job config. Clearing must
|
|
191
|
+
// stay possible (you cannot retype a number otherwise), so the contract is
|
|
192
|
+
// about what SUBMITS, not about forbidding the empty state.
|
|
193
|
+
const { props } = renderStep(agentWithCeiling(4_000));
|
|
194
|
+
const onGenerate = props.onGenerate;
|
|
195
|
+
const input = screen.getByLabelText('Max length') as HTMLInputElement;
|
|
196
|
+
|
|
197
|
+
fillRequired();
|
|
198
|
+
fireEvent.change(input, { target: { value: '' } });
|
|
199
|
+
expect(input.value).toBe('');
|
|
200
|
+
|
|
201
|
+
fireEvent.click(screen.getByText(/Generate/));
|
|
202
|
+
|
|
203
|
+
const submitted = onGenerate.mock.calls[0]![0].maxTokens;
|
|
204
|
+
expect(Number.isFinite(submitted)).toBe(true);
|
|
205
|
+
expect(submitted).toBeGreaterThanOrEqual(100);
|
|
206
|
+
expect(submitted).toBeLessThanOrEqual(4_000);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// The rest of the draft, same contract as the ceiling: every field reports to
|
|
211
|
+
// the owner (WIZARD-NAVIGATION D3). A field that kept its own state would look
|
|
212
|
+
// right until someone pressed Back.
|
|
213
|
+
describe('ConfigureGenerationStep — the draft is the owner\'s', () => {
|
|
214
|
+
it('reports edits to title, instructions, language and creativity', async () => {
|
|
215
|
+
const { input } = renderStep(agentWithCeiling(4_000));
|
|
216
|
+
expect(input()).toBeInTheDocument();
|
|
217
|
+
|
|
218
|
+
fireEvent.change(screen.getByLabelText(translations.resourceTitle), { target: { value: 'Caspian Sea' } });
|
|
219
|
+
expect((screen.getByLabelText(translations.resourceTitle) as HTMLInputElement).value).toBe('Caspian Sea');
|
|
220
|
+
|
|
221
|
+
fireEvent.change(screen.getByLabelText(translations.additionalInstructions), { target: { value: 'hydrology only' } });
|
|
222
|
+
expect((screen.getByLabelText(translations.additionalInstructions) as HTMLTextAreaElement).value).toBe('hydrology only');
|
|
223
|
+
|
|
224
|
+
// The label carries the live value — "Creativity (0.7)" — which is itself
|
|
225
|
+
// the proof the slider is controlled: it can only update if the owner's
|
|
226
|
+
// draft came back down.
|
|
227
|
+
const slider = () => screen.getByLabelText(new RegExp(`^${translations.creativity}`)) as HTMLInputElement;
|
|
228
|
+
fireEvent.change(slider(), { target: { value: '0.2' } });
|
|
229
|
+
expect(slider().value).toBe('0.2');
|
|
230
|
+
expect(screen.getByText(/Creativity \(0\.2\)/)).toBeInTheDocument();
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('lets the max-length field go empty while retyping', () => {
|
|
234
|
+
// Clearing is a legitimate mid-edit state — you cannot retype a number
|
|
235
|
+
// otherwise — so the field must accept '' and let submission clamp it.
|
|
236
|
+
const { input } = renderStep(agentWithCeiling(4_000));
|
|
237
|
+
fireEvent.change(input(), { target: { value: '' } });
|
|
238
|
+
expect(input().value).toBe('');
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('reports a language change to the owner', () => {
|
|
242
|
+
renderStep(agentWithCeiling(4_000));
|
|
243
|
+
const select = () => screen.getByLabelText(translations.language) as HTMLSelectElement;
|
|
244
|
+
const other = Array.from(select().options).map((o) => o.value).find((v) => v !== select().value);
|
|
245
|
+
fireEvent.change(select(), { target: { value: other } });
|
|
246
|
+
expect(select().value).toBe(other);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('carries the typed instructions into the submitted config, trimmed', () => {
|
|
250
|
+
const { props } = renderStep(agentWithCeiling(4_000));
|
|
251
|
+
fillRequired();
|
|
252
|
+
fireEvent.change(screen.getByLabelText(translations.additionalInstructions), { target: { value: ' hydrology only ' } });
|
|
253
|
+
fireEvent.click(screen.getByText(new RegExp(translations.generate)));
|
|
254
|
+
|
|
255
|
+
expect(props.onGenerate).toHaveBeenCalledTimes(1);
|
|
256
|
+
expect(props.onGenerate.mock.calls[0]![0].prompt).toBe('hydrology only');
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('omits the prompt entirely when the field is blank', () => {
|
|
260
|
+
// `prompt` is optional on the wire; sending "" would be a claim the user
|
|
261
|
+
// never made, and the worker would prepend an empty instruction block.
|
|
262
|
+
const { props } = renderStep(agentWithCeiling(4_000));
|
|
263
|
+
fillRequired();
|
|
264
|
+
fireEvent.click(screen.getByText(new RegExp(translations.generate)));
|
|
265
|
+
expect(props.onGenerate.mock.calls[0]![0]).not.toHaveProperty('prompt');
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
describe('ConfigureGenerationStep — the hint echo (GEP P1c, D8)', () => {
|
|
270
|
+
it('echoes a non-empty hint, and renders nothing without one', () => {
|
|
271
|
+
const { container, rerender } = render(
|
|
272
|
+
<ConfigureGenerationStep
|
|
273
|
+
context={context} config={DRAFT} onConfigChange={vi.fn()}
|
|
274
|
+
onBack={vi.fn()} onGenerate={vi.fn()} translations={translations}
|
|
275
|
+
hintEcho={{ label: 'Hint', value: 'the ancient city, not the modern province' }}
|
|
276
|
+
/>,
|
|
277
|
+
);
|
|
278
|
+
const echo = container.querySelector('.semiont-wizard__hint-echo');
|
|
279
|
+
expect(echo).not.toBeNull();
|
|
280
|
+
expect(echo!.textContent).toContain('the ancient city, not the modern province');
|
|
281
|
+
|
|
282
|
+
rerender(
|
|
283
|
+
<ConfigureGenerationStep
|
|
284
|
+
context={context} config={DRAFT} onConfigChange={vi.fn()}
|
|
285
|
+
onBack={vi.fn()} onGenerate={vi.fn()} translations={translations}
|
|
286
|
+
/>,
|
|
287
|
+
);
|
|
288
|
+
expect(container.querySelector('.semiont-wizard__hint-echo')).toBeNull();
|
|
289
|
+
});
|
|
290
|
+
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The search step is CONTROLLED (WIZARD-NAVIGATION D3) — the wizard owns
|
|
3
|
+
* `limit` and `useSemanticScoring` so stepping Back cannot discard them.
|
|
4
|
+
*
|
|
5
|
+
* That makes these tests about the contract rather than the widget: every edit
|
|
6
|
+
* must reach `onConfigChange` (an uncontrolled field would appear to work and
|
|
7
|
+
* silently revert on Back), and submit must send the CURRENT config rather than
|
|
8
|
+
* a stale copy.
|
|
9
|
+
*/
|
|
10
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
11
|
+
import { render, screen } from '@testing-library/react';
|
|
12
|
+
import userEvent from '@testing-library/user-event';
|
|
13
|
+
import '@testing-library/jest-dom';
|
|
14
|
+
import { ConfigureSearchStep, type SearchConfig } from '../ConfigureSearchStep';
|
|
15
|
+
|
|
16
|
+
const T = {
|
|
17
|
+
maxResults: 'Max Results', semanticScoring: 'Semantic Scoring',
|
|
18
|
+
semanticScoringHelp: 'Use AI to score results by semantic relevance',
|
|
19
|
+
back: 'Back', search: 'Search', searching: 'Searching…', searchFailed: 'Search failed',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const CONFIG: SearchConfig = { limit: 10, useSemanticScoring: true };
|
|
23
|
+
|
|
24
|
+
function renderStep(config: SearchConfig = CONFIG) {
|
|
25
|
+
const onConfigChange = vi.fn();
|
|
26
|
+
const onSearch = vi.fn();
|
|
27
|
+
const onBack = vi.fn();
|
|
28
|
+
render(
|
|
29
|
+
<ConfigureSearchStep
|
|
30
|
+
config={config}
|
|
31
|
+
onConfigChange={onConfigChange}
|
|
32
|
+
onBack={onBack}
|
|
33
|
+
onSearch={onSearch}
|
|
34
|
+
translations={T}
|
|
35
|
+
/>,
|
|
36
|
+
);
|
|
37
|
+
return { onConfigChange, onSearch, onBack };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe('ConfigureSearchStep', () => {
|
|
41
|
+
it('reports a new result limit to the owner rather than keeping it', async () => {
|
|
42
|
+
const { onConfigChange } = renderStep();
|
|
43
|
+
await userEvent.selectOptions(screen.getByLabelText(T.maxResults), '20');
|
|
44
|
+
expect(onConfigChange).toHaveBeenCalledWith({ limit: 20, useSemanticScoring: true });
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('reports the semantic-scoring toggle the same way', async () => {
|
|
48
|
+
const { onConfigChange } = renderStep();
|
|
49
|
+
await userEvent.click(screen.getByLabelText(new RegExp(T.semanticScoring)));
|
|
50
|
+
expect(onConfigChange).toHaveBeenCalledWith({ limit: 10, useSemanticScoring: false });
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('echoes the hint being steered by, and stays silent without one (GEP P1c, D8)', () => {
|
|
54
|
+
// The thing the hint steers goes invisible behind the step transition —
|
|
55
|
+
// one quiet line keeps it visible and advertises that the hint did something.
|
|
56
|
+
const { container, rerender } = render(
|
|
57
|
+
<ConfigureSearchStep
|
|
58
|
+
config={CONFIG} onConfigChange={vi.fn()} onBack={vi.fn()} onSearch={vi.fn()}
|
|
59
|
+
translations={T}
|
|
60
|
+
hintEcho={{ label: 'Hint', value: 'the ancient city, not the modern province' }}
|
|
61
|
+
/>,
|
|
62
|
+
);
|
|
63
|
+
const echo = container.querySelector('.semiont-wizard__hint-echo');
|
|
64
|
+
expect(echo).not.toBeNull();
|
|
65
|
+
expect(echo!.textContent).toContain('the ancient city, not the modern province');
|
|
66
|
+
|
|
67
|
+
rerender(
|
|
68
|
+
<ConfigureSearchStep
|
|
69
|
+
config={CONFIG} onConfigChange={vi.fn()} onBack={vi.fn()} onSearch={vi.fn()}
|
|
70
|
+
translations={T}
|
|
71
|
+
/>,
|
|
72
|
+
);
|
|
73
|
+
expect(container.querySelector('.semiont-wizard__hint-echo')).toBeNull();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('submits whatever the owner currently holds, not a default', async () => {
|
|
77
|
+
// The point of controlled state: a step that kept its own copy could submit
|
|
78
|
+
// something the wizard never saw.
|
|
79
|
+
const { onSearch } = renderStep({ limit: 5, useSemanticScoring: false });
|
|
80
|
+
await userEvent.click(screen.getByRole('button', { name: T.search }));
|
|
81
|
+
expect(onSearch).toHaveBeenCalledWith({ limit: 5, useSemanticScoring: false });
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('renders the owner\'s values, so Back-then-forward shows them again', () => {
|
|
85
|
+
renderStep({ limit: 20, useSemanticScoring: false });
|
|
86
|
+
expect((screen.getByLabelText(T.maxResults) as HTMLSelectElement).value).toBe('20');
|
|
87
|
+
expect((screen.getByLabelText(new RegExp(T.semanticScoring)) as HTMLInputElement).checked).toBe(false);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('retreats when Back is pressed', async () => {
|
|
91
|
+
const { onBack } = renderStep();
|
|
92
|
+
await userEvent.click(screen.getByRole('button', { name: /Back/ }));
|
|
93
|
+
expect(onBack).toHaveBeenCalledTimes(1);
|
|
94
|
+
});
|
|
95
|
+
});
|