llms-py 2.0.15__py3-none-any.whl → 2.0.16__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/__pycache__/__init__.cpython-313.pyc +0 -0
- llms/__pycache__/__init__.cpython-314.pyc +0 -0
- llms/__pycache__/__main__.cpython-314.pyc +0 -0
- llms/__pycache__/main.cpython-312.pyc +0 -0
- llms/__pycache__/main.cpython-313.pyc +0 -0
- llms/__pycache__/main.cpython-314.pyc +0 -0
- llms/index.html +5 -1
- llms/llms.json +721 -66
- llms/main.py +203 -11
- llms/ui/Analytics.mjs +1483 -0
- llms/ui/Brand.mjs +19 -8
- llms/ui/ChatPrompt.mjs +58 -36
- llms/ui/Main.mjs +205 -5
- llms/ui/ModelSelector.mjs +35 -4
- llms/ui/ProviderIcon.mjs +29 -0
- llms/ui/Sidebar.mjs +20 -4
- llms/ui/ai.mjs +1 -1
- llms/ui/app.css +211 -64
- llms/ui/lib/chart.js +14 -0
- llms/ui/lib/charts.mjs +20 -0
- llms/ui/lib/color.js +14 -0
- llms/ui/tailwind.input.css +1 -1
- llms/ui/threadStore.mjs +270 -19
- llms/ui/utils.mjs +36 -0
- {llms_py-2.0.15.dist-info → llms_py-2.0.16.dist-info}/METADATA +8 -35
- llms_py-2.0.16.dist-info/RECORD +56 -0
- llms_py-2.0.15.dist-info/RECORD +0 -46
- {llms_py-2.0.15.dist-info → llms_py-2.0.16.dist-info}/WHEEL +0 -0
- {llms_py-2.0.15.dist-info → llms_py-2.0.16.dist-info}/entry_points.txt +0 -0
- {llms_py-2.0.15.dist-info → llms_py-2.0.16.dist-info}/licenses/LICENSE +0 -0
- {llms_py-2.0.15.dist-info → llms_py-2.0.16.dist-info}/top_level.txt +0 -0
llms/ui/Brand.mjs
CHANGED
|
@@ -9,15 +9,26 @@ export default {
|
|
|
9
9
|
>
|
|
10
10
|
History
|
|
11
11
|
</button>
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
12
|
+
|
|
13
|
+
<div class="flex items-center space-x-2">
|
|
14
|
+
<button type="button"
|
|
15
|
+
@click="$emit('analytics')"
|
|
16
|
+
class="text-gray-900 hover:text-blue-600 focus:outline-none transition-colors"
|
|
17
|
+
title="Analytics"
|
|
18
|
+
>
|
|
19
|
+
<svg class="size-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M5 22a1 1 0 0 1-1-1v-8a1 1 0 0 1 2 0v8a1 1 0 0 1-1 1m5 0a1 1 0 0 1-1-1V3a1 1 0 0 1 2 0v18a1 1 0 0 1-1 1m5 0a1 1 0 0 1-1-1V9a1 1 0 0 1 2 0v12a1 1 0 0 1-1 1m5 0a1 1 0 0 1-1-1v-4a1 1 0 0 1 2 0v4a1 1 0 0 1-1 1"/></svg>
|
|
20
|
+
</button>
|
|
21
|
+
|
|
22
|
+
<button type="button"
|
|
23
|
+
@click="$emit('new')"
|
|
24
|
+
class="text-gray-900 hover:text-blue-600 focus:outline-none transition-colors"
|
|
25
|
+
title="New Chat"
|
|
26
|
+
>
|
|
27
|
+
<svg class="size-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path d="M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.375 2.625a1 1 0 0 1 3 3l-9.013 9.014a2 2 0 0 1-.853.505l-2.873.84a.5.5 0 0 1-.62-.62l.84-2.873a2 2 0 0 1 .506-.852z"/></g></svg>
|
|
28
|
+
</button>
|
|
29
|
+
</div>
|
|
19
30
|
</div>
|
|
20
31
|
</div>
|
|
21
32
|
`,
|
|
22
|
-
emits:['home','new'],
|
|
33
|
+
emits:['home','new','analytics'],
|
|
23
34
|
}
|
llms/ui/ChatPrompt.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { ref, nextTick, inject } from 'vue'
|
|
1
|
+
import { ref, nextTick, inject, unref } from 'vue'
|
|
2
2
|
import { useRouter } from 'vue-router'
|
|
3
3
|
import { lastRightPart } from '@servicestack/client'
|
|
4
|
-
import { deepClone, fileToDataUri, fileToBase64, addCopyButtons } from './utils.mjs'
|
|
4
|
+
import { deepClone, fileToDataUri, fileToBase64, addCopyButtons, toModelInfo, tokenCost } from './utils.mjs'
|
|
5
5
|
|
|
6
6
|
const imageExts = 'png,webp,jpg,jpeg,gif,bmp,svg,tiff,ico'.split(',')
|
|
7
7
|
const audioExts = 'mp3,wav,ogg,flac,m4a,opus,webm'.split(',')
|
|
@@ -71,16 +71,28 @@ export default {
|
|
|
71
71
|
</div>
|
|
72
72
|
|
|
73
73
|
<div class="flex-1">
|
|
74
|
-
<
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
74
|
+
<div class="relative">
|
|
75
|
+
<textarea
|
|
76
|
+
ref="messageInput"
|
|
77
|
+
v-model="messageText"
|
|
78
|
+
@keydown.enter.exact.prevent="sendMessage"
|
|
79
|
+
@keydown.enter.shift.exact="addNewLine"
|
|
80
|
+
placeholder="Type your message... (Enter to send, Shift+Enter for new line)"
|
|
81
|
+
rows="3"
|
|
82
|
+
class="block w-full rounded-md border border-gray-300 px-3 py-2 pr-12 text-sm placeholder-gray-500 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
|
83
|
+
:disabled="isGenerating || !model"
|
|
84
|
+
></textarea>
|
|
85
|
+
<button title="Send (Enter)" type="button"
|
|
86
|
+
@click="sendMessage"
|
|
87
|
+
:disabled="!messageText.trim() || isGenerating || !model"
|
|
88
|
+
class="absolute bottom-2 right-2 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 disabled:border-gray-200 transition-colors">
|
|
89
|
+
<svg v-if="isGenerating" class="size-5 animate-spin" fill="none" viewBox="0 0 24 24">
|
|
90
|
+
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
91
|
+
<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>
|
|
92
|
+
</svg>
|
|
93
|
+
<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>
|
|
94
|
+
</button>
|
|
95
|
+
</div>
|
|
84
96
|
|
|
85
97
|
<!-- Attached files preview -->
|
|
86
98
|
<div v-if="attachedFiles.length" class="mt-2 flex flex-wrap gap-2">
|
|
@@ -96,19 +108,6 @@ export default {
|
|
|
96
108
|
Please select a model
|
|
97
109
|
</div>
|
|
98
110
|
</div>
|
|
99
|
-
|
|
100
|
-
<div class="pt-3">
|
|
101
|
-
<button title="Send (Enter)" type="button"
|
|
102
|
-
@click="sendMessage"
|
|
103
|
-
:disabled="!messageText.trim() || isGenerating || !model"
|
|
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">
|
|
106
|
-
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
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>
|
|
108
|
-
</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>
|
|
110
|
-
</button>
|
|
111
|
-
</div>
|
|
112
111
|
</div>
|
|
113
112
|
</div>
|
|
114
113
|
`,
|
|
@@ -222,21 +221,28 @@ export default {
|
|
|
222
221
|
threadId = currentThread.value.id
|
|
223
222
|
// Update the existing thread's model and systemPrompt to match current selection
|
|
224
223
|
await threads.updateThread(threadId, {
|
|
225
|
-
model: props.model,
|
|
224
|
+
model: props.model.id,
|
|
225
|
+
info: toModelInfo(props.model),
|
|
226
226
|
systemPrompt: props.systemPrompt
|
|
227
227
|
})
|
|
228
228
|
}
|
|
229
229
|
|
|
230
|
-
//
|
|
231
|
-
await threads.
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
})
|
|
230
|
+
// Get the thread to check for duplicates
|
|
231
|
+
let thread = await threads.getThread(threadId)
|
|
232
|
+
const lastMessage = thread.messages[thread.messages.length - 1]
|
|
233
|
+
const isDuplicate = lastMessage && lastMessage.role === 'user' && lastMessage.content === message
|
|
235
234
|
|
|
236
|
-
|
|
235
|
+
// Add user message only if it's not a duplicate
|
|
236
|
+
if (!isDuplicate) {
|
|
237
|
+
await threads.addMessageToThread(threadId, {
|
|
238
|
+
role: 'user',
|
|
239
|
+
content: message
|
|
240
|
+
})
|
|
241
|
+
// Reload thread after adding message
|
|
242
|
+
thread = await threads.getThread(threadId)
|
|
243
|
+
}
|
|
237
244
|
|
|
238
|
-
|
|
239
|
-
const thread = await threads.getThread(threadId)
|
|
245
|
+
isGenerating.value = true
|
|
240
246
|
const messages = [...thread.messages]
|
|
241
247
|
|
|
242
248
|
// Add system prompt if present
|
|
@@ -250,7 +256,7 @@ export default {
|
|
|
250
256
|
}
|
|
251
257
|
|
|
252
258
|
const chatRequest = createChatRequest()
|
|
253
|
-
chatRequest.model = props.model
|
|
259
|
+
chatRequest.model = props.model.id
|
|
254
260
|
|
|
255
261
|
// Apply user settings
|
|
256
262
|
applySettings(chatRequest)
|
|
@@ -334,6 +340,7 @@ export default {
|
|
|
334
340
|
|
|
335
341
|
// Send to API
|
|
336
342
|
console.debug('chatRequest', chatRequest)
|
|
343
|
+
const startTime = Date.now()
|
|
337
344
|
const response = await ai.post('/v1/chat/completions', {
|
|
338
345
|
body: JSON.stringify(chatRequest)
|
|
339
346
|
})
|
|
@@ -369,6 +376,7 @@ export default {
|
|
|
369
376
|
} else {
|
|
370
377
|
try {
|
|
371
378
|
result = await response.json()
|
|
379
|
+
console.debug('chatResponse', JSON.stringify(result, null, 2))
|
|
372
380
|
} catch (e) {
|
|
373
381
|
errorStatus.value = {
|
|
374
382
|
errorCode: 'Error',
|
|
@@ -388,7 +396,21 @@ export default {
|
|
|
388
396
|
if (!errorStatus.value) {
|
|
389
397
|
// Add assistant response (save entire message including reasoning)
|
|
390
398
|
const assistantMessage = result.choices?.[0]?.message
|
|
391
|
-
|
|
399
|
+
|
|
400
|
+
const usage = result.usage
|
|
401
|
+
if (usage) {
|
|
402
|
+
if (result.metadata?.pricing) {
|
|
403
|
+
const [ input, output ] = result.metadata.pricing.split('/')
|
|
404
|
+
usage.duration = result.metadata.duration ?? (Date.now() - startTime)
|
|
405
|
+
usage.input = input
|
|
406
|
+
usage.output = output
|
|
407
|
+
usage.tokens = usage.completion_tokens
|
|
408
|
+
usage.price = usage.output
|
|
409
|
+
usage.cost = tokenCost(usage.prompt_tokens * parseFloat(input) + usage.completion_tokens * parseFloat(output))
|
|
410
|
+
}
|
|
411
|
+
await threads.logRequest(threadId, props.model, chatRequest, result)
|
|
412
|
+
}
|
|
413
|
+
await threads.addMessageToThread(threadId, assistantMessage, usage)
|
|
392
414
|
|
|
393
415
|
nextTick(addCopyButtons)
|
|
394
416
|
|
llms/ui/Main.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { ref, computed, nextTick, watch, onMounted, provide, inject } from 'vue'
|
|
2
2
|
import { useRouter, useRoute } from 'vue-router'
|
|
3
|
+
import { useFormatters } from '@servicestack/vue'
|
|
3
4
|
import { useThreadStore } from './threadStore.mjs'
|
|
4
|
-
import { storageObject, addCopyButtons } from './utils.mjs'
|
|
5
|
+
import { storageObject, addCopyButtons, formatCost, statsTitle } from './utils.mjs'
|
|
5
6
|
import { renderMarkdown } from './markdown.mjs'
|
|
6
7
|
import ChatPrompt, { useChatPrompt } from './ChatPrompt.mjs'
|
|
7
8
|
import SignIn from './SignIn.mjs'
|
|
@@ -12,6 +13,8 @@ import SystemPromptEditor from './SystemPromptEditor.mjs'
|
|
|
12
13
|
import { useSettings } from "./SettingsDialog.mjs"
|
|
13
14
|
import Welcome from './Welcome.mjs'
|
|
14
15
|
|
|
16
|
+
const { humanifyMs, humanifyNumber } = useFormatters()
|
|
17
|
+
|
|
15
18
|
export default {
|
|
16
19
|
components: {
|
|
17
20
|
ModelSelector,
|
|
@@ -140,11 +143,12 @@ export default {
|
|
|
140
143
|
<button
|
|
141
144
|
type="button"
|
|
142
145
|
@click="copyMessageContent(message)"
|
|
143
|
-
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-
|
|
146
|
+
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-0"
|
|
144
147
|
:class="message.role === 'user' ? 'text-white/70 hover:text-white hover:bg-white/20' : 'text-gray-500 hover:text-gray-700'"
|
|
145
148
|
title="Copy message content"
|
|
146
149
|
>
|
|
147
|
-
<svg class="
|
|
150
|
+
<svg v-if="copying === message" class="size-4 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg>
|
|
151
|
+
<svg v-else class="size-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
148
152
|
<rect width="14" height="14" x="8" y="8" rx="2" ry="2"/>
|
|
149
153
|
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/>
|
|
150
154
|
</svg>
|
|
@@ -170,9 +174,41 @@ export default {
|
|
|
170
174
|
|
|
171
175
|
<div v-if="message.role !== 'assistant'" class="whitespace-pre-wrap">{{ message.content }}</div>
|
|
172
176
|
<div class="mt-2 text-xs opacity-70">
|
|
173
|
-
{{ formatTime(message.timestamp) }}
|
|
177
|
+
<span>{{ formatTime(message.timestamp) }}</span>
|
|
178
|
+
<span v-if="message.usage" :title="tokensTitle(message.usage)">
|
|
179
|
+
•
|
|
180
|
+
{{ humanifyNumber(message.usage.tokens) }} tokens
|
|
181
|
+
<span v-if="message.usage.cost">· {{ message.usage.cost }}</span>
|
|
182
|
+
<span v-if="message.usage.duration"> in {{ humanifyMs(message.usage.duration) }}</span>
|
|
183
|
+
</span>
|
|
174
184
|
</div>
|
|
175
185
|
</div>
|
|
186
|
+
|
|
187
|
+
<!-- Edit and Redo buttons (shown on hover for user messages, outside bubble) -->
|
|
188
|
+
<div v-if="message.role === 'user'" class="flex flex-col gap-2 opacity-0 group-hover:opacity-100 transition-opacity mt-1">
|
|
189
|
+
<button type="button" @click.stop="editMessage(message)"
|
|
190
|
+
class="text-xs px-2 py-1 rounded text-gray-400 hover:text-green-600 hover:bg-green-50 transition-all"
|
|
191
|
+
title="Edit message">
|
|
192
|
+
<svg class="size-4 inline mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
193
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
|
|
194
|
+
</svg>
|
|
195
|
+
Edit
|
|
196
|
+
</button>
|
|
197
|
+
<button type="button" @click.stop="redoMessage(message)"
|
|
198
|
+
class="text-xs px-2 py-1 rounded text-gray-400 hover:text-blue-600 hover:bg-blue-50 transition-all"
|
|
199
|
+
title="Redo message (clears all responses after this message and re-runs it)">
|
|
200
|
+
<svg class="size-4 inline mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
201
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
|
|
202
|
+
</svg>
|
|
203
|
+
Redo
|
|
204
|
+
</button>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
|
|
208
|
+
<div v-if="currentThread.stats && currentThread.stats.outputTokens" class="text-center text-gray-500 text-sm">
|
|
209
|
+
<span :title="statsTitle(currentThread.stats)">
|
|
210
|
+
{{ currentThread.stats.cost ? formatCost(currentThread.stats.cost) + ' for ' : '' }} {{ humanifyNumber(currentThread.stats.inputTokens) }} → {{ humanifyNumber(currentThread.stats.outputTokens) }} tokens over {{ currentThread.stats.requests }} request{{currentThread.stats.requests===1?'':'s'}} in {{ humanifyMs(currentThread.stats.duration) }}
|
|
211
|
+
</span>
|
|
176
212
|
</div>
|
|
177
213
|
|
|
178
214
|
<!-- Loading indicator -->
|
|
@@ -226,6 +262,29 @@ export default {
|
|
|
226
262
|
</div>
|
|
227
263
|
</div>
|
|
228
264
|
</div>
|
|
265
|
+
|
|
266
|
+
<!-- Edit message modal -->
|
|
267
|
+
<div v-if="editingMessageId" class="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
|
268
|
+
<div class="relative bg-white rounded-lg shadow-lg p-6 max-w-2xl w-full mx-4">
|
|
269
|
+
<CloseButton @click="cancelEdit" class="" />
|
|
270
|
+
<h3 class="text-lg font-semibold text-gray-900 mb-4">Edit Message</h3>
|
|
271
|
+
<textarea
|
|
272
|
+
v-model="editingMessageContent"
|
|
273
|
+
class="w-full h-40 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none"
|
|
274
|
+
placeholder="Edit your message..."
|
|
275
|
+
></textarea>
|
|
276
|
+
<div class="mt-4 flex gap-2 justify-end">
|
|
277
|
+
<button type="button" @click="cancelEdit"
|
|
278
|
+
class="px-4 py-2 rounded-md border border-gray-300 text-gray-700 hover:bg-gray-50 transition-all">
|
|
279
|
+
Cancel
|
|
280
|
+
</button>
|
|
281
|
+
<button type="button" @click="saveEditedMessage"
|
|
282
|
+
class="px-4 py-2 rounded-md bg-blue-600 text-white hover:bg-blue-700 transition-all">
|
|
283
|
+
Save
|
|
284
|
+
</button>
|
|
285
|
+
</div>
|
|
286
|
+
</div>
|
|
287
|
+
</div>
|
|
229
288
|
</div>
|
|
230
289
|
|
|
231
290
|
<!-- Input Area - only show when thread is selected -->
|
|
@@ -273,6 +332,10 @@ export default {
|
|
|
273
332
|
const isExporting = ref(false)
|
|
274
333
|
const isImporting = ref(false)
|
|
275
334
|
const fileInput = ref(null)
|
|
335
|
+
const copying = ref(null)
|
|
336
|
+
const editingMessageId = ref(null)
|
|
337
|
+
const editingMessageContent = ref('')
|
|
338
|
+
const editingMessage = ref(null)
|
|
276
339
|
|
|
277
340
|
// Auto-scroll to bottom when new messages arrive
|
|
278
341
|
const scrollToBottom = async () => {
|
|
@@ -421,7 +484,7 @@ export default {
|
|
|
421
484
|
updatedCount++
|
|
422
485
|
} else {
|
|
423
486
|
// Add new thread directly to IndexedDB
|
|
424
|
-
await threads.initDB()
|
|
487
|
+
//await threads.initDB()
|
|
425
488
|
const db = await threads.initDB()
|
|
426
489
|
const tx = db.transaction(['threads'], 'readwrite')
|
|
427
490
|
await tx.objectStore('threads').add({
|
|
@@ -490,6 +553,7 @@ export default {
|
|
|
490
553
|
// Copy message content to clipboard
|
|
491
554
|
const copyMessageContent = async (message) => {
|
|
492
555
|
try {
|
|
556
|
+
copying.value = message
|
|
493
557
|
await navigator.clipboard.writeText(message.content)
|
|
494
558
|
// Could add a toast notification here if desired
|
|
495
559
|
} catch (err) {
|
|
@@ -502,6 +566,129 @@ export default {
|
|
|
502
566
|
document.execCommand('copy')
|
|
503
567
|
document.body.removeChild(textArea)
|
|
504
568
|
}
|
|
569
|
+
setTimeout(() => { copying.value = null }, 2000)
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Redo a user message (clear all messages after it and re-run)
|
|
573
|
+
const redoMessage = async (message) => {
|
|
574
|
+
if (!currentThread.value || message.role !== 'user') return
|
|
575
|
+
|
|
576
|
+
try {
|
|
577
|
+
const threadId = currentThread.value.id
|
|
578
|
+
|
|
579
|
+
// Clear all messages after this one
|
|
580
|
+
await threads.redoMessageFromThread(threadId, message.id)
|
|
581
|
+
|
|
582
|
+
// Extract the actual message content (remove media indicators if present)
|
|
583
|
+
let messageContent = message.content
|
|
584
|
+
// Remove media indicators like [🖼️ filename] or [🔉 filename] or [📎 filename]
|
|
585
|
+
messageContent = messageContent.replace(/\n\n\[[🖼️🔉📎] [^\]]+\]$/, '')
|
|
586
|
+
|
|
587
|
+
// Set the message text in the chat prompt
|
|
588
|
+
chatPrompt.messageText.value = messageContent
|
|
589
|
+
|
|
590
|
+
// Clear any attached files since we're re-running
|
|
591
|
+
chatPrompt.attachedFiles.value = []
|
|
592
|
+
|
|
593
|
+
// Trigger send by simulating the send action
|
|
594
|
+
// We'll use a small delay to ensure the UI updates
|
|
595
|
+
await nextTick()
|
|
596
|
+
|
|
597
|
+
// Find the send button and click it
|
|
598
|
+
const sendButton = document.querySelector('button[title*="Send"]')
|
|
599
|
+
if (sendButton && !sendButton.disabled) {
|
|
600
|
+
sendButton.click()
|
|
601
|
+
}
|
|
602
|
+
} catch (error) {
|
|
603
|
+
console.error('Failed to redo message:', error)
|
|
604
|
+
errorStatus.value = {
|
|
605
|
+
errorCode: 'Error',
|
|
606
|
+
message: 'Failed to redo message: ' + error.message,
|
|
607
|
+
stackTrace: null
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// Edit a user message
|
|
613
|
+
const editMessage = (message) => {
|
|
614
|
+
if (!currentThread.value || message.role !== 'user') return
|
|
615
|
+
|
|
616
|
+
editingMessage.value = message
|
|
617
|
+
editingMessageId.value = message.id
|
|
618
|
+
// Extract the actual message content (remove media indicators if present)
|
|
619
|
+
let messageContent = message.content
|
|
620
|
+
messageContent = messageContent.replace(/\n\n\[[🖼️🔉📎] [^\]]+\]$/, '')
|
|
621
|
+
editingMessageContent.value = messageContent
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// Save edited message
|
|
625
|
+
const saveEditedMessage = async () => {
|
|
626
|
+
if (!currentThread.value || !editingMessage.value || !editingMessageContent.value.trim()) return
|
|
627
|
+
|
|
628
|
+
try {
|
|
629
|
+
const threadId = currentThread.value.id
|
|
630
|
+
const messageId = editingMessage.value.id
|
|
631
|
+
const updatedContent = editingMessageContent.value
|
|
632
|
+
|
|
633
|
+
// Update the message content
|
|
634
|
+
editingMessage.value.content = updatedContent
|
|
635
|
+
await threads.updateMessageInThread(threadId, messageId, { content: updatedContent })
|
|
636
|
+
|
|
637
|
+
// Clear editing state
|
|
638
|
+
editingMessageId.value = null
|
|
639
|
+
editingMessageContent.value = ''
|
|
640
|
+
editingMessage.value = null
|
|
641
|
+
|
|
642
|
+
// Now redo the message (clear all responses after it and re-run)
|
|
643
|
+
await nextTick()
|
|
644
|
+
await threads.redoMessageFromThread(threadId, messageId)
|
|
645
|
+
|
|
646
|
+
// Set the message text in the chat prompt
|
|
647
|
+
chatPrompt.messageText.value = updatedContent
|
|
648
|
+
|
|
649
|
+
// Clear any attached files since we're re-running
|
|
650
|
+
chatPrompt.attachedFiles.value = []
|
|
651
|
+
|
|
652
|
+
// Trigger send by simulating the send action
|
|
653
|
+
await nextTick()
|
|
654
|
+
|
|
655
|
+
// Find the send button and click it
|
|
656
|
+
const sendButton = document.querySelector('button[title*="Send"]')
|
|
657
|
+
if (sendButton && !sendButton.disabled) {
|
|
658
|
+
sendButton.click()
|
|
659
|
+
}
|
|
660
|
+
} catch (error) {
|
|
661
|
+
console.error('Failed to save edited message:', error)
|
|
662
|
+
errorStatus.value = {
|
|
663
|
+
errorCode: 'Error',
|
|
664
|
+
message: 'Failed to save edited message: ' + error.message,
|
|
665
|
+
stackTrace: null
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// Cancel editing
|
|
671
|
+
const cancelEdit = () => {
|
|
672
|
+
editingMessageId.value = null
|
|
673
|
+
editingMessageContent.value = ''
|
|
674
|
+
editingMessage.value = null
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function tokensTitle(usage) {
|
|
678
|
+
let title = []
|
|
679
|
+
if (usage.tokens && usage.price) {
|
|
680
|
+
const msg = parseFloat(usage.price) > 0
|
|
681
|
+
? `${usage.tokens} tokens @ ${usage.price} = ${tokenCost(usage.price, usage.tokens)}`
|
|
682
|
+
: `${usage.tokens} tokens`
|
|
683
|
+
const duration = usage.duration ? ` in ${usage.duration}ms` : ''
|
|
684
|
+
title.push(msg + duration)
|
|
685
|
+
}
|
|
686
|
+
return title.join('\n')
|
|
687
|
+
}
|
|
688
|
+
const numFmt = new Intl.NumberFormat(undefined, { style: 'currency', currency: 'USD', minimumFractionDigits: 6 })
|
|
689
|
+
function tokenCost(price, tokens) {
|
|
690
|
+
if (!price || !tokens) return ''
|
|
691
|
+
return numFmt.format(parseFloat(price) * tokens)
|
|
505
692
|
}
|
|
506
693
|
|
|
507
694
|
onMounted(() => {
|
|
@@ -522,12 +709,20 @@ export default {
|
|
|
522
709
|
showSystemPrompt,
|
|
523
710
|
messagesContainer,
|
|
524
711
|
errorStatus,
|
|
712
|
+
copying,
|
|
713
|
+
editingMessageId,
|
|
714
|
+
editingMessageContent,
|
|
715
|
+
editingMessage,
|
|
525
716
|
formatTime,
|
|
526
717
|
renderMarkdown,
|
|
527
718
|
isReasoningExpanded,
|
|
528
719
|
toggleReasoning,
|
|
529
720
|
formatReasoning,
|
|
530
721
|
copyMessageContent,
|
|
722
|
+
redoMessage,
|
|
723
|
+
editMessage,
|
|
724
|
+
saveEditedMessage,
|
|
725
|
+
cancelEdit,
|
|
531
726
|
configUpdated,
|
|
532
727
|
exportThreads,
|
|
533
728
|
isExporting,
|
|
@@ -535,6 +730,11 @@ export default {
|
|
|
535
730
|
handleFileImport,
|
|
536
731
|
isImporting,
|
|
537
732
|
fileInput,
|
|
733
|
+
tokensTitle,
|
|
734
|
+
humanifyMs,
|
|
735
|
+
humanifyNumber,
|
|
736
|
+
formatCost,
|
|
737
|
+
statsTitle,
|
|
538
738
|
}
|
|
539
739
|
}
|
|
540
740
|
}
|
llms/ui/ModelSelector.mjs
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import ProviderStatus from "./ProviderStatus.mjs"
|
|
1
|
+
import ProviderStatus from "./ProviderStatus.mjs"
|
|
2
|
+
import ProviderIcon from "./ProviderIcon.mjs"
|
|
3
|
+
import { useFormatters } from "@servicestack/vue"
|
|
2
4
|
|
|
3
5
|
export default {
|
|
4
6
|
components: {
|
|
5
7
|
ProviderStatus,
|
|
8
|
+
ProviderIcon,
|
|
6
9
|
},
|
|
7
10
|
template:`
|
|
8
11
|
<!-- Model Selector -->
|
|
@@ -10,10 +13,26 @@ export default {
|
|
|
10
13
|
<Autocomplete id="model" :options="models" label=""
|
|
11
14
|
:modelValue="modelValue" @update:modelValue="$emit('update:modelValue', $event)"
|
|
12
15
|
class="w-72 xl:w-84"
|
|
13
|
-
:match="(x, value) => x.toLowerCase().includes(value.toLowerCase())"
|
|
16
|
+
:match="(x, value) => x.id.toLowerCase().includes(value.toLowerCase())"
|
|
14
17
|
placeholder="Select Model...">
|
|
15
|
-
<template #item="{
|
|
16
|
-
<div class="truncate max-w-72
|
|
18
|
+
<template #item="{ id, provider, provider_model, pricing }">
|
|
19
|
+
<div :key="id + provider + provider_model" class="group truncate max-w-72 flex justify-between">
|
|
20
|
+
<span :title="id">{{id}}</span>
|
|
21
|
+
<span class="flex items-center space-x-1">
|
|
22
|
+
<span v-if="pricing && (parseFloat(pricing.input) == 0 && parseFloat(pricing.input) == 0)">
|
|
23
|
+
<span class="text-xs text-gray-500" title="Free to use">FREE</span>
|
|
24
|
+
</span>
|
|
25
|
+
<span v-else-if="pricing" class="text-xs text-gray-500"
|
|
26
|
+
:title="'Estimated Cost per token: ' + pricing.input + ' input | ' + pricing.output + ' output'">
|
|
27
|
+
{{tokenPrice(pricing.input)}}
|
|
28
|
+
·
|
|
29
|
+
{{tokenPrice(pricing.output)}} M
|
|
30
|
+
</span>
|
|
31
|
+
<span :title="provider_model + ' from ' + provider">
|
|
32
|
+
<ProviderIcon :provider="provider" />
|
|
33
|
+
</span>
|
|
34
|
+
</span>
|
|
35
|
+
</div>
|
|
17
36
|
</template>
|
|
18
37
|
</Autocomplete>
|
|
19
38
|
<ProviderStatus @updated="$emit('updated', $event)" />
|
|
@@ -25,5 +44,17 @@ export default {
|
|
|
25
44
|
modelValue: String,
|
|
26
45
|
},
|
|
27
46
|
setup() {
|
|
47
|
+
|
|
48
|
+
const numFmt = new Intl.NumberFormat(undefined,{style:'currency',currency:'USD'})
|
|
49
|
+
|
|
50
|
+
function tokenPrice(price) {
|
|
51
|
+
if (!price) return ''
|
|
52
|
+
var ret = numFmt.format(parseFloat(price) * 1_000_000)
|
|
53
|
+
return ret.endsWith('.00') ? ret.slice(0, -3) : ret
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
tokenPrice
|
|
58
|
+
}
|
|
28
59
|
}
|
|
29
60
|
}
|
llms/ui/ProviderIcon.mjs
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
template:`
|
|
3
|
+
<svg v-if="matches(['openrouter'])" class="size-5" xmlns="http://www.w3.org/2000/svg" fill="#71717A" fill-rule="evenodd" viewBox="0 0 24 24"><path d="M16.804 1.957l7.22 4.105v.087L16.73 10.21l.017-2.117-.821-.03c-1.059-.028-1.611.002-2.268.11-1.064.175-2.038.577-3.147 1.352L8.345 11.03c-.284.195-.495.336-.68.455l-.515.322-.397.234.385.23.53.338c.476.314 1.17.796 2.701 1.866 1.11.775 2.083 1.177 3.147 1.352l.3.045c.694.091 1.375.094 2.825.033l.022-2.159 7.22 4.105v.087L16.589 22l.014-1.862-.635.022c-1.386.042-2.137.002-3.138-.162-1.694-.28-3.26-.926-4.881-2.059l-2.158-1.5a21.997 21.997 0 00-.755-.498l-.467-.28a55.927 55.927 0 00-.76-.43C2.908 14.73.563 14.116 0 14.116V9.888l.14.004c.564-.007 2.91-.622 3.809-1.124l1.016-.58.438-.274c.428-.28 1.072-.726 2.686-1.853 1.621-1.133 3.186-1.78 4.881-2.059 1.152-.19 1.974-.213 3.814-.138l.02-1.907z"></path></svg>
|
|
4
|
+
<svg v-else-if="matches(['anthropic','claude','haiku'])" class="size-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M16.765 5h-3.308l5.923 15h3.23zM7.226 5L1.38 20h3.308l1.307-3.154h6.154l1.23 3.077h3.309L10.688 5zm-.308 9.077l2-5.308l2.077 5.308z"/></svg>
|
|
5
|
+
<svg v-else-if="matches(['google','gemini'])" class="size-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="1.5" d="M3 12a9 9 0 0 0 9-9a9 9 0 0 0 9 9a9 9 0 0 0-9 9a9 9 0 0 0-9-9Z"/></svg>
|
|
6
|
+
<svg v-else-if="matches(['grok','xai'])" class="size-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" fill-rule="evenodd"><path d="M6.469 8.776L16.512 23h-4.464L2.005 8.776H6.47zm-.004 7.9l2.233 3.164L6.467 23H2l4.465-6.324zM22 2.582V23h-3.659V7.764L22 2.582zM22 1l-9.952 14.095-2.233-3.163L17.533 1H22z"></path></svg>
|
|
7
|
+
<svg v-else-if="matches(['zai','glm'])" class="size-5" xmlns="http://www.w3.org/2000/svg" fill="currentColor" fill-rule="evenodd" viewBox="0 0 24 24"><title>Z.ai</title><path d="M12.105 2L9.927 4.953H.653L2.83 2h9.276zM23.254 19.048L21.078 22h-9.242l2.174-2.952h9.244zM24 2L9.264 22H0L14.736 2H24z"></path></svg>
|
|
8
|
+
<svg v-else-if="matches(['qwen'])" class="size-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"><path d="M9 2h3.5L14 4.5h5.13L20.5 7m1.5 7.5l-1.645 2.663h-2.477L15 22h-3.217M5 20l-1.5-2.5l1-3l-2.5-5L4 7"/><path d="m19.19 9.662l1.31-2.661H10l1-2l-2-3l-2.251 5H4l5 10H6l-1 3h5.5l1.252 2l5.65-9.935L18.94 14.5H22z"/><path d="M12 15.5L9 10h6z"/></g></svg>
|
|
9
|
+
<svg v-else-if="matches(['mistral','codestral'])" class="size-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 233"><path d="M186.182 0h46.545v46.545h-46.545z"/><path fill="#f7d046" d="M209.455 0H256v46.545h-46.545z"/><path d="M0 0h46.545v46.545H0zm0 46.545h46.545V93.09H0zm0 46.546h46.545v46.545H0zm0 46.545h46.545v46.545H0zm0 46.546h46.545v46.545H0z"/><path fill="#f7d046" d="M23.273 0h46.545v46.545H23.273z"/><path fill="#f2a73b" d="M209.455 46.545H256V93.09h-46.545zm-186.182 0h46.545V93.09H23.273z"/><path d="M139.636 46.545h46.545V93.09h-46.545z"/><path fill="#f2a73b" d="M162.909 46.545h46.545V93.09h-46.545zm-93.091 0h46.545V93.09H69.818z"/><path fill="#ee792f" d="M116.364 93.091h46.545v46.545h-46.545zm46.545 0h46.545v46.545h-46.545zm-93.091 0h46.545v46.545H69.818z"/><path d="M93.091 139.636h46.545v46.545H93.091z"/><path fill="#eb5829" d="M116.364 139.636h46.545v46.545h-46.545z"/><path fill="#ee792f" d="M209.455 93.091H256v46.545h-46.545zm-186.182 0h46.545v46.545H23.273z"/><path d="M186.182 139.636h46.545v46.545h-46.545z"/><path fill="#eb5829" d="M209.455 139.636H256v46.545h-46.545z"/><path d="M186.182 186.182h46.545v46.545h-46.545z"/><path fill="#eb5829" d="M23.273 139.636h46.545v46.545H23.273z"/><path fill="#ea3326" d="M209.455 186.182H256v46.545h-46.545zm-186.182 0h46.545v46.545H23.273z"/></svg>
|
|
10
|
+
<svg v-else-if="matches(['groq'])" class="size-5" xmlns="http://www.w3.org/2000/svg" fill="currentColor" fill-rule="evenodd" viewBox="0 0 24 24"><path d="M12.036 2c-3.853-.035-7 3-7.036 6.781-.035 3.782 3.055 6.872 6.908 6.907h2.42v-2.566h-2.292c-2.407.028-4.38-1.866-4.408-4.23-.029-2.362 1.901-4.298 4.308-4.326h.1c2.407 0 4.358 1.915 4.365 4.278v6.305c0 2.342-1.944 4.25-4.323 4.279a4.375 4.375 0 01-3.033-1.252l-1.851 1.818A7 7 0 0012.029 22h.092c3.803-.056 6.858-3.083 6.879-6.816v-6.5C18.907 4.963 15.817 2 12.036 2z"></path></svg>
|
|
11
|
+
<svg v-else-if="matches(['llama'])" class="size-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" fill-rule="evenodd">
|
|
12
|
+
<path d="M7.905 1.09c.216.085.411.225.588.41.295.306.544.744.734 1.263.191.522.315 1.1.362 1.68a5.054 5.054 0 012.049-.636l.051-.004c.87-.07 1.73.087 2.48.474.101.053.2.11.297.17.05-.569.172-1.134.36-1.644.19-.52.439-.957.733-1.264a1.67 1.67 0 01.589-.41c.257-.1.53-.118.796-.042.401.114.745.368 1.016.737.248.337.434.769.561 1.287.23.934.27 2.163.115 3.645l.053.04.026.019c.757.576 1.284 1.397 1.563 2.35.435 1.487.216 3.155-.534 4.088l-.018.021.002.003c.417.762.67 1.567.724 2.4l.002.03c.064 1.065-.2 2.137-.814 3.19l-.007.01.01.024c.472 1.157.62 2.322.438 3.486l-.006.039a.651.651 0 01-.747.536.648.648 0 01-.54-.742c.167-1.033.01-2.069-.48-3.123a.643.643 0 01.04-.617l.004-.006c.604-.924.854-1.83.8-2.72-.046-.779-.325-1.544-.8-2.273a.644.644 0 01.18-.886l.009-.006c.243-.159.467-.565.58-1.12a4.229 4.229 0 00-.095-1.974c-.205-.7-.58-1.284-1.105-1.683-.595-.454-1.383-.673-2.38-.61a.653.653 0 01-.632-.371c-.314-.665-.772-1.141-1.343-1.436a3.288 3.288 0 00-1.772-.332c-1.245.099-2.343.801-2.67 1.686a.652.652 0 01-.61.425c-1.067.002-1.893.252-2.497.703-.522.39-.878.935-1.066 1.588a4.07 4.07 0 00-.068 1.886c.112.558.331 1.02.582 1.269l.008.007c.212.207.257.53.109.785-.36.622-.629 1.549-.673 2.44-.05 1.018.186 1.902.719 2.536l.016.019a.643.643 0 01.095.69c-.576 1.236-.753 2.252-.562 3.052a.652.652 0 01-1.269.298c-.243-1.018-.078-2.184.473-3.498l.014-.035-.008-.012a4.339 4.339 0 01-.598-1.309l-.005-.019a5.764 5.764 0 01-.177-1.785c.044-.91.278-1.842.622-2.59l.012-.026-.002-.002c-.293-.418-.51-.953-.63-1.545l-.005-.024a5.352 5.352 0 01.093-2.49c.262-.915.777-1.701 1.536-2.269.06-.045.123-.09.186-.132-.159-1.493-.119-2.73.112-3.67.127-.518.314-.95.562-1.287.27-.368.614-.622 1.015-.737.266-.076.54-.059.797.042zm4.116 9.09c.936 0 1.8.313 2.446.855.63.527 1.005 1.235 1.005 1.94 0 .888-.406 1.58-1.133 2.022-.62.375-1.451.557-2.403.557-1.009 0-1.871-.259-2.493-.734-.617-.47-.963-1.13-.963-1.845 0-.707.398-1.417 1.056-1.946.668-.537 1.55-.849 2.485-.849zm0 .896a3.07 3.07 0 00-1.916.65c-.461.37-.722.835-.722 1.25 0 .428.21.829.61 1.134.455.347 1.124.548 1.943.548.799 0 1.473-.147 1.932-.426.463-.28.7-.686.7-1.257 0-.423-.246-.89-.683-1.256-.484-.405-1.14-.643-1.864-.643zm.662 1.21l.004.004c.12.151.095.37-.056.49l-.292.23v.446a.375.375 0 01-.376.373.375.375 0 01-.376-.373v-.46l-.271-.218a.347.347 0 01-.052-.49.353.353 0 01.494-.051l.215.172.22-.174a.353.353 0 01.49.051zm-5.04-1.919c.478 0 .867.39.867.871a.87.87 0 01-.868.871.87.87 0 01-.867-.87.87.87 0 01.867-.872zm8.706 0c.48 0 .868.39.868.871a.87.87 0 01-.868.871.87.87 0 01-.867-.87.87.87 0 01.867-.872zM7.44 2.3l-.003.002a.659.659 0 00-.285.238l-.005.006c-.138.189-.258.467-.348.832-.17.692-.216 1.631-.124 2.782.43-.128.899-.208 1.404-.237l.01-.001.019-.034c.046-.082.095-.161.148-.239.123-.771.022-1.692-.253-2.444-.134-.364-.297-.65-.453-.813a.628.628 0 00-.107-.09L7.44 2.3zm9.174.04l-.002.001a.628.628 0 00-.107.09c-.156.163-.32.45-.453.814-.29.794-.387 1.776-.23 2.572l.058.097.008.014h.03a5.184 5.184 0 011.466.212c.086-1.124.038-2.043-.128-2.722-.09-.365-.21-.643-.349-.832l-.004-.006a.659.659 0 00-.285-.239h-.004z"></path>
|
|
13
|
+
</svg>
|
|
14
|
+
<svg v-else class="size-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linejoin="round" d="M12.019 16.225L8.35 14.13m3.669 2.096l3.65-2.129m-3.65 2.13L9.183 17.88l-5.196-3a5 5 0 0 1-.714-.498m5.077-.252L5.5 12.5v-6q0-.444.075-.867m2.775 8.496l-.018-4.225m5.97-6.652a5.001 5.001 0 0 0-8.727 2.38m8.727-2.38a5 5 0 0 0-.789.369l-5.196 3l.015 3.283m5.97-6.652a5.001 5.001 0 0 1 6.425 6.367M5.575 5.633a5.001 5.001 0 0 0-2.302 8.748m8.708-6.606l3.669 2.096m-3.67-2.096L8.33 9.904m3.65-2.129l2.836-1.654l5.196 3q.384.223.714.498m-5.077.252L18.5 11.5v6q0 .444-.075.867M15.65 9.871l.018 4.225m-5.97 6.652a5.001 5.001 0 0 0 8.727-2.38m-8.727 2.38a5 5 0 0 0 .789-.369l5.196-3l-.015-3.283m-5.97 6.652a5.001 5.001 0 0 1-6.425-6.367m15.152 3.986a5.001 5.001 0 0 0 2.302-8.748" stroke-width="1"/></svg>
|
|
15
|
+
`,
|
|
16
|
+
props: {
|
|
17
|
+
provider: String,
|
|
18
|
+
},
|
|
19
|
+
setup(props) {
|
|
20
|
+
function matches(providers) {
|
|
21
|
+
if (!props.provider) return false
|
|
22
|
+
const providerLower = props.provider.toLowerCase().replace(/[\s-.]+/g, '')
|
|
23
|
+
return providers.some(provider => providerLower.includes(provider))
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
matches
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|