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.
- llms/__init__.py +4 -0
- llms/__main__.py +9 -0
- llms/db.py +359 -0
- llms/extensions/analytics/ui/index.mjs +1444 -0
- llms/extensions/app/README.md +20 -0
- llms/extensions/app/__init__.py +589 -0
- llms/extensions/app/db.py +536 -0
- {llms_py-2.0.9.data/data → llms/extensions/app}/ui/Recents.mjs +100 -73
- llms_py-2.0.9.data/data/ui/Sidebar.mjs → llms/extensions/app/ui/index.mjs +150 -79
- llms/extensions/app/ui/threadStore.mjs +433 -0
- llms/extensions/core_tools/CALCULATOR.md +32 -0
- llms/extensions/core_tools/__init__.py +637 -0
- llms/extensions/core_tools/ui/codemirror/addon/edit/closebrackets.js +201 -0
- llms/extensions/core_tools/ui/codemirror/addon/edit/closetag.js +185 -0
- llms/extensions/core_tools/ui/codemirror/addon/edit/continuelist.js +101 -0
- llms/extensions/core_tools/ui/codemirror/addon/edit/matchbrackets.js +160 -0
- llms/extensions/core_tools/ui/codemirror/addon/edit/matchtags.js +66 -0
- llms/extensions/core_tools/ui/codemirror/addon/edit/trailingspace.js +27 -0
- llms/extensions/core_tools/ui/codemirror/addon/selection/active-line.js +72 -0
- llms/extensions/core_tools/ui/codemirror/addon/selection/mark-selection.js +119 -0
- llms/extensions/core_tools/ui/codemirror/addon/selection/selection-pointer.js +98 -0
- llms/extensions/core_tools/ui/codemirror/codemirror.css +344 -0
- llms/extensions/core_tools/ui/codemirror/codemirror.js +9884 -0
- llms/extensions/core_tools/ui/codemirror/doc/docs.css +225 -0
- llms/extensions/core_tools/ui/codemirror/doc/source_sans.woff +0 -0
- llms/extensions/core_tools/ui/codemirror/mode/clike/clike.js +942 -0
- llms/extensions/core_tools/ui/codemirror/mode/javascript/index.html +118 -0
- llms/extensions/core_tools/ui/codemirror/mode/javascript/javascript.js +962 -0
- llms/extensions/core_tools/ui/codemirror/mode/javascript/typescript.html +62 -0
- llms/extensions/core_tools/ui/codemirror/mode/python/python.js +402 -0
- llms/extensions/core_tools/ui/codemirror/theme/dracula.css +40 -0
- llms/extensions/core_tools/ui/codemirror/theme/mocha.css +135 -0
- llms/extensions/core_tools/ui/index.mjs +650 -0
- llms/extensions/gallery/README.md +61 -0
- llms/extensions/gallery/__init__.py +63 -0
- llms/extensions/gallery/db.py +243 -0
- llms/extensions/gallery/ui/index.mjs +482 -0
- llms/extensions/katex/README.md +39 -0
- llms/extensions/katex/__init__.py +6 -0
- llms/extensions/katex/ui/README.md +125 -0
- llms/extensions/katex/ui/contrib/auto-render.js +338 -0
- llms/extensions/katex/ui/contrib/auto-render.min.js +1 -0
- llms/extensions/katex/ui/contrib/auto-render.mjs +244 -0
- llms/extensions/katex/ui/contrib/copy-tex.js +127 -0
- llms/extensions/katex/ui/contrib/copy-tex.min.js +1 -0
- llms/extensions/katex/ui/contrib/copy-tex.mjs +105 -0
- llms/extensions/katex/ui/contrib/mathtex-script-type.js +109 -0
- llms/extensions/katex/ui/contrib/mathtex-script-type.min.js +1 -0
- llms/extensions/katex/ui/contrib/mathtex-script-type.mjs +24 -0
- llms/extensions/katex/ui/contrib/mhchem.js +3213 -0
- llms/extensions/katex/ui/contrib/mhchem.min.js +1 -0
- llms/extensions/katex/ui/contrib/mhchem.mjs +3109 -0
- llms/extensions/katex/ui/contrib/render-a11y-string.js +887 -0
- llms/extensions/katex/ui/contrib/render-a11y-string.min.js +1 -0
- llms/extensions/katex/ui/contrib/render-a11y-string.mjs +800 -0
- llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
- llms/extensions/katex/ui/index.mjs +92 -0
- llms/extensions/katex/ui/katex-swap.css +1230 -0
- llms/extensions/katex/ui/katex-swap.min.css +1 -0
- llms/extensions/katex/ui/katex.css +1230 -0
- llms/extensions/katex/ui/katex.js +19080 -0
- llms/extensions/katex/ui/katex.min.css +1 -0
- llms/extensions/katex/ui/katex.min.js +1 -0
- llms/extensions/katex/ui/katex.min.mjs +1 -0
- llms/extensions/katex/ui/katex.mjs +18547 -0
- llms/extensions/providers/__init__.py +22 -0
- llms/extensions/providers/anthropic.py +233 -0
- llms/extensions/providers/cerebras.py +37 -0
- llms/extensions/providers/chutes.py +153 -0
- llms/extensions/providers/google.py +481 -0
- llms/extensions/providers/nvidia.py +103 -0
- llms/extensions/providers/openai.py +154 -0
- llms/extensions/providers/openrouter.py +74 -0
- llms/extensions/providers/zai.py +182 -0
- llms/extensions/system_prompts/README.md +22 -0
- llms/extensions/system_prompts/__init__.py +45 -0
- llms/extensions/system_prompts/ui/index.mjs +280 -0
- llms/extensions/system_prompts/ui/prompts.json +1067 -0
- llms/extensions/tools/__init__.py +144 -0
- llms/extensions/tools/ui/index.mjs +706 -0
- llms/index.html +58 -0
- llms/llms.json +400 -0
- llms/main.py +4407 -0
- llms/providers-extra.json +394 -0
- llms/providers.json +1 -0
- llms/ui/App.mjs +188 -0
- llms/ui/ai.mjs +217 -0
- llms/ui/app.css +7081 -0
- llms/ui/ctx.mjs +412 -0
- llms/ui/index.mjs +131 -0
- llms/ui/lib/chart.js +14 -0
- llms/ui/lib/charts.mjs +16 -0
- llms/ui/lib/color.js +14 -0
- llms/ui/lib/servicestack-vue.mjs +37 -0
- llms/ui/lib/vue.min.mjs +13 -0
- llms/ui/lib/vue.mjs +18530 -0
- {llms_py-2.0.9.data/data → llms}/ui/markdown.mjs +33 -15
- llms/ui/modules/chat/ChatBody.mjs +976 -0
- llms/ui/modules/chat/SettingsDialog.mjs +374 -0
- llms/ui/modules/chat/index.mjs +991 -0
- llms/ui/modules/icons.mjs +46 -0
- llms/ui/modules/layout.mjs +271 -0
- llms/ui/modules/model-selector.mjs +811 -0
- llms/ui/tailwind.input.css +742 -0
- {llms_py-2.0.9.data/data → llms}/ui/typography.css +133 -7
- llms/ui/utils.mjs +261 -0
- llms_py-3.0.10.dist-info/METADATA +49 -0
- llms_py-3.0.10.dist-info/RECORD +177 -0
- llms_py-3.0.10.dist-info/entry_points.txt +2 -0
- {llms_py-2.0.9.dist-info → llms_py-3.0.10.dist-info}/licenses/LICENSE +1 -2
- llms.py +0 -1402
- llms_py-2.0.9.data/data/index.html +0 -64
- llms_py-2.0.9.data/data/llms.json +0 -447
- llms_py-2.0.9.data/data/requirements.txt +0 -1
- llms_py-2.0.9.data/data/ui/App.mjs +0 -20
- llms_py-2.0.9.data/data/ui/ChatPrompt.mjs +0 -389
- llms_py-2.0.9.data/data/ui/Main.mjs +0 -680
- llms_py-2.0.9.data/data/ui/app.css +0 -3951
- llms_py-2.0.9.data/data/ui/lib/servicestack-vue.min.mjs +0 -37
- llms_py-2.0.9.data/data/ui/lib/vue.min.mjs +0 -12
- llms_py-2.0.9.data/data/ui/tailwind.input.css +0 -261
- llms_py-2.0.9.data/data/ui/threadStore.mjs +0 -273
- llms_py-2.0.9.data/data/ui/utils.mjs +0 -114
- llms_py-2.0.9.data/data/ui.json +0 -1069
- llms_py-2.0.9.dist-info/METADATA +0 -941
- llms_py-2.0.9.dist-info/RECORD +0 -30
- llms_py-2.0.9.dist-info/entry_points.txt +0 -2
- {llms_py-2.0.9.data/data → llms}/ui/fav.svg +0 -0
- {llms_py-2.0.9.data/data → llms}/ui/lib/highlight.min.mjs +0 -0
- {llms_py-2.0.9.data/data → llms}/ui/lib/idb.min.mjs +0 -0
- {llms_py-2.0.9.data/data → llms}/ui/lib/marked.min.mjs +0 -0
- /llms_py-2.0.9.data/data/ui/lib/servicestack-client.min.mjs → /llms/ui/lib/servicestack-client.mjs +0 -0
- {llms_py-2.0.9.data/data → llms}/ui/lib/vue-router.min.mjs +0 -0
- {llms_py-2.0.9.dist-info → llms_py-3.0.10.dist-info}/WHEEL +0 -0
- {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>·</span>
|
|
221
|
+
<span>{{ allDailyData[selectedDay]?.requests || 0 }} Requests</span>
|
|
222
|
+
<span>·</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
|
+
}
|