llms-py 2.0.29__py3-none-any.whl → 2.0.31__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/llms.json +1091 -1088
- llms/main.py +10 -2
- llms/ui/Analytics.mjs +50 -47
- llms/ui/App.mjs +76 -4
- llms/ui/Brand.mjs +27 -9
- llms/ui/ChatPrompt.mjs +7 -1
- llms/ui/Main.mjs +5 -3
- llms/ui/ModelSelector.mjs +6 -5
- llms/ui/Sidebar.mjs +7 -2
- llms/ui/SystemPromptSelector.mjs +1 -1
- llms/ui/ai.mjs +2 -1
- llms/ui/app.css +156 -10
- {llms_py-2.0.29.dist-info → llms_py-2.0.31.dist-info}/METADATA +21 -1
- {llms_py-2.0.29.dist-info → llms_py-2.0.31.dist-info}/RECORD +18 -18
- {llms_py-2.0.29.dist-info → llms_py-2.0.31.dist-info}/WHEEL +0 -0
- {llms_py-2.0.29.dist-info → llms_py-2.0.31.dist-info}/entry_points.txt +0 -0
- {llms_py-2.0.29.dist-info → llms_py-2.0.31.dist-info}/licenses/LICENSE +0 -0
- {llms_py-2.0.29.dist-info → llms_py-2.0.31.dist-info}/top_level.txt +0 -0
llms/main.py
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env python
|
|
2
2
|
|
|
3
|
+
# Copyright (c) Demis Bellot, ServiceStack <https://servicestack.net>
|
|
4
|
+
# License: https://github.com/ServiceStack/llms/blob/main/LICENSE
|
|
5
|
+
|
|
3
6
|
# A lightweight CLI tool and OpenAI-compatible server for querying multiple Large Language Model (LLM) providers.
|
|
4
7
|
# Docs: https://github.com/ServiceStack/llms
|
|
5
8
|
|
|
@@ -31,7 +34,7 @@ try:
|
|
|
31
34
|
except ImportError:
|
|
32
35
|
HAS_PIL = False
|
|
33
36
|
|
|
34
|
-
VERSION = "2.0.
|
|
37
|
+
VERSION = "2.0.31"
|
|
35
38
|
_ROOT = None
|
|
36
39
|
g_config_path = None
|
|
37
40
|
g_ui_path = None
|
|
@@ -519,6 +522,8 @@ class OpenAiProvider:
|
|
|
519
522
|
chat = await process_chat(chat)
|
|
520
523
|
_log(f"POST {self.chat_url}")
|
|
521
524
|
_log(chat_summary(chat))
|
|
525
|
+
# remove metadata if any (conflicts with some providers, e.g. Z.ai)
|
|
526
|
+
chat.pop('metadata', None)
|
|
522
527
|
|
|
523
528
|
async with aiohttp.ClientSession() as session:
|
|
524
529
|
started_at = time.time()
|
|
@@ -1496,7 +1501,10 @@ def main():
|
|
|
1496
1501
|
parser.add_argument('--verbose', action='store_true', help='Verbose output')
|
|
1497
1502
|
|
|
1498
1503
|
cli_args, extra_args = parser.parse_known_args()
|
|
1499
|
-
|
|
1504
|
+
|
|
1505
|
+
# Check for verbose mode from CLI argument or environment variables
|
|
1506
|
+
verbose_env = os.environ.get('VERBOSE', '').lower()
|
|
1507
|
+
if cli_args.verbose or verbose_env in ('1', 'true'):
|
|
1500
1508
|
g_verbose = True
|
|
1501
1509
|
# printdump(cli_args)
|
|
1502
1510
|
if cli_args.model:
|
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 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">
|
|
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 dark:border-gray-700 bg-white dark:bg-gray-800 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" />
|
|
@@ -179,13 +182,13 @@ export default {
|
|
|
179
182
|
</div>
|
|
180
183
|
|
|
181
184
|
<!-- Cost Analysis Tab -->
|
|
182
|
-
<div v-if="activeTab === 'cost'" class="bg-white dark:bg-gray-800 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 dark:text-gray-100">Daily Costs</h3>
|
|
185
|
-
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">
|
|
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 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">
|
|
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>
|
|
@@ -200,10 +203,10 @@ export default {
|
|
|
200
203
|
</div>
|
|
201
204
|
|
|
202
205
|
<!-- Token Usage Tab -->
|
|
203
|
-
<div v-if="activeTab === 'tokens'" class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
|
204
|
-
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 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>
|
|
@@ -216,16 +219,16 @@ export default {
|
|
|
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 dark:text-gray-300 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
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
|
|
|
@@ -290,10 +293,10 @@ export default {
|
|
|
290
293
|
<!-- Activity Tab - Full Page Layout -->
|
|
291
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 dark:border-gray-700 bg-white dark:bg-gray-800 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 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">
|
|
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 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">
|
|
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 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">
|
|
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 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">
|
|
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>
|
|
@@ -339,30 +342,30 @@ export default {
|
|
|
339
342
|
</div>
|
|
340
343
|
|
|
341
344
|
<div v-else class="divide-y divide-gray-200 dark:divide-gray-700">
|
|
342
|
-
<div v-for="request in activityRequests" :key="request.id" class="px-6 py-4 hover:bg-gray-50 dark:hover:bg-gray-700 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
|
|
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">
|
|
347
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>
|
|
348
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>
|
|
349
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>
|
|
350
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 dark:text-gray-400">
|
|
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 dark:text-gray-100 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
365
|
<div class="text-xs text-gray-500 dark:text-gray-400 font-medium">Cost</div>
|
|
363
366
|
<div class="text-sm font-semibold text-gray-900 dark:text-gray-100">{{ formatCost(request.cost) }}</div>
|
|
364
367
|
</div>
|
|
365
|
-
<div>
|
|
368
|
+
<div class="col-span-2 sm:col-span-1">
|
|
366
369
|
<div class="text-xs text-gray-500 dark:text-gray-400 font-medium">Tokens</div>
|
|
367
370
|
<div class="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
|
368
371
|
{{ humanifyNumber(request.inputTokens) }} -> {{ humanifyNumber(request.outputTokens) }}
|
|
@@ -379,12 +382,12 @@ export default {
|
|
|
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>
|
llms/ui/App.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { inject } from "vue"
|
|
1
|
+
import { inject, ref, onMounted, onUnmounted } from "vue"
|
|
2
2
|
import Sidebar from "./Sidebar.mjs"
|
|
3
3
|
|
|
4
4
|
export default {
|
|
@@ -7,17 +7,89 @@ export default {
|
|
|
7
7
|
},
|
|
8
8
|
setup() {
|
|
9
9
|
const ai = inject('ai')
|
|
10
|
-
|
|
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 }
|
|
11
46
|
},
|
|
12
47
|
template: `
|
|
13
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
|
+
|
|
14
56
|
<!-- Sidebar (hidden when auth required and not authenticated) -->
|
|
15
|
-
<div
|
|
16
|
-
|
|
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" />
|
|
17
67
|
</div>
|
|
18
68
|
|
|
19
69
|
<!-- Main Area -->
|
|
20
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
|
+
|
|
21
93
|
<RouterView />
|
|
22
94
|
</div>
|
|
23
95
|
</div>
|
llms/ui/Brand.mjs
CHANGED
|
@@ -1,14 +1,32 @@
|
|
|
1
1
|
export default {
|
|
2
2
|
template:`
|
|
3
|
-
<div class="flex-shrink-0
|
|
3
|
+
<div class="flex-shrink-0 pl-2 pr-4 py-4 border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 min-h-16 select-none">
|
|
4
4
|
<div class="flex items-center justify-between">
|
|
5
|
-
<
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
5
|
+
<div class="flex items-center space-x-2">
|
|
6
|
+
<button type="button"
|
|
7
|
+
@click="$emit('toggle-sidebar')"
|
|
8
|
+
class="group relative text-gray-500 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 focus:outline-none transition-colors"
|
|
9
|
+
title="Collapse sidebar"
|
|
10
|
+
>
|
|
11
|
+
<div class="relative size-5">
|
|
12
|
+
<!-- Default sidebar icon -->
|
|
13
|
+
<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">
|
|
14
|
+
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
|
15
|
+
<line x1="9" y1="3" x2="9" y2="21"></line>
|
|
16
|
+
</svg>
|
|
17
|
+
<!-- Hover state: |← icon -->
|
|
18
|
+
<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="m10.071 4.929l1.414 1.414L6.828 11H16v2H6.828l4.657 4.657l-1.414 1.414L3 12zM18.001 19V5h2v14z"/></svg>
|
|
19
|
+
</div>
|
|
20
|
+
</button>
|
|
21
|
+
|
|
22
|
+
<button type="button"
|
|
23
|
+
@click="$emit('home')"
|
|
24
|
+
class="text-lg font-semibold text-gray-900 dark:text-gray-200 hover:text-blue-600 dark:hover:text-blue-400 focus:outline-none transition-colors"
|
|
25
|
+
title="Go back to initial state"
|
|
26
|
+
>
|
|
27
|
+
History
|
|
28
|
+
</button>
|
|
29
|
+
</div>
|
|
12
30
|
|
|
13
31
|
<div class="flex items-center space-x-2">
|
|
14
32
|
<button type="button"
|
|
@@ -30,5 +48,5 @@ export default {
|
|
|
30
48
|
</div>
|
|
31
49
|
</div>
|
|
32
50
|
`,
|
|
33
|
-
emits:['home','new','analytics'],
|
|
51
|
+
emits:['home','new','analytics','toggle-sidebar'],
|
|
34
52
|
}
|
llms/ui/ChatPrompt.mjs
CHANGED
|
@@ -87,7 +87,7 @@ export default {
|
|
|
87
87
|
<div class="flex-1">
|
|
88
88
|
<div class="relative">
|
|
89
89
|
<textarea
|
|
90
|
-
ref="
|
|
90
|
+
ref="refMessage"
|
|
91
91
|
v-model="messageText"
|
|
92
92
|
@keydown.enter.exact.prevent="sendMessage"
|
|
93
93
|
@keydown.enter.shift.exact="addNewLine"
|
|
@@ -168,6 +168,7 @@ export default {
|
|
|
168
168
|
} = threads
|
|
169
169
|
|
|
170
170
|
const fileInput = ref(null)
|
|
171
|
+
const refMessage = ref(null)
|
|
171
172
|
const showSettings = ref(false)
|
|
172
173
|
const { applySettings } = chatSettings
|
|
173
174
|
|
|
@@ -550,6 +551,10 @@ export default {
|
|
|
550
551
|
} finally {
|
|
551
552
|
isGenerating.value = false
|
|
552
553
|
chatPrompt.abortController.value = null
|
|
554
|
+
// Restore focus to the textarea
|
|
555
|
+
nextTick(() => {
|
|
556
|
+
refMessage.value?.focus()
|
|
557
|
+
})
|
|
553
558
|
}
|
|
554
559
|
}
|
|
555
560
|
|
|
@@ -567,6 +572,7 @@ export default {
|
|
|
567
572
|
attachedFiles,
|
|
568
573
|
messageText,
|
|
569
574
|
fileInput,
|
|
575
|
+
refMessage,
|
|
570
576
|
showSettings,
|
|
571
577
|
isDragging,
|
|
572
578
|
triggerFilePicker,
|
llms/ui/Main.mjs
CHANGED
|
@@ -30,11 +30,13 @@ export default {
|
|
|
30
30
|
template: `
|
|
31
31
|
<div class="flex flex-col h-full w-full">
|
|
32
32
|
<!-- Header with model and prompt selectors (hidden when auth required and not authenticated) -->
|
|
33
|
-
<div v-if="!($ai.requiresAuth && !$ai.auth)"
|
|
34
|
-
|
|
33
|
+
<div v-if="!($ai.requiresAuth && !$ai.auth)"
|
|
34
|
+
:class="!$ai.isSidebarOpen ? 'pl-6' : ''"
|
|
35
|
+
class="border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 px-2 py-2 w-full min-h-16">
|
|
36
|
+
<div class="flex flex-wrap items-center justify-between w-full">
|
|
35
37
|
<ModelSelector :models="models" v-model="selectedModel" @updated="configUpdated" />
|
|
36
38
|
|
|
37
|
-
<div class="flex items-center space-x-2">
|
|
39
|
+
<div class="flex items-center space-x-2 pl-4">
|
|
38
40
|
<SystemPromptSelector :prompts="prompts" v-model="selectedPrompt"
|
|
39
41
|
:show="showSystemPrompt" @toggle="showSystemPrompt = !showSystemPrompt" />
|
|
40
42
|
<Avatar />
|
llms/ui/ModelSelector.mjs
CHANGED
|
@@ -16,9 +16,10 @@ export default {
|
|
|
16
16
|
:match="(x, value) => x.id.toLowerCase().includes(value.toLowerCase())"
|
|
17
17
|
placeholder="Select Model...">
|
|
18
18
|
<template #item="{ id, provider, provider_model, pricing }">
|
|
19
|
-
<div :key="id + provider + provider_model"
|
|
19
|
+
<div :key="id + provider + provider_model"
|
|
20
|
+
class="group truncate max-w-68 xl:max-w-72 flex justify-between">
|
|
20
21
|
<span :title="id">{{id}}</span>
|
|
21
|
-
<
|
|
22
|
+
<div class="hidden md:flex items-center space-x-1">
|
|
22
23
|
<span v-if="pricing && (parseFloat(pricing.input) == 0 && parseFloat(pricing.input) == 0)">
|
|
23
24
|
<span class="text-xs text-gray-500 dark:text-gray-400" title="Free to use">FREE</span>
|
|
24
25
|
</span>
|
|
@@ -28,10 +29,10 @@ export default {
|
|
|
28
29
|
·
|
|
29
30
|
{{tokenPrice(pricing.output)}} M
|
|
30
31
|
</span>
|
|
31
|
-
<span :title="provider_model + ' from ' + provider">
|
|
32
|
-
<ProviderIcon :provider="provider" />
|
|
32
|
+
<span class="min-w-6" :title="provider_model + ' from ' + provider">
|
|
33
|
+
<ProviderIcon class="hidden xl:inline" :provider="provider" />
|
|
33
34
|
</span>
|
|
34
|
-
</
|
|
35
|
+
</div>
|
|
35
36
|
</div>
|
|
36
37
|
</template>
|
|
37
38
|
</Autocomplete>
|
llms/ui/Sidebar.mjs
CHANGED
|
@@ -164,7 +164,7 @@ const Sidebar = {
|
|
|
164
164
|
},
|
|
165
165
|
template: `
|
|
166
166
|
<div class="flex flex-col h-full bg-gray-50 dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700">
|
|
167
|
-
<Brand @home="goToInitialState" @new="createNewThread" @analytics="goToAnalytics" />
|
|
167
|
+
<Brand @home="goToInitialState" @new="createNewThread" @analytics="goToAnalytics" @toggle-sidebar="$emit('toggle-sidebar')" />
|
|
168
168
|
<!-- Thread List -->
|
|
169
169
|
<div class="flex-1 overflow-y-auto">
|
|
170
170
|
<div v-if="isLoading" class="p-4 text-center text-gray-500 dark:text-gray-400">
|
|
@@ -187,7 +187,8 @@ const Sidebar = {
|
|
|
187
187
|
</div>
|
|
188
188
|
</div>
|
|
189
189
|
`,
|
|
190
|
-
|
|
190
|
+
emits: ['thread-selected', 'toggle-sidebar'],
|
|
191
|
+
setup(props, { emit }) {
|
|
191
192
|
const ai = inject('ai')
|
|
192
193
|
const router = useRouter()
|
|
193
194
|
const threadStore = useThreadStore()
|
|
@@ -208,6 +209,7 @@ const Sidebar = {
|
|
|
208
209
|
|
|
209
210
|
const selectThread = async (threadId) => {
|
|
210
211
|
router.push(`${ai.base}/c/${threadId}`)
|
|
212
|
+
emit('thread-selected')
|
|
211
213
|
}
|
|
212
214
|
|
|
213
215
|
const deleteThread = async (threadId) => {
|
|
@@ -223,15 +225,18 @@ const Sidebar = {
|
|
|
223
225
|
const createNewThread = async () => {
|
|
224
226
|
const newThread = await createThread()
|
|
225
227
|
router.push(`${ai.base}/c/${newThread.id}`)
|
|
228
|
+
emit('thread-selected')
|
|
226
229
|
}
|
|
227
230
|
|
|
228
231
|
const goToInitialState = () => {
|
|
229
232
|
clearCurrentThread()
|
|
230
233
|
router.push(`${ai.base}/`)
|
|
234
|
+
emit('thread-selected')
|
|
231
235
|
}
|
|
232
236
|
|
|
233
237
|
const goToAnalytics = () => {
|
|
234
238
|
router.push(`${ai.base}/analytics`)
|
|
239
|
+
emit('thread-selected')
|
|
235
240
|
}
|
|
236
241
|
|
|
237
242
|
return {
|
llms/ui/SystemPromptSelector.mjs
CHANGED
|
@@ -7,7 +7,7 @@ export default {
|
|
|
7
7
|
|
|
8
8
|
<Autocomplete ref="refSelector" id="prompt" :options="prompts" label=""
|
|
9
9
|
:modelValue="modelValue" @update:modelValue="$emit('update:modelValue', $event)"
|
|
10
|
-
class="w-
|
|
10
|
+
class="w-68 xl:w-84"
|
|
11
11
|
:match="(x, value) => x.name.toLowerCase().includes(value.toLowerCase())"
|
|
12
12
|
placeholder="Select a System Prompt...">
|
|
13
13
|
<template #item="{ value }">
|
llms/ui/ai.mjs
CHANGED
|
@@ -6,7 +6,7 @@ const headers = { 'Accept': 'application/json' }
|
|
|
6
6
|
const prefsKey = 'llms.prefs'
|
|
7
7
|
|
|
8
8
|
export const o = {
|
|
9
|
-
version: '2.0.
|
|
9
|
+
version: '2.0.31',
|
|
10
10
|
base,
|
|
11
11
|
prefsKey,
|
|
12
12
|
welcome: 'Welcome to llms.py',
|
|
@@ -14,6 +14,7 @@ export const o = {
|
|
|
14
14
|
requiresAuth: false,
|
|
15
15
|
authType: 'apikey', // 'oauth' or 'apikey' - controls which SignIn component to use
|
|
16
16
|
headers,
|
|
17
|
+
isSidebarOpen: true, // Shared sidebar state (default open for lg+ screens)
|
|
17
18
|
|
|
18
19
|
resolveUrl(url){
|
|
19
20
|
return url.startsWith('http') || url.startsWith('/v1') ? url : base + url
|