@@ -4,6 +4,11 @@ import { useNavigate, useSearchParams } from 'react-router-dom';
44import { useDemoFlow } from './DemoCreationFlowContext' ;
55import demoService from '../../services/demoService' ;
66import config from '../../config' ;
7+ import chatService from '../../services/chatService' ; // Import chatService for getProviders
8+ import agentService from '../../services/agentService' ; // Import agentService for getDeployments
9+ import ModelConfigDialog from '../../components/ModelConfigDialog' ; // Reused
10+ import DemoApiSelectionDialog from './DemoApiSelectionDialog' ; // New Component
11+
712import {
813 Box ,
914 Typography ,
@@ -14,10 +19,17 @@ import {
1419 Alert ,
1520 Stack ,
1621 IconButton ,
17- Tooltip
22+ Tooltip ,
23+ Tabs ,
24+ Tab ,
25+ Divider ,
1826} from '@mui/material' ;
1927import PlayArrowIcon from '@mui/icons-material/PlayArrow' ;
2028import SaveIcon from '@mui/icons-material/Save' ;
29+ import SettingsIcon from '@mui/icons-material/Settings' ;
30+ import ApiIcon from '@mui/icons-material/Api' ;
31+ import Chip from '@mui/material/Chip' ;
32+
2133
2234const CanvasScreen = ( ) => {
2335 const {
@@ -42,7 +54,23 @@ const CanvasScreen = () => {
4254 const [ iframeSrcDoc , setIframeSrcDoc ] = useState ( '' ) ;
4355 const [ isEditLoading , setIsEditLoading ] = useState ( false ) ;
4456 const [ editLoaded , setEditLoaded ] = useState ( false ) ;
57+ const [ activeTab , setActiveTab ] = useState ( 'canvas' ) ; // 'canvas' or 'code'
58+
59+ // State for Model Configuration Dialog
60+ const [ isModelConfigOpen , setIsModelConfigOpen ] = useState ( false ) ;
61+ const [ providers , setProviders ] = useState ( { } ) ;
62+ const [ localSelectedProvider , setLocalSelectedProvider ] = useState ( '' ) ; // Local state for dialog
63+ const [ localSelectedModel , setLocalSelectedModel ] = useState ( '' ) ; // Local state for dialog
64+ const [ localModelParamSchema , setLocalModelParamSchema ] = useState ( { } ) ; // Local state for dialog
65+ const [ localCurrentModelParams , setLocalCurrentModelParams ] = useState ( { } ) ; // Local state for dialog
66+ const [ loadingProviders , setLoadingProviders ] = useState ( true ) ;
67+ const [ providersError , setProvidersError ] = useState ( null ) ;
68+
69+ // State for API Selection Dialog
70+ const [ isApiSelectionDialogOpen , setIsApiSelectionDialogOpen ] = useState ( false ) ;
71+
4572
73+ // Effect to load demo for editing
4674 useEffect ( ( ) => {
4775 const editDemoId = searchParams . get ( 'edit' ) ;
4876 if ( editDemoId && ! editLoaded ) {
@@ -51,14 +79,13 @@ const CanvasScreen = () => {
5179 setError ( null ) ;
5280 try {
5381 const demoData = await demoService . getDemo ( editDemoId ) ;
54- // Populate the context
5582 setSelectedApis ( demoData . selectedApis ) ;
5683 setModelConfig ( demoData . modelConfig ) ;
5784 setUserPrompt ( demoData . userPrompt ) ;
5885 setGeneratedCode ( demoData . generatedCode ) ;
5986 setAppName ( demoData . name ) ;
6087 setDescription ( demoData . description ) ;
61- setEditLoaded ( true ) ; // Mark as loaded
88+ setEditLoaded ( true ) ;
6289 } catch ( err ) {
6390 setError ( err . message || 'Failed to load demo for editing.' ) ;
6491 } finally {
@@ -71,12 +98,59 @@ const CanvasScreen = () => {
7198 }
7299 } , [ searchParams , editLoaded , setSelectedApis , setModelConfig , setUserPrompt , setGeneratedCode , setAppName , setDescription ] ) ;
73100
101+ // Redirect if essential context data is missing (not in edit mode)
74102 useEffect ( ( ) => {
75103 if ( ! isEditLoading && ! modelConfig && selectedApis . length === 0 && ! searchParams . get ( 'edit' ) ) {
76104 navigate ( '/create-demo/select-apis' ) ;
77105 }
78106 } , [ modelConfig , selectedApis , navigate , isEditLoading , searchParams ] ) ;
79107
108+ // Effect to fetch providers on component mount and initialize model config dialog's local state
109+ useEffect ( ( ) => {
110+ const fetchProviders = async ( ) => {
111+ setLoadingProviders ( true ) ;
112+ setProvidersError ( null ) ;
113+ try {
114+ const providersData = await chatService . getProviders ( ) ;
115+ setProviders ( providersData ) ;
116+
117+ // Initialize local dialog state with context's modelConfig, or first available model
118+ if ( modelConfig ) {
119+ setLocalSelectedProvider ( modelConfig . provider ) ;
120+ setLocalSelectedModel ( modelConfig . model ) ;
121+ const paramsSchema = providersData [ modelConfig . provider ] ?. models [ modelConfig . model ] ?. parameters || { } ;
122+ setLocalModelParamSchema ( paramsSchema ) ;
123+ setLocalCurrentModelParams ( modelConfig . parameters ) ;
124+ } else {
125+ const providerKeys = Object . keys ( providersData ) ;
126+ if ( providerKeys . length > 0 ) {
127+ const defaultProvider = providerKeys [ 0 ] ;
128+ setLocalSelectedProvider ( defaultProvider ) ;
129+ const models = Object . keys ( providersData [ defaultProvider ] . models || { } ) ;
130+ if ( models . length > 0 ) {
131+ const defaultModel = models [ 0 ] ;
132+ setLocalSelectedModel ( defaultModel ) ;
133+ const modelParams = providersData [ defaultProvider ] . models [ defaultModel ] . parameters ;
134+ setLocalModelParamSchema ( modelParams ) ;
135+ const defaultParams = { } ;
136+ Object . keys ( modelParams ) . forEach ( key => {
137+ defaultParams [ key ] = modelParams [ key ] . default ;
138+ } ) ;
139+ setLocalCurrentModelParams ( defaultParams ) ;
140+ }
141+ }
142+ }
143+ } catch ( err ) {
144+ setProvidersError ( 'Failed to fetch AI providers: ' + err . message ) ;
145+ console . error ( "Error fetching providers for demo creation:" , err ) ;
146+ } finally {
147+ setLoadingProviders ( false ) ;
148+ }
149+ } ;
150+ fetchProviders ( ) ;
151+ } , [ modelConfig ] ) ; // Re-run if modelConfig changes (e.g., loaded from edit)
152+
153+
80154 const generateIframeContent = ( code ) => {
81155 const { html, css, js } = code ;
82156 const apiBaseUrl = config . api . baseUrl ;
@@ -120,13 +194,31 @@ const CanvasScreen = () => {
120194 const handleGenerate = async ( ) => {
121195 setIsLoading ( true ) ;
122196 setError ( null ) ;
197+
198+ if ( ! modelConfig ) {
199+ setError ( 'Please select a model for code generation.' ) ;
200+ setIsLoading ( false ) ;
201+ return ;
202+ }
203+ if ( selectedApis . length === 0 ) {
204+ setError ( 'Please select at least one API for your demo app.' ) ;
205+ setIsLoading ( false ) ;
206+ return ;
207+ }
208+ if ( ! userPrompt . trim ( ) ) {
209+ setError ( 'Please describe your app to generate code.' ) ;
210+ setIsLoading ( false ) ;
211+ return ;
212+ }
213+
123214 try {
124215 const code = await demoService . generateDemoAppCode ( {
125216 userPrompt,
126217 selectedApis,
127218 modelConfig,
128219 } ) ;
129220 setGeneratedCode ( code ) ;
221+ setActiveTab ( 'canvas' ) ; // Switch to canvas view after generation
130222 } catch ( err ) {
131223 setError ( err . message || "Failed to generate code." ) ;
132224 } finally {
@@ -143,15 +235,96 @@ const CanvasScreen = () => {
143235 }
144236 } ;
145237
238+ const handleOpenModelConfig = ( ) => {
239+ // Sync local dialog state with context's modelConfig before opening
240+ if ( modelConfig ) {
241+ setLocalSelectedProvider ( modelConfig . provider ) ;
242+ setLocalSelectedModel ( modelConfig . model ) ;
243+ const paramsSchema = providers [ modelConfig . provider ] ?. models [ modelConfig . model ] ?. parameters || { } ;
244+ setLocalModelParamSchema ( paramsSchema ) ;
245+ setLocalCurrentModelParams ( modelConfig . parameters ) ;
246+ }
247+ setIsModelConfigOpen ( true ) ;
248+ } ;
249+
250+ const handleSaveModelConfig = ( ) => {
251+ setModelConfig ( {
252+ provider : localSelectedProvider ,
253+ model : localSelectedModel ,
254+ parameters : localCurrentModelParams ,
255+ } ) ;
256+ setIsModelConfigOpen ( false ) ;
257+ } ;
258+
259+ const handleSaveSelectedApis = ( apis ) => {
260+ setSelectedApis ( apis ) ;
261+ setIsApiSelectionDialogOpen ( false ) ;
262+ } ;
263+
264+ const handleCodeChange = ( part , value ) => {
265+ setGeneratedCode ( prev => ( { ...prev , [ part ] : value } ) ) ;
266+ } ;
267+
268+ const isModelSelected = localSelectedProvider && localSelectedModel ;
269+
270+ if ( isEditLoading ) {
271+ return (
272+ < Box sx = { { display : 'flex' , justifyContent : 'center' , alignItems : 'center' , height : '100%' } } >
273+ < CircularProgress />
274+ </ Box >
275+ ) ;
276+ }
277+
146278 return (
147279 < Paper sx = { { p : 3 , height : '85vh' , display : 'flex' , flexDirection : 'column' } } >
148280 < Typography variant = "h5" component = "h2" gutterBottom >
149281 Create Demo App
150282 </ Typography >
151283
152- { error && < Alert severity = "error" onClose = { ( ) => setError ( null ) } > { error } </ Alert > }
284+ { error && < Alert severity = "error" onClose = { ( ) => setError ( null ) } sx = { { mb : 2 } } > { error } </ Alert > }
285+
286+ { /* Model Selector at the top */ }
287+ < Box sx = { { display : 'flex' , alignItems : 'center' , justifyContent : 'space-between' , mb : 2 } } >
288+ < Typography variant = "h6" > Demo App Composer Model</ Typography >
289+ < Box sx = { { display : 'flex' , alignItems : 'center' , gap : 1 } } >
290+ { loadingProviders ? (
291+ < CircularProgress size = { 20 } />
292+ ) : isModelSelected ? (
293+ < Chip
294+ label = { `${ localSelectedProvider } /${ localSelectedModel } ` }
295+ color = "primary"
296+ variant = "outlined"
297+ />
298+ ) : (
299+ < Typography color = "text.secondary" > No model selected</ Typography >
300+ ) }
301+ < Button
302+ variant = "outlined"
303+ startIcon = { < SettingsIcon /> }
304+ onClick = { handleOpenModelConfig }
305+ disabled = { loadingProviders || providersError }
306+ >
307+ { isModelSelected ? 'Change Model' : 'Choose Model' }
308+ </ Button >
309+ </ Box >
310+ </ Box >
311+
312+ { /* API/Tools Button */ }
313+ < Box sx = { { mb : 2 } } >
314+ < Button
315+ variant = "outlined"
316+ startIcon = { < ApiIcon /> }
317+ onClick = { ( ) => setIsApiSelectionDialogOpen ( true ) }
318+ disabled = { loadingProviders }
319+ fullWidth
320+ >
321+ Manage APIs for Demo ({ selectedApis . length } selected)
322+ </ Button >
323+ </ Box >
324+
325+ < Divider sx = { { my : 2 } } />
153326
154- < Box sx = { { flexGrow : 1 , display : 'flex' , gap : 2 , mt : 2 , minHeight : 0 } } >
327+ < Box sx = { { flexGrow : 1 , display : 'flex' , gap : 2 , minHeight : 0 } } >
155328 < Box sx = { { flex : 1 , display : 'flex' , flexDirection : 'column' , gap : 2 } } >
156329 < TextField
157330 label = "Describe your app"
@@ -166,7 +339,7 @@ const CanvasScreen = () => {
166339 < Button
167340 variant = "contained"
168341 onClick = { handleGenerate }
169- disabled = { isLoading || ! userPrompt }
342+ disabled = { isLoading || ! userPrompt || ! isModelSelected || selectedApis . length === 0 }
170343 startIcon = { isLoading ? < CircularProgress size = { 20 } /> : < PlayArrowIcon /> }
171344 >
172345 { isLoading ? 'Generating...' : 'Generate' }
@@ -185,17 +358,88 @@ const CanvasScreen = () => {
185358 </ Tooltip >
186359 </ Stack >
187360 </ Box >
188- < Box sx = { { flex : 1 , border : '1px solid grey' , borderRadius : 1 } } >
189- < iframe
190- srcDoc = { iframeSrcDoc }
191- title = "Demo App Preview"
192- sandbox = "allow-scripts allow-forms"
193- width = "100%"
194- height = "100%"
195- style = { { border : 'none' } }
196- />
361+ < Box sx = { { flex : 1 , display : 'flex' , flexDirection : 'column' , border : '1px solid grey' , borderRadius : 1 } } >
362+ < Tabs value = { activeTab } onChange = { ( e , newValue ) => setActiveTab ( newValue ) } aria-label = "canvas-code tabs" sx = { { borderBottom : 1 , borderColor : 'divider' } } >
363+ < Tab value = "canvas" label = "Canvas" />
364+ < Tab value = "code" label = "Code" />
365+ </ Tabs >
366+
367+ < Box sx = { { flexGrow : 1 , p : 1 , overflow : 'auto' } } >
368+ { activeTab === 'canvas' && (
369+ < iframe
370+ srcDoc = { iframeSrcDoc }
371+ title = "Demo App Preview"
372+ sandbox = "allow-scripts allow-forms allow-popups allow-modals allow-same-origin"
373+ width = "100%"
374+ height = "100%"
375+ style = { { border : 'none' } }
376+ />
377+ ) }
378+ { activeTab === 'code' && (
379+ < Stack spacing = { 2 } sx = { { height : '100%' } } >
380+ < TextField
381+ fullWidth
382+ multiline
383+ minRows = { 8 }
384+ label = "HTML"
385+ value = { generatedCode . html }
386+ onChange = { ( e ) => handleCodeChange ( 'html' , e . target . value ) }
387+ InputProps = { {
388+ style : { fontFamily : 'monospace' , fontSize : '0.9rem' }
389+ } }
390+ />
391+ < TextField
392+ fullWidth
393+ multiline
394+ minRows = { 8 }
395+ label = "CSS"
396+ value = { generatedCode . css }
397+ onChange = { ( e ) => handleCodeChange ( 'css' , e . target . value ) }
398+ InputProps = { {
399+ style : { fontFamily : 'monospace' , fontSize : '0.9rem' }
400+ } }
401+ />
402+ < TextField
403+ fullWidth
404+ multiline
405+ minRows = { 8 }
406+ label = "JavaScript"
407+ value = { generatedCode . js }
408+ onChange = { ( e ) => handleCodeChange ( 'js' , e . target . value ) }
409+ InputProps = { {
410+ style : { fontFamily : 'monospace' , fontSize : '0.9rem' }
411+ } }
412+ />
413+ </ Stack >
414+ ) }
415+ </ Box >
197416 </ Box >
198417 </ Box >
418+
419+ < ModelConfigDialog
420+ open = { isModelConfigOpen }
421+ onClose = { ( ) => setIsModelConfigOpen ( false ) }
422+ onSave = { handleSaveModelConfig }
423+ title = "Configure Demo App Composer Model"
424+ providers = { providers }
425+ selectedProvider = { localSelectedProvider }
426+ setSelectedProvider = { setLocalSelectedProvider }
427+ selectedModel = { localSelectedModel }
428+ setSelectedModel = { setLocalSelectedModel }
429+ modelParamSchema = { localModelParamSchema }
430+ setModelParamSchema = { setLocalModelParamSchema }
431+ currentModelParams = { localCurrentModelParams }
432+ setCurrentModelParams = { setLocalCurrentModelParams }
433+ loadingProviders = { loadingProviders }
434+ providersError = { providersError }
435+ />
436+
437+ < DemoApiSelectionDialog
438+ open = { isApiSelectionDialogOpen }
439+ onClose = { ( ) => setIsApiSelectionDialogOpen ( false ) }
440+ existingSelectedApis = { selectedApis }
441+ onSaveSelectedApis = { handleSaveSelectedApis }
442+ />
199443 </ Paper >
200444 ) ;
201445} ;
0 commit comments