llms-py 2.0.35__py3-none-any.whl → 3.0.0__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 (206) hide show
  1. llms/__pycache__/__init__.cpython-312.pyc +0 -0
  2. llms/__pycache__/__init__.cpython-313.pyc +0 -0
  3. llms/__pycache__/__init__.cpython-314.pyc +0 -0
  4. llms/__pycache__/__main__.cpython-312.pyc +0 -0
  5. llms/__pycache__/__main__.cpython-314.pyc +0 -0
  6. llms/__pycache__/llms.cpython-312.pyc +0 -0
  7. llms/__pycache__/main.cpython-312.pyc +0 -0
  8. llms/__pycache__/main.cpython-313.pyc +0 -0
  9. llms/__pycache__/main.cpython-314.pyc +0 -0
  10. llms/__pycache__/plugins.cpython-314.pyc +0 -0
  11. llms/{ui/Analytics.mjs → extensions/analytics/ui/index.mjs} +154 -238
  12. llms/extensions/app/README.md +20 -0
  13. llms/extensions/app/__init__.py +530 -0
  14. llms/extensions/app/__pycache__/__init__.cpython-314.pyc +0 -0
  15. llms/extensions/app/__pycache__/db.cpython-314.pyc +0 -0
  16. llms/extensions/app/__pycache__/db_manager.cpython-314.pyc +0 -0
  17. llms/extensions/app/db.py +644 -0
  18. llms/extensions/app/db_manager.py +195 -0
  19. llms/extensions/app/requests.json +9073 -0
  20. llms/extensions/app/threads.json +15290 -0
  21. llms/{ui → extensions/app/ui}/Recents.mjs +91 -65
  22. llms/{ui/Sidebar.mjs → extensions/app/ui/index.mjs} +124 -58
  23. llms/extensions/app/ui/threadStore.mjs +411 -0
  24. llms/extensions/core_tools/CALCULATOR.md +32 -0
  25. llms/extensions/core_tools/__init__.py +598 -0
  26. llms/extensions/core_tools/__pycache__/__init__.cpython-314.pyc +0 -0
  27. llms/extensions/core_tools/ui/codemirror/addon/edit/closebrackets.js +201 -0
  28. llms/extensions/core_tools/ui/codemirror/addon/edit/closetag.js +185 -0
  29. llms/extensions/core_tools/ui/codemirror/addon/edit/continuelist.js +101 -0
  30. llms/extensions/core_tools/ui/codemirror/addon/edit/matchbrackets.js +160 -0
  31. llms/extensions/core_tools/ui/codemirror/addon/edit/matchtags.js +66 -0
  32. llms/extensions/core_tools/ui/codemirror/addon/edit/trailingspace.js +27 -0
  33. llms/extensions/core_tools/ui/codemirror/addon/selection/active-line.js +72 -0
  34. llms/extensions/core_tools/ui/codemirror/addon/selection/mark-selection.js +119 -0
  35. llms/extensions/core_tools/ui/codemirror/addon/selection/selection-pointer.js +98 -0
  36. llms/extensions/core_tools/ui/codemirror/doc/docs.css +225 -0
  37. llms/extensions/core_tools/ui/codemirror/doc/source_sans.woff +0 -0
  38. llms/extensions/core_tools/ui/codemirror/lib/codemirror.css +344 -0
  39. llms/extensions/core_tools/ui/codemirror/lib/codemirror.js +9884 -0
  40. llms/extensions/core_tools/ui/codemirror/mode/clike/clike.js +942 -0
  41. llms/extensions/core_tools/ui/codemirror/mode/javascript/index.html +118 -0
  42. llms/extensions/core_tools/ui/codemirror/mode/javascript/javascript.js +962 -0
  43. llms/extensions/core_tools/ui/codemirror/mode/javascript/typescript.html +62 -0
  44. llms/extensions/core_tools/ui/codemirror/mode/python/python.js +402 -0
  45. llms/extensions/core_tools/ui/codemirror/theme/dracula.css +40 -0
  46. llms/extensions/core_tools/ui/codemirror/theme/mocha.css +135 -0
  47. llms/extensions/core_tools/ui/index.mjs +650 -0
  48. llms/extensions/gallery/README.md +61 -0
  49. llms/extensions/gallery/__init__.py +61 -0
  50. llms/extensions/gallery/__pycache__/__init__.cpython-314.pyc +0 -0
  51. llms/extensions/gallery/__pycache__/db.cpython-314.pyc +0 -0
  52. llms/extensions/gallery/db.py +298 -0
  53. llms/extensions/gallery/ui/index.mjs +482 -0
  54. llms/extensions/katex/README.md +39 -0
  55. llms/extensions/katex/__init__.py +6 -0
  56. llms/extensions/katex/__pycache__/__init__.cpython-314.pyc +0 -0
  57. llms/extensions/katex/ui/README.md +125 -0
  58. llms/extensions/katex/ui/contrib/auto-render.js +338 -0
  59. llms/extensions/katex/ui/contrib/auto-render.min.js +1 -0
  60. llms/extensions/katex/ui/contrib/auto-render.mjs +244 -0
  61. llms/extensions/katex/ui/contrib/copy-tex.js +127 -0
  62. llms/extensions/katex/ui/contrib/copy-tex.min.js +1 -0
  63. llms/extensions/katex/ui/contrib/copy-tex.mjs +105 -0
  64. llms/extensions/katex/ui/contrib/mathtex-script-type.js +109 -0
  65. llms/extensions/katex/ui/contrib/mathtex-script-type.min.js +1 -0
  66. llms/extensions/katex/ui/contrib/mathtex-script-type.mjs +24 -0
  67. llms/extensions/katex/ui/contrib/mhchem.js +3213 -0
  68. llms/extensions/katex/ui/contrib/mhchem.min.js +1 -0
  69. llms/extensions/katex/ui/contrib/mhchem.mjs +3109 -0
  70. llms/extensions/katex/ui/contrib/render-a11y-string.js +887 -0
  71. llms/extensions/katex/ui/contrib/render-a11y-string.min.js +1 -0
  72. llms/extensions/katex/ui/contrib/render-a11y-string.mjs +800 -0
  73. llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.ttf +0 -0
  74. llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.woff +0 -0
  75. llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  76. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
  77. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
  78. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  79. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
  80. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
  81. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  82. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
  83. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.woff +0 -0
  84. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  85. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
  86. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.woff +0 -0
  87. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  88. llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.ttf +0 -0
  89. llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.woff +0 -0
  90. llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.woff2 +0 -0
  91. llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
  92. llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.woff +0 -0
  93. llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  94. llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.ttf +0 -0
  95. llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.woff +0 -0
  96. llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.woff2 +0 -0
  97. llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.ttf +0 -0
  98. llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.woff +0 -0
  99. llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.woff2 +0 -0
  100. llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
  101. llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.woff +0 -0
  102. llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  103. llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.ttf +0 -0
  104. llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.woff +0 -0
  105. llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.woff2 +0 -0
  106. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
  107. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.woff +0 -0
  108. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  109. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
  110. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.woff +0 -0
  111. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  112. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
  113. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.woff +0 -0
  114. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  115. llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.ttf +0 -0
  116. llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.woff +0 -0
  117. llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.woff2 +0 -0
  118. llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.ttf +0 -0
  119. llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.woff +0 -0
  120. llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  121. llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.ttf +0 -0
  122. llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.woff +0 -0
  123. llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  124. llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.ttf +0 -0
  125. llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.woff +0 -0
  126. llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  127. llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.ttf +0 -0
  128. llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.woff +0 -0
  129. llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  130. llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
  131. llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.woff +0 -0
  132. llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  133. llms/extensions/katex/ui/index.mjs +92 -0
  134. llms/extensions/katex/ui/katex-swap.css +1230 -0
  135. llms/extensions/katex/ui/katex-swap.min.css +1 -0
  136. llms/extensions/katex/ui/katex.css +1230 -0
  137. llms/extensions/katex/ui/katex.js +19080 -0
  138. llms/extensions/katex/ui/katex.min.css +1 -0
  139. llms/extensions/katex/ui/katex.min.js +1 -0
  140. llms/extensions/katex/ui/katex.min.mjs +1 -0
  141. llms/extensions/katex/ui/katex.mjs +18547 -0
  142. llms/extensions/providers/__init__.py +18 -0
  143. llms/extensions/providers/__pycache__/__init__.cpython-314.pyc +0 -0
  144. llms/extensions/providers/__pycache__/anthropic.cpython-314.pyc +0 -0
  145. llms/extensions/providers/__pycache__/chutes.cpython-314.pyc +0 -0
  146. llms/extensions/providers/__pycache__/google.cpython-314.pyc +0 -0
  147. llms/extensions/providers/__pycache__/nvidia.cpython-314.pyc +0 -0
  148. llms/extensions/providers/__pycache__/openai.cpython-314.pyc +0 -0
  149. llms/extensions/providers/__pycache__/openrouter.cpython-314.pyc +0 -0
  150. llms/extensions/providers/anthropic.py +229 -0
  151. llms/extensions/providers/chutes.py +155 -0
  152. llms/extensions/providers/google.py +378 -0
  153. llms/extensions/providers/nvidia.py +105 -0
  154. llms/extensions/providers/openai.py +156 -0
  155. llms/extensions/providers/openrouter.py +72 -0
  156. llms/extensions/system_prompts/README.md +22 -0
  157. llms/extensions/system_prompts/__init__.py +45 -0
  158. llms/extensions/system_prompts/__pycache__/__init__.cpython-314.pyc +0 -0
  159. llms/extensions/system_prompts/ui/index.mjs +280 -0
  160. llms/extensions/system_prompts/ui/prompts.json +1067 -0
  161. llms/extensions/tools/__init__.py +5 -0
  162. llms/extensions/tools/__pycache__/__init__.cpython-314.pyc +0 -0
  163. llms/extensions/tools/ui/index.mjs +204 -0
  164. llms/index.html +35 -77
  165. llms/llms.json +357 -1186
  166. llms/main.py +2349 -591
  167. llms/providers-extra.json +356 -0
  168. llms/providers.json +1 -0
  169. llms/ui/App.mjs +151 -60
  170. llms/ui/ai.mjs +132 -60
  171. llms/ui/app.css +2173 -161
  172. llms/ui/ctx.mjs +365 -0
  173. llms/ui/index.mjs +129 -0
  174. llms/ui/lib/charts.mjs +9 -13
  175. llms/ui/lib/servicestack-vue.mjs +3 -3
  176. llms/ui/lib/vue.min.mjs +10 -9
  177. llms/ui/lib/vue.mjs +1796 -1635
  178. llms/ui/markdown.mjs +18 -7
  179. llms/ui/modules/chat/ChatBody.mjs +691 -0
  180. llms/ui/{SettingsDialog.mjs → modules/chat/SettingsDialog.mjs} +9 -9
  181. llms/ui/modules/chat/index.mjs +828 -0
  182. llms/ui/modules/layout.mjs +243 -0
  183. llms/ui/modules/model-selector.mjs +851 -0
  184. llms/ui/tailwind.input.css +496 -80
  185. llms/ui/utils.mjs +161 -93
  186. {llms_py-2.0.35.dist-info → llms_py-3.0.0.dist-info}/METADATA +1 -1
  187. llms_py-3.0.0.dist-info/RECORD +202 -0
  188. llms/ui/Avatar.mjs +0 -85
  189. llms/ui/Brand.mjs +0 -52
  190. llms/ui/ChatPrompt.mjs +0 -590
  191. llms/ui/Main.mjs +0 -823
  192. llms/ui/ModelSelector.mjs +0 -78
  193. llms/ui/OAuthSignIn.mjs +0 -92
  194. llms/ui/ProviderIcon.mjs +0 -30
  195. llms/ui/ProviderStatus.mjs +0 -105
  196. llms/ui/SignIn.mjs +0 -64
  197. llms/ui/SystemPromptEditor.mjs +0 -31
  198. llms/ui/SystemPromptSelector.mjs +0 -56
  199. llms/ui/Welcome.mjs +0 -8
  200. llms/ui/threadStore.mjs +0 -563
  201. llms/ui.json +0 -1069
  202. llms_py-2.0.35.dist-info/RECORD +0 -48
  203. {llms_py-2.0.35.dist-info → llms_py-3.0.0.dist-info}/WHEEL +0 -0
  204. {llms_py-2.0.35.dist-info → llms_py-3.0.0.dist-info}/entry_points.txt +0 -0
  205. {llms_py-2.0.35.dist-info → llms_py-3.0.0.dist-info}/licenses/LICENSE +0 -0
  206. {llms_py-2.0.35.dist-info → llms_py-3.0.0.dist-info}/top_level.txt +0 -0
