llms-py 2.0.9__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 (194) hide show
  1. llms/__init__.py +4 -0
  2. llms/__main__.py +9 -0
  3. llms/db.py +359 -0
  4. llms/extensions/analytics/ui/index.mjs +1444 -0
  5. llms/extensions/app/README.md +20 -0
  6. llms/extensions/app/__init__.py +589 -0
  7. llms/extensions/app/db.py +536 -0
  8. {llms_py-2.0.9.data/data → llms/extensions/app}/ui/Recents.mjs +100 -73
  9. llms_py-2.0.9.data/data/ui/Sidebar.mjs → llms/extensions/app/ui/index.mjs +150 -79
  10. llms/extensions/app/ui/threadStore.mjs +433 -0
  11. llms/extensions/core_tools/CALCULATOR.md +32 -0
  12. llms/extensions/core_tools/__init__.py +637 -0
  13. llms/extensions/core_tools/ui/codemirror/addon/edit/closebrackets.js +201 -0
  14. llms/extensions/core_tools/ui/codemirror/addon/edit/closetag.js +185 -0
  15. llms/extensions/core_tools/ui/codemirror/addon/edit/continuelist.js +101 -0
  16. llms/extensions/core_tools/ui/codemirror/addon/edit/matchbrackets.js +160 -0
  17. llms/extensions/core_tools/ui/codemirror/addon/edit/matchtags.js +66 -0
  18. llms/extensions/core_tools/ui/codemirror/addon/edit/trailingspace.js +27 -0
  19. llms/extensions/core_tools/ui/codemirror/addon/selection/active-line.js +72 -0
  20. llms/extensions/core_tools/ui/codemirror/addon/selection/mark-selection.js +119 -0
  21. llms/extensions/core_tools/ui/codemirror/addon/selection/selection-pointer.js +98 -0
  22. llms/extensions/core_tools/ui/codemirror/codemirror.css +344 -0
  23. llms/extensions/core_tools/ui/codemirror/codemirror.js +9884 -0
  24. llms/extensions/core_tools/ui/codemirror/doc/docs.css +225 -0
  25. llms/extensions/core_tools/ui/codemirror/doc/source_sans.woff +0 -0
  26. llms/extensions/core_tools/ui/codemirror/mode/clike/clike.js +942 -0
  27. llms/extensions/core_tools/ui/codemirror/mode/javascript/index.html +118 -0
  28. llms/extensions/core_tools/ui/codemirror/mode/javascript/javascript.js +962 -0
  29. llms/extensions/core_tools/ui/codemirror/mode/javascript/typescript.html +62 -0
  30. llms/extensions/core_tools/ui/codemirror/mode/python/python.js +402 -0
  31. llms/extensions/core_tools/ui/codemirror/theme/dracula.css +40 -0
  32. llms/extensions/core_tools/ui/codemirror/theme/mocha.css +135 -0
  33. llms/extensions/core_tools/ui/index.mjs +650 -0
  34. llms/extensions/gallery/README.md +61 -0
  35. llms/extensions/gallery/__init__.py +63 -0
  36. llms/extensions/gallery/db.py +243 -0
  37. llms/extensions/gallery/ui/index.mjs +482 -0
  38. llms/extensions/katex/README.md +39 -0
  39. llms/extensions/katex/__init__.py +6 -0
  40. llms/extensions/katex/ui/README.md +125 -0
  41. llms/extensions/katex/ui/contrib/auto-render.js +338 -0
  42. llms/extensions/katex/ui/contrib/auto-render.min.js +1 -0
  43. llms/extensions/katex/ui/contrib/auto-render.mjs +244 -0
  44. llms/extensions/katex/ui/contrib/copy-tex.js +127 -0
  45. llms/extensions/katex/ui/contrib/copy-tex.min.js +1 -0
  46. llms/extensions/katex/ui/contrib/copy-tex.mjs +105 -0
  47. llms/extensions/katex/ui/contrib/mathtex-script-type.js +109 -0
  48. llms/extensions/katex/ui/contrib/mathtex-script-type.min.js +1 -0
  49. llms/extensions/katex/ui/contrib/mathtex-script-type.mjs +24 -0
  50. llms/extensions/katex/ui/contrib/mhchem.js +3213 -0
  51. llms/extensions/katex/ui/contrib/mhchem.min.js +1 -0
  52. llms/extensions/katex/ui/contrib/mhchem.mjs +3109 -0
  53. llms/extensions/katex/ui/contrib/render-a11y-string.js +887 -0
  54. llms/extensions/katex/ui/contrib/render-a11y-string.min.js +1 -0
  55. llms/extensions/katex/ui/contrib/render-a11y-string.mjs +800 -0
  56. llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.ttf +0 -0
  57. llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.woff +0 -0
  58. llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  59. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
  60. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
  61. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  62. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
  63. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
  64. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  65. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
  66. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.woff +0 -0
  67. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  68. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
  69. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.woff +0 -0
  70. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  71. llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.ttf +0 -0
  72. llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.woff +0 -0
  73. llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.woff2 +0 -0
  74. llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
  75. llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.woff +0 -0
  76. llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  77. llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.ttf +0 -0
  78. llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.woff +0 -0
  79. llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.woff2 +0 -0
  80. llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.ttf +0 -0
  81. llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.woff +0 -0
  82. llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.woff2 +0 -0
  83. llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
  84. llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.woff +0 -0
  85. llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  86. llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.ttf +0 -0
  87. llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.woff +0 -0
  88. llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.woff2 +0 -0
  89. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
  90. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.woff +0 -0
  91. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  92. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
  93. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.woff +0 -0
  94. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  95. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
  96. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.woff +0 -0
  97. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  98. llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.ttf +0 -0
  99. llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.woff +0 -0
  100. llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.woff2 +0 -0
  101. llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.ttf +0 -0
  102. llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.woff +0 -0
  103. llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  104. llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.ttf +0 -0
  105. llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.woff +0 -0
  106. llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  107. llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.ttf +0 -0
  108. llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.woff +0 -0
  109. llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  110. llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.ttf +0 -0
  111. llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.woff +0 -0
  112. llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  113. llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
  114. llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.woff +0 -0
  115. llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  116. llms/extensions/katex/ui/index.mjs +92 -0
  117. llms/extensions/katex/ui/katex-swap.css +1230 -0
  118. llms/extensions/katex/ui/katex-swap.min.css +1 -0
  119. llms/extensions/katex/ui/katex.css +1230 -0
  120. llms/extensions/katex/ui/katex.js +19080 -0
  121. llms/extensions/katex/ui/katex.min.css +1 -0
  122. llms/extensions/katex/ui/katex.min.js +1 -0
  123. llms/extensions/katex/ui/katex.min.mjs +1 -0
  124. llms/extensions/katex/ui/katex.mjs +18547 -0
  125. llms/extensions/providers/__init__.py +22 -0
  126. llms/extensions/providers/anthropic.py +233 -0
  127. llms/extensions/providers/cerebras.py +37 -0
  128. llms/extensions/providers/chutes.py +153 -0
  129. llms/extensions/providers/google.py +481 -0
  130. llms/extensions/providers/nvidia.py +103 -0
  131. llms/extensions/providers/openai.py +154 -0
  132. llms/extensions/providers/openrouter.py +74 -0
  133. llms/extensions/providers/zai.py +182 -0
  134. llms/extensions/system_prompts/README.md +22 -0
  135. llms/extensions/system_prompts/__init__.py +45 -0
  136. llms/extensions/system_prompts/ui/index.mjs +280 -0
  137. llms/extensions/system_prompts/ui/prompts.json +1067 -0
  138. llms/extensions/tools/__init__.py +144 -0
  139. llms/extensions/tools/ui/index.mjs +706 -0
  140. llms/index.html +58 -0
  141. llms/llms.json +400 -0
  142. llms/main.py +4407 -0
  143. llms/providers-extra.json +394 -0
  144. llms/providers.json +1 -0
  145. llms/ui/App.mjs +188 -0
  146. llms/ui/ai.mjs +217 -0
  147. llms/ui/app.css +7081 -0
  148. llms/ui/ctx.mjs +412 -0
  149. llms/ui/index.mjs +131 -0
  150. llms/ui/lib/chart.js +14 -0
  151. llms/ui/lib/charts.mjs +16 -0
  152. llms/ui/lib/color.js +14 -0
  153. llms/ui/lib/servicestack-vue.mjs +37 -0
  154. llms/ui/lib/vue.min.mjs +13 -0
  155. llms/ui/lib/vue.mjs +18530 -0
  156. {llms_py-2.0.9.data/data → llms}/ui/markdown.mjs +33 -15
  157. llms/ui/modules/chat/ChatBody.mjs +976 -0
  158. llms/ui/modules/chat/SettingsDialog.mjs +374 -0
  159. llms/ui/modules/chat/index.mjs +991 -0
  160. llms/ui/modules/icons.mjs +46 -0
  161. llms/ui/modules/layout.mjs +271 -0
  162. llms/ui/modules/model-selector.mjs +811 -0
  163. llms/ui/tailwind.input.css +742 -0
  164. {llms_py-2.0.9.data/data → llms}/ui/typography.css +133 -7
  165. llms/ui/utils.mjs +261 -0
  166. llms_py-3.0.10.dist-info/METADATA +49 -0
  167. llms_py-3.0.10.dist-info/RECORD +177 -0
  168. llms_py-3.0.10.dist-info/entry_points.txt +2 -0
  169. {llms_py-2.0.9.dist-info → llms_py-3.0.10.dist-info}/licenses/LICENSE +1 -2
  170. llms.py +0 -1402
  171. llms_py-2.0.9.data/data/index.html +0 -64
  172. llms_py-2.0.9.data/data/llms.json +0 -447
  173. llms_py-2.0.9.data/data/requirements.txt +0 -1
  174. llms_py-2.0.9.data/data/ui/App.mjs +0 -20
  175. llms_py-2.0.9.data/data/ui/ChatPrompt.mjs +0 -389
  176. llms_py-2.0.9.data/data/ui/Main.mjs +0 -680
  177. llms_py-2.0.9.data/data/ui/app.css +0 -3951
  178. llms_py-2.0.9.data/data/ui/lib/servicestack-vue.min.mjs +0 -37
  179. llms_py-2.0.9.data/data/ui/lib/vue.min.mjs +0 -12
  180. llms_py-2.0.9.data/data/ui/tailwind.input.css +0 -261
  181. llms_py-2.0.9.data/data/ui/threadStore.mjs +0 -273
  182. llms_py-2.0.9.data/data/ui/utils.mjs +0 -114
  183. llms_py-2.0.9.data/data/ui.json +0 -1069
  184. llms_py-2.0.9.dist-info/METADATA +0 -941
  185. llms_py-2.0.9.dist-info/RECORD +0 -30
  186. llms_py-2.0.9.dist-info/entry_points.txt +0 -2
  187. {llms_py-2.0.9.data/data → llms}/ui/fav.svg +0 -0
  188. {llms_py-2.0.9.data/data → llms}/ui/lib/highlight.min.mjs +0 -0
  189. {llms_py-2.0.9.data/data → llms}/ui/lib/idb.min.mjs +0 -0
  190. {llms_py-2.0.9.data/data → llms}/ui/lib/marked.min.mjs +0 -0
  191. /llms_py-2.0.9.data/data/ui/lib/servicestack-client.min.mjs → /llms/ui/lib/servicestack-client.mjs +0 -0
  192. {llms_py-2.0.9.data/data → llms}/ui/lib/vue-router.min.mjs +0 -0
  193. {llms_py-2.0.9.dist-info → llms_py-3.0.10.dist-info}/WHEEL +0 -0
  194. {llms_py-2.0.9.dist-info → llms_py-3.0.10.dist-info}/top_level.txt +0 -0
