@skopon-cool/form-sdk 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +2 -0
  2. package/dist/adapter/a2uiAdapter.d.ts +0 -1
  3. package/dist/adapter/a2uiAdapter.d.ts.map +1 -1
  4. package/dist/adapter/formSchema.d.ts.map +1 -1
  5. package/dist/blocks/case_singleselect/adapter.d.ts +10 -0
  6. package/dist/blocks/case_singleselect/adapter.d.ts.map +1 -0
  7. package/dist/blocks/case_singleselect/catalog.d.ts +2 -0
  8. package/dist/blocks/case_singleselect/catalog.d.ts.map +1 -0
  9. package/dist/blocks/case_singleselect/index.d.ts +3 -0
  10. package/dist/blocks/case_singleselect/index.d.ts.map +1 -0
  11. package/dist/blocks/registry.d.ts +8 -0
  12. package/dist/blocks/registry.d.ts.map +1 -0
  13. package/dist/blocks/resume_multiselect/adapter.d.ts +10 -0
  14. package/dist/blocks/resume_multiselect/adapter.d.ts.map +1 -0
  15. package/dist/blocks/resume_multiselect/catalog.d.ts +2 -0
  16. package/dist/blocks/resume_multiselect/catalog.d.ts.map +1 -0
  17. package/dist/blocks/resume_multiselect/index.d.ts +3 -0
  18. package/dist/blocks/resume_multiselect/index.d.ts.map +1 -0
  19. package/dist/blocks/types.d.ts +27 -0
  20. package/dist/blocks/types.d.ts.map +1 -0
  21. package/dist/catalog/a2uiCustomCatalog.d.ts.map +1 -1
  22. package/dist/catalog/caseSearchContext.d.ts +28 -0
  23. package/dist/catalog/caseSearchContext.d.ts.map +1 -0
  24. package/dist/catalog/resumeSearchContext.d.ts +30 -0
  25. package/dist/catalog/resumeSearchContext.d.ts.map +1 -0
  26. package/dist/catalog/skoponCaseSelect.d.ts +2 -0
  27. package/dist/catalog/skoponCaseSelect.d.ts.map +1 -0
  28. package/dist/catalog/skoponResumeSelect.d.ts +2 -0
  29. package/dist/catalog/skoponResumeSelect.d.ts.map +1 -0
  30. package/dist/components/AskUserFormCard.d.ts +7 -1
  31. package/dist/components/AskUserFormCard.d.ts.map +1 -1
  32. package/dist/components/SkoponA2uiStreamRenderer.d.ts +7 -1
  33. package/dist/components/SkoponA2uiStreamRenderer.d.ts.map +1 -1
  34. package/dist/components/SkoponFormRenderer.d.ts +6 -0
  35. package/dist/components/SkoponFormRenderer.d.ts.map +1 -1
  36. package/dist/form-sdk.css +1 -1
  37. package/dist/index.d.ts +5 -0
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +1290 -845
  40. package/dist/types/index.d.ts +26 -2
  41. package/dist/types/index.d.ts.map +1 -1
  42. package/package.json +1 -1
  43. package/src/adapter/a2uiAdapter.test.ts +48 -0
  44. package/src/adapter/a2uiAdapter.ts +41 -1
  45. package/src/adapter/formSchema.ts +65 -2
  46. package/src/blocks/case_singleselect/adapter.ts +74 -0
  47. package/src/blocks/case_singleselect/catalog.ts +1 -0
  48. package/src/blocks/case_singleselect/index.ts +14 -0
  49. package/src/blocks/registry.ts +34 -0
  50. package/src/blocks/resume_multiselect/adapter.ts +57 -0
  51. package/src/blocks/resume_multiselect/catalog.ts +1 -0
  52. package/src/blocks/resume_multiselect/index.ts +14 -0
  53. package/src/blocks/types.ts +34 -0
  54. package/src/catalog/a2uiCustomCatalog.tsx +6 -0
  55. package/src/catalog/caseSearchContext.tsx +46 -0
  56. package/src/catalog/resumeSearchContext.tsx +48 -0
  57. package/src/catalog/skoponCaseSelect.tsx +215 -0
  58. package/src/catalog/skoponResumeSelect.tsx +227 -0
  59. package/src/components/AskUserFormCard.tsx +10 -0
  60. package/src/components/SkoponA2uiStreamRenderer.test.ts +18 -2
  61. package/src/components/SkoponA2uiStreamRenderer.test.tsx +26 -2
  62. package/src/components/SkoponA2uiStreamRenderer.tsx +60 -23
  63. package/src/components/SkoponFormRenderer.tsx +32 -10
  64. package/src/index.ts +23 -0
  65. package/src/styles/index.css +60 -0
  66. package/src/types/index.ts +30 -0
