@tbox.cn/app-template-agent 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/.gitignore +13 -0
  2. package/.nvmrc +1 -0
  3. package/.tbox/app.json +4 -0
  4. package/AGENTS.md +101 -0
  5. package/ENV.md +77 -0
  6. package/LEGAL.md +7 -0
  7. package/LICENSE +21 -0
  8. package/README.md +62 -0
  9. package/apps/client/STYLE_GUIDE.md +81 -0
  10. package/apps/client/VISUAL_RESTORE.md +90 -0
  11. package/apps/client/assets/copy@2x.png +0 -0
  12. package/apps/client/assets/debug.png +0 -0
  13. package/apps/client/assets/dislike@2x.png +0 -0
  14. package/apps/client/assets/keyboard-circle.svg +12 -0
  15. package/apps/client/assets/like@2x.png +0 -0
  16. package/apps/client/assets/more-circle.svg +3 -0
  17. package/apps/client/assets/new-chat.svg +3 -0
  18. package/apps/client/assets/quick-command-star.svg +3 -0
  19. package/apps/client/assets/send-gradient.svg +14 -0
  20. package/apps/client/assets/speaker@2x.png +0 -0
  21. package/apps/client/assets/stop@2x.png +0 -0
  22. package/apps/client/assets/voice-circle.svg +3 -0
  23. package/apps/client/babel-plugins/code-inspect.cjs +110 -0
  24. package/apps/client/index.html +36 -0
  25. package/apps/client/package.json +34 -0
  26. package/apps/client/src/app.tsx +5 -0
  27. package/apps/client/src/business/App.tsx +11 -0
  28. package/apps/client/src/business/README.md +82 -0
  29. package/apps/client/src/cards/CardRenderer.tsx +10 -0
  30. package/apps/client/src/cards/registry.ts +22 -0
  31. package/apps/client/src/components/ActionBar/DebugButton.tsx +49 -0
  32. package/apps/client/src/components/ActionBar/index.less +120 -0
  33. package/apps/client/src/components/ActionBar/index.tsx +157 -0
  34. package/apps/client/src/components/ChatHeader/index.less +104 -0
  35. package/apps/client/src/components/ChatHeader/index.tsx +63 -0
  36. package/apps/client/src/components/ChatInput/index.less +332 -0
  37. package/apps/client/src/components/ChatInput/index.tsx +251 -0
  38. package/apps/client/src/components/ChatInput/voice/VoicePressField.tsx +34 -0
  39. package/apps/client/src/components/ChatInput/voice/VoiceRecordingOverlay.tsx +51 -0
  40. package/apps/client/src/components/ChatInput/voice/machine.ts +27 -0
  41. package/apps/client/src/components/ChatInput/voice/useVoicePress.ts +188 -0
  42. package/apps/client/src/components/ErrorBoundary.tsx +2 -0
  43. package/apps/client/src/components/MediaList/index.less +241 -0
  44. package/apps/client/src/components/MediaList/index.tsx +256 -0
  45. package/apps/client/src/components/MessageBubble/index.less +309 -0
  46. package/apps/client/src/components/MessageBubble/index.tsx +163 -0
  47. package/apps/client/src/components/PullDownIndicator/index.less +32 -0
  48. package/apps/client/src/components/PullDownIndicator/index.tsx +33 -0
  49. package/apps/client/src/components/QuickCommands/index.less +48 -0
  50. package/apps/client/src/components/QuickCommands/index.tsx +22 -0
  51. package/apps/client/src/components/ReasoningBlock/index.less +90 -0
  52. package/apps/client/src/components/ReasoningBlock/index.tsx +56 -0
  53. package/apps/client/src/components/ScrollToBottomButton/index.less +32 -0
  54. package/apps/client/src/components/ScrollToBottomButton/index.tsx +10 -0
  55. package/apps/client/src/components/ToolCallBlock/index.less +91 -0
  56. package/apps/client/src/components/ToolCallBlock/index.tsx +63 -0
  57. package/apps/client/src/examples/tourism/TourismLanding.tsx +96 -0
  58. package/apps/client/src/examples/tourism/index.less +192 -0
  59. package/apps/client/src/hooks/useMediaUpload.ts +237 -0
  60. package/apps/client/src/hooks/usePullDownHistory.ts +97 -0
  61. package/apps/client/src/hooks/useTTSAutoPlay.ts +80 -0
  62. package/apps/client/src/main.tsx +17 -0
  63. package/apps/client/src/modules.ts +30 -0
  64. package/apps/client/src/pages/index/index.less +43 -0
  65. package/apps/client/src/pages/index/index.tsx +145 -0
  66. package/apps/client/src/platform/AgentChatApp.tsx +39 -0
  67. package/apps/client/src/platform/agent-runtime.ts +15 -0
  68. package/apps/client/src/platform/state/createPersistedStore.ts +19 -0
  69. package/apps/client/src/shims/node-stub.ts +14 -0
  70. package/apps/client/src/store/chat-config.ts +29 -0
  71. package/apps/client/src/store/chat.ts +201 -0
  72. package/apps/client/src/styles/global.less +60 -0
  73. package/apps/client/src/styles/tokens.less +60 -0
  74. package/apps/client/src/types/chat.ts +29 -0
  75. package/apps/client/src/utils/copyText.ts +11 -0
  76. package/apps/client/src/utils/imagePreview.ts +10 -0
  77. package/apps/client/src/utils/session-error.ts +5 -0
  78. package/apps/client/src/utils/toast.ts +15 -0
  79. package/apps/client/src/vite-env.d.ts +3 -0
  80. package/apps/client/tsconfig.json +11 -0
  81. package/apps/client/vite.config.ts +40 -0
  82. package/apps/server/package.json +40 -0
  83. package/apps/server/src/cli/collect-registry.ts +26 -0
  84. package/apps/server/src/cli/plugin-doctor.ts +89 -0
  85. package/apps/server/src/cli/plugin-inspect.ts +49 -0
  86. package/apps/server/src/index.ts +230 -0
  87. package/apps/server/src/intent-routes.ts +23 -0
  88. package/apps/server/src/modules.ts +31 -0
  89. package/apps/server/src/tools/translate.ts +41 -0
  90. package/apps/server/tests/cards-registry.test.ts +55 -0
  91. package/apps/server/tests/tool-runtime.test.ts +49 -0
  92. package/apps/server/tsconfig.json +5 -0
  93. package/apps/server/tsup.config.ts +25 -0
  94. package/apps/server/vitest.config.ts +8 -0
  95. package/config/chat.json +8 -0
  96. package/config/llm_provider.json +3 -0
  97. package/config/plugins.json +3 -0
  98. package/eslint.config.mjs +41 -0
  99. package/package.json +51 -0
  100. package/packages/module-weather/package.json +35 -0
  101. package/packages/module-weather/src/client/cards/weather/index.less +22 -0
  102. package/packages/module-weather/src/client/cards/weather/index.tsx +14 -0
  103. package/packages/module-weather/src/client/index.ts +19 -0
  104. package/packages/module-weather/src/index.ts +2 -0
  105. package/packages/module-weather/src/server/cards/weather/meta.ts +28 -0
  106. package/packages/module-weather/src/server/index.ts +19 -0
  107. package/packages/module-weather/src/server/tool.ts +29 -0
  108. package/packages/module-weather/tbox.component.json +27 -0
  109. package/packages/module-weather/tsconfig.json +14 -0
  110. package/pnpm-workspace.template.yaml +60 -0
  111. package/scripts/build.sh +39 -0
  112. package/scripts/check-inspector.sh +91 -0
  113. package/scripts/check-workspace.mjs +164 -0
  114. package/scripts/dev.sh +15 -0
  115. package/scripts/prepare.sh +11 -0
  116. package/scripts/start.sh +11 -0
  117. package/scripts/stop.sh +26 -0
  118. package/scripts/verify-generated.mjs +142 -0
  119. package/tsconfig.base.json +20 -0
