diff --git a/burr/tracking/server/run.py b/burr/tracking/server/run.py
index 0e5ce62b0..2e9759659 100644
--- a/burr/tracking/server/run.py
+++ b/burr/tracking/server/run.py
@@ -42,7 +42,6 @@
from fastapi import FastAPI, HTTPException, Request
from fastapi.staticfiles import StaticFiles
from fastapi_utils.tasks import repeat_every
- from starlette.templating import Jinja2Templates
from burr.tracking.server import schema
from burr.tracking.server.schema import ( # AnnotationUpdate,
@@ -141,20 +140,8 @@ async def lifespan(app: FastAPI):
await backend.lifespan(app).__anext__()
-app = FastAPI(lifespan=lifespan)
-
-
-@app.get("/ready")
-def is_ready():
- if not initialized:
- raise HTTPException(
- status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Backend is not ready yet."
- )
- return {"ready": True}
-
-
-@app.get("/api/v0/metadata/app_spec", response_model=BackendSpec)
-def get_app_spec():
+def _get_app_spec() -> BackendSpec:
+ """Computes the backend spec from the current backend configuration."""
is_indexing_backend = isinstance(backend, IndexingBackendMixin)
is_snapshotting_backend = isinstance(backend, SnapshottingBackendMixin)
is_annotations_backend = isinstance(backend, AnnotationsBackendMixin)
@@ -167,7 +154,7 @@ def get_app_spec():
)
-app_spec = get_app_spec()
+app_spec = _get_app_spec()
logger = logging.getLogger(__name__)
@@ -190,178 +177,258 @@ def get_app_spec():
)(save_snapshot)
-@app.get("/api/v0/projects", response_model=Sequence[schema.Project])
-async def get_projects(request: Request) -> Sequence[schema.Project]:
- """Gets all projects visible by the user.
+def create_burr_ui_app(serve_static: bool = SERVE_STATIC) -> FastAPI:
+ """Create a fully-configured Burr UI FastAPI application.
- :param request: FastAPI request
- :return: a list of projects visible by the user
- """
- return await backend.list_projects(request)
-
-
-@app.get("/api/v0/{project_id}/{partition_key}/apps", response_model=ApplicationPage)
-async def get_apps(
- request: Request,
- project_id: str,
- partition_key: str,
- limit: int = 100,
- offset: int = 0,
-) -> ApplicationPage:
- """Gets all apps visible by the user
-
- :param request: FastAPI request
- :param project_id: project name
- :return: a list of projects visible by the user
- """
- if partition_key == SENTINEL_PARTITION_KEY:
- partition_key = None
- applications, total_count = await backend.list_apps(
- request, project_id, partition_key=partition_key, limit=limit, offset=offset
- )
- return ApplicationPage(
- applications=list(applications),
- total=total_count,
- has_another_page=total_count > offset + limit,
- )
+ This factory creates a new FastAPI instance with all Burr UI routes,
+ demo routers, and (optionally) static file serving configured.
-
-@app.get("/api/v0/{project_id}/{app_id}/{partition_key}/apps")
-async def get_application_logs(
- request: Request, project_id: str, app_id: str, partition_key: str
-) -> ApplicationLogs:
- """Lists steps for a given App.
- TODO: add streaming capabilities for bi-directional communication
- TODO: add pagination for quicker loading
-
- :param request: FastAPI
- :param project_id: ID of the project
- :param app_id: ID of the assIndociated application
- :return: A list of steps with all associated step data
+ :param serve_static: Whether to serve the React UI static files. Defaults to
+ the BURR_SERVE_STATIC environment variable (true by default).
+ :return: A fully-configured FastAPI application.
"""
- if partition_key == SENTINEL_PARTITION_KEY:
- partition_key = None
- return await backend.get_application_logs(
- request, project_id=project_id, app_id=app_id, partition_key=partition_key
- )
+ ui_app = FastAPI(lifespan=lifespan)
+
+ @ui_app.get("/ready")
+ def is_ready():
+ if not initialized:
+ raise HTTPException(
+ status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
+ detail="Backend is not ready yet.",
+ )
+ return {"ready": True}
+
+ @ui_app.get("/api/v0/metadata/app_spec", response_model=BackendSpec)
+ def get_app_spec():
+ return _get_app_spec()
+
+ @ui_app.get("/api/v0/projects", response_model=Sequence[schema.Project])
+ async def get_projects(request: Request) -> Sequence[schema.Project]:
+ """Gets all projects visible by the user.
+
+ :param request: FastAPI request
+ :return: a list of projects visible by the user
+ """
+ return await backend.list_projects(request)
+
+ @ui_app.get("/api/v0/{project_id}/{partition_key}/apps", response_model=ApplicationPage)
+ async def get_apps(
+ request: Request,
+ project_id: str,
+ partition_key: str,
+ limit: int = 100,
+ offset: int = 0,
+ ) -> ApplicationPage:
+ """Gets all apps visible by the user
+
+ :param request: FastAPI request
+ :param project_id: project name
+ :return: a list of projects visible by the user
+ """
+ if partition_key == SENTINEL_PARTITION_KEY:
+ partition_key = None
+ applications, total_count = await backend.list_apps(
+ request, project_id, partition_key=partition_key, limit=limit, offset=offset
+ )
+ return ApplicationPage(
+ applications=list(applications),
+ total=total_count,
+ has_another_page=total_count > offset + limit,
+ )
+ @ui_app.get("/api/v0/{project_id}/{app_id}/{partition_key}/apps")
+ async def get_application_logs(
+ request: Request, project_id: str, app_id: str, partition_key: str
+ ) -> ApplicationLogs:
+ """Lists steps for a given App.
+ TODO: add streaming capabilities for bi-directional communication
+ TODO: add pagination for quicker loading
+
+ :param request: FastAPI
+ :param project_id: ID of the project
+ :param app_id: ID of the assIndociated application
+ :return: A list of steps with all associated step data
+ """
+ if partition_key == SENTINEL_PARTITION_KEY:
+ partition_key = None
+ return await backend.get_application_logs(
+ request, project_id=project_id, app_id=app_id, partition_key=partition_key
+ )
-@app.post(
- "/api/v0/{project_id}/{app_id}/{partition_key}/{sequence_id}/annotations",
- response_model=AnnotationOut,
-)
-async def create_annotation(
- request: Request,
- project_id: str,
- app_id: str,
- partition_key: str,
- sequence_id: int,
- annotation: AnnotationCreate,
-):
- if partition_key == SENTINEL_PARTITION_KEY:
- partition_key = None
- spec = get_app_spec()
- if not spec.supports_annotations:
- return [] # empty default -- the case that we don't support annotations
- return await backend.create_annotation(
- annotation, project_id, partition_key, app_id, sequence_id
+ @ui_app.post(
+ "/api/v0/{project_id}/{app_id}/{partition_key}/{sequence_id}/annotations",
+ response_model=AnnotationOut,
)
+ async def create_annotation(
+ request: Request,
+ project_id: str,
+ app_id: str,
+ partition_key: str,
+ sequence_id: int,
+ annotation: AnnotationCreate,
+ ):
+ if partition_key == SENTINEL_PARTITION_KEY:
+ partition_key = None
+ spec = _get_app_spec()
+ if not spec.supports_annotations:
+ return [] # empty default -- the case that we don't support annotations
+ return await backend.create_annotation(
+ annotation, project_id, partition_key, app_id, sequence_id
+ )
-
-#
-# # TODO -- take out these parameters cause we have the annotation ID
-@app.put(
- "/api/v0/{project_id}/{annotation_id}/update_annotations",
- response_model=AnnotationOut,
-)
-async def update_annotation(
- request: Request,
- project_id: str,
- annotation_id: int,
- annotation: AnnotationUpdate,
-):
- return await backend.update_annotation(
- annotation_id=annotation_id, annotation=annotation, project_id=project_id
+ #
+ # # TODO -- take out these parameters cause we have the annotation ID
+ @ui_app.put(
+ "/api/v0/{project_id}/{annotation_id}/update_annotations",
+ response_model=AnnotationOut,
)
-
-
-@app.get("/api/v0/{project_id}/annotations", response_model=Sequence[AnnotationOut])
-async def get_annotations(
- request: Request,
- project_id: str,
- app_id: Optional[str] = None,
- partition_key: Optional[str] = None,
- step_sequence_id: Optional[int] = None,
-):
- # Handle the sentinel value for partition_key
- if partition_key == SENTINEL_PARTITION_KEY:
- partition_key = None
- backend_spec = get_app_spec()
-
- if not backend_spec.supports_annotations:
- # makes it easier to wire through to the FE
- return []
-
- # Logic to retrieve the annotations
- return await backend.get_annotations(project_id, partition_key, app_id, step_sequence_id)
-
-
-@app.get("/api/v0/ready")
-async def ready() -> bool:
- return True
-
-
-@app.get("/api/v0/indexing_jobs", response_model=Sequence[IndexingJob])
-async def get_indexing_jobs(
- offset: int = 0, limit: int = 100, filter_empty: bool = True
-) -> Sequence[IndexingJob]:
- if not app_spec.indexing:
- raise HTTPException(
- status_code=status.HTTP_404_NOT_FOUND,
- detail="This backend does not support indexing jobs.",
+ async def update_annotation(
+ request: Request,
+ project_id: str,
+ annotation_id: int,
+ annotation: AnnotationUpdate,
+ ):
+ return await backend.update_annotation(
+ annotation_id=annotation_id, annotation=annotation, project_id=project_id
)
- return await backend.indexing_jobs(offset=offset, limit=limit, filter_empty=filter_empty)
+ @ui_app.get("/api/v0/{project_id}/annotations", response_model=Sequence[AnnotationOut])
+ async def get_annotations(
+ request: Request,
+ project_id: str,
+ app_id: Optional[str] = None,
+ partition_key: Optional[str] = None,
+ step_sequence_id: Optional[int] = None,
+ ):
+ # Handle the sentinel value for partition_key
+ if partition_key == SENTINEL_PARTITION_KEY:
+ partition_key = None
+ backend_spec = _get_app_spec()
+
+ if not backend_spec.supports_annotations:
+ # makes it easier to wire through to the FE
+ return []
+
+ # Logic to retrieve the annotations
+ return await backend.get_annotations(project_id, partition_key, app_id, step_sequence_id)
+
+ @ui_app.get("/api/v0/ready")
+ async def ready() -> bool:
+ return True
+
+ @ui_app.get("/api/v0/indexing_jobs", response_model=Sequence[IndexingJob])
+ async def get_indexing_jobs(
+ offset: int = 0, limit: int = 100, filter_empty: bool = True
+ ) -> Sequence[IndexingJob]:
+ if not app_spec.indexing:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail="This backend does not support indexing jobs.",
+ )
+ return await backend.indexing_jobs(offset=offset, limit=limit, filter_empty=filter_empty)
+
+ @ui_app.get("/api/v0/version")
+ async def version() -> dict:
+ """Returns the burr version"""
+ import pkg_resources
-@app.get("/api/v0/version")
-async def version() -> dict:
- """Returns the burr version"""
- import pkg_resources
-
- try:
- version = pkg_resources.get_distribution("apache-burr").version
- except pkg_resources.DistributionNotFound:
try:
- # Fallback for older installations or development
- version = pkg_resources.get_distribution("burr").version
+ burr_version = pkg_resources.get_distribution("apache-burr").version
except pkg_resources.DistributionNotFound:
- version = "unknown"
- return {"version": version}
-
-
-# Examples -- todo -- put them behind `if` statements
-app.include_router(chatbot.router, prefix="/api/v0/chatbot")
-app.include_router(email_assistant.router, prefix="/api/v0/email_assistant")
-app.include_router(streaming_chatbot.router, prefix="/api/v0/streaming_chatbot")
-app.include_router(deep_researcher.router, prefix="/api/v0/deep_researcher")
-app.include_router(counter.router, prefix="/api/v0/counter")
-
-if SERVE_STATIC:
- BASE_ASSET_DIRECTORY = str(files("burr").joinpath("tracking/server/build"))
+ try:
+ # Fallback for older installations or development
+ burr_version = pkg_resources.get_distribution("burr").version
+ except pkg_resources.DistributionNotFound:
+ burr_version = "unknown"
+ return {"version": burr_version}
+
+ # Examples -- todo -- put them behind `if` statements
+ ui_app.include_router(chatbot.router, prefix="/api/v0/chatbot")
+ ui_app.include_router(email_assistant.router, prefix="/api/v0/email_assistant")
+ ui_app.include_router(streaming_chatbot.router, prefix="/api/v0/streaming_chatbot")
+ ui_app.include_router(deep_researcher.router, prefix="/api/v0/deep_researcher")
+ ui_app.include_router(counter.router, prefix="/api/v0/counter")
+
+ if serve_static:
+ base_asset_directory = str(files("burr").joinpath("tracking/server/build"))
+ static_directory = os.path.join(base_asset_directory, "static")
+
+ ui_app.mount(
+ "/static",
+ StaticFiles(directory=static_directory),
+ "/static",
+ )
+ # public assets in create react app don't get put under build/static,
+ # we need to route them over
+ ui_app.mount("/public", StaticFiles(directory=base_asset_directory, html=True), "/public")
+
+ # Read index.html once at startup
+ with open(os.path.join(base_asset_directory, "index.html")) as f:
+ _index_html_template = f.read()
+
+ @ui_app.get("/manifest.json")
+ async def manifest_json():
+ """Serve manifest.json from the build directory."""
+ from starlette.responses import FileResponse
+
+ return FileResponse(
+ os.path.join(base_asset_directory, "manifest.json"),
+ media_type="application/manifest+json",
+ )
+
+ @ui_app.get("/{rest_of_path:path}")
+ async def react_app(req: Request, rest_of_path: str):
+ """Serves the React app, rewriting asset paths to respect the mount prefix.
+
+ When mounted as a sub-app (e.g. under /burr), the CRA build's hardcoded
+ absolute paths (/static/js/..., /api/v0/...) need to be prefixed with the
+ mount path so the browser fetches them from the correct location.
+
+ This rewrites both the HTML (href/src attributes) and injects a script that
+ sets the OpenAPI client's BASE to the mount prefix, so all API calls are
+ also correctly routed.
+ """
+ from starlette.responses import HTMLResponse
+
+ root_path = req.scope.get("root_path", "")
+ if root_path:
+ # Rewrite CRA's absolute paths to include the mount prefix
+ html = _index_html_template.replace('href="/', f'href="{root_path}/')
+ html = html.replace('src="/', f'src="{root_path}/')
+ # Inject a script before that patches the OpenAPI BASE config
+ # so all runtime API calls (fetch) go to the correct mount path.
+ # Also patches image/asset references that use /public/.
+ patch_script = f""
+ html = html.replace("", f"{patch_script}")
+ else:
+ html = _index_html_template
+ return HTMLResponse(html)
+
+ return ui_app
+
+
+def mount_burr_ui(
+ parent_app: FastAPI,
+ path: str = "/burr",
+ name: str = "burr-ui",
+ serve_static: bool = SERVE_STATIC,
+) -> FastAPI:
+ """Mount the Burr UI inside another FastAPI app.
+
+ :param parent_app: The parent FastAPI application to mount onto.
+ :param path: URL path prefix for the Burr UI. Defaults to "/burr".
+ :param name: Name for the mounted sub-application. Defaults to "burr-ui".
+ :param serve_static: Whether to serve the React UI static files. Defaults to
+ the BURR_SERVE_STATIC environment variable (true by default).
+ :return: The mounted Burr UI FastAPI app instance.
+ """
+ ui_app = create_burr_ui_app(serve_static=serve_static)
+ parent_app.mount(path, ui_app, name=name)
+ return ui_app
- templates = Jinja2Templates(directory=BASE_ASSET_DIRECTORY)
- app.mount(
- "/static", StaticFiles(directory=os.path.join(BASE_ASSET_DIRECTORY, "static")), "/static"
- )
- # public assets in create react app don't get put under build/static, we need to route them over
- app.mount("/public", StaticFiles(directory=BASE_ASSET_DIRECTORY, html=True), "/public")
- @app.get("/{rest_of_path:path}")
- async def react_app(req: Request, rest_of_path: str):
- """Quick trick to server the react app
- Thanks to https://github.com/hop-along-polly/fastapi-webapp-react for the example/demo
- """
- return templates.TemplateResponse("index.html", {"request": req})
+# Module-level app for backwards compatibility (used by uvicorn, CLI, etc.)
+app = create_burr_ui_app()
if __name__ == "__main__":
diff --git a/docs/concepts/tracking.rst b/docs/concepts/tracking.rst
index d22b9f89b..1e69b4525 100644
--- a/docs/concepts/tracking.rst
+++ b/docs/concepts/tracking.rst
@@ -131,3 +131,25 @@ This will print the URL to access the Burr UI web app.
from google.colab import output
output.serve_kernel_port_as_window(7241) # this will open a new window
output.serve_kernel_port_as_iframe(7241) # this will inline in an iframe
+
+---------------------------------------------
+Mount Burr UI inside an existing FastAPI app
+---------------------------------------------
+
+You can embed the Burr UI inside an existing FastAPI application using the
+``mount_burr_ui`` helper.
+
+Example:
+
+.. code-block:: python
+
+ from fastapi import FastAPI
+ from burr.tracking.server.run import mount_burr_ui
+
+ app = FastAPI()
+
+ # Mount Burr UI under /burr
+ mount_burr_ui(app, path="/burr")
+
+This allows you to run the Burr tracking UI alongside your own FastAPI
+application in the same server process.
diff --git a/examples/fastapi_mount_example.py b/examples/fastapi_mount_example.py
new file mode 100644
index 000000000..aa1e9922c
--- /dev/null
+++ b/examples/fastapi_mount_example.py
@@ -0,0 +1,35 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import uvicorn
+from fastapi import FastAPI
+
+from burr.tracking.server.run import mount_burr_ui
+
+app = FastAPI()
+
+# Mount Burr UI under /burr
+mount_burr_ui(app, path="/burr")
+
+
+@app.get("/")
+def root():
+ return {"message": "Main FastAPI app with Burr UI mounted at /burr"}
+
+
+if __name__ == "__main__":
+ uvicorn.run(app, host="0.0.0.0", port=8003)
diff --git a/telemetry/ui/package-lock.json b/telemetry/ui/package-lock.json
index 21461ed4e..95f8310a7 100644
--- a/telemetry/ui/package-lock.json
+++ b/telemetry/ui/package-lock.json
@@ -5566,6 +5566,159 @@
"integrity": "sha512-/0hWQfiaD5//LvGNgc8PjvyqV50vGK0cADYzaoOOGN8fxzBn3iAiaq3S0tCRnFBldq0LVveLcxCTi41ZoYgAgg==",
"peer": true
},
+ "node_modules/@next/swc-darwin-arm64": {
+ "version": "14.2.14",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.14.tgz",
+ "integrity": "sha512-bsxbSAUodM1cjYeA4o6y7sp9wslvwjSkWw57t8DtC8Zig8aG8V6r+Yc05/9mDzLKcybb6EN85k1rJDnMKBd9Gw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-darwin-x64": {
+ "version": "14.2.14",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.14.tgz",
+ "integrity": "sha512-cC9/I+0+SK5L1k9J8CInahduTVWGMXhQoXFeNvF0uNs3Bt1Ub0Azb8JzTU9vNCr0hnaMqiWu/Z0S1hfKc3+dww==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-linux-arm64-gnu": {
+ "version": "14.2.14",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.14.tgz",
+ "integrity": "sha512-RMLOdA2NU4O7w1PQ3Z9ft3PxD6Htl4uB2TJpocm+4jcllHySPkFaUIFacQ3Jekcg6w+LBaFvjSPthZHiPmiAUg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-linux-arm64-musl": {
+ "version": "14.2.14",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.14.tgz",
+ "integrity": "sha512-WgLOA4hT9EIP7jhlkPnvz49iSOMdZgDJVvbpb8WWzJv5wBD07M2wdJXLkDYIpZmCFfo/wPqFsFR4JS4V9KkQ2A==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-linux-x64-gnu": {
+ "version": "14.2.14",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.14.tgz",
+ "integrity": "sha512-lbn7svjUps1kmCettV/R9oAvEW+eUI0lo0LJNFOXoQM5NGNxloAyFRNByYeZKL3+1bF5YE0h0irIJfzXBq9Y6w==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-linux-x64-musl": {
+ "version": "14.2.14",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.14.tgz",
+ "integrity": "sha512-7TcQCvLQ/hKfQRgjxMN4TZ2BRB0P7HwrGAYL+p+m3u3XcKTraUFerVbV3jkNZNwDeQDa8zdxkKkw2els/S5onQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-win32-arm64-msvc": {
+ "version": "14.2.14",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.14.tgz",
+ "integrity": "sha512-8i0Ou5XjTLEje0oj0JiI0Xo9L/93ghFtAUYZ24jARSeTMXLUx8yFIdhS55mTExq5Tj4/dC2fJuaT4e3ySvXU1A==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-win32-ia32-msvc": {
+ "version": "14.2.14",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.14.tgz",
+ "integrity": "sha512-2u2XcSaDEOj+96eXpyjHjtVPLhkAFw2nlaz83EPeuK4obF+HmtDJHqgR1dZB7Gb6V/d55FL26/lYVd0TwMgcOQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-win32-x64-msvc": {
+ "version": "14.2.14",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.14.tgz",
+ "integrity": "sha512-MZom+OvZ1NZxuRovKt1ApevjiUJTcU2PmdJKL66xUPaJeRywnbGGRWUlaAOwunD6dX+pm83vj979NTC8QXjGWg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">= 10"
+ }
+ },
"node_modules/@nicolo-ribaudo/eslint-scope-5-internals": {
"version": "5.1.1-v1",
"resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz",
diff --git a/telemetry/ui/src/App.tsx b/telemetry/ui/src/App.tsx
index 4b80c5bd8..121337ae3 100644
--- a/telemetry/ui/src/App.tsx
+++ b/telemetry/ui/src/App.tsx
@@ -50,7 +50,7 @@ import { DeepResearcherWithTelemetry } from './examples/DeepResearcher';
const App = () => {
return (
-
+ } />
diff --git a/telemetry/ui/src/api/core/OpenAPI.ts b/telemetry/ui/src/api/core/OpenAPI.ts
index 48cc36aa5..3a8dbf3e6 100644
--- a/telemetry/ui/src/api/core/OpenAPI.ts
+++ b/telemetry/ui/src/api/core/OpenAPI.ts
@@ -38,8 +38,14 @@ export type OpenAPIConfig = {
ENCODE_PATH?: ((path: string) => string) | undefined;
};
+// When the Burr UI is mounted as a sub-app (e.g. under /burr), the server
+// injects window.__BURR_BASE_PATH__ so API calls are correctly prefixed.
+const basePath = typeof window !== 'undefined'
+ ? window.__BURR_BASE_PATH__ || ''
+ : '';
+
export const OpenAPI: OpenAPIConfig = {
- BASE: '',
+ BASE: basePath,
VERSION: '0.1.0',
WITH_CREDENTIALS: false,
CREDENTIALS: 'include',
diff --git a/telemetry/ui/src/components/nav/appcontainer.tsx b/telemetry/ui/src/components/nav/appcontainer.tsx
index 86e9614d1..35c217e5f 100644
--- a/telemetry/ui/src/components/nav/appcontainer.tsx
+++ b/telemetry/ui/src/components/nav/appcontainer.tsx
@@ -45,8 +45,7 @@ const GithubLogo = () => (
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 22 22"
- stroke="currentColor"
- >
+ stroke="currentColor">
{/* SVG path for GitHub logo */}
{
},
...(backendSpec?.supports_demos
? [
- {
- name: 'Demos',
- href: '/demos',
- icon: ListBulletIcon,
- linkType: 'internal',
- children: [
- { name: 'counter', href: '/demos/counter', current: false, linkType: 'internal' },
- { name: 'chatbot', href: '/demos/chatbot', current: false, linkType: 'internal' },
- {
- name: 'email-assistant',
- href: '/demos/email-assistant',
- current: false,
- linkType: 'internal'
- },
- {
- name: 'streaming-chatbot',
- href: '/demos/streaming-chatbot',
- current: false,
- linkType: 'internal'
- },
- {
- name: 'deep-researcher',
- href: '/demos/deep-researcher',
- current: false,
- linkType: 'internal'
- }
- ]
- }
- ]
+ {
+ name: 'Demos',
+ href: '/demos',
+ icon: ListBulletIcon,
+ linkType: 'internal',
+ children: [
+ { name: 'counter', href: '/demos/counter', current: false, linkType: 'internal' },
+ { name: 'chatbot', href: '/demos/chatbot', current: false, linkType: 'internal' },
+ {
+ name: 'email-assistant',
+ href: '/demos/email-assistant',
+ current: false,
+ linkType: 'internal'
+ },
+ {
+ name: 'streaming-chatbot',
+ href: '/demos/streaming-chatbot',
+ current: false,
+ linkType: 'internal'
+ },
+ {
+ name: 'deep-researcher',
+ href: '/demos/deep-researcher',
+ current: false,
+ linkType: 'internal'
+ }
+ ]
+ }
+ ]
: []),
{
name: 'Develop',
@@ -192,8 +191,7 @@ export const AppContainer = (props: { children: React.ReactNode }) => {
enterTo="opacity-100"
leave="transition-opacity ease-linear duration-300"
leaveFrom="opacity-100"
- leaveTo="opacity-0"
- >
+ leaveTo="opacity-0">
@@ -205,8 +203,7 @@ export const AppContainer = (props: { children: React.ReactNode }) => {
enterTo="translate-x-0"
leave="transition ease-in-out duration-300 transform"
leaveFrom="translate-x-0"
- leaveTo="-translate-x-full"
- >
+ leaveTo="-translate-x-full">
{
enterTo="opacity-100"
leave="ease-in-out duration-300"
leaveFrom="opacity-100"
- leaveTo="opacity-0"
- >
+ leaveTo="opacity-0">
@@ -231,7 +226,11 @@ export const AppContainer = (props: { children: React.ReactNode }) => {
{/* Sidebar component, swap this element with another sidebar if you like */}