llms-py 3.0.1__py3-none-any.whl → 3.0.3__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 (36) hide show
  1. llms/{extensions/app/db_manager.py → db.py} +180 -16
  2. llms/extensions/app/__init__.py +96 -29
  3. llms/extensions/app/db.py +16 -124
  4. llms/extensions/app/ui/threadStore.mjs +23 -2
  5. llms/extensions/core_tools/__init__.py +37 -0
  6. llms/extensions/gallery/__init__.py +15 -13
  7. llms/extensions/gallery/db.py +117 -172
  8. llms/extensions/gallery/ui/index.mjs +1 -1
  9. llms/extensions/providers/__init__.py +3 -1
  10. llms/extensions/providers/anthropic.py +7 -3
  11. llms/extensions/providers/cerebras.py +37 -0
  12. llms/extensions/providers/chutes.py +1 -1
  13. llms/extensions/providers/google.py +131 -28
  14. llms/extensions/providers/nvidia.py +2 -2
  15. llms/extensions/providers/openai.py +2 -2
  16. llms/extensions/providers/openrouter.py +4 -2
  17. llms/llms.json +3 -0
  18. llms/main.py +83 -34
  19. llms/providers.json +1 -1
  20. llms/ui/ai.mjs +1 -1
  21. llms/ui/app.css +106 -3
  22. llms/ui/ctx.mjs +34 -0
  23. llms/ui/index.mjs +2 -0
  24. llms/ui/modules/chat/ChatBody.mjs +245 -248
  25. llms/ui/modules/chat/index.mjs +93 -2
  26. llms/ui/modules/icons.mjs +46 -0
  27. llms/ui/modules/layout.mjs +28 -0
  28. llms/ui/modules/model-selector.mjs +0 -40
  29. llms/ui/tailwind.input.css +1 -1
  30. llms/ui/utils.mjs +9 -1
  31. {llms_py-3.0.1.dist-info → llms_py-3.0.3.dist-info}/METADATA +1 -1
  32. {llms_py-3.0.1.dist-info → llms_py-3.0.3.dist-info}/RECORD +36 -34
  33. {llms_py-3.0.1.dist-info → llms_py-3.0.3.dist-info}/WHEEL +0 -0
  34. {llms_py-3.0.1.dist-info → llms_py-3.0.3.dist-info}/entry_points.txt +0 -0
  35. {llms_py-3.0.1.dist-info → llms_py-3.0.3.dist-info}/licenses/LICENSE +0 -0
  36. {llms_py-3.0.1.dist-info → llms_py-3.0.3.dist-info}/top_level.txt +0 -0
