llms-py 2.0.20__py3-none-any.whl → 3.0.10__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 (190) hide show
  1. llms/__init__.py +3 -1
  2. llms/db.py +359 -0
  3. llms/{ui/Analytics.mjs → extensions/analytics/ui/index.mjs} +254 -327
  4. llms/extensions/app/README.md +20 -0
  5. llms/extensions/app/__init__.py +589 -0
  6. llms/extensions/app/db.py +536 -0
  7. llms/{ui → extensions/app/ui}/Recents.mjs +99 -73
  8. llms/{ui/Sidebar.mjs → extensions/app/ui/index.mjs} +139 -68
  9. llms/extensions/app/ui/threadStore.mjs +433 -0
  10. llms/extensions/core_tools/CALCULATOR.md +32 -0
  11. llms/extensions/core_tools/__init__.py +637 -0
  12. llms/extensions/core_tools/ui/codemirror/addon/edit/closebrackets.js +201 -0
  13. llms/extensions/core_tools/ui/codemirror/addon/edit/closetag.js +185 -0
  14. llms/extensions/core_tools/ui/codemirror/addon/edit/continuelist.js +101 -0
  15. llms/extensions/core_tools/ui/codemirror/addon/edit/matchbrackets.js +160 -0
  16. llms/extensions/core_tools/ui/codemirror/addon/edit/matchtags.js +66 -0
  17. llms/extensions/core_tools/ui/codemirror/addon/edit/trailingspace.js +27 -0
  18. llms/extensions/core_tools/ui/codemirror/addon/selection/active-line.js +72 -0
  19. llms/extensions/core_tools/ui/codemirror/addon/selection/mark-selection.js +119 -0
  20. llms/extensions/core_tools/ui/codemirror/addon/selection/selection-pointer.js +98 -0
  21. llms/extensions/core_tools/ui/codemirror/codemirror.css +344 -0
  22. llms/extensions/core_tools/ui/codemirror/codemirror.js +9884 -0
  23. llms/extensions/core_tools/ui/codemirror/doc/docs.css +225 -0
  24. llms/extensions/core_tools/ui/codemirror/doc/source_sans.woff +0 -0
  25. llms/extensions/core_tools/ui/codemirror/mode/clike/clike.js +942 -0
  26. llms/extensions/core_tools/ui/codemirror/mode/javascript/index.html +118 -0
  27. llms/extensions/core_tools/ui/codemirror/mode/javascript/javascript.js +962 -0
  28. llms/extensions/core_tools/ui/codemirror/mode/javascript/typescript.html +62 -0
  29. llms/extensions/core_tools/ui/codemirror/mode/python/python.js +402 -0
  30. llms/extensions/core_tools/ui/codemirror/theme/dracula.css +40 -0
  31. llms/extensions/core_tools/ui/codemirror/theme/mocha.css +135 -0
  32. llms/extensions/core_tools/ui/index.mjs +650 -0
  33. llms/extensions/gallery/README.md +61 -0
  34. llms/extensions/gallery/__init__.py +63 -0
  35. llms/extensions/gallery/db.py +243 -0
  36. llms/extensions/gallery/ui/index.mjs +482 -0
  37. llms/extensions/katex/README.md +39 -0
  38. llms/extensions/katex/__init__.py +6 -0
  39. llms/extensions/katex/ui/README.md +125 -0
  40. llms/extensions/katex/ui/contrib/auto-render.js +338 -0
  41. llms/extensions/katex/ui/contrib/auto-render.min.js +1 -0
  42. llms/extensions/katex/ui/contrib/auto-render.mjs +244 -0
  43. llms/extensions/katex/ui/contrib/copy-tex.js +127 -0
  44. llms/extensions/katex/ui/contrib/copy-tex.min.js +1 -0
  45. llms/extensions/katex/ui/contrib/copy-tex.mjs +105 -0
  46. llms/extensions/katex/ui/contrib/mathtex-script-type.js +109 -0
  47. llms/extensions/katex/ui/contrib/mathtex-script-type.min.js +1 -0
  48. llms/extensions/katex/ui/contrib/mathtex-script-type.mjs +24 -0
  49. llms/extensions/katex/ui/contrib/mhchem.js +3213 -0
  50. llms/extensions/katex/ui/contrib/mhchem.min.js +1 -0
  51. llms/extensions/katex/ui/contrib/mhchem.mjs +3109 -0
  52. llms/extensions/katex/ui/contrib/render-a11y-string.js +887 -0
  53. llms/extensions/katex/ui/contrib/render-a11y-string.min.js +1 -0
  54. llms/extensions/katex/ui/contrib/render-a11y-string.mjs +800 -0
  55. llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.ttf +0 -0
  56. llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.woff +0 -0
  57. llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  58. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
  59. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
  60. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  61. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
  62. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
  63. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  64. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
  65. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.woff +0 -0
  66. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  67. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
  68. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.woff +0 -0
  69. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  70. llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.ttf +0 -0
  71. llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.woff +0 -0
  72. llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.woff2 +0 -0
  73. llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
  74. llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.woff +0 -0
  75. llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  76. llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.ttf +0 -0
  77. llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.woff +0 -0
  78. llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.woff2 +0 -0
  79. llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.ttf +0 -0
  80. llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.woff +0 -0
  81. llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.woff2 +0 -0
  82. llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
  83. llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.woff +0 -0
  84. llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  85. llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.ttf +0 -0
  86. llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.woff +0 -0
  87. llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.woff2 +0 -0
  88. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
  89. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.woff +0 -0
  90. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  91. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
  92. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.woff +0 -0
  93. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  94. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
  95. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.woff +0 -0
  96. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  97. llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.ttf +0 -0
  98. llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.woff +0 -0
  99. llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.woff2 +0 -0
  100. llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.ttf +0 -0
  101. llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.woff +0 -0
  102. llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  103. llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.ttf +0 -0
  104. llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.woff +0 -0
  105. llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  106. llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.ttf +0 -0
  107. llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.woff +0 -0
  108. llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  109. llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.ttf +0 -0
  110. llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.woff +0 -0
  111. llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  112. llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
  113. llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.woff +0 -0
  114. llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  115. llms/extensions/katex/ui/index.mjs +92 -0
  116. llms/extensions/katex/ui/katex-swap.css +1230 -0
  117. llms/extensions/katex/ui/katex-swap.min.css +1 -0
  118. llms/extensions/katex/ui/katex.css +1230 -0
  119. llms/extensions/katex/ui/katex.js +19080 -0
  120. llms/extensions/katex/ui/katex.min.css +1 -0
  121. llms/extensions/katex/ui/katex.min.js +1 -0
  122. llms/extensions/katex/ui/katex.min.mjs +1 -0
  123. llms/extensions/katex/ui/katex.mjs +18547 -0
  124. llms/extensions/providers/__init__.py +22 -0
  125. llms/extensions/providers/anthropic.py +233 -0
  126. llms/extensions/providers/cerebras.py +37 -0
  127. llms/extensions/providers/chutes.py +153 -0
  128. llms/extensions/providers/google.py +481 -0
  129. llms/extensions/providers/nvidia.py +103 -0
  130. llms/extensions/providers/openai.py +154 -0
  131. llms/extensions/providers/openrouter.py +74 -0
  132. llms/extensions/providers/zai.py +182 -0
  133. llms/extensions/system_prompts/README.md +22 -0
  134. llms/extensions/system_prompts/__init__.py +45 -0
  135. llms/extensions/system_prompts/ui/index.mjs +280 -0
  136. llms/extensions/system_prompts/ui/prompts.json +1067 -0
  137. llms/extensions/tools/__init__.py +144 -0
  138. llms/extensions/tools/ui/index.mjs +706 -0
  139. llms/index.html +36 -62
  140. llms/llms.json +180 -879
  141. llms/main.py +3640 -899
  142. llms/providers-extra.json +394 -0
  143. llms/providers.json +1 -0
  144. llms/ui/App.mjs +176 -8
  145. llms/ui/ai.mjs +156 -20
  146. llms/ui/app.css +3161 -244
  147. llms/ui/ctx.mjs +412 -0
  148. llms/ui/index.mjs +131 -0
  149. llms/ui/lib/chart.js +14 -0
  150. llms/ui/lib/charts.mjs +16 -0
  151. llms/ui/lib/color.js +14 -0
  152. llms/ui/lib/highlight.min.mjs +1243 -0
  153. llms/ui/lib/idb.min.mjs +8 -0
  154. llms/ui/lib/marked.min.mjs +8 -0
  155. llms/ui/lib/servicestack-client.mjs +1 -0
  156. llms/ui/lib/servicestack-vue.mjs +37 -0
  157. llms/ui/lib/vue-router.min.mjs +6 -0
  158. llms/ui/lib/vue.min.mjs +13 -0
  159. llms/ui/lib/vue.mjs +18530 -0
  160. llms/ui/markdown.mjs +25 -14
  161. llms/ui/modules/chat/ChatBody.mjs +976 -0
  162. llms/ui/{SettingsDialog.mjs → modules/chat/SettingsDialog.mjs} +74 -74
  163. llms/ui/modules/chat/index.mjs +991 -0
  164. llms/ui/modules/icons.mjs +46 -0
  165. llms/ui/modules/layout.mjs +271 -0
  166. llms/ui/modules/model-selector.mjs +811 -0
  167. llms/ui/tailwind.input.css +550 -78
  168. llms/ui/typography.css +54 -36
  169. llms/ui/utils.mjs +197 -92
  170. llms_py-3.0.10.dist-info/METADATA +49 -0
  171. llms_py-3.0.10.dist-info/RECORD +177 -0
  172. {llms_py-2.0.20.dist-info → llms_py-3.0.10.dist-info}/licenses/LICENSE +1 -2
  173. llms/ui/Avatar.mjs +0 -28
  174. llms/ui/Brand.mjs +0 -34
  175. llms/ui/ChatPrompt.mjs +0 -443
  176. llms/ui/Main.mjs +0 -740
  177. llms/ui/ModelSelector.mjs +0 -60
  178. llms/ui/ProviderIcon.mjs +0 -29
  179. llms/ui/ProviderStatus.mjs +0 -105
  180. llms/ui/SignIn.mjs +0 -64
  181. llms/ui/SystemPromptEditor.mjs +0 -31
  182. llms/ui/SystemPromptSelector.mjs +0 -36
  183. llms/ui/Welcome.mjs +0 -8
  184. llms/ui/threadStore.mjs +0 -524
  185. llms/ui.json +0 -1069
  186. llms_py-2.0.20.dist-info/METADATA +0 -931
  187. llms_py-2.0.20.dist-info/RECORD +0 -36
  188. {llms_py-2.0.20.dist-info → llms_py-3.0.10.dist-info}/WHEEL +0 -0
  189. {llms_py-2.0.20.dist-info → llms_py-3.0.10.dist-info}/entry_points.txt +0 -0
  190. {llms_py-2.0.20.dist-info → llms_py-3.0.10.dist-info}/top_level.txt +0 -0
