llms-py 2.0.20__py3-none-any.whl → 3.0.18__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (207) hide show
  1. llms/__init__.py +3 -1
  2. llms/db.py +359 -0
  3. llms/{ui/Analytics.mjs → extensions/analytics/ui/index.mjs} +254 -327
  4. llms/extensions/app/README.md +20 -0
  5. llms/extensions/app/__init__.py +588 -0
  6. llms/extensions/app/db.py +540 -0
  7. llms/{ui → extensions/app/ui}/Recents.mjs +99 -73
  8. llms/{ui/Sidebar.mjs → extensions/app/ui/index.mjs} +139 -68
  9. llms/extensions/app/ui/threadStore.mjs +440 -0
  10. llms/extensions/computer/README.md +96 -0
  11. llms/extensions/computer/__init__.py +59 -0
  12. llms/extensions/computer/base.py +80 -0
  13. llms/extensions/computer/bash.py +185 -0
  14. llms/extensions/computer/computer.py +523 -0
  15. llms/extensions/computer/edit.py +299 -0
  16. llms/extensions/computer/filesystem.py +542 -0
  17. llms/extensions/computer/platform.py +461 -0
  18. llms/extensions/computer/run.py +37 -0
  19. llms/extensions/core_tools/CALCULATOR.md +32 -0
  20. llms/extensions/core_tools/__init__.py +599 -0
  21. llms/extensions/core_tools/ui/codemirror/addon/edit/closebrackets.js +201 -0
  22. llms/extensions/core_tools/ui/codemirror/addon/edit/closetag.js +185 -0
  23. llms/extensions/core_tools/ui/codemirror/addon/edit/continuelist.js +101 -0
  24. llms/extensions/core_tools/ui/codemirror/addon/edit/matchbrackets.js +160 -0
  25. llms/extensions/core_tools/ui/codemirror/addon/edit/matchtags.js +66 -0
  26. llms/extensions/core_tools/ui/codemirror/addon/edit/trailingspace.js +27 -0
  27. llms/extensions/core_tools/ui/codemirror/addon/selection/active-line.js +72 -0
  28. llms/extensions/core_tools/ui/codemirror/addon/selection/mark-selection.js +119 -0
  29. llms/extensions/core_tools/ui/codemirror/addon/selection/selection-pointer.js +98 -0
  30. llms/extensions/core_tools/ui/codemirror/codemirror.css +344 -0
  31. llms/extensions/core_tools/ui/codemirror/codemirror.js +9884 -0
  32. llms/extensions/core_tools/ui/codemirror/doc/docs.css +225 -0
  33. llms/extensions/core_tools/ui/codemirror/doc/source_sans.woff +0 -0
  34. llms/extensions/core_tools/ui/codemirror/mode/clike/clike.js +942 -0
  35. llms/extensions/core_tools/ui/codemirror/mode/javascript/index.html +118 -0
  36. llms/extensions/core_tools/ui/codemirror/mode/javascript/javascript.js +962 -0
  37. llms/extensions/core_tools/ui/codemirror/mode/javascript/typescript.html +62 -0
  38. llms/extensions/core_tools/ui/codemirror/mode/python/python.js +402 -0
  39. llms/extensions/core_tools/ui/codemirror/theme/dracula.css +40 -0
  40. llms/extensions/core_tools/ui/codemirror/theme/mocha.css +135 -0
  41. llms/extensions/core_tools/ui/index.mjs +650 -0
  42. llms/extensions/gallery/README.md +61 -0
  43. llms/extensions/gallery/__init__.py +63 -0
  44. llms/extensions/gallery/db.py +243 -0
  45. llms/extensions/gallery/ui/index.mjs +482 -0
  46. llms/extensions/katex/README.md +39 -0
  47. llms/extensions/katex/__init__.py +6 -0
  48. llms/extensions/katex/ui/README.md +125 -0
  49. llms/extensions/katex/ui/contrib/auto-render.js +338 -0
  50. llms/extensions/katex/ui/contrib/auto-render.min.js +1 -0
  51. llms/extensions/katex/ui/contrib/auto-render.mjs +244 -0
  52. llms/extensions/katex/ui/contrib/copy-tex.js +127 -0
  53. llms/extensions/katex/ui/contrib/copy-tex.min.js +1 -0
  54. llms/extensions/katex/ui/contrib/copy-tex.mjs +105 -0
  55. llms/extensions/katex/ui/contrib/mathtex-script-type.js +109 -0
  56. llms/extensions/katex/ui/contrib/mathtex-script-type.min.js +1 -0
  57. llms/extensions/katex/ui/contrib/mathtex-script-type.mjs +24 -0
  58. llms/extensions/katex/ui/contrib/mhchem.js +3213 -0
  59. llms/extensions/katex/ui/contrib/mhchem.min.js +1 -0
  60. llms/extensions/katex/ui/contrib/mhchem.mjs +3109 -0
  61. llms/extensions/katex/ui/contrib/render-a11y-string.js +887 -0
  62. llms/extensions/katex/ui/contrib/render-a11y-string.min.js +1 -0
  63. llms/extensions/katex/ui/contrib/render-a11y-string.mjs +800 -0
  64. llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.ttf +0 -0
  65. llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.woff +0 -0
  66. llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  67. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
  68. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
  69. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  70. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
  71. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
  72. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  73. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
  74. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.woff +0 -0
  75. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  76. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
  77. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.woff +0 -0
  78. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  79. llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.ttf +0 -0
  80. llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.woff +0 -0
  81. llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.woff2 +0 -0
  82. llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
  83. llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.woff +0 -0
  84. llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  85. llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.ttf +0 -0
  86. llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.woff +0 -0
  87. llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.woff2 +0 -0
  88. llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.ttf +0 -0
  89. llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.woff +0 -0
  90. llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.woff2 +0 -0
  91. llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
  92. llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.woff +0 -0
  93. llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  94. llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.ttf +0 -0
  95. llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.woff +0 -0
  96. llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.woff2 +0 -0
  97. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
  98. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.woff +0 -0
  99. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  100. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
  101. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.woff +0 -0
  102. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  103. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
  104. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.woff +0 -0
  105. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  106. llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.ttf +0 -0
  107. llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.woff +0 -0
  108. llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.woff2 +0 -0
  109. llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.ttf +0 -0
  110. llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.woff +0 -0
  111. llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  112. llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.ttf +0 -0
  113. llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.woff +0 -0
  114. llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  115. llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.ttf +0 -0
  116. llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.woff +0 -0
  117. llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  118. llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.ttf +0 -0
  119. llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.woff +0 -0
  120. llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  121. llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
  122. llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.woff +0 -0
  123. llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  124. llms/extensions/katex/ui/index.mjs +92 -0
  125. llms/extensions/katex/ui/katex-swap.css +1230 -0
  126. llms/extensions/katex/ui/katex-swap.min.css +1 -0
  127. llms/extensions/katex/ui/katex.css +1230 -0
  128. llms/extensions/katex/ui/katex.js +19080 -0
  129. llms/extensions/katex/ui/katex.min.css +1 -0
  130. llms/extensions/katex/ui/katex.min.js +1 -0
  131. llms/extensions/katex/ui/katex.min.mjs +1 -0
  132. llms/extensions/katex/ui/katex.mjs +18547 -0
  133. llms/extensions/providers/__init__.py +22 -0
  134. llms/extensions/providers/anthropic.py +260 -0
  135. llms/extensions/providers/cerebras.py +36 -0
  136. llms/extensions/providers/chutes.py +153 -0
  137. llms/extensions/providers/google.py +559 -0
  138. llms/extensions/providers/nvidia.py +103 -0
  139. llms/extensions/providers/openai.py +154 -0
  140. llms/extensions/providers/openrouter.py +74 -0
  141. llms/extensions/providers/zai.py +182 -0
  142. llms/extensions/skills/LICENSE +202 -0
  143. llms/extensions/skills/__init__.py +130 -0
  144. llms/extensions/skills/errors.py +25 -0
  145. llms/extensions/skills/models.py +39 -0
  146. llms/extensions/skills/parser.py +178 -0
  147. llms/extensions/skills/ui/index.mjs +376 -0
  148. llms/extensions/skills/ui/skills/create-plan/SKILL.md +74 -0
  149. llms/extensions/skills/validator.py +177 -0
  150. llms/extensions/system_prompts/README.md +22 -0
  151. llms/extensions/system_prompts/__init__.py +45 -0
  152. llms/extensions/system_prompts/ui/index.mjs +276 -0
  153. llms/extensions/system_prompts/ui/prompts.json +1067 -0
  154. llms/extensions/tools/__init__.py +67 -0
  155. llms/extensions/tools/ui/index.mjs +837 -0
  156. llms/index.html +36 -62
  157. llms/llms.json +180 -879
  158. llms/main.py +4009 -912
  159. llms/providers-extra.json +394 -0
  160. llms/providers.json +1 -0
  161. llms/ui/App.mjs +176 -8
  162. llms/ui/ai.mjs +156 -20
  163. llms/ui/app.css +3768 -321
  164. llms/ui/ctx.mjs +459 -0
  165. llms/ui/index.mjs +131 -0
  166. llms/ui/lib/chart.js +14 -0
  167. llms/ui/lib/charts.mjs +16 -0
  168. llms/ui/lib/color.js +14 -0
  169. llms/ui/lib/highlight.min.mjs +1243 -0
  170. llms/ui/lib/idb.min.mjs +8 -0
  171. llms/ui/lib/marked.min.mjs +8 -0
  172. llms/ui/lib/servicestack-client.mjs +1 -0
  173. llms/ui/lib/servicestack-vue.mjs +37 -0
  174. llms/ui/lib/vue-router.min.mjs +6 -0
  175. llms/ui/lib/vue.min.mjs +13 -0
  176. llms/ui/lib/vue.mjs +18530 -0
  177. llms/ui/markdown.mjs +25 -14
  178. llms/ui/modules/chat/ChatBody.mjs +1156 -0
  179. llms/ui/{SettingsDialog.mjs → modules/chat/SettingsDialog.mjs} +74 -74
  180. llms/ui/modules/chat/index.mjs +995 -0
  181. llms/ui/modules/icons.mjs +46 -0
  182. llms/ui/modules/layout.mjs +271 -0
  183. llms/ui/modules/model-selector.mjs +811 -0
  184. llms/ui/tailwind.input.css +560 -78
  185. llms/ui/typography.css +54 -36
  186. llms/ui/utils.mjs +221 -92
  187. llms_py-3.0.18.dist-info/METADATA +49 -0
  188. llms_py-3.0.18.dist-info/RECORD +194 -0
  189. {llms_py-2.0.20.dist-info → llms_py-3.0.18.dist-info}/WHEEL +1 -1
  190. {llms_py-2.0.20.dist-info → llms_py-3.0.18.dist-info}/licenses/LICENSE +1 -2
  191. llms/ui/Avatar.mjs +0 -28
  192. llms/ui/Brand.mjs +0 -34
  193. llms/ui/ChatPrompt.mjs +0 -443
  194. llms/ui/Main.mjs +0 -740
  195. llms/ui/ModelSelector.mjs +0 -60
  196. llms/ui/ProviderIcon.mjs +0 -29
  197. llms/ui/ProviderStatus.mjs +0 -105
  198. llms/ui/SignIn.mjs +0 -64
  199. llms/ui/SystemPromptEditor.mjs +0 -31
  200. llms/ui/SystemPromptSelector.mjs +0 -36
  201. llms/ui/Welcome.mjs +0 -8
  202. llms/ui/threadStore.mjs +0 -524
  203. llms/ui.json +0 -1069
  204. llms_py-2.0.20.dist-info/METADATA +0 -931
  205. llms_py-2.0.20.dist-info/RECORD +0 -36
  206. {llms_py-2.0.20.dist-info → llms_py-3.0.18.dist-info}/entry_points.txt +0 -0
  207. {llms_py-2.0.20.dist-info → llms_py-3.0.18.dist-info}/top_level.txt +0 -0
