Skip to content

Commit 2cba0de

Browse files
authored
Canvas view code and change model (#414)
Signed-off-by: Trevor Grant <[email protected]>
1 parent 0e6c1e0 commit 2cba0de

File tree

2 files changed

+397
-15
lines changed

2 files changed

+397
-15
lines changed

webapp/packages/webui/src/pages/DemoCreationFlow/CanvasScreen.jsx

Lines changed: 259 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ import { useNavigate, useSearchParams } from 'react-router-dom';
44
import { useDemoFlow } from './DemoCreationFlowContext';
55
import demoService from '../../services/demoService';
66
import 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+
712
import {
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';
1927
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
2028
import 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

2234
const 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

Comments
 (0)