llms-py 2.0.24__py3-none-any.whl → 2.0.26__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.
llms/ui/Avatar.mjs CHANGED
@@ -1,13 +1,40 @@
1
- import { computed, inject } from "vue"
1
+ import { computed, inject, ref, onMounted, onUnmounted } from "vue"
2
2
 
3
3
  export default {
4
4
  template:`
5
- <div v-if="$ai.auth?.profileUrl" :title="authTitle">
6
- <img :src="$ai.auth.profileUrl" class="size-8 rounded-full" />
5
+ <div v-if="$ai.auth?.profileUrl" class="relative" ref="avatarContainer">
6
+ <img
7
+ @click.stop="toggleMenu"
8
+ :src="$ai.auth.profileUrl"
9
+ :title="authTitle"
10
+ class="size-8 rounded-full cursor-pointer hover:ring-2 hover:ring-gray-300"
11
+ />
12
+ <div
13
+ v-if="showMenu"
14
+ @click.stop
15
+ class="absolute right-0 mt-2 w-48 bg-white dark:bg-gray-800 rounded-md shadow-lg py-1 z-50 border border-gray-200 dark:border-gray-700"
16
+ >
17
+ <div class="px-4 py-2 text-sm text-gray-700 dark:text-gray-300 border-b border-gray-200 dark:border-gray-700">
18
+ <div class="font-medium whitespace-nowrap overflow-hidden text-ellipsis">{{ $ai.auth.displayName || $ai.auth.userName }}</div>
19
+ <div class="text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap overflow-hidden text-ellipsis">{{ $ai.auth.email }}</div>
20
+ </div>
21
+ <button type="button"
22
+ @click="handleLogout"
23
+ class="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center whitespace-nowrap"
24
+ >
25
+ <svg class="w-4 h-4 mr-2 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
26
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"></path>
27
+ </svg>
28
+ Sign Out
29
+ </button>
30
+ </div>
7
31
  </div>
8
32
  `,
9
33
  setup() {
10
34
  const ai = inject('ai')
35
+ const showMenu = ref(false)
36
+ const avatarContainer = ref(null)
37
+
11
38
  const authTitle = computed(() => {
12
39
  if (!ai.auth) return ''
13
40
  const { userId, userName, displayName, bearerToken, roles } = ai.auth
@@ -20,9 +47,39 @@ export default {
20
47
  ]
21
48
  return sb.filter(x => x).join('\n')
22
49
  })
23
-
50
+
51
+ function toggleMenu() {
52
+ showMenu.value = !showMenu.value
53
+ }
54
+
55
+ async function handleLogout() {
56
+ showMenu.value = false
57
+ await ai.signOut()
58
+ // Reload the page to show sign-in screen
59
+ window.location.reload()
60
+ }
61
+
62
+ // Close menu when clicking outside
63
+ const handleClickOutside = (event) => {
64
+ if (showMenu.value && avatarContainer.value && !avatarContainer.value.contains(event.target)) {
65
+ showMenu.value = false
66
+ }
67
+ }
68
+
69
+ onMounted(() => {
70
+ document.addEventListener('click', handleClickOutside)
71
+ })
72
+
73
+ onUnmounted(() => {
74
+ document.removeEventListener('click', handleClickOutside)
75
+ })
76
+
24
77
  return {
25
78
  authTitle,
79
+ handleLogout,
80
+ showMenu,
81
+ toggleMenu,
82
+ avatarContainer,
26
83
  }
27
84
  }
28
85
  }
llms/ui/Main.mjs CHANGED
@@ -6,6 +6,7 @@ import { storageObject, addCopyButtons, formatCost, statsTitle } from './utils.m
6
6
  import { renderMarkdown } from './markdown.mjs'
7
7
  import ChatPrompt, { useChatPrompt } from './ChatPrompt.mjs'
8
8
  import SignIn from './SignIn.mjs'
