Compare commits

...

10 Commits

81 changed files with 838 additions and 250 deletions

View File

@@ -45,7 +45,7 @@ if [ "$ACTIVE_HOME" != "$MOUNT_HOME" ]; then
[ -d "$MOUNT_HOME/$item" ] || continue
if [ -e "$ACTIVE_HOME/$item" ] && [ ! -L "$ACTIVE_HOME/$item" ]; then
echo "warning: replacing $ACTIVE_HOME/$item with symlink to $MOUNT_HOME/$item" >&2
rm -rf "$ACTIVE_HOME/$item"
rm -rf "${ACTIVE_HOME:?}/$item"
fi
ln -sfn "$MOUNT_HOME/$item" "$ACTIVE_HOME/$item"
done

View File

@@ -86,6 +86,17 @@ repos:
hooks:
- id: actionlint
- repo: https://github.com/shellcheck-py/shellcheck-py
rev: 745eface02aef23e168a8afb6b5737818efbea95 # frozen: v0.11.0.1
hooks:
- id: shellcheck
exclude: >-
(?x)^(
backend/scripts/setup_craft_templates\.sh|
deployment/docker_compose/init-letsencrypt\.sh|
deployment/docker_compose/install\.sh
)$
- repo: https://github.com/psf/black
rev: 8a737e727ac5ab2f1d4cf5876720ed276dc8dc4b # frozen: 25.1.0
hooks:

View File

@@ -212,7 +212,7 @@ def check_for_doc_permissions_sync(self: Task, *, tenant_id: str) -> bool | None
# Tenant-work-gating hook: refresh this tenant's active-set membership
# whenever doc-permission sync has any due cc_pairs to dispatch.
if cc_pair_ids_to_sync:
maybe_mark_tenant_active(tenant_id)
maybe_mark_tenant_active(tenant_id, caller="doc_permission_sync")
lock_beat.reacquire()
for cc_pair_id in cc_pair_ids_to_sync:

View File

@@ -206,7 +206,7 @@ def check_for_external_group_sync(self: Task, *, tenant_id: str) -> bool | None:
# Tenant-work-gating hook: refresh this tenant's active-set membership
# whenever external-group sync has any due cc_pairs to dispatch.
if cc_pair_ids_to_sync:
maybe_mark_tenant_active(tenant_id)
maybe_mark_tenant_active(tenant_id, caller="external_group_sync")
lock_beat.reacquire()
for cc_pair_id in cc_pair_ids_to_sync:

View File

@@ -181,7 +181,7 @@ def check_for_connector_deletion_task(self: Task, *, tenant_id: str) -> bool | N
# nearly every tenant in the active set since most have cc_pairs
# but almost none are actively being deleted on any given cycle.
if has_deleting_cc_pair:
maybe_mark_tenant_active(tenant_id)
maybe_mark_tenant_active(tenant_id, caller="connector_deletion")
# try running cleanup on the cc_pair_ids
for cc_pair_id in cc_pair_ids:

View File

@@ -1020,7 +1020,7 @@ def check_for_indexing(self: Task, *, tenant_id: str) -> int | None:
# `tasks_created > 0` here gives us a "real work was done" signal
# rather than just "tenant has a cc_pair somewhere."
if tasks_created > 0:
maybe_mark_tenant_active(tenant_id)
maybe_mark_tenant_active(tenant_id, caller="check_for_indexing")
# 2/3: VALIDATE
# Check for inconsistent index attempts - active attempts without task IDs

View File

@@ -263,7 +263,7 @@ def check_for_pruning(self: Task, *, tenant_id: str) -> bool | None:
# since most tenants have cc_pairs but almost none are due on
# any given cycle.
if prune_dispatched:
maybe_mark_tenant_active(tenant_id)
maybe_mark_tenant_active(tenant_id, caller="check_for_pruning")
r.set(OnyxRedisSignals.BLOCK_PRUNING, 1, ex=_get_pruning_block_expiration())
# we want to run this less frequently than the overall task

View File

