+ {t(
+ "Export one table from the AI report, then provide its website, dates and filters. Supported: page, daily history, and Bing grounding queries. Use exact counts, not Topic Portfolio opportunity ranges. CSV only; extract ZIP exports first. Maximum 2 MB / 2,000 rows.",
+ )}
+
+ {t(
+ "Compare only equal-length, non-overlapping periods from the same source, dimension, website and filters. Missing rows are unknown, not zero. Daily reports remain separate observations. Row sums are not site totals; different platforms are never added together.",
+ )}
+
{result.startDate} – {result.endDate} · {t("Google Search, web results")}
+
+ {t("Last successful data fetch")}: {new Date(result.fetchedAt).toLocaleString()}
+
+ {t("Saved search data is not a live connection check or a separate AI report.")}
{result.totals && (
diff --git a/apps/web/src/components/monitoring-start.tsx b/apps/web/src/components/monitoring-start.tsx
new file mode 100644
index 0000000..709e356
--- /dev/null
+++ b/apps/web/src/components/monitoring-start.tsx
@@ -0,0 +1,45 @@
+import { Link } from "@tanstack/react-router";
+import { useBrandParams } from "@/hooks/use-route-params";
+import { useT } from "@/lib/i18n";
+export function MonitoringStart() {
+ const t = useT(),
+ params = useBrandParams();
+ return (
+
+
+ {t(
+ "Question monitoring measures completed answer samples. Platform reports record site impressions or citations. SEO research and referral visits are separate evidence.",
+ )}
+
+
+ {t("Import Bing or Google AI reports")}
+
+
+ );
+}
diff --git a/apps/web/src/lib/i18n/zh.json b/apps/web/src/lib/i18n/zh.json
index 030fc75..76e5d23 100644
--- a/apps/web/src/lib/i18n/zh.json
+++ b/apps/web/src/lib/i18n/zh.json
@@ -145,7 +145,7 @@
"Chrome profile": "Chrome 配置",
"Citation Categories": "引用类别",
"Citation Page Types": "引用页面类型",
- "Citations": "引用",
+ "Citations": "引用次数",
"Clear": "清空",
"Clear filters": "清除筛选",
"Clear search": "清空搜索",
@@ -1185,7 +1185,7 @@
"comparable": "可对比",
"incompatible": "数据口径不兼容",
"insufficient_data": "数据不足",
- "query": "关键词",
+ "query": "查询",
"page": "页面",
"date": "日期",
"clicks": "点击次数",
@@ -1281,5 +1281,82 @@
"Page key events": "页面关键事件数",
"Page session key event rate": "页面会话关键事件率",
"Not enough recent answers to generate a new report. Your saved report is shown below.": "近期回答不足以生成新报告,下面仍展示已保存的报告。",
- "Acquisition, organic search, AI referral visits and crawler observations.": "渠道获客、自然搜索、AI 来源访问和爬虫观测。"
+ "Acquisition, organic search, AI referral visits and crawler observations.": "渠道获客、自然搜索、AI 来源访问和爬虫观测。",
+ "AI platform reports": "AI 平台报表",
+ "Question monitoring": "问题监测",
+ "AI visibility views": "AI 可见度视图",
+ "Import platform observations separately from your monitored questions. Imports do not enable automatic synchronization.": "平台统计与监控问题分别记录。导入报表不会开启自动同步。",
+ "Citations · Microsoft and supported partners": "引用次数 · 微软及支持的合作伙伴",
+ "Impressions · Google AI Overviews and AI Mode": "展示次数 · Google AI Overviews 与 AI Mode",
+ "Could not load AI reports.": "无法加载 AI 报表,请重试。",
+ "No AI report imported": "尚未导入 AI 报表",
+ "Open official dashboard": "打开官方后台",
+ "Import AI report CSV": "导入 AI 报表 CSV",
+ "Export one table from the AI report, then provide its website, dates and filters. Supported: page, daily history, and Bing grounding queries. Use exact counts, not Topic Portfolio opportunity ranges. CSV only; extract ZIP exports first. Maximum 2 MB / 2,000 rows.": "从 AI 报表导出单张表,并填写该报表的网站、日期和筛选条件。支持页面、每日历史及 Bing 检索查询。请使用精确次数,不支持主题机会区间。仅支持 CSV;ZIP 请先解压。最多 2 MB、2,000 行。",
+ "Report source": "报表来源",
+ "Report dimension": "统计维度",
+ "Daily history": "每日历史",
+ "Grounding queries": "检索查询",
+ "Report website": "报表网站",
+ "Report start date": "统计开始日期",
+ "Report end date": "统计结束日期",
+ "Filters and coverage": "筛选条件与覆盖范围",
+ "For example: all countries, text search, no page filter": "例如:所有国家、文本搜索、未筛选页面",
+ "AI report CSV": "AI 报表 CSV 文件",
+ "Import preview": "导入预览",
+ "I confirm this file comes from the selected AI report, and the website, dates and filters match the export. Ordinary search impressions are not AI impressions.": "我确认文件来自所选的 AI 专项报表,网站、日期和筛选条件与导出时一致。普通搜索曝光不能作为 AI 曝光导入。",
+ "Save AI report": "保存 AI 报表",
+ "AI report saved.": "AI 报表已保存。",
+ "This report is already saved; no counts were added.": "此报表已保存,没有重复计入。",
+ "Saved AI observations": "已保存的 AI 观测",
+ "Saved report": "历史报表",
+ "Compare with earlier report": "对比更早的报表",
+ "No comparison": "不对比",
+ "Imported at": "导入时间",
+ "Compare only equal-length, non-overlapping periods from the same source, dimension, website and filters. Missing rows are unknown, not zero. Daily reports remain separate observations. Row sums are not site totals; different platforms are never added together.": "仅对比来源、维度、网站、筛选条件相同,且等长、不重叠的期间。缺失行表示未知,不算零;每日历史独立展示。行数值相加不代表全站总量,不同平台不合计。",
+ "Filter observations": "筛选观测记录",
+ "No matching observations.": "没有匹配的记录。",
+ "Remove this imported report? The original file is unchanged.": "移除此导入报表?原始文件不会被修改。",
+ "Remove import": "移除导入",
+ "Could not remove AI report.": "无法移除 AI 报表。",
+ "Could not save AI report.": "无法保存 AI 报表。",
+ "Could not read CSV.": "无法读取 CSV 文件。",
+ "Your AI monitoring workflow": "开始 AI 数据监控",
+ "1. Check connections": "1. 检查数据连接",
+ "2. Choose monitoring platforms": "2. 选择监测平台",
+ "3. Review monitoring questions": "3. 确认监控问题",
+ "4. Collect and review answers": "4. 采集并检查回答",
+ "Question monitoring measures completed answer samples. Platform reports record site impressions or citations. SEO research and referral visits are separate evidence.": "问题监测统计已完成的回答样本;平台报表记录网站展示或引用。SEO 研究和引荐流量作为独立证据查看。",
+ "Import Bing or Google AI reports": "导入 Bing / Google AI 报表",
+ "Monitoring platforms": "监测平台",
+ "Last successful data fetch": "最近成功获取数据",
+ "Saved search data is not a live connection check or a separate AI report.": "此处为已保存的搜索数据,不代表连接实时正常,也不是独立 AI 报表。",
+ "Authorization saved": "已保存授权",
+ "Saved authorization does not guarantee the next refresh will succeed. Connection errors do not erase the saved report shown below.": "已保存授权不代表下次刷新一定成功。连接报错时,下方已保存的历史报表仍可查看。",
+ "Could not reach Google authorization. Try again.": "暂时无法连接 Google 授权服务,请重试。",
+ "Invalid report website.": "报表网站格式无效。",
+ "CSV is too large (maximum 2 MB).": "CSV 过大,最多支持 2 MB。",
+ "Unclosed CSV quote.": "CSV 引号未闭合。",
+ "Invalid CSV quoting.": "CSV 引号格式无效。",
+ "CSV has more than 2,000 rows. Export a smaller report.": "CSV 超过 2,000 行,请缩小导出范围。",
+ "Unsupported AI report.": "不支持此 AI 报表类型。",
+ "Google AI query exports are not supported.": "暂不支持 Google AI 查询词导入。",
+ "Choose valid report dates.": "请选择有效的统计日期。",
+ "Describe the report filters and coverage.": "请填写报表的筛选条件与覆盖范围。",
+ "Use a two-column AI report: Page/Date (or Bing Grounding Query) and Citations/Impressions.": "请使用两列 AI 报表:页面/日期(或 Bing 检索查询)及引用次数/展示次数。",
+ "The CSV contains no observations.": "CSV 没有观测数据。",
+ "Each CSV row must have two columns.": "CSV 每行必须有两列。",
+ "Empty or duplicate report rows are not supported.": "不支持空白或重复的记录。",
+ "Page URLs must belong to the report website.": "页面网址必须属于填写的报表网站。",
+ "A date is outside the report period.": "数据日期超出了填写的统计期间。",
+ "Use exact non-negative counts; ranges and K/M abbreviations are not supported.": "请使用精确的非负整数,不支持区间或 K/M 缩写。",
+ "Invalid report count.": "报表次数无效。",
+ "Choose a website saved in this brand.": "请选择当前品牌已保存的网站。",
+ "Report limit reached. Remove an unused import first.": "已达到 100 份报表上限,请先移除不再需要的导入。",
+ "Invalid report.": "报表格式无效。",
+ "Pages": "页面",
+ "Loading…": "加载中…",
+ "Saving…": "正在保存…",
+ "rows": "行",
+ "Change": "变化"
}
diff --git a/apps/web/src/routes/_authed/app/index.tsx b/apps/web/src/routes/_authed/app/index.tsx
index f1d391d..77a8030 100644
--- a/apps/web/src/routes/_authed/app/index.tsx
+++ b/apps/web/src/routes/_authed/app/index.tsx
@@ -6,7 +6,7 @@ export const Route = createFileRoute("/_authed/app/")({
loader: async ({ context }) => {
const { organizations } = await context.queryClient.ensureQueryData(organizationsQuery);
const website = context.localNavigation?.website ?? firstWebsite(organizations);
- if (website) throw redirect({ to: "/app/org/$org/brand/$brand/seo", params: website });
+ if (website) throw redirect({ to: "/app/org/$org/brand/$brand", params: website });
throw redirect({ to: "/setup" });
},
});
diff --git a/apps/web/src/routes/_authed/app/org/$org/brand/$brand/index.tsx b/apps/web/src/routes/_authed/app/org/$org/brand/$brand/index.tsx
index 7b08c6e..ed994fb 100644
--- a/apps/web/src/routes/_authed/app/org/$org/brand/$brand/index.tsx
+++ b/apps/web/src/routes/_authed/app/org/$org/brand/$brand/index.tsx
@@ -9,6 +9,7 @@ import { Skeleton } from "@workspace/ui/components/skeleton";
import { Tooltip, TooltipContent, TooltipTrigger } from "@workspace/ui/components/tooltip";
import { type ReactNode, useEffect } from "react";
import { FirstCollectionStatus } from "@/components/first-collection-status";
+import { MonitoringStart } from "@/components/monitoring-start";
import { PageHeader } from "@/components/page-header";
import PromptWizard from "@/components/prompt-wizard";
import { SiteIcon } from "@/components/site-icon";
@@ -508,12 +509,22 @@ function DashboardPage() {
const rankingDomains = rankingDomainsFor(brand);
if (!isLoadingBrand && !brand?.onboarded) {
- return ;
+ return (
+
+
+
+
+ );
}
// No runs yet: the dashboard has nothing to plot, so point at what to do next.
if (!loadingVisibility && totalRuns === 0) {
- return 0} />;
+ return (
+
+
+ 0} />
+
+ );
}
return (
diff --git a/apps/web/src/routes/_authed/app/org/$org/brand/$brand/settings/llms.tsx b/apps/web/src/routes/_authed/app/org/$org/brand/$brand/settings/llms.tsx
index a488330..a015a1a 100644
--- a/apps/web/src/routes/_authed/app/org/$org/brand/$brand/settings/llms.tsx
+++ b/apps/web/src/routes/_authed/app/org/$org/brand/$brand/settings/llms.tsx
@@ -1,3 +1,4 @@
+import { MonitoringStart } from "@/components/monitoring-start";
import { useT } from "@/lib/i18n";
/**
* Which platforms a brand is tracked against, grouped by what each group tells
@@ -32,7 +33,7 @@ import { useWriteErrorMessage } from "@/lib/write-errors";
import { getModelPickerStateFn, type ModelPickerState, updateEnabledModelsFn } from "@/server/platform-picks";
export const Route = createFileRoute("/_authed/app/org/$org/brand/$brand/settings/llms")({
- staticData: { crumb: "Models" },
+ staticData: { crumb: "Monitoring platforms" },
loader: async ({ context }): Promise<{ picker: ModelPickerState }> => ({
picker: await getModelPickerStateFn({ data: { brandId: context.brandId } }),
}),
@@ -46,8 +47,9 @@ function LlmsSettingsPage() {
return (
+
-
{t("Models")}
+
{t("Monitoring platforms")}
{t(
"Your questions are evaluated against these AI models to track how your brand appears across different types of AI search.",
diff --git a/apps/web/src/routes/_authed/app/org/$org/brand/$brand/visibility.tsx b/apps/web/src/routes/_authed/app/org/$org/brand/$brand/visibility.tsx
index 2ab0bf8..3152847 100644
--- a/apps/web/src/routes/_authed/app/org/$org/brand/$brand/visibility.tsx
+++ b/apps/web/src/routes/_authed/app/org/$org/brand/$brand/visibility.tsx
@@ -1,4 +1,6 @@
import { createFileRoute, Link } from "@tanstack/react-router";
+import { AiPlatformReports } from "@/components/ai-platform-reports";
+import { MonitoringStart } from "@/components/monitoring-start";
import { PromptsDisplay } from "@/components/prompts-display";
import { useBrandParams } from "@/hooks/use-route-params";
import { useT } from "@/lib/i18n";
@@ -7,9 +9,12 @@ import { pageHead } from "@/lib/route-head";
export const Route = createFileRoute("/_authed/app/org/$org/brand/$brand/visibility")({
staticData: { crumb: "Visibility" },
- validateSearch: (search: Record): { order?: PromptOrder } => {
+ validateSearch: (search: Record): { order?: PromptOrder; view?: "reports" } => {
const order = coercePromptOrder(search.order);
- return order === DEFAULT_PROMPT_ORDER ? {} : { order };
+ return {
+ ...(order === DEFAULT_PROMPT_ORDER ? {} : { order }),
+ ...(search.view === "reports" ? { view: "reports" as const } : {}),
+ };
},
head: pageHead({ description: "Track how LLMs respond to prompts about your brand." }),
component: VisibilityPage,
@@ -18,6 +23,7 @@ export const Route = createFileRoute("/_authed/app/org/$org/brand/$brand/visibil
function VisibilityPage() {
const t = useT();
const params = useBrandParams();
+ const { view } = Route.useSearch();
const infoContent = (
<>
@@ -30,10 +36,42 @@ function VisibilityPage() {
);
return (
-
+