[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill-9c084087-5f65-4fef-9653-20422b5ba8db":3,"$fzByyQlj1vVa2WBemeoGmWDz0bXiafOdZewPn29B1Zpc":43},{"id":4,"title":5,"description":6,"categoryId":7,"moduleId":8,"tags":9,"prompt":10,"icon":11,"source":12,"sourceUrl":13,"authorId":14,"authorName":15,"isPublic":16,"stars":17,"runs":18,"createdAt":19,"updatedAt":19,"module":20,"category":27,"packages":34},"9c084087-5f65-4fef-9653-20422b5ba8db","molykit","|","cat_life_career","mod_other","sickn33,other","---\nname: molykit\ndescription: |\n  CRITICAL: Use for MolyKit AI chat toolkit. Triggers on:\n  BotClient, OpenAI, SSE streaming, AI chat, molykit,\n  PlatformSend, spawn(), ThreadToken, cross-platform async,\n  Chat widget, Messages, PromptInput, Avatar, LLM\nrisk: unknown\nsource: community\n---\n\n# MolyKit Skill\n\nBest practices for building AI chat interfaces with Makepad using MolyKit - a toolkit for cross-platform AI chat applications.\n\n**Source codebase**: `\u002FUsers\u002Fzhangalex\u002FWork\u002FProjects\u002FFW\u002Frobius\u002Fmoly\u002Fmoly-kit`\n\n## When to Use\nUse this skill when:\n- Building AI chat interfaces with Makepad\n- Integrating OpenAI or other LLM APIs\n- Implementing cross-platform async for native and WASM\n- Creating chat widgets (messages, prompts, avatars)\n- Handling SSE streaming responses\n- Keywords: molykit, moly-kit, ai chat, bot client, openai makepad, chat widget, sse streaming\n\n## Overview\n\nMolyKit provides:\n- Cross-platform async utilities (PlatformSend, spawn(), ThreadToken)\n- Ready-to-use chat widgets (Chat, Messages, PromptInput, Avatar)\n- BotClient trait for AI provider integration\n- OpenAI-compatible client with SSE streaming\n- Protocol types for messages, bots, and tool calls\n- MCP (Model Context Protocol) support\n\n## Cross-Platform Async Patterns\n\n### PlatformSend - Send Only on Native\n\n```rust\n\u002F\u002F\u002F Implies Send only on native platforms, not on WASM\n\u002F\u002F\u002F - On native: implemented by types that implement Send\n\u002F\u002F\u002F - On WASM: implemented by ALL types\npub trait PlatformSend: PlatformSendInner {}\n\n\u002F\u002F\u002F Boxed future type for cross-platform use\npub type BoxPlatformSendFuture\u003C'a, T> = Pin\u003CBox\u003Cdyn PlatformSendFuture\u003COutput = T> + 'a>>;\n\n\u002F\u002F\u002F Boxed stream type for cross-platform use\npub type BoxPlatformSendStream\u003C'a, T> = Pin\u003CBox\u003Cdyn PlatformSendStream\u003CItem = T> + 'a>>;\n```\n\n### Platform-Agnostic Spawning\n\n```rust\n\u002F\u002F\u002F Runs a future independently\n\u002F\u002F\u002F - Uses tokio on native (requires Send)\n\u002F\u002F\u002F - Uses wasm-bindgen-futures on WASM (no Send required)\npub fn spawn(fut: impl PlatformSendFuture\u003COutput = ()> + 'static);\n\n\u002F\u002F Usage\nspawn(async move {\n    let result = fetch_data().await;\n    Cx::post_action(DataReady(result));\n    SignalToUI::set_ui_signal();\n});\n```\n\n### Task Cancellation with AbortOnDropHandle\n\n```rust\n\u002F\u002F\u002F Handle that aborts its future when dropped\npub struct AbortOnDropHandle(AbortHandle);\n\n\u002F\u002F Usage - task cancelled when widget dropped\n#[rust]\ntask_handle: Option\u003CAbortOnDropHandle>,\n\nfn start_task(&mut self) {\n    let (future, handle) = abort_on_drop(async move {\n        \u002F\u002F async work...\n    });\n    self.task_handle = Some(handle);\n    spawn(async move { let _ = future.await; });\n}\n```\n\n### ThreadToken for Non-Send Types on WASM\n\n```rust\n\u002F\u002F\u002F Store non-Send value in thread-local, access via token\npub struct ThreadToken\u003CT: 'static>;\n\nimpl\u003CT> ThreadToken\u003CT> {\n    pub fn new(value: T) -> Self;\n    pub fn peek\u003CR>(&self, f: impl FnOnce(&T) -> R) -> R;\n    pub fn peek_mut\u003CR>(&self, f: impl FnOnce(&mut T) -> R) -> R;\n}\n\n\u002F\u002F Usage - wrap non-Send type for use across Send boundaries\nlet token = ThreadToken::new(non_send_value);\nspawn(async move {\n    token.peek(|value| {\n        \u002F\u002F use value...\n    });\n});\n```\n\n## BotClient Trait\n\n### Implementing AI Provider Integration\n\n```rust\npub trait BotClient: Send {\n    \u002F\u002F\u002F Send message with streamed response\n    fn send(\n        &mut self,\n        bot_id: &BotId,\n        messages: &[Message],\n        tools: &[Tool],\n    ) -> BoxPlatformSendStream\u003C'static, ClientResult\u003CMessageContent>>;\n\n    \u002F\u002F\u002F Get available bots\u002Fmodels\n    fn bots(&self) -> BoxPlatformSendFuture\u003C'static, ClientResult\u003CVec\u003CBot>>>;\n\n    \u002F\u002F\u002F Clone for passing around\n    fn clone_box(&self) -> Box\u003Cdyn BotClient>;\n}\n\n\u002F\u002F Usage\nlet client = OpenAIClient::new(\"https:\u002F\u002Fapi.openai.com\u002Fv1\".into());\nclient.set_key(\"sk-...\")?;\nlet context = BotContext::from(client);\n```\n\n### BotContext - Sharable Wrapper\n\n```rust\n\u002F\u002F\u002F Sharable wrapper with loaded bots for sync UI access\npub struct BotContext(Arc\u003CMutex\u003CInnerBotContext>>);\n\nimpl BotContext {\n    pub fn load(&mut self) -> BoxPlatformSendFuture\u003CClientResult\u003C()>>;\n    pub fn bots(&self) -> Vec\u003CBot>;\n    pub fn get_bot(&self, id: &BotId) -> Option\u003CBot>;\n    pub fn client(&self) -> Box\u003Cdyn BotClient>;\n}\n\n\u002F\u002F Usage\nlet mut context = BotContext::from(client);\nspawn(async move {\n    if let Err(errors) = context.load().await.into_result() {\n        \u002F\u002F handle errors\n    }\n    Cx::post_action(BotsLoaded);\n});\n```\n\n## Protocol Types\n\n### Message Structure\n\n```rust\npub struct Message {\n    pub from: EntityId,         \u002F\u002F User, System, Bot(BotId), App\n    pub metadata: MessageMetadata,\n    pub content: MessageContent,\n}\n\npub struct MessageContent {\n    pub text: String,           \u002F\u002F Main content (markdown)\n    pub reasoning: String,      \u002F\u002F AI reasoning\u002Fthinking\n    pub citations: Vec\u003CString>, \u002F\u002F Source URLs\n    pub attachments: Vec\u003CAttachment>,\n    pub tool_calls: Vec\u003CToolCall>,\n    pub tool_results: Vec\u003CToolResult>,\n}\n\npub struct MessageMetadata {\n    pub is_writing: bool,       \u002F\u002F Still being streamed\n    pub created_at: DateTime\u003CUtc>,\n}\n```\n\n### Bot Identification\n\n```rust\n\u002F\u002F\u002F Globally unique bot ID: \u003Clen>;\u003Cid>@\u003Cprovider>\npub struct BotId(Arc\u003Cstr>);\n\nimpl BotId {\n    pub fn new(id: &str, provider: &str) -> Self;\n    pub fn id(&self) -> &str;       \u002F\u002F provider-local id\n    pub fn provider(&self) -> &str; \u002F\u002F provider domain\n}\n\n\u002F\u002F Example: BotId::new(\"gpt-4\", \"api.openai.com\")\n\u002F\u002F -> \"5;gpt-4@api.openai.com\"\n```\n\n## Widget Patterns\n\n### Slot Widget - Runtime Content Replacement\n\n```rust\nlive_design! {\n    pub Slot = {{Slot}} {\n        width: Fill, height: Fit,\n        slot = \u003CView> {}  \u002F\u002F default content\n    }\n}\n\n\u002F\u002F Usage - replace content at runtime\nlet mut slot = widget.slot(id!(content));\nif let Some(custom) = client.content_widget(cx, ...) {\n    slot.replace(custom);\n} else {\n    slot.restore();  \u002F\u002F back to default\n    slot.default().as_standard_message_content().set_content(cx, &content);\n}\n```\n\n### Avatar Widget - Text\u002FImage Toggle\n\n```rust\nlive_design! {\n    pub Avatar = {{Avatar}} \u003CView> {\n        grapheme = \u003CRoundedView> {\n            visible: false,\n            label = \u003CLabel> { text: \"P\" }\n        }\n        dependency = \u003CRoundedView> {\n            visible: false,\n            image = \u003CImage> {}\n        }\n    }\n}\n\nimpl Widget for Avatar {\n    fn draw_walk(&mut self, cx: &mut Cx2d, ...) -> DrawStep {\n        if let Some(avatar) = &self.avatar {\n            match avatar {\n                Picture::Grapheme(g) => {\n                    self.view(id!(grapheme)).set_visible(cx, true);\n                    self.view(id!(dependency)).set_visible(cx, false);\n                    self.label(id!(label)).set_text(cx, &g);\n                }\n                Picture::Dependency(d) => {\n                    self.view(id!(dependency)).set_visible(cx, true);\n                    self.view(id!(grapheme)).set_visible(cx, false);\n                    self.image(id!(image)).load_image_dep_by_path(cx, d.as_str());\n                }\n            }\n        }\n        self.deref.draw_walk(cx, scope, walk)\n    }\n}\n```\n\n### PromptInput Widget\n\n```rust\n#[derive(Live, Widget)]\npub struct PromptInput {\n    #[deref] deref: CommandTextInput,\n    #[live] pub send_icon: LiveValue,\n    #[live] pub stop_icon: LiveValue,\n    #[rust] pub task: Task,           \u002F\u002F Send or Stop\n    #[rust] pub interactivity: Interactivity,\n}\n\nimpl PromptInput {\n    pub fn submitted(&self, actions: &Actions) -> bool;\n    pub fn reset(&mut self, cx: &mut Cx);\n    pub fn set_send(&mut self);\n    pub fn set_stop(&mut self);\n    pub fn enable(&mut self);\n    pub fn disable(&mut self);\n}\n```\n\n### Messages Widget - Conversation View\n\n```rust\n#[derive(Live, Widget)]\npub struct Messages {\n    #[deref] deref: View,\n    #[rust] pub messages: Vec\u003CMessage>,\n    #[rust] pub bot_context: Option\u003CBotContext>,\n}\n\nimpl Messages {\n    pub fn set_messages(&mut self, messages: Vec\u003CMessage>, scroll_to_bottom: bool);\n    pub fn scroll_to_bottom(&mut self, cx: &mut Cx, triggered_by_stream: bool);\n    pub fn is_at_bottom(&self) -> bool;\n}\n```\n\n## UiRunner Pattern for Async-to-UI\n\n```rust\nimpl Widget for PromptInput {\n    fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {\n        self.deref.handle_event(cx, event, scope);\n        self.ui_runner().handle(cx, event, scope, self);\n\n        if self.button(id!(attach)).clicked(event.actions()) {\n            let ui = self.ui_runner();\n            Attachment::pick_multiple(move |result| match result {\n                Ok(attachments) => {\n                    ui.defer_with_redraw(move |me, cx, _| {\n                        me.attachment_list_ref().write().attachments.extend(attachments);\n                    });\n                }\n                Err(_) => {}\n            });\n        }\n    }\n}\n```\n\n## SSE Streaming\n\n```rust\n\u002F\u002F\u002F Parse SSE byte stream into message stream\npub fn parse_sse\u003CS, B, E>(s: S) -> impl Stream\u003CItem = Result\u003CString, E>>\nwhere\n    S: Stream\u003CItem = Result\u003CB, E>>,\n    B: AsRef\u003C[u8]>,\n{\n    \u002F\u002F Split on \"\\n\\n\", extract \"data:\" content\n    \u002F\u002F Filter comments and [DONE] messages\n}\n\n\u002F\u002F Usage in BotClient::send\nfn send(&mut self, ...) -> BoxPlatformSendStream\u003C...> {\n    let stream = stream! {\n        let response = client.post(url).send().await?;\n        let events = parse_sse(response.bytes_stream());\n\n        for await event in events {\n            let completion: Completion = serde_json::from_str(&event)?;\n            content.text.push_str(&completion.delta.content);\n            yield ClientResult::new_ok(content.clone());\n        }\n    };\n    Box::pin(stream)\n}\n```\n\n## Best Practices\n\n1. **Use PlatformSend for cross-platform**: Same code works on native and WASM\n2. **Use spawn() not tokio::spawn**: Platform-agnostic task spawning\n3. **Use AbortOnDropHandle**: Cancel tasks when widget drops\n4. **Use ThreadToken for non-Send on WASM**: Thread-local storage with token access\n5. **Use Slot for custom content**: Allow BotClient to provide custom widgets\n6. **Use read()\u002Fwrite() pattern**: Safe borrow access via WidgetRef\n7. **Use UiRunner::defer_with_redraw**: Update widget from async context\n8. **Handle ClientResult partial success**: May have value AND errors\n\n## Reference Files\n\n- `llms.txt` - Complete MolyKit API reference\n\n## Limitations\n- Use this skill only when the task clearly matches the scope described above.\n- Do not treat the output as a substitute for environment-specific validation, testing, or expert review.\n- Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.\n","","imported","https:\u002F\u002Fgithub.com\u002Fsickn33\u002Fantigravity-awesome-skills","user_system_seed","SkillOPIC",true,236,915,"2026-05-16 13:29:17",{"id":8,"name":21,"slug":22,"icon":23,"description":24,"sort":25,"createdAt":26},"其他","other","mdi-page-next-outline","其他类型Skill",5,"2026-05-16 12:53:40",{"id":7,"name":28,"slug":29,"icon":30,"description":31,"moduleId":8,"sort":32,"skillCount":33,"createdAt":26},"职场发展","career","mdi-briefcase-outline","面试准备、简历优化、职业规划",4,575,[35],{"id":36,"skillId":4,"version":37,"fileName":38,"fileSize":39,"filePath":40,"fileHash":41,"manifest":42,"createdAt":19},"7289b921-a249-4027-93b2-52cec9938ce4","1.0.0","molykit.zip",4060,"uploads\u002Fskills\u002F9c084087-5f65-4fef-9653-20422b5ba8db\u002Fmolykit.zip","ceec3a6b3b4eecc3c4dd9e65a79627abf2591336ba9d34e9b8284e26b2d4b462","[{\"path\":\"SKILL.md\",\"isDirectory\":false,\"size\":10531}]",{"code":44,"message":45,"data":46},200,"success",{"items":47,"stats":48,"page":51},[],{"averageRating":49,"totalRatings":49,"ratingCounts":50},0,[49,49,49,49,49],{"limit":52,"offset":49,"hasMore":53,"nextOffset":52,"ratedOnly":16},15,false]