AdminDashboard.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. import { createSignal, createResource, onMount, For, Show } from "solid-js"
  2. const API_BASE = "http://localhost:3000/api/v1"
  3. async function fetchJson(url: string, token: string) {
  4. const res = await fetch(url, { headers: token ? { Authorization: `Bearer ${token}` } : {} })
  5. return res.json()
  6. }
  7. export function AdminDashboard() {
  8. const [token, setToken] = createSignal(localStorage.getItem("kirincode_token") || "")
  9. const [email, setEmail] = createSignal("")
  10. const [password, setPassword] = createSignal("")
  11. const [loggedIn, setLoggedIn] = createSignal(!!token())
  12. const [tab, setTab] = createSignal<"today" | "team" | "users" | "minute">("today")
  13. // Login
  14. async function login() {
  15. const res = await fetch(API_BASE + "/auth/login", {
  16. method: "POST",
  17. headers: { "Content-Type": "application/json" },
  18. body: JSON.stringify({ email: email(), password: password() }),
  19. })
  20. const data = await res.json()
  21. if (data.token) {
  22. localStorage.setItem("kirincode_token", data.token)
  23. setToken(data.token)
  24. setLoggedIn(true)
  25. }
  26. }
  27. // Fetch data
  28. const [todayData] = createResource(loggedIn, () => fetchJson(`${API_BASE}/admin/usage/today`, token()))
  29. const [teamData] = createResource(loggedIn, () => fetchJson(`${API_BASE}/admin/usage/team`, token()))
  30. const [usersData] = createResource(loggedIn, () => fetchJson(`${API_BASE}/admin/usage/users`, token()))
  31. const [minuteData] = createResource(loggedIn, () => fetchJson(`${API_BASE}/admin/usage/minute`, token()))
  32. // Refs for auto-refresh
  33. onMount(() => {
  34. const interval = setInterval(() => {
  35. if (loggedIn()) {
  36. fetchJson(`${API_BASE}/admin/usage/today`, token()).then(d => { /* trigger re-fetch */ })
  37. }
  38. }, 30000)
  39. return () => clearInterval(interval)
  40. })
  41. if (!loggedIn()) {
  42. return (
  43. <div style={{ display: "flex", "flex-direction": "column", "align-items": "center", "justify-content": "center", height: "100vh", background: "#0f172a", color: "#ecf0f1", "font-family": "system-ui, sans-serif" }}>
  44. <h1 style={{ color: "#e74c3c", "margin-bottom": "24px" }}>KirinCode Admin</h1>
  45. <div style={{ display: "flex", "flex-direction": "column", gap: "12px", width: "320px" }}>
  46. <input
  47. type="email" placeholder="Email" value={email()} onInput={e => setEmail(e.currentTarget.value)}
  48. style={{ padding: "10px 14px", "border-radius": "8px", border: "1px solid #2c3e50", background: "#1a1a2e", color: "#ecf0f1", "font-size": "14px" }}
  49. />
  50. <input
  51. type="password" placeholder="Password" value={password()} onInput={e => setPassword(e.currentTarget.value)}
  52. style={{ padding: "10px 14px", "border-radius": "8px", border: "1px solid #2c3e50", background: "#1a1a2e", color: "#ecf0f1", "font-size": "14px" }}
  53. />
  54. <button onClick={login}
  55. style={{ padding: "10px", "border-radius": "8px", border: "none", background: "#e74c3c", color: "#fff", "font-size": "14px", "font-weight": "600", cursor: "pointer" }}
  56. >Login</button>
  57. </div>
  58. </div>
  59. )
  60. }
  61. return (
  62. <div style={{ display: "flex", "flex-direction": "column", height: "100vh", background: "#0f172a", color: "#ecf0f1", "font-family": "system-ui, sans-serif" }}>
  63. {/* Header */}
  64. <div style={{ display: "flex", "align-items": "center", padding: "12px 20px", background: "#1a1a2e", "border-bottom": "1px solid #2c3e50" }}>
  65. <span style={{ "font-size": "18px", "font-weight": "700", color: "#e74c3c", "margin-right": "24px" }}>🦄 KirinCode Admin</span>
  66. <div style={{ display: "flex", gap: "4px" }}>
  67. {(["today", "team", "users", "minute"] as const).map(t => (
  68. <button onClick={() => setTab(t)}
  69. style={{
  70. padding: "6px 14px", "border-radius": "6px", border: "none", "font-size": "13px", cursor: "pointer",
  71. background: tab() === t ? "#e74c3c" : "transparent",
  72. color: tab() === t ? "#fff" : "#7f8c8d",
  73. }}
  74. >{t === "today" ? "Today" : t === "team" ? "Team" : t === "users" ? "Users" : "Minute"}</button>
  75. ))}
  76. </div>
  77. <button onClick={() => { localStorage.removeItem("kirincode_token"); setToken(""); setLoggedIn(false) }}
  78. style={{ "margin-left": "auto", padding: "6px 14px", "border-radius": "6px", border: "1px solid #2c3e50", background: "transparent", color: "#7f8c8d", cursor: "pointer", "font-size": "12px" }}
  79. >Logout</button>
  80. </div>
  81. {/* Content */}
  82. <div style={{ flex: 1, overflow: "auto", padding: "24px" }}>
  83. <Show when={tab() === "today" && todayData()}>
  84. {() => {
  85. const d = todayData()?.data || {}
  86. return (
  87. <div>
  88. <h2 style={{ "margin-bottom": "20px" }}>Today's Usage</h2>
  89. <div style={{ display: "grid", "grid-template-columns": "repeat(auto-fit, minmax(200px, 1fr))", gap: "16px" }}>
  90. <MetricCard label="Total Tokens" value={(d.input_tokens + d.output_tokens).toLocaleString()} />
  91. <MetricCard label="Requests" value={d.request_count?.toLocaleString() || "0"} />
  92. <MetricCard label="Cost" value={`$${d.total_cost?.toFixed(4) || "0.0000"}`} />
  93. <MetricCard label="Active Users" value={d.active_users || "0"} />
  94. </div>
  95. </div>
  96. )
  97. }}
  98. </Show>
  99. <Show when={tab() === "team" && teamData()}>
  100. {() => (
  101. <div>
  102. <h2 style={{ "margin-bottom": "20px" }}>Team Usage</h2>
  103. <table style={{ width: "100%", "border-collapse": "collapse" }}>
  104. <thead>
  105. <tr style={{ "text-align": "left", color: "#7f8c8d", "font-size": "12px" }}>
  106. <th style={{ padding: "8px" }}>Day</th>
  107. <th style={{ padding: "8px" }}>Provider</th>
  108. <th style={{ padding: "8px" }}>Model</th>
  109. <th style={{ padding: "8px", "text-align": "right" }}>Input</th>
  110. <th style={{ padding: "8px", "text-align": "right" }}>Output</th>
  111. <th style={{ padding: "8px", "text-align": "right" }}>Requests</th>
  112. <th style={{ padding: "8px", "text-align": "right" }}>Cost</th>
  113. </tr>
  114. </thead>
  115. <For each={(teamData()?.data || []).slice(0, 50)}>
  116. {(row: any) => (
  117. <tr style={{ "border-top": "1px solid #1e293b", "font-size": "13px" }}>
  118. <td style={{ padding: "8px" }}>{row.day}</td>
  119. <td style={{ padding: "8px" }}>{row.provider}</td>
  120. <td style={{ padding: "8px" }}>{row.model}</td>
  121. <td style={{ padding: "8px", "text-align": "right" }}>{row.input_tokens?.toLocaleString()}</td>
  122. <td style={{ padding: "8px", "text-align": "right" }}>{row.output_tokens?.toLocaleString()}</td>
  123. <td style={{ padding: "8px", "text-align": "right" }}>{row.request_count}</td>
  124. <td style={{ padding: "8px", "text-align": "right", color: "#f39c12" }}>${row.total_cost?.toFixed(4)}</td>
  125. </tr>
  126. )}
  127. </For>
  128. </table>
  129. </div>
  130. )}
  131. </Show>
  132. <Show when={tab() === "users" && usersData()}>
  133. {() => (
  134. <div>
  135. <h2 style={{ "margin-bottom": "20px" }}>User Rankings</h2>
  136. <table style={{ width: "100%", "border-collapse": "collapse" }}>
  137. <thead>
  138. <tr style={{ "text-align": "left", color: "#7f8c8d", "font-size": "12px" }}>
  139. <th style={{ padding: "8px" }}>User</th>
  140. <th style={{ padding: "8px" }}>Email</th>
  141. <th style={{ padding: "8px", "text-align": "right" }}>Input</th>
  142. <th style={{ padding: "8px", "text-align": "right" }}>Output</th>
  143. <th style={{ padding: "8px", "text-align": "right" }}>Requests</th>
  144. <th style={{ padding: "8px", "text-align": "right" }}>Cost</th>
  145. </tr>
  146. </thead>
  147. <For each={usersData()?.data || []}>
  148. {(row: any) => (
  149. <tr style={{ "border-top": "1px solid #1e293b", "font-size": "13px" }}>
  150. <td style={{ padding: "8px" }}>{row.display_name}</td>
  151. <td style={{ padding: "8px", color: "#7f8c8d" }}>{row.email}</td>
  152. <td style={{ padding: "8px", "text-align": "right" }}>{row.input_tokens?.toLocaleString()}</td>
  153. <td style={{ padding: "8px", "text-align": "right" }}>{row.output_tokens?.toLocaleString()}</td>
  154. <td style={{ padding: "8px", "text-align": "right" }}>{row.request_count}</td>
  155. <td style={{ padding: "8px", "text-align": "right", color: "#f39c12" }}>${row.total_cost?.toFixed(4)}</td>
  156. </tr>
  157. )}
  158. </For>
  159. </table>
  160. </div>
  161. )}
  162. </Show>
  163. <Show when={tab() === "minute" && minuteData()}>
  164. {() => (
  165. <div>
  166. <h2 style={{ "margin-bottom": "20px" }}>Minute-Level Detail</h2>
  167. <table style={{ width: "100%", "border-collapse": "collapse" }}>
  168. <thead>
  169. <tr style={{ "text-align": "left", color: "#7f8c8d", "font-size": "12px" }}>
  170. <th style={{ padding: "8px" }}>Time</th>
  171. <th style={{ padding: "8px" }}>Provider</th>
  172. <th style={{ padding: "8px" }}>Model</th>
  173. <th style={{ padding: "8px", "text-align": "right" }}>Input</th>
  174. <th style={{ padding: "8px", "text-align": "right" }}>Output</th>
  175. <th style={{ padding: "8px", "text-align": "right" }}>Cost</th>
  176. </tr>
  177. </thead>
  178. <For each={(minuteData()?.data || []).slice(0, 100)}>
  179. {(row: any) => (
  180. <tr style={{ "border-top": "1px solid #1e293b", "font-size": "12px" }}>
  181. <td style={{ padding: "8px" }}>{row.minute}</td>
  182. <td style={{ padding: "8px" }}>{row.provider}</td>
  183. <td style={{ padding: "8px" }}>{row.model}</td>
  184. <td style={{ padding: "8px", "text-align": "right" }}>{row.input_tokens?.toLocaleString()}</td>
  185. <td style={{ padding: "8px", "text-align": "right" }}>{row.output_tokens?.toLocaleString()}</td>
  186. <td style={{ padding: "8px", "text-align": "right", color: "#f39c12" }}>${row.cost_usd?.toFixed(5)}</td>
  187. </tr>
  188. )}
  189. </For>
  190. </table>
  191. </div>
  192. )}
  193. </Show>
  194. </div>
  195. </div>
  196. )
  197. }
  198. function MetricCard(props: { label: string; value: string }) {
  199. return (
  200. <div style={{ background: "#1a1a2e", padding: "20px", "border-radius": "12px", border: "1px solid #2c3e50" }}>
  201. <div style={{ color: "#7f8c8d", "font-size": "12px", "margin-bottom": "8px" }}>{props.label}</div>
  202. <div style={{ "font-size": "24px", "font-weight": "700", color: "#f39c12" }}>{props.value}</div>
  203. </div>
  204. )
  205. }