myaiforone 1.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/README.md +113 -0
- package/agents/_template/CLAUDE.md +18 -0
- package/agents/_template/agent.json +7 -0
- package/agents/platform/agentcreator/CLAUDE.md +300 -0
- package/agents/platform/appcreator/CLAUDE.md +158 -0
- package/agents/platform/gym/CLAUDE.md +486 -0
- package/agents/platform/gym/agent.json +40 -0
- package/agents/platform/gym/programs/agent-building/program.json +160 -0
- package/agents/platform/gym/programs/automations-mastery/program.json +129 -0
- package/agents/platform/gym/programs/getting-started/program.json +124 -0
- package/agents/platform/gym/programs/mcp-integrations/program.json +116 -0
- package/agents/platform/gym/programs/multi-model-strategy/program.json +115 -0
- package/agents/platform/gym/programs/prompt-engineering/program.json +136 -0
- package/agents/platform/gym/souls/alex.md +12 -0
- package/agents/platform/gym/souls/jordan.md +12 -0
- package/agents/platform/gym/souls/morgan.md +12 -0
- package/agents/platform/gym/souls/riley.md +12 -0
- package/agents/platform/gym/souls/sam.md +12 -0
- package/agents/platform/hub/CLAUDE.md +372 -0
- package/agents/platform/promptcreator/CLAUDE.md +130 -0
- package/agents/platform/skillcreator/CLAUDE.md +163 -0
- package/bin/cli.js +566 -0
- package/config.example.json +310 -0
- package/dist/agent-registry.d.ts +32 -0
- package/dist/agent-registry.d.ts.map +1 -0
- package/dist/agent-registry.js +144 -0
- package/dist/agent-registry.js.map +1 -0
- package/dist/channels/discord.d.ts +17 -0
- package/dist/channels/discord.d.ts.map +1 -0
- package/dist/channels/discord.js +114 -0
- package/dist/channels/discord.js.map +1 -0
- package/dist/channels/imessage.d.ts +23 -0
- package/dist/channels/imessage.d.ts.map +1 -0
- package/dist/channels/imessage.js +214 -0
- package/dist/channels/imessage.js.map +1 -0
- package/dist/channels/slack.d.ts +19 -0
- package/dist/channels/slack.d.ts.map +1 -0
- package/dist/channels/slack.js +167 -0
- package/dist/channels/slack.js.map +1 -0
- package/dist/channels/telegram.d.ts +19 -0
- package/dist/channels/telegram.d.ts.map +1 -0
- package/dist/channels/telegram.js +274 -0
- package/dist/channels/telegram.js.map +1 -0
- package/dist/channels/types.d.ts +44 -0
- package/dist/channels/types.d.ts.map +1 -0
- package/dist/channels/types.js +18 -0
- package/dist/channels/types.js.map +1 -0
- package/dist/channels/whatsapp.d.ts +23 -0
- package/dist/channels/whatsapp.d.ts.map +1 -0
- package/dist/channels/whatsapp.js +189 -0
- package/dist/channels/whatsapp.js.map +1 -0
- package/dist/config.d.ts +134 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +127 -0
- package/dist/config.js.map +1 -0
- package/dist/cron.d.ts +8 -0
- package/dist/cron.d.ts.map +1 -0
- package/dist/cron.js +35 -0
- package/dist/cron.js.map +1 -0
- package/dist/decrypt-keys.d.ts +7 -0
- package/dist/decrypt-keys.d.ts.map +1 -0
- package/dist/decrypt-keys.js +53 -0
- package/dist/decrypt-keys.js.map +1 -0
- package/dist/encrypt-keys.d.ts +8 -0
- package/dist/encrypt-keys.d.ts.map +1 -0
- package/dist/encrypt-keys.js +62 -0
- package/dist/encrypt-keys.js.map +1 -0
- package/dist/executor.d.ts +31 -0
- package/dist/executor.d.ts.map +1 -0
- package/dist/executor.js +2009 -0
- package/dist/executor.js.map +1 -0
- package/dist/gemini-executor.d.ts +27 -0
- package/dist/gemini-executor.d.ts.map +1 -0
- package/dist/gemini-executor.js +160 -0
- package/dist/gemini-executor.js.map +1 -0
- package/dist/goals.d.ts +24 -0
- package/dist/goals.d.ts.map +1 -0
- package/dist/goals.js +189 -0
- package/dist/goals.js.map +1 -0
- package/dist/gym/activity-digest.d.ts +30 -0
- package/dist/gym/activity-digest.d.ts.map +1 -0
- package/dist/gym/activity-digest.js +506 -0
- package/dist/gym/activity-digest.js.map +1 -0
- package/dist/gym/dimension-scorer.d.ts +76 -0
- package/dist/gym/dimension-scorer.d.ts.map +1 -0
- package/dist/gym/dimension-scorer.js +236 -0
- package/dist/gym/dimension-scorer.js.map +1 -0
- package/dist/gym/gym-router.d.ts +7 -0
- package/dist/gym/gym-router.d.ts.map +1 -0
- package/dist/gym/gym-router.js +718 -0
- package/dist/gym/gym-router.js.map +1 -0
- package/dist/gym/index.d.ts +11 -0
- package/dist/gym/index.d.ts.map +1 -0
- package/dist/gym/index.js +11 -0
- package/dist/gym/index.js.map +1 -0
- package/dist/heartbeat.d.ts +21 -0
- package/dist/heartbeat.d.ts.map +1 -0
- package/dist/heartbeat.js +163 -0
- package/dist/heartbeat.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +254 -0
- package/dist/index.js.map +1 -0
- package/dist/keystore.d.ts +22 -0
- package/dist/keystore.d.ts.map +1 -0
- package/dist/keystore.js +178 -0
- package/dist/keystore.js.map +1 -0
- package/dist/logger.d.ts +9 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +45 -0
- package/dist/logger.js.map +1 -0
- package/dist/memory/daily.d.ts +22 -0
- package/dist/memory/daily.d.ts.map +1 -0
- package/dist/memory/daily.js +82 -0
- package/dist/memory/daily.js.map +1 -0
- package/dist/memory/embeddings.d.ts +15 -0
- package/dist/memory/embeddings.d.ts.map +1 -0
- package/dist/memory/embeddings.js +154 -0
- package/dist/memory/embeddings.js.map +1 -0
- package/dist/memory/index.d.ts +32 -0
- package/dist/memory/index.d.ts.map +1 -0
- package/dist/memory/index.js +159 -0
- package/dist/memory/index.js.map +1 -0
- package/dist/memory/search.d.ts +21 -0
- package/dist/memory/search.d.ts.map +1 -0
- package/dist/memory/search.js +77 -0
- package/dist/memory/search.js.map +1 -0
- package/dist/memory/store.d.ts +23 -0
- package/dist/memory/store.d.ts.map +1 -0
- package/dist/memory/store.js +144 -0
- package/dist/memory/store.js.map +1 -0
- package/dist/ollama-executor.d.ts +17 -0
- package/dist/ollama-executor.d.ts.map +1 -0
- package/dist/ollama-executor.js +112 -0
- package/dist/ollama-executor.js.map +1 -0
- package/dist/openai-executor.d.ts +38 -0
- package/dist/openai-executor.d.ts.map +1 -0
- package/dist/openai-executor.js +197 -0
- package/dist/openai-executor.js.map +1 -0
- package/dist/router.d.ts +11 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +185 -0
- package/dist/router.js.map +1 -0
- package/dist/test-message.d.ts +2 -0
- package/dist/test-message.d.ts.map +1 -0
- package/dist/test-message.js +60 -0
- package/dist/test-message.js.map +1 -0
- package/dist/utils/imsg-db-reader.d.ts +24 -0
- package/dist/utils/imsg-db-reader.d.ts.map +1 -0
- package/dist/utils/imsg-db-reader.js +92 -0
- package/dist/utils/imsg-db-reader.js.map +1 -0
- package/dist/utils/imsg-rpc.d.ts +25 -0
- package/dist/utils/imsg-rpc.d.ts.map +1 -0
- package/dist/utils/imsg-rpc.js +149 -0
- package/dist/utils/imsg-rpc.js.map +1 -0
- package/dist/utils/message-formatter.d.ts +3 -0
- package/dist/utils/message-formatter.d.ts.map +1 -0
- package/dist/utils/message-formatter.js +69 -0
- package/dist/utils/message-formatter.js.map +1 -0
- package/dist/web-ui.d.ts +12 -0
- package/dist/web-ui.d.ts.map +1 -0
- package/dist/web-ui.js +5784 -0
- package/dist/web-ui.js.map +1 -0
- package/dist/whatsapp-chats.d.ts +2 -0
- package/dist/whatsapp-chats.d.ts.map +1 -0
- package/dist/whatsapp-chats.js +76 -0
- package/dist/whatsapp-chats.js.map +1 -0
- package/dist/whatsapp-login.d.ts +2 -0
- package/dist/whatsapp-login.d.ts.map +1 -0
- package/dist/whatsapp-login.js +90 -0
- package/dist/whatsapp-login.js.map +1 -0
- package/dist/wiki-sync.d.ts +21 -0
- package/dist/wiki-sync.d.ts.map +1 -0
- package/dist/wiki-sync.js +147 -0
- package/dist/wiki-sync.js.map +1 -0
- package/docs/AddNewAgentGuide.md +100 -0
- package/docs/AddNewMcpGuide.md +72 -0
- package/docs/Architecture.md +795 -0
- package/docs/CLAUDE-AI-SETUP.md +166 -0
- package/docs/Setup.md +297 -0
- package/docs/ai-gym-architecture.md +1040 -0
- package/docs/ai-gym-build-plan.md +343 -0
- package/docs/ai-gym-onboarding.md +122 -0
- package/docs/appcreator_plan.md +348 -0
- package/docs/platform-mcp-audit.md +320 -0
- package/docs/server-deployment-plan.md +503 -0
- package/docs/superpowers/plans/2026-03-25-marketplace.md +1281 -0
- package/docs/superpowers/specs/2026-03-25-marketplace-design.md +287 -0
- package/docs/user-guide.md +2016 -0
- package/mcp-catalog.json +628 -0
- package/package.json +63 -0
- package/public/MyAIforOne-logomark-512.svg +16 -0
- package/public/MyAIforOne-logomark-transparent.svg +15 -0
- package/public/activity.html +314 -0
- package/public/admin.html +1674 -0
- package/public/agent-dashboard.html +670 -0
- package/public/api-docs.html +1106 -0
- package/public/automations.html +722 -0
- package/public/canvas.css +223 -0
- package/public/canvas.js +588 -0
- package/public/changelog.html +231 -0
- package/public/gym.html +2766 -0
- package/public/home.html +1930 -0
- package/public/index.html +2809 -0
- package/public/lab.html +1643 -0
- package/public/library.html +1442 -0
- package/public/marketplace.html +1101 -0
- package/public/mcp-docs.html +441 -0
- package/public/mini.html +390 -0
- package/public/monitor.html +584 -0
- package/public/org.html +4304 -0
- package/public/projects.html +734 -0
- package/public/settings.html +645 -0
- package/public/tasks.html +932 -0
- package/public/trainers/alex.svg +12 -0
- package/public/trainers/jordan.svg +12 -0
- package/public/trainers/morgan.svg +12 -0
- package/public/trainers/riley.svg +12 -0
- package/public/trainers/sam.svg +12 -0
- package/public/user-guide.html +218 -0
- package/registry/agents.json +3 -0
- package/registry/apps.json +20 -0
- package/registry/installed-drafts.json +3 -0
- package/registry/mcps.json +1084 -0
- package/registry/prompts/personal/mcp-test-prompt.md +6 -0
- package/registry/prompts/personal/memory-recall.md +6 -0
- package/registry/prompts/platform/brainstorm.md +15 -0
- package/registry/prompts/platform/code-review.md +16 -0
- package/registry/prompts/platform/explain.md +16 -0
- package/registry/prompts.json +58 -0
- package/registry/skills/external/brainstorming.md +5 -0
- package/registry/skills/external/code-review.md +40 -0
- package/registry/skills/external/frontend-patterns.md +642 -0
- package/registry/skills/external/frontend-slides.md +184 -0
- package/registry/skills/external/systematic-debugging.md +5 -0
- package/registry/skills/external/tdd.md +328 -0
- package/registry/skills/external/verification-before-completion.md +5 -0
- package/registry/skills/external/writing-plans.md +5 -0
- package/registry/skills/platform/ai41_app_build.md +930 -0
- package/registry/skills/platform/ai41_app_deploy.md +168 -0
- package/registry/skills/platform/ai41_app_orchestrator.md +239 -0
- package/registry/skills/platform/ai41_app_patterns.md +359 -0
- package/registry/skills/platform/ai41_app_register.md +85 -0
- package/registry/skills/platform/ai41_app_scaffold.md +421 -0
- package/registry/skills/platform/ai41_app_verify.md +107 -0
- package/registry/skills/platform/opProjectCreate.md +239 -0
- package/registry/skills/platform/op_devbrowser.md +136 -0
- package/registry/skills/platform/sop_brandguidelines.md +103 -0
- package/registry/skills/platform/sop_docx.md +117 -0
- package/registry/skills/platform/sop_frontenddesign.md +44 -0
- package/registry/skills/platform/sop_frontenddesign_v2.md +659 -0
- package/registry/skills/platform/sop_mcpbuilder.md +133 -0
- package/registry/skills/platform/sop_pdf.md +172 -0
- package/registry/skills/platform/sop_pptx.md +133 -0
- package/registry/skills/platform/sop_skillcreator.md +104 -0
- package/registry/skills/platform/sop_themefactory.md +128 -0
- package/registry/skills/platform/sop_webapptesting.md +75 -0
- package/registry/skills/platform/sop_webartifactsbuilder.md +97 -0
- package/registry/skills/platform/sop_xlsx.md +134 -0
- package/registry/skills.json +1055 -0
- package/scripts/discover-chats.sh +11 -0
- package/scripts/install-service-windows.ps1 +87 -0
- package/scripts/install-service.sh +52 -0
- package/scripts/seed-registry.ts +195 -0
- package/scripts/test-send.sh +5 -0
- package/scripts/tray-indicator.ps1 +35 -0
- package/scripts/uninstall-service-windows.ps1 +23 -0
- package/scripts/uninstall-service.sh +15 -0
- package/scripts/xbar-myagent.5s.sh +32 -0
- package/server/mcp-server/dist/index.d.ts +11 -0
- package/server/mcp-server/dist/index.js +1332 -0
- package/server/mcp-server/dist/lib/api-client.d.ts +165 -0
- package/server/mcp-server/dist/lib/api-client.js +241 -0
- package/server/mcp-server/index.ts +1545 -0
- package/server/mcp-server/lib/api-client.ts +366 -0
- package/server/mcp-server/tsconfig.json +14 -0
- package/src/agent-registry.ts +180 -0
- package/src/channels/discord.ts +129 -0
- package/src/channels/imessage.ts +261 -0
- package/src/channels/slack.ts +208 -0
- package/src/channels/telegram.ts +307 -0
- package/src/channels/types.ts +62 -0
- package/src/channels/whatsapp.ts +227 -0
- package/src/config.ts +281 -0
- package/src/cron.ts +43 -0
- package/src/decrypt-keys.ts +60 -0
- package/src/encrypt-keys.ts +70 -0
- package/src/executor.ts +2190 -0
- package/src/gemini-executor.ts +212 -0
- package/src/goals.ts +240 -0
- package/src/gym/activity-digest.ts +546 -0
- package/src/gym/dimension-scorer.ts +297 -0
- package/src/gym/gym-router.ts +801 -0
- package/src/gym/index.ts +19 -0
- package/src/heartbeat.ts +220 -0
- package/src/index.ts +275 -0
- package/src/keystore.ts +190 -0
- package/src/logger.ts +51 -0
- package/src/memory/daily.ts +101 -0
- package/src/memory/embeddings.ts +185 -0
- package/src/memory/index.ts +218 -0
- package/src/memory/search.ts +124 -0
- package/src/memory/store.ts +189 -0
- package/src/ollama-executor.ts +126 -0
- package/src/openai-executor.ts +259 -0
- package/src/router.ts +230 -0
- package/src/test-message.ts +72 -0
- package/src/utils/imsg-db-reader.ts +109 -0
- package/src/utils/imsg-rpc.ts +178 -0
- package/src/utils/message-formatter.ts +90 -0
- package/src/web-ui.ts +5778 -0
- package/src/whatsapp-chats.ts +91 -0
- package/src/whatsapp-login.ts +110 -0
- package/src/wiki-sync.ts +199 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,1442 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>MyAIforOne — Library</title>
|
|
7
|
+
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&family=Syne:wght@600;700;800&display=swap" rel="stylesheet">
|
|
8
|
+
<style>
|
|
9
|
+
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
|
10
|
+
:root{
|
|
11
|
+
--bg-deep:#060a13;--bg-surface:rgba(12,18,33,0.92);--bg-card:rgba(16,22,40,0.85);
|
|
12
|
+
--bg-input:rgba(0,0,0,0.35);--border-dim:rgba(56,189,248,0.08);--border-glow:rgba(56,189,248,0.18);
|
|
13
|
+
--border-active:rgba(56,189,248,0.45);--text-primary:rgba(255,255,255,0.92);
|
|
14
|
+
--text-secondary:rgba(255,255,255,0.68);--text-muted:rgba(148,163,184,0.55);
|
|
15
|
+
--accent:#22d3ee;--accent-soft:#38bdf8;--accent-bg:rgba(6,182,212,0.15);
|
|
16
|
+
--accent-glow:rgba(34,211,238,0.12);--purple:rgba(139,92,246,0.7);--purple-bg:rgba(139,92,246,0.12);
|
|
17
|
+
--green:#4ade80;--green-bg:rgba(74,222,128,0.1);--amber:#fbbf24;--amber-bg:rgba(251,191,36,0.1);
|
|
18
|
+
--red:#f87171;--red-bg:rgba(248,113,113,0.1);--shadow:0 2px 12px rgba(0,0,0,0.3);
|
|
19
|
+
--shadow-glow:0 0 20px rgba(34,211,238,0.08);--radius:12px;
|
|
20
|
+
--font-sans:'DM Sans',system-ui,sans-serif;--font-mono:'IBM Plex Mono',monospace;--font-display:'Syne',sans-serif;
|
|
21
|
+
}
|
|
22
|
+
[data-theme="light"]{
|
|
23
|
+
--bg-deep:#f4f6f9;--bg-surface:rgba(255,255,255,0.95);--bg-card:rgba(255,255,255,0.9);
|
|
24
|
+
--bg-input:rgba(0,0,0,0.04);--border-dim:rgba(0,0,0,0.08);--border-glow:rgba(14,116,144,0.18);
|
|
25
|
+
--border-active:rgba(14,116,144,0.45);--text-primary:rgba(15,23,42,0.92);
|
|
26
|
+
--text-secondary:rgba(51,65,85,0.8);--text-muted:rgba(100,116,139,0.6);
|
|
27
|
+
--accent:#0891b2;--accent-soft:#0e7490;--accent-bg:rgba(14,116,144,0.08);
|
|
28
|
+
--accent-glow:rgba(14,116,144,0.06);--purple:rgba(109,40,217,0.75);--purple-bg:rgba(139,92,246,0.08);
|
|
29
|
+
--green:#16a34a;--green-bg:rgba(22,163,74,0.08);--amber:#d97706;--amber-bg:rgba(217,119,6,0.08);
|
|
30
|
+
--red:#dc2626;--red-bg:rgba(220,38,38,0.08);--shadow:0 1px 8px rgba(0,0,0,0.06);--shadow-glow:none;
|
|
31
|
+
}
|
|
32
|
+
html,body{width:100%;height:100%;overflow:hidden;max-width:100vw;background:var(--bg-deep);font-family:var(--font-sans);color:var(--text-primary);transition:background .3s,color .3s}
|
|
33
|
+
|
|
34
|
+
/* ── Topbar ── */
|
|
35
|
+
.topbar{height:56px;display:flex;align-items:center;padding:0 24px;background:var(--bg-surface);border-bottom:1px solid var(--border-dim);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);position:fixed;top:0;left:0;right:0;z-index:100;overflow:hidden;max-width:100vw}
|
|
36
|
+
.topbar-logo{display:flex;align-items:center;gap:10px;margin-right:32px;flex-shrink:0}
|
|
37
|
+
.logo-mark{width:28px;height:28px;border-radius:8px;object-fit:contain}
|
|
38
|
+
.logo-text{font-family:var(--font-display);font-size:15px;font-weight:700;color:var(--accent)}
|
|
39
|
+
.tab-group{display:flex;gap:0;flex-shrink:1;min-width:0;overflow-x:auto}
|
|
40
|
+
.tab-btn{font-family:var(--font-sans);font-size:13px;font-weight:600;color:var(--text-muted);background:none;border:none;padding:16px 14px;cursor:pointer;position:relative;transition:color .2s;text-decoration:none;display:block;white-space:nowrap;flex-shrink:0}
|
|
41
|
+
.tab-btn:hover{color:var(--text-secondary)}
|
|
42
|
+
.tab-btn.active{color:var(--accent)}
|
|
43
|
+
.tab-btn.active::after{content:'';position:absolute;bottom:0;left:12px;right:12px;height:2px;background:var(--accent);border-radius:1px}
|
|
44
|
+
.sub-nav{position:fixed;top:56px;left:0;right:0;z-index:99;height:36px;display:flex;align-items:center;gap:2px;padding:0 24px;background:var(--bg-surface);border-bottom:1px solid var(--border-dim);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px)}
|
|
45
|
+
.sub-nav-link{font-family:var(--font-sans);font-size:12px;font-weight:500;color:var(--text-muted);text-decoration:none;padding:4px 10px;border-radius:5px;transition:color .2s}
|
|
46
|
+
.sub-nav-link:hover{color:var(--text-secondary)}
|
|
47
|
+
.sub-nav-link.active{color:var(--accent)}
|
|
48
|
+
.topbar-right{margin-left:auto;display:flex;align-items:center;gap:12px;flex-shrink:0}
|
|
49
|
+
.gym-nav-btn{font-family:var(--font-sans);font-size:12px;font-weight:700;color:#fff;background:var(--accent);border:none;border-radius:8px;padding:6px 14px;cursor:pointer;text-decoration:none;letter-spacing:.02em;transition:opacity .2s,transform .2s;white-space:nowrap}
|
|
50
|
+
.gym-nav-btn:hover{opacity:.88;transform:translateY(-1px)}
|
|
51
|
+
.theme-toggle,.gear-btn{width:34px;height:34px;border-radius:8px;border:1px solid var(--border-dim);background:transparent;color:var(--text-muted);cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:16px;transition:all .2stext-decoration:none;}
|
|
52
|
+
.theme-toggle:hover,.gear-btn:hover{border-color:var(--border-glow);color:var(--text-secondary)}
|
|
53
|
+
.docs-dropdown{position:relative;display:inline-block}
|
|
54
|
+
.docs-menu{display:none;position:absolute;top:40px;right:0;min-width:160px;background:var(--bg-surface);border:1px solid var(--border-glow);border-radius:8px;box-shadow:0 8px 24px rgba(0,0,0,0.4);z-index:200;padding:4px;backdrop-filter:blur(20px)}
|
|
55
|
+
.docs-menu.open{display:block}
|
|
56
|
+
.docs-menu a{display:flex;align-items:center;gap:8px;padding:7px 12px;font-size:12px;font-weight:500;color:var(--text-secondary);text-decoration:none;border-radius:6px;transition:all .15s}
|
|
57
|
+
.docs-menu a:hover{background:var(--accent-bg);color:var(--accent)}
|
|
58
|
+
.docs-menu a .dm-icon{font-size:14px;width:18px;text-align:center}
|
|
59
|
+
|
|
60
|
+
/* ── Canvas ── */
|
|
61
|
+
.canvas{position:fixed;top:56px;left:0;right:0;bottom:0;overflow:auto;padding:32px 40px}
|
|
62
|
+
|
|
63
|
+
/* ── Toolbar ── */
|
|
64
|
+
.toolbar{display:flex;align-items:center;gap:12px;margin-bottom:28px;flex-wrap:wrap}
|
|
65
|
+
.type-tabs{display:flex;gap:4px;background:var(--bg-card);border:1px solid var(--border-dim);border-radius:10px;padding:4px}
|
|
66
|
+
.type-tab{font-family:var(--font-sans);font-size:12px;font-weight:600;padding:6px 18px;border-radius:7px;border:none;background:transparent;color:var(--text-muted);cursor:pointer;transition:all .2s}
|
|
67
|
+
.type-tab:hover{color:var(--text-secondary)}
|
|
68
|
+
.type-tab.active{background:var(--accent-bg);color:var(--accent);border:1px solid var(--border-active)}
|
|
69
|
+
.search-wrap{flex:1;max-width:320px;position:relative}
|
|
70
|
+
.search-input{width:100%;background:var(--bg-input);border:1px solid var(--border-dim);border-radius:8px;padding:8px 12px 8px 34px;font-family:var(--font-sans);font-size:13px;color:var(--text-primary);outline:none;transition:border-color .2s}
|
|
71
|
+
.search-input::placeholder{color:var(--text-muted)}
|
|
72
|
+
.search-input:focus{border-color:var(--border-active)}
|
|
73
|
+
.search-icon{position:absolute;left:10px;top:50%;transform:translateY(-50%);font-size:14px;color:var(--text-muted);pointer-events:none}
|
|
74
|
+
.filter-pills{display:flex;gap:6px;flex-wrap:wrap}
|
|
75
|
+
.pill{font-family:var(--font-mono);font-size:10px;padding:4px 10px;border-radius:6px;border:1px solid var(--border-dim);background:transparent;color:var(--text-muted);cursor:pointer;transition:all .2s;white-space:nowrap}
|
|
76
|
+
.pill:hover{border-color:var(--border-glow);color:var(--text-secondary)}
|
|
77
|
+
.pill.active{border-color:var(--accent);color:var(--accent);background:var(--accent-bg)}
|
|
78
|
+
|
|
79
|
+
/* ── Sections ── */
|
|
80
|
+
.sections{display:flex;flex-direction:column;gap:36px}
|
|
81
|
+
.section-header{display:flex;align-items:center;gap:12px;margin-bottom:16px;padding-bottom:10px;border-bottom:1px solid var(--border-dim)}
|
|
82
|
+
.section-title{font-family:var(--font-display);font-size:13px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:var(--text-muted)}
|
|
83
|
+
.section-count{font-family:var(--font-mono);font-size:10px;padding:2px 7px;border-radius:4px;background:var(--bg-input);color:var(--text-muted);border:1px solid var(--border-dim)}
|
|
84
|
+
.section-action{margin-left:auto}
|
|
85
|
+
/* ── Add split-reveal ── */
|
|
86
|
+
.add-split{margin-left:auto;display:flex;align-items:center;gap:6px}
|
|
87
|
+
.add-choices{display:flex;align-items:center;gap:4px;animation:choicesIn .15s ease-out}
|
|
88
|
+
.add-choices.hidden{display:none}
|
|
89
|
+
@keyframes choicesIn{from{opacity:0;transform:translateX(-6px)}to{opacity:1;transform:none}}
|
|
90
|
+
.btn-choice{font-family:var(--font-sans);font-size:11px;font-weight:600;padding:5px 12px;border-radius:7px;background:var(--bg-input);color:var(--text-secondary);border:1px solid var(--border-dim);cursor:pointer;transition:all .15s;white-space:nowrap}
|
|
91
|
+
.btn-choice:hover{border-color:var(--accent);color:var(--accent);background:var(--accent-bg)}
|
|
92
|
+
.btn-choice-build{border-color:var(--purple);color:var(--purple);background:var(--purple-bg)}
|
|
93
|
+
.btn-choice-build:hover{opacity:.85}
|
|
94
|
+
|
|
95
|
+
/* ── Grid ── */
|
|
96
|
+
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:16px}
|
|
97
|
+
|
|
98
|
+
/* ── Card ── */
|
|
99
|
+
.card{background:var(--bg-card);border:1px solid var(--border-dim);border-radius:var(--radius);padding:18px;display:flex;flex-direction:column;gap:10px;transition:all .25s;backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);height:200px}
|
|
100
|
+
|
|
101
|
+
/* ── App Card (rich tile from /apps) ── */
|
|
102
|
+
.app-card{background:var(--bg-card);border:1px solid var(--border-dim);border-radius:var(--radius);padding:20px;display:flex;flex-direction:column;gap:12px;transition:all .25s;backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px)}
|
|
103
|
+
.app-card:hover{border-color:var(--border-glow);box-shadow:var(--shadow),var(--shadow-glow);transform:translateY(-1px)}
|
|
104
|
+
.app-card-top{display:flex;align-items:flex-start;gap:12px}
|
|
105
|
+
.app-icon{width:44px;height:44px;border-radius:10px;background:var(--accent-bg);border:1px solid var(--border-active);display:flex;align-items:center;justify-content:center;font-family:var(--font-display);font-size:16px;font-weight:800;color:var(--accent);flex-shrink:0}
|
|
106
|
+
.app-name{font-family:var(--font-display);font-size:15px;font-weight:700;line-height:1.2}
|
|
107
|
+
.app-url{font-family:var(--font-mono);font-size:10px;color:var(--text-muted);margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:200px}
|
|
108
|
+
.app-badges{display:flex;flex-direction:column;align-items:flex-end;gap:4px;margin-left:auto;flex-shrink:0}
|
|
109
|
+
.status-badge{font-family:var(--font-mono);font-size:9px;font-weight:600;padding:3px 8px;border-radius:5px;white-space:nowrap;letter-spacing:.03em}
|
|
110
|
+
.status-live{background:var(--green-bg);color:var(--green);border:1px solid rgba(74,222,128,0.3)}
|
|
111
|
+
.status-draft{background:var(--bg-input);color:var(--text-muted);border:1px solid var(--border-dim)}
|
|
112
|
+
.status-maintenance{background:var(--amber-bg);color:var(--amber);border:1px solid rgba(251,191,36,0.3)}
|
|
113
|
+
.status-down{background:var(--red-bg);color:var(--red);border:1px solid rgba(248,113,113,0.3)}
|
|
114
|
+
.deploy-badge{font-family:var(--font-mono);font-size:9px;padding:2px 7px;border-radius:4px;background:var(--purple-bg);color:var(--purple);border:1px solid rgba(139,92,246,0.2)}
|
|
115
|
+
.app-desc{font-size:12px;color:var(--text-secondary);line-height:1.5;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
|
116
|
+
.app-meta{display:flex;align-items:center;gap:6px;flex-wrap:wrap}
|
|
117
|
+
.app-tag{font-family:var(--font-mono);font-size:9px;padding:2px 7px;border-radius:4px;background:var(--purple-bg);color:var(--purple);border:1px solid rgba(139,92,246,0.2)}
|
|
118
|
+
.agent-dev-badge{display:flex;align-items:center;gap:4px;font-family:var(--font-mono);font-size:9px;padding:2px 7px;border-radius:4px;background:var(--accent-bg);color:var(--accent);border:1px solid rgba(34,211,238,0.2);cursor:pointer;transition:all .2s;text-decoration:none}
|
|
119
|
+
.agent-dev-badge:hover{background:var(--accent);color:#000}
|
|
120
|
+
.app-actions{display:flex;gap:8px;margin-top:2px;align-items:center}
|
|
121
|
+
.btn-launch{font-family:var(--font-sans);font-size:11px;font-weight:600;padding:6px 14px;border-radius:7px;border:none;background:var(--accent-bg);color:var(--accent);border:1px solid var(--border-active);cursor:pointer;transition:all .2s;text-decoration:none;display:inline-flex;align-items:center;gap:4px}
|
|
122
|
+
.btn-launch:hover{background:var(--accent);color:#000}
|
|
123
|
+
.btn-icon{width:28px;height:28px;border-radius:7px;border:1px solid var(--border-dim);background:transparent;color:var(--text-muted);cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:13px;transition:all .2s}
|
|
124
|
+
.btn-icon:hover{border-color:var(--border-glow);color:var(--text-secondary)}
|
|
125
|
+
.btn-icon.danger:hover{border-color:rgba(248,113,113,0.4);color:var(--red)}
|
|
126
|
+
.health-chip{margin-left:auto;font-family:var(--font-mono);font-size:9px;display:flex;align-items:center;gap:4px;color:var(--text-muted);cursor:pointer;border:1px solid var(--border-dim);padding:2px 8px;border-radius:5px;background:transparent;transition:all .2s}
|
|
127
|
+
.health-chip:hover{border-color:var(--border-glow);color:var(--text-secondary)}
|
|
128
|
+
.health-dot{width:6px;height:6px;border-radius:50%;background:var(--text-muted)}
|
|
129
|
+
.health-dot.healthy{background:var(--green)}
|
|
130
|
+
.health-dot.down{background:var(--red)}
|
|
131
|
+
.card:hover{border-color:var(--border-glow);box-shadow:var(--shadow),var(--shadow-glow);transform:translateY(-1px)}
|
|
132
|
+
.card.installed{border-color:rgba(74,222,128,0.2)}
|
|
133
|
+
.card-top{display:flex;align-items:flex-start;justify-content:space-between;gap:8px}
|
|
134
|
+
.card-name{font-family:var(--font-display);font-size:14px;font-weight:700;color:var(--text-primary);line-height:1.2;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
|
135
|
+
.card-badges{display:flex;flex-direction:column;align-items:flex-end;gap:4px;flex-shrink:0}
|
|
136
|
+
.badge{font-family:var(--font-mono);font-size:9px;font-weight:600;padding:3px 7px;border-radius:4px;white-space:nowrap;letter-spacing:.03em}
|
|
137
|
+
.badge-verified{background:var(--accent-bg);color:var(--accent);border:1px solid rgba(34,211,238,0.3)}
|
|
138
|
+
.badge-external{background:var(--bg-input);color:var(--text-muted);border:1px solid var(--border-dim)}
|
|
139
|
+
.badge-personal{background:var(--purple-bg);color:var(--purple);border:1px solid rgba(139,92,246,0.3)}
|
|
140
|
+
.badge-installed{background:var(--green-bg);color:var(--green);border:1px solid rgba(74,222,128,0.3)}
|
|
141
|
+
.card-provider{font-family:var(--font-mono);font-size:10px;color:var(--text-muted);margin-top:1px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:140px}
|
|
142
|
+
.card-desc{font-size:12px;color:var(--text-secondary);line-height:1.5;flex:1;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}
|
|
143
|
+
.card-meta{display:flex;align-items:center;gap:6px;flex-wrap:wrap}
|
|
144
|
+
.tag{font-family:var(--font-mono);font-size:9px;padding:2px 7px;border-radius:4px;background:var(--purple-bg);color:var(--purple);border:1px solid rgba(139,92,246,0.2)}
|
|
145
|
+
.assigned-label{font-family:var(--font-mono);font-size:9px;color:var(--text-muted)}
|
|
146
|
+
.card-actions{display:flex;gap:8px;margin-top:4px}
|
|
147
|
+
.btn{font-family:var(--font-sans);font-size:12px;font-weight:600;padding:7px 14px;border-radius:8px;border:none;cursor:pointer;transition:all .2s;flex:1;text-align:center}
|
|
148
|
+
.btn-install{background:var(--accent-bg);color:var(--accent);border:1px solid var(--border-active)}
|
|
149
|
+
.btn-install:hover{background:var(--accent);color:#000}
|
|
150
|
+
.btn-install:disabled{opacity:.5;cursor:not-allowed;pointer-events:none}
|
|
151
|
+
.btn-manage{background:var(--bg-input);color:var(--text-secondary);border:1px solid var(--border-dim)}
|
|
152
|
+
.btn-manage:hover{border-color:var(--border-glow);color:var(--text-primary)}
|
|
153
|
+
.btn-installing{background:var(--bg-input);color:var(--text-muted);border:1px solid var(--border-dim);pointer-events:none}
|
|
154
|
+
|
|
155
|
+
/* ── Small buttons ── */
|
|
156
|
+
.btn-sm{font-family:var(--font-sans);font-size:11px;font-weight:600;padding:5px 12px;border-radius:7px;border:none;cursor:pointer;transition:all .2s;white-space:nowrap}
|
|
157
|
+
.btn-sm-accent{background:var(--accent-bg);color:var(--accent);border:1px solid var(--border-active)}
|
|
158
|
+
.btn-sm-accent:hover{background:var(--accent);color:#000}
|
|
159
|
+
.btn-sm-ghost{background:transparent;color:var(--text-muted);border:1px solid var(--border-dim)}
|
|
160
|
+
.btn-sm-ghost:hover{border-color:var(--border-glow);color:var(--text-secondary)}
|
|
161
|
+
.btn-sm-green{background:var(--green-bg);color:var(--green);border:1px solid rgba(74,222,128,0.3)}
|
|
162
|
+
.btn-sm-green:hover{background:var(--green);color:#000}
|
|
163
|
+
.btn-sm-green:disabled{opacity:.4;cursor:not-allowed}
|
|
164
|
+
|
|
165
|
+
/* ── Scan / Add panels ── */
|
|
166
|
+
.panel{background:var(--bg-input);border:1px solid var(--border-dim);border-radius:10px;padding:16px;margin-bottom:16px}
|
|
167
|
+
.panel.hidden{display:none}
|
|
168
|
+
.panel-row{display:flex;gap:8px;align-items:center;margin-bottom:10px}
|
|
169
|
+
.panel-input{flex:1;background:var(--bg-card);border:1px solid var(--border-dim);border-radius:7px;padding:7px 11px;font-family:var(--font-mono);font-size:11px;color:var(--text-primary);outline:none;transition:border-color .2s}
|
|
170
|
+
.panel-input::placeholder{color:var(--text-muted)}
|
|
171
|
+
.panel-input:focus{border-color:var(--border-active)}
|
|
172
|
+
.scan-results{display:flex;flex-direction:column;gap:4px;margin-bottom:10px;max-height:200px;overflow-y:auto}
|
|
173
|
+
.scan-result-item{display:flex;align-items:center;gap:8px;padding:7px 9px;border-radius:7px;border:1px solid var(--border-dim);background:var(--bg-card);cursor:pointer;transition:border-color .2s}
|
|
174
|
+
.scan-result-item:hover{border-color:var(--border-glow)}
|
|
175
|
+
.scan-result-item input{accent-color:var(--accent);flex-shrink:0}
|
|
176
|
+
.scan-result-name{font-size:12px;font-weight:600;color:var(--text-primary);flex:1}
|
|
177
|
+
.scan-result-desc{font-size:10px;color:var(--text-muted);font-family:var(--font-mono);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:200px}
|
|
178
|
+
.panel-label{font-family:var(--font-mono);font-size:10px;color:var(--text-muted);margin-bottom:4px;text-transform:uppercase;letter-spacing:.04em}
|
|
179
|
+
.panel-hint{font-size:11px;color:var(--text-muted);margin-top:8px}
|
|
180
|
+
.form-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:10px}
|
|
181
|
+
.form-grid-full{grid-column:1/-1}
|
|
182
|
+
.type-toggle{display:flex;gap:4px;margin-bottom:10px}
|
|
183
|
+
.type-toggle-btn{font-family:var(--font-mono);font-size:11px;padding:5px 14px;border-radius:6px;border:1px solid var(--border-dim);background:transparent;color:var(--text-muted);cursor:pointer;transition:all .2s}
|
|
184
|
+
.type-toggle-btn.active{border-color:var(--accent);color:var(--accent);background:var(--accent-bg)}
|
|
185
|
+
|
|
186
|
+
/* ── Empty state ── */
|
|
187
|
+
.empty-state{padding:32px 20px;text-align:center;color:var(--text-muted)}
|
|
188
|
+
.empty-icon{font-size:28px;margin-bottom:8px}
|
|
189
|
+
.empty-title{font-family:var(--font-display);font-size:14px;font-weight:700;color:var(--text-secondary);margin-bottom:4px}
|
|
190
|
+
.empty-sub{font-size:12px}
|
|
191
|
+
|
|
192
|
+
/* ── Toast ── */
|
|
193
|
+
#toast-container{position:fixed;bottom:24px;right:24px;z-index:9999;display:flex;flex-direction:column;gap:8px;pointer-events:none}
|
|
194
|
+
.toast{font-family:var(--font-sans);font-size:12px;font-weight:500;padding:10px 16px;border-radius:8px;border:1px solid;backdrop-filter:blur(20px);pointer-events:none;animation:toast-in .2s ease;max-width:320px}
|
|
195
|
+
.toast-success{background:var(--green-bg);color:var(--green);border-color:rgba(74,222,128,0.3)}
|
|
196
|
+
.toast-error{background:var(--red-bg);color:var(--red);border-color:rgba(248,113,113,0.3)}
|
|
197
|
+
.toast-info{background:var(--accent-bg);color:var(--accent);border-color:rgba(34,211,238,0.3)}
|
|
198
|
+
@keyframes toast-in{from{opacity:0;transform:translateX(20px)}to{opacity:1;transform:translateX(0)}}
|
|
199
|
+
|
|
200
|
+
/* ── Modal ── */
|
|
201
|
+
.modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,0.7);backdrop-filter:blur(4px);z-index:500;display:flex;align-items:center;justify-content:center;opacity:0;pointer-events:none;transition:opacity .2s}
|
|
202
|
+
.modal-overlay.open{opacity:1;pointer-events:all}
|
|
203
|
+
.modal{background:var(--bg-surface);border:1px solid var(--border-glow);border-radius:16px;padding:28px;width:100%;max-width:440px;box-shadow:0 20px 60px rgba(0,0,0,0.5);transform:translateY(8px);transition:transform .2s}
|
|
204
|
+
.modal-overlay.open .modal{transform:translateY(0)}
|
|
205
|
+
.modal-title{font-family:var(--font-display);font-size:16px;font-weight:700;color:var(--text-primary);margin-bottom:4px}
|
|
206
|
+
.modal-sub{font-size:12px;color:var(--text-muted);margin-bottom:18px}
|
|
207
|
+
.agent-list{display:flex;flex-direction:column;gap:6px;max-height:240px;overflow-y:auto;margin-bottom:18px}
|
|
208
|
+
.agent-check-item{display:flex;align-items:center;gap:10px;padding:8px 10px;border-radius:8px;border:1px solid var(--border-dim);cursor:pointer;transition:all .2s;background:var(--bg-card)}
|
|
209
|
+
.agent-check-item:hover{border-color:var(--border-glow)}
|
|
210
|
+
.agent-check-item input[type="checkbox"]{accent-color:var(--accent);width:14px;height:14px;cursor:pointer}
|
|
211
|
+
.agent-check-name{font-size:12px;font-weight:600;color:var(--text-primary);flex:1}
|
|
212
|
+
.agent-check-id{font-family:var(--font-mono);font-size:10px;color:var(--text-muted)}
|
|
213
|
+
.modal-actions{display:flex;gap:8px}
|
|
214
|
+
.modal-btn{font-family:var(--font-sans);font-size:13px;font-weight:600;padding:9px 18px;border-radius:9px;border:none;cursor:pointer;transition:all .2s;flex:1}
|
|
215
|
+
.modal-btn-primary{background:var(--accent);color:#000}
|
|
216
|
+
.modal-btn-primary:hover{opacity:.9}
|
|
217
|
+
.modal-btn-primary:disabled{opacity:.4;cursor:not-allowed}
|
|
218
|
+
.modal-btn-secondary{background:var(--bg-input);color:var(--text-secondary);border:1px solid var(--border-dim)}
|
|
219
|
+
.modal-btn-secondary:hover{border-color:var(--border-glow);color:var(--text-primary)}
|
|
220
|
+
.missing-keys-alert{background:var(--amber-bg);border:1px solid rgba(251,191,36,0.3);border-radius:8px;padding:10px 12px;font-size:11px;color:var(--amber);margin-top:12px;line-height:1.5}
|
|
221
|
+
/* ── Publish dropdown ── */
|
|
222
|
+
.pub-wrap{position:relative;display:inline-block}
|
|
223
|
+
.pub-menu{position:absolute;bottom:calc(100% + 4px);left:0;min-width:156px;background:var(--bg-surface);border:1px solid var(--border-glow);border-radius:8px;box-shadow:0 8px 24px rgba(0,0,0,0.4);z-index:300;padding:4px;backdrop-filter:blur(20px);display:none}
|
|
224
|
+
.pub-menu.open{display:block}
|
|
225
|
+
.pub-menu-item{display:flex;align-items:center;gap:8px;padding:7px 10px;font-size:11px;font-weight:600;color:var(--text-secondary);border-radius:6px;cursor:pointer;transition:all .15s;background:none;border:none;width:100%;text-align:left;font-family:var(--font-sans)}
|
|
226
|
+
.pub-menu-item:hover{background:var(--accent-bg);color:var(--accent)}
|
|
227
|
+
.pub-menu-item.done{opacity:.5;cursor:default;pointer-events:none}
|
|
228
|
+
/* ── Import modal ── */
|
|
229
|
+
.imp-overlay{position:fixed;inset:0;background:rgba(0,0,0,0.6);z-index:900;display:flex;align-items:center;justify-content:center;backdrop-filter:blur(4px)}
|
|
230
|
+
.imp-box{background:var(--bg-surface);border:1px solid var(--border-glow);border-radius:14px;width:480px;max-height:80vh;display:flex;flex-direction:column;box-shadow:0 20px 60px rgba(0,0,0,0.5)}
|
|
231
|
+
.imp-head{padding:16px 20px;border-bottom:1px solid var(--border-dim);display:flex;align-items:center;justify-content:space-between}
|
|
232
|
+
.imp-title{font-size:14px;font-weight:700;color:var(--text-primary)}
|
|
233
|
+
.imp-close{background:none;border:none;color:var(--text-muted);font-size:18px;cursor:pointer;line-height:1;padding:0 4px}
|
|
234
|
+
.imp-body{padding:16px 20px;flex:1;overflow-y:auto}
|
|
235
|
+
.imp-path-row{display:flex;gap:8px;align-items:center;margin-bottom:12px}
|
|
236
|
+
.imp-path-input{flex:1;background:var(--bg-input);border:1px solid var(--border-dim);border-radius:8px;padding:8px 10px;font-size:12px;font-family:var(--font-mono,monospace);color:var(--text-primary)}
|
|
237
|
+
.imp-path-input:focus{outline:none;border-color:var(--accent)}
|
|
238
|
+
.imp-path-btn{padding:7px 12px;border-radius:8px;border:1px solid var(--border-dim);background:none;color:var(--text-secondary);font-size:11px;font-weight:600;cursor:pointer;white-space:nowrap}
|
|
239
|
+
.imp-path-btn:hover{border-color:var(--accent);color:var(--accent)}
|
|
240
|
+
.imp-preview-list{display:flex;flex-direction:column;gap:6px;margin-bottom:16px}
|
|
241
|
+
.imp-item{display:flex;align-items:center;gap:10px;padding:9px 12px;background:var(--bg-input);border:1px solid var(--border-dim);border-radius:8px;font-size:12px}
|
|
242
|
+
.imp-item-type{font-size:10px;font-weight:700;padding:2px 7px;border-radius:5px;text-transform:uppercase;letter-spacing:.05em;flex-shrink:0}
|
|
243
|
+
.imp-item-type.skill{background:rgba(139,92,246,0.2);color:#a78bfa}
|
|
244
|
+
.imp-item-type.prompt{background:rgba(251,191,36,0.2);color:var(--amber)}
|
|
245
|
+
.imp-item-type.agent{background:rgba(16,185,129,0.2);color:#34d399}
|
|
246
|
+
.imp-item-info{flex:1;min-width:0}
|
|
247
|
+
.imp-item-name{font-weight:600;color:var(--text-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
248
|
+
.imp-item-desc{font-size:11px;color:var(--text-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
249
|
+
.imp-foot{padding:12px 20px;border-top:1px solid var(--border-dim);display:flex;gap:10px;justify-content:flex-end}
|
|
250
|
+
.imp-scan-btn{padding:7px 14px;border-radius:8px;border:1px solid var(--accent);background:none;color:var(--accent);font-size:12px;font-weight:600;cursor:pointer}
|
|
251
|
+
.imp-scan-btn:hover{background:var(--accent-bg)}
|
|
252
|
+
.imp-import-btn{padding:7px 16px;border-radius:8px;border:none;background:var(--accent);color:#000;font-size:12px;font-weight:700;cursor:pointer}
|
|
253
|
+
.imp-import-btn:disabled{opacity:.5;cursor:default}
|
|
254
|
+
.imp-import-btn:not(:disabled):hover{opacity:.85}
|
|
255
|
+
.imp-dir-list{max-height:220px;overflow-y:auto;border:1px solid var(--border-dim);border-radius:8px;margin-bottom:12px}
|
|
256
|
+
.imp-dir-item{padding:9px 14px;cursor:pointer;font-size:12px;border-bottom:1px solid var(--border-dim);color:var(--text-secondary);transition:background .15s}
|
|
257
|
+
.imp-dir-item:last-child{border-bottom:none}
|
|
258
|
+
.imp-dir-item:hover{background:var(--bg-input);color:var(--text-primary)}
|
|
259
|
+
.imp-dir-parent{color:var(--text-muted);font-style:italic}
|
|
260
|
+
.imp-cur-path{font-size:10px;font-family:var(--font-mono,monospace);color:var(--text-muted);margin-bottom:8px;word-break:break-all}
|
|
261
|
+
/* ── Publish confirm modal ── */
|
|
262
|
+
.pub-confirm-overlay{position:fixed;inset:0;background:rgba(0,0,0,0.6);z-index:1000;display:flex;align-items:center;justify-content:center;backdrop-filter:blur(4px)}
|
|
263
|
+
.pub-confirm-box{background:var(--bg-surface);border:1px solid var(--border-glow);border-radius:14px;padding:24px 28px;max-width:400px;width:90%;box-shadow:0 20px 60px rgba(0,0,0,0.5)}
|
|
264
|
+
.pub-confirm-title{font-size:15px;font-weight:700;color:var(--text-primary);margin-bottom:8px}
|
|
265
|
+
.pub-confirm-body{font-size:12px;color:var(--text-secondary);line-height:1.6;margin-bottom:20px}
|
|
266
|
+
.pub-confirm-url{font-size:11px;font-family:var(--font-mono,monospace);color:var(--accent);background:var(--accent-bg);border-radius:6px;padding:5px 9px;margin:8px 0;word-break:break-all}
|
|
267
|
+
.pub-confirm-actions{display:flex;gap:10px;justify-content:flex-end}
|
|
268
|
+
.pub-confirm-cancel{padding:7px 16px;border-radius:8px;border:1px solid var(--border);background:none;color:var(--text-secondary);font-size:12px;font-weight:600;cursor:pointer}
|
|
269
|
+
.pub-confirm-cancel:hover{background:var(--bg-hover)}
|
|
270
|
+
.pub-confirm-ok{padding:7px 16px;border-radius:8px;border:none;background:var(--accent);color:#000;font-size:12px;font-weight:700;cursor:pointer}
|
|
271
|
+
.pub-confirm-ok:hover{opacity:.85}
|
|
272
|
+
/* ── Platform default toggle ── */
|
|
273
|
+
.pill-platform{border-color:rgba(251,191,36,0.4);color:var(--amber)}
|
|
274
|
+
.pill-platform.active{border-color:var(--amber);color:var(--amber);background:var(--amber-bg)}
|
|
275
|
+
.badge-platform{background:var(--amber-bg);color:var(--amber);border:1px solid rgba(251,191,36,0.35)}
|
|
276
|
+
.btn-default-on{background:var(--amber-bg);color:var(--amber);border:1px solid rgba(251,191,36,0.4);font-size:11px;padding:5px 10px;border-radius:7px;cursor:pointer;font-family:var(--font-mono);font-weight:600;transition:all .2s;white-space:nowrap}
|
|
277
|
+
.btn-default-on:hover{background:var(--amber);color:#000}
|
|
278
|
+
.btn-default-off{background:var(--bg-input);color:var(--text-muted);border:1px solid var(--border-dim);font-size:11px;padding:5px 10px;border-radius:7px;cursor:pointer;font-family:var(--font-mono);font-weight:600;transition:all .2s;white-space:nowrap}
|
|
279
|
+
.btn-default-off:hover{border-color:rgba(251,191,36,0.4);color:var(--amber)}
|
|
280
|
+
.card-platform-default{border-color:rgba(251,191,36,0.25)}
|
|
281
|
+
</style>
|
|
282
|
+
</head>
|
|
283
|
+
<body>
|
|
284
|
+
|
|
285
|
+
<!-- Publish confirmation modal -->
|
|
286
|
+
<div id="pubConfirmOverlay" class="pub-confirm-overlay" style="display:none" onclick="if(event.target===this)closePubConfirm()">
|
|
287
|
+
<div class="pub-confirm-box">
|
|
288
|
+
<div class="pub-confirm-title" id="pubConfirmTitle">Publish to SaaS</div>
|
|
289
|
+
<div class="pub-confirm-body" id="pubConfirmBody"></div>
|
|
290
|
+
<div class="pub-confirm-actions">
|
|
291
|
+
<button class="pub-confirm-cancel" onclick="closePubConfirm()">Cancel</button>
|
|
292
|
+
<button class="pub-confirm-ok" id="pubConfirmOk">Publish</button>
|
|
293
|
+
</div>
|
|
294
|
+
</div>
|
|
295
|
+
</div>
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
<!-- Import from folder modal -->
|
|
299
|
+
<div id="impOverlay" class="imp-overlay" style="display:none" onclick="if(event.target===this)closeImport()">
|
|
300
|
+
<div class="imp-box" onclick="event.stopPropagation()">
|
|
301
|
+
<div class="imp-head">
|
|
302
|
+
<span class="imp-title" id="impTitle">↓ Import from Folder</span>
|
|
303
|
+
<button class="imp-close" onclick="closeImport()">×</button>
|
|
304
|
+
</div>
|
|
305
|
+
<div class="imp-body" id="impBody">
|
|
306
|
+
<!-- Step 1: pick folder -->
|
|
307
|
+
<div id="impStep1">
|
|
308
|
+
<div style="font-size:12px;color:var(--text-secondary);margin-bottom:12px;line-height:1.6">
|
|
309
|
+
Paste the path to the folder you downloaded from the SaaS app, or browse to it below.
|
|
310
|
+
</div>
|
|
311
|
+
<div class="imp-path-row">
|
|
312
|
+
<input class="imp-path-input" id="impPathInput" placeholder="~/Desktop/my-skill-export" onkeydown="if(event.key==='Enter')impScan()">
|
|
313
|
+
<button class="imp-path-btn" onclick="impBrowseOpen()">Browse</button>
|
|
314
|
+
</div>
|
|
315
|
+
<!-- inline dir browser -->
|
|
316
|
+
<div id="impBrowser" style="display:none">
|
|
317
|
+
<div class="imp-cur-path" id="impBrowserPath"></div>
|
|
318
|
+
<div class="imp-dir-list" id="impBrowserList"></div>
|
|
319
|
+
<div style="display:flex;gap:8px;margin-bottom:12px">
|
|
320
|
+
<button class="imp-path-btn" onclick="impBrowserSelect()" style="flex:1;text-align:center">Use this folder</button>
|
|
321
|
+
</div>
|
|
322
|
+
</div>
|
|
323
|
+
<div id="impScanMsg" style="font-size:12px;color:var(--text-muted);min-height:18px"></div>
|
|
324
|
+
</div>
|
|
325
|
+
<!-- Step 2: preview -->
|
|
326
|
+
<div id="impStep2" style="display:none">
|
|
327
|
+
<div style="font-size:12px;color:var(--text-secondary);margin-bottom:10px">Found <strong id="impFoundCount">0</strong> item(s) ready to import:</div>
|
|
328
|
+
<div class="imp-preview-list" id="impPreviewList"></div>
|
|
329
|
+
<div id="impErrMsg" style="font-size:11px;color:#f87171;margin-top:-4px;margin-bottom:8px;display:none"></div>
|
|
330
|
+
</div>
|
|
331
|
+
</div>
|
|
332
|
+
<div class="imp-foot">
|
|
333
|
+
<button class="imp-path-btn" onclick="closeImport()">Cancel</button>
|
|
334
|
+
<button class="imp-scan-btn" id="impScanBtn" onclick="impScan()">Scan Folder</button>
|
|
335
|
+
<button class="imp-import-btn" id="impDoImportBtn" style="display:none" onclick="impDoImport()">Import All</button>
|
|
336
|
+
</div>
|
|
337
|
+
</div>
|
|
338
|
+
</div>
|
|
339
|
+
|
|
340
|
+
<div class="topbar">
|
|
341
|
+
<a href="/" style="display:flex;align-items:center;gap:10px;margin-right:28px;text-decoration:none">
|
|
342
|
+
<img class="logo-mark" src="/MyAIforOne-logomark-transparent.svg" alt="MyAIforOne">
|
|
343
|
+
<span class="logo-text">MyAIforOne</span>
|
|
344
|
+
</a>
|
|
345
|
+
<nav class="tab-group">
|
|
346
|
+
<a class="tab-btn" href="/">Home</a>
|
|
347
|
+
<a class="tab-btn" href="/org">Agents</a>
|
|
348
|
+
<a class="tab-btn" href="/ui">Chat</a>
|
|
349
|
+
<a class="tab-btn active" href="/library">Library</a>
|
|
350
|
+
<a class="tab-btn" href="/lab">Lab</a>
|
|
351
|
+
</nav>
|
|
352
|
+
<div class="topbar-right">
|
|
353
|
+
<a class="gym-nav-btn gym-tab-link" href="/gym" style="display:none">Gym</a>
|
|
354
|
+
<a href="/marketplace" class="gear-btn" title="Marketplace" style="font-size:11px;font-weight:700;letter-spacing:.03em;padding:0 10px;width:auto">Marketplace</a>
|
|
355
|
+
<a href="/monitor" class="gear-btn" title="Monitor" style="font-size:11px;font-weight:700;letter-spacing:.03em;padding:0 10px;width:auto">Monitor</a>
|
|
356
|
+
<a href="/admin" class="gear-btn" title="Admin" style="font-size:11px;font-weight:700;letter-spacing:.03em;padding:0 10px;width:auto">Admin</a>
|
|
357
|
+
<a href="/user-guide" class="gear-btn" title="User Guide" style="font-size:11px;font-weight:700;letter-spacing:.03em;padding:0 10px;width:auto">User Guide</a>
|
|
358
|
+
<button onclick="window.open('/mini','minibar','width=440,height=460,resizable=yes,scrollbars=no')" title="Open Mini Bar" style="display:flex;align-items:center;justify-content:center;width:34px;height:34px;border-radius:8px;border:1px solid var(--border-dim);background:transparent;color:var(--text-muted);cursor:pointer;font-size:15px;transition:all .2s" onmouseover="this.style.borderColor='var(--border-glow)'" onmouseout="this.style.borderColor='var(--border-dim)'">⊡</button>
|
|
359
|
+
<button class="theme-toggle" onclick="toggleTheme()" title="Toggle theme">☀</button>
|
|
360
|
+
</div>
|
|
361
|
+
</div>
|
|
362
|
+
|
|
363
|
+
<div class="canvas" id="canvas">
|
|
364
|
+
<div class="toolbar">
|
|
365
|
+
<div class="type-tabs">
|
|
366
|
+
<button class="type-tab active" data-type="apps" onclick="switchType('apps')">Apps</button>
|
|
367
|
+
<button class="type-tab" data-type="skills" onclick="switchType('skills')">Skills</button>
|
|
368
|
+
<button class="type-tab" data-type="mcps" onclick="switchType('mcps')">MCPs</button>
|
|
369
|
+
<button class="type-tab" data-type="prompts" onclick="switchType('prompts')">Prompts</button>
|
|
370
|
+
</div>
|
|
371
|
+
<div id="trigger-config" style="display:none;align-items:center;gap:6px;background:var(--bg-input);border:1px solid var(--border-dim);border-radius:8px;padding:5px 10px">
|
|
372
|
+
<span style="font-family:var(--font-mono);font-size:10px;color:var(--text-muted)">trigger:</span>
|
|
373
|
+
<span id="trigger-display" style="font-family:var(--font-mono);font-size:13px;font-weight:700;color:var(--accent);min-width:16px;text-align:center"></span>
|
|
374
|
+
<button class="btn-sm btn-sm-ghost" style="padding:3px 8px;font-size:10px" onclick="editTrigger()">✎</button>
|
|
375
|
+
</div>
|
|
376
|
+
<div class="search-wrap">
|
|
377
|
+
<span class="search-icon">⌕</span>
|
|
378
|
+
<input class="search-input" id="search-input" placeholder="Search…" oninput="render()">
|
|
379
|
+
</div>
|
|
380
|
+
<div class="filter-pills" id="filter-pills"></div>
|
|
381
|
+
<button class="btn-sm btn-sm-ghost" onclick="openImport()" title="Import skills, prompts, or agents from a SaaS export folder" style="white-space:nowrap;flex-shrink:0">↓ Import</button>
|
|
382
|
+
</div>
|
|
383
|
+
<div id="content"></div>
|
|
384
|
+
</div>
|
|
385
|
+
|
|
386
|
+
<div id="toast-container"></div>
|
|
387
|
+
|
|
388
|
+
<!-- Add App Panel (sits above canvas content, shown on demand) -->
|
|
389
|
+
<div class="panel hidden" id="add-app-panel" style="position:fixed;top:68px;left:40px;right:40px;z-index:200;max-width:700px;margin:0 auto">
|
|
390
|
+
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:10px">
|
|
391
|
+
<span class="panel-label" style="margin:0">Register <span id="add-app-provider-label">External Source</span> App</span>
|
|
392
|
+
<button class="btn-sm btn-sm-ghost" style="padding:3px 8px;font-size:10px" onclick="toggleAddAppPanel()">✕</button>
|
|
393
|
+
</div>
|
|
394
|
+
<div class="form-grid" style="margin-bottom:10px">
|
|
395
|
+
<div>
|
|
396
|
+
<div class="panel-label">Name</div>
|
|
397
|
+
<input class="panel-input" id="app-name" placeholder="My App" style="width:100%" oninput="syncAppId()">
|
|
398
|
+
</div>
|
|
399
|
+
<div>
|
|
400
|
+
<div class="panel-label">ID <span style="font-size:9px;color:var(--text-muted)">(auto)</span></div>
|
|
401
|
+
<input class="panel-input" id="app-id" placeholder="my-app" style="width:100%">
|
|
402
|
+
</div>
|
|
403
|
+
<div class="form-grid-full">
|
|
404
|
+
<div class="panel-label">URL</div>
|
|
405
|
+
<input class="panel-input" id="app-url" placeholder="https://myapp.example.com" style="width:100%">
|
|
406
|
+
</div>
|
|
407
|
+
<div class="form-grid-full">
|
|
408
|
+
<div class="panel-label">Short description</div>
|
|
409
|
+
<input class="panel-input" id="app-desc" placeholder="What does this app do?" style="width:100%">
|
|
410
|
+
</div>
|
|
411
|
+
<div>
|
|
412
|
+
<div class="panel-label">Category</div>
|
|
413
|
+
<input class="panel-input" id="app-category" placeholder="Finance, Compliance…" style="width:100%">
|
|
414
|
+
</div>
|
|
415
|
+
<div>
|
|
416
|
+
<div class="panel-label">GitHub Repo (optional)</div>
|
|
417
|
+
<input class="panel-input" id="app-repo" placeholder="owner/repo" style="width:100%">
|
|
418
|
+
</div>
|
|
419
|
+
</div>
|
|
420
|
+
<div style="display:flex;gap:8px">
|
|
421
|
+
<button class="btn-sm btn-sm-green" onclick="doAddApp()">Register App</button>
|
|
422
|
+
<button class="btn-sm btn-sm-ghost" onclick="toggleAddAppPanel()">Cancel</button>
|
|
423
|
+
</div>
|
|
424
|
+
</div>
|
|
425
|
+
|
|
426
|
+
<!-- Assign Modal -->
|
|
427
|
+
<div class="modal-overlay" id="assign-modal">
|
|
428
|
+
<div class="modal">
|
|
429
|
+
<div class="modal-title" id="modal-title">Assign to agents</div>
|
|
430
|
+
<div class="modal-sub" id="modal-sub">Select agents (optional)</div>
|
|
431
|
+
<div class="agent-list" id="modal-agent-list"></div>
|
|
432
|
+
<div id="modal-missing-keys"></div>
|
|
433
|
+
<div class="modal-actions">
|
|
434
|
+
<button class="modal-btn modal-btn-primary" id="modal-assign-btn" onclick="doAssign()">Assign selected</button>
|
|
435
|
+
<button class="modal-btn modal-btn-secondary" onclick="closeModal()">Skip for now</button>
|
|
436
|
+
</div>
|
|
437
|
+
</div>
|
|
438
|
+
</div>
|
|
439
|
+
|
|
440
|
+
<script>
|
|
441
|
+
// ─── State ────────────────────────────────────────────────────────
|
|
442
|
+
let currentType = 'apps';
|
|
443
|
+
let allItems = [];
|
|
444
|
+
let activeCategory = 'all';
|
|
445
|
+
let showPlatformDefaultsOnly = false;
|
|
446
|
+
let modalItem = null;
|
|
447
|
+
let modalType = null;
|
|
448
|
+
let agentsList = [];
|
|
449
|
+
let promptTrigger = '!';
|
|
450
|
+
let saasCfg = { connected: false };
|
|
451
|
+
|
|
452
|
+
// ─── Theme ────────────────────────────────────────────────────────
|
|
453
|
+
(function(){
|
|
454
|
+
const s = localStorage.getItem('theme')||'dark';
|
|
455
|
+
document.documentElement.setAttribute('data-theme', s);
|
|
456
|
+
document.querySelector('.theme-toggle').textContent = s==='dark'?'☀':'☾';
|
|
457
|
+
})();
|
|
458
|
+
function toggleTheme(){
|
|
459
|
+
const c = document.documentElement.getAttribute('data-theme')||'dark';
|
|
460
|
+
const n = c==='dark'?'light':'dark';
|
|
461
|
+
document.documentElement.setAttribute('data-theme',n);
|
|
462
|
+
localStorage.setItem('theme',n);
|
|
463
|
+
document.querySelector('.theme-toggle').textContent = n==='dark'?'☀':'☾';
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// ─── Toast ────────────────────────────────────────────────────────
|
|
467
|
+
function toast(msg, type='info'){
|
|
468
|
+
const el = document.createElement('div');
|
|
469
|
+
el.className = `toast toast-${type}`;
|
|
470
|
+
el.textContent = msg;
|
|
471
|
+
document.getElementById('toast-container').appendChild(el);
|
|
472
|
+
setTimeout(()=>el.remove(), 3500);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// ─── Escape ───────────────────────────────────────────────────────
|
|
476
|
+
function esc(s){ return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"') }
|
|
477
|
+
|
|
478
|
+
// ─── Slug ─────────────────────────────────────────────────────────
|
|
479
|
+
function slugify(s){ return s.toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/^-|-$/g,'') }
|
|
480
|
+
|
|
481
|
+
// ─── Load agents ──────────────────────────────────────────────────
|
|
482
|
+
async function loadAgents(){
|
|
483
|
+
try{ const d = await (await fetch('/api/agents')).json(); agentsList = d.agents||[]; } catch{ agentsList=[]; }
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// ─── Prompt trigger config ────────────────────────────────────────
|
|
487
|
+
async function loadTrigger(){
|
|
488
|
+
try{ const d = await (await fetch('/api/marketplace/prompt-trigger')).json(); promptTrigger = d.trigger||'!'; } catch{ promptTrigger='!'; }
|
|
489
|
+
document.getElementById('trigger-display').textContent = promptTrigger;
|
|
490
|
+
}
|
|
491
|
+
function editTrigger(){
|
|
492
|
+
const c = prompt(`Set prompt trigger character (current: "${promptTrigger}"):`, promptTrigger);
|
|
493
|
+
if(!c || c.length !== 1){ if(c!==null) toast('Must be exactly 1 character','error'); return; }
|
|
494
|
+
fetch('/api/marketplace/prompt-trigger',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({trigger:c})})
|
|
495
|
+
.then(r=>r.json()).then(d=>{ if(d.ok){ promptTrigger=c; document.getElementById('trigger-display').textContent=c; toast(`Trigger set to "${c}"`,'success'); } else toast(d.error||'Failed','error'); })
|
|
496
|
+
.catch(()=>toast('Failed to set trigger','error'));
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// ─── Type switch ──────────────────────────────────────────────────
|
|
500
|
+
function switchType(type){
|
|
501
|
+
currentType = type; activeCategory = 'all'; showPlatformDefaultsOnly = false;
|
|
502
|
+
document.querySelectorAll('.type-tab').forEach(t=>t.classList.toggle('active', t.dataset.type===type));
|
|
503
|
+
document.getElementById('search-input').value = '';
|
|
504
|
+
// Show trigger config only on Prompts tab
|
|
505
|
+
const tc = document.getElementById('trigger-config');
|
|
506
|
+
tc.style.display = type==='prompts' ? 'flex' : 'none';
|
|
507
|
+
loadItems();
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// ─── Load items ───────────────────────────────────────────────────
|
|
511
|
+
async function loadItems(){
|
|
512
|
+
document.getElementById('content').innerHTML = '<div style="padding:60px;text-align:center;color:var(--text-muted)">Loading…</div>';
|
|
513
|
+
try{
|
|
514
|
+
const d = await (await fetch(`/api/marketplace/${currentType}?source=personal`)).json();
|
|
515
|
+
allItems = (d.items||[]);
|
|
516
|
+
buildPills();
|
|
517
|
+
render();
|
|
518
|
+
} catch{
|
|
519
|
+
document.getElementById('content').innerHTML = '<div style="padding:60px;text-align:center;color:var(--text-muted)">Failed to load registry.</div>';
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// ─── Category pills ───────────────────────────────────────────────
|
|
524
|
+
function buildPills(){
|
|
525
|
+
const cats = [...new Set(allItems.map(i=>i.category).filter(Boolean))].sort();
|
|
526
|
+
const el = document.getElementById('filter-pills');
|
|
527
|
+
el.innerHTML = '';
|
|
528
|
+
const all = mkPill('all', true); el.appendChild(all);
|
|
529
|
+
cats.forEach(c=>el.appendChild(mkPill(c, false)));
|
|
530
|
+
// Platform Defaults pill (skills and MCPs only)
|
|
531
|
+
if(currentType !== 'agents' && currentType !== 'apps'){
|
|
532
|
+
const pd = document.createElement('button');
|
|
533
|
+
pd.id = 'pill-platform-defaults';
|
|
534
|
+
pd.className = 'pill pill-platform' + (showPlatformDefaultsOnly ? ' active' : '');
|
|
535
|
+
pd.textContent = '★ Platform Defaults';
|
|
536
|
+
pd.onclick = ()=>{
|
|
537
|
+
showPlatformDefaultsOnly = !showPlatformDefaultsOnly;
|
|
538
|
+
pd.classList.toggle('active', showPlatformDefaultsOnly);
|
|
539
|
+
render();
|
|
540
|
+
};
|
|
541
|
+
el.appendChild(pd);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
function mkPill(cat, active){
|
|
545
|
+
const b = document.createElement('button');
|
|
546
|
+
b.className = 'pill'+(active?' active':''); b.textContent = cat;
|
|
547
|
+
b.onclick = ()=>{ activeCategory=cat; document.querySelectorAll('.pill').forEach(p=>p.classList.toggle('active', p.textContent===cat)); render(); };
|
|
548
|
+
return b;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// ─── Main render ──────────────────────────────────────────────────
|
|
552
|
+
function render(){
|
|
553
|
+
const q = (document.getElementById('search-input').value||'').toLowerCase().trim();
|
|
554
|
+
let items = allItems;
|
|
555
|
+
if(activeCategory!=='all') items = items.filter(i=>i.category===activeCategory);
|
|
556
|
+
if(q) items = items.filter(i=>(i.name||'').toLowerCase().includes(q)||(i.description||'').toLowerCase().includes(q)||(i.tags||[]).some(t=>t.toLowerCase().includes(q)));
|
|
557
|
+
if(showPlatformDefaultsOnly) items = items.filter(i=>i.isPlatformDefault);
|
|
558
|
+
items = [...items].sort((a, b) => (a.name || '').localeCompare(b.name || ''));
|
|
559
|
+
|
|
560
|
+
if(currentType==='agents'){
|
|
561
|
+
renderFlat(items);
|
|
562
|
+
} else {
|
|
563
|
+
renderSections(items);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function renderFlat(items){
|
|
569
|
+
const el = document.getElementById('content');
|
|
570
|
+
if(!items.length){ el.innerHTML = emptyHtml('Nothing found'); return; }
|
|
571
|
+
el.innerHTML = `<div class="grid">${items.map(i=>cardHtml(i, currentType==='mcps'?'mcp':'agent')).join('')}</div>`;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function renderSections(items){
|
|
575
|
+
const typeSingular = currentType==='mcps'?'mcp':currentType==='prompts'?'prompt':currentType==='apps'?'app':'skill';
|
|
576
|
+
const myItems = items.filter(i=>i.provider==='me');
|
|
577
|
+
const othersItems = items.filter(i=>i.provider!=='me');
|
|
578
|
+
const html = '<div class="sections">' + sectionPersonal(myItems, typeSingular) + sectionOthers(othersItems, typeSingular) + '</div>';
|
|
579
|
+
document.getElementById('content').innerHTML = html;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function section(title, items, typeSingular, count, actionBtn=''){
|
|
583
|
+
return `
|
|
584
|
+
<div>
|
|
585
|
+
<div class="section-header">
|
|
586
|
+
<span class="section-title">${esc(title)}</span>
|
|
587
|
+
<span class="section-count">${count}</span>
|
|
588
|
+
${actionBtn}
|
|
589
|
+
</div>
|
|
590
|
+
${items.length
|
|
591
|
+
? `<div class="grid">${items.map(i=>cardHtml(i,typeSingular)).join('')}</div>`
|
|
592
|
+
: `<div class="empty-state"><div class="empty-sub">None available.</div></div>`}
|
|
593
|
+
</div>`;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function addSplitBtn(type, label, choices){
|
|
597
|
+
const id = `add-choices-${type}`;
|
|
598
|
+
const choiceHtml = choices.map(([txt, action, isBuild])=>
|
|
599
|
+
`<button class="btn-choice${isBuild?' btn-choice-build':''}" onclick="${action};closeAddChoices('${id}')">${txt}</button>`
|
|
600
|
+
).join('');
|
|
601
|
+
return `<div class="add-split" id="${id}">
|
|
602
|
+
<button class="btn-sm btn-sm-accent" onclick="toggleAddChoices('${id}')">⊕ Add ${label}</button>
|
|
603
|
+
<div class="add-choices hidden">${choiceHtml}</div>
|
|
604
|
+
</div>`;
|
|
605
|
+
}
|
|
606
|
+
function toggleAddChoices(id){
|
|
607
|
+
const el = document.getElementById(id);
|
|
608
|
+
if(!el) return;
|
|
609
|
+
el.querySelector('.add-choices').classList.toggle('hidden');
|
|
610
|
+
}
|
|
611
|
+
function closeAddChoices(id){
|
|
612
|
+
const el = document.getElementById(id);
|
|
613
|
+
if(el) el.querySelector('.add-choices').classList.add('hidden');
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function sectionPersonal(items, typeSingular){
|
|
617
|
+
const mktType = typeSingular==='mcp' ? 'mcps' : typeSingular+'s';
|
|
618
|
+
const actionBtn = typeSingular==='app'
|
|
619
|
+
? addSplitBtn('app','App',[
|
|
620
|
+
['Register', "toggleRegisterAppPanel()"],
|
|
621
|
+
['AI Build', "window.location='/lab?build=app'", true],
|
|
622
|
+
['Marketplace', `window.location='/marketplace?type=${mktType}'`]])
|
|
623
|
+
: typeSingular==='skill'
|
|
624
|
+
? addSplitBtn('skill','Skill',[
|
|
625
|
+
['Scan', "toggleScanPanel()"],
|
|
626
|
+
['AI Build', "window.location='/lab?build=skill'", true],
|
|
627
|
+
['Marketplace', `window.location='/marketplace?type=${mktType}'`]])
|
|
628
|
+
: typeSingular==='prompt'
|
|
629
|
+
? addSplitBtn('prompt','Prompt',[
|
|
630
|
+
['Paste', "toggleCreatePromptPanel()"],
|
|
631
|
+
['AI Build', "window.location='/lab?build=prompt'", true],
|
|
632
|
+
['Marketplace', `window.location='/marketplace?type=${mktType}'`]])
|
|
633
|
+
: addSplitBtn('mcp','MCP',[
|
|
634
|
+
['Add', "toggleAddMcpPanel()"],
|
|
635
|
+
['Marketplace', `window.location='/marketplace?type=${mktType}'`]]);
|
|
636
|
+
|
|
637
|
+
const panelHtml = typeSingular==='skill' ? scanPanelHtml() : typeSingular==='prompt' ? createPromptPanelHtml() : typeSingular==='app' ? registerAppPanelHtml() : addMcpPanelHtml();
|
|
638
|
+
|
|
639
|
+
const emptyMsg = typeSingular==='skill'?'Scan your skills folder to import personal skills.'
|
|
640
|
+
:typeSingular==='prompt'?'Paste in a prompt template to save it.'
|
|
641
|
+
:typeSingular==='app'?'Register an app to track it here.'
|
|
642
|
+
:'Add a custom HTTP or stdio MCP you built.';
|
|
643
|
+
|
|
644
|
+
return `
|
|
645
|
+
<div>
|
|
646
|
+
<div class="section-header">
|
|
647
|
+
<span class="section-title">Built by Me — ${typeSingular.charAt(0).toUpperCase()+typeSingular.slice(1)}s</span>
|
|
648
|
+
<span class="section-count">${items.length}</span>
|
|
649
|
+
${actionBtn}
|
|
650
|
+
</div>
|
|
651
|
+
${panelHtml}
|
|
652
|
+
${items.length
|
|
653
|
+
? `<div class="grid">${items.map(i=>cardHtml(i,typeSingular)).join('')}</div>`
|
|
654
|
+
: `<div class="empty-state"><div class="empty-icon">◎</div><div class="empty-title">None yet</div><div class="empty-sub">${emptyMsg}</div></div>`}
|
|
655
|
+
</div>`;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function sectionOthers(items, typeSingular){
|
|
659
|
+
if(!items.length) return '';
|
|
660
|
+
return `
|
|
661
|
+
<div>
|
|
662
|
+
<div class="section-header">
|
|
663
|
+
<span class="section-title">Built by Others — ${typeSingular.charAt(0).toUpperCase()+typeSingular.slice(1)}s</span>
|
|
664
|
+
<span class="section-count">${items.length}</span>
|
|
665
|
+
</div>
|
|
666
|
+
<div class="grid">${items.map(i=>cardHtml(i,typeSingular)).join('')}</div>
|
|
667
|
+
</div>`;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// ─── Scan panel (skills) ──────────────────────────────────────────
|
|
671
|
+
function scanPanelHtml(){
|
|
672
|
+
return `
|
|
673
|
+
<div class="panel hidden" id="scan-panel">
|
|
674
|
+
<div class="panel-label">Scan folder</div>
|
|
675
|
+
<div class="panel-row">
|
|
676
|
+
<input class="panel-input" id="scan-dir-input" placeholder="~/.claude/commands" value="">
|
|
677
|
+
<button class="btn-sm btn-sm-accent" onclick="doScan()">Scan</button>
|
|
678
|
+
</div>
|
|
679
|
+
<div class="panel-hint">Default: ~/.claude/commands — change to any folder containing .md skill files.</div>
|
|
680
|
+
<div id="scan-results" style="margin-top:10px"></div>
|
|
681
|
+
</div>`;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function toggleScanPanel(){
|
|
685
|
+
const p = document.getElementById('scan-panel');
|
|
686
|
+
if(!p) return;
|
|
687
|
+
const open = !p.classList.contains('hidden');
|
|
688
|
+
p.classList.toggle('hidden', open);
|
|
689
|
+
if(!open && !document.getElementById('scan-dir-input').value){
|
|
690
|
+
document.getElementById('scan-dir-input').value = '~/.claude/commands';
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
async function doScan(){
|
|
695
|
+
const dir = document.getElementById('scan-dir-input').value.trim() || '~/.claude/commands';
|
|
696
|
+
const el = document.getElementById('scan-results');
|
|
697
|
+
el.innerHTML = '<div style="font-size:11px;color:var(--text-muted)">Scanning…</div>';
|
|
698
|
+
try{
|
|
699
|
+
const res = await fetch(`/api/marketplace/scan-skills?dir=${encodeURIComponent(dir)}`);
|
|
700
|
+
const data = await res.json();
|
|
701
|
+
if(!res.ok) throw new Error(data.error||'Scan failed');
|
|
702
|
+
if(!data.files.length){
|
|
703
|
+
el.innerHTML = '<div style="font-size:11px;color:var(--text-muted)">No new skills found — all .md files in this folder are already in the registry.</div>';
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
let html = `<div class="scan-results">`;
|
|
707
|
+
for(const f of data.files){
|
|
708
|
+
html += `<label class="scan-result-item">
|
|
709
|
+
<input type="checkbox" value="${esc(JSON.stringify(f))}" checked>
|
|
710
|
+
<span class="scan-result-name">${esc(f.name||f.id)}</span>
|
|
711
|
+
<span class="scan-result-desc">${esc(f.description||f.filename)}</span>
|
|
712
|
+
</label>`;
|
|
713
|
+
}
|
|
714
|
+
html += `</div>`;
|
|
715
|
+
html += `<button class="btn-sm btn-sm-green" id="import-btn" onclick="doImport(${esc(JSON.stringify(data.files))})">Import selected (${data.files.length})</button>`;
|
|
716
|
+
el.innerHTML = html;
|
|
717
|
+
// Update import btn count on checkbox change
|
|
718
|
+
el.querySelectorAll('input[type=checkbox]').forEach(cb=>{
|
|
719
|
+
cb.addEventListener('change', ()=>{
|
|
720
|
+
const n = el.querySelectorAll('input[type=checkbox]:checked').length;
|
|
721
|
+
const btn = document.getElementById('import-btn');
|
|
722
|
+
if(btn){ btn.textContent = `Import selected (${n})`; btn.disabled = n===0; }
|
|
723
|
+
});
|
|
724
|
+
});
|
|
725
|
+
} catch(e){ el.innerHTML = `<div style="font-size:11px;color:var(--red)">${esc(e.message)}</div>`; }
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
async function doImport(allFiles){
|
|
729
|
+
const el = document.getElementById('scan-results');
|
|
730
|
+
const checked = el.querySelectorAll('input[type=checkbox]:checked');
|
|
731
|
+
const files = [...checked].map(cb=>{ try{ return JSON.parse(cb.value); } catch{ return null; } }).filter(Boolean);
|
|
732
|
+
if(!files.length) return;
|
|
733
|
+
const btn = document.getElementById('import-btn');
|
|
734
|
+
if(btn){ btn.disabled=true; btn.textContent='Importing…'; }
|
|
735
|
+
try{
|
|
736
|
+
const res = await fetch('/api/marketplace/import-skills',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({files})});
|
|
737
|
+
const data = await res.json();
|
|
738
|
+
if(!res.ok) throw new Error(data.error||'Import failed');
|
|
739
|
+
toast(`${data.imported.length} skill${data.imported.length!==1?'s':''} imported`, 'success');
|
|
740
|
+
document.getElementById('scan-panel').classList.add('hidden');
|
|
741
|
+
await loadItems();
|
|
742
|
+
} catch(e){ toast(`Import failed: ${e.message}`,'error'); if(btn){btn.disabled=false;btn.textContent='Import selected';} }
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// ─── Create prompt panel ──────────────────────────────────────────
|
|
746
|
+
function createPromptPanelHtml(){
|
|
747
|
+
return `
|
|
748
|
+
<div class="panel hidden" id="create-prompt-panel">
|
|
749
|
+
<div class="form-grid" style="margin-bottom:10px">
|
|
750
|
+
<div>
|
|
751
|
+
<div class="panel-label">Name</div>
|
|
752
|
+
<input class="panel-input" id="prompt-name" placeholder="Code Review" style="width:100%" oninput="syncPromptId()">
|
|
753
|
+
</div>
|
|
754
|
+
<div>
|
|
755
|
+
<div class="panel-label">ID <span style="font-size:9px;color:var(--text-muted)">(auto)</span></div>
|
|
756
|
+
<input class="panel-input" id="prompt-id" placeholder="code-review" style="width:100%">
|
|
757
|
+
</div>
|
|
758
|
+
<div class="form-grid-full">
|
|
759
|
+
<div class="panel-label">Description</div>
|
|
760
|
+
<input class="panel-input" id="prompt-desc" placeholder="What this prompt does…" style="width:100%">
|
|
761
|
+
</div>
|
|
762
|
+
<div class="form-grid-full">
|
|
763
|
+
<div class="panel-label">Prompt content <span style="font-size:9px;color:var(--text-muted)">(paste it here)</span></div>
|
|
764
|
+
<textarea class="panel-input" id="prompt-content" placeholder="You are a… / Act as… / etc." style="width:100%;height:120px;resize:vertical;font-family:var(--font-mono);font-size:11px;line-height:1.5"></textarea>
|
|
765
|
+
</div>
|
|
766
|
+
</div>
|
|
767
|
+
<div style="display:flex;gap:8px">
|
|
768
|
+
<button class="btn-sm btn-sm-green" onclick="doCreatePrompt()">Save prompt</button>
|
|
769
|
+
<button class="btn-sm btn-sm-ghost" onclick="toggleCreatePromptPanel()">Cancel</button>
|
|
770
|
+
</div>
|
|
771
|
+
</div>`;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function toggleCreatePromptPanel(){
|
|
775
|
+
const p = document.getElementById('create-prompt-panel');
|
|
776
|
+
if(p) p.classList.toggle('hidden');
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function syncPromptId(){
|
|
780
|
+
document.getElementById('prompt-id').value = slugify(document.getElementById('prompt-name').value);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
async function doCreatePrompt(){
|
|
784
|
+
const id = document.getElementById('prompt-id').value.trim();
|
|
785
|
+
const name = document.getElementById('prompt-name').value.trim();
|
|
786
|
+
const desc = document.getElementById('prompt-desc').value.trim();
|
|
787
|
+
const content = document.getElementById('prompt-content').value.trim();
|
|
788
|
+
if(!id||!name){ toast('Name and ID are required','error'); return; }
|
|
789
|
+
if(!content){ toast('Prompt content is required','error'); return; }
|
|
790
|
+
try{
|
|
791
|
+
const res = await fetch('/api/marketplace/create-prompt',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id,name,description:desc,content})});
|
|
792
|
+
const data = await res.json();
|
|
793
|
+
if(!res.ok) throw new Error(data.error||'Failed to create prompt');
|
|
794
|
+
toast(`"${name}" saved`,'success');
|
|
795
|
+
document.getElementById('create-prompt-panel').classList.add('hidden');
|
|
796
|
+
document.getElementById('prompt-name').value='';
|
|
797
|
+
document.getElementById('prompt-id').value='';
|
|
798
|
+
document.getElementById('prompt-desc').value='';
|
|
799
|
+
document.getElementById('prompt-content').value='';
|
|
800
|
+
await loadItems();
|
|
801
|
+
} catch(e){ toast(`Error: ${e.message}`,'error'); }
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// ─── Add MCP panel ────────────────────────────────────────────────
|
|
805
|
+
let addMcpType = 'http';
|
|
806
|
+
|
|
807
|
+
function addMcpPanelHtml(){
|
|
808
|
+
return `
|
|
809
|
+
<div class="panel hidden" id="add-mcp-panel">
|
|
810
|
+
<div class="form-grid" style="margin-bottom:10px">
|
|
811
|
+
<div>
|
|
812
|
+
<div class="panel-label">Name</div>
|
|
813
|
+
<input class="panel-input" id="mcp-name" placeholder="My MCP" style="width:100%" oninput="syncMcpId()">
|
|
814
|
+
</div>
|
|
815
|
+
<div>
|
|
816
|
+
<div class="panel-label">ID <span style="font-size:9px;color:var(--text-muted)">(auto)</span></div>
|
|
817
|
+
<input class="panel-input" id="mcp-id" placeholder="my-mcp" style="width:100%">
|
|
818
|
+
</div>
|
|
819
|
+
<div class="form-grid-full">
|
|
820
|
+
<div class="panel-label">Description (optional)</div>
|
|
821
|
+
<input class="panel-input" id="mcp-desc" placeholder="What does this MCP do?" style="width:100%">
|
|
822
|
+
</div>
|
|
823
|
+
</div>
|
|
824
|
+
<div class="panel-label" style="margin-bottom:6px">Type</div>
|
|
825
|
+
<div class="type-toggle">
|
|
826
|
+
<button class="type-toggle-btn active" id="mcp-type-http" onclick="setMcpType('http')">HTTP</button>
|
|
827
|
+
<button class="type-toggle-btn" id="mcp-type-stdio" onclick="setMcpType('stdio')">stdio</button>
|
|
828
|
+
</div>
|
|
829
|
+
<div id="mcp-http-fields">
|
|
830
|
+
<div class="panel-label">URL</div>
|
|
831
|
+
<input class="panel-input" id="mcp-url" placeholder="https://mymcp.example.com/mcp" style="width:100%;margin-bottom:10px">
|
|
832
|
+
</div>
|
|
833
|
+
<div id="mcp-stdio-fields" style="display:none">
|
|
834
|
+
<div class="form-grid" style="margin-bottom:10px">
|
|
835
|
+
<div>
|
|
836
|
+
<div class="panel-label">Command</div>
|
|
837
|
+
<input class="panel-input" id="mcp-command" placeholder="node" style="width:100%">
|
|
838
|
+
</div>
|
|
839
|
+
<div>
|
|
840
|
+
<div class="panel-label">Args (space-separated)</div>
|
|
841
|
+
<input class="panel-input" id="mcp-args" placeholder="./server.js" style="width:100%">
|
|
842
|
+
</div>
|
|
843
|
+
</div>
|
|
844
|
+
</div>
|
|
845
|
+
<div style="display:flex;gap:8px">
|
|
846
|
+
<button class="btn-sm btn-sm-green" onclick="doAddMcp()">Add MCP</button>
|
|
847
|
+
<button class="btn-sm btn-sm-ghost" onclick="toggleAddMcpPanel()">Cancel</button>
|
|
848
|
+
</div>
|
|
849
|
+
</div>`;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
function toggleAddMcpPanel(){
|
|
853
|
+
const p = document.getElementById('add-mcp-panel');
|
|
854
|
+
if(p) p.classList.toggle('hidden');
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
function setMcpType(type){
|
|
858
|
+
addMcpType = type;
|
|
859
|
+
document.getElementById('mcp-type-http').classList.toggle('active', type==='http');
|
|
860
|
+
document.getElementById('mcp-type-stdio').classList.toggle('active', type==='stdio');
|
|
861
|
+
document.getElementById('mcp-http-fields').style.display = type==='http'?'':'none';
|
|
862
|
+
document.getElementById('mcp-stdio-fields').style.display = type==='stdio'?'':'none';
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
function syncMcpId(){
|
|
866
|
+
const name = document.getElementById('mcp-name').value;
|
|
867
|
+
document.getElementById('mcp-id').value = slugify(name);
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
async function doAddMcp(){
|
|
871
|
+
const id = document.getElementById('mcp-id').value.trim();
|
|
872
|
+
const name = document.getElementById('mcp-name').value.trim();
|
|
873
|
+
const desc = document.getElementById('mcp-desc').value.trim();
|
|
874
|
+
if(!id||!name){ toast('Name and ID are required','error'); return; }
|
|
875
|
+
const body = { id, name, description: desc, mcpType: addMcpType };
|
|
876
|
+
if(addMcpType==='http'){
|
|
877
|
+
const url = document.getElementById('mcp-url').value.trim();
|
|
878
|
+
if(!url){ toast('URL is required for HTTP MCP','error'); return; }
|
|
879
|
+
Object.assign(body, { url });
|
|
880
|
+
} else {
|
|
881
|
+
const command = document.getElementById('mcp-command').value.trim();
|
|
882
|
+
const argsRaw = document.getElementById('mcp-args').value.trim();
|
|
883
|
+
if(!command){ toast('Command is required for stdio MCP','error'); return; }
|
|
884
|
+
Object.assign(body, { command, args: argsRaw ? argsRaw.split(/\s+/) : [] });
|
|
885
|
+
}
|
|
886
|
+
try{
|
|
887
|
+
const res = await fetch('/api/marketplace/add-mcp',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
|
|
888
|
+
const data = await res.json();
|
|
889
|
+
if(!res.ok) throw new Error(data.error||'Failed to add MCP');
|
|
890
|
+
toast(`${name} added`, 'success');
|
|
891
|
+
document.getElementById('add-mcp-panel').classList.add('hidden');
|
|
892
|
+
await loadItems();
|
|
893
|
+
} catch(e){ toast(`Error: ${e.message}`,'error'); }
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// ─── Card ─────────────────────────────────────────────────────────
|
|
897
|
+
function cardHtml(item, typeSingular){
|
|
898
|
+
if(typeSingular === 'app') return appCardHtml(item);
|
|
899
|
+
|
|
900
|
+
const providerBadge = (item.provider==='AgenticLedger' || item.provider==='platform')
|
|
901
|
+
? `<span class="badge badge-verified">✓ verified</span>`
|
|
902
|
+
: item.provider==='me'
|
|
903
|
+
? `<span class="badge badge-personal">built by me</span>`
|
|
904
|
+
: `<span class="badge badge-external">community</span>`;
|
|
905
|
+
const installedBadge = item.installed ? `<span class="badge badge-installed">✓ installed</span>` : '';
|
|
906
|
+
const platformBadge = item.isPlatformDefault ? `<span class="badge badge-platform">★ default</span>` : '';
|
|
907
|
+
const assignedLabel = (item.assignedTo?.length)
|
|
908
|
+
? `<span class="assigned-label">→ ${item.assignedTo.slice(0,3).join(', ')}${item.assignedTo.length>3?` +${item.assignedTo.length-3}`:''}</span>`
|
|
909
|
+
: '';
|
|
910
|
+
const tagsHtml = (item.tags||[]).map(t=>`<span class="tag">${esc(t)}</span>`).join('');
|
|
911
|
+
|
|
912
|
+
let actionBtn;
|
|
913
|
+
if(item.installed && typeSingular!=='agent'){
|
|
914
|
+
actionBtn = `<button class="btn btn-manage" onclick='openAssignModal(${JSON.stringify(item)}, "${typeSingular}", false)'>Manage</button>`;
|
|
915
|
+
} else if(item.installed){
|
|
916
|
+
actionBtn = `<button class="btn btn-manage" disabled>Needs setup → Org</button>`;
|
|
917
|
+
} else {
|
|
918
|
+
actionBtn = `<button class="btn btn-install" id="install-btn-${esc(item.id)}" onclick='installItem(${JSON.stringify(item)}, "${typeSingular}")'>+ Install</button>`;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// Platform default toggle (skills and MCPs only)
|
|
922
|
+
const defaultToggle = typeSingular !== 'agent'
|
|
923
|
+
? `<button class="${item.isPlatformDefault ? 'btn-default-on' : 'btn-default-off'}" id="pd-btn-${esc(item.id)}" onclick='togglePlatformDefault(${JSON.stringify(item)}, "${typeSingular}")'>${item.isPlatformDefault ? '★ Default' : '☆ Set Default'}</button>`
|
|
924
|
+
: '';
|
|
925
|
+
|
|
926
|
+
// Publish to SaaS dropdown (skills and prompts only, when SaaS is configured)
|
|
927
|
+
const publishBtn = saasCfg.connected && (typeSingular==='skill' || typeSingular==='prompt')
|
|
928
|
+
? `<div class="pub-wrap" id="pub-wrap-${esc(item.id)}">
|
|
929
|
+
<button class="btn-sm btn-sm-accent" onclick="togglePubMenu('${esc(item.id)}',event)">↑ Publish ▾</button>
|
|
930
|
+
<div class="pub-menu" id="pub-menu-${esc(item.id)}">
|
|
931
|
+
<button class="pub-menu-item${item.saasLibrary?' done':''}" onclick="publishToSaas('${typeSingular}','${esc(item.id)}','${esc(item.name||item.id)}','library')">
|
|
932
|
+
${item.saasLibrary?'✓':''} Company Library
|
|
933
|
+
</button>
|
|
934
|
+
<button class="pub-menu-item${item.saasMarketplace?' done':''}" onclick="publishToSaas('${typeSingular}','${esc(item.id)}','${esc(item.name||item.id)}','marketplace')">
|
|
935
|
+
${item.saasMarketplace?'✓':''} Marketplace
|
|
936
|
+
</button>
|
|
937
|
+
</div>
|
|
938
|
+
</div>`
|
|
939
|
+
: '';
|
|
940
|
+
|
|
941
|
+
return `<div class="card${item.installed?' installed':''}${item.isPlatformDefault?' card-platform-default':''}" id="card-${esc(item.id)}">
|
|
942
|
+
<div class="card-top">
|
|
943
|
+
<div><div class="card-name">${esc(item.name)}</div><div class="card-provider">${esc(item.provider||'')}</div></div>
|
|
944
|
+
<div class="card-badges">${providerBadge}${installedBadge}${platformBadge}</div>
|
|
945
|
+
</div>
|
|
946
|
+
<div class="card-desc">${esc(item.description||'')}</div>
|
|
947
|
+
<div class="card-meta">${tagsHtml}${assignedLabel}</div>
|
|
948
|
+
<div class="card-actions">${actionBtn}${defaultToggle}${publishBtn}</div>
|
|
949
|
+
</div>`;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
// ─── App Card (rich tile design) ──────────────────────────────────
|
|
953
|
+
function appCardHtml(item){
|
|
954
|
+
const initials = (item.name||'?').slice(0,2).toUpperCase();
|
|
955
|
+
const statusClass = 'status-'+(item.status||'draft');
|
|
956
|
+
const statusLabel = (item.status||'draft').charAt(0).toUpperCase()+(item.status||'draft').slice(1);
|
|
957
|
+
|
|
958
|
+
const tags = (item.tags||[]).slice(0,3).map(t=>`<span class="app-tag">${esc(t)}</span>`).join('');
|
|
959
|
+
|
|
960
|
+
const agentDev = item.agentDeveloper
|
|
961
|
+
? `<a class="agent-dev-badge" href="/ui#${esc(item.agentDeveloper)}" title="Open in Chat">\uD83E\uDD16 ${esc(item.agentAlias||item.agentDeveloper)}</a>`
|
|
962
|
+
: '';
|
|
963
|
+
|
|
964
|
+
const deployBadge = item.deployPlatform
|
|
965
|
+
? `<span class="deploy-badge">${esc(deployLabel(item.deployPlatform))}</span>`
|
|
966
|
+
: '';
|
|
967
|
+
|
|
968
|
+
const categoryTag = item.category
|
|
969
|
+
? `<span class="app-tag">${esc(item.category)}</span>`
|
|
970
|
+
: '';
|
|
971
|
+
|
|
972
|
+
const githubBtn = item.githubRepo
|
|
973
|
+
? `<a class="btn-icon" href="${esc(item.githubRepo.startsWith('http')?item.githubRepo:'https://github.com/'+item.githubRepo)}" target="_blank" rel="noopener" title="GitHub">⌂</a>`
|
|
974
|
+
: '';
|
|
975
|
+
|
|
976
|
+
const healthDot = item.healthStatus==='healthy' ? 'healthy' : item.healthStatus==='down' ? 'down' : '';
|
|
977
|
+
const healthLabel = item.lastHealthCheck
|
|
978
|
+
? (item.healthStatus==='healthy' ? 'Healthy' : item.healthStatus==='down' ? 'Down' : 'Check')
|
|
979
|
+
: 'Check';
|
|
980
|
+
|
|
981
|
+
const appPublishBtn = saasCfg.connected
|
|
982
|
+
? `<div class="pub-wrap" id="pub-wrap-${esc(item.id)}">
|
|
983
|
+
<button class="btn-sm btn-sm-accent" onclick="togglePubMenu('${esc(item.id)}',event)">↑ Publish ▾</button>
|
|
984
|
+
<div class="pub-menu" id="pub-menu-${esc(item.id)}">
|
|
985
|
+
<button class="pub-menu-item${item.saasLibrary?' done':''}" onclick="publishToSaas('app','${esc(item.id)}','${esc(item.name||item.id)}','library')">
|
|
986
|
+
${item.saasLibrary?'✓':''} Company Library
|
|
987
|
+
</button>
|
|
988
|
+
<button class="pub-menu-item${item.saasMarketplace?' done':''}" onclick="publishToSaas('app','${esc(item.id)}','${esc(item.name||item.id)}','marketplace')">
|
|
989
|
+
${item.saasMarketplace?'✓':''} Marketplace
|
|
990
|
+
</button>
|
|
991
|
+
</div>
|
|
992
|
+
</div>`
|
|
993
|
+
: '';
|
|
994
|
+
|
|
995
|
+
return `<div class="app-card" id="card-${esc(item.id)}">
|
|
996
|
+
<div class="app-card-top">
|
|
997
|
+
<div class="app-icon">${initials}</div>
|
|
998
|
+
<div style="flex:1;min-width:0">
|
|
999
|
+
<div class="app-name">${esc(item.name)}</div>
|
|
1000
|
+
<div class="app-url">${esc(item.url||'')}</div>
|
|
1001
|
+
</div>
|
|
1002
|
+
<div class="app-badges">
|
|
1003
|
+
<span class="status-badge ${statusClass}">${statusLabel}</span>
|
|
1004
|
+
${deployBadge}
|
|
1005
|
+
</div>
|
|
1006
|
+
</div>
|
|
1007
|
+
${item.shortDescription||item.description ? `<div class="app-desc">${esc(item.shortDescription||item.description)}</div>` : ''}
|
|
1008
|
+
<div class="app-meta">
|
|
1009
|
+
${categoryTag}${tags}${agentDev}
|
|
1010
|
+
</div>
|
|
1011
|
+
<div class="app-actions">
|
|
1012
|
+
${item.url ? `<a class="btn-launch" href="${esc(item.url)}" target="_blank" rel="noopener">↗ Launch</a>` : ''}
|
|
1013
|
+
${appPublishBtn}
|
|
1014
|
+
${githubBtn}
|
|
1015
|
+
<button class="btn-icon danger" onclick="deleteAppFromRegistry('${esc(item.id)}')" title="Delete">✕</button>
|
|
1016
|
+
<button class="health-chip" onclick="checkAppHealth('${esc(item.id)}','${esc(item.url||'')}')" title="Ping app URL">
|
|
1017
|
+
<span class="health-dot ${healthDot}" id="hdot-${esc(item.id)}"></span>
|
|
1018
|
+
<span id="hlabel-${esc(item.id)}">${healthLabel}</span>
|
|
1019
|
+
</button>
|
|
1020
|
+
</div>
|
|
1021
|
+
</div>`;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
function deployLabel(p){
|
|
1025
|
+
return {railway:'Railway',vercel:'Vercel',netlify:'Netlify',render:'Render',local:'Local',other:'Other'}[p]||p;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// ─── Register App panel (inline in My Apps section) ───────────────
|
|
1029
|
+
function registerAppPanelHtml(){
|
|
1030
|
+
return `
|
|
1031
|
+
<div class="panel hidden" id="register-app-panel">
|
|
1032
|
+
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:10px">
|
|
1033
|
+
<span class="panel-label" style="margin:0">Register App</span>
|
|
1034
|
+
<button class="btn-sm btn-sm-ghost" style="padding:3px 8px;font-size:10px" onclick="toggleRegisterAppPanel()">✕</button>
|
|
1035
|
+
</div>
|
|
1036
|
+
<div class="form-grid" style="margin-bottom:10px">
|
|
1037
|
+
<div>
|
|
1038
|
+
<div class="panel-label">Name *</div>
|
|
1039
|
+
<input class="panel-input" id="reg-app-name" placeholder="My App" style="width:100%" oninput="syncRegAppId()">
|
|
1040
|
+
</div>
|
|
1041
|
+
<div>
|
|
1042
|
+
<div class="panel-label">URL *</div>
|
|
1043
|
+
<input class="panel-input" id="reg-app-url" placeholder="https://myapp.example.com" style="width:100%">
|
|
1044
|
+
</div>
|
|
1045
|
+
<div class="form-grid-full">
|
|
1046
|
+
<div class="panel-label">Short description</div>
|
|
1047
|
+
<input class="panel-input" id="reg-app-desc" placeholder="One-liner about this app" style="width:100%">
|
|
1048
|
+
</div>
|
|
1049
|
+
<div>
|
|
1050
|
+
<div class="panel-label">Category</div>
|
|
1051
|
+
<select class="panel-input" id="reg-app-category" style="width:100%;cursor:pointer">
|
|
1052
|
+
<option value="">— none —</option>
|
|
1053
|
+
<option value="productivity">Productivity</option>
|
|
1054
|
+
<option value="development">Development</option>
|
|
1055
|
+
<option value="finance">Finance</option>
|
|
1056
|
+
<option value="analytics">Analytics</option>
|
|
1057
|
+
<option value="communication">Communication</option>
|
|
1058
|
+
<option value="ai">AI / Agents</option>
|
|
1059
|
+
<option value="other">Other</option>
|
|
1060
|
+
</select>
|
|
1061
|
+
</div>
|
|
1062
|
+
<div>
|
|
1063
|
+
<div class="panel-label">Status</div>
|
|
1064
|
+
<select class="panel-input" id="reg-app-status" style="width:100%;cursor:pointer">
|
|
1065
|
+
<option value="draft">Draft</option>
|
|
1066
|
+
<option value="live">Live</option>
|
|
1067
|
+
<option value="maintenance">Maintenance</option>
|
|
1068
|
+
</select>
|
|
1069
|
+
</div>
|
|
1070
|
+
<div>
|
|
1071
|
+
<div class="panel-label">GitHub Repo (optional)</div>
|
|
1072
|
+
<input class="panel-input" id="reg-app-repo" placeholder="https://github.com/you/repo" style="width:100%">
|
|
1073
|
+
</div>
|
|
1074
|
+
<div>
|
|
1075
|
+
<div class="panel-label">Deploy Platform</div>
|
|
1076
|
+
<select class="panel-input" id="reg-app-deploy" style="width:100%;cursor:pointer">
|
|
1077
|
+
<option value="">— unknown —</option>
|
|
1078
|
+
<option value="railway">Railway</option>
|
|
1079
|
+
<option value="vercel">Vercel</option>
|
|
1080
|
+
<option value="netlify">Netlify</option>
|
|
1081
|
+
<option value="render">Render</option>
|
|
1082
|
+
<option value="local">Local</option>
|
|
1083
|
+
<option value="other">Other</option>
|
|
1084
|
+
</select>
|
|
1085
|
+
</div>
|
|
1086
|
+
</div>
|
|
1087
|
+
<div style="display:flex;gap:8px">
|
|
1088
|
+
<button class="btn-sm btn-sm-green" onclick="doRegisterApp()">Register App</button>
|
|
1089
|
+
<button class="btn-sm btn-sm-ghost" onclick="toggleRegisterAppPanel()">Cancel</button>
|
|
1090
|
+
</div>
|
|
1091
|
+
</div>`;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
function toggleRegisterAppPanel(){
|
|
1095
|
+
const p = document.getElementById('register-app-panel');
|
|
1096
|
+
if(p) p.classList.toggle('hidden');
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
function syncRegAppId(){
|
|
1100
|
+
// ID auto-generated server-side from name
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
async function doRegisterApp(){
|
|
1104
|
+
const name = document.getElementById('reg-app-name').value.trim();
|
|
1105
|
+
const url = document.getElementById('reg-app-url').value.trim();
|
|
1106
|
+
const desc = document.getElementById('reg-app-desc').value.trim();
|
|
1107
|
+
const category = document.getElementById('reg-app-category').value;
|
|
1108
|
+
const status = document.getElementById('reg-app-status').value;
|
|
1109
|
+
const repo = document.getElementById('reg-app-repo').value.trim();
|
|
1110
|
+
const deploy = document.getElementById('reg-app-deploy').value;
|
|
1111
|
+
if(!name){ toast('App name is required','error'); return; }
|
|
1112
|
+
if(!url){ toast('App URL is required','error'); return; }
|
|
1113
|
+
const body = { name, url, shortDescription: desc, category, status: status||'draft',
|
|
1114
|
+
githubRepo: repo, deployPlatform: deploy||null, provider: 'me', tags: [] };
|
|
1115
|
+
try{
|
|
1116
|
+
const res = await fetch('/api/apps',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
|
|
1117
|
+
const data = await res.json();
|
|
1118
|
+
if(!res.ok) throw new Error(data.error||'Failed to register app');
|
|
1119
|
+
toast(`"${name}" registered`,'success');
|
|
1120
|
+
document.getElementById('register-app-panel').classList.add('hidden');
|
|
1121
|
+
['reg-app-name','reg-app-url','reg-app-desc','reg-app-repo'].forEach(id=>{ const el=document.getElementById(id); if(el) el.value=''; });
|
|
1122
|
+
document.getElementById('reg-app-category').value='';
|
|
1123
|
+
document.getElementById('reg-app-status').value='draft';
|
|
1124
|
+
document.getElementById('reg-app-deploy').value='';
|
|
1125
|
+
await loadItems();
|
|
1126
|
+
} catch(e){ toast(`Error: ${e.message}`,'error'); }
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
async function deleteAppFromRegistry(id){
|
|
1130
|
+
if(!confirm('Delete this app? This cannot be undone.')) return;
|
|
1131
|
+
try{
|
|
1132
|
+
const res = await fetch(`/api/apps/${encodeURIComponent(id)}`,{method:'DELETE'});
|
|
1133
|
+
if(!res.ok){ const d = await res.json(); throw new Error(d.error||'Delete failed'); }
|
|
1134
|
+
toast('App deleted','success');
|
|
1135
|
+
await loadItems();
|
|
1136
|
+
} catch(e){ toast(`Error: ${e.message}`,'error'); }
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
async function checkAppHealth(id, url){
|
|
1140
|
+
if(!url) return;
|
|
1141
|
+
const dot = document.getElementById('hdot-'+id);
|
|
1142
|
+
const label = document.getElementById('hlabel-'+id);
|
|
1143
|
+
if(!dot||!label) return;
|
|
1144
|
+
label.textContent = '…';
|
|
1145
|
+
dot.className = 'health-dot';
|
|
1146
|
+
try{
|
|
1147
|
+
const res = await fetch(`/api/apps/${encodeURIComponent(id)}/health`,{method:'POST'});
|
|
1148
|
+
const data = await res.json();
|
|
1149
|
+
if(data.healthy){ dot.className='health-dot healthy'; label.textContent='Healthy'; }
|
|
1150
|
+
else { dot.className='health-dot down'; label.textContent='Down'; }
|
|
1151
|
+
} catch{ dot.className='health-dot down'; label.textContent='Error'; }
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
// ─── Platform default toggle ──────────────────────────────────────
|
|
1155
|
+
async function togglePlatformDefault(item, typeSingular){
|
|
1156
|
+
const newEnabled = !item.isPlatformDefault;
|
|
1157
|
+
const btn = document.getElementById(`pd-btn-${item.id}`);
|
|
1158
|
+
if(btn){ btn.textContent = 'Saving…'; btn.disabled = true; }
|
|
1159
|
+
try{
|
|
1160
|
+
const res = await fetch('/api/marketplace/platform-default',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({type:typeSingular,id:item.id,enabled:newEnabled})});
|
|
1161
|
+
const data = await res.json();
|
|
1162
|
+
if(!res.ok) throw new Error(data.error||'Failed');
|
|
1163
|
+
toast(`${item.name} ${newEnabled ? 'set as platform default' : 'removed from platform defaults'}`, 'success');
|
|
1164
|
+
await loadItems();
|
|
1165
|
+
} catch(e){ toast(`Error: ${e.message}`,'error'); await loadItems(); }
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
function emptyHtml(msg){ return `<div class="empty-state"><div class="empty-icon">◎</div><div class="empty-title">${esc(msg)}</div></div>`; }
|
|
1169
|
+
|
|
1170
|
+
// ─── Install ──────────────────────────────────────────────────────
|
|
1171
|
+
async function installItem(item, typeSingular){
|
|
1172
|
+
const btn = document.getElementById(`install-btn-${item.id}`);
|
|
1173
|
+
if(btn){ btn.textContent='Installing…'; btn.className='btn btn-installing'; }
|
|
1174
|
+
try{
|
|
1175
|
+
const res = await fetch('/api/marketplace/install',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({type:typeSingular,id:item.id})});
|
|
1176
|
+
const data = await res.json();
|
|
1177
|
+
if(!res.ok) throw new Error(data.error||'Install failed');
|
|
1178
|
+
toast(`${item.name} installed`,'success');
|
|
1179
|
+
await loadItems();
|
|
1180
|
+
if(typeSingular!=='agent') openAssignModal({...item,installed:true}, typeSingular, true, data.requiresKeys);
|
|
1181
|
+
} catch(e){ toast(`Install failed: ${e.message}`,'error'); await loadItems(); }
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
// ─── Assign modal ─────────────────────────────────────────────────
|
|
1185
|
+
function openAssignModal(item, typeSingular, justInstalled, requiresKeys){
|
|
1186
|
+
modalItem=item; modalType=typeSingular;
|
|
1187
|
+
document.getElementById('modal-title').textContent = justInstalled ? `✓ ${item.name} installed` : `Manage — ${item.name}`;
|
|
1188
|
+
document.getElementById('modal-sub').textContent = justInstalled ? 'Assign to agents now? (optional)' : 'Update agent assignments';
|
|
1189
|
+
const listEl = document.getElementById('modal-agent-list');
|
|
1190
|
+
listEl.innerHTML = agentsList.length
|
|
1191
|
+
? agentsList.map(a=>`<label class="agent-check-item"><input type="checkbox" value="${esc(a.id)}" ${(item.assignedTo||[]).includes(a.id)?'checked':''}><span class="agent-check-name">${esc(a.name||a.id)}</span><span class="agent-check-id">${esc(a.id)}</span></label>`).join('')
|
|
1192
|
+
: '<div style="font-size:12px;color:var(--text-muted);text-align:center;padding:12px">No agents configured</div>';
|
|
1193
|
+
document.getElementById('modal-missing-keys').innerHTML='';
|
|
1194
|
+
document.getElementById('modal-assign-btn').disabled=false;
|
|
1195
|
+
document.getElementById('modal-assign-btn').textContent='Assign selected';
|
|
1196
|
+
document.getElementById('assign-modal').classList.add('open');
|
|
1197
|
+
}
|
|
1198
|
+
function closeModal(){ document.getElementById('assign-modal').classList.remove('open'); modalItem=null; modalType=null; }
|
|
1199
|
+
document.getElementById('assign-modal').addEventListener('click', function(e){ if(e.target===this) closeModal(); });
|
|
1200
|
+
|
|
1201
|
+
async function doAssign(){
|
|
1202
|
+
if(!modalItem||!modalType) return;
|
|
1203
|
+
const agentIds = [...document.querySelectorAll('#modal-agent-list input:checked')].map(c=>c.value);
|
|
1204
|
+
if(!agentIds.length){ closeModal(); return; }
|
|
1205
|
+
document.getElementById('modal-assign-btn').disabled=true;
|
|
1206
|
+
document.getElementById('modal-assign-btn').textContent='Assigning…';
|
|
1207
|
+
try{
|
|
1208
|
+
const res = await fetch('/api/marketplace/assign',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({type:modalType,id:modalItem.id,agentIds})});
|
|
1209
|
+
const data = await res.json();
|
|
1210
|
+
if(!res.ok) throw new Error(data.error||'Assign failed');
|
|
1211
|
+
if(data.missingKeys?.length){
|
|
1212
|
+
document.getElementById('modal-missing-keys').innerHTML=`<div class="missing-keys-alert">⚠ API key needed for: ${data.missingKeys.map(a=>`<strong>${a}</strong>`).join(', ')} — add in Org to activate.</div>`;
|
|
1213
|
+
document.getElementById('modal-assign-btn').textContent='Assigned';
|
|
1214
|
+
} else {
|
|
1215
|
+
toast(`Assigned to ${agentIds.length} agent${agentIds.length!==1?'s':''}`,'success');
|
|
1216
|
+
closeModal();
|
|
1217
|
+
}
|
|
1218
|
+
await loadItems();
|
|
1219
|
+
} catch(e){ toast(`Assign failed: ${e.message}`,'error'); document.getElementById('modal-assign-btn').disabled=false; document.getElementById('modal-assign-btn').textContent='Assign selected'; }
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
// ─── Add App (External / Platform) ───────────────────────────────
|
|
1223
|
+
let addAppProvider = 'me';
|
|
1224
|
+
|
|
1225
|
+
function openAddAppPanel(provider){
|
|
1226
|
+
addAppProvider = provider;
|
|
1227
|
+
const panel = document.getElementById('add-app-panel');
|
|
1228
|
+
if(!panel) return;
|
|
1229
|
+
const label = (provider==='AgenticLedger' || provider==='platform') ? 'Platform' : provider==='me' ? 'My App' : 'External Source';
|
|
1230
|
+
document.getElementById('add-app-provider-label').textContent = label;
|
|
1231
|
+
panel.classList.remove('hidden');
|
|
1232
|
+
panel.scrollIntoView({behavior:'smooth', block:'nearest'});
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
function toggleAddAppPanel(){
|
|
1236
|
+
document.getElementById('add-app-panel')?.classList.toggle('hidden');
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
function syncAppId(){
|
|
1240
|
+
const name = document.getElementById('app-name').value;
|
|
1241
|
+
document.getElementById('app-id').value = slugify(name);
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
async function doAddApp(){
|
|
1245
|
+
const id = document.getElementById('app-id').value.trim();
|
|
1246
|
+
const name = document.getElementById('app-name').value.trim();
|
|
1247
|
+
const url = document.getElementById('app-url').value.trim();
|
|
1248
|
+
const desc = document.getElementById('app-desc').value.trim();
|
|
1249
|
+
const category = document.getElementById('app-category').value.trim();
|
|
1250
|
+
const repo = document.getElementById('app-repo').value.trim();
|
|
1251
|
+
if(!id||!name){ toast('Name and ID are required','error'); return; }
|
|
1252
|
+
const body = { id, name, url, shortDescription: desc, category,
|
|
1253
|
+
githubRepo: repo, provider: addAppProvider,
|
|
1254
|
+
status: 'live', tags: [], deployPlatform: 'other' };
|
|
1255
|
+
try{
|
|
1256
|
+
const res = await fetch('/api/apps',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
|
|
1257
|
+
const data = await res.json();
|
|
1258
|
+
if(!res.ok) throw new Error(data.error||'Failed to register app');
|
|
1259
|
+
toast(`"${name}" registered`,'success');
|
|
1260
|
+
document.getElementById('add-app-panel').classList.add('hidden');
|
|
1261
|
+
['app-name','app-id','app-url','app-desc','app-category','app-repo'].forEach(id=>{ const el=document.getElementById(id); if(el) el.value=''; });
|
|
1262
|
+
await loadItems();
|
|
1263
|
+
} catch(e){ toast(`Error: ${e.message}`,'error'); }
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
// ─── SaaS Publishing ──────────────────────────────────────────────
|
|
1267
|
+
async function loadSaasCfg(){
|
|
1268
|
+
try{ saasCfg = await (await fetch('/api/saas/config')).json(); } catch{ saasCfg={connected:false}; }
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
function togglePubMenu(id, e){
|
|
1272
|
+
e.stopPropagation();
|
|
1273
|
+
const menu = document.getElementById(`pub-menu-${id}`);
|
|
1274
|
+
if(!menu) return;
|
|
1275
|
+
const isOpen = menu.classList.contains('open');
|
|
1276
|
+
// close all open menus first
|
|
1277
|
+
document.querySelectorAll('.pub-menu.open').forEach(m=>m.classList.remove('open'));
|
|
1278
|
+
if(!isOpen) menu.classList.add('open');
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
function closePubConfirm(){
|
|
1282
|
+
document.getElementById('pubConfirmOverlay').style.display = 'none';
|
|
1283
|
+
const ok = document.getElementById('pubConfirmOk');
|
|
1284
|
+
ok.onclick = null;
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
function publishToSaas(type, id, name, destination){
|
|
1288
|
+
document.querySelectorAll('.pub-menu.open').forEach(m=>m.classList.remove('open'));
|
|
1289
|
+
const dest = destination === 'library' ? 'Company Library' : 'Marketplace';
|
|
1290
|
+
const url = (saasCfg.baseUrl || '').replace(/\/$/, '');
|
|
1291
|
+
const overlay = document.getElementById('pubConfirmOverlay');
|
|
1292
|
+
document.getElementById('pubConfirmTitle').textContent = `Publish to ${dest}`;
|
|
1293
|
+
document.getElementById('pubConfirmBody').innerHTML =
|
|
1294
|
+
`This will publish <strong>${esc(name)}</strong> (${type}) to your connected SaaS platform.<br><br>` +
|
|
1295
|
+
`<strong>Destination:</strong> ${dest}<br>` +
|
|
1296
|
+
(destination === 'marketplace' ? '<br>It will be publicly visible in the Marketplace to all users.<br>' : '<br>It will be available privately in your Company Library.<br>') +
|
|
1297
|
+
`<div class="pub-confirm-url">${url || '(SaaS URL not configured)'}</div>`;
|
|
1298
|
+
overlay.style.display = 'flex';
|
|
1299
|
+
const ok = document.getElementById('pubConfirmOk');
|
|
1300
|
+
ok.onclick = async () => {
|
|
1301
|
+
closePubConfirm();
|
|
1302
|
+
const menu = document.getElementById(`pub-menu-${id}`);
|
|
1303
|
+
const menuBtn = menu ? [...menu.querySelectorAll('.pub-menu-item')].find(b=>b.textContent.includes(destination==='library'?'Library':'Marketplace')) : null;
|
|
1304
|
+
if(menuBtn){ menuBtn.textContent = 'Publishing…'; menuBtn.disabled = true; }
|
|
1305
|
+
try{
|
|
1306
|
+
const res = await fetch('/api/saas/publish',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({type,id,destination})});
|
|
1307
|
+
const data = await res.json();
|
|
1308
|
+
if(!res.ok) throw new Error(data.error||'Publish failed');
|
|
1309
|
+
toast(`"${name}" published to ${dest}`,'success');
|
|
1310
|
+
await loadItems();
|
|
1311
|
+
} catch(e){
|
|
1312
|
+
toast(`Publish failed: ${e.message}`,'error');
|
|
1313
|
+
if(menuBtn){ menuBtn.textContent = destination==='library' ? 'Company Library' : 'Marketplace'; menuBtn.disabled = false; }
|
|
1314
|
+
}
|
|
1315
|
+
};
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
// ─── Import from Folder ───────────────────────────────────────────
|
|
1319
|
+
let impBrowserPath = '~';
|
|
1320
|
+
let impPreviewItems = [];
|
|
1321
|
+
|
|
1322
|
+
function openImport(){
|
|
1323
|
+
document.getElementById('impOverlay').style.display = 'flex';
|
|
1324
|
+
document.getElementById('impStep1').style.display = '';
|
|
1325
|
+
document.getElementById('impStep2').style.display = 'none';
|
|
1326
|
+
document.getElementById('impScanBtn').style.display = '';
|
|
1327
|
+
document.getElementById('impDoImportBtn').style.display = 'none';
|
|
1328
|
+
document.getElementById('impScanMsg').textContent = '';
|
|
1329
|
+
document.getElementById('impPathInput').value = '';
|
|
1330
|
+
document.getElementById('impBrowser').style.display = 'none';
|
|
1331
|
+
document.getElementById('impTitle').textContent = '↓ Import from Folder';
|
|
1332
|
+
impPreviewItems = [];
|
|
1333
|
+
}
|
|
1334
|
+
function closeImport(){ document.getElementById('impOverlay').style.display = 'none'; }
|
|
1335
|
+
|
|
1336
|
+
async function impBrowseOpen(){
|
|
1337
|
+
impBrowserPath = '~';
|
|
1338
|
+
document.getElementById('impBrowser').style.display = '';
|
|
1339
|
+
await impBrowserRender();
|
|
1340
|
+
}
|
|
1341
|
+
async function impBrowserRender(){
|
|
1342
|
+
const list = document.getElementById('impBrowserList');
|
|
1343
|
+
const pathEl = document.getElementById('impBrowserPath');
|
|
1344
|
+
list.innerHTML = '<div style="padding:12px;text-align:center;color:var(--text-muted);font-size:12px">Loading…</div>';
|
|
1345
|
+
try {
|
|
1346
|
+
const res = await fetch(`/api/browse-dirs?path=${encodeURIComponent(impBrowserPath)}`);
|
|
1347
|
+
const data = await res.json();
|
|
1348
|
+
if(data.error){ list.innerHTML=`<div style="padding:12px;color:#f87171;font-size:12px">${data.error}</div>`; return; }
|
|
1349
|
+
impBrowserPath = data.path;
|
|
1350
|
+
pathEl.textContent = data.path;
|
|
1351
|
+
let html = '';
|
|
1352
|
+
if(data.path !== '~' && data.path !== '/') html += `<div class="imp-dir-item imp-dir-parent" onclick="impBrowserUp()">↑ ..</div>`;
|
|
1353
|
+
for(const d of data.dirs) html += `<div class="imp-dir-item" onclick="impBrowserInto('${d.replace(/'/g,"\\'")}')">📁 ${d}</div>`;
|
|
1354
|
+
if(!html) html = '<div style="padding:12px;text-align:center;color:var(--text-muted);font-size:12px">Empty</div>';
|
|
1355
|
+
list.innerHTML = html;
|
|
1356
|
+
} catch(e){ list.innerHTML=`<div style="padding:12px;color:#f87171;font-size:12px">Error: ${e.message}</div>`; }
|
|
1357
|
+
}
|
|
1358
|
+
function impBrowserInto(name){ impBrowserPath = (impBrowserPath==='/'?'/':impBrowserPath)+'/'+name; impBrowserRender(); }
|
|
1359
|
+
function impBrowserUp(){ const parts = impBrowserPath.split('/'); parts.pop(); impBrowserPath = parts.join('/')||'~'; impBrowserRender(); }
|
|
1360
|
+
function impBrowserSelect(){
|
|
1361
|
+
document.getElementById('impPathInput').value = impBrowserPath;
|
|
1362
|
+
document.getElementById('impBrowser').style.display = 'none';
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
async function impScan(){
|
|
1366
|
+
const path = document.getElementById('impPathInput').value.trim();
|
|
1367
|
+
if(!path){ document.getElementById('impScanMsg').textContent = 'Enter a folder path first.'; return; }
|
|
1368
|
+
const btn = document.getElementById('impScanBtn');
|
|
1369
|
+
btn.textContent = 'Scanning…'; btn.disabled = true;
|
|
1370
|
+
document.getElementById('impScanMsg').textContent = '';
|
|
1371
|
+
try{
|
|
1372
|
+
const res = await fetch('/api/import-from-folder',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({folderPath:path,preview:true})});
|
|
1373
|
+
const data = await res.json();
|
|
1374
|
+
if(!res.ok) throw new Error(data.error||'Scan failed');
|
|
1375
|
+
impPreviewItems = data.items;
|
|
1376
|
+
document.getElementById('impFoundCount').textContent = data.items.length;
|
|
1377
|
+
const html = data.items.map(it=>`
|
|
1378
|
+
<div class="imp-item">
|
|
1379
|
+
<span class="imp-item-type ${it.type}">${it.type}</span>
|
|
1380
|
+
<div class="imp-item-info">
|
|
1381
|
+
<div class="imp-item-name">${esc(it.name||it.id)}</div>
|
|
1382
|
+
${it.description?`<div class="imp-item-desc">${esc(it.description)}</div>`:''}
|
|
1383
|
+
</div>
|
|
1384
|
+
</div>`).join('');
|
|
1385
|
+
document.getElementById('impPreviewList').innerHTML = html;
|
|
1386
|
+
document.getElementById('impErrMsg').style.display = 'none';
|
|
1387
|
+
document.getElementById('impStep1').style.display = 'none';
|
|
1388
|
+
document.getElementById('impStep2').style.display = '';
|
|
1389
|
+
document.getElementById('impScanBtn').style.display = 'none';
|
|
1390
|
+
document.getElementById('impDoImportBtn').style.display = '';
|
|
1391
|
+
document.getElementById('impTitle').textContent = `↓ Import — ${data.items.length} item${data.items.length!==1?'s':''} found`;
|
|
1392
|
+
} catch(e){
|
|
1393
|
+
document.getElementById('impScanMsg').textContent = e.message;
|
|
1394
|
+
} finally {
|
|
1395
|
+
btn.textContent = 'Scan Folder'; btn.disabled = false;
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
async function impDoImport(){
|
|
1400
|
+
const path = document.getElementById('impPathInput').value.trim();
|
|
1401
|
+
const btn = document.getElementById('impDoImportBtn');
|
|
1402
|
+
btn.textContent = 'Importing…'; btn.disabled = true;
|
|
1403
|
+
try{
|
|
1404
|
+
const res = await fetch('/api/import-from-folder',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({folderPath:path})});
|
|
1405
|
+
const data = await res.json();
|
|
1406
|
+
if(!res.ok) throw new Error(data.error||'Import failed');
|
|
1407
|
+
const n = data.imported.length;
|
|
1408
|
+
if(data.errors&&data.errors.length){
|
|
1409
|
+
const errEl = document.getElementById('impErrMsg');
|
|
1410
|
+
errEl.textContent = 'Some items skipped: '+data.errors.join('; ');
|
|
1411
|
+
errEl.style.display = '';
|
|
1412
|
+
}
|
|
1413
|
+
if(n){
|
|
1414
|
+
toast(`${n} item${n!==1?'s':''} imported successfully`, 'success');
|
|
1415
|
+
closeImport();
|
|
1416
|
+
await loadItems();
|
|
1417
|
+
} else {
|
|
1418
|
+
toast('Nothing imported — check errors', 'error');
|
|
1419
|
+
}
|
|
1420
|
+
} catch(e){
|
|
1421
|
+
toast(`Import failed: ${e.message}`, 'error');
|
|
1422
|
+
btn.textContent = 'Import All'; btn.disabled = false;
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
// ─── Init ─────────────────────────────────────────────────────────
|
|
1427
|
+
(async()=>{
|
|
1428
|
+
await loadSaasCfg();
|
|
1429
|
+
loadAgents();
|
|
1430
|
+
loadTrigger();
|
|
1431
|
+
loadItems();
|
|
1432
|
+
})();
|
|
1433
|
+
document.addEventListener('click', function(e){
|
|
1434
|
+
const dd = document.getElementById('docsDropdown');
|
|
1435
|
+
if(dd && !dd.contains(e.target)) document.getElementById('docsMenu')?.classList.remove('open');
|
|
1436
|
+
// close any open publish menus
|
|
1437
|
+
if(!e.target.closest('.pub-wrap')) document.querySelectorAll('.pub-menu.open').forEach(m=>m.classList.remove('open'));
|
|
1438
|
+
});
|
|
1439
|
+
</script>
|
|
1440
|
+
<script>(function(){fetch("/api/config/service").then(function(r){return r.json()}).then(function(d){if(d.gymEnabled)document.querySelectorAll(".gym-tab-link").forEach(function(el){el.style.display=""});if(d.gymOnlyMode){var keep=["/gym","/org","/admin"];document.querySelectorAll(".tab-group .tab-btn").forEach(function(el){var h=el.getAttribute("href");if(h&&keep.indexOf(h)===-1&&!el.classList.contains("gym-tab-link"))el.style.display="none"});document.querySelectorAll(".topbar-right .gear-btn").forEach(function(el){var h=el.getAttribute("href");if(h&&["/marketplace","/monitor","/user-guide"].indexOf(h)!==-1)el.style.display="none"})}}).catch(function(){})})()</script>
|
|
1441
|
+
</body>
|
|
1442
|
+
</html>
|