llms-py 2.0.35__py3-none-any.whl → 3.0.0b1__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/Main.mjs CHANGED
@@ -2,7 +2,7 @@ import { ref, computed, nextTick, watch, onMounted, provide, inject } from 'vue'
2
2
  import { useRouter, useRoute } from 'vue-router'
3
3
  import { useFormatters } from '@servicestack/vue'
4
4
  import { useThreadStore } from './threadStore.mjs'
5
- import { storageObject, addCopyButtons, formatCost, statsTitle } from './utils.mjs'
5
+ import { storageObject, addCopyButtons, formatCost, statsTitle, fetchCacheInfos } from './utils.mjs'
6
6
  import { renderMarkdown } from './markdown.mjs'
7
7
  import ChatPrompt, { useChatPrompt } from './ChatPrompt.mjs'
8
8
  import SignIn from './SignIn.mjs'
@@ -59,9 +59,8 @@ export default {
59
59
  <Welcome />
60
60
 
61
61
  <!-- Chat input for new conversation -->
62
- <div class="max-w-2xl mx-auto">
63
- <ChatPrompt :model="selectedModel" :systemPrompt="currentSystemPrompt" />
64
- </div>
62
+ <!-- Moved to bottom input area -->
63
+ <div class="h-2"></div>
65
64
 
66
65
  <!-- Export/Import buttons -->
67
66
  <div class="mt-2 flex space-x-3 justify-center items-center">
@@ -180,7 +179,31 @@ export default {
180
179
  </div>
181
180
  </div>
182
181
 
183
- <div v-if="message.role !== 'assistant'" class="whitespace-pre-wrap">{{ message.content }}</div>
182
+ <!-- User Message with separate attachments -->
183
+ <div v-if="message.role !== 'assistant'">
184
+ <div v-html="renderMarkdown(message.content)" class="prose prose-sm max-w-none dark:prose-invert break-words"></div>
185
+
186
+ <!-- Attachments Grid -->
187
+ <div v-if="hasAttachments(message)" class="mt-2 flex flex-wrap gap-2">
188
+ <template v-for="(part, i) in getAttachments(message)" :key="i">
189
+ <!-- Image -->
190
+ <div v-if="part.type === 'image_url'" class="group relative cursor-pointer" @click="openLightbox(part.image_url.url)">
191
+ <img :src="part.image_url.url" class="max-w-[400px] max-h-96 rounded-lg border border-gray-200 dark:border-gray-700 object-contain bg-gray-50 dark:bg-gray-900 shadow-sm transition-transform hover:scale-[1.02]" />
192
+ </div>
193
+ <!-- Audio -->
194
+ <div v-else-if="part.type === 'input_audio'" class="flex items-center gap-2 p-2 rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800">
195
+ <svg class="w-5 h-5 text-gray-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18V5l12-2v13"></path><circle cx="6" cy="18" r="3"></circle><circle cx="18" cy="16" r="3"></circle></svg>
196
+ <audio controls :src="part.input_audio.data" class="h-8 w-48"></audio>
197
+ </div>
198
+ <!-- File -->
199
+ <a v-else-if="part.type === 'file'" :href="part.file.file_data" target="_blank"
200
+ class="flex items-center gap-2 px-3 py-2 rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors text-sm text-blue-600 dark:text-blue-400 hover:underline">
201
+ <svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path><polyline points="13 2 13 9 20 9"></polyline></svg>
202
+ <span class="max-w-xs truncate">{{ part.file.filename || 'Attachment' }}</span>
203
+ </a>
204
+ </template>
205
+ </div>
206
+ </div>
184
207
  <div class="mt-2 text-xs opacity-70">
185
208
  <span>{{ formatTime(message.timestamp) }}</span>
186
209
  <span v-if="message.usage" :title="tokensTitle(message.usage)">
@@ -278,33 +301,25 @@ export default {
278
301
  </div>
279
302
  </div>
280
303
 
281
- <!-- Edit message modal -->
282
- <div v-if="editingMessageId" class="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
283
- <div class="relative bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6 max-w-2xl w-full mx-4">
284
- <CloseButton @click="cancelEdit" class="" />
285
- <h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">Edit Message</h3>
286
- <textarea
287
- v-model="editingMessageContent"
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"
289
- placeholder="Edit your message..."
290
- ></textarea>
291
- <div class="mt-4 flex gap-2 justify-end">
292
- <button type="button" @click="cancelEdit"
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">
294
- Cancel
295
- </button>
296
- <button type="button" @click="saveEditedMessage"
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">
298
- Save
299
- </button>
300
- </div>
301
- </div>
302
- </div>
303
304
  </div>
304
305
 
305
- <!-- Input Area - only show when thread is selected -->
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">
307
- <ChatPrompt :model="selectedModel" :systemPrompt="currentSystemPrompt" />
306
+ <!-- Input Area -->
307
+ <div class="flex-shrink-0 border-t border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 px-6 py-4">
308
+ <ChatPrompt :model="selectedModelObj" :systemPrompt="currentSystemPrompt" />
309
+ </div>
310
+
311
+ <!-- Lightbox -->
312
+ <div v-if="lightboxUrl" class="fixed inset-0 z-[100] bg-black/90 flex items-center justify-center p-4 cursor-pointer" @click="closeLightbox">
313
+ <div class="relative max-w-full max-h-full">
314
+ <img :src="lightboxUrl" class="max-w-full max-h-[90vh] object-contain rounded-sm shadow-2xl" @click.stop />
315
+ <button type="button" @click="closeLightbox"
316
+ class="absolute -top-12 right-0 text-white/70 hover:text-white p-2 rounded-full bg-white/10 hover:bg-white/20 transition-colors"
317
+ title="Close">
318
+ <svg class="size-8" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
319
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
320
+ </svg>
321
+ </button>
322
+ </div>
308
323
  </div>
309
324
  </div>
310
325
  `,
@@ -318,7 +333,7 @@ export default {
318
333
  const { currentThread } = threads
319
334
  const chatPrompt = useChatPrompt()
320
335
  const chatSettings = useSettings()
321
- const {
336
+ const {
322
337
  errorStatus,
323
338
  isGenerating,
324
339
  } = chatPrompt
@@ -340,6 +355,10 @@ export default {
340
355
  const prompts = computed(() => [customPrompt, ...config.prompts])
341
356
 
342
357
  const selectedModel = ref(prefs.model || config.defaults.text.model || '')
358
+ const selectedModelObj = computed(() => {
359
+ if (!selectedModel.value || !models) return null
360
+ return models.find(m => m.name === selectedModel.value) || models.find(m => m.id === selectedModel.value)
361
+ })
343
362
  const selectedPrompt = ref(prefs.systemPrompt || null)
344
363
  const currentSystemPrompt = ref('')
345
364
  const showSystemPrompt = ref(false)
@@ -348,9 +367,14 @@ export default {
348
367
  const isImporting = ref(false)
349
368
  const fileInput = ref(null)
350
369
  const copying = ref(null)
351
- const editingMessageId = ref(null)
352
- const editingMessageContent = ref('')
353
- const editingMessage = ref(null)
370
+ const lightboxUrl = ref(null)
371
+
372
+ const openLightbox = (url) => {
373
+ lightboxUrl.value = url
374
+ }
375
+ const closeLightbox = () => {
376
+ lightboxUrl.value = null
377
+ }
354
378
 
355
379
  // Auto-scroll to bottom when new messages arrive
356
380
  const scrollToBottom = async () => {
@@ -403,7 +427,7 @@ export default {
403
427
  // If using a custom prompt, keep whatever is already in currentSystemPrompt
404
428
  if (newPrompt && newPrompt.id === '_custom_') return
405
429
  const prompt = newPrompt && config.prompts.find(p => p.id === newPrompt.id)
406
- currentSystemPrompt.value = prompt ? prompt.value.replace(/\n/g,' ') : ''
430
+ currentSystemPrompt.value = prompt ? prompt.value.replace(/\n/g, ' ') : ''
407
431
  }, { immediate: true })
408
432
 
409
433
  watch(() => [selectedModel.value, selectedPrompt.value], () => {
@@ -504,8 +528,8 @@ export default {
504
528
  try {
505
529
  const text = await file.text()
506
530
  const importData = JSON.parse(text)
507
- importType = importData.threads
508
- ? 'threads'
531
+ importType = importData.threads
532
+ ? 'threads'
509
533
  : importData.requests
510
534
  ? 'requests'
511
535
  : 'unknown'
@@ -626,17 +650,38 @@ export default {
626
650
  }
627
651
  const formatReasoning = (r) => typeof r === 'string' ? r : JSON.stringify(r, null, 2)
628
652
 
629
- // Copy message content to clipboard
630
653
  const copyMessageContent = async (message) => {
654
+ let content = ''
655
+ if (Array.isArray(message.content)) {
656
+ content = message.content.map(part => {
657
+ if (part.type === 'text') return part.text
658
+ if (part.type === 'image_url') {
659
+ const name = part.image_url.url.split('/').pop() || 'image'
660
+ return `\n![${name}](${part.image_url.url})\n`
661
+ }
662
+ if (part.type === 'input_audio') {
663
+ const name = part.input_audio.data.split('/').pop() || 'audio'
664
+ return `\n[${name}](${part.input_audio.data})\n`
665
+ }
666
+ if (part.type === 'file') {
667
+ const name = part.file.filename || part.file.file_data.split('/').pop() || 'file'
668
+ return `\n[${name}](${part.file.file_data})`
669
+ }
670
+ return ''
671
+ }).join('\n')
672
+ } else {
673
+ content = message.content
674
+ }
675
+
631
676
  try {
632
677
  copying.value = message
633
- await navigator.clipboard.writeText(message.content)
678
+ await navigator.clipboard.writeText(content)
634
679
  // Could add a toast notification here if desired
635
680
  } catch (err) {
636
681
  console.error('Failed to copy message content:', err)
637
682
  // Fallback for older browsers
638
683
  const textArea = document.createElement('textarea')
639
- textArea.value = message.content
684
+ textArea.value = content
640
685
  document.body.appendChild(textArea)
641
686
  textArea.select()
642
687
  document.execCommand('copy')
@@ -645,7 +690,57 @@ export default {
645
690
  setTimeout(() => { copying.value = null }, 2000)
646
691
  }
647
692
 
648
- // Redo a user message (clear all messages after it and re-run)
693
+ const getAttachments = (message) => {
694
+ if (!Array.isArray(message.content)) return []
695
+ return message.content.filter(c => c.type === 'image_url' || c.type === 'input_audio' || c.type === 'file')
696
+ }
697
+ const hasAttachments = (message) => getAttachments(message).length > 0
698
+
699
+ // Helper to extract content and files from message
700
+ const extractMessageState = async (message) => {
701
+ let text = ''
702
+ let files = []
703
+ const getCacheInfos = []
704
+
705
+ if (Array.isArray(message.content)) {
706
+ for (const part of message.content) {
707
+ if (part.type === 'text') {
708
+ text += part.text
709
+ } else if (part.type === 'image_url') {
710
+ const url = part.image_url.url
711
+ const name = url.split('/').pop() || 'image'
712
+ files.push({ name, url, type: 'image/png' }) // Assume image
713
+ getCacheInfos.push(url)
714
+ } else if (part.type === 'input_audio') {
715
+ const url = part.input_audio.data
716
+ const name = url.split('/').pop() || 'audio'
717
+ files.push({ name, url, type: 'audio/wav' }) // Assume audio
718
+ getCacheInfos.push(url)
719
+ } else if (part.type === 'file') {
720
+ const url = part.file.file_data
721
+ const name = part.file.filename || url.split('/').pop() || 'file'
722
+ files.push({ name, url })
723
+ getCacheInfos.push(url)
724
+ }
725
+ }
726
+ } else {
727
+ text = message.content
728
+ }
729
+
730
+ const infos = await fetchCacheInfos(getCacheInfos)
731
+ // replace name with info.name
732
+ for (let i = 0; i < files.length; i++) {
733
+ const url = files[i]?.url
734
+ const info = infos[url]
735
+ if (info) {
736
+ files[i].name = info.name
737
+ }
738
+ }
739
+
740
+ return { text, files }
741
+ }
742
+
743
+ // Redo a user message (clear all messages after this one and re-run)
649
744
  const redoMessage = async (message) => {
650
745
  if (!currentThread.value || message.role !== 'user') return
651
746
 
@@ -655,16 +750,13 @@ export default {
655
750
  // Clear all messages after this one
656
751
  await threads.redoMessageFromThread(threadId, message.id)
657
752
 
658
- // Extract the actual message content (remove media indicators if present)
659
- let messageContent = message.content
660
- // Remove media indicators like [🖼️ filename] or [🔉 filename] or [📎 filename]
661
- messageContent = messageContent.replace(/\n\n\[[🖼️🔉📎] [^\]]+\]$/, '')
753
+ const state = await extractMessageState(message)
662
754
 
663
755
  // Set the message text in the chat prompt
664
- chatPrompt.messageText.value = messageContent
756
+ chatPrompt.messageText.value = state.text
665
757
 
666
- // Clear any attached files since we're re-running
667
- chatPrompt.attachedFiles.value = []
758
+ // Restore attached files
759
+ chatPrompt.attachedFiles.value = state.files
668
760
 
669
761
  // Trigger send by simulating the send action
670
762
  // We'll use a small delay to ensure the UI updates
@@ -686,68 +778,24 @@ export default {
686
778
  }
687
779
 
688
780
  // Edit a user message
689
- const editMessage = (message) => {
781
+ const editMessage = async (message) => {
690
782
  if (!currentThread.value || message.role !== 'user') return
691
783
 
692
- editingMessage.value = message
693
- editingMessageId.value = message.id
694
- // Extract the actual message content (remove media indicators if present)
695
- let messageContent = message.content
696
- messageContent = messageContent.replace(/\n\n\[[🖼️🔉📎] [^\]]+\]$/, '')
697
- editingMessageContent.value = messageContent
698
- }
699
-
700
- // Save edited message
701
- const saveEditedMessage = async () => {
702
- if (!currentThread.value || !editingMessage.value || !editingMessageContent.value.trim()) return
703
-
704
- try {
705
- const threadId = currentThread.value.id
706
- const messageId = editingMessage.value.id
707
- const updatedContent = editingMessageContent.value
708
-
709
- // Update the message content
710
- editingMessage.value.content = updatedContent
711
- await threads.updateMessageInThread(threadId, messageId, { content: updatedContent })
712
-
713
- // Clear editing state
714
- editingMessageId.value = null
715
- editingMessageContent.value = ''
716
- editingMessage.value = null
717
-
718
- // Now redo the message (clear all responses after it and re-run)
719
- await nextTick()
720
- await threads.redoMessageFromThread(threadId, messageId)
721
-
722
- // Set the message text in the chat prompt
723
- chatPrompt.messageText.value = updatedContent
724
-
725
- // Clear any attached files since we're re-running
726
- chatPrompt.attachedFiles.value = []
727
-
728
- // Trigger send by simulating the send action
729
- await nextTick()
730
-
731
- // Find the send button and click it
732
- const sendButton = document.querySelector('button[title*="Send"]')
733
- if (sendButton && !sendButton.disabled) {
734
- sendButton.click()
784
+ // set the message in the input box
785
+ const state = await extractMessageState(message)
786
+ chatPrompt.messageText.value = state.text
787
+ chatPrompt.attachedFiles.value = state.files
788
+ chatPrompt.editingMessageId.value = message.id
789
+
790
+ // Focus the textarea
791
+ nextTick(() => {
792
+ const textarea = document.querySelector('textarea')
793
+ if (textarea) {
794
+ textarea.focus()
795
+ // Set cursor to end
796
+ textarea.selectionStart = textarea.selectionEnd = textarea.value.length
735
797
  }
736
- } catch (error) {
737
- console.error('Failed to save edited message:', error)
738
- errorStatus.value = {
739
- errorCode: 'Error',
740
- message: 'Failed to save edited message: ' + error.message,
741
- stackTrace: null
742
- }
743
- }
744
- }
745
-
746
- // Cancel editing
747
- const cancelEdit = () => {
748
- editingMessageId.value = null
749
- editingMessageContent.value = ''
750
- editingMessage.value = null
798
+ })
751
799
  }
752
800
 
753
801
  // Cancel pending request
@@ -785,15 +833,13 @@ export default {
785
833
  customPromptValue,
786
834
  currentThread,
787
835
  selectedModel,
836
+ selectedModelObj,
788
837
  selectedPrompt,
789
838
  currentSystemPrompt,
790
839
  showSystemPrompt,
791
840
  messagesContainer,
792
841
  errorStatus,
793
842
  copying,
794
- editingMessageId,
795
- editingMessageContent,
796
- editingMessage,
797
843
  formatTime,
798
844
  renderMarkdown,
799
845
  isReasoningExpanded,
@@ -802,8 +848,6 @@ export default {
802
848
  copyMessageContent,
803
849
  redoMessage,
804
850
  editMessage,
805
- saveEditedMessage,
806
- cancelEdit,
807
851
  cancelRequest,
808
852
  configUpdated,
809
853
  exportThreads,
@@ -817,7 +861,13 @@ export default {
817
861
  humanifyMs,
818
862
  humanifyNumber,
819
863
  formatCost,
864
+ formatCost,
820
865
  statsTitle,
866
+ getAttachments,
867
+ hasAttachments,
868
+ lightboxUrl,
869
+ openLightbox,
870
+ closeLightbox,
821
871
  }
822
872
  }
823
873
  }