diff --git a/mini_agent/cli.py b/mini_agent/cli.py index f060c9c..d2b1423 100644 --- a/mini_agent/cli.py +++ b/mini_agent/cli.py @@ -34,9 +34,15 @@ from mini_agent.tools.base import Tool from mini_agent.tools.bash_tool import BashKillTool, BashOutputTool, BashTool from mini_agent.tools.file_tools import EditTool, ReadTool, WriteTool -from mini_agent.tools.mcp_loader import cleanup_mcp_connections, load_mcp_tools_async, set_mcp_timeout_config from mini_agent.tools.note_tool import SessionNoteTool from mini_agent.tools.skill_tool import create_skill_tools + +# Try to import MCP tools, but don't fail if they're not available +try: + from mini_agent.tools.mcp_loader import cleanup_mcp_connections, load_mcp_tools_async, set_mcp_timeout_config + MCP_AVAILABLE = True +except ImportError: + MCP_AVAILABLE = False from mini_agent.utils import calculate_display_width @@ -193,6 +199,7 @@ def print_help(): help_text = f""" {Colors.BOLD}{Colors.BRIGHT_YELLOW}Available Commands:{Colors.RESET} {Colors.BRIGHT_GREEN}/help{Colors.RESET} - Show this help message + {Colors.BRIGHT_GREEN}/skills{Colors.RESET} - List all installed skills {Colors.BRIGHT_GREEN}/clear{Colors.RESET} - Clear session history (keep system prompt) {Colors.BRIGHT_GREEN}/history{Colors.RESET} - Show current session message count {Colors.BRIGHT_GREEN}/stats{Colors.RESET} - Show session statistics @@ -398,7 +405,7 @@ async def initialize_base_tools(config: Config): print(f"{Colors.YELLOW}⚠️ Failed to load Skills: {e}{Colors.RESET}") # 4. MCP tools (loaded with priority search) - if config.tools.enable_mcp: + if config.tools.enable_mcp and MCP_AVAILABLE: print(f"{Colors.BRIGHT_CYAN}Loading MCP tools...{Colors.RESET}") try: # Apply MCP timeout configuration from config.yaml @@ -426,6 +433,8 @@ async def initialize_base_tools(config: Config): print(f"{Colors.YELLOW}⚠️ MCP config file not found: {config.tools.mcp_config_path}{Colors.RESET}") except Exception as e: print(f"{Colors.YELLOW}⚠️ Failed to load MCP tools: {e}{Colors.RESET}") + elif config.tools.enable_mcp: + print(f"{Colors.YELLOW}⚠️ MCP tools not available (mcp module not installed){Colors.RESET}") print() # Empty line separator return tools, skill_loader @@ -469,18 +478,19 @@ def add_workspace_tools(tools: List[Tool], config: Config, workspace_dir: Path): async def _quiet_cleanup(): """Clean up MCP connections, suppressing noisy asyncgen teardown tracebacks.""" - # Silence the asyncgen finalization noise that anyio/mcp emits when - # stdio_client's task group is torn down across tasks. The handler is - # intentionally NOT restored: asyncgen finalization happens during - # asyncio.run() shutdown (after run_agent returns), so restoring the - # handler here would still let the noise through. Since this runs - # right before process exit, swallowing late exceptions is safe. - loop = asyncio.get_event_loop() - loop.set_exception_handler(lambda _loop, _ctx: None) - try: - await cleanup_mcp_connections() - except Exception: - pass + if MCP_AVAILABLE: + # Silence the asyncgen finalization noise that anyio/mcp emits when + # stdio_client's task group is torn down across tasks. The handler is + # intentionally NOT restored: asyncgen finalization happens during + # asyncio.run() shutdown (after run_agent returns), so restoring the + # handler here would still let the noise through. Since this runs + # right before process exit, swallowing late exceptions is safe. + loop = asyncio.get_event_loop() + loop.set_exception_handler(lambda _loop, _ctx: None) + try: + await cleanup_mcp_connections() + except Exception: + pass async def run_agent(workspace_dir: Path, task: str = None): @@ -705,6 +715,20 @@ def _(event): print_help() continue + elif command == "/skills": + # List all installed skills + if skill_loader and skill_loader.loaded_skills: + skills = sorted(skill_loader.loaded_skills.keys()) + print(f"\n{Colors.BRIGHT_CYAN}📦 Installed Skills ({len(skills)}):{Colors.RESET}\n") + for i, skill_name in enumerate(skills, 1): + skill = skill_loader.loaded_skills[skill_name] + desc = skill.description[:60] if skill.description else "No description" + print(f" {i}. {Colors.BRIGHT_GREEN}{skill_name}{Colors.RESET} - {desc}") + print(f"\n{Colors.DIM}Use get_skill(skill_name) to load a skill{Colors.RESET}\n") + else: + print(f"\n{Colors.YELLOW}⚠️ No skills loaded{Colors.RESET}\n") + continue + elif command == "/clear": # Clear message history but keep system prompt old_count = len(agent.messages) diff --git a/mini_agent/config.py b/mini_agent/config.py index bab78f0..03a7a08 100644 --- a/mini_agent/config.py +++ b/mini_agent/config.py @@ -4,6 +4,7 @@ """ from pathlib import Path +from typing import Union, Optional import yaml from pydantic import BaseModel, Field @@ -79,7 +80,7 @@ def load(cls) -> "Config": return cls.from_yaml(config_path) @classmethod - def from_yaml(cls, config_path: str | Path) -> "Config": + def from_yaml(cls, config_path: Union[str, Path]) -> "Config": """Load configuration from YAML file Args: @@ -174,7 +175,7 @@ def get_package_dir() -> Path: return Path(__file__).parent @classmethod - def find_config_file(cls, filename: str) -> Path | None: + def find_config_file(cls, filename: str) -> Optional[Path]: """Find configuration file with priority order Search for config file in the following order of priority: diff --git a/mini_agent/llm/anthropic_client.py b/mini_agent/llm/anthropic_client.py index 6baf994..8984166 100644 --- a/mini_agent/llm/anthropic_client.py +++ b/mini_agent/llm/anthropic_client.py @@ -1,7 +1,7 @@ """Anthropic LLM client implementation.""" import logging -from typing import Any +from typing import Any, Optional, Union import anthropic @@ -26,7 +26,7 @@ def __init__( api_key: str, api_base: str = "https://api.minimaxi.com/anthropic", model: str = "MiniMax-M2.5", - retry_config: RetryConfig | None = None, + retry_config: Optional[RetryConfig] = None, ): """Initialize Anthropic client. @@ -47,9 +47,9 @@ def __init__( async def _make_api_request( self, - system_message: str | None, + system_message: Optional[str], api_messages: list[dict[str, Any]], - tools: list[Any] | None = None, + tools: Optional[list[Any]] = None, ) -> anthropic.types.Message: """Execute API request (core method that can be retried). @@ -111,7 +111,7 @@ def _convert_tools(self, tools: list[Any]) -> list[dict[str, Any]]: raise TypeError(f"Unsupported tool type: {type(tool)}") return result - def _convert_messages(self, messages: list[Message]) -> tuple[str | None, list[dict[str, Any]]]: + def _convert_messages(self, messages: list[Message]) -> tuple[Optional[str], list[dict[str, Any]]]: """Convert internal messages to Anthropic format. Args: @@ -180,7 +180,7 @@ def _convert_messages(self, messages: list[Message]) -> tuple[str | None, list[d def _prepare_request( self, messages: list[Message], - tools: list[Any] | None = None, + tools: Optional[list[Any]] = None, ) -> dict[str, Any]: """Prepare the request for Anthropic API. @@ -257,7 +257,7 @@ def _parse_response(self, response: anthropic.types.Message) -> LLMResponse: async def generate( self, messages: list[Message], - tools: list[Any] | None = None, + tools: Optional[list[Any]] = None, ) -> LLMResponse: """Generate response from Anthropic LLM. diff --git a/mini_agent/llm/base.py b/mini_agent/llm/base.py index 19892a8..97edd68 100644 --- a/mini_agent/llm/base.py +++ b/mini_agent/llm/base.py @@ -1,7 +1,7 @@ """Base class for LLM clients.""" from abc import ABC, abstractmethod -from typing import Any +from typing import Any, Optional, Union from ..retry import RetryConfig from ..schema import LLMResponse, Message @@ -19,7 +19,7 @@ def __init__( api_key: str, api_base: str, model: str, - retry_config: RetryConfig | None = None, + retry_config: Optional[RetryConfig] = None, ): """Initialize the LLM client. @@ -41,7 +41,7 @@ def __init__( async def generate( self, messages: list[Message], - tools: list[Any] | None = None, + tools: Optional[list[Any]] = None, ) -> LLMResponse: """Generate response from LLM. @@ -58,7 +58,7 @@ async def generate( def _prepare_request( self, messages: list[Message], - tools: list[Any] | None = None, + tools: Optional[list[Any]] = None, ) -> dict[str, Any]: """Prepare the request payload for the API. @@ -72,7 +72,7 @@ def _prepare_request( pass @abstractmethod - def _convert_messages(self, messages: list[Message]) -> tuple[str | None, list[dict[str, Any]]]: + def _convert_messages(self, messages: list[Message]) -> tuple[Optional[str], list[dict[str, Any]]]: """Convert internal message format to API-specific format. Args: diff --git a/mini_agent/llm/llm_wrapper.py b/mini_agent/llm/llm_wrapper.py index 28d2c8b..c8d3a0a 100644 --- a/mini_agent/llm/llm_wrapper.py +++ b/mini_agent/llm/llm_wrapper.py @@ -5,6 +5,7 @@ """ import logging +from typing import Optional, Any from ..retry import RetryConfig from ..schema import LLMProvider, LLMResponse, Message @@ -39,7 +40,7 @@ def __init__( provider: LLMProvider = LLMProvider.ANTHROPIC, api_base: str = "https://api.minimaxi.com", model: str = "MiniMax-M2.5", - retry_config: RetryConfig | None = None, + retry_config: Optional[RetryConfig] = None, ): """Initialize LLM client with specified provider. @@ -113,7 +114,7 @@ def retry_callback(self, value): async def generate( self, messages: list[Message], - tools: list | None = None, + tools: Optional[list[Any]] = None, ) -> LLMResponse: """Generate response from LLM. diff --git a/mini_agent/llm/openai_client.py b/mini_agent/llm/openai_client.py index a30fc19..597debf 100644 --- a/mini_agent/llm/openai_client.py +++ b/mini_agent/llm/openai_client.py @@ -2,7 +2,7 @@ import json import logging -from typing import Any +from typing import Any, Optional, Union from openai import AsyncOpenAI @@ -27,7 +27,7 @@ def __init__( api_key: str, api_base: str = "https://api.minimaxi.com/v1", model: str = "MiniMax-M2.5", - retry_config: RetryConfig | None = None, + retry_config: Optional[RetryConfig] = None, ): """Initialize OpenAI client. @@ -48,7 +48,7 @@ def __init__( async def _make_api_request( self, api_messages: list[dict[str, Any]], - tools: list[Any] | None = None, + tools: Optional[list[Any]] = None, ) -> Any: """Execute API request (core method that can be retried). @@ -111,7 +111,7 @@ def _convert_tools(self, tools: list[Any]) -> list[dict[str, Any]]: raise TypeError(f"Unsupported tool type: {type(tool)}") return result - def _convert_messages(self, messages: list[Message]) -> tuple[str | None, list[dict[str, Any]]]: + def _convert_messages(self, messages: list[Message]) -> tuple[Optional[str], list[dict[str, Any]]]: """Convert internal messages to OpenAI format. Args: @@ -182,7 +182,7 @@ def _convert_messages(self, messages: list[Message]) -> tuple[str | None, list[d def _prepare_request( self, messages: list[Message], - tools: list[Any] | None = None, + tools: Optional[list[Any]] = None, ) -> dict[str, Any]: """Prepare the request for OpenAI API. @@ -261,7 +261,7 @@ def _parse_response(self, response: Any) -> LLMResponse: async def generate( self, messages: list[Message], - tools: list[Any] | None = None, + tools: Optional[list[Any]] = None, ) -> LLMResponse: """Generate response from OpenAI LLM. diff --git a/mini_agent/logger.py b/mini_agent/logger.py index 220648f..f5f7373 100644 --- a/mini_agent/logger.py +++ b/mini_agent/logger.py @@ -3,7 +3,7 @@ import json from datetime import datetime from pathlib import Path -from typing import Any +from typing import Any, Optional from .schema import Message, ToolCall @@ -40,7 +40,7 @@ def start_new_run(self): f.write(f"Agent Run Log - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n") f.write("=" * 80 + "\n\n") - def log_request(self, messages: list[Message], tools: list[Any] | None = None): + def log_request(self, messages: list[Message], tools: Optional[list[Any]] = None): """Log LLM request Args: @@ -85,9 +85,9 @@ def log_request(self, messages: list[Message], tools: list[Any] | None = None): def log_response( self, content: str, - thinking: str | None = None, - tool_calls: list[ToolCall] | None = None, - finish_reason: str | None = None, + thinking: Optional[str] = None, + tool_calls: Optional[list[ToolCall]] = None, + finish_reason: Optional[str] = None, ): """Log LLM response @@ -124,8 +124,8 @@ def log_tool_result( tool_name: str, arguments: dict[str, Any], result_success: bool, - result_content: str | None = None, - result_error: str | None = None, + result_content: Optional[str] = None, + result_error: Optional[str] = None, ): """Log tool execution result diff --git a/mini_agent/retry.py b/mini_agent/retry.py index 8b5f4e2..0e36b2b 100644 --- a/mini_agent/retry.py +++ b/mini_agent/retry.py @@ -13,7 +13,7 @@ import asyncio import functools import logging -from typing import Any, Callable, Type, TypeVar +from typing import Any, Callable, Type, TypeVar, Optional logger = logging.getLogger(__name__) @@ -71,8 +71,8 @@ def __init__(self, last_exception: Exception, attempts: int): def async_retry( - config: RetryConfig | None = None, - on_retry: Callable[[Exception, int], None] | None = None, + config: Optional[RetryConfig] = None, + on_retry: Optional[Callable[[Exception, int], None]] = None, ) -> Callable: """Async function retry decorator @@ -97,7 +97,7 @@ async def call_api(): def decorator(func: Callable[..., Any]) -> Callable[..., Any]: @functools.wraps(func) async def wrapper(*args: Any, **kwargs: Any) -> Any: - last_exception: Exception | None = None + last_exception: Optional[Exception] = None for attempt in range(config.max_retries + 1): try: diff --git a/mini_agent/schema/schema.py b/mini_agent/schema/schema.py index 4bffb44..c8ef022 100644 --- a/mini_agent/schema/schema.py +++ b/mini_agent/schema/schema.py @@ -1,5 +1,5 @@ from enum import Enum -from typing import Any +from typing import Any, Union, Optional from pydantic import BaseModel @@ -30,11 +30,11 @@ class Message(BaseModel): """Chat message.""" role: str # "system", "user", "assistant", "tool" - content: str | list[dict[str, Any]] # Can be string or list of content blocks - thinking: str | None = None # Extended thinking content for assistant messages - tool_calls: list[ToolCall] | None = None - tool_call_id: str | None = None - name: str | None = None # For tool role + content: Union[str, list[dict[str, Any]]] # Can be string or list of content blocks + thinking: Optional[str] = None # Extended thinking content for assistant messages + tool_calls: Optional[list[ToolCall]] = None + tool_call_id: Optional[str] = None + name: Optional[str] = None # For tool role class TokenUsage(BaseModel): @@ -49,7 +49,7 @@ class LLMResponse(BaseModel): """LLM response.""" content: str - thinking: str | None = None # Extended thinking blocks - tool_calls: list[ToolCall] | None = None + thinking: Optional[str] = None # Extended thinking blocks + tool_calls: Optional[list[ToolCall]] = None finish_reason: str - usage: TokenUsage | None = None # Token usage from API response + usage: Optional[TokenUsage] = None # Token usage from API response diff --git a/mini_agent/skills/generative-ui/SKILL.md b/mini_agent/skills/generative-ui/SKILL.md new file mode 100644 index 0000000..d7e563f --- /dev/null +++ b/mini_agent/skills/generative-ui/SKILL.md @@ -0,0 +1,525 @@ +--- +name: generative-ui +description: "生产级交互式 Web 体验生成器。作为完整的 AI 产品团队(PM + UX 设计师 + 工程师),生成精致、可动画、功能完整的单文件 HTML 应用。" +metadata: {"emoji": "🎨", "requires": {"bins": ["node"]}} +--- + +# 生成式 UI 专家技能 (Generative UI Expert) + +你是一个**专业、细致、有创意**的 AI 产品团队,同时扮演三个角色来打造精美的交互式 Web 体验。 + +--- + +## 你的角色 + +### 产品经理 (Product Manager) +- 解读用户提示词,定义**核心体验目标** +- 识别**关键用户交互**和信息架构 +- **强制要求**:头脑风暴 **~12 个特性**,然后筛选出**最佳 5-8 个**控制范围 +- 即使是信息类或简单查询,也必须成为交互式应用 — 而非静态文本 +- 定义成功标准:什么让这个体验感觉"真实"而非"演示" + +### UX 设计师 (UX Designer) +- 为提示词量身定制**视觉惊艳、信息密集**的界面 +- 选择统一的配色方案、字体搭配和布局系统 +- 规划**每个交互元素的微交互** — hover、click、focus、active 状态 +- 设计令人愉悦的效果:动画、过渡和视觉反馈 +- 确保响应式设计,支持桌面 (1024px+) 和移动端 (375px) +- 无障碍:交互元素添加 ARIA 标签,关键流程支持键盘导航 + +### 前端工程师 (Frontend Engineer) +- 将整个体验实现为**单个自包含的 HTML 文件** +- 编写**生产级质量**的 HTML5 + Tailwind CSS + 原生 JavaScript +- 所有 JS 包装在 `DOMContentLoaded` 中,所有异步操作在 `try/catch` 中 +- **禁止**:`window.parent`、`window.top`、`window.postMessage` +- 所有外部链接:`target="_blank" rel="noopener noreferrer"` +- 优化首屏加载的即时视觉冲击 + +--- + +## 核心哲学 + +> **每个提示词都值得一个独特、定制的交互体验 — 而非一大段文字。** +> **构建一个真正的、功能完整的应用程序来服务真实内容 — 而非演示或骨架。** + +- **应用优先**:即使是简单的事实查询("什么导致地震?")也必须成为交互式应用(板块运动模拟器),而非文字解释 +- **拒绝大段文字**:用交互功能、视觉元素和数据展示替代段落 +- **深度优于广度**:精心打磨的特性子集胜过残缺的全特性集 +- **"展示给朋友"测试**:你会自豪地分享这个吗?如果不是,就还没完成 + +--- + +## MANDATORY 内部思考流程 + +在生成**任何代码之前**,你**必须**完成以下 7 个步骤: + +### Step 1: 意图分类 (Intent Classification) +- 这是什么类型的体验?映射到:游戏 / 仪表盘 / 工具 / 教育 / 创意 / 落地页 / 混合 +- 用户真正的目标是什么,超出字面意思? +- 什么会让这个体验**超出预期**地令人愉悦? + +### Step 2: 实体与事实识别 (Entity & Fact Identification) +- 列出所有提到的现实世界实体(人物、地点、公司、日期、产品、事件) +- 列出所有需要验证的事实 +- 标记任何时间敏感的数据(价格、排名、分数、天气、新闻) +- **绝对强制**:如果存在现实世界实体或时间敏感的事实,**必须**在代码生成前使用 `WebSearch`。这不是可选的。 + +### Step 3: 特性头脑风暴 (Feature Brainstorming) +- 为这个体验生成 **~12 个可能的特性** +- 每个特性评分:影响力 (1-5)、单文件可行性 (1-5)、愉悦因子 (1-5) +- 选择**最能提升体验的 top 5-8 个特性** +- 识别 **2-3 个"惊喜"特性**,将实现提升到基础实现之上 + +### Step 4: 视觉设计决策 (Visual Design Decision) +- 选择配色方案(具体 hex 代码) +- 选择字体(Google Font 搭配 — 一个展示字体,一个正文字体) +- 选择布局模式:网格、单列、仪表盘、画布、分屏 +- 选择动画风格:微妙/专业、活泼/弹跳、戏剧/电影级 +- 暗色模式作为默认,可选切换亮色模式 + +### Step 5: 技术架构 (Technical Architecture) +- 需要哪些 CDN 库?(最小化 — 每个都会增加加载时间) +- 状态管理方案:原生 JS 对象、类模式、模块模式 +- 核心实体的数据结构设计 +- 事件处理计划(适当的地方使用事件委托) +- 性能考虑:游戏使用 requestAnimationFrame,输入使用 debounce + +### Step 6: 内容策略 (Content Strategy) +- 需要什么真实内容/数据?从哪里来? +- 集成搜索结果?真实生成的数据? +- 图片策略:内联 SVG、CSS 艺术、Unsplash、Emoji 还是 data URI? +- 音频策略:需要吗?用什么方式? + +### Step 7: 质量预检 (Quality Pre-check) +- 这个计划创建的是真实应用还是只是演示? +- 用户真的会重复使用这个吗? +- 信息密度合适吗?(仪表盘应该密集,游戏应该专注) +- 有遗漏的边缘情况吗?(空状态、错误状态、加载状态) + +--- + +## 技术指南 + +### 架构 +- 生成**单个 HTML 文件**,包含所有 HTML、CSS 和 JavaScript +- 使用内联 ` + + +
+ +
+ + + +``` + +### System Instructions 生成模板 + +```markdown +## Goal +创建一个{应用类型},实现{核心功能}。 + +## Planning(7 步强制思考) +1. 意图分类:{体验类型 — 游戏/仪表盘/工具/教育/创意/落地页/混合} +2. 实体与事实识别:{需要 WebSearch 验证的现实世界实体列表} +3. 特性头脑风暴:{~12 个特性 → 筛选 top 5-8} +4. 视觉设计:{配色 hex、字体搭配、布局模式、动画风格} +5. 技术架构:{CDN 库、状态管理、数据结构、事件处理} +6. 内容策略:{图片策略、音频策略、数据来源} +7. 质量预检:{真实 vs 演示、信息密度、边缘情况} + +## Examples +参考: +- {示例1} +- {示例2} + +## Technical Specs +- 格式: 单 HTML 文件 +- 样式: Tailwind CSS(强制主框架)+ 自定义 CSS(仅 @keyframes 等) +- JS: DOMContentLoaded + try/catch + addEventListener + const/let +- 交互: 响应式(375px-1280px),事件驱动 +- 动画: 入场动画 + hover/click 反馈 + prefers-reduced-motion +- 性能: CDN 最小化,游戏使用 requestAnimationFrame + +## Quality Checklist +- [ ] 单文件自包含 HTML +- [ ] Tailwind CSS 作为主框架 +- [ ] 入场动画(交错延迟) +- [ ] 响应式(375px 可用) +- [ ] 零占位符内容 +- [ ] 零控制台错误 +- [ ] DOMContentLoaded + try/catch +- [ ] const/let only + addEventListener only +- [ ] 图片有 fallback +- [ ] 音频默认静音 + 可见切换 +- [ ] prefers-reduced-motion +- [ ] favicon(内联 SVG data URI) +``` + +--- + +## 工作流程 + +``` +用户请求 + ↓ +Step 1: 7 步强制内部思考 + 意图分类 → 实体识别 → 特性头脑风暴(~12→5-8) + → 视觉设计 → 技术架构 → 内容策略 → 质量预检 + ↓ +Step 2: 研究与验证 + WebSearch (现实实体/时效数据 → 强制) + WebFetch (特定文档/数据源 → 按需) + ↓ +Step 3: 生成 HTML 文件 + 解析输出目录(用户指定 or 默认 ~/Desktop/genui-output/) + mkdir → Write/Edit → [输出目录]/[name].html + Tailwind CSS + DOMContentLoaded + try/catch + ↓ +Step 4: 强制自动审查(工具验证) + Read 回读文件 → 确认完整无截断 + Grep 扫描禁止词 → 零匹配才通过 + Grep 扫描 var / onclick → 零匹配才通过 + 发现问题 → Edit 修复 → 重新扫描 + ↓ +Step 5: 呈现给用户 + 2-3 句描述 + 关键交互 + 浏览器打开 + ↓ +用户即时交互使用 +``` + +--- + +## 论文关键结论 + +> "Generative UI 是最新强大模型的**新兴能力** (Emergent Capability)" +> — Gemini 3 达到 0% 错误率 + +**核心价值**: 模型不仅生成内容,还生成**整个用户界面**,实现真正的"AI 即时创建应用"! + +> **每个提示词都值得一个独特、定制的交互体验 — 而非一大段文字。** +> **构建一个真正的、功能完整的应用程序来服务真实内容 — 而非演示或骨架。** diff --git a/mini_agent/skills/generative-ui/_meta.json b/mini_agent/skills/generative-ui/_meta.json new file mode 100644 index 0000000..4549c6d --- /dev/null +++ b/mini_agent/skills/generative-ui/_meta.json @@ -0,0 +1,6 @@ +{ + "ownerId": "kn70pywhg0fyz996kpa8xj89s57yhv26", + "slug": "generative-ui", + "version": "1.0.0", + "publishedAt": 1767545394459 +} diff --git a/mini_agent/skills/generative-ui/examples/README.md b/mini_agent/skills/generative-ui/examples/README.md new file mode 100644 index 0000000..b68623c --- /dev/null +++ b/mini_agent/skills/generative-ui/examples/README.md @@ -0,0 +1,252 @@ +# Generative UI 示例应用 + +这个目录包含了使用 Generative UI Expert skill 可以创建的示例应用。 + +## 📦 示例列表 + +### 1. 冒泡排序可视化 (bubble-sort-visualizer) +- **文件**: `bubble-sort-visualizer.html` (在 SKILL.md 中) +- **类型**: 交互式学习工具 +- **功能**: + - 可视化冒泡排序算法过程 + - 实时显示比较和交换次数 + - 可调速度控制 + - 动画效果展示 + +### 2. 2048 游戏 (game-2048.html) +- **文件**: `game-2048.html` +- **类型**: 实时游戏 +- **功能**: + - 经典 2048 游戏机制 + - 得分和最高分记录 + - 键盘和触摸操作支持 + - 优美的动画效果 + - 本地存储最佳成绩 + +### 3. 番茄工作法计时器 (pomodoro-timer.html) +- **文件**: `pomodoro-timer.html` +- **类型**: 实用工具应用 +- **功能**: + - 25/5/15 分钟标准番茄钟 + - 自定义时长设置 + - 统计今日完成的番茄数 + - 浏览器通知提醒 + - 自动切换工作/休息模式 + - 数据持久化 + +## 🚀 如何使用 + +### 方法 1: 直接打开 +双击任意 `.html` 文件,在浏览器中直接打开即可使用。 + +### 方法 2: 本地服务器 +如果需要更完整的功能(如某些浏览器 API),可以使用本地服务器: + +```bash +# 使用 Python +python3 -m http.server 8000 + +# 或使用 Node.js +npx serve + +# 然后访问 http://localhost:8000 +``` + +## 💡 学习要点 + +### 从这些示例中你可以学到: + +#### 1. **状态管理** +```javascript +// 游戏状态管理示例 (2048) +let grid = []; +let score = 0; + +function updateState() { + // 更新逻辑 + renderGrid(); +} +``` + +#### 2. **动画实现** +```css +/* CSS 过渡动画 */ +.tile { + transition: all 0.15s ease-in-out; +} + +/* CSS 关键帧动画 */ +@keyframes pulse { + 0%, 100% { transform: scale(1); } + 50% { transform: scale(1.05); } +} +``` + +#### 3. **事件处理** +```javascript +// 键盘事件 +document.addEventListener('keydown', (e) => { + if (e.key === 'ArrowLeft') { + move('left'); + } +}); + +// 触摸事件 +document.addEventListener('touchstart', handleTouchStart); +document.addEventListener('touchend', handleTouchEnd); +``` + +#### 4. **数据持久化** +```javascript +// localStorage 使用 +localStorage.setItem('bestScore', score); +const bestScore = localStorage.getItem('bestScore') || 0; +``` + +#### 5. **浏览器 API** +```javascript +// Notification API +if ('Notification' in window) { + Notification.requestPermission(); + new Notification('标题', { body: '内容' }); +} + +// Web Audio API +const audioContext = new AudioContext(); +const oscillator = audioContext.createOscillator(); +``` + +## 🎨 设计模式 + +### 1. **模块化设计** +每个功能都封装在独立的函数中,便于维护和扩展。 + +### 2. **响应式布局** +使用 Flexbox 和 Grid 实现自适应布局。 + +### 3. **渐进增强** +从基础功能开始,逐步添加高级特性。 + +### 4. **用户体验优先** +- 即时反馈 +- 流畅动画 +- 清晰的视觉层次 +- 直观的交互 + +## 🔧 自定义指南 + +### 修改样式 +所有样式都在 ` + + +
+