9
+ import OAuthSignIn from './OAuthSignIn.mjs'
9
10
  import Avatar from './Avatar.mjs'
10
11
  import ModelSelector from './ModelSelector.mjs'
11
12
  import SystemPromptSelector from './SystemPromptSelector.mjs'
@@ -22,32 +23,34 @@ export default {
22
23
  SystemPromptEditor,
23
24
  ChatPrompt,
24
25
  SignIn,
26
+ OAuthSignIn,
25
27
  Avatar,
26
28
  Welcome,
27
29
  },
28
30
  template: `
29
31
  <div class="flex flex-col h-full w-full">
30
- <!-- Header with model and prompt selectors -->
31
- <div class="border-b border-gray-200 bg-white px-2 py-2 w-full min-h-16">
32
+ <!-- Header with model and prompt selectors (hidden when auth required and not authenticated) -->
33
+ <div v-if="!($ai.requiresAuth && !$ai.auth)" class="border-b border-gray-200 bg-white px-2 py-2 w-full min-h-16">
32
34
  <div class="flex items-center justify-between w-full">
33
35
  <ModelSelector :models="models" v-model="selectedModel" @updated="configUpdated" />
34
36
 
35
37
  <div class="flex items-center space-x-2">
36
- <SystemPromptSelector :prompts="prompts" v-model="selectedPrompt"
38
+ <SystemPromptSelector :prompts="prompts" v-model="selectedPrompt"
37
39
  :show="showSystemPrompt" @toggle="showSystemPrompt = !showSystemPrompt" />
38
40
  <Avatar />
39
41
  </div>
40
42
  </div>
41
43
  </div>
42
44
 
43
- <SystemPromptEditor v-if="showSystemPrompt"
45
+ <SystemPromptEditor v-if="showSystemPrompt && !($ai.requiresAuth && !$ai.auth)"
44
46
  v-model="currentSystemPrompt" :prompts="prompts" :selected="selectedPrompt" />
45
47
 
46
48
  <!-- Messages Area -->
47
49
  <div class="flex-1 overflow-y-auto" ref="messagesContainer">
48
50
  <div class="mx-auto max-w-6xl px-4 py-6">
49
51
  <div v-if="$ai.requiresAuth && !$ai.auth">
50
- <SignIn @done="$ai.signIn($event)" />
52
+ <OAuthSignIn v-if="$ai.authType === 'oauth'" @done="$ai.signIn($event)" />
53
+ <SignIn v-else @done="$ai.signIn($event)" />
51
54
  </div>
52
55
  <!-- Welcome message when no thread is selected -->
53
56
  <div v-else-if="!currentThread" class="text-center py-12">
@@ -0,0 +1,92 @@
1
+ import { inject, ref, onMounted } from "vue"
2
+ import Welcome from './Welcome.mjs'
3
+
4
+ export default {
5
+ components: {
6
+ Welcome,
7
+ },
8
+ template: `
9
+ <div class="min-h-full -mt-36 flex flex-col justify-center sm:px-6 lg:px-8">
10
+ <div class="sm:mx-auto sm:w-full sm:max-w-md text-center">
11
+ <Welcome />
12
+ </div>
13
+ <div class="sm:mx-auto sm:w-full sm:max-w-md">
14
+ <div v-if="errorMessage" class="mb-3 bg-red-50 border border-red-200 text-red-800 rounded-lg px-4 py-3">
15
+ <div class="flex items-start space-x-2">
16
+ <div class="flex-1">
17
+ <div class="text-base font-medium">{{ errorMessage }}</div>
18
+ </div>
19
+ <button type="button"
20
+ @click="errorMessage = null"
21
+ class="text-red-400 hover:text-red-600 flex-shrink-0"
22
+ >
23
+ <svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
24
+ <path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path>
25
+ </svg>
26
+ </button>
27
+ </div>
28
+ </div>
29
+ <div class="py-8 px-4 sm:px-10">
30
+ <div class="space-y-4">
31
+ <button
32
+ type="button"
33
+ @click="signInWithGitHub"
34
+ class="w-full inline-flex items-center justify-center px-4 py-3 border border-gray-300 rounded-md shadow-sm text-base font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 transition-colors"
35
+ >
36
+ <svg class="w-6 h-6 mr-3" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
37
+ <path fill-rule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clip-rule="evenodd" />
38
+ </svg>
39
+ Sign in with GitHub
40
+ </button>
41
+ </div>
42
+ </div>
43
+ </div>
44
+ </div>
45
+ `,
46
+ emits: ['done'],
47
+ setup(props, { emit }) {
48
+ const ai = inject('ai')
49
+ const errorMessage = ref(null)
50
+
51
+ function signInWithGitHub() {
52
+ // Redirect to GitHub OAuth endpoint
53
+ window.location.href = '/auth/github'
54
+ }
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
+
86
+ return {
87
+ signInWithGitHub,
88
+ errorMessage,
89
+ }
90
+ }
91
+ }
92
+
llms/ui/ai.mjs CHANGED
@@ -6,12 +6,13 @@ const headers = { 'Accept': 'application/json' }
6
6
  const prefsKey = 'llms.prefs'
7
7
 
8
8
  export const o = {
9
- version: '2.0.24',
9
+ version: '2.0.26',
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,
16
17
 
17
18
  resolveUrl(url){
@@ -50,26 +51,88 @@ export const o = {
50
51
  this.auth = auth
51
52
  if (auth?.apiKey) {
52
53
  this.headers.Authorization = `Bearer ${auth.apiKey}`
53
- } else if (this.headers.Authorization) {
54
+ //localStorage.setItem('llms:auth', JSON.stringify({ apiKey: auth.apiKey }))
55
+ } else if (auth?.sessionToken) {
56
+ this.headers['X-Session-Token'] = auth.sessionToken
57
+ localStorage.setItem('llms:auth', JSON.stringify({ sessionToken: auth.sessionToken }))
58
+ } else {
59
+ if (this.headers.Authorization) {
60
+ delete this.headers.Authorization
61
+ }
62
+ if (this.headers['X-Session-Token']) {
63
+ delete this.headers['X-Session-Token']
64
+ }
65
+ }
66
+ },
67
+ async signOut() {
68
+ if (this.auth?.sessionToken) {
69
+ // Call logout endpoint for OAuth sessions
70
+ try {
71
+ await this.post('/auth/logout', {
72
+ headers: {
73
+ 'X-Session-Token': this.auth.sessionToken
74
+ }
75
+ })
76
+ } catch (error) {
77
+ console.error('Logout error:', error)
78
+ }
79
+ }
80
+ this.auth = null
81
+ if (this.headers.Authorization) {
54
82
  delete this.headers.Authorization
55
83
  }
84
+ if (this.headers['X-Session-Token']) {
85
+ delete this.headers['X-Session-Token']
86
+ }
87
+ localStorage.removeItem('llms:auth')
56
88
  },
57
89
  async init() {
58
90
  // Load models and prompts
59
91
  const { initDB } = useThreadStore()
60
- const [_, configRes, modelsRes, authRes] = await Promise.all([
92
+ const [_, configRes, modelsRes] = await Promise.all([
61
93
  initDB(),
62
94
  this.getConfig(),
63
95
  this.getModels(),
64
- this.getAuth(),
65
96
  ])
66
97
  const config = await configRes.json()
67
98
  const models = await modelsRes.json()
68
- const auth = this.requiresAuth
99
+
100
+ // Update auth settings from server config
101
+ if (config.requiresAuth != null) {
102
+ this.requiresAuth = config.requiresAuth
103
+ }
104
+ if (config.authType != null) {
105
+ this.authType = config.authType
106
+ }
107
+
108
+ // Try to restore session from localStorage
109
+ if (this.requiresAuth) {
110
+ const storedAuth = localStorage.getItem('llms:auth')
111
+ if (storedAuth) {
112
+ try {
113
+ const authData = JSON.parse(storedAuth)
114
+ if (authData.sessionToken) {
115
+ this.headers['X-Session-Token'] = authData.sessionToken
116
+ }
117
+ // else if (authData.apiKey) {
118
+ // this.headers.Authorization = `Bearer ${authData.apiKey}`
119
+ // }
120
+ } catch (e) {
121
+ console.error('Failed to restore auth from localStorage:', e)
122
+ localStorage.removeItem('llms:auth')
123
+ }
124
+ }
125
+ }
126
+
127
+ // Get auth status
128
+ const authRes = await this.getAuth()
129
+ const auth = this.requiresAuth
69
130
  ? await authRes.json()
70
131
  : null
71
132
  if (auth?.responseStatus?.errorCode) {
72
133
  console.error(auth.responseStatus.errorCode, auth.responseStatus.message)
134
+ // Clear invalid session from localStorage
135
+ localStorage.removeItem('llms:auth')
73
136
  } else {
74
137
  this.signIn(auth)
75
138
  }
llms/ui/app.css CHANGED
@@ -426,6 +426,9 @@
426
426
  .-mt-12 {
427
427
  margin-top: calc(var(--spacing) * -12);
428
428
  }
429
+ .-mt-36 {
430
+ margin-top: calc(var(--spacing) * -36);
431
+ }
429
432
  .mt-1 {
430
433
  margin-top: calc(var(--spacing) * 1);
431
434
  }
@@ -662,6 +665,9 @@
662
665
  .w-32 {
663
666
  width: calc(var(--spacing) * 32);
664
667
  }
668
+ .w-48 {
669
+ width: calc(var(--spacing) * 48);
670
+ }
665
671
  .w-72 {
666
672
  width: calc(var(--spacing) * 72);
667
673
  }
@@ -852,6 +858,13 @@
852
858
  margin-block-end: calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse)));
853
859
  }
854
860
  }
861
+ .space-y-4 {
862
+ :where(& > :not(:last-child)) {
863
+ --tw-space-y-reverse: 0;
864
+ margin-block-start: calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));
865
+ margin-block-end: calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)));
866
+ }
867
+ }
855
868
  .space-y-6 {
856
869
  :where(& > :not(:last-child)) {
857
870
  --tw-space-y-reverse: 0;
@@ -1431,6 +1444,9 @@
1431
1444
  .break-words {
1432
1445
  overflow-wrap: break-word;
1433
1446
  }
1447
+ .text-ellipsis {
1448
+ text-overflow: ellipsis;
1449
+ }
1434
1450
  .whitespace-nowrap {
1435
1451
  white-space: nowrap;
1436
1452
  }
@@ -2214,6 +2230,21 @@
2214
2230
  }
2215
2231
  }
2216
2232
  }
2233
+ .hover\:ring-2 {
2234
+ &:hover {
2235
+ @media (hover: hover) {
2236
+ --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, hsl(var(--ring)));
2237
+ box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
2238
+ }
2239
+ }
2240
+ }
2241
+ .hover\:ring-gray-300 {
2242
+ &:hover {
2243
+ @media (hover: hover) {
2244
+ --tw-ring-color: var(--color-gray-300);
2245
+ }
2246
+ }
2247
+ }
2217
2248
  .hover\:file\:bg-violet-100 {
2218
2249
  &:hover {
2219
2250
  @media (hover: hover) {
@@ -2292,6 +2323,11 @@
2292
2323
  --tw-ring-color: var(--color-cyan-500);
2293
2324
  }
2294
2325
  }
2326
+ .focus\:ring-gray-500 {
2327
+ &:focus {
2328
+ --tw-ring-color: var(--color-gray-500);
2329
+ }
2330
+ }
2295
2331
  .focus\:ring-green-500 {
2296
2332
  &:focus {
2297
2333
  --tw-ring-color: var(--color-green-500);