App.jsx
1 import React from 'react'; 2 import ReactDOM from 'react-dom/client'; 3 import 'antd/dist/reset.css'; 4 import axios from 'axios'; 5 import { Layout, Button, Typography, Input, Collapse, Row, Col, Space, Divider, List, Spin, Modal, Checkbox 6 } from 'antd'; 7 import { RobotOutlined, UserOutlined, FileTextOutlined, FileMarkdownOutlined, FileOutlined, FileUnknownOutlined, PlaySquareOutlined } from '@ant-design/icons'; 8 import './index.css'; 9 10 import { notification, Badge } from 'antd'; 11 12 import ReactMarkdown from 'react-markdown'; 13 import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; 14 import { oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism'; 15 import { Prompts, Sender } from '@ant-design/x'; 16 import { App as AntApp } from 'antd'; 17 import { Select } from 'antd'; 18 import { 19 BulbOutlined, 20 InfoCircleOutlined, 21 RocketOutlined, 22 SmileOutlined, 23 WarningOutlined, 24 } from '@ant-design/icons'; 25 26 27 const readmeMd = ` 28 29 **Revolve** is an agentic code and editing tool that produces code and tests it. 30 31 ### Getting Started 32 33 1. Configure your database connection. 34 2. Enter a task prompt describing what you want to build (limited to CRUD operations for now). 35 36 ### What Can Revolve Do? 37 38 - Generate CRUD API endpoints 39 - UI which works with the generated APIs. 40 - Automatically write and include test cases for the generated code. 41 - Continuously edit and refine existing code to match evolving requirements. 42 `; 43 44 45 46 const { Header, Sider, Content } = Layout; 47 const { Panel } = Collapse; 48 const { Text } = Typography; 49 50 const App = () => { 51 const [dbConfig, setDbConfig] = React.useState({ 52 DB_NAME: 'newdb', 53 DB_USER: 'postgres', 54 DB_PASSWORD: 'admin', 55 DB_HOST: 'localhost', 56 DB_PORT: '5432', 57 USE_CLONE_DB: true, 58 DB_TYPE: 'postgres', 59 }); 60 61 const [promptItems, setPromptItems] = React.useState([]); 62 63 64 65 React.useEffect(() => { 66 const fetchEnvSettings = async () => { 67 try { 68 const response = await axios.get('/api/env/settings'); 69 if (response.data) { 70 const data = response.data; 71 setSettings((prev) => ({ 72 ...prev, 73 openaiKey: data.OPENAI_API_KEY || '', 74 sourceFolder: data.SOURCE_FOLDER || '', 75 provider: data.PROVIDER || 'openai', 76 baseUrl: data.BASE_URL || '', 77 modelName: data.MODEL_NAME || '', 78 })); 79 } 80 } catch (error) { 81 console.error('Failed to fetch environment settings:', error); 82 } 83 }; 84 85 fetchEnvSettings(); 86 }, []); 87 88 React.useEffect(() => { 89 const fetchDbConfig = async () => { 90 try { 91 const response = await axios.get('/api/env/db'); 92 if (response.data) { 93 setDbConfig((prev) => ({ 94 ...prev, 95 ...response.data, 96 })); 97 } 98 } catch (error) { 99 console.error('Failed to fetch DB config:', error); 100 } 101 }; 102 103 fetchDbConfig(); 104 }, []); 105 106 const dbNameRef = React.useRef(null); 107 const openAiKeyRef = React.useRef(null); 108 const chatInputRef = React.useRef(null); 109 const senderRef = React.useRef(null); 110 111 const [sidePanelKeys, setSidePanelKeys] = React.useState([]); 112 113 const [showServerControls, setShowServerControls] = React.useState(false); 114 115 const [isConfigComplete, setIsConfigComplete] = React.useState(false); 116 const [activePanels, setActivePanels] = React.useState(['1']); 117 const [hasSentMessage, setHasSentMessage] = React.useState(false); 118 119 const getFileIcon = (filename) => { 120 if (filename.endsWith('.py')) return <FileTextOutlined />; 121 if (filename.endsWith('.md')) return <FileMarkdownOutlined />; 122 if (filename.endsWith('.json')) return <FileOutlined />; 123 return <FileUnknownOutlined />; 124 }; 125 const [currentStep, setCurrentStep] = React.useState(0); 126 const [settings, setSettings] = React.useState({ 127 provider: 'openai', 128 openaiKey: '', 129 baseUrl: '', 130 modelName: '', 131 sourceFolder: '', 132 }); 133 const [isDbValid, setIsDbValid] = React.useState(false); 134 const [suggestions, setSuggestions] = React.useState([ 135 'Create CRUD Operations for all the tables', 136 'Generate CRUD Operations for the doctors table', 137 'Run unit tests for all services', 138 'Generate a new service for the satellite and the related tables', 139 ]); 140 141 const handleSuggestionClick = (text) => { 142 setInputValue(text); 143 setSuggestions([]); 144 145 // Automatically send the message after a short delay to ensure state is updated 146 setTimeout(() => { 147 const message = text.trim(); 148 if (message && !isLoading) { 149 handleSendMessage(message); 150 setInputValue(''); 151 } 152 }, 100); 153 }; 154 155 React.useEffect(() => { 156 if (!settings.sourceFolder) return; 157 158 const fetchFileList = async () => { 159 try { 160 const url = `/api/get-file-list?source=${encodeURIComponent(settings.sourceFolder)}`; 161 console.log('📦 Now sending sourceFolder:', settings.sourceFolder); 162 const response = await axios.get(url); 163 setFileList(response.data.files); 164 } catch (err) { 165 console.error('Failed to fetch file list:', err); 166 } 167 }; 168 169 fetchFileList(); 170 const interval = setInterval(fetchFileList, 10000); 171 return () => clearInterval(interval); 172 }, [settings.sourceFolder]); 173 174 const handleFileClick = async (fileName) => { 175 try { 176 const response = await axios.get('/api/get-file', { 177 params: { name: fileName } 178 }); 179 setSelectedFile(fileName); 180 setFileContent(response.data.content); 181 setIsModalOpen(true); 182 } catch (err) { 183 console.error('Failed to fetch file content:', err); 184 } 185 }; 186 const [fileList, setFileList] = React.useState([]); 187 const [selectedFile, setSelectedFile] = React.useState(null); 188 const [fileContent, setFileContent] = React.useState(''); 189 const [isModalOpen, setIsModalOpen] = React.useState(false); 190 191 const [isLoading, setIsLoading] = React.useState(false); 192 193 194 const updateDbField = (key, value) => { 195 setDbConfig((prev) => ({ ...prev, [key]: value })); 196 }; 197 const [systemMessages, setSystemMessages] = React.useState([]); 198 const [inputValue, setInputValue] = React.useState(''); 199 const [serverStatus, setServerStatus] = React.useState('Server is not running'); 200 const [chatMessages, setChatMessages] = React.useState([ 201 { role: 'assistant', content: 'Hello! How can I assist you today?' } 202 ]); 203 204 const handleTestConnection = async () => { 205 try { 206 const response = await axios.post('/api/test_db', dbConfig); 207 const data = response.data; 208 209 notification.success({ 210 message: 'Connection Successful', 211 description: data?.message || 'Database is reachable.' 212 }); 213 214 setIsDbValid(true); 215 216 // If table names are returned, create smart prompts 217 if (Array.isArray(data.tables) && data.tables.length > 1) { 218 const firstTable = data.tables[0]; 219 const secondTable = data.tables[1]; 220 221 setPromptItems([ 222 { 223 key: '1', 224 icon: <RocketOutlined style={{ color: '#722ED1' }} />, 225 label: `Create CRUD for ${firstTable} table`, 226 description: `Generate CRUD endpoints for the ${firstTable} table.`, 227 data: `Create CRUD operations for the ${firstTable} table`, 228 }, 229 { 230 key: '2', 231 icon: <SmileOutlined style={{ color: '#52C41A' }} />, 232 label: `CRUD for all except ${secondTable}`, 233 description: `Generate CRUD excluding the ${secondTable} table.`, 234 data: `Create CRUD operations for all the tables except ${secondTable}`, 235 }, 236 { 237 key: '3', 238 icon: <BulbOutlined style={{ color: '#FFD700' }} />, 239 label: 'CRUD for all tables', 240 description: 'Quickly scaffold all the CRUD endpoints.', 241 data: 'Create CRUD operations for all the tables', 242 }, 243 ]); 244 } 245 246 } catch (err) { 247 notification.error({ 248 message: 'Connection Failed', 249 duration: 0, 250 style: { 251 width: 'auto', // 👈 override the forced width 252 maxWidth: '90vw', // 👈 or whatever you want 253 }, 254 description: ( 255 <div 256 dangerouslySetInnerHTML={{ 257 __html: err.response?.data?.error || '<pre>Unable to reach the database.</pre>' 258 }} 259 style={{ maxHeight: 300, overflowY: 'auto', width:600 }} 260 /> 261 ), 262 }); 263 setIsDbValid(false); 264 } 265 }; 266 267 const handleServerStart = async () => { 268 try { 269 const response = await axios.post('/api/start'); 270 setServerStatus(response.data.message); 271 } catch (error) { 272 console.error('Error starting server:', error); 273 } 274 }; 275 276 const handleServerStop = async () => { 277 try { 278 const response = await axios.post('/api/stop'); 279 setServerStatus(response.data.message); 280 } catch (error) { 281 console.error('Error stopping server:', error); 282 } 283 }; 284 285 const handleSendMessage = async (message) => { 286 if (!message.trim()) return; 287 288 if (!hasSentMessage) { 289 setHasSentMessage(true); 290 setActivePanels((prev) => { 291 const updated = new Set(prev); 292 updated.add('3'); // Expand the "Generated Resources" panel 293 return Array.from(updated); 294 }); 295 } 296 297 const newMessage = { role: 'user', content: message }; 298 const updatedChat = [...chatMessages, newMessage]; 299 setChatMessages(updatedChat); 300 setIsLoading(true); 301 302 try { 303 const response = await fetch('/api/chat', { 304 method: 'POST', 305 headers: { 306 'Content-Type': 'application/json' 307 }, 308 body: JSON.stringify({ 309 messages: updatedChat, 310 dbConfig, 311 settings 312 }) 313 }); 314 315 if (!response.ok) { 316 const errorData = await response.json(); 317 notification.error({ 318 message: 'Failed', 319 description: errorData?.error || `Server error: ${response.status}` 320 }); 321 return; 322 } 323 324 const reader = response.body.getReader(); 325 const decoder = new TextDecoder(); 326 327 while (true) { 328 const { done, value } = await reader.read(); 329 if (done) break; 330 331 const chunk = decoder.decode(value, { stream: true }); 332 const lines = chunk.split('\n').filter(Boolean); 333 334 for (const line of lines) { 335 let parsed; 336 try { 337 parsed = JSON.parse(line); 338 } catch (err) { 339 console.error('Failed to parse line:', line); 340 continue; 341 } 342 343 switch (parsed.level) { 344 case 'system': 345 setSystemMessages(prev => [...prev, { 346 name: parsed.name, 347 text: parsed.text, 348 level: parsed.level 349 }]); 350 break; 351 352 case 'workflow': 353 setChatMessages(prev => [ 354 ...prev, 355 { role: 'assistant', content: parsed.text || '' } 356 ]); 357 break; 358 359 case 'notification': 360 notification.info({ 361 message: parsed.name || 'Notification', 362 description: parsed.text || '', 363 }); 364 break; 365 366 default: 367 console.warn('Unknown message level:', parsed.level); 368 } 369 370 if (parsed.text?.includes('APIs are generated.') && !showServerControls) { 371 setShowServerControls(true); 372 setSidePanelKeys((prev) => { 373 const updated = new Set(prev); 374 updated.add('2'); 375 return Array.from(updated); 376 }); 377 } 378 } 379 } 380 381 } catch (error) { 382 console.error('Error sending message:', error); 383 notification.error({ 384 message: 'Unexpected Error', 385 description: error.message || 'An unknown error occurred.' 386 }); 387 } finally { 388 setIsLoading(false); 389 } 390 }; 391 392 return ( 393 <Layout style={{ minHeight: '100vh' }}> 394 <Sider width={400} style={{ background: '#f0f2f5', padding: '16px' }}> 395 <Collapse activeKey={sidePanelKeys} onChange={setSidePanelKeys}> 396 {showServerControls && ( 397 <Panel header="Server Controls" key="2"> 398 <Button type="primary" block onClick={handleServerStart} style={{ marginBottom: 8 }}> 399 Start 400 </Button> 401 <Button type="primary" danger block onClick={handleServerStop}> 402 Stop 403 </Button> 404 <Divider /> 405 {serverStatus.includes('http') ? ( 406 <Text> 407 External server started at{' '} 408 <Typography.Link 409 href={serverStatus.match(/http:\/\/[^\s]+/)[0]} 410 target="_blank" 411 > 412 {serverStatus.match(/http:\/\/[^\s]+/)[0]} 413 </Typography.Link> 414 </Text> 415 ) : ( 416 <Text>{serverStatus}</Text> 417 )} 418 </Panel> 419 )} 420 <Panel 421 key="1" 422 header={ 423 <span> 424 System Messages{' '} 425 <Badge 426 count={systemMessages.length} 427 style={{ backgroundColor: '#f5222d', marginLeft: 8 }} 428 overflowCount={99} 429 /> 430 </span> 431 } 432 > 433 {systemMessages.length === 0 ? ( 434 <Text>No messages yet...</Text> 435 ) : ( 436 <div style={{ maxHeight: 500, overflowY: 'auto', paddingRight: 8 }}> 437 <List 438 size="small" 439 dataSource={systemMessages} 440 renderItem={(msg, index) => ( 441 <List.Item 442 key={index} 443 style={{ whiteSpace: 'normal', wordBreak: 'break-word' }} 444 > 445 <List.Item.Meta 446 title={<Text strong>{msg.name}</Text>} 447 description={ 448 <div style={{ whiteSpace: 'normal', wordBreak: 'break-word' }}> 449 {msg.text} 450 </div> 451 } 452 /> 453 </List.Item> 454 )} 455 /> 456 </div> 457 )} 458 </Panel> 459 460 </Collapse> 461 </Sider> 462 463 <Layout> 464 {/* <Header style={{ background: '#001529', padding: '0 16px', color: '#fff' }}>Revolve Interface</Header> */} 465 <Content style={{ padding: '16px' }}> 466 <Row gutter={[16, 16]}> 467 <Col span={24}> 468 <Collapse activeKey={activePanels} onChange={(keys) => setActivePanels(keys)}> 469 <Panel header="Readme" key="1"> 470 <ReactMarkdown>{readmeMd}</ReactMarkdown> 471 <div style={{ marginTop: 16, textAlign: 'left' }}> 472 <Button 473 type="primary" 474 onClick={() => { 475 setActivePanels((prev) => { 476 const updated = prev.filter(key => key !== '1'); 477 if (!updated.includes('2')) updated.push('2'); 478 return updated; 479 }); 480 481 // Delay focus to wait for panel render 482 setTimeout(() => { 483 dbNameRef.current?.focus(); 484 }, 300); 485 }} 486 > 487 Next 488 </Button> 489 </div> 490 </Panel> 491 <Panel header="Configuration" key="2"> 492 {currentStep === 0 && ( 493 <> 494 <List> 495 <List.Item> 496 <Text strong style={{ marginRight: 8 }}>DB Type:</Text> 497 <Select 498 value={dbConfig.DB_TYPE} 499 style={{ width: '70%' }} 500 onChange={(value) => updateDbField('DB_TYPE', value)} 501 > 502 <Select.Option value="postgres">Postgres</Select.Option> 503 <Select.Option value="mongodb">MongoDB</Select.Option> 504 </Select> 505 </List.Item> 506 </List> 507 <List 508 dataSource={Object.entries(dbConfig).filter(([key]) => !['USE_CLONE_DB', 'DB_TYPE'].includes(key))} 509 renderItem={([key, value], index) => ( 510 <List.Item> 511 <Text strong style={{ marginRight: 8 }}>{key}:</Text> 512 <Input 513 ref={index === 0 ? dbNameRef : null} 514 style={{ width: '70%' }} 515 value={value} 516 onChange={(e) => updateDbField(key, e.target.value)} 517 /> 518 </List.Item> 519 )} 520 /> 521 522 <List.Item> 523 <Checkbox 524 checked={dbConfig.USE_CLONE_DB} 525 onChange={(e) => updateDbField('USE_CLONE_DB', e.target.checked)} 526 > 527 Enable test mode (It will create a new DB named `{dbConfig.DB_NAME}_test` and use it for testing) 528 </Checkbox> 529 </List.Item> 530 <Divider /> 531 <Button type="primary" onClick={handleTestConnection}> 532 Test Connection 533 </Button> 534 </> 535 )} 536 537 {currentStep === 1 && ( 538 <> 539 <List> 540 <List.Item> 541 <Text strong style={{ marginRight: 8 }}>Provider:</Text> 542 <Select 543 value={settings.provider} 544 style={{ width: '70%' }} 545 onChange={(value) => 546 setSettings((prev) => ({ ...prev, provider: value })) 547 } 548 > 549 <Select.Option value="openai">OpenAI</Select.Option> 550 <Select.Option value="opensource">OpenSource</Select.Option> 551 </Select> 552 </List.Item> 553 554 {settings.provider === 'openai' && ( 555 <List.Item> 556 <Text strong style={{ marginRight: 8 }}>OpenAI Key:</Text> 557 <Input.Password 558 placeholder='Enter your OpenAI API key sk-...' 559 ref={openAiKeyRef} 560 style={{ width: '70%' }} 561 value={settings.openaiKey} 562 onChange={(e) => 563 setSettings((prev) => ({ 564 ...prev, 565 openaiKey: e.target.value, 566 })) 567 } 568 /> 569 </List.Item> 570 )} 571 572 {settings.provider === 'opensource' && ( 573 <> 574 <List.Item> 575 <Text strong style={{ marginRight: 8 }}>Base URL:</Text> 576 577 <Input 578 placeholder="e.g., http://localhost:8000/v1/" 579 style={{ width: '70%' }} 580 value={settings.baseUrl} 581 onChange={(e) => 582 setSettings((prev) => ({ 583 ...prev, 584 baseUrl: e.target.value, 585 })) 586 } 587 /> 588 </List.Item> 589 </> 590 )} 591 <List.Item> 592 <Text strong style={{ marginRight: 8 }}>Model Name:</Text> 593 <Input 594 placeholder="e.g., gpt-4o or hosted_vllm/kramster/evolve-mistral - see for instructions https://huggingface.co/kramster/evolve-mistral/" 595 style={{ width: '70%' }} 596 value={settings.modelName} 597 onChange={(e) => 598 setSettings((prev) => ({ 599 ...prev, 600 modelName: e.target.value, 601 })) 602 } 603 /> 604 </List.Item> 605 <List.Item> 606 <Text strong style={{ marginRight: 8 }}>Source Folder:</Text> 607 <Input 608 placeholder="e.g., /home/user/desktop/generated_code/" 609 style={{ width: '70%' }} 610 value={settings.sourceFolder} 611 onChange={(e) => 612 setSettings((prev) => ({ 613 ...prev, 614 sourceFolder: e.target.value, 615 })) 616 } 617 /> 618 </List.Item> 619 </List> 620 </> 621 )} 622 623 <Divider /> 624 625 <Space> 626 {currentStep > 0 && ( 627 <Button 628 onClick={() => { 629 setCurrentStep(currentStep - 1); 630 setIsConfigComplete(false); // Reset config complete flag when stepping back 631 }} 632 > 633 Previous 634 </Button> 635 )} 636 {currentStep < 1 && ( 637 <Button 638 type="primary" 639 disabled={!isDbValid} 640 onClick={() => { 641 setCurrentStep(currentStep + 1); 642 setTimeout(() => { 643 openAiKeyRef.current?.focus(); 644 }, 300); 645 }} 646 > 647 Next 648 </Button> 649 )} 650 {currentStep === 1 && ( 651 <Button 652 type="primary" 653 disabled={ 654 settings.provider === 'openai' 655 ? !settings.openaiKey 656 : !settings.baseUrl || !settings.modelName 657 } 658 onClick={() => { 659 setIsConfigComplete(true); 660 setActivePanels((prev) => prev.filter((key) => key !== '2')); 661 662 setTimeout(() => { 663 senderRef.current?.focus?.(); 664 }, 300); 665 }} 666 > 667 Finish 668 </Button> 669 )} 670 </Space> 671 </Panel> 672 {hasSentMessage && ( 673 <Panel header="Generated Resources" key="3"> 674 {fileList.length === 0 ? ( 675 <Text type="secondary">No files generated yet.</Text> 676 ) : ( 677 <Row gutter={[16, 16]}> 678 {fileList.map((file) => ( 679 <Col 680 key={file} 681 xs={24} // 1 column on extra small 682 sm={12} // 2 columns on small screens 683 md={8} // 3 columns on medium and up 684 > 685 <div 686 onClick={() => handleFileClick(file)} 687 style={{ 688 padding: '12px', 689 border: '1px solid #f0f0f0', 690 borderRadius: 6, 691 cursor: 'pointer', 692 transition: 'background 0.2s', 693 background: '#fff', 694 }} 695 onMouseEnter={(e) => (e.currentTarget.style.background = '#f5f5f5')} 696 onMouseLeave={(e) => (e.currentTarget.style.background = '#fff')} 697 > 698 {getFileIcon(file)} <Text code>{file}</Text> 699 </div> 700 </Col> 701 ))} 702 </Row> 703 )} 704 705 </Panel>)} 706 </Collapse> 707 </Col> 708 709 {isConfigComplete && ( 710 <> 711 <Col span={24}> 712 <Prompts 713 title="✨ Suggestions" 714 items={promptItems} 715 onItemClick={(info) => { 716 const text = info?.data?.data || info?.data?.label; 717 718 if (typeof text === 'string' && text.trim()) { 719 handleSuggestionClick(text.trim()); 720 } 721 }} 722 /> 723 </Col> 724 725 <Col span={24}> 726 <Spin spinning={isLoading}> 727 <div style={{ background: '#fafafa', padding: '16px', minHeight: 300, overflowY: 'auto', marginBottom: 16 }}> 728 <List 729 dataSource={chatMessages} 730 renderItem={(item) => ( 731 <List.Item> 732 <List.Item.Meta 733 avatar={ 734 item.role === 'user' ? <UserOutlined /> : <RobotOutlined /> 735 } 736 title={item.role === 'user' ? 'You' : 'Assistant'} 737 description={<ReactMarkdown>{item.content}</ReactMarkdown>} 738 /> 739 </List.Item> 740 )} 741 /> 742 </div> 743 </Spin> 744 <Sender 745 ref={senderRef} 746 disabled={isLoading} 747 value={inputValue} 748 onChange={setInputValue} 749 onSubmit={(value) => { 750 const message = value?.trim(); 751 if (message && !isLoading) { 752 handleSendMessage(message); 753 setInputValue(''); 754 } 755 }} 756 /> 757 </Col></> 758 )} 759 760 </Row> 761 </Content> 762 763 </Layout> 764 <Modal 765 title={selectedFile} 766 open={isModalOpen} 767 onCancel={() => setIsModalOpen(false)} 768 footer={null} 769 width={1200} 770 > 771 <div style={{ maxHeight: '60vh', overflowY: 'auto' }}> 772 <ReactMarkdown 773 components={{ 774 code({ node, inline, className, children, ...props }) { 775 const match = /language-(\w+)/.exec(className || ''); 776 return !inline && match ? ( 777 <SyntaxHighlighter 778 style={oneDark} 779 language={match[1]} 780 PreTag="div" 781 {...props} 782 > 783 {String(children).replace(/\n$/, '')} 784 </SyntaxHighlighter> 785 ) : ( 786 <code className={className} {...props}> 787 {children} 788 </code> 789 ); 790 }, 791 }} 792 > 793 {fileContent} 794 </ReactMarkdown> 795 </div> 796 </Modal> 797 </Layout> 798 ); 799 }; 800 801 ReactDOM.createRoot(document.getElementById('root')).render( 802 <AntApp> 803 <App /> 804 </AntApp> 805 );