llms-py 2.0.34__py3-none-any.whl → 3.0.0__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/__pycache__/__init__.cpython-312.pyc +0 -0
  3. llms/__pycache__/__init__.cpython-313.pyc +0 -0
  4. llms/__pycache__/__init__.cpython-314.pyc +0 -0
  5. llms/__pycache__/__main__.cpython-312.pyc +0 -0
  6. llms/__pycache__/__main__.cpython-314.pyc +0 -0
  7. llms/__pycache__/llms.cpython-312.pyc +0 -0
  8. llms/__pycache__/main.cpython-312.pyc +0 -0
  9. llms/__pycache__/main.cpython-313.pyc +0 -0
  10. llms/__pycache__/main.cpython-314.pyc +0 -0
  11. llms/__pycache__/plugins.cpython-314.pyc +0 -0
  12. llms/{ui/Analytics.mjs → extensions/analytics/ui/index.mjs} +154 -238
  13. llms/extensions/app/README.md +20 -0
  14. llms/extensions/app/__init__.py +530 -0
  15. llms/extensions/app/__pycache__/__init__.cpython-314.pyc +0 -0
  16. llms/extensions/app/__pycache__/db.cpython-314.pyc +0 -0
  17. llms/extensions/app/__pycache__/db_manager.cpython-314.pyc +0 -0
  18. llms/extensions/app/db.py +644 -0
  19. llms/extensions/app/db_manager.py +195 -0
  20. llms/extensions/app/requests.json +9073 -0
  21. llms/extensions/app/threads.json +15290 -0
  22. llms/{ui → extensions/app/ui}/Recents.mjs +91 -65
  23. llms/{ui/Sidebar.mjs → extensions/app/ui/index.mjs} +124 -58
  24. llms/extensions/app/ui/threadStore.mjs +411 -0
  25. llms/extensions/core_tools/CALCULATOR.md +32 -0
  26. llms/extensions/core_tools/__init__.py +598 -0
  27. llms/extensions/core_tools/__pycache__/__init__.cpython-314.pyc +0 -0
  28. llms/extensions/core_tools/ui/codemirror/addon/edit/closebrackets.js +201 -0
  29. llms/extensions/core_tools/ui/codemirror/addon/edit/closetag.js +185 -0
  30. llms/extensions/core_tools/ui/codemirror/addon/edit/continuelist.js +101 -0
  31. llms/extensions/core_tools/ui/codemirror/addon/edit/matchbrackets.js +160 -0
  32. llms/extensions/core_tools/ui/codemirror/addon/edit/matchtags.js +66 -0
  33. llms/extensions/core_tools/ui/codemirror/addon/edit/trailingspace.js +27 -0
  34. llms/extensions/core_tools/ui/codemirror/addon/selection/active-line.js +72 -0
  35. llms/extensions/core_tools/ui/codemirror/addon/selection/mark-selection.js +119 -0
  36. llms/extensions/core_tools/ui/codemirror/addon/selection/selection-pointer.js +98 -0
  37. llms/extensions/core_tools/ui/codemirror/doc/docs.css +225 -0
  38. llms/extensions/core_tools/ui/codemirror/doc/source_sans.woff +0 -0
  39. llms/extensions/core_tools/ui/codemirror/lib/codemirror.css +344 -0
  40. llms/extensions/core_tools/ui/codemirror/lib/codemirror.js +9884 -0
  41. llms/extensions/core_tools/ui/codemirror/mode/clike/clike.js +942 -0
  42. llms/extensions/core_tools/ui/codemirror/mode/javascript/index.html +118 -0
  43. llms/extensions/core_tools/ui/codemirror/mode/javascript/javascript.js +962 -0
  44. llms/extensions/core_tools/ui/codemirror/mode/javascript/typescript.html +62 -0
  45. llms/extensions/core_tools/ui/codemirror/mode/python/python.js +402 -0
  46. llms/extensions/core_tools/ui/codemirror/theme/dracula.css +40 -0
  47. llms/extensions/core_tools/ui/codemirror/theme/mocha.css +135 -0
  48. llms/extensions/core_tools/ui/index.mjs +650 -0
  49. llms/extensions/gallery/README.md +61 -0
  50. llms/extensions/gallery/__init__.py +61 -0
  51. llms/extensions/gallery/__pycache__/__init__.cpython-314.pyc +0 -0
  52. llms/extensions/gallery/__pycache__/db.cpython-314.pyc +0 -0
  53. llms/extensions/gallery/db.py +298 -0
  54. llms/extensions/gallery/ui/index.mjs +482 -0
  55. llms/extensions/katex/README.md +39 -0
  56. llms/extensions/katex/__init__.py +6 -0
  57. llms/extensions/katex/__pycache__/__init__.cpython-314.pyc +0 -0
  58. llms/extensions/katex/ui/README.md +125 -0
  59. llms/extensions/katex/ui/contrib/auto-render.js +338 -0
  60. llms/extensions/katex/ui/contrib/auto-render.min.js +1 -0
  61. llms/extensions/katex/ui/contrib/auto-render.mjs +244 -0
  62. llms/extensions/katex/ui/contrib/copy-tex.js +127 -0
  63. llms/extensions/katex/ui/contrib/copy-tex.min.js +1 -0
  64. llms/extensions/katex/ui/contrib/copy-tex.mjs +105 -0
  65. llms/extensions/katex/ui/contrib/mathtex-script-type.js +109 -0
  66. llms/extensions/katex/ui/contrib/mathtex-script-type.min.js +1 -0
  67. llms/extensions/katex/ui/contrib/mathtex-script-type.mjs +24 -0
  68. llms/extensions/katex/ui/contrib/mhchem.js +3213 -0
  69. llms/extensions/katex/ui/contrib/mhchem.min.js +1 -0
  70. llms/extensions/katex/ui/contrib/mhchem.mjs +3109 -0
  71. llms/extensions/katex/ui/contrib/render-a11y-string.js +887 -0
  72. llms/extensions/katex/ui/contrib/render-a11y-string.min.js +1 -0
  73. llms/extensions/katex/ui/contrib/render-a11y-string.mjs +800 -0
  74. llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.ttf +0 -0
  75. llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.woff +0 -0
  76. llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  77. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
  78. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
  79. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  80. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
  81. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
  82. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  83. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
  84. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.woff +0 -0
  85. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  86. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
  87. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.woff +0 -0
  88. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  89. llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.ttf +0 -0
  90. llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.woff +0 -0
  91. llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.woff2 +0 -0
  92. llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
  93. llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.woff +0 -0
  94. llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  95. llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.ttf +0 -0
  96. llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.woff +0 -0
  97. llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.woff2 +0 -0
  98. llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.ttf +0 -0
  99. llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.woff +0 -0
  100. llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.woff2 +0 -0
  101. llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
  102. llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.woff +0 -0
  103. llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  104. llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.ttf +0 -0
  105. llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.woff +0 -0
  106. llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.woff2 +0 -0
  107. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
  108. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.woff +0 -0
  109. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  110. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
  111. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.woff +0 -0
  112. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  113. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
  114. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.woff +0 -0
  115. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  116. llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.ttf +0 -0
  117. llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.woff +0 -0
  118. llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.woff2 +0 -0
  119. llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.ttf +0 -0
  120. llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.woff +0 -0
  121. llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  122. llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.ttf +0 -0
  123. llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.woff +0 -0
  124. llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  125. llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.ttf +0 -0
  126. llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.woff +0 -0
  127. llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  128. llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.ttf +0 -0
  129. llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.woff +0 -0
  130. llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  131. llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
  132. llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.woff +0 -0
  133. llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  134. llms/extensions/katex/ui/index.mjs +92 -0
  135. llms/extensions/katex/ui/katex-swap.css +1230 -0
  136. llms/extensions/katex/ui/katex-swap.min.css +1 -0
  137. llms/extensions/katex/ui/katex.css +1230 -0
  138. llms/extensions/katex/ui/katex.js +19080 -0
  139. llms/extensions/katex/ui/katex.min.css +1 -0
  140. llms/extensions/katex/ui/katex.min.js +1 -0
  141. llms/extensions/katex/ui/katex.min.mjs +1 -0
  142. llms/extensions/katex/ui/katex.mjs +18547 -0
  143. llms/extensions/providers/__init__.py +18 -0
  144. llms/extensions/providers/__pycache__/__init__.cpython-314.pyc +0 -0
  145. llms/extensions/providers/__pycache__/anthropic.cpython-314.pyc +0 -0
  146. llms/extensions/providers/__pycache__/chutes.cpython-314.pyc +0 -0
  147. llms/extensions/providers/__pycache__/google.cpython-314.pyc +0 -0
  148. llms/extensions/providers/__pycache__/nvidia.cpython-314.pyc +0 -0
  149. llms/extensions/providers/__pycache__/openai.cpython-314.pyc +0 -0
  150. llms/extensions/providers/__pycache__/openrouter.cpython-314.pyc +0 -0
  151. llms/extensions/providers/anthropic.py +229 -0
  152. llms/extensions/providers/chutes.py +155 -0
  153. llms/extensions/providers/google.py +378 -0
  154. llms/extensions/providers/nvidia.py +105 -0
  155. llms/extensions/providers/openai.py +156 -0
  156. llms/extensions/providers/openrouter.py +72 -0
  157. llms/extensions/system_prompts/README.md +22 -0
  158. llms/extensions/system_prompts/__init__.py +45 -0
  159. llms/extensions/system_prompts/__pycache__/__init__.cpython-314.pyc +0 -0
  160. llms/extensions/system_prompts/ui/index.mjs +280 -0
  161. llms/extensions/system_prompts/ui/prompts.json +1067 -0
  162. llms/extensions/tools/__init__.py +5 -0
  163. llms/extensions/tools/__pycache__/__init__.cpython-314.pyc +0 -0
  164. llms/extensions/tools/ui/index.mjs +204 -0
  165. llms/index.html +35 -77
  166. llms/llms.json +357 -1186
  167. llms/main.py +2847 -999
  168. llms/providers-extra.json +356 -0
  169. llms/providers.json +1 -0
  170. llms/ui/App.mjs +151 -60
  171. llms/ui/ai.mjs +132 -60
  172. llms/ui/app.css +2173 -161
  173. llms/ui/ctx.mjs +365 -0
  174. llms/ui/index.mjs +129 -0
  175. llms/ui/lib/charts.mjs +9 -13
  176. llms/ui/lib/servicestack-vue.mjs +3 -3
  177. llms/ui/lib/vue.min.mjs +10 -9
  178. llms/ui/lib/vue.mjs +1796 -1635
  179. llms/ui/markdown.mjs +18 -7
  180. llms/ui/modules/chat/ChatBody.mjs +691 -0
  181. llms/ui/{SettingsDialog.mjs → modules/chat/SettingsDialog.mjs} +9 -9
  182. llms/ui/modules/chat/index.mjs +828 -0
  183. llms/ui/modules/layout.mjs +243 -0
  184. llms/ui/modules/model-selector.mjs +851 -0
  185. llms/ui/tailwind.input.css +496 -80
  186. llms/ui/utils.mjs +161 -93
  187. {llms_py-2.0.34.dist-info → llms_py-3.0.0.dist-info}/METADATA +1 -1
  188. llms_py-3.0.0.dist-info/RECORD +202 -0
  189. llms/ui/Avatar.mjs +0 -85
  190. llms/ui/Brand.mjs +0 -52
  191. llms/ui/ChatPrompt.mjs +0 -590
  192. llms/ui/Main.mjs +0 -823
  193. llms/ui/ModelSelector.mjs +0 -78
  194. llms/ui/OAuthSignIn.mjs +0 -92
  195. llms/ui/ProviderIcon.mjs +0 -30
  196. llms/ui/ProviderStatus.mjs +0 -105
  197. llms/ui/SignIn.mjs +0 -64
  198. llms/ui/SystemPromptEditor.mjs +0 -31
  199. llms/ui/SystemPromptSelector.mjs +0 -56
  200. llms/ui/Welcome.mjs +0 -8
  201. llms/ui/threadStore.mjs +0 -563
  202. llms/ui.json +0 -1069
  203. llms_py-2.0.34.dist-info/RECORD +0 -48
  204. {llms_py-2.0.34.dist-info → llms_py-3.0.0.dist-info}/WHEEL +0 -0
  205. {llms_py-2.0.34.dist-info → llms_py-3.0.0.dist-info}/entry_points.txt +0 -0
  206. {llms_py-2.0.34.dist-info → llms_py-3.0.0.dist-info}/licenses/LICENSE +0 -0
  207. {llms_py-2.0.34.dist-info → llms_py-3.0.0.dist-info}/top_level.txt +0 -0