@@ -70,6 +70,7 @@ export default {
70
70
  <!-- Messages Area -->
71
71
  <div class="flex-1 overflow-y-auto" ref="messagesContainer">
72
72
  <div class="mx-auto max-w-6xl px-4 py-6">
73
+
73
74
  <div v-if="!$ai.hasAccess">
74
75
  <OAuthSignIn v-if="$ai.authType === 'oauth'" @done="$ai.signIn($event)" />
75
76
  <SignIn v-else @done="$ai.signIn($event)" />
@@ -81,299 +82,295 @@ export default {
81
82
  </div>
82
83
 
83
84
  <!-- Messages -->
84
- <div v-else-if="currentThread?.messages?.length" class="space-y-2">
85
- <div v-if="currentThread?.messages.length && currentThread?.model" class="flex items-center justify-center select-none">
86
- <span @click="$chat.setSelectedModel({ name: currentThread.model})"
87
- class="flex items-center cursor-pointer px-1.5 py-0.5 text-xs rounded text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-gray-900 dark:hover:text-gray-100 transition-colors border hover:border-gray-300 dark:hover:border-gray-700">
88
- <ProviderIcon class="size-4 mr-1" :provider="$chat.getProviderForModel(currentThread.model)" />
89
- {{currentThread.model}}
90
- </span>
91
- </div>
92
- <div
93
- v-for="message in currentThread.messages.filter(x => x.role !== 'system')"
94
- :key="message.id"
95
- v-show="!(message.role === 'tool' && isToolLinked(message))"
96
- class="flex items-start space-x-3 group"
97
- :class="message.role === 'user' ? 'flex-row-reverse space-x-reverse' : ''"
98
- >
99
- <!-- Avatar outside the bubble -->
100
- <div class="flex-shrink-0 flex flex-col justify-center">
101
- <div class="w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium"
102
- :class="message.role === 'user'
103
- ? 'bg-blue-100 dark:bg-blue-900 text-gray-900 dark:text-gray-100 border border-blue-200 dark:border-blue-700'
104
- : message.role === 'tool'
105
- ? 'bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 border border-purple-200 dark:border-purple-800'
106
- : 'bg-gray-600 dark:bg-gray-500 text-white'"
107
- >
108
- <span v-if="message.role === 'user'">U</span>
109
- <svg v-else-if="message.role === 'tool'" class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
110
- <path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"></path>
111
- </svg>
112
- <span v-else>AI</span>
113
- </div>
114
-
115
- <!-- Delete button (shown on hover) -->
116
- <button type="button" @click.stop="$threads.deleteMessageFromThread(currentThread.id, message.id)"
117
- class="mx-auto opacity-0 group-hover:opacity-100 mt-2 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"
118
- title="Delete message">
119
- <svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
120
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
121
- </svg>
122
- </button>
123
- </div>
124
-
125
- <!-- Message bubble -->
85
+ <div v-else-if="currentThread">
86
+ <ThreadHeader v-if="currentThread" :thread="currentThread" class="mb-2" />
87
+ <div class="space-y-2" v-if="currentThread?.messages?.length">
126
88
  <div
127
- class="message rounded-lg px-4 py-3 relative group"
128
- :class="message.role === 'user'
129
- ? 'bg-blue-100 dark:bg-blue-900 text-gray-900 dark:text-gray-100 border border-blue-200 dark:border-blue-700'
130
- : 'bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-gray-100 border border-gray-200 dark:border-gray-700'"
89
+ v-for="message in currentThread.messages.filter(x => x.role !== 'system')"
90
+ :key="message.id"
91
+ v-show="!(message.role === 'tool' && isToolLinked(message))"
92
+ class="flex items-start space-x-3 group"
93
+ :class="message.role === 'user' ? 'flex-row-reverse space-x-reverse' : ''"
131
94
  >
132
- <!-- Copy button in top right corner -->
133
- <button
134
- type="button"
135
- @click="copyMessageContent(message)"
136
- class="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 p-1 rounded hover:bg-black/10 dark:hover:bg-white/10 focus:outline-none focus:ring-0"
137
- :class="message.role === 'user' ? 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200' : 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200'"
138
- title="Copy message content"
139
- >
140
- <svg v-if="copying === message" class="size-4 text-green-500 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg>
141
- <svg v-else class="size-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
142
- <rect width="14" height="14" x="8" y="8" rx="2" ry="2"/>
143
- <path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/>
144
- </svg>
145
- </button>
95
+ <!-- Avatar outside the bubble -->
96
+ <div class="flex-shrink-0 flex flex-col justify-center">
97
+ <div class="w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium"
98
+ :class="message.role === 'user'
99
+ ? 'bg-blue-100 dark:bg-blue-900 text-gray-900 dark:text-gray-100 border border-blue-200 dark:border-blue-700'
100
+ : message.role === 'tool'
101
+ ? 'bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 border border-purple-200 dark:border-purple-800'
102
+ : 'bg-gray-600 dark:bg-gray-500 text-white'"
103
+ >
104
+ <span v-if="message.role === 'user'">U</span>
105
+ <svg v-else-if="message.role === 'tool'" class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
106
+ <path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"></path>
107
+ </svg>
108
+ <span v-else>AI</span>
109
+ </div>
110
+
111
+ <!-- Delete button (shown on hover) -->
112
+ <button type="button" @click.stop="$threads.deleteMessageFromThread(currentThread.id, message.id)"
113
+ class="mx-auto opacity-0 group-hover:opacity-100 mt-2 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"
114
+ title="Delete message">
115
+ <svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
116
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
117
+ </svg>
118
+ </button>
119
+ </div>
146
120
 
121
+ <!-- Message bubble -->
147
122
  <div
148
- v-if="message.role === 'assistant'"
149
- v-html="$fmt.markdown(message.content)"
150
- class="prose prose-sm max-w-none dark:prose-invert"
151
- ></div>
152
-
153
- <!-- Collapsible reasoning section -->
154
- <MessageReasoning v-if="message.role === 'assistant' && (message.reasoning || message.thinking || message.reasoning_content)"
155
- :reasoning="message.reasoning || message.thinking || message.reasoning_content" :message="message" />
156
-
157
- <!-- Tool Calls & Outputs -->
158
- <div v-if="message.tool_calls && message.tool_calls.length > 0" class="mb-3 space-y-4">
159
- <div v-for="(tool, i) in message.tool_calls" :key="i" class="rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 overflow-hidden">
160
- <!-- Tool Call Header -->
161
- <div class="px-3 py-2 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between bg-gray-50/30 dark:bg-gray-800 space-x-4">
162
- <div class="flex items-center gap-2">
163
- <svg class="size-3.5 text-gray-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"></path></svg>
164
- <span class="font-mono text-xs font-bold text-gray-700 dark:text-gray-300">{{ tool.function.name }}</span>
123
+ class="message rounded-lg px-4 py-3 relative group"
124
+ :class="message.role === 'user'
125
+ ? 'bg-blue-100 dark:bg-blue-900 text-gray-900 dark:text-gray-100 border border-blue-200 dark:border-blue-700'
126
+ : 'bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-gray-100 border border-gray-200 dark:border-gray-700'"
127
+ >
128
+ <!-- Copy button in top right corner -->
129
+ <button
130
+ type="button"
131
+ @click="copyMessageContent(message)"
132
+ class="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 p-1 rounded hover:bg-black/10 dark:hover:bg-white/10 focus:outline-none focus:ring-0"
133
+ :class="message.role === 'user' ? 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200' : 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200'"
134
+ title="Copy message content"
135
+ >
136
+ <svg v-if="copying === message" class="size-4 text-green-500 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg>
137
+ <svg v-else class="size-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
138
+ <rect width="14" height="14" x="8" y="8" rx="2" ry="2"/>
139
+ <path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/>
140
+ </svg>
141
+ </button>
142
+
143
+ <div
144
+ v-if="message.role === 'assistant'"
145
+ v-html="$fmt.markdown(message.content)"
146
+ class="prose prose-sm max-w-none dark:prose-invert"
147
+ ></div>
148
+
149
+ <!-- Collapsible reasoning section -->
150
+ <MessageReasoning v-if="message.role === 'assistant' && (message.reasoning || message.thinking || message.reasoning_content)"
151
+ :reasoning="message.reasoning || message.thinking || message.reasoning_content" :message="message" />
152
+
153
+ <!-- Tool Calls & Outputs -->
154
+ <div v-if="message.tool_calls && message.tool_calls.length > 0" class="mb-3 space-y-4">
155
+ <div v-for="(tool, i) in message.tool_calls" :key="i" class="rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 overflow-hidden">
156
+ <!-- Tool Call Header -->
157
+ <div class="px-3 py-2 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between bg-gray-50/30 dark:bg-gray-800 space-x-4">
158
+ <div class="flex items-center gap-2">
159
+ <svg class="size-3.5 text-gray-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"></path></svg>
160
+ <span class="font-mono text-xs font-bold text-gray-700 dark:text-gray-300">{{ tool.function.name }}</span>
161
+ </div>
162
+ <span class="text-[10px] uppercase tracking-wider text-gray-400 font-medium">Tool Call</span>
163
+ </div>
164
+
165
+ <!-- Arguments -->
166
+ <div v-if="tool.function.arguments && tool.function.arguments != '{}'" class="not-prose px-3 py-2">
167
+ <HtmlFormat v-if="hasJsonStructure(tool.function.arguments)" :value="tryParseJson(tool.function.arguments)" :classes="customHtmlClasses" />
168
+ <pre v-else class="tool-arguments">{{ tool.function.arguments }}</pre>
165
169
  </div>
166
- <span class="text-[10px] uppercase tracking-wider text-gray-400 font-medium">Tool Call</span>
167
- </div>
168
-
169
- <!-- Arguments -->
170
- <div v-if="tool.function.arguments && tool.function.arguments != '{}'" class="not-prose px-3 py-2">
171
- <HtmlFormat v-if="hasJsonStructure(tool.function.arguments)" :value="tryParseJson(tool.function.arguments)" :classes="customHtmlClasses" />
172
- <pre v-else class="tool-arguments">{{ tool.function.arguments }}</pre>
173
- </div>
174
170
 
175
- <!-- Tool Output (Nested) -->
176
- <div v-if="getToolOutput(tool.id)" class="border-t border-gray-200 dark:border-gray-700">
177
- <div class="px-3 py-1.5 flex justify-between items-center border-b border-gray-200 dark:border-gray-800 bg-gray-50/30 dark:bg-gray-800">
178
- <div class="flex items-center gap-2 ">
179
- <svg class="size-3.5 text-gray-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
180
- <span class="text-[10px] uppercase tracking-wider text-gray-400 font-medium">Output</span>
181
- </div>
182
- <div v-if="hasJsonStructure(getToolOutput(tool.id).content)" class="flex items-center gap-2 text-[10px] uppercase tracking-wider font-medium select-none">
183
- <span @click="setPrefs({ toolFormat: 'text' })"
184
- class="cursor-pointer transition-colors"
185
- :class="prefs.toolFormat !== 'preview' ? 'text-gray-600 dark:text-gray-300' : 'text-gray-400 hover:text-gray-600 dark:hover:text-gray-300'">
186
- text
187
- </span>
188
- <span class="text-gray-300 dark:text-gray-700">|</span>
189
- <span @click="setPrefs({ toolFormat: 'preview' })"
190
- class="cursor-pointer transition-colors"
191
- :class="prefs.toolFormat == 'preview' ? 'text-gray-600 dark:text-gray-300' : 'text-gray-400 hover:text-gray-600 dark:hover:text-gray-300'">
192
- preview
193
- </span>
171
+ <!-- Tool Output (Nested) -->
172
+ <div v-if="getToolOutput(tool.id)" class="border-t border-gray-200 dark:border-gray-700">
173
+ <div class="px-3 py-1.5 flex justify-between items-center border-b border-gray-200 dark:border-gray-800 bg-gray-50/30 dark:bg-gray-800">
174
+ <div class="flex items-center gap-2 ">
175
+ <svg class="size-3.5 text-gray-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
176
+ <span class="text-[10px] uppercase tracking-wider text-gray-400 font-medium">Output</span>
177
+ </div>
178
+ <div v-if="hasJsonStructure(getToolOutput(tool.id).content)" class="flex items-center gap-2 text-[10px] uppercase tracking-wider font-medium select-none">
179
+ <span @click="setPrefs({ toolFormat: 'text' })"
180
+ class="cursor-pointer transition-colors"
181
+ :class="prefs.toolFormat !== 'preview' ? 'text-gray-600 dark:text-gray-300' : 'text-gray-400 hover:text-gray-600 dark:hover:text-gray-300'">
182
+ text
183
+ </span>
184
+ <span class="text-gray-300 dark:text-gray-700">|</span>
185
+ <span @click="setPrefs({ toolFormat: 'preview' })"
186
+ class="cursor-pointer transition-colors"
187
+ :class="prefs.toolFormat == 'preview' ? 'text-gray-600 dark:text-gray-300' : 'text-gray-400 hover:text-gray-600 dark:hover:text-gray-300'">
188
+ preview
189
+ </span>
190
+ </div>
194
191
  </div>
195
- </div>
196
- <div class="not-prose px-3 py-2">
197
- <pre v-if="prefs.toolFormat !== 'preview' || !hasJsonStructure(getToolOutput(tool.id).content)" class="tool-output">{{ getToolOutput(tool.id).content }}</pre>
198
- <div v-else class="text-xs">
199
- <HtmlFormat v-if="tryParseJson(getToolOutput(tool.id).content)" :value="tryParseJson(getToolOutput(tool.id).content)" :classes="customHtmlClasses" />
200
- <div v-else class="text-gray-500 italic p-2">Invalid JSON content</div>
192
+ <div class="not-prose px-3 py-2">
193
+ <pre v-if="prefs.toolFormat !== 'preview' || !hasJsonStructure(getToolOutput(tool.id).content)" class="tool-output">{{ getToolOutput(tool.id).content }}</pre>
194
+ <div v-else class="text-xs">
195
+ <HtmlFormat v-if="tryParseJson(getToolOutput(tool.id).content)" :value="tryParseJson(getToolOutput(tool.id).content)" :classes="customHtmlClasses" />
196
+ <div v-else class="text-gray-500 italic p-2">Invalid JSON content</div>
197
+ </div>
201
198
  </div>
202
199
  </div>
203
200
  </div>
204
201
  </div>
205
- </div>
206
202
 
207
- <!-- Tool Output (Orphaned) -->
208
- <div v-if="message.role === 'tool' && !isToolLinked(message)" class="text-sm">
209
- <div class="flex items-center gap-2 mb-1 opacity-70">
210
- <div class="flex items-center text-xs font-mono font-medium text-gray-500 uppercase tracking-wider">
211
- <svg class="size-3 mr-1" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
212
- Tool Output
213
- </div>
214
- <div v-if="message.name" class="text-xs font-mono bg-gray-200 dark:bg-gray-700 px-1.5 rounded text-gray-700 dark:text-gray-300">
215
- {{ message.name }}
203
+ <!-- Tool Output (Orphaned) -->
204
+ <div v-if="message.role === 'tool' && !isToolLinked(message)" class="text-sm">
205
+ <div class="flex items-center gap-2 mb-1 opacity-70">
206
+ <div class="flex items-center text-xs font-mono font-medium text-gray-500 uppercase tracking-wider">
207
+ <svg class="size-3 mr-1" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
208
+ Tool Output
209
+ </div>
210
+ <div v-if="message.name" class="text-xs font-mono bg-gray-200 dark:bg-gray-700 px-1.5 rounded text-gray-700 dark:text-gray-300">
211
+ {{ message.name }}
212
+ </div>
213
+ <div v-if="message.tool_call_id" class="text-[10px] font-mono text-gray-400">
214
+ {{ message.tool_call_id.slice(0,8) }}
215
+ </div>
216
216
  </div>
217
- <div v-if="message.tool_call_id" class="text-[10px] font-mono text-gray-400">
218
- {{ message.tool_call_id.slice(0,8) }}
217
+ <div class="not-prose bg-white dark:bg-gray-900 rounded border border-gray-200 dark:border-gray-800 p-2 overflow-x-auto">
218
+ <pre class="tool-output">{{ message.content }}</pre>
219
219
  </div>
220
220
  </div>
221
- <div class="not-prose bg-white dark:bg-gray-900 rounded border border-gray-200 dark:border-gray-800 p-2 overflow-x-auto">
222
- <pre class="tool-output">{{ message.content }}</pre>
223
- </div>
224
- </div>
225
-
226
- <!-- Assistant Images -->
227
- <div v-if="message.images && message.images.length > 0" class="mt-2 flex flex-wrap gap-2">
228
- <template v-for="(img, i) in message.images" :key="i">
229
- <div v-if="img.type === 'image_url'" class="group relative cursor-pointer" @click="openLightbox(resolveUrl(img.image_url.url))">
230
- <img :src="resolveUrl(img.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]" />
231
- </div>
232
- </template>
233
- </div>
234
-
235
- <!-- Assistant Audios -->
236
- <div v-if="message.audios && message.audios.length > 0" class="mt-2 flex flex-wrap gap-2">
237
- <template v-for="(audio, i) in message.audios" :key="i">
238
- <div v-if="audio.type === 'audio_url'" 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">
239
- <audio controls :src="resolveUrl(audio.audio_url.url)" class="h-8 w-64"></audio>
240
- </div>
241
- </template>
242
- </div>
243
221
 
244
- <!-- User Message with separate attachments -->
245
- <div v-else-if="message.role !== 'assistant' && message.role !== 'tool'">
246
- <div v-html="$fmt.markdown(message.content)" class="prose prose-sm max-w-none dark:prose-invert break-words"></div>
247
-
248
- <!-- Attachments Grid -->
249
- <div v-if="hasAttachments(message)" class="mt-2 flex flex-wrap gap-2">
250
- <template v-for="(part, i) in getAttachments(message)" :key="i">
251
- <!-- Image -->
252
- <div v-if="part.type === 'image_url'" class="group relative cursor-pointer" @click="openLightbox(part.image_url.url)">
253
- <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]" />
222
+ <!-- Assistant Images -->
223
+ <div v-if="message.images && message.images.length > 0" class="mt-2 flex flex-wrap gap-2">
224
+ <template v-for="(img, i) in message.images" :key="i">
225
+ <div v-if="img.type === 'image_url'" class="group relative cursor-pointer" @click="openLightbox(resolveUrl(img.image_url.url))">
226
+ <img :src="resolveUrl(img.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]" />
254
227
  </div>
255
- <!-- Audio -->
256
- <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">
257
- <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>
258
- <audio controls :src="part.input_audio.data" class="h-8 w-48"></audio>
228
+ </template>
229
+ </div>
230
+
231
+ <!-- Assistant Audios -->
232
+ <div v-if="message.audios && message.audios.length > 0" class="mt-2 flex flex-wrap gap-2">
233
+ <template v-for="(audio, i) in message.audios" :key="i">
234
+ <div v-if="audio.type === 'audio_url'" 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">
235
+ <audio controls :src="resolveUrl(audio.audio_url.url)" class="h-8 w-64"></audio>
259
236
  </div>
260
- <!-- File -->
261
- <a v-else-if="part.type === 'file'" :href="part.file.file_data" target="_blank"
262
- 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">
263
- <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>
264
- <span class="max-w-xs truncate">{{ part.file.filename || 'Attachment' }}</span>
265
- </a>
266
237
  </template>
267
238
  </div>
239
+
240
+ <!-- User Message with separate attachments -->
241
+ <div v-else-if="message.role !== 'assistant' && message.role !== 'tool'">
242
+ <div v-html="$fmt.markdown(message.content)" class="prose prose-sm max-w-none dark:prose-invert break-words"></div>
243
+
244
+ <!-- Attachments Grid -->
245
+ <div v-if="hasAttachments(message)" class="mt-2 flex flex-wrap gap-2">
246
+ <template v-for="(part, i) in getAttachments(message)" :key="i">
247
+ <!-- Image -->
248
+ <div v-if="part.type === 'image_url'" class="group relative cursor-pointer" @click="openLightbox(part.image_url.url)">
249
+ <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]" />
250
+ </div>
251
+ <!-- Audio -->
252
+ <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">
253
+ <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>
254
+ <audio controls :src="part.input_audio.data" class="h-8 w-48"></audio>
255
+ </div>
256
+ <!-- File -->
257
+ <a v-else-if="part.type === 'file'" :href="part.file.file_data" target="_blank"
258
+ 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">
259
+ <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>
260
+ <span class="max-w-xs truncate">{{ part.file.filename || 'Attachment' }}</span>
261
+ </a>
262
+ </template>
263
+ </div>
264
+ </div>
265
+
266
+ <MessageUsage :message="message" :usage="getMessageUsage(message)" />
268
267
  </div>
