llms-py 2.0.9__py3-none-any.whl → 3.0.10__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. llms/__init__.py +4 -0
  2. llms/__main__.py +9 -0
  3. llms/db.py +359 -0
  4. llms/extensions/analytics/ui/index.mjs +1444 -0
  5. llms/extensions/app/README.md +20 -0
  6. llms/extensions/app/__init__.py +589 -0
  7. llms/extensions/app/db.py +536 -0
  8. {llms_py-2.0.9.data/data → llms/extensions/app}/ui/Recents.mjs +100 -73
  9. llms_py-2.0.9.data/data/ui/Sidebar.mjs → llms/extensions/app/ui/index.mjs +150 -79
  10. llms/extensions/app/ui/threadStore.mjs +433 -0
  11. llms/extensions/core_tools/CALCULATOR.md +32 -0
  12. llms/extensions/core_tools/__init__.py +637 -0
  13. llms/extensions/core_tools/ui/codemirror/addon/edit/closebrackets.js +201 -0
  14. llms/extensions/core_tools/ui/codemirror/addon/edit/closetag.js +185 -0
  15. llms/extensions/core_tools/ui/codemirror/addon/edit/continuelist.js +101 -0
  16. llms/extensions/core_tools/ui/codemirror/addon/edit/matchbrackets.js +160 -0
  17. llms/extensions/core_tools/ui/codemirror/addon/edit/matchtags.js +66 -0
  18. llms/extensions/core_tools/ui/codemirror/addon/edit/trailingspace.js +27 -0
  19. llms/extensions/core_tools/ui/codemirror/addon/selection/active-line.js +72 -0
  20. llms/extensions/core_tools/ui/codemirror/addon/selection/mark-selection.js +119 -0
  21. llms/extensions/core_tools/ui/codemirror/addon/selection/selection-pointer.js +98 -0
  22. llms/extensions/core_tools/ui/codemirror/codemirror.css +344 -0
  23. llms/extensions/core_tools/ui/codemirror/codemirror.js +9884 -0
  24. llms/extensions/core_tools/ui/codemirror/doc/docs.css +225 -0
  25. llms/extensions/core_tools/ui/codemirror/doc/source_sans.woff +0 -0
  26. llms/extensions/core_tools/ui/codemirror/mode/clike/clike.js +942 -0
  27. llms/extensions/core_tools/ui/codemirror/mode/javascript/index.html +118 -0
  28. llms/extensions/core_tools/ui/codemirror/mode/javascript/javascript.js +962 -0
  29. llms/extensions/core_tools/ui/codemirror/mode/javascript/typescript.html +62 -0
  30. llms/extensions/core_tools/ui/codemirror/mode/python/python.js +402 -0
  31. llms/extensions/core_tools/ui/codemirror/theme/dracula.css +40 -0
  32. llms/extensions/core_tools/ui/codemirror/theme/mocha.css +135 -0
  33. llms/extensions/core_tools/ui/index.mjs +650 -0
  34. llms/extensions/gallery/README.md +61 -0
  35. llms/extensions/gallery/__init__.py +63 -0
  36. llms/extensions/gallery/db.py +243 -0
  37. llms/extensions/gallery/ui/index.mjs +482 -0
  38. llms/extensions/katex/README.md +39 -0
  39. llms/extensions/katex/__init__.py +6 -0
  40. llms/extensions/katex/ui/README.md +125 -0
  41. llms/extensions/katex/ui/contrib/auto-render.js +338 -0
  42. llms/extensions/katex/ui/contrib/auto-render.min.js +1 -0
  43. llms/extensions/katex/ui/contrib/auto-render.mjs +244 -0
  44. llms/extensions/katex/ui/contrib/copy-tex.js +127 -0
  45. llms/extensions/katex/ui/contrib/copy-tex.min.js +1 -0
  46. llms/extensions/katex/ui/contrib/copy-tex.mjs +105 -0
  47. llms/extensions/katex/ui/contrib/mathtex-script-type.js +109 -0
  48. llms/extensions/katex/ui/contrib/mathtex-script-type.min.js +1 -0
  49. llms/extensions/katex/ui/contrib/mathtex-script-type.mjs +24 -0
  50. llms/extensions/katex/ui/contrib/mhchem.js +3213 -0
  51. llms/extensions/katex/ui/contrib/mhchem.min.js +1 -0
  52. llms/extensions/katex/ui/contrib/mhchem.mjs +3109 -0
  53. llms/extensions/katex/ui/contrib/render-a11y-string.js +887 -0
  54. llms/extensions/katex/ui/contrib/render-a11y-string.min.js +1 -0
  55. llms/extensions/katex/ui/contrib/render-a11y-string.mjs +800 -0
  56. llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.ttf +0 -0
  57. llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.woff +0 -0
  58. llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  59. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
  60. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
  61. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  62. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
  63. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
  64. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  65. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
  66. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.woff +0 -0
  67. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  68. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
  69. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.woff +0 -0
  70. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  71. llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.ttf +0 -0
  72. llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.woff +0 -0
  73. llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.woff2 +0 -0
  74. llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
  75. llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.woff +0 -0
  76. llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  77. llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.ttf +0 -0
  78. llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.woff +0 -0
  79. llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.woff2 +0 -0
  80. llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.ttf +0 -0
  81. llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.woff +0 -0
  82. llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.woff2 +0 -0
  83. llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
  84. llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.woff +0 -0
  85. llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  86. llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.ttf +0 -0
  87. llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.woff +0 -0
  88. llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.woff2 +0 -0
  89. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
  90. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.woff +0 -0
  91. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  92. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
  93. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.woff +0 -0
  94. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  95. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
  96. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.woff +0 -0
  97. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  98. llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.ttf +0 -0
  99. llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.woff +0 -0
  100. llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.woff2 +0 -0
  101. llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.ttf +0 -0
  102. llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.woff +0 -0
  103. llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  104. llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.ttf +0 -0
  105. llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.woff +0 -0
  106. llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  107. llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.ttf +0 -0
  108. llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.woff +0 -0
  109. llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  110. llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.ttf +0 -0
  111. llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.woff +0 -0
  112. llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  113. llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
  114. llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.woff +0 -0
  115. llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  116. llms/extensions/katex/ui/index.mjs +92 -0
  117. llms/extensions/katex/ui/katex-swap.css +1230 -0
  118. llms/extensions/katex/ui/katex-swap.min.css +1 -0
  119. llms/extensions/katex/ui/katex.css +1230 -0
  120. llms/extensions/katex/ui/katex.js +19080 -0
  121. llms/extensions/katex/ui/katex.min.css +1 -0
  122. llms/extensions/katex/ui/katex.min.js +1 -0
  123. llms/extensions/katex/ui/katex.min.mjs +1 -0
  124. llms/extensions/katex/ui/katex.mjs +18547 -0
  125. llms/extensions/providers/__init__.py +22 -0
  126. llms/extensions/providers/anthropic.py +233 -0
  127. llms/extensions/providers/cerebras.py +37 -0
  128. llms/extensions/providers/chutes.py +153 -0
  129. llms/extensions/providers/google.py +481 -0
  130. llms/extensions/providers/nvidia.py +103 -0
  131. llms/extensions/providers/openai.py +154 -0
  132. llms/extensions/providers/openrouter.py +74 -0
  133. llms/extensions/providers/zai.py +182 -0
  134. llms/extensions/system_prompts/README.md +22 -0
  135. llms/extensions/system_prompts/__init__.py +45 -0
  136. llms/extensions/system_prompts/ui/index.mjs +280 -0
  137. llms/extensions/system_prompts/ui/prompts.json +1067 -0
  138. llms/extensions/tools/__init__.py +144 -0
  139. llms/extensions/tools/ui/index.mjs +706 -0
  140. llms/index.html +58 -0
  141. llms/llms.json +400 -0
  142. llms/main.py +4407 -0
  143. llms/providers-extra.json +394 -0
  144. llms/providers.json +1 -0
  145. llms/ui/App.mjs +188 -0
  146. llms/ui/ai.mjs +217 -0
  147. llms/ui/app.css +7081 -0
  148. llms/ui/ctx.mjs +412 -0
  149. llms/ui/index.mjs +131 -0
  150. llms/ui/lib/chart.js +14 -0
  151. llms/ui/lib/charts.mjs +16 -0
  152. llms/ui/lib/color.js +14 -0
  153. llms/ui/lib/servicestack-vue.mjs +37 -0
  154. llms/ui/lib/vue.min.mjs +13 -0
  155. llms/ui/lib/vue.mjs +18530 -0
  156. {llms_py-2.0.9.data/data → llms}/ui/markdown.mjs +33 -15
  157. llms/ui/modules/chat/ChatBody.mjs +976 -0
  158. llms/ui/modules/chat/SettingsDialog.mjs +374 -0
  159. llms/ui/modules/chat/index.mjs +991 -0
  160. llms/ui/modules/icons.mjs +46 -0
  161. llms/ui/modules/layout.mjs +271 -0
  162. llms/ui/modules/model-selector.mjs +811 -0
  163. llms/ui/tailwind.input.css +742 -0
  164. {llms_py-2.0.9.data/data → llms}/ui/typography.css +133 -7
  165. llms/ui/utils.mjs +261 -0
  166. llms_py-3.0.10.dist-info/METADATA +49 -0
  167. llms_py-3.0.10.dist-info/RECORD +177 -0
  168. llms_py-3.0.10.dist-info/entry_points.txt +2 -0
  169. {llms_py-2.0.9.dist-info → llms_py-3.0.10.dist-info}/licenses/LICENSE +1 -2
  170. llms.py +0 -1402
  171. llms_py-2.0.9.data/data/index.html +0 -64
  172. llms_py-2.0.9.data/data/llms.json +0 -447
  173. llms_py-2.0.9.data/data/requirements.txt +0 -1
  174. llms_py-2.0.9.data/data/ui/App.mjs +0 -20
  175. llms_py-2.0.9.data/data/ui/ChatPrompt.mjs +0 -389
  176. llms_py-2.0.9.data/data/ui/Main.mjs +0 -680
  177. llms_py-2.0.9.data/data/ui/app.css +0 -3951
  178. llms_py-2.0.9.data/data/ui/lib/servicestack-vue.min.mjs +0 -37
  179. llms_py-2.0.9.data/data/ui/lib/vue.min.mjs +0 -12
  180. llms_py-2.0.9.data/data/ui/tailwind.input.css +0 -261
  181. llms_py-2.0.9.data/data/ui/threadStore.mjs +0 -273
  182. llms_py-2.0.9.data/data/ui/utils.mjs +0 -114
  183. llms_py-2.0.9.data/data/ui.json +0 -1069
  184. llms_py-2.0.9.dist-info/METADATA +0 -941
  185. llms_py-2.0.9.dist-info/RECORD +0 -30
  186. llms_py-2.0.9.dist-info/entry_points.txt +0 -2
  187. {llms_py-2.0.9.data/data → llms}/ui/fav.svg +0 -0
  188. {llms_py-2.0.9.data/data → llms}/ui/lib/highlight.min.mjs +0 -0
  189. {llms_py-2.0.9.data/data → llms}/ui/lib/idb.min.mjs +0 -0
  190. {llms_py-2.0.9.data/data → llms}/ui/lib/marked.min.mjs +0 -0
  191. /llms_py-2.0.9.data/data/ui/lib/servicestack-client.min.mjs → /llms/ui/lib/servicestack-client.mjs +0 -0
  192. {llms_py-2.0.9.data/data → llms}/ui/lib/vue-router.min.mjs +0 -0
  193. {llms_py-2.0.9.dist-info → llms_py-3.0.10.dist-info}/WHEEL +0 -0
  194. {llms_py-2.0.9.dist-info → llms_py-3.0.10.dist-info}/top_level.txt +0 -0
