added on local at 2026-07-01 12:34:59
| 1 | <style> | |
| 2 | .kb-card { background:var(--col-bg-2); border:1px solid var(--col-border); border-radius:10px; padding:18px; margin-bottom:18px; } | |
| 3 | .kb-form { display:grid; grid-template-columns:1fr 1fr; gap:12px; } | |
| 4 | .kb-form .full { grid-column:1/-1; } | |
| 5 | .kb-form label { display:block; font-size:11px; color:var(--col-text-muted); margin-bottom:3px; text-transform:uppercase; letter-spacing:.5px; font-weight:600; } | |
| 6 | .kb-form input, .kb-form textarea, .kb-form select { width:100%; background:var(--col-bg-1); border:1px solid var(--col-border); color:var(--col-text); padding:7px 10px; border-radius:6px; font-size:13px; font-family:inherit; } | |
| 7 | .kb-form textarea { min-height:160px; } | |
| 8 | .kb-btn { padding:8px 14px; border-radius:7px; font-size:12px; font-weight:600; cursor:pointer; border:1px solid var(--col-border); background:transparent; color:var(--col-text); } | |
| 9 | .kb-btn-primary { background:var(--col-brand-1); color:#fff; border-color:var(--col-brand-2); } | |
| 10 | .kb-btn-danger { color:#ef4444; border-color:rgba(239,68,68,0.4); } | |
| 11 | .kb-flash { padding:10px 14px; border-radius:7px; margin-bottom:14px; font-size:13px; } | |
| 12 | .kb-flash.ok { background:rgba(110,231,167,0.10); border:1px solid rgba(110,231,167,0.4); color:#6ee7a7; } | |
| 13 | .kb-flash.err { background:rgba(239,68,68,0.10); border:1px solid rgba(239,68,68,0.4); color:#ef4444; } | |
| 14 | .kb-row { display:flex; gap:10px; padding:10px; border-bottom:1px solid var(--col-border); align-items:center; font-size:13px; } | |
| 15 | .kb-row .meta { color:var(--col-text-muted); font-size:11px; } | |
| 16 | .kb-actions { margin-top:14px; display:flex; gap:8px; flex-wrap:wrap; } | |
| 17 | .kb-tabs { display:flex; gap:8px; margin-bottom:18px; } | |
| 18 | .kb-tab { padding:8px 14px; background:transparent; border:1px solid var(--col-border); border-radius:7px; cursor:pointer; color:var(--col-text-muted); text-decoration:none; font-size:13px; } | |
| 19 | .kb-tab.active { background:rgba(90,169,255,0.10); border-color:#5aa9ff; color:#5aa9ff; } | |
| 20 | @media (max-width:640px) { .kb-form { grid-template-columns:1fr; } } | |
| 21 | </style> | |
| 22 | ||
| 23 | <div class="page-header"> | |
| 24 | <div> | |
| 25 | <h1>Knowledge Base</h1> | |
| 26 | <p class="subtitle">Internal documentation + customer-facing help articles + canned responses for fast replies.</p> | |
| 27 | </div> | |
| 28 | </div> | |
| 29 | ||
| 30 | [if:$has_flash]<div class="kb-flash $flash_kind">$flash_msg</div>[/if] | |
| 31 | [if:$schema_missing]<div class="kb-flash err">Run <code>_kb_migration.sql</code> to enable this page.</div>[/if] | |
| 32 | ||
| 33 | [if:$schema_ready] | |
| 34 | ||
| 35 | <div class="kb-tabs"> | |
| 36 | <a href="#articles" class="kb-tab active" onclick="kbTab('articles', this)">Articles</a> | |
| 37 | <a href="#canned" class="kb-tab" onclick="kbTab('canned', this)">Canned Responses</a> | |
| 38 | </div> | |
| 39 | ||
| 40 | <div id="kb-articles"> | |
| 41 | [if:$has_articles] | |
| 42 | <div class="kb-card"> | |
| 43 | <h3>Articles</h3> | |
| 44 | [loop:@articles] | |
| 45 | <div class="kb-row"> | |
| 46 | <div style="flex:1"> | |
| 47 | <strong>$loop1.title</strong> [if:$loop1.is_public]<span style="font-size:10px;background:rgba(110,231,167,0.15);color:#6ee7a7;padding:2px 6px;border-radius:8px;margin-left:6px">PUBLIC</span>[/if][if:!$loop1.is_active]<span style="font-size:10px;background:rgba(255,255,255,0.06);color:var(--col-text-muted);padding:2px 6px;border-radius:8px;margin-left:6px">DRAFT</span>[/if] | |
| 48 | <div class="meta">[if:$loop1.category]$loop1.category · [/if]$loop1.view_count views · updated $loop1.updated_at</div> | |
| 49 | </div> | |
| 50 | <form method="POST" style="display:inline" onsubmit="return confirm('Delete this article?');"> | |
| 51 | <input type="hidden" name="act" value="delete_article"> | |
| 52 | <input type="hidden" name="id" value="$loop1.id"> | |
| 53 | <button class="kb-btn kb-btn-danger">Delete</button> | |
| 54 | </form> | |
| 55 | </div> | |
| 56 | [/loop] | |
| 57 | </div> | |
| 58 | [/if] | |
| 59 | ||
| 60 | [if:$is_admin] | |
| 61 | <form method="POST" action="/kb.cgi" class="kb-card" style="border:1px dashed var(--col-border)"> | |
| 62 | <input type="hidden" name="act" value="save_article"> | |
| 63 | <h3>New article</h3> | |
| 64 | <div class="kb-form"> | |
| 65 | <div class="full"><label>Title</label><input type="text" name="title" required maxlength="280"></div> | |
| 66 | <div><label>Category</label><input type="text" name="category" maxlength="120" placeholder="Getting started"></div> | |
| 67 | <div><label>Tags (comma-separated)</label><input type="text" name="tags" maxlength="500"></div> | |
| 68 | <div class="full"><label>Body (markdown OK)</label><textarea name="body" required></textarea></div> | |
| 69 | <div><label><input type="checkbox" name="is_public" value="1"> Public (visible in customer portal)</label></div> | |
| 70 | <div><label><input type="checkbox" name="is_active" value="1" checked> Active</label></div> | |
| 71 | </div> | |
| 72 | <div class="kb-actions"><button class="kb-btn kb-btn-primary">Save article</button></div> | |
| 73 | </form> | |
| 74 | [/if] | |
| 75 | </div> | |
| 76 | ||
| 77 | <div id="kb-canned" style="display:none"> | |
| 78 | [if:$has_canned] | |
| 79 | <div class="kb-card"> | |
| 80 | <h3>Canned responses</h3> | |
| 81 | [loop:@canned] | |
| 82 | <div class="kb-row"> | |
| 83 | <div style="flex:1"> | |
| 84 | <strong>$loop1.name</strong> | |
| 85 | [if:$loop1.shortcode]<code style="font-size:11px;background:var(--col-bg-1);padding:1px 6px;border-radius:5px;margin-left:6px">$loop1.shortcode</code>[/if] | |
| 86 | [if:!$loop1.is_active]<span style="font-size:10px;color:var(--col-text-muted);margin-left:6px">disabled</span>[/if] | |
| 87 | <div class="meta">used $loop1.use_count times</div> | |
| 88 | </div> | |
| 89 | <form method="POST" style="display:inline" onsubmit="return confirm('Delete this response?');"> | |
| 90 | <input type="hidden" name="act" value="delete_canned"> | |
| 91 | <input type="hidden" name="id" value="$loop1.id"> | |
| 92 | <button class="kb-btn kb-btn-danger">Delete</button> | |
| 93 | </form> | |
| 94 | </div> | |
| 95 | [/loop] | |
| 96 | </div> | |
| 97 | [/if] | |
| 98 | ||
| 99 | [if:$is_admin] | |
| 100 | <form method="POST" action="/kb.cgi" class="kb-card" style="border:1px dashed var(--col-border)"> | |
| 101 | <input type="hidden" name="act" value="save_canned"> | |
| 102 | <h3>New canned response</h3> | |
| 103 | <div class="kb-form"> | |
| 104 | <div><label>Name</label><input type="text" name="name" required maxlength="120" placeholder="Thanks for your patience"></div> | |
| 105 | <div><label>Shortcode (optional)</label><input type="text" name="shortcode" maxlength="40" placeholder="/thanks"></div> | |
| 106 | <div class="full"><label>Body</label><textarea name="body" required></textarea></div> | |
| 107 | <div><label><input type="checkbox" name="is_active" value="1" checked> Active</label></div> | |
| 108 | </div> | |
| 109 | <div class="kb-actions"><button class="kb-btn kb-btn-primary">Save response</button></div> | |
| 110 | </form> | |
| 111 | [/if] | |
| 112 | </div> | |
| 113 | ||
| 114 | <script> | |
| 115 | function kbTab(which, el) { | |
| 116 | document.getElementById('kb-articles').style.display = (which === 'articles') ? '' : 'none'; | |
| 117 | document.getElementById('kb-canned').style.display = (which === 'canned') ? '' : 'none'; | |
| 118 | document.querySelectorAll('.kb-tab').forEach(t => t.classList.remove('active')); | |
| 119 | el.classList.add('active'); | |
| 120 | return false; | |
| 121 | } | |
| 122 | </script> | |
| 123 | ||
| 124 | [/if] |