Update index.html: 5-tab UI with Netlify, inference, ChatGPT import
#4
by luguog - opened
- index.html +393 -0
index.html
ADDED
|
@@ -0,0 +1,393 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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>LocalSpace Deployer β DMG, GGUF, Sites & Inference</title>
|
| 7 |
+
<style>
|
| 8 |
+
:root {
|
| 9 |
+
--bg: #0a0a0f; --bg-card: #14141a; --bg-hover: #1c1c24; --border: #2a2a35;
|
| 10 |
+
--text: #e2e2e8; --text-dim: #888898; --text-muted: #5a5a68;
|
| 11 |
+
--accent: #6366f1; --accent-hover: #818cf8; --accent-dim: rgba(99,102,241,0.15);
|
| 12 |
+
--green: #22c55e; --green-dim: rgba(34,197,94,0.15);
|
| 13 |
+
--red: #ef4444; --red-dim: rgba(239,68,68,0.15);
|
| 14 |
+
--yellow: #eab300; --yellow-dim: rgba(234,179,0,0.15);
|
| 15 |
+
--cyan: #06b6d4; --cyan-dim: rgba(6,182,212,0.15);
|
| 16 |
+
--radius: 12px; --radius-sm: 8px;
|
| 17 |
+
}
|
| 18 |
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
| 19 |
+
body { background: var(--bg); color: var(--text); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; min-height: 100vh; line-height: 1.6; }
|
| 20 |
+
a { color: var(--accent-hover); text-decoration: none; }
|
| 21 |
+
a:hover { text-decoration: underline; }
|
| 22 |
+
nav { position: sticky; top: 0; z-index: 100; background: rgba(10,10,15,0.85); backdrop-filter: blur(12px); border-bottom: 1px solid var(--border); padding: 14px 24px; display: flex; align-items: center; justify-content: space-between; }
|
| 23 |
+
nav .logo { font-weight: 700; font-size: 1.1rem; display: flex; align-items: center; gap: 8px; }
|
| 24 |
+
nav .logo span { color: var(--accent); }
|
| 25 |
+
nav .links { display: flex; gap: 20px; align-items: center; }
|
| 26 |
+
nav .links a { color: var(--text-dim); font-size: 0.9rem; }
|
| 27 |
+
nav .links a:hover { color: var(--text); text-decoration: none; }
|
| 28 |
+
nav .cta { background: var(--accent); color: #fff; padding: 7px 16px; border-radius: var(--radius-sm); font-size: 0.85rem; font-weight: 600; }
|
| 29 |
+
nav .cta:hover { background: var(--accent-hover); text-decoration: none; }
|
| 30 |
+
.hero { text-align: center; padding: 80px 24px 60px; max-width: 720px; margin: 0 auto; }
|
| 31 |
+
.hero h1 { font-size: 2.8rem; font-weight: 800; letter-spacing: -0.03em; margin-bottom: 16px; background: linear-gradient(135deg, #e2e2e8 0%, #818cf8 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; }
|
| 32 |
+
.hero p { font-size: 1.15rem; color: var(--text-dim); margin-bottom: 32px; }
|
| 33 |
+
.hero .badges { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; margin-bottom: 40px; }
|
| 34 |
+
.hero .badge { background: var(--bg-card); border: 1px solid var(--border); padding: 6px 14px; border-radius: 999px; font-size: 0.82rem; color: var(--text-dim); }
|
| 35 |
+
.hero .badge strong { color: var(--text); }
|
| 36 |
+
.features { max-width: 900px; margin: 0 auto 60px; display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 16px; padding: 0 24px; }
|
| 37 |
+
.feature { background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius); padding: 24px; transition: border-color 0.2s; }
|
| 38 |
+
.feature:hover { border-color: var(--accent); }
|
| 39 |
+
.feature .icon { font-size: 1.6rem; margin-bottom: 12px; }
|
| 40 |
+
.feature h3 { font-size: 1rem; font-weight: 600; margin-bottom: 6px; }
|
| 41 |
+
.feature p { font-size: 0.88rem; color: var(--text-dim); }
|
| 42 |
+
.section { max-width: 900px; margin: 0 auto 80px; padding: 0 24px; }
|
| 43 |
+
.section-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 24px; }
|
| 44 |
+
.section-header h2 { font-size: 1.4rem; font-weight: 700; }
|
| 45 |
+
.section-header .count { background: var(--bg-card); border: 1px solid var(--border); padding: 4px 12px; border-radius: 999px; font-size: 0.82rem; color: var(--text-dim); }
|
| 46 |
+
.tabs { display: flex; gap: 4px; margin-bottom: 24px; background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius); padding: 4px; }
|
| 47 |
+
.tab { flex: 1; padding: 10px 16px; border-radius: var(--radius-sm); font-size: 0.88rem; font-weight: 500; color: var(--text-dim); cursor: pointer; border: none; background: none; text-align: center; }
|
| 48 |
+
.tab:hover { color: var(--text); }
|
| 49 |
+
.tab.active { background: var(--accent); color: #fff; }
|
| 50 |
+
.tab-content { display: none; }
|
| 51 |
+
.tab-content.active { display: block; }
|
| 52 |
+
.dropzone { border: 2px dashed var(--border); border-radius: var(--radius); padding: 48px 24px; text-align: center; cursor: pointer; transition: all 0.2s; background: var(--bg-card); }
|
| 53 |
+
.dropzone:hover, .dropzone.drag { border-color: var(--accent); background: var(--accent-dim); }
|
| 54 |
+
.dropzone .dz-icon { font-size: 2.5rem; margin-bottom: 12px; }
|
| 55 |
+
.dropzone .dz-text { font-size: 1.05rem; font-weight: 600; margin-bottom: 4px; }
|
| 56 |
+
.dropzone .dz-sub { font-size: 0.85rem; color: var(--text-dim); }
|
| 57 |
+
.dropzone .dz-formats { margin-top: 16px; display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; }
|
| 58 |
+
.dropzone .dz-format { background: var(--bg); border: 1px solid var(--border); padding: 4px 10px; border-radius: 6px; font-size: 0.78rem; color: var(--text-dim); }
|
| 59 |
+
.dropzone input[type=file] { display: none; }
|
| 60 |
+
.upload-progress { margin-top: 16px; background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius); padding: 20px; display: none; }
|
| 61 |
+
.upload-progress.active { display: block; }
|
| 62 |
+
.upload-progress .bar-wrap { background: var(--bg); border-radius: 999px; height: 6px; overflow: hidden; margin-top: 8px; }
|
| 63 |
+
.upload-progress .bar { height: 100%; background: var(--accent); border-radius: 999px; transition: width 0.2s; width: 0%; }
|
| 64 |
+
.upload-progress .status { font-size: 0.88rem; color: var(--text-dim); }
|
| 65 |
+
.creator-box { margin-top: 24px; background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius); padding: 24px; }
|
| 66 |
+
.creator-box h3 { font-size: 1rem; font-weight: 600; margin-bottom: 4px; }
|
| 67 |
+
.creator-box p { font-size: 0.85rem; color: var(--text-dim); margin-bottom: 16px; }
|
| 68 |
+
.creator-box .row { display: flex; gap: 10px; flex-wrap: wrap; }
|
| 69 |
+
.creator-box input { flex: 1; min-width: 180px; background: var(--bg); border: 1px solid var(--border); color: var(--text); padding: 10px 14px; border-radius: var(--radius-sm); font-size: 0.9rem; outline: none; }
|
| 70 |
+
.creator-box input:focus { border-color: var(--accent); }
|
| 71 |
+
.creator-box button { background: var(--accent); color: #fff; border: none; padding: 10px 20px; border-radius: var(--radius-sm); font-size: 0.9rem; font-weight: 600; cursor: pointer; }
|
| 72 |
+
.creator-box button:hover { background: var(--accent-hover); }
|
| 73 |
+
.creator-box button:disabled { opacity: 0.5; }
|
| 74 |
+
.inference-panel { background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius); padding: 24px; }
|
| 75 |
+
.inference-panel .inf-status { display: flex; align-items: center; gap: 8px; margin-bottom: 16px; font-size: 0.88rem; }
|
| 76 |
+
.inference-panel .inf-status .dot { width: 8px; height: 8px; border-radius: 50%; }
|
| 77 |
+
.inference-panel .inf-status .dot.on { background: var(--green); }
|
| 78 |
+
.inference-panel .inf-status .dot.off { background: var(--red); }
|
| 79 |
+
.inference-panel .model-select { display: flex; gap: 10px; margin-bottom: 16px; flex-wrap: wrap; }
|
| 80 |
+
.inference-panel select { flex: 1; min-width: 200px; background: var(--bg); border: 1px solid var(--border); color: var(--text); padding: 10px 14px; border-radius: var(--radius-sm); font-size: 0.9rem; outline: none; }
|
| 81 |
+
.inference-panel .chat-box { background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 16px; min-height: 200px; max-height: 400px; overflow-y: auto; margin-bottom: 12px; }
|
| 82 |
+
.inference-panel .chat-msg { margin-bottom: 12px; }
|
| 83 |
+
.inference-panel .chat-msg .role { font-size: 0.75rem; font-weight: 600; text-transform: uppercase; margin-bottom: 2px; }
|
| 84 |
+
.inference-panel .chat-msg .role.user { color: var(--accent); }
|
| 85 |
+
.inference-panel .chat-msg .role.assistant { color: var(--green); }
|
| 86 |
+
.inference-panel .chat-msg .bubble { background: var(--bg-card); border: 1px solid var(--border); border-radius: 8px; padding: 10px 14px; font-size: 0.88rem; white-space: pre-wrap; }
|
| 87 |
+
.inference-panel .chat-input { display: flex; gap: 10px; }
|
| 88 |
+
.inference-panel .chat-input input { flex: 1; background: var(--bg); border: 1px solid var(--border); color: var(--text); padding: 10px 14px; border-radius: var(--radius-sm); font-size: 0.9rem; outline: none; }
|
| 89 |
+
.inference-panel .chat-input button { background: var(--accent); color: #fff; border: none; padding: 10px 20px; border-radius: var(--radius-sm); font-size: 0.9rem; font-weight: 600; cursor: pointer; }
|
| 90 |
+
.inference-panel .chat-input button:disabled { opacity: 0.5; }
|
| 91 |
+
.inference-panel .inf-hint { font-size: 0.82rem; color: var(--text-muted); margin-top: 12px; padding: 10px; background: var(--bg); border-radius: 8px; border: 1px solid var(--border); }
|
| 92 |
+
.apps-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 16px; margin-top: 24px; }
|
| 93 |
+
.app-card { background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius); padding: 20px; transition: border-color 0.2s, transform 0.15s; }
|
| 94 |
+
.app-card:hover { border-color: var(--accent); transform: translateY(-2px); }
|
| 95 |
+
.app-card .card-header { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 12px; }
|
| 96 |
+
.app-card .card-title { font-weight: 600; font-size: 0.95rem; word-break: break-all; }
|
| 97 |
+
.app-card .card-type { font-size: 0.72rem; padding: 2px 8px; border-radius: 999px; font-weight: 600; white-space: nowrap; margin-left: 8px; }
|
| 98 |
+
.app-card .card-type.dmg { background: var(--accent-dim); color: var(--accent-hover); }
|
| 99 |
+
.app-card .card-type.gguf { background: var(--green-dim); color: var(--green); }
|
| 100 |
+
.app-card .card-type.webapp { background: var(--yellow-dim); color: var(--yellow); }
|
| 101 |
+
.app-card .card-type.site { background: var(--cyan-dim); color: var(--cyan); }
|
| 102 |
+
.app-card .card-type.chatgpt { background: var(--green-dim); color: var(--green); }
|
| 103 |
+
.app-card .card-meta { display: flex; flex-wrap: wrap; gap: 12px; font-size: 0.8rem; color: var(--text-dim); margin-bottom: 12px; }
|
| 104 |
+
.app-card .card-status { display: inline-flex; align-items: center; gap: 6px; font-size: 0.8rem; margin-bottom: 12px; }
|
| 105 |
+
.app-card .card-status.ok { color: var(--green); }
|
| 106 |
+
.app-card .card-status.err { color: var(--red); }
|
| 107 |
+
.app-card .card-status .dot { width: 8px; height: 8px; border-radius: 50%; background: currentColor; }
|
| 108 |
+
.app-card .card-actions { display: flex; gap: 8px; flex-wrap: wrap; }
|
| 109 |
+
.app-card .card-actions a, .app-card .card-actions button { background: var(--bg-hover); border: 1px solid var(--border); color: var(--text); padding: 6px 12px; border-radius: var(--radius-sm); font-size: 0.82rem; text-decoration: none; cursor: pointer; }
|
| 110 |
+
.app-card .card-actions a:hover, .app-card .card-actions button:hover { background: var(--border); text-decoration: none; }
|
| 111 |
+
.app-card .card-actions .danger:hover { background: var(--red-dim); border-color: var(--red); }
|
| 112 |
+
.app-card .card-details { margin-top: 12px; border-top: 1px solid var(--border); padding-top: 12px; font-size: 0.82rem; color: var(--text-dim); }
|
| 113 |
+
.app-card .card-details .detail-row { display: flex; justify-content: space-between; padding: 2px 0; }
|
| 114 |
+
.app-card .card-details .detail-row strong { color: var(--text); }
|
| 115 |
+
.app-card .gguf-meta { margin-top: 12px; border-top: 1px solid var(--border); padding-top: 12px; font-size: 0.8rem; color: var(--text-dim); max-height: 200px; overflow-y: auto; }
|
| 116 |
+
.app-card .gguf-meta .meta-row { display: flex; justify-content: space-between; padding: 2px 0; }
|
| 117 |
+
.app-card .gguf-meta .meta-row strong { color: var(--text); }
|
| 118 |
+
.empty { text-align: center; padding: 48px 24px; color: var(--text-muted); }
|
| 119 |
+
.empty .icon { font-size: 2rem; margin-bottom: 8px; }
|
| 120 |
+
footer { border-top: 1px solid var(--border); padding: 32px 24px; text-align: center; font-size: 0.82rem; color: var(--text-muted); }
|
| 121 |
+
.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.7); display: none; align-items: center; justify-content: center; z-index: 200; padding: 24px; }
|
| 122 |
+
.modal-overlay.active { display: flex; }
|
| 123 |
+
.modal { background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius); max-width: 700px; width: 100%; max-height: 80vh; overflow-y: auto; padding: 28px; }
|
| 124 |
+
.modal h2 { font-size: 1.2rem; margin-bottom: 16px; }
|
| 125 |
+
.modal .close { float: right; cursor: pointer; color: var(--text-dim); font-size: 1.3rem; }
|
| 126 |
+
.modal .close:hover { color: var(--text); }
|
| 127 |
+
.modal pre { background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 16px; overflow-x: auto; font-size: 0.82rem; color: var(--text-dim); }
|
| 128 |
+
@media (max-width: 640px) { .hero h1 { font-size: 2rem; } .hero p { font-size: 1rem; } .features { grid-template-columns: 1fr; } .apps-grid { grid-template-columns: 1fr; } nav .links a:not(.cta) { display: none; } .tab { font-size: 0.78rem; padding: 8px 8px; } }
|
| 129 |
+
.spinner { display: inline-block; width: 16px; height: 16px; border: 2px solid var(--border); border-top-color: var(--accent); border-radius: 50%; animation: spin 0.7s linear infinite; }
|
| 130 |
+
@keyframes spin { to { transform: rotate(360deg); } }
|
| 131 |
+
</style>
|
| 132 |
+
</head>
|
| 133 |
+
<body>
|
| 134 |
+
<nav>
|
| 135 |
+
<div class="logo">⬑ <span>LocalSpace</span> Deployer</div>
|
| 136 |
+
<div class="links">
|
| 137 |
+
<a href="#features">Features</a>
|
| 138 |
+
<a href="#deploy">Deploy</a>
|
| 139 |
+
<a href="https://huggingface.co/spaces/josephrw/localspace-deployer" target="_blank">HF Space</a>
|
| 140 |
+
<a href="#deploy" class="cta">Get Started</a>
|
| 141 |
+
</div>
|
| 142 |
+
</nav>
|
| 143 |
+
|
| 144 |
+
<section class="hero">
|
| 145 |
+
<h1>Drag a DMG, GGUF, or ZIP.<br>It gets opened on the server.</h1>
|
| 146 |
+
<p>Upload macOS disk images, GGUF model files, or static site ZIPs. LocalSpace extracts, inspects, serves, and runs inference β all in one Hugging Face Space. No Gradio. No API keys. Pure FastAPI.</p>
|
| 147 |
+
<div class="badges">
|
| 148 |
+
<div class="badge"><strong>DMG</strong> extraction</div>
|
| 149 |
+
<div class="badge"><strong>GGUF</strong> inference</div>
|
| 150 |
+
<div class="badge"><strong>Tiny Netlify</strong> site deploy</div>
|
| 151 |
+
<div class="badge"><strong>ChatGPT export</strong> β dApp</div>
|
| 152 |
+
<div class="badge"><strong>Persistent</strong> storage</div>
|
| 153 |
+
</div>
|
| 154 |
+
</section>
|
| 155 |
+
|
| 156 |
+
<section class="features" id="features">
|
| 157 |
+
<div class="feature"><div class="icon">π¦</div><h3>DMG Extraction</h3><p>Upload .dmg β 7z extracts server-side, finds .app bundles, reads Info.plist, lists files, SHA-256 hashes.</p></div>
|
| 158 |
+
<div class="feature"><div class="icon">π§ </div><h3>GGUF Inference</h3><p>Upload .gguf β parse metadata, load with llama-cpp-python, serve OpenAI-compatible /api/inference/chat endpoint.</p></div>
|
| 159 |
+
<div class="feature"><div class="icon">π</div><h3>Tiny Netlify</h3><p>Upload a static site ZIP β get a hosted URL. Base-tag injection, full MIME support, file tree browsing.</p></div>
|
| 160 |
+
<div class="feature"><div class="icon">π¬</div><h3>ChatGPT β dApp</h3><p>Upload ChatGPT export (conversations.json or .zip) β get a browsable dApp with sidebar, search, and message viewer.</p></div>
|
| 161 |
+
<div class="feature"><div class="icon">π</div><h3>Web App β DMG</h3><p>Paste any URL β get a macOS .app bundle wrapper. Creates Info.plist, executable, and HTML fallback.</p></div>
|
| 162 |
+
<div class="feature"><div class="icon">πΎ</div><h3>Persistent Storage</h3><p>Auto-detects /data mount on HF Spaces. Falls back to local storage. All artifacts survive restarts when mounted.</p></div>
|
| 163 |
+
</section>
|
| 164 |
+
|
| 165 |
+
<section class="section" id="deploy">
|
| 166 |
+
<div class="section-header">
|
| 167 |
+
<h2>Artifact Deployer</h2>
|
| 168 |
+
<div class="count" id="app-count">0 artifacts</div>
|
| 169 |
+
</div>
|
| 170 |
+
<div class="tabs">
|
| 171 |
+
<button class="tab active" onclick="switchTab(event,'dmg-gguf')">π¦ DMG / GGUF</button>
|
| 172 |
+
<button class="tab" onclick="switchTab(event,'site')">π Deploy Site</button>
|
| 173 |
+
<button class="tab" onclick="switchTab(event,'chatgpt')">π¬ ChatGPT Export</button>
|
| 174 |
+
<button class="tab" onclick="switchTab(event,'inference')">π§ Inference</button>
|
| 175 |
+
<button class="tab" onclick="switchTab(event,'webapp')">π Web App</button>
|
| 176 |
+
</div>
|
| 177 |
+
|
| 178 |
+
<div class="tab-content active" id="tab-dmg-gguf">
|
| 179 |
+
<div class="dropzone" id="dropzone">
|
| 180 |
+
<div class="dz-icon">β¬οΈ</div>
|
| 181 |
+
<div class="dz-text">Drop a DMG or GGUF file here</div>
|
| 182 |
+
<div class="dz-sub">or click to browse</div>
|
| 183 |
+
<div class="dz-formats"><div class="dz-format">.dmg</div><div class="dz-format">.gguf</div></div>
|
| 184 |
+
<input type="file" id="file-input" accept=".dmg,.gguf">
|
| 185 |
+
</div>
|
| 186 |
+
<div class="upload-progress" id="upload-progress">
|
| 187 |
+
<div class="status" id="upload-status">Uploading...</div>
|
| 188 |
+
<div class="bar-wrap"><div class="bar" id="upload-bar"></div></div>
|
| 189 |
+
</div>
|
| 190 |
+
</div>
|
| 191 |
+
|
| 192 |
+
<div class="tab-content" id="tab-site">
|
| 193 |
+
<div class="dropzone" id="site-dropzone">
|
| 194 |
+
<div class="dz-icon">π</div>
|
| 195 |
+
<div class="dz-text">Drop a static site ZIP here</div>
|
| 196 |
+
<div class="dz-sub">index.html will be served at /site/{id}/</div>
|
| 197 |
+
<div class="dz-formats"><div class="dz-format">.zip</div></div>
|
| 198 |
+
<input type="file" id="site-file-input" accept=".zip">
|
| 199 |
+
</div>
|
| 200 |
+
<div class="upload-progress" id="site-upload-progress">
|
| 201 |
+
<div class="status" id="site-upload-status">Deploying...</div>
|
| 202 |
+
<div class="bar-wrap"><div class="bar" id="site-upload-bar"></div></div>
|
| 203 |
+
</div>
|
| 204 |
+
</div>
|
| 205 |
+
|
| 206 |
+
<div class="tab-content" id="tab-chatgpt">
|
| 207 |
+
<div class="dropzone" id="chatgpt-dropzone">
|
| 208 |
+
<div class="dz-icon">π¬</div>
|
| 209 |
+
<div class="dz-text">Drop a ChatGPT export here</div>
|
| 210 |
+
<div class="dz-sub">conversations.json or export ZIP β browsable dApp</div>
|
| 211 |
+
<div class="dz-formats"><div class="dz-format">.json</div><div class="dz-format">.zip</div></div>
|
| 212 |
+
<input type="file" id="chatgpt-file-input" accept=".json,.zip">
|
| 213 |
+
</div>
|
| 214 |
+
<div class="upload-progress" id="chatgpt-upload-progress">
|
| 215 |
+
<div class="status" id="chatgpt-upload-status">Importing...</div>
|
| 216 |
+
<div class="bar-wrap"><div class="bar" id="chatgpt-upload-bar"></div></div>
|
| 217 |
+
</div>
|
| 218 |
+
</div>
|
| 219 |
+
|
| 220 |
+
<div class="tab-content" id="tab-inference">
|
| 221 |
+
<div class="inference-panel">
|
| 222 |
+
<div class="inf-status"><span class="dot" id="inf-dot"></span><span id="inf-status-text">Checking...</span></div>
|
| 223 |
+
<div class="model-select">
|
| 224 |
+
<select id="inf-model-select"><option value="">Select a GGUF model...</option></select>
|
| 225 |
+
<button onclick="loadModel()" id="inf-load-btn">Load Model</button>
|
| 226 |
+
</div>
|
| 227 |
+
<div class="chat-box" id="inf-chat-box"><div style="color:var(--text-muted);text-align:center;padding:40px">Load a GGUF model to start chatting</div></div>
|
| 228 |
+
<div class="chat-input">
|
| 229 |
+
<input type="text" id="inf-chat-input" placeholder="Type a message..." onkeydown="if(event.key==='Enter')sendChat()" disabled>
|
| 230 |
+
<button onclick="sendChat()" id="inf-send-btn" disabled>Send</button>
|
| 231 |
+
</div>
|
| 232 |
+
<div class="inf-hint" id="inf-hint"></div>
|
| 233 |
+
</div>
|
| 234 |
+
</div>
|
| 235 |
+
|
| 236 |
+
<div class="tab-content" id="tab-webapp">
|
| 237 |
+
<div class="creator-box">
|
| 238 |
+
<h3>π Create macOS App from URL</h3>
|
| 239 |
+
<p>Paste a web app URL to generate a .app bundle wrapper that opens it on macOS.</p>
|
| 240 |
+
<div class="row">
|
| 241 |
+
<input type="text" id="webapp-url" placeholder="https://your-web-app-url.com">
|
| 242 |
+
<input type="text" id="webapp-name" placeholder="App name" style="max-width:180px">
|
| 243 |
+
<button id="webapp-btn" onclick="createWebapp()">Create App</button>
|
| 244 |
+
</div>
|
| 245 |
+
</div>
|
| 246 |
+
</div>
|
| 247 |
+
|
| 248 |
+
<div class="apps-grid" id="apps-grid"></div>
|
| 249 |
+
</section>
|
| 250 |
+
|
| 251 |
+
<div class="modal-overlay" id="modal-overlay" onclick="if(event.target===this)closeModal()">
|
| 252 |
+
<div class="modal"><span class="close" onclick="closeModal()">×</span><h2 id="modal-title">Details</h2><div id="modal-body"></div></div>
|
| 253 |
+
</div>
|
| 254 |
+
|
| 255 |
+
<footer>
|
| 256 |
+
LocalSpace Deployer β DMG, GGUF, Sites & Inference on Hugging Face Spaces<br>
|
| 257 |
+
<a href="https://huggingface.co/spaces/josephrw/localspace-deployer" target="_blank">huggingface.co/spaces/josephrw/localspace-deployer</a>
|
| 258 |
+
</footer>
|
| 259 |
+
|
| 260 |
+
<script>
|
| 261 |
+
let apps = [];
|
| 262 |
+
let loadedModelId = null;
|
| 263 |
+
|
| 264 |
+
document.addEventListener('DOMContentLoaded', () => { loadApps(); setupDropzones(); checkInference(); });
|
| 265 |
+
|
| 266 |
+
function switchTab(e, name) {
|
| 267 |
+
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
| 268 |
+
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
|
| 269 |
+
e.target.classList.add('active');
|
| 270 |
+
document.getElementById('tab-' + name).classList.add('active');
|
| 271 |
+
}
|
| 272 |
+
|
| 273 |
+
function setupDropzones() {
|
| 274 |
+
setupDropzone('dropzone', 'file-input', uploadDmgGguf);
|
| 275 |
+
setupDropzone('site-dropzone', 'site-file-input', uploadSite);
|
| 276 |
+
setupDropzone('chatgpt-dropzone', 'chatgpt-file-input', uploadChatGpt);
|
| 277 |
+
}
|
| 278 |
+
function setupDropzone(dzId, inputId, handler) {
|
| 279 |
+
const dz = document.getElementById(dzId); const input = document.getElementById(inputId);
|
| 280 |
+
if (!dz || !input) return;
|
| 281 |
+
dz.addEventListener('click', () => input.click());
|
| 282 |
+
input.addEventListener('change', e => { if (e.target.files[0]) handler(e.target.files[0]); });
|
| 283 |
+
dz.addEventListener('dragover', e => { e.preventDefault(); dz.classList.add('drag'); });
|
| 284 |
+
dz.addEventListener('dragleave', () => dz.classList.remove('drag'));
|
| 285 |
+
dz.addEventListener('drop', e => { e.preventDefault(); dz.classList.remove('drag'); if (e.dataTransfer.files[0]) handler(e.dataTransfer.files[0]); });
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
function uploadFile(file, url, progId, barId, statusId, okMsg) {
|
| 289 |
+
const prog = document.getElementById(progId); const bar = document.getElementById(barId); const status = document.getElementById(statusId);
|
| 290 |
+
prog.classList.add('active'); bar.style.width = '0%'; status.textContent = `Uploading ${file.name}...`;
|
| 291 |
+
const fd = new FormData(); fd.append('file', file);
|
| 292 |
+
const xhr = new XMLHttpRequest(); xhr.open('POST', url);
|
| 293 |
+
xhr.upload.onprogress = e => { if (e.lengthComputable) { const pct = Math.round((e.loaded/e.total)*100); bar.style.width = pct+'%'; status.textContent = `Uploading ${file.name}... ${pct}%`; } };
|
| 294 |
+
xhr.onload = () => {
|
| 295 |
+
if (xhr.status === 201) { const d = JSON.parse(xhr.responseText); status.textContent = `β ${d.message||okMsg}`; bar.style.width = '100%'; setTimeout(() => prog.classList.remove('active'), 2500); loadApps(); }
|
| 296 |
+
else { let m='Upload failed'; try{m=JSON.parse(xhr.responseText).error||m;}catch{} status.textContent = `β ${m}`; setTimeout(() => prog.classList.remove('active'), 4000); }
|
| 297 |
+
};
|
| 298 |
+
xhr.onerror = () => { status.textContent = 'β Network error'; setTimeout(() => prog.classList.remove('active'), 3000); };
|
| 299 |
+
xhr.send(fd);
|
| 300 |
+
}
|
| 301 |
+
function uploadDmgGguf(f) { const e=f.name.toLowerCase().split('.').pop(); if(e!=='dmg'&&e!=='gguf'){alert('Only .dmg and .gguf');return;} uploadFile(f,'/api/upload','upload-progress','upload-bar','upload-status','Uploaded.'); }
|
| 302 |
+
function uploadSite(f) { if(!f.name.toLowerCase().endsWith('.zip')){alert('Only .zip');return;} uploadFile(f,'/api/deploy-site','site-upload-progress','site-upload-bar','site-upload-status','Site deployed.'); }
|
| 303 |
+
function uploadChatGpt(f) { const e=f.name.toLowerCase().split('.').pop(); if(e!=='json'&&e!=='zip'){alert('Only .json or .zip');return;} uploadFile(f,'/api/import/chatgpt','chatgpt-upload-progress','chatgpt-upload-bar','chatgpt-upload-status','dApp deployed.'); }
|
| 304 |
+
|
| 305 |
+
async function loadApps() {
|
| 306 |
+
try { const r = await fetch('/api/apps'); const d = await r.json(); apps = d.apps||[]; renderApps(); updateModelSelect(); } catch(e) { console.error(e); }
|
| 307 |
+
}
|
| 308 |
+
function renderApps() {
|
| 309 |
+
const g = document.getElementById('apps-grid'); const c = document.getElementById('app-count');
|
| 310 |
+
c.textContent = `${apps.length} artifact${apps.length!==1?'s':''}`;
|
| 311 |
+
if (!apps.length) { g.innerHTML = `<div class="empty" style="grid-column:1/-1"><div class="icon">π¦</div><div>No artifacts yet. Upload something to get started.</div></div>`; return; }
|
| 312 |
+
g.innerHTML = apps.map(renderCard).join('');
|
| 313 |
+
}
|
| 314 |
+
function renderCard(a) {
|
| 315 |
+
const t = a.file_type||'dmg';
|
| 316 |
+
const tc = ['dmg','gguf','webapp','site','chatgpt'].includes(t)?t:'dmg';
|
| 317 |
+
const tl = {dmg:'DMG',gguf:'GGUF',webapp:'WEB APP',site:'SITE',chatgpt:'CHATGPT'}[t]||t.toUpperCase();
|
| 318 |
+
const ok = a.opened||a.is_webapp;
|
| 319 |
+
const st = a.opened ? (t==='gguf'?'Inspected':'Opened') : (a.is_webapp?'Packaged':(a.error||'Failed'));
|
| 320 |
+
const sc = ok?'ok':'err';
|
| 321 |
+
let det = '';
|
| 322 |
+
if (t==='dmg'&&a.apps&&a.apps.length) { const app=a.apps[0]; det=`<div class="card-details"><div class="detail-row"><span>Bundle</span><strong>${esc(app.bundle_name||'β')}</strong></div><div class="detail-row"><span>Bundle ID</span><strong>${esc(app.bundle_id||'β')}</strong></div><div class="detail-row"><span>Version</span><strong>${esc(app.version||'β')}</strong></div><div class="detail-row"><span>Files</span><strong>${app.file_count||0}</strong></div></div>`; }
|
| 323 |
+
if (t==='site') { det=`<div class="card-details"><div class="detail-row"><span>Files</span><strong>${a.file_count||0}</strong></div><div class="detail-row"><span>Total size</span><strong>${a.total_size_human||'β'}</strong></div><div class="detail-row"><span>Index</span><strong>${esc(a.index_file||'index.html')}</strong></div></div>`; }
|
| 324 |
+
if (t==='chatgpt') { det=`<div class="card-details"><div class="detail-row"><span>Conversations</span><strong>${a.conversation_count||0}</strong></div><div class="detail-row"><span>Messages</span><strong>${a.total_messages||0}</strong></div></div>`; }
|
| 325 |
+
let gm = '';
|
| 326 |
+
if (t==='gguf'&&a.gguf&&a.gguf.valid) { const g=a.gguf; const mr=Object.entries(g.metadata||{}).slice(0,15).map(([k,v])=>`<div class="meta-row"><span>${esc(k)}</span><strong>${esc(String(v.value)).substring(0,60)}</strong></div>`).join(''); gm=`<div class="gguf-meta"><div class="meta-row"><span>Architecture</span><strong>${esc(g.architecture||'β')}</strong></div><div class="meta-row"><span>Version</span><strong>GGUF v${g.version}</strong></div><div class="meta-row"><span>Tensors</span><strong>${g.tensor_count}</strong></div><div class="meta-row"><span>KV pairs</span><strong>${g.kv_count}</strong></div>${g.context_length?`<div class="meta-row"><span>Context</span><strong>${g.context_length}</strong></div>`:''}${mr}</div>`; }
|
| 327 |
+
let pl = '';
|
| 328 |
+
if (a.has_preview) { if (t==='site') pl=`<a href="/site/${a.app_id}/" target="_blank">π Visit</a>`; else if (t==='chatgpt') pl=`<a href="/dapp/${a.app_id}/" target="_blank">π¬ Open dApp</a>`; else pl=`<a href="/app/${a.app_id}" target="_blank">βΆ Preview</a>`; }
|
| 329 |
+
const dl=`<a href="/api/download/${a.app_id}" download>β¬ Download</a>`;
|
| 330 |
+
const db=(t==='dmg'||t==='gguf'||t==='site')?`<button onclick="showDetails('${a.app_id}')">π Details</button>`:'';
|
| 331 |
+
const lb=(t==='gguf')?`<button onclick="loadModelById('${a.app_id}')">π§ Load</button>`:'';
|
| 332 |
+
const xb=`<button class="danger" onclick="deleteApp('${a.app_id}')">π Delete</button>`;
|
| 333 |
+
return `<div class="app-card"><div class="card-header"><div class="card-title">${esc(a.filename||a.app_name||'Unknown')}</div><div class="card-type ${tc}">${tl}</div></div><div class="card-meta"><span>π ${a.size_human||formatBytes(a.size)}</span><span>π ${timeAgo(a.created_at)}</span></div><div class="card-status ${sc}"><span class="dot"></span>${esc(st)}</div>${det}${gm}<div class="card-actions">${pl}${dl}${lb}${db}${xb}</div></div>`;
|
| 334 |
+
}
|
| 335 |
+
|
| 336 |
+
async function checkInference() {
|
| 337 |
+
try {
|
| 338 |
+
const r = await fetch('/api/inference/status'); const d = await r.json();
|
| 339 |
+
const dot = document.getElementById('inf-dot'); const txt = document.getElementById('inf-status-text'); const hint = document.getElementById('inf-hint');
|
| 340 |
+
if (d.available) { dot.className='dot on'; txt.textContent=`Inference available β ${d.loaded_models} model(s) loaded`; hint.textContent='Select a GGUF model and click Load Model.'; }
|
| 341 |
+
else { dot.className='dot off'; txt.textContent='Inference unavailable β llama-cpp-python not installed'; hint.textContent='Add llama-cpp-python to requirements.txt to enable inference.'; }
|
| 342 |
+
updateModelSelect();
|
| 343 |
+
} catch(e) { document.getElementById('inf-dot').className='dot off'; document.getElementById('inf-status-text').textContent='Failed to check'; }
|
| 344 |
+
}
|
| 345 |
+
function updateModelSelect() {
|
| 346 |
+
const sel = document.getElementById('inf-model-select'); if (!sel) return;
|
| 347 |
+
const gguf = apps.filter(a => a.file_type==='gguf');
|
| 348 |
+
sel.innerHTML = '<option value="">Select a GGUF model...</option>' + gguf.map(a => `<option value="${a.app_id}">${esc(a.filename)}</option>`).join('');
|
| 349 |
+
}
|
| 350 |
+
async function loadModelById(appId) {
|
| 351 |
+
try { const r = await fetch(`/api/inference/load/${appId}`, { method: 'POST' }); const d = await r.json();
|
| 352 |
+
if (r.ok) { loadedModelId = appId; document.getElementById('inf-model-select').value = appId; document.getElementById('inf-chat-input').disabled = false; document.getElementById('inf-send-btn').disabled = false; document.getElementById('inf-chat-box').innerHTML = `<div style="color:var(--green);text-align:center;padding:20px">β ${d.message}</div>`; }
|
| 353 |
+
else { alert(d.error||'Failed to load'); }
|
| 354 |
+
} catch(e) { alert('Error: '+e.message); }
|
| 355 |
+
}
|
| 356 |
+
async function loadModel() { const sel = document.getElementById('inf-model-select'); if (!sel.value) { alert('Select a model'); return; } await loadModelById(sel.value); }
|
| 357 |
+
async function sendChat() {
|
| 358 |
+
const inp = document.getElementById('inf-chat-input'); const msg = inp.value.trim(); if (!msg||!loadedModelId) return;
|
| 359 |
+
inp.value = ''; const box = document.getElementById('inf-chat-box');
|
| 360 |
+
box.innerHTML += `<div class="chat-msg"><div class="role user">You</div><div class="bubble">${esc(msg)}</div></div>`; box.scrollTop = box.scrollHeight;
|
| 361 |
+
const btn = document.getElementById('inf-send-btn'); btn.disabled = true; btn.innerHTML = '<span class="spinner"></span>';
|
| 362 |
+
try { const r = await fetch(`/api/inference/chat/${loadedModelId}`, { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ messages: [{role:'user',content:msg}], max_tokens: 512 }) });
|
| 363 |
+
const d = await r.json();
|
| 364 |
+
if (r.ok && d.choices && d.choices[0]) { box.innerHTML += `<div class="chat-msg"><div class="role assistant">Assistant</div><div class="bubble">${esc(d.choices[0].message.content)}</div></div>`; }
|
| 365 |
+
else { box.innerHTML += `<div class="chat-msg"><div class="role assistant">Error</div><div class="bubble">${esc(d.error||'Failed')}</div></div>`; }
|
| 366 |
+
box.scrollTop = box.scrollHeight;
|
| 367 |
+
} catch(e) { box.innerHTML += `<div class="chat-msg"><div class="role assistant">Error</div><div class="bubble">${esc(e.message)}</div></div>`; }
|
| 368 |
+
btn.disabled = false; btn.textContent = 'Send';
|
| 369 |
+
}
|
| 370 |
+
|
| 371 |
+
async function createWebapp() {
|
| 372 |
+
const url = document.getElementById('webapp-url').value.trim(); const name = document.getElementById('webapp-name').value.trim()||'WebApp'; const btn = document.getElementById('webapp-btn');
|
| 373 |
+
if (!url) { alert('URL required'); return; }
|
| 374 |
+
btn.disabled = true; btn.innerHTML = '<span class="spinner"></span> Creating...';
|
| 375 |
+
try { const r = await fetch('/api/create-webapp', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ url, app_name: name }) });
|
| 376 |
+
const d = await r.json(); if (r.ok) { document.getElementById('webapp-url').value=''; document.getElementById('webapp-name').value=''; loadApps(); } else { alert(d.error||'Failed'); }
|
| 377 |
+
} catch(e) { alert('Error: '+e.message); }
|
| 378 |
+
btn.disabled = false; btn.textContent = 'Create App';
|
| 379 |
+
}
|
| 380 |
+
|
| 381 |
+
function showDetails(appId) { const a = apps.find(x=>x.app_id===appId); if(!a) return; document.getElementById('modal-title').textContent = a.filename||a.app_name||'Details';
|
| 382 |
+
let b=''; if (a.gguf) b=`<pre>${esc(JSON.stringify(a.gguf,null,2))}</pre>`; else if (a.apps&&a.apps.length) b=`<pre>${esc(JSON.stringify(a.apps,null,2))}</pre>`; else if (a.tree) b=`<pre>${esc(JSON.stringify(a.tree,null,2))}</pre>`; else b=`<pre>${esc(JSON.stringify(a,null,2))}</pre>`;
|
| 383 |
+
document.getElementById('modal-body').innerHTML = b; document.getElementById('modal-overlay').classList.add('active');
|
| 384 |
+
}
|
| 385 |
+
function closeModal() { document.getElementById('modal-overlay').classList.remove('active'); }
|
| 386 |
+
async function deleteApp(appId) { if(!confirm('Delete?')) return; try { await fetch(`/api/apps/${appId}`,{method:'DELETE'}); loadApps(); } catch(e) { alert('Failed: '+e.message); } }
|
| 387 |
+
|
| 388 |
+
function esc(s) { if(!s) return ''; return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
|
| 389 |
+
function formatBytes(n) { if(!n) return '0 B'; const u=['B','KB','MB','GB','TB']; let i=0; while(n>=1024&&i<u.length-1){n/=1024;i++;} return n.toFixed(1)+' '+u[i]; }
|
| 390 |
+
function timeAgo(ts) { if(!ts) return 'β'; const d=Date.now()/1000-ts; if(d<60) return 'just now'; if(d<3600) return Math.floor(d/60)+'m ago'; if(d<86400) return Math.floor(d/3600)+'h ago'; return Math.floor(d/86400)+'d ago'; }
|
| 391 |
+
</script>
|
| 392 |
+
</body>
|
| 393 |
+
</html>
|