llms-py 3.0.0__py3-none-any.whl → 3.0.0b1__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/index.html +77 -35
  2. llms/llms.json +23 -72
  3. llms/main.py +732 -1786
  4. llms/providers.json +1 -1
  5. llms/{extensions/analytics/ui/index.mjs → ui/Analytics.mjs} +238 -154
  6. llms/ui/App.mjs +60 -151
  7. llms/ui/Avatar.mjs +85 -0
  8. llms/ui/Brand.mjs +52 -0
  9. llms/ui/ChatPrompt.mjs +606 -0
  10. llms/ui/Main.mjs +873 -0
  11. llms/ui/ModelSelector.mjs +693 -0
  12. llms/ui/OAuthSignIn.mjs +92 -0
  13. llms/ui/ProviderIcon.mjs +36 -0
  14. llms/ui/ProviderStatus.mjs +105 -0
  15. llms/{extensions/app/ui → ui}/Recents.mjs +65 -91
  16. llms/ui/{modules/chat/SettingsDialog.mjs → SettingsDialog.mjs} +9 -9
  17. llms/{extensions/app/ui/index.mjs → ui/Sidebar.mjs} +58 -124
  18. llms/ui/SignIn.mjs +64 -0
  19. llms/ui/SystemPromptEditor.mjs +31 -0
  20. llms/ui/SystemPromptSelector.mjs +56 -0
  21. llms/ui/Welcome.mjs +8 -0
  22. llms/ui/ai.mjs +53 -125
  23. llms/ui/app.css +111 -1837
  24. llms/ui/lib/charts.mjs +13 -9
  25. llms/ui/lib/servicestack-vue.mjs +3 -3
  26. llms/ui/lib/vue.min.mjs +9 -10
  27. llms/ui/lib/vue.mjs +1602 -1763
  28. llms/ui/markdown.mjs +2 -10
  29. llms/ui/tailwind.input.css +80 -496
  30. llms/ui/threadStore.mjs +572 -0
  31. llms/ui/utils.mjs +117 -113
  32. llms/ui.json +1069 -0
  33. {llms_py-3.0.0.dist-info → llms_py-3.0.0b1.dist-info}/METADATA +1 -1
  34. llms_py-3.0.0b1.dist-info/RECORD +49 -0
  35. llms/__pycache__/__init__.cpython-312.pyc +0 -0
  36. llms/__pycache__/__init__.cpython-313.pyc +0 -0
  37. llms/__pycache__/__init__.cpython-314.pyc +0 -0
  38. llms/__pycache__/__main__.cpython-312.pyc +0 -0
  39. llms/__pycache__/__main__.cpython-314.pyc +0 -0
  40. llms/__pycache__/llms.cpython-312.pyc +0 -0
  41. llms/__pycache__/main.cpython-312.pyc +0 -0
  42. llms/__pycache__/main.cpython-313.pyc +0 -0
  43. llms/__pycache__/main.cpython-314.pyc +0 -0
  44. llms/__pycache__/plugins.cpython-314.pyc +0 -0
  45. llms/extensions/app/README.md +0 -20
  46. llms/extensions/app/__init__.py +0 -530
  47. llms/extensions/app/__pycache__/__init__.cpython-314.pyc +0 -0
  48. llms/extensions/app/__pycache__/db.cpython-314.pyc +0 -0
  49. llms/extensions/app/__pycache__/db_manager.cpython-314.pyc +0 -0
  50. llms/extensions/app/db.py +0 -644
  51. llms/extensions/app/db_manager.py +0 -195
  52. llms/extensions/app/requests.json +0 -9073
  53. llms/extensions/app/threads.json +0 -15290
  54. llms/extensions/app/ui/threadStore.mjs +0 -411
  55. llms/extensions/core_tools/CALCULATOR.md +0 -32
  56. llms/extensions/core_tools/__init__.py +0 -598
  57. llms/extensions/core_tools/__pycache__/__init__.cpython-314.pyc +0 -0
  58. llms/extensions/core_tools/ui/codemirror/addon/edit/closebrackets.js +0 -201
  59. llms/extensions/core_tools/ui/codemirror/addon/edit/closetag.js +0 -185
  60. llms/extensions/core_tools/ui/codemirror/addon/edit/continuelist.js +0 -101
  61. llms/extensions/core_tools/ui/codemirror/addon/edit/matchbrackets.js +0 -160
  62. llms/extensions/core_tools/ui/codemirror/addon/edit/matchtags.js +0 -66
  63. llms/extensions/core_tools/ui/codemirror/addon/edit/trailingspace.js +0 -27
  64. llms/extensions/core_tools/ui/codemirror/addon/selection/active-line.js +0 -72
  65. llms/extensions/core_tools/ui/codemirror/addon/selection/mark-selection.js +0 -119
  66. llms/extensions/core_tools/ui/codemirror/addon/selection/selection-pointer.js +0 -98
  67. llms/extensions/core_tools/ui/codemirror/doc/docs.css +0 -225
  68. llms/extensions/core_tools/ui/codemirror/doc/source_sans.woff +0 -0
  69. llms/extensions/core_tools/ui/codemirror/lib/codemirror.css +0 -344
  70. llms/extensions/core_tools/ui/codemirror/lib/codemirror.js +0 -9884
  71. llms/extensions/core_tools/ui/codemirror/mode/clike/clike.js +0 -942
  72. llms/extensions/core_tools/ui/codemirror/mode/javascript/index.html +0 -118
  73. llms/extensions/core_tools/ui/codemirror/mode/javascript/javascript.js +0 -962
  74. llms/extensions/core_tools/ui/codemirror/mode/javascript/typescript.html +0 -62
  75. llms/extensions/core_tools/ui/codemirror/mode/python/python.js +0 -402
  76. llms/extensions/core_tools/ui/codemirror/theme/dracula.css +0 -40
  77. llms/extensions/core_tools/ui/codemirror/theme/mocha.css +0 -135
  78. llms/extensions/core_tools/ui/index.mjs +0 -650
  79. llms/extensions/gallery/README.md +0 -61
  80. llms/extensions/gallery/__init__.py +0 -61
  81. llms/extensions/gallery/__pycache__/__init__.cpython-314.pyc +0 -0
  82. llms/extensions/gallery/__pycache__/db.cpython-314.pyc +0 -0
  83. llms/extensions/gallery/db.py +0 -298
  84. llms/extensions/gallery/ui/index.mjs +0 -482
  85. llms/extensions/katex/README.md +0 -39
  86. llms/extensions/katex/__init__.py +0 -6
  87. llms/extensions/katex/__pycache__/__init__.cpython-314.pyc +0 -0
  88. llms/extensions/katex/ui/README.md +0 -125
  89. llms/extensions/katex/ui/contrib/auto-render.js +0 -338
  90. llms/extensions/katex/ui/contrib/auto-render.min.js +0 -1
  91. llms/extensions/katex/ui/contrib/auto-render.mjs +0 -244
  92. llms/extensions/katex/ui/contrib/copy-tex.js +0 -127
  93. llms/extensions/katex/ui/contrib/copy-tex.min.js +0 -1
  94. llms/extensions/katex/ui/contrib/copy-tex.mjs +0 -105
  95. llms/extensions/katex/ui/contrib/mathtex-script-type.js +0 -109
  96. llms/extensions/katex/ui/contrib/mathtex-script-type.min.js +0 -1
  97. llms/extensions/katex/ui/contrib/mathtex-script-type.mjs +0 -24
  98. llms/extensions/katex/ui/contrib/mhchem.js +0 -3213
  99. llms/extensions/katex/ui/contrib/mhchem.min.js +0 -1
  100. llms/extensions/katex/ui/contrib/mhchem.mjs +0 -3109
  101. llms/extensions/katex/ui/contrib/render-a11y-string.js +0 -887
  102. llms/extensions/katex/ui/contrib/render-a11y-string.min.js +0 -1
  103. llms/extensions/katex/ui/contrib/render-a11y-string.mjs +0 -800
  104. llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.ttf +0 -0
  105. llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.woff +0 -0
  106. llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  107. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
  108. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
  109. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  110. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
  111. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
  112. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  113. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
  114. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.woff +0 -0
  115. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  116. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
  117. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.woff +0 -0
  118. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  119. llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.ttf +0 -0
  120. llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.woff +0 -0
  121. llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.woff2 +0 -0
  122. llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
  123. llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.woff +0 -0
  124. llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  125. llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.ttf +0 -0
  126. llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.woff +0 -0
  127. llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.woff2 +0 -0
  128. llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.ttf +0 -0
  129. llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.woff +0 -0
  130. llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.woff2 +0 -0
  131. llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
  132. llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.woff +0 -0
  133. llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  134. llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.ttf +0 -0
  135. llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.woff +0 -0
  136. llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.woff2 +0 -0
  137. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
  138. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.woff +0 -0
  139. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  140. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
  141. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.woff +0 -0
  142. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  143. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
  144. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.woff +0 -0
  145. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  146. llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.ttf +0 -0
  147. llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.woff +0 -0
  148. llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.woff2 +0 -0
  149. llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.ttf +0 -0
  150. llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.woff +0 -0
  151. llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  152. llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.ttf +0 -0
  153. llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.woff +0 -0
  154. llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  155. llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.ttf +0 -0
  156. llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.woff +0 -0
  157. llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  158. llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.ttf +0 -0
  159. llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.woff +0 -0
  160. llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  161. llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
  162. llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.woff +0 -0
  163. llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  164. llms/extensions/katex/ui/index.mjs +0 -92
  165. llms/extensions/katex/ui/katex-swap.css +0 -1230
  166. llms/extensions/katex/ui/katex-swap.min.css +0 -1
  167. llms/extensions/katex/ui/katex.css +0 -1230
  168. llms/extensions/katex/ui/katex.js +0 -19080
  169. llms/extensions/katex/ui/katex.min.css +0 -1
  170. llms/extensions/katex/ui/katex.min.js +0 -1
  171. llms/extensions/katex/ui/katex.min.mjs +0 -1
  172. llms/extensions/katex/ui/katex.mjs +0 -18547
  173. llms/extensions/providers/__init__.py +0 -18
  174. llms/extensions/providers/__pycache__/__init__.cpython-314.pyc +0 -0
  175. llms/extensions/providers/__pycache__/anthropic.cpython-314.pyc +0 -0
  176. llms/extensions/providers/__pycache__/chutes.cpython-314.pyc +0 -0
  177. llms/extensions/providers/__pycache__/google.cpython-314.pyc +0 -0
  178. llms/extensions/providers/__pycache__/nvidia.cpython-314.pyc +0 -0
  179. llms/extensions/providers/__pycache__/openai.cpython-314.pyc +0 -0
  180. llms/extensions/providers/__pycache__/openrouter.cpython-314.pyc +0 -0
  181. llms/extensions/providers/anthropic.py +0 -229
  182. llms/extensions/providers/chutes.py +0 -155
  183. llms/extensions/providers/google.py +0 -378
  184. llms/extensions/providers/nvidia.py +0 -105
  185. llms/extensions/providers/openai.py +0 -156
  186. llms/extensions/providers/openrouter.py +0 -72
  187. llms/extensions/system_prompts/README.md +0 -22
  188. llms/extensions/system_prompts/__init__.py +0 -45
  189. llms/extensions/system_prompts/__pycache__/__init__.cpython-314.pyc +0 -0
  190. llms/extensions/system_prompts/ui/index.mjs +0 -280
  191. llms/extensions/system_prompts/ui/prompts.json +0 -1067
  192. llms/extensions/tools/__init__.py +0 -5
  193. llms/extensions/tools/__pycache__/__init__.cpython-314.pyc +0 -0
  194. llms/extensions/tools/ui/index.mjs +0 -204
  195. llms/providers-extra.json +0 -356
  196. llms/ui/ctx.mjs +0 -365
  197. llms/ui/index.mjs +0 -129
  198. llms/ui/modules/chat/ChatBody.mjs +0 -691
  199. llms/ui/modules/chat/index.mjs +0 -828
  200. llms/ui/modules/layout.mjs +0 -243
  201. llms/ui/modules/model-selector.mjs +0 -851
  202. llms_py-3.0.0.dist-info/RECORD +0 -202
  203. {llms_py-3.0.0.dist-info → llms_py-3.0.0b1.dist-info}/WHEEL +0 -0
  204. {llms_py-3.0.0.dist-info → llms_py-3.0.0b1.dist-info}/entry_points.txt +0 -0
  205. {llms_py-3.0.0.dist-info → llms_py-3.0.0b1.dist-info}/licenses/LICENSE +0 -0
  206. {llms_py-3.0.0.dist-info → llms_py-3.0.0b1.dist-info}/top_level.txt +0 -0
