work-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 (245) hide show
  1. package/README.md +234 -0
  2. package/app/(admin)/approvals/page.tsx +16 -0
  3. package/app/(admin)/audit/page.tsx +18 -0
  4. package/app/(admin)/layout.tsx +47 -0
  5. package/app/(admin)/scheduled-tasks/page.tsx +17 -0
  6. package/app/(admin)/settings/page.tsx +46 -0
  7. package/app/(admin)/skills/[name]/page.tsx +378 -0
  8. package/app/(admin)/skills/page.tsx +406 -0
  9. package/app/(admin)/statistics/page.tsx +416 -0
  10. package/app/(admin)/tickets/[id]/page.tsx +348 -0
  11. package/app/(admin)/tickets/new/page.tsx +309 -0
  12. package/app/(admin)/tickets/page.tsx +27 -0
  13. package/app/api/audit/route.ts +30 -0
  14. package/app/api/auth/feishu/callback/route.ts +72 -0
  15. package/app/api/auth/feishu/login/route.ts +17 -0
  16. package/app/api/auth/feishu/sso/route.ts +78 -0
  17. package/app/api/auth/login/route.ts +85 -0
  18. package/app/api/auth/oauth/route.ts +168 -0
  19. package/app/api/config/providers/route.ts +105 -0
  20. package/app/api/config/route.ts +115 -0
  21. package/app/api/config/status/route.ts +56 -0
  22. package/app/api/config/test/route.ts +212 -0
  23. package/app/api/documents/[id]/route.ts +88 -0
  24. package/app/api/documents/route.ts +53 -0
  25. package/app/api/health/route.ts +32 -0
  26. package/app/api/knowledge/[id]/route.ts +152 -0
  27. package/app/api/knowledge/from-session/route.ts +27 -0
  28. package/app/api/knowledge/route.ts +100 -0
  29. package/app/api/market/knowledge/[id]/route.ts +92 -0
  30. package/app/api/market/knowledge/route.ts +130 -0
  31. package/app/api/marketplace/skills/[id]/approve/route.ts +68 -0
  32. package/app/api/marketplace/skills/[id]/certify/route.ts +54 -0
  33. package/app/api/marketplace/skills/[id]/install/route.ts +180 -0
  34. package/app/api/marketplace/skills/[id]/promote-to-system/route.ts +219 -0
  35. package/app/api/marketplace/skills/[id]/rate/route.ts +90 -0
  36. package/app/api/marketplace/skills/[id]/ratings/route.ts +55 -0
  37. package/app/api/marketplace/skills/[id]/reject/route.ts +68 -0
  38. package/app/api/marketplace/skills/[id]/route.ts +177 -0
  39. package/app/api/marketplace/skills/route.ts +235 -0
  40. package/app/api/memory/route.ts +40 -0
  41. package/app/api/my/files/[id]/route.ts +52 -0
  42. package/app/api/my/files/route.ts +230 -0
  43. package/app/api/my/knowledge/route.ts +36 -0
  44. package/app/api/pi-chat/route.ts +443 -0
  45. package/app/api/recommend/route.ts +38 -0
  46. package/app/api/scheduled-tasks/[id]/execute/route.ts +132 -0
  47. package/app/api/scheduled-tasks/[id]/route.ts +165 -0
  48. package/app/api/scheduled-tasks/[id]/toggle/route.ts +53 -0
  49. package/app/api/scheduled-tasks/route.ts +101 -0
  50. package/app/api/sessions/[id]/messages/route.ts +212 -0
  51. package/app/api/sessions/route.ts +101 -0
  52. package/app/api/share/file/[id]/route.ts +37 -0
  53. package/app/api/skills/[name]/execute/route.ts +121 -0
  54. package/app/api/skills/[name]/route.ts +167 -0
  55. package/app/api/skills/create/route.ts +65 -0
  56. package/app/api/skills/generate/route.ts +405 -0
  57. package/app/api/skills/installed/route.ts +151 -0
  58. package/app/api/skills/route.ts +174 -0
  59. package/app/api/skills/translate/route.ts +40 -0
  60. package/app/api/skills/user/[name]/route.ts +159 -0
  61. package/app/api/skills/user/route.ts +90 -0
  62. package/app/api/statistics/route.ts +94 -0
  63. package/app/api/task-executions/[id]/route.ts +34 -0
  64. package/app/api/task-executions/route.ts +29 -0
  65. package/app/api/tickets/[id]/approve/route.ts +129 -0
  66. package/app/api/tickets/[id]/execute/route.ts +201 -0
  67. package/app/api/tickets/[id]/route.ts +127 -0
  68. package/app/api/tickets/route.ts +103 -0
  69. package/app/api/user/skills/route.ts +175 -0
  70. package/app/api/users/route.ts +80 -0
  71. package/app/chat/page.tsx +5 -0
  72. package/app/globals.css +84 -0
  73. package/app/h5/layout.tsx +5 -0
  74. package/app/h5/mobile-approvals-page.tsx +167 -0
  75. package/app/h5/mobile-chat-page.tsx +951 -0
  76. package/app/h5/mobile-profile-page.tsx +147 -0
  77. package/app/h5/mobile-tickets-page.tsx +121 -0
  78. package/app/h5/page.tsx +23 -0
  79. package/app/h5/ticket-action-buttons.tsx +80 -0
  80. package/app/layout.tsx +26 -0
  81. package/app/login/page.tsx +318 -0
  82. package/app/market/knowledge/[id]/page.tsx +77 -0
  83. package/app/market/knowledge/page.tsx +358 -0
  84. package/app/market/layout.tsx +29 -0
  85. package/app/market/page.tsx +18 -0
  86. package/app/market/skills/page.tsx +397 -0
  87. package/app/my/files/page.tsx +511 -0
  88. package/app/my/knowledge/[id]/page.tsx +271 -0
  89. package/app/my/knowledge/new/page.tsx +234 -0
  90. package/app/my/knowledge/page.tsx +248 -0
  91. package/app/my/layout.tsx +32 -0
  92. package/app/my/memory/page.tsx +164 -0
  93. package/app/my/page.tsx +18 -0
  94. package/app/my/scheduled-tasks/[id]/edit/page.tsx +290 -0
  95. package/app/my/scheduled-tasks/[id]/executions/page.tsx +275 -0
  96. package/app/my/scheduled-tasks/[id]/page.tsx +284 -0
  97. package/app/my/scheduled-tasks/new/page.tsx +230 -0
  98. package/app/my/scheduled-tasks/page.tsx +27 -0
  99. package/app/my/skills/[name]/page.tsx +320 -0
  100. package/app/my/skills/new/page.tsx +394 -0
  101. package/app/my/skills/page.tsx +303 -0
  102. package/app/page.tsx +2288 -0
  103. package/app/share/[sessionId]/page.tsx +226 -0
  104. package/app/share/file/[id]/page.tsx +140 -0
  105. package/bin/README.md +63 -0
  106. package/bin/generate-api-system +300 -0
  107. package/bin/postinstall.js +95 -0
  108. package/bin/work-agent.js +173 -0
  109. package/components/ai-elements/agent.tsx +142 -0
  110. package/components/ai-elements/artifact.tsx +149 -0
  111. package/components/ai-elements/attachments.tsx +427 -0
  112. package/components/ai-elements/audio-player.tsx +232 -0
  113. package/components/ai-elements/canvas.tsx +26 -0
  114. package/components/ai-elements/chain-of-thought.tsx +223 -0
  115. package/components/ai-elements/checkpoint.tsx +72 -0
  116. package/components/ai-elements/code-block.tsx +555 -0
  117. package/components/ai-elements/commit.tsx +449 -0
  118. package/components/ai-elements/confirmation.tsx +173 -0
  119. package/components/ai-elements/connection.tsx +28 -0
  120. package/components/ai-elements/context.tsx +410 -0
  121. package/components/ai-elements/controls.tsx +19 -0
  122. package/components/ai-elements/conversation.tsx +167 -0
  123. package/components/ai-elements/edge.tsx +144 -0
  124. package/components/ai-elements/environment-variables.tsx +325 -0
  125. package/components/ai-elements/file-tree.tsx +298 -0
  126. package/components/ai-elements/image.tsx +25 -0
  127. package/components/ai-elements/inline-citation.tsx +294 -0
  128. package/components/ai-elements/jsx-preview.tsx +250 -0
  129. package/components/ai-elements/message.tsx +367 -0
  130. package/components/ai-elements/mic-selector.tsx +372 -0
  131. package/components/ai-elements/model-selector.tsx +214 -0
  132. package/components/ai-elements/node.tsx +72 -0
  133. package/components/ai-elements/open-in-chat.tsx +367 -0
  134. package/components/ai-elements/package-info.tsx +235 -0
  135. package/components/ai-elements/panel.tsx +16 -0
  136. package/components/ai-elements/persona.tsx +280 -0
  137. package/components/ai-elements/plan.tsx +144 -0
  138. package/components/ai-elements/prompt-input.tsx +1341 -0
  139. package/components/ai-elements/queue.tsx +275 -0
  140. package/components/ai-elements/reasoning.tsx +355 -0
  141. package/components/ai-elements/sandbox.tsx +133 -0
  142. package/components/ai-elements/schema-display.tsx +473 -0
  143. package/components/ai-elements/shimmer.tsx +78 -0
  144. package/components/ai-elements/snippet.tsx +141 -0
  145. package/components/ai-elements/sources.tsx +78 -0
  146. package/components/ai-elements/speech-input.tsx +324 -0
  147. package/components/ai-elements/stack-trace.tsx +531 -0
  148. package/components/ai-elements/suggestion.tsx +58 -0
  149. package/components/ai-elements/task.tsx +88 -0
  150. package/components/ai-elements/terminal.tsx +277 -0
  151. package/components/ai-elements/test-results.tsx +497 -0
  152. package/components/ai-elements/tool.tsx +174 -0
  153. package/components/ai-elements/toolbar.tsx +17 -0
  154. package/components/ai-elements/transcription.tsx +126 -0
  155. package/components/ai-elements/voice-selector.tsx +525 -0
  156. package/components/ai-elements/web-preview.tsx +282 -0
  157. package/components/audit-log-list.tsx +114 -0
  158. package/components/chat/EmptyPreviewState.tsx +12 -0
  159. package/components/chat/KnowledgePickerDialog.tsx +464 -0
  160. package/components/chat/KnowledgePreview.tsx +70 -0
  161. package/components/chat/KnowledgePreviewPanel.tsx +86 -0
  162. package/components/chat/MentionInput.tsx +309 -0
  163. package/components/chat/OrganizeDialog.tsx +258 -0
  164. package/components/chat/RecommendationBanner.tsx +94 -0
  165. package/components/chat/SaveToKnowledgeDialog.tsx +193 -0
  166. package/components/chat/SkillSelector.tsx +305 -0
  167. package/components/chat/SkillSwitcher.tsx +163 -0
  168. package/components/client-layout.tsx +15 -0
  169. package/components/knowledge/KnowledgeMetadataPanel.tsx +293 -0
  170. package/components/layout-wrapper.tsx +18 -0
  171. package/components/mobile-layout.tsx +62 -0
  172. package/components/scheduled-task-list.tsx +356 -0
  173. package/components/setup-guide.tsx +484 -0
  174. package/components/sub-nav.tsx +54 -0
  175. package/components/ticket-detail-content.tsx +383 -0
  176. package/components/ticket-list.tsx +366 -0
  177. package/components/top-nav.tsx +132 -0
  178. package/components/ui/accordion.tsx +58 -0
  179. package/components/ui/alert.tsx +59 -0
  180. package/components/ui/avatar.tsx +50 -0
  181. package/components/ui/badge.tsx +36 -0
  182. package/components/ui/button-group.tsx +83 -0
  183. package/components/ui/button.tsx +57 -0
  184. package/components/ui/card.tsx +91 -0
  185. package/components/ui/carousel.tsx +262 -0
  186. package/components/ui/collapsible.tsx +11 -0
  187. package/components/ui/command.tsx +153 -0
  188. package/components/ui/dialog.tsx +122 -0
  189. package/components/ui/dropdown-menu.tsx +200 -0
  190. package/components/ui/hover-card.tsx +29 -0
  191. package/components/ui/input-group.tsx +170 -0
  192. package/components/ui/input.tsx +22 -0
  193. package/components/ui/label.tsx +26 -0
  194. package/components/ui/popover.tsx +31 -0
  195. package/components/ui/progress.tsx +28 -0
  196. package/components/ui/scroll-area.tsx +48 -0
  197. package/components/ui/select.tsx +174 -0
  198. package/components/ui/separator.tsx +31 -0
  199. package/components/ui/spinner.tsx +16 -0
  200. package/components/ui/switch.tsx +29 -0
  201. package/components/ui/table.tsx +120 -0
  202. package/components/ui/tabs.tsx +55 -0
  203. package/components/ui/textarea.tsx +22 -0
  204. package/components/ui/tooltip.tsx +30 -0
  205. package/components/welcome-guide.tsx +182 -0
  206. package/components.json +24 -0
  207. package/lib/command-parser.ts +331 -0
  208. package/lib/dangerous-commands.ts +672 -0
  209. package/lib/db.ts +2250 -0
  210. package/lib/feishu-auth.ts +135 -0
  211. package/lib/file-storage.ts +306 -0
  212. package/lib/file-tool.ts +583 -0
  213. package/lib/knowledge-tool.ts +152 -0
  214. package/lib/knowledge-types.ts +66 -0
  215. package/lib/market-client.ts +313 -0
  216. package/lib/market-db.ts +736 -0
  217. package/lib/market-types.ts +51 -0
  218. package/lib/memory-tool.ts +211 -0
  219. package/lib/memory.ts +197 -0
  220. package/lib/pi-config.ts +436 -0
  221. package/lib/pi-session.ts +799 -0
  222. package/lib/pinyin.ts +13 -0
  223. package/lib/recommendation.ts +227 -0
  224. package/lib/risk-estimator.ts +350 -0
  225. package/lib/scheduled-task-tool.ts +184 -0
  226. package/lib/scheduler-init.ts +43 -0
  227. package/lib/scheduler.ts +416 -0
  228. package/lib/secure-bash-tool.ts +413 -0
  229. package/lib/skill-engine.ts +396 -0
  230. package/lib/skill-generator.ts +269 -0
  231. package/lib/skill-loader.ts +234 -0
  232. package/lib/skill-tool.ts +188 -0
  233. package/lib/skill-types.ts +82 -0
  234. package/lib/skills-init.ts +58 -0
  235. package/lib/ticket-tool.ts +246 -0
  236. package/lib/user-skill-types.ts +30 -0
  237. package/lib/user-skills.ts +362 -0
  238. package/lib/utils.ts +6 -0
  239. package/lib/workflow.ts +154 -0
  240. package/lib/zip-tool.ts +191 -0
  241. package/next.config.js +8 -0
  242. package/package.json +106 -0
  243. package/public/.gitkeep +1 -0
  244. package/public/icon.svg +1 -0
  245. package/tsconfig.json +42 -0
