mirror of
https://github.com/onyx-dot-app/onyx.git
synced 2026-02-27 20:55:45 +00:00
Compare commits
4 Commits
refactor/l
...
default_py
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2036847a2d | ||
|
|
9ff1ac862e | ||
|
|
e4179629ae | ||
|
|
4077c20def |
@@ -0,0 +1,69 @@
|
||||
"""add python tool on default
|
||||
|
||||
Revision ID: 57122d037335
|
||||
Revises: c0c937d5c9e5
|
||||
Create Date: 2026-02-27 10:10:40.124925
|
||||
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "57122d037335"
|
||||
down_revision = "c0c937d5c9e5"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
PYTHON_TOOL_NAME = "python"
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
|
||||
# Look up the PythonTool id
|
||||
result = conn.execute(
|
||||
sa.text("SELECT id FROM tool WHERE name = :name"),
|
||||
{"name": PYTHON_TOOL_NAME},
|
||||
).fetchone()
|
||||
|
||||
if not result:
|
||||
return
|
||||
|
||||
tool_id = result[0]
|
||||
|
||||
# Attach to the default persona (id=0) if not already attached
|
||||
conn.execute(
|
||||
sa.text(
|
||||
"""
|
||||
INSERT INTO persona__tool (persona_id, tool_id)
|
||||
VALUES (0, :tool_id)
|
||||
ON CONFLICT DO NOTHING
|
||||
"""
|
||||
),
|
||||
{"tool_id": tool_id},
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
|
||||
result = conn.execute(
|
||||
sa.text("SELECT id FROM tool WHERE name = :name"),
|
||||
{"name": PYTHON_TOOL_NAME},
|
||||
).fetchone()
|
||||
|
||||
if not result:
|
||||
return
|
||||
|
||||
conn.execute(
|
||||
sa.text(
|
||||
"""
|
||||
DELETE FROM persona__tool
|
||||
WHERE persona_id = 0 AND tool_id = :tool_id
|
||||
"""
|
||||
),
|
||||
{"tool_id": result[0]},
|
||||
)
|
||||
@@ -72,6 +72,9 @@ def test_cold_startup_default_assistant() -> None:
|
||||
assert (
|
||||
"read_file" in tool_names
|
||||
), "Default assistant should have FileReaderTool attached"
|
||||
assert (
|
||||
"python" in tool_names
|
||||
), "Default assistant should have PythonTool attached"
|
||||
|
||||
# Also verify by display names for clarity
|
||||
assert (
|
||||
@@ -86,8 +89,11 @@ def test_cold_startup_default_assistant() -> None:
|
||||
assert (
|
||||
"File Reader" in tool_display_names
|
||||
), "Default assistant should have File Reader tool"
|
||||
|
||||
# Should have exactly 5 tools
|
||||
assert (
|
||||
len(tool_associations) == 5
|
||||
), f"Default assistant should have exactly 5 tools attached, got {len(tool_associations)}"
|
||||
"Code Interpreter" in tool_display_names
|
||||
), "Default assistant should have Code Interpreter tool"
|
||||
|
||||
# Should have exactly 6 tools
|
||||
assert (
|
||||
len(tool_associations) == 6
|
||||
), f"Default assistant should have exactly 6 tools attached, got {len(tool_associations)}"
|
||||
|
||||
@@ -51,7 +51,6 @@ func NewRootCommand() *cobra.Command {
|
||||
cmd.AddCommand(NewRunCICommand())
|
||||
cmd.AddCommand(NewScreenshotDiffCommand())
|
||||
cmd.AddCommand(NewWebCommand())
|
||||
cmd.AddCommand(NewWhoisCommand())
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -1,159 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/onyx-dot-app/onyx/tools/ods/internal/kube"
|
||||
)
|
||||
|
||||
var safeIdentifier = regexp.MustCompile(`^[a-zA-Z0-9_\-]+$`)
|
||||
|
||||
// NewWhoisCommand creates the whois command for looking up users/tenants.
|
||||
func NewWhoisCommand() *cobra.Command {
|
||||
var ctx string
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "whois <email-fragment or tenant-id>",
|
||||
Short: "Look up users and admins by email or tenant ID",
|
||||
Long: `Look up tenant and user information from the data plane PostgreSQL database.
|
||||
|
||||
Requires: AWS SSO login, kubectl access to the EKS cluster.
|
||||
|
||||
Two modes (auto-detected):
|
||||
|
||||
Email fragment:
|
||||
ods whois chris
|
||||
→ Searches user_tenant_mapping for emails matching '%chris%'
|
||||
|
||||
Tenant ID:
|
||||
ods whois tenant_abcd1234-...
|
||||
→ Lists all admin emails in that tenant
|
||||
|
||||
Cluster connection is configured via KUBE_CTX_* environment variables.
|
||||
Each variable is a space-separated tuple: "cluster region namespace"
|
||||
|
||||
export KUBE_CTX_DATA_PLANE="<cluster> <region> <namespace>"
|
||||
export KUBE_CTX_CONTROL_PLANE="<cluster> <region> <namespace>"
|
||||
etc...
|
||||
|
||||
Use -c to select which context (default: data_plane).`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
runWhois(args[0], ctx)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&ctx, "context", "c", "data_plane", "cluster context name (maps to KUBE_CTX_<NAME> env var)")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func clusterFromEnv(name string) *kube.Cluster {
|
||||
envKey := "KUBE_CTX_" + strings.ToUpper(name)
|
||||
val := os.Getenv(envKey)
|
||||
if val == "" {
|
||||
log.Fatalf("Environment variable %s is not set.\n\nSet it as a space-separated tuple:\n export %s=\"<cluster> <region> <namespace>\"", envKey, envKey)
|
||||
}
|
||||
|
||||
parts := strings.Fields(val)
|
||||
if len(parts) != 3 {
|
||||
log.Fatalf("%s must be a space-separated tuple of 3 values (cluster region namespace), got: %q", envKey, val)
|
||||
}
|
||||
|
||||
return &kube.Cluster{Name: parts[0], Region: parts[1], Namespace: parts[2]}
|
||||
}
|
||||
|
||||
// queryPod runs a SQL query via pginto on the given pod and returns cleaned output lines.
|
||||
func queryPod(c *kube.Cluster, pod, sql string) []string {
|
||||
raw, err := c.ExecOnPod(pod, "pginto", "-A", "-t", "-F", "\t", "-c", sql)
|
||||
if err != nil {
|
||||
log.Fatalf("Query failed: %v", err)
|
||||
}
|
||||
|
||||
var lines []string
|
||||
for _, line := range strings.Split(strings.TrimSpace(raw), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line != "" && !strings.HasPrefix(line, "Connecting to ") {
|
||||
lines = append(lines, line)
|
||||
}
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
func runWhois(query string, ctx string) {
|
||||
c := clusterFromEnv(ctx)
|
||||
|
||||
if err := c.EnsureContext(); err != nil {
|
||||
log.Fatalf("Failed to ensure cluster context: %v", err)
|
||||
}
|
||||
|
||||
log.Info("Finding api-server pod...")
|
||||
pod, err := c.FindPod("api-server")
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to find api-server pod: %v", err)
|
||||
}
|
||||
log.Debugf("Using pod: %s", pod)
|
||||
|
||||
if strings.HasPrefix(query, "tenant_") {
|
||||
findAdminsByTenant(c, pod, query)
|
||||
} else {
|
||||
findByEmail(c, pod, query)
|
||||
}
|
||||
}
|
||||
|
||||
func findByEmail(c *kube.Cluster, pod, fragment string) {
|
||||
fragment = strings.NewReplacer("'", "", `"`, "", `;`, "", `\`, `\\`, `%`, `\%`, `_`, `\_`).Replace(fragment)
|
||||
|
||||
sql := fmt.Sprintf(
|
||||
`SELECT email, tenant_id, active FROM public.user_tenant_mapping WHERE email LIKE '%%%s%%' ORDER BY email;`,
|
||||
fragment,
|
||||
)
|
||||
|
||||
log.Infof("Searching for emails matching '%%%s%%'...", fragment)
|
||||
lines := queryPod(c, pod, sql)
|
||||
if len(lines) == 0 {
|
||||
fmt.Println("No results found.")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
_, _ = fmt.Fprintln(w, "EMAIL\tTENANT ID\tACTIVE")
|
||||
_, _ = fmt.Fprintln(w, "-----\t---------\t------")
|
||||
for _, line := range lines {
|
||||
_, _ = fmt.Fprintln(w, line)
|
||||
}
|
||||
_ = w.Flush()
|
||||
}
|
||||
|
||||
func findAdminsByTenant(c *kube.Cluster, pod, tenantID string) {
|
||||
if !safeIdentifier.MatchString(tenantID) {
|
||||
log.Fatalf("Invalid tenant ID: %q (must be alphanumeric, hyphens, underscores only)", tenantID)
|
||||
}
|
||||
|
||||
sql := fmt.Sprintf(
|
||||
`SELECT email FROM "%s"."user" WHERE role = 'ADMIN' AND is_active = true AND email NOT LIKE 'api_key__%%' ORDER BY email;`,
|
||||
tenantID,
|
||||
)
|
||||
|
||||
log.Infof("Fetching admin emails for %s...", tenantID)
|
||||
lines := queryPod(c, pod, sql)
|
||||
if len(lines) == 0 {
|
||||
fmt.Println("No admin users found for this tenant.")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println("EMAIL")
|
||||
fmt.Println("-----")
|
||||
for _, line := range lines {
|
||||
fmt.Println(line)
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
package kube
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Cluster holds the connection info for a Kubernetes cluster.
|
||||
type Cluster struct {
|
||||
Name string
|
||||
Region string
|
||||
Namespace string
|
||||
}
|
||||
|
||||
// EnsureContext makes sure the cluster exists in kubeconfig, calling
|
||||
// aws eks update-kubeconfig only if the context is missing.
|
||||
func (c *Cluster) EnsureContext() error {
|
||||
// Check if context already exists in kubeconfig
|
||||
cmd := exec.Command("kubectl", "config", "get-contexts", c.Name, "--no-headers")
|
||||
if err := cmd.Run(); err == nil {
|
||||
log.Debugf("Context %s already exists, skipping aws eks update-kubeconfig", c.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Infof("Context %s not found, fetching kubeconfig from AWS...", c.Name)
|
||||
cmd = exec.Command("aws", "eks", "update-kubeconfig", "--region", c.Region, "--name", c.Name, "--alias", c.Name)
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("aws eks update-kubeconfig failed: %w\n%s", err, string(out))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// kubectlArgs returns common kubectl flags to target this cluster without mutating global context.
|
||||
func (c *Cluster) kubectlArgs() []string {
|
||||
return []string{"--context", c.Name, "--namespace", c.Namespace}
|
||||
}
|
||||
|
||||
// FindPod returns the name of the first Running/Ready pod matching the given substring.
|
||||
func (c *Cluster) FindPod(substring string) (string, error) {
|
||||
args := append(c.kubectlArgs(), "get", "po",
|
||||
"--field-selector", "status.phase=Running",
|
||||
"--no-headers",
|
||||
"-o", "custom-columns=NAME:.metadata.name,READY:.status.conditions[?(@.type=='Ready')].status",
|
||||
)
|
||||
cmd := exec.Command("kubectl", args...)
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||
return "", fmt.Errorf("kubectl get po failed: %w\n%s", err, string(exitErr.Stderr))
|
||||
}
|
||||
return "", fmt.Errorf("kubectl get po failed: %w", err)
|
||||
}
|
||||
|
||||
for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 2 {
|
||||
continue
|
||||
}
|
||||
name, ready := fields[0], fields[1]
|
||||
if strings.Contains(name, substring) && ready == "True" {
|
||||
log.Debugf("Found pod: %s", name)
|
||||
return name, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("no ready pod found matching %q", substring)
|
||||
}
|
||||
|
||||
// ExecOnPod runs a command on a pod and returns its stdout.
|
||||
func (c *Cluster) ExecOnPod(pod string, command ...string) (string, error) {
|
||||
args := append(c.kubectlArgs(), "exec", pod, "--")
|
||||
args = append(args, command...)
|
||||
log.Debugf("Running: kubectl %s", strings.Join(args, " "))
|
||||
|
||||
cmd := exec.Command("kubectl", args...)
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return "", fmt.Errorf("kubectl exec failed: %w\n%s", err, stderr.String())
|
||||
}
|
||||
|
||||
return stdout.String(), nil
|
||||
}
|
||||
@@ -1,7 +1,11 @@
|
||||
import "@opal/components/buttons/Button/styles.css";
|
||||
import "@opal/components/tooltip.css";
|
||||
import { Interactive, type InteractiveBaseProps } from "@opal/core";
|
||||
import type { SizeVariant, WidthVariant } from "@opal/shared";
|
||||
import {
|
||||
Interactive,
|
||||
type InteractiveBaseProps,
|
||||
type InteractiveContainerWidthVariant,
|
||||
} from "@opal/core";
|
||||
import type { SizeVariant } from "@opal/shared";
|
||||
import type { TooltipSide } from "@opal/components";
|
||||
import type { IconFunctionComponent } from "@opal/types";
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
@@ -87,7 +91,7 @@ type ButtonProps = InteractiveBaseProps &
|
||||
tooltip?: string;
|
||||
|
||||
/** Width preset. `"auto"` shrink-wraps, `"full"` stretches to parent width. */
|
||||
width?: WidthVariant;
|
||||
width?: InteractiveContainerWidthVariant;
|
||||
|
||||
/** Which side the tooltip appears on. */
|
||||
tooltipSide?: TooltipSide;
|
||||
|
||||
@@ -12,5 +12,6 @@ export {
|
||||
type InteractiveBaseProps,
|
||||
type InteractiveBaseVariantProps,
|
||||
type InteractiveContainerProps,
|
||||
type InteractiveContainerWidthVariant,
|
||||
type InteractiveContainerRoundingVariant,
|
||||
} from "@opal/core/interactive/components";
|
||||
|
||||
@@ -3,12 +3,7 @@ import React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cn } from "@opal/utils";
|
||||
import type { WithoutStyles } from "@opal/types";
|
||||
import {
|
||||
sizeVariants,
|
||||
type SizeVariant,
|
||||
widthVariants,
|
||||
type WidthVariant,
|
||||
} from "@opal/shared";
|
||||
import { sizeVariants, type SizeVariant } from "@opal/shared";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
@@ -44,6 +39,18 @@ type InteractiveBaseVariantProps =
|
||||
selected?: never;
|
||||
};
|
||||
|
||||
/**
|
||||
* Width presets for `Interactive.Container`.
|
||||
*
|
||||
* - `"auto"` — Shrink-wraps to content width (default)
|
||||
* - `"full"` — Stretches to fill the parent's width (`w-full`)
|
||||
*/
|
||||
type InteractiveContainerWidthVariant = "auto" | "full";
|
||||
const interactiveContainerWidthVariants = {
|
||||
auto: "w-auto",
|
||||
full: "w-full",
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Border-radius presets for `Interactive.Container`.
|
||||
*
|
||||
@@ -338,7 +345,7 @@ interface InteractiveContainerProps
|
||||
*
|
||||
* @default "auto"
|
||||
*/
|
||||
widthVariant?: WidthVariant;
|
||||
widthVariant?: InteractiveContainerWidthVariant;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -406,7 +413,7 @@ function InteractiveContainer({
|
||||
height,
|
||||
minWidth,
|
||||
padding,
|
||||
widthVariants[widthVariant],
|
||||
interactiveContainerWidthVariants[widthVariant],
|
||||
slotClassName
|
||||
),
|
||||
"data-border": border ? ("true" as const) : undefined,
|
||||
@@ -483,5 +490,6 @@ export {
|
||||
type InteractiveBaseVariantProps,
|
||||
type InteractiveBaseSelectVariantProps,
|
||||
type InteractiveContainerProps,
|
||||
type InteractiveContainerWidthVariant,
|
||||
type InteractiveContainerRoundingVariant,
|
||||
};
|
||||
|
||||
@@ -14,8 +14,6 @@ import {
|
||||
} from "@opal/layouts/Content/LabelLayout";
|
||||
import type { TagProps } from "@opal/components/Tag/components";
|
||||
import type { IconFunctionComponent } from "@opal/types";
|
||||
import { widthVariants, type WidthVariant } from "@opal/shared";
|
||||
import { cn } from "@opal/utils";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Shared types
|
||||
@@ -45,17 +43,6 @@ interface ContentBaseProps {
|
||||
|
||||
/** Called when the user commits an edit. */
|
||||
onTitleChange?: (newTitle: string) => void;
|
||||
|
||||
/**
|
||||
* Width preset controlling the component's horizontal size.
|
||||
* Uses the shared `WidthVariant` scale from `@opal/shared`.
|
||||
*
|
||||
* - `"auto"` — Shrink-wraps to content width
|
||||
* - `"full"` — Stretches to fill the parent's width
|
||||
*
|
||||
* @default "auto"
|
||||
*/
|
||||
widthVariant?: WidthVariant;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -100,20 +87,11 @@ type ContentProps = HeadingContentProps | LabelContentProps | BodyContentProps;
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function Content(props: ContentProps) {
|
||||
const {
|
||||
sizePreset = "headline",
|
||||
variant = "heading",
|
||||
widthVariant = "auto",
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
const widthClass = widthVariants[widthVariant];
|
||||
|
||||
let layout: React.ReactNode = null;
|
||||
const { sizePreset = "headline", variant = "heading", ...rest } = props;
|
||||
|
||||
// Heading layout: headline/section presets with heading/section variant
|
||||
if (sizePreset === "headline" || sizePreset === "section") {
|
||||
layout = (
|
||||
return (
|
||||
<HeadingLayout
|
||||
sizePreset={sizePreset}
|
||||
variant={variant as HeadingLayoutProps["variant"]}
|
||||
@@ -123,8 +101,8 @@ function Content(props: ContentProps) {
|
||||
}
|
||||
|
||||
// Label layout: main-content/main-ui/secondary with section variant
|
||||
else if (variant === "section" || variant === "heading") {
|
||||
layout = (
|
||||
if (variant === "section" || variant === "heading") {
|
||||
return (
|
||||
<LabelLayout
|
||||
sizePreset={sizePreset}
|
||||
{...(rest as Omit<LabelLayoutProps, "sizePreset">)}
|
||||
@@ -133,8 +111,8 @@ function Content(props: ContentProps) {
|
||||
}
|
||||
|
||||
// Body layout: main-content/main-ui/secondary with body variant
|
||||
else if (variant === "body") {
|
||||
layout = (
|
||||
if (variant === "body") {
|
||||
return (
|
||||
<BodyLayout
|
||||
sizePreset={sizePreset}
|
||||
{...(rest as Omit<
|
||||
@@ -145,17 +123,7 @@ function Content(props: ContentProps) {
|
||||
);
|
||||
}
|
||||
|
||||
// This case should NEVER be hit.
|
||||
if (!layout)
|
||||
throw new Error(
|
||||
`Content: no layout matched for sizePreset="${sizePreset}" variant="${variant}"`
|
||||
);
|
||||
|
||||
// "auto" → return layout directly (a block div with w-auto still
|
||||
// stretches to its parent, defeating shrink-to-content).
|
||||
if (widthVariant === "auto") return layout;
|
||||
|
||||
return <div className={widthClass}>{layout}</div>;
|
||||
return null;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -58,7 +58,7 @@ function ContentAction({
|
||||
|
||||
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", padding)}>
|
||||
<Content {...contentProps} />
|
||||
</div>
|
||||
{rightChildren && (
|
||||
|
||||
@@ -50,31 +50,4 @@ const sizeVariants = {
|
||||
/** Named size preset key. */
|
||||
type SizeVariant = keyof typeof sizeVariants;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Width Variants
|
||||
//
|
||||
// A named scale of width presets that map to Tailwind width utility classes.
|
||||
//
|
||||
// Consumers:
|
||||
// - Interactive.Container (widthVariant)
|
||||
// - Button (width)
|
||||
// - Content (widthVariant)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Width-variant scale.
|
||||
*
|
||||
* | Key | Tailwind class |
|
||||
* |--------|----------------|
|
||||
* | `auto` | `w-auto` |
|
||||
* | `full` | `w-full` |
|
||||
*/
|
||||
const widthVariants = {
|
||||
auto: "w-auto",
|
||||
full: "w-full",
|
||||
} as const;
|
||||
|
||||
/** Named width preset key. */
|
||||
type WidthVariant = keyof typeof widthVariants;
|
||||
|
||||
export { sizeVariants, type SizeVariant, widthVariants, type WidthVariant };
|
||||
export { sizeVariants, type SizeVariant };
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { Section } from "@/layouts/general-layouts";
|
||||
import { Content } from "@opal/layouts";
|
||||
import { Section, LineItemLayout } from "@/layouts/general-layouts";
|
||||
import * as InputLayouts from "@/layouts/input-layouts";
|
||||
import Card from "@/refresh-components/cards/Card";
|
||||
import Button from "@/refresh-components/buttons/Button";
|
||||
@@ -373,11 +372,9 @@ function SeatsCard({
|
||||
padding={1}
|
||||
height="auto"
|
||||
>
|
||||
<Content
|
||||
<LineItemLayout
|
||||
title="Update Seats"
|
||||
description="Add or remove seats to reflect your team size."
|
||||
sizePreset="main-content"
|
||||
variant="section"
|
||||
/>
|
||||
<Button main secondary onClick={handleCancel} disabled={isSubmitting}>
|
||||
Cancel
|
||||
|
||||
@@ -12,7 +12,6 @@ import Tabs from "@/refresh-components/Tabs";
|
||||
import { useFormikContext } from "formik";
|
||||
import * as GeneralLayouts from "@/layouts/general-layouts";
|
||||
import * as InputLayouts from "@/layouts/input-layouts";
|
||||
import { Content } from "@opal/layouts";
|
||||
import CheckboxField from "@/refresh-components/form/LabeledCheckboxField";
|
||||
import InputTextAreaField from "@/refresh-components/form/InputTextAreaField";
|
||||
import Text from "@/refresh-components/texts/Text";
|
||||
@@ -47,11 +46,9 @@ const TabsField: FC<TabsFieldProps> = ({
|
||||
return (
|
||||
<GeneralLayouts.Section gap={0.5} alignItems="start">
|
||||
{tabField.label && (
|
||||
<Content
|
||||
<InputLayouts.Title
|
||||
title={resolvedLabel ?? ""}
|
||||
description={resolvedDescription}
|
||||
sizePreset="main-content"
|
||||
variant="section"
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -5,8 +5,7 @@ import { cn } from "@/lib/utils";
|
||||
import { ThreeDotsLoader } from "@/components/Loading";
|
||||
import { ErrorCallout } from "@/components/ErrorCallout";
|
||||
import { toast } from "@/hooks/useToast";
|
||||
import { Section } from "@/layouts/general-layouts";
|
||||
import { ContentAction } from "@opal/layouts";
|
||||
import { Section, LineItemLayout } from "@/layouts/general-layouts";
|
||||
import * as SettingsLayouts from "@/layouts/settings-layouts";
|
||||
import Text from "@/refresh-components/texts/Text";
|
||||
import Card from "@/refresh-components/cards/Card";
|
||||
@@ -90,11 +89,9 @@ function GuildDetailContent({
|
||||
)}
|
||||
|
||||
<Card variant={disabled ? "disabled" : "primary"}>
|
||||
<ContentAction
|
||||
<LineItemLayout
|
||||
title="Channel Configuration"
|
||||
description="Run !sync-channels in Discord to update the channel list."
|
||||
sizePreset="main-content"
|
||||
variant="section"
|
||||
rightChildren={
|
||||
isRegistered && !channelsLoading && !channelsError ? (
|
||||
<Section
|
||||
@@ -342,11 +339,9 @@ export default function Page({ params }: Props) {
|
||||
<SettingsLayouts.Body>
|
||||
{/* Default Persona Selector */}
|
||||
<Card variant={!guild?.enabled ? "disabled" : "primary"}>
|
||||
<ContentAction
|
||||
<LineItemLayout
|
||||
title="Default Agent"
|
||||
description="The agent used by the bot in all channels unless overridden."
|
||||
sizePreset="main-content"
|
||||
variant="section"
|
||||
rightChildren={
|
||||
<InputSelect
|
||||
value={guild?.default_persona_id?.toString() ?? "default"}
|
||||
|
||||
@@ -5,7 +5,7 @@ import useSWR from "swr";
|
||||
import { SvgArrowExchange } from "@opal/icons";
|
||||
import * as SettingsLayouts from "@/layouts/settings-layouts";
|
||||
import Card from "@/refresh-components/cards/Card";
|
||||
import { Content, ContentAction } from "@opal/layouts";
|
||||
import { LineItemLayout } from "@/layouts/general-layouts";
|
||||
import Text from "@/refresh-components/texts/Text";
|
||||
import InputSelect from "@/refresh-components/inputs/InputSelect";
|
||||
import Button from "@/refresh-components/buttons/Button";
|
||||
@@ -72,10 +72,9 @@ function MigrationStatusSection() {
|
||||
<Card>
|
||||
<Text headingH3>Migration Status</Text>
|
||||
|
||||
<ContentAction
|
||||
<LineItemLayout
|
||||
title="Started"
|
||||
sizePreset="main-ui"
|
||||
variant="section"
|
||||
variant="secondary"
|
||||
rightChildren={
|
||||
<Text mainUiBody>
|
||||
{hasStarted ? formatTimestamp(data.created_at!) : "Not started"}
|
||||
@@ -83,10 +82,9 @@ function MigrationStatusSection() {
|
||||
}
|
||||
/>
|
||||
|
||||
<ContentAction
|
||||
<LineItemLayout
|
||||
title="Chunks Migrated"
|
||||
sizePreset="main-ui"
|
||||
variant="section"
|
||||
variant="secondary"
|
||||
rightChildren={
|
||||
<Text mainUiBody>
|
||||
{progressPercentage !== null
|
||||
@@ -98,10 +96,9 @@ function MigrationStatusSection() {
|
||||
}
|
||||
/>
|
||||
|
||||
<ContentAction
|
||||
<LineItemLayout
|
||||
title="Completed"
|
||||
sizePreset="main-ui"
|
||||
variant="section"
|
||||
variant="secondary"
|
||||
rightChildren={
|
||||
<Text mainUiBody>
|
||||
{hasCompleted
|
||||
@@ -177,11 +174,10 @@ function RetrievalSourceSection() {
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Content
|
||||
<LineItemLayout
|
||||
title="Retrieval Source"
|
||||
description="Controls which document index is used for retrieval."
|
||||
sizePreset="main-ui"
|
||||
variant="section"
|
||||
variant="secondary"
|
||||
/>
|
||||
|
||||
<InputSelect
|
||||
|
||||
@@ -47,8 +47,6 @@ export interface RendererResult {
|
||||
|
||||
// Whether this renderer supports collapsible mode (collapse button shown only when true)
|
||||
supportsCollapsible?: boolean;
|
||||
/** Whether the step should remain collapsible even in single-step timelines */
|
||||
alwaysCollapsible?: boolean;
|
||||
/** Whether the result should be wrapped by timeline UI or rendered as-is */
|
||||
timelineLayout?: TimelineLayout;
|
||||
}
|
||||
|
||||
@@ -50,9 +50,7 @@ export function TimelineStepComposer({
|
||||
header={result.status}
|
||||
isExpanded={result.isExpanded}
|
||||
onToggle={result.onToggle}
|
||||
collapsible={
|
||||
collapsible && (!isSingleStep || !!result.alwaysCollapsible)
|
||||
}
|
||||
collapsible={collapsible && !isSingleStep}
|
||||
supportsCollapsible={result.supportsCollapsible}
|
||||
isLastStep={index === results.length - 1 && isLastStep}
|
||||
isFirstStep={index === 0 && isFirstStep}
|
||||
|
||||
@@ -6,8 +6,7 @@ import { Button } from "@opal/components";
|
||||
import Tag from "@/refresh-components/buttons/Tag";
|
||||
import Text from "@/refresh-components/texts/Text";
|
||||
import SimpleTooltip from "@/refresh-components/SimpleTooltip";
|
||||
import { Section } from "@/layouts/general-layouts";
|
||||
import { ContentAction } from "@opal/layouts";
|
||||
import { Section, LineItemLayout } from "@/layouts/general-layouts";
|
||||
import { formatDurationSeconds } from "@/lib/time";
|
||||
import { noProp } from "@/lib/utils";
|
||||
import MemoriesModal from "@/refresh-components/modals/MemoriesModal";
|
||||
@@ -62,12 +61,10 @@ function MemoryTagWithTooltip({
|
||||
{memoryText}
|
||||
</Text>
|
||||
</div>
|
||||
<ContentAction
|
||||
<LineItemLayout
|
||||
variant="mini"
|
||||
icon={SvgAddLines}
|
||||
title={operationLabel}
|
||||
sizePreset="secondary"
|
||||
variant="body"
|
||||
prominence="muted"
|
||||
rightChildren={
|
||||
<Button
|
||||
prominence="tertiary"
|
||||
|
||||
@@ -54,7 +54,7 @@ export function TimelineRow({
|
||||
isHover={isHover}
|
||||
/>
|
||||
)}
|
||||
<div className="flex-1 min-w-0">{children}</div>
|
||||
<div className="flex-1">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -138,7 +138,7 @@ export const PythonToolRenderer: MessageRenderer<PythonToolPacket, {}> = ({
|
||||
{stdout && (
|
||||
<div className="rounded-md bg-background-neutral-02 p-3">
|
||||
<div className="text-xs font-semibold mb-1 text-text-03">Output:</div>
|
||||
<pre className="text-sm whitespace-pre-wrap font-mono text-text-01 overflow-x-auto">
|
||||
<pre className="text-sm whitespace-pre-wrap font-mono text-text-01">
|
||||
{stdout}
|
||||
</pre>
|
||||
</div>
|
||||
@@ -150,7 +150,7 @@ export const PythonToolRenderer: MessageRenderer<PythonToolPacket, {}> = ({
|
||||
<div className="text-xs font-semibold mb-1 text-status-error-05">
|
||||
Error:
|
||||
</div>
|
||||
<pre className="text-sm whitespace-pre-wrap font-mono text-status-error-05 overflow-x-auto">
|
||||
<pre className="text-sm whitespace-pre-wrap font-mono text-status-error-05">
|
||||
{stderr}
|
||||
</pre>
|
||||
</div>
|
||||
@@ -181,7 +181,6 @@ export const PythonToolRenderer: MessageRenderer<PythonToolPacket, {}> = ({
|
||||
status,
|
||||
content,
|
||||
supportsCollapsible: true,
|
||||
alwaysCollapsible: true,
|
||||
},
|
||||
]);
|
||||
}
|
||||
@@ -192,7 +191,6 @@ export const PythonToolRenderer: MessageRenderer<PythonToolPacket, {}> = ({
|
||||
icon: SvgTerminal,
|
||||
status,
|
||||
supportsCollapsible: true,
|
||||
alwaysCollapsible: true,
|
||||
content: (
|
||||
<FadingEdgeContainer
|
||||
direction="bottom"
|
||||
|
||||
@@ -9,8 +9,7 @@ import type { SharingScope } from "@/app/craft/types/streamingTypes";
|
||||
import { cn } from "@/lib/utils";
|
||||
import Popover from "@/refresh-components/Popover";
|
||||
import Truncated from "@/refresh-components/texts/Truncated";
|
||||
import { Section } from "@/layouts/general-layouts";
|
||||
import { ContentAction } from "@opal/layouts";
|
||||
import { Section, LineItemLayout } from "@/layouts/general-layouts";
|
||||
|
||||
interface ShareButtonProps {
|
||||
sessionId: string;
|
||||
@@ -134,12 +133,11 @@ export default function ShareButton({
|
||||
: "hover:bg-background-tint-02"
|
||||
)}
|
||||
>
|
||||
<ContentAction
|
||||
<LineItemLayout
|
||||
title={opt.label}
|
||||
description={opt.description}
|
||||
sizePreset="main-ui"
|
||||
variant="section"
|
||||
paddingVariant="sm"
|
||||
variant="tertiary"
|
||||
reducedPadding
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useState } from "react";
|
||||
import Card from "@/refresh-components/cards/Card";
|
||||
import Text from "@/refresh-components/texts/Text";
|
||||
import { Content } from "@opal/layouts";
|
||||
import { LineItemLayout } from "@/layouts/general-layouts";
|
||||
import Separator from "@/refresh-components/Separator";
|
||||
import { ValidSources } from "@/lib/types";
|
||||
import { getSourceMetadata } from "@/lib/sources";
|
||||
@@ -99,15 +99,14 @@ export default function ComingSoonConnectors() {
|
||||
const card = (
|
||||
<div key={type} className="opacity-60">
|
||||
<Card variant="secondary">
|
||||
<Content
|
||||
<LineItemLayout
|
||||
icon={
|
||||
type === ValidSources.Imap
|
||||
? OutlookIcon
|
||||
: sourceMetadata.icon
|
||||
}
|
||||
title={displayName}
|
||||
sizePreset="main-ui"
|
||||
variant="body"
|
||||
center
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
@@ -119,12 +118,7 @@ export default function ComingSoonConnectors() {
|
||||
card,
|
||||
<div key="onedrive" className="opacity-60">
|
||||
<Card variant="secondary">
|
||||
<Content
|
||||
icon={OneDriveIcon}
|
||||
title="OneDrive"
|
||||
sizePreset="main-ui"
|
||||
variant="body"
|
||||
/>
|
||||
<LineItemLayout icon={OneDriveIcon} title="OneDrive" center />
|
||||
</Card>
|
||||
</div>,
|
||||
];
|
||||
@@ -136,12 +130,7 @@ export default function ComingSoonConnectors() {
|
||||
card,
|
||||
<div key="box" className="opacity-60">
|
||||
<Card variant="secondary">
|
||||
<Content
|
||||
icon={BoxIcon}
|
||||
title="Box"
|
||||
sizePreset="main-ui"
|
||||
variant="body"
|
||||
/>
|
||||
<LineItemLayout icon={BoxIcon} title="Box" center />
|
||||
</Card>
|
||||
</div>,
|
||||
];
|
||||
@@ -152,23 +141,13 @@ export default function ComingSoonConnectors() {
|
||||
{/* Enterprise/ERP */}
|
||||
<div className="opacity-60">
|
||||
<Card variant="secondary">
|
||||
<Content
|
||||
icon={ServiceNowIcon}
|
||||
title="ServiceNow"
|
||||
sizePreset="main-ui"
|
||||
variant="body"
|
||||
/>
|
||||
<LineItemLayout icon={ServiceNowIcon} title="ServiceNow" center />
|
||||
</Card>
|
||||
</div>
|
||||
{/* Project Management */}
|
||||
<div className="opacity-60">
|
||||
<Card variant="secondary">
|
||||
<Content
|
||||
icon={TrelloIcon}
|
||||
title="Trello"
|
||||
sizePreset="main-ui"
|
||||
variant="body"
|
||||
/>
|
||||
<LineItemLayout icon={TrelloIcon} title="Trello" center />
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,8 @@ import { useState } from "react";
|
||||
import Card from "@/refresh-components/cards/Card";
|
||||
import Popover from "@/refresh-components/Popover";
|
||||
import LineItem from "@/refresh-components/buttons/LineItem";
|
||||
import { ContentAction } from "@opal/layouts";
|
||||
import Text from "@/refresh-components/texts/Text";
|
||||
import { Section, LineItemLayout } from "@/layouts/general-layouts";
|
||||
import { ValidSources } from "@/lib/types";
|
||||
import { getSourceMetadata } from "@/lib/sources";
|
||||
import { SvgMoreHorizontal, SvgPlug, SvgSettings, SvgTrash } from "@opal/icons";
|
||||
@@ -40,6 +41,15 @@ interface ConnectorCardProps {
|
||||
onDelete: () => void;
|
||||
}
|
||||
|
||||
const STATUS_COLORS: Record<ConnectorStatus, string> = {
|
||||
connected: "bg-status-success-05",
|
||||
connected_with_errors: "bg-status-warning-05",
|
||||
indexing: "bg-status-warning-05 animate-pulse",
|
||||
error: "bg-status-error-05",
|
||||
deleting: "bg-status-error-05 animate-pulse",
|
||||
not_connected: "bg-background-neutral-03",
|
||||
};
|
||||
|
||||
function getStatusText(status: ConnectorStatus, docsIndexed: number): string {
|
||||
switch (status) {
|
||||
case "connected":
|
||||
@@ -62,6 +72,29 @@ function getStatusText(status: ConnectorStatus, docsIndexed: number): string {
|
||||
}
|
||||
}
|
||||
|
||||
function StatusDescription({
|
||||
status,
|
||||
docsIndexed,
|
||||
}: {
|
||||
status: ConnectorStatus;
|
||||
docsIndexed: number;
|
||||
}) {
|
||||
return (
|
||||
<Section
|
||||
flexDirection="row"
|
||||
alignItems="center"
|
||||
gap={0.375}
|
||||
width="fit"
|
||||
height="fit"
|
||||
>
|
||||
<div className={cn(STATUS_COLORS[status], "w-2 h-2 rounded-full")} />
|
||||
<Text secondaryBody text03>
|
||||
{getStatusText(status, docsIndexed)}
|
||||
</Text>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ConnectorCard({
|
||||
connectorType,
|
||||
config,
|
||||
@@ -147,8 +180,17 @@ export default function ConnectorCard({
|
||||
const cardVariant =
|
||||
isAlwaysConnected || isConnected ? "primary" : "secondary";
|
||||
|
||||
const descriptionText =
|
||||
customDescription ?? getStatusText(status, config?.docs_indexed || 0);
|
||||
// Use custom description if provided, otherwise show status
|
||||
const descriptionContent = customDescription ? (
|
||||
<Text secondaryBody text03>
|
||||
{customDescription}
|
||||
</Text>
|
||||
) : (
|
||||
<StatusDescription
|
||||
status={status}
|
||||
docsIndexed={config?.docs_indexed || 0}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -156,13 +198,12 @@ export default function ConnectorCard({
|
||||
onClick={handleCardClick}
|
||||
>
|
||||
<Card variant={cardVariant}>
|
||||
<ContentAction
|
||||
<LineItemLayout
|
||||
icon={sourceMetadata.icon}
|
||||
title={sourceMetadata.displayName}
|
||||
description={descriptionText}
|
||||
sizePreset="main-content"
|
||||
variant="section"
|
||||
description={descriptionContent}
|
||||
rightChildren={rightContent}
|
||||
center
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
:root {
|
||||
--app-page-main-content-width: 52.5rem;
|
||||
--block-width-form-input-min: 10rem;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ConnectorStatus } from "@/lib/types";
|
||||
import { ConnectorTitle } from "@/components/admin/connectors/ConnectorTitle";
|
||||
import { Content } from "@opal/layouts";
|
||||
import * as InputLayouts from "@/layouts/input-layouts";
|
||||
import Text from "@/refresh-components/texts/Text";
|
||||
import { SvgLock } from "@opal/icons";
|
||||
interface NonSelectableConnectorsProps {
|
||||
@@ -20,12 +20,7 @@ export const NonSelectableConnectors = ({
|
||||
|
||||
return (
|
||||
<div className="mt-6 mb-4">
|
||||
<Content
|
||||
title={title}
|
||||
description={description}
|
||||
sizePreset="main-content"
|
||||
variant="section"
|
||||
/>
|
||||
<InputLayouts.Title title={title} description={description} />
|
||||
|
||||
<div className="p-3 border border-dashed border-border-02 rounded-12 bg-background-neutral-01">
|
||||
<div className="mb-2 flex items-center gap-1.5">
|
||||
|
||||
@@ -9,7 +9,7 @@ import { getSourceMetadata } from "@/lib/sources";
|
||||
import useFederatedOAuthStatus from "@/hooks/useFederatedOAuthStatus";
|
||||
import { SvgLink } from "@opal/icons";
|
||||
import { Card } from "@/refresh-components/cards";
|
||||
import { ContentAction } from "@opal/layouts";
|
||||
import { LineItemLayout } from "@/layouts/general-layouts";
|
||||
|
||||
export interface FederatedConnectorOAuthStatus {
|
||||
federated_connector_id: number;
|
||||
@@ -136,12 +136,10 @@ export default function FederatedOAuthModal() {
|
||||
|
||||
return (
|
||||
<Card key={connector.federated_connector_id}>
|
||||
<ContentAction
|
||||
<LineItemLayout
|
||||
icon={sourceMetadata.icon}
|
||||
title={sourceMetadata.displayName}
|
||||
description={sourceMetadata.category}
|
||||
sizePreset="main-content"
|
||||
variant="section"
|
||||
rightChildren={
|
||||
<Button
|
||||
secondary
|
||||
@@ -151,6 +149,7 @@ export default function FederatedOAuthModal() {
|
||||
Connect
|
||||
</Button>
|
||||
}
|
||||
center
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
import React, { HtmlHTMLAttributes } from "react";
|
||||
import type { IconProps } from "@opal/types";
|
||||
import { WithoutStyles } from "@/types";
|
||||
import { ContentAction } from "@opal/layouts";
|
||||
import { LineItemLayout } from "@/layouts/general-layouts";
|
||||
import * as ExpandableCard from "@/layouts/expandable-card-layouts";
|
||||
import { Card } from "@/refresh-components/cards";
|
||||
import Label from "@/refresh-components/form/Label";
|
||||
@@ -119,14 +119,11 @@ function ActionsHeader({
|
||||
<div className="flex flex-col gap-2 pt-4 pb-2">
|
||||
<div className="px-4">
|
||||
<Label name={name}>
|
||||
<ContentAction
|
||||
<LineItemLayout
|
||||
icon={Icon}
|
||||
title={title}
|
||||
description={description}
|
||||
sizePreset="section"
|
||||
variant="section"
|
||||
rightChildren={rightChildren}
|
||||
paddingVariant="fit"
|
||||
/>
|
||||
</Label>
|
||||
</div>
|
||||
@@ -240,14 +237,12 @@ function ActionsTool({
|
||||
return (
|
||||
<Card padding={0.75} variant={disabled ? "disabled" : undefined}>
|
||||
<Label name={name} disabled={disabled}>
|
||||
<ContentAction
|
||||
<LineItemLayout
|
||||
icon={icon}
|
||||
title={title}
|
||||
description={description}
|
||||
sizePreset="main-ui"
|
||||
variant="section"
|
||||
rightChildren={rightChildren}
|
||||
paddingVariant="fit"
|
||||
variant="secondary"
|
||||
/>
|
||||
</Label>
|
||||
</Card>
|
||||
|
||||
@@ -48,6 +48,7 @@ import { PopoverSearchInput } from "@/sections/sidebar/ChatButton";
|
||||
import SimplePopover from "@/refresh-components/SimplePopover";
|
||||
import { Interactive } from "@opal/core";
|
||||
import { Button, OpenButton } from "@opal/components";
|
||||
import { LineItemLayout } from "@/layouts/general-layouts";
|
||||
import { useAppSidebarContext } from "@/providers/AppSidebarProvider";
|
||||
import useScreenSize from "@/hooks/useScreenSize";
|
||||
import {
|
||||
|
||||
@@ -2,7 +2,6 @@ import { cn } from "@/lib/utils";
|
||||
import Text from "@/refresh-components/texts/Text";
|
||||
import Truncated from "@/refresh-components/texts/Truncated";
|
||||
import { WithoutStyles } from "@/types";
|
||||
import { Content } from "@opal/layouts";
|
||||
import { IconProps } from "@opal/types";
|
||||
import React from "react";
|
||||
|
||||
@@ -161,6 +160,152 @@ function Section({
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* LineItemLayout - A layout for icon + title + description rows
|
||||
*
|
||||
* Structure:
|
||||
* Flexbox Row [
|
||||
* Grid [
|
||||
* [Icon] [Title ]
|
||||
* [ ] [Description]
|
||||
* ],
|
||||
* rightChildren
|
||||
* ]
|
||||
*
|
||||
* - Icon column auto-sizes to icon width
|
||||
* - Icon vertically centers with title
|
||||
* - Description aligns with title's left edge (both in grid column 2)
|
||||
* - rightChildren is outside the grid, in the outer flexbox
|
||||
*
|
||||
* Variants:
|
||||
* - `primary`: Standard size (20px icon) with emphasized text. The default for prominent list items.
|
||||
* - `secondary`: Compact size (16px icon) with standard text. Use for denser lists or nested items.
|
||||
* - `tertiary`: Compact size (16px icon) with standard text. Use for less prominent items in tight layouts.
|
||||
* - `tertiary-muted`: Compact size (16px icon) with muted text styling. Use for de-emphasized or secondary information.
|
||||
* - `mini`: Smallest size (12px icon) with muted secondary text. Use for metadata labels (e.g., owner, action count).
|
||||
*
|
||||
* @param icon - Optional icon component to display on the left
|
||||
* @param title - The main title text (required)
|
||||
* @param description - Optional description content below the title (string or ReactNode)
|
||||
* @param rightChildren - Optional content to render on the right side
|
||||
* @param variant - Visual variant. Default: "primary"
|
||||
* @param strikethrough - If true, applies line-through style to title. Default: false
|
||||
* @param loading - If true, renders skeleton placeholders instead of content. Default: false
|
||||
* @param center - If true, vertically centers items; otherwise aligns to start. Default: false
|
||||
*/
|
||||
type LineItemLayoutVariant =
|
||||
| "primary"
|
||||
| "secondary"
|
||||
| "tertiary"
|
||||
| "tertiary-muted"
|
||||
| "mini";
|
||||
export interface LineItemLayoutProps {
|
||||
icon?: React.FunctionComponent<IconProps>;
|
||||
title: string;
|
||||
description?: React.ReactNode;
|
||||
middleText?: string;
|
||||
rightChildren?: React.ReactNode;
|
||||
|
||||
variant?: LineItemLayoutVariant;
|
||||
width?: Length;
|
||||
strikethrough?: boolean;
|
||||
loading?: boolean;
|
||||
center?: boolean;
|
||||
reducedPadding?: boolean;
|
||||
}
|
||||
function LineItemLayout({
|
||||
icon: Icon,
|
||||
title,
|
||||
description,
|
||||
middleText,
|
||||
rightChildren,
|
||||
|
||||
variant = "primary",
|
||||
width,
|
||||
strikethrough,
|
||||
loading,
|
||||
center,
|
||||
reducedPadding,
|
||||
}: LineItemLayoutProps) {
|
||||
// Derive styling from variant
|
||||
const isMini = variant === "mini";
|
||||
const isCompact =
|
||||
variant === "secondary" ||
|
||||
variant === "tertiary" ||
|
||||
variant === "tertiary-muted";
|
||||
const isMuted = variant === "tertiary-muted" || isMini;
|
||||
|
||||
// Determine icon size: mini=12px, compact=16px, primary=20px
|
||||
const iconSize = isMini ? 12 : isCompact ? 16 : 20;
|
||||
|
||||
// Determine gap: mini=0.25rem, others=1.5rem
|
||||
const gap = isMini ? 0.25 : 1.5;
|
||||
|
||||
return (
|
||||
<Section
|
||||
flexDirection="row"
|
||||
justifyContent="between"
|
||||
alignItems={center || isMini ? "center" : "start"}
|
||||
gap={gap}
|
||||
width={width}
|
||||
>
|
||||
<div
|
||||
className="line-item-layout"
|
||||
data-variant={variant}
|
||||
data-has-icon={Icon ? "true" : undefined}
|
||||
data-loading={loading ? "true" : undefined}
|
||||
data-strikethrough={strikethrough ? "true" : undefined}
|
||||
data-reduced-padding={reducedPadding ? "true" : undefined}
|
||||
>
|
||||
{/* Row 1: Icon, Title */}
|
||||
{Icon && <Icon size={iconSize} className="line-item-layout-icon" />}
|
||||
{loading ? (
|
||||
<div className="line-item-layout-skeleton-title" />
|
||||
) : (
|
||||
<Truncated
|
||||
mainContentEmphasis={!isCompact && !isMini}
|
||||
secondaryBody={isMini}
|
||||
mainUiAction={variant === "secondary"}
|
||||
text03={isMuted}
|
||||
className="line-item-layout-title"
|
||||
>
|
||||
{title}
|
||||
</Truncated>
|
||||
)}
|
||||
|
||||
{/* Row 2: Description (column 2, or column 1 if no icon) */}
|
||||
{loading && description ? (
|
||||
<div className="line-item-layout-skeleton-description" />
|
||||
) : description ? (
|
||||
<div className="line-item-layout-description">
|
||||
{typeof description === "string" ? (
|
||||
<Text secondaryBody text03>
|
||||
{description}
|
||||
</Text>
|
||||
) : (
|
||||
description
|
||||
)}
|
||||
</div>
|
||||
) : undefined}
|
||||
</div>
|
||||
|
||||
{!loading && middleText && (
|
||||
<div className="flex-1">
|
||||
<Truncated text03 secondaryBody>
|
||||
{middleText}
|
||||
</Truncated>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading && rightChildren ? (
|
||||
<div className="line-item-layout-skeleton-right" />
|
||||
) : rightChildren ? (
|
||||
<div className="flex-shrink-0">{rightChildren}</div>
|
||||
) : undefined}
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
export interface AttachmentItemLayoutProps {
|
||||
title: string;
|
||||
description: string;
|
||||
@@ -184,29 +329,18 @@ function AttachmentItemLayout({
|
||||
</div>
|
||||
</Section>
|
||||
</div>
|
||||
<Section
|
||||
flexDirection="row"
|
||||
justifyContent="between"
|
||||
alignItems="center"
|
||||
gap={1.5}
|
||||
>
|
||||
<Content
|
||||
title={title}
|
||||
description={description}
|
||||
sizePreset="main-ui"
|
||||
variant="section"
|
||||
/>
|
||||
{middleText && (
|
||||
<div className="flex-1">
|
||||
<Truncated text03 secondaryBody>
|
||||
{middleText}
|
||||
</Truncated>
|
||||
</div>
|
||||
)}
|
||||
{rightChildren && (
|
||||
<div className="flex-shrink-0 px-1">{rightChildren}</div>
|
||||
)}
|
||||
</Section>
|
||||
<LineItemLayout
|
||||
title={title}
|
||||
description={description}
|
||||
middleText={middleText}
|
||||
rightChildren={
|
||||
rightChildren ? (
|
||||
<div className="px-1">{rightChildren}</div>
|
||||
) : undefined
|
||||
}
|
||||
center
|
||||
variant="secondary"
|
||||
/>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
@@ -279,4 +413,4 @@ function CardItemLayout({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export { Section, CardItemLayout, AttachmentItemLayout };
|
||||
export { Section, LineItemLayout, CardItemLayout, AttachmentItemLayout };
|
||||
|
||||
@@ -4,18 +4,13 @@ import Text from "@/refresh-components/texts/Text";
|
||||
import { SvgXOctagon, SvgAlertCircle } from "@opal/icons";
|
||||
import { useField, useFormikContext } from "formik";
|
||||
import { Section } from "@/layouts/general-layouts";
|
||||
import { Content } from "@opal/layouts";
|
||||
import Label from "@/refresh-components/form/Label";
|
||||
|
||||
interface OrientationLayoutProps {
|
||||
interface OrientationLayoutProps extends TitleLayoutProps {
|
||||
name?: string;
|
||||
disabled?: boolean;
|
||||
nonInteractive?: boolean;
|
||||
children?: React.ReactNode;
|
||||
title: string;
|
||||
description?: string;
|
||||
optional?: boolean;
|
||||
sizePreset?: "main-content" | "main-ui";
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -49,20 +44,11 @@ function VerticalInputLayout({
|
||||
nonInteractive,
|
||||
children,
|
||||
subDescription,
|
||||
title,
|
||||
description,
|
||||
optional,
|
||||
sizePreset = "main-content",
|
||||
...titleLayoutProps
|
||||
}: VerticalLayoutProps) {
|
||||
const content = (
|
||||
<Section gap={0.25} alignItems="start">
|
||||
<Content
|
||||
title={title}
|
||||
description={description}
|
||||
optional={optional}
|
||||
sizePreset={sizePreset}
|
||||
variant="section"
|
||||
/>
|
||||
<TitleLayout {...titleLayoutProps} />
|
||||
{children}
|
||||
{name && <ErrorLayout name={name} />}
|
||||
{subDescription && (
|
||||
@@ -124,10 +110,7 @@ function HorizontalInputLayout({
|
||||
nonInteractive,
|
||||
children,
|
||||
center,
|
||||
title,
|
||||
description,
|
||||
optional,
|
||||
sizePreset = "main-content",
|
||||
...titleLayoutProps
|
||||
}: HorizontalLayoutProps) {
|
||||
const content = (
|
||||
<Section gap={0.25} alignItems="start">
|
||||
@@ -137,13 +120,7 @@ function HorizontalInputLayout({
|
||||
alignItems={center ? "center" : "start"}
|
||||
>
|
||||
<div className="flex flex-col flex-1 self-stretch">
|
||||
<Content
|
||||
title={title}
|
||||
description={description}
|
||||
optional={optional}
|
||||
sizePreset={sizePreset}
|
||||
variant="section"
|
||||
/>
|
||||
<TitleLayout {...titleLayoutProps} />
|
||||
</div>
|
||||
<div className="flex flex-col items-end">{children}</div>
|
||||
</Section>
|
||||
@@ -159,6 +136,80 @@ function HorizontalInputLayout({
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* TitleLayout - A reusable title/description component for form fields
|
||||
*
|
||||
* Renders a title with an optional description and "Optional" indicator.
|
||||
* This is a pure presentational component — it does not render a `<label>`
|
||||
* element. Label semantics are handled by the parent orientation layout
|
||||
* (Vertical/Horizontal) or by the caller.
|
||||
*
|
||||
* Exported as `Title` for convenient usage.
|
||||
*
|
||||
* @param title - The main label text
|
||||
* @param description - Additional helper text shown below the title
|
||||
* @param optional - Whether to show "(Optional)" indicator
|
||||
* @param center - If true, centers the title and description text. Default: false
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* import { Title } from "@/layouts/input-layouts";
|
||||
*
|
||||
* <Title
|
||||
* name="username"
|
||||
* title="Username"
|
||||
* description="Choose a unique username"
|
||||
* optional
|
||||
* />
|
||||
* ```
|
||||
*/
|
||||
type TitleLayoutVariants = "primary" | "secondary";
|
||||
export interface TitleLayoutProps {
|
||||
title: string;
|
||||
description?: string;
|
||||
optional?: boolean;
|
||||
center?: boolean;
|
||||
variant?: TitleLayoutVariants;
|
||||
}
|
||||
function TitleLayout({
|
||||
title,
|
||||
description,
|
||||
optional,
|
||||
center,
|
||||
variant = "primary",
|
||||
}: TitleLayoutProps) {
|
||||
return (
|
||||
<Section gap={0} height="fit">
|
||||
<Section
|
||||
flexDirection="row"
|
||||
justifyContent={center ? "center" : "start"}
|
||||
gap={0.25}
|
||||
>
|
||||
<Text
|
||||
mainContentEmphasis={variant === "primary"}
|
||||
mainUiAction={variant === "secondary"}
|
||||
text04
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
{optional && (
|
||||
<Text text03 mainContentMuted>
|
||||
(Optional)
|
||||
</Text>
|
||||
)}
|
||||
</Section>
|
||||
|
||||
{description && (
|
||||
<Section alignItems={center ? "center" : "start"}>
|
||||
<Text secondaryBody text03>
|
||||
{description}
|
||||
</Text>
|
||||
</Section>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* ErrorLayout - Displays Formik field validation errors
|
||||
*
|
||||
@@ -229,6 +280,7 @@ function ErrorTextLayout({ children, type = "error" }: ErrorTextLayoutProps) {
|
||||
export {
|
||||
VerticalInputLayout as Vertical,
|
||||
HorizontalInputLayout as Horizontal,
|
||||
TitleLayout as Title,
|
||||
ErrorLayout as Error,
|
||||
ErrorTextLayout,
|
||||
};
|
||||
|
||||
@@ -30,8 +30,7 @@
|
||||
|
||||
import { SvgEmpty } from "@opal/icons";
|
||||
import Card from "@/refresh-components/cards/Card";
|
||||
import Text from "@/refresh-components/texts/Text";
|
||||
import { Content } from "@opal/layouts";
|
||||
import { LineItemLayout } from "@/layouts/general-layouts";
|
||||
import { IconProps } from "@opal/types";
|
||||
|
||||
export interface EmptyMessageProps {
|
||||
@@ -47,18 +46,12 @@ export default function EmptyMessage({
|
||||
}: EmptyMessageProps) {
|
||||
return (
|
||||
<Card variant="tertiary">
|
||||
<Content
|
||||
<LineItemLayout
|
||||
icon={Icon}
|
||||
title={title}
|
||||
sizePreset="main-ui"
|
||||
variant="body"
|
||||
prominence="muted"
|
||||
description={description}
|
||||
variant="tertiary-muted"
|
||||
/>
|
||||
{description && (
|
||||
<Text secondaryBody text03>
|
||||
{description}
|
||||
</Text>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ import {
|
||||
CollapsibleTrigger,
|
||||
} from "@/refresh-components/Collapsible";
|
||||
import { Button } from "@opal/components";
|
||||
import { Content } from "@opal/layouts";
|
||||
import Text from "@/refresh-components/texts/Text";
|
||||
import { SvgFold, SvgExpand } from "@opal/icons";
|
||||
import { WithoutStyles } from "@/types";
|
||||
|
||||
@@ -198,13 +198,15 @@ const Header = React.forwardRef<HTMLDivElement, SimpleCollapsibleHeaderProps>(
|
||||
className="flex flex-row items-center justify-between gap-4 cursor-pointer select-none"
|
||||
{...props}
|
||||
>
|
||||
<div ref={boundingRef} className="w-full">
|
||||
<Content
|
||||
title={title}
|
||||
description={description}
|
||||
sizePreset="main-content"
|
||||
variant="section"
|
||||
/>
|
||||
<div ref={boundingRef} className="flex flex-col w-full">
|
||||
<Text mainContentEmphasis text04>
|
||||
{title}
|
||||
</Text>
|
||||
{description && (
|
||||
<Text secondaryBody text03>
|
||||
{description}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
icon={open ? SvgFold : SvgExpand}
|
||||
@@ -235,7 +237,7 @@ Header.displayName = "SimpleCollapsible.Header";
|
||||
* </SimpleCollapsible>
|
||||
* ```
|
||||
*/
|
||||
const ContentPanel = React.forwardRef<
|
||||
const Content = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
WithoutStyles<React.HTMLAttributes<HTMLDivElement>>
|
||||
>(({ children, ...props }, ref) => {
|
||||
@@ -247,9 +249,9 @@ const ContentPanel = React.forwardRef<
|
||||
</CollapsibleContent>
|
||||
);
|
||||
});
|
||||
ContentPanel.displayName = "SimpleCollapsible.Content";
|
||||
Content.displayName = "SimpleCollapsible.Content";
|
||||
|
||||
export default Object.assign(Root, {
|
||||
Header,
|
||||
Content: ContentPanel,
|
||||
Content,
|
||||
});
|
||||
|
||||
@@ -153,7 +153,7 @@ function InputSelectRoot({
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="w-full min-w-[var(--block-width-form-input-min)] relative">
|
||||
<div className={cn("w-full relative")}>
|
||||
<InputSelectContext.Provider value={contextValue}>
|
||||
<SelectPrimitive.Root
|
||||
{...(isControlled ? { value: currentValue } : { defaultValue })}
|
||||
|
||||
@@ -136,8 +136,8 @@ const InputTypeIn = React.forwardRef<HTMLInputElement, InputTypeInProps>(
|
||||
}}
|
||||
>
|
||||
{leftSearchIcon && (
|
||||
<div className="pr-2 pl-1">
|
||||
<div className="pl-[2px]">
|
||||
<div className="pr-2">
|
||||
<div className="pl-1">
|
||||
<SvgSearch className="w-[1rem] h-[1rem] stroke-text-02" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,8 +10,7 @@ import Button from "@/refresh-components/buttons/Button";
|
||||
import { Button as OpalButton } from "@opal/components";
|
||||
import { SvgProgressCircle, SvgX } from "@opal/icons";
|
||||
import { Card } from "@/refresh-components/cards";
|
||||
import { Section } from "@/layouts/general-layouts";
|
||||
import { ContentAction } from "@opal/layouts";
|
||||
import { LineItemLayout, Section } from "@/layouts/general-layouts";
|
||||
|
||||
interface OnboardingHeaderProps {
|
||||
state: OnboardingState;
|
||||
@@ -41,15 +40,11 @@ const OnboardingHeader = React.memo(
|
||||
|
||||
return (
|
||||
<Card padding={0.5} data-label="onboarding-header">
|
||||
<ContentAction
|
||||
<LineItemLayout
|
||||
icon={(props) => (
|
||||
<SvgProgressCircle value={iconPercentage} {...props} />
|
||||
)}
|
||||
title={STEP_CONFIG[onboardingState.currentStep].title}
|
||||
sizePreset="main-ui"
|
||||
variant="body"
|
||||
prominence="muted"
|
||||
paddingVariant="sm"
|
||||
rightChildren={
|
||||
stepButtonText ? (
|
||||
<Section flexDirection="row">
|
||||
@@ -75,6 +70,9 @@ const OnboardingHeader = React.memo(
|
||||
/>
|
||||
)
|
||||
}
|
||||
variant="tertiary-muted"
|
||||
reducedPadding
|
||||
center
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -5,8 +5,7 @@ import Button from "@/refresh-components/buttons/Button";
|
||||
import { FINAL_SETUP_CONFIG } from "@/refresh-components/onboarding/constants";
|
||||
import { FinalStepItemProps } from "@/refresh-components/onboarding/types";
|
||||
import { SvgExternalLink } from "@opal/icons";
|
||||
import { Section } from "@/layouts/general-layouts";
|
||||
import { ContentAction } from "@opal/layouts";
|
||||
import { LineItemLayout, Section } from "@/layouts/general-layouts";
|
||||
import { Card } from "@/refresh-components/cards";
|
||||
|
||||
const FinalStepItem = React.memo(
|
||||
@@ -24,13 +23,10 @@ const FinalStepItem = React.memo(
|
||||
|
||||
return (
|
||||
<Card padding={0.25} variant="secondary">
|
||||
<ContentAction
|
||||
<LineItemLayout
|
||||
icon={Icon}
|
||||
title={title}
|
||||
description={description}
|
||||
sizePreset="main-ui"
|
||||
variant="section"
|
||||
paddingVariant="sm"
|
||||
rightChildren={
|
||||
<Link href={buttonHref as Route} {...linkProps}>
|
||||
<Button tertiary rightIcon={SvgExternalLink}>
|
||||
@@ -38,6 +34,8 @@ const FinalStepItem = React.memo(
|
||||
</Button>
|
||||
</Link>
|
||||
}
|
||||
reducedPadding
|
||||
variant="tertiary"
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -89,7 +89,6 @@ import AgentKnowledgePane from "@/sections/knowledge/AgentKnowledgePane";
|
||||
import { ValidSources } from "@/lib/types";
|
||||
import { useSettingsContext } from "@/providers/SettingsProvider";
|
||||
import { useUser } from "@/providers/UserProvider";
|
||||
import SimpleLoader from "@/refresh-components/loaders/SimpleLoader";
|
||||
|
||||
interface AgentIconEditorProps {
|
||||
existingAgent?: FullPersona | null;
|
||||
@@ -314,11 +313,7 @@ function MCPServerCard({
|
||||
description={server.description}
|
||||
icon={getActionIcon(server.server_url, server.name)}
|
||||
rightChildren={
|
||||
<GeneralLayouts.Section
|
||||
flexDirection="row"
|
||||
gap={0.5}
|
||||
alignItems="start"
|
||||
>
|
||||
<GeneralLayouts.Section flexDirection="row" gap={0.5}>
|
||||
<EnabledCount
|
||||
enabledCount={enabledCount}
|
||||
totalCount={enabledTools.length}
|
||||
@@ -357,9 +352,18 @@ function MCPServerCard({
|
||||
</ActionsLayouts.Header>
|
||||
{isLoading ? (
|
||||
<ActionsLayouts.Content>
|
||||
<GeneralLayouts.Section padding={1}>
|
||||
<SimpleLoader />
|
||||
</GeneralLayouts.Section>
|
||||
{Array.from({ length: 3 }).map((_, index) => (
|
||||
<Card key={index} padding={0.75}>
|
||||
<GeneralLayouts.LineItemLayout
|
||||
// We provide dummy values here.
|
||||
// The `loading` prop will always render a pulsing box instead, so the dummy-values will actually NOT be rendered at all.
|
||||
title="..."
|
||||
description="..."
|
||||
rightChildren={<></>}
|
||||
loading
|
||||
/>
|
||||
</Card>
|
||||
))}
|
||||
</ActionsLayouts.Content>
|
||||
) : (
|
||||
enabledTools.length > 0 &&
|
||||
@@ -1164,6 +1168,7 @@ export default function AgentEditorPage({
|
||||
<InputLayouts.Vertical
|
||||
name="agent_avatar"
|
||||
title="Agent Avatar"
|
||||
center
|
||||
>
|
||||
<AgentIconEditor existingAgent={existingAgent} />
|
||||
</InputLayouts.Vertical>
|
||||
|
||||
@@ -3,8 +3,11 @@
|
||||
import { useRef, useCallback, useEffect, useState } from "react";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import * as InputLayouts from "@/layouts/input-layouts";
|
||||
import { Section, AttachmentItemLayout } from "@/layouts/general-layouts";
|
||||
import { Content, ContentAction } from "@opal/layouts";
|
||||
import {
|
||||
LineItemLayout,
|
||||
Section,
|
||||
AttachmentItemLayout,
|
||||
} from "@/layouts/general-layouts";
|
||||
import { Formik, Form } from "formik";
|
||||
import * as Yup from "yup";
|
||||
import {
|
||||
@@ -260,12 +263,7 @@ function GeneralSettings() {
|
||||
|
||||
<Section gap={2}>
|
||||
<Section gap={0.75}>
|
||||
<Content
|
||||
title="Profile"
|
||||
sizePreset="main-content"
|
||||
variant="section"
|
||||
widthVariant="full"
|
||||
/>
|
||||
<InputLayouts.Title title="Profile" />
|
||||
<Card>
|
||||
<InputLayouts.Horizontal
|
||||
title="Full Name"
|
||||
@@ -321,12 +319,7 @@ function GeneralSettings() {
|
||||
</Section>
|
||||
|
||||
<Section gap={0.75}>
|
||||
<Content
|
||||
title="Appearance"
|
||||
sizePreset="main-content"
|
||||
variant="section"
|
||||
widthVariant="full"
|
||||
/>
|
||||
<InputLayouts.Title title="Appearance" />
|
||||
<Card>
|
||||
<InputLayouts.Horizontal
|
||||
title="Color Mode"
|
||||
@@ -431,12 +424,7 @@ function GeneralSettings() {
|
||||
<Separator noPadding />
|
||||
|
||||
<Section gap={0.75}>
|
||||
<Content
|
||||
title="Danger Zone"
|
||||
sizePreset="main-content"
|
||||
variant="section"
|
||||
widthVariant="full"
|
||||
/>
|
||||
<InputLayouts.Title title="Danger Zone" />
|
||||
<Card>
|
||||
<InputLayouts.Horizontal
|
||||
title="Delete All Chats"
|
||||
@@ -781,12 +769,7 @@ function ChatPreferencesSettings() {
|
||||
return (
|
||||
<Section gap={2}>
|
||||
<Section gap={0.75}>
|
||||
<Content
|
||||
title="Chats"
|
||||
sizePreset="main-content"
|
||||
variant="section"
|
||||
widthVariant="full"
|
||||
/>
|
||||
<InputLayouts.Title title="Chats" />
|
||||
<Card>
|
||||
<InputLayouts.Horizontal
|
||||
title="Default Model"
|
||||
@@ -866,12 +849,7 @@ function ChatPreferencesSettings() {
|
||||
limit={500}
|
||||
/>
|
||||
</InputLayouts.Vertical>
|
||||
<Content
|
||||
title="Memory"
|
||||
sizePreset="main-content"
|
||||
variant="section"
|
||||
widthVariant="full"
|
||||
/>
|
||||
<InputLayouts.Title title="Memory" />
|
||||
<Card>
|
||||
<InputLayouts.Horizontal
|
||||
title="Reference Stored Memories"
|
||||
@@ -912,12 +890,7 @@ function ChatPreferencesSettings() {
|
||||
</Section>
|
||||
|
||||
<Section gap={0.75}>
|
||||
<Content
|
||||
title="Prompt Shortcuts"
|
||||
sizePreset="main-content"
|
||||
variant="section"
|
||||
widthVariant="full"
|
||||
/>
|
||||
<InputLayouts.Title title="Prompt Shortcuts" />
|
||||
<Card>
|
||||
<InputLayouts.Horizontal
|
||||
title="Use Prompt Shortcuts"
|
||||
@@ -1242,12 +1215,7 @@ function AccountsAccessSettings() {
|
||||
|
||||
<Section gap={2}>
|
||||
<Section gap={0.75}>
|
||||
<Content
|
||||
title="Accounts"
|
||||
sizePreset="main-content"
|
||||
variant="section"
|
||||
widthVariant="full"
|
||||
/>
|
||||
<InputLayouts.Title title="Accounts" />
|
||||
<Card>
|
||||
<InputLayouts.Horizontal
|
||||
title="Email"
|
||||
@@ -1279,12 +1247,7 @@ function AccountsAccessSettings() {
|
||||
|
||||
{showTokensSection && (
|
||||
<Section gap={0.75}>
|
||||
<Content
|
||||
title="Access Tokens"
|
||||
sizePreset="main-content"
|
||||
variant="section"
|
||||
widthVariant="full"
|
||||
/>
|
||||
<InputLayouts.Title title="Access Tokens" />
|
||||
{canCreateTokens ? (
|
||||
<Card padding={0.25}>
|
||||
<Section gap={0}>
|
||||
@@ -1400,12 +1363,10 @@ function IndexedConnectorCard({ source, isActive }: IndexedConnectorCardProps) {
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Content
|
||||
<LineItemLayout
|
||||
icon={sourceMetadata.icon}
|
||||
title={sourceMetadata.displayName}
|
||||
description={isActive ? "Connected" : "Paused"}
|
||||
sizePreset="main-content"
|
||||
variant="section"
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
@@ -1479,15 +1440,12 @@ function FederatedConnectorCard({
|
||||
)}
|
||||
|
||||
<Card padding={0.5}>
|
||||
<ContentAction
|
||||
<LineItemLayout
|
||||
icon={sourceMetadata.icon}
|
||||
title={sourceMetadata.displayName}
|
||||
description={
|
||||
connector.has_oauth_token ? "Connected" : "Not connected"
|
||||
}
|
||||
sizePreset="main-content"
|
||||
variant="section"
|
||||
paddingVariant="sm"
|
||||
rightChildren={
|
||||
connector.has_oauth_token ? (
|
||||
<OpalButton
|
||||
@@ -1508,6 +1466,7 @@ function FederatedConnectorCard({
|
||||
</Button>
|
||||
) : undefined
|
||||
}
|
||||
reducedPadding
|
||||
/>
|
||||
</Card>
|
||||
</>
|
||||
@@ -1556,12 +1515,7 @@ function ConnectorsSettings() {
|
||||
return (
|
||||
<Section gap={2}>
|
||||
<Section gap={0.75} justifyContent="start">
|
||||
<Content
|
||||
title="Connectors"
|
||||
sizePreset="main-content"
|
||||
variant="section"
|
||||
widthVariant="full"
|
||||
/>
|
||||
<InputLayouts.Title title="Connectors" />
|
||||
{hasConnectors ? (
|
||||
<>
|
||||
{/* Indexed Connectors */}
|
||||
|
||||
@@ -7,7 +7,7 @@ import useSWR from "swr";
|
||||
import { errorHandlingFetcher } from "@/lib/fetcher";
|
||||
import * as SettingsLayouts from "@/layouts/settings-layouts";
|
||||
import * as InputLayouts from "@/layouts/input-layouts";
|
||||
import { Section } from "@/layouts/general-layouts";
|
||||
import { Section, LineItemLayout } from "@/layouts/general-layouts";
|
||||
import Card from "@/refresh-components/cards/Card";
|
||||
import Separator from "@/refresh-components/Separator";
|
||||
import SimpleCollapsible from "@/refresh-components/SimpleCollapsible";
|
||||
@@ -400,11 +400,7 @@ function ChatPreferencesForm() {
|
||||
|
||||
{/* Features */}
|
||||
<Section gap={0.75}>
|
||||
<Content
|
||||
title="Features"
|
||||
sizePreset="main-content"
|
||||
variant="section"
|
||||
/>
|
||||
<InputLayouts.Title title="Features" />
|
||||
<Card>
|
||||
<SimpleTooltip
|
||||
tooltip={
|
||||
@@ -464,11 +460,7 @@ function ChatPreferencesForm() {
|
||||
<Section gap={1.5}>
|
||||
{/* Connectors */}
|
||||
<Section gap={0.75}>
|
||||
<Content
|
||||
title="Connectors"
|
||||
sizePreset="main-content"
|
||||
variant="section"
|
||||
/>
|
||||
<InputLayouts.Title title="Connectors" />
|
||||
|
||||
<Section
|
||||
flexDirection="row"
|
||||
|
||||
@@ -31,8 +31,7 @@ import { useCreateModal } from "@/refresh-components/contexts/ModalContext";
|
||||
import ShareAgentModal from "@/sections/modals/ShareAgentModal";
|
||||
import AgentViewerModal from "@/sections/modals/AgentViewerModal";
|
||||
import { toast } from "@/hooks/useToast";
|
||||
import { CardItemLayout } from "@/layouts/general-layouts";
|
||||
import { Content } from "@opal/layouts";
|
||||
import { LineItemLayout, CardItemLayout } from "@/layouts/general-layouts";
|
||||
import { Interactive } from "@opal/core";
|
||||
import { Card } from "@/refresh-components/cards";
|
||||
|
||||
@@ -194,14 +193,12 @@ export default function AgentCard({ agent }: AgentCardProps) {
|
||||
<div className="bg-background-tint-01 p-1 flex flex-row items-end justify-between w-full">
|
||||
{/* Left side - creator and actions */}
|
||||
<div className="flex flex-col gap-1 py-1 px-2">
|
||||
<Content
|
||||
<LineItemLayout
|
||||
icon={SvgUser}
|
||||
title={agent.owner?.email || "Onyx"}
|
||||
sizePreset="secondary"
|
||||
variant="body"
|
||||
prominence="muted"
|
||||
variant="mini"
|
||||
/>
|
||||
<Content
|
||||
<LineItemLayout
|
||||
icon={SvgActions}
|
||||
title={
|
||||
agent.tools.length > 0
|
||||
@@ -210,9 +207,7 @@ export default function AgentCard({ agent }: AgentCardProps) {
|
||||
}`
|
||||
: "No Actions"
|
||||
}
|
||||
sizePreset="secondary"
|
||||
variant="body"
|
||||
prominence="muted"
|
||||
variant="mini"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import React, {
|
||||
useEffect,
|
||||
} from "react";
|
||||
import * as GeneralLayouts from "@/layouts/general-layouts";
|
||||
import { Content } from "@opal/layouts";
|
||||
import * as TableLayouts from "@/layouts/table-layouts";
|
||||
import * as InputLayouts from "@/layouts/input-layouts";
|
||||
import { Card } from "@/refresh-components/cards";
|
||||
@@ -319,11 +318,10 @@ function DocumentSetsTableContent({
|
||||
header: "Name",
|
||||
sortable: true,
|
||||
render: (ds) => (
|
||||
<Content
|
||||
<GeneralLayouts.LineItemLayout
|
||||
icon={SvgFolder}
|
||||
title={ds.name}
|
||||
sizePreset="main-ui"
|
||||
variant="section"
|
||||
variant="secondary"
|
||||
/>
|
||||
),
|
||||
},
|
||||
@@ -445,11 +443,10 @@ function RecentFilesTableContent({
|
||||
header: "Name",
|
||||
sortable: true,
|
||||
render: (file) => (
|
||||
<Content
|
||||
<GeneralLayouts.LineItemLayout
|
||||
icon={SvgFiles}
|
||||
title={file.name}
|
||||
sizePreset="main-ui"
|
||||
variant="section"
|
||||
variant="secondary"
|
||||
/>
|
||||
),
|
||||
},
|
||||
@@ -1138,11 +1135,9 @@ export default function AgentKnowledgePane({
|
||||
|
||||
return (
|
||||
<GeneralLayouts.Section gap={0.5} alignItems="stretch" height="auto">
|
||||
<Content
|
||||
<InputLayouts.Title
|
||||
title="Knowledge"
|
||||
description="Add specific connectors and documents for this agent to use to inform its responses."
|
||||
sizePreset="main-content"
|
||||
variant="section"
|
||||
/>
|
||||
|
||||
<Card>
|
||||
|
||||
@@ -6,8 +6,7 @@ import type { Route } from "next";
|
||||
import { FullPersona } from "@/app/admin/assistants/interfaces";
|
||||
import { useModal } from "@/refresh-components/contexts/ModalContext";
|
||||
import Modal from "@/refresh-components/Modal";
|
||||
import { Section } from "@/layouts/general-layouts";
|
||||
import { Content, ContentAction } from "@opal/layouts";
|
||||
import { Section, LineItemLayout } from "@/layouts/general-layouts";
|
||||
import Text from "@/refresh-components/texts/Text";
|
||||
import AgentAvatar from "@/refresh-components/avatars/AgentAvatar";
|
||||
import Separator from "@/refresh-components/Separator";
|
||||
@@ -27,7 +26,7 @@ import useMcpServersForAgentEditor from "@/hooks/useMcpServersForAgentEditor";
|
||||
import { getActionIcon } from "@/lib/tools/mcpUtils";
|
||||
import { MCPServer, ToolSnapshot } from "@/lib/tools/interfaces";
|
||||
import EmptyMessage from "@/refresh-components/EmptyMessage";
|
||||
import { Horizontal } from "@/layouts/input-layouts";
|
||||
import { Horizontal, Title } from "@/layouts/input-layouts";
|
||||
import Switch from "@/refresh-components/inputs/Switch";
|
||||
import Button from "@/refresh-components/buttons/Button";
|
||||
import { SEARCH_PARAM_NAMES } from "@/app/app/services/searchParams";
|
||||
@@ -37,7 +36,10 @@ import { formatMmDdYyyy } from "@/lib/dateUtils";
|
||||
import { useProjectsContext } from "@/providers/ProjectsContext";
|
||||
import { FileCard } from "@/sections/cards/FileCard";
|
||||
import DocumentSetCard from "@/sections/cards/DocumentSetCard";
|
||||
import { getDisplayName } from "@/lib/llm/utils";
|
||||
import {
|
||||
getLLMProviderOverrideForPersona,
|
||||
getDisplayName,
|
||||
} from "@/lib/llm/utils";
|
||||
import { useLLMProviders } from "@/lib/hooks/useLLMProviders";
|
||||
import { Interactive } from "@opal/core";
|
||||
|
||||
@@ -58,12 +60,11 @@ function ViewerMCPServerCard({ server, tools }: ViewerMCPServerCardProps) {
|
||||
<ExpandableCard.Root isFolded={folded} onFoldedChange={setFolded}>
|
||||
<ExpandableCard.Header>
|
||||
<div className="p-2">
|
||||
<ContentAction
|
||||
<LineItemLayout
|
||||
icon={serverIcon}
|
||||
title={server.name}
|
||||
description={server.description}
|
||||
sizePreset="main-ui"
|
||||
variant="section"
|
||||
variant="secondary"
|
||||
rightChildren={
|
||||
<Button
|
||||
internal
|
||||
@@ -73,6 +74,7 @@ function ViewerMCPServerCard({ server, tools }: ViewerMCPServerCardProps) {
|
||||
{folded ? "Expand" : "Fold"}
|
||||
</Button>
|
||||
}
|
||||
center
|
||||
/>
|
||||
</div>
|
||||
</ExpandableCard.Header>
|
||||
@@ -80,11 +82,10 @@ function ViewerMCPServerCard({ server, tools }: ViewerMCPServerCardProps) {
|
||||
<ActionsLayouts.Content>
|
||||
{tools.map((tool) => (
|
||||
<Section key={tool.id} padding={0.25}>
|
||||
<Content
|
||||
<LineItemLayout
|
||||
title={tool.display_name}
|
||||
description={tool.description}
|
||||
sizePreset="main-ui"
|
||||
variant="section"
|
||||
variant="secondary"
|
||||
/>
|
||||
</Section>
|
||||
))}
|
||||
@@ -103,12 +104,12 @@ function ViewerOpenApiToolCard({ tool }: { tool: ToolSnapshot }) {
|
||||
<ExpandableCard.Root>
|
||||
<ExpandableCard.Header>
|
||||
<div className="p-2">
|
||||
<Content
|
||||
<LineItemLayout
|
||||
icon={SvgActions}
|
||||
title={tool.display_name}
|
||||
description={tool.description}
|
||||
sizePreset="main-ui"
|
||||
variant="section"
|
||||
variant="secondary"
|
||||
center
|
||||
/>
|
||||
</div>
|
||||
</ExpandableCard.Header>
|
||||
@@ -257,27 +258,25 @@ export default function AgentViewerModal({ agent }: AgentViewerModalProps) {
|
||||
{/* Metadata */}
|
||||
<Section flexDirection="row" justifyContent="start">
|
||||
{!agent.is_default_persona && (
|
||||
<Content
|
||||
<LineItemLayout
|
||||
icon={SvgStar}
|
||||
title="Featured"
|
||||
sizePreset="main-ui"
|
||||
variant="body"
|
||||
variant="tertiary"
|
||||
width="fit"
|
||||
/>
|
||||
)}
|
||||
<Content
|
||||
<LineItemLayout
|
||||
icon={SvgUser}
|
||||
title={agent.owner?.email ?? "Onyx"}
|
||||
sizePreset="main-ui"
|
||||
variant="body"
|
||||
prominence="muted"
|
||||
variant="tertiary-muted"
|
||||
width="fit"
|
||||
/>
|
||||
{agent.is_public && (
|
||||
<Content
|
||||
<LineItemLayout
|
||||
icon={SvgOrganization}
|
||||
title="Public to your organization"
|
||||
sizePreset="main-ui"
|
||||
variant="body"
|
||||
prominence="muted"
|
||||
variant="tertiary-muted"
|
||||
width="fit"
|
||||
/>
|
||||
)}
|
||||
</Section>
|
||||
@@ -288,11 +287,7 @@ export default function AgentViewerModal({ agent }: AgentViewerModalProps) {
|
||||
{/* Knowledge */}
|
||||
<Separator noPadding />
|
||||
<Section gap={0.5} alignItems="start">
|
||||
<Content
|
||||
title="Knowledge"
|
||||
sizePreset="main-content"
|
||||
variant="section"
|
||||
/>
|
||||
<Title title="Knowledge" />
|
||||
{hasKnowledge ? (
|
||||
<Section
|
||||
gap={0.5}
|
||||
@@ -345,11 +340,10 @@ export default function AgentViewerModal({ agent }: AgentViewerModalProps) {
|
||||
<SimpleCollapsible.Content>
|
||||
<Section gap={0.5} alignItems="start">
|
||||
{agent.system_prompt && (
|
||||
<Content
|
||||
<LineItemLayout
|
||||
title="Instructions"
|
||||
description={agent.system_prompt}
|
||||
sizePreset="main-ui"
|
||||
variant="section"
|
||||
variant="secondary"
|
||||
/>
|
||||
)}
|
||||
{defaultModel && (
|
||||
@@ -357,7 +351,7 @@ export default function AgentViewerModal({ agent }: AgentViewerModalProps) {
|
||||
title="Default Model"
|
||||
description="This model will be used by Onyx by default in your chats."
|
||||
nonInteractive
|
||||
sizePreset="main-ui"
|
||||
variant="secondary"
|
||||
>
|
||||
<Text>{defaultModel}</Text>
|
||||
</Horizontal>
|
||||
@@ -367,7 +361,7 @@ export default function AgentViewerModal({ agent }: AgentViewerModalProps) {
|
||||
title="Knowledge Cutoff Date"
|
||||
description="Documents with a last-updated date prior to this will be ignored."
|
||||
nonInteractive
|
||||
sizePreset="main-ui"
|
||||
variant="secondary"
|
||||
>
|
||||
<Text mainUiMono>
|
||||
{formatMmDdYyyy(agent.search_start_date)}
|
||||
@@ -378,7 +372,7 @@ export default function AgentViewerModal({ agent }: AgentViewerModalProps) {
|
||||
title="Overwrite System Prompts"
|
||||
description='Remove the base system prompt which includes useful instructions (e.g. "You can use Markdown tables"). This may affect response quality.'
|
||||
nonInteractive
|
||||
sizePreset="main-ui"
|
||||
variant="secondary"
|
||||
>
|
||||
<Switch disabled checked={agent.replace_base_system_prompt} />
|
||||
</Horizontal>
|
||||
@@ -390,12 +384,7 @@ export default function AgentViewerModal({ agent }: AgentViewerModalProps) {
|
||||
{agent.task_prompt && (
|
||||
<>
|
||||
<Separator noPadding />
|
||||
<Content
|
||||
title="Prompt Reminders"
|
||||
description={agent.task_prompt}
|
||||
sizePreset="main-content"
|
||||
variant="section"
|
||||
/>
|
||||
<Title title="Prompt Reminders" description={agent.task_prompt} />
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -403,11 +392,7 @@ export default function AgentViewerModal({ agent }: AgentViewerModalProps) {
|
||||
{agent.starter_messages && agent.starter_messages.length > 0 && (
|
||||
<>
|
||||
<Separator noPadding />
|
||||
<Content
|
||||
title="Conversation Starters"
|
||||
sizePreset="main-content"
|
||||
variant="section"
|
||||
/>
|
||||
<Title title="Conversation Starters" />
|
||||
<div className="grid grid-cols-2 gap-1 w-full">
|
||||
{agent.starter_messages.map((starter, index) => (
|
||||
<Interactive.Base
|
||||
@@ -416,13 +401,10 @@ export default function AgentViewerModal({ agent }: AgentViewerModalProps) {
|
||||
prominence="tertiary"
|
||||
>
|
||||
<Interactive.Container>
|
||||
<Content
|
||||
<LineItemLayout
|
||||
icon={SvgBubbleText}
|
||||
title={starter.message}
|
||||
sizePreset="main-ui"
|
||||
variant="body"
|
||||
prominence="muted"
|
||||
widthVariant="full"
|
||||
variant="tertiary-muted"
|
||||
/>
|
||||
</Interactive.Container>
|
||||
</Interactive.Base>
|
||||
|
||||
Reference in New Issue
Block a user