llms-py 2.0.18__py3-none-any.whl → 2.0.33__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/index.html +17 -1
- llms/llms.json +1132 -1075
- llms/main.py +561 -103
- llms/ui/Analytics.mjs +115 -104
- llms/ui/App.mjs +81 -4
- llms/ui/Avatar.mjs +61 -4
- llms/ui/Brand.mjs +29 -11
- llms/ui/ChatPrompt.mjs +163 -16
- llms/ui/Main.mjs +177 -94
- llms/ui/ModelSelector.mjs +28 -10
- llms/ui/OAuthSignIn.mjs +92 -0
- llms/ui/ProviderStatus.mjs +12 -12
- llms/ui/Recents.mjs +13 -13
- llms/ui/SettingsDialog.mjs +65 -65
- llms/ui/Sidebar.mjs +24 -19
- llms/ui/SystemPromptEditor.mjs +5 -5
- llms/ui/SystemPromptSelector.mjs +26 -6
- llms/ui/Welcome.mjs +2 -2
- llms/ui/ai.mjs +69 -5
- llms/ui/app.css +548 -34
- llms/ui/lib/servicestack-vue.mjs +9 -9
- llms/ui/markdown.mjs +8 -8
- llms/ui/tailwind.input.css +2 -0
- llms/ui/threadStore.mjs +39 -0
- llms/ui/typography.css +54 -36
- {llms_py-2.0.18.dist-info → llms_py-2.0.33.dist-info}/METADATA +403 -47
- llms_py-2.0.33.dist-info/RECORD +48 -0
- {llms_py-2.0.18.dist-info → llms_py-2.0.33.dist-info}/licenses/LICENSE +1 -2
- llms/__pycache__/__init__.cpython-312.pyc +0 -0
- llms/__pycache__/__init__.cpython-313.pyc +0 -0
- llms/__pycache__/__init__.cpython-314.pyc +0 -0
- llms/__pycache__/__main__.cpython-312.pyc +0 -0
- llms/__pycache__/__main__.cpython-314.pyc +0 -0
- llms/__pycache__/llms.cpython-312.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_py-2.0.18.dist-info/RECORD +0 -56
- {llms_py-2.0.18.dist-info → llms_py-2.0.33.dist-info}/WHEEL +0 -0
- {llms_py-2.0.18.dist-info → llms_py-2.0.33.dist-info}/entry_points.txt +0 -0
- {llms_py-2.0.18.dist-info → llms_py-2.0.33.dist-info}/top_level.txt +0 -0
llms/ui/Avatar.mjs
CHANGED
|
@@ -1,13 +1,40 @@
|
|
|
1
|
-
import { computed, inject } from "vue"
|
|
1
|
+
import { computed, inject, ref, onMounted, onUnmounted } from "vue"
|
|
2
2
|
|
|
3
3
|
export default {
|
|
4
4
|
template:`
|
|
5
|
-
<div v-if="$ai.auth?.profileUrl"
|
|
6
|
-
<img
|
|
5
|
+
<div v-if="$ai.auth?.profileUrl" class="relative" ref="avatarContainer">
|
|
6
|
+
<img
|
|
7
|
+
@click.stop="toggleMenu"
|
|
8
|
+
:src="$ai.auth.profileUrl"
|
|
9
|
+
:title="authTitle"
|
|
10
|
+
class="size-8 rounded-full cursor-pointer hover:ring-2 hover:ring-gray-300"
|
|
11
|
+
/>
|
|
12
|
+
<div
|
|
13
|
+
v-if="showMenu"
|
|
14
|
+
@click.stop
|
|
15
|
+
class="absolute right-0 mt-2 w-48 bg-white dark:bg-gray-800 rounded-md shadow-lg py-1 z-50 border border-gray-200 dark:border-gray-700"
|
|
16
|
+
>
|
|
17
|
+
<div class="px-4 py-2 text-sm text-gray-700 dark:text-gray-300 border-b border-gray-200 dark:border-gray-700">
|
|
18
|
+
<div class="font-medium whitespace-nowrap overflow-hidden text-ellipsis">{{ $ai.auth.displayName || $ai.auth.userName }}</div>
|
|
19
|
+
<div class="text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap overflow-hidden text-ellipsis">{{ $ai.auth.email }}</div>
|
|
20
|
+
</div>
|
|
21
|
+
<button type="button"
|
|
22
|
+
@click="handleLogout"
|
|
23
|
+
class="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center whitespace-nowrap"
|
|
24
|
+
>
|
|
25
|
+
<svg class="w-4 h-4 mr-2 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
26
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"></path>
|
|
27
|
+
</svg>
|
|
28
|
+
Sign Out
|
|
29
|
+
</button>
|
|
30
|
+
</div>
|
|
7
31
|
</div>
|
|
8
32
|
`,
|
|
9
33
|
setup() {
|
|
10
34
|
const ai = inject('ai')
|
|
35
|
+
const showMenu = ref(false)
|
|
36
|
+
const avatarContainer = ref(null)
|
|
37
|
+
|
|
11
38
|
const authTitle = computed(() => {
|
|
12
39
|
if (!ai.auth) return ''
|
|
13
40
|
const { userId, userName, displayName, bearerToken, roles } = ai.auth
|
|
@@ -20,9 +47,39 @@ export default {
|
|
|
20
47
|
]
|
|
21
48
|
return sb.filter(x => x).join('\n')
|
|
22
49
|
})
|
|
23
|
-
|
|
50
|
+
|
|
51
|
+
function toggleMenu() {
|
|
52
|
+
showMenu.value = !showMenu.value
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function handleLogout() {
|
|
56
|
+
showMenu.value = false
|
|
57
|
+
await ai.signOut()
|
|
58
|
+
// Reload the page to show sign-in screen
|
|
59
|
+
window.location.reload()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Close menu when clicking outside
|
|
63
|
+
const handleClickOutside = (event) => {
|
|
64
|
+
if (showMenu.value && avatarContainer.value && !avatarContainer.value.contains(event.target)) {
|
|
65
|
+
showMenu.value = false
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
onMounted(() => {
|
|
70
|
+
document.addEventListener('click', handleClickOutside)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
onUnmounted(() => {
|
|
74
|
+
document.removeEventListener('click', handleClickOutside)
|
|
75
|
+
})
|
|
76
|
+
|
|
24
77
|
return {
|
|
25
78
|
authTitle,
|
|
79
|
+
handleLogout,
|
|
80
|
+
showMenu,
|
|
81
|
+
toggleMenu,
|
|
82
|
+
avatarContainer,
|
|
26
83
|
}
|
|
27
84
|
}
|
|
28
85
|
}
|
llms/ui/Brand.mjs
CHANGED
|
@@ -1,19 +1,37 @@
|
|
|
1
1
|
export default {
|
|
2
2
|
template:`
|
|
3
|
-
<div class="flex-shrink-0
|
|
3
|
+
<div class="flex-shrink-0 pl-2 pr-4 py-4 border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 min-h-16 select-none">
|
|
4
4
|
<div class="flex items-center justify-between">
|
|
5
|
-
<
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
5
|
+
<div class="flex items-center space-x-2">
|
|
6
|
+
<button type="button"
|
|
7
|
+
@click="$emit('toggle-sidebar')"
|
|
8
|
+
class="group relative text-gray-500 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 focus:outline-none transition-colors"
|
|
9
|
+
title="Collapse sidebar"
|
|
10
|
+
>
|
|
11
|
+
<div class="relative size-5">
|
|
12
|
+
<!-- Default sidebar icon -->
|
|
13
|
+
<svg class="absolute inset-0 group-hover:hidden" 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">
|
|
14
|
+
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
|
15
|
+
<line x1="9" y1="3" x2="9" y2="21"></line>
|
|
16
|
+
</svg>
|
|
17
|
+
<!-- Hover state: |← icon -->
|
|
18
|
+
<svg class="absolute inset-0 hidden group-hover:block" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="m10.071 4.929l1.414 1.414L6.828 11H16v2H6.828l4.657 4.657l-1.414 1.414L3 12zM18.001 19V5h2v14z"/></svg>
|
|
19
|
+
</div>
|
|
20
|
+
</button>
|
|
21
|
+
|
|
22
|
+
<button type="button"
|
|
23
|
+
@click="$emit('home')"
|
|
24
|
+
class="text-lg font-semibold text-gray-900 dark:text-gray-200 hover:text-blue-600 dark:hover:text-blue-400 focus:outline-none transition-colors"
|
|
25
|
+
title="Go back to initial state"
|
|
26
|
+
>
|
|
27
|
+
History
|
|
28
|
+
</button>
|
|
29
|
+
</div>
|
|
12
30
|
|
|
13
31
|
<div class="flex items-center space-x-2">
|
|
14
32
|
<button type="button"
|
|
15
33
|
@click="$emit('analytics')"
|
|
16
|
-
class="text-gray-900 hover:text-blue-600 focus:outline-none transition-colors"
|
|
34
|
+
class="text-gray-900 dark:text-gray-200 hover:text-blue-600 dark:hover:text-blue-400 focus:outline-none transition-colors"
|
|
17
35
|
title="Analytics"
|
|
18
36
|
>
|
|
19
37
|
<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>
|
|
@@ -21,7 +39,7 @@ export default {
|
|
|
21
39
|
|
|
22
40
|
<button type="button"
|
|
23
41
|
@click="$emit('new')"
|
|
24
|
-
class="text-gray-900 hover:text-blue-600 focus:outline-none transition-colors"
|
|
42
|
+
class="text-gray-900 dark:text-gray-200 hover:text-blue-600 dark:hover:text-blue-400 focus:outline-none transition-colors"
|
|
25
43
|
title="New Chat"
|
|
26
44
|
>
|
|
27
45
|
<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>
|
|
@@ -30,5 +48,5 @@ export default {
|
|
|
30
48
|
</div>
|
|
31
49
|
</div>
|
|
32
50
|
`,
|
|
33
|
-
emits:['home','new','analytics'],
|
|
51
|
+
emits:['home','new','analytics','toggle-sidebar'],
|
|
34
52
|
}
|
llms/ui/ChatPrompt.mjs
CHANGED
|
@@ -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
|
|
|
@@ -50,7 +64,7 @@ export default {
|
|
|
50
64
|
<button type="button"
|
|
51
65
|
@click="triggerFilePicker"
|
|
52
66
|
:disabled="isGenerating || !model"
|
|
53
|
-
class="size-8 flex items-center justify-center rounded-md border border-gray-300 text-gray-600 hover:bg-gray-50 disabled:text-gray-400 disabled:cursor-not-allowed"
|
|
67
|
+
class="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"
|
|
54
68
|
title="Attach image or audio">
|
|
55
69
|
<svg class="size-5" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 256 256">
|
|
56
70
|
<path d="M224,128a8,8,0,0,1-8,8H136v80a8,8,0,0,1-16,0V136H40a8,8,0,0,1,0-16h80V40a8,8,0,0,1,16,0v80h80A8,8,0,0,1,224,128Z"></path>
|
|
@@ -64,8 +78,8 @@ export default {
|
|
|
64
78
|
<div>
|
|
65
79
|
<button type="button" title="Settings" @click="showSettings = true"
|
|
66
80
|
:disabled="isGenerating || !model"
|
|
67
|
-
class="size-8 flex items-center justify-center rounded-md border border-gray-300 text-gray-600 hover:bg-gray-50 disabled:text-gray-400 disabled:cursor-not-allowed">
|
|
68
|
-
<svg class="size-4 text-gray-600 disabled:text-gray-400" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 256 256"><path d="M40,88H73a32,32,0,0,0,62,0h81a8,8,0,0,0,0-16H135a32,32,0,0,0-62,0H40a8,8,0,0,0,0,16Zm64-24A16,16,0,1,1,88,80,16,16,0,0,1,104,64ZM216,168H199a32,32,0,0,0-62,0H40a8,8,0,0,0,0,16h97a32,32,0,0,0,62,0h17a8,8,0,0,0,0-16Zm-48,24a16,16,0,1,1,16-16A16,16,0,0,1,168,192Z"></path></svg>
|
|
81
|
+
class="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">
|
|
82
|
+
<svg class="size-4 text-gray-600 dark:text-gray-400 disabled:text-gray-400" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 256 256"><path d="M40,88H73a32,32,0,0,0,62,0h81a8,8,0,0,0,0-16H135a32,32,0,0,0-62,0H40a8,8,0,0,0,0,16Zm64-24A16,16,0,1,1,88,80,16,16,0,0,1,104,64ZM216,168H199a32,32,0,0,0-62,0H40a8,8,0,0,0,0,16h97a32,32,0,0,0,62,0h17a8,8,0,0,0,0-16Zm-48,24a16,16,0,1,1,16-16A16,16,0,0,1,168,192Z"></path></svg>
|
|
69
83
|
</button>
|
|
70
84
|
</div>
|
|
71
85
|
</div>
|
|
@@ -73,38 +87,50 @@ export default {
|
|
|
73
87
|
<div class="flex-1">
|
|
74
88
|
<div class="relative">
|
|
75
89
|
<textarea
|
|
76
|
-
ref="
|
|
90
|
+
ref="refMessage"
|
|
77
91
|
v-model="messageText"
|
|
78
92
|
@keydown.enter.exact.prevent="sendMessage"
|
|
79
93
|
@keydown.enter.shift.exact="addNewLine"
|
|
80
|
-
|
|
94
|
+
@paste="onPaste"
|
|
95
|
+
@dragover="onDragOver"
|
|
96
|
+
@dragleave="onDragLeave"
|
|
97
|
+
@drop="onDrop"
|
|
98
|
+
placeholder="Type message... (Enter to send, Shift+Enter for new line, drag & drop or paste files)"
|
|
81
99
|
rows="3"
|
|
82
|
-
class="
|
|
100
|
+
:class="[
|
|
101
|
+
'block w-full rounded-md border px-3 py-2 pr-12 text-sm text-gray-900 dark:text-gray-100 bg-white dark:bg-gray-900 placeholder-gray-500 dark:placeholder-gray-400 focus:outline-none focus:ring-1',
|
|
102
|
+
isDragging
|
|
103
|
+
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/30 ring-1 ring-blue-500'
|
|
104
|
+
: 'border-gray-300 dark:border-gray-600 focus:border-blue-500 focus:ring-blue-500'
|
|
105
|
+
]"
|
|
83
106
|
:disabled="isGenerating || !model"
|
|
84
107
|
></textarea>
|
|
85
|
-
<button title="Send (Enter)" type="button"
|
|
108
|
+
<button v-if="!isGenerating" title="Send (Enter)" type="button"
|
|
86
109
|
@click="sendMessage"
|
|
87
110
|
: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
|
|
90
|
-
|
|
91
|
-
|
|
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">
|
|
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>
|
|
92
119
|
</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
120
|
</button>
|
|
95
121
|
</div>
|
|
96
122
|
|
|
97
123
|
<!-- Attached files preview -->
|
|
98
124
|
<div v-if="attachedFiles.length" class="mt-2 flex flex-wrap gap-2">
|
|
99
|
-
<div v-for="(f,i) in attachedFiles" :key="i" class="flex items-center gap-2 px-2 py-1 rounded-md border border-gray-300 text-xs text-gray-700 bg-gray-50">
|
|
125
|
+
<div v-for="(f,i) in attachedFiles" :key="i" class="flex items-center gap-2 px-2 py-1 rounded-md border border-gray-300 dark:border-gray-600 text-xs text-gray-700 dark:text-gray-300 bg-gray-50 dark:bg-gray-800">
|
|
100
126
|
<span class="truncate max-w-48" :title="f.name">{{ f.name }}</span>
|
|
101
|
-
<button type="button" class="text-gray-500 hover:text-gray-700" @click="removeAttachment(i)" title="Remove Attachment">
|
|
127
|
+
<button type="button" class="text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200" @click="removeAttachment(i)" title="Remove Attachment">
|
|
102
128
|
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
|
|
103
129
|
</button>
|
|
104
130
|
</div>
|
|
105
131
|
</div>
|
|
106
132
|
|
|
107
|
-
<div v-if="!model" class="mt-2 text-sm text-red-600">
|
|
133
|
+
<div v-if="!model" class="mt-2 text-sm text-red-600 dark:text-red-400">
|
|
108
134
|
Please select a model
|
|
109
135
|
</div>
|
|
110
136
|
</div>
|
|
@@ -142,6 +168,7 @@ export default {
|
|
|
142
168
|
} = threads
|
|
143
169
|
|
|
144
170
|
const fileInput = ref(null)
|
|
171
|
+
const refMessage = ref(null)
|
|
145
172
|
const showSettings = ref(false)
|
|
146
173
|
const { applySettings } = chatSettings
|
|
147
174
|
|
|
@@ -169,6 +196,93 @@ export default {
|
|
|
169
196
|
attachedFiles.value.splice(i, 1)
|
|
170
197
|
}
|
|
171
198
|
|
|
199
|
+
// Helper function to add files and set default message
|
|
200
|
+
const addFilesAndSetMessage = (files) => {
|
|
201
|
+
if (files.length === 0) return
|
|
202
|
+
|
|
203
|
+
attachedFiles.value.push(...files)
|
|
204
|
+
|
|
205
|
+
// Set default message text if empty
|
|
206
|
+
if (!messageText.value.trim()) {
|
|
207
|
+
if (hasImage()) {
|
|
208
|
+
messageText.value = getTextContent(config.defaults.image)
|
|
209
|
+
} else if (hasAudio()) {
|
|
210
|
+
messageText.value = getTextContent(config.defaults.audio)
|
|
211
|
+
} else {
|
|
212
|
+
messageText.value = getTextContent(config.defaults.file)
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Handle paste events for clipboard images, audio, and files
|
|
218
|
+
const onPaste = async (e) => {
|
|
219
|
+
// Use the paste event's clipboardData directly (works best for paste events)
|
|
220
|
+
const items = e.clipboardData?.items
|
|
221
|
+
if (!items) return
|
|
222
|
+
|
|
223
|
+
const files = []
|
|
224
|
+
|
|
225
|
+
// Check all clipboard items
|
|
226
|
+
for (let i = 0; i < items.length; i++) {
|
|
227
|
+
const item = items[i]
|
|
228
|
+
|
|
229
|
+
// Handle files (images, audio, etc.)
|
|
230
|
+
if (item.kind === 'file') {
|
|
231
|
+
const file = item.getAsFile()
|
|
232
|
+
if (file) {
|
|
233
|
+
// Generate a better filename based on type
|
|
234
|
+
let filename = file.name
|
|
235
|
+
if (!filename || filename === 'image.png' || filename === 'blob') {
|
|
236
|
+
const ext = file.type.split('/')[1] || 'png'
|
|
237
|
+
const timestamp = new Date().getTime()
|
|
238
|
+
if (file.type.startsWith('image/')) {
|
|
239
|
+
filename = `pasted-image-${timestamp}.${ext}`
|
|
240
|
+
} else if (file.type.startsWith('audio/')) {
|
|
241
|
+
filename = `pasted-audio-${timestamp}.${ext}`
|
|
242
|
+
} else {
|
|
243
|
+
filename = `pasted-file-${timestamp}.${ext}`
|
|
244
|
+
}
|
|
245
|
+
// Create a new File object with the better name
|
|
246
|
+
files.push(new File([file], filename, { type: file.type }))
|
|
247
|
+
} else {
|
|
248
|
+
files.push(file)
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (files.length > 0) {
|
|
255
|
+
e.preventDefault()
|
|
256
|
+
addFilesAndSetMessage(files)
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Handle drag and drop events
|
|
261
|
+
const isDragging = ref(false)
|
|
262
|
+
|
|
263
|
+
const onDragOver = (e) => {
|
|
264
|
+
e.preventDefault()
|
|
265
|
+
e.stopPropagation()
|
|
266
|
+
isDragging.value = true
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const onDragLeave = (e) => {
|
|
270
|
+
e.preventDefault()
|
|
271
|
+
e.stopPropagation()
|
|
272
|
+
isDragging.value = false
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const onDrop = (e) => {
|
|
276
|
+
e.preventDefault()
|
|
277
|
+
e.stopPropagation()
|
|
278
|
+
isDragging.value = false
|
|
279
|
+
|
|
280
|
+
const files = Array.from(e.dataTransfer?.files || [])
|
|
281
|
+
if (files.length > 0) {
|
|
282
|
+
addFilesAndSetMessage(files)
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
172
286
|
function createChatRequest() {
|
|
173
287
|
if (hasImage()) {
|
|
174
288
|
return deepClone(config.defaults.image)
|
|
@@ -208,6 +322,10 @@ export default {
|
|
|
208
322
|
}
|
|
209
323
|
messageText.value = ''
|
|
210
324
|
|
|
325
|
+
// Create AbortController for this request
|
|
326
|
+
const controller = new AbortController()
|
|
327
|
+
chatPrompt.abortController.value = controller
|
|
328
|
+
|
|
211
329
|
try {
|
|
212
330
|
let threadId
|
|
213
331
|
|
|
@@ -338,11 +456,15 @@ export default {
|
|
|
338
456
|
}))
|
|
339
457
|
}
|
|
340
458
|
|
|
459
|
+
chatRequest.metadata ??= {}
|
|
460
|
+
chatRequest.metadata.threadId = threadId
|
|
461
|
+
|
|
341
462
|
// Send to API
|
|
342
463
|
console.debug('chatRequest', chatRequest)
|
|
343
464
|
const startTime = Date.now()
|
|
344
465
|
const response = await ai.post('/v1/chat/completions', {
|
|
345
|
-
body: JSON.stringify(chatRequest)
|
|
466
|
+
body: JSON.stringify(chatRequest),
|
|
467
|
+
signal: controller.signal
|
|
346
468
|
})
|
|
347
469
|
|
|
348
470
|
let result = null
|
|
@@ -417,11 +539,29 @@ export default {
|
|
|
417
539
|
attachedFiles.value = []
|
|
418
540
|
// Error will be cleared when user sends next message (no auto-timeout)
|
|
419
541
|
}
|
|
542
|
+
} catch (error) {
|
|
543
|
+
// Check if the error is due to abort
|
|
544
|
+
if (error.name === 'AbortError') {
|
|
545
|
+
console.log('Request was cancelled by user')
|
|
546
|
+
// Don't show error for cancelled requests
|
|
547
|
+
} else {
|
|
548
|
+
// Re-throw other errors to be handled by outer catch
|
|
549
|
+
throw error
|
|
550
|
+
}
|
|
420
551
|
} finally {
|
|
421
552
|
isGenerating.value = false
|
|
553
|
+
chatPrompt.abortController.value = null
|
|
554
|
+
// Restore focus to the textarea
|
|
555
|
+
nextTick(() => {
|
|
556
|
+
refMessage.value?.focus()
|
|
557
|
+
})
|
|
422
558
|
}
|
|
423
559
|
}
|
|
424
560
|
|
|
561
|
+
const cancelRequest = () => {
|
|
562
|
+
chatPrompt.cancel()
|
|
563
|
+
}
|
|
564
|
+
|
|
425
565
|
const addNewLine = () => {
|
|
426
566
|
// Enter key already adds new line
|
|
427
567
|
//messageText.value += '\n'
|
|
@@ -432,11 +572,18 @@ export default {
|
|
|
432
572
|
attachedFiles,
|
|
433
573
|
messageText,
|
|
434
574
|
fileInput,
|
|
575
|
+
refMessage,
|
|
435
576
|
showSettings,
|
|
577
|
+
isDragging,
|
|
436
578
|
triggerFilePicker,
|
|
437
579
|
onFilesSelected,
|
|
580
|
+
onPaste,
|
|
581
|
+
onDragOver,
|
|
582
|
+
onDragLeave,
|
|
583
|
+
onDrop,
|
|
438
584
|
removeAttachment,
|
|
439
585
|
sendMessage,
|
|
586
|
+
cancelRequest,
|
|
440
587
|
addNewLine,
|
|
441
588
|
}
|
|
442
589
|
}
|