llms-py 2.0.18__py3-none-any.whl → 2.0.33__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/index.html +17 -1
- llms/llms.json +1132 -1075
- llms/main.py +561 -103
- llms/ui/Analytics.mjs +115 -104
- llms/ui/App.mjs +81 -4
- llms/ui/Avatar.mjs +61 -4
- llms/ui/Brand.mjs +29 -11
- llms/ui/ChatPrompt.mjs +163 -16
- llms/ui/Main.mjs +177 -94
- llms/ui/ModelSelector.mjs +28 -10
- llms/ui/OAuthSignIn.mjs +92 -0
- llms/ui/ProviderStatus.mjs +12 -12
- llms/ui/Recents.mjs +13 -13
- llms/ui/SettingsDialog.mjs +65 -65
- llms/ui/Sidebar.mjs +24 -19
- llms/ui/SystemPromptEditor.mjs +5 -5
- llms/ui/SystemPromptSelector.mjs +26 -6
- llms/ui/Welcome.mjs +2 -2
- llms/ui/ai.mjs +69 -5
- llms/ui/app.css +548 -34
- llms/ui/lib/servicestack-vue.mjs +9 -9
- llms/ui/markdown.mjs +8 -8
- llms/ui/tailwind.input.css +2 -0
- llms/ui/threadStore.mjs +39 -0
- llms/ui/typography.css +54 -36
- {llms_py-2.0.18.dist-info → llms_py-2.0.33.dist-info}/METADATA +403 -47
- llms_py-2.0.33.dist-info/RECORD +48 -0
- {llms_py-2.0.18.dist-info → llms_py-2.0.33.dist-info}/licenses/LICENSE +1 -2
- llms/__pycache__/__init__.cpython-312.pyc +0 -0
- llms/__pycache__/__init__.cpython-313.pyc +0 -0
- llms/__pycache__/__init__.cpython-314.pyc +0 -0
- llms/__pycache__/__main__.cpython-312.pyc +0 -0
- llms/__pycache__/__main__.cpython-314.pyc +0 -0
- llms/__pycache__/llms.cpython-312.pyc +0 -0
- llms/__pycache__/main.cpython-312.pyc +0 -0
- llms/__pycache__/main.cpython-313.pyc +0 -0
- llms/__pycache__/main.cpython-314.pyc +0 -0
- llms_py-2.0.18.dist-info/RECORD +0 -56
- {llms_py-2.0.18.dist-info → llms_py-2.0.33.dist-info}/WHEEL +0 -0
- {llms_py-2.0.18.dist-info → llms_py-2.0.33.dist-info}/entry_points.txt +0 -0
- {llms_py-2.0.18.dist-info → llms_py-2.0.33.dist-info}/top_level.txt +0 -0
llms/ui/Analytics.mjs
CHANGED
|
@@ -24,16 +24,17 @@ export const colors = [
|
|
|
24
24
|
|
|
25
25
|
const MonthSelector = {
|
|
26
26
|
template:`
|
|
27
|
-
<div class="flex gap-4 items-center">
|
|
27
|
+
<div class="flex flex-col sm:flex-row gap-2 sm:gap-4 items-stretch sm:items-center w-full sm:w-auto">
|
|
28
28
|
<!-- Months Row -->
|
|
29
|
-
<div class="flex gap-2 flex-wrap justify-center">
|
|
29
|
+
<div class="flex gap-1 sm:gap-2 flex-wrap justify-center overflow-x-auto">
|
|
30
30
|
<template v-for="month in availableMonthsForYear" :key="month">
|
|
31
31
|
<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' }) }}
|
|
32
|
+
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">
|
|
33
|
+
<span class="hidden sm:inline">{{ new Date(selectedYear + '-' + month.toString().padStart(2,'0') + '-01').toLocaleString('default', { month: 'long' }) }}</span>
|
|
34
|
+
<span class="sm:hidden">{{ new Date(selectedYear + '-' + month.toString().padStart(2,'0') + '-01').toLocaleString('default', { month: 'short' }) }}</span>
|
|
34
35
|
</span>
|
|
35
36
|
<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"
|
|
37
|
+
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
38
|
@click="updateSelection(selectedYear, month)">
|
|
38
39
|
{{ new Date(selectedYear + '-' + month.toString().padStart(2,'0') + '-01').toLocaleString('default', { month: 'short' }) }}
|
|
39
40
|
</button>
|
|
@@ -42,7 +43,7 @@ const MonthSelector = {
|
|
|
42
43
|
|
|
43
44
|
<!-- Year Dropdown -->
|
|
44
45
|
<select :value="selectedYear" @change="(e) => updateSelection(parseInt(e.target.value), selectedMonth)"
|
|
45
|
-
class="border border-gray-300 rounded-md text-sm font-medium
|
|
46
|
+
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
47
|
<option v-for="year in availableYears" :key="year" :value="year">
|
|
47
48
|
{{ year }}
|
|
48
49
|
</option>
|
|
@@ -115,9 +116,11 @@ export default {
|
|
|
115
116
|
template: `
|
|
116
117
|
<div class="flex flex-col h-full w-full">
|
|
117
118
|
<!-- Header -->
|
|
118
|
-
<div class="border-b border-gray-200 bg-white px-4 py-3
|
|
119
|
-
<div
|
|
120
|
-
|
|
119
|
+
<div class="border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 px-2 sm:px-4 py-3">
|
|
120
|
+
<div
|
|
121
|
+
:class="!$ai.isSidebarOpen ? 'pl-3' : ''"
|
|
122
|
+
class="max-w-6xl mx-auto flex flex-col sm:flex-row items-stretch sm:items-center justify-between gap-3">
|
|
123
|
+
<h2 class="text-lg font-semibold text-gray-900 dark:text-gray-100 flex-shrink-0">
|
|
121
124
|
<RouterLink to="/analytics">Analytics</RouterLink>
|
|
122
125
|
</h2>
|
|
123
126
|
<MonthSelector :dailyData="allDailyData" />
|
|
@@ -125,67 +128,67 @@ export default {
|
|
|
125
128
|
</div>
|
|
126
129
|
|
|
127
130
|
<!-- Tabs -->
|
|
128
|
-
<div class="border-b border-gray-200 bg-white px-4">
|
|
131
|
+
<div class="border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 px-4">
|
|
129
132
|
<div class="max-w-6xl mx-auto flex gap-8">
|
|
130
133
|
<button type="button"
|
|
131
134
|
@click="activeTab = 'cost'"
|
|
132
135
|
:class="['py-3 px-1 border-b-2 font-medium text-sm transition-colors',
|
|
133
136
|
activeTab === 'cost'
|
|
134
|
-
? 'border-blue-500 text-blue-600'
|
|
135
|
-
: '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']">
|
|
136
139
|
Cost Analysis
|
|
137
140
|
</button>
|
|
138
141
|
<button type="button"
|
|
139
142
|
@click="activeTab = 'tokens'"
|
|
140
143
|
:class="['py-3 px-1 border-b-2 font-medium text-sm transition-colors',
|
|
141
144
|
activeTab === 'tokens'
|
|
142
|
-
? 'border-blue-500 text-blue-600'
|
|
143
|
-
: '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']">
|
|
144
147
|
Token Usage
|
|
145
148
|
</button>
|
|
146
149
|
<button type="button"
|
|
147
150
|
@click="activeTab = 'activity'"
|
|
148
151
|
:class="['py-3 px-1 border-b-2 font-medium text-sm transition-colors',
|
|
149
152
|
activeTab === 'activity'
|
|
150
|
-
? 'border-blue-500 text-blue-600'
|
|
151
|
-
: 'border-transparent text-gray-600 hover:text-gray-900']">
|
|
153
|
+
? 'border-blue-500 text-blue-600 dark:text-blue-400'
|
|
154
|
+
: 'border-transparent text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200']">
|
|
152
155
|
Activity
|
|
153
156
|
</button>
|
|
154
157
|
</div>
|
|
155
158
|
</div>
|
|
156
159
|
|
|
157
160
|
<!-- Content -->
|
|
158
|
-
<div class="flex-1 overflow-auto bg-gray-50" :class="activeTab === 'activity' ? 'p-0' : 'p-4'">
|
|
161
|
+
<div class="flex-1 overflow-auto bg-gray-50 dark:bg-gray-900" :class="activeTab === 'activity' ? 'p-0' : 'p-4'">
|
|
159
162
|
|
|
160
163
|
<div :class="activeTab === 'activity' ? 'h-full' : 'max-w-6xl mx-auto'">
|
|
161
164
|
<!-- Stats Summary (hidden for Activity tab) -->
|
|
162
165
|
<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">{{ formatCost(totalCost) }}</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 Cost</div>
|
|
168
|
+
<div class="text-2xl font-bold text-gray-900 dark:text-gray-100 mt-1">{{ formatCost(totalCost) }}</div>
|
|
166
169
|
</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>
|
|
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 Requests</div>
|
|
172
|
+
<div class="text-2xl font-bold text-gray-900 dark:text-gray-100 mt-1">{{ totalRequests }}</div>
|
|
170
173
|
</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>
|
|
174
|
+
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
|
|
175
|
+
<div class="text-sm font-medium text-gray-600 dark:text-gray-400">Total Input Tokens</div>
|
|
176
|
+
<div class="text-2xl font-bold text-gray-900 dark:text-gray-100 mt-1">{{ humanifyNumber(totalInputTokens) }}</div>
|
|
174
177
|
</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>
|
|
178
|
+
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
|
|
179
|
+
<div class="text-sm font-medium text-gray-600 dark:text-gray-400">Total Output Tokens</div>
|
|
180
|
+
<div class="text-2xl font-bold text-gray-900 dark:text-gray-100 mt-1">{{ humanifyNumber(totalOutputTokens) }}</div>
|
|
178
181
|
</div>
|
|
179
182
|
</div>
|
|
180
183
|
|
|
181
184
|
<!-- 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">
|
|
185
|
+
<div v-if="activeTab === 'cost'" class="bg-white dark:bg-gray-800 rounded-lg shadow p-4 sm:p-6">
|
|
186
|
+
<div class="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 mb-6">
|
|
187
|
+
<h3 class="text-base sm:text-lg font-semibold text-gray-900 dark:text-gray-100">Daily Costs</h3>
|
|
188
|
+
<h3 class="text-sm sm:text-lg font-semibold text-gray-900 dark:text-gray-100">
|
|
186
189
|
{{ new Date(selectedDay).toLocaleDateString(undefined, { year: 'numeric', month: 'long' }) }}
|
|
187
190
|
</h3>
|
|
188
|
-
<select v-model="costChartType" class="px-3 pr-6 py-2 border border-gray-300 rounded-md text-sm font-medium
|
|
191
|
+
<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
192
|
<option value="bar">Bar Chart</option>
|
|
190
193
|
<option value="line">Line Chart</option>
|
|
191
194
|
</select>
|
|
@@ -194,16 +197,16 @@ export default {
|
|
|
194
197
|
<div v-if="chartData.labels.length > 0" class="relative h-96">
|
|
195
198
|
<canvas ref="costChartCanvas"></canvas>
|
|
196
199
|
</div>
|
|
197
|
-
<div v-else class="flex items-center justify-center h-96 text-gray-500">
|
|
200
|
+
<div v-else class="flex items-center justify-center h-96 text-gray-500 dark:text-gray-400">
|
|
198
201
|
<p>No request data available</p>
|
|
199
202
|
</div>
|
|
200
203
|
</div>
|
|
201
204
|
|
|
202
205
|
<!-- 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">
|
|
206
|
+
<div v-if="activeTab === 'tokens'" class="bg-white dark:bg-gray-800 rounded-lg shadow p-4 sm:p-6">
|
|
207
|
+
<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
208
|
<span>Daily Token Usage</span>
|
|
206
|
-
<span>
|
|
209
|
+
<span class="text-sm sm:text-base">
|
|
207
210
|
{{ new Date(selectedDay).toLocaleDateString(undefined, { year: 'numeric', month: 'long' }) }}
|
|
208
211
|
</span>
|
|
209
212
|
</h3>
|
|
@@ -211,48 +214,48 @@ export default {
|
|
|
211
214
|
<div v-if="tokenChartData.labels.length > 0" class="relative h-96">
|
|
212
215
|
<canvas ref="tokenChartCanvas"></canvas>
|
|
213
216
|
</div>
|
|
214
|
-
<div v-else class="flex items-center justify-center h-96 text-gray-500">
|
|
217
|
+
<div v-else class="flex items-center justify-center h-96 text-gray-500 dark:text-gray-400">
|
|
215
218
|
<p>No request data available</p>
|
|
216
219
|
</div>
|
|
217
220
|
</div>
|
|
218
221
|
|
|
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">
|
|
222
|
+
<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
223
|
<div>
|
|
221
|
-
{{ new Date(selectedDay).toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' }) }}
|
|
224
|
+
{{ new Date(selectedDay).toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' }) }}
|
|
222
225
|
</div>
|
|
223
|
-
<div>
|
|
224
|
-
{{ formatCost(allDailyData[selectedDay]?.cost || 0) }}
|
|
225
|
-
|
|
226
|
-
{{ allDailyData[selectedDay]?.requests || 0 }} Requests
|
|
227
|
-
|
|
228
|
-
{{ humanifyNumber(allDailyData[selectedDay]?.inputTokens || 0) }} -> {{ humanifyNumber(allDailyData[selectedDay]?.outputTokens || 0) }} Tokens
|
|
226
|
+
<div class="flex flex-wrap gap-x-2 gap-y-1">
|
|
227
|
+
<span>{{ formatCost(allDailyData[selectedDay]?.cost || 0) }}</span>
|
|
228
|
+
<span>·</span>
|
|
229
|
+
<span>{{ allDailyData[selectedDay]?.requests || 0 }} Requests</span>
|
|
230
|
+
<span>·</span>
|
|
231
|
+
<span>{{ humanifyNumber(allDailyData[selectedDay]?.inputTokens || 0) }} -> {{ humanifyNumber(allDailyData[selectedDay]?.outputTokens || 0) }} Tokens</span>
|
|
229
232
|
</div>
|
|
230
233
|
</div>
|
|
231
234
|
|
|
232
235
|
<!-- Pie Charts for Selected Day -->
|
|
233
236
|
<div v-if="allDailyData[selectedDay]?.requests && activeTab === 'cost' && selectedDay" class="mt-6 grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
234
237
|
<!-- 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">
|
|
238
|
+
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
|
239
|
+
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
|
|
237
240
|
Cost by Model
|
|
238
241
|
</h3>
|
|
239
242
|
<div v-if="modelPieData.labels.length > 0" class="relative h-80">
|
|
240
243
|
<canvas ref="modelPieCanvas"></canvas>
|
|
241
244
|
</div>
|
|
242
|
-
<div v-else class="flex items-center justify-center h-80 text-gray-500">
|
|
245
|
+
<div v-else class="flex items-center justify-center h-80 text-gray-500 dark:text-gray-400">
|
|
243
246
|
<p>No data for selected day</p>
|
|
244
247
|
</div>
|
|
245
248
|
</div>
|
|
246
249
|
|
|
247
250
|
<!-- 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">
|
|
251
|
+
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
|
252
|
+
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
|
|
250
253
|
Cost by Provider
|
|
251
254
|
</h3>
|
|
252
255
|
<div v-if="providerPieData.labels.length > 0" class="relative h-80">
|
|
253
256
|
<canvas ref="providerPieCanvas"></canvas>
|
|
254
257
|
</div>
|
|
255
|
-
<div v-else class="flex items-center justify-center h-80 text-gray-500">
|
|
258
|
+
<div v-else class="flex items-center justify-center h-80 text-gray-500 dark:text-gray-400">
|
|
256
259
|
<p>No data for selected day</p>
|
|
257
260
|
</div>
|
|
258
261
|
</div>
|
|
@@ -261,39 +264,39 @@ export default {
|
|
|
261
264
|
<!-- Token Pie Charts for Selected Day -->
|
|
262
265
|
<div v-if="allDailyData[selectedDay]?.requests && activeTab === 'tokens' && selectedDay" class="mt-6 grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
263
266
|
<!-- 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">
|
|
267
|
+
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
|
268
|
+
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
|
|
266
269
|
Tokens by Model
|
|
267
270
|
</h3>
|
|
268
271
|
<div v-if="tokenModelPieData.labels.length > 0" class="relative h-80">
|
|
269
272
|
<canvas ref="tokenModelPieCanvas"></canvas>
|
|
270
273
|
</div>
|
|
271
|
-
<div v-else class="flex items-center justify-center h-80 text-gray-500">
|
|
274
|
+
<div v-else class="flex items-center justify-center h-80 text-gray-500 dark:text-gray-400">
|
|
272
275
|
<p>No data for selected day</p>
|
|
273
276
|
</div>
|
|
274
277
|
</div>
|
|
275
278
|
|
|
276
279
|
<!-- 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">
|
|
280
|
+
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
|
281
|
+
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
|
|
279
282
|
Tokens by Provider
|
|
280
283
|
</h3>
|
|
281
284
|
<div v-if="tokenProviderPieData.labels.length > 0" class="relative h-80">
|
|
282
285
|
<canvas ref="tokenProviderPieCanvas"></canvas>
|
|
283
286
|
</div>
|
|
284
|
-
<div v-else class="flex items-center justify-center h-80 text-gray-500">
|
|
287
|
+
<div v-else class="flex items-center justify-center h-80 text-gray-500 dark:text-gray-400">
|
|
285
288
|
<p>No data for selected day</p>
|
|
286
289
|
</div>
|
|
287
290
|
</div>
|
|
288
291
|
</div>
|
|
289
292
|
|
|
290
293
|
<!-- Activity Tab - Full Page Layout -->
|
|
291
|
-
<div v-if="activeTab === 'activity'" class="h-full flex flex-col bg-white">
|
|
294
|
+
<div v-if="activeTab === 'activity'" class="h-full flex flex-col bg-white dark:bg-gray-800">
|
|
292
295
|
<!-- 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">
|
|
296
|
+
<div class="flex-shrink-0 border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 px-3 sm:px-6 py-4">
|
|
297
|
+
<div class="flex flex-wrap gap-2 sm:gap-4 items-end">
|
|
298
|
+
<div class="flex flex-col flex-1 min-w-[120px] sm:flex-initial">
|
|
299
|
+
<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
300
|
<option value="">All Models</option>
|
|
298
301
|
<option v-for="model in filterOptions.models" :key="model" :value="model">
|
|
299
302
|
{{ model }}
|
|
@@ -301,8 +304,8 @@ export default {
|
|
|
301
304
|
</select>
|
|
302
305
|
</div>
|
|
303
306
|
|
|
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">
|
|
307
|
+
<div class="flex flex-col flex-1 min-w-[120px] sm:flex-initial">
|
|
308
|
+
<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
309
|
<option value="">All Providers</option>
|
|
307
310
|
<option v-for="provider in filterOptions.providers" :key="provider" :value="provider">
|
|
308
311
|
{{ provider }}
|
|
@@ -310,8 +313,8 @@ export default {
|
|
|
310
313
|
</select>
|
|
311
314
|
</div>
|
|
312
315
|
|
|
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">
|
|
316
|
+
<div class="flex flex-col flex-1 min-w-[140px] sm:flex-initial">
|
|
317
|
+
<select v-model="sortBy" class="px-3 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 w-full">
|
|
315
318
|
<option value="created">Date (Newest)</option>
|
|
316
319
|
<option value="cost">Cost (Highest)</option>
|
|
317
320
|
<option value="duration">Duration (Longest)</option>
|
|
@@ -319,7 +322,7 @@ export default {
|
|
|
319
322
|
</select>
|
|
320
323
|
</div>
|
|
321
324
|
|
|
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">
|
|
325
|
+
<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
326
|
Clear Filters
|
|
324
327
|
</button>
|
|
325
328
|
</div>
|
|
@@ -330,61 +333,61 @@ export default {
|
|
|
330
333
|
<div v-if="isActivityLoading && activityRequests.length === 0" class="flex items-center justify-center h-full">
|
|
331
334
|
<div class="text-center">
|
|
332
335
|
<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>
|
|
336
|
+
<p class="mt-4 text-gray-600 dark:text-gray-400">Loading requests...</p>
|
|
334
337
|
</div>
|
|
335
338
|
</div>
|
|
336
339
|
|
|
337
340
|
<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>
|
|
341
|
+
<p class="text-gray-500 dark:text-gray-400">No requests found</p>
|
|
339
342
|
</div>
|
|
340
343
|
|
|
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>
|
|
344
|
+
<div v-else class="divide-y divide-gray-200 dark:divide-gray-700">
|
|
345
|
+
<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">
|
|
346
|
+
<div class="flex flex-col lg:flex-row items-start justify-between gap-4">
|
|
347
|
+
<div class="flex-1 min-w-0 w-full">
|
|
348
|
+
<div class="flex flex-col sm:flex-row justify-between gap-2 mb-2">
|
|
349
|
+
<div class="flex items-center gap-2 flex-wrap">
|
|
350
|
+
<span class="text-xs px-2 py-1 bg-blue-100 dark:bg-blue-900/30 text-blue-800 dark:text-blue-300 rounded font-medium">{{ request.model }}</span>
|
|
351
|
+
<span class="text-xs px-2 py-1 bg-purple-100 dark:bg-purple-900/30 text-purple-800 dark:text-purple-300 rounded font-medium">{{ request.provider }}</span>
|
|
352
|
+
<span v-if="request.providerRef" class="text-xs px-2 py-1 bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-300 rounded font-medium">{{ request.providerRef }}</span>
|
|
353
|
+
<span v-if="request.finishReason" class="text-xs px-2 py-1 bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-300 rounded font-medium">{{ request.finishReason }}</span>
|
|
351
354
|
</div>
|
|
352
|
-
<div class="text-xs text-gray-500">
|
|
355
|
+
<div class="text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
|
353
356
|
{{ formatActivityDate(request.created) }}
|
|
354
357
|
</div>
|
|
355
358
|
</div>
|
|
356
|
-
<div class="text-sm font-semibold text-gray-900 truncate">
|
|
359
|
+
<div class="text-sm font-semibold text-gray-900 dark:text-gray-100 truncate mb-3">
|
|
357
360
|
{{ request.title }}
|
|
358
361
|
</div>
|
|
359
362
|
|
|
360
|
-
<div class="grid grid-cols-2
|
|
363
|
+
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3 sm:gap-4">
|
|
361
364
|
<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">{{ formatCost(request.cost) }}</div>
|
|
365
|
+
<div class="text-xs text-gray-500 dark:text-gray-400 font-medium">Cost</div>
|
|
366
|
+
<div class="text-sm font-semibold text-gray-900 dark:text-gray-100">{{ formatCost(request.cost) }}</div>
|
|
364
367
|
</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
|
+
<div class="col-span-2 sm:col-span-1">
|
|
369
|
+
<div class="text-xs text-gray-500 dark:text-gray-400 font-medium">Tokens</div>
|
|
370
|
+
<div class="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
|
368
371
|
{{ 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>
|
|
372
|
+
<span v-if="request.inputCachedTokens" class="ml-1 text-xs text-gray-500 dark:text-gray-400">({{ humanifyNumber(request.inputCachedTokens) }} cached)</span>
|
|
370
373
|
</div>
|
|
371
374
|
</div>
|
|
372
375
|
<div>
|
|
373
|
-
<div class="text-xs text-gray-500 font-medium">Duration</div>
|
|
374
|
-
<div class="text-sm font-semibold text-gray-900">{{ request.duration ? humanifyMs(request.duration) : '—' }}</div>
|
|
376
|
+
<div class="text-xs text-gray-500 dark:text-gray-400 font-medium">Duration</div>
|
|
377
|
+
<div class="text-sm font-semibold text-gray-900 dark:text-gray-100">{{ request.duration ? humanifyMs(request.duration) : '—' }}</div>
|
|
375
378
|
</div>
|
|
376
379
|
<div>
|
|
377
|
-
<div class="text-xs text-gray-500 font-medium">Speed</div>
|
|
378
|
-
<div class="text-sm font-semibold text-gray-900">{{ request.duration && request.outputTokens ? (request.outputTokens / (request.duration / 1000)).toFixed(1) + ' tok/s' : '—' }}</div>
|
|
380
|
+
<div class="text-xs text-gray-500 dark:text-gray-400 font-medium">Speed</div>
|
|
381
|
+
<div class="text-sm font-semibold text-gray-900 dark:text-gray-100">{{ request.duration && request.outputTokens ? (request.outputTokens / (request.duration / 1000)).toFixed(1) + ' tok/s' : '—' }}</div>
|
|
379
382
|
</div>
|
|
380
383
|
</div>
|
|
381
384
|
</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
|
|
385
|
+
<div class="flex flex-row lg:flex-col gap-2 w-full lg:w-auto">
|
|
386
|
+
<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">
|
|
387
|
+
View<span class="hidden sm:inline"> Thread</span>
|
|
385
388
|
</button>
|
|
386
|
-
<button type="button" @click="deleteRequestLog(request.id)" class="flex-
|
|
387
|
-
Delete<span class="hidden
|
|
389
|
+
<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">
|
|
390
|
+
Delete<span class="hidden sm:inline"> Request</span>
|
|
388
391
|
</button>
|
|
389
392
|
</div>
|
|
390
393
|
</div>
|
|
@@ -394,7 +397,7 @@ export default {
|
|
|
394
397
|
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto"></div>
|
|
395
398
|
</div>
|
|
396
399
|
|
|
397
|
-
<div v-if="!activityHasMore && activityRequests.length > 0" class="px-6 py-8 text-center text-gray-500 text-sm">
|
|
400
|
+
<div v-if="!activityHasMore && activityRequests.length > 0" class="px-6 py-8 text-center text-gray-500 dark:text-gray-400 text-sm">
|
|
398
401
|
No more requests to load
|
|
399
402
|
</div>
|
|
400
403
|
</div>
|
|
@@ -1021,7 +1024,9 @@ export default {
|
|
|
1021
1024
|
|
|
1022
1025
|
// Only display label if percentage > 1%
|
|
1023
1026
|
if (parseFloat(percentage) > 1) {
|
|
1024
|
-
|
|
1027
|
+
// Use white color in dark mode, black in light mode
|
|
1028
|
+
const isDarkMode = document.documentElement.classList.contains('dark')
|
|
1029
|
+
chartCtx.fillStyle = isDarkMode ? '#fff' : '#000'
|
|
1025
1030
|
chartCtx.font = 'bold 12px Arial'
|
|
1026
1031
|
chartCtx.textAlign = 'center'
|
|
1027
1032
|
chartCtx.textBaseline = 'middle'
|
|
@@ -1078,7 +1083,9 @@ export default {
|
|
|
1078
1083
|
|
|
1079
1084
|
// Only display label if percentage > 1%
|
|
1080
1085
|
if (parseFloat(percentage) > 1) {
|
|
1081
|
-
|
|
1086
|
+
// Use white color in dark mode, black in light mode
|
|
1087
|
+
const isDarkMode = document.documentElement.classList.contains('dark')
|
|
1088
|
+
chartCtx.fillStyle = isDarkMode ? '#fff' : '#000'
|
|
1082
1089
|
chartCtx.font = 'bold 12px Arial'
|
|
1083
1090
|
chartCtx.textAlign = 'center'
|
|
1084
1091
|
chartCtx.textBaseline = 'middle'
|
|
@@ -1135,7 +1142,9 @@ export default {
|
|
|
1135
1142
|
|
|
1136
1143
|
// Only display label if percentage > 1%
|
|
1137
1144
|
if (parseFloat(percentage) > 1) {
|
|
1138
|
-
|
|
1145
|
+
// Use white color in dark mode, black in light mode
|
|
1146
|
+
const isDarkMode = document.documentElement.classList.contains('dark')
|
|
1147
|
+
chartCtx.fillStyle = isDarkMode ? '#fff' : '#000'
|
|
1139
1148
|
chartCtx.font = 'bold 12px Arial'
|
|
1140
1149
|
chartCtx.textAlign = 'center'
|
|
1141
1150
|
chartCtx.textBaseline = 'middle'
|
|
@@ -1192,7 +1201,9 @@ export default {
|
|
|
1192
1201
|
|
|
1193
1202
|
// Only display label if percentage > 1%
|
|
1194
1203
|
if (parseFloat(percentage) > 1) {
|
|
1195
|
-
|
|
1204
|
+
// Use white color in dark mode, black in light mode
|
|
1205
|
+
const isDarkMode = document.documentElement.classList.contains('dark')
|
|
1206
|
+
chartCtx.fillStyle = isDarkMode ? '#fff' : '#000'
|
|
1196
1207
|
chartCtx.font = 'bold 12px Arial'
|
|
1197
1208
|
chartCtx.textAlign = 'center'
|
|
1198
1209
|
chartCtx.textBaseline = 'middle'
|
llms/ui/App.mjs
CHANGED
|
@@ -1,18 +1,95 @@
|
|
|
1
|
+
import { inject, ref, onMounted, onUnmounted } from "vue"
|
|
1
2
|
import Sidebar from "./Sidebar.mjs"
|
|
2
3
|
|
|
3
4
|
export default {
|
|
4
5
|
components: {
|
|
5
6
|
Sidebar,
|
|
6
7
|
},
|
|
8
|
+
setup() {
|
|
9
|
+
const ai = inject('ai')
|
|
10
|
+
const isMobile = ref(false)
|
|
11
|
+
|
|
12
|
+
const checkMobile = () => {
|
|
13
|
+
const wasMobile = isMobile.value
|
|
14
|
+
isMobile.value = window.innerWidth < 1024 // lg breakpoint
|
|
15
|
+
|
|
16
|
+
// Only auto-adjust sidebar state when transitioning between mobile/desktop
|
|
17
|
+
if (wasMobile !== isMobile.value) {
|
|
18
|
+
if (isMobile.value) {
|
|
19
|
+
ai.isSidebarOpen = false
|
|
20
|
+
} else {
|
|
21
|
+
ai.isSidebarOpen = true
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const toggleSidebar = () => {
|
|
27
|
+
ai.isSidebarOpen = !ai.isSidebarOpen
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const closeSidebar = () => {
|
|
31
|
+
if (isMobile.value) {
|
|
32
|
+
ai.isSidebarOpen = false
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
onMounted(() => {
|
|
37
|
+
checkMobile()
|
|
38
|
+
window.addEventListener('resize', checkMobile)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
onUnmounted(() => {
|
|
42
|
+
window.removeEventListener('resize', checkMobile)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
return { ai, isMobile, toggleSidebar, closeSidebar }
|
|
46
|
+
},
|
|
7
47
|
template: `
|
|
8
|
-
<div class="flex h-screen bg-white">
|
|
9
|
-
<!--
|
|
10
|
-
<div
|
|
11
|
-
|
|
48
|
+
<div class="flex h-screen bg-white dark:bg-gray-900">
|
|
49
|
+
<!-- Mobile Overlay -->
|
|
50
|
+
<div
|
|
51
|
+
v-if="isMobile && ai.isSidebarOpen && !(ai.requiresAuth && !ai.auth)"
|
|
52
|
+
@click="closeSidebar"
|
|
53
|
+
class="fixed inset-0 bg-black/50 z-40 lg:hidden"
|
|
54
|
+
></div>
|
|
55
|
+
|
|
56
|
+
<!-- Sidebar (hidden when auth required and not authenticated) -->
|
|
57
|
+
<div
|
|
58
|
+
v-if="!(ai.requiresAuth && !ai.auth) && ai.isSidebarOpen"
|
|
59
|
+
:class="[
|
|
60
|
+
'transition-transform duration-300 ease-in-out z-50',
|
|
61
|
+
'w-72 xl:w-80 flex-shrink-0',
|
|
62
|
+
'lg:relative',
|
|
63
|
+
'fixed inset-y-0 left-0'
|
|
64
|
+
]"
|
|
65
|
+
>
|
|
66
|
+
<Sidebar @thread-selected="closeSidebar" @toggle-sidebar="toggleSidebar" />
|
|
12
67
|
</div>
|
|
13
68
|
|
|
14
69
|
<!-- Main Area -->
|
|
15
70
|
<div class="flex-1 flex flex-col">
|
|
71
|
+
<!-- Collapsed Sidebar Toggle Button -->
|
|
72
|
+
<div
|
|
73
|
+
v-if="!(ai.requiresAuth && !ai.auth) && !ai.isSidebarOpen"
|
|
74
|
+
class="fixed top-4 left-0"
|
|
75
|
+
>
|
|
76
|
+
<button type="button"
|
|
77
|
+
@click="toggleSidebar"
|
|
78
|
+
class="group p-1 text-gray-500 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
|
|
79
|
+
title="Open sidebar"
|
|
80
|
+
>
|
|
81
|
+
<div class="relative w-5 h-5">
|
|
82
|
+
<!-- Default sidebar icon -->
|
|
83
|
+
<svg class="absolute inset-0 group-hover:hidden" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
84
|
+
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
|
85
|
+
<line x1="9" y1="3" x2="9" y2="21"></line>
|
|
86
|
+
</svg>
|
|
87
|
+
<!-- Hover state: |→ icon -->
|
|
88
|
+
<svg class="absolute inset-0 hidden group-hover:block" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="m17.172 11l-4.657-4.657l1.414-1.414L21 12l-7.071 7.071l-1.414-1.414L17.172 13H8v-2zM4 19V5h2v14z"/></svg>
|
|
89
|
+
</div>
|
|
90
|
+
</button>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
16
93
|
<RouterView />
|
|
17
94
|
</div>
|
|
18
95
|
</div>
|