llms-py 3.0.0b1__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 (39) hide show
  1. llms/__pycache__/__init__.cpython-312.pyc +0 -0
  2. llms/__pycache__/__init__.cpython-313.pyc +0 -0
  3. llms/__pycache__/__init__.cpython-314.pyc +0 -0
  4. llms/__pycache__/__main__.cpython-312.pyc +0 -0
  5. llms/__pycache__/__main__.cpython-314.pyc +0 -0
  6. llms/__pycache__/llms.cpython-312.pyc +0 -0
  7. llms/__pycache__/main.cpython-312.pyc +0 -0
  8. llms/__pycache__/main.cpython-313.pyc +0 -0
  9. llms/__pycache__/main.cpython-314.pyc +0 -0
  10. llms/__pycache__/plugins.cpython-314.pyc +0 -0
  11. llms/index.html +25 -56
  12. llms/llms.json +2 -2
  13. llms/main.py +452 -93
  14. llms/providers.json +1 -1
  15. llms/ui/App.mjs +25 -4
  16. llms/ui/Avatar.mjs +3 -2
  17. llms/ui/ChatPrompt.mjs +43 -52
  18. llms/ui/Main.mjs +87 -98
  19. llms/ui/OAuthSignIn.mjs +2 -33
  20. llms/ui/ProviderStatus.mjs +7 -8
  21. llms/ui/Recents.mjs +10 -9
  22. llms/ui/Sidebar.mjs +2 -1
  23. llms/ui/SignIn.mjs +7 -6
  24. llms/ui/ai.mjs +9 -41
  25. llms/ui/app.css +137 -138
  26. llms/ui/index.mjs +213 -0
  27. llms/ui/{ModelSelector.mjs → model-selector.mjs} +193 -200
  28. llms/ui/tailwind.input.css +441 -79
  29. llms/ui/threadStore.mjs +17 -6
  30. llms/ui/utils.mjs +1 -0
  31. {llms_py-3.0.0b1.dist-info → llms_py-3.0.0b2.dist-info}/METADATA +1 -1
  32. llms_py-3.0.0b2.dist-info/RECORD +58 -0
  33. llms/ui/SystemPromptEditor.mjs +0 -31
  34. llms/ui/SystemPromptSelector.mjs +0 -56
  35. llms_py-3.0.0b1.dist-info/RECORD +0 -49
  36. {llms_py-3.0.0b1.dist-info → llms_py-3.0.0b2.dist-info}/WHEEL +0 -0
  37. {llms_py-3.0.0b1.dist-info → llms_py-3.0.0b2.dist-info}/entry_points.txt +0 -0
  38. {llms_py-3.0.0b1.dist-info → llms_py-3.0.0b2.dist-info}/licenses/LICENSE +0 -0
  39. {llms_py-3.0.0b1.dist-info → llms_py-3.0.0b2.dist-info}/top_level.txt +0 -0
llms/ui/App.mjs CHANGED
@@ -1,13 +1,20 @@
1
- import { inject, ref, onMounted, onUnmounted } from "vue"
1
+ import { inject, ref, watch, onMounted, onUnmounted } from "vue"
2
+ import { useRouter, useRoute } from "vue-router"
2
3
  import Sidebar from "./Sidebar.mjs"
3
4
 