@@ -1,39 +1,35 @@
1
- import { ref, onMounted, watch, nextTick, computed } from 'vue'
1
+ import { ref, watch, nextTick, computed, inject, onMounted, onUnmounted } from 'vue'
2
2
  import { useRouter, useRoute } from 'vue-router'
3
- import { useFormatters } from "@servicestack/vue"
4
3
  import { leftPart } from '@servicestack/client'
5
4
  import { Chart, registerables } from "chart.js"
6
- import { useThreadStore } from './threadStore.mjs'
7
- import { formatCost } from './utils.mjs'
8
5
  Chart.register(...registerables)
9
6
 
10
- const { humanifyNumber, humanifyMs } = useFormatters()
11
-
12
7
  export const colors = [
13
- { background: 'rgba(54, 162, 235, 0.2)', border: 'rgb(54, 162, 235)' }, //blue
14
- { background: 'rgba(255, 99, 132, 0.2)', border: 'rgb(255, 99, 132)' },
8
+ { background: 'rgba(54, 162, 235, 0.2)', border: 'rgb(54, 162, 235)' }, //blue
9
+ { background: 'rgba(255, 99, 132, 0.2)', border: 'rgb(255, 99, 132)' },
15
10
  { background: 'rgba(153, 102, 255, 0.2)', border: 'rgb(153, 102, 255)' },
16
- { background: 'rgba(54, 162, 235, 0.2)', border: 'rgb(54, 162, 235)' },
17
- { background: 'rgba(255, 159, 64, 0.2)', border: 'rgb(255, 159, 64)' },
18
- { background: 'rgba(67, 56, 202, 0.2)', border: 'rgb(67, 56, 202)' },
19
- { background: 'rgba(255, 99, 132, 0.2)', border: 'rgb(255, 99, 132)' },
20
- { background: 'rgba(14, 116, 144, 0.2)', border: 'rgb(14, 116, 144)' },
21
- { background: 'rgba(162, 28, 175, 0.2)', border: 'rgb(162, 28, 175)' },
11
+ { background: 'rgba(54, 162, 235, 0.2)', border: 'rgb(54, 162, 235)' },
12
+ { background: 'rgba(255, 159, 64, 0.2)', border: 'rgb(255, 159, 64)' },
13
+ { background: 'rgba(67, 56, 202, 0.2)', border: 'rgb(67, 56, 202)' },
14
+ { background: 'rgba(255, 99, 132, 0.2)', border: 'rgb(255, 99, 132)' },
15
+ { background: 'rgba(14, 116, 144, 0.2)', border: 'rgb(14, 116, 144)' },
16
+ { background: 'rgba(162, 28, 175, 0.2)', border: 'rgb(162, 28, 175)' },
22
17
  { background: 'rgba(201, 203, 207, 0.2)', border: 'rgb(201, 203, 207)' },
23
18
  ]
24
19
 
25
20
  const MonthSelector = {
26
- template:`
27
- <div class="flex gap-4 items-center">
21
+ template: `
22
+ <div class="flex flex-col sm:flex-row gap-2 sm:gap-4 items-stretch sm:items-center w-full sm:w-auto">
28
23
  <!-- Months Row -->
29
- <div class="flex gap-2 flex-wrap justify-center">
24
+ <div class="flex gap-1 sm:gap-2 flex-wrap justify-center overflow-x-auto">
30
25
  <template v-for="month in availableMonthsForYear" :key="month">
31
26
  <span v-if="selectedMonth === month"
32
- class="text-xs leading-5 font-semibold bg-indigo-600 text-white rounded-full py-1 px-3 flex items-center space-x-2">
33
- {{ new Date(selectedYear + '-' + month.toString().padStart(2,'0') + '-01').toLocaleString('default', { month: 'long' }) }}
27
+ class="text-xs leading-5 font-semibold bg-indigo-600 text-white rounded-full py-1 px-2 sm:px-3 flex items-center space-x-2 whitespace-nowrap">
28
+ <span class="hidden sm:inline">{{ new Date(selectedYear + '-' + month.toString().padStart(2,'0') + '-01').toLocaleString('default', { month: 'long' }) }}</span>
29
+ <span class="sm:hidden">{{ new Date(selectedYear + '-' + month.toString().padStart(2,'0') + '-01').toLocaleString('default', { month: 'short' }) }}</span>
34
30
  </span>
35
31
  <button v-else type="button"
36
- class="text-xs leading-5 font-semibold bg-slate-400/10 rounded-full py-1 px-3 flex items-center space-x-2 hover:bg-slate-400/20 dark:highlight-white/5"
32
+ class="text-xs leading-5 font-semibold bg-slate-400/10 rounded-full py-1 px-2 sm:px-3 flex items-center space-x-2 hover:bg-slate-400/20 dark:highlight-white/5 whitespace-nowrap"
37
33
  @click="updateSelection(selectedYear, month)">
38
34
  {{ new Date(selectedYear + '-' + month.toString().padStart(2,'0') + '-01').toLocaleString('default', { month: 'short' }) }}
39
35
  </button>
@@ -42,7 +38,7 @@ const MonthSelector = {
42
38
 
43
39
  <!-- Year Dropdown -->
44
40
  <select :value="selectedYear" @change="(e) => updateSelection(parseInt(e.target.value), selectedMonth)"
45
- class="border border-gray-300 rounded-md text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500">
41
+ class="border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-900 text-gray-700 dark:text-gray-300 rounded-md text-sm font-medium hover:bg-gray-50 dark:hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-indigo-500 flex-shrink-0">
46
42
  <option v-for="year in availableYears" :key="year" :value="year">
47
43
  {{ year }}
48
44
  </option>
@@ -50,7 +46,7 @@ const MonthSelector = {
50
46
  </div>
51
47
  `,
52
48
  props: {
53
- dailyData: Array,
49
+ dailyData: Object,
54
50
  },
55
51
  setup(props) {
56
52
  const router = useRouter()
@@ -108,16 +104,15 @@ const MonthSelector = {
108
104
  }
109
105
  }
110
106
 
111
- export default {
112
- components: {
113
- MonthSelector,
114
- },
107
+ export const Analytics = {
115
108
  template: `
116
- <div class="flex flex-col h-full w-full">
109
+ <div class="flex flex-col w-full">
117
110
  <!-- Header -->
118
- <div class="border-b border-gray-200 bg-white px-4 py-3 min-h-16">
119
- <div class="max-w-6xl mx-auto flex items-center justify-between gap-3">
120
- <h2 class="text-lg font-semibold text-gray-900">
111
+ <div class="border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 px-2 sm:px-4 py-3">
112
+ <div
113
+ :class="!$ai.isSidebarOpen ? 'pl-3' : ''"
114
+ class="max-w-6xl mx-auto flex flex-col sm:flex-row items-stretch sm:items-center justify-between gap-3">
115
+ <h2 class="text-lg font-semibold text-gray-900 dark:text-gray-100 flex-shrink-0">
121
116
  <RouterLink to="/analytics">Analytics</RouterLink>
122
117
  </h2>
123
118
  <MonthSelector :dailyData="allDailyData" />
@@ -125,67 +120,67 @@ export default {
125
120
  </div>
126
121
 
127
122
  <!-- Tabs -->
128
- <div class="border-b border-gray-200 bg-white px-4">
123
+ <div class="border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 px-4">
129
124
  <div class="max-w-6xl mx-auto flex gap-8">
130
125
  <button type="button"
131
126
  @click="activeTab = 'cost'"
132
127
  :class="['py-3 px-1 border-b-2 font-medium text-sm transition-colors',
133
128
  activeTab === 'cost'
134
- ? 'border-blue-500 text-blue-600'
135
- : 'border-transparent text-gray-600 hover:text-gray-900']">
129
+ ? 'border-blue-500 text-blue-600 dark:text-blue-400'
130
+ : 'border-transparent text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200']">
136
131
  Cost Analysis
137
132
  </button>
138
133
  <button type="button"
139
134
  @click="activeTab = 'tokens'"
140
135
  :class="['py-3 px-1 border-b-2 font-medium text-sm transition-colors',
141
136
  activeTab === 'tokens'
142
- ? 'border-blue-500 text-blue-600'
143
- : 'border-transparent text-gray-600 hover:text-gray-900']">
137
+ ? 'border-blue-500 text-blue-600 dark:text-blue-400'
138
+ : 'border-transparent text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200']">
144
139
  Token Usage