269
268
 
270
- <MessageUsage :message="message" :usage="getMessageUsage(message)" />
269
+ <!-- Edit and Redo buttons (shown on hover for user messages, outside bubble) -->
270
+ <div v-if="message.role === 'user'" class="flex flex-col gap-2 opacity-0 group-hover:opacity-100 transition-opacity mt-1">
271
+ <button type="button" @click.stop="editMessage(message)"
272
+ class="whitespace-nowrap text-xs px-2 py-1 rounded text-gray-400 dark:text-gray-500 hover:text-green-600 dark:hover:text-green-400 hover:bg-green-50 dark:hover:bg-green-900/30 transition-all"
273
+ title="Edit message">
274
+ <svg class="size-4 inline mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
275
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
276
+ </svg>
277
+ Edit
278
+ </button>
279
+ <button type="button" @click.stop="redoMessage(message)"
280
+ class="whitespace-nowrap text-xs px-2 py-1 rounded text-gray-400 dark:text-gray-500 hover:text-blue-600 dark:hover:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/30 transition-all"
281
+ title="Redo message (clears all responses after this message and re-runs it)">
282
+ <svg class="size-4 inline mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
283
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
284
+ </svg>
285
+ Redo
286
+ </button>
287
+ </div>
271
288
  </div>
272
289
 
