const STORAGE_KEY = "aurask.portal.session"; const TAB_KEY = "aurask.portal.activeTab"; const LOCALE_KEY = "aurask.portal.locale"; const SUPPORTED_LOCALES = ["en", "zh"]; const COPY = { en: { pageTitleSignin: "Aurask | Sign In", pageTitleDashboard: "Aurask | Workspace", status: { api: "API", web: "Web", devcloud: "DevCloud API Image", }, signin: { brand: "Aurask", welcomeBack: "Welcome back", title: "Sign in to Aurask", subtitle: "Continue with Google to access your workspace or create a new one on first sign-in.", continueWithGoogle: "Continue with Google", helperReady: "Google sign-in is ready. First-time users get a dedicated Aurask workspace automatically.", helperLoading: "Google sign-in is loading. Please wait a moment.", helperUnavailable: "Google sign-in is unavailable until `AURASK_GOOGLE_CLIENT_ID` is configured.", needWorkspace: "Need a first workspace?", workspaceAuto: "Aurask provisions one automatically after your first successful sign-in.", languageLabel: "Language", heroTitle: "Ship AI workflows with private knowledge, safer by default.", heroCopy: "Sign in to open your personal workspace, manage Langflow workflows, and use AnythingLLM knowledge bases from one portal.", }, dashboard: { workspaceKicker: "Aurask Workspace", workspaceSections: "Workspace sections", tabs: { workflows: "Workflows", knowledge: "Knowledge Base", }, profileLabel: "Profile", signOut: "Sign out", personalCenter: "Personal Center", userFallback: "Aurask User", deployDefaults: "Deploy Defaults", tenant: "Tenant", workspace: "Workspace", plan: "Plan", tbu: "TBU", knowledgeBases: "Knowledge Bases", tenantId: "Tenant ID", workspaceId: "Workspace ID", apiImage: "API Image", webImage: "Web Image", apiGateway: "API Gateway", langflowEmbed: "Langflow Embed", anythingllmEmbed: "AnythingLLM Embed", workflowStudio: "Workflow Studio", langflow: "Langflow", knowledgeBaseTitle: "Knowledge Base", anythingllm: "AnythingLLM", openNewTab: "Open in new tab", plans: { free_trial: "Free Trial", basic_monthly: "Basic", dedicated_space: "Dedicated Space", }, }, messages: { loading: "Loading Aurask...", sessionExpired: "Your Aurask session has expired. Please sign in again.", googleSignInFailed: "Google sign-in failed", configLoadFailed: "Failed to load Aurask configuration", }, }, zh: { pageTitleSignin: "Aurask | 登录", pageTitleDashboard: "Aurask | 工作台", status: { api: "接口", web: "站点", devcloud: "DevCloud API 镜像", }, signin: { brand: "Aurask", welcomeBack: "欢迎回来", title: "登录 Aurask", subtitle: "使用 Google 登录访问你的工作空间;首次登录时会自动创建专属 workspace。", continueWithGoogle: "使用 Google 继续", helperReady: "Google 登录已就绪,首次登录的用户会自动开通独立 Aurask workspace。", helperLoading: "Google 登录组件正在加载,请稍候。", helperUnavailable: "未配置 `AURASK_GOOGLE_CLIENT_ID` 前,Google 登录按钮会保持灰色禁用状态。", needWorkspace: "还没有 workspace?", workspaceAuto: "首次成功登录后,Aurask 会自动为你创建一个。", languageLabel: "语言", heroTitle: "以更稳妥的方式交付连接私有知识的 AI 工作流。", heroCopy: "登录后即可进入你的个人 workspace,在同一门户中管理 Langflow 工作流并使用 AnythingLLM 知识库。", }, dashboard: { workspaceKicker: "Aurask 工作台", workspaceSections: "工作区导航", tabs: { workflows: "工作流", knowledge: "知识库", }, profileLabel: "个人中心", signOut: "退出登录", personalCenter: "个人中心", userFallback: "Aurask 用户", deployDefaults: "部署信息", tenant: "租户", workspace: "工作空间", plan: "套餐", tbu: "TBU", knowledgeBases: "知识库数量", tenantId: "租户 ID", workspaceId: "工作空间 ID", apiImage: "API 镜像", webImage: "Web 镜像", apiGateway: "API 网关", langflowEmbed: "Langflow 嵌入地址", anythingllmEmbed: "AnythingLLM 嵌入地址", workflowStudio: "工作流工作台", langflow: "Langflow", knowledgeBaseTitle: "知识库", anythingllm: "AnythingLLM", openNewTab: "新标签页打开", plans: { free_trial: "免费试用", basic_monthly: "基础版", dedicated_space: "独享空间", }, }, messages: { loading: "Aurask 加载中...", sessionExpired: "Aurask 会话已过期,请重新登录。", googleSignInFailed: "Google 登录失败", configLoadFailed: "Aurask 配置加载失败", }, }, }; const app = document.querySelector("#app"); const state = { config: null, sessionToken: window.localStorage.getItem(STORAGE_KEY) || "", profile: null, activeTab: window.localStorage.getItem(TAB_KEY) || "workflows", locale: detectInitialLocale(), googleSdkReady: Boolean(window.google?.accounts?.id), loading: true, errorKey: "", errorDetail: "", }; applyDocumentLanguage(); function normalizeLocale(locale) { return SUPPORTED_LOCALES.includes(locale) ? locale : null; } function detectInitialLocale() { const storedLocale = normalizeLocale(window.localStorage.getItem(LOCALE_KEY)); if (storedLocale) return storedLocale; return String(window.navigator.language || "").toLowerCase().startsWith("zh") ? "zh" : "en"; } function persistLocale(locale) { state.locale = normalizeLocale(locale) || "en"; window.localStorage.setItem(LOCALE_KEY, state.locale); applyDocumentLanguage(); } function applyDocumentLanguage() { document.documentElement.lang = state.locale === "zh" ? "zh-CN" : "en"; } function copy(path) { return path.split(".").reduce((value, part) => value?.[part], COPY[state.locale]) ?? path; } function setDocumentTitle() { document.title = state.profile ? copy("pageTitleDashboard") : copy("pageTitleSignin"); } function setLocale(locale) { const nextLocale = normalizeLocale(locale); if (!nextLocale || nextLocale === state.locale) return; persistLocale(nextLocale); render(); if (!state.profile) { mountGoogleButton(); } } function setError(key, detail = "") { state.errorKey = key; state.errorDetail = detail || ""; } function clearError() { state.errorKey = ""; state.errorDetail = ""; } function renderError() { if (!state.errorKey) return ""; const message = copy(state.errorKey); if (!state.errorDetail) return escapeHtml(message); return `${escapeHtml(message)}: ${escapeHtml(state.errorDetail)}`; } 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"); } } function googleStatus() { const googleConfig = state.config?.auth?.google || {}; if (!googleConfig.enabled || !googleConfig.client_id) return "unavailable"; if (!state.googleSdkReady) return "loading"; return "ready"; } function renderStatusPills() { if (!state.config) return ""; return `
${escapeHtml(copy("status.api"))}: ${escapeHtml(state.config.public_api_base_url)} ${escapeHtml(copy("status.web"))}: ${escapeHtml(state.config.public_base_url)} ${escapeHtml(copy("status.devcloud"))}
`; } function renderLocaleSwitcher() { return `
`; } function renderSigninGoogleArea() { if (googleStatus() === "ready") { return `
`; } return `
`; } function renderSigninHelper() { const status = googleStatus(); if (status === "unavailable") return copy("signin.helperUnavailable"); if (status === "loading") return copy("signin.helperLoading"); return copy("signin.helperReady"); } function formatPlanName(planCode) { return copy(`dashboard.plans.${planCode}`) === `dashboard.plans.${planCode}` ? planCode || "" : copy(`dashboard.plans.${planCode}`); } function renderSignin() { app.innerHTML = `
A

