@json-render/ink
Ink 終端機渲染器,將 JSON 規格轉換成交互式終端機元件樹,支援標準元件、資料綁定、可見性判斷、動作處理與動態屬性。
快速開始
import { defineCatalog } from "@json-render/core";
import { schema } from "@json-render/ink/schema";
import {
standardComponentDefinitions,
standardActionDefinitions,
} from "@json-render/ink/catalog";
import { defineRegistry, Renderer, type Components } from "@json-render/ink";
import { z } from "zod";
// 建立目錄(包含標準元件及自訂元件)
const catalog = defineCatalog(schema, {
components: {
...standardComponentDefinitions,
CustomWidget: {
props: z.object({ title: z.string() }),
slots: [],
description: "自訂小工具",
},
},
actions: standardActionDefinitions,
});
// 僅註冊自訂元件(標準元件已內建)
const { registry } = defineRegistry(catalog, {
components: {
CustomWidget: ({ props }) => <Text>{props.title}</Text>,
} as Components<typeof catalog>,
});
// 渲染
function App({ spec }) {
return (
<JSONUIProvider initialState={{}}>
<Renderer spec={spec} registry={registry} />
</JSONUIProvider>
);
}
規格結構(扁平元素對應表)
Ink 的規格使用扁平的元素對應表,並以一個根鍵值作為開頭:
{
"root": "main",
"elements": {
"main": {
"type": "Box",
"props": { "flexDirection": "column", "padding": 1 },
"children": ["heading", "content"]
},
"heading": {
"type": "Heading",
"props": { "text": "儀表板", "level": "h1" },
"children": []
},
"content": {
"type": "Text",
"props": { "text": "來自終端機的問候!" },
"children": []
}
}
}
標準元件
佈局
Box- Flexbox 佈局容器(類似終端機的<div>)。用於分組、間距、邊框、對齊。預設 flexDirection 為 row。Text- 文字輸出,可選樣式(顏色、粗體、斜體等)Newline- 插入空白行。必須放在 flexDirection 為 column 的 Box 內。Spacer- 可沿主軸擴展的彈性空白空間。
內容
Heading- 區段標題(h1:粗體+底線,h2:粗體,h3:粗體+暗淡,h4:暗淡)Divider- 水平分隔線,可選置中標題Badge- 彩色行內標籤(變體:default、info、success、warning、error)Spinner- 帶有可選標籤的動畫載入旋轉器ProgressBar- 水平進度條(0-1)Sparkline- 使用 Unicode 區塊字元的行內圖表BarChart- 帶標籤與數值的水平長條圖Table- 含標題行與資料行的表格List- 項目符號或數字清單ListItem- 結構化清單行,包含標題、副標題、前導/結尾文字Card- 帶邊框的容器,可選標題KeyValue- 鍵值對顯示Link- 可點擊的 URL,可選標籤StatusLine- 帶有彩色圖示的狀態訊息(info、success、warning、error)Markdown- 以終端機樣式渲染 Markdown 文字
互動式
TextInput- 文字輸入欄位(事件:submit、change)Select- 使用方向鍵導航的選單(事件:change)MultiSelect- 按空格切換的多選(事件:change、submit)ConfirmInput- 是/否確認提示(事件:confirm、deny)Tabs- 使用左右方向鍵導航的分頁列(事件:change)
可見性條件
使用元素的 visible 屬性,根據狀態控制顯示/隱藏。語法:{ "$state": "/path" }、{ "$state": "/path", "eq": value }、{ "$state": "/path", "not": true }、{ "$and": [cond1, cond2] } 表示 AND、{ "$or": [cond1, cond2] } 表示 OR。
動態屬性表達式
任何屬性值都可以是資料驅動的表達式,在渲染時解析:
{ "$state": "/state/key" }- 從狀態模型中讀取(單向讀取){ "$bindState": "/path" }- 雙向綁定:用於表單元件的自然值屬性{ "$bindItem": "field" }- 雙向綁定至重複項目的欄位{ "$cond": <條件>, "$then": <值>, "$else": <值> }- 條件值{ "$template": "Hello, ${/name}!" }- 將狀態值插值到字串中
元件不使用 statePath 屬性進行雙向綁定。請改用 { "$bindState": "/path" } 於自然值屬性上。
事件系統
元件使用 emit 觸發具名事件。元素的 on 欄位將事件對應到動作綁定:
CustomButton: ({ props, emit }) => (
<Box>
<Text>{props.label}</Text>
{/* emit("press") 將觸發規格 on.press 中綁定的動作 */}
</Box>
),
{
"type": "CustomButton",
"props": { "label": "提交" },
"on": { "press": { "action": "submit" } },
"children": []
}
內建動作
setState、pushState 和 removeState 為內建動作,會自動處理:
{ "action": "setState", "params": { "statePath": "/activeTab", "value": "home" } }
{ "action": "pushState", "params": { "statePath": "/items", "value": { "text": "New" } } }
{ "action": "removeState", "params": { "statePath": "/items", "index": 0 } }
重複(動態清單)
在容器元素上使用 repeat 欄位,從狀態陣列渲染項目:
{
"type": "Box",
"props": { "flexDirection": "column" },
"repeat": { "statePath": "/items", "key": "id" },
"children": ["item-row"]
}
在重複的子元素內,使用 { "$item": "field" } 讀取當前項目的欄位,使用 { "$index": true } 取得當前索引。
串流
使用 useUIStream 從 JSONL 修補串流逐步渲染規格:
import { useUIStream } from "@json-render/ink";
const { spec, send, isStreaming } = useUIStream({ api: "/api/generate" });
伺服器端提示生成
使用 ./server 匯出,從您的目錄生成 AI 系統提示詞:
import { catalog } from "./catalog";
const systemPrompt = catalog.prompt({ system: "你是一個終端機助手。" });
提供者
| 提供者 | 用途 |
|---|---|
StateProvider | 跨元件共享狀態(JSON Pointer 路徑)。接受可選的 store 屬性以使用受控模式。 |
ActionProvider | 處理透過事件系統分派的動作 |
VisibilityProvider | 根據狀態啟用條件式渲染 |
ValidationProvider | 表單欄位驗證 |
FocusProvider | 管理互動式元件之間的焦點 |
JSONUIProvider | 所有上下文提供者的組合 |
外部儲存(受控模式)
將 StateStore 傳遞給 StateProvider(或 JSONUIProvider)以使用外部狀態管理:
import { createStateStore, type StateStore } from "@json-render/ink";
const store = createStateStore({ count: 0 });
<StateProvider store={store}>{children}</StateProvider>
store.set("/count", 1); // React 會自動重新渲染
當提供了 store 時,initialState 和 onStateChange 將被忽略。
createRenderer(高階 API)
import { createRenderer } from "@json-render/ink";
import { standardComponents } from "@json-render/ink";
import { catalog } from "./catalog";
const InkRenderer = createRenderer(catalog, {
...standardComponents,
// 在此覆寫自訂元件
});
// InkRenderer 包含所有提供者(狀態、可見性、動作、焦點)
render(
<InkRenderer spec={spec} state={{ activeTab: "overview" }} />
);
主要匯出
| 匯出名稱 | 用途 |
|---|---|
defineRegistry | 從目錄建立型別安全的元件註冊表 |
Renderer | 使用註冊表渲染規格 |
createRenderer | 高階:建立包含內建提供者的元件 |
JSONUIProvider | 所有上下文提供者的組合 |
schema | Ink 扁平元素對應表規格(包含內建狀態動作) |
standardComponentDefinitions | 所有標準元件的目錄定義 |
standardActionDefinitions | 標準動作的目錄定義 |
standardComponents | 預建立的元件實作 |
useStateStore | 存取狀態上下文 |
useStateValue | 從狀態中取得單一值 |
useBoundProp | $bindState/$bindItem 表達式的雙向綁定 |
useActions | 存取動作上下文 |
useAction | 取得單一動作分派函式 |
useOptionalValidation | useValidation 的非拋出變體 |
useUIStream | 從 API 端點串流規格 |
createStateStore | 建立與框架無關的記憶體 StateStore |
StateStore | 插入外部狀態管理的介面 |
Components | 型別化的元件映射(可感知目錄) |
Actions | 型別化的動作映射(可感知目錄) |
ComponentContext | 型別化的元件上下文(可感知目錄) |
flatToTree | 將扁平元素對應表轉換為樹狀結構 |
終端機 UI 設計指南
- 使用 Box 進行佈局(flexDirection、padding、gap)。預設 flexDirection 為 row。
- 終端機寬度約為 80-120 欄。主要結構建議使用垂直佈局(flexDirection: column)。
- 在 Box 上使用 borderStyle 進行視覺分組(single、double、round、bold)。
- 使用標準終端機顏色:red、green、yellow、blue、magenta、cyan、white、gray。
- 使用 Heading 作為區段標題、Divider 分隔區段、Badge 標示狀態、KeyValue 顯示標籤資料、Card 作為邊框群組。
- 使用 Tabs 搭配子內容的可見性條件,實現多檢視 UI。
- 使用 Sparkline 顯示行內趨勢、使用 BarChart 比較數值。