@@ -0,0 +1,309 @@
1
+ .msg-row {
2
+ display: flex;
3
+ align-items: flex-start;
4
+ margin-bottom: 20px;
5
+ padding: 0 24px;
6
+
7
+ &--user {
8
+ flex-direction: row-reverse;
9
+ }
10
+ &--bot {
11
+ flex-direction: row;
12
+ }
13
+ }
14
+
15
+ .msg-main {
16
+ min-width: 0;
17
+ }
18
+ .msg-row--bot .msg-main {
19
+ flex: 1;
20
+ }
21
+
22
+ .msg-meta {
23
+ display: flex;
24
+ align-items: center;
25
+ margin-bottom: 6px;
26
+
27
+ &--user {
28
+ justify-content: flex-end;
29
+ margin-right: 12px;
30
+ }
31
+ &--bot {
32
+ justify-content: flex-start;
33
+ }
34
+
35
+ &__role {
36
+ font-size: 19px;
37
+ color: #70788d;
38
+ margin-right: 8px;
39
+ }
40
+ &__time {
41
+ font-size: 18px;
42
+ color: #a0a7b8;
43
+ }
44
+ }
45
+
46
+ .msg-bubble {
47
+ padding: 18px 22px;
48
+ border-radius: 20px;
49
+ line-height: 1.6;
50
+ box-shadow: 0 4px 16px rgba(50, 65, 92, 0.08);
51
+
52
+ &--user {
53
+ background-color: rgba(255, 255, 255, 0.9);
54
+ border-radius: 48px 48px 8px;
55
+ width: fit-content;
56
+ max-width: 100%;
57
+ margin-left: auto;
58
+ margin-right: 12px;
59
+ margin-bottom: 8px;
60
+ display: flex;
61
+ align-items: center;
62
+ min-height: 48px;
63
+ }
64
+
65
+ &--bot {
66
+ position: relative;
67
+ font-size: 30px;
68
+ line-height: 1.6;
69
+ padding: 22px;
70
+ color: #1b1212;
71
+ background-color: rgba(255, 255, 255, 0.4);
72
+ border: 1px solid #edf0f6;
73
+ border-radius: 48px 8px 48px 48px;
74
+ box-shadow:
75
+ 0 3px 8px 5px rgba(1, 112, 254, 0.0392),
76
+ 0 2px 4px rgba(72, 78, 86, 0.0706);
77
+ }
78
+
79
+ &__text {
80
+ font-size: 25px;
81
+ }
82
+
83
+ &__error {
84
+ color: var(--tbox-error);
85
+ font-size: 25px;
86
+ line-height: 1.6;
87
+ white-space: pre-wrap;
88
+ overflow-wrap: anywhere;
89
+
90
+ &--with-content {
91
+ margin-top: 14px;
92
+ padding-top: 14px;
93
+ border-top: 1px solid var(--tbox-error);
94
+ }
95
+ }
96
+
97
+ &--user &__text {
98
+ color: #1f3b63;
99
+ font-weight: 500;
100
+ overflow-wrap: break-word;
101
+ }
102
+ }
103
+
104
+ /* 用户消息附带的图片缩略图 */
105
+ .msg-media {
106
+ display: flex;
107
+ flex-wrap: nowrap;
108
+ overflow-x: auto;
109
+ gap: 16px;
110
+ direction: rtl;
111
+ margin-right: 12px;
112
+ margin-top: 4px;
113
+ -webkit-overflow-scrolling: touch;
114
+
115
+ &::-webkit-scrollbar {
116
+ display: none;
117
+ }
118
+
119
+ &__thumb {
120
+ direction: ltr;
121
+ width: var(--tbox-media-item-size);
122
+ min-width: var(--tbox-media-item-size);
123
+ height: var(--tbox-media-item-size);
124
+ border-radius: var(--tbox-media-item-radius);
125
+ object-fit: cover;
126
+ flex-shrink: 0;
127
+ }
128
+ }
129
+
130
+ .msg-toolbar {
131
+ display: flex;
132
+ align-items: center;
133
+ margin-top: 8px;
134
+ opacity: 0.4;
135
+
136
+ &--user {
137
+ justify-content: flex-end;
138
+ padding-right: 12px;
139
+ }
140
+
141
+ &__action {
142
+ display: flex;
143
+ align-items: center;
144
+ justify-content: center;
145
+ width: 34px;
146
+ height: 28px;
147
+ }
148
+ }
149
+
150
+ .msg-toolbar-row {
151
+ display: flex;
152
+ align-items: center;
153
+ gap: 12px;
154
+ margin-top: 12px;
155
+ flex-wrap: wrap;
156
+ }
157
+
158
+ /* 3 个点 loading 指示器 */
159
+ .msg-thinking,
160
+ .msg-thinking-inline {
161
+ display: flex;
162
+ justify-content: flex-start;
163
+ align-items: center;
164
+ gap: 8px;
165
+ padding: 10px 0;
166
+ margin-left: 12px;
167
+
168
+ &__dot {
169
+ width: 10px;
170
+ height: 10px;
171
+ border-radius: 50%;
172
+ background: #8a9ab8;
173
+ animation: dot-fade 1.4s ease-in-out infinite;
174
+
175
+ &--2 {
176
+ animation-delay: 0.2s;
177
+ }
178
+ &--3 {
179
+ animation-delay: 0.4s;
180
+ }
181
+ }
182
+ }
183
+
184
+ @keyframes dot-fade {
185
+ 0%,
186
+ 80%,
187
+ 100% {
188
+ opacity: 0.3;
189
+ }
190
+ 40% {
191
+ opacity: 1;
192
+ }
193
+ }
194
+
195
+ /* 流式光标 */
196
+ .msg-cursor {
197
+ display: inline-block;
198
+ width: 4px;
199
+ height: 24px;
200
+ background: #5874ff;
201
+ margin-left: 4px;
202
+ vertical-align: middle;
203
+ animation: blink 1s step-end infinite;
204
+ }
205
+
206
+ @keyframes blink {
207
+ 0%,
208
+ 100% {
209
+ opacity: 1;
210
+ }
211
+ 50% {
212
+ opacity: 0;
213
+ }
214
+ }
215
+
216
+ /* Markdown */
217
+ .msg-markdown {
218
+ font-size: 25px;
219
+ color: #2a3142;
220
+ overflow-wrap: anywhere;
221
+
222
+ h1,
223
+ h2,
224
+ h3,
225
+ h4,
226
+ h5,
227
+ h6 {
228
+ margin: 16px 0 8px;
229
+ font-weight: 600;
230
+ line-height: 1.4;
231
+ }
232
+ h1 {
233
+ font-size: 36px;
234
+ }
235
+ h2 {
236
+ font-size: 32px;
237
+ }
238
+ h3 {
239
+ font-size: 30px;
240
+ }
241
+ h4 {
242
+ font-size: 28px;
243
+ }
244
+
245
+ p {
246
+ margin: 8px 0;
247
+ }
248
+ strong {
249
+ font-weight: 600;
250
+ }
251
+ em {
252
+ font-style: italic;
253
+ }
254
+ a {
255
+ color: #1677ff;
256
+ text-decoration: underline;
257
+ }
258
+ ul,
259
+ ol {
260
+ padding-left: 32px;
261
+ margin: 8px 0;
262
+ }
263
+ li {
264
+ margin: 4px 0;
265
+ }
266
+ blockquote {
267
+ border-left: 4px solid #667eea;
268
+ padding-left: 16px;
269
+ margin: 12px 0;
270
+ color: #666;
271
+ }
272
+ code {
273
+ background: rgba(0, 0, 0, 0.06);
274
+ padding: 2px 8px;
275
+ border-radius: 6px;
276
+ font-size: 26px;
277
+ font-family: Menlo, Monaco, Consolas, monospace;
278
+ }
279
+ pre {
280
+ background: #1e1e2e;
281
+ border-radius: 12px;
282
+ padding: 20px;
283
+ margin: 12px 0;
284
+ overflow-x: auto;
285
+
286
+ code {
287
+ background: none;
288
+ padding: 0;
289
+ color: #cdd6f4;
290
+ font-size: 24px;
291
+ }
292
+ }
293
+ table {
294
+ width: 100%;
295
+ border-collapse: collapse;
296
+ margin: 12px 0;
297
+ }
298
+ th,
299
+ td {
300
+ border: 1px solid #ddd;
301
+ padding: 8px 12px;
302
+ text-align: left;
303
+ font-size: 26px;
304
+ }
305
+ th {
306
+ background: #f0f0f5;
307
+ font-weight: 600;
308
+ }
309
+ }
@@ -0,0 +1,163 @@
1
+ import { useMemo } from 'react';
2
+ import ReactMarkdown from 'react-markdown';
3
+ import remarkGfm from 'remark-gfm';
4
+ import { Copy } from '@phosphor-icons/react';
5
+ import { previewImages } from '@/utils/imagePreview';
6
+ import type { ChatMessage } from '@/types/chat';
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 '@/cards/CardRenderer';
12
+ import { useChatStore } from '@/store/chat';
13
+ import { copyText } from '@/utils/copyText';
14
+ import { toast } from '@/utils/toast';
15
+ import './index.less';
16
+
17
+ const TBOX_FILE_URL = 'https://mdn.alipayobjects.com/tcmarket/afts/file/';
18
+
19
+ /** 消息气泡(对齐原模板):meta + 思考/工具/卡片 + 气泡 + 媒体 + 操作栏。 */
20
+ export function MessageBubble({ message }: { message: ChatMessage }) {
21
+ const cardsByMessageId = useChatStore((s) => s.cardsByMessageId);
22
+ const conversationId = useChatStore((s) => s.conversationId);
23
+ const isUser = message.role === 'user';
24
+ const roleName = isUser ? '你' : 'AI 助手';
25
+ const timeLabel = new Date(message.createdAt).toLocaleTimeString('zh-CN', {
26
+ hour: '2-digit',
27
+ minute: '2-digit',
28
+ hour12: false,
29
+ });
30
+
31
+ const isThinking = message.status === 'thinking';
32
+ const isStreaming = message.status === 'streaming';
33
+ const hasReasoning = !!message.reasoning;
34
+ const visibleToolCalls = useMemo(
35
+ () => (message.toolCalls || []).filter((tc) => tc.name !== '__a2uiFormSubmit'),
36
+ [message.toolCalls],
37
+ );
38
+ const hasToolCalls = visibleToolCalls.length > 0;
39
+
40
+ const messageCards = useMemo(
41
+ () => (isUser ? [] : cardsByMessageId[message.id] || []),
42
+ [cardsByMessageId, message.id, isUser],
43
+ );
44
+ const imageItems = isUser
45
+ ? message.mediaItems?.filter((i) => i.type === 'image' && (i.src || i.fileId))
46
+ : undefined;
47
+
48
+ const isPureThinking = isThinking && !hasReasoning && !message.content && !hasToolCalls;
49
+ const isReasoningActive = isThinking || (isStreaming && !message.content);
50
+ const showBubble = isUser || !!message.content || !!message.error || isStreaming;
51
+
52
+ const copy = async () => {
53
+ const copied = await copyText(message.content);
54
+ void toast(copied ? '复制成功' : '复制失败,请重试', copied ? 'success' : 'error');
55
+ };
56
+
57
+ const imageUrls = (imageItems || []).map((it) => it.src || `${TBOX_FILE_URL}${it.fileId}`);
58
+
59
+ return (
60
+ <div className={`msg-row ${isUser ? 'msg-row--user' : 'msg-row--bot'}`}>
61
+ <div className="msg-main">
62
+ <div className={`msg-meta ${isUser ? 'msg-meta--user' : 'msg-meta--bot'}`}>
63
+ <span className="msg-meta__role">{roleName}</span>
64
+ <span className="msg-meta__time">{timeLabel}</span>
65
+ </div>
66
+
67
+ {isPureThinking && (
68
+ <div className="msg-thinking">
69
+ <div className="msg-thinking__dot" />
70
+ <div className="msg-thinking__dot msg-thinking__dot--2" />
71
+ <div className="msg-thinking__dot msg-thinking__dot--3" />
72
+ </div>
73
+ )}
74
+
75
+ {hasReasoning && <ReasoningBlock reasoning={message.reasoning!} isActive={isReasoningActive} />}
76
+
77
+ {hasToolCalls && <ToolCallBlock toolCalls={visibleToolCalls} />}
78
+
79
+ {isThinking && hasToolCalls && !message.content && (
80
+ <div className="msg-thinking-inline">
81
+ <div className="msg-thinking-inline__dot" />
82
+ <div className="msg-thinking-inline__dot msg-thinking-inline__dot--2" />
83
+ <div className="msg-thinking-inline__dot msg-thinking-inline__dot--3" />
84
+ </div>
85
+ )}
86
+
87
+ {messageCards.map((card) => (
88
+ <CardRenderer key={card.id} card={card} />
89
+ ))}
90
+
91
+ {showBubble && (
92
+ <div className={`msg-bubble ${isUser ? 'msg-bubble--user' : 'msg-bubble--bot'}`}>
93
+ {isUser ? (
94
+ <span className="msg-bubble__text">{message.content}</span>
95
+ ) : (
96
+ <>
97
+ {message.content && (
98
+ <div className="msg-markdown">
99
+ <ReactMarkdown remarkPlugins={[remarkGfm]}>{message.content}</ReactMarkdown>
100
+ </div>
101
+ )}
102
+ {message.error && (
103
+ <div
104
+ className={`msg-bubble__error${message.content ? ' msg-bubble__error--with-content' : ''}`}
105
+ role="alert"
106
+ >
107
+ {message.error}
108
+ </div>
109
+ )}
110
+ {isStreaming && <span className="msg-cursor" />}
111
+ </>
112
+ )}
113
+ </div>
114
+ )}
115
+
116
+ {imageItems && imageItems.length > 0 && (
117
+ <div className="msg-media">
118
+ {imageItems.map((item, idx) => (
119
+ <img
120
+ key={item.id}
121
+ className="msg-media__thumb"
122
+ src={item.src || `${TBOX_FILE_URL}${item.fileId}`}
123
+ alt=""
124
+ onClick={() => previewImages(imageUrls, idx)}
125
+ />
126
+ ))}
127
+ </div>
128
+ )}
129
+
130
+ {isUser && !!message.content && (
131
+ <div className="msg-toolbar msg-toolbar--user">
132
+ <div className="msg-toolbar__action" onClick={copy}>
133
+ <Copy size={12} color="#5f7ec6" />
134
+ </div>
135
+ </div>
136
+ )}
137
+
138
+ {!isUser && message.status === 'done' && !!message.content && (
139
+ <div className="msg-toolbar-row">
140
+ <ActionBar message={message} />
141
+ </div>
142
+ )}
143
+
144
+ {/* 出错消息不提供朗读/反馈,但支持复制错误详情和拉 trace 排查。 */}
145
+ {!isUser && message.status === 'error' && (
146
+ <div className="msg-toolbar-row">
147
+ <div className="tbox-action-bar">
148
+ <CopyButton text={getMessageCopyText(message)} />
149
+ {message.requestId && (
150
+ <DebugButton
151
+ requestId={message.requestId}
152
+ conversationId={conversationId ?? undefined}
153
+ messageId={message.id}
154
+ messageCreatedAt={new Date(message.createdAt).getTime()}
155
+ />
156
+ )}
157
+ </div>
158
+ </div>
159
+ )}
160
+ </div>
161
+ </div>
162
+ );
163
+ }
@@ -0,0 +1,32 @@
1
+ .pull-indicator {
2
+ display: flex;
3
+ align-items: center;
4
+ justify-content: center;
5
+ height: 60px;
6
+ gap: 12px;
7
+
8
+ &--animated {
9
+ transition: margin-top 0.3s ease-out;
10
+ }
11
+
12
+ &__spinner {
13
+ width: 32px;
14
+ height: 32px;
15
+ border: 3px solid #d0d5dd;
16
+ border-top-color: #1677ff;
17
+ border-radius: 50%;
18
+ animation: pull-spin 0.6s linear infinite;
19
+ }
20
+
21
+ &__text {
22
+ font-size: 26px;
23
+ color: #8c8c8c;
24
+ line-height: 1;
25
+ }
26
+ }
27
+
28
+ @keyframes pull-spin {
29
+ to {
30
+ transform: rotate(360deg);
31
+ }
32
+ }
@@ -0,0 +1,33 @@
1
+ import type { PullStatus } from '@/hooks/usePullDownHistory';
2
+ import './index.less';
3
+
4
+ const INDICATOR_HEIGHT = 60;
5
+
6
+ /** 下拉加载历史的顶部指示器。 */
7
+ export default function PullDownIndicator({ status, distance }: { status: PullStatus; distance: number }) {
8
+ if (status === 'idle') return null;
9
+
10
+ const marginTop = status === 'loading' ? 0 : -INDICATOR_HEIGHT + Math.min(distance, INDICATOR_HEIGHT);
11
+ const showTransition = status === 'loading' || status === 'noMore';
12
+
13
+ const content =
14
+ status === 'pulling'
15
+ ? '↓ 下拉加载更多'
16
+ : status === 'ready'
17
+ ? '↑ 释放加载历史'
18
+ : status === 'loading'
19
+ ? '加载中...'
20
+ : status === 'noMore'
21
+ ? '没有更多历史记录'
22
+ : '';
23
+
24
+ return (
25
+ <div
26
+ className={`pull-indicator${showTransition ? ' pull-indicator--animated' : ''}`}
27
+ style={{ marginTop: `${marginTop}px` }}
28
+ >
29
+ {status === 'loading' && <div className="pull-indicator__spinner" />}
30
+ <span className="pull-indicator__text">{content}</span>
31
+ </div>
32
+ );
33
+ }
@@ -0,0 +1,48 @@
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: 16px;
10
+ overflow-x: auto;
11
+ padding: 0 24px 12px;
12
+ -ms-overflow-style: none;
13
+ scrollbar-width: none;
14
+
15
+ &::-webkit-scrollbar {
16
+ display: none;
17
+ }
18
+
19
+ &__item {
20
+ flex-shrink: 0;
21
+ display: inline-flex;
22
+ align-items: center;
23
+ gap: 8px;
24
+ height: 61px;
25
+ padding: 0 24px 0 16px;
26
+ appearance: none;
27
+ border: none;
28
+ background: #fff;
29
+ border-radius: 31px;
30
+ box-shadow: 0 2px 12px rgba(110, 139, 180, 0.12);
31
+ }
32
+
33
+ &__icon {
34
+ width: 32px;
35
+ height: 32px;
36
+ object-fit: contain;
37
+ }
38
+
39
+ &__icon-svg {
40
+ color: var(--tbox-primary);
41
+ }
42
+
43
+ &__label {
44
+ font-size: 26px;
45
+ color: rgba(31, 59, 99, 0.85);
46
+ white-space: nowrap;
47
+ }
48
+ }
@@ -0,0 +1,22 @@
1
+ // ⚠️ 平台标准组件:输入框上方的「快捷指令」。视觉还原时不改本组件、不另画。
2
+ // 改内容只改 client/config/chat.json 的 quickCommands 数组({ label, query, icon? })。
3
+ // label 只写纯文字;前面的星标由本组件渲染(icon 字段或默认 ✦),不要把 ✦/emoji 写进 label。
4
+ import { useChatConfigStore } from '@/store/chat-config';
5
+ import starIcon from '../../../assets/quick-command-star.svg';
6
+ import './index.less';
7
+
8
+ export function QuickCommands({ onCommand }: { onCommand: (query: string) => void }) {
9
+ const commands = useChatConfigStore((s) => s.quickCommands);
10
+ if (!commands.length) return null;
11
+
12
+ return (
13
+ <div className="quick-commands">
14
+ {commands.map((cmd) => (
15
+ <button key={cmd.label} className="quick-commands__item" onClick={() => onCommand(cmd.query)}>
16
+ <img className="quick-commands__icon" src={cmd.icon || starIcon} alt="" />
17
+ <span className="quick-commands__label">{cmd.label}</span>
18
+ </button>
19
+ ))}
20
+ </div>
21
+ );
22
+ }
@@ -0,0 +1,90 @@
1
+ .reasoning {
2
+ margin: 0 0 16px;
3
+ border-radius: 14px;
4
+ overflow: hidden;
5
+ min-width: 0;
6
+ max-width: 100%;
7
+ box-sizing: border-box;
8
+
9
+ &--active {
10
+ background: linear-gradient(135deg, rgba(102, 126, 234, 0.08) 0%, rgba(118, 75, 162, 0.06) 100%);
11
+ }
12
+ &--done {
13
+ background: rgba(0, 0, 0, 0.03);
14
+ }
15
+
16
+ &__header {
17
+ display: flex;
18
+ align-items: center;
19
+ justify-content: space-between;
20
+ padding: 14px 18px;
21
+ }
22
+
23
+ &__left {
24
+ display: flex;
25
+ align-items: center;
26
+ gap: 8px;
27
+ }
28
+
29
+ &__indicator {
30
+ width: 10px;
31
+ height: 10px;
32
+ border-radius: 50%;
33
+ background: #b0bec5;
34
+ flex-shrink: 0;
35
+
36
+ &--pulse {
37
+ background: #667eea;
38
+ animation: reasoning-pulse 1.5s ease-in-out infinite;
39
+ }
40
+ }
41
+
42
+ &__title {
43
+ font-size: 22px;
44
+ font-weight: 600;
45
+ color: #6b7a99;
46
+ }
47
+ &--active &__title {
48
+ color: #667eea;
49
+ }
50
+
51
+ &__duration {
52
+ font-size: 20px;
53
+ color: #9ba7c3;
54
+ }
55
+
56
+ &__toggle {
57
+ font-size: 20px;
58
+ color: #8b95b0;
59
+ }
60
+
61
+ &__content {
62
+ margin: 0 18px 16px;
63
+ padding-left: 42px;
64
+ border-left: 3px solid #d6dce8;
65
+ font-size: 22px;
66
+ color: #8b95b0;
67
+ line-height: 1.7;
68
+ overflow-wrap: break-word;
69
+ overflow: hidden;
70
+
71
+ p {
72
+ margin: 6px 0;
73
+ }
74
+ }
75
+ &--active &__content {
76
+ border-left-color: #667eea;
77
+ }
78
+ }
79
+
80
+ @keyframes reasoning-pulse {
81
+ 0%,
82
+ 100% {
83
+ opacity: 0.4;
84
+ transform: scale(0.8);
85
+ }
86
+ 50% {
87
+ opacity: 1;
88
+ transform: scale(1.2);
89
+ }
90
+ }