aidn commited on
Commit
29afff7
Β·
verified Β·
1 Parent(s): 6405207

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +230 -304
app.py CHANGED
@@ -8,6 +8,7 @@ External access: hf sync hf://buckets/aidn/AnyAgent-storage ./local
8
  """
9
 
10
  import os
 
11
  import json
12
  import datetime
13
  import gradio as gr
@@ -17,17 +18,13 @@ from huggingface_hub import InferenceClient
17
  HF_TOKEN = os.environ.get("HF_TOKEN", "")
18
  MODEL_ID = os.environ.get("MODEL_ID", "meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8")
19
 
20
- # HF Bucket is auto-mounted at /data β€” just use it like a local filesystem
21
- BUCKET_DIR = "/data"
22
- AGENTS_FILE = os.path.join(BUCKET_DIR, "agents.jsonl")
23
-
24
- # Ensure /data exists (it always should when bucket is mounted, but be safe)
25
  os.makedirs(BUCKET_DIR, exist_ok=True)
26
 
27
- # ── Bucket Storage β€” simple file I/O ─────────────────────────────────────────
28
 
29
  def _fetch_all() -> list:
30
- """Read all agents from the bucket-mounted JSONL file."""
31
  if not os.path.exists(AGENTS_FILE):
32
  return []
33
  entries = []
@@ -46,7 +43,6 @@ def _fetch_all() -> list:
46
 
47
 
48
  def _push_agent(agent: dict) -> str:
49
- """Append one agent as a new JSONL line to the bucket file."""
50
  try:
51
  with open(AGENTS_FILE, "a", encoding="utf-8") as f:
52
  f.write(json.dumps(agent, ensure_ascii=False) + "\n")
@@ -61,7 +57,6 @@ def _collection_agents() -> list:
61
  # ── File Processing ───────────────────────────────────────────────────────────
62
 
63
  def files_to_prompt(files, current: str) -> str:
64
- """Read uploaded files and append/merge their content into the prompt textarea."""
65
  if not files:
66
  return current
67
  parts = [current.strip()] if current.strip() else []
@@ -79,9 +74,6 @@ def files_to_prompt(files, current: str) -> str:
79
  # ── Chat ──────────────────────────────────────────────────────────────────────
80
 
81
  def _bot_stream(history: list, system_prompt: str, max_tokens: int):
82
- """Generator: yields updated history list with streaming assistant response.
83
- Gradio 6 default: messages format β€” dicts with role/content.
84
- """
85
  if not history:
86
  return
87
 
@@ -111,21 +103,15 @@ def _bot_stream(history: list, system_prompt: str, max_tokens: int):
111
  history[-1]["content"] += f"\n\n⚠️ Error: {e}"
112
  yield history
113
 
114
-
115
- def _user_submit(msg: str, history: list):
116
- """Add user message + empty assistant placeholder to history."""
117
- history = history + [[msg, None]]
118
- return "", history
119
-
120
  # ── Activate Agent ────────────────────────────────────────────────────────────
121
 
122
  def activate_agent(name: str, prompt: str, in_coll: bool):
123
- name = name.strip() or "Unnamed Agent"
124
  prompt = prompt.strip()
125
 
126
  if not prompt:
127
  return (
128
- "", # system_prompt_state
129
  _status_html("⚠️ Please add a system prompt before activating.", "warn"),
130
  _render_collection(),
131
  _render_quick_list(),
@@ -147,7 +133,7 @@ def activate_agent(name: str, prompt: str, in_coll: bool):
147
  else:
148
  status_msg += (
149
  '  πŸŒ Saved to collection '
150
- '<span style="color:#888;font-size:.75rem;">Β· /data/agents.jsonl</span>'
151
  )
152
 
153
  return (
@@ -158,15 +144,15 @@ def activate_agent(name: str, prompt: str, in_coll: bool):
158
  name,
159
  )
160
 
161
- # ── Load from collection signal (JS β†’ Python) ─────────────────────────────────
162
 
163
  def _load_signal(signal: str):
164
- """Parse the JSON signal emitted by agentForgeLoad() in JS and activate the agent."""
165
- if not signal or signal.strip() == "":
166
  return gr.update(), gr.update(), gr.update(), gr.update()
167
  try:
168
  data = json.loads(signal)
169
- name = data.get("name", "")
170
  prompt = data.get("prompt", "")
171
  return name, prompt, prompt, name
172
  except Exception:
@@ -176,13 +162,13 @@ def _load_signal(signal: str):
176
 
177
  def _status_html(msg: str, kind: str = "ok") -> str:
178
  colors = {
179
- "ok": ("rgba(74,222,128,.12)", "#4ade80", "#166534"),
180
- "warn": ("rgba(251,191,36,.12)", "#fbbf24", "#92400e"),
181
- "err": ("rgba(248,113,113,.12)", "#f87171", "#7f1d1d"),
182
  }
183
  bg, border, text = colors.get(kind, colors["ok"])
184
  return (
185
- f'<div style="background:{bg};border:1px solid {border}33;border-left:3px solid {border};'
186
  f'border-radius:8px;padding:10px 14px;font-size:.83rem;color:{text};line-height:1.6;">'
187
  f'{msg}</div>'
188
  )
@@ -195,172 +181,175 @@ def _agent_color(i: int) -> str:
195
  return _PALETTE[i % len(_PALETTE)]
196
 
197
 
198
- def _render_collection() -> str:
199
- agents = _collection_agents()
200
- count = len(agents)
 
 
 
 
 
 
 
 
 
 
 
201
 
 
 
 
 
202
  total_count = len(_fetch_all())
 
203
  bucket_badge = (
204
- f'<span style="font-size:.7rem;color:#888;border:1px solid #2a2a3a;'
205
- f'border-radius:99px;padding:2px 10px;margin-left:auto;font-family:JetBrains Mono,monospace;"'
206
- f' title="hf sync hf://buckets/aidn/AnyAgent-storage ./local">'
207
- f'πŸ—„οΈ /data &nbsp;Β·&nbsp; {total_count} total</span>'
 
 
 
 
 
 
 
208
  )
209
 
210
  if not agents:
211
- return f"""
212
- <div style="text-align:center;padding:60px 20px;color:#555;">
213
- <div style="font-size:3rem;margin-bottom:14px;">βš—οΈ</div>
214
- <div style="font-weight:700;font-size:.95rem;color:#888;margin-bottom:8px;">The forge is cold.</div>
215
- <div style="font-size:.82rem;color:#777;max-width:280px;margin:0 auto;line-height:1.6;">
216
- Activate an agent and check <strong style="color:#f97316;">Add to Collection</strong>
217
- to be the first to light it up.
218
- </div>
219
- </div>"""
220
 
221
  cards = ""
222
  for i, agent in enumerate(reversed(agents)):
223
  name = agent.get("name", "Unnamed Agent")
224
  prompt = agent.get("system_prompt", "")
225
- preview = (prompt[:100] + "…") if len(prompt) > 100 else prompt
226
  date = agent.get("timestamp", "")[:10]
227
  color = _agent_color(i)
228
  chars = len(prompt)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
229
 
230
- # Escape for JS
231
- p_js = (prompt
232
- .replace("\\", "\\\\")
233
- .replace("'", "\\'")
234
- .replace("\n", "\\n")
235
- .replace("\r", ""))
236
- n_js = name.replace("'", "\\'")
237
-
238
- cards += f"""
239
- <div style="background:#0f0f1c;border:1px solid #1e1e30;border-left:3px solid {color};
240
- border-radius:10px;padding:16px 18px;margin-bottom:10px;cursor:pointer;
241
- transition:all .18s;box-shadow:0 2px 8px rgba(0,0,0,.4);"
242
- onmouseover="this.style.background='#14142a';this.style.boxShadow='0 4px 16px rgba(0,0,0,.5)';"
243
- onmouseout="this.style.background='#0f0f1c';this.style.boxShadow='0 2px 8px rgba(0,0,0,.4)';"
244
- onclick="agentForgeLoad('{n_js}', '{p_js}')">
245
- <div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;">
246
- <div style="width:26px;height:26px;border-radius:7px;
247
- background:{color}1a;border:1px solid {color}44;
248
- display:flex;align-items:center;justify-content:center;font-size:.8rem;">
249
- πŸ€–
250
- </div>
251
- <span style="font-weight:700;color:#e0e0ff;font-size:.88rem;
252
- flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">
253
- {name}
254
- </span>
255
- <span style="font-size:.68rem;color:#6b6b88;white-space:nowrap;">{date}</span>
256
- </div>
257
- <div style="font-size:.74rem;color:#aaa;line-height:1.5;font-family:'JetBrains Mono',monospace;
258
- white-space:pre-wrap;max-height:42px;overflow:hidden;">
259
- {preview.replace('<','&lt;').replace('>','&gt;')}
260
- </div>
261
- <div style="margin-top:10px;display:flex;align-items:center;gap:8px;">
262
- <span style="font-size:.68rem;color:{color};border:1px solid {color}44;
263
- background:{color}11;border-radius:99px;padding:1px 8px;font-weight:600;">
264
- Load into Forge β†’
265
- </span>
266
- <span style="margin-left:auto;font-size:.67rem;color:#6b6b88;">
267
- {chars:,} chars
268
- </span>
269
- </div>
270
- </div>"""
271
-
272
- refresh_btn = """
273
- <button onclick="document.getElementById('hidden_refresh_btn').click()"
274
- style="background:transparent;border:1px solid #2a2a3a;color:#555;
275
- font-size:.68rem;border-radius:99px;padding:2px 8px;cursor:pointer;
276
- transition:all .15s;"
277
- onmouseover="this.style.color='#f97316';this.style.borderColor='#f97316';"
278
- onmouseout="this.style.color='#555';this.style.borderColor='#2a2a3a';">
279
- πŸ”„
280
- </button>"""
281
-
282
- return f"""
283
- <div>
284
- <div style="display:flex;align-items:center;gap:8px;margin-bottom:14px;flex-wrap:wrap;">
285
- <span style="font-weight:700;color:#f97316;font-size:.75rem;
286
- text-transform:uppercase;letter-spacing:.6px;">
287
- ⚑ {count} Agent{'s' if count != 1 else ''} Forged
288
- </span>
289
- {bucket_badge}
290
- {refresh_btn}
291
- </div>
292
- {cards}
293
- </div>"""
294
 
295
 
296
  def _render_quick_list() -> str:
297
  agents = _collection_agents()
298
  if not agents:
299
- return ('<div style="color:#444;font-size:.78rem;padding:6px 2px;">'
300
- 'No agents yet β€” forge the first one! ⚑</div>')
301
  items = ""
302
  for i, agent in enumerate(reversed(agents[:10])):
303
  name = agent.get("name", "Unnamed")
304
  prompt = agent.get("system_prompt", "")
305
  color = _agent_color(i)
306
- p_js = (prompt
307
- .replace("\\", "\\\\")
308
- .replace("'", "\\'")
309
- .replace("\n", "\\n")
310
- .replace("\r", ""))
311
- n_js = name.replace("'", "\\'")
312
- items += f"""
313
- <div style="display:flex;align-items:center;gap:8px;padding:6px 8px;border-radius:6px;
314
- cursor:pointer;transition:background .15s;"
315
- onmouseover="this.style.background='#14142a';"
316
- onmouseout="this.style.background='transparent';"
317
- onclick="agentForgeLoad('{n_js}', '{p_js}')">
318
- <div style="width:6px;height:6px;border-radius:50%;background:{color};flex-shrink:0;"></div>
319
- <span style="font-size:.8rem;color:#bbb;flex:1;overflow:hidden;
320
- text-overflow:ellipsis;white-space:nowrap;">{name}</span>
321
- <span style="font-size:.67rem;color:#6b6b88;white-space:nowrap;">Load β†’</span>
322
- </div>"""
323
- extra = (f'<div style="font-size:.68rem;color:#555;padding:4px 8px;margin-top:2px;">'
324
- f'+ {len(agents)-10} more in collection</div>') if len(agents) > 10 else ""
325
- return items + extra
326
 
327
 
328
  def _render_active_badge(name: str) -> str:
329
  if not name or name == "No agent active":
330
- return ('<div class="agent-badge inactive">'
331
- '<div class="dot-inactive"></div>No agent active</div>')
332
  safe = name[:40] + ("…" if len(name) > 40 else "")
333
- return (f'<div class="agent-badge active">'
334
- f'<div class="dot-active"></div>Active: <strong>{safe}</strong></div>')
335
 
336
  # ── JS ────────────────────────────────────────────────────────────────────────
 
337
 
338
- JS_HEAD = """
339
  <link rel="preconnect" href="https://fonts.googleapis.com">
