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.
- package/README.md +234 -0
- package/app/(admin)/approvals/page.tsx +16 -0
- package/app/(admin)/audit/page.tsx +18 -0
- package/app/(admin)/layout.tsx +47 -0
- package/app/(admin)/scheduled-tasks/page.tsx +17 -0
- package/app/(admin)/settings/page.tsx +46 -0
- package/app/(admin)/skills/[name]/page.tsx +378 -0
- package/app/(admin)/skills/page.tsx +406 -0
- package/app/(admin)/statistics/page.tsx +416 -0
- package/app/(admin)/tickets/[id]/page.tsx +348 -0
- package/app/(admin)/tickets/new/page.tsx +309 -0
- package/app/(admin)/tickets/page.tsx +27 -0
- package/app/api/audit/route.ts +30 -0
- package/app/api/auth/feishu/callback/route.ts +72 -0
- package/app/api/auth/feishu/login/route.ts +17 -0
- package/app/api/auth/feishu/sso/route.ts +78 -0
- package/app/api/auth/login/route.ts +85 -0
- package/app/api/auth/oauth/route.ts +168 -0
- package/app/api/config/providers/route.ts +105 -0
- package/app/api/config/route.ts +115 -0
- package/app/api/config/status/route.ts +56 -0
- package/app/api/config/test/route.ts +212 -0
- package/app/api/documents/[id]/route.ts +88 -0
- package/app/api/documents/route.ts +53 -0
- package/app/api/health/route.ts +32 -0
- package/app/api/knowledge/[id]/route.ts +152 -0
- package/app/api/knowledge/from-session/route.ts +27 -0
- package/app/api/knowledge/route.ts +100 -0
- package/app/api/market/knowledge/[id]/route.ts +92 -0
- package/app/api/market/knowledge/route.ts +130 -0
- package/app/api/marketplace/skills/[id]/approve/route.ts +68 -0
- package/app/api/marketplace/skills/[id]/certify/route.ts +54 -0
- package/app/api/marketplace/skills/[id]/install/route.ts +180 -0
- package/app/api/marketplace/skills/[id]/promote-to-system/route.ts +219 -0
- package/app/api/marketplace/skills/[id]/rate/route.ts +90 -0
- package/app/api/marketplace/skills/[id]/ratings/route.ts +55 -0
- package/app/api/marketplace/skills/[id]/reject/route.ts +68 -0
- package/app/api/marketplace/skills/[id]/route.ts +177 -0
- package/app/api/marketplace/skills/route.ts +235 -0
- package/app/api/memory/route.ts +40 -0
- package/app/api/my/files/[id]/route.ts +52 -0
- package/app/api/my/files/route.ts +230 -0
- package/app/api/my/knowledge/route.ts +36 -0
- package/app/api/pi-chat/route.ts +443 -0
- package/app/api/recommend/route.ts +38 -0
- package/app/api/scheduled-tasks/[id]/execute/route.ts +132 -0
- package/app/api/scheduled-tasks/[id]/route.ts +165 -0
- package/app/api/scheduled-tasks/[id]/toggle/route.ts +53 -0
- package/app/api/scheduled-tasks/route.ts +101 -0
- package/app/api/sessions/[id]/messages/route.ts +212 -0
- package/app/api/sessions/route.ts +101 -0
- package/app/api/share/file/[id]/route.ts +37 -0
- package/app/api/skills/[name]/execute/route.ts +121 -0
- package/app/api/skills/[name]/route.ts +167 -0
- package/app/api/skills/create/route.ts +65 -0
- package/app/api/skills/generate/route.ts +405 -0
- package/app/api/skills/installed/route.ts +151 -0
- package/app/api/skills/route.ts +174 -0
- package/app/api/skills/translate/route.ts +40 -0
- package/app/api/skills/user/[name]/route.ts +159 -0
- package/app/api/skills/user/route.ts +90 -0
- package/app/api/statistics/route.ts +94 -0
- package/app/api/task-executions/[id]/route.ts +34 -0
- package/app/api/task-executions/route.ts +29 -0
- package/app/api/tickets/[id]/approve/route.ts +129 -0
- package/app/api/tickets/[id]/execute/route.ts +201 -0
- package/app/api/tickets/[id]/route.ts +127 -0
- package/app/api/tickets/route.ts +103 -0
- package/app/api/user/skills/route.ts +175 -0
- package/app/api/users/route.ts +80 -0
- package/app/chat/page.tsx +5 -0
- package/app/globals.css +84 -0
- package/app/h5/layout.tsx +5 -0
- package/app/h5/mobile-approvals-page.tsx +167 -0
- package/app/h5/mobile-chat-page.tsx +951 -0
- package/app/h5/mobile-profile-page.tsx +147 -0
- package/app/h5/mobile-tickets-page.tsx +121 -0
- package/app/h5/page.tsx +23 -0
- package/app/h5/ticket-action-buttons.tsx +80 -0
- package/app/layout.tsx +26 -0
- package/app/login/page.tsx +318 -0
- package/app/market/knowledge/[id]/page.tsx +77 -0
- package/app/market/knowledge/page.tsx +358 -0
- package/app/market/layout.tsx +29 -0
- package/app/market/page.tsx +18 -0
- package/app/market/skills/page.tsx +397 -0
- package/app/my/files/page.tsx +511 -0
- package/app/my/knowledge/[id]/page.tsx +271 -0
- package/app/my/knowledge/new/page.tsx +234 -0
- package/app/my/knowledge/page.tsx +248 -0
- package/app/my/layout.tsx +32 -0
- package/app/my/memory/page.tsx +164 -0
- package/app/my/page.tsx +18 -0
- package/app/my/scheduled-tasks/[id]/edit/page.tsx +290 -0
- package/app/my/scheduled-tasks/[id]/executions/page.tsx +275 -0
- package/app/my/scheduled-tasks/[id]/page.tsx +284 -0
- package/app/my/scheduled-tasks/new/page.tsx +230 -0
- package/app/my/scheduled-tasks/page.tsx +27 -0
- package/app/my/skills/[name]/page.tsx +320 -0
- package/app/my/skills/new/page.tsx +394 -0
- package/app/my/skills/page.tsx +303 -0
- package/app/page.tsx +2288 -0
- package/app/share/[sessionId]/page.tsx +226 -0
- package/app/share/file/[id]/page.tsx +140 -0
- package/bin/README.md +63 -0
- package/bin/generate-api-system +300 -0
- package/bin/postinstall.js +95 -0
- package/bin/work-agent.js +173 -0
- package/components/ai-elements/agent.tsx +142 -0
- package/components/ai-elements/artifact.tsx +149 -0
- package/components/ai-elements/attachments.tsx +427 -0
- package/components/ai-elements/audio-player.tsx +232 -0
- package/components/ai-elements/canvas.tsx +26 -0
- package/components/ai-elements/chain-of-thought.tsx +223 -0
- package/components/ai-elements/checkpoint.tsx +72 -0
- package/components/ai-elements/code-block.tsx +555 -0
- package/components/ai-elements/commit.tsx +449 -0
- package/components/ai-elements/confirmation.tsx +173 -0
- package/components/ai-elements/connection.tsx +28 -0
- package/components/ai-elements/context.tsx +410 -0
- package/components/ai-elements/controls.tsx +19 -0
- package/components/ai-elements/conversation.tsx +167 -0
- package/components/ai-elements/edge.tsx +144 -0
- package/components/ai-elements/environment-variables.tsx +325 -0
- package/components/ai-elements/file-tree.tsx +298 -0
- package/components/ai-elements/image.tsx +25 -0
- package/components/ai-elements/inline-citation.tsx +294 -0
- package/components/ai-elements/jsx-preview.tsx +250 -0
- package/components/ai-elements/message.tsx +367 -0
- package/components/ai-elements/mic-selector.tsx +372 -0
- package/components/ai-elements/model-selector.tsx +214 -0
- package/components/ai-elements/node.tsx +72 -0
- package/components/ai-elements/open-in-chat.tsx +367 -0
- package/components/ai-elements/package-info.tsx +235 -0
- package/components/ai-elements/panel.tsx +16 -0
- package/components/ai-elements/persona.tsx +280 -0
- package/components/ai-elements/plan.tsx +144 -0
- package/components/ai-elements/prompt-input.tsx +1341 -0
- package/components/ai-elements/queue.tsx +275 -0
- package/components/ai-elements/reasoning.tsx +355 -0
- package/components/ai-elements/sandbox.tsx +133 -0
- package/components/ai-elements/schema-display.tsx +473 -0
- package/components/ai-elements/shimmer.tsx +78 -0
- package/components/ai-elements/snippet.tsx +141 -0
- package/components/ai-elements/sources.tsx +78 -0
- package/components/ai-elements/speech-input.tsx +324 -0
- package/components/ai-elements/stack-trace.tsx +531 -0
- package/components/ai-elements/suggestion.tsx +58 -0
- package/components/ai-elements/task.tsx +88 -0
- package/components/ai-elements/terminal.tsx +277 -0
- package/components/ai-elements/test-results.tsx +497 -0
- package/components/ai-elements/tool.tsx +174 -0
- package/components/ai-elements/toolbar.tsx +17 -0
- package/components/ai-elements/transcription.tsx +126 -0
- package/components/ai-elements/voice-selector.tsx +525 -0
- package/components/ai-elements/web-preview.tsx +282 -0
- package/components/audit-log-list.tsx +114 -0
- package/components/chat/EmptyPreviewState.tsx +12 -0
- package/components/chat/KnowledgePickerDialog.tsx +464 -0
- package/components/chat/KnowledgePreview.tsx +70 -0
- package/components/chat/KnowledgePreviewPanel.tsx +86 -0
- package/components/chat/MentionInput.tsx +309 -0
- package/components/chat/OrganizeDialog.tsx +258 -0
- package/components/chat/RecommendationBanner.tsx +94 -0
- package/components/chat/SaveToKnowledgeDialog.tsx +193 -0
- package/components/chat/SkillSelector.tsx +305 -0
- package/components/chat/SkillSwitcher.tsx +163 -0
- package/components/client-layout.tsx +15 -0
- package/components/knowledge/KnowledgeMetadataPanel.tsx +293 -0
- package/components/layout-wrapper.tsx +18 -0
- package/components/mobile-layout.tsx +62 -0
- package/components/scheduled-task-list.tsx +356 -0
- package/components/setup-guide.tsx +484 -0
- package/components/sub-nav.tsx +54 -0
- package/components/ticket-detail-content.tsx +383 -0
- package/components/ticket-list.tsx +366 -0
- package/components/top-nav.tsx +132 -0
- package/components/ui/accordion.tsx +58 -0
- package/components/ui/alert.tsx +59 -0
- package/components/ui/avatar.tsx +50 -0
- package/components/ui/badge.tsx +36 -0
- package/components/ui/button-group.tsx +83 -0
- package/components/ui/button.tsx +57 -0
- package/components/ui/card.tsx +91 -0
- package/components/ui/carousel.tsx +262 -0
- package/components/ui/collapsible.tsx +11 -0
- package/components/ui/command.tsx +153 -0
- package/components/ui/dialog.tsx +122 -0
- package/components/ui/dropdown-menu.tsx +200 -0
- package/components/ui/hover-card.tsx +29 -0
- package/components/ui/input-group.tsx +170 -0
- package/components/ui/input.tsx +22 -0
- package/components/ui/label.tsx +26 -0
- package/components/ui/popover.tsx +31 -0
- package/components/ui/progress.tsx +28 -0
- package/components/ui/scroll-area.tsx +48 -0
- package/components/ui/select.tsx +174 -0
- package/components/ui/separator.tsx +31 -0
- package/components/ui/spinner.tsx +16 -0
- package/components/ui/switch.tsx +29 -0
- package/components/ui/table.tsx +120 -0
- package/components/ui/tabs.tsx +55 -0
- package/components/ui/textarea.tsx +22 -0
- package/components/ui/tooltip.tsx +30 -0
- package/components/welcome-guide.tsx +182 -0
- package/components.json +24 -0
- package/lib/command-parser.ts +331 -0
- package/lib/dangerous-commands.ts +672 -0
- package/lib/db.ts +2250 -0
- package/lib/feishu-auth.ts +135 -0
- package/lib/file-storage.ts +306 -0
- package/lib/file-tool.ts +583 -0
- package/lib/knowledge-tool.ts +152 -0
- package/lib/knowledge-types.ts +66 -0
- package/lib/market-client.ts +313 -0
- package/lib/market-db.ts +736 -0
- package/lib/market-types.ts +51 -0
- package/lib/memory-tool.ts +211 -0
- package/lib/memory.ts +197 -0
- package/lib/pi-config.ts +436 -0
- package/lib/pi-session.ts +799 -0
- package/lib/pinyin.ts +13 -0
- package/lib/recommendation.ts +227 -0
- package/lib/risk-estimator.ts +350 -0
- package/lib/scheduled-task-tool.ts +184 -0
- package/lib/scheduler-init.ts +43 -0
- package/lib/scheduler.ts +416 -0
- package/lib/secure-bash-tool.ts +413 -0
- package/lib/skill-engine.ts +396 -0
- package/lib/skill-generator.ts +269 -0
- package/lib/skill-loader.ts +234 -0
- package/lib/skill-tool.ts +188 -0
- package/lib/skill-types.ts +82 -0
- package/lib/skills-init.ts +58 -0
- package/lib/ticket-tool.ts +246 -0
- package/lib/user-skill-types.ts +30 -0
- package/lib/user-skills.ts +362 -0
- package/lib/utils.ts +6 -0
- package/lib/workflow.ts +154 -0
- package/lib/zip-tool.ts +191 -0
- package/next.config.js +8 -0
- package/package.json +106 -0
- package/public/.gitkeep +1 -0
- package/public/icon.svg +1 -0
- package/tsconfig.json +42 -0
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import Link from 'next/link';
|
|
5
|
+
import { useRouter } from 'next/navigation';
|
|
6
|
+
import { ArrowLeft, Plus, CheckCircle, Loader2, MessageSquare, Zap, XCircle } from 'lucide-react';
|
|
7
|
+
import { Button } from '@/components/ui/button';
|
|
8
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
9
|
+
import { Input } from '@/components/ui/input';
|
|
10
|
+
import { Label } from '@/components/ui/label';
|
|
11
|
+
import { Terminal, TerminalHeader, TerminalTitle, TerminalContent } from '@/components/ai-elements/terminal';
|
|
12
|
+
import {
|
|
13
|
+
Dialog,
|
|
14
|
+
DialogContent,
|
|
15
|
+
DialogHeader,
|
|
16
|
+
DialogTitle,
|
|
17
|
+
DialogDescription,
|
|
18
|
+
DialogFooter,
|
|
19
|
+
} from '@/components/ui/dialog';
|
|
20
|
+
|
|
21
|
+
interface GenerationStep {
|
|
22
|
+
status: 'pending' | 'running' | 'success' | 'error';
|
|
23
|
+
message: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default function NewSkillPage() {
|
|
27
|
+
const router = useRouter();
|
|
28
|
+
|
|
29
|
+
const [swaggerUrl, setSwaggerUrl] = useState('');
|
|
30
|
+
const [systemName, setSystemName] = useState('');
|
|
31
|
+
const [baseUrl, setBaseUrl] = useState('');
|
|
32
|
+
const [token, setToken] = useState('');
|
|
33
|
+
|
|
34
|
+
const [generating, setGenerating] = useState(false);
|
|
35
|
+
const [steps, setSteps] = useState<GenerationStep[]>([]);
|
|
36
|
+
const [result, setResult] = useState<{
|
|
37
|
+
success: boolean;
|
|
38
|
+
skillName: string;
|
|
39
|
+
displayName: string;
|
|
40
|
+
scriptCount?: number;
|
|
41
|
+
apiCount?: number;
|
|
42
|
+
type?: string;
|
|
43
|
+
path?: string;
|
|
44
|
+
} | null>(null);
|
|
45
|
+
|
|
46
|
+
const [showSuccessDialog, setShowSuccessDialog] = useState(false);
|
|
47
|
+
const [showErrorDialog, setShowErrorDialog] = useState(false);
|
|
48
|
+
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
|
49
|
+
|
|
50
|
+
const addStep = (message: string) => {
|
|
51
|
+
setSteps(prev => [...prev, { status: 'running', message }]);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const updateStep = (index: number, status: GenerationStep['status'], message?: string) => {
|
|
55
|
+
setSteps(prev => {
|
|
56
|
+
const updated = [...prev];
|
|
57
|
+
updated[index] = { ...updated[index], status, message: message || updated[index].message };
|
|
58
|
+
return updated;
|
|
59
|
+
});
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const handleSwaggerGenerate = async () => {
|
|
63
|
+
if (!swaggerUrl) {
|
|
64
|
+
setErrorMessage('请输入 Swagger URL');
|
|
65
|
+
setShowErrorDialog(true);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const userId = localStorage.getItem('userId');
|
|
70
|
+
|
|
71
|
+
setGenerating(true);
|
|
72
|
+
setSteps([]);
|
|
73
|
+
setResult(null);
|
|
74
|
+
setShowErrorDialog(false);
|
|
75
|
+
setErrorMessage(null);
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
addStep(`开始生成 Skill: ${systemName || '新系统'}`);
|
|
79
|
+
updateStep(0, 'running', '正在解析 Swagger 文档...');
|
|
80
|
+
|
|
81
|
+
const res = await fetch('/api/skills/generate', {
|
|
82
|
+
method: 'POST',
|
|
83
|
+
headers: { 'Content-Type': 'application/json' },
|
|
84
|
+
body: JSON.stringify({
|
|
85
|
+
swaggerUrl,
|
|
86
|
+
systemName: systemName || undefined,
|
|
87
|
+
baseUrl: baseUrl || undefined,
|
|
88
|
+
token: token || undefined,
|
|
89
|
+
userId: userId || undefined,
|
|
90
|
+
}),
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const data = await res.json();
|
|
94
|
+
|
|
95
|
+
if (!res.ok) {
|
|
96
|
+
throw new Error(data.error || '生成失败');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
updateStep(0, 'success', `生成完成! ${data.apiCount} 个 API, ${data.scriptCount} 个脚本`);
|
|
100
|
+
|
|
101
|
+
setResult({
|
|
102
|
+
success: true,
|
|
103
|
+
skillName: data.skillName,
|
|
104
|
+
displayName: data.displayName,
|
|
105
|
+
scriptCount: data.scriptCount,
|
|
106
|
+
apiCount: data.apiCount,
|
|
107
|
+
});
|
|
108
|
+
setShowSuccessDialog(true);
|
|
109
|
+
} catch (err) {
|
|
110
|
+
const message = err instanceof Error ? err.message : '生成失败';
|
|
111
|
+
if (steps.length > 0) {
|
|
112
|
+
updateStep(steps.length - 1, 'error', message);
|
|
113
|
+
}
|
|
114
|
+
setErrorMessage(message);
|
|
115
|
+
setShowErrorDialog(true);
|
|
116
|
+
} finally {
|
|
117
|
+
setGenerating(false);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const handleAICreate = () => {
|
|
122
|
+
const userId = localStorage.getItem('userId');
|
|
123
|
+
if (userId) {
|
|
124
|
+
router.push(`/?skill=skill-creator&prompt=${encodeURIComponent('帮我创建一个新的技能')}`);
|
|
125
|
+
} else {
|
|
126
|
+
router.push('/');
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const output = steps.map((step) => {
|
|
131
|
+
const icon = step.status === 'running' ? '⏳' : step.status === 'success' ? '✓' : step.status === 'error' ? '✗' : '○';
|
|
132
|
+
return `${icon} [${step.status.toUpperCase()}] ${step.message}`;
|
|
133
|
+
}).join('\n');
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<div className="p-6">
|
|
137
|
+
<div className="max-w-4xl mx-auto">
|
|
138
|
+
<div className="flex items-center gap-4 mb-8">
|
|
139
|
+
<Link href="/my/skills">
|
|
140
|
+
<Button variant="ghost" size="icon">
|
|
141
|
+
<ArrowLeft className="w-5 h-5" />
|
|
142
|
+
</Button>
|
|
143
|
+
</Link>
|
|
144
|
+
<div>
|
|
145
|
+
<h1 className="text-2xl font-bold">创建技能</h1>
|
|
146
|
+
<p className="text-muted-foreground mt-1">选择创建方式</p>
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
|
|
151
|
+
<Card className="cursor-pointer hover:border-primary/50 hover:bg-primary/5 transition-all" onClick={handleAICreate}>
|
|
152
|
+
<CardHeader>
|
|
153
|
+
<div className="flex items-center gap-3">
|
|
154
|
+
<div className="w-12 h-12 rounded-lg bg-primary/10 flex items-center justify-center">
|
|
155
|
+
<MessageSquare className="w-6 h-6 text-primary" />
|
|
156
|
+
</div>
|
|
157
|
+
<div>
|
|
158
|
+
<CardTitle className="flex items-center gap-2">
|
|
159
|
+
AI 引导创建
|
|
160
|
+
<span className="text-xs bg-primary text-primary-foreground px-2 py-0.5 rounded">推荐</span>
|
|
161
|
+
</CardTitle>
|
|
162
|
+
<CardDescription>通过对话创建通用技能</CardDescription>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
</CardHeader>
|
|
166
|
+
<CardContent>
|
|
167
|
+
<ul className="text-sm text-muted-foreground space-y-2">
|
|
168
|
+
<li className="flex items-center gap-2">
|
|
169
|
+
<span className="text-green-500">✓</span>
|
|
170
|
+
多轮对话引导,更易上手
|
|
171
|
+
</li>
|
|
172
|
+
<li className="flex items-center gap-2">
|
|
173
|
+
<span className="text-green-500">✓</span>
|
|
174
|
+
自动生成符合规范的 Skill
|
|
175
|
+
</li>
|
|
176
|
+
<li className="flex items-center gap-2">
|
|
177
|
+
<span className="text-green-500">✓</span>
|
|
178
|
+
支持知识型、命令型、工作流型
|
|
179
|
+
</li>
|
|
180
|
+
</ul>
|
|
181
|
+
<Button className="w-full mt-4">
|
|
182
|
+
开始对话创建
|
|
183
|
+
</Button>
|
|
184
|
+
</CardContent>
|
|
185
|
+
</Card>
|
|
186
|
+
|
|
187
|
+
<Card className="hover:border-primary/50 transition-all">
|
|
188
|
+
<CardHeader>
|
|
189
|
+
<div className="flex items-center gap-3">
|
|
190
|
+
<div className="w-12 h-12 rounded-lg bg-orange-100 dark:bg-orange-900/30 flex items-center justify-center">
|
|
191
|
+
<Zap className="w-6 h-6 text-orange-600" />
|
|
192
|
+
</div>
|
|
193
|
+
<div>
|
|
194
|
+
<CardTitle>Swagger/API 生成</CardTitle>
|
|
195
|
+
<CardDescription>从 API 文档自动生成</CardDescription>
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
</CardHeader>
|
|
199
|
+
<CardContent>
|
|
200
|
+
<ul className="text-sm text-muted-foreground space-y-2">
|
|
201
|
+
<li className="flex items-center gap-2">
|
|
202
|
+
<span className="text-orange-500">⚡</span>
|
|
203
|
+
自动解析 Swagger/OpenAPI
|
|
204
|
+
</li>
|
|
205
|
+
<li className="flex items-center gap-2">
|
|
206
|
+
<span className="text-orange-500">⚡</span>
|
|
207
|
+
生成可执行的 API 调用脚本
|
|
208
|
+
</li>
|
|
209
|
+
<li className="flex items-center gap-2">
|
|
210
|
+
<span className="text-orange-500">⚡</span>
|
|
211
|
+
自动分类只读/修改操作
|
|
212
|
+
</li>
|
|
213
|
+
</ul>
|
|
214
|
+
</CardContent>
|
|
215
|
+
</Card>
|
|
216
|
+
</div>
|
|
217
|
+
|
|
218
|
+
<Card>
|
|
219
|
+
<CardHeader>
|
|
220
|
+
<CardTitle className="flex items-center gap-2">
|
|
221
|
+
<Zap className="w-5 h-5 text-orange-600" />
|
|
222
|
+
Swagger/API Skill 生成
|
|
223
|
+
</CardTitle>
|
|
224
|
+
<CardDescription>
|
|
225
|
+
提供 API 文档地址,系统将自动解析并生成 Skill
|
|
226
|
+
</CardDescription>
|
|
227
|
+
</CardHeader>
|
|
228
|
+
<CardContent>
|
|
229
|
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
230
|
+
<div className="space-y-4">
|
|
231
|
+
<div className="space-y-2">
|
|
232
|
+
<Label htmlFor="swaggerUrl">Swagger URL *</Label>
|
|
233
|
+
<Input
|
|
234
|
+
id="swaggerUrl"
|
|
235
|
+
placeholder="https://api.example.com/openapi.json"
|
|
236
|
+
value={swaggerUrl}
|
|
237
|
+
onChange={(e) => setSwaggerUrl(e.target.value)}
|
|
238
|
+
disabled={generating}
|
|
239
|
+
/>
|
|
240
|
+
<p className="text-xs text-muted-foreground">
|
|
241
|
+
支持 Swagger 2.0 和 OpenAPI 3.0 格式
|
|
242
|
+
</p>
|
|
243
|
+
</div>
|
|
244
|
+
|
|
245
|
+
<div className="space-y-2">
|
|
246
|
+
<Label htmlFor="systemName">显示名称</Label>
|
|
247
|
+
<Input
|
|
248
|
+
id="systemName"
|
|
249
|
+
placeholder="My API System"
|
|
250
|
+
value={systemName}
|
|
251
|
+
onChange={(e) => setSystemName(e.target.value)}
|
|
252
|
+
disabled={generating}
|
|
253
|
+
/>
|
|
254
|
+
<p className="text-xs text-muted-foreground">
|
|
255
|
+
用于 @引用和显示,目录名自动生成
|
|
256
|
+
</p>
|
|
257
|
+
</div>
|
|
258
|
+
|
|
259
|
+
<div className="grid grid-cols-2 gap-4">
|
|
260
|
+
<div className="space-y-2">
|
|
261
|
+
<Label htmlFor="baseUrl">API Base URL</Label>
|
|
262
|
+
<Input
|
|
263
|
+
id="baseUrl"
|
|
264
|
+
placeholder="https://api.example.com"
|
|
265
|
+
value={baseUrl}
|
|
266
|
+
onChange={(e) => setBaseUrl(e.target.value)}
|
|
267
|
+
disabled={generating}
|
|
268
|
+
/>
|
|
269
|
+
</div>
|
|
270
|
+
|
|
271
|
+
<div className="space-y-2">
|
|
272
|
+
<Label htmlFor="token">API Token(可选)</Label>
|
|
273
|
+
<Input
|
|
274
|
+
id="token"
|
|
275
|
+
type="password"
|
|
276
|
+
placeholder="your-api-token"
|
|
277
|
+
value={token}
|
|
278
|
+
onChange={(e) => setToken(e.target.value)}
|
|
279
|
+
disabled={generating}
|
|
280
|
+
/>
|
|
281
|
+
</div>
|
|
282
|
+
</div>
|
|
283
|
+
|
|
284
|
+
<Button
|
|
285
|
+
className="w-full"
|
|
286
|
+
onClick={handleSwaggerGenerate}
|
|
287
|
+
disabled={generating || !swaggerUrl}
|
|
288
|
+
>
|
|
289
|
+
{generating ? (
|
|
290
|
+
<>
|
|
291
|
+
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
|
292
|
+
生成中...
|
|
293
|
+
</>
|
|
294
|
+
) : (
|
|
295
|
+
<>
|
|
296
|
+
<Plus className="w-4 h-4 mr-2" />
|
|
297
|
+
生成 Skill
|
|
298
|
+
</>
|
|
299
|
+
)}
|
|
300
|
+
</Button>
|
|
301
|
+
|
|
302
|
+
<div className="p-3 rounded bg-muted text-sm">
|
|
303
|
+
<div className="font-medium mb-2">示例 Swagger URL</div>
|
|
304
|
+
<div className="space-y-1 text-xs font-mono text-muted-foreground">
|
|
305
|
+
<div>https://petstore.swagger.io/v2/swagger.json</div>
|
|
306
|
+
<div>https://jsonplaceholder.typicode.com/openapi.yaml</div>
|
|
307
|
+
</div>
|
|
308
|
+
</div>
|
|
309
|
+
</div>
|
|
310
|
+
|
|
311
|
+
<div>
|
|
312
|
+
<Label className="mb-2 block">生成进度</Label>
|
|
313
|
+
<Terminal output={output || '等待开始生成...'} className="min-h-[280px]">
|
|
314
|
+
<TerminalHeader>
|
|
315
|
+
<TerminalTitle>生成日志</TerminalTitle>
|
|
316
|
+
</TerminalHeader>
|
|
317
|
+
<TerminalContent />
|
|
318
|
+
</Terminal>
|
|
319
|
+
</div>
|
|
320
|
+
</div>
|
|
321
|
+
</CardContent>
|
|
322
|
+
</Card>
|
|
323
|
+
</div>
|
|
324
|
+
|
|
325
|
+
<Dialog open={showSuccessDialog} onOpenChange={setShowSuccessDialog}>
|
|
326
|
+
<DialogContent className="sm:max-w-md">
|
|
327
|
+
<DialogHeader>
|
|
328
|
+
<DialogTitle className="flex items-center gap-2 text-green-600">
|
|
329
|
+
<CheckCircle className="w-5 h-5" />
|
|
330
|
+
生成成功!
|
|
331
|
+
</DialogTitle>
|
|
332
|
+
<DialogDescription>
|
|
333
|
+
技能已成功创建
|
|
334
|
+
</DialogDescription>
|
|
335
|
+
</DialogHeader>
|
|
336
|
+
<div className="space-y-4">
|
|
337
|
+
{result && (
|
|
338
|
+
<div className="grid grid-cols-2 gap-4">
|
|
339
|
+
<div className="p-3 rounded bg-muted text-center">
|
|
340
|
+
<div className="text-2xl font-bold">{result.apiCount}</div>
|
|
341
|
+
<div className="text-sm text-muted-foreground">API 端点</div>
|
|
342
|
+
</div>
|
|
343
|
+
<div className="p-3 rounded bg-muted text-center">
|
|
344
|
+
<div className="text-2xl font-bold">{result.scriptCount}</div>
|
|
345
|
+
<div className="text-sm text-muted-foreground">脚本</div>
|
|
346
|
+
</div>
|
|
347
|
+
</div>
|
|
348
|
+
)}
|
|
349
|
+
</div>
|
|
350
|
+
<DialogFooter className="sm:justify-center gap-2">
|
|
351
|
+
<Link href={result ? `/my/skills/${result.skillName}` : '/my/skills'} className="flex-1">
|
|
352
|
+
<Button variant="outline" className="w-full">
|
|
353
|
+
查看 Skill
|
|
354
|
+
</Button>
|
|
355
|
+
</Link>
|
|
356
|
+
<Button onClick={() => {
|
|
357
|
+
setShowSuccessDialog(false);
|
|
358
|
+
setSwaggerUrl('');
|
|
359
|
+
setSystemName('');
|
|
360
|
+
setBaseUrl('');
|
|
361
|
+
setToken('');
|
|
362
|
+
setSteps([]);
|
|
363
|
+
setResult(null);
|
|
364
|
+
}}>
|
|
365
|
+
再生成一个
|
|
366
|
+
</Button>
|
|
367
|
+
</DialogFooter>
|
|
368
|
+
</DialogContent>
|
|
369
|
+
</Dialog>
|
|
370
|
+
|
|
371
|
+
<Dialog open={showErrorDialog} onOpenChange={setShowErrorDialog}>
|
|
372
|
+
<DialogContent className="sm:max-w-md">
|
|
373
|
+
<DialogHeader>
|
|
374
|
+
<DialogTitle className="flex items-center gap-2 text-red-600">
|
|
375
|
+
<XCircle className="w-5 h-5" />
|
|
376
|
+
生成失败
|
|
377
|
+
</DialogTitle>
|
|
378
|
+
<DialogDescription>
|
|
379
|
+
操作未能完成,请检查错误信息
|
|
380
|
+
</DialogDescription>
|
|
381
|
+
</DialogHeader>
|
|
382
|
+
<div className="p-3 rounded-lg bg-red-50 text-red-700 text-sm border border-red-200">
|
|
383
|
+
{errorMessage}
|
|
384
|
+
</div>
|
|
385
|
+
<DialogFooter className="sm:justify-center">
|
|
386
|
+
<Button onClick={() => setShowErrorDialog(false)}>
|
|
387
|
+
关闭
|
|
388
|
+
</Button>
|
|
389
|
+
</DialogFooter>
|
|
390
|
+
</DialogContent>
|
|
391
|
+
</Dialog>
|
|
392
|
+
</div>
|
|
393
|
+
);
|
|
394
|
+
}
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
import Link from 'next/link';
|
|
5
|
+
import { useRouter } from 'next/navigation';
|
|
6
|
+
import {
|
|
7
|
+
ArrowLeft,
|
|
8
|
+
Bot,
|
|
9
|
+
Trash2,
|
|
10
|
+
Plus,
|
|
11
|
+
Package,
|
|
12
|
+
Star,
|
|
13
|
+
Upload,
|
|
14
|
+
Edit
|
|
15
|
+
} from 'lucide-react';
|
|
16
|
+
import { Button } from '@/components/ui/button';
|
|
17
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
18
|
+
import { Badge } from '@/components/ui/badge';
|
|
19
|
+
|
|
20
|
+
interface UserSkill {
|
|
21
|
+
id: string;
|
|
22
|
+
skillName: string;
|
|
23
|
+
source: 'market' | 'builtin' | 'personal';
|
|
24
|
+
useCount: number;
|
|
25
|
+
isBuiltin: boolean;
|
|
26
|
+
installedAt: string;
|
|
27
|
+
lastUsedAt?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default function MySkillsPage() {
|
|
31
|
+
const router = useRouter();
|
|
32
|
+
const [skills, setSkills] = useState<UserSkill[]>([]);
|
|
33
|
+
const [loading, setLoading] = useState(true);
|
|
34
|
+
const [userId, setUserId] = useState<string>('');
|
|
35
|
+
const [uploadingSkill, setUploadingSkill] = useState<string | null>(null);
|
|
36
|
+
const [uploadMessage, setUploadMessage] = useState<{ type: 'success' | 'error', text: string } | null>(null);
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
const uid = localStorage.getItem('userId');
|
|
40
|
+
if (!uid) {
|
|
41
|
+
router.push('/login');
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
setUserId(uid);
|
|
45
|
+
fetchSkills(uid);
|
|
46
|
+
}, [router]);
|
|
47
|
+
|
|
48
|
+
const fetchSkills = async (uid: string) => {
|
|
49
|
+
try {
|
|
50
|
+
const res = await fetch(`/api/user/skills?userId=${uid}`);
|
|
51
|
+
if (res.ok) {
|
|
52
|
+
const data = await res.json();
|
|
53
|
+
setSkills(data.skills || []);
|
|
54
|
+
}
|
|
55
|
+
} catch (error) {
|
|
56
|
+
console.error('Failed to fetch skills:', error);
|
|
57
|
+
} finally {
|
|
58
|
+
setLoading(false);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const handleUninstall = async (skillName: string) => {
|
|
63
|
+
if (!userId) return;
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const res = await fetch('/api/user/skills', {
|
|
67
|
+
method: 'POST',
|
|
68
|
+
headers: { 'Content-Type': 'application/json' },
|
|
69
|
+
body: JSON.stringify({
|
|
70
|
+
userId,
|
|
71
|
+
action: 'uninstall',
|
|
72
|
+
skillName,
|
|
73
|
+
}),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
if (res.ok) {
|
|
77
|
+
setSkills(prev => prev.filter(s => s.skillName !== skillName));
|
|
78
|
+
}
|
|
79
|
+
} catch (error) {
|
|
80
|
+
console.error('Failed to uninstall skill:', error);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const handleUploadToMarket = async (skillName: string) => {
|
|
85
|
+
if (!userId) return;
|
|
86
|
+
|
|
87
|
+
setUploadingSkill(skillName);
|
|
88
|
+
setUploadMessage(null);
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const res = await fetch('/api/marketplace/skills', {
|
|
92
|
+
method: 'POST',
|
|
93
|
+
headers: { 'Content-Type': 'application/json' },
|
|
94
|
+
body: JSON.stringify({
|
|
95
|
+
userId,
|
|
96
|
+
skillName,
|
|
97
|
+
}),
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const data = await res.json();
|
|
101
|
+
|
|
102
|
+
if (res.ok) {
|
|
103
|
+
const message = data.updated ? '技能已更新到市场' : '技能已上传到市场';
|
|
104
|
+
setUploadMessage({ type: 'success', text: message });
|
|
105
|
+
} else {
|
|
106
|
+
setUploadMessage({ type: 'error', text: data.error || '上传失败' });
|
|
107
|
+
}
|
|
108
|
+
} catch (error) {
|
|
109
|
+
console.error('Failed to upload skill:', error);
|
|
110
|
+
setUploadMessage({ type: 'error', text: '上传失败,请稍后重试' });
|
|
111
|
+
} finally {
|
|
112
|
+
setUploadingSkill(null);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const builtinSkills = skills.filter(s => s.isBuiltin);
|
|
117
|
+
const marketSkills = skills.filter(s => s.source === 'market');
|
|
118
|
+
const personalSkills = skills.filter(s => s.source === 'personal');
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<div className="p-6">
|
|
122
|
+
<div className="flex items-center justify-between mb-6">
|
|
123
|
+
<div />
|
|
124
|
+
<div className="flex items-center gap-2">
|
|
125
|
+
<Link href="/my/skills/new">
|
|
126
|
+
<Button size="sm">
|
|
127
|
+
<Plus className="w-4 h-4 mr-2" />
|
|
128
|
+
创建技能
|
|
129
|
+
</Button>
|
|
130
|
+
</Link>
|
|
131
|
+
<Link href="/market/skills">
|
|
132
|
+
<Button variant="outline" size="sm">
|
|
133
|
+
<Package className="w-4 h-4 mr-2" />
|
|
134
|
+
市场
|
|
135
|
+
</Button>
|
|
136
|
+
</Link>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
<div>
|
|
140
|
+
{loading ? (
|
|
141
|
+
<div className="text-center py-12 text-gray-500">加载中...</div>
|
|
142
|
+
) : skills.length === 0 ? (
|
|
143
|
+
<div className="text-center py-12">
|
|
144
|
+
<Bot className="w-12 h-12 mx-auto text-gray-300 mb-4" />
|
|
145
|
+
<p className="text-gray-500 mb-6">还没有安装任何Skill</p>
|
|
146
|
+
<div className="flex items-center justify-center gap-4">
|
|
147
|
+
<Link href="/my/skills/new">
|
|
148
|
+
<Button>
|
|
149
|
+
<Plus className="w-4 h-4 mr-2" />
|
|
150
|
+
创建技能
|
|
151
|
+
</Button>
|
|
152
|
+
</Link>
|
|
153
|
+
<Link href="/market/skills">
|
|
154
|
+
<Button variant="outline">
|
|
155
|
+
<Package className="w-4 h-4 mr-2" />
|
|
156
|
+
去市场安装
|
|
157
|
+
</Button>
|
|
158
|
+
</Link>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
) : (
|
|
162
|
+
<div className="space-y-8">
|
|
163
|
+
{/* 内置Skill */}
|
|
164
|
+
{builtinSkills.length > 0 && (
|
|
165
|
+
<section>
|
|
166
|
+
<h2 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
|
167
|
+
<Star className="w-5 h-5 text-yellow-500" />
|
|
168
|
+
内置Skill
|
|
169
|
+
</h2>
|
|
170
|
+
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
171
|
+
{builtinSkills.map((skill) => (
|
|
172
|
+
<Card key={skill.id}>
|
|
173
|
+
<CardHeader className="pb-3">
|
|
174
|
+
<CardTitle className="text-lg">{skill.skillName}</CardTitle>
|
|
175
|
+
<CardDescription>内置技能,无法卸载</CardDescription>
|
|
176
|
+
</CardHeader>
|
|
177
|
+
<CardContent>
|
|
178
|
+
<div className="flex items-center justify-between text-sm text-gray-500">
|
|
179
|
+
<span>使用 {skill.useCount} 次</span>
|
|
180
|
+
<Badge>内置</Badge>
|
|
181
|
+
</div>
|
|
182
|
+
</CardContent>
|
|
183
|
+
</Card>
|
|
184
|
+
))}
|
|
185
|
+
</div>
|
|
186
|
+
</section>
|
|
187
|
+
)}
|
|
188
|
+
|
|
189
|
+
{/* 市场安装的Skill */}
|
|
190
|
+
{marketSkills.length > 0 && (
|
|
191
|
+
<section>
|
|
192
|
+
<h2 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
|
193
|
+
<Package className="w-5 h-5 text-blue-500" />
|
|
194
|
+
市场Skill ({marketSkills.length})
|
|
195
|
+
</h2>
|
|
196
|
+
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
197
|
+
{marketSkills.map((skill) => (
|
|
198
|
+
<Card key={skill.id}>
|
|
199
|
+
<CardHeader className="pb-3">
|
|
200
|
+
<CardTitle className="text-lg">{skill.skillName}</CardTitle>
|
|
201
|
+
<CardDescription>
|
|
202
|
+
安装于 {skill.installedAt ? new Date(skill.installedAt).toLocaleDateString() : '未知'}
|
|
203
|
+
</CardDescription>
|
|
204
|
+
</CardHeader>
|
|
205
|
+
<CardContent>
|
|
206
|
+
<div className="flex items-center justify-between">
|
|
207
|
+
<span className="text-sm text-gray-500">
|
|
208
|
+
使用 {skill.useCount} 次
|
|
209
|
+
</span>
|
|
210
|
+
<Button
|
|
211
|
+
variant="outline"
|
|
212
|
+
size="sm"
|
|
213
|
+
onClick={() => handleUninstall(skill.skillName)}
|
|
214
|
+
>
|
|
215
|
+
<Trash2 className="w-4 h-4 mr-1" />
|
|
216
|
+
卸载
|
|
217
|
+
</Button>
|
|
218
|
+
</div>
|
|
219
|
+
</CardContent>
|
|
220
|
+
</Card>
|
|
221
|
+
))}
|
|
222
|
+
</div>
|
|
223
|
+
</section>
|
|
224
|
+
)}
|
|
225
|
+
|
|
226
|
+
{/* 个人创建的Skill */}
|
|
227
|
+
{personalSkills.length > 0 && (
|
|
228
|
+
<section>
|
|
229
|
+
<h2 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
|
230
|
+
<Bot className="w-5 h-5 text-green-500" />
|
|
231
|
+
个人Skill ({personalSkills.length})
|
|
232
|
+
</h2>
|
|
233
|
+
{uploadMessage && (
|
|
234
|
+
<div className={`mb-4 p-3 rounded-lg text-sm ${uploadMessage.type === 'success' ? 'bg-green-50 text-green-700 border border-green-200' : 'bg-red-50 text-red-700 border border-red-200'}`}>
|
|
235
|
+
{uploadMessage.text}
|
|
236
|
+
</div>
|
|
237
|
+
)}
|
|
238
|
+
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
239
|
+
{personalSkills.map((skill) => (
|
|
240
|
+
<Card key={skill.id}>
|
|
241
|
+
<CardHeader className="pb-3">
|
|
242
|
+
<CardTitle className="text-lg">{skill.skillName}</CardTitle>
|
|
243
|
+
<CardDescription>
|
|
244
|
+
{skill.installedAt ? `创建于 ${new Date(skill.installedAt).toLocaleDateString()}` : '未知创建时间'}
|
|
245
|
+
</CardDescription>
|
|
246
|
+
</CardHeader>
|
|
247
|
+
<CardContent>
|
|
248
|
+
<div className="flex items-center justify-between mb-2">
|
|
249
|
+
<span className="text-sm text-gray-500">
|
|
250
|
+
使用 {skill.useCount ?? 0} 次
|
|
251
|
+
</span>
|
|
252
|
+
</div>
|
|
253
|
+
<div className="flex gap-2">
|
|
254
|
+
<Button
|
|
255
|
+
variant="outline"
|
|
256
|
+
size="sm"
|
|
257
|
+
className="flex-1"
|
|
258
|
+
asChild
|
|
259
|
+
>
|
|
260
|
+
<Link href={`/my/skills/${skill.skillName}`}>
|
|
261
|
+
<Edit className="w-4 h-4 mr-1" />
|
|
262
|
+
编辑
|
|
263
|
+
</Link>
|
|
264
|
+
</Button>
|
|
265
|
+
<Button
|
|
266
|
+
variant="outline"
|
|
267
|
+
size="sm"
|
|
268
|
+
className="flex-1"
|
|
269
|
+
onClick={() => handleUploadToMarket(skill.skillName)}
|
|
270
|
+
disabled={uploadingSkill === skill.skillName}
|
|
271
|
+
>
|
|
272
|
+
{uploadingSkill === skill.skillName ? (
|
|
273
|
+
<>
|
|
274
|
+
<span className="w-4 h-4 mr-1 animate-spin">⏳</span>
|
|
275
|
+
上传中
|
|
276
|
+
</>
|
|
277
|
+
) : (
|
|
278
|
+
<>
|
|
279
|
+
<Upload className="w-4 h-4 mr-1" />
|
|
280
|
+
上传
|
|
281
|
+
</>
|
|
282
|
+
)}
|
|
283
|
+
</Button>
|
|
284
|
+
<Button
|
|
285
|
+
variant="outline"
|
|
286
|
+
size="sm"
|
|
287
|
+
onClick={() => handleUninstall(skill.skillName)}
|
|
288
|
+
>
|
|
289
|
+
<Trash2 className="w-4 h-4" />
|
|
290
|
+
</Button>
|
|
291
|
+
</div>
|
|
292
|
+
</CardContent>
|
|
293
|
+
</Card>
|
|
294
|
+
))}
|
|
295
|
+
</div>
|
|
296
|
+
</section>
|
|
297
|
+
)}
|
|
298
|
+
</div>
|
|
299
|
+
)}
|
|
300
|
+
</div>
|
|
301
|
+
</div>
|
|
302
|
+
);
|
|
303
|
+
}
|