llms-py 2.0.28__tar.gz → 2.0.29__tar.gz

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 (58) hide show
  1. {llms_py-2.0.28/llms_py.egg-info → llms_py-2.0.29}/PKG-INFO +3 -3
  2. {llms_py-2.0.28 → llms_py-2.0.29}/README.md +2 -2
  3. {llms_py-2.0.28 → llms_py-2.0.29}/llms/main.py +70 -3
  4. {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui/ChatPrompt.mjs +46 -6
  5. {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui/Main.mjs +14 -1
  6. {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui/ModelSelector.mjs +20 -2
  7. {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui/SystemPromptSelector.mjs +21 -1
  8. {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui/ai.mjs +1 -1
  9. {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui/app.css +16 -0
  10. llms_py-2.0.29/llms/ui/lib/servicestack-vue.mjs +37 -0
  11. {llms_py-2.0.28 → llms_py-2.0.29/llms_py.egg-info}/PKG-INFO +3 -3
  12. {llms_py-2.0.28 → llms_py-2.0.29}/pyproject.toml +1 -1
  13. {llms_py-2.0.28 → llms_py-2.0.29}/setup.py +1 -1
  14. llms_py-2.0.28/llms/ui/lib/servicestack-vue.mjs +0 -37
  15. {llms_py-2.0.28 → llms_py-2.0.29}/LICENSE +0 -0
  16. {llms_py-2.0.28 → llms_py-2.0.29}/MANIFEST.in +0 -0
  17. {llms_py-2.0.28 → llms_py-2.0.29}/llms/__init__.py +0 -0
  18. {llms_py-2.0.28 → llms_py-2.0.29}/llms/__main__.py +0 -0
  19. {llms_py-2.0.28 → llms_py-2.0.29}/llms/index.html +0 -0
  20. {llms_py-2.0.28 → llms_py-2.0.29}/llms/llms.json +0 -0
  21. {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui/Analytics.mjs +0 -0
  22. {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui/App.mjs +0 -0
  23. {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui/Avatar.mjs +0 -0
  24. {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui/Brand.mjs +0 -0
  25. {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui/OAuthSignIn.mjs +0 -0
  26. {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui/ProviderIcon.mjs +0 -0
  27. {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui/ProviderStatus.mjs +0 -0
  28. {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui/Recents.mjs +0 -0
  29. {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui/SettingsDialog.mjs +0 -0
  30. {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui/Sidebar.mjs +0 -0
  31. {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui/SignIn.mjs +0 -0
  32. {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui/SystemPromptEditor.mjs +0 -0
  33. {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui/Welcome.mjs +0 -0
  34. {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui/fav.svg +0 -0
  35. {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui/lib/chart.js +0 -0
  36. {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui/lib/charts.mjs +0 -0
  37. {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui/lib/color.js +0 -0
  38. {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui/lib/highlight.min.mjs +0 -0
  39. {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui/lib/idb.min.mjs +0 -0
  40. {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui/lib/marked.min.mjs +0 -0
  41. {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui/lib/servicestack-client.mjs +0 -0
  42. {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui/lib/vue-router.min.mjs +0 -0
  43. {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui/lib/vue.min.mjs +0 -0
  44. {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui/lib/vue.mjs +0 -0
  45. {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui/markdown.mjs +0 -0
  46. {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui/tailwind.input.css +0 -0
  47. {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui/threadStore.mjs +0 -0
  48. {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui/typography.css +0 -0
  49. {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui/utils.mjs +0 -0
  50. {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui.json +0 -0
  51. {llms_py-2.0.28 → llms_py-2.0.29}/llms_py.egg-info/SOURCES.txt +0 -0
  52. {llms_py-2.0.28 → llms_py-2.0.29}/llms_py.egg-info/dependency_links.txt +0 -0
  53. {llms_py-2.0.28 → llms_py-2.0.29}/llms_py.egg-info/entry_points.txt +0 -0
  54. {llms_py-2.0.28 → llms_py-2.0.29}/llms_py.egg-info/not-zip-safe +0 -0
  55. {llms_py-2.0.28 → llms_py-2.0.29}/llms_py.egg-info/requires.txt +0 -0
  56. {llms_py-2.0.28 → llms_py-2.0.29}/llms_py.egg-info/top_level.txt +0 -0
  57. {llms_py-2.0.28 → llms_py-2.0.29}/requirements.txt +0 -0
  58. {llms_py-2.0.28 → llms_py-2.0.29}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: llms-py
3
- Version: 2.0.28
3
+ Version: 2.0.29
4
4
  Summary: A lightweight CLI tool and OpenAI-compatible server for querying multiple Large Language Model (LLM) providers
5
5
  Home-page: https://github.com/ServiceStack/llms
6
6
  Author: ServiceStack
@@ -73,7 +73,7 @@ Access all your local all remote LLMs with a single ChatGPT-like UI:
73
73
 
74
74
  #### Dark Mode Support
75
75
 
76
- [![](https://servicestack.net/img/posts/llms-py-ui/dark-attach-image.webp)](https://servicestack.net/posts/llms-py-ui)
76
+ [![](https://servicestack.net/img/posts/llms-py-ui/dark-attach-image.webp?)](https://servicestack.net/posts/llms-py-ui)
77
77
 
78
78
  #### Monthly Costs Analysis
79
79
 
@@ -81,7 +81,7 @@ Access all your local all remote LLMs with a single ChatGPT-like UI:
81
81
 
82
82
  #### Monthly Token Usage (Dark Mode)
83
83
 
84
- [![](https://servicestack.net/img/posts/llms-py-ui/dark-analytics-tokens.webp)](https://servicestack.net/posts/llms-py-ui)
84
+ [![](https://servicestack.net/img/posts/llms-py-ui/dark-analytics-tokens.webp?)](https://servicestack.net/posts/llms-py-ui)
85
85
 
86
86
  #### Monthly Activity Log
87
87
 
@@ -33,7 +33,7 @@ Access all your local all remote LLMs with a single ChatGPT-like UI:
33
33
 
34
34
  #### Dark Mode Support
35
35
 
36
- [![](https://servicestack.net/img/posts/llms-py-ui/dark-attach-image.webp)](https://servicestack.net/posts/llms-py-ui)
36
+ [![](https://servicestack.net/img/posts/llms-py-ui/dark-attach-image.webp?)](https://servicestack.net/posts/llms-py-ui)
37
37
 
38
38
  #### Monthly Costs Analysis
39
39
 
@@ -41,7 +41,7 @@ Access all your local all remote LLMs with a single ChatGPT-like UI:
41
41
 
42
42
  #### Monthly Token Usage (Dark Mode)
43
43
 
44
- [![](https://servicestack.net/img/posts/llms-py-ui/dark-analytics-tokens.webp)](https://servicestack.net/posts/llms-py-ui)
44
+ [![](https://servicestack.net/img/posts/llms-py-ui/dark-analytics-tokens.webp?)](https://servicestack.net/posts/llms-py-ui)
45
45
 
46
46
  #### Monthly Activity Log
47
47
 
@@ -31,7 +31,7 @@ try:
31
31
  except ImportError:
32
32
  HAS_PIL = False
33
33
 
34
- VERSION = "2.0.28"
34
+ VERSION = "2.0.29"
35
35
  _ROOT = None
36
36
  g_config_path = None
37
37
  g_ui_path = None
@@ -1405,6 +1405,66 @@ async def save_home_configs():
1405
1405
  print("Could not create llms.json. Create one with --init or use --config <path>")
1406
1406
  exit(1)
1407
1407
 
1408
+ async def reload_providers():
1409
+ global g_config, g_handlers
1410
+ g_handlers = init_llms(g_config)
1411
+ await load_llms()
1412
+ _log(f"{len(g_handlers)} providers loaded")
1413
+ return g_handlers
1414
+
1415
+ async def watch_config_files(config_path, ui_path, interval=1):
1416
+ """Watch config files and reload providers when they change"""
1417
+ global g_config
1418
+
1419
+ config_path = Path(config_path)
1420
+ ui_path = Path(ui_path) if ui_path else None
1421
+
1422
+ file_mtimes = {}
1423
+
1424
+ _log(f"Watching config files: {config_path}" + (f", {ui_path}" if ui_path else ""))
1425
+
1426
+ while True:
1427
+ await asyncio.sleep(interval)
1428
+
1429
+ # Check llms.json
1430
+ try:
1431
+ if config_path.is_file():
1432
+ mtime = config_path.stat().st_mtime
1433
+
1434
+ if str(config_path) not in file_mtimes:
1435
+ file_mtimes[str(config_path)] = mtime
1436
+ elif file_mtimes[str(config_path)] != mtime:
1437
+ _log(f"Config file changed: {config_path.name}")
1438
+ file_mtimes[str(config_path)] = mtime
1439
+
1440
+ try:
1441
+ # Reload llms.json
1442
+ with open(config_path, "r") as f:
1443
+ g_config = json.load(f)
1444
+
1445
+ # Reload providers
1446
+ await reload_providers()
1447
+ _log("Providers reloaded successfully")
1448
+ except Exception as e:
1449
+ _log(f"Error reloading config: {e}")
1450
+ except FileNotFoundError:
1451
+ pass
1452
+
1453
+ # Check ui.json
1454
+ if ui_path:
1455
+ try:
1456
+ if ui_path.is_file():
1457
+ mtime = ui_path.stat().st_mtime
1458
+
1459
+ if str(ui_path) not in file_mtimes:
1460
+ file_mtimes[str(ui_path)] = mtime
1461
+ elif file_mtimes[str(ui_path)] != mtime:
1462
+ _log(f"Config file changed: {ui_path.name}")
1463
+ file_mtimes[str(ui_path)] = mtime
1464
+ _log("ui.json reloaded - reload page to update")
1465
+ except FileNotFoundError:
1466
+ pass
1467
+
1408
1468
  def main():
1409
1469
  global _ROOT, g_verbose, g_default_model, g_logprefix, g_config, g_config_path, g_ui_path
1410
1470
 
@@ -1492,8 +1552,7 @@ def main():
1492
1552
  g_ui_path = home_ui_path
1493
1553
  g_config = json.loads(text_from_file(g_config_path))
1494
1554
 
1495
- init_llms(g_config)
1496
- asyncio.run(load_llms())
1555
+ asyncio.run(reload_providers())
1497
1556
 
1498
1557
  # print names
1499
1558
  _log(f"enabled providers: {', '.join(g_handlers.keys())}")
@@ -1935,6 +1994,14 @@ def main():
1935
1994
  # Serve index.html as fallback route (SPA routing)
1936
1995
  app.router.add_route('*', '/{tail:.*}', index_handler)
1937
1996
 
1997
+ # Setup file watcher for config files
1998
+ async def start_background_tasks(app):
1999
+ """Start background tasks when the app starts"""
2000
+ # Start watching config files in the background
2001
+ asyncio.create_task(watch_config_files(g_config_path, g_ui_path))
2002
+
2003
+ app.on_startup.append(start_background_tasks)
2004
+
1938
2005
  print(f"Starting server on port {port}...")
1939
2006
  web.run_app(app, host='0.0.0.0', port=port, print=_log)
1940
2007
  exit(0)
@@ -11,6 +11,7 @@ export function useChatPrompt() {
11
11
  const attachedFiles = ref([])
12
12
  const isGenerating = ref(false)
13
13
  const errorStatus = ref(null)
14
+ const abortController = ref(null)
14
15
  const hasImage = () => attachedFiles.value.some(f => imageExts.includes(lastRightPart(f.name, '.')))
15
16
  const hasAudio = () => attachedFiles.value.some(f => audioExts.includes(lastRightPart(f.name, '.')))
16
17
  const hasFile = () => attachedFiles.value.length > 0
@@ -21,6 +22,17 @@ export function useChatPrompt() {
21
22
  isGenerating.value = false
22
23
  attachedFiles.value = []
23
24
  messageText.value = ''
25
+ abortController.value = null
26
+ }
27
+
28
+ function cancel() {
29
+ // Cancel the pending request
30
+ if (abortController.value) {
31
+ abortController.value.abort()
32
+ }
33
+ // Reset UI state
34
+ isGenerating.value = false
35
+ abortController.value = null
24
36
  }
25
37
 
26
38
  return {
@@ -28,6 +40,7 @@ export function useChatPrompt() {
28
40
  attachedFiles,
29
41
  errorStatus,
30
42
  isGenerating,
43
+ abortController,
31
44
  get generating() {
32
45
  return isGenerating.value
33
46
  },
@@ -36,6 +49,7 @@ export function useChatPrompt() {
36
49
  hasFile,
37
50
  // hasText,
38
51
  reset,
52
+ cancel,
39
53
  }
40
54
  }
41
55
 
@@ -91,15 +105,18 @@ export default {
91
105
  ]"
92
106
  :disabled="isGenerating || !model"
93
107
  ></textarea>
94
- <button title="Send (Enter)" type="button"
108
+ <button v-if="!isGenerating" title="Send (Enter)" type="button"
95
109
  @click="sendMessage"
96
110
  :disabled="!messageText.trim() || isGenerating || !model"
97
111
  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">
98
- <svg v-if="isGenerating" class="size-5 animate-spin" fill="none" viewBox="0 0 24 24">
99
- <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
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>
112
+ <svg class="size-5" 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>
113
+ </button>
114
+ <button v-else title="Cancel request" type="button"
115
+ @click="cancelRequest"
116
+ class="absolute bottom-2 right-2 size-8 flex items-center justify-center rounded-md border border-red-300 dark:border-red-600 text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/30 transition-colors">
117
+ <svg class="size-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">
118
+ <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
101
119
  </svg>
102
- <svg v-else class="size-5" 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
120
  </button>
104
121
  </div>
105
122
 
@@ -304,6 +321,10 @@ export default {
304
321
  }
305
322
  messageText.value = ''
306
323
 
324
+ // Create AbortController for this request
325
+ const controller = new AbortController()
326
+ chatPrompt.abortController.value = controller
327
+
307
328
  try {
308
329
  let threadId
309
330
 
@@ -434,11 +455,15 @@ export default {
434
455
  }))
435
456
  }
436
457
 
458
+ chatRequest.metadata ??= {}
459
+ chatRequest.metadata.threadId = threadId
460
+
437
461
  // Send to API
438
462
  console.debug('chatRequest', chatRequest)
439
463
  const startTime = Date.now()
440
464
  const response = await ai.post('/v1/chat/completions', {
441
- body: JSON.stringify(chatRequest)
465
+ body: JSON.stringify(chatRequest),
466
+ signal: controller.signal
442
467
  })
443
468
 
444
469
  let result = null
@@ -513,11 +538,25 @@ export default {
513
538
  attachedFiles.value = []
514
539
  // Error will be cleared when user sends next message (no auto-timeout)
515
540
  }
541
+ } catch (error) {
542
+ // Check if the error is due to abort
543
+ if (error.name === 'AbortError') {
544
+ console.log('Request was cancelled by user')
545
+ // Don't show error for cancelled requests
546
+ } else {
547
+ // Re-throw other errors to be handled by outer catch
548
+ throw error
549
+ }
516
550
  } finally {
517
551
  isGenerating.value = false
552
+ chatPrompt.abortController.value = null
518
553
  }
519
554
  }
520
555
 
556
+ const cancelRequest = () => {
557
+ chatPrompt.cancel()
558
+ }
559
+
521
560
  const addNewLine = () => {
522
561
  // Enter key already adds new line
523
562
  //messageText.value += '\n'
@@ -538,6 +577,7 @@ export default {
538
577
  onDrop,
539
578
  removeAttachment,
540
579
  sendMessage,
580
+ cancelRequest,
541
581
  addNewLine,
542
582
  }
543
583
  }
@@ -218,7 +218,7 @@ export default {
218
218
  </div>
219
219
 
220
220
  <!-- Loading indicator -->
221
- <div v-if="isGenerating" class="flex items-start space-x-3">
221
+ <div v-if="isGenerating" class="flex items-start space-x-3 group">
222
222
  <!-- Avatar outside the bubble -->
223
223
  <div class="flex-shrink-0">
224
224
  <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">
@@ -234,6 +234,13 @@ export default {
234
234
  <div class="w-2 h-2 bg-gray-400 dark:bg-gray-500 rounded-full animate-bounce" style="animation-delay: 0.2s"></div>
235
235
  </div>
236
236
  </div>
237
+
238
+ <!-- Cancel button -->
239
+ <button type="button" @click="cancelRequest"
240
+ 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"
241
+ title="Cancel request">
242
+ cancel
243
+ </button>
237
244
  </div>
238
245
 
239
246
  <!-- Error message bubble -->
@@ -741,6 +748,11 @@ export default {
741
748
  editingMessage.value = null
742
749
  }
743
750
 
751
+ // Cancel pending request
752
+ const cancelRequest = () => {
753
+ chatPrompt.cancel()
754
+ }
755
+
744
756
  function tokensTitle(usage) {
745
757
  let title = []
746
758
  if (usage.tokens && usage.price) {
@@ -790,6 +802,7 @@ export default {
790
802
  editMessage,
791
803
  saveEditedMessage,
792
804
  cancelEdit,
805
+ cancelRequest,
793
806
  configUpdated,
794
807
  exportThreads,
795
808
  exportRequests,
@@ -1,3 +1,4 @@
1
+ import { ref, onMounted, onUnmounted } from "vue"
1
2
  import ProviderStatus from "./ProviderStatus.mjs"
2
3
  import ProviderIcon from "./ProviderIcon.mjs"
3
4
 
@@ -9,7 +10,7 @@ export default {
9
10
  template:`
10
11
  <!-- Model Selector -->
11
12
  <div class="pl-1 flex space-x-2">
12
- <Autocomplete id="model" :options="models" label=""
13
+ <Autocomplete ref="refSelector" id="model" :options="models" label=""
13
14
  :modelValue="modelValue" @update:modelValue="$emit('update:modelValue', $event)"
14
15
  class="w-72 xl:w-84"
15
16
  :match="(x, value) => x.id.toLowerCase().includes(value.toLowerCase())"
@@ -52,8 +53,25 @@ export default {
52
53
  return ret.endsWith('.00') ? ret.slice(0, -3) : ret
53
54
  }
54
55
 
56
+ const refSelector = ref()
57
+
58
+ function collapse(e) {
59
+ // call toggle when clicking outside of the Autocomplete component
60
+ if (refSelector.value && !refSelector.value.$el.contains(e.target)) {
61
+ refSelector.value.toggle(false)
62
+ }
63
+ }
64
+
65
+ onMounted(() => {
66
+ document.addEventListener('click', collapse)
67
+ })
68
+ onUnmounted(() => {
69
+ document.removeEventListener('click', collapse)
70
+ })
71
+
55
72
  return {
56
- tokenPrice
73
+ refSelector,
74
+ tokenPrice,
57
75
  }
58
76
  }
59
77
  }
@@ -1,10 +1,11 @@
1
+ import { ref, onMounted, onUnmounted } from "vue"
1
2
  export default {
2
3
  template:`
3
4
  <button v-if="modelValue" type="button" title="Clear System Prompt" @click="$emit('update:modelValue', null)">
4
5
  <svg class="size-4 text-gray-500 dark:text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12z"/></svg>
5
6
  </button>
6
7
 
7
- <Autocomplete id="prompt" :options="prompts" label=""
8
+ <Autocomplete ref="refSelector" id="prompt" :options="prompts" label=""
8
9
  :modelValue="modelValue" @update:modelValue="$emit('update:modelValue', $event)"
9
10
  class="w-72 xl:w-84"
10
11
  :match="(x, value) => x.name.toLowerCase().includes(value.toLowerCase())"
@@ -32,5 +33,24 @@ export default {
32
33
  show: Boolean,
33
34
  },
34
35
  setup() {
36
+ const refSelector = ref()
37
+
38
+ function collapse(e) {
39
+ // call toggle when clicking outside of the Autocomplete component
40
+ if (refSelector.value && !refSelector.value.$el.contains(e.target)) {
41
+ refSelector.value.toggle(false)
42
+ }
43
+ }
44
+
45
+ onMounted(() => {
46
+ document.addEventListener('click', collapse)
47
+ })
48
+ onUnmounted(() => {
49
+ document.removeEventListener('click', collapse)
50
+ })
51
+
52
+ return {
53
+ refSelector,
54
+ }
35
55
  }
36
56
  }
@@ -6,7 +6,7 @@ const headers = { 'Accept': 'application/json' }
6
6
  const prefsKey = 'llms.prefs'
7
7
 
8
8
  export const o = {
9
- version: '2.0.28',
9
+ version: '2.0.29',
10
10
  base,
11
11
  prefsKey,
12
12
  welcome: 'Welcome to llms.py',
@@ -1916,6 +1916,13 @@
1916
1916
  }
1917
1917
  }
1918
1918
  }
1919
+ .hover\:border-red-300 {
1920
+ &:hover {
1921
+ @media (hover: hover) {
1922
+ border-color: var(--color-red-300);
1923
+ }
1924
+ }
1925
+ }
1919
1926
  .hover\:bg-black\/10 {
1920
1927
  &:hover {
1921
1928
  @media (hover: hover) {
@@ -3296,6 +3303,15 @@
3296
3303
  }
3297
3304
  }
3298
3305
  }
3306
+ .dark\:hover\:border-red-600 {
3307
+ &:where(.dark, .dark *) {
3308
+ &:hover {
3309
+ @media (hover: hover) {
3310
+ border-color: var(--color-red-600);
3311
+ }
3312
+ }
3313
+ }
3314
+ }
3299
3315
  .dark\:hover\:bg-blue-600 {
3300
3316
  &:where(.dark, .dark *) {
3301
3317
  &:hover {