@@ -1,44 +1,43 @@
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 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">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">
20
- <td class="py-3 px-1 border-b border-gray-200 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">
24
- <div class="font-medium text-gray-900 truncate" :title="t.title">{{ t.title || 'Untitled chat' }}</div>
25
- <div class="mt-1 text-sm text-gray-600 line-clamp-2">
22
+ <div class="font-medium text-gray-900 dark:text-gray-100 truncate" :title="t.title">{{ t.title || 'Untitled chat' }}</div>
23
+ <div class="mt-1 text-sm text-gray-600 dark:text-gray-400 line-clamp-2">
26
24
  <div v-html="snippet(t)"></div>
27
25
  </div>
28
26
  </div>
29
27
  </div>
30
28
  </button>
31
29
  </td>
32
- <td class="py-3 px-1 border-b border-gray-200">
30
+ <td class="py-3 px-1 border-b border-gray-200 dark:border-gray-700">
33
31
  <div class="text-right whitespace-nowrap">
34
- <div class="text-xs text-gray-500">{{ formatDate(t.updatedAt || t.createdAt) }}</div>
35
- <div class="text-[11px] text-gray-500/80">{{ (t.messages?.length || 0) }} messages</div>
36
- <div v-if="t.model" class="text-[11px] text-blue-600">{{ t.model }}</div>
32
+ <div class="text-xs text-gray-500 dark:text-gray-400">{{ formatDate(t.updatedAt || t.createdAt) }}</div>
33
+ <div class="text-[11px] text-gray-500/80 dark:text-gray-400/80">{{ (t.messages?.length || 0) }} messages</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,99 +45,126 @@ const RecentResults = {
46
45
  q: String
47
46
  },
