Spaces:
Running
Running
Upload folder using huggingface_hub
Browse files- main.py +4 -1
- static/dashboard.html +146 -18
- supabase_provider_configs.sql +59 -0
main.py
CHANGED
|
@@ -33,7 +33,10 @@ app.mount("/static", StaticFiles(directory="static"), name="static")
|
|
| 33 |
async def get_config():
|
| 34 |
return {
|
| 35 |
"supabase_url": os.environ.get("SUPABASE_URL", "https://znhglkwefxdhgajvrqmb.supabase.co"),
|
| 36 |
-
"supabase_key": os.environ.get("SUPABASE_KEY")
|
|
|
|
|
|
|
|
|
|
| 37 |
}
|
| 38 |
|
| 39 |
@app.get("/")
|
|
|
|
| 33 |
async def get_config():
|
| 34 |
return {
|
| 35 |
"supabase_url": os.environ.get("SUPABASE_URL", "https://znhglkwefxdhgajvrqmb.supabase.co"),
|
| 36 |
+
"supabase_key": os.environ.get("SUPABASE_KEY"),
|
| 37 |
+
"default_base_url": os.environ.get("DEFAULT_BASE_URL", "https://router.huggingface.co/v1"),
|
| 38 |
+
"default_api_key": os.environ.get("DEFAULT_API_KEY", ""),
|
| 39 |
+
"default_model": os.environ.get("DEFAULT_MODEL", "gpt-3.5-turbo"),
|
| 40 |
}
|
| 41 |
|
| 42 |
@app.get("/")
|
static/dashboard.html
CHANGED
|
@@ -246,6 +246,17 @@
|
|
| 246 |
class="bg-red-600 hover:bg-red-700 text-white px-2 rounded text-xs">Del</button>
|
| 247 |
</div>
|
| 248 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 249 |
<div>
|
| 250 |
<label class="block text-xs font-semibold text-gray-400 mb-1 uppercase">API Key</label>
|
| 251 |
<input type="password" id="chat-api-key" placeholder="sk-..."
|
|
@@ -549,6 +560,18 @@
|
|
| 549 |
window.location.href = '/';
|
| 550 |
});
|
| 551 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 552 |
loadProviders();
|
| 553 |
await loadChatHistoryList();
|
| 554 |
createTerminalTab(); // Init first tab
|
|
@@ -1036,11 +1059,51 @@
|
|
| 1036 |
} catch (e) { alert(e.message); }
|
| 1037 |
}
|
| 1038 |
|
| 1039 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1040 |
const select = document.getElementById('saved-providers');
|
| 1041 |
const saved = JSON.parse(localStorage.getItem('chat_providers') || '{}');
|
|
|
|
| 1042 |
select.innerHTML = '<option value="">Select...</option>';
|
| 1043 |
-
Object.keys(saved).forEach(k => {
|
| 1044 |
const opt = document.createElement('option');
|
| 1045 |
opt.value = k;
|
| 1046 |
opt.innerText = k;
|
|
@@ -1048,39 +1111,104 @@
|
|
| 1048 |
});
|
| 1049 |
}
|
| 1050 |
|
| 1051 |
-
function
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1052 |
const name = prompt("Name:");
|
| 1053 |
if (!name) return;
|
|
|
|
| 1054 |
const config = {
|
| 1055 |
apiKey: document.getElementById('chat-api-key').value,
|
| 1056 |
baseUrl: document.getElementById('chat-base-url').value,
|
| 1057 |
model: document.getElementById('chat-model').value
|
| 1058 |
};
|
| 1059 |
-
|
| 1060 |
-
|
| 1061 |
-
|
| 1062 |
-
|
| 1063 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1064 |
}
|
| 1065 |
|
| 1066 |
-
function deleteProvider() {
|
| 1067 |
const name = document.getElementById('saved-providers').value;
|
| 1068 |
if (!name) return;
|
| 1069 |
if (!confirm("Delete?")) return;
|
| 1070 |
-
|
| 1071 |
-
|
| 1072 |
-
|
| 1073 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1074 |
}
|
| 1075 |
|
| 1076 |
function loadSelectedProvider(name) {
|
| 1077 |
if (!name) return;
|
| 1078 |
-
const
|
| 1079 |
-
const config = saved[name];
|
| 1080 |
if (config) {
|
| 1081 |
-
document.getElementById('chat-api-key').value = config.apiKey;
|
| 1082 |
-
document.getElementById('chat-base-url').value = config.baseUrl;
|
| 1083 |
-
document.getElementById('chat-model').value = config.model;
|
| 1084 |
}
|
| 1085 |
}
|
| 1086 |
|
|
|
|
| 246 |
class="bg-red-600 hover:bg-red-700 text-white px-2 rounded text-xs">Del</button>
|
| 247 |
</div>
|
| 248 |
</div>
|
| 249 |
+
<div>
|
| 250 |
+
<label class="block text-xs font-semibold text-gray-400 mb-1 uppercase">Provider Preset</label>
|
| 251 |
+
<div class="flex gap-2">
|
| 252 |
+
<select id="provider-presets" onchange="applyProviderPreset(this.value)"
|
| 253 |
+
class="flex-grow bg-gray-700 text-sm rounded border border-gray-600 p-2 text-white outline-none focus:border-blue-500">
|
| 254 |
+
<option value="">Select preset...</option>
|
| 255 |
+
</select>
|
| 256 |
+
<button onclick="applyProviderPreset(document.getElementById('provider-presets').value)"
|
| 257 |
+
class="bg-gray-600 hover:bg-gray-700 text-white px-2 rounded text-xs">Apply</button>
|
| 258 |
+
</div>
|
| 259 |
+
</div>
|
| 260 |
<div>
|
| 261 |
<label class="block text-xs font-semibold text-gray-400 mb-1 uppercase">API Key</label>
|
| 262 |
<input type="password" id="chat-api-key" placeholder="sk-..."
|
|
|
|
| 560 |
window.location.href = '/';
|
| 561 |
});
|
| 562 |
|
| 563 |
+
initProviderPresets();
|
| 564 |
+
// Apply defaults if fields are empty
|
| 565 |
+
if (config.default_base_url && !document.getElementById('chat-base-url').value) {
|
| 566 |
+
document.getElementById('chat-base-url').value = config.default_base_url;
|
| 567 |
+
}
|
| 568 |
+
if (config.default_api_key && !document.getElementById('chat-api-key').value) {
|
| 569 |
+
document.getElementById('chat-api-key').value = config.default_api_key;
|
| 570 |
+
}
|
| 571 |
+
if (config.default_model && !document.getElementById('chat-model').value) {
|
| 572 |
+
document.getElementById('chat-model').value = config.default_model;
|
| 573 |
+
}
|
| 574 |
+
|
| 575 |
loadProviders();
|
| 576 |
await loadChatHistoryList();
|
| 577 |
createTerminalTab(); // Init first tab
|
|
|
|
| 1059 |
} catch (e) { alert(e.message); }
|
| 1060 |
}
|
| 1061 |
|
| 1062 |
+
const STANDARD_PROVIDERS = [
|
| 1063 |
+
{ key: 'huggingface-router', name: 'HuggingFace Router', baseUrl: 'https://router.huggingface.co/v1' },
|
| 1064 |
+
{ key: 'openai', name: 'OpenAI', baseUrl: 'https://api.openai.com/v1' },
|
| 1065 |
+
{ key: 'openrouter', name: 'OpenRouter', baseUrl: 'https://openrouter.ai/api/v1' },
|
| 1066 |
+
{ key: 'groq', name: 'Groq (OpenAI-compatible)', baseUrl: 'https://api.groq.com/openai/v1' },
|
| 1067 |
+
{ key: 'together', name: 'Together', baseUrl: 'https://api.together.xyz/v1' },
|
| 1068 |
+
{ key: 'fireworks', name: 'Fireworks', baseUrl: 'https://api.fireworks.ai/inference/v1' },
|
| 1069 |
+
{ key: 'mistral', name: 'Mistral', baseUrl: 'https://api.mistral.ai/v1' },
|
| 1070 |
+
{ key: 'deepseek', name: 'DeepSeek', baseUrl: 'https://api.deepseek.com/v1' },
|
| 1071 |
+
];
|
| 1072 |
+
|
| 1073 |
+
let providerCache = {}; // name -> { apiKey, baseUrl, model }
|
| 1074 |
+
let warnedProviderFallback = false;
|
| 1075 |
+
|
| 1076 |
+
function initProviderPresets() {
|
| 1077 |
+
const presets = document.getElementById('provider-presets');
|
| 1078 |
+
if (!presets) return;
|
| 1079 |
+
presets.innerHTML = '<option value="">Select preset...</option>';
|
| 1080 |
+
STANDARD_PROVIDERS.forEach((p) => {
|
| 1081 |
+
const opt = document.createElement('option');
|
| 1082 |
+
opt.value = p.key;
|
| 1083 |
+
opt.innerText = `${p.name} — ${p.baseUrl}`;
|
| 1084 |
+
presets.appendChild(opt);
|
| 1085 |
+
});
|
| 1086 |
+
}
|
| 1087 |
+
|
| 1088 |
+
function applyProviderPreset(key) {
|
| 1089 |
+
if (!key) return;
|
| 1090 |
+
const preset = STANDARD_PROVIDERS.find((p) => p.key === key);
|
| 1091 |
+
if (!preset) return;
|
| 1092 |
+
document.getElementById('chat-base-url').value = preset.baseUrl;
|
| 1093 |
+
}
|
| 1094 |
+
|
| 1095 |
+
async function getActiveUserId() {
|
| 1096 |
+
const { data, error } = await supabase.auth.getUser();
|
| 1097 |
+
if (error) throw error;
|
| 1098 |
+
return data?.user?.id || null;
|
| 1099 |
+
}
|
| 1100 |
+
|
| 1101 |
+
function loadProvidersFromLocalStorage() {
|
| 1102 |
const select = document.getElementById('saved-providers');
|
| 1103 |
const saved = JSON.parse(localStorage.getItem('chat_providers') || '{}');
|
| 1104 |
+
providerCache = saved;
|
| 1105 |
select.innerHTML = '<option value="">Select...</option>';
|
| 1106 |
+
Object.keys(saved).forEach((k) => {
|
| 1107 |
const opt = document.createElement('option');
|
| 1108 |
opt.value = k;
|
| 1109 |
opt.innerText = k;
|
|
|
|
| 1111 |
});
|
| 1112 |
}
|
| 1113 |
|
| 1114 |
+
async function loadProviders() {
|
| 1115 |
+
try {
|
| 1116 |
+
const uid = await getActiveUserId();
|
| 1117 |
+
if (!uid) return loadProvidersFromLocalStorage();
|
| 1118 |
+
|
| 1119 |
+
const { data, error } = await supabase
|
| 1120 |
+
.from('provider_configs')
|
| 1121 |
+
.select('name, api_key, base_url, model')
|
| 1122 |
+
.eq('user_id', uid)
|
| 1123 |
+
.order('name', { ascending: true });
|
| 1124 |
+
if (error) throw error;
|
| 1125 |
+
|
| 1126 |
+
providerCache = {};
|
| 1127 |
+
(data || []).forEach((row) => {
|
| 1128 |
+
providerCache[row.name] = { apiKey: row.api_key || '', baseUrl: row.base_url || '', model: row.model || '' };
|
| 1129 |
+
});
|
| 1130 |
+
|
| 1131 |
+
const select = document.getElementById('saved-providers');
|
| 1132 |
+
select.innerHTML = '<option value="">Select...</option>';
|
| 1133 |
+
Object.keys(providerCache).forEach((k) => {
|
| 1134 |
+
const opt = document.createElement('option');
|
| 1135 |
+
opt.value = k;
|
| 1136 |
+
opt.innerText = k;
|
| 1137 |
+
select.appendChild(opt);
|
| 1138 |
+
});
|
| 1139 |
+
} catch (e) {
|
| 1140 |
+
if (!warnedProviderFallback) {
|
| 1141 |
+
warnedProviderFallback = true;
|
| 1142 |
+
console.warn('Provider configs table missing or RLS blocked; falling back to localStorage.', e);
|
| 1143 |
+
}
|
| 1144 |
+
loadProvidersFromLocalStorage();
|
| 1145 |
+
}
|
| 1146 |
+
}
|
| 1147 |
+
|
| 1148 |
+
async function saveProvider() {
|
| 1149 |
const name = prompt("Name:");
|
| 1150 |
if (!name) return;
|
| 1151 |
+
|
| 1152 |
const config = {
|
| 1153 |
apiKey: document.getElementById('chat-api-key').value,
|
| 1154 |
baseUrl: document.getElementById('chat-base-url').value,
|
| 1155 |
model: document.getElementById('chat-model').value
|
| 1156 |
};
|
| 1157 |
+
|
| 1158 |
+
try {
|
| 1159 |
+
const uid = await getActiveUserId();
|
| 1160 |
+
if (!uid) throw new Error('No active user session');
|
| 1161 |
+
|
| 1162 |
+
const { error } = await supabase
|
| 1163 |
+
.from('provider_configs')
|
| 1164 |
+
.upsert(
|
| 1165 |
+
{ user_id: uid, name, api_key: config.apiKey, base_url: config.baseUrl, model: config.model },
|
| 1166 |
+
{ onConflict: 'user_id,name' }
|
| 1167 |
+
);
|
| 1168 |
+
if (error) throw error;
|
| 1169 |
+
await loadProviders();
|
| 1170 |
+
document.getElementById('saved-providers').value = name;
|
| 1171 |
+
} catch (e) {
|
| 1172 |
+
// Fallback to local storage if table isn't available.
|
| 1173 |
+
const saved = JSON.parse(localStorage.getItem('chat_providers') || '{}');
|
| 1174 |
+
saved[name] = config;
|
| 1175 |
+
localStorage.setItem('chat_providers', JSON.stringify(saved));
|
| 1176 |
+
loadProvidersFromLocalStorage();
|
| 1177 |
+
document.getElementById('saved-providers').value = name;
|
| 1178 |
+
}
|
| 1179 |
}
|
| 1180 |
|
| 1181 |
+
async function deleteProvider() {
|
| 1182 |
const name = document.getElementById('saved-providers').value;
|
| 1183 |
if (!name) return;
|
| 1184 |
if (!confirm("Delete?")) return;
|
| 1185 |
+
|
| 1186 |
+
try {
|
| 1187 |
+
const uid = await getActiveUserId();
|
| 1188 |
+
if (!uid) throw new Error('No active user session');
|
| 1189 |
+
|
| 1190 |
+
const { error } = await supabase
|
| 1191 |
+
.from('provider_configs')
|
| 1192 |
+
.delete()
|
| 1193 |
+
.eq('user_id', uid)
|
| 1194 |
+
.eq('name', name);
|
| 1195 |
+
if (error) throw error;
|
| 1196 |
+
await loadProviders();
|
| 1197 |
+
} catch (e) {
|
| 1198 |
+
const saved = JSON.parse(localStorage.getItem('chat_providers') || '{}');
|
| 1199 |
+
delete saved[name];
|
| 1200 |
+
localStorage.setItem('chat_providers', JSON.stringify(saved));
|
| 1201 |
+
loadProvidersFromLocalStorage();
|
| 1202 |
+
}
|
| 1203 |
}
|
| 1204 |
|
| 1205 |
function loadSelectedProvider(name) {
|
| 1206 |
if (!name) return;
|
| 1207 |
+
const config = providerCache[name];
|
|
|
|
| 1208 |
if (config) {
|
| 1209 |
+
document.getElementById('chat-api-key').value = config.apiKey || '';
|
| 1210 |
+
document.getElementById('chat-base-url').value = config.baseUrl || '';
|
| 1211 |
+
document.getElementById('chat-model').value = config.model || '';
|
| 1212 |
}
|
| 1213 |
}
|
| 1214 |
|
supabase_provider_configs.sql
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
-- Provider config storage for per-user API keys and base URLs.
|
| 2 |
+
-- Run this in Supabase SQL Editor.
|
| 3 |
+
|
| 4 |
+
create table if not exists public.provider_configs (
|
| 5 |
+
id uuid primary key default gen_random_uuid(),
|
| 6 |
+
user_id uuid not null references auth.users (id) on delete cascade,
|
| 7 |
+
name text not null,
|
| 8 |
+
api_key text not null default '',
|
| 9 |
+
base_url text not null default '',
|
| 10 |
+
model text not null default '',
|
| 11 |
+
created_at timestamptz not null default now(),
|
| 12 |
+
updated_at timestamptz not null default now(),
|
| 13 |
+
unique (user_id, name)
|
| 14 |
+
);
|
| 15 |
+
|
| 16 |
+
-- Keep updated_at current
|
| 17 |
+
create or replace function public.set_updated_at()
|
| 18 |
+
returns trigger
|
| 19 |
+
language plpgsql
|
| 20 |
+
as $$
|
| 21 |
+
begin
|
| 22 |
+
new.updated_at = now();
|
| 23 |
+
return new;
|
| 24 |
+
end;
|
| 25 |
+
$$;
|
| 26 |
+
|
| 27 |
+
drop trigger if exists provider_configs_set_updated_at on public.provider_configs;
|
| 28 |
+
create trigger provider_configs_set_updated_at
|
| 29 |
+
before update on public.provider_configs
|
| 30 |
+
for each row
|
| 31 |
+
execute procedure public.set_updated_at();
|
| 32 |
+
|
| 33 |
+
alter table public.provider_configs enable row level security;
|
| 34 |
+
|
| 35 |
+
drop policy if exists "provider_configs_select_own" on public.provider_configs;
|
| 36 |
+
create policy "provider_configs_select_own"
|
| 37 |
+
on public.provider_configs
|
| 38 |
+
for select
|
| 39 |
+
using (auth.uid() = user_id);
|
| 40 |
+
|
| 41 |
+
drop policy if exists "provider_configs_insert_own" on public.provider_configs;
|
| 42 |
+
create policy "provider_configs_insert_own"
|
| 43 |
+
on public.provider_configs
|
| 44 |
+
for insert
|
| 45 |
+
with check (auth.uid() = user_id);
|
| 46 |
+
|
| 47 |
+
drop policy if exists "provider_configs_update_own" on public.provider_configs;
|
| 48 |
+
create policy "provider_configs_update_own"
|
| 49 |
+
on public.provider_configs
|
| 50 |
+
for update
|
| 51 |
+
using (auth.uid() = user_id)
|
| 52 |
+
with check (auth.uid() = user_id);
|
| 53 |
+
|
| 54 |
+
drop policy if exists "provider_configs_delete_own" on public.provider_configs;
|
| 55 |
+
create policy "provider_configs_delete_own"
|
| 56 |
+
on public.provider_configs
|
| 57 |
+
for delete
|
| 58 |
+
using (auth.uid() = user_id);
|
| 59 |
+
|