@@ -1,828 +0,0 @@
1
-
2
- import { ref, watch, nextTick, inject } from 'vue'
3
- import { $$, createElement, lastRightPart, ApiResult, createErrorStatus, pick } from "@servicestack/client"
4
- import SettingsDialog, { useSettings } from './SettingsDialog.mjs'
5
- import ChatBody from './ChatBody.mjs'
6
- import { AppContext } from '../../ctx.mjs'
7
-
8
- const imageExts = 'png,webp,jpg,jpeg,gif,bmp,svg,tiff,ico'.split(',')
9
- const audioExts = 'mp3,wav,ogg,flac,m4a,opus,webm'.split(',')
10
-
11
- /* Example image generation request: https://openrouter.ai/docs/guides/overview/multimodal/image-generation
12
- {
13
- "model": "google/gemini-2.5-flash-image-preview",
14
- "messages": [
15
- {
16
- "role": "user",
17
- "content": "Create a picture of a nano banana dish in a fancy restaurant with a Gemini theme"
18
- }
19
- ],
20
- "modalities": ["image", "text"],
21
- "image_config": {
22
- "aspect_ratio": "16:9"
23
- }
24
- }
25
- Example response:
26
- {
27
- "choices": [
28
- {
29
- "message": {
30
- "role": "assistant",
31
- "content": "I've generated a beautiful sunset image for you.",
32
- "images": [
33
- {
34
- "type": "image_url",
35
- "image_url": {
36
- "url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA..."
37
- }
38
- }
39
- ]
40
- }
41
- }
42
- ]
43
- }
44
- */
45
- const imageAspectRatios = {
46
- '1024×1024': '1:1',
47
- '832×1248': '2:3',
48
- '1248×832': '3:2',
49
- '864×1184': '3:4',
50
- '1184×864': '4:3',
51
- '896×1152': '4:5',
52
- '1152×896': '5:4',
53
- '768×1344': '9:16',
54
- '1344×768': '16:9',
55
- '1536×672': '21:9',
56
- }
57
- // Reverse lookup
58
- const imageRatioSizes = Object.entries(imageAspectRatios).reduce((acc, [key, value]) => {
59
- acc[value] = key
60
- return acc
61
- }, {})
62
-
63
- const svg = {
64
- clipboard: `<svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g fill="none"><path d="M8 5H6a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-1M8 5a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2M8 5a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2m0 0h2a2 2 0 0 1 2 2v3m2 4H10m0 0l3-3m-3 3l3 3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></g></svg>`,
65
- check: `<svg class="w-6 h-6 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg>`,
66
- }
67
-
68
- function copyBlock(btn) {
69
- // console.log('copyBlock',btn)
70
- const label = btn.previousElementSibling
71
- const code = btn.parentElement.nextElementSibling
72
- label.classList.remove('hidden')
73
- label.innerHTML = 'copied'
74
- btn.classList.add('border-gray-600', 'bg-gray-700')
75
- btn.classList.remove('border-gray-700')
76
- btn.innerHTML = svg.check
77
- navigator.clipboard.writeText(code.innerText)
78
- setTimeout(() => {
79
- label.classList.add('hidden')
80
- label.innerHTML = ''
81
- btn.innerHTML = svg.clipboard
82
- btn.classList.remove('border-gray-600', 'bg-gray-700')
83
- btn.classList.add('border-gray-700')
84
- }, 2000)
85
- }
86
-
87
- function addCopyButtonToCodeBlocks(sel) {
88
- globalThis.copyBlock ??= copyBlock
89
- //console.log('addCopyButtonToCodeBlocks', sel, [...$$(sel)].length)
90
-
91
- $$(sel).forEach(code => {
92
- let pre = code.parentElement;
93
- if (pre.classList.contains('group')) return
94
- pre.classList.add('relative', 'group')
95
-
96
- const div = createElement('div', { attrs: { className: 'opacity-0 group-hover:opacity-100 transition-opacity duration-100 flex absolute right-2 -mt-1 select-none' } })
97
- const label = createElement('div', { attrs: { className: 'hidden font-sans p-1 px-2 mr-1 rounded-md border border-gray-600 bg-gray-700 text-gray-400' } })
98
- const btn = createElement('button', {
99
- attrs: {
100
- type: 'button',
101
- className: 'p-1 rounded-md border block text-gray-500 hover:text-gray-400 border-gray-700 hover:border-gray-600',
102
- onclick: 'copyBlock(this)'
103
- }
104
- })
105
- btn.innerHTML = svg.clipboard
106
- div.appendChild(label)
107
- div.appendChild(btn)
108
- pre.insertBefore(div, code)
109
- })
110
- }
111
-
112
- export function addCopyButtons() {
113
- addCopyButtonToCodeBlocks('.prose pre>code')
114
- }
115
-
116
- export function useChatPrompt(ctx) {
117
- const messageText = ref('')
118
- const attachedFiles = ref([])
119
- const hasImage = () => attachedFiles.value.some(f => imageExts.includes(lastRightPart(f.name, '.')))
120
- const hasAudio = () => attachedFiles.value.some(f => audioExts.includes(lastRightPart(f.name, '.')))
121
- const hasFile = () => attachedFiles.value.length > 0
122
-
123
- const editingMessage = ref(null)
124
-
125
- function reset() {
126
- // Ensure initial state is ready to accept input
127
- attachedFiles.value = []
128
- messageText.value = ''
129
- editingMessage.value = null
130
- }
131
-
132
- const settings = useSettings()
133
-
134
- function getModel(name) {
135
- return ctx.state.models.find(x => x.name === name) ?? ctx.state.models.find(x => x.id === name)
136
- }
137
-
138
- function getSelectedModel() {
139
- const candidates = [ctx.state.selectedModel, ctx.state.config.defaults.text.model]
140
- const ret = candidates.map(name => name && getModel(name)).find(x => !!x)
141
- if (!ret) {
142
- // Try to find a model in the latest threads
143
- for (const thread in ctx.threads.threads) {
144
- const model = thread.model && getModel(thread.model)
145
- if (model) return model
146
- }
147
- }
148
- return ret
149
- }
150
-
151
- function setSelectedModel(model) {
152
- ctx.setState({
153
- selectedModel: model.name
154
- })
155
- ctx.setPrefs({
156
- model: model.name
157
- })
158
- }
159
-
160
- function getProviderForModel(model) {
161
- return getModel(model)?.provider
162
- }
163
-
164
- const canGenerateImage = model => {
165
- return model?.modalities?.output?.includes('image')
166
- }
167
- const canGenerateAudio = model => {
168
- return model?.modalities?.output?.includes('audio')
169
- }
170
-
171
- function applySettings(request) {
172
- settings.applySettings(request)
173
- }
174
-
175
- function createContent({ text, files }) {
176
- let content = []
177
-
178
- // Add Text Block
179
- if (text) {
180
- content.push({ type: 'text', text })
181
- }
182
-
183
- // Add Attachment Blocks
184
- if (Array.isArray(files)) {
185
- for (const f of files) {
186
- const ext = lastRightPart(f.name, '.')
187
- if (imageExts.includes(ext)) {
188
- content.push({ type: 'image_url', image_url: { url: f.url } })
189
- } else if (audioExts.includes(ext)) {
190
- content.push({ type: 'input_audio', input_audio: { data: f.url, format: ext } })
191
- } else {
192
- content.push({ type: 'file', file: { file_data: f.url, filename: f.name } })
193
- }
194
- }
195
- }
196
- return content
197
- }
198
-
199
- function createRequest({ model, text, files, systemPrompt, aspectRatio }) {
200
- // Construct API Request from History
201
- const request = {
202
- model: model.name,
203
- messages: [],
204
- metadata: {}
205
- }
206
-
207
- // Apply user settings
208
- applySettings(request)
209
-
210
- if (systemPrompt) {
211
- request.messages = request.messages.filter(m => m.role !== 'system')
212
- request.messages.unshift({
213
- role: 'system',
214
- content: systemPrompt
215
- })
216
- }
217
-
218
- if (canGenerateImage(model)) {
219
- request.image_config = {
220
- aspect_ratio: aspectRatio || imageAspectRatios[ctx.state.selectedAspectRatio] || '1:1'
221
- }
222
- request.modalities = ["image", "text"]
223
- }
224
- else if (canGenerateAudio(model)) {
225
- request.modalities = ["audio", "text"]
226
- }
227
-
228
- if (text) {
229
- const content = createContent({ text, files })
230
- request.messages.push({
231
- role: 'user',
232
- content
233
- })
234
- }
235
-
236
- return request
237
- }
238
-
239
- async function completion({ request, thread, model, controller, redirect }) {
240
- try {
241
- let error
242
- if (!model) {
243
- if (request.model) {
244
- model = getModel(request.model)
245
- } else {
246
- model = getModel(request.model) ?? getSelectedModel()
247
- }
248
- }
249
-
250
- if (!model) {
251
- return ctx.createErrorResult({ message: `Model ${request.model || ''} not found`, errorCode: 'NotFound' })
252
- }
253
-
254
- if (!request.messages) request.messages = []
255
- if (!request.metadata) request.metadata = {}
256
-
257
- if (!thread) {
258
- const title = getTextContent(request) || 'New Chat'
259
- thread = await ctx.threads.startNewThread({ title, model, redirect })
260
- }
261
-
262
- const threadId = thread?.id
263
-
264
- const ctxRequest = {
265
- request,
266
- thread,
267
- }
268
- ctx.chatRequestFilters.forEach(f => f(ctxRequest))
269
-
270
- console.debug('completion.request', request)
271
-
272
- // Send to API
273
- const startTime = Date.now()
274
- const res = await ctx.post('/v1/chat/completions', {
275
- body: JSON.stringify(request),
276
- signal: controller?.signal
277
- })
278
-
279
- let response = null
280
- if (!res.ok) {
281
- error = ctx.createErrorStatus({ message: `HTTP ${res.status} ${res.statusText}` })
282
- let errorBody = null
283
- try {
284
- errorBody = await res.text()
285
- if (errorBody) {
286
- // Try to parse as JSON for better formatting
287
- try {
288
- const errorJson = JSON.parse(errorBody)
289
- const status = errorJson?.responseStatus
290
- if (status) {
291
- error.errorCode += ` ${status.errorCode}`
292
- error.message = status.message
293
- error.stackTrace = status.stackTrace
294
- } else {
295
- error.stackTrace = JSON.stringify(errorJson, null, 2)
296
- }
297
- } catch (e) {
298
- }
299
- }
300
- } catch (e) {
301
- // If we can't read the response body, just use the status
302
- }
303
- } else {
304
- try {
305
- response = await res.json()
306
- const ctxResponse = {
307
- response,
308
- thread,
309
- }
310
- ctx.chatResponseFilters.forEach(f => f(ctxResponse))
311
- console.debug('completion.response', JSON.stringify(response, null, 2))
312
- } catch (e) {
313
- error = createErrorStatus(e.message)
314
- }
315
- }
316
-
317
- if (response?.error) {
318
- error ??= createErrorStatus()
319
- error.message = response.error
320
- }
321
-
322
- if (error) {
323
- ctx.chatErrorFilters.forEach(f => f(error))
324
- return new ApiResult({ error })
325
- }
326
-
327
- if (!error) {
328
- // Add tool history messages if any
329
- if (response.tool_history && Array.isArray(response.tool_history)) {
330
- for (const msg of response.tool_history) {
331
- if (msg.role === 'assistant') {
332
- msg.model = model.name // tag with model
333
- }
334
- }
335
- }
336
-
337
- nextTick(addCopyButtons)
338
-
339
- return new ApiResult({ response })
340
- }
341
- } catch (e) {
342
- console.log('completion.error', e)
343
- return new ApiResult({ error: createErrorStatus(e.message, 'ChatFailed') })
344
- }
345
- }
346
- function getTextContent(chat) {
347
- const textMessage = chat.messages.find(m =>
348
- m.role === 'user' && Array.isArray(m.content) && m.content.some(c => c.type === 'text'))
349
- return textMessage?.content.find(c => c.type === 'text')?.text || ''
350
- }
351
- function getAnswer(response) {
352
- const textMessage = response.choices?.[0]?.message
353
- return textMessage?.content || ''
354
- }
355
- function selectAspectRatio(ratio) {
356
- const selectedAspectRatio = imageRatioSizes[ratio] || '1024×1024'
357
- console.log(`selectAspectRatio(${ratio})`, selectedAspectRatio)
358
- ctx.setState({ selectedAspectRatio })
359
- }
360
-
361
- return {
362
- completion,
363
- createContent,
364
- createRequest,
365
- applySettings,
366
- messageText,
367
- attachedFiles,
368
- editingMessage,
369
- hasImage,
370
- hasAudio,
371
- hasFile,
372
- reset,
373
- settings,
374
- addCopyButtons,
375
- getModel,
376
- getSelectedModel,
377
- setSelectedModel,
378
- getProviderForModel,
379
- canGenerateImage,
380
- canGenerateAudio,
381
- getTextContent,
382
- getAnswer,
383
- selectAspectRatio,
384
- }
385
- }
386
-
387
- const ChatPrompt = {
388
- template: `
389
- <div class="mx-auto max-w-3xl">
390
- <SettingsDialog :isOpen="showSettings" @close="showSettings = false" />
391
- <div class="flex space-x-2">
392
- <!-- Attach (+) button and Settings button -->
393
- <div class="mt-1.5 flex flex-col space-y-1 items-center">
394
- <div>
395
- <button type="button"
396
- @click="triggerFilePicker"
397
- :disabled="$threads.isWatchingThread.value || !model"
398
- 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"
399
- title="Attach image or audio">
400
- <svg class="size-5" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 256 256">
401
- <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>
402
- </svg>
403
- </button>
404
- <!-- Hidden file input -->
405
- <input ref="fileInput" type="file" multiple @change="onFilesSelected"
406
- class="hidden" accept="image/*,audio/*,.pdf,.doc,.docx,.xml,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document"
407
- />
408
- </div>
409
- <div>
410
- <button type="button" title="Settings" @click="showSettings = true"
411
- :disabled="$threads.watchingThread || !model"
412
- 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">
413
- <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>
414
- </button>
415
- </div>
416
- </div>
417
-
418
- <div class="flex-1">
419
- <div class="relative">
420
- <textarea
421
- ref="refMessage"
422
- v-model="messageText"
423
- @keydown.enter.exact.prevent="sendMessage"
424
- @keydown.enter.shift.exact="addNewLine"
425
- @paste="onPaste"
426
- @dragover="onDragOver"
427
- @dragleave="onDragLeave"
428
- @drop="onDrop"
429
- placeholder="Type message... (Enter to send, Shift+Enter for new line, drag & drop or paste files)"
430
- rows="3"
431
- :class="[
432
- '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',
433
- isDragging
434
- ? 'border-blue-500 bg-blue-50 dark:bg-blue-900/30 ring-1 ring-blue-500'
435
- : 'border-gray-300 dark:border-gray-600 focus:border-blue-500 focus:ring-blue-500'
436
- ]"
437
- :disabled="$threads.watchingThread || !model"
438
- ></textarea>
439
- <button v-if="!$threads.watchingThread" title="Send (Enter)" type="button"
440
- @click="sendMessage"
441
- :disabled="!messageText.trim() || $threads.watchingThread || !model"
442
- 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">
443
- <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>
444
- </button>
445
- <button v-else title="Cancel request" type="button"
446
- @click="$threads.cancelThread()"
447
- 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">
448
- <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">
449
- <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
450
- </svg>
451
- </button>
452
- </div>
453
-
454
- <!-- Attachments & Image Options -->
455
- <div class="mt-2 flex justify-between items-start gap-2">
456
- <div class="flex flex-wrap gap-2">
457
- <div v-for="(f,i) in $chat.attachedFiles.value" :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">
458
- <span class="truncate max-w-48" :title="f.name">{{ f.name }}</span>
459
- <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">
460
- <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>
461
- </button>
462
- </div>
463
- </div>
464
-
465
- <!-- Image Aspect Ratio Selector -->
466
- <div v-if="$chat.canGenerateImage(model)" class="min-w-[120px]">
467
- <select name="aspect_ratio" v-model="$state.selectedAspectRatio"
468
- class="block w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-xs text-gray-700 dark:text-gray-300 pl-2 pr-6 py-1 focus:ring-blue-500 focus:border-blue-500">
469
- <option v-for="(ratio, size) in imageAspectRatios" :key="size" :value="size">
470
- {{ size }} ({{ ratio }})
471
- </option>
472
- </select>
473
- </div>
474
- </div>
475
-
476
- <div v-if="!model" class="mt-2 text-sm text-red-600 dark:text-red-400">
477
- Please select a model
478
- </div>
479
- </div>
480
- </div>
481
- </div>
482
- `,
483
- props: {
484
- model: {
485
- type: Object,
486
- default: null
487
- }
488
- },
489
- setup(props) {
490
- const ctx = inject('ctx')
491
- const config = ctx.state.config
492
- const {
493
- messageText,
494
- hasImage,
495
- hasAudio,
496
- hasFile,
497
- getTextContent,
498
- } = ctx.chat
499
-
500
- const fileInput = ref(null)
501
- const refMessage = ref(null)
502
- const showSettings = ref(false)
503
-
504
- // File attachments (+) handlers
505
- const triggerFilePicker = () => {
506
- if (fileInput.value) fileInput.value.click()
507
- }
508
- const onFilesSelected = async (e) => {
509
- const files = Array.from(e.target?.files || [])
510
- if (files.length) {
511
- // Upload files immediately
512
- const uploadedFiles = await Promise.all(files.map(async f => {
513
- try {
514
- const response = await ctx.ai.uploadFile(f)
515
- const metadata = {
516
- url: response.url,
517
- name: f.name,
518
- size: response.size,
519
- type: f.type,
520
- width: response.width,
521
- height: response.height,
522
- threadId: ctx.threads.currentThread.value?.id,
523
- created: Date.now()
524
- }
525
-
526
- return {
527
- ...metadata,
528
- file: f // Keep original file for preview/fallback if needed
529
- }
530
- } catch (error) {
531
- ctx.setError({
532
- errorCode: 'Upload Failed',
533
- message: `Failed to upload ${f.name}: ${error.message}`
534
- })
535
- return null
536
- }
537
- }))
538
-
539
- ctx.chat.attachedFiles.value.push(...uploadedFiles.filter(f => f))
540
- }
541
-
542
- // allow re-selecting the same file
543
- if (fileInput.value) fileInput.value.value = ''
544
-
545
- if (!messageText.value?.trim()) {
546
- if (hasImage()) {
547
- messageText.value = getTextContent(config.defaults.image)
548
- } else if (hasAudio()) {
549
- messageText.value = getTextContent(config.defaults.audio)
550
- } else {
551
- messageText.value = getTextContent(config.defaults.file)
552
- }
553
- }
554
- }
555
- const removeAttachment = (i) => {
556
- ctx.chat.attachedFiles.value.splice(i, 1)
557
- }
558
-
559
- // Handle paste events for clipboard images, audio, and files
560
- const onPaste = async (e) => {
561
- // Use the paste event's clipboardData directly (works best for paste events)
562
- const items = e.clipboardData?.items
563
- if (!items) return
564
-
565
- const files = []
566
-
567
- // Check all clipboard items
568
- for (let i = 0; i < items.length; i++) {
569
- const item = items[i]
570
-
571
- // Handle files (images, audio, etc.)
572
- if (item.kind === 'file') {
573
- const file = item.getAsFile()
574
- if (file) {
575
- // Generate a better filename based on type
576
- let filename = file.name
577
- if (!filename || filename === 'image.png' || filename === 'blob') {
578
- const ext = file.type.split('/')[1] || 'png'
579
- const timestamp = new Date().getTime()
580
- if (file.type.startsWith('image/')) {
581
- filename = `pasted-image-${timestamp}.${ext}`
582
- } else if (file.type.startsWith('audio/')) {
583
- filename = `pasted-audio-${timestamp}.${ext}`
584
- } else {
585
- filename = `pasted-file-${timestamp}.${ext}`
586
- }
587
- // Create a new File object with the better name
588
- files.push(new File([file], filename, { type: file.type }))
589
- } else {
590
- files.push(file)
591
- }
592
- }
593
- }
594
- }
595
-
596
- if (files.length > 0) {
597
- e.preventDefault()
598
- // Reuse the same logic as onFilesSelected for consistency
599
- const event = { target: { files: files } }
600
- await onFilesSelected(event)
601
- }
602
- }
603
-
604
- // Handle drag and drop events
605
- const isDragging = ref(false)
606
-
607
- const onDragOver = (e) => {
608
- e.preventDefault()
609
- e.stopPropagation()
610
- isDragging.value = true
611
- }
612
-
613
- const onDragLeave = (e) => {
614
- e.preventDefault()
615
- e.stopPropagation()
616
- isDragging.value = false
617
- }
618
-
619
- const onDrop = async (e) => {
620
- e.preventDefault()
621
- e.stopPropagation()
622
- isDragging.value = false
623
-
624
- const files = Array.from(e.dataTransfer?.files || [])
625
- if (files.length > 0) {
626
- // Reuse the same logic as onFilesSelected for consistency
627
- const event = { target: { files: files } }
628
- await onFilesSelected(event)
629
- }
630
- }
631
-
632
- // Send message
633
- const sendMessage = async () => {
634
- if (!messageText.value?.trim() && !hasImage() && !hasAudio() && !hasFile()) return
635
- if (ctx.threads.isWatchingThread.value || !props.model) return
636
-
637
- ctx.clearError()
638
-
639
- // 1. Construct Structured Content (Text + Attachments)
640
- let text = messageText.value.trim()
641
-
642
- messageText.value = ''
643
- let content = ctx.chat.createContent({ text, files: ctx.chat.attachedFiles.value })
644
-
645
- let thread
646
-
647
- // Create thread if none exists
648
- if (!ctx.threads.currentThread.value) {
649
- thread = await ctx.threads.startNewThread({ model: props.model, redirect: true })
650
- } else {
651
- thread = ctx.threads.currentThread.value
652
- }
653
-
654
- let threadId = thread.id
655
- let messages = thread.messages || []
656
- if (!threadId) {
657
- console.error('No thread ID found', thread, ctx.threads.currentThread.value)
658
- return
659
- }
660
-
661
- // Handle Editing / Redo Logic
662
- const editingMessage = ctx.chat.editingMessage.value
663
- if (editingMessage) {
664
- let messageIndex = messages.findIndex(m => m.timestamp === editingMessage)
665
- if (messageIndex == -1) {
666
- messageIndex = messages.findLastIndex(m => m.role === 'user')
667
- }
668
- console.log('Editing message', editingMessage, messageIndex, messages)
669
-
670
- if (messageIndex >= 0) {
671
- messages[messageIndex].content = content
672
- // Truncate messages to only include up to the edited message
673
- messages.length = messageIndex + 1
674
- } else {
675
- messages.push({
676
- timestamp: new Date().valueOf(),
677
- role: 'user',
678
- content,
679
- })
680
- }
681
- } else {
682
- // Regular Send Logic
683
- const lastMessage = messages[messages.length - 1]
684
-
685
- // Check duplicate based on text content extracted from potential array
686
- const getLastText = (msgContent) => {
687
- if (typeof msgContent === 'string') return msgContent
688
- if (Array.isArray(msgContent)) return msgContent.find(c => c.type === 'text')?.text || ''
689
- return ''
690
- }
691
- const newText = text // content[0].text
692
- const lastText = lastMessage && lastMessage.role === 'user' ? getLastText(lastMessage.content) : null
693
- const isDuplicate = lastText === newText
694
-
695
- // Add user message only if it's not a duplicate
696
- // Note: We are saving the FULL STRUCTURED CONTENT array here
697
- if (!isDuplicate) {
698
- messages.push({
699
- timestamp: new Date().valueOf(),
700
- role: 'user',
701
- content,
702
- })
703
- }
704
- }
705
-
706
- const request = ctx.chat.createRequest({ model: props.model })
707
-
708
- // Add Thread History
709
- messages.forEach(m => {
710
- request.messages.push(m)
711
- })
712
-
713
- // Update Thread Title if not set or is default
714
- if (!thread.title || thread.title === 'New Chat' || request.title === 'New Chat') {
715
- request.title = text.length > 100
716
- ? text.slice(0, 100) + '...'
717
- : text
718
- console.debug(`changing thread title from '${thread.title}' to '${request.title}'`)
719
- } else {
720
- console.debug(`thread title is '${thread.title}'`, request.title)
721
- }
722
-
723
- const api = await ctx.threads.queueChat({ request, thread })
724
- if (api.response) {
725
- // success
726
- ctx.chat.editingMessage.value = null
727
- ctx.chat.attachedFiles.value = []
728
- thread = api.response
729
- ctx.threads.replaceThread(thread)
730
- } else {
731
- ctx.setError(api.error)
732
- }
733
-
734
- // Restore focus to the textarea
735
- nextTick(() => {
736
- refMessage.value?.focus()
737
- })
738
- }
739
-
740
- const addNewLine = () => {
741
- // Enter key already adds new line
742
- //messageText.value += '\n'
743
- }
744
-
745
- watch(() => ctx.state.selectedAspectRatio, newValue => {
746
- ctx.setPrefs({ aspectRatio: newValue })
747
- })
748
-
749
- watch(() => ctx.layout.path, newValue => {
750
- if (newValue === '/' || newValue.startsWith('/c/')) {
751
- nextTick(() => {
752
- refMessage.value?.focus()
753
- })
754
- }
755
- })
756
-
757
- return {
758
- messageText,
759
- fileInput,
760
- refMessage,
761
- showSettings,
762
- isDragging,
763
- triggerFilePicker,
764
- onFilesSelected,
765
- onPaste,
766
- onDragOver,
767
- onDragLeave,
768
- onDrop,
769
- removeAttachment,
770
- sendMessage,
771
- addNewLine,
772
- imageAspectRatios,
773
- }
774
- }
775
- }
776
-
777
- const HomeTools = {
778
- template: `
779
- <div class="mt-4 flex space-x-3 justify-center items-center">
780
- <DarkModeToggle />
781
- </div>
782
- `,
783
- }
784
-
785
- export default {
786
- /**@param {AppContext} ctx */
787
- install(ctx) {
788
- const Home = ChatBody
789
- ctx.components({
790
- SettingsDialog,
791
- ChatPrompt,
792
- ChatBody,
793
- HomeTools,
794
- Home,
795
- })
796
- ctx.setGlobals({
797
- chat: useChatPrompt(ctx)
798
- })
799
-
800
- ctx.setLeftIcons({
801
- chat: {
802
- component: {
803
- template: `<svg @click="$ctx.togglePath('/')" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="currentColor" d="M8 2.19c3.13 0 5.68 2.25 5.68 5s-2.55 5-5.68 5a5.7 5.7 0 0 1-1.89-.29l-.75-.26l-.56.56a14 14 0 0 1-2 1.55a.13.13 0 0 1-.07 0v-.06a6.58 6.58 0 0 0 .15-4.29a5.25 5.25 0 0 1-.55-2.16c0-2.77 2.55-5 5.68-5M8 .94c-3.83 0-6.93 2.81-6.93 6.27a6.4 6.4 0 0 0 .64 2.64a5.53 5.53 0 0 1-.18 3.48a1.32 1.32 0 0 0 2 1.5a15 15 0 0 0 2.16-1.71a6.8 6.8 0 0 0 2.31.36c3.83 0 6.93-2.81 6.93-6.27S11.83.94 8 .94"/><ellipse cx="5.2" cy="7.7" fill="currentColor" rx=".8" ry=".75"/><ellipse cx="8" cy="7.7" fill="currentColor" rx=".8" ry=".75"/><ellipse cx="10.8" cy="7.7" fill="currentColor" rx=".8" ry=".75"/></svg>`,
804
- },
805
- isActive({ path }) {
806
- return path === '/' || path.startsWith('/c/')
807
- }
808
- }
809
- })
810
-
811
- const title = 'Chat'
812
- ctx.setState({
813
- title
814
- })
815
-
816
- const meta = { title }
817
- ctx.routes.push(...[
818
- { path: '/', component: Home, meta },
819
- { path: '/c/:id', component: ChatBody, meta },
820
- ])
821
-
822
- const prefs = ctx.getPrefs()
823
- if (prefs.model) {
824
- ctx.state.selectedModel = prefs.model
825
- }
826
- ctx.state.selectedAspectRatio = prefs.aspectRatio || '1:1'
827
- }
828
- }