145
140
  </button>
146
141
  <button type="button"
147
142
  @click="activeTab = 'activity'"
148
143
  :class="['py-3 px-1 border-b-2 font-medium text-sm transition-colors',
149
144
  activeTab === 'activity'
150
- ? 'border-blue-500 text-blue-600'
151
- : 'border-transparent text-gray-600 hover:text-gray-900']">
145
+ ? 'border-blue-500 text-blue-600 dark:text-blue-400'
146
+ : 'border-transparent text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200']">
152
147
  Activity
153
148
  </button>
154
149
  </div>
155
150
  </div>
156
151
 
157
152
  <!-- Content -->
158
- <div class="flex-1 overflow-auto bg-gray-50" :class="activeTab === 'activity' ? 'p-0' : 'p-4'">
153
+ <div class="flex-1 bg-gray-50 dark:bg-gray-900" :class="activeTab === 'activity' ? 'p-0' : 'p-4'">
159
154
 
160
- <div :class="activeTab === 'activity' ? 'h-full' : 'max-w-6xl mx-auto'">
155
+ <div :class="activeTab === 'activity' ? '' : 'max-w-6xl mx-auto'">
161
156
  <!-- Stats Summary (hidden for Activity tab) -->
162
157
  <div v-if="activeTab !== 'activity'" class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
163
- <div class="bg-white rounded-lg shadow p-4">
164
- <div class="text-sm font-medium text-gray-600">Total Cost</div>
165
- <div class="text-2xl font-bold text-gray-900 mt-1">{{ formatCost(totalCost) }}</div>
158
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
159
+ <div class="text-sm font-medium text-gray-600 dark:text-gray-400">Total Cost</div>
160
+ <div class="text-2xl font-bold text-gray-900 dark:text-gray-100 mt-1">{{ $fmt.cost(totalCost) }}</div>
166
161
  </div>
167
- <div class="bg-white rounded-lg shadow p-4">
168
- <div class="text-sm font-medium text-gray-600">Total Requests</div>
169
- <div class="text-2xl font-bold text-gray-900 mt-1">{{ totalRequests }}</div>
162
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
163
+ <div class="text-sm font-medium text-gray-600 dark:text-gray-400">Total Requests</div>
164
+ <div class="text-2xl font-bold text-gray-900 dark:text-gray-100 mt-1">{{ totalRequests }}</div>
170
165
  </div>
171
- <div class="bg-white rounded-lg shadow p-4">
172
- <div class="text-sm font-medium text-gray-600">Total Input Tokens</div>
173
- <div class="text-2xl font-bold text-gray-900 mt-1">{{ humanifyNumber(totalInputTokens) }}</div>
166
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
167
+ <div class="text-sm font-medium text-gray-600 dark:text-gray-400">Total Input Tokens</div>
168
+ <div class="text-2xl font-bold text-gray-900 dark:text-gray-100 mt-1">{{ $fmt.humanifyNumber(totalInputTokens) }}</div>
174
169
  </div>
175
- <div class="bg-white rounded-lg shadow p-4">
176
- <div class="text-sm font-medium text-gray-600">Total Output Tokens</div>
177
- <div class="text-2xl font-bold text-gray-900 mt-1">{{ humanifyNumber(totalOutputTokens) }}</div>
170
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
171
+ <div class="text-sm font-medium text-gray-600 dark:text-gray-400">Total Output Tokens</div>
172
+ <div class="text-2xl font-bold text-gray-900 dark:text-gray-100 mt-1">{{ $fmt.humanifyNumber(totalOutputTokens) }}</div>
178
173
  </div>
179
174
  </div>
180
175
 
181
176
  <!-- Cost Analysis Tab -->
182
- <div v-if="activeTab === 'cost'" class="bg-white rounded-lg shadow p-6">
183
- <div class="flex items-center justify-between mb-6">
184
- <h3 class="text-lg font-semibold text-gray-900">Daily Costs</h3>
185
- <h3 class="text-lg font-semibold text-gray-900">
177
+ <div v-if="activeTab === 'cost'" class="bg-white dark:bg-gray-800 rounded-lg shadow p-4 sm:p-6">
178
+ <div class="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 mb-6">
179
+ <h3 class="text-base sm:text-lg font-semibold text-gray-900 dark:text-gray-100">Daily Costs</h3>
180
+ <h3 class="text-sm sm:text-lg font-semibold text-gray-900 dark:text-gray-100">
186
181
  {{ new Date(selectedDay).toLocaleDateString(undefined, { year: 'numeric', month: 'long' }) }}
187
182
  </h3>
188
- <select v-model="costChartType" class="px-3 pr-6 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 hover:bg-gray-50">
183
+ <select v-model="costChartType" class="px-3 pr-6 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-900 text-gray-700 dark:text-gray-300 rounded-md text-sm font-medium hover:bg-gray-50 dark:hover:bg-gray-800 flex-shrink-0">
189
184
  <option value="bar">Bar Chart</option>
190
185
  <option value="line">Line Chart</option>
191
186
  </select>
