Skip to main content

yggdrasil/pages/admin/
runner.rs

1//! 管理后台「代码试运行」页面。
2//!
3//! 作者在写作时可在此沙箱快速试运行代码(验证围栏 ` ```lang runnable ` 的预期输出),
4//! 而无需进入文章渲染后才能运行。沙箱使用与读者相同的 StartExec / GetExecResult
5//! 接口,受同一套资源钳制约束(admin 跳过速率限制,见 `start_exec`)。
6//!
7//! 仅 WASM 前端交互;语言在受支持集合内切换。
8
9use 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
16/// 受支持的语言集合(与 LANGUAGES 注册表 / CODE_RUNNER_LANGUAGES 对齐)。
17/// 仅列 canonical key;别名(js/ts/rs 等)经 normalize_lang 归一到此处某项,
18/// 按钮不重复展示别名,避免选择拥挤。
19const SUPPORTED_LANGS: &[&str] = &["python", "node", "go", "rust", "bun"];
20
21/// 默认示例源码(按语言)。
22fn 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 跑 TypeScript:示例用 TS 类型注解体现语言特性。
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(),
30        _ => String::new(),
31    }
32}
33
34/// 管理后台代码试运行页面。
35#[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    // 语言切换时刷新示例源码(首次进入也有默认值)。
40    let mut source = use_signal(|| default_source("python"));
41    let mut overrides_json = use_signal(String::new);
42
43    // overrides 解析用 use_memo 承载:render 体只读不写(Dioxus render purity),
44    // 避免 render 期间 .set() override_error。畸形 JSON 标记在 memo 返回值里。
45    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            // 页头:与 dashboard / posts / system 对齐(h1 text-4xl + 底部分割线)
63            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            // 配置卡片:语言切换 + 资源覆盖
75            div { class: "{ADMIN_CARD_CLASS} p-8 flex flex-col gap-6",
76                // 语言切换
77                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                // 资源覆盖(JSON)
107                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            // 运行器(admin 试运行页单实例、纯客户端渲染,instance_id 固定 0 即可)
129            CodeRunner {
130                source: source(),
131                language: lang(),
132                overrides,
133                instance_id: 0,
134            }
135        }
136    }
137}