@huyooo/ai-chat-frontend-react 0.1.6 → 0.2.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 (91) hide show
  1. package/README.md +368 -0
  2. package/dist/index.css +2575 -0
  3. package/dist/index.css.map +1 -0
  4. package/dist/index.d.ts +378 -135
  5. package/dist/index.js +3956 -1042
  6. package/dist/index.js.map +1 -1
  7. package/dist/style.css +48 -987
  8. package/package.json +7 -4
  9. package/src/adapter.ts +10 -70
  10. package/src/components/ChatPanel.tsx +373 -117
  11. package/src/components/common/ConfirmDialog.css +136 -0
  12. package/src/components/common/ConfirmDialog.tsx +91 -0
  13. package/src/components/common/CopyButton.css +22 -0
  14. package/src/components/common/CopyButton.tsx +46 -0
  15. package/src/components/common/IndexingSettings.css +207 -0
  16. package/src/components/common/IndexingSettings.tsx +398 -0
  17. package/src/components/common/SettingsPanel.css +256 -0
  18. package/src/components/common/SettingsPanel.tsx +120 -0
  19. package/src/components/common/Toast.css +50 -0
  20. package/src/components/common/Toast.tsx +38 -0
  21. package/src/components/common/ToggleSwitch.css +52 -0
  22. package/src/components/common/ToggleSwitch.tsx +20 -0
  23. package/src/components/header/ChatHeader.css +285 -0
  24. package/src/components/header/ChatHeader.tsx +376 -0
  25. package/src/components/input/AtFilePicker.css +147 -0
  26. package/src/components/input/AtFilePicker.tsx +519 -0
  27. package/src/components/input/ChatInput.css +204 -0
  28. package/src/components/input/ChatInput.tsx +506 -0
  29. package/src/components/input/DropdownSelector.css +159 -0
  30. package/src/components/input/DropdownSelector.tsx +195 -0
  31. package/src/components/input/ImagePreviewModal.css +124 -0
  32. package/src/components/input/ImagePreviewModal.tsx +118 -0
  33. package/src/components/input/at-views/AtBranchView.tsx +34 -0
  34. package/src/components/input/at-views/AtBrowserView.tsx +34 -0
  35. package/src/components/input/at-views/AtChatsView.tsx +34 -0
  36. package/src/components/input/at-views/AtDocsView.tsx +34 -0
  37. package/src/components/input/at-views/AtFilesView.tsx +168 -0
  38. package/src/components/input/at-views/AtTerminalsView.tsx +34 -0
  39. package/src/components/input/at-views/AtViewStyles.css +143 -0
  40. package/src/components/input/at-views/index.ts +9 -0
  41. package/src/components/message/ContentRenderer.css +9 -0
  42. package/src/components/message/ContentRenderer.tsx +63 -0
  43. package/src/components/message/MessageBubble.css +190 -0
  44. package/src/components/message/MessageBubble.tsx +231 -0
  45. package/src/components/message/PartsRenderer.css +4 -0
  46. package/src/components/message/PartsRenderer.tsx +114 -0
  47. package/src/components/message/ToolResultRenderer.tsx +21 -0
  48. package/src/components/message/WelcomeMessage.css +221 -0
  49. package/src/components/message/WelcomeMessage.tsx +93 -0
  50. package/src/components/message/blocks/CodeBlock.tsx +60 -0
  51. package/src/components/message/blocks/TextBlock.tsx +15 -0
  52. package/src/components/message/blocks/blocks.css +141 -0
  53. package/src/components/message/blocks/index.ts +6 -0
  54. package/src/components/message/parts/CollapsibleCard.css +78 -0
  55. package/src/components/message/parts/CollapsibleCard.tsx +77 -0
  56. package/src/components/message/parts/ErrorPart.css +9 -0
  57. package/src/components/message/parts/ErrorPart.tsx +40 -0
  58. package/src/components/message/parts/ImagePart.css +50 -0
  59. package/src/components/message/parts/ImagePart.tsx +54 -0
  60. package/src/components/message/parts/SearchPart.css +44 -0
  61. package/src/components/message/parts/SearchPart.tsx +63 -0
  62. package/src/components/message/parts/TextPart.css +10 -0
  63. package/src/components/message/parts/TextPart.tsx +20 -0
  64. package/src/components/message/parts/ThinkingPart.css +9 -0
  65. package/src/components/message/parts/ThinkingPart.tsx +48 -0
  66. package/src/components/message/parts/ToolCallPart.css +220 -0
  67. package/src/components/message/parts/ToolCallPart.tsx +285 -0
  68. package/src/components/message/parts/ToolResultPart.css +68 -0
  69. package/src/components/message/parts/ToolResultPart.tsx +96 -0
  70. package/src/components/message/parts/index.ts +11 -0
  71. package/src/components/message/tool-results/DefaultToolResult.tsx +26 -0
  72. package/src/components/message/tool-results/SearchResults.tsx +69 -0
  73. package/src/components/message/tool-results/WeatherCard.tsx +63 -0
  74. package/src/components/message/tool-results/index.ts +7 -0
  75. package/src/components/message/tool-results/tool-results.css +179 -0
  76. package/src/components/message/welcome-types.ts +46 -0
  77. package/src/context/AutoRunConfigContext.tsx +13 -0
  78. package/src/context/ChatAdapterContext.tsx +8 -0
  79. package/src/context/ChatInputContext.tsx +40 -0
  80. package/src/context/RenderersContext.tsx +41 -0
  81. package/src/hooks/useChat.ts +855 -237
  82. package/src/hooks/useImageUpload.ts +253 -0
  83. package/src/index.ts +96 -39
  84. package/src/styles.css +48 -987
  85. package/src/types/index.ts +172 -103
  86. package/src/utils/fileIcon.ts +49 -0
  87. package/src/components/ChatInput.tsx +0 -368
  88. package/src/components/chat/messages/ExecutionSteps.tsx +0 -234
  89. package/src/components/chat/messages/MessageBubble.tsx +0 -130
  90. package/src/components/chat/ui/ChatHeader.tsx +0 -301
  91. package/src/components/chat/ui/WelcomeMessage.tsx +0 -107
