llms-py 3.0.0__py3-none-any.whl → 3.0.0b2__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.
Files changed (195) hide show
  1. llms/__pycache__/main.cpython-314.pyc +0 -0
  2. llms/index.html +37 -26
  3. llms/llms.json +21 -70
  4. llms/main.py +731 -1426
  5. llms/providers.json +1 -1
  6. llms/{extensions/analytics/ui/index.mjs → ui/Analytics.mjs} +238 -154
  7. llms/ui/App.mjs +63 -133
  8. llms/ui/Avatar.mjs +86 -0
  9. llms/ui/Brand.mjs +52 -0
  10. llms/ui/ChatPrompt.mjs +597 -0
  11. llms/ui/Main.mjs +862 -0
  12. llms/ui/OAuthSignIn.mjs +61 -0
  13. llms/ui/ProviderIcon.mjs +36 -0
  14. llms/ui/ProviderStatus.mjs +104 -0
  15. llms/{extensions/app/ui → ui}/Recents.mjs +57 -82
  16. llms/ui/{modules/chat/SettingsDialog.mjs → SettingsDialog.mjs} +9 -9
  17. llms/{extensions/app/ui/index.mjs → ui/Sidebar.mjs} +57 -122
  18. llms/ui/SignIn.mjs +65 -0
  19. llms/ui/Welcome.mjs +8 -0
  20. llms/ui/ai.mjs +13 -117
  21. llms/ui/app.css +49 -1776
  22. llms/ui/index.mjs +171 -87
  23. llms/ui/lib/charts.mjs +13 -9
  24. llms/ui/lib/servicestack-vue.mjs +3 -3
  25. llms/ui/lib/vue.min.mjs +9 -10
  26. llms/ui/lib/vue.mjs +1602 -1763
  27. llms/ui/markdown.mjs +2 -10
  28. llms/ui/model-selector.mjs +686 -0
  29. llms/ui/tailwind.input.css +1 -55
  30. llms/ui/threadStore.mjs +583 -0
  31. llms/ui/utils.mjs +118 -113
  32. llms/ui.json +1069 -0
  33. {llms_py-3.0.0.dist-info → llms_py-3.0.0b2.dist-info}/METADATA +1 -1
  34. llms_py-3.0.0b2.dist-info/RECORD +58 -0
  35. llms/extensions/app/README.md +0 -20
  36. llms/extensions/app/__init__.py +0 -530
  37. llms/extensions/app/__pycache__/__init__.cpython-314.pyc +0 -0
  38. llms/extensions/app/__pycache__/db.cpython-314.pyc +0 -0
  39. llms/extensions/app/__pycache__/db_manager.cpython-314.pyc +0 -0
  40. llms/extensions/app/db.py +0 -644
  41. llms/extensions/app/db_manager.py +0 -195
  42. llms/extensions/app/requests.json +0 -9073
  43. llms/extensions/app/threads.json +0 -15290
  44. llms/extensions/app/ui/threadStore.mjs +0 -411
  45. llms/extensions/core_tools/CALCULATOR.md +0 -32
  46. llms/extensions/core_tools/__init__.py +0 -598
  47. llms/extensions/core_tools/__pycache__/__init__.cpython-314.pyc +0 -0
  48. llms/extensions/core_tools/ui/codemirror/addon/edit/closebrackets.js +0 -201
  49. llms/extensions/core_tools/ui/codemirror/addon/edit/closetag.js +0 -185
  50. llms/extensions/core_tools/ui/codemirror/addon/edit/continuelist.js +0 -101
  51. llms/extensions/core_tools/ui/codemirror/addon/edit/matchbrackets.js +0 -160
  52. llms/extensions/core_tools/ui/codemirror/addon/edit/matchtags.js +0 -66
  53. llms/extensions/core_tools/ui/codemirror/addon/edit/trailingspace.js +0 -27
  54. llms/extensions/core_tools/ui/codemirror/addon/selection/active-line.js +0 -72
  55. llms/extensions/core_tools/ui/codemirror/addon/selection/mark-selection.js +0 -119
  56. llms/extensions/core_tools/ui/codemirror/addon/selection/selection-pointer.js +0 -98
  57. llms/extensions/core_tools/ui/codemirror/doc/docs.css +0 -225
  58. llms/extensions/core_tools/ui/codemirror/doc/source_sans.woff +0 -0
  59. llms/extensions/core_tools/ui/codemirror/lib/codemirror.css +0 -344
  60. llms/extensions/core_tools/ui/codemirror/lib/codemirror.js +0 -9884
  61. llms/extensions/core_tools/ui/codemirror/mode/clike/clike.js +0 -942
  62. llms/extensions/core_tools/ui/codemirror/mode/javascript/index.html +0 -118
  63. llms/extensions/core_tools/ui/codemirror/mode/javascript/javascript.js +0 -962
  64. llms/extensions/core_tools/ui/codemirror/mode/javascript/typescript.html +0 -62
  65. llms/extensions/core_tools/ui/codemirror/mode/python/python.js +0 -402
  66. llms/extensions/core_tools/ui/codemirror/theme/dracula.css +0 -40
  67. llms/extensions/core_tools/ui/codemirror/theme/mocha.css +0 -135
  68. llms/extensions/core_tools/ui/index.mjs +0 -650
  69. llms/extensions/gallery/README.md +0 -61
  70. llms/extensions/gallery/__init__.py +0 -61
  71. llms/extensions/gallery/__pycache__/__init__.cpython-314.pyc +0 -0
  72. llms/extensions/gallery/__pycache__/db.cpython-314.pyc +0 -0
  73. llms/extensions/gallery/db.py +0 -298
  74. llms/extensions/gallery/ui/index.mjs +0 -482
  75. llms/extensions/katex/README.md +0 -39
  76. llms/extensions/katex/__init__.py +0 -6
  77. llms/extensions/katex/__pycache__/__init__.cpython-314.pyc +0 -0
  78. llms/extensions/katex/ui/README.md +0 -125
  79. llms/extensions/katex/ui/contrib/auto-render.js +0 -338
  80. llms/extensions/katex/ui/contrib/auto-render.min.js +0 -1
  81. llms/extensions/katex/ui/contrib/auto-render.mjs +0 -244
  82. llms/extensions/katex/ui/contrib/copy-tex.js +0 -127
  83. llms/extensions/katex/ui/contrib/copy-tex.min.js +0 -1
  84. llms/extensions/katex/ui/contrib/copy-tex.mjs +0 -105
  85. llms/extensions/katex/ui/contrib/mathtex-script-type.js +0 -109
  86. llms/extensions/katex/ui/contrib/mathtex-script-type.min.js +0 -1
  87. llms/extensions/katex/ui/contrib/mathtex-script-type.mjs +0 -24
  88. llms/extensions/katex/ui/contrib/mhchem.js +0 -3213
  89. llms/extensions/katex/ui/contrib/mhchem.min.js +0 -1
  90. llms/extensions/katex/ui/contrib/mhchem.mjs +0 -3109
  91. llms/extensions/katex/ui/contrib/render-a11y-string.js +0 -887
  92. llms/extensions/katex/ui/contrib/render-a11y-string.min.js +0 -1
  93. llms/extensions/katex/ui/contrib/render-a11y-string.mjs +0 -800
  94. llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.ttf +0 -0
  95. llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.woff +0 -0
  96. llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  97. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
  98. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
  99. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  100. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
  101. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
  102. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  103. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
  104. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.woff +0 -0
  105. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  106. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
  107. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.woff +0 -0
  108. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  109. llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.ttf +0 -0
  110. llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.woff +0 -0
  111. llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.woff2 +0 -0
  112. llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
  113. llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.woff +0 -0
  114. llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  115. llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.ttf +0 -0
  116. llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.woff +0 -0
  117. llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.woff2 +0 -0
  118. llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.ttf +0 -0
  119. llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.woff +0 -0
  120. llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.woff2 +0 -0
  121. llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
  122. llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.woff +0 -0
  123. llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  124. llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.ttf +0 -0
  125. llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.woff +0 -0
  126. llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.woff2 +0 -0
  127. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
  128. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.woff +0 -0
  129. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  130. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
  131. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.woff +0 -0
  132. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  133. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
  134. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.woff +0 -0
  135. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  136. llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.ttf +0 -0
  137. llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.woff +0 -0
  138. llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.woff2 +0 -0
  139. llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.ttf +0 -0
  140. llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.woff +0 -0
  141. llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  142. llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.ttf +0 -0
  143. llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.woff +0 -0
  144. llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  145. llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.ttf +0 -0
  146. llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.woff +0 -0
  147. llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  148. llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.ttf +0 -0
  149. llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.woff +0 -0
  150. llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  151. llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
  152. llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.woff +0 -0
  153. llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  154. llms/extensions/katex/ui/index.mjs +0 -92
  155. llms/extensions/katex/ui/katex-swap.css +0 -1230
  156. llms/extensions/katex/ui/katex-swap.min.css +0 -1
  157. llms/extensions/katex/ui/katex.css +0 -1230
  158. llms/extensions/katex/ui/katex.js +0 -19080
  159. llms/extensions/katex/ui/katex.min.css +0 -1
  160. llms/extensions/katex/ui/katex.min.js +0 -1
  161. llms/extensions/katex/ui/katex.min.mjs +0 -1
  162. llms/extensions/katex/ui/katex.mjs +0 -18547
  163. llms/extensions/providers/__init__.py +0 -18
  164. llms/extensions/providers/__pycache__/__init__.cpython-314.pyc +0 -0
  165. llms/extensions/providers/__pycache__/anthropic.cpython-314.pyc +0 -0
  166. llms/extensions/providers/__pycache__/chutes.cpython-314.pyc +0 -0
  167. llms/extensions/providers/__pycache__/google.cpython-314.pyc +0 -0
  168. llms/extensions/providers/__pycache__/nvidia.cpython-314.pyc +0 -0
  169. llms/extensions/providers/__pycache__/openai.cpython-314.pyc +0 -0
  170. llms/extensions/providers/__pycache__/openrouter.cpython-314.pyc +0 -0
  171. llms/extensions/providers/anthropic.py +0 -229
  172. llms/extensions/providers/chutes.py +0 -155
  173. llms/extensions/providers/google.py +0 -378
  174. llms/extensions/providers/nvidia.py +0 -105
  175. llms/extensions/providers/openai.py +0 -156
  176. llms/extensions/providers/openrouter.py +0 -72
  177. llms/extensions/system_prompts/README.md +0 -22
  178. llms/extensions/system_prompts/__init__.py +0 -45
  179. llms/extensions/system_prompts/__pycache__/__init__.cpython-314.pyc +0 -0
  180. llms/extensions/system_prompts/ui/index.mjs +0 -280
  181. llms/extensions/system_prompts/ui/prompts.json +0 -1067
  182. llms/extensions/tools/__init__.py +0 -5
  183. llms/extensions/tools/__pycache__/__init__.cpython-314.pyc +0 -0
  184. llms/extensions/tools/ui/index.mjs +0 -204
  185. llms/providers-extra.json +0 -356
  186. llms/ui/ctx.mjs +0 -365
  187. llms/ui/modules/chat/ChatBody.mjs +0 -691
  188. llms/ui/modules/chat/index.mjs +0 -828
  189. llms/ui/modules/layout.mjs +0 -243
  190. llms/ui/modules/model-selector.mjs +0 -851
  191. llms_py-3.0.0.dist-info/RECORD +0 -202
  192. {llms_py-3.0.0.dist-info → llms_py-3.0.0b2.dist-info}/WHEEL +0 -0
  193. {llms_py-3.0.0.dist-info → llms_py-3.0.0b2.dist-info}/entry_points.txt +0 -0
  194. {llms_py-3.0.0.dist-info → llms_py-3.0.0b2.dist-info}/licenses/LICENSE +0 -0
  195. {llms_py-3.0.0.dist-info → llms_py-3.0.0b2.dist-info}/top_level.txt +0 -0
