feat: launch scripts
ci-front / build (push) Successful in 2m19s

This commit is contained in:
nikita
2026-04-05 06:54:33 +03:00
parent d6512d6c97
commit 2bc3da21fd
11 changed files with 774 additions and 107 deletions
+2 -1
View File
@@ -8,7 +8,8 @@
"Bash(dir)", "Bash(dir)",
"Bash(move *)", "Bash(move *)",
"Bash(findstr *)", "Bash(findstr *)",
"Bash(del *)" "Bash(del *)",
"Bash(mkdir *)"
] ]
}, },
"$version": 3 "$version": 3
@@ -10,29 +10,8 @@ interface AgentState {
removeAgent: (name: string) => void; removeAgent: (name: string) => void;
} }
const mockAgents: AgentInfo[] = [
{
label: "agent-core-01",
token: "tok_a1b2c3d4e5f6g7h8",
services: ["postgres", "redis", "log-collector"],
connected_at: "2026-04-04 15:25:09",
},
{
label: "agent-worker-02",
token: "tok_x9y8z7w6v5u4t3s2",
services: ["celery-worker", "flower"],
connected_at: "2026-04-04 15:25:09",
},
{
label: "agent-monitor-03",
token: "tok_m1n2o3p4q5r6s7t8",
services: ["prometheus", "grafana", "alertmanager"],
connected_at: "2026-04-04 15:25:09",
},
];
export const useAgentStore = create<AgentState>()((set, get) => ({ export const useAgentStore = create<AgentState>()((set, get) => ({
agents: mockAgents, agents: [],
isLoading: false, isLoading: false,
error: null, error: null,
+32 -4
View File
@@ -31,6 +31,26 @@ export interface UpdateScriptPayload {
path: string; path: string;
} }
export interface RunScriptPayload {
stdin?: string;
token: string;
}
export interface RunScriptResponse {
command: string[];
id: number;
wait_url: string;
}
export interface JobWaitResponse {
command: string[];
id: number;
status: number;
stderr: string;
stdin: string;
stdout: string;
}
// apiClient уже имеет интерсептор для Authorization header // apiClient уже имеет интерсептор для Authorization header
export const scriptsApi = { export const scriptsApi = {
getInterpreters: async (): Promise<Interpreter[]> => { getInterpreters: async (): Promise<Interpreter[]> => {
@@ -84,11 +104,19 @@ export const scriptsApi = {
return res.data; return res.data;
}, },
run: async (path: string): Promise<{ id: number; status: string }> => { runScript: async (
const res = await apiClient.post<{ id: number; status: string }>( id: number,
"/scripts/run", payload: RunScriptPayload,
{ path }, ): Promise<RunScriptResponse> => {
const res = await apiClient.post<RunScriptResponse>(
`/scripts/${id}/run`,
payload,
); );
return res.data; return res.data;
}, },
waitJob: async (id: number): Promise<JobWaitResponse> => {
const res = await apiClient.post<JobWaitResponse>(`/jobs/${id}/wait`);
return res.data;
},
}; };
@@ -14,12 +14,10 @@ const FilePickerTree: React.FC<{
onRun?: (path: string) => void; onRun?: (path: string) => void;
}> = ({ node, level, onRun }) => { }> = ({ node, level, onRun }) => {
const expandedFolders = useFilePickerStore((s) => s.expandedFolders); const expandedFolders = useFilePickerStore((s) => s.expandedFolders);
const runningScripts = useFilePickerStore((s) => s.runningScripts);
const toggleFolder = useFilePickerStore((s) => s.toggleFolder); const toggleFolder = useFilePickerStore((s) => s.toggleFolder);
const nodePath = node.path || node.name; const nodePath = node.path || node.name;
const isExpanded = expandedFolders.has(nodePath); const isExpanded = expandedFolders.has(nodePath);
const isRunning = node.type === "file" && runningScripts.has(nodePath);
if (node.type === "file") { if (node.type === "file") {
return ( return (
@@ -27,7 +25,6 @@ const FilePickerTree: React.FC<{
name={node.name} name={node.name}
type="file" type="file"
path={nodePath} path={nodePath}
isSelected={isRunning}
level={level} level={level}
onRun={onRun} onRun={onRun}
/> />
@@ -11,7 +11,6 @@ interface FilePickerItemProps {
name: string; name: string;
type: "file" | "folder"; type: "file" | "folder";
path: string; path: string;
isSelected?: boolean;
isExpanded?: boolean; isExpanded?: boolean;
children?: React.ReactNode; children?: React.ReactNode;
level: number; level: number;
@@ -24,7 +23,6 @@ export const FilePickerItem: React.FC<FilePickerItemProps> = ({
name, name,
type, type,
path, path,
isSelected,
isExpanded, isExpanded,
children, children,
level, level,
@@ -131,12 +129,10 @@ export const FilePickerItem: React.FC<FilePickerItemProps> = ({
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
padding: "4px", padding: "4px",
backgroundColor: isSelected ? "#238636" : "transparent", backgroundColor: "transparent",
border: isSelected border: "1px solid transparent",
? "1px solid #2ea043"
: "1px solid transparent",
borderRadius: "3px", borderRadius: "3px",
color: isSelected ? "#ffffff" : "var(--text-secondary)", color: "var(--text-secondary)",
cursor: "pointer", cursor: "pointer",
flexShrink: 0, flexShrink: 0,
transition: "all 0.15s", transition: "all 0.15s",
@@ -151,15 +147,9 @@ export const FilePickerItem: React.FC<FilePickerItemProps> = ({
e.currentTarget.style.borderColor = "#2ea043"; e.currentTarget.style.borderColor = "#2ea043";
}} }}
onMouseLeave={(e) => { onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = isSelected e.currentTarget.style.backgroundColor = "transparent";
? "#238636" e.currentTarget.style.color = "var(--text-secondary)";
: "transparent"; e.currentTarget.style.borderColor = "transparent";
e.currentTarget.style.color = isSelected
? "#ffffff"
: "var(--text-secondary)";
e.currentTarget.style.borderColor = isSelected
? "#2ea043"
: "transparent";
}} }}
title="Run script" title="Run script"
> >
@@ -0,0 +1,309 @@
import React, { useState, useRef, useEffect } from "react";
import { MdClose } from "react-icons/md";
import { scriptsApi } from "../api/scripts.api";
import { useTerminalStore } from "@/modules/terminal/store/useTerminalStore";
import { useAgentStore } from "@/app/providers/layout/store/agent.store";
interface RunScriptModalProps {
scriptPath: string;
scriptId: number;
onClose: () => void;
}
export const RunScriptModal: React.FC<RunScriptModalProps> = ({
scriptPath,
scriptId,
onClose,
}) => {
const [selectedAgentIdx, setSelectedAgentIdx] = useState(0);
const [stdinValue, setStdinValue] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const inputRef = useRef<HTMLSelectElement>(null);
const agents = useAgentStore((s) => s.agents);
const addJob = useTerminalStore((s) => s.addJob);
const openTerminal = useTerminalStore((s) => s.openTerminal);
const selectedAgent = agents[selectedAgentIdx];
useEffect(() => {
inputRef.current?.focus();
}, []);
const handleRun = async () => {
if (!selectedAgent) {
setError("No agents available");
return;
}
setLoading(true);
setError(null);
try {
// 1. Запускаем скрипт
const runResult = await scriptsApi.runScript(scriptId, {
stdin: stdinValue,
token: selectedAgent.token,
});
// 2. Добавляем джоб в терминал
addJob({
id: runResult.id,
scriptPath,
command: runResult.command,
});
// 3. Открываем терминал
openTerminal();
// 4. Ждём завершения по id
const jobResult = await scriptsApi.waitJob(runResult.id);
// 5. Обновляем джоб
addJob({
id: jobResult.id,
scriptPath,
command: jobResult.command,
stdin: jobResult.stdin,
});
// Обновляем с финальным статусом
const terminalStore = useTerminalStore.getState();
terminalStore.updateJob(jobResult.id, {
status: jobResult.status,
stdout: jobResult.stdout,
stderr: jobResult.stderr,
stdin: jobResult.stdin,
isRunning: false,
});
onClose();
} catch (e: any) {
console.error("Failed to run script:", e);
setError(e?.response?.data?.detail || "Failed to run script");
} finally {
setLoading(false);
}
};
return (
<div
style={{
position: "fixed",
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: "rgba(0, 0, 0, 0.5)",
display: "flex",
alignItems: "center",
justifyContent: "center",
zIndex: 2000,
}}
onClick={onClose}
>
<div
style={{
backgroundColor: "var(--card-bg)",
border: "1px solid var(--border)",
borderRadius: "8px",
width: "420px",
maxWidth: "90vw",
boxShadow: "0 8px 32px rgba(0, 0, 0, 0.4)",
}}
onClick={(e) => e.stopPropagation()}
>
{/* Header */}
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: "16px 20px",
borderBottom: "1px solid var(--border)",
}}
>
<h3
style={{
margin: 0,
color: "var(--text-primary)",
fontSize: "14px",
fontWeight: 600,
}}
>
Run Script
</h3>
<button
onClick={onClose}
style={{
background: "transparent",
border: "none",
color: "var(--text-secondary)",
cursor: "pointer",
padding: "4px",
borderRadius: "4px",
display: "flex",
}}
>
<MdClose size={18} />
</button>
</div>
{/* Content */}
<div style={{ padding: "20px" }}>
{/* Script path */}
<div style={{ marginBottom: "16px" }}>
<label
style={{
display: "block",
color: "var(--text-secondary)",
fontSize: "12px",
marginBottom: "6px",
}}
>
Script
</label>
<div
style={{
padding: "8px 12px",
backgroundColor: "var(--bg-secondary)",
borderRadius: "4px",
color: "var(--text-primary)",
fontSize: "13px",
fontFamily: "monospace",
border: "1px solid var(--border)",
}}
>
{scriptPath}
</div>
</div>
{/* Agent selector */}
<div style={{ marginBottom: "16px" }}>
<label
style={{
display: "block",
color: "var(--text-secondary)",
fontSize: "12px",
marginBottom: "6px",
}}
>
Agent <span style={{ color: "#f44747" }}>*</span>
</label>
<select
ref={inputRef}
value={selectedAgentIdx}
onChange={(e) => setSelectedAgentIdx(Number(e.target.value))}
style={{
width: "100%",
padding: "8px 12px",
backgroundColor: "var(--input-bg)",
border: "1px solid var(--border)",
borderRadius: "4px",
color: "var(--text-primary)",
fontSize: "13px",
outline: "none",
}}
>
{agents.length === 0 && (
<option value="">No agents available</option>
)}
{agents.map((agent, idx) => (
<option key={agent.label} value={idx}>
{agent.label}
</option>
))}
</select>
</div>
{/* Stdin (optional) */}
<div style={{ marginBottom: "16px" }}>
<label
style={{
display: "block",
color: "var(--text-secondary)",
fontSize: "12px",
marginBottom: "6px",
}}
>
Stdin <span style={{ color: "#858585" }}>(optional)</span>
</label>
<textarea
value={stdinValue}
onChange={(e) => setStdinValue(e.target.value)}
placeholder="Enter input data..."
rows={4}
style={{
width: "100%",
padding: "8px 12px",
backgroundColor: "var(--input-bg)",
border: "1px solid var(--border)",
borderRadius: "4px",
color: "var(--text-primary)",
fontSize: "13px",
fontFamily: "monospace",
resize: "vertical",
outline: "none",
}}
/>
</div>
{/* Error */}
{error && (
<div
style={{
padding: "8px 12px",
backgroundColor: "rgba(244, 71, 71, 0.1)",
border: "1px solid #f44747",
borderRadius: "4px",
color: "#f44747",
fontSize: "12px",
marginBottom: "16px",
}}
>
{error}
</div>
)}
{/* Run button */}
<button
onClick={handleRun}
disabled={loading || !selectedAgent}
style={{
width: "100%",
padding: "10px",
backgroundColor: loading || !selectedAgent ? "#555" : "#0e639c",
border: "none",
borderRadius: "4px",
color: "#ffffff",
fontSize: "13px",
fontWeight: 500,
cursor: loading || !selectedAgent ? "not-allowed" : "pointer",
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: "8px",
}}
>
{loading ? (
<>
<span
style={{
display: "inline-block",
animation: "spin 1s linear infinite",
}}
>
</span>
Running...
</>
) : (
<> Run</>
)}
</button>
</div>
</div>
</div>
);
};
@@ -1,23 +1,19 @@
import { create } from "zustand"; import { create } from "zustand";
import { scriptsApi } from "../api/scripts.api";
interface FilePickerState { interface FilePickerState {
selectedPaths: Set<string>; selectedPaths: Set<string>;
expandedFolders: Set<string>; expandedFolders: Set<string>;
runningScripts: Map<string, { id: number; status: string }>;
toggleSelection: (path: string) => void; toggleSelection: (path: string) => void;
selectAll: (paths: string[]) => void; selectAll: (paths: string[]) => void;
clearSelection: () => void; clearSelection: () => void;
toggleFolder: (path: string) => void; toggleFolder: (path: string) => void;
getSelectedPaths: () => string[]; getSelectedPaths: () => string[];
runScript: (path: string) => Promise<void>;
} }
export const useFilePickerStore = create<FilePickerState>((set, get) => ({ export const useFilePickerStore = create<FilePickerState>((set, get) => ({
selectedPaths: new Set(), selectedPaths: new Set(),
expandedFolders: new Set(), expandedFolders: new Set(),
runningScripts: new Map(),
toggleSelection: (path: string) => { toggleSelection: (path: string) => {
set((state) => { set((state) => {
@@ -58,17 +54,4 @@ export const useFilePickerStore = create<FilePickerState>((set, get) => ({
getSelectedPaths: () => { getSelectedPaths: () => {
return Array.from(get().selectedPaths); return Array.from(get().selectedPaths);
}, },
runScript: async (path: string) => {
try {
const result = await scriptsApi.run(path);
set((state) => {
const newMap = new Map(state.runningScripts);
newMap.set(path, result);
return { runningScripts: newMap };
});
} catch (e) {
console.error("Failed to run script:", e);
}
},
})); }));
@@ -0,0 +1,262 @@
import React from "react";
import { useTerminalStore } from "../store/useTerminalStore";
import { MdClose, MdClearAll } from "react-icons/md";
import { FiTerminal } from "react-icons/fi";
export const TerminalOutput: React.FC = () => {
const {
jobs,
isOpen,
activeJobId,
closeTerminal,
setActiveJob,
clearJobs,
removeJob,
} = useTerminalStore();
if (!isOpen) return null;
const activeJob = jobs.find((j) => j.id === activeJobId) || jobs[jobs.length - 1];
return (
<div
style={{
height: "100%",
display: "flex",
flexDirection: "column",
backgroundColor: "#1e1e1e",
borderTop: "1px solid #3e3e42",
}}
>
{/* Terminal header */}
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: "0 12px",
height: "35px",
borderBottom: "1px solid #3e3e42",
backgroundColor: "#252526",
}}
>
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<FiTerminal size={14} color="#bbbbbb" />
<span
style={{
color: "#bbbbbb",
fontWeight: 500,
fontSize: "11px",
letterSpacing: "0.8px",
}}
>
TERMINAL
</span>
{jobs.length > 0 && (
<span
style={{
color: "#858585",
fontSize: "11px",
backgroundColor: "#3c3c3c",
padding: "2px 8px",
borderRadius: "10px",
}}
>
{jobs.length}
</span>
)}
</div>
<div style={{ display: "flex", gap: "4px", alignItems: "center" }}>
{jobs.length > 0 && (
<button
onClick={clearJobs}
style={{
background: "transparent",
border: "none",
color: "#858585",
cursor: "pointer",
padding: "4px",
borderRadius: "4px",
display: "flex",
alignItems: "center",
}}
title="Clear all"
>
<MdClearAll size={14} />
</button>
)}
<button
onClick={closeTerminal}
style={{
background: "transparent",
border: "none",
color: "#858585",
cursor: "pointer",
padding: "4px",
borderRadius: "4px",
display: "flex",
alignItems: "center",
}}
title="Close"
>
<MdClose size={14} />
</button>
</div>
</div>
{/* Job tabs */}
{jobs.length > 1 && (
<div
style={{
display: "flex",
backgroundColor: "#2d2d2d",
borderBottom: "1px solid #3e3e42",
overflowX: "auto",
}}
>
{jobs.map((job) => (
<button
key={job.id}
onClick={() => setActiveJob(job.id)}
style={{
padding: "6px 16px",
backgroundColor:
job.id === activeJobId ? "#1e1e1e" : "transparent",
border: "none",
borderBottom:
job.id === activeJobId
? "2px solid #0e639c"
: "2px solid transparent",
color: job.isRunning ? "#cccccc" : "#858585",
fontSize: "12px",
cursor: "pointer",
display: "flex",
alignItems: "center",
gap: "6px",
whiteSpace: "nowrap",
}}
>
<span
style={{
width: "8px",
height: "8px",
borderRadius: "50%",
backgroundColor: job.isRunning ? "#4ec9b0" : "#858585",
display: "inline-block",
}}
/>
{job.scriptPath.split("/").pop()}
</button>
))}
</div>
)}
{/* Terminal output */}
<div
style={{
flex: 1,
overflowY: "auto",
padding: "12px",
fontFamily: "'Consolas', 'Courier New', monospace",
fontSize: "13px",
lineHeight: "1.5",
}}
>
{activeJob ? (
<>
{/* Command header */}
<div style={{ marginBottom: "8px" }}>
<span style={{ color: "#6a9955" }}>$ </span>
<span style={{ color: "#cccccc" }}>
{activeJob.command.join(" ")}
</span>
</div>
{/* Stdin if provided */}
{activeJob.stdin && (
<div
style={{
marginBottom: "8px",
padding: "8px",
backgroundColor: "#2d2d2d",
borderRadius: "4px",
borderLeft: "3px solid #0e639c",
}}
>
<span style={{ color: "#858585" }}>stdin: </span>
<pre
style={{
margin: 0,
color: "#cccccc",
whiteSpace: "pre-wrap",
wordBreak: "break-word",
}}
>
{activeJob.stdin}
</pre>
</div>
)}
{/* Stdout */}
{activeJob.stdout && (
<pre
style={{
margin: "0 0 8px 0",
color: "#cccccc",
whiteSpace: "pre-wrap",
wordBreak: "break-word",
}}
>
{activeJob.stdout}
</pre>
)}
{/* Stderr */}
{activeJob.stderr && (
<pre
style={{
margin: "0 0 8px 0",
color: "#f44747",
whiteSpace: "pre-wrap",
wordBreak: "break-word",
}}
>
{activeJob.stderr}
</pre>
)}
{/* Status */}
{activeJob.isRunning ? (
<div style={{ color: "#4ec9b0" }}> Running...</div>
) : activeJob.status !== null ? (
<div
style={{
color: activeJob.status === 0 ? "#4ec9b0" : "#f44747",
}}
>
{activeJob.status === 0
? "✓ Process exited with code 0"
: `✗ Process exited with code ${activeJob.status}`}
</div>
) : null}
</>
) : (
<div
style={{
color: "#858585",
display: "flex",
alignItems: "center",
justifyContent: "center",
height: "100%",
flexDirection: "column",
gap: "8px",
}}
>
<FiTerminal size={32} />
<span>No active jobs</span>
</div>
)}
</div>
</div>
);
};
+1
View File
@@ -0,0 +1 @@
export { TerminalOutput } from "./components/TerminalOutput";
@@ -0,0 +1,75 @@
import { create } from "zustand";
export interface TerminalJob {
id: number;
scriptPath: string;
command: string[];
status: number | null;
stdout: string;
stderr: string;
stdin: string;
isRunning: boolean;
}
interface TerminalState {
jobs: TerminalJob[];
isOpen: boolean;
activeJobId: number | null;
openTerminal: () => void;
closeTerminal: () => void;
addJob: (job: Omit<TerminalJob, "status" | "stdout" | "stderr" | "isRunning">) => void;
updateJob: (id: number, updates: Partial<TerminalJob>) => void;
setActiveJob: (id: number | null) => void;
clearJobs: () => void;
removeJob: (id: number) => void;
}
export const useTerminalStore = create<TerminalState>((set) => ({
jobs: [],
isOpen: false,
activeJobId: null,
openTerminal: () => set({ isOpen: true }),
closeTerminal: () => set({ isOpen: false }),
addJob: (job) =>
set((state) => ({
jobs: [
...state.jobs,
{
...job,
status: null,
stdout: "",
stderr: "",
stdin: "",
isRunning: true,
},
],
activeJobId: job.id,
})),
updateJob: (id, updates) =>
set((state) => ({
jobs: state.jobs.map((j) => (j.id === id ? { ...j, ...updates } : j)),
})),
setActiveJob: (id) => set({ activeJobId: id }),
clearJobs: () => set({ jobs: [], activeJobId: null }),
removeJob: (id) =>
set((state) => {
const newJobs = state.jobs.filter((j) => j.id !== id);
return {
jobs: newJobs,
activeJobId:
state.activeJobId === id
? newJobs.length > 0
? newJobs[newJobs.length - 1].id
: null
: state.activeJobId,
};
}),
}));
+71 -29
View File
@@ -2,7 +2,10 @@ 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 { FaSpinner } from "react-icons/fa";
import { FilePicker, useFilePickerStore } from "../modules/ide"; import { FilePicker } from "../modules/ide";
import { RunScriptModal } from "../modules/ide/components/RunScriptModal";
import { TerminalOutput } from "../modules/terminal";
import { useTerminalStore } from "../modules/terminal/store/useTerminalStore";
import type { FileNode } from "../modules/ide"; import type { FileNode } from "../modules/ide";
import { scriptsApi } from "../modules/ide/api/scripts.api"; import { scriptsApi } from "../modules/ide/api/scripts.api";
@@ -40,11 +43,15 @@ const convertTreeToFileNode = (data: any[]): FileNode => {
export const TemplatesPage = () => { export const TemplatesPage = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const selectedPaths = useFilePickerStore((s) => s.selectedPaths);
const runningScripts = useFilePickerStore((s) => s.runningScripts);
const runScript = useFilePickerStore((s) => s.runScript);
const [files, setFiles] = useState<FileNode | null>(null); const [files, setFiles] = useState<FileNode | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [runModal, setRunModal] = useState<{
scriptPath: string;
scriptId: number;
} | null>(null);
const terminalOpen = useTerminalStore((s) => s.isOpen);
const [terminalHeight] = useState(300);
useEffect(() => { useEffect(() => {
scriptsApi scriptsApi
@@ -59,18 +66,22 @@ export const TemplatesPage = () => {
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}, []); }, []);
const handleRun = async (path: string) => { const handleRun = (path: string, id?: number) => {
await runScript(path); if (!id) {
console.warn("Script ID not found for:", path);
return;
}
setRunModal({ scriptPath: path, scriptId: id });
}; };
const runningCount = runningScripts.size;
return ( return (
<div <div
style={{ style={{
height: "100vh", height: "100vh",
position: "relative", position: "relative",
backgroundColor: "var(--bg-primary)", backgroundColor: "var(--bg-primary)",
display: "flex",
flexDirection: "column",
}} }}
> >
{/* Floating header */} {/* Floating header */}
@@ -85,24 +96,6 @@ export const TemplatesPage = () => {
gap: "16px", gap: "16px",
}} }}
> >
{/* Running scripts counter */}
<div
style={{
display: "flex",
alignItems: "center",
gap: "8px",
padding: "6px 12px",
backgroundColor: "var(--card-bg)",
borderRadius: "4px",
border: "1px solid var(--border)",
}}
>
<FiPlay size={13} color="#61c454" />
<span style={{ fontSize: "12px", color: "var(--text-secondary)" }}>
{runningCount} script{runningCount !== 1 ? "s" : ""} running
</span>
</div>
{/* Open in Editor button */} {/* Open in Editor button */}
<button <button
onClick={() => navigate("/ide")} onClick={() => navigate("/ide")}
@@ -132,8 +125,16 @@ export const TemplatesPage = () => {
</button> </button>
</div> </div>
{/* File Picker */} {/* File Picker + Terminal */}
<div style={{ height: "100%", overflow: "hidden" }}> <div
style={{
flex: 1,
display: "flex",
flexDirection: "column",
overflow: "hidden",
}}
>
<div style={{ flex: 1, overflow: "hidden" }}>
{loading ? ( {loading ? (
<div <div
style={{ style={{
@@ -152,9 +153,50 @@ export const TemplatesPage = () => {
/> />
</div> </div>
) : files ? ( ) : files ? (
<FilePicker files={files} onRun={handleRun} /> <FilePicker
files={files}
onRun={(path) => {
// Находим ID скрипта по пути
const findNodeById = (
node: FileNode,
p: string,
): FileNode | null => {
if (node.path === p) return node;
if (node.children) {
for (const child of node.children) {
const found = findNodeById(child, p);
if (found) return found;
}
}
return null;
};
const node = findNodeById(files, path);
if (node?.id) {
handleRun(path, node.id);
} else {
console.warn("Script ID not found for path:", path);
}
}}
/>
) : null} ) : null}
</div> </div>
{/* Terminal */}
{terminalOpen && (
<div style={{ height: `${terminalHeight}px`, flexShrink: 0 }}>
<TerminalOutput />
</div>
)}
</div>
{/* Run Script Modal */}
{runModal && (
<RunScriptModal
scriptPath={runModal.scriptPath}
scriptId={runModal.scriptId}
onClose={() => setRunModal(null)}
/>
)}
</div> </div>
); );
}; };