4
5
  export default {
5
6
  components: {
6
7
  Sidebar,
7
8
  },
8
- setup() {
9
- const ai = inject('ai')
9
+ props: ['config', 'models'],
10
+ setup(props) {
11
+ const router = useRouter()
12
+ const route = useRoute()
13
+
14
+ const ctx = inject('ctx')
15
+ const ai = ctx.ai
10
16
  const isMobile = ref(false)
17
+ const modal = ref()
11
18
 
12
19
  const checkMobile = () => {
13
20
  const wasMobile = isMobile.value
@@ -36,13 +43,25 @@ export default {
36
43
  onMounted(() => {
37
44
  checkMobile()
38
45
  window.addEventListener('resize', checkMobile)
46
+ if (route.query.open) {
47
+ modal.value = ctx.openModal(route.query.open)
48
+ }
39
49
  })
40
50
 
41
51
  onUnmounted(() => {
42
52
  window.removeEventListener('resize', checkMobile)
43
53
  })
44
54
 
45
- return { ai, isMobile, toggleSidebar, closeSidebar }
55
+ function closeModal() {
56
+ ctx.closeModal(route.query.open)
57
+ }
58
+
59
+ watch(() => route.query.open, (newVal) => {
60
+ modal.value = ctx.modalComponents[newVal]
61
+ console.log('open', newVal)
62
+ })
63
+
64
+ return { ai, modal, isMobile, toggleSidebar, closeSidebar, closeModal }
46
65
  },
47
66
  template: `
48
67
  <div class="flex h-screen bg-white dark:bg-gray-900">
@@ -92,6 +111,8 @@ export default {
92
111
 
93
112
  <RouterView />
94
113
  </div>
114
+
115
+ <component v-if="modal" :is="modal" @done="closeModal" />
95
116
  </div>
96
117
  `,
97
118
  }
llms/ui/Avatar.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { computed, inject, ref, onMounted, onUnmounted } from "vue"
2
2
 
3
3
  export default {
4
- template:`
4
+ template: `
5
5
  <div v-if="$ai.auth?.profileUrl" class="relative" ref="avatarContainer">
6
6
  <img
7
7
  @click.stop="toggleMenu"
@@ -31,7 +31,8 @@ export default {
31
31
  </div>
32
32
  `,
33
33
  setup() {
34
- const ai = inject('ai')
34
+ const ctx = inject('ctx')
35
+ const ai = ctx.ai
35
36
  const showMenu = ref(false)
36
37
  const avatarContainer = ref(null)
37
38
 
llms/ui/ChatPrompt.mjs CHANGED
@@ -146,17 +146,14 @@ export default {
146
146
  model: {
147
147
  type: Object,
148
148
  default: null
149
- },
150
- systemPrompt: {
151
- type: String,
152
- default: ''
153
149
  }
154
150
  },
155
151
  setup(props) {
156
- const ai = inject('ai')
152
+ const ctx = inject('ctx')
153
+ const config = ctx.state.config
154
+ const ai = ctx.ai
157
155
  const chatSettings = inject('chatSettings')
158
156
  const router = useRouter()
159
- const config = inject('config')
160
157
  const chatPrompt = inject('chatPrompt')
161
158
  const {
162
159
  messageText,
@@ -325,20 +322,6 @@ export default {
325
322
  }
326
323
  }
327
324
 
328
- function createChatRequest() {
329
- if (hasImage()) {
330
- return deepClone(config.defaults.image)
331
- }
332
- if (hasAudio()) {
333
- return deepClone(config.defaults.audio)
334
- }
335
- if (attachedFiles.value.length) {
336
- return deepClone(config.defaults.file)
337
- }
338
- const text = deepClone(config.defaults.text)
339
- return text
340
- }
341
-
342
325
  function getTextContent(chat) {
343
326
  const textMessage = chat.messages.find(m =>
344
327
  m.role === 'user' && Array.isArray(m.content) && m.content.some(c => c.type === 'text'))
@@ -383,17 +366,20 @@ export default {
383
366
 
384
367
  // Create thread if none exists
385
368
  if (!currentThread.value) {
386
- const newThread = await threads.createThread('New Chat', props.model, props.systemPrompt)
369
+ const newThread = await threads.createThread({
370
+ title: 'New Chat',
371
+ model: props.model.id,
372
+ info: toModelInfo(props.model),
373
+ })
387
374
  threadId = newThread.id
388
375
  // Navigate to the new thread URL
389
376
  router.push(`${ai.base}/c/${newThread.id}`)
390
377
  } else {
391
378
  threadId = currentThread.value.id
392
- // Update the existing thread's model and systemPrompt to match current selection
379
+ // Update the existing thread's model to match current selection
393
380
  await threads.updateThread(threadId, {
394
381
  model: props.model.name,
395
382
  info: toModelInfo(props.model),
396
- systemPrompt: props.systemPrompt
397
383
  })
398
384
  }
399
385
 
@@ -448,51 +434,49 @@ export default {
448
434
  isGenerating.value = true
449
435
 
450
436
  // Construct API Request from History
451
- const chatRequest = {
437
+ const request = {
452
438
  model: props.model.name,
453
439
  messages: [],
454
440
  metadata: {}
455
441
  }
456
442
 
457
- // Add system prompt if present
458
- if (props.systemPrompt?.trim()) {
459
- chatRequest.messages.push({
460
- role: 'system',
461
- content: props.systemPrompt // assuming system prompt is just string
462
- })
463
- }
464
-
465
443
  // Add History
466
444
  thread.messages.forEach(m => {
467
- chatRequest.messages.push({
445
+ request.messages.push({
468
446
  role: m.role,
469
447
  content: m.content
470
448
  })
471
449
  })
472
450
 
473
451
  // Apply user settings
474
- applySettings(chatRequest)
475
- chatRequest.metadata.threadId = threadId
452
+ applySettings(request)
453
+ request.metadata.threadId = threadId
476
454
 
477
- console.debug('chatRequest', chatRequest)
455
+ const ctxRequest = {
456
+ request,
457
+ thread,
458
+ }
459
+ ctx.chatRequestFilters.forEach(f => f(ctxRequest))
460
+
461
+ console.debug('chatRequest', request)
478
462
 
479
463
  // Send to API
480
464
  const startTime = Date.now()
481
- const response = await ai.post('/v1/chat/completions', {
482
- body: JSON.stringify(chatRequest),
465
+ const res = await ai.post('/v1/chat/completions', {
466
+ body: JSON.stringify(request),
483
467
  signal: controller.signal
484
468
  })
485
469
 
486
- let result = null
487
- if (!response.ok) {
470
+ let response = null
471
+ if (!res.ok) {
488
472
  errorStatus.value = {
489
- errorCode: `HTTP ${response.status} ${response.statusText}`,
473
+ errorCode: `HTTP ${res.status} ${res.statusText}`,
490
474
  message: null,
491
475
  stackTrace: null
492
476
  }
493
477
  let errorBody = null
494
478
  try {
495
- errorBody = await response.text()
479
+ errorBody = await res.text()
496
480
  if (errorBody) {
497
481
  // Try to parse as JSON for better formatting
498
482
  try {
@@ -513,8 +497,13 @@ export default {
513
497
  }
514
498
  } else {
515
499
  try {
516
- result = await response.json()
517
- console.debug('chatResponse', JSON.stringify(result, null, 2))
500
+ response = await res.json()
501
+ const ctxResponse = {
502
+ response,
503
+ thread,
504
+ }
505
+ ctx.chatResponseFilters.forEach(f => f(ctxResponse))
506
+ console.debug('chatResponse', JSON.stringify(response, null, 2))
518
507
  } catch (e) {
519
508
  errorStatus.value = {
520
509
  errorCode: 'Error',
@@ -524,29 +513,29 @@ export default {
524
513
  }
525
514
  }
526
515
 
527
- if (result?.error) {
516
+ if (response?.error) {
528
517
  errorStatus.value ??= {
529
518
  errorCode: 'Error',
530
519
  }
531
- errorStatus.value.message = result.error
520
+ errorStatus.value.message = response.error
532
521
  }
533
522
 
534
523
  if (!errorStatus.value) {
535
524
  // Add assistant response (save entire message including reasoning)
536
- const assistantMessage = result.choices?.[0]?.message
525
+ const assistantMessage = response.choices?.[0]?.message
537
526
 
538
- const usage = result.usage
527
+ const usage = response.usage
539
528
  if (usage) {
540
- if (result.metadata?.pricing) {
541
- const [input, output] = result.metadata.pricing.split('/')
542
- usage.duration = result.metadata.duration ?? (Date.now() - startTime)
529
+ if (response.metadata?.pricing) {
530
+ const [input, output] = response.metadata.pricing.split('/')
531
+ usage.duration = response.metadata.duration ?? (Date.now() - startTime)
543
532
  usage.input = input
544
533
  usage.output = output
545
534
  usage.tokens = usage.completion_tokens
546
535
  usage.price = usage.output
547
536
  usage.cost = tokenCost(usage.prompt_tokens / 1_000_000 * parseFloat(input) + usage.completion_tokens / 1_000_000 * parseFloat(output))
548
537
  }
549
- await threads.logRequest(threadId, props.model, chatRequest, result)
538
+ await threads.logRequest(threadId, props.model, request, response)
550
539
  }
551
540
  await threads.addMessageToThread(threadId, assistantMessage, usage)
552
541
 
@@ -554,6 +543,8 @@ export default {
554
543
 
555
544
  attachedFiles.value = []
556
545
  // Error will be cleared when user sends next message (no auto-timeout)
546
+ } else {
547
+ ctx.chatErrorFilters.forEach(f => f(errorStatus.value))
557
548
  }
558
549
  } catch (error) {
559
550
  // Check if the error is due to abort
llms/ui/Main.mjs CHANGED
@@ -2,25 +2,63 @@ import { ref, computed, nextTick, watch, onMounted, provide, inject } from 'vue'
2
2
  import { useRouter, useRoute } from 'vue-router'
3
3
  import { useFormatters } from '@servicestack/vue'
4
4
  import { useThreadStore } from './threadStore.mjs'
5
- import { storageObject, addCopyButtons, formatCost, statsTitle, fetchCacheInfos } from './utils.mjs'
5
+ import { addCopyButtons, formatCost, statsTitle, fetchCacheInfos } from './utils.mjs'
6
6
  import { renderMarkdown } from './markdown.mjs'
7
7
  import ChatPrompt, { useChatPrompt } from './ChatPrompt.mjs'
8
8
  import SignIn from './SignIn.mjs'
9
9
  import OAuthSignIn from './OAuthSignIn.mjs'
10
10
  import Avatar from './Avatar.mjs'
11
- import ModelSelector from './ModelSelector.mjs'
12
- import SystemPromptSelector from './SystemPromptSelector.mjs'
13
- import SystemPromptEditor from './SystemPromptEditor.mjs'
14
11
  import { useSettings } from "./SettingsDialog.mjs"
15
12
  import Welcome from './Welcome.mjs'
16
13
 
17
14
  const { humanifyMs, humanifyNumber } = useFormatters()
18
15
 
16
+ const TopBar = {
17
+ template: `
18
+ <div class="flex space-x-2">
19
+ <div v-for="(ext, index) in extensions" :key="ext.id" class="relative flex items-center justify-center">
20
+ <component :is="ext.topBarIcon"
21
+ class="size-7 p-1 cursor-pointer text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 block"
22
+ :class="{ 'bg-gray-100 dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded' : ext.isActive($layout.top) }"
23
+ @mouseenter="tooltip = ext.name"
24
+ @mouseleave="tooltip = ''"
25
+ />
26
+ <div v-if="tooltip === ext.name"
27
+ class="absolute top-full mt-2 px-2 py-1 text-xs text-white bg-gray-900 dark:bg-gray-800 rounded shadow-md z-50 whitespace-nowrap pointer-events-none"
28
+ :class="index <= extensions.length - 1 ? 'right-0' : 'left-1/2 -translate-x-1/2'">
29
+ {{ext.name}}
30
+ </div>
31
+ </div>
32
+ </div>
33
+ `,
34
+ setup() {
35
+ const ctx = inject('ctx')
36
+ const tooltip = ref('')
37
+ const extensions = computed(() => ctx.extensions.filter(x => x.topBarIcon))
38
+ return {
39
+ extensions,
40
+ tooltip,
41
+ }
42
+ }
43
+ }
44
+
45
+ const TopPanel = {
46
+ template: `
47
+ <component v-if="component" :is="component" />
48
+ `,
49
+ setup() {
50
+ const ctx = inject('ctx')
51
+ const component = computed(() => ctx.component(ctx.layout.top))
52
+ return {
53
+ component,
54
+ }
55
+ }
56
+ }
57
+
19
58
  export default {
20
59
  components: {
21
- ModelSelector,
22
- SystemPromptSelector,
23
- SystemPromptEditor,
60
+ TopBar,
61
+ TopPanel,
24
62
  ChatPrompt,
25
63
  SignIn,
26
64
  OAuthSignIn,
@@ -29,28 +67,26 @@ export default {
29
67
  },
30
68
  template: `
31
69
  <div class="flex flex-col h-full w-full">
32
- <!-- Header with model and prompt selectors (hidden when auth required and not authenticated) -->
33
- <div v-if="!($ai.requiresAuth && !$ai.auth)"
70
+ <!-- Header with model selectors -->
71
+ <div v-if="$ai.hasAccess"
34
72
  :class="!$ai.isSidebarOpen ? 'pl-6' : ''"
35
- class="border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 px-2 py-2 w-full min-h-16">
73
+ class="flex items-center border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 px-2 w-full min-h-16">
36
74
  <div class="flex flex-wrap items-center justify-between w-full">
37
75
  <ModelSelector :models="models" v-model="selectedModel" @updated="configUpdated" />
38
76
 
39
77
  <div class="flex items-center space-x-2 pl-4">
40
- <SystemPromptSelector :prompts="prompts" v-model="selectedPrompt"
41
- :show="showSystemPrompt" @toggle="showSystemPrompt = !showSystemPrompt" />
78
+ <TopBar />
42
79
  <Avatar />
43
80
  </div>
44
81
  </div>
45
82
  </div>
46
83
 
47
- <SystemPromptEditor v-if="showSystemPrompt && !($ai.requiresAuth && !$ai.auth)"
48
- v-model="currentSystemPrompt" :prompts="prompts" :selected="selectedPrompt" />
84
+ <TopPanel />
49
85
 
50
86
  <!-- Messages Area -->
51
87
  <div class="flex-1 overflow-y-auto" ref="messagesContainer">
52
88
  <div class="mx-auto max-w-6xl px-4 py-6">
53
- <div v-if="$ai.requiresAuth && !$ai.auth">
89
+ <div v-if="!$ai.hasAccess">
54
90
  <OAuthSignIn v-if="$ai.authType === 'oauth'" @done="$ai.signIn($event)" />
55
91
  <SignIn v-else @done="$ai.signIn($event)" />
56
92
  </div>
@@ -179,31 +215,32 @@ export default {
179
215
  </div>
180
216
  </div>
181
217
 
182
- <!-- User Message with separate attachments -->
183
- <div v-if="message.role !== 'assistant'">
184
- <div v-html="renderMarkdown(message.content)" class="prose prose-sm max-w-none dark:prose-invert break-words"></div>
185
-
186
- <!-- Attachments Grid -->
187
- <div v-if="hasAttachments(message)" class="mt-2 flex flex-wrap gap-2">
188
- <template v-for="(part, i) in getAttachments(message)" :key="i">
189
- <!-- Image -->
190
- <div v-if="part.type === 'image_url'" class="group relative cursor-pointer" @click="openLightbox(part.image_url.url)">
191
- <img :src="part.image_url.url" class="max-w-[400px] max-h-96 rounded-lg border border-gray-200 dark:border-gray-700 object-contain bg-gray-50 dark:bg-gray-900 shadow-sm transition-transform hover:scale-[1.02]" />
192
- </div>
193
- <!-- Audio -->
194
- <div v-else-if="part.type === 'input_audio'" class="flex items-center gap-2 p-2 rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800">
195
- <svg class="w-5 h-5 text-gray-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18V5l12-2v13"></path><circle cx="6" cy="18" r="3"></circle><circle cx="18" cy="16" r="3"></circle></svg>
196
- <audio controls :src="part.input_audio.data" class="h-8 w-48"></audio>
197
- </div>
198
- <!-- File -->
199
- <a v-else-if="part.type === 'file'" :href="part.file.file_data" target="_blank"
200
- class="flex items-center gap-2 px-3 py-2 rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors text-sm text-blue-600 dark:text-blue-400 hover:underline">
201
- <svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path><polyline points="13 2 13 9 20 9"></polyline></svg>
202
- <span class="max-w-xs truncate">{{ part.file.filename || 'Attachment' }}</span>
203
- </a>
204
- </template>
205
- </div>
206
- </div>
218
+ <!-- User Message with separate attachments -->
219
+ <div v-if="message.role !== 'assistant'">
220
+ <div v-html="renderMarkdown(message.content)" class="prose prose-sm max-w-none dark:prose-invert break-words"></div>
221
+
222
+ <!-- Attachments Grid -->
223
+ <div v-if="hasAttachments(message)" class="mt-2 flex flex-wrap gap-2">
224
+ <template v-for="(part, i) in getAttachments(message)" :key="i">
225
+ <!-- Image -->
226
+ <div v-if="part.type === 'image_url'" class="group relative cursor-pointer" @click="openLightbox(part.image_url.url)">
227
+ <img :src="part.image_url.url" class="max-w-[400px] max-h-96 rounded-lg border border-gray-200 dark:border-gray-700 object-contain bg-gray-50 dark:bg-gray-900 shadow-sm transition-transform hover:scale-[1.02]" />
228
+ </div>
229
+ <!-- Audio -->
230
+ <div v-else-if="part.type === 'input_audio'" class="flex items-center gap-2 p-2 rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800">
231
+ <svg class="w-5 h-5 text-gray-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18V5l12-2v13"></path><circle cx="6" cy="18" r="3"></circle><circle cx="18" cy="16" r="3"></circle></svg>
232
+ <audio controls :src="part.input_audio.data" class="h-8 w-48"></audio>
233
+ </div>
234
+ <!-- File -->
235
+ <a v-else-if="part.type === 'file'" :href="part.file.file_data" target="_blank"
236
+ class="flex items-center gap-2 px-3 py-2 rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors text-sm text-blue-600 dark:text-blue-400 hover:underline">
237
+ <svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path><polyline points="13 2 13 9 20 9"></polyline></svg>
238
+ <span class="max-w-xs truncate">{{ part.file.filename || 'Attachment' }}</span>
239
+ </a>
240
+ </template>
241
+ </div>
242
+ </div>
243
+
207
244
  <div class="mt-2 text-xs opacity-70">
208
245
  <span>{{ formatTime(message.timestamp) }}</span>
209
246
  <span v-if="message.usage" :title="tokensTitle(message.usage)">
@@ -304,8 +341,8 @@ export default {
304
341
  </div>
305
342
 
306
343
  <!-- Input Area -->
307
- <div class="flex-shrink-0 border-t border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 px-6 py-4">
308
- <ChatPrompt :model="selectedModelObj" :systemPrompt="currentSystemPrompt" />
344
+ <div v-if="$ai.hasAccess" class="flex-shrink-0 border-t border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 px-6 py-4">
345
+ <ChatPrompt :model="selectedModelObj" />
309
346
  </div>
310
347
 
311
348
  <!-- Lightbox -->
@@ -323,10 +360,10 @@ export default {
323
360
  </div>
324
361
  </div>
325
362
  `,
326
- props: {
327
- },
328
- setup(props) {
329
- const ai = inject('ai')
363
+ setup() {
364
+ const ctx = inject('ctx')
365
+ const models = ctx.state.models
366
+ const config = ctx.state.config
330
367
  const router = useRouter()
331
368
  const route = useRoute()
332
369
  const threads = useThreadStore()
@@ -340,28 +377,14 @@ export default {
340
377
  provide('threads', threads)
341
378
  provide('chatPrompt', chatPrompt)
342
379
  provide('chatSettings', chatSettings)
343
- const models = inject('models')
344
- const config = inject('config')
345
380
 
346
- const prefs = storageObject(ai.prefsKey)
347
-
348
- const customPromptValue = ref('')
349
- const customPrompt = {
350
- id: '_custom_',
351
- name: 'Custom...',
352
- value: ''
353
- }
354
-
355
- const prompts = computed(() => [customPrompt, ...config.prompts])
381
+ const prefs = ctx.getPrefs()
356
382
 
357
383
  const selectedModel = ref(prefs.model || config.defaults.text.model || '')
358
384
  const selectedModelObj = computed(() => {
359
385
  if (!selectedModel.value || !models) return null
360
386
  return models.find(m => m.name === selectedModel.value) || models.find(m => m.id === selectedModel.value)
361
387
  })
362
- const selectedPrompt = ref(prefs.systemPrompt || null)
363
- const currentSystemPrompt = ref('')
364
- const showSystemPrompt = ref(false)
365
388
  const messagesContainer = ref(null)
366
389
  const isExporting = ref(false)
367
390
  const isImporting = ref(false)
@@ -396,45 +419,16 @@ export default {
396
419
  selectedModel.value = thread.model
397
420
  }
398
421
 
399
- // Sync System Prompt selection from thread
400
- if (thread) {
401
- const norm = s => (s || '').replace(/\s+/g, ' ').trim()
402
- const tsp = norm(thread.systemPrompt || '')
403
- if (tsp) {
404
- const match = config.prompts.find(p => norm(p.value) === tsp)
405
- if (match) {
406
- selectedPrompt.value = match
407
- currentSystemPrompt.value = match.value.replace(/\n/g, ' ')
408
- } else {
409
- selectedPrompt.value = customPrompt
410
- currentSystemPrompt.value = thread.systemPrompt
411
- }
412
- } else {
413
- // Preserve existing selected prompt
414
- // selectedPrompt.value = null
415
- // currentSystemPrompt.value = ''
416
- }
417
- }
418
-
419
422
  if (!newId) {
420
423
  chatPrompt.reset()
421
424
  }
422
425
  nextTick(addCopyButtons)
423
426
  }, { immediate: true })
424
427
 
425
- // Watch selectedPrompt and update currentSystemPrompt
426
- watch(selectedPrompt, (newPrompt) => {
427
- // If using a custom prompt, keep whatever is already in currentSystemPrompt
428
- if (newPrompt && newPrompt.id === '_custom_') return
429
- const prompt = newPrompt && config.prompts.find(p => p.id === newPrompt.id)
430
- currentSystemPrompt.value = prompt ? prompt.value.replace(/\n/g, ' ') : ''
431
- }, { immediate: true })
432
-
433
- watch(() => [selectedModel.value, selectedPrompt.value], () => {
434
- localStorage.setItem(ai.prefsKey, JSON.stringify({
428
+ watch(() => [selectedModel.value], () => {
429
+ ctx.setPrefs({
435
430
  model: selectedModel.value,
436
- systemPrompt: selectedPrompt.value
437
- }))
431
+ })
438
432
  })
439
433
 
440
434
  async function exportThreads() {
@@ -828,15 +822,10 @@ export default {
828
822
  config,
829
823
  models,
830
824
  threads,
831
- prompts,
832
825
  isGenerating,
833
- customPromptValue,
834
826
  currentThread,
835
827
  selectedModel,
836
828
  selectedModelObj,
837
- selectedPrompt,
838
- currentSystemPrompt,
839
- showSystemPrompt,
840
829
  messagesContainer,
841
830
  errorStatus,
842
831
  copying,
llms/ui/OAuthSignIn.mjs CHANGED
@@ -45,44 +45,13 @@ export default {
45
45
  `,
46
46
  emits: ['done'],
47
47
  setup(props, { emit }) {
48
- const ai = inject('ai')
49
48
  const errorMessage = ref(null)
50
-
49
+
51
50
  function signInWithGitHub() {
52
51
  // Redirect to GitHub OAuth endpoint
53
52
  window.location.href = '/auth/github'
54
53
  }
55
-
56
- // Check for session token in URL (after OAuth callback redirect)
57
- onMounted(async () => {
58
- const urlParams = new URLSearchParams(window.location.search)
59
- const sessionToken = urlParams.get('session')
60
-
61
- if (sessionToken) {
62
- try {
63
- // Validate session with server
64
- const response = await ai.get(`/auth/session?session=${sessionToken}`)
65
-
66
- if (response.ok) {
67
- const sessionData = await response.json()
68
-
69
- // Clean up URL
70
- const url = new URL(window.location.href)
71
- url.searchParams.delete('session')
72
- window.history.replaceState({}, '', url.toString())
73
-
74
- // Emit done event with session data
75
- emit('done', sessionData)
76
- } else {
77
- errorMessage.value = 'Failed to validate session'
78
- }
79
- } catch (error) {
80
- console.error('Session validation error:', error)
81
- errorMessage.value = 'Failed to validate session'
82
- }
83
- }
84
- })
85
-
54
+
86
55
  return {
87
56
  signInWithGitHub,
88
57
  errorMessage,
@@ -31,9 +31,10 @@ export default {
31
31
  `,
32
32
  emits: ['updated'],
33
33
  setup(props, { emit }) {
34
- const ai = inject('ai')
35
- const config = inject('config')
36
- const models = inject('models')
34
+ const ctx = inject('ctx')
35
+ const ai = ctx.ai
36
+ const config = ctx.state.config
37
+ const models = ctx.state.models
37
38
  const showPopover = ref(false)
38
39
  const triggerRef = ref(null)
39
40
  const popoverRef = ref(null)
@@ -62,11 +63,9 @@ export default {
62
63
  ai.getConfig(),
63
64
  ai.getModels(),
64
65
  ])
65
- const newConfig = await configRes.json()
66
- const newModels = await modelsRes.json()
67
- Object.assign(config, newConfig)
68
- models.length = 0
69
- newModels.forEach(m => models.push(m))
66
+ const config = await configRes.json()
67
+ const models = await modelsRes.json()
68
+ Object.assign(ctx.state, { config, models })
70
69
  emit('updated')
71
70
  renderKey.value++
72
71
  } catch (e) {