llms-py 2.0.18__py3-none-any.whl → 2.0.33__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 (41) hide show
  1. llms/index.html +17 -1
  2. llms/llms.json +1132 -1075
  3. llms/main.py +561 -103
  4. llms/ui/Analytics.mjs +115 -104
  5. llms/ui/App.mjs +81 -4
  6. llms/ui/Avatar.mjs +61 -4
  7. llms/ui/Brand.mjs +29 -11
  8. llms/ui/ChatPrompt.mjs +163 -16
  9. llms/ui/Main.mjs +177 -94
  10. llms/ui/ModelSelector.mjs +28 -10
  11. llms/ui/OAuthSignIn.mjs +92 -0
  12. llms/ui/ProviderStatus.mjs +12 -12
  13. llms/ui/Recents.mjs +13 -13
  14. llms/ui/SettingsDialog.mjs +65 -65
  15. llms/ui/Sidebar.mjs +24 -19
  16. llms/ui/SystemPromptEditor.mjs +5 -5
  17. llms/ui/SystemPromptSelector.mjs +26 -6
  18. llms/ui/Welcome.mjs +2 -2
  19. llms/ui/ai.mjs +69 -5
  20. llms/ui/app.css +548 -34
  21. llms/ui/lib/servicestack-vue.mjs +9 -9
  22. llms/ui/markdown.mjs +8 -8
  23. llms/ui/tailwind.input.css +2 -0
  24. llms/ui/threadStore.mjs +39 -0
  25. llms/ui/typography.css +54 -36
  26. {llms_py-2.0.18.dist-info → llms_py-2.0.33.dist-info}/METADATA +403 -47
  27. llms_py-2.0.33.dist-info/RECORD +48 -0
  28. {llms_py-2.0.18.dist-info → llms_py-2.0.33.dist-info}/licenses/LICENSE +1 -2
  29. llms/__pycache__/__init__.cpython-312.pyc +0 -0
  30. llms/__pycache__/__init__.cpython-313.pyc +0 -0
  31. llms/__pycache__/__init__.cpython-314.pyc +0 -0
  32. llms/__pycache__/__main__.cpython-312.pyc +0 -0
  33. llms/__pycache__/__main__.cpython-314.pyc +0 -0
  34. llms/__pycache__/llms.cpython-312.pyc +0 -0
  35. llms/__pycache__/main.cpython-312.pyc +0 -0
  36. llms/__pycache__/main.cpython-313.pyc +0 -0
  37. llms/__pycache__/main.cpython-314.pyc +0 -0
  38. llms_py-2.0.18.dist-info/RECORD +0 -56
  39. {llms_py-2.0.18.dist-info → llms_py-2.0.33.dist-info}/WHEEL +0 -0
  40. {llms_py-2.0.18.dist-info → llms_py-2.0.33.dist-info}/entry_points.txt +0 -0
  41. {llms_py-2.0.18.dist-info → llms_py-2.0.33.dist-info}/top_level.txt +0 -0
