widget-design

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Widget Design Best Practices

Widget设计最佳实践

Decision Framework

决策框架

Platform Selection

平台选择

Choose based on target client:
  • GPT Apps: Target is ChatGPT. Requires
    _meta.openai
    with
    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

处理方式选择

ScenarioHandlerReason
User interaction needed (buttons, inputs)React (
.tsx
)
State management with hooks
Display external widget libraryTemplate literalJust load scripts/styles
Dynamic content from tool paramsReactProps flow naturally
Static HTML with no stateTemplate literalSimpler, less overhead
Rule of thumb: If unsure, start with React. Converting later is harder than starting simple.
场景处理方式原因
需要用户交互(按钮、输入框)React(
.tsx
使用Hooks进行状态管理
展示外部组件库模板字面量仅需加载脚本/样式
从工具参数获取动态内容ReactProps可自然传递
无状态的静态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
useToolOutput()
hook.
从工具向组件传递数据:
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检查清单

  • widgetAccessible: true
    in metadata
  • toolInvocation
    messages under 64 chars
  • CSP for external domains
  • 元数据中设置
    widgetAccessible: true
  • toolInvocation
    消息长度不超过64字符
  • 为外部域名配置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提交要求
  • 项目结构模板