llms-py 3.0.0b6__py3-none-any.whl → 3.0.0b7__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 (55) hide show
  1. llms/__pycache__/main.cpython-314.pyc +0 -0
  2. llms/{ui/modules/analytics.mjs → extensions/analytics/ui/index.mjs} +4 -2
  3. llms/extensions/core_tools/__init__.py +358 -0
  4. llms/extensions/core_tools/__pycache__/__init__.cpython-314.pyc +0 -0
  5. llms/extensions/gallery/__init__.py +61 -0
  6. llms/extensions/gallery/__pycache__/__init__.cpython-314.pyc +0 -0
  7. llms/extensions/gallery/__pycache__/db.cpython-314.pyc +0 -0
  8. llms/extensions/gallery/db.py +298 -0
  9. llms/extensions/gallery/ui/index.mjs +480 -0
  10. llms/extensions/providers/__init__.py +18 -0
  11. llms/extensions/providers/__pycache__/__init__.cpython-314.pyc +0 -0
  12. llms/{providers → extensions/providers}/__pycache__/anthropic.cpython-314.pyc +0 -0
  13. llms/extensions/providers/__pycache__/chutes.cpython-314.pyc +0 -0
  14. llms/extensions/providers/__pycache__/google.cpython-314.pyc +0 -0
  15. llms/{providers → extensions/providers}/__pycache__/nvidia.cpython-314.pyc +0 -0
  16. llms/{providers → extensions/providers}/__pycache__/openai.cpython-314.pyc +0 -0
  17. llms/extensions/providers/__pycache__/openrouter.cpython-314.pyc +0 -0
  18. llms/{providers → extensions/providers}/anthropic.py +1 -4
  19. llms/{providers → extensions/providers}/chutes.py +21 -18
  20. llms/{providers → extensions/providers}/google.py +99 -27
  21. llms/{providers → extensions/providers}/nvidia.py +6 -8
  22. llms/{providers → extensions/providers}/openai.py +3 -6
  23. llms/{providers → extensions/providers}/openrouter.py +12 -10
  24. llms/extensions/system_prompts/__init__.py +45 -0
  25. llms/extensions/system_prompts/__pycache__/__init__.cpython-314.pyc +0 -0
  26. llms/extensions/system_prompts/ui/index.mjs +284 -0
  27. llms/extensions/system_prompts/ui/prompts.json +1067 -0
  28. llms/{ui/modules/tools.mjs → extensions/tools/ui/index.mjs} +4 -2
  29. llms/llms.json +17 -1
  30. llms/main.py +381 -170
  31. llms/providers-extra.json +0 -32
  32. llms/ui/App.mjs +17 -18
  33. llms/ui/ai.mjs +10 -3
  34. llms/ui/app.css +1553 -24
  35. llms/ui/ctx.mjs +70 -12
  36. llms/ui/index.mjs +13 -8
  37. llms/ui/modules/chat/ChatBody.mjs +11 -248
  38. llms/ui/modules/chat/HomeTools.mjs +254 -0
  39. llms/ui/modules/chat/SettingsDialog.mjs +1 -1
  40. llms/ui/modules/chat/index.mjs +278 -174
  41. llms/ui/modules/layout.mjs +2 -26
  42. llms/ui/modules/model-selector.mjs +1 -1
  43. llms/ui/modules/threads/index.mjs +5 -11
  44. llms/ui/modules/threads/threadStore.mjs +56 -2
  45. llms/ui/utils.mjs +21 -3
  46. {llms_py-3.0.0b6.dist-info → llms_py-3.0.0b7.dist-info}/METADATA +1 -1
  47. llms_py-3.0.0b7.dist-info/RECORD +80 -0
  48. llms/providers/__pycache__/chutes.cpython-314.pyc +0 -0
  49. llms/providers/__pycache__/google.cpython-314.pyc +0 -0
  50. llms/providers/__pycache__/openrouter.cpython-314.pyc +0 -0
  51. llms_py-3.0.0b6.dist-info/RECORD +0 -66
  52. {llms_py-3.0.0b6.dist-info → llms_py-3.0.0b7.dist-info}/WHEEL +0 -0
  53. {llms_py-3.0.0b6.dist-info → llms_py-3.0.0b7.dist-info}/entry_points.txt +0 -0
  54. {llms_py-3.0.0b6.dist-info → llms_py-3.0.0b7.dist-info}/licenses/LICENSE +0 -0
  55. {llms_py-3.0.0b6.dist-info → llms_py-3.0.0b7.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,480 @@
1
+ import { ref, watch, computed, inject, onMounted, onUnmounted } from "vue"
2
+
3
+ let ext
4
+
5
+ const GalleryPage = {
6
+ template: `
7
+ <div class="w-full max-w-[1600px] mx-auto p-4 md:p-8 text-gray-900 dark:text-gray-200 font-sans selection:bg-blue-500/30">
8
+
9
+ <!-- Header -->
10
+ <div class="flex flex-col md:flex-row justify-between items-center mb-8 gap-4">
11
+
12
+ <!-- Left: Tabs -->
13
+ <div class="flex bg-gray-100 dark:bg-gray-800/50 p-1.5 rounded-xl border border-gray-200 dark:border-white/5 backdrop-blur-sm self-start md:self-auto">
14
+ <button type="button"
15
+ @click="setFilter('image')"
16
+ class="px-6 py-2 rounded-lg font-medium transition-all duration-200 text-sm"
17
+ :class="ext.prefs.type === 'image' ? 'bg-white dark:bg-blue-600 text-blue-600 dark:text-white shadow-sm dark:shadow-blue-500/20 shadow-gray-200/50' : 'text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-200/50 dark:hover:bg-white/5'"
18
+ >
19
+ Images
20
+ </button>
21
+ <button type="button"
22
+ @click="setFilter('audio')"
23
+ class="px-6 py-2 rounded-lg font-medium transition-all duration-200 text-sm"
24
+ :class="ext.prefs.type === 'audio' ? 'bg-white dark:bg-blue-600 text-blue-600 dark:text-white shadow-sm dark:shadow-blue-500/20 shadow-gray-200/50' : 'text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-200/50 dark:hover:bg-white/5'"
25
+ >
26
+ Audio
27
+ </button>
28
+ </div>
29
+
30
+ <!-- Center: Format Filter -->
31
+ <div v-if="ext.prefs.type === 'image'" class="flex justify-between w-full md:w-auto gap-2">
32
+ <button type="button"
33
+ v-for="fmt in formats"
34
+ :key="fmt.id"
35
+ @click="setFormat(fmt.id)"
36
+ class="p-2 rounded-xl transition-all duration-200 flex flex-col items-center gap-1 min-w-[4.5rem]"
37
+ :class="ext.prefs.format === fmt.id ? 'bg-blue-100 dark:bg-blue-600 text-blue-600 dark:text-white shadow-sm' : 'text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-white/5 hover:text-gray-900 dark:hover:text-white'"
38
+ >
39
+ <span v-html="fmt.icon" class="w-5 h-5"></span>
40
+ <span class="text-[10px] font-medium uppercase tracking-wider">{{ fmt.label }}</span>
41
+ </button>
42
+ </div>
43
+
44
+ <!-- Right: Search -->
45
+ <div class="relative group w-full md:w-72">
46
+ <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
47
+ <svg class="h-4 w-4 text-gray-400 dark:text-gray-500 group-focus-within:text-blue-500 dark:group-focus-within:text-blue-400 transition-colors" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
48
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
49
+ </svg>
50
+ </div>
51
+ <input
52
+ type="text"
53
+ class="block w-full pl-10 pr-3 py-2.5 bg-white dark:bg-gray-900/50 border border-gray-200 dark:border-gray-700 rounded-full leading-5 text-gray-900 dark:text-gray-300 placeholder-gray-400 dark:placeholder-gray-600 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500 sm:text-sm transition-all shadow-sm"
54
+ placeholder="Search prompts, models..."
55
+ v-model="ext.prefs.q"
56
+ @input="onSearch"
57
+ >
58
+ </div>
59
+ </div>
60
+
61
+ <!-- Image Grid -->
62
+ <div v-if="ext.prefs.type === 'image'" class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 2xl:grid-cols-5 gap-3">
63
+ <div
64
+ v-for="(item, index) in items"
65
+ :key="item.id"
66
+ class="group relative rounded-lg overflow-hidden bg-gray-100 dark:bg-gray-800/30 cursor-pointer border border-gray-200 dark:border-white/5 transition-all duration-300 hover:shadow-xl dark:hover:shadow-2xl hover:shadow-blue-500/10 hover:border-blue-400/50 dark:hover:border-blue-500/30"
67
+ :class="ext.prefs.format === 'landscape' ? 'aspect-video' : ext.prefs.format === 'square' ? 'aspect-square' : 'aspect-[3/4]'"
68
+ @click="openLightbox(index)"
69
+ >
70
+ <img
71
+ :src="item.url"
72
+ loading="lazy"
73
+ :alt="item.prompt"
74
+ class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105"
75
+ >
76
+ <div class="absolute inset-0 bg-gradient-to-t from-black/80 via-black/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex flex-col justify-end p-4">
77
+ <div class="transform translate-y-4 group-hover:translate-y-0 transition-transform duration-300">
78
+ <div class="text-xs font-bold text-blue-300 mb-1 uppercase tracking-wider">{{ item.model }}</div>
79
+ <div class="text-xs text-gray-300 font-medium">{{ $fmt.formatDate(item.created) }}</div>
80
+ </div>
81
+ </div>
82
+ </div>
83
+ </div>
84
+
85
+ <!-- Audio List -->
86
+ <div v-if="ext.prefs.type === 'audio'" class="flex flex-col gap-4 max-w-3xl mx-auto">
87
+ <div v-for="(item, index) in items" :key="item.id" class="bg-white dark:bg-gray-800/40 p-4 rounded-2xl border border-gray-200 dark:border-white/5 flex items-center gap-4 hover:border-gray-300 dark:hover:border-gray-700 transition-colors shadow-sm">
88
+ <div class="flex flex-col items-center gap-2 shrink-0">
89
+ <div class="w-12 h-12 rounded-full bg-blue-100 dark:bg-blue-500/20 text-blue-600 dark:text-blue-400 flex items-center justify-center shrink-0">
90
+ <svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
91
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19V6l12-2v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM9 10l12-2" />
92
+ </svg>
93
+ </div>
94
+ <button type="button" @click="remixAudio(item)" class="mb-1 px-2 py-0.5 bg-fuchsia-700 text-white border border-fuchsia-600 hover:bg-fuchsia-600 hover:border-fuchsia-400 rounded-full text-[10px] font-bold uppercase tracking-wider shadow-lg shadow-fuchsia-500/10 hover:shadow-fuchsia-500/40 transition-all duration-200 shrink-0">
95
+ Remix
96
+ </button>
97
+ </div>
98
+ <div class="flex-1 min-w-0">
99
+ <div class="flex justify-between items-center mb-1">
100
+ <h3 class="text-gray-900 dark:text-white font-medium truncate pr-4" :title="item.caption || item.prompt || ''">
101
+ {{ item.caption || item.prompt || 'Untitled' }}
102
+ </h3>
103
+ <span class="text-xs text-gray-500 shrink-0">{{ $fmt.formatDate(item.created) }}</span>
104
+ </div>
105
+ <div class="flex justify-between items-center mb-2">
106
+ <div class="text-xs text-blue-600 dark:text-blue-300/80">{{ item.model }}</div>
107
+ </div>
108
+ <div class="flex items-center gap-2">
109
+ <audio controls class="w-full h-8 opacity-90" :src="item.url"></audio>
110
+ <button type="button" @click="deleteMedia(item)" class="p-1 text-gray-400 hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-full transition-colors" title="Delete">
111
+ <svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></svg>
112
+ </button>
113
+ </div>
114
+ </div>
115
+ </div>
116
+ </div>
117
+
118
+ <!-- Loading State -->
119
+ <div class="h-20 flex items-center justify-center mt-8 text-gray-500" ref="loadingTrigger">
120
+ <div v-if="loading" class="flex items-center gap-3">
121
+ <div class="w-2 h-2 bg-blue-500 rounded-full animate-bounce [animation-delay:-0.3s]"></div>
122
+ <div class="w-2 h-2 bg-blue-500 rounded-full animate-bounce [animation-delay:-0.15s]"></div>
123
+ <div class="w-2 h-2 bg-blue-500 rounded-full animate-bounce"></div>
124
+ </div>
125
+ <div v-else-if="allLoaded && items.length > 0" class="text-sm font-medium opacity-50">
126
+ All caught up
127
+ </div>
128
+ <div v-else-if="allLoaded && items.length === 0" class="flex flex-col items-center gap-2 opacity-50 py-12">
129
+ <svg class="w-12 h-12" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg>
130
+ <span>No media found</span>
131
+ </div>
132
+ </div>
133
+
134
+ <!-- Lightbox -->
135
+ <transition
136
+ enter-active-class="transition ease-out duration-300"
137
+ enter-from-class="opacity-0"
138
+ enter-to-class="opacity-100"
139
+ leave-active-class="transition ease-in duration-200"
140
+ leave-from-class="opacity-100"
141
+ leave-to-class="opacity-0"
142
+ >
143
+ <div v-if="lightboxItem" class="fixed inset-0 z-100 flex bg-white/95 dark:bg-black/95 backdrop-blur-xl" @click.self="closeLightbox" @keydown.esc="closeLightbox" tabindex="0">
144
+
145
+ <!-- Main Content -->
146
+ <div class="flex-1 relative flex items-center justify-center p-4">
147
+
148
+ <button type="button" class="absolute top-4 right-4 z-50 p-2 text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white bg-gray-100 hover:bg-gray-200 dark:bg-black/50 dark:hover:bg-white/10 rounded-full transition-all" @click="closeLightbox">
149
+ <svg class="w-8 h-8" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /></svg>
150
+ </button>
151
+
152
+ <button v-if="hasPrev" type="button" class="hidden md:flex absolute left-4 top-1/2 -translate-y-1/2 p-3 text-gray-700 dark:text-white bg-white/80 dark:bg-white/10 hover:bg-white dark:hover:bg-white/20 hover:scale-110 rounded-full backdrop-blur-md transition-all border border-gray-200 dark:border-white/5 shadow-lg" @click.stop="prevItem">
153
+ <svg class="w-8 h-8" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" /></svg>
154
+ </button>
155
+
156
+ <img :src="lightboxItem.url" class="max-w-full max-h-[90vh] object-contain shadow-2xl rounded-sm" @click.stop>
157
+
158
+ <button v-if="hasNext" type="button" class="hidden md:flex absolute right-4 top-1/2 -translate-y-1/2 p-3 text-gray-700 dark:text-white bg-white/80 dark:bg-white/10 hover:bg-white dark:hover:bg-white/20 hover:scale-110 rounded-full backdrop-blur-md transition-all border border-gray-200 dark:border-white/5 shadow-lg" @click.stop="nextItem">
159
+ <svg class="w-8 h-8" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" /></svg>
160
+ </button>
161
+ </div>
162
+
163
+ <!-- Sidebar -->
164
+ <div class="w-full md:w-[400px] h-full bg-gray-50 dark:bg-[#111111] border-l border-gray-200 dark:border-white/10 flex flex-col shadow-2xl text-gray-900 dark:text-gray-200">
165
+ <div class="p-6 overflow-y-auto custom-scrollbar flex-1 space-y-8">
166
+
167
+ <!-- Model Badge -->
168
+ <div>
169
+ <h3 class="text-xs uppercase tracking-widest text-gray-500 font-semibold mb-2">Generated With</h3>
170
+ <div class="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-blue-100 dark:bg-blue-500/10 border border-blue-200 dark:border-blue-500/20 text-blue-600 dark:text-blue-400 text-sm font-medium">
171
+ <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" /></svg>
172
+ {{ lightboxItem.model }}
173
+ </div>
174
+ </div>
175
+
176
+ <!-- Prompt -->
177
+ <div>
178
+ <div class="flex justify-between">
179
+ <h3 class="text-xs uppercase tracking-widest text-gray-500 font-semibold mb-3">Prompt</h3>
180
+ <button type="button" @click="remixImage" class="mb-2 px-3 py-1 bg-fuchsia-700 text-white border border-fuchsia-600 hover:bg-fuchsia-600 hover:border-fuchsia-400 rounded-full text-xs font-bold uppercase tracking-wider shadow-lg shadow-fuchsia-500/10 hover:shadow-fuchsia-500/40 transition-all duration-200">
181
+ Remix
182
+ </button>
183
+ </div>
184
+ <div class="bg-white dark:bg-gray-900/50 p-4 rounded-xl border border-gray-200 dark:border-white/5 text-gray-600 dark:text-gray-300 text-sm leading-relaxed font-mono max-h-60 overflow-y-auto custom-scrollbar shadow-inner">
185
+ {{ lightboxItem.prompt }}
186
+ </div>
187
+ </div>
188
+
189
+ <!-- Parameters -->
190
+ <div v-if="lightboxItem.params && Object.keys(lightboxItem.params).length">
191
+ <h3 class="text-xs uppercase tracking-widest text-gray-500 font-semibold mb-3">Parameters</h3>
192
+ <div class="flex flex-wrap gap-2">
193
+ <span v-for="(val, key) in lightboxItem.params" :key="key" class="px-2.5 py-1 bg-white dark:bg-gray-800 rounded-md text-xs text-gray-600 dark:text-gray-300 border border-gray-200 dark:border-white/5 shadow-sm">
194
+ <span class="text-gray-400 dark:text-gray-500 mr-1">{{key}}:</span> {{val}}
195
+ </span>
196
+ </div>
197
+ </div>
198
+
199
+ <!-- Details Grid -->
200
+ <div>
201
+ <h3 class="text-xs uppercase tracking-widest text-gray-500 font-semibold mb-3">Details</h3>
202
+ <div class="grid grid-cols-2 gap-4 text-sm">
203
+ <div class="bg-white dark:bg-gray-800/20 p-3 rounded-lg border border-gray-200 dark:border-white/5">
204
+ <div class="text-gray-500 text-xs mb-1">Dimensions</div>
205
+ <div class="font-mono text-gray-700 dark:text-gray-300">{{ lightboxItem.width }} × {{ lightboxItem.height }}</div>
206
+ </div>
207
+ <div class="bg-white dark:bg-gray-800/20 p-3 rounded-lg border border-gray-200 dark:border-white/5">
208
+ <div class="text-gray-500 text-xs mb-1">File Size</div>
209
+ <div class="font-mono text-gray-700 dark:text-gray-300">{{ $fmt.bytes(lightboxItem.size) }}</div>
210
+ </div>
211
+ <div class="bg-white dark:bg-gray-800/20 p-3 rounded-lg border border-gray-200 dark:border-white/5">
212
+ <div class="text-gray-500 text-xs mb-1">Created</div>
213
+ <div class="text-gray-700 dark:text-gray-300">{{ $fmt.shortDate(lightboxItem.created) }}</div>
214
+ </div>
215
+ <div v-if="lightboxItem.cost" class="bg-white dark:bg-gray-800/20 p-3 rounded-lg border border-gray-200 dark:border-white/5">
216
+ <div class="text-gray-500 text-xs mb-1">Cost</div>
217
+ <div class="text-green-600 dark:text-green-400 font-mono">$\{{ lightboxItem.cost.toFixed(5) }}</div>
218
+ </div>
219
+ </div>
220
+ </div>
221
+ </div>
222
+
223
+ <!-- Footer Actions -->
224
+ <div class="p-6 border-t border-gray-200 dark:border-white/5 bg-gray-50 dark:bg-[#161616] flex gap-2">
225
+ <button type="button" @click="deleteMedia" class="flex items-center justify-center p-3 bg-red-100 dark:bg-red-900/20 text-red-600 dark:text-red-400 font-bold rounded-xl hover:bg-red-200 dark:hover:bg-red-900/40 transition-colors" title="Delete">
226
+ <svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></svg>
227
+ </button>
228
+ <a :href="lightboxItem.url" download class="flex-1 flex items-center justify-center gap-2 bg-gray-900 dark:bg-white text-white dark:text-black font-bold py-3 px-6 rounded-xl hover:bg-gray-800 dark:hover:bg-gray-200 transition-colors shadow-lg shadow-black/5 dark:shadow-white/5">
229
+ <svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" /></svg>
230
+ Download
231
+ </a>
232
+ </div>
233
+ </div>
234
+ </div>
235
+ </transition>
236
+ </div>
237
+ `,
238
+ setup() {
239
+ const ctx = inject('ctx')
240
+ const items = ref([])
241
+ const loading = ref(false)
242
+ const allLoaded = ref(false)
243
+ const lightboxIndex = ref(-1)
244
+ const loadingTrigger = ref(null)
245
+
246
+ const PAGE_SIZE = 50
247
+ let observer = null
248
+ let searchTimeout = null
249
+
250
+ async function loadMedia({ reset } = {}) {
251
+ if (loading.value) return
252
+ ext.savePrefs()
253
+ const skip = reset ? 0 : items.value.length
254
+ if (reset) {
255
+ allLoaded.value = false
256
+ }
257
+ if (allLoaded.value) return
258
+
259
+ loading.value = true
260
+ try {
261
+ const params = new URLSearchParams({
262
+ type: ext.prefs.type,
263
+ sort: '-id',
264
+ skip,
265
+ take: PAGE_SIZE,
266
+ })
267
+ if (ext.prefs.q) {
268
+ params.append('q', ext.prefs.q)
269
+ }
270
+ if (ext.prefs.format && ext.prefs.type !== 'audio') {
271
+ params.append('format', ext.prefs.format)
272
+ }
273
+
274
+ // USE ext.getJson AS REQUESTED
275
+ const data = await ext.getJson(`/media?${params}`)
276
+
277
+ if (data.length < PAGE_SIZE) {
278
+ allLoaded.value = true
279
+ }
280
+
281
+ const processed = data.map(item => {
282
+ try {
283
+ if (typeof item.category === 'string') item.category = JSON.parse(item.category)
284
+ if (typeof item.tags === 'string') item.tags = JSON.parse(item.tags)
285
+ if (typeof item.metadata === 'string') item.metadata = JSON.parse(item.metadata)
286
+ } catch (e) { }
287
+
288
+ return {
289
+ ...item,
290
+ params: {
291
+ ...(item.aspect_ratio ? { aspect: item.aspect_ratio } : {}),
292
+ ...(item.seed ? { seed: item.seed } : {}),
293
+ }
294
+ }
295
+ })
296
+
297
+ items.value = reset ? processed : [...items.value, ...processed]
298
+ } catch (e) {
299
+ console.error("Failed to load media", e)
300
+ } finally {
301
+ loading.value = false
302
+ }
303
+ }
304
+
305
+ function setFilter(type) {
306
+ ext.setPrefs({ type })
307
+ loadMedia({ reset: true })
308
+ }
309
+
310
+ const formats = [
311
+ { id: 'portrait', label: 'Portrait', icon: `<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mx-auto" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="5" y="2" width="14" height="20" rx="2" ry="2"></rect></svg>` },
312
+ { id: 'square', label: 'Square', icon: `<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mx-auto" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect></svg>` },
313
+ { id: 'landscape', label: 'Landscape', icon: `<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mx-auto" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="5" width="20" height="14" rx="2" ry="2"></rect></svg>` },
314
+ ]
315
+
316
+ function setFormat(fmt) {
317
+ ext.prefs.format = fmt === ext.prefs.format ? '' : fmt
318
+ loadMedia({ reset: true })
319
+ }
320
+
321
+ function onSearch() {
322
+ if (searchTimeout) clearTimeout(searchTimeout)
323
+ searchTimeout = setTimeout(() => {
324
+ loadMedia({ reset: true })
325
+ }, 500)
326
+ }
327
+
328
+ onMounted(() => {
329
+ if (!ext.prefs.type) {
330
+ ext.setPrefs({ type: 'image' })
331
+ }
332
+ loadMedia({ reset: true })
333
+ observer = new IntersectionObserver((entries) => {
334
+ if (entries[0].isIntersecting) {
335
+ loadMedia()
336
+ }
337
+ }, { threshold: 0.1 })
338
+ if (loadingTrigger.value) observer.observe(loadingTrigger.value)
339
+ window.addEventListener('keydown', handleKeydown)
340
+ })
341
+
342
+ onUnmounted(() => {
343
+ if (observer) observer.disconnect()
344
+ window.removeEventListener('keydown', handleKeydown)
345
+ })
346
+
347
+ watch(loadingTrigger, (el) => {
348
+ if (el && observer) observer.observe(el)
349
+ })
350
+
351
+ const lightboxItem = computed(() => {
352
+ return lightboxIndex.value >= 0 ? items.value[lightboxIndex.value] : null
353
+ })
354
+
355
+ const hasNext = computed(() => lightboxIndex.value < items.value.length - 1)
356
+ const hasPrev = computed(() => lightboxIndex.value > 0)
357
+
358
+ function openLightbox(index) {
359
+ lightboxIndex.value = index
360
+ document.body.style.overflow = 'hidden'
361
+ }
362
+
363
+ function closeLightbox() {
364
+ lightboxIndex.value = -1
365
+ document.body.style.overflow = ''
366
+ }
367
+
368
+ function nextItem() {
369
+ if (hasNext.value) lightboxIndex.value++
370
+ }
371
+
372
+ function prevItem() {
373
+ if (hasPrev.value) lightboxIndex.value--
374
+ }
375
+
376
+ function handleKeydown(e) {
377
+ if (lightboxIndex.value === -1) return
378
+ if (e.key === 'ArrowRight') nextItem()
379
+ if (e.key === 'ArrowLeft') prevItem()
380
+ if (e.key === 'Escape') closeLightbox()
381
+ }
382
+
383
+ function remixImage() {
384
+ const selected = lightboxItem.value
385
+ closeLightbox()
386
+ ctx.chat.setSelectedModel(ctx.chat.getModel(selected.model))
387
+ ctx.chat.messageText.value = selected.prompt
388
+ ctx.chat.selectAspectRatio(selected.aspect_ratio)
389
+ ctx.threads.startNewThread({
390
+ title: selected.prompt,
391
+ model: ctx.chat.getSelectedModel(),
392
+ })
393
+ }
394
+
395
+ function remixAudio(item) {
396
+ const selected = item || lightboxItem.value
397
+ if (lightboxItem.value) closeLightbox()
398
+
399
+ ctx.chat.setSelectedModel(ctx.chat.getModel(selected.model))
400
+ ctx.chat.messageText.value = selected.prompt
401
+ ctx.threads.startNewThread({
402
+ title: selected.prompt,
403
+ model: ctx.chat.getSelectedModel(),
404
+ })
405
+ }
406
+
407
+ async function deleteMedia(item) {
408
+ const target = item && item.hash ? item : lightboxItem.value
409
+ if (!target) return
410
+
411
+ if (!confirm('Are you sure you want to delete this media?')) return
412
+
413
+ const hash = target.hash
414
+ try {
415
+ const response = await fetch(`${ext.baseUrl}/media/${hash}`, {
416
+ method: 'DELETE'
417
+ })
418
+ if (response.ok) {
419
+ items.value = items.value.filter(item => item.hash !== hash)
420
+ if (lightboxItem.value && lightboxItem.value.hash === hash) {
421
+ closeLightbox()
422
+ }
423
+ } else {
424
+ console.error("Failed to delete media", response)
425
+ alert("Failed to delete media")
426
+ }
427
+ } catch (e) {
428
+ console.error("Error deleting media", e)
429
+ alert("Error deleting media")
430
+ }
431
+ }
432
+
433
+ return {
434
+ ext,
435
+ items,
436
+ loading,
437
+ allLoaded,
438
+ loadingTrigger,
439
+ formats,
440
+ setFilter,
441
+ setFormat,
442
+ onSearch,
443
+ loadMedia,
444
+ lightboxIndex,
445
+ lightboxItem,
446
+ openLightbox,
447
+ closeLightbox,
448
+ nextItem,
449
+ prevItem,
450
+ hasNext,
451
+ hasPrev,
452
+ remixImage,
453
+ remixAudio,
454
+ deleteMedia,
455
+ }
456
+ }
457
+ }
458
+
459
+ export default {
460
+ order: 40 - 100,
461
+
462
+ install(ctx) {
463
+ ext = ctx.scope('gallery')
464
+
465
+ ctx.components({
466
+ GalleryPage,
467
+ })
468
+
469
+ ctx.setLeftIcons({
470
+ gallery: {
471
+ component: {
472
+ template: `<svg @click="$ctx.togglePath('/gallery')" viewBox="0 0 15 15" class="w-6 h-6"><path fill="currentColor" d="M10.71 3L7.85.15a.5.5 0 0 0-.707-.003L7.14.15L4.29 3H1.5a.5.5 0 0 0-.5.5v9a.5.5 0 0 0 .5.5h12a.5.5 0 0 0 .5-.5v-9a.5.5 0 0 0-.5-.5zM7.5 1.21L9.29 3H5.71zM13 12H2V4h11zM5 7a1 1 0 1 1 0-2a1 1 0 0 1 0 2m7 4H4.5L6 8l1.25 2.5L9.5 6z"/></svg>`,
473
+ },
474
+ isActive({ path }) { return path === '/gallery' }
475
+ }
476
+ })
477
+
478
+ ctx.routes.push({ path: '/gallery', component: GalleryPage, meta: { title: `Gallery` } })
479
+ }
480
+ }
@@ -0,0 +1,18 @@
1
+ from .anthropic import install_anthropic
2
+ from .chutes import install_chutes
3
+ from .google import install_google
4
+ from .nvidia import install_nvidia
5
+ from .openai import install_openai
6
+ from .openrouter import install_openrouter
7
+
8
+
9
+ def install(ctx):
10
+ install_anthropic(ctx)
11
+ install_chutes(ctx)
12
+ install_google(ctx)
13
+ install_openai(ctx)
14
+ install_openrouter(ctx)
15
+ install_nvidia(ctx)
16
+
17
+
18
+ __install__ = install
@@ -4,7 +4,7 @@ import time
4
4
  import aiohttp
5
5
 
6
6
 
7
- def install(ctx):
7
+ def install_anthropic(ctx):
8
8
  from llms.main import OpenAiCompatible
9
9
 
10
10
  class AnthropicProvider(OpenAiCompatible):
@@ -184,6 +184,3 @@ def install(ctx):
184
184
  return ret
185
185
 
186
186
  ctx.add_provider(AnthropicProvider)
187
-
188
-
189
- __install__ = install
@@ -5,7 +5,7 @@ import time
5
5
  import aiohttp
6
6
 
7
7
 
8
- def install(ctx):
8
+ def install_chutes(ctx):
9
9
  from llms.main import GeneratorBase
10
10
 
11
11
  class ChutesImage(GeneratorBase):
@@ -50,15 +50,18 @@ def install(ctx):
50
50
  if "messages" in chat and len(chat["messages"]) > 0:
51
51
  aspect_ratio = chat["messages"][0].get("aspect_ratio", "1:1")
52
52
  cfg_scale = self.cfg_scale
53
+ steps = self.steps
54
+ width = self.width
55
+ height = self.height
53
56
  if chat["model"] == "chutes-z-image-turbo":
54
57
  cfg_scale = min(self.cfg_scale, 5)
55
58
  payload = {
56
59
  "model": chat["model"],
57
60
  "prompt": ctx.last_user_prompt(chat),
58
61
  "guidance_scale": cfg_scale,
59
- "width": self.width,
60
- "height": self.height,
61
- "num_inference_steps": self.steps,
62
+ "width": width,
63
+ "height": height,
64
+ "num_inference_steps": steps,
62
65
  }
63
66
  if chat["model"] in self.model_negative_prompt:
64
67
  payload["negative_prompt"] = self.negative_prompt
@@ -68,9 +71,10 @@ def install(ctx):
68
71
  if aspect_ratio:
69
72
  dimension = ctx.app.aspect_ratios.get(aspect_ratio)
70
73
  if dimension:
71
- width, height = dimension.split("×")
72
- payload["width"] = int(width)
73
- payload["height"] = int(height)
74
+ w, h = dimension.split("×")
75
+ width, height = int(w), int(h)
76
+ payload["width"] = width
77
+ payload["height"] = height
74
78
 
75
79
  if chat["model"] in self.model_resolutions:
76
80
  # if models use resolution, remove width and height
@@ -107,14 +111,16 @@ def install(ctx):
107
111
  relative_url, info = ctx.save_image_to_cache(
108
112
  image_data,
109
113
  f"{chat['model']}.{ext}",
110
- {
111
- "model": chat["model"],
112
- "prompt": ctx.last_user_prompt(chat),
113
- "width": self.width,
114
- "height": self.height,
115
- "cfg_scale": self.cfg_scale,
116
- "steps": self.steps,
117
- },
114
+ ctx.to_file_info(
115
+ chat,
116
+ {
117
+ "aspect_ratio": aspect_ratio,
118
+ "width": width,
119
+ "height": height,
120
+ "cfg_scale": cfg_scale,
121
+ "steps": steps,
122
+ },
123
+ ),
118
124
  )
119
125
  return {
120
126
  "choices": [
@@ -147,6 +153,3 @@ def install(ctx):
147
153
  raise Exception(f"Failed to generate image {response.status}")
148
154
 
149
155
  ctx.add_provider(ChutesImage)
150
-
151
-
152
- __install__ = install