@@ -153,7 +153,7 @@ def try_generate_stale_document_sync_tasks(
# Tenant-work-gating hook: refresh this tenant's active-set membership
# whenever vespa sync actually has stale docs to dispatch.
maybe_mark_tenant_active(tenant_id)
maybe_mark_tenant_active(tenant_id, caller="vespa_sync")
logger.info(
f"Stale documents found (at least {stale_doc_count}). Generating sync tasks in one batch."

View File

@@ -282,6 +282,7 @@ OPENSEARCH_ADMIN_USERNAME = os.environ.get("OPENSEARCH_ADMIN_USERNAME", "admin")
OPENSEARCH_ADMIN_PASSWORD = os.environ.get(
"OPENSEARCH_ADMIN_PASSWORD", "StrongPassword123!"
)
OPENSEARCH_USE_SSL = os.environ.get("OPENSEARCH_USE_SSL", "true").lower() == "true"
USING_AWS_MANAGED_OPENSEARCH = (
os.environ.get("USING_AWS_MANAGED_OPENSEARCH", "").lower() == "true"
)

View File

@@ -62,17 +62,19 @@ def best_effort_get_field_from_issue(jira_issue: Issue, field: str) -> Any:
def extract_text_from_adf(adf: dict | None) -> str:
"""Extracts plain text from Atlassian Document Format:
https://developer.atlassian.com/cloud/jira/platform/apis/document/structure/
WARNING: This function is incomplete and will e.g. skip lists!
"""
# TODO: complete this function
texts = []
if adf is not None and "content" in adf:
for block in adf["content"]:
if "content" in block:
for item in block["content"]:
if item["type"] == "text":
texts.append(item["text"])
texts: list[str] = []
def _extract(node: dict) -> None:
if node.get("type") == "text":
text = node.get("text", "")
if text:
texts.append(text)
for child in node.get("content", []):
_extract(child)
if adf is not None:
_extract(adf)
return " ".join(texts)

View File

@@ -17,6 +17,7 @@ from onyx.configs.app_configs import OPENSEARCH_ADMIN_PASSWORD
from onyx.configs.app_configs import OPENSEARCH_ADMIN_USERNAME
from onyx.configs.app_configs import OPENSEARCH_HOST
from onyx.configs.app_configs import OPENSEARCH_REST_API_PORT
from onyx.configs.app_configs import OPENSEARCH_USE_SSL
from onyx.document_index.interfaces_new import TenantState
from onyx.document_index.opensearch.constants import OpenSearchSearchType
from onyx.document_index.opensearch.schema import DocumentChunk
@@ -132,7 +133,7 @@ class OpenSearchClient(AbstractContextManager):
host: str = OPENSEARCH_HOST,
port: int = OPENSEARCH_REST_API_PORT,
auth: tuple[str, str] = (OPENSEARCH_ADMIN_USERNAME, OPENSEARCH_ADMIN_PASSWORD),
use_ssl: bool = True,
use_ssl: bool = OPENSEARCH_USE_SSL,
verify_certs: bool = False,
ssl_show_warn: bool = False,
timeout: int = DEFAULT_OPENSEARCH_CLIENT_TIMEOUT_S,
@@ -302,7 +303,7 @@ class OpenSearchIndexClient(OpenSearchClient):
host: str = OPENSEARCH_HOST,
port: int = OPENSEARCH_REST_API_PORT,
auth: tuple[str, str] = (OPENSEARCH_ADMIN_USERNAME, OPENSEARCH_ADMIN_PASSWORD),
use_ssl: bool = True,
use_ssl: bool = OPENSEARCH_USE_SSL,
verify_certs: bool = False,
ssl_show_warn: bool = False,
timeout: int = DEFAULT_OPENSEARCH_CLIENT_TIMEOUT_S,
@@ -507,8 +508,55 @@ class OpenSearchIndexClient(OpenSearchClient):
Raises:
Exception: There was an error updating the settings of the index.
"""
# TODO(andrei): Implement this.
raise NotImplementedError
logger.debug(f"Updating settings of index {self._index_name} with {settings}.")
response = self._client.indices.put_settings(
index=self._index_name, body=settings
)
if not response.get("acknowledged", False):
raise RuntimeError(
f"Failed to update settings of index {self._index_name}."
)
logger.debug(f"Settings of index {self._index_name} updated successfully.")
@log_function_time(print_only=True, debug_only=True)
def get_settings(self) -> dict[str, Any]:
"""Gets the settings of the index.
Returns:
The settings of the index.
Raises:
Exception: There was an error getting the settings of the index.
"""
logger.debug(f"Getting settings of index {self._index_name}.")
response = self._client.indices.get_settings(index=self._index_name)
return response[self._index_name]["settings"]
@log_function_time(print_only=True, debug_only=True)
def open_index(self) -> None:
"""Opens the index.
Raises:
Exception: There was an error opening the index.
"""
logger.debug(f"Opening index {self._index_name}.")
response = self._client.indices.open(index=self._index_name)
if not response.get("acknowledged", False):
raise RuntimeError(f"Failed to open index {self._index_name}.")
logger.debug(f"Index {self._index_name} opened successfully.")
@log_function_time(print_only=True, debug_only=True)
def close_index(self) -> None:
"""Closes the index.
Raises:
Exception: There was an error closing the index.
"""
logger.debug(f"Closing index {self._index_name}.")
response = self._client.indices.close(index=self._index_name)
if not response.get("acknowledged", False):
raise RuntimeError(f"Failed to close index {self._index_name}.")
logger.debug(f"Index {self._index_name} closed successfully.")
@log_function_time(
print_only=True,

View File

@@ -5,6 +5,7 @@ import uvicorn
from onyx.configs.app_configs import MCP_SERVER_ENABLED
from onyx.configs.app_configs import MCP_SERVER_HOST
from onyx.configs.app_configs import MCP_SERVER_PORT
from onyx.tracing.setup import setup_tracing
from onyx.utils.logger import setup_logger
from onyx.utils.variable_functionality import set_is_ee_based_on_env_variable
@@ -18,6 +19,7 @@ def main() -> None:
return
set_is_ee_based_on_env_variable()
setup_tracing()
logger.info(f"Starting MCP server on {MCP_SERVER_HOST}:{MCP_SERVER_PORT}")
from onyx.mcp_server.api import mcp_app

View File

@@ -584,7 +584,7 @@ def associate_credential_to_connector(
# Tenant-work-gating lifecycle hook: keep new-tenant latency to
# seconds instead of one full-fanout interval.
maybe_mark_tenant_active(tenant_id)
maybe_mark_tenant_active(tenant_id, caller="cc_pair_lifecycle")
# trigger indexing immediately
client_app.send_task(

View File

@@ -1643,7 +1643,7 @@ def create_connector_with_mock_credential(
# Tenant-work-gating lifecycle hook: keep new-tenant latency to
# seconds instead of one full-fanout interval.
maybe_mark_tenant_active(tenant_id)
maybe_mark_tenant_active(tenant_id, caller="cc_pair_lifecycle")
# trigger indexing immediately
client_app.send_task(

View File

@@ -113,7 +113,7 @@ def cleanup_idle_sandboxes_task(self: Task, *, tenant_id: str) -> None: # noqa:
# Tenant-work-gating hook: refresh this tenant's active-set
# membership whenever sandbox cleanup has work to do.
maybe_mark_tenant_active(tenant_id)
maybe_mark_tenant_active(tenant_id, caller="sandbox_cleanup")
task_logger.info(
f"Found {len(idle_sandboxes)} idle sandboxes to put to sleep"

View File

@@ -0,0 +1,151 @@
"""Prometheus metrics for embedding generation latency and throughput.
Tracks client-side round-trip latency (as seen by callers of
``EmbeddingModel.encode``) and server-side execution time (as measured inside
the model server for the local-model path). Both API-provider and local-model
paths flow through the client-side metric; only the local path populates the
server-side metric.
"""
import logging
from collections.abc import Generator
from contextlib import contextmanager
from prometheus_client import Counter
from prometheus_client import Gauge
from prometheus_client import Histogram
from shared_configs.enums import EmbeddingProvider
from shared_configs.enums import EmbedTextType
logger = logging.getLogger(__name__)
LOCAL_PROVIDER_LABEL = "local"
_EMBEDDING_LATENCY_BUCKETS = (
0.005,
0.01,
0.025,
0.05,
0.1,
0.25,
0.5,
1.0,
2.5,
5.0,
10.0,
25.0,
)
PROVIDER_LABEL_NAME = "provider"
TEXT_TYPE_LABEL_NAME = "text_type"
STATUS_LABEL_NAME = "status"
_client_duration = Histogram(
"onyx_embedding_client_duration_seconds",
"Client-side end-to-end latency of an embedding batch as seen by the caller.",
[PROVIDER_LABEL_NAME, TEXT_TYPE_LABEL_NAME],
buckets=_EMBEDDING_LATENCY_BUCKETS,
)
_embedding_requests_total = Counter(
"onyx_embedding_requests_total",
"Total embedding batch requests, labeled by outcome.",
[PROVIDER_LABEL_NAME, TEXT_TYPE_LABEL_NAME, STATUS_LABEL_NAME],
)
_embedding_texts_total = Counter(
"onyx_embedding_texts_total",
"Total number of individual texts submitted for embedding.",
[PROVIDER_LABEL_NAME, TEXT_TYPE_LABEL_NAME],
)
_embedding_input_chars_total = Counter(
"onyx_embedding_input_chars_total",
"Total number of input characters submitted for embedding.",
[PROVIDER_LABEL_NAME, TEXT_TYPE_LABEL_NAME],
)
_embeddings_in_progress = Gauge(
"onyx_embeddings_in_progress",
"Number of embedding batches currently in-flight.",
[PROVIDER_LABEL_NAME, TEXT_TYPE_LABEL_NAME],
)
def provider_label(provider: EmbeddingProvider | None) -> str:
if provider is None:
return LOCAL_PROVIDER_LABEL
return provider.value
def observe_embedding_client(
provider: EmbeddingProvider | None,
text_type: EmbedTextType,
duration_s: float,
num_texts: int,
num_chars: int,
success: bool,
) -> None:
"""Records a completed embedding batch.
Args:
provider: The embedding provider, or ``None`` for the local model path.
text_type: Whether this was a query- or passage-style embedding.
duration_s: Wall-clock duration measured on the client side, in seconds.
num_texts: Number of texts in the batch.
num_chars: Total number of input characters in the batch.
success: Whether the embedding call succeeded.
"""
try:
provider_lbl = provider_label(provider)
text_type_lbl = text_type.value
status_lbl = "success" if success else "failure"
_embedding_requests_total.labels(
provider=provider_lbl, text_type=text_type_lbl, status=status_lbl
).inc()
_client_duration.labels(provider=provider_lbl, text_type=text_type_lbl).observe(
duration_s
)
if success:
_embedding_texts_total.labels(
provider=provider_lbl, text_type=text_type_lbl
).inc(num_texts)
_embedding_input_chars_total.labels(
provider=provider_lbl, text_type=text_type_lbl
).inc(num_chars)
except Exception:
logger.warning("Failed to record embedding client metrics.", exc_info=True)
@contextmanager
def track_embedding_in_progress(
provider: EmbeddingProvider | None,
text_type: EmbedTextType,
) -> Generator[None, None, None]:
"""Context manager that tracks in-flight embedding batches via a Gauge."""
incremented = False
provider_lbl = provider_label(provider)
text_type_lbl = text_type.value
try:
_embeddings_in_progress.labels(
provider=provider_lbl, text_type=text_type_lbl
).inc()
incremented = True
except Exception:
logger.warning(
"Failed to increment in-progress embedding gauge.", exc_info=True
)
try:
yield
finally:
if incremented:
try:
_embeddings_in_progress.labels(
provider=provider_lbl, text_type=text_type_lbl
).dec()
except Exception:
logger.warning(
"Failed to decrement in-progress embedding gauge.", exc_info=True
)

View File

@@ -46,7 +46,7 @@ stop_and_remove_containers
# Start the PostgreSQL container with optional volume
echo "Starting PostgreSQL container..."
if [[ -n "$POSTGRES_VOLUME" ]]; then
docker run -p 5432:5432 --name onyx_postgres -e POSTGRES_PASSWORD=password -d -v $POSTGRES_VOLUME:/var/lib/postgresql/data postgres -c max_connections=250
docker run -p 5432:5432 --name onyx_postgres -e POSTGRES_PASSWORD=password -d -v "$POSTGRES_VOLUME":/var/lib/postgresql/data postgres -c max_connections=250
else
docker run -p 5432:5432 --name onyx_postgres -e POSTGRES_PASSWORD=password -d postgres -c max_connections=250
fi
@@ -54,7 +54,7 @@ fi
# Start the Vespa container with optional volume
echo "Starting Vespa container..."
if [[ -n "$VESPA_VOLUME" ]]; then
docker run --detach --name onyx_vespa --hostname vespa-container --publish 8081:8081 --publish 19071:19071 -v $VESPA_VOLUME:/opt/vespa/var vespaengine/vespa:8
docker run --detach --name onyx_vespa --hostname vespa-container --publish 8081:8081 --publish 19071:19071 -v "$VESPA_VOLUME":/opt/vespa/var vespaengine/vespa:8
else
docker run --detach --name onyx_vespa --hostname vespa-container --publish 8081:8081 --publish 19071:19071 vespaengine/vespa:8
fi
@@ -85,7 +85,7 @@ docker compose -f "$COMPOSE_FILE" -f "$COMPOSE_DEV_FILE" --profile opensearch-en
# Start the Redis container with optional volume
echo "Starting Redis container..."
if [[ -n "$REDIS_VOLUME" ]]; then
docker run --detach --name onyx_redis --publish 6379:6379 -v $REDIS_VOLUME:/data redis
docker run --detach --name onyx_redis --publish 6379:6379 -v "$REDIS_VOLUME":/data redis
else
docker run --detach --name onyx_redis --publish 6379:6379 redis
fi
@@ -93,7 +93,7 @@ fi
# Start the MinIO container with optional volume
echo "Starting MinIO container..."
if [[ -n "$MINIO_VOLUME" ]]; then
docker run --detach --name onyx_minio --publish 9004:9000 --publish 9005:9001 -e MINIO_ROOT_USER=minioadmin -e MINIO_ROOT_PASSWORD=minioadmin -v $MINIO_VOLUME:/data minio/minio server /data --console-address ":9001"
docker run --detach --name onyx_minio --publish 9004:9000 --publish 9005:9001 -e MINIO_ROOT_USER=minioadmin -e MINIO_ROOT_PASSWORD=minioadmin -v "$MINIO_VOLUME":/data minio/minio server /data --console-address ":9001"
else
docker run --detach --name onyx_minio --publish 9004:9000 --publish 9005:9001 -e MINIO_ROOT_USER=minioadmin -e MINIO_ROOT_PASSWORD=minioadmin minio/minio server /data --console-address ":9001"
fi
@@ -111,6 +111,7 @@ sleep 1
# Alembic should be configured in the virtualenv for this repo
if [[ -f "../.venv/bin/activate" ]]; then
# shellcheck source=/dev/null
source ../.venv/bin/activate
else
echo "Warning: Python virtual environment not found at .venv/bin/activate; alembic may not work."

View File

@@ -446,10 +446,107 @@ class TestOpenSearchClient:
test_client.create_index(mappings=mappings, settings=settings)
def test_update_settings(self, test_client: OpenSearchIndexClient) -> None:
"""Tests that update_settings raises NotImplementedError."""
"""Tests updating index settings on an existing index."""
# Precondition.
mappings = DocumentSchema.get_document_schema(
vector_dimension=128, multitenant=True
)
settings = DocumentSchema.get_index_settings_based_on_environment()
test_client.create_index(mappings=mappings, settings=settings)
# Assert that the current number of replicas is not the desired test
# number we are updating to.
test_num_replicas = 0
current_settings = test_client.get_settings()
assert current_settings["index"]["number_of_replicas"] != f"{test_num_replicas}"
# Under test.
# Should not raise. number_of_replicas is a dynamic setting that can be
# changed without closing the index.
test_client.update_settings(
settings={"index": {"number_of_replicas": test_num_replicas}}
)
# Postcondition.
current_settings = test_client.get_settings()
assert current_settings["index"]["number_of_replicas"] == f"{test_num_replicas}"
def test_update_settings_on_nonexistent_index(
self, test_client: OpenSearchIndexClient
) -> None:
"""Tests updating settings on a nonexistent index raises an error."""
# Under test and postcondition.
with pytest.raises(NotImplementedError):
test_client.update_settings(settings={})
with pytest.raises(Exception, match="index_not_found_exception|404"):
test_client.update_settings(settings={"index": {"number_of_replicas": 0}})
def test_get_settings(self, test_client: OpenSearchIndexClient) -> None:
"""Tests getting index settings."""
# Precondition.
mappings = DocumentSchema.get_document_schema(
vector_dimension=128, multitenant=True
)
settings = DocumentSchema.get_index_settings_based_on_environment()
test_client.create_index(mappings=mappings, settings=settings)
# Under test.
current_settings = test_client.get_settings()
# Postcondition.
assert "index" in current_settings
# These are always present for any index.
assert "number_of_shards" in current_settings["index"]
assert "number_of_replicas" in current_settings["index"]
assert current_settings["index"]["provided_name"] == test_client._index_name
def test_get_settings_on_nonexistent_index(
self, test_client: OpenSearchIndexClient
) -> None:
"""Tests getting settings on a nonexistent index raises an error."""
# Under test and postcondition.
with pytest.raises(Exception, match="index_not_found_exception|404"):
test_client.get_settings()
def test_close_and_open_index(self, test_client: OpenSearchIndexClient) -> None:
"""Tests closing and reopening an index."""
# Precondition.
mappings = DocumentSchema.get_document_schema(
vector_dimension=128, multitenant=True
)
settings = DocumentSchema.get_index_settings_based_on_environment()
test_client.create_index(mappings=mappings, settings=settings)
# Under test.
# Closing should not raise.
test_client.close_index()
# Postcondition.
# Searches on a closed index should fail.
with pytest.raises(Exception, match="index_closed_exception|closed"):
test_client.search_for_document_ids(
body={"_source": False, "query": {"match_all": {}}}
)
# Under test.
# Reopening should not raise.
test_client.open_index()
# Postcondition.
# Searches should work again after reopening.
result = test_client.search_for_document_ids(
body={"_source": False, "query": {"match_all": {}}}
)
assert result == []
def test_close_nonexistent_index(self, test_client: OpenSearchIndexClient) -> None:
"""Tests closing a nonexistent index raises an error."""
# Under test and postcondition.
with pytest.raises(Exception, match="index_not_found_exception|404"):
test_client.close_index()
def test_open_nonexistent_index(self, test_client: OpenSearchIndexClient) -> None:
"""Tests opening a nonexistent index raises an error."""
# Under test and postcondition.
with pytest.raises(Exception, match="index_not_found_exception|404"):
test_client.open_index()
def test_create_and_delete_search_pipeline(
self, test_client: OpenSearchIndexClient

View File

@@ -0,0 +1,257 @@
"""Tests for embedding Prometheus metrics."""
from unittest.mock import patch
from onyx.server.metrics.embedding import _client_duration
from onyx.server.metrics.embedding import _embedding_input_chars_total
from onyx.server.metrics.embedding import _embedding_requests_total
from onyx.server.metrics.embedding import _embedding_texts_total
from onyx.server.metrics.embedding import _embeddings_in_progress
from onyx.server.metrics.embedding import LOCAL_PROVIDER_LABEL
from onyx.server.metrics.embedding import observe_embedding_client
from onyx.server.metrics.embedding import provider_label
from onyx.server.metrics.embedding import PROVIDER_LABEL_NAME
from onyx.server.metrics.embedding import TEXT_TYPE_LABEL_NAME
from onyx.server.metrics.embedding import track_embedding_in_progress
from shared_configs.enums import EmbeddingProvider
from shared_configs.enums import EmbedTextType
class TestProviderLabel:
def test_none_maps_to_local(self) -> None:
assert provider_label(None) == LOCAL_PROVIDER_LABEL
def test_enum_maps_to_value(self) -> None:
assert provider_label(EmbeddingProvider.OPENAI) == "openai"
assert provider_label(EmbeddingProvider.COHERE) == "cohere"
class TestObserveEmbeddingClient:
def test_success_records_all_counters(self) -> None:
# Precondition.
provider = EmbeddingProvider.OPENAI
text_type = EmbedTextType.QUERY
labels = {
PROVIDER_LABEL_NAME: provider.value,
TEXT_TYPE_LABEL_NAME: text_type.value,
}
before_requests = _embedding_requests_total.labels(
**labels, status="success"
)._value.get()
before_texts = _embedding_texts_total.labels(**labels)._value.get()
before_chars = _embedding_input_chars_total.labels(**labels)._value.get()
before_duration_sum = _client_duration.labels(**labels)._sum.get()
test_duration_s = 0.123
test_num_texts = 4
test_num_chars = 200
# Under test.
observe_embedding_client(
provider=provider,
text_type=text_type,
duration_s=test_duration_s,
num_texts=test_num_texts,
num_chars=test_num_chars,
success=True,
)
# Postcondition.
assert (
_embedding_requests_total.labels(**labels, status="success")._value.get()
== before_requests + 1
)
assert (
_embedding_texts_total.labels(**labels)._value.get()
== before_texts + test_num_texts
)
assert (
_embedding_input_chars_total.labels(**labels)._value.get()
== before_chars + test_num_chars
)
assert (
_client_duration.labels(**labels)._sum.get()
== before_duration_sum + test_duration_s
)
def test_failure_records_duration_and_failure_counter_only(self) -> None:
# Precondition.
provider = EmbeddingProvider.COHERE
text_type = EmbedTextType.PASSAGE
labels = {
PROVIDER_LABEL_NAME: provider.value,
TEXT_TYPE_LABEL_NAME: text_type.value,
}
before_failure = _embedding_requests_total.labels(
**labels, status="failure"
)._value.get()
before_texts = _embedding_texts_total.labels(**labels)._value.get()
before_chars = _embedding_input_chars_total.labels(**labels)._value.get()
before_duration_sum = _client_duration.labels(**labels)._sum.get()
test_duration_s = 0.5
test_num_texts = 3
test_num_chars = 150
# Under test.
observe_embedding_client(
provider=provider,
text_type=text_type,
duration_s=test_duration_s,
num_texts=test_num_texts,
num_chars=test_num_chars,
success=False,
)
# Postcondition.
# Failure counter incremented.
assert (
_embedding_requests_total.labels(**labels, status="failure")._value.get()
== before_failure + 1
)
# Duration still recorded.
assert (
_client_duration.labels(**labels)._sum.get()
== before_duration_sum + test_duration_s
)
# Throughput counters NOT bumped on failure.
assert _embedding_texts_total.labels(**labels)._value.get() == before_texts
assert (
_embedding_input_chars_total.labels(**labels)._value.get() == before_chars
)
def test_local_provider_uses_local_label(self) -> None:
# Precondition.
text_type = EmbedTextType.QUERY
labels = {
PROVIDER_LABEL_NAME: LOCAL_PROVIDER_LABEL,
TEXT_TYPE_LABEL_NAME: text_type.value,
}
before = _embedding_requests_total.labels(
**labels, status="success"
)._value.get()
test_duration_s = 0.05
test_num_texts = 1
test_num_chars = 10
# Under test.
observe_embedding_client(
provider=None,
text_type=text_type,
duration_s=test_duration_s,
num_texts=test_num_texts,
num_chars=test_num_chars,
success=True,
)
# Postcondition.
assert (
_embedding_requests_total.labels(**labels, status="success")._value.get()
== before + 1
)
def test_exceptions_do_not_propagate(self) -> None:
with patch.object(
_embedding_requests_total,
"labels",
side_effect=RuntimeError("boom"),
):
# Must not raise.
observe_embedding_client(
provider=EmbeddingProvider.OPENAI,
text_type=EmbedTextType.QUERY,
duration_s=0.1,
num_texts=1,
num_chars=10,
success=True,
)
class TestTrackEmbeddingInProgress:
def test_gauge_increments_and_decrements(self) -> None:
# Precondition.
provider = EmbeddingProvider.OPENAI
text_type = EmbedTextType.QUERY
labels = {
PROVIDER_LABEL_NAME: provider.value,
TEXT_TYPE_LABEL_NAME: text_type.value,
}
before = _embeddings_in_progress.labels(**labels)._value.get()
# Under test.
with track_embedding_in_progress(provider, text_type):
during = _embeddings_in_progress.labels(**labels)._value.get()
assert during == before + 1
# Postcondition.
after = _embeddings_in_progress.labels(**labels)._value.get()
assert after == before
def test_gauge_decrements_on_exception(self) -> None:
# Precondition.
provider = EmbeddingProvider.COHERE
text_type = EmbedTextType.PASSAGE
labels = {
PROVIDER_LABEL_NAME: provider.value,
TEXT_TYPE_LABEL_NAME: text_type.value,
}
before = _embeddings_in_progress.labels(**labels)._value.get()
# Under test.
raised = False
try:
with track_embedding_in_progress(provider, text_type):
raise ValueError("simulated embedding failure")
except ValueError:
raised = True
assert raised
# Postcondition.
after = _embeddings_in_progress.labels(**labels)._value.get()
assert after == before
def test_local_provider_uses_local_label(self) -> None:
# Precondition.
text_type = EmbedTextType.QUERY
labels = {
PROVIDER_LABEL_NAME: LOCAL_PROVIDER_LABEL,
TEXT_TYPE_LABEL_NAME: text_type.value,
}
before = _embeddings_in_progress.labels(**labels)._value.get()
# Under test.
with track_embedding_in_progress(None, text_type):
during = _embeddings_in_progress.labels(**labels)._value.get()
assert during == before + 1
# Postcondition.
after = _embeddings_in_progress.labels(**labels)._value.get()
assert after == before
def test_inc_exception_does_not_break_call(self) -> None:
# Precondition.
provider = EmbeddingProvider.VOYAGE
text_type = EmbedTextType.QUERY
labels = {
PROVIDER_LABEL_NAME: provider.value,
TEXT_TYPE_LABEL_NAME: text_type.value,
}
before = _embeddings_in_progress.labels(**labels)._value.get()
# Under test.
with patch.object(
_embeddings_in_progress.labels(**labels),
"inc",
side_effect=RuntimeError("boom"),
):
# Context manager should still yield without decrementing.
with track_embedding_in_progress(provider, text_type):
during = _embeddings_in_progress.labels(**labels)._value.get()
assert during == before
# Postcondition.
after = _embeddings_in_progress.labels(**labels)._value.get()
assert after == before

View File

@@ -58,8 +58,7 @@ SERVICE_ORDER=(
validate_template() {
local template_file=$1
echo "Validating template: $template_file..."
aws cloudformation validate-template --template-body file://"$template_file" --region "$AWS_REGION" > /dev/null
if [ $? -ne 0 ]; then
if ! aws cloudformation validate-template --template-body file://"$template_file" --region "$AWS_REGION" > /dev/null; then
echo "Error: Validation failed for $template_file. Exiting."
exit 1
fi
@@ -108,13 +107,15 @@ deploy_stack() {
fi
# Create temporary parameters file for this template
local temp_params_file=$(create_parameters_from_json "$template_file")
local temp_params_file
temp_params_file=$(create_parameters_from_json "$template_file")
# Special handling for SubnetIDs parameter if needed
if grep -q "SubnetIDs" "$template_file"; then
echo "Template uses SubnetIDs parameter, ensuring it's properly formatted..."
# Make sure we're passing SubnetIDs as a comma-separated list
local subnet_ids=$(remove_comments "$CONFIG_FILE" | jq -r '.SubnetIDs // empty')
local subnet_ids
subnet_ids=$(remove_comments "$CONFIG_FILE" | jq -r '.SubnetIDs // empty')
if [ -n "$subnet_ids" ]; then
echo "Using SubnetIDs from config: $subnet_ids"
else
@@ -123,15 +124,13 @@ deploy_stack() {
fi
echo "Deploying stack: $stack_name with template: $template_file and generated config from: $CONFIG_FILE..."
aws cloudformation deploy \
if ! aws cloudformation deploy \
--stack-name "$stack_name" \
--template-file "$template_file" \
--parameter-overrides file://"$temp_params_file" \
--capabilities CAPABILITY_IAM CAPABILITY_NAMED_IAM CAPABILITY_AUTO_EXPAND \
--region "$AWS_REGION" \
--no-cli-auto-prompt > /dev/null
if [ $? -ne 0 ]; then
--no-cli-auto-prompt > /dev/null; then
echo "Error: Deployment failed for $stack_name. Exiting."
exit 1
fi

View File

@@ -52,11 +52,9 @@ delete_stack() {
--region "$AWS_REGION"
echo "Waiting for stack $stack_name to be deleted..."
aws cloudformation wait stack-delete-complete \
if aws cloudformation wait stack-delete-complete \
--stack-name "$stack_name" \
--region "$AWS_REGION"
if [ $? -eq 0 ]; then
--region "$AWS_REGION"; then
echo "Stack $stack_name deleted successfully."
sleep 10
else

View File

@@ -1,3 +1,4 @@
#!/bin/sh
# fill in the template
export ONYX_BACKEND_API_HOST="${ONYX_BACKEND_API_HOST:-api_server}"
export ONYX_WEB_SERVER_HOST="${ONYX_WEB_SERVER_HOST:-web_server}"
@@ -16,12 +17,15 @@ echo "Using web server host: $ONYX_WEB_SERVER_HOST"
echo "Using MCP server host: $ONYX_MCP_SERVER_HOST"
echo "Using nginx proxy timeouts - connect: ${NGINX_PROXY_CONNECT_TIMEOUT}s, send: ${NGINX_PROXY_SEND_TIMEOUT}s, read: ${NGINX_PROXY_READ_TIMEOUT}s"
# shellcheck disable=SC2016
envsubst '$DOMAIN $SSL_CERT_FILE_NAME $SSL_CERT_KEY_FILE_NAME $ONYX_BACKEND_API_HOST $ONYX_WEB_SERVER_HOST $ONYX_MCP_SERVER_HOST $NGINX_PROXY_CONNECT_TIMEOUT $NGINX_PROXY_SEND_TIMEOUT $NGINX_PROXY_READ_TIMEOUT' < "/etc/nginx/conf.d/$1" > /etc/nginx/conf.d/app.conf
# Conditionally create MCP server configuration
if [ "${MCP_SERVER_ENABLED}" = "True" ] || [ "${MCP_SERVER_ENABLED}" = "true" ]; then
echo "MCP server is enabled, creating MCP configuration..."
# shellcheck disable=SC2016
envsubst '$ONYX_MCP_SERVER_HOST' < "/etc/nginx/conf.d/mcp_upstream.conf.inc.template" > /etc/nginx/conf.d/mcp_upstream.conf.inc
# shellcheck disable=SC2016
envsubst '$ONYX_MCP_SERVER_HOST' < "/etc/nginx/conf.d/mcp.conf.inc.template" > /etc/nginx/conf.d/mcp.conf.inc
else
echo "MCP server is disabled, removing MCP configuration..."

View File

@@ -48,6 +48,19 @@ func runWebScript(args []string) {
log.Fatalf("Failed to find web directory: %v", err)
}
nodeModules := filepath.Join(webDir, "node_modules")
if _, err := os.Stat(nodeModules); os.IsNotExist(err) {
log.Info("node_modules not found, running npm install --no-save...")
installCmd := exec.Command("npm", "install", "--no-save")
installCmd.Dir = webDir
installCmd.Stdout = os.Stdout
installCmd.Stderr = os.Stderr
installCmd.Stdin = os.Stdin
if err := installCmd.Run(); err != nil {
log.Fatalf("Failed to run npm install: %v", err)
}
}
scriptName := args[0]
scriptArgs := args[1:]
if len(scriptArgs) > 0 && scriptArgs[0] == "--" {

View File

@@ -55,7 +55,7 @@ A two-axis layout component that automatically routes to the correct internal la
Wraps `Content` and adds a `rightChildren` slot. Accepts all `Content` props plus:
- `rightChildren`: `ReactNode` — actions rendered on the right
- `paddingVariant`: `SizeVariant` — controls outer padding
- `padding`: `SizeVariant` — controls outer padding
```typescript
<ContentAction
@@ -544,7 +544,7 @@ function UserCard({
## 4. Spacing Guidelines
**Prefer padding over margins for spacing. When a library component exposes a padding prop
(e.g., `paddingVariant`), use that prop instead of wrapping it in a `<div>` with padding classes.
(e.g., `padding`), use that prop instead of wrapping it in a `<div>` with padding classes.
If a library component does not expose a padding override and you find yourself adding a wrapper
div for spacing, consider updating the library component to accept one.**
@@ -553,7 +553,7 @@ divs that exist solely for spacing.
```typescript
// ✅ Good — use the component's padding prop
<ContentAction paddingVariant="md" ... />
<ContentAction padding="md" ... />
// ✅ Good — padding utilities when no component prop exists
<div className="p-4 space-y-2">

View File

@@ -68,9 +68,7 @@ SCRIPT_DIR="$(dirname "${BASH_SOURCE[0]}")"
# Run the conversion into a temp file so a failed run doesn't destroy an existing .tsx
TMPFILE="${BASE_NAME}.tsx.tmp"
bunx @svgr/cli "$SVG_FILE" --typescript --svgo-config "$SVGO_CONFIG" --template "${SCRIPT_DIR}/icon-template.js" > "$TMPFILE"
if [ $? -eq 0 ]; then
if bunx @svgr/cli "$SVG_FILE" --typescript --svgo-config "$SVGO_CONFIG" --template "${SCRIPT_DIR}/icon-template.js" > "$TMPFILE"; then
# Verify the temp file has content before replacing the destination
if [ ! -s "$TMPFILE" ]; then
rm -f "$TMPFILE"
@@ -84,16 +82,14 @@ if [ $? -eq 0 ]; then
# Using perl for cross-platform compatibility (works on macOS, Linux, Windows with WSL)
# Note: perl -i returns 0 even on some failures, so we validate the output
perl -i -pe 's/<svg/<svg width={size} height={size}/g' "${BASE_NAME}.tsx"
if [ $? -ne 0 ]; then
if ! perl -i -pe 's/<svg/<svg width={size} height={size}/g' "${BASE_NAME}.tsx"; then
echo "Error: Failed to add width/height attributes" >&2
exit 1
fi
# Icons additionally get stroke="currentColor"
if [ "$MODE" = "icon" ]; then
perl -i -pe 's/\{\.\.\.props\}/stroke="currentColor" {...props}/g' "${BASE_NAME}.tsx"
if [ $? -ne 0 ]; then
if ! perl -i -pe 's/\{\.\.\.props\}/stroke="currentColor" {...props}/g' "${BASE_NAME}.tsx"; then
echo "Error: Failed to add stroke attribute" >&2
exit 1
fi

View File

@@ -95,9 +95,9 @@ function Button({
<Interactive.Container
type={type}
border={interactiveProps.prominence === "secondary"}
heightVariant={size}
widthVariant={width}
roundingVariant={isLarge ? "md" : size === "2xs" ? "xs" : "sm"}
size={size}
width={width}
rounding={isLarge ? "md" : size === "2xs" ? "xs" : "sm"}
>
<div className="flex flex-row items-center gap-1">
{iconWrapper(Icon, size, !!children)}

View File

@@ -8,13 +8,13 @@ A composite component that wraps `Interactive.Stateful > Interactive.Container >
```
Interactive.Stateful <- selectVariant, state, interaction, onClick, href, ref
└─ Interactive.Container <- type, width, roundingVariant
└─ ContentAction <- withInteractive, paddingVariant="lg"
└─ Interactive.Container <- type, width, rounding
└─ ContentAction <- withInteractive, padding="lg"
├─ Content <- icon, title, description, sizePreset, variant, ...
└─ rightChildren
```
`paddingVariant` is hardcoded to `"lg"` and `withInteractive` is always `true`. These are not exposed as props.
`padding` is hardcoded to `"lg"` and `withInteractive` is always `true`. These are not exposed as props.
## Props
@@ -35,7 +35,7 @@ Interactive.Stateful <- selectVariant, state, interaction, onClick, href
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `roundingVariant` | `InteractiveContainerRoundingVariant` | `"md"` | Corner rounding preset (height is content-driven) |
| `rounding` | `InteractiveContainerRoundingVariant` | `"md"` | Corner rounding preset (height is content-driven) |
| `width` | `WidthVariant` | `"full"` | Container width |
| `type` | `"submit" \| "button" \| "reset"` | `"button"` | HTML button type |
| `tooltip` | `string` | — | Tooltip text shown on hover |
@@ -63,7 +63,7 @@ import { LineItemButton } from "@opal/components";
<LineItemButton
selectVariant="select-heavy"
state={isSelected ? "selected" : "empty"}
roundingVariant="sm"
rounding="sm"
onClick={handleClick}
title="gpt-4o"
sizePreset="main-ui"

View File

@@ -5,8 +5,7 @@ import {
} from "@opal/core";
import type { ExtremaSizeVariants, DistributiveOmit } from "@opal/types";
import { Tooltip, type TooltipSide } from "@opal/components";
import type { ContentActionProps } from "@opal/layouts/content-action/components";
import { ContentAction } from "@opal/layouts";
import { type ContentActionProps, ContentAction } from "@opal/layouts";
// ---------------------------------------------------------------------------
// Types
@@ -14,7 +13,7 @@ import { ContentAction } from "@opal/layouts";
type ContentPassthroughProps = DistributiveOmit<
ContentActionProps,
"paddingVariant" | "widthVariant" | "ref"
"padding" | "width" | "ref"
>;
type LineItemButtonOwnProps = Pick<
@@ -32,7 +31,7 @@ type LineItemButtonOwnProps = Pick<
selectVariant?: "select-light" | "select-heavy";
/** Corner rounding preset (height is always content-driven). @default "md" */
roundingVariant?: InteractiveContainerRoundingVariant;
rounding?: InteractiveContainerRoundingVariant;
/** Container width. @default "full" */
width?: ExtremaSizeVariants;
@@ -63,7 +62,7 @@ function LineItemButton({
type = "button",
// Sizing
roundingVariant = "md",
rounding = "md",
width = "full",
tooltip,
tooltipSide = "top",
@@ -84,14 +83,16 @@ function LineItemButton({
>
<Interactive.Container
type={type}
widthVariant={width}
heightVariant="lg"
roundingVariant={roundingVariant}
width={width}
size="fit"
rounding={rounding}
>
<ContentAction
{...(contentActionProps as ContentActionProps)}
paddingVariant="fit"
/>
<div className="w-full p-2">
<ContentAction
{...(contentActionProps as ContentActionProps)}
padding="fit"
/>
</div>
</Interactive.Container>
</Interactive.Stateful>
);

View File

@@ -70,7 +70,7 @@ type OpenButtonProps = Omit<InteractiveStatefulProps, "variant"> & {
tooltipSide?: TooltipSide;
/** Override the default rounding derived from `size`. */
roundingVariant?: InteractiveContainerRoundingVariant;
rounding?: InteractiveContainerRoundingVariant;
/** Applies disabled styling and suppresses clicks. */
disabled?: boolean;
@@ -89,7 +89,7 @@ function OpenButton({
justifyContent,
tooltip,
tooltipSide = "top",
roundingVariant: roundingVariantOverride,
rounding: roundingOverride,
interaction,
variant = "select-heavy",
disabled,
@@ -123,11 +123,10 @@ function OpenButton({
>
<Interactive.Container
type="button"
heightVariant={size}
widthVariant={width}
roundingVariant={
roundingVariantOverride ??
(isLarge ? "md" : size === "2xs" ? "xs" : "sm")
size={size}
width={width}
rounding={
roundingOverride ?? (isLarge ? "md" : size === "2xs" ? "xs" : "sm")
}
>
<div

View File

@@ -96,9 +96,9 @@ function SelectButton({
<Interactive.Stateful disabled={disabled} {...statefulProps}>
<Interactive.Container
type={type}
heightVariant={size}
widthVariant={width}
roundingVariant={isLarge ? "md" : size === "2xs" ? "xs" : "sm"}
size={size}
width={width}
rounding={isLarge ? "md" : size === "2xs" ? "xs" : "sm"}
>
<div
className={cn(

View File

@@ -93,12 +93,7 @@ function SidebarTab({
type="button"
group="group/SidebarTab"
>
<Interactive.Container
roundingVariant="sm"
heightVariant="lg"
widthVariant="full"
type={type}
>
<Interactive.Container rounding="sm" size="lg" width="full" type={type}>
{href && !disabled && (
<Link
href={href as Route}
@@ -120,8 +115,8 @@ function SidebarTab({
title={folded ? "" : children}
sizePreset="main-ui"
variant="body"
widthVariant="full"
paddingVariant="fit"
width="full"
padding="fit"
rightChildren={truncationSpacer}
/>
) : (

View File

@@ -55,7 +55,7 @@ export const PaddingVariants: Story = {
<div className="flex flex-col gap-4 w-96">
{PADDING_VARIANTS.map((padding) => (
<Card key={padding} padding={padding} border="solid">
<p>paddingVariant: {padding}</p>
<p>padding: {padding}</p>
</Card>
))}
</div>
@@ -67,7 +67,7 @@ export const RoundingVariants: Story = {
<div className="flex flex-col gap-4 w-96">
{ROUNDING_VARIANTS.map((rounding) => (
<Card key={rounding} rounding={rounding} border="solid">
<p>roundingVariant: {rounding}</p>
<p>rounding: {rounding}</p>
</Card>
))}
</div>
@@ -79,7 +79,7 @@ export const AllCombinations: Story = {
<div className="flex flex-col gap-8">
{PADDING_VARIANTS.map((padding) => (
<div key={padding}>
<p className="font-bold pb-2">paddingVariant: {padding}</p>
<p className="font-bold pb-2">padding: {padding}</p>
<div className="grid grid-cols-3 gap-4">
{BACKGROUND_VARIANTS.map((bg) =>
BORDER_VARIANTS.map((border) => (

View File

@@ -156,7 +156,7 @@ function MessageCard({
description={description}
sizePreset="main-ui"
variant="section"
paddingVariant="md"
padding="md"
rightChildren={right}
/>

View File

@@ -209,7 +209,7 @@ export const PaddingVariants: Story = {
sizePreset="main-ui"
variant="section"
icon={SvgGlobe}
title={`paddingVariant: ${padding}`}
title={`padding: ${padding}`}
description="Shows padding differences."
/>
</SelectCard>
@@ -227,7 +227,7 @@ export const RoundingVariants: Story = {
sizePreset="main-ui"
variant="section"
icon={SvgGlobe}
title={`roundingVariant: ${rounding}`}
title={`rounding: ${rounding}`}
description="Shows rounding differences."
/>
</SelectCard>

View File

@@ -167,11 +167,7 @@ function FoldableDivider({
interaction={isOpen ? "hover" : "rest"}
onClick={toggle}
>
<Interactive.Container
roundingVariant="sm"
heightVariant="fit"
widthVariant="full"
>
<Interactive.Container rounding="sm" size="fit" width="full">
<div className="opal-divider">
<div className="opal-divider-row">
<div className="opal-divider-title">

View File

@@ -22,7 +22,7 @@ interface TableProps
/** Row selection behavior. @default "no-select" */
selectionBehavior?: SelectionBehavior;
/** Height behavior. `"fit"` = shrink to content, `"full"` = fill available space. */
heightVariant?: ExtremaSizeVariants;
size?: ExtremaSizeVariants;
/** Explicit pixel width for the table (e.g. from `table.getTotalSize()`).
* When provided the table uses exactly this width instead of stretching
* to fill its container, which prevents `table-layout: fixed` from
@@ -38,7 +38,7 @@ function Table({
ref,
variant = "cards",
selectionBehavior = "no-select",
heightVariant,
size: heightVariant,
width,
...props
}: TableProps) {

View File

@@ -17,7 +17,7 @@ interface HoverableRootProps
children: React.ReactNode;
group: string;
/** Width preset. @default "auto" */
widthVariant?: ExtremaSizeVariants;
width?: ExtremaSizeVariants;
/**
* JS-controllable interaction state override.
*
@@ -70,7 +70,7 @@ interface HoverableItemProps
function HoverableRoot({
group,
children,
widthVariant = "full",
width = "full",
interaction = "rest",
ref,
...props
@@ -79,7 +79,7 @@ function HoverableRoot({
<div
{...props}
ref={ref}
className={cn(widthVariants[widthVariant])}
className={cn(widthVariants[width])}
data-hover-group={group}
data-interaction={interaction !== "rest" ? interaction : undefined}
>

View File

@@ -135,7 +135,7 @@ export const VariantMatrix: StoryObj = {
),
};
/** All heightVariant sizes (lg, md, sm, xs, 2xs, fit). */
/** All size presets (lg, md, sm, xs, 2xs, fit). */
export const Sizes: StoryObj = {
render: () => (
<div style={{ display: "flex", alignItems: "center", gap: "0.75rem" }}>
@@ -146,7 +146,7 @@ export const Sizes: StoryObj = {
prominence="secondary"
onClick={() => {}}
>
<Interactive.Container border heightVariant={size}>
<Interactive.Container border size={size}>
<span>{size}</span>
</Interactive.Container>
</Interactive.Stateless>
@@ -155,7 +155,7 @@ export const Sizes: StoryObj = {
),
};
/** Container with widthVariant="full" stretching to fill its parent. */
/** Container with width="full" stretching to fill its parent. */
export const WidthFull: StoryObj = {
render: () => (
<div style={{ width: 400 }}>
@@ -164,7 +164,7 @@ export const WidthFull: StoryObj = {
prominence="secondary"
onClick={() => {}}
>
<Interactive.Container border widthVariant="full">
<Interactive.Container border width="full">
<span>Full width container</span>
</Interactive.Container>
</Interactive.Stateless>
@@ -183,7 +183,7 @@ export const Rounding: StoryObj = {
prominence="secondary"
onClick={() => {}}
>
<Interactive.Container border roundingVariant={rounding}>
<Interactive.Container border rounding={rounding}>
<span>{rounding}</span>
</Interactive.Container>
</Interactive.Stateless>

View File

@@ -8,9 +8,9 @@ Structural container shared by both `Interactive.Stateless` and `Interactive.Sta
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `heightVariant` | `SizeVariant` | `"lg"` | Height preset (`2xs``lg`, `fit`) |
| `roundingVariant` | `"md" \| "sm" \| "xs"` | `"md"` | Border-radius preset |
| `widthVariant` | `WidthVariant` | — | Width preset (`"auto"`, `"fit"`, `"full"`) |
| `size` | `SizeVariant` | `"lg"` | Height preset (`2xs``lg`, `fit`) |
| `rounding` | `"md" \| "sm" \| "xs"` | `"md"` | Border-radius preset |
| `width` | `WidthVariant` | — | Width preset (`"auto"`, `"fit"`, `"full"`) |
| `border` | `boolean` | `false` | Renders a 1px border |
| `type` | `"submit" \| "button" \| "reset"` | — | When set, renders a `<button>` element |
@@ -18,7 +18,7 @@ Structural container shared by both `Interactive.Stateless` and `Interactive.Sta
```tsx
<Interactive.Stateless variant="default" prominence="primary">
<Interactive.Container heightVariant="sm" roundingVariant="sm" border>
<Interactive.Container size="sm" rounding="sm" border>
<span>Content</span>
</Interactive.Container>
</Interactive.Stateless>

View File

@@ -63,21 +63,21 @@ interface InteractiveContainerProps
*
* @default "default"
*/
roundingVariant?: InteractiveContainerRoundingVariant;
rounding?: InteractiveContainerRoundingVariant;
/**
* Size preset controlling the container's height, min-width, and padding.
*
* @default "lg"
*/
heightVariant?: ContainerSizeVariants;
size?: ContainerSizeVariants;
/**
* Width preset controlling the container's horizontal size.
*
* @default "fit"
*/
widthVariant?: ExtremaSizeVariants;
width?: ExtremaSizeVariants;
}
// ---------------------------------------------------------------------------
@@ -96,9 +96,9 @@ function InteractiveContainer({
ref,
type,
border,
roundingVariant = "md",
heightVariant = "lg",
widthVariant = "fit",
rounding = "md",
size = "lg",
width = "fit",
...props
}: InteractiveContainerProps) {
const {
@@ -115,16 +115,16 @@ function InteractiveContainer({
target?: string;
rel?: string;
};
const { height, minWidth, padding } = containerSizeVariants[heightVariant];
const { height, minWidth, padding } = containerSizeVariants[size];
const sharedProps = {
...rest,
className: cn(
"interactive-container",
interactiveContainerRoundingVariants[roundingVariant],
interactiveContainerRoundingVariants[rounding],
height,
minWidth,
padding,
widthVariants[widthVariant],
widthVariants[width],
slotClassName
),
"data-border": border ? ("true" as const) : undefined,

View File

@@ -46,7 +46,7 @@ import SvgNoResult from "@opal/illustrations/no-result";
description="Some description"
sizePreset="main-content"
variant="section"
paddingVariant="lg"
padding="lg"
rightChildren={
<Button icon={SvgSettings} prominence="tertiary" />
}

View File

@@ -63,7 +63,7 @@ export const NoPadding: Story = {
variant: "section",
title: "Compact Row",
description: "No padding around content area.",
paddingVariant: "fit",
padding: "fit",
rightChildren: <Button prominence="tertiary">Action</Button>,
},
};

View File

@@ -15,9 +15,9 @@ Inherits **all** props from [`Content`](../content/README.md) (same discriminate
| Prop | Type | Default | Description |
|---|---|---|---|
| `rightChildren` | `ReactNode` | `undefined` | Content rendered on the right side. Wrapper stretches to the full height of the row. |
| `paddingVariant` | `SizeVariant` | `"lg"` | Padding preset applied around the `Content` area. Uses the shared size scale from `@opal/shared`. |
| `padding` | `SizeVariant` | `"lg"` | Padding preset applied around the `Content` area. Uses the shared size scale from `@opal/shared`. |
### `paddingVariant` reference
### `padding` reference
| Value | Padding class | Effective padding |
|---|---|---|
@@ -37,7 +37,7 @@ These values are identical to the padding applied by `Interactive.Container` at
```
- The outer wrapper is `flex flex-row items-stretch w-full`.
- `Content` sits inside a `flex-1 min-w-0` div with padding from `paddingVariant`.
- `Content` sits inside a `flex-1 min-w-0` div with padding from `padding`.
- `rightChildren` is wrapped in `flex items-stretch shrink-0` so it stretches vertically.
## Usage Examples
@@ -56,7 +56,7 @@ import SvgSettings from "@opal/icons/settings";
sizePreset="main-content"
variant="section"
tag={{ title: "Default", color: "blue" }}
paddingVariant="lg"
padding="lg"
rightChildren={
<Button icon={SvgSettings} prominence="tertiary" onClick={handleEdit} />
}
@@ -76,7 +76,7 @@ import { SvgArrowExchange, SvgCloud } from "@opal/icons";
description="Gemini"
sizePreset="main-content"
variant="section"
paddingVariant="md"
padding="md"
rightChildren={
<Button rightIcon={SvgArrowExchange} prominence="tertiary">
Connect
@@ -92,7 +92,7 @@ import { SvgArrowExchange, SvgCloud } from "@opal/icons";
title="Section Header"
sizePreset="main-content"
variant="section"
paddingVariant="lg"
padding="lg"
/>
```

View File

@@ -20,7 +20,7 @@ type ContentActionProps = ContentProps & {
* @default "lg"
* @see {@link ContainerSizeVariants} for the full list of presets.
*/
paddingVariant?: ContainerSizeVariants;
padding?: ContainerSizeVariants;
};
// ---------------------------------------------------------------------------
@@ -31,7 +31,7 @@ type ContentActionProps = ContentProps & {
* A row layout that pairs a {@link Content} block with optional right-side
* action children (e.g. buttons, badges).
*
* The `Content` area receives padding controlled by `paddingVariant`, using
* The `Content` area receives padding controlled by `padding`, using
* the same size scale as `Interactive.Container` and `Button`. The
* `rightChildren` wrapper stretches to the full height of the row.
*
@@ -47,21 +47,21 @@ type ContentActionProps = ContentProps & {
* description="GPT"
* sizePreset="main-content"
* variant="section"
* paddingVariant="lg"
* padding="lg"
* rightChildren={<Button icon={SvgSettings} prominence="tertiary" />}
* />
* ```
*/
function ContentAction({
rightChildren,
paddingVariant = "lg",
padding = "lg",
...contentProps
}: ContentActionProps) {
const { padding } = containerSizeVariants[paddingVariant];
const { padding: paddingClass } = containerSizeVariants[padding];
return (
<div className="flex flex-row items-stretch w-full">
<div className={cn("flex-1 min-w-0 self-center", padding)}>
<div className={cn("flex-1 min-w-0 self-center", paddingClass)}>
<Content {...contentProps} />
</div>
{rightChildren && (

View File

@@ -184,7 +184,7 @@ export const SmMuted: Story = {
};
// ---------------------------------------------------------------------------
// widthVariant: full
// width: full
// ---------------------------------------------------------------------------
export const WidthFull: Story = {
@@ -192,7 +192,7 @@ export const WidthFull: Story = {
sizePreset: "main-content",
variant: "section",
title: "Full Width Content",
widthVariant: "full",
width: "full",
},
decorators: [
(Story) => (

View File

@@ -58,9 +58,20 @@ interface ContentBaseProps {
* - `"fit"` — Shrink-wraps to content width
* - `"full"` — Stretches to fill the parent's width
*
* @default "fit"
* @default "full"
*/
widthVariant?: ExtremaSizeVariants;
width?: ExtremaSizeVariants;
/**
* Opt out of the automatic interactive color override.
*
* When `Content` is nested inside an `.interactive` element, its title and
* icon colors are normally overridden by the interactive foreground palette.
* Set this to `true` to keep Content's own colors regardless of context.
*
* @default false
*/
nonInteractive?: boolean;
/** Ref forwarded to the root `<div>` of the resolved layout. */
ref?: React.Ref<HTMLDivElement>;
@@ -126,7 +137,8 @@ function Content(props: ContentProps) {
const {
sizePreset = "headline",
variant = "heading",
widthVariant = "full",
width = "full",
nonInteractive,
ref,
...rest
} = props;
@@ -186,7 +198,14 @@ function Content(props: ContentProps) {
`Content: no layout matched for sizePreset="${sizePreset}" variant="${variant}"`
);
return <div className={widthVariants[widthVariant]}>{layout}</div>;
return (
<div
className={widthVariants[width]}
data-opal-non-interactive={nonInteractive || undefined}
>
{layout}
</div>
);
}
// ---------------------------------------------------------------------------

View File

@@ -352,36 +352,52 @@
the title inherits color from the Interactive's `--interactive-foreground`
and icons switch to `--interactive-foreground-icon`. This is automatic —
no opt-in prop is required.
Opt-out: set `nonInteractive` on <Content> to add
`data-opal-non-interactive` to the wrapper div, which excludes
the element from these overrides via the `:not(…) >` selector.
=========================================================================== */
.interactive .opal-content-xl {
.interactive :not([data-opal-non-interactive]) > .opal-content-xl {
color: inherit;
}
.interactive .opal-content-xl .opal-content-xl-icon {
.interactive
:not([data-opal-non-interactive])
> .opal-content-xl
.opal-content-xl-icon {
color: var(--interactive-foreground-icon);
}
.interactive .opal-content-lg {
.interactive :not([data-opal-non-interactive]) > .opal-content-lg {
color: inherit;
}
.interactive .opal-content-lg .opal-content-lg-icon {
.interactive
:not([data-opal-non-interactive])
> .opal-content-lg
.opal-content-lg-icon {
color: var(--interactive-foreground-icon);
}
.interactive .opal-content-md {
.interactive :not([data-opal-non-interactive]) > .opal-content-md {
color: inherit;
}
.interactive .opal-content-md .opal-content-md-icon {
.interactive
:not([data-opal-non-interactive])
> .opal-content-md
.opal-content-md-icon {
color: var(--interactive-foreground-icon);
}
.interactive .opal-content-sm {
.interactive :not([data-opal-non-interactive]) > .opal-content-sm {
color: inherit;
}
.interactive .opal-content-sm .opal-content-sm-icon {
.interactive
:not([data-opal-non-interactive])
> .opal-content-sm
.opal-content-sm-icon {
color: var(--interactive-foreground-icon);
}

View File

@@ -152,7 +152,7 @@ function Horizontal({
tag={tag}
sizePreset="main-ui"
variant="section"
widthVariant="full"
width="full"
/>
</div>
<div className="flex flex-col items-end">{children}</div>

View File

@@ -57,9 +57,9 @@ const containerSizeVariants: Record<
// A named scale of width/height presets that map to Tailwind width/height utility classes.
//
// Consumers (for width):
// - Interactive.Container (widthVariant)
// - Interactive.Container (width)
// - Button (width)
// - Content (widthVariant)
// - Content (width)
// ---------------------------------------------------------------------------
/**
@@ -96,8 +96,8 @@ const heightVariants: Record<ExtremaSizeVariants, string> = {
// Shared padding and rounding scales for card components (Card, SelectCard).
//
// Consumers:
// - Card (paddingVariant, roundingVariant)
// - SelectCard (paddingVariant, roundingVariant)
// - Card (padding, rounding)
// - SelectCard (padding, rounding)
// ---------------------------------------------------------------------------
const paddingVariants: Record<PaddingVariants, string> = {

View File

@@ -40,7 +40,7 @@ export default function ScimSyncCard({
description="Connect your identity provider to import and sync users and groups."
sizePreset="main-ui"
variant="section"
paddingVariant="fit"
padding="fit"
rightChildren={
hasToken ? (
<Button

View File

@@ -77,7 +77,7 @@ export const InMessageImage = memo(function InMessageImage({
onOpenChange={(open) => setFullImageShowing(open)}
/>
<Hoverable.Root group="messageImage" widthVariant="fit">
<Hoverable.Root group="messageImage" width="fit">
<div className={cn("relative", shapeContainerClasses)}>
{!imageLoaded && (
<div className="absolute inset-0 bg-background-tint-02 animate-pulse rounded-lg" />

View File

@@ -133,7 +133,7 @@ export default function ProjectContextPanel({
<div className="flex flex-col gap-6 w-full max-w-[var(--app-page-main-content-width)] mx-auto p-4 pt-14 pb-6">
<div className="flex flex-col gap-1 text-text-04">
<SvgFolderOpen className="h-8 w-8 text-text-04" />
<Hoverable.Root group="projectName" widthVariant="fit">
<Hoverable.Root group="projectName" width="fit">
<div className="flex items-center gap-2">
{isEditingName ? (
<ButtonRenaming

View File

@@ -198,7 +198,7 @@ const HumanMessage = React.memo(function HumanMessage({
);
return (
<Hoverable.Root group="humanMessage" widthVariant="full">
<Hoverable.Root group="humanMessage" width="full">
<div
id="onyx-human-message"
className="flex flex-col justify-end w-full relative"

View File

@@ -97,7 +97,7 @@ export default function MultiModelPanel({
<ContentAction
sizePreset="main-ui"
variant="body"
paddingVariant="lg"
padding="lg"
icon={ModelIcon}
title={isHidden ? markdown(`~~${displayName}~~`) : displayName}
rightChildren={

View File

@@ -71,7 +71,7 @@ function MemoryTagWithTooltip({
icon={SvgAddLines}
title={operationLabel}
sizePreset="secondary"
paddingVariant="sm"
padding="sm"
variant="body"
prominence="muted"
rightChildren={

View File

@@ -138,7 +138,7 @@ export default function ShareButton({
description={opt.description}
sizePreset="main-ui"
variant="section"
paddingVariant="sm"
padding="sm"
/>
</div>
))}

View File

@@ -272,7 +272,7 @@ export default function HookFormModal({
<ContentAction
sizePreset="main-ui"
variant="section"
paddingVariant="fit"
padding="fit"
title={hookPointDisplayName}
description={hookPointDescription}
rightChildren={
@@ -283,7 +283,7 @@ export default function HookFormModal({
icon={SvgShareWebhook}
title="Hook Point"
prominence="muted"
widthVariant="fit"
width="fit"
/>
{docsUrl && (
<LinkButton href={docsUrl} target="_blank">

View File

@@ -56,7 +56,7 @@ export default function SearchCard({
return (
<Interactive.Stateless onClick={handleClick} prominence="secondary">
<Interactive.Container heightVariant="fit" widthVariant="full">
<Interactive.Container size="fit" width="full">
<Section alignItems="start" gap={0} padding={0.25}>
{/* Title Row */}
<Section

View File

@@ -202,7 +202,7 @@ function AttachmentItemLayout({
description={description}
sizePreset="main-ui"
variant="section"
widthVariant="full"
width="full"
/>
</div>
{middleText && (

View File

@@ -173,7 +173,7 @@ export default function InputImage({
const dropzoneProps = onDrop ? getRootProps() : {};
return (
<Hoverable.Root group="inputImage" widthVariant="fit">
<Hoverable.Root group="inputImage" width="fit">
<div
className={cn("relative", className)}
style={{ width: size, height: size }}

View File

@@ -78,7 +78,7 @@ export default function FileTile({
const isMuted = state === "processing" || state === "disabled";
return (
<Hoverable.Root group="fileTile" widthVariant="fit">
<Hoverable.Root group="fileTile" width="fit">
<div
onClick={onOpen && state !== "disabled" ? () => onOpen() : undefined}
className={cn(

View File

@@ -211,7 +211,7 @@ function AgentIconEditor({ existingAgent }: AgentIconEditorProps) {
<Popover open={popoverOpen} onOpenChange={setPopoverOpen}>
<Popover.Trigger asChild>
<Hoverable.Root group="inputAvatar" widthVariant="fit">
<Hoverable.Root group="inputAvatar" width="fit">
<InputAvatar className="relative flex flex-col items-center justify-center h-[7.5rem] w-[7.5rem]">
{/* We take the `InputAvatar`'s height/width (in REM) and multiply it by 16 (the REM -> px conversion factor). */}
<CustomAgentAvatar
@@ -288,7 +288,7 @@ function OpenApiToolCard({ tool }: OpenApiToolCardProps) {
description={tool.description}
sizePreset="main-ui"
variant="section"
paddingVariant="fit"
padding="fit"
/>
}
topRightChildren={<SwitchField name={toolFieldName} />}
@@ -353,7 +353,7 @@ function MCPServerCard({
description={tool.description}
sizePreset="main-ui"
variant="section"
paddingVariant="fit"
padding="fit"
/>
}
topRightChildren={
@@ -388,7 +388,7 @@ function MCPServerCard({
description={server.description}
sizePreset="main-ui"
variant="section"
paddingVariant="fit"
padding="fit"
rightChildren={
<GeneralLayouts.Section
flexDirection="row"

View File

@@ -269,7 +269,7 @@ function GeneralSettings() {
title="Profile"
sizePreset="main-content"
variant="section"
widthVariant="full"
width="full"
/>
<Card>
<InputHorizontal
@@ -332,7 +332,7 @@ function GeneralSettings() {
title="Appearance"
sizePreset="main-content"
variant="section"
widthVariant="full"
width="full"
/>
<Card>
<InputHorizontal
@@ -443,7 +443,7 @@ function GeneralSettings() {
title="Danger Zone"
sizePreset="main-content"
variant="section"
widthVariant="full"
width="full"
/>
<Card>
<InputHorizontal
@@ -831,7 +831,7 @@ function ChatPreferencesSettings() {
title="Chats"
sizePreset="main-content"
variant="section"
widthVariant="full"
width="full"
/>
<Card>
<InputHorizontal
@@ -920,7 +920,7 @@ function ChatPreferencesSettings() {
title="Memory"
sizePreset="main-content"
variant="section"
widthVariant="full"
width="full"
/>
<Card>
<InputHorizontal
@@ -968,7 +968,7 @@ function ChatPreferencesSettings() {
title="Prompt Shortcuts"
sizePreset="main-content"
variant="section"
widthVariant="full"
width="full"
/>
<Card>
<InputHorizontal
@@ -993,7 +993,7 @@ function ChatPreferencesSettings() {
title="Voice"
sizePreset="main-content"
variant="section"
widthVariant="full"
width="full"
/>
<Card>
<InputHorizontal
@@ -1365,7 +1365,7 @@ function AccountsAccessSettings() {
title="Accounts"
sizePreset="main-content"
variant="section"
widthVariant="full"
width="full"
/>
<Card>
<InputHorizontal
@@ -1401,7 +1401,7 @@ function AccountsAccessSettings() {
title="Access Tokens"
sizePreset="main-content"
variant="section"
widthVariant="full"
width="full"
/>
{canCreateTokens ? (
<Card padding={0.25}>
@@ -1463,8 +1463,8 @@ function AccountsAccessSettings() {
return (
<Interactive.Container
key={pat.id}
heightVariant="fit"
widthVariant="full"
size="fit"
width="full"
>
<div className="w-full bg-background-tint-01">
<AttachmentItemLayout
@@ -1605,7 +1605,7 @@ function FederatedConnectorCard({
}
sizePreset="main-content"
variant="section"
paddingVariant="sm"
padding="sm"
rightChildren={
connector.has_oauth_token ? (
<Button
@@ -1678,7 +1678,7 @@ function ConnectorsSettings() {
title="Connectors"
sizePreset="main-content"
variant="section"
widthVariant="full"
width="full"
/>
{hasConnectors ? (
<>

View File

@@ -21,7 +21,6 @@ import {
SvgExpand,
SvgFold,
SvgExternalLink,
SvgAlertCircle,
SvgRefreshCw,
} from "@opal/icons";
import { ADMIN_ROUTES } from "@/lib/admin-routes";
@@ -49,7 +48,7 @@ import {
PYTHON_TOOL_ID,
OPEN_URL_TOOL_ID,
} from "@/app/app/components/tools/constants";
import { Button, Divider, Text, Card } from "@opal/components";
import { Button, Divider, Text, Card, MessageCard } from "@opal/components";
import Modal from "@/refresh-components/Modal";
import Switch from "@/refresh-components/inputs/Switch";
import useMcpServersForAgentEditor from "@/hooks/useMcpServersForAgentEditor";
@@ -157,7 +156,7 @@ function MCPServerCard({
description={server.description}
sizePreset="main-ui"
variant="section"
paddingVariant="fit"
padding="fit"
rightChildren={
<Tooltip tooltip={authTooltip} side="top">
<Switch
@@ -287,7 +286,7 @@ function NumericLimitField({
};
return (
<Hoverable.Root group="numericLimit" widthVariant="full">
<Hoverable.Root group="numericLimit" width="full">
<InputTypeIn
inputMode="numeric"
showClearButton={false}
@@ -1091,14 +1090,11 @@ export default function ChatPreferencesPage() {
)}
</Text>
</Section>
<Card background="none" border="solid" padding="sm">
<Content
sizePreset="main-ui"
icon={SvgAlertCircle}
title="Modify with caution."
description="System prompt affects all chats, agents, and projects. Significant changes may degrade response quality."
/>
</Card>
<MessageCard
title="Modify with caution."
description="System prompt affects all chats, agents, and projects. Significant changes may degrade response quality."
padding="xs"
/>
</Modal.Body>
<Modal.Footer>
<Button

View File

@@ -294,7 +294,7 @@ export default function EditUserModal({
description="This controls their general permissions."
sizePreset="main-ui"
variant="section"
paddingVariant="fit"
padding="fit"
rightChildren={
<InputSelect
value={selectedRole}

View File

@@ -103,7 +103,7 @@ export default function UserRoleCell({ user, onMutate }: UserRoleCellProps) {
variant="select-tinted"
width="full"
justifyContent="between"
roundingVariant="sm"
rounding="sm"
>
{USER_ROLE_LABELS[user.role]}
</OpenButton>

View File

@@ -23,7 +23,7 @@ function StatCell({ value, label, onFilter }: StatCellProps) {
const display = value === null ? "\u2014" : value.toLocaleString();
return (
<Hoverable.Root group="stat" widthVariant="full">
<Hoverable.Root group="stat" width="full">
<div
className={`relative flex flex-col items-start gap-0.5 w-full p-2 rounded-08 transition-colors ${
onFilter ? "cursor-pointer hover:bg-background-tint-02" : ""
@@ -70,7 +70,7 @@ function ScimCard() {
description="Users are synced from your identity provider."
sizePreset="main-ui"
variant="section"
paddingVariant="fit"
padding="fit"
rightChildren={
<Link href={ADMIN_ROUTES.SCIM.path}>
<Button prominence="tertiary" rightIcon={SvgArrowUpRight} size="sm">

View File

@@ -36,16 +36,12 @@ export default function Suggestions({ onSubmit }: SuggestionsProps) {
prominence="tertiary"
onClick={() => handleSuggestionClick(message)}
>
<Interactive.Container
widthVariant="full"
roundingVariant="sm"
heightVariant="lg"
>
<Interactive.Container width="full" rounding="sm" size="lg">
<Content
title={message}
sizePreset="main-ui"
variant="body"
widthVariant="full"
width="full"
prominence="muted"
/>
</Interactive.Container>

View File

@@ -241,7 +241,7 @@ function FormContent({
`Specify an OpenAPI schema that defines the APIs you want to make available as part of this action. Learn more about [OpenAPI actions](${DOCS_ADMINS_PATH}/actions/openapi).`
)}
>
<Hoverable.Root group="definitionField" widthVariant="full">
<Hoverable.Root group="definitionField" width="full">
<div className="relative w-full">
{values.definition.trim() && (
<div className="absolute z-[100000] top-2 right-2 bg-background-tint-00">

View File

@@ -74,7 +74,7 @@ export default function AdminListHeader({
sizePreset="main-ui"
variant="body"
prominence="muted"
widthVariant="fit"
width="fit"
/>
{actionButton}
</div>

View File

@@ -38,7 +38,7 @@ export default function DocumentSetCard({
<Interactive.Container
data-testid={`document-set-card-${documentSet.id}`}
border
heightVariant="fit"
size="fit"
>
<AttachmentItemLayout
icon={SvgFiles}

View File

@@ -21,7 +21,7 @@ function Removable({ onRemove, children }: RemovableProps) {
}
return (
<Hoverable.Root group="fileCard" widthVariant="fit">
<Hoverable.Root group="fileCard" width="fit">
<div className="relative">
<div
className={cn(
@@ -184,7 +184,7 @@ export function FileCard({
}
>
<div className="min-w-0 max-w-[12rem]">
<Interactive.Container border heightVariant="fit" widthVariant="full">
<Interactive.Container border size="fit" width="full">
<AttachmentItemLayout
icon={isProcessing ? SimpleLoader : SvgFileText}
title={file.name}

View File

@@ -88,7 +88,7 @@ function ViewerMCPServerCard({ server, tools }: ViewerMCPServerCardProps) {
description={server.description}
sizePreset="main-ui"
variant="section"
paddingVariant="fit"
padding="fit"
/>
}
topRightChildren={
@@ -267,7 +267,7 @@ export default function AgentViewerModal({ agent }: AgentViewerModalProps) {
title="Featured"
sizePreset="main-ui"
variant="body"
widthVariant="fit"
width="fit"
/>
)}
<Content
@@ -276,7 +276,7 @@ export default function AgentViewerModal({ agent }: AgentViewerModalProps) {
sizePreset="main-ui"
variant="body"
prominence="muted"
widthVariant="fit"
width="fit"
/>
{agent.is_public && (
<Content
@@ -285,7 +285,7 @@ export default function AgentViewerModal({ agent }: AgentViewerModalProps) {
sizePreset="main-ui"
variant="body"
prominence="muted"
widthVariant="fit"
width="fit"
/>
)}
</Section>
@@ -425,7 +425,7 @@ export default function AgentViewerModal({ agent }: AgentViewerModalProps) {
sizePreset="main-ui"
variant="body"
prominence="muted"
widthVariant="full"
width="full"
/>
</Interactive.Container>
</Interactive.Stateless>

View File

@@ -28,15 +28,9 @@ import {
ModalWrapper,
} from "@/sections/modals/llmConfig/shared";
import { fetchBedrockModels } from "@/lib/llmConfig/svc";
import { Card } from "@opal/components";
import { Card, MessageCard } from "@opal/components";
import { Section } from "@/layouts/general-layouts";
import { SvgAlertCircle } from "@opal/icons";
import {
Content,
InputDivider,
InputPadder,
InputVertical,
} from "@opal/layouts";
import { InputDivider, InputPadder, InputVertical } from "@opal/layouts";
import { toast } from "@/hooks/useToast";
import { refreshLlmProviderCaches } from "@/lib/llmConfig/cache";
@@ -218,14 +212,10 @@ function BedrockModalInternals({
{authMethod === AUTH_METHOD_IAM && (
<InputPadder>
<Card background="none" border="solid" padding="sm">
<Content
icon={SvgAlertCircle}
title="Onyx will use the IAM role attached to the environment its running in to authenticate."
variant="body"
sizePreset="main-ui"
/>
</Card>
<MessageCard
variant="info"
title="Onyx will use the IAM role attached to the environment its running in to authenticate."
/>
</InputPadder>
)}

View File

@@ -409,7 +409,7 @@ export default function CustomModal({
description={markdown(
"Add extra properties as needed by the model provider. These are passed to LiteLLM's `completion()` call as [environment variables](https://docs.litellm.ai/docs/set_keys#environment-variables). See [documentation](https://docs.onyx.app/admins/ai_models/custom_inference_provider) for more instructions."
)}
widthVariant="full"
width="full"
variant="section"
sizePreset="main-content"
/>
@@ -433,7 +433,7 @@ export default function CustomModal({
description="List LLM models you wish to use and their configurations for this provider. See full list of models at LiteLLM."
variant="section"
sizePreset="main-content"
widthVariant="full"
width="full"
/>
</InputPadder>

View File

@@ -276,7 +276,7 @@ export function ModelAccessField() {
Always shared
</Text>
}
paddingVariant="fit"
padding="fit"
/>
</Card>
{selectedGroupIds.length > 0 && (
@@ -304,7 +304,7 @@ export function ModelAccessField() {
type="button"
/>
}
paddingVariant="fit"
padding="fit"
/>
</Card>
</div>
@@ -341,7 +341,7 @@ export function ModelAccessField() {
type="button"
/>
}
paddingVariant="fit"
padding="fit"
/>
</Card>
</div>
@@ -548,7 +548,7 @@ export function ModelSelectionField({
prominence="tertiary"
onClick={() => setIsExpanded(!isExpanded)}
>
<Interactive.Container type="button" widthVariant="full">
<Interactive.Container type="button" width="full">
<Content
sizePreset="secondary"
variant="body"

View File

@@ -74,7 +74,7 @@ export default function NonAdminStep() {
sizePreset="main-ui"
variant="body"
prominence="muted"
paddingVariant="fit"
padding="fit"
rightChildren={
<Button
prominence="tertiary"
@@ -99,7 +99,7 @@ export default function NonAdminStep() {
description="We will display this name in the app."
sizePreset="main-ui"
variant="section"
paddingVariant="fit"
padding="fit"
rightChildren={
<div className="flex items-center justify-end gap-2">
<InputTypeIn
@@ -125,7 +125,7 @@ export default function NonAdminStep() {
/>
</div>
) : (
<Hoverable.Root group="nonAdminName" widthVariant="full">
<Hoverable.Root group="nonAdminName" width="full">
<div
className={containerClasses}
aria-label="Edit display name"

View File

@@ -48,7 +48,7 @@ const OnboardingHeader = React.memo(
sizePreset="main-ui"
variant="body"
prominence="muted"
paddingVariant="sm"
padding="sm"
rightChildren={
stepButtonText ? (
<Section flexDirection="row">

View File

@@ -30,7 +30,7 @@ const FinalStepItem = React.memo(
description={description}
sizePreset="main-ui"
variant="section"
paddingVariant="sm"
padding="sm"
rightChildren={
<Link href={buttonHref as Route} {...linkProps}>
<Button prominence="tertiary" rightIcon={SvgExternalLink}>

View File

@@ -156,7 +156,7 @@ const LLMStep = memo(
description="Onyx supports both self-hosted models and popular providers."
sizePreset="main-ui"
variant="section"
paddingVariant="lg"
padding="lg"
rightChildren={
<Button
disabled={disabled}

View File

@@ -52,7 +52,7 @@ const NameStep = React.memo(
description="We will display this name in the app."
sizePreset="main-ui"
variant="section"
paddingVariant="fit"
padding="fit"
rightChildren={
<InputTypeIn
ref={inputRef}
@@ -66,7 +66,7 @@ const NameStep = React.memo(
/>
</div>
) : (
<Hoverable.Root group="nameStep" widthVariant="full">
<Hoverable.Root group="nameStep" width="full">
<div
className={containerClasses}
onClick={() => {