mirror of
https://github.com/onyx-dot-app/onyx.git
synced 2026-04-13 10:52:42 +00:00
Compare commits
9 Commits
edge
...
nikg/scrol
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30076357e7 | ||
|
|
bb1c44daff | ||
|
|
f26ecafb51 | ||
|
|
9fdb425c0d | ||
|
|
47e20e89c5 | ||
|
|
8b28c127f2 | ||
|
|
9a861a71ad | ||
|
|
b4bc12f6dc | ||
|
|
9af9148ca7 |
@@ -24,13 +24,11 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
|
||||
&& apt-get install -y nodejs \
|
||||
&& install -m 0755 -d /etc/apt/keyrings \
|
||||
&& curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc \
|
||||
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" > /etc/apt/sources.list.d/docker.list \
|
||||
&& curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg -o /etc/apt/keyrings/githubcli-archive-keyring.gpg \
|
||||
&& chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \
|
||||
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" > /etc/apt/sources.list.d/github-cli.list \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y --no-install-recommends docker-ce-cli docker-compose-plugin gh \
|
||||
&& apt-get install -y --no-install-recommends gh \
|
||||
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# fd-find installs as fdfind on Debian/Ubuntu — symlink to fd
|
||||
|
||||
@@ -6,7 +6,7 @@ A containerized development environment for working on Onyx.
|
||||
|
||||
- Ubuntu 26.04 base image
|
||||
- Node.js 20, uv, Claude Code
|
||||
- Docker CLI, GitHub CLI (`gh`)
|
||||
- GitHub CLI (`gh`)
|
||||
- Neovim, ripgrep, fd, fzf, jq, make, wget, unzip
|
||||
- Zsh as default shell (sources host `~/.zshrc` if available)
|
||||
- Python venv auto-activation
|
||||
@@ -73,19 +73,6 @@ user has read/write access to the bind-mounted workspace:
|
||||
To override the auto-detection, set `DEVCONTAINER_REMOTE_USER` before running
|
||||
`ods dev up`.
|
||||
|
||||
## Docker socket
|
||||
|
||||
The container mounts the host's Docker socket so you can run `docker` commands
|
||||
from inside. `ods dev` auto-detects the socket path and sets `DOCKER_SOCK`:
|
||||
|
||||
| Environment | Socket path |
|
||||
| ----------------------- | ------------------------------ |
|
||||
| Linux (rootless Docker) | `$XDG_RUNTIME_DIR/docker.sock` |
|
||||
| macOS (Docker Desktop) | `~/.docker/run/docker.sock` |
|
||||
| Linux (standard Docker) | `/var/run/docker.sock` |
|
||||
|
||||
To override, set `DOCKER_SOCK` before running `ods dev up`.
|
||||
|
||||
## Firewall
|
||||
|
||||
The container starts with a default-deny firewall (`init-firewall.sh`) that only allows outbound traffic to:
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
"image": "onyxdotapp/onyx-devcontainer@sha256:12184169c5bcc9cca0388286d5ffe504b569bc9c37bfa631b76ee8eee2064055",
|
||||
"runArgs": ["--cap-add=NET_ADMIN", "--cap-add=NET_RAW"],
|
||||
"mounts": [
|
||||
"source=${localEnv:DOCKER_SOCK},target=/var/run/docker.sock,type=bind",
|
||||
"source=${localEnv:HOME}/.claude,target=/home/dev/.claude,type=bind",
|
||||
"source=${localEnv:HOME}/.claude.json,target=/home/dev/.claude.json,type=bind",
|
||||
"source=${localEnv:HOME}/.zshrc,target=/home/dev/.zshrc.host,type=bind,readonly",
|
||||
|
||||
@@ -56,9 +56,10 @@ for domain in "${ALLOWED_DOMAINS[@]}"; do
|
||||
done
|
||||
done
|
||||
|
||||
# Detect host network
|
||||
if [[ "${DOCKER_HOST:-}" == "unix://"* ]]; then
|
||||
DOCKER_GATEWAY=$(ip -4 route show | grep "^default" | awk '{print $3}')
|
||||
# Allow traffic to the Docker gateway so the container can reach host services
|
||||
# (e.g. the Onyx stack at localhost:3000, localhost:8080, etc.)
|
||||
DOCKER_GATEWAY=$(ip -4 route show default | awk '{print $3}')
|
||||
if [ -n "$DOCKER_GATEWAY" ]; then
|
||||
if ! ipset add allowed-domains "$DOCKER_GATEWAY/32" -exist 2>&1; then
|
||||
echo "warning: failed to add Docker gateway $DOCKER_GATEWAY to allowlist" >&2
|
||||
fi
|
||||
|
||||
@@ -5,7 +5,7 @@ home: https://www.onyx.app/
|
||||
sources:
|
||||
- "https://github.com/onyx-dot-app/onyx"
|
||||
type: application
|
||||
version: 0.4.43
|
||||
version: 0.4.44
|
||||
appVersion: latest
|
||||
annotations:
|
||||
category: Productivity
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
{{- if and .Values.ingress.enabled .Values.mcpServer.enabled -}}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ include "onyx.fullname" . }}-ingress-mcp-oauth-callback
|
||||
annotations:
|
||||
{{- if not .Values.ingress.className }}
|
||||
kubernetes.io/ingress.class: nginx
|
||||
{{- end }}
|
||||
cert-manager.io/cluster-issuer: {{ include "onyx.fullname" . }}-letsencrypt
|
||||
spec:
|
||||
{{- if .Values.ingress.className }}
|
||||
ingressClassName: {{ .Values.ingress.className }}
|
||||
{{- end }}
|
||||
rules:
|
||||
- host: {{ .Values.ingress.api.host }}
|
||||
http:
|
||||
paths:
|
||||
- path: /mcp/oauth/callback
|
||||
pathType: Exact
|
||||
backend:
|
||||
service:
|
||||
name: {{ include "onyx.fullname" . }}-webserver
|
||||
port:
|
||||
number: {{ .Values.webserver.service.servicePort }}
|
||||
tls:
|
||||
- hosts:
|
||||
- {{ .Values.ingress.api.host }}
|
||||
secretName: {{ include "onyx.fullname" . }}-ingress-mcp-oauth-callback-tls
|
||||
{{- end }}
|
||||
@@ -63,7 +63,7 @@ func checkDevcontainerCLI() {
|
||||
}
|
||||
|
||||
// ensureDockerSock sets the DOCKER_SOCK environment variable if not already set.
|
||||
// devcontainer.json references ${localEnv:DOCKER_SOCK} for the socket mount.
|
||||
// Used by ensureRemoteUser to detect rootless Docker.
|
||||
func ensureDockerSock() {
|
||||
if os.Getenv("DOCKER_SOCK") != "" {
|
||||
return
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import "@opal/components/cards/card/styles.css";
|
||||
import type { PaddingVariants, RoundingVariants } from "@opal/types";
|
||||
import { cardPaddingVariants, cardRoundingVariants } from "@opal/shared";
|
||||
import { paddingVariants, cardRoundingVariants } from "@opal/shared";
|
||||
import { cn } from "@opal/utils";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -79,7 +79,7 @@ function Card({
|
||||
ref,
|
||||
children,
|
||||
}: CardProps) {
|
||||
const padding = cardPaddingVariants[paddingProp];
|
||||
const padding = paddingVariants[paddingProp];
|
||||
const rounding = cardRoundingVariants[roundingProp];
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import "@opal/components/cards/select-card/styles.css";
|
||||
import type { PaddingVariants, RoundingVariants } from "@opal/types";
|
||||
import { cardPaddingVariants, cardRoundingVariants } from "@opal/shared";
|
||||
import { paddingVariants, cardRoundingVariants } from "@opal/shared";
|
||||
import { cn } from "@opal/utils";
|
||||
import { Interactive, type InteractiveStatefulProps } from "@opal/core";
|
||||
|
||||
@@ -78,7 +78,7 @@ function SelectCard({
|
||||
children,
|
||||
...statefulProps
|
||||
}: SelectCardProps) {
|
||||
const padding = cardPaddingVariants[paddingProp];
|
||||
const padding = paddingVariants[paddingProp];
|
||||
const rounding = cardRoundingVariants[roundingProp];
|
||||
|
||||
return (
|
||||
|
||||
@@ -15,6 +15,42 @@ export const Plain: Story = {
|
||||
render: () => <Divider />,
|
||||
};
|
||||
|
||||
export const Vertical: Story = {
|
||||
render: () => (
|
||||
<div
|
||||
style={{ display: "flex", alignItems: "stretch", height: 64, gap: 16 }}
|
||||
>
|
||||
<span>Left</span>
|
||||
<Divider orientation="vertical" />
|
||||
<span>Right</span>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
export const NoPadding: Story = {
|
||||
render: () => <Divider paddingParallel="fit" paddingPerpendicular="fit" />,
|
||||
};
|
||||
|
||||
export const CustomPadding: Story = {
|
||||
render: () => <Divider paddingParallel="lg" paddingPerpendicular="sm" />,
|
||||
};
|
||||
|
||||
export const VerticalNoPadding: Story = {
|
||||
render: () => (
|
||||
<div
|
||||
style={{ display: "flex", alignItems: "stretch", height: 64, gap: 16 }}
|
||||
>
|
||||
<span>Left</span>
|
||||
<Divider
|
||||
orientation="vertical"
|
||||
paddingParallel="fit"
|
||||
paddingPerpendicular="fit"
|
||||
/>
|
||||
<span>Right</span>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
export const WithTitle: Story = {
|
||||
render: () => <Divider title="Section" />,
|
||||
};
|
||||
|
||||
@@ -10,7 +10,13 @@ The component uses a discriminated union with four variants. `title` and `descri
|
||||
|
||||
### Bare divider
|
||||
|
||||
No props — renders a plain horizontal line.
|
||||
A plain line with no title or description.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `orientation` | `"horizontal" \| "vertical"` | `"horizontal"` | Direction of the line |
|
||||
| `paddingParallel` | `PaddingVariants` | `"sm"` | Padding along the line direction (0.5rem) |
|
||||
| `paddingPerpendicular` | `PaddingVariants` | `"xs"` | Padding perpendicular to the line (0.25rem) |
|
||||
|
||||
### Titled divider
|
||||
|
||||
@@ -40,9 +46,18 @@ No props — renders a plain horizontal line.
|
||||
```tsx
|
||||
import { Divider } from "@opal/components";
|
||||
|
||||
// Plain line
|
||||
// Plain horizontal line
|
||||
<Divider />
|
||||
|
||||
// Vertical line
|
||||
<Divider orientation="vertical" />
|
||||
|
||||
// No padding
|
||||
<Divider paddingParallel="fit" paddingPerpendicular="fit" />
|
||||
|
||||
// Custom padding
|
||||
<Divider paddingParallel="lg" paddingPerpendicular="sm" />
|
||||
|
||||
// With title
|
||||
<Divider title="Advanced" />
|
||||
|
||||
|
||||
@@ -2,16 +2,25 @@
|
||||
|
||||
import "@opal/components/divider/styles.css";
|
||||
import { useState, useCallback } from "react";
|
||||
import type { RichStr } from "@opal/types";
|
||||
import type { PaddingVariants, RichStr } from "@opal/types";
|
||||
import { Button, Text } from "@opal/components";
|
||||
import { SvgChevronRight } from "@opal/icons";
|
||||
import { Interactive } from "@opal/core";
|
||||
import { cn } from "@opal/utils";
|
||||
import { paddingXVariants, paddingYVariants } from "@opal/shared";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface DividerNeverFields {
|
||||
interface DividerSharedProps {
|
||||
ref?: React.Ref<HTMLDivElement>;
|
||||
title?: never;
|
||||
description?: never;
|
||||
foldable?: false;
|
||||
orientation?: never;
|
||||
paddingParallel?: never;
|
||||
paddingPerpendicular?: never;
|
||||
open?: never;
|
||||
defaultOpen?: never;
|
||||
onOpenChange?: never;
|
||||
@@ -19,36 +28,37 @@ interface DividerNeverFields {
|
||||
}
|
||||
|
||||
/** Plain line — no title, no description. */
|
||||
interface DividerBareProps extends DividerNeverFields {
|
||||
title?: never;
|
||||
description?: never;
|
||||
foldable?: false;
|
||||
ref?: React.Ref<HTMLDivElement>;
|
||||
}
|
||||
type DividerBareProps = Omit<
|
||||
DividerSharedProps,
|
||||
"orientation" | "paddingParallel" | "paddingPerpendicular"
|
||||
> & {
|
||||
/** Orientation of the line. Default: `"horizontal"`. */
|
||||
orientation?: "horizontal" | "vertical";
|
||||
/** Padding along the line direction. Default: `"sm"` (0.5rem). */
|
||||
paddingParallel?: PaddingVariants;
|
||||
/** Padding perpendicular to the line. Default: `"xs"` (0.25rem). */
|
||||
paddingPerpendicular?: PaddingVariants;
|
||||
};
|
||||
|
||||
/** Line with a title to the left. */
|
||||
interface DividerTitledProps extends DividerNeverFields {
|
||||
type DividerTitledProps = Omit<DividerSharedProps, "title"> & {
|
||||
title: string | RichStr;
|
||||
description?: never;
|
||||
foldable?: false;
|
||||
ref?: React.Ref<HTMLDivElement>;
|
||||
}
|
||||
};
|
||||
|
||||
/** Line with a description below. */
|
||||
interface DividerDescribedProps extends DividerNeverFields {
|
||||
title?: never;
|
||||
type DividerDescribedProps = Omit<DividerSharedProps, "description"> & {
|
||||
/** Description rendered below the divider line. */
|
||||
description: string | RichStr;
|
||||
foldable?: false;
|
||||
ref?: React.Ref<HTMLDivElement>;
|
||||
}
|
||||
};
|
||||
|
||||
/** Foldable — requires title, reveals children. */
|
||||
interface DividerFoldableProps {
|
||||
type DividerFoldableProps = Omit<
|
||||
DividerSharedProps,
|
||||
"title" | "foldable" | "open" | "defaultOpen" | "onOpenChange" | "children"
|
||||
> & {
|
||||
/** Title is required when foldable. */
|
||||
title: string | RichStr;
|
||||
foldable: true;
|
||||
description?: never;
|
||||
/** Controlled open state. */
|
||||
open?: boolean;
|
||||
/** Uncontrolled default open state. */
|
||||
@@ -57,8 +67,7 @@ interface DividerFoldableProps {
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
/** Content revealed when open. */
|
||||
children?: React.ReactNode;
|
||||
ref?: React.Ref<HTMLDivElement>;
|
||||
}
|
||||
};
|
||||
|
||||
type DividerProps =
|
||||
| DividerBareProps
|
||||
@@ -75,12 +84,39 @@ function Divider(props: DividerProps) {
|
||||
return <FoldableDivider {...props} />;
|
||||
}
|
||||
|
||||
const { ref } = props;
|
||||
const title = "title" in props ? props.title : undefined;
|
||||
const description = "description" in props ? props.description : undefined;
|
||||
const {
|
||||
ref,
|
||||
title,
|
||||
description,
|
||||
orientation = "horizontal",
|
||||
paddingParallel = "sm",
|
||||
paddingPerpendicular = "xs",
|
||||
} = props;
|
||||
|
||||
if (orientation === "vertical") {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"opal-divider-vertical",
|
||||
paddingXVariants[paddingPerpendicular],
|
||||
paddingYVariants[paddingParallel]
|
||||
)}
|
||||
>
|
||||
<div className="opal-divider-line-vertical" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={ref} className="opal-divider">
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"opal-divider",
|
||||
paddingXVariants[paddingParallel],
|
||||
paddingYVariants[paddingPerpendicular]
|
||||
)}
|
||||
>
|
||||
<div className="opal-divider-row">
|
||||
{title && (
|
||||
<div className="opal-divider-title">
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
Divider
|
||||
|
||||
A horizontal rule with optional title, foldable chevron, or description.
|
||||
Padding is controlled via Tailwind classes applied by the component.
|
||||
--------------------------------------------------------------------------- */
|
||||
|
||||
/* ── Horizontal ─────────────────────────────────────────────────────────────── */
|
||||
|
||||
.opal-divider {
|
||||
@apply flex flex-col w-full;
|
||||
padding: 0.25rem 0.5rem;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
@@ -29,6 +31,18 @@
|
||||
padding: 0px 2px;
|
||||
}
|
||||
|
||||
/* ── Vertical orientation ───────────────────────────────────────────────────── */
|
||||
|
||||
.opal-divider-vertical {
|
||||
@apply flex flex-row h-full;
|
||||
}
|
||||
|
||||
.opal-divider-line-vertical {
|
||||
@apply flex-1 w-px bg-border-01;
|
||||
}
|
||||
|
||||
/* ── Foldable chevron ───────────────────────────────────────────────────────── */
|
||||
|
||||
.opal-divider-chevron {
|
||||
@apply transition-transform duration-200 ease-in-out;
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ const heightVariants: Record<ExtremaSizeVariants, string> = {
|
||||
// - SelectCard (paddingVariant, roundingVariant)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const cardPaddingVariants: Record<PaddingVariants, string> = {
|
||||
const paddingVariants: Record<PaddingVariants, string> = {
|
||||
lg: "p-6",
|
||||
md: "p-4",
|
||||
sm: "p-2",
|
||||
@@ -109,6 +109,24 @@ const cardPaddingVariants: Record<PaddingVariants, string> = {
|
||||
fit: "p-0",
|
||||
};
|
||||
|
||||
const paddingXVariants: Record<PaddingVariants, string> = {
|
||||
lg: "px-6",
|
||||
md: "px-4",
|
||||
sm: "px-2",
|
||||
xs: "px-1",
|
||||
"2xs": "px-0.5",
|
||||
fit: "px-0",
|
||||
};
|
||||
|
||||
const paddingYVariants: Record<PaddingVariants, string> = {
|
||||
lg: "py-6",
|
||||
md: "py-4",
|
||||
sm: "py-2",
|
||||
xs: "py-1",
|
||||
"2xs": "py-0.5",
|
||||
fit: "py-0",
|
||||
};
|
||||
|
||||
const cardRoundingVariants: Record<RoundingVariants, string> = {
|
||||
lg: "rounded-16",
|
||||
md: "rounded-12",
|
||||
@@ -122,7 +140,9 @@ export {
|
||||
type OverridableExtremaSizeVariants,
|
||||
type SizeVariants,
|
||||
containerSizeVariants,
|
||||
cardPaddingVariants,
|
||||
paddingVariants,
|
||||
paddingXVariants,
|
||||
paddingYVariants,
|
||||
cardRoundingVariants,
|
||||
widthVariants,
|
||||
heightVariants,
|
||||
|
||||
16
web/package-lock.json
generated
16
web/package-lock.json
generated
@@ -47,6 +47,7 @@
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.0.0",
|
||||
"cookies-next": "^5.1.0",
|
||||
"copy-to-clipboard": "^3.3.3",
|
||||
"date-fns": "^3.6.0",
|
||||
"docx-preview": "^0.3.7",
|
||||
"favicon-fetch": "^1.0.0",
|
||||
@@ -8843,6 +8844,15 @@
|
||||
"react": ">= 16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/copy-to-clipboard": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz",
|
||||
"integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"toggle-selection": "^1.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/core-js": {
|
||||
"version": "3.46.0",
|
||||
"hasInstallScript": true,
|
||||
@@ -17426,6 +17436,12 @@
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/toggle-selection": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz",
|
||||
"integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/toposort": {
|
||||
"version": "2.0.2",
|
||||
"license": "MIT"
|
||||
|
||||
@@ -65,6 +65,7 @@
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.0.0",
|
||||
"cookies-next": "^5.1.0",
|
||||
"copy-to-clipboard": "^3.3.3",
|
||||
"date-fns": "^3.6.0",
|
||||
"docx-preview": "^0.3.7",
|
||||
"favicon-fetch": "^1.0.0",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Modal from "@/refresh-components/Modal";
|
||||
import { Button } from "@opal/components";
|
||||
import { CloudEmbeddingModel } from "../../../../components/embedding/interfaces";
|
||||
import { markdown } from "@opal/utils";
|
||||
import { SvgCheck } from "@opal/icons";
|
||||
|
||||
export interface AlreadyPickedModalProps {
|
||||
@@ -17,7 +18,7 @@ export default function AlreadyPickedModal({
|
||||
<Modal.Content width="sm" height="sm">
|
||||
<Modal.Header
|
||||
icon={SvgCheck}
|
||||
title={`${model.model_name} already chosen`}
|
||||
title={markdown(`*${model.model_name}* already chosen`)}
|
||||
description="You can select a different one if you want!"
|
||||
onClose={onClose}
|
||||
/>
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
getFormattedProviderName,
|
||||
} from "@/components/embedding/interfaces";
|
||||
import { EMBEDDING_PROVIDERS_ADMIN_URL } from "@/lib/llmConfig/constants";
|
||||
import { markdown } from "@opal/utils";
|
||||
import { mutate } from "swr";
|
||||
import { SWR_KEYS } from "@/lib/swr-keys";
|
||||
import { testEmbedding } from "@/app/admin/embeddings/pages/utils";
|
||||
@@ -172,9 +173,11 @@ export default function ChangeCredentialsModal({
|
||||
<Modal.Content>
|
||||
<Modal.Header
|
||||
icon={SvgSettings}
|
||||
title={`Modify your ${getFormattedProviderName(
|
||||
provider.provider_type
|
||||
)} ${isProxy ? "Configuration" : "key"}`}
|
||||
title={markdown(
|
||||
`Modify your *${getFormattedProviderName(
|
||||
provider.provider_type
|
||||
)}* ${isProxy ? "configuration" : "key"}`
|
||||
)}
|
||||
onClose={onCancel}
|
||||
/>
|
||||
<Modal.Body>
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
getFormattedProviderName,
|
||||
} from "../../../../components/embedding/interfaces";
|
||||
import { SvgTrash } from "@opal/icons";
|
||||
import { markdown } from "@opal/utils";
|
||||
|
||||
export interface DeleteCredentialsModalProps {
|
||||
modelProvider: CloudEmbeddingProvider;
|
||||
@@ -24,9 +25,11 @@ export default function DeleteCredentialsModal({
|
||||
<Modal.Content width="sm" height="sm">
|
||||
<Modal.Header
|
||||
icon={SvgTrash}
|
||||
title={`Delete ${getFormattedProviderName(
|
||||
modelProvider.provider_type
|
||||
)} Credentials?`}
|
||||
title={markdown(
|
||||
`Delete *${getFormattedProviderName(
|
||||
modelProvider.provider_type
|
||||
)}* credentials?`
|
||||
)}
|
||||
onClose={onCancel}
|
||||
/>
|
||||
<Modal.Body>
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from "@/components/embedding/interfaces";
|
||||
import { EMBEDDING_PROVIDERS_ADMIN_URL } from "@/lib/llmConfig/constants";
|
||||
import Modal from "@/refresh-components/Modal";
|
||||
import { markdown } from "@opal/utils";
|
||||
import { SvgSettings } from "@opal/icons";
|
||||
import SimpleLoader from "@/refresh-components/loaders/SimpleLoader";
|
||||
export interface ProviderCreationModalProps {
|
||||
@@ -185,9 +186,11 @@ export default function ProviderCreationModal({
|
||||
<Modal.Content width="sm" height="sm">
|
||||
<Modal.Header
|
||||
icon={SvgSettings}
|
||||
title={`Configure ${getFormattedProviderName(
|
||||
selectedProvider.provider_type
|
||||
)}`}
|
||||
title={markdown(
|
||||
`Configure *${getFormattedProviderName(
|
||||
selectedProvider.provider_type
|
||||
)}*`
|
||||
)}
|
||||
onClose={onCancel}
|
||||
/>
|
||||
<Modal.Body>
|
||||
|
||||
@@ -2,6 +2,7 @@ import Modal from "@/refresh-components/Modal";
|
||||
import { Button } from "@opal/components";
|
||||
import Text from "@/refresh-components/texts/Text";
|
||||
import { CloudEmbeddingModel } from "@/components/embedding/interfaces";
|
||||
import { markdown } from "@opal/utils";
|
||||
import { SvgServer } from "@opal/icons";
|
||||
|
||||
export interface SelectModelModalProps {
|
||||
@@ -20,7 +21,7 @@ export default function SelectModelModal({
|
||||
<Modal.Content width="sm" height="sm">
|
||||
<Modal.Header
|
||||
icon={SvgServer}
|
||||
title={`Select ${model.model_name}`}
|
||||
title={markdown(`Select *${model.model_name}*`)}
|
||||
onClose={onCancel}
|
||||
/>
|
||||
<Modal.Body>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { toast } from "@/hooks/useToast";
|
||||
import { markdown } from "@opal/utils";
|
||||
|
||||
import EmbeddingModelSelection from "../EmbeddingModelSelectionForm";
|
||||
import { useCallback, useEffect, useMemo, useState, useRef } from "react";
|
||||
@@ -538,7 +539,9 @@ export default function EmbeddingForm() {
|
||||
<Modal.Content>
|
||||
<Modal.Header
|
||||
icon={SvgAlertTriangle}
|
||||
title={`Are you sure you want to select ${selectedProvider.model_name}?`}
|
||||
title={markdown(
|
||||
`Are you sure you want to select *${selectedProvider.model_name}*?`
|
||||
)}
|
||||
onClose={() => setShowPoorModel(false)}
|
||||
/>
|
||||
<Modal.Body>
|
||||
|
||||
@@ -210,8 +210,10 @@ export default function MultiModelResponseView({
|
||||
const response = responses.find((r) => r.modelIndex === modelIndex);
|
||||
if (!response) return;
|
||||
|
||||
// Persist preferred response to backend + update local tree so the
|
||||
// input bar unblocks (awaitingPreferredSelection clears).
|
||||
// Persist preferred response + sync `latestChildNodeId`. Backend's
|
||||
// `set_preferred_response` updates `latest_child_message_id`; if the
|
||||
// frontend chain walk disagrees, the next follow-up fails with
|
||||
// "not on the latest mainline".
|
||||
if (parentMessage?.messageId && response.messageId && currentSessionId) {
|
||||
setPreferredResponse(parentMessage.messageId, response.messageId).catch(
|
||||
(err) => console.error("Failed to persist preferred response:", err)
|
||||
@@ -227,6 +229,7 @@ export default function MultiModelResponseView({
|
||||
updated.set(parentMessage.nodeId, {
|
||||
...userMsg,
|
||||
preferredResponseId: response.messageId,
|
||||
latestChildNodeId: response.nodeId,
|
||||
});
|
||||
updateSessionMessageTree(currentSessionId, updated);
|
||||
}
|
||||
|
||||
@@ -237,22 +237,54 @@ pre[class*="language-"] {
|
||||
}
|
||||
|
||||
/*
|
||||
* Table breakout container - allows tables to extend beyond their parent's
|
||||
* constrained width to use the full container query width (100cqw).
|
||||
*
|
||||
* Requires an ancestor element with `container-type: inline-size` (@container in Tailwind).
|
||||
*
|
||||
* How the math works:
|
||||
* - width: 100cqw → expand to full container query width
|
||||
* - marginLeft: calc((100% - 100cqw) / 2) → negative margin pulls element left
|
||||
* (100% is parent width, 100cqw is larger, so result is negative)
|
||||
* - paddingLeft/Right: calc((100cqw - 100%) / 2) → padding keeps content aligned
|
||||
* with original position while allowing scroll area to extend
|
||||
* Scrollable table container — keeps tables within their parent's width.
|
||||
* Fades edges when content overflows and reveals a scrollbar on hover.
|
||||
*/
|
||||
.markdown-table-breakout {
|
||||
position: relative;
|
||||
overflow-x: auto;
|
||||
width: 100cqw;
|
||||
margin-left: calc((100% - 100cqw) / 2);
|
||||
padding-left: calc((100cqw - 100%) / 2);
|
||||
padding-right: calc((100cqw - 100%) / 2);
|
||||
|
||||
/* Hide scrollbar by default, reveal on hover */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
.markdown-table-breakout::-webkit-scrollbar {
|
||||
height: 6px;
|
||||
}
|
||||
.markdown-table-breakout::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
.markdown-table-breakout::-webkit-scrollbar-thumb {
|
||||
background: transparent;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.markdown-table-breakout:hover {
|
||||
scrollbar-width: thin; /* Firefox */
|
||||
scrollbar-color: var(--border-03) transparent;
|
||||
}
|
||||
.markdown-table-breakout:hover::-webkit-scrollbar-thumb {
|
||||
background: var(--border-03);
|
||||
}
|
||||
|
||||
/* Fade edges when table overflows */
|
||||
.markdown-table-fade {
|
||||
position: relative;
|
||||
}
|
||||
.markdown-table-fade::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 1.5rem;
|
||||
pointer-events: none;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
transparent,
|
||||
var(--background-neutral-01)
|
||||
);
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
.markdown-table-fade[data-overflows="true"]::after {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useMemo, JSX } from "react";
|
||||
import React, { useCallback, useEffect, useRef, useMemo, JSX } from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import remarkMath from "remark-math";
|
||||
@@ -17,6 +17,65 @@ import { transformLinkUri, cn } from "@/lib/utils";
|
||||
import { InMessageImage } from "@/app/app/components/files/images/InMessageImage";
|
||||
import { extractChatImageFileId } from "@/app/app/components/files/images/utils";
|
||||
|
||||
/** Table wrapper that detects horizontal overflow and shows a fade + scrollbar. */
|
||||
interface ScrollableTableProps
|
||||
extends React.TableHTMLAttributes<HTMLTableElement> {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
function ScrollableTable({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: ScrollableTableProps) {
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const wrapRef = useRef<HTMLDivElement>(null);
|
||||
const tableRef = useRef<HTMLTableElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const el = scrollRef.current;
|
||||
const wrap = wrapRef.current;
|
||||
const table = tableRef.current;
|
||||
if (!el || !wrap) return;
|
||||
|
||||
const check = () => {
|
||||
const overflows = el.scrollWidth > el.clientWidth;
|
||||
const atEnd = el.scrollLeft + el.clientWidth >= el.scrollWidth - 2;
|
||||
wrap.dataset.overflows = overflows && !atEnd ? "true" : "false";
|
||||
};
|
||||
|
||||
check();
|
||||
el.addEventListener("scroll", check, { passive: true });
|
||||
// Observe both the scroll container (parent resize) and the table
|
||||
// itself (content growth during streaming).
|
||||
const ro = new ResizeObserver(check);
|
||||
ro.observe(el);
|
||||
if (table) ro.observe(table);
|
||||
|
||||
return () => {
|
||||
el.removeEventListener("scroll", check);
|
||||
ro.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div ref={wrapRef} className="markdown-table-fade">
|
||||
<div ref={scrollRef} className="markdown-table-breakout">
|
||||
<table
|
||||
ref={tableRef}
|
||||
className={cn(
|
||||
className,
|
||||
"min-w-full [&_th]:whitespace-nowrap [&_td]:whitespace-nowrap"
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes content for markdown rendering by handling code blocks and LaTeX
|
||||
*/
|
||||
@@ -127,11 +186,9 @@ export const useMarkdownComponents = (
|
||||
},
|
||||
table: ({ node, className, children, ...props }: any) => {
|
||||
return (
|
||||
<div className="markdown-table-breakout">
|
||||
<table className={cn(className, "min-w-full")} {...props}>
|
||||
{children}
|
||||
</table>
|
||||
</div>
|
||||
<ScrollableTable className={className} {...props}>
|
||||
{children}
|
||||
</ScrollableTable>
|
||||
);
|
||||
},
|
||||
code: ({ node, className, children }: any) => {
|
||||
|
||||
@@ -137,7 +137,7 @@ function DeleteConfirmModal({ hook, onDelete }: DeleteConfirmModalProps) {
|
||||
<Modal.Header
|
||||
// TODO(@raunakab): replace the colour of this SVG with red.
|
||||
icon={SvgTrash}
|
||||
title={`Delete ${hook.name}`}
|
||||
title={markdown(`Delete *${hook.name}*`)}
|
||||
onClose={onClose}
|
||||
/>
|
||||
<Modal.Body>
|
||||
|
||||
@@ -694,6 +694,25 @@ export function useLlmManager(
|
||||
prevAgentIdRef.current = liveAgent?.id;
|
||||
}, [liveAgent?.id]);
|
||||
|
||||
// Clear manual override when arriving at a *different* existing session
|
||||
// from any previously-seen defined session. Tracks only the last
|
||||
// *defined* session id so a round-trip through new-chat (A → undefined
|
||||
// → B) still resets, while A → undefined (new-chat) preserves it.
|
||||
const prevDefinedSessionIdRef = useRef<string | undefined>(undefined);
|
||||
useEffect(() => {
|
||||
const nextId = currentChatSession?.id;
|
||||
if (
|
||||
nextId !== undefined &&
|
||||
prevDefinedSessionIdRef.current !== undefined &&
|
||||
nextId !== prevDefinedSessionIdRef.current
|
||||
) {
|
||||
setUserHasManuallyOverriddenLLM(false);
|
||||
}
|
||||
if (nextId !== undefined) {
|
||||
prevDefinedSessionIdRef.current = nextId;
|
||||
}
|
||||
}, [currentChatSession?.id]);
|
||||
|
||||
function getValidLlmDescriptor(
|
||||
modelName: string | null | undefined
|
||||
): LlmDescriptor {
|
||||
@@ -715,8 +734,9 @@ export function useLlmManager(
|
||||
|
||||
if (llmProviders === undefined || llmProviders === null) {
|
||||
resolved = manualLlm;
|
||||
} else if (userHasManuallyOverriddenLLM && !currentChatSession) {
|
||||
// User has overridden in this session and switched to a new session
|
||||
} else if (userHasManuallyOverriddenLLM) {
|
||||
// Manual override wins over session's `current_alternate_model`.
|
||||
// Cleared on cross-session navigation by the effect above.
|
||||
resolved = manualLlm;
|
||||
} else if (currentChatSession?.current_alternate_model) {
|
||||
resolved = getValidLlmDescriptorForProviders(
|
||||
@@ -728,8 +748,6 @@ export function useLlmManager(
|
||||
liveAgent.llm_model_version_override,
|
||||
llmProviders
|
||||
);
|
||||
} else if (userHasManuallyOverriddenLLM) {
|
||||
resolved = manualLlm;
|
||||
} else if (user?.preferences?.default_model) {
|
||||
resolved = getValidLlmDescriptorForProviders(
|
||||
user.preferences.default_model,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import copy from "copy-to-clipboard";
|
||||
import { Button, ButtonProps } from "@opal/components";
|
||||
import { SvgAlertTriangle, SvgCheck, SvgCopy } from "@opal/icons";
|
||||
|
||||
@@ -40,26 +41,19 @@ export default function CopyIconButton({
|
||||
}
|
||||
|
||||
try {
|
||||
// Check if Clipboard API is available
|
||||
if (!navigator.clipboard) {
|
||||
throw new Error("Clipboard API not available");
|
||||
}
|
||||
|
||||
// If HTML content getter is provided, copy both HTML and plain text
|
||||
if (getHtmlContent) {
|
||||
if (navigator.clipboard && getHtmlContent) {
|
||||
const htmlContent = getHtmlContent();
|
||||
const clipboardItem = new ClipboardItem({
|
||||
"text/html": new Blob([htmlContent], { type: "text/html" }),
|
||||
"text/plain": new Blob([text], { type: "text/plain" }),
|
||||
});
|
||||
await navigator.clipboard.write([clipboardItem]);
|
||||
}
|
||||
// Default: plain text only
|
||||
else {
|
||||
} else if (navigator.clipboard) {
|
||||
await navigator.clipboard.writeText(text);
|
||||
} else if (!copy(text)) {
|
||||
throw new Error("copy-to-clipboard returned false");
|
||||
}
|
||||
|
||||
// Show "copied" state
|
||||
setCopyState("copied");
|
||||
} catch (err) {
|
||||
console.error("Failed to copy:", err);
|
||||
|
||||
@@ -159,9 +159,12 @@ export default function ModelSelector({
|
||||
);
|
||||
|
||||
if (!isMultiModel) {
|
||||
// Stable key — keying on model would unmount the pill
|
||||
// on change and leave Radix's anchorRef detached,
|
||||
// flashing the closing popover at (0,0).
|
||||
return (
|
||||
<OpenButton
|
||||
key={modelKey(model.provider, model.modelName)}
|
||||
key="single-model-pill"
|
||||
icon={ProviderIcon}
|
||||
onClick={(e: React.MouseEvent) =>
|
||||
handlePillClick(index, e.currentTarget as HTMLElement)
|
||||
|
||||
@@ -425,16 +425,27 @@ export default function AppPage({ firstMessage }: ChatPageProps) {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [multiModel.isMultiModelActive]);
|
||||
|
||||
// Sync single-model selection to llmManager so the submission path
|
||||
// uses the correct provider/version (replaces the old LLMPopover sync).
|
||||
// Sync single-model selection to llmManager so the submission path uses
|
||||
// the correct provider/version. Guard against echoing derived state back
|
||||
// — only call updateCurrentLlm when the selection actually differs from
|
||||
// currentLlm, otherwise the initial [] → [currentLlmModel] sync would
|
||||
// pin `userHasManuallyOverriddenLLM=true` with whatever was resolved
|
||||
// first (often the default model before the session's alt_model loads).
|
||||
useEffect(() => {
|
||||
if (multiModel.selectedModels.length === 1) {
|
||||
const model = multiModel.selectedModels[0]!;
|
||||
llmManager.updateCurrentLlm({
|
||||
name: model.name,
|
||||
provider: model.provider,
|
||||
modelName: model.modelName,
|
||||
});
|
||||
const current = llmManager.currentLlm;
|
||||
if (
|
||||
model.provider !== current.provider ||
|
||||
model.modelName !== current.modelName ||
|
||||
model.name !== current.name
|
||||
) {
|
||||
llmManager.updateCurrentLlm({
|
||||
name: model.name,
|
||||
provider: model.provider,
|
||||
modelName: model.modelName,
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [multiModel.selectedModels]);
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ 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 { markdown } from "@opal/utils";
|
||||
import { Formik, Form } from "formik";
|
||||
import * as Yup from "yup";
|
||||
import {
|
||||
@@ -1556,7 +1557,7 @@ function FederatedConnectorCard({
|
||||
{showDisconnectConfirmation && (
|
||||
<ConfirmationModalLayout
|
||||
icon={SvgUnplug}
|
||||
title={`Disconnect ${sourceMetadata.displayName}`}
|
||||
title={markdown(`Disconnect *${sourceMetadata.displayName}*`)}
|
||||
onClose={() => setShowDisconnectConfirmation(false)}
|
||||
submit={
|
||||
<Button
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useCallback, useState } from "react";
|
||||
import { Button } from "@opal/components";
|
||||
// TODO(@raunakab): migrate to Opal LineItemButton once it supports danger variant
|
||||
import LineItem from "@/refresh-components/buttons/LineItem";
|
||||
import { cn } from "@opal/utils";
|
||||
import { cn, markdown } from "@opal/utils";
|
||||
import {
|
||||
SvgMoreHorizontal,
|
||||
SvgEdit,
|
||||
@@ -341,7 +341,7 @@ export default function AgentRowActions({
|
||||
{unlistOpen && (
|
||||
<ConfirmationModalLayout
|
||||
icon={SvgEyeOff}
|
||||
title={`Unlist ${agent.name}`}
|
||||
title={markdown(`Unlist *${agent.name}*`)}
|
||||
onClose={isSubmitting ? undefined : () => setUnlistOpen(false)}
|
||||
submit={
|
||||
<Button
|
||||
|
||||
@@ -347,7 +347,7 @@ export default function ImageGenerationContent() {
|
||||
{disconnectProvider && (
|
||||
<ConfirmationModalLayout
|
||||
icon={SvgUnplug}
|
||||
title={`Disconnect ${disconnectProvider.title}`}
|
||||
title={markdown(`Disconnect *${disconnectProvider.title}*`)}
|
||||
description="This will remove the stored credentials for this provider."
|
||||
onClose={() => {
|
||||
setDisconnectProvider(null);
|
||||
|
||||
@@ -201,7 +201,7 @@ function VoiceDisconnectModal({
|
||||
return (
|
||||
<ConfirmationModalLayout
|
||||
icon={SvgUnplug}
|
||||
title={`Disconnect ${disconnectTarget.providerLabel}`}
|
||||
title={markdown(`Disconnect *${disconnectTarget.providerLabel}*`)}
|
||||
description="Voice models"
|
||||
onClose={onClose}
|
||||
submit={
|
||||
|
||||
@@ -9,6 +9,7 @@ import Modal from "@/refresh-components/Modal";
|
||||
import { Button } from "@opal/components";
|
||||
|
||||
import { SvgArrowExchange } from "@opal/icons";
|
||||
import { markdown } from "@opal/utils";
|
||||
import { SvgOnyxLogo } from "@opal/logos";
|
||||
import type { IconProps } from "@opal/types";
|
||||
|
||||
@@ -81,7 +82,7 @@ export const WebProviderSetupModal = memo(
|
||||
<Modal.Content width="sm" preventAccidentalClose>
|
||||
<Modal.Header
|
||||
icon={LogoArrangement}
|
||||
title={`Set up ${providerLabel}`}
|
||||
title={markdown(`Set up *${providerLabel}*`)}
|
||||
description={description}
|
||||
onClose={onClose}
|
||||
/>
|
||||
|
||||
@@ -7,6 +7,7 @@ import Text from "@/refresh-components/texts/Text";
|
||||
import { Section } from "@/layouts/general-layouts";
|
||||
import * as SettingsLayouts from "@/layouts/settings-layouts";
|
||||
import { Content, Card } from "@opal/layouts";
|
||||
import { markdown } from "@opal/utils";
|
||||
import useSWR from "swr";
|
||||
import { errorHandlingFetcher, FetchError } from "@/lib/fetcher";
|
||||
import { SWR_KEYS } from "@/lib/swr-keys";
|
||||
@@ -146,7 +147,7 @@ function WebSearchDisconnectModal({
|
||||
return (
|
||||
<ConfirmationModalLayout
|
||||
icon={SvgUnplug}
|
||||
title={`Disconnect ${disconnectTarget.label}`}
|
||||
title={markdown(`Disconnect *${disconnectTarget.label}*`)}
|
||||
description="This will remove the stored credentials for this provider."
|
||||
onClose={onClose}
|
||||
submit={
|
||||
|
||||
@@ -5,6 +5,7 @@ import Modal from "@/refresh-components/Modal";
|
||||
import { Button } from "@opal/components";
|
||||
import Text from "@/refresh-components/texts/Text";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { markdown } from "@opal/utils";
|
||||
import { SvgUnplug } from "@opal/icons";
|
||||
interface DisconnectEntityModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -51,7 +52,7 @@ export default function DisconnectEntityModal({
|
||||
icon={({ className }) => (
|
||||
<SvgUnplug className={cn(className, "stroke-action-danger-05")} />
|
||||
)}
|
||||
title={`Disconnect ${name}`}
|
||||
title={markdown(`Disconnect *${name}*`)}
|
||||
onClose={onClose}
|
||||
/>
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import InputSelect from "@/refresh-components/inputs/InputSelect";
|
||||
import InputTypeIn from "@/refresh-components/inputs/InputTypeIn";
|
||||
import PasswordInputTypeIn from "@/refresh-components/inputs/PasswordInputTypeIn";
|
||||
import { Button } from "@opal/components";
|
||||
import { markdown } from "@opal/utils";
|
||||
import CopyIconButton from "@/refresh-components/buttons/CopyIconButton";
|
||||
import Text from "@/refresh-components/texts/Text";
|
||||
import { Formik, Form } from "formik";
|
||||
@@ -317,7 +318,11 @@ export default function MCPAuthenticationModal({
|
||||
<Modal.Content width="sm" height="lg" skipOverlay={skipOverlay}>
|
||||
<Modal.Header
|
||||
icon={SvgArrowExchange}
|
||||
title={`Authenticate ${mcpServer?.name || "MCP Server"}`}
|
||||
title={
|
||||
mcpServer
|
||||
? markdown(`Authenticate *${mcpServer.name}*`)
|
||||
: "Authenticate MCP Server"
|
||||
}
|
||||
description="Authenticate your connection to start using the MCP server."
|
||||
/>
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import React, { useEffect, useRef, useState } from "react";
|
||||
import { Formik, Form, useFormikContext } from "formik";
|
||||
import type { FormikConfig } from "formik";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { markdown } from "@opal/utils";
|
||||
import { Interactive } from "@opal/core";
|
||||
import { usePaidEnterpriseFeaturesEnabled } from "@/components/settings/usePaidEnterpriseFeaturesEnabled";
|
||||
import { useAgents } from "@/hooks/useAgents";
|
||||
@@ -720,7 +721,7 @@ function ModalWrapperInner({
|
||||
} = getProvider(providerName);
|
||||
|
||||
const title = llmProvider
|
||||
? `Configure "${llmProvider.name}"`
|
||||
? markdown(`Configure *${llmProvider.name}*`)
|
||||
: `Set up ${providerProductName}`;
|
||||
const description =
|
||||
descriptionOverride ??
|
||||
|
||||
Reference in New Issue
Block a user