48
47
  setup(props) {
48
+ const ctx = inject('ctx')
49
+ const ai = ctx.ai
49
50
  const router = useRouter()
50
- const config = inject('config')
51
- const { threads, loadThreads } = useThreadStore()
52
- let defaultVisibleCount = 25
53
- const visibleCount = ref(defaultVisibleCount)
54
- const filtered = ref([])
55
- const displayed = ref([])
56
-
57
- const start = Date.now()
58
- console.log('start', start, threads.value.length)
59
-
60
- onMounted(async () => {
61
- visibleCount.value = defaultVisibleCount
62
- if (!threads.value.length) {
63
- 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)
64
65
  }
65
- update()
66
- console.log('end', Date.now() - start)
67
- })
66
+ }
68
67
 
69
68
  const normalized = (s) => (s || '').toString().toLowerCase()
70
-
71
69
  const replaceChars = new Set('<>`*|#'.split(''))
72
- const clean = s => [...s].map(c => replaceChars.has(c) ? ' ' : c).join('')
70
+ const clean = s => [...(s || '')].map(c => replaceChars.has(c) ? ' ' : c).join('')
73
71
 
74
- function update() {
75
- console.log('update', props.q)
76
- const query = normalized(props.q)
77
- filtered.value = !query
78
- ? threads.value
79
- : threads.value.filter(t => {
80
- const inTitle = normalized(t.title).includes(query)
81
- const inMsgs = Array.isArray(t.messages) && t.messages.some(m => normalized(m?.content).includes(query))
82
- return inTitle || inMsgs
83
- })
84
- updateVisible()
85
- }
86
- function updateVisible() {
87
- 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
+ }
88
109
  }