340
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
341
  <link href="https://fonts.googleapis.com/css2?family=Chakra+Petch:wght@400;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
342
  <script>
343
  function agentForgeLoad(name, prompt) {
344
- const decoded = prompt.replace(/\n/g, "
345
- ");
346
- const signal = JSON.stringify({ name: name, prompt: decoded });
347
 
348
- let el = document.querySelector("#agent_load_signal textarea");
349
- if (!el) el = document.querySelector("#agent_load_signal input");
 
350
  if (!el) {
351
- const wrap = document.getElementById("agent_load_signal");
352
  if (wrap) el = wrap.querySelector("textarea, input");
353
  }
354
- if (!el) { console.warn("PromptHub: signal el not found"); return; }
355
-
356
- // React/Svelte-compatible value setter
357
- const proto = el.tagName === "TEXTAREA"
358
- ? window.HTMLTextAreaElement.prototype
359
- : window.HTMLInputElement.prototype;
360
- const setter = Object.getOwnPropertyDescriptor(proto, "value");
361
- if (setter && setter.set) setter.set.call(el, signal);
362
- else el.value = signal;
363
-
 
364
  el.dispatchEvent(new Event("input", { bubbles: true }));
365
  el.dispatchEvent(new Event("change", { bubbles: true }));
366
  }
@@ -370,7 +359,6 @@ function agentForgeLoad(name, prompt) {
370
  # ── CSS ───────────────────────────────────────────────────────────────────────
371
 
372
  CSS = """
373
- /* ── Tokens ── */
374
  :root {
375
  --bg: #08080f;
376
  --card: #0d0d1a;
@@ -380,16 +368,14 @@ CSS = """
380
  --orange: #f97316;
381
  --orange-dim: rgba(249,115,22,.15);
382
  --orange-glow:rgba(249,115,22,.25);
383
- --violet: #a78bfa;
384
  --text: #e0e0f0;
385
- --muted: #555570;
386
  --muted2: #888;
387
  --green: #4ade80;
388
  --font-head: 'Chakra Petch', sans-serif;
389
  --font-mono: 'JetBrains Mono', monospace;
390
  }
391
 
392
- /* ── Base ── */
393
  body, .gradio-container {
394
  background: var(--bg) !important;
395
  font-family: var(--font-head) !important;
@@ -402,8 +388,8 @@ body, .gradio-container {
402
  background: linear-gradient(135deg, #0d0d1a 0%, #130a20 50%, #0a1020 100%);
403
  border: 1px solid var(--border2);
404
  border-radius: 14px;
405
- padding: 26px 30px;
406
- margin-bottom: 18px;
407
  position: relative;
408
  overflow: hidden;
409
  }
@@ -418,16 +404,14 @@ body, .gradio-container {
418
  }
419
  .forge-header h1 {
420
  font-family: var(--font-head) !important;
421
- font-size: 2rem !important;
422
  font-weight: 800 !important;
423
  color: var(--text) !important;
424
- margin: 0 0 5px 0 !important;
425
- letter-spacing: -0.3px !important;
426
  position: relative;
427
  }
428
- .forge-header h1 em { font-style: normal; color: var(--orange); }
429
  .forge-header p {
430
- font-size: .83rem !important;
431
  color: var(--muted2) !important;
432
  margin: 0 !important;
433
  position: relative;
@@ -444,45 +428,14 @@ body, .gradio-container {
444
  font-size: .78rem;
445
  font-weight: 700;
446
  font-family: var(--font-head);
447
- letter-spacing: .2px;
448
  border: 1px solid;
449
  margin-bottom: 8px;
450
  }
451
- .agent-badge.active {
452
- background: rgba(249,115,22,.08);
453
- border-color: rgba(249,115,22,.3);
454
- color: var(--orange);
455
- }
456
- .agent-badge.inactive {
457
- background: rgba(85,85,112,.08);
458
- border-color: var(--border2);
459
- color: var(--muted2);
460
- }
461
- .dot-active {
462
- width: 7px; height: 7px; border-radius: 50%; background: var(--green);
463
- box-shadow: 0 0 7px var(--green);
464
- animation: pulse-dot 2s ease-in-out infinite;
465
- }
466
- .dot-inactive { width: 7px; height: 7px; border-radius: 50%; background: var(--muted); }
467
- @keyframes pulse-dot { 0%,100%{opacity:1} 50%{opacity:.35} }
468
-
469
- /* ── Section cards ── */
470
- .section-card {
471
- background: var(--card2);
472
- border: 1px solid var(--border);
473
- border-radius: 12px;
474
- padding: 16px 18px;
475
- margin-bottom: 10px;
476
- }
477
- .section-title {
478
- font-size: .68rem;
479
- font-weight: 700;
480
- text-transform: uppercase;
481
- letter-spacing: .7px;
482
- color: var(--orange);
483
- margin-bottom: 12px;
484
- font-family: var(--font-head);
485
- }
486
 
487
  /* ── Labels ── */
488
  label > span {
@@ -502,6 +455,7 @@ textarea, input[type="text"] {
502
  border-radius: 8px !important;
503
  font-size: .87rem !important;
504
  font-family: var(--font-mono) !important;
 
505
  }
506
  textarea:focus, input[type="text"]:focus {
507
  border-color: var(--orange) !important;
@@ -513,6 +467,35 @@ textarea::placeholder, input[type="text"]::placeholder {
513
  font-family: var(--font-mono) !important;
514
  }
515
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
516
  /* ── File drop ── */
517
  .file-upload-container, .upload-box {
518
  background: var(--card) !important;
@@ -536,15 +519,10 @@ button.primary {
536
  font-weight: 700 !important;
537
  font-family: var(--font-head) !important;
538
  color: #fff !important;
539
- padding: 10px 28px !important;
540
  box-shadow: 0 4px 16px var(--orange-glow) !important;
541
  transition: all .15s !important;
542
- letter-spacing: .3px !important;
543
- }
544
- button.primary:hover {
545
- box-shadow: 0 6px 22px rgba(249,115,22,.45) !important;
546
- transform: translateY(-1px) !important;
547
  }
 
548
  button.secondary {
549
  background: transparent !important;
550
  border: 1px solid var(--border2) !important;
@@ -554,25 +532,7 @@ button.secondary {
554
  font-weight: 600 !important;
555
  transition: all .15s !important;
556
  }
557
- button.secondary:hover {
558
- border-color: var(--orange) !important;
559
- color: var(--orange) !important;
560
- }
561
-
562
- /* ── Chatbot ── */
563
- .chatbot-wrap { border-color: var(--border2) !important; }
564
- .message-bubble-border { border-color: var(--border2) !important; }
565
-
566
- /* Gradio 6: ensure chat messages use full available width */
567
- .chatbot .message-wrap {
568
- max-width: 85% !important;
569
- min-width: 120px !important;
570
- }
571
- .chatbot .message-wrap p {
572
- white-space: pre-wrap !important;
573
- word-break: break-word !important;
574
- overflow-wrap: break-word !important;
575
- }
576
 
577
  /* ── Accordion ── */
578
  .accordion > .label-wrap {
@@ -584,16 +544,12 @@ button.secondary:hover {
584
  font-weight: 700 !important;
585
  }
586
  .accordion > .label-wrap:hover { background: var(--card) !important; }
587
- .accordion .icon { color: var(--orange) !important; }
588
-
589
- /* ── Sliders ── */
590
- input[type="range"] { accent-color: var(--orange) !important; }
591
 
592
  /* ── Tabs ── */
593
  .tab-nav button {
594
  font-family: var(--font-head) !important;
595
  font-weight: 700 !important;
596
- font-size: .78rem !important;
597
  text-transform: uppercase !important;
598
  letter-spacing: .4px !important;
599
  color: var(--muted2) !important;
@@ -601,29 +557,30 @@ input[type="range"] { accent-color: var(--orange) !important; }
601
  border-bottom: 2px solid transparent !important;
602
  transition: all .15s !important;
603
  }
604
- .tab-nav button.selected {
605
- color: var(--orange) !important;
606
- border-bottom-color: var(--orange) !important;
607
- }
608
 
609
  /* ── Hidden helpers ── */
610
  #hidden_refresh_btn { display: none !important; }
611
- #agent_load_signal { display: none !important; position: absolute !important; pointer-events: none !important; }
 
 
 
 
 
 
 
 
612
 
613
- /* ── Misc ── */
614
  footer { display: none !important; }
615
- .no-border { border: none !important; box-shadow: none !important; }
616
  """
617
 
618
  # ── Build UI ──────────────────────────────────────────────────────────────────
619
 
620
  with gr.Blocks(title="PromptHub ⚑") as demo:
621
 
622
- # ── Global state ──────────────────────────────────────────────────────────
623
  system_prompt_state = gr.State("")
624
  active_name_state = gr.State("No agent active")
625
 
626
- # ── Header ────────────────────────────────────────────────────────────────
627
  gr.HTML("""
628
  <div class="forge-header">
629
  <h1>Prompt<em style="font-style:normal;color:#f97316;">Hub</em> ⚑</h1>
@@ -631,101 +588,88 @@ with gr.Blocks(title="PromptHub ⚑") as demo:
631
  </div>
632
  """)
633
 
634
- # ── Main layout ────────────��──────────────────────────────────────────────
635
  with gr.Row(equal_height=False):
636
 
637
- # ── LEFT: Forge panel ─────────────────────────────────────────────────
638
- with gr.Column(scale=4, min_width=300):
639
 
640
- # File upload
641
- gr.HTML('<div class="section-title">πŸ“ Drop Files</div>')
642
  file_upload = gr.File(
643
  label="",
644
  file_count="multiple",
645
  file_types=[".md", ".txt", ".skill", ".yaml", ".yml",
646
  ".json", ".py", ".ts", ".js", ".toml"],
647
- height=120,
648
  )
649
 
650
- # System prompt textarea
651
  prompt_box = gr.Textbox(
652
  label="System Prompt",
653
  placeholder="Paste or type your system prompt…\nOr drop files above ↑\n\nTip: multiple files are auto-concatenated.",
654
- lines=10,
655
- max_lines=24,
656
  )
657
 
658
- # Agent name
659
  agent_name_box = gr.Textbox(
660
  label="Agent Name",
661
  placeholder="e.g. Code Reviewer Β· Sales Coach Β· SQL Expert",
662
  max_lines=1,
663
  )
664
 
665
- # Collection checkbox
666
  in_coll_check = gr.Checkbox(
667
  label="🌐 Add to public Agent Collection",
668
  value=False,
669
- info="Saves the raw system prompt string to the shared HF Dataset.",
670
  )
671
 
672
- # Activate button
673
  activate_btn = gr.Button("⚑ Activate Agent", variant="primary", size="lg")
 
674
 
675
- # Status feedback
676
- status_out = gr.HTML("")
677
-
678
- # Max tokens (hidden, accessible via accordion)
679
  with gr.Accordion("βš™οΈ Model Settings", open=False):
680
  max_tokens_slider = gr.Slider(
681
  minimum=256, maximum=8192, value=1024, step=128,
682
  label="Max output tokens",
683
  )
684
- gr.Markdown(
685
- f"*Model: `{MODEL_ID}`*",
686
- elem_classes=["no-border"],
687
- )
688
 
689
- gr.HTML("<div style='height:6px;'></div>")
690
 
691
- # Quick-load from collection
692
  with gr.Accordion("πŸ“š Quick Load from Collection", open=True):
693
  quick_list_html = gr.HTML(_render_quick_list())
694
 
695
- # Hidden: JS β†’ Python signal for loading an agent
696
  agent_load_signal = gr.Textbox(
697
  value="",
698
  elem_id="agent_load_signal",
699
  label="",
700
- visible=True, # hidden via CSS β€” visible=False skips DOM render in Gr6
701
  )
702
 
703
- # ── RIGHT: Chat ───────────────────────────────────────────────────────
704
  with gr.Column(scale=6):
705
 
706
  active_badge_html = gr.HTML(_render_active_badge("No agent active"))
707
 
708
  chatbot = gr.Chatbot(
709
- height=430,
710
  show_label=False,
711
  )
712
 
 
713
  with gr.Row():
714
- msg_box = gr.Textbox(
715
  placeholder="Message your agent…",
716
  show_label=False,
717
- container=False,
718
- scale=8,
719
  max_lines=4,
720
  )
721
- send_btn = gr.Button("Send", variant="primary", scale=1, min_width=72)
722
- clear_btn = gr.Button("Clear", variant="secondary", scale=1, min_width=72)
723
 
724
- # ── Full collection tab ────────────────────────────────────────────────────
725
- gr.HTML("<div style='height:10px;'></div>")
726
  with gr.Tabs():
727
  with gr.Tab("🌐 Agent Collection"):
728
- collection_out = gr.HTML(_render_collection())
729
  hidden_refresh_btn = gr.Button("refresh", elem_id="hidden_refresh_btn", visible=False)
730
 
731
  with gr.Tab("ℹ️ About"):
@@ -734,9 +678,8 @@ with gr.Blocks(title="PromptHub ⚑") as demo:
734
 
735
  **Forge any AI agent from plain text files.**
736
 
737
- Drop `.md`, `.skill`, `.txt`, `.yaml`, or any text file β€” PromptHub reads them,
738
- concatenates their content, and uses it as the system prompt for a live chat session
739
- with a large language model.
740
 
741
  ### How it works
742
  1. **Drop files** or paste text directly into the *System Prompt* box
@@ -744,46 +687,33 @@ with a large language model.
744
  3. Optionally **add it to the Collection** so others can load and use it
745
  4. Hit **⚑ Activate Agent** β€” then chat!
746
 
747
- ### The Collection & Dataset
748
- Every agent added to the Collection is stored as a raw JSON line in a public
749
- Hugging Face Dataset. The schema is intentionally minimal:
750
 
751
- ```json
752
- {
753
- "name": "My Agent",
754
- "system_prompt": "...",
755
- "in_collection": true,
756
- "timestamp": "2025-01-01T00:00:00Z"
757
- }
758
  ```
759
 
760
- This lets you fine-tune models, run evals, or build RAG pipelines directly
761
- from community-forged prompts.
762
-
763
- ### Environment secrets
764
  | Secret | Purpose |
765
  |--------|---------|
766
- | `HF_TOKEN` | Required for inference + dataset writes |
767
- | `DATASET_REPO` | Your HF dataset repo, e.g. `yourname/agent-forge` |
768
- | `MODEL_ID` | Optional override (default: Llama 4 Maverick) |
769
  """)
770
 
771
- # ── Config warnings ───────────────────────────────────────────────────────
772
  if not HF_TOKEN:
773
  gr.HTML(_status_html(
774
- "No <code>HF_TOKEN</code> found β€” add it in <em>Settings β†’ Secrets</em>. "
775
- "Inference and dataset writes will fail.", "warn"))
776
 
777
- # ── Event wiring ──────────────────────────────────────────────────────────
778
 
779
- # 1. Files β†’ append to prompt textarea
780
  file_upload.change(
781
  fn=files_to_prompt,
782
  inputs=[file_upload, prompt_box],
783
  outputs=[prompt_box],
784
  )
785
 
786
- # 2. Activate agent
787
  activate_btn.click(
788
  fn=activate_agent,
789
  inputs=[agent_name_box, prompt_box, in_coll_check],
@@ -795,7 +725,6 @@ from community-forged prompts.
795
  outputs=[active_badge_html],
796
  )
797
 
798
- # 3. Chat: user message submit
799
  def _submit(msg, hist):
800
  if not msg.strip():
801
  return gr.update(), hist
@@ -824,10 +753,8 @@ from community-forged prompts.
824
  outputs=[chatbot],
825
  )
826
 
827
- # 4. Clear chat
828
  clear_btn.click(fn=lambda: [], outputs=[chatbot])
829
 
830
- # 5. Load agent from JS signal (collection card click) β†’ also activates
831
  agent_load_signal.change(
832
  fn=_load_signal,
833
  inputs=[agent_load_signal],
@@ -838,7 +765,6 @@ from community-forged prompts.
838
  outputs=[active_badge_html],
839
  )
840
 
841
- # 6. Refresh collection (triggered by hidden button from JS)
842
  hidden_refresh_btn.click(
843
  fn=lambda: (_render_collection(), _render_quick_list()),
844
  outputs=[collection_out, quick_list_html],
 
8
  """
9
 
10
  import os
11
+ import re
12
  import json
13
  import datetime
14
  import gradio as gr
 
18
  HF_TOKEN = os.environ.get("HF_TOKEN", "")
19
  MODEL_ID = os.environ.get("MODEL_ID", "meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8")
20
 
21
+ BUCKET_DIR = "/data"
22
+ AGENTS_FILE = os.path.join(BUCKET_DIR, "agents.jsonl")
 
 
 
23
  os.makedirs(BUCKET_DIR, exist_ok=True)
24
 
25
+ # ── Storage ───────────────────────────────────────────────────────────────────
26
 
27
  def _fetch_all() -> list:
 
28
  if not os.path.exists(AGENTS_FILE):
29
  return []
30
  entries = []
 
43
 
44
 
45
  def _push_agent(agent: dict) -> str:
 
46
  try:
47
  with open(AGENTS_FILE, "a", encoding="utf-8") as f:
48
  f.write(json.dumps(agent, ensure_ascii=False) + "\n")
 
57
  # ── File Processing ───────────────────────────────────────────────────────────
58
 
59
  def files_to_prompt(files, current: str) -> str:
 
60
  if not files:
61
  return current
62
  parts = [current.strip()] if current.strip() else []
 
74
  # ── Chat ──────────────────────────────────────────────────────────────────────
75
 
76
  def _bot_stream(history: list, system_prompt: str, max_tokens: int):
 
 
 
77
  if not history:
78
  return
79
 
 
103
  history[-1]["content"] += f"\n\n⚠️ Error: {e}"
104
  yield history
105
 
 
 
 
 
 
 
106
  # ── Activate Agent ────────────────────────────────────────────────────────────
107
 
108
  def activate_agent(name: str, prompt: str, in_coll: bool):
109
+ name = name.strip() or "Unnamed Agent"
110
  prompt = prompt.strip()
111
 
112
  if not prompt:
113
  return (
114
+ "",
115
  _status_html("⚠️ Please add a system prompt before activating.", "warn"),
116
  _render_collection(),
117
  _render_quick_list(),
 
133
  else:
134
  status_msg += (
135
  ' &nbsp;🌐 Saved to collection '
136
+ '<span style="color:#666;font-size:.75rem;">Β· /data/agents.jsonl</span>'
137
  )
138
 
139
  return (
 
144
  name,
145
  )
146
 
147
+ # ── Load from collection (JS β†’ Python via hidden state) ──────────────────────
148
 
149
  def _load_signal(signal: str):
150
+ """Parse JSON signal from agentForgeLoad() JS call."""
151
+ if not signal or not signal.strip():
152
  return gr.update(), gr.update(), gr.update(), gr.update()
153
  try:
154
  data = json.loads(signal)
155
+ name = data.get("name", "")
156
  prompt = data.get("prompt", "")
157
  return name, prompt, prompt, name
158
  except Exception:
 
162
 
163
  def _status_html(msg: str, kind: str = "ok") -> str:
164
  colors = {
165
+ "ok": ("rgba(74,222,128,.10)", "#4ade80", "#a7f3d0"),
166
+ "warn": ("rgba(251,191,36,.10)", "#fbbf24", "#fde68a"),
167
+ "err": ("rgba(248,113,113,.10)", "#f87171", "#fca5a5"),
168
  }
169
  bg, border, text = colors.get(kind, colors["ok"])
170
  return (
171
+ f'<div style="background:{bg};border:1px solid {border}44;border-left:3px solid {border};'
172
  f'border-radius:8px;padding:10px 14px;font-size:.83rem;color:{text};line-height:1.6;">'
173
  f'{msg}</div>'
174
  )
 
181
  return _PALETTE[i % len(_PALETTE)]
182
 
183
 
184
+ def _prompt_preview(prompt: str, max_len: int = 90) -> str:
185
+ """One-line preview: strip all newlines, truncate."""
186
+ clean = re.sub(r'\s+', ' ', prompt).strip()
187
+ return (clean[:max_len] + "…") if len(clean) > max_len else clean
188
+
189
+
190
+ def _js_escape(s: str) -> str:
191
+ """Escape a string for safe embedding inside JS single-quoted string."""
192
+ return (s
193
+ .replace("\\", "\\\\")
194
+ .replace("'", "\\'")
195
+ .replace("\n", "\\n")
196
+ .replace("\r", "")
197
+ .replace('"', '\\"'))
198
 
199
+
200
+ def _render_collection() -> str:
201
+ agents = _collection_agents()
202
+ count = len(agents)
203
  total_count = len(_fetch_all())
204
+
205
  bucket_badge = (
206
+ f'<span style="font-size:.68rem;color:#666;border:1px solid #2a2a3a;'
207
+ f'border-radius:99px;padding:2px 10px;font-family:\'JetBrains Mono\',monospace;'
208
+ f'white-space:nowrap;" title="hf sync hf://buckets/aidn/AnyAgent-storage ./local">'
209
+ f'πŸ—„οΈ /data Β· {total_count}</span>'
210
+ )
211
+ refresh_btn = (
212
+ '<button onclick="document.getElementById(\'hidden_refresh_btn\').click()"'
213
+ ' style="background:transparent;border:1px solid #2a2a3a;color:#666;'
214
+ 'font-size:.68rem;border-radius:99px;padding:2px 8px;cursor:pointer;transition:all .15s;"'
215
+ ' onmouseover="this.style.color=\'#f97316\';this.style.borderColor=\'#f97316\';"'
216
+ ' onmouseout="this.style.color=\'#666\';this.style.borderColor=\'#2a2a3a\';">πŸ”„</button>'
217
  )
218
 
219
  if not agents:
220
+ return (
221
+ '<div style="text-align:center;padding:50px 20px;">'
222
+ '<div style="font-size:2.5rem;margin-bottom:12px;">βš—οΈ</div>'
223
+ '<div style="font-weight:700;font-size:.9rem;color:#888;margin-bottom:6px;">The forge is cold.</div>'
224
+ '<div style="font-size:.8rem;color:#666;max-width:260px;margin:0 auto;line-height:1.6;">'
225
+ 'Activate an agent and check <strong style="color:#f97316;">Add to Collection</strong>'
226
+ ' to be the first.</div></div>'
227
+ )
 
228
 
229
  cards = ""
230
  for i, agent in enumerate(reversed(agents)):
231
  name = agent.get("name", "Unnamed Agent")
232
  prompt = agent.get("system_prompt", "")
 
233
  date = agent.get("timestamp", "")[:10]
234
  color = _agent_color(i)
235
  chars = len(prompt)
236
+ preview = _prompt_preview(prompt, 80)
237
+
238
+ n_js = _js_escape(name)
239
+ p_js = _js_escape(prompt)
240
+
241
+ safe_preview = preview.replace("<", "&lt;").replace(">", "&gt;")
242
+
243
+ cards += (
244
+ f'<div style="background:#0c0c1a;border:1px solid #1e1e30;border-left:3px solid {color};'
245
+ f'border-radius:10px;padding:14px 16px;margin-bottom:8px;cursor:pointer;transition:background .15s;"'
246
+ f' onmouseover="this.style.background=\'#12122a\';"'
247
+ f' onmouseout="this.style.background=\'#0c0c1a\';"'
248
+ f' onclick="agentForgeLoad(\'{n_js}\', \'{p_js}\')">'
249
+
250
+ f'<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px;">'
251
+ f'<span style="font-weight:700;color:#ddddf0;font-size:.86rem;flex:1;'
252
+ f'overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">{name}</span>'
253
+ f'<span style="font-size:.65rem;color:#555;white-space:nowrap;">{date}</span>'
254
+ f'</div>'
255
+
256
+ # Expandable preview using <details>
257
+ f'<details style="margin-bottom:8px;">'
258
+ f'<summary style="font-size:.72rem;color:#777;cursor:pointer;'
259
+ f'list-style:none;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;'
260
+ f'font-family:\'JetBrains Mono\',monospace;">{safe_preview}</summary>'
261
+ f'<div style="margin-top:8px;font-size:.72rem;color:#999;font-family:\'JetBrains Mono\',monospace;'
262
+ f'white-space:pre-wrap;line-height:1.5;max-height:180px;overflow-y:auto;'
263
+ f'background:#080812;padding:8px;border-radius:6px;">'
264
+ f'{prompt[:800].replace("<","&lt;").replace(">","&gt;")}{"…" if len(prompt)>800 else ""}'
265
+ f'</div></details>'
266
+
267
+ f'<div style="display:flex;align-items:center;gap:8px;">'
268
+ f'<span style="font-size:.65rem;color:{color};border:1px solid {color}44;'
269
+ f'background:{color}11;border-radius:99px;padding:1px 8px;font-weight:600;white-space:nowrap;">'
270
+ f'Load into Forge β†’</span>'
271
+ f'<span style="margin-left:auto;font-size:.65rem;color:#555;">{chars:,} chars</span>'
272
+ f'</div>'
273
+ f'</div>'
274
+ )
275
 
276
+ return (
277
+ f'<div>'
278
+ f'<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px;flex-wrap:wrap;">'
279
+ f'<span style="font-weight:700;color:#f97316;font-size:.72rem;'
280
+ f'text-transform:uppercase;letter-spacing:.6px;">⚑ {count} Agent{"s" if count!=1 else ""} Forged</span>'
281
+ f'<div style="margin-left:auto;display:flex;gap:6px;align-items:center;">'
282
+ f'{bucket_badge}{refresh_btn}'
283
+ f'</div></div>'
284
+ f'{cards}'
285
+ f'</div>'
286
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
287
 
288
 
289
  def _render_quick_list() -> str:
290
  agents = _collection_agents()
291
  if not agents:
292
+ return '<div style="color:#555;font-size:.78rem;padding:6px 2px;">No agents yet β€” forge the first one! ⚑</div>'
 
293
  items = ""
294
  for i, agent in enumerate(reversed(agents[:10])):
295
  name = agent.get("name", "Unnamed")
296
  prompt = agent.get("system_prompt", "")
297
  color = _agent_color(i)
298
+ n_js = _js_escape(name)
299
+ p_js = _js_escape(prompt)
300
+ items += (
301
+ f'<div style="display:flex;align-items:center;gap:8px;padding:5px 6px;border-radius:6px;'
302
+ f'cursor:pointer;transition:background .15s;"'
303
+ f' onmouseover="this.style.background=\'#14142a\';"'
304
+ f' onmouseout="this.style.background=\'transparent\';"'
305
+ f' onclick="agentForgeLoad(\'{n_js}\', \'{p_js}\')">'
306
+ f'<div style="width:5px;height:5px;border-radius:50%;background:{color};flex-shrink:0;"></div>'
307
+ f'<span style="font-size:.8rem;color:#ccc;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">{name}</span>'
308
+ f'<span style="font-size:.65rem;color:#555;white-space:nowrap;">Load β†’</span>'
309
+ f'</div>'
310
+ )
311
+ if len(agents) > 10:
312
+ items += f'<div style="font-size:.67rem;color:#555;padding:4px 6px;">+ {len(agents)-10} more</div>'
313
+ return items
 
 
 
 
314
 
315
 
316
  def _render_active_badge(name: str) -> str:
317
  if not name or name == "No agent active":
318
+ return '<div class="agent-badge inactive"><div class="dot-inactive"></div>No agent active</div>'
 
319
  safe = name[:40] + ("…" if len(name) > 40 else "")
320
+ return f'<div class="agent-badge active"><div class="dot-active"></div>Active: <strong>{safe}</strong></div>'
 
321
 
322
  # ── JS ────────────────────────────────────────────────────────────────────────
323
+ # NOTE: head= is passed to demo.launch() in Gradio 6
324
 
325
+ JS_HEAD = r"""
326
  <link rel="preconnect" href="https://fonts.googleapis.com">
327
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
328
  <link href="https://fonts.googleapis.com/css2?family=Chakra+Petch:wght@400;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
329
  <script>
330
  function agentForgeLoad(name, prompt) {
331
+ // Decode escaped newlines back to real newlines
332
+ var decoded = prompt.split("\\n").join("\n");
333
+ var signal = JSON.stringify({ name: name, prompt: decoded });
334
 
335
+ // Find the hidden signal textarea β€” try multiple selectors
336
+ var el = document.querySelector("#agent_load_signal textarea");
337
+ if (!el) el = document.querySelector("#agent_load_signal input[type='text']");
338
  if (!el) {
339
+ var wrap = document.getElementById("agent_load_signal");
340
  if (wrap) el = wrap.querySelector("textarea, input");
341
  }
342
+ if (!el) { console.warn("PromptHub: signal element not found"); return; }
343
+
344
+ // Use native setter so Svelte/React reactivity picks it up
345
+ var nativeProto = Object.getPrototypeOf(el);
346
+ var descriptor = Object.getOwnPropertyDescriptor(nativeProto, "value")
347
+ || Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value");
348
+ if (descriptor && descriptor.set) {
349
+ descriptor.set.call(el, signal);
350
+ } else {
351
+ el.value = signal;
352
+ }
353
  el.dispatchEvent(new Event("input", { bubbles: true }));
354
  el.dispatchEvent(new Event("change", { bubbles: true }));
355
  }
 
359
  # ── CSS ───────────────────────────────────────────────────────────────────────
360
 
361
  CSS = """
 
362
  :root {
363
  --bg: #08080f;
364
  --card: #0d0d1a;
 
368
  --orange: #f97316;
369
  --orange-dim: rgba(249,115,22,.15);
370
  --orange-glow:rgba(249,115,22,.25);
 
371
  --text: #e0e0f0;
372
+ --muted: #44445a;
373
  --muted2: #888;
374
  --green: #4ade80;
375
  --font-head: 'Chakra Petch', sans-serif;
376
  --font-mono: 'JetBrains Mono', monospace;
377
  }
378
 
 
379
  body, .gradio-container {
380
  background: var(--bg) !important;
381
  font-family: var(--font-head) !important;
 
388
  background: linear-gradient(135deg, #0d0d1a 0%, #130a20 50%, #0a1020 100%);
389
  border: 1px solid var(--border2);
390
  border-radius: 14px;
391
+ padding: 24px 28px;
392
+ margin-bottom: 16px;
393
  position: relative;
394
  overflow: hidden;
395
  }
 
404
  }
405
  .forge-header h1 {
406
  font-family: var(--font-head) !important;
407
+ font-size: 1.9rem !important;
408
  font-weight: 800 !important;
409
  color: var(--text) !important;
410
+ margin: 0 0 4px 0 !important;
 
411
  position: relative;
412
  }
 
413
  .forge-header p {
414
+ font-size: .8rem !important;
415
  color: var(--muted2) !important;
416
  margin: 0 !important;
417
  position: relative;
 
428
  font-size: .78rem;
429
  font-weight: 700;
430
  font-family: var(--font-head);
 
431
  border: 1px solid;
432
  margin-bottom: 8px;
433
  }
434
+ .agent-badge.active { background: rgba(249,115,22,.08); border-color: rgba(249,115,22,.3); color: var(--orange); }
435
+ .agent-badge.inactive { background: rgba(85,85,112,.06); border-color: var(--border2); color: var(--muted2); }
436
+ .dot-active { width:7px;height:7px;border-radius:50%;background:var(--green);box-shadow:0 0 7px var(--green);animation:pulse-dot 2s ease-in-out infinite; }
437
+ .dot-inactive { width:7px;height:7px;border-radius:50%;background:var(--muted); }
438
+ @keyframes pulse-dot { 0%,100%{opacity:1} 50%{opacity:.3} }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
439
 
440
  /* ── Labels ── */
441
  label > span {
 
455
  border-radius: 8px !important;
456
  font-size: .87rem !important;
457
  font-family: var(--font-mono) !important;
458
+ writing-mode: horizontal-tb !important;
459
  }
460
  textarea:focus, input[type="text"]:focus {
461
  border-color: var(--orange) !important;
 
467
  font-family: var(--font-mono) !important;
468
  }
469
 
470
+ /* ── Chatbot β€” fix vertical letter stacking ── */
471
+ /* Force horizontal writing mode on ALL chatbot children */
472
+ .chatbot,
473
+ .chatbot *,
474
+ [class*="chatbot"] *,
475
+ [data-testid="bot"] *,
476
+ [data-testid="user"] * {
477
+ writing-mode: horizontal-tb !important;
478
+ text-orientation: mixed !important;
479
+ }
480
+ /* Message bubble width */
481
+ [data-testid="bot"],
482
+ [data-testid="user"] {
483
+ max-width: 85% !important;
484
+ min-width: 80px !important;
485
+ width: fit-content !important;
486
+ }
487
+ /* Ensure text wraps properly inside bubbles */
488
+ [data-testid="bot"] p,
489
+ [data-testid="user"] p,
490
+ [data-testid="bot"] span,
491
+ [data-testid="user"] span {
492
+ white-space: pre-wrap !important;
493
+ word-break: break-word !important;
494
+ overflow-wrap: break-word !important;
495
+ display: block !important;
496
+ width: 100% !important;
497
+ }
498
+
499
  /* ── File drop ── */
500
  .file-upload-container, .upload-box {
501
  background: var(--card) !important;
 
519
  font-weight: 700 !important;
520
  font-family: var(--font-head) !important;
521
  color: #fff !important;
 
522
  box-shadow: 0 4px 16px var(--orange-glow) !important;
523
  transition: all .15s !important;
 
 
 
 
 
524
  }
525
+ button.primary:hover { box-shadow: 0 6px 22px rgba(249,115,22,.45) !important; }
526
  button.secondary {
527
  background: transparent !important;
528
  border: 1px solid var(--border2) !important;
 
532
  font-weight: 600 !important;
533
  transition: all .15s !important;
534
  }
535
+ button.secondary:hover { border-color: var(--orange) !important; color: var(--orange) !important; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
536
 
537
  /* ── Accordion ── */
538
  .accordion > .label-wrap {
 
544
  font-weight: 700 !important;
545
  }
546
  .accordion > .label-wrap:hover { background: var(--card) !important; }
 
 
 
 
547
 
548
  /* ── Tabs ── */
549
  .tab-nav button {
550
  font-family: var(--font-head) !important;
551
  font-weight: 700 !important;
552
+ font-size: .75rem !important;
553
  text-transform: uppercase !important;
554
  letter-spacing: .4px !important;
555
  color: var(--muted2) !important;
 
557
  border-bottom: 2px solid transparent !important;
558
  transition: all .15s !important;
559
  }
560
+ .tab-nav button.selected { color: var(--orange) !important; border-bottom-color: var(--orange) !important; }
 
 
 
561
 
562
  /* ── Hidden helpers ── */
563
  #hidden_refresh_btn { display: none !important; }
564
+ /* Signal box: in DOM but invisible */
565
+ #agent_load_signal {
566
+ position: absolute !important;
567
+ opacity: 0 !important;
568
+ pointer-events: none !important;
569
+ height: 0 !important;
570
+ overflow: hidden !important;
571
+ width: 1px !important;
572
+ }
573
 
 
574
  footer { display: none !important; }
 
575
  """
576
 
577
  # ── Build UI ──────────────────────────────────────────────────────────────────
578
 
579
  with gr.Blocks(title="PromptHub ⚑") as demo:
580
 
 
581
  system_prompt_state = gr.State("")
582
  active_name_state = gr.State("No agent active")
583
 
 
584
  gr.HTML("""
585
  <div class="forge-header">
586
  <h1>Prompt<em style="font-style:normal;color:#f97316;">Hub</em> ⚑</h1>
 
588
  </div>
589
  """)
590
 
 
591
  with gr.Row(equal_height=False):
592
 
593
+ # ── LEFT ──────────────────────────────────────────────────────────────
594
+ with gr.Column(scale=4, min_width=280):
595
 
596
+ gr.HTML('<div style="font-size:.66rem;font-weight:700;text-transform:uppercase;letter-spacing:.7px;color:#f97316;margin-bottom:8px;">πŸ“ Drop Files</div>')
 
597
  file_upload = gr.File(
598
  label="",
599
  file_count="multiple",
600
  file_types=[".md", ".txt", ".skill", ".yaml", ".yml",
601
  ".json", ".py", ".ts", ".js", ".toml"],
602
+ height=110,
603
  )
604
 
 
605
  prompt_box = gr.Textbox(
606
  label="System Prompt",
607
  placeholder="Paste or type your system prompt…\nOr drop files above ↑\n\nTip: multiple files are auto-concatenated.",
608
+ lines=9,
609
+ max_lines=22,
610
  )
611
 
 
612
  agent_name_box = gr.Textbox(
613
  label="Agent Name",
614
  placeholder="e.g. Code Reviewer Β· Sales Coach Β· SQL Expert",
615
  max_lines=1,
616
  )
617
 
 
618
  in_coll_check = gr.Checkbox(
619
  label="🌐 Add to public Agent Collection",
620
  value=False,
621
+ info="Saves the raw system prompt string to the shared HF Bucket.",
622
  )
623
 
 
624
  activate_btn = gr.Button("⚑ Activate Agent", variant="primary", size="lg")
625
+ status_out = gr.HTML("")
626
 
 
 
 
 
627
  with gr.Accordion("βš™οΈ Model Settings", open=False):
628
  max_tokens_slider = gr.Slider(
629
  minimum=256, maximum=8192, value=1024, step=128,
630
  label="Max output tokens",
631
  )
632
+ gr.Markdown(f"*Model: `{MODEL_ID}`*")
 
 
 
633
 
634
+ gr.HTML("<div style='height:4px;'></div>")
635
 
 
636
  with gr.Accordion("πŸ“š Quick Load from Collection", open=True):
637
  quick_list_html = gr.HTML(_render_quick_list())
638
 
639
+ # Signal textbox: visible=True but hidden via CSS (display:none breaks Gr6 reactivity)
640
  agent_load_signal = gr.Textbox(
641
  value="",
642
  elem_id="agent_load_signal",
643
  label="",
644
+ visible=True,
645
  )
646
 
647
+ # ── RIGHT ─────────────────────────────────────────────────────────────
648
  with gr.Column(scale=6):
649
 
650
  active_badge_html = gr.HTML(_render_active_badge("No agent active"))
651
 
652
  chatbot = gr.Chatbot(
653
+ height=440,
654
  show_label=False,
655
  )
656
 
657
+ # Chat input row β€” no container=False to avoid layout collapse on mobile
658
  with gr.Row():
659
+ msg_box = gr.Textbox(
660
  placeholder="Message your agent…",
661
  show_label=False,
662
+ scale=7,
 
663
  max_lines=4,
664
  )
665
+ send_btn = gr.Button("Send", variant="primary", scale=2, min_width=80)
666
+ clear_btn = gr.Button("Clear", variant="secondary", scale=1, min_width=70)
667
 
668
+ # ── Tabs ──────────────────────────────────────────────────────────────────
669
+ gr.HTML("<div style='height:8px;'></div>")
670
  with gr.Tabs():
671
  with gr.Tab("🌐 Agent Collection"):
672
+ collection_out = gr.HTML(_render_collection())
673
  hidden_refresh_btn = gr.Button("refresh", elem_id="hidden_refresh_btn", visible=False)
674
 
675
  with gr.Tab("ℹ️ About"):
 
678
 
679
  **Forge any AI agent from plain text files.**
680
 
681
+ Drop `.md`, `.skill`, `.txt`, `.yaml`, or any text file β€” PromptHub reads them,
682
+ concatenates their content, and uses it as the system prompt for a live chat session.
 
683
 
684
  ### How it works
685
  1. **Drop files** or paste text directly into the *System Prompt* box
 
687
  3. Optionally **add it to the Collection** so others can load and use it
688
  4. Hit **⚑ Activate Agent** β€” then chat!
689
 
690
+ ### Storage
691
+ All agents are stored in `/data/agents.jsonl` via HF Bucket (auto-mounted).
 
692
 
693
+ ```bash
694
+ # Download the bucket
695
+ hf sync hf://buckets/aidn/AnyAgent-storage ./local
 
 
 
 
696
  ```
697
 
698
+ ### Secrets
 
 
 
699
  | Secret | Purpose |
700
  |--------|---------|
701
+ | `HF_TOKEN` | Required for inference |
702
+ | `MODEL_ID` | Optional model override |
 
703
  """)
704
 
 
705
  if not HF_TOKEN:
706
  gr.HTML(_status_html(
707
+ "No <code>HF_TOKEN</code> β€” add it in <em>Settings β†’ Secrets</em>.", "warn"))
 
708
 
709
+ # ── Events ────────────────────────────────────────────────────────────────
710
 
 
711
  file_upload.change(
712
  fn=files_to_prompt,
713
  inputs=[file_upload, prompt_box],
714
  outputs=[prompt_box],
715
  )
716
 
 
717
  activate_btn.click(
718
  fn=activate_agent,
719
  inputs=[agent_name_box, prompt_box, in_coll_check],
 
725
  outputs=[active_badge_html],
726
  )
727
 
 
728
  def _submit(msg, hist):
729
  if not msg.strip():
730
  return gr.update(), hist
 
753
  outputs=[chatbot],
754
  )
755
 
 
756
  clear_btn.click(fn=lambda: [], outputs=[chatbot])
757
 
 
758
  agent_load_signal.change(
759
  fn=_load_signal,
760
  inputs=[agent_load_signal],
 
765
  outputs=[active_badge_html],
766
  )
767
 
 
768
  hidden_refresh_btn.click(
769
  fn=lambda: (_render_collection(), _render_quick_list()),
770
  outputs=[collection_out, quick_list_html],