@xbrain-swe/nova-chat-widget 0.1.6 → 0.1.8

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.d.cts CHANGED
@@ -148,7 +148,7 @@ type ViewRenderer = (props: {
148
148
  }) => React.ReactNode;
149
149
  type ViewRendererMap = Partial<Record<ViewKind, ViewRenderer>>;
150
150
 
151
- declare function XbNovaChat({ viewRenderers, botId, barStyle, panelStyle, backdropStyle, defaultOpen, idleText, onVerdictAction, }: {
151
+ declare function XbNovaChat({ viewRenderers, botId, barStyle, panelStyle, backdropStyle, defaultOpen, idleText, avatarSize, onVerdictAction, }: {
152
152
  /** 특정 view만 자체 컴포넌트로 덮어쓰고 싶을 때 — 지정 안 한 view는 기본 렌더러를 쓴다. */
153
153
  viewRenderers?: ViewRendererMap;
154
154
  /** Provider의 기본 봇 대신 다른 봇과 고정적으로 대화하고 싶을 때. 힌트를 눌러 열면
@@ -167,6 +167,9 @@ declare function XbNovaChat({ viewRenderers, botId, barStyle, panelStyle, backdr
167
167
  defaultOpen?: boolean;
168
168
  /** 기본(idle) 상태 바 문구. */
169
169
  idleText?: string;
170
+ /** 바(AI 버튼)의 동그란 아바타 지름(px), 기본 28. 바의 접힌 원은 52px이라 44~48이면 원을 거의 채운다.
171
+ * 채팅 헤더의 아바타는 이 값에 맞춰 24~36px로 따라 커진다. */
172
+ avatarSize?: number;
170
173
  /** 판단 카드의 행동 버튼을 눌렀을 때 — 실제 동작(상담신청 폼 열기, 섹션 스크롤 등)은
171
174
  * 소비자가 정한다. SDK는 버튼에 적힌 `action` 문자열을 그대로 넘겨줄 뿐 뜻을 모른다. */
172
175
  onVerdictAction?: (action: string) => void;
package/dist/index.d.ts CHANGED
@@ -148,7 +148,7 @@ type ViewRenderer = (props: {
148
148
  }) => React.ReactNode;
149
149
  type ViewRendererMap = Partial<Record<ViewKind, ViewRenderer>>;
150
150
 
151
- declare function XbNovaChat({ viewRenderers, botId, barStyle, panelStyle, backdropStyle, defaultOpen, idleText, onVerdictAction, }: {
151
+ declare function XbNovaChat({ viewRenderers, botId, barStyle, panelStyle, backdropStyle, defaultOpen, idleText, avatarSize, onVerdictAction, }: {
152
152
  /** 특정 view만 자체 컴포넌트로 덮어쓰고 싶을 때 — 지정 안 한 view는 기본 렌더러를 쓴다. */
153
153
  viewRenderers?: ViewRendererMap;
154
154
  /** Provider의 기본 봇 대신 다른 봇과 고정적으로 대화하고 싶을 때. 힌트를 눌러 열면
@@ -167,6 +167,9 @@ declare function XbNovaChat({ viewRenderers, botId, barStyle, panelStyle, backdr
167
167
  defaultOpen?: boolean;
168
168
  /** 기본(idle) 상태 바 문구. */
169
169
  idleText?: string;
170
+ /** 바(AI 버튼)의 동그란 아바타 지름(px), 기본 28. 바의 접힌 원은 52px이라 44~48이면 원을 거의 채운다.
171
+ * 채팅 헤더의 아바타는 이 값에 맞춰 24~36px로 따라 커진다. */
172
+ avatarSize?: number;
170
173
  /** 판단 카드의 행동 버튼을 눌렀을 때 — 실제 동작(상담신청 폼 열기, 섹션 스크롤 등)은
171
174
  * 소비자가 정한다. SDK는 버튼에 적힌 `action` 문자열을 그대로 넘겨줄 뿐 뜻을 모른다. */
172
175
  onVerdictAction?: (action: string) => void;
package/dist/index.js CHANGED
@@ -1476,6 +1476,7 @@ function XbNovaChat({
1476
1476
  backdropStyle,
1477
1477
  defaultOpen = false,
1478
1478
  idleText = "AI\uC5D0\uAC8C \uC774 \uCC28 \uBB3C\uC5B4\uBCF4\uAE30",
1479
+ avatarSize = 28,
1479
1480
  onVerdictAction
1480
1481
  }) {
1481
1482
  const { auth, trackRegistry, factsRegistry, visitorId, clientActionHandlers, theme } = useXbNovaInternal();
@@ -1483,6 +1484,7 @@ function XbNovaChat({
1483
1484
  "--xbnova-accent-1": theme.accentFrom,
1484
1485
  "--xbnova-accent-2": theme.accentTo
1485
1486
  };
1487
+ const headerAvatarSize = Math.min(36, Math.max(24, Math.round(avatarSize * 0.8)));
1486
1488
  const avatarGradientStyle = {
1487
1489
  background: "linear-gradient(135deg, var(--xbnova-accent-1), var(--xbnova-accent-2))"
1488
1490
  };
@@ -1503,6 +1505,28 @@ function XbNovaChat({
1503
1505
  nudgeNodeId: activeNudgeNodeId,
1504
1506
  onDismissSheet: () => setOpen(false)
1505
1507
  });
1508
+ const effectiveBotId = activeBotId ?? botId;
1509
+ const [cachedBranding, setCachedBranding] = useState4(null);
1510
+ useEffect8(() => {
1511
+ try {
1512
+ const raw = window.localStorage.getItem(`xbnova_branding:${effectiveBotId}`);
1513
+ setCachedBranding(raw ? JSON.parse(raw) : null);
1514
+ } catch {
1515
+ setCachedBranding(null);
1516
+ }
1517
+ }, [effectiveBotId]);
1518
+ useEffect8(() => {
1519
+ if (!onboarding?.branding) return;
1520
+ try {
1521
+ window.localStorage.setItem(
1522
+ `xbnova_branding:${effectiveBotId}`,
1523
+ JSON.stringify({ logo_url: onboarding.branding.logo_url, avatar_text: onboarding.branding.avatar_text })
1524
+ );
1525
+ } catch {
1526
+ }
1527
+ }, [onboarding?.branding, effectiveBotId]);
1528
+ const branding = onboarding?.branding ?? cachedBranding ?? void 0;
1529
+ const brandingReady = Boolean(onboarding) || cachedBranding !== null || Boolean(error);
1506
1530
  useEffect8(() => {
1507
1531
  bottomRef.current?.scrollIntoView({ behavior: "smooth" });
1508
1532
  }, [messages]);
@@ -1630,8 +1654,10 @@ function XbNovaChat({
1630
1654
  children: /* @__PURE__ */ jsx7(
1631
1655
  Avatar,
1632
1656
  {
1633
- branding: onboarding?.branding,
1634
- className: "h-7 w-7 text-[11px] shadow-md",
1657
+ branding,
1658
+ ready: brandingReady,
1659
+ size: avatarSize,
1660
+ className: "shadow-md",
1635
1661
  gradient: avatarGradientStyle
1636
1662
  }
1637
1663
  )
@@ -1678,8 +1704,10 @@ function XbNovaChat({
1678
1704
  /* @__PURE__ */ jsx7(
1679
1705
  Avatar,
1680
1706
  {
1681
- branding: onboarding?.branding,
1682
- className: "h-6 w-6 shrink-0 text-[10px]",
1707
+ branding,
1708
+ ready: brandingReady,
1709
+ size: headerAvatarSize,
1710
+ className: "shrink-0",
1683
1711
  gradient: avatarGradientStyle
1684
1712
  }
1685
1713
  ),
@@ -1797,31 +1825,34 @@ function XbNovaChat({
1797
1825
  }
1798
1826
  function Avatar({
1799
1827
  branding,
1800
- className,
1828
+ ready,
1829
+ size,
1830
+ className = "",
1801
1831
  gradient
1802
1832
  }) {
1833
+ const box = { width: size, height: size };
1803
1834
  const logoUrl = branding?.logo_url?.trim() ?? "";
1804
1835
  const [failedUrl, setFailedUrl] = useState4("");
1836
+ const [loadedUrl, setLoadedUrl] = useState4("");
1837
+ const base = `${className} relative flex flex-none items-center justify-center overflow-hidden rounded-full font-bold text-white`;
1838
+ if (!ready) {
1839
+ return /* @__PURE__ */ jsx7("span", { className: base, style: { ...gradient, ...box }, "aria-hidden": true });
1840
+ }
1805
1841
  if (logoUrl && failedUrl !== logoUrl) {
1806
- return /* @__PURE__ */ jsx7(
1842
+ return /* @__PURE__ */ jsx7("span", { className: base, style: { ...gradient, ...box }, children: /* @__PURE__ */ jsx7(
1807
1843
  "img",
1808
1844
  {
1809
1845
  src: logoUrl,
1810
1846
  alt: "",
1847
+ onLoad: () => setLoadedUrl(logoUrl),
1811
1848
  onError: () => setFailedUrl(logoUrl),
1812
- className: `${className} flex-none rounded-full bg-white object-cover`
1849
+ className: "absolute inset-0 h-full w-full rounded-full bg-white object-cover transition-opacity duration-200",
1850
+ style: { opacity: loadedUrl === logoUrl ? 1 : 0 }
1813
1851
  }
1814
- );
1852
+ ) });
1815
1853
  }
1816
1854
  const text = branding?.avatar_text?.trim() || "AI";
1817
- return /* @__PURE__ */ jsx7(
1818
- "span",
1819
- {
1820
- className: `${className} flex items-center justify-center rounded-full font-bold text-white`,
1821
- style: gradient,
1822
- children: text
1823
- }
1824
- );
1855
+ return /* @__PURE__ */ jsx7("span", { className: base, style: { ...gradient, ...box, fontSize: Math.max(10, Math.round(size * 0.4)) }, children: text });
1825
1856
  }
1826
1857
  function Bubble({ role, children }) {
1827
1858
  const mine = role === "user";