${escapeHtml(copy("signin.brand"))}

${escapeHtml(copy("signin.heroTitle"))}

${escapeHtml(copy("signin.heroCopy"))}

${renderStatusPills()}
${renderSigninGoogleArea()} ${state.errorKey ? `

${renderError()}

` : ""}
`; document.querySelectorAll("[data-locale]").forEach((button) => { button.addEventListener("click", () => setLocale(button.dataset.locale)); }); } 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 = `
A

${escapeHtml(copy("dashboard.workspaceKicker"))}

${escapeHtml(tenant.name || "Aurask")}

${ user.avatar_url ? `${escapeHtml(user.display_name || user.email || copy(` : `${escapeHtml(initials(user.display_name || user.email))}` } ${escapeHtml(user.display_name || user.email || copy("dashboard.profileLabel"))}
${escapeHtml(user.display_name || user.email || "")} ${escapeHtml(user.email || "")}
${escapeHtml(copy("dashboard.tenant"))} ${escapeHtml(tenant.name || "")}
${escapeHtml(copy("dashboard.workspace"))} ${escapeHtml(workspace.name || "")}
${escapeHtml(copy("dashboard.plan"))} ${escapeHtml(formatPlanName(quota.plan_code))} ${escapeHtml(copy("dashboard.tbu"))} ${escapeHtml(String(quota.available_tbu ?? 0))} ${escapeHtml(copy("dashboard.knowledgeBases"))} ${escapeHtml(String(quota.knowledge_bases ?? 0))}

