@windrun-huaiin/third-ui 15.1.0 → 16.0.0

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.
Files changed (133) hide show
  1. package/LICENSE +1 -1
  2. package/dist/ai/ai-chat-composer.d.ts +2 -0
  3. package/dist/ai/ai-chat-composer.js +47 -0
  4. package/dist/ai/ai-chat-composer.mjs +45 -0
  5. package/dist/ai/ai-markdown.d.ts +2 -0
  6. package/dist/ai/ai-markdown.js +36 -0
  7. package/dist/ai/ai-markdown.mjs +34 -0
  8. package/dist/ai/ai-message-actions.d.ts +2 -0
  9. package/dist/ai/ai-message-actions.js +14 -0
  10. package/dist/ai/ai-message-actions.mjs +12 -0
  11. package/dist/ai/ai-message-bubble.d.ts +2 -0
  12. package/dist/ai/ai-message-bubble.js +66 -0
  13. package/dist/ai/ai-message-bubble.mjs +64 -0
  14. package/dist/ai/ai-message-content.d.ts +2 -0
  15. package/dist/ai/ai-message-content.js +63 -0
  16. package/dist/ai/ai-message-content.mjs +61 -0
  17. package/dist/ai/ai-message-list.d.ts +2 -0
  18. package/dist/ai/ai-message-list.js +24 -0
  19. package/dist/ai/ai-message-list.mjs +22 -0
  20. package/dist/ai/ai-message-meta.d.ts +2 -0
  21. package/dist/ai/ai-message-meta.js +38 -0
  22. package/dist/ai/ai-message-meta.mjs +36 -0
  23. package/dist/ai/ai-status-indicator.d.ts +2 -0
  24. package/dist/ai/ai-status-indicator.js +51 -0
  25. package/dist/ai/ai-status-indicator.mjs +49 -0
  26. package/dist/ai/index.d.ts +11 -0
  27. package/dist/ai/index.js +33 -0
  28. package/dist/ai/index.mjs +11 -0
  29. package/dist/ai/types.d.ts +110 -0
  30. package/dist/ai/use-ai-conversation.d.ts +13 -0
  31. package/dist/ai/use-ai-conversation.js +276 -0
  32. package/dist/ai/use-ai-conversation.mjs +274 -0
  33. package/dist/clerk/clerk-organization-client.js +2 -2
  34. package/dist/clerk/clerk-organization-client.mjs +2 -2
  35. package/dist/clerk/clerk-page-generator.d.ts +1 -1
  36. package/dist/clerk/clerk-user-client.js +2 -2
  37. package/dist/clerk/clerk-user-client.mjs +2 -2
  38. package/dist/clerk/fingerprint/fingerprint-provider.js +9 -9
  39. package/dist/clerk/fingerprint/fingerprint-provider.mjs +9 -9
  40. package/dist/fuma/base/custom-header.js +4 -4
  41. package/dist/fuma/base/custom-header.mjs +4 -4
  42. package/dist/fuma/mdx/banner.js +3 -3
  43. package/dist/fuma/mdx/banner.mjs +3 -3
  44. package/dist/fuma/mdx/fuma-github-info.js +3 -3
  45. package/dist/fuma/mdx/fuma-github-info.mjs +3 -3
  46. package/dist/fuma/mdx/gradient-button.js +3 -3
  47. package/dist/fuma/mdx/gradient-button.mjs +3 -3
  48. package/dist/fuma/mdx/index.d.ts +1 -0
  49. package/dist/fuma/mdx/index.js +2 -0
  50. package/dist/fuma/mdx/index.mjs +1 -0
  51. package/dist/fuma/mdx/markdown-component-map.d.ts +3 -0
  52. package/dist/fuma/mdx/markdown-component-map.js +73 -0
  53. package/dist/fuma/mdx/markdown-component-map.mjs +71 -0
  54. package/dist/fuma/mdx/mermaid.d.ts +2 -1
  55. package/dist/fuma/mdx/mermaid.js +130 -6
  56. package/dist/fuma/mdx/mermaid.mjs +130 -6
  57. package/dist/fuma/mdx/toc-base.js +4 -4
  58. package/dist/fuma/mdx/toc-base.mjs +4 -4
  59. package/dist/fuma/mdx/trophy-card.js +2 -2
  60. package/dist/fuma/mdx/trophy-card.mjs +2 -2
  61. package/dist/fuma/mdx/zia-card.js +3 -3
  62. package/dist/fuma/mdx/zia-card.mjs +3 -3
  63. package/dist/fuma/mdx/zia-file.js +3 -3
  64. package/dist/fuma/mdx/zia-file.mjs +3 -3
  65. package/dist/main/ads-alert-dialog.js +2 -2
  66. package/dist/main/ads-alert-dialog.mjs +2 -2
  67. package/dist/main/credit/credit-nav-button.js +2 -2
  68. package/dist/main/credit/credit-nav-button.mjs +2 -2
  69. package/dist/main/credit/credit-overview-client.js +4 -4
  70. package/dist/main/credit/credit-overview-client.mjs +4 -4
  71. package/dist/main/footer.js +2 -2
  72. package/dist/main/footer.mjs +2 -2
  73. package/dist/main/go-to-top.js +2 -2
  74. package/dist/main/go-to-top.mjs +2 -2
  75. package/dist/main/hero-media.d.ts +14 -0
  76. package/dist/main/hero-media.js +12 -0
  77. package/dist/main/hero-media.mjs +10 -0
  78. package/dist/main/hero-section.d.ts +10 -0
  79. package/dist/main/hero-section.js +11 -0
  80. package/dist/main/hero-section.mjs +9 -0
  81. package/dist/main/index.d.ts +3 -0
  82. package/dist/main/index.js +6 -0
  83. package/dist/main/index.mjs +3 -0
  84. package/dist/main/info-tooltip.d.ts +8 -0
  85. package/dist/main/info-tooltip.js +48 -0
  86. package/dist/main/info-tooltip.mjs +46 -0
  87. package/dist/main/pill-select/x-pill-select.js +2 -2
  88. package/dist/main/pill-select/x-pill-select.mjs +2 -2
  89. package/dist/main/pill-select/x-token-input.js +2 -2
  90. package/dist/main/pill-select/x-token-input.mjs +2 -2
  91. package/dist/main/x-button.js +3 -3
  92. package/dist/main/x-button.mjs +3 -3
  93. package/package.json +16 -3
  94. package/src/ai/ai-chat-composer.tsx +187 -0
  95. package/src/ai/ai-markdown.tsx +45 -0
  96. package/src/ai/ai-message-actions.tsx +16 -0
  97. package/src/ai/ai-message-bubble.tsx +138 -0
  98. package/src/ai/ai-message-content.tsx +149 -0
  99. package/src/ai/ai-message-list.tsx +59 -0
  100. package/src/ai/ai-message-meta.tsx +56 -0
  101. package/src/ai/ai-status-indicator.tsx +61 -0
  102. package/src/ai/index.ts +13 -0
  103. package/src/ai/types.ts +131 -0
  104. package/src/ai/use-ai-conversation.ts +422 -0
  105. package/src/clerk/clerk-organization-client.tsx +5 -5
  106. package/src/clerk/clerk-page-generator.tsx +1 -1
  107. package/src/clerk/clerk-user-client.tsx +4 -4
  108. package/src/clerk/fingerprint/fingerprint-provider.tsx +34 -22
  109. package/src/fuma/base/custom-header.tsx +5 -5
  110. package/src/fuma/mdx/banner.tsx +3 -3
  111. package/src/fuma/mdx/fuma-github-info.tsx +4 -4
  112. package/src/fuma/mdx/gradient-button.tsx +3 -3
  113. package/src/fuma/mdx/index.ts +2 -1
  114. package/src/fuma/mdx/markdown-component-map.tsx +174 -0
  115. package/src/fuma/mdx/mermaid.tsx +145 -10
  116. package/src/fuma/mdx/toc-base.tsx +5 -5
  117. package/src/fuma/mdx/trophy-card.tsx +2 -2
  118. package/src/fuma/mdx/zia-card.tsx +3 -3
  119. package/src/fuma/mdx/zia-file.tsx +3 -3
  120. package/src/main/ads-alert-dialog.tsx +5 -5
  121. package/src/main/credit/credit-nav-button.tsx +3 -3
  122. package/src/main/credit/credit-overview-client.tsx +15 -7
  123. package/src/main/features.tsx +5 -3
  124. package/src/main/footer.tsx +4 -5
  125. package/src/main/go-to-top.tsx +2 -2
  126. package/src/main/hero-media.tsx +53 -0
  127. package/src/main/hero-section.tsx +36 -0
  128. package/src/main/index.ts +5 -0
  129. package/src/main/info-tooltip.tsx +99 -0
  130. package/src/main/language-detector.tsx +4 -4
  131. package/src/main/pill-select/x-pill-select.tsx +2 -2
  132. package/src/main/pill-select/x-token-input.tsx +2 -2
  133. package/src/main/x-button.tsx +4 -4
