llms-py 2.0.8__py3-none-any.whl → 2.0.10__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.
Files changed (43) hide show
  1. llms.py +144 -13
  2. llms_py-2.0.10.data/data/index.html +80 -0
  3. {llms_py-2.0.8.data → llms_py-2.0.10.data}/data/llms.json +16 -10
  4. llms_py-2.0.10.data/data/ui/Avatar.mjs +28 -0
  5. llms_py-2.0.10.data/data/ui/Brand.mjs +23 -0
  6. {llms_py-2.0.8.data → llms_py-2.0.10.data}/data/ui/ChatPrompt.mjs +101 -69
  7. {llms_py-2.0.8.data → llms_py-2.0.10.data}/data/ui/Main.mjs +43 -183
  8. llms_py-2.0.10.data/data/ui/ModelSelector.mjs +29 -0
  9. llms_py-2.0.10.data/data/ui/ProviderStatus.mjs +105 -0
  10. {llms_py-2.0.8.data → llms_py-2.0.10.data}/data/ui/Recents.mjs +2 -1
  11. llms_py-2.0.10.data/data/ui/SettingsDialog.mjs +374 -0
  12. {llms_py-2.0.8.data → llms_py-2.0.10.data}/data/ui/Sidebar.mjs +11 -27
  13. llms_py-2.0.10.data/data/ui/SignIn.mjs +64 -0
  14. llms_py-2.0.10.data/data/ui/SystemPromptEditor.mjs +31 -0
  15. llms_py-2.0.10.data/data/ui/SystemPromptSelector.mjs +36 -0
  16. llms_py-2.0.10.data/data/ui/Welcome.mjs +8 -0
  17. llms_py-2.0.10.data/data/ui/ai.mjs +80 -0
  18. {llms_py-2.0.8.data → llms_py-2.0.10.data}/data/ui/app.css +76 -10
  19. llms_py-2.0.10.data/data/ui/lib/servicestack-vue.mjs +37 -0
  20. {llms_py-2.0.8.data → llms_py-2.0.10.data}/data/ui/markdown.mjs +9 -2
  21. {llms_py-2.0.8.data → llms_py-2.0.10.data}/data/ui/tailwind.input.css +13 -4
  22. {llms_py-2.0.8.data → llms_py-2.0.10.data}/data/ui/threadStore.mjs +2 -2
  23. {llms_py-2.0.8.data → llms_py-2.0.10.data}/data/ui/typography.css +109 -1
  24. {llms_py-2.0.8.data → llms_py-2.0.10.data}/data/ui/utils.mjs +8 -2
  25. {llms_py-2.0.8.dist-info → llms_py-2.0.10.dist-info}/METADATA +124 -39
  26. llms_py-2.0.10.dist-info/RECORD +40 -0
  27. llms_py-2.0.8.data/data/index.html +0 -64
  28. llms_py-2.0.8.data/data/ui/lib/servicestack-vue.min.mjs +0 -37
  29. llms_py-2.0.8.dist-info/RECORD +0 -30
  30. {llms_py-2.0.8.data → llms_py-2.0.10.data}/data/requirements.txt +0 -0
  31. {llms_py-2.0.8.data → llms_py-2.0.10.data}/data/ui/App.mjs +0 -0
  32. {llms_py-2.0.8.data → llms_py-2.0.10.data}/data/ui/fav.svg +0 -0
  33. {llms_py-2.0.8.data → llms_py-2.0.10.data}/data/ui/lib/highlight.min.mjs +0 -0
  34. {llms_py-2.0.8.data → llms_py-2.0.10.data}/data/ui/lib/idb.min.mjs +0 -0
  35. {llms_py-2.0.8.data → llms_py-2.0.10.data}/data/ui/lib/marked.min.mjs +0 -0
  36. /llms_py-2.0.8.data/data/ui/lib/servicestack-client.min.mjs → /llms_py-2.0.10.data/data/ui/lib/servicestack-client.mjs +0 -0
  37. {llms_py-2.0.8.data → llms_py-2.0.10.data}/data/ui/lib/vue-router.min.mjs +0 -0
  38. {llms_py-2.0.8.data → llms_py-2.0.10.data}/data/ui/lib/vue.min.mjs +0 -0
  39. {llms_py-2.0.8.data → llms_py-2.0.10.data}/data/ui.json +0 -0
  40. {llms_py-2.0.8.dist-info → llms_py-2.0.10.dist-info}/WHEEL +0 -0
  41. {llms_py-2.0.8.dist-info → llms_py-2.0.10.dist-info}/entry_points.txt +0 -0
  42. {llms_py-2.0.8.dist-info → llms_py-2.0.10.dist-info}/licenses/LICENSE +0 -0
  43. {llms_py-2.0.8.dist-info → llms_py-2.0.10.dist-info}/top_level.txt +0 -0