${escapeHtml(copy("dashboard.workflowStudio"))}

${escapeHtml(copy("dashboard.langflow"))}

${escapeHtml(copy("dashboard.openNewTab"))}

${escapeHtml(copy("dashboard.knowledgeBaseTitle"))}

${escapeHtml(copy("dashboard.anythingllm"))}

${escapeHtml(copy("dashboard.openNewTab"))}
${state.errorKey ? `
${renderError()}
` : ""}
`; document.querySelectorAll("[data-tab]").forEach((button) => { button.addEventListener("click", () => setTab(button.dataset.tab)); }); document.querySelector("#logoutButton")?.addEventListener("click", logout); } function renderLoading() { app.innerHTML = `

${escapeHtml(copy("messages.loading"))}

`; } function render() { applyDocumentLanguage(); setDocumentTitle(); if (state.loading) { renderLoading(); return; } if (state.profile) { renderDashboard(); return; } renderSignin(); } async function bootstrap() { state.loading = true; clearError(); 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, ); state.googleSdkReady = state.googleSdkReady || Boolean(window.google?.accounts?.id); if (state.sessionToken) { try { state.profile = await request("/auth/session"); ensureDashboardRoute(); } catch (_error) { persistSession(""); state.profile = null; } } if (!state.profile) { ensureSigninRoute(); } } catch (error) { setError("messages.configLoadFailed", error.message || ""); } finally { state.loading = false; render(); mountGoogleButton(); } } async function signInWithGoogle(idToken) { clearError(); render(); try { const payload = await request("/auth/google/login", { method: "POST", body: { id_token: idToken }, }); persistSession(payload.token); state.profile = payload; ensureDashboardRoute(); render(); } catch (error) { setError("messages.googleSignInFailed", error.message || ""); render(); mountGoogleButton(); } } 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 || googleStatus() !== "ready" || !window.google?.accounts?.id) return; googleBox.innerHTML = ""; window.google.accounts.id.initialize({ client_id: state.config.auth.google.client_id, callback: ({ credential }) => signInWithGoogle(credential), }); const width = Math.max(280, Math.floor(googleBox.getBoundingClientRect().width || 320)); window.google.accounts.id.renderButton(googleBox, { theme: "outline", size: "large", shape: "pill", text: "continue_with", width, }); } window.addEventListener("popstate", render); document.addEventListener("visibilitychange", async () => { if (document.visibilityState !== "visible" || !state.profile || !state.sessionToken) return; try { state.profile = await request("/auth/session"); render(); } catch (_error) { persistSession(""); state.profile = null; setError("messages.sessionExpired"); ensureSigninRoute(); render(); mountGoogleButton(); } }); const googleScript = document.createElement("script"); googleScript.src = "https://accounts.google.com/gsi/client"; googleScript.async = true; googleScript.defer = true; googleScript.onload = () => { state.googleSdkReady = Boolean(window.google?.accounts?.id); if (!state.profile && !state.loading) { render(); mountGoogleButton(); } }; document.head.appendChild(googleScript); bootstrap();