@@ -52,6 +52,14 @@ export default function WorkflowsTab() {
5252 const [ showFilePicker , setShowFilePicker ] = useState ( false ) ;
5353 const [ executeTarget , setExecuteTarget ] = useState < number | null > ( null ) ;
5454
55+ // AI Workflow Generator state (v23.0)
56+ const [ showAiGen , setShowAiGen ] = useState ( false ) ;
57+ const [ aiDesc , setAiDesc ] = useState ( '' ) ;
58+ const [ aiGenerating , setAiGenerating ] = useState ( false ) ;
59+ const [ aiPreview , setAiPreview ] = useState < any > ( null ) ;
60+ const [ aiError , setAiError ] = useState ( '' ) ;
61+ const [ aiSaving , setAiSaving ] = useState ( false ) ;
62+
5563 const fetchWorkflows = async ( ) => {
5664 setLoading ( true ) ;
5765 try {
@@ -69,6 +77,54 @@ export default function WorkflowsTab() {
6977 const handleCreate = ( ) => {
7078 setEditWorkflow ( null ) ;
7179 setEditing ( true ) ;
80+ setShowAiGen ( false ) ;
81+ } ;
82+
83+ // --- AI Workflow Generator handlers (v23.0) ---
84+ const handleAiGenerate = async ( ) => {
85+ if ( ! aiDesc . trim ( ) ) { setAiError ( '请描述你想创建的工作流' ) ; return ; }
86+ setAiGenerating ( true ) ; setAiError ( '' ) ; setAiPreview ( null ) ;
87+ try {
88+ const resp = await fetch ( '/api/workflows/generate' , {
89+ method : 'POST' , credentials : 'include' ,
90+ headers : { 'Content-Type' : 'application/json' } ,
91+ body : JSON . stringify ( { description : aiDesc . trim ( ) } ) ,
92+ } ) ;
93+ const data = await resp . json ( ) ;
94+ if ( resp . ok && data . workflow ) {
95+ setAiPreview ( data ) ;
96+ } else {
97+ setAiError ( data . error || data . message || 'AI 生成失败,请重试' ) ;
98+ }
99+ } catch { setAiError ( '网络错误' ) ; }
100+ finally { setAiGenerating ( false ) ; }
101+ } ;
102+
103+ const handleAiSave = async ( ) => {
104+ if ( ! aiPreview ?. workflow ) return ;
105+ setAiSaving ( true ) ; setAiError ( '' ) ;
106+ try {
107+ const resp = await fetch ( '/api/workflows' , {
108+ method : 'POST' , credentials : 'include' ,
109+ headers : { 'Content-Type' : 'application/json' } ,
110+ body : JSON . stringify ( aiPreview . workflow ) ,
111+ } ) ;
112+ if ( resp . ok ) {
113+ setShowAiGen ( false ) ; setAiPreview ( null ) ; setAiDesc ( '' ) ;
114+ fetchWorkflows ( ) ;
115+ } else {
116+ const data = await resp . json ( ) ;
117+ setAiError ( data . error || '保存失败' ) ;
118+ }
119+ } catch { setAiError ( '网络错误' ) ; }
120+ finally { setAiSaving ( false ) ; }
121+ } ;
122+
123+ const handleAiEdit = ( ) => {
124+ if ( ! aiPreview ?. workflow ) return ;
125+ setEditWorkflow ( aiPreview . workflow ) ;
126+ setShowAiGen ( false ) ; setAiPreview ( null ) ;
127+ setEditing ( true ) ;
72128 } ;
73129
74130 const handleEdit = async ( id : number ) => {
@@ -285,7 +341,70 @@ export default function WorkflowsTab() {
285341 < div className = "workflows-view" >
286342 < div className = "workflows-header" >
287343 < button className = "btn-primary btn-sm" onClick = { handleCreate } > + 新建工作流</ button >
344+ < button className = "btn-primary btn-sm" onClick = { ( ) => { setShowAiGen ( ! showAiGen ) ; setAiPreview ( null ) ; setAiError ( '' ) ; } }
345+ style = { { background : '#8b5cf6' , marginLeft : 6 } } > ✨ AI生成</ button >
288346 </ div >
347+
348+ { /* AI Workflow Generator Panel (v23.0) */ }
349+ { showAiGen && (
350+ < div style = { { margin : '8px 0' , padding : 12 , background : '#faf5ff' , borderRadius : 8 , border : '1px solid #c4b5fd' } } >
351+ < div style = { { fontSize : 13 , fontWeight : 600 , color : '#8b5cf6' , marginBottom : 8 } } > ✨ AI 辅助创建工作流</ div >
352+ { ! aiPreview ? (
353+ < >
354+ < textarea
355+ placeholder = { "用自然语言描述工作流,例如:\n• 先做数据画像,再检查拓扑,最后生成专题图\n• 并行执行生态评估和耕地合规检查,然后合并结果做用地优化\n• 加载遥感影像,计算NDVI,检测植被变化,生成报告" }
356+ rows = { 4 } maxLength = { 2000 }
357+ value = { aiDesc } onChange = { e => setAiDesc ( e . target . value ) }
358+ style = { { width : '100%' , fontSize : 12 , lineHeight : 1.6 , padding : 8 , borderRadius : 6 , border : '1px solid #d1d5db' , resize : 'vertical' } }
359+ />
360+ { aiError && < div style = { { color : '#ef4444' , fontSize : 12 , marginTop : 4 } } > { aiError } </ div > }
361+ < div style = { { display : 'flex' , gap : 6 , marginTop : 8 } } >
362+ < button className = "btn-secondary btn-sm" onClick = { ( ) => setShowAiGen ( false ) } > 取消</ button >
363+ < button className = "btn-primary btn-sm" disabled = { aiGenerating || ! aiDesc . trim ( ) }
364+ onClick = { handleAiGenerate } style = { { background : '#8b5cf6' } } >
365+ { aiGenerating ? '生成中...' : '生成工作流' }
366+ </ button >
367+ </ div >
368+ </ >
369+ ) : (
370+ < >
371+ < div style = { { fontSize : 12 , color : '#6b7280' , marginBottom : 6 } } > AI 已生成以下工作流,请确认或编辑后保存:</ div >
372+ < div style = { { background : '#fff' , borderRadius : 6 , padding : 10 , fontSize : 12 , lineHeight : 1.7 , border : '1px solid #e5e7eb' } } >
373+ < div > < b > 名称:</ b > { aiPreview . workflow ?. workflow_name } </ div >
374+ < div > < b > 描述:</ b > { aiPreview . workflow ?. description } </ div >
375+ < div style = { { marginTop : 6 } } > < b > 步骤 ({ aiPreview . workflow ?. steps ?. length || 0 } ):</ b > </ div >
376+ { ( aiPreview . workflow ?. steps || [ ] ) . map ( ( step : any , i : number ) => (
377+ < div key = { i } style = { { display : 'flex' , alignItems : 'center' , gap : 6 , padding : '4px 0' , fontSize : 11 } } >
378+ < span style = { { width : 20 , height : 20 , borderRadius : '50%' , background : '#8b5cf6' , color : '#fff' ,
379+ display : 'flex' , alignItems : 'center' , justifyContent : 'center' , fontSize : 10 , flexShrink : 0 } } >
380+ { i + 1 }
381+ </ span >
382+ < span style = { { fontWeight : 500 } } > { step . label || step . step_id } </ span >
383+ < span style = { { color : '#6b7280' } } > ({ step . pipeline_type } { step . skill_name ? ` / ${ step . skill_name } ` : '' } )</ span >
384+ { step . depends_on ?. length > 0 && (
385+ < span style = { { color : '#9ca3af' , fontSize : 10 } } > ← { step . depends_on . join ( ', ' ) } </ span >
386+ ) }
387+ </ div >
388+ ) ) }
389+ { aiPreview . explanation && (
390+ < div style = { { marginTop : 8 , padding : 6 , background : '#f8fafc' , borderRadius : 4 , fontSize : 11 , color : '#374151' } } >
391+ { aiPreview . explanation }
392+ </ div >
393+ ) }
394+ </ div >
395+ { aiError && < div style = { { color : '#ef4444' , fontSize : 12 , marginTop : 4 } } > { aiError } </ div > }
396+ < div style = { { display : 'flex' , gap : 6 , marginTop : 8 } } >
397+ < button className = "btn-secondary btn-sm" onClick = { ( ) => { setAiPreview ( null ) ; setAiDesc ( '' ) ; } } > 重新生成</ button >
398+ < button className = "btn-secondary btn-sm" onClick = { handleAiEdit } > 编辑后保存</ button >
399+ < button className = "btn-primary btn-sm" disabled = { aiSaving } onClick = { handleAiSave }
400+ style = { { background : '#8b5cf6' } } >
401+ { aiSaving ? '保存中...' : '直接保存' }
402+ </ button >
403+ </ div >
404+ </ >
405+ ) }
406+ </ div >
407+ ) }
289408 { loading && workflows . length === 0 ? (
290409 < div className = "empty-state" > 加载中...</ div >
291410 ) : workflows . length === 0 ? (
0 commit comments