Skip to main content

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 컴포넌트를 추가하거나 이름·인자·사용 규칙을 변경하면, 프런트엔드 렌더링 카탈로그와 백엔드 시스템 프롬프트를 함께 갱신해야 한다.

현재 방식: 생성 후 명시적으로 복사

    1. 새 컴포넌트 구현과 명세를 OpenUI 라이브러리에 등록하고, 실제 렌더러의 지원도 확인한다. 생성용 export가 같은 카탈로그와 일치하도록 맞춘다.
    1. frontend에서 프롬프트를 생성하고 백엔드 자산으로 복사한다.
    1. 변경된 프롬프트에 새 컴포넌트의 이름·인자·규칙이 포함됐는지 확인한다.
[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 이름
레이아웃9Col, Row, Box, Grid, Card, Carousel, Popover, Divider, Spacer
텍스트·문서12Title, Text, Caption, Label, Badge, Link, List, ListItem, Markdown, Quote, Code, Math
입력·폼13Button, IconButton, Pressable, Input, TextArea, Select, SegmentedControl, Slider, Checkbox, RadioGroup, DatePicker, Timer, Form
미디어·데이터8SearchImage, Image, Icon, Table, Chart, Map, MapMarker, YouTube
학습·피드백2Flashcard, Celebration
도형·HTML11Svg, G, Circle, Ellipse, Rect, Line, Path, Polyline, Polygon, SvgText, HtmlView

MathBlock은 DSL에서 Math, MapView는 Map으로 등록되어 호출됩니다.

2. Functions (59개)​

브라우저 환경에서 실행되는 UI 내장 계산 함수 목록입니다 (@함수명(...) 형태로 호출).

분류개수등록 함수 목록
목록 (Lists)19Append, Prepend, Concat, Remove, RemoveAt, Update, Toggle, Includes, Find, IndexOf, Take, Slice, Unique, Reverse, Range, Pick, Shuffle, Flatten, Chunk
수치 (Numbers)9Pow, Sqrt, Random, Clamp, Percent, Lerp, Allocate, Payment, Convert
문자열 (Text)12Format, Join, Plural, Pad, Upper, Lower, Trim, Capitalize, Split, Replace, Duration, Default
날짜 (Dates)8Now, FormatDate, AddDays, DaysBetween, Today, AddMonths, MonthsBetween, StartOf
데이터 (Data)10GroupBy, Pivot, Top, Between, Bucket, Cumulative, MovingAvg, Change, Histogram, Stats
색상 (Colors)1MixColor

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)
DrawingSVG 10개, HtmlViewSVG 기반 도식/다이어그램 및 샌드박스형 HTML/JS 임베드

질의​

질문, openui-lang을 사용하게 되면 그럼 마크다운 출력대신에 openui lang으로만 대답하게 되는거야 ?​

  1. **OpenUI 모드만 사용해도 일반 텍스트와 Markdown을 충분히 출력할 수 있다.
  • TextContent와 MarkDownRenderer에 설명 문장이나 Markdown을 담아 렌더링한다.
  1. OpenUI의 인라인 모드 (inlineMode: 텍스트 + UI 혼합)
  • promptOptions: { inlineMode: true }를 주면 프롬프트에 ## Inline Mode 지침이 포함된다.
  • 단순 질문일 때: UI 코드를 생성하지 않고 일반 챗봇처럼 순수 텍스트/마크다운으로만 응답한다 (기존 UI 유지).
  • UI 생성/수정 요청일 때: 마크다운 텍스트 설명과 함께 openui-lang ... 코드 펜스로 UI 코드를 감싸서 출력한다.