llms/ui/ChatPrompt.mjs ADDED
@@ -0,0 +1,597 @@
1
+ import { ref, nextTick, inject, unref } from 'vue'
2
+ import { useRouter } from 'vue-router'
3
+ import { lastRightPart } from '@servicestack/client'
4
+ import { deepClone, fileToDataUri, fileToBase64, addCopyButtons, toModelInfo, tokenCost, uploadFile } from './utils.mjs'
5
+ import { toRaw } from 'vue'
6
+
7
+ const imageExts = 'png,webp,jpg,jpeg,gif,bmp,svg,tiff,ico'.split(',')
8
+ const audioExts = 'mp3,wav,ogg,flac,m4a,opus,webm'.split(',')
9
+
10
+ export function useChatPrompt() {
11
+ const messageText = ref('')
12
+ const attachedFiles = ref([])
13
+ const isGenerating = ref(false)
14
+ const errorStatus = ref(null)
15
+ const abortController = ref(null)
16
+ const hasImage = () => attachedFiles.value.some(f => imageExts.includes(lastRightPart(f.name, '.')))
17
+ const hasAudio = () => attachedFiles.value.some(f => audioExts.includes(lastRightPart(f.name, '.')))
18
+ const hasFile = () => attachedFiles.value.length > 0
19
+ // const hasText = () => !hasImage() && !hasAudio() && !hasFile()
20
+
21
+ const editingMessageId = ref(null)
22
+
23
+ function reset() {
24
+ // Ensure initial state is ready to accept input
25
+ isGenerating.value = false
26
+ attachedFiles.value = []
27
+ messageText.value = ''
28
+ abortController.value = null
29
+ editingMessageId.value = null
30
+ }
31
+
32
+ function cancel() {
33
+ // Cancel the pending request
34
+ if (abortController.value) {
35
+ abortController.value.abort()
36
+ }
37
+ // Reset UI state
38
+ isGenerating.value = false
39
+ abortController.value = null
40
+ }
41
+
42
+ return {
43
+ messageText,
44
+ attachedFiles,
45
+ errorStatus,
46
+ isGenerating,
47
+ abortController,
48
+ editingMessageId,
49
+ get generating() {
50
+ return isGenerating.value
51
+ },
52
+ hasImage,
53
+ hasAudio,
54
+ hasFile,
55
+ // hasText,
56
+ reset,
57
+ cancel,
58
+ }
59
+ }
60
+
61
+ export default {
62
+ template: `
63
+ <div class="mx-auto max-w-3xl">
64
+ <SettingsDialog :isOpen="showSettings" @close="showSettings = false" />
65
+ <div class="flex space-x-2">
66
+ <!-- Attach (+) button and Settings button -->
67
+ <div class="mt-1.5 flex flex-col space-y-1 items-center">
68
+ <div>
69
+ <button type="button"
70
+ @click="triggerFilePicker"
71
+ :disabled="isGenerating || !model"
72
+ 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"
73
+ title="Attach image or audio">
74
+ <svg class="size-5" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 256 256">
75
+ <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>
76
+ </svg>
77
+ </button>
78
+ <!-- Hidden file input -->
79
+ <input ref="fileInput" type="file" multiple @change="onFilesSelected"
80
+ class="hidden" accept="image/*,audio/*,.pdf,.doc,.docx,.xml,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document"
81
+ />
82
+ </div>
83
+ <div>
84
+ <button type="button" title="Settings" @click="showSettings = true"
85
+ :disabled="isGenerating || !model"
86
+ 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">
87
+ <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>
88
+ </button>
89
+ </div>
90
+ </div>
91
+
92
+ <div class="flex-1">
93
+ <div class="relative">
94
+ <textarea
95
+ ref="refMessage"
96
+ v-model="messageText"
97
+ @keydown.enter.exact.prevent="sendMessage"
98
+ @keydown.enter.shift.exact="addNewLine"
99
+ @paste="onPaste"
100
+ @dragover="onDragOver"
101
+ @dragleave="onDragLeave"
102
+ @drop="onDrop"
103
+ placeholder="Type message... (Enter to send, Shift+Enter for new line, drag & drop or paste files)"
104
+ rows="3"
105
+ :class="[
106
+ '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',
107
+ isDragging
108
+ ? 'border-blue-500 bg-blue-50 dark:bg-blue-900/30 ring-1 ring-blue-500'
109
+ : 'border-gray-300 dark:border-gray-600 focus:border-blue-500 focus:ring-blue-500'
110
+ ]"
111
+ :disabled="isGenerating || !model"
112
+ ></textarea>
113
+ <button v-if="!isGenerating" title="Send (Enter)" type="button"
114
+ @click="sendMessage"
115
+ :disabled="!messageText.trim() || isGenerating || !model"
116
+ 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">
117
+ <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>
118
+ </button>
119
+ <button v-else title="Cancel request" type="button"
120
+ @click="cancelRequest"
121
+ 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">
122
+ <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">
123
+ <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
124
+ </svg>
125
+ </button>
126
+ </div>
127
+
128
+ <!-- Attached files preview -->
129
+ <div v-if="attachedFiles.length" class="mt-2 flex flex-wrap gap-2">
130
+ <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">
131
+ <span class="truncate max-w-48" :title="f.name">{{ f.name }}</span>
132
+ <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">
133
+ <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>
134
+ </button>
135
+ </div>
136
+ </div>
137
+
138
+ <div v-if="!model" class="mt-2 text-sm text-red-600 dark:text-red-400">
139
+ Please select a model
140
+ </div>
141
+ </div>
142
+ </div>
143
+ </div>
144
+ `,
145
+ props: {
146
+ model: {
147
+ type: Object,
148
+ default: null
149
+ }
150
+ },
151
+ setup(props) {
152
+ const ctx = inject('ctx')
153
+ const config = ctx.state.config
154
+ const ai = ctx.ai
155
+ const chatSettings = inject('chatSettings')
156
+ const router = useRouter()
157
+ const chatPrompt = inject('chatPrompt')
158
+ const {
159
+ messageText,
160
+ attachedFiles,
161
+ isGenerating,
162
+ errorStatus,
163
+ hasImage,
164
+ hasAudio,
165
+ hasFile,
166
+ editingMessageId
167
+ } = chatPrompt
168
+ const threads = inject('threads')
169
+ const {
170
+ currentThread,
171
+ } = threads
172
+
173
+ const fileInput = ref(null)
174
+ const refMessage = ref(null)
175
+ const showSettings = ref(false)
176
+ const { applySettings } = chatSettings
177
+
178
+ // File attachments (+) handlers
179
+ const triggerFilePicker = () => {
180
+ if (fileInput.value) fileInput.value.click()
181
+ }
182
+ const onFilesSelected = async (e) => {
183
+ const files = Array.from(e.target?.files || [])
184
+ if (files.length) {
185
+ // Upload files immediately
186
+ const uploadedFiles = await Promise.all(files.map(async f => {
187
+ try {
188
+ const response = await uploadFile(f)
189
+ const metadata = {
190
+ url: response.url,
191
+ name: f.name,
192
+ size: response.size,
193
+ type: f.type,
194
+ width: response.width,
195
+ height: response.height,
196
+ threadId: currentThread.value?.id,
197
+ created: Date.now()
198
+ }
199
+
200
+ return {
201
+ ...metadata,
202
+ file: f // Keep original file for preview/fallback if needed
203
+ }
204
+ } catch (error) {
205
+ console.error('File upload failed:', error)
206
+ errorStatus.value = {
207
+ errorCode: 'Upload Failed',
208
+ message: `Failed to upload ${f.name}: ${error.message}`
209
+ }
210
+ return null
211
+ }
212
+ }))
213
+
214
+ attachedFiles.value.push(...uploadedFiles.filter(f => f))
215
+ }
216
+
217
+ // allow re-selecting the same file
218
+ if (fileInput.value) fileInput.value.value = ''
219
+
220
+ if (!messageText.value.trim()) {
221
+ if (hasImage()) {
222
+ messageText.value = getTextContent(config.defaults.image)
223
+ } else if (hasAudio()) {
224
+ messageText.value = getTextContent(config.defaults.audio)
225
+ } else {
226
+ messageText.value = getTextContent(config.defaults.file)
227
+ }
228
+ }
229
+ }
230
+ const removeAttachment = (i) => {
231
+ attachedFiles.value.splice(i, 1)
232
+ }
233
+
234
+ // Helper function to add files and set default message
235
+ const addFilesAndSetMessage = (files) => {
236
+ if (files.length === 0) return
237
+
238
+ attachedFiles.value.push(...files)
239
+
240
+ // Set default message text if empty
241
+ if (!messageText.value.trim()) {
242
+ if (hasImage()) {
243
+ messageText.value = getTextContent(config.defaults.image)
244
+ } else if (hasAudio()) {
245
+ messageText.value = getTextContent(config.defaults.audio)
246
+ } else {
247
+ messageText.value = getTextContent(config.defaults.file)
248
+ }
249
+ }
250
+ }
251
+
252
+ // Handle paste events for clipboard images, audio, and files
253
+ const onPaste = async (e) => {
254
+ // Use the paste event's clipboardData directly (works best for paste events)
255
+ const items = e.clipboardData?.items
256
+ if (!items) return
257
+
258
+ const files = []
259
+
260
+ // Check all clipboard items
261
+ for (let i = 0; i < items.length; i++) {
262
+ const item = items[i]
263
+
264
+ // Handle files (images, audio, etc.)
265
+ if (item.kind === 'file') {
266
+ const file = item.getAsFile()
267
+ if (file) {
268
+ // Generate a better filename based on type
269
+ let filename = file.name
270
+ if (!filename || filename === 'image.png' || filename === 'blob') {
271
+ const ext = file.type.split('/')[1] || 'png'
272
+ const timestamp = new Date().getTime()
273
+ if (file.type.startsWith('image/')) {
274
+ filename = `pasted-image-${timestamp}.${ext}`
275
+ } else if (file.type.startsWith('audio/')) {
276
+ filename = `pasted-audio-${timestamp}.${ext}`
277
+ } else {
278
+ filename = `pasted-file-${timestamp}.${ext}`
279
+ }
280
+ // Create a new File object with the better name
281
+ files.push(new File([file], filename, { type: file.type }))
282
+ } else {
283
+ files.push(file)
284
+ }
285
+ }
286
+ }
287
+ }
288
+
289
+ if (files.length > 0) {
290
+ e.preventDefault()
291
+ // Reuse the same logic as onFilesSelected for consistency
292
+ const event = { target: { files: files } }
293
+ await onFilesSelected(event)
294
+ }
295
+ }
296
+
297
+ // Handle drag and drop events
298
+ const isDragging = ref(false)
299
+
300
+ const onDragOver = (e) => {
301
+ e.preventDefault()
302
+ e.stopPropagation()
303
+ isDragging.value = true
304
+ }
305
+
306
+ const onDragLeave = (e) => {
307
+ e.preventDefault()
308
+ e.stopPropagation()
309
+ isDragging.value = false
310
+ }
311
+
312
+ const onDrop = async (e) => {
313
+ e.preventDefault()
314
+ e.stopPropagation()
315
+ isDragging.value = false
316
+
317
+ const files = Array.from(e.dataTransfer?.files || [])
318
+ if (files.length > 0) {
319
+ // Reuse the same logic as onFilesSelected for consistency
320
+ const event = { target: { files: files } }
321
+ await onFilesSelected(event)
322
+ }
323
+ }
324
+
325
+ function getTextContent(chat) {
326
+ const textMessage = chat.messages.find(m =>
327
+ m.role === 'user' && Array.isArray(m.content) && m.content.some(c => c.type === 'text'))
328
+ return textMessage?.content.find(c => c.type === 'text')?.text || ''
329
+ }
330
+
331
+ // Send message
332
+ const sendMessage = async () => {
333
+ if (!messageText.value.trim() || isGenerating.value || !props.model) return
334
+
335
+ // Clear any existing error message
336
+ errorStatus.value = null
337
+
338
+ // 1. Construct Structured Content (Text + Attachments)
339
+ let text = messageText.value.trim()
340
+ let content = []
341
+
342
+
343
+ messageText.value = ''
344
+
345
+ // Add Text Block
346
+ content.push({ type: 'text', text: text })
347
+
348
+ // Add Attachment Blocks
349
+ for (const f of attachedFiles.value) {
350
+ const ext = lastRightPart(f.name, '.')
351
+ if (imageExts.includes(ext)) {
352
+ content.push({ type: 'image_url', image_url: { url: f.url } })
353
+ } else if (audioExts.includes(ext)) {
354
+ content.push({ type: 'input_audio', input_audio: { data: f.url, format: ext } })
355
+ } else {
356
+ content.push({ type: 'file', file: { file_data: f.url, filename: f.name } })
357
+ }
358
+ }
359
+
360
+ // Create AbortController for this request
361
+ const controller = new AbortController()
362
+ chatPrompt.abortController.value = controller
363
+
364
+ try {
365
+ let threadId
366
+
367
+ // Create thread if none exists
368
+ if (!currentThread.value) {
369
+ const newThread = await threads.createThread({
370
+ title: 'New Chat',
371
+ model: props.model.id,
372
+ info: toModelInfo(props.model),
373
+ })
374
+ threadId = newThread.id
375
+ // Navigate to the new thread URL
376
+ router.push(`${ai.base}/c/${newThread.id}`)
377
+ } else {
378
+ threadId = currentThread.value.id
379
+ // Update the existing thread's model to match current selection
380
+ await threads.updateThread(threadId, {
381
+ model: props.model.name,
382
+ info: toModelInfo(props.model),
383
+ })
384
+ }
385
+
386
+ // Get the thread to check for duplicates
387
+ let thread = await threads.getThread(threadId)
388
+
389
+ // Handle Editing / Redo Logic
390
+ if (editingMessageId.value) {
391
+ // Check if message still exists
392
+ const messageExists = thread.messages.find(m => m.id === editingMessageId.value)
393
+ if (messageExists) {
394
+ // Update the message content
395
+ await threads.updateMessageInThread(threadId, editingMessageId.value, { content: content })
396
+ // Redo from this message (clears subsequent)
397
+ await threads.redoMessageFromThread(threadId, editingMessageId.value)
398
+
399
+ // Clear editing state
400
+ editingMessageId.value = null
401
+ } else {
402
+ // Fallback if message was deleted
403
+ editingMessageId.value = null
404
+ }
405
+ // Refresh thread state
406
+ thread = await threads.getThread(threadId)
407
+ } else {
408
+ // Regular Send Logic
409
+ const lastMessage = thread.messages[thread.messages.length - 1]
410
+
411
+ // Check duplicate based on text content extracted from potential array
412
+ const getLastText = (msgContent) => {
413
+ if (typeof msgContent === 'string') return msgContent
414
+ if (Array.isArray(msgContent)) return msgContent.find(c => c.type === 'text')?.text || ''
415
+ return ''
416
+ }
417
+ const newText = text // content[0].text
418
+ const lastText = lastMessage && lastMessage.role === 'user' ? getLastText(lastMessage.content) : null
419
+
420
+ const isDuplicate = lastText === newText
421
+
422
+ // Add user message only if it's not a duplicate
423
+ // Note: We are saving the FULL STRUCTURED CONTENT array here
424
+ if (!isDuplicate) {
425
+ await threads.addMessageToThread(threadId, {
426
+ role: 'user',
427
+ content: content
428
+ })
429
+ // Reload thread after adding message
430
+ thread = await threads.getThread(threadId)
431
+ }
432
+ }
433
+
434
+ isGenerating.value = true
435
+
436
+ // Construct API Request from History
437
+ const request = {
438
+ model: props.model.name,
439
+ messages: [],
440
+ metadata: {}
441
+ }
442
+
443
+ // Add History
444
+ thread.messages.forEach(m => {
445
+ request.messages.push({
446
+ role: m.role,
447
+ content: m.content
448
+ })
449
+ })
450
+
451
+ // Apply user settings
452
+ applySettings(request)
453
+ request.metadata.threadId = threadId
454
+
455
+ const ctxRequest = {
456
+ request,
457
+ thread,
458
+ }
459
+ ctx.chatRequestFilters.forEach(f => f(ctxRequest))
460
+
461
+ console.debug('chatRequest', request)
462
+
463
+ // Send to API
464
+ const startTime = Date.now()
465
+ const res = await ai.post('/v1/chat/completions', {
466
+ body: JSON.stringify(request),
467
+ signal: controller.signal
468
+ })
469
+
470
+ let response = null
471
+ if (!res.ok) {
472
+ errorStatus.value = {
473
+ errorCode: `HTTP ${res.status} ${res.statusText}`,
474
+ message: null,
475
+ stackTrace: null
476
+ }
477
+ let errorBody = null
478
+ try {
479
+ errorBody = await res.text()
480
+ if (errorBody) {
481
+ // Try to parse as JSON for better formatting
482
+ try {
483
+ const errorJson = JSON.parse(errorBody)
484
+ const status = errorJson?.responseStatus
485
+ if (status) {
486
+ errorStatus.value.errorCode += ` ${status.errorCode}`
487
+ errorStatus.value.message = status.message
488
+ errorStatus.value.stackTrace = status.stackTrace
489
+ } else {
490
+ errorStatus.value.stackTrace = JSON.stringify(errorJson, null, 2)
491
+ }
492
+ } catch (e) {
493
+ }
494
+ }
495
+ } catch (e) {
496
+ // If we can't read the response body, just use the status
497
+ }
498
+ } else {
499
+ try {
500
+ response = await res.json()
501
+ const ctxResponse = {
502
+ response,
503
+ thread,
504
+ }
505
+ ctx.chatResponseFilters.forEach(f => f(ctxResponse))
506
+ console.debug('chatResponse', JSON.stringify(response, null, 2))
507
+ } catch (e) {
508
+ errorStatus.value = {
509
+ errorCode: 'Error',
510
+ message: e.message,
511
+ stackTrace: null
512
+ }
513
+ }
514
+ }
515
+
516
+ if (response?.error) {
517
+ errorStatus.value ??= {
518
+ errorCode: 'Error',
519
+ }
520
+ errorStatus.value.message = response.error
521
+ }
522
+
523
+ if (!errorStatus.value) {
524
+ // Add assistant response (save entire message including reasoning)
525
+ const assistantMessage = response.choices?.[0]?.message
526
+
527
+ const usage = response.usage
528
+ if (usage) {
529
+ if (response.metadata?.pricing) {
530
+ const [input, output] = response.metadata.pricing.split('/')
531
+ usage.duration = response.metadata.duration ?? (Date.now() - startTime)
532
+ usage.input = input
533
+ usage.output = output
534
+ usage.tokens = usage.completion_tokens
535
+ usage.price = usage.output
536
+ usage.cost = tokenCost(usage.prompt_tokens / 1_000_000 * parseFloat(input) + usage.completion_tokens / 1_000_000 * parseFloat(output))
537
+ }
538
+ await threads.logRequest(threadId, props.model, request, response)
539
+ }
540
+ await threads.addMessageToThread(threadId, assistantMessage, usage)
541
+
542
+ nextTick(addCopyButtons)
543
+
544
+ attachedFiles.value = []
545
+ // Error will be cleared when user sends next message (no auto-timeout)
546
+ } else {
547
+ ctx.chatErrorFilters.forEach(f => f(errorStatus.value))
548
+ }
549
+ } catch (error) {
550
+ // Check if the error is due to abort
551
+ if (error.name === 'AbortError') {
552
+ console.log('Request was cancelled by user')
553
+ // Don't show error for cancelled requests
554
+ } else {
555
+ // Re-throw other errors to be handled by outer catch
556
+ throw error
557
+ }
558
+ } finally {
559
+ isGenerating.value = false
560
+ chatPrompt.abortController.value = null
561
+ // Restore focus to the textarea
562
+ nextTick(() => {
563
+ refMessage.value?.focus()
564
+ })
565
+ }
566
+ }
567
+
568
+ const cancelRequest = () => {
569
+ chatPrompt.cancel()
570
+ }
571
+
572
+ const addNewLine = () => {
573
+ // Enter key already adds new line
574
+ //messageText.value += '\n'
575
+ }
576
+
577
+ return {
578
+ isGenerating,
579
+ attachedFiles,
580
+ messageText,
581
+ fileInput,
582
+ refMessage,
583
+ showSettings,
584
+ isDragging,
585
+ triggerFilePicker,
586
+ onFilesSelected,
587
+ onPaste,
588
+ onDragOver,
589
+ onDragLeave,
590
+ onDrop,
591
+ removeAttachment,
592
+ sendMessage,
593
+ cancelRequest,
594
+ addNewLine,
595
+ }
596
+ }
597
+ }