/ src / revolve / ui / App.jsx
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  );