llms-py 2.0.26__py3-none-any.whl → 2.0.27__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/ui/ChatPrompt.mjs CHANGED
@@ -50,7 +50,7 @@ export default {
50
50
  <button type="button"
51
51
  @click="triggerFilePicker"
52
52
  :disabled="isGenerating || !model"
53
- class="size-8 flex items-center justify-center rounded-md border border-gray-300 text-gray-600 hover:bg-gray-50 disabled:text-gray-400 disabled:cursor-not-allowed"
53
+ class="size-8 flex items-center justify-center rounded-md border border-gray-300 dark:border-gray-600 text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-700 disabled:text-gray-400 disabled:cursor-not-allowed"
54
54
  title="Attach image or audio">
55
55
  <svg class="size-5" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 256 256">
56
56
  <path d="M224,128a8,8,0,0,1-8,8H136v80a8,8,0,0,1-16,0V136H40a8,8,0,0,1,0-16h80V40a8,8,0,0,1,16,0v80h80A8,8,0,0,1,224,128Z"></path>
@@ -64,8 +64,8 @@ export default {
64
64
  <div>
65
65
  <button type="button" title="Settings" @click="showSettings = true"
66
66
  :disabled="isGenerating || !model"
67
- class="size-8 flex items-center justify-center rounded-md border border-gray-300 text-gray-600 hover:bg-gray-50 disabled:text-gray-400 disabled:cursor-not-allowed">
68
- <svg class="size-4 text-gray-600 disabled:text-gray-400" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 256 256"><path d="M40,88H73a32,32,0,0,0,62,0h81a8,8,0,0,0,0-16H135a32,32,0,0,0-62,0H40a8,8,0,0,0,0,16Zm64-24A16,16,0,1,1,88,80,16,16,0,0,1,104,64ZM216,168H199a32,32,0,0,0-62,0H40a8,8,0,0,0,0,16h97a32,32,0,0,0,62,0h17a8,8,0,0,0,0-16Zm-48,24a16,16,0,1,1,16-16A16,16,0,0,1,168,192Z"></path></svg>
67
+ class="size-8 flex items-center justify-center rounded-md border border-gray-300 dark:border-gray-600 text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-700 disabled:text-gray-400 disabled:cursor-not-allowed">
68
+ <svg class="size-4 text-gray-600 dark:text-gray-400 disabled:text-gray-400" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 256 256"><path d="M40,88H73a32,32,0,0,0,62,0h81a8,8,0,0,0,0-16H135a32,32,0,0,0-62,0H40a8,8,0,0,0,0,16Zm64-24A16,16,0,1,1,88,80,16,16,0,0,1,104,64ZM216,168H199a32,32,0,0,0-62,0H40a8,8,0,0,0,0,16h97a32,32,0,0,0,62,0h17a8,8,0,0,0,0-16Zm-48,24a16,16,0,1,1,16-16A16,16,0,0,1,168,192Z"></path></svg>
69
69
  </button>
70
70
  </div>
71
71
  </div>
@@ -77,15 +77,24 @@ export default {
77
77
  v-model="messageText"
78
78
  @keydown.enter.exact.prevent="sendMessage"
79
79
  @keydown.enter.shift.exact="addNewLine"
80
- placeholder="Type your message... (Enter to send, Shift+Enter for new line)"
80
+ @paste="onPaste"
81
+ @dragover="onDragOver"
82
+ @dragleave="onDragLeave"
83
+ @drop="onDrop"
84
+ placeholder="Type message... (Enter to send, Shift+Enter for new line, drag & drop or paste files)"
81
85
  rows="3"
82
- class="block w-full rounded-md border border-gray-300 px-3 py-2 pr-12 text-sm placeholder-gray-500 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
86
+ :class="[
87
+ 'block w-full rounded-md border px-3 py-2 pr-12 text-sm text-gray-900 dark:text-gray-100 bg-white dark:bg-gray-900 placeholder-gray-500 dark:placeholder-gray-400 focus:outline-none focus:ring-1',
88
+ isDragging
89
+ ? 'border-blue-500 bg-blue-50 dark:bg-blue-900/30 ring-1 ring-blue-500'
90
+ : 'border-gray-300 dark:border-gray-600 focus:border-blue-500 focus:ring-blue-500'
91
+ ]"
83
92
  :disabled="isGenerating || !model"
84
93
  ></textarea>
85
94
  <button title="Send (Enter)" type="button"
86
95
  @click="sendMessage"
87
96
  :disabled="!messageText.trim() || isGenerating || !model"
88
- class="absolute bottom-2 right-2 size-8 flex items-center justify-center rounded-md border border-gray-300 text-gray-600 hover:bg-gray-50 disabled:text-gray-400 disabled:cursor-not-allowed disabled:border-gray-200 transition-colors">
97
+ class="absolute bottom-2 right-2 size-8 flex items-center justify-center rounded-md border border-gray-300 dark:border-gray-600 text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-700 disabled:text-gray-400 disabled:cursor-not-allowed disabled:border-gray-200 dark:disabled:border-gray-700 transition-colors">
89
98
  <svg v-if="isGenerating" class="size-5 animate-spin" fill="none" viewBox="0 0 24 24">
90
99
  <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
91
100
  <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
@@ -96,15 +105,15 @@ export default {
96
105
 
97
106
  <!-- Attached files preview -->
98
107
  <div v-if="attachedFiles.length" class="mt-2 flex flex-wrap gap-2">
99
- <div v-for="(f,i) in attachedFiles" :key="i" class="flex items-center gap-2 px-2 py-1 rounded-md border border-gray-300 text-xs text-gray-700 bg-gray-50">
108
+ <div v-for="(f,i) in attachedFiles" :key="i" class="flex items-center gap-2 px-2 py-1 rounded-md border border-gray-300 dark:border-gray-600 text-xs text-gray-700 dark:text-gray-300 bg-gray-50 dark:bg-gray-800">
100
109
  <span class="truncate max-w-48" :title="f.name">{{ f.name }}</span>
101
- <button type="button" class="text-gray-500 hover:text-gray-700" @click="removeAttachment(i)" title="Remove Attachment">
110
+ <button type="button" class="text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200" @click="removeAttachment(i)" title="Remove Attachment">
102
111
  <svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
103
112
  </button>
104
113
  </div>
105
114
  </div>
106
115
 
107
- <div v-if="!model" class="mt-2 text-sm text-red-600">
116
+ <div v-if="!model" class="mt-2 text-sm text-red-600 dark:text-red-400">
108
117
  Please select a model
109
118
  </div>
110
119
  </div>
@@ -169,6 +178,93 @@ export default {
169
178
  attachedFiles.value.splice(i, 1)
170
179
  }
171
180
 
181
+ // Helper function to add files and set default message
182
+ const addFilesAndSetMessage = (files) => {
183
+ if (files.length === 0) return
184
+
185
+ attachedFiles.value.push(...files)
186
+
187
+ // Set default message text if empty
188
+ if (!messageText.value.trim()) {
189
+ if (hasImage()) {
190
+ messageText.value = getTextContent(config.defaults.image)
191
+ } else if (hasAudio()) {
192
+ messageText.value = getTextContent(config.defaults.audio)
193
+ } else {
194
+ messageText.value = getTextContent(config.defaults.file)
195
+ }
196
+ }
197
+ }
198
+
199
+ // Handle paste events for clipboard images, audio, and files
200
+ const onPaste = async (e) => {
201
+ // Use the paste event's clipboardData directly (works best for paste events)
202
+ const items = e.clipboardData?.items
203
+ if (!items) return
204
+
205
+ const files = []
206
+
207
+ // Check all clipboard items
208
+ for (let i = 0; i < items.length; i++) {
209
+ const item = items[i]
210
+
211
+ // Handle files (images, audio, etc.)
212
+ if (item.kind === 'file') {
213
+ const file = item.getAsFile()
214
+ if (file) {
215
+ // Generate a better filename based on type
216
+ let filename = file.name
217
+ if (!filename || filename === 'image.png' || filename === 'blob') {
218
+ const ext = file.type.split('/')[1] || 'png'
219
+ const timestamp = new Date().getTime()
220
+ if (file.type.startsWith('image/')) {
221
+ filename = `pasted-image-${timestamp}.${ext}`
222
+ } else if (file.type.startsWith('audio/')) {
223
+ filename = `pasted-audio-${timestamp}.${ext}`
224
+ } else {
225
+ filename = `pasted-file-${timestamp}.${ext}`
226
+ }
227
+ // Create a new File object with the better name
228
+ files.push(new File([file], filename, { type: file.type }))
229
+ } else {
230
+ files.push(file)
231
+ }
232
+ }
233
+ }
234
+ }
235
+
236
+ if (files.length > 0) {
237
+ e.preventDefault()
238
+ addFilesAndSetMessage(files)
239
+ }
240
+ }
241
+
242
+ // Handle drag and drop events
243
+ const isDragging = ref(false)
244
+
245
+ const onDragOver = (e) => {
246
+ e.preventDefault()
247
+ e.stopPropagation()
248
+ isDragging.value = true
249
+ }
250
+
251
+ const onDragLeave = (e) => {
252
+ e.preventDefault()
253
+ e.stopPropagation()
254
+ isDragging.value = false
255
+ }
256
+
257
+ const onDrop = (e) => {
258
+ e.preventDefault()
259
+ e.stopPropagation()
260
+ isDragging.value = false
261
+
262
+ const files = Array.from(e.dataTransfer?.files || [])
263
+ if (files.length > 0) {
264
+ addFilesAndSetMessage(files)
265
+ }
266
+ }
267
+
172
268
  function createChatRequest() {
173
269
  if (hasImage()) {
174
270
  return deepClone(config.defaults.image)
@@ -433,8 +529,13 @@ export default {
433
529
  messageText,
434
530
  fileInput,
435
531
  showSettings,
532
+ isDragging,
436
533
  triggerFilePicker,
437
534
  onFilesSelected,
535
+ onPaste,
536
+ onDragOver,
537
+ onDragLeave,
538
+ onDrop,
438
539
  removeAttachment,
439
540
  sendMessage,
440
541
  addNewLine,
llms/ui/Main.mjs CHANGED
@@ -30,7 +30,7 @@ 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)" class="border-b border-gray-200 bg-white px-2 py-2 w-full min-h-16">
33
+ <div v-if="!($ai.requiresAuth && !$ai.auth)" 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">
34
34
  <div class="flex items-center justify-between w-full">
35
35
  <ModelSelector :models="models" v-model="selectedModel" @updated="configUpdated" />
36
36
 
@@ -38,6 +38,7 @@ export default {
38
38
  <SystemPromptSelector :prompts="prompts" v-model="selectedPrompt"
39
39
  :show="showSystemPrompt" @toggle="showSystemPrompt = !showSystemPrompt" />
40
40
  <Avatar />
41
+ <DarkModeToggle />
41
42
  </div>
42
43
  </div>
43
44
  </div>
@@ -67,7 +68,7 @@ export default {
67
68
  @click="(e) => e.altKey ? exportRequests() : exportThreads()"
68
69
  :disabled="isExporting"
69
70
  :title="'Export ' + threads?.threads?.value?.length + ' conversations'"
70
- 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"
71
+ 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"
71
72
  >
72
73
  <svg v-if="!isExporting" class="size-5 mr-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
73
74
  <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>
@@ -83,7 +84,7 @@ export default {
83
84
  @click="triggerImport"
84
85
  :disabled="isImporting"
85
86
  title="Import conversations from JSON file"
86
- 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"
87
+ 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"
87
88
  >
88
89
  <svg v-if="!isImporting" class="size-5 mr-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
89
90
  <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"/>
@@ -119,15 +120,15 @@ export default {
119
120
  <div class="flex-shrink-0 flex flex-col justify-center">
120
121
  <div class="w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium"
121
122
  :class="message.role === 'user'
122
- ? 'bg-blue-600 text-white'
123
- : 'bg-gray-600 text-white'"
123
+ ? 'bg-blue-100 dark:bg-blue-900 text-gray-900 dark:text-gray-100 border border-blue-200 dark:border-blue-700'
124
+ : 'bg-gray-600 dark:bg-gray-500 text-white'"
124
125
  >
125
126
  {{ message.role === 'user' ? 'U' : 'AI' }}
126
127
  </div>
127
128
 
128
129
  <!-- Delete button (shown on hover) -->
129
130
  <button type="button" @click.stop="threads.deleteMessageFromThread(currentThread.id, message.id)"
130
- 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"
131
+ 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"
131
132
  title="Delete message">
132
133
  <svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
133
134
  <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>
@@ -139,18 +140,18 @@ export default {
139
140
  <div
140
141
  class="message rounded-lg px-4 py-3 relative group"
141
142
  :class="message.role === 'user'
142
- ? 'bg-blue-600 text-white'
143
- : 'bg-gray-100 text-gray-900 border border-gray-200'"
143
+ ? 'bg-blue-100 dark:bg-blue-900 text-gray-900 dark:text-gray-100 border border-blue-200 dark:border-blue-700'
144
+ : 'bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-gray-100 border border-gray-200 dark:border-gray-700'"
144
145
  >
145
146
  <!-- Copy button in top right corner -->
146
147
  <button
147
148
  type="button"
148
149
  @click="copyMessageContent(message)"
149
- 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"
150
- :class="message.role === 'user' ? 'text-white/70 hover:text-white hover:bg-white/20' : 'text-gray-500 hover:text-gray-700'"
150
+ 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"
151
+ :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'"
151
152
  title="Copy message content"
152
153
  >
153
- <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>
154
+ <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>
154
155
  <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">
155
156
  <rect width="14" height="14" x="8" y="8" rx="2" ry="2"/>
156
157
  <path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/>
@@ -160,18 +161,18 @@ export default {
160
161
  <div
161
162
  v-if="message.role === 'assistant'"
162
163
  v-html="renderMarkdown(message.content)"
163
- class="prose prose-sm max-w-none"
164
+ class="prose prose-sm max-w-none dark:prose-invert"
164
165
  ></div>
165
166
 
166
167
  <!-- Collapsible reasoning section -->
167
168
  <div v-if="message.role === 'assistant' && message.reasoning" class="mt-2">
168
- <button type="button" @click="toggleReasoning(message.id)" class="text-xs text-gray-600 hover:text-gray-800 flex items-center space-x-1">
169
+ <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">
169
170
  <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>
170
171
  <span>{{ isReasoningExpanded(message.id) ? 'Hide reasoning' : 'Show reasoning' }}</span>
171
172
  </button>
172
- <div v-if="isReasoningExpanded(message.id)" class="mt-2 rounded border border-gray-200 bg-gray-50 p-2">
173
- <div v-if="typeof message.reasoning === 'string'" v-html="renderMarkdown(message.reasoning)" class="prose prose-xs max-w-none"></div>
174
- <pre v-else class="text-xs whitespace-pre-wrap overflow-x-auto">{{ formatReasoning(message.reasoning) }}</pre>
173
+ <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">
174
+ <div v-if="typeof message.reasoning === 'string'" v-html="renderMarkdown(message.reasoning)" class="prose prose-xs max-w-none dark:prose-invert"></div>
175
+ <pre v-else class="text-xs whitespace-pre-wrap overflow-x-auto text-gray-900 dark:text-gray-100">{{ formatReasoning(message.reasoning) }}</pre>
175
176
  </div>
176
177
  </div>
177
178
 
@@ -190,7 +191,7 @@ export default {
190
191
  <!-- Edit and Redo buttons (shown on hover for user messages, outside bubble) -->
191
192
  <div v-if="message.role === 'user'" class="flex flex-col gap-2 opacity-0 group-hover:opacity-100 transition-opacity mt-1">
192
193
  <button type="button" @click.stop="editMessage(message)"
193
- class="whitespace-nowrap text-xs px-2 py-1 rounded text-gray-400 hover:text-green-600 hover:bg-green-50 transition-all"
194
+ 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"
194
195
  title="Edit message">
195
196
  <svg class="size-4 inline mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
196
197
  <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>
@@ -198,7 +199,7 @@ export default {
198
199
  Edit
199
200
  </button>
200
201
  <button type="button" @click.stop="redoMessage(message)"
201
- class="whitespace-nowrap text-xs px-2 py-1 rounded text-gray-400 hover:text-blue-600 hover:bg-blue-50 transition-all"
202
+ 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"
202
203
  title="Redo message (clears all responses after this message and re-runs it)">
203
204
  <svg class="size-4 inline mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
204
205
  <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>
@@ -208,7 +209,7 @@ export default {
208
209
  </div>
209
210
  </div>
210
211
 
211
- <div v-if="currentThread.stats && currentThread.stats.outputTokens" class="text-center text-gray-500 text-sm">
212
+ <div v-if="currentThread.stats && currentThread.stats.outputTokens" class="text-center text-gray-500 dark:text-gray-400 text-sm">
212
213
  <span :title="statsTitle(currentThread.stats)">
213
214
  {{ 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) }}
214
215
  </span>
@@ -218,17 +219,17 @@ export default {
218
219
  <div v-if="isGenerating" class="flex items-start space-x-3">
219
220
  <!-- Avatar outside the bubble -->
220
221
  <div class="flex-shrink-0">
221
- <div class="w-8 h-8 rounded-full bg-gray-600 text-white flex items-center justify-center text-sm font-medium">
222
+ <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">
222
223
  AI
223
224
  </div>
224
225
  </div>
225
226
 
226
227
  <!-- Loading bubble -->
227
- <div class="rounded-lg px-4 py-3 bg-gray-100 border border-gray-200">
228
+ <div class="rounded-lg px-4 py-3 bg-gray-100 dark:bg-gray-800 border border-gray-200 dark:border-gray-700">
228
229
  <div class="flex space-x-1">
229
- <div class="w-2 h-2 bg-gray-400 rounded-full animate-bounce"></div>
230
- <div class="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style="animation-delay: 0.1s"></div>
231
- <div class="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style="animation-delay: 0.2s"></div>
230
+ <div class="w-2 h-2 bg-gray-400 dark:bg-gray-500 rounded-full animate-bounce"></div>
231
+ <div class="w-2 h-2 bg-gray-400 dark:bg-gray-500 rounded-full animate-bounce" style="animation-delay: 0.1s"></div>
232
+ <div class="w-2 h-2 bg-gray-400 dark:bg-gray-500 rounded-full animate-bounce" style="animation-delay: 0.2s"></div>
232
233
  </div>
233
234
  </div>
234
235
  </div>
@@ -237,24 +238,24 @@ export default {
237
238
  <div v-if="errorStatus" class="flex items-start space-x-3">
238
239
  <!-- Avatar outside the bubble -->
239
240
  <div class="flex-shrink-0">
240
- <div class="w-8 h-8 rounded-full bg-red-600 text-white flex items-center justify-center text-sm font-medium">
241
+ <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">
241
242
  !
242
243
  </div>
243
244
  </div>
244
245
 
245
246
  <!-- Error bubble -->
246
- <div class="max-w-[85%] rounded-lg px-4 py-3 bg-red-50 border border-red-200 text-red-800 shadow-sm">
247
+ <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">
247
248
  <div class="flex items-start space-x-2">
248
249
  <div class="flex-1 min-w-0">
249
250
  <div class="text-base font-medium mb-1">{{ errorStatus?.errorCode || 'Error' }}</div>
250
251
  <div v-if="errorStatus?.message" class="text-base mb-1">{{ errorStatus.message }}</div>
251
- <div v-if="errorStatus?.stackTrace" class="text-sm whitespace-pre-wrap break-words max-h-80 overflow-y-auto font-mono p-2 rounded">
252
+ <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">
252
253
  {{ errorStatus.stackTrace }}
253
254
  </div>
254
255
  </div>
255
256
  <button type="button"
256
257
  @click="errorStatus = null"
257
- class="text-red-400 hover:text-red-600 flex-shrink-0"
258
+ class="text-red-400 dark:text-red-300 hover:text-red-600 dark:hover:text-red-100 flex-shrink-0"
258
259
  >
259
260
  <svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
260
261
  <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>
@@ -268,21 +269,21 @@ export default {
268
269
 
269
270
  <!-- Edit message modal -->
270
271
  <div v-if="editingMessageId" class="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
271
- <div class="relative bg-white rounded-lg shadow-lg p-6 max-w-2xl w-full mx-4">
272
+ <div class="relative bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6 max-w-2xl w-full mx-4">
272
273
  <CloseButton @click="cancelEdit" class="" />
273
- <h3 class="text-lg font-semibold text-gray-900 mb-4">Edit Message</h3>
274
+ <h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">Edit Message</h3>
274
275
  <textarea
275
276
  v-model="editingMessageContent"
276
- 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"
277
+ 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"
277
278
  placeholder="Edit your message..."
278
279
  ></textarea>
279
280
  <div class="mt-4 flex gap-2 justify-end">
280
281
  <button type="button" @click="cancelEdit"
281
- class="px-4 py-2 rounded-md border border-gray-300 text-gray-700 hover:bg-gray-50 transition-all">
282
+ 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">
282
283
  Cancel
283
284
  </button>
284
285
  <button type="button" @click="saveEditedMessage"
285
- class="px-4 py-2 rounded-md bg-blue-600 text-white hover:bg-blue-700 transition-all">
286
+ 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">
286
287
  Save
287
288
  </button>
288
289
  </div>
@@ -291,7 +292,7 @@ export default {
291
292
  </div>
292
293
 
293
294
  <!-- Input Area - only show when thread is selected -->
294
- <div v-if="currentThread" class="flex-shrink-0 border-t border-gray-200 bg-white px-6 py-4">
295
+ <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">
295
296
  <ChatPrompt :model="selectedModel" :systemPrompt="currentSystemPrompt" />
296
297
  </div>
297
298
  </div>
llms/ui/ModelSelector.mjs CHANGED
@@ -1,6 +1,5 @@
1
1
  import ProviderStatus from "./ProviderStatus.mjs"
2
2
  import ProviderIcon from "./ProviderIcon.mjs"
3
- import { useFormatters } from "@servicestack/vue"
4
3
 
5
4
  export default {
6
5
  components: {
@@ -20,15 +19,15 @@ export default {
20
19
  <span :title="id">{{id}}</span>
21
20
  <span class="flex items-center space-x-1">
22
21
  <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>
22
+ <span class="text-xs text-gray-500 dark:text-gray-400" title="Free to use">FREE</span>
24
23
  </span>
25
- <span v-else-if="pricing" class="text-xs text-gray-500"
24
+ <span v-else-if="pricing" class="text-xs text-gray-500 dark:text-gray-400"
26
25
  :title="'Estimated Cost per token: ' + pricing.input + ' input | ' + pricing.output + ' output'">
27
26
  {{tokenPrice(pricing.input)}}
28
27
  &#183;
29
28
  {{tokenPrice(pricing.output)}} M
30
29
  </span>
31
- <span :title="provider_model + ' from ' + provider">
30
+ <span :title="provider_model + ' from ' + provider">
32
31
  <ProviderIcon :provider="provider" />
33
32
  </span>
34
33
  </span>
llms/ui/OAuthSignIn.mjs CHANGED
@@ -11,14 +11,14 @@ export default {
11
11
  <Welcome />
12
12
  </div>
13
13
  <div class="sm:mx-auto sm:w-full sm:max-w-md">
14
- <div v-if="errorMessage" class="mb-3 bg-red-50 border border-red-200 text-red-800 rounded-lg px-4 py-3">
14
+ <div v-if="errorMessage" class="mb-3 bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-800 text-red-800 dark:text-red-200 rounded-lg px-4 py-3">
15
15
  <div class="flex items-start space-x-2">
16
16
  <div class="flex-1">
17
17
  <div class="text-base font-medium">{{ errorMessage }}</div>
18
18
  </div>
19
19
  <button type="button"
20
20
  @click="errorMessage = null"
21
- class="text-red-400 hover:text-red-600 flex-shrink-0"
21
+ class="text-red-400 dark:text-red-300 hover:text-red-600 dark:hover:text-red-100 flex-shrink-0"
22
22
  >
23
23
  <svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
24
24
  <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>
@@ -28,10 +28,10 @@ export default {
28
28
  </div>
29
29
  <div class="py-8 px-4 sm:px-10">
30
30
  <div class="space-y-4">
31
- <button
31
+ <button
32
32
  type="button"
33
33
  @click="signInWithGitHub"
34
- class="w-full inline-flex items-center justify-center px-4 py-3 border border-gray-300 rounded-md shadow-sm text-base font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 transition-colors"
34
+ class="w-full inline-flex items-center justify-center px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-base 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-gray-500 transition-colors"
35
35
  >
36
36
  <svg class="w-6 h-6 mr-3" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
37
37
  <path fill-rule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clip-rule="evenodd" />
@@ -3,25 +3,25 @@ import { ref, computed, inject, onMounted, onUnmounted } from "vue"
3
3
  export default {
4
4
  template:`
5
5
  <div v-if="$ai.isAdmin" ref="triggerRef" class="relative" :key="renderKey">
6
- <button type="button" @click="togglePopover"
7
- class="mt-1 flex space-x-2 items-center text-sm font-semibold select-none rounded-sm py-2 px-3 border border-transparent hover:bg-gray-50 hover:shadow hover:border-gray-200">
8
- <span class="text-gray-600" :title="models.length + ' models from ' + (config.status.enabled||[]).length + ' enabled providers'">{{models.length}}</span>
6
+ <button type="button" @click="togglePopover"
7
+ class="mt-1 flex space-x-2 items-center text-sm font-semibold select-none rounded-sm py-2 px-3 border border-transparent hover:bg-gray-50 dark:hover:bg-gray-700 hover:shadow hover:border-gray-200 dark:hover:border-gray-600">
8
+ <span class="text-gray-600 dark:text-gray-400" :title="models.length + ' models from ' + (config.status.enabled||[]).length + ' enabled providers'">{{models.length}}</span>
9
9
  <div class="cursor-pointer flex items-center" :title="'Enabled:\\n' + (config.status.enabled||[]).map(x => ' ' + x).join('\\n')">
10
- <svg class="size-4 text-green-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9" fill="currentColor"/></svg>
11
- <span class="text-green-700">{{(config.status.enabled||[]).length}}</span>
10
+ <svg class="size-4 text-green-400 dark:text-green-500" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9" fill="currentColor"/></svg>
11
+ <span class="text-green-700 dark:text-green-400">{{(config.status.enabled||[]).length}}</span>
12
12
  </div>
13
13
  <div class="cursor-pointer flex items-center" :title="'Disabled:\\n' + (config.status.disabled||[]).map(x => ' ' + x).join('\\n')">
14
- <svg class="size-4 text-red-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9" fill="currentColor"/></svg>
15
- <span class="text-red-700">{{(config.status.disabled||[]).length}}</span>
14
+ <svg class="size-4 text-red-400 dark:text-red-500" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9" fill="currentColor"/></svg>
15
+ <span class="text-red-700 dark:text-red-400">{{(config.status.disabled||[]).length}}</span>
16
16
  </div>
17
17
  </button>
18
- <div v-if="showPopover" ref="popoverRef" class="absolute right-0 mt-2 w-72 max-h-120 overflow-y-auto bg-white border border-gray-200 rounded-md shadow-lg z-10">
19
- <div class="divide-y divide-gray-100">
18
+ <div v-if="showPopover" ref="popoverRef" class="absolute right-0 mt-2 w-72 max-h-120 overflow-y-auto bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-md shadow-lg z-10">
19
+ <div class="divide-y divide-gray-100 dark:divide-gray-700">
20
20
  <div v-for="p in allProviders" :key="p" class="flex items-center justify-between px-3 py-2">
21
- <label :for="'chk_' + p" class="cursor-pointer text-sm text-gray-900 truncate mr-2" :title="p">{{ p }}</label>
21
+ <label :for="'chk_' + p" class="cursor-pointer text-sm text-gray-900 dark:text-gray-100 truncate mr-2" :title="p">{{ p }}</label>
22
22
  <div @click="onToggle(p, !isEnabled(p))" class="cursor-pointer group relative inline-flex h-5 w-10 shrink-0 items-center justify-center rounded-full outline-offset-2 outline-green-600 has-focus-visible:outline-2">
23
- <span class="absolute mx-auto h-4 w-9 rounded-full bg-gray-200 inset-ring inset-ring-gray-900/5 transition-colors duration-200 ease-in-out group-has-checked:bg-green-600" />
24
- <span class="absolute left-0 size-5 rounded-full border border-gray-300 bg-white shadow-xs transition-transform duration-200 ease-in-out group-has-checked:translate-x-5" />
23
+ <span class="absolute mx-auto h-4 w-9 rounded-full bg-gray-200 dark:bg-gray-700 inset-ring inset-ring-gray-900/5 dark:inset-ring-gray-100/5 transition-colors duration-200 ease-in-out group-has-checked:bg-green-600 dark:group-has-checked:bg-green-500" />
24
+ <span class="absolute left-0 size-5 rounded-full border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-200 shadow-xs transition-transform duration-200 ease-in-out group-has-checked:translate-x-5" />
25
25
  <input :id="'chk_' + p" type="checkbox" :checked="isEnabled(p)" class="switch cursor-pointer absolute inset-0 appearance-none focus:outline-hidden" aria-label="Use setting" name="setting" />
26
26
  </div>
27
27
  </div>
llms/ui/Recents.mjs CHANGED
@@ -7,33 +7,33 @@ const RecentResults = {
7
7
  template:`
8
8
  <div class="flex-1 overflow-y-auto" @scroll="onScroll">
9
9
  <div class="mx-auto max-w-6xl px-4 py-4">
10
- <div class="text-sm text-gray-600 mb-3" v-if="threads.length">
10
+ <div class="text-sm text-gray-600 dark:text-gray-400 mb-3" v-if="threads.length">
11
11
  <span v-if="q">{{ filtered.length }} result{{ filtered.length===1?'':'s' }}</span>
12
12
  <span v-else>Searching {{ threads.length }} conversation{{ threads.length===1?'':'s' }}</span>
13
13
  </div>
14
14
 
15
- <div v-if="!threads.length" class="text-gray-500">No conversations yet.</div>
15
+ <div v-if="!threads.length" class="text-gray-500 dark:text-gray-400">No conversations yet.</div>
16
16
 
17
17
  <table class="w-full">
18
18
  <tbody>
19
- <tr v-for="t in displayed" :key="t.id" class="hover:bg-gray-50">
20
- <td class="py-3 px-1 border-b border-gray-200 max-w-3xl">
19
+ <tr v-for="t in displayed" :key="t.id" class="hover:bg-gray-50 dark:hover:bg-gray-800">
20
+ <td class="py-3 px-1 border-b border-gray-200 dark:border-gray-700 max-w-3xl">
21
21
  <button type="button" @click="open(t.id)" class="w-full text-left">
22
22
  <div class="flex items-start justify-between gap-3">
23
23
  <div class="min-w-0 flex-1">
24
- <div class="font-medium text-gray-900 truncate" :title="t.title">{{ t.title || 'Untitled chat' }}</div>
25
- <div class="mt-1 text-sm text-gray-600 line-clamp-2">
24
+ <div class="font-medium text-gray-900 dark:text-gray-100 truncate" :title="t.title">{{ t.title || 'Untitled chat' }}</div>
25
+ <div class="mt-1 text-sm text-gray-600 dark:text-gray-400 line-clamp-2">
26
26
  <div v-html="snippet(t)"></div>
27
27
  </div>
28
28
  </div>
29
29
  </div>
30
30
  </button>
31
31
  </td>
32
- <td class="py-3 px-1 border-b border-gray-200">
32
+ <td class="py-3 px-1 border-b border-gray-200 dark:border-gray-700">
33
33
  <div class="text-right whitespace-nowrap">
34
- <div class="text-xs text-gray-500">{{ formatDate(t.updatedAt || t.createdAt) }}</div>
35
- <div class="text-[11px] text-gray-500/80">{{ (t.messages?.length || 0) }} messages</div>
36
- <div v-if="t.model" class="text-[11px] text-blue-600">{{ t.model }}</div>
34
+ <div class="text-xs text-gray-500 dark:text-gray-400">{{ formatDate(t.updatedAt || t.createdAt) }}</div>
35
+ <div class="text-[11px] text-gray-500/80 dark:text-gray-400/80">{{ (t.messages?.length || 0) }} messages</div>
36
+ <div v-if="t.model" class="text-[11px] text-blue-600 dark:text-blue-400">{{ t.model }}</div>
37
37
  </div>
38
38
  </td>
39
39
  </tr>
@@ -152,16 +152,16 @@ export default {
152
152
  template: `
153
153
  <div class="flex flex-col h-full w-full">
154
154
  <!-- Header -->
155
- <div class="border-b border-gray-200 bg-white px-4 py-3 min-h-16">
155
+ <div class="border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 px-4 py-3 min-h-16">
156
156
  <div class="max-w-6xl mx-auto flex items-center justify-between gap-3">
157
- <h2 class="text-lg font-semibold text-gray-900">Search Chats</h2>
157
+ <h2 class="text-lg font-semibold text-gray-900 dark:text-gray-100">Search Chats</h2>
158
158
  <div class="flex-1 flex items-center gap-2">
159
159
  <input
160
160
  v-model="q"
161
161
  type="search"
162
162
  placeholder="Search titles and messages..."
163
163
  spellcheck="false"
164
- class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm placeholder-gray-500 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
164
+ class="w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 px-3 py-2 text-sm placeholder-gray-500 dark:placeholder-gray-400 focus:border-blue-500 dark:focus:border-blue-400 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:focus:ring-blue-400"
165
165
  />
166
166
  </div>
167
167
  </div>