@@ -1,29 +1,24 @@
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:`
21
+ template: `
27
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
24
  <div class="flex gap-1 sm:gap-2 flex-wrap justify-center overflow-x-auto">
@@ -51,7 +46,7 @@ const MonthSelector = {
51
46
  </div>
52
47
  `,
53
48
  props: {
54
- dailyData: Array,
49
+ dailyData: Object,
55
50
  },
56
51
  setup(props) {
57
52
  const router = useRouter()
@@ -109,12 +104,9 @@ const MonthSelector = {
109
104
  }
110
105
  }
111
106
 
112
- export default {
113
- components: {
114
- MonthSelector,
115
- },
107
+ export const Analytics = {
116
108
  template: `
117
- <div class="flex flex-col h-full w-full">
109
+ <div class="flex flex-col w-full">
118
110
  <!-- Header -->
119
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">
120
112
  <div
@@ -158,14 +150,14 @@ export default {
158
150
  </div>
159
151
 
160
152
  <!-- Content -->
161
- <div class="flex-1 overflow-auto bg-gray-50 dark:bg-gray-900" :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'">
162
154
 
163
- <div :class="activeTab === 'activity' ? 'h-full' : 'max-w-6xl mx-auto'">
155
+ <div :class="activeTab === 'activity' ? '' : 'max-w-6xl mx-auto'">
164
156
  <!-- Stats Summary (hidden for Activity tab) -->
165
157
  <div v-if="activeTab !== 'activity'" class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
166
158
  <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
167
159
  <div class="text-sm font-medium text-gray-600 dark:text-gray-400">Total Cost</div>
168
- <div class="text-2xl font-bold text-gray-900 dark:text-gray-100 mt-1">{{ formatCost(totalCost) }}</div>
160
+ <div class="text-2xl font-bold text-gray-900 dark:text-gray-100 mt-1">{{ $fmt.cost(totalCost) }}</div>
169
161
  </div>
170
162
  <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
171
163
  <div class="text-sm font-medium text-gray-600 dark:text-gray-400">Total Requests</div>
@@ -173,11 +165,11 @@ export default {
173
165
  </div>
174
166
  <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
175
167
  <div class="text-sm font-medium text-gray-600 dark:text-gray-400">Total Input Tokens</div>
176
- <div class="text-2xl font-bold text-gray-900 dark:text-gray-100 mt-1">{{ humanifyNumber(totalInputTokens) }}</div>
168
+ <div class="text-2xl font-bold text-gray-900 dark:text-gray-100 mt-1">{{ $fmt.humanifyNumber(totalInputTokens) }}</div>
177
169
  </div>
178
170
  <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
179
171
  <div class="text-sm font-medium text-gray-600 dark:text-gray-400">Total Output Tokens</div>
180
- <div class="text-2xl font-bold text-gray-900 dark:text-gray-100 mt-1">{{ humanifyNumber(totalOutputTokens) }}</div>
172
+ <div class="text-2xl font-bold text-gray-900 dark:text-gray-100 mt-1">{{ $fmt.humanifyNumber(totalOutputTokens) }}</div>
181
173
  </div>
182
174
  </div>
183
175
 
@@ -224,11 +216,11 @@ export default {
224
216
  {{ new Date(selectedDay).toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' }) }}
225
217
  </div>
226
218
  <div class="flex flex-wrap gap-x-2 gap-y-1">
227
- <span>{{ formatCost(allDailyData[selectedDay]?.cost || 0) }}</span>
219
+ <span>{{ $fmt.cost(allDailyData[selectedDay]?.cost || 0) }}</span>
228
220
  <span>&#183;</span>
229
221
  <span>{{ allDailyData[selectedDay]?.requests || 0 }} Requests</span>
230
222
  <span>&#183;</span>
231
- <span>{{ humanifyNumber(allDailyData[selectedDay]?.inputTokens || 0) }} -> {{ humanifyNumber(allDailyData[selectedDay]?.outputTokens || 0) }} Tokens</span>
223
+ <span>{{ $fmt.humanifyNumber(allDailyData[selectedDay]?.inputTokens || 0) }} -> {{ $fmt.humanifyNumber(allDailyData[selectedDay]?.outputTokens || 0) }} Tokens</span>
232
224
  </div>
233
225
  </div>
234
226
 
@@ -291,7 +283,7 @@ export default {
291
283
  </div>
292
284
 
293
285
  <!-- Activity Tab - Full Page Layout -->
294
- <div v-if="activeTab === 'activity'" class="h-full flex flex-col bg-white dark:bg-gray-800">
286
+ <div v-if="activeTab === 'activity'" class="flex flex-col bg-white dark:bg-gray-800">
295
287
  <!-- Filters Bar -->
296
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">
297
289
  <div class="flex flex-wrap gap-2 sm:gap-4 items-end">
@@ -315,7 +307,7 @@ export default {
315
307
 
316
308
  <div class="flex flex-col flex-1 min-w-[140px] sm:flex-initial">
317
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">
318
- <option value="created">Date (Newest)</option>
310
+ <option value="createdAt">Date (Newest)</option>
319
311
  <option value="cost">Cost (Highest)</option>
320
312
  <option value="duration">Duration (Longest)</option>
321
313
  <option value="totalTokens">Tokens (Most)</option>
@@ -329,15 +321,15 @@ export default {
329
321
  </div>
330
322
 
331
323
  <!-- Requests List with Infinite Scroll -->
332
- <div class="flex-1 overflow-y-auto" @scroll="onActivityScroll" ref="activityScrollContainer">
333
- <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">
334
326
  <div class="text-center">
335
327
  <div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
336
328
  <p class="mt-4 text-gray-600 dark:text-gray-400">Loading requests...</p>
337
329
  </div>
338
330
  </div>
339
331
 
340
- <div v-else-if="activityRequests.length === 0" class="flex items-center justify-center h-full">
332
+ <div v-else-if="activityRequests.length === 0" class="mt-4 flex items-center justify-center h-full">
341
333
  <p class="text-gray-500 dark:text-gray-400">No requests found</p>
342
334
  </div>
343
335
 
@@ -347,38 +339,42 @@ export default {
347
339
  <div class="flex-1 min-w-0 w-full">
348
340
  <div class="flex flex-col sm:flex-row justify-between gap-2 mb-2">
349
341
  <div class="flex items-center gap-2 flex-wrap">
350
- <span 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>
351
- <span 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>
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>
352
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>
353
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>
354
346
  </div>
355
347
  <div class="text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap">
356
- {{ formatActivityDate(request.created) }}
348
+ {{ formatActivityDate(request.createdAt) }}
357
349
  </div>
358
350
  </div>
359
351
  <div class="text-sm font-semibold text-gray-900 dark:text-gray-100 truncate mb-3">
360
352
  {{ request.title }}
361
353
  </div>
362
354
 
363
- <div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3 sm:gap-4">
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">
364
360
  <div :title="request.cost">
365
361
  <div class="text-xs text-gray-500 dark:text-gray-400 font-medium">Cost</div>
366
- <div class="text-sm font-semibold text-gray-900 dark:text-gray-100">{{ formatCost(request.cost) }}</div>
362
+ <div class="text-sm font-semibold text-gray-900 dark:text-gray-100">{{ $fmt.costLong(request.cost) }}</div>
367
363
  </div>
368
364
  <div class="col-span-2 sm:col-span-1">
369
365
  <div class="text-xs text-gray-500 dark:text-gray-400 font-medium">Tokens</div>
370
- <div class="text-sm font-semibold text-gray-900 dark:text-gray-100">
371
- {{ humanifyNumber(request.inputTokens) }} -> {{ humanifyNumber(request.outputTokens) }}
372
- <span v-if="request.inputCachedTokens" class="ml-1 text-xs text-gray-500 dark:text-gray-400">({{ humanifyNumber(request.inputCachedTokens) }} cached)</span>
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>
373
369
  </div>
374
370
  </div>
375
371
  <div>
376
372
  <div class="text-xs text-gray-500 dark:text-gray-400 font-medium">Duration</div>
377
- <div class="text-sm font-semibold text-gray-900 dark:text-gray-100">{{ request.duration ? humanifyMs(request.duration) : '—' }}</div>
373
+ <div v-if="request.duration" class="text-sm font-semibold text-gray-900 dark:text-gray-100">{{ $fmt.humanifyMs(request.duration) }}</div>
378
374
  </div>
379
375
  <div>
380
376
  <div class="text-xs text-gray-500 dark:text-gray-400 font-medium">Speed</div>
381
- <div class="text-sm font-semibold text-gray-900 dark:text-gray-100">{{ request.duration && request.outputTokens ? (request.outputTokens / (request.duration / 1000)).toFixed(1) + ' tok/s' : '—' }}</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>
382
378
  </div>
383
379
  </div>
384
380
  </div>
@@ -401,6 +397,7 @@ export default {
401
397
  No more requests to load
402
398
  </div>
403
399
  </div>
400
+ <div ref="scrollSentinel" class="h-4 w-full"></div>
404
401
  </div>
405
402
  </div>
406
403
  </div>
@@ -408,10 +405,10 @@ export default {
408
405
  </div>
409
406
  `,
410
407
  setup() {
408
+ const ctx = inject('ctx')
411
409
  const router = useRouter()
412
410
  const route = useRoute()
413
- const threads = useThreadStore()
414
- const { initDB } = threads
411
+ const analyticsData = ref()
415
412
 
416
413
  // Initialize activeTab from URL query parameter, default to 'cost'
417
414
  const activeTab = ref(route.query.tab || 'cost')
@@ -437,6 +434,9 @@ export default {
437
434
  const selectedYear = computed(() => {
438
435
  return route.query.year !== undefined ? parseInt(route.query.year) : currentDate.getFullYear()
439
436
  })
437
+ const selectedYearMonth = computed(() => {
438
+ return `${selectedYear.value}-${selectedMonth.value < 10 ? '0' + selectedMonth.value : selectedMonth.value}`
439
+ })
440
440
  const allDailyData = ref({}) // Store all data for filtering
441
441
 
442
442
  // Selected day - read from URL, default to today
@@ -532,52 +532,18 @@ export default {
532
532
 
533
533
  const selectedModel = ref('')
534
534
  const selectedProvider = ref('')
535
- const sortBy = ref('created')
535
+ const sortBy = ref('createdAt')
536
536
  const filterOptions = ref({ models: [], providers: [] })
537
- const activityScrollContainer = ref(null)
537
+ const scrollSentinel = ref(null)
538
+ let observer = null
538
539
 
539
540
  const hasActiveFilters = computed(() => selectedModel.value || selectedProvider.value)
540
541
 
541
542
  async function loadAnalyticsData() {
542
543
  try {
543
- const db = await initDB()
544
- const tx = db.transaction(['requests'], 'readonly')
545
- const store = tx.objectStore('requests')
546
- const allRequests = await store.getAll()
547
-
548
544
  // Group requests by date
549
- const dailyData = {}
550
- let totalCostSum = 0
551
- let totalInputSum = 0
552
- let totalOutputSum = 0
553
- const yearsSet = new Set()
554
-
555
- allRequests.forEach(req => {
556
- const date = new Date(req.created * 1000)
557
- const dateKey = date.toISOString().split('T')[0] // YYYY-MM-DD
558
- yearsSet.add(date.getFullYear())
559
-
560
- if (!dailyData[dateKey]) {
561
- dailyData[dateKey] = {
562
- cost: 0,
563
- requests: 0,
564
- inputTokens: 0,
565
- outputTokens: 0
566
- }
567
- }
568
-
569
- dailyData[dateKey].cost += req.cost || 0
570
- dailyData[dateKey].requests += 1
571
- dailyData[dateKey].inputTokens += req.inputTokens || 0
572
- dailyData[dateKey].outputTokens += req.outputTokens || 0
573
-
574
- totalCostSum += req.cost || 0
575
- totalInputSum += req.inputTokens || 0
576
- totalOutputSum += req.outputTokens || 0
577
- })
578
-
579
- // Store all daily data for filtering
580
- allDailyData.value = dailyData
545
+ analyticsData.value = await ctx.requests.getSummary()
546
+ allDailyData.value = analyticsData.value.dailyData
581
547
 
582
548
  // Update chart data based on selected month/year
583
549
  updateChartData()
@@ -654,36 +620,8 @@ export default {
654
620
  }
655
621
 
656
622
  try {
657
- const db = await initDB()
658
- const tx = db.transaction(['requests'], 'readonly')
659
- const store = tx.objectStore('requests')
660
- const allRequests = await store.getAll()
661
-
662
- // Filter requests for the selected day
663
- const dayStart = Math.floor(new Date(dateKey + 'T00:00:00Z').getTime() / 1000)
664
- const dayEnd = Math.floor(new Date(dateKey + 'T23:59:59Z').getTime() / 1000)
665
-
666
- const dayRequests = allRequests.filter(req => req.created >= dayStart && req.created <= dayEnd)
667
-
668
- // Aggregate by model
669
- const modelData = {}
670
- const providerData = {}
671
-
672
- dayRequests.forEach(req => {
673
- // Model aggregation
674
- if (!modelData[req.model]) {
675
- modelData[req.model] = { cost: 0, count: 0 }
676
- }
677
- modelData[req.model].cost += req.cost || 0
678
- modelData[req.model].count += 1
679
-
680
- // Provider aggregation
681
- if (!providerData[req.provider]) {
682
- providerData[req.provider] = { cost: 0, count: 0 }
683
- }
684
- providerData[req.provider].cost += req.cost || 0
685
- providerData[req.provider].count += 1
686
- })
623
+ const dailySummary = await ctx.requests.getDailySummary(dateKey)
624
+ const { modelData, providerData } = dailySummary
687
625
 
688
626
  // Prepare model pie chart data
689
627
  const modelLabels = Object.keys(modelData).sort()
@@ -727,38 +665,8 @@ export default {
727
665
  }
728
666
 
729
667
  try {
730
- const db = await initDB()
731
- const tx = db.transaction(['requests'], 'readonly')
732
- const store = tx.objectStore('requests')
733
- const allRequests = await store.getAll()
734
-
735
- // Filter requests for the selected day
736
- const dayStart = Math.floor(new Date(dateKey + 'T00:00:00Z').getTime() / 1000)
737
- const dayEnd = Math.floor(new Date(dateKey + 'T23:59:59Z').getTime() / 1000)
738
-
739
- const dayRequests = allRequests.filter(req => req.created >= dayStart && req.created <= dayEnd)
740
-
741
- // Aggregate by model and provider (using tokens)
742
- const modelData = {}
743
- const providerData = {}
744
-
745
- dayRequests.forEach(req => {
746
- const totalTokens = (req.inputTokens || 0) + (req.outputTokens || 0)
747
-
748
- // Model aggregation
749
- if (!modelData[req.model]) {
750
- modelData[req.model] = { tokens: 0, count: 0 }
751
- }
752
- modelData[req.model].tokens += totalTokens
753
- modelData[req.model].count += 1
754
-
755
- // Provider aggregation
756
- if (!providerData[req.provider]) {
757
- providerData[req.provider] = { tokens: 0, count: 0 }
758
- }
759
- providerData[req.provider].tokens += totalTokens
760
- providerData[req.provider].count += 1
761
- })
668
+ const dailySummary = await ctx.requests.getDailySummary(dateKey)
669
+ const { modelData, providerData } = dailySummary
762
670
 
763
671
  // Prepare model pie chart data
764
672
  const modelLabels = Object.keys(modelData).sort()
@@ -802,7 +710,7 @@ export default {
802
710
  costChartInstance.destroy()
803
711
  }
804
712
 
805
- const ctx = costChartCanvas.value.getContext('2d')
713
+ const ctx2d = costChartCanvas.value.getContext('2d')
806
714
  const chartTypeValue = costChartType.value
807
715
 
808
716
  // Find the index of the selected day
@@ -833,7 +741,7 @@ export default {
833
741
  }]
834
742
  }
835
743
 
836
- costChartInstance = new Chart(ctx, {
744
+ costChartInstance = new Chart(ctx2d, {
837
745
  type: chartTypeValue,
838
746
  data: chartDataWithColors,
839
747
  options: {
@@ -859,14 +767,14 @@ export default {
859
767
  },
860
768
  tooltip: {
861
769
  callbacks: {
862
- title: function(context) {
770
+ title: function (context) {
863
771
  const index = context[0].dataIndex
864
772
  const dateKey = chartData.value.dateKeys[index]
865
773
  const date = new Date(dateKey + 'T00:00:00Z')
866
774
  return date.toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' })
867
775
  },
868
- label: function(context) {
869
- return `Cost: ${formatCost(context.parsed.y)}`
776
+ label: function (context) {
777
+ return `Cost: ${ctx.fmt.cost(context.parsed.y)}`
870
778
  }
871
779
  }
872
780
  }
@@ -875,7 +783,7 @@ export default {
875
783
  y: {
876
784
  beginAtZero: true,
877
785
  ticks: {
878
- callback: function(value) {
786
+ callback: function (value) {
879
787
  return '$' + value.toFixed(4)
880
788
  }
881
789
  }
@@ -893,7 +801,7 @@ export default {
893
801
  tokenChartInstance.destroy()
894
802
  }
895
803
 
896
- const ctx = tokenChartCanvas.value.getContext('2d')
804
+ const ctx2d = tokenChartCanvas.value.getContext('2d')
897
805
 
898
806
  // Find the index of the selected day
899
807
  const selectedDayIndex = tokenChartData.value.dateKeys.indexOf(selectedDay.value)
@@ -944,7 +852,7 @@ export default {
944
852
  ]
945
853
  }
946
854
 
947
- tokenChartInstance = new Chart(ctx, {
855
+ tokenChartInstance = new Chart(ctx2d, {
948
856
  type: 'bar',
949
857
  data: chartDataWithColors,
950
858
  options: {
@@ -972,8 +880,8 @@ export default {
972
880
  stacked: true,
973
881
  beginAtZero: true,
974
882
  ticks: {
975
- callback: function(value) {
976
- return humanifyNumber(value)
883
+ callback: function (value) {
884
+ return ctx.fmt.humanifyNumber(value)
977
885
  }
978
886
  }
979
887
  }
@@ -985,14 +893,14 @@ export default {
985
893
  },
986
894
  tooltip: {
987
895
  callbacks: {
988
- title: function(context) {
896
+ title: function (context) {
989
897
  const index = context[0].dataIndex
990
898
  const dateKey = tokenChartData.value.dateKeys[index]
991
899
  const date = new Date(dateKey + 'T00:00:00Z')
992
900
  return date.toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' })
993
901
  },
994
- label: function(context) {
995
- return `${context.dataset.label}: ${humanifyNumber(context.parsed.y)}`
902
+ label: function (context) {
903
+ return `${context.dataset.label}: ${ctx.fmt.humanifyNumber(context.parsed.y)}`
996
904
  }
997
905
  }
998
906
  }
@@ -1009,7 +917,7 @@ export default {
1009
917
  modelPieChartInstance.destroy()
1010
918
  }
1011
919
 
1012
- const ctx = modelPieCanvas.value.getContext('2d')
920
+ const ctx2d = modelPieCanvas.value.getContext('2d')
1013
921
 
1014
922
  // Custom plugin to draw percentage labels on pie slices
1015
923
  const percentagePlugin = {
@@ -1036,7 +944,7 @@ export default {
1036
944
  }
1037
945
  }
1038
946
 
1039
- modelPieChartInstance = new Chart(ctx, {
947
+ modelPieChartInstance = new Chart(ctx2d, {
1040
948
  type: 'pie',
1041
949
  data: modelPieData.value,
1042
950
  options: {
@@ -1049,8 +957,8 @@ export default {
1049
957
  },
1050
958
  tooltip: {
1051
959
  callbacks: {
1052
- label: function(context) {
1053
- return `${context.label}: ${formatCost(context.parsed)}`
960
+ label: function (context) {
961
+ return `${context.label}: ${ctx.fmt.cost(context.parsed)}`
1054
962
  }
1055
963
  }
1056
964
  }
@@ -1068,7 +976,7 @@ export default {
1068
976
  providerPieChartInstance.destroy()
1069
977
  }
1070
978
 
1071
- const ctx = providerPieCanvas.value.getContext('2d')
979
+ const ctx2d = providerPieCanvas.value.getContext('2d')
1072
980
 
1073
981
  // Custom plugin to draw percentage labels on pie slices
1074
982
  const percentagePlugin = {
@@ -1095,7 +1003,7 @@ export default {
1095
1003
  }
1096
1004
  }
1097
1005
 
1098
- providerPieChartInstance = new Chart(ctx, {
1006
+ providerPieChartInstance = new Chart(ctx2d, {
1099
1007
  type: 'pie',
1100
1008
  data: providerPieData.value,
1101
1009
  options: {
@@ -1108,8 +1016,8 @@ export default {
1108
1016
  },
1109
1017
  tooltip: {
1110
1018
  callbacks: {
1111
- label: function(context) {
1112
- return `${context.label}: ${formatCost(context.parsed)}`
1019
+ label: function (context) {
1020
+ return `${context.label}: ${ctx.fmt.cost(context.parsed)}`
1113
1021
  }
1114
1022
  }
1115
1023
  }
@@ -1127,7 +1035,7 @@ export default {
1127
1035
  tokenModelPieChartInstance.destroy()
1128
1036
  }
1129
1037
 
1130
- const ctx = tokenModelPieCanvas.value.getContext('2d')
1038
+ const ctx2d = tokenModelPieCanvas.value.getContext('2d')
1131
1039
 
1132
1040
  // Custom plugin to draw percentage labels on pie slices
1133
1041
  const percentagePlugin = {
@@ -1154,7 +1062,7 @@ export default {
1154
1062
  }
1155
1063
  }
1156
1064
 
1157
- tokenModelPieChartInstance = new Chart(ctx, {
1065
+ tokenModelPieChartInstance = new Chart(ctx2d, {
1158
1066
  type: 'pie',
1159
1067
  data: tokenModelPieData.value,
1160
1068
  options: {
@@ -1167,8 +1075,8 @@ export default {
1167
1075
  },
1168
1076
  tooltip: {
1169
1077
  callbacks: {
1170
- label: function(context) {
1171
- return `${context.label}: ${humanifyNumber(context.parsed)}`
1078
+ label: function (context) {
1079
+ return `${context.label}: ${ctx.fmt.humanifyNumber(context.parsed)}`
1172
1080
  }
1173
1081
  }
1174
1082
  }
@@ -1186,7 +1094,7 @@ export default {
1186
1094
  tokenProviderPieChartInstance.destroy()
1187
1095
  }
1188
1096
 
1189
- const ctx = tokenProviderPieCanvas.value.getContext('2d')
1097
+ const ctx2d = tokenProviderPieCanvas.value.getContext('2d')
1190
1098
 
1191
1099
  // Custom plugin to draw percentage labels on pie slices
1192
1100
  const percentagePlugin = {
@@ -1213,7 +1121,7 @@ export default {
1213
1121
  }
1214
1122
  }
1215
1123
 
1216
- tokenProviderPieChartInstance = new Chart(ctx, {
1124
+ tokenProviderPieChartInstance = new Chart(ctx2d, {
1217
1125
  type: 'pie',
1218
1126
  data: tokenProviderPieData.value,
1219
1127
  options: {
@@ -1226,8 +1134,8 @@ export default {
1226
1134
  },
1227
1135
  tooltip: {
1228
1136
  callbacks: {
1229
- label: function(context) {
1230
- return `${context.label}: ${humanifyNumber(context.parsed)}`
1137
+ label: function (context) {
1138
+ return `${context.label}: ${ctx.fmt.humanifyNumber(context.parsed)}`
1231
1139
  }
1232
1140
  }
1233
1141
  }
@@ -1240,7 +1148,7 @@ export default {
1240
1148
  // Activity tab functions
1241
1149
  const loadActivityFilterOptions = async () => {
1242
1150
  try {
1243
- filterOptions.value = await threads.getFilterOptions()
1151
+ filterOptions.value = await ctx.requests.getFilterOptions()
1244
1152
  } catch (error) {
1245
1153
  console.error('Failed to load filter options:', error)
1246
1154
  }
@@ -1248,24 +1156,9 @@ export default {
1248
1156
 
1249
1157
  const loadExistingThreadIds = async () => {
1250
1158
  try {
1251
- // Calculate date range for selected month/year
1252
- const startDate = new Date(selectedYear.value, selectedMonth.value - 1, 1)
1253
- const endDate = new Date(selectedYear.value, selectedMonth.value, 0, 23, 59, 59)
1254
-
1255
- // Convert to timestamp strings (threadId format)
1256
- const startThreadId = startDate.getTime().toString()
1257
- const endThreadId = endDate.getTime().toString()
1258
-
1259
- const db = await initDB()
1260
- const tx = db.transaction(['threads'], 'readonly')
1261
- const store = tx.objectStore('threads')
1262
-
1263
- // Use IDBKeyRange to only load threads within the month's timestamp range
1264
- const range = IDBKeyRange.bound(startThreadId, endThreadId)
1265
- const monthThreads = await store.getAll(range)
1266
-
1267
- // Create a Set of existing thread IDs for the month
1268
- existingThreadIds.value = new Set(monthThreads.map(thread => thread.id))
1159
+ existingThreadIds.value = new Set(await ctx.requests.getThreadIds({
1160
+ month: selectedYearMonth.value,
1161
+ }))
1269
1162
  } catch (error) {
1270
1163
  console.error('Failed to load existing thread IDs:', error)
1271
1164
  existingThreadIds.value = new Set()
@@ -1288,28 +1181,24 @@ export default {
1288
1181
  }
1289
1182
 
1290
1183
  try {
1291
- // Calculate date range for selected month/year
1292
- const startDate = new Date(selectedYear.value, selectedMonth.value - 1, 1)
1293
- const endDate = new Date(selectedYear.value, selectedMonth.value, 0, 23, 59, 59)
1294
-
1295
- const filters = {
1296
- model: selectedModel.value || null,
1297
- provider: selectedProvider.value || null,
1298
- sortBy: sortBy.value,
1299
- sortOrder: 'desc',
1300
- startDate: Math.floor(startDate.getTime() / 1000),
1301
- endDate: Math.floor(endDate.getTime() / 1000)
1302
- }
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
+ })
1303
1192
 
1304
- const result = await threads.getRequests(filters, activityPageSize, activityOffset.value)
1193
+ const hasMore = requests.length >= activityPageSize
1305
1194
 
1306
1195
  if (reset) {
1307
- activityRequests.value = result.requests
1196
+ activityRequests.value = requests
1308
1197
  } else {
1309
- activityRequests.value.push(...result.requests)
1198
+ activityRequests.value.push(...requests)
1310
1199
  }
1311
1200
 
1312
- activityHasMore.value = result.hasMore
1201
+ activityHasMore.value = hasMore
1313
1202
  activityOffset.value += activityPageSize
1314
1203
  } catch (error) {
1315
1204
  console.error('Failed to load requests:', error)
@@ -1319,29 +1208,32 @@ export default {
1319
1208
  }
1320
1209
  }
1321
1210
 
1322
- const onActivityScroll = async () => {
1323
- if (!activityScrollContainer.value) return
1211
+ const setupObserver = () => {
1212
+ if (observer) observer.disconnect()
1324
1213
 
1325
- const { scrollTop, scrollHeight, clientHeight } = activityScrollContainer.value
1326
- 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' })
1327
1219
 
1328
- if (isNearBottom && activityHasMore.value && !isActivityLoadingMore.value && !isActivityLoading.value) {
1329
- await loadActivityRequests(false)
1220
+ if (scrollSentinel.value) {
1221
+ observer.observe(scrollSentinel.value)
1330
1222
  }
1331
1223
  }
1332
1224
 
1333
1225
  const clearActivityFilters = async () => {
1334
1226
  selectedModel.value = ''
1335
1227
  selectedProvider.value = ''
1336
- sortBy.value = 'created'
1228
+ sortBy.value = 'createdAt'
1337
1229
  await loadActivityRequests(true)
1338
1230
  }
1339
1231
 
1340
- const formatActivityDate = (timestamp) => {
1341
- const date = new Date(timestamp * 1000)
1342
- return date.toLocaleTimeString(undefined, { hour12: false }) + ' '
1343
- + date.toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' })
1344
-
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
+
1345
1237
  }
1346
1238
 
1347
1239
  const openThread = (threadId) => {
@@ -1349,17 +1241,12 @@ export default {
1349
1241
  }
1350
1242
 
1351
1243
  const deleteRequestLog = async (requestId) => {
1352
- if (confirm('Are you sure you want to delete this request log?')) {
1353
- try {
1354
- await threads.deleteRequest(requestId)
1355
- // Remove from the list
1356
- activityRequests.value = activityRequests.value.filter(r => r.id !== requestId)
1357
- // Reload analytics data
1358
- await loadAnalyticsData()
1359
- } catch (error) {
1360
- console.error('Failed to delete request:', error)
1361
- alert('Failed to delete request')
1362
- }
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()
1363
1250
  }
1364
1251
  }
1365
1252
 
@@ -1442,6 +1329,8 @@ export default {
1442
1329
  } else if (newTab === 'activity') {
1443
1330
  await loadActivityFilterOptions()
1444
1331
  await loadActivityRequests(true)
1332
+ await nextTick()
1333
+ setupObserver()
1445
1334
  }
1446
1335
  })
1447
1336
 
@@ -1474,9 +1363,15 @@ export default {
1474
1363
  if (activeTab.value === 'activity') {
1475
1364
  await loadActivityFilterOptions()
1476
1365
  await loadActivityRequests(true)
1366
+ await nextTick()
1367
+ setupObserver()
1477
1368
  }
1478
1369
  })
1479
1370
 
1371
+ onUnmounted(() => {
1372
+ if (observer) observer.disconnect()
1373
+ })
1374
+
1480
1375
  return {
1481
1376
  activeTab,
1482
1377
  costChartType,
@@ -1497,9 +1392,6 @@ export default {
1497
1392
  totalRequests,
1498
1393
  totalInputTokens,
1499
1394
  totalOutputTokens,
1500
- formatCost,
1501
- humanifyNumber,
1502
- humanifyMs,
1503
1395
  // Month/Year selection
1504
1396
  selectedMonth,
1505
1397
  selectedYear,
@@ -1514,8 +1406,8 @@ export default {
1514
1406
  sortBy,
1515
1407
  filterOptions,
1516
1408
  hasActiveFilters,
1517
- activityScrollContainer,
1518
- onActivityScroll,
1409
+ hasActiveFilters,
1410
+ scrollSentinel,
1519
1411
  clearActivityFilters,
1520
1412
  formatActivityDate,
1521
1413
  threadExists,
@@ -1526,3 +1418,27 @@ export default {
1526
1418
  }
1527
1419
  }
1528
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
+ }