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