273
- <!-- Edit and Redo buttons (shown on hover for user messages, outside bubble) -->
274
- <div v-if="message.role === 'user'" class="flex flex-col gap-2 opacity-0 group-hover:opacity-100 transition-opacity mt-1">
275
- <button type="button" @click.stop="editMessage(message)"
276
- class="whitespace-nowrap text-xs px-2 py-1 rounded text-gray-400 dark:text-gray-500 hover:text-green-600 dark:hover:text-green-400 hover:bg-green-50 dark:hover:bg-green-900/30 transition-all"
277
- title="Edit message">
278
- <svg class="size-4 inline mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
279
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
280
- </svg>
281
- Edit
282
- </button>
283
- <button type="button" @click.stop="redoMessage(message)"
284
- class="whitespace-nowrap text-xs px-2 py-1 rounded text-gray-400 dark:text-gray-500 hover:text-blue-600 dark:hover:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/30 transition-all"
285
- title="Redo message (clears all responses after this message and re-runs it)">
286
- <svg class="size-4 inline mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
287
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
288
- </svg>
289
- Redo
290
- </button>
290
+ <div v-if="currentThread.stats && currentThread.stats.outputTokens" class="text-center text-gray-500 dark:text-gray-400 text-sm">
291
+ <span :title="$fmt.statsTitle(currentThread.stats)">
292
+ {{ currentThread.stats.cost ? $fmt.costLong(currentThread.stats.cost) + ' for ' : '' }} {{ $fmt.humanifyNumber(currentThread.stats.inputTokens) }} → {{ $fmt.humanifyNumber(currentThread.stats.outputTokens) }} tokens over {{ currentThread.stats.requests }} request{{currentThread.stats.requests===1?'':'s'}} in {{ $fmt.humanifyMs(currentThread.stats.duration) }}
293
+ </span>
291
294
  </div>
