online-compiler-widget 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/FileStorage/obj/FileStorage.csproj.EntityFrameworkCore.targets +28 -0
  2. package/README.md +1 -0
  3. package/eslint.config.js +26 -0
  4. package/index.html +13 -0
  5. package/openapitools.json +7 -0
  6. package/package.json +36 -0
  7. package/pnpm-workspace.yaml +2 -0
  8. package/public/vite.svg +1 -0
  9. package/src/App.css +49 -0
  10. package/src/App.tsx +84 -0
  11. package/src/api/.openapi-generator/FILES +25 -0
  12. package/src/api/.openapi-generator/VERSION +1 -0
  13. package/src/api/.openapi-generator-ignore +23 -0
  14. package/src/api/api.ts +1312 -0
  15. package/src/api/base.ts +62 -0
  16. package/src/api/common.ts +113 -0
  17. package/src/api/configuration.ts +121 -0
  18. package/src/api/docs/CompilationError.md +26 -0
  19. package/src/api/docs/CompileRequest.md +22 -0
  20. package/src/api/docs/CompileResult.md +28 -0
  21. package/src/api/docs/CompilerApi.md +263 -0
  22. package/src/api/docs/CreateFileDto.md +22 -0
  23. package/src/api/docs/CreateProjectRequest.md +20 -0
  24. package/src/api/docs/FileApi.md +274 -0
  25. package/src/api/docs/ProcessStatus.md +28 -0
  26. package/src/api/docs/ProjectApi.md +362 -0
  27. package/src/api/docs/ProjectInfo.md +24 -0
  28. package/src/api/docs/ProjectStats.md +28 -0
  29. package/src/api/docs/RenameFileDto.md +20 -0
  30. package/src/api/docs/RenameProjectRequest.md +20 -0
  31. package/src/api/docs/RunRequest.md +24 -0
  32. package/src/api/docs/RunResult.md +30 -0
  33. package/src/api/docs/RunningProjectInfo.md +26 -0
  34. package/src/api/docs/UpdateFileDto.md +20 -0
  35. package/src/api/git_push.sh +57 -0
  36. package/src/api/index.ts +18 -0
  37. package/src/assets/Badge.svg +17 -0
  38. package/src/assets/closeIcon.svg +20 -0
  39. package/src/assets/documentIcon.svg +11 -0
  40. package/src/assets/history.svg +11 -0
  41. package/src/assets/output.svg +12 -0
  42. package/src/assets/plus.svg +20 -0
  43. package/src/assets/react.svg +1 -0
  44. package/src/assets/save-icon.svg +11 -0
  45. package/src/assets/shield.svg +10 -0
  46. package/src/assets/start.svg +11 -0
  47. package/src/assets/stop.svg +11 -0
  48. package/src/components/CompilerWidget.module.scss +169 -0
  49. package/src/components/CompilerWidget.tsx +279 -0
  50. package/src/components/FileExplorer.module.scss +372 -0
  51. package/src/components/FileExplorer.tsx +285 -0
  52. package/src/components/MonacoEditorWrapper.module.scss +29 -0
  53. package/src/components/MonacoEditorWrapper.tsx +74 -0
  54. package/src/components/OutputPanel.module.scss +123 -0
  55. package/src/components/OutputPanel.tsx +53 -0
  56. package/src/components/RunContainer.module.scss +150 -0
  57. package/src/components/RunContainer.tsx +34 -0
  58. package/src/hooks/useCompiler.ts +228 -0
  59. package/src/hooks/useInitialNodes.ts +0 -0
  60. package/src/index.css +78 -0
  61. package/src/main.tsx +7 -0
  62. package/src/types/EditorDocument.ts +8 -0
  63. package/swagger.json +1020 -0
  64. package/tsconfig.app.json +29 -0
  65. package/tsconfig.json +7 -0
  66. package/tsconfig.node.json +26 -0
  67. package/vite.config.ts +8 -0
