@tbox.cn/app-template-agent 0.7.5 → 0.8.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.
- package/.gitignore +5 -0
- package/.tbox/app.json +9 -1
- package/AGENTS.md +12 -4
- package/ENV.md +102 -17
- package/README.md +36 -34
- package/apps/client/VISUAL_RESTORE.md +11 -11
- package/apps/client/index.html +3 -1
- package/apps/client/package.json +9 -8
- package/apps/client/src/business/App.tsx +4 -4
- package/apps/client/src/business/README.md +5 -11
- package/apps/client/src/cards/registry.ts +23 -8
- package/apps/client/src/main.tsx +36 -2
- package/apps/client/src/modules.ts +7 -4
- package/apps/client/src/pages/index/index.tsx +18 -162
- package/apps/client/src/platform/AgentChatApp.tsx +3 -3
- package/apps/client/src/platform/agent-runtime.ts +5 -0
- package/apps/client/src/store/chat.ts +47 -185
- package/apps/client/src/test/setup.ts +7 -0
- package/apps/client/vite.config.ts +16 -1
- package/apps/server/package.json +0 -1
- package/apps/server/src/app-tools.ts +2 -1
- package/apps/server/src/app.ts +178 -110
- package/apps/server/src/config/integration.ts +2 -6
- package/apps/server/src/dev.ts +0 -1
- package/apps/server/src/intent-routes.ts +3 -3
- package/apps/server/src/modules.ts +4 -4
- package/apps/server/src/pre-tools.ts +4 -3
- package/apps/server/tests/agent-prompt.test.ts +81 -0
- package/apps/server/tests/cards-registry.test.ts +1 -3
- package/apps/server/tests/deployment-loader.test.ts +35 -56
- package/apps/server/tests/tool-runtime.test.ts +25 -61
- package/package.json +5 -4
- package/packages/agent-ui-kit/LICENSE +21 -0
- package/packages/agent-ui-kit/README.md +17 -0
- package/packages/agent-ui-kit/package.json +58 -0
- package/packages/agent-ui-kit/src/ChatPage.less +62 -0
- package/packages/agent-ui-kit/src/ChatPage.tsx +305 -0
- package/packages/agent-ui-kit/src/chat-config.test.ts +82 -0
- package/packages/agent-ui-kit/src/chat-config.ts +62 -0
- package/{apps/client → packages/agent-ui-kit}/src/components/ActionBar/DebugButton.tsx +2 -2
- package/{apps/client → packages/agent-ui-kit}/src/components/ActionBar/index.less +19 -16
- package/{apps/client → packages/agent-ui-kit}/src/components/ActionBar/index.tsx +10 -13
- package/packages/agent-ui-kit/src/components/CardRenderer.tsx +36 -0
- package/packages/agent-ui-kit/src/components/ChatHeader/index.less +148 -0
- package/packages/agent-ui-kit/src/components/ChatHeader/index.test.tsx +74 -0
- package/{apps/client → packages/agent-ui-kit}/src/components/ChatHeader/index.tsx +27 -15
- package/{apps/client → packages/agent-ui-kit}/src/components/ChatInput/index.less +47 -39
- package/packages/agent-ui-kit/src/components/ChatInput/index.test.tsx +156 -0
- package/{apps/client → packages/agent-ui-kit}/src/components/ChatInput/index.tsx +13 -4
- package/{apps/client → packages/agent-ui-kit}/src/components/ChatInput/voice/useVoicePress.ts +2 -2
- package/packages/agent-ui-kit/src/components/HistoryEntry/index.less +49 -0
- package/packages/agent-ui-kit/src/components/HistoryEntry/index.tsx +42 -0
- package/{apps/client → packages/agent-ui-kit}/src/components/MediaList/index.less +3 -3
- package/{apps/client → packages/agent-ui-kit}/src/components/MediaList/index.tsx +6 -6
- package/{apps/client → packages/agent-ui-kit}/src/components/MessageBubble/index.less +25 -20
- package/packages/agent-ui-kit/src/components/MessageBubble/index.test.tsx +112 -0
- package/{apps/client → packages/agent-ui-kit}/src/components/MessageBubble/index.tsx +22 -15
- package/{apps/client → packages/agent-ui-kit}/src/components/PullDownIndicator/index.less +2 -2
- package/{apps/client → packages/agent-ui-kit}/src/components/PullDownIndicator/index.tsx +2 -2
- package/packages/agent-ui-kit/src/components/QuickCommands/index.less +63 -0
- package/packages/agent-ui-kit/src/components/QuickCommands/index.test.tsx +99 -0
- package/packages/agent-ui-kit/src/components/QuickCommands/index.tsx +52 -0
- package/{apps/client → packages/agent-ui-kit}/src/components/ScrollToBottomButton/index.less +1 -1
- package/packages/agent-ui-kit/src/components/ToolCallBlock/detail.test.ts +346 -0
- package/packages/agent-ui-kit/src/components/ToolCallBlock/detail.ts +204 -0
- package/packages/agent-ui-kit/src/components/ToolCallBlock/index.less +312 -0
- package/packages/agent-ui-kit/src/components/ToolCallBlock/index.test.tsx +261 -0
- package/packages/agent-ui-kit/src/components/ToolCallBlock/index.tsx +212 -0
- package/packages/agent-ui-kit/src/components/ToolCallBlock/titles.ts +102 -0
- package/packages/agent-ui-kit/src/hooks/use-drag-gesture.test.ts +66 -0
- package/packages/agent-ui-kit/src/hooks/use-drag-gesture.ts +129 -0
- package/packages/agent-ui-kit/src/hooks/use-pull-down-history.ts +241 -0
- package/packages/agent-ui-kit/src/hooks/use-pull-down-wheel-native.test.ts +64 -0
- package/{apps/client → packages/agent-ui-kit}/src/hooks/useMediaUpload.ts +3 -3
- package/{apps/client → packages/agent-ui-kit}/src/hooks/useTTSAutoPlay.ts +7 -5
- package/packages/agent-ui-kit/src/index.ts +69 -0
- package/packages/agent-ui-kit/src/store-context.tsx +92 -0
- package/packages/agent-ui-kit/src/test/setup.ts +7 -0
- package/packages/agent-ui-kit/src/theme/contract.json +72 -0
- package/packages/agent-ui-kit/src/theme/global.less +99 -0
- package/packages/agent-ui-kit/src/theme/motion.less +19 -0
- package/packages/agent-ui-kit/src/theme/theme-contract.test.ts +69 -0
- package/packages/agent-ui-kit/src/theme/theme.ts +17 -0
- package/packages/agent-ui-kit/src/theme/tokens.less +79 -0
- package/packages/agent-ui-kit/src/utils/haptics.test.ts +51 -0
- package/packages/agent-ui-kit/src/utils/haptics.ts +44 -0
- package/packages/agent-ui-kit/src/utils/pull-down-machine.test.ts +188 -0
- package/packages/agent-ui-kit/src/utils/pull-down-machine.ts +157 -0
- package/packages/agent-ui-kit/src/utils/tool-call-display.test.ts +240 -0
- package/packages/agent-ui-kit/src/utils/tool-call-display.ts +146 -0
- package/packages/agent-ui-kit/src/vite-env.d.ts +1 -0
- package/packages/agent-ui-kit/tsconfig.json +8 -0
- package/packages/agent-ui-kit/vitest.config.ts +7 -0
- package/pnpm-lock.template.yaml +177 -83
- package/pnpm-workspace.template.yaml +12 -4
- package/profiles/mall/config/agent.jsonc +33 -0
- package/profiles/mall/config/chat.json +21 -0
- package/profiles/mall/config/knowledge.json +5 -0
- package/profiles/mall/config/llm_provider.json +5 -0
- package/profiles/mall/config/plugins.json +3 -0
- package/profiles/mall.json +16 -0
- package/scripts/check-workspace.mjs +3 -2
- package/apps/client/assets/copy@2x.png +0 -0
- package/apps/client/assets/debug.png +0 -0
- package/apps/client/assets/dislike@2x.png +0 -0
- package/apps/client/assets/like@2x.png +0 -0
- package/apps/client/assets/speaker@2x.png +0 -0
- package/apps/client/src/cards/CardRenderer.tsx +0 -20
- package/apps/client/src/components/ChatHeader/index.less +0 -104
- package/apps/client/src/components/ErrorBoundary.tsx +0 -2
- package/apps/client/src/components/QuickCommands/index.less +0 -48
- package/apps/client/src/components/QuickCommands/index.tsx +0 -29
- package/apps/client/src/components/ToolCallBlock/index.less +0 -131
- package/apps/client/src/components/ToolCallBlock/index.tsx +0 -125
- package/apps/client/src/examples/tourism/TourismLanding.tsx +0 -96
- package/apps/client/src/examples/tourism/index.less +0 -192
- package/apps/client/src/pages/index/index.less +0 -43
- package/apps/client/src/store/chat-config.ts +0 -40
- package/apps/client/src/styles/global.less +0 -60
- package/apps/client/src/styles/tokens.less +0 -60
- package/apps/server/src/config/deployment.ts +0 -47
- package/packages/module-weather/package.json +0 -36
- package/packages/module-weather/skills/weather-guide/SKILL.md +0 -22
- package/packages/module-weather/skills/weather-guide/references/dressing-tips.md +0 -5
- package/packages/module-weather/src/client/cards/weather/index.less +0 -22
- package/packages/module-weather/src/client/cards/weather/index.tsx +0 -14
- package/packages/module-weather/src/client/index.ts +0 -19
- package/packages/module-weather/src/index.ts +0 -2
- package/packages/module-weather/src/server/cards/weather/meta.ts +0 -27
- package/packages/module-weather/src/server/index.ts +0 -45
- package/packages/module-weather/src/server/pre-tools.ts +0 -20
- package/packages/module-weather/src/server/tool.ts +0 -29
- package/packages/module-weather/tbox.module.json +0 -27
- package/packages/module-weather/tsconfig.json +0 -14
- /package/{apps/client → packages/agent-ui-kit}/assets/keyboard-circle.svg +0 -0
- /package/{apps/client → packages/agent-ui-kit}/assets/more-circle.svg +0 -0
- /package/{apps/client → packages/agent-ui-kit}/assets/new-chat.svg +0 -0
- /package/{apps/client → packages/agent-ui-kit}/assets/quick-command-star.svg +0 -0
- /package/{apps/client → packages/agent-ui-kit}/assets/send-gradient.svg +0 -0
- /package/{apps/client → packages/agent-ui-kit}/assets/stop@2x.png +0 -0
- /package/{apps/client → packages/agent-ui-kit}/assets/voice-circle.svg +0 -0
- /package/{apps/client → packages/agent-ui-kit}/src/components/ChatInput/voice/VoicePressField.tsx +0 -0
- /package/{apps/client → packages/agent-ui-kit}/src/components/ChatInput/voice/VoiceRecordingOverlay.tsx +0 -0
- /package/{apps/client → packages/agent-ui-kit}/src/components/ChatInput/voice/machine.ts +0 -0
- /package/{apps/client → packages/agent-ui-kit}/src/components/ReasoningBlock/index.less +0 -0
- /package/{apps/client → packages/agent-ui-kit}/src/components/ReasoningBlock/index.tsx +0 -0
- /package/{apps/client → packages/agent-ui-kit}/src/components/ScrollToBottomButton/index.tsx +0 -0
- /package/{apps/client/src/types/chat.ts → packages/agent-ui-kit/src/types.ts} +0 -0
- /package/{apps/client → packages/agent-ui-kit}/src/utils/copyText.ts +0 -0
- /package/{apps/client → packages/agent-ui-kit}/src/utils/imagePreview.ts +0 -0
- /package/{apps/client → packages/agent-ui-kit}/src/utils/session-error.ts +0 -0
- /package/{apps/client → packages/agent-ui-kit}/src/utils/toast.ts +0 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from 'react';
|
|
2
|
-
import { useMediaUpload } from '
|
|
2
|
+
import { useMediaUpload } from '../../hooks/useMediaUpload';
|
|
3
3
|
import { useASRAdapter, isInAlipayShell } from '@tbox.cn/app-sdk/client';
|
|
4
|
-
import { useChatConfigStore } from '
|
|
4
|
+
import { useChatConfigStore } from '../../chat-config';
|
|
5
5
|
|
|
6
|
-
import type { ChatInputMessage, InputMethod } from '
|
|
7
|
-
import { MediaList } from '
|
|
6
|
+
import type { ChatInputMessage, InputMethod } from '../../types';
|
|
7
|
+
import { MediaList } from '../../components/MediaList';
|
|
8
8
|
import { useVoicePress } from './voice/useVoicePress';
|
|
9
9
|
import { VoicePressField } from './voice/VoicePressField';
|
|
10
10
|
import { VoiceRecordingOverlay } from './voice/VoiceRecordingOverlay';
|
|
@@ -48,6 +48,7 @@ export function ChatInput({
|
|
|
48
48
|
onSend,
|
|
49
49
|
onCancel,
|
|
50
50
|
onNewChat,
|
|
51
|
+
onVoiceActivityChange,
|
|
51
52
|
disclaimer = '以上内容由AI生成,请谨慎甄别。',
|
|
52
53
|
}: {
|
|
53
54
|
generating?: boolean;
|
|
@@ -55,6 +56,7 @@ export function ChatInput({
|
|
|
55
56
|
onSend: (msg: ChatInputMessage) => void;
|
|
56
57
|
onCancel?: () => void;
|
|
57
58
|
onNewChat?: () => void;
|
|
59
|
+
onVoiceActivityChange?: (active: boolean) => void;
|
|
58
60
|
disclaimer?: string;
|
|
59
61
|
}) {
|
|
60
62
|
const [text, setText] = useState('');
|
|
@@ -85,6 +87,13 @@ export function ChatInput({
|
|
|
85
87
|
asr,
|
|
86
88
|
onResult: (t) => onSend({ text: t, mediaItems: [], inputMethod: 'voice' }),
|
|
87
89
|
});
|
|
90
|
+
const voiceActive = voice.interactionState !== 'idle';
|
|
91
|
+
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
onVoiceActivityChange?.(voiceActive);
|
|
94
|
+
}, [onVoiceActivityChange, voiceActive]);
|
|
95
|
+
|
|
96
|
+
useEffect(() => () => onVoiceActivityChange?.(false), [onVoiceActivityChange]);
|
|
88
97
|
|
|
89
98
|
const isWebPc = !inShell && typeof window !== 'undefined' && window.innerWidth > 450;
|
|
90
99
|
|
package/{apps/client → packages/agent-ui-kit}/src/components/ChatInput/voice/useVoicePress.ts
RENAMED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
2
|
import type { IASRAdapter } from '@tbox.cn/app-sdk/client';
|
|
3
3
|
import { useRefreshSession } from '@tbox.cn/app-sdk/client';
|
|
4
|
-
import { isSessionExpiredError } from '
|
|
5
|
-
import { toast } from '
|
|
4
|
+
import { isSessionExpiredError } from '../../../utils/session-error';
|
|
5
|
+
import { toast } from '../../../utils/toast';
|
|
6
6
|
import {
|
|
7
7
|
getMoveInteractionState,
|
|
8
8
|
getReleaseAction,
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/* 顶部历史入口:参考稿的浅灰胶囊,居中跟随内容滚动;历史拉出后由页面撤下。 */
|
|
2
|
+
.history-entry {
|
|
3
|
+
display: flex;
|
|
4
|
+
justify-content: center;
|
|
5
|
+
/* 自身不留边距;与欢迎区的 4PX 间距由页面 index.less 的 `.history-entry ~ .mall-home` 统一编排。 */
|
|
6
|
+
padding: 0;
|
|
7
|
+
|
|
8
|
+
&__button {
|
|
9
|
+
appearance: none;
|
|
10
|
+
padding: 3PX 12PX;
|
|
11
|
+
border: none;
|
|
12
|
+
border-radius: 25PX;
|
|
13
|
+
background: rgba(15, 64, 113, 0.06);
|
|
14
|
+
color: var(--tbox-text-muted);
|
|
15
|
+
font-size: 11.5PX;
|
|
16
|
+
font-weight: 400;
|
|
17
|
+
line-height: 16PX;
|
|
18
|
+
transition: background var(--tbox-transition-fast);
|
|
19
|
+
|
|
20
|
+
&:active {
|
|
21
|
+
background: rgba(15, 64, 113, 0.1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/* 加载态小转子:与文案同处胶囊内,替代原独立指示器 */
|
|
25
|
+
display: inline-flex;
|
|
26
|
+
align-items: center;
|
|
27
|
+
gap: 5PX;
|
|
28
|
+
|
|
29
|
+
&:disabled {
|
|
30
|
+
/* 未就绪只是短暂不可点,保持视觉常态;加载态由文案表达。 */
|
|
31
|
+
cursor: default;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
&__spinner {
|
|
36
|
+
width: 10PX;
|
|
37
|
+
height: 10PX;
|
|
38
|
+
border: 1.5PX solid rgba(15, 64, 113, 0.18);
|
|
39
|
+
border-top-color: var(--tbox-primary);
|
|
40
|
+
border-radius: 50%;
|
|
41
|
+
animation: history-entry-spin 0.6s linear infinite;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@keyframes history-entry-spin {
|
|
46
|
+
to {
|
|
47
|
+
transform: rotate(360deg);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// 顶部历史入口胶囊:同时承载点击入口和轻量下拉状态反馈。
|
|
2
|
+
// 常驻渲染是刻意的:运行时未就绪时只禁用,不隐藏——否则就绪后插入 DOM 会把下方欢迎区顶开。
|
|
3
|
+
import type { PullStatus } from '../../utils/pull-down-machine';
|
|
4
|
+
import './index.less';
|
|
5
|
+
|
|
6
|
+
function labelOf(status: PullStatus, loading: boolean): string {
|
|
7
|
+
if (loading || status === 'loading') return '加载中...';
|
|
8
|
+
if (status === 'pulling') return '下拉查看历史';
|
|
9
|
+
if (status === 'ready') return '松手查看历史';
|
|
10
|
+
if (status === 'noMore') return '没有更多历史记录';
|
|
11
|
+
return '历史记录';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default function HistoryEntry({
|
|
15
|
+
visible,
|
|
16
|
+
loading,
|
|
17
|
+
disabled,
|
|
18
|
+
pullStatus,
|
|
19
|
+
onLoad,
|
|
20
|
+
}: {
|
|
21
|
+
visible: boolean;
|
|
22
|
+
loading: boolean;
|
|
23
|
+
disabled: boolean;
|
|
24
|
+
pullStatus: PullStatus;
|
|
25
|
+
onLoad: () => void;
|
|
26
|
+
}) {
|
|
27
|
+
if (!visible) return null;
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div className="history-entry">
|
|
31
|
+
<button
|
|
32
|
+
type="button"
|
|
33
|
+
className="history-entry__button"
|
|
34
|
+
disabled={disabled}
|
|
35
|
+
onClick={onLoad}
|
|
36
|
+
>
|
|
37
|
+
{(loading || pullStatus === 'loading') && <span className="history-entry__spinner" />}
|
|
38
|
+
{labelOf(pullStatus, loading)}
|
|
39
|
+
</button>
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
display: flex;
|
|
59
59
|
align-items: center;
|
|
60
60
|
justify-content: center;
|
|
61
|
-
transition: background
|
|
61
|
+
transition: background var(--tbox-motion-duration-base) ease;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
&__progress-inner {
|
|
@@ -157,7 +157,7 @@
|
|
|
157
157
|
white-space: nowrap;
|
|
158
158
|
|
|
159
159
|
&--failed {
|
|
160
|
-
color:
|
|
160
|
+
color: var(--tbox-error);
|
|
161
161
|
}
|
|
162
162
|
}
|
|
163
163
|
|
|
@@ -195,7 +195,7 @@
|
|
|
195
195
|
z-index: 10;
|
|
196
196
|
opacity: 0;
|
|
197
197
|
transform: translateY(8px);
|
|
198
|
-
transition: opacity
|
|
198
|
+
transition: opacity var(--tbox-motion-duration-fast) ease-out, transform var(--tbox-motion-duration-fast) ease-out;
|
|
199
199
|
pointer-events: none;
|
|
200
200
|
|
|
201
201
|
&--visible {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { useCallback, useEffect, useState } from 'react';
|
|
2
2
|
import { X, Warning, Prohibit, Plus, VideoCamera } from '@phosphor-icons/react';
|
|
3
|
-
import { previewImages } from '
|
|
4
|
-
import { toast } from '
|
|
5
|
-
import type { MediaItem } from '
|
|
3
|
+
import { previewImages } from '../../utils/imagePreview';
|
|
4
|
+
import { toast } from '../../utils/toast';
|
|
5
|
+
import type { MediaItem } from '../../types';
|
|
6
6
|
import './index.less';
|
|
7
7
|
|
|
8
8
|
/** 媒体选项图标(远程素材,对齐原模板)。 */
|
|
@@ -15,12 +15,12 @@ const POPOVER_ICONS: Record<string, { icon: string; label: string; bg: string }>
|
|
|
15
15
|
camera: {
|
|
16
16
|
icon: 'https://mdn.alipayobjects.com/huamei_nzq4hr/afts/img/A*xdKLSabxqmQAAAAAOnAAAAgAel-hAQ/original',
|
|
17
17
|
label: '拍摄',
|
|
18
|
-
bg: '#
|
|
18
|
+
bg: '#096bfa26',
|
|
19
19
|
},
|
|
20
20
|
video: {
|
|
21
|
-
icon: 'https://mdn.alipayobjects.com/huamei_nzq4hr/afts/img/A*
|
|
21
|
+
icon: 'https://mdn.alipayobjects.com/huamei_nzq4hr/afts/img/A*DfDhSYAMzoQAAAAAHiAAAAgAel-hAQ/original',
|
|
22
22
|
label: '视频',
|
|
23
|
-
bg: '#
|
|
23
|
+
bg: '#096bfa26',
|
|
24
24
|
},
|
|
25
25
|
};
|
|
26
26
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
display: flex;
|
|
3
3
|
align-items: flex-start;
|
|
4
4
|
margin-bottom: 20px;
|
|
5
|
-
padding: 0
|
|
5
|
+
padding: 0 var(--tbox-page-gutter);
|
|
6
6
|
|
|
7
7
|
&--user {
|
|
8
8
|
flex-direction: row-reverse;
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
|
|
27
27
|
&--user {
|
|
28
28
|
justify-content: flex-end;
|
|
29
|
-
margin-right:
|
|
29
|
+
margin-right: 0;
|
|
30
30
|
}
|
|
31
31
|
&--bot {
|
|
32
32
|
justify-content: flex-start;
|
|
@@ -34,12 +34,12 @@
|
|
|
34
34
|
|
|
35
35
|
&__role {
|
|
36
36
|
font-size: 19px;
|
|
37
|
-
color:
|
|
37
|
+
color: var(--tbox-text-secondary);
|
|
38
38
|
margin-right: 8px;
|
|
39
39
|
}
|
|
40
40
|
&__time {
|
|
41
41
|
font-size: 18px;
|
|
42
|
-
color:
|
|
42
|
+
color: var(--tbox-text-muted);
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
|
|
@@ -49,19 +49,24 @@
|
|
|
49
49
|
margin-bottom: 12px;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
/* 多卡纵向铺开 wrapper(决策出卡 >1 张时 SDK CardCarousel 外层;单卡直出无此层) */
|
|
53
|
+
.msg-cards {
|
|
54
|
+
margin-top: 8px;
|
|
55
|
+
}
|
|
56
|
+
|
|
52
57
|
.msg-bubble {
|
|
53
58
|
padding: 18px 22px;
|
|
54
|
-
border-radius: 20px;
|
|
55
59
|
line-height: 1.6;
|
|
60
|
+
/* mall-vis 气泡影(demo .msg-bubble 基座影,用户气泡沿用;bot 气泡自持双层影) */
|
|
56
61
|
box-shadow: 0 4px 16px rgba(50, 65, 92, 0.08);
|
|
57
62
|
|
|
58
63
|
&--user {
|
|
59
|
-
background
|
|
60
|
-
border-radius: 48px 48px
|
|
64
|
+
background: rgba(255, 255, 255, 0.9);
|
|
65
|
+
border-radius: 48px 8px 48px 48px;
|
|
61
66
|
width: fit-content;
|
|
62
67
|
max-width: 100%;
|
|
63
68
|
margin-left: auto;
|
|
64
|
-
margin-right:
|
|
69
|
+
margin-right: 0;
|
|
65
70
|
margin-bottom: 8px;
|
|
66
71
|
display: flex;
|
|
67
72
|
align-items: center;
|
|
@@ -73,10 +78,10 @@
|
|
|
73
78
|
font-size: 30px;
|
|
74
79
|
line-height: 1.6;
|
|
75
80
|
padding: 22px;
|
|
76
|
-
color:
|
|
77
|
-
background
|
|
81
|
+
color: var(--tbox-text-primary);
|
|
82
|
+
background: rgba(255, 255, 255, 0.4);
|
|
78
83
|
border: 1px solid #edf0f6;
|
|
79
|
-
border-radius:
|
|
84
|
+
border-radius: 8px 48px 48px;
|
|
80
85
|
box-shadow:
|
|
81
86
|
0 3px 8px 5px rgba(1, 112, 254, 0.0392),
|
|
82
87
|
0 2px 4px rgba(72, 78, 86, 0.0706);
|
|
@@ -104,7 +109,7 @@
|
|
|
104
109
|
}
|
|
105
110
|
|
|
106
111
|
&--user &__text {
|
|
107
|
-
color:
|
|
112
|
+
color: var(--tbox-text-primary);
|
|
108
113
|
font-weight: 500;
|
|
109
114
|
overflow-wrap: break-word;
|
|
110
115
|
}
|
|
@@ -180,7 +185,7 @@
|
|
|
180
185
|
width: 10px;
|
|
181
186
|
height: 10px;
|
|
182
187
|
border-radius: 50%;
|
|
183
|
-
background: #
|
|
188
|
+
background: #aab3c2; /* mall-vis thinking 点灰(非主色) */
|
|
184
189
|
animation: dot-fade 1.4s ease-in-out infinite;
|
|
185
190
|
|
|
186
191
|
&--2 {
|
|
@@ -208,7 +213,7 @@
|
|
|
208
213
|
display: inline-block;
|
|
209
214
|
width: 4px;
|
|
210
215
|
height: 24px;
|
|
211
|
-
background: #5874ff;
|
|
216
|
+
background: #5874ff; /* mall-vis 流式光标蓝 */
|
|
212
217
|
margin-left: 4px;
|
|
213
218
|
vertical-align: middle;
|
|
214
219
|
animation: blink 1s step-end infinite;
|
|
@@ -227,7 +232,7 @@
|
|
|
227
232
|
/* Markdown */
|
|
228
233
|
.msg-markdown {
|
|
229
234
|
font-size: 25px;
|
|
230
|
-
color:
|
|
235
|
+
color: var(--tbox-text-primary);
|
|
231
236
|
overflow-wrap: anywhere;
|
|
232
237
|
|
|
233
238
|
h1,
|
|
@@ -263,7 +268,7 @@
|
|
|
263
268
|
font-style: italic;
|
|
264
269
|
}
|
|
265
270
|
a {
|
|
266
|
-
color:
|
|
271
|
+
color: var(--tbox-primary);
|
|
267
272
|
text-decoration: underline;
|
|
268
273
|
}
|
|
269
274
|
ul,
|
|
@@ -275,10 +280,10 @@
|
|
|
275
280
|
margin: 4px 0;
|
|
276
281
|
}
|
|
277
282
|
blockquote {
|
|
278
|
-
border-left: 4px solid #667eea;
|
|
283
|
+
border-left: 4px solid #667eea; /* mall-vis 引用竖条(ReasoningBlock 折叠族同源) */
|
|
279
284
|
padding-left: 16px;
|
|
280
285
|
margin: 12px 0;
|
|
281
|
-
color:
|
|
286
|
+
color: var(--tbox-text-secondary);
|
|
282
287
|
}
|
|
283
288
|
code {
|
|
284
289
|
background: rgba(0, 0, 0, 0.06);
|
|
@@ -308,13 +313,13 @@
|
|
|
308
313
|
}
|
|
309
314
|
th,
|
|
310
315
|
td {
|
|
311
|
-
border: 1px solid
|
|
316
|
+
border: 1px solid var(--tbox-border);
|
|
312
317
|
padding: 8px 12px;
|
|
313
318
|
text-align: left;
|
|
314
319
|
font-size: 26px;
|
|
315
320
|
}
|
|
316
321
|
th {
|
|
317
|
-
background:
|
|
322
|
+
background: var(--tbox-bg-subtle);
|
|
318
323
|
font-weight: 600;
|
|
319
324
|
}
|
|
320
325
|
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import { act } from 'react';
|
|
3
|
+
import { createRoot } from 'react-dom/client';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import type { TboxCardPayload } from '@tbox.cn/app-contracts';
|
|
6
|
+
|
|
7
|
+
const mocks = vi.hoisted(() => ({
|
|
8
|
+
state: {
|
|
9
|
+
cardsByMessageId: {} as Record<string, TboxCardPayload[]>,
|
|
10
|
+
conversationId: null,
|
|
11
|
+
},
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
vi.mock('../../store-context', () => ({
|
|
15
|
+
useChatStore: (selector: (state: typeof mocks.state) => unknown) => selector(mocks.state),
|
|
16
|
+
// W2-1:内联卡过滤经 context 注入(域语义保持原 witness:会员门禁卡系剥离,
|
|
17
|
+
// 原直连 '@/components/MemberGateSheet/model' 的 MEMBER_GATE_CARD_TYPES 语义)
|
|
18
|
+
useInlineCardFilter: () =>
|
|
19
|
+
(cards: Array<{ cardType: string }>) =>
|
|
20
|
+
cards.filter((c) => !['member-login-cta', 'member-enrollment-cta'].includes(c.cardType)),
|
|
21
|
+
}));
|
|
22
|
+
vi.mock('../CardRenderer', () => ({
|
|
23
|
+
CardRenderer: ({ card }: { card: TboxCardPayload }) => <div data-card-id={card.id} />,
|
|
24
|
+
}));
|
|
25
|
+
vi.mock('../../components/ReasoningBlock', () => ({ default: () => null }));
|
|
26
|
+
vi.mock('../../components/ToolCallBlock', () => ({ default: () => null }));
|
|
27
|
+
vi.mock('../../components/ActionBar', () => ({
|
|
28
|
+
ActionBar: () => null,
|
|
29
|
+
CopyButton: () => null,
|
|
30
|
+
getMessageCopyText: () => '',
|
|
31
|
+
}));
|
|
32
|
+
vi.mock('../../components/ActionBar/DebugButton', () => ({ DebugButton: () => null }));
|
|
33
|
+
|
|
34
|
+
import { MessageBubble } from './index';
|
|
35
|
+
|
|
36
|
+
function card(id: string, cardType: string): TboxCardPayload {
|
|
37
|
+
return { id, cardType, data: {}, status: 'ready', meta: { createdAt: 1 } };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const emptyAssistantMessage = {
|
|
41
|
+
id: 'assistant-1',
|
|
42
|
+
role: 'assistant' as const,
|
|
43
|
+
content: '',
|
|
44
|
+
status: 'done' as const,
|
|
45
|
+
createdAt: 1,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
describe('MessageBubble member gate card partition', () => {
|
|
49
|
+
let host: HTMLDivElement;
|
|
50
|
+
let root: ReturnType<typeof createRoot>;
|
|
51
|
+
|
|
52
|
+
beforeEach(() => {
|
|
53
|
+
host = document.createElement('div');
|
|
54
|
+
document.body.appendChild(host);
|
|
55
|
+
root = createRoot(host);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
afterEach(() => {
|
|
59
|
+
act(() => root.unmount());
|
|
60
|
+
host.remove();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('does not leave an empty message row for a login-only card', () => {
|
|
64
|
+
mocks.state.cardsByMessageId = {
|
|
65
|
+
'assistant-1': [card('login-1', 'member-login-cta')],
|
|
66
|
+
};
|
|
67
|
+
act(() => root.render(<MessageBubble message={emptyAssistantMessage} />));
|
|
68
|
+
expect(host.innerHTML).toBe('');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('keeps ordinary cards while removing the login card from a mixed carousel', () => {
|
|
72
|
+
mocks.state.cardsByMessageId = {
|
|
73
|
+
'assistant-1': [
|
|
74
|
+
card('login-1', 'member-login-cta'),
|
|
75
|
+
card('coupon-1', 'coupon-detail'),
|
|
76
|
+
],
|
|
77
|
+
};
|
|
78
|
+
act(() => root.render(<MessageBubble message={emptyAssistantMessage} />));
|
|
79
|
+
expect(host.querySelector('[data-card-id="login-1"]')).toBeNull();
|
|
80
|
+
expect(host.querySelector('[data-card-id="coupon-1"]')).not.toBeNull();
|
|
81
|
+
expect(host.querySelector('[aria-label^="第 "]')).toBeNull();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('strips enrollment-cta from inline carousel (module button is a bypassing openLink jump)', () => {
|
|
85
|
+
mocks.state.cardsByMessageId = {
|
|
86
|
+
'assistant-1': [
|
|
87
|
+
card('enroll-1', 'member-enrollment-cta'),
|
|
88
|
+
card('member-card-1', 'member-card-summary'),
|
|
89
|
+
],
|
|
90
|
+
};
|
|
91
|
+
act(() => root.render(<MessageBubble message={emptyAssistantMessage} />));
|
|
92
|
+
// 入会卡内联渲染会旁路 gate 闭环(openLink 裸外跳)——必须剥离,由 MemberGateSheet 投影
|
|
93
|
+
expect(host.querySelector('[data-card-id="enroll-1"]')).toBeNull();
|
|
94
|
+
expect(host.querySelector('[data-card-id="member-card-1"]')).not.toBeNull();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('多卡决策出卡 → 纵向按序渲染,无轮播圆点', () => {
|
|
98
|
+
mocks.state.cardsByMessageId = {
|
|
99
|
+
'assistant-1': [
|
|
100
|
+
card('coupon-1', 'coupon-detail'),
|
|
101
|
+
card('member-1', 'member-card-summary'),
|
|
102
|
+
],
|
|
103
|
+
};
|
|
104
|
+
act(() => root.render(<MessageBubble message={emptyAssistantMessage} />));
|
|
105
|
+
const all = host.querySelectorAll('[data-card-id]');
|
|
106
|
+
expect(all.length).toBe(2);
|
|
107
|
+
expect(all[0].getAttribute('data-card-id')).toBe('coupon-1');
|
|
108
|
+
expect(all[1].getAttribute('data-card-id')).toBe('member-1');
|
|
109
|
+
// 纵向铺开:不再有轮播圆点指示器
|
|
110
|
+
expect(host.querySelector('[aria-label^="第 "]')).toBeNull();
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -2,17 +2,17 @@ import { useMemo } from 'react';
|
|
|
2
2
|
import ReactMarkdown from 'react-markdown';
|
|
3
3
|
import remarkGfm from 'remark-gfm';
|
|
4
4
|
import { Copy } from '@phosphor-icons/react';
|
|
5
|
-
import { previewImages } from '
|
|
6
|
-
import type { ChatMessage } from '
|
|
7
|
-
import ReasoningBlock from '
|
|
8
|
-
import ToolCallBlock from '
|
|
9
|
-
import { ActionBar, CopyButton, getMessageCopyText } from '
|
|
10
|
-
import { DebugButton } from '
|
|
11
|
-
import { CardRenderer } from '
|
|
5
|
+
import { previewImages } from '../../utils/imagePreview';
|
|
6
|
+
import type { ChatMessage } from '../../types';
|
|
7
|
+
import ReasoningBlock from '../../components/ReasoningBlock';
|
|
8
|
+
import ToolCallBlock from '../../components/ToolCallBlock';
|
|
9
|
+
import { ActionBar, CopyButton, getMessageCopyText } from '../../components/ActionBar';
|
|
10
|
+
import { DebugButton } from '../../components/ActionBar/DebugButton';
|
|
11
|
+
import { CardRenderer } from '../CardRenderer';
|
|
12
12
|
import { CardCarousel } from '@tbox.cn/app-sdk/client';
|
|
13
|
-
import { useChatStore } from '
|
|
14
|
-
import { copyText } from '
|
|
15
|
-
import { toast } from '
|
|
13
|
+
import { useChatStore, useInlineCardFilter } from '../../store-context';
|
|
14
|
+
import { copyText } from '../../utils/copyText';
|
|
15
|
+
import { toast } from '../../utils/toast';
|
|
16
16
|
import './index.less';
|
|
17
17
|
|
|
18
18
|
const TBOX_FILE_URL = 'https://mdn.alipayobjects.com/tcmarket/afts/file/';
|
|
@@ -21,6 +21,8 @@ const TBOX_FILE_URL = 'https://mdn.alipayobjects.com/tcmarket/afts/file/';
|
|
|
21
21
|
export function MessageBubble({ message }: { message: ChatMessage }) {
|
|
22
22
|
const cardsByMessageId = useChatStore((s) => s.cardsByMessageId);
|
|
23
23
|
const conversationId = useChatStore((s) => s.conversationId);
|
|
24
|
+
// 018 tool-debug:服务端旗标单源(HELLO_ACK;无 URL 逃逸——数据面信任锚点在服务端)
|
|
25
|
+
const toolDebug = useChatStore((s) => s.helloAck?.toolDebug === true);
|
|
24
26
|
const isUser = message.role === 'user';
|
|
25
27
|
const roleName = isUser ? '你' : 'AI 助手';
|
|
26
28
|
const timeLabel = new Date(message.createdAt).toLocaleTimeString('zh-CN', {
|
|
@@ -32,8 +34,6 @@ export function MessageBubble({ message }: { message: ChatMessage }) {
|
|
|
32
34
|
const isThinking = message.status === 'thinking';
|
|
33
35
|
const isStreaming = message.status === 'streaming';
|
|
34
36
|
const hasReasoning = !!message.reasoning;
|
|
35
|
-
// 018 tool-debug:服务端旗标单源(HELLO_ACK;无 URL 逃逸——数据面信任锚点在服务端)
|
|
36
|
-
const toolDebug = useChatStore((s) => s.helloAck?.toolDebug === true);
|
|
37
37
|
// 018:formSubmit 伪工具 prod 过滤不变;dev 放行(防御性——若事件到达则可见)
|
|
38
38
|
const visibleToolCalls = useMemo(
|
|
39
39
|
() => (message.toolCalls || []).filter((tc) => toolDebug || tc.name !== '__a2uiFormSubmit'),
|
|
@@ -41,9 +41,10 @@ export function MessageBubble({ message }: { message: ChatMessage }) {
|
|
|
41
41
|
);
|
|
42
42
|
const hasToolCalls = visibleToolCalls.length > 0;
|
|
43
43
|
|
|
44
|
+
const filterInlineCards = useInlineCardFilter();
|
|
44
45
|
const messageCards = useMemo(
|
|
45
|
-
() => (isUser ? [] : cardsByMessageId[message.id] || []),
|
|
46
|
-
[cardsByMessageId, message.id, isUser],
|
|
46
|
+
() => (isUser ? [] : filterInlineCards(cardsByMessageId[message.id] || [])),
|
|
47
|
+
[cardsByMessageId, message.id, isUser, filterInlineCards],
|
|
47
48
|
);
|
|
48
49
|
const imageItems = isUser
|
|
49
50
|
? message.mediaItems?.filter((i) => i.type === 'image' && (i.src || i.fileId))
|
|
@@ -60,6 +61,11 @@ export function MessageBubble({ message }: { message: ChatMessage }) {
|
|
|
60
61
|
|
|
61
62
|
const imageUrls = (imageItems || []).map((it) => it.src || `${TBOX_FILE_URL}${it.fileId}`);
|
|
62
63
|
|
|
64
|
+
// 登录入口由页面级半屏浮层承载;仅含登录卡的空 assistant 行不进入聊天流。
|
|
65
|
+
if (!isUser && !isPureThinking && !hasReasoning && !hasToolCalls && !showBubble && messageCards.length === 0) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
63
69
|
return (
|
|
64
70
|
<div className={`msg-row ${isUser ? 'msg-row--user' : 'msg-row--bot'}`}>
|
|
65
71
|
<div className="msg-main">
|
|
@@ -117,6 +123,7 @@ export function MessageBubble({ message }: { message: ChatMessage }) {
|
|
|
117
123
|
|
|
118
124
|
{/* 卡片在对话气泡之后显示(决策出卡多张/意图卡/历史卡统一经 cardsByMessageId 挂载,多卡纵向铺开) */}
|
|
119
125
|
<CardCarousel
|
|
126
|
+
className="msg-cards"
|
|
120
127
|
cards={messageCards}
|
|
121
128
|
renderItem={(card) => <CardRenderer key={card.id} card={card} />}
|
|
122
129
|
/>
|
|
@@ -138,7 +145,7 @@ export function MessageBubble({ message }: { message: ChatMessage }) {
|
|
|
138
145
|
{isUser && !!message.content && (
|
|
139
146
|
<div className="msg-toolbar msg-toolbar--user">
|
|
140
147
|
<div className="msg-toolbar__action" onClick={copy}>
|
|
141
|
-
<Copy size={12} color="#
|
|
148
|
+
<Copy size={12} color="#535b6c" />
|
|
142
149
|
</div>
|
|
143
150
|
</div>
|
|
144
151
|
)}
|
|
@@ -6,13 +6,13 @@
|
|
|
6
6
|
gap: 12px;
|
|
7
7
|
|
|
8
8
|
&--animated {
|
|
9
|
-
transition: margin-top
|
|
9
|
+
transition: margin-top var(--tbox-motion-duration-base) ease-out;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
&__spinner {
|
|
13
13
|
width: 32px;
|
|
14
14
|
height: 32px;
|
|
15
|
-
border: 3px solid
|
|
15
|
+
border: 3px solid var(--tbox-border-strong);
|
|
16
16
|
border-top-color: var(--tbox-primary);
|
|
17
17
|
border-radius: 50%;
|
|
18
18
|
animation: pull-spin 0.6s linear infinite;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { PullStatus } from '
|
|
2
|
-
import { PULL_DOWN_THRESHOLD } from '
|
|
1
|
+
import type { PullStatus } from '../../utils/pull-down-machine';
|
|
2
|
+
import { PULL_DOWN_THRESHOLD } from '../../utils/pull-down-machine';
|
|
3
3
|
import './index.less';
|
|
4
4
|
|
|
5
5
|
/** 下拉加载历史的顶部指示器。 */
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
.quick-commands {
|
|
2
|
+
position: relative;
|
|
3
|
+
z-index: 1;
|
|
4
|
+
width: 100%;
|
|
5
|
+
max-width: var(--tbox-content-max-width);
|
|
6
|
+
margin: 0 auto;
|
|
7
|
+
box-sizing: border-box;
|
|
8
|
+
display: flex;
|
|
9
|
+
gap: 8PX;
|
|
10
|
+
overflow-x: auto;
|
|
11
|
+
padding: 0 var(--tbox-page-gutter) 9PX;
|
|
12
|
+
-ms-overflow-style: none;
|
|
13
|
+
scrollbar-width: none;
|
|
14
|
+
|
|
15
|
+
&::-webkit-scrollbar {
|
|
16
|
+
display: none;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/* 语音交互时只隐藏胶囊,保留原高度和页面灰底,继续遮挡聊天内容。 */
|
|
20
|
+
&--hidden {
|
|
21
|
+
visibility: hidden;
|
|
22
|
+
pointer-events: none;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
&__item {
|
|
26
|
+
flex: 1 0 auto;
|
|
27
|
+
display: inline-flex;
|
|
28
|
+
align-items: center;
|
|
29
|
+
justify-content: center;
|
|
30
|
+
gap: 5PX;
|
|
31
|
+
height: 34PX;
|
|
32
|
+
padding: 0 13PX;
|
|
33
|
+
appearance: none;
|
|
34
|
+
/* mall-vis 快捷指令胶囊:白底 + 2px 描边 + 50px 胶囊圆角(数据仍服务端驱动) */
|
|
35
|
+
background: rgba(255, 255, 255, 0.92);
|
|
36
|
+
border: 1PX solid rgba(15, 64, 113, 0.14);
|
|
37
|
+
border-radius: 25PX;
|
|
38
|
+
box-shadow: 0 4PX 16PX rgba(38, 59, 92, 0.05);
|
|
39
|
+
transition: background var(--tbox-transition-fast), border-color var(--tbox-transition-fast);
|
|
40
|
+
|
|
41
|
+
&:active {
|
|
42
|
+
border-color: var(--tbox-primary);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
&__icon {
|
|
47
|
+
width: 16PX;
|
|
48
|
+
height: 16PX;
|
|
49
|
+
object-fit: contain;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
&__icon-svg {
|
|
53
|
+
color: var(--tbox-primary);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
&__label {
|
|
57
|
+
color: var(--tbox-text-primary);
|
|
58
|
+
font-size: 13PX;
|
|
59
|
+
font-weight: 500;
|
|
60
|
+
line-height: 18PX;
|
|
61
|
+
white-space: nowrap;
|
|
62
|
+
}
|
|
63
|
+
}
|