@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.
- package/LICENSE +1 -1
- package/dist/ai/ai-chat-composer.d.ts +2 -0
- package/dist/ai/ai-chat-composer.js +47 -0
- package/dist/ai/ai-chat-composer.mjs +45 -0
- package/dist/ai/ai-markdown.d.ts +2 -0
- package/dist/ai/ai-markdown.js +36 -0
- package/dist/ai/ai-markdown.mjs +34 -0
- package/dist/ai/ai-message-actions.d.ts +2 -0
- package/dist/ai/ai-message-actions.js +14 -0
- package/dist/ai/ai-message-actions.mjs +12 -0
- package/dist/ai/ai-message-bubble.d.ts +2 -0
- package/dist/ai/ai-message-bubble.js +66 -0
- package/dist/ai/ai-message-bubble.mjs +64 -0
- package/dist/ai/ai-message-content.d.ts +2 -0
- package/dist/ai/ai-message-content.js +63 -0
- package/dist/ai/ai-message-content.mjs +61 -0
- package/dist/ai/ai-message-list.d.ts +2 -0
- package/dist/ai/ai-message-list.js +24 -0
- package/dist/ai/ai-message-list.mjs +22 -0
- package/dist/ai/ai-message-meta.d.ts +2 -0
- package/dist/ai/ai-message-meta.js +38 -0
- package/dist/ai/ai-message-meta.mjs +36 -0
- package/dist/ai/ai-status-indicator.d.ts +2 -0
- package/dist/ai/ai-status-indicator.js +51 -0
- package/dist/ai/ai-status-indicator.mjs +49 -0
- package/dist/ai/index.d.ts +11 -0
- package/dist/ai/index.js +33 -0
- package/dist/ai/index.mjs +11 -0
- package/dist/ai/types.d.ts +110 -0
- package/dist/ai/use-ai-conversation.d.ts +13 -0
- package/dist/ai/use-ai-conversation.js +276 -0
- package/dist/ai/use-ai-conversation.mjs +274 -0
- package/dist/clerk/clerk-organization-client.js +2 -2
- package/dist/clerk/clerk-organization-client.mjs +2 -2
- package/dist/clerk/clerk-page-generator.d.ts +1 -1
- package/dist/clerk/clerk-user-client.js +2 -2
- package/dist/clerk/clerk-user-client.mjs +2 -2
- package/dist/clerk/fingerprint/fingerprint-provider.js +9 -9
- package/dist/clerk/fingerprint/fingerprint-provider.mjs +9 -9
- package/dist/fuma/base/custom-header.js +4 -4
- package/dist/fuma/base/custom-header.mjs +4 -4
- package/dist/fuma/mdx/banner.js +3 -3
- package/dist/fuma/mdx/banner.mjs +3 -3
- package/dist/fuma/mdx/fuma-github-info.js +3 -3
- package/dist/fuma/mdx/fuma-github-info.mjs +3 -3
- package/dist/fuma/mdx/gradient-button.js +3 -3
- package/dist/fuma/mdx/gradient-button.mjs +3 -3
- package/dist/fuma/mdx/index.d.ts +1 -0
- package/dist/fuma/mdx/index.js +2 -0
- package/dist/fuma/mdx/index.mjs +1 -0
- package/dist/fuma/mdx/markdown-component-map.d.ts +3 -0
- package/dist/fuma/mdx/markdown-component-map.js +73 -0
- package/dist/fuma/mdx/markdown-component-map.mjs +71 -0
- package/dist/fuma/mdx/mermaid.d.ts +2 -1
- package/dist/fuma/mdx/mermaid.js +130 -6
- package/dist/fuma/mdx/mermaid.mjs +130 -6
- package/dist/fuma/mdx/toc-base.js +4 -4
- package/dist/fuma/mdx/toc-base.mjs +4 -4
- package/dist/fuma/mdx/trophy-card.js +2 -2
- package/dist/fuma/mdx/trophy-card.mjs +2 -2
- package/dist/fuma/mdx/zia-card.js +3 -3
- package/dist/fuma/mdx/zia-card.mjs +3 -3
- package/dist/fuma/mdx/zia-file.js +3 -3
- package/dist/fuma/mdx/zia-file.mjs +3 -3
- package/dist/main/ads-alert-dialog.js +2 -2
- package/dist/main/ads-alert-dialog.mjs +2 -2
- package/dist/main/credit/credit-nav-button.js +2 -2
- package/dist/main/credit/credit-nav-button.mjs +2 -2
- package/dist/main/credit/credit-overview-client.js +4 -4
- package/dist/main/credit/credit-overview-client.mjs +4 -4
- package/dist/main/footer.js +2 -2
- package/dist/main/footer.mjs +2 -2
- package/dist/main/go-to-top.js +2 -2
- package/dist/main/go-to-top.mjs +2 -2
- package/dist/main/index.d.ts +1 -0
- package/dist/main/index.js +2 -0
- package/dist/main/index.mjs +1 -0
- package/dist/main/info-tooltip.d.ts +8 -0
- package/dist/main/info-tooltip.js +48 -0
- package/dist/main/info-tooltip.mjs +46 -0
- package/dist/main/pill-select/x-pill-select.js +2 -2
- package/dist/main/pill-select/x-pill-select.mjs +2 -2
- package/dist/main/pill-select/x-token-input.js +2 -2
- package/dist/main/pill-select/x-token-input.mjs +2 -2
- package/dist/main/x-button.js +3 -3
- package/dist/main/x-button.mjs +3 -3
- package/package.json +16 -3
- package/src/ai/ai-chat-composer.tsx +187 -0
- package/src/ai/ai-markdown.tsx +45 -0
- package/src/ai/ai-message-actions.tsx +16 -0
- package/src/ai/ai-message-bubble.tsx +138 -0
- package/src/ai/ai-message-content.tsx +149 -0
- package/src/ai/ai-message-list.tsx +59 -0
- package/src/ai/ai-message-meta.tsx +56 -0
- package/src/ai/ai-status-indicator.tsx +61 -0
- package/src/ai/index.ts +13 -0
- package/src/ai/types.ts +131 -0
- package/src/ai/use-ai-conversation.ts +422 -0
- package/src/clerk/clerk-organization-client.tsx +5 -5
- package/src/clerk/clerk-page-generator.tsx +1 -1
- package/src/clerk/clerk-user-client.tsx +4 -4
- package/src/clerk/fingerprint/fingerprint-provider.tsx +34 -22
- package/src/fuma/base/custom-header.tsx +5 -5
- package/src/fuma/mdx/banner.tsx +3 -3
- package/src/fuma/mdx/fuma-github-info.tsx +4 -4
- package/src/fuma/mdx/gradient-button.tsx +3 -3
- package/src/fuma/mdx/index.ts +2 -1
- package/src/fuma/mdx/markdown-component-map.tsx +174 -0
- package/src/fuma/mdx/mermaid.tsx +145 -10
- package/src/fuma/mdx/toc-base.tsx +5 -5
- package/src/fuma/mdx/trophy-card.tsx +2 -2
- package/src/fuma/mdx/zia-card.tsx +3 -3
- package/src/fuma/mdx/zia-file.tsx +3 -3
- package/src/main/ads-alert-dialog.tsx +5 -5
- package/src/main/credit/credit-nav-button.tsx +3 -3
- package/src/main/credit/credit-overview-client.tsx +15 -7
- package/src/main/features.tsx +5 -3
- package/src/main/footer.tsx +4 -5
- package/src/main/go-to-top.tsx +2 -2
- package/src/main/index.ts +2 -0
- package/src/main/info-tooltip.tsx +99 -0
- package/src/main/language-detector.tsx +4 -4
- package/src/main/pill-select/x-pill-select.tsx +2 -2
- package/src/main/pill-select/x-token-input.tsx +2 -2
- 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 {
|
|
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(
|
|
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]'
|
package/dist/main/x-button.js
CHANGED
|
@@ -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
|
|
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(
|
|
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(
|
|
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))) }))] }));
|
package/dist/main/x-button.mjs
CHANGED
|
@@ -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 {
|
|
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(
|
|
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(
|
|
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": "
|
|
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": "^
|
|
94
|
-
"@windrun-huaiin/lib": "^
|
|
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
|
+
}
|