@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,241 @@
1
+ .tbox-media-list {
2
+ display: flex;
3
+ align-items: flex-end;
4
+ gap: 16px;
5
+ padding: 0 0 16px;
6
+ overflow: visible;
7
+ }
8
+
9
+ .tbox-media-list__scroll {
10
+ display: flex;
11
+ flex-wrap: nowrap;
12
+ overflow-x: auto;
13
+ gap: 16px;
14
+ -webkit-overflow-scrolling: touch;
15
+ min-width: 0;
16
+
17
+ &::-webkit-scrollbar {
18
+ display: none;
19
+ }
20
+ }
21
+
22
+ .tbox-media-item {
23
+ width: var(--tbox-media-item-size);
24
+ min-width: var(--tbox-media-item-size);
25
+ height: var(--tbox-media-item-size);
26
+ border-radius: var(--tbox-media-item-radius);
27
+ overflow: visible;
28
+ position: relative;
29
+ background: #f5f5f5;
30
+
31
+ &--image {
32
+ overflow: hidden;
33
+ }
34
+
35
+ &__thumb {
36
+ width: 100%;
37
+ height: 100%;
38
+ display: block;
39
+ object-fit: cover;
40
+ }
41
+
42
+ &__overlay {
43
+ position: absolute;
44
+ inset: 0;
45
+ display: flex;
46
+ flex-direction: column;
47
+ align-items: center;
48
+ justify-content: center;
49
+ background: rgba(0, 0, 0, 0.5);
50
+ border-radius: var(--tbox-media-item-radius);
51
+ gap: 8px;
52
+ }
53
+
54
+ &__progress-ring {
55
+ width: 60px;
56
+ height: 60px;
57
+ border-radius: 50%;
58
+ display: flex;
59
+ align-items: center;
60
+ justify-content: center;
61
+ transition: background 0.3s ease;
62
+ }
63
+
64
+ &__progress-inner {
65
+ width: 48px;
66
+ height: 48px;
67
+ border-radius: 50%;
68
+ background: rgba(0, 0, 0, 0.7);
69
+ display: flex;
70
+ align-items: center;
71
+ justify-content: center;
72
+ }
73
+
74
+ &__progress-text {
75
+ color: #fff;
76
+ font-size: 20px;
77
+ font-weight: bold;
78
+ text-align: center;
79
+ }
80
+
81
+ &__status-icon {
82
+ display: flex;
83
+ align-items: center;
84
+ justify-content: center;
85
+ }
86
+
87
+ &__status-text {
88
+ color: rgba(255, 255, 255, 0.85);
89
+ font-size: 20px;
90
+ font-weight: 500;
91
+ text-align: center;
92
+ white-space: nowrap;
93
+ }
94
+
95
+ &__remove {
96
+ position: absolute;
97
+ top: 12px;
98
+ right: 12px;
99
+ width: 36px;
100
+ height: 36px;
101
+ border-radius: 18px;
102
+ background: rgba(0, 0, 0, 0.55);
103
+ display: flex;
104
+ align-items: center;
105
+ justify-content: center;
106
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
107
+ z-index: 2;
108
+ }
109
+ }
110
+
111
+ /* 非图片(视频/文件)卡片 */
112
+ .tbox-file-item {
113
+ display: flex;
114
+ flex-direction: row;
115
+ align-items: center;
116
+ gap: 12px;
117
+ min-width: 280px;
118
+ height: var(--tbox-media-item-size);
119
+ border-radius: var(--tbox-media-item-radius);
120
+ background: #fcfcfc;
121
+ padding: 16px;
122
+ box-sizing: border-box;
123
+ position: relative;
124
+ overflow: visible;
125
+
126
+ &__icon {
127
+ flex-shrink: 0;
128
+ width: 48px;
129
+ height: 48px;
130
+ display: flex;
131
+ align-items: center;
132
+ justify-content: center;
133
+ }
134
+
135
+ &__info {
136
+ display: flex;
137
+ flex-direction: column;
138
+ justify-content: center;
139
+ gap: 4px;
140
+ flex: 1;
141
+ min-width: 0;
142
+ }
143
+
144
+ &__name {
145
+ font-size: 24px;
146
+ color: #333;
147
+ font-weight: 500;
148
+ overflow: hidden;
149
+ text-overflow: ellipsis;
150
+ white-space: nowrap;
151
+ max-width: 100%;
152
+ }
153
+
154
+ &__meta {
155
+ font-size: 20px;
156
+ color: rgba(0, 0, 0, 0.25);
157
+ white-space: nowrap;
158
+
159
+ &--failed {
160
+ color: #ff4d4f;
161
+ }
162
+ }
163
+
164
+ .tbox-media-item__remove {
165
+ position: absolute;
166
+ top: 12px;
167
+ right: 12px;
168
+ }
169
+ }
170
+
171
+ /* 末尾“加更多”按钮 */
172
+ .tbox-media-add {
173
+ width: var(--tbox-media-item-size);
174
+ min-width: var(--tbox-media-item-size);
175
+ height: var(--tbox-media-item-size);
176
+ border-radius: var(--tbox-media-item-radius);
177
+ background: rgba(255, 255, 255, 0.6);
178
+ display: flex;
179
+ align-items: center;
180
+ justify-content: center;
181
+ flex-shrink: 0;
182
+ position: relative;
183
+ }
184
+
185
+ /* 加更多的二级 popover */
186
+ .tbox-media-popover {
187
+ position: absolute;
188
+ bottom: calc(var(--tbox-media-item-size) + 8px);
189
+ right: 0;
190
+ width: var(--tbox-media-item-size);
191
+ background: #fff;
192
+ border-radius: 20px;
193
+ padding: 16px 0;
194
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
195
+ z-index: 10;
196
+ opacity: 0;
197
+ transform: translateY(8px);
198
+ transition: opacity 0.2s ease-out, transform 0.2s ease-out;
199
+ pointer-events: none;
200
+
201
+ &--visible {
202
+ opacity: 1;
203
+ transform: translateY(0);
204
+ pointer-events: auto;
205
+ }
206
+
207
+ &__item {
208
+ display: flex;
209
+ flex-direction: row;
210
+ align-items: center;
211
+ height: 60px;
212
+ gap: 8px;
213
+ padding: 0 16px;
214
+
215
+ & + & {
216
+ margin-top: 8px;
217
+ }
218
+ }
219
+
220
+ &__icon-wrap {
221
+ width: 40px;
222
+ height: 40px;
223
+ border-radius: 8px;
224
+ display: flex;
225
+ align-items: center;
226
+ justify-content: center;
227
+ flex-shrink: 0;
228
+ }
229
+
230
+ &__icon {
231
+ width: 32px;
232
+ height: 32px;
233
+ }
234
+
235
+ &__label {
236
+ font-size: 28px;
237
+ color: rgba(31, 59, 99, 0.88);
238
+ font-weight: 400;
239
+ line-height: 1.4;
240
+ }
241
+ }
@@ -0,0 +1,256 @@
1
+ import { useCallback, useEffect, useState } from 'react';
2
+ import { X, Warning, Prohibit, Plus, VideoCamera } from '@phosphor-icons/react';
3
+ import { previewImages } from '@/utils/imagePreview';
4
+ import { toast } from '@/utils/toast';
5
+ import type { MediaItem } from '@/types/chat';
6
+ import './index.less';
7
+
8
+ /** 媒体选项图标(远程素材,对齐原模板)。 */
9
+ const POPOVER_ICONS: Record<string, { icon: string; label: string; bg: string }> = {
10
+ image: {
11
+ icon: 'https://mdn.alipayobjects.com/huamei_nzq4hr/afts/img/A*DfDhSYAMzoQAAAAAHiAAAAgAel-hAQ/original',
12
+ label: '相册',
13
+ bg: '#0000001a',
14
+ },
15
+ camera: {
16
+ icon: 'https://mdn.alipayobjects.com/huamei_nzq4hr/afts/img/A*xdKLSabxqmQAAAAAOnAAAAgAel-hAQ/original',
17
+ label: '拍摄',
18
+ bg: '#4f58ec26',
19
+ },
20
+ video: {
21
+ icon: 'https://mdn.alipayobjects.com/huamei_nzq4hr/afts/img/A*xdKLSabxqmQAAAAAOnAAAAgAel-hAQ/original',
22
+ label: '视频',
23
+ bg: '#4f58ec26',
24
+ },
25
+ };
26
+
27
+ function formatSize(bytes?: number): string {
28
+ if (!bytes) return '';
29
+ if (bytes < 1024) return `${bytes}B`;
30
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
31
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
32
+ }
33
+
34
+ function ImageItemCard({
35
+ item,
36
+ allImages,
37
+ onRemove,
38
+ onRetry,
39
+ }: {
40
+ item: MediaItem;
41
+ allImages: string[];
42
+ onRemove: (id: string) => void;
43
+ onRetry?: (id: string) => void;
44
+ }) {
45
+ const isUploading = !!item.isUploading;
46
+ const isFailed = !!item.uploadFailed;
47
+ const isIllegal = !!item.illegal;
48
+ const progress = item.uploadProgress ?? 0;
49
+
50
+ const onClick = () => {
51
+ if (isUploading) return;
52
+ if (isFailed) return onRetry?.(item.id);
53
+ if (isIllegal) return void toast('内容未过审核');
54
+ previewImages(allImages, allImages.indexOf(item.src));
55
+ };
56
+
57
+ return (
58
+ <div className="tbox-media-item tbox-media-item--image" onClick={onClick}>
59
+ <img className="tbox-media-item__thumb" src={item.src} alt="" />
60
+
61
+ {isUploading && (
62
+ <div className="tbox-media-item__overlay">
63
+ <div
64
+ className="tbox-media-item__progress-ring"
65
+ style={{ background: `conic-gradient(#fff ${progress}%, rgba(255,255,255,0.25) ${progress}%)` }}
66
+ >
67
+ <div className="tbox-media-item__progress-inner">
68
+ <span className="tbox-media-item__progress-text">{progress}%</span>
69
+ </div>
70
+ </div>
71
+ </div>
72
+ )}
73
+
74
+ {isFailed && (
75
+ <div className="tbox-media-item__overlay">
76
+ <div className="tbox-media-item__status-icon">
77
+ <Warning size={20} color="#fff" />
78
+ </div>
79
+ <span className="tbox-media-item__status-text">点击重试</span>
80
+ </div>
81
+ )}
82
+
83
+ {isIllegal && (
84
+ <div className="tbox-media-item__overlay">
85
+ <div className="tbox-media-item__status-icon">
86
+ <Prohibit size={20} color="#fff" />
87
+ </div>
88
+ <span className="tbox-media-item__status-text">未过审核</span>
89
+ </div>
90
+ )}
91
+
92
+ {!isUploading && (
93
+ <div className="tbox-media-item__remove" onClick={(e) => { e.stopPropagation(); onRemove(item.id); }}>
94
+ <X size={14} color="#fff" />
95
+ </div>
96
+ )}
97
+ </div>
98
+ );
99
+ }
100
+
101
+ function FileItemCard({
102
+ item,
103
+ onRemove,
104
+ onRetry,
105
+ }: {
106
+ item: MediaItem;
107
+ onRemove: (id: string) => void;
108
+ onRetry?: (id: string) => void;
109
+ }) {
110
+ const isUploading = !!item.isUploading;
111
+ const isFailed = !!item.uploadFailed;
112
+
113
+ const meta = isUploading
114
+ ? `上传中...${item.uploadProgress ?? 0}%`
115
+ : isFailed
116
+ ? '上传失败'
117
+ : formatSize(item.size);
118
+
119
+ return (
120
+ <div className="tbox-file-item" onClick={() => isFailed && onRetry?.(item.id)}>
121
+ <div className="tbox-file-item__icon">
122
+ <VideoCamera size={28} color="#506fff" />
123
+ </div>
124
+ <div className="tbox-file-item__info">
125
+ <span className="tbox-file-item__name">{item.name ?? '未知文件'}</span>
126
+ <span className={`tbox-file-item__meta${isFailed ? ' tbox-file-item__meta--failed' : ''}`}>{meta}</span>
127
+ </div>
128
+ {!isUploading && (
129
+ <div className="tbox-media-item__remove" onClick={(e) => { e.stopPropagation(); onRemove(item.id); }}>
130
+ <X size={14} color="#fff" />
131
+ </div>
132
+ )}
133
+ </div>
134
+ );
135
+ }
136
+
137
+ function MediaPopover({
138
+ visible,
139
+ options,
140
+ onSelect,
141
+ }: {
142
+ visible: boolean;
143
+ options: { type: string; icon: string; label: string; bg: string }[];
144
+ onSelect: (type: string) => void;
145
+ }) {
146
+ return (
147
+ <div className={`tbox-media-popover${visible ? ' tbox-media-popover--visible' : ''}`}>
148
+ {options.map((opt) => (
149
+ <div
150
+ key={opt.type}
151
+ className="tbox-media-popover__item"
152
+ onClick={(e) => { e.stopPropagation(); onSelect(opt.type); }}
153
+ >
154
+ <div className="tbox-media-popover__icon-wrap" style={{ background: opt.bg }}>
155
+ <img className="tbox-media-popover__icon" src={opt.icon} alt="" />
156
+ </div>
157
+ <span className="tbox-media-popover__label">{opt.label}</span>
158
+ </div>
159
+ ))}
160
+ </div>
161
+ );
162
+ }
163
+
164
+ export interface MediaListProps {
165
+ items: MediaItem[];
166
+ onRemove: (id: string) => void;
167
+ onRetry?: (id: string) => void;
168
+ onPickImage?: () => void;
169
+ onPickCamera?: () => void;
170
+ onPickVideo?: () => void;
171
+ enableImage?: boolean;
172
+ enableVideo?: boolean;
173
+ isWebPc?: boolean;
174
+ }
175
+
176
+ /** 已选媒体预览列表(对齐原模板:进度环 / 失败-未过审 overlay / 删除 / 末尾加号 popover)。 */
177
+ export function MediaList({
178
+ items,
179
+ onRemove,
180
+ onRetry,
181
+ onPickImage,
182
+ onPickCamera,
183
+ onPickVideo,
184
+ enableImage,
185
+ enableVideo,
186
+ isWebPc,
187
+ }: MediaListProps) {
188
+ const [popoverVisible, setPopoverVisible] = useState(false);
189
+ const hasCamera = !!enableImage && !isWebPc;
190
+
191
+ const options = [
192
+ enableImage && { type: 'image', ...POPOVER_ICONS.image },
193
+ hasCamera && { type: 'camera', ...POPOVER_ICONS.camera },
194
+ enableVideo && { type: 'video', ...POPOVER_ICONS.video },
195
+ ].filter(Boolean) as { type: string; icon: string; label: string; bg: string }[];
196
+
197
+ const allImages = items
198
+ .filter((i) => i.type === 'image' && !i.isUploading && !i.uploadFailed && !i.illegal)
199
+ .map((i) => i.src);
200
+
201
+ const handleAddClick = useCallback(() => {
202
+ if (options.length <= 1) {
203
+ if (enableImage) onPickImage?.();
204
+ else if (enableVideo) onPickVideo?.();
205
+ return;
206
+ }
207
+ setPopoverVisible((v) => !v);
208
+ }, [options.length, enableImage, enableVideo, onPickImage, onPickVideo]);
209
+
210
+ const handleSelect = useCallback(
211
+ (type: string) => {
212
+ setPopoverVisible(false);
213
+ if (type === 'image') onPickImage?.();
214
+ else if (type === 'camera') onPickCamera?.();
215
+ else if (type === 'video') onPickVideo?.();
216
+ },
217
+ [onPickImage, onPickCamera, onPickVideo],
218
+ );
219
+
220
+ useEffect(() => {
221
+ if (!popoverVisible) return;
222
+ const onDown = (e: MouseEvent | TouchEvent) => {
223
+ const t = e.target as HTMLElement;
224
+ if (t && !t.closest('.tbox-media-add') && !t.closest('.tbox-media-popover')) setPopoverVisible(false);
225
+ };
226
+ document.addEventListener('mousedown', onDown);
227
+ document.addEventListener('touchstart', onDown, { passive: true });
228
+ return () => {
229
+ document.removeEventListener('mousedown', onDown);
230
+ document.removeEventListener('touchstart', onDown);
231
+ };
232
+ }, [popoverVisible]);
233
+
234
+ if (items.length === 0) return null;
235
+ const showAddButton = options.length > 0;
236
+
237
+ return (
238
+ <div className="tbox-media-list">
239
+ <div className="tbox-media-list__scroll">
240
+ {items.map((item) =>
241
+ item.type === 'image' ? (
242
+ <ImageItemCard key={item.id} item={item} allImages={allImages} onRemove={onRemove} onRetry={onRetry} />
243
+ ) : (
244
+ <FileItemCard key={item.id} item={item} onRemove={onRemove} onRetry={onRetry} />
245
+ ),
246
+ )}
247
+ </div>
248
+ {showAddButton && (
249
+ <div className="tbox-media-add" onClick={handleAddClick}>
250
+ <Plus size={22} color="rgba(0,0,0,0.65)" />
251
+ <MediaPopover visible={popoverVisible} options={options} onSelect={handleSelect} />
252
+ </div>
253
+ )}
254
+ </div>
255
+ );
256
+ }