@skopon-cool/form-sdk 0.1.3 → 0.1.5
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 +2 -0
- package/dist/adapter/a2uiAdapter.d.ts +0 -1
- package/dist/adapter/a2uiAdapter.d.ts.map +1 -1
- package/dist/adapter/formSchema.d.ts.map +1 -1
- package/dist/blocks/case_multiselect/adapter.d.ts +10 -0
- package/dist/blocks/case_multiselect/adapter.d.ts.map +1 -0
- package/dist/blocks/case_multiselect/index.d.ts +3 -0
- package/dist/blocks/case_multiselect/index.d.ts.map +1 -0
- package/dist/blocks/case_singleselect/catalog.d.ts +2 -0
- package/dist/blocks/case_singleselect/catalog.d.ts.map +1 -0
- package/dist/blocks/registry.d.ts +8 -0
- package/dist/blocks/registry.d.ts.map +1 -0
- package/dist/blocks/resume_multiselect/adapter.d.ts +10 -0
- package/dist/blocks/resume_multiselect/adapter.d.ts.map +1 -0
- package/dist/blocks/resume_multiselect/catalog.d.ts +2 -0
- package/dist/blocks/resume_multiselect/catalog.d.ts.map +1 -0
- package/dist/blocks/resume_multiselect/index.d.ts +3 -0
- package/dist/blocks/resume_multiselect/index.d.ts.map +1 -0
- package/dist/blocks/types.d.ts +27 -0
- package/dist/blocks/types.d.ts.map +1 -0
- package/dist/catalog/a2uiCustomCatalog.d.ts.map +1 -1
- package/dist/catalog/caseSearchContext.d.ts +28 -0
- package/dist/catalog/caseSearchContext.d.ts.map +1 -0
- package/dist/catalog/resumeSearchContext.d.ts +39 -0
- package/dist/catalog/resumeSearchContext.d.ts.map +1 -0
- package/dist/catalog/skoponCaseSelect.d.ts +2 -0
- package/dist/catalog/skoponCaseSelect.d.ts.map +1 -0
- package/dist/catalog/skoponResumeSelect.d.ts +2 -0
- package/dist/catalog/skoponResumeSelect.d.ts.map +1 -0
- package/dist/components/AskUserFormCard.d.ts +7 -1
- package/dist/components/AskUserFormCard.d.ts.map +1 -1
- package/dist/components/SkoponA2uiStreamRenderer.d.ts +7 -1
- package/dist/components/SkoponA2uiStreamRenderer.d.ts.map +1 -1
- package/dist/components/SkoponFormRenderer.d.ts +6 -0
- package/dist/components/SkoponFormRenderer.d.ts.map +1 -1
- package/dist/form-sdk.css +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1374 -889
- package/dist/submit/buildCurlStatement.d.ts +1 -1
- package/dist/submit/buildCurlStatement.d.ts.map +1 -1
- package/dist/submit/intersectPayloadBlocksWithForm.d.ts.map +1 -1
- package/dist/types/index.d.ts +33 -2
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/adapter/a2uiAdapter.test.ts +71 -0
- package/src/adapter/a2uiAdapter.ts +41 -4
- package/src/adapter/formSchema.ts +102 -19
- package/src/blocks/case_multiselect/adapter.ts +90 -0
- package/src/blocks/case_multiselect/index.ts +14 -0
- package/src/blocks/case_singleselect/catalog.ts +1 -0
- package/src/blocks/registry.ts +34 -0
- package/src/blocks/resume_multiselect/adapter.ts +57 -0
- package/src/blocks/resume_multiselect/catalog.ts +1 -0
- package/src/blocks/resume_multiselect/index.ts +14 -0
- package/src/blocks/types.ts +34 -0
- package/src/catalog/a2uiCustomCatalog.tsx +6 -0
- package/src/catalog/caseSearchContext.tsx +46 -0
- package/src/catalog/resumeSearchContext.tsx +58 -0
- package/src/catalog/skoponCaseSelect.tsx +240 -0
- package/src/catalog/skoponResumeSelect.tsx +293 -0
- package/src/components/AskUserFormCard.tsx +13 -1
- package/src/components/SkoponA2uiStreamRenderer.test.ts +18 -2
- package/src/components/SkoponA2uiStreamRenderer.test.tsx +26 -2
- package/src/components/SkoponA2uiStreamRenderer.tsx +71 -27
- package/src/components/SkoponFormRenderer.tsx +32 -10
- package/src/index.ts +23 -0
- package/src/styles/a2ui-preview.css +4 -4
- package/src/styles/index.css +191 -0
- package/src/submit/buildCurlStatement.ts +2 -23
- package/src/submit/intersectPayloadBlocksWithForm.ts +14 -2
- package/src/submit/submit.test.ts +34 -3
- package/src/types/index.ts +37 -0
|
@@ -12,6 +12,8 @@ import {
|
|
|
12
12
|
payloadHasInputBlocks,
|
|
13
13
|
} from '../submit/intersectPayloadBlocksWithForm'
|
|
14
14
|
import { copyTextToClipboard, submitFormJson } from '../submit/submitFormJson'
|
|
15
|
+
import type { CaseSearchFn } from '../catalog/caseSearchContext'
|
|
16
|
+
import type { ResumeSearchFn } from '../catalog/resumeSearchContext'
|
|
15
17
|
import SkoponFormRenderer, { type SkoponFormRendererRef } from './SkoponFormRenderer'
|
|
16
18
|
import CurlSubmitBlock from './CurlSubmitBlock'
|
|
17
19
|
|
|
@@ -37,6 +39,10 @@ export interface AskUserFormCardProps {
|
|
|
37
39
|
/** 需 useCallback 稳定引用,避免重复拉取 */
|
|
38
40
|
fetchFormDetail: (ref: { formUniqueId: string }) => Promise<FormDetailResult>
|
|
39
41
|
onNotify?: (type: 'success' | 'error', message: string) => void
|
|
42
|
+
/** 简历多选组件:注入搜索函数 */
|
|
43
|
+
resumeSearch?: ResumeSearchFn | null
|
|
44
|
+
/** 案例单选组件:注入搜索函数 */
|
|
45
|
+
caseSearch?: CaseSearchFn | null
|
|
40
46
|
}
|
|
41
47
|
|
|
42
48
|
export default function AskUserFormCard({
|
|
@@ -46,6 +52,8 @@ export default function AskUserFormCard({
|
|
|
46
52
|
submitMode = 'curl',
|
|
47
53
|
fetchFormDetail,
|
|
48
54
|
onNotify,
|
|
55
|
+
resumeSearch = null,
|
|
56
|
+
caseSearch = null,
|
|
49
57
|
}: AskUserFormCardProps) {
|
|
50
58
|
const [formDef, setFormDef] = useState<FormSchema | null>(null)
|
|
51
59
|
const [formDisabled, setFormDisabled] = useState(false)
|
|
@@ -112,10 +120,12 @@ export default function AskUserFormCard({
|
|
|
112
120
|
Boolean(payloadDef && payloadHasInputBlocks(payloadDef) && matchedBlocks.length === 0)
|
|
113
121
|
|
|
114
122
|
const renderBlocks = useMemo(() => {
|
|
123
|
+
if (usePayloadFallback && payloadDef) {
|
|
124
|
+
return getPayloadRenderableBlocks(payloadDef)
|
|
125
|
+
}
|
|
115
126
|
if (intersection && intersection.renderBlocks.length > 0) {
|
|
116
127
|
return intersection.renderBlocks
|
|
117
128
|
}
|
|
118
|
-
if (usePayloadFallback && payloadDef) return getPayloadRenderableBlocks(payloadDef)
|
|
119
129
|
return []
|
|
120
130
|
}, [intersection, usePayloadFallback, payloadDef])
|
|
121
131
|
|
|
@@ -213,6 +223,8 @@ export default function AskUserFormCard({
|
|
|
213
223
|
surfaceId={surfaceId}
|
|
214
224
|
fieldNames={fieldNames}
|
|
215
225
|
interactive
|
|
226
|
+
resumeSearch={resumeSearch}
|
|
227
|
+
caseSearch={caseSearch}
|
|
216
228
|
/>
|
|
217
229
|
|
|
218
230
|
<div className="ask-user-form-actions">
|
|
@@ -11,12 +11,12 @@ function createProcessor(): MessageProcessor<ReactComponentImplementation> {
|
|
|
11
11
|
return new MessageProcessor([buildSkoponCatalog()])
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
function buildStreamMessages(surfaceId: string): A2uiMessage[] {
|
|
14
|
+
function buildStreamMessages(surfaceId: string, label = '姓名'): A2uiMessage[] {
|
|
15
15
|
const doc = blocksToA2ui(
|
|
16
16
|
{
|
|
17
17
|
title: '',
|
|
18
18
|
description: '',
|
|
19
|
-
blocks: [{ id: 'b-name', type: 'text', name: 'name', label
|
|
19
|
+
blocks: [{ id: 'b-name', type: 'text', name: 'name', label }],
|
|
20
20
|
},
|
|
21
21
|
{ includeHeader: false },
|
|
22
22
|
)
|
|
@@ -59,4 +59,20 @@ describe('SkoponA2uiStreamRenderer stream lifecycle', () => {
|
|
|
59
59
|
rebuilt.processMessages(messages.slice(0, 1))
|
|
60
60
|
expect(rebuilt.model.surfacesMap.size).toBeLessThanOrEqual(countAfterFull)
|
|
61
61
|
})
|
|
62
|
+
|
|
63
|
+
it('rebuilds processor when message count is unchanged but content replaced', () => {
|
|
64
|
+
const surfaceId = 'stream-same-len'
|
|
65
|
+
const messagesA = buildStreamMessages(surfaceId, '姓名')
|
|
66
|
+
const messagesB = buildStreamMessages(surfaceId, '昵称')
|
|
67
|
+
expect(messagesA.length).toBe(messagesB.length)
|
|
68
|
+
|
|
69
|
+
const processor = createProcessor()
|
|
70
|
+
processor.processMessages(messagesA)
|
|
71
|
+
expect(processor.model.surfacesMap.size).toBeGreaterThan(0)
|
|
72
|
+
|
|
73
|
+
const rebuilt = createProcessor()
|
|
74
|
+
rebuilt.processMessages(messagesB)
|
|
75
|
+
expect(rebuilt.model.surfacesMap.size).toBeGreaterThan(0)
|
|
76
|
+
expect(JSON.stringify(messagesA)).not.toBe(JSON.stringify(messagesB))
|
|
77
|
+
})
|
|
62
78
|
})
|
|
@@ -18,12 +18,12 @@ import { blocksToA2ui, surfaceDocToMessages } from '../adapter/a2uiAdapter'
|
|
|
18
18
|
import { SKOPON_CATALOG_ID } from '../catalog/a2uiCustomCatalog'
|
|
19
19
|
import SkoponA2uiStreamRenderer from './SkoponA2uiStreamRenderer'
|
|
20
20
|
|
|
21
|
-
function buildStreamMessages(surfaceId: string): A2uiMessage[] {
|
|
21
|
+
function buildStreamMessages(surfaceId: string, label = '姓名'): A2uiMessage[] {
|
|
22
22
|
const doc = blocksToA2ui(
|
|
23
23
|
{
|
|
24
24
|
title: '',
|
|
25
25
|
description: '',
|
|
26
|
-
blocks: [{ id: 'b-name', type: 'text', name: 'name', label
|
|
26
|
+
blocks: [{ id: 'b-name', type: 'text', name: 'name', label }],
|
|
27
27
|
},
|
|
28
28
|
{ includeHeader: false },
|
|
29
29
|
)
|
|
@@ -76,4 +76,28 @@ describe('SkoponA2uiStreamRenderer component', () => {
|
|
|
76
76
|
expect(getByText('姓名')).toBeTruthy()
|
|
77
77
|
expect(queryByText('stream-empty')).toBeNull()
|
|
78
78
|
})
|
|
79
|
+
|
|
80
|
+
it('reprocesses messages when stream content is replaced at same length', () => {
|
|
81
|
+
const surfaceId = 'stream-replace'
|
|
82
|
+
const messagesA = buildStreamMessages(surfaceId, '姓名')
|
|
83
|
+
const messagesB = buildStreamMessages(surfaceId, '昵称')
|
|
84
|
+
expect(messagesA.length).toBe(messagesB.length)
|
|
85
|
+
|
|
86
|
+
const { rerender, getByText, queryByText } = render(
|
|
87
|
+
createElement(SkoponA2uiStreamRenderer, {
|
|
88
|
+
messages: messagesA,
|
|
89
|
+
surfaceId,
|
|
90
|
+
}),
|
|
91
|
+
)
|
|
92
|
+
expect(getByText('姓名')).toBeTruthy()
|
|
93
|
+
|
|
94
|
+
rerender(
|
|
95
|
+
createElement(SkoponA2uiStreamRenderer, {
|
|
96
|
+
messages: messagesB,
|
|
97
|
+
surfaceId,
|
|
98
|
+
}),
|
|
99
|
+
)
|
|
100
|
+
expect(getByText('昵称')).toBeTruthy()
|
|
101
|
+
expect(queryByText('姓名')).toBeNull()
|
|
102
|
+
})
|
|
79
103
|
})
|
|
@@ -5,6 +5,8 @@ import { A2uiSurface, type ReactComponentImplementation } from '@a2ui/react/v0_9
|
|
|
5
5
|
import { injectStyles } from '@a2ui/react/styles'
|
|
6
6
|
import { buildSkoponCatalog } from '../catalog/a2uiCustomCatalog'
|
|
7
7
|
import { A2uiPreviewModeProvider } from '../catalog/a2uiPreviewContext'
|
|
8
|
+
import { CaseSearchProvider, type CaseSearchFn } from '../catalog/caseSearchContext'
|
|
9
|
+
import { ResumeSearchProvider, type ResumeSearchFn } from '../catalog/resumeSearchContext'
|
|
8
10
|
|
|
9
11
|
export interface SkoponA2uiStreamRendererProps {
|
|
10
12
|
/** 增量追加的 A2UI v0.9 消息 */
|
|
@@ -12,6 +14,10 @@ export interface SkoponA2uiStreamRendererProps {
|
|
|
12
14
|
surfaceId?: string
|
|
13
15
|
emptyHint?: ReactNode
|
|
14
16
|
interactive?: boolean
|
|
17
|
+
/** 简历多选组件:注入搜索函数 */
|
|
18
|
+
resumeSearch?: ResumeSearchFn | null
|
|
19
|
+
/** 案例单选组件:注入搜索函数 */
|
|
20
|
+
caseSearch?: CaseSearchFn | null
|
|
15
21
|
}
|
|
16
22
|
|
|
17
23
|
function createProcessor(): MessageProcessor<ReactComponentImplementation> {
|
|
@@ -23,20 +29,55 @@ export default function SkoponA2uiStreamRenderer({
|
|
|
23
29
|
surfaceId = 'skopon-form-stream',
|
|
24
30
|
emptyHint = null,
|
|
25
31
|
interactive = true,
|
|
32
|
+
resumeSearch = null,
|
|
33
|
+
caseSearch = null,
|
|
26
34
|
}: SkoponA2uiStreamRendererProps) {
|
|
27
35
|
useEffect(() => {
|
|
28
36
|
injectStyles()
|
|
29
37
|
injectBasicCatalogStyles()
|
|
30
38
|
}, [])
|
|
31
39
|
|
|
32
|
-
const processorRef = useRef<MessageProcessor<ReactComponentImplementation
|
|
40
|
+
const processorRef = useRef<MessageProcessor<ReactComponentImplementation> | null>(null)
|
|
41
|
+
if (processorRef.current === null) {
|
|
42
|
+
processorRef.current = createProcessor()
|
|
43
|
+
}
|
|
33
44
|
const processedCountRef = useRef(0)
|
|
45
|
+
const lastProcessedMessagesRef = useRef('')
|
|
34
46
|
const mountedRef = useRef(true)
|
|
35
47
|
const surfaceIdRef = useRef(surfaceId)
|
|
36
48
|
|
|
37
|
-
|
|
49
|
+
function commitProcessor(
|
|
50
|
+
next: MessageProcessor<ReactComponentImplementation>,
|
|
51
|
+
processedMessages: A2uiMessage[],
|
|
52
|
+
) {
|
|
53
|
+
processorRef.current = next
|
|
54
|
+
processedCountRef.current = processedMessages.length
|
|
55
|
+
lastProcessedMessagesRef.current = JSON.stringify(processedMessages)
|
|
56
|
+
if (!mountedRef.current) return
|
|
57
|
+
setProcessor(next)
|
|
58
|
+
setSurfaces(Array.from(next.model.surfacesMap.values()))
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function processedPrefixChanged(messages: A2uiMessage[], prefixLen: number): boolean {
|
|
62
|
+
if (prefixLen <= 0) return false
|
|
63
|
+
const newPrefix = JSON.stringify(messages.slice(0, prefixLen))
|
|
64
|
+
const stored = lastProcessedMessagesRef.current
|
|
65
|
+
if (!stored) return true
|
|
66
|
+
try {
|
|
67
|
+
const parsed = JSON.parse(stored) as A2uiMessage[]
|
|
68
|
+
return JSON.stringify(parsed.slice(0, prefixLen)) !== newPrefix
|
|
69
|
+
} catch {
|
|
70
|
+
return true
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const [processor, setProcessor] = useState(
|
|
75
|
+
() => processorRef.current as MessageProcessor<ReactComponentImplementation>,
|
|
76
|
+
)
|
|
38
77
|
const [surfaces, setSurfaces] = useState(() =>
|
|
39
|
-
Array.from(
|
|
78
|
+
Array.from(
|
|
79
|
+
(processorRef.current as MessageProcessor<ReactComponentImplementation>).model.surfacesMap.values(),
|
|
80
|
+
),
|
|
40
81
|
)
|
|
41
82
|
|
|
42
83
|
useEffect(() => {
|
|
@@ -70,18 +111,17 @@ export default function SkoponA2uiStreamRenderer({
|
|
|
70
111
|
|
|
71
112
|
if (surfaceIdChanged) {
|
|
72
113
|
const next = createProcessor()
|
|
73
|
-
processorRef.current = next
|
|
74
|
-
processedCountRef.current = 0
|
|
75
114
|
if (Array.isArray(messages) && messages.length > 0) {
|
|
76
115
|
next.processMessages(messages)
|
|
77
|
-
|
|
116
|
+
if (!cancelled) commitProcessor(next, messages)
|
|
117
|
+
} else {
|
|
118
|
+
processorRef.current = next
|
|
119
|
+
processedCountRef.current = 0
|
|
120
|
+
lastProcessedMessagesRef.current = ''
|
|
78
121
|
if (!cancelled) {
|
|
79
122
|
setProcessor(next)
|
|
80
|
-
setSurfaces(
|
|
123
|
+
setSurfaces([])
|
|
81
124
|
}
|
|
82
|
-
} else if (!cancelled) {
|
|
83
|
-
setProcessor(next)
|
|
84
|
-
setSurfaces([])
|
|
85
125
|
}
|
|
86
126
|
return () => {
|
|
87
127
|
cancelled = true
|
|
@@ -93,6 +133,7 @@ export default function SkoponA2uiStreamRenderer({
|
|
|
93
133
|
const next = createProcessor()
|
|
94
134
|
processorRef.current = next
|
|
95
135
|
processedCountRef.current = 0
|
|
136
|
+
lastProcessedMessagesRef.current = ''
|
|
96
137
|
if (!cancelled) {
|
|
97
138
|
setProcessor(next)
|
|
98
139
|
setSurfaces([])
|
|
@@ -103,24 +144,22 @@ export default function SkoponA2uiStreamRenderer({
|
|
|
103
144
|
}
|
|
104
145
|
}
|
|
105
146
|
|
|
106
|
-
const activeProcessor = processorRef.current
|
|
147
|
+
const activeProcessor = processorRef.current as MessageProcessor<ReactComponentImplementation>
|
|
148
|
+
const processedCount = processedCountRef.current
|
|
107
149
|
|
|
108
|
-
if (
|
|
150
|
+
if (
|
|
151
|
+
messages.length < processedCount ||
|
|
152
|
+
processedPrefixChanged(messages, processedCount)
|
|
153
|
+
) {
|
|
109
154
|
const next = createProcessor()
|
|
110
|
-
processorRef.current = next
|
|
111
|
-
processedCountRef.current = 0
|
|
112
155
|
next.processMessages(messages)
|
|
113
|
-
|
|
114
|
-
if (!cancelled) {
|
|
115
|
-
setProcessor(next)
|
|
116
|
-
setSurfaces(Array.from(next.model.surfacesMap.values()))
|
|
117
|
-
}
|
|
156
|
+
if (!cancelled) commitProcessor(next, messages)
|
|
118
157
|
return () => {
|
|
119
158
|
cancelled = true
|
|
120
159
|
}
|
|
121
160
|
}
|
|
122
161
|
|
|
123
|
-
const pending = messages.slice(
|
|
162
|
+
const pending = messages.slice(processedCount)
|
|
124
163
|
if (pending.length === 0) {
|
|
125
164
|
return () => {
|
|
126
165
|
cancelled = true
|
|
@@ -128,6 +167,7 @@ export default function SkoponA2uiStreamRenderer({
|
|
|
128
167
|
}
|
|
129
168
|
activeProcessor.processMessages(pending)
|
|
130
169
|
processedCountRef.current = messages.length
|
|
170
|
+
lastProcessedMessagesRef.current = JSON.stringify(messages)
|
|
131
171
|
if (!cancelled) {
|
|
132
172
|
setSurfaces(Array.from(activeProcessor.model.surfacesMap.values()))
|
|
133
173
|
}
|
|
@@ -140,12 +180,16 @@ export default function SkoponA2uiStreamRenderer({
|
|
|
140
180
|
if (surfaces.length === 0) return <>{emptyHint}</>
|
|
141
181
|
|
|
142
182
|
return (
|
|
143
|
-
<
|
|
144
|
-
<
|
|
145
|
-
{
|
|
146
|
-
<
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
183
|
+
<ResumeSearchProvider resumeSearch={resumeSearch}>
|
|
184
|
+
<CaseSearchProvider caseSearch={caseSearch}>
|
|
185
|
+
<A2uiPreviewModeProvider interactive={interactive}>
|
|
186
|
+
<div className="a2ui-surface a2ui-container">
|
|
187
|
+
{surfaces.map((surface) => (
|
|
188
|
+
<A2uiSurface key={surface.id} surface={surface} />
|
|
189
|
+
))}
|
|
190
|
+
</div>
|
|
191
|
+
</A2uiPreviewModeProvider>
|
|
192
|
+
</CaseSearchProvider>
|
|
193
|
+
</ResumeSearchProvider>
|
|
150
194
|
)
|
|
151
195
|
}
|
|
@@ -15,6 +15,8 @@ import { extractSurfaceValues } from '../adapter/extractSurfaceValues'
|
|
|
15
15
|
import { isA2uiSurfaceEmpty, surfaceDocToMessages } from '../adapter/a2uiAdapter'
|
|
16
16
|
import { SKOPON_CATALOG_ID, buildSkoponCatalog } from '../catalog/a2uiCustomCatalog'
|
|
17
17
|
import { A2uiPreviewModeProvider } from '../catalog/a2uiPreviewContext'
|
|
18
|
+
import { CaseSearchProvider, type CaseSearchFn } from '../catalog/caseSearchContext'
|
|
19
|
+
import { ResumeSearchProvider, type ResumeSearchFn } from '../catalog/resumeSearchContext'
|
|
18
20
|
|
|
19
21
|
export interface SkoponFormRendererRef {
|
|
20
22
|
getValues: (fieldNames?: string[]) => Record<string, unknown>
|
|
@@ -27,6 +29,10 @@ export interface SkoponFormRendererProps {
|
|
|
27
29
|
interactive?: boolean
|
|
28
30
|
/** 默认用于 getValues() 的字段名列表 */
|
|
29
31
|
fieldNames?: string[]
|
|
32
|
+
/** 简历多选组件:注入搜索函数 */
|
|
33
|
+
resumeSearch?: ResumeSearchFn | null
|
|
34
|
+
/** 案例单选组件:注入搜索函数 */
|
|
35
|
+
caseSearch?: CaseSearchFn | null
|
|
30
36
|
}
|
|
31
37
|
|
|
32
38
|
const SkoponFormRenderer = forwardRef<SkoponFormRendererRef, SkoponFormRendererProps>(
|
|
@@ -37,6 +43,8 @@ const SkoponFormRenderer = forwardRef<SkoponFormRendererRef, SkoponFormRendererP
|
|
|
37
43
|
emptyHint = null,
|
|
38
44
|
interactive = true,
|
|
39
45
|
fieldNames = [],
|
|
46
|
+
resumeSearch = null,
|
|
47
|
+
caseSearch = null,
|
|
40
48
|
},
|
|
41
49
|
ref,
|
|
42
50
|
) {
|
|
@@ -49,13 +57,23 @@ const SkoponFormRenderer = forwardRef<SkoponFormRendererRef, SkoponFormRendererP
|
|
|
49
57
|
const fieldNamesRef = useRef(fieldNames)
|
|
50
58
|
fieldNamesRef.current = fieldNames
|
|
51
59
|
|
|
60
|
+
const docKey = useMemo(() => {
|
|
61
|
+
if (isA2uiSurfaceEmpty(doc)) return ''
|
|
62
|
+
try {
|
|
63
|
+
return JSON.stringify(doc)
|
|
64
|
+
} catch {
|
|
65
|
+
return String(doc)
|
|
66
|
+
}
|
|
67
|
+
}, [doc])
|
|
68
|
+
|
|
52
69
|
const renderResult = useMemo(() => {
|
|
53
|
-
if (
|
|
70
|
+
if (!docKey) {
|
|
54
71
|
return { surfaces: [], processor: null as MessageProcessor<ReactComponentImplementation> | null }
|
|
55
72
|
}
|
|
56
73
|
try {
|
|
74
|
+
const resolvedDoc = JSON.parse(docKey) as A2uiSurfaceDoc
|
|
57
75
|
const processor = new MessageProcessor([buildSkoponCatalog()])
|
|
58
|
-
const messages = surfaceDocToMessages(
|
|
76
|
+
const messages = surfaceDocToMessages(resolvedDoc, {
|
|
59
77
|
surfaceId,
|
|
60
78
|
catalogId: SKOPON_CATALOG_ID,
|
|
61
79
|
}) as unknown as A2uiMessage[]
|
|
@@ -68,7 +86,7 @@ const SkoponFormRenderer = forwardRef<SkoponFormRendererRef, SkoponFormRendererP
|
|
|
68
86
|
console.error('[SkoponFormRenderer] 渲染 A2UI surface 失败', error)
|
|
69
87
|
return { surfaces: [], processor: null as MessageProcessor<ReactComponentImplementation> | null }
|
|
70
88
|
}
|
|
71
|
-
}, [
|
|
89
|
+
}, [docKey, surfaceId])
|
|
72
90
|
|
|
73
91
|
const { surfaces, processor } = renderResult
|
|
74
92
|
processorRef.current = processor
|
|
@@ -89,13 +107,17 @@ const SkoponFormRenderer = forwardRef<SkoponFormRendererRef, SkoponFormRendererP
|
|
|
89
107
|
if (surfaces.length === 0) return <>{emptyHint}</>
|
|
90
108
|
|
|
91
109
|
return (
|
|
92
|
-
<
|
|
93
|
-
<
|
|
94
|
-
{
|
|
95
|
-
<
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
110
|
+
<ResumeSearchProvider resumeSearch={resumeSearch}>
|
|
111
|
+
<CaseSearchProvider caseSearch={caseSearch}>
|
|
112
|
+
<A2uiPreviewModeProvider interactive={interactive}>
|
|
113
|
+
<div className="a2ui-surface a2ui-container">
|
|
114
|
+
{surfaces.map((surface) => (
|
|
115
|
+
<A2uiSurface key={surface.id} surface={surface} />
|
|
116
|
+
))}
|
|
117
|
+
</div>
|
|
118
|
+
</A2uiPreviewModeProvider>
|
|
119
|
+
</CaseSearchProvider>
|
|
120
|
+
</ResumeSearchProvider>
|
|
99
121
|
)
|
|
100
122
|
},
|
|
101
123
|
)
|
package/src/index.ts
CHANGED
|
@@ -63,6 +63,8 @@ export { createFormClient, type FormClient, type FormClientOptions } from './cli
|
|
|
63
63
|
|
|
64
64
|
export { default as SkoponFormRenderer } from './components/SkoponFormRenderer'
|
|
65
65
|
export type { SkoponFormRendererProps, SkoponFormRendererRef } from './components/SkoponFormRenderer'
|
|
66
|
+
export type { ResumeSearchFn as SkoponResumeSearchFn } from './catalog/resumeSearchContext'
|
|
67
|
+
export type { CaseSearchFn as SkoponCaseSearchFn } from './catalog/caseSearchContext'
|
|
66
68
|
|
|
67
69
|
export { default as SkoponA2uiStreamRenderer } from './components/SkoponA2uiStreamRenderer'
|
|
68
70
|
export type { SkoponA2uiStreamRendererProps } from './components/SkoponA2uiStreamRenderer'
|
|
@@ -73,4 +75,25 @@ export type { AskUserFormCardProps } from './components/AskUserFormCard'
|
|
|
73
75
|
export { default as CurlSubmitBlock } from './components/CurlSubmitBlock'
|
|
74
76
|
export type { CurlSubmitBlockProps } from './components/CurlSubmitBlock'
|
|
75
77
|
|
|
78
|
+
|
|
79
|
+
export {
|
|
80
|
+
ResumeSearchProvider,
|
|
81
|
+
useResumeSearch,
|
|
82
|
+
type ResumeSearchFn,
|
|
83
|
+
type ResumeSearchItem,
|
|
84
|
+
type ResumeSearchParams,
|
|
85
|
+
type ResumeSearchResult,
|
|
86
|
+
} from './catalog/resumeSearchContext'
|
|
87
|
+
|
|
88
|
+
export {
|
|
89
|
+
CaseSearchProvider,
|
|
90
|
+
useCaseSearch,
|
|
91
|
+
type CaseSearchFn,
|
|
92
|
+
type CaseSearchItem,
|
|
93
|
+
type CaseSearchParams,
|
|
94
|
+
type CaseSearchResult,
|
|
95
|
+
} from './catalog/caseSearchContext'
|
|
96
|
+
|
|
97
|
+
export type { FormResumeFilter, FormCaseFilter } from './types/index'
|
|
98
|
+
|
|
76
99
|
import './styles/index.css'
|
|
@@ -254,8 +254,8 @@
|
|
|
254
254
|
box-shadow: var(--shadow-focus);
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
-
/* 原生 A2UI 按钮/chip;勿作用于 Ant Design(Switch、Select、Picker
|
|
258
|
-
.a2ui-surface.a2ui-container button:not([class*='ant-']) {
|
|
257
|
+
/* 原生 A2UI 按钮/chip;勿作用于 Ant Design(Switch、Select、Picker 等)及 Skopon 简历/案例卡片 */
|
|
258
|
+
.a2ui-surface.a2ui-container button:not([class*='ant-']):not(.skopon-resume-select-card) {
|
|
259
259
|
min-height: var(--button-height-md);
|
|
260
260
|
padding: 0 var(--button-padding-x-md);
|
|
261
261
|
font-family: var(--font-sans);
|
|
@@ -273,12 +273,12 @@
|
|
|
273
273
|
color var(--duration-fast) var(--ease-default);
|
|
274
274
|
}
|
|
275
275
|
|
|
276
|
-
.a2ui-surface.a2ui-container button:not([class*='ant-']):hover:not(:disabled) {
|
|
276
|
+
.a2ui-surface.a2ui-container button:not([class*='ant-']):not(.skopon-resume-select-card):hover:not(:disabled) {
|
|
277
277
|
border-color: var(--color-border-strong);
|
|
278
278
|
background: var(--color-bg-subtle);
|
|
279
279
|
}
|
|
280
280
|
|
|
281
|
-
.a2ui-surface.a2ui-container button:not([class*='ant-']):disabled {
|
|
281
|
+
.a2ui-surface.a2ui-container button:not([class*='ant-']):not(.skopon-resume-select-card):disabled {
|
|
282
282
|
cursor: not-allowed;
|
|
283
283
|
opacity: 0.45;
|
|
284
284
|
}
|
package/src/styles/index.css
CHANGED
|
@@ -193,3 +193,194 @@
|
|
|
193
193
|
white-space: pre-wrap;
|
|
194
194
|
word-break: break-all;
|
|
195
195
|
}
|
|
196
|
+
|
|
197
|
+
.skopon-resume-select-list {
|
|
198
|
+
display: flex;
|
|
199
|
+
flex-direction: row;
|
|
200
|
+
flex-wrap: nowrap;
|
|
201
|
+
gap: var(--space-3, 12px);
|
|
202
|
+
margin-top: var(--space-2, 8px);
|
|
203
|
+
overflow-x: auto;
|
|
204
|
+
padding-bottom: var(--space-1, 4px);
|
|
205
|
+
-webkit-overflow-scrolling: touch;
|
|
206
|
+
}
|
|
207
|
+
.skopon-resume-select-card {
|
|
208
|
+
display: flex;
|
|
209
|
+
flex: 0 0 auto;
|
|
210
|
+
box-sizing: border-box;
|
|
211
|
+
padding: 12px;
|
|
212
|
+
border: 1px solid var(--color-border, #d9d9d9);
|
|
213
|
+
border-radius: 4px;
|
|
214
|
+
background: var(--color-surface, #fff);
|
|
215
|
+
text-align: left;
|
|
216
|
+
cursor: pointer;
|
|
217
|
+
min-height: unset;
|
|
218
|
+
font-weight: inherit;
|
|
219
|
+
line-height: inherit;
|
|
220
|
+
appearance: none;
|
|
221
|
+
-webkit-appearance: none;
|
|
222
|
+
}
|
|
223
|
+
.a2ui-surface.a2ui-container button.skopon-resume-select-card {
|
|
224
|
+
border-radius: 4px;
|
|
225
|
+
min-height: unset;
|
|
226
|
+
padding: 12px;
|
|
227
|
+
font-weight: inherit;
|
|
228
|
+
}
|
|
229
|
+
.skopon-resume-select-card--profile {
|
|
230
|
+
flex-direction: column;
|
|
231
|
+
align-items: center;
|
|
232
|
+
width: 200px;
|
|
233
|
+
min-width: 200px;
|
|
234
|
+
max-width: 200px;
|
|
235
|
+
height: 320px;
|
|
236
|
+
min-height: 320px;
|
|
237
|
+
max-height: 320px;
|
|
238
|
+
padding: 10px;
|
|
239
|
+
overflow: hidden;
|
|
240
|
+
}
|
|
241
|
+
.a2ui-surface.a2ui-container button.skopon-resume-select-card--profile {
|
|
242
|
+
padding: 10px;
|
|
243
|
+
min-height: 320px;
|
|
244
|
+
}
|
|
245
|
+
.skopon-resume-select-card:not(.skopon-resume-select-card--profile) {
|
|
246
|
+
flex-direction: row;
|
|
247
|
+
align-items: flex-start;
|
|
248
|
+
gap: 12px;
|
|
249
|
+
width: 220px;
|
|
250
|
+
min-width: 220px;
|
|
251
|
+
}
|
|
252
|
+
.skopon-resume-select-card:hover:not(:disabled) {
|
|
253
|
+
border-color: var(--color-primary, #1677ff);
|
|
254
|
+
}
|
|
255
|
+
.skopon-resume-select-card--selected {
|
|
256
|
+
border-color: var(--color-primary, #1677ff);
|
|
257
|
+
background: color-mix(in srgb, var(--color-primary, #1677ff) 8%, transparent);
|
|
258
|
+
}
|
|
259
|
+
.skopon-resume-select-card:disabled {
|
|
260
|
+
cursor: not-allowed;
|
|
261
|
+
opacity: 0.72;
|
|
262
|
+
}
|
|
263
|
+
.skopon-resume-select-card-avatar {
|
|
264
|
+
flex-shrink: 0;
|
|
265
|
+
}
|
|
266
|
+
.skopon-resume-select-card--profile .skopon-resume-select-card-avatar {
|
|
267
|
+
margin-bottom: 6px;
|
|
268
|
+
}
|
|
269
|
+
.skopon-resume-select-card-name {
|
|
270
|
+
font-weight: 600;
|
|
271
|
+
font-size: 14px;
|
|
272
|
+
line-height: 1.3;
|
|
273
|
+
margin-bottom: 6px;
|
|
274
|
+
overflow: hidden;
|
|
275
|
+
text-overflow: ellipsis;
|
|
276
|
+
white-space: nowrap;
|
|
277
|
+
color: var(--color-text, rgba(0, 0, 0, 0.88));
|
|
278
|
+
}
|
|
279
|
+
.skopon-resume-select-card--profile > .skopon-resume-select-card-name {
|
|
280
|
+
flex-shrink: 0;
|
|
281
|
+
width: 100%;
|
|
282
|
+
text-align: center;
|
|
283
|
+
}
|
|
284
|
+
.skopon-resume-select-card-body {
|
|
285
|
+
width: 100%;
|
|
286
|
+
min-width: 0;
|
|
287
|
+
text-align: left;
|
|
288
|
+
}
|
|
289
|
+
.skopon-resume-select-card--profile .skopon-resume-select-card-body {
|
|
290
|
+
flex: 1;
|
|
291
|
+
min-height: 0;
|
|
292
|
+
display: flex;
|
|
293
|
+
flex-direction: column;
|
|
294
|
+
overflow-x: hidden;
|
|
295
|
+
overflow-y: auto;
|
|
296
|
+
}
|
|
297
|
+
.skopon-resume-select-card-field {
|
|
298
|
+
margin-bottom: 4px;
|
|
299
|
+
flex-shrink: 0;
|
|
300
|
+
}
|
|
301
|
+
.skopon-resume-select-card-field-value {
|
|
302
|
+
font-size: 12px;
|
|
303
|
+
color: var(--color-text, rgba(0, 0, 0, 0.88));
|
|
304
|
+
}
|
|
305
|
+
.skopon-resume-select-card-tags-empty {
|
|
306
|
+
font-size: 12px;
|
|
307
|
+
color: var(--color-text-secondary, rgba(0, 0, 0, 0.45));
|
|
308
|
+
}
|
|
309
|
+
.skopon-resume-select-card-tags {
|
|
310
|
+
margin-bottom: 4px;
|
|
311
|
+
flex-shrink: 0;
|
|
312
|
+
}
|
|
313
|
+
.skopon-resume-select-card-tags-label,
|
|
314
|
+
.skopon-resume-select-card-works-label {
|
|
315
|
+
display: block;
|
|
316
|
+
margin-bottom: 2px;
|
|
317
|
+
font-size: 11px;
|
|
318
|
+
color: var(--color-text-secondary, rgba(0, 0, 0, 0.45));
|
|
319
|
+
}
|
|
320
|
+
.skopon-resume-select-card-tags-list {
|
|
321
|
+
display: flex;
|
|
322
|
+
flex-wrap: wrap;
|
|
323
|
+
gap: 4px;
|
|
324
|
+
}
|
|
325
|
+
.skopon-resume-select-card-tag {
|
|
326
|
+
margin: 0;
|
|
327
|
+
font-size: 11px;
|
|
328
|
+
line-height: 18px;
|
|
329
|
+
}
|
|
330
|
+
.skopon-resume-select-card-works {
|
|
331
|
+
margin-top: 4px;
|
|
332
|
+
flex-shrink: 0;
|
|
333
|
+
}
|
|
334
|
+
.skopon-resume-select-card-works-list {
|
|
335
|
+
display: flex;
|
|
336
|
+
flex-direction: column;
|
|
337
|
+
gap: 4px;
|
|
338
|
+
}
|
|
339
|
+
.skopon-resume-select-card-works-empty {
|
|
340
|
+
font-size: 12px;
|
|
341
|
+
color: var(--color-text-secondary, rgba(0, 0, 0, 0.45));
|
|
342
|
+
}
|
|
343
|
+
.skopon-resume-select-card-work {
|
|
344
|
+
display: flex;
|
|
345
|
+
align-items: center;
|
|
346
|
+
gap: 6px;
|
|
347
|
+
min-width: 0;
|
|
348
|
+
}
|
|
349
|
+
.skopon-resume-select-card-work-cover {
|
|
350
|
+
display: flex;
|
|
351
|
+
align-items: center;
|
|
352
|
+
justify-content: center;
|
|
353
|
+
flex-shrink: 0;
|
|
354
|
+
width: 32px;
|
|
355
|
+
height: 32px;
|
|
356
|
+
border-radius: 2px;
|
|
357
|
+
border: 1px solid var(--color-border, #d9d9d9);
|
|
358
|
+
background: var(--color-bg-subtle, rgba(0, 0, 0, 0.02));
|
|
359
|
+
overflow: hidden;
|
|
360
|
+
}
|
|
361
|
+
.skopon-resume-select-card-work-img {
|
|
362
|
+
width: 100%;
|
|
363
|
+
height: 100%;
|
|
364
|
+
object-fit: cover;
|
|
365
|
+
}
|
|
366
|
+
.skopon-resume-select-card-work-placeholder {
|
|
367
|
+
font-size: 16px;
|
|
368
|
+
color: var(--color-text-secondary, rgba(0, 0, 0, 0.45));
|
|
369
|
+
}
|
|
370
|
+
.skopon-resume-select-card-work-title {
|
|
371
|
+
flex: 1;
|
|
372
|
+
min-width: 0;
|
|
373
|
+
font-size: 12px;
|
|
374
|
+
color: var(--color-text, rgba(0, 0, 0, 0.88));
|
|
375
|
+
overflow: hidden;
|
|
376
|
+
text-overflow: ellipsis;
|
|
377
|
+
white-space: nowrap;
|
|
378
|
+
}
|
|
379
|
+
.skopon-resume-select-actions {
|
|
380
|
+
margin-top: var(--space-2, 8px);
|
|
381
|
+
}
|
|
382
|
+
.skopon-resume-select-empty,
|
|
383
|
+
.skopon-resume-select-status {
|
|
384
|
+
padding: 12px 0;
|
|
385
|
+
color: var(--color-text-secondary, rgba(0, 0, 0, 0.45));
|
|
386
|
+
}
|
|
@@ -12,33 +12,12 @@ export function buildCurlStatement(payload: unknown, callbackUrl?: string | null
|
|
|
12
12
|
].join('\n')
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
return ` ${JSON.stringify(key)}: ${JSON.stringify(value)}${trailingComma ? ',' : ''}`
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/** 构建含行注释的 JSON body:extra 段标注「额外字段(未在卡片展示)」 */
|
|
15
|
+
/** 构建合法 JSON body(card + extra 合并,便于 curl 直接执行) */
|
|
20
16
|
export function buildAskUserCurlBodyJson(
|
|
21
17
|
cardValues: Record<string, unknown>,
|
|
22
18
|
extraValues: Record<string, unknown>,
|
|
23
19
|
): string {
|
|
24
|
-
|
|
25
|
-
const cardEntries = Object.entries(cardValues)
|
|
26
|
-
const extraEntries = Object.entries(extraValues)
|
|
27
|
-
|
|
28
|
-
cardEntries.forEach(([key, value], index) => {
|
|
29
|
-
const hasMore = index < cardEntries.length - 1 || extraEntries.length > 0
|
|
30
|
-
lines.push(formatJsonBodyLine(key, value, hasMore))
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
if (extraEntries.length > 0) {
|
|
34
|
-
lines.push(' // 额外字段(未在卡片展示)')
|
|
35
|
-
extraEntries.forEach(([key, value], index) => {
|
|
36
|
-
lines.push(formatJsonBodyLine(key, value, index < extraEntries.length - 1))
|
|
37
|
-
})
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
lines.push('}')
|
|
41
|
-
return lines.join('\n')
|
|
20
|
+
return JSON.stringify({ ...cardValues, ...extraValues }, null, 2)
|
|
42
21
|
}
|
|
43
22
|
|
|
44
23
|
export interface BuildAskUserCurlStatementOptions {
|