@mazees/mark 5.0.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/LICENSE +29 -0
- package/README.md +209 -0
- package/bin/mark.js +27 -0
- package/out/renderer/assets/icon-DHo2CYph.svg +1 -0
- package/out/renderer/assets/index-G2fADWO-.js +4768 -0
- package/out/renderer/assets/index-f3mVse3c.css +1 -0
- package/out/renderer/assets/music-cover-CG4vi3kZ.png +0 -0
- package/out/renderer/assets/poppins-devanagari-400-normal-CJDn6rn8.woff2 +0 -0
- package/out/renderer/assets/poppins-devanagari-400-normal-CqVvlrh5.woff +0 -0
- package/out/renderer/assets/poppins-devanagari-500-normal-BIdkeU1p.woff2 +0 -0
- package/out/renderer/assets/poppins-devanagari-500-normal-DMPDjHtT.woff +0 -0
- package/out/renderer/assets/poppins-devanagari-600-normal-ClASKHrr.woff +0 -0
- package/out/renderer/assets/poppins-devanagari-600-normal-STEjXBNN.woff2 +0 -0
- package/out/renderer/assets/poppins-devanagari-700-normal-O-jipLrW.woff2 +0 -0
- package/out/renderer/assets/poppins-devanagari-700-normal-fHs-vx92.woff +0 -0
- package/out/renderer/assets/poppins-latin-400-normal-BOb3E3N0.woff +0 -0
- package/out/renderer/assets/poppins-latin-400-normal-cpxAROuN.woff2 +0 -0
- package/out/renderer/assets/poppins-latin-500-normal-C8OXljZJ.woff2 +0 -0
- package/out/renderer/assets/poppins-latin-500-normal-DGXqpDMm.woff +0 -0
- package/out/renderer/assets/poppins-latin-600-normal-BJdTmd5m.woff +0 -0
- package/out/renderer/assets/poppins-latin-600-normal-zEkxB9Mr.woff2 +0 -0
- package/out/renderer/assets/poppins-latin-700-normal-BVuQR_eA.woff +0 -0
- package/out/renderer/assets/poppins-latin-700-normal-Qrb0O0WB.woff2 +0 -0
- package/out/renderer/assets/poppins-latin-ext-400-normal-DaBSavcJ.woff +0 -0
- package/out/renderer/assets/poppins-latin-ext-400-normal-by3JarPu.woff2 +0 -0
- package/out/renderer/assets/poppins-latin-ext-500-normal-CK-6C4Hw.woff2 +0 -0
- package/out/renderer/assets/poppins-latin-ext-500-normal-CgAe2rWW.woff +0 -0
- package/out/renderer/assets/poppins-latin-ext-600-normal-CAhIAdZj.woff2 +0 -0
- package/out/renderer/assets/poppins-latin-ext-600-normal-Df5ffKXP.woff +0 -0
- package/out/renderer/assets/poppins-latin-ext-700-normal-DctTR6Tg.woff +0 -0
- package/out/renderer/assets/poppins-latin-ext-700-normal-cby-RkWa.woff2 +0 -0
- package/out/renderer/favicon.ico +0 -0
- package/out/renderer/icon-256.png +0 -0
- package/out/renderer/icon.svg +1 -0
- package/out/renderer/index.html +25 -0
- package/out/renderer/manifest.json +25 -0
- package/out/renderer/robot-simulator.html +1640 -0
- package/package.json +106 -0
- package/src/cli/index.js +332 -0
- package/src/cli/monitor.js +413 -0
- package/src/cli/theme.js +347 -0
- package/src/launcher/MarkTray.cs +227 -0
- package/src/main/browser-agent.js +1132 -0
- package/src/main/git-service.js +90 -0
- package/src/main/google/google-calendar.js +69 -0
- package/src/main/google/google-drive.js +263 -0
- package/src/main/google/google-gmail.js +131 -0
- package/src/main/google/google-service.js +241 -0
- package/src/main/node-tools.js +23 -0
- package/src/main/pc-agent-scripts/mouse-locker.ps1 +76 -0
- package/src/main/pc-agent-scripts/ocr-region.ps1 +185 -0
- package/src/main/pc-agent-scripts/pc-daemon.ps1 +726 -0
- package/src/main/pc-agent-scripts/pc-overlay.ps1 +253 -0
- package/src/main/pc-agent-scripts/pick-folder.ps1 +33 -0
- package/src/main/pc-agent-scripts/read-ui.ps1 +181 -0
- package/src/main/pc-agent-scripts/take-screenshot.ps1 +24 -0
- package/src/main/pc-agent-scripts/win-action.ps1 +380 -0
- package/src/main/plugins/plugin-loader.js +306 -0
- package/src/main/skills/skill-manager.js +265 -0
- package/src/main/syntax-validator.js +455 -0
- package/src/main/task-daemon.js +117 -0
- package/src/main/telegram/telegram-service.js +835 -0
- package/src/main/tools/browser-tools.js +516 -0
- package/src/main/tools/file-tools.js +1126 -0
- package/src/main/tools/git-tools.js +79 -0
- package/src/main/tools/google-tools.js +566 -0
- package/src/main/tools/index.js +36 -0
- package/src/main/tools/media-tools.js +42 -0
- package/src/main/tools/system-tools.js +295 -0
- package/src/main/tools/task-tools.js +73 -0
- package/src/main/tools/telegram-tools.js +101 -0
- package/src/renderer/index.html +24 -0
- package/src/renderer/public/favicon.ico +0 -0
- package/src/renderer/public/icon-256.png +0 -0
- package/src/renderer/public/icon.svg +1 -0
- package/src/renderer/public/manifest.json +25 -0
- package/src/renderer/public/robot-simulator.html +1640 -0
- package/src/renderer/src/App.jsx +304 -0
- package/src/renderer/src/api/ai/awareness.js +294 -0
- package/src/renderer/src/api/ai/chatSummarizer.js +79 -0
- package/src/renderer/src/api/ai/contextCompactor.js +129 -0
- package/src/renderer/src/api/ai/contextManager.js +1130 -0
- package/src/renderer/src/api/ai/core.js +167 -0
- package/src/renderer/src/api/ai/index.js +4 -0
- package/src/renderer/src/api/ai/memoryGroomer.js +101 -0
- package/src/renderer/src/api/ai/persona.js +115 -0
- package/src/renderer/src/api/ai/planning.js +317 -0
- package/src/renderer/src/api/ai/relationship.js +177 -0
- package/src/renderer/src/api/ai/skillSynthesizer.js +168 -0
- package/src/renderer/src/api/ai/tools.js +167 -0
- package/src/renderer/src/api/ai/utils.js +359 -0
- package/src/renderer/src/api/db.js +1013 -0
- package/src/renderer/src/api/oramaStore.js +592 -0
- package/src/renderer/src/api/ragPipeline.js +87 -0
- package/src/renderer/src/api/subagent/subagentExecutor.js +780 -0
- package/src/renderer/src/api/subagent/subagentPrompt.js +44 -0
- package/src/renderer/src/api/subagent/subagentStore.js +173 -0
- package/src/renderer/src/api/taskExecutor.js +18 -0
- package/src/renderer/src/api/taskStore.js +315 -0
- package/src/renderer/src/api/tools/core-tools.js +707 -0
- package/src/renderer/src/api/tools/index.js +151 -0
- package/src/renderer/src/api/turnPairMigrator.js +200 -0
- package/src/renderer/src/api/vectorMemory.js +136 -0
- package/src/renderer/src/api/wakeWord.js +101 -0
- package/src/renderer/src/api/web-bridge.js +885 -0
- package/src/renderer/src/api/webSpeech.js +178 -0
- package/src/renderer/src/api/workspaceRag.js +76 -0
- package/src/renderer/src/assets/electron.svg +10 -0
- package/src/renderer/src/assets/icon.svg +1 -0
- package/src/renderer/src/assets/main.css +589 -0
- package/src/renderer/src/assets/music-cover.png +0 -0
- package/src/renderer/src/assets/wavy-lines.svg +25 -0
- package/src/renderer/src/components/Chat/ApprovalBubble.jsx +127 -0
- package/src/renderer/src/components/Chat/CodeBlock.jsx +83 -0
- package/src/renderer/src/components/Chat/DurableTaskBubble.jsx +455 -0
- package/src/renderer/src/components/Chat/FileDiffModal.jsx +328 -0
- package/src/renderer/src/components/Chat/GeminiWidgets.jsx +82 -0
- package/src/renderer/src/components/Chat/MemoryFooterBubble.jsx +34 -0
- package/src/renderer/src/components/Chat/MessageBubble.jsx +459 -0
- package/src/renderer/src/components/Chat/PluginExecutionBubble.jsx +41 -0
- package/src/renderer/src/components/Chat/ThinkingBubble.jsx +316 -0
- package/src/renderer/src/components/Chat/YoutubeSearchBubble.jsx +43 -0
- package/src/renderer/src/components/Chat/YoutubeSummaryBubble.jsx +40 -0
- package/src/renderer/src/components/Chat/index.js +11 -0
- package/src/renderer/src/components/ChatList.jsx +434 -0
- package/src/renderer/src/components/GlobalCameraManager.jsx +67 -0
- package/src/renderer/src/components/Versions.jsx +15 -0
- package/src/renderer/src/components/YoutubeMusicPlayer.jsx +375 -0
- package/src/renderer/src/components/camera/CameraPreview.jsx +236 -0
- package/src/renderer/src/components/config/AiEngineSection.jsx +363 -0
- package/src/renderer/src/components/config/CameraSection.jsx +100 -0
- package/src/renderer/src/components/config/PermissionsSection.jsx +130 -0
- package/src/renderer/src/components/config/ShortcutSection.jsx +102 -0
- package/src/renderer/src/components/config/VoiceSection.jsx +110 -0
- package/src/renderer/src/components/config/index.js +5 -0
- package/src/renderer/src/components/core/Avatar.jsx +2197 -0
- package/src/renderer/src/components/core/ConfirmModal.jsx +80 -0
- package/src/renderer/src/components/core/DraggableHoloCard.jsx +125 -0
- package/src/renderer/src/components/core/FloatingMenu.jsx +272 -0
- package/src/renderer/src/components/core/HoloCard.jsx +64 -0
- package/src/renderer/src/components/core/InputBar.jsx +1346 -0
- package/src/renderer/src/components/core/ProcessPanel.jsx +216 -0
- package/src/renderer/src/components/core/ResponseArea.jsx +149 -0
- package/src/renderer/src/components/core/StatusIndicator.jsx +59 -0
- package/src/renderer/src/components/core/ThoughtNeuralFlow.jsx +168 -0
- package/src/renderer/src/components/core/ToolClustersDeck.jsx +210 -0
- package/src/renderer/src/components/core/native-skills.js +43 -0
- package/src/renderer/src/components/neural-core/EmotionalCortexLobe.jsx +359 -0
- package/src/renderer/src/components/neural-core/NodeDissectionModal.jsx +57 -0
- package/src/renderer/src/components/neural-core/ProceduralMatrixLobe.jsx +193 -0
- package/src/renderer/src/components/neural-core/ProceduralSkillModal.jsx +53 -0
- package/src/renderer/src/components/neural-core/ResetAiModal.jsx +198 -0
- package/src/renderer/src/components/neural-core/SynapticMemoryLobe.jsx +204 -0
- package/src/renderer/src/components/neural-core/traitMeta.js +102 -0
- package/src/renderer/src/components/subagent/AgentChatFeed.jsx +603 -0
- package/src/renderer/src/components/subagent/AgentInspector.jsx +224 -0
- package/src/renderer/src/components/subagent/AgentSidebar.jsx +216 -0
- package/src/renderer/src/components/subagent/SubagentIntercom.jsx +572 -0
- package/src/renderer/src/components/subagent/SubagentTopologyMap.jsx +423 -0
- package/src/renderer/src/contexts/ApprovalContext.jsx +425 -0
- package/src/renderer/src/contexts/ChatContext.jsx +61 -0
- package/src/renderer/src/contexts/YoutubeMusicContext.jsx +333 -0
- package/src/renderer/src/data/faqData.js +202 -0
- package/src/renderer/src/hooks/agent/executeAgentTool.js +895 -0
- package/src/renderer/src/hooks/agent/index.js +4 -0
- package/src/renderer/src/hooks/agent/useMarkMusic.js +75 -0
- package/src/renderer/src/hooks/agent/useMarkPlan.js +2739 -0
- package/src/renderer/src/hooks/agent/useMarkState.js +173 -0
- package/src/renderer/src/hooks/agent/useMarkYoutube.js +82 -0
- package/src/renderer/src/hooks/agent/useRelationalGrowth.js +201 -0
- package/src/renderer/src/hooks/telegram/useTelegramBot.js +63 -0
- package/src/renderer/src/hooks/useAwareness.js +289 -0
- package/src/renderer/src/hooks/useChatArchiver.js +52 -0
- package/src/renderer/src/hooks/useConfirm.jsx +68 -0
- package/src/renderer/src/hooks/useMarkAgent.js +454 -0
- package/src/renderer/src/hooks/useMemoryGroomer.js +152 -0
- package/src/renderer/src/hooks/useVAD.js +511 -0
- package/src/renderer/src/hooks/useWebSpeechWakeWord.js +122 -0
- package/src/renderer/src/main.jsx +12 -0
- package/src/renderer/src/pages/ChatStudio.jsx +922 -0
- package/src/renderer/src/pages/Configuration.jsx +360 -0
- package/src/renderer/src/pages/GoogleWorkspace.jsx +293 -0
- package/src/renderer/src/pages/Guidebook.jsx +917 -0
- package/src/renderer/src/pages/Knowledge.jsx +241 -0
- package/src/renderer/src/pages/MarkHome.jsx +491 -0
- package/src/renderer/src/pages/NeuralCore.jsx +786 -0
- package/src/renderer/src/pages/Plugins.jsx +630 -0
- package/src/renderer/src/pages/SkillEditor.jsx +380 -0
- package/src/renderer/src/pages/Skills.jsx +219 -0
- package/src/renderer/src/pages/Subagents.jsx +342 -0
- package/src/renderer/src/pages/TelegramBot.jsx +343 -0
- package/src/renderer/src/utils/chatContentParser.js +203 -0
- package/src/renderer/src/utils/toolClusters.js +201 -0
- package/src/server/bin/sha3_wasm_bg.wasm +0 -0
- package/src/server/config-manager.js +57 -0
- package/src/server/index.js +195 -0
- package/src/server/launcher.js +87 -0
- package/src/server/memory/db-store.js +758 -0
- package/src/server/memory/orama-store.js +177 -0
- package/src/server/memory/vector-engine.js +109 -0
- package/src/server/routes/ai.routes.js +280 -0
- package/src/server/routes/awareness.routes.js +62 -0
- package/src/server/routes/chat.routes.js +61 -0
- package/src/server/routes/config.routes.js +41 -0
- package/src/server/routes/index.js +35 -0
- package/src/server/routes/integrations.routes.js +116 -0
- package/src/server/routes/memory.routes.js +256 -0
- package/src/server/routes/plugins.routes.js +72 -0
- package/src/server/routes/skills.routes.js +139 -0
- package/src/server/routes/subagents.routes.js +70 -0
- package/src/server/routes/tasks.routes.js +175 -0
- package/src/server/routes/workspace.routes.js +127 -0
- package/src/server/services/ai/ai-utils.js +161 -0
- package/src/server/services/ai/openai-provider.js +757 -0
- package/src/server/services/ai/web-provider.js +729 -0
- package/src/server/services/ai-bridge.js +46 -0
- package/src/server/services/deepseek-web.js +956 -0
- package/src/server/services/gemini-web.js +170 -0
- package/src/server/services/notification-service.js +91 -0
- package/src/server/services/updater.js +125 -0
- package/src/server/tools/awareness-tracker.js +292 -0
- package/src/server/tools/group-tools.js +1164 -0
- package/src/server/tools/media-tools.js +244 -0
- package/src/server/tools/pc-agent.js +327 -0
- package/src/server/tools/screen-service.js +51 -0
- package/src/server/ws-hub.js +168 -0
- package/src/shared/parsers/mark-tag-parser.js +275 -0
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
import { useState, useEffect, useRef } from 'react'
|
|
2
|
+
import { useParams, useNavigate } from 'react-router-dom'
|
|
3
|
+
import { Save, Trash2, Terminal, Folder, File as FileIcon, Plus, FolderPlus, Edit2, ChevronRight, ChevronDown, Heading1, Heading2, Bold, Italic, CheckSquare, Code, Link, Quote, List } from 'lucide-react'
|
|
4
|
+
import Editor from '@monaco-editor/react'
|
|
5
|
+
import { webApi } from '../api/web-bridge'
|
|
6
|
+
|
|
7
|
+
const FileTreeNode = ({ node, level = 0, selectedPath, onSelect, onRename, onDelete, expandedFolders, toggleFolder }) => {
|
|
8
|
+
const isSelected = selectedPath === node.path
|
|
9
|
+
const isExpanded = expandedFolders[node.path]
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<div className="select-none">
|
|
13
|
+
<div
|
|
14
|
+
className={`flex items-center gap-1.5 py-1 px-2 cursor-pointer hover:bg-white/10 transition-colors group ${isSelected ? 'bg-primary/20 text-primary' : 'text-gray-300'}`}
|
|
15
|
+
style={{ paddingLeft: `${level * 12 + 8}px` }}
|
|
16
|
+
onClick={() => {
|
|
17
|
+
if (node.type === 'folder') {
|
|
18
|
+
toggleFolder(node.path)
|
|
19
|
+
} else {
|
|
20
|
+
onSelect(node.path)
|
|
21
|
+
}
|
|
22
|
+
}}
|
|
23
|
+
>
|
|
24
|
+
<div className="flex-shrink-0 w-4 h-4 flex items-center justify-center">
|
|
25
|
+
{node.type === 'folder' ? (
|
|
26
|
+
isExpanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />
|
|
27
|
+
) : (
|
|
28
|
+
<FileIcon size={14} className="opacity-70" />
|
|
29
|
+
)}
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
{node.type === 'folder' && (
|
|
33
|
+
<Folder size={14} className={isExpanded ? 'text-blue-400' : 'text-blue-400/70'} />
|
|
34
|
+
)}
|
|
35
|
+
|
|
36
|
+
<span className="text-sm truncate flex-1">{node.name}</span>
|
|
37
|
+
|
|
38
|
+
<div className="opacity-0 group-hover:opacity-100 flex items-center gap-1">
|
|
39
|
+
<button
|
|
40
|
+
className="p-1 hover:bg-white/20 rounded text-gray-400 hover:text-white transition-colors"
|
|
41
|
+
onClick={(e) => { e.stopPropagation(); onRename(node) }}
|
|
42
|
+
title="Rename"
|
|
43
|
+
>
|
|
44
|
+
<Edit2 size={12} />
|
|
45
|
+
</button>
|
|
46
|
+
<button
|
|
47
|
+
className="p-1 hover:bg-red-500/20 rounded text-gray-400 hover:text-red-400 transition-colors"
|
|
48
|
+
onClick={(e) => { e.stopPropagation(); onDelete(node) }}
|
|
49
|
+
title="Delete"
|
|
50
|
+
>
|
|
51
|
+
<Trash2 size={12} />
|
|
52
|
+
</button>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
{node.type === 'folder' && isExpanded && node.children && (
|
|
57
|
+
<div>
|
|
58
|
+
{node.children.map((child) => (
|
|
59
|
+
<FileTreeNode
|
|
60
|
+
key={child.path}
|
|
61
|
+
node={child}
|
|
62
|
+
level={level + 1}
|
|
63
|
+
selectedPath={selectedPath}
|
|
64
|
+
onSelect={onSelect}
|
|
65
|
+
onRename={onRename}
|
|
66
|
+
onDelete={onDelete}
|
|
67
|
+
expandedFolders={expandedFolders}
|
|
68
|
+
toggleFolder={toggleFolder}
|
|
69
|
+
/>
|
|
70
|
+
))}
|
|
71
|
+
</div>
|
|
72
|
+
)}
|
|
73
|
+
</div>
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const SkillEditor = () => {
|
|
78
|
+
const { id } = useParams()
|
|
79
|
+
const navigate = useNavigate()
|
|
80
|
+
|
|
81
|
+
const [tree, setTree] = useState([])
|
|
82
|
+
const [selectedPath, setSelectedPath] = useState('SKILL.md')
|
|
83
|
+
const [content, setContent] = useState('')
|
|
84
|
+
const [isEditing, setIsEditing] = useState(false)
|
|
85
|
+
const [isLoading, setIsLoading] = useState(true)
|
|
86
|
+
const [expandedFolders, setExpandedFolders] = useState({})
|
|
87
|
+
const editorRef = useRef(null)
|
|
88
|
+
|
|
89
|
+
const handleFormat = (format) => {
|
|
90
|
+
if (!editorRef.current) return
|
|
91
|
+
const editor = editorRef.current
|
|
92
|
+
const selection = editor.getSelection()
|
|
93
|
+
const model = editor.getModel()
|
|
94
|
+
if (!selection || !model) return
|
|
95
|
+
|
|
96
|
+
const selectedText = model.getValueInRange(selection)
|
|
97
|
+
|
|
98
|
+
let textToInsert = ''
|
|
99
|
+
|
|
100
|
+
switch (format) {
|
|
101
|
+
case 'h1': textToInsert = `# ${selectedText || 'Heading 1'}`; break;
|
|
102
|
+
case 'h2': textToInsert = `## ${selectedText || 'Heading 2'}`; break;
|
|
103
|
+
case 'bold': textToInsert = `**${selectedText || 'teks tebal'}**`; break;
|
|
104
|
+
case 'italic': textToInsert = `*${selectedText || 'teks miring'}*`; break;
|
|
105
|
+
case 'check': textToInsert = `- [ ] ${selectedText || 'Tugas baru'}`; break;
|
|
106
|
+
case 'list': textToInsert = `- ${selectedText || 'Item list'}`; break;
|
|
107
|
+
case 'code': textToInsert = `\`\`\`javascript\n${selectedText || '// Tulis kode di sini'}\n\`\`\``; break;
|
|
108
|
+
case 'quote': textToInsert = `> ${selectedText || 'Kutipan'}`; break;
|
|
109
|
+
case 'link': textToInsert = `[${selectedText || 'Teks Link'}](https://url-di-sini)`; break;
|
|
110
|
+
default: return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
editor.executeEdits('toolbar', [{
|
|
114
|
+
range: selection,
|
|
115
|
+
text: textToInsert,
|
|
116
|
+
forceMoveMarkers: true
|
|
117
|
+
}])
|
|
118
|
+
editor.focus()
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const loadTree = async () => {
|
|
122
|
+
try {
|
|
123
|
+
const data = await webApi.getSkillTree(id)
|
|
124
|
+
setTree(data)
|
|
125
|
+
} catch (e) {
|
|
126
|
+
console.error(e)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const loadFileContent = async (path) => {
|
|
131
|
+
setIsLoading(true)
|
|
132
|
+
try {
|
|
133
|
+
const text = await webApi.readSkillFile(id, path)
|
|
134
|
+
setContent(text)
|
|
135
|
+
setSelectedPath(path)
|
|
136
|
+
setIsEditing(false)
|
|
137
|
+
} catch (e) {
|
|
138
|
+
console.error(e)
|
|
139
|
+
} finally {
|
|
140
|
+
setIsLoading(false)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
useEffect(() => {
|
|
145
|
+
loadTree().then(() => loadFileContent('SKILL.md'))
|
|
146
|
+
}, [id])
|
|
147
|
+
|
|
148
|
+
const toggleFolder = (path) => {
|
|
149
|
+
setExpandedFolders(prev => ({ ...prev, [path]: !prev[path] }))
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const handleSave = async () => {
|
|
153
|
+
try {
|
|
154
|
+
await webApi.saveSkillFile(id, selectedPath, content)
|
|
155
|
+
setIsEditing(false)
|
|
156
|
+
} catch (e) {
|
|
157
|
+
console.error(e)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const handleCreateItem = async (isFolder) => {
|
|
162
|
+
const defaultPath = selectedPath.includes('/') ? selectedPath.substring(0, selectedPath.lastIndexOf('/')) : ''
|
|
163
|
+
const itemName = prompt(`Nama ${isFolder ? 'folder' : 'file'} baru:`)
|
|
164
|
+
if (!itemName) return
|
|
165
|
+
|
|
166
|
+
const newPath = defaultPath ? `${defaultPath}/${itemName}` : itemName
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
await webApi.createSkillItem(id, newPath, isFolder)
|
|
170
|
+
await loadTree()
|
|
171
|
+
if (!isFolder) {
|
|
172
|
+
loadFileContent(newPath)
|
|
173
|
+
} else {
|
|
174
|
+
setExpandedFolders(prev => ({ ...prev, [newPath]: true }))
|
|
175
|
+
}
|
|
176
|
+
} catch (e) {
|
|
177
|
+
console.error(e)
|
|
178
|
+
alert('Gagal membuat item')
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const handleDeleteItem = async (node) => {
|
|
183
|
+
if (!confirm(`Hapus ${node.type} "${node.name}"?`)) return
|
|
184
|
+
try {
|
|
185
|
+
await webApi.deleteSkillItem(id, node.path)
|
|
186
|
+
await loadTree()
|
|
187
|
+
if (selectedPath === node.path || selectedPath.startsWith(node.path + '/')) {
|
|
188
|
+
loadFileContent('SKILL.md')
|
|
189
|
+
}
|
|
190
|
+
} catch (e) {
|
|
191
|
+
console.error(e)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const handleRenameItem = async (node) => {
|
|
196
|
+
const newName = prompt('Nama baru:', node.name)
|
|
197
|
+
if (!newName || newName === node.name) return
|
|
198
|
+
|
|
199
|
+
const basePath = node.path.includes('/') ? node.path.substring(0, node.path.lastIndexOf('/')) : ''
|
|
200
|
+
const newPath = basePath ? `${basePath}/${newName}` : newName
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
await webApi.renameSkillItem(id, node.path, newPath)
|
|
204
|
+
await loadTree()
|
|
205
|
+
if (selectedPath === node.path) {
|
|
206
|
+
setSelectedPath(newPath)
|
|
207
|
+
}
|
|
208
|
+
} catch (e) {
|
|
209
|
+
console.error(e)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const handleDeleteSkill = async () => {
|
|
214
|
+
if (!confirm(`Hapus keseluruhan skill ${id} beserta semua filenya?`)) return
|
|
215
|
+
try {
|
|
216
|
+
await webApi.deleteSkill(id)
|
|
217
|
+
navigate('/skills')
|
|
218
|
+
} catch (e) {
|
|
219
|
+
console.error(e)
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return (
|
|
224
|
+
<div className="h-full flex flex-col p-6 overflow-hidden relative font-['Poppins',sans-serif] bg-base-300 rounded-xl border border-white/5 shadow-2xl">
|
|
225
|
+
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_center,oklch(var(--n))_0%,transparent_70%)] opacity-20 pointer-events-none" />
|
|
226
|
+
<div className="absolute inset-0 bg-[url('https://www.transparenttextures.com/patterns/stardust.png')] opacity-10 pointer-events-none" />
|
|
227
|
+
|
|
228
|
+
<div className="relative z-10 flex flex-col h-full">
|
|
229
|
+
{/* Header */}
|
|
230
|
+
<div className="flex items-center justify-between mb-4">
|
|
231
|
+
<div className="flex items-center gap-4">
|
|
232
|
+
<button
|
|
233
|
+
type="button"
|
|
234
|
+
onClick={() => navigate('/skills')}
|
|
235
|
+
className="btn btn-ghost btn-sm btn-circle shrink-0"
|
|
236
|
+
style={{ WebkitAppRegion: 'no-drag' }}
|
|
237
|
+
title="Kembali ke Skills"
|
|
238
|
+
>
|
|
239
|
+
<svg
|
|
240
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
241
|
+
width="1.2em"
|
|
242
|
+
height="1.2em"
|
|
243
|
+
fill="none"
|
|
244
|
+
viewBox="0 0 24 24"
|
|
245
|
+
stroke="currentColor"
|
|
246
|
+
strokeWidth={2}
|
|
247
|
+
>
|
|
248
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
|
|
249
|
+
</svg>
|
|
250
|
+
</button>
|
|
251
|
+
<div>
|
|
252
|
+
<h1 className="text-2xl font-bold flex items-center gap-2">
|
|
253
|
+
<Terminal className="text-emerald-400" size={22} />
|
|
254
|
+
<span>{id}</span>
|
|
255
|
+
</h1>
|
|
256
|
+
<p className="opacity-50 text-xs mt-0.5">Workspace Editor</p>
|
|
257
|
+
</div>
|
|
258
|
+
</div>
|
|
259
|
+
|
|
260
|
+
<div className="flex items-center gap-2">
|
|
261
|
+
<button
|
|
262
|
+
className="btn btn-outline btn-error gap-2"
|
|
263
|
+
onClick={handleDeleteSkill}
|
|
264
|
+
>
|
|
265
|
+
<Trash2 size={16} /> Delete Skill
|
|
266
|
+
</button>
|
|
267
|
+
<button
|
|
268
|
+
className={`btn gap-2 ${isEditing ? 'btn-primary' : 'btn-disabled'}`}
|
|
269
|
+
onClick={handleSave}
|
|
270
|
+
disabled={!isEditing}
|
|
271
|
+
>
|
|
272
|
+
<Save size={16} /> Save Changes
|
|
273
|
+
</button>
|
|
274
|
+
</div>
|
|
275
|
+
</div>
|
|
276
|
+
|
|
277
|
+
{/* IDE Layout */}
|
|
278
|
+
<div className="flex-1 flex gap-4 overflow-hidden">
|
|
279
|
+
|
|
280
|
+
{/* Sidebar: Explorer */}
|
|
281
|
+
<div className="w-64 flex flex-col bg-[#1e1e1e]/80 rounded-2xl border border-white/10 overflow-hidden shadow-2xl">
|
|
282
|
+
<div className="p-3 flex items-center justify-between border-b border-white/5 bg-black/20">
|
|
283
|
+
<span className="text-xs font-bold uppercase tracking-wider text-gray-400">Explorer</span>
|
|
284
|
+
<div className="flex gap-1">
|
|
285
|
+
<button
|
|
286
|
+
className="p-1.5 hover:bg-white/10 rounded-md text-gray-400 transition-colors"
|
|
287
|
+
onClick={() => handleCreateItem(false)}
|
|
288
|
+
title="New File"
|
|
289
|
+
>
|
|
290
|
+
<Plus size={14} />
|
|
291
|
+
</button>
|
|
292
|
+
<button
|
|
293
|
+
className="p-1.5 hover:bg-white/10 rounded-md text-gray-400 transition-colors"
|
|
294
|
+
onClick={() => handleCreateItem(true)}
|
|
295
|
+
title="New Folder"
|
|
296
|
+
>
|
|
297
|
+
<FolderPlus size={14} />
|
|
298
|
+
</button>
|
|
299
|
+
</div>
|
|
300
|
+
</div>
|
|
301
|
+
|
|
302
|
+
<div className="flex-1 overflow-y-auto py-2 custom-scrollbar">
|
|
303
|
+
{tree.map(node => (
|
|
304
|
+
<FileTreeNode
|
|
305
|
+
key={node.path}
|
|
306
|
+
node={node}
|
|
307
|
+
selectedPath={selectedPath}
|
|
308
|
+
onSelect={loadFileContent}
|
|
309
|
+
onRename={handleRenameItem}
|
|
310
|
+
onDelete={handleDeleteItem}
|
|
311
|
+
expandedFolders={expandedFolders}
|
|
312
|
+
toggleFolder={toggleFolder}
|
|
313
|
+
/>
|
|
314
|
+
))}
|
|
315
|
+
</div>
|
|
316
|
+
</div>
|
|
317
|
+
|
|
318
|
+
{/* Editor Container */}
|
|
319
|
+
<div className="flex-1 flex flex-col bg-[#1e1e1e] rounded-2xl border border-white/10 overflow-hidden shadow-2xl relative">
|
|
320
|
+
<div className="bg-black/20 border-b border-white/5 px-4 py-2 flex items-center justify-between">
|
|
321
|
+
<div className="flex items-center gap-2">
|
|
322
|
+
<FileIcon size={14} className="text-gray-400" />
|
|
323
|
+
<span className="text-sm text-gray-300 font-mono">{selectedPath}</span>
|
|
324
|
+
{isEditing && <span className="w-2 h-2 rounded-full bg-primary ml-2"></span>}
|
|
325
|
+
</div>
|
|
326
|
+
|
|
327
|
+
{selectedPath.endsWith('.md') && (
|
|
328
|
+
<div className="flex items-center gap-1 bg-black/30 rounded-lg p-1 border border-white/5">
|
|
329
|
+
<button onClick={() => handleFormat('h1')} className="p-1.5 hover:bg-white/10 rounded text-gray-400 hover:text-white transition-colors" title="Heading 1"><Heading1 size={14} /></button>
|
|
330
|
+
<button onClick={() => handleFormat('h2')} className="p-1.5 hover:bg-white/10 rounded text-gray-400 hover:text-white transition-colors" title="Heading 2"><Heading2 size={14} /></button>
|
|
331
|
+
<div className="w-px h-4 bg-white/10 mx-1"></div>
|
|
332
|
+
<button onClick={() => handleFormat('bold')} className="p-1.5 hover:bg-white/10 rounded text-gray-400 hover:text-white transition-colors" title="Bold"><Bold size={14} /></button>
|
|
333
|
+
<button onClick={() => handleFormat('italic')} className="p-1.5 hover:bg-white/10 rounded text-gray-400 hover:text-white transition-colors" title="Italic"><Italic size={14} /></button>
|
|
334
|
+
<div className="w-px h-4 bg-white/10 mx-1"></div>
|
|
335
|
+
<button onClick={() => handleFormat('check')} className="p-1.5 hover:bg-white/10 rounded text-gray-400 hover:text-white transition-colors" title="Checklist"><CheckSquare size={14} /></button>
|
|
336
|
+
<button onClick={() => handleFormat('list')} className="p-1.5 hover:bg-white/10 rounded text-gray-400 hover:text-white transition-colors" title="Bulleted List"><List size={14} /></button>
|
|
337
|
+
<div className="w-px h-4 bg-white/10 mx-1"></div>
|
|
338
|
+
<button onClick={() => handleFormat('code')} className="p-1.5 hover:bg-white/10 rounded text-gray-400 hover:text-white transition-colors" title="Code Block"><Code size={14} /></button>
|
|
339
|
+
<button onClick={() => handleFormat('link')} className="p-1.5 hover:bg-white/10 rounded text-gray-400 hover:text-white transition-colors" title="Link"><Link size={14} /></button>
|
|
340
|
+
<button onClick={() => handleFormat('quote')} className="p-1.5 hover:bg-white/10 rounded text-gray-400 hover:text-white transition-colors" title="Quote"><Quote size={14} /></button>
|
|
341
|
+
</div>
|
|
342
|
+
)}
|
|
343
|
+
</div>
|
|
344
|
+
|
|
345
|
+
<div className="flex-1 relative">
|
|
346
|
+
{isLoading ? (
|
|
347
|
+
<div className="flex items-center justify-center h-full text-gray-500">
|
|
348
|
+
Loading editor...
|
|
349
|
+
</div>
|
|
350
|
+
) : (
|
|
351
|
+
<Editor
|
|
352
|
+
height="100%"
|
|
353
|
+
defaultLanguage={selectedPath.endsWith('.md') ? 'markdown' : selectedPath.endsWith('.json') ? 'json' : 'javascript'}
|
|
354
|
+
theme="vs-dark"
|
|
355
|
+
value={content}
|
|
356
|
+
onMount={(editor) => { editorRef.current = editor }}
|
|
357
|
+
onChange={(val) => {
|
|
358
|
+
setContent(val || '')
|
|
359
|
+
setIsEditing(true)
|
|
360
|
+
}}
|
|
361
|
+
options={{
|
|
362
|
+
minimap: { enabled: false },
|
|
363
|
+
fontSize: 14,
|
|
364
|
+
wordWrap: 'on',
|
|
365
|
+
lineNumbers: 'on',
|
|
366
|
+
padding: { top: 16 },
|
|
367
|
+
fontFamily: "'Fira Code', 'JetBrains Mono', monospace",
|
|
368
|
+
}}
|
|
369
|
+
/>
|
|
370
|
+
)}
|
|
371
|
+
</div>
|
|
372
|
+
</div>
|
|
373
|
+
|
|
374
|
+
</div>
|
|
375
|
+
</div>
|
|
376
|
+
</div>
|
|
377
|
+
)
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export default SkillEditor
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { useState, useEffect, useRef } from 'react'
|
|
2
|
+
import { Plus, FileText, Upload, RefreshCw } from 'lucide-react'
|
|
3
|
+
import { useNavigate } from 'react-router-dom'
|
|
4
|
+
import { webApi } from '../api/web-bridge'
|
|
5
|
+
|
|
6
|
+
const Skills = () => {
|
|
7
|
+
const [skills, setSkills] = useState([])
|
|
8
|
+
const [newSkillName, setNewSkillName] = useState('')
|
|
9
|
+
const [isRefreshing, setIsRefreshing] = useState(false)
|
|
10
|
+
const fileInputRef = useRef(null)
|
|
11
|
+
const navigate = useNavigate()
|
|
12
|
+
|
|
13
|
+
const loadSkills = async () => {
|
|
14
|
+
try {
|
|
15
|
+
setIsRefreshing(true)
|
|
16
|
+
const list = await webApi.getSkills()
|
|
17
|
+
setSkills(list)
|
|
18
|
+
} catch (e) {
|
|
19
|
+
console.error(e)
|
|
20
|
+
} finally {
|
|
21
|
+
setTimeout(() => setIsRefreshing(false), 500)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
loadSkills()
|
|
27
|
+
const unsub = webApi.onSkillsUpdated(() => {
|
|
28
|
+
loadSkills()
|
|
29
|
+
})
|
|
30
|
+
return () => {
|
|
31
|
+
if (typeof unsub === 'function') unsub()
|
|
32
|
+
}
|
|
33
|
+
}, [])
|
|
34
|
+
|
|
35
|
+
const handleCreateNew = async () => {
|
|
36
|
+
if (!newSkillName.trim()) return
|
|
37
|
+
const name = newSkillName.trim().replace(/\s+/g, '-').toLowerCase()
|
|
38
|
+
try {
|
|
39
|
+
const template = `---
|
|
40
|
+
name: ${name}
|
|
41
|
+
description: Tulis deskripsi singkat tentang skill ini...
|
|
42
|
+
author: User
|
|
43
|
+
tags: []
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
# Skill: ${name}
|
|
47
|
+
|
|
48
|
+
Tulis instruksi mendetail untuk AI di sini...
|
|
49
|
+
- Gunakan format checklist [ ] jika ada langkah-langkah.
|
|
50
|
+
- Tambahkan blok Critical Rules untuk batasan mutlak.
|
|
51
|
+
`
|
|
52
|
+
await webApi.saveSkillFile(name, 'SKILL.md', template)
|
|
53
|
+
setNewSkillName('')
|
|
54
|
+
navigate(`/skill-editor/${name}`)
|
|
55
|
+
} catch (e) {
|
|
56
|
+
console.error(e)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const handleFileSelected = async (e) => {
|
|
61
|
+
const file = e.target.files?.[0]
|
|
62
|
+
if (!file) return
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
setIsRefreshing(true)
|
|
66
|
+
const result = await webApi.installSkill(file)
|
|
67
|
+
if (result.success) {
|
|
68
|
+
await loadSkills()
|
|
69
|
+
} else {
|
|
70
|
+
alert(result.error || 'Gagal menginstal skill package.')
|
|
71
|
+
}
|
|
72
|
+
} catch (err) {
|
|
73
|
+
console.error(err)
|
|
74
|
+
alert('Gagal mengunggah dan menginstal skill: ' + err.message)
|
|
75
|
+
} finally {
|
|
76
|
+
if (fileInputRef.current) fileInputRef.current.value = ''
|
|
77
|
+
setIsRefreshing(false)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<div className="h-screen bg-base-300 text-base-content overflow-hidden relative font-['Poppins',sans-serif]">
|
|
83
|
+
{/* Hidden File Input for ZIP Upload */}
|
|
84
|
+
<input
|
|
85
|
+
type="file"
|
|
86
|
+
ref={fileInputRef}
|
|
87
|
+
onChange={handleFileSelected}
|
|
88
|
+
accept=".zip"
|
|
89
|
+
className="hidden"
|
|
90
|
+
/>
|
|
91
|
+
|
|
92
|
+
{/* Background Ambience */}
|
|
93
|
+
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_center,oklch(var(--n))_0%,transparent_70%)] opacity-20 pointer-events-none" />
|
|
94
|
+
<div className="absolute inset-0 bg-[url('https://www.transparenttextures.com/patterns/stardust.png')] opacity-10 pointer-events-none" />
|
|
95
|
+
|
|
96
|
+
{/* Main Content Area */}
|
|
97
|
+
<div className="relative z-10 w-full h-full overflow-y-auto custom-scrollbar">
|
|
98
|
+
<div className="max-w-2xl mx-auto px-4 py-8 pb-32 space-y-8">
|
|
99
|
+
{/* Page Header */}
|
|
100
|
+
<div className="flex items-center justify-between">
|
|
101
|
+
<div className="flex items-center gap-4">
|
|
102
|
+
<button
|
|
103
|
+
type="button"
|
|
104
|
+
onClick={() => navigate('/')}
|
|
105
|
+
className="btn btn-ghost btn-sm btn-circle shrink-0"
|
|
106
|
+
style={{ WebkitAppRegion: 'no-drag' }}
|
|
107
|
+
title="Kembali"
|
|
108
|
+
>
|
|
109
|
+
<svg
|
|
110
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
111
|
+
width="1.2em"
|
|
112
|
+
height="1.2em"
|
|
113
|
+
fill="none"
|
|
114
|
+
viewBox="0 0 24 24"
|
|
115
|
+
stroke="currentColor"
|
|
116
|
+
strokeWidth={2}
|
|
117
|
+
>
|
|
118
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
|
|
119
|
+
</svg>
|
|
120
|
+
</button>
|
|
121
|
+
<div>
|
|
122
|
+
<h1 className="text-2xl font-bold">Mark Skills</h1>
|
|
123
|
+
<p className="opacity-50 text-sm mt-1">
|
|
124
|
+
Daftar kemampuan kustom AI yang diinstal via Markdown.
|
|
125
|
+
</p>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
<div className="flex items-center gap-2">
|
|
129
|
+
<button
|
|
130
|
+
className="btn btn-ghost btn-sm btn-circle"
|
|
131
|
+
onClick={loadSkills}
|
|
132
|
+
style={{ WebkitAppRegion: 'no-drag' }}
|
|
133
|
+
title="Refresh Skills"
|
|
134
|
+
>
|
|
135
|
+
<RefreshCw size={16} className={isRefreshing ? 'animate-spin' : ''} />
|
|
136
|
+
</button>
|
|
137
|
+
<button
|
|
138
|
+
className="btn btn-outline btn-success btn-sm gap-2"
|
|
139
|
+
onClick={() => fileInputRef.current?.click()}
|
|
140
|
+
style={{ WebkitAppRegion: 'no-drag' }}
|
|
141
|
+
>
|
|
142
|
+
<Upload size={14} /> Install Skill (.zip)
|
|
143
|
+
</button>
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
|
|
147
|
+
{/* Form Create New */}
|
|
148
|
+
<section className="space-y-5">
|
|
149
|
+
<div className="card bg-base-100/50 backdrop-blur-xl border border-base-content/10 shadow-xl">
|
|
150
|
+
<div className="card-body">
|
|
151
|
+
<h2 className="card-title text-base font-bold uppercase tracking-wider opacity-70">
|
|
152
|
+
Buat Skill Baru
|
|
153
|
+
</h2>
|
|
154
|
+
<div className="flex gap-2 mt-2">
|
|
155
|
+
<input
|
|
156
|
+
type="text"
|
|
157
|
+
placeholder="Nama skill (cth: copywriter)..."
|
|
158
|
+
className="input input-bordered w-full bg-base-200/50"
|
|
159
|
+
value={newSkillName}
|
|
160
|
+
onChange={(e) => setNewSkillName(e.target.value)}
|
|
161
|
+
onKeyDown={(e) => e.key === 'Enter' && handleCreateNew()}
|
|
162
|
+
/>
|
|
163
|
+
<button className="btn btn-primary gap-2" onClick={handleCreateNew}>
|
|
164
|
+
<Plus size={16} /> Buat
|
|
165
|
+
</button>
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
</section>
|
|
170
|
+
|
|
171
|
+
{/* Grid List */}
|
|
172
|
+
<section className="space-y-5">
|
|
173
|
+
<h2 className="text-base font-bold uppercase tracking-wider opacity-70">
|
|
174
|
+
Installed Skills
|
|
175
|
+
</h2>
|
|
176
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
177
|
+
{skills.map((skillObj) => {
|
|
178
|
+
const targetKey = skillObj.folderName || skillObj.name
|
|
179
|
+
return (
|
|
180
|
+
<div
|
|
181
|
+
key={targetKey}
|
|
182
|
+
className="card bg-base-100/50 backdrop-blur-xl border border-base-content/10 shadow-xl hover:border-primary/30 transition-all cursor-pointer group"
|
|
183
|
+
onClick={() => navigate(`/skill-editor/${targetKey}`)}
|
|
184
|
+
>
|
|
185
|
+
<div className="card-body p-5">
|
|
186
|
+
<div className="flex items-start gap-4">
|
|
187
|
+
<div className="p-3 bg-base-200 rounded-xl group-hover:text-primary transition-colors">
|
|
188
|
+
<FileText size={24} />
|
|
189
|
+
</div>
|
|
190
|
+
<div className="flex-1 overflow-hidden">
|
|
191
|
+
<h3 className="font-semibold text-lg truncate group-hover:text-primary transition-colors">
|
|
192
|
+
{skillObj.name}
|
|
193
|
+
</h3>
|
|
194
|
+
<p className="text-xs opacity-70 mt-1 line-clamp-2">
|
|
195
|
+
{skillObj.description}
|
|
196
|
+
</p>
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
)
|
|
202
|
+
})}
|
|
203
|
+
|
|
204
|
+
{skills.length === 0 && (
|
|
205
|
+
<div className="col-span-full flex flex-col items-center justify-center p-12 text-base-content/50 border border-dashed border-base-content/10 rounded-2xl bg-base-100/20">
|
|
206
|
+
<FileText size={48} className="opacity-20 mb-4" />
|
|
207
|
+
<p className="text-lg font-medium">Belum ada skill.</p>
|
|
208
|
+
<p className="text-sm opacity-60">Install dari file .zip atau buat baru di atas!</p>
|
|
209
|
+
</div>
|
|
210
|
+
)}
|
|
211
|
+
</div>
|
|
212
|
+
</section>
|
|
213
|
+
</div>
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export default Skills
|