292
- </div>
293
-
294
- <div v-if="currentThread.stats && currentThread.stats.outputTokens" class="text-center text-gray-500 dark:text-gray-400 text-sm">
295
- <span :title="$fmt.statsTitle(currentThread.stats)">
296
- {{ currentThread.stats.cost ? $fmt.costLong(currentThread.stats.cost) + ' for ' : '' }} {{ $fmt.humanifyNumber(currentThread.stats.inputTokens) }} → {{ $fmt.humanifyNumber(currentThread.stats.outputTokens) }} tokens over {{ currentThread.stats.requests }} request{{currentThread.stats.requests===1?'':'s'}} in {{ $fmt.humanifyMs(currentThread.stats.duration) }}
297
- </span>
298
- </div>
299
295
 
300
- <!-- Loading indicator -->
301
- <div v-if="$threads.watchingThread" class="flex items-start space-x-3 group">
302
- <!-- Avatar outside the bubble -->
303
- <div class="flex-shrink-0">
304
- <div class="w-8 h-8 rounded-full bg-gray-600 dark:bg-gray-500 text-white flex items-center justify-center text-sm font-medium">
305
- AI
296
+ <!-- Loading indicator -->
297
+ <div v-if="$threads.watchingThread" class="flex items-start space-x-3 group">
298
+ <!-- Avatar outside the bubble -->
299
+ <div class="flex-shrink-0">
300
+ <div class="w-8 h-8 rounded-full bg-gray-600 dark:bg-gray-500 text-white flex items-center justify-center text-sm font-medium">
301
+ AI
302
+ </div>
306
303
  </div>
