Compare commits

..

2 Commits

Author SHA1 Message Date
Bo-Onyx
6604d8a7a6 address comments 2026-03-24 10:03:17 -07:00
Bo-Onyx
6306fc5200 feat(hook): Add frontend feature control and admin hook page 2026-03-24 10:01:53 -07:00
39 changed files with 471 additions and 145 deletions

View File

@@ -6,4 +6,3 @@
3134e5f840c12c8f32613ce520101a047c89dcc2 # refactor(whitespace): rm temporary react fragments (#7161)
ed3f72bc75f3e3a9ae9e4d8cd38278f9c97e78b4 # refactor(whitespace): rm react fragment #7190
7b927e79c25f4ddfd18a067f489e122acd2c89de # chore(format): format files where `ruff` and `black` agree (#9339)

View File

@@ -26,6 +26,8 @@ class DocumentIngestionSpec(HookPointSpec):
default_timeout_seconds = 30.0
fail_hard_description = "The document will not be indexed."
default_fail_strategy = HookFailStrategy.HARD
# TODO(Bo-Onyx): update later
docs_url = "https://docs.google.com/document/d/1pGhB8Wcnhhj8rS4baEJL6CX05yFhuIDNk1gbBRiWu94/edit?tab=t.ue263ual5vdi"
payload_model = DocumentIngestionPayload
response_model = DocumentIngestionResponse

View File

@@ -65,6 +65,8 @@ class QueryProcessingSpec(HookPointSpec):
"The query will be blocked and the user will see an error message."
)
default_fail_strategy = HookFailStrategy.HARD
# TODO(Bo-Onyx): update later
docs_url = "https://docs.google.com/document/d/1pGhB8Wcnhhj8rS4baEJL6CX05yFhuIDNk1gbBRiWu94/edit?tab=t.g2r1a1699u87"
payload_model = QueryProcessingPayload
response_model = QueryProcessingResponse

View File

@@ -17,6 +17,7 @@ from onyx.db.models import User
from onyx.db.notification import dismiss_all_notifications
from onyx.db.notification import get_notifications
from onyx.db.notification import update_notification_last_shown
from onyx.hooks.utils import HOOKS_AVAILABLE
from onyx.key_value_store.factory import get_kv_store
from onyx.key_value_store.interface import KvKeyNotFoundError
from onyx.server.features.build.utils import is_onyx_craft_enabled
@@ -80,6 +81,7 @@ def fetch_settings(
needs_reindexing=needs_reindexing,
onyx_craft_enabled=onyx_craft_enabled_for_user,
vector_db_enabled=not DISABLE_VECTOR_DB,
hooks_enabled=HOOKS_AVAILABLE,
version=onyx_version,
)

View File

@@ -104,5 +104,7 @@ class UserSettings(Settings):
# False when DISABLE_VECTOR_DB is set — connectors, RAG search, and
# document sets are unavailable.
vector_db_enabled: bool = True
# True when hooks are available: single-tenant deployment with HOOK_ENABLED=true.
hooks_enabled: bool = False
# Application version, read from the ONYX_VERSION env var at startup.
version: str | None = None

View File

