@windrun-huaiin/third-ui 15.1.1 → 16.0.1

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 (125) 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/index.d.ts +1 -0
  76. package/dist/main/index.js +2 -0
  77. package/dist/main/index.mjs +1 -0
  78. package/dist/main/info-tooltip.d.ts +8 -0
  79. package/dist/main/info-tooltip.js +48 -0
  80. package/dist/main/info-tooltip.mjs +46 -0
  81. package/dist/main/pill-select/x-pill-select.js +2 -2
  82. package/dist/main/pill-select/x-pill-select.mjs +2 -2
  83. package/dist/main/pill-select/x-token-input.js +2 -2
  84. package/dist/main/pill-select/x-token-input.mjs +2 -2
  85. package/dist/main/x-button.js +3 -3
  86. package/dist/main/x-button.mjs +3 -3
  87. package/package.json +16 -3
  88. package/src/ai/ai-chat-composer.tsx +187 -0
  89. package/src/ai/ai-markdown.tsx +45 -0
  90. package/src/ai/ai-message-actions.tsx +16 -0
  91. package/src/ai/ai-message-bubble.tsx +138 -0
  92. package/src/ai/ai-message-content.tsx +149 -0
  93. package/src/ai/ai-message-list.tsx +59 -0
  94. package/src/ai/ai-message-meta.tsx +56 -0
  95. package/src/ai/ai-status-indicator.tsx +61 -0
  96. package/src/ai/index.ts +13 -0
  97. package/src/ai/types.ts +131 -0
  98. package/src/ai/use-ai-conversation.ts +422 -0
  99. package/src/clerk/clerk-organization-client.tsx +5 -5
  100. package/src/clerk/clerk-page-generator.tsx +1 -1
  101. package/src/clerk/clerk-user-client.tsx +4 -4
  102. package/src/clerk/fingerprint/fingerprint-provider.tsx +34 -22
  103. package/src/fuma/base/custom-header.tsx +5 -5
  104. package/src/fuma/mdx/banner.tsx +3 -3
  105. package/src/fuma/mdx/fuma-github-info.tsx +4 -4
  106. package/src/fuma/mdx/gradient-button.tsx +3 -3
  107. package/src/fuma/mdx/index.ts +2 -1
  108. package/src/fuma/mdx/markdown-component-map.tsx +174 -0
  109. package/src/fuma/mdx/mermaid.tsx +145 -10
  110. package/src/fuma/mdx/toc-base.tsx +5 -5
  111. package/src/fuma/mdx/trophy-card.tsx +2 -2
  112. package/src/fuma/mdx/zia-card.tsx +3 -3
  113. package/src/fuma/mdx/zia-file.tsx +3 -3
  114. package/src/main/ads-alert-dialog.tsx +5 -5
  115. package/src/main/credit/credit-nav-button.tsx +3 -3
  116. package/src/main/credit/credit-overview-client.tsx +15 -7
  117. package/src/main/features.tsx +5 -3
  118. package/src/main/footer.tsx +4 -5
  119. package/src/main/go-to-top.tsx +2 -2
  120. package/src/main/index.ts +2 -0
  121. package/src/main/info-tooltip.tsx +99 -0
  122. package/src/main/language-detector.tsx +4 -4
  123. package/src/main/pill-select/x-pill-select.tsx +2 -2
  124. package/src/main/pill-select/x-token-input.tsx +2 -2
  125. package/src/main/x-button.tsx +4 -4