llms/ui/Sidebar.mjs CHANGED
@@ -12,22 +12,22 @@ const ThreadItem = {
12
12
  template: `
13
13
  <div
14
14
  class="group relative mx-2 mb-1 rounded-md cursor-pointer transition-colors border border-transparent"
15
- :class="isActive ? 'bg-blue-100 border-blue-200' : 'hover:bg-gray-100'"
15
+ :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'"
16
16
  @click="$emit('select', thread.id)"
17
17
  >
18
18
  <div class="flex items-center px-3 py-2">
19
19
  <div class="flex-1 min-w-0">
20
- <div class="text-sm font-medium text-gray-900 truncate" :title="thread.title">
20
+ <div class="text-sm font-medium text-gray-900 dark:text-gray-100 truncate" :title="thread.title">
21
21
  {{ thread.title }}
22
22
  </div>
23
- <div class="text-xs text-gray-500 truncate">
23
+ <div class="text-xs text-gray-500 dark:text-gray-400 truncate">
24
24
  <span>{{ formatRelativeTime(thread.updatedAt) }} • {{ thread.messages.length }} msgs</span>
25
25
  <span v-if="thread.stats?.inputTokens" :title="statsTitle(thread.stats)">
26
26
  &#8226; {{ humanifyNumber(thread.stats.inputTokens + thread.stats.outputTokens) }} toks
27
27
  {{ thread.stats.cost ? ' ' + formatCost(thread.stats.cost) : '' }}
28
28
  </span>
29
29
  </div>
30
- <div v-if="thread.model" class="text-xs text-blue-600 truncate">
30
+ <div v-if="thread.model" class="text-xs text-blue-600 dark:text-blue-400 truncate">
31
31
  {{ thread.model }}
32
32
  </div>
33
33
  </div>
@@ -35,7 +35,7 @@ const ThreadItem = {
35
35
  <!-- Delete button (shown on hover) -->
36
36
  <button type="button"
37
37
  @click.stop="$emit('delete', thread.id)"
38
- 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"
38
+ 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"
39
39
  title="Delete conversation"
40
40
  >
41
41
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -89,7 +89,7 @@ const GroupedThreads = {
89
89
  template: `
90
90
  <!-- Today -->
91
91
  <div v-if="groupedThreads.today.length > 0" class="mb-4">
92
- <h3 class="px-4 py-2 text-xs font-semibold text-gray-500 uppercase tracking-wider select-none">Today</h3>
92
+ <h3 class="px-4 py-2 text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider select-none">Today</h3>
93
93
  <ThreadItem
94
94
  v-for="thread in groupedThreads.today"
95
95
  :key="thread.id"
@@ -102,7 +102,7 @@ const GroupedThreads = {
102
102
 
103
103
  <!-- Last 7 Days -->
104
104
  <div v-if="groupedThreads.lastWeek.length > 0" class="mb-4">
105
- <h3 class="px-4 py-2 text-xs font-semibold text-gray-500 uppercase tracking-wider select-none">Last 7 Days</h3>
105
+ <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>
106
106
  <ThreadItem
107
107
  v-for="thread in groupedThreads.lastWeek"
108
108
  :key="thread.id"
@@ -115,7 +115,7 @@ const GroupedThreads = {
115
115
 
116
116
  <!-- Last 30 Days -->
117
117
  <div v-if="groupedThreads.lastMonth.length > 0" class="mb-4">
118
- <h3 class="px-4 py-2 text-xs font-semibold text-gray-500 uppercase tracking-wider select-none">Last 30 Days</h3>
118
+ <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>
119
119
  <ThreadItem
120
120
  v-for="thread in groupedThreads.lastMonth"
121
121
  :key="thread.id"
@@ -128,7 +128,7 @@ const GroupedThreads = {
128
128
 
129
129
  <!-- Older (grouped by month/year) -->
130
130
  <div v-for="(monthThreads, monthKey) in groupedThreads.older" :key="monthKey" class="mb-4">
131
- <h3 class="px-4 py-2 text-xs font-semibold text-gray-500 uppercase tracking-wider select-none">{{ monthKey }}</h3>
131
+ <h3 class="px-4 py-2 text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider select-none">{{ monthKey }}</h3>
132
132
  <ThreadItem
133
133
  v-for="thread in monthThreads"
134
134
  :key="thread.id"
@@ -140,7 +140,7 @@ const GroupedThreads = {
140
140
  </div>
141
141
  <div class="mb-4 flex w-full justify-center">
142
142
  <button @click="$router.push($ai.base + '/recents')" type="button"
143
- class="flex text-sm space-x-1 font-semibold text-gray-900 hover:text-blue-600 focus:outline-none transition-colors">
143
+ 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">
144
144
  <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>
145
145
  <span>All Chats</span>
146
146
  </button>
@@ -163,31 +163,32 @@ const Sidebar = {
163
163
  ThreadItem,
164
164
  },
165
165
  template: `
166
- <div class="flex flex-col h-full bg-gray-50 border-r border-gray-200">
167
- <Brand @home="goToInitialState" @new="createNewThread" @analytics="goToAnalytics" />
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')" />
168
168
  <!-- Thread List -->
169
169
  <div class="flex-1 overflow-y-auto">
170
- <div v-if="isLoading" class="p-4 text-center text-gray-500">
171
- <div class="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600 mx-auto"></div>
170
+ <div v-if="isLoading" class="p-4 text-center text-gray-500 dark:text-gray-400">
171
+ <div class="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600 dark:border-blue-400 mx-auto"></div>
172
172
  <p class="mt-2 text-sm">Loading threads...</p>
173
173
  </div>
174
174
 
175
- <div v-else-if="threads.length === 0" class="p-4 text-center text-gray-500">
175
+ <div v-else-if="threads.length === 0" class="p-4 text-center text-gray-500 dark:text-gray-400">
176
176
  <div class="mb-2 flex justify-center">
177
177
  <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>
178
178
  </div>
179
179
  <p class="text-sm">No conversations yet</p>
180
- <p class="text-xs text-gray-400 mt-1">Start a new chat to begin</p>
180
+ <p class="text-xs text-gray-400 dark:text-gray-500 mt-1">Start a new chat to begin</p>
181
181
  </div>
182
182
 
183
183
  <div v-else class="py-2">
184
- <GroupedThreads :currentThread="currentThread" :groupedThreads="threadStore.getGroupedThreads(18)"
185
- @select="selectThread" @delete="deleteThread" />
184
+ <GroupedThreads :currentThread="currentThread" :groupedThreads="threadStore.getGroupedThreads(18)"
185
+ @select="selectThread" @delete="deleteThread" />
186
186
  </div>
187
187
  </div>
188
188
  </div>
189
189
  `,
190
- setup() {
190
+ emits: ['thread-selected', 'toggle-sidebar'],
191
+ setup(props, { emit }) {
191
192
  const ai = inject('ai')
192
193
  const router = useRouter()
193
194
  const threadStore = useThreadStore()
@@ -208,6 +209,7 @@ const Sidebar = {
208
209
 
209
210
  const selectThread = async (threadId) => {
210
211
  router.push(`${ai.base}/c/${threadId}`)
212
+ emit('thread-selected')
211
213
  }
212
214
 
213
215
  const deleteThread = async (threadId) => {
@@ -223,15 +225,18 @@ const Sidebar = {
223
225
  const createNewThread = async () => {
224
226
  const newThread = await createThread()
225
227
  router.push(`${ai.base}/c/${newThread.id}`)
228
+ emit('thread-selected')
226
229
  }
227
230
 
228
231
  const goToInitialState = () => {
229
232
  clearCurrentThread()
230
233
  router.push(`${ai.base}/`)
234
+ emit('thread-selected')
231
235
  }
232
236
 
233
237
  const goToAnalytics = () => {
234
238
  router.push(`${ai.base}/analytics`)
239
+ emit('thread-selected')
235
240
  }
236
241
 
237
242
  return {
@@ -1,10 +1,10 @@
1
1
  export default {
2
2
  template:`
3
- <div class="border-b border-gray-200 bg-gray-50 px-6 py-4">
3
+ <div class="border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 px-6 py-4">
4
4
  <div class="max-w-6xl mx-auto">
5
- <label class="block text-sm font-medium text-gray-700 mb-2">
5
+ <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
6
6
  System Prompt
7
- <span v-if="selected" class="text-gray-500 font-normal">
7
+ <span v-if="selected" class="text-gray-500 dark:text-gray-400 font-normal">
8
8
  ({{ prompts.find(p => p.id === selected.id)?.name || 'Custom' }})
9
9
  </span>
10
10
  </label>
@@ -12,9 +12,9 @@ export default {
12
12
  :value="modelValue" @input="$emit('update:modelValue', $event.target.value)"
13
13
  placeholder="Enter a system prompt to guide AI's behavior..."
14
14
  rows="6"
15
- class="block w-full resize-vertical 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"
15
+ class="block w-full resize-vertical 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 focus:outline-none focus:ring-1 focus:ring-blue-500"
16
16
  ></textarea>
17
- <div class="mt-2 text-xs text-gray-500">
17
+ <div class="mt-2 text-xs text-gray-500 dark:text-gray-400">
18
18
  You can modify this system prompt before sending messages. Changes will only apply to new conversations.
19
19
  </div>
20
20
  </div>
@@ -1,12 +1,13 @@
1
+ import { ref, onMounted, onUnmounted } from "vue"
1
2
  export default {
2
3
  template:`
3
4
  <button v-if="modelValue" type="button" title="Clear System Prompt" @click="$emit('update:modelValue', null)">
4
- <svg class="size-4 text-gray-500" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12z"/></svg>
5
+ <svg class="size-4 text-gray-500 dark:text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12z"/></svg>
5
6
  </button>
6
-
7
- <Autocomplete id="prompt" :options="prompts" label=""
7
+
8
+ <Autocomplete ref="refSelector" id="prompt" :options="prompts" label=""
8
9
  :modelValue="modelValue" @update:modelValue="$emit('update:modelValue', $event)"
9
- class="w-72 xl:w-84"
10
+ class="w-68 xl:w-84"
10
11
  :match="(x, value) => x.name.toLowerCase().includes(value.toLowerCase())"
11
12
  placeholder="Select a System Prompt...">
12
13
  <template #item="{ value }">
@@ -17,8 +18,8 @@ export default {
17
18
  <!-- Toggle System Prompt Visibility -->
18
19
  <button type="button"
19
20
  @click="$emit('toggle')"
20
- :class="show ? 'text-blue-700' : 'text-gray-600'"
21
- class="p-1 rounded-md hover:bg-blue-100 hover:text-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
21
+ :class="show ? 'text-blue-700 dark:text-blue-400' : 'text-gray-600 dark:text-gray-400'"
22
+ class="p-1 rounded-md hover:bg-blue-100 dark:hover:bg-blue-900/30 hover:text-blue-700 dark:hover:text-blue-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
22
23
  :title="show ? 'Hide system prompt' : 'Show system prompt'"
23
24
  >
24
25
  <svg v-if="!show" class="size-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path fill="currentColor" d="M33.62 17.53c-3.37-6.23-9.28-10-15.82-10S5.34 11.3 2 17.53l-.28.47l.26.48c3.37 6.23 9.28 10 15.82 10s12.46-3.72 15.82-10l.26-.48Zm-15.82 8.9C12.17 26.43 7 23.29 4 18c3-5.29 8.17-8.43 13.8-8.43S28.54 12.72 31.59 18c-3.05 5.29-8.17 8.43-13.79 8.43"/><path fill="currentColor" d="M18.09 11.17A6.86 6.86 0 1 0 25 18a6.86 6.86 0 0 0-6.91-6.83m0 11.72A4.86 4.86 0 1 1 23 18a4.87 4.87 0 0 1-4.91 4.89"/><path fill="none" d="M0 0h36v36H0z"/></svg>
@@ -32,5 +33,24 @@ export default {
32
33
  show: Boolean,
33
34
  },
34
35
  setup() {
36
+ const refSelector = ref()
37
+
38
+ function collapse(e) {
39
+ // call toggle when clicking outside of the Autocomplete component
40
+ if (refSelector.value && !refSelector.value.$el.contains(e.target)) {
41
+ refSelector.value.toggle(false)
42
+ }
43
+ }
44
+
45
+ onMounted(() => {
46
+ document.addEventListener('click', collapse)
47
+ })
48
+ onUnmounted(() => {
49
+ document.removeEventListener('click', collapse)
50
+ })
51
+
52
+ return {
53
+ refSelector,
54
+ }
35
55
  }
36
56
  }
llms/ui/Welcome.mjs CHANGED
@@ -1,8 +1,8 @@
1
1
  export default {
2
2
  template: `
3
3
  <div class="mb-2 flex justify-center">
4
- <svg class="size-20 text-gray-700" 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>
4
+ <svg class="size-20 text-gray-700 dark:text-gray-300" 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>
5
5
  </div>
6
- <h2 class="text-2xl font-semibold text-gray-900 mb-2">{{ $ai.welcome }}</h2>
6
+ <h2 class="text-2xl font-semibold text-gray-900 dark:text-gray-100 mb-2">{{ $ai.welcome }}</h2>
7
7
  `
8
8
  }
llms/ui/ai.mjs CHANGED
@@ -6,13 +6,15 @@ const headers = { 'Accept': 'application/json' }
6
6
  const prefsKey = 'llms.prefs'
7
7
 
8
8
  export const o = {
9
- version: '2.0.18',
9
+ version: '2.0.33',
10
10
  base,
11
11
  prefsKey,
12
12
  welcome: 'Welcome to llms.py',
13
13
  auth: null,
14
14
  requiresAuth: false,
15
+ authType: 'apikey', // 'oauth' or 'apikey' - controls which SignIn component to use
15
16
  headers,
17
+ isSidebarOpen: true, // Shared sidebar state (default open for lg+ screens)
16
18
 
17
19
  resolveUrl(url){
18
20
  return url.startsWith('http') || url.startsWith('/v1') ? url : base + url
@@ -50,26 +52,88 @@ export const o = {
50
52
  this.auth = auth
51
53
  if (auth?.apiKey) {
52
54
  this.headers.Authorization = `Bearer ${auth.apiKey}`
53
- } else if (this.headers.Authorization) {
55
+ //localStorage.setItem('llms:auth', JSON.stringify({ apiKey: auth.apiKey }))
56
+ } else if (auth?.sessionToken) {
57
+ this.headers['X-Session-Token'] = auth.sessionToken
58
+ localStorage.setItem('llms:auth', JSON.stringify({ sessionToken: auth.sessionToken }))
59
+ } else {
60
+ if (this.headers.Authorization) {
61
+ delete this.headers.Authorization
62
+ }
63
+ if (this.headers['X-Session-Token']) {
64
+ delete this.headers['X-Session-Token']
65
+ }
66
+ }
67
+ },
68
+ async signOut() {
69
+ if (this.auth?.sessionToken) {
70
+ // Call logout endpoint for OAuth sessions
71
+ try {
72
+ await this.post('/auth/logout', {
73
+ headers: {
74
+ 'X-Session-Token': this.auth.sessionToken
75
+ }
76
+ })
77
+ } catch (error) {
78
+ console.error('Logout error:', error)
79
+ }
80
+ }
81
+ this.auth = null
82
+ if (this.headers.Authorization) {
54
83
  delete this.headers.Authorization
55
84
  }
85
+ if (this.headers['X-Session-Token']) {
86
+ delete this.headers['X-Session-Token']
87
+ }
88
+ localStorage.removeItem('llms:auth')
56
89
  },
57
90
  async init() {
58
91
  // Load models and prompts
59
92
  const { initDB } = useThreadStore()
60
- const [_, configRes, modelsRes, authRes] = await Promise.all([
93
+ const [_, configRes, modelsRes] = await Promise.all([
61
94
  initDB(),
62
95
  this.getConfig(),
63
96
  this.getModels(),
64
- this.getAuth(),
65
97
  ])
66
98
  const config = await configRes.json()
67
99
  const models = await modelsRes.json()
68
- const auth = this.requiresAuth
100
+
101
+ // Update auth settings from server config
102
+ if (config.requiresAuth != null) {
103
+ this.requiresAuth = config.requiresAuth
104
+ }
105
+ if (config.authType != null) {
106
+ this.authType = config.authType
107
+ }
108
+
109
+ // Try to restore session from localStorage
110
+ if (this.requiresAuth) {
111
+ const storedAuth = localStorage.getItem('llms:auth')
112
+ if (storedAuth) {
113
+ try {
114
+ const authData = JSON.parse(storedAuth)
115
+ if (authData.sessionToken) {
116
+ this.headers['X-Session-Token'] = authData.sessionToken
117
+ }
118
+ // else if (authData.apiKey) {
119
+ // this.headers.Authorization = `Bearer ${authData.apiKey}`
120
+ // }
121
+ } catch (e) {
122
+ console.error('Failed to restore auth from localStorage:', e)
123
+ localStorage.removeItem('llms:auth')
124
+ }
125
+ }
126
+ }
127
+
128
+ // Get auth status
129
+ const authRes = await this.getAuth()
130
+ const auth = this.requiresAuth
69
131
  ? await authRes.json()
70
132
  : null
71
133
  if (auth?.responseStatus?.errorCode) {
72
134
  console.error(auth.responseStatus.errorCode, auth.responseStatus.message)
135
+ // Clear invalid session from localStorage
136
+ localStorage.removeItem('llms:auth')
73
137
  } else {
74
138
  this.signIn(auth)
75
139
  }