mirror of
https://github.com/onyx-dot-app/onyx.git
synced 2026-03-24 17:12:44 +00:00
Compare commits
2 Commits
main
...
bo/hook_ui
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6604d8a7a6 | ||
|
|
6306fc5200 |
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
|
||||
21
web/lib/opal/src/icons/file-broadcast.tsx
Normal file
21
web/lib/opal/src/icons/file-broadcast.tsx
Normal 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;
|
||||
21
web/lib/opal/src/icons/hook-nodes.tsx
Normal file
21
web/lib/opal/src/icons/hook-nodes.tsx
Normal 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;
|
||||
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
|
||||
1
web/src/app/admin/hooks/page.tsx
Normal file
1
web/src/app/admin/hooks/page.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from "@/refresh-pages/admin/HooksPage";
|
||||
15
web/src/hooks/useHookSpecs.ts
Normal file
15
web/src/hooks/useHookSpecs.ts
Normal 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 };
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
59
web/src/refresh-components/inputs/InputSearch.stories.tsx
Normal file
59
web/src/refresh-components/inputs/InputSearch.stories.tsx
Normal 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
|
||||
/>
|
||||
),
|
||||
};
|
||||
70
web/src/refresh-components/inputs/InputSearch.tsx
Normal file
70
web/src/refresh-components/inputs/InputSearch.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
110
web/src/refresh-pages/admin/HooksPage/HooksContent.tsx
Normal file
110
web/src/refresh-pages/admin/HooksPage/HooksContent.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
42
web/src/refresh-pages/admin/HooksPage/index.tsx
Normal file
42
web/src/refresh-pages/admin/HooksPage/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
63
web/src/refresh-pages/admin/HooksPage/interfaces.ts
Normal file
63
web/src/refresh-pages/admin/HooksPage/interfaces.ts
Normal 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;
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
|
||||
@@ -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) ??
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 ??
|
||||
"";
|
||||
|
||||
@@ -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, []);
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user