widget-design
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseWidget Design Best Practices
Widget设计最佳实践
Decision Framework
决策框架
Platform Selection
平台选择
Choose based on target client:
- GPT Apps: Target is ChatGPT. Requires with
_meta.openai.widgetAccessible: true - MCP Apps: Target is any ext-apps client. Minimal config, works automatically.
根据目标客户端选择:
- GPT Apps:目标客户端为ChatGPT。需要在中设置
_meta.openai。widgetAccessible: true - MCP Apps:目标客户端为任意ext-apps客户端。配置要求极少,可自动运行。
Handler Type Selection
处理方式选择
| Scenario | Handler | Reason |
|---|---|---|
| User interaction needed (buttons, inputs) | React ( | State management with hooks |
| Display external widget library | Template literal | Just load scripts/styles |
| Dynamic content from tool params | React | Props flow naturally |
| Static HTML with no state | Template literal | Simpler, less overhead |
Rule of thumb: If unsure, start with React. Converting later is harder than starting simple.
| 场景 | 处理方式 | 原因 |
|---|---|---|
| 需要用户交互(按钮、输入框) | React( | 使用Hooks进行状态管理 |
| 展示外部组件库 | 模板字面量 | 仅需加载脚本/样式 |
| 从工具参数获取动态内容 | React | Props可自然传递 |
| 无状态的静态HTML | 模板字面量 | 更简单、开销更低 |
经验法则:若不确定,从React开始。后续转换比从简单方案起步更困难。
Widget Design Principles
Widget设计原则
1. Widgets Are Not Web Apps
1. Widget并非Web应用
Widgets render inline in conversations. Design constraints:
- No navigation: Single-screen experience only
- Limited width: ~600-700px max in most clients
- Sandboxed: External resources need CSP declarations
- Ephemeral: May be re-rendered, don't rely on persistence
Widget在对话中内联渲染,需遵循以下设计约束:
- 无导航:仅支持单屏体验
- 宽度受限:在大多数客户端中最大宽度约为600-700px
- 沙箱环境:外部资源需要CSP声明
- 临时性:可能会被重新渲染,不要依赖持久化存储
2. Immediate Value
2. 即时呈现价值
Show useful content without requiring user action:
tsx
// Bad: Requires click to see anything
export default function Widget() {
const [data, setData] = useState(null);
return <button onClick={fetchData}>Load Data</button>;
}
// Good: Shows data immediately
export default function Widget({ query }) {
const [data, setData] = useState(null);
useEffect(() => { fetchData(query).then(setData); }, [query]);
return data ? <Results data={data} /> : <Loading />;
}无需用户操作即可展示有用内容:
tsx
// 不佳:需要点击才能查看内容
export default function Widget() {
const [data, setData] = useState(null);
return <button onClick={fetchData}>加载数据</button>;
}
// 推荐:立即展示数据
export default function Widget({ query }) {
const [data, setData] = useState(null);
useEffect(() => { fetchData(query).then(setData); }, [query]);
return data ? <Results data={data} /> : <Loading />;
}3. Visual Hierarchy in Limited Space
3. 有限空间内的视觉层级
With limited width, hierarchy matters more:
tsx
<div className="space-y-4">
{/* Label: small, muted, uppercase */}
<div className="text-sm text-zinc-500 uppercase tracking-wider">Temperature</div>
{/* Value: large, prominent */}
<div className="text-5xl font-light">72°F</div>
{/* Supporting: medium, secondary */}
<div className="text-zinc-400">Feels like 68°F</div>
</div>在宽度受限的情况下,视觉层级尤为重要:
tsx
<div className="space-y-4">
{/* 标签:小字号、浅色调、大写 */}
<div className="text-sm text-zinc-500 uppercase tracking-wider">温度</div>
{/* 数值:大字号、突出显示 */}
<div className="text-5xl font-light">72°F</div>
{/* 辅助信息:中字号、次要色调 */}
<div className="text-zinc-400">体感温度 68°F</div>
</div>4. Every Interactive Element Needs Feedback
4. 所有交互元素需提供反馈
Users need visual confirmation that elements are interactive:
tsx
// Bad: No visual feedback
<button className="px-4 py-2 bg-white/10">Click</button>
// Good: Hover + transition
<button className="px-4 py-2 bg-white/10 hover:bg-white/20 border border-white/10 hover:border-white/20 transition-all duration-200">
Click
</button>用户需要视觉确认元素可交互:
tsx
// 不佳:无视觉反馈
<button className="px-4 py-2 bg-white/10">点击</button>
// 推荐:悬停效果 + 过渡动画
<button className="px-4 py-2 bg-white/10 hover:bg-white/20 border border-white/10 hover:border-white/20 transition-all duration-200">
点击
</button>State Management Guidelines
状态管理指南
Keep State Local and Simple
保持状态本地化与简洁性
Widgets are isolated. No Redux, Zustand, or external state.
tsx
// Good: Local state with hooks
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);Widget是独立隔离的,无需使用Redux、Zustand或外部状态管理工具。
tsx
// 推荐:使用Hooks管理本地状态
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);Always Handle Three States
始终处理三种状态
Every async operation has three states. Handle all of them:
tsx
export default function Widget() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetchData()
.then(setData)
.catch(e => setError(e.message))
.finally(() => setLoading(false));
}, []);
if (loading) return <Loading />;
if (error) return <Error message={error} />;
return <Display data={data} />;
}每个异步操作都有三种状态,需全部处理:
tsx
export default function Widget() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetchData()
.then(setData)
.catch(e => setError(e.message))
.finally(() => setLoading(false));
}, []);
if (loading) return <Loading />;
if (error) return <Error message={error} />;
return <Display data={data} />;
}Fetch on Mount, Not on Click
在组件挂载时获取数据,而非点击时
Widgets should show value immediately:
tsx
// Bad: User must click to see data
<button onClick={() => fetchData()}>Load</button>
// Good: Fetch automatically
useEffect(() => { fetchData(); }, []);Widget应立即展示价值:
tsx
// 不佳:用户必须点击才能查看数据
<button onClick={() => fetchData()}>加载</button>
// 推荐:自动获取数据
useEffect(() => { fetchData(); }, []);Common Mistakes
常见错误
1. Missing widgetAccessible (GPT Apps)
1. GPT Apps中缺少widgetAccessible配置
Widget won't render without this:
typescript
// Broken
_meta: { openai: { toolInvocation: { ... } } }
// Fixed
_meta: { openai: { widgetAccessible: true, toolInvocation: { ... } } }缺少该配置时组件无法渲染:
typescript
// 错误配置
_meta: { openai: { toolInvocation: { ... } } }
// 修复后
_meta: { openai: { widgetAccessible: true, toolInvocation: { ... } } }2. External Fetch Without CSP
2. 未声明CSP的外部请求
Requests blocked silently:
typescript
// Broken: fetch fails silently
fetch('https://api.weather.com/data');
// Fixed: declare in metadata
_meta: {
openai: {
widgetCSP: { connect_domains: ["https://api.weather.com"] }
}
}请求会被静默拦截:
typescript
// 错误写法:请求会静默失败
fetch('https://api.weather.com/data');
// 修复后:在元数据中声明
_meta: {
openai: {
widgetCSP: { connect_domains: ["https://api.weather.com"] }
}
}3. Hardcoded Dimensions
3. 硬编码尺寸
Widgets break on different screen sizes:
tsx
// Bad
<div style={{ width: '800px' }}>...</div>
// Good
<div className="w-full max-w-2xl mx-auto">...</div>组件在不同屏幕尺寸下会显示异常:
tsx
// 不佳
<div style={{ width: '800px' }}>...</div>
// 推荐
<div className="w-full max-w-2xl mx-auto">...</div>4. No Error Boundaries
4. 无错误边界处理
Crashes show blank widget:
tsx
// Always wrap risky operations
try {
const result = JSON.parse(data);
} catch {
return <Error message="Invalid data format" />;
}崩溃时会显示空白组件:
tsx
// 始终对风险操作进行包裹
try {
const result = JSON.parse(data);
} catch {
return <Error message="数据格式无效" />;
}Platform-Specific Notes
平台特定说明
GPT Apps: Structured Content for Widget Communication
GPT Apps:组件通信的结构化内容
Pass data from tool to widget:
typescript
return {
structuredContent: { game: "doom", url: "..." },
content: [{ type: "text", text: "Launching DOOM..." }],
};Widget reads via hook.
useToolOutput()从工具向组件传递数据:
typescript
return {
structuredContent: { game: "doom", url: "..." },
content: [{ type: "text", text: "正在启动DOOM..." }],
};组件通过钩子读取数据。
useToolOutput()MCP Apps: Minimal Config
MCP Apps:极简配置
MCP Apps work with just a React component:
tsx
// This is enough for MCP Apps
export const metadata = { name: "widget", description: "..." };
export default function Widget() { return <div>Hello</div>; }仅需一个React组件即可运行MCP Apps:
tsx
// 此配置足以运行MCP Apps
export const metadata = { name: "widget", description: "..." };
export default function Widget() { return <div>你好</div>; }Quick Reference
快速参考
GPT Apps Checklist
GPT Apps检查清单
- in metadata
widgetAccessible: true - messages under 64 chars
toolInvocation - CSP for external domains
- 元数据中设置
widgetAccessible: true - 消息长度不超过64字符
toolInvocation - 为外部域名配置CSP
References
参考资料
See references/design-principles.md for:
- Complete widget examples (counter, weather, arcade)
- Component patterns (cards, buttons, tabs)
- CSS Modules examples
- GPT App submission requirements
- Project structure templates
查看references/design-principles.md获取:
- 完整的组件示例(计数器、天气、街机游戏)
- 组件模式(卡片、按钮、标签页)
- CSS Modules示例
- GPT Apps提交要求
- 项目结构模板