llms-py 2.0.5__tar.gz → 2.0.7__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.5/llms_py.egg-info → llms_py-2.0.7}/PKG-INFO +9 -1
- {llms_py-2.0.5 → llms_py-2.0.7}/README.md +8 -0
- {llms_py-2.0.5 → llms_py-2.0.7}/llms.py +12 -2
- {llms_py-2.0.5 → llms_py-2.0.7/llms_py.egg-info}/PKG-INFO +9 -1
- {llms_py-2.0.5 → llms_py-2.0.7}/pyproject.toml +1 -1
- {llms_py-2.0.5 → llms_py-2.0.7}/setup.py +1 -1
- {llms_py-2.0.5 → llms_py-2.0.7}/ui/ChatPrompt.mjs +8 -6
- {llms_py-2.0.5 → llms_py-2.0.7}/ui/Main.mjs +210 -1
- {llms_py-2.0.5 → llms_py-2.0.7}/ui/app.css +41 -55
- {llms_py-2.0.5 → llms_py-2.0.7}/LICENSE +0 -0
- {llms_py-2.0.5 → llms_py-2.0.7}/MANIFEST.in +0 -0
- {llms_py-2.0.5 → llms_py-2.0.7}/index.html +0 -0
- {llms_py-2.0.5 → llms_py-2.0.7}/llms.json +0 -0
- {llms_py-2.0.5 → llms_py-2.0.7}/llms_py.egg-info/SOURCES.txt +0 -0
- {llms_py-2.0.5 → llms_py-2.0.7}/llms_py.egg-info/dependency_links.txt +0 -0
- {llms_py-2.0.5 → llms_py-2.0.7}/llms_py.egg-info/entry_points.txt +0 -0
- {llms_py-2.0.5 → llms_py-2.0.7}/llms_py.egg-info/not-zip-safe +0 -0
- {llms_py-2.0.5 → llms_py-2.0.7}/llms_py.egg-info/requires.txt +0 -0
- {llms_py-2.0.5 → llms_py-2.0.7}/llms_py.egg-info/top_level.txt +0 -0
- {llms_py-2.0.5 → llms_py-2.0.7}/requirements.txt +0 -0
- {llms_py-2.0.5 → llms_py-2.0.7}/setup.cfg +0 -0
- {llms_py-2.0.5 → llms_py-2.0.7}/ui/App.mjs +0 -0
- {llms_py-2.0.5 → llms_py-2.0.7}/ui/Recents.mjs +0 -0
- {llms_py-2.0.5 → llms_py-2.0.7}/ui/Sidebar.mjs +0 -0
- {llms_py-2.0.5 → llms_py-2.0.7}/ui/fav.svg +0 -0
- {llms_py-2.0.5 → llms_py-2.0.7}/ui/lib/highlight.min.mjs +0 -0
- {llms_py-2.0.5 → llms_py-2.0.7}/ui/lib/idb.min.mjs +0 -0
- {llms_py-2.0.5 → llms_py-2.0.7}/ui/lib/marked.min.mjs +0 -0
- {llms_py-2.0.5 → llms_py-2.0.7}/ui/lib/servicestack-client.min.mjs +0 -0
- {llms_py-2.0.5 → llms_py-2.0.7}/ui/lib/servicestack-vue.min.mjs +0 -0
- {llms_py-2.0.5 → llms_py-2.0.7}/ui/lib/vue-router.min.mjs +0 -0
- {llms_py-2.0.5 → llms_py-2.0.7}/ui/lib/vue.min.mjs +0 -0
- {llms_py-2.0.5 → llms_py-2.0.7}/ui/lib/vue.mjs +0 -0
- {llms_py-2.0.5 → llms_py-2.0.7}/ui/markdown.mjs +0 -0
- {llms_py-2.0.5 → llms_py-2.0.7}/ui/tailwind.input.css +0 -0
- {llms_py-2.0.5 → llms_py-2.0.7}/ui/threadStore.mjs +0 -0
- {llms_py-2.0.5 → llms_py-2.0.7}/ui/typography.css +0 -0
- {llms_py-2.0.5 → llms_py-2.0.7}/ui/utils.mjs +0 -0
- {llms_py-2.0.5 → llms_py-2.0.7}/ui.json +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.7
|
|
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
|
|
@@ -63,6 +63,14 @@ Configure additional providers and models in [llms.json](llms.json)
|
|
|
63
63
|
- **Unified Models**: Define custom model names that map to different provider-specific names
|
|
64
64
|
- **Multi-Model Support**: Support for over 160+ different LLMs
|
|
65
65
|
|
|
66
|
+
## llms.py UI
|
|
67
|
+
|
|
68
|
+
Simple ChatGPT-like UI to access ALL Your LLMs, Locally or Remotely!
|
|
69
|
+
|
|
70
|
+
[](https://servicestack.net/posts/llms-py-ui)
|
|
71
|
+
|
|
72
|
+
Read the [Introductory Blog Post](https://servicestack.net/posts/llms-py-ui).
|
|
73
|
+
|
|
66
74
|
## Installation
|
|
67
75
|
|
|
68
76
|
### Option 1: Install from PyPI
|
|
@@ -23,6 +23,14 @@ Configure additional providers and models in [llms.json](llms.json)
|
|
|
23
23
|
- **Unified Models**: Define custom model names that map to different provider-specific names
|
|
24
24
|
- **Multi-Model Support**: Support for over 160+ different LLMs
|
|
25
25
|
|
|
26
|
+
## llms.py UI
|
|
27
|
+
|
|
28
|
+
Simple ChatGPT-like UI to access ALL Your LLMs, Locally or Remotely!
|
|
29
|
+
|
|
30
|
+
[](https://servicestack.net/posts/llms-py-ui)
|
|
31
|
+
|
|
32
|
+
Read the [Introductory Blog Post](https://servicestack.net/posts/llms-py-ui).
|
|
33
|
+
|
|
26
34
|
## Installation
|
|
27
35
|
|
|
28
36
|
### Option 1: Install from PyPI
|
|
@@ -21,7 +21,7 @@ from aiohttp import web
|
|
|
21
21
|
from pathlib import Path
|
|
22
22
|
from importlib import resources # Py≥3.9 (pip install importlib_resources for 3.7/3.8)
|
|
23
23
|
|
|
24
|
-
VERSION = "2.0.
|
|
24
|
+
VERSION = "2.0.7"
|
|
25
25
|
_ROOT = None
|
|
26
26
|
g_config_path = None
|
|
27
27
|
g_ui_path = None
|
|
@@ -66,6 +66,16 @@ def chat_summary(chat):
|
|
|
66
66
|
item['file']['file_data'] = f"({len(data)})"
|
|
67
67
|
return json.dumps(clone, indent=2)
|
|
68
68
|
|
|
69
|
+
def gemini_chat_summary(gemini_chat):
|
|
70
|
+
"""Summarize Gemini chat completion request for logging. Replace inline_data with size of content only"""
|
|
71
|
+
clone = json.loads(json.dumps(gemini_chat))
|
|
72
|
+
for content in clone['contents']:
|
|
73
|
+
for part in content['parts']:
|
|
74
|
+
if 'inline_data' in part:
|
|
75
|
+
data = part['inline_data']['data']
|
|
76
|
+
part['inline_data']['data'] = f"({len(data)})"
|
|
77
|
+
return json.dumps(clone, indent=2)
|
|
78
|
+
|
|
69
79
|
image_exts = 'png,webp,jpg,jpeg,gif,bmp,svg,tiff,ico'.split(',')
|
|
70
80
|
audio_exts = 'mp3,wav,ogg,flac,m4a,opus,webm'.split(',')
|
|
71
81
|
|
|
@@ -417,7 +427,7 @@ class GoogleProvider(OpenAiProvider):
|
|
|
417
427
|
gemini_chat_url = f"https://generativelanguage.googleapis.com/v1beta/models/{chat['model']}:generateContent?key={self.api_key}"
|
|
418
428
|
|
|
419
429
|
_log(f"POST {gemini_chat_url}")
|
|
420
|
-
_log(
|
|
430
|
+
_log(gemini_chat_summary(gemini_chat))
|
|
421
431
|
|
|
422
432
|
if self.curl:
|
|
423
433
|
curl_args = [
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: llms-py
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.7
|
|
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
|
|
@@ -63,6 +63,14 @@ Configure additional providers and models in [llms.json](llms.json)
|
|
|
63
63
|
- **Unified Models**: Define custom model names that map to different provider-specific names
|
|
64
64
|
- **Multi-Model Support**: Support for over 160+ different LLMs
|
|
65
65
|
|
|
66
|
+
## llms.py UI
|
|
67
|
+
|
|
68
|
+
Simple ChatGPT-like UI to access ALL Your LLMs, Locally or Remotely!
|
|
69
|
+
|
|
70
|
+
[](https://servicestack.net/posts/llms-py-ui)
|
|
71
|
+
|
|
72
|
+
Read the [Introductory Blog Post](https://servicestack.net/posts/llms-py-ui).
|
|
73
|
+
|
|
66
74
|
## Installation
|
|
67
75
|
|
|
68
76
|
### Option 1: Install from PyPI
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "llms-py"
|
|
7
|
-
version = "2.0.
|
|
7
|
+
version = "2.0.7"
|
|
8
8
|
description = "A lightweight CLI tool and OpenAI-compatible server for querying multiple Large Language Model (LLM) providers"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "BSD-3-Clause"
|
|
@@ -16,7 +16,7 @@ with open(os.path.join(this_directory, "requirements.txt"), encoding="utf-8") as
|
|
|
16
16
|
|
|
17
17
|
setup(
|
|
18
18
|
name="llms-py",
|
|
19
|
-
version="2.0.
|
|
19
|
+
version="2.0.7",
|
|
20
20
|
author="ServiceStack",
|
|
21
21
|
author_email="team@servicestack.net",
|
|
22
22
|
description="A lightweight CLI tool and OpenAI-compatible server for querying multiple Large Language Model (LLM) providers",
|
|
@@ -146,12 +146,14 @@ export default {
|
|
|
146
146
|
// allow re-selecting the same file
|
|
147
147
|
if (fileInput.value) fileInput.value.value = ''
|
|
148
148
|
|
|
149
|
-
if (
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
149
|
+
if (!messageText.value.trim()) {
|
|
150
|
+
if (hasImage()) {
|
|
151
|
+
messageText.value = getTextContent(config.defaults.image)
|
|
152
|
+
} else if (hasAudio()) {
|
|
153
|
+
messageText.value = getTextContent(config.defaults.audio)
|
|
154
|
+
} else {
|
|
155
|
+
messageText.value = getTextContent(config.defaults.file)
|
|
156
|
+
}
|
|
155
157
|
}
|
|
156
158
|
}
|
|
157
159
|
const removeAttachment = (i) => {
|
|
@@ -197,6 +197,51 @@ export default {
|
|
|
197
197
|
<div class="max-w-2xl mx-auto">
|
|
198
198
|
<ChatPrompt :model="selectedModel" :systemPrompt="currentSystemPrompt" />
|
|
199
199
|
</div>
|
|
200
|
+
|
|
201
|
+
<!-- Export/Import buttons -->
|
|
202
|
+
<div class="mt-2 flex space-x-3 justify-center">
|
|
203
|
+
<button type="button"
|
|
204
|
+
@click="exportThreads"
|
|
205
|
+
:disabled="isExporting"
|
|
206
|
+
:title="'Export ' + threads?.threads?.value?.length + ' conversations'"
|
|
207
|
+
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"
|
|
208
|
+
>
|
|
209
|
+
<svg v-if="!isExporting" class="size-5 mr-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
210
|
+
<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>
|
|
211
|
+
</svg>
|
|
212
|
+
<svg v-else class="size-5 mr-1 animate-spin" fill="none" viewBox="0 0 24 24">
|
|
213
|
+
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
214
|
+
<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>
|
|
215
|
+
</svg>
|
|
216
|
+
{{ isExporting ? 'Exporting...' : 'Export' }}
|
|
217
|
+
</button>
|
|
218
|
+
|
|
219
|
+
<button type="button"
|
|
220
|
+
@click="triggerImport"
|
|
221
|
+
:disabled="isImporting"
|
|
222
|
+
title="Import conversations from JSON file"
|
|
223
|
+
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"
|
|
224
|
+
>
|
|
225
|
+
<svg v-if="!isImporting" class="size-5 mr-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
226
|
+
<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"/>
|
|
227
|
+
</svg>
|
|
228
|
+
<svg v-else class="size-5 mr-1 animate-spin" fill="none" viewBox="0 0 24 24">
|
|
229
|
+
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
230
|
+
<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>
|
|
231
|
+
</svg>
|
|
232
|
+
{{ isImporting ? 'Importing...' : 'Import' }}
|
|
233
|
+
</button>
|
|
234
|
+
|
|
235
|
+
<!-- Hidden file input for import -->
|
|
236
|
+
<input
|
|
237
|
+
ref="fileInput"
|
|
238
|
+
type="file"
|
|
239
|
+
accept=".json"
|
|
240
|
+
@change="handleFileImport"
|
|
241
|
+
class="hidden"
|
|
242
|
+
/>
|
|
243
|
+
</div>
|
|
244
|
+
|
|
200
245
|
</div>
|
|
201
246
|
|
|
202
247
|
<!-- Messages -->
|
|
@@ -229,11 +274,25 @@ export default {
|
|
|
229
274
|
|
|
230
275
|
<!-- Message bubble -->
|
|
231
276
|
<div
|
|
232
|
-
class="message rounded-lg px-4 py-3"
|
|
277
|
+
class="message rounded-lg px-4 py-3 relative group"
|
|
233
278
|
:class="message.role === 'user'
|
|
234
279
|
? 'bg-blue-600 text-white'
|
|
235
280
|
: 'bg-gray-100 text-gray-900 border border-gray-200'"
|
|
236
281
|
>
|
|
282
|
+
<!-- Copy button in top right corner -->
|
|
283
|
+
<button
|
|
284
|
+
type="button"
|
|
285
|
+
@click="copyMessageContent(message)"
|
|
286
|
+
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-2 focus:ring-blue-500"
|
|
287
|
+
:class="message.role === 'user' ? 'text-white/70 hover:text-white hover:bg-white/20' : 'text-gray-500 hover:text-gray-700'"
|
|
288
|
+
title="Copy message content"
|
|
289
|
+
>
|
|
290
|
+
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
291
|
+
<rect width="14" height="14" x="8" y="8" rx="2" ry="2"/>
|
|
292
|
+
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/>
|
|
293
|
+
</svg>
|
|
294
|
+
</button>
|
|
295
|
+
|
|
237
296
|
<div
|
|
238
297
|
v-if="message.role === 'assistant'"
|
|
239
298
|
v-html="renderMarkdown(message.content)"
|
|
@@ -351,6 +410,9 @@ export default {
|
|
|
351
410
|
const currentSystemPrompt = ref('')
|
|
352
411
|
const showSystemPrompt = ref(false)
|
|
353
412
|
const messagesContainer = ref(null)
|
|
413
|
+
const isExporting = ref(false)
|
|
414
|
+
const isImporting = ref(false)
|
|
415
|
+
const fileInput = ref(null)
|
|
354
416
|
|
|
355
417
|
// Auto-scroll to bottom when new messages arrive
|
|
356
418
|
const scrollToBottom = async () => {
|
|
@@ -412,6 +474,129 @@ export default {
|
|
|
412
474
|
}))
|
|
413
475
|
})
|
|
414
476
|
|
|
477
|
+
async function exportThreads() {
|
|
478
|
+
if (isExporting.value) return
|
|
479
|
+
|
|
480
|
+
isExporting.value = true
|
|
481
|
+
try {
|
|
482
|
+
// Load all threads from IndexedDB
|
|
483
|
+
await threads.loadThreads()
|
|
484
|
+
const allThreads = threads.threads.value
|
|
485
|
+
|
|
486
|
+
// Create export data with metadata
|
|
487
|
+
const exportData = {
|
|
488
|
+
exportedAt: new Date().toISOString(),
|
|
489
|
+
version: '1.0',
|
|
490
|
+
source: 'llms.py',
|
|
491
|
+
threadCount: allThreads.length,
|
|
492
|
+
threads: allThreads
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Create and download JSON file
|
|
496
|
+
const jsonString = JSON.stringify(exportData, null, 2)
|
|
497
|
+
const blob = new Blob([jsonString], { type: 'application/json' })
|
|
498
|
+
const url = URL.createObjectURL(blob)
|
|
499
|
+
|
|
500
|
+
const link = document.createElement('a')
|
|
501
|
+
link.href = url
|
|
502
|
+
link.download = `llms-threads-export-${new Date().toISOString().split('T')[0]}.json`
|
|
503
|
+
document.body.appendChild(link)
|
|
504
|
+
link.click()
|
|
505
|
+
document.body.removeChild(link)
|
|
506
|
+
URL.revokeObjectURL(url)
|
|
507
|
+
|
|
508
|
+
} catch (error) {
|
|
509
|
+
console.error('Failed to export threads:', error)
|
|
510
|
+
alert('Failed to export threads: ' + error.message)
|
|
511
|
+
} finally {
|
|
512
|
+
isExporting.value = false
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function triggerImport() {
|
|
517
|
+
if (isImporting.value) return
|
|
518
|
+
fileInput.value?.click()
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
async function handleFileImport(event) {
|
|
522
|
+
const file = event.target.files?.[0]
|
|
523
|
+
if (!file) return
|
|
524
|
+
|
|
525
|
+
isImporting.value = true
|
|
526
|
+
try {
|
|
527
|
+
const text = await file.text()
|
|
528
|
+
const importData = JSON.parse(text)
|
|
529
|
+
|
|
530
|
+
// Validate import data structure
|
|
531
|
+
if (!importData.threads || !Array.isArray(importData.threads)) {
|
|
532
|
+
throw new Error('Invalid import file: missing or invalid threads array')
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Import threads one by one
|
|
536
|
+
let importedCount = 0
|
|
537
|
+
let updatedCount = 0
|
|
538
|
+
|
|
539
|
+
for (const threadData of importData.threads) {
|
|
540
|
+
if (!threadData.id) {
|
|
541
|
+
console.warn('Skipping thread without ID:', threadData)
|
|
542
|
+
continue
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
try {
|
|
546
|
+
// Check if thread already exists
|
|
547
|
+
const existingThread = await threads.getThread(threadData.id)
|
|
548
|
+
|
|
549
|
+
if (existingThread) {
|
|
550
|
+
// Update existing thread
|
|
551
|
+
await threads.updateThread(threadData.id, {
|
|
552
|
+
title: threadData.title,
|
|
553
|
+
model: threadData.model,
|
|
554
|
+
systemPrompt: threadData.systemPrompt,
|
|
555
|
+
messages: threadData.messages || [],
|
|
556
|
+
createdAt: threadData.createdAt,
|
|
557
|
+
// Keep the existing updatedAt or use imported one
|
|
558
|
+
updatedAt: threadData.updatedAt || existingThread.updatedAt
|
|
559
|
+
})
|
|
560
|
+
updatedCount++
|
|
561
|
+
} else {
|
|
562
|
+
// Add new thread directly to IndexedDB
|
|
563
|
+
await threads.initDB()
|
|
564
|
+
const db = await threads.initDB()
|
|
565
|
+
const tx = db.transaction(['threads'], 'readwrite')
|
|
566
|
+
await tx.objectStore('threads').add({
|
|
567
|
+
id: threadData.id,
|
|
568
|
+
title: threadData.title || 'Imported Chat',
|
|
569
|
+
model: threadData.model || '',
|
|
570
|
+
systemPrompt: threadData.systemPrompt || '',
|
|
571
|
+
messages: threadData.messages || [],
|
|
572
|
+
createdAt: threadData.createdAt || new Date().toISOString(),
|
|
573
|
+
updatedAt: threadData.updatedAt || new Date().toISOString()
|
|
574
|
+
})
|
|
575
|
+
await tx.complete
|
|
576
|
+
importedCount++
|
|
577
|
+
}
|
|
578
|
+
} catch (error) {
|
|
579
|
+
console.error('Failed to import thread:', threadData.id, error)
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Reload threads to reflect changes
|
|
584
|
+
await threads.loadThreads()
|
|
585
|
+
|
|
586
|
+
alert(`Import completed!\nNew threads: ${importedCount}\nUpdated threads: ${updatedCount}`)
|
|
587
|
+
|
|
588
|
+
} catch (error) {
|
|
589
|
+
console.error('Failed to import threads:', error)
|
|
590
|
+
alert('Failed to import threads: ' + error.message)
|
|
591
|
+
} finally {
|
|
592
|
+
isImporting.value = false
|
|
593
|
+
// Clear the file input
|
|
594
|
+
if (fileInput.value) {
|
|
595
|
+
fileInput.value.value = ''
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
415
600
|
function configUpdated() {
|
|
416
601
|
console.log('configUpdated', selectedModel.value, models.length, models.includes(selectedModel.value))
|
|
417
602
|
if (selectedModel.value && !models.includes(selectedModel.value)) {
|
|
@@ -441,6 +626,23 @@ export default {
|
|
|
441
626
|
}
|
|
442
627
|
const formatReasoning = (r) => typeof r === 'string' ? r : JSON.stringify(r, null, 2)
|
|
443
628
|
|
|
629
|
+
// Copy message content to clipboard
|
|
630
|
+
const copyMessageContent = async (message) => {
|
|
631
|
+
try {
|
|
632
|
+
await navigator.clipboard.writeText(message.content)
|
|
633
|
+
// Could add a toast notification here if desired
|
|
634
|
+
} catch (err) {
|
|
635
|
+
console.error('Failed to copy message content:', err)
|
|
636
|
+
// Fallback for older browsers
|
|
637
|
+
const textArea = document.createElement('textarea')
|
|
638
|
+
textArea.value = message.content
|
|
639
|
+
document.body.appendChild(textArea)
|
|
640
|
+
textArea.select()
|
|
641
|
+
document.execCommand('copy')
|
|
642
|
+
document.body.removeChild(textArea)
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
444
646
|
onMounted(() => {
|
|
445
647
|
setTimeout(addCopyButtons, 1)
|
|
446
648
|
})
|
|
@@ -465,7 +667,14 @@ export default {
|
|
|
465
667
|
isReasoningExpanded,
|
|
466
668
|
toggleReasoning,
|
|
467
669
|
formatReasoning,
|
|
670
|
+
copyMessageContent,
|
|
468
671
|
configUpdated,
|
|
672
|
+
exportThreads,
|
|
673
|
+
isExporting,
|
|
674
|
+
triggerImport,
|
|
675
|
+
handleFileImport,
|
|
676
|
+
isImporting,
|
|
677
|
+
fileInput,
|
|
469
678
|
}
|
|
470
679
|
}
|
|
471
680
|
}
|
|
@@ -135,7 +135,6 @@
|
|
|
135
135
|
--default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
|
136
136
|
--default-font-family: var(--font-sans);
|
|
137
137
|
--default-mono-font-family: var(--font-mono);
|
|
138
|
-
--default-ring-color: hsl(var(--ring));
|
|
139
138
|
}
|
|
140
139
|
}
|
|
141
140
|
@layer base {
|
|
@@ -331,6 +330,9 @@
|
|
|
331
330
|
.top-0 {
|
|
332
331
|
top: calc(var(--spacing) * 0);
|
|
333
332
|
}
|
|
333
|
+
.top-2 {
|
|
334
|
+
top: calc(var(--spacing) * 2);
|
|
335
|
+
}
|
|
334
336
|
.right-0 {
|
|
335
337
|
right: calc(var(--spacing) * 0);
|
|
336
338
|
}
|
|
@@ -376,15 +378,9 @@
|
|
|
376
378
|
max-width: 96rem;
|
|
377
379
|
}
|
|
378
380
|
}
|
|
379
|
-
.-m-2 {
|
|
380
|
-
margin: calc(var(--spacing) * -2);
|
|
381
|
-
}
|
|
382
381
|
.-m-2\.5 {
|
|
383
382
|
margin: calc(var(--spacing) * -2.5);
|
|
384
383
|
}
|
|
385
|
-
.-mx-1 {
|
|
386
|
-
margin-inline: calc(var(--spacing) * -1);
|
|
387
|
-
}
|
|
388
384
|
.-mx-1\.5 {
|
|
389
385
|
margin-inline: calc(var(--spacing) * -1.5);
|
|
390
386
|
}
|
|
@@ -394,9 +390,6 @@
|
|
|
394
390
|
.mx-auto {
|
|
395
391
|
margin-inline: auto;
|
|
396
392
|
}
|
|
397
|
-
.-my-1 {
|
|
398
|
-
margin-block: calc(var(--spacing) * -1);
|
|
399
|
-
}
|
|
400
393
|
.-my-1\.5 {
|
|
401
394
|
margin-block: calc(var(--spacing) * -1.5);
|
|
402
395
|
}
|
|
@@ -460,9 +453,6 @@
|
|
|
460
453
|
.-ml-px {
|
|
461
454
|
margin-left: -1px;
|
|
462
455
|
}
|
|
463
|
-
.ml-0 {
|
|
464
|
-
margin-left: calc(var(--spacing) * 0);
|
|
465
|
-
}
|
|
466
456
|
.ml-0\.5 {
|
|
467
457
|
margin-left: calc(var(--spacing) * 0.5);
|
|
468
458
|
}
|
|
@@ -633,9 +623,6 @@
|
|
|
633
623
|
.w-80 {
|
|
634
624
|
width: calc(var(--spacing) * 80);
|
|
635
625
|
}
|
|
636
|
-
.w-84 {
|
|
637
|
-
width: calc(var(--spacing) * 84);
|
|
638
|
-
}
|
|
639
626
|
.w-full {
|
|
640
627
|
width: 100%;
|
|
641
628
|
}
|
|
@@ -1031,9 +1018,6 @@
|
|
|
1031
1018
|
.bg-gray-400 {
|
|
1032
1019
|
background-color: var(--color-gray-400);
|
|
1033
1020
|
}
|
|
1034
|
-
.bg-gray-500 {
|
|
1035
|
-
background-color: var(--color-gray-500);
|
|
1036
|
-
}
|
|
1037
1021
|
.bg-gray-500\/75 {
|
|
1038
1022
|
background-color: color-mix(in srgb, oklch(55.1% 0.027 264.364) 75%, transparent);
|
|
1039
1023
|
@supports (color: color-mix(in lab, red, red)) {
|
|
@@ -1046,9 +1030,6 @@
|
|
|
1046
1030
|
.bg-gray-700 {
|
|
1047
1031
|
background-color: var(--color-gray-700);
|
|
1048
1032
|
}
|
|
1049
|
-
.bg-gray-900 {
|
|
1050
|
-
background-color: var(--color-gray-900);
|
|
1051
|
-
}
|
|
1052
1033
|
.bg-gray-900\/80 {
|
|
1053
1034
|
background-color: color-mix(in srgb, oklch(21% 0.034 264.665) 80%, transparent);
|
|
1054
1035
|
@supports (color: color-mix(in lab, red, red)) {
|
|
@@ -1142,9 +1123,6 @@
|
|
|
1142
1123
|
.px-6 {
|
|
1143
1124
|
padding-inline: calc(var(--spacing) * 6);
|
|
1144
1125
|
}
|
|
1145
|
-
.py-0 {
|
|
1146
|
-
padding-block: calc(var(--spacing) * 0);
|
|
1147
|
-
}
|
|
1148
1126
|
.py-0\.5 {
|
|
1149
1127
|
padding-block: calc(var(--spacing) * 0.5);
|
|
1150
1128
|
}
|
|
@@ -1175,9 +1153,6 @@
|
|
|
1175
1153
|
.py-12 {
|
|
1176
1154
|
padding-block: calc(var(--spacing) * 12);
|
|
1177
1155
|
}
|
|
1178
|
-
.pt-0 {
|
|
1179
|
-
padding-top: calc(var(--spacing) * 0);
|
|
1180
|
-
}
|
|
1181
1156
|
.pt-0\.5 {
|
|
1182
1157
|
padding-top: calc(var(--spacing) * 0.5);
|
|
1183
1158
|
}
|
|
@@ -1444,15 +1419,18 @@
|
|
|
1444
1419
|
.text-sky-600 {
|
|
1445
1420
|
color: var(--color-sky-600);
|
|
1446
1421
|
}
|
|
1447
|
-
.text-slate-300 {
|
|
1448
|
-
color: var(--color-slate-300);
|
|
1449
|
-
}
|
|
1450
1422
|
.text-slate-500 {
|
|
1451
1423
|
color: var(--color-slate-500);
|
|
1452
1424
|
}
|
|
1453
1425
|
.text-white {
|
|
1454
1426
|
color: var(--color-white);
|
|
1455
1427
|
}
|
|
1428
|
+
.text-white\/70 {
|
|
1429
|
+
color: color-mix(in srgb, #fff 70%, transparent);
|
|
1430
|
+
@supports (color: color-mix(in lab, red, red)) {
|
|
1431
|
+
color: color-mix(in oklab, var(--color-white) 70%, transparent);
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1456
1434
|
.text-yellow-400 {
|
|
1457
1435
|
color: var(--color-yellow-400);
|
|
1458
1436
|
}
|
|
@@ -1471,9 +1449,6 @@
|
|
|
1471
1449
|
.uppercase {
|
|
1472
1450
|
text-transform: uppercase;
|
|
1473
1451
|
}
|
|
1474
|
-
.underline {
|
|
1475
|
-
text-decoration-line: underline;
|
|
1476
|
-
}
|
|
1477
1452
|
.placeholder-gray-500 {
|
|
1478
1453
|
&::placeholder {
|
|
1479
1454
|
color: var(--color-gray-500);
|
|
@@ -1531,9 +1506,6 @@
|
|
|
1531
1506
|
--tw-inset-ring-shadow: inset 0 0 0 1px var(--tw-inset-ring-color, currentcolor);
|
|
1532
1507
|
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
|
1533
1508
|
}
|
|
1534
|
-
.ring-black {
|
|
1535
|
-
--tw-ring-color: var(--color-black);
|
|
1536
|
-
}
|
|
1537
1509
|
.ring-black\/5 {
|
|
1538
1510
|
--tw-ring-color: color-mix(in srgb, #000 5%, transparent);
|
|
1539
1511
|
@supports (color: color-mix(in lab, red, red)) {
|
|
@@ -1543,9 +1515,6 @@
|
|
|
1543
1515
|
.ring-indigo-500 {
|
|
1544
1516
|
--tw-ring-color: var(--color-indigo-500);
|
|
1545
1517
|
}
|
|
1546
|
-
.inset-ring-gray-900 {
|
|
1547
|
-
--tw-inset-ring-color: var(--color-gray-900);
|
|
1548
|
-
}
|
|
1549
1518
|
.inset-ring-gray-900\/5 {
|
|
1550
1519
|
--tw-inset-ring-color: color-mix(in srgb, oklch(21% 0.034 264.665) 5%, transparent);
|
|
1551
1520
|
@supports (color: color-mix(in lab, red, red)) {
|
|
@@ -1788,6 +1757,16 @@
|
|
|
1788
1757
|
}
|
|
1789
1758
|
}
|
|
1790
1759
|
}
|
|
1760
|
+
.hover\:bg-black\/10 {
|
|
1761
|
+
&:hover {
|
|
1762
|
+
@media (hover: hover) {
|
|
1763
|
+
background-color: color-mix(in srgb, #000 10%, transparent);
|
|
1764
|
+
@supports (color: color-mix(in lab, red, red)) {
|
|
1765
|
+
background-color: color-mix(in oklab, var(--color-black) 10%, transparent);
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1791
1770
|
.hover\:bg-blue-100 {
|
|
1792
1771
|
&:hover {
|
|
1793
1772
|
@media (hover: hover) {
|
|
@@ -1886,6 +1865,16 @@
|
|
|
1886
1865
|
}
|
|
1887
1866
|
}
|
|
1888
1867
|
}
|
|
1868
|
+
.hover\:bg-white\/20 {
|
|
1869
|
+
&:hover {
|
|
1870
|
+
@media (hover: hover) {
|
|
1871
|
+
background-color: color-mix(in srgb, #fff 20%, transparent);
|
|
1872
|
+
@supports (color: color-mix(in lab, red, red)) {
|
|
1873
|
+
background-color: color-mix(in oklab, var(--color-white) 20%, transparent);
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1889
1878
|
.hover\:bg-yellow-50 {
|
|
1890
1879
|
&:hover {
|
|
1891
1880
|
@media (hover: hover) {
|
|
@@ -2019,6 +2008,13 @@
|
|
|
2019
2008
|
}
|
|
2020
2009
|
}
|
|
2021
2010
|
}
|
|
2011
|
+
.hover\:text-white {
|
|
2012
|
+
&:hover {
|
|
2013
|
+
@media (hover: hover) {
|
|
2014
|
+
color: var(--color-white);
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2022
2018
|
.hover\:opacity-70 {
|
|
2023
2019
|
&:hover {
|
|
2024
2020
|
@media (hover: hover) {
|
|
@@ -2245,6 +2241,11 @@
|
|
|
2245
2241
|
color: var(--color-slate-500);
|
|
2246
2242
|
}
|
|
2247
2243
|
}
|
|
2244
|
+
.disabled\:opacity-50 {
|
|
2245
|
+
&:disabled {
|
|
2246
|
+
opacity: 50%;
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2248
2249
|
.disabled\:shadow-none {
|
|
2249
2250
|
&:disabled {
|
|
2250
2251
|
--tw-shadow: 0 0 #0000;
|
|
@@ -2507,11 +2508,6 @@
|
|
|
2507
2508
|
display: table-cell;
|
|
2508
2509
|
}
|
|
2509
2510
|
}
|
|
2510
|
-
.md\:w-84 {
|
|
2511
|
-
@media (width >= 48rem) {
|
|
2512
|
-
width: calc(var(--spacing) * 84);
|
|
2513
|
-
}
|
|
2514
|
-
}
|
|
2515
2511
|
.md\:max-w-xl {
|
|
2516
2512
|
@media (width >= 48rem) {
|
|
2517
2513
|
max-width: var(--container-xl);
|
|
@@ -2562,16 +2558,6 @@
|
|
|
2562
2558
|
width: calc(var(--spacing) * 72);
|
|
2563
2559
|
}
|
|
2564
2560
|
}
|
|
2565
|
-
.lg\:w-80 {
|
|
2566
|
-
@media (width >= 64rem) {
|
|
2567
|
-
width: calc(var(--spacing) * 80);
|
|
2568
|
-
}
|
|
2569
|
-
}
|
|
2570
|
-
.lg\:w-84 {
|
|
2571
|
-
@media (width >= 64rem) {
|
|
2572
|
-
width: calc(var(--spacing) * 84);
|
|
2573
|
-
}
|
|
2574
|
-
}
|
|
2575
2561
|
.lg\:max-w-screen-md {
|
|
2576
2562
|
@media (width >= 64rem) {
|
|
2577
2563
|
max-width: var(--breakpoint-md);
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|