307
- </div>
308
304
 
309
- <!-- Loading bubble -->
310
- <div class="rounded-lg px-4 py-3 bg-gray-100 dark:bg-gray-800 border border-gray-200 dark:border-gray-700">
311
- <div class="flex space-x-1">
312
- <div class="w-2 h-2 bg-gray-400 dark:bg-gray-500 rounded-full animate-bounce"></div>
313
- <div class="w-2 h-2 bg-gray-400 dark:bg-gray-500 rounded-full animate-bounce" style="animation-delay: 0.1s"></div>
314
- <div class="w-2 h-2 bg-gray-400 dark:bg-gray-500 rounded-full animate-bounce" style="animation-delay: 0.2s"></div>
305
+ <!-- Loading bubble -->
306
+ <div class="rounded-lg px-4 py-3 bg-gray-100 dark:bg-gray-800 border border-gray-200 dark:border-gray-700">
307
+ <div class="flex space-x-1">
308
+ <div class="w-2 h-2 bg-gray-400 dark:bg-gray-500 rounded-full animate-bounce"></div>
309
+ <div class="w-2 h-2 bg-gray-400 dark:bg-gray-500 rounded-full animate-bounce" style="animation-delay: 0.1s"></div>
310
+ <div class="w-2 h-2 bg-gray-400 dark:bg-gray-500 rounded-full animate-bounce" style="animation-delay: 0.2s"></div>
311
+ </div>
315
312
  </div>