@@ -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.1",
3
+ "version": "16.0.1",
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.1",
94
- "@windrun-huaiin/lib": "^15.1.1"
105
+ "@windrun-huaiin/base-ui": "^16.0.0",
106
+ "@windrun-huaiin/lib": "^16.0.0",
107
+ "@windrun-huaiin/contracts": "^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
+ }
@@ -0,0 +1,149 @@
1
+ 'use client';
2
+
3
+ import type { MessagePart } from '@windrun-huaiin/contracts/ai';
4
+ import { themeIconColor } from '@windrun-huaiin/base-ui/lib';
5
+ import { cn } from '@windrun-huaiin/lib/utils';
6
+ import { TrophyCard } from '../fuma/mdx/trophy-card';
7
+ import { AIMarkdown } from './ai-markdown';
8
+ import type { AIMessageContentProps } from './types';
9
+
10
+ function hasRenderablePart(part: MessagePart) {
11
+ if (part.type === 'text') {
12
+ return part.text.trim().length > 0;
13
+ }
14
+
15
+ return true;
16
+ }
17
+
18
+ function getEmptyAssistantFallback(message: AIMessageContentProps['message']) {
19
+ if (message.status === 'streaming') {
20
+ return null;
21
+ }
22
+
23
+ if (message.status === 'timeout') {
24
+ return 'No response received. The request timed out before the model returned any content.';
25
+ }
26
+
27
+ if (message.status === 'request_aborted' || message.status === 'stopped') {
28
+ return 'Response stopped before any content was returned.';
29
+ }
30
+
31
+ if (message.status === 'failed') {
32
+ return message.errorMessage || 'The model did not return any visible content.';
33
+ }
34
+
35
+ if (message.errorMessage) {
36
+ return message.errorMessage;
37
+ }
38
+
39
+ return 'The model returned no visible content.';
40
+ }
41
+
42
+ function renderPart(
43
+ part: MessagePart,
44
+ index: number,
45
+ markdownComponents?: AIMessageContentProps['markdownComponents'],
46
+ ) {
47
+ if (part.type === 'text') {
48
+ return (
49
+ <AIMarkdown
50
+ key={`text-${index}`}
51
+ content={part.text}
52
+ components={markdownComponents}
53
+ />
54
+ );
55
+ }
56
+
57
+ if (part.type === 'image') {
58
+ return (
59
+ <div
60
+ key={`image-${index}`}
61
+ className="rounded-2xl border border-dashed border-border/70 px-3 py-2 text-xs text-muted-foreground"
62
+ >
63
+ Image part reserved: {part.alt || part.url}
64
+ </div>
65
+ );
66
+ }
67
+
68
+ if (part.type === 'trophy_card') {
69
+ return (
70
+ <div
71
+ key={`trophy-card-${index}`}
72
+ className="rounded-2xl bg-muted/35 p-1 text-foreground"
73
+ >
74
+ <TrophyCard title={part.title}>
75
+ {part.description ? (
76
+ <div className="mt-2">
77
+ <AIMarkdown
78
+ content={part.description}
79
+ components={markdownComponents}
80
+ className="space-y-3 text-sm text-inherit"
81
+ />
82
+ </div>
83
+ ) : null}
84
+ </TrophyCard>
85
+ </div>
86
+ );
87
+ }
88
+
89
+ return (
90
+ <div
91
+ key={`file-${index}`}
92
+ className="rounded-2xl border border-dashed border-border/70 px-3 py-2 text-xs text-muted-foreground"
93
+ >
94
+ File part reserved: {part.name || part.url}
95
+ </div>
96
+ );
97
+ }
98
+
99
+ export function AIMessageContent({
100
+ message,
101
+ className,
102
+ markdownComponents,
103
+ }: AIMessageContentProps) {
104
+ const parts = message.parts.filter(hasRenderablePart);
105
+
106
+ if (
107
+ message.role === 'assistant' &&
108
+ message.status === 'streaming' &&
109
+ parts.length === 0
110
+ ) {
111
+ return (
112
+ <div
113
+ className={cn(
114
+ 'flex items-center gap-2 text-sm text-muted-foreground',
115
+ themeIconColor,
116
+ className,
117
+ )}
118
+ >
119
+ <span>AI is thinking</span>
120
+ <span className="inline-flex items-center gap-1" aria-hidden="true">
121
+ <span className="size-1.5 rounded-full bg-current animate-pulse [animation-delay:0ms]" />
122
+ <span className="size-1.5 rounded-full bg-current animate-pulse [animation-delay:180ms]" />
123
+ <span className="size-1.5 rounded-full bg-current animate-pulse [animation-delay:360ms]" />
124
+ </span>
125
+ </div>
126
+ );
127
+ }
128
+
129
+ if (parts.length === 0) {
130
+ const fallbackText = getEmptyAssistantFallback(message);
131
+ if (!fallbackText) {
132
+ return null;
133
+ }
134
+
135
+ return (
136
+ <AIMarkdown
137
+ content={fallbackText}
138
+ components={markdownComponents}
139
+ className={cn('space-y-3 text-sm text-inherit', className)}
140
+ />
141
+ );
142
+ }
143
+
144
+ return (
145
+ <div className={cn('space-y-3', className)}>
146
+ {parts.map((part, index) => renderPart(part, index, markdownComponents))}
147
+ </div>
148
+ );
149
+ }