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