89
110
 
111
+ const update = debounce(() => loadMore(true), 250)
112
+
113
+ onMounted(() => {
114
+ loadMore(true)
115
+ })
116
+
90
117
  const onScroll = (e) => {
91
118
  const el = e.target
92
- if (el.scrollTop + el.clientHeight >= el.scrollHeight - 24) {
93
- if (visibleCount.value < filtered.value.length) {
94
- visibleCount.value = Math.min(visibleCount.value + defaultVisibleCount, filtered.value.length)
95
- updateVisible()
96
- }
119
+ if (el.scrollTop + el.clientHeight >= el.scrollHeight - 50) { // 50px threshold
120
+ loadMore()
97
121
  }
98
122
  }
99
123
 
100
124
  watch(() => props.q, () => {
101
- visibleCount.value = defaultVisibleCount
102
125
  update()
103
126
  })
104
127
 
105
128
  const snippet = (t) => {
106
129
  const highlight = (s) => clean(s).replace(new RegExp(`(${query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi'), `<mark>$1</mark>`)
107
130
  const query = normalized(props.q)
108
- 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
109
134
  if (normalized(t.title).includes(query)) return highlight(t.title)
110
- if (Array.isArray(t.messages)){
111
- for (const m of t.messages){
135
+
136
+ // Check messages
137
+ if (Array.isArray(t.messages)) {
138
+ for (const m of t.messages) {
112
139
  const c = normalized(m?.content)
113
- if (c.includes(query)){
140
+ if (c.includes(query)) {
114
141
  // return small excerpt around first match
115
142
  const idx = c.indexOf(query)
116
143
  const orig = (m?.content || '')
117
144
  const start = Math.max(0, idx - 40)
118
145
  const end = Math.min(orig.length, idx + query.length + 60)
119
- const prefix = start>0 ? '…' : ''
120
- const suffix = end<orig.length ? '…' : ''
121
- const snippet = prefix + orig.slice(start, end) + suffix
122
- // return snippet
123
- 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)
124
150
  }
125
151
  }
126
152
  }
127
- 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) : ''
128
156
  }
129
157
 
130
- const open = (id) => router.push(`/c/${id}`)
158
+ const open = (id) => router.push(`${ai.base}/c/${id}`)
131
159
  const formatDate = (iso) => new Date(iso).toLocaleString()
132
160
 
133
161
  return {
134
- config,
135
162
  threads,
136
- filtered,
137
- displayed,
163
+ loading,
164
+ total,
138
165
  snippet,
139
166
  open,
140
167
  formatDate,
141
- renderMarkdown,
142
168
  onScroll,
143
169
  }
144
170
  }
@@ -151,16 +177,17 @@ export default {
151
177
  template: `
152
178
  <div class="flex flex-col h-full w-full">
153
179
  <!-- Header -->
154
- <div class="border-b border-gray-200 bg-white px-4 py-3 min-h-16">
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">
155
181
  <div class="max-w-6xl mx-auto flex items-center justify-between gap-3">
156
- <h2 class="text-lg font-semibold text-gray-900">Search Chats</h2>
157
- <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">
158
184
  <input
185
+ id="search-history"
159
186
  v-model="q"
160
187
  type="search"
161
188
  placeholder="Search titles and messages..."
162
189
  spellcheck="false"
163
- class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm placeholder-gray-500 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
190
+ class="w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 px-3 py-2 text-sm placeholder-gray-500 dark:placeholder-gray-400 focus:border-blue-500 dark:focus:border-blue-400 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:focus:ring-blue-400"
164
191
  />
165
192
  </div>
166
193
  </div>
@@ -1,24 +1,32 @@
1
- import { onMounted } from 'vue'
1
+ import { onMounted, inject } from 'vue'
2
2
  import { useRouter } from 'vue-router'
3
- import { useThreadStore } from './threadStore.mjs'
3
+ import { appendQueryString } from '@servicestack/client'
4
+ import ThreadStore from './threadStore.mjs'
5
+ import Recents from './Recents.mjs'
6
+
7
+ let ext
4
8
 
5
9
  // Thread Item Component
6
10
  const ThreadItem = {
7
11
  template: `
8
12
  <div
9
13
  class="group relative mx-2 mb-1 rounded-md cursor-pointer transition-colors border border-transparent"
10
- :class="isActive ? 'bg-blue-100 border-blue-200' : 'hover:bg-gray-100'"
14
+ :class="isActive ? 'bg-blue-100 dark:bg-blue-900 border-blue-200 dark:border-blue-700' : 'hover:bg-gray-100 dark:hover:bg-gray-800'"
11
15
  @click="$emit('select', thread.id)"
12
16
  >
13
17
  <div class="flex items-center px-3 py-2">
14
18
  <div class="flex-1 min-w-0">
15
- <div class="text-sm font-medium text-gray-900 truncate" :title="thread.title">
19
+ <div class="text-sm font-medium text-gray-900 dark:text-gray-100 truncate" :title="thread.title">
16
20
  {{ thread.title }}
17
21
  </div>
18
- <div class="text-xs text-gray-500 truncate">
19
- {{ formatRelativeTime(thread.updatedAt) }} • {{ thread.messages.length }} messages
22
+ <div class="text-xs text-gray-500 dark:text-gray-400 truncate">
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) : '' }}
27
+ </span>
20
28
  </div>
21
- <div v-if="thread.model" class="text-xs text-blue-600 truncate">
29
+ <div v-if="thread.model" class="text-xs text-blue-600 dark:text-blue-400 truncate">
22
30
  {{ thread.model }}
23
31
  </div>
24
32
  </div>
@@ -26,7 +34,7 @@ const ThreadItem = {
26
34
  <!-- Delete button (shown on hover) -->
27
35
  <button type="button"
28
36
  @click.stop="$emit('delete', thread.id)"
29
- class="opacity-0 group-hover:opacity-100 ml-2 p-1 rounded text-gray-400 hover:text-red-600 hover:bg-red-50 transition-all"
37
+ class="opacity-0 group-hover:opacity-100 ml-2 p-1 rounded text-gray-400 dark:text-gray-500 hover:text-red-600 dark:hover:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/30 transition-all"
30
38
  title="Delete conversation"
31
39
  >
32
40
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -51,38 +59,34 @@ const ThreadItem = {
51
59
  emits: ['select', 'delete'],
52
60
 
53
61
  setup() {
54
- const formatRelativeTime = (timestamp) => {
55
- const now = new Date()
56
- const date = new Date(timestamp)
57
- const diffInSeconds = Math.floor((now - date) / 1000)
58
-
59
- if (diffInSeconds < 60) return 'Just now'
60
- if (diffInSeconds < 3600) return `${Math.floor(diffInSeconds / 60)}m ago`
61
- if (diffInSeconds < 86400) return `${Math.floor(diffInSeconds / 3600)}h ago`
62
- if (diffInSeconds < 604800) return `${Math.floor(diffInSeconds / 86400)}d ago`
63
-
64
- return date.toLocaleDateString()
65
- }
66
-
67
62
  return {
68
- formatRelativeTime
69
63
  }
70
64
  }
71
65
  }
72
66
 
73
67
  const GroupedThreads = {
74
- components: {
75
- ThreadItem,
76
- },
77
68
  template: `
78
69
  <!-- Today -->
79
70
  <div v-if="groupedThreads.today.length > 0" class="mb-4">
80
- <h3 class="px-4 py-2 text-xs font-semibold text-gray-500 uppercase tracking-wider select-none">Today</h3>
71
+ <h3 class="px-4 py-2 text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider select-none">Today</h3>
81
72
  <ThreadItem
82
73
  v-for="thread in groupedThreads.today"
83
74
  :key="thread.id"
84
75
  :thread="thread"
85
- :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"
86
90
  @select="$emit('select', $event)"
87
91
  @delete="$emit('delete', $event)"
88
92
  />
@@ -90,12 +94,12 @@ const GroupedThreads = {
90
94
 
91
95
  <!-- Last 7 Days -->
92
96
  <div v-if="groupedThreads.lastWeek.length > 0" class="mb-4">
93
- <h3 class="px-4 py-2 text-xs font-semibold text-gray-500 uppercase tracking-wider select-none">Last 7 Days</h3>
97
+ <h3 class="px-4 py-2 text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider select-none">Last 7 Days</h3>
94
98
  <ThreadItem
95
99
  v-for="thread in groupedThreads.lastWeek"
96
100
  :key="thread.id"
97
101
  :thread="thread"
98
- :is-active="currentThread?.id === thread.id"
102
+ :is-active="currentThread?.id == thread.id"
99
103
  @select="$emit('select', $event)"
100
104
  @delete="$emit('delete', $event)"
101
105
  />
@@ -103,12 +107,12 @@ const GroupedThreads = {
103
107
 
104
108
  <!-- Last 30 Days -->
105
109
  <div v-if="groupedThreads.lastMonth.length > 0" class="mb-4">
106
- <h3 class="px-4 py-2 text-xs font-semibold text-gray-500 uppercase tracking-wider select-none">Last 30 Days</h3>
110
+ <h3 class="px-4 py-2 text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider select-none">Last 30 Days</h3>
107
111
  <ThreadItem
108
112
  v-for="thread in groupedThreads.lastMonth"
109
113
  :key="thread.id"
110
114
  :thread="thread"
111
- :is-active="currentThread?.id === thread.id"
115
+ :is-active="currentThread?.id == thread.id"
112
116
  @select="$emit('select', $event)"
113
117
  @delete="$emit('delete', $event)"
114
118
  />
@@ -116,19 +120,19 @@ const GroupedThreads = {
116
120
 
117
121
  <!-- Older (grouped by month/year) -->
118
122
  <div v-for="(monthThreads, monthKey) in groupedThreads.older" :key="monthKey" class="mb-4">
119
- <h3 class="px-4 py-2 text-xs font-semibold text-gray-500 uppercase tracking-wider select-none">{{ monthKey }}</h3>
123
+ <h3 class="px-4 py-2 text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider select-none">{{ monthKey }}</h3>
120
124
  <ThreadItem
121
125
  v-for="thread in monthThreads"
122
126
  :key="thread.id"
123
127
  :thread="thread"
124
- :is-active="currentThread?.id === thread.id"
128
+ :is-active="currentThread?.id == thread.id"
125
129
  @select="$emit('select', $event)"
126
130
  @delete="$emit('delete', $event)"
127
131
  />
128
132
  </div>
129
133
  <div class="mb-4 flex w-full justify-center">
130
- <button @click="$router.push('/recents')" type="button"
131
- class="flex text-sm space-x-1 font-semibold text-gray-900 hover:text-blue-600 focus:outline-none transition-colors">
134
+ <button @click="$router.push($ai.base + '/recents')" type="button"
135
+ class="flex text-sm space-x-1 font-semibold text-gray-900 dark:text-gray-100 hover:text-blue-600 dark:hover:text-blue-400 focus:outline-none transition-colors">
132
136
  <svg class="size-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="currentColor" d="M8 2.19c3.13 0 5.68 2.25 5.68 5s-2.55 5-5.68 5a5.7 5.7 0 0 1-1.89-.29l-.75-.26l-.56.56a14 14 0 0 1-2 1.55a.13.13 0 0 1-.07 0v-.06a6.58 6.58 0 0 0 .15-4.29a5.25 5.25 0 0 1-.55-2.16c0-2.77 2.55-5 5.68-5M8 .94c-3.83 0-6.93 2.81-6.93 6.27a6.4 6.4 0 0 0 .64 2.64a5.53 5.53 0 0 1-.18 3.48a1.32 1.32 0 0 0 2 1.5a15 15 0 0 0 2.16-1.71a6.8 6.8 0 0 0 2.31.36c3.83 0 6.93-2.81 6.93-6.27S11.83.94 8 .94"></path><ellipse cx="5.2" cy="7.7" fill="currentColor" rx=".8" ry=".75"></ellipse><ellipse cx="8" cy="7.7" fill="currentColor" rx=".8" ry=".75"></ellipse><ellipse cx="10.8" cy="7.7" fill="currentColor" rx=".8" ry=".75"></ellipse></svg>
133
137
  <span>All Chats</span>
134
138
  </button>
@@ -144,58 +148,47 @@ const GroupedThreads = {
144
148
  emits: ['select', 'delete'],
145
149
  }
146
150
 
147
- const Sidebar = {
148
- components: {
149
- GroupedThreads,
150
- ThreadItem,
151
- },
151
+ const ThreadsSidebar = {
152
152
  template: `
153
- <div class="flex flex-col h-full bg-gray-50 border-r border-gray-200">
154
- <!-- Header -->
155
- <div class="flex-shrink-0 px-4 py-4 border-b border-gray-200 bg-white min-h-16 select-none">
156
- <div class="flex items-center justify-between">
157
- <button type="button"
158
- @click="goToInitialState"
159
- class="text-lg font-semibold text-gray-900 hover:text-blue-600 focus:outline-none transition-colors"
160
- title="Go back to initial state"
161
- >
162
- History
163
- </button>
164
- <button type="button"
165
- @click="createNewThread"
166
- class="text-gray-900 hover:text-blue-600 focus:outline-none transition-colors"
167
- title="New Chat"
168
- >
169
- <svg class="size-6" 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>
170
- </button>
171
- </div>
172
- </div>
173
-
153
+ <div class="flex flex-col h-full">
154
+ <Brand />
174
155
  <!-- Thread List -->
175
156
  <div class="flex-1 overflow-y-auto">
176
- <div v-if="isLoading" class="p-4 text-center text-gray-500">
177
- <div class="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600 mx-auto"></div>
157
+ <div v-if="isLoading" class="p-4 text-center text-gray-500 dark:text-gray-400">
158
+ <div class="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600 dark:border-blue-400 mx-auto"></div>
178
159
  <p class="mt-2 text-sm">Loading threads...</p>
179
160
  </div>
180
161
 
181
- <div v-else-if="threads.length === 0" class="p-4 text-center text-gray-500">
162
+ <div v-else-if="threads.length === 0" class="p-4 text-center text-gray-500 dark:text-gray-400">
182
163
  <div class="mb-2 flex justify-center">
183
164
  <svg class="size-8" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="currentColor" d="M8 2.19c3.13 0 5.68 2.25 5.68 5s-2.55 5-5.68 5a5.7 5.7 0 0 1-1.89-.29l-.75-.26l-.56.56a14 14 0 0 1-2 1.55a.13.13 0 0 1-.07 0v-.06a6.58 6.58 0 0 0 .15-4.29a5.25 5.25 0 0 1-.55-2.16c0-2.77 2.55-5 5.68-5M8 .94c-3.83 0-6.93 2.81-6.93 6.27a6.4 6.4 0 0 0 .64 2.64a5.53 5.53 0 0 1-.18 3.48a1.32 1.32 0 0 0 2 1.5a15 15 0 0 0 2.16-1.71a6.8 6.8 0 0 0 2.31.36c3.83 0 6.93-2.81 6.93-6.27S11.83.94 8 .94"/><ellipse cx="5.2" cy="7.7" fill="currentColor" rx=".8" ry=".75"/><ellipse cx="8" cy="7.7" fill="currentColor" rx=".8" ry=".75"/><ellipse cx="10.8" cy="7.7" fill="currentColor" rx=".8" ry=".75"/></svg>
184
165
  </div>
185
166
  <p class="text-sm">No conversations yet</p>
186
- <p class="text-xs text-gray-400 mt-1">Start a new chat to begin</p>
167
+ <p class="text-xs text-gray-400 dark:text-gray-500 mt-1">Start a new chat to begin</p>
187
168
  </div>
188
169
 
189
- <div v-else class="py-2">
190
- <GroupedThreads :currentThread="currentThread" :groupedThreads="threadStore.getGroupedThreads(19)"
191
- @select="selectThread" @delete="deleteThread" />
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)"
183
+ @select="selectThread" @delete="deleteThread" />
192
184
  </div>
193
185
  </div>
194
186
  </div>
195
187
  `,
196
- setup() {
188
+ setup(props) {
189
+ const ctx = inject('ctx')
190
+ const ai = ctx.ai
197
191
  const router = useRouter()
198
- const threadStore = useThreadStore()
199
192
  const {
200
193
  threads,
201
194
  currentThread,
@@ -205,14 +198,14 @@ const Sidebar = {
205
198
  createThread,
206
199
  deleteThread: deleteThreadFromStore,
207
200
  clearCurrentThread
208
- } = threadStore
201
+ } = ctx.threads
209
202
 
210
203
  onMounted(async () => {
211
204
  await loadThreads()
212
205
  })
213
206
 
214
207
  const selectThread = async (threadId) => {
215
- router.push(`/c/${threadId}`)
208
+ router.push(`${ai.base}/c/${threadId}`)
216
209
  }
217
210
 
218
211
  const deleteThread = async (threadId) => {
@@ -220,23 +213,25 @@ const Sidebar = {
220
213
  const wasCurrent = currentThread?.value?.id === threadId
221
214
  await deleteThreadFromStore(threadId)
222
215
  if (wasCurrent) {
223
- router.push('/')
216
+ router.push(`${ai.base}/`)
224
217
  }
225
218
  }
226
219
  }
227
220
 
228
221
  const createNewThread = async () => {
229
- const newThread = await createThread()
230
- router.push(`/c/${newThread.id}`)
222
+ ctx.threads.startNewThread({ title: 'New Chat', model: ctx.chat.getSelectedModel(), redirect: true })
231
223
  }
232
224
 
233
225
  const goToInitialState = () => {
234
226
  clearCurrentThread()
235
- router.push('/')
227
+ ctx.to(`/`)
228
+ }
229
+
230
+ const goToAnalytics = () => {
231
+ ctx.to(`/analytics`)
236
232
  }
237
233
 
238
234
  return {
239
- threadStore,
240
235
  threads,
241
236
  currentThread,
242
237
  isLoading,
@@ -244,9 +239,85 @@ const Sidebar = {
244
239
  selectThread,
245
240
  deleteThread,
246
241
  createNewThread,
247
- goToInitialState
242
+ goToInitialState,
243
+ goToAnalytics,
244
+ }
245
+ }
246
+ }
247
+
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')
248
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,
249
296
  }
250
297
  }
251
298
 
252
- export default Sidebar
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
+ }