@@ -3,7 +3,7 @@
3
3
 
4
4
  var jsxRuntime = require('react/jsx-runtime');
5
5
  var React = require('react');
6
- var server = require('@windrun-huaiin/base-ui/components/server');
6
+ var icons = require('@windrun-huaiin/base-ui/icons');
7
7
  var lib = require('@windrun-huaiin/base-ui/lib');
8
8
  var utils = require('@windrun-huaiin/lib/utils');
9
9
 
@@ -126,7 +126,7 @@ function XPillSelect(props) {
126
126
  event.stopPropagation();
127
127
  removeValue(selectedValue);
128
128
  }, disabled: disabled, className: utils.cn('inline-flex max-w-full items-center rounded-full font-semibold transition', compact ? 'gap-1 px-2.5 py-0.5 text-[11px]' : 'gap-1.5 px-3 py-1 text-xs', lib.themeBgColor, lib.themeIconColor, 'hover:brightness-95 dark:hover:brightness-110', disabled && 'cursor-not-allowed opacity-60'), title: optionLabel, children: jsxRuntime.jsx("span", { className: utils.cn('truncate', maxPillWidthClassName), children: optionLabel }) }, selectedValue));
129
- }), hiddenSelectedCount > 0 ? (jsxRuntime.jsxs("span", { className: utils.cn('inline-flex max-w-full items-center rounded-full font-semibold transition', compact ? 'px-2.5 py-0.5 text-[11px]' : 'px-3 py-1 text-xs', 'bg-neutral-200 text-neutral-700 dark:bg-neutral-800 dark:text-white'), title: `还有 ${hiddenSelectedCount} 项未展开`, children: ["+", hiddenSelectedCount] })) : null] }))) : (jsxRuntime.jsx("span", { className: utils.cn(compact ? 'text-xs' : 'text-sm', 'text-slate-500 dark:text-slate-400'), children: emptyLabel })) }), jsxRuntime.jsx(server.globalLucideIcons.ChevronDown, { className: utils.cn(compact ? 'h-3.5 w-3.5' : 'h-4 w-4', 'shrink-0 text-slate-500 transition-transform dark:text-slate-400', open && 'rotate-180') })] }), open ? (jsxRuntime.jsxs("div", { role: "listbox", "aria-multiselectable": props.mode === 'multiple' ? true : undefined, className: utils.cn('absolute left-0 right-0 top-[calc(100%+0.375rem)] z-50 rounded-3xl border border-black/10 bg-neutral-100 shadow-xl dark:border-white/10 dark:bg-neutral-900', compact ? 'space-y-2.5 p-3' : 'space-y-3 p-4', open && lib.themeBorderColor), children: [inputEnabled ? (jsxRuntime.jsx("input", { value: draftValue, onChange: (event) => setDraftValue(event.target.value.replaceAll(',', '')), onKeyDown: (event) => {
129
+ }), hiddenSelectedCount > 0 ? (jsxRuntime.jsxs("span", { className: utils.cn('inline-flex max-w-full items-center rounded-full font-semibold transition', compact ? 'px-2.5 py-0.5 text-[11px]' : 'px-3 py-1 text-xs', 'bg-neutral-200 text-neutral-700 dark:bg-neutral-800 dark:text-white'), title: `还有 ${hiddenSelectedCount} 项未展开`, children: ["+", hiddenSelectedCount] })) : null] }))) : (jsxRuntime.jsx("span", { className: utils.cn(compact ? 'text-xs' : 'text-sm', 'text-slate-500 dark:text-slate-400'), children: emptyLabel })) }), jsxRuntime.jsx(icons.ChevronDownIcon, { className: utils.cn(compact ? 'h-3.5 w-3.5' : 'h-4 w-4', 'shrink-0 text-slate-500 transition-transform dark:text-slate-400', open && 'rotate-180') })] }), open ? (jsxRuntime.jsxs("div", { role: "listbox", "aria-multiselectable": props.mode === 'multiple' ? true : undefined, className: utils.cn('absolute left-0 right-0 top-[calc(100%+0.375rem)] z-50 rounded-3xl border border-black/10 bg-neutral-100 shadow-xl dark:border-white/10 dark:bg-neutral-900', compact ? 'space-y-2.5 p-3' : 'space-y-3 p-4', open && lib.themeBorderColor), children: [inputEnabled ? (jsxRuntime.jsx("input", { value: draftValue, onChange: (event) => setDraftValue(event.target.value.replaceAll(',', '')), onKeyDown: (event) => {
130
130
  if (event.key !== 'Enter') {
131
131
  return;
132
132
  }
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
3
3
  import { useState, useRef, useMemo, useEffect } from 'react';
4
- import { globalLucideIcons } from '@windrun-huaiin/base-ui/components/server';
4
+ import { ChevronDownIcon } from '@windrun-huaiin/base-ui/icons';
5
5
  import { themeBgColor, themeIconColor, themeBorderColor } from '@windrun-huaiin/base-ui/lib';
6
6
  import { cn } from '@windrun-huaiin/lib/utils';
7
7
 
@@ -124,7 +124,7 @@ function XPillSelect(props) {
124
124
  event.stopPropagation();
125
125
  removeValue(selectedValue);
126
126
  }, disabled: disabled, className: cn('inline-flex max-w-full items-center rounded-full font-semibold transition', compact ? 'gap-1 px-2.5 py-0.5 text-[11px]' : 'gap-1.5 px-3 py-1 text-xs', themeBgColor, themeIconColor, 'hover:brightness-95 dark:hover:brightness-110', disabled && 'cursor-not-allowed opacity-60'), title: optionLabel, children: jsx("span", { className: cn('truncate', maxPillWidthClassName), children: optionLabel }) }, selectedValue));
127
- }), hiddenSelectedCount > 0 ? (jsxs("span", { className: cn('inline-flex max-w-full items-center rounded-full font-semibold transition', compact ? 'px-2.5 py-0.5 text-[11px]' : 'px-3 py-1 text-xs', 'bg-neutral-200 text-neutral-700 dark:bg-neutral-800 dark:text-white'), title: `还有 ${hiddenSelectedCount} 项未展开`, children: ["+", hiddenSelectedCount] })) : null] }))) : (jsx("span", { className: cn(compact ? 'text-xs' : 'text-sm', 'text-slate-500 dark:text-slate-400'), children: emptyLabel })) }), jsx(globalLucideIcons.ChevronDown, { className: cn(compact ? 'h-3.5 w-3.5' : 'h-4 w-4', 'shrink-0 text-slate-500 transition-transform dark:text-slate-400', open && 'rotate-180') })] }), open ? (jsxs("div", { role: "listbox", "aria-multiselectable": props.mode === 'multiple' ? true : undefined, className: cn('absolute left-0 right-0 top-[calc(100%+0.375rem)] z-50 rounded-3xl border border-black/10 bg-neutral-100 shadow-xl dark:border-white/10 dark:bg-neutral-900', compact ? 'space-y-2.5 p-3' : 'space-y-3 p-4', open && themeBorderColor), children: [inputEnabled ? (jsx("input", { value: draftValue, onChange: (event) => setDraftValue(event.target.value.replaceAll(',', '')), onKeyDown: (event) => {
127
+ }), hiddenSelectedCount > 0 ? (jsxs("span", { className: cn('inline-flex max-w-full items-center rounded-full font-semibold transition', compact ? 'px-2.5 py-0.5 text-[11px]' : 'px-3 py-1 text-xs', 'bg-neutral-200 text-neutral-700 dark:bg-neutral-800 dark:text-white'), title: `还有 ${hiddenSelectedCount} 项未展开`, children: ["+", hiddenSelectedCount] })) : null] }))) : (jsx("span", { className: cn(compact ? 'text-xs' : 'text-sm', 'text-slate-500 dark:text-slate-400'), children: emptyLabel })) }), jsx(ChevronDownIcon, { className: cn(compact ? 'h-3.5 w-3.5' : 'h-4 w-4', 'shrink-0 text-slate-500 transition-transform dark:text-slate-400', open && 'rotate-180') })] }), open ? (jsxs("div", { role: "listbox", "aria-multiselectable": props.mode === 'multiple' ? true : undefined, className: cn('absolute left-0 right-0 top-[calc(100%+0.375rem)] z-50 rounded-3xl border border-black/10 bg-neutral-100 shadow-xl dark:border-white/10 dark:bg-neutral-900', compact ? 'space-y-2.5 p-3' : 'space-y-3 p-4', open && themeBorderColor), children: [inputEnabled ? (jsx("input", { value: draftValue, onChange: (event) => setDraftValue(event.target.value.replaceAll(',', '')), onKeyDown: (event) => {
128
128
  if (event.key !== 'Enter') {
129
129
  return;
130
130
  }
@@ -3,7 +3,7 @@
3
3
 
4
4
  var jsxRuntime = require('react/jsx-runtime');
5
5
  var React = require('react');
6
- var server = require('@windrun-huaiin/base-ui/components/server');
6
+ var icons = require('@windrun-huaiin/base-ui/icons');
7
7
  var lib = require('@windrun-huaiin/base-ui/lib');
8
8
  var utils = require('@windrun-huaiin/lib/utils');
9
9
 
@@ -50,7 +50,7 @@ function XTokenInput({ value, onChange, placeholder, emptyLabel, disabled = fals
50
50
  }, className: utils.cn('w-full min-w-0 rounded-3xl border border-black/10 transition dark:border-white/10', compact ? 'min-h-9 px-3 py-1.5' : 'min-h-11 px-4 py-2.5', focused && lib.themeBorderColor), children: jsxRuntime.jsxs("div", { className: utils.cn('flex w-full min-w-0 flex-wrap items-center', compact ? 'gap-1.5' : 'gap-2'), children: [tokens.length > 0 ? (jsxRuntime.jsx("ul", { className: "contents", role: "list", children: tokens.map((token) => (jsxRuntime.jsx("li", { className: "max-w-full list-none", children: jsxRuntime.jsxs("span", { className: utils.cn('inline-flex max-w-full items-center rounded-full font-semibold transition', compact ? 'gap-1 px-2.5 py-0.5 text-[11px]' : 'gap-1 px-3 py-1 text-xs', lib.themeBgColor, lib.themeIconColor, disabled && 'opacity-60'), title: token, children: [jsxRuntime.jsx("span", { className: utils.cn('truncate', maxPillWidthClassName), children: token }), jsxRuntime.jsx("button", { type: "button", onClick: (event) => {
51
51
  event.stopPropagation();
52
52
  removeToken(token);
53
- }, disabled: disabled, "aria-label": `Remove ${token}`, className: utils.cn('inline-flex shrink-0 items-center justify-center rounded-full transition', compact ? 'h-3.5 w-3.5' : 'h-4 w-4', 'hover:bg-black/10 dark:hover:bg-white/10', 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-1', lib.themeRingColor, disabled && 'cursor-not-allowed'), children: jsxRuntime.jsx(server.globalLucideIcons.X, { className: utils.cn(compact ? 'h-2 w-2' : 'h-2.5 w-2.5') }) })] }) }, token))) })) : null, jsxRuntime.jsx("div", { className: utils.cn('min-w-0 overflow-hidden', tokens.length === 0
53
+ }, disabled: disabled, "aria-label": `Remove ${token}`, className: utils.cn('inline-flex shrink-0 items-center justify-center rounded-full transition', compact ? 'h-3.5 w-3.5' : 'h-4 w-4', 'hover:bg-black/10 dark:hover:bg-white/10', 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-1', lib.themeRingColor, disabled && 'cursor-not-allowed'), children: jsxRuntime.jsx(icons.XIcon, { className: utils.cn(compact ? 'h-2 w-2' : 'h-2.5 w-2.5') }) })] }) }, token))) })) : null, jsxRuntime.jsx("div", { className: utils.cn('min-w-0 overflow-hidden', tokens.length === 0
54
54
  ? 'flex-1 min-w-[160px]'
55
55
  : draftValue || focused
56
56
  ? 'flex-1 min-w-[120px]'
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { jsxs, jsx } from 'react/jsx-runtime';
3
3
  import { useState, useRef } from 'react';
4
- import { globalLucideIcons } from '@windrun-huaiin/base-ui/components/server';
4
+ import { XIcon } from '@windrun-huaiin/base-ui/icons';
5
5
  import { themeRingColor, themeBgColor, themeIconColor, themeBorderColor } from '@windrun-huaiin/base-ui/lib';
6
6
  import { cn } from '@windrun-huaiin/lib/utils';
7
7
 
@@ -48,7 +48,7 @@ function XTokenInput({ value, onChange, placeholder, emptyLabel, disabled = fals
48
48
  }, className: cn('w-full min-w-0 rounded-3xl border border-black/10 transition dark:border-white/10', compact ? 'min-h-9 px-3 py-1.5' : 'min-h-11 px-4 py-2.5', focused && themeBorderColor), children: jsxs("div", { className: cn('flex w-full min-w-0 flex-wrap items-center', compact ? 'gap-1.5' : 'gap-2'), children: [tokens.length > 0 ? (jsx("ul", { className: "contents", role: "list", children: tokens.map((token) => (jsx("li", { className: "max-w-full list-none", children: jsxs("span", { className: cn('inline-flex max-w-full items-center rounded-full font-semibold transition', compact ? 'gap-1 px-2.5 py-0.5 text-[11px]' : 'gap-1 px-3 py-1 text-xs', themeBgColor, themeIconColor, disabled && 'opacity-60'), title: token, children: [jsx("span", { className: cn('truncate', maxPillWidthClassName), children: token }), jsx("button", { type: "button", onClick: (event) => {
49
49
  event.stopPropagation();
50
50
  removeToken(token);
51
- }, disabled: disabled, "aria-label": `Remove ${token}`, className: cn('inline-flex shrink-0 items-center justify-center rounded-full transition', compact ? 'h-3.5 w-3.5' : 'h-4 w-4', 'hover:bg-black/10 dark:hover:bg-white/10', 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-1', themeRingColor, disabled && 'cursor-not-allowed'), children: jsx(globalLucideIcons.X, { className: cn(compact ? 'h-2 w-2' : 'h-2.5 w-2.5') }) })] }) }, token))) })) : null, jsx("div", { className: cn('min-w-0 overflow-hidden', tokens.length === 0
51
+ }, disabled: disabled, "aria-label": `Remove ${token}`, className: cn('inline-flex shrink-0 items-center justify-center rounded-full transition', compact ? 'h-3.5 w-3.5' : 'h-4 w-4', 'hover:bg-black/10 dark:hover:bg-white/10', 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-1', themeRingColor, disabled && 'cursor-not-allowed'), children: jsx(XIcon, { className: cn(compact ? 'h-2 w-2' : 'h-2.5 w-2.5') }) })] }) }, token))) })) : null, jsx("div", { className: cn('min-w-0 overflow-hidden', tokens.length === 0
52
52
  ? 'flex-1 min-w-[160px]'
53
53
  : draftValue || focused
54
54
  ? 'flex-1 min-w-[120px]'
@@ -4,7 +4,7 @@
4
4
  var tslib = require('tslib');
5
5
  var jsxRuntime = require('react/jsx-runtime');
6
6
  var React = require('react');
7
- var server = require('@windrun-huaiin/base-ui/components/server');
7
+ var icons = require('@windrun-huaiin/base-ui/icons');
8
8
  var lib = require('@windrun-huaiin/base-ui/lib');
9
9
  var utils = require('@windrun-huaiin/lib/utils');
10
10
 
@@ -81,7 +81,7 @@ function XButton(props) {
81
81
  const isDisabled = button.disabled || isLoading;
82
82
  // loadingText: props.loadingText > button.text > 'Loading...'
83
83
  const actualLoadingText = loadingText || ((_b = button.text) === null || _b === void 0 ? void 0 : _b.trim()) || 'Loading...';
84
- return (jsxRuntime.jsx("button", { onClick: () => handleButtonClick(button.onClick), disabled: isDisabled, className: utils.cn("w-full sm:w-auto", minWidth, baseButtonClass, singleButtonVariantClass, "rounded-full", isDisabled && disabledClass, className), title: button.text, children: isLoading ? (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(server.globalLucideIcons.Loader2, { className: loadingIconClass }), jsxRuntime.jsx("span", { children: actualLoadingText })] })) : (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [renderIcon(button.icon), jsxRuntime.jsx("span", { children: button.text })] })) }));
84
+ return (jsxRuntime.jsx("button", { onClick: () => handleButtonClick(button.onClick), disabled: isDisabled, className: utils.cn("w-full sm:w-auto", minWidth, baseButtonClass, singleButtonVariantClass, "rounded-full", isDisabled && disabledClass, className), title: button.text, children: isLoading ? (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(icons.Loader2Icon, { className: loadingIconClass }), jsxRuntime.jsx("span", { children: actualLoadingText })] })) : (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [renderIcon(button.icon), jsxRuntime.jsx("span", { children: button.text })] })) }));
85
85
  }
86
86
  // Split button
87
87
  const { mainButton, menuItems, loadingText, menuWidth = 'w-full sm:w-40', className = '', mainButtonClassName = '', dropdownButtonClassName = '' } = props;
@@ -93,7 +93,7 @@ function XButton(props) {
93
93
  : variant === 'subtle'
94
94
  ? utils.cn(lib.themeMainBgColor, "border border-neutral-200 dark:border-neutral-800")
95
95
  : "bg-neutral-200 dark:bg-neutral-800", className), children: [jsxRuntime.jsx("button", { onClick: () => handleButtonClick(mainButton.onClick), disabled: isMainDisabled, className: utils.cn("min-w-0 flex-1", baseButtonClass, splitMainButtonVariantClass, "rounded-l-full rounded-r-none", isMainDisabled && disabledClass, mainButtonClassName), onMouseDown: e => { if (e.button === 2)
96
- e.preventDefault(); }, children: isLoading ? (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(server.globalLucideIcons.Loader2, { className: loadingIconClass }), jsxRuntime.jsx("span", { children: actualLoadingText })] })) : (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [renderIcon(mainButton.icon), jsxRuntime.jsx("span", { className: "min-w-0 truncate", children: mainButton.text })] })) }), jsxRuntime.jsx("button", { type: "button", className: utils.cn("flex h-full w-9 shrink-0 items-center justify-center px-0 py-1.5 cursor-pointer transition rounded-r-full rounded-l-none border-l sm:w-10", splitDropdownVariantClass, dropdownButtonClassName), onClick: e => { e.stopPropagation(); setMenuOpen(v => !v); }, "aria-label": "More actions", "aria-expanded": menuOpen, children: jsxRuntime.jsx(server.globalLucideIcons.ChevronDown, { className: chevronIconClass }) }), menuOpen && (jsxRuntime.jsx("div", { ref: menuRef, className: `absolute right-0 top-full ${menuWidth} bg-white dark:bg-neutral-800 text-neutral-800 dark:text-white text-sm rounded-xl shadow-lg z-[100] border border-neutral-200 dark:border-neutral-700 overflow-hidden animate-fade-in`, children: menuItems.map((item, index) => (jsxRuntime.jsxs("button", { onClick: () => {
96
+ e.preventDefault(); }, children: isLoading ? (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(icons.Loader2Icon, { className: loadingIconClass }), jsxRuntime.jsx("span", { children: actualLoadingText })] })) : (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [renderIcon(mainButton.icon), jsxRuntime.jsx("span", { className: "min-w-0 truncate", children: mainButton.text })] })) }), jsxRuntime.jsx("button", { type: "button", className: utils.cn("flex h-full w-9 shrink-0 items-center justify-center px-0 py-1.5 cursor-pointer transition rounded-r-full rounded-l-none border-l sm:w-10", splitDropdownVariantClass, dropdownButtonClassName), onClick: e => { e.stopPropagation(); setMenuOpen(v => !v); }, "aria-label": "More actions", "aria-expanded": menuOpen, children: jsxRuntime.jsx(icons.ChevronDownIcon, { className: chevronIconClass }) }), menuOpen && (jsxRuntime.jsx("div", { ref: menuRef, className: `absolute right-0 top-full ${menuWidth} bg-white dark:bg-neutral-800 text-neutral-800 dark:text-white text-sm rounded-xl shadow-lg z-[100] border border-neutral-200 dark:border-neutral-700 overflow-hidden animate-fade-in`, children: menuItems.map((item, index) => (jsxRuntime.jsxs("button", { onClick: () => {
97
97
  handleButtonClick(item.onClick);
98
98
  setMenuOpen(false);
99
99
  }, disabled: item.disabled, className: `flex items-center w-full px-4 py-3 transition hover:bg-neutral-300 dark:hover:bg-neutral-600 text-left relative ${item.disabled ? disabledClass : ''}`, style: item.splitTopBorder ? { borderTop: '1px solid #AC62FD' } : undefined, children: [jsxRuntime.jsxs("span", { className: "flex items-center", children: [item.icon, jsxRuntime.jsx("span", { children: item.text })] }), item.tag && (jsxRuntime.jsx("span", { className: "absolute right-3 top-1 text-[10px] font-semibold", style: { color: item.tag.color || '#A855F7', pointerEvents: 'none' }, children: item.tag.text }))] }, index))) }))] }));
@@ -2,7 +2,7 @@
2
2
  import { __awaiter } from 'tslib';
3
3
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
4
4
  import React__default, { useState, useRef, useEffect } from 'react';
5
- import { globalLucideIcons } from '@windrun-huaiin/base-ui/components/server';
5
+ import { Loader2Icon, ChevronDownIcon } from '@windrun-huaiin/base-ui/icons';
6
6
  import { themeIconColor, themeBgColor, themeBorderColor, themeMainBgColor } from '@windrun-huaiin/base-ui/lib';
7
7
  import { cn } from '@windrun-huaiin/lib/utils';
8
8
 
@@ -79,7 +79,7 @@ function XButton(props) {
79
79
  const isDisabled = button.disabled || isLoading;
80
80
  // loadingText: props.loadingText > button.text > 'Loading...'
81
81
  const actualLoadingText = loadingText || ((_b = button.text) === null || _b === void 0 ? void 0 : _b.trim()) || 'Loading...';
82
- return (jsx("button", { onClick: () => handleButtonClick(button.onClick), disabled: isDisabled, className: cn("w-full sm:w-auto", minWidth, baseButtonClass, singleButtonVariantClass, "rounded-full", isDisabled && disabledClass, className), title: button.text, children: isLoading ? (jsxs(Fragment, { children: [jsx(globalLucideIcons.Loader2, { className: loadingIconClass }), jsx("span", { children: actualLoadingText })] })) : (jsxs(Fragment, { children: [renderIcon(button.icon), jsx("span", { children: button.text })] })) }));
82
+ return (jsx("button", { onClick: () => handleButtonClick(button.onClick), disabled: isDisabled, className: cn("w-full sm:w-auto", minWidth, baseButtonClass, singleButtonVariantClass, "rounded-full", isDisabled && disabledClass, className), title: button.text, children: isLoading ? (jsxs(Fragment, { children: [jsx(Loader2Icon, { className: loadingIconClass }), jsx("span", { children: actualLoadingText })] })) : (jsxs(Fragment, { children: [renderIcon(button.icon), jsx("span", { children: button.text })] })) }));
83
83
  }
84
84
  // Split button
85
85
  const { mainButton, menuItems, loadingText, menuWidth = 'w-full sm:w-40', className = '', mainButtonClassName = '', dropdownButtonClassName = '' } = props;
@@ -91,7 +91,7 @@ function XButton(props) {
91
91
  : variant === 'subtle'
92
92
  ? cn(themeMainBgColor, "border border-neutral-200 dark:border-neutral-800")
93
93
  : "bg-neutral-200 dark:bg-neutral-800", className), children: [jsx("button", { onClick: () => handleButtonClick(mainButton.onClick), disabled: isMainDisabled, className: cn("min-w-0 flex-1", baseButtonClass, splitMainButtonVariantClass, "rounded-l-full rounded-r-none", isMainDisabled && disabledClass, mainButtonClassName), onMouseDown: e => { if (e.button === 2)
94
- e.preventDefault(); }, children: isLoading ? (jsxs(Fragment, { children: [jsx(globalLucideIcons.Loader2, { className: loadingIconClass }), jsx("span", { children: actualLoadingText })] })) : (jsxs(Fragment, { children: [renderIcon(mainButton.icon), jsx("span", { className: "min-w-0 truncate", children: mainButton.text })] })) }), jsx("button", { type: "button", className: cn("flex h-full w-9 shrink-0 items-center justify-center px-0 py-1.5 cursor-pointer transition rounded-r-full rounded-l-none border-l sm:w-10", splitDropdownVariantClass, dropdownButtonClassName), onClick: e => { e.stopPropagation(); setMenuOpen(v => !v); }, "aria-label": "More actions", "aria-expanded": menuOpen, children: jsx(globalLucideIcons.ChevronDown, { className: chevronIconClass }) }), menuOpen && (jsx("div", { ref: menuRef, className: `absolute right-0 top-full ${menuWidth} bg-white dark:bg-neutral-800 text-neutral-800 dark:text-white text-sm rounded-xl shadow-lg z-[100] border border-neutral-200 dark:border-neutral-700 overflow-hidden animate-fade-in`, children: menuItems.map((item, index) => (jsxs("button", { onClick: () => {
94
+ e.preventDefault(); }, children: isLoading ? (jsxs(Fragment, { children: [jsx(Loader2Icon, { className: loadingIconClass }), jsx("span", { children: actualLoadingText })] })) : (jsxs(Fragment, { children: [renderIcon(mainButton.icon), jsx("span", { className: "min-w-0 truncate", children: mainButton.text })] })) }), jsx("button", { type: "button", className: cn("flex h-full w-9 shrink-0 items-center justify-center px-0 py-1.5 cursor-pointer transition rounded-r-full rounded-l-none border-l sm:w-10", splitDropdownVariantClass, dropdownButtonClassName), onClick: e => { e.stopPropagation(); setMenuOpen(v => !v); }, "aria-label": "More actions", "aria-expanded": menuOpen, children: jsx(ChevronDownIcon, { className: chevronIconClass }) }), menuOpen && (jsx("div", { ref: menuRef, className: `absolute right-0 top-full ${menuWidth} bg-white dark:bg-neutral-800 text-neutral-800 dark:text-white text-sm rounded-xl shadow-lg z-[100] border border-neutral-200 dark:border-neutral-700 overflow-hidden animate-fade-in`, children: menuItems.map((item, index) => (jsxs("button", { onClick: () => {
95
95
  handleButtonClick(item.onClick);
96
96
  setMenuOpen(false);
97
97
  }, disabled: item.disabled, className: `flex items-center w-full px-4 py-3 transition hover:bg-neutral-300 dark:hover:bg-neutral-600 text-left relative ${item.disabled ? disabledClass : ''}`, style: item.splitTopBorder ? { borderTop: '1px solid #AC62FD' } : undefined, children: [jsxs("span", { className: "flex items-center", children: [item.icon, jsx("span", { children: item.text })] }), item.tag && (jsx("span", { className: "absolute right-3 top-1 text-[10px] font-semibold", style: { color: item.tag.color || '#A855F7', pointerEvents: 'none' }, children: item.tag.text }))] }, index))) }))] }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windrun-huaiin/third-ui",
3
- "version": "15.1.0",
3
+ "version": "16.0.0",
4
4
  "description": "Third-party integrated UI components for windrun-huaiin projects",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -41,6 +41,11 @@
41
41
  "import": "./dist/main/server.mjs",
42
42
  "require": "./dist/main/server.js"
43
43
  },
44
+ "./ai": {
45
+ "types": "./dist/ai/index.d.ts",
46
+ "import": "./dist/ai/index.mjs",
47
+ "require": "./dist/ai/index.js"
48
+ },
44
49
  "./fuma/server": {
45
50
  "types": "./dist/fuma/server.d.ts",
46
51
  "import": "./dist/fuma/server.mjs",
@@ -84,14 +89,22 @@
84
89
  "fumadocs-mdx": "13.0.6",
85
90
  "fumadocs-typescript": "4.0.13",
86
91
  "fumadocs-ui": "16.0.9",
92
+ "hast-util-to-jsx-runtime": "^2.3.6",
87
93
  "katex": "^0.16.33",
88
94
  "mermaid": "11.12.1",
89
95
  "react-medium-image-zoom": "^5.4.1",
96
+ "remark": "^15.0.1",
97
+ "remark-gfm": "^4.0.1",
98
+ "remark-parse": "^11.0.0",
99
+ "remark-rehype": "^11.1.2",
100
+ "roughjs": "^4.6.6",
90
101
  "swiper": "^12.1.2",
91
102
  "tslib": "^2.8.1",
103
+ "unified": "^11.0.5",
92
104
  "zod": "^4.3.6",
93
- "@windrun-huaiin/base-ui": "^15.1.0",
94
- "@windrun-huaiin/lib": "^15.1.0"
105
+ "@windrun-huaiin/base-ui": "^16.0.0",
106
+ "@windrun-huaiin/contracts": "^15.0.0",
107
+ "@windrun-huaiin/lib": "^16.0.0"
95
108
  },
96
109
  "peerDependencies": {
97
110
  "clsx": "^2.1.1",
@@ -0,0 +1,187 @@
1
+ 'use client';
2
+
3
+ import { cn } from '@windrun-huaiin/lib/utils';
4
+ import { useEffect, useRef } from 'react';
5
+ import type { KeyboardEvent } from 'react';
6
+ import type { AIChatComposerProps } from './types';
7
+
8
+ function resizeTextarea(
9
+ textarea: HTMLTextAreaElement,
10
+ minHeight: number,
11
+ maxHeight: number,
12
+ ) {
13
+ textarea.style.height = 'auto';
14
+ const nextHeight = Math.min(Math.max(textarea.scrollHeight, minHeight), maxHeight);
15
+ textarea.style.height = `${nextHeight}px`;
16
+ textarea.style.overflowY = textarea.scrollHeight > maxHeight ? 'auto' : 'hidden';
17
+ }
18
+
19
+ export function AIChatComposer({
20
+ value,
21
+ onChange,
22
+ onSubmit,
23
+ onStop,
24
+ disabled = false,
25
+ isStreaming = false,
26
+ placeholder = 'Ask anything...',
27
+ className,
28
+ leftSlot,
29
+ attachments,
30
+ helper,
31
+ submitLabel = 'Send',
32
+ stopLabel = 'Stop',
33
+ minHeight = 52,
34
+ maxHeight = 220,
35
+ submitOnEnter = true,
36
+ shellClassName,
37
+ textareaClassName,
38
+ submitControl,
39
+ stopControl,
40
+ textareaRef: externalTextareaRef,
41
+ secondaryActions,
42
+ actionLayout = 'inline',
43
+ }: AIChatComposerProps) {
44
+ const internalTextareaRef = useRef<HTMLTextAreaElement | null>(null);
45
+ const textareaRef = externalTextareaRef ?? internalTextareaRef;
46
+
47
+ useEffect(() => {
48
+ if (!textareaRef.current) {
49
+ return;
50
+ }
51
+
52
+ resizeTextarea(textareaRef.current, minHeight, maxHeight);
53
+ }, [actionLayout, maxHeight, minHeight, value]);
54
+
55
+ const handleKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
56
+ if (!submitOnEnter || event.nativeEvent.isComposing) {
57
+ return;
58
+ }
59
+
60
+ if (event.key === 'Enter' && !event.shiftKey) {
61
+ event.preventDefault();
62
+ if (isStreaming && onStop) {
63
+ onStop();
64
+ return;
65
+ }
66
+
67
+ if (!disabled && value.trim()) {
68
+ onSubmit();
69
+ }
70
+ }
71
+ };
72
+
73
+ const primaryAction = isStreaming && onStop
74
+ ? (
75
+ stopControl ?? (
76
+ <button
77
+ type="button"
78
+ onClick={onStop}
79
+ className="inline-flex h-10 items-center justify-center rounded-2xl border border-border px-4 text-sm text-foreground transition hover:bg-muted"
80
+ >
81
+ {stopLabel}
82
+ </button>
83
+ )
84
+ )
85
+ : (
86
+ submitControl ?? (
87
+ <button
88
+ type="button"
89
+ onClick={onSubmit}
90
+ disabled={disabled || isStreaming || value.trim().length === 0}
91
+ className="inline-flex h-10 items-center justify-center rounded-2xl bg-foreground px-4 text-sm text-background transition disabled:cursor-not-allowed disabled:opacity-50"
92
+ >
93
+ {submitLabel}
94
+ </button>
95
+ )
96
+ );
97
+
98
+ if (actionLayout === 'stacked') {
99
+ return (
100
+ <div className={cn('space-y-3', className)}>
101
+ {attachments ? <div>{attachments}</div> : null}
102
+
103
+ <div
104
+ className={cn(
105
+ 'rounded-3xl border border-border bg-background px-3 py-3',
106
+ shellClassName,
107
+ )}
108
+ >
109
+ <div className="flex items-end gap-3">
110
+ <div className="flex shrink-0 items-center">
111
+ {leftSlot}
112
+ </div>
113
+
114
+ <div className="min-w-0 flex-1">
115
+ <textarea
116
+ ref={textareaRef}
117
+ rows={1}
118
+ value={value}
119
+ onChange={(event) => onChange(event.target.value)}
120
+ onKeyDown={handleKeyDown}
121
+ placeholder={placeholder}
122
+ disabled={disabled}
123
+ className={cn(
124
+ 'block w-full resize-none border-0 bg-transparent px-0 py-2 text-sm leading-6 text-foreground outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-60 box-border',
125
+ textareaClassName,
126
+ )}
127
+ style={{ minHeight: `${minHeight}px`, maxHeight: `${maxHeight}px` }}
128
+ />
129
+ </div>
130
+ </div>
131
+
132
+ <div className="mt-3 flex items-center justify-between gap-3 border-t border-border/70 pt-3">
133
+ <div className="flex min-w-0 flex-1 items-center gap-2">
134
+ {secondaryActions}
135
+ </div>
136
+ <div className="flex shrink-0 items-center gap-2">
137
+ {primaryAction}
138
+ </div>
139
+ </div>
140
+ </div>
141
+
142
+ {helper ? <div>{helper}</div> : null}
143
+ </div>
144
+ );
145
+ }
146
+
147
+ return (
148
+ <div className={cn('space-y-3', className)}>
149
+ {attachments ? <div>{attachments}</div> : null}
150
+
151
+ <div
152
+ className={cn(
153
+ 'flex items-end gap-3 rounded-3xl border border-border bg-background px-3 py-3',
154
+ shellClassName,
155
+ )}
156
+ >
157
+ <div className="flex shrink-0 items-center">
158
+ {leftSlot}
159
+ </div>
160
+
161
+ <div className="min-w-0 flex-1">
162
+ <textarea
163
+ ref={textareaRef}
164
+ rows={1}
165
+ value={value}
166
+ onChange={(event) => onChange(event.target.value)}
167
+ onKeyDown={handleKeyDown}
168
+ placeholder={placeholder}
169
+ disabled={disabled}
170
+ className={cn(
171
+ 'block w-full resize-none border-0 bg-transparent px-0 py-2 text-sm leading-6 text-foreground outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-60 box-border',
172
+ textareaClassName,
173
+ )}
174
+ style={{ minHeight: `${minHeight}px`, maxHeight: `${maxHeight}px` }}
175
+ />
176
+ </div>
177
+
178
+ <div className="flex shrink-0 items-center gap-2">
179
+ {secondaryActions}
180
+ {primaryAction}
181
+ </div>
182
+ </div>
183
+
184
+ {helper ? <div>{helper}</div> : null}
185
+ </div>
186
+ );
187
+ }
@@ -0,0 +1,45 @@
1
+ 'use client';
2
+
3
+ import { cn } from '@windrun-huaiin/lib/utils';
4
+ import { toJsxRuntime } from 'hast-util-to-jsx-runtime';
5
+ import { baseMarkdownComponents } from '../fuma/mdx/markdown-component-map';
6
+ import { useMemo } from 'react';
7
+ import { Fragment, jsx, jsxs } from 'react/jsx-runtime';
8
+ import remarkGfm from 'remark-gfm';
9
+ import remarkParse from 'remark-parse';
10
+ import remarkRehype from 'remark-rehype';
11
+ import { unified } from 'unified';
12
+ import type { AIMarkdownComponentMap, AIMarkdownProps } from './types';
13
+
14
+ const processor = unified()
15
+ .use(remarkParse)
16
+ .use(remarkGfm)
17
+ .use(remarkRehype);
18
+
19
+ const defaultComponents: AIMarkdownComponentMap = baseMarkdownComponents;
20
+
21
+ export function AIMarkdown({ content, className, components }: AIMarkdownProps) {
22
+ const tree = useMemo(() => {
23
+ return processor.runSync(processor.parse(content));
24
+ }, [content]);
25
+
26
+ const element = useMemo(() => {
27
+ return toJsxRuntime(tree, {
28
+ Fragment,
29
+ jsx,
30
+ jsxs,
31
+ components: {
32
+ ...defaultComponents,
33
+ ...(components ?? {}),
34
+ },
35
+ elementAttributeNameCase: 'html',
36
+ stylePropertyNameCase: 'css',
37
+ });
38
+ }, [components, tree]);
39
+
40
+ return (
41
+ <div className={cn('space-y-4 text-sm text-inherit', className)}>
42
+ {element}
43
+ </div>
44
+ );
45
+ }
@@ -0,0 +1,16 @@
1
+ 'use client';
2
+
3
+ import { cn } from '@windrun-huaiin/lib/utils';
4
+ import type { AIMessageActionsProps } from './types';
5
+
6
+ export function AIMessageActions({ className, children }: AIMessageActionsProps) {
7
+ if (!children) {
8
+ return null;
9
+ }
10
+
11
+ return (
12
+ <div className={cn('flex flex-wrap items-center justify-end gap-2', className)}>
13
+ {children}
14
+ </div>
15
+ );
16
+ }
@@ -0,0 +1,138 @@
1
+ 'use client';
2
+
3
+ import type { ConversationMessage } from '@windrun-huaiin/contracts/ai';
4
+ import { cn } from '@windrun-huaiin/lib/utils';
5
+ import { useEffect, useRef, useState } from 'react';
6
+ import { AIMessageActions } from './ai-message-actions';
7
+ import { AIMessageContent } from './ai-message-content';
8
+ import { AIMessageMeta } from './ai-message-meta';
9
+ import type { AIMessageBubbleProps } from './types';
10
+
11
+ function getRoleLabel(role: ConversationMessage['role']) {
12
+ switch (role) {
13
+ case 'assistant':
14
+ return 'Assistant';
15
+ case 'system':
16
+ return 'System';
17
+ case 'tool':
18
+ return 'Tool';
19
+ default:
20
+ return 'You';
21
+ }
22
+ }
23
+
24
+ export function AIMessageBubble({
25
+ message,
26
+ className,
27
+ cardClassName,
28
+ contentClassName,
29
+ footerClassName,
30
+ maxWidthClassName,
31
+ showRoleLabel = false,
32
+ markdownComponents,
33
+ showFooter = true,
34
+ renderContent,
35
+ renderMeta,
36
+ renderActions,
37
+ }: AIMessageBubbleProps) {
38
+ const isUser = message.role === 'user';
39
+ const contentWrapperRef = useRef<HTMLDivElement | null>(null);
40
+ const [isCompactSingleLine, setIsCompactSingleLine] = useState(false);
41
+ const content = renderContent
42
+ ? renderContent(message)
43
+ : <AIMessageContent message={message} className={contentClassName} markdownComponents={markdownComponents} />;
44
+ const meta = renderMeta ? renderMeta(message) : <AIMessageMeta message={message} />;
45
+ const actions = renderActions ? renderActions(message) : null;
46
+ const hasFooter = Boolean(meta) || Boolean(actions);
47
+ const isTextOnlyMessage =
48
+ message.parts.length > 0
49
+ ? message.parts.every((part) => part.type === 'text')
50
+ : Boolean(message.errorMessage);
51
+
52
+ useEffect(() => {
53
+ if (!isTextOnlyMessage || !contentWrapperRef.current) {
54
+ setIsCompactSingleLine(false);
55
+ return;
56
+ }
57
+
58
+ const element = contentWrapperRef.current;
59
+
60
+ const measure = () => {
61
+ const computedStyle = window.getComputedStyle(element);
62
+ const lineHeight = Number.parseFloat(computedStyle.lineHeight);
63
+ if (!Number.isFinite(lineHeight) || lineHeight <= 0) {
64
+ setIsCompactSingleLine(false);
65
+ return;
66
+ }
67
+
68
+ const nextIsSingleLine = element.scrollHeight <= lineHeight * 1.75;
69
+ setIsCompactSingleLine(nextIsSingleLine);
70
+ };
71
+
72
+ measure();
73
+
74
+ const observer = new ResizeObserver(() => {
75
+ measure();
76
+ });
77
+
78
+ observer.observe(element);
79
+
80
+ return () => {
81
+ observer.disconnect();
82
+ };
83
+ }, [isTextOnlyMessage, message.errorMessage, message.id, message.parts]);
84
+
85
+ return (
86
+ <div
87
+ className={cn(
88
+ 'flex w-full',
89
+ isUser ? 'justify-end' : 'justify-start',
90
+ className,
91
+ )}
92
+ >
93
+ <article
94
+ className={cn(
95
+ 'min-h-12 min-w-30 max-w-full rounded-3xl border px-4 py-3 sm:min-h-13 sm:min-w-36',
96
+ isUser ? 'w-fit' : 'w-full',
97
+ maxWidthClassName ?? 'max-w-[92%] sm:max-w-[82%]',
98
+ isUser
99
+ ? 'border-foreground/10 bg-foreground text-background'
100
+ : 'border-border bg-background text-foreground',
101
+ cardClassName,
102
+ )}
103
+ >
104
+ {showRoleLabel ? (
105
+ <div className="mb-2 flex items-center gap-3">
106
+ <span className="text-[11px] font-semibold uppercase tracking-[0.14em] opacity-60">
107
+ {getRoleLabel(message.role)}
108
+ </span>
109
+ </div>
110
+ ) : null}
111
+
112
+ <div
113
+ ref={contentWrapperRef}
114
+ className={cn(
115
+ 'min-w-0',
116
+ isTextOnlyMessage && isCompactSingleLine && 'flex justify-center',
117
+ )}
118
+ >
119
+ {content}
120
+ </div>
121
+
122
+ {showFooter && hasFooter ? (
123
+ <div
124
+ className={cn(
125
+ 'mt-3 flex flex-wrap items-center justify-between gap-3 border-t border-border/70 pt-3',
126
+ footerClassName,
127
+ )}
128
+ >
129
+ <div className="min-w-0 flex-1">
130
+ {meta}
131
+ </div>
132
+ {actions ? <AIMessageActions>{actions}</AIMessageActions> : null}
133
+ </div>
134
+ ) : null}
135
+ </article>
136
+ </div>
137
+ );
138
+ }