llms/ui/ChatPrompt.mjs DELETED
@@ -1,443 +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 hasImage = () => attachedFiles.value.some(f => imageExts.includes(lastRightPart(f.name, '.')))
15
- const hasAudio = () => attachedFiles.value.some(f => audioExts.includes(lastRightPart(f.name, '.')))
16
- const hasFile = () => attachedFiles.value.length > 0
17
- // const hasText = () => !hasImage() && !hasAudio() && !hasFile()
18
-
19
- function reset() {
20
- // Ensure initial state is ready to accept input
21
- isGenerating.value = false
22
- attachedFiles.value = []
23
- messageText.value = ''
24
- }
25
-
26
- return {
27
- messageText,
28
- attachedFiles,
29
- errorStatus,
30
- isGenerating,
31
- get generating() {
32
- return isGenerating.value
33
- },
34
- hasImage,
35
- hasAudio,
36
- hasFile,
37
- // hasText,
38
- reset,
39
- }
40
- }
41
-
42
- export default {
43
- template:`
44
- <div class="mx-auto max-w-3xl">
45
- <SettingsDialog :isOpen="showSettings" @close="showSettings = false" />
46
- <div class="flex space-x-2">
47
- <!-- Attach (+) button and Settings button -->
48
- <div class="mt-1.5 flex flex-col space-y-1 items-center">
49
- <div>
50
- <button type="button"
51
- @click="triggerFilePicker"
52
- :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"
54
- title="Attach image or audio">
55
- <svg class="size-5" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 256 256">
56
- <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>
57
- </svg>
58
- </button>
59
- <!-- Hidden file input -->
60
- <input ref="fileInput" type="file" multiple @change="onFilesSelected"
61
- class="hidden" accept="image/*,audio/*,.pdf,.doc,.docx,.xml,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document"
62
- />
63
- </div>
64
- <div>
65
- <button type="button" title="Settings" @click="showSettings = true"
66
- :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>
69
- </button>
70
- </div>
71
- </div>
72
-
73
- <div class="flex-1">
74
- <div class="relative">
75
- <textarea
76
- ref="messageInput"
77
- v-model="messageText"
78
- @keydown.enter.exact.prevent="sendMessage"
79
- @keydown.enter.shift.exact="addNewLine"
80
- placeholder="Type your message... (Enter to send, Shift+Enter for new line)"
81
- rows="3"
82
- class="block w-full rounded-md border border-gray-300 px-3 py-2 pr-12 text-sm placeholder-gray-500 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
83
- :disabled="isGenerating || !model"
84
- ></textarea>
85
- <button title="Send (Enter)" type="button"
86
- @click="sendMessage"
87
- :disabled="!messageText.trim() || isGenerating || !model"
88
- class="absolute bottom-2 right-2 size-8 flex items-center justify-center rounded-md border border-gray-300 text-gray-600 hover:bg-gray-50 disabled:text-gray-400 disabled:cursor-not-allowed disabled:border-gray-200 transition-colors">
89
- <svg v-if="isGenerating" class="size-5 animate-spin" fill="none" viewBox="0 0 24 24">
90
- <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
91
- <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
92
- </svg>
93
- <svg v-else class="size-5" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path stroke-dasharray="20" stroke-dashoffset="20" d="M12 21l0 -17.5"><animate fill="freeze" attributeName="stroke-dashoffset" dur="0.2s" values="20;0"/></path><path stroke-dasharray="12" stroke-dashoffset="12" d="M12 3l7 7M12 3l-7 7"><animate fill="freeze" attributeName="stroke-dashoffset" begin="0.2s" dur="0.2s" values="12;0"/></path></g></svg>
94
- </button>
95
- </div>
96
-
97
- <!-- Attached files preview -->
98
- <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">
100
- <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">
102
- <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
- </button>
104
- </div>
105
- </div>
106
-
107
- <div v-if="!model" class="mt-2 text-sm text-red-600">
108
- Please select a model
109
- </div>
110
- </div>
111
- </div>
112
- </div>
113
- `,
114
- props: {
115
- model: {
116
- type: String,
117
- default: ''
118
- },
119
- systemPrompt: {
120
- type: String,
121
- default: ''
122
- }
123
- },
124
- setup(props) {
125
- const ai = inject('ai')
126
- const chatSettings = inject('chatSettings')
127
- const router = useRouter()
128
- const config = inject('config')
129
- const chatPrompt = inject('chatPrompt')
130
- const {
131
- messageText,
132
- attachedFiles,
133
- isGenerating,
134
- errorStatus,
135
- hasImage,
136
- hasAudio,
137
- hasFile
138
- } = chatPrompt
139
- const threads = inject('threads')
140
- const {
141
- currentThread,
142
- } = threads
143
-
144
- const fileInput = ref(null)
145
- const showSettings = ref(false)
146
- const { applySettings } = chatSettings
147
-
148
- // File attachments (+) handlers
149
- const triggerFilePicker = () => {
150
- if (fileInput.value) fileInput.value.click()
151
- }
152
- const onFilesSelected = (e) => {
153
- const files = Array.from(e.target?.files || [])
154
- if (files.length) attachedFiles.value.push(...files)
155
- // allow re-selecting the same file
156
- if (fileInput.value) fileInput.value.value = ''
157
-
158
- if (!messageText.value.trim()) {
159
- if (hasImage()) {
160
- messageText.value = getTextContent(config.defaults.image)
161
- } else if (hasAudio()) {
162
- messageText.value = getTextContent(config.defaults.audio)
163
- } else {
164
- messageText.value = getTextContent(config.defaults.file)
165
- }
166
- }
167
- }
168
- const removeAttachment = (i) => {
169
- attachedFiles.value.splice(i, 1)
170
- }
171
-
172
- function createChatRequest() {
173
- if (hasImage()) {
174
- return deepClone(config.defaults.image)
175
- }
176
- if (hasAudio()) {
177
- return deepClone(config.defaults.audio)
178
- }
179
- if (attachedFiles.value.length) {
180
- return deepClone(config.defaults.file)
181
- }
182
- const text = deepClone(config.defaults.text)
183
- return text
184
- }
185
-
186
- function getTextContent(chat) {
187
- const textMessage = chat.messages.find(m =>
188
- m.role === 'user' && Array.isArray(m.content) && m.content.some(c => c.type === 'text'))
189
- return textMessage?.content.find(c => c.type === 'text')?.text || ''
190
- }
191
-
192
- // Send message
193
- const sendMessage = async () => {
194
- if (!messageText.value.trim() || isGenerating.value || !props.model) return
195
-
196
- // Clear any existing error message
197
- errorStatus.value = null
198
-
199
- let message = messageText.value.trim()
200
- if (attachedFiles.value.length) {
201
- const names = attachedFiles.value.map(f => f.name).join(', ')
202
- const mediaType = imageExts.some(ext => names.includes(ext))
203
- ? '🖼️'
204
- : audioExts.some(ext => names.includes(ext))
205
- ? '🔉'
206
- : '📎'
207
- message += `\n\n[${mediaType} ${names}]`
208
- }
209
- messageText.value = ''
210
-
211
- try {
212
- let threadId
213
-
214
- // Create thread if none exists
215
- if (!currentThread.value) {
216
- const newThread = await threads.createThread('New Chat', props.model, props.systemPrompt)
217
- threadId = newThread.id
218
- // Navigate to the new thread URL
219
- router.push(`${ai.base}/c/${newThread.id}`)
220
- } else {
221
- threadId = currentThread.value.id
222
- // Update the existing thread's model and systemPrompt to match current selection
223
- await threads.updateThread(threadId, {
224
- model: props.model.id,
225
- info: toModelInfo(props.model),
226
- systemPrompt: props.systemPrompt
227
- })
228
- }
229
-
230
- // Get the thread to check for duplicates
231
- let thread = await threads.getThread(threadId)
232
- const lastMessage = thread.messages[thread.messages.length - 1]
233
- const isDuplicate = lastMessage && lastMessage.role === 'user' && lastMessage.content === message
234
-
235
- // Add user message only if it's not a duplicate
236
- if (!isDuplicate) {
237
- await threads.addMessageToThread(threadId, {
238
- role: 'user',
239
- content: message
240
- })
241
- // Reload thread after adding message
242
- thread = await threads.getThread(threadId)
243
- }
244
-
245
- isGenerating.value = true
246
- const messages = [...thread.messages]
247
-
248
- // Add system prompt if present
249
- if (props.systemPrompt?.trim()) {
250
- messages.unshift({
251
- role: 'system',
252
- content: [
253
- { type: 'text', text: props.systemPrompt }
254
- ]
255
- })
256
- }
257
-
258
- const chatRequest = createChatRequest()
259
- chatRequest.model = props.model.id
260
-
261
- // Apply user settings
262
- applySettings(chatRequest)
263
-
264
- console.debug('chatRequest', chatRequest, hasImage(), hasAudio(), attachedFiles.value.length, attachedFiles.value)
265
-
266
- function setContentText(chatRequest, text) {
267
- // Replace text message
268
- const textImage = chatRequest.messages.find(m =>
269
- m.role === 'user' && Array.isArray(m.content) && m.content.some(c => c.type === 'text'))
270
- for (const c of textImage.content) {
271
- if (c.type === 'text') {
272
- c.text = text
273
- }
274
- }
275
- }
276
-
277
- if (hasImage()) {
278
- const imageMessage = chatRequest.messages.find(m =>
279
- m.role === 'user' && Array.isArray(m.content) && m.content.some(c => c.type === 'image_url'))
280
- console.debug('hasImage', chatRequest, imageMessage)
281
- if (imageMessage) {
282
- const imgs = []
283
- let imagePart = deepClone(imageMessage.content.find(c => c.type === 'image_url'))
284
- for (const f of attachedFiles.value) {
285
- if (imageExts.includes(lastRightPart(f.name, '.'))) {
286
- imagePart.image_url.url = await fileToDataUri(f)
287
- }
288
- imgs.push(imagePart)
289
- }
290
- imageMessage.content = imageMessage.content.filter(c => c.type !== 'image_url')
291
- imageMessage.content = [...imgs, ...imageMessage.content]
292
- setContentText(chatRequest, message)
293
- }
294
-
295
- } else if (hasAudio()) {
296
- console.debug('hasAudio', chatRequest)
297
- const audioMessage = chatRequest.messages.find(m =>
298
- m.role === 'user' && Array.isArray(m.content) && m.content.some(c => c.type === 'input_audio'))
299
- if (audioMessage) {
300
- const audios = []
301
- let audioPart = deepClone(audioMessage.content.find(c => c.type === 'input_audio'))
302
- for (const f of attachedFiles.value) {
303
- if (audioExts.includes(lastRightPart(f.name, '.'))) {
304
- audioPart.input_audio.data = await fileToBase64(f)
305
- }
306
- audios.push(audioPart)
307
- }
308
- audioMessage.content = audioMessage.content.filter(c => c.type !== 'input_audio')
309
- audioMessage.content = [...audios, ...audioMessage.content]
310
- setContentText(chatRequest, message)
311
- }
312
- } else if (attachedFiles.value.length) {
313
- console.debug('hasFile', chatRequest)
314
- const fileMessage = chatRequest.messages.find(m =>
315
- m.role === 'user' && Array.isArray(m.content) && m.content.some(c => c.type === 'file'))
316
- if (fileMessage) {
317
- const files = []
318
- let filePart = deepClone(fileMessage.content.find(c => c.type === 'file'))
319
- for (const f of attachedFiles.value) {
320
- filePart.file.file_data = await fileToDataUri(f)
321
- filePart.file.filename = f.name
322
- files.push(filePart)
323
- }
324
- fileMessage.content = fileMessage.content.filter(c => c.type !== 'file')
325
- fileMessage.content = [...files, ...fileMessage.content]
326
- setContentText(chatRequest, message)
327
- }
328
-
329
- } else {
330
- console.debug('hasText', chatRequest)
331
- // Chat template message needs to be empty
332
- chatRequest.messages = []
333
- messages.forEach(m => chatRequest.messages.push({
334
- role: m.role,
335
- content: typeof m.content === 'string'
336
- ? [{ type: 'text', text: m.content }]
337
- : m.content
338
- }))
339
- }
340
-
341
- // Send to API
342
- console.debug('chatRequest', chatRequest)
343
- const startTime = Date.now()
344
- const response = await ai.post('/v1/chat/completions', {
345
- body: JSON.stringify(chatRequest)
346
- })
347
-
348
- let result = null
349
- if (!response.ok) {
350
- errorStatus.value = {
351
- errorCode: `HTTP ${response.status} ${response.statusText}`,
352
- message: null,
353
- stackTrace: null
354
- }
355
- let errorBody = null
356
- try {
357
- errorBody = await response.text()
358
- if (errorBody) {
359
- // Try to parse as JSON for better formatting
360
- try {
361
- const errorJson = JSON.parse(errorBody)
362
- const status = errorJson?.responseStatus
363
- if (status) {
364
- errorStatus.value.errorCode += ` ${status.errorCode}`
365
- errorStatus.value.message = status.message
366
- errorStatus.value.stackTrace = status.stackTrace
367
- } else {
368
- errorStatus.value.stackTrace = JSON.stringify(errorJson, null, 2)
369
- }
370
- } catch (e) {
371
- }
372
- }
373
- } catch (e) {
374
- // If we can't read the response body, just use the status
375
- }
376
- } else {
377
- try {
378
- result = await response.json()
379
- console.debug('chatResponse', JSON.stringify(result, null, 2))
380
- } catch (e) {
381
- errorStatus.value = {
382
- errorCode: 'Error',
383
- message: e.message,
384
- stackTrace: null
385
- }
386
- }
387
- }
388
-
389
- if (result?.error) {
390
- errorStatus.value ??= {
391
- errorCode: 'Error',
392
- }
393
- errorStatus.value.message = result.error
394
- }
395
-
396
- if (!errorStatus.value) {
397
- // Add assistant response (save entire message including reasoning)
398
- const assistantMessage = result.choices?.[0]?.message
399
-
400
- const usage = result.usage
401
- if (usage) {
402
- if (result.metadata?.pricing) {
403
- const [ input, output ] = result.metadata.pricing.split('/')
404
- usage.duration = result.metadata.duration ?? (Date.now() - startTime)
405
- usage.input = input
406
- usage.output = output
407
- usage.tokens = usage.completion_tokens
408
- usage.price = usage.output
409
- usage.cost = tokenCost(usage.prompt_tokens * parseFloat(input) + usage.completion_tokens * parseFloat(output))
410
- }
411
- await threads.logRequest(threadId, props.model, chatRequest, result)
412
- }
413
- await threads.addMessageToThread(threadId, assistantMessage, usage)
414
-
415
- nextTick(addCopyButtons)
416
-
417
- attachedFiles.value = []
418
- // Error will be cleared when user sends next message (no auto-timeout)
419
- }
420
- } finally {
421
- isGenerating.value = false
422
- }
423
- }
424
-
425
- const addNewLine = () => {
426
- // Enter key already adds new line
427
- //messageText.value += '\n'
428
- }
429
-
430
- return {
431
- isGenerating,
432
- attachedFiles,
433
- messageText,
434
- fileInput,
435
- showSettings,
436
- triggerFilePicker,
437
- onFilesSelected,
438
- removeAttachment,
439
- sendMessage,
440
- addNewLine,
441
- }
442
- }
443
- }