@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,630 @@
|
|
|
1
|
+
import Editor from '@monaco-editor/react'
|
|
2
|
+
import { driver } from 'driver.js'
|
|
3
|
+
import { FaTimes, FaExclamationTriangle } from 'react-icons/fa'
|
|
4
|
+
import { useNavigate } from 'react-router-dom'
|
|
5
|
+
import 'driver.js/dist/driver.css'
|
|
6
|
+
import { useEffect, useState } from 'react'
|
|
7
|
+
import { useConfirm } from '../hooks/useConfirm'
|
|
8
|
+
import { webApi } from '../api/web-bridge'
|
|
9
|
+
|
|
10
|
+
export default function Plugins() {
|
|
11
|
+
const navigate = useNavigate()
|
|
12
|
+
const [plugins, setPlugins] = useState([])
|
|
13
|
+
const [isFormOpen, setIsFormOpen] = useState(false)
|
|
14
|
+
const [formData, setFormData] = useState({
|
|
15
|
+
name: '',
|
|
16
|
+
description: '',
|
|
17
|
+
actions: [{ name: '', description: '', triggerHint: '', code: '' }],
|
|
18
|
+
isEdit: false
|
|
19
|
+
})
|
|
20
|
+
const [formStatus, setFormStatus] = useState(null)
|
|
21
|
+
const [syntaxErrors, setSyntaxErrors] = useState([])
|
|
22
|
+
const { confirm, ModalComponent } = useConfirm()
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
const errors = []
|
|
26
|
+
formData.actions.forEach((act, index) => {
|
|
27
|
+
if (act.code) {
|
|
28
|
+
try {
|
|
29
|
+
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor
|
|
30
|
+
new AsyncFunction('query', act.code)
|
|
31
|
+
errors[index] = null
|
|
32
|
+
} catch (err) {
|
|
33
|
+
errors[index] = err.message
|
|
34
|
+
}
|
|
35
|
+
} else {
|
|
36
|
+
errors[index] = null
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
setSyntaxErrors(errors)
|
|
40
|
+
}, [formData.actions])
|
|
41
|
+
|
|
42
|
+
const updateAction = (index, field, value) => {
|
|
43
|
+
const newActions = [...formData.actions]
|
|
44
|
+
newActions[index][field] = value
|
|
45
|
+
setFormData({ ...formData, actions: newActions })
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const addAction = () => {
|
|
49
|
+
setFormData({
|
|
50
|
+
...formData,
|
|
51
|
+
actions: [...formData.actions, { name: '', description: '', triggerHint: '', code: '' }]
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const removeAction = async (index) => {
|
|
56
|
+
if (formData.actions.length <= 1) return
|
|
57
|
+
const result = await confirm({
|
|
58
|
+
title: 'Yakin mau hapus action ini?',
|
|
59
|
+
message: 'Code yang ada di action ini bakal hilang!',
|
|
60
|
+
isError: true,
|
|
61
|
+
confirmText: 'Ya, Hapus!',
|
|
62
|
+
cancelText: 'Batal'
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
if (result.isConfirmed) {
|
|
66
|
+
const newActions = formData.actions.filter((_, i) => i !== index)
|
|
67
|
+
setFormData({ ...formData, actions: newActions })
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const loadData = async () => {
|
|
72
|
+
const data = await webApi.getPlugins()
|
|
73
|
+
setPlugins(data || [])
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
loadData()
|
|
78
|
+
const unsub = webApi.onPluginsUpdated(() => {
|
|
79
|
+
loadData()
|
|
80
|
+
})
|
|
81
|
+
return () => {
|
|
82
|
+
if (typeof unsub === 'function') unsub()
|
|
83
|
+
}
|
|
84
|
+
}, [])
|
|
85
|
+
|
|
86
|
+
const handleOpenFolder = async () => {
|
|
87
|
+
await webApi.openPluginFolder()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const handleReload = async () => {
|
|
91
|
+
await webApi.reloadPlugins()
|
|
92
|
+
loadData()
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const startTour = () => {
|
|
96
|
+
const driverObj = driver({
|
|
97
|
+
showProgress: true,
|
|
98
|
+
animate: true,
|
|
99
|
+
nextBtnText: 'Lanjut',
|
|
100
|
+
prevBtnText: 'Kembali',
|
|
101
|
+
doneBtnText: 'Siap Ngoding!',
|
|
102
|
+
steps: [
|
|
103
|
+
{
|
|
104
|
+
element: '#btn-tambah-plugin',
|
|
105
|
+
popover: {
|
|
106
|
+
title: '1. Buat Plugin',
|
|
107
|
+
description:
|
|
108
|
+
'Klik tombol ini untuk membuat fungsi/plugin baru. Fitur baru yang kamu tulis di sini bakal otomatis dipahami oleh Mark.',
|
|
109
|
+
side: 'bottom',
|
|
110
|
+
align: 'start'
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
element: '#btn-buka-folder',
|
|
115
|
+
popover: {
|
|
116
|
+
title: '2. Folder Fisik',
|
|
117
|
+
description:
|
|
118
|
+
'Plugin yang kamu buat disimpan dalam file .js sungguhan. Kamu bisa klik ini untuk ngedit file-filenya pakai VS Code secara manual.',
|
|
119
|
+
side: 'bottom'
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
element: '#btn-reload-plugin',
|
|
124
|
+
popover: {
|
|
125
|
+
title: '3. Reload Plugin',
|
|
126
|
+
description:
|
|
127
|
+
'Kalau kamu abis ngedit file plugin secara manual lewat VS Code, tekan tombol ini supaya Mark memuat ulang versi terbarunya.',
|
|
128
|
+
side: 'bottom'
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
]
|
|
132
|
+
})
|
|
133
|
+
driverObj.drive()
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const openCreateForm = () => {
|
|
137
|
+
setFormData({
|
|
138
|
+
name: '',
|
|
139
|
+
description: '',
|
|
140
|
+
dependencies: '',
|
|
141
|
+
actions: [{ name: '', description: '', triggerHint: '', code: '' }],
|
|
142
|
+
isEdit: false
|
|
143
|
+
})
|
|
144
|
+
setFormStatus(null)
|
|
145
|
+
setIsFormOpen(true)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const openEditForm = (plugin) => {
|
|
149
|
+
setFormData({
|
|
150
|
+
name: plugin.name,
|
|
151
|
+
description: plugin.description,
|
|
152
|
+
dependencies: plugin.dependencies ? plugin.dependencies.join(', ') : '',
|
|
153
|
+
actions: plugin.actions.map((act) => ({
|
|
154
|
+
name: act.name,
|
|
155
|
+
description: act.description,
|
|
156
|
+
triggerHint: act.triggerHint,
|
|
157
|
+
code: act.code || '// Kode tidak ditemukan\nreturn "Hello";'
|
|
158
|
+
})),
|
|
159
|
+
isEdit: true
|
|
160
|
+
})
|
|
161
|
+
setFormStatus(null)
|
|
162
|
+
setIsFormOpen(true)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return (
|
|
166
|
+
<div className="h-screen bg-base-300 text-white overflow-hidden relative font-['Poppins',sans-serif]">
|
|
167
|
+
{/* Background Ambience */}
|
|
168
|
+
<ModalComponent />
|
|
169
|
+
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_center,oklch(var(--n))_0%,transparent_70%)] opacity-20 pointer-events-none" />
|
|
170
|
+
<div className="absolute inset-0 bg-[url('https://www.transparenttextures.com/patterns/stardust.png')] opacity-10 pointer-events-none" />
|
|
171
|
+
|
|
172
|
+
{/* Main Content Area */}
|
|
173
|
+
<div className="relative z-10 w-full h-full overflow-y-auto custom-scrollbar">
|
|
174
|
+
<div className="max-w-5xl mx-auto px-4 py-8 pb-40 space-y-8">
|
|
175
|
+
<div className="flex items-center justify-between">
|
|
176
|
+
<div className="flex items-center gap-4">
|
|
177
|
+
<button
|
|
178
|
+
type="button"
|
|
179
|
+
onClick={() => navigate('/')}
|
|
180
|
+
className="btn btn-ghost btn-sm btn-circle shrink-0"
|
|
181
|
+
style={{ WebkitAppRegion: 'no-drag' }}
|
|
182
|
+
title="Kembali"
|
|
183
|
+
>
|
|
184
|
+
<svg
|
|
185
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
186
|
+
width="1.2em"
|
|
187
|
+
height="1.2em"
|
|
188
|
+
fill="none"
|
|
189
|
+
viewBox="0 0 24 24"
|
|
190
|
+
stroke="currentColor"
|
|
191
|
+
strokeWidth={2}
|
|
192
|
+
>
|
|
193
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
|
|
194
|
+
</svg>
|
|
195
|
+
</button>
|
|
196
|
+
<div>
|
|
197
|
+
<h1 className="text-2xl font-bold">Plugin Manager</h1>
|
|
198
|
+
<p className="opacity-50 text-sm mt-1">Buat dan kelola custom skill lokal buat Mark.</p>
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
<div className="flex items-center gap-2">
|
|
202
|
+
<button
|
|
203
|
+
type="button"
|
|
204
|
+
onClick={startTour}
|
|
205
|
+
className="btn btn-ghost btn-sm btn-circle"
|
|
206
|
+
style={{ WebkitAppRegion: 'no-drag' }}
|
|
207
|
+
title="Tutorial Plugin"
|
|
208
|
+
>
|
|
209
|
+
<svg
|
|
210
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
211
|
+
className="h-5 w-5"
|
|
212
|
+
fill="none"
|
|
213
|
+
viewBox="0 0 24 24"
|
|
214
|
+
stroke="currentColor"
|
|
215
|
+
>
|
|
216
|
+
<path
|
|
217
|
+
strokeLinecap="round"
|
|
218
|
+
strokeLinejoin="round"
|
|
219
|
+
strokeWidth={2}
|
|
220
|
+
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
221
|
+
/>
|
|
222
|
+
</svg>
|
|
223
|
+
</button>
|
|
224
|
+
<button
|
|
225
|
+
id="btn-tambah-plugin"
|
|
226
|
+
type="button"
|
|
227
|
+
onClick={openCreateForm}
|
|
228
|
+
className="btn btn-primary btn-sm"
|
|
229
|
+
style={{ WebkitAppRegion: 'no-drag' }}
|
|
230
|
+
>
|
|
231
|
+
+ Tambah Plugin
|
|
232
|
+
</button>
|
|
233
|
+
<button
|
|
234
|
+
id="btn-buka-folder"
|
|
235
|
+
type="button"
|
|
236
|
+
onClick={handleOpenFolder}
|
|
237
|
+
className="btn btn-sm btn-outline border-base-content/20 hover:border-primary hover:bg-primary/10 hover:text-primary"
|
|
238
|
+
style={{ WebkitAppRegion: 'no-drag' }}
|
|
239
|
+
>
|
|
240
|
+
Buka Folder
|
|
241
|
+
</button>
|
|
242
|
+
<button
|
|
243
|
+
id="btn-reload-plugin"
|
|
244
|
+
type="button"
|
|
245
|
+
onClick={handleReload}
|
|
246
|
+
className="btn btn-sm btn-ghost btn-outline"
|
|
247
|
+
style={{ WebkitAppRegion: 'no-drag' }}
|
|
248
|
+
>
|
|
249
|
+
Reload
|
|
250
|
+
</button>
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
|
|
254
|
+
{isFormOpen && (
|
|
255
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4">
|
|
256
|
+
<div className="bg-base-100 w-full max-w-5xl max-h-[90vh] rounded-2xl shadow-2xl flex flex-col overflow-hidden border border-base-content/10">
|
|
257
|
+
<div className="p-6 border-b border-base-content/10 flex justify-between items-center bg-base-200/50">
|
|
258
|
+
<h2 className="text-2xl font-bold">
|
|
259
|
+
{formData.isEdit ? 'Edit Plugin' : 'Buat Plugin Baru'}
|
|
260
|
+
</h2>
|
|
261
|
+
<button
|
|
262
|
+
className="btn btn-sm btn-circle btn-ghost"
|
|
263
|
+
onClick={() => setIsFormOpen(false)}
|
|
264
|
+
>
|
|
265
|
+
<FaTimes />
|
|
266
|
+
</button>
|
|
267
|
+
</div>
|
|
268
|
+
|
|
269
|
+
<div className="p-6 overflow-y-auto flex-1">
|
|
270
|
+
{formStatus && (
|
|
271
|
+
<div
|
|
272
|
+
className={`alert mb-6 py-3 shadow-sm ${formStatus.success ? 'alert-success' : 'alert-error'}`}
|
|
273
|
+
>
|
|
274
|
+
{formStatus.message}
|
|
275
|
+
</div>
|
|
276
|
+
)}
|
|
277
|
+
|
|
278
|
+
<div className="grid grid-cols-1 gap-6">
|
|
279
|
+
<div className="flex gap-4 p-4 bg-base-200/30 rounded-xl border border-base-content/10">
|
|
280
|
+
<div className="form-control flex-1">
|
|
281
|
+
<label className="label font-semibold">
|
|
282
|
+
<span className="label-text">Nama Plugin</span>
|
|
283
|
+
</label>
|
|
284
|
+
<input
|
|
285
|
+
type="text"
|
|
286
|
+
className="input input-bordered w-full bg-base-200/50 focus:bg-base-100 transition-colors"
|
|
287
|
+
placeholder="Cth: Kalkulator"
|
|
288
|
+
value={formData.name}
|
|
289
|
+
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
|
290
|
+
disabled={formData.isEdit}
|
|
291
|
+
/>
|
|
292
|
+
{formData.isEdit && (
|
|
293
|
+
<span className="text-xs opacity-50 mt-1">
|
|
294
|
+
Nama plugin tidak bisa diubah saat edit
|
|
295
|
+
</span>
|
|
296
|
+
)}
|
|
297
|
+
</div>
|
|
298
|
+
<div className="form-control flex-1">
|
|
299
|
+
<label className="label font-semibold">
|
|
300
|
+
<span className="label-text">Deskripsi Plugin</span>
|
|
301
|
+
</label>
|
|
302
|
+
<input
|
|
303
|
+
type="text"
|
|
304
|
+
className="input input-bordered w-full bg-base-200/50 focus:bg-base-100 transition-colors"
|
|
305
|
+
placeholder="Cth: Kumpulan alat matematika"
|
|
306
|
+
value={formData.description}
|
|
307
|
+
onChange={(e) =>
|
|
308
|
+
setFormData({ ...formData, description: e.target.value })
|
|
309
|
+
}
|
|
310
|
+
/>
|
|
311
|
+
</div>
|
|
312
|
+
</div>
|
|
313
|
+
<div className="form-control w-full p-4 bg-base-200/30 rounded-xl border border-base-content/10">
|
|
314
|
+
<label className="label font-semibold">
|
|
315
|
+
<span className="label-text">Dependencies (Library NPM)</span>
|
|
316
|
+
<span className="label-text-alt opacity-70">Opsional</span>
|
|
317
|
+
</label>
|
|
318
|
+
<input
|
|
319
|
+
type="text"
|
|
320
|
+
className="input input-bordered w-full bg-base-200/50 focus:bg-base-100 transition-colors"
|
|
321
|
+
placeholder="Cth: axios, cheerio, moment"
|
|
322
|
+
value={formData.dependencies}
|
|
323
|
+
onChange={(e) => setFormData({ ...formData, dependencies: e.target.value })}
|
|
324
|
+
/>
|
|
325
|
+
<span className="text-xs opacity-50 mt-2">
|
|
326
|
+
Pisahkan dengan koma. Library akan otomatis di-install (npm install) saat
|
|
327
|
+
disimpan.
|
|
328
|
+
</span>
|
|
329
|
+
</div>
|
|
330
|
+
|
|
331
|
+
<div className="flex items-center justify-between mt-4">
|
|
332
|
+
<h3 className="text-lg font-bold">Daftar Actions</h3>
|
|
333
|
+
<button className="btn btn-sm btn-outline btn-accent" onClick={addAction}>
|
|
334
|
+
+ Tambah Action
|
|
335
|
+
</button>
|
|
336
|
+
</div>
|
|
337
|
+
|
|
338
|
+
{formData.actions.map((act, idx) => (
|
|
339
|
+
<div
|
|
340
|
+
key={idx}
|
|
341
|
+
className="relative p-6 bg-base-200/30 rounded-xl border border-base-content/10 grid grid-cols-1 md:grid-cols-2 gap-6 group"
|
|
342
|
+
>
|
|
343
|
+
{formData.actions.length > 1 && (
|
|
344
|
+
<button
|
|
345
|
+
className="btn btn-xs btn-error btn-outline transition-all absolute top-5 right-5 transition-opacity z-20"
|
|
346
|
+
onClick={() => removeAction(idx)}
|
|
347
|
+
title="Hapus Action"
|
|
348
|
+
>
|
|
349
|
+
DELETE
|
|
350
|
+
</button>
|
|
351
|
+
)}
|
|
352
|
+
|
|
353
|
+
<div className="space-y-4">
|
|
354
|
+
<div className="form-control w-full">
|
|
355
|
+
<label className="label font-semibold">
|
|
356
|
+
<span className="label-text">
|
|
357
|
+
Deskripsi Action{' '}
|
|
358
|
+
<span className="opacity-50 text-xs">#{idx + 1}</span>
|
|
359
|
+
</span>
|
|
360
|
+
</label>
|
|
361
|
+
<input
|
|
362
|
+
type="text"
|
|
363
|
+
className="input input-bordered w-full bg-base-200/50"
|
|
364
|
+
placeholder="Cth: Menjumlahkan dua angka"
|
|
365
|
+
value={act.description}
|
|
366
|
+
onChange={(e) => updateAction(idx, 'description', e.target.value)}
|
|
367
|
+
/>
|
|
368
|
+
</div>
|
|
369
|
+
<div className="form-control w-full">
|
|
370
|
+
<label className="label font-semibold">
|
|
371
|
+
<span className="label-text">
|
|
372
|
+
Trigger Hint{' '}
|
|
373
|
+
<span className="opacity-50 font-normal">
|
|
374
|
+
(Kapan AI Pakai Ini?)
|
|
375
|
+
</span>
|
|
376
|
+
</span>
|
|
377
|
+
</label>
|
|
378
|
+
<input
|
|
379
|
+
type="text"
|
|
380
|
+
className="input input-bordered w-full bg-base-200/50"
|
|
381
|
+
placeholder="Cth: user ingin menambah angka"
|
|
382
|
+
value={act.triggerHint}
|
|
383
|
+
onChange={(e) => updateAction(idx, 'triggerHint', e.target.value)}
|
|
384
|
+
/>
|
|
385
|
+
</div>
|
|
386
|
+
</div>
|
|
387
|
+
|
|
388
|
+
<div className="form-control w-full h-full flex flex-col">
|
|
389
|
+
<label className="label font-semibold flex justify-between items-end pb-1">
|
|
390
|
+
<span className="label-text flex items-center gap-2">
|
|
391
|
+
<svg
|
|
392
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
393
|
+
width="16"
|
|
394
|
+
height="16"
|
|
395
|
+
viewBox="0 0 24 24"
|
|
396
|
+
fill="none"
|
|
397
|
+
stroke="currentColor"
|
|
398
|
+
strokeWidth="2"
|
|
399
|
+
strokeLinecap="round"
|
|
400
|
+
strokeLinejoin="round"
|
|
401
|
+
>
|
|
402
|
+
<polyline points="16 18 22 12 16 6"></polyline>
|
|
403
|
+
<polyline points="8 6 2 12 8 18"></polyline>
|
|
404
|
+
</svg>
|
|
405
|
+
Kode Node.js
|
|
406
|
+
</span>
|
|
407
|
+
</label>
|
|
408
|
+
<div className="relative mt-2 flex-1 rounded-xl overflow-hidden border border-neutral/50 bg-[#1e1e1e] shadow-inner focus-within:ring-2 focus-within:ring-primary/50 transition-all">
|
|
409
|
+
<div className="absolute top-0 left-0 w-full h-8 bg-[#2d2d2d] border-b border-white/5 flex items-center px-4 gap-1.5 z-10 shadow-sm">
|
|
410
|
+
<div className="w-2.5 h-2.5 rounded-full bg-[#ff5f56]"></div>
|
|
411
|
+
<div className="w-2.5 h-2.5 rounded-full bg-[#ffbd2e]"></div>
|
|
412
|
+
<div className="w-2.5 h-2.5 rounded-full bg-[#27c93f]"></div>
|
|
413
|
+
<span className="ml-2 text-xs font-mono text-white/40">
|
|
414
|
+
{act.name ? act.name : 'action'}.js
|
|
415
|
+
</span>
|
|
416
|
+
{syntaxErrors[idx] && (
|
|
417
|
+
<span
|
|
418
|
+
className="ml-auto text-[10px] text-error font-bold max-w-[150px] truncate"
|
|
419
|
+
title={syntaxErrors[idx]}
|
|
420
|
+
>
|
|
421
|
+
<FaExclamationTriangle className="inline mr-1" /> Error
|
|
422
|
+
</span>
|
|
423
|
+
)}
|
|
424
|
+
</div>
|
|
425
|
+
|
|
426
|
+
<div className="w-full h-full min-h-[200px] pt-10 pb-4 bg-[#1e1e1e] flex flex-col">
|
|
427
|
+
<div
|
|
428
|
+
className="px-4 pb-1 text-sm font-mono flex items-center flex-wrap"
|
|
429
|
+
style={{
|
|
430
|
+
fontFamily:
|
|
431
|
+
'Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace'
|
|
432
|
+
}}
|
|
433
|
+
>
|
|
434
|
+
<span className="text-[#c678dd]">async function</span>
|
|
435
|
+
<input
|
|
436
|
+
type="text"
|
|
437
|
+
className="bg-transparent outline-none border-b border-dashed border-[#56b6c2]/50 focus:border-[#56b6c2] text-[#61afef] px-1 mx-2 w-32 focus:w-48 transition-all placeholder-[#61afef]/30"
|
|
438
|
+
placeholder="nama-action"
|
|
439
|
+
spellCheck="false"
|
|
440
|
+
value={act.name}
|
|
441
|
+
onChange={(e) => updateAction(idx, 'name', e.target.value)}
|
|
442
|
+
/>
|
|
443
|
+
<span className="text-[#abb2bf]">(</span>
|
|
444
|
+
<span className="text-[#e06c75]">{`{ query }`}</span>
|
|
445
|
+
<span className="text-[#abb2bf]">) {`{`}</span>
|
|
446
|
+
</div>
|
|
447
|
+
|
|
448
|
+
<div className="flex-1 overflow-auto relative">
|
|
449
|
+
<Editor
|
|
450
|
+
height="150px"
|
|
451
|
+
defaultLanguage="javascript"
|
|
452
|
+
theme="vs-dark"
|
|
453
|
+
value={act.code}
|
|
454
|
+
onChange={(value) => updateAction(idx, 'code', value || '')}
|
|
455
|
+
options={{
|
|
456
|
+
minimap: { enabled: false },
|
|
457
|
+
scrollBeyondLastLine: false,
|
|
458
|
+
fontSize: 14,
|
|
459
|
+
fontFamily: 'Consolas, Monaco, monospace',
|
|
460
|
+
padding: { top: 16, bottom: 16 },
|
|
461
|
+
overviewRulerLanes: 0,
|
|
462
|
+
hideCursorInOverviewRuler: true,
|
|
463
|
+
scrollbar: { vertical: 'hidden', horizontal: 'hidden' },
|
|
464
|
+
lineNumbers: 'off',
|
|
465
|
+
glyphMargin: false,
|
|
466
|
+
folding: false,
|
|
467
|
+
lineDecorationsWidth: 0,
|
|
468
|
+
lineNumbersMinChars: 0
|
|
469
|
+
}}
|
|
470
|
+
className="border-l border-white/10 ml-4"
|
|
471
|
+
/>
|
|
472
|
+
</div>
|
|
473
|
+
|
|
474
|
+
<div
|
|
475
|
+
className="px-4 pt-1 text-sm font-mono text-[#abb2bf]"
|
|
476
|
+
style={{
|
|
477
|
+
fontFamily:
|
|
478
|
+
'Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace'
|
|
479
|
+
}}
|
|
480
|
+
>
|
|
481
|
+
{`}`}
|
|
482
|
+
</div>
|
|
483
|
+
</div>
|
|
484
|
+
</div>
|
|
485
|
+
</div>
|
|
486
|
+
</div>
|
|
487
|
+
))}
|
|
488
|
+
</div>
|
|
489
|
+
|
|
490
|
+
<div className="modal-action mt-8 pt-4 border-t border-base-content/10">
|
|
491
|
+
<button
|
|
492
|
+
className="btn btn-ghost"
|
|
493
|
+
onClick={() => setIsFormOpen(false)}
|
|
494
|
+
disabled={formStatus?.loading}
|
|
495
|
+
>
|
|
496
|
+
Batal
|
|
497
|
+
</button>
|
|
498
|
+
<button
|
|
499
|
+
className="btn btn-primary px-8"
|
|
500
|
+
disabled={formStatus?.loading}
|
|
501
|
+
onClick={async () => {
|
|
502
|
+
setFormStatus({
|
|
503
|
+
loading: true,
|
|
504
|
+
message: 'Menyimpan dan menginstall library (jika ada)...'
|
|
505
|
+
})
|
|
506
|
+
if (!formData.name)
|
|
507
|
+
return setFormStatus({
|
|
508
|
+
success: false,
|
|
509
|
+
message: 'Nama Plugin wajib diisi'
|
|
510
|
+
})
|
|
511
|
+
for (let i = 0; i < formData.actions.length; i++) {
|
|
512
|
+
if (!formData.actions[i].name || !formData.actions[i].code) {
|
|
513
|
+
return setFormStatus({
|
|
514
|
+
success: false,
|
|
515
|
+
message: `Nama & Kode Action #${i + 1} wajib diisi`
|
|
516
|
+
})
|
|
517
|
+
}
|
|
518
|
+
if (syntaxErrors[i]) {
|
|
519
|
+
return setFormStatus({
|
|
520
|
+
success: false,
|
|
521
|
+
message: `Ada error syntax di Action #${i + 1} bro!`
|
|
522
|
+
})
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const res = await webApi.createPlugin(formData)
|
|
527
|
+
if (res.success) {
|
|
528
|
+
setFormStatus({
|
|
529
|
+
success: true,
|
|
530
|
+
message: 'Plugin berhasil dibuat! Me-reload...'
|
|
531
|
+
})
|
|
532
|
+
setFormData({
|
|
533
|
+
name: '',
|
|
534
|
+
description: '',
|
|
535
|
+
actions: [{ name: '', description: '', triggerHint: '', code: '' }]
|
|
536
|
+
})
|
|
537
|
+
loadData()
|
|
538
|
+
setTimeout(() => setIsFormOpen(false), 1500)
|
|
539
|
+
} else {
|
|
540
|
+
setFormStatus({ success: false, message: res.error })
|
|
541
|
+
}
|
|
542
|
+
}}
|
|
543
|
+
>
|
|
544
|
+
{formStatus?.loading ? (
|
|
545
|
+
<span className="loading loading-spinner"></span>
|
|
546
|
+
) : (
|
|
547
|
+
'Simpan Plugin'
|
|
548
|
+
)}
|
|
549
|
+
</button>
|
|
550
|
+
</div>
|
|
551
|
+
</div>
|
|
552
|
+
</div>
|
|
553
|
+
</div>
|
|
554
|
+
)}
|
|
555
|
+
|
|
556
|
+
{plugins.length === 0 && !isFormOpen ? (
|
|
557
|
+
<div className="alert alert-info shadow-lg border border-info/20 bg-info/10 text-info">
|
|
558
|
+
Belum ada plugin yang terinstall. Buka folder atau klik Tambah Plugin.
|
|
559
|
+
</div>
|
|
560
|
+
) : (
|
|
561
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
562
|
+
{plugins.map((pl, i) => (
|
|
563
|
+
<div
|
|
564
|
+
key={i}
|
|
565
|
+
className="card bg-base-200/50 shadow-xl border border-base-content/10 backdrop-blur-md flex flex-col"
|
|
566
|
+
>
|
|
567
|
+
<div className="card-body flex flex-col h-full">
|
|
568
|
+
<div className="flex justify-between items-start">
|
|
569
|
+
<h2
|
|
570
|
+
className={`card-title ${pl.isEnabled !== false ? 'text-primary' : 'text-base-content/50 line-through'}`}
|
|
571
|
+
>
|
|
572
|
+
{pl.name}{' '}
|
|
573
|
+
<span className="text-xs opacity-50 bg-base-300 px-2 py-1 rounded-full no-underline">
|
|
574
|
+
v{pl.version || '1.0.0'}
|
|
575
|
+
</span>
|
|
576
|
+
</h2>
|
|
577
|
+
<input
|
|
578
|
+
type="checkbox"
|
|
579
|
+
className="toggle toggle-primary toggle-sm mt-1"
|
|
580
|
+
checked={pl.isEnabled !== false}
|
|
581
|
+
onChange={async (e) => {
|
|
582
|
+
await webApi.togglePlugin(pl.name, e.target.checked)
|
|
583
|
+
loadData()
|
|
584
|
+
}}
|
|
585
|
+
/>
|
|
586
|
+
</div>
|
|
587
|
+
<p
|
|
588
|
+
className={`text-sm flex-1 ${pl.isEnabled !== false ? 'opacity-80' : 'opacity-40'}`}
|
|
589
|
+
>
|
|
590
|
+
{pl.description}
|
|
591
|
+
</p>
|
|
592
|
+
<div className="mt-4">
|
|
593
|
+
<span className="text-xs font-bold uppercase opacity-50">
|
|
594
|
+
Available Actions:
|
|
595
|
+
</span>
|
|
596
|
+
<ul className="list-disc ml-4 text-xs mt-1 space-y-1">
|
|
597
|
+
{pl.actions?.map((act) => (
|
|
598
|
+
<li key={act.name}>
|
|
599
|
+
<span className="font-mono text-accent bg-accent/10 px-1 rounded">
|
|
600
|
+
{act.name}
|
|
601
|
+
</span>{' '}
|
|
602
|
+
<span className="opacity-70">- {act.description}</span>
|
|
603
|
+
</li>
|
|
604
|
+
))}
|
|
605
|
+
</ul>
|
|
606
|
+
</div>
|
|
607
|
+
<div className="flex gap-2 w-full mt-6 pt-4 border-t border-base-content/10">
|
|
608
|
+
<button
|
|
609
|
+
className="btn btn-sm btn-outline flex-1"
|
|
610
|
+
onClick={() => webApi.openSpecificFolder(pl.folderPath)}
|
|
611
|
+
>
|
|
612
|
+
Buka Folder
|
|
613
|
+
</button>
|
|
614
|
+
<button
|
|
615
|
+
className="btn btn-sm btn-primary flex-1"
|
|
616
|
+
onClick={() => openEditForm(pl)}
|
|
617
|
+
>
|
|
618
|
+
Edit Plugin
|
|
619
|
+
</button>
|
|
620
|
+
</div>
|
|
621
|
+
</div>
|
|
622
|
+
</div>
|
|
623
|
+
))}
|
|
624
|
+
</div>
|
|
625
|
+
)}
|
|
626
|
+
</div>
|
|
627
|
+
</div>
|
|
628
|
+
</div>
|
|
629
|
+
)
|
|
630
|
+
}
|