Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/ai-platform-reports.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@workspace/web": patch
"@workspace/lib": patch
---

Add separate Bing and Google AI report imports with comparable history and guide users through their first monitoring run.
8 changes: 8 additions & 0 deletions .changeset/beatapi-monitoring-platforms.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@workspace/web": patch
"@workspace/lib": patch
"@workspace/config": patch
"@workspace/worker": patch
---

Track platforms only through BeatBrowser or BeatAPI with a default model per platform, remove product analytics, and reorder the sidebar into SEO, GEO and Optimize.
1 change: 0 additions & 1 deletion .github/FUNDING.yml

This file was deleted.

1 change: 0 additions & 1 deletion .github/contributors.txt

This file was deleted.

2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ Before handing work back or opening a PR, run `pnpm lint` and get it passing; CI

## Environment

`.env` must exist at **both** the repo root and `apps/web/.env` (Vite reads its project root; the worker reads `apps/web/.env` via `--env-file`). Minimum for the local workspace: `DATABASE_URL`, `BETTER_AUTH_SECRET`, `BEAT_GEO_ENCRYPTION_KEY` (`openssl rand -base64 32`), `APP_URL`, `DISABLE_TELEMETRY=1`, and `SCRAPE_TARGETS` plus the keys of the providers it names. Local workspaces establish a private owner/session automatically and expose no registration, login, password, team or organization-switching UI. Local setup remains accessible without a research key; industry starter questions and browser monitoring need no research provider. AI-assisted onboarding and Opportunities require a research provider: `BEATAPI_API_KEY` plus `BEATAPI_RESEARCH_MODEL`, or one direct LLM key (`OPENAI_API_KEY`, `OPENROUTER_API_KEY`, `ANTHROPIC_API_KEY`, or `MISTRAL_API_KEY`) for onboarding and the Opportunities report. For UI-only work, `SCRAPE_TARGETS=stub:stub` and `ONBOARDING_LLM_TARGET=stub:stub` need no keys at all.
`.env` must exist at **both** the repo root and `apps/web/.env` (Vite reads its project root; the worker reads `apps/web/.env` via `--env-file`). Minimum for the local workspace: `DATABASE_URL`, `BETTER_AUTH_SECRET`, `BEAT_GEO_ENCRYPTION_KEY` (`openssl rand -base64 32`), `APP_URL` and `SCRAPE_TARGETS` plus the keys of the providers it names. Local workspaces establish a private owner/session automatically and expose no registration, login, password, team or organization-switching UI. Local setup remains accessible without a research key; industry starter questions and browser monitoring need no research provider. AI-assisted onboarding and Opportunities require `BEATAPI_API_KEY` plus `BEATAPI_RESEARCH_MODEL`. Monitoring reaches platforms only through BeatBrowser (`beat-browser`) or BeatAPI (`beatapi`); see `docs/providers.md`. For UI-only work, `SCRAPE_TARGETS=stub:stub` and `ONBOARDING_LLM_TARGET=stub:stub` need no keys at all.

## Git workflow

Expand Down
3 changes: 0 additions & 3 deletions CLA.md

This file was deleted.

1 change: 0 additions & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@
"lucide-react": "^1.51.0",
"pg": "^8.23.1",
"pg-boss": "^12.36.0",
"posthog-js": "^1.435.8",
"react": "^19.3.0",
"react-dom": "^19.3.0",
"react-icons": "^5.7.0",
Expand Down
76 changes: 7 additions & 69 deletions apps/web/src/components/__tests__/response-markdown.test.tsx
Original file line number Diff line number Diff line change
@@ -1,92 +1,30 @@
/**
* Every provider stores its answers in a different shape, and each one reaches
* the prompt detail page through `extractTextContent` before it is rendered.
* A provider whose extraction hands back flattened text or glued-together
* blocks looks identical to a broken renderer from the page's side, so the two
* halves are checked together here rather than in isolation.
*
* The shapes mirror the fixtures in each provider's own tests in
* `packages/lib/src/providers/registry`.
* Stored answers reach the prompt detail page through `extractTextContent`
* before they are rendered, so extraction and rendering are checked together:
* flattened text looks identical to a broken renderer from the page's side.
*/
import { extractTextContent } from "@workspace/lib/text-extraction";
import { renderToStaticMarkup } from "react-dom/server";
import { describe, expect, it } from "vitest";
import { ResponseMarkdown } from "@/components/response-markdown";

