dashboard.html 46 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913
  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>GH-Hub</title>
  7. <style>
  8. *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
  9. :root {
  10. --bg: var(--vscode-editor-background, #1e1e1e);
  11. --bg2: var(--vscode-sideBar-background, #252526);
  12. --bg3: var(--vscode-input-background, #3c3c3c);
  13. --border: var(--vscode-panel-border, #474747);
  14. --text: var(--vscode-editor-foreground, #cccccc);
  15. --text2: var(--vscode-descriptionForeground, #858585);
  16. --accent: var(--vscode-textLink-foreground, #3794ff);
  17. --green: #3fb950; --yellow: #d29922; --red: #f85149; --purple: #a371f7;
  18. --font: var(--vscode-font-family, system-ui);
  19. --mono: var(--vscode-editor-font-family, monospace);
  20. --radius: 4px;
  21. }
  22. body { font-family: var(--font); background: var(--bg); color: var(--text); font-size: 13px; line-height: 1.5; height: 100vh; display: flex; flex-direction: column; }
  23. a { color: var(--accent); text-decoration: none; }
  24. a:hover { text-decoration: underline; }
  25. button {
  26. font-family: var(--font); font-size: 12px; padding: 4px 12px;
  27. border: 1px solid var(--border); border-radius: var(--radius);
  28. background: var(--bg3); color: var(--text); cursor: pointer;
  29. }
  30. button:hover { opacity: 0.85; }
  31. button.primary { background: #238636; border-color: #2ea043; color: #fff; }
  32. button.danger { background: #da3633; border-color: #f85149; color: #fff; }
  33. button.ai { background: #1f1a3e; border-color: var(--purple); color: var(--purple); }
  34. button:disabled { opacity: 0.4; cursor: default; }
  35. input, textarea, select {
  36. font-family: var(--font); font-size: 12px; padding: 4px 8px;
  37. border: 1px solid var(--border); border-radius: var(--radius);
  38. background: var(--bg3); color: var(--text); width: 100%;
  39. }
  40. textarea { resize: vertical; min-height: 80px; font-family: var(--mono); font-size: 12px; }
  41. /* ─── Header ──────────────────────────────── */
  42. .header {
  43. display: flex; align-items: center; gap: 10px; padding: 8px 14px;
  44. background: var(--bg2); border-bottom: 1px solid var(--border);
  45. }
  46. .header h1 { font-size: 15px; font-weight: 600; }
  47. .header h1 b { color: var(--accent); }
  48. .header .user { color: var(--text2); font-size: 12px; }
  49. .header .spacer { flex: 1; }
  50. .badge {
  51. background: var(--accent); color: #000; font-size: 10px; font-weight: 700;
  52. padding: 1px 7px; border-radius: 8px; cursor: pointer; min-width: 18px; text-align: center;
  53. }
  54. .badge.zero { background: var(--bg3); color: var(--text2); }
  55. .dot { width: 7px; height: 7px; border-radius: 50%; display: inline-block; }
  56. .dot.ok { background: var(--green); }
  57. .dot.err { background: var(--red); }
  58. /* ─── Tabs ────────────────────────────────── */
  59. .tabs {
  60. display: flex; background: var(--bg2); border-bottom: 1px solid var(--border); padding: 0 14px;
  61. }
  62. .tab {
  63. padding: 7px 14px; font-size: 12px; color: var(--text2); cursor: pointer;
  64. border-bottom: 2px solid transparent;
  65. }
  66. .tab:hover { color: var(--text); }
  67. .tab.active { color: var(--text); border-bottom-color: var(--accent); }
  68. /* ─── Panels ──────────────────────────────── */
  69. .content { flex: 1; overflow: hidden; }
  70. .panel { display: none; height: 100%; overflow-y: auto; padding: 14px; }
  71. .panel.active { display: flex; flex-direction: column; }
  72. /* ─── Repos ───────────────────────────────── */
  73. .toolbar { display: flex; gap: 8px; align-items: center; margin-bottom: 10px; }
  74. .toolbar h2 { font-size: 14px; flex: 1; }
  75. .repo-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 8px; flex: 1; overflow-y: auto; }
  76. .repo-card {
  77. background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius);
  78. padding: 12px; cursor: pointer;
  79. }
  80. .repo-card:hover { border-color: var(--accent); }
  81. .repo-card.sel { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
  82. .repo-card h3 { font-size: 13px; color: var(--accent); }
  83. .repo-card .desc { color: var(--text2); font-size: 11px; margin: 2px 0 6px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  84. .repo-meta { display: flex; gap: 12px; font-size: 11px; color: var(--text2); }
  85. .lang-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; margin-right: 2px; vertical-align: middle; }
  86. .tag-private { color: var(--yellow); border: 1px solid var(--yellow); padding: 0 4px; border-radius: 3px; font-size: 9px; }
  87. /* ─── Split View ──────────────────────────── */
  88. .split { display: flex; height: 100%; gap: 0; flex: 1; overflow: hidden; }
  89. .list-pane { width: 320px; min-width: 240px; border-right: 1px solid var(--border); overflow-y: auto; display: flex; flex-direction: column; }
  90. .detail-pane { flex: 1; overflow-y: auto; padding: 14px; }
  91. .filter-bar { padding: 6px 10px; border-bottom: 1px solid var(--border); display: flex; gap: 6px; align-items: center; }
  92. .filter-bar .label { font-size: 11px; color: var(--accent); font-weight: 600; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  93. .filter-bar select { width: auto; font-size: 11px; }
  94. .item-card { padding: 8px 12px; border-bottom: 1px solid var(--border); cursor: pointer; }
  95. .item-card:hover { background: var(--bg3); }
  96. .item-card.sel { background: var(--bg3); border-left: 3px solid var(--accent); }
  97. .item-card .num { color: var(--text2); font-size: 11px; }
  98. .item-card .title { font-size: 12px; font-weight: 500; }
  99. .item-card .meta { font-size: 10px; color: var(--text2); display: flex; gap: 8px; margin-top: 2px; }
  100. .label-tag { display: inline-block; font-size: 9px; font-weight: 600; padding: 0 5px; border-radius: 8px; margin-right: 3px; }
  101. /* ─── Detail ──────────────────────────────── */
  102. .detail-header h2 { font-size: 15px; margin-bottom: 2px; }
  103. .detail-header .meta { font-size: 11px; color: var(--text2); margin-bottom: 10px; }
  104. .detail-body {
  105. background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius);
  106. padding: 12px; margin-bottom: 12px; font-size: 12px; line-height: 1.6; word-wrap: break-word;
  107. }
  108. .detail-body pre { background: var(--bg); padding: 8px; border-radius: 3px; overflow-x: auto; font-family: var(--mono); font-size: 11px; margin: 6px 0; }
  109. .detail-body code { font-family: var(--mono); font-size: 11px; background: var(--bg3); padding: 1px 3px; border-radius: 2px; }
  110. .detail-body pre code { background: none; padding: 0; }
  111. .comment { background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius); padding: 10px 12px; margin-bottom: 6px; }
  112. .comment .author { font-weight: 600; font-size: 12px; }
  113. .comment .time { color: var(--text2); font-size: 10px; margin-left: 6px; }
  114. .comment .body { margin-top: 6px; font-size: 12px; line-height: 1.5; }
  115. .reply-area { margin-top: 12px; }
  116. .reply-actions { display: flex; gap: 6px; margin-top: 6px; align-items: center; }
  117. .confidence { font-size: 10px; padding: 1px 6px; border-radius: 8px; font-weight: 600; }
  118. .confidence.high { background: #0d3321; color: var(--green); }
  119. .confidence.medium { background: #3d2e00; color: var(--yellow); }
  120. .confidence.low { background: #3d1214; color: var(--red); }
  121. .empty { display: flex; align-items: center; justify-content: center; height: 100%; color: var(--text2); flex-direction: column; gap: 6px; padding: 30px; text-align: center; }
  122. /* ─── Notifications ───────────────────────── */
  123. .notif-item {
  124. display: flex; gap: 10px; padding: 8px 12px; background: var(--bg2);
  125. border: 1px solid var(--border); border-radius: var(--radius); margin-bottom: 6px; align-items: flex-start;
  126. }
  127. .notif-item .icon { font-size: 14px; margin-top: 2px; }
  128. .notif-item .info { flex: 1; }
  129. .notif-item .repo { font-size: 10px; color: var(--text2); }
  130. .notif-item .title { font-size: 12px; }
  131. .notif-item .reason { font-size: 10px; color: var(--text2); }
  132. /* ─── Push ────────────────────────────────── */
  133. .push-panel { max-width: 600px; }
  134. .field { margin-bottom: 10px; }
  135. .field label { font-size: 11px; color: var(--text2); margin-bottom: 3px; display: block; }
  136. .field-row { display: flex; gap: 6px; }
  137. .git-info { margin: 10px 0; }
  138. .git-info .branch { font-size: 13px; font-weight: 600; color: var(--green); }
  139. .git-info .counts { font-size: 11px; color: var(--text2); margin-top: 2px; }
  140. .git-info pre { background: var(--bg2); padding: 8px; border-radius: var(--radius); font-family: var(--mono); font-size: 11px; margin-top: 6px; max-height: 150px; overflow-y: auto; }
  141. .log { background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius); padding: 8px; font-family: var(--mono); font-size: 11px; min-height: 60px; white-space: pre-wrap; color: var(--text2); margin-top: 10px; }
  142. .log .ok { color: var(--green); }
  143. .log .err { color: var(--red); }
  144. /* ─── Publish Tab ─────────────────────────── */
  145. .pub-top { display: flex; gap: 8px; align-items: center; margin-bottom: 8px; flex-shrink: 0; }
  146. .pub-top h2 { font-size: 14px; flex: 1; }
  147. .pub-settings { display: flex; gap: 6px; align-items: center; flex-shrink: 0; margin-bottom: 8px; }
  148. .pub-settings select, .pub-settings input { width: auto; font-size: 11px; }
  149. .pub-settings input { max-width: 180px; }
  150. .pub-split { display: flex; flex: 1; gap: 0; overflow: hidden; border: 1px solid var(--border); border-radius: var(--radius); }
  151. .pub-editor { flex: 1; display: flex; flex-direction: column; border-right: 1px solid var(--border); }
  152. .pub-toolbar { display: flex; gap: 2px; padding: 4px 6px; background: var(--bg2); border-bottom: 1px solid var(--border); flex-wrap: wrap; }
  153. .pub-toolbar button { font-size: 11px; padding: 2px 7px; min-width: 26px; border: none; background: transparent; color: var(--text2); }
  154. .pub-toolbar button:hover { background: var(--bg3); color: var(--text); border-radius: 3px; }
  155. .pub-toolbar .sep { width: 1px; background: var(--border); margin: 2px 4px; }
  156. .pub-editor textarea { flex: 1; border: none; border-radius: 0; resize: none; padding: 10px; font-family: var(--mono); font-size: 12px; line-height: 1.6; background: var(--bg); }
  157. .pub-preview { flex: 1; overflow-y: auto; padding: 16px; background: var(--bg); font-size: 13px; line-height: 1.7; }
  158. .pub-preview h1 { font-size: 22px; border-bottom: 1px solid var(--border); padding-bottom: 6px; margin: 16px 0 10px; }
  159. .pub-preview h2 { font-size: 18px; border-bottom: 1px solid var(--border); padding-bottom: 4px; margin: 14px 0 8px; }
  160. .pub-preview h3 { font-size: 15px; margin: 12px 0 6px; }
  161. .pub-preview h4 { font-size: 13px; margin: 10px 0 4px; }
  162. .pub-preview p { margin: 6px 0; }
  163. .pub-preview img { max-width: 100%; border-radius: 4px; margin: 8px 0; }
  164. .pub-preview pre { background: var(--bg2); padding: 10px; border-radius: 4px; overflow-x: auto; font-family: var(--mono); font-size: 11px; margin: 8px 0; }
  165. .pub-preview code { font-family: var(--mono); font-size: 11px; background: var(--bg3); padding: 1px 4px; border-radius: 2px; }
  166. .pub-preview pre code { background: none; padding: 0; }
  167. .pub-preview blockquote { border-left: 3px solid var(--accent); padding: 4px 12px; margin: 8px 0; color: var(--text2); }
  168. .pub-preview table { border-collapse: collapse; margin: 8px 0; width: 100%; }
  169. .pub-preview th, .pub-preview td { border: 1px solid var(--border); padding: 4px 8px; font-size: 12px; text-align: left; }
  170. .pub-preview th { background: var(--bg2); }
  171. .pub-preview ul, .pub-preview ol { padding-left: 20px; margin: 6px 0; }
  172. .pub-preview li { margin: 2px 0; }
  173. .pub-preview hr { border: none; border-top: 1px solid var(--border); margin: 12px 0; }
  174. .pub-preview a { color: var(--accent); }
  175. .img-upload-area { padding: 6px 10px; background: var(--bg2); border-top: 1px solid var(--border); font-size: 11px; color: var(--text2); display: flex; align-items: center; gap: 6px; }
  176. .img-upload-area button { font-size: 11px; padding: 2px 8px; }
  177. /* ─── Modal ───────────────────────────────── */
  178. .modal-bg { position: fixed; inset: 0; background: rgba(0,0,0,0.5); display: none; align-items: center; justify-content: center; z-index: 99; }
  179. .modal-bg.show { display: flex; }
  180. .modal { background: var(--bg2); border: 1px solid var(--border); border-radius: 6px; padding: 18px; width: 380px; }
  181. .modal h3 { margin-bottom: 12px; font-size: 14px; }
  182. .modal .actions { display: flex; gap: 6px; justify-content: flex-end; margin-top: 14px; }
  183. .spinner { display: inline-block; width: 12px; height: 12px; border: 2px solid var(--border); border-top-color: var(--accent); border-radius: 50%; animation: spin .5s linear infinite; }
  184. @keyframes spin { to { transform: rotate(360deg); } }
  185. .loading { display: flex; align-items: center; gap: 6px; padding: 20px; color: var(--text2); }
  186. </style>
  187. </head>
  188. <body>
  189. <div class="header">
  190. <h1>GH-<b>Hub</b></h1>
  191. <span class="dot" id="hDot"></span>
  192. <span class="user" id="hUser">...</span>
  193. <span class="spacer"></span>
  194. <span class="badge zero" id="hBadge" onclick="switchTab('notifications')">0</span>
  195. </div>
  196. <div class="tabs">
  197. <div class="tab active" data-t="repos" onclick="switchTab('repos')">Repos</div>
  198. <div class="tab" data-t="issues" onclick="switchTab('issues')">Issues</div>
  199. <div class="tab" data-t="pulls" onclick="switchTab('pulls')">PRs</div>
  200. <div class="tab" data-t="notifications" onclick="switchTab('notifications')">Notifications</div>
  201. <div class="tab" data-t="push" onclick="switchTab('push')">Push</div>
  202. <div class="tab" data-t="publish" onclick="switchTab('publish')">Publish</div>
  203. </div>
  204. <div class="content">
  205. <!-- ═══ Repos ═══ -->
  206. <div class="panel active" id="p-repos">
  207. <div class="toolbar">
  208. <h2>Repositories</h2>
  209. <button onclick="loadRepos()">Refresh</button>
  210. <button class="primary" onclick="showModal('createRepoModal')">+ New</button>
  211. </div>
  212. <div class="repo-grid" id="repoGrid"><div class="loading"><span class="spinner"></span> Loading...</div></div>
  213. </div>
  214. <!-- ═══ Issues ═══ -->
  215. <div class="panel" id="p-issues" style="padding:0;">
  216. <div class="split">
  217. <div class="list-pane">
  218. <div class="filter-bar">
  219. <span class="label" id="iRepoLabel">Select a repo</span>
  220. <select id="iState" onchange="loadIssues()"><option value="open">Open</option><option value="closed">Closed</option><option value="all">All</option></select>
  221. </div>
  222. <div id="iList" style="flex:1;overflow-y:auto;"><div class="empty">Click a repo first</div></div>
  223. </div>
  224. <div class="detail-pane" id="iDetail"><div class="empty">Select an issue</div></div>
  225. </div>
  226. </div>
  227. <!-- ═══ PRs ═══ -->
  228. <div class="panel" id="p-pulls" style="padding:0;">
  229. <div class="split">
  230. <div class="list-pane">
  231. <div class="filter-bar">
  232. <span class="label" id="pRepoLabel">Select a repo</span>
  233. <select id="pState" onchange="loadPRs()"><option value="open">Open</option><option value="closed">Closed</option><option value="all">All</option></select>
  234. </div>
  235. <div id="pList" style="flex:1;overflow-y:auto;"><div class="empty">Click a repo first</div></div>
  236. </div>
  237. <div class="detail-pane" id="pDetail"><div class="empty">Select a PR</div></div>
  238. </div>
  239. </div>
  240. <!-- ═══ Notifications ═══ -->
  241. <div class="panel" id="p-notifications">
  242. <div class="toolbar">
  243. <h2>Notifications</h2>
  244. <button onclick="loadNotifs()">Refresh</button>
  245. <button onclick="markAllRead()">Mark All Read</button>
  246. </div>
  247. <div id="notifList" style="flex:1;overflow-y:auto;max-width:650px;"><div class="loading"><span class="spinner"></span> Loading...</div></div>
  248. </div>
  249. <!-- ═══ Push ═══ -->
  250. <div class="panel" id="p-push">
  251. <h2 style="font-size:14px;margin-bottom:10px;">Quick Push</h2>
  252. <div class="push-panel">
  253. <div class="field">
  254. <label>Local Directory</label>
  255. <div class="field-row">
  256. <input id="pushDir" placeholder="/path/to/project">
  257. <button onclick="pickFolder()" style="white-space:nowrap;">Browse</button>
  258. <button onclick="useWorkspace()" style="white-space:nowrap;">Workspace</button>
  259. </div>
  260. </div>
  261. <button onclick="checkGit()">Check Status</button>
  262. <div class="git-info" id="gitInfo"></div>
  263. <div style="display:flex;gap:6px;margin-top:8px;">
  264. <button class="primary" id="pushBtn" onclick="doPush()" disabled>Push</button>
  265. <button onclick="doQuickSetup()">Quick Setup + Push</button>
  266. </div>
  267. <div class="log" id="pushLog" style="display:none;"></div>
  268. </div>
  269. </div>
  270. <!-- ═══ Publish ═══ -->
  271. <div class="panel" id="p-publish">
  272. <div class="pub-top">
  273. <h2>Publish to GitHub</h2>
  274. <span id="pubResult" style="font-size:12px;"></span>
  275. </div>
  276. <div class="pub-settings">
  277. <select id="pubRepo" onchange="onRepoChange()"><option value="">Target Repo</option></select>
  278. <input id="pubPath" value="README.md" style="max-width:140px;" placeholder="Path (e.g. README.md)">
  279. <input id="pubMsg" value="Update README" style="max-width:200px;" placeholder="Commit message">
  280. <button onclick="fetchExistingReadme()">Load Existing</button>
  281. <button class="primary" onclick="doPublish()" id="pubBtn">Publish</button>
  282. </div>
  283. <div class="pub-split">
  284. <div class="pub-editor">
  285. <div class="pub-toolbar">
  286. <button onclick="tbWrap('**','**')" title="Bold"><b>B</b></button>
  287. <button onclick="tbWrap('*','*')" title="Italic"><i>I</i></button>
  288. <button onclick="tbWrap('~~','~~')" title="Strikethrough"><s>S</s></button>
  289. <div class="sep"></div>
  290. <button onclick="tbPrefix('# ')" title="H1">H1</button>
  291. <button onclick="tbPrefix('## ')" title="H2">H2</button>
  292. <button onclick="tbPrefix('### ')" title="H3">H3</button>
  293. <div class="sep"></div>
  294. <button onclick="tbPrefix('- ')" title="Bullet list">&#8226;</button>
  295. <button onclick="tbPrefix('1. ')" title="Numbered list">1.</button>
  296. <button onclick="tbPrefix('> ')" title="Blockquote">&gt;</button>
  297. <div class="sep"></div>
  298. <button onclick="tbLink()" title="Insert link">Link</button>
  299. <button onclick="tbImage()" title="Insert image from URL">Img</button>
  300. <button onclick="tbUploadImage()" title="Upload image to repo">Upload</button>
  301. <div class="sep"></div>
  302. <button onclick="tbWrap('`','`')" title="Inline code">code</button>
  303. <button onclick="tbCodeBlock()" title="Code block">```</button>
  304. <button onclick="tbPrefix('---')" title="Horizontal rule">HR</button>
  305. <div class="sep"></div>
  306. <button onclick="tbTable()" title="Insert table">Table</button>
  307. </div>
  308. <textarea id="pubEditor" placeholder="Write your markdown here..." oninput="updatePreview()"></textarea>
  309. <div class="img-upload-area">
  310. <span>Images:</span>
  311. <button onclick="tbUploadImage()">Browse & Upload</button>
  312. <span id="imgStatus" style="color:var(--text2);font-size:10px;"></span>
  313. </div>
  314. </div>
  315. <div class="pub-preview" id="pubPreview">
  316. <div class="empty" style="height:auto;">Live preview will appear here</div>
  317. </div>
  318. </div>
  319. </div>
  320. </div>
  321. <!-- ═══ Create Repo Modal ═══ -->
  322. <div class="modal-bg" id="createRepoModal">
  323. <div class="modal">
  324. <h3>New Repository</h3>
  325. <div class="field"><label>Name</label><input id="nrName" placeholder="my-project"></div>
  326. <div class="field"><label>Description</label><input id="nrDesc" placeholder="optional"></div>
  327. <div class="field"><label><input type="checkbox" id="nrPrivate" style="width:auto;margin-right:4px;">Private</label></div>
  328. <div class="actions">
  329. <button onclick="hideModal('createRepoModal')">Cancel</button>
  330. <button class="primary" onclick="createRepo()">Create</button>
  331. </div>
  332. </div>
  333. </div>
  334. <!-- ═══ Quick Setup Modal ═══ -->
  335. <div class="modal-bg" id="quickSetupModal">
  336. <div class="modal">
  337. <h3>Quick Setup + Push</h3>
  338. <p style="font-size:11px;color:var(--text2);margin-bottom:10px;">Create repo, init git, commit all, and push in one step.</p>
  339. <div class="field"><label>Repo Name (owner/name)</label><input id="qsName" placeholder="GeneralNoCodeDP/my-project"></div>
  340. <div class="field"><label>Description</label><input id="qsDesc"></div>
  341. <div class="field"><label>Commit Message</label><input id="qsMsg" value="Initial commit"></div>
  342. <div class="field"><label><input type="checkbox" id="qsPrivate" style="width:auto;margin-right:4px;">Private</label></div>
  343. <div class="actions">
  344. <button onclick="hideModal('quickSetupModal')">Cancel</button>
  345. <button class="primary" onclick="execQuickSetup()">Go</button>
  346. </div>
  347. </div>
  348. </div>
  349. <script>
  350. const vscode = acquireVsCodeApi();
  351. let S = { repo: null, repos: [], aiReady: false, pubContent: '', pendingCallbacks: {} };
  352. let msgId = 0;
  353. // ─── Bridge ──────────────────────────────────
  354. function send(command, args = {}) {
  355. return new Promise((resolve, reject) => {
  356. const id = ++msgId;
  357. S.pendingCallbacks[id] = { resolve, reject };
  358. vscode.postMessage({ id, command, args });
  359. });
  360. }
  361. window.addEventListener('message', e => {
  362. const msg = e.data;
  363. // Async response
  364. if (msg.id && S.pendingCallbacks[msg.id]) {
  365. const cb = S.pendingCallbacks[msg.id];
  366. delete S.pendingCallbacks[msg.id];
  367. if (msg.error) cb.reject(new Error(msg.error));
  368. else cb.resolve(msg.data);
  369. return;
  370. }
  371. // Push events
  372. if (msg.command === 'notifUpdate') {
  373. const n = Array.isArray(msg.data) ? msg.data.length : 0;
  374. setBadge(n);
  375. }
  376. });
  377. // ─── Tabs ────────────────────────────────────
  378. function switchTab(t) {
  379. document.querySelectorAll('.tab').forEach(el => el.classList.toggle('active', el.dataset.t === t));
  380. document.querySelectorAll('.panel').forEach(el => el.classList.toggle('active', el.id === `p-${t}`));
  381. if (t === 'notifications') loadNotifs();
  382. if (t === 'publish') populateRepoSelect();
  383. }
  384. // ─── Init ────────────────────────────────────
  385. async function init() {
  386. try {
  387. const s = await send('status');
  388. document.getElementById('hDot').className = s.ok ? 'dot ok' : 'dot err';
  389. document.getElementById('hUser').textContent = s.ok ? s.user : 'Not authenticated';
  390. S.aiReady = s.aiReady;
  391. } catch (e) {
  392. document.getElementById('hDot').className = 'dot err';
  393. document.getElementById('hUser').textContent = 'Error';
  394. }
  395. loadRepos();
  396. }
  397. function setBadge(n) {
  398. const b = document.getElementById('hBadge');
  399. b.textContent = n;
  400. b.className = n > 0 ? 'badge' : 'badge zero';
  401. }
  402. function showModal(id) { document.getElementById(id).classList.add('show'); }
  403. function hideModal(id) { document.getElementById(id).classList.remove('show'); }
  404. // ─── Repos ───────────────────────────────────
  405. const LC = { JavaScript:'#f1e05a', TypeScript:'#3178c6', Python:'#3572a5', Go:'#00add8', Rust:'#dea584', HTML:'#e34c26', CSS:'#563d7c', Shell:'#89e051', Vue:'#41b883' };
  406. async function loadRepos() {
  407. const g = document.getElementById('repoGrid');
  408. g.innerHTML = '<div class="loading"><span class="spinner"></span> Loading...</div>';
  409. try {
  410. const repos = await send('listRepos');
  411. S.repos = repos || [];
  412. if (!repos?.length) { g.innerHTML = '<div class="empty">No repos</div>'; return; }
  413. g.innerHTML = repos.map(r => {
  414. const nwo = r.owner.login + '/' + r.name;
  415. const l = r.primaryLanguage?.name || '';
  416. const c = LC[l] || '#858585';
  417. return `<div class="repo-card${S.repo===nwo?' sel':''}" onclick="selectRepo('${nwo}')" data-r="${nwo}">
  418. <h3>${r.name}</h3>
  419. <div class="desc">${esc(r.description||'No description')}</div>
  420. <div class="repo-meta">
  421. ${l?`<span><span class="lang-dot" style="background:${c}"></span>${l}</span>`:''}
  422. <span>&#9733;${r.stargazerCount}</span>
  423. <span>${timeAgo(r.updatedAt)}</span>
  424. ${r.isPrivate?'<span class="tag-private">Private</span>':''}
  425. </div></div>`;
  426. }).join('');
  427. } catch (e) { g.innerHTML = `<div class="empty">Error: ${esc(e.message)}</div>`; }
  428. }
  429. function selectRepo(nwo) {
  430. S.repo = nwo;
  431. document.querySelectorAll('.repo-card').forEach(c => c.classList.toggle('sel', c.dataset.r === nwo));
  432. document.getElementById('iRepoLabel').textContent = nwo;
  433. document.getElementById('pRepoLabel').textContent = nwo;
  434. loadIssues(); loadPRs();
  435. }
  436. async function createRepo() {
  437. const name = document.getElementById('nrName').value.trim();
  438. if (!name) return;
  439. try {
  440. await send('createRepo', { name, description: document.getElementById('nrDesc').value.trim(), isPrivate: document.getElementById('nrPrivate').checked });
  441. hideModal('createRepoModal');
  442. document.getElementById('nrName').value = '';
  443. document.getElementById('nrDesc').value = '';
  444. loadRepos();
  445. } catch (e) { alert('Error: ' + e.message); }
  446. }
  447. // ─── Issues ──────────────────────────────────
  448. async function loadIssues() {
  449. if (!S.repo) return;
  450. const el = document.getElementById('iList');
  451. el.innerHTML = '<div class="loading"><span class="spinner"></span></div>';
  452. try {
  453. const items = await send('listIssues', { repo: S.repo, state: document.getElementById('iState').value });
  454. if (!items?.length) { el.innerHTML = '<div class="empty">No issues</div>'; return; }
  455. el.innerHTML = items.map(i => `<div class="item-card" onclick="viewIssue(${i.number})">
  456. <div class="num">#${i.number}</div>
  457. <div class="title">${esc(i.title)}</div>
  458. <div class="meta"><span>${i.author?.login||'?'}</span><span>${timeAgo(i.createdAt)}</span><span>${i.comments||0} comments</span></div>
  459. ${(i.labels||[]).map(l=>`<span class="label-tag" style="background:${l.color?'#'+l.color+'33':'var(--bg3)'};color:${l.color?'#'+l.color:'var(--text2)'}">${esc(l.name)}</span>`).join('')}
  460. </div>`).join('');
  461. } catch (e) { el.innerHTML = `<div class="empty">${esc(e.message)}</div>`; }
  462. }
  463. async function viewIssue(num) {
  464. const el = document.getElementById('iDetail');
  465. el.innerHTML = '<div class="loading"><span class="spinner"></span></div>';
  466. document.querySelectorAll('#iList .item-card').forEach(c => c.classList.toggle('sel', c.querySelector('.num')?.textContent === '#'+num));
  467. try {
  468. const d = await send('viewIssue', { repo: S.repo, number: num });
  469. const cm = d.commentList || [];
  470. el.innerHTML = `
  471. <div class="detail-header"><h2>${esc(d.title)} <span style="color:var(--text2);font-weight:400">#${d.number}</span></h2>
  472. <div class="meta"><span style="color:${d.state==='OPEN'?'var(--green)':'var(--purple)'}">${d.state}</span> &middot; ${d.author?.login||'?'} &middot; ${timeAgo(d.createdAt)}</div></div>
  473. <div class="detail-body">${renderMd(d.body||'(empty)')}</div>
  474. ${cm.length?cm.map(c=>`<div class="comment"><span class="author">${esc(c.author)}</span><span class="time">${timeAgo(c.createdAt)}</span><div class="body">${renderMd(c.body)}</div></div>`).join(''):''}
  475. <div class="reply-area"><textarea id="iReply" placeholder="Write a reply..."></textarea>
  476. <div class="reply-actions">
  477. ${S.aiReady?`<button class="ai" onclick="aiDraft('issue',${num})">AI Draft</button>`:''}<span id="iConf"></span><span style="flex:1"></span>
  478. <button class="primary" onclick="postIssueComment(${num})">Reply</button>
  479. ${d.state==='OPEN'?`<button class="danger" onclick="doCloseIssue(${num})">Close</button>`:`<button onclick="doReopenIssue(${num})">Reopen</button>`}
  480. </div></div>`;
  481. } catch (e) { el.innerHTML = `<div class="empty">${esc(e.message)}</div>`; }
  482. }
  483. async function postIssueComment(num) {
  484. const body = document.getElementById('iReply').value.trim();
  485. if (!body) return;
  486. await send('commentIssue', { repo: S.repo, number: num, body });
  487. viewIssue(num);
  488. }
  489. async function doCloseIssue(num) { await send('closeIssue', { repo: S.repo, number: num }); viewIssue(num); loadIssues(); }
  490. async function doReopenIssue(num) { await send('reopenIssue', { repo: S.repo, number: num }); viewIssue(num); loadIssues(); }
  491. // ─── PRs ─────────────────────────────────────
  492. async function loadPRs() {
  493. if (!S.repo) return;
  494. const el = document.getElementById('pList');
  495. el.innerHTML = '<div class="loading"><span class="spinner"></span></div>';
  496. try {
  497. const items = await send('listPRs', { repo: S.repo, state: document.getElementById('pState').value });
  498. if (!items?.length) { el.innerHTML = '<div class="empty">No PRs</div>'; return; }
  499. el.innerHTML = items.map(p => `<div class="item-card" onclick="viewPR(${p.number})">
  500. <div class="num">#${p.number}${p.isDraft?' <span style="color:var(--text2)">(Draft)</span>':''}</div>
  501. <div class="title">${esc(p.title)}</div>
  502. <div class="meta"><span>${p.author?.login||'?'}</span><span>${p.headRefName} &rarr; ${p.baseRefName}</span><span>${timeAgo(p.createdAt)}</span></div>
  503. </div>`).join('');
  504. } catch (e) { el.innerHTML = `<div class="empty">${esc(e.message)}</div>`; }
  505. }
  506. async function viewPR(num) {
  507. const el = document.getElementById('pDetail');
  508. el.innerHTML = '<div class="loading"><span class="spinner"></span></div>';
  509. document.querySelectorAll('#pList .item-card').forEach(c => c.classList.toggle('sel', c.querySelector('.num')?.textContent?.startsWith('#'+num)));
  510. try {
  511. const d = await send('viewPR', { repo: S.repo, number: num });
  512. const cm = d.commentList || [];
  513. const sc = d.state==='OPEN'?'var(--green)':d.state==='MERGED'?'var(--purple)':'var(--red)';
  514. el.innerHTML = `
  515. <div class="detail-header"><h2>${esc(d.title)} <span style="color:var(--text2);font-weight:400">#${d.number}</span></h2>
  516. <div class="meta"><span style="color:${sc}">${d.state}</span> &middot; ${d.author?.login||'?'} &middot; ${d.headRefName}&rarr;${d.baseRefName} &middot; +${d.additions||0} -${d.deletions||0} (${d.changedFiles||0} files)</div></div>
  517. <div class="detail-body">${renderMd(d.body||'(empty)')}</div>
  518. ${cm.length?cm.map(c=>`<div class="comment"><span class="author">${esc(c.author)}</span><span class="time">${timeAgo(c.createdAt)}</span><div class="body">${renderMd(c.body)}</div></div>`).join(''):''}
  519. <div class="reply-area"><textarea id="pReply" placeholder="Write a comment..."></textarea>
  520. <div class="reply-actions">
  521. ${S.aiReady?`<button class="ai" onclick="aiDraft('pr',${num})">AI Draft</button>`:''}<span id="pConf"></span><span style="flex:1"></span>
  522. <button class="primary" onclick="postPRComment(${num})">Comment</button>
  523. ${d.state==='OPEN'?`<button class="primary" onclick="doMergePR(${num})">Merge</button><button class="danger" onclick="doClosePR(${num})">Close</button>`:''}
  524. </div></div>`;
  525. } catch (e) { el.innerHTML = `<div class="empty">${esc(e.message)}</div>`; }
  526. }
  527. async function postPRComment(num) { const b = document.getElementById('pReply').value.trim(); if (!b) return; await send('commentPR', { repo: S.repo, number: num, body: b }); viewPR(num); }
  528. async function doMergePR(num) { await send('mergePR', { repo: S.repo, number: num, method: 'merge' }); viewPR(num); loadPRs(); }
  529. async function doClosePR(num) { await send('closePR', { repo: S.repo, number: num }); viewPR(num); loadPRs(); }
  530. // ─── AI Draft ────────────────────────────────
  531. async function aiDraft(type, num) {
  532. const tid = type==='issue'?'iReply':'pReply';
  533. const cid = type==='issue'?'iConf':'pConf';
  534. const ta = document.getElementById(tid);
  535. const conf = document.getElementById(cid);
  536. ta.value = 'Generating...'; ta.disabled = true;
  537. conf.innerHTML = '<span class="spinner"></span>';
  538. try {
  539. const detail = await send(type==='issue'?'viewIssue':'viewPR', { repo: S.repo, number: num });
  540. const res = await send('aiDraft', { type, title: detail.title, body: detail.body, comments: detail.commentList, repoName: S.repo });
  541. ta.value = res.draft;
  542. conf.innerHTML = `<span class="confidence ${res.confidence}">${res.confidence}</span>`;
  543. } catch (e) { ta.value = ''; conf.textContent = ''; alert('AI error: '+e.message); }
  544. ta.disabled = false; ta.focus();
  545. }
  546. // ─── Notifications ───────────────────────────
  547. const NI = { Issue:'<span style="color:var(--green)">&#9679;</span>', PullRequest:'<span style="color:var(--purple)">&#10542;</span>', Release:'<span style="color:var(--accent)">&#9830;</span>' };
  548. async function loadNotifs() {
  549. const el = document.getElementById('notifList');
  550. el.innerHTML = '<div class="loading"><span class="spinner"></span></div>';
  551. try {
  552. const items = await send('listNotifications');
  553. setBadge(items?.length || 0);
  554. if (!items?.length) { el.innerHTML = '<div class="empty">No unread notifications</div>'; return; }
  555. el.innerHTML = items.map(n => `<div class="notif-item">
  556. <div class="icon">${NI[n.type]||'&#9679;'}</div>
  557. <div class="info"><div class="repo">${esc(n.repo)}</div><div class="title">${esc(n.title)}</div><div class="reason">${n.reason} &middot; ${timeAgo(n.updatedAt)}</div></div>
  558. <button onclick="markRead('${n.id}',this.parentElement)">Read</button>
  559. </div>`).join('');
  560. } catch (e) { el.innerHTML = `<div class="empty">${esc(e.message)}</div>`; }
  561. }
  562. async function markRead(id, el) { await send('markRead', { id }); if (el) el.style.opacity = '0.3'; }
  563. async function markAllRead() { await send('markAllRead'); loadNotifs(); }
  564. // ─── Push ────────────────────────────────────
  565. async function pickFolder() { const p = await send('pickFolder'); if (p) document.getElementById('pushDir').value = p; }
  566. async function useWorkspace() { const p = await send('getWorkspaceDir'); if (p) document.getElementById('pushDir').value = p; }
  567. async function checkGit() {
  568. const dir = document.getElementById('pushDir').value.trim(); if (!dir) return;
  569. const el = document.getElementById('gitInfo');
  570. el.innerHTML = '<div class="loading"><span class="spinner"></span></div>';
  571. try {
  572. const s = await send('gitStatus', { dir });
  573. el.innerHTML = `<div class="branch">${esc(s.branch)}</div>
  574. <div class="counts">${s.ahead>0?`<span style="color:var(--green)">${s.ahead} ahead</span> `:''}${s.behind>0?`<span style="color:var(--red)">${s.behind} behind</span> `:''}${s.ahead===0&&s.behind===0?'Up to date':''}</div>
  575. ${s.status?`<pre>${esc(s.status)}</pre>`:'<pre style="color:var(--text2)">Clean</pre>'}`;
  576. document.getElementById('pushBtn').disabled = false;
  577. } catch (e) { el.innerHTML = `<span style="color:var(--red)">${esc(e.message)}</span>`; document.getElementById('pushBtn').disabled = true; }
  578. }
  579. async function doPush() {
  580. const dir = document.getElementById('pushDir').value.trim(); if (!dir) return;
  581. const log = document.getElementById('pushLog');
  582. log.style.display = 'block'; log.innerHTML = 'Pushing...\n';
  583. document.getElementById('pushBtn').disabled = true;
  584. try {
  585. const r = await send('gitPush', { dir });
  586. log.innerHTML += `<span class="ok">${esc(typeof r==='string'?r:r?.message||'Done')}</span>\n`;
  587. } catch (e) { log.innerHTML += `<span class="err">Error: ${esc(e.message)}</span>\n`; }
  588. document.getElementById('pushBtn').disabled = false;
  589. checkGit();
  590. }
  591. function doQuickSetup() {
  592. const dir = document.getElementById('pushDir').value.trim();
  593. if (!dir) { alert('Set directory first'); return; }
  594. const n = dir.split('/').pop();
  595. document.getElementById('qsName').value = `GeneralNoCodeDP/${n}`;
  596. showModal('quickSetupModal');
  597. }
  598. async function execQuickSetup() {
  599. const dir = document.getElementById('pushDir').value.trim();
  600. const log = document.getElementById('pushLog');
  601. log.style.display = 'block'; log.innerHTML = 'Setting up...\n';
  602. hideModal('quickSetupModal');
  603. try {
  604. const r = await send('quickSetup', {
  605. dir,
  606. createRepo: true,
  607. repoName: document.getElementById('qsName').value.trim(),
  608. description: document.getElementById('qsDesc').value.trim(),
  609. isPrivate: document.getElementById('qsPrivate').checked,
  610. commitMessage: document.getElementById('qsMsg').value.trim() || 'Initial commit'
  611. });
  612. log.innerHTML += `<span class="ok">${esc(r?.message||'Done!')}</span>\n`;
  613. } catch (e) { log.innerHTML += `<span class="err">Error: ${esc(e.message)}</span>\n`; }
  614. checkGit();
  615. }
  616. // ─── Publish ─────────────────────────────────
  617. function populateRepoSelect() {
  618. const sel = document.getElementById('pubRepo');
  619. const cur = sel.value;
  620. sel.innerHTML = '<option value="">Target Repo</option>' + S.repos.map(r => {
  621. const nwo = r.owner.login+'/'+r.name;
  622. return `<option value="${nwo}"${nwo===cur?' selected':''}>${nwo}</option>`;
  623. }).join('');
  624. }
  625. function onRepoChange() {
  626. // Auto-load existing README when repo changes
  627. }
  628. async function fetchExistingReadme() {
  629. const repo = document.getElementById('pubRepo').value;
  630. if (!repo) { alert('Select a repo first'); return; }
  631. const editor = document.getElementById('pubEditor');
  632. const res = document.getElementById('pubResult');
  633. res.innerHTML = '<span class="spinner"></span> Loading...';
  634. try {
  635. const data = await send('fetchReadme', { repo });
  636. if (data.content) {
  637. editor.value = data.content;
  638. updatePreview();
  639. res.innerHTML = '<span style="color:var(--green)">Loaded existing README</span>';
  640. } else {
  641. res.innerHTML = '<span style="color:var(--text2)">No existing README found</span>';
  642. }
  643. } catch (e) { res.innerHTML = `<span style="color:var(--red)">Error: ${esc(e.message)}</span>`; }
  644. }
  645. function updatePreview() {
  646. const md = document.getElementById('pubEditor').value;
  647. const el = document.getElementById('pubPreview');
  648. if (!md.trim()) { el.innerHTML = '<div class="empty" style="height:auto;">Live preview will appear here</div>'; return; }
  649. el.innerHTML = renderFullMd(md);
  650. }
  651. // ─── Toolbar Actions ────────────────────────
  652. function getEditor() { return document.getElementById('pubEditor'); }
  653. function tbWrap(before, after) {
  654. const ta = getEditor();
  655. const start = ta.selectionStart, end = ta.selectionEnd;
  656. const sel = ta.value.substring(start, end) || 'text';
  657. ta.value = ta.value.substring(0, start) + before + sel + after + ta.value.substring(end);
  658. ta.selectionStart = start + before.length;
  659. ta.selectionEnd = start + before.length + sel.length;
  660. ta.focus(); updatePreview();
  661. }
  662. function tbPrefix(prefix) {
  663. const ta = getEditor();
  664. const start = ta.selectionStart;
  665. // Find line start
  666. const lineStart = ta.value.lastIndexOf('\n', start - 1) + 1;
  667. ta.value = ta.value.substring(0, lineStart) + prefix + ta.value.substring(lineStart);
  668. ta.selectionStart = ta.selectionEnd = start + prefix.length;
  669. ta.focus(); updatePreview();
  670. }
  671. function tbLink() {
  672. const ta = getEditor();
  673. const sel = ta.value.substring(ta.selectionStart, ta.selectionEnd) || 'link text';
  674. const start = ta.selectionStart;
  675. const ins = `[${sel}](url)`;
  676. ta.value = ta.value.substring(0, start) + ins + ta.value.substring(ta.selectionEnd);
  677. // Select "url" for easy replacement
  678. ta.selectionStart = start + sel.length + 3;
  679. ta.selectionEnd = start + sel.length + 6;
  680. ta.focus(); updatePreview();
  681. }
  682. function tbImage() {
  683. const ta = getEditor();
  684. const start = ta.selectionStart;
  685. const ins = '![alt text](image-url)';
  686. ta.value = ta.value.substring(0, start) + ins + ta.value.substring(ta.selectionEnd);
  687. ta.selectionStart = start + 12;
  688. ta.selectionEnd = start + 21;
  689. ta.focus(); updatePreview();
  690. }
  691. function tbCodeBlock() {
  692. const ta = getEditor();
  693. const sel = ta.value.substring(ta.selectionStart, ta.selectionEnd) || 'code here';
  694. const start = ta.selectionStart;
  695. const ins = '```\n' + sel + '\n```';
  696. ta.value = ta.value.substring(0, start) + ins + ta.value.substring(ta.selectionEnd);
  697. ta.selectionStart = start + 4;
  698. ta.selectionEnd = start + 4 + sel.length;
  699. ta.focus(); updatePreview();
  700. }
  701. function tbTable() {
  702. const ta = getEditor();
  703. const start = ta.selectionStart;
  704. const ins = '\n| Column 1 | Column 2 | Column 3 |\n|----------|----------|----------|\n| Cell 1 | Cell 2 | Cell 3 |\n| Cell 4 | Cell 5 | Cell 6 |\n';
  705. ta.value = ta.value.substring(0, start) + ins + ta.value.substring(ta.selectionEnd);
  706. ta.selectionStart = start + 3;
  707. ta.selectionEnd = start + 11;
  708. ta.focus(); updatePreview();
  709. }
  710. // ─── Image Upload ───────────────────────────
  711. async function tbUploadImage() {
  712. const repo = document.getElementById('pubRepo').value;
  713. if (!repo) { alert('Select a target repo first — images will be uploaded there'); return; }
  714. const status = document.getElementById('imgStatus');
  715. status.textContent = 'Picking image...';
  716. try {
  717. const img = await send('pickImage');
  718. if (!img) { status.textContent = ''; return; }
  719. status.textContent = `Uploading ${img.name} (${(img.size/1024).toFixed(1)}KB)...`;
  720. const filePath = `assets/images/${img.name}`;
  721. const result = await send('uploadImage', { repo, filePath, base64: img.base64, message: `Upload ${img.name}` });
  722. // Insert markdown image at cursor
  723. const ta = getEditor();
  724. const pos = ta.selectionStart;
  725. const imgMd = `![${img.name}](${result.url})\n`;
  726. ta.value = ta.value.substring(0, pos) + imgMd + ta.value.substring(ta.selectionEnd);
  727. ta.selectionStart = ta.selectionEnd = pos + imgMd.length;
  728. updatePreview();
  729. status.textContent = `Uploaded ${img.name}`;
  730. setTimeout(() => { status.textContent = ''; }, 3000);
  731. } catch (e) { status.textContent = `Error: ${e.message}`; }
  732. }
  733. // ─── Publish Action ─────────────────────────
  734. async function doPublish() {
  735. const content = document.getElementById('pubEditor').value;
  736. if (!content.trim()) { alert('Write some content first'); return; }
  737. const repo = document.getElementById('pubRepo').value;
  738. if (!repo) { alert('Select a repo'); return; }
  739. const fpath = document.getElementById('pubPath').value.trim() || 'README.md';
  740. const msg = document.getElementById('pubMsg').value.trim() || 'Update file';
  741. const res = document.getElementById('pubResult');
  742. res.innerHTML = '<span class="spinner"></span> Publishing...';
  743. document.getElementById('pubBtn').disabled = true;
  744. try {
  745. await send('updateFile', { repo, content, path: fpath, message: msg });
  746. res.innerHTML = `<span style="color:var(--green)">Published ${esc(fpath)} to ${esc(repo)}</span>`;
  747. } catch (e) { res.innerHTML = `<span style="color:var(--red)">Error: ${esc(e.message)}</span>`; }
  748. document.getElementById('pubBtn').disabled = false;
  749. }
  750. // ─── Utils ───────────────────────────────────
  751. function esc(s) { return s==null?'':String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
  752. function timeAgo(d) {
  753. if (!d) return '';
  754. const m = Math.floor((Date.now()-new Date(d))/60000);
  755. if (m<1) return 'just now'; if (m<60) return m+'m'; const h=Math.floor(m/60);
  756. if (h<24) return h+'h'; const dy=Math.floor(h/24); if (dy<30) return dy+'d';
  757. return new Date(d).toLocaleDateString();
  758. }
  759. function renderMd(t) {
  760. if (!t) return '';
  761. let h = esc(t);
  762. h = h.replace(/```(\w*)\n([\s\S]*?)```/g,'<pre><code>$2</code></pre>');
  763. h = h.replace(/`([^`]+)`/g,'<code>$1</code>');
  764. h = h.replace(/\*\*(.+?)\*\*/g,'<strong>$1</strong>');
  765. h = h.replace(/\*(.+?)\*/g,'<em>$1</em>');
  766. h = h.replace(/^### (.+)$/gm,'<h4 style="margin:6px 0 3px">$1</h4>');
  767. h = h.replace(/^## (.+)$/gm,'<h3 style="margin:8px 0 4px">$1</h3>');
  768. h = h.replace(/\[([^\]]+)\]\(([^)]+)\)/g,'<a href="$2" target="_blank">$1</a>');
  769. h = h.replace(/^- (.+)$/gm,'&bull; $1');
  770. h = h.replace(/\n/g,'<br>');
  771. return h;
  772. }
  773. function renderFullMd(t) {
  774. if (!t) return '';
  775. let h = esc(t);
  776. // Code blocks (fenced)
  777. h = h.replace(/```(\w*)\n([\s\S]*?)```/g, '<pre><code>$2</code></pre>');
  778. // Inline code
  779. h = h.replace(/`([^`\n]+)`/g, '<code>$1</code>');
  780. // Images (before links)
  781. h = h.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1">');
  782. // Links
  783. h = h.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank">$1</a>');
  784. // Bold + Italic
  785. h = h.replace(/\*\*\*(.+?)\*\*\*/g, '<strong><em>$1</em></strong>');
  786. h = h.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
  787. h = h.replace(/\*(.+?)\*/g, '<em>$1</em>');
  788. h = h.replace(/~~(.+?)~~/g, '<del>$1</del>');
  789. // Headings
  790. h = h.replace(/^#### (.+)$/gm, '<h4>$1</h4>');
  791. h = h.replace(/^### (.+)$/gm, '<h3>$1</h3>');
  792. h = h.replace(/^## (.+)$/gm, '<h2>$1</h2>');
  793. h = h.replace(/^# (.+)$/gm, '<h1>$1</h1>');
  794. // Horizontal rule
  795. h = h.replace(/^---+$/gm, '<hr>');
  796. // Blockquote
  797. h = h.replace(/^&gt; (.+)$/gm, '<blockquote>$1</blockquote>');
  798. // Tables
  799. h = h.replace(/^(\|.+\|)\n(\|[-| :]+\|)\n((?:\|.+\|\n?)+)/gm, (_, header, sep, body) => {
  800. const ths = header.split('|').filter(c=>c.trim()).map(c=>'<th>'+c.trim()+'</th>').join('');
  801. const rows = body.trim().split('\n').map(row => {
  802. const tds = row.split('|').filter(c=>c.trim()).map(c=>'<td>'+c.trim()+'</td>').join('');
  803. return '<tr>'+tds+'</tr>';
  804. }).join('');
  805. return '<table><thead><tr>'+ths+'</tr></thead><tbody>'+rows+'</tbody></table>';
  806. });
  807. // Unordered list
  808. h = h.replace(/^- (.+)$/gm, '<li>$1</li>');
  809. h = h.replace(/((?:<li>.*<\/li>\n?)+)/g, '<ul>$1</ul>');
  810. // Ordered list
  811. h = h.replace(/^\d+\. (.+)$/gm, '<li>$1</li>');
  812. // Paragraphs: double newline
  813. h = h.replace(/\n\n/g, '</p><p>');
  814. // Single newlines (not inside block elements)
  815. h = h.replace(/\n/g, '<br>');
  816. // Clean up
  817. h = '<p>' + h + '</p>';
  818. h = h.replace(/<p>\s*<(h[1-4]|hr|pre|ul|ol|table|blockquote)/g, '<$1');
  819. h = h.replace(/<\/(h[1-4]|pre|ul|ol|table|blockquote)>\s*<\/p>/g, '</$1>');
  820. h = h.replace(/<p>\s*<\/p>/g, '');
  821. return h;
  822. }
  823. // ─── Boot ────────────────────────────────────
  824. init();
  825. </script>
  826. </body>
  827. </html>