@@ -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,6 +29,8 @@ 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()
@@ -31,9 +39,35 @@ export default function SkoponA2uiStreamRenderer({
31
39
 
32
40
  const processorRef = useRef<MessageProcessor<ReactComponentImplementation>>(createProcessor())
33
41
  const processedCountRef = useRef(0)
42
+ const lastProcessedMessagesRef = useRef('')
34
43
  const mountedRef = useRef(true)
35
44
  const surfaceIdRef = useRef(surfaceId)
36
45
 
46
+ function commitProcessor(
47
+ next: MessageProcessor<ReactComponentImplementation>,
48
+ processedMessages: A2uiMessage[],
49
+ ) {
50
+ processorRef.current = next
51
+ processedCountRef.current = processedMessages.length
52
+ lastProcessedMessagesRef.current = JSON.stringify(processedMessages)
53
+ if (!mountedRef.current) return
54
+ setProcessor(next)
55
+ setSurfaces(Array.from(next.model.surfacesMap.values()))
56
+ }
57
+
58
+ function processedPrefixChanged(messages: A2uiMessage[], prefixLen: number): boolean {
59
+ if (prefixLen <= 0) return false
60
+ const newPrefix = JSON.stringify(messages.slice(0, prefixLen))
61
+ const stored = lastProcessedMessagesRef.current
62
+ if (!stored) return true
63
+ try {
64
+ const parsed = JSON.parse(stored) as A2uiMessage[]
65
+ return JSON.stringify(parsed.slice(0, prefixLen)) !== newPrefix
66
+ } catch {
67
+ return true
68
+ }
69
+ }
70
+
37
71
  const [processor, setProcessor] = useState(() => processorRef.current)
38
72
  const [surfaces, setSurfaces] = useState(() =>
39
73
  Array.from(processorRef.current.model.surfacesMap.values()),
@@ -70,18 +104,17 @@ export default function SkoponA2uiStreamRenderer({
70
104
 
71
105
  if (surfaceIdChanged) {
72
106
  const next = createProcessor()
73
- processorRef.current = next
74
- processedCountRef.current = 0
75
107
  if (Array.isArray(messages) && messages.length > 0) {
76
108
  next.processMessages(messages)
77
- processedCountRef.current = messages.length
109
+ if (!cancelled) commitProcessor(next, messages)
110
+ } else {
111
+ processorRef.current = next
112
+ processedCountRef.current = 0
113
+ lastProcessedMessagesRef.current = ''
78
114
  if (!cancelled) {
79
115
  setProcessor(next)
80
- setSurfaces(Array.from(next.model.surfacesMap.values()))
116
+ setSurfaces([])
81
117
  }
82
- } else if (!cancelled) {
83
- setProcessor(next)
84
- setSurfaces([])
85
118
  }
86
119
  return () => {
87
120
  cancelled = true
@@ -93,6 +126,7 @@ export default function SkoponA2uiStreamRenderer({
93
126
  const next = createProcessor()
94
127
  processorRef.current = next
95
128
  processedCountRef.current = 0
129
+ lastProcessedMessagesRef.current = ''
96
130
  if (!cancelled) {
97
131
  setProcessor(next)
98
132
  setSurfaces([])
@@ -104,23 +138,21 @@ export default function SkoponA2uiStreamRenderer({
104
138
  }
105
139
 
106
140
  const activeProcessor = processorRef.current
141
+ const processedCount = processedCountRef.current
107
142
 
108
- if (messages.length < processedCountRef.current) {
143
+ if (
144
+ messages.length < processedCount ||
145
+ processedPrefixChanged(messages, processedCount)
146
+ ) {
109
147
  const next = createProcessor()
110
- processorRef.current = next
111
- processedCountRef.current = 0
112
148
  next.processMessages(messages)
113
- processedCountRef.current = messages.length
114
- if (!cancelled) {
115
- setProcessor(next)
116
- setSurfaces(Array.from(next.model.surfacesMap.values()))
117
- }
149
+ if (!cancelled) commitProcessor(next, messages)
118
150
  return () => {
119
151
  cancelled = true
120
152
  }
121
153
  }
122
154
 
123
- const pending = messages.slice(processedCountRef.current)
155
+ const pending = messages.slice(processedCount)
124
156
  if (pending.length === 0) {
125
157
  return () => {
126
158
  cancelled = true
@@ -128,6 +160,7 @@ export default function SkoponA2uiStreamRenderer({
128
160
  }
129
161
  activeProcessor.processMessages(pending)
130
162
  processedCountRef.current = messages.length
163
+ lastProcessedMessagesRef.current = JSON.stringify(messages)
131
164
  if (!cancelled) {
132
165
  setSurfaces(Array.from(activeProcessor.model.surfacesMap.values()))
133
166
  }
@@ -140,12 +173,16 @@ export default function SkoponA2uiStreamRenderer({
140
173
  if (surfaces.length === 0) return <>{emptyHint}</>
141
174
 
142
175
  return (
143
- <A2uiPreviewModeProvider interactive={interactive}>
144
- <div className="a2ui-surface a2ui-container">
145
- {surfaces.map((surface) => (
146
- <A2uiSurface key={surface.id} surface={surface} />
147
- ))}
148
- </div>
149
- </A2uiPreviewModeProvider>
176
+ <ResumeSearchProvider resumeSearch={resumeSearch}>
177
+ <CaseSearchProvider caseSearch={caseSearch}>
178
+ <A2uiPreviewModeProvider interactive={interactive}>
179
+ <div className="a2ui-surface a2ui-container">
180
+ {surfaces.map((surface) => (
181
+ <A2uiSurface key={surface.id} surface={surface} />
182
+ ))}
183
+ </div>
184
+ </A2uiPreviewModeProvider>
185
+ </CaseSearchProvider>
186
+ </ResumeSearchProvider>
150
187
  )
151
188
  }
@@ -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 (isA2uiSurfaceEmpty(doc)) {
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(doc!, {
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
- }, [doc, surfaceId])
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
- <A2uiPreviewModeProvider interactive={interactive}>
93
- <div className="a2ui-surface a2ui-container">
94
- {surfaces.map((surface) => (
95
- <A2uiSurface key={surface.id} surface={surface} />
96
- ))}
97
- </div>
98
- </A2uiPreviewModeProvider>
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'
@@ -193,3 +193,63 @@
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: column;
200
+ gap: var(--space-2, 8px);
201
+ margin-top: var(--space-2, 8px);
202
+ }
203
+ .skopon-resume-select-card {
204
+ display: flex;
205
+ align-items: flex-start;
206
+ gap: 12px;
207
+ width: 100%;
208
+ padding: 12px;
209
+ border: 1px solid var(--color-border, #d9d9d9);
210
+ border-radius: var(--radius-md, 8px);
211
+ background: var(--color-surface, #fff);
212
+ text-align: left;
213
+ cursor: pointer;
214
+ }
215
+ .skopon-resume-select-card:hover:not(:disabled) {
216
+ border-color: var(--color-primary, #1677ff);
217
+ }
218
+ .skopon-resume-select-card--selected {
219
+ border-color: var(--color-primary, #1677ff);
220
+ background: color-mix(in srgb, var(--color-primary, #1677ff) 8%, transparent);
221
+ }
222
+ .skopon-resume-select-card:disabled {
223
+ cursor: not-allowed;
224
+ opacity: 0.72;
225
+ }
226
+ .skopon-resume-select-card-body {
227
+ flex: 1;
228
+ min-width: 0;
229
+ }
230
+ .skopon-resume-select-card-name {
231
+ font-weight: 600;
232
+ margin-bottom: 4px;
233
+ }
234
+ .skopon-resume-select-card-desc {
235
+ color: var(--color-text-secondary, rgba(0, 0, 0, 0.65));
236
+ font-size: 13px;
237
+ margin-bottom: 4px;
238
+ overflow: hidden;
239
+ text-overflow: ellipsis;
240
+ display: -webkit-box;
241
+ -webkit-line-clamp: 2;
242
+ -webkit-box-orient: vertical;
243
+ }
244
+ .skopon-resume-select-card-meta {
245
+ color: var(--color-text-secondary, rgba(0, 0, 0, 0.45));
246
+ font-size: 12px;
247
+ }
248
+ .skopon-resume-select-actions {
249
+ margin-top: var(--space-2, 8px);
250
+ }
251
+ .skopon-resume-select-empty,
252
+ .skopon-resume-select-status {
253
+ padding: 12px 0;
254
+ color: var(--color-text-secondary, rgba(0, 0, 0, 0.45));
255
+ }
@@ -17,6 +17,8 @@ export type A2uiComponentName =
17
17
  | 'FileUpload'
18
18
  | 'SkoponMedia'
19
19
  | 'SkoponSelect'
20
+ | 'SkoponResumeSelect'
21
+ | 'SkoponCaseSelect'
20
22
 
21
23
  export interface A2uiComponentNode {
22
24
  id: string
@@ -53,6 +55,8 @@ export type FormBlockType =
53
55
  | 'datetime'
54
56
  | 'time'
55
57
  | 'file'
58
+ | 'resume_multiselect'
59
+ | 'case_singleselect'
56
60
  | 'image'
57
61
  | 'video'
58
62
  | 'audio'
@@ -80,6 +84,24 @@ export interface FormBlockOption {
80
84
  label: string
81
85
  }
82
86
 
87
+ /** 简历多选组件:渲染时用于 /univ/resume/search 的筛选条件 */
88
+ export interface FormResumeFilter {
89
+ names?: string[]
90
+ agentUniqueIds?: string[]
91
+ resumeUniqueIds?: string[]
92
+ pageSize?: number
93
+ }
94
+
95
+ /** 案例单选组件:渲染时用于 /univ/case/list 的筛选条件 */
96
+ export interface FormCaseFilter {
97
+ /** Agent 类型(编辑器辅助,运行时不用) */
98
+ agentKind?: string
99
+ /** Agent unique_id(编辑器辅助,运行时不用) */
100
+ agentUniqueId?: string
101
+ flowId?: number
102
+ pageSize?: number
103
+ }
104
+
83
105
  export interface FormBlock {
84
106
  id: string
85
107
  type: FormBlockType
@@ -98,6 +120,14 @@ export interface FormBlock {
98
120
  fileMinCount?: number
99
121
  fileMaxCount?: number
100
122
  defaultValue?: string | string[] | boolean
123
+ /** 简历多选:是否显示「换一批」 */
124
+ resumeEnableRefresh?: boolean
125
+ /** 简历多选:列表筛选条件(持久化进 block json) */
126
+ resumeFilter?: FormResumeFilter
127
+ /** 案例单选:是否显示「换一批」 */
128
+ caseEnableRefresh?: boolean
129
+ /** 案例单选:列表筛选条件(持久化进 block json) */
130
+ caseFilter?: FormCaseFilter
101
131
  }
102
132
 
103
133
  export interface FormSchema {