diff --git a/.changeset/beatgeo-dark-aeo-workspace.md b/.changeset/beatgeo-dark-aeo-workspace.md new file mode 100644 index 00000000..15e28e14 --- /dev/null +++ b/.changeset/beatgeo-dark-aeo-workspace.md @@ -0,0 +1,5 @@ +--- +"@workspace/web": patch +--- + +Unify the AEO workspace in dark mode and separate recommendations, actions, evidence and website checks with clearer collection status and bilingual guidance. diff --git a/.changeset/beatgeo-insight-navigation.md b/.changeset/beatgeo-insight-navigation.md new file mode 100644 index 00000000..910d12c8 --- /dev/null +++ b/.changeset/beatgeo-insight-navigation.md @@ -0,0 +1,5 @@ +--- +"@workspace/web": patch +--- + +Group answer analytics into a shared navigation with persistent filters and a more compact dark workspace. diff --git a/.changeset/beatgeo-workbench-refinement.md b/.changeset/beatgeo-workbench-refinement.md new file mode 100644 index 00000000..7d42e364 --- /dev/null +++ b/.changeset/beatgeo-workbench-refinement.md @@ -0,0 +1,5 @@ +--- +"@workspace/web": patch +--- + +Refine the dark workbench with locally bundled typography, compact tabs and flat AEO sections. diff --git a/.changeset/bright-local-workspace.md b/.changeset/bright-local-workspace.md new file mode 100644 index 00000000..46ad89e3 --- /dev/null +++ b/.changeset/bright-local-workspace.md @@ -0,0 +1,6 @@ +--- +"@workspace/web": patch +"@workspace/worker": patch +"@workspace/lib": patch +--- +Simplify local setup with bundled PostgreSQL and add worker diagnostics, website checks, reviewed content drafts and single-question collection controls. diff --git a/AGENTS.md b/AGENTS.md index 60ddec41..5ae61b0f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -49,6 +49,7 @@ Before handing work back or opening a PR, run `pnpm lint` and get it passing; CI - **Always use pnpm.** Never install or run dependencies with npm, yarn, or `npx` — that sidesteps the workspace's protections. - This repo enforces [pnpm supply-chain security](https://pnpm.io/supply-chain-security) via `pnpm-workspace.yaml`: `minimumReleaseAge` (a multi-day cooldown on new releases), `trustPolicy: no-downgrade`, `blockExoticSubdeps`, and an `allowBuilds` allowlist for install scripts. +- Owner-approved exception (2026-10-16): the exact `@embedded-postgres/*@17.10.0-beta.17` platform builds may restore their package-local native library symlinks for bundled local storage. Version upgrades need a fresh review; all other protections below still apply. - **Never weaken or bypass these controls**: don't add `minimumReleaseAgeExclude` entries, don't flip packages to `true` in `allowBuilds`, don't suppress `pnpm audit` advisories, and don't remove `overrides` (many are scoped security patches or dedup anchors). If an install fails because of these controls, that is the system working — report it instead of working around it. ## Environment diff --git a/README.de.md b/README.de.md index 8d35b7bd..2330cdc3 100644 --- a/README.de.md +++ b/README.de.md @@ -23,7 +23,7 @@ Gib deine Website und dein Geschäftsfeld an, bestätige die vorgeschlagenen Fra ## Schnellstart -Benötigt werden **Git, Node.js 24, pnpm 12.4.1, Docker mit Compose und Chrome**. Starte Docker vor der Einrichtung. +Benötigt werden **Git, Node.js 24, pnpm 12.4.1 und Chrome**. BeatGEO bringt PostgreSQL mit und verwaltet es automatisch. Docker und eine separate Datenbankinstallation sind nicht erforderlich. ```sh git clone https://github.com/BeatAPI/BeatGEO.git @@ -41,7 +41,7 @@ pnpm local:start Das Setup richtet eine eigene lokale PostgreSQL-Datenbank und die Erweiterung ein. Kein zweiter Repository-Klon, kein zusätzliches BeatBrowser-Konto und keine globale CLI-Installation nötig. Eine vorhandene BeatBrowser-Verbindung kann verwendet werden; pro Chrome-Profil nur eine Erweiterungskopie aktivieren. -**Für laufendes Monitoring muss die Umgebung laufen:** Computer wach und online halten; Docker/PostgreSQL, Chrome und `pnpm local:start` müssen aktiv bleiben. Der Dashboard-Tab darf geschlossen werden. Zum Neustart reicht `pnpm local:start`; die Datenbank nicht neu anlegen. Bei ausgeschaltetem oder offline befindlichem Computer findet keine Erfassung statt. +**Für laufendes Monitoring muss die Umgebung laufen:** Computer wach und online halten; Chrome und `pnpm local:start` müssen aktiv bleiben. Der Dashboard-Tab darf geschlossen werden. Zum Neustart reicht `pnpm local:start`; die Datenbank nicht neu anlegen. Bei ausgeschaltetem oder offline befindlichem Computer findet keine Erfassung statt. [Ausführliche Einrichtung auf Chinesisch](docs/beat-geo/getting-started.md) · [Vorhandenes PostgreSQL einrichten, Chinesisch](docs/self-hosting.md) @@ -79,7 +79,8 @@ Den [interaktiven Entwurf](docs/product/flow.html) lokal öffnen. Er zeigt das * | Fragen und Verbesserungen | Fragen auf Englisch/Chinesisch, Branchenvorlagen, gespeicherte Ausgangswerte und vergleichbare Wiederholungstests. | | Planung und Speicherung | PostgreSQL + pg-boss. Der nächste Lauf wurde im Live-Test eingereiht; unbeaufsichtigter Dauerbetrieb bleibt zu prüfen. Kein SQLite. | | BeatAPI-Recherche | Suche, Seitenabruf und Modellintegration implementiert; Abnahme mit echtem Key und kostenpflichtigen Aufrufen ausstehend. | -| Technisches Website-Audit und SEO-Daten | Geplant oder nicht angeschlossen. Keine behaupteten Keyword-Volumina, Backlink-Indizes oder echten KI-Fragevolumina. | +| Website-Prüfung und Entwürfe | Begrenzte HTML-/robots-/Sitemap-Prüfung, kostenlose Gliederungen, Bearbeitung und Freigabe je Version. Grenzen der Netzwerk- und kostenpflichtigen KI-Tests stehen im Prüfbericht. | +| SEO-Daten | Nicht angeschlossen. Keine Keyword-Volumina, Backlink-Indizes oder echten KI-Fragevolumina. | Die README gibt es auf Englisch, Chinesisch, Japanisch, Deutsch und Koreanisch. **Die Produktoberfläche lässt sich über EN / 中文 zwischen Englisch und Chinesisch umschalten. Die Auswahl wird im Browser gespeichert.** Fragen, Markennamen und gesammelte Antworten bleiben in ihrer ursprünglichen Sprache. Die README-Übersetzungen bedeuten keine fünfsprachige Oberfläche. diff --git a/README.ja.md b/README.ja.md index 10ccb3f4..928b4f64 100644 --- a/README.ja.md +++ b/README.ja.md @@ -23,7 +23,7 @@ ## クイックスタート -**Git、Node.js 24、pnpm 12.4.1、Docker(Compose を含む)、Chrome** を用意し、Docker を起動してください。 +**Git、Node.js 24、pnpm 12.4.1、Chrome** を用意してください。PostgreSQL は BeatGEO に同梱され、自動管理されます。Docker やデータベースの別途インストールは不要です。 ```sh git clone https://github.com/BeatAPI/BeatGEO.git @@ -41,7 +41,7 @@ pnpm local:start セットアップが専用のローカル PostgreSQL と拡張機能を準備します。BeatBrowser を別にクローンしたり、別アカウントやグローバル CLI を用意したりする必要はありません。既存の接続も利用できますが、同じ Chrome プロファイルで拡張機能を二重に有効化しないでください。 -**継続監視には稼働中の環境が必要です。** パソコンをスリープさせず、ネット接続、Docker/PostgreSQL、Chrome、`pnpm local:start` を維持します。ダッシュボードのタブは閉じても構いません。再開時は `pnpm local:start` を実行し、データベースは作り直さないでください。オフライン中の収集はできません。 +**継続監視には稼働中の環境が必要です。** パソコンをスリープさせず、ネット接続、Chrome、`pnpm local:start` を維持します。ダッシュボードのタブは閉じても構いません。再開時は `pnpm local:start` を実行し、データベースは作り直さないでください。オフライン中の収集はできません。 [詳しい導入手順(中国語)](docs/beat-geo/getting-started.md) · [既存 PostgreSQL の設定(中国語)](docs/self-hosting.md) @@ -79,7 +79,8 @@ BeatGEO が製品データとスケジュールを管理し、**BeatBrowser** | 質問と改善 | 英語・中国語の質問生成、業種別テンプレート、改善の基準値保存、同一質問での再測定。 | | スケジュールと保存 | PostgreSQL + pg-boss。実測では次回処理の予約まで確認。長期間の無人運用と SQLite には未対応です。 | | BeatAPI リサーチ | 検索・読み取り・モデル連携は実装済み。実キーによる有料呼び出しの受け入れ検証は未完了です。 | -| サイト技術監査・SEO データ | 計画中または未接続。検索量、被リンク索引、実際の AI 質問量を保有しているとは表明しません。 | +| サイト確認・コンテンツ下書き | 範囲を限定した HTML/robots/sitemap 確認、無料アウトライン、編集と版ごとの承認。公開ネットワークと有料生成の検証範囲はレポートを参照。 | +| SEO データ | 未接続。検索量、被リンク索引、実際の AI 質問量は提供しません。 | README は英語、中国語、日本語、ドイツ語、韓国語で提供します。**製品 UI は EN / 中文で英語と中国語を切り替えられ、選択はブラウザーに保存されます。** 質問、ブランド名、収集した回答の言語は変更しません。README の翻訳は UI の五言語対応を意味しません。 diff --git a/README.ko.md b/README.ko.md index bba40ef7..ca6e294e 100644 --- a/README.ko.md +++ b/README.ko.md @@ -23,7 +23,7 @@ ## 빠른 시작 -**Git, Node.js 24, pnpm 12.4.1, Compose가 포함된 Docker, Chrome**을 준비하고 Docker를 먼저 실행하세요. +**Git, Node.js 24, pnpm 12.4.1, Chrome**을 준비하세요. PostgreSQL은 BeatGEO에 포함되어 자동으로 관리됩니다. Docker나 별도의 데이터베이스를 설치할 필요가 없습니다. ```sh git clone https://github.com/BeatAPI/BeatGEO.git @@ -41,7 +41,7 @@ pnpm local:start 설치 과정에서 전용 로컬 PostgreSQL과 확장 프로그램을 준비합니다. 별도의 저장소 복제, BeatBrowser 계정, 전역 CLI 설치는 필요하지 않습니다. 기존 BeatBrowser 연결을 재사용할 수 있지만, Chrome 프로필 하나에는 확장 프로그램 한 개만 활성화하세요. -**지속적인 모니터링에는 실행 중인 환경이 필요합니다.** 컴퓨터가 절전 모드에 들어가지 않도록 하고 인터넷 연결, Docker/PostgreSQL, Chrome, `pnpm local:start`를 유지하세요. 대시보드 탭은 닫아도 됩니다. 다음에는 `pnpm local:start`로 다시 시작하며, 데이터베이스를 새로 만들지 않습니다. 컴퓨터가 오프라인이면 수집도 중단됩니다. +**지속적인 모니터링에는 실행 중인 환경이 필요합니다.** 컴퓨터가 절전 모드에 들어가지 않도록 하고 인터넷 연결, Chrome, `pnpm local:start`를 유지하세요. 대시보드 탭은 닫아도 됩니다. 다음에는 `pnpm local:start`로 다시 시작하며, 데이터베이스를 새로 만들지 않습니다. 컴퓨터가 오프라인이면 수집도 중단됩니다. [상세 설치 안내 — 중국어](docs/beat-geo/getting-started.md) · [기존 PostgreSQL 설정 — 중국어](docs/self-hosting.md) @@ -79,7 +79,8 @@ BeatGEO는 제품 데이터와 일정을 관리합니다. **BeatBrowser**는 로 | 질문 및 개선 | 영어·중국어 질문 생성, 업종별 템플릿, 개선 기준값 저장, 같은 질문으로 재측정. | | 일정 및 저장 | PostgreSQL + pg-boss. 실제 검증에서 다음 작업이 예약됨을 확인했으며 장기 무인 운영은 미검증입니다. SQLite는 지원하지 않습니다. | | BeatAPI 리서치 | 검색·읽기·모델 연동 구현. 실제 키를 사용한 유료 호출 검증은 아직 완료되지 않았습니다. | -| 사이트 기술 진단 및 SEO 데이터 | 계획 중이거나 미연동입니다. 키워드 검색량, 백링크 색인, 실제 AI 질문량을 제공한다고 주장하지 않습니다. | +| 사이트 확인 및 콘텐츠 초안 | 제한된 HTML/robots/sitemap 확인, 무료 개요, 편집과 버전별 승인. 공개 네트워크 및 유료 AI 검증 범위는 보고서를 참고하세요. | +| SEO 데이터 | 미연동입니다. 키워드 검색량, 백링크 색인, 실제 AI 질문량을 제공하지 않습니다. | README는 영어, 중국어, 일본어, 독일어, 한국어로 제공합니다. **제품 UI는 EN / 中文으로 영어와 중국어를 전환할 수 있으며 선택은 브라우저에 저장됩니다.** 질문, 브랜드명, 수집한 답변은 원래 언어를 유지합니다. README 번역이 UI의 5개 언어 지원을 뜻하지는 않습니다. diff --git a/README.md b/README.md index b96f22db..84db1d5a 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Start with your website and business. Review suggested monitoring questions, col ## Quick start -Prepare **Git, Node.js 24, pnpm 12.4.1, Docker with Compose, and Chrome**. Start Docker before setup. +Prepare **Git, Node.js 24, pnpm 12.4.1, and Chrome**. PostgreSQL is bundled and managed by BeatGEO; no Docker or separate database installation is required. ```sh git clone https://github.com/BeatAPI/BeatGEO.git @@ -41,7 +41,7 @@ Open **http://localhost:3140/setup** and register your local administrator: The installer prepares a dedicated local PostgreSQL database and the included extension. No second repository clone, BeatBrowser account or global CLI installation is needed. Existing BeatBrowser connections can be reused; enable only one extension copy per profile. -**To keep monitoring:** leave the computer awake and online, with Docker/PostgreSQL, Chrome and `pnpm local:start` running. You can close the dashboard tab. Restart with `pnpm local:start`; do not recreate the database. This local edition does not provide monitoring while your computer is offline. +**To keep monitoring:** leave the computer awake and online, with Chrome and `pnpm local:start` running. You can close the dashboard tab. Restart with `pnpm local:start`; do not recreate the database. This local edition does not provide monitoring while your computer is offline. [Detailed onboarding — Chinese](docs/beat-geo/getting-started.md) · [Manual PostgreSQL setup — Chinese](docs/self-hosting.md) @@ -79,7 +79,8 @@ Open [the interactive blueprint](docs/product/flow.html) locally to explore its | Questions and optimization | English/Chinese question generation, industry templates, saved action baselines and comparable retests. | | Scheduling and storage | PostgreSQL + pg-boss. The next cycle was queued in live validation; long unattended operation has not been established. SQLite is not supported. | | BeatAPI research | Search/read/model integration is implemented; acceptance with a real key and billed calls remains pending. | -| Technical site audit and SEO datasets | Planned or not connected. No claimed keyword volumes, backlink index or real AI prompt-volume dataset. | +| Website checks and drafts | Bounded HTML/robots/sitemap checks, free outlines, editable drafts and revision approval. Public-network and paid AI acceptance have stated limits. | +| SEO datasets | Not connected. No claimed keyword volumes, backlink index or real AI prompt-volume dataset. | README translations: English, Chinese, Japanese, German and Korean. **Switch the dashboard between English and Chinese using EN / 中文. Your preference is saved in this browser.** Questions, brand names and collected answers retain their original language. The translated READMEs do not imply five-language UI support. diff --git a/README.zh-CN.md b/README.zh-CN.md index f63e1e19..7b73d414 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -23,7 +23,7 @@ ## 快速开始 -准备 **Git、Node.js 24、pnpm 12.4.1、Docker(含 Compose)和 Chrome**,先启动 Docker。 +准备 **Git、Node.js 24、pnpm 12.4.1 和 Chrome**。BeatGEO 自带并管理独立 PostgreSQL,无需安装 Docker 或数据库。 ```sh git clone https://github.com/BeatAPI/BeatGEO.git @@ -41,7 +41,7 @@ pnpm local:start 安装脚本会准备独立 PostgreSQL 和内置扩展,不必再克隆 BeatBrowser、注册另一套账号或安装全局 CLI。已有 BeatBrowser 可以复用,同一 Chrome 配置只启用一份扩展。 -**持续监测需要持续运行:** 电脑保持唤醒和联网,Docker/PostgreSQL、Chrome、`pnpm local:start` 保持运行。可以关闭工作台网页;下次执行 `pnpm local:start` 即可,不要重建数据库。本地版不会在电脑离线时继续采集。 +**持续监测需要持续运行:** 电脑保持唤醒和联网,Chrome、`pnpm local:start` 保持运行。可以关闭工作台网页;下次执行 `pnpm local:start` 即可,不要重建数据库。本地版不会在电脑离线时继续采集。 [详细使用引导](docs/beat-geo/getting-started.md) · [已有 PostgreSQL 的手动配置](docs/self-hosting.md) @@ -79,7 +79,8 @@ BeatGEO 管理产品数据和调度,**BeatBrowser** 执行已登录浏览器 | 题库与优化 | 中英文问题生成、行业模板、优化行动基线与同组复测。 | | 调度与存储 | PostgreSQL + pg-boss;实测已排入下一轮任务,长期无人值守仍待验收。不支持 SQLite。 | | BeatAPI 研究 | 搜索、阅读、模型分析已接入代码,真实 Key 的付费调用仍待验收。 | -| 网站技术审计与 SEO 数据 | 待开发或接入,不提供虚构的关键词量、外链库或真实 AI 提问量。 | +| 网站检查与内容草稿 | 有限 HTML/robots/sitemap 检查、免费大纲、可编辑草稿及版本审核;公网检查与真实付费生成的验收边界见报告。 | +| SEO 数据 | 尚未接入,不提供虚构的关键词量、外链库或真实 AI 提问量。 | README 提供英文、中文、日文、德文和韩文。**产品界面通过 EN / 中文切换,选择会保存在当前浏览器。** 问题、品牌名和采集原文保留原语言。文档五语言不代表界面五语言。 diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 4245d4ff..bd815922 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -4,7 +4,7 @@ BeatGEO is maintained by BeatAPI and incorporates the MIT-licensed Elmo codebase Inherited statistics include share-of-voice and citation-stability calculations. The local metrics module reuses the same answer-level share-of-voice ratio. -[BeatBrowser](https://github.com/BeatAPI/BeatBrowser) is included as a pinned MIT dependency (`e39d1ee488fd5d069afa385b1cbcc326c8850018`); its original license and notices remain in the package. Its Chrome extension is copied locally during setup, with no separate product account or database. +[BeatBrowser](https://github.com/BeatAPI/BeatBrowser) is included as a pinned MIT dependency (`9aefd38bbd4e3fc7762751bcff57599fe8405588`); its original license and notices remain in the package. Its Chrome extension is copied locally during setup, with no separate product account or database. Research references, not bundled complete products: @@ -14,3 +14,5 @@ Research references, not bundled complete products: - [Ansvisor](https://github.com/ansvisor/ansvisor): opportunity-to-action workflow. Geist and Lucide notices remain in `apps/browser-lab/public/assets/`. Dependency notices remain with packages. Historical changelogs and source documentation retain original attribution. Compatibility identifiers for existing ciphertext are not product branding. + +The default local installer uses [embedded-postgres](https://github.com/leinelissen/embedded-postgres), pinned to `17.10.0-beta.17` (MIT wrapper; platform packages declare Apache-2.0). PostgreSQL and bundled native libraries retain their own notices in the dependency packages. The fixed-version installation scripts restore native library symlinks inside those packages; BeatGEO does not create operating-system users or install a system-wide database service. diff --git a/apps/web/package.json b/apps/web/package.json index 1dee22f2..46fec075 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -22,6 +22,7 @@ "@base-ui/react": "^1.8.0", "@fontsource/geist-mono": "^5.3.0", "@fontsource/geist-sans": "^5.3.0", + "@fontsource/inter": "5.2.8", "@fontsource/titan-one": "^5.3.0", "@modelcontextprotocol/sdk": "^1.32.0", "@scalar/api-reference-react": "^0.9.77", diff --git a/apps/web/src/components/answer-insight-nav.tsx b/apps/web/src/components/answer-insight-nav.tsx new file mode 100644 index 00000000..fadf0975 --- /dev/null +++ b/apps/web/src/components/answer-insight-nav.tsx @@ -0,0 +1,40 @@ +import { Link, useLocation, useRouter } from "@tanstack/react-router"; +import { brandLinkParams } from "@workspace/lib/app-urls"; +import { validateBrandFilterSearch } from "@/hooks/use-list-filters"; +import { useT } from "@/lib/i18n"; +import { INSIGHT_PAGES } from "@/lib/insight-navigation"; +import { activeNavHref } from "@/lib/nav-active"; +import type { ShellScope } from "@/lib/shell-scope"; + +export function AnswerInsightNav({ section, organization, brand }: ShellScope) { + const t = useT(); + const router = useRouter(); + const { pathname } = useLocation(); + if (section !== "brand" || !organization || !brand?.onboarded) return null; + const params = brandLinkParams(organization, brand); + const pages = INSIGHT_PAGES.map((page) => ({ + ...page, + href: router.buildLocation({ to: page.to, params }).pathname, + })); + const active = activeNavHref(pages, pathname); + if (!active) return null; + return ( + + ); +} diff --git a/apps/web/src/components/app-shell.tsx b/apps/web/src/components/app-shell.tsx index 412180c2..1a6fc3a0 100644 --- a/apps/web/src/components/app-shell.tsx +++ b/apps/web/src/components/app-shell.tsx @@ -1,5 +1,5 @@ import { SidebarInset, SidebarProvider } from "@workspace/ui/components/sidebar"; -import type { ReactNode } from "react"; +import type { CSSProperties, ReactNode } from "react"; export function AppShell({ sidebar, @@ -11,12 +11,10 @@ export function AppShell({ children: ReactNode; }) { return ( - + {sidebar} - {/* `overflow-clip`, not `overflow-hidden`: both clip to the rounded corners, - but `hidden` makes this a scroll container, which stops descendants from - sticking to the viewport (the site header included). */} - + {/* Clip horizontal overflow without changing the sticky header's scroll parent. */} + {header} {children} @@ -28,7 +26,7 @@ export function PageContent({ children }: { children: ReactNode }) { return (
-
{children}
+
{children}
); diff --git a/apps/web/src/components/app-sidebar.tsx b/apps/web/src/components/app-sidebar.tsx index dfcb0ccc..56a176ce 100644 --- a/apps/web/src/components/app-sidebar.tsx +++ b/apps/web/src/components/app-sidebar.tsx @@ -8,13 +8,9 @@ import { IconCreditCard, IconDashboard, IconKey, - IconLink, IconListDetails, - IconMessages, IconPlugConnected, IconSchool, - IconSitemap, - IconSpeakerphone, IconTarget, IconUsers, } from "@tabler/icons-react"; @@ -41,6 +37,7 @@ import { NavUser } from "@/components/nav-user"; import { useDeploymentFeatures } from "@/hooks/use-deployment-features"; import { useLooseRouteContext, useViewer } from "@/hooks/use-route-context"; import { adminNavItems } from "@/lib/admin-nav"; +import { INSIGHT_PAGES } from "@/lib/insight-navigation"; import type { OrganizationSummary } from "@/lib/organizations/types"; import type { ShellScope } from "@/lib/shell-scope"; @@ -91,15 +88,12 @@ function brandGroups(organization: OrganizationSummary, brand: BrandWithPrompts) if (brand.onboarded) { dashboard.push( - { title: "Visibility", link: { to: "/app/org/$org/brand/$brand/visibility", params }, icon: IconChartBar }, { - title: "Share of Voice", - link: { to: "/app/org/$org/brand/$brand/share-of-voice", params }, - icon: IconSpeakerphone, + title: "Answer Engine Insights", + link: { to: INSIGHT_PAGES[0].to, params }, + matchLinks: INSIGHT_PAGES.slice(1).map((page) => ({ to: page.to, params })), + icon: IconChartBar, }, - { title: "Responses", link: { to: "/app/org/$org/brand/$brand/responses", params }, icon: IconMessages }, - { title: "Query Fan-Out", link: { to: "/app/org/$org/brand/$brand/query-fan-out", params }, icon: IconSitemap }, - { title: "Citations", link: { to: "/app/org/$org/brand/$brand/citations", params }, icon: IconLink }, { title: "Opportunities", link: { to: "/app/org/$org/brand/$brand/opportunities", params }, icon: IconTarget }, ); } @@ -165,7 +159,7 @@ export function AppSidebar({ section, organization, brand }: AppSidebarProps) { ); return ( - + diff --git a/apps/web/src/components/cached-prompt-chart.tsx b/apps/web/src/components/cached-prompt-chart.tsx index aaee08d0..0f80ef40 100644 --- a/apps/web/src/components/cached-prompt-chart.tsx +++ b/apps/web/src/components/cached-prompt-chart.tsx @@ -6,6 +6,7 @@ import { Fragment, memo, useCallback, useMemo } from "react"; import { useOptionalChartDataContext } from "@/contexts/chart-data-context"; import { useChartExport } from "@/hooks/use-chart-export"; import { visibilityBadgeProps } from "@/lib/chart-utils"; +import { useT } from "@/lib/i18n"; import type { LookbackPeriod } from "@/lib/lookback"; import { BaseChart } from "./base-chart"; import { ChartActionsFooter } from "./chart-footer"; @@ -107,6 +108,7 @@ export const CachedPromptChart = memo(function CachedPromptChart({ searchHighlight = "", hasEverBeenEvaluated = false, }: CachedPromptChartProps) { + const t = useT(); const chartContext = useOptionalChartDataContext(); const chartData = useMemo(() => { @@ -148,39 +150,19 @@ export const CachedPromptChart = memo(function CachedPromptChart({ const isFirstEval = !hasEverBeenEvaluated; return ( - - - - - - {/* h-[300px] instead of h-[250px] to compensate for the missing footer section, - keeping overall card height consistent with data-filled cards for virtualization */} - -
-
- {isFirstEval ? ( - <> -
-
-
-
-
-

Evaluating for the first time

-

Results will appear here shortly.

- - ) : ( - <> - -

No data in selected time range

-

- Try selecting a longer time period to see historical data. -

- - )} -
-
- - +
+ +

+ {t(isFirstEval ? "No completed answers yet" : "No data in selected time range")} +

+

+ {t( + isFirstEval + ? "Check collection status in Overview or Actions & retests." + : "Try selecting a longer time period to see historical data.", + )} +

+
); } diff --git a/apps/web/src/components/chart-export-preview.tsx b/apps/web/src/components/chart-export-preview.tsx index 92268166..12e5f113 100644 --- a/apps/web/src/components/chart-export-preview.tsx +++ b/apps/web/src/components/chart-export-preview.tsx @@ -55,14 +55,18 @@ export function ChartExportPreview({ return (
{/* Title bar */}
-

+

{promptName}

{visibility !== null && ( @@ -80,7 +84,7 @@ export function ChartExportPreview({ {/* Chart card */}
)} {isWhitelabel ? ( - + {name} ) : ( - + {name} )}
- + {domain}
diff --git a/apps/web/src/components/collection-check.tsx b/apps/web/src/components/collection-check.tsx new file mode 100644 index 00000000..0f0f1392 --- /dev/null +++ b/apps/web/src/components/collection-check.tsx @@ -0,0 +1,74 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { useState } from "react"; +import { useT } from "@/lib/i18n"; +import { collectOneQuestionFn } from "@/server/monitoring-fn"; +export function CollectionCheck({ + brandId, + ready, + questions, +}: { + brandId: string; + ready: boolean; + questions: { id: string; value: string }[]; +}) { + const t = useT(); + const client = useQueryClient(); + const [selected, setSelected] = useState(""); + const promptId = questions.some((q) => q.id === selected) ? selected : questions[0]?.id; + const mutation = useMutation({ + mutationFn: () => collectOneQuestionFn({ data: { brandId, promptId: promptId! } }), + onSuccess: () => client.invalidateQueries({ queryKey: ["monitoring", brandId] }), + }); + return ( +
+

+ + {t(ready ? "Worker ready" : "Worker not ready")} +

+ {!ready && ( +

+ {t("Collection is not ready. Start pnpm local:start and refresh; queued questions are preserved.")} +

+ )} +
+ + +
+

+ {t( + "Runs this question on configured targets. Existing scheduled jobs remain queued. API targets may incur charges; duplicate clicks are limited for 5 minutes.", + )} +

+ {mutation.data && ( +

+ {t( + mutation.data.queued + ? "Question queued. Follow the collection history below." + : "This question is already queued or was requested recently.", + )} +

+ )} + {mutation.error &&

{t(mutation.error.message)}

} +
+ ); +} diff --git a/apps/web/src/components/content-draft.tsx b/apps/web/src/components/content-draft.tsx new file mode 100644 index 00000000..29c4edaf --- /dev/null +++ b/apps/web/src/components/content-draft.tsx @@ -0,0 +1,136 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { useState } from "react"; +import { useLocale, useT } from "@/lib/i18n"; +import { createContentDraftFn, reviewContentDraftFn, saveContentDraftFn } from "@/server/content-draft-fn"; + +type DraftAction = { + id: string; + contentDraft: string | null; + draftRevision: number; + reviewedRevision: number | null; + status: string; +}; +const button = "rounded-md border px-3 py-1.5 text-sm hover:bg-muted disabled:opacity-50"; +function isReviewed(action: DraftAction) { + return Boolean(action.contentDraft) && action.draftRevision === action.reviewedRevision; +} +function draftText(edit: { content: string } | null, action: DraftAction) { + return edit?.content ?? action.contentDraft ?? ""; +} +export function ContentDraft({ brandId, action }: { brandId: string; action: DraftAction }) { + const t = useT(); + const { locale } = useLocale(); + const client = useQueryClient(); + const [edit, setEdit] = useState<{ content: string; revision: number } | null>(null); + const [notice, setNotice] = useState(""); + const content = draftText(edit, action); + const dirty = edit !== null && edit.content !== action.contentDraft; + const reviewed = isReviewed(action); + const mutation = useMutation({ + mutationFn: async (operation: "outline" | "ai" | "save" | "review") => { + setNotice(""); + const data = { brandId, id: action.id, revision: edit?.revision ?? action.draftRevision }; + if (operation === "save") return saveContentDraftFn({ data: { ...data, content } }); + if (operation === "review") return reviewContentDraftFn({ data }); + return createContentDraftFn({ data: { ...data, language: locale, useAi: operation === "ai" } }); + }, + onSuccess: async () => { + setEdit(null); + await client.invalidateQueries({ queryKey: ["monitoring", brandId] }); + }, + }); + const readOnly = action.status !== "planned"; + return ( +
+ + {t("Content draft")} · {t(reviewed ? "Reviewed" : "Needs review")} + +
+

+ {t( + "Drafts stay here until you publish them yourself. Review facts and references before marking an improvement implemented.", + )} +

+ {!readOnly && ( +
+ + +
+ )} + {content && ( + <> +