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()}
${escapeHtml(renderSigninHelper())}
${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 = `
${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();