llms-py 3.0.0__py3-none-any.whl → 3.0.0b2__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 (195) hide show
  1. llms/__pycache__/main.cpython-314.pyc +0 -0
  2. llms/index.html +37 -26
  3. llms/llms.json +21 -70
  4. llms/main.py +731 -1426
  5. llms/providers.json +1 -1
  6. llms/{extensions/analytics/ui/index.mjs → ui/Analytics.mjs} +238 -154
  7. llms/ui/App.mjs +63 -133
  8. llms/ui/Avatar.mjs +86 -0
  9. llms/ui/Brand.mjs +52 -0
  10. llms/ui/ChatPrompt.mjs +597 -0
  11. llms/ui/Main.mjs +862 -0
  12. llms/ui/OAuthSignIn.mjs +61 -0
  13. llms/ui/ProviderIcon.mjs +36 -0
  14. llms/ui/ProviderStatus.mjs +104 -0
  15. llms/{extensions/app/ui → ui}/Recents.mjs +57 -82
  16. llms/ui/{modules/chat/SettingsDialog.mjs → SettingsDialog.mjs} +9 -9
  17. llms/{extensions/app/ui/index.mjs → ui/Sidebar.mjs} +57 -122
  18. llms/ui/SignIn.mjs +65 -0
  19. llms/ui/Welcome.mjs +8 -0
  20. llms/ui/ai.mjs +13 -117
  21. llms/ui/app.css +49 -1776
  22. llms/ui/index.mjs +171 -87
  23. llms/ui/lib/charts.mjs +13 -9
  24. llms/ui/lib/servicestack-vue.mjs +3 -3
  25. llms/ui/lib/vue.min.mjs +9 -10
  26. llms/ui/lib/vue.mjs +1602 -1763
  27. llms/ui/markdown.mjs +2 -10
  28. llms/ui/model-selector.mjs +686 -0
  29. llms/ui/tailwind.input.css +1 -55
  30. llms/ui/threadStore.mjs +583 -0
  31. llms/ui/utils.mjs +118 -113
  32. llms/ui.json +1069 -0
  33. {llms_py-3.0.0.dist-info → llms_py-3.0.0b2.dist-info}/METADATA +1 -1
  34. llms_py-3.0.0b2.dist-info/RECORD +58 -0
  35. llms/extensions/app/README.md +0 -20
  36. llms/extensions/app/__init__.py +0 -530
  37. llms/extensions/app/__pycache__/__init__.cpython-314.pyc +0 -0
  38. llms/extensions/app/__pycache__/db.cpython-314.pyc +0 -0
  39. llms/extensions/app/__pycache__/db_manager.cpython-314.pyc +0 -0
  40. llms/extensions/app/db.py +0 -644
  41. llms/extensions/app/db_manager.py +0 -195
  42. llms/extensions/app/requests.json +0 -9073
  43. llms/extensions/app/threads.json +0 -15290
  44. llms/extensions/app/ui/threadStore.mjs +0 -411
  45. llms/extensions/core_tools/CALCULATOR.md +0 -32
  46. llms/extensions/core_tools/__init__.py +0 -598
  47. llms/extensions/core_tools/__pycache__/__init__.cpython-314.pyc +0 -0
  48. llms/extensions/core_tools/ui/codemirror/addon/edit/closebrackets.js +0 -201
  49. llms/extensions/core_tools/ui/codemirror/addon/edit/closetag.js +0 -185
  50. llms/extensions/core_tools/ui/codemirror/addon/edit/continuelist.js +0 -101
  51. llms/extensions/core_tools/ui/codemirror/addon/edit/matchbrackets.js +0 -160
  52. llms/extensions/core_tools/ui/codemirror/addon/edit/matchtags.js +0 -66
  53. llms/extensions/core_tools/ui/codemirror/addon/edit/trailingspace.js +0 -27
  54. llms/extensions/core_tools/ui/codemirror/addon/selection/active-line.js +0 -72
  55. llms/extensions/core_tools/ui/codemirror/addon/selection/mark-selection.js +0 -119
  56. llms/extensions/core_tools/ui/codemirror/addon/selection/selection-pointer.js +0 -98
  57. llms/extensions/core_tools/ui/codemirror/doc/docs.css +0 -225
  58. llms/extensions/core_tools/ui/codemirror/doc/source_sans.woff +0 -0
  59. llms/extensions/core_tools/ui/codemirror/lib/codemirror.css +0 -344
  60. llms/extensions/core_tools/ui/codemirror/lib/codemirror.js +0 -9884
  61. llms/extensions/core_tools/ui/codemirror/mode/clike/clike.js +0 -942
  62. llms/extensions/core_tools/ui/codemirror/mode/javascript/index.html +0 -118
  63. llms/extensions/core_tools/ui/codemirror/mode/javascript/javascript.js +0 -962
  64. llms/extensions/core_tools/ui/codemirror/mode/javascript/typescript.html +0 -62
  65. llms/extensions/core_tools/ui/codemirror/mode/python/python.js +0 -402
  66. llms/extensions/core_tools/ui/codemirror/theme/dracula.css +0 -40
  67. llms/extensions/core_tools/ui/codemirror/theme/mocha.css +0 -135
  68. llms/extensions/core_tools/ui/index.mjs +0 -650
  69. llms/extensions/gallery/README.md +0 -61
  70. llms/extensions/gallery/__init__.py +0 -61
  71. llms/extensions/gallery/__pycache__/__init__.cpython-314.pyc +0 -0
  72. llms/extensions/gallery/__pycache__/db.cpython-314.pyc +0 -0
  73. llms/extensions/gallery/db.py +0 -298
  74. llms/extensions/gallery/ui/index.mjs +0 -482
  75. llms/extensions/katex/README.md +0 -39
  76. llms/extensions/katex/__init__.py +0 -6
  77. llms/extensions/katex/__pycache__/__init__.cpython-314.pyc +0 -0
  78. llms/extensions/katex/ui/README.md +0 -125
  79. llms/extensions/katex/ui/contrib/auto-render.js +0 -338
  80. llms/extensions/katex/ui/contrib/auto-render.min.js +0 -1
  81. llms/extensions/katex/ui/contrib/auto-render.mjs +0 -244
  82. llms/extensions/katex/ui/contrib/copy-tex.js +0 -127
  83. llms/extensions/katex/ui/contrib/copy-tex.min.js +0 -1
  84. llms/extensions/katex/ui/contrib/copy-tex.mjs +0 -105
  85. llms/extensions/katex/ui/contrib/mathtex-script-type.js +0 -109
  86. llms/extensions/katex/ui/contrib/mathtex-script-type.min.js +0 -1
  87. llms/extensions/katex/ui/contrib/mathtex-script-type.mjs +0 -24
  88. llms/extensions/katex/ui/contrib/mhchem.js +0 -3213
  89. llms/extensions/katex/ui/contrib/mhchem.min.js +0 -1
  90. llms/extensions/katex/ui/contrib/mhchem.mjs +0 -3109
  91. llms/extensions/katex/ui/contrib/render-a11y-string.js +0 -887
  92. llms/extensions/katex/ui/contrib/render-a11y-string.min.js +0 -1
  93. llms/extensions/katex/ui/contrib/render-a11y-string.mjs +0 -800
  94. llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.ttf +0 -0
  95. llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.woff +0 -0
  96. llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  97. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
  98. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
  99. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  100. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
  101. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
  102. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  103. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
  104. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.woff +0 -0
  105. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  106. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
  107. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.woff +0 -0
  108. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  109. llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.ttf +0 -0
  110. llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.woff +0 -0
  111. llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.woff2 +0 -0
  112. llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
  113. llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.woff +0 -0
  114. llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  115. llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.ttf +0 -0
  116. llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.woff +0 -0
  117. llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.woff2 +0 -0
  118. llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.ttf +0 -0
  119. llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.woff +0 -0
  120. llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.woff2 +0 -0
  121. llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
  122. llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.woff +0 -0
  123. llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  124. llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.ttf +0 -0
  125. llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.woff +0 -0
  126. llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.woff2 +0 -0
  127. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
  128. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.woff +0 -0
  129. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  130. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
  131. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.woff +0 -0
  132. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  133. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
  134. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.woff +0 -0
  135. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  136. llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.ttf +0 -0
  137. llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.woff +0 -0
  138. llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.woff2 +0 -0
  139. llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.ttf +0 -0
  140. llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.woff +0 -0
  141. llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  142. llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.ttf +0 -0
  143. llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.woff +0 -0
  144. llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  145. llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.ttf +0 -0
  146. llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.woff +0 -0
  147. llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  148. llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.ttf +0 -0
  149. llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.woff +0 -0
  150. llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  151. llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
  152. llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.woff +0 -0
  153. llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  154. llms/extensions/katex/ui/index.mjs +0 -92
  155. llms/extensions/katex/ui/katex-swap.css +0 -1230
  156. llms/extensions/katex/ui/katex-swap.min.css +0 -1
  157. llms/extensions/katex/ui/katex.css +0 -1230
  158. llms/extensions/katex/ui/katex.js +0 -19080
  159. llms/extensions/katex/ui/katex.min.css +0 -1
  160. llms/extensions/katex/ui/katex.min.js +0 -1
  161. llms/extensions/katex/ui/katex.min.mjs +0 -1
  162. llms/extensions/katex/ui/katex.mjs +0 -18547
  163. llms/extensions/providers/__init__.py +0 -18
  164. llms/extensions/providers/__pycache__/__init__.cpython-314.pyc +0 -0
  165. llms/extensions/providers/__pycache__/anthropic.cpython-314.pyc +0 -0
  166. llms/extensions/providers/__pycache__/chutes.cpython-314.pyc +0 -0
  167. llms/extensions/providers/__pycache__/google.cpython-314.pyc +0 -0
  168. llms/extensions/providers/__pycache__/nvidia.cpython-314.pyc +0 -0
  169. llms/extensions/providers/__pycache__/openai.cpython-314.pyc +0 -0
  170. llms/extensions/providers/__pycache__/openrouter.cpython-314.pyc +0 -0
  171. llms/extensions/providers/anthropic.py +0 -229
  172. llms/extensions/providers/chutes.py +0 -155
  173. llms/extensions/providers/google.py +0 -378
  174. llms/extensions/providers/nvidia.py +0 -105
  175. llms/extensions/providers/openai.py +0 -156
  176. llms/extensions/providers/openrouter.py +0 -72
  177. llms/extensions/system_prompts/README.md +0 -22
  178. llms/extensions/system_prompts/__init__.py +0 -45
  179. llms/extensions/system_prompts/__pycache__/__init__.cpython-314.pyc +0 -0
  180. llms/extensions/system_prompts/ui/index.mjs +0 -280
  181. llms/extensions/system_prompts/ui/prompts.json +0 -1067
  182. llms/extensions/tools/__init__.py +0 -5
  183. llms/extensions/tools/__pycache__/__init__.cpython-314.pyc +0 -0
  184. llms/extensions/tools/ui/index.mjs +0 -204
  185. llms/providers-extra.json +0 -356
  186. llms/ui/ctx.mjs +0 -365
  187. llms/ui/modules/chat/ChatBody.mjs +0 -691
  188. llms/ui/modules/chat/index.mjs +0 -828
  189. llms/ui/modules/layout.mjs +0 -243
  190. llms/ui/modules/model-selector.mjs +0 -851
  191. llms_py-3.0.0.dist-info/RECORD +0 -202
  192. {llms_py-3.0.0.dist-info → llms_py-3.0.0b2.dist-info}/WHEEL +0 -0
  193. {llms_py-3.0.0.dist-info → llms_py-3.0.0b2.dist-info}/entry_points.txt +0 -0
  194. {llms_py-3.0.0.dist-info → llms_py-3.0.0b2.dist-info}/licenses/LICENSE +0 -0
  195. {llms_py-3.0.0.dist-info → llms_py-3.0.0b2.dist-info}/top_level.txt +0 -0