2048

+ +
+
+
得分
+
0
+
+ +
+
最高分
+
0
+
+
+ +
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+ 使用方向键 ← ↑ → ↓ 移动方块,合并相同数字! +
+
+ +
+
+

+ +
+
+ + + + diff --git a/mini_agent/skills/generative-ui/examples/pomodoro-timer.html b/mini_agent/skills/generative-ui/examples/pomodoro-timer.html new file mode 100644 index 0000000..2971de9 --- /dev/null +++ b/mini_agent/skills/generative-ui/examples/pomodoro-timer.html @@ -0,0 +1,566 @@ + + + + + + 🍅 番茄工作法计时器 + + + +
+

🍅 番茄钟

+

专注工作,高效休息

+ +
+
+
+
+
+
+ +
25:00
+ +
+ + + + +
+ +
+
⚙️ 设置
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+
+
0
+
完成番茄
+
+
+
0
+
总分钟
+
+
+
0
+
连续
+
+
+
+ +
+ 🎉 番茄完成!休息一下~ +
+ + + + diff --git a/mini_agent/tools/base.py b/mini_agent/tools/base.py index 1bfcd25..7cc2f45 100644 --- a/mini_agent/tools/base.py +++ b/mini_agent/tools/base.py @@ -1,6 +1,6 @@ """Base tool classes.""" -from typing import Any +from typing import Any, Optional, Union from pydantic import BaseModel @@ -10,7 +10,7 @@ class ToolResult(BaseModel): success: bool content: str = "" - error: str | None = None + error: Optional[str] = None class Tool: diff --git a/mini_agent/tools/bash_tool.py b/mini_agent/tools/bash_tool.py index 47375ff..f0087bd 100644 --- a/mini_agent/tools/bash_tool.py +++ b/mini_agent/tools/bash_tool.py @@ -8,7 +8,7 @@ import re import time import uuid -from typing import Any +from typing import Any, Optional from pydantic import Field, model_validator @@ -27,7 +27,7 @@ class BashOutputResult(ToolResult): stdout: str = Field(description="The command's standard output") stderr: str = Field(description="The command's standard error output") exit_code: int = Field(description="The command's exit code") - bash_id: str | None = Field(default=None, description="Shell process ID (only when run_in_background=True)") + bash_id: Optional[str] = Field(default=None, description="Shell process ID (only when run_in_background=True)") @model_validator(mode="after") def format_content(self) -> "BashOutputResult": @@ -64,13 +64,13 @@ def __init__(self, bash_id: str, command: str, process: "asyncio.subprocess.Proc self.output_lines: list[str] = [] self.last_read_index = 0 self.status = "running" - self.exit_code: int | None = None + self.exit_code: Optional[int] = None def add_output(self, line: str): """Add new output line.""" self.output_lines.append(line) - def get_new_output(self, filter_pattern: str | None = None) -> list[str]: + def get_new_output(self, filter_pattern: Optional[str] = None) -> list[str]: """Get new output since last check, optionally filtered by regex.""" new_lines = self.output_lines[self.last_read_index :] self.last_read_index = len(self.output_lines) @@ -85,7 +85,7 @@ def get_new_output(self, filter_pattern: str | None = None) -> list[str]: return new_lines - def update_status(self, is_alive: bool, exit_code: int | None = None): + def update_status(self, is_alive: bool, exit_code: Optional[int] = None): """Update process status.""" if not is_alive: self.status = "completed" if exit_code == 0 else "failed" @@ -117,7 +117,7 @@ def add(cls, shell: BackgroundShell) -> None: cls._shells[shell.bash_id] = shell @classmethod - def get(cls, bash_id: str) -> BackgroundShell | None: + def get(cls, bash_id: str) -> Optional[BackgroundShell]: """Get a background shell by ID.""" return cls._shells.get(bash_id) @@ -222,7 +222,7 @@ class BashTool(Tool): - Unix/Linux/macOS: bash """ - def __init__(self, workspace_dir: str | None = None): + def __init__(self, workspace_dir: Optional[str] = None): """Initialize BashTool with OS-specific shell detection. Args: @@ -485,7 +485,7 @@ def parameters(self) -> dict[str, Any]: async def execute( self, bash_id: str, - filter_str: str | None = None, + filter_str: Optional[str] = None, ) -> BashOutputResult: """Retrieve output from background shell. diff --git a/mini_agent/tools/file_tools.py b/mini_agent/tools/file_tools.py index 74b7eee..86d6544 100644 --- a/mini_agent/tools/file_tools.py +++ b/mini_agent/tools/file_tools.py @@ -1,7 +1,7 @@ """File operation tools.""" from pathlib import Path -from typing import Any +from typing import Any, Optional import tiktoken @@ -105,7 +105,7 @@ def parameters(self) -> dict[str, Any]: "required": ["path"], } - async def execute(self, path: str, offset: int | None = None, limit: int | None = None) -> ToolResult: + async def execute(self, path: str, offset: Optional[int] = None, limit: Optional[int] = None) -> ToolResult: """Execute read file.""" try: file_path = Path(path)