@@ -1,389 +0,0 @@
1
- import { ref, nextTick, inject } from 'vue'
2
- import { useRouter } from 'vue-router'
3
- import { lastRightPart } from '@servicestack/client'
4
- import { deepClone, fileToDataUri, fileToBase64, addCopyButtons } 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 errorMessage = 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
- }
26
-
27
- return {
28
- messageText,
29
- attachedFiles,
30
- errorStatus,
31
- errorMessage,
32
- isGenerating,
33
- get generating() {
34
- return isGenerating.value
35
- },
36
- hasImage,
37
- hasAudio,
38
- hasFile,
39
- // hasText,
40
- reset,
41
- }
42
- }
43
-
44
- export default {
45
- template:`
46
- <div class="mx-auto max-w-3xl">
47
- <div class="flex space-x-3">
48
- <!-- Attach (+) button -->
49
- <div>
50
- <button type="button"
51
- @click="triggerFilePicker"
52
- :disabled="isGenerating || !model"
53
- class="mt-2 h-10 w-10 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="w-5 h-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">
56
- <line x1="12" y1="5" x2="12" y2="19"></line>
57
- <line x1="5" y1="12" x2="19" y2="12"></line>
58
- </svg>
59
- </button>
60
- <!-- Hidden file input -->
61
- <input ref="fileInput" type="file" multiple @change="onFilesSelected"
62
- class="hidden" accept="image/*,audio/*,.pdf,.doc,.docx,.xml,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document"
63
- />
64
- </div>
65
-
66
- <div class="flex-1">
67
- <textarea
68
- ref="messageInput"
69
- v-model="messageText"
70
- @keydown.enter.exact.prevent="sendMessage"
71
- @keydown.enter.shift.exact="addNewLine"
72
- placeholder="Type your message... (Enter to send, Shift+Enter for new line)"
73
- rows="2"
74
- class="block w-full rounded-md border border-gray-300 px-3 py-2 text-sm placeholder-gray-500 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
75
- :disabled="isGenerating || !model"
76
- ></textarea>
77
-
78
- <!-- Attached files preview -->
79
- <div v-if="attachedFiles.length" class="mt-2 flex flex-wrap gap-2">
80
- <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">
81
- <span class="truncate max-w-48" :title="f.name">{{ f.name }}</span>
82
- <button type="button" class="text-gray-500 hover:text-gray-700" @click="removeAttachment(i)" title="Remove Attachment">
83
- <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>
84
- </button>
85
- </div>
86
- </div>
87
-
88
- <div v-if="!model" class="mt-2 text-sm text-red-600">
89
- Please select a model
90
- </div>
91
- </div>
92
-
93
- <div>
94
- <button title="Send (Enter)" type="button"
95
- @click="sendMessage"
96
- :disabled="!messageText.trim() || isGenerating || !model"
97
- class="mt-2 p-2 flex items-center justify-center rounded-full bg-gray-700 text-white transition-colors hover:opacity-70 focus-visible:outline-none focus-visible:outline-black disabled:bg-[#D7D7D7] disabled:text-[#f4f4f4] disabled:hover:opacity-100 dark:bg-white dark:text-black dark:focus-visible:outline-white disabled:dark:bg-token-text-quaternary dark:disabled:text-token-main-surface-secondary">
98
- <svg v-if="isGenerating" class="size-6 animate-spin" fill="none" viewBox="0 0 24 24">
99
- <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
100
- <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>
101
- </svg>
102
- <svg v-else class="size-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="m3.165 19.503l7.362-16.51c.59-1.324 2.355-1.324 2.946 0l7.362 16.51c.667 1.495-.814 3.047-2.202 2.306l-5.904-3.152c-.459-.245-1-.245-1.458 0l-5.904 3.152c-1.388.74-2.87-.81-2.202-2.306"></path></svg>
103
- </button>
104
- </div>
105
- </div>
106
- </div>
107
- `,
108
- props: {
109
- model: {
110
- type: String,
111
- default: ''
112
- },
113
- systemPrompt: {
114
- type: String,
115
- default: ''
116
- }
117
- },
118
- setup(props) {
119
- const router = useRouter()
120
- const config = inject('config')
121
- const chatPrompt = inject('chatPrompt')
122
- const {
123
- messageText,
124
- attachedFiles,
125
- isGenerating,
126
- errorStatus,
127
- errorMessage,
128
- hasImage,
129
- hasAudio,
130
- hasFile
131
- } = chatPrompt
132
- const threads = inject('threads')
133
- const {
134
- currentThread,
135
- } = threads
136
-
137
- const fileInput = ref(null)
138
-
139
- // File attachments (+) handlers
140
- const triggerFilePicker = () => {
141
- if (fileInput.value) fileInput.value.click()
142
- }
143
- const onFilesSelected = (e) => {
144
- const files = Array.from(e.target?.files || [])
145
- if (files.length) attachedFiles.value.push(...files)
146
- // allow re-selecting the same file
147
- if (fileInput.value) fileInput.value.value = ''
148
-
149
- if (!messageText.value.trim()) {
150
- if (hasImage()) {
151
- messageText.value = getTextContent(config.defaults.image)
152
- } else if (hasAudio()) {
153
- messageText.value = getTextContent(config.defaults.audio)
154
- } else {
155
- messageText.value = getTextContent(config.defaults.file)
156
- }
157
- }
158
- }
159
- const removeAttachment = (i) => {
160
- attachedFiles.value.splice(i, 1)
161
- }
162
-
163
- function createChatRequest() {
164
- if (hasImage()) {
165
- return deepClone(config.defaults.image)
166
- }
167
- if (hasAudio()) {
168
- return deepClone(config.defaults.audio)
169
- }
170
- if (attachedFiles.value.length) {
171
- return deepClone(config.defaults.file)
172
- }
173
- const text = deepClone(config.defaults.text)
174
- return text
175
- }
176
-
177
- function getTextContent(chat) {
178
- const textMessage = chat.messages.find(m =>
179
- m.role === 'user' && Array.isArray(m.content) && m.content.some(c => c.type === 'text'))
180
- return textMessage?.content.find(c => c.type === 'text')?.text || ''
181
- }
182
-
183
- // Send message
184
- const sendMessage = async () => {
185
- if (!messageText.value.trim() || isGenerating.value || !props.model) return
186
-
187
- // Clear any existing error message
188
- errorStatus.value = errorMessage.value = null
189
-
190
- let message = messageText.value.trim()
191
- if (attachedFiles.value.length) {
192
- const names = attachedFiles.value.map(f => f.name).join(', ')
193
- const mediaType = imageExts.some(ext => names.includes(ext))
194
- ? '🖼️'
195
- : audioExts.some(ext => names.includes(ext))
196
- ? '🔉'
197
- : '📎'
198
- message += `\n\n[${mediaType} ${names}]`
199
- }
200
- messageText.value = ''
201
-
202
- try {
203
- let threadId
204
-
205
- // Create thread if none exists
206
- if (!currentThread.value) {
207
- const newThread = await threads.createThread('New Chat', props.model, props.systemPrompt)
208
- threadId = newThread.id
209
- // Navigate to the new thread URL
210
- router.push(`/c/${newThread.id}`)
211
- } else {
212
- threadId = currentThread.value.id
213
- // Update the existing thread's model and systemPrompt to match current selection
214
- await threads.updateThread(threadId, {
215
- model: props.model,
216
- systemPrompt: props.systemPrompt
217
- })
218
- }
219
-
220
- // Add user message
221
- await threads.addMessageToThread(threadId, {
222
- role: 'user',
223
- content: message
224
- })
225
-
226
- isGenerating.value = true
227
-
228
- // Get the updated thread to prepare chat request
229
- const thread = await threads.getThread(threadId)
230
- const messages = [...thread.messages]
231
-
232
- // Add system prompt if present
233
- if (props.systemPrompt?.trim()) {
234
- messages.unshift({
235
- role: 'system',
236
- content: props.systemPrompt.trim()
237
- })
238
- }
239
-
240
- const chatRequest = createChatRequest()
241
- chatRequest.model = props.model
242
-
243
- console.log('chatRequest', chatRequest, hasImage(), hasAudio(), attachedFiles.value.length, attachedFiles.value)
244
-
245
- function setContentText(chatRequest, text) {
246
- // Replace text message
247
- const textImage = chatRequest.messages.find(m =>
248
- m.role === 'user' && Array.isArray(m.content) && m.content.some(c => c.type === 'text'))
249
- for (const c of textImage.content) {
250
- if (c.type === 'text') {
251
- c.text = text
252
- }
253
- }
254
- }
255
-
256
- if (hasImage()) {
257
- const imageMessage = chatRequest.messages.find(m =>
258
- m.role === 'user' && Array.isArray(m.content) && m.content.some(c => c.type === 'image_url'))
259
- console.log('hasImage', chatRequest, imageMessage)
260
- if (imageMessage) {
261
- const imgs = []
262
- let imagePart = deepClone(imageMessage.content.find(c => c.type === 'image_url'))
263
- for (const f of attachedFiles.value) {
264
- if (imageExts.includes(lastRightPart(f.name, '.'))) {
265
- imagePart.image_url.url = await fileToDataUri(f)
266
- }
267
- imgs.push(imagePart)
268
- }
269
- imageMessage.content = imageMessage.content.filter(c => c.type !== 'image_url')
270
- imageMessage.content = [...imgs, ...imageMessage.content]
271
- setContentText(chatRequest, message)
272
- }
273
-
274
- } else if (hasAudio()) {
275
- console.log('hasAudio', chatRequest)
276
- const audioMessage = chatRequest.messages.find(m =>
277
- m.role === 'user' && Array.isArray(m.content) && m.content.some(c => c.type === 'input_audio'))
278
- if (audioMessage) {
279
- const audios = []
280
- let audioPart = deepClone(audioMessage.content.find(c => c.type === 'input_audio'))
281
- for (const f of attachedFiles.value) {
282
- if (audioExts.includes(lastRightPart(f.name, '.'))) {
283
- audioPart.input_audio.data = await fileToBase64(f)
284
- }
285
- audios.push(audioPart)
286
- }
287
- audioMessage.content = audioMessage.content.filter(c => c.type !== 'input_audio')
288
- audioMessage.content = [...audios, ...audioMessage.content]
289
- setContentText(chatRequest, message)
290
- }
291
- } else if (attachedFiles.value.length) {
292
- console.log('hasFile', chatRequest)
293
- const fileMessage = chatRequest.messages.find(m =>
294
- m.role === 'user' && Array.isArray(m.content) && m.content.some(c => c.type === 'file'))
295
- if (fileMessage) {
296
- const files = []
297
- let filePart = deepClone(fileMessage.content.find(c => c.type === 'file'))
298
- for (const f of attachedFiles.value) {
299
- filePart.file.file_data = await fileToDataUri(f)
300
- filePart.file.filename = f.name
301
- files.push(filePart)
302
- }
303
- fileMessage.content = fileMessage.content.filter(c => c.type !== 'file')
304
- fileMessage.content = [...files, ...fileMessage.content]
305
- setContentText(chatRequest, message)
306
- }
307
-
308
- } else {
309
- console.log('hasText', chatRequest)
310
- // Chat template message needs to be empty
311
- chatRequest.messages = []
312
- messages.forEach(m => chatRequest.messages.push({
313
- role: m.role,
314
- content: m.content
315
- }))
316
- }
317
-
318
- // Send to API
319
- const response = await fetch('/v1/chat/completions', {
320
- method: 'POST',
321
- headers: {
322
- 'Content-Type': 'application/json'
323
- },
324
- body: JSON.stringify(chatRequest)
325
- })
326
-
327
- if (!response.ok) {
328
- errorStatus.value = `HTTP ${response.status} ${response.statusText}`
329
- let errorBody = ''
330
- try {
331
- errorBody = await response.text()
332
- if (errorBody) {
333
- // Try to parse as JSON for better formatting
334
- try {
335
- const errorJson = JSON.parse(errorBody)
336
- errorBody = JSON.stringify(errorJson, null, 2)
337
- } catch (e) {
338
- }
339
- }
340
- } catch (e) {
341
- // If we can't read the response body, just use the status
342
- }
343
- throw new Error(errorBody || '')
344
- }
345
-
346
- const result = await response.json()
347
-
348
- if (result.error) {
349
- throw new Error(result.error)
350
- }
351
-
352
- // Add assistant response (save entire message including reasoning)
353
- const assistantMessage = result.choices?.[0]?.message
354
- await threads.addMessageToThread(threadId, assistantMessage)
355
-
356
- nextTick(addCopyButtons)
357
-
358
- attachedFiles.value = []
359
-
360
- } catch (error) {
361
- console.error('Error sending message:', error)
362
- errorMessage.value = error.message
363
-
364
- // Error will be cleared when user sends next message (no auto-timeout)
365
- } finally {
366
- isGenerating.value = false
367
- }
368
- }
369
-
370
- const addNewLine = () => {
371
- // Enter key already adds new line
372
- //messageText.value += '\n'
373
- }
374
-
375
- return {
376
- isGenerating,
377
- attachedFiles,
378
- errorStatus,
379
- errorMessage,
380
- messageText,
381
- fileInput,
382
- triggerFilePicker,
383
- onFilesSelected,
384
- removeAttachment,
385
- sendMessage,
386
- addNewLine,
387
- }
388
- }
389
- }