yggdrasil/pages/admin/
runner.rs1use dioxus::prelude::*;
10
11use crate::components::code_runner::CodeRunner;
12use crate::components::forms::FormInput;
13use crate::components::ui::{ADMIN_CARD_CLASS, BTN_PRIMARY_SM};
14use crate::infra::runner_config::ResourceLimits;
15
16const SUPPORTED_LANGS: &[&str] = &["python", "node", "go", "rust", "bun"];
20
21fn default_source(lang: &str) -> String {
23 match lang {
24 "python" => "print('Hello from author sandbox')\nfor i in range(3):\n print(f'line {i}')\n".to_string(),
25 "node" => "console.log('Hello from author sandbox');\n[0,1,2].forEach(i => console.log(`line ${i}`));\n".to_string(),
26 "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(),
27 "rust" => "fn main() {\n println!(\"Hello from author sandbox\");\n for i in 0..3 {\n println!(\"line {}\", i);\n }\n}\n".to_string(),
28 "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(),
30 _ => String::new(),
31 }
32}
33
34#[component]
36#[cfg_attr(not(target_arch = "wasm32"), allow(unused_mut))]
37pub fn Runner() -> Element {
38 let mut lang = use_signal(|| "python".to_string());
39 let mut source = use_signal(|| default_source("python"));
41 let mut overrides_json = use_signal(String::new);
42
43 let parsed = use_memo(move || {
46 let raw = overrides_json();
47 match serde_json::from_str::<ResourceLimits>(raw.trim()) {
48 Ok(o) => (Some(o), String::new()),
49 Err(_) => {
50 if raw.trim().is_empty() {
51 (None, String::new())
52 } else {
53 (None, "overrides JSON 格式错误,已忽略".to_string())
54 }
55 }
56 }
57 });
58 let (overrides, override_error) = (parsed.read().0.clone(), parsed.read().1.clone());
59
60 rsx! {
61 div { class: "w-full max-w-7xl mx-auto space-y-8",
62 div { class: "flex flex-col md:flex-row md:items-end justify-between gap-6 pb-8 border-b border-[var(--color-paper-border)]/50",
64 div {
65 h1 { class: "text-4xl font-extrabold tracking-tight text-[var(--color-paper-primary)]",
66 "代码试运行沙箱"
67 }
68 p { class: "text-base text-[var(--color-paper-secondary)] mt-2",
69 "在此快速试运行代码,验证文章中可运行代码块的预期输出。资源钳制与读者侧一致,速率限制对 admin 放行。"
70 }
71 }
72 }
73
74 div { class: "{ADMIN_CARD_CLASS} p-8 flex flex-col gap-6",
76 div { class: "flex flex-col gap-2",
78 label { class: "text-sm font-medium text-[var(--color-paper-secondary)]",
79 "语言"
80 }
81 div { class: "flex gap-2",
82 for l in SUPPORTED_LANGS {
83 button {
84 key: "{l}",
85 class: (if lang() == *l {
86 BTN_PRIMARY_SM
87 } else {
88 "px-4 py-1.5 text-sm font-medium rounded-full text-[var(--color-paper-secondary)] bg-[var(--color-paper-theme)] hover:bg-[var(--color-paper-border)] hover:text-[var(--color-paper-primary)] transition cursor-pointer"
89 })
90 .to_string(),
91 onclick: {
92 let ll = (*l).to_string();
93 move |_| {
94 if ll != lang() {
95 lang.set(ll.clone());
96 source.set(default_source(&ll));
97 }
98 }
99 },
100 "{l}"
101 }
102 }
103 }
104 }
105
106 div { class: "flex flex-col gap-2",
108 label { class: "text-sm font-medium text-[var(--color-paper-secondary)]",
109 "资源覆盖 (JSON, 可选)"
110 }
111 FormInput {
112 r#type: "text",
113 placeholder: "如 {{\"timeout_secs\":10,\"memory_mb\":512}}",
114 value: overrides_json(),
115 mono: true,
116 oninput: move |v: String| overrides_json.set(v),
117 }
118 if !override_error.is_empty() {
119 p { class: "text-xs text-red-500 dark:text-red-400", "{override_error}" }
120 } else {
121 p { class: "text-xs text-[var(--color-paper-tertiary)]",
122 "覆盖 cpu_cores / memory_mb / timeout_secs / output_bytes / allow_network;最终仍受 CODE_RUNNER_MAX_* 钳制"
123 }
124 }
125 }
126 }
127
128 CodeRunner {
130 source: source(),
131 language: lang(),
132 overrides,
133 instance_id: 0,
134 }
135 }
136 }
137}