@@ -194,16 +189,16 @@ export default {
194
189
  <div v-if="chartData.labels.length > 0" class="relative h-96">
195
190
  <canvas ref="costChartCanvas"></canvas>
196
191
  </div>
197
- <div v-else class="flex items-center justify-center h-96 text-gray-500">
192
+ <div v-else class="flex items-center justify-center h-96 text-gray-500 dark:text-gray-400">
198
193
  <p>No request data available</p>
199
194
  </div>
200
195
  </div>
201
196
 
202
197
  <!-- Token Usage Tab -->
203
- <div v-if="activeTab === 'tokens'" class="bg-white rounded-lg shadow p-6">
204
- <h3 class="text-lg font-semibold text-gray-900 mb-6 flex justify-between items-center">
198
+ <div v-if="activeTab === 'tokens'" class="bg-white dark:bg-gray-800 rounded-lg shadow p-4 sm:p-6">
199
+ <h3 class="text-base sm:text-lg font-semibold text-gray-900 dark:text-gray-100 mb-6 flex flex-col sm:flex-row justify-between items-start sm:items-center gap-2">
205
200
  <span>Daily Token Usage</span>
206
- <span>
201
+ <span class="text-sm sm:text-base">
207
202
  {{ new Date(selectedDay).toLocaleDateString(undefined, { year: 'numeric', month: 'long' }) }}
208
203
  </span>
209
204
  </h3>
@@ -211,48 +206,48 @@ export default {
211
206
  <div v-if="tokenChartData.labels.length > 0" class="relative h-96">
212
207
  <canvas ref="tokenChartCanvas"></canvas>
213
208
  </div>
214
- <div v-else class="flex items-center justify-center h-96 text-gray-500">
209
+ <div v-else class="flex items-center justify-center h-96 text-gray-500 dark:text-gray-400">
215
210
  <p>No request data available</p>
216
211
  </div>
217
212
  </div>
218
213
 
219
- <div v-if="allDailyData[selectedDay]?.requests && ['cost', 'tokens'].includes(activeTab)" class="mt-8 px-2 text-gray-700 font-medium flex items-center justify-between">
214
+ <div v-if="allDailyData[selectedDay]?.requests && ['cost', 'tokens'].includes(activeTab)" class="mt-8 px-2 text-sm sm:text-base text-gray-700 dark:text-gray-300 font-medium flex flex-col sm:flex-row items-start sm:items-center justify-between gap-2">
220
215
  <div>
221
- {{ new Date(selectedDay).toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' }) }}
216
+ {{ new Date(selectedDay).toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' }) }}
222
217
  </div>
223
- <div>
224
- {{ formatCost(allDailyData[selectedDay]?.cost || 0) }}
225
- &#183;
226
- {{ allDailyData[selectedDay]?.requests || 0 }} Requests
227
- &#183;
228
- {{ humanifyNumber(allDailyData[selectedDay]?.inputTokens || 0) }} -> {{ humanifyNumber(allDailyData[selectedDay]?.outputTokens || 0) }} Tokens
218
+ <div class="flex flex-wrap gap-x-2 gap-y-1">
219
+ <span>{{ $fmt.cost(allDailyData[selectedDay]?.cost || 0) }}</span>
220
+ <span>&#183;</span>
221
+ <span>{{ allDailyData[selectedDay]?.requests || 0 }} Requests</span>
222
+ <span>&#183;</span>
223
+ <span>{{ $fmt.humanifyNumber(allDailyData[selectedDay]?.inputTokens || 0) }} -> {{ $fmt.humanifyNumber(allDailyData[selectedDay]?.outputTokens || 0) }} Tokens</span>
229
224
  </div>
230
225
  </div>
231
226
 
232
227
  <!-- Pie Charts for Selected Day -->
233
228
  <div v-if="allDailyData[selectedDay]?.requests && activeTab === 'cost' && selectedDay" class="mt-6 grid grid-cols-1 lg:grid-cols-2 gap-6">
234
229
  <!-- Model Pie Chart -->
235
- <div class="bg-white rounded-lg shadow p-6">
236
- <h3 class="text-lg font-semibold text-gray-900 mb-4">
230
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
231
+ <h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
237
232
  Cost by Model
238
233
  </h3>
239
234
  <div v-if="modelPieData.labels.length > 0" class="relative h-80">
240
235
  <canvas ref="modelPieCanvas"></canvas>
241
236
  </div>
242
- <div v-else class="flex items-center justify-center h-80 text-gray-500">
237
+ <div v-else class="flex items-center justify-center h-80 text-gray-500 dark:text-gray-400">
243
238
  <p>No data for selected day</p>
244
239
  </div>
245
240
  </div>
246
241
 
247
242
  <!-- Provider Pie Chart -->
248
- <div class="bg-white rounded-lg shadow p-6">
249
- <h3 class="text-lg font-semibold text-gray-900 mb-4">
243
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
244
+ <h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
250
245
  Cost by Provider
251
246
  </h3>
252
247
  <div v-if="providerPieData.labels.length > 0" class="relative h-80">
253
248
  <canvas ref="providerPieCanvas"></canvas>
254
249
  </div>
255
- <div v-else class="flex items-center justify-center h-80 text-gray-500">
250
+ <div v-else class="flex items-center justify-center h-80 text-gray-500 dark:text-gray-400">
256
251
  <p>No data for selected day</p>
257
252
  </div>
258
253
  </div>
@@ -261,39 +256,39 @@ export default {
261
256
  <!-- Token Pie Charts for Selected Day -->
262
257
  <div v-if="allDailyData[selectedDay]?.requests && activeTab === 'tokens' && selectedDay" class="mt-6 grid grid-cols-1 lg:grid-cols-2 gap-6">
263
258
  <!-- Token Model Pie Chart -->
264
- <div class="bg-white rounded-lg shadow p-6">
265
- <h3 class="text-lg font-semibold text-gray-900 mb-4">
259
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
260
+ <h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
266
261
  Tokens by Model
267
262
  </h3>
268
263
  <div v-if="tokenModelPieData.labels.length > 0" class="relative h-80">
269
264
  <canvas ref="tokenModelPieCanvas"></canvas>
270
265
  </div>
271
- <div v-else class="flex items-center justify-center h-80 text-gray-500">
266
+ <div v-else class="flex items-center justify-center h-80 text-gray-500 dark:text-gray-400">
272
267
  <p>No data for selected day</p>
273
268
  </div>
274
269
  </div>
275
270
 
276
271
  <!-- Token Provider Pie Chart -->
277
- <div class="bg-white rounded-lg shadow p-6">
278
- <h3 class="text-lg font-semibold text-gray-900 mb-4">
272
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
273
+ <h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
279
274
  Tokens by Provider
280
275
  </h3>
281
276
  <div v-if="tokenProviderPieData.labels.length > 0" class="relative h-80">
282
277
  <canvas ref="tokenProviderPieCanvas"></canvas>
283
278
  </div>
284
- <div v-else class="flex items-center justify-center h-80 text-gray-500">
279
+ <div v-else class="flex items-center justify-center h-80 text-gray-500 dark:text-gray-400">
285
280
  <p>No data for selected day</p>
286
281
  </div>
287
282
  </div>
288
283
  </div>
289
284
 
290
285
  <!-- Activity Tab - Full Page Layout -->
291
- <div v-if="activeTab === 'activity'" class="h-full flex flex-col bg-white">
286
+ <div v-if="activeTab === 'activity'" class="flex flex-col bg-white dark:bg-gray-800">
292
287
  <!-- Filters Bar -->
293
- <div class="flex-shrink-0 border-b border-gray-200 bg-white px-6 py-4">
294
- <div class="flex flex-wrap gap-4 items-end">
295
- <div class="flex flex-col">
296
- <select v-model="selectedModel" class="px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
288
+ <div class="flex-shrink-0 border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 px-3 sm:px-6 py-4">
289
+ <div class="flex flex-wrap gap-2 sm:gap-4 items-end">
290
+ <div class="flex flex-col flex-1 min-w-[120px] sm:flex-initial">
291
+ <select v-model="selectedModel" class="px-3 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 w-full">
297
292
  <option value="">All Models</option>
298
293
  <option v-for="model in filterOptions.models" :key="model" :value="model">
299
294
  {{ model }}
@@ -301,8 +296,8 @@ export default {
301
296
  </select>
302
297
  </div>
303
298
 
304
- <div class="flex flex-col">
305
- <select v-model="selectedProvider" class="px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
299
+ <div class="flex flex-col flex-1 min-w-[120px] sm:flex-initial">
300
+ <select v-model="selectedProvider" class="px-3 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 w-full">
306
301
  <option value="">All Providers</option>
307
302
  <option v-for="provider in filterOptions.providers" :key="provider" :value="provider">
308
303
  {{ provider }}
@@ -310,81 +305,85 @@ export default {
310
305
  </select>
311
306
  </div>
312
307
 
313
- <div class="flex flex-col">
314
- <select v-model="sortBy" class="px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
315
- <option value="created">Date (Newest)</option>
308
+ <div class="flex flex-col flex-1 min-w-[140px] sm:flex-initial">
309
+ <select v-model="sortBy" class="px-3 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 w-full">
310
+ <option value="createdAt">Date (Newest)</option>
316
311
  <option value="cost">Cost (Highest)</option>
317
312
  <option value="duration">Duration (Longest)</option>
318
313
  <option value="totalTokens">Tokens (Most)</option>
319
314
  </select>
320
315
  </div>
321
316
 
322
- <button v-if="hasActiveFilters" @click="clearActivityFilters" class="px-4 py-2 text-sm text-gray-600 hover:text-gray-900 border border-gray-300 rounded-md hover:bg-gray-100 transition-colors">
317
+ <button v-if="hasActiveFilters" @click="clearActivityFilters" class="px-4 py-2 text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors whitespace-nowrap">
323
318
  Clear Filters
324
319
  </button>
325
320
  </div>
326
321
  </div>
327
322
 
328
323
  <!-- Requests List with Infinite Scroll -->
329
- <div class="flex-1 overflow-y-auto" @scroll="onActivityScroll" ref="activityScrollContainer">
330
- <div v-if="isActivityLoading && activityRequests.length === 0" class="flex items-center justify-center h-full">
324
+ <div class="flex-1">
325
+ <div v-if="isActivityLoading && activityRequests.length === 0" class="mt-8 flex items-center justify-center h-full">
331
326
  <div class="text-center">
332
327
  <div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
333
- <p class="mt-4 text-gray-600">Loading requests...</p>
328
+ <p class="mt-4 text-gray-600 dark:text-gray-400">Loading requests...</p>
334
329
  </div>
335
330
  </div>
336
331
 
337
- <div v-else-if="activityRequests.length === 0" class="flex items-center justify-center h-full">
338
- <p class="text-gray-500">No requests found</p>
332
+ <div v-else-if="activityRequests.length === 0" class="mt-4 flex items-center justify-center h-full">
333
+ <p class="text-gray-500 dark:text-gray-400">No requests found</p>
339
334
  </div>
340
335
 
341
- <div v-else class="divide-y divide-gray-200">
342
- <div v-for="request in activityRequests" :key="request.id" class="px-6 py-4 hover:bg-gray-50 transition-colors">
343
- <div class="flex items-start justify-between gap-4">
344
- <div class="flex-1 min-w-0">
345
- <div class="flex justify-between">
346
- <div class="flex items-center gap-2 mb-2 flex-wrap">
347
- <span class="text-xs px-2 py-1 bg-blue-100 text-blue-800 rounded font-medium">{{ request.model }}</span>
348
- <span class="text-xs px-2 py-1 bg-purple-100 text-purple-800 rounded font-medium">{{ request.provider }}</span>
349
- <span v-if="request.providerRef" class="text-xs px-2 py-1 bg-green-100 text-green-800 rounded font-medium">{{ request.providerRef }}</span>
350
- <span v-if="request.finishReason" class="text-xs px-2 py-1 bg-gray-100 text-gray-800 rounded font-medium">{{ request.finishReason }}</span>
336
+ <div v-else class="divide-y divide-gray-200 dark:divide-gray-700">
337
+ <div v-for="request in activityRequests" :key="request.id" class="px-3 sm:px-6 py-4 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
338
+ <div class="flex flex-col lg:flex-row items-start justify-between gap-4">
339
+ <div class="flex-1 min-w-0 w-full">
340
+ <div class="flex flex-col sm:flex-row justify-between gap-2 mb-2">
341
+ <div class="flex items-center gap-2 flex-wrap">
342
+ <span v-if="request.model" class="text-xs px-2 py-1 bg-blue-100 dark:bg-blue-900/30 text-blue-800 dark:text-blue-300 rounded font-medium">{{ request.model }}</span>
343
+ <span v-if="request.provider" class="text-xs px-2 py-1 bg-purple-100 dark:bg-purple-900/30 text-purple-800 dark:text-purple-300 rounded font-medium">{{ request.provider }}</span>
344
+ <span v-if="request.providerRef" class="text-xs px-2 py-1 bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-300 rounded font-medium">{{ request.providerRef }}</span>
345
+ <span v-if="request.finishReason" class="text-xs px-2 py-1 bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-300 rounded font-medium">{{ request.finishReason }}</span>
351
346
  </div>
352
- <div class="text-xs text-gray-500">
353
- {{ formatActivityDate(request.created) }}
347
+ <div class="text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap">
348
+ {{ formatActivityDate(request.createdAt) }}
354
349
  </div>
355
350
  </div>
356
- <div class="text-sm font-semibold text-gray-900 truncate">
351
+ <div class="text-sm font-semibold text-gray-900 dark:text-gray-100 truncate mb-3">
357
352
  {{ request.title }}
358
353
  </div>
359
354
 
360
- <div class="grid grid-cols-2 md:grid-cols-5 gap-4 mt-3">
355
+ <div v-if="request.error" class="rounded-lg px-2 py-1 bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-800 text-red-800 dark:text-red-200 text-sm"
356
+ :title="request.error + '\\n' + (request.stacktrace || '')">
357
+ {{ request.error }}
358
+ </div>
359
+ <div v-else class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3 sm:gap-4">
361
360
  <div :title="request.cost">
362
- <div class="text-xs text-gray-500 font-medium">Cost</div>
363
- <div class="text-sm font-semibold text-gray-900">{{ formatCost(request.cost) }}</div>
361
+ <div class="text-xs text-gray-500 dark:text-gray-400 font-medium">Cost</div>
362
+ <div class="text-sm font-semibold text-gray-900 dark:text-gray-100">{{ $fmt.costLong(request.cost) }}</div>
364
363
  </div>
365
- <div>
366
- <div class="text-xs text-gray-500 font-medium">Tokens</div>
367
- <div class="text-sm font-semibold text-gray-900">
368
- {{ humanifyNumber(request.inputTokens) }} -> {{ humanifyNumber(request.outputTokens) }}
369
- <span v-if="request.inputCachedTokens" class="ml-1 text-xs text-gray-500">({{ humanifyNumber(request.inputCachedTokens) }} cached)</span>
364
+ <div class="col-span-2 sm:col-span-1">
365
+ <div class="text-xs text-gray-500 dark:text-gray-400 font-medium">Tokens</div>
366
+ <div v-if="request.inputTokens || request.outputTokens" class="text-sm font-semibold text-gray-900 dark:text-gray-100">
367
+ {{ $fmt.humanifyNumber(request.inputTokens || 0) }} -> {{ $fmt.humanifyNumber(request.outputTokens || 0) }}
368
+ <span v-if="request.inputCachedTokens" class="ml-1 text-xs text-gray-500 dark:text-gray-400">({{ $fmt.humanifyNumber(request.inputCachedTokens || 0) }} cached)</span>
370
369
  </div>
371
370
  </div>
372
371
  <div>
373
- <div class="text-xs text-gray-500 font-medium">Duration</div>
374
- <div class="text-sm font-semibold text-gray-900">{{ request.duration ? humanifyMs(request.duration) : '—' }}</div>
372
+ <div class="text-xs text-gray-500 dark:text-gray-400 font-medium">Duration</div>
373
+ <div v-if="request.duration" class="text-sm font-semibold text-gray-900 dark:text-gray-100">{{ $fmt.humanifyMs(request.duration * 1000) }}</div>
375
374
  </div>
376
375
  <div>
377
- <div class="text-xs text-gray-500 font-medium">Speed</div>
378
- <div class="text-sm font-semibold text-gray-900">{{ request.duration && request.outputTokens ? (request.outputTokens / (request.duration / 1000)).toFixed(1) + ' tok/s' : '—' }}</div>
376
+ <div class="text-xs text-gray-500 dark:text-gray-400 font-medium">Speed</div>
377
+ <div v-if="request.duration && request.outputTokens" class="text-sm font-semibold text-gray-900 dark:text-gray-100">{{ (request.outputTokens / (request.duration / 1000)).toFixed(1) + ' tok/s' }}</div>
379
378
  </div>
380
379
  </div>
381
380
  </div>
382
- <div class="flex flex-col gap-2">
383
- <button type="button" v-if="threadExists(request.threadId)" @click="openThread(request.threadId)" class="flex-shrink-0 px-4 py-2 text-sm font-medium text-blue-600 hover:text-blue-800 border border-blue-300 rounded hover:bg-blue-50 transition-colors whitespace-nowrap">
384
- View<span class="hidden lg:inline"> Thread</span>
381
+ <div class="flex flex-row lg:flex-col gap-2 w-full lg:w-auto">
382
+ <button type="button" v-if="threadExists(request.threadId)" @click="openThread(request.threadId)" class="flex-1 lg:flex-initial px-3 sm:px-4 py-2 text-sm font-medium text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 border border-blue-300 dark:border-blue-600 rounded hover:bg-blue-50 dark:hover:bg-blue-900/30 transition-colors whitespace-nowrap">
383
+ View<span class="hidden sm:inline"> Thread</span>
385
384
  </button>
386
- <button type="button" @click="deleteRequestLog(request.id)" class="flex-shrink-0 px-4 py-2 text-sm font-medium text-red-600 hover:text-red-800 border border-red-300 rounded hover:bg-red-50 transition-colors whitespace-nowrap">
387
- Delete<span class="hidden lg:inline"> Request</span>
385
+ <button type="button" @click="deleteRequestLog(request.id)" class="flex-1 lg:flex-initial px-3 sm:px-4 py-2 text-sm font-medium text-red-600 dark:text-red-500 hover:text-red-800 dark:hover:text-red-400 border border-red-300 dark:border-red-600 rounded hover:bg-red-50 dark:hover:bg-red-900/30 transition-colors whitespace-nowrap">
386
+ Delete<span class="hidden sm:inline"> Request</span>
388
387
  </button>
389
388
  </div>
390
389
  </div>
@@ -394,10 +393,11 @@ export default {
394
393
  <div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto"></div>
395
394
  </div>
396
395
 
397
- <div v-if="!activityHasMore && activityRequests.length > 0" class="px-6 py-8 text-center text-gray-500 text-sm">
396
+ <div v-if="!activityHasMore && activityRequests.length > 0" class="px-6 py-8 text-center text-gray-500 dark:text-gray-400 text-sm">
398
397
  No more requests to load
399
398
  </div>
400
399
  </div>
400
+ <div ref="scrollSentinel" class="h-4 w-full"></div>
401
401
  </div>
402
402
  </div>
403
403
  </div>
@@ -405,10 +405,10 @@ export default {
405
405
  </div>
406
406
  `,
407
407
  setup() {
408
+ const ctx = inject('ctx')
408
409
  const router = useRouter()
409
410
  const route = useRoute()
410
- const threads = useThreadStore()
411
- const { initDB } = threads
411
+ const analyticsData = ref()
412
412
 
413
413
  // Initialize activeTab from URL query parameter, default to 'cost'
414
414
  const activeTab = ref(route.query.tab || 'cost')
@@ -434,6 +434,9 @@ export default {
434
434
  const selectedYear = computed(() => {
435
435
  return route.query.year !== undefined ? parseInt(route.query.year) : currentDate.getFullYear()
436
436
  })
437
+ const selectedYearMonth = computed(() => {
438
+ return `${selectedYear.value}-${selectedMonth.value < 10 ? '0' + selectedMonth.value : selectedMonth.value}`
439
+ })
437
440
  const allDailyData = ref({}) // Store all data for filtering
438
441
 
439
442
  // Selected day - read from URL, default to today
@@ -529,52 +532,18 @@ export default {
529
532
 
530
533
  const selectedModel = ref('')
531
534
  const selectedProvider = ref('')
532
- const sortBy = ref('created')
535
+ const sortBy = ref('createdAt')
533
536
  const filterOptions = ref({ models: [], providers: [] })
534
- const activityScrollContainer = ref(null)
537
+ const scrollSentinel = ref(null)
538
+ let observer = null
535
539
 
536
540
  const hasActiveFilters = computed(() => selectedModel.value || selectedProvider.value)
537
541
 
538
542
  async function loadAnalyticsData() {
539
543
  try {
540
- const db = await initDB()
541
- const tx = db.transaction(['requests'], 'readonly')
542
- const store = tx.objectStore('requests')
543
- const allRequests = await store.getAll()
544
-
545
544
  // Group requests by date
546
- const dailyData = {}
547
- let totalCostSum = 0
548
- let totalInputSum = 0
549
- let totalOutputSum = 0
550
- const yearsSet = new Set()
551
-
552
- allRequests.forEach(req => {
553
- const date = new Date(req.created * 1000)
554
- const dateKey = date.toISOString().split('T')[0] // YYYY-MM-DD
555
- yearsSet.add(date.getFullYear())
556
-
557
- if (!dailyData[dateKey]) {
558
- dailyData[dateKey] = {
559
- cost: 0,
560
- requests: 0,
561
- inputTokens: 0,
562
- outputTokens: 0
563
- }
564
- }
565
-
566
- dailyData[dateKey].cost += req.cost || 0
567
- dailyData[dateKey].requests += 1
568
- dailyData[dateKey].inputTokens += req.inputTokens || 0
569
- dailyData[dateKey].outputTokens += req.outputTokens || 0
570
-
571
- totalCostSum += req.cost || 0
572
- totalInputSum += req.inputTokens || 0
573
- totalOutputSum += req.outputTokens || 0
574
- })
575
-
576
- // Store all daily data for filtering
577
- allDailyData.value = dailyData
545
+ analyticsData.value = await ctx.requests.getSummary()
546
+ allDailyData.value = analyticsData.value.dailyData
578
547
 
579
548
  // Update chart data based on selected month/year
580
549
  updateChartData()
@@ -651,36 +620,8 @@ export default {
651
620
  }
652
621
 
653
622
  try {
654
- const db = await initDB()
655
- const tx = db.transaction(['requests'], 'readonly')
656
- const store = tx.objectStore('requests')
657
- const allRequests = await store.getAll()
658
-
659
- // Filter requests for the selected day
660
- const dayStart = Math.floor(new Date(dateKey + 'T00:00:00Z').getTime() / 1000)
661
- const dayEnd = Math.floor(new Date(dateKey + 'T23:59:59Z').getTime() / 1000)
662
-
663
- const dayRequests = allRequests.filter(req => req.created >= dayStart && req.created <= dayEnd)
664
-
665
- // Aggregate by model
666
- const modelData = {}
667
- const providerData = {}
668
-
669
- dayRequests.forEach(req => {
670
- // Model aggregation
671
- if (!modelData[req.model]) {
672
- modelData[req.model] = { cost: 0, count: 0 }
673
- }
674
- modelData[req.model].cost += req.cost || 0
675
- modelData[req.model].count += 1
676
-
677
- // Provider aggregation
678
- if (!providerData[req.provider]) {
679
- providerData[req.provider] = { cost: 0, count: 0 }
680
- }
681
- providerData[req.provider].cost += req.cost || 0
682
- providerData[req.provider].count += 1
683
- })
623
+ const dailySummary = await ctx.requests.getDailySummary(dateKey)
624
+ const { modelData, providerData } = dailySummary
684
625
 
685
626
  // Prepare model pie chart data
686
627
  const modelLabels = Object.keys(modelData).sort()
@@ -724,38 +665,8 @@ export default {
724
665
  }
725
666
 
726
667
  try {
727
- const db = await initDB()
728
- const tx = db.transaction(['requests'], 'readonly')
729
- const store = tx.objectStore('requests')
730
- const allRequests = await store.getAll()
731
-
732
- // Filter requests for the selected day
733
- const dayStart = Math.floor(new Date(dateKey + 'T00:00:00Z').getTime() / 1000)
734
- const dayEnd = Math.floor(new Date(dateKey + 'T23:59:59Z').getTime() / 1000)
735
-
736
- const dayRequests = allRequests.filter(req => req.created >= dayStart && req.created <= dayEnd)
737
-
738
- // Aggregate by model and provider (using tokens)
739
- const modelData = {}
740
- const providerData = {}
741
-
742
- dayRequests.forEach(req => {
743
- const totalTokens = (req.inputTokens || 0) + (req.outputTokens || 0)
744
-
745
- // Model aggregation
746
- if (!modelData[req.model]) {
747
- modelData[req.model] = { tokens: 0, count: 0 }
748
- }
749
- modelData[req.model].tokens += totalTokens
750
- modelData[req.model].count += 1
751
-
752
- // Provider aggregation
753
- if (!providerData[req.provider]) {
754
- providerData[req.provider] = { tokens: 0, count: 0 }
755
- }
756
- providerData[req.provider].tokens += totalTokens
757
- providerData[req.provider].count += 1
758
- })
668
+ const dailySummary = await ctx.requests.getDailySummary(dateKey)
669
+ const { modelData, providerData } = dailySummary
759
670
 
760
671
  // Prepare model pie chart data
761
672
  const modelLabels = Object.keys(modelData).sort()
@@ -799,7 +710,7 @@ export default {
799
710
  costChartInstance.destroy()
800
711
  }
801
712
 
802
- const ctx = costChartCanvas.value.getContext('2d')
713
+ const ctx2d = costChartCanvas.value.getContext('2d')
803
714
  const chartTypeValue = costChartType.value
804
715
 
805
716
  // Find the index of the selected day
@@ -830,7 +741,7 @@ export default {
830
741
  }]
831
742
  }
832
743
 
833
- costChartInstance = new Chart(ctx, {
744
+ costChartInstance = new Chart(ctx2d, {
834
745
  type: chartTypeValue,
835
746
  data: chartDataWithColors,
836
747
  options: {
@@ -856,14 +767,14 @@ export default {
856
767
  },
857
768
  tooltip: {
858
769
  callbacks: {
859
- title: function(context) {
770
+ title: function (context) {
860
771
  const index = context[0].dataIndex
861
772
  const dateKey = chartData.value.dateKeys[index]
862
773
  const date = new Date(dateKey + 'T00:00:00Z')
863
774
  return date.toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' })
864
775
  },
865
- label: function(context) {
866
- return `Cost: ${formatCost(context.parsed.y)}`
776
+ label: function (context) {
777
+ return `Cost: ${ctx.fmt.cost(context.parsed.y)}`
867
778
  }
868
779
  }
869
780
  }
@@ -872,7 +783,7 @@ export default {
872
783
  y: {
873
784
  beginAtZero: true,
874
785
  ticks: {
875
- callback: function(value) {
786
+ callback: function (value) {
876
787
  return '$' + value.toFixed(4)
877
788
  }
878
789
  }
@@ -890,7 +801,7 @@ export default {
890
801
  tokenChartInstance.destroy()
891
802
  }
892
803
 
893
- const ctx = tokenChartCanvas.value.getContext('2d')
804
+ const ctx2d = tokenChartCanvas.value.getContext('2d')
894
805
 
895
806
  // Find the index of the selected day
896
807
  const selectedDayIndex = tokenChartData.value.dateKeys.indexOf(selectedDay.value)
@@ -941,7 +852,7 @@ export default {
941
852
  ]
942
853
  }
943
854
 
944
- tokenChartInstance = new Chart(ctx, {
855
+ tokenChartInstance = new Chart(ctx2d, {
945
856
  type: 'bar',
946
857
  data: chartDataWithColors,
947
858
  options: {
@@ -969,8 +880,8 @@ export default {
969
880
  stacked: true,
970
881
  beginAtZero: true,
971
882
  ticks: {
972
- callback: function(value) {
973
- return humanifyNumber(value)
883
+ callback: function (value) {
884
+ return ctx.fmt.humanifyNumber(value)
974
885
  }
975
886
  }
976
887
  }
@@ -982,14 +893,14 @@ export default {
982
893
  },
983
894
  tooltip: {
984
895
  callbacks: {
985
- title: function(context) {
896
+ title: function (context) {
986
897
  const index = context[0].dataIndex
987
898
  const dateKey = tokenChartData.value.dateKeys[index]
988
899
  const date = new Date(dateKey + 'T00:00:00Z')
989
900
  return date.toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' })
990
901
  },
991
- label: function(context) {
992
- return `${context.dataset.label}: ${humanifyNumber(context.parsed.y)}`
902
+ label: function (context) {
903
+ return `${context.dataset.label}: ${ctx.fmt.humanifyNumber(context.parsed.y)}`
993
904
  }
994
905
  }
995
906
  }
@@ -1006,7 +917,7 @@ export default {
1006
917
  modelPieChartInstance.destroy()
1007
918
  }
1008
919
 
1009
- const ctx = modelPieCanvas.value.getContext('2d')
920
+ const ctx2d = modelPieCanvas.value.getContext('2d')
1010
921
 
1011
922
  // Custom plugin to draw percentage labels on pie slices
1012
923
  const percentagePlugin = {
@@ -1021,7 +932,9 @@ export default {
1021
932
 
1022
933
  // Only display label if percentage > 1%
1023
934
  if (parseFloat(percentage) > 1) {
1024
- chartCtx.fillStyle = '#000'
935
+ // Use white color in dark mode, black in light mode
936
+ const isDarkMode = document.documentElement.classList.contains('dark')
937
+ chartCtx.fillStyle = isDarkMode ? '#fff' : '#000'
1025
938
  chartCtx.font = 'bold 12px Arial'
1026
939
  chartCtx.textAlign = 'center'
1027
940
  chartCtx.textBaseline = 'middle'
@@ -1031,7 +944,7 @@ export default {
1031
944
  }
1032
945
  }
1033
946
 
1034
- modelPieChartInstance = new Chart(ctx, {
947
+ modelPieChartInstance = new Chart(ctx2d, {
1035
948
  type: 'pie',
1036
949
  data: modelPieData.value,
1037
950
  options: {
@@ -1044,8 +957,8 @@ export default {
1044
957
  },
1045
958
  tooltip: {
1046
959
  callbacks: {
1047
- label: function(context) {
1048
- return `${context.label}: ${formatCost(context.parsed)}`
960
+ label: function (context) {
961
+ return `${context.label}: ${ctx.fmt.cost(context.parsed)}`
1049
962
  }
1050
963
  }
1051
964
  }
@@ -1063,7 +976,7 @@ export default {
1063
976
  providerPieChartInstance.destroy()
1064
977
  }
1065
978
 
1066
- const ctx = providerPieCanvas.value.getContext('2d')
979
+ const ctx2d = providerPieCanvas.value.getContext('2d')
1067
980
 
1068
981
  // Custom plugin to draw percentage labels on pie slices
1069
982
  const percentagePlugin = {
@@ -1078,7 +991,9 @@ export default {
1078
991
 
1079
992
  // Only display label if percentage > 1%
1080
993
  if (parseFloat(percentage) > 1) {
1081
- chartCtx.fillStyle = '#000'
994
+ // Use white color in dark mode, black in light mode
995
+ const isDarkMode = document.documentElement.classList.contains('dark')
996
+ chartCtx.fillStyle = isDarkMode ? '#fff' : '#000'
1082
997
  chartCtx.font = 'bold 12px Arial'
1083
998
  chartCtx.textAlign = 'center'
1084
999
  chartCtx.textBaseline = 'middle'
@@ -1088,7 +1003,7 @@ export default {
1088
1003
  }
1089
1004
  }
1090
1005
 
1091
- providerPieChartInstance = new Chart(ctx, {
1006
+ providerPieChartInstance = new Chart(ctx2d, {
1092
1007
  type: 'pie',
1093
1008
  data: providerPieData.value,
1094
1009
  options: {
@@ -1101,8 +1016,8 @@ export default {
1101
1016
  },
1102
1017
  tooltip: {
1103
1018
  callbacks: {
1104
- label: function(context) {
1105
- return `${context.label}: ${formatCost(context.parsed)}`
1019
+ label: function (context) {
1020
+ return `${context.label}: ${ctx.fmt.cost(context.parsed)}`
1106
1021
  }
1107
1022
  }
1108
1023
  }
@@ -1120,7 +1035,7 @@ export default {
1120
1035
  tokenModelPieChartInstance.destroy()
1121
1036
  }
1122
1037
 
1123
- const ctx = tokenModelPieCanvas.value.getContext('2d')
1038
+ const ctx2d = tokenModelPieCanvas.value.getContext('2d')
1124
1039
 
1125
1040
  // Custom plugin to draw percentage labels on pie slices
1126
1041
  const percentagePlugin = {
@@ -1135,7 +1050,9 @@ export default {
1135
1050
 
1136
1051
  // Only display label if percentage > 1%
1137
1052
  if (parseFloat(percentage) > 1) {
1138
- chartCtx.fillStyle = '#000'
1053
+ // Use white color in dark mode, black in light mode
1054
+ const isDarkMode = document.documentElement.classList.contains('dark')
1055
+ chartCtx.fillStyle = isDarkMode ? '#fff' : '#000'
1139
1056
  chartCtx.font = 'bold 12px Arial'
1140
1057
  chartCtx.textAlign = 'center'
1141
1058
  chartCtx.textBaseline = 'middle'
@@ -1145,7 +1062,7 @@ export default {
1145
1062
  }
1146
1063
  }
1147
1064
 
1148
- tokenModelPieChartInstance = new Chart(ctx, {
1065
+ tokenModelPieChartInstance = new Chart(ctx2d, {
1149
1066
  type: 'pie',
1150
1067
  data: tokenModelPieData.value,
1151
1068
  options: {
@@ -1158,8 +1075,8 @@ export default {
1158
1075
  },
1159
1076
  tooltip: {
1160
1077
  callbacks: {
1161
- label: function(context) {
1162
- return `${context.label}: ${humanifyNumber(context.parsed)}`
1078
+ label: function (context) {
1079
+ return `${context.label}: ${ctx.fmt.humanifyNumber(context.parsed)}`
1163
1080
  }
1164
1081
  }
1165
1082
  }
@@ -1177,7 +1094,7 @@ export default {
1177
1094
  tokenProviderPieChartInstance.destroy()
1178
1095
  }
1179
1096
 
1180
- const ctx = tokenProviderPieCanvas.value.getContext('2d')
1097
+ const ctx2d = tokenProviderPieCanvas.value.getContext('2d')
1181
1098
 
1182
1099
  // Custom plugin to draw percentage labels on pie slices
1183
1100
  const percentagePlugin = {
@@ -1192,7 +1109,9 @@ export default {
1192
1109
 
1193
1110
  // Only display label if percentage > 1%
1194
1111
  if (parseFloat(percentage) > 1) {
1195
- chartCtx.fillStyle = '#000'
1112
+ // Use white color in dark mode, black in light mode
1113
+ const isDarkMode = document.documentElement.classList.contains('dark')
1114
+ chartCtx.fillStyle = isDarkMode ? '#fff' : '#000'
1196
1115
  chartCtx.font = 'bold 12px Arial'
1197
1116
  chartCtx.textAlign = 'center'
1198
1117
  chartCtx.textBaseline = 'middle'
@@ -1202,7 +1121,7 @@ export default {
1202
1121
  }
1203
1122
  }
1204
1123
 
1205
- tokenProviderPieChartInstance = new Chart(ctx, {
1124
+ tokenProviderPieChartInstance = new Chart(ctx2d, {
1206
1125
  type: 'pie',
1207
1126
  data: tokenProviderPieData.value,
1208
1127
  options: {
@@ -1215,8 +1134,8 @@ export default {
1215
1134
  },
1216
1135
  tooltip: {
1217
1136
  callbacks: {
1218
- label: function(context) {
1219
- return `${context.label}: ${humanifyNumber(context.parsed)}`
1137
+ label: function (context) {
1138
+ return `${context.label}: ${ctx.fmt.humanifyNumber(context.parsed)}`
1220
1139
  }
1221
1140
  }
1222
1141
  }
@@ -1229,7 +1148,7 @@ export default {
1229
1148
  // Activity tab functions
1230
1149
  const loadActivityFilterOptions = async () => {
1231
1150
  try {
1232
- filterOptions.value = await threads.getFilterOptions()
1151
+ filterOptions.value = await ctx.requests.getFilterOptions()
1233
1152
  } catch (error) {
1234
1153
  console.error('Failed to load filter options:', error)
1235
1154
  }
@@ -1237,24 +1156,9 @@ export default {
1237
1156
 
1238
1157
  const loadExistingThreadIds = async () => {
1239
1158
  try {
1240
- // Calculate date range for selected month/year
1241
- const startDate = new Date(selectedYear.value, selectedMonth.value - 1, 1)
1242
- const endDate = new Date(selectedYear.value, selectedMonth.value, 0, 23, 59, 59)
1243
-
1244
- // Convert to timestamp strings (threadId format)
1245
- const startThreadId = startDate.getTime().toString()
1246
- const endThreadId = endDate.getTime().toString()
1247
-
1248
- const db = await initDB()
1249
- const tx = db.transaction(['threads'], 'readonly')
1250
- const store = tx.objectStore('threads')
1251
-
1252
- // Use IDBKeyRange to only load threads within the month's timestamp range
1253
- const range = IDBKeyRange.bound(startThreadId, endThreadId)
1254
- const monthThreads = await store.getAll(range)
1255
-
1256
- // Create a Set of existing thread IDs for the month
1257
- existingThreadIds.value = new Set(monthThreads.map(thread => thread.id))
1159
+ existingThreadIds.value = new Set(await ctx.requests.getThreadIds({
1160
+ month: selectedYearMonth.value,
1161
+ }))
1258
1162
  } catch (error) {
1259
1163
  console.error('Failed to load existing thread IDs:', error)
1260
1164
  existingThreadIds.value = new Set()
@@ -1277,28 +1181,24 @@ export default {
1277
1181
  }
1278
1182
 
1279
1183
  try {
1280
- // Calculate date range for selected month/year
1281
- const startDate = new Date(selectedYear.value, selectedMonth.value - 1, 1)
1282
- const endDate = new Date(selectedYear.value, selectedMonth.value, 0, 23, 59, 59)
1283
-
1284
- const filters = {
1285
- model: selectedModel.value || null,
1286
- provider: selectedProvider.value || null,
1287
- sortBy: sortBy.value,
1288
- sortOrder: 'desc',
1289
- startDate: Math.floor(startDate.getTime() / 1000),
1290
- endDate: Math.floor(endDate.getTime() / 1000)
1291
- }
1184
+ const requests = await ctx.requests.query({
1185
+ model: selectedModel.value || undefined,
1186
+ provider: selectedProvider.value || undefined,
1187
+ sort: `-${sortBy.value}`,
1188
+ take: activityPageSize,
1189
+ skip: activityOffset.value,
1190
+ month: selectedYearMonth.value,
1191
+ })
1292
1192
 
1293
- const result = await threads.getRequests(filters, activityPageSize, activityOffset.value)
1193
+ const hasMore = requests.length >= activityPageSize
1294
1194
 
1295
1195
  if (reset) {
1296
- activityRequests.value = result.requests
1196
+ activityRequests.value = requests
1297
1197
  } else {
1298
- activityRequests.value.push(...result.requests)
1198
+ activityRequests.value.push(...requests)
1299
1199
  }
1300
1200
 
1301
- activityHasMore.value = result.hasMore
1201
+ activityHasMore.value = hasMore
1302
1202
  activityOffset.value += activityPageSize
1303
1203
  } catch (error) {
1304
1204
  console.error('Failed to load requests:', error)
@@ -1308,29 +1208,32 @@ export default {
1308
1208
  }
1309
1209
  }
1310
1210
 
1311
- const onActivityScroll = async () => {
1312
- if (!activityScrollContainer.value) return
1211
+ const setupObserver = () => {
1212
+ if (observer) observer.disconnect()
1313
1213
 
1314
- const { scrollTop, scrollHeight, clientHeight } = activityScrollContainer.value
1315
- const isNearBottom = scrollHeight - scrollTop - clientHeight < 200
1214
+ observer = new IntersectionObserver((entries) => {
1215
+ if (entries[0].isIntersecting && activityHasMore.value && !isActivityLoadingMore.value && !isActivityLoading.value) {
1216
+ loadActivityRequests(false)
1217
+ }
1218
+ }, { rootMargin: '200px' })
1316
1219
 
1317
- if (isNearBottom && activityHasMore.value && !isActivityLoadingMore.value && !isActivityLoading.value) {
1318
- await loadActivityRequests(false)
1220
+ if (scrollSentinel.value) {
1221
+ observer.observe(scrollSentinel.value)
1319
1222
  }
1320
1223
  }
1321
1224
 
1322
1225
  const clearActivityFilters = async () => {
1323
1226
  selectedModel.value = ''
1324
1227
  selectedProvider.value = ''
1325
- sortBy.value = 'created'
1228
+ sortBy.value = 'createdAt'
1326
1229
  await loadActivityRequests(true)
1327
1230
  }
1328
1231
 
1329
- const formatActivityDate = (timestamp) => {
1330
- const date = new Date(timestamp * 1000)
1331
- return date.toLocaleTimeString(undefined, { hour12: false }) + ' '
1332
- + date.toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' })
1333
-
1232
+ const formatActivityDate = (d) => {
1233
+ const date = new Date(d)
1234
+ return date.toLocaleTimeString(undefined, { hour12: false }) + ' '
1235
+ + date.toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' })
1236
+
1334
1237
  }
1335
1238
 
1336
1239
  const openThread = (threadId) => {
@@ -1338,17 +1241,12 @@ export default {
1338
1241
  }
1339
1242
 
1340
1243
  const deleteRequestLog = async (requestId) => {
1341
- if (confirm('Are you sure you want to delete this request log?')) {
1342
- try {
1343
- await threads.deleteRequest(requestId)
1344
- // Remove from the list
1345
- activityRequests.value = activityRequests.value.filter(r => r.id !== requestId)
1346
- // Reload analytics data
1347
- await loadAnalyticsData()
1348
- } catch (error) {
1349
- console.error('Failed to delete request:', error)
1350
- alert('Failed to delete request')
1351
- }
1244
+ if (confirm(`Are you sure you want to delete this request log ${requestId}?`)) {
1245
+ await ctx.requests.deleteById(requestId)
1246
+ // Remove from the list
1247
+ activityRequests.value = activityRequests.value.filter(r => r.id !== requestId)
1248
+ // Reload analytics data
1249
+ await loadAnalyticsData()
1352
1250
  }
1353
1251
  }
1354
1252
 
@@ -1431,6 +1329,8 @@ export default {
1431
1329
  } else if (newTab === 'activity') {
1432
1330
  await loadActivityFilterOptions()
1433
1331
  await loadActivityRequests(true)
1332
+ await nextTick()
1333
+ setupObserver()
1434
1334
  }
1435
1335
  })
1436
1336
 
@@ -1463,9 +1363,15 @@ export default {
1463
1363
  if (activeTab.value === 'activity') {
1464
1364
  await loadActivityFilterOptions()
1465
1365
  await loadActivityRequests(true)
1366
+ await nextTick()
1367
+ setupObserver()
1466
1368
  }
1467
1369
  })
1468
1370
 
1371
+ onUnmounted(() => {
1372
+ if (observer) observer.disconnect()
1373
+ })
1374
+
1469
1375
  return {
1470
1376
  activeTab,
1471
1377
  costChartType,
@@ -1486,9 +1392,6 @@ export default {
1486
1392
  totalRequests,
1487
1393
  totalInputTokens,
1488
1394
  totalOutputTokens,
1489
- formatCost,
1490
- humanifyNumber,
1491
- humanifyMs,
1492
1395
  // Month/Year selection
1493
1396
  selectedMonth,
1494
1397
  selectedYear,
@@ -1503,8 +1406,8 @@ export default {
1503
1406
  sortBy,
1504
1407
  filterOptions,
1505
1408
  hasActiveFilters,
1506
- activityScrollContainer,
1507
- onActivityScroll,
1409
+ hasActiveFilters,
1410
+ scrollSentinel,
1508
1411
  clearActivityFilters,
1509
1412
  formatActivityDate,
1510
1413
  threadExists,
@@ -1515,3 +1418,27 @@ export default {
1515
1418
  }
1516
1419
  }
1517
1420
  }
1421
+
1422
+ export default {
1423
+ order: 20 - 100,
1424
+
1425
+ install(ctx) {
1426
+ ctx.components({
1427
+ MonthSelector,
1428
+ Analytics,
1429
+ })
1430
+
1431
+ ctx.setLeftIcons({
1432
+ analytics: {
1433
+ component: {
1434
+ template: `<svg @click="$ctx.togglePath('/analytics')" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M5 22a1 1 0 0 1-1-1v-8a1 1 0 0 1 2 0v8a1 1 0 0 1-1 1m5 0a1 1 0 0 1-1-1V3a1 1 0 0 1 2 0v18a1 1 0 0 1-1 1m5 0a1 1 0 0 1-1-1V9a1 1 0 0 1 2 0v12a1 1 0 0 1-1 1m5 0a1 1 0 0 1-1-1v-4a1 1 0 0 1 2 0v4a1 1 0 0 1-1 1"/></svg>`
1435
+ },
1436
+ isActive({ path }) {
1437
+ return path === '/analytics'
1438
+ }
1439
+ }
1440
+ })
1441
+
1442
+ ctx.routes.push({ path: '/analytics', component: Analytics, meta: { title: 'Analytics' } })
1443
+ }
1444
+ }