@@ -2,7 +2,6 @@
2
2
  @import "tailwindcss";
3
3
  @source "./lib/servicestack-vue.mjs";
4
4
  @source "../../extensions";
5
- @source "../../llms-home/extensions";
6
5
 
7
6
  @custom-variant dark (&:where(.dark, .dark *));
8
7
 
@@ -31,11 +30,6 @@
31
30
  ::file-selector-button {
32
31
  border-color: hsl(var(--border));
33
32
  }
34
-
35
- .reasoning .prose-xs p,
36
- .reasoning .prose-xs li {
37
- font-size: 13px;
38
- }
39
33
  }
40
34
 
41
35
  @theme {
@@ -150,22 +144,6 @@
150
144
  font-weight: 600;
151
145
  }
152
146
 
153
- /* Tool specific styles to override global prose */
154
- .tool-arguments,
155
- .tool-output {
156
- margin: 0 !important;
157
- padding: 0 !important;
158
- background-color: transparent !important;
159
- color: inherit !important;
160
- font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace !important;
161
- font-size: 0.75rem !important;
162
- white-space: pre-wrap !important;
163
- word-break: break-all !important;
164
- border: none !important;
165
- border-radius: 0 !important;
166
- overflow: auto !important;
167
- }
168
-
169
147
  /* highlight.js - vs.css */