const PROSE = "Here are the **top** trackers:";
const TABLE = "| Tool | Price |\n| --- | --- |\n| Profound | $99 |";
const TABLE = "| Tool | Price |\n| --- | --- |\n| Example Tracker | $99 |";
const ANSWER = `${PROSE}\n\n${TABLE}`;

const render = (rawOutput: unknown, provider: string) =>
renderToStaticMarkup(<ResponseMarkdown>{extractTextContent(rawOutput, provider)}</ResponseMarkdown>);

/** One answer, one shape per provider surface. */
const SURFACES: [name: string, provider: string, rawOutput: unknown][] = [
["openai-api", "openai-api", { output: [{ type: "message", content: [{ type: "output_text", text: ANSWER }] }] }],
["anthropic-api", "anthropic-api", { content: [{ type: "text", text: ANSWER }] }],
["mistral-api", "mistral-api", { outputs: [{ content: [{ type: "text", text: ANSWER }] }] }],
["openrouter", "openrouter", { choices: [{ message: { content: ANSWER } }] }],
["olostep", "olostep", { json_content: JSON.stringify({ result: { markdown_content: ANSWER } }) }],
["brightdata chatbot", "brightdata", [{ answer_text_markdown: ANSWER, answer_text: "flattened" }]],
["brightdata AI Overview", "brightdata", [{ ai_overview: { markdown: ANSWER } }]],
["oxylabs ChatGPT", "oxylabs", { results: [{ content: { markdown_text: ANSWER } }] }],
["oxylabs Perplexity", "oxylabs", { results: [{ content: { answer_results_md: ANSWER } }] }],
["cloro chatbot", "cloro", { markdown: ANSWER, text: "flattened" }],
["cloro AI Overview", "cloro", { aioverview: { markdown: ANSWER, text: "flattened" } }],
["dataforseo scraper", "dataforseo", { tasks: [{ result: [{ markdown: ANSWER, sources: [] }] }] }],
[
"dataforseo LLM Responses",
"dataforseo",
{ tasks: [{ result: [{ items: [{ type: "message", sections: [{ type: "text", text: ANSWER }] }] }] }] },
],
[
"dataforseo AI Overview",
"dataforseo",
{ tasks: [{ result: [{ items: [{ type: "ai_overview", markdown: ANSWER }] }] }] },
],
// Rows written before the provider column existed dispatch on the model.
[
"legacy row without a provider",
"chatgpt",
{ output: [{ type: "message", content: [{ type: "output_text", text: ANSWER }] }] },
],
["BeatBrowser observation", "beat-browser", { status: "completed", answer: ANSWER, citations: [] }],
["BeatAPI answer", "beatapi", { choices: [{ finish_reason: "stop", message: { content: ANSWER } }] }],
];

describe("answer rendering across providers", () => {
it.each(SURFACES)("renders %s answers as markdown", (_name, provider, rawOutput) => {
it.each(SURFACES)("renders %s as markdown", (_name, provider, rawOutput) => {
const html = render(rawOutput, provider);
expect(html).toContain("<table>");
expect(html).toContain("<strong>top</strong>");
expect(html).not.toContain("flattened");
});

/**
* These three surfaces arrive as a list of blocks rather than one string.
* Joined with a single newline they render as one run-on paragraph, which
* is what the extraction's blank-line join exists to prevent.
*/
it.each([
[
"brightdata AI Overview",
"brightdata",
[{ ai_overview: { texts: [{ snippet: "First." }, { snippet: "Second." }] } }],
],
[
"dataforseo scraper items",
"dataforseo",
{ tasks: [{ result: [{ sources: [], items: [{ markdown: "First." }, { markdown: "Second." }] }] }] },
],
[
"oxylabs AI Overview",
"oxylabs",
{
results: [
{ content: { ai_overviews: [{ answer_text: [{ fragments: [{ text: "First." }, { text: "Second." }] }] }] } },
],
},
],
])("keeps %s blocks as separate paragraphs", (_name, provider, rawOutput) => {
expect(render(rawOutput, provider).match(/<p>/g)).toHaveLength(2);
});

it("does not load relative or protocol-relative images", () => {
Expand Down
Loading
Loading