@@ -0,0 +1,464 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
4
+ import { Search, X, ExternalLink, BookOpen, Loader2 } from 'lucide-react';
5
+ import Link from 'next/link';
6
+ import { cn } from '@/lib/utils';
7
+ import { MessageResponse } from '@/components/ai-elements/message';
8
+ import { KnowledgePreviewPanel } from './KnowledgePreviewPanel';
9
+ import { EmptyPreviewState } from './EmptyPreviewState';
10
+
11
+ interface KnowledgeItem {
12
+ id: string;
13
+ title: string;
14
+ content?: string;
15
+ category?: string;
16
+ createdAt: string;
17
+ updatedAt: string;
18
+ author?: string;
19
+ marketStatus?: string;
20
+ }
21
+
22
+ interface KnowledgePickerDialogProps {
23
+ open: boolean;
24
+ onOpenChange: (open: boolean) => void;
25
+ onSelect: (knowledge: KnowledgeItem) => void;
26
+ }
27
+
28
+ const PAGE_SIZE = 10;
29
+
30
+ function debounce<T extends (...args: any[]) => any>(func: T, wait: number): T {
31
+ let timeout: NodeJS.Timeout | null = null;
32
+ return ((...args: Parameters<T>) => {
33
+ if (timeout) clearTimeout(timeout);
34
+ timeout = setTimeout(() => func(...args), wait);
35
+ }) as T;
36
+ }
37
+
38
+ export function KnowledgePickerDialog({ open, onOpenChange, onSelect }: KnowledgePickerDialogProps) {
39
+ const [activeTab, setActiveTab] = useState<'my' | 'market'>('my');
40
+ const [search, setSearch] = useState('');
41
+ const [knowledge, setKnowledge] = useState<KnowledgeItem[]>([]);
42
+ const [total, setTotal] = useState(0);
43
+ const [page, setPage] = useState(1);
44
+ const [loading, setLoading] = useState(false);
45
+ const searchInputRef = useRef<HTMLInputElement>(null);
46
+
47
+ const [selectedKnowledge, setSelectedKnowledge] = useState<KnowledgeItem | null>(null);
48
+ const [previewContent, setPreviewContent] = useState<string>('');
49
+ const [previewLoading, setPreviewLoading] = useState(false);
50
+ const [contentCache, setContentCache] = useState<Map<string, string>>(new Map());
51
+ const [isMobile, setIsMobile] = useState(false);
52
+
53
+ const fetchKnowledge = useCallback(async () => {
54
+ setLoading(true);
55
+ try {
56
+ const offset = (page - 1) * PAGE_SIZE;
57
+ const userId = localStorage.getItem('userId');
58
+
59
+ let url = '';
60
+ if (activeTab === 'my') {
61
+ url = `/api/knowledge?author=${encodeURIComponent(userId || '')}&limit=${PAGE_SIZE}&offset=${offset}`;
62
+ if (search) {
63
+ url += `&search=${encodeURIComponent(search)}`;
64
+ }
65
+ } else {
66
+ url = `/api/market/knowledge?limit=${PAGE_SIZE}&offset=${offset}`;
67
+ if (search) {
68
+ url += `&search=${encodeURIComponent(search)}`;
69
+ }
70
+ }
71
+
72
+ const response = await fetch(url);
73
+ const data = await response.json();
74
+
75
+ if (activeTab === 'my') {
76
+ setKnowledge(data.documents || []);
77
+ setTotal(data.total || 0);
78
+ } else {
79
+ setKnowledge(data.documents || []);
80
+ setTotal(data.total || 0);
81
+ }
82
+ } catch (error) {
83
+ console.error('Failed to fetch knowledge:', error);
84
+ } finally {
85
+ setLoading(false);
86
+ }
87
+ }, [activeTab, search, page]);
88
+
89
+ useEffect(() => {
90
+ if (open) {
91
+ setPage(1);
92
+ setSearch('');
93
+ setSelectedKnowledge(null);
94
+ setPreviewContent('');
95
+ fetchKnowledge();
96
+ }
97
+ }, [open, activeTab]);
98
+
99
+ useEffect(() => {
100
+ fetchKnowledge();
101
+ }, [page]);
102
+
103
+ useEffect(() => {
104
+ if (open && searchInputRef.current) {
105
+ setTimeout(() => searchInputRef.current?.focus(), 100);
106
+ }
107
+ }, [open, search]);
108
+
109
+ const totalPages = Math.ceil(total / PAGE_SIZE);
110
+
111
+ useEffect(() => {
112
+ const checkMobile = () => setIsMobile(window.innerWidth < 768);
113
+ checkMobile();
114
+ window.addEventListener('resize', checkMobile);
115
+ return () => window.removeEventListener('resize', checkMobile);
116
+ }, []);
117
+
118
+ const loadPreviewContent = useCallback(async (id: string) => {
119
+ if (contentCache.has(id)) {
120
+ setPreviewContent(contentCache.get(id)!);
121
+ return;
122
+ }
123
+
124
+ setPreviewLoading(true);
125
+ try {
126
+ const response = await fetch(`/api/knowledge/${id}`);
127
+ const data = await response.json();
128
+ const content = data.document?.content || '';
129
+
130
+ setContentCache(prev => new Map(prev).set(id, content));
131
+ setPreviewContent(content);
132
+ } catch (error) {
133
+ console.error('Failed to load preview:', error);
134
+ setPreviewContent('');
135
+ } finally {
136
+ setPreviewLoading(false);
137
+ }
138
+ }, [contentCache]);
139
+
140
+ const handlePreview = useCallback((item: KnowledgeItem) => {
141
+ setSelectedKnowledge(item);
142
+ loadPreviewContent(item.id);
143
+ }, [loadPreviewContent]);
144
+
145
+ const handleSelect = useCallback((item: KnowledgeItem) => {
146
+ onSelect(item);
147
+ onOpenChange(false);
148
+ }, [onSelect, onOpenChange]);
149
+
150
+ const debouncedSearch = useMemo(
151
+ () => debounce((value: string) => {
152
+ setPage(1);
153
+ fetchKnowledge();
154
+ }, 300),
155
+ [fetchKnowledge]
156
+ );
157
+
158
+ useEffect(() => {
159
+ if (!open) return;
160
+
161
+ const handleKeyDown = (e: KeyboardEvent) => {
162
+ if (e.key === 'Escape') {
163
+ onOpenChange(false);
164
+ return;
165
+ }
166
+
167
+ if (knowledge.length === 0) return;
168
+
169
+ const currentIndex = selectedKnowledge
170
+ ? knowledge.findIndex(k => k.id === selectedKnowledge.id)
171
+ : -1;
172
+
173
+ if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
174
+ e.preventDefault();
175
+ let newIndex: number;
176
+
177
+ if (e.key === 'ArrowDown') {
178
+ newIndex = currentIndex < knowledge.length - 1 ? currentIndex + 1 : 0;
179
+ } else {
180
+ newIndex = currentIndex > 0 ? currentIndex - 1 : knowledge.length - 1;
181
+ }
182
+
183
+ handlePreview(knowledge[newIndex]);
184
+ } else if (e.key === 'Enter' && selectedKnowledge) {
185
+ e.preventDefault();
186
+ if (e.ctrlKey || e.metaKey) {
187
+ handleSelect(selectedKnowledge);
188
+ } else {
189
+ handlePreview(selectedKnowledge);
190
+ }
191
+ }
192
+ };
193
+
194
+ window.addEventListener('keydown', handleKeyDown);
195
+ return () => window.removeEventListener('keydown', handleKeyDown);
196
+ }, [open, knowledge, selectedKnowledge, onOpenChange, handlePreview, handleSelect]);
197
+
198
+ return (
199
+ <>
200
+ {open && (
201
+ <div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
202
+ <div className={cn(
203
+ "bg-white rounded-xl shadow-2xl w-full flex flex-col overflow-hidden",
204
+ isMobile ? "max-w-2xl max-h-[90vh]" : "max-w-5xl max-h-[80vh]"
205
+ )}>
206
+ {/* Header */}
207
+ <div className="px-4 py-3 border-b border-gray-200 flex items-center justify-between">
208
+ <h2 className="text-lg font-semibold text-gray-900 flex items-center gap-2">
209
+ <BookOpen className="w-5 h-5 text-amber-600" />
210
+ 知识库
211
+ </h2>
212
+ <button
213
+ onClick={() => onOpenChange(false)}
214
+ className="p-1 text-gray-400 hover:text-gray-600 rounded"
215
+ >
216
+ <X className="w-5 h-5" />
217
+ </button>
218
+ </div>
219
+
220
+ {/* Tabs & Search */}
221
+ <div className="px-4 py-3 border-b border-gray-200 space-y-3">
222
+ <div className="flex gap-2">
223
+ <button
224
+ onClick={() => setActiveTab('my')}
225
+ className={`px-4 py-1.5 rounded-lg text-sm font-medium transition-colors ${
226
+ activeTab === 'my'
227
+ ? 'bg-amber-100 text-amber-700'
228
+ : 'text-gray-600 hover:bg-gray-100'
229
+ }`}
230
+ >
231
+ 我的知识
232
+ </button>
233
+ <button
234
+ onClick={() => setActiveTab('market')}
235
+ className={`px-4 py-1.5 rounded-lg text-sm font-medium transition-colors ${
236
+ activeTab === 'market'
237
+ ? 'bg-amber-100 text-amber-700'
238
+ : 'text-gray-600 hover:bg-gray-100'
239
+ }`}
240
+ >
241
+ 市场知识
242
+ </button>
243
+ </div>
244
+
245
+ <div className="relative">
246
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
247
+ <input
248
+ ref={searchInputRef}
249
+ type="text"
250
+ value={search}
251
+ onChange={(e) => {
252
+ setSearch(e.target.value);
253
+ setSelectedKnowledge(null);
254
+ setPreviewContent('');
255
+ debouncedSearch(e.target.value);
256
+ }}
257
+ placeholder="搜索知识..."
258
+ className="w-full pl-10 pr-4 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-amber-500"
259
+ />
260
+ </div>
261
+ </div>
262
+
263
+ {/* Main Content - 桌面端左右分栏,移动端手风琴 */}
264
+ {isMobile ? (
265
+ <div className="flex-1 overflow-y-auto min-h-0">
266
+ {loading ? (
267
+ <div className="flex items-center justify-center py-12">
268
+ <Loader2 className="w-6 h-6 text-amber-600 animate-spin" />
269
+ </div>
270
+ ) : knowledge.length === 0 ? (
271
+ <div className="flex flex-col items-center justify-center py-12 text-gray-500">
272
+ <BookOpen className="w-12 h-12 mb-3 text-gray-300" />
273
+ <p className="text-sm">暂无知识</p>
274
+ {activeTab === 'my' && (
275
+ <Link
276
+ href="/my/knowledge/new"
277
+ className="mt-2 text-sm text-amber-600 hover:text-amber-700"
278
+ onClick={() => onOpenChange(false)}
279
+ >
280
+ 创建第一条知识
281
+ </Link>
282
+ )}
283
+ </div>
284
+ ) : (
285
+ <div className="divide-y divide-gray-100">
286
+ {knowledge.map((item) => (
287
+ <div key={item.id}>
288
+ <div
289
+ className={cn(
290
+ "px-4 py-3 transition-colors cursor-pointer",
291
+ selectedKnowledge?.id === item.id
292
+ ? "bg-amber-50"
293
+ : "hover:bg-gray-50"
294
+ )}
295
+ onClick={() => {
296
+ if (selectedKnowledge?.id === item.id) {
297
+ setSelectedKnowledge(null);
298
+ setPreviewContent('');
299
+ } else {
300
+ handlePreview(item);
301
+ }
302
+ }}
303
+ >
304
+ <div className="flex items-start justify-between gap-3">
305
+ <div className="min-w-0 flex-1">
306
+ <h3 className="font-medium text-gray-900 truncate">
307
+ {item.title}
308
+ </h3>
309
+ <div className="flex items-center gap-2 mt-1 text-xs text-gray-500 flex-wrap">
310
+ {item.category && (
311
+ <span className="px-2 py-0.5 bg-gray-100 rounded">
312
+ {item.category}
313
+ </span>
314
+ )}
315
+ <span>
316
+ {new Date(item.updatedAt).toLocaleDateString('zh-CN')}
317
+ </span>
318
+ {activeTab === 'market' && item.author && (
319
+ <span>· {item.author}</span>
320
+ )}
321
+ </div>
322
+ </div>
323
+ <button
324
+ onClick={(e) => {
325
+ e.stopPropagation();
326
+ handleSelect(item);
327
+ }}
328
+ className="px-3 py-1 text-xs font-medium bg-amber-100 text-amber-700 rounded-lg hover:bg-amber-200 transition-colors shrink-0"
329
+ >
330
+ 引用
331
+ </button>
332
+ </div>
333
+ </div>
334
+
335
+ {selectedKnowledge?.id === item.id && (
336
+ <div className="px-4 py-3 bg-gray-50 border-t border-gray-200">
337
+ {previewLoading ? (
338
+ <div className="space-y-3 animate-pulse">
339
+ <div className="h-4 bg-gray-200 rounded w-3/4"></div>
340
+ <div className="h-4 bg-gray-200 rounded w-full"></div>
341
+ <div className="h-4 bg-gray-200 rounded w-5/6"></div>
342
+ </div>
343
+ ) : (
344
+ <div className="prose prose-sm max-w-none">
345
+ <MessageResponse>{previewContent}</MessageResponse>
346
+ </div>
347
+ )}
348
+ </div>
349
+ )}
350
+ </div>
351
+ ))}
352
+ </div>
353
+ )}
354
+ </div>
355
+ ) : (
356
+ <div className="flex flex-1 overflow-hidden min-h-0">
357
+ <div className="w-2/5 border-r border-gray-200 overflow-y-auto">
358
+ {loading ? (
359
+ <div className="flex items-center justify-center py-12">
360
+ <Loader2 className="w-6 h-6 text-amber-600 animate-spin" />
361
+ </div>
362
+ ) : knowledge.length === 0 ? (
363
+ <div className="flex flex-col items-center justify-center py-12 text-gray-500">
364
+ <BookOpen className="w-12 h-12 mb-3 text-gray-300" />
365
+ <p className="text-sm">暂无知识</p>
366
+ {activeTab === 'my' && (
367
+ <Link
368
+ href="/my/knowledge/new"
369
+ className="mt-2 text-sm text-amber-600 hover:text-amber-700"
370
+ onClick={() => onOpenChange(false)}
371
+ >
372
+ 创建第一条知识
373
+ </Link>
374
+ )}
375
+ </div>
376
+ ) : (
377
+ <div className="divide-y divide-gray-100">
378
+ {knowledge.map((item) => (
379
+ <div
380
+ key={item.id}
381
+ className={cn(
382
+ "px-4 py-3 transition-colors cursor-pointer",
383
+ selectedKnowledge?.id === item.id
384
+ ? "bg-amber-50 border-l-2 border-amber-600"
385
+ : "hover:bg-gray-50 border-l-2 border-transparent"
386
+ )}
387
+ onClick={() => handlePreview(item)}
388
+ onDoubleClick={() => handleSelect(item)}
389
+ >
390
+ <div className="flex items-start justify-between gap-3">
391
+ <div className="min-w-0 flex-1">
392
+ <h3 className="font-medium text-gray-900 truncate">
393
+ {item.title}
394
+ </h3>
395
+ <div className="flex items-center gap-2 mt-1 text-xs text-gray-500 flex-wrap">
396
+ {item.category && (
397
+ <span className="px-2 py-0.5 bg-gray-100 rounded">
398
+ {item.category}
399
+ </span>
400
+ )}
401
+ <span>
402
+ {new Date(item.updatedAt).toLocaleDateString('zh-CN')}
403
+ </span>
404
+ {activeTab === 'market' && item.author && (
405
+ <span>· {item.author}</span>
406
+ )}
407
+ </div>
408
+ </div>
409
+ </div>
410
+ </div>
411
+ ))}
412
+ </div>
413
+ )}
414
+ </div>
415
+
416
+ <div className="w-3/5 overflow-hidden bg-gray-50">
417
+ {selectedKnowledge ? (
418
+ <KnowledgePreviewPanel
419
+ knowledge={selectedKnowledge}
420
+ content={previewContent}
421
+ loading={previewLoading}
422
+ onCite={handleSelect}
423
+ activeTab={activeTab}
424
+ />
425
+ ) : (
426
+ <EmptyPreviewState />
427
+ )}
428
+ </div>
429
+ </div>
430
+ )}
431
+
432
+ {/* Pagination */}
433
+ {totalPages > 1 && (
434
+ <div className="px-4 py-3 border-t border-gray-200 flex items-center justify-between">
435
+ <span className="text-xs text-gray-500">
436
+ 共 {total} 条知识
437
+ </span>
438
+ <div className="flex items-center gap-1">
439
+ <button
440
+ onClick={() => setPage(p => Math.max(1, p - 1))}
441
+ disabled={page === 1}
442
+ className="px-2 py-1 text-xs text-gray-600 hover:bg-gray-100 rounded disabled:opacity-50 disabled:cursor-not-allowed"
443
+ >
444
+ 上一页
445
+ </button>
446
+ <span className="px-2 py-1 text-xs text-gray-600">
447
+ {page} / {totalPages}
448
+ </span>
449
+ <button
450
+ onClick={() => setPage(p => Math.min(totalPages, p + 1))}
451
+ disabled={page === totalPages}
452
+ className="px-2 py-1 text-xs text-gray-600 hover:bg-gray-100 rounded disabled:opacity-50 disabled:cursor-not-allowed"
453
+ >
454
+ 下一页
455
+ </button>
456
+ </div>
457
+ </div>
458
+ )}
459
+ </div>
460
+ </div>
461
+ )}
462
+ </>
463
+ );
464
+ }
@@ -0,0 +1,70 @@
1
+ 'use client';
2
+
3
+ import dynamic from 'next/dynamic';
4
+ import { KNOWLEDGE_TYPE_LABELS } from '@/lib/knowledge-types';
5
+
6
+ const MDEditor = dynamic(
7
+ () => import('@uiw/react-md-editor'),
8
+ { ssr: false }
9
+ );
10
+
11
+ interface KnowledgePreviewProps {
12
+ title: string;
13
+ content: string;
14
+ category?: string;
15
+ tags?: string[];
16
+ author: string;
17
+ updatedAt: string;
18
+ type?: string;
19
+ }
20
+
21
+ export function KnowledgePreview({
22
+ title,
23
+ content,
24
+ category,
25
+ tags,
26
+ author,
27
+ updatedAt,
28
+ type,
29
+ }: KnowledgePreviewProps) {
30
+ return (
31
+ <div className="bg-white rounded-lg shadow-sm border p-6">
32
+ <div className="mb-6">
33
+ <h1 className="text-2xl font-bold text-gray-900 mb-2">{title}</h1>
34
+ <div className="flex items-center gap-3 text-sm text-gray-500">
35
+ {category && (
36
+ <span className="px-2 py-0.5 bg-gray-100 rounded text-xs">
37
+ {category}
38
+ </span>
39
+ )}
40
+ {type && KNOWLEDGE_TYPE_LABELS[type as keyof typeof KNOWLEDGE_TYPE_LABELS] && (
41
+ <span className="px-2 py-0.5 bg-blue-100 text-blue-700 rounded text-xs">
42
+ {KNOWLEDGE_TYPE_LABELS[type as keyof typeof KNOWLEDGE_TYPE_LABELS]}
43
+ </span>
44
+ )}
45
+ <span>作者: {author}</span>
46
+ <span>更新于: {new Date(updatedAt).toLocaleDateString('zh-CN')}</span>
47
+ </div>
48
+ </div>
49
+
50
+ {tags && tags.length > 0 && (
51
+ <div className="flex flex-wrap gap-2 mb-6">
52
+ {tags.map((tag, index) => (
53
+ <span key={index} className="px-2 py-0.5 bg-gray-100 text-gray-600 rounded text-xs">
54
+ {tag}
55
+ </span>
56
+ ))}
57
+ </div>
58
+ )}
59
+
60
+ <div className="prose max-w-none" data-color-mode="light" style={{ minHeight: '60vh' }}>
61
+ <MDEditor
62
+ value={content}
63
+ preview="preview"
64
+ hideToolbar
65
+ height={600}
66
+ />
67
+ </div>
68
+ </div>
69
+ );
70
+ }
@@ -0,0 +1,86 @@
1
+ 'use client';
2
+
3
+ import { ExternalLink } from 'lucide-react';
4
+ import Link from 'next/link';
5
+ import { MessageResponse } from '@/components/ai-elements/message';
6
+
7
+ interface KnowledgeItem {
8
+ id: string;
9
+ title: string;
10
+ content?: string;
11
+ category?: string;
12
+ createdAt: string;
13
+ updatedAt: string;
14
+ author?: string;
15
+ marketStatus?: string;
16
+ }
17
+
18
+ interface KnowledgePreviewPanelProps {
19
+ knowledge: KnowledgeItem;
20
+ content: string;
21
+ loading: boolean;
22
+ onCite: (knowledge: KnowledgeItem) => void;
23
+ activeTab: 'my' | 'market';
24
+ }
25
+
26
+ export function KnowledgePreviewPanel({
27
+ knowledge,
28
+ content,
29
+ loading,
30
+ onCite,
31
+ activeTab
32
+ }: KnowledgePreviewPanelProps) {
33
+ return (
34
+ <div className="h-full flex flex-col bg-white">
35
+ <div className="px-6 py-4 border-b border-gray-200">
36
+ <h3 className="text-lg font-semibold text-gray-900">{knowledge.title}</h3>
37
+ <div className="flex items-center gap-2 mt-2 text-xs text-gray-500 flex-wrap">
38
+ {knowledge.category && (
39
+ <span className="px-2 py-0.5 bg-amber-100 text-amber-700 rounded">
40
+ {knowledge.category}
41
+ </span>
42
+ )}
43
+ <span>更新于 {new Date(knowledge.updatedAt).toLocaleDateString('zh-CN')}</span>
44
+ {activeTab === 'market' && knowledge.author && (
45
+ <span>· 作者: {knowledge.author}</span>
46
+ )}
47
+ </div>
48
+ </div>
49
+
50
+ <div className="flex-1 overflow-y-auto px-6 py-4 min-h-0">
51
+ {loading ? (
52
+ <div className="space-y-3 animate-pulse">
53
+ <div className="h-4 bg-gray-200 rounded w-3/4"></div>
54
+ <div className="h-4 bg-gray-200 rounded w-full"></div>
55
+ <div className="h-4 bg-gray-200 rounded w-5/6"></div>
56
+ <div className="h-4 bg-gray-200 rounded w-full"></div>
57
+ <div className="h-4 bg-gray-200 rounded w-2/3"></div>
58
+ <div className="h-4 bg-gray-200 rounded w-full"></div>
59
+ <div className="h-4 bg-gray-200 rounded w-4/5"></div>
60
+ </div>
61
+ ) : (
62
+ <div className="prose prose-sm max-w-none">
63
+ <MessageResponse>{content}</MessageResponse>
64
+ </div>
65
+ )}
66
+ </div>
67
+
68
+ <div className="px-6 py-3 border-t border-gray-200 bg-gray-50 flex items-center gap-2">
69
+ <button
70
+ onClick={() => onCite(knowledge)}
71
+ className="px-4 py-2 bg-amber-600 text-white rounded-lg hover:bg-amber-700 transition-colors text-sm font-medium"
72
+ >
73
+ 引用此知识
74
+ </button>
75
+ <Link
76
+ href={activeTab === 'my' ? `/my/knowledge/${knowledge.id}` : `/market/knowledge/${knowledge.id}`}
77
+ target="_blank"
78
+ className="px-4 py-2 text-gray-600 hover:text-gray-800 rounded-lg hover:bg-gray-100 transition-colors text-sm font-medium flex items-center gap-1"
79
+ >
80
+ <ExternalLink className="w-4 h-4" />
81
+ 在新窗口打开
82
+ </Link>
83
+ </div>
84
+ </div>
85
+ );
86
+ }