@xbrain-swe/nova-chat-widget 0.3.0-dev.17 → 0.3.0-dev.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +62 -29
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -2
- package/dist/index.d.ts +7 -2
- package/dist/index.js +62 -29
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/provider.tsx","../src/client-actions.tsx","../src/internal-context.tsx","../src/built-in-facts.ts","../src/nudge-context.tsx","../src/nudge-loop.tsx","../src/track.tsx","../src/chat.tsx","../src/shadow-root.tsx","../src/use-chat.ts","../src/stream.ts","../src/views.tsx","../src/facts.ts"],"sourcesContent":["/**\n * `@xbrain-swe/nova-chat-widget` 공개 API. 여기서 안 내보내는 건 전부 내부 구현이다(`nova/docs/SDK.md`\n * 참고) — 모든 공개 이름은 `XbNova`/`useXbNova` 접두사를 붙인다.\n */\n\nexport { XbNovaProvider } from \"./provider\";\nexport { XbNovaChat, type XbNovaOpenEvent } from \"./chat\";\nexport {\n type XbNovaClientAction,\n type XbNovaClientActions,\n type XbNovaClientActionResult,\n} from \"./client-actions\";\nexport {\n useXbNovaChat,\n type UseXbNovaChatResult,\n type XbNovaOnboarding,\n type XbNovaFeedbackEvent,\n type XbNovaFeedbackRating,\n type XbNovaFeedbackReason,\n} from \"./use-chat\";\nexport { XbNovaTrack, useXbNovaTrack } from \"./track\";\nexport { useXbNovaFact, useXbNovaFacts, useXbNovaContext, useXbNovaVisitor, XbNovaFact, XbNovaFacts } from \"./facts\";\nexport { trackXbNovaEvent } from \"./nudge-loop\";\nexport { useXbNovaSettings } from \"./nudge-context\";\nexport type { ViewKind, ChatMessage, MessagePart, TextPart, ToolPart } from \"./stream\";\nexport type { ViewRenderer, ViewRendererMap, ProductItem, VerdictOutput, VerdictAction } from \"./views\";\nexport type { FactValue, XbNovaContextEntry, XbNovaTheme } from \"./internal-context\";\n","\"use client\";\n\n/**\n * 앱 루트에서 한 번 초기화 — `useXbNovaChat`/`<XbNovaChat>`/`XbNovaTrack`/넛지 루프가 전부\n * 여기서 인증 정보를 읽는다. 넛지 평가는 `<XbNovaChat>`과 무관하게 Provider가 직접 돌린다\n * (\"설치만 하면 채팅·동행 바가 다 도는 게 기본값\" — SDK.md). 실제 동행 바(idle/hint/sheet)\n * UI는 `<XbNovaChat>` 하나가 그린다 — 루프는 `nudge-context`에 힌트만 밀어넣는다.\n *\n * SDK.md 예시엔 없던 `appId`가 필수 prop이다: `GET /v1/apps/{app_id}/nudge-graph`,\n * `POST /v1/events` 둘 다 App 스코프 엔드포인트라 app_id가 URL/바디에 그대로\n * 필요한데, apiKey는 해시로 저장돼 있어 브라우저에서 역산할 방법이 없다 — 명시적으로\n * 받는다.\n */\n\nimport { useEffect, useMemo } from \"react\";\n\nimport { type XbNovaClientActions, XbNovaClientActionSync } from \"./client-actions\";\nimport { DEFAULT_THEME, type Auth, InternalProvider, type XbNovaTheme } from \"./internal-context\";\nimport { markSessionStart, bumpVisitCount } from \"./built-in-facts\";\nimport { NudgeStateProvider } from \"./nudge-context\";\nimport { XbNovaNudgeLoop } from \"./nudge-loop\";\n\nconst DEFAULT_API_BASE_URL = \"http://localhost:10100\";\n\nexport function XbNovaProvider({\n apiKey,\n botId,\n appId,\n screen,\n apiBaseUrl = DEFAULT_API_BASE_URL,\n clientActions,\n theme,\n userKey,\n threadKey,\n sessionKey,\n children,\n}: {\n apiKey: string;\n botId: string;\n appId: string;\n screen?: string;\n apiBaseUrl?: string;\n /** 이 방문자를 가리키는 호스트 쪽 키(예: GA4 user_pseudo_id). nova가 남기는 방문자 ID(대화·넛지\n * 이벤트·피드백) 전부가 이 값이 되고, `threadKey`와 함께 이전 대화를 찾는 데도 쓴다. 공개 키로 조회하므로 추측하기 어려운 값이어야 한다 — 회원 ID를 그대로\n * 넘기지 않는다. 안 주면 SDK가 브라우저에 만들어 둔 `visitorId`를 쓴다. */\n userKey?: string;\n /** 대화를 이어 붙일 단위(예: `vehicle:${매물ID}`). 같은 유저 키·스레드 키로 위젯이 다시 열리면\n * 이전 대화와 이력을 복원한다. 안 주면 열 때마다 새 대화다. */\n threadKey?: string;\n /** 이 방문의 세션 키(예: GA4 session_id). nova가 남기는 이벤트(위젯 노출·넛지·상담 버튼)와 대화에 실려\n * 콘솔에서 세션 단위로 집계된다. 값이 바뀌면(세션 갱신) 그 뒤 이벤트부터 새 값으로 실린다. 안 주면 세션은 비운다. */\n sessionKey?: string;\n /** 이 페이지가 AI에게 내어줄 client action tool — 이름별로 설명·스키마·실행 함수를\n * 등록한다(\"SDK 초기화 시점에 등록\"). 스펙은 App 레벨 레지스트리로 자동 동기화되고\n * (`XbNovaClientActionSync`), 콘솔의 tool 목록에 반영된다. 핸들러는 여기서만 산다. */\n clientActions?: XbNovaClientActions;\n /** 위젯 아바타·pill 배경에 쓰는 브랜드 컬러(그라디언트 시작/끝). 안 주면 SDK 기본\n * 톤(인디고→바이올렛)을 쓴다 — 소비자 사이트 톤에 안 맞으면 여기서 자기 브랜드 컬러로\n * 바꾸면 된다(예: `{accentFrom: \"#BD1B21\", accentTo: \"#6B1317\"}`). */\n theme?: Partial<XbNovaTheme>;\n children: React.ReactNode;\n}) {\n useEffect(() => {\n if (apiKey.startsWith(\"sk_\")) {\n console.error(\n \"[XbNova] sk_ 키를 브라우저에 노출했습니다 — pk_(publishable) 키를 쓰세요. sk_는 서버 전용입니다.\",\n );\n }\n }, [apiKey]);\n\n useEffect(() => {\n markSessionStart();\n }, []);\n\n useEffect(() => {\n if (screen) bumpVisitCount(screen);\n }, [screen]);\n\n const auth = useMemo<Auth>(\n () => ({\n apiKey,\n botId,\n appId,\n apiBaseUrl,\n screen,\n userKey: userKey || undefined,\n threadKey: threadKey || undefined,\n sessionKey: sessionKey || undefined,\n }),\n [apiKey, botId, appId, apiBaseUrl, screen, userKey, threadKey, sessionKey],\n );\n\n const resolvedTheme = useMemo<XbNovaTheme>(\n () => ({ ...DEFAULT_THEME, ...theme }),\n [theme],\n );\n\n // 핸들러+awaitResult만 뽑아 Map으로 — internal-context는 이 Map을 그대로 들고 있다가\n // use-chat.ts가 tool 이름으로 찾아 디스패치한다. 스펙(설명·스키마) 쪽은 핸들러와 별개로\n // XbNovaClientActionSync가 서버에 동기화한다.\n const clientActionHandlers = useMemo(() => {\n if (!clientActions) return undefined;\n return new Map(\n Object.entries(clientActions).map(([name, spec]) => [\n name,\n {\n handler: spec.handler,\n awaitResult: spec.awaitResult ?? false,\n timeoutMs: spec.timeoutMs,\n dismissSheet: spec.dismissSheet ?? false,\n render: spec.render,\n },\n ]),\n );\n }, [clientActions]);\n\n return (\n <InternalProvider auth={auth} clientActionHandlers={clientActionHandlers} theme={resolvedTheme}>\n <NudgeStateProvider>\n <XbNovaNudgeLoop appId={appId} />\n <XbNovaClientActionSync actions={clientActions} />\n {children}\n </NudgeStateProvider>\n </InternalProvider>\n );\n}\n","\"use client\";\n\n/**\n * client action tool — AI가 부르면 MCP 서버가 아니라 이 SDK를 설치한 호스트 페이지가 직접\n * 실행하는 tool(`nova/api`의 `ClientActionTool`과 같은 개념, `chat.tsx` 상단 doc 참고).\n *\n * 스펙(설명·입력 스키마)의 진실의 원천은 클라이언트다 — 이 페이지가 실제로 뭘 할 수\n * 있는지(어떤 region이 있는지, payload 모양이 뭔지)는 콘솔이 아니라 이 코드가 제일\n * 정확히 안다. 그래서 `<XbNovaProvider clientActions={{...}}>`로 등록하면, 여기(SDK)가\n * App 레벨 레지스트리로 스펙을 동기화하고(`XbNovaClientActionSync`), 콘솔은 그걸\n * 읽기 전용으로 보여준다 — 관리자가 스키마를 손으로 타이핑하다 페이지 구조와 어긋나는\n * 걸 막는다. 핸들러 함수 자체는 직렬화할 수 없어서 동기화 대상이 아니다 — 이 프로세스\n * 안에서만 산다(`internal-context`의 `clientActionHandlers`로 `chat.tsx`가 디스패치).\n */\n\nimport { useEffect, useRef } from \"react\";\n\nimport { useXbNovaInternal } from \"./internal-context\";\nimport type { ViewKind } from \"./stream\";\n\n/** `awaitResult` 핸들러가 구조화된 결과를 돌려줄 때의 모양. `text`는 모델이 읽는 결과(서버로\n * 전송)고, `view`+`output`은 채팅 말풍선에 직접 그릴 정규화된 데이터다 — 서버에는 가지 않는다.\n * 렌더러는 tool 이름이 아니라 `view`로 고르므로(`views.tsx`) 고객사가 UI를 만들 필요가 없다.\n * 예: 상품 검색 결과는 `view:\"product_list\"`, `output:{ items: ProductItem[], total }`.\n * MCP 서버를 쓸 수 없는 환경에서 MCP tool 결과의 `_meta` view와 같은 효과를 내는 대체 경로다.\n * 대화를 새로고침하면 이 그림은 사라진다(서버에는 `text`만 남는다). */\nexport type XbNovaClientActionResult = {\n text: string;\n view?: ViewKind;\n output?: unknown;\n};\n\nexport type XbNovaClientAction = {\n /** 모델이 \"언제 이 tool을 부를지\" 판단하는 근거 — MCP tool description과 같은 역할. */\n description: string;\n /** JSON Schema. 안 주면 인자 없는 tool로 노출된다. */\n inputSchema?: Record<string, unknown>;\n /** `awaitResult`가 false(기본, fire-and-forget)일 때만 쓴다 — 모델에게 되돌려줄 순수\n * 텍스트 템플릿, `{필드명}`으로 이 tool의 input을 치환한다(예:\n * `\"{region} 섹션을 화면에 표시했습니다.\"`). 안 주면 서버 기본값(\"완료했습니다.\")을 쓴다. */\n resultText?: string;\n /** true면 이건 \"보여주기\"가 아니라 \"진짜 데이터를 조회해오는\" tool이다 — `handler`가\n * 반환한 값이 실제 tool 결과로 서버에 다시 전송되고(`POST\n * /v1/conversations/{id}/tool-results`), AI는 그 데이터로 이어서 대화한다(LangGraph의\n * interrupt/resume과 같은 패턴). false(기본)면 `handler`는 그냥 실행되고 끝 —\n * 반환값은 안 쓴다. */\n awaitResult?: boolean;\n /** `awaitResult:true`일 때 핸들러 응답을 기다리는 최대 시간(ms, 기본 15000). 넘으면 SDK가\n * \"오류: 응답 시간이 초과되었습니다.\"를 tool 결과로 서버에 보내 모델이 대화를 이어가게\n * 한다 — 이후 핸들러가 뒤늦게 resolve돼도 그 값은 무시된다. fire-and-forget에는 적용 안 된다. */\n timeoutMs?: number;\n /** 실제로 실행할 동작 — 채팅에는 아무것도 안 그리고 이 함수만 호출된다. `payload`는 이\n * tool 호출의 input을 그대로 되돌려준다. `awaitResult:true`면 반환값(또는 resolve된\n * Promise)이 진짜 tool 결과 텍스트가 된다 — 비워두면 \"핸들러가 결과를 반환하지\n * 않았습니다\" 에러로 처리된다. */\n handler: (\n payload: unknown,\n ) => void | string | XbNovaClientActionResult | Promise<void | string | XbNovaClientActionResult>;\n /** true면 이 tool이 실행되는 시점에 대화 시트를 닫는다(대화는 안 사라진다, 나중에\n * 다시 열면 이어짐). `highlight_region`처럼 \"페이지의 특정 부분을 보여주는\" 액션에\n * 필요하다 — 모바일에서는 시트가 화면 대부분(위 7rem 제외)을 덮어서, 시트를 안 닫으면\n * 스크롤·하이라이트가 그 밑에 가려 사용자 눈에 전혀 안 보인다. */\n dismissSheet?: boolean;\n /** 주면 이 tool 호출을 채팅 말풍선 자리에 직접 그린다(안 주면 기존처럼 아무것도 안\n * 그리고 `handler`만 조용히 실행된다). \"상담신청 버튼을 대화 안에 띄워줘\"처럼, 결과가\n * 텍스트가 아니라 실제 페이지 UI(버튼·팝업 트리거 등)여야 하는 client action을 위한\n * 자리다. `payload`는 이 tool 호출의 input을 그대로 받는다 — 서버에는 안 보낸다(순수\n * 렌더링용이라 `handler`처럼 서버 스펙 동기화 대상이 아니다). */\n render?: (payload: unknown) => React.ReactNode;\n};\n\nexport type XbNovaClientActions = Record<string, XbNovaClientAction>;\n\nfunction toSyncPayload(actions: XbNovaClientActions) {\n return {\n actions: Object.entries(actions).map(([name, spec]) => ({\n name,\n description: spec.description,\n input_schema: spec.inputSchema ?? { type: \"object\", properties: {} },\n result_text: spec.resultText ?? \"완료했습니다.\",\n await_result: spec.awaitResult ?? false,\n })),\n };\n}\n\n/** `<XbNovaProvider>`가 내부적으로 마운트한다 — `XbNovaNudgeLoop`와 같은 자리의 \"설치만\n * 하면 끝\" 백그라운드 컴포넌트. 스펙이 이전에 보낸 것과 내용이 같으면(문자열 비교) 다시\n * 안 보낸다 — 방문자마다 매번 같은 내용을 다시 쓰는 낭비를 막는다. */\nexport function XbNovaClientActionSync({ actions }: { actions?: XbNovaClientActions }) {\n const { auth } = useXbNovaInternal();\n const { apiKey, apiBaseUrl, appId } = auth;\n const lastSyncedRef = useRef<string | null>(null);\n\n useEffect(() => {\n if (!actions || Object.keys(actions).length === 0) return;\n const serialized = JSON.stringify(toSyncPayload(actions));\n if (serialized === lastSyncedRef.current) return;\n lastSyncedRef.current = serialized;\n\n fetch(`${apiBaseUrl}/v1/apps/${appId}/client-actions`, {\n method: \"PUT\",\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${apiKey}` },\n body: serialized,\n }).catch(() => {\n // 동기화 실패해도 대화 자체는 동작해야 한다 — 다음 마운트 때 다시 시도된다.\n });\n }, [actions, apiKey, apiBaseUrl, appId]);\n\n return null;\n}\n","\"use client\";\n\n/**\n * `<XbNovaProvider>` 내부 상태. 공개 API가 아니다 — `useXbNovaChat`/`XbNovaTrack`/\n * `useXbNovaFact`/넛지 엔진이 전부 여기서 읽는다.\n *\n * track/facts 레지스트리는 React state가 아니라 ref로 든다 — dwell 추적이나 폴링마다\n * 리렌더를 유발하면 안 되고, \"값이 필요한 순간\"(넛지 평가, context 조립)에만 pull로\n * 읽으면 되기 때문이다(SDK.md의 \"push가 아니라 pull\" 규칙).\n */\n\nimport { createContext, useCallback, useContext, useEffect, useMemo, useRef } from \"react\";\n\nimport type { XbNovaClientActionResult } from \"./client-actions\";\n\nexport type FactValue = unknown | (() => unknown);\n\nexport type TrackEntry = {\n region: string;\n data: FactValue;\n /** 사람이 읽는 이름 — 힌트 문구·대화 오프닝에 쓴다(\"보증 정보를 보고 계시네요\"). 안 주면\n * region 키를 그대로 쓴다. */\n label?: string;\n enteredAt: number | null; // 지금 보이는 중이면 그 시작 시각(ms), 안 보이면 null\n totalMs: number; // 이전까지 누적된 체류 시간(ms) — 지금 보이는 중이면 여기에 안 더해져 있다\n /** 지금 마운트된 실제 DOM 노드 — 넛지 루프가 트리거별 \"화면 몇 % 구간에 있는지\" 밴드\n * 판정을 직접 하려면 위치(`getBoundingClientRect`)를 재야 해서 필요하다. 언마운트되면\n * null. */\n node: HTMLElement | null;\n};\n\n/** `useXbNovaContext`로 호스트가 AI에게 알려주는 상품·화면 부가 설명 한 건. 내용은 전부\n * 호스트 자율이다 — 가입 됨/안됨 같은 판단도 호스트가 `description`에 문장으로 쓴다. */\nexport type XbNovaContextEntry = {\n title: string;\n description: string;\n};\n\nexport type Auth = {\n apiKey: string;\n botId: string;\n appId: string;\n apiBaseUrl: string;\n screen?: string;\n /** 호스트가 정한 유저 키(카통령은 GA4 user_pseudo_id). 있으면 `visitorId`가 이 값이 된다. */\n userKey?: string;\n /** 호스트가 정한 대화 단위(카통령은 `vehicle:{매물ID}`). 있으면 같은 유저 키·스레드 키로 다시 열 때\n * 이전 대화를 이어 연다. */\n threadKey?: string;\n /** 호스트가 정한 세션 키(카통령은 GA4 session_id). nova가 남기는 이벤트·대화에 실려 세션 단위 집계에 쓴다. */\n sessionKey?: string;\n};\n\n/** 위젯 분석 이벤트 이름. 새 이벤트는 여기에 추가하고 `sendEvent`로 보낸다 — 서버 수정은 필요 없다.\n * 대화 내용 같은 사용자 입력 원문은 속성에 싣지 않는다. */\nexport type WidgetEventName =\n | \"widget_view\"\n | \"nudge_impression\"\n | \"nudge_open\"\n | \"chat_open\"\n | \"chat_close\"\n | \"message_send\"\n | \"feedback_submit\"\n | \"cta_impression\"\n | \"cta_click\";\n\n/** 평평한 원시값만 싣는다. `conversation_id`는 서버가 대화의 넛지·봇을 찾는 데 쓰고, `occurred_at`은\n * 전송보다 앞선 실제 발생 시각이다(없으면 보내는 시각). */\nexport type WidgetEventProperties = Record<string, string | number | boolean | null | undefined>;\n\n/** 위젯 아바타·pill 배경 그라디언트에 쓰는 두 색상 — 기본값은 SDK 자체 톤(인디고→바이올렛)\n * 이고, 소비자가 `<XbNovaProvider theme={{accentFrom, accentTo}}>`로 자기 브랜드 컬러를\n * 넘기면 그걸 쓴다(\"카통령 브랜드 컬러를 따르게 해달라\" — 파랑/보라가 사이트 톤과 안\n * 어울린다는 피드백). CSS 커스텀 프로퍼티(`--xbnova-accent-1/2`)로 shadow host에 실려\n * shadow DOM 안까지 상속된다(styles.css의 `.xbnova-glow-bg`, chat.tsx의 아바타 배지 참고). */\nexport type XbNovaTheme = {\n accentFrom: string;\n accentTo: string;\n /** 위젯의 라이트/다크. `\"system\"`(기본)은 브라우저·OS 설정을 따른다 — 호스트 사이트가 다크\n * 모드를 지원하지 않는데 사용자 OS만 다크이면 사이트는 밝은데 위젯만 어두워진다. 그런 사이트는\n * `\"light\"`로 고정하고, 사이트 자체 테마를 가진 호스트는 그 값을 넘기면 된다. */\n colorScheme: \"light\" | \"dark\" | \"system\";\n};\n\nexport const DEFAULT_THEME: XbNovaTheme = {\n accentFrom: \"#4f46e5\",\n accentTo: \"#7c3aed\",\n colorScheme: \"system\",\n};\n\n/** tool 이름 → 실행부. `handler`의 반환값은 `awaitResult:true`일 때만 의미가 있다 — 진짜\n * tool 결과 텍스트가 돼서 서버로 다시 전송된다(`use-chat.ts`). */\nexport type ClientActionHandler = {\n handler: (\n payload: unknown,\n ) => void | string | XbNovaClientActionResult | Promise<void | string | XbNovaClientActionResult>;\n awaitResult: boolean;\n /** awaitResult 핸들러 타임아웃(ms). 없으면 use-chat의 기본값(15000)을 쓴다. */\n timeoutMs?: number;\n dismissSheet: boolean;\n render?: (payload: unknown) => React.ReactNode;\n};\n\nexport type InternalState = {\n auth: Auth;\n trackRegistry: Map<string, TrackEntry>;\n factsRegistry: Map<string, FactValue>;\n /** `useXbNovaContext`가 등록한 부가 설명 — 훅 인스턴스(id)별 값 또는 함수. 메시지를 보내는\n * 순간에 pull로 읽어 챗 `context.notes`로 싣는다. */\n contextRegistry: Map<string, XbNovaContextEntry[] | (() => XbNovaContextEntry[])>;\n /** `<XbNovaProvider clientActions={{...}}>`에서 등록한 것 — `use-chat.ts`가 client\n * action tool 호출을 받으면 이 맵에서 찾아 디스패치한다. */\n clientActionHandlers: Map<string, ClientActionHandler>;\n visitorId: string;\n /** 호스트 세션 키. 없으면 null — SDK가 따로 만들지 않는다(세션 규칙은 호스트 분석 도구와 맞춰야 의미가 있다). */\n sessionId: string | null;\n /** 분석 이벤트의 유일한 전송 지점. app_id·visitor_id·session_id·screen은 보내는 순간의 최신 값을\n * 자동으로 싣는다. 실패는 조용히 버린다. */\n sendEvent: (event: WidgetEventName, properties?: WidgetEventProperties) => void;\n theme: XbNovaTheme;\n};\n\nconst InternalContext = createContext<InternalState | null>(null);\n\nexport function useXbNovaInternal(): InternalState {\n const ctx = useContext(InternalContext);\n if (!ctx) {\n throw new Error(\n \"XbNova 훅/컴포넌트는 <XbNovaProvider> 안에서만 쓸 수 있습니다. 앱 루트를 <XbNovaProvider>로 감싸세요.\",\n );\n }\n return ctx;\n}\n\nconst VISITOR_STORAGE_KEY = \"xbnova_visitor_id\";\n\n// 위젯 노출(widget_view)을 호스트 세션 키가 붙을 때까지 기다리는 상한. GA처럼 페이지보다 늦게 뜨는 도구의\n// 값을 받으려는 것 — 끝까지 안 오면(세션 키를 안 주는 고객사, 동의 거부) 세션 없이 보낸다.\nconst WIDGET_VIEW_WAIT_MS = 10_000;\n\nfunction resolveVisitorId(): string {\n if (typeof window === \"undefined\") return \"\";\n try {\n const existing = window.localStorage.getItem(VISITOR_STORAGE_KEY);\n if (existing) return existing;\n const id = `v_${crypto.randomUUID()}`;\n window.localStorage.setItem(VISITOR_STORAGE_KEY, id);\n return id;\n } catch {\n // localStorage 접근 불가(시크릿 모드 등) — 이번 세션 동안만 쓰는 값으로 대체한다.\n return `v_${Math.random().toString(36).slice(2)}`;\n }\n}\n\n/** 값 또는 함수를 받아서 실제 값을 지금 이 순간에 읽는다 — pull 규칙의 유일한 구현부. */\nexport function resolveFactValue(value: FactValue): unknown {\n return typeof value === \"function\" ? (value as () => unknown)() : value;\n}\n\n/** `contextRegistry`에 등록된 모든 항목을 지금 이 순간에 읽어 하나의 배열로 합친다. 값이\n * 아닌 항목(형식 오류)은 서버가 한 번 더 거르지만 여기서도 버려 전송량을 줄인다. */\nexport function collectContextEntries(registry: InternalState[\"contextRegistry\"]): XbNovaContextEntry[] {\n const out: XbNovaContextEntry[] = [];\n for (const source of registry.values()) {\n const entries = typeof source === \"function\" ? source() : source;\n if (!Array.isArray(entries)) continue;\n for (const e of entries) {\n if (e && typeof e.title === \"string\" && typeof e.description === \"string\") out.push(e);\n }\n }\n return out;\n}\n\nexport function InternalProvider({\n auth,\n clientActionHandlers,\n theme,\n children,\n}: {\n auth: Auth;\n clientActionHandlers?: Map<string, ClientActionHandler>;\n theme: XbNovaTheme;\n children: React.ReactNode;\n}) {\n const trackRegistry = useRef<Map<string, TrackEntry>>(new Map()).current;\n const factsRegistry = useRef<Map<string, FactValue>>(new Map()).current;\n const contextRegistry = useRef<InternalState[\"contextRegistry\"]>(new Map()).current;\n const emptyHandlers = useRef<Map<string, ClientActionHandler>>(new Map()).current;\n\n const visitorIdRef = useRef<string | null>(null);\n if (visitorIdRef.current === null) {\n visitorIdRef.current = resolveVisitorId();\n }\n // 호스트가 유저 키를 주면 nova가 남기는 모든 방문자 기록(대화·넛지 이벤트·피드백)을 그 값으로\n // 맞춘다 — 대화만 호스트 키로 남으면 넛지 노출 → 대화 퍼널과 호스트 분석(GA) 조인이 끊긴다.\n const visitorId = auth.userKey || visitorIdRef.current!;\n const sessionId = auth.sessionKey || null;\n\n // 넛지 엔진처럼 오래 사는 콜백이 이벤트를 보낼 때도 지금의 유저·세션 키를 싣도록 ref로 읽는다.\n const latestRef = useRef({ auth, visitorId, sessionId });\n latestRef.current = { auth, visitorId, sessionId };\n const sendEvent = useCallback((event: WidgetEventName, properties: WidgetEventProperties = {}) => {\n const { auth: a, visitorId: v, sessionId: sid } = latestRef.current;\n if (!v) return;\n const { conversation_id: conversationId, occurred_at: occurredAt, ...rest } = properties;\n // keepalive — 페이지를 떠나는 순간의 이벤트(시트 닫기 뒤 이동 등)도 잃지 않는다.\n fetch(`${a.apiBaseUrl}/v1/events`, {\n method: \"POST\",\n keepalive: true,\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${a.apiKey}` },\n body: JSON.stringify({\n app_id: a.appId,\n event,\n visitor_id: v,\n session_id: sid,\n conversation_id: conversationId ?? null,\n occurred_at: occurredAt ?? new Date().toISOString(),\n properties: { screen: a.screen, ...rest },\n }),\n }).catch(() => {});\n }, []);\n\n // 위젯이 이 페이지에 떴다 — 순 방문자·세션의 분모. Provider 마운트당 한 번, 세션 키가 오면 바로 보낸다.\n // 전송은 세션 키를 기다려 늦어질 수 있어, 위젯이 뜬 시각을 따로 싣는다 — 안 그러면 먼저 뜬 넛지보다 늦게 기록돼\n // 순서 고정 퍼널에서 빠진다.\n const widgetViewSentRef = useRef(false);\n const mountedAtRef = useRef(new Date().toISOString());\n useEffect(() => {\n if (widgetViewSentRef.current) return;\n const send = () => {\n if (widgetViewSentRef.current) return;\n widgetViewSentRef.current = true;\n sendEvent(\"widget_view\", { occurred_at: mountedAtRef.current });\n };\n if (sessionId) {\n send();\n return;\n }\n const timer = setTimeout(send, WIDGET_VIEW_WAIT_MS);\n return () => clearTimeout(timer);\n }, [sessionId, sendEvent]);\n\n const value = useMemo<InternalState>(\n () => ({\n auth,\n trackRegistry,\n factsRegistry,\n contextRegistry,\n clientActionHandlers: clientActionHandlers ?? emptyHandlers,\n visitorId,\n sessionId,\n sendEvent,\n theme,\n }),\n [auth, trackRegistry, factsRegistry, contextRegistry, clientActionHandlers, emptyHandlers, visitorId, sessionId, sendEvent, theme],\n );\n\n return <InternalContext.Provider value={value}>{children}</InternalContext.Provider>;\n}\n","/**\n * 고객사가 등록하지 않아도 SDK가 스스로 계산하는 조건 값들. `useXbNovaFact`로 등록하는\n * 커스텀 facts와 달리 항상 존재하고, 넛지 엔진이 조건을 평가하는 매 순간 새로 계산한다.\n */\n\nlet sessionStart: number | null = null;\n\n/** `XbNovaProvider`가 마운트될 때 한 번 호출 — 세션 시작 시각을 고정한다. */\nexport function markSessionStart(): void {\n if (sessionStart === null) sessionStart = Date.now();\n}\n\nexport function getSessionDurationMs(): number {\n if (sessionStart === null) return 0;\n return Date.now() - sessionStart;\n}\n\nexport function getScrollDepth(): number {\n if (typeof document === \"undefined\" || typeof window === \"undefined\") return 0;\n const scrollable = document.documentElement.scrollHeight - window.innerHeight;\n if (scrollable <= 0) return 100;\n return Math.min(100, Math.max(0, Math.round((window.scrollY / scrollable) * 100)));\n}\n\nfunction visitCountKey(screen: string): string {\n return `xbnova_visit_count:${screen}`;\n}\n\n/** `screen`에 진입할 때마다 한 번 호출한다 — 누적 방문 횟수를 1 늘리고 그 값을 반환한다. */\nexport function bumpVisitCount(screen: string): number {\n if (typeof window === \"undefined\") return 1;\n try {\n const key = visitCountKey(screen);\n const next = (Number(window.localStorage.getItem(key)) || 0) + 1;\n window.localStorage.setItem(key, String(next));\n return next;\n } catch {\n return 1;\n }\n}\n\n/** 넛지 엔진이 조건 평가 때마다 읽는다 — `bumpVisitCount`와 달리 값을 늘리지 않는다. */\nexport function peekVisitCount(screen: string): number {\n if (typeof window === \"undefined\") return 0;\n try {\n return Number(window.localStorage.getItem(visitCountKey(screen))) || 0;\n } catch {\n return 0;\n }\n}\n","\"use client\";\n\n/**\n * 동행 바(idle/hint/sheet)와 넛지 평가 루프(`nudge-loop.tsx`)가 공유하는 상태. 루프가\n * `offerHint`로 힌트를 밀어넣으면 `<XbNovaChat>`이 바 UI에 그대로 반영한다 — 둘은 형제\n * 컴포넌트라 props로 못 넘기고 Context로 공유해야 한다.\n *\n * \"말 걸기 예절\"(기획서 4.1)의 페이지당/세션당 노출 횟수 상한은 트리거마다 넛지룰(콘솔\n * 그래프 에디터)로 설정한 값이라 `nudge-loop.tsx`가 그래프에서 직접 읽어 센다(SDK에\n * 하드코딩하지 않는다). 여기서는 힌트 자체의 생명주기(제안 → 소멸/닫기/열기)와, \"연속\n * 닫기 N회 → 먼저 말 걸기 자동 off\"의 N도 힌트를 낸 트리거의 넛지룰 값을 그대로 쓴다\n * (`NudgeHint.autoDisableAfterDismissals`) — 트리거마다 사용자가 참는 정도를 다르게\n * 설정해뒀을 수 있어서다.\n */\n\nimport { createContext, useCallback, useContext, useEffect, useRef, useState } from \"react\";\n\nexport type NudgeHint = {\n messageNodeId: string;\n /** 이 힌트를 낸 트리거 노드 id — 넛지 루프가 \"지금 뜬 힌트가 지금의 대표 트리거와 같은\n * 것인지\" 비교해 대표가 바뀌면 조용히 갈아탈 때 쓴다. */\n triggerNodeId: string;\n text: string;\n botId: string | null;\n /** 이 힌트를 낸 트리거의 넛지룰 값 — 몇 번 연속 닫아야 먼저 말 걸기를 자동으로 끌지. */\n autoDisableAfterDismissals: number;\n /** 이 힌트를 낸 트리거의 넛지룰 값 — 몇 ms 후 조용히 사라질지. 0이면 안 사라진다. */\n autoHideMs: number;\n /** 이 힌트를 눌러 열었을 때 보여줄 빠른 답변 버튼 — 비어 있으면 봇 기본 quick_replies를 쓴다. */\n quickReplies: string[];\n};\n\ntype NudgeContextValue = {\n hint: NudgeHint | null;\n /** 마지막으로 떴던 힌트 전체 — `hint`는 소멸(타임아웃)·닫힘·열림으로 null이 돼도 이건\n * 안 지워진다. 조용히 사라진 뒤(`autoHideMs` 경과) 원형으로 접힌 바를 사용자가 그래도\n * 눌렀을 때, 엉뚱한 기본 인사말이 아니라 방금 그 힌트가 열렸던 것과 똑같이(문구·quick\n * replies·handoff bot까지) 대화를 이어가기 위한 값이다. */\n lastHint: NudgeHint | null;\n /** 루프가 새 힌트를 제안한다 — 이미 뭔가 떠 있으면 무시한다(한 번에 하나만). */\n offerHint: (hint: NudgeHint) => void;\n /** 바의 ✕ — \"거절\"로 센다. 3회 연속이면 먼저 말 걸기를 자동으로 끈다. */\n dismissHint: () => void;\n /** 바 클릭으로 대화를 열 때 — 지금 힌트를 지우고, 연속 닫기 스트릭을 리셋한다(참여는\n * 거절이 아니다). 값을 반환하지 않는다 — 호출부가 이미 `hint`를 reactive 값으로 갖고\n * 있으니 클릭 핸들러에서 그걸 직접 읽으면 된다(아래 구현 주석 참고). */\n openHint: () => void;\n /** 조용한 소멸(타임아웃) — 힌트만 지운다. 거절로 안 친다. */\n autoHideHint: () => void;\n hintsEnabled: boolean;\n disableHints: () => void;\n enableHints: () => void;\n barHidden: boolean;\n hideBar: () => void;\n showBar: () => void;\n};\n\nconst NudgeContext = createContext<NudgeContextValue | null>(null);\n\nconst HINTS_DISABLED_KEY = \"xbnova_hints_disabled\";\nconst BAR_HIDDEN_KEY = \"xbnova_bar_hidden\";\nconst DISMISS_STREAK_KEY = \"xbnova_hint_dismiss_streak\";\n/** 힌트를 낸 트리거에 넛지룰 값이 없을 때만 쓰는 안전망 — 정상 경로는 항상 트리거의\n * `auto_disable_after_dismissals`를 쓴다. */\nconst DEFAULT_DISMISS_STREAK_LIMIT = 3;\n\nfunction readFlag(key: string): boolean {\n if (typeof window === \"undefined\") return false;\n try {\n return window.localStorage.getItem(key) === \"1\";\n } catch {\n return false;\n }\n}\n\nfunction writeFlag(key: string, value: boolean): void {\n if (typeof window === \"undefined\") return;\n try {\n if (value) window.localStorage.setItem(key, \"1\");\n else window.localStorage.removeItem(key);\n } catch {\n // localStorage 접근 불가 — 이번 세션은 메모리 상태로만 동작(이미 useState가 반영 중)\n }\n}\n\nfunction readStreak(): number {\n if (typeof window === \"undefined\") return 0;\n try {\n return Number(window.localStorage.getItem(DISMISS_STREAK_KEY)) || 0;\n } catch {\n return 0;\n }\n}\n\nfunction writeStreak(value: number): void {\n if (typeof window === \"undefined\") return;\n try {\n window.localStorage.setItem(DISMISS_STREAK_KEY, String(value));\n } catch {\n // no-op\n }\n}\n\nexport function NudgeStateProvider({ children }: { children: React.ReactNode }) {\n const [hint, setHint] = useState<NudgeHint | null>(null);\n const [lastHint, setLastHint] = useState<NudgeHint | null>(null);\n const [hintsEnabled, setHintsEnabled] = useState(() => !readFlag(HINTS_DISABLED_KEY));\n const [barHidden, setBarHidden] = useState(() => readFlag(BAR_HIDDEN_KEY));\n const streakRef = useRef(readStreak());\n\n const disableHints = useCallback(() => {\n setHintsEnabled(false);\n writeFlag(HINTS_DISABLED_KEY, true);\n }, []);\n const enableHints = useCallback(() => {\n setHintsEnabled(true);\n writeFlag(HINTS_DISABLED_KEY, false);\n streakRef.current = 0;\n writeStreak(0);\n }, []);\n const hideBar = useCallback(() => {\n setBarHidden(true);\n writeFlag(BAR_HIDDEN_KEY, true);\n }, []);\n const showBar = useCallback(() => {\n setBarHidden(false);\n writeFlag(BAR_HIDDEN_KEY, false);\n }, []);\n\n // 함수형 업데이터를 쓴다 — 클로저로 캡처한 바깥 `hint`를 직접 읽으면 안 된다. 넛지\n // 루프가 같은 tick 안에서 `autoHideHint()`(→`setHint(null)`) 호출 직후 곧바로\n // `offerHint(next)`를 부르는 \"조용히 갈아타기\" 경로가 있는데, React의 상태 갱신은\n // 비동기라 그 순간 이 함수가 클로저로 들고 있는 `hint`는 아직 옛 값(비어있지 않음)이라\n // 아래 가드에 걸려 새 힌트가 조용히 무시돼버렸다(실제로 이 버그 때문에 대표가 바뀌어도\n // 메시지가 안 바뀌는 문제가 있었다). 함수형 업데이터는 같은 배치 안의 앞선 `setHint(null)`\n // 결과를 그대로 이어받으므로 이 경합이 없다.\n const offerHint = useCallback((next: NudgeHint) => {\n setHint((current) => (current ? current : next));\n }, []);\n\n // hint가 실제로 바뀔 때만(비어있지 않은 새 값으로) lastHint를 갱신한다 — offerHint\n // 업데이터 안에서 직접 부르면 순수 함수여야 하는 업데이터에 부작용이 섞인다.\n useEffect(() => {\n if (hint) setLastHint(hint);\n }, [hint]);\n\n const dismissHint = useCallback(() => {\n const limit = hint?.autoDisableAfterDismissals ?? DEFAULT_DISMISS_STREAK_LIMIT;\n setHint(null);\n streakRef.current += 1;\n writeStreak(streakRef.current);\n if (streakRef.current >= limit) disableHints();\n }, [hint, disableHints]);\n\n // 예전엔 `setHint(current => { opened = current; return null; })`로 \"지우면서 읽기\"를\n // 한 번에 하려 했는데, 이건 React가 useState 업데이트를 \"즉시(eager) 계산\"해줄 때만\n // 동작하는 내부 구현 디테일에 기대는 패턴이라 — 이 컴포넌트가 다른 이유로 이미 리렌더가\n // 걸려 있는 순간(꽤 자주 있다, `hint`/`barHidden` 등이 자주 바뀌는 컨텍스트라서)에는\n // eager 계산이 스킵돼서 `opened`가 항상 초기값(null)으로 남았다 — 힌트 알약을 눌러도\n // 매번 \"힌트가 없다\"로 읽혀서 엉뚱한 폴백 인사말이 뜨는 버그였다. `openHint`는 이제\n // 지우기만 하고, 값 읽기는 호출부(`chat.tsx`)가 이미 갖고 있는 `hint` state를 클릭\n // 시점에 그대로 읽는다 — 일반적인 React state 읽기라 이런 함정이 없다.\n const openHint = useCallback(() => {\n setHint(null);\n streakRef.current = 0;\n writeStreak(0);\n }, []);\n\n const autoHideHint = useCallback(() => {\n setHint(null);\n }, []);\n\n return (\n <NudgeContext.Provider\n value={{\n hint,\n lastHint,\n offerHint,\n dismissHint,\n openHint,\n autoHideHint,\n hintsEnabled,\n disableHints,\n enableHints,\n barHidden,\n hideBar,\n showBar,\n }}\n >\n {children}\n </NudgeContext.Provider>\n );\n}\n\nexport function useNudgeState(): NudgeContextValue {\n const ctx = useContext(NudgeContext);\n if (!ctx) {\n throw new Error(\"useNudgeState는 <XbNovaProvider> 안에서만 쓸 수 있습니다.\");\n }\n return ctx;\n}\n\n/** 공개 API — 고객사가 설정 화면(F6)에서 동행 바/힌트를 끄고 켤 수 있게 노출한다. */\nexport function useXbNovaSettings() {\n const { hintsEnabled, disableHints, enableHints, barHidden, hideBar, showBar } = useNudgeState();\n return { hintsEnabled, disableHints, enableHints, barHidden, hideBar, showBar };\n}\n","\"use client\";\n\n/**\n * 넛지 평가 루프 — 렌더링은 안 한다(`null` 컴포넌트). `<XbNovaProvider>`가 마운트되면\n * 백그라운드로 같이 켜진다(설치만 하면 끝이라는 원칙 — SDK.md). `GET\n * /v1/apps/{app_id}/nudge-graph`를 마운트 시 한 번 불러오고, 이후 0.25초 간격으로 각\n * `trigger` 노드에서 시작해 엣지를 따라 조건을 검사해 힌트가 나오면 `nudge-context`에\n * 밀어넣는다 — 실제로 보여주는 건 `<XbNovaChat>`의 동행 바다.\n *\n * \"말 걸기 예절\"(기획서 4.1)의 빈도 상한은 세션당(`max_per_session`, 각 트리거 자기 것만\n * 센다, 0=무제한, 안 채웠으면 서버 기본값 3)만 본다 — 페이지당 상한(`max_per_page`)은\n * 규칙을 단순하게 가려고 없앴다. \"N회 연속 닫기 → 자동 off\"의 N도 힌트를 낸 트리거의\n * `auto_disable_after_dismissals`를 힌트에 실어 보내고, 실제 카운팅은 `nudge-context`가\n * 한다.\n *\n * dwell 누적은 다른 힌트가 이미 떠 있어도 멈추지 않는다 — 그때는 \"새로 보여주기\"만\n * 발동 직전(`canOffer`)에 막는다. 조건은 충족했는데 이미 힌트가 떠 있어서 못 보여준\n * 경우, 그 시도 자체는 세션 카운트에 잡히지 않는다(다음 tick에 다시 시도한다).\n *\n * dwell 트리거는 `region_dwell_ms`(트랙 레지스트리가 IntersectionObserver로 재는, \"한\n * 픽셀이라도 보이면\") 대신 트리거별 `region_visible_from_percent`~`to_percent` band를\n * 직접 잰다 — 스크롤로 스쳐 지나가는 것까지 dwell로 잡히지 않게, 매 tick마다\n * `getBoundingClientRect()`로 뷰포트 band와의 겹침을 확인해 트리거별로 누적한다\n * (`bandDwellRef`, `computeBandStatus`). 트리거마다 band가 달라질 수 있어서 region이 아니라\n * 트리거 단위로 센다.\n */\n\nimport { useCallback, useEffect, useRef } from \"react\";\n\nimport { getScrollDepth, getSessionDurationMs, peekVisitCount } from \"./built-in-facts\";\nimport { resolveFactValue, useXbNovaInternal } from \"./internal-context\";\nimport { useNudgeState } from \"./nudge-context\";\nimport { getRegionDwellMs } from \"./track\";\n\ntype TriggerType = \"dwell\" | \"visit_count\" | \"event\";\n\ntype NudgeNode = {\n id: string;\n kind: \"trigger\" | \"condition\" | \"message\" | \"ai_handoff\";\n trigger_type?: TriggerType | null;\n trigger_params?: Record<string, unknown> | null;\n max_per_page?: number | null;\n max_per_session?: number | null;\n auto_disable_after_dismissals?: number | null;\n auto_hide_ms?: number | null;\n quick_replies?: string[] | null;\n condition_field?: string | null;\n operator?: string | null;\n value?: unknown;\n messages?: string[] | null;\n bot_id?: string | null;\n};\n\ntype NudgeEdge = { from_id: string; to_id: string; branch_label?: \"true\" | \"false\" | null };\ntype NudgeGraph = { nodes: NudgeNode[]; edges: NudgeEdge[] };\n\n/** 판정 간격. dwell 누적은 `Date.now()` 차이로 더하므로 간격과 무관하게 정확하고, 간격은\n * 문턱을 넘은 뒤 실제로 발동하기까지의 양자화 지연(0~간격)만 정한다 — 1초면 멈춘 뒤 평균\n * 0.5초가 더 붙어 \"먼저 말 거는 게 느리다\"는 제보가 있었다(#59). */\nconst EVAL_INTERVAL_MS = 250;\nconst XBNOVA_EVENT_NAME = \"xbnova:event\";\nconst SESSION_COUNT_KEY_PREFIX = \"xbnova_hint_session_count:\";\n/** 이 시간(ms) 안에 스크롤 이벤트가 있었으면 \"아직 스크롤 중\"으로 보고 새 힌트를 띄우지\n * 않는다 — 스크롤 중에도 대표 region이면 dwell은 누적되므로, 멈춘 뒤 힌트까지의 지연은\n * 사실상 이 값이 바닥이다(dwell 문턱 0.5초 기준 멈춘 뒤 0.5~0.75초). */\nconst SCROLL_SETTLE_MS = 500;\n\n/** 이벤트 트리거를 발동시킨다 — 폴링을 기다리지 않고 그 즉시 평가한다. */\nexport function trackXbNovaEvent(eventName: string): void {\n if (typeof window === \"undefined\") return;\n window.dispatchEvent(new CustomEvent(XBNOVA_EVENT_NAME, { detail: { eventName } }));\n}\n\n/** 트리거별로 센다 — \"같은 이유의 힌트는 세션 내 반복하지 않습니다\"가 트리거 단위 규칙이라서다. */\nfunction peekSessionCount(triggerId: string): number {\n if (typeof window === \"undefined\") return 0;\n try {\n return Number(window.sessionStorage.getItem(SESSION_COUNT_KEY_PREFIX + triggerId)) || 0;\n } catch {\n return 0;\n }\n}\n\nfunction bumpSessionCount(triggerId: string): void {\n if (typeof window === \"undefined\") return;\n try {\n window.sessionStorage.setItem(SESSION_COUNT_KEY_PREFIX + triggerId, String(peekSessionCount(triggerId) + 1));\n } catch {\n // no-op\n }\n}\n\nfunction resolveConditionValue(\n field: string,\n ctx: {\n screen?: string;\n trackRegistry: Map<string, import(\"./internal-context\").TrackEntry>;\n factsRegistry: Map<string, import(\"./internal-context\").FactValue>;\n },\n): unknown {\n if (field === \"scroll_depth\") return getScrollDepth();\n if (field === \"session_duration_ms\") return getSessionDurationMs();\n if (field === \"visit_count\") return ctx.screen ? peekVisitCount(ctx.screen) : 0;\n if (field.startsWith(\"region_dwell_ms.\")) {\n return getRegionDwellMs(ctx.trackRegistry, field.slice(\"region_dwell_ms.\".length));\n }\n return resolveFactValue(ctx.factsRegistry.get(field));\n}\n\nfunction compare(actual: unknown, operator: string | null | undefined, expected: unknown): boolean {\n switch (operator) {\n case \"eq\":\n return String(actual) === String(expected);\n case \"ne\":\n return String(actual) !== String(expected);\n case \"gt\":\n return Number(actual) > Number(expected);\n case \"gte\":\n return Number(actual) >= Number(expected);\n case \"lt\":\n return Number(actual) < Number(expected);\n case \"lte\":\n return Number(actual) <= Number(expected);\n default:\n return false;\n }\n}\n\n/** dwell 이외 트리거 타입만 다룬다 — dwell은 트리거별 band 상태가 필요해서 `evaluate` 안에서\n * 따로 처리한다(`evaluateDwellBand`). */\nfunction triggerFires(\n node: NudgeNode,\n ctx: {\n screen?: string;\n firedEventName: string | null;\n },\n): boolean {\n const params = node.trigger_params ?? {};\n switch (node.trigger_type) {\n case \"visit_count\": {\n const threshold = Number(params.threshold ?? 0);\n return Boolean(ctx.screen) && peekVisitCount(ctx.screen!) >= threshold;\n }\n case \"event\":\n return ctx.firedEventName !== null && ctx.firedEventName === params.event_name;\n default:\n return false;\n }\n}\n\n/** 요소가 뷰포트 세로축 기준 `[fromPercent, toPercent]` 구간과 겹치는지 — \"화면 중앙\n * 근처에 있을 때만 보는 중으로 친다\" 판정의 핵심. 한 픽셀이라도 스쳐도 잡히는\n * `IntersectionObserver` threshold:0보다 \"실제로 눈에 담겼는가\"에 더 가깝다. 판정 근거를\n * 그대로 로그에 찍을 수 있도록 픽셀 값까지 함께 반환한다. */\nfunction computeBandStatus(\n el: HTMLElement,\n fromPercent: number,\n toPercent: number,\n): {\n inBand: boolean;\n overlapPx: number;\n rectTop: number;\n rectBottom: number;\n bandTop: number;\n bandBottom: number;\n vh: number;\n} {\n const rect = el.getBoundingClientRect();\n const vh = window.innerHeight;\n const bandTop = (vh * fromPercent) / 100;\n const bandBottom = (vh * toPercent) / 100;\n // 겹치는 세로 픽셀 폭 — 여러 region이 동시에 band 안에 있을 때 \"누가 가장 많이\n // 걸쳐 있는가\"로 대표를 가리는 데 쓴다(아래 `evaluate`의 대표 선정 로직 참고).\n const overlapPx = Math.max(0, Math.min(rect.bottom, bandBottom) - Math.max(rect.top, bandTop));\n const inBand = overlapPx > 0;\n return { inBand, overlapPx, rectTop: rect.top, rectBottom: rect.bottom, bandTop, bandBottom, vh };\n}\n\n/** 디버그 오버레이 on/off — `localStorage.xbnova_debug = \"1\"` 이거나 URL에 `?xbnova_debug=1`을\n * 붙이면 켜진다. dwell 대상 요소를 실제 화면 위에 색깔 박스로 그려서, \"지금 몇 개의\n * 컴포넌트가 dwell 설정돼 있고 각각 어떤 상태인지\"를 눈으로 바로 확인할 수 있게 한다. */\nfunction isDebugEnabled(): boolean {\n if (typeof window === \"undefined\") return false;\n try {\n if (new URLSearchParams(window.location.search).get(\"xbnova_debug\") === \"1\") return true;\n return window.localStorage.getItem(\"xbnova_debug\") === \"1\";\n } catch {\n return false;\n }\n}\n\nconst DEBUG_COLLAPSED_KEY = \"xbnova_debug_collapsed\";\n\nfunction readDebugCollapsed(): boolean {\n if (typeof window === \"undefined\") return true;\n try {\n return window.localStorage.getItem(DEBUG_COLLAPSED_KEY) !== \"0\";\n } catch {\n return true;\n }\n}\n\n/** 앱을 가리지 않게 접었다 펼 수 있는 패널. 예전엔 패널 전체가 `pointer-events: none`이라\n * 클릭도 스크롤도 아예 안 먹었다 — 긴 로그 줄이 `white-space: pre`로 안 줄바꿈되고 가로로\n * 넘쳤는데, 그걸 읽으려는 가로 스크롤 자체가 막혀 있었던 것. 헤더만 클릭 가능하게 만들고,\n * 본문은 줄바꿈시켜서 가로 스크롤이 아예 필요 없게 바꿨다. */\nfunction ensureDebugPanel(\n ref: { current: HTMLDivElement | null },\n collapsedRef: { current: boolean },\n): { body: HTMLDivElement } {\n if (ref.current) {\n return { body: ref.current.querySelector<HTMLDivElement>(\"[data-xbnova-debug-body]\")! };\n }\n const container = document.createElement(\"div\");\n container.style.position = \"fixed\";\n container.style.top = \"8px\";\n container.style.left = \"8px\";\n container.style.zIndex = \"2147483647\";\n container.style.maxWidth = \"min(420px, 90vw)\";\n container.style.borderRadius = \"8px\";\n container.style.background = \"rgba(17, 17, 17, 0.9)\";\n container.style.color = \"#fff\";\n container.style.font = \"11px/1.5 ui-monospace, monospace\";\n container.style.boxShadow = \"0 2px 12px rgba(0,0,0,0.4)\";\n\n const header = document.createElement(\"div\");\n header.style.display = \"flex\";\n header.style.alignItems = \"center\";\n header.style.justifyContent = \"space-between\";\n header.style.gap = \"8px\";\n header.style.padding = \"6px 10px\";\n header.style.cursor = \"pointer\";\n header.style.userSelect = \"none\";\n header.style.fontWeight = \"600\";\n\n const title = document.createElement(\"span\");\n title.textContent = \"XbNova dwell debug\";\n const toggle = document.createElement(\"span\");\n header.append(title, toggle);\n\n const body = document.createElement(\"div\");\n body.setAttribute(\"data-xbnova-debug-body\", \"1\");\n body.style.padding = \"0 10px 8px\";\n body.style.maxHeight = \"70vh\";\n body.style.overflowY = \"auto\";\n body.style.whiteSpace = \"pre-wrap\";\n body.style.wordBreak = \"break-all\";\n\n const applyCollapsed = () => {\n body.style.display = collapsedRef.current ? \"none\" : \"block\";\n toggle.textContent = collapsedRef.current ? \"▸ 펼치기\" : \"▾ 접기\";\n };\n applyCollapsed();\n\n header.addEventListener(\"click\", () => {\n collapsedRef.current = !collapsedRef.current;\n applyCollapsed();\n try {\n window.localStorage.setItem(DEBUG_COLLAPSED_KEY, collapsedRef.current ? \"1\" : \"0\");\n } catch {\n // no-op\n }\n });\n\n container.append(header, body);\n document.body.appendChild(container);\n ref.current = container;\n return { body };\n}\n\nfunction ensureDebugBox(map: Map<string, HTMLDivElement>, key: string): HTMLDivElement {\n let box = map.get(key);\n if (!box) {\n box = document.createElement(\"div\");\n box.style.position = \"fixed\";\n box.style.zIndex = \"2147483646\";\n box.style.boxSizing = \"border-box\";\n box.style.pointerEvents = \"none\";\n box.style.display = \"flex\";\n box.style.alignItems = \"flex-start\";\n box.style.font = \"11px/1.2 ui-monospace, monospace\";\n box.style.color = \"#fff\";\n box.style.padding = \"2px 4px\";\n document.body.appendChild(box);\n map.set(key, box);\n }\n return box;\n}\n\n/** dwell band(%)를 그대로 뷰포트 가로 전체 점선으로 그린다 — \"여기부터 여기까지가 중앙\n * band\"를 눈으로 확인하기 위함. 같은 band(%)를 쓰는 트리거끼리는 선을 공유한다. */\nfunction ensureBandLines(\n map: Map<string, { top: HTMLDivElement; bottom: HTMLDivElement }>,\n key: string,\n): { top: HTMLDivElement; bottom: HTMLDivElement } {\n let pair = map.get(key);\n if (!pair) {\n const makeLine = () => {\n const line = document.createElement(\"div\");\n line.style.position = \"fixed\";\n line.style.left = \"0\";\n line.style.right = \"0\";\n line.style.height = \"0\";\n line.style.borderTop = \"2px dashed #3b82f6\";\n line.style.zIndex = \"2147483645\";\n line.style.pointerEvents = \"none\";\n document.body.appendChild(line);\n return line;\n };\n pair = { top: makeLine(), bottom: makeLine() };\n map.set(key, pair);\n }\n return pair;\n}\n\nfunction clearDebugOverlay(refs: {\n panelRef: { current: HTMLDivElement | null };\n boxesRef: { current: Map<string, HTMLDivElement> };\n bandLinesRef: { current: Map<string, { top: HTMLDivElement; bottom: HTMLDivElement }> };\n}): void {\n refs.panelRef.current?.remove();\n refs.panelRef.current = null;\n for (const box of refs.boxesRef.current.values()) box.remove();\n refs.boxesRef.current.clear();\n for (const pair of refs.bandLinesRef.current.values()) {\n pair.top.remove();\n pair.bottom.remove();\n }\n refs.bandLinesRef.current.clear();\n}\n\nexport function XbNovaNudgeLoop({ appId }: { appId: string }) {\n const { auth, trackRegistry, factsRegistry, sendEvent } = useXbNovaInternal();\n const { apiKey, apiBaseUrl, screen } = auth;\n const { hint, offerHint, autoHideHint, hintsEnabled } = useNudgeState();\n\n const graphRef = useRef<NudgeGraph | null>(null);\n /** 트리거 id → band 안에 누적으로 머문 시간. 트리거마다 band(%) 설정이 다를 수 있어서\n * region이 아니라 트리거별로 잰다 — 같은 region을 보는 트리거 두 개가 서로 다른 band를\n * 요구할 수 있다. */\n const bandDwellRef = useRef<\n Map<string, { accumulatedMs: number; lastCheckedAt: number | null; firedAndAwaitingExit: boolean }>\n >(new Map());\n /** 마지막 스크롤 이벤트 시각. 폭넓은 band(예: 15~60%)를 쓰면 빠르게 스크롤해서 지나가는\n * 중에도 \"지나가는 순간의 대표\"가 몇 tick 연속으로 겹쳐 dwell 문턱을 넘어버릴 수\n * 있다 — 사용자가 실제로 멈춰서 보는 게 아니라 그냥 스쳐 지나갔을 뿐인데 힌트가 뜨고,\n * 곧바로 다음 region으로 또 넘어가면서 힌트가 계속 바뀌어 \"깜빡거림\"처럼 보인다. 힌트를\n * 새로 띄우는 시점만 \"스크롤이 실제로 멈춘 뒤\"로 미룬다 — 대표 선정·박스 하이라이트(디버그\n * 표시)는 그대로 실시간으로 움직여도 되지만, 메시지 전환만은 사용자가 정말 멈췄을 때\n * 한정한다. */\n const lastScrollAtRef = useRef<number>(0);\n /** TICK 로그 중복 억제용 — 직전에 찍은 요약 문자열. 매초 그대로 찍으면 가만히 있을 때도\n * 로그가 끝없이 쌓여서 복붙으로 원인을 추적하기 힘들다(\"틱 필터가 매초떠서 로그 너무\n * 길어서 복사가힘듦\" 피드백) — 대표/overlap/잠금 상태가 실제로 바뀐 tick만 찍는다. */\n const lastTickLogRef = useRef<string>(\"\");\n const debugPanelRef = useRef<HTMLDivElement | null>(null);\n const debugCollapsedRef = useRef<boolean>(readDebugCollapsed());\n const debugBoxesRef = useRef<Map<string, HTMLDivElement>>(new Map());\n const debugBandLinesRef = useRef<Map<string, { top: HTMLDivElement; bottom: HTMLDivElement }>>(new Map());\n const hintRef = useRef(hint);\n hintRef.current = hint;\n const hintsEnabledRef = useRef(hintsEnabled);\n hintsEnabledRef.current = hintsEnabled;\n\n useEffect(() => {\n let cancelled = false;\n fetch(`${apiBaseUrl}/v1/apps/${appId}/nudge-graph`, {\n headers: { Authorization: `Bearer ${apiKey}` },\n })\n .then((r) => (r.ok ? r.json() : Promise.reject(new Error(`${r.status}`))))\n .then((graph: NudgeGraph) => {\n if (!cancelled) graphRef.current = graph;\n })\n .catch(() => {\n // 넛지 그래프를 못 불러와도 채팅 자체는 동작해야 한다 — 조용히 포기한다.\n });\n return () => {\n cancelled = true;\n };\n }, [apiBaseUrl, apiKey, appId]);\n\n const evaluate = useCallback(\n (firedEventName: string | null) => {\n const graph = graphRef.current;\n const debugEnabled = isDebugEnabled();\n const debugRefs = { panelRef: debugPanelRef, boxesRef: debugBoxesRef, bandLinesRef: debugBandLinesRef };\n if (!debugEnabled && debugPanelRef.current) clearDebugOverlay(debugRefs);\n // 매 tick(0.25초) 반복되는 상태 로그는 디버그 모드에서만 찍는다 — 트리거마다 찍으면\n // 초당 수십 줄이 쌓인다. 발동·설정 오류처럼 상태가 바뀔 때의 로그는 그대로 둔다.\n const tickDebug = debugEnabled ? (msg: string) => console.debug(msg) : () => {};\n if (!graph) {\n tickDebug(\"[XbNova] nudge 평가 건너뜀: 그래프를 아직 못 불러옴\");\n return;\n }\n\n // 규칙 단순화: dwell 누적은 항상 계속된다(다른 힌트가 떠 있어도 멈추지 않는다) —\n // \"이미 힌트가 떠 있으니 이번 트리거는 새로 보여주지 않는다\"만 발동 직전에 따로\n // 걸러낸다(아래 `if (!canOffer) continue;`). 예전엔 힌트가 떠 있으면 평가 전체를\n // return 해버려서, 그동안 다른 곳(예: FAQ)에 머물러도 그 트리거의 누적이 통째로\n // 멈춰버리는 게 \"이상하다\"는 신고로 이어졌다.\n const canOffer = !hintRef.current && hintsEnabledRef.current;\n\n const dwellTriggerCount = graph.nodes.filter((n) => n.kind === \"trigger\" && n.trigger_type === \"dwell\").length;\n const scrollIdleMs = Date.now() - lastScrollAtRef.current;\n const debugLines: string[] = debugEnabled\n ? [\n `[XbNova] dwell 트리거 ${dwellTriggerCount}개 (화면=\"${screen}\")${canOffer ? \"\" : \" — 새 힌트 표시는 잠깐 막힘(누적은 계속됨)\"}`,\n scrollIdleMs < SCROLL_SETTLE_MS\n ? `스크롤 중(유휴 ${scrollIdleMs}ms < ${SCROLL_SETTLE_MS}ms) — 새 힌트 발동 보류`\n : `스크롤 유휴 ${scrollIdleMs >= 10000 ? \"10s+\" : scrollIdleMs + \"ms\"}`,\n ]\n : [];\n const activeDebugTriggerIds = new Set<string>();\n\n const nodesById = new Map(graph.nodes.map((n) => [n.id, n]));\n const edgesFrom = new Map<string, NudgeEdge[]>();\n for (const edge of graph.edges) {\n const list = edgesFrom.get(edge.from_id) ?? [];\n list.push(edge);\n edgesFrom.set(edge.from_id, list);\n }\n\n const walkToMessage = (nodeId: string): NudgeNode | null => {\n const node = nodesById.get(nodeId);\n if (!node) return null;\n if (node.kind === \"message\") return node;\n if (node.kind === \"condition\") {\n const matches = compare(\n resolveConditionValue(node.condition_field ?? \"\", { screen, trackRegistry, factsRegistry }),\n node.operator,\n node.value,\n );\n const branch = matches ? \"true\" : \"false\";\n const nextEdge = (edgesFrom.get(nodeId) ?? []).find((e) => e.branch_label === branch);\n return nextEdge ? walkToMessage(nextEdge.to_id) : null;\n }\n return null;\n };\n\n const fire = (node: NudgeNode, text: string, messageNode: NudgeNode) => {\n console.debug(`[XbNova] 힌트 발동: 트리거 ${node.id} → 메시지 \"${text}\"`);\n const handoffEdge = (edgesFrom.get(messageNode.id) ?? [])[0];\n const handoffNode = handoffEdge ? nodesById.get(handoffEdge.to_id) : undefined;\n const botId = handoffNode?.kind === \"ai_handoff\" ? (handoffNode.bot_id ?? null) : null;\n\n bumpSessionCount(node.id);\n offerHint({\n messageNodeId: messageNode.id,\n triggerNodeId: node.id,\n text,\n botId,\n autoDisableAfterDismissals: node.auto_disable_after_dismissals ?? 3,\n // 여러 트리거가 같은 message 노드를 공유할 수 있어서(DAG), \"몇 ms 후 사라질지\"는\n // 트리거(node)가 아니라 메시지(messageNode) 자체의 속성이다.\n autoHideMs: messageNode.auto_hide_ms ?? 20000,\n quickReplies: messageNode.quick_replies ?? [],\n });\n\n sendEvent(\"nudge_impression\", { nudge_node_id: messageNode.id, bot_id: botId });\n };\n\n const tryFireFrom = (node: NudgeNode): boolean => {\n const nextEdge = (edgesFrom.get(node.id) ?? [])[0];\n if (!nextEdge) {\n console.debug(`[XbNova] 트리거 ${node.id} 발동했지만 나가는 엣지가 없음`);\n return false;\n }\n const messageNode = walkToMessage(nextEdge.to_id);\n if (!messageNode) {\n console.debug(`[XbNova] 트리거 ${node.id} 발동했지만 message 노드에 못 닿음(조건 분기 확인)`);\n return false;\n }\n const text = messageNode.messages?.[0];\n if (!text) {\n console.debug(`[XbNova] message 노드 ${messageNode.id}에 문구가 없음`);\n return false;\n }\n fire(node, text, messageNode);\n return true;\n };\n\n // dwell 트리거는 즉시 발동하지 않는다 — 같은 순간에 여러 region이 동시에 band 안에\n // 걸칠 수 있어서(예: 스크롤 중 보증 게이지·기본정보·차량 상세정보가 한 화면에 다\n // 보임), 일단 후보로만 모아두고 전부 평가한 뒤 \"band와 가장 많이 겹치는(overlapPx\n // 최대) 하나\"만 대표로 뽑아 그 하나만 발동시킨다. 나머지는 계속 누적만 하고\n // 기다린다(다음 tick에 대표가 바뀔 수 있다).\n const dwellRecords: Array<{\n node: NudgeNode;\n state: { accumulatedMs: number; lastCheckedAt: number | null; firedAndAwaitingExit: boolean };\n requiredMs: number;\n region: string;\n found: boolean;\n screenMatches: boolean;\n inBand: boolean;\n overlapPx: number;\n }> = [];\n let firedThisTick = false;\n\n for (const node of graph.nodes) {\n if (node.kind !== \"trigger\") continue;\n\n // 규칙 단순화: 페이지당 상한은 없앴다(요청) — 세션당 상한만 본다. 0은\n // \"무제한\"이다(auto_hide_ms=0이 \"안 사라짐\"인 것과 같은 관례) — 안 채웠으면(널)\n // 서버 기본값 3, 명시적으로 0을 넣으면 상한 없음.\n const maxPerSession = node.max_per_session === 0 ? Infinity : (node.max_per_session ?? 3);\n const sessionCount = peekSessionCount(node.id);\n if (sessionCount >= maxPerSession) {\n tickDebug(`[XbNova] 트리거 ${node.id}(${node.trigger_type}) 건너뜀: 세션당 상한(${maxPerSession}) 도달`);\n if (debugEnabled && node.trigger_type === \"dwell\") {\n debugLines.push(`- [${node.id}] 세션당 상한(${maxPerSession}) 도달 — 더 안 잼`);\n }\n continue;\n }\n\n if (node.trigger_type === \"dwell\") {\n const params = node.trigger_params ?? {};\n const region = String(params.region ?? \"\");\n const triggerScreen = String(params.screen ?? \"\");\n const requiredMs = Number(params.duration_ms ?? 0);\n const fromPercent = Number(params.region_visible_from_percent ?? 35);\n const toPercent = Number(params.region_visible_to_percent ?? 65);\n\n const state = bandDwellRef.current.get(node.id) ?? {\n accumulatedMs: 0,\n lastCheckedAt: null,\n firedAndAwaitingExit: false,\n };\n const trackEntry = trackRegistry.get(region);\n const el = trackEntry?.node ?? null;\n const screenMatches = screen === triggerScreen;\n const found = screenMatches && el !== null;\n\n let inBand = false;\n let overlapPx = 0;\n let statusDetail: string;\n if (!screenMatches) {\n statusDetail = `screen 불일치 (현재=\"${screen}\" 필요=\"${triggerScreen}\")`;\n } else if (!el) {\n statusDetail = `트랙 요소를 못 찾음 — region=\"${region}\"으로 등록된 <XbNovaTrack>이 이 페이지에 없거나 트랙 키가 다름 (등록된 region 목록: ${Array.from(trackRegistry.keys()).join(\", \") || \"(없음)\"})`;\n } else {\n const status = computeBandStatus(el, fromPercent, toPercent);\n inBand = status.inBand;\n overlapPx = status.overlapPx;\n\n statusDetail = `rect=[top=${status.rectTop.toFixed(0)}, bottom=${status.rectBottom.toFixed(0)}] band=[top=${status.bandTop.toFixed(0)}, bottom=${status.bandBottom.toFixed(0)}] overlapPx=${overlapPx.toFixed(0)} inBand=${inBand}`;\n }\n\n // 누적(accumulatedMs 증가)은 여기서 안 한다 — 대표가 누군지 다 모아본 뒤에\n // 결정한다(아래 두 번째 루프). 여러 region이 동시에 band 안에 걸쳐 있을 때\n // 전부 따로 타이머가 도는 게 문제였다 — \"대표 하나만 도는 시계\" 하나로\n // 만들려면 대표가 정해지기 전엔 아무도 시간을 더하면 안 된다.\n tickDebug(\n `[XbNova] band dwell 트리거=${node.id} region=\"${region}\" band%=${fromPercent}~${toPercent} | ${statusDetail}`,\n );\n\n dwellRecords.push({ node, state, requiredMs, region, found, screenMatches, inBand, overlapPx });\n continue;\n }\n\n const fires = triggerFires(node, { screen, firedEventName });\n if (!fires) continue;\n if (!canOffer) {\n tickDebug(`[XbNova] 트리거 ${node.id} 조건은 충족했지만 새 힌트 표시는 잠깐 막힘`);\n continue;\n }\n if (tryFireFrom(node)) {\n firedThisTick = true;\n break;\n }\n }\n\n // band와 가장 많이 겹치는 하나만 대표로 — 매 tick 새로 뽑는다(직전 tick의 기억을\n // 안 들고 간다). \"다른 곳으로 이동하면 바로 그 자리에서 대표가 바뀌어야 한다\"는\n // 요구사항이라 스위칭에 유예(히스테리시스·디바운스)를 걸면 오히려 옛 대표가 이미\n // band를 벗어났는데도 몇 tick 더 \"대표\"로 남아 있다가 뒤늦게 넘어가는 것처럼\n // 보인다 — 그 지연이 \"꺼졌다 켜졌다\" 하는 것처럼 보이는 원인이었다. 나머지는 화면에\n // 같이 있어도(inBand) 대표가 아니면 이번 tick엔 발동 자격이 없다(디버그 박스는\n // 옅은 초록으로만 표시).\n let primaryId: string | null = null;\n let primaryOverlap = -1;\n for (const r of dwellRecords) {\n if (r.inBand && r.overlapPx > primaryOverlap) {\n primaryId = r.node.id;\n primaryOverlap = r.overlapPx;\n }\n }\n\n\n for (const r of dwellRecords) {\n const isPrimary = r.node.id === primaryId;\n\n // 대표일 때만 시계가 돈다 — 대표가 아니면 band 안에 걸쳐 있어도(inBand) 진짜\n // \"보는 중\"으로 안 치고 0으로 계속 리셋한다. 대표가 바뀌면 새 대표는 이 순간부터\n // 다시 처음(0ms)부터 잰다.\n //\n // `firedAndAwaitingExit`이 true인 동안은 대표+inBand여도 시계를 안 돌린다 — 안\n // 그러면: 힌트가 뜨고 requiredMs(예: 0.5초)는 금방 다시 채워지는데, 힌트 자체는\n // autoHideMs(기본 20초) 동안 화면에 떠 있다. 사용자가 그 20초 내내 같은 곳을 보고\n // 있으면 accumulatedMs가 계속 threshold를 넘은 채로 대기하다가, 힌트가 타임아웃으로\n // 사라지는 바로 다음 tick에 \"이미 문턱 넘음\" 상태라 곧장 같은 메시지가 재발동한다\n // (20초마다 똑같은 힌트가 깜빡깜빡 다시 뜨는 버그로 관찰됨). 한 번 발동한 트리거는\n // 실제로 그 자리를 벗어났다가(대표에서 밀려나거나 band를 나가거나) 다시 들어와야만\n // 재발동 자격을 회복한다.\n const now = Date.now();\n if (isPrimary && r.inBand) {\n if (!r.state.firedAndAwaitingExit && r.state.lastCheckedAt !== null) {\n r.state.accumulatedMs += now - r.state.lastCheckedAt;\n }\n r.state.lastCheckedAt = now;\n } else {\n r.state.accumulatedMs = 0;\n r.state.lastCheckedAt = null;\n r.state.firedAndAwaitingExit = false;\n }\n bandDwellRef.current.set(r.node.id, r.state);\n\n if (debugEnabled) {\n activeDebugTriggerIds.add(r.node.id);\n const statusLabel = r.found\n ? r.inBand\n ? isPrimary\n ? \"IN-BAND(대표)\"\n : \"IN-BAND(겹침, 대표 아님)\"\n : \"band 밖(화면엔 있음)\"\n : r.screenMatches\n ? \"요소 못 찾음\"\n : \"다른 화면\";\n const lockLabel = r.state.firedAndAwaitingExit ? \" 🔒재발동잠금\" : \"\";\n debugLines.push(`- ${r.region || \"(region 없음)\"} [${r.node.id}] ${statusLabel} ${r.state.accumulatedMs}/${r.requiredMs}ms${lockLabel}`);\n\n const el = trackRegistry.get(r.region)?.node ?? null;\n if (r.found && el) {\n const rect = el.getBoundingClientRect();\n const box = ensureDebugBox(debugBoxesRef.current, r.node.id);\n box.style.left = `${rect.left}px`;\n box.style.top = `${rect.top}px`;\n box.style.width = `${Math.max(rect.width, 2)}px`;\n box.style.height = `${Math.max(rect.height, 2)}px`;\n if (r.inBand && isPrimary) {\n box.style.border = \"3px solid #22c55e\";\n box.style.backgroundColor = \"rgba(34,197,94,0.28)\";\n } else if (r.inBand) {\n box.style.border = \"2px solid #86efac\";\n box.style.backgroundColor = \"rgba(134,239,172,0.12)\";\n } else {\n box.style.border = \"2px dashed #f97316\";\n box.style.backgroundColor = \"rgba(249,115,22,0.10)\";\n }\n box.textContent = `${r.region}${isPrimary ? \" ★\" : \"\"} ${r.state.accumulatedMs}/${r.requiredMs}ms${lockLabel}`;\n\n const params = r.node.trigger_params ?? {};\n const fromPercent = Number(params.region_visible_from_percent ?? 35);\n const toPercent = Number(params.region_visible_to_percent ?? 65);\n const bandKey = `${fromPercent}_${toPercent}`;\n const bandLines = ensureBandLines(debugBandLinesRef.current, bandKey);\n const vh = window.innerHeight;\n bandLines.top.style.top = `${(vh * fromPercent) / 100}px`;\n bandLines.bottom.style.top = `${(vh * toPercent) / 100}px`;\n } else {\n debugBoxesRef.current.get(r.node.id)?.remove();\n debugBoxesRef.current.delete(r.node.id);\n }\n }\n\n if (firedThisTick || !isPrimary || !hintsEnabledRef.current) continue;\n if (r.state.accumulatedMs < r.requiredMs) continue;\n if (Date.now() - lastScrollAtRef.current < SCROLL_SETTLE_MS) continue; // 아직 스크롤 중 — 멎을 때까지 대기\n\n const shownHint = hintRef.current;\n if (shownHint && shownHint.triggerNodeId === r.node.id) continue; // 이미 이 트리거의 힌트가 떠 있음\n\n // 대표가 바뀌었는데 화면엔 옛 대표의 힌트가 그대로 떠 있으면(스크롤은 이미\n // 다른 곳으로 갔는데 메시지만 안 바뀌는 문제) — 거절로 세지 않고 조용히\n // 갈아탄다. dwell 문턱(requiredMs)을 새로 넘은 시점에만 갈아타므로, 스쳐\n // 지나가는 동안 힌트가 깜빡이진 않는다.\n if (shownHint) autoHideHint();\n\n if (tryFireFrom(r.node)) {\n // 발동한 대표는 리셋하고 \"재발동 대기\" 상태로 잠근다 — 안 그러면 다음 tick에도\n // 여전히 문턱을 넘어 있어서 계속 다시 발동하려 든다(무제한 세션이면 매 tick\n // 재발동, 세션 상한이 있어도 최소 힌트가 사라지는 즉시 재발동). 이 자리를 벗어나야\n // (위 accumulatedMs 리셋 분기에서) 잠금이 풀린다.\n r.state.accumulatedMs = 0;\n r.state.lastCheckedAt = null;\n r.state.firedAndAwaitingExit = true;\n firedThisTick = true;\n }\n }\n\n // 대표/누적/잠금 상태가 실제로 바뀐 tick만 찍는다 — 매초 그대로 찍으면 가만히 있을\n // 때도 로그가 끝없이 쌓여서 복붙으로 원인을 추적하기 힘들다.\n if (debugEnabled) {\n const snapshot = dwellRecords\n .map(\n (r) =>\n `${r.region}:${r.inBand ? \"IN\" : \"OUT\"}(${r.overlapPx.toFixed(0)}px) ${r.state.accumulatedMs}/${r.requiredMs}ms${r.state.firedAndAwaitingExit ? \"🔒\" : \"\"}`,\n )\n .join(\" | \");\n const tickLog = `primary=${primaryId} :: ${snapshot}`;\n if (tickLog !== lastTickLogRef.current) {\n lastTickLogRef.current = tickLog;\n console.debug(`[XbNova TICK ${new Date().toISOString().slice(11, 23)}] ${tickLog}`);\n }\n }\n\n if (debugEnabled) {\n for (const [id, box] of debugBoxesRef.current) {\n if (!activeDebugTriggerIds.has(id)) {\n box.remove();\n debugBoxesRef.current.delete(id);\n }\n }\n ensureDebugPanel(debugPanelRef, debugCollapsedRef).body.textContent = debugLines.join(\"\\n\");\n }\n },\n [apiBaseUrl, appId, autoHideHint, factsRegistry, offerHint, screen, trackRegistry, sendEvent],\n );\n\n useEffect(() => {\n const interval = window.setInterval(() => evaluate(null), EVAL_INTERVAL_MS);\n const onEvent = (e: Event) => evaluate((e as CustomEvent<{ eventName: string }>).detail.eventName);\n window.addEventListener(XBNOVA_EVENT_NAME, onEvent);\n // capture:true — 페이지 안의 중첩 스크롤 컨테이너(예: 가로 캐러셀)에서 나는 scroll도\n // window까지 안 버블링되므로 캡처 단계에서 잡는다.\n const onScroll = () => {\n lastScrollAtRef.current = Date.now();\n };\n window.addEventListener(\"scroll\", onScroll, { passive: true, capture: true });\n return () => {\n window.clearInterval(interval);\n window.removeEventListener(XBNOVA_EVENT_NAME, onEvent);\n window.removeEventListener(\"scroll\", onScroll, { capture: true });\n clearDebugOverlay({ panelRef: debugPanelRef, boxesRef: debugBoxesRef, bandLinesRef: debugBandLinesRef });\n };\n }, [evaluate]);\n\n return null;\n}\n","\"use client\";\n\n/**\n * 화면 요소 추적 — `context` 자동 채우기와 넛지 dwell 트리거가 공유하는 원시 기능.\n *\n * `IntersectionObserver`(조금이라도 보이는 동안 \"보는 중\")로 영역별 진입 시각·누적 체류\n * 시간을 잰다. `data`는 값이어도 함수여도 된다 — 함수면 훅 호출 시점이 아니라 \"읽히는\n * 순간\"(넛지 평가, context 조립)에 호출한다(push가 아니라 pull, SDK.md 참고).\n *\n * threshold는 0이다 — 예전엔 0.5(면적 절반 이상)였는데, 뷰포트보다 훨씬 긴 섹션(매물\n * 상세의 \"차량 정보\" 등)은 스크롤해도 전체 면적의 50%가 동시에 보이는 순간이 아예 없어서\n * dwell이 영영 안 쌓이는 버그가 있었다 — \"한 픽셀이라도 보이면 보는 중\"으로 바꿨다.\n */\n\nimport { useCallback, useEffect, useRef } from \"react\";\n\nimport { type FactValue, type TrackEntry, useXbNovaInternal } from \"./internal-context\";\n\nconst VISIBILITY_THRESHOLD = 0;\n\nfunction finalizeIfVisible(registry: Map<string, TrackEntry>, region: string): void {\n const entry = registry.get(region);\n if (entry && entry.enteredAt !== null) {\n entry.totalMs += Date.now() - entry.enteredAt;\n entry.enteredAt = null;\n }\n}\n\n/** ref형 — 감싸는 엘리먼트를 늘리기 싫을 때. `<div ref={useXbNovaTrack(\"price\", data)}>`. */\nexport function useXbNovaTrack<T extends HTMLElement = HTMLElement>(\n region: string,\n data?: FactValue,\n label?: string,\n): (node: T | null) => void {\n const { trackRegistry } = useXbNovaInternal();\n const observerRef = useRef<IntersectionObserver | null>(null);\n // ref 콜백은 마운트 커밋 단계에서 아래 useEffect보다 먼저 실행된다 — 그때 registry에\n // entry가 아직 없을 수 있으니, data/label의 최신값을 ref에 담아뒀다가 ref 콜백이 직접\n // entry를 만들 때 쓴다(최신 렌더의 data/label을 반영하기 위함).\n const dataRef = useRef(data);\n dataRef.current = data;\n const labelRef = useRef(label);\n labelRef.current = label;\n\n useEffect(() => {\n const entry = trackRegistry.get(region);\n if (entry) {\n entry.data = data;\n entry.label = label;\n } else {\n trackRegistry.set(region, { region, data, label, enteredAt: null, totalMs: 0, node: null });\n }\n }, [region, data, label, trackRegistry]);\n\n useEffect(() => {\n return () => {\n observerRef.current?.disconnect();\n finalizeIfVisible(trackRegistry, region);\n const entry = trackRegistry.get(region);\n if (entry) entry.node = null;\n };\n }, [region, trackRegistry]);\n\n // useCallback으로 identity를 [region, trackRegistry]에만 묶어 안정화한다 — 매 렌더마다\n // 새 함수를 반환하면 React가 매번 ref를 떼었다 다시 붙이면서(IntersectionObserver\n // disconnect/재생성 반복) 불필요하게 흔들린다. 안정화 자체가 \"entry가 없으면 만든다\"\n // 버그를 완전히 없애주진 않으므로(아래 참고), 핵심 수정은 그 아래 로직이다.\n return useCallback(\n (node: T | null) => {\n observerRef.current?.disconnect();\n observerRef.current = null;\n finalizeIfVisible(trackRegistry, region);\n\n // 마운트 시 ref 콜백이 등록 useEffect보다 먼저 실행돼서 entry가 아직 없을 수 있다 —\n // 그때 `if (entry) entry.node = node`처럼 조용히 넘어가면, 뒤이어 실행되는 등록\n // useEffect가 `node: null`로 entry를 새로 만들어버려서 이 섹션이 다시 리렌더되기\n // 전까지 node가 영원히 null로 박제되는 버그가 있었다(이후 리렌더가 없는 정적\n // 섹션에서 실제로 발생 — dwell 디버그 오버레이가 \"요소 못 찾음\"으로 드러냄).\n // entry가 없으면 여기서 직접 만들어 이 레이스를 원천적으로 없앤다.\n let entry = trackRegistry.get(region);\n if (!entry) {\n entry = { region, data: dataRef.current, label: labelRef.current, enteredAt: null, totalMs: 0, node: null };\n trackRegistry.set(region, entry);\n }\n entry.node = node;\n\n if (!node || typeof IntersectionObserver === \"undefined\") return;\n\n const observer = new IntersectionObserver(\n ([intersection]) => {\n const entry = trackRegistry.get(region);\n if (!entry) return;\n const now = Date.now();\n if (intersection.isIntersecting) {\n if (entry.enteredAt === null) entry.enteredAt = now;\n } else if (entry.enteredAt !== null) {\n entry.totalMs += now - entry.enteredAt;\n entry.enteredAt = null;\n }\n console.debug(\n `[XbNova] track \"${region}\" isIntersecting=${intersection.isIntersecting} ratio=${intersection.intersectionRatio.toFixed(2)} totalMs=${entry.totalMs}${entry.enteredAt !== null ? \" (진입 중)\" : \"\"}`,\n );\n },\n { threshold: VISIBILITY_THRESHOLD },\n );\n observer.observe(node);\n observerRef.current = observer;\n },\n [region, trackRegistry],\n );\n}\n\n/** 선언형 — 감싸기만 하면 됨. `<XbNovaTrack region=\"price\" data={{...}}>...</XbNovaTrack>`. */\nexport function XbNovaTrack({\n region,\n data,\n label,\n className,\n children,\n}: {\n region: string;\n data?: FactValue;\n /** 사람이 읽는 이름 — 넛지 힌트·대화 오프닝 문구에 쓴다(예: \"보증 정보\"). */\n label?: string;\n className?: string;\n children: React.ReactNode;\n}) {\n const ref = useXbNovaTrack(region, data, label);\n return (\n // `data-xbnova-region`은 호스트 앱이 React context 없이도 이 요소를 찾을 수 있게\n // 한다 — client action 핸들러(`<XbNovaProvider clientActions={{...}}>`)는\n // `<XbNovaProvider>`에 넘기는 prop이라 그 안의 훅(`useXbNovaInternal`)을 못 쓴다.\n // `document.querySelector('[data-xbnova-region=\"...\"]')`로 스크롤·하이라이트하면 된다.\n <div ref={ref} className={className} data-xbnova-region={region}>\n {children}\n </div>\n );\n}\n\nfunction dwellMsOf(entry: TrackEntry): number {\n const live = entry.enteredAt !== null ? Date.now() - entry.enteredAt : 0;\n return entry.totalMs + live;\n}\n\n/** 넛지 dwell 트리거 평가가 쓴다 — `region_dwell_ms.<region>` 값. */\nexport function getRegionDwellMs(trackRegistry: Map<string, TrackEntry>, region: string): number {\n const entry = trackRegistry.get(region);\n return entry ? dwellMsOf(entry) : 0;\n}\n\n/** `context` 자동 채우기가 쓴다 — 지금 보고 있는 영역(여러 개면 먼저 들어온 것), 없으면\n * 가장 오래 본 영역 하나를 고른다. */\nexport function getFocusedTrackEntry(trackRegistry: Map<string, TrackEntry>): TrackEntry | null {\n let visible: TrackEntry | null = null;\n let longest: TrackEntry | null = null;\n for (const entry of trackRegistry.values()) {\n if (entry.enteredAt !== null && (visible === null || entry.enteredAt < (visible.enteredAt ?? Infinity))) {\n visible = entry;\n }\n if (longest === null || dwellMsOf(entry) > dwellMsOf(longest)) {\n longest = entry;\n }\n }\n return visible ?? longest;\n}\n","\"use client\";\n\n/**\n * 완제품 채팅 위젯 — \"AI 동행 바\" 하나가 기본(idle)/힌트(hint)/대화 시트(sheet) 세 상태를\n * 오간다(`nova/docs/기획/AI_중고차_동행_서비스_기획서_v0.3.html` 5장 목업 기준). 힌트는\n * `nudge-loop.tsx`가 `nudge-context`에 밀어넣은 걸 그대로 반영한다 — 둘은 형제라 여기서\n * import하지 않는다.\n *\n * 배치(위치·z-index)는 SDK가 정하지 않는다 — `XbNovaShadowRoot`가 `document.body`로\n * 포탈하므로 실제 DOM 위치는 항상 body 바로 아래이고, 사이트마다 z-index 스케일과 하단\n * CTA 높이가 달라 SDK가 하드코딩하면 반드시 어딘가와 충돌한다. `barStyle`(닫힌 바)·\n * `panelStyle`(열린 시트)·`backdropStyle`(모바일 딤 배경)로 소비자가 직접 위치를 정한다.\n *\n * 반드시 인라인 style(React.CSSProperties)이다 — Tailwind 클래스 문자열이 아니라. 호스트\n * 요소는 Shadow DOM *밖*(라이트 DOM, `document.body` 바로 아래)이라 SDK가 빌드 시점에\n * 미리 컴파일해 shadow root 안에 주입하는 CSS가 거기까지 안 닿는다 — 소비자가 런타임에\n * 넘긴 클래스 문자열(`z-[900]` 같은)은 그 컴파일 대상에 없어서 조용히 무시된다(우연히\n * 소비자 자신의 페이지 스타일시트에 같은 클래스가 이미 있지 않은 한). 실제로 이 문제로\n * ctr-client에서 위젯 패널이 사이트 CTA 뒤에 깔려 입력창이 안 보이는 버그가 났었다 —\n * 인라인 style은 클래스 조회 자체가 없어서 이 문제를 원천적으로 피한다. 반응형 전환\n * (모바일 바텀시트 ↔ 데스크탑 플로팅 패널)도 같은 이유로 CSS 미디어쿼리 대신\n * `useIsDesktopViewport`(matchMedia)로 JS에서 직접 판단한다.\n */\n\nimport { useEffect, useRef, useState } from \"react\";\nimport ReactMarkdown from \"react-markdown\";\nimport remarkBreaks from \"remark-breaks\";\nimport remarkGfm from \"remark-gfm\";\n\nimport { type ClientActionHandler, resolveFactValue, useXbNovaInternal } from \"./internal-context\";\nimport { useNudgeState } from \"./nudge-context\";\nimport { XbNovaShadowRoot } from \"./shadow-root\";\nimport type { MessagePart } from \"./stream\";\nimport { getFocusedTrackEntry } from \"./track\";\nimport { type ClientResults, type XbNovaFeedbackEvent, type XbNovaFeedbackRating, type XbNovaFeedbackReason, useXbNovaChat } from \"./use-chat\";\nimport { ToolView, type ViewRendererMap } from \"./views\";\n\n// client action tool 호출(예: get_vehicle_detail)은 화면에 아무것도 안 그린다(위\n// clientActionHandlers.has 체크가 null을 반환). \"생각하는 중…\" 표시를\n// `parts.length === 0`으로만 판단하면, tool 호출 part가 스트리밍으로 먼저 들어오는 순간\n// (아직 안 보이는데) 그 조건이 꺼져버려 답이 나올 때까지 화면이 텅 빈 채로 몇 초 멎어\n// 있는 것처럼 보인다 — \"생각중이라고 뜨다가 사라지고 답이 늦게 뜬다\"는 신고가 이 때문이었다.\n// \"화면에 실제로 보이는 파트가 하나라도 있는가\"로 판단해야 tool 호출 중에도 로딩 표시가\n// 안 끊긴다.\nfunction hasVisiblePart(\n parts: MessagePart[],\n clientActionHandlers: Map<string, ClientActionHandler>,\n clientResults: ClientResults,\n): boolean {\n return parts.some((part) => {\n if (part.kind === \"text\") return true;\n if (clientResults[part.toolCallId]) return true;\n const spec = clientActionHandlers.get(part.toolName);\n return spec ? Boolean(spec.render) : true; // client action인데 render가 없으면 계속 안 보임\n });\n}\n\nconst DISCLOSURE_SEEN_KEY = \"xbnova_disclosure_seen\";\n/** 소비자가 `useXbNovaFact(\"xbnova_summary\", \"트레일블레이저 RS · 22년형 · 2,190만원\")`처럼\n * 등록해두면 시트 헤더 부제로 쓴다 — 안 등록했으면 지금 보고 있는 영역 이름으로 대신한다. */\nconst SUMMARY_FACT_KEY = \"xbnova_summary\";\n\n/** client action 영역 안에서 눌린 버튼·링크와 그 문구. 버튼이 아닌 여백을 누른 거면 null — 클릭으로 안 센다. */\nfunction clickedControl(e: React.MouseEvent<HTMLElement>): { label: string | null } | null {\n const target = e.target instanceof Element ? e.target : null;\n const control = target?.closest(\"button, a, [role='button']\");\n if (!control || !e.currentTarget.contains(control)) return null;\n const text = (control.textContent ?? \"\").replace(/\\s+/g, \" \").trim();\n return { label: text || control.getAttribute(\"aria-label\") || null };\n}\n\nfunction readDisclosureSeen(): boolean {\n if (typeof window === \"undefined\") return true;\n try {\n return window.localStorage.getItem(DISCLOSURE_SEEN_KEY) === \"1\";\n } catch {\n return true;\n }\n}\n\nfunction markDisclosureSeen(): void {\n if (typeof window === \"undefined\") return;\n try {\n window.localStorage.setItem(DISCLOSURE_SEEN_KEY, \"1\");\n } catch {\n // no-op\n }\n}\n\nconst DESKTOP_MEDIA_QUERY = \"(min-width: 640px)\"; // Tailwind sm: 과 같은 값\n\n/** 반응형 전환을 CSS 미디어쿼리가 아니라 JS에서 직접 판단한다 — 호스트(라이트 DOM) 요소는\n * SDK가 컴파일해 shadow root에 넣어둔 CSS의 사정권 밖이라 `sm:` 클래스를 거기 써봐야\n * 안 먹는다(파일 상단 설명 참고). */\nfunction useIsDesktopViewport(): boolean {\n const [isDesktop, setIsDesktop] = useState(false);\n useEffect(() => {\n if (typeof window === \"undefined\") return;\n const mql = window.matchMedia(DESKTOP_MEDIA_QUERY);\n setIsDesktop(mql.matches);\n const onChange = () => setIsDesktop(mql.matches);\n mql.addEventListener(\"change\", onChange);\n return () => mql.removeEventListener(\"change\", onChange);\n }, []);\n return isDesktop;\n}\n\nconst Z_BAR = 2147483000;\nconst Z_PANEL = 2147483000;\nconst Z_BACKDROP = 2147482999;\n\n/** 호스트(라이트 DOM)에 적용할 기본 위치 — 소비자의 `barStyle` 등이 이 위에 개별 속성만\n * 덮어쓴다(전체 교체가 아니라 병합). 실사용은 사이트별 CTA·z-index에 맞춰 `zIndex`·\n * `bottom` 정도는 거의 항상 덮어써야 한다. */\nfunction barBaseStyle(isDesktop: boolean): React.CSSProperties {\n // 데스크탑은 폭을 여기서 명시적으로 고정해야 한다 — `right`만 주고 `left`/`width`를\n // 안 주면 position:fixed가 \"내용에 맞춰 줄어드는\" shrink-to-fit 폭이 되는데, 그 안의\n // pill(children)이 width:100%로 펼쳐지려 하면 \"auto인 부모의 100%\"라는 순환이 생겨\n // 브라우저마다 다르게 계산된다. 폭을 여기서 못박아두고, 원형↔pill 전환은 안쪽\n // pill(div)의 width만 그 안에서 움직인다(아래 XbNovaChat 렌더 부분 참고) — 접혀있을\n // 땐 `margin-left: auto`로 이 고정폭 컨테이너의 오른쪽 끝에 붙인다.\n return isDesktop\n ? { position: \"fixed\", right: \"1rem\", bottom: \"1rem\", width: \"20rem\", zIndex: Z_BAR }\n : { position: \"fixed\", left: \"0.625rem\", right: \"0.625rem\", bottom: \"1rem\", zIndex: Z_BAR };\n}\n\nfunction panelBaseStyle(isDesktop: boolean): React.CSSProperties {\n return isDesktop\n ? { position: \"fixed\", right: \"1rem\", bottom: \"1rem\", width: \"20rem\", height: \"32rem\", zIndex: Z_PANEL }\n : { position: \"fixed\", left: 0, right: 0, top: \"7rem\", bottom: 0, zIndex: Z_PANEL };\n}\n\nfunction backdropBaseStyle(isDesktop: boolean): React.CSSProperties {\n return isDesktop ? { display: \"none\" } : { position: \"fixed\", inset: 0, zIndex: Z_BACKDROP };\n}\n\n/** 힌트 바 문구를 한 글자씩 타이핑되듯 보여준다. `key`(보통 트리거 id)가 바뀌면 진행 중이던\n * 타이핑을 이어붙이지 않고 처음부터 다시 시작한다 — 대표 트리거가 바뀌어 문구가 갈아탈\n * 때마다 \"다다닥\" 새로 찍히는 느낌을 주기 위해서다. */\nfunction useTypewriter(text: string, key: string, speedMs = 28): string {\n const [shown, setShown] = useState(\"\");\n useEffect(() => {\n setShown(\"\");\n if (!text) return;\n let i = 0;\n const id = window.setInterval(() => {\n i += 1;\n setShown(text.slice(0, i));\n if (i >= text.length) window.clearInterval(id);\n }, speedMs);\n return () => window.clearInterval(id);\n }, [key, text, speedMs]);\n return shown;\n}\n\n/** 힌트 pill 배경의 \"액체처럼 흐르는\" 색 덩어리 — 블러 먹인 원(div) 4개를 겹쳐서\n * 부드러운 색 구름으로 흐르게 한다(\"aurora/liquid UI\" 레퍼런스처럼). 색은\n * `--xbnova-accent-1/2`(테마, chat.tsx 상단 themeVars 참고) 두 포인트만 쓴다 —\n * accent-1을 두 개(1번·4번, 더 크고 진하게)로 늘려서 accent-1(브랜드 메인 컬러, 예:\n * 카통령 레드)이 더 강조되고 더 넓게 퍼지도록 했다(\"빨간색이 더 강조돼도, 더 넓게\n * 분포해도 좋겠다\"). 3번째 원은 accent-1 쪽으로 치우친 혼합 톤이라 부드럽게 이어\n * 보인다.\n *\n * SVG(viewBox 스케일링)로 처음 만들었다가 두 문제로 갈아엎었다:\n * - 정사각형 viewBox를 가로로 긴 pill에 비균등 스케일(`preserveAspectRatio=\"none\"`)로\n * 늘렸더니 원이 타원으로 찌그러져 \"아메바\" 같다는 피드백을 받았다. 비율을 지키는\n * `slice`로 바꿔도, pill이 정사각형 viewBox보다 훨씬 넓고 낮아서 세로 대부분이\n * 화면 밖으로 잘려나가 오히려 안 보이는 새 문제가 생겼을 것이다.\n * - gooey 필터(블러 뒤 알파 채널을 급격히 증폭·클리핑하는 feColorMatrix)는 원래\n * \"부드러운 그라디언트\"가 아니라 서로 뭉쳐 붙는 딱딱한 실루엣을 만드는 기법이라\n * (\"이게 그라데이션이 아니잖아\") 부드러운 느낌과 안 맞았다.\n *\n * 그래서 SVG 좌표계 자체를 버리고 평범한 `<span>` + `border-radius:9999px` +\n * `filter: blur()`로 바꿨다 — 크기(px)가 컨테이너 비율과 무관하게 고정이라 어떤\n * 가로세로 비율의 pill에서도 항상 진짜 원으로 남는다(찌그러질 수가 없다). 위치만\n * `left`/`top`(%) 애니메이션으로 움직인다.\n *\n * `position: absolute`/크기/모양(둥근 정도)은 전부 인라인 style이다(파일 상단 설명의\n * \"구조에 영향 주는 속성은 인라인으로\" 규칙 — shadow DOM 비동기 CSS 로딩과 경합하면\n * 안 된다). 블러·움직임(`left`/`top` 애니메이션)만 styles.css의 클래스에 맡긴다(순수\n * 장식이라 최악의 경우 안 흐려지거나 안 움직여도 새는 게 아니라 그냥 또렷한 원이\n * 정지해 보일 뿐이다). */\nfunction GlowBlobs() {\n const blobBase: React.CSSProperties = {\n position: \"absolute\",\n width: \"6.5rem\",\n height: \"6.5rem\",\n borderRadius: \"9999px\",\n pointerEvents: \"none\",\n };\n const accent1 = \"var(--xbnova-accent-1, #4f46e5)\";\n const accent2 = \"var(--xbnova-accent-2, #7c3aed)\";\n return (\n <>\n {/* accent-1(메인 브랜드 컬러) 원 두 개(1·4번)를 다른 것보다 크게(7.5rem) + 진하게\n 둬서 더 강조되고 더 넓게 퍼지게 한다. */}\n <span\n className=\"xbnova-blob-1\"\n style={{\n ...blobBase,\n width: \"7.5rem\",\n height: \"7.5rem\",\n left: \"0%\",\n top: \"5%\",\n background: accent1,\n opacity: 0.62,\n }}\n />\n <span\n className=\"xbnova-blob-2\"\n style={{ ...blobBase, left: \"70%\", top: \"45%\", background: accent2, opacity: 0.42 }}\n />\n <span\n className=\"xbnova-blob-3\"\n style={{\n ...blobBase,\n left: \"35%\",\n top: \"70%\",\n background: `color-mix(in srgb, ${accent1} 65%, ${accent2} 35%)`,\n opacity: 0.48,\n }}\n />\n <span\n className=\"xbnova-blob-4\"\n style={{\n ...blobBase,\n width: \"7.5rem\",\n height: \"7.5rem\",\n left: \"50%\",\n top: \"10%\",\n background: accent1,\n opacity: 0.5,\n }}\n />\n </>\n );\n}\n\n/** 바(AI 버튼)를 눌러 대화 시트를 열 때 `onOpen`으로 넘어오는 값. */\nexport type XbNovaOpenEvent = {\n /** 넛지 힌트 맥락으로 열었는지 — 힌트가 떠 있었거나, 방금 조용히 소멸한 힌트를 이어 열었으면 true. */\n fromNudge: boolean;\n /** `fromNudge`일 때 그 힌트 문구. */\n hintText?: string;\n};\n\nexport function XbNovaChat({\n viewRenderers,\n botId,\n barStyle,\n panelStyle,\n backdropStyle,\n defaultOpen = false,\n idleText = \"AI에게 이 차 물어보기\",\n avatarSize = 28,\n onVerdictAction,\n feedback: feedbackEnabled = true,\n onFeedback,\n onOpen,\n}: {\n /** 특정 view만 자체 컴포넌트로 덮어쓰고 싶을 때 — 지정 안 한 view는 기본 렌더러를 쓴다. */\n viewRenderers?: ViewRendererMap;\n /** Provider의 기본 봇 대신 다른 봇과 고정적으로 대화하고 싶을 때. 힌트를 눌러 열면\n * 그 힌트의 handoff 봇이 이 값보다 우선한다. */\n botId?: string;\n /** 닫힌 상태(기본/힌트 바)의 z-index·하단 offset — 이 사이트의 CTA 위에 오도록. SDK\n * 기본값 위에 준 속성만 덮어쓴다(전체 교체 아님) — 보통 `zIndex`·`bottom` 정도만 주면\n * 된다. 반드시 인라인 style이다(Tailwind 클래스 문자열은 안 먹는다, 위 파일 설명 참고).\n * 예: `{ bottom: \"76px\", zIndex: 45 }`. */\n barStyle?: React.CSSProperties;\n /** 열린 상태(대화 시트/패널)의 z-index·offset. 마찬가지로 기본값 위에 병합된다. */\n panelStyle?: React.CSSProperties;\n /** 모바일 바텀시트 뒤 딤 배경의 z-index. `panelStyle`보다 살짝 낮게 둬야 한다. */\n backdropStyle?: React.CSSProperties;\n /** true면 처음부터 시트가 열려 있다. */\n defaultOpen?: boolean;\n /** 기본(idle) 상태 바 문구. */\n idleText?: string;\n /** 바(AI 버튼)의 동그란 아바타 지름(px), 기본 28. 바의 접힌 원은 52px이라 44~48이면 원을 거의 채운다.\n * 채팅 헤더의 아바타는 이 값에 맞춰 24~36px로 따라 커진다. */\n avatarSize?: number;\n /** 판단 카드의 행동 버튼을 눌렀을 때 — 실제 동작(상담신청 폼 열기, 섹션 스크롤 등)은\n * 소비자가 정한다. SDK는 버튼에 적힌 `action` 문자열을 그대로 넘겨줄 뿐 뜻을 모른다. */\n onVerdictAction?: (action: string) => void;\n /** 완료된 답변 아래 👍/👎 \"상담 만족도\" 버튼을 보일지. 기본 true. */\n feedback?: boolean;\n /** 만족도가 서버에 저장된 뒤 호출된다(분석 이벤트 전송용). `rating: null`은 평가 취소. */\n onFeedback?: (e: XbNovaFeedbackEvent) => void;\n /** 바를 눌러 대화 시트를 열 때 호출된다(분석 이벤트 전송용). 클릭이 Shadow DOM 안에서\n * 일어나 호스트가 직접 잡을 수 없어서 여는 콜백이다. `defaultOpen`으로 처음부터 열린 경우엔\n * 호출되지 않는다. */\n onOpen?: (e: XbNovaOpenEvent) => void;\n}) {\n // client action 핸들러는 `<XbNovaChat>` prop이 아니라 `<XbNovaProvider clientActions={{...}}>`에\n // 등록한다 — 이 화면에서 여러 <XbNovaChat> 인스턴스가 떠도 같은 페이지 능력을 공유해야\n // 해서(넛지 handoff로 다른 봇과 열릴 때도 마찬가지) Provider 레벨 개념이다.\n const { auth, trackRegistry, factsRegistry, clientActionHandlers, theme, sendEvent } = useXbNovaInternal();\n // CSS 커스텀 프로퍼티는 상속되는 값이라, shadow host(라이트 DOM)에 인라인으로 주면\n // shadow DOM 안쪽까지 그대로 상속된다 — `.xbnova-glow-bg`(styles.css)와 아래 아바타\n // 배지 그라디언트가 이 값을 읽는다. `<XbNovaProvider theme={...}>`로 소비자가 브랜드\n // 컬러를 넘기면(예: ctr-client는 자기 브랜드 레드) 여기까지 흘러온다.\n const themeVars = {\n \"--xbnova-accent-1\": theme.accentFrom,\n \"--xbnova-accent-2\": theme.accentTo,\n } as React.CSSProperties;\n const headerAvatarSize = Math.min(36, Math.max(24, Math.round(avatarSize * 0.8)));\n const avatarGradientStyle: React.CSSProperties = {\n background: \"linear-gradient(135deg, var(--xbnova-accent-1), var(--xbnova-accent-2))\",\n };\n const { hint, lastHint, dismissHint, openHint, autoHideHint, hintsEnabled, barHidden } = useNudgeState();\n const isDesktop = useIsDesktopViewport();\n\n const [open, setOpen] = useState(defaultOpen);\n // 시트가 열리는 순간 밑에서 슬라이드업하는 애니메이션용 — 처음엔 화면 밖(entered=false)에\n // 두고, 마운트 다음 프레임에 entered=true로 바꿔 transition이 실제로 재생되게 한다(같은\n // 프레임에 최종 상태로 바로 그리면 브라우저가 전환을 건너뛴다).\n const [entered, setEntered] = useState(false);\n const [activeBotId, setActiveBotId] = useState<string | undefined>(undefined);\n const [activeNudgeNodeId, setActiveNudgeNodeId] = useState<string | undefined>(undefined);\n const [nudgeQuickReplies, setNudgeQuickReplies] = useState<string[]>([]);\n // 눌러서 연 넛지의 문구. 대화가 있으면 이력을 건드리지 않고 칩 위에 띄웠다가, 답할 때 이력에 넣는다.\n const [nudgePrompt, setNudgePrompt] = useState<string | null>(null);\n const [openingLine, setOpeningLine] = useState<string | null>(null);\n const [showDisclosure, setShowDisclosure] = useState(false);\n const [input, setInput] = useState(\"\");\n const bottomRef = useRef<HTMLDivElement>(null);\n // 힌트가 바뀌어도 이 컴포넌트 자체는 계속 렌더되므로(원형↔pill을 같은 노드로 전환하려고\n // 한 return만 쓴다), hook은 조건 없이 최상단에서 불러야 한다 — `triggerNodeId`를 key로\n // 줘서 같은 문구를 다시 받아도(이론상) 아니라 트리거가 실제로 바뀔 때만 재시작한다.\n const typedHintText = useTypewriter(hint?.text ?? \"\", hint?.triggerNodeId ?? \"\");\n\n const { onboarding, opening, messages, busy, error, send, clientResults, feedback, setFeedback, trackClientActionClick } = useXbNovaChat({\n onFeedback,\n botId: activeBotId ?? botId,\n nudgeNodeId: activeNudgeNodeId,\n openingMessage: openingLine ?? undefined,\n onDismissSheet: () => setOpen(false),\n });\n\n // 서버 응답 전에는 직전에 받아 둔 branding(localStorage)을 쓴다 — 첫 화면부터 이미지가 보인다.\n const effectiveBotId = activeBotId ?? botId;\n const [cachedBranding, setCachedBranding] = useState<{\n logo_url?: string;\n avatar_text?: string;\n badge_text?: string;\n disclaimer?: string;\n } | null>(null);\n useEffect(() => {\n try {\n const raw = window.localStorage.getItem(`xbnova_branding:${effectiveBotId}`);\n setCachedBranding(raw ? JSON.parse(raw) : null);\n } catch {\n setCachedBranding(null);\n }\n }, [effectiveBotId]);\n useEffect(() => {\n if (!onboarding?.branding) return;\n try {\n window.localStorage.setItem(\n `xbnova_branding:${effectiveBotId}`,\n JSON.stringify({\n logo_url: onboarding.branding.logo_url,\n avatar_text: onboarding.branding.avatar_text,\n badge_text: onboarding.branding.badge_text,\n disclaimer: onboarding.branding.disclaimer,\n }),\n );\n } catch {\n // 저장소를 못 쓰는 환경(사생활 보호 모드 등)이면 캐시 없이 동작한다.\n }\n }, [onboarding?.branding, effectiveBotId]);\n const branding = onboarding?.branding ?? cachedBranding ?? undefined;\n const badgeText = branding?.badge_text?.trim() ?? \"\";\n const disclaimer = branding?.disclaimer?.trim() ?? \"\";\n // 서버 응답이 오거나, 캐시가 있거나, 이미 오류가 났으면(서버를 못 불러도 \"AI\" 배지는 보여준다) 확정으로 본다.\n const brandingReady = Boolean(onboarding) || cachedBranding !== null || Boolean(error);\n\n useEffect(() => {\n bottomRef.current?.scrollIntoView({ behavior: \"smooth\" });\n }, [messages]);\n\n useEffect(() => {\n if (!open) {\n setEntered(false);\n return;\n }\n const raf = requestAnimationFrame(() => setEntered(true));\n return () => cancelAnimationFrame(raf);\n }, [open]);\n\n // 힌트는 잠시 후 스스로 사라진다(\"조용한 소멸\") — 사용자가 직접 닫은 게 아니라서 거절로 안 친다.\n // 몇 ms 후 사라질지는 이 힌트를 낸 트리거의 넛지룰(auto_hide_ms) 값이다 — 0이면 안 사라진다.\n useEffect(() => {\n if (!hint || hint.autoHideMs <= 0) return;\n console.debug(`[XbNova] 힌트 \"${hint.text}\" 표시 — ${hint.autoHideMs}ms 후 자동으로 사라짐`);\n const timer = window.setTimeout(() => {\n console.debug(`[XbNova] 힌트 자동 소멸(타임아웃) — \"${hint.text}\"`);\n autoHideHint();\n }, hint.autoHideMs);\n return () => window.clearTimeout(timer);\n }, [hint, autoHideHint]);\n\n function openSheet() {\n // `hint`는 이미 이 컴포넌트가 들고 있는 reactive state다 — 클릭 핸들러 안에서 읽으면\n // 항상 그 순간의 실제 값이다(React state 읽기의 기본 보장). `openHint()`는 이제\n // 부수효과(지우기)만 한다 — 예전엔 `openHint()`의 반환값으로 읽으려 했는데, 그건\n // React의 \"즉시 계산\" 내부 최적화가 스킵될 때마다(이 컴포넌트가 자주 리렌더돼서 꽤\n // 흔했다) 항상 null을 돌려줘서 힌트를 눌러도 엉뚱한 폴백 인사말이 뜨는 버그였다.\n // `hint`가 이미 조용히 소멸(autoHideMs 경과)해 원형으로 접힌 뒤에도, 그 상태에서\n // 바를 누르면 방금 사라진 힌트와 똑같이 이어가야 한다 — `lastHint`로 폴백한다.\n const consumed = hint ?? lastHint;\n openHint();\n if (consumed?.botId) setActiveBotId(consumed.botId);\n\n if (consumed) {\n // 힌트를 눌러 열었으면 그 힌트가 보여준 문구를 그대로 대화 시작 메시지로 이어간다 —\n // 다른 문구로 바뀌면 방금 읽고 클릭한 이유가 사라져 버린다.\n setOpeningLine(consumed.text);\n setActiveNudgeNodeId(consumed.messageNodeId);\n setNudgeQuickReplies(consumed.quickReplies);\n setNudgePrompt(consumed.text);\n\n // 넛지 전환 퍼널의 \"AI 클릭\" 단계 — 이 힌트를 눌러서 열었을 때만 기록한다(기본\n // 문구를 눌러 연 건 넛지 전환이 아니다).\n sendEvent(\"nudge_open\", { nudge_node_id: consumed.messageNodeId, bot_id: consumed.botId });\n } else {\n // 기본 바를 눌러 열었으면 넛지 맥락이 없다 — 이전 힌트의 버튼이 남아있으면 안 된다.\n setNudgeQuickReplies([]);\n setNudgePrompt(null);\n setActiveNudgeNodeId(undefined);\n const focused = getFocusedTrackEntry(trackRegistry);\n setOpeningLine(\n focused ? `${focused.label ?? focused.region}을(를) 보고 계시네요. 궁금하신 점을 편하게 물어보세요.` : null,\n );\n }\n\n if (!readDisclosureSeen()) {\n setShowDisclosure(true);\n markDisclosureSeen();\n }\n setOpen(true);\n sendEvent(\"chat_open\", { from_nudge: Boolean(consumed) });\n // 호스트 콜백이 던져도 시트 열기는 이미 끝났다 — 분석 코드 오류가 위젯 동작을 막지 않게 삼킨다.\n try {\n onOpen?.({ fromNudge: Boolean(consumed), hintText: consumed?.text });\n } catch (err) {\n console.warn(\"[XbNova] onOpen 콜백 오류\", err);\n }\n }\n\n function closeSheet() {\n setOpen(false);\n sendEvent(\"chat_close\");\n }\n\n if (!open) {\n if (barHidden) return null;\n\n // 평소엔 원형 아바타 하나뿐이다가, 힌트가 뜨는 순간 가로로 펼쳐지며 문구가 나타난다.\n // 힌트 유무에 따라 완전히 다른 JSX(별도 return)를 그리면 DOM 자체가 통째로 바뀌어서\n // 브라우저가 전환을 애니메이션할 대상이 없다 — 같은 노드를 계속 그리고 width만 CSS\n // transition으로 움직여야 \"슈우웅\" 펼쳐지는 효과가 난다.\n const showHint = Boolean(hint && hintsEnabled);\n const collapsedSize = \"3.25rem\";\n const idleLabel = lastHint?.text ?? idleText;\n\n return (\n <XbNovaShadowRoot style={{ ...barBaseStyle(isDesktop), ...themeVars, ...barStyle }}>\n {/* `marginLeft: auto`로 고정폭 호스트(barBaseStyle)의 오른쪽 끝에 붙인다 — 접혀\n 있을 때도 펼쳐져 있을 때도 pill의 오른쪽 끝은 항상 같은 자리라, 아바타(맨\n 오른쪽 슬롯)는 두 상태에서 화면상 거의 같은 위치에 머물고 pill만 왼쪽으로\n 자라나는 것처럼 보인다. */}\n {/* 바깥쪽은 크기/위치만 잡는다. overflow-hidden·배경·내용은 전부 안쪽 div가\n 맡는다 — 배경 자체가 (참고 이미지처럼) 부드럽게 번진 색 덩어리라, border처럼\n 요소 밖으로 삐져나오는 부분이 아예 없다. 그래서 여기 바깥쪽엔 회전 테두리\n 때 있었던 position:relative 같은 위치 계산이 더 필요 없다 — background는\n 항상 자기 요소의 박스 안에서만 그려지므로 구조적으로 새어나갈 수 없다. */}\n <div\n style={{\n minHeight: collapsedSize,\n height: showHint ? \"auto\" : collapsedSize,\n width: showHint ? \"100%\" : collapsedSize,\n marginLeft: \"auto\",\n borderRadius: showHint ? \"1.5rem\" : \"9999px\",\n transition: \"width 320ms cubic-bezier(0.22, 1, 0.36, 1), height 200ms ease, border-radius 200ms ease\",\n }}\n >\n <div\n className={`flex h-full w-full items-center overflow-hidden ${showHint ? \"xbnova-glow-bg\" : \"\"}`}\n style={{\n borderRadius: \"inherit\",\n // GlowBlobs(SVG)를 이 안에 절대위치로 깔려면 이 요소가 위치 기준점이어야\n // 한다 — 인라인으로 줘서 shadow DOM 비동기 CSS 로딩과 경합하지 않는다.\n position: \"relative\",\n // 접혀있을 땐 이 컨테이너 자체가 완전히 안 보인다(배경도 그림자도 없음) —\n // 스크롤 중에도 마찬가지다. 예전엔 스크롤 중 반투명 회색 배경을 깔았는데,\n // 그 배경 자체가 페이지 위에 원형 얼룩처럼 보인다는 피드백을 받았다\n // (\"스크롤 좀만 움직여도 저 투명한게 회색으로 바뀜\") — 그냥 안 보이는 게\n // 맞다. 펼쳐지면(showHint) xbnova-glow-bg 클래스가 배경/블러를 전부 맡는다.\n background: showHint ? undefined : \"transparent\",\n // 접힌 상태의 \"떠 있는\" 느낌은 이 컨테이너가 아니라 안쪽 아바타 배지\n // (아래 `shadow-md`, 지름 28px)만 낸다 — 52px 컨테이너 전체에 그림자를\n // 주면 그 자체가 회색 원 얼룩처럼 보인다.\n // 참고 이미지(카드 위에 살짝 겹쳐 얹힌 \"프로\"/\"미완\" 뱃지)처럼 진짜 스티커가\n // 붕 떠 있는 느낌은 은은하게 퍼지는 그림자가 아니라 — 붙어있는 면에 짧고\n // 진한 그림자(contact shadow) + 그보다 넓게 퍼지지만 방향성 있는 그림자를\n // 겹쳐야 난다. 예전 값(퍼지기만 하는 단일 그림자)은 배경에 스며드는\n // \"glow\"처럼 보여서 뜬 느낌이 안 났다.\n boxShadow: showHint\n ? \"0 2px 3px rgba(16,24,40,.35), 0 14px 24px -6px rgba(16,24,40,.45)\"\n : \"none\",\n transition: \"background 200ms ease, box-shadow 200ms ease\",\n }}\n >\n {showHint ? <GlowBlobs /> : null}\n {showHint ? (\n <button\n type=\"button\"\n onClick={dismissHint}\n aria-label=\"힌트 닫기\"\n className=\"shrink-0 py-3.5 pl-3.5 pr-1 text-slate-400 hover:text-slate-200\"\n style={{ position: \"relative\" }}\n >\n ✕\n </button>\n ) : null}\n <button\n type=\"button\"\n onClick={openSheet}\n aria-label={showHint ? hint!.text : idleLabel}\n className=\"flex min-w-0 flex-1 items-center text-left\"\n style={{ position: \"relative\" }}\n >\n {/* 길어지면 줄임표로 자르지 않고 줄바꿈한다 — pill 높이가 그만큼 늘어난다\n (`height: auto`, 바깥쪽 div 참고). */}\n {showHint ? (\n <span className=\"min-w-0 flex-1 whitespace-normal break-words py-3.5 pl-2 pr-1 text-[13px] font-semibold leading-snug text-white\">\n {typedHintText}\n </span>\n ) : null}\n {/* 아바타 슬롯을 collapsedSize로 고정해둔다 — 접혀있을 땐 이 슬롯 자체가 원이고\n (안이 정중앙), 펼쳐지면 같은 슬롯이 pill의 맨 오른쪽 위쪽에 그대로 남고\n 왼쪽에 문구가 줄바꿈되며 늘어난다. 버튼에 직접 padding을 주면 접힌 원\n 크기(52px)보다 아바타+패딩이 커져서 아바타가 안 맞는 문제가 있었다. */}\n <span\n className=\"flex shrink-0 items-center justify-center\"\n style={{ width: collapsedSize, height: collapsedSize }}\n >\n <Avatar\n branding={branding}\n ready={brandingReady}\n size={avatarSize}\n className=\"shadow-md\"\n gradient={avatarGradientStyle}\n />\n </span>\n </button>\n </div>\n </div>\n </XbNovaShadowRoot>\n );\n }\n\n const summary = resolveFactValue(factsRegistry.get(SUMMARY_FACT_KEY)) as string | undefined;\n const focusedLabel = getFocusedTrackEntry(trackRegistry)?.label;\n const contextLine = summary ?? (focusedLabel ? `${focusedLabel} 보는 중` : undefined);\n\n return (\n <>\n {!isDesktop ? (\n <XbNovaShadowRoot\n style={{\n ...backdropBaseStyle(isDesktop),\n opacity: entered ? 1 : 0,\n transition: \"opacity 260ms ease-out\",\n ...backdropStyle,\n }}\n >\n <div className=\"h-full w-full bg-black/45\" onClick={closeSheet} />\n </XbNovaShadowRoot>\n ) : null}\n <XbNovaShadowRoot\n style={{\n ...panelBaseStyle(isDesktop),\n ...themeVars,\n transform: entered ? \"translateY(0)\" : \"translateY(100%)\",\n transition: \"transform 280ms cubic-bezier(0.22, 1, 0.36, 1)\",\n ...panelStyle,\n }}\n >\n <div className=\"flex h-full w-full flex-col rounded-t-[20px] bg-white shadow-2xl sm:rounded-2xl sm:border sm:border-neutral-200 dark:bg-neutral-900 sm:dark:border-neutral-700\">\n <header className=\"flex items-center gap-2 border-b border-neutral-200 px-3.5 py-2.5 dark:border-neutral-700\">\n <Avatar\n branding={branding}\n ready={brandingReady}\n size={headerAvatarSize}\n className=\"shrink-0\"\n gradient={avatarGradientStyle}\n />\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex min-w-0 items-center gap-1.5\">\n <span className=\"truncate text-sm font-semibold\">{onboarding?.branding?.title ?? \"AI 동행\"}</span>\n {/* 봇 설정의 배지(예: \"Beta\"). 비어 있으면 그리지 않는다. */}\n {badgeText ? (\n <span className=\"shrink-0 rounded-full border border-neutral-300 px-1.5 py-px text-[10px] font-semibold leading-4 text-neutral-500 dark:border-neutral-600 dark:text-neutral-400\">\n {badgeText}\n </span>\n ) : null}\n </div>\n {contextLine ? (\n <div className=\"truncate text-[11px] text-neutral-400\">{contextLine}</div>\n ) : null}\n </div>\n <button\n type=\"button\"\n onClick={closeSheet}\n aria-label=\"채팅 닫기\"\n className=\"shrink-0 text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-200\"\n >\n ×\n </button>\n </header>\n\n <div className=\"flex-1 space-y-3 overflow-y-auto p-4\">\n {showDisclosure ? (\n <div className=\"rounded-lg bg-neutral-50 px-3 py-2 text-[11px] text-neutral-400 dark:bg-neutral-800\">\n 지금 보고 계신 화면과 매물 정보를 기준으로 안내해 드려요.\n </div>\n ) : null}\n\n {opening ? (\n <Bubble role=\"assistant\">\n <AssistantMarkdown text={opening} />\n </Bubble>\n ) : null}\n\n {messages.map((message, i) => (\n <div key={i}>\n {message.role === \"user\" ? (\n <Bubble role=\"user\">\n {message.parts.map((p) => (p.kind === \"text\" ? p.text : \"\")).join(\"\")}\n </Bubble>\n ) : (\n <AnswerActionsFrame\n enabled={feedbackEnabled && !busy && !!message.id && message.parts.some((p) => p.kind === \"text\" && p.text.trim())}\n text={message.parts.filter((p) => p.kind === \"text\").map((p) => (p.kind === \"text\" ? p.text : \"\")).join(\"\\n\\n\")}\n rating={message.id ? (feedback[message.id] ?? null) : null}\n isLast={i === messages.length - 1}\n onRate={(r, reason) => void setFeedback(message.id!, r, reason)}\n >\n {message.parts.map((part, j) => {\n if (part.kind === \"text\") {\n return (\n <Bubble key={j} role=\"assistant\">\n <AssistantMarkdown text={part.text} />\n </Bubble>\n );\n }\n // client action tool은 기본적으로 채팅에 아무것도 안 그린다 — 핸들러\n // 디스패치는 위 useEffect가 담당한다(딱 한 번, 렌더와 분리). `render`를\n // 등록해둔 client action만 예외로, 그 자리에 직접 UI를 그린다(예: 상담\n // 신청 버튼).\n if (clientActionHandlers.has(part.toolName)) {\n // 핸들러가 view+output(정규화된 결과)을 돌려줬으면 tool 이름과 무관하게 그 view로 그린다.\n const result = clientResults[part.toolCallId];\n if (result) {\n return (\n <ToolView\n key={j}\n part={{ ...part, state: \"output\", view: result.view, output: result.output }}\n viewRenderers={viewRenderers}\n onVerdictAction={onVerdictAction}\n />\n );\n }\n const spec = clientActionHandlers.get(part.toolName);\n // 호스트가 그린 버튼이라 SDK는 onClick을 못 붙인다 — 감싼 div에서 캡처해 어떤 버튼이 눌렸는지\n // (문구) 기록한다. 호스트의 클릭 동작(팝업 열기 등)은 그대로 진행된다.\n return spec?.render ? (\n <div\n key={j}\n onClickCapture={(e) => {\n const control = clickedControl(e);\n if (control) trackClientActionClick(part.toolName, control.label);\n }}\n >\n {spec.render(part.input)}\n </div>\n ) : null;\n }\n return (\n <ToolView key={j} part={part} viewRenderers={viewRenderers} onVerdictAction={onVerdictAction} />\n );\n })}\n {!hasVisiblePart(message.parts, clientActionHandlers, clientResults) && busy ? (\n <Bubble role=\"assistant\">\n <span className=\"text-neutral-400\">생각하는 중…</span>\n </Bubble>\n ) : null}\n </AnswerActionsFrame>\n )}\n </div>\n ))}\n\n {error ? (\n <div className=\"rounded-lg bg-red-50 px-3 py-2 text-xs text-red-700 dark:bg-red-950 dark:text-red-300\">\n {error}\n </div>\n ) : null}\n <div ref={bottomRef} />\n </div>\n\n {(() => {\n // 넛지를 눌러 열었으면 그 넛지 전용 버튼을 우선한다 — 어떤 트리거든 공통으로\n // 뜨는 봇 기본 quick_replies(온보딩 설정)보다 지금 맥락에 맞는 버튼이 낫다.\n // 넛지 칩은 messages.length와 무관하게 뜬다 — 이미 대화가 있는 상태에서 다른\n // 곳을 보다가 새 넛지를 눌러 열어도, 그 넛지의 칩은 보여야 방금 클릭한 이유가\n // 이어진다(기존엔 messages.length !== 0이면 통째로 숨겨서 이 경우가 안 됐다).\n // 반면 봇 기본 quick_replies(onboarding)는 정말 \"첫 진입\"에만 의미가 있으니\n // 대화가 이미 있으면 그대로 숨긴다.\n const prompt = messages.length > 0 ? nudgePrompt : null;\n if (prompt || nudgeQuickReplies.length > 0) {\n return (\n <div className=\"space-y-2 border-t border-neutral-200 px-3.5 py-2.5 dark:border-neutral-700\">\n {prompt ? (\n <div className=\"text-sm font-medium\">\n <AssistantMarkdown text={prompt} />\n </div>\n ) : null}\n {nudgeQuickReplies.length > 0 ? (\n <div className=\"flex flex-wrap gap-1.5\">\n {nudgeQuickReplies.map((q) => (\n <button\n key={q}\n onClick={() => {\n setNudgeQuickReplies([]);\n setNudgePrompt(null);\n send(q, prompt);\n }}\n className=\"xbnova-chip rounded-full border px-2.5 py-1 text-[11px] font-semibold transition\"\n >\n {q}\n </button>\n ))}\n </div>\n ) : null}\n </div>\n );\n }\n const quickReplies = onboarding?.quick_replies ?? [];\n if (messages.length !== 0 || quickReplies.length === 0) return null;\n return (\n <div className=\"flex flex-wrap gap-1.5 border-t border-neutral-200 px-3.5 py-2.5 dark:border-neutral-700\">\n {quickReplies.map((q) => (\n <button\n key={q}\n onClick={() => send(q)}\n className=\"xbnova-chip rounded-full border px-2.5 py-1 text-[11px] font-semibold transition\"\n >\n {q}\n </button>\n ))}\n </div>\n );\n })()}\n\n <form\n onSubmit={(e) => {\n e.preventDefault();\n setNudgeQuickReplies([]);\n setNudgePrompt(null);\n send(input, messages.length > 0 ? nudgePrompt : null);\n setInput(\"\");\n }}\n className=\"flex items-center gap-2 border-t border-neutral-200 px-3 py-2.5 dark:border-neutral-700\"\n >\n <input\n value={input}\n onChange={(e) => setInput(e.target.value)}\n placeholder=\"이 차에 대해 무엇이든 물어보세요\"\n disabled={busy}\n className=\"xbnova-input-accent flex-1 rounded-full border border-neutral-300 bg-white px-3.5 py-2 text-sm text-neutral-900 placeholder:text-neutral-400 outline-none disabled:opacity-50 dark:border-neutral-600 dark:bg-neutral-800 dark:text-neutral-100 dark:placeholder:text-neutral-500\"\n />\n <button\n type=\"submit\"\n disabled={busy || !input.trim()}\n aria-label=\"전송\"\n className=\"xbnova-accent-btn flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-white transition disabled:opacity-40\"\n >\n ↑\n </button>\n </form>\n {/* 봇 설정의 AI 고지 문구. 대화 중에도 늘 보이도록 입력창 아래에 둔다. 비어 있으면 그리지 않는다. */}\n {disclaimer ? (\n <p className=\"-mt-1 px-4 pb-2.5 text-center text-[11px] leading-snug text-neutral-400 dark:text-neutral-500\">\n {disclaimer}\n </p>\n ) : null}\n </div>\n </XbNovaShadowRoot>\n </>\n );\n}\n\n/** AI 버튼·채팅 헤더의 동그란 아바타. 봇 설정(branding)에 이미지 주소가 있으면 이미지, 없고 글자가\n * 있으면 글자(이모지), 둘 다 없거나 이미지가 깨지면 기본 \"AI\" 배지. 호스트 사이트의 CSP가 외부 이미지를\n * 막거나 주소가 틀려도 빈 원이 되지 않게 onError로 글자 배지로 떨어진다.\n *\n * 깜빡임 방지: branding 은 서버에서 받아오고(`ready` 전), 이미지는 내려받는 시간이 걸린다. 그 사이에 \"AI\"\n * 글자를 먼저 보여주면 이미지로 바뀌며 깜빡이므로, 확정되기 전에는 **글자 없는 그라디언트 원**만 두고\n * 이미지는 다 받은 뒤 부드럽게 나타나게 한다. */\nfunction Avatar({\n branding,\n ready,\n size,\n className = \"\",\n gradient,\n}: {\n branding?: { logo_url?: string; avatar_text?: string };\n /** branding 이 확정됐는가(서버 응답 또는 캐시). false 면 글자·이미지 없이 빈 원만 그린다. */\n ready: boolean;\n /** 지름(px). 글자 배지의 글자 크기도 여기서 정해진다. */\n size: number;\n className?: string;\n gradient: React.CSSProperties;\n}) {\n const box: React.CSSProperties = { width: size, height: size };\n const logoUrl = branding?.logo_url?.trim() ?? \"\";\n const [failedUrl, setFailedUrl] = useState(\"\");\n const [loadedUrl, setLoadedUrl] = useState(\"\");\n const base = `${className} relative flex flex-none items-center justify-center overflow-hidden rounded-full font-bold text-white`;\n\n if (!ready) {\n return <span className={base} style={{ ...gradient, ...box }} aria-hidden />;\n }\n if (logoUrl && failedUrl !== logoUrl) {\n return (\n <span className={base} style={{ ...gradient, ...box }}>\n <img\n src={logoUrl}\n alt=\"\"\n onLoad={() => setLoadedUrl(logoUrl)}\n onError={() => setFailedUrl(logoUrl)}\n className=\"absolute inset-0 h-full w-full rounded-full bg-white object-cover transition-opacity duration-200\"\n style={{ opacity: loadedUrl === logoUrl ? 1 : 0 }}\n />\n </span>\n );\n }\n const text = branding?.avatar_text?.trim() || \"AI\";\n return (\n <span className={base} style={{ ...gradient, ...box, fontSize: Math.max(10, Math.round(size * 0.4)) }}>\n {text}\n </span>\n );\n}\n\nconst THUMB_UP_PATH =\n \"M7 10v12M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2a3.13 3.13 0 0 1 3 3.88Z\";\nconst THUMB_DOWN_PATH =\n \"M17 14V2M9 18.12 10 14H4.17a2 2 0 0 1-1.92-2.56l2.33-8A2 2 0 0 1 6.5 2H20a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-2.76a2 2 0 0 0-1.79 1.11L12 22a3.13 3.13 0 0 1-3-3.88Z\";\nconst COPY_PATH = \"M9 9h11v11H9zM5 15H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1\";\nconst CHECK_PATH = \"m5 12 5 5L20 7\";\n\nconst FEEDBACK_REASONS: { code: XbNovaFeedbackReason; label: string }[] = [\n { code: \"inaccurate\", label: \"정확하지 않아요\" },\n { code: \"irrelevant\", label: \"질문과 달라요\" },\n { code: \"too_long\", label: \"너무 길어요\" },\n { code: \"unclear\", label: \"이해하기 어려워요\" },\n];\n\nconst LONG_PRESS_MS = 420;\n\nfunction ActionIcon({ path, filled }: { path: string; filled?: boolean }) {\n return (\n <svg\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 24 24\"\n fill={filled ? \"currentColor\" : \"none\"}\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d={path} />\n </svg>\n );\n}\n\nasync function copyText(text: string): Promise<boolean> {\n try {\n await navigator.clipboard.writeText(text);\n return true;\n } catch {\n // clipboard API가 막힌 환경(http, iframe 등) — 임시 textarea 로 폴백.\n try {\n const ta = document.createElement(\"textarea\");\n ta.value = text;\n ta.style.position = \"fixed\";\n ta.style.opacity = \"0\";\n document.body.appendChild(ta);\n ta.select();\n const ok = document.execCommand(\"copy\");\n ta.remove();\n return ok;\n } catch {\n return false;\n }\n }\n}\n\n/**\n * 어시스턴트 답변 하나를 감싸 복사·👍·👎 액션을 붙인다.\n * - 마지막 답변: 아래에 아이콘 3개가 항상 보인다.\n * - 이전 답변: 평소엔 숨기고, 터치는 길게 누르면 / 마우스는 올리면 떠오르는 알약 메뉴로 보여준다.\n * - 👎를 누르면 사유 칩이 한 줄 나온다(안 골라도 된다).\n */\nfunction AnswerActions({\n text,\n rating,\n isLast,\n onRate,\n children,\n}: {\n text: string;\n rating: XbNovaFeedbackRating;\n isLast: boolean;\n onRate: (r: XbNovaFeedbackRating, reason?: XbNovaFeedbackReason) => void;\n children: React.ReactNode;\n}) {\n const rootRef = useRef<HTMLDivElement>(null);\n const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const [menuOpen, setMenuOpen] = useState(false);\n const [reasonOpen, setReasonOpen] = useState(false);\n const [copied, setCopied] = useState(false);\n const [entered, setEntered] = useState(false);\n\n const clearTimer = () => {\n if (timerRef.current) clearTimeout(timerRef.current);\n timerRef.current = null;\n };\n useEffect(() => clearTimer, []);\n\n // 메뉴가 열려 있으면 바깥을 건드릴 때 닫는다.\n useEffect(() => {\n if (!menuOpen) {\n setEntered(false);\n return;\n }\n const raf = requestAnimationFrame(() => setEntered(true));\n const onDown = (e: PointerEvent) => {\n if (!e.composedPath().includes(rootRef.current as EventTarget)) setMenuOpen(false);\n };\n document.addEventListener(\"pointerdown\", onDown, true);\n return () => {\n cancelAnimationFrame(raf);\n document.removeEventListener(\"pointerdown\", onDown, true);\n };\n }, [menuOpen]);\n\n const startPress = (e: React.PointerEvent) => {\n if (e.pointerType === \"mouse\" || isLast) return;\n clearTimer();\n timerRef.current = setTimeout(() => {\n setMenuOpen(true);\n if (typeof navigator.vibrate === \"function\") navigator.vibrate(10);\n }, LONG_PRESS_MS);\n };\n\n const handleCopy = async () => {\n if (await copyText(text)) {\n setCopied(true);\n setTimeout(() => setCopied(false), 1400);\n }\n };\n const handleRate = (kind: \"up\" | \"down\") => {\n if (rating === kind) {\n onRate(null);\n setReasonOpen(false);\n return;\n }\n onRate(kind);\n setReasonOpen(kind === \"down\");\n if (kind === \"up\") setMenuOpen(false);\n };\n\n const barVisible = isLast || menuOpen;\n const floating = menuOpen && !isLast;\n const btn = (label: string, onClick: () => void, selected: boolean, color: string | undefined, icon: React.ReactNode) => (\n <button\n type=\"button\"\n aria-label={label}\n aria-pressed={selected}\n onClick={onClick}\n className={`flex items-center justify-center gap-1 rounded-full text-neutral-400 transition-colors hover:text-neutral-600 dark:text-neutral-500 dark:hover:text-neutral-300 ${\n floating ? \"h-9 px-3 text-xs font-semibold\" : \"h-7 w-7\"\n }`}\n style={color ? { color } : undefined}\n >\n {icon}\n {floating ? <span>{label}</span> : null}\n </button>\n );\n\n return (\n <div\n ref={rootRef}\n className=\"group space-y-1\"\n onPointerDown={startPress}\n onPointerUp={clearTimer}\n onPointerCancel={clearTimer}\n onPointerLeave={clearTimer}\n onPointerMove={(e) => {\n if (Math.abs(e.movementX) + Math.abs(e.movementY) > 6) clearTimer();\n }}\n onContextMenu={(e) => {\n // 터치 길게 누르기가 브라우저 기본 메뉴를 띄우지 않게 한다 — 마우스 우클릭은 그대로 둔다.\n if (!isLast && (e.nativeEvent as PointerEvent).pointerType !== \"mouse\") e.preventDefault();\n }}\n >\n {children}\n <div\n className={`flex pl-1 ${barVisible ? \"\" : \"hidden md:group-hover:flex\"}`}\n style={{ transition: \"opacity 140ms, transform 140ms\", opacity: floating && !entered ? 0 : 1, transform: floating && !entered ? \"scale(0.94)\" : \"none\" }}\n >\n <div\n className={`flex items-center ${\n floating\n ? \"gap-0.5 rounded-full border border-neutral-200 bg-white p-1 shadow-lg dark:border-neutral-700 dark:bg-neutral-800\"\n : \"gap-0.5\"\n }`}\n >\n {btn(copied ? \"복사됨\" : \"복사\", handleCopy, false, copied ? \"var(--xbnova-accent-1, #4f46e5)\" : undefined, (\n <ActionIcon path={copied ? CHECK_PATH : COPY_PATH} />\n ))}\n {btn(\n \"좋아요\",\n () => handleRate(\"up\"),\n rating === \"up\",\n rating === \"up\" ? \"var(--xbnova-accent-1, #4f46e5)\" : undefined,\n <ActionIcon path={THUMB_UP_PATH} filled={rating === \"up\"} />,\n )}\n {btn(\n \"별로예요\",\n () => handleRate(\"down\"),\n rating === \"down\",\n rating === \"down\" ? \"#dc2626\" : undefined,\n <ActionIcon path={THUMB_DOWN_PATH} filled={rating === \"down\"} />,\n )}\n </div>\n </div>\n {rating === \"down\" && reasonOpen ? (\n <div className=\"flex flex-wrap items-center gap-1.5 pl-1 pt-0.5\">\n <span className=\"text-[11px] text-neutral-400\">어떤 점이 아쉬웠나요?</span>\n {FEEDBACK_REASONS.map((r) => (\n <button\n key={r.code}\n type=\"button\"\n onClick={() => {\n onRate(\"down\", r.code);\n setReasonOpen(false);\n setMenuOpen(false);\n }}\n className=\"xbnova-chip rounded-full border px-2.5 py-1 text-[11px] font-semibold transition\"\n >\n {r.label}\n </button>\n ))}\n </div>\n ) : null}\n </div>\n );\n}\n\nfunction Bubble({ role, children }: { role: \"user\" | \"assistant\"; children: React.ReactNode }) {\n const mine = role === \"user\";\n return (\n <div className={mine ? \"flex justify-end\" : \"flex justify-start\"}>\n <div\n className={`max-w-[85%] rounded-2xl px-3.5 py-2 text-sm ${\n mine ? \"whitespace-pre-wrap\" : \"\"\n } ${\n mine\n ? \"xbnova-accent-btn text-white\"\n : \"bg-neutral-100 text-neutral-800 dark:bg-neutral-800 dark:text-neutral-100\"\n }`}\n >\n {children}\n </div>\n </div>\n );\n}\n\n// 어시스턴트 텍스트만 마크다운으로 그린다 — 사용자 입력은 항상 순수 텍스트다(마크다운으로\n// 잘못 해석할 이유가 없고, 오히려 사용자가 실수로 친 `*`/`#`이 서식으로 바뀌면 이상하다).\n// remark-breaks: 모델이 문단 사이에 빈 줄 없이 한 줄바꿈만 쓰는 경우가 흔해서(CommonMark\n// 기본은 이걸 무시한다) 강제로 <br>로 살린다.\nfunction AssistantMarkdown({ text }: { text: string }) {\n return (\n <div className=\"space-y-1.5 text-sm leading-relaxed [&_ol]:list-decimal [&_ul]:list-disc [&_ol]:pl-5 [&_ul]:pl-5 [&_li]:pl-0.5 [&_strong]:font-semibold [&_a]:underline [&_code]:rounded [&_code]:bg-black/10 [&_code]:px-1 [&_code]:py-0.5 [&_code]:text-[0.85em] [&_p]:mb-1.5 last:[&_p]:mb-0\">\n <ReactMarkdown remarkPlugins={[remarkGfm, remarkBreaks]}>{text}</ReactMarkdown>\n </div>\n );\n}\n\n/** 액션을 못 붙이는 상태(스트리밍 중, id 없음)에선 기존과 같은 래퍼만 그린다. */\nfunction AnswerActionsFrame({\n enabled,\n children,\n ...rest\n}: {\n enabled: boolean;\n text: string;\n rating: XbNovaFeedbackRating;\n isLast: boolean;\n onRate: (r: XbNovaFeedbackRating, reason?: XbNovaFeedbackReason) => void;\n children: React.ReactNode;\n}) {\n if (!enabled) return <div className=\"space-y-1\">{children}</div>;\n return <AnswerActions {...rest}>{children}</AnswerActions>;\n}\n","\"use client\";\n\n/**\n * Shadow DOM 마운트 — 고객사 페이지 CSS와 위젯 스타일을 서로 못 건드리게 격리한다\n * (Intercom·Drift 같은 임베드형 위젯 방식, SDK.md \"스타일 격리\" 참고).\n *\n * 컴파일된 CSS(`dist/style.css`, 빌드 시 Tailwind CLI가 만든다)를 `import.meta.url` 기준\n * 상대 경로로 fetch해 shadow root 안 `<style>`에 주입한다 — 그래서 소비자는\n * `import \"@xbrain-swe/nova-chat-widget/style.css\"`를 따로 안 해도 된다. 같은 CSS를 여러 위젯\n * 인스턴스(채팅 + 넛지 버블)가 동시에 쓰므로 fetch 결과를 모듈 스코프에서 캐시한다.\n *\n * 호스트 요소를 `document.body`로 포탈한다 — `<XbNovaProvider>`가 고객사 페이지 트리\n * 깊숙이 들어가 있으면(실제로 ctr-client가 그렇다) 조상 중 하나가 `overflow: clip`이나\n * `transform`을 걸어뒀을 때 `position: fixed`가 escape 못 하고 잘리거나 엉뚱한 곳에\n * 고정될 수 있다 — `overflow: clip`은 `hidden`과 달리 fixed 자손까지 잘라내는 게\n * 스펙이다. body 바로 아래로 포탈하면 이 문제 자체가 생기지 않는다(Intercom 등 실제\n * 임베드 위젯이 쓰는 방식과 같다).\n */\n\nimport { useEffect, useRef, useState } from \"react\";\nimport { createPortal } from \"react-dom\";\n\nimport { type XbNovaTheme, useXbNovaInternal } from \"./internal-context\";\n\nlet cssPromise: Promise<string> | null = null;\n\nfunction loadCss(): Promise<string> {\n if (!cssPromise) {\n // CJS 빌드는 import.meta.url이 비어 있다(tsup 경고) — 그 경우 스타일 없이 조용히 넘어간다.\n // 최신 번들러(Next.js/Vite/webpack)는 전부 ESM을 골라 쓰므로 실제로는 거의 안 타는 경로다.\n try {\n cssPromise = fetch(new URL(\"./style.css\", import.meta.url))\n .then((r) => (r.ok ? r.text() : \"\"))\n .catch(() => \"\");\n } catch {\n cssPromise = Promise.resolve(\"\");\n }\n }\n return cssPromise;\n}\n\n/** `\"system\"`이면 브라우저 설정(prefers-color-scheme)을 구독해 실제 값으로 바꾼다. SSR/첫 렌더는\n * 항상 light — 마운트 뒤에 맞춘다(위젯은 마운트 후에야 그려지므로 깜빡임이 보이지 않는다). */\nfunction useResolvedScheme(scheme: XbNovaTheme[\"colorScheme\"]): \"light\" | \"dark\" {\n const [systemDark, setSystemDark] = useState(false);\n useEffect(() => {\n if (scheme !== \"system\") return;\n const mq = window.matchMedia(\"(prefers-color-scheme: dark)\");\n setSystemDark(mq.matches);\n const onChange = (e: MediaQueryListEvent) => setSystemDark(e.matches);\n mq.addEventListener(\"change\", onChange);\n return () => mq.removeEventListener(\"change\", onChange);\n }, [scheme]);\n if (scheme === \"system\") return systemDark ? \"dark\" : \"light\";\n return scheme;\n}\n\nexport function XbNovaShadowRoot({\n children,\n style,\n}: {\n children: React.ReactNode;\n /** 호스트(라이트 DOM) 요소의 위치. 반드시 인라인 style이어야 한다 — Tailwind 클래스\n * 문자열은 안 된다. 호스트는 Shadow DOM *밖*이라 SDK가 빌드 시점에 미리 컴파일해\n * shadow root 안에 주입하는 CSS(`dist/style.css`)가 아예 안 닿는다 — 페이지 자체\n * 스타일시트에 우연히 같은 클래스가 있지 않은 한 조용히 무시된다. 인라인 style은\n * 클래스 조회 자체가 없어서 이 문제를 원천적으로 피한다. 화면에 보이는 색상·둥근\n * 모서리 같은 건 `children`(shadow root 안쪽) 쪽 Tailwind 클래스로 준다 — 그건\n * SDK 소스에 정적으로 있어서 항상 컴파일된다. */\n style?: React.CSSProperties;\n}) {\n const { theme } = useXbNovaInternal();\n const scheme = useResolvedScheme(theme.colorScheme);\n const hostRef = useRef<HTMLDivElement>(null);\n const [mountNode, setMountNode] = useState<HTMLElement | null>(null);\n // document.body는 SSR에 없다 — 마운트 이후에만 포탈 타깃을 채운다(그 전까지는 렌더 안 함).\n const [portalTarget, setPortalTarget] = useState<HTMLElement | null>(null);\n\n useEffect(() => {\n setPortalTarget(document.body);\n }, []);\n\n useEffect(() => {\n const host = hostRef.current;\n if (!host || host.shadowRoot) return;\n\n const shadow = host.attachShadow({ mode: \"open\" });\n const style = document.createElement(\"style\");\n shadow.appendChild(style);\n loadCss().then((css) => {\n style.textContent = css;\n });\n\n const mount = document.createElement(\"div\");\n // 호스트(`position:fixed` + top/bottom 등)는 정의된 높이를 갖지만, 이 mount div는\n // 아무 스타일도 없는 평범한 div라 기본값(auto)은 \"내용물 높이\"다 — 그래서 안쪽 `h-full`\n // Tailwind 클래스가 기준 삼을 부모 높이가 없어 패널이 내용물 크기로 쪼그라들고, 그\n // 뒤로 페이지가 비쳐 보이는 버그가 있었다. 100%로 명시해 호스트 크기를 그대로 물려준다.\n mount.style.height = \"100%\";\n mount.style.width = \"100%\";\n shadow.appendChild(mount);\n setMountNode(mount);\n }, [portalTarget]);\n\n // styles.css 의 `dark:` 변형이 이 속성을 본다. color-scheme 은 입력창·스크롤바 같은 기본\n // 컨트롤 색을 맞춘다.\n useEffect(() => {\n if (!mountNode) return;\n mountNode.dataset.xbnovaScheme = scheme;\n mountNode.style.colorScheme = scheme;\n }, [mountNode, scheme]);\n\n if (!portalTarget) return null;\n\n return createPortal(\n <div ref={hostRef} style={style}>\n {mountNode ? createPortal(children, mountNode) : null}\n </div>,\n portalTarget,\n );\n}\n","\"use client\";\n\n/**\n * 헤드리스 채팅 훅 — `nova/web/components/chat.tsx`에서 상태·전송 로직만 뺀 것. 로직 자체는\n * 안 바꿨다(SDK.md). 추가된 건 두 가지: `context` 자동 채우기(메시지를 보낼 때 지금 보고\n * 있는 `XbNovaTrack` 영역을 자동으로 실어 보낸다)와 client action tool 디스패치다.\n *\n * client action 디스패치를 `chat.tsx`(UI)가 아니라 여기서 하는 이유: `messages`/\n * `setMessages`를 이미 들고 있고, `awaitResult: true`인 tool은 핸들러가 실제로 조회한\n * 결과를 서버로 다시 보내(`resumeToolResult`) 응답을 이어받아야 하는데 — 그 이어받은\n * 조각을 \"지금 그리고 있는 바로 그 assistant 메시지\"에 이어붙이려면 상태를 쥔 곳에서\n * 처리하는 게 자연스럽다. `chat.tsx`는 결과 렌더링만 신경 쓴다.\n */\n\nimport { useEffect, useRef, useState } from \"react\";\n\nimport {\n type ClientActionHandler,\n collectContextEntries,\n resolveFactValue,\n useXbNovaInternal,\n} from \"./internal-context\";\nimport type { XbNovaClientActionResult } from \"./client-actions\";\nimport {\n type ChatMessage,\n type MessagePart,\n type StoredMessage,\n type ToolPart,\n type ViewKind,\n fromStoredMessages,\n resumeToolResult,\n streamChat,\n} from \"./stream\";\nimport { getFocusedTrackEntry } from \"./track\";\n\n/** awaitResult client action 핸들러의 기본 응답 대기 시간. `timeoutMs`로 액션별 override. */\nconst DEFAULT_CLIENT_ACTION_TIMEOUT_MS = 15000;\n\nexport type ClientResults = Record<string, { view: ViewKind; output: unknown }>;\n\nexport type XbNovaFeedbackRating = \"up\" | \"down\" | null;\n\n/** 👎 사유 칩의 코드. */\nexport type XbNovaFeedbackReason = \"inaccurate\" | \"irrelevant\" | \"too_long\" | \"unclear\";\n\n/** 답변 만족도 저장 성공 후 호스트로 전달되는 이벤트. `rating: null`은 평가 취소. */\nexport type XbNovaFeedbackEvent = {\n conversationId: string;\n messageId: string;\n rating: XbNovaFeedbackRating;\n /** 👎일 때 사용자가 고른 사유 코드(없으면 undefined). */\n reason?: XbNovaFeedbackReason;\n};\n\nexport type XbNovaOnboarding = {\n /** 이어 열 대화가 없으면 null — 첫 메시지를 보낼 때 서버가 만든다(지연 생성). */\n conversation_id: string | null;\n /** 이어 연 대화의 이력. 새 대화면 빈 배열. */\n messages?: StoredMessage[];\n greeting: string;\n quick_replies: string[];\n branding: { title?: string; logo_url?: string; avatar_text?: string; badge_text?: string; disclaimer?: string };\n};\n\nexport type UseXbNovaChatResult = {\n onboarding: XbNovaOnboarding | null;\n /** 대화 맨 위에 그릴 첫 말풍선(넛지 문구·인사말). 이어 연 대화면 이력에 이미 있으니 null. */\n opening: string | null;\n messages: ChatMessage[];\n busy: boolean;\n error: string | null;\n /** `nudgeOpening`은 대화 중 넛지를 눌러 띄운 질문이다. 답을 보낼 때 그 앞에 AI 발화로 남긴다. */\n send: (text: string, nudgeOpening?: string | null) => Promise<void>;\n /** client action 핸들러가 돌려준 정규화 결과(tool_call_id별). 채팅 화면이 view 렌더러로 그린다. */\n clientResults: ClientResults;\n /** messageId → 현재 평가. 평가 안 한 메시지는 키가 없다. */\n feedback: Record<string, XbNovaFeedbackRating>;\n /** 낙관적으로 반영하고 서버에 저장한다. 실패하면 되돌린다. */\n setFeedback: (messageId: string, rating: XbNovaFeedbackRating, reason?: XbNovaFeedbackReason) => Promise<void>;\n /** client action이 그린 버튼(상담신청 등)이 눌렸을 때 채팅 화면이 부른다 — nova 넛지 퍼널의 마지막 단계. */\n trackClientActionClick: (action: string, label: string | null) => void;\n};\n\n/** `botId`를 넘기면 Provider의 기본 봇 대신 그 봇으로 새 대화를 연다 — 넛지 handoff가 쓴다.\n * `nudgeNodeId`를 넘기면 이 대화가 그 넛지에서 시작됐다고 서버에 표시한다 — 넛지 전환\n * 퍼널(노출→클릭→첫 질문→상담 버튼 노출→클릭)이 이 값이 채워진 대화를 넛지 대화로 본다.\n * `onDismissSheet`는 `dismissSheet: true`인 client action이 실행될 때 호출된다 —\n * `<XbNovaChat>`이 시트를 닫는 함수를 넘겨준다(모바일에서 시트가 페이지 대부분을 덮어서,\n * 안 닫으면 그 밑에서 일어나는 일이 안 보인다). */\nexport function useXbNovaChat(options?: {\n botId?: string;\n nudgeNodeId?: string;\n /** 위젯이 첫 말풍선으로 보여준 문구(넛지 문구). 첫 메시지를 보낼 때 서버에 같이 보내 AI가\n * \"방금 이런 말을 건넸다\"는 걸 알게 한다 — 없으면 봇 인사말을 쓴다. 서버가 설정과 일치하는지\n * 검증해서 맞는 것만 이력에 저장한다. */\n openingMessage?: string;\n onDismissSheet?: () => void;\n onFeedback?: (e: XbNovaFeedbackEvent) => void;\n}): UseXbNovaChatResult {\n const { auth, trackRegistry, contextRegistry, clientActionHandlers, visitorId, sessionId, sendEvent } = useXbNovaInternal();\n const botId = options?.botId ?? auth.botId;\n const nudgeNodeId = options?.nudgeNodeId;\n const { apiKey, apiBaseUrl, threadKey } = auth;\n // 호스트 유저 키가 있으면 visitorId가 곧 그 값이다(internal-context).\n const userKey = visitorId;\n\n const [onboarding, setOnboarding] = useState<XbNovaOnboarding | null>(null);\n const [messages, setMessages] = useState<ChatMessage[]>([]);\n // 이어 연 대화인지 — 그렇다면 첫 말풍선은 이력에 이미 있고, 넛지를 누르면 이력 아래에 붙인다.\n const [resumed, setResumed] = useState(false);\n // 첫 메시지를 보낼 때 실제로 서버에 보낸 첫 말풍선 — 그 뒤로 넛지를 또 눌러도 맨 위 말풍선은 바뀌지 않는다.\n const [sentOpening, setSentOpening] = useState<string | null>(null);\n // 대화 id는 state(onboarding)와 별도로 ref에도 둔다 — 지연 생성이면 첫 스트림의 응답 헤더로 받는데,\n // 같은 스트림 안에서 디스패치되는 client action이 리렌더를 기다리지 않고 그 id를 써야 한다.\n const conversationIdRef = useRef<string | null>(null);\n // 대화가 이미 있는데 넛지를 눌러 이력 아래 붙인 문구 — 다음 메시지와 함께 서버에 보내 이력에 남긴다.\n const messagesRef = useRef(messages);\n messagesRef.current = messages;\n const [busy, setBusy] = useState(false);\n const [error, setError] = useState<string | null>(null);\n // tool_call_id → 클라이언트 핸들러가 돌려준 정규화 결과(view+output). 채팅 화면이 이 값으로 그린다.\n const [clientResults, setClientResults] = useState<ClientResults>({});\n const [feedback, setFeedbackState] = useState<Record<string, XbNovaFeedbackRating>>({});\n const feedbackRef = useRef(feedback);\n feedbackRef.current = feedback;\n\n // tool_call_id별로 한 번만 디스패치한다 — `messages`가 스트리밍 중 계속 새 배열로\n // 갈아끼워지므로(참조가 매번 바뀜) 이 ref 없이는 리렌더마다 다시 불릴 수 있다.\n const dispatchedClientActionsRef = useRef<Set<string>>(new Set());\n\n // 마운트 시점의 nudgeNodeId만 쓴다 — effect의 의존성이 아니다. 대화가 이미 열려 있는데\n // 다른 넛지 힌트를 눌러 다시 열면(같은 <XbNovaChat> 인스턴스, activeNudgeNodeId만 새\n // 값으로 바뀜) 여기 걸려 있으면 대화가 통째로 리셋되고 새 conversation이 생겨버린다 —\n // 서버 쪽 규칙(\"conversation_id가 이미 있으면 nudge_node_id는 무시된다\", schema/chat.py\n // 참고)과도 안 맞는다. nudgeNodeId는 \"새 대화를 열 때만\" 의미가 있다.\n const nudgeNodeIdRef = useRef(nudgeNodeId);\n nudgeNodeIdRef.current = nudgeNodeId;\n\n useEffect(() => {\n let cancelled = false;\n setOnboarding(null);\n setMessages([]);\n setResumed(false);\n setSentOpening(null);\n setError(null);\n setClientResults({});\n setFeedbackState({});\n dispatchedClientActionsRef.current = new Set();\n conversationIdRef.current = null;\n // 인사말·시작질문은 봇 설정에서 온다. 모델이 만들지 않는다.\n // `defer`: 이어 열 대화가 없으면 서버는 대화를 만들지 않는다 — 첫 메시지(`/v1/chat`)에서 만든다.\n // 위젯을 열기만 한 방문이 빈 대화를 남기지 않게 하려는 것.\n fetch(`${apiBaseUrl}/v1/conversations`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${apiKey}` },\n body: JSON.stringify({\n bot_id: botId,\n nudge_node_id: nudgeNodeIdRef.current ?? null,\n visitor_id: userKey || null,\n thread_key: threadKey ?? null,\n session_id: sessionId,\n defer: true,\n }),\n })\n .then((r) => (r.ok ? r.json() : Promise.reject(new Error(`${r.status}`))))\n .then((data: XbNovaOnboarding) => {\n if (cancelled) return;\n // 응답이 오기 전에 사용자가 먼저 메시지를 보냈으면(`/v1/chat` 이 대화를 만들고 헤더로 id 를 줌)\n // 그 대화를 그대로 쓴다. 여기서 id·이력을 덮어쓰면 방금 주고받은 대화가 지워지고 다음 메시지가\n // 다른 대화로 갈라진다.\n if (conversationIdRef.current || messagesRef.current.length > 0) {\n setOnboarding(data);\n return;\n }\n conversationIdRef.current = data.conversation_id;\n const restored = fromStoredMessages(data.messages ?? []);\n // 이미 끝난 대화의 tool 호출은 다시 디스패치하지 않는다.\n for (const m of restored) {\n for (const p of m.parts) if (p.kind === \"tool\") dispatchedClientActionsRef.current.add(p.toolCallId);\n }\n setMessages(restored);\n setResumed(restored.length > 0);\n setOnboarding(data);\n })\n .catch((e) => {\n if (!cancelled) setError(`봇을 불러올 수 없습니다: ${e.message}`);\n });\n return () => {\n cancelled = true;\n };\n // nudgeNodeId는 의도적으로 의존성에서 뺐다 — 위 nudgeNodeIdRef 설명 참고.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [apiBaseUrl, apiKey, botId, userKey, threadKey]);\n\n const appendToLastAssistant = (existingParts: MessagePart[], newParts: MessagePart[]) =>\n setMessages((prev) => {\n const next = [...prev];\n next[next.length - 1] = { ...next[next.length - 1], role: \"assistant\", parts: [...existingParts, ...newParts] };\n return next;\n });\n\n // 마지막 assistant 메시지에 서버가 부여한 id를 붙인다(재개 스트림이면 최신 id로 갱신).\n const setLastAssistantId = (id: string) =>\n setMessages((prev) => {\n if (prev.length === 0) return prev;\n const next = [...prev];\n next[next.length - 1] = { ...next[next.length - 1], id };\n return next;\n });\n\n async function setFeedback(messageId: string, rating: XbNovaFeedbackRating, reason?: XbNovaFeedbackReason) {\n const conversationId = conversationIdRef.current;\n if (!conversationId || !messageId) return;\n const prevRating = feedbackRef.current[messageId] ?? null;\n const apply = (r: XbNovaFeedbackRating) =>\n setFeedbackState((prev) => {\n const next = { ...prev };\n if (r === null) delete next[messageId];\n else next[messageId] = r;\n return next;\n });\n apply(rating);\n try {\n const res = await fetch(\n `${apiBaseUrl}/v1/conversations/${encodeURIComponent(conversationId)}/messages/${encodeURIComponent(messageId)}/feedback`,\n {\n method: \"PUT\",\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${apiKey}` },\n body: JSON.stringify({ rating, visitor_id: visitorId, reason: rating === \"down\" ? (reason ?? null) : null }),\n },\n );\n if (!res.ok) throw new Error(`${res.status}`);\n sendEvent(\"feedback_submit\", { conversation_id: conversationId, message_id: messageId, rating, reason });\n options?.onFeedback?.({ conversationId, messageId, rating, reason });\n } catch (e) {\n console.warn(\"[xbnova] 피드백 저장 실패\", e);\n apply(prevRating);\n }\n }\n\n // client action 버튼의 노출·클릭을 nova에 남긴다(넛지 퍼널의 \"상담 버튼 노출→클릭\"). 넛지 ID·봇은 서버가\n // 대화에서 찾아 채우므로 대화 id만 있으면 된다. 분석 이벤트라 실패해도 조용히 버린다.\n function postClientActionEvent(kind: \"cta_impression\" | \"cta_click\", action: string, label: string | null) {\n const conversationId = conversationIdRef.current;\n if (!conversationId) return;\n sendEvent(kind, { conversation_id: conversationId, action, label });\n }\n\n function trackClientActionClick(action: string, label: string | null) {\n postClientActionEvent(\"cta_click\", action, label);\n }\n\n async function runClientAction(spec: ClientActionHandler, part: ToolPart, existingParts: MessagePart[]) {\n // 핸들러 실행 *전에* 닫는다 — 핸들러가 스크롤·하이라이트 같은 걸 하면, 그게 실행되는\n // 순간 시트가 이미 없어야 사용자 눈에 보인다(늦게 닫으면 애니메이션을 놓친다).\n if (spec.dismissSheet) options?.onDismissSheet?.();\n\n let result: void | string | XbNovaClientActionResult;\n try {\n if (!spec.awaitResult) {\n await spec.handler(part.input);\n return;\n }\n // 핸들러가 영영 안 끝나면 busy가 풀리지 않고 대화가 멈춘다(서버도 타임아웃이 없다).\n // 타임아웃이 먼저 정착하면 Promise.race가 거기서 끝나므로, 뒤늦은 핸들러 결과는\n // 버려지고 tool 결과가 두 번 전송되지 않는다.\n const timeoutMs = spec.timeoutMs ?? DEFAULT_CLIENT_ACTION_TIMEOUT_MS;\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timeout = new Promise<string>((resolve) => {\n timer = setTimeout(() => resolve(\"오류: 응답 시간이 초과되었습니다.\"), timeoutMs);\n });\n try {\n result = await Promise.race([Promise.resolve(spec.handler(part.input)), timeout]);\n } finally {\n clearTimeout(timer);\n }\n } catch (e) {\n if (!spec.awaitResult) return; // fire-and-forget — 실패해도 대화 자체엔 영향 없다.\n result = e instanceof Error ? `오류: ${e.message}` : \"오류가 발생했습니다.\";\n }\n if (!spec.awaitResult) return;\n\n const conversationId = conversationIdRef.current;\n if (!conversationId) return; // 이 tool을 부른 스트림의 응답 헤더로 이미 받았다 — 방어적으로만 체크한다.\n\n // 구조화된 결과면 text만 모델에 보내고, view+output은 이 tool 호출 자리에 그릴 수 있게 보관한다.\n const structured = result && typeof result === \"object\" ? result : null;\n if (structured?.view) {\n setClientResults((prev) => ({\n ...prev,\n [part.toolCallId]: { view: structured.view as ViewKind, output: structured.output },\n }));\n }\n const rawText = structured ? structured.text : result;\n const resultText = typeof rawText === \"string\" && rawText.trim() ? rawText : \"핸들러가 결과를 반환하지 않았습니다.\";\n setBusy(true);\n try {\n await resumeToolResult(\n apiBaseUrl,\n apiKey,\n {\n bot_id: botId,\n conversation_id: conversationId,\n tool_call_id: part.toolCallId,\n result_text: resultText,\n },\n {\n onPart: (newParts) => appendToLastAssistant(existingParts, newParts),\n onMessageId: setLastAssistantId,\n onError: setError,\n },\n );\n } catch (e) {\n setError(e instanceof Error ? e.message : String(e));\n } finally {\n setBusy(false);\n }\n }\n\n // client action tool 호출이 나타나면(입력이 확정되는 즉시) 디스패치한다 — output을\n // 기다리지 않는다. awaitResult tool은 애초에 output 이벤트가 안 온다(서버가 거기서\n // 턴을 멈춘다 — nova/api의 loop.py 참고). 재개 응답이 또 다른 client action을 부르면\n // 이 effect가 그 새 tool part도 그대로 다시 잡아 디스패치한다(체이닝 자연 지원).\n useEffect(() => {\n if (clientActionHandlers.size === 0) return;\n const last = messages[messages.length - 1];\n if (!last || last.role !== \"assistant\") return;\n for (const part of last.parts) {\n if (part.kind !== \"tool\" || part.state !== \"input\") continue;\n const spec = clientActionHandlers.get(part.toolName);\n if (!spec) continue;\n if (dispatchedClientActionsRef.current.has(part.toolCallId)) continue;\n dispatchedClientActionsRef.current.add(part.toolCallId);\n // 화면에 버튼을 그리는 액션만 \"노출\"이다. 이어 연 대화의 지난 호출은 위에서 디스패치 목록에 미리\n // 넣어 두므로 여기 오지 않는다 — 다시 열 때마다 노출이 중복으로 쌓이지 않는다.\n if (spec.render) postClientActionEvent(\"cta_impression\", part.toolName, null);\n void runClientAction(spec, part, last.parts);\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [messages, clientActionHandlers, onboarding, apiBaseUrl, apiKey, botId]);\n\n async function send(text: string, nudgeOpening?: string | null) {\n const trimmed = text.trim();\n if (!trimmed || busy) return;\n // 내용이 아니라 길이만 보낸다 — 대화 원문은 분석 이벤트로 나가지 않는다.\n sendEvent(\"message_send\", {\n conversation_id: conversationIdRef.current,\n is_first: messages.length === 0,\n length: trimmed.length,\n });\n setError(null);\n setBusy(true);\n // 대화 중 넛지 질문에 답하면 질문을 이력에 남긴다. 서버도 `opening_message`로 받아 넛지 문구인지 확인한 뒤\n // 저장한다 — 그래야 AI가 \"응 해줘\"가 무엇에 대한 답인지 안다.\n const resumedOpening = messages.length > 0 && nudgeOpening ? nudgeOpening : null;\n setMessages((prev) => [\n ...prev,\n ...(resumedOpening\n ? [{ role: \"assistant\" as const, parts: [{ kind: \"text\" as const, id: `nudge:${prev.length}`, text: resumedOpening }] }]\n : []),\n { role: \"user\", parts: [{ kind: \"text\", id: \"u\", text: trimmed }] },\n { role: \"assistant\", parts: [] },\n ]);\n\n const replaceLast = (parts: MessagePart[]) =>\n setMessages((prev) => {\n const next = [...prev];\n next[next.length - 1] = { ...next[next.length - 1], role: \"assistant\", parts };\n return next;\n });\n\n const focused = getFocusedTrackEntry(trackRegistry);\n const notes = collectContextEntries(contextRegistry);\n const screen = focused ? { region: focused.region, data: resolveFactValue(focused.data) } : {};\n // `useXbNovaContext`로 등록된 부가 설명은 포커스된 영역이 없어도 실린다.\n const context = focused || notes.length ? { ...screen, ...(notes.length ? { notes } : {}) } : undefined;\n\n // 첫 메시지면 맨 위 말풍선을, 대화 중이면 답한 넛지 질문을 함께 보낸다.\n let openingMessage: string | null;\n if (messages.length === 0) {\n openingMessage = (options?.openingMessage ?? onboarding?.greeting) || null;\n setSentOpening(openingMessage);\n } else {\n openingMessage = resumedOpening;\n }\n\n try {\n await streamChat(\n apiBaseUrl,\n apiKey,\n {\n bot_id: botId,\n message: trimmed,\n conversation_id: conversationIdRef.current,\n context,\n nudge_node_id: nudgeNodeId ?? null,\n opening_message: openingMessage,\n visitor_id: userKey || null,\n thread_key: threadKey ?? null,\n session_id: sessionId,\n },\n {\n onPart: replaceLast,\n onMessageId: setLastAssistantId,\n onConversationId: (id) => {\n conversationIdRef.current = id;\n },\n onError: setError,\n },\n );\n } catch (e) {\n setError(e instanceof Error ? e.message : String(e));\n } finally {\n setBusy(false);\n }\n }\n\n // 대화가 시작되면 맨 위 말풍선은 그때 보낸 문구로 고정한다 — 뒤에 누른 넛지가 대화 위에 나타나면 안 된다.\n const opening = resumed\n ? null\n : messages.length > 0\n ? sentOpening\n : (options?.openingMessage ?? onboarding?.greeting ?? null);\n\n return {\n onboarding,\n opening,\n messages,\n busy,\n error,\n send,\n clientResults,\n feedback,\n setFeedback,\n trackClientActionClick,\n };\n}\n","/**\n * AI SDK UI Message Stream v1 파서. `nova/web/lib/chat-stream.ts`에서 그대로 옮겼다 — 이미\n * 콘솔에서 검증된 로직이라 새로 설계하지 않는다.\n *\n * `ai` 패키지에 의존하지 않는다 — 고객사 번들에 peer 버전 충돌을 만들지 않기 위해서다.\n */\n\nexport type ViewKind = \"product_list\" | \"product_detail\" | \"metric\" | \"choices\" | \"form\" | \"verdict\";\n\nexport type ToolPart = {\n kind: \"tool\";\n toolCallId: string;\n toolName: string;\n state: \"input\" | \"output\" | \"error\";\n input?: unknown;\n output?: unknown;\n /** 위젯은 tool 이름이 아니라 이 값으로 렌더러를 고른다. */\n view?: ViewKind | null;\n};\n\nexport type TextPart = { kind: \"text\"; id: string; text: string };\nexport type MessagePart = TextPart | ToolPart;\n\nexport type ChatMessage = {\n /** assistant 메시지가 서버에 저장될 때의 id(`start` 청크의 `messageId`). 만족도 피드백 대상 식별용. */\n id?: string;\n role: \"user\" | \"assistant\";\n parts: MessagePart[];\n};\n\ntype Handlers = {\n onPart: (parts: MessagePart[]) => void;\n /** `start` 청크로 받은 assistant messageId. 재개 스트림이면 최신 값으로 다시 불린다. */\n onMessageId?: (messageId: string) => void;\n /** 응답 헤더(`x-conversation-id`)의 대화 id. 본문보다 먼저 불린다 — 대화를 첫 메시지에서 만드는\n * 지연 생성이면 이 스트림 안에서 디스패치되는 client action이 이 id로 결과를 돌려보내야 한다. */\n onConversationId?: (conversationId: string) => void;\n onError?: (message: string) => void;\n onFinish?: () => void;\n};\n\n/** 서버에 저장된 메시지(`parts`는 AI SDK UIMessage.parts 원본) — 이어 연 대화의 이력. */\nexport type StoredMessage = {\n id: string;\n role: \"user\" | \"assistant\";\n sender?: string;\n parts: Record<string, unknown>[];\n};\n\n/** 저장된 parts → 위젯이 그리는 parts. 이미 끝난 대화라 tool은 결과가 있든 없든 \"끝남\"으로 둔다 —\n * `input`으로 두면 client action이 다시 디스패치되거나 진행 중 표시가 남는다. */\nexport function fromStoredMessages(stored: StoredMessage[]): ChatMessage[] {\n return stored.map((m) => ({\n id: m.role === \"assistant\" ? m.id : undefined,\n role: m.role === \"assistant\" ? \"assistant\" : \"user\",\n parts: m.parts.flatMap((p, i): MessagePart[] => {\n const type = String(p.type ?? \"\");\n if (type === \"text\") {\n const text = typeof p.text === \"string\" ? p.text : \"\";\n return text ? [{ kind: \"text\", id: `${m.id}:${i}`, text }] : [];\n }\n if (type.startsWith(\"tool-\")) {\n const meta = p.providerMetadata as { \"kr.dsauto.chat\"?: { view?: ViewKind | null } } | undefined;\n return [\n {\n kind: \"tool\",\n toolCallId: String(p.toolCallId ?? `${m.id}:${i}`),\n toolName: type.slice(\"tool-\".length),\n state: p.state === \"output-error\" ? \"error\" : \"output\",\n input: p.input,\n output: p.output,\n view: meta?.[\"kr.dsauto.chat\"]?.view ?? null,\n },\n ];\n }\n return [];\n }),\n }));\n}\n\n/** SSE 한 줄(`data: {...}`)을 파싱해 누적 중인 `parts`를 갱신한다. `streamChat`(새 질문)과\n * `resumeToolResult`(client action await_result 재개)가 같은 와이어 포맷(`tool-output-available`\n * 등)을 받으므로 이 루프를 공유한다 — 둘의 차이는 어느 URL/body로 fetch하느냐뿐이다. */\nasync function consumeSse(response: Response, handlers: Handlers): Promise<string | null> {\n if (!response.ok || !response.body) {\n handlers.onError?.(`요청 실패 (${response.status})`);\n return null;\n }\n const conversationId = response.headers.get(\"x-conversation-id\");\n if (conversationId) handlers.onConversationId?.(conversationId);\n\n const reader = response.body.getReader();\n const decoder = new TextDecoder();\n let buffer = \"\";\n\n // 순서를 보존하려고 배열로 들고, 갱신할 때 참조로 찾는다.\n const parts: MessagePart[] = [];\n const textIndex = new Map<string, number>();\n const toolIndex = new Map<string, number>();\n\n const emit = () => handlers.onPart([...parts]);\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value, { stream: true });\n\n const lines = buffer.split(\"\\n\\n\");\n buffer = lines.pop() ?? \"\";\n\n for (const chunk of lines) {\n const line = chunk.trim();\n if (!line.startsWith(\"data:\")) continue;\n const payload = line.slice(5).trim();\n if (payload === \"[DONE]\") continue;\n\n let event: Record<string, unknown>;\n try {\n event = JSON.parse(payload);\n } catch {\n continue;\n }\n\n switch (event.type) {\n case \"start\":\n if (typeof event.messageId === \"string\" && event.messageId) handlers.onMessageId?.(event.messageId);\n break;\n case \"text-start\": {\n const id = String(event.id);\n textIndex.set(id, parts.length);\n parts.push({ kind: \"text\", id, text: \"\" });\n emit();\n break;\n }\n case \"text-delta\": {\n const id = String(event.id);\n let at = textIndex.get(id);\n if (at === undefined) {\n at = parts.length;\n textIndex.set(id, at);\n parts.push({ kind: \"text\", id, text: \"\" });\n }\n (parts[at] as TextPart).text += String(event.delta ?? \"\");\n emit();\n break;\n }\n case \"tool-input-available\": {\n const id = String(event.toolCallId);\n toolIndex.set(id, parts.length);\n parts.push({\n kind: \"tool\",\n toolCallId: id,\n toolName: String(event.toolName),\n state: \"input\",\n input: event.input,\n });\n emit();\n break;\n }\n case \"tool-output-available\": {\n const id = String(event.toolCallId);\n const at = toolIndex.get(id);\n if (at !== undefined) {\n const meta = event.providerMetadata as { \"kr.dsauto.chat\"?: { view?: ViewKind } };\n const part = parts[at] as ToolPart;\n part.state = event.isError ? \"error\" : \"output\";\n part.output = event.output;\n part.view = meta?.[\"kr.dsauto.chat\"]?.view ?? null;\n emit();\n }\n break;\n }\n case \"error\":\n handlers.onError?.(String(event.errorText ?? \"알 수 없는 오류\"));\n break;\n case \"finish\":\n handlers.onFinish?.();\n break;\n }\n }\n }\n return conversationId;\n}\n\nexport async function streamChat(\n apiBaseUrl: string,\n apiKey: string,\n body: {\n bot_id: string;\n message: string;\n conversation_id?: string | null;\n context?: unknown;\n nudge_node_id?: string | null;\n opening_message?: string | null;\n visitor_id?: string | null;\n thread_key?: string | null;\n session_id?: string | null;\n },\n handlers: Handlers,\n): Promise<string | null> {\n const response = await fetch(`${apiBaseUrl}/v1/chat`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${apiKey}` },\n body: JSON.stringify(body),\n });\n return consumeSse(response, handlers);\n}\n\n/** client action(`awaitResult: true`)이 멈춰둔 턴을, 핸들러가 실제로 조회한 결과를 들고\n * 재개한다. 서버가 `tool_call_id`로 저장해둔 pending tool 호출을 찾아 이어가므로\n * `tool_name`/`input`은 안 보낸다(SDK.md의 \"클라이언트가 스펙의 진실 원천\"이라는 것과는\n * 별개로, 결과 자체를 클라이언트가 주장하게 두지 않기 위한 서버 쪽 안전장치). */\nexport async function resumeToolResult(\n apiBaseUrl: string,\n apiKey: string,\n body: {\n bot_id: string;\n conversation_id: string;\n tool_call_id: string;\n result_text: string;\n is_error?: boolean;\n },\n handlers: Handlers,\n): Promise<string | null> {\n const response = await fetch(`${apiBaseUrl}/v1/conversations/${body.conversation_id}/tool-results`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${apiKey}` },\n body: JSON.stringify(body),\n });\n return consumeSse(response, handlers);\n}\n","\"use client\";\n\n/**\n * view 렌더러. `nova/web/components/views.tsx`에서 그대로 옮겼다.\n *\n * 중요: 디스패치는 **tool 이름이 아니라 `view`** 로 한다. 이 규칙을 어기면 렌더러가\n * 고객사 tool 이름을 알아야 하고, 그 순간 \"고객사가 달라지면 MCP만 개발\" 이 깨진다.\n */\n\nimport type { ToolPart, ViewKind } from \"./stream\";\n\nexport type ProductItem = {\n id: string;\n title: string;\n subtitle?: string;\n price?: { amount: number | null; currency: string };\n imageUrl?: string | null;\n badges?: string[];\n attributes?: { label: string; value: string }[];\n href?: string | null;\n description?: string | null;\n};\n\nexport type VerdictAction = {\n label: string;\n /** primary(기본, 채운 버튼) | ghost(테두리만) — 기획서 4.2의 [상담신청]/[성능점검 보기] 처럼 하나는 강조, 나머지는 보조로 두고 싶을 때 쓴다. */\n kind?: \"primary\" | \"ghost\";\n /** 카통령 등 소비자 쪽 동작을 가리키는 자유 문자열 — SDK는 뜻을 모른다, `onVerdictAction`으로 그대로 넘긴다. */\n action: string;\n};\n\nexport type VerdictOutput = {\n /** 카드 상단 라벨 — 기본 \"조건부 검토\". */\n headline?: string;\n conclusion: string;\n evidence: string;\n /** 데이터로 답할 수 없는 것 — 기획서 4.2 ③: 비워두지 말고 최소 1개 이상 밝히도록 유도한다. */\n unknowns?: string[];\n actions?: VerdictAction[];\n};\n\nexport type ViewRenderer = (props: {\n output: unknown;\n onVerdictAction?: (action: string) => void;\n}) => React.ReactNode;\nexport type ViewRendererMap = Partial<Record<ViewKind, ViewRenderer>>;\n\nfunction formatPrice(price?: { amount: number | null }) {\n if (!price?.amount) return \"가격문의\";\n return `${(price.amount / 10000).toLocaleString()}만원`;\n}\n\nfunction ProductCard({ item, detailed = false }: { item: ProductItem; detailed?: boolean }) {\n return (\n <a\n href={item.href ?? undefined}\n target=\"_blank\"\n rel=\"noreferrer\"\n className=\"flex w-64 shrink-0 flex-col overflow-hidden rounded-xl border border-neutral-200 bg-white transition hover:shadow-md dark:border-neutral-700 dark:bg-neutral-900\"\n >\n {item.imageUrl ? (\n // eslint-disable-next-line @next/next/no-img-element\n <img src={item.imageUrl} alt={item.title} className=\"h-36 w-full object-cover\" />\n ) : (\n <div className=\"flex h-36 w-full items-center justify-center bg-neutral-100 text-xs text-neutral-400 dark:bg-neutral-800\">\n 이미지 없음\n </div>\n )}\n <div className=\"flex flex-1 flex-col gap-1 p-3\">\n <div className=\"text-sm font-semibold\">{item.title}</div>\n <div className=\"text-xs text-neutral-500\">{item.subtitle}</div>\n <div className=\"xbnova-accent-price mt-1 text-base font-bold\">{formatPrice(item.price)}</div>\n {item.badges?.length ? (\n <div className=\"mt-1 flex flex-wrap gap-1\">\n {item.badges.map((b) => (\n <span\n key={b}\n className=\"rounded bg-neutral-100 px-1.5 py-0.5 text-[11px] text-neutral-600 dark:bg-neutral-800 dark:text-neutral-300\"\n >\n {b}\n </span>\n ))}\n </div>\n ) : null}\n {detailed && item.attributes?.length ? (\n <dl className=\"mt-2 grid grid-cols-2 gap-x-2 gap-y-1 text-[11px]\">\n {item.attributes.map((a) => (\n <div key={a.label} className=\"flex gap-1\">\n <dt className=\"text-neutral-400\">{a.label}</dt>\n <dd className=\"text-neutral-700 dark:text-neutral-300\">{a.value}</dd>\n </div>\n ))}\n </dl>\n ) : null}\n </div>\n </a>\n );\n}\n\nfunction ProductList({ output }: { output: unknown }) {\n const data = output as { items?: ProductItem[]; total?: number } | null;\n if (!data?.items?.length) return <Fallback label=\"검색 결과 없음\" />;\n return (\n <div className=\"my-2\">\n <div className=\"mb-1 text-xs text-neutral-500\">총 {data.total?.toLocaleString()}건</div>\n <div className=\"flex gap-2 overflow-x-auto pb-2\">\n {data.items.map((item) => (\n <ProductCard key={item.id} item={item} />\n ))}\n </div>\n </div>\n );\n}\n\nfunction ProductDetail({ output }: { output: unknown }) {\n const data = output as { item?: ProductItem } | null;\n if (!data?.item) return <Fallback label=\"매물 없음\" />;\n return (\n <div className=\"my-2\">\n <ProductCard item={data.item} detailed />\n </div>\n );\n}\n\n/** 판단형 질문에 대한 답변의 고정 구조(기획서 4.2) — 결론 → 근거 → 확인 불가 → 다음 행동.\n * 실제 데이터는 tool이 채운다(SDK는 렌더만 한다) — 이 view를 실제로 채우는 tool은 아직\n * 고객사마다 따로 만들어야 한다. */\nfunction Verdict({\n output,\n onVerdictAction,\n}: {\n output: unknown;\n onVerdictAction?: (action: string) => void;\n}) {\n const data = output as VerdictOutput | null;\n if (!data?.conclusion) return <Fallback label=\"판단 결과 없음\" />;\n return (\n <div className=\"my-2 max-w-[92%] overflow-hidden rounded-xl border border-amber-200 bg-white text-xs dark:border-amber-900 dark:bg-neutral-900\">\n <div className=\"border-b border-amber-200 bg-amber-50 px-3 py-1.5 font-bold text-amber-800 dark:border-amber-900 dark:bg-amber-950 dark:text-amber-300\">\n △ {data.headline ?? \"조건부 검토\"}\n </div>\n <div className=\"flex gap-2 border-b border-neutral-100 px-3 py-1.5 dark:border-neutral-800\">\n <span className=\"w-10 shrink-0 pt-px font-semibold text-neutral-400\">결론</span>\n <span className=\"text-neutral-800 dark:text-neutral-100\">{data.conclusion}</span>\n </div>\n <div className=\"flex gap-2 border-b border-neutral-100 px-3 py-1.5 dark:border-neutral-800\">\n <span className=\"w-10 shrink-0 pt-px font-semibold text-neutral-400\">근거</span>\n <span className=\"text-neutral-600 dark:text-neutral-300\">{data.evidence}</span>\n </div>\n {data.unknowns?.length ? (\n <div className=\"flex gap-2 border-b border-neutral-100 px-3 py-1.5 dark:border-neutral-800\">\n <span className=\"w-10 shrink-0 pt-px font-semibold text-neutral-400\">확인 불가</span>\n <span className=\"text-neutral-500\">{data.unknowns.join(\" · \")}</span>\n </div>\n ) : null}\n {data.actions?.length ? (\n <div className=\"flex flex-wrap gap-1.5 p-2\">\n {data.actions.map((action) => (\n <button\n key={action.label}\n type=\"button\"\n onClick={() => onVerdictAction?.(action.action)}\n className={\n action.kind === \"ghost\"\n ? \"xbnova-accent-btn-ghost rounded-md border px-2.5 py-1 text-[11px] font-semibold\"\n : \"xbnova-accent-btn rounded-md px-2.5 py-1 text-[11px] font-semibold text-white\"\n }\n >\n {action.label}\n </button>\n ))}\n </div>\n ) : null}\n </div>\n );\n}\n\nexport function Fallback({ label }: { label: string }) {\n return (\n <div className=\"my-2 rounded-lg border border-dashed border-neutral-300 px-3 py-2 text-xs text-neutral-500 dark:border-neutral-700\">\n {label}\n </div>\n );\n}\n\nconst DEFAULT_RENDERERS: ViewRendererMap = {\n product_list: ProductList,\n product_detail: ProductDetail,\n verdict: Verdict,\n};\n\nexport function ToolView({\n part,\n viewRenderers,\n onVerdictAction,\n}: {\n part: ToolPart;\n viewRenderers?: ViewRendererMap;\n onVerdictAction?: (action: string) => void;\n}) {\n if (part.state === \"input\") {\n return <Fallback label={`${part.toolName} 조회 중…`} />;\n }\n if (part.state === \"error\") {\n return <Fallback label={`${part.toolName} 실패`} />;\n }\n const Renderer = part.view ? (viewRenderers?.[part.view] ?? DEFAULT_RENDERERS[part.view]) : undefined;\n if (!Renderer) {\n // 모르는 view는 조용히 접어둔다 — 표준 MCP 서버를 그냥 붙여도 대화는 동작해야 한다.\n return <Fallback label={`${part.toolName} 결과 (view: ${part.view ?? \"없음\"})`} />;\n }\n return <Renderer output={part.output} onVerdictAction={onVerdictAction} />;\n}\n","\"use client\";\n\n/**\n * 넛지 조건이 참조하는 \"facts\" 중 커스텀 값 등록 훅. 내장 facts(scroll_depth 등)는\n * `built-in-facts.ts`가 따로 계산한다 — 여기는 고객사가 아는 값만 다룬다.\n */\n\nimport { useEffect, useId, useRef } from \"react\";\n\nimport { type FactValue, type XbNovaContextEntry, useXbNovaInternal } from \"./internal-context\";\n\n/** 커스텀 값 하나를 조건에서 쓸 수 있게 등록한다. 값 또는 함수(pull) 둘 다 된다. */\nexport function useXbNovaFact(key: string, value: FactValue): void {\n const { factsRegistry } = useXbNovaInternal();\n\n useEffect(() => {\n factsRegistry.set(key, value);\n });\n\n useEffect(() => {\n return () => {\n factsRegistry.delete(key);\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [key, factsRegistry]);\n}\n\n/** 필드가 여러 개인 객체 하나를 한 번에 등록한다 — 매물 데이터처럼. `useXbNovaFact`를\n * 필드마다 반복하는 것과 같다. */\nexport function useXbNovaFacts(values: Record<string, FactValue>): void {\n const { factsRegistry } = useXbNovaInternal();\n const keysRef = useRef<string[]>([]);\n\n useEffect(() => {\n for (const [key, value] of Object.entries(values)) {\n factsRegistry.set(key, value);\n }\n keysRef.current = Object.keys(values);\n });\n\n useEffect(() => {\n return () => {\n for (const key of keysRef.current) {\n factsRegistry.delete(key);\n }\n };\n }, [factsRegistry]);\n}\n\n/** AI에게 알려줄 상품·화면 부가 설명을 등록한다. 넛지 조건용인 facts와 달리 **챗 요청의\n * `context`로 전송**되어 답변의 근거가 된다. 내용(가입 가능 여부 같은 판단 포함)은 호스트가\n * `description`에 문장으로 쓴다 — SDK는 형식 검증과 전달만 한다. 배열 또는 함수(pull) 둘 다\n * 된다. 컴포넌트가 언마운트되면 자동으로 빠진다. */\nexport function useXbNovaContext(entries: XbNovaContextEntry[] | (() => XbNovaContextEntry[])): void {\n const { contextRegistry } = useXbNovaInternal();\n const id = useId();\n\n useEffect(() => {\n contextRegistry.set(id, entries);\n });\n\n useEffect(() => {\n return () => {\n contextRegistry.delete(id);\n };\n }, [id, contextRegistry]);\n}\n\n/** 방문자 식별자 — 없으면 SDK가 즉시 만들어 localStorage에 고정한 뒤 그 값을 반환한다. */\nexport function useXbNovaVisitor(): string {\n const { visitorId } = useXbNovaInternal();\n return visitorId;\n}\n\n/** 선언형 — 서버 컴포넌트 트리에서 훅을 못 쓸 때. `<XbNovaFact id=\"xbnova_summary\" value=\"...\" />`. */\nexport function XbNovaFact({ id, value }: { id: string; value: FactValue }): null {\n useXbNovaFact(id, value);\n return null;\n}\n\n/** 선언형 — `useXbNovaFacts`의 컴포넌트 버전. */\nexport function XbNovaFacts({ values }: { values: Record<string, FactValue> }): null {\n useXbNovaFacts(values);\n return null;\n}\n"],"mappings":"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","names":["import_react","import_react","import_react","import_jsx_runtime","import_react","import_react","import_jsx_runtime","entry","import_jsx_runtime","import_react","import_react","import_jsx_runtime","style","import_react","import_jsx_runtime","import_jsx_runtime","ReactMarkdown","remarkGfm","remarkBreaks","import_react"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/provider.tsx","../src/client-actions.tsx","../src/internal-context.tsx","../src/built-in-facts.ts","../src/nudge-context.tsx","../src/nudge-loop.tsx","../src/track.tsx","../src/chat.tsx","../src/shadow-root.tsx","../src/use-chat.ts","../src/stream.ts","../src/views.tsx","../src/facts.ts"],"sourcesContent":["/**\n * `@xbrain-swe/nova-chat-widget` 공개 API. 여기서 안 내보내는 건 전부 내부 구현이다(`nova/docs/SDK.md`\n * 참고) — 모든 공개 이름은 `XbNova`/`useXbNova` 접두사를 붙인다.\n */\n\nexport { XbNovaProvider } from \"./provider\";\nexport { XbNovaChat, type XbNovaOpenEvent } from \"./chat\";\nexport {\n type XbNovaClientAction,\n type XbNovaClientActions,\n type XbNovaClientActionResult,\n} from \"./client-actions\";\nexport {\n useXbNovaChat,\n type UseXbNovaChatResult,\n type XbNovaOnboarding,\n type XbNovaFeedbackEvent,\n type XbNovaFeedbackRating,\n type XbNovaFeedbackReason,\n} from \"./use-chat\";\nexport { XbNovaTrack, useXbNovaTrack } from \"./track\";\nexport { useXbNovaFact, useXbNovaFacts, useXbNovaContext, useXbNovaVisitor, XbNovaFact, XbNovaFacts } from \"./facts\";\nexport { trackXbNovaEvent } from \"./nudge-loop\";\nexport { useXbNovaSettings } from \"./nudge-context\";\nexport type { ViewKind, ChatMessage, MessagePart, TextPart, ToolPart } from \"./stream\";\nexport type { ViewRenderer, ViewRendererMap, ProductItem, VerdictOutput, VerdictAction } from \"./views\";\nexport type { FactValue, XbNovaContextEntry, XbNovaTheme } from \"./internal-context\";\n","\"use client\";\n\n/**\n * 앱 루트에서 한 번 초기화 — `useXbNovaChat`/`<XbNovaChat>`/`XbNovaTrack`/넛지 루프가 전부\n * 여기서 인증 정보를 읽는다. 넛지 평가는 `<XbNovaChat>`과 무관하게 Provider가 직접 돌린다\n * (\"설치만 하면 채팅·동행 바가 다 도는 게 기본값\" — SDK.md). 실제 동행 바(idle/hint/sheet)\n * UI는 `<XbNovaChat>` 하나가 그린다 — 루프는 `nudge-context`에 힌트만 밀어넣는다.\n *\n * SDK.md 예시엔 없던 `appId`가 필수 prop이다: `GET /v1/apps/{app_id}/nudge-graph`,\n * `POST /v1/events` 둘 다 App 스코프 엔드포인트라 app_id가 URL/바디에 그대로\n * 필요한데, apiKey는 해시로 저장돼 있어 브라우저에서 역산할 방법이 없다 — 명시적으로\n * 받는다.\n */\n\nimport { useEffect, useMemo } from \"react\";\n\nimport { type XbNovaClientActions, XbNovaClientActionSync } from \"./client-actions\";\nimport { DEFAULT_THEME, type Auth, InternalProvider, type XbNovaTheme } from \"./internal-context\";\nimport { markSessionStart, bumpVisitCount } from \"./built-in-facts\";\nimport { NudgeStateProvider } from \"./nudge-context\";\nimport { XbNovaNudgeLoop } from \"./nudge-loop\";\n\nconst DEFAULT_API_BASE_URL = \"http://localhost:10100\";\n\nexport function XbNovaProvider({\n apiKey,\n botId,\n appId,\n screen,\n apiBaseUrl = DEFAULT_API_BASE_URL,\n clientActions,\n theme,\n userKey,\n threadKey,\n sessionKey,\n eventProperties,\n children,\n}: {\n apiKey: string;\n botId: string;\n appId: string;\n screen?: string;\n apiBaseUrl?: string;\n /** 이 방문자를 가리키는 호스트 쪽 키(예: GA4 user_pseudo_id). nova가 남기는 방문자 ID(대화·넛지\n * 이벤트·피드백) 전부가 이 값이 되고, `threadKey`와 함께 이전 대화를 찾는 데도 쓴다. 공개 키로 조회하므로 추측하기 어려운 값이어야 한다 — 회원 ID를 그대로\n * 넘기지 않는다. 안 주면 SDK가 브라우저에 만들어 둔 `visitorId`를 쓴다. */\n userKey?: string;\n /** 대화를 이어 붙일 단위(예: `vehicle:${매물ID}`). 같은 유저 키·스레드 키로 위젯이 다시 열리면\n * 이전 대화와 이력을 복원한다. 안 주면 열 때마다 새 대화다. */\n threadKey?: string;\n /** 이 방문의 세션 키(예: GA4 session_id). nova가 남기는 이벤트(위젯 노출·넛지·상담 버튼)와 대화에 실려\n * 콘솔에서 세션 단위로 집계된다. 값이 바뀌면(세션 갱신) 그 뒤 이벤트부터 새 값으로 실린다. 안 주면 세션은 비운다. */\n sessionKey?: string;\n /** 모든 분석 이벤트에 함께 실을 값(예: `{ vehicle_id: data.id }`). 평평한 원시값만 넣고 개인정보는 넣지 않는다. */\n eventProperties?: Record<string, string | number | boolean | null | undefined>;\n /** 이 페이지가 AI에게 내어줄 client action tool — 이름별로 설명·스키마·실행 함수를\n * 등록한다(\"SDK 초기화 시점에 등록\"). 스펙은 App 레벨 레지스트리로 자동 동기화되고\n * (`XbNovaClientActionSync`), 콘솔의 tool 목록에 반영된다. 핸들러는 여기서만 산다. */\n clientActions?: XbNovaClientActions;\n /** 위젯 아바타·pill 배경에 쓰는 브랜드 컬러(그라디언트 시작/끝). 안 주면 SDK 기본\n * 톤(인디고→바이올렛)을 쓴다 — 소비자 사이트 톤에 안 맞으면 여기서 자기 브랜드 컬러로\n * 바꾸면 된다(예: `{accentFrom: \"#BD1B21\", accentTo: \"#6B1317\"}`). */\n theme?: Partial<XbNovaTheme>;\n children: React.ReactNode;\n}) {\n useEffect(() => {\n if (apiKey.startsWith(\"sk_\")) {\n console.error(\n \"[XbNova] sk_ 키를 브라우저에 노출했습니다 — pk_(publishable) 키를 쓰세요. sk_는 서버 전용입니다.\",\n );\n }\n }, [apiKey]);\n\n useEffect(() => {\n markSessionStart();\n }, []);\n\n useEffect(() => {\n if (screen) bumpVisitCount(screen);\n }, [screen]);\n\n // 호스트가 객체를 인라인으로 넘겨도 값이 같으면 auth 를 새로 만들지 않는다.\n const eventPropertiesJson = JSON.stringify(eventProperties ?? null);\n const auth = useMemo<Auth>(\n () => ({\n apiKey,\n botId,\n appId,\n apiBaseUrl,\n screen,\n userKey: userKey || undefined,\n threadKey: threadKey || undefined,\n sessionKey: sessionKey || undefined,\n eventProperties: JSON.parse(eventPropertiesJson) ?? undefined,\n }),\n [apiKey, botId, appId, apiBaseUrl, screen, userKey, threadKey, sessionKey, eventPropertiesJson],\n );\n\n const resolvedTheme = useMemo<XbNovaTheme>(\n () => ({ ...DEFAULT_THEME, ...theme }),\n [theme],\n );\n\n // 핸들러+awaitResult만 뽑아 Map으로 — internal-context는 이 Map을 그대로 들고 있다가\n // use-chat.ts가 tool 이름으로 찾아 디스패치한다. 스펙(설명·스키마) 쪽은 핸들러와 별개로\n // XbNovaClientActionSync가 서버에 동기화한다.\n const clientActionHandlers = useMemo(() => {\n if (!clientActions) return undefined;\n return new Map(\n Object.entries(clientActions).map(([name, spec]) => [\n name,\n {\n handler: spec.handler,\n awaitResult: spec.awaitResult ?? false,\n timeoutMs: spec.timeoutMs,\n dismissSheet: spec.dismissSheet ?? false,\n render: spec.render,\n },\n ]),\n );\n }, [clientActions]);\n\n return (\n <InternalProvider auth={auth} clientActionHandlers={clientActionHandlers} theme={resolvedTheme}>\n <NudgeStateProvider>\n <XbNovaNudgeLoop appId={appId} />\n <XbNovaClientActionSync actions={clientActions} />\n {children}\n </NudgeStateProvider>\n </InternalProvider>\n );\n}\n","\"use client\";\n\n/**\n * client action tool — AI가 부르면 MCP 서버가 아니라 이 SDK를 설치한 호스트 페이지가 직접\n * 실행하는 tool(`nova/api`의 `ClientActionTool`과 같은 개념, `chat.tsx` 상단 doc 참고).\n *\n * 스펙(설명·입력 스키마)의 진실의 원천은 클라이언트다 — 이 페이지가 실제로 뭘 할 수\n * 있는지(어떤 region이 있는지, payload 모양이 뭔지)는 콘솔이 아니라 이 코드가 제일\n * 정확히 안다. 그래서 `<XbNovaProvider clientActions={{...}}>`로 등록하면, 여기(SDK)가\n * App 레벨 레지스트리로 스펙을 동기화하고(`XbNovaClientActionSync`), 콘솔은 그걸\n * 읽기 전용으로 보여준다 — 관리자가 스키마를 손으로 타이핑하다 페이지 구조와 어긋나는\n * 걸 막는다. 핸들러 함수 자체는 직렬화할 수 없어서 동기화 대상이 아니다 — 이 프로세스\n * 안에서만 산다(`internal-context`의 `clientActionHandlers`로 `chat.tsx`가 디스패치).\n */\n\nimport { useEffect, useRef } from \"react\";\n\nimport { useXbNovaInternal } from \"./internal-context\";\nimport type { ViewKind } from \"./stream\";\n\n/** `awaitResult` 핸들러가 구조화된 결과를 돌려줄 때의 모양. `text`는 모델이 읽는 결과(서버로\n * 전송)고, `view`+`output`은 채팅 말풍선에 직접 그릴 정규화된 데이터다 — 서버에는 가지 않는다.\n * 렌더러는 tool 이름이 아니라 `view`로 고르므로(`views.tsx`) 고객사가 UI를 만들 필요가 없다.\n * 예: 상품 검색 결과는 `view:\"product_list\"`, `output:{ items: ProductItem[], total }`.\n * MCP 서버를 쓸 수 없는 환경에서 MCP tool 결과의 `_meta` view와 같은 효과를 내는 대체 경로다.\n * 대화를 새로고침하면 이 그림은 사라진다(서버에는 `text`만 남는다). */\nexport type XbNovaClientActionResult = {\n text: string;\n view?: ViewKind;\n output?: unknown;\n};\n\nexport type XbNovaClientAction = {\n /** 모델이 \"언제 이 tool을 부를지\" 판단하는 근거 — MCP tool description과 같은 역할. */\n description: string;\n /** JSON Schema. 안 주면 인자 없는 tool로 노출된다. */\n inputSchema?: Record<string, unknown>;\n /** `awaitResult`가 false(기본, fire-and-forget)일 때만 쓴다 — 모델에게 되돌려줄 순수\n * 텍스트 템플릿, `{필드명}`으로 이 tool의 input을 치환한다(예:\n * `\"{region} 섹션을 화면에 표시했습니다.\"`). 안 주면 서버 기본값(\"완료했습니다.\")을 쓴다. */\n resultText?: string;\n /** true면 이건 \"보여주기\"가 아니라 \"진짜 데이터를 조회해오는\" tool이다 — `handler`가\n * 반환한 값이 실제 tool 결과로 서버에 다시 전송되고(`POST\n * /v1/conversations/{id}/tool-results`), AI는 그 데이터로 이어서 대화한다(LangGraph의\n * interrupt/resume과 같은 패턴). false(기본)면 `handler`는 그냥 실행되고 끝 —\n * 반환값은 안 쓴다. */\n awaitResult?: boolean;\n /** `awaitResult:true`일 때 핸들러 응답을 기다리는 최대 시간(ms, 기본 15000). 넘으면 SDK가\n * \"오류: 응답 시간이 초과되었습니다.\"를 tool 결과로 서버에 보내 모델이 대화를 이어가게\n * 한다 — 이후 핸들러가 뒤늦게 resolve돼도 그 값은 무시된다. fire-and-forget에는 적용 안 된다. */\n timeoutMs?: number;\n /** 실제로 실행할 동작 — 채팅에는 아무것도 안 그리고 이 함수만 호출된다. `payload`는 이\n * tool 호출의 input을 그대로 되돌려준다. `awaitResult:true`면 반환값(또는 resolve된\n * Promise)이 진짜 tool 결과 텍스트가 된다 — 비워두면 \"핸들러가 결과를 반환하지\n * 않았습니다\" 에러로 처리된다. */\n handler: (\n payload: unknown,\n ) => void | string | XbNovaClientActionResult | Promise<void | string | XbNovaClientActionResult>;\n /** true면 이 tool이 실행되는 시점에 대화 시트를 닫는다(대화는 안 사라진다, 나중에\n * 다시 열면 이어짐). `highlight_region`처럼 \"페이지의 특정 부분을 보여주는\" 액션에\n * 필요하다 — 모바일에서는 시트가 화면 대부분(위 7rem 제외)을 덮어서, 시트를 안 닫으면\n * 스크롤·하이라이트가 그 밑에 가려 사용자 눈에 전혀 안 보인다. */\n dismissSheet?: boolean;\n /** 주면 이 tool 호출을 채팅 말풍선 자리에 직접 그린다(안 주면 기존처럼 아무것도 안\n * 그리고 `handler`만 조용히 실행된다). \"상담신청 버튼을 대화 안에 띄워줘\"처럼, 결과가\n * 텍스트가 아니라 실제 페이지 UI(버튼·팝업 트리거 등)여야 하는 client action을 위한\n * 자리다. `payload`는 이 tool 호출의 input을 그대로 받는다 — 서버에는 안 보낸다(순수\n * 렌더링용이라 `handler`처럼 서버 스펙 동기화 대상이 아니다). */\n render?: (payload: unknown) => React.ReactNode;\n};\n\nexport type XbNovaClientActions = Record<string, XbNovaClientAction>;\n\nfunction toSyncPayload(actions: XbNovaClientActions) {\n return {\n actions: Object.entries(actions).map(([name, spec]) => ({\n name,\n description: spec.description,\n input_schema: spec.inputSchema ?? { type: \"object\", properties: {} },\n result_text: spec.resultText ?? \"완료했습니다.\",\n await_result: spec.awaitResult ?? false,\n })),\n };\n}\n\n/** `<XbNovaProvider>`가 내부적으로 마운트한다 — `XbNovaNudgeLoop`와 같은 자리의 \"설치만\n * 하면 끝\" 백그라운드 컴포넌트. 스펙이 이전에 보낸 것과 내용이 같으면(문자열 비교) 다시\n * 안 보낸다 — 방문자마다 매번 같은 내용을 다시 쓰는 낭비를 막는다. */\nexport function XbNovaClientActionSync({ actions }: { actions?: XbNovaClientActions }) {\n const { auth } = useXbNovaInternal();\n const { apiKey, apiBaseUrl, appId } = auth;\n const lastSyncedRef = useRef<string | null>(null);\n\n useEffect(() => {\n if (!actions || Object.keys(actions).length === 0) return;\n const serialized = JSON.stringify(toSyncPayload(actions));\n if (serialized === lastSyncedRef.current) return;\n lastSyncedRef.current = serialized;\n\n fetch(`${apiBaseUrl}/v1/apps/${appId}/client-actions`, {\n method: \"PUT\",\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${apiKey}` },\n body: serialized,\n }).catch(() => {\n // 동기화 실패해도 대화 자체는 동작해야 한다 — 다음 마운트 때 다시 시도된다.\n });\n }, [actions, apiKey, apiBaseUrl, appId]);\n\n return null;\n}\n","\"use client\";\n\n/**\n * `<XbNovaProvider>` 내부 상태. 공개 API가 아니다 — `useXbNovaChat`/`XbNovaTrack`/\n * `useXbNovaFact`/넛지 엔진이 전부 여기서 읽는다.\n *\n * track/facts 레지스트리는 React state가 아니라 ref로 든다 — dwell 추적이나 폴링마다\n * 리렌더를 유발하면 안 되고, \"값이 필요한 순간\"(넛지 평가, context 조립)에만 pull로\n * 읽으면 되기 때문이다(SDK.md의 \"push가 아니라 pull\" 규칙).\n */\n\nimport { createContext, useCallback, useContext, useEffect, useMemo, useRef } from \"react\";\n\nimport type { XbNovaClientActionResult } from \"./client-actions\";\n\nexport type FactValue = unknown | (() => unknown);\n\nexport type TrackEntry = {\n region: string;\n data: FactValue;\n /** 사람이 읽는 이름 — 힌트 문구·대화 오프닝에 쓴다(\"보증 정보를 보고 계시네요\"). 안 주면\n * region 키를 그대로 쓴다. */\n label?: string;\n enteredAt: number | null; // 지금 보이는 중이면 그 시작 시각(ms), 안 보이면 null\n totalMs: number; // 이전까지 누적된 체류 시간(ms) — 지금 보이는 중이면 여기에 안 더해져 있다\n /** 지금 마운트된 실제 DOM 노드 — 넛지 루프가 트리거별 \"화면 몇 % 구간에 있는지\" 밴드\n * 판정을 직접 하려면 위치(`getBoundingClientRect`)를 재야 해서 필요하다. 언마운트되면\n * null. */\n node: HTMLElement | null;\n};\n\n/** `useXbNovaContext`로 호스트가 AI에게 알려주는 상품·화면 부가 설명 한 건. 내용은 전부\n * 호스트 자율이다 — 가입 됨/안됨 같은 판단도 호스트가 `description`에 문장으로 쓴다. */\nexport type XbNovaContextEntry = {\n title: string;\n description: string;\n};\n\nexport type Auth = {\n apiKey: string;\n botId: string;\n appId: string;\n apiBaseUrl: string;\n screen?: string;\n /** 호스트가 정한 유저 키(카통령은 GA4 user_pseudo_id). 있으면 `visitorId`가 이 값이 된다. */\n userKey?: string;\n /** 호스트가 정한 대화 단위(카통령은 `vehicle:{매물ID}`). 있으면 같은 유저 키·스레드 키로 다시 열 때\n * 이전 대화를 이어 연다. */\n threadKey?: string;\n /** 호스트가 정한 세션 키(카통령은 GA4 session_id). nova가 남기는 이벤트·대화에 실려 세션 단위 집계에 쓴다. */\n sessionKey?: string;\n /** 모든 분석 이벤트에 실을 호스트 값(카통령은 `vehicle_id`). */\n eventProperties?: WidgetEventProperties;\n};\n\n/** 위젯 분석 이벤트 이름. 카통령 GTM 이벤트와 같은 이름을 쓴다(dsauto#5) — GA 와 PostHog 를 바로 비교하기 위해서다.\n * 새 이벤트는 여기에 추가하고 `sendEvent`로 보낸다 — 서버 수정은 필요 없다.\n * 대화 내용 같은 사용자 입력 원문은 속성에 싣지 않는다. */\nexport type WidgetEventName =\n | \"view_vehicle\"\n | \"nudge_AI_view\"\n | \"nudge_AI_click\"\n | \"chat_AI_open\"\n | \"chat_close\"\n | \"question_AI_first\"\n | \"message_send\"\n | \"feedback_submit\"\n | \"cta_impression\"\n | \"apply_AI_click\"\n | \"finance_AI_click\"\n | \"cta_click\";\n\n/** 평평한 원시값만 싣는다. `conversation_id`는 서버가 대화의 넛지·봇을 찾는 데 쓰고, `occurred_at`은\n * 전송보다 앞선 실제 발생 시각이다(없으면 보내는 시각). */\nexport type WidgetEventProperties = Record<string, string | number | boolean | null | undefined>;\n\n/** 위젯 아바타·pill 배경 그라디언트에 쓰는 두 색상 — 기본값은 SDK 자체 톤(인디고→바이올렛)\n * 이고, 소비자가 `<XbNovaProvider theme={{accentFrom, accentTo}}>`로 자기 브랜드 컬러를\n * 넘기면 그걸 쓴다(\"카통령 브랜드 컬러를 따르게 해달라\" — 파랑/보라가 사이트 톤과 안\n * 어울린다는 피드백). CSS 커스텀 프로퍼티(`--xbnova-accent-1/2`)로 shadow host에 실려\n * shadow DOM 안까지 상속된다(styles.css의 `.xbnova-glow-bg`, chat.tsx의 아바타 배지 참고). */\nexport type XbNovaTheme = {\n accentFrom: string;\n accentTo: string;\n /** 위젯의 라이트/다크. `\"system\"`(기본)은 브라우저·OS 설정을 따른다 — 호스트 사이트가 다크\n * 모드를 지원하지 않는데 사용자 OS만 다크이면 사이트는 밝은데 위젯만 어두워진다. 그런 사이트는\n * `\"light\"`로 고정하고, 사이트 자체 테마를 가진 호스트는 그 값을 넘기면 된다. */\n colorScheme: \"light\" | \"dark\" | \"system\";\n};\n\nexport const DEFAULT_THEME: XbNovaTheme = {\n accentFrom: \"#4f46e5\",\n accentTo: \"#7c3aed\",\n colorScheme: \"system\",\n};\n\n/** tool 이름 → 실행부. `handler`의 반환값은 `awaitResult:true`일 때만 의미가 있다 — 진짜\n * tool 결과 텍스트가 돼서 서버로 다시 전송된다(`use-chat.ts`). */\nexport type ClientActionHandler = {\n handler: (\n payload: unknown,\n ) => void | string | XbNovaClientActionResult | Promise<void | string | XbNovaClientActionResult>;\n awaitResult: boolean;\n /** awaitResult 핸들러 타임아웃(ms). 없으면 use-chat의 기본값(15000)을 쓴다. */\n timeoutMs?: number;\n dismissSheet: boolean;\n render?: (payload: unknown) => React.ReactNode;\n};\n\nexport type InternalState = {\n auth: Auth;\n trackRegistry: Map<string, TrackEntry>;\n factsRegistry: Map<string, FactValue>;\n /** `useXbNovaContext`가 등록한 부가 설명 — 훅 인스턴스(id)별 값 또는 함수. 메시지를 보내는\n * 순간에 pull로 읽어 챗 `context.notes`로 싣는다. */\n contextRegistry: Map<string, XbNovaContextEntry[] | (() => XbNovaContextEntry[])>;\n /** `<XbNovaProvider clientActions={{...}}>`에서 등록한 것 — `use-chat.ts`가 client\n * action tool 호출을 받으면 이 맵에서 찾아 디스패치한다. */\n clientActionHandlers: Map<string, ClientActionHandler>;\n visitorId: string;\n /** 호스트 세션 키. 없으면 null — SDK가 따로 만들지 않는다(세션 규칙은 호스트 분석 도구와 맞춰야 의미가 있다). */\n sessionId: string | null;\n /** 분석 이벤트의 유일한 전송 지점. app_id·visitor_id·session_id·screen은 보내는 순간의 최신 값을\n * 자동으로 싣는다. 실패는 조용히 버린다. */\n sendEvent: (event: WidgetEventName, properties?: WidgetEventProperties) => void;\n theme: XbNovaTheme;\n};\n\nconst InternalContext = createContext<InternalState | null>(null);\n\nexport function useXbNovaInternal(): InternalState {\n const ctx = useContext(InternalContext);\n if (!ctx) {\n throw new Error(\n \"XbNova 훅/컴포넌트는 <XbNovaProvider> 안에서만 쓸 수 있습니다. 앱 루트를 <XbNovaProvider>로 감싸세요.\",\n );\n }\n return ctx;\n}\n\nconst VISITOR_STORAGE_KEY = \"xbnova_visitor_id\";\n\n// 분석 이벤트를 호스트 식별자가 올 때까지 모아 두는 상한. GA처럼 페이지보다 늦게 뜨는 도구의 값을 받으려는 것이다.\nconst HOST_IDENTITY_WAIT_MS = 10_000;\n\nfunction resolveVisitorId(): string {\n if (typeof window === \"undefined\") return \"\";\n try {\n const existing = window.localStorage.getItem(VISITOR_STORAGE_KEY);\n if (existing) return existing;\n const id = `v_${crypto.randomUUID()}`;\n window.localStorage.setItem(VISITOR_STORAGE_KEY, id);\n return id;\n } catch {\n // localStorage 접근 불가(시크릿 모드 등) — 이번 세션 동안만 쓰는 값으로 대체한다.\n return `v_${Math.random().toString(36).slice(2)}`;\n }\n}\n\n/** 값 또는 함수를 받아서 실제 값을 지금 이 순간에 읽는다 — pull 규칙의 유일한 구현부. */\nexport function resolveFactValue(value: FactValue): unknown {\n return typeof value === \"function\" ? (value as () => unknown)() : value;\n}\n\n/** `contextRegistry`에 등록된 모든 항목을 지금 이 순간에 읽어 하나의 배열로 합친다. 값이\n * 아닌 항목(형식 오류)은 서버가 한 번 더 거르지만 여기서도 버려 전송량을 줄인다. */\nexport function collectContextEntries(registry: InternalState[\"contextRegistry\"]): XbNovaContextEntry[] {\n const out: XbNovaContextEntry[] = [];\n for (const source of registry.values()) {\n const entries = typeof source === \"function\" ? source() : source;\n if (!Array.isArray(entries)) continue;\n for (const e of entries) {\n if (e && typeof e.title === \"string\" && typeof e.description === \"string\") out.push(e);\n }\n }\n return out;\n}\n\nexport function InternalProvider({\n auth,\n clientActionHandlers,\n theme,\n children,\n}: {\n auth: Auth;\n clientActionHandlers?: Map<string, ClientActionHandler>;\n theme: XbNovaTheme;\n children: React.ReactNode;\n}) {\n const trackRegistry = useRef<Map<string, TrackEntry>>(new Map()).current;\n const factsRegistry = useRef<Map<string, FactValue>>(new Map()).current;\n const contextRegistry = useRef<InternalState[\"contextRegistry\"]>(new Map()).current;\n const emptyHandlers = useRef<Map<string, ClientActionHandler>>(new Map()).current;\n\n const visitorIdRef = useRef<string | null>(null);\n if (visitorIdRef.current === null) {\n visitorIdRef.current = resolveVisitorId();\n }\n // 호스트가 유저 키를 주면 nova가 남기는 모든 방문자 기록(대화·넛지 이벤트·피드백)을 그 값으로\n // 맞춘다 — 대화만 호스트 키로 남으면 넛지 노출 → 대화 퍼널과 호스트 분석(GA) 조인이 끊긴다.\n const visitorId = auth.userKey || visitorIdRef.current!;\n const sessionId = auth.sessionKey || null;\n\n // 넛지 엔진처럼 오래 사는 콜백이 이벤트를 보낼 때도 지금의 유저·세션 키를 싣도록 ref로 읽는다.\n const latestRef = useRef({ auth, visitorId, sessionId });\n latestRef.current = { auth, visitorId, sessionId };\n const postEvent = useCallback((event: WidgetEventName, properties: WidgetEventProperties) => {\n const { auth: a, visitorId: v, sessionId: sid } = latestRef.current;\n if (!v) return;\n const { conversation_id: conversationId, occurred_at: occurredAt, ...rest } = properties;\n // keepalive — 페이지를 떠나는 순간의 이벤트(시트 닫기 뒤 이동 등)도 잃지 않는다.\n fetch(`${a.apiBaseUrl}/v1/events`, {\n method: \"POST\",\n keepalive: true,\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${a.apiKey}` },\n body: JSON.stringify({\n app_id: a.appId,\n event,\n visitor_id: v,\n session_id: sid,\n conversation_id: conversationId ?? null,\n occurred_at: occurredAt,\n properties: { screen: a.screen, ...a.eventProperties, ...rest },\n }),\n }).catch(() => {});\n }, []);\n\n // 호스트 식별자(GA 등)는 페이지보다 늦게 온다. 그 전에 보내면 한 사람이 위젯 ID 와 호스트 ID 로 나뉘어 퍼널이\n // 끊기므로, 식별자가 오거나 상한이 지날 때까지 모았다가 보낸다. 순서는 발생 시각(`occurred_at`)이 지킨다.\n const eventQueueRef = useRef<Array<[WidgetEventName, WidgetEventProperties]>>([]);\n const identitySettledRef = useRef(false);\n const flushEvents = useCallback(() => {\n identitySettledRef.current = true;\n for (const [event, properties] of eventQueueRef.current.splice(0)) postEvent(event, properties);\n }, [postEvent]);\n const sendEvent = useCallback(\n (event: WidgetEventName, properties: WidgetEventProperties = {}) => {\n const stamped = { ...properties, occurred_at: properties.occurred_at ?? new Date().toISOString() };\n if (identitySettledRef.current) postEvent(event, stamped);\n else eventQueueRef.current.push([event, stamped]);\n },\n [postEvent],\n );\n\n const hostIdentity = auth.userKey || sessionId;\n useEffect(() => {\n if (hostIdentity) {\n flushEvents();\n return;\n }\n // 식별자를 안 주는 고객사·동의 거부면 끝내 안 온다. 상한이 지나거나 페이지를 떠나면 위젯 ID 로 보낸다.\n const timer = setTimeout(flushEvents, HOST_IDENTITY_WAIT_MS);\n window.addEventListener(\"pagehide\", flushEvents);\n return () => {\n clearTimeout(timer);\n window.removeEventListener(\"pagehide\", flushEvents);\n };\n }, [hostIdentity, flushEvents]);\n\n // 위젯이 이 페이지에 떴다 — 순 방문자·세션의 분모. Provider 마운트당 한 번.\n const widgetViewSentRef = useRef(false);\n useEffect(() => {\n if (widgetViewSentRef.current) return;\n widgetViewSentRef.current = true;\n sendEvent(\"view_vehicle\");\n }, [sendEvent]);\n\n const value = useMemo<InternalState>(\n () => ({\n auth,\n trackRegistry,\n factsRegistry,\n contextRegistry,\n clientActionHandlers: clientActionHandlers ?? emptyHandlers,\n visitorId,\n sessionId,\n sendEvent,\n theme,\n }),\n [auth, trackRegistry, factsRegistry, contextRegistry, clientActionHandlers, emptyHandlers, visitorId, sessionId, sendEvent, theme],\n );\n\n return <InternalContext.Provider value={value}>{children}</InternalContext.Provider>;\n}\n","/**\n * 고객사가 등록하지 않아도 SDK가 스스로 계산하는 조건 값들. `useXbNovaFact`로 등록하는\n * 커스텀 facts와 달리 항상 존재하고, 넛지 엔진이 조건을 평가하는 매 순간 새로 계산한다.\n */\n\nlet sessionStart: number | null = null;\n\n/** `XbNovaProvider`가 마운트될 때 한 번 호출 — 세션 시작 시각을 고정한다. */\nexport function markSessionStart(): void {\n if (sessionStart === null) sessionStart = Date.now();\n}\n\nexport function getSessionDurationMs(): number {\n if (sessionStart === null) return 0;\n return Date.now() - sessionStart;\n}\n\nexport function getScrollDepth(): number {\n if (typeof document === \"undefined\" || typeof window === \"undefined\") return 0;\n const scrollable = document.documentElement.scrollHeight - window.innerHeight;\n if (scrollable <= 0) return 100;\n return Math.min(100, Math.max(0, Math.round((window.scrollY / scrollable) * 100)));\n}\n\nfunction visitCountKey(screen: string): string {\n return `xbnova_visit_count:${screen}`;\n}\n\n/** `screen`에 진입할 때마다 한 번 호출한다 — 누적 방문 횟수를 1 늘리고 그 값을 반환한다. */\nexport function bumpVisitCount(screen: string): number {\n if (typeof window === \"undefined\") return 1;\n try {\n const key = visitCountKey(screen);\n const next = (Number(window.localStorage.getItem(key)) || 0) + 1;\n window.localStorage.setItem(key, String(next));\n return next;\n } catch {\n return 1;\n }\n}\n\n/** 넛지 엔진이 조건 평가 때마다 읽는다 — `bumpVisitCount`와 달리 값을 늘리지 않는다. */\nexport function peekVisitCount(screen: string): number {\n if (typeof window === \"undefined\") return 0;\n try {\n return Number(window.localStorage.getItem(visitCountKey(screen))) || 0;\n } catch {\n return 0;\n }\n}\n","\"use client\";\n\n/**\n * 동행 바(idle/hint/sheet)와 넛지 평가 루프(`nudge-loop.tsx`)가 공유하는 상태. 루프가\n * `offerHint`로 힌트를 밀어넣으면 `<XbNovaChat>`이 바 UI에 그대로 반영한다 — 둘은 형제\n * 컴포넌트라 props로 못 넘기고 Context로 공유해야 한다.\n *\n * \"말 걸기 예절\"(기획서 4.1)의 페이지당/세션당 노출 횟수 상한은 트리거마다 넛지룰(콘솔\n * 그래프 에디터)로 설정한 값이라 `nudge-loop.tsx`가 그래프에서 직접 읽어 센다(SDK에\n * 하드코딩하지 않는다). 여기서는 힌트 자체의 생명주기(제안 → 소멸/닫기/열기)와, \"연속\n * 닫기 N회 → 먼저 말 걸기 자동 off\"의 N도 힌트를 낸 트리거의 넛지룰 값을 그대로 쓴다\n * (`NudgeHint.autoDisableAfterDismissals`) — 트리거마다 사용자가 참는 정도를 다르게\n * 설정해뒀을 수 있어서다.\n */\n\nimport { createContext, useCallback, useContext, useEffect, useRef, useState } from \"react\";\n\nexport type NudgeHint = {\n messageNodeId: string;\n /** 이 힌트를 낸 트리거 노드 id — 넛지 루프가 \"지금 뜬 힌트가 지금의 대표 트리거와 같은\n * 것인지\" 비교해 대표가 바뀌면 조용히 갈아탈 때 쓴다. */\n triggerNodeId: string;\n text: string;\n botId: string | null;\n /** 이 힌트를 낸 트리거의 넛지룰 값 — 몇 번 연속 닫아야 먼저 말 걸기를 자동으로 끌지. */\n autoDisableAfterDismissals: number;\n /** 이 힌트를 낸 트리거의 넛지룰 값 — 몇 ms 후 조용히 사라질지. 0이면 안 사라진다. */\n autoHideMs: number;\n /** 이 힌트를 눌러 열었을 때 보여줄 빠른 답변 버튼 — 비어 있으면 봇 기본 quick_replies를 쓴다. */\n quickReplies: string[];\n};\n\ntype NudgeContextValue = {\n hint: NudgeHint | null;\n /** 마지막으로 떴던 힌트 전체 — `hint`는 소멸(타임아웃)·닫힘·열림으로 null이 돼도 이건\n * 안 지워진다. 조용히 사라진 뒤(`autoHideMs` 경과) 원형으로 접힌 바를 사용자가 그래도\n * 눌렀을 때, 엉뚱한 기본 인사말이 아니라 방금 그 힌트가 열렸던 것과 똑같이(문구·quick\n * replies·handoff bot까지) 대화를 이어가기 위한 값이다. */\n lastHint: NudgeHint | null;\n /** 루프가 새 힌트를 제안한다 — 이미 뭔가 떠 있으면 무시한다(한 번에 하나만). */\n offerHint: (hint: NudgeHint) => void;\n /** 바의 ✕ — \"거절\"로 센다. 3회 연속이면 먼저 말 걸기를 자동으로 끈다. */\n dismissHint: () => void;\n /** 바 클릭으로 대화를 열 때 — 지금 힌트를 지우고, 연속 닫기 스트릭을 리셋한다(참여는\n * 거절이 아니다). 값을 반환하지 않는다 — 호출부가 이미 `hint`를 reactive 값으로 갖고\n * 있으니 클릭 핸들러에서 그걸 직접 읽으면 된다(아래 구현 주석 참고). */\n openHint: () => void;\n /** 조용한 소멸(타임아웃) — 힌트만 지운다. 거절로 안 친다. */\n autoHideHint: () => void;\n hintsEnabled: boolean;\n disableHints: () => void;\n enableHints: () => void;\n barHidden: boolean;\n hideBar: () => void;\n showBar: () => void;\n};\n\nconst NudgeContext = createContext<NudgeContextValue | null>(null);\n\nconst HINTS_DISABLED_KEY = \"xbnova_hints_disabled\";\nconst BAR_HIDDEN_KEY = \"xbnova_bar_hidden\";\nconst DISMISS_STREAK_KEY = \"xbnova_hint_dismiss_streak\";\n/** 힌트를 낸 트리거에 넛지룰 값이 없을 때만 쓰는 안전망 — 정상 경로는 항상 트리거의\n * `auto_disable_after_dismissals`를 쓴다. */\nconst DEFAULT_DISMISS_STREAK_LIMIT = 3;\n\nfunction readFlag(key: string): boolean {\n if (typeof window === \"undefined\") return false;\n try {\n return window.localStorage.getItem(key) === \"1\";\n } catch {\n return false;\n }\n}\n\nfunction writeFlag(key: string, value: boolean): void {\n if (typeof window === \"undefined\") return;\n try {\n if (value) window.localStorage.setItem(key, \"1\");\n else window.localStorage.removeItem(key);\n } catch {\n // localStorage 접근 불가 — 이번 세션은 메모리 상태로만 동작(이미 useState가 반영 중)\n }\n}\n\nfunction readStreak(): number {\n if (typeof window === \"undefined\") return 0;\n try {\n return Number(window.localStorage.getItem(DISMISS_STREAK_KEY)) || 0;\n } catch {\n return 0;\n }\n}\n\nfunction writeStreak(value: number): void {\n if (typeof window === \"undefined\") return;\n try {\n window.localStorage.setItem(DISMISS_STREAK_KEY, String(value));\n } catch {\n // no-op\n }\n}\n\nexport function NudgeStateProvider({ children }: { children: React.ReactNode }) {\n const [hint, setHint] = useState<NudgeHint | null>(null);\n const [lastHint, setLastHint] = useState<NudgeHint | null>(null);\n const [hintsEnabled, setHintsEnabled] = useState(() => !readFlag(HINTS_DISABLED_KEY));\n const [barHidden, setBarHidden] = useState(() => readFlag(BAR_HIDDEN_KEY));\n const streakRef = useRef(readStreak());\n\n const disableHints = useCallback(() => {\n setHintsEnabled(false);\n writeFlag(HINTS_DISABLED_KEY, true);\n }, []);\n const enableHints = useCallback(() => {\n setHintsEnabled(true);\n writeFlag(HINTS_DISABLED_KEY, false);\n streakRef.current = 0;\n writeStreak(0);\n }, []);\n const hideBar = useCallback(() => {\n setBarHidden(true);\n writeFlag(BAR_HIDDEN_KEY, true);\n }, []);\n const showBar = useCallback(() => {\n setBarHidden(false);\n writeFlag(BAR_HIDDEN_KEY, false);\n }, []);\n\n // 함수형 업데이터를 쓴다 — 클로저로 캡처한 바깥 `hint`를 직접 읽으면 안 된다. 넛지\n // 루프가 같은 tick 안에서 `autoHideHint()`(→`setHint(null)`) 호출 직후 곧바로\n // `offerHint(next)`를 부르는 \"조용히 갈아타기\" 경로가 있는데, React의 상태 갱신은\n // 비동기라 그 순간 이 함수가 클로저로 들고 있는 `hint`는 아직 옛 값(비어있지 않음)이라\n // 아래 가드에 걸려 새 힌트가 조용히 무시돼버렸다(실제로 이 버그 때문에 대표가 바뀌어도\n // 메시지가 안 바뀌는 문제가 있었다). 함수형 업데이터는 같은 배치 안의 앞선 `setHint(null)`\n // 결과를 그대로 이어받으므로 이 경합이 없다.\n const offerHint = useCallback((next: NudgeHint) => {\n setHint((current) => (current ? current : next));\n }, []);\n\n // hint가 실제로 바뀔 때만(비어있지 않은 새 값으로) lastHint를 갱신한다 — offerHint\n // 업데이터 안에서 직접 부르면 순수 함수여야 하는 업데이터에 부작용이 섞인다.\n useEffect(() => {\n if (hint) setLastHint(hint);\n }, [hint]);\n\n const dismissHint = useCallback(() => {\n const limit = hint?.autoDisableAfterDismissals ?? DEFAULT_DISMISS_STREAK_LIMIT;\n setHint(null);\n streakRef.current += 1;\n writeStreak(streakRef.current);\n if (streakRef.current >= limit) disableHints();\n }, [hint, disableHints]);\n\n // 예전엔 `setHint(current => { opened = current; return null; })`로 \"지우면서 읽기\"를\n // 한 번에 하려 했는데, 이건 React가 useState 업데이트를 \"즉시(eager) 계산\"해줄 때만\n // 동작하는 내부 구현 디테일에 기대는 패턴이라 — 이 컴포넌트가 다른 이유로 이미 리렌더가\n // 걸려 있는 순간(꽤 자주 있다, `hint`/`barHidden` 등이 자주 바뀌는 컨텍스트라서)에는\n // eager 계산이 스킵돼서 `opened`가 항상 초기값(null)으로 남았다 — 힌트 알약을 눌러도\n // 매번 \"힌트가 없다\"로 읽혀서 엉뚱한 폴백 인사말이 뜨는 버그였다. `openHint`는 이제\n // 지우기만 하고, 값 읽기는 호출부(`chat.tsx`)가 이미 갖고 있는 `hint` state를 클릭\n // 시점에 그대로 읽는다 — 일반적인 React state 읽기라 이런 함정이 없다.\n const openHint = useCallback(() => {\n setHint(null);\n streakRef.current = 0;\n writeStreak(0);\n }, []);\n\n const autoHideHint = useCallback(() => {\n setHint(null);\n }, []);\n\n return (\n <NudgeContext.Provider\n value={{\n hint,\n lastHint,\n offerHint,\n dismissHint,\n openHint,\n autoHideHint,\n hintsEnabled,\n disableHints,\n enableHints,\n barHidden,\n hideBar,\n showBar,\n }}\n >\n {children}\n </NudgeContext.Provider>\n );\n}\n\nexport function useNudgeState(): NudgeContextValue {\n const ctx = useContext(NudgeContext);\n if (!ctx) {\n throw new Error(\"useNudgeState는 <XbNovaProvider> 안에서만 쓸 수 있습니다.\");\n }\n return ctx;\n}\n\n/** 공개 API — 고객사가 설정 화면(F6)에서 동행 바/힌트를 끄고 켤 수 있게 노출한다. */\nexport function useXbNovaSettings() {\n const { hintsEnabled, disableHints, enableHints, barHidden, hideBar, showBar } = useNudgeState();\n return { hintsEnabled, disableHints, enableHints, barHidden, hideBar, showBar };\n}\n","\"use client\";\n\n/**\n * 넛지 평가 루프 — 렌더링은 안 한다(`null` 컴포넌트). `<XbNovaProvider>`가 마운트되면\n * 백그라운드로 같이 켜진다(설치만 하면 끝이라는 원칙 — SDK.md). `GET\n * /v1/apps/{app_id}/nudge-graph`를 마운트 시 한 번 불러오고, 이후 0.25초 간격으로 각\n * `trigger` 노드에서 시작해 엣지를 따라 조건을 검사해 힌트가 나오면 `nudge-context`에\n * 밀어넣는다 — 실제로 보여주는 건 `<XbNovaChat>`의 동행 바다.\n *\n * \"말 걸기 예절\"(기획서 4.1)의 빈도 상한은 세션당(`max_per_session`, 각 트리거 자기 것만\n * 센다, 0=무제한, 안 채웠으면 서버 기본값 3)만 본다 — 페이지당 상한(`max_per_page`)은\n * 규칙을 단순하게 가려고 없앴다. \"N회 연속 닫기 → 자동 off\"의 N도 힌트를 낸 트리거의\n * `auto_disable_after_dismissals`를 힌트에 실어 보내고, 실제 카운팅은 `nudge-context`가\n * 한다.\n *\n * dwell 누적은 다른 힌트가 이미 떠 있어도 멈추지 않는다 — 그때는 \"새로 보여주기\"만\n * 발동 직전(`canOffer`)에 막는다. 조건은 충족했는데 이미 힌트가 떠 있어서 못 보여준\n * 경우, 그 시도 자체는 세션 카운트에 잡히지 않는다(다음 tick에 다시 시도한다).\n *\n * dwell 트리거는 `region_dwell_ms`(트랙 레지스트리가 IntersectionObserver로 재는, \"한\n * 픽셀이라도 보이면\") 대신 트리거별 `region_visible_from_percent`~`to_percent` band를\n * 직접 잰다 — 스크롤로 스쳐 지나가는 것까지 dwell로 잡히지 않게, 매 tick마다\n * `getBoundingClientRect()`로 뷰포트 band와의 겹침을 확인해 트리거별로 누적한다\n * (`bandDwellRef`, `computeBandStatus`). 트리거마다 band가 달라질 수 있어서 region이 아니라\n * 트리거 단위로 센다.\n */\n\nimport { useCallback, useEffect, useRef } from \"react\";\n\nimport { getScrollDepth, getSessionDurationMs, peekVisitCount } from \"./built-in-facts\";\nimport { resolveFactValue, useXbNovaInternal } from \"./internal-context\";\nimport { useNudgeState } from \"./nudge-context\";\nimport { getRegionDwellMs } from \"./track\";\n\ntype TriggerType = \"dwell\" | \"visit_count\" | \"event\";\n\ntype NudgeNode = {\n id: string;\n kind: \"trigger\" | \"condition\" | \"message\" | \"ai_handoff\";\n trigger_type?: TriggerType | null;\n trigger_params?: Record<string, unknown> | null;\n max_per_page?: number | null;\n max_per_session?: number | null;\n auto_disable_after_dismissals?: number | null;\n auto_hide_ms?: number | null;\n quick_replies?: string[] | null;\n condition_field?: string | null;\n operator?: string | null;\n value?: unknown;\n messages?: string[] | null;\n bot_id?: string | null;\n};\n\ntype NudgeEdge = { from_id: string; to_id: string; branch_label?: \"true\" | \"false\" | null };\ntype NudgeGraph = { nodes: NudgeNode[]; edges: NudgeEdge[] };\n\n/** 판정 간격. dwell 누적은 `Date.now()` 차이로 더하므로 간격과 무관하게 정확하고, 간격은\n * 문턱을 넘은 뒤 실제로 발동하기까지의 양자화 지연(0~간격)만 정한다 — 1초면 멈춘 뒤 평균\n * 0.5초가 더 붙어 \"먼저 말 거는 게 느리다\"는 제보가 있었다(#59). */\nconst EVAL_INTERVAL_MS = 250;\nconst XBNOVA_EVENT_NAME = \"xbnova:event\";\nconst SESSION_COUNT_KEY_PREFIX = \"xbnova_hint_session_count:\";\n/** 이 시간(ms) 안에 스크롤 이벤트가 있었으면 \"아직 스크롤 중\"으로 보고 새 힌트를 띄우지\n * 않는다 — 스크롤 중에도 대표 region이면 dwell은 누적되므로, 멈춘 뒤 힌트까지의 지연은\n * 사실상 이 값이 바닥이다(dwell 문턱 0.5초 기준 멈춘 뒤 0.5~0.75초). */\nconst SCROLL_SETTLE_MS = 500;\n\n/** 이벤트 트리거를 발동시킨다 — 폴링을 기다리지 않고 그 즉시 평가한다. */\nexport function trackXbNovaEvent(eventName: string): void {\n if (typeof window === \"undefined\") return;\n window.dispatchEvent(new CustomEvent(XBNOVA_EVENT_NAME, { detail: { eventName } }));\n}\n\n/** 트리거별로 센다 — \"같은 이유의 힌트는 세션 내 반복하지 않습니다\"가 트리거 단위 규칙이라서다. */\nfunction peekSessionCount(triggerId: string): number {\n if (typeof window === \"undefined\") return 0;\n try {\n return Number(window.sessionStorage.getItem(SESSION_COUNT_KEY_PREFIX + triggerId)) || 0;\n } catch {\n return 0;\n }\n}\n\nfunction bumpSessionCount(triggerId: string): void {\n if (typeof window === \"undefined\") return;\n try {\n window.sessionStorage.setItem(SESSION_COUNT_KEY_PREFIX + triggerId, String(peekSessionCount(triggerId) + 1));\n } catch {\n // no-op\n }\n}\n\nfunction resolveConditionValue(\n field: string,\n ctx: {\n screen?: string;\n trackRegistry: Map<string, import(\"./internal-context\").TrackEntry>;\n factsRegistry: Map<string, import(\"./internal-context\").FactValue>;\n },\n): unknown {\n if (field === \"scroll_depth\") return getScrollDepth();\n if (field === \"session_duration_ms\") return getSessionDurationMs();\n if (field === \"visit_count\") return ctx.screen ? peekVisitCount(ctx.screen) : 0;\n if (field.startsWith(\"region_dwell_ms.\")) {\n return getRegionDwellMs(ctx.trackRegistry, field.slice(\"region_dwell_ms.\".length));\n }\n return resolveFactValue(ctx.factsRegistry.get(field));\n}\n\nfunction compare(actual: unknown, operator: string | null | undefined, expected: unknown): boolean {\n switch (operator) {\n case \"eq\":\n return String(actual) === String(expected);\n case \"ne\":\n return String(actual) !== String(expected);\n case \"gt\":\n return Number(actual) > Number(expected);\n case \"gte\":\n return Number(actual) >= Number(expected);\n case \"lt\":\n return Number(actual) < Number(expected);\n case \"lte\":\n return Number(actual) <= Number(expected);\n default:\n return false;\n }\n}\n\n/** dwell 이외 트리거 타입만 다룬다 — dwell은 트리거별 band 상태가 필요해서 `evaluate` 안에서\n * 따로 처리한다(`evaluateDwellBand`). */\nfunction triggerFires(\n node: NudgeNode,\n ctx: {\n screen?: string;\n firedEventName: string | null;\n },\n): boolean {\n const params = node.trigger_params ?? {};\n switch (node.trigger_type) {\n case \"visit_count\": {\n const threshold = Number(params.threshold ?? 0);\n return Boolean(ctx.screen) && peekVisitCount(ctx.screen!) >= threshold;\n }\n case \"event\":\n return ctx.firedEventName !== null && ctx.firedEventName === params.event_name;\n default:\n return false;\n }\n}\n\n/** 요소가 뷰포트 세로축 기준 `[fromPercent, toPercent]` 구간과 겹치는지 — \"화면 중앙\n * 근처에 있을 때만 보는 중으로 친다\" 판정의 핵심. 한 픽셀이라도 스쳐도 잡히는\n * `IntersectionObserver` threshold:0보다 \"실제로 눈에 담겼는가\"에 더 가깝다. 판정 근거를\n * 그대로 로그에 찍을 수 있도록 픽셀 값까지 함께 반환한다. */\nfunction computeBandStatus(\n el: HTMLElement,\n fromPercent: number,\n toPercent: number,\n): {\n inBand: boolean;\n overlapPx: number;\n rectTop: number;\n rectBottom: number;\n bandTop: number;\n bandBottom: number;\n vh: number;\n} {\n const rect = el.getBoundingClientRect();\n const vh = window.innerHeight;\n const bandTop = (vh * fromPercent) / 100;\n const bandBottom = (vh * toPercent) / 100;\n // 겹치는 세로 픽셀 폭 — 여러 region이 동시에 band 안에 있을 때 \"누가 가장 많이\n // 걸쳐 있는가\"로 대표를 가리는 데 쓴다(아래 `evaluate`의 대표 선정 로직 참고).\n const overlapPx = Math.max(0, Math.min(rect.bottom, bandBottom) - Math.max(rect.top, bandTop));\n const inBand = overlapPx > 0;\n return { inBand, overlapPx, rectTop: rect.top, rectBottom: rect.bottom, bandTop, bandBottom, vh };\n}\n\n/** 디버그 오버레이 on/off — `localStorage.xbnova_debug = \"1\"` 이거나 URL에 `?xbnova_debug=1`을\n * 붙이면 켜진다. dwell 대상 요소를 실제 화면 위에 색깔 박스로 그려서, \"지금 몇 개의\n * 컴포넌트가 dwell 설정돼 있고 각각 어떤 상태인지\"를 눈으로 바로 확인할 수 있게 한다. */\nfunction isDebugEnabled(): boolean {\n if (typeof window === \"undefined\") return false;\n try {\n if (new URLSearchParams(window.location.search).get(\"xbnova_debug\") === \"1\") return true;\n return window.localStorage.getItem(\"xbnova_debug\") === \"1\";\n } catch {\n return false;\n }\n}\n\nconst DEBUG_COLLAPSED_KEY = \"xbnova_debug_collapsed\";\n\nfunction readDebugCollapsed(): boolean {\n if (typeof window === \"undefined\") return true;\n try {\n return window.localStorage.getItem(DEBUG_COLLAPSED_KEY) !== \"0\";\n } catch {\n return true;\n }\n}\n\n/** 앱을 가리지 않게 접었다 펼 수 있는 패널. 예전엔 패널 전체가 `pointer-events: none`이라\n * 클릭도 스크롤도 아예 안 먹었다 — 긴 로그 줄이 `white-space: pre`로 안 줄바꿈되고 가로로\n * 넘쳤는데, 그걸 읽으려는 가로 스크롤 자체가 막혀 있었던 것. 헤더만 클릭 가능하게 만들고,\n * 본문은 줄바꿈시켜서 가로 스크롤이 아예 필요 없게 바꿨다. */\nfunction ensureDebugPanel(\n ref: { current: HTMLDivElement | null },\n collapsedRef: { current: boolean },\n): { body: HTMLDivElement } {\n if (ref.current) {\n return { body: ref.current.querySelector<HTMLDivElement>(\"[data-xbnova-debug-body]\")! };\n }\n const container = document.createElement(\"div\");\n container.style.position = \"fixed\";\n container.style.top = \"8px\";\n container.style.left = \"8px\";\n container.style.zIndex = \"2147483647\";\n container.style.maxWidth = \"min(420px, 90vw)\";\n container.style.borderRadius = \"8px\";\n container.style.background = \"rgba(17, 17, 17, 0.9)\";\n container.style.color = \"#fff\";\n container.style.font = \"11px/1.5 ui-monospace, monospace\";\n container.style.boxShadow = \"0 2px 12px rgba(0,0,0,0.4)\";\n\n const header = document.createElement(\"div\");\n header.style.display = \"flex\";\n header.style.alignItems = \"center\";\n header.style.justifyContent = \"space-between\";\n header.style.gap = \"8px\";\n header.style.padding = \"6px 10px\";\n header.style.cursor = \"pointer\";\n header.style.userSelect = \"none\";\n header.style.fontWeight = \"600\";\n\n const title = document.createElement(\"span\");\n title.textContent = \"XbNova dwell debug\";\n const toggle = document.createElement(\"span\");\n header.append(title, toggle);\n\n const body = document.createElement(\"div\");\n body.setAttribute(\"data-xbnova-debug-body\", \"1\");\n body.style.padding = \"0 10px 8px\";\n body.style.maxHeight = \"70vh\";\n body.style.overflowY = \"auto\";\n body.style.whiteSpace = \"pre-wrap\";\n body.style.wordBreak = \"break-all\";\n\n const applyCollapsed = () => {\n body.style.display = collapsedRef.current ? \"none\" : \"block\";\n toggle.textContent = collapsedRef.current ? \"▸ 펼치기\" : \"▾ 접기\";\n };\n applyCollapsed();\n\n header.addEventListener(\"click\", () => {\n collapsedRef.current = !collapsedRef.current;\n applyCollapsed();\n try {\n window.localStorage.setItem(DEBUG_COLLAPSED_KEY, collapsedRef.current ? \"1\" : \"0\");\n } catch {\n // no-op\n }\n });\n\n container.append(header, body);\n document.body.appendChild(container);\n ref.current = container;\n return { body };\n}\n\nfunction ensureDebugBox(map: Map<string, HTMLDivElement>, key: string): HTMLDivElement {\n let box = map.get(key);\n if (!box) {\n box = document.createElement(\"div\");\n box.style.position = \"fixed\";\n box.style.zIndex = \"2147483646\";\n box.style.boxSizing = \"border-box\";\n box.style.pointerEvents = \"none\";\n box.style.display = \"flex\";\n box.style.alignItems = \"flex-start\";\n box.style.font = \"11px/1.2 ui-monospace, monospace\";\n box.style.color = \"#fff\";\n box.style.padding = \"2px 4px\";\n document.body.appendChild(box);\n map.set(key, box);\n }\n return box;\n}\n\n/** dwell band(%)를 그대로 뷰포트 가로 전체 점선으로 그린다 — \"여기부터 여기까지가 중앙\n * band\"를 눈으로 확인하기 위함. 같은 band(%)를 쓰는 트리거끼리는 선을 공유한다. */\nfunction ensureBandLines(\n map: Map<string, { top: HTMLDivElement; bottom: HTMLDivElement }>,\n key: string,\n): { top: HTMLDivElement; bottom: HTMLDivElement } {\n let pair = map.get(key);\n if (!pair) {\n const makeLine = () => {\n const line = document.createElement(\"div\");\n line.style.position = \"fixed\";\n line.style.left = \"0\";\n line.style.right = \"0\";\n line.style.height = \"0\";\n line.style.borderTop = \"2px dashed #3b82f6\";\n line.style.zIndex = \"2147483645\";\n line.style.pointerEvents = \"none\";\n document.body.appendChild(line);\n return line;\n };\n pair = { top: makeLine(), bottom: makeLine() };\n map.set(key, pair);\n }\n return pair;\n}\n\nfunction clearDebugOverlay(refs: {\n panelRef: { current: HTMLDivElement | null };\n boxesRef: { current: Map<string, HTMLDivElement> };\n bandLinesRef: { current: Map<string, { top: HTMLDivElement; bottom: HTMLDivElement }> };\n}): void {\n refs.panelRef.current?.remove();\n refs.panelRef.current = null;\n for (const box of refs.boxesRef.current.values()) box.remove();\n refs.boxesRef.current.clear();\n for (const pair of refs.bandLinesRef.current.values()) {\n pair.top.remove();\n pair.bottom.remove();\n }\n refs.bandLinesRef.current.clear();\n}\n\nexport function XbNovaNudgeLoop({ appId }: { appId: string }) {\n const { auth, trackRegistry, factsRegistry, sendEvent } = useXbNovaInternal();\n const { apiKey, apiBaseUrl, screen } = auth;\n const { hint, offerHint, autoHideHint, hintsEnabled } = useNudgeState();\n\n const graphRef = useRef<NudgeGraph | null>(null);\n /** 트리거 id → band 안에 누적으로 머문 시간. 트리거마다 band(%) 설정이 다를 수 있어서\n * region이 아니라 트리거별로 잰다 — 같은 region을 보는 트리거 두 개가 서로 다른 band를\n * 요구할 수 있다. */\n const bandDwellRef = useRef<\n Map<string, { accumulatedMs: number; lastCheckedAt: number | null; firedAndAwaitingExit: boolean }>\n >(new Map());\n /** 마지막 스크롤 이벤트 시각. 폭넓은 band(예: 15~60%)를 쓰면 빠르게 스크롤해서 지나가는\n * 중에도 \"지나가는 순간의 대표\"가 몇 tick 연속으로 겹쳐 dwell 문턱을 넘어버릴 수\n * 있다 — 사용자가 실제로 멈춰서 보는 게 아니라 그냥 스쳐 지나갔을 뿐인데 힌트가 뜨고,\n * 곧바로 다음 region으로 또 넘어가면서 힌트가 계속 바뀌어 \"깜빡거림\"처럼 보인다. 힌트를\n * 새로 띄우는 시점만 \"스크롤이 실제로 멈춘 뒤\"로 미룬다 — 대표 선정·박스 하이라이트(디버그\n * 표시)는 그대로 실시간으로 움직여도 되지만, 메시지 전환만은 사용자가 정말 멈췄을 때\n * 한정한다. */\n const lastScrollAtRef = useRef<number>(0);\n /** TICK 로그 중복 억제용 — 직전에 찍은 요약 문자열. 매초 그대로 찍으면 가만히 있을 때도\n * 로그가 끝없이 쌓여서 복붙으로 원인을 추적하기 힘들다(\"틱 필터가 매초떠서 로그 너무\n * 길어서 복사가힘듦\" 피드백) — 대표/overlap/잠금 상태가 실제로 바뀐 tick만 찍는다. */\n const lastTickLogRef = useRef<string>(\"\");\n const debugPanelRef = useRef<HTMLDivElement | null>(null);\n const debugCollapsedRef = useRef<boolean>(readDebugCollapsed());\n const debugBoxesRef = useRef<Map<string, HTMLDivElement>>(new Map());\n const debugBandLinesRef = useRef<Map<string, { top: HTMLDivElement; bottom: HTMLDivElement }>>(new Map());\n const hintRef = useRef(hint);\n hintRef.current = hint;\n const hintsEnabledRef = useRef(hintsEnabled);\n hintsEnabledRef.current = hintsEnabled;\n\n useEffect(() => {\n let cancelled = false;\n fetch(`${apiBaseUrl}/v1/apps/${appId}/nudge-graph`, {\n headers: { Authorization: `Bearer ${apiKey}` },\n })\n .then((r) => (r.ok ? r.json() : Promise.reject(new Error(`${r.status}`))))\n .then((graph: NudgeGraph) => {\n if (!cancelled) graphRef.current = graph;\n })\n .catch(() => {\n // 넛지 그래프를 못 불러와도 채팅 자체는 동작해야 한다 — 조용히 포기한다.\n });\n return () => {\n cancelled = true;\n };\n }, [apiBaseUrl, apiKey, appId]);\n\n const evaluate = useCallback(\n (firedEventName: string | null) => {\n const graph = graphRef.current;\n const debugEnabled = isDebugEnabled();\n const debugRefs = { panelRef: debugPanelRef, boxesRef: debugBoxesRef, bandLinesRef: debugBandLinesRef };\n if (!debugEnabled && debugPanelRef.current) clearDebugOverlay(debugRefs);\n // 매 tick(0.25초) 반복되는 상태 로그는 디버그 모드에서만 찍는다 — 트리거마다 찍으면\n // 초당 수십 줄이 쌓인다. 발동·설정 오류처럼 상태가 바뀔 때의 로그는 그대로 둔다.\n const tickDebug = debugEnabled ? (msg: string) => console.debug(msg) : () => {};\n if (!graph) {\n tickDebug(\"[XbNova] nudge 평가 건너뜀: 그래프를 아직 못 불러옴\");\n return;\n }\n\n // 규칙 단순화: dwell 누적은 항상 계속된다(다른 힌트가 떠 있어도 멈추지 않는다) —\n // \"이미 힌트가 떠 있으니 이번 트리거는 새로 보여주지 않는다\"만 발동 직전에 따로\n // 걸러낸다(아래 `if (!canOffer) continue;`). 예전엔 힌트가 떠 있으면 평가 전체를\n // return 해버려서, 그동안 다른 곳(예: FAQ)에 머물러도 그 트리거의 누적이 통째로\n // 멈춰버리는 게 \"이상하다\"는 신고로 이어졌다.\n const canOffer = !hintRef.current && hintsEnabledRef.current;\n\n const dwellTriggerCount = graph.nodes.filter((n) => n.kind === \"trigger\" && n.trigger_type === \"dwell\").length;\n const scrollIdleMs = Date.now() - lastScrollAtRef.current;\n const debugLines: string[] = debugEnabled\n ? [\n `[XbNova] dwell 트리거 ${dwellTriggerCount}개 (화면=\"${screen}\")${canOffer ? \"\" : \" — 새 힌트 표시는 잠깐 막힘(누적은 계속됨)\"}`,\n scrollIdleMs < SCROLL_SETTLE_MS\n ? `스크롤 중(유휴 ${scrollIdleMs}ms < ${SCROLL_SETTLE_MS}ms) — 새 힌트 발동 보류`\n : `스크롤 유휴 ${scrollIdleMs >= 10000 ? \"10s+\" : scrollIdleMs + \"ms\"}`,\n ]\n : [];\n const activeDebugTriggerIds = new Set<string>();\n\n const nodesById = new Map(graph.nodes.map((n) => [n.id, n]));\n const edgesFrom = new Map<string, NudgeEdge[]>();\n for (const edge of graph.edges) {\n const list = edgesFrom.get(edge.from_id) ?? [];\n list.push(edge);\n edgesFrom.set(edge.from_id, list);\n }\n\n const walkToMessage = (nodeId: string): NudgeNode | null => {\n const node = nodesById.get(nodeId);\n if (!node) return null;\n if (node.kind === \"message\") return node;\n if (node.kind === \"condition\") {\n const matches = compare(\n resolveConditionValue(node.condition_field ?? \"\", { screen, trackRegistry, factsRegistry }),\n node.operator,\n node.value,\n );\n const branch = matches ? \"true\" : \"false\";\n const nextEdge = (edgesFrom.get(nodeId) ?? []).find((e) => e.branch_label === branch);\n return nextEdge ? walkToMessage(nextEdge.to_id) : null;\n }\n return null;\n };\n\n const fire = (node: NudgeNode, text: string, messageNode: NudgeNode) => {\n console.debug(`[XbNova] 힌트 발동: 트리거 ${node.id} → 메시지 \"${text}\"`);\n const handoffEdge = (edgesFrom.get(messageNode.id) ?? [])[0];\n const handoffNode = handoffEdge ? nodesById.get(handoffEdge.to_id) : undefined;\n const botId = handoffNode?.kind === \"ai_handoff\" ? (handoffNode.bot_id ?? null) : null;\n\n bumpSessionCount(node.id);\n offerHint({\n messageNodeId: messageNode.id,\n triggerNodeId: node.id,\n text,\n botId,\n autoDisableAfterDismissals: node.auto_disable_after_dismissals ?? 3,\n // 여러 트리거가 같은 message 노드를 공유할 수 있어서(DAG), \"몇 ms 후 사라질지\"는\n // 트리거(node)가 아니라 메시지(messageNode) 자체의 속성이다.\n autoHideMs: messageNode.auto_hide_ms ?? 20000,\n quickReplies: messageNode.quick_replies ?? [],\n });\n\n sendEvent(\"nudge_AI_view\", { nudge_node_id: messageNode.id, bot_id: botId });\n };\n\n const tryFireFrom = (node: NudgeNode): boolean => {\n const nextEdge = (edgesFrom.get(node.id) ?? [])[0];\n if (!nextEdge) {\n console.debug(`[XbNova] 트리거 ${node.id} 발동했지만 나가는 엣지가 없음`);\n return false;\n }\n const messageNode = walkToMessage(nextEdge.to_id);\n if (!messageNode) {\n console.debug(`[XbNova] 트리거 ${node.id} 발동했지만 message 노드에 못 닿음(조건 분기 확인)`);\n return false;\n }\n const text = messageNode.messages?.[0];\n if (!text) {\n console.debug(`[XbNova] message 노드 ${messageNode.id}에 문구가 없음`);\n return false;\n }\n fire(node, text, messageNode);\n return true;\n };\n\n // dwell 트리거는 즉시 발동하지 않는다 — 같은 순간에 여러 region이 동시에 band 안에\n // 걸칠 수 있어서(예: 스크롤 중 보증 게이지·기본정보·차량 상세정보가 한 화면에 다\n // 보임), 일단 후보로만 모아두고 전부 평가한 뒤 \"band와 가장 많이 겹치는(overlapPx\n // 최대) 하나\"만 대표로 뽑아 그 하나만 발동시킨다. 나머지는 계속 누적만 하고\n // 기다린다(다음 tick에 대표가 바뀔 수 있다).\n const dwellRecords: Array<{\n node: NudgeNode;\n state: { accumulatedMs: number; lastCheckedAt: number | null; firedAndAwaitingExit: boolean };\n requiredMs: number;\n region: string;\n found: boolean;\n screenMatches: boolean;\n inBand: boolean;\n overlapPx: number;\n }> = [];\n let firedThisTick = false;\n\n for (const node of graph.nodes) {\n if (node.kind !== \"trigger\") continue;\n\n // 규칙 단순화: 페이지당 상한은 없앴다(요청) — 세션당 상한만 본다. 0은\n // \"무제한\"이다(auto_hide_ms=0이 \"안 사라짐\"인 것과 같은 관례) — 안 채웠으면(널)\n // 서버 기본값 3, 명시적으로 0을 넣으면 상한 없음.\n const maxPerSession = node.max_per_session === 0 ? Infinity : (node.max_per_session ?? 3);\n const sessionCount = peekSessionCount(node.id);\n if (sessionCount >= maxPerSession) {\n tickDebug(`[XbNova] 트리거 ${node.id}(${node.trigger_type}) 건너뜀: 세션당 상한(${maxPerSession}) 도달`);\n if (debugEnabled && node.trigger_type === \"dwell\") {\n debugLines.push(`- [${node.id}] 세션당 상한(${maxPerSession}) 도달 — 더 안 잼`);\n }\n continue;\n }\n\n if (node.trigger_type === \"dwell\") {\n const params = node.trigger_params ?? {};\n const region = String(params.region ?? \"\");\n const triggerScreen = String(params.screen ?? \"\");\n const requiredMs = Number(params.duration_ms ?? 0);\n const fromPercent = Number(params.region_visible_from_percent ?? 35);\n const toPercent = Number(params.region_visible_to_percent ?? 65);\n\n const state = bandDwellRef.current.get(node.id) ?? {\n accumulatedMs: 0,\n lastCheckedAt: null,\n firedAndAwaitingExit: false,\n };\n const trackEntry = trackRegistry.get(region);\n const el = trackEntry?.node ?? null;\n const screenMatches = screen === triggerScreen;\n const found = screenMatches && el !== null;\n\n let inBand = false;\n let overlapPx = 0;\n let statusDetail: string;\n if (!screenMatches) {\n statusDetail = `screen 불일치 (현재=\"${screen}\" 필요=\"${triggerScreen}\")`;\n } else if (!el) {\n statusDetail = `트랙 요소를 못 찾음 — region=\"${region}\"으로 등록된 <XbNovaTrack>이 이 페이지에 없거나 트랙 키가 다름 (등록된 region 목록: ${Array.from(trackRegistry.keys()).join(\", \") || \"(없음)\"})`;\n } else {\n const status = computeBandStatus(el, fromPercent, toPercent);\n inBand = status.inBand;\n overlapPx = status.overlapPx;\n\n statusDetail = `rect=[top=${status.rectTop.toFixed(0)}, bottom=${status.rectBottom.toFixed(0)}] band=[top=${status.bandTop.toFixed(0)}, bottom=${status.bandBottom.toFixed(0)}] overlapPx=${overlapPx.toFixed(0)} inBand=${inBand}`;\n }\n\n // 누적(accumulatedMs 증가)은 여기서 안 한다 — 대표가 누군지 다 모아본 뒤에\n // 결정한다(아래 두 번째 루프). 여러 region이 동시에 band 안에 걸쳐 있을 때\n // 전부 따로 타이머가 도는 게 문제였다 — \"대표 하나만 도는 시계\" 하나로\n // 만들려면 대표가 정해지기 전엔 아무도 시간을 더하면 안 된다.\n tickDebug(\n `[XbNova] band dwell 트리거=${node.id} region=\"${region}\" band%=${fromPercent}~${toPercent} | ${statusDetail}`,\n );\n\n dwellRecords.push({ node, state, requiredMs, region, found, screenMatches, inBand, overlapPx });\n continue;\n }\n\n const fires = triggerFires(node, { screen, firedEventName });\n if (!fires) continue;\n if (!canOffer) {\n tickDebug(`[XbNova] 트리거 ${node.id} 조건은 충족했지만 새 힌트 표시는 잠깐 막힘`);\n continue;\n }\n if (tryFireFrom(node)) {\n firedThisTick = true;\n break;\n }\n }\n\n // band와 가장 많이 겹치는 하나만 대표로 — 매 tick 새로 뽑는다(직전 tick의 기억을\n // 안 들고 간다). \"다른 곳으로 이동하면 바로 그 자리에서 대표가 바뀌어야 한다\"는\n // 요구사항이라 스위칭에 유예(히스테리시스·디바운스)를 걸면 오히려 옛 대표가 이미\n // band를 벗어났는데도 몇 tick 더 \"대표\"로 남아 있다가 뒤늦게 넘어가는 것처럼\n // 보인다 — 그 지연이 \"꺼졌다 켜졌다\" 하는 것처럼 보이는 원인이었다. 나머지는 화면에\n // 같이 있어도(inBand) 대표가 아니면 이번 tick엔 발동 자격이 없다(디버그 박스는\n // 옅은 초록으로만 표시).\n let primaryId: string | null = null;\n let primaryOverlap = -1;\n for (const r of dwellRecords) {\n if (r.inBand && r.overlapPx > primaryOverlap) {\n primaryId = r.node.id;\n primaryOverlap = r.overlapPx;\n }\n }\n\n\n for (const r of dwellRecords) {\n const isPrimary = r.node.id === primaryId;\n\n // 대표일 때만 시계가 돈다 — 대표가 아니면 band 안에 걸쳐 있어도(inBand) 진짜\n // \"보는 중\"으로 안 치고 0으로 계속 리셋한다. 대표가 바뀌면 새 대표는 이 순간부터\n // 다시 처음(0ms)부터 잰다.\n //\n // `firedAndAwaitingExit`이 true인 동안은 대표+inBand여도 시계를 안 돌린다 — 안\n // 그러면: 힌트가 뜨고 requiredMs(예: 0.5초)는 금방 다시 채워지는데, 힌트 자체는\n // autoHideMs(기본 20초) 동안 화면에 떠 있다. 사용자가 그 20초 내내 같은 곳을 보고\n // 있으면 accumulatedMs가 계속 threshold를 넘은 채로 대기하다가, 힌트가 타임아웃으로\n // 사라지는 바로 다음 tick에 \"이미 문턱 넘음\" 상태라 곧장 같은 메시지가 재발동한다\n // (20초마다 똑같은 힌트가 깜빡깜빡 다시 뜨는 버그로 관찰됨). 한 번 발동한 트리거는\n // 실제로 그 자리를 벗어났다가(대표에서 밀려나거나 band를 나가거나) 다시 들어와야만\n // 재발동 자격을 회복한다.\n const now = Date.now();\n if (isPrimary && r.inBand) {\n if (!r.state.firedAndAwaitingExit && r.state.lastCheckedAt !== null) {\n r.state.accumulatedMs += now - r.state.lastCheckedAt;\n }\n r.state.lastCheckedAt = now;\n } else {\n r.state.accumulatedMs = 0;\n r.state.lastCheckedAt = null;\n r.state.firedAndAwaitingExit = false;\n }\n bandDwellRef.current.set(r.node.id, r.state);\n\n if (debugEnabled) {\n activeDebugTriggerIds.add(r.node.id);\n const statusLabel = r.found\n ? r.inBand\n ? isPrimary\n ? \"IN-BAND(대표)\"\n : \"IN-BAND(겹침, 대표 아님)\"\n : \"band 밖(화면엔 있음)\"\n : r.screenMatches\n ? \"요소 못 찾음\"\n : \"다른 화면\";\n const lockLabel = r.state.firedAndAwaitingExit ? \" 🔒재발동잠금\" : \"\";\n debugLines.push(`- ${r.region || \"(region 없음)\"} [${r.node.id}] ${statusLabel} ${r.state.accumulatedMs}/${r.requiredMs}ms${lockLabel}`);\n\n const el = trackRegistry.get(r.region)?.node ?? null;\n if (r.found && el) {\n const rect = el.getBoundingClientRect();\n const box = ensureDebugBox(debugBoxesRef.current, r.node.id);\n box.style.left = `${rect.left}px`;\n box.style.top = `${rect.top}px`;\n box.style.width = `${Math.max(rect.width, 2)}px`;\n box.style.height = `${Math.max(rect.height, 2)}px`;\n if (r.inBand && isPrimary) {\n box.style.border = \"3px solid #22c55e\";\n box.style.backgroundColor = \"rgba(34,197,94,0.28)\";\n } else if (r.inBand) {\n box.style.border = \"2px solid #86efac\";\n box.style.backgroundColor = \"rgba(134,239,172,0.12)\";\n } else {\n box.style.border = \"2px dashed #f97316\";\n box.style.backgroundColor = \"rgba(249,115,22,0.10)\";\n }\n box.textContent = `${r.region}${isPrimary ? \" ★\" : \"\"} ${r.state.accumulatedMs}/${r.requiredMs}ms${lockLabel}`;\n\n const params = r.node.trigger_params ?? {};\n const fromPercent = Number(params.region_visible_from_percent ?? 35);\n const toPercent = Number(params.region_visible_to_percent ?? 65);\n const bandKey = `${fromPercent}_${toPercent}`;\n const bandLines = ensureBandLines(debugBandLinesRef.current, bandKey);\n const vh = window.innerHeight;\n bandLines.top.style.top = `${(vh * fromPercent) / 100}px`;\n bandLines.bottom.style.top = `${(vh * toPercent) / 100}px`;\n } else {\n debugBoxesRef.current.get(r.node.id)?.remove();\n debugBoxesRef.current.delete(r.node.id);\n }\n }\n\n if (firedThisTick || !isPrimary || !hintsEnabledRef.current) continue;\n if (r.state.accumulatedMs < r.requiredMs) continue;\n if (Date.now() - lastScrollAtRef.current < SCROLL_SETTLE_MS) continue; // 아직 스크롤 중 — 멎을 때까지 대기\n\n const shownHint = hintRef.current;\n if (shownHint && shownHint.triggerNodeId === r.node.id) continue; // 이미 이 트리거의 힌트가 떠 있음\n\n // 대표가 바뀌었는데 화면엔 옛 대표의 힌트가 그대로 떠 있으면(스크롤은 이미\n // 다른 곳으로 갔는데 메시지만 안 바뀌는 문제) — 거절로 세지 않고 조용히\n // 갈아탄다. dwell 문턱(requiredMs)을 새로 넘은 시점에만 갈아타므로, 스쳐\n // 지나가는 동안 힌트가 깜빡이진 않는다.\n if (shownHint) autoHideHint();\n\n if (tryFireFrom(r.node)) {\n // 발동한 대표는 리셋하고 \"재발동 대기\" 상태로 잠근다 — 안 그러면 다음 tick에도\n // 여전히 문턱을 넘어 있어서 계속 다시 발동하려 든다(무제한 세션이면 매 tick\n // 재발동, 세션 상한이 있어도 최소 힌트가 사라지는 즉시 재발동). 이 자리를 벗어나야\n // (위 accumulatedMs 리셋 분기에서) 잠금이 풀린다.\n r.state.accumulatedMs = 0;\n r.state.lastCheckedAt = null;\n r.state.firedAndAwaitingExit = true;\n firedThisTick = true;\n }\n }\n\n // 대표/누적/잠금 상태가 실제로 바뀐 tick만 찍는다 — 매초 그대로 찍으면 가만히 있을\n // 때도 로그가 끝없이 쌓여서 복붙으로 원인을 추적하기 힘들다.\n if (debugEnabled) {\n const snapshot = dwellRecords\n .map(\n (r) =>\n `${r.region}:${r.inBand ? \"IN\" : \"OUT\"}(${r.overlapPx.toFixed(0)}px) ${r.state.accumulatedMs}/${r.requiredMs}ms${r.state.firedAndAwaitingExit ? \"🔒\" : \"\"}`,\n )\n .join(\" | \");\n const tickLog = `primary=${primaryId} :: ${snapshot}`;\n if (tickLog !== lastTickLogRef.current) {\n lastTickLogRef.current = tickLog;\n console.debug(`[XbNova TICK ${new Date().toISOString().slice(11, 23)}] ${tickLog}`);\n }\n }\n\n if (debugEnabled) {\n for (const [id, box] of debugBoxesRef.current) {\n if (!activeDebugTriggerIds.has(id)) {\n box.remove();\n debugBoxesRef.current.delete(id);\n }\n }\n ensureDebugPanel(debugPanelRef, debugCollapsedRef).body.textContent = debugLines.join(\"\\n\");\n }\n },\n [apiBaseUrl, appId, autoHideHint, factsRegistry, offerHint, screen, trackRegistry, sendEvent],\n );\n\n useEffect(() => {\n const interval = window.setInterval(() => evaluate(null), EVAL_INTERVAL_MS);\n const onEvent = (e: Event) => evaluate((e as CustomEvent<{ eventName: string }>).detail.eventName);\n window.addEventListener(XBNOVA_EVENT_NAME, onEvent);\n // capture:true — 페이지 안의 중첩 스크롤 컨테이너(예: 가로 캐러셀)에서 나는 scroll도\n // window까지 안 버블링되므로 캡처 단계에서 잡는다.\n const onScroll = () => {\n lastScrollAtRef.current = Date.now();\n };\n window.addEventListener(\"scroll\", onScroll, { passive: true, capture: true });\n return () => {\n window.clearInterval(interval);\n window.removeEventListener(XBNOVA_EVENT_NAME, onEvent);\n window.removeEventListener(\"scroll\", onScroll, { capture: true });\n clearDebugOverlay({ panelRef: debugPanelRef, boxesRef: debugBoxesRef, bandLinesRef: debugBandLinesRef });\n };\n }, [evaluate]);\n\n return null;\n}\n","\"use client\";\n\n/**\n * 화면 요소 추적 — `context` 자동 채우기와 넛지 dwell 트리거가 공유하는 원시 기능.\n *\n * `IntersectionObserver`(조금이라도 보이는 동안 \"보는 중\")로 영역별 진입 시각·누적 체류\n * 시간을 잰다. `data`는 값이어도 함수여도 된다 — 함수면 훅 호출 시점이 아니라 \"읽히는\n * 순간\"(넛지 평가, context 조립)에 호출한다(push가 아니라 pull, SDK.md 참고).\n *\n * threshold는 0이다 — 예전엔 0.5(면적 절반 이상)였는데, 뷰포트보다 훨씬 긴 섹션(매물\n * 상세의 \"차량 정보\" 등)은 스크롤해도 전체 면적의 50%가 동시에 보이는 순간이 아예 없어서\n * dwell이 영영 안 쌓이는 버그가 있었다 — \"한 픽셀이라도 보이면 보는 중\"으로 바꿨다.\n */\n\nimport { useCallback, useEffect, useRef } from \"react\";\n\nimport { type FactValue, type TrackEntry, useXbNovaInternal } from \"./internal-context\";\n\nconst VISIBILITY_THRESHOLD = 0;\n\nfunction finalizeIfVisible(registry: Map<string, TrackEntry>, region: string): void {\n const entry = registry.get(region);\n if (entry && entry.enteredAt !== null) {\n entry.totalMs += Date.now() - entry.enteredAt;\n entry.enteredAt = null;\n }\n}\n\n/** ref형 — 감싸는 엘리먼트를 늘리기 싫을 때. `<div ref={useXbNovaTrack(\"price\", data)}>`. */\nexport function useXbNovaTrack<T extends HTMLElement = HTMLElement>(\n region: string,\n data?: FactValue,\n label?: string,\n): (node: T | null) => void {\n const { trackRegistry } = useXbNovaInternal();\n const observerRef = useRef<IntersectionObserver | null>(null);\n // ref 콜백은 마운트 커밋 단계에서 아래 useEffect보다 먼저 실행된다 — 그때 registry에\n // entry가 아직 없을 수 있으니, data/label의 최신값을 ref에 담아뒀다가 ref 콜백이 직접\n // entry를 만들 때 쓴다(최신 렌더의 data/label을 반영하기 위함).\n const dataRef = useRef(data);\n dataRef.current = data;\n const labelRef = useRef(label);\n labelRef.current = label;\n\n useEffect(() => {\n const entry = trackRegistry.get(region);\n if (entry) {\n entry.data = data;\n entry.label = label;\n } else {\n trackRegistry.set(region, { region, data, label, enteredAt: null, totalMs: 0, node: null });\n }\n }, [region, data, label, trackRegistry]);\n\n useEffect(() => {\n return () => {\n observerRef.current?.disconnect();\n finalizeIfVisible(trackRegistry, region);\n const entry = trackRegistry.get(region);\n if (entry) entry.node = null;\n };\n }, [region, trackRegistry]);\n\n // useCallback으로 identity를 [region, trackRegistry]에만 묶어 안정화한다 — 매 렌더마다\n // 새 함수를 반환하면 React가 매번 ref를 떼었다 다시 붙이면서(IntersectionObserver\n // disconnect/재생성 반복) 불필요하게 흔들린다. 안정화 자체가 \"entry가 없으면 만든다\"\n // 버그를 완전히 없애주진 않으므로(아래 참고), 핵심 수정은 그 아래 로직이다.\n return useCallback(\n (node: T | null) => {\n observerRef.current?.disconnect();\n observerRef.current = null;\n finalizeIfVisible(trackRegistry, region);\n\n // 마운트 시 ref 콜백이 등록 useEffect보다 먼저 실행돼서 entry가 아직 없을 수 있다 —\n // 그때 `if (entry) entry.node = node`처럼 조용히 넘어가면, 뒤이어 실행되는 등록\n // useEffect가 `node: null`로 entry를 새로 만들어버려서 이 섹션이 다시 리렌더되기\n // 전까지 node가 영원히 null로 박제되는 버그가 있었다(이후 리렌더가 없는 정적\n // 섹션에서 실제로 발생 — dwell 디버그 오버레이가 \"요소 못 찾음\"으로 드러냄).\n // entry가 없으면 여기서 직접 만들어 이 레이스를 원천적으로 없앤다.\n let entry = trackRegistry.get(region);\n if (!entry) {\n entry = { region, data: dataRef.current, label: labelRef.current, enteredAt: null, totalMs: 0, node: null };\n trackRegistry.set(region, entry);\n }\n entry.node = node;\n\n if (!node || typeof IntersectionObserver === \"undefined\") return;\n\n const observer = new IntersectionObserver(\n ([intersection]) => {\n const entry = trackRegistry.get(region);\n if (!entry) return;\n const now = Date.now();\n if (intersection.isIntersecting) {\n if (entry.enteredAt === null) entry.enteredAt = now;\n } else if (entry.enteredAt !== null) {\n entry.totalMs += now - entry.enteredAt;\n entry.enteredAt = null;\n }\n console.debug(\n `[XbNova] track \"${region}\" isIntersecting=${intersection.isIntersecting} ratio=${intersection.intersectionRatio.toFixed(2)} totalMs=${entry.totalMs}${entry.enteredAt !== null ? \" (진입 중)\" : \"\"}`,\n );\n },\n { threshold: VISIBILITY_THRESHOLD },\n );\n observer.observe(node);\n observerRef.current = observer;\n },\n [region, trackRegistry],\n );\n}\n\n/** 선언형 — 감싸기만 하면 됨. `<XbNovaTrack region=\"price\" data={{...}}>...</XbNovaTrack>`. */\nexport function XbNovaTrack({\n region,\n data,\n label,\n className,\n children,\n}: {\n region: string;\n data?: FactValue;\n /** 사람이 읽는 이름 — 넛지 힌트·대화 오프닝 문구에 쓴다(예: \"보증 정보\"). */\n label?: string;\n className?: string;\n children: React.ReactNode;\n}) {\n const ref = useXbNovaTrack(region, data, label);\n return (\n // `data-xbnova-region`은 호스트 앱이 React context 없이도 이 요소를 찾을 수 있게\n // 한다 — client action 핸들러(`<XbNovaProvider clientActions={{...}}>`)는\n // `<XbNovaProvider>`에 넘기는 prop이라 그 안의 훅(`useXbNovaInternal`)을 못 쓴다.\n // `document.querySelector('[data-xbnova-region=\"...\"]')`로 스크롤·하이라이트하면 된다.\n <div ref={ref} className={className} data-xbnova-region={region}>\n {children}\n </div>\n );\n}\n\nfunction dwellMsOf(entry: TrackEntry): number {\n const live = entry.enteredAt !== null ? Date.now() - entry.enteredAt : 0;\n return entry.totalMs + live;\n}\n\n/** 넛지 dwell 트리거 평가가 쓴다 — `region_dwell_ms.<region>` 값. */\nexport function getRegionDwellMs(trackRegistry: Map<string, TrackEntry>, region: string): number {\n const entry = trackRegistry.get(region);\n return entry ? dwellMsOf(entry) : 0;\n}\n\n/** `context` 자동 채우기가 쓴다 — 지금 보고 있는 영역(여러 개면 먼저 들어온 것), 없으면\n * 가장 오래 본 영역 하나를 고른다. */\nexport function getFocusedTrackEntry(trackRegistry: Map<string, TrackEntry>): TrackEntry | null {\n let visible: TrackEntry | null = null;\n let longest: TrackEntry | null = null;\n for (const entry of trackRegistry.values()) {\n if (entry.enteredAt !== null && (visible === null || entry.enteredAt < (visible.enteredAt ?? Infinity))) {\n visible = entry;\n }\n if (longest === null || dwellMsOf(entry) > dwellMsOf(longest)) {\n longest = entry;\n }\n }\n return visible ?? longest;\n}\n","\"use client\";\n\n/**\n * 완제품 채팅 위젯 — \"AI 동행 바\" 하나가 기본(idle)/힌트(hint)/대화 시트(sheet) 세 상태를\n * 오간다(`nova/docs/기획/AI_중고차_동행_서비스_기획서_v0.3.html` 5장 목업 기준). 힌트는\n * `nudge-loop.tsx`가 `nudge-context`에 밀어넣은 걸 그대로 반영한다 — 둘은 형제라 여기서\n * import하지 않는다.\n *\n * 배치(위치·z-index)는 SDK가 정하지 않는다 — `XbNovaShadowRoot`가 `document.body`로\n * 포탈하므로 실제 DOM 위치는 항상 body 바로 아래이고, 사이트마다 z-index 스케일과 하단\n * CTA 높이가 달라 SDK가 하드코딩하면 반드시 어딘가와 충돌한다. `barStyle`(닫힌 바)·\n * `panelStyle`(열린 시트)·`backdropStyle`(모바일 딤 배경)로 소비자가 직접 위치를 정한다.\n *\n * 반드시 인라인 style(React.CSSProperties)이다 — Tailwind 클래스 문자열이 아니라. 호스트\n * 요소는 Shadow DOM *밖*(라이트 DOM, `document.body` 바로 아래)이라 SDK가 빌드 시점에\n * 미리 컴파일해 shadow root 안에 주입하는 CSS가 거기까지 안 닿는다 — 소비자가 런타임에\n * 넘긴 클래스 문자열(`z-[900]` 같은)은 그 컴파일 대상에 없어서 조용히 무시된다(우연히\n * 소비자 자신의 페이지 스타일시트에 같은 클래스가 이미 있지 않은 한). 실제로 이 문제로\n * ctr-client에서 위젯 패널이 사이트 CTA 뒤에 깔려 입력창이 안 보이는 버그가 났었다 —\n * 인라인 style은 클래스 조회 자체가 없어서 이 문제를 원천적으로 피한다. 반응형 전환\n * (모바일 바텀시트 ↔ 데스크탑 플로팅 패널)도 같은 이유로 CSS 미디어쿼리 대신\n * `useIsDesktopViewport`(matchMedia)로 JS에서 직접 판단한다.\n */\n\nimport { useEffect, useRef, useState } from \"react\";\nimport ReactMarkdown from \"react-markdown\";\nimport remarkBreaks from \"remark-breaks\";\nimport remarkGfm from \"remark-gfm\";\n\nimport { type ClientActionHandler, resolveFactValue, useXbNovaInternal } from \"./internal-context\";\nimport { useNudgeState } from \"./nudge-context\";\nimport { XbNovaShadowRoot } from \"./shadow-root\";\nimport type { MessagePart } from \"./stream\";\nimport { getFocusedTrackEntry } from \"./track\";\nimport { type ClientResults, type XbNovaFeedbackEvent, type XbNovaFeedbackRating, type XbNovaFeedbackReason, useXbNovaChat } from \"./use-chat\";\nimport { ToolView, type ViewRendererMap } from \"./views\";\n\n// client action tool 호출(예: get_vehicle_detail)은 화면에 아무것도 안 그린다(위\n// clientActionHandlers.has 체크가 null을 반환). \"생각하는 중…\" 표시를\n// `parts.length === 0`으로만 판단하면, tool 호출 part가 스트리밍으로 먼저 들어오는 순간\n// (아직 안 보이는데) 그 조건이 꺼져버려 답이 나올 때까지 화면이 텅 빈 채로 몇 초 멎어\n// 있는 것처럼 보인다 — \"생각중이라고 뜨다가 사라지고 답이 늦게 뜬다\"는 신고가 이 때문이었다.\n// \"화면에 실제로 보이는 파트가 하나라도 있는가\"로 판단해야 tool 호출 중에도 로딩 표시가\n// 안 끊긴다.\nfunction hasVisiblePart(\n parts: MessagePart[],\n clientActionHandlers: Map<string, ClientActionHandler>,\n clientResults: ClientResults,\n): boolean {\n return parts.some((part) => {\n if (part.kind === \"text\") return true;\n if (clientResults[part.toolCallId]) return true;\n const spec = clientActionHandlers.get(part.toolName);\n return spec ? Boolean(spec.render) : true; // client action인데 render가 없으면 계속 안 보임\n });\n}\n\nconst DISCLOSURE_SEEN_KEY = \"xbnova_disclosure_seen\";\n/** 소비자가 `useXbNovaFact(\"xbnova_summary\", \"트레일블레이저 RS · 22년형 · 2,190만원\")`처럼\n * 등록해두면 시트 헤더 부제로 쓴다 — 안 등록했으면 지금 보고 있는 영역 이름으로 대신한다. */\nconst SUMMARY_FACT_KEY = \"xbnova_summary\";\n\n/** client action 영역 안에서 눌린 버튼·링크와 그 문구. 버튼이 아닌 여백을 누른 거면 null — 클릭으로 안 센다. */\nfunction clickedControl(e: React.MouseEvent<HTMLElement>): { label: string | null } | null {\n const target = e.target instanceof Element ? e.target : null;\n const control = target?.closest(\"button, a, [role='button']\");\n if (!control || !e.currentTarget.contains(control)) return null;\n const text = (control.textContent ?? \"\").replace(/\\s+/g, \" \").trim();\n return { label: text || control.getAttribute(\"aria-label\") || null };\n}\n\nfunction readDisclosureSeen(): boolean {\n if (typeof window === \"undefined\") return true;\n try {\n return window.localStorage.getItem(DISCLOSURE_SEEN_KEY) === \"1\";\n } catch {\n return true;\n }\n}\n\nfunction markDisclosureSeen(): void {\n if (typeof window === \"undefined\") return;\n try {\n window.localStorage.setItem(DISCLOSURE_SEEN_KEY, \"1\");\n } catch {\n // no-op\n }\n}\n\nconst DESKTOP_MEDIA_QUERY = \"(min-width: 640px)\"; // Tailwind sm: 과 같은 값\n\n/** 반응형 전환을 CSS 미디어쿼리가 아니라 JS에서 직접 판단한다 — 호스트(라이트 DOM) 요소는\n * SDK가 컴파일해 shadow root에 넣어둔 CSS의 사정권 밖이라 `sm:` 클래스를 거기 써봐야\n * 안 먹는다(파일 상단 설명 참고). */\nfunction useIsDesktopViewport(): boolean {\n const [isDesktop, setIsDesktop] = useState(false);\n useEffect(() => {\n if (typeof window === \"undefined\") return;\n const mql = window.matchMedia(DESKTOP_MEDIA_QUERY);\n setIsDesktop(mql.matches);\n const onChange = () => setIsDesktop(mql.matches);\n mql.addEventListener(\"change\", onChange);\n return () => mql.removeEventListener(\"change\", onChange);\n }, []);\n return isDesktop;\n}\n\nconst Z_BAR = 2147483000;\nconst Z_PANEL = 2147483000;\nconst Z_BACKDROP = 2147482999;\n\n/** 호스트(라이트 DOM)에 적용할 기본 위치 — 소비자의 `barStyle` 등이 이 위에 개별 속성만\n * 덮어쓴다(전체 교체가 아니라 병합). 실사용은 사이트별 CTA·z-index에 맞춰 `zIndex`·\n * `bottom` 정도는 거의 항상 덮어써야 한다. */\nfunction barBaseStyle(isDesktop: boolean): React.CSSProperties {\n // 데스크탑은 폭을 여기서 명시적으로 고정해야 한다 — `right`만 주고 `left`/`width`를\n // 안 주면 position:fixed가 \"내용에 맞춰 줄어드는\" shrink-to-fit 폭이 되는데, 그 안의\n // pill(children)이 width:100%로 펼쳐지려 하면 \"auto인 부모의 100%\"라는 순환이 생겨\n // 브라우저마다 다르게 계산된다. 폭을 여기서 못박아두고, 원형↔pill 전환은 안쪽\n // pill(div)의 width만 그 안에서 움직인다(아래 XbNovaChat 렌더 부분 참고) — 접혀있을\n // 땐 `margin-left: auto`로 이 고정폭 컨테이너의 오른쪽 끝에 붙인다.\n return isDesktop\n ? { position: \"fixed\", right: \"1rem\", bottom: \"1rem\", width: \"20rem\", zIndex: Z_BAR }\n : { position: \"fixed\", left: \"0.625rem\", right: \"0.625rem\", bottom: \"1rem\", zIndex: Z_BAR };\n}\n\nfunction panelBaseStyle(isDesktop: boolean): React.CSSProperties {\n return isDesktop\n ? { position: \"fixed\", right: \"1rem\", bottom: \"1rem\", width: \"20rem\", height: \"32rem\", zIndex: Z_PANEL }\n : { position: \"fixed\", left: 0, right: 0, top: \"7rem\", bottom: 0, zIndex: Z_PANEL };\n}\n\nfunction backdropBaseStyle(isDesktop: boolean): React.CSSProperties {\n return isDesktop ? { display: \"none\" } : { position: \"fixed\", inset: 0, zIndex: Z_BACKDROP };\n}\n\n/** 힌트 바 문구를 한 글자씩 타이핑되듯 보여준다. `key`(보통 트리거 id)가 바뀌면 진행 중이던\n * 타이핑을 이어붙이지 않고 처음부터 다시 시작한다 — 대표 트리거가 바뀌어 문구가 갈아탈\n * 때마다 \"다다닥\" 새로 찍히는 느낌을 주기 위해서다. */\nfunction useTypewriter(text: string, key: string, speedMs = 28): string {\n const [shown, setShown] = useState(\"\");\n useEffect(() => {\n setShown(\"\");\n if (!text) return;\n let i = 0;\n const id = window.setInterval(() => {\n i += 1;\n setShown(text.slice(0, i));\n if (i >= text.length) window.clearInterval(id);\n }, speedMs);\n return () => window.clearInterval(id);\n }, [key, text, speedMs]);\n return shown;\n}\n\n/** 힌트 pill 배경의 \"액체처럼 흐르는\" 색 덩어리 — 블러 먹인 원(div) 4개를 겹쳐서\n * 부드러운 색 구름으로 흐르게 한다(\"aurora/liquid UI\" 레퍼런스처럼). 색은\n * `--xbnova-accent-1/2`(테마, chat.tsx 상단 themeVars 참고) 두 포인트만 쓴다 —\n * accent-1을 두 개(1번·4번, 더 크고 진하게)로 늘려서 accent-1(브랜드 메인 컬러, 예:\n * 카통령 레드)이 더 강조되고 더 넓게 퍼지도록 했다(\"빨간색이 더 강조돼도, 더 넓게\n * 분포해도 좋겠다\"). 3번째 원은 accent-1 쪽으로 치우친 혼합 톤이라 부드럽게 이어\n * 보인다.\n *\n * SVG(viewBox 스케일링)로 처음 만들었다가 두 문제로 갈아엎었다:\n * - 정사각형 viewBox를 가로로 긴 pill에 비균등 스케일(`preserveAspectRatio=\"none\"`)로\n * 늘렸더니 원이 타원으로 찌그러져 \"아메바\" 같다는 피드백을 받았다. 비율을 지키는\n * `slice`로 바꿔도, pill이 정사각형 viewBox보다 훨씬 넓고 낮아서 세로 대부분이\n * 화면 밖으로 잘려나가 오히려 안 보이는 새 문제가 생겼을 것이다.\n * - gooey 필터(블러 뒤 알파 채널을 급격히 증폭·클리핑하는 feColorMatrix)는 원래\n * \"부드러운 그라디언트\"가 아니라 서로 뭉쳐 붙는 딱딱한 실루엣을 만드는 기법이라\n * (\"이게 그라데이션이 아니잖아\") 부드러운 느낌과 안 맞았다.\n *\n * 그래서 SVG 좌표계 자체를 버리고 평범한 `<span>` + `border-radius:9999px` +\n * `filter: blur()`로 바꿨다 — 크기(px)가 컨테이너 비율과 무관하게 고정이라 어떤\n * 가로세로 비율의 pill에서도 항상 진짜 원으로 남는다(찌그러질 수가 없다). 위치만\n * `left`/`top`(%) 애니메이션으로 움직인다.\n *\n * `position: absolute`/크기/모양(둥근 정도)은 전부 인라인 style이다(파일 상단 설명의\n * \"구조에 영향 주는 속성은 인라인으로\" 규칙 — shadow DOM 비동기 CSS 로딩과 경합하면\n * 안 된다). 블러·움직임(`left`/`top` 애니메이션)만 styles.css의 클래스에 맡긴다(순수\n * 장식이라 최악의 경우 안 흐려지거나 안 움직여도 새는 게 아니라 그냥 또렷한 원이\n * 정지해 보일 뿐이다). */\nfunction GlowBlobs() {\n const blobBase: React.CSSProperties = {\n position: \"absolute\",\n width: \"6.5rem\",\n height: \"6.5rem\",\n borderRadius: \"9999px\",\n pointerEvents: \"none\",\n };\n const accent1 = \"var(--xbnova-accent-1, #4f46e5)\";\n const accent2 = \"var(--xbnova-accent-2, #7c3aed)\";\n return (\n <>\n {/* accent-1(메인 브랜드 컬러) 원 두 개(1·4번)를 다른 것보다 크게(7.5rem) + 진하게\n 둬서 더 강조되고 더 넓게 퍼지게 한다. */}\n <span\n className=\"xbnova-blob-1\"\n style={{\n ...blobBase,\n width: \"7.5rem\",\n height: \"7.5rem\",\n left: \"0%\",\n top: \"5%\",\n background: accent1,\n opacity: 0.62,\n }}\n />\n <span\n className=\"xbnova-blob-2\"\n style={{ ...blobBase, left: \"70%\", top: \"45%\", background: accent2, opacity: 0.42 }}\n />\n <span\n className=\"xbnova-blob-3\"\n style={{\n ...blobBase,\n left: \"35%\",\n top: \"70%\",\n background: `color-mix(in srgb, ${accent1} 65%, ${accent2} 35%)`,\n opacity: 0.48,\n }}\n />\n <span\n className=\"xbnova-blob-4\"\n style={{\n ...blobBase,\n width: \"7.5rem\",\n height: \"7.5rem\",\n left: \"50%\",\n top: \"10%\",\n background: accent1,\n opacity: 0.5,\n }}\n />\n </>\n );\n}\n\n/** 바(AI 버튼)를 눌러 대화 시트를 열 때 `onOpen`으로 넘어오는 값. */\nexport type XbNovaOpenEvent = {\n /** 넛지 힌트 맥락으로 열었는지 — 힌트가 떠 있었거나, 방금 조용히 소멸한 힌트를 이어 열었으면 true. */\n fromNudge: boolean;\n /** `fromNudge`일 때 그 힌트 문구. */\n hintText?: string;\n};\n\nexport function XbNovaChat({\n viewRenderers,\n botId,\n barStyle,\n panelStyle,\n backdropStyle,\n defaultOpen = false,\n idleText = \"AI에게 이 차 물어보기\",\n avatarSize = 28,\n onVerdictAction,\n feedback: feedbackEnabled = true,\n onFeedback,\n onOpen,\n}: {\n /** 특정 view만 자체 컴포넌트로 덮어쓰고 싶을 때 — 지정 안 한 view는 기본 렌더러를 쓴다. */\n viewRenderers?: ViewRendererMap;\n /** Provider의 기본 봇 대신 다른 봇과 고정적으로 대화하고 싶을 때. 힌트를 눌러 열면\n * 그 힌트의 handoff 봇이 이 값보다 우선한다. */\n botId?: string;\n /** 닫힌 상태(기본/힌트 바)의 z-index·하단 offset — 이 사이트의 CTA 위에 오도록. SDK\n * 기본값 위에 준 속성만 덮어쓴다(전체 교체 아님) — 보통 `zIndex`·`bottom` 정도만 주면\n * 된다. 반드시 인라인 style이다(Tailwind 클래스 문자열은 안 먹는다, 위 파일 설명 참고).\n * 예: `{ bottom: \"76px\", zIndex: 45 }`. */\n barStyle?: React.CSSProperties;\n /** 열린 상태(대화 시트/패널)의 z-index·offset. 마찬가지로 기본값 위에 병합된다. */\n panelStyle?: React.CSSProperties;\n /** 모바일 바텀시트 뒤 딤 배경의 z-index. `panelStyle`보다 살짝 낮게 둬야 한다. */\n backdropStyle?: React.CSSProperties;\n /** true면 처음부터 시트가 열려 있다. */\n defaultOpen?: boolean;\n /** 기본(idle) 상태 바 문구. */\n idleText?: string;\n /** 바(AI 버튼)의 동그란 아바타 지름(px), 기본 28. 바의 접힌 원은 52px이라 44~48이면 원을 거의 채운다.\n * 채팅 헤더의 아바타는 이 값에 맞춰 24~36px로 따라 커진다. */\n avatarSize?: number;\n /** 판단 카드의 행동 버튼을 눌렀을 때 — 실제 동작(상담신청 폼 열기, 섹션 스크롤 등)은\n * 소비자가 정한다. SDK는 버튼에 적힌 `action` 문자열을 그대로 넘겨줄 뿐 뜻을 모른다. */\n onVerdictAction?: (action: string) => void;\n /** 완료된 답변 아래 👍/👎 \"상담 만족도\" 버튼을 보일지. 기본 true. */\n feedback?: boolean;\n /** 만족도가 서버에 저장된 뒤 호출된다(분석 이벤트 전송용). `rating: null`은 평가 취소. */\n onFeedback?: (e: XbNovaFeedbackEvent) => void;\n /** 바를 눌러 대화 시트를 열 때 호출된다(분석 이벤트 전송용). 클릭이 Shadow DOM 안에서\n * 일어나 호스트가 직접 잡을 수 없어서 여는 콜백이다. `defaultOpen`으로 처음부터 열린 경우엔\n * 호출되지 않는다. */\n onOpen?: (e: XbNovaOpenEvent) => void;\n}) {\n // client action 핸들러는 `<XbNovaChat>` prop이 아니라 `<XbNovaProvider clientActions={{...}}>`에\n // 등록한다 — 이 화면에서 여러 <XbNovaChat> 인스턴스가 떠도 같은 페이지 능력을 공유해야\n // 해서(넛지 handoff로 다른 봇과 열릴 때도 마찬가지) Provider 레벨 개념이다.\n const { auth, trackRegistry, factsRegistry, clientActionHandlers, theme, sendEvent } = useXbNovaInternal();\n // CSS 커스텀 프로퍼티는 상속되는 값이라, shadow host(라이트 DOM)에 인라인으로 주면\n // shadow DOM 안쪽까지 그대로 상속된다 — `.xbnova-glow-bg`(styles.css)와 아래 아바타\n // 배지 그라디언트가 이 값을 읽는다. `<XbNovaProvider theme={...}>`로 소비자가 브랜드\n // 컬러를 넘기면(예: ctr-client는 자기 브랜드 레드) 여기까지 흘러온다.\n const themeVars = {\n \"--xbnova-accent-1\": theme.accentFrom,\n \"--xbnova-accent-2\": theme.accentTo,\n } as React.CSSProperties;\n const headerAvatarSize = Math.min(36, Math.max(24, Math.round(avatarSize * 0.8)));\n const avatarGradientStyle: React.CSSProperties = {\n background: \"linear-gradient(135deg, var(--xbnova-accent-1), var(--xbnova-accent-2))\",\n };\n const { hint, lastHint, dismissHint, openHint, autoHideHint, hintsEnabled, barHidden } = useNudgeState();\n const isDesktop = useIsDesktopViewport();\n\n const [open, setOpen] = useState(defaultOpen);\n // 시트가 열리는 순간 밑에서 슬라이드업하는 애니메이션용 — 처음엔 화면 밖(entered=false)에\n // 두고, 마운트 다음 프레임에 entered=true로 바꿔 transition이 실제로 재생되게 한다(같은\n // 프레임에 최종 상태로 바로 그리면 브라우저가 전환을 건너뛴다).\n const [entered, setEntered] = useState(false);\n const [activeBotId, setActiveBotId] = useState<string | undefined>(undefined);\n const [activeNudgeNodeId, setActiveNudgeNodeId] = useState<string | undefined>(undefined);\n const [nudgeQuickReplies, setNudgeQuickReplies] = useState<string[]>([]);\n // 눌러서 연 넛지의 문구. 대화가 있으면 이력을 건드리지 않고 칩 위에 띄웠다가, 답할 때 이력에 넣는다.\n const [nudgePrompt, setNudgePrompt] = useState<string | null>(null);\n const [openingLine, setOpeningLine] = useState<string | null>(null);\n const [showDisclosure, setShowDisclosure] = useState(false);\n const [input, setInput] = useState(\"\");\n const bottomRef = useRef<HTMLDivElement>(null);\n // 힌트가 바뀌어도 이 컴포넌트 자체는 계속 렌더되므로(원형↔pill을 같은 노드로 전환하려고\n // 한 return만 쓴다), hook은 조건 없이 최상단에서 불러야 한다 — `triggerNodeId`를 key로\n // 줘서 같은 문구를 다시 받아도(이론상) 아니라 트리거가 실제로 바뀔 때만 재시작한다.\n const typedHintText = useTypewriter(hint?.text ?? \"\", hint?.triggerNodeId ?? \"\");\n\n const { onboarding, opening, messages, busy, error, send, clientResults, feedback, setFeedback, trackClientActionClick } = useXbNovaChat({\n onFeedback,\n botId: activeBotId ?? botId,\n nudgeNodeId: activeNudgeNodeId,\n openingMessage: openingLine ?? undefined,\n onDismissSheet: () => setOpen(false),\n });\n\n // 서버 응답 전에는 직전에 받아 둔 branding(localStorage)을 쓴다 — 첫 화면부터 이미지가 보인다.\n const effectiveBotId = activeBotId ?? botId;\n const [cachedBranding, setCachedBranding] = useState<{\n logo_url?: string;\n avatar_text?: string;\n badge_text?: string;\n disclaimer?: string;\n } | null>(null);\n useEffect(() => {\n try {\n const raw = window.localStorage.getItem(`xbnova_branding:${effectiveBotId}`);\n setCachedBranding(raw ? JSON.parse(raw) : null);\n } catch {\n setCachedBranding(null);\n }\n }, [effectiveBotId]);\n useEffect(() => {\n if (!onboarding?.branding) return;\n try {\n window.localStorage.setItem(\n `xbnova_branding:${effectiveBotId}`,\n JSON.stringify({\n logo_url: onboarding.branding.logo_url,\n avatar_text: onboarding.branding.avatar_text,\n badge_text: onboarding.branding.badge_text,\n disclaimer: onboarding.branding.disclaimer,\n }),\n );\n } catch {\n // 저장소를 못 쓰는 환경(사생활 보호 모드 등)이면 캐시 없이 동작한다.\n }\n }, [onboarding?.branding, effectiveBotId]);\n const branding = onboarding?.branding ?? cachedBranding ?? undefined;\n const badgeText = branding?.badge_text?.trim() ?? \"\";\n const disclaimer = branding?.disclaimer?.trim() ?? \"\";\n // 서버 응답이 오거나, 캐시가 있거나, 이미 오류가 났으면(서버를 못 불러도 \"AI\" 배지는 보여준다) 확정으로 본다.\n const brandingReady = Boolean(onboarding) || cachedBranding !== null || Boolean(error);\n\n useEffect(() => {\n bottomRef.current?.scrollIntoView({ behavior: \"smooth\" });\n }, [messages]);\n\n useEffect(() => {\n if (!open) {\n setEntered(false);\n return;\n }\n const raf = requestAnimationFrame(() => setEntered(true));\n return () => cancelAnimationFrame(raf);\n }, [open]);\n\n // 힌트는 잠시 후 스스로 사라진다(\"조용한 소멸\") — 사용자가 직접 닫은 게 아니라서 거절로 안 친다.\n // 몇 ms 후 사라질지는 이 힌트를 낸 트리거의 넛지룰(auto_hide_ms) 값이다 — 0이면 안 사라진다.\n useEffect(() => {\n if (!hint || hint.autoHideMs <= 0) return;\n console.debug(`[XbNova] 힌트 \"${hint.text}\" 표시 — ${hint.autoHideMs}ms 후 자동으로 사라짐`);\n const timer = window.setTimeout(() => {\n console.debug(`[XbNova] 힌트 자동 소멸(타임아웃) — \"${hint.text}\"`);\n autoHideHint();\n }, hint.autoHideMs);\n return () => window.clearTimeout(timer);\n }, [hint, autoHideHint]);\n\n function openSheet() {\n // `hint`는 이미 이 컴포넌트가 들고 있는 reactive state다 — 클릭 핸들러 안에서 읽으면\n // 항상 그 순간의 실제 값이다(React state 읽기의 기본 보장). `openHint()`는 이제\n // 부수효과(지우기)만 한다 — 예전엔 `openHint()`의 반환값으로 읽으려 했는데, 그건\n // React의 \"즉시 계산\" 내부 최적화가 스킵될 때마다(이 컴포넌트가 자주 리렌더돼서 꽤\n // 흔했다) 항상 null을 돌려줘서 힌트를 눌러도 엉뚱한 폴백 인사말이 뜨는 버그였다.\n // `hint`가 이미 조용히 소멸(autoHideMs 경과)해 원형으로 접힌 뒤에도, 그 상태에서\n // 바를 누르면 방금 사라진 힌트와 똑같이 이어가야 한다 — `lastHint`로 폴백한다.\n const consumed = hint ?? lastHint;\n openHint();\n if (consumed?.botId) setActiveBotId(consumed.botId);\n\n if (consumed) {\n // 힌트를 눌러 열었으면 그 힌트가 보여준 문구를 그대로 대화 시작 메시지로 이어간다 —\n // 다른 문구로 바뀌면 방금 읽고 클릭한 이유가 사라져 버린다.\n setOpeningLine(consumed.text);\n setActiveNudgeNodeId(consumed.messageNodeId);\n setNudgeQuickReplies(consumed.quickReplies);\n setNudgePrompt(consumed.text);\n\n // 넛지 전환 퍼널의 \"AI 클릭\" 단계 — 이 힌트를 눌러서 열었을 때만 기록한다(기본\n // 문구를 눌러 연 건 넛지 전환이 아니다).\n sendEvent(\"nudge_AI_click\", { nudge_node_id: consumed.messageNodeId, bot_id: consumed.botId });\n } else {\n // 기본 바를 눌러 열었으면 넛지 맥락이 없다 — 이전 힌트의 버튼이 남아있으면 안 된다.\n setNudgeQuickReplies([]);\n setNudgePrompt(null);\n setActiveNudgeNodeId(undefined);\n sendEvent(\"chat_AI_open\", { source: \"direct\" });\n const focused = getFocusedTrackEntry(trackRegistry);\n setOpeningLine(\n focused ? `${focused.label ?? focused.region}을(를) 보고 계시네요. 궁금하신 점을 편하게 물어보세요.` : null,\n );\n }\n\n if (!readDisclosureSeen()) {\n setShowDisclosure(true);\n markDisclosureSeen();\n }\n setOpen(true);\n // 호스트 콜백이 던져도 시트 열기는 이미 끝났다 — 분석 코드 오류가 위젯 동작을 막지 않게 삼킨다.\n try {\n onOpen?.({ fromNudge: Boolean(consumed), hintText: consumed?.text });\n } catch (err) {\n console.warn(\"[XbNova] onOpen 콜백 오류\", err);\n }\n }\n\n function closeSheet() {\n setOpen(false);\n sendEvent(\"chat_close\");\n }\n\n if (!open) {\n if (barHidden) return null;\n\n // 평소엔 원형 아바타 하나뿐이다가, 힌트가 뜨는 순간 가로로 펼쳐지며 문구가 나타난다.\n // 힌트 유무에 따라 완전히 다른 JSX(별도 return)를 그리면 DOM 자체가 통째로 바뀌어서\n // 브라우저가 전환을 애니메이션할 대상이 없다 — 같은 노드를 계속 그리고 width만 CSS\n // transition으로 움직여야 \"슈우웅\" 펼쳐지는 효과가 난다.\n const showHint = Boolean(hint && hintsEnabled);\n const collapsedSize = \"3.25rem\";\n const idleLabel = lastHint?.text ?? idleText;\n\n return (\n <XbNovaShadowRoot style={{ ...barBaseStyle(isDesktop), ...themeVars, ...barStyle }}>\n {/* `marginLeft: auto`로 고정폭 호스트(barBaseStyle)의 오른쪽 끝에 붙인다 — 접혀\n 있을 때도 펼쳐져 있을 때도 pill의 오른쪽 끝은 항상 같은 자리라, 아바타(맨\n 오른쪽 슬롯)는 두 상태에서 화면상 거의 같은 위치에 머물고 pill만 왼쪽으로\n 자라나는 것처럼 보인다. */}\n {/* 바깥쪽은 크기/위치만 잡는다. overflow-hidden·배경·내용은 전부 안쪽 div가\n 맡는다 — 배경 자체가 (참고 이미지처럼) 부드럽게 번진 색 덩어리라, border처럼\n 요소 밖으로 삐져나오는 부분이 아예 없다. 그래서 여기 바깥쪽엔 회전 테두리\n 때 있었던 position:relative 같은 위치 계산이 더 필요 없다 — background는\n 항상 자기 요소의 박스 안에서만 그려지므로 구조적으로 새어나갈 수 없다. */}\n <div\n style={{\n minHeight: collapsedSize,\n height: showHint ? \"auto\" : collapsedSize,\n width: showHint ? \"100%\" : collapsedSize,\n marginLeft: \"auto\",\n borderRadius: showHint ? \"1.5rem\" : \"9999px\",\n transition: \"width 320ms cubic-bezier(0.22, 1, 0.36, 1), height 200ms ease, border-radius 200ms ease\",\n }}\n >\n <div\n className={`flex h-full w-full items-center overflow-hidden ${showHint ? \"xbnova-glow-bg\" : \"\"}`}\n style={{\n borderRadius: \"inherit\",\n // GlowBlobs(SVG)를 이 안에 절대위치로 깔려면 이 요소가 위치 기준점이어야\n // 한다 — 인라인으로 줘서 shadow DOM 비동기 CSS 로딩과 경합하지 않는다.\n position: \"relative\",\n // 접혀있을 땐 이 컨테이너 자체가 완전히 안 보인다(배경도 그림자도 없음) —\n // 스크롤 중에도 마찬가지다. 예전엔 스크롤 중 반투명 회색 배경을 깔았는데,\n // 그 배경 자체가 페이지 위에 원형 얼룩처럼 보인다는 피드백을 받았다\n // (\"스크롤 좀만 움직여도 저 투명한게 회색으로 바뀜\") — 그냥 안 보이는 게\n // 맞다. 펼쳐지면(showHint) xbnova-glow-bg 클래스가 배경/블러를 전부 맡는다.\n background: showHint ? undefined : \"transparent\",\n // 접힌 상태의 \"떠 있는\" 느낌은 이 컨테이너가 아니라 안쪽 아바타 배지\n // (아래 `shadow-md`, 지름 28px)만 낸다 — 52px 컨테이너 전체에 그림자를\n // 주면 그 자체가 회색 원 얼룩처럼 보인다.\n // 참고 이미지(카드 위에 살짝 겹쳐 얹힌 \"프로\"/\"미완\" 뱃지)처럼 진짜 스티커가\n // 붕 떠 있는 느낌은 은은하게 퍼지는 그림자가 아니라 — 붙어있는 면에 짧고\n // 진한 그림자(contact shadow) + 그보다 넓게 퍼지지만 방향성 있는 그림자를\n // 겹쳐야 난다. 예전 값(퍼지기만 하는 단일 그림자)은 배경에 스며드는\n // \"glow\"처럼 보여서 뜬 느낌이 안 났다.\n boxShadow: showHint\n ? \"0 2px 3px rgba(16,24,40,.35), 0 14px 24px -6px rgba(16,24,40,.45)\"\n : \"none\",\n transition: \"background 200ms ease, box-shadow 200ms ease\",\n }}\n >\n {showHint ? <GlowBlobs /> : null}\n {showHint ? (\n <button\n type=\"button\"\n onClick={dismissHint}\n aria-label=\"힌트 닫기\"\n className=\"shrink-0 py-3.5 pl-3.5 pr-1 text-slate-400 hover:text-slate-200\"\n style={{ position: \"relative\" }}\n >\n ✕\n </button>\n ) : null}\n <button\n type=\"button\"\n onClick={openSheet}\n aria-label={showHint ? hint!.text : idleLabel}\n className=\"flex min-w-0 flex-1 items-center text-left\"\n style={{ position: \"relative\" }}\n >\n {/* 길어지면 줄임표로 자르지 않고 줄바꿈한다 — pill 높이가 그만큼 늘어난다\n (`height: auto`, 바깥쪽 div 참고). */}\n {showHint ? (\n <span className=\"min-w-0 flex-1 whitespace-normal break-words py-3.5 pl-2 pr-1 text-[13px] font-semibold leading-snug text-white\">\n {typedHintText}\n </span>\n ) : null}\n {/* 아바타 슬롯을 collapsedSize로 고정해둔다 — 접혀있을 땐 이 슬롯 자체가 원이고\n (안이 정중앙), 펼쳐지면 같은 슬롯이 pill의 맨 오른쪽 위쪽에 그대로 남고\n 왼쪽에 문구가 줄바꿈되며 늘어난다. 버튼에 직접 padding을 주면 접힌 원\n 크기(52px)보다 아바타+패딩이 커져서 아바타가 안 맞는 문제가 있었다. */}\n <span\n className=\"flex shrink-0 items-center justify-center\"\n style={{ width: collapsedSize, height: collapsedSize }}\n >\n <Avatar\n branding={branding}\n ready={brandingReady}\n size={avatarSize}\n className=\"shadow-md\"\n gradient={avatarGradientStyle}\n />\n </span>\n </button>\n </div>\n </div>\n </XbNovaShadowRoot>\n );\n }\n\n const summary = resolveFactValue(factsRegistry.get(SUMMARY_FACT_KEY)) as string | undefined;\n const focusedLabel = getFocusedTrackEntry(trackRegistry)?.label;\n const contextLine = summary ?? (focusedLabel ? `${focusedLabel} 보는 중` : undefined);\n\n return (\n <>\n {!isDesktop ? (\n <XbNovaShadowRoot\n style={{\n ...backdropBaseStyle(isDesktop),\n opacity: entered ? 1 : 0,\n transition: \"opacity 260ms ease-out\",\n ...backdropStyle,\n }}\n >\n <div className=\"h-full w-full bg-black/45\" onClick={closeSheet} />\n </XbNovaShadowRoot>\n ) : null}\n <XbNovaShadowRoot\n style={{\n ...panelBaseStyle(isDesktop),\n ...themeVars,\n transform: entered ? \"translateY(0)\" : \"translateY(100%)\",\n transition: \"transform 280ms cubic-bezier(0.22, 1, 0.36, 1)\",\n ...panelStyle,\n }}\n >\n <div className=\"flex h-full w-full flex-col rounded-t-[20px] bg-white shadow-2xl sm:rounded-2xl sm:border sm:border-neutral-200 dark:bg-neutral-900 sm:dark:border-neutral-700\">\n <header className=\"flex items-center gap-2 border-b border-neutral-200 px-3.5 py-2.5 dark:border-neutral-700\">\n <Avatar\n branding={branding}\n ready={brandingReady}\n size={headerAvatarSize}\n className=\"shrink-0\"\n gradient={avatarGradientStyle}\n />\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex min-w-0 items-center gap-1.5\">\n <span className=\"truncate text-sm font-semibold\">{onboarding?.branding?.title ?? \"AI 동행\"}</span>\n {/* 봇 설정의 배지(예: \"Beta\"). 비어 있으면 그리지 않는다. */}\n {badgeText ? (\n <span className=\"shrink-0 rounded-full border border-neutral-300 px-1.5 py-px text-[10px] font-semibold leading-4 text-neutral-500 dark:border-neutral-600 dark:text-neutral-400\">\n {badgeText}\n </span>\n ) : null}\n </div>\n {contextLine ? (\n <div className=\"truncate text-[11px] text-neutral-400\">{contextLine}</div>\n ) : null}\n </div>\n <button\n type=\"button\"\n onClick={closeSheet}\n aria-label=\"채팅 닫기\"\n className=\"shrink-0 text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-200\"\n >\n ×\n </button>\n </header>\n\n <div className=\"flex-1 space-y-3 overflow-y-auto p-4\">\n {showDisclosure ? (\n <div className=\"rounded-lg bg-neutral-50 px-3 py-2 text-[11px] text-neutral-400 dark:bg-neutral-800\">\n 지금 보고 계신 화면과 매물 정보를 기준으로 안내해 드려요.\n </div>\n ) : null}\n\n {opening ? (\n <Bubble role=\"assistant\">\n <AssistantMarkdown text={opening} />\n </Bubble>\n ) : null}\n\n {messages.map((message, i) => (\n <div key={i}>\n {message.role === \"user\" ? (\n <Bubble role=\"user\">\n {message.parts.map((p) => (p.kind === \"text\" ? p.text : \"\")).join(\"\")}\n </Bubble>\n ) : (\n <AnswerActionsFrame\n enabled={feedbackEnabled && !busy && !!message.id && message.parts.some((p) => p.kind === \"text\" && p.text.trim())}\n text={message.parts.filter((p) => p.kind === \"text\").map((p) => (p.kind === \"text\" ? p.text : \"\")).join(\"\\n\\n\")}\n rating={message.id ? (feedback[message.id] ?? null) : null}\n isLast={i === messages.length - 1}\n onRate={(r, reason) => void setFeedback(message.id!, r, reason)}\n >\n {message.parts.map((part, j) => {\n if (part.kind === \"text\") {\n return (\n <Bubble key={j} role=\"assistant\">\n <AssistantMarkdown text={part.text} />\n </Bubble>\n );\n }\n // client action tool은 기본적으로 채팅에 아무것도 안 그린다 — 핸들러\n // 디스패치는 위 useEffect가 담당한다(딱 한 번, 렌더와 분리). `render`를\n // 등록해둔 client action만 예외로, 그 자리에 직접 UI를 그린다(예: 상담\n // 신청 버튼).\n if (clientActionHandlers.has(part.toolName)) {\n // 핸들러가 view+output(정규화된 결과)을 돌려줬으면 tool 이름과 무관하게 그 view로 그린다.\n const result = clientResults[part.toolCallId];\n if (result) {\n return (\n <ToolView\n key={j}\n part={{ ...part, state: \"output\", view: result.view, output: result.output }}\n viewRenderers={viewRenderers}\n onVerdictAction={onVerdictAction}\n />\n );\n }\n const spec = clientActionHandlers.get(part.toolName);\n // 호스트가 그린 버튼이라 SDK는 onClick을 못 붙인다 — 감싼 div에서 캡처해 어떤 버튼이 눌렸는지\n // (문구) 기록한다. 호스트의 클릭 동작(팝업 열기 등)은 그대로 진행된다.\n return spec?.render ? (\n <div\n key={j}\n onClickCapture={(e) => {\n const control = clickedControl(e);\n if (control) trackClientActionClick(part.toolName, control.label);\n }}\n >\n {spec.render(part.input)}\n </div>\n ) : null;\n }\n return (\n <ToolView key={j} part={part} viewRenderers={viewRenderers} onVerdictAction={onVerdictAction} />\n );\n })}\n {!hasVisiblePart(message.parts, clientActionHandlers, clientResults) && busy ? (\n <Bubble role=\"assistant\">\n <span className=\"text-neutral-400\">생각하는 중…</span>\n </Bubble>\n ) : null}\n </AnswerActionsFrame>\n )}\n </div>\n ))}\n\n {error ? (\n <div className=\"rounded-lg bg-red-50 px-3 py-2 text-xs text-red-700 dark:bg-red-950 dark:text-red-300\">\n {error}\n </div>\n ) : null}\n <div ref={bottomRef} />\n </div>\n\n {(() => {\n // 넛지를 눌러 열었으면 그 넛지 전용 버튼을 우선한다 — 어떤 트리거든 공통으로\n // 뜨는 봇 기본 quick_replies(온보딩 설정)보다 지금 맥락에 맞는 버튼이 낫다.\n // 넛지 칩은 messages.length와 무관하게 뜬다 — 이미 대화가 있는 상태에서 다른\n // 곳을 보다가 새 넛지를 눌러 열어도, 그 넛지의 칩은 보여야 방금 클릭한 이유가\n // 이어진다(기존엔 messages.length !== 0이면 통째로 숨겨서 이 경우가 안 됐다).\n // 반면 봇 기본 quick_replies(onboarding)는 정말 \"첫 진입\"에만 의미가 있으니\n // 대화가 이미 있으면 그대로 숨긴다.\n const prompt = messages.length > 0 ? nudgePrompt : null;\n if (prompt || nudgeQuickReplies.length > 0) {\n return (\n <div className=\"space-y-2 border-t border-neutral-200 px-3.5 py-2.5 dark:border-neutral-700\">\n {prompt ? (\n <div className=\"text-sm font-medium\">\n <AssistantMarkdown text={prompt} />\n </div>\n ) : null}\n {nudgeQuickReplies.length > 0 ? (\n <div className=\"flex flex-wrap gap-1.5\">\n {nudgeQuickReplies.map((q) => (\n <button\n key={q}\n onClick={() => {\n setNudgeQuickReplies([]);\n setNudgePrompt(null);\n send(q, { nudgeOpening: prompt, inputType: \"quick_reply\" });\n }}\n className=\"xbnova-chip rounded-full border px-2.5 py-1 text-[11px] font-semibold transition\"\n >\n {q}\n </button>\n ))}\n </div>\n ) : null}\n </div>\n );\n }\n const quickReplies = onboarding?.quick_replies ?? [];\n if (messages.length !== 0 || quickReplies.length === 0) return null;\n return (\n <div className=\"flex flex-wrap gap-1.5 border-t border-neutral-200 px-3.5 py-2.5 dark:border-neutral-700\">\n {quickReplies.map((q) => (\n <button\n key={q}\n onClick={() => send(q, { inputType: \"quick_reply\" })}\n className=\"xbnova-chip rounded-full border px-2.5 py-1 text-[11px] font-semibold transition\"\n >\n {q}\n </button>\n ))}\n </div>\n );\n })()}\n\n <form\n onSubmit={(e) => {\n e.preventDefault();\n setNudgeQuickReplies([]);\n setNudgePrompt(null);\n send(input, { nudgeOpening: messages.length > 0 ? nudgePrompt : null });\n setInput(\"\");\n }}\n className=\"flex items-center gap-2 border-t border-neutral-200 px-3 py-2.5 dark:border-neutral-700\"\n >\n <input\n value={input}\n onChange={(e) => setInput(e.target.value)}\n placeholder=\"이 차에 대해 무엇이든 물어보세요\"\n disabled={busy}\n className=\"xbnova-input-accent flex-1 rounded-full border border-neutral-300 bg-white px-3.5 py-2 text-sm text-neutral-900 placeholder:text-neutral-400 outline-none disabled:opacity-50 dark:border-neutral-600 dark:bg-neutral-800 dark:text-neutral-100 dark:placeholder:text-neutral-500\"\n />\n <button\n type=\"submit\"\n disabled={busy || !input.trim()}\n aria-label=\"전송\"\n className=\"xbnova-accent-btn flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-white transition disabled:opacity-40\"\n >\n ↑\n </button>\n </form>\n {/* 봇 설정의 AI 고지 문구. 대화 중에도 늘 보이도록 입력창 아래에 둔다. 비어 있으면 그리지 않는다. */}\n {disclaimer ? (\n <p className=\"-mt-1 px-4 pb-2.5 text-center text-[11px] leading-snug text-neutral-400 dark:text-neutral-500\">\n {disclaimer}\n </p>\n ) : null}\n </div>\n </XbNovaShadowRoot>\n </>\n );\n}\n\n/** AI 버튼·채팅 헤더의 동그란 아바타. 봇 설정(branding)에 이미지 주소가 있으면 이미지, 없고 글자가\n * 있으면 글자(이모지), 둘 다 없거나 이미지가 깨지면 기본 \"AI\" 배지. 호스트 사이트의 CSP가 외부 이미지를\n * 막거나 주소가 틀려도 빈 원이 되지 않게 onError로 글자 배지로 떨어진다.\n *\n * 깜빡임 방지: branding 은 서버에서 받아오고(`ready` 전), 이미지는 내려받는 시간이 걸린다. 그 사이에 \"AI\"\n * 글자를 먼저 보여주면 이미지로 바뀌며 깜빡이므로, 확정되기 전에는 **글자 없는 그라디언트 원**만 두고\n * 이미지는 다 받은 뒤 부드럽게 나타나게 한다. */\nfunction Avatar({\n branding,\n ready,\n size,\n className = \"\",\n gradient,\n}: {\n branding?: { logo_url?: string; avatar_text?: string };\n /** branding 이 확정됐는가(서버 응답 또는 캐시). false 면 글자·이미지 없이 빈 원만 그린다. */\n ready: boolean;\n /** 지름(px). 글자 배지의 글자 크기도 여기서 정해진다. */\n size: number;\n className?: string;\n gradient: React.CSSProperties;\n}) {\n const box: React.CSSProperties = { width: size, height: size };\n const logoUrl = branding?.logo_url?.trim() ?? \"\";\n const [failedUrl, setFailedUrl] = useState(\"\");\n const [loadedUrl, setLoadedUrl] = useState(\"\");\n const base = `${className} relative flex flex-none items-center justify-center overflow-hidden rounded-full font-bold text-white`;\n\n if (!ready) {\n return <span className={base} style={{ ...gradient, ...box }} aria-hidden />;\n }\n if (logoUrl && failedUrl !== logoUrl) {\n return (\n <span className={base} style={{ ...gradient, ...box }}>\n <img\n src={logoUrl}\n alt=\"\"\n onLoad={() => setLoadedUrl(logoUrl)}\n onError={() => setFailedUrl(logoUrl)}\n className=\"absolute inset-0 h-full w-full rounded-full bg-white object-cover transition-opacity duration-200\"\n style={{ opacity: loadedUrl === logoUrl ? 1 : 0 }}\n />\n </span>\n );\n }\n const text = branding?.avatar_text?.trim() || \"AI\";\n return (\n <span className={base} style={{ ...gradient, ...box, fontSize: Math.max(10, Math.round(size * 0.4)) }}>\n {text}\n </span>\n );\n}\n\nconst THUMB_UP_PATH =\n \"M7 10v12M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2a3.13 3.13 0 0 1 3 3.88Z\";\nconst THUMB_DOWN_PATH =\n \"M17 14V2M9 18.12 10 14H4.17a2 2 0 0 1-1.92-2.56l2.33-8A2 2 0 0 1 6.5 2H20a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-2.76a2 2 0 0 0-1.79 1.11L12 22a3.13 3.13 0 0 1-3-3.88Z\";\nconst COPY_PATH = \"M9 9h11v11H9zM5 15H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1\";\nconst CHECK_PATH = \"m5 12 5 5L20 7\";\n\nconst FEEDBACK_REASONS: { code: XbNovaFeedbackReason; label: string }[] = [\n { code: \"inaccurate\", label: \"정확하지 않아요\" },\n { code: \"irrelevant\", label: \"질문과 달라요\" },\n { code: \"too_long\", label: \"너무 길어요\" },\n { code: \"unclear\", label: \"이해하기 어려워요\" },\n];\n\nconst LONG_PRESS_MS = 420;\n\nfunction ActionIcon({ path, filled }: { path: string; filled?: boolean }) {\n return (\n <svg\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 24 24\"\n fill={filled ? \"currentColor\" : \"none\"}\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d={path} />\n </svg>\n );\n}\n\nasync function copyText(text: string): Promise<boolean> {\n try {\n await navigator.clipboard.writeText(text);\n return true;\n } catch {\n // clipboard API가 막힌 환경(http, iframe 등) — 임시 textarea 로 폴백.\n try {\n const ta = document.createElement(\"textarea\");\n ta.value = text;\n ta.style.position = \"fixed\";\n ta.style.opacity = \"0\";\n document.body.appendChild(ta);\n ta.select();\n const ok = document.execCommand(\"copy\");\n ta.remove();\n return ok;\n } catch {\n return false;\n }\n }\n}\n\n/**\n * 어시스턴트 답변 하나를 감싸 복사·👍·👎 액션을 붙인다.\n * - 마지막 답변: 아래에 아이콘 3개가 항상 보인다.\n * - 이전 답변: 평소엔 숨기고, 터치는 길게 누르면 / 마우스는 올리면 떠오르는 알약 메뉴로 보여준다.\n * - 👎를 누르면 사유 칩이 한 줄 나온다(안 골라도 된다).\n */\nfunction AnswerActions({\n text,\n rating,\n isLast,\n onRate,\n children,\n}: {\n text: string;\n rating: XbNovaFeedbackRating;\n isLast: boolean;\n onRate: (r: XbNovaFeedbackRating, reason?: XbNovaFeedbackReason) => void;\n children: React.ReactNode;\n}) {\n const rootRef = useRef<HTMLDivElement>(null);\n const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const [menuOpen, setMenuOpen] = useState(false);\n const [reasonOpen, setReasonOpen] = useState(false);\n const [copied, setCopied] = useState(false);\n const [entered, setEntered] = useState(false);\n\n const clearTimer = () => {\n if (timerRef.current) clearTimeout(timerRef.current);\n timerRef.current = null;\n };\n useEffect(() => clearTimer, []);\n\n // 메뉴가 열려 있으면 바깥을 건드릴 때 닫는다.\n useEffect(() => {\n if (!menuOpen) {\n setEntered(false);\n return;\n }\n const raf = requestAnimationFrame(() => setEntered(true));\n const onDown = (e: PointerEvent) => {\n if (!e.composedPath().includes(rootRef.current as EventTarget)) setMenuOpen(false);\n };\n document.addEventListener(\"pointerdown\", onDown, true);\n return () => {\n cancelAnimationFrame(raf);\n document.removeEventListener(\"pointerdown\", onDown, true);\n };\n }, [menuOpen]);\n\n const startPress = (e: React.PointerEvent) => {\n if (e.pointerType === \"mouse\" || isLast) return;\n clearTimer();\n timerRef.current = setTimeout(() => {\n setMenuOpen(true);\n if (typeof navigator.vibrate === \"function\") navigator.vibrate(10);\n }, LONG_PRESS_MS);\n };\n\n const handleCopy = async () => {\n if (await copyText(text)) {\n setCopied(true);\n setTimeout(() => setCopied(false), 1400);\n }\n };\n const handleRate = (kind: \"up\" | \"down\") => {\n if (rating === kind) {\n onRate(null);\n setReasonOpen(false);\n return;\n }\n onRate(kind);\n setReasonOpen(kind === \"down\");\n if (kind === \"up\") setMenuOpen(false);\n };\n\n const barVisible = isLast || menuOpen;\n const floating = menuOpen && !isLast;\n const btn = (label: string, onClick: () => void, selected: boolean, color: string | undefined, icon: React.ReactNode) => (\n <button\n type=\"button\"\n aria-label={label}\n aria-pressed={selected}\n onClick={onClick}\n className={`flex items-center justify-center gap-1 rounded-full text-neutral-400 transition-colors hover:text-neutral-600 dark:text-neutral-500 dark:hover:text-neutral-300 ${\n floating ? \"h-9 px-3 text-xs font-semibold\" : \"h-7 w-7\"\n }`}\n style={color ? { color } : undefined}\n >\n {icon}\n {floating ? <span>{label}</span> : null}\n </button>\n );\n\n return (\n <div\n ref={rootRef}\n className=\"group space-y-1\"\n onPointerDown={startPress}\n onPointerUp={clearTimer}\n onPointerCancel={clearTimer}\n onPointerLeave={clearTimer}\n onPointerMove={(e) => {\n if (Math.abs(e.movementX) + Math.abs(e.movementY) > 6) clearTimer();\n }}\n onContextMenu={(e) => {\n // 터치 길게 누르기가 브라우저 기본 메뉴를 띄우지 않게 한다 — 마우스 우클릭은 그대로 둔다.\n if (!isLast && (e.nativeEvent as PointerEvent).pointerType !== \"mouse\") e.preventDefault();\n }}\n >\n {children}\n <div\n className={`flex pl-1 ${barVisible ? \"\" : \"hidden md:group-hover:flex\"}`}\n style={{ transition: \"opacity 140ms, transform 140ms\", opacity: floating && !entered ? 0 : 1, transform: floating && !entered ? \"scale(0.94)\" : \"none\" }}\n >\n <div\n className={`flex items-center ${\n floating\n ? \"gap-0.5 rounded-full border border-neutral-200 bg-white p-1 shadow-lg dark:border-neutral-700 dark:bg-neutral-800\"\n : \"gap-0.5\"\n }`}\n >\n {btn(copied ? \"복사됨\" : \"복사\", handleCopy, false, copied ? \"var(--xbnova-accent-1, #4f46e5)\" : undefined, (\n <ActionIcon path={copied ? CHECK_PATH : COPY_PATH} />\n ))}\n {btn(\n \"좋아요\",\n () => handleRate(\"up\"),\n rating === \"up\",\n rating === \"up\" ? \"var(--xbnova-accent-1, #4f46e5)\" : undefined,\n <ActionIcon path={THUMB_UP_PATH} filled={rating === \"up\"} />,\n )}\n {btn(\n \"별로예요\",\n () => handleRate(\"down\"),\n rating === \"down\",\n rating === \"down\" ? \"#dc2626\" : undefined,\n <ActionIcon path={THUMB_DOWN_PATH} filled={rating === \"down\"} />,\n )}\n </div>\n </div>\n {rating === \"down\" && reasonOpen ? (\n <div className=\"flex flex-wrap items-center gap-1.5 pl-1 pt-0.5\">\n <span className=\"text-[11px] text-neutral-400\">어떤 점이 아쉬웠나요?</span>\n {FEEDBACK_REASONS.map((r) => (\n <button\n key={r.code}\n type=\"button\"\n onClick={() => {\n onRate(\"down\", r.code);\n setReasonOpen(false);\n setMenuOpen(false);\n }}\n className=\"xbnova-chip rounded-full border px-2.5 py-1 text-[11px] font-semibold transition\"\n >\n {r.label}\n </button>\n ))}\n </div>\n ) : null}\n </div>\n );\n}\n\nfunction Bubble({ role, children }: { role: \"user\" | \"assistant\"; children: React.ReactNode }) {\n const mine = role === \"user\";\n return (\n <div className={mine ? \"flex justify-end\" : \"flex justify-start\"}>\n <div\n className={`max-w-[85%] rounded-2xl px-3.5 py-2 text-sm ${\n mine ? \"whitespace-pre-wrap\" : \"\"\n } ${\n mine\n ? \"xbnova-accent-btn text-white\"\n : \"bg-neutral-100 text-neutral-800 dark:bg-neutral-800 dark:text-neutral-100\"\n }`}\n >\n {children}\n </div>\n </div>\n );\n}\n\n// 어시스턴트 텍스트만 마크다운으로 그린다 — 사용자 입력은 항상 순수 텍스트다(마크다운으로\n// 잘못 해석할 이유가 없고, 오히려 사용자가 실수로 친 `*`/`#`이 서식으로 바뀌면 이상하다).\n// remark-breaks: 모델이 문단 사이에 빈 줄 없이 한 줄바꿈만 쓰는 경우가 흔해서(CommonMark\n// 기본은 이걸 무시한다) 강제로 <br>로 살린다.\nfunction AssistantMarkdown({ text }: { text: string }) {\n return (\n <div className=\"space-y-1.5 text-sm leading-relaxed [&_ol]:list-decimal [&_ul]:list-disc [&_ol]:pl-5 [&_ul]:pl-5 [&_li]:pl-0.5 [&_strong]:font-semibold [&_a]:underline [&_code]:rounded [&_code]:bg-black/10 [&_code]:px-1 [&_code]:py-0.5 [&_code]:text-[0.85em] [&_p]:mb-1.5 last:[&_p]:mb-0\">\n <ReactMarkdown remarkPlugins={[remarkGfm, remarkBreaks]}>{text}</ReactMarkdown>\n </div>\n );\n}\n\n/** 액션을 못 붙이는 상태(스트리밍 중, id 없음)에선 기존과 같은 래퍼만 그린다. */\nfunction AnswerActionsFrame({\n enabled,\n children,\n ...rest\n}: {\n enabled: boolean;\n text: string;\n rating: XbNovaFeedbackRating;\n isLast: boolean;\n onRate: (r: XbNovaFeedbackRating, reason?: XbNovaFeedbackReason) => void;\n children: React.ReactNode;\n}) {\n if (!enabled) return <div className=\"space-y-1\">{children}</div>;\n return <AnswerActions {...rest}>{children}</AnswerActions>;\n}\n","\"use client\";\n\n/**\n * Shadow DOM 마운트 — 고객사 페이지 CSS와 위젯 스타일을 서로 못 건드리게 격리한다\n * (Intercom·Drift 같은 임베드형 위젯 방식, SDK.md \"스타일 격리\" 참고).\n *\n * 컴파일된 CSS(`dist/style.css`, 빌드 시 Tailwind CLI가 만든다)를 `import.meta.url` 기준\n * 상대 경로로 fetch해 shadow root 안 `<style>`에 주입한다 — 그래서 소비자는\n * `import \"@xbrain-swe/nova-chat-widget/style.css\"`를 따로 안 해도 된다. 같은 CSS를 여러 위젯\n * 인스턴스(채팅 + 넛지 버블)가 동시에 쓰므로 fetch 결과를 모듈 스코프에서 캐시한다.\n *\n * 호스트 요소를 `document.body`로 포탈한다 — `<XbNovaProvider>`가 고객사 페이지 트리\n * 깊숙이 들어가 있으면(실제로 ctr-client가 그렇다) 조상 중 하나가 `overflow: clip`이나\n * `transform`을 걸어뒀을 때 `position: fixed`가 escape 못 하고 잘리거나 엉뚱한 곳에\n * 고정될 수 있다 — `overflow: clip`은 `hidden`과 달리 fixed 자손까지 잘라내는 게\n * 스펙이다. body 바로 아래로 포탈하면 이 문제 자체가 생기지 않는다(Intercom 등 실제\n * 임베드 위젯이 쓰는 방식과 같다).\n */\n\nimport { useEffect, useRef, useState } from \"react\";\nimport { createPortal } from \"react-dom\";\n\nimport { type XbNovaTheme, useXbNovaInternal } from \"./internal-context\";\n\nlet cssPromise: Promise<string> | null = null;\n\nfunction loadCss(): Promise<string> {\n if (!cssPromise) {\n // CJS 빌드는 import.meta.url이 비어 있다(tsup 경고) — 그 경우 스타일 없이 조용히 넘어간다.\n // 최신 번들러(Next.js/Vite/webpack)는 전부 ESM을 골라 쓰므로 실제로는 거의 안 타는 경로다.\n try {\n cssPromise = fetch(new URL(\"./style.css\", import.meta.url))\n .then((r) => (r.ok ? r.text() : \"\"))\n .catch(() => \"\");\n } catch {\n cssPromise = Promise.resolve(\"\");\n }\n }\n return cssPromise;\n}\n\n/** `\"system\"`이면 브라우저 설정(prefers-color-scheme)을 구독해 실제 값으로 바꾼다. SSR/첫 렌더는\n * 항상 light — 마운트 뒤에 맞춘다(위젯은 마운트 후에야 그려지므로 깜빡임이 보이지 않는다). */\nfunction useResolvedScheme(scheme: XbNovaTheme[\"colorScheme\"]): \"light\" | \"dark\" {\n const [systemDark, setSystemDark] = useState(false);\n useEffect(() => {\n if (scheme !== \"system\") return;\n const mq = window.matchMedia(\"(prefers-color-scheme: dark)\");\n setSystemDark(mq.matches);\n const onChange = (e: MediaQueryListEvent) => setSystemDark(e.matches);\n mq.addEventListener(\"change\", onChange);\n return () => mq.removeEventListener(\"change\", onChange);\n }, [scheme]);\n if (scheme === \"system\") return systemDark ? \"dark\" : \"light\";\n return scheme;\n}\n\nexport function XbNovaShadowRoot({\n children,\n style,\n}: {\n children: React.ReactNode;\n /** 호스트(라이트 DOM) 요소의 위치. 반드시 인라인 style이어야 한다 — Tailwind 클래스\n * 문자열은 안 된다. 호스트는 Shadow DOM *밖*이라 SDK가 빌드 시점에 미리 컴파일해\n * shadow root 안에 주입하는 CSS(`dist/style.css`)가 아예 안 닿는다 — 페이지 자체\n * 스타일시트에 우연히 같은 클래스가 있지 않은 한 조용히 무시된다. 인라인 style은\n * 클래스 조회 자체가 없어서 이 문제를 원천적으로 피한다. 화면에 보이는 색상·둥근\n * 모서리 같은 건 `children`(shadow root 안쪽) 쪽 Tailwind 클래스로 준다 — 그건\n * SDK 소스에 정적으로 있어서 항상 컴파일된다. */\n style?: React.CSSProperties;\n}) {\n const { theme } = useXbNovaInternal();\n const scheme = useResolvedScheme(theme.colorScheme);\n const hostRef = useRef<HTMLDivElement>(null);\n const [mountNode, setMountNode] = useState<HTMLElement | null>(null);\n // document.body는 SSR에 없다 — 마운트 이후에만 포탈 타깃을 채운다(그 전까지는 렌더 안 함).\n const [portalTarget, setPortalTarget] = useState<HTMLElement | null>(null);\n\n useEffect(() => {\n setPortalTarget(document.body);\n }, []);\n\n useEffect(() => {\n const host = hostRef.current;\n if (!host || host.shadowRoot) return;\n\n const shadow = host.attachShadow({ mode: \"open\" });\n const style = document.createElement(\"style\");\n shadow.appendChild(style);\n loadCss().then((css) => {\n style.textContent = css;\n });\n\n const mount = document.createElement(\"div\");\n // 호스트(`position:fixed` + top/bottom 등)는 정의된 높이를 갖지만, 이 mount div는\n // 아무 스타일도 없는 평범한 div라 기본값(auto)은 \"내용물 높이\"다 — 그래서 안쪽 `h-full`\n // Tailwind 클래스가 기준 삼을 부모 높이가 없어 패널이 내용물 크기로 쪼그라들고, 그\n // 뒤로 페이지가 비쳐 보이는 버그가 있었다. 100%로 명시해 호스트 크기를 그대로 물려준다.\n mount.style.height = \"100%\";\n mount.style.width = \"100%\";\n shadow.appendChild(mount);\n setMountNode(mount);\n }, [portalTarget]);\n\n // styles.css 의 `dark:` 변형이 이 속성을 본다. color-scheme 은 입력창·스크롤바 같은 기본\n // 컨트롤 색을 맞춘다.\n useEffect(() => {\n if (!mountNode) return;\n mountNode.dataset.xbnovaScheme = scheme;\n mountNode.style.colorScheme = scheme;\n }, [mountNode, scheme]);\n\n if (!portalTarget) return null;\n\n return createPortal(\n <div ref={hostRef} style={style}>\n {mountNode ? createPortal(children, mountNode) : null}\n </div>,\n portalTarget,\n );\n}\n","\"use client\";\n\n/**\n * 헤드리스 채팅 훅 — `nova/web/components/chat.tsx`에서 상태·전송 로직만 뺀 것. 로직 자체는\n * 안 바꿨다(SDK.md). 추가된 건 두 가지: `context` 자동 채우기(메시지를 보낼 때 지금 보고\n * 있는 `XbNovaTrack` 영역을 자동으로 실어 보낸다)와 client action tool 디스패치다.\n *\n * client action 디스패치를 `chat.tsx`(UI)가 아니라 여기서 하는 이유: `messages`/\n * `setMessages`를 이미 들고 있고, `awaitResult: true`인 tool은 핸들러가 실제로 조회한\n * 결과를 서버로 다시 보내(`resumeToolResult`) 응답을 이어받아야 하는데 — 그 이어받은\n * 조각을 \"지금 그리고 있는 바로 그 assistant 메시지\"에 이어붙이려면 상태를 쥔 곳에서\n * 처리하는 게 자연스럽다. `chat.tsx`는 결과 렌더링만 신경 쓴다.\n */\n\nimport { useEffect, useRef, useState } from \"react\";\n\nimport {\n type ClientActionHandler,\n type WidgetEventName,\n collectContextEntries,\n resolveFactValue,\n useXbNovaInternal,\n} from \"./internal-context\";\nimport type { XbNovaClientActionResult } from \"./client-actions\";\nimport {\n type ChatMessage,\n type MessagePart,\n type StoredMessage,\n type ToolPart,\n type ViewKind,\n fromStoredMessages,\n resumeToolResult,\n streamChat,\n} from \"./stream\";\nimport { getFocusedTrackEntry } from \"./track\";\n\n/** awaitResult client action 핸들러의 기본 응답 대기 시간. `timeoutMs`로 액션별 override. */\nconst DEFAULT_CLIENT_ACTION_TIMEOUT_MS = 15000;\n\n/** 버튼을 그리는 client action 별 클릭 이벤트. 목록에 없는 버튼은 `cta_click` 으로 남긴다. */\nconst CTA_CLICK_EVENTS: Record<string, { event: WidgetEventName; ctaType: string }> = {\n suggest_consultation: { event: \"apply_AI_click\", ctaType: \"consult\" },\n suggest_finance_limit: { event: \"finance_AI_click\", ctaType: \"finance\" },\n};\n\nexport type ClientResults = Record<string, { view: ViewKind; output: unknown }>;\n\nexport type XbNovaFeedbackRating = \"up\" | \"down\" | null;\n\n/** 👎 사유 칩의 코드. */\nexport type XbNovaFeedbackReason = \"inaccurate\" | \"irrelevant\" | \"too_long\" | \"unclear\";\n\n/** 답변 만족도 저장 성공 후 호스트로 전달되는 이벤트. `rating: null`은 평가 취소. */\nexport type XbNovaFeedbackEvent = {\n conversationId: string;\n messageId: string;\n rating: XbNovaFeedbackRating;\n /** 👎일 때 사용자가 고른 사유 코드(없으면 undefined). */\n reason?: XbNovaFeedbackReason;\n};\n\nexport type XbNovaOnboarding = {\n /** 이어 열 대화가 없으면 null — 첫 메시지를 보낼 때 서버가 만든다(지연 생성). */\n conversation_id: string | null;\n /** 이어 연 대화의 이력. 새 대화면 빈 배열. */\n messages?: StoredMessage[];\n greeting: string;\n quick_replies: string[];\n branding: { title?: string; logo_url?: string; avatar_text?: string; badge_text?: string; disclaimer?: string };\n};\n\nexport type UseXbNovaChatResult = {\n onboarding: XbNovaOnboarding | null;\n /** 대화 맨 위에 그릴 첫 말풍선(넛지 문구·인사말). 이어 연 대화면 이력에 이미 있으니 null. */\n opening: string | null;\n messages: ChatMessage[];\n busy: boolean;\n error: string | null;\n /** `nudgeOpening`은 대화 중 넛지를 눌러 띄운 질문이다. 답을 보낼 때 그 앞에 AI 발화로 남긴다. */\n send: (text: string, options?: { nudgeOpening?: string | null; inputType?: \"typed\" | \"quick_reply\" }) => Promise<void>;\n /** client action 핸들러가 돌려준 정규화 결과(tool_call_id별). 채팅 화면이 view 렌더러로 그린다. */\n clientResults: ClientResults;\n /** messageId → 현재 평가. 평가 안 한 메시지는 키가 없다. */\n feedback: Record<string, XbNovaFeedbackRating>;\n /** 낙관적으로 반영하고 서버에 저장한다. 실패하면 되돌린다. */\n setFeedback: (messageId: string, rating: XbNovaFeedbackRating, reason?: XbNovaFeedbackReason) => Promise<void>;\n /** client action이 그린 버튼(상담신청 등)이 눌렸을 때 채팅 화면이 부른다 — nova 넛지 퍼널의 마지막 단계. */\n trackClientActionClick: (action: string, label: string | null) => void;\n};\n\n/** `botId`를 넘기면 Provider의 기본 봇 대신 그 봇으로 새 대화를 연다 — 넛지 handoff가 쓴다.\n * `nudgeNodeId`를 넘기면 이 대화가 그 넛지에서 시작됐다고 서버에 표시한다 — 넛지 전환\n * 퍼널(노출→클릭→첫 질문→상담 버튼 노출→클릭)이 이 값이 채워진 대화를 넛지 대화로 본다.\n * `onDismissSheet`는 `dismissSheet: true`인 client action이 실행될 때 호출된다 —\n * `<XbNovaChat>`이 시트를 닫는 함수를 넘겨준다(모바일에서 시트가 페이지 대부분을 덮어서,\n * 안 닫으면 그 밑에서 일어나는 일이 안 보인다). */\nexport function useXbNovaChat(options?: {\n botId?: string;\n nudgeNodeId?: string;\n /** 위젯이 첫 말풍선으로 보여준 문구(넛지 문구). 첫 메시지를 보낼 때 서버에 같이 보내 AI가\n * \"방금 이런 말을 건넸다\"는 걸 알게 한다 — 없으면 봇 인사말을 쓴다. 서버가 설정과 일치하는지\n * 검증해서 맞는 것만 이력에 저장한다. */\n openingMessage?: string;\n onDismissSheet?: () => void;\n onFeedback?: (e: XbNovaFeedbackEvent) => void;\n}): UseXbNovaChatResult {\n const { auth, trackRegistry, contextRegistry, clientActionHandlers, visitorId, sessionId, sendEvent } = useXbNovaInternal();\n const botId = options?.botId ?? auth.botId;\n const nudgeNodeId = options?.nudgeNodeId;\n const { apiKey, apiBaseUrl, threadKey } = auth;\n // 호스트 유저 키가 있으면 visitorId가 곧 그 값이다(internal-context).\n const userKey = visitorId;\n\n const [onboarding, setOnboarding] = useState<XbNovaOnboarding | null>(null);\n const [messages, setMessages] = useState<ChatMessage[]>([]);\n // 이어 연 대화인지 — 그렇다면 첫 말풍선은 이력에 이미 있고, 넛지를 누르면 이력 아래에 붙인다.\n const [resumed, setResumed] = useState(false);\n // 첫 메시지를 보낼 때 실제로 서버에 보낸 첫 말풍선 — 그 뒤로 넛지를 또 눌러도 맨 위 말풍선은 바뀌지 않는다.\n const [sentOpening, setSentOpening] = useState<string | null>(null);\n // 대화 id는 state(onboarding)와 별도로 ref에도 둔다 — 지연 생성이면 첫 스트림의 응답 헤더로 받는데,\n // 같은 스트림 안에서 디스패치되는 client action이 리렌더를 기다리지 않고 그 id를 써야 한다.\n const conversationIdRef = useRef<string | null>(null);\n // 대화가 이미 있는데 넛지를 눌러 이력 아래 붙인 문구 — 다음 메시지와 함께 서버에 보내 이력에 남긴다.\n const messagesRef = useRef(messages);\n messagesRef.current = messages;\n const [busy, setBusy] = useState(false);\n const [error, setError] = useState<string | null>(null);\n // tool_call_id → 클라이언트 핸들러가 돌려준 정규화 결과(view+output). 채팅 화면이 이 값으로 그린다.\n const [clientResults, setClientResults] = useState<ClientResults>({});\n const [feedback, setFeedbackState] = useState<Record<string, XbNovaFeedbackRating>>({});\n const feedbackRef = useRef(feedback);\n feedbackRef.current = feedback;\n\n // tool_call_id별로 한 번만 디스패치한다 — `messages`가 스트리밍 중 계속 새 배열로\n // 갈아끼워지므로(참조가 매번 바뀜) 이 ref 없이는 리렌더마다 다시 불릴 수 있다.\n const dispatchedClientActionsRef = useRef<Set<string>>(new Set());\n\n // 마운트 시점의 nudgeNodeId만 쓴다 — effect의 의존성이 아니다. 대화가 이미 열려 있는데\n // 다른 넛지 힌트를 눌러 다시 열면(같은 <XbNovaChat> 인스턴스, activeNudgeNodeId만 새\n // 값으로 바뀜) 여기 걸려 있으면 대화가 통째로 리셋되고 새 conversation이 생겨버린다 —\n // 서버 쪽 규칙(\"conversation_id가 이미 있으면 nudge_node_id는 무시된다\", schema/chat.py\n // 참고)과도 안 맞는다. nudgeNodeId는 \"새 대화를 열 때만\" 의미가 있다.\n const nudgeNodeIdRef = useRef(nudgeNodeId);\n nudgeNodeIdRef.current = nudgeNodeId;\n\n useEffect(() => {\n let cancelled = false;\n setOnboarding(null);\n setMessages([]);\n setResumed(false);\n setSentOpening(null);\n setError(null);\n setClientResults({});\n setFeedbackState({});\n dispatchedClientActionsRef.current = new Set();\n conversationIdRef.current = null;\n // 인사말·시작질문은 봇 설정에서 온다. 모델이 만들지 않는다.\n // `defer`: 이어 열 대화가 없으면 서버는 대화를 만들지 않는다 — 첫 메시지(`/v1/chat`)에서 만든다.\n // 위젯을 열기만 한 방문이 빈 대화를 남기지 않게 하려는 것.\n fetch(`${apiBaseUrl}/v1/conversations`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${apiKey}` },\n body: JSON.stringify({\n bot_id: botId,\n nudge_node_id: nudgeNodeIdRef.current ?? null,\n visitor_id: userKey || null,\n thread_key: threadKey ?? null,\n session_id: sessionId,\n defer: true,\n }),\n })\n .then((r) => (r.ok ? r.json() : Promise.reject(new Error(`${r.status}`))))\n .then((data: XbNovaOnboarding) => {\n if (cancelled) return;\n // 응답이 오기 전에 사용자가 먼저 메시지를 보냈으면(`/v1/chat` 이 대화를 만들고 헤더로 id 를 줌)\n // 그 대화를 그대로 쓴다. 여기서 id·이력을 덮어쓰면 방금 주고받은 대화가 지워지고 다음 메시지가\n // 다른 대화로 갈라진다.\n if (conversationIdRef.current || messagesRef.current.length > 0) {\n setOnboarding(data);\n return;\n }\n conversationIdRef.current = data.conversation_id;\n const restored = fromStoredMessages(data.messages ?? []);\n // 이미 끝난 대화의 tool 호출은 다시 디스패치하지 않는다.\n for (const m of restored) {\n for (const p of m.parts) if (p.kind === \"tool\") dispatchedClientActionsRef.current.add(p.toolCallId);\n }\n setMessages(restored);\n setResumed(restored.length > 0);\n setOnboarding(data);\n })\n .catch((e) => {\n if (!cancelled) setError(`봇을 불러올 수 없습니다: ${e.message}`);\n });\n return () => {\n cancelled = true;\n };\n // nudgeNodeId는 의도적으로 의존성에서 뺐다 — 위 nudgeNodeIdRef 설명 참고.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [apiBaseUrl, apiKey, botId, userKey, threadKey]);\n\n const appendToLastAssistant = (existingParts: MessagePart[], newParts: MessagePart[]) =>\n setMessages((prev) => {\n const next = [...prev];\n next[next.length - 1] = { ...next[next.length - 1], role: \"assistant\", parts: [...existingParts, ...newParts] };\n return next;\n });\n\n // 마지막 assistant 메시지에 서버가 부여한 id를 붙인다(재개 스트림이면 최신 id로 갱신).\n const setLastAssistantId = (id: string) =>\n setMessages((prev) => {\n if (prev.length === 0) return prev;\n const next = [...prev];\n next[next.length - 1] = { ...next[next.length - 1], id };\n return next;\n });\n\n async function setFeedback(messageId: string, rating: XbNovaFeedbackRating, reason?: XbNovaFeedbackReason) {\n const conversationId = conversationIdRef.current;\n if (!conversationId || !messageId) return;\n const prevRating = feedbackRef.current[messageId] ?? null;\n const apply = (r: XbNovaFeedbackRating) =>\n setFeedbackState((prev) => {\n const next = { ...prev };\n if (r === null) delete next[messageId];\n else next[messageId] = r;\n return next;\n });\n apply(rating);\n try {\n const res = await fetch(\n `${apiBaseUrl}/v1/conversations/${encodeURIComponent(conversationId)}/messages/${encodeURIComponent(messageId)}/feedback`,\n {\n method: \"PUT\",\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${apiKey}` },\n body: JSON.stringify({ rating, visitor_id: visitorId, reason: rating === \"down\" ? (reason ?? null) : null }),\n },\n );\n if (!res.ok) throw new Error(`${res.status}`);\n sendEvent(\"feedback_submit\", { conversation_id: conversationId, message_id: messageId, rating, reason });\n options?.onFeedback?.({ conversationId, messageId, rating, reason });\n } catch (e) {\n console.warn(\"[xbnova] 피드백 저장 실패\", e);\n apply(prevRating);\n }\n }\n\n // client action 버튼의 노출·클릭을 nova에 남긴다(넛지 퍼널의 \"상담 버튼 노출→클릭\"). 넛지 ID·봇은 서버가\n // 대화에서 찾아 채우므로 대화 id만 있으면 된다. 분석 이벤트라 실패해도 조용히 버린다.\n function postClientActionEvent(\n event: WidgetEventName,\n action: string,\n label: string | null,\n ctaType?: string,\n ) {\n const conversationId = conversationIdRef.current;\n if (!conversationId) return;\n sendEvent(event, { conversation_id: conversationId, action, label, cta_type: ctaType });\n }\n\n function trackClientActionClick(action: string, label: string | null) {\n const cta = CTA_CLICK_EVENTS[action];\n postClientActionEvent(cta?.event ?? \"cta_click\", action, label, cta?.ctaType);\n }\n\n async function runClientAction(spec: ClientActionHandler, part: ToolPart, existingParts: MessagePart[]) {\n // 핸들러 실행 *전에* 닫는다 — 핸들러가 스크롤·하이라이트 같은 걸 하면, 그게 실행되는\n // 순간 시트가 이미 없어야 사용자 눈에 보인다(늦게 닫으면 애니메이션을 놓친다).\n if (spec.dismissSheet) options?.onDismissSheet?.();\n\n let result: void | string | XbNovaClientActionResult;\n try {\n if (!spec.awaitResult) {\n await spec.handler(part.input);\n return;\n }\n // 핸들러가 영영 안 끝나면 busy가 풀리지 않고 대화가 멈춘다(서버도 타임아웃이 없다).\n // 타임아웃이 먼저 정착하면 Promise.race가 거기서 끝나므로, 뒤늦은 핸들러 결과는\n // 버려지고 tool 결과가 두 번 전송되지 않는다.\n const timeoutMs = spec.timeoutMs ?? DEFAULT_CLIENT_ACTION_TIMEOUT_MS;\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timeout = new Promise<string>((resolve) => {\n timer = setTimeout(() => resolve(\"오류: 응답 시간이 초과되었습니다.\"), timeoutMs);\n });\n try {\n result = await Promise.race([Promise.resolve(spec.handler(part.input)), timeout]);\n } finally {\n clearTimeout(timer);\n }\n } catch (e) {\n if (!spec.awaitResult) return; // fire-and-forget — 실패해도 대화 자체엔 영향 없다.\n result = e instanceof Error ? `오류: ${e.message}` : \"오류가 발생했습니다.\";\n }\n if (!spec.awaitResult) return;\n\n const conversationId = conversationIdRef.current;\n if (!conversationId) return; // 이 tool을 부른 스트림의 응답 헤더로 이미 받았다 — 방어적으로만 체크한다.\n\n // 구조화된 결과면 text만 모델에 보내고, view+output은 이 tool 호출 자리에 그릴 수 있게 보관한다.\n const structured = result && typeof result === \"object\" ? result : null;\n if (structured?.view) {\n setClientResults((prev) => ({\n ...prev,\n [part.toolCallId]: { view: structured.view as ViewKind, output: structured.output },\n }));\n }\n const rawText = structured ? structured.text : result;\n const resultText = typeof rawText === \"string\" && rawText.trim() ? rawText : \"핸들러가 결과를 반환하지 않았습니다.\";\n setBusy(true);\n try {\n await resumeToolResult(\n apiBaseUrl,\n apiKey,\n {\n bot_id: botId,\n conversation_id: conversationId,\n tool_call_id: part.toolCallId,\n result_text: resultText,\n },\n {\n onPart: (newParts) => appendToLastAssistant(existingParts, newParts),\n onMessageId: setLastAssistantId,\n onError: setError,\n },\n );\n } catch (e) {\n setError(e instanceof Error ? e.message : String(e));\n } finally {\n setBusy(false);\n }\n }\n\n // client action tool 호출이 나타나면(입력이 확정되는 즉시) 디스패치한다 — output을\n // 기다리지 않는다. awaitResult tool은 애초에 output 이벤트가 안 온다(서버가 거기서\n // 턴을 멈춘다 — nova/api의 loop.py 참고). 재개 응답이 또 다른 client action을 부르면\n // 이 effect가 그 새 tool part도 그대로 다시 잡아 디스패치한다(체이닝 자연 지원).\n useEffect(() => {\n if (clientActionHandlers.size === 0) return;\n const last = messages[messages.length - 1];\n if (!last || last.role !== \"assistant\") return;\n for (const part of last.parts) {\n if (part.kind !== \"tool\" || part.state !== \"input\") continue;\n const spec = clientActionHandlers.get(part.toolName);\n if (!spec) continue;\n if (dispatchedClientActionsRef.current.has(part.toolCallId)) continue;\n dispatchedClientActionsRef.current.add(part.toolCallId);\n // 화면에 버튼을 그리는 액션만 \"노출\"이다. 이어 연 대화의 지난 호출은 위에서 디스패치 목록에 미리\n // 넣어 두므로 여기 오지 않는다 — 다시 열 때마다 노출이 중복으로 쌓이지 않는다.\n if (spec.render) postClientActionEvent(\"cta_impression\", part.toolName, null);\n void runClientAction(spec, part, last.parts);\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [messages, clientActionHandlers, onboarding, apiBaseUrl, apiKey, botId]);\n\n async function send(\n text: string,\n { nudgeOpening, inputType = \"typed\" }: { nudgeOpening?: string | null; inputType?: \"typed\" | \"quick_reply\" } = {},\n ) {\n const trimmed = text.trim();\n if (!trimmed || busy) return;\n // 내용이 아니라 길이만 보낸다 — 대화 원문은 분석 이벤트로 나가지 않는다.\n sendEvent(\"message_send\", {\n conversation_id: conversationIdRef.current,\n is_first: messages.length === 0,\n length: trimmed.length,\n });\n // 첫 질문 이벤트는 대화 ID 가 필수다. 대화는 첫 메시지 때 서버가 만드므로 ID 를 받은 뒤 보낸다.\n let firstQuestionPending = messages.length === 0;\n const reportFirstQuestion = (conversationId: string) => {\n if (!firstQuestionPending) return;\n firstQuestionPending = false;\n sendEvent(\"question_AI_first\", { conversation_id: conversationId, input_type: inputType });\n };\n if (conversationIdRef.current) reportFirstQuestion(conversationIdRef.current);\n setError(null);\n setBusy(true);\n // 대화 중 넛지 질문에 답하면 질문을 이력에 남긴다. 서버도 `opening_message`로 받아 넛지 문구인지 확인한 뒤\n // 저장한다 — 그래야 AI가 \"응 해줘\"가 무엇에 대한 답인지 안다.\n const resumedOpening = messages.length > 0 && nudgeOpening ? nudgeOpening : null;\n setMessages((prev) => [\n ...prev,\n ...(resumedOpening\n ? [{ role: \"assistant\" as const, parts: [{ kind: \"text\" as const, id: `nudge:${prev.length}`, text: resumedOpening }] }]\n : []),\n { role: \"user\", parts: [{ kind: \"text\", id: \"u\", text: trimmed }] },\n { role: \"assistant\", parts: [] },\n ]);\n\n const replaceLast = (parts: MessagePart[]) =>\n setMessages((prev) => {\n const next = [...prev];\n next[next.length - 1] = { ...next[next.length - 1], role: \"assistant\", parts };\n return next;\n });\n\n const focused = getFocusedTrackEntry(trackRegistry);\n const notes = collectContextEntries(contextRegistry);\n const screen = focused ? { region: focused.region, data: resolveFactValue(focused.data) } : {};\n // `useXbNovaContext`로 등록된 부가 설명은 포커스된 영역이 없어도 실린다.\n const context = focused || notes.length ? { ...screen, ...(notes.length ? { notes } : {}) } : undefined;\n\n // 첫 메시지면 맨 위 말풍선을, 대화 중이면 답한 넛지 질문을 함께 보낸다.\n let openingMessage: string | null;\n if (messages.length === 0) {\n openingMessage = (options?.openingMessage ?? onboarding?.greeting) || null;\n setSentOpening(openingMessage);\n } else {\n openingMessage = resumedOpening;\n }\n\n try {\n await streamChat(\n apiBaseUrl,\n apiKey,\n {\n bot_id: botId,\n message: trimmed,\n conversation_id: conversationIdRef.current,\n context,\n nudge_node_id: nudgeNodeId ?? null,\n opening_message: openingMessage,\n visitor_id: userKey || null,\n thread_key: threadKey ?? null,\n session_id: sessionId,\n },\n {\n onPart: replaceLast,\n onMessageId: setLastAssistantId,\n onConversationId: (id) => {\n conversationIdRef.current = id;\n reportFirstQuestion(id);\n },\n onError: setError,\n },\n );\n } catch (e) {\n setError(e instanceof Error ? e.message : String(e));\n } finally {\n setBusy(false);\n }\n }\n\n // 대화가 시작되면 맨 위 말풍선은 그때 보낸 문구로 고정한다 — 뒤에 누른 넛지가 대화 위에 나타나면 안 된다.\n const opening = resumed\n ? null\n : messages.length > 0\n ? sentOpening\n : (options?.openingMessage ?? onboarding?.greeting ?? null);\n\n return {\n onboarding,\n opening,\n messages,\n busy,\n error,\n send,\n clientResults,\n feedback,\n setFeedback,\n trackClientActionClick,\n };\n}\n","/**\n * AI SDK UI Message Stream v1 파서. `nova/web/lib/chat-stream.ts`에서 그대로 옮겼다 — 이미\n * 콘솔에서 검증된 로직이라 새로 설계하지 않는다.\n *\n * `ai` 패키지에 의존하지 않는다 — 고객사 번들에 peer 버전 충돌을 만들지 않기 위해서다.\n */\n\nexport type ViewKind = \"product_list\" | \"product_detail\" | \"metric\" | \"choices\" | \"form\" | \"verdict\";\n\nexport type ToolPart = {\n kind: \"tool\";\n toolCallId: string;\n toolName: string;\n state: \"input\" | \"output\" | \"error\";\n input?: unknown;\n output?: unknown;\n /** 위젯은 tool 이름이 아니라 이 값으로 렌더러를 고른다. */\n view?: ViewKind | null;\n};\n\nexport type TextPart = { kind: \"text\"; id: string; text: string };\nexport type MessagePart = TextPart | ToolPart;\n\nexport type ChatMessage = {\n /** assistant 메시지가 서버에 저장될 때의 id(`start` 청크의 `messageId`). 만족도 피드백 대상 식별용. */\n id?: string;\n role: \"user\" | \"assistant\";\n parts: MessagePart[];\n};\n\ntype Handlers = {\n onPart: (parts: MessagePart[]) => void;\n /** `start` 청크로 받은 assistant messageId. 재개 스트림이면 최신 값으로 다시 불린다. */\n onMessageId?: (messageId: string) => void;\n /** 응답 헤더(`x-conversation-id`)의 대화 id. 본문보다 먼저 불린다 — 대화를 첫 메시지에서 만드는\n * 지연 생성이면 이 스트림 안에서 디스패치되는 client action이 이 id로 결과를 돌려보내야 한다. */\n onConversationId?: (conversationId: string) => void;\n onError?: (message: string) => void;\n onFinish?: () => void;\n};\n\n/** 서버에 저장된 메시지(`parts`는 AI SDK UIMessage.parts 원본) — 이어 연 대화의 이력. */\nexport type StoredMessage = {\n id: string;\n role: \"user\" | \"assistant\";\n sender?: string;\n parts: Record<string, unknown>[];\n};\n\n/** 저장된 parts → 위젯이 그리는 parts. 이미 끝난 대화라 tool은 결과가 있든 없든 \"끝남\"으로 둔다 —\n * `input`으로 두면 client action이 다시 디스패치되거나 진행 중 표시가 남는다. */\nexport function fromStoredMessages(stored: StoredMessage[]): ChatMessage[] {\n return stored.map((m) => ({\n id: m.role === \"assistant\" ? m.id : undefined,\n role: m.role === \"assistant\" ? \"assistant\" : \"user\",\n parts: m.parts.flatMap((p, i): MessagePart[] => {\n const type = String(p.type ?? \"\");\n if (type === \"text\") {\n const text = typeof p.text === \"string\" ? p.text : \"\";\n return text ? [{ kind: \"text\", id: `${m.id}:${i}`, text }] : [];\n }\n if (type.startsWith(\"tool-\")) {\n const meta = p.providerMetadata as { \"kr.dsauto.chat\"?: { view?: ViewKind | null } } | undefined;\n return [\n {\n kind: \"tool\",\n toolCallId: String(p.toolCallId ?? `${m.id}:${i}`),\n toolName: type.slice(\"tool-\".length),\n state: p.state === \"output-error\" ? \"error\" : \"output\",\n input: p.input,\n output: p.output,\n view: meta?.[\"kr.dsauto.chat\"]?.view ?? null,\n },\n ];\n }\n return [];\n }),\n }));\n}\n\n/** SSE 한 줄(`data: {...}`)을 파싱해 누적 중인 `parts`를 갱신한다. `streamChat`(새 질문)과\n * `resumeToolResult`(client action await_result 재개)가 같은 와이어 포맷(`tool-output-available`\n * 등)을 받으므로 이 루프를 공유한다 — 둘의 차이는 어느 URL/body로 fetch하느냐뿐이다. */\nasync function consumeSse(response: Response, handlers: Handlers): Promise<string | null> {\n if (!response.ok || !response.body) {\n handlers.onError?.(`요청 실패 (${response.status})`);\n return null;\n }\n const conversationId = response.headers.get(\"x-conversation-id\");\n if (conversationId) handlers.onConversationId?.(conversationId);\n\n const reader = response.body.getReader();\n const decoder = new TextDecoder();\n let buffer = \"\";\n\n // 순서를 보존하려고 배열로 들고, 갱신할 때 참조로 찾는다.\n const parts: MessagePart[] = [];\n const textIndex = new Map<string, number>();\n const toolIndex = new Map<string, number>();\n\n const emit = () => handlers.onPart([...parts]);\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value, { stream: true });\n\n const lines = buffer.split(\"\\n\\n\");\n buffer = lines.pop() ?? \"\";\n\n for (const chunk of lines) {\n const line = chunk.trim();\n if (!line.startsWith(\"data:\")) continue;\n const payload = line.slice(5).trim();\n if (payload === \"[DONE]\") continue;\n\n let event: Record<string, unknown>;\n try {\n event = JSON.parse(payload);\n } catch {\n continue;\n }\n\n switch (event.type) {\n case \"start\":\n if (typeof event.messageId === \"string\" && event.messageId) handlers.onMessageId?.(event.messageId);\n break;\n case \"text-start\": {\n const id = String(event.id);\n textIndex.set(id, parts.length);\n parts.push({ kind: \"text\", id, text: \"\" });\n emit();\n break;\n }\n case \"text-delta\": {\n const id = String(event.id);\n let at = textIndex.get(id);\n if (at === undefined) {\n at = parts.length;\n textIndex.set(id, at);\n parts.push({ kind: \"text\", id, text: \"\" });\n }\n (parts[at] as TextPart).text += String(event.delta ?? \"\");\n emit();\n break;\n }\n case \"tool-input-available\": {\n const id = String(event.toolCallId);\n toolIndex.set(id, parts.length);\n parts.push({\n kind: \"tool\",\n toolCallId: id,\n toolName: String(event.toolName),\n state: \"input\",\n input: event.input,\n });\n emit();\n break;\n }\n case \"tool-output-available\": {\n const id = String(event.toolCallId);\n const at = toolIndex.get(id);\n if (at !== undefined) {\n const meta = event.providerMetadata as { \"kr.dsauto.chat\"?: { view?: ViewKind } };\n const part = parts[at] as ToolPart;\n part.state = event.isError ? \"error\" : \"output\";\n part.output = event.output;\n part.view = meta?.[\"kr.dsauto.chat\"]?.view ?? null;\n emit();\n }\n break;\n }\n case \"error\":\n handlers.onError?.(String(event.errorText ?? \"알 수 없는 오류\"));\n break;\n case \"finish\":\n handlers.onFinish?.();\n break;\n }\n }\n }\n return conversationId;\n}\n\nexport async function streamChat(\n apiBaseUrl: string,\n apiKey: string,\n body: {\n bot_id: string;\n message: string;\n conversation_id?: string | null;\n context?: unknown;\n nudge_node_id?: string | null;\n opening_message?: string | null;\n visitor_id?: string | null;\n thread_key?: string | null;\n session_id?: string | null;\n },\n handlers: Handlers,\n): Promise<string | null> {\n const response = await fetch(`${apiBaseUrl}/v1/chat`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${apiKey}` },\n body: JSON.stringify(body),\n });\n return consumeSse(response, handlers);\n}\n\n/** client action(`awaitResult: true`)이 멈춰둔 턴을, 핸들러가 실제로 조회한 결과를 들고\n * 재개한다. 서버가 `tool_call_id`로 저장해둔 pending tool 호출을 찾아 이어가므로\n * `tool_name`/`input`은 안 보낸다(SDK.md의 \"클라이언트가 스펙의 진실 원천\"이라는 것과는\n * 별개로, 결과 자체를 클라이언트가 주장하게 두지 않기 위한 서버 쪽 안전장치). */\nexport async function resumeToolResult(\n apiBaseUrl: string,\n apiKey: string,\n body: {\n bot_id: string;\n conversation_id: string;\n tool_call_id: string;\n result_text: string;\n is_error?: boolean;\n },\n handlers: Handlers,\n): Promise<string | null> {\n const response = await fetch(`${apiBaseUrl}/v1/conversations/${body.conversation_id}/tool-results`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${apiKey}` },\n body: JSON.stringify(body),\n });\n return consumeSse(response, handlers);\n}\n","\"use client\";\n\n/**\n * view 렌더러. `nova/web/components/views.tsx`에서 그대로 옮겼다.\n *\n * 중요: 디스패치는 **tool 이름이 아니라 `view`** 로 한다. 이 규칙을 어기면 렌더러가\n * 고객사 tool 이름을 알아야 하고, 그 순간 \"고객사가 달라지면 MCP만 개발\" 이 깨진다.\n */\n\nimport type { ToolPart, ViewKind } from \"./stream\";\n\nexport type ProductItem = {\n id: string;\n title: string;\n subtitle?: string;\n price?: { amount: number | null; currency: string };\n imageUrl?: string | null;\n badges?: string[];\n attributes?: { label: string; value: string }[];\n href?: string | null;\n description?: string | null;\n};\n\nexport type VerdictAction = {\n label: string;\n /** primary(기본, 채운 버튼) | ghost(테두리만) — 기획서 4.2의 [상담신청]/[성능점검 보기] 처럼 하나는 강조, 나머지는 보조로 두고 싶을 때 쓴다. */\n kind?: \"primary\" | \"ghost\";\n /** 카통령 등 소비자 쪽 동작을 가리키는 자유 문자열 — SDK는 뜻을 모른다, `onVerdictAction`으로 그대로 넘긴다. */\n action: string;\n};\n\nexport type VerdictOutput = {\n /** 카드 상단 라벨 — 기본 \"조건부 검토\". */\n headline?: string;\n conclusion: string;\n evidence: string;\n /** 데이터로 답할 수 없는 것 — 기획서 4.2 ③: 비워두지 말고 최소 1개 이상 밝히도록 유도한다. */\n unknowns?: string[];\n actions?: VerdictAction[];\n};\n\nexport type ViewRenderer = (props: {\n output: unknown;\n onVerdictAction?: (action: string) => void;\n}) => React.ReactNode;\nexport type ViewRendererMap = Partial<Record<ViewKind, ViewRenderer>>;\n\nfunction formatPrice(price?: { amount: number | null }) {\n if (!price?.amount) return \"가격문의\";\n return `${(price.amount / 10000).toLocaleString()}만원`;\n}\n\nfunction ProductCard({ item, detailed = false }: { item: ProductItem; detailed?: boolean }) {\n return (\n <a\n href={item.href ?? undefined}\n target=\"_blank\"\n rel=\"noreferrer\"\n className=\"flex w-64 shrink-0 flex-col overflow-hidden rounded-xl border border-neutral-200 bg-white transition hover:shadow-md dark:border-neutral-700 dark:bg-neutral-900\"\n >\n {item.imageUrl ? (\n // eslint-disable-next-line @next/next/no-img-element\n <img src={item.imageUrl} alt={item.title} className=\"h-36 w-full object-cover\" />\n ) : (\n <div className=\"flex h-36 w-full items-center justify-center bg-neutral-100 text-xs text-neutral-400 dark:bg-neutral-800\">\n 이미지 없음\n </div>\n )}\n <div className=\"flex flex-1 flex-col gap-1 p-3\">\n <div className=\"text-sm font-semibold\">{item.title}</div>\n <div className=\"text-xs text-neutral-500\">{item.subtitle}</div>\n <div className=\"xbnova-accent-price mt-1 text-base font-bold\">{formatPrice(item.price)}</div>\n {item.badges?.length ? (\n <div className=\"mt-1 flex flex-wrap gap-1\">\n {item.badges.map((b) => (\n <span\n key={b}\n className=\"rounded bg-neutral-100 px-1.5 py-0.5 text-[11px] text-neutral-600 dark:bg-neutral-800 dark:text-neutral-300\"\n >\n {b}\n </span>\n ))}\n </div>\n ) : null}\n {detailed && item.attributes?.length ? (\n <dl className=\"mt-2 grid grid-cols-2 gap-x-2 gap-y-1 text-[11px]\">\n {item.attributes.map((a) => (\n <div key={a.label} className=\"flex gap-1\">\n <dt className=\"text-neutral-400\">{a.label}</dt>\n <dd className=\"text-neutral-700 dark:text-neutral-300\">{a.value}</dd>\n </div>\n ))}\n </dl>\n ) : null}\n </div>\n </a>\n );\n}\n\nfunction ProductList({ output }: { output: unknown }) {\n const data = output as { items?: ProductItem[]; total?: number } | null;\n if (!data?.items?.length) return <Fallback label=\"검색 결과 없음\" />;\n return (\n <div className=\"my-2\">\n <div className=\"mb-1 text-xs text-neutral-500\">총 {data.total?.toLocaleString()}건</div>\n <div className=\"flex gap-2 overflow-x-auto pb-2\">\n {data.items.map((item) => (\n <ProductCard key={item.id} item={item} />\n ))}\n </div>\n </div>\n );\n}\n\nfunction ProductDetail({ output }: { output: unknown }) {\n const data = output as { item?: ProductItem } | null;\n if (!data?.item) return <Fallback label=\"매물 없음\" />;\n return (\n <div className=\"my-2\">\n <ProductCard item={data.item} detailed />\n </div>\n );\n}\n\n/** 판단형 질문에 대한 답변의 고정 구조(기획서 4.2) — 결론 → 근거 → 확인 불가 → 다음 행동.\n * 실제 데이터는 tool이 채운다(SDK는 렌더만 한다) — 이 view를 실제로 채우는 tool은 아직\n * 고객사마다 따로 만들어야 한다. */\nfunction Verdict({\n output,\n onVerdictAction,\n}: {\n output: unknown;\n onVerdictAction?: (action: string) => void;\n}) {\n const data = output as VerdictOutput | null;\n if (!data?.conclusion) return <Fallback label=\"판단 결과 없음\" />;\n return (\n <div className=\"my-2 max-w-[92%] overflow-hidden rounded-xl border border-amber-200 bg-white text-xs dark:border-amber-900 dark:bg-neutral-900\">\n <div className=\"border-b border-amber-200 bg-amber-50 px-3 py-1.5 font-bold text-amber-800 dark:border-amber-900 dark:bg-amber-950 dark:text-amber-300\">\n △ {data.headline ?? \"조건부 검토\"}\n </div>\n <div className=\"flex gap-2 border-b border-neutral-100 px-3 py-1.5 dark:border-neutral-800\">\n <span className=\"w-10 shrink-0 pt-px font-semibold text-neutral-400\">결론</span>\n <span className=\"text-neutral-800 dark:text-neutral-100\">{data.conclusion}</span>\n </div>\n <div className=\"flex gap-2 border-b border-neutral-100 px-3 py-1.5 dark:border-neutral-800\">\n <span className=\"w-10 shrink-0 pt-px font-semibold text-neutral-400\">근거</span>\n <span className=\"text-neutral-600 dark:text-neutral-300\">{data.evidence}</span>\n </div>\n {data.unknowns?.length ? (\n <div className=\"flex gap-2 border-b border-neutral-100 px-3 py-1.5 dark:border-neutral-800\">\n <span className=\"w-10 shrink-0 pt-px font-semibold text-neutral-400\">확인 불가</span>\n <span className=\"text-neutral-500\">{data.unknowns.join(\" · \")}</span>\n </div>\n ) : null}\n {data.actions?.length ? (\n <div className=\"flex flex-wrap gap-1.5 p-2\">\n {data.actions.map((action) => (\n <button\n key={action.label}\n type=\"button\"\n onClick={() => onVerdictAction?.(action.action)}\n className={\n action.kind === \"ghost\"\n ? \"xbnova-accent-btn-ghost rounded-md border px-2.5 py-1 text-[11px] font-semibold\"\n : \"xbnova-accent-btn rounded-md px-2.5 py-1 text-[11px] font-semibold text-white\"\n }\n >\n {action.label}\n </button>\n ))}\n </div>\n ) : null}\n </div>\n );\n}\n\nexport function Fallback({ label }: { label: string }) {\n return (\n <div className=\"my-2 rounded-lg border border-dashed border-neutral-300 px-3 py-2 text-xs text-neutral-500 dark:border-neutral-700\">\n {label}\n </div>\n );\n}\n\nconst DEFAULT_RENDERERS: ViewRendererMap = {\n product_list: ProductList,\n product_detail: ProductDetail,\n verdict: Verdict,\n};\n\nexport function ToolView({\n part,\n viewRenderers,\n onVerdictAction,\n}: {\n part: ToolPart;\n viewRenderers?: ViewRendererMap;\n onVerdictAction?: (action: string) => void;\n}) {\n if (part.state === \"input\") {\n return <Fallback label={`${part.toolName} 조회 중…`} />;\n }\n if (part.state === \"error\") {\n return <Fallback label={`${part.toolName} 실패`} />;\n }\n const Renderer = part.view ? (viewRenderers?.[part.view] ?? DEFAULT_RENDERERS[part.view]) : undefined;\n if (!Renderer) {\n // 모르는 view는 조용히 접어둔다 — 표준 MCP 서버를 그냥 붙여도 대화는 동작해야 한다.\n return <Fallback label={`${part.toolName} 결과 (view: ${part.view ?? \"없음\"})`} />;\n }\n return <Renderer output={part.output} onVerdictAction={onVerdictAction} />;\n}\n","\"use client\";\n\n/**\n * 넛지 조건이 참조하는 \"facts\" 중 커스텀 값 등록 훅. 내장 facts(scroll_depth 등)는\n * `built-in-facts.ts`가 따로 계산한다 — 여기는 고객사가 아는 값만 다룬다.\n */\n\nimport { useEffect, useId, useRef } from \"react\";\n\nimport { type FactValue, type XbNovaContextEntry, useXbNovaInternal } from \"./internal-context\";\n\n/** 커스텀 값 하나를 조건에서 쓸 수 있게 등록한다. 값 또는 함수(pull) 둘 다 된다. */\nexport function useXbNovaFact(key: string, value: FactValue): void {\n const { factsRegistry } = useXbNovaInternal();\n\n useEffect(() => {\n factsRegistry.set(key, value);\n });\n\n useEffect(() => {\n return () => {\n factsRegistry.delete(key);\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [key, factsRegistry]);\n}\n\n/** 필드가 여러 개인 객체 하나를 한 번에 등록한다 — 매물 데이터처럼. `useXbNovaFact`를\n * 필드마다 반복하는 것과 같다. */\nexport function useXbNovaFacts(values: Record<string, FactValue>): void {\n const { factsRegistry } = useXbNovaInternal();\n const keysRef = useRef<string[]>([]);\n\n useEffect(() => {\n for (const [key, value] of Object.entries(values)) {\n factsRegistry.set(key, value);\n }\n keysRef.current = Object.keys(values);\n });\n\n useEffect(() => {\n return () => {\n for (const key of keysRef.current) {\n factsRegistry.delete(key);\n }\n };\n }, [factsRegistry]);\n}\n\n/** AI에게 알려줄 상품·화면 부가 설명을 등록한다. 넛지 조건용인 facts와 달리 **챗 요청의\n * `context`로 전송**되어 답변의 근거가 된다. 내용(가입 가능 여부 같은 판단 포함)은 호스트가\n * `description`에 문장으로 쓴다 — SDK는 형식 검증과 전달만 한다. 배열 또는 함수(pull) 둘 다\n * 된다. 컴포넌트가 언마운트되면 자동으로 빠진다. */\nexport function useXbNovaContext(entries: XbNovaContextEntry[] | (() => XbNovaContextEntry[])): void {\n const { contextRegistry } = useXbNovaInternal();\n const id = useId();\n\n useEffect(() => {\n contextRegistry.set(id, entries);\n });\n\n useEffect(() => {\n return () => {\n contextRegistry.delete(id);\n };\n }, [id, contextRegistry]);\n}\n\n/** 방문자 식별자 — 없으면 SDK가 즉시 만들어 localStorage에 고정한 뒤 그 값을 반환한다. */\nexport function useXbNovaVisitor(): string {\n const { visitorId } = useXbNovaInternal();\n return visitorId;\n}\n\n/** 선언형 — 서버 컴포넌트 트리에서 훅을 못 쓸 때. `<XbNovaFact id=\"xbnova_summary\" value=\"...\" />`. */\nexport function XbNovaFact({ id, value }: { id: string; value: FactValue }): null {\n useXbNovaFact(id, value);\n return null;\n}\n\n/** 선언형 — `useXbNovaFacts`의 컴포넌트 버전. */\nexport function XbNovaFacts({ values }: { values: Record<string, FactValue> }): null {\n useXbNovaFacts(values);\n return null;\n}\n"],"mappings":"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","names":["import_react","import_react","import_react","import_jsx_runtime","import_react","import_react","import_jsx_runtime","entry","import_jsx_runtime","import_react","import_react","import_jsx_runtime","style","import_react","import_jsx_runtime","import_jsx_runtime","ReactMarkdown","remarkGfm","remarkBreaks","import_react"]}
|