@@ -0,0 +1,279 @@
1
+ // src/components/CompilerWidget.tsx
2
+ import React, { memo, useState, useEffect, useRef, useLayoutEffect } from 'react';
3
+ import { Handle, Position, NodeResizer, useReactFlow } from '@xyflow/react';
4
+ import { FileExplorer } from './FileExplorer';
5
+ import { MonacoEditorWrapper } from './MonacoEditorWrapper';
6
+ import { OutputPanel } from './OutputPanel';
7
+ import cls from './CompilerWidget.module.scss';
8
+ import { useCompiler } from '../hooks/useCompiler';
9
+
10
+ import ShieldIcon from "../assets/shield.svg?react";
11
+ import BadgeIcon from "../assets/Badge.svg?react";
12
+ import CloseIcon from "../assets/closeIcon.svg?react";
13
+
14
+ import type { EditorDocument } from '../types/EditorDocument';
15
+ import { RunContainer } from "./RunContainer.tsx";
16
+
17
+ interface GetInfoModel {
18
+ "widgetId": number,
19
+ "userId": number,
20
+ "role": string,
21
+ "config": string, // json
22
+ "board": {
23
+ "id": number,
24
+ "name": string,
25
+ "parentId": number
26
+ }
27
+ }
28
+
29
+ interface ConfigModel {
30
+ // ... то, как мы будем общаться с потребителем
31
+ }
32
+
33
+ interface CompilerWidgetProps {
34
+ id: string;
35
+ data?: {
36
+ initialFiles?: Record<string, string>;
37
+ language?: 'csharp' | 'js';
38
+ };
39
+ setNodeHeight?: (id: string, height: number) => void;
40
+ getInfo?: (info: GetInfoModel) => void;
41
+ }
42
+
43
+ const CompilerWidget: React.FC<CompilerWidgetProps> = ({ id, data, setNodeHeight }) => {
44
+ const {
45
+ documents,
46
+ selectedDocument,
47
+ selectedId,
48
+ setSelectedId,
49
+ setDocumentContent,
50
+ addDocument,
51
+ deleteDocument,
52
+ updateDocument,
53
+ output,
54
+ history,
55
+ run,
56
+ stop,
57
+ saveAll
58
+ } = useCompiler(
59
+ data?.initialFiles || {
60
+ 'Program.cs': '// Write your code here\nConsole.WriteLine("Hello, World!");',
61
+ }
62
+ );
63
+
64
+ // useReactFlow мы оставляем, но НЕ полагаемся на updateNodeDimensions — вызываем опционально
65
+ const rf = useReactFlow();
66
+ const maybeUpdateNodeDimensions = (nodeId: string) => {
67
+ if (rf && typeof (rf as any).updateNodeDimensions === 'function') {
68
+ try {
69
+ (rf as any).updateNodeDimensions(nodeId);
70
+ } catch {
71
+ // игнорируем ошибки — основной механизм через setNodeHeight
72
+ }
73
+ }
74
+ };
75
+
76
+ const currentDocument: EditorDocument | null = selectedDocument ?? (documents[0] ?? null);
77
+ const currentCode = currentDocument?.content ?? '';
78
+ const currentLanguage =
79
+ currentDocument?.language ??
80
+ (data?.language === 'js' ? 'javascript' : 'csharp');
81
+
82
+ // panel widths
83
+ const [leftWidth, setLeftWidth] = useState(180);
84
+ const [rightWidth, setRightWidth] = useState(220);
85
+
86
+ const [collapsed, setCollapsed] = useState(false);
87
+
88
+ const handleCodeChange = (newCode: string) => {
89
+ if (currentDocument) {
90
+ setDocumentContent(currentDocument.id, newCode);
91
+ }
92
+ };
93
+
94
+ // Обработчик переименования файла
95
+ const handleRename = (id: string, newName: string) => {
96
+ const doc = documents.find(d => d.id === id);
97
+ if (!doc || newName === doc.name) return;
98
+
99
+ updateDocument(id, {
100
+ name: newName
101
+ });
102
+ };
103
+
104
+ const containerRef = useRef<HTMLDivElement | null>(null);
105
+ const resizeObserverRef = useRef<ResizeObserver | null>(null);
106
+
107
+ // Синхронизировать высоту ноды при изменении контейнера
108
+ useLayoutEffect(() => {
109
+ if (!containerRef.current || !setNodeHeight) return;
110
+
111
+ // initial set
112
+ const rect = containerRef.current.getBoundingClientRect();
113
+ const initialHeight = collapsed ? 42 : Math.round(rect.height);
114
+ setNodeHeight(id, initialHeight);
115
+ maybeUpdateNodeDimensions(id);
116
+
117
+ // Создаём ResizeObserver, если доступен
118
+ const ro = new ResizeObserver((entries) => {
119
+ for (const entry of entries) {
120
+ if (entry.target === containerRef.current) {
121
+ const h = collapsed ? 42 : Math.round(entry.contentRect.height);
122
+ setNodeHeight(id, h);
123
+ // опционально форсируем перерисовку XYFlow (если API доступен)
124
+ maybeUpdateNodeDimensions(id);
125
+ }
126
+ }
127
+ });
128
+
129
+ resizeObserverRef.current = ro;
130
+ ro.observe(containerRef.current);
131
+
132
+ return () => {
133
+ ro.disconnect();
134
+ resizeObserverRef.current = null;
135
+ };
136
+ // eslint-disable-next-line react-hooks/exhaustive-deps
137
+ }, [id, setNodeHeight, collapsed]);
138
+
139
+ useEffect(() => {
140
+ // при смене collapsed форсируем одноразовый пересчёт (на случай, если ResizeObserver не сработал моментально)
141
+ if (!containerRef.current || !setNodeHeight) return;
142
+ const h = collapsed ? 42 : Math.round(containerRef.current.getBoundingClientRect().height);
143
+ setNodeHeight(id, h);
144
+ maybeUpdateNodeDimensions(id);
145
+ // eslint-disable-next-line react-hooks/exhaustive-deps
146
+ }, [collapsed]);
147
+
148
+ const toggleCollapsed = () => setCollapsed(prev => !prev);
149
+
150
+ // Manual panel resizing: убираем прямые вызовы updateNodeDimensions, теперь ResizeObserver всё подхватит
151
+ const startResizing = (
152
+ e: React.MouseEvent,
153
+ setter: (v: number) => void,
154
+ startWidth: number,
155
+ direction: "left" | "right",
156
+ min = 120,
157
+ max = 1000
158
+ ) => {
159
+ e.preventDefault();
160
+ e.stopPropagation();
161
+
162
+ const startX = e.clientX;
163
+
164
+ const onMove = (ev: MouseEvent) => {
165
+ const dx = ev.clientX - startX;
166
+ const newWidth = direction === "left" ? startWidth + dx : startWidth - dx;
167
+ setter(Math.min(Math.max(newWidth, min), max));
168
+ // НЕ вызываем updateNodeDimensions здесь — ResizeObserver увидит изменение размеров DOM и обновит ноду
169
+ };
170
+
171
+ const onUp = () => {
172
+ window.removeEventListener('mousemove', onMove);
173
+ window.removeEventListener('mouseup', onUp);
174
+ };
175
+
176
+ window.addEventListener('mousemove', onMove);
177
+ window.addEventListener('mouseup', onUp);
178
+ };
179
+
180
+ return (
181
+ <div
182
+ ref={containerRef}
183
+ className={`${cls.widget} ${collapsed ? cls.collapsed : ''}`}
184
+ >
185
+ <Handle type="target" position={Position.Top} />
186
+
187
+ {/* GLOBAL NodeResizer (xyflow) — виден только если не collapsed */}
188
+ {!collapsed && (
189
+ <NodeResizer
190
+ minWidth={600}
191
+ minHeight={300}
192
+ // Некоторые версии NodeResizer поддержуют onResize/onResizeEnd, некоторые — нет.
193
+ // Мы полагаемся на ResizeObserver для синхронизации высоты, так что не обязаны использовать коллбэки.
194
+ />
195
+ )}
196
+
197
+ {/* HEADER */}
198
+ <div className="drag-handle__custom">
199
+ <div className={cls.header}>
200
+ <h4>Code Block</h4>
201
+
202
+ <div className={cls.shieldContainer}>
203
+ <ShieldIcon className={cls.shield} />
204
+ <BadgeIcon className={cls.badge} />
205
+ </div>
206
+
207
+ <button className={cls.closeButton} onClick={toggleCollapsed}>
208
+ {collapsed ? (
209
+ <svg width="16" height="16" viewBox="0 0 16 16">
210
+ <path d="M4 8h8M8 4v8" stroke="currentColor" strokeWidth="2" />
211
+ </svg>
212
+ ) : (
213
+ <CloseIcon className={cls.close} />
214
+ )}
215
+ </button>
216
+ </div>
217
+ </div>
218
+
219
+ {/* BODY */}
220
+ {!collapsed && (
221
+ <div className={cls.body}>
222
+ {/* LEFT PANEL */}
223
+ <div className={cls.panel} style={{ width: leftWidth }}>
224
+ <FileExplorer
225
+ documents={documents}
226
+ selectedId={selectedId}
227
+ onSelect={setSelectedId}
228
+ onAdd={addDocument}
229
+ onRename={handleRename} // Используем inline-редактирование
230
+ onDelete={deleteDocument}
231
+ />
232
+
233
+ <div
234
+ className={cls.resizer}
235
+ onMouseDown={(e) =>
236
+ startResizing(e, setLeftWidth, leftWidth, "left")
237
+ }
238
+ />
239
+ </div>
240
+
241
+ {/* CENTER */}
242
+ <div className={cls.centerPanel}>
243
+ {currentDocument ? (
244
+ <div className={cls.editCont}>
245
+ <MonacoEditorWrapper
246
+ code={currentCode}
247
+ language={currentLanguage}
248
+ onChange={handleCodeChange}
249
+ theme="vs-light"
250
+ />
251
+ <RunContainer run={run} stop={stop} save={saveAll}/>
252
+ </div>
253
+ ) : (
254
+ <div style={{ padding: 16, color: '#666' }}>
255
+ Нет открытого документа
256
+ </div>
257
+ )}
258
+ </div>
259
+
260
+ {/* RIGHT */}
261
+ <div className={cls.panel} style={{ width: rightWidth }}>
262
+ <OutputPanel output={output} history={history} />
263
+
264
+ <div
265
+ className={cls.resizer}
266
+ onMouseDown={(e) =>
267
+ startResizing(e, setRightWidth, rightWidth, "right")
268
+ }
269
+ />
270
+ </div>
271
+ </div>
272
+ )}
273
+
274
+ <Handle type="source" position={Position.Bottom} />
275
+ </div>
276
+ );
277
+ };
278
+
279
+ export default memo(CompilerWidget);
@@ -0,0 +1,372 @@
1
+ $file-bg: #f5f5f5;
2
+ $file-border: #ddd;
3
+ $file-selected: #d0eaff;
4
+
5
+ .fileExplorer {
6
+ width: auto;
7
+ background: #F8FAFC;
8
+ border-right: 1px solid $file-border;
9
+ display: flex;
10
+ flex-direction: column;
11
+ height: 100%;
12
+ width: auto;
13
+
14
+ .header {
15
+ padding: 6px 7px;
16
+ display: flex;
17
+ justify-content: start;
18
+ align-items: center;
19
+ background: #F1F5F9;
20
+ border-bottom: 1px solid #E2E8F0;
21
+ min-height: 21px;
22
+
23
+ span {
24
+ font-family: Arial;
25
+ font-weight: 400;
26
+ font-size: 6.18px;
27
+ margin-right: 1.7px;
28
+ align-items: center;
29
+ }
30
+
31
+ .documentIcon{
32
+ height: 12px;
33
+ margin-right: 1.5px;
34
+
35
+ svg {
36
+ height: 100%;
37
+ width: auto;
38
+ display: block;
39
+ }
40
+ }
41
+
42
+ button {
43
+ all: unset;
44
+ cursor: pointer;
45
+ display: inline-flex;
46
+ align-items: center;
47
+ justify-content: center;
48
+ padding: 0;
49
+ margin: 0;
50
+ outline: none;
51
+ }
52
+
53
+ .addButton {
54
+ height: auto;
55
+ margin-left: auto;
56
+ .plusIcon {
57
+ height: 13px;
58
+ }
59
+
60
+ &:hover {
61
+ opacity: 0.8;
62
+ }
63
+ }
64
+ }
65
+
66
+ .fileList {
67
+ list-style: none;
68
+ margin: 0;
69
+ padding: 0;
70
+ overflow-y: auto;
71
+ flex-grow: 1;
72
+ padding: 4.12px;
73
+
74
+ .fileItem {
75
+ padding: 3.74px 4.12px;
76
+ cursor: pointer;
77
+ display: flex;
78
+ align-items: center;
79
+ gap: 4.12px;
80
+ transition: background 0.1s;
81
+ position: relative;
82
+ border-radius: 2px;
83
+
84
+ &:hover {
85
+ background: #e9e9e9;
86
+ }
87
+
88
+ &.selected {
89
+ background: #DBEAFE;
90
+ }
91
+
92
+ &.modified {
93
+ .modifiedDot {
94
+ display: inline-block;
95
+ }
96
+ }
97
+
98
+ .fileIcon {
99
+ font-size: 7px;
100
+ margin-right: 3px;
101
+ flex-shrink: 0;
102
+ }
103
+
104
+ .nameContainer {
105
+ display: flex;
106
+ align-items: center;
107
+ gap: 4px;
108
+ flex: 1;
109
+ min-width: 0;
110
+ }
111
+
112
+ .itemText {
113
+ font-size: 7px;
114
+ font-family: sans-serif;
115
+ color: #1C398E;
116
+ align-items: center;
117
+ white-space: nowrap;
118
+ overflow: hidden;
119
+ text-overflow: ellipsis;
120
+ flex: 1;
121
+ line-height: 14px;
122
+ padding: 0;
123
+ margin: 0;
124
+ }
125
+
126
+ .modifiedDot {
127
+ display: none;
128
+ color: #ff6b6b;
129
+ font-size: 12px;
130
+ flex-shrink: 0;
131
+ animation: pulse 2s infinite;
132
+
133
+ &:hover {
134
+ color: #ff5252;
135
+ }
136
+ }
137
+
138
+ .editContainer {
139
+ flex: 1;
140
+ min-width: 0;
141
+ }
142
+ }
143
+ }
144
+ }
145
+
146
+ @keyframes pulse {
147
+ 0% {
148
+ opacity: 1;
149
+ }
150
+ 50% {
151
+ opacity: 0.5;
152
+ }
153
+ 100% {
154
+ opacity: 1;
155
+ }
156
+ }
157
+
158
+ .moreBtn {
159
+ margin-left: auto;
160
+ background: none;
161
+ border: none;
162
+ font-size: 12px;
163
+ padding: 2px 6px;
164
+ cursor: pointer;
165
+ opacity: 0.6;
166
+ outline: none;
167
+ border-radius: 3px;
168
+ flex-shrink: 0;
169
+
170
+ &:hover {
171
+ opacity: 1;
172
+ background: rgba(0, 0, 0, 0.05);
173
+ }
174
+ }
175
+
176
+ .clickZone {
177
+ display: flex;
178
+ align-items: center;
179
+ flex-grow: 1;
180
+ gap: 4px;
181
+ min-width: 0;
182
+ padding: 0;
183
+ margin: 0;
184
+ }
185
+
186
+ .contextMenu {
187
+ position: absolute;
188
+ right: 4px;
189
+ top: 28px;
190
+ background: #F1F5F9;
191
+ border: 1px solid #444;
192
+ border-radius: 4px;
193
+ padding: 4px 0;
194
+ min-width: 140px;
195
+ z-index: 100;
196
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
197
+ }
198
+
199
+ .menuItem {
200
+ padding: 6px 10px;
201
+ cursor: pointer;
202
+ white-space: nowrap;
203
+ font-size: 14px;
204
+
205
+ &:hover {
206
+ background: #DBEAFE;
207
+ }
208
+ }
209
+
210
+ // СТИЛИ ДЛЯ INPUT ПРИ РЕДАКТИРОВАНИИ
211
+ .editInput {
212
+ flex: 1;
213
+ min-width: 0;
214
+ background: #ffffff;
215
+ border: 1px solid #0078d4;
216
+ border-radius: 2px;
217
+ padding: 0 2px;
218
+ margin: 0;
219
+ font-size: 7px;
220
+ font-family: sans-serif;
221
+ color: #1C398E;
222
+ outline: none;
223
+ height: 14px;
224
+ line-height: 12px;
225
+ box-sizing: border-box;
226
+ width: 100%;
227
+ max-width: 100%;
228
+
229
+ &:focus {
230
+ border-color: #0078d4;
231
+ box-shadow: 0 0 0 1px rgba(0, 120, 212, 0.3);
232
+ }
233
+ }
234
+
235
+ // Состояние редактирования - скрываем кнопку меню
236
+ .fileItem:has(.editInput) {
237
+ .moreBtn {
238
+ display: none;
239
+ }
240
+
241
+ &:hover {
242
+ background: transparent;
243
+ }
244
+
245
+ .clickZone {
246
+ cursor: default;
247
+ }
248
+ }
249
+
250
+ // СТИЛИ ДЛЯ МОДАЛЬНОГО ОКНА
251
+ .modalOverlay {
252
+ position: fixed;
253
+ top: 0;
254
+ left: 0;
255
+ right: 0;
256
+ bottom: 0;
257
+ background: rgba(0, 0, 0, 0.5);
258
+ display: flex;
259
+ align-items: center;
260
+ justify-content: center;
261
+ z-index: 1000;
262
+ }
263
+
264
+ .modalContent {
265
+ background: white;
266
+ padding: 24px;
267
+ border-radius: 8px;
268
+ min-width: 320px;
269
+ max-width: 400px;
270
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
271
+
272
+ h3 {
273
+ margin: 0 0 20px 0;
274
+ font-size: 16px;
275
+ font-weight: 600;
276
+ color: #333;
277
+ text-align: center;
278
+ }
279
+ }
280
+
281
+ .formGroup {
282
+ margin-bottom: 16px;
283
+
284
+ label {
285
+ display: block;
286
+ margin-bottom: 6px;
287
+ font-size: 13px;
288
+ font-weight: 500;
289
+ color: #555;
290
+ }
291
+
292
+ input {
293
+ width: 100%;
294
+ padding: 10px 12px;
295
+ border: 1px solid #ddd;
296
+ border-radius: 4px;
297
+ font-size: 14px;
298
+ box-sizing: border-box;
299
+ transition: border-color 0.2s, box-shadow 0.2s;
300
+
301
+ &:focus {
302
+ outline: none;
303
+ border-color: #0078d4;
304
+ box-shadow: 0 0 0 2px rgba(0, 120, 212, 0.1);
305
+ }
306
+
307
+ &.error {
308
+ border-color: #d32f2f;
309
+
310
+ &:focus {
311
+ border-color: #d32f2f;
312
+ box-shadow: 0 0 0 2px rgba(211, 47, 47, 0.1);
313
+ }
314
+ }
315
+ }
316
+
317
+ .helpText {
318
+ font-size: 12px;
319
+ color: #666;
320
+ margin-top: 4px;
321
+ margin-bottom: 8px;
322
+ }
323
+
324
+ .errorMessage {
325
+ color: #d32f2f;
326
+ font-size: 12px;
327
+ margin-top: 4px;
328
+ font-weight: 500;
329
+ }
330
+ }
331
+
332
+ .modalActions {
333
+ display: flex;
334
+ justify-content: flex-end;
335
+ gap: 12px;
336
+ margin-top: 24px;
337
+
338
+ button {
339
+ padding: 10px 20px;
340
+ border: none;
341
+ border-radius: 4px;
342
+ cursor: pointer;
343
+ font-size: 14px;
344
+ font-weight: 500;
345
+ transition: all 0.2s;
346
+ min-width: 80px;
347
+
348
+ &.cancelBtn {
349
+ background: #f5f5f5;
350
+ color: #333;
351
+
352
+ &:hover {
353
+ background: #e9e9e9;
354
+ }
355
+ }
356
+
357
+ &.confirmBtn {
358
+ background: #0078d4;
359
+ color: white;
360
+
361
+ &:hover:not(:disabled) {
362
+ background: #106ebe;
363
+ }
364
+
365
+ &:disabled {
366
+ background: #ccc;
367
+ cursor: not-allowed;
368
+ opacity: 0.6;
369
+ }
370
+ }
371
+ }
372
+ }