316
- </div>
317
313
 
318
- <!-- Cancel button -->
319
- <button type="button" @click="$threads.cancelThread()"
320
- class="px-3 py-1 rounded text-sm 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 border border-transparent hover:border-red-300 dark:hover:border-red-600 transition-all"
321
- title="Cancel request">
322
- cancel
323
- </button>
324
- </div>
314
+ <!-- Cancel button -->
315
+ <button type="button" @click="$threads.cancelThread()"
316
+ class="px-3 py-1 rounded text-sm 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 border border-transparent hover:border-red-300 dark:hover:border-red-600 transition-all"
317
+ title="Cancel request">
318
+ cancel
319
+ </button>
320
+ </div>
325
321
 
326
- <!-- Thread error message bubble -->
327
- <div v-if="currentThread?.error" class="mt-8 flex items-center space-x-3">
328
- <!-- Avatar outside the bubble -->
329
- <div class="flex-shrink-0">
330
- <div class="size-8 rounded-full bg-red-600 dark:bg-red-500 text-white flex items-center justify-center text-lg font-bold">
331
- !
322
+ <!-- Thread error message bubble -->
323
+ <div v-if="currentThread?.error" class="mt-8 flex items-center space-x-3">
324
+ <!-- Avatar outside the bubble -->
325
+ <div class="flex-shrink-0">
326
+ <div class="size-8 rounded-full bg-red-600 dark:bg-red-500 text-white flex items-center justify-center text-lg font-bold">
327
+ !
328
+ </div>
332
329
  </div>
