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,271 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useCallback } from 'react';
4
+ import { useRouter, useParams } from 'next/navigation';
5
+ import dynamic from 'next/dynamic';
6
+ import { ArrowLeft, Eye, EyeOff, FileText } from 'lucide-react';
7
+ import { Button } from '@/components/ui/button';
8
+ import { KnowledgeMetadataPanel } from '@/components/knowledge/KnowledgeMetadataPanel';
9
+ import { KnowledgePreview } from '@/components/chat/KnowledgePreview';
10
+ import type { KnowledgeType, KnowledgeSourceType } from '@/lib/db';
11
+ import { KNOWLEDGE_SOURCE_LABELS } from '@/lib/knowledge-types';
12
+
13
+ const MDEditor = dynamic(
14
+ () => import('@uiw/react-md-editor'),
15
+ { ssr: false }
16
+ );
17
+
18
+ interface KnowledgeItem {
19
+ id: string;
20
+ title: string;
21
+ content: string;
22
+ category?: string;
23
+ tags?: string[];
24
+ author: string;
25
+ createdAt: Date;
26
+ updatedAt: Date;
27
+ type?: KnowledgeType;
28
+ sourceType?: KnowledgeSourceType;
29
+ sessionId?: string;
30
+ isActive?: boolean;
31
+ }
32
+
33
+ export default function KnowledgeDetailPage() {
34
+ const router = useRouter();
35
+ const params = useParams();
36
+ const knowledgeId = params.id as string;
37
+
38
+ const [knowledge, setKnowledge] = useState<KnowledgeItem | null>(null);
39
+ const [title, setTitle] = useState('');
40
+ const [content, setContent] = useState('');
41
+ const [category, setCategory] = useState('none');
42
+ const [tags, setTags] = useState<string[]>([]);
43
+ const [type, setType] = useState<KnowledgeType>('documentation');
44
+ const [isActive, setIsActive] = useState(true);
45
+ const [previewMode, setPreviewMode] = useState<'edit' | 'preview'>('edit');
46
+ const [saving, setSaving] = useState(false);
47
+ const [loading, setLoading] = useState(true);
48
+ const [lastSaved, setLastSaved] = useState<Date | null>(null);
49
+
50
+ const [categories, setCategories] = useState<string[]>([]);
51
+
52
+ useEffect(() => {
53
+ loadKnowledge();
54
+ loadCategories();
55
+ }, [knowledgeId]);
56
+
57
+ useEffect(() => {
58
+ const handleKeyDown = (e: KeyboardEvent) => {
59
+ if ((e.ctrlKey || e.metaKey) && e.key === 's') {
60
+ e.preventDefault();
61
+ handleSave();
62
+ }
63
+ if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
64
+ e.preventDefault();
65
+ setPreviewMode(p => p === 'edit' ? 'preview' : 'edit');
66
+ }
67
+ };
68
+ window.addEventListener('keydown', handleKeyDown);
69
+ return () => window.removeEventListener('keydown', handleKeyDown);
70
+ }, [title, content, type, category, tags, isActive]);
71
+
72
+ const loadKnowledge = async () => {
73
+ setLoading(true);
74
+ try {
75
+ const response = await fetch(`/api/knowledge/${knowledgeId}`);
76
+ if (response.ok) {
77
+ const data = await response.json();
78
+ const item = data.document;
79
+ setKnowledge(item);
80
+ setTitle(item.title);
81
+ setContent(item.content || '');
82
+ setCategory(item.category || 'none');
83
+ setTags(item.tags || []);
84
+ setType(item.type || 'documentation');
85
+ setIsActive(item.isActive !== false);
86
+ } else {
87
+ alert('知识不存在');
88
+ router.push('/my/knowledge');
89
+ }
90
+ } catch (error) {
91
+ console.error('Failed to load knowledge:', error);
92
+ router.push('/my/knowledge');
93
+ } finally {
94
+ setLoading(false);
95
+ }
96
+ };
97
+
98
+ const loadCategories = async () => {
99
+ try {
100
+ const response = await fetch('/api/knowledge');
101
+ const data = await response.json();
102
+ const cats = Array.from(new Set(
103
+ data.documents
104
+ .map((d: KnowledgeItem) => d.category)
105
+ .filter(Boolean)
106
+ )) as string[];
107
+ setCategories(cats);
108
+ } catch (error) {
109
+ console.error('Failed to load categories:', error);
110
+ }
111
+ };
112
+
113
+ const handleSave = async () => {
114
+ if (!title.trim()) {
115
+ alert('请输入知识标题');
116
+ return;
117
+ }
118
+
119
+ setSaving(true);
120
+ try {
121
+ const response = await fetch(`/api/knowledge/${knowledgeId}`, {
122
+ method: 'PUT',
123
+ headers: { 'Content-Type': 'application/json' },
124
+ body: JSON.stringify({
125
+ title: title.trim(),
126
+ content,
127
+ category: category && category !== 'none' ? category : undefined,
128
+ tags: tags.length > 0 ? tags : undefined,
129
+ type,
130
+ isActive,
131
+ }),
132
+ });
133
+
134
+ if (response.ok) {
135
+ const data = await response.json();
136
+ setKnowledge(data.document);
137
+ setLastSaved(new Date());
138
+ } else {
139
+ const error = await response.json();
140
+ alert(error.error || '保存失败');
141
+ }
142
+ } catch (error) {
143
+ console.error('Failed to save knowledge:', error);
144
+ alert('保存失败');
145
+ } finally {
146
+ setSaving(false);
147
+ }
148
+ };
149
+
150
+ const handleDelete = async () => {
151
+ try {
152
+ const response = await fetch(`/api/knowledge/${knowledgeId}`, {
153
+ method: 'DELETE',
154
+ });
155
+
156
+ if (response.ok) {
157
+ router.push('/my/knowledge');
158
+ } else {
159
+ alert('删除失败');
160
+ }
161
+ } catch (error) {
162
+ console.error('Failed to delete knowledge:', error);
163
+ alert('删除失败');
164
+ }
165
+ };
166
+
167
+ const wordCount = content.trim() ? content.trim().split(/\s+/).length : 0;
168
+
169
+ if (loading) {
170
+ return (
171
+ <div className="h-full flex items-center justify-center">
172
+ <div className="text-muted-foreground">加载中...</div>
173
+ </div>
174
+ );
175
+ }
176
+
177
+ if (!knowledge) {
178
+ return (
179
+ <div className="h-full flex items-center justify-center">
180
+ <div className="text-muted-foreground">知识不存在</div>
181
+ </div>
182
+ );
183
+ }
184
+
185
+ return (
186
+ <div className="h-full flex flex-col bg-background">
187
+ <header className="bg-card border-b flex-shrink-0">
188
+ <div className="px-4 py-3 flex items-center justify-between gap-4">
189
+ <div className="flex items-center gap-3">
190
+ <Button variant="ghost" size="icon" asChild>
191
+ <a href="/my/knowledge">
192
+ <ArrowLeft className="h-5 w-5" />
193
+ </a>
194
+ </Button>
195
+ <div className="flex items-center gap-2">
196
+ <FileText className="h-5 w-5 text-muted-foreground" />
197
+ <span className="font-medium">知识编辑</span>
198
+ </div>
199
+ </div>
200
+ <div className="flex items-center gap-2">
201
+ <Button
202
+ variant="outline"
203
+ size="sm"
204
+ onClick={() => setPreviewMode(previewMode === 'edit' ? 'preview' : 'edit')}
205
+ >
206
+ {previewMode === 'edit' ? <Eye className="h-4 w-4 mr-1" /> : <EyeOff className="h-4 w-4 mr-1" />}
207
+ {previewMode === 'edit' ? '预览' : '编辑'}
208
+ </Button>
209
+ <Button onClick={handleSave} disabled={saving || !title.trim()}>
210
+ {saving ? '保存中...' : '保存'}
211
+ </Button>
212
+ </div>
213
+ </div>
214
+ </header>
215
+
216
+ <div className="flex-1 flex overflow-hidden">
217
+ <div className="flex-1 flex flex-col overflow-hidden">
218
+ <div className="flex-1 overflow-auto p-4">
219
+ {previewMode === 'preview' ? (
220
+ <KnowledgePreview
221
+ title={title}
222
+ content={content}
223
+ category={knowledge?.category}
224
+ tags={tags}
225
+ author={knowledge?.author || ''}
226
+ updatedAt={knowledge?.updatedAt ? new Date(knowledge.updatedAt).toISOString() : ''}
227
+ type={type}
228
+ />
229
+ ) : (
230
+ <div className="h-full max-w-6xl mx-auto" data-color-mode="light">
231
+ <MDEditor
232
+ value={content}
233
+ onChange={(val) => setContent(val || '')}
234
+ height="100%"
235
+ preview="live"
236
+ hideToolbar={false}
237
+ visibleDragbar={false}
238
+ />
239
+ </div>
240
+ )}
241
+ </div>
242
+ <div className="flex-shrink-0 px-4 py-2 border-t bg-muted/30 text-xs text-muted-foreground flex items-center justify-between">
243
+ <span>字数: {wordCount}</span>
244
+ {lastSaved && <span>上次保存: {lastSaved.toLocaleTimeString('zh-CN')}</span>}
245
+ </div>
246
+ </div>
247
+
248
+ <KnowledgeMetadataPanel
249
+ title={title}
250
+ onTitleChange={setTitle}
251
+ type={type}
252
+ onTypeChange={setType}
253
+ category={category}
254
+ onCategoryChange={setCategory}
255
+ tags={tags}
256
+ onTagsChange={setTags}
257
+ isActive={isActive}
258
+ onIsActiveChange={setIsActive}
259
+ categories={categories}
260
+ onSave={handleSave}
261
+ onDelete={handleDelete}
262
+ saving={saving}
263
+ author={knowledge.author}
264
+ createdAt={knowledge.createdAt}
265
+ updatedAt={knowledge.updatedAt}
266
+ sourceType={knowledge.sourceType ? (KNOWLEDGE_SOURCE_LABELS[knowledge.sourceType] || knowledge.sourceType) : undefined}
267
+ />
268
+ </div>
269
+ </div>
270
+ );
271
+ }
@@ -0,0 +1,234 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect } from 'react';
4
+ import { useRouter } from 'next/navigation';
5
+ import dynamic from 'next/dynamic';
6
+ import { ArrowLeft, Eye, EyeOff, FileText, CheckCircle, XCircle } from 'lucide-react';
7
+ import { Button } from '@/components/ui/button';
8
+ import { KnowledgeMetadataPanel } from '@/components/knowledge/KnowledgeMetadataPanel';
9
+ import type { KnowledgeType } from '@/lib/db';
10
+
11
+ const MDEditor = dynamic(
12
+ () => import('@uiw/react-md-editor'),
13
+ { ssr: false }
14
+ );
15
+
16
+ interface KnowledgeItem {
17
+ id: string;
18
+ title: string;
19
+ content: string;
20
+ category?: string;
21
+ tags?: string[];
22
+ author: string;
23
+ createdAt: Date;
24
+ updatedAt: Date;
25
+ }
26
+
27
+ export default function NewKnowledgePage() {
28
+ const router = useRouter();
29
+ const [title, setTitle] = useState('');
30
+ const [content, setContent] = useState('');
31
+ const [category, setCategory] = useState('none');
32
+ const [tags, setTags] = useState<string[]>([]);
33
+ const [type, setType] = useState<KnowledgeType>('documentation');
34
+ const [isActive, setIsActive] = useState(true);
35
+ const [previewMode, setPreviewMode] = useState<'edit' | 'preview'>('edit');
36
+ const [saving, setSaving] = useState(false);
37
+ const [saveStatus, setSaveStatus] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
38
+ const [lastSaved, setLastSaved] = useState<Date | null>(null);
39
+
40
+ const [categories, setCategories] = useState<string[]>([]);
41
+
42
+ useEffect(() => {
43
+ loadCategories();
44
+ loadPendingKnowledge();
45
+ }, []);
46
+
47
+ const loadPendingKnowledge = () => {
48
+ try {
49
+ const pending = localStorage.getItem('pendingKnowledge');
50
+ if (pending) {
51
+ console.log('[NewKnowledge] Loading pending knowledge:', pending.substring(0, 200));
52
+ const data = JSON.parse(pending);
53
+ console.log('[NewKnowledge] Parsed data:', data);
54
+ setTitle(data.title || '');
55
+ setContent(data.content || '');
56
+ setCategory(data.category || 'none');
57
+ setTags(data.tags || []);
58
+ setType(data.type || 'documentation');
59
+ localStorage.removeItem('pendingKnowledge');
60
+ }
61
+ } catch (error) {
62
+ console.error('[NewKnowledge] Failed to load pending knowledge:', error);
63
+ }
64
+ };
65
+
66
+ useEffect(() => {
67
+ const handleKeyDown = (e: KeyboardEvent) => {
68
+ if ((e.ctrlKey || e.metaKey) && e.key === 's') {
69
+ e.preventDefault();
70
+ handleSave();
71
+ }
72
+ if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
73
+ e.preventDefault();
74
+ setPreviewMode(p => p === 'edit' ? 'preview' : 'edit');
75
+ }
76
+ };
77
+ window.addEventListener('keydown', handleKeyDown);
78
+ return () => window.removeEventListener('keydown', handleKeyDown);
79
+ }, [title, content, type, category, tags, isActive]);
80
+
81
+ const loadCategories = async () => {
82
+ try {
83
+ const response = await fetch('/api/knowledge');
84
+ const data = await response.json();
85
+ const cats = Array.from(new Set(
86
+ data.documents
87
+ .map((d: KnowledgeItem) => d.category)
88
+ .filter(Boolean)
89
+ )) as string[];
90
+ setCategories(cats);
91
+ } catch (error) {
92
+ console.error('Failed to load categories:', error);
93
+ }
94
+ };
95
+
96
+ const handleSave = async () => {
97
+ if (!title.trim()) {
98
+ alert('请输入知识标题');
99
+ return;
100
+ }
101
+
102
+ const userId = localStorage.getItem('userId');
103
+ if (!userId) {
104
+ alert('未找到用户ID,请先登录');
105
+ return;
106
+ }
107
+
108
+ setSaving(true);
109
+ setSaveStatus(null);
110
+ try {
111
+ const response = await fetch('/api/knowledge', {
112
+ method: 'POST',
113
+ headers: { 'Content-Type': 'application/json' },
114
+ body: JSON.stringify({
115
+ title: title.trim(),
116
+ content,
117
+ category: category && category !== 'none' ? category : undefined,
118
+ tags: tags.length > 0 ? tags : undefined,
119
+ author: userId,
120
+ type,
121
+ sourceType: 'manual',
122
+ isActive,
123
+ }),
124
+ });
125
+
126
+ if (response.ok) {
127
+ const data = await response.json();
128
+ setSaveStatus({ type: 'success', text: '保存成功!' });
129
+ setLastSaved(new Date());
130
+ setTimeout(() => {
131
+ router.push(`/my/knowledge/${data.document.id}`);
132
+ }, 800);
133
+ } else {
134
+ const error = await response.json();
135
+ setSaveStatus({ type: 'error', text: error.error || '保存失败' });
136
+ }
137
+ } catch (error) {
138
+ console.error('Failed to save knowledge:', error);
139
+ setSaveStatus({ type: 'error', text: '保存失败' });
140
+ } finally {
141
+ setSaving(false);
142
+ }
143
+ };
144
+
145
+ const wordCount = content.trim() ? content.trim().split(/\s+/).length : 0;
146
+
147
+ return (
148
+ <div className="h-full flex flex-col bg-background">
149
+ <header className="bg-card border-b flex-shrink-0">
150
+ <div className="px-4 py-3 flex items-center justify-between gap-4">
151
+ <div className="flex items-center gap-3">
152
+ <Button variant="ghost" size="icon" asChild>
153
+ <a href="/my/knowledge">
154
+ <ArrowLeft className="h-5 w-5" />
155
+ </a>
156
+ </Button>
157
+ <div className="flex items-center gap-2">
158
+ <FileText className="h-5 w-5 text-muted-foreground" />
159
+ <span className="font-medium">新建知识</span>
160
+ </div>
161
+ </div>
162
+ <div className="flex items-center gap-3">
163
+ {saveStatus && (
164
+ <div className={`flex items-center gap-1 text-sm ${saveStatus.type === 'success' ? 'text-green-600' : 'text-red-600'}`}>
165
+ {saveStatus.type === 'success' ? (
166
+ <CheckCircle className="h-4 w-4" />
167
+ ) : (
168
+ <XCircle className="h-4 w-4" />
169
+ )}
170
+ {saveStatus.text}
171
+ </div>
172
+ )}
173
+ <Button
174
+ variant="outline"
175
+ size="sm"
176
+ onClick={() => setPreviewMode(previewMode === 'edit' ? 'preview' : 'edit')}
177
+ >
178
+ {previewMode === 'edit' ? <Eye className="h-4 w-4 mr-1" /> : <EyeOff className="h-4 w-4 mr-1" />}
179
+ {previewMode === 'edit' ? '预览' : '编辑'}
180
+ </Button>
181
+ <Button onClick={handleSave} disabled={saving || !title.trim()}>
182
+ {saving ? '保存中...' : '保存'}
183
+ </Button>
184
+ </div>
185
+ </div>
186
+ </header>
187
+
188
+ <div className="flex-1 flex overflow-hidden">
189
+ <div className="flex-1 flex flex-col overflow-hidden">
190
+ <div className="flex-1 overflow-auto p-4">
191
+ {previewMode === 'preview' ? (
192
+ <div className="max-w-4xl mx-auto prose prose-sm dark:prose-invert">
193
+ <h1>{title || '未命名知识'}</h1>
194
+ <div className="whitespace-pre-wrap">{content}</div>
195
+ </div>
196
+ ) : (
197
+ <div className="h-full max-w-6xl mx-auto" data-color-mode="light">
198
+ <MDEditor
199
+ value={content}
200
+ onChange={(val) => setContent(val || '')}
201
+ height="100%"
202
+ preview="live"
203
+ hideToolbar={false}
204
+ visibleDragbar={false}
205
+ />
206
+ </div>
207
+ )}
208
+ </div>
209
+ <div className="flex-shrink-0 px-4 py-2 border-t bg-muted/30 text-xs text-muted-foreground flex items-center justify-between">
210
+ <span>字数: {wordCount}</span>
211
+ {lastSaved && <span>上次保存: {lastSaved.toLocaleTimeString('zh-CN')}</span>}
212
+ </div>
213
+ </div>
214
+
215
+ <KnowledgeMetadataPanel
216
+ title={title}
217
+ onTitleChange={setTitle}
218
+ type={type}
219
+ onTypeChange={setType}
220
+ category={category}
221
+ onCategoryChange={setCategory}
222
+ tags={tags}
223
+ onTagsChange={setTags}
224
+ isActive={isActive}
225
+ onIsActiveChange={setIsActive}
226
+ categories={categories}
227
+ onSave={handleSave}
228
+ showDelete={false}
229
+ saving={saving}
230
+ />
231
+ </div>
232
+ </div>
233
+ );
234
+ }