170
148
  .hljs {
171
149
  background: white;
@@ -321,10 +299,6 @@
321
299
  background-color: #282c34 !important;
322
300
  }
323
301
 
324
- .prose :where(h1):not(:where([class~="not-prose"] *)) {
325
- font-weight: 700 !important;
326
- }
327
-
328
302
  .hljs-comment,
329
303
  .hljs-quote {
330
304
  color: rgb(148 163 184);
@@ -342,7 +316,7 @@
342
316
  padding: .8571429em 1.1428571em;
343
317
  max-width: calc(100vw - 1rem);
344
318
  min-width: fit-content;
345
- /* background-color: #282c34 !important; overrides CodeMirror */
319
+ background-color: #282c34 !important;
346
320
  }
347
321
 
348
322
  pre code.hljs {
@@ -657,32 +631,4 @@
657
631
  left: 0;
658
632
  }
659
633
 
660
-
661
- @media (min-width: 640px) {
662
- .message pre {
663
- max-width: 500px !important;
664
- }
665
- }
666
-
667
- @media (min-width: 768px) {
668
- .message pre {
669
- max-width: 600px !important;
670
- }
671
- }
672
-
673
- @media (min-width: 1024px) {
674
-
675
- .message pre,
676
- .message .prose pre {
677
- max-width: 700px !important;
678
- }
679
- }
680
-
681
- @media (min-width: 1280px) {
682
-
683
- .message pre,
684
- .message .prose pre {
685
- max-width: 800px !important;
686
- }
687
- }
688
634
  }
