Compare commits
2 Commits
31eecf4ba5
...
8f5558fdb7
| Author | SHA1 | Date | |
|---|---|---|---|
| 8f5558fdb7 | |||
| 07066ec8c0 |
@@ -58,19 +58,39 @@ export const IDE: React.FC<IDEProps> = ({
|
|||||||
const createNewProject = useIDEStore((state) => state.createNewProject);
|
const createNewProject = useIDEStore((state) => state.createNewProject);
|
||||||
const selectFile = useIDEStore((state) => state.selectFile);
|
const selectFile = useIDEStore((state) => state.selectFile);
|
||||||
const updateFileContent = useIDEStore((state) => state.updateFileContent);
|
const updateFileContent = useIDEStore((state) => state.updateFileContent);
|
||||||
|
const saveActiveFile = useIDEStore((state) => state.saveActiveFile);
|
||||||
const closeFile = useIDEStore((state) => state.closeFile);
|
const closeFile = useIDEStore((state) => state.closeFile);
|
||||||
const closeAllFiles = useIDEStore((state) => state.closeAllFiles);
|
const closeAllFiles = useIDEStore((state) => state.closeAllFiles);
|
||||||
const closeOtherFiles = useIDEStore((state) => state.closeOtherFiles);
|
const closeOtherFiles = useIDEStore((state) => state.closeOtherFiles);
|
||||||
const initialize = useIDEStore((state) => state.initialize);
|
const initialize = useIDEStore((state) => state.initialize);
|
||||||
const isInitialized = useIDEStore((state) => state.isInitialized);
|
const isInitialized = useIDEStore((state) => state.isInitialized);
|
||||||
|
const fetchTree = useIDEStore((state) => state.fetchTree);
|
||||||
|
|
||||||
// Инициализация файлов
|
// Обработка Ctrl+S
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if ((e.ctrlKey || e.metaKey) && e.key === "s") {
|
||||||
|
e.preventDefault();
|
||||||
|
saveActiveFile();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener("keydown", handleKeyDown);
|
||||||
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||||
|
}, [saveActiveFile]);
|
||||||
|
|
||||||
|
// При загрузке пробуем загрузить дерево с сервера
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isInitialized) {
|
if (!isInitialized) {
|
||||||
const filesToInit = externalFiles || defaultInitialFiles;
|
fetchTree().catch(() => {
|
||||||
initialize(filesToInit);
|
// Только при ошибке — используем моковые данные
|
||||||
|
const state = useIDEStore.getState();
|
||||||
|
if (!state.files) {
|
||||||
|
const filesToInit = externalFiles || defaultInitialFiles;
|
||||||
|
initialize(filesToInit);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, [isInitialized, externalFiles, initialize]);
|
}, [isInitialized]);
|
||||||
|
|
||||||
// Если проект не открыт
|
// Если проект не открыт
|
||||||
if (!files) {
|
if (!files) {
|
||||||
@@ -249,10 +269,30 @@ export const IDE: React.FC<IDEProps> = ({
|
|||||||
)}
|
)}
|
||||||
{!onBack && <div />}
|
{!onBack && <div />}
|
||||||
<span style={{ fontWeight: 400 }}>
|
<span style={{ fontWeight: 400 }}>
|
||||||
{activeFile ? `${activeFile.name} - ` : ""}
|
{activeFile
|
||||||
|
? `${activeFile.name}${activeFile.dirty ? " •" : ""} - `
|
||||||
|
: ""}
|
||||||
{files.name}
|
{files.name}
|
||||||
</span>
|
</span>
|
||||||
<div style={{ width: 60 }} />
|
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
||||||
|
{activeFile?.dirty && (
|
||||||
|
<button
|
||||||
|
onClick={saveActiveFile}
|
||||||
|
style={{
|
||||||
|
background: "transparent",
|
||||||
|
border: "none",
|
||||||
|
color: c.textPrimary,
|
||||||
|
cursor: "pointer",
|
||||||
|
fontSize: "11px",
|
||||||
|
padding: "4px 8px",
|
||||||
|
borderRadius: "4px",
|
||||||
|
}}
|
||||||
|
title="Сохранить (Ctrl+S)"
|
||||||
|
>
|
||||||
|
Сохранить
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: "flex", flex: 1, overflow: "hidden" }}>
|
<div style={{ display: "flex", flex: 1, overflow: "hidden" }}>
|
||||||
<div style={{ width: "260px", flexShrink: 0 }}>
|
<div style={{ width: "260px", flexShrink: 0 }}>
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { apiClient } from "@/shared/api/axios.instance";
|
||||||
|
|
||||||
|
export interface ScriptNodeDto {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
type: "file" | "folder";
|
||||||
|
content?: string;
|
||||||
|
children?: string[];
|
||||||
|
interpreter_id?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ScriptResponse {
|
||||||
|
id: number;
|
||||||
|
content: string;
|
||||||
|
interpreter_id: number;
|
||||||
|
path: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateScriptPayload {
|
||||||
|
content: string;
|
||||||
|
interpreter_id: number;
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateScriptPayload {
|
||||||
|
content: string;
|
||||||
|
interpreter_id: number;
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// apiClient уже имеет интерсептор для Authorization header
|
||||||
|
export const scriptsApi = {
|
||||||
|
getTree: async (): Promise<ScriptNodeDto[]> => {
|
||||||
|
const res = await apiClient.get<ScriptNodeDto[]>("/scripts/tree");
|
||||||
|
return res.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
createScript: async (
|
||||||
|
payload: CreateScriptPayload,
|
||||||
|
): Promise<ScriptResponse> => {
|
||||||
|
const res = await apiClient.post<ScriptResponse>("/scripts", payload);
|
||||||
|
return res.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
updateScript: async (
|
||||||
|
id: number,
|
||||||
|
payload: UpdateScriptPayload,
|
||||||
|
): Promise<ScriptResponse> => {
|
||||||
|
const res = await apiClient.put<ScriptResponse>(`/scripts/${id}`, payload);
|
||||||
|
return res.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteScript: async (id: number): Promise<void> => {
|
||||||
|
await apiClient.delete(`/scripts/${id}`);
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
addNode,
|
addNode,
|
||||||
renameNode,
|
renameNode,
|
||||||
} from "../helpers/fileTree";
|
} from "../helpers/fileTree";
|
||||||
|
import { scriptsApi } from "../api/scripts.api";
|
||||||
|
|
||||||
export const initialFiles: FileNode = {
|
export const initialFiles: FileNode = {
|
||||||
name: "my-project",
|
name: "my-project",
|
||||||
@@ -38,11 +39,15 @@ export const initialFiles: FileNode = {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface IDEFileNode extends FileNode {
|
||||||
|
dirty?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
interface IDEState {
|
interface IDEState {
|
||||||
// Файловая система
|
// Файловая система
|
||||||
files: FileNode | null;
|
files: FileNode | null;
|
||||||
openFiles: FileNode[];
|
openFiles: IDEFileNode[];
|
||||||
activeFile: FileNode | null;
|
activeFile: IDEFileNode | null;
|
||||||
expandedFolders: Set<string>;
|
expandedFolders: Set<string>;
|
||||||
searchQuery: string;
|
searchQuery: string;
|
||||||
showSearch: boolean;
|
showSearch: boolean;
|
||||||
@@ -59,6 +64,7 @@ interface IDEState {
|
|||||||
// Действия с файлами
|
// Действия с файлами
|
||||||
selectFile: (node: FileNode) => void;
|
selectFile: (node: FileNode) => void;
|
||||||
updateFileContent: (content: string) => void;
|
updateFileContent: (content: string) => void;
|
||||||
|
saveActiveFile: () => Promise<void>;
|
||||||
closeFile: (file: FileNode) => void;
|
closeFile: (file: FileNode) => void;
|
||||||
closeAllFiles: () => void;
|
closeAllFiles: () => void;
|
||||||
closeOtherFiles: (file: FileNode) => void;
|
closeOtherFiles: (file: FileNode) => void;
|
||||||
@@ -72,6 +78,20 @@ interface IDEState {
|
|||||||
deleteRoot: () => void;
|
deleteRoot: () => void;
|
||||||
createNewProject: () => void;
|
createNewProject: () => void;
|
||||||
|
|
||||||
|
// API методы
|
||||||
|
fetchTree: () => Promise<void>;
|
||||||
|
createScript: (payload: {
|
||||||
|
content: string;
|
||||||
|
interpreter_id: number;
|
||||||
|
path: string;
|
||||||
|
}) => Promise<void>;
|
||||||
|
updateScript: (
|
||||||
|
id: number,
|
||||||
|
payload: { content: string; interpreter_id: number; path: string },
|
||||||
|
) => Promise<void>;
|
||||||
|
deleteScript: (id: number) => Promise<void>;
|
||||||
|
saveActiveFile: () => Promise<void>;
|
||||||
|
|
||||||
// Поиск
|
// Поиск
|
||||||
setSearchQuery: (query: string) => void;
|
setSearchQuery: (query: string) => void;
|
||||||
toggleSearch: () => void;
|
toggleSearch: () => void;
|
||||||
@@ -94,8 +114,8 @@ interface IDEState {
|
|||||||
initialize: (initialFiles: FileNode) => void;
|
initialize: (initialFiles: FileNode) => void;
|
||||||
|
|
||||||
// Диалог подтверждения
|
// Диалог подтверждения
|
||||||
handleDialogConfirm: (value: string) => void;
|
handleDialogConfirm: (value: string) => Promise<void>;
|
||||||
handleDeleteNode: (node: FileNode) => void;
|
handleDeleteNode: (node: FileNode) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useIDEStore = create<IDEState>((set, get) => ({
|
export const useIDEStore = create<IDEState>((set, get) => ({
|
||||||
@@ -142,7 +162,7 @@ export const useIDEStore = create<IDEState>((set, get) => ({
|
|||||||
updateFileContent: (content: string) => {
|
updateFileContent: (content: string) => {
|
||||||
const { activeFile, files } = get();
|
const { activeFile, files } = get();
|
||||||
if (activeFile && files) {
|
if (activeFile && files) {
|
||||||
const updatedFile = { ...activeFile, content };
|
const updatedFile = { ...activeFile, content, dirty: true };
|
||||||
set({ activeFile: updatedFile });
|
set({ activeFile: updatedFile });
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
openFiles: state.openFiles.map((f) =>
|
openFiles: state.openFiles.map((f) =>
|
||||||
@@ -275,6 +295,113 @@ export const useIDEStore = create<IDEState>((set, get) => ({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// API: загрузка дерева с сервера
|
||||||
|
fetchTree: async () => {
|
||||||
|
try {
|
||||||
|
const data = await scriptsApi.getTree();
|
||||||
|
const nodeMap = new Map<string, FileNode>();
|
||||||
|
|
||||||
|
data.forEach((item) => {
|
||||||
|
nodeMap.set(item.name, {
|
||||||
|
id: item.id,
|
||||||
|
name: item.name,
|
||||||
|
type: item.type === "folder" ? "folder" : "file",
|
||||||
|
content: item.content || "",
|
||||||
|
path: item.name,
|
||||||
|
interpreter_id: item.interpreter_id,
|
||||||
|
children: item.type === "folder" ? [] : undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasParent = new Set<string>();
|
||||||
|
data.forEach((item) => {
|
||||||
|
if (item.children) {
|
||||||
|
item.children.forEach((childName: string) => {
|
||||||
|
hasParent.add(childName);
|
||||||
|
const parent = nodeMap.get(item.name);
|
||||||
|
const child = nodeMap.get(childName);
|
||||||
|
if (parent?.children && child) {
|
||||||
|
parent.children.push(child);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const roots = [...nodeMap.entries()]
|
||||||
|
.filter(([name]) => !hasParent.has(name))
|
||||||
|
.map(([, node]) => node);
|
||||||
|
|
||||||
|
set({
|
||||||
|
files: {
|
||||||
|
name: "scripts",
|
||||||
|
type: "folder",
|
||||||
|
children: roots,
|
||||||
|
},
|
||||||
|
expandedFolders: new Set(),
|
||||||
|
isInitialized: true,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to fetch tree:", e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// API: создание скрипта
|
||||||
|
createScript: async (payload) => {
|
||||||
|
try {
|
||||||
|
await scriptsApi.createScript(payload);
|
||||||
|
await get().fetchTree();
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to create script:", e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// API: обновление скрипта
|
||||||
|
updateScript: async (id, payload) => {
|
||||||
|
try {
|
||||||
|
await scriptsApi.updateScript(id, payload);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to update script:", e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// API: удаление скрипта
|
||||||
|
deleteScript: async (id) => {
|
||||||
|
try {
|
||||||
|
await scriptsApi.deleteScript(id);
|
||||||
|
await get().fetchTree();
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to delete script:", e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// API: сохранение активного файла
|
||||||
|
saveActiveFile: async () => {
|
||||||
|
const { activeFile } = get();
|
||||||
|
if (!activeFile || !activeFile.id) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await scriptsApi.updateScript(activeFile.id, {
|
||||||
|
content: activeFile.content || "",
|
||||||
|
interpreter_id: activeFile.interpreter_id || 0,
|
||||||
|
path: activeFile.path || "",
|
||||||
|
});
|
||||||
|
set((state) => ({
|
||||||
|
activeFile: state.activeFile
|
||||||
|
? { ...state.activeFile, dirty: false }
|
||||||
|
: null,
|
||||||
|
openFiles: state.openFiles.map((f) =>
|
||||||
|
f.path === state.activeFile?.path ? { ...f, dirty: false } : f,
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to save file:", e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// Поиск
|
// Поиск
|
||||||
setSearchQuery: (query: string) => {
|
setSearchQuery: (query: string) => {
|
||||||
set({ searchQuery: query });
|
set({ searchQuery: query });
|
||||||
@@ -290,9 +417,8 @@ export const useIDEStore = create<IDEState>((set, get) => ({
|
|||||||
setTabContextMenu: (menu) => set({ tabContextMenu: menu }),
|
setTabContextMenu: (menu) => set({ tabContextMenu: menu }),
|
||||||
|
|
||||||
// Подтверждение диалога
|
// Подтверждение диалога
|
||||||
handleDialogConfirm: (value: string) => {
|
handleDialogConfirm: async (value: string) => {
|
||||||
const { dialog, files, refreshFiles, toggleFolder, autoExpandPaths } =
|
const { dialog, files, toggleFolder, autoExpandPaths } = get();
|
||||||
get();
|
|
||||||
if (!dialog) return;
|
if (!dialog) return;
|
||||||
|
|
||||||
if (dialog.type === "rename" && dialog.node) {
|
if (dialog.type === "rename" && dialog.node) {
|
||||||
@@ -315,47 +441,34 @@ export const useIDEStore = create<IDEState>((set, get) => ({
|
|||||||
value,
|
value,
|
||||||
);
|
);
|
||||||
if (newFiles) {
|
if (newFiles) {
|
||||||
refreshFiles(newFiles);
|
set({ files: newFiles });
|
||||||
}
|
}
|
||||||
set({ dialog: null });
|
set({ dialog: null });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let parentPath: string;
|
let parentPath: string;
|
||||||
|
|
||||||
if (!dialog.node) {
|
if (!dialog.node) {
|
||||||
parentPath = files!.path || files!.name;
|
parentPath = "";
|
||||||
} else if (dialog.node.type === "folder") {
|
} else if (dialog.node.type === "folder") {
|
||||||
parentPath = dialog.node.path || dialog.node.name;
|
parentPath = dialog.node.path || dialog.node.name;
|
||||||
} else {
|
} else {
|
||||||
const pathParts = (dialog.node.path || dialog.node.name).split("/");
|
const pathParts = (dialog.node.path || dialog.node.name).split("/");
|
||||||
pathParts.pop();
|
pathParts.pop();
|
||||||
parentPath = pathParts.join("/") || files!.path || files!.name;
|
parentPath = pathParts.join("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
const parentNode = findNode(files!, parentPath);
|
const fullPath = parentPath ? `${parentPath}/${value}` : value;
|
||||||
if (
|
|
||||||
parentNode?.children?.some(
|
|
||||||
(c) => c.name.toLowerCase() === value.toLowerCase(),
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
alert(`"${value}" already exists in this folder.`);
|
|
||||||
set({ dialog: null });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let newFiles: FileNode | null = null;
|
try {
|
||||||
let createdNode: FileNode | null = null;
|
const result = await scriptsApi.createScript({
|
||||||
|
content: "",
|
||||||
|
interpreter_id: 0,
|
||||||
|
path: fullPath,
|
||||||
|
});
|
||||||
|
|
||||||
if (dialog.type === "newFile") {
|
await get().fetchTree();
|
||||||
createdNode = { name: value, type: "file", content: "" };
|
|
||||||
newFiles = addNode(files!, parentPath, createdNode);
|
|
||||||
} else if (dialog.type === "newFolder") {
|
|
||||||
createdNode = { name: value, type: "folder", children: [] };
|
|
||||||
newFiles = addNode(files!, parentPath, createdNode);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newFiles) {
|
|
||||||
const allParentPaths: string[] = [];
|
const allParentPaths: string[] = [];
|
||||||
let current = parentPath;
|
let current = parentPath;
|
||||||
while (current) {
|
while (current) {
|
||||||
@@ -371,35 +484,38 @@ export const useIDEStore = create<IDEState>((set, get) => ({
|
|||||||
});
|
});
|
||||||
autoExpandPaths(new Set(allParentPaths));
|
autoExpandPaths(new Set(allParentPaths));
|
||||||
|
|
||||||
if (createdNode && createdNode.type === "file") {
|
if (dialog.type === "newFile") {
|
||||||
const findAndOpen = (node: FileNode, name: string): FileNode | null => {
|
const createdNode: FileNode = {
|
||||||
if (node.name === name && node.type === "file") return node;
|
id: result.id,
|
||||||
if (node.children) {
|
name: value,
|
||||||
for (const child of node.children) {
|
type: "file",
|
||||||
const found = findAndOpen(child, name);
|
content: result.content,
|
||||||
if (found) return found;
|
path: result.path,
|
||||||
}
|
interpreter_id: result.interpreter_id,
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
};
|
||||||
const openedFile = findAndOpen(newFiles, value);
|
get().selectFile(createdNode);
|
||||||
refreshFiles(newFiles, openedFile || undefined);
|
|
||||||
} else {
|
|
||||||
refreshFiles(newFiles);
|
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to create:", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
set({ dialog: null });
|
set({ dialog: null });
|
||||||
},
|
},
|
||||||
|
|
||||||
// Удаление узла
|
// Удаление узла
|
||||||
handleDeleteNode: (node: FileNode) => {
|
handleDeleteNode: async (node: FileNode) => {
|
||||||
const { files, refreshFiles } = get();
|
const { files } = get();
|
||||||
const isRootNode = node.path === files?.path;
|
const isRootNode = node.path === files?.path;
|
||||||
if (isRootNode) {
|
if (isRootNode) {
|
||||||
get().deleteRoot();
|
get().deleteRoot();
|
||||||
} else if (window.confirm(`Delete "${node.name}"?`)) {
|
} else if (window.confirm(`Delete "${node.name}"?`)) {
|
||||||
const newFiles = deleteNode(files!, node.path || node.name);
|
if (node.id) {
|
||||||
if (newFiles) refreshFiles(newFiles);
|
try {
|
||||||
|
await get().deleteScript(node.id);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to delete:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -1,117 +1,81 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { FiEdit3, FiPlay } from "react-icons/fi";
|
import { FiEdit3, FiPlay } from "react-icons/fi";
|
||||||
|
import { FaSpinner } from "react-icons/fa";
|
||||||
import { FilePicker, useFilePickerStore } from "../modules/ide";
|
import { FilePicker, useFilePickerStore } from "../modules/ide";
|
||||||
import type { FileNode } from "../modules/ide";
|
import type { FileNode } from "../modules/ide";
|
||||||
|
import { scriptsApi } from "../modules/ide/api/scripts.api";
|
||||||
|
|
||||||
const mockFiles: FileNode = {
|
const convertTreeToFileNode = (data: any[]): FileNode => {
|
||||||
name: "templates",
|
const nodeMap = new Map<string, FileNode>();
|
||||||
type: "folder",
|
const childrenMap = new Map<string, string[]>();
|
||||||
children: [
|
|
||||||
{
|
// Создаём все узлы
|
||||||
name: "python-basic",
|
data.forEach((item) => {
|
||||||
type: "folder",
|
nodeMap.set(item.name, {
|
||||||
children: [
|
id: item.id,
|
||||||
{
|
name: item.name,
|
||||||
name: "src",
|
type: item.type === "folder" ? "folder" : "file",
|
||||||
type: "folder",
|
content: item.content || "",
|
||||||
children: [
|
path: item.name,
|
||||||
{
|
interpreter_id: item.interpreter_id,
|
||||||
name: "main.py",
|
children: item.type === "folder" ? [] : undefined,
|
||||||
type: "file",
|
});
|
||||||
content:
|
|
||||||
'print("Hello, World!")\n\ndef main():\n print("Welcome!")\n\nif __name__ == "__main__":\n main()',
|
if (item.children && item.children.length > 0) {
|
||||||
},
|
childrenMap.set(item.name, item.children);
|
||||||
{
|
}
|
||||||
name: "utils.py",
|
});
|
||||||
type: "file",
|
|
||||||
content: "def helper():\n return 42",
|
// Строим дерево
|
||||||
},
|
const roots: FileNode[] = [];
|
||||||
],
|
const hasParent = new Set<string>();
|
||||||
},
|
|
||||||
{
|
childrenMap.forEach((children, parentName) => {
|
||||||
name: "README.md",
|
const parentNode = nodeMap.get(parentName);
|
||||||
type: "file",
|
if (!parentNode) return;
|
||||||
content: "# Python Project\n\nA basic Python project.",
|
|
||||||
},
|
children.forEach((childName: string) => {
|
||||||
],
|
hasParent.add(childName);
|
||||||
},
|
const childNode = nodeMap.get(childName);
|
||||||
{
|
if (childNode && parentNode.children) {
|
||||||
name: "react-starter",
|
parentNode.children.push(childNode);
|
||||||
type: "folder",
|
}
|
||||||
children: [
|
});
|
||||||
{
|
});
|
||||||
name: "src",
|
|
||||||
type: "folder",
|
// Корневые элементы — те у кого нет родителя
|
||||||
children: [
|
nodeMap.forEach((node, name) => {
|
||||||
{
|
if (!hasParent.has(name)) {
|
||||||
name: "App.tsx",
|
roots.push(node);
|
||||||
type: "file",
|
}
|
||||||
content:
|
});
|
||||||
'import React from "react";\n\nexport const App: React.FC = () => {\n return <div>Hello React!</div>;\n};',
|
|
||||||
},
|
return {
|
||||||
{
|
name: "templates",
|
||||||
name: "index.tsx",
|
type: "folder",
|
||||||
type: "file",
|
children: roots,
|
||||||
content:
|
};
|
||||||
'import React from "react";\nimport { createRoot } from "react-dom/client";\nimport { App } from "./App";\n\ncreateRoot(document.getElementById("root")!).render(<App />);',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "package.json",
|
|
||||||
type: "file",
|
|
||||||
content: '{\n "name": "react-project",\n "version": "1.0.0"\n}',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "node-api",
|
|
||||||
type: "folder",
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
name: "src",
|
|
||||||
type: "folder",
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
name: "index.js",
|
|
||||||
type: "file",
|
|
||||||
content:
|
|
||||||
'const express = require("express");\nconst app = express();\nconst PORT = 3000;\n\napp.get("/", (req, res) => {\n res.json({ message: "Hello!" });\n});\n\napp.listen(PORT, () => {\n console.log(`Server running on port ${PORT}`);\n});',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "package.json",
|
|
||||||
type: "file",
|
|
||||||
content:
|
|
||||||
'{\n "name": "api-project",\n "dependencies": {\n "express": "^4.18.0"\n }\n}',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "html-css",
|
|
||||||
type: "folder",
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
name: "index.html",
|
|
||||||
type: "file",
|
|
||||||
content:
|
|
||||||
'<!DOCTYPE html>\n<html>\n<head>\n <title>My Landing</title>\n <link rel="stylesheet" href="styles.css">\n</head>\n<body>\n <h1>Welcome!</h1>\n</body>\n</html>',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "styles.css",
|
|
||||||
type: "file",
|
|
||||||
content:
|
|
||||||
"body {\n font-family: sans-serif;\n margin: 0;\n padding: 2rem;\n background: #f5f5f5;\n}\n\nh1 {\n color: #333;\n}",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TemplatesPage = () => {
|
export const TemplatesPage = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const selectedPaths = useFilePickerStore((s) => s.selectedPaths);
|
const selectedPaths = useFilePickerStore((s) => s.selectedPaths);
|
||||||
|
const [files, setFiles] = useState<FileNode | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
scriptsApi
|
||||||
|
.getTree()
|
||||||
|
.then((data) => {
|
||||||
|
setFiles(convertTreeToFileNode(data));
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.error("Failed to load tree:", e);
|
||||||
|
setFiles({ name: "templates", type: "folder", children: [] });
|
||||||
|
})
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -183,7 +147,26 @@ export const TemplatesPage = () => {
|
|||||||
|
|
||||||
{/* File Picker */}
|
{/* File Picker */}
|
||||||
<div style={{ height: "100%", overflow: "hidden" }}>
|
<div style={{ height: "100%", overflow: "hidden" }}>
|
||||||
<FilePicker files={mockFiles} />
|
{loading ? (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: "100%",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FaSpinner
|
||||||
|
size={24}
|
||||||
|
style={{
|
||||||
|
color: "var(--accent)",
|
||||||
|
animation: "spin 1s linear infinite",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : files ? (
|
||||||
|
<FilePicker files={files} />
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user