Files

548 lines
26 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { dataFetch, isManagementAuthorized, managedFetch, onAccessChange, routes } from "../core/runtime.js";
import { localDateTimeValue, money, number, option, result } from "../core/shared.js";
const keyRowsNode = document.querySelector("#key-rows");
const keyStatusNode = document.querySelector("#key-status");
const resetAllBillingNode = document.querySelector("#reset-all-billing");
const keyStatsNode = document.querySelector("#key-stats");
const drawerNode = document.querySelector("#key-drawer");
const drawerOverlayNode = document.querySelector("#drawer-overlay");
const editorNode = document.querySelector("#key-editor");
const managedFieldsNode = document.querySelector("#managed-fields");
const editorFooterNode = document.querySelector("#editor-footer");
const statsGridNode = document.querySelector("#stats-grid");
const statsRecentNode = document.querySelector("#stats-recent");
let managedKeys = [];
let upstreamAccounts = [];
let modelSuggestions = [];
let usageDashboard = { today: { requests: 0, total_tokens: 0, cost_usd: 0 }, users: [], days: [] };
let editingKey = null;
export function initializeKeys() {
document.querySelector("#create-key").addEventListener("click", () => openKeyEditor());
resetAllBillingNode.addEventListener("click", resetAllManagedBilling);
document.querySelector("#close-drawer").addEventListener("click", closeKeyDrawer);
document.querySelector("#cancel-editor").addEventListener("click", closeKeyDrawer);
drawerOverlayNode.addEventListener("click", closeKeyDrawer);
document.querySelector("#save-key").addEventListener("click", saveKeyEditor);
document.querySelector("#archive-key").addEventListener("click", () => editingKey && archiveManagedKey(editingKey));
document.querySelector("#editor-route").addEventListener("change", syncEditorRoute);
document.querySelector("#editor-all-models").addEventListener("change", syncEditorRoute);
document.querySelector("#editor-reset-period").addEventListener("change", syncEditorRoute);
document.querySelector("#reset-billing").addEventListener("click", resetManagedBilling);
document.addEventListener("keydown", event => {
if (event.key === "Escape") closeKeyDrawer();
});
onAccessChange(authorized => {
if (!authorized) closeKeyDrawer();
if (managedKeys.length) renderManagedKeys();
});
}
export async function loadKeys() {
keyStatusNode.textContent = "正在读取";
try {
const [upstreams, models, summary] = await Promise.all([
dataFetch(routes.upstreams),
dataFetch(routes.models),
dataFetch(routes.usageSummary)
]);
upstreamAccounts = upstreams.accounts || [];
modelSuggestions = models.models || [];
usageDashboard = summary;
const keys = await dataFetch(routes.keys);
managedKeys = keys.keys || [];
renderManagedKeys();
resetAllBillingNode.disabled = !managedKeys.length;
keyStatusNode.textContent = isManagementAuthorized() ? `共 ${managedKeys.length} 个 Key${upstreamAccounts.length} 个上游账号${upstreams.warning ? ";同步警告:" + upstreams.warning : ""}` : "";
} catch (error) {
keyStatusNode.textContent = "读取失败: " + error.message;
}
}
export function closeKeyDrawer() {
drawerNode.classList.add("hidden");
drawerOverlayNode.classList.add("hidden");
drawerNode.setAttribute("aria-hidden", "true");
editingKey = null;
}
function openDrawer() {
drawerNode.classList.remove("hidden");
drawerOverlayNode.classList.remove("hidden");
drawerNode.setAttribute("aria-hidden", "false");
}
function openKeyEditor(key = null) {
editingKey = key;
const creating = !key;
document.querySelector("#drawer-title").textContent = creating ? "创建 Key" : `管理 ${key.name}`;
document.querySelector("#drawer-subtitle").textContent = creating ? "配置访问、模型、路由与初始额度" : "管理访问、模型、路由与额度";
editorNode.classList.remove("hidden");
keyStatsNode.classList.add("hidden");
editorFooterNode.classList.remove("hidden");
managedFieldsNode.classList.remove("hidden");
document.querySelector("#editor-name").value = key?.name || "";
const secret = document.querySelector("#editor-secret");
secret.value = key?.secret || "";
secret.readOnly = !creating;
document.querySelector("#editor-status").value = key?.status || "active";
document.querySelector("#editor-route").value = key?.route_mode || "auto";
const upstream = document.querySelector("#editor-upstream");
upstream.replaceChildren(option("", "未指定", !key?.upstream_account_id), ...upstreamAccounts.map(account => option(account.id, `${upstreamLabel(account)}${account.disabled || account.unavailable ? "(不可用)" : ""}`, account.id === key?.upstream_account_id)));
document.querySelector("#editor-all-models").checked = key?.all_models ?? true;
document.querySelector("#editor-show-stats").checked = key?.show_in_stats ?? true;
const models = document.querySelector("#editor-models");
models.value = (key?.models || []).join(", ");
models.placeholder = modelSuggestions.slice(0, 3).join(", ") || "gpt-5.6-sol";
const billing = key?.billing || { quota_usd: "0", spent_usd: "0", balance_usd: "0", reset_period: "none", max_concurrency: 4, active_requests: 0 };
document.querySelector("#editor-quota").value = billing.quota_usd || "0";
document.querySelector("#editor-concurrency").value = billing.max_concurrency || 4;
document.querySelector("#editor-reset-period").value = billing.reset_period || "none";
document.querySelector("#editor-next-reset").value = localDateTimeValue(billing.next_reset_at);
document.querySelector("#billing-quota-view").textContent = money(billing.quota_usd);
document.querySelector("#billing-spent-view").textContent = money(billing.spent_usd);
document.querySelector("#billing-balance-view").textContent = money(billing.balance_usd);
document.querySelector("#billing-status").textContent = creating ? "新用户默认额度为 $0" : `${billing.active_requests || 0} 个请求正在执行`;
document.querySelector("#billing-preview").classList.toggle("hidden", creating);
document.querySelector("#ledger-section").classList.toggle("hidden", creating);
document.querySelector("#reset-billing").classList.toggle("hidden", creating || key?.status === "archived");
document.querySelector("#archive-key").classList.toggle("hidden", creating || key.status === "archived");
document.querySelector("#save-key").classList.toggle("hidden", key?.status === "archived");
document.querySelector("#save-key").textContent = creating ? "创建 Key" : "保存修改";
["editor-name", "editor-status", "editor-route", "editor-all-models", "editor-show-stats", "editor-quota", "editor-concurrency", "editor-reset-period"].forEach(id => document.querySelector("#" + id).disabled = key?.status === "archived");
syncEditorRoute();
openDrawer();
if (!creating) loadBillingLedger(key);
}
function renderManagedKeys() {
keyRowsNode.replaceChildren(...managedKeys.map(key => {
const card = document.createElement("article");
card.className = "user-card";
const archived = key.status === "archived";
const identity = document.createElement("button");
identity.type = "button";
identity.className = "key-identity key-copy";
identity.disabled = !key.secret;
identity.title = "点击复制 Key";
const avatar = document.createElement("span");
avatar.className = "avatar";
avatar.textContent = (key.name || "K").slice(0, 2).toUpperCase();
const identityText = document.createElement("div");
const name = document.createElement("strong");
name.textContent = key.name;
identityText.append(name);
identity.append(avatar, identityText);
identity.addEventListener("click", () => copyText(key.secret, name));
const header = document.createElement("header");
header.className = "user-card-header";
const billing = key.billing || {};
const resetPeriod = billing.reset_period && billing.reset_period !== "none" ? ({ daily: "每天重置", weekly: "每周重置", monthly: "每月重置" }[billing.reset_period] || billing.reset_period) : "不重置";
const details = document.createElement("div");
details.className = "user-card-details";
details.append(
userCardDetail("剩余额度", money(billing.balance_usd), resetPeriod),
userCardDetail("总额度", money(billing.quota_usd), `累计消耗 ${money(billing.lifetime_spent_usd)}`),
userCardDetail("并发", `${billing.active_requests || 0} / ${billing.max_concurrency || 4}`, "执行中 / 上限"),
userCardDetail("路由", routeLabel(key), "")
);
const modelBlock = document.createElement("div");
modelBlock.className = "user-card-line";
const models = document.createElement("strong");
models.textContent = key.all_models ? "全部模型" : (key.models || []).join(", ") || "未配置";
modelBlock.append(Object.assign(document.createElement("span"), { textContent: "允许模型" }), models);
const actions = document.createElement("div");
actions.className = "key-actions";
const stats = document.createElement("button");
stats.type = "button";
stats.className = "small";
stats.textContent = "统计";
stats.addEventListener("click", () => loadKeyStats(key));
actions.append(stats);
if (isManagementAuthorized()) {
const manage = document.createElement("button");
manage.type = "button";
manage.className = "small admin-only";
manage.textContent = archived ? "查看" : "管理";
manage.addEventListener("click", () => openKeyEditor(key));
actions.append(manage);
}
header.append(identity, actions);
card.append(header, details, modelBlock);
return card;
}));
renderUserCharts();
}
function userCardDetail(label, value, hint) {
const item = document.createElement("div");
item.className = "user-card-detail";
const content = document.createElement("div");
content.append(Object.assign(document.createElement("strong"), { textContent: value }));
if (hint) content.append(Object.assign(document.createElement("small"), { textContent: hint }));
item.append(
Object.assign(document.createElement("span"), { textContent: label }),
content
);
return item;
}
function renderUserCharts() {
const visibleKeys = managedKeys.filter(key => key.show_in_stats !== false);
const byUser = new Map(visibleKeys.map(key => [key.name, {
name: key.name, requests: 0, tokens: 0, cost: 0,
balance: key.billing ? Number(key.billing.balance_usd || 0) : null,
resetPeriod: key.billing?.reset_period || null,
nextResetAt: key.billing?.next_reset_at ? new Date(key.billing.next_reset_at) : null,
days: [], last: null
}]));
(usageDashboard.users || []).forEach(user => {
const name = user.key_alias || "未识别";
if (!byUser.has(name)) byUser.set(name, { name, requests: 0, tokens: 0, cost: 0, balance: null, resetPeriod: null, nextResetAt: null, days: [], last: null });
const value = byUser.get(name);
const today = user.today || {};
value.requests = today.requests || 0;
value.tokens = today.total_tokens || 0;
value.cost = today.cost_usd || 0;
value.days = user.days || [];
value.last = user.last_used_at ? new Date(user.last_used_at) : null;
});
const userRows = [...byUser.values()].sort((left, right) => right.tokens - left.tokens || left.name.localeCompare(right.name));
const header = document.createElement("div");
header.className = "user-usage-row header";
["用户", "今日请求", "今日 Token", "今日使用", "剩余额度", "最近使用", "重置时间"].forEach(label => {
const cell = document.createElement("span");
cell.textContent = label;
header.append(cell);
});
document.querySelector("#user-usage").replaceChildren(header, ...userRows.map(item => {
const row = document.createElement("div");
row.className = "user-usage-row";
[item.name, number(item.requests), number(item.tokens), quotaAmount(item.cost), quotaAmount(item.balance), item.last ? item.last.toLocaleString() : "从未", resetTime(item)].forEach(value => {
const cell = document.createElement("span");
cell.textContent = value;
row.append(cell);
});
return row;
}));
const days = [...(usageDashboard.days || [])].reverse().map(item => ({ date: new Date(item.date + "T00:00:00"), cost: Number(item.cost_usd || 0) }));
renderQuotaMatrix(days, [...byUser.values()]);
renderStatsInsights(visibleKeys);
}
function quotaAmount(value) {
if (value === null) return "—";
const amount = Number(value);
return Number.isFinite(amount) ? amount.toFixed(2) : "—";
}
function resetTime(item) {
if (item.resetPeriod === "none") return "永不";
if (!item.nextResetAt || !Number.isFinite(item.nextResetAt.getTime())) return "—";
const value = item.nextResetAt;
const parts = [
value.getFullYear(),
String(value.getMonth() + 1).padStart(2, "0"),
String(value.getDate()).padStart(2, "0")
];
return `${parts.join("-")} ${String(value.getHours()).padStart(2, "0")}:${String(value.getMinutes()).padStart(2, "0")}`;
}
function renderStatsInsights(visibleKeys) {
renderQuotaHealth(visibleKeys);
}
function renderQuotaHealth(keys) {
const items = keys.map(key => {
const billing = key.billing || {};
const quota = Number(billing.quota_usd || 0);
const balance = Number(billing.balance_usd || 0);
const ratio = quota > 0 ? balance / quota : null;
return { key, billing, quota, balance, ratio };
}).sort((left, right) => {
if (left.ratio === null) return right.ratio === null ? left.key.name.localeCompare(right.key.name) : 1;
if (right.ratio === null) return -1;
return left.ratio - right.ratio || left.key.name.localeCompare(right.key.name);
});
const node = document.querySelector("#quota-health");
if (!items.length) {
node.replaceChildren(Object.assign(document.createElement("div"), { className: "empty", textContent: "暂无展示中的用户" }));
return;
}
node.replaceChildren(...items.map(item => {
const row = document.createElement("div");
row.className = "quota-health-row";
const heading = document.createElement("div");
heading.className = "quota-health-heading";
const identity = document.createElement("strong");
identity.textContent = item.key.name;
const percent = item.ratio === null ? null : Math.round(item.ratio * 100);
const detail = document.createElement("span");
const active = Number(item.billing.active_requests || 0);
const maximum = Number(item.billing.max_concurrency || 4);
detail.textContent = item.ratio === null
? `未配置周期额度 · 当前并发 ${active} / ${maximum}`
: `${money(item.balance)} / ${money(item.quota)} · 剩余 ${percent}% · 当前并发 ${active} / ${maximum}`;
detail.className = item.ratio !== null && item.ratio <= 0.2 ? "danger" : "";
heading.append(identity, detail);
const track = document.createElement("div");
track.className = "quota-health-track";
const bar = document.createElement("div");
bar.className = "quota-health-bar";
if (item.ratio !== null && item.ratio <= 0.2) bar.classList.add("danger");
bar.style.width = `${item.ratio === null ? 0 : Math.max(0, Math.min(100, percent))}%`;
track.append(bar);
row.append(heading, track);
return row;
}));
}
function renderQuotaMatrix(days, users) {
const dates = days.map(day => dateKey(day.date));
const rows = users.map(user => {
const costs = new Map(user.days.map(day => [day.date, Number(day.cost_usd || 0)]));
const values = dates.map(date => costs.get(date) || 0);
return { name: user.name, values, total: values.reduce((sum, value) => sum + value, 0) };
}).sort((left, right) => right.total - left.total || left.name.localeCompare(right.name));
const maximum = Math.max(0.000001, ...rows.flatMap(row => row.values));
const matrix = document.createElement("div");
matrix.className = "quota-matrix";
matrix.style.gridTemplateColumns = `minmax(100px, 1.25fr) repeat(${days.length}, minmax(74px, 1fr)) minmax(82px, 1fr)`;
matrix.append(quotaCell("用户", "header user"), ...days.map(day => quotaCell(`${day.date.getMonth() + 1}/${day.date.getDate()}`, "header")), quotaCell("7 日合计", "header total"));
rows.forEach(row => {
matrix.append(quotaCell(row.name, "user"));
row.values.forEach((value, index) => {
const cell = quotaCell(value ? money(value) : "—", value ? "usage" : "");
cell.style.setProperty("--quota-intensity", `${12 + value / maximum * 58}%`);
cell.title = `${row.name} · ${dates[index]} · ${money(value)}`;
matrix.append(cell);
});
matrix.append(quotaCell(money(row.total), "total"));
});
const weekTotal = days.reduce((sum, day) => sum + day.cost, 0);
matrix.append(quotaCell("每日合计", "header user"), ...days.map(day => quotaCell(money(day.cost), "total")), quotaCell(money(weekTotal), "total"));
document.querySelector("#quota-chart").replaceChildren(matrix);
}
function quotaCell(text, classes = "") {
const cell = document.createElement("div");
cell.className = "quota-cell" + (classes ? " " + classes : "");
cell.textContent = text;
return cell;
}
function dateKey(value) {
const year = value.getFullYear();
const month = String(value.getMonth() + 1).padStart(2, "0");
const day = String(value.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
}
async function createManagedKey() {
const route = document.querySelector("#editor-route").value;
const allModels = document.querySelector("#editor-all-models").checked;
const payload = {
name: document.querySelector("#editor-name").value.trim(),
secret: document.querySelector("#editor-secret").value.trim(),
route_mode: route,
upstream_account_id: route === "strict" ? document.querySelector("#editor-upstream").value : "",
all_models: allModels,
show_in_stats: document.querySelector("#editor-show-stats").checked,
models: modelRules(),
billing: billingPayload()
};
try {
const created = await managedFetch(routes.keys, { method: "POST", body: JSON.stringify(payload) });
keyStatusNode.textContent = `已创建 ${created.name}${created.secret}`;
closeKeyDrawer();
await loadKeys();
} catch (error) {
keyStatusNode.textContent = "创建失败: " + error.message;
}
}
async function saveKeyEditor() {
if (!editingKey) return createManagedKey();
const route = document.querySelector("#editor-route").value;
try {
await managedFetch(routes.keys, { method: "PATCH", body: JSON.stringify({
id: editingKey.id,
name: document.querySelector("#editor-name").value.trim(),
status: document.querySelector("#editor-status").value,
route_mode: route,
upstream_account_id: route === "strict" ? document.querySelector("#editor-upstream").value : "",
all_models: document.querySelector("#editor-all-models").checked,
show_in_stats: document.querySelector("#editor-show-stats").checked,
models: modelRules(),
billing: billingPayload()
}) });
closeKeyDrawer();
await loadKeys();
} catch (error) {
keyStatusNode.textContent = "保存失败: " + error.message;
}
}
async function loadBillingLedger(key) {
const node = document.querySelector("#billing-ledger");
node.replaceChildren(Object.assign(document.createElement("div"), { className: "empty", textContent: "正在读取" }));
try {
const entries = (await managedFetch(routes.billingLedger + "?id=" + encodeURIComponent(key.id) + "&limit=50")).entries || [];
node.replaceChildren(...(entries.length ? entries.map(entry => {
const row = document.createElement("div");
row.className = "ledger-item";
const label = document.createElement("strong");
label.textContent = ({ charge: "请求扣费", quota_change: "调整额度", cycle_reset: "额度重置" }[entry.kind] || entry.kind);
const amount = document.createElement("strong");
const numeric = Number(entry.amount_usd || 0);
amount.className = numeric < 0 ? "negative" : "positive";
amount.textContent = (numeric > 0 ? "+" : "") + money(numeric);
const detail = document.createElement("small");
detail.textContent = `${new Date(entry.occurred_at).toLocaleString()} · 余额 ${money(entry.balance_after_usd)}${entry.model ? " · " + entry.model : ""}${entry.request_id ? " · " + entry.request_id : ""}`;
row.append(label, amount, detail);
return row;
}) : [Object.assign(document.createElement("div"), { className: "empty", textContent: "暂无账目" })]));
} catch (error) {
node.replaceChildren(Object.assign(document.createElement("div"), { className: "empty", textContent: "读取失败:" + error.message }));
}
}
async function resetManagedBilling() {
if (!editingKey || !confirm(`立即重置 ${editingKey.name} 的额度?当前剩余余额不会结转。`)) return;
try {
await managedFetch(routes.billingReset, { method: "POST", body: JSON.stringify({ id: editingKey.id }) });
await loadKeys();
const refreshed = managedKeys.find(item => item.id === editingKey.id);
if (refreshed) openKeyEditor(refreshed);
} catch (error) {
document.querySelector("#billing-status").textContent = "重置失败:" + error.message;
}
}
async function resetAllManagedBilling() {
const targets = managedKeys.filter(key => key.status !== "archived");
if (!targets.length || !confirm(`重置全部 ${targets.length} 个用户的额度?当前剩余余额不会结转。`)) return;
resetAllBillingNode.disabled = true;
keyStatusNode.textContent = `正在重置 ${targets.length} 个用户`;
try {
const result = await managedFetch(routes.billingReset, { method: "POST", body: JSON.stringify({ all: true }) });
await loadKeys();
keyStatusNode.textContent = `已重置全部 ${result.reset_count || 0} 个用户的额度`;
} catch (error) {
keyStatusNode.textContent = "重置失败:" + error.message;
} finally {
resetAllBillingNode.disabled = false;
}
}
async function archiveManagedKey(key) {
if (!confirm(`永久归档 ${key.name}?该 Key 将不能恢复,但历史统计会保留。`)) return;
try {
await managedFetch(routes.keys, { method: "DELETE", body: JSON.stringify({ id: key.id }) });
closeKeyDrawer();
await loadKeys();
} catch (error) {
keyStatusNode.textContent = "归档失败: " + error.message;
}
}
async function loadKeyStats(key) {
try {
const payload = await dataFetch(routes.keyStats + "?id=" + encodeURIComponent(key.id));
const total = payload.stats.total;
const today = payload.stats.today;
document.querySelector("#drawer-title").textContent = `${key.name} · 统计`;
document.querySelector("#drawer-subtitle").textContent = "历史统计永久保留";
editorNode.classList.add("hidden");
editorFooterNode.classList.add("hidden");
keyStatsNode.classList.remove("hidden");
statsGridNode.replaceChildren(...[
["今日请求", number(today.requests)], ["今日 Token", number(today.total_tokens)],
["累计请求", number(total.requests)], ["累计成本", "$" + (total.cost_micros / 1000000).toFixed(4)]
].map(([label, value]) => {
const tile = document.createElement("div");
tile.className = "stats-tile";
const name = document.createElement("span");
name.textContent = label;
const strong = document.createElement("strong");
strong.textContent = value;
tile.append(name, strong);
return tile;
}));
const recent = (payload.recent || []).slice(0, 10);
statsRecentNode.replaceChildren(...(recent.length ? recent.map(item => {
const row = document.createElement("div");
row.className = "recent-item";
const model = document.createElement("strong");
model.textContent = `${item.model || "-"} · ${result(item)}`;
const detail = document.createElement("span");
detail.textContent = `${new Date(item.requested_at).toLocaleString()} · ${number(item.total_tokens)} Token · ${item.auth_id || "自动选择"}`;
row.append(model, detail);
return row;
}) : [Object.assign(document.createElement("div"), { className: "empty", textContent: "暂无请求" })]));
openDrawer();
} catch (error) {
keyStatusNode.textContent = "读取统计失败: " + error.message;
}
}
function syncEditorRoute() {
const archived = editingKey?.status === "archived";
const strict = document.querySelector("#editor-route").value === "strict";
document.querySelector("#editor-upstream").disabled = archived || !strict;
document.querySelector("#editor-models").disabled = archived || document.querySelector("#editor-all-models").checked;
const period = document.querySelector("#editor-reset-period").value;
const nextReset = document.querySelector("#editor-next-reset");
nextReset.disabled = archived || period === "none";
if (period !== "none" && !nextReset.value) {
const next = new Date();
if (period === "daily") next.setDate(next.getDate() + 1);
else if (period === "weekly") next.setDate(next.getDate() + 7);
else next.setMonth(next.getMonth() + 1);
nextReset.value = localDateTimeValue(next);
}
}
function billingPayload() {
const period = document.querySelector("#editor-reset-period").value;
const nextValue = document.querySelector("#editor-next-reset").value;
return {
quota_usd: document.querySelector("#editor-quota").value.trim() || "0",
reset_period: period,
next_reset_at: period === "none" || !nextValue ? null : new Date(nextValue).toISOString(),
max_concurrency: Number(document.querySelector("#editor-concurrency").value || 4)
};
}
function modelRules() {
return document.querySelector("#editor-models").value.split(",").map(value => value.trim()).filter(Boolean);
}
function upstreamLabel(account) {
const provider = account.provider || "unknown";
const name = account.display_name || account.cpa_auth_id || account.id;
const duplicated = upstreamAccounts.some(item => item !== account &&
(item.provider || "unknown") === provider &&
(item.display_name || item.cpa_auth_id || item.id) === name);
if (!duplicated) return `${provider} · ${name}`;
const identity = (account.cpa_auth_index || account.cpa_auth_id || account.id).slice(0, 8);
return `${provider} · ${name} · ${identity}`;
}
function routeLabel(key) {
if (key.route_mode !== "strict") return "自由选择";
const account = upstreamAccounts.find(item => item.id === key.upstream_account_id);
return account ? account.display_name || account.cpa_auth_id || account.id : "未指定";
}
function copyText(value, button) {
if (!navigator.clipboard) return;
navigator.clipboard.writeText(value).then(() => {
const previous = button.textContent;
button.textContent = "已复制";
setTimeout(() => { button.textContent = previous; }, 1200);
});
}