llms-py 2.0.34__py3-none-any.whl → 3.0.0__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 (207) hide show
  1. llms/__init__.py +3 -1
  2. llms/__pycache__/__init__.cpython-312.pyc +0 -0
  3. llms/__pycache__/__init__.cpython-313.pyc +0 -0
  4. llms/__pycache__/__init__.cpython-314.pyc +0 -0
  5. llms/__pycache__/__main__.cpython-312.pyc +0 -0
  6. llms/__pycache__/__main__.cpython-314.pyc +0 -0
  7. llms/__pycache__/llms.cpython-312.pyc +0 -0
  8. llms/__pycache__/main.cpython-312.pyc +0 -0
  9. llms/__pycache__/main.cpython-313.pyc +0 -0
  10. llms/__pycache__/main.cpython-314.pyc +0 -0
  11. llms/__pycache__/plugins.cpython-314.pyc +0 -0
  12. llms/{ui/Analytics.mjs → extensions/analytics/ui/index.mjs} +154 -238
  13. llms/extensions/app/README.md +20 -0
  14. llms/extensions/app/__init__.py +530 -0
  15. llms/extensions/app/__pycache__/__init__.cpython-314.pyc +0 -0
  16. llms/extensions/app/__pycache__/db.cpython-314.pyc +0 -0
  17. llms/extensions/app/__pycache__/db_manager.cpython-314.pyc +0 -0
  18. llms/extensions/app/db.py +644 -0
  19. llms/extensions/app/db_manager.py +195 -0
  20. llms/extensions/app/requests.json +9073 -0
  21. llms/extensions/app/threads.json +15290 -0
  22. llms/{ui → extensions/app/ui}/Recents.mjs +91 -65
  23. llms/{ui/Sidebar.mjs → extensions/app/ui/index.mjs} +124 -58
  24. llms/extensions/app/ui/threadStore.mjs +411 -0
  25. llms/extensions/core_tools/CALCULATOR.md +32 -0
  26. llms/extensions/core_tools/__init__.py +598 -0
  27. llms/extensions/core_tools/__pycache__/__init__.cpython-314.pyc +0 -0
  28. llms/extensions/core_tools/ui/codemirror/addon/edit/closebrackets.js +201 -0
  29. llms/extensions/core_tools/ui/codemirror/addon/edit/closetag.js +185 -0
  30. llms/extensions/core_tools/ui/codemirror/addon/edit/continuelist.js +101 -0
  31. llms/extensions/core_tools/ui/codemirror/addon/edit/matchbrackets.js +160 -0
  32. llms/extensions/core_tools/ui/codemirror/addon/edit/matchtags.js +66 -0
  33. llms/extensions/core_tools/ui/codemirror/addon/edit/trailingspace.js +27 -0
  34. llms/extensions/core_tools/ui/codemirror/addon/selection/active-line.js +72 -0
  35. llms/extensions/core_tools/ui/codemirror/addon/selection/mark-selection.js +119 -0
  36. llms/extensions/core_tools/ui/codemirror/addon/selection/selection-pointer.js +98 -0
  37. llms/extensions/core_tools/ui/codemirror/doc/docs.css +225 -0
  38. llms/extensions/core_tools/ui/codemirror/doc/source_sans.woff +0 -0
  39. llms/extensions/core_tools/ui/codemirror/lib/codemirror.css +344 -0
  40. llms/extensions/core_tools/ui/codemirror/lib/codemirror.js +9884 -0
  41. llms/extensions/core_tools/ui/codemirror/mode/clike/clike.js +942 -0
  42. llms/extensions/core_tools/ui/codemirror/mode/javascript/index.html +118 -0
  43. llms/extensions/core_tools/ui/codemirror/mode/javascript/javascript.js +962 -0
  44. llms/extensions/core_tools/ui/codemirror/mode/javascript/typescript.html +62 -0
  45. llms/extensions/core_tools/ui/codemirror/mode/python/python.js +402 -0
  46. llms/extensions/core_tools/ui/codemirror/theme/dracula.css +40 -0
  47. llms/extensions/core_tools/ui/codemirror/theme/mocha.css +135 -0
  48. llms/extensions/core_tools/ui/index.mjs +650 -0
  49. llms/extensions/gallery/README.md +61 -0
  50. llms/extensions/gallery/__init__.py +61 -0
  51. llms/extensions/gallery/__pycache__/__init__.cpython-314.pyc +0 -0
  52. llms/extensions/gallery/__pycache__/db.cpython-314.pyc +0 -0
  53. llms/extensions/gallery/db.py +298 -0
  54. llms/extensions/gallery/ui/index.mjs +482 -0
  55. llms/extensions/katex/README.md +39 -0
  56. llms/extensions/katex/__init__.py +6 -0
  57. llms/extensions/katex/__pycache__/__init__.cpython-314.pyc +0 -0
  58. llms/extensions/katex/ui/README.md +125 -0
  59. llms/extensions/katex/ui/contrib/auto-render.js +338 -0
  60. llms/extensions/katex/ui/contrib/auto-render.min.js +1 -0
  61. llms/extensions/katex/ui/contrib/auto-render.mjs +244 -0
  62. llms/extensions/katex/ui/contrib/copy-tex.js +127 -0
  63. llms/extensions/katex/ui/contrib/copy-tex.min.js +1 -0
  64. llms/extensions/katex/ui/contrib/copy-tex.mjs +105 -0
  65. llms/extensions/katex/ui/contrib/mathtex-script-type.js +109 -0
  66. llms/extensions/katex/ui/contrib/mathtex-script-type.min.js +1 -0
  67. llms/extensions/katex/ui/contrib/mathtex-script-type.mjs +24 -0
  68. llms/extensions/katex/ui/contrib/mhchem.js +3213 -0
  69. llms/extensions/katex/ui/contrib/mhchem.min.js +1 -0
  70. llms/extensions/katex/ui/contrib/mhchem.mjs +3109 -0
  71. llms/extensions/katex/ui/contrib/render-a11y-string.js +887 -0
  72. llms/extensions/katex/ui/contrib/render-a11y-string.min.js +1 -0
  73. llms/extensions/katex/ui/contrib/render-a11y-string.mjs +800 -0
  74. llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.ttf +0 -0
  75. llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.woff +0 -0
  76. llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  77. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
  78. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
  79. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  80. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
  81. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
  82. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  83. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
  84. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.woff +0 -0
  85. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  86. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
  87. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.woff +0 -0
  88. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  89. llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.ttf +0 -0
  90. llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.woff +0 -0
  91. llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.woff2 +0 -0
  92. llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
  93. llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.woff +0 -0
  94. llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  95. llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.ttf +0 -0
  96. llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.woff +0 -0
  97. llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.woff2 +0 -0
  98. llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.ttf +0 -0
  99. llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.woff +0 -0
  100. llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.woff2 +0 -0
  101. llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
  102. llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.woff +0 -0
  103. llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  104. llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.ttf +0 -0
  105. llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.woff +0 -0
  106. llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.woff2 +0 -0
  107. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
  108. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.woff +0 -0
  109. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  110. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
  111. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.woff +0 -0
  112. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  113. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
  114. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.woff +0 -0
  115. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  116. llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.ttf +0 -0
  117. llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.woff +0 -0
  118. llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.woff2 +0 -0
  119. llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.ttf +0 -0
  120. llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.woff +0 -0
  121. llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  122. llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.ttf +0 -0
  123. llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.woff +0 -0
  124. llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  125. llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.ttf +0 -0
  126. llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.woff +0 -0
  127. llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  128. llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.ttf +0 -0
  129. llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.woff +0 -0
  130. llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  131. llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
  132. llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.woff +0 -0
  133. llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  134. llms/extensions/katex/ui/index.mjs +92 -0
  135. llms/extensions/katex/ui/katex-swap.css +1230 -0
  136. llms/extensions/katex/ui/katex-swap.min.css +1 -0
  137. llms/extensions/katex/ui/katex.css +1230 -0
  138. llms/extensions/katex/ui/katex.js +19080 -0
  139. llms/extensions/katex/ui/katex.min.css +1 -0
  140. llms/extensions/katex/ui/katex.min.js +1 -0
  141. llms/extensions/katex/ui/katex.min.mjs +1 -0
  142. llms/extensions/katex/ui/katex.mjs +18547 -0
  143. llms/extensions/providers/__init__.py +18 -0
  144. llms/extensions/providers/__pycache__/__init__.cpython-314.pyc +0 -0
  145. llms/extensions/providers/__pycache__/anthropic.cpython-314.pyc +0 -0
  146. llms/extensions/providers/__pycache__/chutes.cpython-314.pyc +0 -0
  147. llms/extensions/providers/__pycache__/google.cpython-314.pyc +0 -0
  148. llms/extensions/providers/__pycache__/nvidia.cpython-314.pyc +0 -0
  149. llms/extensions/providers/__pycache__/openai.cpython-314.pyc +0 -0
  150. llms/extensions/providers/__pycache__/openrouter.cpython-314.pyc +0 -0
  151. llms/extensions/providers/anthropic.py +229 -0
  152. llms/extensions/providers/chutes.py +155 -0
  153. llms/extensions/providers/google.py +378 -0
  154. llms/extensions/providers/nvidia.py +105 -0
  155. llms/extensions/providers/openai.py +156 -0
  156. llms/extensions/providers/openrouter.py +72 -0
  157. llms/extensions/system_prompts/README.md +22 -0
  158. llms/extensions/system_prompts/__init__.py +45 -0
  159. llms/extensions/system_prompts/__pycache__/__init__.cpython-314.pyc +0 -0
  160. llms/extensions/system_prompts/ui/index.mjs +280 -0
  161. llms/extensions/system_prompts/ui/prompts.json +1067 -0
  162. llms/extensions/tools/__init__.py +5 -0
  163. llms/extensions/tools/__pycache__/__init__.cpython-314.pyc +0 -0
  164. llms/extensions/tools/ui/index.mjs +204 -0
  165. llms/index.html +35 -77
  166. llms/llms.json +357 -1186
  167. llms/main.py +2847 -999
  168. llms/providers-extra.json +356 -0
  169. llms/providers.json +1 -0
  170. llms/ui/App.mjs +151 -60
  171. llms/ui/ai.mjs +132 -60
  172. llms/ui/app.css +2173 -161
  173. llms/ui/ctx.mjs +365 -0
  174. llms/ui/index.mjs +129 -0
  175. llms/ui/lib/charts.mjs +9 -13
  176. llms/ui/lib/servicestack-vue.mjs +3 -3
  177. llms/ui/lib/vue.min.mjs +10 -9
  178. llms/ui/lib/vue.mjs +1796 -1635
  179. llms/ui/markdown.mjs +18 -7
  180. llms/ui/modules/chat/ChatBody.mjs +691 -0
  181. llms/ui/{SettingsDialog.mjs → modules/chat/SettingsDialog.mjs} +9 -9
  182. llms/ui/modules/chat/index.mjs +828 -0
  183. llms/ui/modules/layout.mjs +243 -0
  184. llms/ui/modules/model-selector.mjs +851 -0
  185. llms/ui/tailwind.input.css +496 -80
  186. llms/ui/utils.mjs +161 -93
  187. {llms_py-2.0.34.dist-info → llms_py-3.0.0.dist-info}/METADATA +1 -1
  188. llms_py-3.0.0.dist-info/RECORD +202 -0
  189. llms/ui/Avatar.mjs +0 -85
  190. llms/ui/Brand.mjs +0 -52
  191. llms/ui/ChatPrompt.mjs +0 -590
  192. llms/ui/Main.mjs +0 -823
  193. llms/ui/ModelSelector.mjs +0 -78
  194. llms/ui/OAuthSignIn.mjs +0 -92
  195. llms/ui/ProviderIcon.mjs +0 -30
  196. llms/ui/ProviderStatus.mjs +0 -105
  197. llms/ui/SignIn.mjs +0 -64
  198. llms/ui/SystemPromptEditor.mjs +0 -31
  199. llms/ui/SystemPromptSelector.mjs +0 -56
  200. llms/ui/Welcome.mjs +0 -8
  201. llms/ui/threadStore.mjs +0 -563
  202. llms/ui.json +0 -1069
  203. llms_py-2.0.34.dist-info/RECORD +0 -48
  204. {llms_py-2.0.34.dist-info → llms_py-3.0.0.dist-info}/WHEEL +0 -0
  205. {llms_py-2.0.34.dist-info → llms_py-3.0.0.dist-info}/entry_points.txt +0 -0
  206. {llms_py-2.0.34.dist-info → llms_py-3.0.0.dist-info}/licenses/LICENSE +0 -0
  207. {llms_py-2.0.34.dist-info → llms_py-3.0.0.dist-info}/top_level.txt +0 -0
