Open UI + Langgraph Runbook (Frontend Side)
OpenUI Lang
자체 컴포넌트로 인터페이스를 기술하는 선언형 DSL이다.
- JSON 기반 포맷 대비 절반 미만의 토큰으로 모델이 UI를 생성할 수 있으며, 스트리밍에 맞춰 렌더러가 점진적으로 화면을 표시한다.
- 사용하는 이유: 1. JSON 기반 대비 토큰 67% 감소 2. 스트리밍이 가능한 언어의 필요성(호이스팅 허용 -> 점진적 렌더링 가능)
Open UI - system prompt
모델에 전달하는 시스템 프롬프트이다. 모델이 openui-lang 전용 문법으로 응답하도록 지시한다.
- 주요 내용: openui-lang 문법에 맞는 코드로 응답하도록 지시하고 component catalog 정보를 제공한다.
- UI에서 사용하는 컴포넌트는 프런트엔드에 있으므로
pnpm generate명령어로 시스템 프롬프트를 생성해야 한다.- system-prompt.txt: 모델에게 전달할 프롬프트 (spec.json의 내용을 설명에 포함)
- system-prompt.spec.json: 컴포넌트 구조와 타입 명세
system-prompt.spec.json: Open UI 컴포넌트 입력 규격을 명세한다.
- root: 최상위 컴포넌트 이름
- components: 82개 컴포넌트의 호출 형식(signature)과 설명(description)이 있다.
- componentGroups: 82개 컴포넌트를 묶는 상위 그룹이다. 예를 들어 레이아웃에서는 Stack, Tabs, TabItem, Accordion 등을 사용하도록 안내한다. 총 10개 그룹으로 구성되어 있다.
- 레이아웃, 콘텐츠, 테이블, 차트, 폼, 버튼, 데이터 표현, 카드
UI 컴포넌트 카탈로그와 LangGraph 시스템 프롬프트 동기화 방법
LLM이 생성할 OpenUI 컴포넌트를 추가하거나 이름·인자·사용 규칙을 변경하면, 프런트엔드 렌더링 카탈로그와 백엔드 시스템 프롬프트를 함께 갱신해야 한다.
현재 방식: 생성 후 명시적으로 복사
- 새 컴포넌트 구현과 명세를 OpenUI 라이브러리에 등록하고, 실제 렌더러의 지원도 확인한다. 생성용 export가 같은 카탈로그와 일치하도록 맞춘다.
frontend에서 프롬프트를 생성하고 백엔드 자산으로 복사한다.
- 변경된 프롬프트에 새 컴포넌트의 이름·인자·규칙이 포함됐는지 확인한다.
[Frontend: 카탈로그 SSOT]
├── components (React UI 컴포넌트)
├── defineFunction() (사용자 정의 계산 함수)
└── defineAction() (사용자 정의 버튼 액션)
│
▼ (npm run generate)
[빌드 타임 프롬프트 생성 (@openuidev/cli)]
├── src/generated/spec.json (컴포넌트/함수 메타데이터 스펙)
└── src/generated/system-prompt.txt (순수 openui-lang 지침 텍스트)
│
▼ (명시적 파일 복사: cp)
[Python LangGraph 백엔드]
└── backend-langgraph/agent/system-prompt.txt
│ + 런타임 추가 지시문 (도구 설명, Mock 데이터 안내)
▼
[create_deep_agent()] ──► [LLM (OpenAI)]
│ (openui-lang 코드 생성)
▼
[브라우저 OpenUI 런타임]
├── React 컴포넌트 렌더링
└── 클라이언트 fn 로직 직접 실행
defineFunction과 defineAction
- defineFunction : 순수 함수 정의
- defineAction: 사이드 이펙트(부수 효과) 함수 정의
defineFunction 예제
// 정의와 등록
import { defineFunction } from "@openuidev/react-lang";
import { openuiLibrary, openuiPromptOptions } from "@openuidev/react-ui/genui-lib";
import { z } from "zod/v4";
const DiscountPrice = defineFunction({
name: "DiscountPrice",
description: "가격에 할인율(0~100%)을 적용한 금액",
params: z.object({
price: z.number(),
discountPercent: z.number().min(0).max(100),
}),
returns: z.number(),
fn: ({ price, discountPercent }) => price * (1 - discountPercent / 100),
});
export const library = openuiLibrary.extend({
functions: { add: [DiscountPrice] },
});
export const promptOptions = openuiPromptOptions;
---
root = Stack([title, priceText, finalPriceText])
title = TextContent("결제 금액 안내", "large-heavy")
$originalPrice = 50000
$discount = 20
# @DiscountPrice로 선언한 함수 호출
discounted = @DiscountPrice($originalPrice, $discount)
priceText = TextContent("정가: 50,000원")
finalPriceText = TextContent("할인가: " + discounted + "원", "large-heavy")
import { defineAction, type ActionEvent } from "@openuidev/react-lang";
import { openuiLibrary } from "@openuidev/react-ui/genui-lib";
import { z } from "zod/v4";
// 1. 액션 스펙 정의 (이벤트 이름과 파라미터 규격)
export const CopyToClipboard = defineAction({
name: "CopyToClipboard",
description: "주어진 텍스트를 사용자의 클립보드에 복사합니다.",
params: z.object({
text: z.string().describe("복사할 문자열"),
}),
});
// 2. 라이브러리에 등록
export const customLibrary = openuiLibrary.extend({
actions: { add: [CopyToClipboard] },
});
---
import { AgentInterface } from "@openuidev/react-ui";
import { customLibrary } from "./library";
export default function Page() {
// 버튼 클릭 시 커스텀 액션 이벤트를 받아 브라우저 동작 실행
const handleAction = async (event: any) => {
if (event.type === "CopyToClipboard") {
const text = event.params?.text;
if (text) {
await navigator.clipboard.writeText(text);
alert("클립보드에 복사되었습니다: " + text);
}
}
};
return (
<AgentInterface
llm={llm}
componentLibrary={customLibrary}
onAction={handleAction} // <-- 여기서 커스텀 액션 수신!
/>
);
}
---
root = Stack([couponCode, copyBtn])
couponCode = TextContent("할인 쿠폰: DISCOUNT2026", "large-heavy")
# 버튼의 action에 @CopyToClipboard 액션을 연결!
copyBtn = Button("쿠폰 복사하기", @CopyToClipboard("DISCOUNT2026"), "primary")
Button example
Open UI의 Button 컴포넌트를 호출하는 부분을 보면 Label과 Action 두 가지 인자를 전달한다.
- Label: 버튼에 표시할 텍스트
- Action: 액션으로 해석할 수 있는 표현식이 들어간다.
// @Set이라는 로컬 상태를 $count에 1을 더한 값으로 변경하라는 의미
root = Col([Button("증가", @Set($count, $count + 1)), Text("횟수: " + $count)])
$count = 0
// action() → 이 인자는 액션을 받는 자리, .optional() → 생략할 수 있음
action: action().optional()
들어갈 수 있는 대표 사례입니다.
// 상태 변경
Button("증가", @Set($count, $count + 1))
// 초기값 복원
Button("초기화", @Reset($count))
// 후속 대화 요청
Button("설명", @ToAssistant("자세히 설명해줘"))
// URL 열기
Button("사이트", @OpenUrl("https://example.com"))
// 컴포넌트 정의
// Actions
export const Button = defineComponent({
name: "Button",
props: z.object({
label: z.string(),
action: action().optional(),
variant: z.enum(["solid", "outline", "soft", "ghost"]).optional(),
color: z.enum(["primary", "danger", "success"]).optional(),
size: z.enum(["sm", "md", "lg"]).optional(),
icon: z.string().optional(),
disabled: z.boolean().optional(),
block: z.boolean().optional(),
}),
description: "icon is a lucide name.",
component: ({ props }) => {
// useRunAction()은 컴포넌트의 클릭을 OpenUI 런타임의 액션 실행기로 연결하는 래퍼 훅입니다
const run = useRunAction();
const streaming = useIsStreaming(); // 스트리밍 동안 disabled 처리
const label = String(props.label ?? "");
return (
<button
type="button"
className={cx(
"iui-button",
`iui-button-${props.variant ?? "solid"}`,
props.color && props.color !== "primary" && `iui-button-${props.color}`,
`iui-button-${props.size ?? "md"}`,
props.block && "iui-block",
)}
disabled={!!props.disabled || streaming}
onClick={() => run(label, props.action, true)}
>
{props.icon ? <Glyph name={props.icon} size={props.size === "sm" ? 14 : 16} /> : null}
{label}
</button>
);
},
});
---
/* 컴포넌트의 클릭을 OpenUI 런타임 액션 실행기에 연결합니다. */
function useRunAction() {
// Renderer의 React Context에서 액션 실행 함수를 가져옵니다.
const trigger = useTriggerAction();
// 가장 가까운 부모 Form의 식별자를 가져옵니다. Form 밖에서는 undefined입니다. 실행기는 이 식별자로 이벤트에 포함할 폼 데이터의 범위를 선택합니다.
const formName = useFormName();
// message: 버튼 라벨 등의 요청 문장, plan: DSL에서 해석된 액션 계획,
// sendLabel: plan이 없을 때 message를 후속 대화 요청으로 보낼지 여부입니다.
return (message: string, plan: unknown, sendLabel: boolean) => {
// 액션이 있으면 해당 계획을 실행합니다.
// Set 안의 계산식도( 예를들어, Button("실행", @Set($result, @Pick(["A","B","C"]))) 의 Pick) 실제 클릭 시점에 평가됩니다.
// void는 반환 Promise를 여기서 기다리지 않는다는 뜻이며, 실행 자체는 즉시 요청
if (plan) void trigger(message, formName, plan as Plan);
// 액션이 없고 sendLabel=true이면 라벨을 대화 이벤트로 전달합니다. 실제 모델 호출은 호스트가 결정합니다.
else if (sendLabel) void trigger(message, formName);
};
}
---
// trigger("실행", "formName" , @Pick(["A","B","C"] )
Button("실행", @Set($result, @Pick(["A","B","C"])))
Form example
export const Form = defineComponent({
name: "Form",
props: z.object({ children }),
description: "Groups fields; their values go with any @ToAssistant inside.",
component: function FormView({ props, renderNode, statementId }) {
// DSL 선언 이름(statementId)이 없는 인라인 Form에도 고유한 식별자를 제공합니다.
const id = useId();
return (
// 자식 입력·버튼이 useFormName()으로 현재 Form의 식별자를 읽도록 전달합니다.
<FormNameContext.Provider value={statementId ?? id}>
{/* 브라우저 기본 제출·페이지 이동을 막고 버튼의 OpenUI 액션으로 처리합니다. */}
<form className="iui-form" onSubmit={(event) => event.preventDefault()}>
{/* DSL의 자식 노드를 실제 React 입력·버튼 등으로 렌더링합니다. */}
{renderNode(props.children)}
</form>
</FormNameContext.Provider>
);
},
});
AgentInterface
AgentInterface: 풀스택 챗봇 애플리케이션의 화면 전체(All-in-one Shell)를 제공하는 최상위 컴포넌트
- 사이드바 (Sidebar) : 이전 대화 목록 (Thread History 히스토리 브라우징) + 새 대화 시작 버튼 (New Chat) + 테마 모드 토글 (라이트/다크)
- 환영 화면 (Welcome Screen) : 서비스 이름/로고 + 대화 시작 추천 칩/카드 (starters)
- 채팅 스레드 영역 (Chat Thread Area) : 사용자 메시지와 에이전트 답변 말풍선 + 도구 실행 상태 렌더러 (Tool Call 타임라인/카드) + openui-lang 코드를 컴포넌트(openuiLibrary)로 변환하여 보여주는 생성형 UI 영역
- 입력창 영역 (Composer): 텍스트 입력창, 전송 버튼, 중단(Stop) 버튼
- 아티팩트(Artifact) 패널 분할 뷰
// 컴포넌트 라이브러리 적용
export {
openuiLibrary as library,
openuiPromptOptions as promptOptions,
} from "@openuidev/react-ui/genui-lib";
---
import { AgentInterface, agUIAdapter, fetchLLM, useSystemThemeMode } from "@openuidev/react-ui";
import { openuiLibrary } from "@openuidev/react-ui/genui-lib";
import { useMemo } from "react";
export default function Page() {
// OS(운영체제)의 다크모드/라이트모드 설정을 실시간으로 감지하고 구독하는 React Hook입
const mode = useSystemThemeMode();
const llm = useMemo(
() =>
fetchLLM({
url: "/api/chat",
streamAdapter: agUIAdapter(),
}),
[],
);
return (
<div className="app-shell">
<AgentInterface
llm={llm}
componentLibrary={openuiLibrary}
agentName="OpenUI + DeepAgents Chat"
theme={{ mode }}
starterVariant="short"
starters={[
{
displayText: "Weather in Tokyo",
prompt: "What's the weather like in Tokyo right now?",
},
{ displayText: "AAPL stock price", prompt: "What's the current Apple stock price?" },
{
displayText: "Research a topic",
prompt: "Give me a quick briefing on the James Webb Space Telescope.",
},
{
displayText: "Compare cities",
prompt: "Compare the weather in London and Sydney right now.",
},
]}
/>
</div>
);
}
---
/*
* 브라우저의 AG-UI 요청을 Python LangGraph protocol v2 실행으로 연결합니다. JS 프록시가 임시 thread와 스트림 수명주기를 관리합니다.
*/
import { createLangChainStreamResponse } from "@openuidev/langchain";
import { NextRequest } from "next/server";
export const runtime = "nodejs";
/**
* Proxies the browser <-> LangGraph server. The browser posts native AG-UI
* messages; {@link createLangChainStreamResponse} converts them to LangChain
* messages, starts the graph over the protocol-v2 endpoints, and relays only
* the custom OpenUI channel.
*/
export async function POST(req: NextRequest) {
return createLangChainStreamResponse(req, {
apiUrl: API_URL,
assistantId: ASSISTANT_ID,
apiKey: process.env.LANGSMITH_API_KEY,
debug: process.env.NODE_ENV !== "production",
});
}
---
// global.css
@import "@openuidev/react-ui/styles/index.css";
Compound Component + Slot 패턴
AgentInterface.ThreadHeader,AgentInterface.SidebarFooter처럼 관련 컴포넌트를 부모 API 아래에 묶어 조합하는 방식은 Compound Component(복합 컴포넌트) 패턴이다.- 이 앱의 OpenUI 구현은 전달받은 자식을 정해진 화면 영역에 배치하는 Slot 패턴을 함께 사용한다.
장점: AgentInterface를 사용하면 UI를 간편하게 구성할 수 있다. 특정 부분(예를 들어 footer 영역)을 원하는 UI로 변경하려면 AgentInterface.SidebarFooter를 정의해서 AgentInterface의 children으로 전달하면 된다.
- 기본적으로 AgentInterface에서 제공하는 컴포넌트를 사용한다. 커스텀 UI를 전달하면 해당 컴포넌트로 대체된다.
- Compound Component 패턴을 사용하므로
AgentInterface.ThreadList는 props를 주입받는다. - Slot 패턴을 사용하므로 레이아웃은 직접 수정할 수 없다.
<AgentInterface llm={llm} componentLibrary={library}>
{/* AgentInterface.ThreadHeader override 하는 컴포넌트 */}
<AgentInterface.ThreadHeader
showChatTitle={false}
end={<ReportDocumentInput />}
/>
<AgentInterface.SidebarFooter>
<AgentInterface.ThemeModeToggle mode={mode} onModeChange={setPreference} />
</AgentInterface.SidebarFooter>
<SaveMessages />
</AgentInterface>
Process:
// 1. 관련 컴포넌트를 부모 API에 노출한다.
AgentInterface.ThreadHeader = ThreadHeader;
// 2. 컴포넌트 타입과 슬롯 이름을 연결한다.
const SLOT_KEY_BY_TYPE = new Map([
// key - 같은 함수 객체를 가리키는가?”라는 점에서 포인터와 비슷하게 이해하면 됩니다.
[ThreadHeader, "threadHeader"],
[SidebarFooter, "sidebarFooter"],
// 나머지 슬롯 생략
]);
// 3. children에서 등록된 타입을 찾아 슬롯으로 분류한다.
const slots = useMemo(() => extractSlots(children), [children]);
// extractSlots 내부 핵심: 유효한 React 요소인지 확인한 뒤 실행한다.
const key = SLOT_KEY_BY_TYPE.get(child.type);
if (key) {
// 실제 구현은 중복 슬롯이면 첫 번째만 사용한다.
result[key] = child;
} else {
result.rest.push(child);
}
// 4. JSX에 작성한 순서 대신, 부모가 정한 화면 위치에 배치한다.
{slots.threadHeader ?? <ThreadHeader />}
{slots.sidebarFooter}
{slots.rest}
주의
- 직접 자식으로 선언해야 하는 이유: 현재
extractSlots()는Children.forEach(children, ...)로 자식을 순회하지만 자식의props.children을 재귀 탐색하지 않는다. useMemo는children참조가 바뀌면 슬롯 분류를 다시 계산하도록 캐시한다. 슬롯을 식별하는 기준은 컴포넌트 이름 문자열이나 실제 DOM 태그가 아니라 React 요소의type참조다.AgentInterface.ThreadHeader가 동일한ThreadHeader를 참조하므로 Map에서 찾을 수 있다.
Open UI Lang 사전 지식
- $로 시작하는 변수는 상태(State) 변수입니다.
- @로 시작하는 함수는 내장 표현식/액션 함수입니다 (@Set, @Clamp, @Each, @Includes, @Toggle, @CopyToClipboard 등).
- @Set은 상태 변수의 값을 바꾸는 내장 액션이다.
- 대문자로 시작하는 함수(Col, Row, Grid, Card, Title, Text, Badge, Button 등)는 UI 컴포넌트입니다.
- 일반 변수는 접두사 없이 그냥 사용하면 된다.
- Call expression: 대문자로 시작하는 이름 + 괄호 형태. 예: Col(..), @Each(..)
@Set은 상태 변수의 값을 바꾸는 내장 액션입니다.
root = Col([Button("실행", @Set($result, @Pick(["A","B","C"]))), Text($result == null ? "null" : "결과: " + $result)])
$result = null
---
@Set($result, @Pick(["A","B","C"]))
└ 대상 └ 새로 저장할 값
이 예제는 다음 순서로 동작합니다.
1. 처음에는 $result = null이므로 화면에 "null"이 표시됩니다.
2. 실행 버튼을 누르면 @Pick(["A","B","C"])가 하나를 뽑습니다.
3. @Set이 뽑은 값을 $result에 저장합니다.
4. $result를 읽는 Text(...)가 갱신됩니다.
렌더러와 런타임의 내부 처리 로직 (실제 라이브러리 기반 간소화)
OpenUI는 (1) 평가(Evaluation) 단계 (@openuidev/lang-core)와 (2) 렌더링(Rendering) 단계 (@openuidev/react-lang)의 2단계로 동작합니다.
1단계: AST 평가 단계 (evaluate in @openuidev/lang-core)
코드 파싱 후 만들어진 AST 노드를 순회하며 상태, 변수, 함수, 액션, 컴포넌트 호출을 각각 알맞은 값으로 평가합니다.
function evaluate(node, context) {
switch (node.k) {
// ① 상태 변수 ($guests, $got 등)
case "StateRef":
return context.getState(node.n);
// ② 일반 변수 참조 (lines, ingredients, head 등)
case "Ref":
case "RuntimeRef":
return context.resolveRef(node.n);
// ③ @내장함수, @액션, 커스텀 함수 (@Each, @Clamp, @Set 등)
case "Comp": {
// 1) 내장/커스텀 함수 (@Each, @Format, @Clamp 등): 즉시 실행해 결과값 반환
if (context.functions[node.name]) {
return callFunction(node.name, node.args, context);
}
// 2) 액션 (@Set, @Toggle, @CopyToClipboard 등): 클릭 시 실행될 액션 플랜({ steps: [...] }) 반환
if (context.actions[node.name]) {
return createActionPlan(node.name, node.args, context);
}
// 3) UI 컴포넌트 (Col, Card, Button 등): props를 평가한 뒤 ElementNode 반환
return {
type: "element",
typeName: node.name, // "Card", "Col" 등
props: evaluateProps(node.args, context),
statementId: node.statementId,
};
}
// ④ 데이터 구조 및 리터럴 (배열, 객체, 문자열, 숫자 등)
case "Arr": return node.els.map(el => evaluate(el, context));
case "Obj": return Object.fromEntries(node.entries.map(([k, v]) => [k, evaluate(v, context)]));
case "Str": case "Num": case "Bool": return node.v;
case "Null": return null;
}
}
2단계: React 렌더링 단계 (renderDeep in @openuidev/react-lang)
평가된 결과 트리에서 UI 컴포넌트(type === "element")와 일반 데이터(lines, string, number 등)를 구별하여 React 화면을 그립니다.
function renderDeep(value) {
if (value == null) return null;
// ① 일반 문자열/숫자/불리언 -> 텍스트로 바로 출력
if (typeof value === "string") return value;
if (typeof value === "number" || typeof value === "boolean") return String(value);
// ② 배열 -> 자식 요소들을 재귀적으로 렌더링
if (Array.isArray(value)) {
return value.map((v, i) => <Fragment key={i}>{renderDeep(v)}</Fragment>);
}
// ③ UI 컴포넌트 객체 -> 라이브러리에 등록된 실제 React 컴포넌트로 렌더링
if (typeof value === "object" && value !== null) {
if (value.type === "element") {
const Comp = library.components[value.typeName]?.component;
if (!Comp) return null;
return <Comp props={value.props} renderNode={renderDeep} />;
}
}
// ④ 그 외 일반 순수 데이터 객체 (lines의 { name, amount } 등)
// UI 노드가 아니므로 화면에 그리지 않고 무시(null) 처리 (참조 데이터로만 쓰임)
return null;
}
Intelligent UI Catalog
1. Components (55개)
| 분류 | 개수 | 등록된 DSL 이름 |
|---|---|---|
| 레이아웃 | 9 | Col, Row, Box, Grid, Card, Carousel, Popover, Divider, Spacer |
| 텍스트·문서 | 12 | Title, Text, Caption, Label, Badge, Link, List, ListItem, Markdown, Quote, Code, Math |
| 입력·폼 | 13 | Button, IconButton, Pressable, Input, TextArea, Select, SegmentedControl, Slider, Checkbox, RadioGroup, DatePicker, Timer, Form |
| 미디어·데이터 | 8 | SearchImage, Image, Icon, Table, Chart, Map, MapMarker, YouTube |
| 학습·피드백 | 2 | Flashcard, Celebration |
| 도형·HTML | 11 | Svg, G, Circle, Ellipse, Rect, Line, Path, Polyline, Polygon, SvgText, HtmlView |
MathBlock은 DSL에서Math,MapView는Map으로 등록되어 호출됩니다.
2. Functions (59개)
브라우저 환경에서 실행되는 UI 내장 계산 함수 목록입니다 (@함수명(...) 형태로 호출).
| 분류 | 개수 | 등록 함수 목록 |
|---|---|---|
| 목록 (Lists) | 19 | Append, Prepend, Concat, Remove, RemoveAt, Update, Toggle, Includes, Find, IndexOf, Take, Slice, Unique, Reverse, Range, Pick, Shuffle, Flatten, Chunk |
| 수치 (Numbers) | 9 | Pow, Sqrt, Random, Clamp, Percent, Lerp, Allocate, Payment, Convert |
| 문자열 (Text) | 12 | Format, Join, Plural, Pad, Upper, Lower, Trim, Capitalize, Split, Replace, Duration, Default |
| 날짜 (Dates) | 8 | Now, FormatDate, AddDays, DaysBetween, Today, AddMonths, MonthsBetween, StartOf |
| 데이터 (Data) | 10 | GroupBy, Pivot, Top, Between, Bucket, Cumulative, MovingAvg, Change, Histogram, Stats |
| 색상 (Colors) | 1 | MixColor |
3. Actions (1개)
버튼 클릭 등 사용자 이벤트에 반응하여 브라우저 부수 효과를 수행하는 커스텀 액션입니다.
| 액션명 | 파라미터 | 설명 |
|---|---|---|
CopyToClipboard | { text: string } | 지정한 텍스트를 클립보드에 복사 |
4. Component Groups & 가이드
| 그룹명 | 대상 컴포넌트 | 주요 사용 규칙 및 가이드 |
|---|---|---|
| Layout | 레이아웃 9개 | gap/padding은 4px 배수 단위(단계), 길이는 px 또는 CSS 단위, 곡률(radius) 및 색상 토큰 적용 |
| Text | 텍스트·문서 12개 | 마크다운 포맷팅 및 인라인 뱃지/아이콘 조합 지원 |
| Inputs | 입력·폼 13개 | 첫 번째 인자로 반응형 $variable 바인딩, 계산식 변경 또는 Form 제출과 연계 |
| Media & Data | 미디어·데이터 8개 | SearchImage 검색 쿼리 매핑, 시계열/카테고리형 차트 및 지도 마커 렌더링 |
| Play | 인터랙티브 2개 | 플래시카드 뒤집기 인터랙션, 조건 달성 시 축하 효과 (Celebration) |
| Drawing | SVG 10개, HtmlView | SVG 기반 도식/다이어그램 및 샌드박스형 HTML/JS 임베드 |
질의
질문, openui-lang을 사용하게 되면 그럼 마크다운 출력대신에 openui lang으로만 대답하게 되는거야 ?
- **OpenUI 모드만 사용해도 일반 텍스트와 Markdown을 충분히 출력할 수 있다.
TextContent와MarkDownRenderer에 설명 문장이나 Markdown을 담아 렌더링한다.
- OpenUI의 인라인 모드 (
inlineMode: 텍스트 + UI 혼합)
promptOptions: { inlineMode: true }를 주면 프롬프트에## Inline Mode지침이 포함된다.- 단순 질문일 때: UI 코드를 생성하지 않고 일반 챗봇처럼 순수 텍스트/마크다운으로만 응답한다 (기존 UI 유지).
- UI 생성/수정 요청일 때: 마크다운 텍스트 설명과 함께
openui-lang ...코드 펜스로 UI 코드를 감싸서 출력한다.