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
9 changes: 7 additions & 2 deletions .github/workflows/components-build-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ on:
- 'components/ambient-api-server/**'
- 'components/ambient-control-plane/**'
- 'components/ambient-mcp/**'
- 'components/ambient-ui/**'
pull_request:
branches: [main, alpha]
paths:
Expand All @@ -27,10 +28,11 @@ on:
- 'components/ambient-api-server/**'
- 'components/ambient-control-plane/**'
- 'components/ambient-mcp/**'
- 'components/ambient-ui/**'
workflow_dispatch:
inputs:
components:
description: 'Components to build (comma-separated: frontend,backend,operator,ambient-runner,state-sync,public-api,ambient-api-server,ambient-control-plane,ambient-mcp) - leave empty for all'
description: 'Components to build (comma-separated: frontend,backend,operator,ambient-runner,state-sync,public-api,ambient-api-server,ambient-control-plane,ambient-mcp,ambient-ui) - leave empty for all'
required: false
type: string
default: ''
Expand Down Expand Up @@ -60,7 +62,8 @@ jobs:
{"name":"public-api","context":"./components/public-api","image":"quay.io/ambient_code/vteam_public_api","dockerfile":"./components/public-api/Dockerfile"},
{"name":"ambient-api-server","context":"./components/ambient-api-server","image":"quay.io/ambient_code/vteam_api_server","dockerfile":"./components/ambient-api-server/Dockerfile"},
{"name":"ambient-control-plane","context":"./components","image":"quay.io/ambient_code/vteam_control_plane","dockerfile":"./components/ambient-control-plane/Dockerfile"},
{"name":"ambient-mcp","context":"./components/ambient-mcp","image":"quay.io/ambient_code/vteam_mcp","dockerfile":"./components/ambient-mcp/Dockerfile"}
{"name":"ambient-mcp","context":"./components/ambient-mcp","image":"quay.io/ambient_code/vteam_mcp","dockerfile":"./components/ambient-mcp/Dockerfile"},
{"name":"ambient-ui","context":"./components","image":"quay.io/ambient_code/vteam_ambient_ui","dockerfile":"./components/ambient-ui/Dockerfile"}
]'

SELECTED="${{ github.event.inputs.components }}"
Expand Down Expand Up @@ -384,6 +387,7 @@ jobs:
kustomize edit set image quay.io/ambient_code/vteam_public_api:latest=quay.io/ambient_code/vteam_public_api:${{ github.sha }}
kustomize edit set image quay.io/ambient_code/vteam_control_plane:latest=quay.io/ambient_code/vteam_control_plane:${{ github.sha }}
kustomize edit set image quay.io/ambient_code/vteam_mcp:latest=quay.io/ambient_code/vteam_mcp:${{ github.sha }}
kustomize edit set image quay.io/ambient_code/vteam_ambient_ui:latest=quay.io/ambient_code/vteam_ambient_ui:${{ github.sha }}

- name: Validate kustomization
working-directory: components/manifests/overlays/production
Expand Down Expand Up @@ -462,6 +466,7 @@ jobs:
kustomize edit set image quay.io/ambient_code/vteam_public_api:latest=quay.io/ambient_code/vteam_public_api:${{ github.sha }}
kustomize edit set image quay.io/ambient_code/vteam_control_plane:latest=quay.io/ambient_code/vteam_control_plane:${{ github.sha }}
kustomize edit set image quay.io/ambient_code/vteam_mcp:latest=quay.io/ambient_code/vteam_mcp:${{ github.sha }}
kustomize edit set image quay.io/ambient_code/vteam_ambient_ui:latest=quay.io/ambient_code/vteam_ambient_ui:${{ github.sha }}

- name: Validate kustomization
working-directory: components/manifests/overlays/production
Expand Down
10 changes: 6 additions & 4 deletions components/ambient-ui/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ WORKDIR /app

USER 0

# Copy SDK dependency first (it's a file: dependency in package.json)
COPY ambient-sdk/ts-sdk ./ambient-sdk/ts-sdk
# Copy and build SDK dependency. package.json references file:../ambient-sdk/ts-sdk
# which resolves to /ambient-sdk/ts-sdk from WORKDIR /app.
COPY ambient-sdk/ts-sdk /ambient-sdk/ts-sdk
RUN cd /ambient-sdk/ts-sdk && npm install --ignore-scripts && npm run build

# Copy ambient-ui package files
COPY ambient-ui/package.json ambient-ui/package-lock.json* ./
Expand All @@ -21,9 +23,9 @@ USER 0

WORKDIR /app

# Copy node_modules from deps stage
# Copy node_modules and SDK from deps stage
COPY --from=deps /app/node_modules ./node_modules
COPY --from=deps /app/ambient-sdk ./ambient-sdk
COPY --from=deps /ambient-sdk /ambient-sdk
COPY ambient-ui/ .

# Next.js collects completely anonymous telemetry data about general usage.
Expand Down
54 changes: 54 additions & 0 deletions components/ambient-ui/next.config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,47 @@
// eslint-disable-next-line @typescript-eslint/no-require-imports
const path = require('path')

