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/Main.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import { storageObject, addCopyButtons, formatCost, statsTitle } from './utils.m
|
|
|
6
6
|
import { renderMarkdown } from './markdown.mjs'
|
|
7
7
|
import ChatPrompt, { useChatPrompt } from './ChatPrompt.mjs'
|
|
8
8
|
import SignIn from './SignIn.mjs'
|
|
9
|
+
import OAuthSignIn from './OAuthSignIn.mjs'
|
|
9
10
|
import Avatar from './Avatar.mjs'
|
|
10
11
|
import ModelSelector from './ModelSelector.mjs'
|
|
11
12
|
import SystemPromptSelector from './SystemPromptSelector.mjs'
|
|
@@ -22,32 +23,36 @@ export default {
|
|
|
22
23
|
SystemPromptEditor,
|
|
23
24
|
ChatPrompt,
|
|
24
25
|
SignIn,
|
|
26
|
+
OAuthSignIn,
|
|
25
27
|
Avatar,
|
|
26
28
|
Welcome,
|
|
27
29
|
},
|
|
28
30
|
template: `
|
|
29
31
|
<div class="flex flex-col h-full w-full">
|
|
30
|
-
<!-- Header with model and prompt selectors -->
|
|
31
|
-
<div
|
|
32
|
-
|
|
32
|
+
<!-- Header with model and prompt selectors (hidden when auth required and not authenticated) -->
|
|
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">
|
|
33
37
|
<ModelSelector :models="models" v-model="selectedModel" @updated="configUpdated" />
|
|
34
38
|
|
|
35
|
-
<div class="flex items-center space-x-2">
|
|
36
|
-
<SystemPromptSelector :prompts="prompts" v-model="selectedPrompt"
|
|
39
|
+
<div class="flex items-center space-x-2 pl-4">
|
|
40
|
+
<SystemPromptSelector :prompts="prompts" v-model="selectedPrompt"
|
|
37
41
|
:show="showSystemPrompt" @toggle="showSystemPrompt = !showSystemPrompt" />
|
|
38
42
|
<Avatar />
|
|
39
43
|
</div>
|
|
40
44
|
</div>
|
|
41
45
|
</div>
|
|
42
46
|
|
|
43
|
-
<SystemPromptEditor v-if="showSystemPrompt"
|
|
47
|
+
<SystemPromptEditor v-if="showSystemPrompt && !($ai.requiresAuth && !$ai.auth)"
|
|
44
48
|
v-model="currentSystemPrompt" :prompts="prompts" :selected="selectedPrompt" />
|
|
45
49
|
|
|
46
50
|
<!-- Messages Area -->
|
|
47
51
|
<div class="flex-1 overflow-y-auto" ref="messagesContainer">
|
|
48
52
|
<div class="mx-auto max-w-6xl px-4 py-6">
|
|
49
53
|
<div v-if="$ai.requiresAuth && !$ai.auth">
|
|
50
|
-
<
|
|
54
|
+
<OAuthSignIn v-if="$ai.authType === 'oauth'" @done="$ai.signIn($event)" />
|
|
55
|
+
<SignIn v-else @done="$ai.signIn($event)" />
|
|
51
56
|
</div>
|
|
52
57
|
<!-- Welcome message when no thread is selected -->
|
|
53
58
|
<div v-else-if="!currentThread" class="text-center py-12">
|
|
@@ -59,12 +64,12 @@ export default {
|
|
|
59
64
|
</div>
|
|
60
65
|
|
|
61
66
|
<!-- Export/Import buttons -->
|
|
62
|
-
<div class="mt-2 flex space-x-3 justify-center">
|
|
67
|
+
<div class="mt-2 flex space-x-3 justify-center items-center">
|
|
63
68
|
<button type="button"
|
|
64
|
-
@click="exportThreads"
|
|
69
|
+
@click="(e) => e.altKey ? exportRequests() : exportThreads()"
|
|
65
70
|
:disabled="isExporting"
|
|
66
71
|
:title="'Export ' + threads?.threads?.value?.length + ' conversations'"
|
|
67
|
-
class="inline-flex items-center px-3 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
72
|
+
class="inline-flex items-center px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
68
73
|
>
|
|
69
74
|
<svg v-if="!isExporting" class="size-5 mr-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
70
75
|
<path fill="currentColor" d="m12 16l-5-5l1.4-1.45l2.6 2.6V4h2v8.15l2.6-2.6L17 11zm-6 4q-.825 0-1.412-.587T4 18v-3h2v3h12v-3h2v3q0 .825-.587 1.413T18 20z"></path>
|
|
@@ -80,7 +85,7 @@ export default {
|
|
|
80
85
|
@click="triggerImport"
|
|
81
86
|
:disabled="isImporting"
|
|
82
87
|
title="Import conversations from JSON file"
|
|
83
|
-
class="inline-flex items-center px-3 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
88
|
+
class="inline-flex items-center px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
84
89
|
>
|
|
85
90
|
<svg v-if="!isImporting" class="size-5 mr-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
86
91
|
<path fill="currentColor" d="m14 12l-4-4v3H2v2h8v3m10 2V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v3h2V6h12v12H6v-3H4v3a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2"/>
|
|
@@ -100,6 +105,9 @@ export default {
|
|
|
100
105
|
@change="handleFileImport"
|
|
101
106
|
class="hidden"
|
|
102
107
|
/>
|
|
108
|
+
|
|
109
|
+
<DarkModeToggle />
|
|
110
|
+
|
|
103
111
|
</div>
|
|
104
112
|
|
|
105
113
|
</div>
|
|
@@ -116,15 +124,15 @@ export default {
|
|
|
116
124
|
<div class="flex-shrink-0 flex flex-col justify-center">
|
|
117
125
|
<div class="w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium"
|
|
118
126
|
:class="message.role === 'user'
|
|
119
|
-
? 'bg-blue-
|
|
120
|
-
: 'bg-gray-600 text-white'"
|
|
127
|
+
? 'bg-blue-100 dark:bg-blue-900 text-gray-900 dark:text-gray-100 border border-blue-200 dark:border-blue-700'
|
|
128
|
+
: 'bg-gray-600 dark:bg-gray-500 text-white'"
|
|
121
129
|
>
|
|
122
130
|
{{ message.role === 'user' ? 'U' : 'AI' }}
|
|
123
131
|
</div>
|
|
124
132
|
|
|
125
133
|
<!-- Delete button (shown on hover) -->
|
|
126
134
|
<button type="button" @click.stop="threads.deleteMessageFromThread(currentThread.id, message.id)"
|
|
127
|
-
class="mx-auto opacity-0 group-hover:opacity-100 mt-2 rounded text-gray-400 hover:text-red-600 hover:bg-red-50 transition-all"
|
|
135
|
+
class="mx-auto opacity-0 group-hover:opacity-100 mt-2 rounded text-gray-400 dark:text-gray-500 hover:text-red-600 dark:hover:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/30 transition-all"
|
|
128
136
|
title="Delete message">
|
|
129
137
|
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
130
138
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
|
|
@@ -136,18 +144,18 @@ export default {
|
|
|
136
144
|
<div
|
|
137
145
|
class="message rounded-lg px-4 py-3 relative group"
|
|
138
146
|
:class="message.role === 'user'
|
|
139
|
-
? 'bg-blue-
|
|
140
|
-
: 'bg-gray-100 text-gray-900 border border-gray-200'"
|
|
147
|
+
? 'bg-blue-100 dark:bg-blue-900 text-gray-900 dark:text-gray-100 border border-blue-200 dark:border-blue-700'
|
|
148
|
+
: 'bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-gray-100 border border-gray-200 dark:border-gray-700'"
|
|
141
149
|
>
|
|
142
150
|
<!-- Copy button in top right corner -->
|
|
143
151
|
<button
|
|
144
152
|
type="button"
|
|
145
153
|
@click="copyMessageContent(message)"
|
|
146
|
-
class="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 p-1 rounded hover:bg-black/10 focus:outline-none focus:ring-0"
|
|
147
|
-
:class="message.role === 'user' ? 'text-
|
|
154
|
+
class="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 p-1 rounded hover:bg-black/10 dark:hover:bg-white/10 focus:outline-none focus:ring-0"
|
|
155
|
+
:class="message.role === 'user' ? 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200' : 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200'"
|
|
148
156
|
title="Copy message content"
|
|
149
157
|
>
|
|
150
|
-
<svg v-if="copying === message" class="size-4 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg>
|
|
158
|
+
<svg v-if="copying === message" class="size-4 text-green-500 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg>
|
|
151
159
|
<svg v-else class="size-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
152
160
|
<rect width="14" height="14" x="8" y="8" rx="2" ry="2"/>
|
|
153
161
|
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/>
|
|
@@ -157,18 +165,18 @@ export default {
|
|
|
157
165
|
<div
|
|
158
166
|
v-if="message.role === 'assistant'"
|
|
159
167
|
v-html="renderMarkdown(message.content)"
|
|
160
|
-
class="prose prose-sm max-w-none"
|
|
168
|
+
class="prose prose-sm max-w-none dark:prose-invert"
|
|
161
169
|
></div>
|
|
162
170
|
|
|
163
171
|
<!-- Collapsible reasoning section -->
|
|
164
172
|
<div v-if="message.role === 'assistant' && message.reasoning" class="mt-2">
|
|
165
|
-
<button type="button" @click="toggleReasoning(message.id)" class="text-xs text-gray-600 hover:text-gray-800 flex items-center space-x-1">
|
|
173
|
+
<button type="button" @click="toggleReasoning(message.id)" class="text-xs text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 flex items-center space-x-1">
|
|
166
174
|
<svg class="w-3 h-3" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" :class="isReasoningExpanded(message.id) ? 'transform rotate-90' : ''"><path fill="currentColor" d="M7 5l6 5l-6 5z"/></svg>
|
|
167
175
|
<span>{{ isReasoningExpanded(message.id) ? 'Hide reasoning' : 'Show reasoning' }}</span>
|
|
168
176
|
</button>
|
|
169
|
-
<div v-if="isReasoningExpanded(message.id)" class="mt-2 rounded border border-gray-200 bg-gray-50 p-2">
|
|
170
|
-
<div v-if="typeof message.reasoning === 'string'" v-html="renderMarkdown(message.reasoning)" class="prose prose-xs max-w-none"></div>
|
|
171
|
-
<pre v-else class="text-xs whitespace-pre-wrap overflow-x-auto">{{ formatReasoning(message.reasoning) }}</pre>
|
|
177
|
+
<div v-if="isReasoningExpanded(message.id)" class="mt-2 rounded border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900 p-2">
|
|
178
|
+
<div v-if="typeof message.reasoning === 'string'" v-html="renderMarkdown(message.reasoning)" class="prose prose-xs max-w-none dark:prose-invert"></div>
|
|
179
|
+
<pre v-else class="text-xs whitespace-pre-wrap overflow-x-auto text-gray-900 dark:text-gray-100">{{ formatReasoning(message.reasoning) }}</pre>
|
|
172
180
|
</div>
|
|
173
181
|
</div>
|
|
174
182
|
|
|
@@ -187,7 +195,7 @@ export default {
|
|
|
187
195
|
<!-- Edit and Redo buttons (shown on hover for user messages, outside bubble) -->
|
|
188
196
|
<div v-if="message.role === 'user'" class="flex flex-col gap-2 opacity-0 group-hover:opacity-100 transition-opacity mt-1">
|
|
189
197
|
<button type="button" @click.stop="editMessage(message)"
|
|
190
|
-
class="whitespace-nowrap text-xs px-2 py-1 rounded text-gray-400 hover:text-green-600 hover:bg-green-50 transition-all"
|
|
198
|
+
class="whitespace-nowrap text-xs px-2 py-1 rounded text-gray-400 dark:text-gray-500 hover:text-green-600 dark:hover:text-green-400 hover:bg-green-50 dark:hover:bg-green-900/30 transition-all"
|
|
191
199
|
title="Edit message">
|
|
192
200
|
<svg class="size-4 inline mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
193
201
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
|
|
@@ -195,7 +203,7 @@ export default {
|
|
|
195
203
|
Edit
|
|
196
204
|
</button>
|
|
197
205
|
<button type="button" @click.stop="redoMessage(message)"
|
|
198
|
-
class="whitespace-nowrap text-xs px-2 py-1 rounded text-gray-400 hover:text-blue-600 hover:bg-blue-50 transition-all"
|
|
206
|
+
class="whitespace-nowrap text-xs px-2 py-1 rounded text-gray-400 dark:text-gray-500 hover:text-blue-600 dark:hover:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/30 transition-all"
|
|
199
207
|
title="Redo message (clears all responses after this message and re-runs it)">
|
|
200
208
|
<svg class="size-4 inline mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
201
209
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
|
|
@@ -205,53 +213,60 @@ export default {
|
|
|
205
213
|
</div>
|
|
206
214
|
</div>
|
|
207
215
|
|
|
208
|
-
<div v-if="currentThread.stats && currentThread.stats.outputTokens" class="text-center text-gray-500 text-sm">
|
|
216
|
+
<div v-if="currentThread.stats && currentThread.stats.outputTokens" class="text-center text-gray-500 dark:text-gray-400 text-sm">
|
|
209
217
|
<span :title="statsTitle(currentThread.stats)">
|
|
210
218
|
{{ currentThread.stats.cost ? formatCost(currentThread.stats.cost) + ' for ' : '' }} {{ humanifyNumber(currentThread.stats.inputTokens) }} → {{ humanifyNumber(currentThread.stats.outputTokens) }} tokens over {{ currentThread.stats.requests }} request{{currentThread.stats.requests===1?'':'s'}} in {{ humanifyMs(currentThread.stats.duration) }}
|
|
211
219
|
</span>
|
|
212
220
|
</div>
|
|
213
221
|
|
|
214
222
|
<!-- Loading indicator -->
|
|
215
|
-
<div v-if="isGenerating" class="flex items-start space-x-3">
|
|
223
|
+
<div v-if="isGenerating" class="flex items-start space-x-3 group">
|
|
216
224
|
<!-- Avatar outside the bubble -->
|
|
217
225
|
<div class="flex-shrink-0">
|
|
218
|
-
<div class="w-8 h-8 rounded-full bg-gray-600 text-white flex items-center justify-center text-sm font-medium">
|
|
226
|
+
<div class="w-8 h-8 rounded-full bg-gray-600 dark:bg-gray-500 text-white flex items-center justify-center text-sm font-medium">
|
|
219
227
|
AI
|
|
220
228
|
</div>
|
|
221
229
|
</div>
|
|
222
230
|
|
|
223
231
|
<!-- Loading bubble -->
|
|
224
|
-
<div class="rounded-lg px-4 py-3 bg-gray-100 border border-gray-200">
|
|
232
|
+
<div class="rounded-lg px-4 py-3 bg-gray-100 dark:bg-gray-800 border border-gray-200 dark:border-gray-700">
|
|
225
233
|
<div class="flex space-x-1">
|
|
226
|
-
<div class="w-2 h-2 bg-gray-400 rounded-full animate-bounce"></div>
|
|
227
|
-
<div class="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style="animation-delay: 0.1s"></div>
|
|
228
|
-
<div class="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style="animation-delay: 0.2s"></div>
|
|
234
|
+
<div class="w-2 h-2 bg-gray-400 dark:bg-gray-500 rounded-full animate-bounce"></div>
|
|
235
|
+
<div class="w-2 h-2 bg-gray-400 dark:bg-gray-500 rounded-full animate-bounce" style="animation-delay: 0.1s"></div>
|
|
236
|
+
<div class="w-2 h-2 bg-gray-400 dark:bg-gray-500 rounded-full animate-bounce" style="animation-delay: 0.2s"></div>
|
|
229
237
|
</div>
|
|
230
238
|
</div>
|
|
239
|
+
|
|
240
|
+
<!-- Cancel button -->
|
|
241
|
+
<button type="button" @click="cancelRequest"
|
|
242
|
+
class="px-3 py-1 rounded text-sm text-gray-400 dark:text-gray-500 hover:text-red-600 dark:hover:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/30 border border-transparent hover:border-red-300 dark:hover:border-red-600 transition-all"
|
|
243
|
+
title="Cancel request">
|
|
244
|
+
cancel
|
|
245
|
+
</button>
|
|
231
246
|
</div>
|
|
232
247
|
|
|
233
248
|
<!-- Error message bubble -->
|
|
234
249
|
<div v-if="errorStatus" class="flex items-start space-x-3">
|
|
235
250
|
<!-- Avatar outside the bubble -->
|
|
236
251
|
<div class="flex-shrink-0">
|
|
237
|
-
<div class="w-8 h-8 rounded-full bg-red-600 text-white flex items-center justify-center text-sm font-medium">
|
|
252
|
+
<div class="w-8 h-8 rounded-full bg-red-600 dark:bg-red-500 text-white flex items-center justify-center text-sm font-medium">
|
|
238
253
|
!
|
|
239
254
|
</div>
|
|
240
255
|
</div>
|
|
241
256
|
|
|
242
257
|
<!-- Error bubble -->
|
|
243
|
-
<div class="max-w-[85%] rounded-lg px-4 py-3 bg-red-50 border border-red-200 text-red-800 shadow-sm">
|
|
258
|
+
<div class="max-w-[85%] rounded-lg px-4 py-3 bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-800 text-red-800 dark:text-red-200 shadow-sm">
|
|
244
259
|
<div class="flex items-start space-x-2">
|
|
245
260
|
<div class="flex-1 min-w-0">
|
|
246
261
|
<div class="text-base font-medium mb-1">{{ errorStatus?.errorCode || 'Error' }}</div>
|
|
247
262
|
<div v-if="errorStatus?.message" class="text-base mb-1">{{ errorStatus.message }}</div>
|
|
248
|
-
<div v-if="errorStatus?.stackTrace" class="text-sm whitespace-pre-wrap break-words max-h-80 overflow-y-auto font-mono p-2 rounded">
|
|
263
|
+
<div v-if="errorStatus?.stackTrace" class="text-sm whitespace-pre-wrap break-words max-h-80 overflow-y-auto font-mono p-2 rounded bg-red-100 dark:bg-red-950/50">
|
|
249
264
|
{{ errorStatus.stackTrace }}
|
|
250
265
|
</div>
|
|
251
266
|
</div>
|
|
252
267
|
<button type="button"
|
|
253
268
|
@click="errorStatus = null"
|
|
254
|
-
class="text-red-400 hover:text-red-600 flex-shrink-0"
|
|
269
|
+
class="text-red-400 dark:text-red-300 hover:text-red-600 dark:hover:text-red-100 flex-shrink-0"
|
|
255
270
|
>
|
|
256
271
|
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
|
257
272
|
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path>
|
|
@@ -265,21 +280,21 @@ export default {
|
|
|
265
280
|
|
|
266
281
|
<!-- Edit message modal -->
|
|
267
282
|
<div v-if="editingMessageId" class="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
|
268
|
-
<div class="relative bg-white rounded-lg shadow-lg p-6 max-w-2xl w-full mx-4">
|
|
283
|
+
<div class="relative bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6 max-w-2xl w-full mx-4">
|
|
269
284
|
<CloseButton @click="cancelEdit" class="" />
|
|
270
|
-
<h3 class="text-lg font-semibold text-gray-900 mb-4">Edit Message</h3>
|
|
285
|
+
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">Edit Message</h3>
|
|
271
286
|
<textarea
|
|
272
287
|
v-model="editingMessageContent"
|
|
273
|
-
class="w-full h-40 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none"
|
|
288
|
+
class="w-full h-40 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 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none"
|
|
274
289
|
placeholder="Edit your message..."
|
|
275
290
|
></textarea>
|
|
276
291
|
<div class="mt-4 flex gap-2 justify-end">
|
|
277
292
|
<button type="button" @click="cancelEdit"
|
|
278
|
-
class="px-4 py-2 rounded-md border border-gray-300 text-gray-700 hover:bg-gray-50 transition-all">
|
|
293
|
+
class="px-4 py-2 rounded-md border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 transition-all">
|
|
279
294
|
Cancel
|
|
280
295
|
</button>
|
|
281
296
|
<button type="button" @click="saveEditedMessage"
|
|
282
|
-
class="px-4 py-2 rounded-md bg-blue-600 text-white hover:bg-blue-700 transition-all">
|
|
297
|
+
class="px-4 py-2 rounded-md bg-blue-600 dark:bg-blue-500 text-white hover:bg-blue-700 dark:hover:bg-blue-600 transition-all">
|
|
283
298
|
Save
|
|
284
299
|
</button>
|
|
285
300
|
</div>
|
|
@@ -288,7 +303,7 @@ export default {
|
|
|
288
303
|
</div>
|
|
289
304
|
|
|
290
305
|
<!-- Input Area - only show when thread is selected -->
|
|
291
|
-
<div v-if="currentThread" class="flex-shrink-0 border-t border-gray-200 bg-white px-6 py-4">
|
|
306
|
+
<div v-if="currentThread" class="flex-shrink-0 border-t border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 px-6 py-4">
|
|
292
307
|
<ChatPrompt :model="selectedModel" :systemPrompt="currentSystemPrompt" />
|
|
293
308
|
</div>
|
|
294
309
|
</div>
|
|
@@ -411,7 +426,7 @@ export default {
|
|
|
411
426
|
const exportData = {
|
|
412
427
|
exportedAt: new Date().toISOString(),
|
|
413
428
|
version: '1.0',
|
|
414
|
-
source: '
|
|
429
|
+
source: 'llmspy',
|
|
415
430
|
threadCount: allThreads.length,
|
|
416
431
|
threads: allThreads
|
|
417
432
|
}
|
|
@@ -423,7 +438,7 @@ export default {
|
|
|
423
438
|
|
|
424
439
|
const link = document.createElement('a')
|
|
425
440
|
link.href = url
|
|
426
|
-
link.download = `
|
|
441
|
+
link.download = `llmsthreads-export-${new Date().toISOString().split('T')[0]}.json`
|
|
427
442
|
document.body.appendChild(link)
|
|
428
443
|
link.click()
|
|
429
444
|
document.body.removeChild(link)
|
|
@@ -437,6 +452,44 @@ export default {
|
|
|
437
452
|
}
|
|
438
453
|
}
|
|
439
454
|
|
|
455
|
+
async function exportRequests() {
|
|
456
|
+
if (isExporting.value) return
|
|
457
|
+
|
|
458
|
+
isExporting.value = true
|
|
459
|
+
try {
|
|
460
|
+
// Load all threads from IndexedDB
|
|
461
|
+
const allRequests = await threads.getAllRequests()
|
|
462
|
+
|
|
463
|
+
// Create export data with metadata
|
|
464
|
+
const exportData = {
|
|
465
|
+
exportedAt: new Date().toISOString(),
|
|
466
|
+
version: '1.0',
|
|
467
|
+
source: 'llmspy',
|
|
468
|
+
requestsCount: allRequests.length,
|
|
469
|
+
requests: allRequests
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Create and download JSON file
|
|
473
|
+
const jsonString = JSON.stringify(exportData, null, 2)
|
|
474
|
+
const blob = new Blob([jsonString], { type: 'application/json' })
|
|
475
|
+
const url = URL.createObjectURL(blob)
|
|
476
|
+
|
|
477
|
+
const link = document.createElement('a')
|
|
478
|
+
link.href = url
|
|
479
|
+
link.download = `llmsrequests-export-${new Date().toISOString().split('T')[0]}.json`
|
|
480
|
+
document.body.appendChild(link)
|
|
481
|
+
link.click()
|
|
482
|
+
document.body.removeChild(link)
|
|
483
|
+
URL.revokeObjectURL(url)
|
|
484
|
+
|
|
485
|
+
} catch (error) {
|
|
486
|
+
console.error('Failed to export requests:', error)
|
|
487
|
+
alert('Failed to export requests: ' + error.message)
|
|
488
|
+
} finally {
|
|
489
|
+
isExporting.value = false
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
440
493
|
function triggerImport() {
|
|
441
494
|
if (isImporting.value) return
|
|
442
495
|
fileInput.value?.click()
|
|
@@ -447,71 +500,94 @@ export default {
|
|
|
447
500
|
if (!file) return
|
|
448
501
|
|
|
449
502
|
isImporting.value = true
|
|
503
|
+
var importType = 'threads'
|
|
450
504
|
try {
|
|
451
505
|
const text = await file.text()
|
|
452
506
|
const importData = JSON.parse(text)
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
507
|
+
importType = importData.threads
|
|
508
|
+
? 'threads'
|
|
509
|
+
: importData.requests
|
|
510
|
+
? 'requests'
|
|
511
|
+
: 'unknown'
|
|
458
512
|
|
|
459
513
|
// Import threads one by one
|
|
460
514
|
let importedCount = 0
|
|
461
|
-
let
|
|
515
|
+
let existingCount = 0
|
|
462
516
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
517
|
+
const db = await threads.initDB()
|
|
518
|
+
|
|
519
|
+
if (importData.threads) {
|
|
520
|
+
if (!Array.isArray(importData.threads)) {
|
|
521
|
+
throw new Error('Invalid import file: missing or invalid threads array')
|
|
467
522
|
}
|
|
468
523
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
id: threadData.id,
|
|
492
|
-
title: threadData.title || 'Imported Chat',
|
|
493
|
-
model: threadData.model || '',
|
|
494
|
-
systemPrompt: threadData.systemPrompt || '',
|
|
495
|
-
messages: threadData.messages || [],
|
|
496
|
-
createdAt: threadData.createdAt || new Date().toISOString(),
|
|
497
|
-
updatedAt: threadData.updatedAt || new Date().toISOString()
|
|
498
|
-
})
|
|
499
|
-
await tx.complete
|
|
500
|
-
importedCount++
|
|
524
|
+
const threadIds = new Set(await threads.getAllThreadIds())
|
|
525
|
+
|
|
526
|
+
for (const threadData of importData.threads) {
|
|
527
|
+
if (!threadData.id) {
|
|
528
|
+
console.warn('Skipping thread without ID:', threadData)
|
|
529
|
+
continue
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
try {
|
|
533
|
+
// Check if thread already exists
|
|
534
|
+
const existingThread = threadIds.has(threadData.id)
|
|
535
|
+
if (existingThread) {
|
|
536
|
+
existingCount++
|
|
537
|
+
} else {
|
|
538
|
+
// Add new thread directly to IndexedDB
|
|
539
|
+
const tx = db.transaction(['threads'], 'readwrite')
|
|
540
|
+
await tx.objectStore('threads').add(threadData)
|
|
541
|
+
await tx.complete
|
|
542
|
+
importedCount++
|
|
543
|
+
}
|
|
544
|
+
} catch (error) {
|
|
545
|
+
console.error('Failed to import thread:', threadData.id, error)
|
|
501
546
|
}
|
|
502
|
-
} catch (error) {
|
|
503
|
-
console.error('Failed to import thread:', threadData.id, error)
|
|
504
547
|
}
|
|
548
|
+
|
|
549
|
+
// Reload threads to reflect changes
|
|
550
|
+
await threads.loadThreads()
|
|
551
|
+
|
|
552
|
+
alert(`Import completed!\nNew threads: ${importedCount}\nExisting threads: ${existingCount}`)
|
|
505
553
|
}
|
|
554
|
+
if (importData.requests) {
|
|
555
|
+
if (!Array.isArray(importData.requests)) {
|
|
556
|
+
throw new Error('Invalid import file: missing or invalid requests array')
|
|
557
|
+
}
|
|
506
558
|
|
|
507
|
-
|
|
508
|
-
await threads.loadThreads()
|
|
559
|
+
const requestIds = new Set(await threads.getAllRequestIds())
|
|
509
560
|
|
|
510
|
-
|
|
561
|
+
for (const requestData of importData.requests) {
|
|
562
|
+
if (!requestData.id) {
|
|
563
|
+
console.warn('Skipping request without ID:', requestData)
|
|
564
|
+
continue
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
try {
|
|
568
|
+
// Check if request already exists
|
|
569
|
+
const existingRequest = requestIds.has(requestData.id)
|
|
570
|
+
if (existingRequest) {
|
|
571
|
+
existingCount++
|
|
572
|
+
} else {
|
|
573
|
+
// Add new request directly to IndexedDB
|
|
574
|
+
const db = await threads.initDB()
|
|
575
|
+
const tx = db.transaction(['requests'], 'readwrite')
|
|
576
|
+
await tx.objectStore('requests').add(requestData)
|
|
577
|
+
await tx.complete
|
|
578
|
+
importedCount++
|
|
579
|
+
}
|
|
580
|
+
} catch (error) {
|
|
581
|
+
console.error('Failed to import request:', requestData.id, error)
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
alert(`Import completed!\nNew requests: ${importedCount}\nExisting requests: ${existingCount}`)
|
|
586
|
+
}
|
|
511
587
|
|
|
512
588
|
} catch (error) {
|
|
513
|
-
console.error('Failed to import
|
|
514
|
-
alert('Failed to import
|
|
589
|
+
console.error('Failed to import ' + importType + ':', error)
|
|
590
|
+
alert('Failed to import ' + importType + ': ' + error.message)
|
|
515
591
|
} finally {
|
|
516
592
|
isImporting.value = false
|
|
517
593
|
// Clear the file input
|
|
@@ -674,6 +750,11 @@ export default {
|
|
|
674
750
|
editingMessage.value = null
|
|
675
751
|
}
|
|
676
752
|
|
|
753
|
+
// Cancel pending request
|
|
754
|
+
const cancelRequest = () => {
|
|
755
|
+
chatPrompt.cancel()
|
|
756
|
+
}
|
|
757
|
+
|
|
677
758
|
function tokensTitle(usage) {
|
|
678
759
|
let title = []
|
|
679
760
|
if (usage.tokens && usage.price) {
|
|
@@ -723,8 +804,10 @@ export default {
|
|
|
723
804
|
editMessage,
|
|
724
805
|
saveEditedMessage,
|
|
725
806
|
cancelEdit,
|
|
807
|
+
cancelRequest,
|
|
726
808
|
configUpdated,
|
|
727
809
|
exportThreads,
|
|
810
|
+
exportRequests,
|
|
728
811
|
isExporting,
|
|
729
812
|
triggerImport,
|
|
730
813
|
handleFileImport,
|
llms/ui/ModelSelector.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { ref, onMounted, onUnmounted } from "vue"
|
|
1
2
|
import ProviderStatus from "./ProviderStatus.mjs"
|
|
2
3
|
import ProviderIcon from "./ProviderIcon.mjs"
|
|
3
|
-
import { useFormatters } from "@servicestack/vue"
|
|
4
4
|
|
|
5
5
|
export default {
|
|
6
6
|
components: {
|
|
@@ -10,28 +10,29 @@ export default {
|
|
|
10
10
|
template:`
|
|
11
11
|
<!-- Model Selector -->
|
|
12
12
|
<div class="pl-1 flex space-x-2">
|
|
13
|
-
<Autocomplete id="model" :options="models" label=""
|
|
13
|
+
<Autocomplete ref="refSelector" id="model" :options="models" label=""
|
|
14
14
|
:modelValue="modelValue" @update:modelValue="$emit('update:modelValue', $event)"
|
|
15
15
|
class="w-72 xl:w-84"
|
|
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
|
-
<span class="text-xs text-gray-500" title="Free to use">FREE</span>
|
|
24
|
+
<span class="text-xs text-gray-500 dark:text-gray-400" title="Free to use">FREE</span>
|
|
24
25
|
</span>
|
|
25
|
-
<span v-else-if="pricing" class="text-xs text-gray-500"
|
|
26
|
+
<span v-else-if="pricing" class="text-xs text-gray-500 dark:text-gray-400"
|
|
26
27
|
:title="'Estimated Cost per token: ' + pricing.input + ' input | ' + pricing.output + ' output'">
|
|
27
28
|
{{tokenPrice(pricing.input)}}
|
|
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>
|
|
@@ -53,8 +54,25 @@ export default {
|
|
|
53
54
|
return ret.endsWith('.00') ? ret.slice(0, -3) : ret
|
|
54
55
|
}
|
|
55
56
|
|
|
57
|
+
const refSelector = ref()
|
|
58
|
+
|
|
59
|
+
function collapse(e) {
|
|
60
|
+
// call toggle when clicking outside of the Autocomplete component
|
|
61
|
+
if (refSelector.value && !refSelector.value.$el.contains(e.target)) {
|
|
62
|
+
refSelector.value.toggle(false)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
onMounted(() => {
|
|
67
|
+
document.addEventListener('click', collapse)
|
|
68
|
+
})
|
|
69
|
+
onUnmounted(() => {
|
|
70
|
+
document.removeEventListener('click', collapse)
|
|
71
|
+
})
|
|
72
|
+
|
|
56
73
|
return {
|
|
57
|
-
|
|
74
|
+
refSelector,
|
|
75
|
+
tokenPrice,
|
|
58
76
|
}
|
|
59
77
|
}
|
|
60
78
|
}
|