@@ -0,0 +1,159 @@
1
+ /**
2
+ * DropdownSelector 组件样式
3
+ */
4
+
5
+ .dropdown-selector {
6
+ position: relative;
7
+ display: flex;
8
+ align-items: center;
9
+ gap: 4px;
10
+ padding: 4px 8px;
11
+ background: var(--chat-muted, #3c3c3c);
12
+ border: none;
13
+ border-radius: 6px;
14
+ font-size: 14px;
15
+ color: var(--chat-text-muted, #888);
16
+ cursor: pointer;
17
+ transition: all 0.15s;
18
+ }
19
+
20
+ .dropdown-selector:hover:not(.disabled) {
21
+ background: var(--chat-muted-hover, #444);
22
+ color: var(--chat-text, #ccc);
23
+ }
24
+
25
+ .dropdown-selector.disabled {
26
+ opacity: 0.6;
27
+ cursor: not-allowed;
28
+ }
29
+
30
+ .dropdown-selector .chevron {
31
+ color: var(--chat-text-muted, #666);
32
+ }
33
+
34
+ /* 下拉菜单 */
35
+ .dropdown-selector .dropdown-menu {
36
+ position: absolute;
37
+ left: 0;
38
+ right: auto;
39
+ min-width: 180px;
40
+ max-height: 320px;
41
+ overflow-y: auto;
42
+ background: var(--chat-dropdown-bg, #252526);
43
+ border: 1px solid rgba(255, 255, 255, 0.1);
44
+ border-radius: 8px;
45
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
46
+ z-index: 9999;
47
+ padding: 4px 8px 4px 4px;
48
+ display: flex;
49
+ flex-direction: column;
50
+ gap: 2px;
51
+ }
52
+
53
+ /* 向上弹出 */
54
+ .dropdown-selector .dropdown-menu.dropdown-up {
55
+ bottom: 100%;
56
+ top: auto;
57
+ margin-bottom: 4px;
58
+ }
59
+
60
+ /* 向下弹出 */
61
+ .dropdown-selector .dropdown-menu.dropdown-down {
62
+ top: 100%;
63
+ bottom: auto;
64
+ margin-top: 4px;
65
+ }
66
+
67
+ /* 右对齐 */
68
+ .dropdown-selector .dropdown-menu.dropdown-align-right {
69
+ left: auto;
70
+ right: 0;
71
+ }
72
+
73
+ .dropdown-selector .dropdown-item {
74
+ display: flex;
75
+ align-items: center;
76
+ gap: 8px;
77
+ width: 100%;
78
+ padding: 8px 10px;
79
+ border: none;
80
+ background: transparent;
81
+ border-radius: 4px;
82
+ font-size: 14px;
83
+ color: var(--chat-text-muted, #999);
84
+ cursor: pointer;
85
+ transition: all 0.15s;
86
+ white-space: nowrap;
87
+ }
88
+
89
+ .dropdown-selector .dropdown-item:hover {
90
+ background: rgba(255, 255, 255, 0.08);
91
+ color: var(--chat-text, #ccc);
92
+ }
93
+
94
+ .dropdown-selector .dropdown-item.active {
95
+ background: rgba(255, 255, 255, 0.1);
96
+ color: var(--chat-text, #fff);
97
+ }
98
+
99
+ .dropdown-selector .check-icon {
100
+ margin-left: auto;
101
+ color: var(--chat-text, #ccc);
102
+ flex-shrink: 0;
103
+ }
104
+
105
+ .dropdown-selector .selector-text {
106
+ /* max-width: 150px; */
107
+ overflow: hidden;
108
+ text-overflow: ellipsis;
109
+ white-space: nowrap;
110
+ }
111
+
112
+ /* 空状态 */
113
+ .dropdown-selector .dropdown-empty {
114
+ padding: 12px 10px;
115
+ font-size: 12px;
116
+ color: var(--chat-text-muted, #666);
117
+ text-align: center;
118
+ }
119
+
120
+ /* 分组标题 */
121
+ .dropdown-selector .group-title {
122
+ padding: 8px 10px 4px;
123
+ margin-top: 4px;
124
+ font-size: 11px;
125
+ color: var(--chat-text-muted, #666);
126
+ }
127
+
128
+ .dropdown-selector .group-title:first-child {
129
+ margin-top: 0;
130
+ }
131
+
132
+ /* 选项标签 */
133
+ .dropdown-selector .option-label {
134
+ flex: 1;
135
+ text-align: left;
136
+ }
137
+
138
+ /* 右侧容器(标签和勾选图标) */
139
+ .dropdown-selector .option-right {
140
+ display: flex;
141
+ align-items: center;
142
+ gap: 8px;
143
+ margin-left: auto;
144
+ }
145
+
146
+ /* 提供商标记 */
147
+ .dropdown-selector .provider-badge {
148
+ padding: 2px 6px;
149
+ font-size: 11px;
150
+ font-weight: 500;
151
+ color: var(--chat-text-muted, #999);
152
+ background: rgba(255, 255, 255, 0.08);
153
+ border-radius: 3px;
154
+ white-space: nowrap;
155
+ }
156
+
157
+ .dropdown-selector .provider-badge.native {
158
+ color: var(--chat-text-muted, #999);
159
+ }
@@ -0,0 +1,195 @@
1
+ /**
2
+ * DropdownSelector Component
3
+ * 与 Vue 版本 DropdownSelector.vue 保持一致
4
+ * 前端只负责渲染,分组数据由后端提供
5
+ */
6
+
7
+ import { useState, useRef, useCallback, useEffect, useMemo } from 'react';
8
+ import { Icon } from '@iconify/react';
9
+ import './DropdownSelector.css';
10
+
11
+ /** 下拉选项 */
12
+ export interface DropdownOption {
13
+ value: string;
14
+ label: string;
15
+ icon?: string;
16
+ /** 分组名称(由后端决定,前端只负责渲染) */
17
+ group?: string;
18
+ }
19
+
20
+ /** 分组后的选项(由后端提供) */
21
+ export interface GroupedOptions {
22
+ [groupName: string]: DropdownOption[];
23
+ }
24
+
25
+ interface DropdownSelectorProps {
26
+ /** 当前选中的值 */
27
+ value: string;
28
+ /** 选项列表(扁平列表) */
29
+ options?: DropdownOption[];
30
+ /** 分组后的选项(优先级高于 options,由后端提供) */
31
+ groupedOptions?: GroupedOptions;
32
+ /** 占位符文本 */
33
+ placeholder?: string;
34
+ /** 选择回调 */
35
+ onSelect?: (value: string) => void;
36
+ /** 是否禁用 */
37
+ disabled?: boolean;
38
+ /** 下拉菜单对齐方式 */
39
+ align?: 'left' | 'right';
40
+ }
41
+
42
+ export function DropdownSelector({
43
+ value,
44
+ options,
45
+ groupedOptions,
46
+ placeholder = '请选择',
47
+ onSelect,
48
+ disabled = false,
49
+ align = 'left',
50
+ }: DropdownSelectorProps) {
51
+ const selectorRef = useRef<HTMLDivElement>(null);
52
+ const [menuOpen, setMenuOpen] = useState(false);
53
+ const [dropdownDirection, setDropdownDirection] = useState<'up' | 'down'>('up');
54
+
55
+ // 检查是否有选项
56
+ const hasOptions = useMemo(() => {
57
+ if (groupedOptions && Object.keys(groupedOptions).length > 0) {
58
+ return Object.values(groupedOptions).some(group => group.length > 0);
59
+ }
60
+ return options && options.length > 0;
61
+ }, [options, groupedOptions]);
62
+
63
+ // 当前选中的选项(从扁平列表或分组数据中查找)
64
+ const currentOption = useMemo(() => {
65
+ if (groupedOptions) {
66
+ for (const group of Object.values(groupedOptions)) {
67
+ const found = group.find(opt => opt.value === value);
68
+ if (found) return found;
69
+ }
70
+ }
71
+ return options?.find(opt => opt.value === value);
72
+ }, [value, options, groupedOptions]);
73
+
74
+ // 排序后的选项(扁平列表模式使用)
75
+ const sortedOptions = useMemo(() => {
76
+ if (!options) return [];
77
+ return [...options].sort((a, b) => a.label.localeCompare(b.label));
78
+ }, [options]);
79
+
80
+ /**
81
+ * 计算下拉方向(根据可用空间)
82
+ */
83
+ const calculateDropdownDirection = useCallback((): 'up' | 'down' => {
84
+ if (!selectorRef.current) return 'up';
85
+ const rect = selectorRef.current.getBoundingClientRect();
86
+ const spaceAbove = rect.top;
87
+ const spaceBelow = window.innerHeight - rect.bottom;
88
+ // 如果上方空间小于 200px 且下方空间更大,则向下弹出
89
+ return spaceAbove < 200 && spaceBelow > spaceAbove ? 'down' : 'up';
90
+ }, []);
91
+
92
+ /**
93
+ * 切换菜单
94
+ */
95
+ const toggleMenu = useCallback(
96
+ (e: React.MouseEvent) => {
97
+ e.stopPropagation();
98
+ if (disabled) return;
99
+ if (!menuOpen) {
100
+ setDropdownDirection(calculateDropdownDirection());
101
+ }
102
+ setMenuOpen((prev) => !prev);
103
+ },
104
+ [menuOpen, calculateDropdownDirection, disabled]
105
+ );
106
+
107
+ /**
108
+ * 选择选项
109
+ */
110
+ const selectOption = useCallback(
111
+ (optValue: string) => {
112
+ onSelect?.(optValue);
113
+ setMenuOpen(false);
114
+ },
115
+ [onSelect]
116
+ );
117
+
118
+ /**
119
+ * 点击外部关闭菜单
120
+ */
121
+ useEffect(() => {
122
+ const handleClickOutside = (event: MouseEvent) => {
123
+ const target = event.target as HTMLElement;
124
+ if (selectorRef.current && !selectorRef.current.contains(target)) {
125
+ setMenuOpen(false);
126
+ }
127
+ };
128
+
129
+ document.addEventListener('click', handleClickOutside);
130
+ return () => document.removeEventListener('click', handleClickOutside);
131
+ }, []);
132
+
133
+ return (
134
+ <div
135
+ ref={selectorRef}
136
+ className={`dropdown-selector${disabled ? ' disabled' : ''}`}
137
+ onClick={!disabled ? toggleMenu : undefined}
138
+ >
139
+ {currentOption?.icon && <Icon icon={currentOption.icon} width={14} />}
140
+ <span className="selector-text">{currentOption?.label || placeholder}</span>
141
+ <Icon icon="lucide:chevron-down" width={14} className="chevron" />
142
+
143
+ {menuOpen && (
144
+ <div
145
+ className={`dropdown-menu dropdown-${dropdownDirection}${align === 'right' ? ' dropdown-align-right' : ''}`}
146
+ onClick={(e) => e.stopPropagation()}
147
+ >
148
+ {/* 空状态 */}
149
+ {!hasOptions && (
150
+ <div className="dropdown-empty">暂无数据</div>
151
+ )}
152
+
153
+ {/* 分组模式:使用后端返回的分组数据 */}
154
+ {groupedOptions && Object.keys(groupedOptions).length > 0 ? (
155
+ <>
156
+ {Object.entries(groupedOptions).map(([groupName, groupItems]) => (
157
+ <div key={groupName}>
158
+ <div className="group-title">{groupName}</div>
159
+ {groupItems.map((opt) => (
160
+ <button
161
+ key={opt.value}
162
+ className={`dropdown-item${value === opt.value ? ' active' : ''}`}
163
+ onClick={() => selectOption(opt.value)}
164
+ >
165
+ {opt.icon && <Icon icon={opt.icon} width={14} />}
166
+ <span className="option-label">{opt.label}</span>
167
+ <span className="option-right">
168
+ {value === opt.value && <Icon icon="lucide:check" width={14} className="check-icon" />}
169
+ </span>
170
+ </button>
171
+ ))}
172
+ </div>
173
+ ))}
174
+ </>
175
+ ) : (
176
+ /* 扁平列表模式:无分组数据时使用 */
177
+ sortedOptions.map((opt) => (
178
+ <button
179
+ key={opt.value}
180
+ className={`dropdown-item${value === opt.value ? ' active' : ''}`}
181
+ onClick={() => selectOption(opt.value)}
182
+ >
183
+ {opt.icon && <Icon icon={opt.icon} width={14} />}
184
+ <span className="option-label">{opt.label}</span>
185
+ <span className="option-right">
186
+ {value === opt.value && <Icon icon="lucide:check" width={14} className="check-icon" />}
187
+ </span>
188
+ </button>
189
+ ))
190
+ )}
191
+ </div>
192
+ )}
193
+ </div>
194
+ );
195
+ }
@@ -0,0 +1,124 @@
1
+ /* 遮罩层 */
2
+ .image-preview-modal {
3
+ position: fixed;
4
+ inset: 0;
5
+ z-index: 99999;
6
+ display: flex;
7
+ flex-direction: column;
8
+ background: rgb(61 61 61 / 40%);
9
+ backdrop-filter: blur(8px);
10
+ animation: fadeIn 0.2s ease;
11
+ }
12
+
13
+ @keyframes fadeIn {
14
+ from {
15
+ opacity: 0;
16
+ }
17
+ to {
18
+ opacity: 1;
19
+ }
20
+ }
21
+
22
+ /* 顶部栏 */
23
+ .preview-header {
24
+ position: relative;
25
+ z-index: 10;
26
+ display: flex;
27
+ align-items: center;
28
+ justify-content: center;
29
+ gap: 12px;
30
+ padding: 16px 20px;
31
+ min-height: 52px;
32
+ }
33
+
34
+ .preview-counter {
35
+ color: rgba(255, 255, 255, 0.85);
36
+ font-size: 14px;
37
+ font-variant-numeric: tabular-nums;
38
+ }
39
+
40
+ .preview-close-btn {
41
+ position: absolute;
42
+ right: 16px;
43
+ top: 50%;
44
+ transform: translateY(-50%);
45
+ display: flex;
46
+ align-items: center;
47
+ justify-content: center;
48
+ height: 36px;
49
+ padding: 0 12px;
50
+ background: rgba(255, 255, 255, 0.1);
51
+ border: none;
52
+ border-radius: 8px;
53
+ color: #fff;
54
+ cursor: pointer;
55
+ transition: all 0.15s;
56
+ }
57
+
58
+ .preview-close-btn:hover {
59
+ background: rgba(255, 255, 255, 0.2);
60
+ transform: translateY(-50%) scale(1.05);
61
+ }
62
+
63
+ .preview-close-btn:active {
64
+ transform: translateY(-50%) scale(0.95);
65
+ }
66
+
67
+ /* 导航按钮 */
68
+ .preview-nav-btn {
69
+ position: absolute;
70
+ top: 50%;
71
+ transform: translateY(-50%);
72
+ z-index: 10;
73
+ display: flex;
74
+ align-items: center;
75
+ justify-content: center;
76
+ height: 40px;
77
+ padding: 0 12px;
78
+ background: rgba(255, 255, 255, 0.1);
79
+ border: none;
80
+ border-radius: 8px;
81
+ color: #fff;
82
+ cursor: pointer;
83
+ transition: all 0.15s;
84
+ }
85
+
86
+ .preview-nav-btn:hover:not(.disabled) {
87
+ background: rgba(255, 255, 255, 0.2);
88
+ transform: translateY(-50%) scale(1.1);
89
+ }
90
+
91
+ .preview-nav-btn:active:not(.disabled) {
92
+ transform: translateY(-50%) scale(0.95);
93
+ }
94
+
95
+ .preview-nav-btn.disabled {
96
+ opacity: 0.4;
97
+ cursor: not-allowed;
98
+ }
99
+
100
+ .preview-nav-prev {
101
+ left: 24px;
102
+ }
103
+
104
+ .preview-nav-next {
105
+ right: 24px;
106
+ }
107
+
108
+ /* 主图片区域 */
109
+ .preview-main {
110
+ flex: 1;
111
+ display: flex;
112
+ align-items: center;
113
+ justify-content: center;
114
+ overflow: hidden;
115
+ padding: 0 80px;
116
+ }
117
+
118
+ .preview-image {
119
+ max-width: calc(100% - 80px);
120
+ max-height: calc(100vh - 140px);
121
+ object-fit: contain;
122
+ border-radius: 8px;
123
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
124
+ }
@@ -0,0 +1,118 @@
1
+ import { useCallback, useEffect, useState } from 'react'
2
+ import { createPortal } from 'react-dom'
3
+ import { Icon } from '@iconify/react'
4
+ import './ImagePreviewModal.css'
5
+
6
+ interface ImagePreviewModalProps {
7
+ visible: boolean
8
+ images: string[]
9
+ initialIndex?: number
10
+ onClose: () => void
11
+ onIndexChange?: (index: number) => void
12
+ }
13
+
14
+ export function ImagePreviewModal({
15
+ visible,
16
+ images,
17
+ initialIndex = 0,
18
+ onClose,
19
+ onIndexChange,
20
+ }: ImagePreviewModalProps) {
21
+ const [currentIndex, setCurrentIndex] = useState(initialIndex)
22
+
23
+ // 重置索引
24
+ useEffect(() => {
25
+ if (visible) {
26
+ setCurrentIndex(initialIndex)
27
+ }
28
+ }, [visible, initialIndex])
29
+
30
+ // 通知父组件索引变化
31
+ useEffect(() => {
32
+ onIndexChange?.(currentIndex)
33
+ }, [currentIndex, onIndexChange])
34
+
35
+ // 上一张
36
+ const prev = useCallback(() => {
37
+ if (currentIndex > 0) {
38
+ setCurrentIndex(currentIndex - 1)
39
+ }
40
+ }, [currentIndex])
41
+
42
+ // 下一张
43
+ const next = useCallback(() => {
44
+ if (currentIndex < images.length - 1) {
45
+ setCurrentIndex(currentIndex + 1)
46
+ }
47
+ }, [currentIndex, images.length])
48
+
49
+ // 键盘导航
50
+ useEffect(() => {
51
+ if (!visible) return
52
+ const handleKeyDown = (e: KeyboardEvent) => {
53
+ if (e.key === 'Escape') {
54
+ onClose()
55
+ } else if (e.key === 'ArrowLeft') {
56
+ prev()
57
+ } else if (e.key === 'ArrowRight') {
58
+ next()
59
+ }
60
+ }
61
+ document.addEventListener('keydown', handleKeyDown)
62
+ return () => document.removeEventListener('keydown', handleKeyDown)
63
+ }, [visible, onClose, prev, next])
64
+
65
+ if (!visible) return null
66
+
67
+ const currentSrc = images[currentIndex] || ''
68
+
69
+ return createPortal(
70
+ <div className="image-preview-modal" onClick={onClose}>
71
+ {/* 顶部栏 */}
72
+ <div className="preview-header" onClick={(e) => e.stopPropagation()}>
73
+ {images.length > 1 && (
74
+ <div className="preview-counter">
75
+ {currentIndex + 1} / {images.length}
76
+ </div>
77
+ )}
78
+ <button className="preview-close-btn" title="关闭 (Esc)" onClick={onClose}>
79
+ <Icon icon="lucide:x" width={18} />
80
+ </button>
81
+ </div>
82
+
83
+ {/* 左导航 */}
84
+ {images.length > 1 && (
85
+ <button
86
+ className={`preview-nav-btn preview-nav-prev${currentIndex <= 0 ? ' disabled' : ''}`}
87
+ disabled={currentIndex <= 0}
88
+ onClick={(e) => {
89
+ e.stopPropagation()
90
+ prev()
91
+ }}
92
+ >
93
+ <Icon icon="lucide:chevron-left" width={20} />
94
+ </button>
95
+ )}
96
+
97
+ {/* 主图片区域 */}
98
+ <div className="preview-main" onClick={(e) => e.stopPropagation()}>
99
+ <img src={currentSrc} alt="预览" className="preview-image" />
100
+ </div>
101
+
102
+ {/* 右导航 */}
103
+ {images.length > 1 && (
104
+ <button
105
+ className={`preview-nav-btn preview-nav-next${currentIndex >= images.length - 1 ? ' disabled' : ''}`}
106
+ disabled={currentIndex >= images.length - 1}
107
+ onClick={(e) => {
108
+ e.stopPropagation()
109
+ next()
110
+ }}
111
+ >
112
+ <Icon icon="lucide:chevron-right" width={20} />
113
+ </button>
114
+ )}
115
+ </div>,
116
+ document.body
117
+ )
118
+ }
@@ -0,0 +1,34 @@
1
+ import { forwardRef, useImperativeHandle } from 'react'
2
+ import { Icon } from '@iconify/react'
3
+ import './AtViewStyles.css'
4
+
5
+ export interface AtPlaceholderViewProps {
6
+ query: string
7
+ activeIndex: number
8
+ onSelect: (path: string) => void
9
+ onSetActive: (index: number) => void
10
+ onUpdateCount: (count: number) => void
11
+ }
12
+
13
+ export interface AtPlaceholderViewHandle {
14
+ getActivePath: () => string | null
15
+ confirmActive: () => void
16
+ }
17
+
18
+ export const AtBranchView = forwardRef<AtPlaceholderViewHandle, AtPlaceholderViewProps>((_, ref) => {
19
+ useImperativeHandle(ref, () => ({
20
+ getActivePath: () => null,
21
+ confirmActive: () => {},
22
+ }), [])
23
+
24
+ return (
25
+ <div className="at-placeholder-view">
26
+ <Icon icon="lucide:git-branch" width={32} className="at-placeholder-icon" />
27
+ <div className="at-placeholder-title">分支差异</div>
28
+ <div className="at-placeholder-desc">功能待实现</div>
29
+ <div className="at-placeholder-hint">将支持引用当前分支与主分支的差异</div>
30
+ </div>
31
+ )
32
+ })
33
+
34
+ AtBranchView.displayName = 'AtBranchView'
@@ -0,0 +1,34 @@
1
+ import { forwardRef, useImperativeHandle } from 'react'
2
+ import { Icon } from '@iconify/react'
3
+ import './AtViewStyles.css'
4
+
5
+ export interface AtPlaceholderViewProps {
6
+ query: string
7
+ activeIndex: number
8
+ onSelect: (path: string) => void
9
+ onSetActive: (index: number) => void
10
+ onUpdateCount: (count: number) => void
11
+ }
12
+
13
+ export interface AtPlaceholderViewHandle {
14
+ getActivePath: () => string | null
15
+ confirmActive: () => void
16
+ }
17
+
18
+ export const AtBrowserView = forwardRef<AtPlaceholderViewHandle, AtPlaceholderViewProps>((_, ref) => {
19
+ useImperativeHandle(ref, () => ({
20
+ getActivePath: () => null,
21
+ confirmActive: () => {},
22
+ }), [])
23
+
24
+ return (
25
+ <div className="at-placeholder-view">
26
+ <Icon icon="lucide:globe" width={32} className="at-placeholder-icon" />
27
+ <div className="at-placeholder-title">网页</div>
28
+ <div className="at-placeholder-desc">功能待实现</div>
29
+ <div className="at-placeholder-hint">将支持引用网页内容、URL 等</div>
30
+ </div>
31
+ )
32
+ })
33
+
34
+ AtBrowserView.displayName = 'AtBrowserView'
@@ -0,0 +1,34 @@
1
+ import { forwardRef, useImperativeHandle } from 'react'
2
+ import { Icon } from '@iconify/react'
3
+ import './AtViewStyles.css'
4
+
5
+ export interface AtPlaceholderViewProps {
6
+ query: string
7
+ activeIndex: number
8
+ onSelect: (path: string) => void
9
+ onSetActive: (index: number) => void
10
+ onUpdateCount: (count: number) => void
11
+ }
12
+
13
+ export interface AtPlaceholderViewHandle {
14
+ getActivePath: () => string | null
15
+ confirmActive: () => void
16
+ }
17
+
18
+ export const AtChatsView = forwardRef<AtPlaceholderViewHandle, AtPlaceholderViewProps>((_, ref) => {
19
+ useImperativeHandle(ref, () => ({
20
+ getActivePath: () => null,
21
+ confirmActive: () => {},
22
+ }), [])
23
+
24
+ return (
25
+ <div className="at-placeholder-view">
26
+ <Icon icon="lucide:message-square" width={32} className="at-placeholder-icon" />
27
+ <div className="at-placeholder-title">历史对话</div>
28
+ <div className="at-placeholder-desc">功能待实现</div>
29
+ <div className="at-placeholder-hint">将支持引用之前的对话记录</div>
30
+ </div>
31
+ )
32
+ })
33
+
34
+ AtChatsView.displayName = 'AtChatsView'