@xbrain-swe/nova-chat-widget 0.1.0 → 0.1.2

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.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../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":["\"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/nudge-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 children,\n}: {\n apiKey: string;\n botId: string;\n appId: string;\n screen?: string;\n apiBaseUrl?: 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 () => ({ apiKey, botId, appId, apiBaseUrl, screen }),\n [apiKey, botId, appId, apiBaseUrl, screen],\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 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\";\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 /** 실제로 실행할 동작 — 채팅에는 아무것도 안 그리고 이 함수만 호출된다. `payload`는 이\n * tool 호출의 input을 그대로 되돌려준다. `awaitResult:true`면 반환값(또는 resolve된\n * Promise)이 진짜 tool 결과 텍스트가 된다 — 비워두면 \"핸들러가 결과를 반환하지\n * 않았습니다\" 에러로 처리된다. */\n handler: (payload: unknown) => void | string | Promise<void | string>;\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, useContext, useMemo, useRef } from \"react\";\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\nexport type Auth = {\n apiKey: string;\n botId: string;\n appId: string;\n apiBaseUrl: string;\n screen?: string;\n};\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};\n\nexport const DEFAULT_THEME: XbNovaTheme = {\n accentFrom: \"#4f46e5\",\n accentTo: \"#7c3aed\",\n};\n\n/** tool 이름 → 실행부. `handler`의 반환값은 `awaitResult:true`일 때만 의미가 있다 — 진짜\n * tool 결과 텍스트가 돼서 서버로 다시 전송된다(`use-chat.ts`). */\nexport type ClientActionHandler = {\n handler: (payload: unknown) => void | string | Promise<void | string>;\n awaitResult: boolean;\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 /** `<XbNovaProvider clientActions={{...}}>`에서 등록한 것 — `use-chat.ts`가 client\n * action tool 호출을 받으면 이 맵에서 찾아 디스패치한다. */\n clientActionHandlers: Map<string, ClientActionHandler>;\n visitorId: string;\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\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\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 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\n const value = useMemo<InternalState>(\n () => ({\n auth,\n trackRegistry,\n factsRegistry,\n clientActionHandlers: clientActionHandlers ?? emptyHandlers,\n visitorId: visitorIdRef.current!,\n theme,\n }),\n [auth, trackRegistry, factsRegistry, clientActionHandlers, emptyHandlers, 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`를 마운트 시 한 번 불러오고, 이후 1초 간격으로 각\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\nconst EVAL_INTERVAL_MS = 1000;\nconst XBNOVA_EVENT_NAME = \"xbnova:event\";\nconst SESSION_COUNT_KEY_PREFIX = \"xbnova_hint_session_count:\";\n/** 이 시간(ms) 안에 스크롤 이벤트가 있었으면 \"아직 스크롤 중\"으로 보고 새 힌트를 띄우지\n * 않는다 — dwell 문턱(1초)과 비슷한 크기라 스크롤이 완전히 멎은 뒤에만 힌트가 나온다. */\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, visitorId } = 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 문턱(1초)을 넘어버릴 수\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 if (!graph) {\n console.debug(\"[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 fetch(`${apiBaseUrl}/v1/nudge-events`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n app_id: appId,\n nudge_node_id: messageNode.id,\n kind: \"nudge_impression\",\n bot_id: botId,\n visitor_id: visitorId,\n }),\n }).catch(() => {});\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 console.debug(`[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 console.debug(\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 console.debug(`[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(예: 1초)는 금방 다시 채워지는데, 힌트 자체는\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, visitorId],\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 { 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(parts: MessagePart[], clientActionHandlers: Map<string, ClientActionHandler>): boolean {\n return parts.some((part) => {\n if (part.kind === \"text\") 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\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\nexport function XbNovaChat({\n viewRenderers,\n botId,\n barStyle,\n panelStyle,\n backdropStyle,\n defaultOpen = false,\n idleText = \"AI에게 이 차 물어보기\",\n onVerdictAction,\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 /** 판단 카드의 행동 버튼을 눌렀을 때 — 실제 동작(상담신청 폼 열기, 섹션 스크롤 등)은\n * 소비자가 정한다. SDK는 버튼에 적힌 `action` 문자열을 그대로 넘겨줄 뿐 뜻을 모른다. */\n onVerdictAction?: (action: string) => void;\n}) {\n // client action 핸들러는 `<XbNovaChat>` prop이 아니라 `<XbNovaProvider clientActions={{...}}>`에\n // 등록한다 — 이 화면에서 여러 <XbNovaChat> 인스턴스가 떠도 같은 페이지 능력을 공유해야\n // 해서(넛지 handoff로 다른 봇과 열릴 때도 마찬가지) Provider 레벨 개념이다.\n const { auth, trackRegistry, factsRegistry, visitorId, clientActionHandlers, theme } = 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 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 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, messages, busy, error, send } = useXbNovaChat({\n botId: activeBotId ?? botId,\n nudgeNodeId: activeNudgeNodeId,\n onDismissSheet: () => setOpen(false),\n });\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\n // 넛지 전환 퍼널의 \"AI 클릭\" 단계 — 이 힌트를 눌러서 열었을 때만 기록한다(기본\n // 문구를 눌러 연 건 넛지 전환이 아니다).\n fetch(`${auth.apiBaseUrl}/v1/nudge-events`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n app_id: auth.appId,\n nudge_node_id: consumed.messageNodeId,\n kind: \"nudge_open\",\n bot_id: consumed.botId,\n visitor_id: visitorId,\n }),\n }).catch(() => {});\n } else {\n // 기본 바를 눌러 열었으면 넛지 맥락이 없다 — 이전 힌트의 버튼이 남아있으면 안 된다.\n setNudgeQuickReplies([]);\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 }\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 <span\n className=\"flex h-7 w-7 items-center justify-center rounded-full text-[11px] font-bold text-white shadow-md\"\n style={avatarGradientStyle}\n >\n AI\n </span>\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 const firstBubble = openingLine ?? onboarding?.greeting;\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={() => setOpen(false)} />\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 <span\n className=\"flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-[10px] font-bold text-white\"\n style={avatarGradientStyle}\n >\n AI\n </span>\n <div className=\"min-w-0 flex-1\">\n <div className=\"text-sm font-semibold\">{onboarding?.branding?.title ?? \"AI 동행\"}</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={() => setOpen(false)}\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 {firstBubble ? (\n <Bubble role=\"assistant\">\n <AssistantMarkdown text={firstBubble} />\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 <div className=\"space-y-1\">\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 const spec = clientActionHandlers.get(part.toolName);\n return spec?.render ? <div key={j}>{spec.render(part.input)}</div> : null;\n }\n return (\n <ToolView key={j} part={part} viewRenderers={viewRenderers} onVerdictAction={onVerdictAction} />\n );\n })}\n {!hasVisiblePart(message.parts, clientActionHandlers) && busy ? (\n <Bubble role=\"assistant\">\n <span className=\"text-neutral-400\">생각하는 중…</span>\n </Bubble>\n ) : null}\n </div>\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 if (nudgeQuickReplies.length > 0) {\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 {nudgeQuickReplies.map((q) => (\n <button\n key={q}\n onClick={() => {\n setNudgeQuickReplies([]);\n send(q);\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 );\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 send(input);\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 px-3.5 py-2 text-sm outline-none disabled:opacity-50 dark:border-neutral-600 dark:bg-neutral-800\"\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 </div>\n </XbNovaShadowRoot>\n </>\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","\"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/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\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\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 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 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 { type ClientActionHandler, resolveFactValue, useXbNovaInternal } from \"./internal-context\";\nimport { type ChatMessage, type MessagePart, type ToolPart, resumeToolResult, streamChat } from \"./stream\";\nimport { getFocusedTrackEntry } from \"./track\";\n\nexport type XbNovaOnboarding = {\n conversation_id: string;\n greeting: string;\n quick_replies: string[];\n branding: { title?: string };\n};\n\nexport type UseXbNovaChatResult = {\n onboarding: XbNovaOnboarding | null;\n messages: ChatMessage[];\n busy: boolean;\n error: string | null;\n send: (text: string) => Promise<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 onDismissSheet?: () => void;\n}): UseXbNovaChatResult {\n const { auth, trackRegistry, clientActionHandlers } = useXbNovaInternal();\n const botId = options?.botId ?? auth.botId;\n const nudgeNodeId = options?.nudgeNodeId;\n const { apiKey, apiBaseUrl } = auth;\n\n const [onboarding, setOnboarding] = useState<XbNovaOnboarding | null>(null);\n const [messages, setMessages] = useState<ChatMessage[]>([]);\n const [busy, setBusy] = useState(false);\n const [error, setError] = useState<string | null>(null);\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 setOnboarding(null);\n setMessages([]);\n setError(null);\n dispatchedClientActionsRef.current = new Set();\n // 인사말·시작질문은 봇 설정에서 온다. 모델이 만들지 않는다.\n fetch(`${apiBaseUrl}/v1/conversations`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${apiKey}` },\n body: JSON.stringify({ bot_id: botId, nudge_node_id: nudgeNodeIdRef.current ?? null }),\n })\n .then((r) => (r.ok ? r.json() : Promise.reject(new Error(`${r.status}`))))\n .then(setOnboarding)\n .catch((e) => setError(`봇을 불러올 수 없습니다: ${e.message}`));\n // nudgeNodeId는 의도적으로 의존성에서 뺐다 — 위 nudgeNodeIdRef 설명 참고.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [apiBaseUrl, apiKey, botId]);\n\n const appendToLastAssistant = (existingParts: MessagePart[], newParts: MessagePart[]) =>\n setMessages((prev) => {\n const next = [...prev];\n next[next.length - 1] = { role: \"assistant\", parts: [...existingParts, ...newParts] };\n return next;\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;\n try {\n result = await spec.handler(part.input);\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 = onboarding?.conversation_id;\n if (!conversationId) return; // 이론상 항상 있다 — 방어적으로만 체크한다.\n\n const resultText = typeof result === \"string\" && result.trim() ? result : \"핸들러가 결과를 반환하지 않았습니다.\";\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 { onPart: (newParts) => appendToLastAssistant(existingParts, newParts), onError: setError },\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 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) {\n const trimmed = text.trim();\n if (!trimmed || busy) return;\n setError(null);\n setBusy(true);\n setMessages((prev) => [\n ...prev,\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] = { role: \"assistant\", parts };\n return next;\n });\n\n const focused = getFocusedTrackEntry(trackRegistry);\n const context = focused ? { region: focused.region, data: resolveFactValue(focused.data) } : undefined;\n\n try {\n await streamChat(\n apiBaseUrl,\n apiKey,\n {\n bot_id: botId,\n message: trimmed,\n conversation_id: onboarding?.conversation_id ?? null,\n context,\n nudge_node_id: nudgeNodeId ?? null,\n },\n { onPart: replaceLast, onError: setError },\n );\n } catch (e) {\n setError(e instanceof Error ? e.message : String(e));\n } finally {\n setBusy(false);\n }\n }\n\n return { onboarding, messages, busy, error, send };\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 role: \"user\" | \"assistant\";\n parts: MessagePart[];\n};\n\ntype Handlers = {\n onPart: (parts: MessagePart[]) => void;\n onError?: (message: string) => void;\n onFinish?: () => void;\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\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 \"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 },\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, useRef } from \"react\";\n\nimport { type FactValue, 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/** 방문자 식별자 — 없으면 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":["useEffect","useMemo","useRef","useRef","createContext","useContext","useEffect","useRef","jsx","useCallback","useEffect","useRef","useCallback","useEffect","useRef","jsx","useRef","useEffect","useCallback","entry","useRef","useEffect","useCallback","jsx","useEffect","useMemo","useEffect","useRef","useState","useEffect","useRef","useState","jsx","style","useEffect","useRef","useState","useState","useRef","useEffect","jsx","jsxs","jsx","jsxs","useState","useEffect","useRef","useEffect","useRef","useEffect","useRef"]}
1
+ {"version":3,"sources":["../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":["\"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/nudge-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 children,\n}: {\n apiKey: string;\n botId: string;\n appId: string;\n screen?: string;\n apiBaseUrl?: 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 () => ({ apiKey, botId, appId, apiBaseUrl, screen }),\n [apiKey, botId, appId, apiBaseUrl, screen],\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 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\";\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 /** 실제로 실행할 동작 — 채팅에는 아무것도 안 그리고 이 함수만 호출된다. `payload`는 이\n * tool 호출의 input을 그대로 되돌려준다. `awaitResult:true`면 반환값(또는 resolve된\n * Promise)이 진짜 tool 결과 텍스트가 된다 — 비워두면 \"핸들러가 결과를 반환하지\n * 않았습니다\" 에러로 처리된다. */\n handler: (payload: unknown) => void | string | Promise<void | string>;\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, useContext, useMemo, useRef } from \"react\";\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\nexport type Auth = {\n apiKey: string;\n botId: string;\n appId: string;\n apiBaseUrl: string;\n screen?: string;\n};\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};\n\nexport const DEFAULT_THEME: XbNovaTheme = {\n accentFrom: \"#4f46e5\",\n accentTo: \"#7c3aed\",\n};\n\n/** tool 이름 → 실행부. `handler`의 반환값은 `awaitResult:true`일 때만 의미가 있다 — 진짜\n * tool 결과 텍스트가 돼서 서버로 다시 전송된다(`use-chat.ts`). */\nexport type ClientActionHandler = {\n handler: (payload: unknown) => void | string | Promise<void | string>;\n awaitResult: boolean;\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 /** `<XbNovaProvider clientActions={{...}}>`에서 등록한 것 — `use-chat.ts`가 client\n * action tool 호출을 받으면 이 맵에서 찾아 디스패치한다. */\n clientActionHandlers: Map<string, ClientActionHandler>;\n visitorId: string;\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\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\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 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\n const value = useMemo<InternalState>(\n () => ({\n auth,\n trackRegistry,\n factsRegistry,\n clientActionHandlers: clientActionHandlers ?? emptyHandlers,\n visitorId: visitorIdRef.current!,\n theme,\n }),\n [auth, trackRegistry, factsRegistry, clientActionHandlers, emptyHandlers, 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`를 마운트 시 한 번 불러오고, 이후 1초 간격으로 각\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\nconst EVAL_INTERVAL_MS = 1000;\nconst XBNOVA_EVENT_NAME = \"xbnova:event\";\nconst SESSION_COUNT_KEY_PREFIX = \"xbnova_hint_session_count:\";\n/** 이 시간(ms) 안에 스크롤 이벤트가 있었으면 \"아직 스크롤 중\"으로 보고 새 힌트를 띄우지\n * 않는다 — dwell 문턱(1초)과 비슷한 크기라 스크롤이 완전히 멎은 뒤에만 힌트가 나온다. */\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, visitorId } = 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 문턱(1초)을 넘어버릴 수\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 if (!graph) {\n console.debug(\"[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 fetch(`${apiBaseUrl}/v1/nudge-events`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n app_id: appId,\n nudge_node_id: messageNode.id,\n kind: \"nudge_impression\",\n bot_id: botId,\n visitor_id: visitorId,\n }),\n }).catch(() => {});\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 console.debug(`[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 console.debug(\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 console.debug(`[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(예: 1초)는 금방 다시 채워지는데, 힌트 자체는\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, visitorId],\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 { 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(parts: MessagePart[], clientActionHandlers: Map<string, ClientActionHandler>): boolean {\n return parts.some((part) => {\n if (part.kind === \"text\") 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\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\nexport function XbNovaChat({\n viewRenderers,\n botId,\n barStyle,\n panelStyle,\n backdropStyle,\n defaultOpen = false,\n idleText = \"AI에게 이 차 물어보기\",\n onVerdictAction,\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 /** 판단 카드의 행동 버튼을 눌렀을 때 — 실제 동작(상담신청 폼 열기, 섹션 스크롤 등)은\n * 소비자가 정한다. SDK는 버튼에 적힌 `action` 문자열을 그대로 넘겨줄 뿐 뜻을 모른다. */\n onVerdictAction?: (action: string) => void;\n}) {\n // client action 핸들러는 `<XbNovaChat>` prop이 아니라 `<XbNovaProvider clientActions={{...}}>`에\n // 등록한다 — 이 화면에서 여러 <XbNovaChat> 인스턴스가 떠도 같은 페이지 능력을 공유해야\n // 해서(넛지 handoff로 다른 봇과 열릴 때도 마찬가지) Provider 레벨 개념이다.\n const { auth, trackRegistry, factsRegistry, visitorId, clientActionHandlers, theme } = 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 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 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, messages, busy, error, send } = useXbNovaChat({\n botId: activeBotId ?? botId,\n nudgeNodeId: activeNudgeNodeId,\n onDismissSheet: () => setOpen(false),\n });\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\n // 넛지 전환 퍼널의 \"AI 클릭\" 단계 — 이 힌트를 눌러서 열었을 때만 기록한다(기본\n // 문구를 눌러 연 건 넛지 전환이 아니다).\n fetch(`${auth.apiBaseUrl}/v1/nudge-events`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n app_id: auth.appId,\n nudge_node_id: consumed.messageNodeId,\n kind: \"nudge_open\",\n bot_id: consumed.botId,\n visitor_id: visitorId,\n }),\n }).catch(() => {});\n } else {\n // 기본 바를 눌러 열었으면 넛지 맥락이 없다 — 이전 힌트의 버튼이 남아있으면 안 된다.\n setNudgeQuickReplies([]);\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 }\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 <span\n className=\"flex h-7 w-7 items-center justify-center rounded-full text-[11px] font-bold text-white shadow-md\"\n style={avatarGradientStyle}\n >\n AI\n </span>\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 const firstBubble = openingLine ?? onboarding?.greeting;\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={() => setOpen(false)} />\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 <span\n className=\"flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-[10px] font-bold text-white\"\n style={avatarGradientStyle}\n >\n AI\n </span>\n <div className=\"min-w-0 flex-1\">\n <div className=\"text-sm font-semibold\">{onboarding?.branding?.title ?? \"AI 동행\"}</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={() => setOpen(false)}\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 {firstBubble ? (\n <Bubble role=\"assistant\">\n <AssistantMarkdown text={firstBubble} />\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 <div className=\"space-y-1\">\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 const spec = clientActionHandlers.get(part.toolName);\n return spec?.render ? <div key={j}>{spec.render(part.input)}</div> : null;\n }\n return (\n <ToolView key={j} part={part} viewRenderers={viewRenderers} onVerdictAction={onVerdictAction} />\n );\n })}\n {!hasVisiblePart(message.parts, clientActionHandlers) && busy ? (\n <Bubble role=\"assistant\">\n <span className=\"text-neutral-400\">생각하는 중…</span>\n </Bubble>\n ) : null}\n </div>\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 if (nudgeQuickReplies.length > 0) {\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 {nudgeQuickReplies.map((q) => (\n <button\n key={q}\n onClick={() => {\n setNudgeQuickReplies([]);\n send(q);\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 );\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 send(input);\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 px-3.5 py-2 text-sm outline-none disabled:opacity-50 dark:border-neutral-600 dark:bg-neutral-800\"\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 </div>\n </XbNovaShadowRoot>\n </>\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","\"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\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\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 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 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 { type ClientActionHandler, resolveFactValue, useXbNovaInternal } from \"./internal-context\";\nimport { type ChatMessage, type MessagePart, type ToolPart, resumeToolResult, streamChat } from \"./stream\";\nimport { getFocusedTrackEntry } from \"./track\";\n\nexport type XbNovaOnboarding = {\n conversation_id: string;\n greeting: string;\n quick_replies: string[];\n branding: { title?: string };\n};\n\nexport type UseXbNovaChatResult = {\n onboarding: XbNovaOnboarding | null;\n messages: ChatMessage[];\n busy: boolean;\n error: string | null;\n send: (text: string) => Promise<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 onDismissSheet?: () => void;\n}): UseXbNovaChatResult {\n const { auth, trackRegistry, clientActionHandlers } = useXbNovaInternal();\n const botId = options?.botId ?? auth.botId;\n const nudgeNodeId = options?.nudgeNodeId;\n const { apiKey, apiBaseUrl } = auth;\n\n const [onboarding, setOnboarding] = useState<XbNovaOnboarding | null>(null);\n const [messages, setMessages] = useState<ChatMessage[]>([]);\n const [busy, setBusy] = useState(false);\n const [error, setError] = useState<string | null>(null);\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 setOnboarding(null);\n setMessages([]);\n setError(null);\n dispatchedClientActionsRef.current = new Set();\n // 인사말·시작질문은 봇 설정에서 온다. 모델이 만들지 않는다.\n fetch(`${apiBaseUrl}/v1/conversations`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${apiKey}` },\n body: JSON.stringify({ bot_id: botId, nudge_node_id: nudgeNodeIdRef.current ?? null }),\n })\n .then((r) => (r.ok ? r.json() : Promise.reject(new Error(`${r.status}`))))\n .then(setOnboarding)\n .catch((e) => setError(`봇을 불러올 수 없습니다: ${e.message}`));\n // nudgeNodeId는 의도적으로 의존성에서 뺐다 — 위 nudgeNodeIdRef 설명 참고.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [apiBaseUrl, apiKey, botId]);\n\n const appendToLastAssistant = (existingParts: MessagePart[], newParts: MessagePart[]) =>\n setMessages((prev) => {\n const next = [...prev];\n next[next.length - 1] = { role: \"assistant\", parts: [...existingParts, ...newParts] };\n return next;\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;\n try {\n result = await spec.handler(part.input);\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 = onboarding?.conversation_id;\n if (!conversationId) return; // 이론상 항상 있다 — 방어적으로만 체크한다.\n\n const resultText = typeof result === \"string\" && result.trim() ? result : \"핸들러가 결과를 반환하지 않았습니다.\";\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 { onPart: (newParts) => appendToLastAssistant(existingParts, newParts), onError: setError },\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 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) {\n const trimmed = text.trim();\n if (!trimmed || busy) return;\n setError(null);\n setBusy(true);\n setMessages((prev) => [\n ...prev,\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] = { role: \"assistant\", parts };\n return next;\n });\n\n const focused = getFocusedTrackEntry(trackRegistry);\n const context = focused ? { region: focused.region, data: resolveFactValue(focused.data) } : undefined;\n\n try {\n await streamChat(\n apiBaseUrl,\n apiKey,\n {\n bot_id: botId,\n message: trimmed,\n conversation_id: onboarding?.conversation_id ?? null,\n context,\n nudge_node_id: nudgeNodeId ?? null,\n },\n { onPart: replaceLast, onError: setError },\n );\n } catch (e) {\n setError(e instanceof Error ? e.message : String(e));\n } finally {\n setBusy(false);\n }\n }\n\n return { onboarding, messages, busy, error, send };\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 role: \"user\" | \"assistant\";\n parts: MessagePart[];\n};\n\ntype Handlers = {\n onPart: (parts: MessagePart[]) => void;\n onError?: (message: string) => void;\n onFinish?: () => void;\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\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 \"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 },\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, useRef } from \"react\";\n\nimport { type FactValue, 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/** 방문자 식별자 — 없으면 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":["useEffect","useMemo","useRef","useRef","createContext","useContext","useEffect","useRef","jsx","useCallback","useEffect","useRef","useCallback","useEffect","useRef","jsx","useRef","useEffect","useCallback","entry","useRef","useEffect","useCallback","jsx","useEffect","useMemo","useEffect","useRef","useState","useEffect","useRef","useState","jsx","style","useEffect","useRef","useState","useState","useRef","useEffect","jsx","jsxs","jsx","jsxs","useState","useEffect","useRef","useEffect","useRef","useEffect","useRef"]}