@@ -11,7 +11,6 @@ export function useChatPrompt() {
11
11
  const attachedFiles = ref([])
12
12
  const isGenerating = ref(false)
13
13
  const errorStatus = ref(null)
14
- const errorMessage = ref(null)
15
14
  const hasImage = () => attachedFiles.value.some(f => imageExts.includes(lastRightPart(f.name, '.')))
16
15
  const hasAudio = () => attachedFiles.value.some(f => audioExts.includes(lastRightPart(f.name, '.')))
17
16
  const hasFile = () => attachedFiles.value.length > 0
@@ -28,7 +27,6 @@ export function useChatPrompt() {
28
27
  messageText,
29
28
  attachedFiles,
30
29
  errorStatus,
31
- errorMessage,
32
30
  isGenerating,
33
31
  get generating() {
34
32
  return isGenerating.value
@@ -44,23 +42,32 @@ export function useChatPrompt() {
44
42
  export default {
45
43
  template:`
46
44
  <div class="mx-auto max-w-3xl">
47
- <div class="flex space-x-3">
48
- <!-- Attach (+) button -->
49
- <div>
50
- <button type="button"
51
- @click="triggerFilePicker"
45
+ <SettingsDialog :isOpen="showSettings" @close="showSettings = false" />
46
+ <div class="flex space-x-2">
47
+ <!-- Attach (+) button and Settings button -->
48
+ <div class="mt-1.5 flex flex-col space-y-1 items-center">
49
+ <div>
50
+ <button type="button"
51
+ @click="triggerFilePicker"
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"
54
+ title="Attach image or audio">
55
+ <svg class="size-5" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 256 256">
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>
57
+ </svg>
58
+ </button>
59
+ <!-- Hidden file input -->
60
+ <input ref="fileInput" type="file" multiple @change="onFilesSelected"
61
+ class="hidden" accept="image/*,audio/*,.pdf,.doc,.docx,.xml,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document"
62
+ />
63
+ </div>
64
+ <div>
65
+ <button type="button" title="Settings" @click="showSettings = true"
52
66
  :disabled="isGenerating || !model"
53
- class="mt-2 h-10 w-10 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"
54
- title="Attach image or audio">
55
- <svg class="w-5 h-5" 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">
56
- <line x1="12" y1="5" x2="12" y2="19"></line>
57
- <line x1="5" y1="12" x2="19" y2="12"></line>
58
- </svg>
59
- </button>
60
- <!-- Hidden file input -->
61
- <input ref="fileInput" type="file" multiple @change="onFilesSelected"
62
- class="hidden" accept="image/*,audio/*,.pdf,.doc,.docx,.xml,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document"
63
- />
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>
69
+ </button>
70
+ </div>
64
71
  </div>
65
72
 
66
73
  <div class="flex-1">
@@ -70,7 +77,7 @@ export default {
70
77
  @keydown.enter.exact.prevent="sendMessage"
71
78
  @keydown.enter.shift.exact="addNewLine"
72
79
  placeholder="Type your message... (Enter to send, Shift+Enter for new line)"
73
- rows="2"
80
+ rows="3"
74
81
  class="block 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"
75
82
  :disabled="isGenerating || !model"
76
83
  ></textarea>
@@ -90,16 +97,16 @@ export default {
90
97
  </div>
91
98
  </div>
92
99
 
93
- <div>
100
+ <div class="pt-3">
94
101
  <button title="Send (Enter)" type="button"
95
102
  @click="sendMessage"
96
103
  :disabled="!messageText.trim() || isGenerating || !model"
97
- class="mt-2 p-2 flex items-center justify-center rounded-full bg-gray-700 text-white transition-colors hover:opacity-70 focus-visible:outline-none focus-visible:outline-black disabled:bg-[#D7D7D7] disabled:text-[#f4f4f4] disabled:hover:opacity-100 dark:bg-white dark:text-black dark:focus-visible:outline-white disabled:dark:bg-token-text-quaternary dark:disabled:text-token-main-surface-secondary">
98
- <svg v-if="isGenerating" class="size-6 animate-spin" fill="none" viewBox="0 0 24 24">
104
+ class="p-2 flex items-center justify-center rounded-full bg-gray-700 text-white transition-colors hover:opacity-70 focus-visible:outline-none focus-visible:outline-black disabled:bg-[#D7D7D7] disabled:text-[#f4f4f4] disabled:hover:opacity-100 dark:bg-white dark:text-black dark:focus-visible:outline-white disabled:dark:bg-token-text-quaternary dark:disabled:text-token-main-surface-secondary">
105
+ <svg v-if="isGenerating" class="size-8 animate-spin" fill="none" viewBox="0 0 24 24">
99
106
  <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
100
107
  <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>
101
108
  </svg>
102
- <svg v-else class="size-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="m3.165 19.503l7.362-16.51c.59-1.324 2.355-1.324 2.946 0l7.362 16.51c.667 1.495-.814 3.047-2.202 2.306l-5.904-3.152c-.459-.245-1-.245-1.458 0l-5.904 3.152c-1.388.74-2.87-.81-2.202-2.306"></path></svg>
109
+ <svg v-else class="size-8" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path stroke-dasharray="20" stroke-dashoffset="20" d="M12 21l0 -17.5"><animate fill="freeze" attributeName="stroke-dashoffset" dur="0.2s" values="20;0"/></path><path stroke-dasharray="12" stroke-dashoffset="12" d="M12 3l7 7M12 3l-7 7"><animate fill="freeze" attributeName="stroke-dashoffset" begin="0.2s" dur="0.2s" values="12;0"/></path></g></svg>
103
110
  </button>
104
111
  </div>
105
112
  </div>
@@ -116,18 +123,19 @@ export default {
116
123
  }
117
124
  },
118
125
  setup(props) {
126
+ const ai = inject('ai')
127
+ const chatSettings = inject('chatSettings')
119
128
  const router = useRouter()
120
129
  const config = inject('config')
121
130
  const chatPrompt = inject('chatPrompt')
122
- const {
123
- messageText,
124
- attachedFiles,
125
- isGenerating,
131
+ const {
132
+ messageText,
133
+ attachedFiles,
134
+ isGenerating,
126
135
  errorStatus,
127
- errorMessage,
128
- hasImage,
129
- hasAudio,
130
- hasFile
136
+ hasImage,
137
+ hasAudio,
138
+ hasFile
131
139
  } = chatPrompt
132
140
  const threads = inject('threads')
133
141
  const {
@@ -135,6 +143,8 @@ export default {
135
143
  } = threads
136
144
 
137
145
  const fileInput = ref(null)
146
+ const showSettings = ref(false)
147
+ const { applySettings } = chatSettings
138
148
 
139
149
  // File attachments (+) handlers
140
150
  const triggerFilePicker = () => {
@@ -185,7 +195,7 @@ export default {
185
195
  if (!messageText.value.trim() || isGenerating.value || !props.model) return
186
196
 
187
197
  // Clear any existing error message
188
- errorStatus.value = errorMessage.value = null
198
+ errorStatus.value = null
189
199
 
190
200
  let message = messageText.value.trim()
191
201
  if (attachedFiles.value.length) {
@@ -207,7 +217,7 @@ export default {
207
217
  const newThread = await threads.createThread('New Chat', props.model, props.systemPrompt)
208
218
  threadId = newThread.id
209
219
  // Navigate to the new thread URL
210
- router.push(`/c/${newThread.id}`)
220
+ router.push(`${ai.base}/c/${newThread.id}`)
211
221
  } else {
212
222
  threadId = currentThread.value.id
213
223
  // Update the existing thread's model and systemPrompt to match current selection
@@ -233,14 +243,19 @@ export default {
233
243
  if (props.systemPrompt?.trim()) {
234
244
  messages.unshift({
235
245
  role: 'system',
236
- content: props.systemPrompt.trim()
246
+ content: [
247
+ { type: 'text', text: props.systemPrompt }
248
+ ]
237
249
  })
238
250
  }
239
251
 
240
252
  const chatRequest = createChatRequest()
241
253
  chatRequest.model = props.model
242
254
 
243
- console.log('chatRequest', chatRequest, hasImage(), hasAudio(), attachedFiles.value.length, attachedFiles.value)
255
+ // Apply user settings
256
+ applySettings(chatRequest)
257
+
258
+ console.debug('chatRequest', chatRequest, hasImage(), hasAudio(), attachedFiles.value.length, attachedFiles.value)
244
259
 
245
260
  function setContentText(chatRequest, text) {
246
261
  // Replace text message
@@ -256,7 +271,7 @@ export default {
256
271
  if (hasImage()) {
257
272
  const imageMessage = chatRequest.messages.find(m =>
258
273
  m.role === 'user' && Array.isArray(m.content) && m.content.some(c => c.type === 'image_url'))
259
- console.log('hasImage', chatRequest, imageMessage)
274
+ console.debug('hasImage', chatRequest, imageMessage)
260
275
  if (imageMessage) {
261
276
  const imgs = []
262
277
  let imagePart = deepClone(imageMessage.content.find(c => c.type === 'image_url'))
@@ -272,7 +287,7 @@ export default {
272
287
  }
273
288
 
274
289
  } else if (hasAudio()) {
275
- console.log('hasAudio', chatRequest)
290
+ console.debug('hasAudio', chatRequest)
276
291
  const audioMessage = chatRequest.messages.find(m =>
277
292
  m.role === 'user' && Array.isArray(m.content) && m.content.some(c => c.type === 'input_audio'))
278
293
  if (audioMessage) {
@@ -289,7 +304,7 @@ export default {
289
304
  setContentText(chatRequest, message)
290
305
  }
291
306
  } else if (attachedFiles.value.length) {
292
- console.log('hasFile', chatRequest)
307
+ console.debug('hasFile', chatRequest)
293
308
  const fileMessage = chatRequest.messages.find(m =>
294
309
  m.role === 'user' && Array.isArray(m.content) && m.content.some(c => c.type === 'file'))
295
310
  if (fileMessage) {
@@ -306,62 +321,80 @@ export default {
306
321
  }
307
322
 
308
323
  } else {
309
- console.log('hasText', chatRequest)
324
+ console.debug('hasText', chatRequest)
310
325
  // Chat template message needs to be empty
311
326
  chatRequest.messages = []
312
327
  messages.forEach(m => chatRequest.messages.push({
313
328
  role: m.role,
314
- content: m.content
329
+ content: typeof m.content === 'string'
330
+ ? [{ type: 'text', text: m.content }]
331
+ : m.content
315
332
  }))
316
333
  }
317
334
 
318
335
  // Send to API
319
- const response = await fetch('/v1/chat/completions', {
320
- method: 'POST',
321
- headers: {
322
- 'Content-Type': 'application/json'
323
- },
336
+ console.debug('chatRequest', chatRequest)
337
+ const response = await ai.post('/v1/chat/completions', {
324
338
  body: JSON.stringify(chatRequest)
325
339
  })
326
340
 
341
+ let result = null
327
342
  if (!response.ok) {
328
- errorStatus.value = `HTTP ${response.status} ${response.statusText}`
329
- let errorBody = ''
343
+ errorStatus.value = {
344
+ errorCode: `HTTP ${response.status} ${response.statusText}`,
345
+ message: null,
346
+ stackTrace: null
347
+ }
348
+ let errorBody = null
330
349
  try {
331
350
  errorBody = await response.text()
332
351
  if (errorBody) {
333
352
  // Try to parse as JSON for better formatting
334
353
  try {
335
354
  const errorJson = JSON.parse(errorBody)
336
- errorBody = JSON.stringify(errorJson, null, 2)
355
+ const status = errorJson?.responseStatus
356
+ if (status) {
357
+ errorStatus.value.errorCode += ` ${status.errorCode}`
358
+ errorStatus.value.message = status.message
359
+ errorStatus.value.stackTrace = status.stackTrace
360
+ } else {
361
+ errorStatus.value.stackTrace = JSON.stringify(errorJson, null, 2)
362
+ }
337
363
  } catch (e) {
338
364
  }
339
365
  }
340
366
  } catch (e) {
341
367
  // If we can't read the response body, just use the status
342
368
  }
343
- throw new Error(errorBody || '')
369
+ } else {
370
+ try {
371
+ result = await response.json()
372
+ } catch (e) {
373
+ errorStatus.value = {
374
+ errorCode: 'Error',
375
+ message: e.message,
376
+ stackTrace: null
377
+ }
378
+ }
344
379
  }
345
380
 
346
- const result = await response.json()
347
-
348
- if (result.error) {
349
- throw new Error(result.error)
381
+ if (result?.error) {
382
+ errorStatus.value ??= {
383
+ errorCode: 'Error',
384
+ }
385
+ errorStatus.value.message = result.error
386
+ }
387
+
388
+ if (!errorStatus.value) {
389
+ // Add assistant response (save entire message including reasoning)
390
+ const assistantMessage = result.choices?.[0]?.message
391
+ await threads.addMessageToThread(threadId, assistantMessage)
392
+
393
+ nextTick(addCopyButtons)
394
+
395
+ attachedFiles.value = []
396
+ // Error will be cleared when user sends next message (no auto-timeout)
350
397
  }
351
-
352
- // Add assistant response (save entire message including reasoning)
353
- const assistantMessage = result.choices?.[0]?.message
354
- await threads.addMessageToThread(threadId, assistantMessage)
355
-
356
- nextTick(addCopyButtons)
357
-
358
- attachedFiles.value = []
359
-
360
- } catch (error) {
361
- console.error('Error sending message:', error)
362
- errorMessage.value = error.message
363
-
364
- // Error will be cleared when user sends next message (no auto-timeout)
365
398
  } finally {
366
399
  isGenerating.value = false
367
400
  }
@@ -375,10 +408,9 @@ export default {
375
408
  return {
376
409
  isGenerating,
377
410
  attachedFiles,
378
- errorStatus,
379
- errorMessage,
380
411
  messageText,
381
412
  fileInput,
413
+ showSettings,
382
414
  triggerFilePicker,
383
415
  onFilesSelected,
384
416
  removeAttachment,