@@ -0,0 +1,583 @@
1
+ import { ref, computed, unref } from 'vue'
2
+ import { openDB } from 'idb'
3
+ import { nextId, toModelInfo } from './utils.mjs'
4
+
5
+ // Thread store for managing chat threads with IndexedDB
6
+ const threads = ref([])
7
+ const currentThread = ref(null)
8
+ const isLoading = ref(false)
9
+
10
+ let db = null
11
+ let ctx = null
12
+
13
+ export default {
14
+ install(context) {
15
+ ctx = context
16
+ }
17
+ }
18
+
19
+ // Initialize IndexedDB
20
+ async function initDB() {
21
+ if (db) return db
22
+
23
+ db = await openDB('LlmsThreads', 3, {
24
+ upgrade(db, _oldVersion, _newVersion, transaction) {
25
+ if (!db.objectStoreNames.contains('threads')) {
26
+ // Create threads store
27
+ const threadStore = db.createObjectStore('threads', {
28
+ keyPath: 'id',
29
+ autoIncrement: false
30
+ })
31
+
32
+ // Create indexes for efficient querying
33
+ threadStore.createIndex('createdAt', 'createdAt')
34
+ threadStore.createIndex('updatedAt', 'updatedAt')
35
+ threadStore.createIndex('title', 'title')
36
+ }
37
+ if (!db.objectStoreNames.contains('requests')) {
38
+ // Create requests store
39
+ const requestStore = db.createObjectStore('requests', {
40
+ keyPath: 'id',
41
+ autoIncrement: false
42
+ })
43
+ requestStore.createIndex('threadId', 'threadId')
44
+ requestStore.createIndex('model', 'model')
45
+ requestStore.createIndex('provider', 'provider')
46
+ requestStore.createIndex('inputTokens', 'inputTokens')
47
+ requestStore.createIndex('outputTokens', 'outputTokens')
48
+ requestStore.createIndex('cost', 'cost')
49
+ requestStore.createIndex('duration', 'duration')
50
+ requestStore.createIndex('created', 'created')
51
+ }
52
+ }
53
+ })
54
+
55
+ return db
56
+ }
57
+
58
+ // Generate unique thread ID
59
+ function generateThreadId() {
60
+ return Date.now().toString()
61
+ }
62
+
63
+ async function logRequest(threadId, model, request, response) {
64
+ await initDB()
65
+ const metadata = response.metadata || {}
66
+ const usage = response.usage || {}
67
+ const [inputPrice, outputPrice] = metadata.pricing ? metadata.pricing.split('/') : [0, 0]
68
+ const lastUserContent = request.messages?.slice().reverse().find(m => m.role === 'user')?.content
69
+ const content = Array.isArray(lastUserContent)
70
+ ? lastUserContent.filter(c => c?.text).map(c => c.text).join(' ')
71
+ : lastUserContent
72
+ const title = content.slice(0, 100) + (content.length > 100 ? '...' : '')
73
+ const inputTokens = usage?.prompt_tokens ?? 0
74
+ const outputTokens = usage?.completion_tokens ?? 0
75
+ const inputCachedTokens = usage?.prompt_token_details?.cached_tokens ?? 0
76
+ const finishReason = response.choices[0]?.finish_reason || 'unknown'
77
+
78
+ const subtractDays = (date, days) => {
79
+ const result = new Date(date * 1000)
80
+ result.setDate(result.getDate() - days)
81
+ return parseInt(result.valueOf() / 1000)
82
+ }
83
+
84
+ const log = {
85
+ id: nextId(),
86
+ threadId: threadId,
87
+ model: model.id,
88
+ provider: model.provider,
89
+ providerModel: response.model || model.provider_model,
90
+ title,
91
+ inputTokens,
92
+ outputTokens,
93
+ inputCachedTokens,
94
+ totalTokens: usage.total_tokens ?? (inputTokens + outputTokens),
95
+ inputPrice,
96
+ outputPrice,
97
+ cost: (parseFloat(inputPrice) * inputTokens / 1_000_000) + (parseFloat(outputPrice) * outputTokens / 1_000_000),
98
+ duration: parseInt(metadata.duration) || 0,
99
+ created: response.created ?? Math.floor(Date.now() / 1000),
100
+ finishReason,
101
+ providerRef: response.provider,
102
+ ref: response.id || undefined,
103
+ usage: usage,
104
+ }
105
+ console.debug('logRequest', log)
106
+ const tx = db.transaction(['requests'], 'readwrite')
107
+ await tx.objectStore('requests').add(log)
108
+ await tx.complete
109
+ return log
110
+ }
111
+
112
+ // Create a new thread
113
+ async function createThread(args = {}) {
114
+ await initDB()
115
+
116
+ const thread = {
117
+ id: generateThreadId(),
118
+ messages: [],
119
+ createdAt: new Date().toISOString(),
120
+ updatedAt: new Date().toISOString(),
121
+ ...args
122
+ }
123
+ if (!thread.title) {
124
+ thread.title = 'New Chat'
125
+ }
126
+
127
+ ctx.createThreadFilters.forEach(f => f(thread))
128
+
129
+ try {
130
+ const tx = db.transaction(['threads'], 'readwrite')
131
+ await tx.objectStore('threads').add(thread)
132
+ await tx.complete
133
+
134
+ threads.value.unshift(thread)
135
+ // Note: currentThread will be set by router navigation
136
+
137
+ } catch (e) {
138
+ console.error('Error creating thread', e, thread)
139
+ }
140
+
141
+ return thread
142
+ }
143
+
144
+ // Update thread
145
+ async function updateThread(threadId, updates) {
146
+ await initDB()
147
+
148
+ const tx = db.transaction(['threads'], 'readwrite')
149
+ const store = tx.objectStore('threads')
150
+
151
+ const thread = await store.get(threadId)
152
+ if (!thread) throw new Error('Thread not found')
153
+
154
+ const updatedThread = {
155
+ ...thread,
156
+ ...updates,
157
+ updatedAt: new Date().toISOString()
158
+ }
159
+
160
+ ctx.updateThreadFilters.forEach(f => f(updatedThread))
161
+
162
+ await store.put(updatedThread)
163
+ await tx.complete
164
+
165
+ // Update in memory
166
+ const index = threads.value.findIndex(t => t.id === threadId)
167
+ if (index !== -1) {
168
+ threads.value[index] = updatedThread
169
+ }
170
+
171
+ if (currentThread.value?.id === threadId) {
172
+ currentThread.value = updatedThread
173
+ }
174
+
175
+ return updatedThread
176
+ }
177
+
178
+ async function calculateThreadStats(threadId) {
179
+ await initDB()
180
+
181
+ const tx = db.transaction(['requests'], 'readonly')
182
+ const store = tx.objectStore('requests')
183
+ const index = store.index('threadId')
184
+
185
+ const requests = await index.getAll(threadId)
186
+
187
+ let inputTokens = 0
188
+ let outputTokens = 0
189
+ let cost = 0.0
190
+ let duration = 0
191
+
192
+ requests.forEach(req => {
193
+ inputTokens += req.inputTokens || 0
194
+ outputTokens += req.outputTokens || 0
195
+ cost += req.cost || 0.0
196
+ duration += req.duration || 0
197
+ })
198
+
199
+ return {
200
+ inputTokens,
201
+ outputTokens,
202
+ cost,
203
+ duration,
204
+ requests: requests.length
205
+ }
206
+ }
207
+
208
+ // Add message to thread
209
+ async function addMessageToThread(threadId, message, usage) {
210
+ const thread = await getThread(threadId)
211
+ if (!thread) throw new Error('Thread not found')
212
+
213
+ const newMessage = {
214
+ id: nextId(),
215
+ timestamp: new Date().toISOString(),
216
+ ...message
217
+ }
218
+
219
+ // Add input and output token usage to previous 'input' message
220
+ if (usage?.prompt_tokens != null) {
221
+ const lastMessage = thread.messages[thread.messages.length - 1]
222
+ if (lastMessage && lastMessage.role === 'user') {
223
+ lastMessage.usage = {
224
+ tokens: parseInt(usage.prompt_tokens),
225
+ price: usage.input || '0',
226
+ }
227
+ }
228
+ }
229
+ if (usage?.completion_tokens != null) {
230
+ newMessage.usage = {
231
+ tokens: parseInt(usage.completion_tokens),
232
+ price: usage.output || '0',
233
+ duration: usage.duration || undefined,
234
+ }
235
+ }
236
+
237
+ const updatedMessages = [...thread.messages, newMessage]
238
+
239
+ // Auto-generate title from first user message if still "New Chat"
240
+ let title = thread.title
241
+ if (title === 'New Chat' && message.role === 'user' && updatedMessages.length <= 2) {
242
+ let contentText = message.content
243
+ if (Array.isArray(contentText)) {
244
+ contentText = contentText.filter(c => c.type === 'text').map(c => c.text).join(' ')
245
+ }
246
+ title = contentText.slice(0, 200) + (contentText.length > 200 ? '...' : '')
247
+ }
248
+
249
+ const stats = await calculateThreadStats(threadId)
250
+
251
+ await updateThread(threadId, {
252
+ messages: updatedMessages,
253
+ title: title,
254
+ stats,
255
+ })
256
+
257
+ return newMessage
258
+ }
259
+
260
+ async function deleteMessageFromThread(threadId, messageId) {
261
+ const thread = await getThread(threadId)
262
+ if (!thread) throw new Error('Thread not found')
263
+ const updatedMessages = thread.messages.filter(m => m.id !== messageId)
264
+ await updateThread(threadId, { messages: updatedMessages })
265
+ }
266
+
267
+ async function updateMessageInThread(threadId, messageId, updates) {
268
+ const thread = await getThread(threadId)
269
+ if (!thread) throw new Error('Thread not found')
270
+
271
+ const messageIndex = thread.messages.findIndex(m => m.id === messageId)
272
+ if (messageIndex === -1) throw new Error('Message not found')
273
+
274
+ const updatedMessages = [...thread.messages]
275
+ updatedMessages[messageIndex] = {
276
+ ...updatedMessages[messageIndex],
277
+ ...updates
278
+ }
279
+
280
+ await updateThread(threadId, { messages: updatedMessages })
281
+ }
282
+
283
+ async function redoMessageFromThread(threadId, messageId) {
284
+ const thread = await getThread(threadId)
285
+ if (!thread) throw new Error('Thread not found')
286
+
287
+ // Find the index of the message to redo
288
+ const messageIndex = thread.messages.findIndex(m => m.id === messageId)
289
+ if (messageIndex === -1) throw new Error('Message not found')
290
+
291
+ // Keep only messages up to and including the target message
292
+ const updatedMessages = thread.messages.slice(0, messageIndex + 1)
293
+
294
+ // Update the thread with the new messages
295
+ await updateThread(threadId, { messages: updatedMessages })
296
+ }
297
+
298
+ // Get all threads
299
+ async function loadThreads() {
300
+ await initDB()
301
+ isLoading.value = true
302
+
303
+ try {
304
+ const tx = db.transaction(['threads'], 'readonly')
305
+ const store = tx.objectStore('threads')
306
+ const index = store.index('updatedAt')
307
+
308
+ const allThreads = await index.getAll()
309
+ threads.value = allThreads.reverse() // Most recent first
310
+
311
+ return threads.value
312
+ } finally {
313
+ isLoading.value = false
314
+ }
315
+ }
316
+
317
+ // Get single thread
318
+ async function getThread(threadId) {
319
+ await initDB()
320
+
321
+ const tx = db.transaction(['threads'], 'readonly')
322
+ const thread = await tx.objectStore('threads').get(threadId)
323
+
324
+ return thread
325
+ }
326
+
327
+ // Delete thread
328
+ async function deleteThread(threadId) {
329
+ await initDB()
330
+
331
+ const tx = db.transaction(['threads'], 'readwrite')
332
+ await tx.objectStore('threads').delete(threadId)
333
+ await tx.complete
334
+
335
+ threads.value = threads.value.filter(t => t.id !== threadId)
336
+
337
+ if (currentThread.value?.id === threadId) {
338
+ currentThread.value = null
339
+ }
340
+ }
341
+
342
+ // Set current thread
343
+ async function setCurrentThread(threadId) {
344
+ const thread = await getThread(threadId)
345
+ if (thread) {
346
+ currentThread.value = thread
347
+ }
348
+ return thread
349
+ }
350
+
351
+ // Set current thread from router params (router-aware version)
352
+ async function setCurrentThreadFromRoute(threadId, router) {
353
+ if (!threadId) {
354
+ currentThread.value = null
355
+ return null
356
+ }
357
+
358
+ const thread = await getThread(threadId)
359
+ if (thread) {
360
+ currentThread.value = thread
361
+ return thread
362
+ } else {
363
+ // Thread not found, redirect to home
364
+ if (router) {
365
+ router.push((globalThis.ai?.base || '') + '/')
366
+ }
367
+ currentThread.value = null
368
+ return null
369
+ }
370
+ }
371
+
372
+ // Clear current thread (go back to initial state)
373
+ function clearCurrentThread() {
374
+ currentThread.value = null
375
+ }
376
+
377
+ function getGroupedThreads(total) {
378
+ const now = new Date()
379
+ const today = new Date(now.getFullYear(), now.getMonth(), now.getDate())
380
+ const lastWeek = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000)
381
+ const lastMonth = new Date(today.getTime() - 30 * 24 * 60 * 60 * 1000)
382
+
383
+ const groups = {
384
+ today: [],
385
+ lastWeek: [],
386
+ lastMonth: [],
387
+ older: {}
388
+ }
389
+
390
+ const takeThreads = threads.value.slice(0, total)
391
+
392
+ takeThreads.forEach(thread => {
393
+ const threadDate = new Date(thread.updatedAt)
394
+
395
+ if (threadDate >= today) {
396
+ groups.today.push(thread)
397
+ } else if (threadDate >= lastWeek) {
398
+ groups.lastWeek.push(thread)
399
+ } else if (threadDate >= lastMonth) {
400
+ groups.lastMonth.push(thread)
401
+ } else {
402
+ const year = threadDate.getFullYear()
403
+ const month = threadDate.toLocaleString('default', { month: 'long' })
404
+ const key = `${month} ${year}`
405
+
406
+ if (!groups.older[key]) {
407
+ groups.older[key] = []
408
+ }
409
+ groups.older[key].push(thread)
410
+ }
411
+ })
412
+
413
+ return groups
414
+ }
415
+
416
+ // Group threads by time periods
417
+ const groupedThreads = computed(() => getGroupedThreads(threads.value.length))
418
+
419
+ async function getAllRequests() {
420
+ await initDB()
421
+
422
+ const tx = db.transaction(['requests'], 'readonly')
423
+ const store = tx.objectStore('requests')
424
+ const allRequests = await store.getAll()
425
+ return allRequests
426
+ }
427
+
428
+ async function getRequest(requestId) {
429
+ await initDB()
430
+
431
+ const tx = db.transaction(['requests'], 'readonly')
432
+ const store = tx.objectStore('requests')
433
+ const request = await store.get(requestId)
434
+ return request
435
+ }
436
+
437
+ async function getAllRequestIds() {
438
+ await initDB()
439
+
440
+ const tx = db.transaction(['requests'], 'readonly')
441
+ const store = tx.objectStore('requests')
442
+ const ids = await store.getAllKeys()
443
+ return ids
444
+ }
445
+
446
+ async function getAllThreadIds() {
447
+ await initDB()
448
+ const tx = db.transaction(['threads'], 'readonly')
449
+ const store = tx.objectStore('threads')
450
+ const ids = await store.getAllKeys()
451
+ return ids
452
+ }
453
+
454
+ // Query requests with pagination and filtering
455
+ async function getRequests(filters = {}, limit = 20, offset = 0) {
456
+ try {
457
+ await initDB()
458
+
459
+ const {
460
+ model = null,
461
+ provider = null,
462
+ threadId = null,
463
+ sortBy = 'created',
464
+ sortOrder = 'desc',
465
+ startDate = null,
466
+ endDate = null
467
+ } = filters
468
+
469
+ const tx = db.transaction(['requests'], 'readonly')
470
+ const store = tx.objectStore('requests')
471
+
472
+ // Get all requests and filter in memory (IndexedDB limitations)
473
+ const allRequests = await store.getAll()
474
+
475
+ // Apply filters
476
+ let results = allRequests.filter(req => {
477
+ if (model && req.model !== model) return false
478
+ if (provider && req.provider !== provider) return false
479
+ if (threadId && req.threadId !== threadId) return false
480
+ if (startDate && req.created < startDate) return false
481
+ if (endDate && req.created > endDate) return false
482
+ return true
483
+ })
484
+
485
+ // Sort
486
+ results.sort((a, b) => {
487
+ let aVal = a[sortBy]
488
+ let bVal = b[sortBy]
489
+
490
+ if (sortOrder === 'desc') {
491
+ return bVal - aVal
492
+ } else {
493
+ return aVal - bVal
494
+ }
495
+ })
496
+
497
+ // Paginate
498
+ const total = results.length
499
+ const paginatedResults = results.slice(offset, offset + limit)
500
+
501
+ return {
502
+ requests: paginatedResults,
503
+ total,
504
+ hasMore: offset + limit < total
505
+ }
506
+ } catch (error) {
507
+ console.error('Error in getRequests:', error)
508
+ return {
509
+ requests: [],
510
+ total: 0,
511
+ hasMore: false
512
+ }
513
+ }
514
+ }
515
+
516
+ // Get unique values for filter options
517
+ async function getFilterOptions() {
518
+ try {
519
+ await initDB()
520
+
521
+ const tx = db.transaction(['requests'], 'readonly')
522
+ const store = tx.objectStore('requests')
523
+ const allRequests = await store.getAll()
524
+
525
+ const models = [...new Set(allRequests.map(r => r.model).filter(m => m))].sort()
526
+ const providers = [...new Set(allRequests.map(r => r.provider).filter(p => p))].sort()
527
+
528
+ return {
529
+ models,
530
+ providers
531
+ }
532
+ } catch (error) {
533
+ console.error('Error in getFilterOptions:', error)
534
+ return {
535
+ models: [],
536
+ providers: []
537
+ }
538
+ }
539
+ }
540
+
541
+ // Delete a request by ID
542
+ async function deleteRequest(requestId) {
543
+ await initDB()
544
+
545
+ const tx = db.transaction(['requests'], 'readwrite')
546
+ await tx.objectStore('requests').delete(requestId)
547
+ await tx.complete
548
+ }
549
+
550
+ // Export the store
551
+ export function useThreadStore() {
552
+ return {
553
+ // State
554
+ threads,
555
+ currentThread,
556
+ isLoading,
557
+ groupedThreads,
558
+
559
+ // Actions
560
+ initDB,
561
+ logRequest,
562
+ createThread,
563
+ updateThread,
564
+ addMessageToThread,
565
+ deleteMessageFromThread,
566
+ updateMessageInThread,
567
+ redoMessageFromThread,
568
+ loadThreads,
569
+ getThread,
570
+ deleteThread,
571
+ setCurrentThread,
572
+ setCurrentThreadFromRoute,
573
+ clearCurrentThread,
574
+ getGroupedThreads,
575
+ getRequest,
576
+ getRequests,
577
+ getAllRequests,
578
+ getFilterOptions,
579
+ deleteRequest,
580
+ getAllRequestIds,
581
+ getAllThreadIds,
582
+ }
583
+ }