@@ -76,7 +76,7 @@ function Button({
) : null;
const button = (
<Interactive.Stateless type={type} {...interactiveProps}>
<Interactive.Stateless {...interactiveProps}>
<Interactive.Container
type={type}
border={interactiveProps.prominence === "secondary"}

View File

@@ -4,7 +4,7 @@ import React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cn } from "@opal/utils";
import { useDisabled } from "@opal/core/disabled/components";
import type { ButtonType, WithoutStyles } from "@opal/types";
import type { WithoutStyles } from "@opal/types";
// ---------------------------------------------------------------------------
// Types
@@ -63,13 +63,6 @@ interface InteractiveStatefulProps
*/
group?: string;
/**
* HTML button type. When set to `"submit"`, `"button"`, or `"reset"`, the
* element is treated as inherently interactive for cursor styling purposes
* even without an explicit `onClick` or `href`.
*/
type?: ButtonType;
/**
* URL to navigate to when clicked. Passed through Slot to the child.
*/
@@ -101,7 +94,6 @@ function InteractiveStateful({
state = "empty",
interaction = "rest",
group,
type,
href,
target,
...props
@@ -112,7 +104,7 @@ function InteractiveStateful({
// so Radix Slot-injected handlers don't bypass this guard.
const classes = cn(
"interactive",
!props.onClick && !href && !type && "!cursor-default !select-auto",
!props.onClick && !href && "!cursor-default !select-auto",
group
);

View File

@@ -4,7 +4,7 @@ import React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cn } from "@opal/utils";
import { useDisabled } from "@opal/core/disabled/components";
import type { ButtonType, WithoutStyles } from "@opal/types";
import type { WithoutStyles } from "@opal/types";
// ---------------------------------------------------------------------------
// Types
@@ -53,13 +53,6 @@ interface InteractiveStatelessProps
*/
group?: string;
/**
* HTML button type. When set to `"submit"`, `"button"`, or `"reset"`, the
* element is treated as inherently interactive for cursor styling purposes
* even without an explicit `onClick` or `href`.
*/
type?: ButtonType;
/**
* URL to navigate to when clicked. Passed through Slot to the child.
*/
@@ -92,7 +85,6 @@ function InteractiveStateless({
prominence = "primary",
interaction = "rest",
group,
type,
href,
target,
...props
@@ -103,7 +95,7 @@ function InteractiveStateless({
// so Radix Slot-injected handlers don't bypass this guard.
const classes = cn(
"interactive",
!props.onClick && !href && !type && "!cursor-default !select-auto",
!props.onClick && !href && "!cursor-default !select-auto",
group
);

View File

@@ -0,0 +1,21 @@
import type { IconProps } from "@opal/types";
const SvgFileBroadcast = ({ size, ...props }: IconProps) => (
<svg
width={size}
height={size}
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
stroke="currentColor"
{...props}
>
<path
d="M6.1875 2.25003H2.625C1.808 2.25003 1.125 2.93303 1.125 3.75003L1.125 14.25C1.125 15.067 1.808 15.75 2.625 15.75L9.37125 15.75C10.1883 15.75 10.8713 15.067 10.8713 14.25L10.8713 6.94128M6.1875 2.25003L10.8713 6.94128M6.1875 2.25003V6.94128H10.8713M10.3069 2.25L13.216 5.15914C13.6379 5.5811 13.875 6.15339 13.875 6.75013V13.875C13.875 14.5212 13.737 15.2081 13.4392 15.7538M16.4391 15.7538C16.737 15.2081 16.875 14.5213 16.875 13.8751L16.875 7.02481C16.875 5.53418 16.2833 4.10451 15.23 3.04982L14.4301 2.25003"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
export default SvgFileBroadcast;

View File

@@ -0,0 +1,21 @@
import type { IconProps } from "@opal/types";
const SvgHookNodes = ({ size, ...props }: IconProps) => (
<svg
width={size}
height={size}
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
stroke="currentColor"
{...props}
>
<path
d="M10.0002 4C10.0002 3.99708 10.0002 3.99415 10.0001 3.99123C9.99542 2.8907 9.10181 2 8.00016 2C6.89559 2 6.00016 2.89543 6.00016 4C6.00016 4.73701 6.39882 5.38092 6.99226 5.72784L4.67276 9.70412M11.6589 13.7278C11.9549 13.9009 12.2993 14 12.6668 14C13.7714 14 14.6668 13.1046 14.6668 12C14.6668 10.8954 13.7714 10 12.6668 10C12.2993 10 11.9549 10.0991 11.6589 10.2722L9.33943 6.29588M2.33316 10.2678C1.73555 10.6136 1.3335 11.2599 1.3335 12C1.3335 13.1046 2.22893 14 3.3335 14C4.43807 14 5.3335 13.1046 5.3335 12H10.0002"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
export default SvgHookNodes;

View File

@@ -69,8 +69,9 @@ export { default as SvgExternalLink } from "@opal/icons/external-link";
export { default as SvgEye } from "@opal/icons/eye";
export { default as SvgEyeClosed } from "@opal/icons/eye-closed";
export { default as SvgEyeOff } from "@opal/icons/eye-off";
export { default as SvgFiles } from "@opal/icons/files";
export { default as SvgFileBraces } from "@opal/icons/file-braces";
export { default as SvgFileBroadcast } from "@opal/icons/file-broadcast";
export { default as SvgFiles } from "@opal/icons/files";
export { default as SvgFileChartPie } from "@opal/icons/file-chart-pie";
export { default as SvgFileSmall } from "@opal/icons/file-small";
export { default as SvgFileText } from "@opal/icons/file-text";
@@ -90,6 +91,7 @@ export { default as SvgHashSmall } from "@opal/icons/hash-small";
export { default as SvgHash } from "@opal/icons/hash";
export { default as SvgHeadsetMic } from "@opal/icons/headset-mic";
export { default as SvgHistory } from "@opal/icons/history";
export { default as SvgHookNodes } from "@opal/icons/hook-nodes";
export { default as SvgHourglass } from "@opal/icons/hourglass";
export { default as SvgImage } from "@opal/icons/image";
export { default as SvgImageSmall } from "@opal/icons/image-small";

View File

@@ -86,15 +86,6 @@ export interface IconProps extends SVGProps<SVGSVGElement> {
/** Strips `className` and `style` from a props type to enforce design-system styling. */
export type WithoutStyles<T> = Omit<T, "className" | "style">;
/**
* HTML button `type` attribute values.
*
* Used by interactive primitives and button-like components to indicate that
* the element is inherently interactive for cursor-styling purposes, even
* without an explicit `onClick` or `href`.
*/
export type ButtonType = "submit" | "button" | "reset";
/** Like `Omit` but distributes over union types, preserving discriminated unions. */
export type DistributiveOmit<T, K extends keyof any> = T extends any
? Omit<T, K>

View File

@@ -0,0 +1 @@
export { default } from "@/refresh-pages/admin/HooksPage";

View File

@@ -0,0 +1,15 @@
"use client";
import useSWR from "swr";
import { errorHandlingFetcher } from "@/lib/fetcher";
import { HookPointMeta } from "@/refresh-pages/admin/HooksPage/interfaces";
export function useHookSpecs() {
const { data, isLoading, error } = useSWR<HookPointMeta[]>(
"/api/admin/hooks/specs",
errorHandlingFetcher,
{ revalidateOnFocus: false }
);
return { specs: data, isLoading, error };
}

View File

@@ -123,9 +123,6 @@ export interface LLMProviderFormProps {
open?: boolean;
onOpenChange?: (open: boolean) => void;
/** The current default model name for this provider (from the global default). */
defaultModelName?: string;
// Onboarding-specific (only when variant === "onboarding")
onboardingState?: OnboardingState;
onboardingActions?: OnboardingActions;

View File

@@ -63,6 +63,9 @@ export interface Settings {
// are unavailable.
vector_db_enabled?: boolean;
// True when hooks are available: single-tenant deployment with HOOK_ENABLED=true.
hooks_enabled?: boolean;
// Application version from the ONYX_VERSION env var on the server.
version?: string | null;
}

View File

@@ -4,6 +4,7 @@ import {
SvgActivity,
SvgArrowExchange,
SvgAudio,
SvgHookNodes,
SvgBarChart,
SvgBookOpen,
SvgBubbleText,
@@ -227,6 +228,12 @@ export const ADMIN_ROUTES = {
title: "Document Index Migration",
sidebarLabel: "Document Index Migration",
},
HOOKS: {
path: "/admin/hooks",
icon: SvgHookNodes,
title: "Hook Extensions",
sidebarLabel: "Hook Extensions",
},
SCIM: {
path: "/admin/scim",
icon: SvgUserSync,

View File

@@ -1,7 +1,7 @@
"use client";
import React from "react";
import type { ButtonType, IconFunctionComponent, IconProps } from "@opal/types";
import type { IconFunctionComponent, IconProps } from "@opal/types";
import type { Route } from "next";
import { Interactive } from "@opal/core";
import { ContentAction } from "@opal/layouts";
@@ -18,7 +18,6 @@ export interface SidebarTabProps {
// Button properties:
onClick?: React.MouseEventHandler<HTMLElement>;
href?: string;
type?: ButtonType;
icon?: React.FunctionComponent<IconProps>;
children?: React.ReactNode;
rightChildren?: React.ReactNode;
@@ -32,7 +31,6 @@ export default function SidebarTab({
onClick,
href,
type,
icon,
rightChildren,
children,
@@ -60,14 +58,12 @@ export default function SidebarTab({
variant="sidebar"
state={selected ? "selected" : "empty"}
onClick={onClick}
type="button"
group="group/SidebarTab"
>
<Interactive.Container
roundingVariant="compact"
heightVariant="lg"
widthVariant="full"
type={type}
>
{href && (
<Link

View File

@@ -0,0 +1,59 @@
import type { Meta, StoryObj } from "@storybook/react";
import React from "react";
import InputSearch from "./InputSearch";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
const meta: Meta<typeof InputSearch> = {
title: "refresh-components/inputs/InputSearch",
component: InputSearch,
tags: ["autodocs"],
decorators: [
(Story) => (
<TooltipPrimitive.Provider>
<div style={{ width: 320 }}>
<Story />
</div>
</TooltipPrimitive.Provider>
),
],
};
export default meta;
type Story = StoryObj<typeof InputSearch>;
export const Default: Story = {
render: function DefaultStory() {
const [value, setValue] = React.useState("");
return (
<InputSearch
placeholder="Search..."
value={value}
onChange={(e) => setValue(e.target.value)}
/>
);
},
};
export const WithValue: Story = {
render: function WithValueStory() {
const [value, setValue] = React.useState("Search Value");
return (
<InputSearch
placeholder="Search..."
value={value}
onChange={(e) => setValue(e.target.value)}
/>
);
},
};
export const Disabled: Story = {
render: () => (
<InputSearch
placeholder="Search..."
value=""
onChange={() => {}}
disabled
/>
),
};

View File

@@ -0,0 +1,70 @@
"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
import InputTypeIn, {
InputTypeInProps,
} from "@/refresh-components/inputs/InputTypeIn";
/**
* InputSearch Component
*
* A subtle search input that follows the "Subtle Input Styles" spec:
* no border by default, border appears on hover/focus/active.
*
* @example
* ```tsx
* // Basic usage
* <InputSearch
* placeholder="Search..."
* value={search}
* onChange={(e) => setSearch(e.target.value)}
* />
*
* // Disabled state
* <InputSearch
* disabled
* placeholder="Search..."
* value=""
* onChange={() => {}}
* />
* ```
*/
export interface InputSearchProps
extends Omit<InputTypeInProps, "variant" | "leftSearchIcon"> {
/**
* Ref to the underlying input element.
*/
ref?: React.Ref<HTMLInputElement>;
/**
* Whether the input is disabled.
*/
disabled?: boolean;
}
export default function InputSearch({
ref,
disabled,
className,
...props
}: InputSearchProps) {
return (
<InputTypeIn
ref={ref}
variant={disabled ? "disabled" : "internal"}
leftSearchIcon
className={cn(
"[&_input]:font-main-ui-muted [&_input]:text-text-02 [&_input]:placeholder:text-text-02",
!disabled && [
"border border-transparent",
"hover:border-border-03",
"active:border-border-05",
"focus-within:shadow-[0px_0px_0px_2px_var(--background-tint-04)]",
"focus-within:hover:border-border-03",
],
className
)}
{...props}
/>
);
}

View File

@@ -0,0 +1,110 @@
"use client";
import { useState, useEffect } from "react";
import { toast } from "@/hooks/useToast";
import { useHookSpecs } from "@/hooks/useHookSpecs";
import SimpleLoader from "@/refresh-components/loaders/SimpleLoader";
import { ContentAction } from "@opal/layouts";
import { Button } from "@opal/components";
import InputSearch from "@/refresh-components/inputs/InputSearch";
import Card from "@/refresh-components/cards/Card";
import Text from "@/refresh-components/texts/Text";
import {
SvgArrowExchange,
SvgBubbleText,
SvgExternalLink,
SvgFileBroadcast,
SvgHookNodes,
} from "@opal/icons";
import { IconFunctionComponent } from "@opal/types";
const HOOK_POINT_ICONS: Record<string, IconFunctionComponent> = {
document_ingestion: SvgFileBroadcast,
query_processing: SvgBubbleText,
};
function getHookPointIcon(hookPoint: string): IconFunctionComponent {
return HOOK_POINT_ICONS[hookPoint] ?? SvgHookNodes;
}
export default function HooksContent() {
const [search, setSearch] = useState("");
const { specs, isLoading, error } = useHookSpecs();
useEffect(() => {
if (error) {
console.error("Failed to load hook specs", error);
toast.error("Failed to load hook specs");
}
}, [error]);
if (isLoading) {
return <SimpleLoader />;
}
const filtered = (specs ?? []).filter(
(spec) =>
spec.display_name.toLowerCase().includes(search.toLowerCase()) ||
spec.description.toLowerCase().includes(search.toLowerCase())
);
return (
<div className="flex flex-col gap-6">
<InputSearch
placeholder="Search hooks..."
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<div className="flex flex-col gap-2">
{filtered.length === 0 ? (
<Text text03 secondaryBody>
{search
? "No hooks match your search."
: "No hook points are available."}
</Text>
) : (
filtered.map((spec) => (
<Card
key={spec.hook_point}
variant="secondary"
padding={0.5}
gap={0}
>
<ContentAction
icon={getHookPointIcon(spec.hook_point)}
title={spec.display_name}
description={spec.description}
sizePreset="main-content"
variant="section"
paddingVariant="fit"
rightChildren={
// TODO(Bo-Onyx): wire up Connect — open modal to create/edit hook
<Button prominence="tertiary" rightIcon={SvgArrowExchange}>
Connect
</Button>
}
/>
{spec.docs_url && (
<div className="pl-7 pt-1">
<a
href={spec.docs_url}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1 w-fit text-text-03"
>
<Text as="span" secondaryBody text03 className="underline">
Documentation
</Text>
<SvgExternalLink size={16} className="text-text-02" />
</a>
</div>
)}
</Card>
))
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,42 @@
"use client";
import { useEffect } from "react";
import { useRouter } from "next/navigation";
import * as SettingsLayouts from "@/layouts/settings-layouts";
import { ADMIN_ROUTES } from "@/lib/admin-routes";
import { useSettingsContext } from "@/providers/SettingsProvider";
import { toast } from "@/hooks/useToast";
import SimpleLoader from "@/refresh-components/loaders/SimpleLoader";
import HooksContent from "./HooksContent";
const route = ADMIN_ROUTES.HOOKS;
export default function HooksPage() {
const router = useRouter();
const { settings, settingsLoading } = useSettingsContext();
useEffect(() => {
if (!settingsLoading && !settings.hooks_enabled) {
toast.error("This page needs to be enabled");
router.replace("/");
}
}, [settingsLoading, settings.hooks_enabled, router]);
if (settingsLoading || !settings.hooks_enabled) {
return <SimpleLoader />;
}
return (
<SettingsLayouts.Root>
<SettingsLayouts.Header
icon={route.icon}
title={route.title}
description="Extend Onyx pipelines by registering external API endpoints as callbacks at predefined hook points."
separator
/>
<SettingsLayouts.Body>
<HooksContent />
</SettingsLayouts.Body>
</SettingsLayouts.Root>
);
}

View File

@@ -0,0 +1,63 @@
export type HookPoint = string;
export type HookFailStrategy = "hard" | "soft";
export interface HookPointMeta {
hook_point: HookPoint;
display_name: string;
description: string;
docs_url: string | null;
input_schema: Record<string, unknown>;
output_schema: Record<string, unknown>;
default_timeout_seconds: number;
default_fail_strategy: HookFailStrategy;
fail_hard_description: string;
}
export interface HookResponse {
id: number;
name: string;
hook_point: HookPoint;
endpoint_url: string | null;
fail_strategy: HookFailStrategy;
timeout_seconds: number;
is_active: boolean;
is_reachable: boolean | null;
creator_email: string | null;
created_at: string;
updated_at: string;
}
export interface HookCreateRequest {
name: string;
hook_point: HookPoint;
endpoint_url: string;
api_key?: string;
fail_strategy?: HookFailStrategy;
timeout_seconds?: number;
}
export interface HookUpdateRequest {
name?: string;
endpoint_url?: string;
api_key?: string | null;
fail_strategy?: HookFailStrategy;
timeout_seconds?: number;
}
export interface HookExecutionRecord {
error_message: string | null;
status_code: number | null;
duration_ms: number | null;
created_at: string;
}
export type HookValidateStatus =
| "passed"
| "auth_failed"
| "timeout"
| "cannot_connect";
export interface HookValidateResponse {
status: HookValidateStatus;
error_message: string | null;
}

View File

@@ -148,14 +148,12 @@ interface ExistingProviderCardProps {
provider: LLMProviderView;
isDefault: boolean;
isLastProvider: boolean;
defaultModelName?: string;
}
function ExistingProviderCard({
provider,
isDefault,
isLastProvider,
defaultModelName,
}: ExistingProviderCardProps) {
const { mutate } = useSWRConfig();
const [isOpen, setIsOpen] = useState(false);
@@ -232,12 +230,7 @@ function ExistingProviderCard({
</Section>
}
/>
{getModalForExistingProvider(
provider,
isOpen,
setIsOpen,
defaultModelName
)}
{getModalForExistingProvider(provider, isOpen, setIsOpen)}
</Card>
</Hoverable.Root>
</>
@@ -453,11 +446,6 @@ export default function LLMConfigurationPage() {
provider={provider}
isDefault={defaultText?.provider_id === provider.id}
isLastProvider={sortedProviders.length === 1}
defaultModelName={
defaultText?.provider_id === provider.id
? defaultText.model_name
: undefined
}
/>
))}
</div>

View File

@@ -35,7 +35,6 @@ export default function AnthropicModal({
shouldMarkAsDefault,
open,
onOpenChange,
defaultModelName,
onboardingState,
onboardingActions,
llmDescriptor,
@@ -65,18 +64,10 @@ export default function AnthropicModal({
default_model_name: DEFAULT_DEFAULT_MODEL_NAME,
}
: {
...buildDefaultInitialValues(
existingLlmProvider,
modelConfigurations,
defaultModelName
),
...buildDefaultInitialValues(existingLlmProvider, modelConfigurations),
api_key: existingLlmProvider?.api_key ?? "",
api_base: existingLlmProvider?.api_base ?? undefined,
default_model_name:
(defaultModelName &&
modelConfigurations.some((m) => m.name === defaultModelName)
? defaultModelName
: undefined) ??
wellKnownLLMProvider?.recommended_default_model?.name ??
DEFAULT_DEFAULT_MODEL_NAME,
is_auto_mode: existingLlmProvider?.is_auto_mode ?? true,

View File

@@ -81,7 +81,6 @@ export default function AzureModal({
shouldMarkAsDefault,
open,
onOpenChange,
defaultModelName,
onboardingState,
onboardingActions,
llmDescriptor,
@@ -110,11 +109,7 @@ export default function AzureModal({
default_model_name: "",
} as AzureModalValues)
: {
...buildDefaultInitialValues(
existingLlmProvider,
modelConfigurations,
defaultModelName
),
...buildDefaultInitialValues(existingLlmProvider, modelConfigurations),
api_key: existingLlmProvider?.api_key ?? "",
target_uri: buildTargetUri(existingLlmProvider),
};

View File

@@ -315,7 +315,6 @@ export default function BedrockModal({
shouldMarkAsDefault,
open,
onOpenChange,
defaultModelName,
onboardingState,
onboardingActions,
llmDescriptor,
@@ -352,11 +351,7 @@ export default function BedrockModal({
},
} as BedrockModalValues)
: {
...buildDefaultInitialValues(
existingLlmProvider,
modelConfigurations,
defaultModelName
),
...buildDefaultInitialValues(existingLlmProvider, modelConfigurations),
custom_config: {
AWS_REGION_NAME:
(existingLlmProvider?.custom_config?.AWS_REGION_NAME as string) ??

View File

@@ -197,7 +197,6 @@ export default function CustomModal({
shouldMarkAsDefault,
open,
onOpenChange,
defaultModelName,
onboardingState,
onboardingActions,
}: LLMProviderFormProps) {
@@ -210,11 +209,7 @@ export default function CustomModal({
const onClose = () => onOpenChange?.(false);
const initialValues = {
...buildDefaultInitialValues(
existingLlmProvider,
undefined,
defaultModelName
),
...buildDefaultInitialValues(existingLlmProvider),
...(isOnboarding ? buildOnboardingInitialValues() : {}),
provider: existingLlmProvider?.provider ?? "",
model_configurations: existingLlmProvider?.model_configurations.map(

View File

@@ -192,7 +192,6 @@ export default function LMStudioForm({
shouldMarkAsDefault,
open,
onOpenChange,
defaultModelName,
onboardingState,
onboardingActions,
llmDescriptor,
@@ -226,11 +225,7 @@ export default function LMStudioForm({
},
} as LMStudioFormValues)
: {
...buildDefaultInitialValues(
existingLlmProvider,
modelConfigurations,
defaultModelName
),
...buildDefaultInitialValues(existingLlmProvider, modelConfigurations),
api_base: existingLlmProvider?.api_base ?? DEFAULT_API_BASE,
custom_config: {
LM_STUDIO_API_KEY:

View File

@@ -159,7 +159,6 @@ export default function LiteLLMProxyModal({
shouldMarkAsDefault,
open,
onOpenChange,
defaultModelName,
onboardingState,
onboardingActions,
llmDescriptor,
@@ -191,11 +190,7 @@ export default function LiteLLMProxyModal({
default_model_name: "",
} as LiteLLMProxyModalValues)
: {
...buildDefaultInitialValues(
existingLlmProvider,
modelConfigurations,
defaultModelName
),
...buildDefaultInitialValues(existingLlmProvider, modelConfigurations),
api_key: existingLlmProvider?.api_key ?? "",
api_base: existingLlmProvider?.api_base ?? DEFAULT_API_BASE,
};

View File

@@ -212,7 +212,6 @@ export default function OllamaModal({
shouldMarkAsDefault,
open,
onOpenChange,
defaultModelName,
onboardingState,
onboardingActions,
llmDescriptor,
@@ -245,11 +244,7 @@ export default function OllamaModal({
},
} as OllamaModalValues)
: {
...buildDefaultInitialValues(
existingLlmProvider,
modelConfigurations,
defaultModelName
),
...buildDefaultInitialValues(existingLlmProvider, modelConfigurations),
api_base: existingLlmProvider?.api_base ?? DEFAULT_API_BASE,
custom_config: {
OLLAMA_API_KEY:

View File

@@ -35,7 +35,6 @@ export default function OpenAIModal({
shouldMarkAsDefault,
open,
onOpenChange,
defaultModelName,
onboardingState,
onboardingActions,
llmDescriptor,
@@ -64,17 +63,9 @@ export default function OpenAIModal({
default_model_name: DEFAULT_DEFAULT_MODEL_NAME,
}
: {
...buildDefaultInitialValues(
existingLlmProvider,
modelConfigurations,
defaultModelName
),
...buildDefaultInitialValues(existingLlmProvider, modelConfigurations),
api_key: existingLlmProvider?.api_key ?? "",
default_model_name:
(defaultModelName &&
modelConfigurations.some((m) => m.name === defaultModelName)
? defaultModelName
: undefined) ??
wellKnownLLMProvider?.recommended_default_model?.name ??
DEFAULT_DEFAULT_MODEL_NAME,
is_auto_mode: existingLlmProvider?.is_auto_mode ?? true,

View File

@@ -158,7 +158,6 @@ export default function OpenRouterModal({
shouldMarkAsDefault,
open,
onOpenChange,
defaultModelName,
onboardingState,
onboardingActions,
llmDescriptor,
@@ -190,11 +189,7 @@ export default function OpenRouterModal({
default_model_name: "",
} as OpenRouterModalValues)
: {
...buildDefaultInitialValues(
existingLlmProvider,
modelConfigurations,
defaultModelName
),
...buildDefaultInitialValues(existingLlmProvider, modelConfigurations),
api_key: existingLlmProvider?.api_key ?? "",
api_base: existingLlmProvider?.api_base ?? DEFAULT_API_BASE,
};

View File

@@ -48,7 +48,6 @@ export default function VertexAIModal({
shouldMarkAsDefault,
open,
onOpenChange,
defaultModelName,
onboardingState,
onboardingActions,
llmDescriptor,
@@ -81,16 +80,8 @@ export default function VertexAIModal({
},
} as VertexAIModalValues)
: {
...buildDefaultInitialValues(
existingLlmProvider,
modelConfigurations,
defaultModelName
),
...buildDefaultInitialValues(existingLlmProvider, modelConfigurations),
default_model_name:
(defaultModelName &&
modelConfigurations.some((m) => m.name === defaultModelName)
? defaultModelName
: undefined) ??
wellKnownLLMProvider?.recommended_default_model?.name ??
VERTEXAI_DEFAULT_MODEL,
is_auto_mode: existingLlmProvider?.is_auto_mode ?? true,

View File

@@ -22,15 +22,9 @@ function detectIfRealOpenAIProvider(provider: LLMProviderView) {
export function getModalForExistingProvider(
provider: LLMProviderView,
open?: boolean,
onOpenChange?: (open: boolean) => void,
defaultModelName?: string
onOpenChange?: (open: boolean) => void
) {
const props = {
existingLlmProvider: provider,
open,
onOpenChange,
defaultModelName,
};
const props = { existingLlmProvider: provider, open, onOpenChange };
switch (provider.provider) {
case LLMProviderName.OPENAI:

View File

@@ -12,16 +12,9 @@ export const LLM_FORM_CLASS_NAME = "flex flex-col gap-y-4 items-stretch mt-6";
export const buildDefaultInitialValues = (
existingLlmProvider?: LLMProviderView,
modelConfigurations?: ModelConfiguration[],
currentDefaultModelName?: string
modelConfigurations?: ModelConfiguration[]
) => {
const defaultModelName =
(currentDefaultModelName &&
existingLlmProvider?.model_configurations?.some(
(m) => m.name === currentDefaultModelName
)
? currentDefaultModelName
: undefined) ??
existingLlmProvider?.model_configurations?.[0]?.name ??
modelConfigurations?.[0]?.name ??
"";

View File

@@ -56,7 +56,8 @@ function buildItems(
settings: CombinedSettings | null,
kgExposed: boolean,
customAnalyticsEnabled: boolean,
hasSubscription: boolean
hasSubscription: boolean,
hooksEnabled: boolean
): SidebarItemEntry[] {
const vectorDbEnabled = settings?.settings.vector_db_enabled !== false;
const items: SidebarItemEntry[] = [];
@@ -122,6 +123,9 @@ function buildItems(
add(SECTIONS.INTEGRATIONS, ADMIN_ROUTES.API_KEYS);
add(SECTIONS.INTEGRATIONS, ADMIN_ROUTES.SLACK_BOTS);
add(SECTIONS.INTEGRATIONS, ADMIN_ROUTES.DISCORD_BOTS);
if (hooksEnabled) {
add(SECTIONS.INTEGRATIONS, ADMIN_ROUTES.HOOKS);
}
}
// 5. Permissions
@@ -202,6 +206,7 @@ export default function AdminSidebar({ enableCloudSS }: AdminSidebarProps) {
(billingData && hasActiveSubscription(billingData)) ||
licenseData?.has_license
);
const hooksEnabled = settings?.settings.hooks_enabled ?? false;
const allItems = buildItems(
isCurator,
@@ -210,7 +215,8 @@ export default function AdminSidebar({ enableCloudSS }: AdminSidebarProps) {
settings,
kgExposed,
customAnalyticsEnabled,
hasSubscriptionOrLicense
hasSubscriptionOrLicense,
hooksEnabled
);
const itemExtractor = useCallback((item: SidebarItemEntry) => item.name, []);

View File

@@ -541,7 +541,18 @@ const MemoizedAppSidebarInner = memo(
() => (
<ChatSearchCommandMenu
trigger={
<SidebarTab icon={SvgSearchMenu} folded={folded}>
<SidebarTab
icon={SvgSearchMenu}
folded={folded}
// TODO (@raunakab)
//
// The internals of `SidebarTab` (`Interactive.Base`) was designed such that providing an `onClick` or `href` would trigger rendering a `cursor-pointer`.
// However, since instance is wired up as a "trigger", it doesn't have either of those explicitly specified.
// Therefore, the default cursor would be rendered.
//
// Specifying a dummy `onClick` handler solves that.
onClick={() => undefined}
>
Search Chats
</SidebarTab>
}

View File

@@ -206,9 +206,16 @@ export default function UserAvatarPopover({
</Section>
) : undefined
}
type="button"
selected={!!popupState || appFocus.isUserSettings()}
folded={folded}
// TODO (@raunakab)
//
// The internals of `SidebarTab` (`Interactive.Base`) was designed such that providing an `onClick` or `href` would trigger rendering a `cursor-pointer`.
// However, since instance is wired up as a "trigger", it doesn't have either of those explicitly specified.
// Therefore, the default cursor would be rendered.
//
// Specifying a dummy `onClick` handler solves that.
onClick={() => undefined}
>
{userDisplayName}
</SidebarTab>