Skip to content

Commit 3a047f2

Browse files
committed
2 parents 4b1a0cf + 2796cb4 commit 3a047f2

48 files changed

Lines changed: 3796 additions & 87 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/components-build-deploy.yml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ on:
1414
- 'components/ambient-api-server/**'
1515
- 'components/ambient-control-plane/**'
1616
- 'components/ambient-mcp/**'
17+
- 'components/ambient-ui/**'
1718
pull_request:
1819
branches: [main, alpha]
1920
paths:
@@ -27,10 +28,11 @@ on:
2728
- 'components/ambient-api-server/**'
2829
- 'components/ambient-control-plane/**'
2930
- 'components/ambient-mcp/**'
31+
- 'components/ambient-ui/**'
3032
workflow_dispatch:
3133
inputs:
3234
components:
33-
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'
35+
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'
3436
required: false
3537
type: string
3638
default: ''
@@ -60,7 +62,8 @@ jobs:
6062
{"name":"public-api","context":"./components/public-api","image":"quay.io/ambient_code/vteam_public_api","dockerfile":"./components/public-api/Dockerfile"},
6163
{"name":"ambient-api-server","context":"./components/ambient-api-server","image":"quay.io/ambient_code/vteam_api_server","dockerfile":"./components/ambient-api-server/Dockerfile"},
6264
{"name":"ambient-control-plane","context":"./components","image":"quay.io/ambient_code/vteam_control_plane","dockerfile":"./components/ambient-control-plane/Dockerfile"},
63-
{"name":"ambient-mcp","context":"./components/ambient-mcp","image":"quay.io/ambient_code/vteam_mcp","dockerfile":"./components/ambient-mcp/Dockerfile"}
65+
{"name":"ambient-mcp","context":"./components/ambient-mcp","image":"quay.io/ambient_code/vteam_mcp","dockerfile":"./components/ambient-mcp/Dockerfile"},
66+
{"name":"ambient-ui","context":"./components","image":"quay.io/ambient_code/vteam_ambient_ui","dockerfile":"./components/ambient-ui/Dockerfile"}
6467
]'
6568
6669
SELECTED="${{ github.event.inputs.components }}"
@@ -384,6 +387,7 @@ jobs:
384387
kustomize edit set image quay.io/ambient_code/vteam_public_api:latest=quay.io/ambient_code/vteam_public_api:${{ github.sha }}
385388
kustomize edit set image quay.io/ambient_code/vteam_control_plane:latest=quay.io/ambient_code/vteam_control_plane:${{ github.sha }}
386389
kustomize edit set image quay.io/ambient_code/vteam_mcp:latest=quay.io/ambient_code/vteam_mcp:${{ github.sha }}
390+
kustomize edit set image quay.io/ambient_code/vteam_ambient_ui:latest=quay.io/ambient_code/vteam_ambient_ui:${{ github.sha }}
387391
388392
- name: Validate kustomization
389393
working-directory: components/manifests/overlays/production
@@ -462,6 +466,7 @@ jobs:
462466
kustomize edit set image quay.io/ambient_code/vteam_public_api:latest=quay.io/ambient_code/vteam_public_api:${{ github.sha }}
463467
kustomize edit set image quay.io/ambient_code/vteam_control_plane:latest=quay.io/ambient_code/vteam_control_plane:${{ github.sha }}
464468
kustomize edit set image quay.io/ambient_code/vteam_mcp:latest=quay.io/ambient_code/vteam_mcp:${{ github.sha }}
469+
kustomize edit set image quay.io/ambient_code/vteam_ambient_ui:latest=quay.io/ambient_code/vteam_ambient_ui:${{ github.sha }}
465470
466471
- name: Validate kustomization
467472
working-directory: components/manifests/overlays/production

components/ambient-ui/Dockerfile

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ WORKDIR /app
55

66
USER 0
77

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

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

2224
WORKDIR /app
2325

24-
# Copy node_modules from deps stage
26+
# Copy node_modules and SDK from deps stage
2527
COPY --from=deps /app/node_modules ./node_modules
26-
COPY --from=deps /app/ambient-sdk ./ambient-sdk
28+
COPY --from=deps /ambient-sdk /ambient-sdk
2729
COPY ambient-ui/ .
2830

2931
# Next.js collects completely anonymous telemetry data about general usage.
@@ -34,8 +36,10 @@ RUN npm run build
3436

3537
# Prepare standalone output with OpenShift-compatible permissions in the builder
3638
# (the hardened runner image is distroless — no shell or chmod available)
39+
# outputFileTracingRoot is ../.. which resolves to / in Docker (WORKDIR /app),
40+
# so standalone nests files under app/ (i.e. .next/standalone/app/server.js).
3741
RUN mkdir -p /app-output/public /app-output/.next/static && \
38-
cp -r .next/standalone/. /app-output/ && \
42+
cp -r .next/standalone/app/. /app-output/ && \
3943
cp -r .next/static/. /app-output/.next/static/ && \
4044
cp -r public/. /app-output/public/ && \
4145
chmod -R g=u /app-output && \

components/ambient-ui/next.config.js

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,47 @@
11
// eslint-disable-next-line @typescript-eslint/no-require-imports
22
const path = require('path')
33

