yggdrasil/pages/admin/
runner.rs1use dioxus::prelude::*;
10
11use crate::components::code_runner::CodeRunner;
12use crate::components::forms::FormInput;
13use crate::infra::runner_config::ResourceLimits;
14const SUPPORTED_LANGS: &[(&str, &str)] = &[
16 ("python", "Python"),
17 ("node", "Node.js"),
18 ("go", "Go"),
19 ("rust", "Rust"),
20 ("bun", "Bun (TS)"),
21];
22fn default_source(lang: &str) -> String {
24 match lang {
25 "python" => "print('Hello from author sandbox')\nfor i in range(3):\n print(f'line {i}')\n".to_string(),
26 "node" => "console.log('Hello from author sandbox');\n[0,1,2].forEach(i => console.log(`line ${i}`));\n".to_string(),
27 "go" => "package main\n\nimport \"fmt\"\n\nfunc main() {\n\tfmt.Println(\"Hello from author sandbox\")\n\tfor i := 0; i < 3; i++ {\n\t\tfmt.Printf(\"line %d\\n\", i)\n\t}\n}\n".to_string(),
28 "rust" => "fn main() {\n println!(\"Hello from author sandbox\");\n for i in 0..3 {\n println!(\"line {}\", i);\n }\n}\n".to_string(),
29 "bun" => "const greeting: string = 'Hello from author sandbox';\nconsole.log(greeting);\n[0, 1, 2].forEach((i: number) => console.log(`line ${i}`));\n".to_string(),
31 _ => String::new(),
32 }
33}
34
35#[component]
37#[cfg_attr(not(target_arch = "wasm32"), allow(unused_mut))]
38pub fn Runner() -> Element {
39 let mut lang = use_signal(|| "python".to_string());
40 let mut source = use_signal(|| default_source("python"));
42 let mut overrides_json = use_signal(String::new);
43
44 let parsed = use_memo(move || {
47 let raw = overrides_json();
48 match serde_json::from_str::<ResourceLimits>(raw.trim()) {
49 Ok(o) => (Some(o), String::new()),
50 Err(_) => {
51 if raw.trim().is_empty() {
52 (None, String::new())
53 } else {
54 (None, "overrides JSON 格式错误,已忽略".to_string())
55 }
56 }
57 }
58 });
59 let (overrides, override_error) = (parsed.read().0.clone(), parsed.read().1.clone());
60
61 rsx! {
62 div { class: "animate-page-enter w-full max-w-7xl mx-auto space-y-8",
63 div { class: "flex flex-col sm:flex-row sm:items-center justify-between gap-4 pb-6 border-b border-[var(--color-paper-border)]/70",
65 div {
66 h1 { class: "text-3xl sm:text-4xl font-extrabold tracking-tight text-[var(--color-paper-primary)]",
67 "代码试运行沙箱"
68 }
69 p { class: "text-sm text-[var(--color-paper-secondary)] mt-1.5",
70 "在线运行与调试多语言代码,实时验证文章内交互代码块的预期输出"
71 }
72 }
73 div { class: "flex items-center gap-2.5",
74 div { class: "inline-flex items-center gap-1.5 px-3.5 py-1.5 rounded-full text-xs font-mono bg-[var(--color-paper-entry)] text-[var(--color-paper-secondary)] border border-[var(--color-paper-border)]/70 shadow-2xs",
75 span { class: "w-1.5 h-1.5 rounded-full bg-[var(--color-paper-accent)]" }
76 span { "Docker 沙箱环境" }
77 }
78 }
79 }
80
81 div { class: "bg-[var(--color-paper-entry)]/40 rounded-2xl shadow-xs border border-[var(--color-paper-border)]/70 p-6 sm:p-8 flex flex-col gap-6",
83 div { class: "flex items-center gap-2.5 border-b border-[var(--color-paper-border)]/60 pb-4",
85 svg {
86 class: "w-5 h-5 text-[var(--color-paper-accent)]",
87 xmlns: "http://www.w3.org/2000/svg",
88 view_box: "0 0 24 24",
89 fill: "none",
90 stroke: "currentColor",
91 stroke_width: "2",
92 stroke_linecap: "round",
93 stroke_linejoin: "round",
94 path { d: "M4 17l6-6-6-6" }
95 path { d: "M12 19h8" }
96 }
97 h2 { class: "text-lg sm:text-xl font-bold text-[var(--color-paper-primary)]",
98 "运行环境与配置"
99 }
100 }
101
102 div { class: "flex flex-col gap-2.5",
104 label { class: "text-xs font-semibold uppercase tracking-wider text-[var(--color-paper-secondary)]",
105 "编程语言"
106 }
107 div { class: "flex flex-wrap gap-2.5",
108 for (idx, (l, label_text)) in SUPPORTED_LANGS.iter().enumerate() {
109 button {
110 key: "{l}",
111 class: format!(
112 "animate-row-enter inline-flex items-center gap-1.5 px-4 py-1.5 text-xs font-medium rounded-full transition-all cursor-pointer {}",
113 if lang() == *l {
114 "bg-[var(--color-paper-accent)] text-[var(--color-paper-theme)] shadow-2xs font-semibold"
115 } else {
116 "text-[var(--color-paper-secondary)] bg-[var(--color-paper-entry)] hover:bg-[var(--color-paper-theme)] hover:text-[var(--color-paper-primary)] border border-[var(--color-paper-border)]/70"
117 },
118 ),
119 style: "animation-delay: {idx * 40}ms",
120 onclick: {
121 let ll = (*l).to_string();
122 move |_| {
123 if ll != lang() {
124 lang.set(ll.clone());
125 source.set(default_source(&ll));
126 }
127 }
128 },
129 span { "{label_text}" }
130 }
131 }
132 }
133 }
134
135 div { class: "flex flex-col gap-2",
137 div { class: "flex items-center justify-between",
138 label { class: "text-xs font-semibold uppercase tracking-wider text-[var(--color-paper-secondary)]",
139 "资源限制覆盖 (JSON, 可选)"
140 }
141 span { class: "text-[11px] text-[var(--color-paper-tertiary)] font-mono",
142 "timeout_secs / memory_mb / cpu_cores"
143 }
144 }
145 FormInput {
146 r#type: "text",
147 placeholder: "如 {{\"timeout_secs\":10,\"memory_mb\":512}}",
148 value: overrides_json(),
149 mono: true,
150 oninput: move |v: String| overrides_json.set(v),
151 }
152 if !override_error.is_empty() {
153 p { class: "text-xs text-red-500 dark:text-red-400 font-medium", "{override_error}" }
154 } else {
155 p { class: "text-xs text-[var(--color-paper-tertiary)] leading-normal",
156 "支持覆盖 cpu_cores / memory_mb / timeout_secs / output_bytes / allow_network;最终仍受服务端全局上限约束"
157 }
158 }
159 }
160 }
161 for lang_key in std::iter::once(lang().clone()) {
171 div { key: "{lang_key}", class: "animate-section-enter",
172 CodeRunner {
173 source: source(),
174 language: lang(),
175 overrides: overrides.clone(),
176 instance_id: 0,
177 }
178 }
179 }
180 }
181 }
182}