const DEFAULT_PATTERNS = 'localhost:*,127.0.0.1:*'

/**
* Convert a preview-host glob pattern to CSP frame-src source(s).
*
* CSP requires a scheme for host:port patterns, so `localhost:*` becomes
* `http://localhost:* https://localhost:*`. Subdomain wildcards like
* `*.example.com` are valid CSP syntax and pass through unchanged.
*/
function toFrameSrcEntries(pattern) {
if (pattern.includes('://')) {
return [pattern]
}
// CSP only supports a wildcard as the leftmost label (e.g. *.example.com).
// Mid-domain wildcards like *.apps.rosa.*.openshiftapps.com are invalid CSP.
// Collapse to a valid prefix wildcard by keeping everything after the last *.
// e.g. *.apps.rosa.*.openshiftapps.com → *.openshiftapps.com
let cspPattern = pattern
const midWildcard = /\*\.[^*]+\*\./
if (midWildcard.test(pattern)) {
const lastWildIdx = pattern.lastIndexOf('*.')
cspPattern = '*.' + pattern.slice(lastWildIdx + 2)
}
return [`http://${cspPattern}`, `https://${cspPattern}`]
}

/**
* Build the CSP frame-src directive from NEXT_PUBLIC_PREVIEW_ALLOWED_HOSTS.
*/
function buildFrameSrc() {
const raw = process.env.NEXT_PUBLIC_PREVIEW_ALLOWED_HOSTS
const source = (raw && raw.trim()) || DEFAULT_PATTERNS
const patterns = source
.split(',')
.map((p) => p.trim())
.filter((p) => p.length > 0)

const entries = patterns.flatMap(toFrameSrcEntries)
return ["'self'", ...entries].join(' ')
}

/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'standalone',
Expand All @@ -9,6 +50,19 @@ const nextConfig = {
experimental: {
staticGenerationMinPagesPerWorker: 100,
},
async headers() {
return [
{
source: '/(.*)',
headers: [
{
key: 'Content-Security-Policy',
value: `frame-src ${buildFrameSrc()};`,
},
],
},
]
},
}

module.exports = nextConfig
2 changes: 1 addition & 1 deletion components/ambient-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"private": true,
"scripts": {
"dev": "next dev --webpack --port 3001",
"build": "next build",
"build": "next build --webpack",
"start": "next start",
"lint": "eslint",
"test": "vitest run",
Expand Down
86 changes: 86 additions & 0 deletions components/ambient-ui/public/preview-bridge.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/**
* Ambient UI Preview Bridge
*
* Include this script in pages rendered inside the Ambient UI preview iframe
* to enable cross-origin element capture and hover highlighting.
*
* Usage: <script src="/preview-bridge.js"></script>
*
* The bridge listens for postMessage requests from the parent frame and
* responds with element information at the requested coordinates.
*/
(function () {
'use strict';

var currentHighlight = null;

function getClassName(el) {
// SVG elements have SVGAnimatedString for className, not a plain string
return el.getAttribute('class') || null;
}

// Element capture: parent asks for the element at (x, y)
window.addEventListener('message', function (e) {
if (!e.data || e.data.type !== 'ambient-capture') return;
if (!e.origin || e.origin === 'null') return;

var x = e.data.x;
var y = e.data.y;
var el = document.elementFromPoint(x, y);

if (!el) {
e.source.postMessage({ type: 'ambient-captured', html: null, rect: null }, e.origin);
return;
}

var rect = el.getBoundingClientRect();
e.source.postMessage({
type: 'ambient-captured',
html: el.outerHTML.slice(0, 500),
tagName: el.tagName.toLowerCase(),
id: el.id || null,
className: getClassName(el),
textContent: (el.textContent || '').slice(0, 100),
rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height }
}, e.origin);
});

// Hover highlight: parent sends cursor position, bridge outlines the element
window.addEventListener('message', function (e) {
if (!e.data || e.data.type !== 'ambient-hover') return;
if (!e.origin || e.origin === 'null') return;

var el = document.elementFromPoint(e.data.x, e.data.y);

// Remove previous highlight
if (currentHighlight) {
currentHighlight.style.outline = currentHighlight._ambientSavedOutline || '';
delete currentHighlight._ambientSavedOutline;
currentHighlight = null;
}

if (el && el !== document.documentElement && el !== document.body) {
el._ambientSavedOutline = el.style.outline;
el.style.outline = '2px solid #4394e5';
currentHighlight = el;
}

if (el) {
var rect = el.getBoundingClientRect();
e.source.postMessage({
type: 'ambient-hovered',
rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height }
}, e.origin);
}
});

// Clear hover: remove any active highlight
window.addEventListener('message', function (e) {
if (!e.data || e.data.type !== 'ambient-hover-clear') return;
if (currentHighlight) {
currentHighlight.style.outline = currentHighlight._ambientSavedOutline || '';
delete currentHighlight._ambientSavedOutline;
currentHighlight = null;
}
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
})();
Loading
Loading