diff --git a/.env.example b/.env.example index c1022f911a..a1beb90d55 100644 --- a/.env.example +++ b/.env.example @@ -408,6 +408,11 @@ S3_PUBLIC_URL= # PORTAL_DESIGN_ENGINE=auto +# A website reader that renders a page in a headless browser (docker/browser). +# Optional: without it, websites are read from their HTML and CSS. +# QUACKBACK_BROWSER_URL=http://localhost:3200 +# QUACKBACK_BROWSER_TOKEN= + # Cloud only; leave unset when self-hosting. # QUACKBACK_CONTROL_PLANE_URL=https://app.quackback.io # INTEGRATION_OAUTH_GATEWAY_URL=https://app.quackback.io diff --git a/.env.prod.example b/.env.prod.example index beaa727802..8da2463c7d 100644 --- a/.env.prod.example +++ b/.env.prod.example @@ -105,3 +105,9 @@ EMAIL_FROM=Quackback # Enables summaries, duplicate detection, feedback extraction, etc. # OPENAI_API_KEY= # OPENAI_BASE_URL= + +# --- Website reader (optional) ------------------------------------------ +# Renders a website in a headless browser so Quackback AI reads its exact +# type, colours and buttons. Start it with `--profile browser`. +# QUACKBACK_BROWSER_URL=http://browser:3200 +# QUACKBACK_BROWSER_TOKEN= # openssl rand -hex 24 diff --git a/apps/web/package.json b/apps/web/package.json index 4baa9aedd2..afc3df31e4 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -33,6 +33,7 @@ "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", + "@duckduckgo/autoconsent": "17.2.0", "@floating-ui/dom": "^1.7.6", "@fontsource-variable/inter": "^5.3.0", "@fontsource/barlow-condensed": "^5.3.0", diff --git a/apps/web/src/lib/server/config.ts b/apps/web/src/lib/server/config.ts index 5409fbf482..299c28185d 100644 --- a/apps/web/src/lib/server/config.ts +++ b/apps/web/src/lib/server/config.ts @@ -124,6 +124,10 @@ const configSchema = z // Quackback AI may write a portal design's CSS as well as its fields. Off // until it is shown to design better with it. portalDesignAiCss: envBoolean, + // A website reader beside Quackback that renders a page in a headless + // browser. Optional: without it, websites are read from their HTML and CSS. + browserUrl: z.string().url().optional(), + browserToken: z.string().min(16).optional(), // Database // @@ -379,6 +383,8 @@ function buildConfigFromEnv(): unknown { port: env('PORT'), portalDesignEngine: env('PORTAL_DESIGN_ENGINE'), portalDesignAiCss: env('PORTAL_DESIGN_AI_CSS'), + browserUrl: env('QUACKBACK_BROWSER_URL'), + browserToken: env('QUACKBACK_BROWSER_TOKEN'), // Database databaseUrl: env('DATABASE_URL'), @@ -578,6 +584,12 @@ export const config = { get portalDesignAiCss() { return loadConfig().portalDesignAiCss }, + get browserUrl() { + return loadConfig().browserUrl + }, + get browserToken() { + return loadConfig().browserToken + }, get controlDatabaseUrl() { return loadConfig().controlDatabaseUrl }, diff --git a/apps/web/src/lib/server/domains/design/ai/__tests__/page-tree.test.ts b/apps/web/src/lib/server/domains/design/ai/__tests__/page-tree.test.ts index 0601473893..292c7c7164 100644 --- a/apps/web/src/lib/server/domains/design/ai/__tests__/page-tree.test.ts +++ b/apps/web/src/lib/server/domains/design/ai/__tests__/page-tree.test.ts @@ -37,4 +37,17 @@ describe('pageTree', () => { ['p', true], ]) }) + + it('never leaves a script behind when its tags are split by another', () => { + const nodes = pageTree( + `
ipt>alert(1)Go
` + ) + expect(nodes.map((n) => n.tag)).not.toContain('script') + expect(nodes.find((n) => n.tag === 'a')?.text).toBe('Go') + }) + + it('reads a script left open as running to the end of the page', () => { + const nodes = pageTree(`
Real