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
@@ -0,0 +1,482 @@
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 api = await ext.getJson(`/media?${params}`)
276
+ const data = api.response || []
277
+
278
+ if (data.length < PAGE_SIZE) {
279
+ allLoaded.value = true
280
+ }
281
+
282
+ const processed = data.map(item => {
283
+ try {
284
+ if (typeof item.category === 'string') item.category = JSON.parse(item.category)
285
+ if (typeof item.tags === 'string') item.tags = JSON.parse(item.tags)
286
+ if (typeof item.metadata === 'string') item.metadata = JSON.parse(item.metadata)
287
+ } catch (e) { }
288
+
289
+ return {
290
+ ...item,
291
+ params: {
292
+ ...(item.aspect_ratio ? { aspect: item.aspect_ratio } : {}),
293
+ ...(item.seed ? { seed: item.seed } : {}),
294
+ }
295
+ }
296
+ })
297
+
298
+ items.value = reset ? processed : [...items.value, ...processed]
299
+ } catch (e) {
300
+ console.error("Failed to load media", e)
301
+ } finally {
302
+ loading.value = false
303
+ }
304
+ }
305
+
306
+ function setFilter(type) {
307
+ ext.setPrefs({ type })
308
+ loadMedia({ reset: true })
309
+ }
310
+
311
+ const formats = [
312
+ { 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>` },
313
+ { 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>` },
314
+ { 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>` },
315
+ ]
316
+
317
+ function setFormat(fmt) {
318
+ ext.prefs.format = fmt === ext.prefs.format ? '' : fmt
319
+ loadMedia({ reset: true })
320
+ }
321
+
322
+ function onSearch() {
323
+ if (searchTimeout) clearTimeout(searchTimeout)
324
+ searchTimeout = setTimeout(() => {
325
+ loadMedia({ reset: true })
326
+ }, 500)
327
+ }
328
+
329
+ onMounted(() => {
330
+ if (!ext.prefs.type) {
331
+ ext.setPrefs({ type: 'image' })
332
+ }
333
+ loadMedia({ reset: true })
334
+ observer = new IntersectionObserver((entries) => {
335
+ if (entries[0].isIntersecting) {
336
+ loadMedia()
337
+ }
338
+ }, { threshold: 0.1 })
339
+ if (loadingTrigger.value) observer.observe(loadingTrigger.value)
340
+ window.addEventListener('keydown', handleKeydown)
341
+ })
342
+
343
+ onUnmounted(() => {
344
+ if (observer) observer.disconnect()
345
+ window.removeEventListener('keydown', handleKeydown)
346
+ })
347
+
348
+ watch(loadingTrigger, (el) => {
349
+ if (el && observer) observer.observe(el)
350
+ })
351
+
352
+ const lightboxItem = computed(() => {
353
+ return lightboxIndex.value >= 0 ? items.value[lightboxIndex.value] : null
354
+ })
355
+
356
+ const hasNext = computed(() => lightboxIndex.value < items.value.length - 1)
357
+ const hasPrev = computed(() => lightboxIndex.value > 0)
358
+
359
+ function openLightbox(index) {
360
+ lightboxIndex.value = index
361
+ document.body.style.overflow = 'hidden'
362
+ }
363
+
364
+ function closeLightbox() {
365
+ lightboxIndex.value = -1
366
+ document.body.style.overflow = ''
367
+ }
368
+
369
+ function nextItem() {
370
+ if (hasNext.value) lightboxIndex.value++
371
+ }
372
+
373
+ function prevItem() {
374
+ if (hasPrev.value) lightboxIndex.value--
375
+ }
376
+
377
+ function handleKeydown(e) {
378
+ if (lightboxIndex.value === -1) return
379
+ if (e.key === 'ArrowRight') nextItem()
380
+ if (e.key === 'ArrowLeft') prevItem()
381
+ if (e.key === 'Escape') closeLightbox()
382
+ }
383
+
384
+ function remixImage() {
385
+ const selected = lightboxItem.value
386
+ closeLightbox()
387
+ ctx.chat.setSelectedModel(ctx.chat.getModel(selected.model))
388
+ ctx.chat.messageText.value = selected.prompt
389
+ ctx.chat.selectAspectRatio(selected.aspect_ratio)
390
+ ctx.threads.startNewThread({
391
+ title: selected.prompt,
392
+ model: ctx.chat.getSelectedModel(),
393
+ redirect: true,
394
+ })
395
+ }
396
+
397
+ function remixAudio(item) {
398
+ const selected = item || lightboxItem.value
399
+ if (lightboxItem.value) closeLightbox()
400
+
401
+ ctx.chat.setSelectedModel(ctx.chat.getModel(selected.model))
402
+ ctx.chat.messageText.value = selected.prompt
403
+ ctx.threads.startNewThread({
404
+ title: selected.prompt,
405
+ model: ctx.chat.getSelectedModel(),
406
+ })
407
+ }
408
+
409
+ async function deleteMedia(item) {
410
+ const target = item && item.hash ? item : lightboxItem.value
411
+ if (!target) return
412
+
413
+ if (!confirm('Are you sure you want to delete this media?')) return
414
+
415
+ const hash = target.hash
416
+ try {
417
+ const response = await fetch(`${ext.baseUrl}/media/${hash}`, {
418
+ method: 'DELETE'
419
+ })
420
+ if (response.ok) {
421
+ items.value = items.value.filter(item => item.hash !== hash)
422
+ if (lightboxItem.value && lightboxItem.value.hash === hash) {
423
+ closeLightbox()
424
+ }
425
+ } else {
426
+ console.error("Failed to delete media", response)
427
+ alert("Failed to delete media")
428
+ }
429
+ } catch (e) {
430
+ console.error("Error deleting media", e)
431
+ alert("Error deleting media")
432
+ }
433
+ }
434
+
435
+ return {
436
+ ext,
437
+ items,
438
+ loading,
439
+ allLoaded,
440
+ loadingTrigger,
441
+ formats,
442
+ setFilter,
443
+ setFormat,
444
+ onSearch,
445
+ loadMedia,
446
+ lightboxIndex,
447
+ lightboxItem,
448
+ openLightbox,
449
+ closeLightbox,
450
+ nextItem,
451
+ prevItem,
452
+ hasNext,
453
+ hasPrev,
454
+ remixImage,
455
+ remixAudio,
456
+ deleteMedia,
457
+ }
458
+ }
459
+ }
460
+
461
+ export default {
462
+ order: 40 - 100,
463
+
464
+ install(ctx) {
465
+ ext = ctx.scope('gallery')
466
+
467
+ ctx.components({
468
+ GalleryPage,
469
+ })
470
+
471
+ ctx.setLeftIcons({
472
+ gallery: {
473
+ component: {
474
+ template: `<svg @click="$ctx.togglePath('/gallery')" viewBox="0 0 15 15"><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>`,
475
+ },
476
+ isActive({ path }) { return path === '/gallery' }
477
+ }
478
+ })
479
+
480
+ ctx.routes.push({ path: '/gallery', component: GalleryPage, meta: { title: `Gallery` } })
481
+ }
482
+ }
@@ -0,0 +1,39 @@
1
+ # KaTeX Extension
2
+
3
+ This extension enables beautiful rendering of LaTeX math expressions in AI responses using [KaTeX](https://katex.org/). It integrates automatically with the markdown parser to render math equations in both inline and block formats.
4
+
5
+ ## Features
6
+
7
+ - **Fast Rendering**: Uses KaTeX for high-performance rendering of math expressions.
8
+ - **Inline Math**: Renders math within text using `$` or `$$` delimiters.
9
+ - **Block Math**: Renders complex equations in their own block using `$` or `$$` delimiters across multiple lines.
10
+ - **Auto-Integration**: Automatically extends the `marked` parser used in the application.
11
+
12
+ ## Usage
13
+
14
+ The extension supports standard LaTeX math syntax.
15
+
16
+ ### Inline Math
17
+
18
+ Surround your LaTeX expression with single `$` (for inline style) or double `$$` (for display style) delimiters.
19
+
20
+ **Example:**
21
+ `The quadratic formula is $x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}$.`
22
+
23
+ ### Block Math
24
+
25
+ For larger equations or when you want the math to be displayed on its own line, use block syntax by placing the delimiters on separate lines. Standard usage is to use double `$$` delimiters.
26
+
27
+ **Example:**
28
+ ```latex
29
+ $$
30
+ \int_{-\infty}^{\infty} e^{-x^2} dx = \sqrt{\pi}
31
+ $$
32
+ ```
33
+
34
+ ## Configuration
35
+
36
+ The extension automatically registers:
37
+ - **Import Maps**: Loads `katex.min.mjs` for the frontend.
38
+ - **CSS**: Injects `katex.min.css` for styling.
39
+ - **Markdown Extension**: Adds a custom tokenizer and renderer to `marked` to detect and render LaTeX patterns.
@@ -0,0 +1,6 @@
1
+ def install(ctx):
2
+ ctx.add_importmaps({"katex": f"{ctx.ext_prefix}/katex.min.mjs"})
3
+ ctx.add_index_footer(f"""<link rel="stylesheet" href="{ctx.ext_prefix}/katex.min.css">""")
4
+
5
+
6
+ __install__ = install