Spaces:
Running
Running
Commit ·
e3e0a75
1
Parent(s): 12db32d
commit initial 12-12-2025 0001
Browse files- README.md +1 -1
- package-lock.json +0 -0
- package.json +7 -1
- public/index.html +3 -2
- src/App.css +485 -21
- src/App.js +623 -17
- src/Terminal.js +137 -0
- src/agent/assistant.js +12 -0
- src/agent/projectGenerator.js +22 -0
- src/agent/runner.js +30 -0
- src/apiClient.js +7 -0
- src/fileStore.js +211 -0
- src/problemParser.js +32 -0
- src/zipExport.js +25 -0
README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
---
|
| 2 |
-
title: CodeIDE
|
| 3 |
emoji: 🐠
|
| 4 |
colorFrom: indigo
|
| 5 |
colorTo: red
|
|
|
|
| 1 |
---
|
| 2 |
+
title: CodeIDE
|
| 3 |
emoji: 🐠
|
| 4 |
colorFrom: indigo
|
| 5 |
colorTo: red
|
package-lock.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
package.json
CHANGED
|
@@ -3,14 +3,20 @@
|
|
| 3 |
"version": "0.1.0",
|
| 4 |
"private": true,
|
| 5 |
"dependencies": {
|
|
|
|
| 6 |
"@testing-library/dom": "^10.4.0",
|
| 7 |
"@testing-library/jest-dom": "^6.6.3",
|
| 8 |
"@testing-library/react": "^16.3.0",
|
| 9 |
"@testing-library/user-event": "^13.5.0",
|
|
|
|
|
|
|
| 10 |
"react": "^19.1.0",
|
| 11 |
"react-dom": "^19.1.0",
|
|
|
|
| 12 |
"react-scripts": "5.0.1",
|
| 13 |
-
"web-vitals": "^2.1.4"
|
|
|
|
|
|
|
| 14 |
},
|
| 15 |
"scripts": {
|
| 16 |
"start": "react-scripts start",
|
|
|
|
| 3 |
"version": "0.1.0",
|
| 4 |
"private": true,
|
| 5 |
"dependencies": {
|
| 6 |
+
"@monaco-editor/react": "^4.7.0",
|
| 7 |
"@testing-library/dom": "^10.4.0",
|
| 8 |
"@testing-library/jest-dom": "^6.6.3",
|
| 9 |
"@testing-library/react": "^16.3.0",
|
| 10 |
"@testing-library/user-event": "^13.5.0",
|
| 11 |
+
"axios": "^1.13.2",
|
| 12 |
+
"jszip": "^3.10.1",
|
| 13 |
"react": "^19.1.0",
|
| 14 |
"react-dom": "^19.1.0",
|
| 15 |
+
"react-hot-toast": "^2.6.0",
|
| 16 |
"react-scripts": "5.0.1",
|
| 17 |
+
"web-vitals": "^2.1.4",
|
| 18 |
+
"xterm": "^5.3.0",
|
| 19 |
+
"xterm-addon-fit": "^0.8.0"
|
| 20 |
},
|
| 21 |
"scripts": {
|
| 22 |
"start": "react-scripts start",
|
public/index.html
CHANGED
|
@@ -7,7 +7,8 @@
|
|
| 7 |
<meta name="theme-color" content="#000000" />
|
| 8 |
<meta
|
| 9 |
name="description"
|
| 10 |
-
content="
|
|
|
|
| 11 |
/>
|
| 12 |
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
| 13 |
<!--
|
|
@@ -24,7 +25,7 @@
|
|
| 24 |
work correctly both with client-side routing and a non-root public URL.
|
| 25 |
Learn how to configure a non-root public URL by running `npm run build`.
|
| 26 |
-->
|
| 27 |
-
<title>
|
| 28 |
</head>
|
| 29 |
<body>
|
| 30 |
<noscript>You need to enable JavaScript to run this app.</noscript>
|
|
|
|
| 7 |
<meta name="theme-color" content="#000000" />
|
| 8 |
<meta
|
| 9 |
name="description"
|
| 10 |
+
content="DevMate AI
|
| 11 |
+
An AI-powered code editor and IDE that helps you write, debug, and optimize code faster."
|
| 12 |
/>
|
| 13 |
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
| 14 |
<!--
|
|
|
|
| 25 |
work correctly both with client-side routing and a non-root public URL.
|
| 26 |
Learn how to configure a non-root public URL by running `npm run build`.
|
| 27 |
-->
|
| 28 |
+
<title>DevMate IDE</title>
|
| 29 |
</head>
|
| 30 |
<body>
|
| 31 |
<noscript>You need to enable JavaScript to run this app.</noscript>
|
src/App.css
CHANGED
|
@@ -1,38 +1,502 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
.App {
|
| 2 |
text-align: center;
|
| 3 |
}
|
| 4 |
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
}
|
| 9 |
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
}
|
| 15 |
|
| 16 |
-
.
|
| 17 |
-
background
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
display: flex;
|
| 20 |
-
flex-direction: column;
|
| 21 |
align-items: center;
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
}
|
| 26 |
|
| 27 |
-
.
|
| 28 |
-
|
| 29 |
}
|
| 30 |
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
}
|
| 35 |
-
|
| 36 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
}
|
|
|
|
| 1 |
+
/* ============================
|
| 2 |
+
Global & Base
|
| 3 |
+
============================ */
|
| 4 |
+
|
| 5 |
+
html, body, #root {
|
| 6 |
+
margin: 0;
|
| 7 |
+
padding: 0;
|
| 8 |
+
overflow-x: hidden; /* prevent page-level horizontal scroll */
|
| 9 |
+
box-sizing: border-box;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
*,
|
| 13 |
+
*::before,
|
| 14 |
+
*::after {
|
| 15 |
+
box-sizing: inherit;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
.App {
|
| 19 |
text-align: center;
|
| 20 |
}
|
| 21 |
|
| 22 |
+
/* Root layout */
|
| 23 |
+
.ide-root {
|
| 24 |
+
height: 100vh;
|
| 25 |
+
width: 100vw;
|
| 26 |
+
display: flex;
|
| 27 |
+
flex-direction: column;
|
| 28 |
+
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
| 29 |
+
overflow: hidden;
|
| 30 |
+
max-width: 100%;
|
| 31 |
}
|
| 32 |
|
| 33 |
+
/* Themes */
|
| 34 |
+
.ide-dark {
|
| 35 |
+
background: #1e1e1e;
|
| 36 |
+
color: #ddd;
|
| 37 |
}
|
| 38 |
|
| 39 |
+
.ide-light {
|
| 40 |
+
background: #f5f5f5;
|
| 41 |
+
color: #222;
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
/* ============================
|
| 45 |
+
Menubar
|
| 46 |
+
============================ */
|
| 47 |
+
|
| 48 |
+
.ide-menubar {
|
| 49 |
+
height: 32px;
|
| 50 |
+
display: flex;
|
| 51 |
+
align-items: center;
|
| 52 |
+
justify-content: space-between;
|
| 53 |
+
padding: 0 10px;
|
| 54 |
+
background: #252526;
|
| 55 |
+
color: #eee;
|
| 56 |
+
font-size: 13px;
|
| 57 |
+
border-bottom: 1px solid #444;
|
| 58 |
+
flex: 0 0 auto;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
.ide-menubar-left,
|
| 62 |
+
.ide-menubar-right {
|
| 63 |
display: flex;
|
|
|
|
| 64 |
align-items: center;
|
| 65 |
+
gap: 8px;
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
.ide-logo {
|
| 69 |
+
font-weight: 600;
|
| 70 |
+
margin-right: 12px;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
.ide-menubar button {
|
| 74 |
+
margin-right: 4px;
|
| 75 |
+
background: transparent;
|
| 76 |
+
border: none;
|
| 77 |
+
color: inherit;
|
| 78 |
+
cursor: pointer;
|
| 79 |
+
padding: 2px 6px;
|
| 80 |
+
border-radius: 3px;
|
| 81 |
+
font-size: 13px;
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
.ide-menubar button:hover {
|
| 85 |
+
background: rgba(255, 255, 255, 0.08);
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
/* ============================
|
| 89 |
+
Body Layout
|
| 90 |
+
============================ */
|
| 91 |
+
|
| 92 |
+
.ide-body {
|
| 93 |
+
flex: 1;
|
| 94 |
+
display: flex;
|
| 95 |
+
overflow: hidden; /* prevent children from creating page overflow */
|
| 96 |
+
min-height: 0; /* allow children to shrink (important for flex) */
|
| 97 |
+
max-width: 100%;
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
/* ============================
|
| 101 |
+
Sidebar (Explorer)
|
| 102 |
+
============================ */
|
| 103 |
+
|
| 104 |
+
.ide-sidebar {
|
| 105 |
+
flex: 0 0 210px; /* fixed sidebar width */
|
| 106 |
+
min-width: 0; /* critical for preventing overflow from child */
|
| 107 |
+
width: 210px;
|
| 108 |
+
background: #252526;
|
| 109 |
+
color: #ccc;
|
| 110 |
+
padding: 5px;
|
| 111 |
+
border-right: 1px solid #444;
|
| 112 |
+
overflow-y: auto;
|
| 113 |
+
font-size: 14px;
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
.tree-item {
|
| 117 |
+
padding: 4px 6px;
|
| 118 |
+
cursor: pointer;
|
| 119 |
+
user-select: none;
|
| 120 |
+
border-radius: 3px;
|
| 121 |
+
white-space: nowrap;
|
| 122 |
+
overflow: hidden;
|
| 123 |
+
text-overflow: ellipsis;
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
.tree-item:hover {
|
| 127 |
+
background: #3a3d41;
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
/* ============================
|
| 131 |
+
Main (Editor + Output)
|
| 132 |
+
============================ */
|
| 133 |
+
|
| 134 |
+
.ide-main {
|
| 135 |
+
flex: 1 1 auto;
|
| 136 |
+
display: flex;
|
| 137 |
+
flex-direction: column;
|
| 138 |
+
position: relative;
|
| 139 |
+
min-width: 0; /* CRITICAL: allow editor to shrink inside flex */
|
| 140 |
+
overflow: hidden;
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
/* Monaco wrapper */
|
| 144 |
+
.ide-editor-wrapper {
|
| 145 |
+
flex: 1 1 auto;
|
| 146 |
+
min-height: 0;
|
| 147 |
+
min-width: 0; /* important for monaco inside flex */
|
| 148 |
+
overflow: hidden;
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
/* Bottom panels (output, stdin, problems) */
|
| 152 |
+
.ide-panels {
|
| 153 |
+
background: #1e1e1e;
|
| 154 |
+
border-top: 1px solid #444;
|
| 155 |
+
padding: 6px;
|
| 156 |
+
font-size: 13px;
|
| 157 |
+
height: 28%;
|
| 158 |
+
overflow: auto;
|
| 159 |
+
min-height: 120px;
|
| 160 |
+
box-sizing: border-box;
|
| 161 |
+
max-width: 100%;
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
.ide-output {
|
| 165 |
+
background: #000;
|
| 166 |
+
color: #0f0;
|
| 167 |
+
padding: 6px;
|
| 168 |
+
min-height: 60px;
|
| 169 |
+
max-height: 120px;
|
| 170 |
+
overflow-y: auto;
|
| 171 |
+
border-radius: 4px;
|
| 172 |
+
font-family: "Consolas", monospace;
|
| 173 |
+
font-size: 12px;
|
| 174 |
+
white-space: pre-wrap;
|
| 175 |
+
word-break: break-word;
|
| 176 |
+
max-width: 100%;
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
.ide-input-box {
|
| 180 |
+
width: 100%;
|
| 181 |
+
margin-top: 4px;
|
| 182 |
+
padding: 6px;
|
| 183 |
+
background: #111;
|
| 184 |
+
border: 1px solid #444;
|
| 185 |
+
color: #eee;
|
| 186 |
+
border-radius: 3px;
|
| 187 |
+
font-size: 12px;
|
| 188 |
+
box-sizing: border-box;
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
/* Problems panel */
|
| 192 |
+
.ide-problems-panel {
|
| 193 |
+
margin-top: 6px;
|
| 194 |
+
padding: 8px;
|
| 195 |
+
background: #3c0000;
|
| 196 |
+
color: #fff;
|
| 197 |
+
border-left: 3px solid red;
|
| 198 |
+
font-size: 12px;
|
| 199 |
+
border-radius: 3px;
|
| 200 |
+
overflow: auto;
|
| 201 |
+
white-space: pre-wrap;
|
| 202 |
+
word-break: break-word;
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
/* ============================
|
| 206 |
+
Terminal / XTerm container
|
| 207 |
+
============================ */
|
| 208 |
+
|
| 209 |
+
/* Ensure terminal fits container and doesn't push layout */
|
| 210 |
+
#terminal-container {
|
| 211 |
+
width: 100% !important;
|
| 212 |
+
max-width: 100% !important;
|
| 213 |
+
box-sizing: border-box;
|
| 214 |
+
overflow-x: auto; /* keep horizontal scroll inside terminal only */
|
| 215 |
+
height: 180px;
|
| 216 |
+
background: #1e1e1e;
|
| 217 |
+
border-top: 1px solid #333;
|
| 218 |
+
border-radius: 4px;
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
/* Terminal content wrapper (if you have a wrapper class) */
|
| 222 |
+
.terminal-content {
|
| 223 |
+
width: 100%;
|
| 224 |
+
max-width: 100%;
|
| 225 |
+
overflow-x: auto;
|
| 226 |
+
white-space: pre-wrap;
|
| 227 |
+
word-break: break-word;
|
| 228 |
+
font-family: "Consolas", monospace;
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
/* ============================
|
| 232 |
+
Right AI Panel
|
| 233 |
+
============================ */
|
| 234 |
+
|
| 235 |
+
.ide-right-panel {
|
| 236 |
+
flex: 0 0 280px;
|
| 237 |
+
min-width: 0;
|
| 238 |
+
width: 280px;
|
| 239 |
+
border-left: 1px solid #444;
|
| 240 |
+
background: #252526;
|
| 241 |
+
padding: 8px;
|
| 242 |
+
display: flex;
|
| 243 |
+
flex-direction: column;
|
| 244 |
+
gap: 8px;
|
| 245 |
+
font-size: 13px;
|
| 246 |
+
overflow: auto;
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
.ide-ai-header {
|
| 250 |
+
font-weight: 600;
|
| 251 |
+
margin-bottom: 4px;
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
.ide-ai-section {
|
| 255 |
+
display: flex;
|
| 256 |
+
flex-direction: column;
|
| 257 |
+
gap: 4px;
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
.ide-ai-label {
|
| 261 |
+
font-size: 12px;
|
| 262 |
+
text-transform: uppercase;
|
| 263 |
+
letter-spacing: 0.06em;
|
| 264 |
+
opacity: 0.8;
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
/* AI instruction textarea */
|
| 268 |
+
.ide-agent-textarea {
|
| 269 |
+
width: 100%;
|
| 270 |
+
min-height: 80px;
|
| 271 |
+
max-height: 160px;
|
| 272 |
+
resize: vertical;
|
| 273 |
+
padding: 6px;
|
| 274 |
+
border-radius: 4px;
|
| 275 |
+
border: 1px solid #444;
|
| 276 |
+
background: #1e1e1e;
|
| 277 |
+
color: #eee;
|
| 278 |
+
font-family: Consolas, monospace;
|
| 279 |
+
font-size: 12px;
|
| 280 |
+
box-sizing: border-box;
|
| 281 |
+
}
|
| 282 |
+
|
| 283 |
+
/* AI buttons */
|
| 284 |
+
.ide-ai-buttons {
|
| 285 |
+
display: flex;
|
| 286 |
+
gap: 6px;
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
+
.ide-ai-buttons button {
|
| 290 |
+
flex: 1;
|
| 291 |
+
padding: 6px;
|
| 292 |
+
font-size: 12px;
|
| 293 |
+
border-radius: 4px;
|
| 294 |
+
border: 1px solid #555;
|
| 295 |
+
background: #0e639c;
|
| 296 |
+
color: #fff;
|
| 297 |
+
cursor: pointer;
|
| 298 |
+
}
|
| 299 |
+
|
| 300 |
+
.ide-ai-buttons button:hover {
|
| 301 |
+
background: #1177bb;
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
/* AI explanation box */
|
| 305 |
+
.ide-explain {
|
| 306 |
+
max-height: 180px;
|
| 307 |
+
overflow-y: auto;
|
| 308 |
+
padding: 6px;
|
| 309 |
+
border-radius: 4px;
|
| 310 |
+
background: #1e1e1e;
|
| 311 |
+
border: 1px solid #444;
|
| 312 |
+
font-size: 12px;
|
| 313 |
+
color: #eee;
|
| 314 |
+
font-family: Consolas, monospace;
|
| 315 |
+
}
|
| 316 |
+
|
| 317 |
+
/* ============================
|
| 318 |
+
AI Suggestions Popup
|
| 319 |
+
============================ */
|
| 320 |
+
|
| 321 |
+
.ai-popup {
|
| 322 |
+
position: absolute;
|
| 323 |
+
bottom: 35%;
|
| 324 |
+
right: 5px;
|
| 325 |
+
width: 230px;
|
| 326 |
+
background: #333;
|
| 327 |
+
border: 1px solid #444;
|
| 328 |
+
border-radius: 6px;
|
| 329 |
+
padding: 4px;
|
| 330 |
+
animation: fadeIn 0.25s ease-in-out;
|
| 331 |
+
font-size: 12px;
|
| 332 |
+
z-index: 200; /* ensure it sits above other UI */
|
| 333 |
+
max-width: calc(100vw - 24px);
|
| 334 |
+
box-sizing: border-box;
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
.ai-suggest {
|
| 338 |
+
padding: 4px;
|
| 339 |
+
cursor: pointer;
|
| 340 |
}
|
| 341 |
|
| 342 |
+
.ai-suggest:hover {
|
| 343 |
+
background: #444;
|
| 344 |
}
|
| 345 |
|
| 346 |
+
/* ============================
|
| 347 |
+
Search Dialog
|
| 348 |
+
============================ */
|
| 349 |
+
|
| 350 |
+
.search-dialog {
|
| 351 |
+
position: absolute;
|
| 352 |
+
right: 10px;
|
| 353 |
+
top: 50px;
|
| 354 |
+
background: #333;
|
| 355 |
+
padding: 10px;
|
| 356 |
+
border: 1px solid #444;
|
| 357 |
+
border-radius: 5px;
|
| 358 |
+
z-index: 300;
|
| 359 |
+
max-width: calc(100vw - 24px);
|
| 360 |
+
box-sizing: border-box;
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
.search-dialog input {
|
| 364 |
+
width: 200px;
|
| 365 |
+
padding: 4px 6px;
|
| 366 |
+
border-radius: 3px;
|
| 367 |
+
border: 1px solid #555;
|
| 368 |
+
background: #1e1e1e;
|
| 369 |
+
color: #eee;
|
| 370 |
+
}
|
| 371 |
+
|
| 372 |
+
/* ============================
|
| 373 |
+
Context Menu
|
| 374 |
+
============================ */
|
| 375 |
+
|
| 376 |
+
.ide-context-menu {
|
| 377 |
+
position: absolute;
|
| 378 |
+
background: #333;
|
| 379 |
+
color: #eee;
|
| 380 |
+
border: 1px solid #555;
|
| 381 |
+
border-radius: 5px;
|
| 382 |
+
padding: 4px;
|
| 383 |
+
z-index: 400;
|
| 384 |
+
max-width: calc(100vw - 24px);
|
| 385 |
+
box-sizing: border-box;
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
+
.ide-context-menu div {
|
| 389 |
+
padding: 4px 8px;
|
| 390 |
+
cursor: pointer;
|
| 391 |
+
font-size: 13px;
|
| 392 |
+
}
|
| 393 |
+
|
| 394 |
+
.ide-context-menu div:hover {
|
| 395 |
+
background: #555;
|
| 396 |
+
}
|
| 397 |
+
|
| 398 |
+
/* ============================
|
| 399 |
+
Progress / Spinner
|
| 400 |
+
============================ */
|
| 401 |
+
|
| 402 |
+
.ide-progress-wrap {
|
| 403 |
+
height: 3px;
|
| 404 |
+
background: transparent;
|
| 405 |
+
width: 100%;
|
| 406 |
+
position: relative;
|
| 407 |
+
overflow: hidden;
|
| 408 |
+
}
|
| 409 |
+
.ide-progress {
|
| 410 |
+
position: absolute;
|
| 411 |
+
height: 3px;
|
| 412 |
+
width: 30%;
|
| 413 |
+
left: -30%;
|
| 414 |
+
top: 0;
|
| 415 |
+
background: linear-gradient(90deg, #0e9, #08f);
|
| 416 |
+
animation: progress-slide 1.2s linear infinite;
|
| 417 |
+
border-radius: 2px;
|
| 418 |
+
}
|
| 419 |
+
|
| 420 |
+
@keyframes progress-slide {
|
| 421 |
+
0% { left: -30%; width: 30%; }
|
| 422 |
+
50% { left: 35%; width: 40%; }
|
| 423 |
+
100% { left: 100%; width: 30%; }
|
| 424 |
+
}
|
| 425 |
+
|
| 426 |
+
/* ---------- small inline spinner (optional) ---------- */
|
| 427 |
+
.button-spinner {
|
| 428 |
+
display: inline-block;
|
| 429 |
+
width: 12px;
|
| 430 |
+
height: 12px;
|
| 431 |
+
border: 2px solid rgba(255,255,255,0.25);
|
| 432 |
+
border-top-color: rgba(255,255,255,0.95);
|
| 433 |
+
border-radius: 50%;
|
| 434 |
+
animation: spin 0.8s linear infinite;
|
| 435 |
+
margin-left: 6px;
|
| 436 |
+
vertical-align: middle;
|
| 437 |
+
}
|
| 438 |
+
|
| 439 |
+
@keyframes spin {
|
| 440 |
+
to { transform: rotate(360deg); }
|
| 441 |
+
}
|
| 442 |
+
|
| 443 |
+
/* ============================
|
| 444 |
+
Misc: Text wrapping & safety
|
| 445 |
+
============================ */
|
| 446 |
+
|
| 447 |
+
.ide-output, .ide-explain, .ide-problems-panel, .tree-item, .ai-popup, .ai-suggest {
|
| 448 |
+
overflow-wrap: break-word;
|
| 449 |
+
word-break: break-word;
|
| 450 |
+
white-space: pre-wrap;
|
| 451 |
+
}
|
| 452 |
+
|
| 453 |
+
/* Buttons, inputs, selects must not exceed container */
|
| 454 |
+
button, input, textarea, select {
|
| 455 |
+
max-width: 100%;
|
| 456 |
+
box-sizing: border-box;
|
| 457 |
+
}
|
| 458 |
+
|
| 459 |
+
/* Final safety: hide any stray horizontal overflow */
|
| 460 |
+
#root, .ide-root, .ide-body, .ide-main { overflow-x: hidden; }
|
| 461 |
+
|
| 462 |
+
/* ============================
|
| 463 |
+
📱 Mobile / Tablet Responsive
|
| 464 |
+
============================ */
|
| 465 |
+
|
| 466 |
+
@media (max-width: 900px) {
|
| 467 |
+
/* Stack layout vertically on small screens */
|
| 468 |
+
.ide-body { flex-direction: column; }
|
| 469 |
+
|
| 470 |
+
/* Sidebar becomes top strip */
|
| 471 |
+
.ide-sidebar {
|
| 472 |
+
width: 100%;
|
| 473 |
+
flex: 0 0 auto;
|
| 474 |
+
max-height: 22vh;
|
| 475 |
+
border-right: none;
|
| 476 |
+
border-bottom: 1px solid #444;
|
| 477 |
}
|
| 478 |
+
|
| 479 |
+
.ide-main { order: 2; min-height: 40vh; }
|
| 480 |
+
.ide-editor-wrapper { height: 40vh; }
|
| 481 |
+
|
| 482 |
+
/* Bottom panels adapt */
|
| 483 |
+
.ide-panels { height: auto; max-height: 26vh; }
|
| 484 |
+
|
| 485 |
+
/* Right panel becomes bottom full width */
|
| 486 |
+
.ide-right-panel {
|
| 487 |
+
width: 100%;
|
| 488 |
+
order: 3;
|
| 489 |
+
border-left: none;
|
| 490 |
+
border-top: 1px solid #444;
|
| 491 |
}
|
| 492 |
+
|
| 493 |
+
.ai-popup { right: 8px; left: 8px; width: auto; max-width: calc(100vw - 16px); }
|
| 494 |
+
.search-dialog { right: 8px; left: 8px; width: auto; }
|
| 495 |
+
.ide-context-menu { max-width: 70vw; }
|
| 496 |
+
}
|
| 497 |
+
|
| 498 |
+
/* Small-screen tweak for very small devices */
|
| 499 |
+
@media (max-width: 420px) {
|
| 500 |
+
.ide-menubar { font-size: 12px; padding: 0 6px; }
|
| 501 |
+
.ide-ai-buttons button, .ide-menubar button { padding: 4px; font-size: 12px; }
|
| 502 |
}
|
src/App.js
CHANGED
|
@@ -1,23 +1,629 @@
|
|
| 1 |
-
|
| 2 |
-
import
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
function App() {
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
>
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
</div>
|
| 22 |
);
|
| 23 |
}
|
|
|
|
| 1 |
+
// src/App.js
|
| 2 |
+
import { useState, useEffect, useRef } from "react";
|
| 3 |
+
import Editor from "@monaco-editor/react";
|
| 4 |
+
import { askAgent } from "./agent/assistant";
|
| 5 |
+
import { runCode } from "./agent/runner";
|
| 6 |
+
import {
|
| 7 |
+
loadTree,
|
| 8 |
+
saveTree,
|
| 9 |
+
addFile,
|
| 10 |
+
addFolder,
|
| 11 |
+
renameNode,
|
| 12 |
+
deleteNode,
|
| 13 |
+
getNodeByPath,
|
| 14 |
+
updateFileContent,
|
| 15 |
+
searchTree,
|
| 16 |
+
} from "./fileStore";
|
| 17 |
+
import { downloadProjectZip } from "./zipExport";
|
| 18 |
+
import { parseProblems } from "./problemParser";
|
| 19 |
+
import "./App.css";
|
| 20 |
+
import "xterm/css/xterm.css";
|
| 21 |
+
import XTerm from "./Terminal"; // your existing wrapper
|
| 22 |
|
| 23 |
+
// =================== SUPPORTED LANGUAGES ===================
|
| 24 |
+
const LANGUAGE_OPTIONS = [
|
| 25 |
+
{ id: "python", ext: ".py", icon: "🐍", monaco: "python" },
|
| 26 |
+
{ id: "javascript", ext: ".js", icon: "🟨", monaco: "javascript" },
|
| 27 |
+
{ id: "typescript", ext: ".ts", icon: "🟦", monaco: "typescript" },
|
| 28 |
+
{ id: "cpp", ext: ".cpp", icon: "💠", monaco: "cpp" },
|
| 29 |
+
{ id: "c", ext: ".c", icon: "🔷", monaco: "c" },
|
| 30 |
+
{ id: "java", ext: ".java", icon: "☕", monaco: "java" },
|
| 31 |
+
{ id: "html", ext: ".html", icon: "🌐", monaco: "html" },
|
| 32 |
+
{ id: "css", ext: ".css", icon: "🎨", monaco: "css" },
|
| 33 |
+
{ id: "json", ext: ".json", icon: "🧾", monaco: "json" },
|
| 34 |
+
];
|
| 35 |
+
|
| 36 |
+
const RUNNABLE_LANGS = ["python", "javascript", "java"];
|
| 37 |
+
|
| 38 |
+
// =================== Heuristics ===================
|
| 39 |
+
// patterns that indicate program is waiting for input
|
| 40 |
+
function outputLooksForInput(output) {
|
| 41 |
+
if (!output) return false;
|
| 42 |
+
const o = output.toString();
|
| 43 |
+
const patterns = [
|
| 44 |
+
/enter.*:/i,
|
| 45 |
+
/input.*:/i,
|
| 46 |
+
/please enter/i,
|
| 47 |
+
/scanner/i,
|
| 48 |
+
/press enter/i,
|
| 49 |
+
/: $/,
|
| 50 |
+
/:\n$/,
|
| 51 |
+
/> $/,
|
| 52 |
+
/awaiting input/i,
|
| 53 |
+
/provide input/i,
|
| 54 |
+
/stdin/i,
|
| 55 |
+
/enter a value/i,
|
| 56 |
+
];
|
| 57 |
+
return patterns.some((p) => p.test(o));
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
// code-level heuristics to detect input calls
|
| 61 |
+
function codeNeedsInput(code, langId) {
|
| 62 |
+
if (!code) return false;
|
| 63 |
+
try {
|
| 64 |
+
const c = code.toString();
|
| 65 |
+
if (langId === "python") {
|
| 66 |
+
if (/\binput\s*\(/i.test(c)) return true;
|
| 67 |
+
if (/\bsys\.stdin\.(read|readline|readlines)\s*\(/i.test(c)) return true;
|
| 68 |
+
if (/\braw_input\s*\(/i.test(c)) return true;
|
| 69 |
+
}
|
| 70 |
+
if (langId === "java") {
|
| 71 |
+
if (/\bScanner\s*\(/i.test(c)) return true;
|
| 72 |
+
if (/\bBufferedReader\b.*readLine/i.test(c)) return true;
|
| 73 |
+
if (/\bSystem\.console\(\)/i.test(c)) return true;
|
| 74 |
+
if (/\bnext(Int|Line|Double|)\b/i.test(c)) return true;
|
| 75 |
+
}
|
| 76 |
+
if (langId === "javascript") {
|
| 77 |
+
if (/process\.stdin|readline|readlineSync|prompt\(|require\(['"]readline['"]\)/i.test(c)) return true;
|
| 78 |
+
}
|
| 79 |
+
if (langId === "cpp" || langId === "c") {
|
| 80 |
+
if (/\bscanf\s*\(/i.test(c)) return true;
|
| 81 |
+
if (/\bstd::cin\b|cin\s*>>/i.test(c)) return true;
|
| 82 |
+
if (/\bgets?\s*\(/i.test(c)) return true;
|
| 83 |
+
}
|
| 84 |
+
if (/\binput\b|\bscanf\b|\bscanf_s\b|\bcin\b|\bScanner\b|readLine|readline/i.test(c)) return true;
|
| 85 |
+
return false;
|
| 86 |
+
} catch {
|
| 87 |
+
return false;
|
| 88 |
+
}
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
// Helper: focus xterm's hidden textarea (works with xterm.js default markup)
|
| 92 |
+
function focusXtermHelper() {
|
| 93 |
+
setTimeout(() => {
|
| 94 |
+
const ta = document.querySelector("#terminal-container .xterm-helper-textarea");
|
| 95 |
+
if (ta) {
|
| 96 |
+
try {
|
| 97 |
+
ta.focus();
|
| 98 |
+
const len = ta.value?.length ?? 0;
|
| 99 |
+
ta.setSelectionRange(len, len);
|
| 100 |
+
} catch {}
|
| 101 |
+
} else {
|
| 102 |
+
const cont = document.getElementById("terminal-container");
|
| 103 |
+
if (cont) cont.focus();
|
| 104 |
+
}
|
| 105 |
+
}, 120);
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
// =================== APP ===================
|
| 109 |
function App() {
|
| 110 |
+
// ----- file tree + selection -----
|
| 111 |
+
const [tree, setTree] = useState(loadTree());
|
| 112 |
+
const [activePath, setActivePath] = useState("main.py");
|
| 113 |
+
|
| 114 |
+
// ----- terminal / interactive state -----
|
| 115 |
+
const [accumStdin, setAccumStdin] = useState(""); // accumulated input for interactive runs
|
| 116 |
+
const [awaitingInput, setAwaitingInput] = useState(false);
|
| 117 |
+
const [terminalLines, setTerminalLines] = useState([]); // visible lines in terminal area
|
| 118 |
+
const [output, setOutput] = useState(""); // "write" prop for XTerm (Terminal component picks this up)
|
| 119 |
+
const [interactivePromptShown, setInteractivePromptShown] = useState(false);
|
| 120 |
+
|
| 121 |
+
// ----- AI + editor state -----
|
| 122 |
+
const [prompt, setPrompt] = useState("");
|
| 123 |
+
const [explanation, setExplanation] = useState("");
|
| 124 |
+
const [problems, setProblems] = useState([]);
|
| 125 |
+
const [theme, setTheme] = useState("vs-dark");
|
| 126 |
+
const [searchOpen, setSearchOpen] = useState(false);
|
| 127 |
+
const [searchQuery, setSearchQuery] = useState("");
|
| 128 |
+
const [aiSuggestions, setAiSuggestions] = useState([]);
|
| 129 |
+
const [contextMenu, setContextMenu] = useState(null);
|
| 130 |
+
const [isRunning, setIsRunning] = useState(false);
|
| 131 |
+
const [isFixing, setIsFixing] = useState(false);
|
| 132 |
+
const [isExplaining, setIsExplaining] = useState(false);
|
| 133 |
+
|
| 134 |
+
// refs & helpers
|
| 135 |
+
const editorRef = useRef(null);
|
| 136 |
+
const fileInputRef = useRef(null);
|
| 137 |
+
|
| 138 |
+
useEffect(() => {
|
| 139 |
+
saveTree(tree);
|
| 140 |
+
}, [tree]);
|
| 141 |
+
|
| 142 |
+
const currentNode = getNodeByPath(tree, activePath);
|
| 143 |
+
const langMeta =
|
| 144 |
+
LANGUAGE_OPTIONS.find((l) => currentNode?.name?.endsWith(l.ext)) ||
|
| 145 |
+
LANGUAGE_OPTIONS[0];
|
| 146 |
+
|
| 147 |
+
// ---------- File / Folder actions ----------
|
| 148 |
+
const collectFolderPaths = (node, acc = []) => {
|
| 149 |
+
if (!node) return acc;
|
| 150 |
+
if (node.type === "folder") acc.push(node.path || "");
|
| 151 |
+
node.children?.forEach((c) => collectFolderPaths(c, acc));
|
| 152 |
+
return acc;
|
| 153 |
+
};
|
| 154 |
+
|
| 155 |
+
const handleNewFile = () => {
|
| 156 |
+
const filename = window.prompt("Filename (with extension):", "untitled.js");
|
| 157 |
+
if (!filename) return;
|
| 158 |
+
const selected = getNodeByPath(tree, activePath);
|
| 159 |
+
let parentPath = "";
|
| 160 |
+
if (selected?.type === "folder") parentPath = selected.path;
|
| 161 |
+
else if (selected?.type === "file") {
|
| 162 |
+
const parts = selected.path.split("/").slice(0, -1);
|
| 163 |
+
parentPath = parts.join("/");
|
| 164 |
+
}
|
| 165 |
+
const folders = collectFolderPaths(tree);
|
| 166 |
+
const suggestion = parentPath || folders[0] || "";
|
| 167 |
+
const chosen = window.prompt(
|
| 168 |
+
`Parent folder (enter path). Available:\n${folders.join("\n")}\n\nLeave empty for root.`,
|
| 169 |
+
suggestion
|
| 170 |
+
);
|
| 171 |
+
const targetParent = chosen == null ? parentPath : (chosen.trim() || "");
|
| 172 |
+
const updated = addFile(tree, filename, targetParent);
|
| 173 |
+
setTree(updated);
|
| 174 |
+
const newPath = (targetParent ? targetParent + "/" : "") + filename;
|
| 175 |
+
setActivePath(newPath);
|
| 176 |
+
};
|
| 177 |
+
|
| 178 |
+
const handleNewFolder = () => {
|
| 179 |
+
const name = window.prompt("Folder name:", "new_folder");
|
| 180 |
+
if (!name) return;
|
| 181 |
+
const selected = getNodeByPath(tree, activePath);
|
| 182 |
+
const parentPath = selected && selected.type === "folder" ? selected.path : "";
|
| 183 |
+
const updated = addFolder(tree, name, parentPath);
|
| 184 |
+
setTree(updated);
|
| 185 |
+
};
|
| 186 |
+
|
| 187 |
+
const handleRename = () => {
|
| 188 |
+
if (!activePath) return;
|
| 189 |
+
const node = getNodeByPath(tree, activePath);
|
| 190 |
+
if (!node) return;
|
| 191 |
+
const newName = window.prompt("New name:", node.name);
|
| 192 |
+
if (!newName || newName === node.name) return;
|
| 193 |
+
const updated = renameNode(tree, activePath, newName);
|
| 194 |
+
setTree(updated);
|
| 195 |
+
const parts = activePath.split("/");
|
| 196 |
+
parts.pop();
|
| 197 |
+
const parent = parts.join("/");
|
| 198 |
+
const newPath = (parent ? parent + "/" : "") + newName;
|
| 199 |
+
setActivePath(newPath);
|
| 200 |
+
};
|
| 201 |
+
|
| 202 |
+
const handleDelete = () => {
|
| 203 |
+
if (!activePath) return;
|
| 204 |
+
const node = getNodeByPath(tree, activePath);
|
| 205 |
+
if (!node) return;
|
| 206 |
+
if (node.type === "folder" && node.children?.length > 0) {
|
| 207 |
+
const ok = window.confirm(`Folder "${node.name}" has ${node.children.length} items. Delete anyway?`);
|
| 208 |
+
if (!ok) return;
|
| 209 |
+
} else {
|
| 210 |
+
const ok = window.confirm(`Delete "${node.name}"?`);
|
| 211 |
+
if (!ok) return;
|
| 212 |
+
}
|
| 213 |
+
const updated = deleteNode(tree, activePath);
|
| 214 |
+
setTree(updated);
|
| 215 |
+
setActivePath("");
|
| 216 |
+
};
|
| 217 |
+
|
| 218 |
+
const downloadFile = () => {
|
| 219 |
+
const node = getNodeByPath(tree, activePath);
|
| 220 |
+
if (!node || node.type !== "file") return;
|
| 221 |
+
const blob = new Blob([node.content || ""], { type: "text/plain;charset=utf-8" });
|
| 222 |
+
const a = document.createElement("a");
|
| 223 |
+
a.href = URL.createObjectURL(blob);
|
| 224 |
+
a.download = node.name;
|
| 225 |
+
a.click();
|
| 226 |
+
};
|
| 227 |
+
|
| 228 |
+
const handleImportFileClick = () => fileInputRef.current?.click();
|
| 229 |
+
const handleFileInputChange = async (e) => {
|
| 230 |
+
const f = e.target.files?.[0];
|
| 231 |
+
if (!f) return;
|
| 232 |
+
const text = await f.text();
|
| 233 |
+
const selected = getNodeByPath(tree, activePath);
|
| 234 |
+
let parentPath = "";
|
| 235 |
+
if (selected?.type === "folder") parentPath = selected.path;
|
| 236 |
+
else if (selected?.type === "file") parentPath = selected.path.split("/").slice(0, -1).join("");
|
| 237 |
+
const updated = addFile(tree, f.name, parentPath);
|
| 238 |
+
const newPath = (parentPath ? parentPath + "/" : "") + f.name;
|
| 239 |
+
const finalTree = updateFileContent(updated, newPath, text);
|
| 240 |
+
setTree(finalTree);
|
| 241 |
+
setActivePath(newPath);
|
| 242 |
+
e.target.value = "";
|
| 243 |
+
};
|
| 244 |
+
|
| 245 |
+
// ---------- Terminal helpers ----------
|
| 246 |
+
const appendTerminal = (text) => {
|
| 247 |
+
// push to visible lines and set `output` (which Terminal writes)
|
| 248 |
+
setTerminalLines((prev) => {
|
| 249 |
+
const next = [...prev, text];
|
| 250 |
+
// also keep the XTerm single-output prop to trigger Terminal.writeln
|
| 251 |
+
setOutput(text);
|
| 252 |
+
return next;
|
| 253 |
+
});
|
| 254 |
+
};
|
| 255 |
+
|
| 256 |
+
const clearTerminal = () => {
|
| 257 |
+
// ANSI sequence to clear screen + move cursor home (xterm will honor)
|
| 258 |
+
setTerminalLines([]);
|
| 259 |
+
setOutput("\x1b[2J\x1b[H");
|
| 260 |
+
setAccumStdin("");
|
| 261 |
+
setAwaitingInput(false);
|
| 262 |
+
setInteractivePromptShown(false);
|
| 263 |
+
};
|
| 264 |
+
|
| 265 |
+
const resetTerminal = (keepAccum = false) => {
|
| 266 |
+
setTerminalLines([]);
|
| 267 |
+
setOutput("");
|
| 268 |
+
if (!keepAccum) {
|
| 269 |
+
setAccumStdin("");
|
| 270 |
+
}
|
| 271 |
+
setAwaitingInput(false);
|
| 272 |
+
setInteractivePromptShown(false);
|
| 273 |
+
};
|
| 274 |
+
|
| 275 |
+
// Unified runner used when terminal provides input (or small input)
|
| 276 |
+
const runCodeWithUpdatedInput = async (inputLine) => {
|
| 277 |
+
if (typeof inputLine !== "string") inputLine = String(inputLine || "");
|
| 278 |
+
const trimmed = inputLine.replace(/\r$/, "");
|
| 279 |
+
// if user pressed Enter with empty line and no accum, ignore
|
| 280 |
+
if (trimmed.length === 0 && !accumStdin) {
|
| 281 |
+
return;
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
// append newline like console
|
| 285 |
+
const newAccum = (accumStdin || "") + trimmed + "\n";
|
| 286 |
+
setAccumStdin(newAccum);
|
| 287 |
+
setInteractivePromptShown(false);
|
| 288 |
+
|
| 289 |
+
const node = getNodeByPath(tree, activePath);
|
| 290 |
+
if (!node || node.type !== "file") {
|
| 291 |
+
appendTerminal("[Error] No file selected to run.");
|
| 292 |
+
setAwaitingInput(false);
|
| 293 |
+
return;
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
+
const selectedLang = LANGUAGE_OPTIONS.find((l) => node.name.endsWith(l.ext))?.id;
|
| 297 |
+
if (!selectedLang || !RUNNABLE_LANGS.includes(selectedLang)) {
|
| 298 |
+
appendTerminal(`[Error] Run not supported for ${node.name}`);
|
| 299 |
+
setAwaitingInput(false);
|
| 300 |
+
return;
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
setIsRunning(true);
|
| 304 |
+
try {
|
| 305 |
+
const res = await runCode(node.content, selectedLang, newAccum);
|
| 306 |
+
const out = res.output ?? "";
|
| 307 |
+
if (out) appendTerminal(out);
|
| 308 |
+
setProblems(res.error ? parseProblems(res.output) : []);
|
| 309 |
+
if (outputLooksForInput(out)) {
|
| 310 |
+
setAwaitingInput(true);
|
| 311 |
+
focusXtermHelper();
|
| 312 |
+
} else {
|
| 313 |
+
setAwaitingInput(false);
|
| 314 |
+
setAccumStdin(""); // finished -> clear accumulated input so next Run is fresh
|
| 315 |
+
}
|
| 316 |
+
} catch (err) {
|
| 317 |
+
appendTerminal(String(err));
|
| 318 |
+
setAwaitingInput(true);
|
| 319 |
+
} finally {
|
| 320 |
+
setIsRunning(false);
|
| 321 |
+
}
|
| 322 |
+
};
|
| 323 |
+
|
| 324 |
+
// ---------- Initial Run handler (fresh runs) ----------
|
| 325 |
+
const handleRun = async () => {
|
| 326 |
+
const node = getNodeByPath(tree, activePath);
|
| 327 |
+
if (!node || node.type !== "file") {
|
| 328 |
+
appendTerminal("Select a file to run.");
|
| 329 |
+
return;
|
| 330 |
+
}
|
| 331 |
+
|
| 332 |
+
const selectedLang = LANGUAGE_OPTIONS.find((l) => node.name.endsWith(l.ext))?.id;
|
| 333 |
+
if (!selectedLang || !RUNNABLE_LANGS.includes(selectedLang)) {
|
| 334 |
+
appendTerminal(`⚠️ Run not supported for this file type.`);
|
| 335 |
+
return;
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
// Force fresh run: clear accumulated input and terminal
|
| 339 |
+
setAccumStdin("");
|
| 340 |
+
resetTerminal(false);
|
| 341 |
+
setAwaitingInput(false);
|
| 342 |
+
setInteractivePromptShown(false);
|
| 343 |
+
|
| 344 |
+
const needs = codeNeedsInput(node.content, selectedLang);
|
| 345 |
+
|
| 346 |
+
if (needs) {
|
| 347 |
+
appendTerminal("[Interactive program detected — type input directly into the terminal]");
|
| 348 |
+
setAwaitingInput(true);
|
| 349 |
+
setInteractivePromptShown(true);
|
| 350 |
+
focusXtermHelper();
|
| 351 |
+
return; // wait for user's input to avoid EOFError
|
| 352 |
+
}
|
| 353 |
+
|
| 354 |
+
// Non-interactive: run immediately with empty stdin
|
| 355 |
+
appendTerminal(`[Running (fresh)]`);
|
| 356 |
+
setIsRunning(true);
|
| 357 |
+
setProblems([]);
|
| 358 |
+
try {
|
| 359 |
+
const res = await runCode(node.content, selectedLang, "");
|
| 360 |
+
const out = res.output ?? "";
|
| 361 |
+
if (out) appendTerminal(out);
|
| 362 |
+
setProblems(res.error ? parseProblems(res.output) : []);
|
| 363 |
+
if (outputLooksForInput(out)) {
|
| 364 |
+
setAwaitingInput(true);
|
| 365 |
+
focusXtermHelper();
|
| 366 |
+
} else {
|
| 367 |
+
setAwaitingInput(false);
|
| 368 |
+
setAccumStdin("");
|
| 369 |
+
}
|
| 370 |
+
} catch (err) {
|
| 371 |
+
appendTerminal(String(err));
|
| 372 |
+
setAwaitingInput(true);
|
| 373 |
+
focusXtermHelper();
|
| 374 |
+
} finally {
|
| 375 |
+
setIsRunning(false);
|
| 376 |
+
}
|
| 377 |
+
};
|
| 378 |
+
|
| 379 |
+
// ---------- Agent functions ----------
|
| 380 |
+
const handleAskFix = async () => {
|
| 381 |
+
const node = getNodeByPath(tree, activePath);
|
| 382 |
+
if (!node || node.type !== "file") {
|
| 383 |
+
appendTerminal("Select a file to apply fix.");
|
| 384 |
+
return;
|
| 385 |
+
}
|
| 386 |
+
setIsFixing(true);
|
| 387 |
+
try {
|
| 388 |
+
const userHint = prompt.trim() ? `User request: ${prompt}` : "";
|
| 389 |
+
const reply = await askAgent(
|
| 390 |
+
`Improve, debug, or refactor this ${LANGUAGE_OPTIONS.find((l) => node.name.endsWith(l.ext))?.id || "file"} file.\n${userHint}\nReturn ONLY updated code, no explanation.\n\nCODE:\n${node.content}`
|
| 391 |
+
);
|
| 392 |
+
const updatedTree = updateFileContent(tree, node.path, reply);
|
| 393 |
+
setTree(updatedTree);
|
| 394 |
+
appendTerminal("[AI] Applied fixes to file.");
|
| 395 |
+
} catch (err) {
|
| 396 |
+
appendTerminal(String(err));
|
| 397 |
+
} finally {
|
| 398 |
+
setIsFixing(false);
|
| 399 |
+
}
|
| 400 |
+
};
|
| 401 |
+
|
| 402 |
+
const handleExplainSelection = async () => {
|
| 403 |
+
const node = getNodeByPath(tree, activePath);
|
| 404 |
+
if (!node || node.type !== "file") {
|
| 405 |
+
setExplanation("Select a file to explain.");
|
| 406 |
+
return;
|
| 407 |
+
}
|
| 408 |
+
setIsExplaining(true);
|
| 409 |
+
try {
|
| 410 |
+
const editor = editorRef.current;
|
| 411 |
+
let selectedCode = "";
|
| 412 |
+
try {
|
| 413 |
+
selectedCode = editor?.getModel()?.getValueInRange(editor.getSelection()) || "";
|
| 414 |
+
} catch {}
|
| 415 |
+
const code = selectedCode.trim() || node.content;
|
| 416 |
+
const userHint = prompt.trim() ? `Focus on: ${prompt}` : "Give a clear and simple explanation.";
|
| 417 |
+
const reply = await askAgent(
|
| 418 |
+
`Explain what this code does, any risks, and improvements.\n${userHint}\n\nCODE:\n${code}`
|
| 419 |
+
);
|
| 420 |
+
setExplanation(reply);
|
| 421 |
+
} catch (err) {
|
| 422 |
+
setExplanation(String(err));
|
| 423 |
+
} finally {
|
| 424 |
+
setIsExplaining(false);
|
| 425 |
+
}
|
| 426 |
+
};
|
| 427 |
+
|
| 428 |
+
// AI suggestions for continuation
|
| 429 |
+
const fetchAiSuggestions = async (code) => {
|
| 430 |
+
if (!code?.trim()) return;
|
| 431 |
+
try {
|
| 432 |
+
const reply = await askAgent(`Suggest possible next lines for continuation. Return 3 short snippets.\n${code}`);
|
| 433 |
+
setAiSuggestions(reply.split("\n").filter((l) => l.trim()));
|
| 434 |
+
} catch {
|
| 435 |
+
// ignore
|
| 436 |
+
}
|
| 437 |
+
};
|
| 438 |
+
|
| 439 |
+
// ---------- Search ----------
|
| 440 |
+
const handleSearchToggle = () => setSearchOpen(!searchOpen);
|
| 441 |
+
const handleSearchNow = () => {
|
| 442 |
+
if (!searchQuery) return;
|
| 443 |
+
const results = searchTree(tree, searchQuery);
|
| 444 |
+
alert(`Found ${results.length} results:\n` + JSON.stringify(results, null, 2));
|
| 445 |
+
};
|
| 446 |
+
|
| 447 |
+
// Editor change
|
| 448 |
+
const updateActiveFileContent = (value) => {
|
| 449 |
+
const node = getNodeByPath(tree, activePath);
|
| 450 |
+
if (!node) return;
|
| 451 |
+
const updated = updateFileContent(tree, activePath, value ?? "");
|
| 452 |
+
setTree(updated);
|
| 453 |
+
};
|
| 454 |
+
|
| 455 |
+
// Render tree
|
| 456 |
+
const renderTree = (node, depth = 0) => {
|
| 457 |
+
const isActive = node.path === activePath;
|
| 458 |
+
return (
|
| 459 |
+
<div key={node.path || node.name} style={{ paddingLeft: depth * 10 }}>
|
| 460 |
+
<div
|
| 461 |
+
className={`tree-item ${node.type} ${isActive ? "ide-file-item-active" : ""}`}
|
| 462 |
+
onClick={() => setActivePath(node.path)}
|
| 463 |
+
onContextMenu={(e) => {
|
| 464 |
+
e.preventDefault();
|
| 465 |
+
setActivePath(node.path);
|
| 466 |
+
setContextMenu({ x: e.pageX, y: e.pageY, file: node.path });
|
| 467 |
+
}}
|
| 468 |
+
style={{ display: "flex", alignItems: "center", gap: 8 }}
|
| 469 |
>
|
| 470 |
+
<span style={{ width: 18 }}>{node.type === "folder" ? "📁" : "📄"}</span>
|
| 471 |
+
<span className="ide-file-name">{node.name}</span>
|
| 472 |
+
</div>
|
| 473 |
+
|
| 474 |
+
{node.children && node.children.map((c) => renderTree(c, depth + 1))}
|
| 475 |
+
</div>
|
| 476 |
+
);
|
| 477 |
+
};
|
| 478 |
+
|
| 479 |
+
const anyLoading = isRunning || isFixing || isExplaining;
|
| 480 |
+
|
| 481 |
+
// ---------- JSX ----------
|
| 482 |
+
return (
|
| 483 |
+
<div
|
| 484 |
+
className={`ide-root ${theme === "vs-dark" ? "ide-dark" : "ide-light"}`}
|
| 485 |
+
style={{ overflowX: "hidden" }} // prevent horizontal white gap / scroller issue
|
| 486 |
+
>
|
| 487 |
+
<input ref={fileInputRef} id="file-import-input" type="file" style={{ display: "none" }} onChange={handleFileInputChange} />
|
| 488 |
+
|
| 489 |
+
<div className="ide-menubar">
|
| 490 |
+
<div className="ide-menubar-left">
|
| 491 |
+
<span className="ide-logo">⚙️ DevMate IDE</span>
|
| 492 |
+
|
| 493 |
+
<button onClick={handleNewFile} disabled={anyLoading}>📄 New File</button>
|
| 494 |
+
<button onClick={handleNewFolder} disabled={anyLoading}>📁 New Folder</button>
|
| 495 |
+
<button onClick={handleRename} disabled={anyLoading}>✏️ Rename</button>
|
| 496 |
+
<button onClick={handleDelete} disabled={anyLoading}>🗑 Delete</button>
|
| 497 |
+
<button onClick={downloadFile} disabled={anyLoading}>📥 Download</button>
|
| 498 |
+
<button onClick={() => downloadProjectZip()} disabled={anyLoading}>📦 ZIP</button>
|
| 499 |
+
<button onClick={handleImportFileClick} disabled={anyLoading}>📤 Import File</button>
|
| 500 |
+
</div>
|
| 501 |
+
|
| 502 |
+
<div className="ide-menubar-right">
|
| 503 |
+
<button onClick={handleSearchToggle} disabled={anyLoading}>🔍 Search</button>
|
| 504 |
+
<button onClick={handleRun} disabled={isRunning || anyLoading}>{isRunning ? "⏳ Running..." : "▶ Run"}</button>
|
| 505 |
+
<button onClick={handleAskFix} disabled={isFixing || anyLoading}>{isFixing ? "⏳ Fixing..." : "🤖 Fix"}</button>
|
| 506 |
+
<button onClick={handleExplainSelection} disabled={isExplaining || anyLoading}>{isExplaining ? "⏳ Explaining" : "📖 Explain"}</button>
|
| 507 |
+
<button onClick={() => setTheme((t) => (t === "vs-dark" ? "light" : "vs-dark"))} disabled={anyLoading}>
|
| 508 |
+
{theme === "vs-dark" ? "☀️" : "🌙"}
|
| 509 |
+
</button>
|
| 510 |
+
<button onClick={clearTerminal} disabled={anyLoading} title="Clear terminal">🧹 Clear</button>
|
| 511 |
+
</div>
|
| 512 |
+
</div>
|
| 513 |
+
|
| 514 |
+
{(isRunning || isFixing || isExplaining) && (
|
| 515 |
+
<div className="ide-progress-wrap">
|
| 516 |
+
<div className="ide-progress" />
|
| 517 |
+
</div>
|
| 518 |
+
)}
|
| 519 |
+
|
| 520 |
+
<div className="ide-body">
|
| 521 |
+
<div className="ide-sidebar">
|
| 522 |
+
<div className="ide-sidebar-header">
|
| 523 |
+
<span>EXPLORER</span>
|
| 524 |
+
<button className="ide-icon-button" onClick={handleNewFile} title="New File" disabled={anyLoading}>+</button>
|
| 525 |
+
</div>
|
| 526 |
+
<div className="ide-file-list" style={{ padding: 6 }}>{renderTree(tree)}</div>
|
| 527 |
+
</div>
|
| 528 |
+
|
| 529 |
+
<div className="ide-main">
|
| 530 |
+
<div className="ide-editor-wrapper">
|
| 531 |
+
<Editor
|
| 532 |
+
height="100%"
|
| 533 |
+
theme={theme}
|
| 534 |
+
language={langMeta.monaco}
|
| 535 |
+
value={currentNode?.content || ""}
|
| 536 |
+
onChange={updateActiveFileContent}
|
| 537 |
+
onMount={(editor) => (editorRef.current = editor)}
|
| 538 |
+
onBlur={() => fetchAiSuggestions(currentNode?.content)}
|
| 539 |
+
options={{ minimap: { enabled: true }, fontSize: 14, scrollBeyondLastLine: false }}
|
| 540 |
+
/>
|
| 541 |
+
</div>
|
| 542 |
+
|
| 543 |
+
{aiSuggestions.length > 0 && (
|
| 544 |
+
<div className="ai-popup">
|
| 545 |
+
{aiSuggestions.map((s, i) => (
|
| 546 |
+
<div key={i} className="ai-suggest" onClick={() => updateFileContent(tree, activePath, (currentNode?.content || "") + "\n" + s)}>{s}</div>
|
| 547 |
+
))}
|
| 548 |
+
</div>
|
| 549 |
+
)}
|
| 550 |
+
|
| 551 |
+
<div className="ide-panels">
|
| 552 |
+
<div style={{ marginBottom: 8 }}>
|
| 553 |
+
<div style={{ fontSize: 12, color: "#ccc", marginBottom: 6 }}>Terminal</div>
|
| 554 |
+
|
| 555 |
+
<XTerm
|
| 556 |
+
output={output}
|
| 557 |
+
onData={(line) => {
|
| 558 |
+
// line is the typed content (no CR), pass to unified runner
|
| 559 |
+
const trimmed = (line || "").replace(/\r$/, "");
|
| 560 |
+
if (!trimmed && !awaitingInput) {
|
| 561 |
+
// nothing typed and not expecting input
|
| 562 |
+
return;
|
| 563 |
+
}
|
| 564 |
+
runCodeWithUpdatedInput(trimmed);
|
| 565 |
+
}}
|
| 566 |
+
/>
|
| 567 |
+
|
| 568 |
+
{awaitingInput && (
|
| 569 |
+
<div style={{ marginTop: 8, padding: 8, background: "#252526", border: "1px solid #333", borderRadius: 6 }}>
|
| 570 |
+
<div style={{ marginBottom: 6, color: "#ddd" }}>
|
| 571 |
+
This program appears to be interactive and requires console input.
|
| 572 |
+
</div>
|
| 573 |
+
<div style={{ display: "flex", gap: 8 }}>
|
| 574 |
+
<button onClick={() => { focusXtermHelper(); }} className="ide-button">▶ Focus Terminal</button>
|
| 575 |
+
<button onClick={() => { resetTerminal(true); appendTerminal("[Interactive session started — type into the terminal]"); setAwaitingInput(true); focusXtermHelper(); }} className="ide-button">▶ Start interactive session</button>
|
| 576 |
+
<div style={{ color: "#999", alignSelf: "center" }}>Type & press Enter in terminal.</div>
|
| 577 |
+
</div>
|
| 578 |
+
</div>
|
| 579 |
+
)}
|
| 580 |
+
</div>
|
| 581 |
+
|
| 582 |
+
{problems.length > 0 && (
|
| 583 |
+
<div className="ide-problems-panel">
|
| 584 |
+
<div>🚨 Problems ({problems.length})</div>
|
| 585 |
+
{problems.map((p, i) => <div key={i}>{p.path}:{p.line} — {p.message}</div>)}
|
| 586 |
+
</div>
|
| 587 |
+
)}
|
| 588 |
+
</div>
|
| 589 |
+
</div>
|
| 590 |
+
|
| 591 |
+
<div className="ide-right-panel">
|
| 592 |
+
<div className="ide-ai-header">🤖 AI Assistant</div>
|
| 593 |
+
|
| 594 |
+
<div className="ide-ai-section">
|
| 595 |
+
<label className="ide-ai-label">Instruction</label>
|
| 596 |
+
<textarea className="ide-agent-textarea" placeholder="Ask the AI (optimize, add tests, convert, etc.)" value={prompt} onChange={(e) => setPrompt(e.target.value)} />
|
| 597 |
+
</div>
|
| 598 |
+
|
| 599 |
+
<div className="ide-ai-buttons">
|
| 600 |
+
<button onClick={handleAskFix} disabled={isFixing || anyLoading}>{isFixing ? "⏳ Apply Fix" : "💡 Apply Fix"}</button>
|
| 601 |
+
<button onClick={handleExplainSelection} disabled={isExplaining || anyLoading}>{isExplaining ? "⏳ Explaining" : "📖 Explain Code"}</button>
|
| 602 |
+
</div>
|
| 603 |
+
|
| 604 |
+
{explanation && (
|
| 605 |
+
<div className="ide-ai-section">
|
| 606 |
+
<label className="ide-ai-label">Explanation</label>
|
| 607 |
+
<div className="ide-explain">{explanation}</div>
|
| 608 |
+
</div>
|
| 609 |
+
)}
|
| 610 |
+
</div>
|
| 611 |
+
</div>
|
| 612 |
+
|
| 613 |
+
{searchOpen && (
|
| 614 |
+
<div className="search-dialog">
|
| 615 |
+
<input placeholder="Search text..." onChange={(e) => setSearchQuery(e.target.value)} />
|
| 616 |
+
<button onClick={handleSearchNow}>Search</button>
|
| 617 |
+
</div>
|
| 618 |
+
)}
|
| 619 |
+
|
| 620 |
+
{contextMenu && (
|
| 621 |
+
<div className="ide-context-menu" style={{ top: contextMenu.y, left: contextMenu.x }} onMouseLeave={() => setContextMenu(null)}>
|
| 622 |
+
<div onClick={() => { setContextMenu(null); handleRename(); }}>✏️ Rename</div>
|
| 623 |
+
<div onClick={() => { setContextMenu(null); handleDelete(); }}>🗑 Delete</div>
|
| 624 |
+
<div onClick={() => { setContextMenu(null); downloadFile(); }}>📥 Download</div>
|
| 625 |
+
</div>
|
| 626 |
+
)}
|
| 627 |
</div>
|
| 628 |
);
|
| 629 |
}
|
src/Terminal.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// src/Terminal.js
|
| 2 |
+
import { useEffect, useRef } from "react";
|
| 3 |
+
import { Terminal } from "xterm";
|
| 4 |
+
import { FitAddon } from "xterm-addon-fit";
|
| 5 |
+
import "xterm/css/xterm.css";
|
| 6 |
+
|
| 7 |
+
/**
|
| 8 |
+
* Props:
|
| 9 |
+
* - onData(line: string) -> called when user presses Enter with the typed line (no trailing \r)
|
| 10 |
+
* - output (string) -> append output to the terminal when it changes
|
| 11 |
+
*/
|
| 12 |
+
export default function XTerm({ onData, output }) {
|
| 13 |
+
const containerId = "terminal-container";
|
| 14 |
+
const termRef = useRef(null);
|
| 15 |
+
const bufferRef = useRef(""); // collects user typed chars until Enter
|
| 16 |
+
const fitRef = useRef(null);
|
| 17 |
+
|
| 18 |
+
useEffect(() => {
|
| 19 |
+
const term = new Terminal({
|
| 20 |
+
cursorBlink: true,
|
| 21 |
+
fontSize: 14,
|
| 22 |
+
disableStdin: false,
|
| 23 |
+
convertEol: true,
|
| 24 |
+
theme: {
|
| 25 |
+
background: "#1e1e1e",
|
| 26 |
+
foreground: "#ffffff",
|
| 27 |
+
},
|
| 28 |
+
});
|
| 29 |
+
|
| 30 |
+
const fitAddon = new FitAddon();
|
| 31 |
+
fitRef.current = fitAddon;
|
| 32 |
+
|
| 33 |
+
term.loadAddon(fitAddon);
|
| 34 |
+
term.open(document.getElementById(containerId));
|
| 35 |
+
fitAddon.fit();
|
| 36 |
+
|
| 37 |
+
// Keep a reference
|
| 38 |
+
termRef.current = term;
|
| 39 |
+
|
| 40 |
+
// echo typed characters and detect Enter
|
| 41 |
+
term.onData((data) => {
|
| 42 |
+
// xterm sends strings including characters and control chars like '\r'
|
| 43 |
+
// append to visible terminal
|
| 44 |
+
term.write(data);
|
| 45 |
+
|
| 46 |
+
// common Enter is '\r' (CR)
|
| 47 |
+
if (data === "\r" || data === "\n") {
|
| 48 |
+
// capture current buffer as line, trim trailing CR/LF
|
| 49 |
+
const line = (bufferRef.current || "").replace(/\r?\n$/, "");
|
| 50 |
+
bufferRef.current = ""; // reset buffer
|
| 51 |
+
// echo newline if not already
|
| 52 |
+
// (we already wrote the '\r' above)
|
| 53 |
+
// call parent handler
|
| 54 |
+
try {
|
| 55 |
+
if (typeof onData === "function" && line !== null) onData(line);
|
| 56 |
+
} catch (e) {
|
| 57 |
+
// swallow
|
| 58 |
+
console.error("XTerm onData handler threw:", e);
|
| 59 |
+
}
|
| 60 |
+
return;
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
// backspace handling: if user presses backspace key, it may come as '\x7f' or '\b'
|
| 64 |
+
if (data === "\x7f" || data === "\b") {
|
| 65 |
+
bufferRef.current = bufferRef.current.slice(0, -1);
|
| 66 |
+
return;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
// other control sequences ignore
|
| 70 |
+
if (data.charCodeAt(0) < 32) {
|
| 71 |
+
// ignore other ctrl chars
|
| 72 |
+
return;
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
// normal characters: append to buffer
|
| 76 |
+
bufferRef.current += data;
|
| 77 |
+
});
|
| 78 |
+
|
| 79 |
+
// expose a simple focus method on container element for external focusing
|
| 80 |
+
const container = document.getElementById(containerId);
|
| 81 |
+
if (container) container.tabIndex = 0;
|
| 82 |
+
|
| 83 |
+
return () => {
|
| 84 |
+
try {
|
| 85 |
+
term.dispose();
|
| 86 |
+
} catch {}
|
| 87 |
+
};
|
| 88 |
+
}, [onData]);
|
| 89 |
+
|
| 90 |
+
// Append new output when `output` prop changes
|
| 91 |
+
useEffect(() => {
|
| 92 |
+
const term = termRef.current;
|
| 93 |
+
if (!term || !output) return;
|
| 94 |
+
// write a newline and the output text (preserves newlines)
|
| 95 |
+
term.writeln("");
|
| 96 |
+
// if output includes multiple lines, write each
|
| 97 |
+
const lines = output.split(/\r?\n/);
|
| 98 |
+
lines.forEach((ln, idx) => {
|
| 99 |
+
// avoid extra blank at very start
|
| 100 |
+
if (idx === 0 && ln === "") return;
|
| 101 |
+
term.writeln(ln);
|
| 102 |
+
});
|
| 103 |
+
}, [output]);
|
| 104 |
+
|
| 105 |
+
// helper to focus the hidden xterm textarea
|
| 106 |
+
const focus = () => {
|
| 107 |
+
// xterm's helper textarea is what receives keyboard input
|
| 108 |
+
const ta = document.querySelector(`#${containerId} .xterm-helper-textarea`);
|
| 109 |
+
if (ta) {
|
| 110 |
+
ta.focus();
|
| 111 |
+
const len = ta.value?.length ?? 0;
|
| 112 |
+
try { ta.setSelectionRange(len, len); } catch {}
|
| 113 |
+
} else {
|
| 114 |
+
const cont = document.getElementById(containerId);
|
| 115 |
+
if (cont) cont.focus();
|
| 116 |
+
}
|
| 117 |
+
};
|
| 118 |
+
|
| 119 |
+
// expose a global method in-case parent wants to call it (not ideal but handy)
|
| 120 |
+
useEffect(() => {
|
| 121 |
+
window.__xterm_focus = focus;
|
| 122 |
+
return () => { try { delete window.__xterm_focus } catch {} };
|
| 123 |
+
}, []);
|
| 124 |
+
|
| 125 |
+
return (
|
| 126 |
+
<div
|
| 127 |
+
id={containerId}
|
| 128 |
+
style={{
|
| 129 |
+
width: "100%",
|
| 130 |
+
height: "180px",
|
| 131 |
+
background: "#1e1e1e",
|
| 132 |
+
borderTop: "1px solid #333",
|
| 133 |
+
outline: "none",
|
| 134 |
+
}}
|
| 135 |
+
/>
|
| 136 |
+
);
|
| 137 |
+
}
|
src/agent/assistant.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// src/agent/assistant.js
|
| 2 |
+
import { api } from "../apiClient";
|
| 3 |
+
|
| 4 |
+
export async function askAgent(message, history = []) {
|
| 5 |
+
const res = await api.post(
|
| 6 |
+
"/chat-stream",
|
| 7 |
+
{ message, history },
|
| 8 |
+
{ responseType: "text" } // backend returns text/plain
|
| 9 |
+
);
|
| 10 |
+
|
| 11 |
+
return res.data; // whole reply text
|
| 12 |
+
}
|
src/agent/projectGenerator.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// src/agent/projectGenerator.js
|
| 2 |
+
import { api } from "../apiClient";
|
| 3 |
+
|
| 4 |
+
export async function generateProject(file, frontend, backend, database) {
|
| 5 |
+
const formData = new FormData();
|
| 6 |
+
formData.append("file", file);
|
| 7 |
+
formData.append("frontend", frontend);
|
| 8 |
+
formData.append("backend", backend);
|
| 9 |
+
formData.append("database", database);
|
| 10 |
+
|
| 11 |
+
const res = await api.post("/chat-stream-doc", formData, {
|
| 12 |
+
responseType: "blob", // you’re getting ZIP back
|
| 13 |
+
});
|
| 14 |
+
|
| 15 |
+
// trigger download
|
| 16 |
+
const url = window.URL.createObjectURL(new Blob([res.data]));
|
| 17 |
+
const a = document.createElement("a");
|
| 18 |
+
a.href = url;
|
| 19 |
+
a.download = "generated_project.zip";
|
| 20 |
+
a.click();
|
| 21 |
+
window.URL.revokeObjectURL(url);
|
| 22 |
+
}
|
src/agent/runner.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// src/agent/runner.js
|
| 2 |
+
import { api } from "../apiClient";
|
| 3 |
+
|
| 4 |
+
// Map language id -> file extension
|
| 5 |
+
const EXT_MAP = {
|
| 6 |
+
python: ".py",
|
| 7 |
+
javascript: ".js",
|
| 8 |
+
typescript: ".ts",
|
| 9 |
+
c: ".c",
|
| 10 |
+
cpp: ".cpp",
|
| 11 |
+
java: ".java",
|
| 12 |
+
html: ".html",
|
| 13 |
+
css: ".css",
|
| 14 |
+
json: ".json",
|
| 15 |
+
};
|
| 16 |
+
|
| 17 |
+
export async function runCode(code, language, stdin = "") {
|
| 18 |
+
const ext = EXT_MAP[language] || ".txt";
|
| 19 |
+
|
| 20 |
+
// Use a consistent name – backend only cares about extension
|
| 21 |
+
const filename = "main" + ext;
|
| 22 |
+
|
| 23 |
+
const res = await api.post("/execute", {
|
| 24 |
+
code,
|
| 25 |
+
filename,
|
| 26 |
+
input: stdin,
|
| 27 |
+
});
|
| 28 |
+
|
| 29 |
+
return res.data; // { output, error }
|
| 30 |
+
}
|
src/apiClient.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import axios from "axios";
|
| 2 |
+
|
| 3 |
+
const API_BASE = process.env.REACT_APP_API_BASE; // 👈 CRA uses REACT_APP_
|
| 4 |
+
|
| 5 |
+
export const api = axios.create({
|
| 6 |
+
baseURL: API_BASE,
|
| 7 |
+
});
|
src/fileStore.js
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// fileStore.js
|
| 2 |
+
const STORAGE_KEY = "devmate_ide_tree_v2";
|
| 3 |
+
|
| 4 |
+
/**
|
| 5 |
+
* Tree node structure:
|
| 6 |
+
* {
|
| 7 |
+
* type: "folder" | "file",
|
| 8 |
+
* name: "src" or "main.py",
|
| 9 |
+
* path: "src" or "src/main.py",
|
| 10 |
+
* children: [ ... ] // only for folders
|
| 11 |
+
* content: "..." // only for files
|
| 12 |
+
* }
|
| 13 |
+
*/
|
| 14 |
+
|
| 15 |
+
const defaultTree = {
|
| 16 |
+
type: "folder",
|
| 17 |
+
name: "root",
|
| 18 |
+
path: "",
|
| 19 |
+
children: [
|
| 20 |
+
{
|
| 21 |
+
type: "file",
|
| 22 |
+
name: "main.py",
|
| 23 |
+
path: "main.py",
|
| 24 |
+
content: "# Python\nprint('Hello from IDE')",
|
| 25 |
+
},
|
| 26 |
+
{
|
| 27 |
+
type: "folder",
|
| 28 |
+
name: "src",
|
| 29 |
+
path: "src",
|
| 30 |
+
children: [
|
| 31 |
+
{
|
| 32 |
+
type: "file",
|
| 33 |
+
name: "script.js",
|
| 34 |
+
path: "src/script.js",
|
| 35 |
+
content: "console.log('Hello from src');",
|
| 36 |
+
},
|
| 37 |
+
],
|
| 38 |
+
},
|
| 39 |
+
],
|
| 40 |
+
};
|
| 41 |
+
|
| 42 |
+
// ---------- persistence ----------
|
| 43 |
+
export function loadTree() {
|
| 44 |
+
try {
|
| 45 |
+
const raw = localStorage.getItem(STORAGE_KEY);
|
| 46 |
+
return raw ? JSON.parse(raw) : defaultTree;
|
| 47 |
+
} catch (e) {
|
| 48 |
+
console.warn("loadTree failed:", e);
|
| 49 |
+
return defaultTree;
|
| 50 |
+
}
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
export function saveTree(tree) {
|
| 54 |
+
try {
|
| 55 |
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(tree));
|
| 56 |
+
} catch (e) {
|
| 57 |
+
console.warn("saveTree failed:", e);
|
| 58 |
+
}
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
// ---------- traversal helpers ----------
|
| 62 |
+
function clone(obj) {
|
| 63 |
+
return JSON.parse(JSON.stringify(obj));
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
// find node and parent by path
|
| 67 |
+
export function findNodeAndParent(root, path) {
|
| 68 |
+
if (path === "" || path == null) return { node: root, parent: null };
|
| 69 |
+
const parts = path.split("/").filter(Boolean);
|
| 70 |
+
let node = root;
|
| 71 |
+
let parent = null;
|
| 72 |
+
for (let i = 0; i < parts.length; i++) {
|
| 73 |
+
const part = parts[i];
|
| 74 |
+
parent = node;
|
| 75 |
+
if (!parent.children) return { node: null, parent: null };
|
| 76 |
+
node = parent.children.find((c) => c.name === part);
|
| 77 |
+
if (!node) return { node: null, parent: null };
|
| 78 |
+
}
|
| 79 |
+
return { node, parent };
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
export function getNodeByPath(root, path) {
|
| 83 |
+
const r = findNodeAndParent(root, path);
|
| 84 |
+
return r.node || null;
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
// build child path
|
| 88 |
+
function joinPath(parentPath, name) {
|
| 89 |
+
if (!parentPath) return name;
|
| 90 |
+
if (!name) return parentPath;
|
| 91 |
+
return parentPath + "/" + name;
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
// ---------- CRUD operations ----------
|
| 95 |
+
|
| 96 |
+
// Add file under parentPath (string). If parentPath points to file, use its parent.
|
| 97 |
+
export function addFile(tree, filename, parentPath = "") {
|
| 98 |
+
const newTree = clone(tree);
|
| 99 |
+
// find parent
|
| 100 |
+
const { node: parent } = findNodeAndParent(newTree, parentPath);
|
| 101 |
+
const target = parent?.type === "folder" ? parent : newTree;
|
| 102 |
+
// ensure unique name
|
| 103 |
+
let name = filename;
|
| 104 |
+
const exists = (name) =>
|
| 105 |
+
target.children && target.children.some((c) => c.name === name);
|
| 106 |
+
let idx = 1;
|
| 107 |
+
const base = name.includes(".") ? name.slice(0, name.lastIndexOf(".")) : name;
|
| 108 |
+
const ext = name.includes(".") ? name.slice(name.lastIndexOf(".")) : "";
|
| 109 |
+
while (exists(name)) {
|
| 110 |
+
name = `${base}_${idx++}${ext}`;
|
| 111 |
+
}
|
| 112 |
+
const path = joinPath(target.path, name);
|
| 113 |
+
const newNode = { type: "file", name, path, content: `// ${name}\n` };
|
| 114 |
+
target.children = target.children || [];
|
| 115 |
+
target.children.push(newNode);
|
| 116 |
+
return newTree;
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
// Add folder under parentPath
|
| 120 |
+
export function addFolder(tree, folderName, parentPath = "") {
|
| 121 |
+
const newTree = clone(tree);
|
| 122 |
+
const { node: parent } = findNodeAndParent(newTree, parentPath);
|
| 123 |
+
const target = parent?.type === "folder" ? parent : newTree;
|
| 124 |
+
let name = folderName;
|
| 125 |
+
const exists = (name) =>
|
| 126 |
+
target.children && target.children.some((c) => c.name === name && c.type === "folder");
|
| 127 |
+
let idx = 1;
|
| 128 |
+
while (exists(name)) {
|
| 129 |
+
name = `${folderName}_${idx++}`;
|
| 130 |
+
}
|
| 131 |
+
const path = joinPath(target.path, name);
|
| 132 |
+
const newNode = { type: "folder", name, path, children: [] };
|
| 133 |
+
target.children = target.children || [];
|
| 134 |
+
target.children.push(newNode);
|
| 135 |
+
return newTree;
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
// Delete node (file or folder) at given path (recursive for folders)
|
| 139 |
+
export function deleteNode(tree, path) {
|
| 140 |
+
const newTree = clone(tree);
|
| 141 |
+
if (!path) return newTree; // cannot delete root
|
| 142 |
+
const parts = path.split("/").filter(Boolean);
|
| 143 |
+
const nameToDelete = parts.pop();
|
| 144 |
+
const parentPath = parts.join("/");
|
| 145 |
+
const { node: parent } = findNodeAndParent(newTree, parentPath);
|
| 146 |
+
if (!parent || !parent.children) return newTree;
|
| 147 |
+
parent.children = parent.children.filter((c) => c.name !== nameToDelete);
|
| 148 |
+
return newTree;
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
// Rename node: change name and update paths recursively for children
|
| 152 |
+
export function renameNode(tree, path, newName) {
|
| 153 |
+
const newTree = clone(tree);
|
| 154 |
+
const { node, parent } = findNodeAndParent(newTree, path);
|
| 155 |
+
if (!node || !parent) {
|
| 156 |
+
// special case root rename not allowed
|
| 157 |
+
if (node && !parent) return newTree;
|
| 158 |
+
return newTree;
|
| 159 |
+
}
|
| 160 |
+
// ensure no duplicate under parent
|
| 161 |
+
if (parent.children.some((c) => c.name === newName && c.path !== node.path)) {
|
| 162 |
+
// collision: abort
|
| 163 |
+
return newTree;
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
const oldPath = node.path;
|
| 167 |
+
node.name = newName;
|
| 168 |
+
const newPath = joinPath(parent.path, newName);
|
| 169 |
+
// update path recursively
|
| 170 |
+
function updatePaths(n, currPath) {
|
| 171 |
+
n.path = currPath;
|
| 172 |
+
if (n.children) {
|
| 173 |
+
for (let child of n.children) {
|
| 174 |
+
const childNewPath = currPath ? currPath + "/" + child.name : child.name;
|
| 175 |
+
updatePaths(child, childNewPath);
|
| 176 |
+
}
|
| 177 |
+
}
|
| 178 |
+
}
|
| 179 |
+
updatePaths(node, newPath);
|
| 180 |
+
return newTree;
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
// Update file content
|
| 184 |
+
export function updateFileContent(tree, path, content) {
|
| 185 |
+
const newTree = clone(tree);
|
| 186 |
+
const { node } = findNodeAndParent(newTree, path);
|
| 187 |
+
if (node && node.type === "file") {
|
| 188 |
+
node.content = content;
|
| 189 |
+
}
|
| 190 |
+
return newTree;
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
// Search within files (returns array of {path, file, excerpt})
|
| 194 |
+
export function searchTree(node, term, results = []) {
|
| 195 |
+
if (!node) return results;
|
| 196 |
+
if (node.type === "file" && node.content && node.content.includes(term)) {
|
| 197 |
+
results.push({
|
| 198 |
+
path: node.path,
|
| 199 |
+
file: node.name,
|
| 200 |
+
excerpt: node.content
|
| 201 |
+
.split("\n")
|
| 202 |
+
.filter((l) => l.includes(term))
|
| 203 |
+
.slice(0, 5)
|
| 204 |
+
.join("\n"),
|
| 205 |
+
});
|
| 206 |
+
}
|
| 207 |
+
if (node.children) {
|
| 208 |
+
for (const c of node.children) searchTree(c, term, results);
|
| 209 |
+
}
|
| 210 |
+
return results;
|
| 211 |
+
}
|
src/problemParser.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// problemParser.js
|
| 2 |
+
|
| 3 |
+
export function parseProblems(output) {
|
| 4 |
+
if (!output) return [];
|
| 5 |
+
|
| 6 |
+
const lines = output.split("\n");
|
| 7 |
+
const problems = [];
|
| 8 |
+
|
| 9 |
+
const regexes = [
|
| 10 |
+
// Python
|
| 11 |
+
{ re: /(File "(.+)", line (\d+))/, lang: "python" },
|
| 12 |
+
// Java
|
| 13 |
+
{ re: /(.*\.java):(\d+): (.+)/, lang: "java" },
|
| 14 |
+
// JS
|
| 15 |
+
{ re: /(.*):(\d+):(\d+)/, lang: "js" },
|
| 16 |
+
];
|
| 17 |
+
|
| 18 |
+
for (let line of lines) {
|
| 19 |
+
for (let { re, lang } of regexes) {
|
| 20 |
+
const m = line.match(re);
|
| 21 |
+
if (m) {
|
| 22 |
+
problems.push({
|
| 23 |
+
file: m[2] || m[1],
|
| 24 |
+
line: m[3] || m[2],
|
| 25 |
+
message: m[4] || m[0],
|
| 26 |
+
lang,
|
| 27 |
+
});
|
| 28 |
+
}
|
| 29 |
+
}
|
| 30 |
+
}
|
| 31 |
+
return problems;
|
| 32 |
+
}
|
src/zipExport.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// zipExport.js
|
| 2 |
+
import JSZip from "jszip";
|
| 3 |
+
import { loadTree } from "./fileStore";
|
| 4 |
+
|
| 5 |
+
export async function downloadProjectZip() {
|
| 6 |
+
const tree = loadTree();
|
| 7 |
+
const zip = new JSZip();
|
| 8 |
+
|
| 9 |
+
function addToZip(node, folder) {
|
| 10 |
+
if (node.type === "file") {
|
| 11 |
+
folder.file(node.name, node.content || "");
|
| 12 |
+
} else if (node.type === "folder") {
|
| 13 |
+
const newFolder = folder.folder(node.name);
|
| 14 |
+
node.children?.forEach((c) => addToZip(c, newFolder));
|
| 15 |
+
}
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
addToZip(tree, zip);
|
| 19 |
+
const blob = await zip.generateAsync({ type: "blob" });
|
| 20 |
+
|
| 21 |
+
const a = document.createElement("a");
|
| 22 |
+
a.href = URL.createObjectURL(blob);
|
| 23 |
+
a.download = "project.zip";
|
| 24 |
+
a.click();
|
| 25 |
+
}
|