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.
- {llms_py-2.0.28/llms_py.egg-info → llms_py-2.0.29}/PKG-INFO +3 -3
- {llms_py-2.0.28 → llms_py-2.0.29}/README.md +2 -2
- {llms_py-2.0.28 → llms_py-2.0.29}/llms/main.py +70 -3
- {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui/ChatPrompt.mjs +46 -6
- {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui/Main.mjs +14 -1
- {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui/ModelSelector.mjs +20 -2
- {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui/SystemPromptSelector.mjs +21 -1
- {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui/ai.mjs +1 -1
- {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui/app.css +16 -0
- llms_py-2.0.29/llms/ui/lib/servicestack-vue.mjs +37 -0
- {llms_py-2.0.28 → llms_py-2.0.29/llms_py.egg-info}/PKG-INFO +3 -3
- {llms_py-2.0.28 → llms_py-2.0.29}/pyproject.toml +1 -1
- {llms_py-2.0.28 → llms_py-2.0.29}/setup.py +1 -1
- llms_py-2.0.28/llms/ui/lib/servicestack-vue.mjs +0 -37
- {llms_py-2.0.28 → llms_py-2.0.29}/LICENSE +0 -0
- {llms_py-2.0.28 → llms_py-2.0.29}/MANIFEST.in +0 -0
- {llms_py-2.0.28 → llms_py-2.0.29}/llms/__init__.py +0 -0
- {llms_py-2.0.28 → llms_py-2.0.29}/llms/__main__.py +0 -0
- {llms_py-2.0.28 → llms_py-2.0.29}/llms/index.html +0 -0
- {llms_py-2.0.28 → llms_py-2.0.29}/llms/llms.json +0 -0
- {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui/Analytics.mjs +0 -0
- {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui/App.mjs +0 -0
- {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui/Avatar.mjs +0 -0
- {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui/Brand.mjs +0 -0
- {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui/OAuthSignIn.mjs +0 -0
- {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui/ProviderIcon.mjs +0 -0
- {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui/ProviderStatus.mjs +0 -0
- {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui/Recents.mjs +0 -0
- {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui/SettingsDialog.mjs +0 -0
- {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui/Sidebar.mjs +0 -0
- {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui/SignIn.mjs +0 -0
- {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui/SystemPromptEditor.mjs +0 -0
- {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui/Welcome.mjs +0 -0
- {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui/fav.svg +0 -0
- {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui/lib/chart.js +0 -0
- {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui/lib/charts.mjs +0 -0
- {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui/lib/color.js +0 -0
- {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui/lib/highlight.min.mjs +0 -0
- {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui/lib/idb.min.mjs +0 -0
- {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui/lib/marked.min.mjs +0 -0
- {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui/lib/servicestack-client.mjs +0 -0
- {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui/lib/vue-router.min.mjs +0 -0
- {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui/lib/vue.min.mjs +0 -0
- {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui/lib/vue.mjs +0 -0
- {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui/markdown.mjs +0 -0
- {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui/tailwind.input.css +0 -0
- {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui/threadStore.mjs +0 -0
- {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui/typography.css +0 -0
- {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui/utils.mjs +0 -0
- {llms_py-2.0.28 → llms_py-2.0.29}/llms/ui.json +0 -0
- {llms_py-2.0.28 → llms_py-2.0.29}/llms_py.egg-info/SOURCES.txt +0 -0
- {llms_py-2.0.28 → llms_py-2.0.29}/llms_py.egg-info/dependency_links.txt +0 -0
- {llms_py-2.0.28 → llms_py-2.0.29}/llms_py.egg-info/entry_points.txt +0 -0
- {llms_py-2.0.28 → llms_py-2.0.29}/llms_py.egg-info/not-zip-safe +0 -0
- {llms_py-2.0.28 → llms_py-2.0.29}/llms_py.egg-info/requires.txt +0 -0
- {llms_py-2.0.28 → llms_py-2.0.29}/llms_py.egg-info/top_level.txt +0 -0
- {llms_py-2.0.28 → llms_py-2.0.29}/requirements.txt +0 -0
- {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.
|
|
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/posts/llms-py-ui)
|
|
76
|
+
[](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/posts/llms-py-ui)
|
|
84
|
+
[](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/posts/llms-py-ui)
|
|
36
|
+
[](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/posts/llms-py-ui)
|
|
44
|
+
[](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.
|
|
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
|
-
|
|
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
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
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
|
}
|
|
@@ -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 {
|