333
- </div>
334
- <!-- Error bubble -->
335
- <div class="max-w-[85%] rounded-lg px-3 py-1 bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-800 text-red-800 dark:text-red-200 shadow-sm">
336
- <div class="flex items-start space-x-2">
337
- <div class="flex-1 min-w-0">
338
- <div v-if="currentThread.error" class="text-base mb-1">{{ currentThread.error }}</div>
330
+ <!-- Error bubble -->
331
+ <div class="max-w-[85%] rounded-lg px-3 py-1 bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-800 text-red-800 dark:text-red-200 shadow-sm">
332
+ <div class="flex items-start space-x-2">
333
+ <div class="flex-1 min-w-0">
334
+ <div v-if="currentThread.error" class="text-base mb-1">{{ currentThread.error }}</div>
335
+ </div>
339
336
  </div>
340
337
  </div>
341
338
  </div>
342
- </div>
343
339
 
344
- <!-- Error message bubble -->
345
- <div v-if="$state.error" class="mt-8 flex items-start space-x-3">
346
- <!-- Avatar outside the bubble -->
347
- <div class="flex-shrink-0">
348
- <div class="size-8 rounded-full bg-red-600 dark:bg-red-500 text-white flex items-center justify-center text-lg font-bold">
349
- !
340
+ <!-- Error message bubble -->
341
+ <div v-if="$state.error" class="mt-8 flex items-start space-x-3">
342
+ <!-- Avatar outside the bubble -->
343
+ <div class="flex-shrink-0">
344
+ <div class="size-8 rounded-full bg-red-600 dark:bg-red-500 text-white flex items-center justify-center text-lg font-bold">
345
+ !
346
+ </div>
350
347
  </div>
351
- </div>
352
348
 
353
- <!-- Error bubble -->
354
- <div class="max-w-[85%] rounded-lg px-4 py-3 bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-800 text-red-800 dark:text-red-200 shadow-sm">
355
- <div class="flex items-start space-x-2">
356
- <div class="flex-1 min-w-0">
357
- <div class="flex justify-between items-start">
358
- <div class="text-base font-medium mb-1">{{ $state.error?.errorCode || 'Error' }}</div>
359
- <button type="button" @click="$ctx.clearError()" title="Clear Error"
360
- class="text-red-400 dark:text-red-300 hover:text-red-600 dark:hover:text-red-100 flex-shrink-0">
361
- <svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
362
- <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>
363
- </svg>
364
- </button>
365
- </div>
366
- <div v-if="$state.error?.message" class="text-base mb-1">{{ $state.error.message }}</div>
367
- <div v-if="$state.error?.stackTrace" class="mt-2 text-sm whitespace-pre-wrap break-words max-h-80 overflow-y-auto font-mono p-2 border border-red-200/70 dark:border-red-800/70">
368
- {{ $state.error.stackTrace }}
349
+ <!-- Error bubble -->
350
+ <div class="max-w-[85%] rounded-lg px-4 py-3 bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-800 text-red-800 dark:text-red-200 shadow-sm">
351
+ <div class="flex items-start space-x-2">
352
+ <div class="flex-1 min-w-0">
353
+ <div class="flex justify-between items-start">
354
+ <div class="text-base font-medium mb-1">{{ $state.error?.errorCode || 'Error' }}</div>
355
+ <button type="button" @click="$ctx.clearError()" title="Clear Error"
356
+ class="text-red-400 dark:text-red-300 hover:text-red-600 dark:hover:text-red-100 flex-shrink-0">
357
+ <svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
358
+ <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>
359
+ </svg>
360
+ </button>
361
+ </div>
362
+ <div v-if="$state.error?.message" class="text-base mb-1">{{ $state.error.message }}</div>
363
+ <div v-if="$state.error?.stackTrace" class="mt-2 text-sm whitespace-pre-wrap break-words max-h-80 overflow-y-auto font-mono p-2 border border-red-200/70 dark:border-red-800/70">
364
+ {{ $state.error.stackTrace }}
365
+ </div>
369
366
  </div>
370
367
  </div>
371
368
  </div>
372
369
  </div>
373
370
  </div>
371
+ <ThreadFooter v-if="$threads.threadDetails.value[currentThread.id]" :thread="$threads.threadDetails.value[currentThread.id]" />
374
372
  </div>
375
373
  </div>
376
-
377
374
  </div>
378
375
 
379
376
  <!-- Input Area -->