llms/ui/Brand.mjs DELETED
@@ -1,52 +0,0 @@
1
- export default {
2
- template:`
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
- <div class="flex items-center justify-between">
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>
30
-
31
- <div class="flex items-center space-x-2">
32
- <button type="button"
33
- @click="$emit('analytics')"
34
- class="text-gray-900 dark:text-gray-200 hover:text-blue-600 dark:hover:text-blue-400 focus:outline-none transition-colors"
35
- title="Analytics"
36
- >
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>
38
- </button>
39
-
40
- <button type="button"
41
- @click="$emit('new')"
42
- class="text-gray-900 dark:text-gray-200 hover:text-blue-600 dark:hover:text-blue-400 focus:outline-none transition-colors"
43
- title="New Chat"
44
- >
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>
46
- </button>
47
- </div>
48
- </div>
49
- </div>
50
- `,
51
- emits:['home','new','analytics','toggle-sidebar'],
52
- }
llms/ui/ChatPrompt.mjs DELETED
@@ -1,590 +0,0 @@
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 } from './utils.mjs'
5
-
6
- const imageExts = 'png,webp,jpg,jpeg,gif,bmp,svg,tiff,ico'.split(',')
7
- const audioExts = 'mp3,wav,ogg,flac,m4a,opus,webm'.split(',')
8
-
9
- export function useChatPrompt() {
10
- const messageText = ref('')
11
- const attachedFiles = ref([])
12
- const isGenerating = ref(false)
13
- const errorStatus = ref(null)
14
- const abortController = ref(null)
15
- const hasImage = () => attachedFiles.value.some(f => imageExts.includes(lastRightPart(f.name, '.')))
16
- const hasAudio = () => attachedFiles.value.some(f => audioExts.includes(lastRightPart(f.name, '.')))
17
- const hasFile = () => attachedFiles.value.length > 0
18
- // const hasText = () => !hasImage() && !hasAudio() && !hasFile()
19
-
20
- function reset() {
21
- // Ensure initial state is ready to accept input
22
- isGenerating.value = false
23
- attachedFiles.value = []
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
36
- }
37
-
38
- return {
39
- messageText,
40
- attachedFiles,
41
- errorStatus,
42
- isGenerating,
43
- abortController,
44
- get generating() {
45
- return isGenerating.value
46
- },
47
- hasImage,
48
- hasAudio,
49
- hasFile,
50
- // hasText,
51
- reset,
52
- cancel,
53
- }
54
- }
55
-
56
- export default {
57
- template:`
58
- <div class="mx-auto max-w-3xl">
59
- <SettingsDialog :isOpen="showSettings" @close="showSettings = false" />
60
- <div class="flex space-x-2">
61
- <!-- Attach (+) button and Settings button -->
62
- <div class="mt-1.5 flex flex-col space-y-1 items-center">
63
- <div>
64
- <button type="button"
65
- @click="triggerFilePicker"
66
- :disabled="isGenerating || !model"
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"
68
- title="Attach image or audio">
69
- <svg class="size-5" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 256 256">
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>
71
- </svg>
72
- </button>
73
- <!-- Hidden file input -->
74
- <input ref="fileInput" type="file" multiple @change="onFilesSelected"
75
- class="hidden" accept="image/*,audio/*,.pdf,.doc,.docx,.xml,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document"
76
- />
77
- </div>
78
- <div>
79
- <button type="button" title="Settings" @click="showSettings = true"
80
- :disabled="isGenerating || !model"
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>
83
- </button>
84
- </div>
85
- </div>
86
-
87
- <div class="flex-1">
88
- <div class="relative">
89
- <textarea
90
- ref="refMessage"
91
- v-model="messageText"
92
- @keydown.enter.exact.prevent="sendMessage"
93
- @keydown.enter.shift.exact="addNewLine"
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)"
99
- rows="3"
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
- ]"
106
- :disabled="isGenerating || !model"
107
- ></textarea>
108
- <button v-if="!isGenerating" title="Send (Enter)" type="button"
109
- @click="sendMessage"
110
- :disabled="!messageText.trim() || isGenerating || !model"
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>
119
- </svg>
120
- </button>
121
- </div>
122
-
123
- <!-- Attached files preview -->
124
- <div v-if="attachedFiles.length" class="mt-2 flex flex-wrap gap-2">
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">
126
- <span class="truncate max-w-48" :title="f.name">{{ f.name }}</span>
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">
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>
129
- </button>
130
- </div>
131
- </div>
132
-
133
- <div v-if="!model" class="mt-2 text-sm text-red-600 dark:text-red-400">
134
- Please select a model
135
- </div>
136
- </div>
137
- </div>
138
- </div>
139
- `,
140
- props: {
141
- model: {
142
- type: String,
143
- default: ''
144
- },
145
- systemPrompt: {
146
- type: String,
147
- default: ''
148
- }
149
- },
150
- setup(props) {
151
- const ai = inject('ai')
152
- const chatSettings = inject('chatSettings')
153
- const router = useRouter()
154
- const config = inject('config')
155
- const chatPrompt = inject('chatPrompt')
156
- const {
157
- messageText,
158
- attachedFiles,
159
- isGenerating,
160
- errorStatus,
161
- hasImage,
162
- hasAudio,
163
- hasFile
164
- } = chatPrompt
165
- const threads = inject('threads')
166
- const {
167
- currentThread,
168
- } = threads
169
-
170
- const fileInput = ref(null)
171
- const refMessage = ref(null)
172
- const showSettings = ref(false)
173
- const { applySettings } = chatSettings
174
-
175
- // File attachments (+) handlers
176
- const triggerFilePicker = () => {
177
- if (fileInput.value) fileInput.value.click()
178
- }
179
- const onFilesSelected = (e) => {
180
- const files = Array.from(e.target?.files || [])
181
- if (files.length) attachedFiles.value.push(...files)
182
- // allow re-selecting the same file
183
- if (fileInput.value) fileInput.value.value = ''
184
-
185
- if (!messageText.value.trim()) {
186
- if (hasImage()) {
187
- messageText.value = getTextContent(config.defaults.image)
188
- } else if (hasAudio()) {
189
- messageText.value = getTextContent(config.defaults.audio)
190
- } else {
191
- messageText.value = getTextContent(config.defaults.file)
192
- }
193
- }
194
- }
195
- const removeAttachment = (i) => {
196
- attachedFiles.value.splice(i, 1)
197
- }
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
-
286
- function createChatRequest() {
287
- if (hasImage()) {
288
- return deepClone(config.defaults.image)
289
- }
290
- if (hasAudio()) {
291
- return deepClone(config.defaults.audio)
292
- }
293
- if (attachedFiles.value.length) {
294
- return deepClone(config.defaults.file)
295
- }
296
- const text = deepClone(config.defaults.text)
297
- return text
298
- }
299
-
300
- function getTextContent(chat) {
301
- const textMessage = chat.messages.find(m =>
302
- m.role === 'user' && Array.isArray(m.content) && m.content.some(c => c.type === 'text'))
303
- return textMessage?.content.find(c => c.type === 'text')?.text || ''
304
- }
305
-
306
- // Send message
307
- const sendMessage = async () => {
308
- if (!messageText.value.trim() || isGenerating.value || !props.model) return
309
-
310
- // Clear any existing error message
311
- errorStatus.value = null
312
-
313
- let message = messageText.value.trim()
314
- if (attachedFiles.value.length) {
315
- const names = attachedFiles.value.map(f => f.name).join(', ')
316
- const mediaType = imageExts.some(ext => names.includes(ext))
317
- ? '🖼️'
318
- : audioExts.some(ext => names.includes(ext))
319
- ? '🔉'
320
- : '📎'
321
- message += `\n\n[${mediaType} ${names}]`
322
- }
323
- messageText.value = ''
324
-
325
- // Create AbortController for this request
326
- const controller = new AbortController()
327
- chatPrompt.abortController.value = controller
328
-
329
- try {
330
- let threadId
331
-
332
- // Create thread if none exists
333
- if (!currentThread.value) {
334
- const newThread = await threads.createThread('New Chat', props.model, props.systemPrompt)
335
- threadId = newThread.id
336
- // Navigate to the new thread URL
337
- router.push(`${ai.base}/c/${newThread.id}`)
338
- } else {
339
- threadId = currentThread.value.id
340
- // Update the existing thread's model and systemPrompt to match current selection
341
- await threads.updateThread(threadId, {
342
- model: props.model.id,
343
- info: toModelInfo(props.model),
344
- systemPrompt: props.systemPrompt
345
- })
346
- }
347
-
348
- // Get the thread to check for duplicates
349
- let thread = await threads.getThread(threadId)
350
- const lastMessage = thread.messages[thread.messages.length - 1]
351
- const isDuplicate = lastMessage && lastMessage.role === 'user' && lastMessage.content === message
352
-
353
- // Add user message only if it's not a duplicate
354
- if (!isDuplicate) {
355
- await threads.addMessageToThread(threadId, {
356
- role: 'user',
357
- content: message
358
- })
359
- // Reload thread after adding message
360
- thread = await threads.getThread(threadId)
361
- }
362
-
363
- isGenerating.value = true
364
- const messages = [...thread.messages]
365
-
366
- // Add system prompt if present
367
- if (props.systemPrompt?.trim()) {
368
- messages.unshift({
369
- role: 'system',
370
- content: [
371
- { type: 'text', text: props.systemPrompt }
372
- ]
373
- })
374
- }
375
-
376
- const chatRequest = createChatRequest()
377
- chatRequest.model = props.model.id
378
-
379
- // Apply user settings
380
- applySettings(chatRequest)
381
-
382
- console.debug('chatRequest', chatRequest, hasImage(), hasAudio(), attachedFiles.value.length, attachedFiles.value)
383
-
384
- function setContentText(chatRequest, text) {
385
- // Replace text message
386
- const textImage = chatRequest.messages.find(m =>
387
- m.role === 'user' && Array.isArray(m.content) && m.content.some(c => c.type === 'text'))
388
- for (const c of textImage.content) {
389
- if (c.type === 'text') {
390
- c.text = text
391
- }
392
- }
393
- }
394
-
395
- if (hasImage()) {
396
- const imageMessage = chatRequest.messages.find(m =>
397
- m.role === 'user' && Array.isArray(m.content) && m.content.some(c => c.type === 'image_url'))
398
- console.debug('hasImage', chatRequest, imageMessage)
399
- if (imageMessage) {
400
- const imgs = []
401
- let imagePart = deepClone(imageMessage.content.find(c => c.type === 'image_url'))
402
- for (const f of attachedFiles.value) {
403
- if (imageExts.includes(lastRightPart(f.name, '.'))) {
404
- imagePart.image_url.url = await fileToDataUri(f)
405
- }
406
- imgs.push(imagePart)
407
- }
408
- imageMessage.content = imageMessage.content.filter(c => c.type !== 'image_url')
409
- imageMessage.content = [...imgs, ...imageMessage.content]
410
- setContentText(chatRequest, message)
411
- }
412
-
413
- } else if (hasAudio()) {
414
- console.debug('hasAudio', chatRequest)
415
- const audioMessage = chatRequest.messages.find(m =>
416
- m.role === 'user' && Array.isArray(m.content) && m.content.some(c => c.type === 'input_audio'))
417
- if (audioMessage) {
418
- const audios = []
419
- let audioPart = deepClone(audioMessage.content.find(c => c.type === 'input_audio'))
420
- for (const f of attachedFiles.value) {
421
- if (audioExts.includes(lastRightPart(f.name, '.'))) {
422
- audioPart.input_audio.data = await fileToBase64(f)
423
- }
424
- audios.push(audioPart)
425
- }
426
- audioMessage.content = audioMessage.content.filter(c => c.type !== 'input_audio')
427
- audioMessage.content = [...audios, ...audioMessage.content]
428
- setContentText(chatRequest, message)
429
- }
430
- } else if (attachedFiles.value.length) {
431
- console.debug('hasFile', chatRequest)
432
- const fileMessage = chatRequest.messages.find(m =>
433
- m.role === 'user' && Array.isArray(m.content) && m.content.some(c => c.type === 'file'))
434
- if (fileMessage) {
435
- const files = []
436
- let filePart = deepClone(fileMessage.content.find(c => c.type === 'file'))
437
- for (const f of attachedFiles.value) {
438
- filePart.file.file_data = await fileToDataUri(f)
439
- filePart.file.filename = f.name
440
- files.push(filePart)
441
- }
442
- fileMessage.content = fileMessage.content.filter(c => c.type !== 'file')
443
- fileMessage.content = [...files, ...fileMessage.content]
444
- setContentText(chatRequest, message)
445
- }
446
-
447
- } else {
448
- console.debug('hasText', chatRequest)
449
- // Chat template message needs to be empty
450
- chatRequest.messages = []
451
- messages.forEach(m => chatRequest.messages.push({
452
- role: m.role,
453
- content: typeof m.content === 'string'
454
- ? [{ type: 'text', text: m.content }]
455
- : m.content
456
- }))
457
- }
458
-
459
- chatRequest.metadata ??= {}
460
- chatRequest.metadata.threadId = threadId
461
-
462
- // Send to API
463
- console.debug('chatRequest', chatRequest)
464
- const startTime = Date.now()
465
- const response = await ai.post('/v1/chat/completions', {
466
- body: JSON.stringify(chatRequest),
467
- signal: controller.signal
468
- })
469
-
470
- let result = null
471
- if (!response.ok) {
472
- errorStatus.value = {
473
- errorCode: `HTTP ${response.status} ${response.statusText}`,
474
- message: null,
475
- stackTrace: null
476
- }
477
- let errorBody = null
478
- try {
479
- errorBody = await response.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
- result = await response.json()
501
- console.debug('chatResponse', JSON.stringify(result, null, 2))
502
- } catch (e) {
503
- errorStatus.value = {
504
- errorCode: 'Error',
505
- message: e.message,
506
- stackTrace: null
507
- }
508
- }
509
- }
510
-
511
- if (result?.error) {
512
- errorStatus.value ??= {
513
- errorCode: 'Error',
514
- }
515
- errorStatus.value.message = result.error
516
- }
517
-
518
- if (!errorStatus.value) {
519
- // Add assistant response (save entire message including reasoning)
520
- const assistantMessage = result.choices?.[0]?.message
521
-
522
- const usage = result.usage
523
- if (usage) {
524
- if (result.metadata?.pricing) {
525
- const [ input, output ] = result.metadata.pricing.split('/')
526
- usage.duration = result.metadata.duration ?? (Date.now() - startTime)
527
- usage.input = input
528
- usage.output = output
529
- usage.tokens = usage.completion_tokens
530
- usage.price = usage.output
531
- usage.cost = tokenCost(usage.prompt_tokens * parseFloat(input) + usage.completion_tokens * parseFloat(output))
532
- }
533
- await threads.logRequest(threadId, props.model, chatRequest, result)
534
- }
535
- await threads.addMessageToThread(threadId, assistantMessage, usage)
536
-
537
- nextTick(addCopyButtons)
538
-
539
- attachedFiles.value = []
540
- // Error will be cleared when user sends next message (no auto-timeout)
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
- }
551
- } finally {
552
- isGenerating.value = false
553
- chatPrompt.abortController.value = null
554
- // Restore focus to the textarea
555
- nextTick(() => {
556
- refMessage.value?.focus()
557
- })
558
- }
559
- }
560
-
561
- const cancelRequest = () => {
562
- chatPrompt.cancel()
563
- }
564
-
565
- const addNewLine = () => {
566
- // Enter key already adds new line
567
- //messageText.value += '\n'
568
- }
569
-
570
- return {
571
- isGenerating,
572
- attachedFiles,
573
- messageText,
574
- fileInput,
575
- refMessage,
576
- showSettings,
577
- isDragging,
578
- triggerFilePicker,
579
- onFilesSelected,
580
- onPaste,
581
- onDragOver,
582
- onDragLeave,
583
- onDrop,
584
- removeAttachment,
585
- sendMessage,
586
- cancelRequest,
587
- addNewLine,
588
- }
589
- }
590
- }