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,193 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { Save, Loader2 } from 'lucide-react';
|
|
5
|
+
import { Button } from '@/components/ui/button';
|
|
6
|
+
import { Input } from '@/components/ui/input';
|
|
7
|
+
import {
|
|
8
|
+
Dialog,
|
|
9
|
+
DialogContent,
|
|
10
|
+
DialogDescription,
|
|
11
|
+
DialogHeader,
|
|
12
|
+
DialogTitle,
|
|
13
|
+
DialogTrigger,
|
|
14
|
+
} from '@/components/ui/dialog';
|
|
15
|
+
import {
|
|
16
|
+
Select,
|
|
17
|
+
SelectContent,
|
|
18
|
+
SelectItem,
|
|
19
|
+
SelectTrigger,
|
|
20
|
+
SelectValue,
|
|
21
|
+
} from '@/components/ui/select';
|
|
22
|
+
import { KNOWLEDGE_TYPE_LABELS } from '@/lib/knowledge-types';
|
|
23
|
+
|
|
24
|
+
interface Message {
|
|
25
|
+
id: string;
|
|
26
|
+
role: 'user' | 'assistant' | 'system';
|
|
27
|
+
content: string;
|
|
28
|
+
timestamp: Date;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface SaveToKnowledgeDialogProps {
|
|
32
|
+
messages: Message[];
|
|
33
|
+
sessionId?: string | null;
|
|
34
|
+
onSuccess?: () => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function SaveToKnowledgeDialog({ messages, sessionId, onSuccess }: SaveToKnowledgeDialogProps) {
|
|
38
|
+
const [open, setOpen] = useState(false);
|
|
39
|
+
const [title, setTitle] = useState('');
|
|
40
|
+
const [category, setCategory] = useState('会话总结');
|
|
41
|
+
const [saving, setSaving] = useState(false);
|
|
42
|
+
|
|
43
|
+
// 生成会话摘要
|
|
44
|
+
const generateSummary = () => {
|
|
45
|
+
const userMessages = messages.filter(m => m.role === 'user');
|
|
46
|
+
const assistantMessages = messages.filter(m => m.role === 'assistant');
|
|
47
|
+
|
|
48
|
+
let summary = '# 会话总结\n\n';
|
|
49
|
+
|
|
50
|
+
// 添加主要话题
|
|
51
|
+
if (userMessages.length > 0) {
|
|
52
|
+
summary += '## 主要讨论\n\n';
|
|
53
|
+
userMessages.slice(0, 5).forEach((m, i) => {
|
|
54
|
+
summary += `${i + 1}. ${m.content.slice(0, 100)}${m.content.length > 100 ? '...' : ''}\n`;
|
|
55
|
+
});
|
|
56
|
+
summary += '\n';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 添加关键回复
|
|
60
|
+
if (assistantMessages.length > 0) {
|
|
61
|
+
summary += '## 关键内容\n\n';
|
|
62
|
+
assistantMessages.slice(0, 3).forEach((m, i) => {
|
|
63
|
+
summary += `### 回复 ${i + 1}\n\n${m.content.slice(0, 500)}${m.content.length > 500 ? '...' : ''}\n\n`;
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return summary;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const handleSave = async () => {
|
|
71
|
+
if (!title.trim()) {
|
|
72
|
+
alert('请输入知识标题');
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
setSaving(true);
|
|
77
|
+
try {
|
|
78
|
+
const content = generateSummary();
|
|
79
|
+
|
|
80
|
+
const response = await fetch('/api/knowledge', {
|
|
81
|
+
method: 'POST',
|
|
82
|
+
headers: { 'Content-Type': 'application/json' },
|
|
83
|
+
body: JSON.stringify({
|
|
84
|
+
title: title.trim(),
|
|
85
|
+
content,
|
|
86
|
+
category,
|
|
87
|
+
type: 'conversation-summary',
|
|
88
|
+
sourceType: 'conversation-summary',
|
|
89
|
+
sessionId,
|
|
90
|
+
isActive: true,
|
|
91
|
+
author: 'user',
|
|
92
|
+
}),
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
if (response.ok) {
|
|
96
|
+
setTitle('');
|
|
97
|
+
setOpen(false);
|
|
98
|
+
onSuccess?.();
|
|
99
|
+
} else {
|
|
100
|
+
const data = await response.json();
|
|
101
|
+
alert(data.error || '保存失败');
|
|
102
|
+
}
|
|
103
|
+
} catch (error) {
|
|
104
|
+
console.error('Failed to save knowledge:', error);
|
|
105
|
+
alert('保存失败');
|
|
106
|
+
} finally {
|
|
107
|
+
setSaving(false);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const hasMessages = messages.length > 0;
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
115
|
+
<DialogTrigger asChild>
|
|
116
|
+
<Button
|
|
117
|
+
variant="outline"
|
|
118
|
+
size="sm"
|
|
119
|
+
disabled={!hasMessages}
|
|
120
|
+
className="h-8"
|
|
121
|
+
title="保存会话到知识库"
|
|
122
|
+
>
|
|
123
|
+
<Save className="h-4 w-4 mr-1" />
|
|
124
|
+
保存为知识
|
|
125
|
+
</Button>
|
|
126
|
+
</DialogTrigger>
|
|
127
|
+
<DialogContent className="sm:max-w-md">
|
|
128
|
+
<DialogHeader>
|
|
129
|
+
<DialogTitle>保存会话到知识库</DialogTitle>
|
|
130
|
+
<DialogDescription>
|
|
131
|
+
将当前对话内容总结并保存为知识条目,保存后可在AI会话中被引用。
|
|
132
|
+
</DialogDescription>
|
|
133
|
+
</DialogHeader>
|
|
134
|
+
|
|
135
|
+
<div className="space-y-4 py-4">
|
|
136
|
+
<div className="space-y-2">
|
|
137
|
+
<label className="text-sm font-medium">标题</label>
|
|
138
|
+
<Input
|
|
139
|
+
placeholder="例如:Docker容器管理经验"
|
|
140
|
+
value={title}
|
|
141
|
+
onChange={(e) => setTitle(e.target.value)}
|
|
142
|
+
/>
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
<div className="space-y-2">
|
|
146
|
+
<label className="text-sm font-medium">分类</label>
|
|
147
|
+
<Select value={category} onValueChange={setCategory}>
|
|
148
|
+
<SelectTrigger>
|
|
149
|
+
<SelectValue placeholder="选择分类" />
|
|
150
|
+
</SelectTrigger>
|
|
151
|
+
<SelectContent>
|
|
152
|
+
<SelectItem value="会话总结">会话总结</SelectItem>
|
|
153
|
+
<SelectItem value="运维">运维</SelectItem>
|
|
154
|
+
<SelectItem value="开发">开发</SelectItem>
|
|
155
|
+
<SelectItem value="排查">排查</SelectItem>
|
|
156
|
+
<SelectItem value="最佳实践">最佳实践</SelectItem>
|
|
157
|
+
<SelectItem value="其他">其他</SelectItem>
|
|
158
|
+
</SelectContent>
|
|
159
|
+
</Select>
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
<div className="bg-muted/50 rounded-lg p-3 text-sm text-muted-foreground">
|
|
163
|
+
<p className="font-medium mb-1">将保存以下内容:</p>
|
|
164
|
+
<ul className="list-disc list-inside space-y-1 text-xs">
|
|
165
|
+
<li>用户提问摘要(最多5条)</li>
|
|
166
|
+
<li>AI回复要点(最多3条)</li>
|
|
167
|
+
<li>会话ID:{sessionId || '未分配'}</li>
|
|
168
|
+
</ul>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
<div className="flex justify-end gap-2">
|
|
173
|
+
<Button variant="outline" onClick={() => setOpen(false)}>
|
|
174
|
+
取消
|
|
175
|
+
</Button>
|
|
176
|
+
<Button onClick={handleSave} disabled={saving || !title.trim()}>
|
|
177
|
+
{saving ? (
|
|
178
|
+
<>
|
|
179
|
+
<Loader2 className="h-4 w-4 mr-1 animate-spin" />
|
|
180
|
+
保存中...
|
|
181
|
+
</>
|
|
182
|
+
) : (
|
|
183
|
+
<>
|
|
184
|
+
<Save className="h-4 w-4 mr-1" />
|
|
185
|
+
保存
|
|
186
|
+
</>
|
|
187
|
+
)}
|
|
188
|
+
</Button>
|
|
189
|
+
</div>
|
|
190
|
+
</DialogContent>
|
|
191
|
+
</Dialog>
|
|
192
|
+
);
|
|
193
|
+
}
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
4
|
+
import { ChevronDown, X, Plus, Sparkles } from 'lucide-react';
|
|
5
|
+
|
|
6
|
+
interface SkillMetadata {
|
|
7
|
+
name: string;
|
|
8
|
+
displayName?: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
version?: string;
|
|
11
|
+
author?: string;
|
|
12
|
+
keywords?: string[];
|
|
13
|
+
scriptCount: number;
|
|
14
|
+
apiDocCount: number;
|
|
15
|
+
isBuiltIn: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface SkillSelectorProps {
|
|
19
|
+
selectedSkills: string[];
|
|
20
|
+
onSkillsChange: (skills: string[]) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function SkillSelector({ selectedSkills, onSkillsChange }: SkillSelectorProps) {
|
|
24
|
+
const [availableSkills, setAvailableSkills] = useState<SkillMetadata[]>([]);
|
|
25
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
26
|
+
const [loading, setLoading] = useState(true);
|
|
27
|
+
const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0, width: 0 });
|
|
28
|
+
const buttonRef = useRef<HTMLButtonElement>(null);
|
|
29
|
+
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
async function loadSkills() {
|
|
33
|
+
try {
|
|
34
|
+
const response = await fetch('/api/skills');
|
|
35
|
+
if (response.ok) {
|
|
36
|
+
const skills = await response.json();
|
|
37
|
+
setAvailableSkills(skills);
|
|
38
|
+
}
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.error('Failed to load skills:', error);
|
|
41
|
+
} finally {
|
|
42
|
+
setLoading(false);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
loadSkills();
|
|
46
|
+
}, []);
|
|
47
|
+
|
|
48
|
+
const updatePosition = useCallback(() => {
|
|
49
|
+
if (buttonRef.current && isOpen) {
|
|
50
|
+
const rect = buttonRef.current.getBoundingClientRect();
|
|
51
|
+
const viewportHeight = window.innerHeight;
|
|
52
|
+
const dropdownHeight = 420;
|
|
53
|
+
const spaceBelow = viewportHeight - rect.bottom;
|
|
54
|
+
const spaceAbove = rect.top;
|
|
55
|
+
|
|
56
|
+
let top = rect.bottom + 8;
|
|
57
|
+
if (spaceBelow < dropdownHeight && spaceAbove > spaceBelow) {
|
|
58
|
+
top = rect.top - 8 - dropdownHeight;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
setDropdownPosition({
|
|
62
|
+
top: Math.max(8, top),
|
|
63
|
+
left: Math.max(8, Math.min(rect.left, window.innerWidth - 400)),
|
|
64
|
+
width: Math.min(384, window.innerWidth - 16),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}, [isOpen]);
|
|
68
|
+
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (isOpen) {
|
|
71
|
+
updatePosition();
|
|
72
|
+
window.addEventListener('resize', updatePosition);
|
|
73
|
+
window.addEventListener('scroll', updatePosition, true);
|
|
74
|
+
}
|
|
75
|
+
return () => {
|
|
76
|
+
window.removeEventListener('resize', updatePosition);
|
|
77
|
+
window.removeEventListener('scroll', updatePosition, true);
|
|
78
|
+
};
|
|
79
|
+
}, [isOpen, updatePosition]);
|
|
80
|
+
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
function handleClickOutside(event: MouseEvent) {
|
|
83
|
+
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node) &&
|
|
84
|
+
!buttonRef.current?.contains(event.target as Node)) {
|
|
85
|
+
setIsOpen(false);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
89
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
90
|
+
}, []);
|
|
91
|
+
|
|
92
|
+
const getDisplayName = (skill: SkillMetadata): string => {
|
|
93
|
+
return skill.displayName || skill.name;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const getSelectedSkillObjects = (): SkillMetadata[] => {
|
|
97
|
+
return availableSkills.filter(s => selectedSkills.includes(s.name));
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const toggleSkill = (skillName: string) => {
|
|
101
|
+
if (selectedSkills.includes(skillName)) {
|
|
102
|
+
onSkillsChange(selectedSkills.filter(s => s !== skillName));
|
|
103
|
+
} else {
|
|
104
|
+
onSkillsChange([...selectedSkills, skillName]);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const selectedSkillObjects = getSelectedSkillObjects();
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<div className="relative" ref={dropdownRef}>
|
|
112
|
+
{selectedSkillObjects.length > 0 && (
|
|
113
|
+
<div className="flex flex-wrap items-center gap-2 mb-3 p-3 bg-gradient-to-r from-purple-50 to-indigo-50 rounded-lg border border-purple-100">
|
|
114
|
+
<span className="text-xs font-semibold text-purple-700 flex items-center gap-1">
|
|
115
|
+
<Sparkles className="w-3.5 h-3.5" />
|
|
116
|
+
已激活:
|
|
117
|
+
</span>
|
|
118
|
+
|
|
119
|
+
{selectedSkillObjects.map(skill => (
|
|
120
|
+
<span
|
|
121
|
+
key={skill.name}
|
|
122
|
+
className="inline-flex items-center gap-1.5 px-3 py-1 bg-white text-purple-700 rounded-full text-sm font-medium shadow-sm border border-purple-200"
|
|
123
|
+
>
|
|
124
|
+
{getDisplayName(skill)}
|
|
125
|
+
<button
|
|
126
|
+
onClick={() => toggleSkill(skill.name)}
|
|
127
|
+
className="ml-1 hover:text-purple-900 transition-colors"
|
|
128
|
+
title="移除"
|
|
129
|
+
>
|
|
130
|
+
<X className="w-3.5 h-3.5" />
|
|
131
|
+
</button>
|
|
132
|
+
</span>
|
|
133
|
+
))}
|
|
134
|
+
|
|
135
|
+
<button
|
|
136
|
+
onClick={() => onSkillsChange([])}
|
|
137
|
+
className="text-xs text-purple-500 hover:text-purple-700 ml-2"
|
|
138
|
+
>
|
|
139
|
+
清除全部
|
|
140
|
+
</button>
|
|
141
|
+
</div>
|
|
142
|
+
)}
|
|
143
|
+
|
|
144
|
+
<div className="flex items-center justify-between gap-3">
|
|
145
|
+
<div className="flex items-center gap-2">
|
|
146
|
+
<button
|
|
147
|
+
ref={buttonRef}
|
|
148
|
+
onClick={() => setIsOpen(!isOpen)}
|
|
149
|
+
className="flex items-center gap-2 px-4 py-2.5 bg-white border border-gray-200 rounded-lg hover:border-purple-300 hover:shadow-sm hover:bg-purple-50/50 transition-all duration-200"
|
|
150
|
+
>
|
|
151
|
+
<span className="text-lg">🔧</span>
|
|
152
|
+
<span className="text-sm font-medium text-gray-700">Skill</span>
|
|
153
|
+
{selectedSkills.length > 0 && (
|
|
154
|
+
<span className="px-2 py-0.5 bg-purple-100 text-purple-700 text-xs font-semibold rounded-full">
|
|
155
|
+
{selectedSkills.length}
|
|
156
|
+
</span>
|
|
157
|
+
)}
|
|
158
|
+
<ChevronDown className={`w-4 h-4 text-gray-400 transition-transform ${isOpen ? 'rotate-180' : ''}`} />
|
|
159
|
+
</button>
|
|
160
|
+
|
|
161
|
+
{availableSkills.length > 0 && (
|
|
162
|
+
<button
|
|
163
|
+
onClick={() => {
|
|
164
|
+
console.log('智能推荐功能待实现');
|
|
165
|
+
}}
|
|
166
|
+
className="p-2.5 text-gray-400 hover:text-purple-600 hover:bg-purple-50 rounded-lg transition-colors"
|
|
167
|
+
title="智能推荐 Skill"
|
|
168
|
+
>
|
|
169
|
+
<Sparkles className="w-5 h-5" />
|
|
170
|
+
</button>
|
|
171
|
+
)}
|
|
172
|
+
</div>
|
|
173
|
+
|
|
174
|
+
<a
|
|
175
|
+
href="/skills"
|
|
176
|
+
target="_blank"
|
|
177
|
+
className="flex items-center gap-1 text-sm text-gray-400 hover:text-purple-600 transition-colors"
|
|
178
|
+
>
|
|
179
|
+
<Plus className="w-4 h-4" />
|
|
180
|
+
<span>管理 Skill</span>
|
|
181
|
+
</a>
|
|
182
|
+
</div>
|
|
183
|
+
|
|
184
|
+
{isOpen && (
|
|
185
|
+
<>
|
|
186
|
+
<div
|
|
187
|
+
className="fixed inset-0 z-40"
|
|
188
|
+
onClick={() => setIsOpen(false)}
|
|
189
|
+
/>
|
|
190
|
+
<div
|
|
191
|
+
ref={dropdownRef}
|
|
192
|
+
className="fixed bg-white border border-gray-200 rounded-xl shadow-2xl z-50 overflow-hidden"
|
|
193
|
+
style={{
|
|
194
|
+
top: dropdownPosition.top,
|
|
195
|
+
left: dropdownPosition.left,
|
|
196
|
+
width: dropdownPosition.width,
|
|
197
|
+
maxHeight: 'calc(100vh - 16px)',
|
|
198
|
+
}}
|
|
199
|
+
>
|
|
200
|
+
<div className="px-4 py-3 bg-gray-50 border-b border-gray-100">
|
|
201
|
+
<h3 className="text-sm font-semibold text-gray-900">
|
|
202
|
+
选择要激活的 Skill
|
|
203
|
+
</h3>
|
|
204
|
+
<p className="text-xs text-gray-500 mt-0.5">
|
|
205
|
+
选中的 Skill 将被注入到 AI 上下文中,帮助处理特定任务
|
|
206
|
+
</p>
|
|
207
|
+
</div>
|
|
208
|
+
|
|
209
|
+
{loading ? (
|
|
210
|
+
<div className="p-8 text-center">
|
|
211
|
+
<div className="inline-flex items-center gap-2 text-gray-500">
|
|
212
|
+
<svg className="animate-spin h-5 w-5" viewBox="0 0 24 24">
|
|
213
|
+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none" />
|
|
214
|
+
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
|
|
215
|
+
</svg>
|
|
216
|
+
<span className="text-sm">加载中...</span>
|
|
217
|
+
</div>
|
|
218
|
+
</div>
|
|
219
|
+
) : (
|
|
220
|
+
<div className="overflow-y-auto" style={{ maxHeight: 'calc(100vh - 180px)' }}>
|
|
221
|
+
{availableSkills.length === 0 ? (
|
|
222
|
+
<div className="p-6 text-center">
|
|
223
|
+
<p className="text-sm text-gray-500">暂无可用的 Skill</p>
|
|
224
|
+
<a
|
|
225
|
+
href="/skills/new"
|
|
226
|
+
className="inline-flex items-center gap-1 mt-2 text-sm text-purple-600 hover:text-purple-700"
|
|
227
|
+
>
|
|
228
|
+
<Plus className="w-4 h-4" />
|
|
229
|
+
创建第一个 Skill
|
|
230
|
+
</a>
|
|
231
|
+
</div>
|
|
232
|
+
) : (
|
|
233
|
+
availableSkills.map(skill => {
|
|
234
|
+
const isSelected = selectedSkills.includes(skill.name);
|
|
235
|
+
return (
|
|
236
|
+
<label
|
|
237
|
+
key={skill.name}
|
|
238
|
+
className={`flex items-start gap-3 px-4 py-3 cursor-pointer transition-colors ${
|
|
239
|
+
isSelected ? 'bg-purple-50' : 'hover:bg-gray-50'
|
|
240
|
+
}`}
|
|
241
|
+
>
|
|
242
|
+
<input
|
|
243
|
+
type="checkbox"
|
|
244
|
+
checked={isSelected}
|
|
245
|
+
onChange={() => toggleSkill(skill.name)}
|
|
246
|
+
className="mt-0.5 rounded border-gray-300 text-purple-600 focus:ring-purple-500"
|
|
247
|
+
/>
|
|
248
|
+
<div className="flex-1 min-w-0">
|
|
249
|
+
<div className="flex items-center gap-2">
|
|
250
|
+
<span className="font-medium text-sm text-gray-900">
|
|
251
|
+
{getDisplayName(skill)}
|
|
252
|
+
</span>
|
|
253
|
+
{skill.isBuiltIn && (
|
|
254
|
+
<span className="text-xs bg-gray-100 text-gray-600 px-1.5 py-0.5 rounded">
|
|
255
|
+
内置
|
|
256
|
+
</span>
|
|
257
|
+
)}
|
|
258
|
+
{skill.version && (
|
|
259
|
+
<span className="text-xs text-gray-400">
|
|
260
|
+
v{skill.version}
|
|
261
|
+
</span>
|
|
262
|
+
)}
|
|
263
|
+
</div>
|
|
264
|
+
<p className="text-xs text-gray-500 mt-0.5 line-clamp-2">
|
|
265
|
+
{skill.description}
|
|
266
|
+
</p>
|
|
267
|
+
{(skill.scriptCount > 0 || skill.apiDocCount > 0) && (
|
|
268
|
+
<div className="flex items-center gap-2 mt-1">
|
|
269
|
+
{skill.scriptCount > 0 && (
|
|
270
|
+
<span className="text-xs bg-gray-100 text-gray-600 px-1.5 py-0.5 rounded">
|
|
271
|
+
{skill.scriptCount} 脚本
|
|
272
|
+
</span>
|
|
273
|
+
)}
|
|
274
|
+
{skill.apiDocCount > 0 && (
|
|
275
|
+
<span className="text-xs bg-gray-100 text-gray-600 px-1.5 py-0.5 rounded">
|
|
276
|
+
{skill.apiDocCount} API
|
|
277
|
+
</span>
|
|
278
|
+
)}
|
|
279
|
+
</div>
|
|
280
|
+
)}
|
|
281
|
+
</div>
|
|
282
|
+
</label>
|
|
283
|
+
);
|
|
284
|
+
})
|
|
285
|
+
)}
|
|
286
|
+
</div>
|
|
287
|
+
)}
|
|
288
|
+
|
|
289
|
+
<div className="px-4 py-3 bg-gray-50 border-t border-gray-100 flex items-center justify-between">
|
|
290
|
+
<span className="text-xs text-gray-500">
|
|
291
|
+
{selectedSkills.length} / {availableSkills.length} 已选择
|
|
292
|
+
</span>
|
|
293
|
+
<button
|
|
294
|
+
onClick={() => setIsOpen(false)}
|
|
295
|
+
className="px-4 py-1.5 text-sm text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors"
|
|
296
|
+
>
|
|
297
|
+
确定
|
|
298
|
+
</button>
|
|
299
|
+
</div>
|
|
300
|
+
</div>
|
|
301
|
+
</>
|
|
302
|
+
)}
|
|
303
|
+
</div>
|
|
304
|
+
);
|
|
305
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
4
|
+
import { ChevronDown, Sparkles } from 'lucide-react';
|
|
5
|
+
|
|
6
|
+
interface Skill {
|
|
7
|
+
name: string;
|
|
8
|
+
displayName?: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface SkillSwitcherProps {
|
|
13
|
+
skills: Skill[];
|
|
14
|
+
selectedSkills: string[];
|
|
15
|
+
onSkillsChange: (skills: string[]) => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function SkillSwitcher({ skills, selectedSkills, onSkillsChange }: SkillSwitcherProps) {
|
|
19
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
20
|
+
const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0 });
|
|
21
|
+
const buttonRef = useRef<HTMLButtonElement>(null);
|
|
22
|
+
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
23
|
+
|
|
24
|
+
const getDisplayName = (skill: Skill) => skill.displayName || skill.name;
|
|
25
|
+
|
|
26
|
+
const currentSkill = selectedSkills.length === 1
|
|
27
|
+
? skills.find(s => s.name === selectedSkills[0])
|
|
28
|
+
: null;
|
|
29
|
+
|
|
30
|
+
const updatePosition = useCallback(() => {
|
|
31
|
+
if (buttonRef.current && isOpen) {
|
|
32
|
+
const rect = buttonRef.current.getBoundingClientRect();
|
|
33
|
+
setDropdownPosition({
|
|
34
|
+
top: rect.bottom + 4,
|
|
35
|
+
left: Math.min(rect.left, window.innerWidth - 300),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}, [isOpen]);
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (isOpen) {
|
|
42
|
+
updatePosition();
|
|
43
|
+
window.addEventListener('resize', updatePosition);
|
|
44
|
+
window.addEventListener('scroll', updatePosition, true);
|
|
45
|
+
}
|
|
46
|
+
return () => {
|
|
47
|
+
window.removeEventListener('resize', updatePosition);
|
|
48
|
+
window.removeEventListener('scroll', updatePosition, true);
|
|
49
|
+
};
|
|
50
|
+
}, [isOpen, updatePosition]);
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
function handleClickOutside(event: MouseEvent) {
|
|
54
|
+
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node) &&
|
|
55
|
+
!buttonRef.current?.contains(event.target as Node)) {
|
|
56
|
+
setIsOpen(false);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
60
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
61
|
+
}, []);
|
|
62
|
+
|
|
63
|
+
const toggleSkill = (skillName: string) => {
|
|
64
|
+
if (selectedSkills.includes(skillName)) {
|
|
65
|
+
onSkillsChange(selectedSkills.filter(s => s !== skillName));
|
|
66
|
+
} else {
|
|
67
|
+
onSkillsChange([skillName]);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<div className="relative">
|
|
73
|
+
<button
|
|
74
|
+
ref={buttonRef}
|
|
75
|
+
onClick={() => setIsOpen(!isOpen)}
|
|
76
|
+
className="flex items-center gap-1.5 px-3 py-1.5 bg-purple-50 hover:bg-purple-100 border border-purple-200 rounded-lg transition-colors"
|
|
77
|
+
>
|
|
78
|
+
<Sparkles className="w-4 h-4 text-purple-600" />
|
|
79
|
+
<span className="font-medium text-sm text-purple-900">
|
|
80
|
+
{currentSkill ? getDisplayName(currentSkill) : '默认助手'}
|
|
81
|
+
</span>
|
|
82
|
+
{selectedSkills.length > 1 && (
|
|
83
|
+
<span className="text-xs bg-purple-200 text-purple-700 px-1.5 py-0.5 rounded-full">
|
|
84
|
+
{selectedSkills.length}
|
|
85
|
+
</span>
|
|
86
|
+
)}
|
|
87
|
+
<ChevronDown className={`w-3.5 h-3.5 text-purple-400 transition-transform ${isOpen ? 'rotate-180' : ''}`} />
|
|
88
|
+
</button>
|
|
89
|
+
|
|
90
|
+
{isOpen && (
|
|
91
|
+
<>
|
|
92
|
+
<div
|
|
93
|
+
className="fixed inset-0 z-40"
|
|
94
|
+
onClick={() => setIsOpen(false)}
|
|
95
|
+
/>
|
|
96
|
+
<div
|
|
97
|
+
ref={dropdownRef}
|
|
98
|
+
className="fixed bg-white border border-gray-200 rounded-lg shadow-lg z-50 w-64 overflow-hidden"
|
|
99
|
+
style={{
|
|
100
|
+
top: dropdownPosition.top,
|
|
101
|
+
left: dropdownPosition.left,
|
|
102
|
+
}}
|
|
103
|
+
>
|
|
104
|
+
<div className="px-3 py-2 bg-gray-50 border-b border-gray-100">
|
|
105
|
+
<span className="text-xs font-medium text-gray-600">选择对话 Skill</span>
|
|
106
|
+
</div>
|
|
107
|
+
<div className="max-h-64 overflow-y-auto">
|
|
108
|
+
<button
|
|
109
|
+
onClick={() => {
|
|
110
|
+
onSkillsChange([]);
|
|
111
|
+
setIsOpen(false);
|
|
112
|
+
}}
|
|
113
|
+
className={`w-full px-3 py-2 text-left flex items-center gap-2 hover:bg-gray-50 transition-colors ${
|
|
114
|
+
selectedSkills.length === 0 ? 'bg-purple-50' : ''
|
|
115
|
+
}`}
|
|
116
|
+
>
|
|
117
|
+
<span className="w-6 h-6 rounded bg-gray-100 flex items-center justify-center text-gray-500 text-xs">🤖</span>
|
|
118
|
+
<div>
|
|
119
|
+
<div className="font-medium text-sm text-gray-900">默认助手</div>
|
|
120
|
+
<div className="text-xs text-gray-500">默认对话模式</div>
|
|
121
|
+
</div>
|
|
122
|
+
{selectedSkills.length === 0 && (
|
|
123
|
+
<span className="ml-auto text-purple-600 text-xs">✓</span>
|
|
124
|
+
)}
|
|
125
|
+
</button>
|
|
126
|
+
{skills.map(skill => {
|
|
127
|
+
const isSelected = selectedSkills.includes(skill.name);
|
|
128
|
+
return (
|
|
129
|
+
<button
|
|
130
|
+
key={skill.name}
|
|
131
|
+
onClick={() => {
|
|
132
|
+
toggleSkill(skill.name);
|
|
133
|
+
setIsOpen(false);
|
|
134
|
+
}}
|
|
135
|
+
className={`w-full px-3 py-2 text-left flex items-center gap-2 hover:bg-gray-50 transition-colors ${
|
|
136
|
+
isSelected ? 'bg-purple-50' : ''
|
|
137
|
+
}`}
|
|
138
|
+
>
|
|
139
|
+
<span className="w-6 h-6 rounded bg-purple-100 flex items-center justify-center text-purple-600 text-xs font-medium">
|
|
140
|
+
{getDisplayName(skill).charAt(0).toUpperCase()}
|
|
141
|
+
</span>
|
|
142
|
+
<div className="min-w-0 flex-1">
|
|
143
|
+
<div className="font-medium text-sm text-gray-900 truncate">{getDisplayName(skill)}</div>
|
|
144
|
+
{skill.description && (
|
|
145
|
+
<div className="text-xs text-gray-500 truncate">{skill.description}</div>
|
|
146
|
+
)}
|
|
147
|
+
</div>
|
|
148
|
+
{isSelected && (
|
|
149
|
+
<span className="ml-auto text-purple-600 text-xs">✓</span>
|
|
150
|
+
)}
|
|
151
|
+
</button>
|
|
152
|
+
);
|
|
153
|
+
})}
|
|
154
|
+
</div>
|
|
155
|
+
<div className="px-3 py-2 bg-gray-50 border-t border-gray-100 text-xs text-gray-400">
|
|
156
|
+
点击切换当前对话的 Skill
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
</>
|
|
160
|
+
)}
|
|
161
|
+
</div>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { usePathname } from 'next/navigation';
|
|
4
|
+
import { LayoutWrapper } from '@/components/layout-wrapper';
|
|
5
|
+
|
|
6
|
+
export function ClientLayout({ children }: { children: React.ReactNode }) {
|
|
7
|
+
const pathname = usePathname() || '';
|
|
8
|
+
|
|
9
|
+
// 移动端路径不显示全局导航
|
|
10
|
+
if (pathname === '/h5' || pathname.startsWith('/h5/')) {
|
|
11
|
+
return <>{children}</>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return <LayoutWrapper>{children}</LayoutWrapper>;
|
|
15
|
+
}
|