@@ -1,23 +1,21 @@
1
1
  import { ref, onMounted, watch, inject } from 'vue'
2
2
  import { useRouter, useRoute } from 'vue-router'
3
- import { useThreadStore } from './threadStore.mjs'
4
- import { renderMarkdown } from './markdown.mjs'
5
3
 
6
4
  const RecentResults = {
7
- template:`
5
+ template: `
8
6
  <div class="flex-1 overflow-y-auto" @scroll="onScroll">
9
7
  <div class="mx-auto max-w-6xl px-4 py-4">
10
- <div class="text-sm text-gray-600 dark:text-gray-400 mb-3" v-if="threads.length">
11
- <span v-if="q">{{ filtered.length }} result{{ filtered.length===1?'':'s' }}</span>
12
- <span v-else>Searching {{ threads.length }} conversation{{ threads.length===1?'':'s' }}</span>
8
+ <div class="text-sm text-gray-600 dark:text-gray-400 mb-3">
9
+ <span v-if="q">{{ total }} result{{ total===1?'':'s' }}</span>
10
+ <span v-else>All conversations</span>
13
11
  </div>
14
12
 
15
- <div v-if="!threads.length" class="text-gray-500 dark:text-gray-400">No conversations yet.</div>
13
+ <div v-if="!loading && threads.length === 0" class="text-gray-500 dark:text-gray-400">No conversations found.</div>
16
14
 
17
15
  <table class="w-full">
18
16
  <tbody>
19
- <tr v-for="t in displayed" :key="t.id" class="hover:bg-gray-50 dark:hover:bg-gray-800">
20
- <td class="py-3 px-1 border-b border-gray-200 dark:border-gray-700 max-w-3xl">
17
+ <tr v-for="t in threads" :key="t.id" class="hover:bg-gray-50 dark:hover:bg-gray-800">
18
+ <td class="py-3 px-1 border-b border-gray-200 dark:border-gray-700 max-w-2xl">
21
19
  <button type="button" @click="open(t.id)" class="w-full text-left">
22
20
  <div class="flex items-start justify-between gap-3">
23
21
  <div class="min-w-0 flex-1">
@@ -33,12 +31,13 @@ const RecentResults = {
33
31
  <div class="text-right whitespace-nowrap">
34
32
  <div class="text-xs text-gray-500 dark:text-gray-400">{{ formatDate(t.updatedAt || t.createdAt) }}</div>
35
33
  <div class="text-[11px] text-gray-500/80 dark:text-gray-400/80">{{ (t.messages?.length || 0) }} messages</div>
36
- <div v-if="t.model" class="text-[11px] text-blue-600 dark:text-blue-400">{{ t.model }}</div>
34
+ <div v-if="t.model" class="text-[11px] text-blue-600 dark:text-blue-400 max-w-[140px] truncate" :title="t.model">{{ t.model }}</div>
37
35
  </div>
38
36
  </td>
39
37
  </tr>
40
38
  </tbody>
41
39
  </table>
40
+ <div v-if="loading" class="py-4 text-center text-gray-500 dark:text-gray-400">Loading...</div>
42
41
  </div>
43
42
  </div>
44
43
  `,
@@ -46,100 +45,126 @@ const RecentResults = {
46
45
  q: String
47
46
  },
48
47
  setup(props) {
49
- const ai = inject('ai')
48
+ const ctx = inject('ctx')
49
+ const ai = ctx.ai
50
50
  const router = useRouter()
51
- const config = inject('config')
52
- const { threads, loadThreads } = useThreadStore()
53
- let defaultVisibleCount = 25
54
- const visibleCount = ref(defaultVisibleCount)
55
- const filtered = ref([])
56
- const displayed = ref([])
57
-
58
- const start = Date.now()
59
- console.log('start', start, threads.value.length)
60
-
61
- onMounted(async () => {
62
- visibleCount.value = defaultVisibleCount
63
- if (!threads.value.length) {
64
- await loadThreads()
51
+
52
+ const threads = ref([])
53
+ const loading = ref(false)
54
+ const noMore = ref(false)
55
+ const total = ref(0)
56
+ let skip = 0
57
+ const take = 25
58
+
59
+ // Simple debounce function
60
+ function debounce(fn, delay) {
61
+ let timeoutID = null
62
+ return function () {
63
+ clearTimeout(timeoutID)
64
+ timeoutID = setTimeout(() => fn.apply(this, arguments), delay)
65
65
  }
66
- update()
67
- console.log('end', Date.now() - start)
68
- })
66
+ }
69
67
 
70
68
  const normalized = (s) => (s || '').toString().toLowerCase()
71
-
72
69
  const replaceChars = new Set('<>`*|#'.split(''))
73
70
  const clean = s => [...s].map(c => replaceChars.has(c) ? ' ' : c).join('')
74
71
 
75
- function update() {
76
- console.log('update', props.q)
77
- const query = normalized(props.q)
78
- filtered.value = !query
79
- ? threads.value
80
- : threads.value.filter(t => {
81
- const inTitle = normalized(t.title).includes(query)
82
- const inMsgs = Array.isArray(t.messages) && t.messages.some(m => normalized(m?.content).includes(query))
83
- return inTitle || inMsgs
84
- })
85
- updateVisible()
86
- }
87
- function updateVisible() {
88
- displayed.value = filtered.value.slice(0, Math.min(visibleCount.value, filtered.value.length))
72
+ const loadMore = async (reset = false) => {
73
+ if (reset) {
74
+ skip = 0
75
+ threads.value = []
76
+ noMore.value = false
77
+ }
78
+
79
+ if (loading.value || noMore.value) return
80
+
81
+ loading.value = true
82
+ try {
83
+ const query = {
84
+ take,
85
+ skip,
86
+ ...(props.q ? { q: props.q } : {})
87
+ }
88
+
89
+ const results = await ctx.threads.query(query)
90
+
91
+ if (results.length < take) {
92
+ noMore.value = true
93
+ }
94
+
95
+ if (reset) {
96
+ threads.value = results
97
+ } else {
98
+ threads.value.push(...results)
99
+ }
100
+
101
+ skip += results.length
102
+
103
+ total.value = threads.value.length
104
+ } catch (e) {
105
+ console.error("Failed to load threads", e)
106
+ } finally {
107
+ loading.value = false
108
+ }
89
109
  }
90
110
 
111
+ const update = debounce(() => loadMore(true), 250)
112
+
113
+ onMounted(() => {
114
+ loadMore(true)
115
+ })
116
+
91
117
  const onScroll = (e) => {
92
118
  const el = e.target
93
- if (el.scrollTop + el.clientHeight >= el.scrollHeight - 24) {
94
- if (visibleCount.value < filtered.value.length) {
95
- visibleCount.value = Math.min(visibleCount.value + defaultVisibleCount, filtered.value.length)
96
- updateVisible()
97
- }
119
+ if (el.scrollTop + el.clientHeight >= el.scrollHeight - 50) { // 50px threshold
120
+ loadMore()
98
121
  }
99
122
  }
100
123
 
101
124
  watch(() => props.q, () => {
102
- visibleCount.value = defaultVisibleCount
103
125
  update()
104
126
  })
105
127
 
106
128
  const snippet = (t) => {
107
129
  const highlight = (s) => clean(s).replace(new RegExp(`(${query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi'), `<mark>$1</mark>`)
108
130
  const query = normalized(props.q)
109
- if (!query) return (t.messages && t.messages.length) ? highlight(t.messages[t.messages.length-1].content) : ''
131
+ if (!query) return (t.messages && t.messages.length) ? highlight(t.messages[t.messages.length - 1].content) : ''
132
+
133
+ // Check title
110
134
  if (normalized(t.title).includes(query)) return highlight(t.title)
111
- if (Array.isArray(t.messages)){
112
- for (const m of t.messages){
135
+
136
+ // Check messages
137
+ if (Array.isArray(t.messages)) {
138
+ for (const m of t.messages) {
113
139
  const c = normalized(m?.content)
114
- if (c.includes(query)){
140
+ if (c.includes(query)) {
115
141
  // return small excerpt around first match
116
142
  const idx = c.indexOf(query)
117
143
  const orig = (m?.content || '')
118
144
  const start = Math.max(0, idx - 40)
119
145
  const end = Math.min(orig.length, idx + query.length + 60)
120
- const prefix = start>0 ? '…' : ''
121
- const suffix = end<orig.length ? '…' : ''
122
- const snippet = prefix + orig.slice(start, end) + suffix
123
- // return snippet
124
- return highlight(snippet)
146
+ const prefix = start > 0 ? '…' : ''
147
+ const suffix = end < orig.length ? '…' : ''
148
+ const snippetText = prefix + orig.slice(start, end) + suffix
149
+ return highlight(snippetText)
125
150
  }
126
151
  }
127
152
  }
128
- return ''
153
+
154
+ // Fallback to last message if no specific match found (e.g. matched on hidden metadata or partial?)
155
+ return (t.messages && t.messages.length) ? highlight(t.messages[t.messages.length - 1].content) : ''
129
156
  }
130
157
 
131
158
  const open = (id) => router.push(`${ai.base}/c/${id}`)
132
159
  const formatDate = (iso) => new Date(iso).toLocaleString()
133
160
 
134
161
  return {
135
- config,
136
162
  threads,
137
- filtered,
138
- displayed,
163
+ loading,
164
+ total,
139
165
  snippet,
140
166
  open,
141
167
  formatDate,
142
- renderMarkdown,
143
168
  onScroll,
144
169
  }
145
170
  }
@@ -154,9 +179,10 @@ export default {
154
179
  <!-- Header -->
155
180
  <div class="border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 px-4 py-3 min-h-16">
156
181
  <div class="max-w-6xl mx-auto flex items-center justify-between gap-3">
157
- <h2 class="text-lg font-semibold text-gray-900 dark:text-gray-100">Search Chats</h2>
158
- <div class="flex-1 flex items-center gap-2">
182
+ <label for="search-history" class="cursor-pointer text-lg font-semibold text-gray-900 dark:text-gray-100">Search History</label>
183
+ <div class="flex-1 flex items-center gap-2 max-w-sm">
159
184
  <input
185
+ id="search-history"
160
186
  v-model="q"
161
187
  type="search"
162
188
  placeholder="Search titles and messages..."
@@ -1,11 +1,10 @@
1
1
  import { onMounted, inject } from 'vue'
2
2
  import { useRouter } from 'vue-router'
3
- import { useFormatters } from '@servicestack/vue'
4
- import { useThreadStore } from './threadStore.mjs'
5
- import Brand from './Brand.mjs'
6
- import { statsTitle, formatCost } from './utils.mjs'
3
+ import { appendQueryString } from '@servicestack/client'
4
+ import ThreadStore from './threadStore.mjs'
5
+ import Recents from './Recents.mjs'
7
6
 
8
- const { humanifyNumber, humanifyMs } = useFormatters()
7
+ let ext
9
8
 
10
9
  // Thread Item Component
11
10
  const ThreadItem = {
@@ -21,10 +20,10 @@ const ThreadItem = {
21
20
  {{ thread.title }}
22
21
  </div>
23
22
  <div class="text-xs text-gray-500 dark:text-gray-400 truncate">
24
- <span>{{ formatRelativeTime(thread.updatedAt) }} • {{ thread.messages.length }} msgs</span>
25
- <span v-if="thread.stats?.inputTokens" :title="statsTitle(thread.stats)">
26
- &#8226; {{ humanifyNumber(thread.stats.inputTokens + thread.stats.outputTokens) }} toks
27
- {{ thread.stats.cost ? ' ' + formatCost(thread.stats.cost) : '' }}
23
+ <span>{{ $fmt.relativeTime(thread.updatedAt) }} • {{ thread.messages.length }} msgs</span>
24
+ <span v-if="thread.stats?.inputTokens" :title="$fmt.statsTitle(thread.stats)">
25
+ &#8226; {{ $fmt.humanifyNumber(thread.stats.inputTokens + thread.stats.outputTokens) }} toks
26
+ {{ thread.stats.cost ? ' ' + $fmt.cost(thread.stats.cost) : '' }}
28
27
  </span>
29
28
  </div>
30
29
  <div v-if="thread.model" class="text-xs text-blue-600 dark:text-blue-400 truncate">
@@ -60,32 +59,12 @@ const ThreadItem = {
60
59
  emits: ['select', 'delete'],
61
60
 
62
61
  setup() {
63
- const formatRelativeTime = (timestamp) => {
64
- const now = new Date()
65
- const date = new Date(timestamp)
66
- const diffInSeconds = Math.floor((now - date) / 1000)
67
-
68
- if (diffInSeconds < 60) return 'Just now'
69
- if (diffInSeconds < 3600) return `${Math.floor(diffInSeconds / 60)}m ago`
70
- if (diffInSeconds < 86400) return `${Math.floor(diffInSeconds / 3600)}h ago`
71
- if (diffInSeconds < 604800) return `${Math.floor(diffInSeconds / 86400)}d ago`
72
-
73
- return date.toLocaleDateString()
74
- }
75
-
76
62
  return {
77
- formatRelativeTime,
78
- humanifyNumber,
79
- statsTitle,
80
- formatCost,
81
63
  }
82
64
  }
83
65
  }
84
66
 
85
67
  const GroupedThreads = {
86
- components: {
87
- ThreadItem,
88
- },
89
68
  template: `
90
69
  <!-- Today -->
91
70
  <div v-if="groupedThreads.today.length > 0" class="mb-4">
@@ -94,7 +73,20 @@ const GroupedThreads = {
94
73
  v-for="thread in groupedThreads.today"
95
74
  :key="thread.id"
96
75
  :thread="thread"
97
- :is-active="currentThread?.id === thread.id"
76
+ :is-active="currentThread?.id == thread.id"
77
+ @select="$emit('select', $event)"
78
+ @delete="$emit('delete', $event)"
79
+ />
80
+ </div>
81
+
82
+ <!-- Yesterday -->
83
+ <div v-if="groupedThreads.yesterday.length > 0" class="mb-4">
84
+ <h3 class="px-4 py-2 text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider select-none">Yesterday</h3>
85
+ <ThreadItem
86
+ v-for="thread in groupedThreads.yesterday"
87
+ :key="thread.id"
88
+ :thread="thread"
89
+ :is-active="currentThread?.id == thread.id"
98
90
  @select="$emit('select', $event)"
99
91
  @delete="$emit('delete', $event)"
100
92
  />
@@ -107,7 +99,7 @@ const GroupedThreads = {
107
99
  v-for="thread in groupedThreads.lastWeek"
108
100
  :key="thread.id"
109
101
  :thread="thread"
110
- :is-active="currentThread?.id === thread.id"
102
+ :is-active="currentThread?.id == thread.id"
111
103
  @select="$emit('select', $event)"
112
104
  @delete="$emit('delete', $event)"
113
105
  />
@@ -120,7 +112,7 @@ const GroupedThreads = {
120
112
  v-for="thread in groupedThreads.lastMonth"
121
113
  :key="thread.id"
122
114
  :thread="thread"
123
- :is-active="currentThread?.id === thread.id"
115
+ :is-active="currentThread?.id == thread.id"
124
116
  @select="$emit('select', $event)"
125
117
  @delete="$emit('delete', $event)"
126
118
  />
@@ -133,7 +125,7 @@ const GroupedThreads = {
133
125
  v-for="thread in monthThreads"
134
126
  :key="thread.id"
135
127
  :thread="thread"
136
- :is-active="currentThread?.id === thread.id"
128
+ :is-active="currentThread?.id == thread.id"
137
129
  @select="$emit('select', $event)"
138
130
  @delete="$emit('delete', $event)"
139
131
  />
@@ -156,15 +148,10 @@ const GroupedThreads = {
156
148
  emits: ['select', 'delete'],
157
149
  }
158
150
 
159
- const Sidebar = {
160
- components: {
161
- Brand,
162
- GroupedThreads,
163
- ThreadItem,
164
- },
151
+ const ThreadsSidebar = {
165
152
  template: `
166
- <div class="flex flex-col h-full bg-gray-50 dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700">
167
- <Brand @home="goToInitialState" @new="createNewThread" @analytics="goToAnalytics" @toggle-sidebar="$emit('toggle-sidebar')" />
153
+ <div class="flex flex-col h-full">
154
+ <Brand />
168
155
  <!-- Thread List -->
169
156
  <div class="flex-1 overflow-y-auto">
170
157
  <div v-if="isLoading" class="p-4 text-center text-gray-500 dark:text-gray-400">
@@ -180,18 +167,28 @@ const Sidebar = {
180
167
  <p class="text-xs text-gray-400 dark:text-gray-500 mt-1">Start a new chat to begin</p>
181
168
  </div>
182
169
 
183
- <div v-else class="py-2">
184
- <GroupedThreads :currentThread="currentThread" :groupedThreads="threadStore.getGroupedThreads(18)"
170
+ <div v-else class="relative py-2">
171
+
172
+ <div class="flex items-center space-x-2 absolute top-2 right-2">
173
+ <button type="button"
174
+ @click="createNewThread"
175
+ class="text-gray-900 dark:text-gray-200 hover:text-blue-600 dark:hover:text-blue-400 focus:outline-none transition-colors"
176
+ title="New Chat"
177
+ >
178
+ <svg class="size-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path d="M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.375 2.625a1 1 0 0 1 3 3l-9.013 9.014a2 2 0 0 1-.853.505l-2.873.84a.5.5 0 0 1-.62-.62l.84-2.873a2 2 0 0 1 .506-.852z"/></g></svg>
179
+ </button>
180
+ </div>
181
+
182
+ <GroupedThreads :currentThread="currentThread" :groupedThreads="$threads.getGroupedThreads(50)"
185
183
  @select="selectThread" @delete="deleteThread" />
186
184
  </div>
187
185
  </div>
188
186
  </div>
189
187
  `,
190
- emits: ['thread-selected', 'toggle-sidebar'],
191
- setup(props, { emit }) {
192
- const ai = inject('ai')
188
+ setup(props) {
189
+ const ctx = inject('ctx')
190
+ const ai = ctx.ai
193
191
  const router = useRouter()
194
- const threadStore = useThreadStore()
195
192
  const {
196
193
  threads,
197
194
  currentThread,
@@ -201,7 +198,7 @@ const Sidebar = {
201
198
  createThread,
202
199
  deleteThread: deleteThreadFromStore,
203
200
  clearCurrentThread
204
- } = threadStore
201
+ } = ctx.threads
205
202
 
206
203
  onMounted(async () => {
207
204
  await loadThreads()
@@ -209,7 +206,6 @@ const Sidebar = {
209
206
 
210
207
  const selectThread = async (threadId) => {
211
208
  router.push(`${ai.base}/c/${threadId}`)
212
- emit('thread-selected')
213
209
  }
214
210
 
215
211
  const deleteThread = async (threadId) => {
@@ -223,24 +219,19 @@ const Sidebar = {
223
219
  }
224
220
 
225
221
  const createNewThread = async () => {
226
- const newThread = await createThread()
227
- router.push(`${ai.base}/c/${newThread.id}`)
228
- emit('thread-selected')
222
+ ctx.threads.startNewThread({ title: 'New Chat', model: ctx.chat.getSelectedModel(), redirect: true })
229
223
  }
230
224
 
231
225
  const goToInitialState = () => {
232
226
  clearCurrentThread()
233
- router.push(`${ai.base}/`)
234
- emit('thread-selected')
227
+ ctx.to(`/`)
235
228
  }
236
229
 
237
230
  const goToAnalytics = () => {
238
- router.push(`${ai.base}/analytics`)
239
- emit('thread-selected')
231
+ ctx.to(`/analytics`)
240
232
  }
241
233
 
242
234
  return {
243
- threadStore,
244
235
  threads,
245
236
  currentThread,
246
237
  isLoading,
@@ -254,4 +245,79 @@ const Sidebar = {
254
245
  }
255
246
  }
256
247
 
257
- export default Sidebar
248
+ function useRequests(ext) {
249
+ async function query(query) {
250
+ return (await ext.getJson(appendQueryString(`/requests`, query))).response || []
251
+ }
252
+ async function deleteById(requestId) {
253
+ if (!requestId) {
254
+ throw new Error('Request ID is required')
255
+ }
256
+ return await ext.deleteJson(`/requests/${requestId}`)
257
+ }
258
+
259
+ async function getThreadIds(query) {
260
+ return (await ext.getJson(appendQueryString(`/requests?fields=threadId&not_null=threadId&as=column&take=10000`, query))).response || []
261
+ }
262
+
263
+ async function getSummary() {
264
+ return (await ext.getJson(`/requests/summary`)).response
265
+ }
266
+ async function getDailySummary(day) {
267
+ return (await ext.getJson(`/requests/summary/${day}`)).response
268
+ }
269
+
270
+ // Get unique values for filter options
271
+ async function getFilterOptions() {
272
+ const results = await query({
273
+ select: 'distinct',
274
+ fields: 'model,provider',
275
+ not_null: 'model,provider',
276
+ })
277
+
278
+ if (results) {
279
+ const models = [...new Set(results.map(r => r.model).filter(m => m))].sort()
280
+ const providers = [...new Set(results.map(r => r.provider).filter(p => p))].sort()
281
+ console.log('getFilterOptions', models, providers)
282
+ return {
283
+ models,
284
+ providers
285
+ }
286
+ }
287
+ }
288
+
289
+ return {
290
+ query,
291
+ deleteById,
292
+ getThreadIds,
293
+ getSummary,
294
+ getDailySummary,
295
+ getFilterOptions,
296
+ }
297
+ }
298
+
299
+ export default {
300
+ order: -100,
301
+
302
+ install(ctx) {
303
+ ext = ctx.scope('app')
304
+ ctx.components({
305
+ ThreadsSidebar,
306
+ ThreadItem,
307
+ GroupedThreads,
308
+ Recents,
309
+ })
310
+ ctx.routes.push(...[
311
+ { path: '/recents', component: Recents },
312
+ ])
313
+ ThreadStore.install(ctx)
314
+
315
+ ctx.setGlobals({
316
+ requests: useRequests(ext)
317
+ })
318
+
319
+ ctx.setLayout({
320
+ left: 'ThreadsSidebar',
321
+ })
322
+ }
323
+ }