1. 什么是 inline 模式,以及为何它是“默认”
OpenAI 的官方指南强调:inline 显示是 ChatGPT Apps 的主要模式。 Inline 小部件直接渲染在聊天消息流中、模型回答的上方,包含一个小型 UI 区块(卡片、列表或轮播)以及其下方来自 GPT 的后续消息。
理念很简单:我们不把用户带到一个单独的大界面,而是在对话上下文中直接给一个简洁的可视化“闪现”。 模型解释发生了什么、接下来有哪些选项,而小部件则清晰地展示结构和可用操作。
Inline 小部件的特点:
- 内容轻量,不需要很多步骤;
- 不把用户引入带有标签页和内部滚动条的复杂导航;
- 解决一两件小事:展示选项列表、允许选择、确认动作、显示状态。
Fullscreen 模式(下一讲详述)用于复杂向导和重内容场景。此刻更重要的是: 默认思考为 inline,只有当 fullscreen 能明显更好地解决问题时,才有意识地切换;当 inline 明显力有不逮时再用全屏。
本讲的目标——熟练运用三大 inline 模式,并在其之上合理设计 CTA:
- 卡片
- 列表
- 轮播
同时要为它们恰当地绑定CTA 按钮(Call to Action)。
2. 何时 inline 优于 fullscreen
简化来看,inline 模式是“快速助手”,而 fullscreen 则是“嵌在 ChatGPT 里的独立应用”。
Inline 尤其适合:
- 需要展示几个备选项并允许选择一两个;
- 结果可以用紧凑结构表达:礼物卡片、订单摘要、小表格;
- 用户执行短操作:“选择”、“查看详情”、“更改筛选”;
- 对话仍是主角:GPT 进行解释/调侃/评论,而小部件只提供方便的表单或视觉呈现。
以 GiftGenius 为例,inline 适合:
- 展示 3–5 个为特定对象挑选的最佳礼物;
- 快速筛选:“只显示数字类礼物”;
- 确认选择:“这是订单摘要,可以了吗?”。
Fullscreen 可用于稍后的三步复杂结账向导。现在我们保持轻量: 一次工具调用的结果 → 一个 inline 小部件。
直观起见——来一张小表:
| 模式 | 理想适用场景 | GiftGenius 示例 |
|---|---|---|
| 卡片 | 1–3 个实体,包含关键参数与 CTA | 3 个顶级礼物 |
| 列表 | 5–10 个文本项,重视可读性 | 无图的创意清单 |
| 轮播 | 3–8 个相似的可视化选项,需要横向翻阅 | 较长的礼物清单 |
理解这些后,我们落到具体:这些模式如何体现在 UI 与代码中。 接下来对每个模式都用相同结构展开:先讲 UX 视角是什么, 然后给出 GiftGenius 的简易 React 组件,最后是如何把它们嵌入 inline 小部件。
3. 卡片:inline UI 的基本砖块
在 Apps SDK 背景下的“卡片”是什么
根据 OpenAI 指南,inline 卡片是一个轻量的单用户小部件,用于展示少量 结构化数据,以及底部的 1–2 个操作。它可以包含标题、图片、两三行元信息,以及一个主 CTA 按钮 (可选再加一个次级按钮)。
在 GiftGenius 中,每张卡片代表一个礼物。适合放置:
- 礼物名称;
- 价格;
- 适用对象(如“同事”“挚友”);
- 简短说明:为何这是个好选择;
- “选择此礼物”或“查看详情”按钮。
卡片应当自给自足:用户扫一眼就能明白这是哪个对象、主要操作是什么。
数据类型与简易组件 GiftCard
先定义礼物的数据类型。假设我们已从 ToolOutput 得到该对象数组;此处只关注 UI。
// 礼物的通用 UI 结构
export type GiftSuggestion = {
id: string;
title: string;
priceLabel: string; // 例如 “≈ 40 $”
recipientLabel: string; // “送给同事”
reason?: string; // 来自模型的说明
imageUrl?: string;
};
现在实现一个简单的卡片 React 组件:
type GiftCardProps = {
gift: GiftSuggestion;
onSelect: (gift: GiftSuggestion) => void;
};
export function GiftCard({ gift, onSelect }: GiftCardProps) {
return (
<div className="flex flex-col gap-2 rounded-lg border p-3">
<div className="text-sm font-medium">{gift.title}</div>
<div className="text-xs text-muted-foreground">
送给:{gift.recipientLabel} · {gift.priceLabel}
</div>
{gift.reason && (
<div className="text-xs text-muted-foreground">{gift.reason}</div>
)}
<button
className="mt-2 self-start rounded bg-primary px-3 py-1 text-xs text-primary-foreground"
onClick={() => onSelect(gift)}
>
选择这份礼物
</button>
</div>
);
}
几个要点:
- 不要用文本把卡片塞满,最多 2–3 行元信息加一段简短说明;
- 仅保留一个主 CTA——“选择这份礼物”;不要塞 5 种不同操作;
- 该组件既可用于 inline 列表,也能用于轮播中。
卡片如何融入整体小部件
假设我们有通过 MCP 调用 giftgenius.suggestGifts 工具得到的 gifts 数组。 在 inline 模式下,小部件可以将它们以 1–3 列网格的形式渲染。
type GiftGridProps = {
gifts: GiftSuggestion[];
onSelect: (gift: GiftSuggestion) => void;
};
export function GiftGrid({ gifts, onSelect }: GiftGridProps) {
return (
<div className="grid gap-3 sm:grid-cols-2">
{gifts.map((gift) => (
<GiftCard key={gift.id} gift={gift} onSelect={onSelect} />
))}
</div>
);
}
这里我们:
- 使用 1–2 列的网格,避免把小部件变成“砖墙”;
- 可以轻松限制卡片数量,例如只展示前 3–6 个。
回调处理器 onSelect 可以调用结账工具,或仅将选择保存到 Widget State 并让模型继续对话。 一个与工具集成的简单示例:
async function handleSelect(gift: GiftSuggestion) {
await window.openai.actions.call("giftgenius.startCheckout", {
giftId: gift.id,
});
}
这里的 window.openai.actions.call 是从小部件直接调用已注册 MCP 工具的桥。
通常在此调用之后,模型会展示状态,或打开下一个小部件(例如订单摘要)。关键是—— 不要把整个结账流程塞进卡片的内部逻辑;卡片只需启动清晰的下一步。
4. 列表:当视觉不是重点
如果说卡片是小“海报”,那么列表就是整洁的文本清单。文档与 UX 建议表明,当文本内容比视觉吸引更重要时,列表更合适。
适用列表的场景:
- 需要展示 5–10 个选项,但它们不需要图片;
- 用户只想快速扫一眼标题和简短描述;
- 每项的操作一致,且 UI 不应分散注意力。
GiftGenius 示例:
- “快速”礼物创意清单(不含细节);
- 常用类别列表:“送给同事”“送给父母”“送给孩子”;
- 已保存的合集(“送 HR 部门的礼物”“20 美元内的新年小物”)。
简易列表组件
做一个紧凑列表,右侧放一个 CTA 按钮“查看详情”。
type GiftListProps = {
gifts: GiftSuggestion[];
onSelect: (gift: GiftSuggestion) => void;
};
export function GiftList({ gifts, onSelect }: GiftListProps) {
return (
<ul className="flex flex-col gap-2">
{gifts.map((gift) => (
<li
key={gift.id}
className="flex items-center justify-between rounded-md border px-3 py-2 text-sm"
>
<span className="truncate">{gift.title}</span>
<button
className="text-xs text-primary"
onClick={() => onSelect(gift)}
>
查看详情
</button>
</li>
))}
</ul>
);
}
这里我们:
- 给标题加上 truncate,避免长标题破坏布局;
- 仍然是一项只放一个 CTA;
- 把“丰富信息”(描述、图片、评价)留到下一步——例如点击后打开独立卡片或 fullscreen 视图。
列表和来自 GPT 的后续建议(follow‑up)非常相配。 小部件展示“候选项”列表,其下方 GPT 会写类似:
“我可以把价格缩小到 30 美元以内,或者只显示数字类礼物。我们选哪个?”并给出两三个后续交互按钮。
在单独的章节中,我们还会讨论如何在不同场景下,把 inline 小部件与后续消息组合得更好。
5. 轮播:当选项多但相似时
轮播是一组横向排列、可滑动或用导航按钮翻页的卡片。 指南建议在展示少量相似元素(一般 3–8 个)时使用轮播, 每个元素包含图片、标题和少量元数据。
核心思路:用户可以快速扫描一组选项,而不会被无尽的纵向列表淹没。
在 GiftGenius 中,轮播适用于:
- 我们有 10–15 个合适礼物,但 inline 小部件只需展示“热门八个”;
- 每份礼物都有不错的视觉表现(图片、样式);
- 希望用户在不往下滚太多的情况下翻看选项。
轮播的 UX 规则
基于指南与研究:
- 轮播中的卡片数量建议 3 到 8 个;再多就提供“显示更多”的额外命令;
- 每张卡片:
- 包含图片或其他视觉元素;
- 不超过两行元信息文本;
- 有一个清晰的 CTA,如“选择”或“查看详情”;
- 不要在卡片内加入复杂嵌套导航(标签、二级跳转);
- 避免内部(纵向)滚动条:卡片高度可在合理范围适配,但不要出现自身滚动条。
“一次一张卡”的简易轮播
若不想处理复杂的横向滚动,可实现最简单的形式:一次显示一张卡片,提供“上一页/下一页”按钮。
import { useState } from "react";
type GiftCarouselProps = {
gifts: GiftSuggestion[];
onSelect: (gift: GiftSuggestion) => void;
};
export function GiftCarousel({ gifts, onSelect }: GiftCarouselProps) {
const [index, setIndex] = useState(0);
const gift = gifts[index];
return (
<div className="flex flex-col gap-2">
<GiftCard gift={gift} onSelect={onSelect} />
<div className="flex items-center justify-between text-xs">
<button
disabled={index === 0}
onClick={() => setIndex((i) => i - 1)}
>
← 上一个
</button>
<span>
{index + 1} / {gifts.length}
</span>
<button
disabled={index === gifts.length - 1}
onClick={() => setIndex((i) => i + 1)}
>
下一个 →
</button>
</div>
</div>
);
}
这样已经有“轮播”的感觉,同时:
- 代码保持简洁;
- 无需与容器宽度、内部横向滚动搏斗;
- 在传给组件前,容易把 gifts 限制到 8 个以内。
若需要更“像真”的轮播,可以使用 overflow-x-auto 与固定卡片宽度。 但在这种情况下,直接使用 UI 库(如 shadcn/ui、与 Radix 兼容的方案等)往往比从零自研更省心。
6. CTA 按钮:少而精、直指要点
CTA(Call to Action)是任何 inline 模式的核心。按钮让你的小部件从“图片”变成“工具”。
基本原则
OpenAI 文档给出了比较严格的建议:
- 一张卡片最多两个主按钮(一个主、一个次);
- 轮播中尽量每项只放一个 CTA;
- CTA 文案应是明确的动词:“显示详情”“加入清单”“前往支付”,不要用抽象的“好的”或“操作”。
按钮越少,对模型和用户都越简单。别忘了,在小部件的上方和下方还有文本回答,以及 GPT 的后续建议。
把 CTA 绑定到应用逻辑
在 GiftGenius 中,大多数 CTA 要么:
- 修改筛选/推荐条件(新的工具调用 giftgenius.refineSearch),
- 启动结账(giftgenius.startCheckout),
- 打开外部网站(通过 openExternal,你在前面的课程里已见过)。
“更改筛选器”这一 CTA 的简易处理器示例:
async function handleRefineFilters(gift: GiftSuggestion) {
await window.openai.actions.call("giftgenius.refineSearch", {
baseGiftId: gift.id,
});
}
从 UX 角度,非常重要的是在 system 指令中明确模型何时应该提供哪些 CTA 按钮。 例如:
- 如果用户说“再多给些选项”,最好展示新的轮播,按钮为“选择”;
- 如果已进入购买环节,“前往支付”的 CTA 应触发工具调用以启动 ACP 结账(我们会在“商业与支付”模块详细说明)。
另一个好实践——不要在 CTA 中重复 ChatGPT 的能力。别做“询问 ChatGPT”的按钮,用户已经有输入框与语音。 指南明确建议避免在卡片内重复输入方式。
7. Inline + 后续消息:双人配合
Inline 小部件从不孤立存在。一个回答通常是这样:
- 模型决定使用你的 App 并调用工具;
- 你的 MCP 返回数据;
- ChatGPT 渲染包含这些数据的 inline 小部件;
- 其下模型补充一段简短的后续文本,并给出继续的现成选项。
在 GiftGenius 中可能是这样:
- inline 小部件:三张礼物卡片,按钮为 CTA “选择”;
- 下方文本:
“这是三种送同事的想法:台灯、演讲课程、咖啡店礼品卡。我可以: — 只显示 30 美元以内的选项; — 再挑几件风格相近的; — 直接帮你购买其中一个。”
模型在后续消息中可以引用你的小部件 CTA(“点击喜欢的选项下方的‘选择’”),也可以给出文本命令, 再次触发工具调用并重绘 inline UI。
牢记:小部件不必什么都做。有时把部分流程留给文本对话更好, 仅把小部件作为对话中的“可视化模块”。
8. 这在 GiftGenius 的整体流程中如何落地
为更清晰,我们用一张简单的时序图:
sequenceDiagram participant U as 用户 participant C as ChatGPT participant A as GiftGenius 小部件 participant B as MCP/后端 U->>C: "为同事挑选 3 份 50$ 以内的礼物" C->>B: call_tool(giftgenius.suggestGifts) B-->>C: 3 个最佳选项 C->>A: 渲染 inline 小部件(卡片/轮播) A-->>U: 带有 "选择" CTA 的卡片 U->>A: 点击 CTA A->>B: call_tool(giftgenius.startCheckout) B-->>A: 状态 / 支付链接 A-->>U: 选择摘要 / 状态 C-->>U: follow-up: "我可以再给一些主意,或帮你写一张贺卡"
从架构视角:
- MCP 是“脑”(推荐、业务逻辑、ACP);
- 小部件是“脸”(卡片/列表/轮播);
- ChatGPT 是“对话主持人”,解释发生了什么,并提供下一步。
为了让流程更顺滑:
- 不要用太多操作把小部件拖沉;
- 卡片中的数据保持紧凑;
- 思考每次 inline 展示后,哪些后续选项对用户最有用。
9. 关于 inline 模式的视觉细节
我们会在后续课程中深入聊视觉设计,这里先提几条对 inline 模式非常关键的点。
首先,确保你的卡片与列表看起来不像嵌进 ChatGPT 的外站。 配色与留白要克制,别用夸张的渐变或 Comic Sans 字体。 Inline 小部件是 ChatGPT 整体 UI 的一部分,不是 2007 年的广告横幅。
其次,避免内部滚动条。 如果卡片长到出现自身的滚动条,说明哪里不对:要么内容塞太多,要么模式选错了(可能需要 fullscreen)。
第三,控制密度:
- 卡片之间要有明显间距;
- CTA 要易于点击(合理的内边距);
- 文本在手机上也应易读,避免过小字号。
这些听起来像“设计吹毛求疵”,但实践证明:当 inline 小部件看起来“原生”, 模型更愿意使用它,用户也更少困惑。
10. 实践:基于本讲改进 GiftGenius
如果你想巩固内容,这里有一份简单的实践清单:
先拿到工具 giftgenius.suggestGifts 的当前结果(礼物数组),然后:
- 在一个组件里实现三个不同的 UI 变体:
- GiftGrid(卡片网格);
- GiftList(文本列表);
- GiftCarousel(“上一页/下一页”的导航)。
- 分别为它们添加一到两个CTA 按钮:
- 卡片——“选择”;
- 列表——“查看详情”;
- 轮播——也用“选择”,并在小部件下方单独加一个“显示更多选项”。
- 根据状态(例如工具返回的礼物总数)决定采用哪种模式:
- 如果选项少(≤ 3)——卡片网格;
- 如果是很多文本创意——列表;
- 如果是很多可视化礼物——轮播。
这样不仅能练习 UI,还能开始根据上下文进行模式的动态选择, 这会让用户和 Store 的审核者都更满意。
总之,inline 模式是快速、轻量的 UI 层,直接生活在聊天流中,不试图取代独立应用。 卡片、列表与轮播覆盖 80% 的常见需求:展示备选、允许选择,并顺畅地继续对话。
下一讲我们将讨论当 inline “顶不住”时该怎么办: 拆解 fullscreen 向导、PiP 模式,以及你的 App 确实需要在 ChatGPT 内拥有一个大屏的场景。
11. 使用 inline 模式时的常见错误
错误 1:把 inline 小部件做成迷你网站。
有时开发者会试图把标签页、手风琴、表单、表格和一堆元素都塞进一张卡片。结果是沉重的 UI, 不仅打乱聊天的节奏,还在移动设备上很难用。指南明确指出:不要在 inline 卡片中做深层导航和复杂视图; 复杂场景请放到 fullscreen。
错误 2:CTA 按钮过多。
“我们在卡片上放‘查看详情’、‘购买’、‘收藏’、‘分享’、‘投诉’、‘生成贺卡’吧。” 最后用户也迷糊,模型也发懵,点中“正确按钮”的概率下降。记住规则:一个主 CTA,最多再加一个次级。 其他场景更适合放到 GPT 的后续消息或后续步骤里。
错误 3:在一个回答里无缘由地把列表、卡片、轮播混着用。
如果相同的内容时而列表、时而卡片、时而轮播(“因为我们能这么做”),用户会失去一致性体验。 更好的做法是为特定输出选定一个模式(例如无图创意——列表,有图礼物——轮播)并坚持使用。
错误 4:卡片信息过载。
一张卡里有三段描述、三种价格、两个“为何很棒”的区块,会变成文字墙。 用户就不再“扫描”,而是直接滑过。尽量只保留最重要的信息:标题、一个关键参数、 一条简短理由和 CTA。其他内容可以放在旁边 GPT 的文本回答里解释。
错误 5:只依赖 UI,忽略后续对话。
有时会见到“全部用按钮,用户不需要对话”的做法。这与 ChatGPT 的理念相悖。 Inline 小部件应当补充对话,而不是替代它。别忘了设计模型在小部件下方可以提供的后续选项: 修改筛选、请求更多选项、进入下一步。
错误 6:忽视元素数量的限制。
在一个 inline 小部件里做 25 张卡片的轮播,或 50 项的列表,基本会被用户整体滑过。 文档建议轮播 3–8 个、列表 5–10 个。如果数据更多,建议加上 CTA,如“显示更多”或“全部以文本显示”。
错误 7:在该用 fullscreen 时仍勉强用 inline。
有时会贪心地“都用 inline”,即使已经有 4 个步骤、十几个表单字段和大表格。 最后要么得到一个怪物,要么在卡片里造嵌套滚动与伪步骤器。 一旦感觉步骤和字段变多,就是考虑切到 fullscreen 向导的信号,把 inline 留给快速预览与行动摘要。
GO TO FULL VERSION