Skip to content

Commit b285dda

Browse files
committed
Add support for themes
1 parent 2c14e39 commit b285dda

File tree

34 files changed

+3477
-685
lines changed

34 files changed

+3477
-685
lines changed

llms/extensions/analytics/ui/index.mjs

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,17 @@ export const colors = [
1919

2020
const MonthSelector = {
2121
template: `
22-
<div class="flex flex-col sm:flex-row gap-2 sm:gap-4 items-stretch sm:items-center w-full sm:w-auto">
22+
<div class="pb-1 flex flex-col sm:flex-row gap-2 sm:gap-4 items-stretch sm:items-center w-full sm:w-auto">
2323
<!-- Months Row -->
24-
<div class="flex gap-1 sm:gap-2 flex-wrap justify-center overflow-x-auto">
24+
<div class="flex gap-1 sm:gap-2 flex-wrap justify-center">
2525
<template v-for="month in availableMonthsForYear" :key="month">
2626
<span v-if="selectedMonth === month"
27-
class="text-xs leading-5 font-semibold bg-indigo-600 text-white rounded-full py-1 px-2 sm:px-3 flex items-center space-x-2 whitespace-nowrap">
27+
class="text-xs leading-5 font-semibold py-1 px-2 sm:px-3 flex items-center space-x-2 whitespace-nowrap" :class="[$styles.tagButtonActive,$styles.tagButtonSmall]">
2828
<span class="hidden sm:inline">{{ new Date(selectedYear + '-' + month.toString().padStart(2,'0') + '-01').toLocaleString('default', { month: 'long' }) }}</span>
2929
<span class="sm:hidden">{{ new Date(selectedYear + '-' + month.toString().padStart(2,'0') + '-01').toLocaleString('default', { month: 'short' }) }}</span>
3030
</span>
3131
<button v-else type="button"
32-
class="text-xs leading-5 font-semibold bg-slate-400/10 rounded-full py-1 px-2 sm:px-3 flex items-center space-x-2 hover:bg-slate-400/20 dark:highlight-white/5 whitespace-nowrap"
32+
class="text-xs leading-5 font-semibold py-1 px-2 sm:px-3 flex items-center space-x-2 whitespace-nowrap" :class="[$styles.tagButton,$styles.tagButtonSmall]"
3333
@click="updateSelection(selectedYear, month)">
3434
{{ new Date(selectedYear + '-' + month.toString().padStart(2,'0') + '-01').toLocaleString('default', { month: 'short' }) }}
3535
</button>
@@ -38,7 +38,7 @@ const MonthSelector = {
3838
3939
<!-- Year Dropdown -->
4040
<select :value="selectedYear" @change="(e) => updateSelection(parseInt(e.target.value), selectedMonth)"
41-
class="border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-900 text-gray-700 dark:text-gray-300 rounded-md text-sm font-medium hover:bg-gray-50 dark:hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-indigo-500 flex-shrink-0">
41+
class="rounded-md text-sm font-medium flex-shrink-0 transition-colors" :class="[$styles.bgSelect, $styles.textInput, $styles.borderInput]">
4242
<option v-for="year in availableYears" :key="year" :value="year">
4343
{{ year }}
4444
</option>
@@ -108,11 +108,11 @@ export const Analytics = {
108108
template: `
109109
<div class="flex flex-col w-full">
110110
<!-- Header -->
111-
<div class="border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 px-2 sm:px-4 py-3">
111+
<div class="px-2 sm:px-4 py-3">
112112
<div
113113
:class="!$ai.isSidebarOpen ? 'pl-3' : ''"
114114
class="max-w-6xl mx-auto flex flex-col sm:flex-row items-stretch sm:items-center justify-between gap-3">
115-
<h2 class="text-lg font-semibold text-gray-900 dark:text-gray-100 flex-shrink-0">
115+
<h2 class="text-lg font-semibold flex-shrink-0" :class="[$styles.heading]">
116116
<RouterLink to="/analytics">Analytics</RouterLink>
117117
</h2>
118118
<MonthSelector :dailyData="allDailyData" />
@@ -150,7 +150,7 @@ export const Analytics = {
150150
</div>
151151
152152
<!-- Content -->
153-
<div class="flex-1 bg-gray-50 dark:bg-gray-900" :class="activeTab === 'activity' ? 'p-0' : 'p-4'">
153+
<div class="flex-1" :class="[activeTab === 'activity' ? 'p-0' : 'p-4', $styles.bgPage]">
154154
155155
<div :class="activeTab === 'activity' ? '' : 'max-w-6xl mx-auto'">
156156
<!-- Stats Summary (hidden for Activity tab) -->
@@ -180,7 +180,7 @@ export const Analytics = {
180180
<h3 class="text-sm sm:text-lg font-semibold text-gray-900 dark:text-gray-100">
181181
{{ new Date(selectedDay).toLocaleDateString(undefined, { year: 'numeric', month: 'long' }) }}
182182
</h3>
183-
<select v-model="costChartType" class="px-3 pr-6 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-900 text-gray-700 dark:text-gray-300 rounded-md text-sm font-medium hover:bg-gray-50 dark:hover:bg-gray-800 flex-shrink-0">
183+
<select v-model="costChartType" class="px-3 pr-6 py-2 border rounded-md text-sm font-medium flex-shrink-0" :class="[$styles.bgSelect, $styles.textInput, $styles.borderInput]">
184184
<option value="bar">Bar Chart</option>
185185
<option value="line">Line Chart</option>
186186
</select>
@@ -288,7 +288,7 @@ export const Analytics = {
288288
<div class="flex-shrink-0 border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 px-3 sm:px-6 py-4">
289289
<div class="flex flex-wrap gap-2 sm:gap-4 items-end">
290290
<div class="flex flex-col flex-1 min-w-[120px] sm:flex-initial">
291-
<select v-model="selectedModel" class="px-3 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 w-full">
291+
<select v-model="selectedModel" class="px-3 py-2 border rounded-md text-sm font-medium flex-shrink-0" :class="[$styles.bgSelect, $styles.textInput, $styles.borderInput]">
292292
<option value="">All Models</option>
293293
<option v-for="model in filterOptions.models" :key="model" :value="model">
294294
{{ model }}
@@ -297,7 +297,7 @@ export const Analytics = {
297297
</div>
298298
299299
<div class="flex flex-col flex-1 min-w-[120px] sm:flex-initial">
300-
<select v-model="selectedProvider" class="px-3 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 w-full">
300+
<select v-model="selectedProvider" class="px-3 py-2 border rounded-md text-sm font-medium flex-shrink-0" :class="[$styles.bgSelect, $styles.textInput, $styles.borderInput]">
301301
<option value="">All Providers</option>
302302
<option v-for="provider in filterOptions.providers" :key="provider" :value="provider">
303303
{{ provider }}
@@ -306,7 +306,7 @@ export const Analytics = {
306306
</div>
307307
308308
<div class="flex flex-col flex-1 min-w-[140px] sm:flex-initial">
309-
<select v-model="sortBy" class="px-3 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 w-full">
309+
<select v-model="sortBy" class="px-3 py-2 border rounded-md text-sm font-medium flex-shrink-0" :class="[$styles.bgSelect, $styles.textInput, $styles.borderInput]">
310310
<option value="createdAt">Date (Newest)</option>
311311
<option value="cost">Cost (Highest)</option>
312312
<option value="duration">Duration (Longest)</option>
@@ -393,7 +393,7 @@ export const Analytics = {
393393
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto"></div>
394394
</div>
395395
396-
<div v-if="!activityHasMore && activityRequests.length > 0" class="px-6 py-8 text-center text-gray-500 dark:text-gray-400 text-sm">
396+
<div v-if="!activityHasMore && activityRequests.length > 0" class="px-6 py-8 text-sm" :class="[$styles.muted]">
397397
No more requests to load
398398
</div>
399399
</div>

llms/extensions/app/__init__.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import asyncio
22
import io
33
import json
4+
import mimetypes
45
import os
56
import time
67
from datetime import datetime
@@ -614,6 +615,65 @@ async def upload_agent_avatar(request):
614615

615616
ctx.add_post("/agents/avatar", upload_agent_avatar)
616617

618+
def get_theme_roots(request):
619+
themes_dirs = [os.path.join(os.path.dirname(__file__), "themes"), os.path.join(ctx.get_user_path(), "themes")]
620+
user = ctx.get_username(request)
621+
if user:
622+
themes_dirs.append(os.path.join(ctx.get_user_path(user), "themes"))
623+
return themes_dirs
624+
625+
# THEMES
626+
async def get_themes(request):
627+
themes = {}
628+
629+
themes_dirs = get_theme_roots(request)
630+
for themes_dir in themes_dirs:
631+
if os.path.exists(themes_dir):
632+
for theme_name in os.listdir(themes_dir):
633+
theme_path = os.path.join(themes_dir, theme_name)
634+
styles_path = os.path.join(theme_path, "theme.json")
635+
if os.path.isdir(theme_path) and os.path.exists(styles_path):
636+
try:
637+
with open(styles_path, encoding="utf-8") as f:
638+
themes[theme_name] = json.load(f)
639+
except Exception as e:
640+
if hasattr(ctx, "err"):
641+
ctx.err(f"Failed to load theme {theme_name}", e)
642+
return web.json_response(themes)
643+
644+
ctx.add_get("/themes", get_themes)
645+
646+
async def get_theme_file(request):
647+
theme_name = request.match_info.get("theme")
648+
file_name = request.match_info.get("file_name")
649+
650+
def get_file_response(path):
651+
if not os.path.exists(path):
652+
return None
653+
654+
content_type, _ = mimetypes.guess_type(path)
655+
headers = {}
656+
if content_type:
657+
headers["Content-Type"] = content_type
658+
return web.FileResponse(path, headers=headers)
659+
660+
themes_dirs = get_theme_roots(request)
661+
# Search themes in reverse order to return the last overridden theme
662+
for themes_dir in reversed(themes_dirs):
663+
theme_path = os.path.join(themes_dir, theme_name)
664+
if os.path.isdir(theme_path) and os.path.exists(os.path.join(theme_path, "theme.json")):
665+
response = get_file_response(os.path.join(theme_path, "ui", file_name))
666+
if response:
667+
return response
668+
669+
response = get_file_response(os.path.join(os.path.dirname(__file__), "themes", theme_name, "ui", file_name))
670+
if response:
671+
return response
672+
673+
return web.HTTPNotFound()
674+
675+
ctx.add_get("/themes/{theme}/ui/{file_name}", get_theme_file)
676+
617677
async def chat_request(openai_request, context):
618678
nohistory = context.get("nohistory")
619679
chat = openai_request
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
{
2+
"preview": {
3+
"chromeBorder": "border-blue-500/60",
4+
"bgBody": "bg-gray-950",
5+
"bgSidebar": "bg-gray-950",
6+
"icon": "text-blue-400",
7+
"heading": "text-blue-100"
8+
},
9+
"vars": {
10+
"colorScheme": "dark",
11+
"--background-image": "url(/themes/blue_smoke/ui/bg.webp)",
12+
"--scrollbar-track-bg": "#0a0a1a",
13+
"--scrollbar-thumb-bg": "#1e3a5f"
14+
},
15+
"styles": {
16+
"chromeBorder": "border-blue-500/60",
17+
"appInner": "bg-black/50",
18+
"bgBody": "bg-black/20",
19+
"bgSidebar": "bg-black/50 border-r border-blue-500/30",
20+
"bgChat": "bg-transparent",
21+
"bgPage": "",
22+
"bgInput": "bg-blue-950/80 focus:bg-black/70",
23+
"textInput": "text-blue-50 placeholder-blue-300/50",
24+
"borderInput": "border-blue-500/60 focus:border-blue-500/60 focus:ring-blue-500/60 shadow-inner",
25+
"labelInput": "text-blue-300",
26+
"helpInput": "text-blue-400/80",
27+
"draggingInput": "border-blue-400 bg-blue-900/40 ring-1 ring-blue-400 shadow-[0_0_20px_rgba(59,130,246,0.2)]",
28+
"dropdownButton": "border border-blue-500/50 bg-black/50 hover:bg-blue-900/60 text-blue-100 focus:outline-none transition-colors",
29+
"bgPopover": "bg-black/90 backdrop-blur-lg border border-blue-500/40 shadow-xl shadow-black/50",
30+
"popoverButton": "hover:bg-blue-800/60 text-blue-100 transition-colors",
31+
"popoverButtonActive": "bg-blue-800/80 text-white",
32+
"codeTag": "text-blue-200 bg-blue-900/60 border border-blue-600/60",
33+
"codeTagStrong": "border border-blue-500/60 text-blue-100 bg-blue-800/60 shadow-[0_0_10px_rgba(59,130,246,0.1)]",
34+
"tagButton": "cursor-pointer border border-transparent text-blue-300 hover:text-blue-100 hover:bg-blue-800/40 transition-colors",
35+
"tagButtonActive": "cursor-default border border-blue-500/70 text-blue-50 bg-blue-800/50 shadow-[0_0_10px_rgba(59,130,246,0.2)]",
36+
"tagButtonStrongActive": "bg-green-600/40 text-green-50 border-green-400/80 shadow-[0_0_15px_rgba(59,130,246,0.3)]",
37+
"tagLabel": "text-blue-300 border border-blue-600/60",
38+
"tagLabelHover": "hover:bg-blue-800/60 hover:text-blue-100 hover:border-blue-500/80 transition-colors",
39+
"panel": "border-blue-500/30 bg-black/40 backdrop-blur-md",
40+
"card": "rounded-xl bg-black/30 border border-blue-500/40 backdrop-blur-sm hover:border-blue-400/60 transition-all",
41+
"cardTitle": "border-b border-blue-500/40 bg-black/30 text-blue-50 rounded-t-xl",
42+
"cardActive": "rounded-xl shadow-lg shadow-blue-500/10 bg-black/40 border border-blue-400/60",
43+
"cardActiveTitleBar": "border-b border-blue-400/60 bg-blue-900/40 text-white rounded-t-xl",
44+
"textBlock": "backdrop-blur-lg",
45+
"primaryButton": "border border-transparent shadow shadow-blue-900/50 text-white bg-blue-600 hover:bg-blue-500 hover:shadow-blue-500/25 hover:shadow-lg focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-950 focus:ring-blue-400 disabled:opacity-50 disabled:cursor-not-allowed rounded-lg transition-all",
46+
"secondaryButton": "text-blue-200 bg-blue-900/40 border border-blue-600/60 hover:bg-blue-800 hover:border-blue-500/80 hover:text-white rounded-lg transition-all",
47+
"textLink": "text-blue-300 underline decoration-blue-500/30 hover:text-blue-100 hover:decoration-blue-400 transition-colors",
48+
"icon": "text-blue-400/80",
49+
"iconHover": "hover:text-blue-100 hover:bg-blue-800/50 transition-colors rounded-lg",
50+
"iconActive": "text-blue-200 bg-blue-800/60 shadow-[0_0_10px_rgba(59,130,246,0.15)] rounded-lg",
51+
"chatButton": "border border-blue-500/50 text-blue-100 bg-blue-900/60 hover:bg-blue-800/70 hover:border-blue-400/60 disabled:text-blue-400 disabled:cursor-not-allowed disabled:border-blue-700/30 disabled:bg-black/30 transition-all shadow-sm rounded-lg",
52+
"voiceButtonDefault": "text-blue-400 hover:text-white hover:bg-blue-800/70 hover:border-blue-400/60 transition-all",
53+
"voiceButtonRecording": "border bg-red-900/30 border-red-600 text-red-400 animate-pulse",
54+
"voiceButtonProcessing": "bg-blue-800/80 text-blue-200 border border-blue-500/60 animate-spin",
55+
"threadItemActiveBorder": "border-blue-500",
56+
"threadItemActive": "bg-blue-900/30 border-blue-500/50 shadow-inner",
57+
"threadItem": "border-transparent text-blue-200 hover:bg-blue-900/20 hover:text-blue-50 transition-colors",
58+
"messageUser": "bg-blue-800/40 text-blue-50 border border-blue-400/40 shadow-md shadow-black/30 backdrop-blur-sm",
59+
"messageAssistant": "bg-black/30 text-blue-50 border border-blue-400/20 shadow-md shadow-black/20 backdrop-blur-sm"
60+
}
61+
}
230 KB
Loading
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"vars": {
3+
"colorScheme": "dark"
4+
}
5+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"vars": {
3+
"colorScheme": "light"
4+
}
5+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
{
2+
"vars": {
3+
"colorScheme": "light",
4+
"--background-image": "url(/themes/light_sky/ui/bg.webp)",
5+
"--background": "#ffffff",
6+
"--border": "#e2e8f0",
7+
"--input": "#e2e8f0",
8+
"--scrollbar-thumb-bg": "#bae6fd66",
9+
"--scrollbar-track-bg": "#e0f2fe44",
10+
"--primary-bg": "#ffffff",
11+
"--secondary-bg": "#f0f9ff",
12+
"--secondary-border": "#bae6fd",
13+
"--tw-prose-counters": "#7dd3fc",
14+
"--tw-prose-bullets": "#7dd3fc"
15+
},
16+
"styles": {
17+
"chromeBorder": "border-sky-300/80",
18+
"app": "bg-[image:var(--background-image)] bg-cover",
19+
"appInner": "bg-white/90 backdrop-blur-lg shadow-inner",
20+
"highlighted": "text-sky-600",
21+
"linkHover": "hover:text-sky-700 group-hover:text-sky-700",
22+
"bgBody": "bg-white",
23+
"bgSidebar": "bg-sky-50/40",
24+
"bgChat": "bg-white/40",
25+
"bgPage": "bg-white/40",
26+
"heading": "text-sky-900",
27+
"muted": "text-sky-900/60",
28+
"icon": "text-sky-800/70",
29+
"iconHover": "hover:text-sky-900 transition-colors",
30+
"iconActive": "bg-blue-300/30 text-sky-900",
31+
"chatButton": "border border-sky-300 text-sky-700 bg-white hover:bg-sky-50 disabled:text-sky-400 disabled:cursor-not-allowed disabled:border-sky-200 transition-all shadow-sm hover:shadow",
32+
"voiceButtonDefault": "bg-white text-sky-500 hover:text-sky-600 shadow-sm",
33+
"borderInput": "border-sky-300 focus:border-sky-500 focus:ring-sky-500 bg-white focus:bg-white transition-colors",
34+
"secondaryButton": "text-sky-700 bg-white border border-sky-300 hover:bg-sky-50 transition-colors shadow-sm rounded-md",
35+
"dropdownButton": "border border-sky-300/80 bg-white hover:bg-sky-50 text-sky-900 focus:outline-none",
36+
"bgPopover": "bg-white/95 backdrop-blur-xl border border-sky-200 shadow-lg",
37+
"textBlock": "backdrop-blur-sm",
38+
"threadItemActiveBorder": "border-sky-400",
39+
"threadItemActive": "bg-sky-100/80 border-sky-400 shadow-sm",
40+
"threadItem": "border-transparent hover:bg-sky-50 transition-colors",
41+
"tagButtonGroup": "rounded-xl bg-sky-700/10",
42+
"tagButton": "cursor-pointer border border-transparent bg-white/90 backdrop-blur text-slate-600 border-sky-200 hover:border-sky-300 hover:bg-white transition-all",
43+
"tagButtonActive": "cursor-default border border-sky-400 text-sky-800 bg-sky-100",
44+
"tagButtonStrongActive": "bg-sky-500 text-white border-sky-600 shadow-md",
45+
"tagLabel": "text-sky-900 border border-sky-300 bg-sky-50/50",
46+
"tagLabelHover": "hover:border-sky-400 hover:bg-sky-100 transition-colors",
47+
"panel": "border-sky-300/50 bg-white/90 backdrop-blur-md shadow-sm",
48+
"card": "rounded-lg bg-white/95 backdrop-blur-sm border border-sky-300/80 shadow-sm hover:shadow transition-shadow",
49+
"messageUser": "bg-white/95 backdrop-blur-sm text-slate-800 border border-sky-300 shadow-sm",
50+
"messageAssistant": "bg-sky-50/90 text-sky-950 border border-sky-300 shadow-sm"
51+
}
52+
}
92.1 KB
Loading

0 commit comments

Comments
 (0)