4+
const DEFAULT_PATTERNS = 'localhost:*,127.0.0.1:*'
5+
6+
/**
7+
* Convert a preview-host glob pattern to CSP frame-src source(s).
8+
*
9+
* CSP requires a scheme for host:port patterns, so `localhost:*` becomes
10+
* `http://localhost:* https://localhost:*`. Subdomain wildcards like
11+
* `*.example.com` are valid CSP syntax and pass through unchanged.
12+
*/
13+
function toFrameSrcEntries(pattern) {
14+
if (pattern.includes('://')) {
15+
return [pattern]
16+
}
17+
// CSP only supports a wildcard as the leftmost label (e.g. *.example.com).
18+
// Mid-domain wildcards like *.apps.rosa.*.openshiftapps.com are invalid CSP.
19+
// Collapse to a valid prefix wildcard by keeping everything after the last *.
20+
// e.g. *.apps.rosa.*.openshiftapps.com → *.openshiftapps.com
21+
let cspPattern = pattern
22+
const midWildcard = /\*\.[^*]+\*\./
23+
if (midWildcard.test(pattern)) {
24+
const lastWildIdx = pattern.lastIndexOf('*.')
25+
cspPattern = '*.' + pattern.slice(lastWildIdx + 2)
26+
}
27+
return [`http://${cspPattern}`, `https://${cspPattern}`]
28+
}
29+
30+
/**
31+
* Build the CSP frame-src directive from NEXT_PUBLIC_PREVIEW_ALLOWED_HOSTS.
32+
*/
33+
function buildFrameSrc() {
34+
const raw = process.env.NEXT_PUBLIC_PREVIEW_ALLOWED_HOSTS
35+
const source = (raw && raw.trim()) || DEFAULT_PATTERNS
36+
const patterns = source
37+
.split(',')
38+
.map((p) => p.trim())
39+
.filter((p) => p.length > 0)
40+
41+
const entries = patterns.flatMap(toFrameSrcEntries)
42+
return ["'self'", ...entries].join(' ')
43+
}
44+
445
/** @type {import('next').NextConfig} */
546
const nextConfig = {
647
output: 'standalone',
@@ -9,6 +50,19 @@ const nextConfig = {
950
experimental: {
1051
staticGenerationMinPagesPerWorker: 100,
1152
},
53+
async headers() {
54+
return [
55+
{
56+
source: '/(.*)',
57+
headers: [
58+
{
59+
key: 'Content-Security-Policy',
60+
value: `frame-src ${buildFrameSrc()};`,
61+
},
62+
],
63+
},
64+
]
65+
},
1266
}
1367

1468
module.exports = nextConfig

components/ambient-ui/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"private": true,
55
"scripts": {
66
"dev": "next dev --webpack --port 3001",
7-
"build": "next build",
7+
"build": "next build --webpack",
88
"start": "next start",
99
"lint": "eslint",
1010
"test": "vitest run",
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/**
2+
* Ambient UI Preview Bridge
3+
*
4+
* Include this script in pages rendered inside the Ambient UI preview iframe
5+
* to enable cross-origin element capture and hover highlighting.
6+
*
7+
* Usage: <script src="/preview-bridge.js"></script>
8+
*
9+
* The bridge listens for postMessage requests from the parent frame and
10+
* responds with element information at the requested coordinates.
11+
*/
12+
(function () {
13+
'use strict';
14+
15+
var currentHighlight = null;
16+
17+
function getClassName(el) {
18+
// SVG elements have SVGAnimatedString for className, not a plain string
19+
return el.getAttribute('class') || null;
20+
}
21+
22+
// Element capture: parent asks for the element at (x, y)
23+
window.addEventListener('message', function (e) {
24+
if (!e.data || e.data.type !== 'ambient-capture') return;
25+
if (!e.origin || e.origin === 'null') return;
26+
27+
var x = e.data.x;
28+
var y = e.data.y;
29+
var el = document.elementFromPoint(x, y);
30+
31+
if (!el) {
32+
e.source.postMessage({ type: 'ambient-captured', html: null, rect: null }, e.origin);
33+
return;
34+
}
35+
36+
var rect = el.getBoundingClientRect();
37+
e.source.postMessage({
38+
type: 'ambient-captured',
39+
html: el.outerHTML.slice(0, 500),
40+
tagName: el.tagName.toLowerCase(),
41+
id: el.id || null,
42+
className: getClassName(el),
43+
textContent: (el.textContent || '').slice(0, 100),
44+
rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height }
45+
}, e.origin);
46+
});
47+
48+
// Hover highlight: parent sends cursor position, bridge outlines the element
49+
window.addEventListener('message', function (e) {
50+
if (!e.data || e.data.type !== 'ambient-hover') return;
51+
if (!e.origin || e.origin === 'null') return;
52+
53+
var el = document.elementFromPoint(e.data.x, e.data.y);
54+
55+
// Remove previous highlight
56+
if (currentHighlight) {
57+
currentHighlight.style.outline = currentHighlight._ambientSavedOutline || '';
58+
delete currentHighlight._ambientSavedOutline;
59+
currentHighlight = null;
60+
}
61+
62+
if (el && el !== document.documentElement && el !== document.body) {
63+
el._ambientSavedOutline = el.style.outline;
64+
el.style.outline = '2px solid #4394e5';
65+
currentHighlight = el;
66+
}
67+
68+
if (el) {
69+
var rect = el.getBoundingClientRect();
70+
e.source.postMessage({
71+
type: 'ambient-hovered',
72+
rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height }
73+
}, e.origin);
74+
}
75+
});
76+
77+
// Clear hover: remove any active highlight
78+
window.addEventListener('message', function (e) {
79+
if (!e.data || e.data.type !== 'ambient-hover-clear') return;
80+
if (currentHighlight) {
81+
currentHighlight.style.outline = currentHighlight._ambientSavedOutline || '';
82+
delete currentHighlight._ambientSavedOutline;
83+
currentHighlight = null;
84+
}
85+
});
86+
})();

0 commit comments

Comments
 (0)