mirror of
https://18126008609:longquanjian123@gitee.com/feigong123/aurask.git
synced 2026-04-19 15:00:35 +00:00
393 lines
14 KiB
JavaScript
393 lines
14 KiB
JavaScript
const STORAGE_KEY = "aurask.portal.session";
|
|
const TAB_KEY = "aurask.portal.activeTab";
|
|
|
|
const app = document.querySelector("#app");
|
|
|
|
const state = {
|
|
config: null,
|
|
sessionToken: window.localStorage.getItem(STORAGE_KEY) || "",
|
|
profile: null,
|
|
activeTab: window.localStorage.getItem(TAB_KEY) || "workflows",
|
|
loading: true,
|
|
error: "",
|
|
};
|
|
|
|
function detectApiBase() {
|
|
if (window.location.protocol === "file:") return "http://127.0.0.1:8080";
|
|
if (["127.0.0.1", "localhost"].includes(window.location.hostname)) return "http://127.0.0.1:8080";
|
|
return `${window.location.origin}/api`;
|
|
}
|
|
|
|
function normalizeApiBase(url) {
|
|
return (url || detectApiBase()).replace(/\/$/, "");
|
|
}
|
|
|
|
async function request(path, options = {}) {
|
|
const headers = new Headers(options.headers || {});
|
|
headers.set("Accept", "application/json");
|
|
if (options.body !== undefined) headers.set("Content-Type", "application/json");
|
|
if (state.sessionToken) headers.set("Authorization", `Bearer ${state.sessionToken}`);
|
|
const response = await fetch(`${normalizeApiBase(state.config?.public_api_base_url)}${path}`, {
|
|
...options,
|
|
headers,
|
|
body: options.body !== undefined ? JSON.stringify(options.body) : undefined,
|
|
});
|
|
const payload = await response.json().catch(() => ({}));
|
|
if (!response.ok) {
|
|
const message = payload?.error?.message || payload?.message || "Request failed";
|
|
const error = new Error(message);
|
|
error.status = response.status;
|
|
error.payload = payload;
|
|
throw error;
|
|
}
|
|
return payload;
|
|
}
|
|
|
|
function persistSession(token) {
|
|
state.sessionToken = token || "";
|
|
if (state.sessionToken) {
|
|
window.localStorage.setItem(STORAGE_KEY, state.sessionToken);
|
|
} else {
|
|
window.localStorage.removeItem(STORAGE_KEY);
|
|
}
|
|
}
|
|
|
|
function setTab(tab) {
|
|
state.activeTab = tab;
|
|
window.localStorage.setItem(TAB_KEY, tab);
|
|
render();
|
|
}
|
|
|
|
function initials(label) {
|
|
return (label || "AU")
|
|
.split(/\s+/)
|
|
.filter(Boolean)
|
|
.slice(0, 2)
|
|
.map((part) => part[0]?.toUpperCase())
|
|
.join("");
|
|
}
|
|
|
|
function escapeHtml(value) {
|
|
return String(value || "")
|
|
.replaceAll("&", "&")
|
|
.replaceAll("<", "<")
|
|
.replaceAll(">", ">")
|
|
.replaceAll('"', """)
|
|
.replaceAll("'", "'");
|
|
}
|
|
|
|
function withEmbedContext(baseUrl) {
|
|
if (!baseUrl) return "";
|
|
const url = new URL(baseUrl, window.location.origin);
|
|
if (state.profile?.tenant?.id) url.searchParams.set("tenant_id", state.profile.tenant.id);
|
|
if (state.profile?.workspace?.id) url.searchParams.set("workspace_id", state.profile.workspace.id);
|
|
url.searchParams.set("source", "aurask-protal");
|
|
return url.toString();
|
|
}
|
|
|
|
function ensureSigninRoute() {
|
|
if (state.profile) return;
|
|
if (window.location.pathname !== "/signin") {
|
|
window.history.replaceState({}, "", "/signin");
|
|
}
|
|
}
|
|
|
|
function ensureDashboardRoute() {
|
|
if (!state.profile) return;
|
|
if (!window.location.pathname.startsWith("/app")) {
|
|
window.history.replaceState({}, "", "/app");
|
|
}
|
|
}
|
|
|
|
async function bootstrap() {
|
|
state.loading = true;
|
|
render();
|
|
try {
|
|
const activeApiBase = detectApiBase();
|
|
const localHost = window.location.protocol === "file:" || ["127.0.0.1", "localhost"].includes(window.location.hostname);
|
|
state.config = await fetch(`${activeApiBase}/auth/config`).then((response) => response.json());
|
|
state.config.public_api_base_url = normalizeApiBase(localHost ? activeApiBase : state.config.public_api_base_url || activeApiBase);
|
|
if (state.sessionToken) {
|
|
try {
|
|
state.profile = await request("/auth/session");
|
|
ensureDashboardRoute();
|
|
} catch (error) {
|
|
persistSession("");
|
|
state.profile = null;
|
|
}
|
|
}
|
|
if (!state.profile) ensureSigninRoute();
|
|
} catch (error) {
|
|
state.error = error.message || "Failed to load Aurask configuration";
|
|
} finally {
|
|
state.loading = false;
|
|
render();
|
|
mountGoogleButton();
|
|
}
|
|
}
|
|
|
|
async function signInWithGoogle(idToken) {
|
|
state.error = "";
|
|
render();
|
|
try {
|
|
const payload = await request("/auth/google/login", {
|
|
method: "POST",
|
|
body: { id_token: idToken },
|
|
});
|
|
persistSession(payload.token);
|
|
state.profile = payload;
|
|
ensureDashboardRoute();
|
|
} catch (error) {
|
|
state.error = error.message || "Google sign-in failed";
|
|
render();
|
|
}
|
|
}
|
|
|
|
async function logout() {
|
|
try {
|
|
await request("/auth/logout", { method: "POST" });
|
|
} catch (error) {
|
|
console.warn("Logout request failed", error);
|
|
}
|
|
persistSession("");
|
|
state.profile = null;
|
|
ensureSigninRoute();
|
|
render();
|
|
mountGoogleButton();
|
|
}
|
|
|
|
function mountGoogleButton() {
|
|
const googleBox = document.querySelector("#googleButton");
|
|
if (!googleBox || !state.config?.auth?.google?.enabled || !state.config?.auth?.google?.client_id) return;
|
|
if (!window.google?.accounts?.id) return;
|
|
googleBox.innerHTML = "";
|
|
window.google.accounts.id.initialize({
|
|
client_id: state.config.auth.google.client_id,
|
|
callback: ({ credential }) => signInWithGoogle(credential),
|
|
});
|
|
window.google.accounts.id.renderButton(googleBox, {
|
|
theme: "outline",
|
|
size: "large",
|
|
shape: "pill",
|
|
text: "signup_with",
|
|
width: 320,
|
|
});
|
|
}
|
|
|
|
function renderStatusPills() {
|
|
if (!state.config) return "";
|
|
return `
|
|
<div class="pill-row">
|
|
<span class="pill">API: ${escapeHtml(state.config.public_api_base_url)}</span>
|
|
<span class="pill">Web: ${escapeHtml(state.config.public_base_url)}</span>
|
|
<span class="pill">DevCloud API Image Ready</span>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function renderSignin() {
|
|
const googleConfig = state.config?.auth?.google || {};
|
|
const googleEnabled = Boolean(googleConfig.enabled && googleConfig.client_id);
|
|
app.innerHTML = `
|
|
<main class="signin-layout">
|
|
<section class="signin-brand">
|
|
<div class="brand-mark">A</div>
|
|
<p class="brand-eyebrow">Aurask</p>
|
|
<h1>Ship AI workflows with private knowledge, safer by default.</h1>
|
|
<p class="brand-copy">
|
|
Sign in to open your personal workspace, manage Langflow workflows, and use AnythingLLM knowledge bases from one portal.
|
|
</p>
|
|
${renderStatusPills()}
|
|
</section>
|
|
<section class="signin-card">
|
|
<div class="signin-header">
|
|
<p class="section-kicker">Welcome back</p>
|
|
<h2>Sign in to Aurask</h2>
|
|
<p>Continue with Google to access your workspace or create a new one on first sign-in.</p>
|
|
</div>
|
|
${googleEnabled ? '<div id="googleButton" class="google-box"></div>' : '<div class="signin-actions"><button class="auth-btn auth-btn-secondary" type="button" disabled>Continue with Google</button></div>'}
|
|
${
|
|
googleEnabled
|
|
? '<p class="signin-tip">Google new-user login provisions a dedicated Aurask workspace on first sign-in.</p>'
|
|
: '<p class="signin-tip">Set `AURASK_GOOGLE_CLIENT_ID` to enable Google one-tap registration.</p>'
|
|
}
|
|
${state.error ? `<p class="message error">${escapeHtml(state.error)}</p>` : ""}
|
|
<div class="signin-footer">
|
|
<span>Need a first workspace?</span>
|
|
<span>Aurask creates one automatically after your first successful login.</span>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
`;
|
|
}
|
|
|
|
function renderDashboard() {
|
|
const user = state.profile?.user || {};
|
|
const tenant = state.profile?.tenant || {};
|
|
const workspace = state.profile?.workspace || {};
|
|
const quota = state.profile?.quota || {};
|
|
const config = state.profile?.config || state.config || {};
|
|
const langflowUrl = withEmbedContext(config.embeds?.langflow_url);
|
|
const anythingllmUrl = withEmbedContext(config.embeds?.anythingllm_url);
|
|
app.innerHTML = `
|
|
<main class="dashboard-shell">
|
|
<header class="topbar">
|
|
<div class="topbar-left">
|
|
<div class="brand-mark brand-mark-small">A</div>
|
|
<div>
|
|
<p class="section-kicker">Aurask Workspace</p>
|
|
<h1>${escapeHtml(tenant.name || "Aurask")}</h1>
|
|
</div>
|
|
</div>
|
|
<div class="topbar-right">
|
|
<nav class="tabbar" aria-label="Workspace sections">
|
|
<button class="tab ${state.activeTab === "workflows" ? "is-active" : ""}" data-tab="workflows" type="button">Workflows</button>
|
|
<button class="tab ${state.activeTab === "knowledge" ? "is-active" : ""}" data-tab="knowledge" type="button">Knowledge Base</button>
|
|
</nav>
|
|
<details class="profile-menu">
|
|
<summary>
|
|
${user.avatar_url ? `<img class="avatar-image" src="${escapeHtml(user.avatar_url)}" alt="${escapeHtml(user.display_name || user.email)}" />` : `<span class="avatar">${escapeHtml(initials(user.display_name || user.email))}</span>`}
|
|
<span class="profile-name">${escapeHtml(user.display_name || user.email || "Profile")}</span>
|
|
</summary>
|
|
<div class="profile-panel">
|
|
<div class="profile-block">
|
|
<strong>${escapeHtml(user.display_name || user.email || "")}</strong>
|
|
<span>${escapeHtml(user.email || "")}</span>
|
|
</div>
|
|
<div class="profile-block">
|
|
<span>Tenant</span>
|
|
<strong>${escapeHtml(tenant.name || "")}</strong>
|
|
</div>
|
|
<div class="profile-block">
|
|
<span>Workspace</span>
|
|
<strong>${escapeHtml(workspace.name || "")}</strong>
|
|
</div>
|
|
<div class="profile-block profile-grid">
|
|
<span>Plan</span>
|
|
<strong>${escapeHtml(quota.plan_code || "")}</strong>
|
|
<span>TBU</span>
|
|
<strong>${escapeHtml(String(quota.available_tbu ?? 0))}</strong>
|
|
<span>Knowledge Bases</span>
|
|
<strong>${escapeHtml(String(quota.knowledge_bases ?? 0))}</strong>
|
|
</div>
|
|
<button id="logoutButton" class="auth-btn auth-btn-secondary" type="button">Sign out</button>
|
|
</div>
|
|
</details>
|
|
</div>
|
|
</header>
|
|
<section class="dashboard-grid">
|
|
<aside class="side-panel">
|
|
<section class="info-card">
|
|
<p class="section-kicker">Personal Center</p>
|
|
<h2>${escapeHtml(user.display_name || user.email || "Aurask User")}</h2>
|
|
<p>${escapeHtml(user.email || "")}</p>
|
|
<div class="stat-list">
|
|
<div class="stat-item">
|
|
<span>Tenant ID</span>
|
|
<strong>${escapeHtml(tenant.id || "")}</strong>
|
|
</div>
|
|
<div class="stat-item">
|
|
<span>Workspace ID</span>
|
|
<strong>${escapeHtml(workspace.id || "")}</strong>
|
|
</div>
|
|
<div class="stat-item">
|
|
<span>API Image</span>
|
|
<strong>${escapeHtml(config.devcloud?.api_image || "")}</strong>
|
|
</div>
|
|
<div class="stat-item">
|
|
<span>Web Image</span>
|
|
<strong>${escapeHtml(config.devcloud?.web_image || "")}</strong>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
<section class="info-card">
|
|
<p class="section-kicker">Deploy Defaults</p>
|
|
<ul class="plain-list">
|
|
<li>API Gateway: ${escapeHtml(config.public_api_base_url || "")}</li>
|
|
<li>Langflow Embed: ${escapeHtml(config.embeds?.langflow_url || "")}</li>
|
|
<li>AnythingLLM Embed: ${escapeHtml(config.embeds?.anythingllm_url || "")}</li>
|
|
</ul>
|
|
</section>
|
|
</aside>
|
|
<section class="workspace-panel">
|
|
<div class="embed-card ${state.activeTab === "workflows" ? "" : "is-hidden"}">
|
|
<div class="embed-header">
|
|
<div>
|
|
<p class="section-kicker">Workflow Studio</p>
|
|
<h2>Langflow</h2>
|
|
</div>
|
|
<a href="${escapeHtml(langflowUrl)}" target="_blank" rel="noreferrer">Open in new tab</a>
|
|
</div>
|
|
<iframe title="Aurask Langflow" src="${escapeHtml(langflowUrl)}"></iframe>
|
|
</div>
|
|
<div class="embed-card ${state.activeTab === "knowledge" ? "" : "is-hidden"}">
|
|
<div class="embed-header">
|
|
<div>
|
|
<p class="section-kicker">Knowledge Base</p>
|
|
<h2>AnythingLLM</h2>
|
|
</div>
|
|
<a href="${escapeHtml(anythingllmUrl)}" target="_blank" rel="noreferrer">Open in new tab</a>
|
|
</div>
|
|
<iframe title="Aurask AnythingLLM" src="${escapeHtml(anythingllmUrl)}"></iframe>
|
|
</div>
|
|
</section>
|
|
</section>
|
|
${state.error ? `<div class="toast error">${escapeHtml(state.error)}</div>` : ""}
|
|
</main>
|
|
`;
|
|
|
|
document.querySelectorAll("[data-tab]").forEach((button) => {
|
|
button.addEventListener("click", () => setTab(button.dataset.tab));
|
|
});
|
|
document.querySelector("#logoutButton")?.addEventListener("click", logout);
|
|
}
|
|
|
|
function renderLoading() {
|
|
app.innerHTML = `
|
|
<main class="loading-shell">
|
|
<div class="loader"></div>
|
|
<p>Loading Aurask...</p>
|
|
</main>
|
|
`;
|
|
}
|
|
|
|
function render() {
|
|
if (state.loading) {
|
|
renderLoading();
|
|
return;
|
|
}
|
|
if (state.profile) {
|
|
renderDashboard();
|
|
return;
|
|
}
|
|
renderSignin();
|
|
}
|
|
|
|
window.addEventListener("popstate", render);
|
|
|
|
if (state.sessionToken) {
|
|
document.addEventListener("visibilitychange", async () => {
|
|
if (document.visibilityState !== "visible" || !state.profile) return;
|
|
try {
|
|
state.profile = await request("/auth/session");
|
|
render();
|
|
} catch (error) {
|
|
persistSession("");
|
|
state.profile = null;
|
|
state.error = "Your Aurask session has expired. Please sign in again.";
|
|
ensureSigninRoute();
|
|
render();
|
|
mountGoogleButton();
|
|
}
|
|
});
|
|
}
|
|
|
|
const googleScript = document.createElement("script");
|
|
googleScript.src = "https://accounts.google.com/gsi/client";
|
|
googleScript.async = true;
|
|
googleScript.defer = true;
|
|
googleScript.onload = mountGoogleButton;
|
|
document.head.appendChild(googleScript);
|
|
|
|
bootstrap();
|