1use dioxus::prelude::*;
3
4use crate::api::writing_guide::{get_writing_guide, SKILL_SOURCE, SKILL_URL};
5use crate::router::Route;
6
7const CLIENTS: [(&str, &str, &str); 3] = [
8 (
9 "Codex",
10 "~/.codex/config.toml",
11 "https://developers.openai.com/codex/mcp",
12 ),
13 (
14 "OMP",
15 "~/.omp/agent/mcp.json",
16 "https://github.com/can1357/oh-my-pi/blob/main/docs/mcp-config.md",
17 ),
18 (
19 "OpenCode",
20 "~/.config/opencode/opencode.json",
21 "https://opencode.ai/docs/mcp-servers/",
22 ),
23];
24
25#[component]
26pub fn WritingGuide() -> Element {
27 let response = use_server_future(get_writing_guide)?;
28 let mut selected = use_signal(|| 0usize);
29 let data = match response.read().as_ref() {
30 Some(Ok(data)) => data.clone(),
31 Some(Err(error)) => return Err(error.clone().into()),
32 None => return rsx! { p { role: "status", "正在载入写作指南…" } },
33 };
34 let current = selected();
35 let (client, config_path, docs_url) = CLIENTS[current];
36
37 rsx! {
38 document::Title { "Agent 写作指南 · Yggdrasil" }
39 article { class: "writing-page",
40 Link { class: "writing-back", to: Route::About {}, "← 关于这里" }
41 header { class: "writing-hero",
42 div {
43 p { class: "writing-eyebrow", span {} "A FIELD GUIDE FOR AGENTS" }
44 h1 { "让灵感生根," br {} em { "让 Agent 懂得书写。" } }
45 p { class: "writing-lead", "一份写作技能,连接你的 AI 与这棵世界树。" br {} "从一段能运行的代码,到一张讲清思路的图。" }
46 div { class: "writing-actions",
47 CopyButton { text: SKILL_SOURCE.to_string(), label: "复制 Skill 源码", primary: true }
48 a { class: "writing-download", href: SKILL_URL, download: "SKILL.md", "下载 SKILL.md ↓" }
49 }
50 }
51 div { class: "writing-emblem", aria_hidden: "true",
52 span { class: "writing-emblem-orbit" }
53 span { class: "writing-emblem-star", "✳" }
54 span { class: "writing-emblem-code", "{{ }}" }
55 span { class: "writing-emblem-caption", "WORDS → WORLDS" }
56 }
57 }
58 div { class: "writing-capabilities", aria_label: "支持的写作功能",
59 for (symbol, label) in [("⌘", "可运行代码"), ("⑂", "Mermaid 图表"), ("∑", "数学与化学公式"), ("↗", "MCP 写作") ] {
60 span { span { aria_hidden: "true", "{symbol}" } "{label}" }
61 }
62 }
63
64 section { id: "connect", class: "writing-connect", aria_labelledby: "connect-title",
65 div { class: "writing-section-heading",
66 div { p { class: "writing-eyebrow", "01 / GET CONNECTED" } h2 { id: "connect-title", "给你的 Agent,一份写作地图" } }
67 a { href: "#skill", class: "writing-text-link", "先读技能全文 ↓" }
68 }
69 section { class: "writing-install", aria_labelledby: "install-title",
70 div { class: "writing-install-heading",
71 h3 { id: "install-title", span { aria_hidden: "true", "↓" } "一句话,让 Agent 安装" }
72 span { class: "writing-install-badge", "用户全局" }
73 }
74 p { class: "writing-install-description", "复制后发送给你的 Agent,一次安装,在不同项目中使用。" }
75 p { class: "writing-install-prompt", "{data.install_prompt}" }
76 div { class: "writing-install-footer",
77 span { "安装到用户目录,跨项目可用" }
78 CopyButton { text: data.install_prompt.clone(), label: "复制安装指令", primary: true }
79 }
80 }
81 div { class: "writing-steps",
82 div { span { "01" } h3 { "全局安装" } p { "将上方指令发给 Agent,或手动保存到" } code { "~/.agents/skills/yggdrasil-writing/SKILL.md" } p { "Codex、OMP 与 OpenCode 均支持此用户级目录。" } }
83 div { span { "02" } h3 { "连接站点" } p { "将下方配置合并到客户端配置文件,并替换令牌占位符。" } Link { to: Route::Mcp {}, class: "writing-text-link", "管理 MCP 令牌 ↗" } }
84 div { span { "03" } h3 { "开始写作" } p { "让 Agent 使用 yggdrasil-writing,写一篇带可运行示例的文章,先保存为草稿。" } }
85 }
86 div { class: "writing-config",
87 div { class: "writing-config-tabs", role: "group", aria_label: "选择 Agent 客户端",
88 for (index, (name, _, _)) in CLIENTS.iter().enumerate() {
89 button {
90 r#type: "button",
91 aria_pressed: "{current == index}",
92 class: if current == index { "is-selected" } else { "" },
93 onclick: move |_| selected.set(index),
94 "{name}"
95 }
96 }
97 span { "MCP / STREAMABLE HTTP" }
98 }
99 div { class: "writing-config-meta",
100 code { "{config_path}" }
101 CopyButton { key: "{client}", text: data.configs[current].clone(), label: "复制配置" }
102 }
103 pre { class: "writing-config-code", aria_label: "{client} MCP 配置", code { "{data.configs[current]}" } }
104 div { class: "writing-config-footer",
105 p { "将 YOUR_YGGDRASIL_TOKEN 替换为站点签发的令牌。" }
106 a { href: docs_url, target: "_blank", rel: "noopener noreferrer", "{client} 官方文档 ↗" }
107 }
108 }
109 p { class: "writing-permissions", "技能可以公开阅读;通过 MCP 保存文章需要站点管理员签发的 write 令牌,执行代码验证需要 admin 权限。已有配置请合并,保留其他服务。" }
110 details { class: "writing-sources",
111 summary { "技能安装参考" }
112 a { href: "https://developers.openai.com/codex/skills", target: "_blank", rel: "noopener noreferrer", "Codex Skills ↗" }
113 a { href: "https://github.com/can1357/oh-my-pi/blob/main/docs/skills.md", target: "_blank", rel: "noopener noreferrer", "OMP Skills ↗" }
114 a { href: "https://opencode.ai/docs/skills/", target: "_blank", rel: "noopener noreferrer", "OpenCode Skills ↗" }
115 }
116 }
117
118 section { id: "skill", class: "writing-document", aria_labelledby: "skill-title",
119 div { class: "writing-section-heading",
120 div { p { class: "writing-eyebrow", "02 / THE SKILL" } h2 { id: "skill-title", "Yggdrasil 文章写作" } }
121 span { class: "writing-file-label", "SKILL.md" }
122 }
123 details { class: "writing-toc",
124 summary { "本页技能目录" span { aria_hidden: "true", "+" } }
125 nav { aria_label: "技能目录", dangerous_inner_html: data.toc_html }
126 }
127 div { class: "md-content writing-content", dangerous_inner_html: data.html }
128 footer { class: "writing-document-footer",
129 p { "准备好了,就让下一篇文章在这里生长。" }
130 CopyButton { text: SKILL_SOURCE.to_string(), label: "复制 Skill 源码", primary: true }
131 }
132 }
133 }
134 }
135}
136
137#[component]
139fn CopyButton(text: String, label: &'static str, #[props(default)] primary: bool) -> Element {
140 #[allow(unused_mut)]
141 let mut result = use_signal(|| None::<bool>);
142 rsx! {
143 span { class: "writing-copy-wrap",
144 button {
145 r#type: "button",
146 class: if primary { "writing-copy writing-copy-primary" } else { "writing-copy" },
147 onclick: move |_| {
148 let text = text.clone();
149 async move {
150 #[cfg(target_arch = "wasm32")]
151 {
152 use wasm_bindgen::JsCast;
153 let clipboard = web_sys::window().and_then(|window| {
154 js_sys::Reflect::get(window.navigator().as_ref(), &"clipboard".into())
155 .ok()?
156 .dyn_into::<web_sys::Clipboard>()
157 .ok()
158 });
159 let success = if let Some(clipboard) = clipboard {
160 wasm_bindgen_futures::JsFuture::from(clipboard.write_text(&text)).await.is_ok()
161 } else { false };
162 result.set(Some(success));
163 }
164 #[cfg(not(target_arch = "wasm32"))]
165 let _ = text;
166 }
167 },
168 span { aria_hidden: "true", if result() == Some(true) { "✓" } else { "⧉" } }
169 "{label}"
170 }
171 span { class: "writing-copy-status", role: "status", aria_live: "polite",
172 match result() {
173 Some(true) => "已复制",
174 Some(false) => "复制失败,请下载或手动复制",
175 None => "",
176 }
177 }
178 }
179 }
180}