llms-py 2.0.9__py3-none-any.whl → 3.0.10__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 (194) hide show
  1. llms/__init__.py +4 -0
  2. llms/__main__.py +9 -0
  3. llms/db.py +359 -0
  4. llms/extensions/analytics/ui/index.mjs +1444 -0
  5. llms/extensions/app/README.md +20 -0
  6. llms/extensions/app/__init__.py +589 -0
  7. llms/extensions/app/db.py +536 -0
  8. {llms_py-2.0.9.data/data → llms/extensions/app}/ui/Recents.mjs +100 -73
  9. llms_py-2.0.9.data/data/ui/Sidebar.mjs → llms/extensions/app/ui/index.mjs +150 -79
  10. llms/extensions/app/ui/threadStore.mjs +433 -0
  11. llms/extensions/core_tools/CALCULATOR.md +32 -0
  12. llms/extensions/core_tools/__init__.py +637 -0
  13. llms/extensions/core_tools/ui/codemirror/addon/edit/closebrackets.js +201 -0
  14. llms/extensions/core_tools/ui/codemirror/addon/edit/closetag.js +185 -0
  15. llms/extensions/core_tools/ui/codemirror/addon/edit/continuelist.js +101 -0
  16. llms/extensions/core_tools/ui/codemirror/addon/edit/matchbrackets.js +160 -0
  17. llms/extensions/core_tools/ui/codemirror/addon/edit/matchtags.js +66 -0
  18. llms/extensions/core_tools/ui/codemirror/addon/edit/trailingspace.js +27 -0
  19. llms/extensions/core_tools/ui/codemirror/addon/selection/active-line.js +72 -0
  20. llms/extensions/core_tools/ui/codemirror/addon/selection/mark-selection.js +119 -0
  21. llms/extensions/core_tools/ui/codemirror/addon/selection/selection-pointer.js +98 -0
  22. llms/extensions/core_tools/ui/codemirror/codemirror.css +344 -0
  23. llms/extensions/core_tools/ui/codemirror/codemirror.js +9884 -0
  24. llms/extensions/core_tools/ui/codemirror/doc/docs.css +225 -0
  25. llms/extensions/core_tools/ui/codemirror/doc/source_sans.woff +0 -0
  26. llms/extensions/core_tools/ui/codemirror/mode/clike/clike.js +942 -0
  27. llms/extensions/core_tools/ui/codemirror/mode/javascript/index.html +118 -0
  28. llms/extensions/core_tools/ui/codemirror/mode/javascript/javascript.js +962 -0
  29. llms/extensions/core_tools/ui/codemirror/mode/javascript/typescript.html +62 -0
  30. llms/extensions/core_tools/ui/codemirror/mode/python/python.js +402 -0
  31. llms/extensions/core_tools/ui/codemirror/theme/dracula.css +40 -0
  32. llms/extensions/core_tools/ui/codemirror/theme/mocha.css +135 -0
  33. llms/extensions/core_tools/ui/index.mjs +650 -0
  34. llms/extensions/gallery/README.md +61 -0
  35. llms/extensions/gallery/__init__.py +63 -0
  36. llms/extensions/gallery/db.py +243 -0
  37. llms/extensions/gallery/ui/index.mjs +482 -0
  38. llms/extensions/katex/README.md +39 -0
  39. llms/extensions/katex/__init__.py +6 -0
  40. llms/extensions/katex/ui/README.md +125 -0
  41. llms/extensions/katex/ui/contrib/auto-render.js +338 -0
  42. llms/extensions/katex/ui/contrib/auto-render.min.js +1 -0
  43. llms/extensions/katex/ui/contrib/auto-render.mjs +244 -0
  44. llms/extensions/katex/ui/contrib/copy-tex.js +127 -0
  45. llms/extensions/katex/ui/contrib/copy-tex.min.js +1 -0
  46. llms/extensions/katex/ui/contrib/copy-tex.mjs +105 -0
  47. llms/extensions/katex/ui/contrib/mathtex-script-type.js +109 -0
  48. llms/extensions/katex/ui/contrib/mathtex-script-type.min.js +1 -0
  49. llms/extensions/katex/ui/contrib/mathtex-script-type.mjs +24 -0
  50. llms/extensions/katex/ui/contrib/mhchem.js +3213 -0
  51. llms/extensions/katex/ui/contrib/mhchem.min.js +1 -0
  52. llms/extensions/katex/ui/contrib/mhchem.mjs +3109 -0
  53. llms/extensions/katex/ui/contrib/render-a11y-string.js +887 -0
  54. llms/extensions/katex/ui/contrib/render-a11y-string.min.js +1 -0
  55. llms/extensions/katex/ui/contrib/render-a11y-string.mjs +800 -0
  56. llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.ttf +0 -0
  57. llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.woff +0 -0
  58. llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  59. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
  60. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
  61. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  62. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
  63. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
  64. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  65. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
  66. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.woff +0 -0
  67. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  68. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
  69. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.woff +0 -0
  70. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  71. llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.ttf +0 -0
  72. llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.woff +0 -0
  73. llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.woff2 +0 -0
  74. llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
  75. llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.woff +0 -0
  76. llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  77. llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.ttf +0 -0
  78. llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.woff +0 -0
  79. llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.woff2 +0 -0
  80. llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.ttf +0 -0
  81. llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.woff +0 -0
  82. llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.woff2 +0 -0
  83. llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
  84. llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.woff +0 -0
  85. llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  86. llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.ttf +0 -0
  87. llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.woff +0 -0
  88. llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.woff2 +0 -0
  89. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
  90. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.woff +0 -0
  91. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  92. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
  93. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.woff +0 -0
  94. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  95. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
  96. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.woff +0 -0
  97. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  98. llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.ttf +0 -0
  99. llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.woff +0 -0
  100. llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.woff2 +0 -0
  101. llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.ttf +0 -0
  102. llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.woff +0 -0
  103. llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  104. llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.ttf +0 -0
  105. llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.woff +0 -0
  106. llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  107. llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.ttf +0 -0
  108. llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.woff +0 -0
  109. llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  110. llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.ttf +0 -0
  111. llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.woff +0 -0
  112. llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  113. llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
  114. llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.woff +0 -0
  115. llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  116. llms/extensions/katex/ui/index.mjs +92 -0
  117. llms/extensions/katex/ui/katex-swap.css +1230 -0
  118. llms/extensions/katex/ui/katex-swap.min.css +1 -0
  119. llms/extensions/katex/ui/katex.css +1230 -0
  120. llms/extensions/katex/ui/katex.js +19080 -0
  121. llms/extensions/katex/ui/katex.min.css +1 -0
  122. llms/extensions/katex/ui/katex.min.js +1 -0
  123. llms/extensions/katex/ui/katex.min.mjs +1 -0
  124. llms/extensions/katex/ui/katex.mjs +18547 -0
  125. llms/extensions/providers/__init__.py +22 -0
  126. llms/extensions/providers/anthropic.py +233 -0
  127. llms/extensions/providers/cerebras.py +37 -0
  128. llms/extensions/providers/chutes.py +153 -0
  129. llms/extensions/providers/google.py +481 -0
  130. llms/extensions/providers/nvidia.py +103 -0
  131. llms/extensions/providers/openai.py +154 -0
  132. llms/extensions/providers/openrouter.py +74 -0
  133. llms/extensions/providers/zai.py +182 -0
  134. llms/extensions/system_prompts/README.md +22 -0
  135. llms/extensions/system_prompts/__init__.py +45 -0
  136. llms/extensions/system_prompts/ui/index.mjs +280 -0
  137. llms/extensions/system_prompts/ui/prompts.json +1067 -0
  138. llms/extensions/tools/__init__.py +144 -0
  139. llms/extensions/tools/ui/index.mjs +706 -0
  140. llms/index.html +58 -0
  141. llms/llms.json +400 -0
  142. llms/main.py +4407 -0
  143. llms/providers-extra.json +394 -0
  144. llms/providers.json +1 -0
  145. llms/ui/App.mjs +188 -0
  146. llms/ui/ai.mjs +217 -0
  147. llms/ui/app.css +7081 -0
  148. llms/ui/ctx.mjs +412 -0
  149. llms/ui/index.mjs +131 -0
  150. llms/ui/lib/chart.js +14 -0
  151. llms/ui/lib/charts.mjs +16 -0
  152. llms/ui/lib/color.js +14 -0
  153. llms/ui/lib/servicestack-vue.mjs +37 -0
  154. llms/ui/lib/vue.min.mjs +13 -0
  155. llms/ui/lib/vue.mjs +18530 -0
  156. {llms_py-2.0.9.data/data → llms}/ui/markdown.mjs +33 -15
  157. llms/ui/modules/chat/ChatBody.mjs +976 -0
  158. llms/ui/modules/chat/SettingsDialog.mjs +374 -0
  159. llms/ui/modules/chat/index.mjs +991 -0
  160. llms/ui/modules/icons.mjs +46 -0
  161. llms/ui/modules/layout.mjs +271 -0
  162. llms/ui/modules/model-selector.mjs +811 -0
  163. llms/ui/tailwind.input.css +742 -0
  164. {llms_py-2.0.9.data/data → llms}/ui/typography.css +133 -7
  165. llms/ui/utils.mjs +261 -0
  166. llms_py-3.0.10.dist-info/METADATA +49 -0
  167. llms_py-3.0.10.dist-info/RECORD +177 -0
  168. llms_py-3.0.10.dist-info/entry_points.txt +2 -0
  169. {llms_py-2.0.9.dist-info → llms_py-3.0.10.dist-info}/licenses/LICENSE +1 -2
  170. llms.py +0 -1402
  171. llms_py-2.0.9.data/data/index.html +0 -64
  172. llms_py-2.0.9.data/data/llms.json +0 -447
  173. llms_py-2.0.9.data/data/requirements.txt +0 -1
  174. llms_py-2.0.9.data/data/ui/App.mjs +0 -20
  175. llms_py-2.0.9.data/data/ui/ChatPrompt.mjs +0 -389
  176. llms_py-2.0.9.data/data/ui/Main.mjs +0 -680
  177. llms_py-2.0.9.data/data/ui/app.css +0 -3951
  178. llms_py-2.0.9.data/data/ui/lib/servicestack-vue.min.mjs +0 -37
  179. llms_py-2.0.9.data/data/ui/lib/vue.min.mjs +0 -12
  180. llms_py-2.0.9.data/data/ui/tailwind.input.css +0 -261
  181. llms_py-2.0.9.data/data/ui/threadStore.mjs +0 -273
  182. llms_py-2.0.9.data/data/ui/utils.mjs +0 -114
  183. llms_py-2.0.9.data/data/ui.json +0 -1069
  184. llms_py-2.0.9.dist-info/METADATA +0 -941
  185. llms_py-2.0.9.dist-info/RECORD +0 -30
  186. llms_py-2.0.9.dist-info/entry_points.txt +0 -2
  187. {llms_py-2.0.9.data/data → llms}/ui/fav.svg +0 -0
  188. {llms_py-2.0.9.data/data → llms}/ui/lib/highlight.min.mjs +0 -0
  189. {llms_py-2.0.9.data/data → llms}/ui/lib/idb.min.mjs +0 -0
  190. {llms_py-2.0.9.data/data → llms}/ui/lib/marked.min.mjs +0 -0
  191. /llms_py-2.0.9.data/data/ui/lib/servicestack-client.min.mjs → /llms/ui/lib/servicestack-client.mjs +0 -0
  192. {llms_py-2.0.9.data/data → llms}/ui/lib/vue-router.min.mjs +0 -0
  193. {llms_py-2.0.9.dist-info → llms_py-3.0.10.dist-info}/WHEEL +0 -0
  194. {llms_py-2.0.9.dist-info → llms_py-3.0.10.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1444 @@
1
+ import { ref, watch, nextTick, computed, inject, onMounted, onUnmounted } from 'vue'
2
+ import { useRouter, useRoute } from 'vue-router'
3
+ import { leftPart } from '@servicestack/client'
4
+ import { Chart, registerables } from "chart.js"
5
+ Chart.register(...registerables)
6
+
7
+ 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)' },
10
+ { 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)' },
17
+ { background: 'rgba(201, 203, 207, 0.2)', border: 'rgb(201, 203, 207)' },
18
+ ]
19
+
20
+ const MonthSelector = {
21
+ template: `
22
+ <div class="flex flex-col sm:flex-row gap-2 sm:gap-4 items-stretch sm:items-center w-full sm:w-auto">
23
+ <!-- Months Row -->
24
+ <div class="flex gap-1 sm:gap-2 flex-wrap justify-center overflow-x-auto">
25
+ <template v-for="month in availableMonthsForYear" :key="month">
26
+ <span v-if="selectedMonth === month"
27
+ class="text-xs leading-5 font-semibold bg-indigo-600 text-white rounded-full py-1 px-2 sm:px-3 flex items-center space-x-2 whitespace-nowrap">
28
+ <span class="hidden sm:inline">{{ new Date(selectedYear + '-' + month.toString().padStart(2,'0') + '-01').toLocaleString('default', { month: 'long' }) }}</span>
29
+ <span class="sm:hidden">{{ new Date(selectedYear + '-' + month.toString().padStart(2,'0') + '-01').toLocaleString('default', { month: 'short' }) }}</span>
30
+ </span>
31
+ <button v-else type="button"
32
+ class="text-xs leading-5 font-semibold bg-slate-400/10 rounded-full py-1 px-2 sm:px-3 flex items-center space-x-2 hover:bg-slate-400/20 dark:highlight-white/5 whitespace-nowrap"
33
+ @click="updateSelection(selectedYear, month)">
34
+ {{ new Date(selectedYear + '-' + month.toString().padStart(2,'0') + '-01').toLocaleString('default', { month: 'short' }) }}
35
+ </button>
36
+ </template>
37
+ </div>
38
+
39
+ <!-- Year Dropdown -->
40
+ <select :value="selectedYear" @change="(e) => updateSelection(parseInt(e.target.value), selectedMonth)"
41
+ class="border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-900 text-gray-700 dark:text-gray-300 rounded-md text-sm font-medium hover:bg-gray-50 dark:hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-indigo-500 flex-shrink-0">
42
+ <option v-for="year in availableYears" :key="year" :value="year">
43
+ {{ year }}
44
+ </option>
45
+ </select>
46
+ </div>
47
+ `,
48
+ props: {
49
+ dailyData: Object,
50
+ },
51
+ setup(props) {
52
+ const router = useRouter()
53
+ const route = useRoute()
54
+
55
+ const selectedMonth = computed(() => {
56
+ const now = new Date()
57
+ return route.query.month !== undefined ? parseInt(route.query.month) : now.getMonth() + 1
58
+ })
59
+
60
+ const selectedYear = computed(() => {
61
+ const now = new Date()
62
+ return route.query.year !== undefined ? parseInt(route.query.year) : now.getFullYear()
63
+ })
64
+
65
+ const updateSelection = (year, month) => {
66
+ router.push({
67
+ query: {
68
+ ...route.query,
69
+ month,
70
+ year
71
+ }
72
+ })
73
+ }
74
+
75
+ const availableYears = computed(() => {
76
+ // Get all years that have data
77
+ const yearsSet = new Set()
78
+ Object.keys(props.dailyData || {}).forEach(dateKey => {
79
+ const year = parseInt(leftPart(dateKey, '-'))
80
+ yearsSet.add(year)
81
+ })
82
+ return Array.from(yearsSet).sort((a, b) => a - b)
83
+ })
84
+
85
+ const availableMonthsForYear = computed(() => {
86
+ // Get all months that have data for the selected year
87
+ const monthsSet = new Set()
88
+ Object.keys(props.dailyData || {}).forEach(dateKey => {
89
+ const date = new Date(dateKey + 'T00:00:00Z')
90
+ if (date.getFullYear() === selectedYear.value) {
91
+ monthsSet.add(date.getMonth() + 1)
92
+ }
93
+ })
94
+ return Array.from(monthsSet).sort((a, b) => a - b)
95
+ })
96
+
97
+ return {
98
+ selectedMonth,
99
+ selectedYear,
100
+ updateSelection,
101
+ availableYears,
102
+ availableMonthsForYear,
103
+ }
104
+ }
105
+ }
106
+
107
+ export const Analytics = {
108
+ template: `
109
+ <div class="flex flex-col w-full">
110
+ <!-- Header -->
111
+ <div class="border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 px-2 sm:px-4 py-3">
112
+ <div
113
+ :class="!$ai.isSidebarOpen ? 'pl-3' : ''"
114
+ class="max-w-6xl mx-auto flex flex-col sm:flex-row items-stretch sm:items-center justify-between gap-3">
115
+ <h2 class="text-lg font-semibold text-gray-900 dark:text-gray-100 flex-shrink-0">
116
+ <RouterLink to="/analytics">Analytics</RouterLink>
117
+ </h2>
118
+ <MonthSelector :dailyData="allDailyData" />
119
+ </div>
120
+ </div>
121
+
122
+ <!-- Tabs -->
123
+ <div class="border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 px-4">
124
+ <div class="max-w-6xl mx-auto flex gap-8">
125
+ <button type="button"
126
+ @click="activeTab = 'cost'"
127
+ :class="['py-3 px-1 border-b-2 font-medium text-sm transition-colors',
128
+ activeTab === 'cost'
129
+ ? 'border-blue-500 text-blue-600 dark:text-blue-400'
130
+ : 'border-transparent text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200']">
131
+ Cost Analysis
132
+ </button>
133
+ <button type="button"
134
+ @click="activeTab = 'tokens'"
135
+ :class="['py-3 px-1 border-b-2 font-medium text-sm transition-colors',
136
+ activeTab === 'tokens'
137
+ ? 'border-blue-500 text-blue-600 dark:text-blue-400'
138
+ : 'border-transparent text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200']">
139
+ Token Usage
140
+ </button>
141
+ <button type="button"
142
+ @click="activeTab = 'activity'"
143
+ :class="['py-3 px-1 border-b-2 font-medium text-sm transition-colors',
144
+ activeTab === 'activity'
145
+ ? 'border-blue-500 text-blue-600 dark:text-blue-400'
146
+ : 'border-transparent text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200']">
147
+ Activity
148
+ </button>
149
+ </div>
150
+ </div>
151
+
152
+ <!-- Content -->
153
+ <div class="flex-1 bg-gray-50 dark:bg-gray-900" :class="activeTab === 'activity' ? 'p-0' : 'p-4'">
154
+
155
+ <div :class="activeTab === 'activity' ? '' : 'max-w-6xl mx-auto'">
156
+ <!-- Stats Summary (hidden for Activity tab) -->
157
+ <div v-if="activeTab !== 'activity'" class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
158
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
159
+ <div class="text-sm font-medium text-gray-600 dark:text-gray-400">Total Cost</div>
160
+ <div class="text-2xl font-bold text-gray-900 dark:text-gray-100 mt-1">{{ $fmt.cost(totalCost) }}</div>
161
+ </div>
162
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
163
+ <div class="text-sm font-medium text-gray-600 dark:text-gray-400">Total Requests</div>
164
+ <div class="text-2xl font-bold text-gray-900 dark:text-gray-100 mt-1">{{ totalRequests }}</div>
165
+ </div>
166
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
167
+ <div class="text-sm font-medium text-gray-600 dark:text-gray-400">Total Input Tokens</div>
168
+ <div class="text-2xl font-bold text-gray-900 dark:text-gray-100 mt-1">{{ $fmt.humanifyNumber(totalInputTokens) }}</div>
169
+ </div>
170
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
171
+ <div class="text-sm font-medium text-gray-600 dark:text-gray-400">Total Output Tokens</div>
172
+ <div class="text-2xl font-bold text-gray-900 dark:text-gray-100 mt-1">{{ $fmt.humanifyNumber(totalOutputTokens) }}</div>
173
+ </div>
174
+ </div>
175
+
176
+ <!-- Cost Analysis Tab -->
177
+ <div v-if="activeTab === 'cost'" class="bg-white dark:bg-gray-800 rounded-lg shadow p-4 sm:p-6">
178
+ <div class="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 mb-6">
179
+ <h3 class="text-base sm:text-lg font-semibold text-gray-900 dark:text-gray-100">Daily Costs</h3>
180
+ <h3 class="text-sm sm:text-lg font-semibold text-gray-900 dark:text-gray-100">
181
+ {{ new Date(selectedDay).toLocaleDateString(undefined, { year: 'numeric', month: 'long' }) }}
182
+ </h3>
183
+ <select v-model="costChartType" class="px-3 pr-6 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-900 text-gray-700 dark:text-gray-300 rounded-md text-sm font-medium hover:bg-gray-50 dark:hover:bg-gray-800 flex-shrink-0">
184
+ <option value="bar">Bar Chart</option>
185
+ <option value="line">Line Chart</option>
186
+ </select>
187
+ </div>
188
+
189
+ <div v-if="chartData.labels.length > 0" class="relative h-96">
190
+ <canvas ref="costChartCanvas"></canvas>
191
+ </div>
192
+ <div v-else class="flex items-center justify-center h-96 text-gray-500 dark:text-gray-400">
193
+ <p>No request data available</p>
194
+ </div>
195
+ </div>
196
+
197
+ <!-- Token Usage Tab -->
198
+ <div v-if="activeTab === 'tokens'" class="bg-white dark:bg-gray-800 rounded-lg shadow p-4 sm:p-6">
199
+ <h3 class="text-base sm:text-lg font-semibold text-gray-900 dark:text-gray-100 mb-6 flex flex-col sm:flex-row justify-between items-start sm:items-center gap-2">
200
+ <span>Daily Token Usage</span>
201
+ <span class="text-sm sm:text-base">
202
+ {{ new Date(selectedDay).toLocaleDateString(undefined, { year: 'numeric', month: 'long' }) }}
203
+ </span>
204
+ </h3>
205
+
206
+ <div v-if="tokenChartData.labels.length > 0" class="relative h-96">
207
+ <canvas ref="tokenChartCanvas"></canvas>
208
+ </div>
209
+ <div v-else class="flex items-center justify-center h-96 text-gray-500 dark:text-gray-400">
210
+ <p>No request data available</p>
211
+ </div>
212
+ </div>
213
+
214
+ <div v-if="allDailyData[selectedDay]?.requests && ['cost', 'tokens'].includes(activeTab)" class="mt-8 px-2 text-sm sm:text-base text-gray-700 dark:text-gray-300 font-medium flex flex-col sm:flex-row items-start sm:items-center justify-between gap-2">
215
+ <div>
216
+ {{ new Date(selectedDay).toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' }) }}
217
+ </div>
218
+ <div class="flex flex-wrap gap-x-2 gap-y-1">
219
+ <span>{{ $fmt.cost(allDailyData[selectedDay]?.cost || 0) }}</span>
220
+ <span>&#183;</span>
221
+ <span>{{ allDailyData[selectedDay]?.requests || 0 }} Requests</span>
222
+ <span>&#183;</span>
223
+ <span>{{ $fmt.humanifyNumber(allDailyData[selectedDay]?.inputTokens || 0) }} -> {{ $fmt.humanifyNumber(allDailyData[selectedDay]?.outputTokens || 0) }} Tokens</span>
224
+ </div>
225
+ </div>
226
+
227
+ <!-- Pie Charts for Selected Day -->
228
+ <div v-if="allDailyData[selectedDay]?.requests && activeTab === 'cost' && selectedDay" class="mt-6 grid grid-cols-1 lg:grid-cols-2 gap-6">
229
+ <!-- Model Pie Chart -->
230
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
231
+ <h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
232
+ Cost by Model
233
+ </h3>
234
+ <div v-if="modelPieData.labels.length > 0" class="relative h-80">
235
+ <canvas ref="modelPieCanvas"></canvas>
236
+ </div>
237
+ <div v-else class="flex items-center justify-center h-80 text-gray-500 dark:text-gray-400">
238
+ <p>No data for selected day</p>
239
+ </div>
240
+ </div>
241
+
242
+ <!-- Provider Pie Chart -->
243
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
244
+ <h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
245
+ Cost by Provider
246
+ </h3>
247
+ <div v-if="providerPieData.labels.length > 0" class="relative h-80">
248
+ <canvas ref="providerPieCanvas"></canvas>
249
+ </div>
250
+ <div v-else class="flex items-center justify-center h-80 text-gray-500 dark:text-gray-400">
251
+ <p>No data for selected day</p>
252
+ </div>
253
+ </div>
254
+ </div>
255
+
256
+ <!-- Token Pie Charts for Selected Day -->
257
+ <div v-if="allDailyData[selectedDay]?.requests && activeTab === 'tokens' && selectedDay" class="mt-6 grid grid-cols-1 lg:grid-cols-2 gap-6">
258
+ <!-- Token Model Pie Chart -->
259
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
260
+ <h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
261
+ Tokens by Model
262
+ </h3>
263
+ <div v-if="tokenModelPieData.labels.length > 0" class="relative h-80">
264
+ <canvas ref="tokenModelPieCanvas"></canvas>
265
+ </div>
266
+ <div v-else class="flex items-center justify-center h-80 text-gray-500 dark:text-gray-400">
267
+ <p>No data for selected day</p>
268
+ </div>
269
+ </div>
270
+
271
+ <!-- Token Provider Pie Chart -->
272
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
273
+ <h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
274
+ Tokens by Provider
275
+ </h3>
276
+ <div v-if="tokenProviderPieData.labels.length > 0" class="relative h-80">
277
+ <canvas ref="tokenProviderPieCanvas"></canvas>
278
+ </div>
279
+ <div v-else class="flex items-center justify-center h-80 text-gray-500 dark:text-gray-400">
280
+ <p>No data for selected day</p>
281
+ </div>
282
+ </div>
283
+ </div>
284
+
285
+ <!-- Activity Tab - Full Page Layout -->
286
+ <div v-if="activeTab === 'activity'" class="flex flex-col bg-white dark:bg-gray-800">
287
+ <!-- Filters Bar -->
288
+ <div class="flex-shrink-0 border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 px-3 sm:px-6 py-4">
289
+ <div class="flex flex-wrap gap-2 sm:gap-4 items-end">
290
+ <div class="flex flex-col flex-1 min-w-[120px] sm:flex-initial">
291
+ <select v-model="selectedModel" class="px-3 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 w-full">
292
+ <option value="">All Models</option>
293
+ <option v-for="model in filterOptions.models" :key="model" :value="model">
294
+ {{ model }}
295
+ </option>
296
+ </select>
297
+ </div>
298
+
299
+ <div class="flex flex-col flex-1 min-w-[120px] sm:flex-initial">
300
+ <select v-model="selectedProvider" class="px-3 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 w-full">
301
+ <option value="">All Providers</option>
302
+ <option v-for="provider in filterOptions.providers" :key="provider" :value="provider">
303
+ {{ provider }}
304
+ </option>
305
+ </select>
306
+ </div>
307
+
308
+ <div class="flex flex-col flex-1 min-w-[140px] sm:flex-initial">
309
+ <select v-model="sortBy" class="px-3 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 w-full">
310
+ <option value="createdAt">Date (Newest)</option>
311
+ <option value="cost">Cost (Highest)</option>
312
+ <option value="duration">Duration (Longest)</option>
313
+ <option value="totalTokens">Tokens (Most)</option>
314
+ </select>
315
+ </div>
316
+
317
+ <button v-if="hasActiveFilters" @click="clearActivityFilters" class="px-4 py-2 text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors whitespace-nowrap">
318
+ Clear Filters
319
+ </button>
320
+ </div>
321
+ </div>
322
+
323
+ <!-- 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">
326
+ <div class="text-center">
327
+ <div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
328
+ <p class="mt-4 text-gray-600 dark:text-gray-400">Loading requests...</p>
329
+ </div>
330
+ </div>
331
+
332
+ <div v-else-if="activityRequests.length === 0" class="mt-4 flex items-center justify-center h-full">
333
+ <p class="text-gray-500 dark:text-gray-400">No requests found</p>
334
+ </div>
335
+
336
+ <div v-else class="divide-y divide-gray-200 dark:divide-gray-700">
337
+ <div v-for="request in activityRequests" :key="request.id" class="px-3 sm:px-6 py-4 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
338
+ <div class="flex flex-col lg:flex-row items-start justify-between gap-4">
339
+ <div class="flex-1 min-w-0 w-full">
340
+ <div class="flex flex-col sm:flex-row justify-between gap-2 mb-2">
341
+ <div class="flex items-center gap-2 flex-wrap">
342
+ <span v-if="request.model" class="text-xs px-2 py-1 bg-blue-100 dark:bg-blue-900/30 text-blue-800 dark:text-blue-300 rounded font-medium">{{ request.model }}</span>
343
+ <span v-if="request.provider" class="text-xs px-2 py-1 bg-purple-100 dark:bg-purple-900/30 text-purple-800 dark:text-purple-300 rounded font-medium">{{ request.provider }}</span>
344
+ <span v-if="request.providerRef" class="text-xs px-2 py-1 bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-300 rounded font-medium">{{ request.providerRef }}</span>
345
+ <span v-if="request.finishReason" class="text-xs px-2 py-1 bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-300 rounded font-medium">{{ request.finishReason }}</span>
346
+ </div>
347
+ <div class="text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap">
348
+ {{ formatActivityDate(request.createdAt) }}
349
+ </div>
350
+ </div>
351
+ <div class="text-sm font-semibold text-gray-900 dark:text-gray-100 truncate mb-3">
352
+ {{ request.title }}
353
+ </div>
354
+
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">
360
+ <div :title="request.cost">
361
+ <div class="text-xs text-gray-500 dark:text-gray-400 font-medium">Cost</div>
362
+ <div class="text-sm font-semibold text-gray-900 dark:text-gray-100">{{ $fmt.costLong(request.cost) }}</div>
363
+ </div>
364
+ <div class="col-span-2 sm:col-span-1">
365
+ <div class="text-xs text-gray-500 dark:text-gray-400 font-medium">Tokens</div>
366
+ <div v-if="request.inputTokens || request.outputTokens" class="text-sm font-semibold text-gray-900 dark:text-gray-100">
367
+ {{ $fmt.humanifyNumber(request.inputTokens || 0) }} -> {{ $fmt.humanifyNumber(request.outputTokens || 0) }}
368
+ <span v-if="request.inputCachedTokens" class="ml-1 text-xs text-gray-500 dark:text-gray-400">({{ $fmt.humanifyNumber(request.inputCachedTokens || 0) }} cached)</span>
369
+ </div>
370
+ </div>
371
+ <div>
372
+ <div class="text-xs text-gray-500 dark:text-gray-400 font-medium">Duration</div>
373
+ <div v-if="request.duration" class="text-sm font-semibold text-gray-900 dark:text-gray-100">{{ $fmt.humanifyMs(request.duration * 1000) }}</div>
374
+ </div>
375
+ <div>
376
+ <div class="text-xs text-gray-500 dark:text-gray-400 font-medium">Speed</div>
377
+ <div v-if="request.duration && request.outputTokens" class="text-sm font-semibold text-gray-900 dark:text-gray-100">{{ (request.outputTokens / (request.duration / 1000)).toFixed(1) + ' tok/s' }}</div>
378
+ </div>
379
+ </div>
380
+ </div>
381
+ <div class="flex flex-row lg:flex-col gap-2 w-full lg:w-auto">
382
+ <button type="button" v-if="threadExists(request.threadId)" @click="openThread(request.threadId)" class="flex-1 lg:flex-initial px-3 sm:px-4 py-2 text-sm font-medium text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 border border-blue-300 dark:border-blue-600 rounded hover:bg-blue-50 dark:hover:bg-blue-900/30 transition-colors whitespace-nowrap">
383
+ View<span class="hidden sm:inline"> Thread</span>
384
+ </button>
385
+ <button type="button" @click="deleteRequestLog(request.id)" class="flex-1 lg:flex-initial px-3 sm:px-4 py-2 text-sm font-medium text-red-600 dark:text-red-500 hover:text-red-800 dark:hover:text-red-400 border border-red-300 dark:border-red-600 rounded hover:bg-red-50 dark:hover:bg-red-900/30 transition-colors whitespace-nowrap">
386
+ Delete<span class="hidden sm:inline"> Request</span>
387
+ </button>
388
+ </div>
389
+ </div>
390
+ </div>
391
+
392
+ <div v-if="isActivityLoadingMore" class="px-6 py-8 text-center">
393
+ <div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto"></div>
394
+ </div>
395
+
396
+ <div v-if="!activityHasMore && activityRequests.length > 0" class="px-6 py-8 text-center text-gray-500 dark:text-gray-400 text-sm">
397
+ No more requests to load
398
+ </div>
399
+ </div>
400
+ <div ref="scrollSentinel" class="h-4 w-full"></div>
401
+ </div>
402
+ </div>
403
+ </div>
404
+ </div>
405
+ </div>
406
+ `,
407
+ setup() {
408
+ const ctx = inject('ctx')
409
+ const router = useRouter()
410
+ const route = useRoute()
411
+ const analyticsData = ref()
412
+
413
+ // Initialize activeTab from URL query parameter, default to 'cost'
414
+ const activeTab = ref(route.query.tab || 'cost')
415
+ const costChartType = ref('bar')
416
+ const costChartCanvas = ref(null)
417
+ const tokenChartCanvas = ref(null)
418
+ const modelPieCanvas = ref(null)
419
+ const providerPieCanvas = ref(null)
420
+ const tokenModelPieCanvas = ref(null)
421
+ const tokenProviderPieCanvas = ref(null)
422
+ let costChartInstance = null
423
+ let tokenChartInstance = null
424
+ let modelPieChartInstance = null
425
+ let providerPieChartInstance = null
426
+ let tokenModelPieChartInstance = null
427
+ let tokenProviderPieChartInstance = null
428
+
429
+ // Month/Year selection - read from URL as source of truth
430
+ const currentDate = new Date()
431
+ const selectedMonth = computed(() => {
432
+ return route.query.month !== undefined ? parseInt(route.query.month) : currentDate.getMonth() + 1
433
+ })
434
+ const selectedYear = computed(() => {
435
+ return route.query.year !== undefined ? parseInt(route.query.year) : currentDate.getFullYear()
436
+ })
437
+ const selectedYearMonth = computed(() => {
438
+ return `${selectedYear.value}-${selectedMonth.value < 10 ? '0' + selectedMonth.value : selectedMonth.value}`
439
+ })
440
+ const allDailyData = ref({}) // Store all data for filtering
441
+
442
+ // Selected day - read from URL, default to today
443
+ const selectedDay = computed(() => {
444
+ if (route.query.day !== undefined) {
445
+ return route.query.day
446
+ }
447
+ // Default to today
448
+ const today = new Date()
449
+ return today.toISOString().split('T')[0]
450
+ })
451
+
452
+ const chartData = ref({
453
+ labels: [],
454
+ datasets: [],
455
+ dateKeys: [] // Store full date keys for click handling
456
+ })
457
+
458
+ const tokenChartData = ref({
459
+ labels: [],
460
+ datasets: [],
461
+ dateKeys: [] // Store full date keys for click handling
462
+ })
463
+
464
+ const modelPieData = ref({
465
+ labels: [],
466
+ datasets: []
467
+ })
468
+
469
+ const providerPieData = ref({
470
+ labels: [],
471
+ datasets: []
472
+ })
473
+
474
+ const tokenModelPieData = ref({
475
+ labels: [],
476
+ datasets: []
477
+ })
478
+
479
+ const tokenProviderPieData = ref({
480
+ labels: [],
481
+ datasets: []
482
+ })
483
+
484
+ const totalCost = computed(() => {
485
+ // Calculate totals for selected month/year only
486
+ const filteredDates = Object.keys(allDailyData.value)
487
+ .filter(dateKey => {
488
+ const date = new Date(dateKey + 'T00:00:00Z')
489
+ return date.getFullYear() === selectedYear.value && (date.getMonth() + 1) === selectedMonth.value
490
+ })
491
+ return filteredDates.reduce((sum, date) => sum + (allDailyData.value[date].cost || 0), 0)
492
+ })
493
+
494
+ const totalRequests = computed(() => {
495
+ // Calculate totals for selected month/year only
496
+ const filteredDates = Object.keys(allDailyData.value)
497
+ .filter(dateKey => {
498
+ const date = new Date(dateKey + 'T00:00:00Z')
499
+ return date.getFullYear() === selectedYear.value && (date.getMonth() + 1) === selectedMonth.value
500
+ })
501
+ return filteredDates.reduce((sum, date) => sum + (allDailyData.value[date].requests || 0), 0)
502
+ })
503
+
504
+ const totalInputTokens = computed(() => {
505
+ // Calculate totals for selected month/year only
506
+ const filteredDates = Object.keys(allDailyData.value)
507
+ .filter(dateKey => {
508
+ const date = new Date(dateKey + 'T00:00:00Z')
509
+ return date.getFullYear() === selectedYear.value && (date.getMonth() + 1) === selectedMonth.value
510
+ })
511
+ return filteredDates.reduce((sum, date) => sum + (allDailyData.value[date].inputTokens || 0), 0)
512
+ })
513
+
514
+ const totalOutputTokens = computed(() => {
515
+ // Calculate totals for selected month/year only
516
+ const filteredDates = Object.keys(allDailyData.value)
517
+ .filter(dateKey => {
518
+ const date = new Date(dateKey + 'T00:00:00Z')
519
+ return date.getFullYear() === selectedYear.value && (date.getMonth() + 1) === selectedMonth.value
520
+ })
521
+ return filteredDates.reduce((sum, date) => sum + (allDailyData.value[date].outputTokens || 0), 0)
522
+ })
523
+
524
+ // Activity tab state
525
+ const activityRequests = ref([])
526
+ const isActivityLoading = ref(false)
527
+ const isActivityLoadingMore = ref(false)
528
+ const activityHasMore = ref(true)
529
+ const activityOffset = ref(0)
530
+ const activityPageSize = 20
531
+ const existingThreadIds = ref(new Set())
532
+
533
+ const selectedModel = ref('')
534
+ const selectedProvider = ref('')
535
+ const sortBy = ref('createdAt')
536
+ const filterOptions = ref({ models: [], providers: [] })
537
+ const scrollSentinel = ref(null)
538
+ let observer = null
539
+
540
+ const hasActiveFilters = computed(() => selectedModel.value || selectedProvider.value)
541
+
542
+ async function loadAnalyticsData() {
543
+ try {
544
+ // Group requests by date
545
+ analyticsData.value = await ctx.requests.getSummary()
546
+ allDailyData.value = analyticsData.value.dailyData
547
+
548
+ // Update chart data based on selected month/year
549
+ updateChartData()
550
+
551
+ await nextTick()
552
+ renderCostChart()
553
+ renderTokenChart()
554
+ } catch (error) {
555
+ console.error('Error loading analytics data:', error)
556
+ }
557
+ }
558
+
559
+ function updateChartData() {
560
+ // Filter data for selected month and year
561
+ const filteredDates = Object.keys(allDailyData.value)
562
+ .filter(dateKey => {
563
+ const date = new Date(dateKey + 'T00:00:00Z')
564
+ return date.getFullYear() === selectedYear.value && (date.getMonth() + 1) === selectedMonth.value
565
+ })
566
+ .sort()
567
+
568
+ const costs = filteredDates.map(date => allDailyData.value[date].cost)
569
+ const inputTokens = filteredDates.map(date => allDailyData.value[date].inputTokens)
570
+ const outputTokens = filteredDates.map(date => allDailyData.value[date].outputTokens)
571
+
572
+ // Extract day numbers from dates for labels
573
+ const dayLabels = filteredDates.map(dateKey => {
574
+ const date = new Date(dateKey + 'T00:00:00Z')
575
+ return date.getDate().toString()
576
+ })
577
+
578
+ // Cost chart data
579
+ chartData.value = {
580
+ labels: dayLabels,
581
+ dateKeys: filteredDates, // Store full date keys for click handling
582
+ datasets: [{
583
+ label: 'Daily Cost ($)',
584
+ data: costs,
585
+ backgroundColor: colors[0].background,
586
+ borderColor: colors[0].border,
587
+ borderWidth: 2,
588
+ tension: 0.1
589
+ }]
590
+ }
591
+
592
+ // Token chart data (stacked)
593
+ tokenChartData.value = {
594
+ labels: dayLabels,
595
+ dateKeys: filteredDates, // Store full date keys for click handling
596
+ datasets: [
597
+ {
598
+ label: 'Input Tokens',
599
+ data: inputTokens,
600
+ backgroundColor: 'rgba(168, 85, 247, 0.2)',
601
+ borderColor: 'rgb(126, 34, 206)',
602
+ borderWidth: 1
603
+ },
604
+ {
605
+ label: 'Output Tokens',
606
+ data: outputTokens,
607
+ backgroundColor: 'rgba(251, 146, 60, 0.2)',
608
+ borderColor: 'rgb(234, 88, 12)',
609
+ borderWidth: 1
610
+ }
611
+ ]
612
+ }
613
+ }
614
+
615
+ async function updatePieChartData(dateKey) {
616
+ if (!dateKey) {
617
+ modelPieData.value = { labels: [], datasets: [] }
618
+ providerPieData.value = { labels: [], datasets: [] }
619
+ return
620
+ }
621
+
622
+ try {
623
+ const dailySummary = await ctx.requests.getDailySummary(dateKey)
624
+ const { modelData, providerData } = dailySummary
625
+
626
+ // Prepare model pie chart data
627
+ const modelLabels = Object.keys(modelData).sort()
628
+ const modelCosts = modelLabels.map(model => modelData[model].cost)
629
+
630
+ modelPieData.value = {
631
+ labels: modelLabels,
632
+ datasets: [{
633
+ label: 'Cost by Model',
634
+ data: modelCosts,
635
+ backgroundColor: colors.map(c => c.background),
636
+ borderColor: colors.map(c => c.border),
637
+ borderWidth: 2
638
+ }]
639
+ }
640
+
641
+ // Prepare provider pie chart data
642
+ const providerLabels = Object.keys(providerData).sort()
643
+ const providerCosts = providerLabels.map(provider => providerData[provider].cost)
644
+
645
+ providerPieData.value = {
646
+ labels: providerLabels,
647
+ datasets: [{
648
+ label: 'Cost by Provider',
649
+ data: providerCosts,
650
+ backgroundColor: colors.map(c => c.background),
651
+ borderColor: colors.map(c => c.border),
652
+ borderWidth: 2
653
+ }]
654
+ }
655
+ } catch (error) {
656
+ console.error('Error updating pie chart data:', error)
657
+ }
658
+ }
659
+
660
+ async function updateTokenPieChartData(dateKey) {
661
+ if (!dateKey) {
662
+ tokenModelPieData.value = { labels: [], datasets: [] }
663
+ tokenProviderPieData.value = { labels: [], datasets: [] }
664
+ return
665
+ }
666
+
667
+ try {
668
+ const dailySummary = await ctx.requests.getDailySummary(dateKey)
669
+ const { modelData, providerData } = dailySummary
670
+
671
+ // Prepare model pie chart data
672
+ const modelLabels = Object.keys(modelData).sort()
673
+ const modelTokens = modelLabels.map(model => modelData[model].tokens)
674
+
675
+ tokenModelPieData.value = {
676
+ labels: modelLabels,
677
+ datasets: [{
678
+ label: 'Tokens by Model',
679
+ data: modelTokens,
680
+ backgroundColor: colors.map(c => c.background),
681
+ borderColor: colors.map(c => c.border),
682
+ borderWidth: 2
683
+ }]
684
+ }
685
+
686
+ // Prepare provider pie chart data
687
+ const providerLabels = Object.keys(providerData).sort()
688
+ const providerTokens = providerLabels.map(provider => providerData[provider].tokens)
689
+
690
+ tokenProviderPieData.value = {
691
+ labels: providerLabels,
692
+ datasets: [{
693
+ label: 'Tokens by Provider',
694
+ data: providerTokens,
695
+ backgroundColor: colors.map(c => c.background),
696
+ borderColor: colors.map(c => c.border),
697
+ borderWidth: 2
698
+ }]
699
+ }
700
+ } catch (error) {
701
+ console.error('Error updating token pie chart data:', error)
702
+ }
703
+ }
704
+
705
+ function renderCostChart() {
706
+ if (!costChartCanvas.value || chartData.value.labels.length === 0) return
707
+
708
+ // Destroy existing chart
709
+ if (costChartInstance) {
710
+ costChartInstance.destroy()
711
+ }
712
+
713
+ const ctx2d = costChartCanvas.value.getContext('2d')
714
+ const chartTypeValue = costChartType.value
715
+
716
+ // Find the index of the selected day
717
+ const selectedDayIndex = chartData.value.dateKeys.indexOf(selectedDay.value)
718
+
719
+ // Create color arrays with highlight for selected day
720
+ const backgroundColor = chartData.value.dateKeys.map((_, index) => {
721
+ if (index === selectedDayIndex) {
722
+ return 'rgba(34, 197, 94, 0.8)' // Green for selected day
723
+ }
724
+ return colors[0].background
725
+ })
726
+
727
+ const borderColor = chartData.value.dateKeys.map((_, index) => {
728
+ if (index === selectedDayIndex) {
729
+ return 'rgb(22, 163, 74)' // Darker green for selected day
730
+ }
731
+ return colors[0].border
732
+ })
733
+
734
+ // Update dataset with dynamic colors
735
+ const chartDataWithColors = {
736
+ ...chartData.value,
737
+ datasets: [{
738
+ ...chartData.value.datasets[0],
739
+ backgroundColor: backgroundColor,
740
+ borderColor: borderColor
741
+ }]
742
+ }
743
+
744
+ costChartInstance = new Chart(ctx2d, {
745
+ type: chartTypeValue,
746
+ data: chartDataWithColors,
747
+ options: {
748
+ responsive: true,
749
+ maintainAspectRatio: false,
750
+ onClick: async (_, elements) => {
751
+ if (chartTypeValue === 'bar' && elements.length > 0) {
752
+ const index = elements[0].index
753
+ const dateKey = chartData.value.dateKeys[index]
754
+ // Update URL with selected day
755
+ router.push({
756
+ query: {
757
+ ...route.query,
758
+ day: dateKey
759
+ }
760
+ })
761
+ }
762
+ },
763
+ plugins: {
764
+ legend: {
765
+ display: true,
766
+ position: 'top'
767
+ },
768
+ tooltip: {
769
+ callbacks: {
770
+ title: function (context) {
771
+ const index = context[0].dataIndex
772
+ const dateKey = chartData.value.dateKeys[index]
773
+ const date = new Date(dateKey + 'T00:00:00Z')
774
+ return date.toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' })
775
+ },
776
+ label: function (context) {
777
+ return `Cost: ${ctx.fmt.cost(context.parsed.y)}`
778
+ }
779
+ }
780
+ }
781
+ },
782
+ scales: {
783
+ y: {
784
+ beginAtZero: true,
785
+ ticks: {
786
+ callback: function (value) {
787
+ return '$' + value.toFixed(4)
788
+ }
789
+ }
790
+ }
791
+ }
792
+ }
793
+ })
794
+ }
795
+
796
+ function renderTokenChart() {
797
+ if (!tokenChartCanvas.value || tokenChartData.value.labels.length === 0) return
798
+
799
+ // Destroy existing chart
800
+ if (tokenChartInstance) {
801
+ tokenChartInstance.destroy()
802
+ }
803
+
804
+ const ctx2d = tokenChartCanvas.value.getContext('2d')
805
+
806
+ // Find the index of the selected day
807
+ const selectedDayIndex = tokenChartData.value.dateKeys.indexOf(selectedDay.value)
808
+
809
+ // Create color arrays with highlight for selected day
810
+ const inputBackgroundColor = tokenChartData.value.dateKeys.map((_, index) => {
811
+ if (index === selectedDayIndex) {
812
+ return 'rgba(34, 197, 94, 0.2)' // Green for selected day (light/transparent)
813
+ }
814
+ return 'rgba(168, 85, 247, 0.2)' // Purple for input tokens (light/transparent)
815
+ })
816
+
817
+ const inputBorderColor = tokenChartData.value.dateKeys.map((_, index) => {
818
+ if (index === selectedDayIndex) {
819
+ return 'rgb(22, 163, 74)' // Darker green for selected day
820
+ }
821
+ return 'rgb(126, 34, 206)' // Darker purple for input tokens
822
+ })
823
+
824
+ const outputBackgroundColor = tokenChartData.value.dateKeys.map((_, index) => {
825
+ if (index === selectedDayIndex) {
826
+ return 'rgba(34, 197, 94, 0.2)' // Green for selected day (light/transparent)
827
+ }
828
+ return 'rgba(251, 146, 60, 0.2)' // Orange for output tokens (light/transparent)
829
+ })
830
+
831
+ const outputBorderColor = tokenChartData.value.dateKeys.map((_, index) => {
832
+ if (index === selectedDayIndex) {
833
+ return 'rgb(22, 163, 74)' // Darker green for selected day
834
+ }
835
+ return 'rgb(234, 88, 12)' // Darker orange for output tokens
836
+ })
837
+
838
+ // Update datasets with dynamic colors
839
+ const chartDataWithColors = {
840
+ ...tokenChartData.value,
841
+ datasets: [
842
+ {
843
+ ...tokenChartData.value.datasets[0],
844
+ backgroundColor: inputBackgroundColor,
845
+ borderColor: inputBorderColor
846
+ },
847
+ {
848
+ ...tokenChartData.value.datasets[1],
849
+ backgroundColor: outputBackgroundColor,
850
+ borderColor: outputBorderColor
851
+ }
852
+ ]
853
+ }
854
+
855
+ tokenChartInstance = new Chart(ctx2d, {
856
+ type: 'bar',
857
+ data: chartDataWithColors,
858
+ options: {
859
+ responsive: true,
860
+ maintainAspectRatio: false,
861
+ indexAxis: 'x',
862
+ onClick: async (_, elements) => {
863
+ if (elements.length > 0) {
864
+ const index = elements[0].index
865
+ const dateKey = tokenChartData.value.dateKeys[index]
866
+ // Update URL with selected day
867
+ router.push({
868
+ query: {
869
+ ...route.query,
870
+ day: dateKey
871
+ }
872
+ })
873
+ }
874
+ },
875
+ scales: {
876
+ x: {
877
+ stacked: true
878
+ },
879
+ y: {
880
+ stacked: true,
881
+ beginAtZero: true,
882
+ ticks: {
883
+ callback: function (value) {
884
+ return ctx.fmt.humanifyNumber(value)
885
+ }
886
+ }
887
+ }
888
+ },
889
+ plugins: {
890
+ legend: {
891
+ display: true,
892
+ position: 'top'
893
+ },
894
+ tooltip: {
895
+ callbacks: {
896
+ title: function (context) {
897
+ const index = context[0].dataIndex
898
+ const dateKey = tokenChartData.value.dateKeys[index]
899
+ const date = new Date(dateKey + 'T00:00:00Z')
900
+ return date.toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' })
901
+ },
902
+ label: function (context) {
903
+ return `${context.dataset.label}: ${ctx.fmt.humanifyNumber(context.parsed.y)}`
904
+ }
905
+ }
906
+ }
907
+ }
908
+ }
909
+ })
910
+ }
911
+
912
+ function renderModelPieChart() {
913
+ if (!modelPieCanvas.value || modelPieData.value.labels.length === 0) return
914
+
915
+ // Destroy existing chart
916
+ if (modelPieChartInstance) {
917
+ modelPieChartInstance.destroy()
918
+ }
919
+
920
+ const ctx2d = modelPieCanvas.value.getContext('2d')
921
+
922
+ // Custom plugin to draw percentage labels on pie slices
923
+ const percentagePlugin = {
924
+ id: 'percentageLabel',
925
+ afterDatasetsDraw(chart) {
926
+ const { ctx: chartCtx, data } = chart
927
+ chart.getDatasetMeta(0).data.forEach((datapoint, index) => {
928
+ const { x, y } = datapoint.tooltipPosition()
929
+ const value = data.datasets[0].data[index]
930
+ const sum = data.datasets[0].data.reduce((a, b) => a + b, 0)
931
+ const percentage = ((value * 100) / sum).toFixed(1)
932
+
933
+ // Only display label if percentage > 1%
934
+ if (parseFloat(percentage) > 1) {
935
+ // Use white color in dark mode, black in light mode
936
+ const isDarkMode = document.documentElement.classList.contains('dark')
937
+ chartCtx.fillStyle = isDarkMode ? '#fff' : '#000'
938
+ chartCtx.font = 'bold 12px Arial'
939
+ chartCtx.textAlign = 'center'
940
+ chartCtx.textBaseline = 'middle'
941
+ chartCtx.fillText(percentage + '%', x, y)
942
+ }
943
+ })
944
+ }
945
+ }
946
+
947
+ modelPieChartInstance = new Chart(ctx2d, {
948
+ type: 'pie',
949
+ data: modelPieData.value,
950
+ options: {
951
+ responsive: true,
952
+ maintainAspectRatio: false,
953
+ plugins: {
954
+ legend: {
955
+ display: true,
956
+ position: 'right'
957
+ },
958
+ tooltip: {
959
+ callbacks: {
960
+ label: function (context) {
961
+ return `${context.label}: ${ctx.fmt.cost(context.parsed)}`
962
+ }
963
+ }
964
+ }
965
+ }
966
+ },
967
+ plugins: [percentagePlugin]
968
+ })
969
+ }
970
+
971
+ function renderProviderPieChart() {
972
+ if (!providerPieCanvas.value || providerPieData.value.labels.length === 0) return
973
+
974
+ // Destroy existing chart
975
+ if (providerPieChartInstance) {
976
+ providerPieChartInstance.destroy()
977
+ }
978
+
979
+ const ctx2d = providerPieCanvas.value.getContext('2d')
980
+
981
+ // Custom plugin to draw percentage labels on pie slices
982
+ const percentagePlugin = {
983
+ id: 'percentageLabel',
984
+ afterDatasetsDraw(chart) {
985
+ const { ctx: chartCtx, data } = chart
986
+ chart.getDatasetMeta(0).data.forEach((datapoint, index) => {
987
+ const { x, y } = datapoint.tooltipPosition()
988
+ const value = data.datasets[0].data[index]
989
+ const sum = data.datasets[0].data.reduce((a, b) => a + b, 0)
990
+ const percentage = ((value * 100) / sum).toFixed(1)
991
+
992
+ // Only display label if percentage > 1%
993
+ if (parseFloat(percentage) > 1) {
994
+ // Use white color in dark mode, black in light mode
995
+ const isDarkMode = document.documentElement.classList.contains('dark')
996
+ chartCtx.fillStyle = isDarkMode ? '#fff' : '#000'
997
+ chartCtx.font = 'bold 12px Arial'
998
+ chartCtx.textAlign = 'center'
999
+ chartCtx.textBaseline = 'middle'
1000
+ chartCtx.fillText(percentage + '%', x, y)
1001
+ }
1002
+ })
1003
+ }
1004
+ }
1005
+
1006
+ providerPieChartInstance = new Chart(ctx2d, {
1007
+ type: 'pie',
1008
+ data: providerPieData.value,
1009
+ options: {
1010
+ responsive: true,
1011
+ maintainAspectRatio: false,
1012
+ plugins: {
1013
+ legend: {
1014
+ display: true,
1015
+ position: 'right'
1016
+ },
1017
+ tooltip: {
1018
+ callbacks: {
1019
+ label: function (context) {
1020
+ return `${context.label}: ${ctx.fmt.cost(context.parsed)}`
1021
+ }
1022
+ }
1023
+ }
1024
+ }
1025
+ },
1026
+ plugins: [percentagePlugin]
1027
+ })
1028
+ }
1029
+
1030
+ function renderTokenModelPieChart() {
1031
+ if (!tokenModelPieCanvas.value || tokenModelPieData.value.labels.length === 0) return
1032
+
1033
+ // Destroy existing chart
1034
+ if (tokenModelPieChartInstance) {
1035
+ tokenModelPieChartInstance.destroy()
1036
+ }
1037
+
1038
+ const ctx2d = tokenModelPieCanvas.value.getContext('2d')
1039
+
1040
+ // Custom plugin to draw percentage labels on pie slices
1041
+ const percentagePlugin = {
1042
+ id: 'percentageLabel',
1043
+ afterDatasetsDraw(chart) {
1044
+ const { ctx: chartCtx, data } = chart
1045
+ chart.getDatasetMeta(0).data.forEach((datapoint, index) => {
1046
+ const { x, y } = datapoint.tooltipPosition()
1047
+ const value = data.datasets[0].data[index]
1048
+ const sum = data.datasets[0].data.reduce((a, b) => a + b, 0)
1049
+ const percentage = ((value * 100) / sum).toFixed(1)
1050
+
1051
+ // Only display label if percentage > 1%
1052
+ if (parseFloat(percentage) > 1) {
1053
+ // Use white color in dark mode, black in light mode
1054
+ const isDarkMode = document.documentElement.classList.contains('dark')
1055
+ chartCtx.fillStyle = isDarkMode ? '#fff' : '#000'
1056
+ chartCtx.font = 'bold 12px Arial'
1057
+ chartCtx.textAlign = 'center'
1058
+ chartCtx.textBaseline = 'middle'
1059
+ chartCtx.fillText(percentage + '%', x, y)
1060
+ }
1061
+ })
1062
+ }
1063
+ }
1064
+
1065
+ tokenModelPieChartInstance = new Chart(ctx2d, {
1066
+ type: 'pie',
1067
+ data: tokenModelPieData.value,
1068
+ options: {
1069
+ responsive: true,
1070
+ maintainAspectRatio: false,
1071
+ plugins: {
1072
+ legend: {
1073
+ display: true,
1074
+ position: 'right'
1075
+ },
1076
+ tooltip: {
1077
+ callbacks: {
1078
+ label: function (context) {
1079
+ return `${context.label}: ${ctx.fmt.humanifyNumber(context.parsed)}`
1080
+ }
1081
+ }
1082
+ }
1083
+ }
1084
+ },
1085
+ plugins: [percentagePlugin]
1086
+ })
1087
+ }
1088
+
1089
+ function renderTokenProviderPieChart() {
1090
+ if (!tokenProviderPieCanvas.value || tokenProviderPieData.value.labels.length === 0) return
1091
+
1092
+ // Destroy existing chart
1093
+ if (tokenProviderPieChartInstance) {
1094
+ tokenProviderPieChartInstance.destroy()
1095
+ }
1096
+
1097
+ const ctx2d = tokenProviderPieCanvas.value.getContext('2d')
1098
+
1099
+ // Custom plugin to draw percentage labels on pie slices
1100
+ const percentagePlugin = {
1101
+ id: 'percentageLabel',
1102
+ afterDatasetsDraw(chart) {
1103
+ const { ctx: chartCtx, data } = chart
1104
+ chart.getDatasetMeta(0).data.forEach((datapoint, index) => {
1105
+ const { x, y } = datapoint.tooltipPosition()
1106
+ const value = data.datasets[0].data[index]
1107
+ const sum = data.datasets[0].data.reduce((a, b) => a + b, 0)
1108
+ const percentage = ((value * 100) / sum).toFixed(1)
1109
+
1110
+ // Only display label if percentage > 1%
1111
+ if (parseFloat(percentage) > 1) {
1112
+ // Use white color in dark mode, black in light mode
1113
+ const isDarkMode = document.documentElement.classList.contains('dark')
1114
+ chartCtx.fillStyle = isDarkMode ? '#fff' : '#000'
1115
+ chartCtx.font = 'bold 12px Arial'
1116
+ chartCtx.textAlign = 'center'
1117
+ chartCtx.textBaseline = 'middle'
1118
+ chartCtx.fillText(percentage + '%', x, y)
1119
+ }
1120
+ })
1121
+ }
1122
+ }
1123
+
1124
+ tokenProviderPieChartInstance = new Chart(ctx2d, {
1125
+ type: 'pie',
1126
+ data: tokenProviderPieData.value,
1127
+ options: {
1128
+ responsive: true,
1129
+ maintainAspectRatio: false,
1130
+ plugins: {
1131
+ legend: {
1132
+ display: true,
1133
+ position: 'right'
1134
+ },
1135
+ tooltip: {
1136
+ callbacks: {
1137
+ label: function (context) {
1138
+ return `${context.label}: ${ctx.fmt.humanifyNumber(context.parsed)}`
1139
+ }
1140
+ }
1141
+ }
1142
+ }
1143
+ },
1144
+ plugins: [percentagePlugin]
1145
+ })
1146
+ }
1147
+
1148
+ // Activity tab functions
1149
+ const loadActivityFilterOptions = async () => {
1150
+ try {
1151
+ filterOptions.value = await ctx.requests.getFilterOptions()
1152
+ } catch (error) {
1153
+ console.error('Failed to load filter options:', error)
1154
+ }
1155
+ }
1156
+
1157
+ const loadExistingThreadIds = async () => {
1158
+ try {
1159
+ existingThreadIds.value = new Set(await ctx.requests.getThreadIds({
1160
+ month: selectedYearMonth.value,
1161
+ }))
1162
+ } catch (error) {
1163
+ console.error('Failed to load existing thread IDs:', error)
1164
+ existingThreadIds.value = new Set()
1165
+ }
1166
+ }
1167
+
1168
+ const threadExists = (threadId) => {
1169
+ return threadId ? existingThreadIds.value.has(threadId) : false
1170
+ }
1171
+
1172
+ const loadActivityRequests = async (reset = false) => {
1173
+ if (reset) {
1174
+ activityOffset.value = 0
1175
+ activityRequests.value = []
1176
+ isActivityLoading.value = true
1177
+ // Load all existing thread IDs once when resetting
1178
+ await loadExistingThreadIds()
1179
+ } else {
1180
+ isActivityLoadingMore.value = true
1181
+ }
1182
+
1183
+ 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
+ })
1192
+
1193
+ const hasMore = requests.length >= activityPageSize
1194
+
1195
+ if (reset) {
1196
+ activityRequests.value = requests
1197
+ } else {
1198
+ activityRequests.value.push(...requests)
1199
+ }
1200
+
1201
+ activityHasMore.value = hasMore
1202
+ activityOffset.value += activityPageSize
1203
+ } catch (error) {
1204
+ console.error('Failed to load requests:', error)
1205
+ } finally {
1206
+ isActivityLoading.value = false
1207
+ isActivityLoadingMore.value = false
1208
+ }
1209
+ }
1210
+
1211
+ const setupObserver = () => {
1212
+ if (observer) observer.disconnect()
1213
+
1214
+ observer = new IntersectionObserver((entries) => {
1215
+ if (entries[0].isIntersecting && activityHasMore.value && !isActivityLoadingMore.value && !isActivityLoading.value) {
1216
+ loadActivityRequests(false)
1217
+ }
1218
+ }, { rootMargin: '200px' })
1219
+
1220
+ if (scrollSentinel.value) {
1221
+ observer.observe(scrollSentinel.value)
1222
+ }
1223
+ }
1224
+
1225
+ const clearActivityFilters = async () => {
1226
+ selectedModel.value = ''
1227
+ selectedProvider.value = ''
1228
+ sortBy.value = 'createdAt'
1229
+ await loadActivityRequests(true)
1230
+ }
1231
+
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
+
1237
+ }
1238
+
1239
+ const openThread = (threadId) => {
1240
+ router.push(`${router.currentRoute.value.path.split('/').slice(0, -1).join('/')}/c/${threadId}`)
1241
+ }
1242
+
1243
+ 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()
1250
+ }
1251
+ }
1252
+
1253
+ watch(costChartType, () => {
1254
+ renderCostChart()
1255
+ })
1256
+
1257
+ watch(() => route.query, async () => {
1258
+ updateChartData()
1259
+ await nextTick()
1260
+ renderCostChart()
1261
+ renderTokenChart()
1262
+
1263
+ // Also update pie charts if a day is selected
1264
+ if (selectedDay.value) {
1265
+ if (activeTab.value === 'cost') {
1266
+ await updatePieChartData(selectedDay.value)
1267
+ await nextTick()
1268
+ renderModelPieChart()
1269
+ renderProviderPieChart()
1270
+ } else if (activeTab.value === 'tokens') {
1271
+ await updateTokenPieChartData(selectedDay.value)
1272
+ await nextTick()
1273
+ renderTokenModelPieChart()
1274
+ renderTokenProviderPieChart()
1275
+ }
1276
+ }
1277
+ })
1278
+
1279
+ watch(selectedDay, async (newDay) => {
1280
+ if (newDay) {
1281
+ if (activeTab.value === 'cost') {
1282
+ await updatePieChartData(newDay)
1283
+ await nextTick()
1284
+ renderModelPieChart()
1285
+ renderProviderPieChart()
1286
+ } else if (activeTab.value === 'tokens') {
1287
+ await updateTokenPieChartData(newDay)
1288
+ await nextTick()
1289
+ renderTokenModelPieChart()
1290
+ renderTokenProviderPieChart()
1291
+ }
1292
+ }
1293
+ })
1294
+
1295
+ watch(modelPieData, () => {
1296
+ renderModelPieChart()
1297
+ }, { deep: true })
1298
+
1299
+ watch(providerPieData, () => {
1300
+ renderProviderPieChart()
1301
+ }, { deep: true })
1302
+
1303
+ watch(tokenModelPieData, () => {
1304
+ renderTokenModelPieChart()
1305
+ }, { deep: true })
1306
+
1307
+ watch(tokenProviderPieData, () => {
1308
+ renderTokenProviderPieChart()
1309
+ }, { deep: true })
1310
+
1311
+ watch(activeTab, async (newTab) => {
1312
+ // Update URL when tab changes, preserving other query parameters
1313
+ router.push({ query: { ...route.query, tab: newTab } })
1314
+
1315
+ await nextTick()
1316
+ if (newTab === 'cost') {
1317
+ renderCostChart()
1318
+ renderModelPieChart()
1319
+ renderProviderPieChart()
1320
+ } else if (newTab === 'tokens') {
1321
+ renderTokenChart()
1322
+ // Load token pie data if not already loaded
1323
+ if (tokenModelPieData.value.labels.length === 0 && selectedDay.value) {
1324
+ await updateTokenPieChartData(selectedDay.value)
1325
+ await nextTick()
1326
+ }
1327
+ renderTokenModelPieChart()
1328
+ renderTokenProviderPieChart()
1329
+ } else if (newTab === 'activity') {
1330
+ await loadActivityFilterOptions()
1331
+ await loadActivityRequests(true)
1332
+ await nextTick()
1333
+ setupObserver()
1334
+ }
1335
+ })
1336
+
1337
+ // Watch for activity filter changes and reload requests
1338
+ watch([selectedModel, selectedProvider, sortBy, selectedMonth, selectedYear], async () => {
1339
+ if (activeTab.value === 'activity') {
1340
+ await loadActivityRequests(true)
1341
+ }
1342
+ })
1343
+
1344
+ onMounted(async () => {
1345
+ await loadAnalyticsData()
1346
+
1347
+ // Load pie chart data for the selected day (default to today)
1348
+ await nextTick()
1349
+
1350
+ if (activeTab.value === 'cost') {
1351
+ await updatePieChartData(selectedDay.value)
1352
+ await nextTick()
1353
+ renderModelPieChart()
1354
+ renderProviderPieChart()
1355
+ } else if (activeTab.value === 'tokens') {
1356
+ await updateTokenPieChartData(selectedDay.value)
1357
+ await nextTick()
1358
+ renderTokenModelPieChart()
1359
+ renderTokenProviderPieChart()
1360
+ }
1361
+
1362
+ // If Activity tab is active on page load, load activity data
1363
+ if (activeTab.value === 'activity') {
1364
+ await loadActivityFilterOptions()
1365
+ await loadActivityRequests(true)
1366
+ await nextTick()
1367
+ setupObserver()
1368
+ }
1369
+ })
1370
+
1371
+ onUnmounted(() => {
1372
+ if (observer) observer.disconnect()
1373
+ })
1374
+
1375
+ return {
1376
+ activeTab,
1377
+ costChartType,
1378
+ costChartCanvas,
1379
+ tokenChartCanvas,
1380
+ modelPieCanvas,
1381
+ providerPieCanvas,
1382
+ tokenModelPieCanvas,
1383
+ tokenProviderPieCanvas,
1384
+ chartData,
1385
+ tokenChartData,
1386
+ modelPieData,
1387
+ providerPieData,
1388
+ tokenModelPieData,
1389
+ tokenProviderPieData,
1390
+ selectedDay,
1391
+ totalCost,
1392
+ totalRequests,
1393
+ totalInputTokens,
1394
+ totalOutputTokens,
1395
+ // Month/Year selection
1396
+ selectedMonth,
1397
+ selectedYear,
1398
+ allDailyData,
1399
+ // Activity tab
1400
+ activityRequests,
1401
+ isActivityLoading,
1402
+ isActivityLoadingMore,
1403
+ activityHasMore,
1404
+ selectedModel,
1405
+ selectedProvider,
1406
+ sortBy,
1407
+ filterOptions,
1408
+ hasActiveFilters,
1409
+ hasActiveFilters,
1410
+ scrollSentinel,
1411
+ clearActivityFilters,
1412
+ formatActivityDate,
1413
+ threadExists,
1414
+ openThread,
1415
+ deleteRequestLog,
1416
+ loadActivityFilterOptions,
1417
+ loadActivityRequests,
1418
+ }
1419
+ }
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
+ }