mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-06-29 15:31:05 +08:00
feat: Unify the 'Add Model Provider' modal (#15768)
### What problem does this PR solve? feat:Unify the 'Add Model Provider' modal ### Type of change - [x] New Feature (non-breaking change which adds functionality) - [x] Refactoring
This commit is contained in:
@@ -1,17 +1,38 @@
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { X } from 'lucide-react';
|
||||
import { ChevronDown, X } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from './popover';
|
||||
|
||||
/**
|
||||
* Extracts text content from a ReactNode for filtering purposes.
|
||||
* Handles strings, numbers, JSX elements with nested text, and arrays.
|
||||
*/
|
||||
const getNodeText = (node: React.ReactNode): string => {
|
||||
if (typeof node === 'string' || typeof node === 'number') {
|
||||
return String(node);
|
||||
}
|
||||
if (React.isValidElement(node)) {
|
||||
const children = (node.props as { children?: React.ReactNode }).children;
|
||||
if (children) {
|
||||
return getNodeText(children);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
if (Array.isArray(node)) {
|
||||
return node.map(getNodeText).join('');
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
/** Interface for tag select options */
|
||||
export interface InputSelectOption {
|
||||
/** Value of the option */
|
||||
value: string;
|
||||
/** Display label of the option */
|
||||
label: string;
|
||||
label: string | React.ReactNode;
|
||||
}
|
||||
|
||||
/** Properties for the InputSelect component */
|
||||
@@ -36,20 +57,72 @@ export interface InputSelectProps {
|
||||
type?: 'text' | 'number' | 'date' | 'datetime';
|
||||
}
|
||||
|
||||
/** Internal display for single-select selected value. Click label to re-edit (string labels only). */
|
||||
const SingleSelectDisplay: React.FC<{
|
||||
value: string | number | Date;
|
||||
options: InputSelectOption[];
|
||||
type: 'text' | 'number' | 'date' | 'datetime';
|
||||
onEdit: (editText: string) => void;
|
||||
onRemove: () => void;
|
||||
}> = ({ value, options, type, onEdit, onRemove }) => {
|
||||
const selectedOption = options.find((opt) =>
|
||||
type === 'number'
|
||||
? Number(opt.value) === Number(value)
|
||||
: type === 'date' || type === 'datetime'
|
||||
? new Date(opt.value).getTime() === new Date(value as any).getTime()
|
||||
: String(opt.value) === String(value),
|
||||
);
|
||||
|
||||
const label =
|
||||
selectedOption?.label ??
|
||||
(type === 'number'
|
||||
? String(value)
|
||||
: type === 'date' || type === 'datetime'
|
||||
? new Date(value as any).toLocaleString()
|
||||
: String(value));
|
||||
|
||||
const canEdit = typeof label === 'string';
|
||||
|
||||
return (
|
||||
<div className={cn('flex items-center max-w-full')}>
|
||||
<div
|
||||
className={cn(
|
||||
'flex-1 truncate',
|
||||
canEdit ? 'cursor-text' : 'cursor-default',
|
||||
)}
|
||||
onClick={(e) => {
|
||||
if (!canEdit) return;
|
||||
e.stopPropagation();
|
||||
onEdit(getNodeText(label));
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="ml-2 flex-[0_0_24px] text-text-secondary hover:text-text-primary focus:outline-none"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onRemove();
|
||||
}}
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const InputSelect = React.forwardRef<HTMLInputElement, InputSelectProps>(
|
||||
(
|
||||
{
|
||||
options = [],
|
||||
value = [],
|
||||
onChange,
|
||||
placeholder = 'Select tags...',
|
||||
className,
|
||||
style,
|
||||
multi = false,
|
||||
type = 'text',
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
({
|
||||
options = [],
|
||||
value = [],
|
||||
onChange,
|
||||
placeholder = 'Select tags...',
|
||||
className,
|
||||
style,
|
||||
multi = false,
|
||||
type = 'text',
|
||||
}) => {
|
||||
const [inputValue, setInputValue] = React.useState('');
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [isFocused, setIsFocused] = React.useState(false);
|
||||
@@ -170,6 +243,50 @@ const InputSelect = React.forwardRef<HTMLInputElement, InputSelectProps>(
|
||||
setOpen(!!newValue); // Open popover when there's input
|
||||
};
|
||||
|
||||
/**
|
||||
* Commits the current inputValue to the selected values, matching by label first,
|
||||
* then falling back to the typed value. No-op when inputValue is empty/whitespace.
|
||||
* Used by Enter key handler and blur handler.
|
||||
*/
|
||||
const commitInputValue = () => {
|
||||
if (inputValue.trim() === '') return;
|
||||
|
||||
// Match by label text first
|
||||
const matchedOption = options.find(
|
||||
(opt) =>
|
||||
getNodeText(opt.label).toLowerCase() === inputValue.toLowerCase(),
|
||||
);
|
||||
if (matchedOption) {
|
||||
handleAddTag(matchedOption.value);
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, validate by type and add as a new value
|
||||
let valueToAdd: any;
|
||||
if (type === 'number') {
|
||||
const numValue = Number(inputValue);
|
||||
if (isNaN(numValue)) return;
|
||||
valueToAdd = numValue;
|
||||
} else if (type === 'date' || type === 'datetime') {
|
||||
const dateValue = new Date(inputValue);
|
||||
if (isNaN(dateValue.getTime())) return;
|
||||
valueToAdd = dateValue;
|
||||
} else {
|
||||
valueToAdd = inputValue;
|
||||
}
|
||||
|
||||
// Skip if value is already selected
|
||||
const isAlreadySelected = normalizedValue.some((v) =>
|
||||
type === 'number'
|
||||
? Number(v) === Number(valueToAdd)
|
||||
: type === 'date' || type === 'datetime'
|
||||
? new Date(v as any).getTime() === valueToAdd.getTime()
|
||||
: String(v) === valueToAdd,
|
||||
);
|
||||
if (!isAlreadySelected) {
|
||||
handleAddTag(valueToAdd);
|
||||
}
|
||||
};
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (
|
||||
e.key === 'Backspace' &&
|
||||
@@ -196,43 +313,7 @@ const InputSelect = React.forwardRef<HTMLInputElement, InputSelectProps>(
|
||||
onChange?.(result);
|
||||
} else if (e.key === 'Enter' && inputValue.trim() !== '') {
|
||||
e.preventDefault();
|
||||
|
||||
let valueToAdd: any;
|
||||
|
||||
if (type === 'number') {
|
||||
const numValue = Number(inputValue);
|
||||
if (isNaN(numValue)) return; // Don't add invalid numbers
|
||||
valueToAdd = numValue;
|
||||
} else if (type === 'date' || type === 'datetime') {
|
||||
const dateValue = new Date(inputValue);
|
||||
if (isNaN(dateValue.getTime())) return; // Don't add invalid dates
|
||||
valueToAdd = dateValue;
|
||||
} else {
|
||||
valueToAdd = inputValue;
|
||||
}
|
||||
|
||||
// Add input value as a new tag if it doesn't exist in options
|
||||
const matchedOption = options.find(
|
||||
(opt) => opt.label.toLowerCase() === inputValue.toLowerCase(),
|
||||
);
|
||||
|
||||
if (matchedOption) {
|
||||
handleAddTag(matchedOption.value);
|
||||
} else {
|
||||
// If not in options, create a new tag with the input value
|
||||
if (
|
||||
!normalizedValue.some((v) =>
|
||||
type === 'number'
|
||||
? Number(v) === Number(valueToAdd)
|
||||
: type === 'date' || type === 'datetime'
|
||||
? new Date(v as any).getTime() === valueToAdd.getTime()
|
||||
: String(v) === valueToAdd,
|
||||
) &&
|
||||
inputValue.trim() !== ''
|
||||
) {
|
||||
handleAddTag(valueToAdd);
|
||||
}
|
||||
}
|
||||
commitInputValue();
|
||||
} else if (e.key === 'Escape') {
|
||||
inputRef.current?.blur();
|
||||
setOpen(false);
|
||||
@@ -254,8 +335,9 @@ const InputSelect = React.forwardRef<HTMLInputElement, InputSelectProps>(
|
||||
};
|
||||
|
||||
const handleInputBlur = () => {
|
||||
// Delay closing to allow click on options
|
||||
// Delay closing to allow click on options to register
|
||||
setTimeout(() => {
|
||||
commitInputValue();
|
||||
setOpen(false);
|
||||
setIsFocused(false);
|
||||
}, 150);
|
||||
@@ -279,7 +361,7 @@ const InputSelect = React.forwardRef<HTMLInputElement, InputSelectProps>(
|
||||
const filteredOptions = availableOptions.filter(
|
||||
(option) =>
|
||||
!inputValue ||
|
||||
option.label
|
||||
getNodeText(option.label)
|
||||
.toLowerCase()
|
||||
.includes(inputValue.toString().toLowerCase()),
|
||||
);
|
||||
@@ -290,7 +372,8 @@ const InputSelect = React.forwardRef<HTMLInputElement, InputSelectProps>(
|
||||
|
||||
const hasLabelMatch = options.some(
|
||||
(option) =>
|
||||
option.label.toLowerCase() === inputValue.toString().toLowerCase(),
|
||||
getNodeText(option.label).toLowerCase() ===
|
||||
inputValue.toString().toLowerCase(),
|
||||
);
|
||||
|
||||
let isAlreadySelected = false;
|
||||
@@ -318,7 +401,7 @@ const InputSelect = React.forwardRef<HTMLInputElement, InputSelectProps>(
|
||||
const triggerElement = (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-wrap items-center gap-1 w-full rounded-md border-0.5 border-border-button bg-bg-input px-3 py-1 min-h-8 cursor-text',
|
||||
'flex items-center gap-1 w-full rounded-md border-0.5 border-border-button bg-bg-input px-3 py-1 min-h-8 cursor-text',
|
||||
'outline-none transition-colors',
|
||||
'focus-within:outline-none focus-within:ring-1 focus-within:ring-accent-primary',
|
||||
className,
|
||||
@@ -326,109 +409,110 @@ const InputSelect = React.forwardRef<HTMLInputElement, InputSelectProps>(
|
||||
style={style}
|
||||
onClick={handleContainerClick}
|
||||
>
|
||||
{/* Render selected tags - only show tags if multi is true or if single select has a value */}
|
||||
{multi &&
|
||||
normalizedValue.map((tagValue, index) => {
|
||||
const option = options.find((opt) =>
|
||||
type === 'number'
|
||||
? Number(opt.value) === Number(tagValue)
|
||||
: type === 'date' || type === 'datetime'
|
||||
? new Date(opt.value).getTime() ===
|
||||
new Date(tagValue).getTime()
|
||||
: String(opt.value) === String(tagValue),
|
||||
) || {
|
||||
value: String(tagValue),
|
||||
label: String(tagValue),
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`${tagValue}-${index}`}
|
||||
className="flex items-center bg-bg-card text-text-primary rounded px-2 py-1 text-xs mr-1 mb-1 border border-border-card truncate"
|
||||
>
|
||||
<div className="flex-1 truncate">{option.label}</div>
|
||||
<button
|
||||
type="button"
|
||||
className="ml-1 text-text-secondary hover:text-text-primary focus:outline-none"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleRemoveTag(tagValue);
|
||||
}}
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* For single select, show the selected value as text instead of a tag */}
|
||||
{!multi && !isEmpty(normalizedValue[0]) && (
|
||||
<div className={cn('flex items-center max-w-full')}>
|
||||
<div className="flex-1 truncate">
|
||||
{options.find((opt) =>
|
||||
{/* Wrapper for tags and input - this part wraps */}
|
||||
<div className="flex flex-wrap items-center gap-1 flex-1 min-w-0">
|
||||
{/* Render selected tags - only show tags if multi is true or if single select has a value */}
|
||||
{multi &&
|
||||
normalizedValue.map((tagValue, index) => {
|
||||
const option = options.find((opt) =>
|
||||
type === 'number'
|
||||
? Number(opt.value) === Number(normalizedValue[0])
|
||||
? Number(opt.value) === Number(tagValue)
|
||||
: type === 'date' || type === 'datetime'
|
||||
? new Date(opt.value).getTime() ===
|
||||
new Date(normalizedValue[0]).getTime()
|
||||
: String(opt.value) === String(normalizedValue[0]),
|
||||
)?.label ||
|
||||
(type === 'number'
|
||||
? String(normalizedValue[0])
|
||||
: type === 'date' || type === 'datetime'
|
||||
? new Date(normalizedValue[0] as any).toLocaleString()
|
||||
: String(normalizedValue[0]))}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="ml-2 flex-[0_0_24px] text-text-secondary hover:text-text-primary focus:outline-none"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleRemoveTag(normalizedValue[0]);
|
||||
}}
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
new Date(tagValue).getTime()
|
||||
: String(opt.value) === String(tagValue),
|
||||
) || {
|
||||
value: String(tagValue),
|
||||
label: String(tagValue),
|
||||
};
|
||||
|
||||
{/* Input field for adding new tags - hide if single select and value is already selected, or in multi select when not focused */}
|
||||
{(multi ? isFocused : multi || isEmpty(normalizedValue[0])) && (
|
||||
<Input
|
||||
ref={inputRef}
|
||||
type={
|
||||
type === 'date'
|
||||
? 'date'
|
||||
: type === 'datetime'
|
||||
? 'datetime-local'
|
||||
: type === 'number'
|
||||
? 'number'
|
||||
: 'text'
|
||||
}
|
||||
value={
|
||||
type === 'number' && inputValue
|
||||
? String(inputValue)
|
||||
: type === 'date' || type === 'datetime'
|
||||
? inputValue
|
||||
: inputValue
|
||||
}
|
||||
onChange={handleInputChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={
|
||||
(
|
||||
multi
|
||||
? normalizedValue.length === 0
|
||||
: isEmpty(normalizedValue[0])
|
||||
)
|
||||
? placeholder
|
||||
: ''
|
||||
}
|
||||
className="flex-grow min-w-[50px] border-none px-1 py-0 bg-transparent focus-visible:ring-0 focus-visible:ring-offset-0 h-auto "
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onFocus={handleInputFocus}
|
||||
onBlur={handleInputBlur}
|
||||
/>
|
||||
)}
|
||||
return (
|
||||
<div
|
||||
key={`${tagValue}-${index}`}
|
||||
className="flex items-center bg-bg-card text-text-primary rounded px-2 py-1 text-xs mr-1 mb-1 border border-border-card truncate"
|
||||
>
|
||||
<div className="flex-1 truncate">{option.label}</div>
|
||||
<button
|
||||
type="button"
|
||||
className="ml-1 text-text-secondary hover:text-text-primary focus:outline-none"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleRemoveTag(tagValue);
|
||||
}}
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* For single select, show the selected value as text instead of a tag */}
|
||||
{!multi && !isEmpty(normalizedValue[0]) && (
|
||||
<SingleSelectDisplay
|
||||
value={normalizedValue[0]}
|
||||
options={options}
|
||||
type={type}
|
||||
onEdit={(editText) => {
|
||||
handleRemoveTag(normalizedValue[0]);
|
||||
setInputValue(editText);
|
||||
setIsFocused(true);
|
||||
setOpen(true);
|
||||
requestAnimationFrame(() => {
|
||||
const input = inputRef.current;
|
||||
if (input) {
|
||||
input.focus();
|
||||
input.setSelectionRange(editText.length, editText.length);
|
||||
}
|
||||
});
|
||||
}}
|
||||
onRemove={() => handleRemoveTag(normalizedValue[0])}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Input field for adding new tags - hide if single select and value is already selected, or in multi select when not focused */}
|
||||
{(multi ? isFocused : multi || isEmpty(normalizedValue[0])) && (
|
||||
<Input
|
||||
ref={inputRef}
|
||||
type={
|
||||
type === 'date'
|
||||
? 'date'
|
||||
: type === 'datetime'
|
||||
? 'datetime-local'
|
||||
: type === 'number'
|
||||
? 'number'
|
||||
: 'text'
|
||||
}
|
||||
value={
|
||||
type === 'number' && inputValue
|
||||
? String(inputValue)
|
||||
: type === 'date' || type === 'datetime'
|
||||
? inputValue
|
||||
: inputValue
|
||||
}
|
||||
onChange={handleInputChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={
|
||||
(
|
||||
multi
|
||||
? normalizedValue.length === 0
|
||||
: isEmpty(normalizedValue[0])
|
||||
)
|
||||
? placeholder
|
||||
: ''
|
||||
}
|
||||
className="flex-grow min-w-[50px] border-none px-1 py-0 bg-transparent focus-visible:ring-0 focus-visible:ring-offset-0 h-auto "
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onFocus={handleInputFocus}
|
||||
onBlur={handleInputBlur}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<ChevronDown
|
||||
className={cn(
|
||||
'h-4 w-4 text-text-secondary shrink-0 transition-transform',
|
||||
open && 'rotate-180',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
308
web/src/components/ui/toggle-list.tsx
Normal file
308
web/src/components/ui/toggle-list.tsx
Normal file
@@ -0,0 +1,308 @@
|
||||
'use client';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
import { ChevronsDown, Loader2, Search, X } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
|
||||
export interface ToggleListOption<V = unknown> {
|
||||
/** Arbitrary value, including objects. Used as the React key when primitive. */
|
||||
value: V;
|
||||
/** Display content. Strings participate in local search filtering. */
|
||||
label: React.ReactNode;
|
||||
/** Per-item click handler. Whether the list closes afterwards is up to the caller. */
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export interface ToggleListLoadMore {
|
||||
/** Whether more items are available to load. When false, the button is hidden. */
|
||||
hasMore: boolean;
|
||||
/** Triggered when the user clicks the "Load more" button at the bottom of the list. */
|
||||
onLoadMore: () => void;
|
||||
/** Show a loading spinner inside the button and disable interaction. */
|
||||
loading?: boolean;
|
||||
/** Custom label for the button. Defaults to "Load more". */
|
||||
text?: React.ReactNode;
|
||||
}
|
||||
|
||||
export interface ToggleListProps<V = unknown> {
|
||||
/** Class applied to the outer container that wraps the button and the list. */
|
||||
className?: string;
|
||||
/** Text (or any node) shown inside the trigger button. */
|
||||
btnText?: React.ReactNode;
|
||||
/** List items rendered inside the scrollable area. */
|
||||
options: ToggleListOption<V>[];
|
||||
/**
|
||||
* When true (default), clicking anywhere outside the component will close the list.
|
||||
* Set to false if the list should only be toggled by the button itself.
|
||||
*/
|
||||
closeOnOutsideClick?: boolean;
|
||||
/** Max height (in px) of the scrollable list area. Defaults to 500. */
|
||||
maxHeight?: number;
|
||||
/** Class applied to the list container (the box around the search input + items). */
|
||||
listClassName?: string;
|
||||
/** Class applied to each list item. */
|
||||
itemClassName?: string;
|
||||
/** Class applied to the trigger button (merged with the default styles). */
|
||||
buttonClassName?: string;
|
||||
/** Placeholder rendered when `options` is empty (and no query is active). */
|
||||
emptyText?: React.ReactNode;
|
||||
/** Placeholder rendered when a search query yields no matches. */
|
||||
noResultsText?: React.ReactNode;
|
||||
|
||||
/** Show a search input above the list. */
|
||||
searchable?: boolean;
|
||||
/** Placeholder for the search input. */
|
||||
searchPlaceholder?: string;
|
||||
/**
|
||||
* If provided, switches to API search mode: every query change calls this
|
||||
* callback. The component does NOT filter `options` locally — the caller is
|
||||
* expected to update `options` (typically after a debounce + fetch).
|
||||
* If omitted, the component performs a case-insensitive substring filter
|
||||
* locally against the stringified label.
|
||||
*/
|
||||
onSearch?: (query: string) => void;
|
||||
/** Show a spinner inside the search input. Useful for API search loading state. */
|
||||
searchLoading?: boolean;
|
||||
/** Class applied to the search input wrapper. */
|
||||
searchClassName?: string;
|
||||
|
||||
/**
|
||||
* Load-more pagination config. The caller owns the data; the component just
|
||||
* renders a button at the bottom of the list when `hasMore` is true or a
|
||||
* load is in flight.
|
||||
*/
|
||||
loadMore?: ToggleListLoadMore;
|
||||
/**
|
||||
* Optional callback fired whenever the list is opened (`true`) or
|
||||
* closed (`false`). The component still owns the open/close state; this
|
||||
* is just a notification hook so callers can trigger side effects
|
||||
* (e.g. lazy-fetching list data) on first open.
|
||||
*/
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* ToggleList — a button that toggles a vertically stacked, scrollable list area
|
||||
* rendered directly below it (no portal/overlay). Click the button to expand,
|
||||
* click again to collapse.
|
||||
*
|
||||
* Features:
|
||||
* - The list occupies real DOM space and stretches to the parent's width
|
||||
* (the trigger button keeps its natural width).
|
||||
* - Optional search input. Local mode (no `onSearch`) filters options client-side;
|
||||
* API mode (with `onSearch`) only emits the query and lets the caller refetch.
|
||||
* - Optional load-more pagination. The caller owns the data; the component
|
||||
* renders the trigger button.
|
||||
*
|
||||
* Behavior notes:
|
||||
* - Clicking a list item calls that item's `onClick`. The component does not
|
||||
* auto-close after a click — let the caller's onClick decide via controlled
|
||||
* state if needed.
|
||||
* - External-click closing is opt-out via `closeOnOutsideClick={false}`.
|
||||
*/
|
||||
export function ToggleList<V = unknown>({
|
||||
className,
|
||||
btnText,
|
||||
options,
|
||||
closeOnOutsideClick = true,
|
||||
maxHeight = 500,
|
||||
listClassName,
|
||||
itemClassName,
|
||||
buttonClassName,
|
||||
emptyText = 'No options',
|
||||
noResultsText = 'No matching results',
|
||||
searchable = false,
|
||||
searchPlaceholder = 'Search…',
|
||||
onSearch,
|
||||
searchLoading = false,
|
||||
searchClassName,
|
||||
loadMore,
|
||||
onOpenChange,
|
||||
}: ToggleListProps<V>) {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [query, setQuery] = React.useState('');
|
||||
const containerRef = React.useRef<HTMLDivElement>(null);
|
||||
const listId = React.useId();
|
||||
|
||||
// Close on outside click
|
||||
React.useEffect(() => {
|
||||
if (!open || !closeOnOutsideClick) return;
|
||||
const handlePointerDown = (event: MouseEvent) => {
|
||||
const node = containerRef.current;
|
||||
if (node && !node.contains(event.target as Node)) {
|
||||
setOpen(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener('mousedown', handlePointerDown);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handlePointerDown);
|
||||
};
|
||||
}, [open, closeOnOutsideClick]);
|
||||
|
||||
const isApiSearch = Boolean(onSearch);
|
||||
|
||||
// Local search: case-insensitive substring filter on the stringified label.
|
||||
// In API search mode the caller is responsible for filtering, so we pass through.
|
||||
const filteredOptions = React.useMemo(() => {
|
||||
if (isApiSearch || !query.trim()) return options;
|
||||
const q = query.trim().toLowerCase();
|
||||
return options.filter((opt) =>
|
||||
String(opt.label ?? '')
|
||||
.toLowerCase()
|
||||
.includes(q),
|
||||
);
|
||||
}, [options, query, isApiSearch]);
|
||||
|
||||
const handleQueryChange = React.useCallback(
|
||||
(next: string) => {
|
||||
setQuery(next);
|
||||
if (onSearch) onSearch(next);
|
||||
},
|
||||
[onSearch],
|
||||
);
|
||||
|
||||
const showNoResults =
|
||||
searchable && query.trim().length > 0 && filteredOptions.length === 0;
|
||||
const showEmpty = !showNoResults && options.length === 0;
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className={cn('flex flex-col', className)}>
|
||||
<button
|
||||
type="button"
|
||||
aria-expanded={open}
|
||||
aria-controls={listId}
|
||||
onClick={() =>
|
||||
setOpen((prev) => {
|
||||
const next = !prev;
|
||||
onOpenChange?.(next);
|
||||
return next;
|
||||
})
|
||||
}
|
||||
className={cn(
|
||||
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded text-sm',
|
||||
'h-8 px-3 bg-bg-card text-text-secondary border border-border-button self-start',
|
||||
'hover:bg-border-button hover:text-text-primary',
|
||||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-primary/40',
|
||||
buttonClassName,
|
||||
)}
|
||||
>
|
||||
{btnText}
|
||||
{typeof btnText === 'string' && (
|
||||
<ChevronsDown
|
||||
className={cn(
|
||||
'size-4 shrink-0 transition-transform duration-200',
|
||||
open && 'rotate-180',
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
{open && (
|
||||
<div
|
||||
id={listId}
|
||||
role="list"
|
||||
className={cn(
|
||||
'mt-1 flex flex-col rounded-md border border-border-button bg-bg-card w-full overflow-hidden',
|
||||
listClassName,
|
||||
)}
|
||||
>
|
||||
{searchable && (
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center gap-2 px-2 h-9 border-b border-border-button',
|
||||
searchClassName,
|
||||
)}
|
||||
>
|
||||
<Search className="size-4 shrink-0 text-text-secondary" />
|
||||
<input
|
||||
type="text"
|
||||
value={query}
|
||||
onChange={(e) => handleQueryChange(e.target.value)}
|
||||
placeholder={searchPlaceholder}
|
||||
className="flex-1 min-w-0 bg-transparent text-sm outline-none placeholder:text-text-secondary/60"
|
||||
/>
|
||||
{searchLoading && (
|
||||
<Loader2 className="size-4 shrink-0 animate-spin text-text-secondary" />
|
||||
)}
|
||||
{query && !searchLoading && (
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Clear search"
|
||||
onClick={() => handleQueryChange('')}
|
||||
className="text-text-secondary hover:text-text-primary"
|
||||
>
|
||||
<X className="size-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className="flex-1 overflow-y-auto divide-y divide-border-button"
|
||||
style={{ maxHeight }}
|
||||
>
|
||||
{showEmpty && (
|
||||
<div className="px-3 py-2 text-sm text-text-secondary">
|
||||
{emptyText}
|
||||
</div>
|
||||
)}
|
||||
{showNoResults && (
|
||||
<div className="px-3 py-2 text-sm text-text-secondary">
|
||||
{noResultsText}
|
||||
</div>
|
||||
)}
|
||||
{!showEmpty &&
|
||||
!showNoResults &&
|
||||
filteredOptions.map((option, index) => {
|
||||
const raw = option.value;
|
||||
// Use index as the key for objects (no stable identity) and
|
||||
// for nullish values; stringify primitives for a stable key.
|
||||
const key =
|
||||
raw === null ||
|
||||
raw === undefined ||
|
||||
(typeof raw === 'object' && raw !== null)
|
||||
? index
|
||||
: String(raw);
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
role="listitem"
|
||||
onClick={() => {
|
||||
option.onClick?.();
|
||||
}}
|
||||
className={cn(
|
||||
'cursor-pointer px-3 py-2 text-sm hover:bg-border-button ',
|
||||
itemClassName,
|
||||
)}
|
||||
>
|
||||
{option.label}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{loadMore &&
|
||||
(loadMore.hasMore || loadMore.loading) &&
|
||||
filteredOptions.length > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
disabled={!loadMore.hasMore || loadMore.loading}
|
||||
onClick={loadMore.onLoadMore}
|
||||
className={cn(
|
||||
'w-full px-3 py-2 text-sm border-t border-border-button',
|
||||
'text-text-secondary hover:text-text-primary hover:bg-border-button',
|
||||
'disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-transparent',
|
||||
)}
|
||||
>
|
||||
{loadMore.loading ? (
|
||||
<span className="inline-flex items-center justify-center gap-2">
|
||||
<Loader2 className="size-3.5 animate-spin" /> Loading…
|
||||
</span>
|
||||
) : (
|
||||
(loadMore.text ?? 'Load more')
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -14,7 +14,9 @@ import {
|
||||
IAddProviderRequestBody,
|
||||
IDeleteProviderInstanceRequestBody,
|
||||
IListAllModelsRequestParams,
|
||||
IListProviderModelsRequestBody,
|
||||
IListProvidersRequestParams,
|
||||
IModelInfo,
|
||||
ISetDefaultModelRequestBody,
|
||||
IUpdateModelStatusRequestBody,
|
||||
} from '@/interfaces/request/llm';
|
||||
@@ -33,6 +35,7 @@ export const enum LLMApiAction {
|
||||
AddProvider = 'addProvider',
|
||||
AddProviderInstance = 'addProviderInstance',
|
||||
VerifyProviderConnection = 'verifyProviderConnection',
|
||||
ListProviderModels = 'listProviderModels',
|
||||
AddInstanceModel = 'addInstanceModel',
|
||||
DeleteProviderInstance = 'deleteProviderInstance',
|
||||
ListDefaultModels = 'listDefaultModels',
|
||||
@@ -46,6 +49,13 @@ export const LlmKeys = {
|
||||
[LLMApiAction.AllModels, modelType] as const,
|
||||
providerInstances: (providerName: string) =>
|
||||
[LLMApiAction.AddedProviders, providerName, 'instances'] as const,
|
||||
providerInstance: (providerName: string, instanceName: string) =>
|
||||
[
|
||||
LLMApiAction.AddedProviders,
|
||||
providerName,
|
||||
instanceName,
|
||||
'instance',
|
||||
] as const,
|
||||
instanceModels: (providerName: string, instanceName: string) =>
|
||||
[
|
||||
LLMApiAction.AddedProviders,
|
||||
@@ -141,6 +151,31 @@ export const useFetchProviderInstances = (providerName: string) => {
|
||||
return { data, loading };
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch full details of a single provider instance (used in viewMode to
|
||||
* retrieve fields like `baseUrl` that the list endpoint does not return).
|
||||
* Disabled by default; call from an event handler (e.g. onClick) and
|
||||
* rely on the returned `refetch` to actually trigger the request.
|
||||
*/
|
||||
export const useFetchProviderInstance = (
|
||||
providerName: string,
|
||||
instanceName: string,
|
||||
) => {
|
||||
return useQuery<IProviderInstance>({
|
||||
queryKey: LlmKeys.providerInstance(providerName, instanceName),
|
||||
initialData: undefined as unknown as IProviderInstance,
|
||||
gcTime: 0,
|
||||
enabled: false,
|
||||
queryFn: async () => {
|
||||
const { data } = await llmService.showProviderInstance(
|
||||
{ provider_name: providerName, instance_name: instanceName },
|
||||
true,
|
||||
);
|
||||
return (data?.data ?? {}) as IProviderInstance;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useFetchInstanceModels = (
|
||||
providerName: string,
|
||||
instanceName: string,
|
||||
@@ -251,6 +286,7 @@ export const useVerifyProviderConnection = () => {
|
||||
api_key: string;
|
||||
base_url?: string;
|
||||
region?: string;
|
||||
model_info?: IModelInfo[];
|
||||
}) => {
|
||||
const { data } = await llmService.verifyProviderConnection(params);
|
||||
return data;
|
||||
@@ -260,6 +296,34 @@ export const useVerifyProviderConnection = () => {
|
||||
return { data, loading, verifyProviderConnection: mutateAsync };
|
||||
};
|
||||
|
||||
export const useListProviderModels = () => {
|
||||
const { isPending: loading, mutateAsync } = useMutation({
|
||||
mutationKey: [LLMApiAction.ListProviderModels],
|
||||
mutationFn: async (params: IListProviderModelsRequestBody) => {
|
||||
const { provider_name, api_key, base_url } = params;
|
||||
// GET /api/v1/providers/<provider_name>/models
|
||||
// The API accepts api_key and base_url as optional query parameters.
|
||||
// api_key is expected as a string; values in {} object form must be
|
||||
// JSON-stringified before being sent.
|
||||
const queryParams: Record<string, string> = {};
|
||||
if (api_key) {
|
||||
queryParams.api_key =
|
||||
typeof api_key === 'string' ? api_key : JSON.stringify(api_key);
|
||||
}
|
||||
if (base_url) {
|
||||
queryParams.base_url = base_url;
|
||||
}
|
||||
const { data } = await llmService.listProviderModels(
|
||||
{ provider_name, params: queryParams },
|
||||
true,
|
||||
);
|
||||
return data;
|
||||
},
|
||||
});
|
||||
|
||||
return { loading, listProviderModels: mutateAsync };
|
||||
};
|
||||
|
||||
export const useAddInstanceModel = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const {
|
||||
|
||||
@@ -54,6 +54,12 @@ export interface IProviderInstance {
|
||||
provider_id: string;
|
||||
region: string;
|
||||
status: string;
|
||||
/**
|
||||
* Optional: only returned by the showProviderInstance endpoint. Used
|
||||
* to pre-fill the base_url/api_base form field in the ProviderModal
|
||||
* (e.g. when opening an existing instance in viewMode).
|
||||
*/
|
||||
base_url?: string;
|
||||
}
|
||||
export interface IAddedModel {
|
||||
model_type: string[];
|
||||
|
||||
@@ -1,12 +1,25 @@
|
||||
export interface IAddLlmRequestBody {
|
||||
llm_factory: string; // Ollama
|
||||
llm_name: string;
|
||||
model_type: string | string[];
|
||||
api_base?: string; // chat|embedding|speech2text|image2text
|
||||
// model_name: string;
|
||||
// model_type: string | string[];
|
||||
base_url?: string; // chat|embedding|speech2text|image2text
|
||||
api_key?: string | Record<string, any>;
|
||||
max_tokens: number;
|
||||
is_tools?: boolean;
|
||||
region?: string;
|
||||
model_info: IModelInfo[];
|
||||
}
|
||||
|
||||
export interface IModelInfo {
|
||||
model_name: string;
|
||||
model_type: string | string[];
|
||||
max_tokens: number;
|
||||
/**
|
||||
* Per-model extras (e.g. `is_tools` derived from the model descriptor's
|
||||
* `features`). Optional for backward compatibility with legacy
|
||||
* single-model payloads.
|
||||
*/
|
||||
extra?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface IDeleteLlmRequestBody {
|
||||
@@ -61,3 +74,27 @@ export interface ISetDefaultModelRequestBody {
|
||||
model_type: string;
|
||||
model_name: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Item shape returned by the list-provider-models endpoint.
|
||||
* Fields match the backend's available-model descriptor.
|
||||
*/
|
||||
export interface IProviderModelItem {
|
||||
name: string;
|
||||
max_tokens: number;
|
||||
model_types: string[];
|
||||
features: string[] | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request payload for the list-provider-models endpoint.
|
||||
* Mirrors the verifyProviderConnection payload so the same form
|
||||
* fields (api_key, base_url, region, model_info) can be reused.
|
||||
*/
|
||||
export interface IListProviderModelsRequestBody {
|
||||
provider_name: string;
|
||||
api_key: string;
|
||||
base_url?: string;
|
||||
region?: string;
|
||||
model_info?: IModelInfo[];
|
||||
}
|
||||
|
||||
@@ -1543,6 +1543,39 @@ Example: Virtual Hosted Style`,
|
||||
'International users only: use https://api.minimax.io/v1',
|
||||
minimaxBaseUrlPlaceholder:
|
||||
'(International users only, fill in https://api.minimax.io/v1)',
|
||||
openaiBaseUrlPlaceholder: 'https://api.openai.com/v1',
|
||||
anthropicBaseUrlPlaceholder: 'https://api.anthropic.com/v1',
|
||||
siliconflowBaseUrlPlaceholder: 'https://api.siliconflow.cn/v1',
|
||||
groupId: 'Group ID',
|
||||
providerOrder: 'Provider order',
|
||||
paddleocrApiUrl: 'PaddleOCR API URL',
|
||||
paddleocrApiUrlMessage: 'Please input the PaddleOCR API URL!',
|
||||
paddleocrApiUrlPlaceholder:
|
||||
'e.g. https://paddleocr-server.com/layout-parsing',
|
||||
paddleocrAccessToken: 'PaddleOCR Access Token',
|
||||
paddleocrAccessTokenMessage: 'Please input the PaddleOCR access token!',
|
||||
paddleocrAccessTokenPlaceholder: 'Your PaddleOCR access token (optional)',
|
||||
paddleocrAlgorithm: 'PaddleOCR Algorithm',
|
||||
paddleocrAlgorithmMessage: 'Please select a PaddleOCR algorithm',
|
||||
mineruApiserver: 'MinerU API Server',
|
||||
mineruApiserverMessage: 'Please input the MinerU API Server URL!',
|
||||
mineruApiserverPlaceholder: 'e.g. http://host.docker.internal:9987',
|
||||
mineruOutputDir: 'MinerU Output Directory',
|
||||
mineruOutputDirMessage: 'Please input the MinerU output directory!',
|
||||
mineruOutputDirPlaceholder: '/tmp/mineru',
|
||||
mineruBackend: 'MinerU Backend',
|
||||
mineruBackendMessage: 'Please select a MinerU backend!',
|
||||
mineruSelectBackend: 'Select processing backend',
|
||||
mineruServerUrl: 'MinerU Server URL',
|
||||
mineruServerUrlMessage: 'Please input the MinerU Server URL!',
|
||||
mineruServerUrlPlaceholder: 'e.g. http://your-vllm-server:30000',
|
||||
mineruDeleteOutput: 'Delete Output Files',
|
||||
mineruDeleteOutputMessage: 'Invalid value for delete output',
|
||||
opendataloaderApiserver: 'OpenDataLoader API Server',
|
||||
opendataloaderApiserverMessage:
|
||||
'Please input the OpenDataLoader API Server!',
|
||||
opendataloaderApiserverPlaceholder:
|
||||
'http://your-opendataloader-service:9383',
|
||||
modify: 'Modify',
|
||||
systemModelSettings: 'Set default models',
|
||||
chatModel: 'LLM',
|
||||
@@ -1736,6 +1769,11 @@ Example: Virtual Hosted Style`,
|
||||
},
|
||||
showToc: 'Show content',
|
||||
hideToc: 'Hide content',
|
||||
listModels: 'List models',
|
||||
allModels: 'All models',
|
||||
listModelsSearchPlaceholder: 'Search models…',
|
||||
listModelsEmpty: 'No models available',
|
||||
listModelsLoading: 'Loading models…',
|
||||
},
|
||||
message: {
|
||||
registered: 'Registered!',
|
||||
|
||||
@@ -1267,6 +1267,38 @@ NER:使用 spaCy NER 和基于规则的关键词提取来抽取实体和关系
|
||||
tongyiBaseUrlPlaceholder: '(仅国际用户需要)',
|
||||
minimaxBaseUrlTip: '仅国际用户:使用 https://api.minimax.io/v1。',
|
||||
minimaxBaseUrlPlaceholder: '(仅国际用户填写 https://api.minimax.io/v1)',
|
||||
openaiBaseUrlPlaceholder: 'https://api.openai.com/v1',
|
||||
anthropicBaseUrlPlaceholder: 'https://api.anthropic.com/v1',
|
||||
siliconflowBaseUrlPlaceholder: 'https://api.siliconflow.cn/v1',
|
||||
groupId: 'Group ID',
|
||||
providerOrder: 'Provider 顺序',
|
||||
paddleocrApiUrl: 'PaddleOCR API URL',
|
||||
paddleocrApiUrlMessage: '请输入 PaddleOCR API URL!',
|
||||
paddleocrApiUrlPlaceholder:
|
||||
'例如:https://paddleocr-server.com/layout-parsing',
|
||||
paddleocrAccessToken: 'PaddleOCR 访问令牌',
|
||||
paddleocrAccessTokenMessage: '请输入 PaddleOCR 访问令牌!',
|
||||
paddleocrAccessTokenPlaceholder: '您的 PaddleOCR 访问令牌(可选)',
|
||||
paddleocrAlgorithm: 'PaddleOCR 算法',
|
||||
paddleocrAlgorithmMessage: '请选择 PaddleOCR 算法',
|
||||
mineruApiserver: 'MinerU API 服务器',
|
||||
mineruApiserverMessage: '请输入 MinerU API 服务器地址!',
|
||||
mineruApiserverPlaceholder: '例如:http://host.docker.internal:9987',
|
||||
mineruOutputDir: 'MinerU 输出目录',
|
||||
mineruOutputDirMessage: '请输入 MinerU 输出目录!',
|
||||
mineruOutputDirPlaceholder: '/tmp/mineru',
|
||||
mineruBackend: 'MinerU 后端',
|
||||
mineruBackendMessage: '请选择 MinerU 后端!',
|
||||
mineruSelectBackend: '选择处理后端',
|
||||
mineruServerUrl: 'MinerU 服务器 URL',
|
||||
mineruServerUrlMessage: '请输入 MinerU 服务器 URL!',
|
||||
mineruServerUrlPlaceholder: '例如:http://your-vllm-server:30000',
|
||||
mineruDeleteOutput: '处理完成后删除输出文件',
|
||||
mineruDeleteOutputMessage: '删除输出值无效',
|
||||
opendataloaderApiserver: 'OpenDataLoader API 服务器',
|
||||
opendataloaderApiserverMessage: '请输入 OpenDataLoader API 服务器!',
|
||||
opendataloaderApiserverPlaceholder:
|
||||
'http://your-opendataloader-service:9383',
|
||||
modify: '修改',
|
||||
systemModelSettings: '设置默认模型',
|
||||
chatModel: 'LLM',
|
||||
@@ -1425,6 +1457,11 @@ NER:使用 spaCy NER 和基于规则的关键词提取来抽取实体和关系
|
||||
},
|
||||
showToc: '显示目录',
|
||||
hideToc: '隐藏目录',
|
||||
listModels: '模型列表',
|
||||
allModels: '所有模型',
|
||||
listModelsSearchPlaceholder: '搜索模型…',
|
||||
listModelsEmpty: '暂无可用模型',
|
||||
listModelsLoading: '正在加载模型…',
|
||||
},
|
||||
message: {
|
||||
registered: '注册成功',
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
useDeleteProviderInstance,
|
||||
useFetchAddedProviders,
|
||||
useFetchInstanceModels,
|
||||
useFetchProviderInstance,
|
||||
useFetchProviderInstances,
|
||||
useUpdateModelStatus,
|
||||
} from '@/hooks/use-llm-request';
|
||||
@@ -20,15 +21,21 @@ import {
|
||||
IInstanceModel,
|
||||
IProviderInstance,
|
||||
} from '@/interfaces/database/llm';
|
||||
import { ChevronsDown, ChevronsUp, Trash2 } from 'lucide-react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { ChevronsDown, ChevronsUp, Settings, Trash2 } from 'lucide-react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { mapModelKey } from './un-add-model';
|
||||
|
||||
export function UsedModel({
|
||||
handleAddModel,
|
||||
onEditInstance,
|
||||
}: {
|
||||
handleAddModel: (factory: string) => void;
|
||||
onEditInstance?: (
|
||||
providerName: string,
|
||||
instance: IProviderInstance,
|
||||
models: IInstanceModel[],
|
||||
) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { data: providerList } = useFetchAddedProviders();
|
||||
@@ -46,6 +53,7 @@ export function UsedModel({
|
||||
key={provider.name}
|
||||
provider={provider}
|
||||
handleAddModel={handleAddModel}
|
||||
onEditInstance={onEditInstance}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -55,9 +63,15 @@ export function UsedModel({
|
||||
function ProviderCard({
|
||||
provider,
|
||||
handleAddModel,
|
||||
onEditInstance,
|
||||
}: {
|
||||
provider: IAvailableProvider;
|
||||
handleAddModel: (factory: string) => void;
|
||||
onEditInstance?: (
|
||||
providerName: string,
|
||||
instance: IProviderInstance,
|
||||
models: IInstanceModel[],
|
||||
) => void;
|
||||
}) {
|
||||
const { data: instances } = useFetchProviderInstances(provider.name);
|
||||
|
||||
@@ -86,6 +100,7 @@ function ProviderCard({
|
||||
instance={instance}
|
||||
providerName={provider.name}
|
||||
handleAddModel={handleAddModel}
|
||||
onEditInstance={onEditInstance}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -98,10 +113,16 @@ function InstanceRow({
|
||||
instance,
|
||||
providerName,
|
||||
// handleAddModel,
|
||||
onEditInstance,
|
||||
}: {
|
||||
instance: IProviderInstance;
|
||||
providerName: string;
|
||||
handleAddModel: (factory: string) => void;
|
||||
onEditInstance?: (
|
||||
providerName: string,
|
||||
instance: IProviderInstance,
|
||||
models: IInstanceModel[],
|
||||
) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [visible, setVisible] = useState(false);
|
||||
@@ -157,6 +178,8 @@ function InstanceRow({
|
||||
<InstanceModelList
|
||||
providerName={providerName}
|
||||
instanceName={instance.instance_name}
|
||||
instance={instance}
|
||||
onEditInstance={onEditInstance}
|
||||
/>
|
||||
</CollapsibleContent>
|
||||
</div>
|
||||
@@ -167,11 +190,41 @@ function InstanceRow({
|
||||
function InstanceModelList({
|
||||
providerName,
|
||||
instanceName,
|
||||
instance,
|
||||
onEditInstance,
|
||||
}: {
|
||||
providerName: string;
|
||||
instanceName: string;
|
||||
instance: IProviderInstance;
|
||||
onEditInstance?: (
|
||||
providerName: string,
|
||||
instance: IProviderInstance,
|
||||
models: IInstanceModel[],
|
||||
) => void;
|
||||
}) {
|
||||
const { data: models } = useFetchInstanceModels(providerName, instanceName);
|
||||
// Lazily fetches the full instance details (incl. baseUrl) only when
|
||||
// the user opens the settings dialog — keeps the collapsed section
|
||||
// cheap and avoids the extra request for users who never click it.
|
||||
const { refetch: fetchInstanceDetails } = useFetchProviderInstance(
|
||||
providerName,
|
||||
instanceName,
|
||||
);
|
||||
|
||||
const handleSettingsClick = useCallback(async () => {
|
||||
let details: IProviderInstance = instance;
|
||||
try {
|
||||
const ret = await fetchInstanceDetails();
|
||||
if (ret.data) {
|
||||
details = { ...instance, ...(ret.data as IProviderInstance) };
|
||||
}
|
||||
} catch {
|
||||
// Fall back to the list-level instance data if the show request
|
||||
// fails (e.g. network error) — the modal still gets a usable
|
||||
// baseline.
|
||||
}
|
||||
onEditInstance?.(providerName, details, models);
|
||||
}, [fetchInstanceDetails, instance, models, onEditInstance, providerName]);
|
||||
|
||||
const modelTypes = useMemo(() => {
|
||||
const types = new Set<string>();
|
||||
@@ -187,15 +240,22 @@ function InstanceModelList({
|
||||
<div className="px-4 pb-4">
|
||||
{/* Model type tags */}
|
||||
{modelTypes.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 mb-3">
|
||||
{modelTypes.map((type) => (
|
||||
<span
|
||||
key={type}
|
||||
className="px-2 py-1 text-xs bg-bg-card text-text-secondary rounded-md"
|
||||
>
|
||||
{mapModelKey[type.trim() as keyof typeof mapModelKey] || type}
|
||||
</span>
|
||||
))}
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex flex-wrap gap-2 mb-3">
|
||||
{modelTypes.map((type) => (
|
||||
<span
|
||||
key={type}
|
||||
className="px-2 py-1 text-xs bg-bg-card text-text-secondary rounded-md"
|
||||
>
|
||||
{mapModelKey[type.trim() as keyof typeof mapModelKey] || type}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
{false && (
|
||||
<Button size="icon" variant="ghost" onClick={handleSettingsClick}>
|
||||
<Settings size={12} />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { LLMFactory } from '@/constants/llm';
|
||||
import { useSetModalState } from '@/hooks/common-hooks';
|
||||
import {
|
||||
useAddInstanceModel,
|
||||
@@ -7,25 +6,22 @@ import {
|
||||
useFetchProviderInstances,
|
||||
useVerifyProviderConnection,
|
||||
} from '@/hooks/use-llm-request';
|
||||
import { IAddProviderInstanceRequestBody } from '@/interfaces/request/llm';
|
||||
import { getRealModelName } from '@/utils/llm-util';
|
||||
import {
|
||||
IAddProviderInstanceRequestBody,
|
||||
IModelInfo,
|
||||
} from '@/interfaces/request/llm';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { ApiKeyPostBody } from '../interface';
|
||||
import { MinerUFormValues } from './modal/mineru-modal';
|
||||
import { splitProviderPayload } from './payload-utils';
|
||||
|
||||
type SavingParamsState = {
|
||||
llm_factory: string;
|
||||
llm_name?: string;
|
||||
model_type?: string;
|
||||
instance_name?: string;
|
||||
base_url?: string;
|
||||
};
|
||||
export type VerifyResult = {
|
||||
isValid: boolean | null;
|
||||
logs: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Unified Provider instance submission hook
|
||||
* Internally handles both verify and save modes
|
||||
*/
|
||||
const useSubmitProviderInstance = () => {
|
||||
const { addProviderInstance } = useAddProviderInstance();
|
||||
const { addInstanceModel } = useAddInstanceModel();
|
||||
@@ -36,6 +32,13 @@ const useSubmitProviderInstance = () => {
|
||||
return addProviderInstance({ ...payload, verify: true });
|
||||
}
|
||||
|
||||
// Multi-model flow: when model_info is provided as an array, the
|
||||
// backend is expected to create the instance and all listed models
|
||||
// in a single addProviderInstance call. Skip the instance/model split.
|
||||
if (Array.isArray((payload as any).model_info)) {
|
||||
return addProviderInstance(payload as IAddProviderInstanceRequestBody);
|
||||
}
|
||||
|
||||
const { instancePayload, modelPayload } = splitProviderPayload(payload);
|
||||
const hasModelPayload =
|
||||
!!modelPayload.model_name && !!modelPayload.model_type;
|
||||
@@ -88,6 +91,7 @@ export const useHideWhenInstanceExists = (instanceNameSet: Set<string>) => {
|
||||
[instanceNameSet],
|
||||
);
|
||||
};
|
||||
|
||||
export const useVerifyConnection = () => {
|
||||
const { verifyProviderConnection } = useVerifyProviderConnection();
|
||||
|
||||
@@ -97,12 +101,14 @@ export const useVerifyConnection = () => {
|
||||
apiKey: string,
|
||||
baseUrl?: string,
|
||||
region?: string,
|
||||
modelInfo?: IModelInfo[],
|
||||
) => {
|
||||
const ret = await verifyProviderConnection({
|
||||
provider_name: providerName,
|
||||
api_key: apiKey,
|
||||
base_url: baseUrl,
|
||||
region: region,
|
||||
model_info: modelInfo,
|
||||
});
|
||||
|
||||
if (ret.code === 0) {
|
||||
@@ -121,494 +127,13 @@ export const useVerifyConnection = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export const useSubmitApiKey = () => {
|
||||
const [savingParams, setSavingParams] = useState<SavingParamsState>(
|
||||
{} as SavingParamsState,
|
||||
);
|
||||
const [editMode, setEditMode] = useState(false);
|
||||
const submitProviderInstance = useSubmitProviderInstance();
|
||||
const verifyConnection = useVerifyConnection();
|
||||
const [saveLoading, setSaveLoading] = useState(false);
|
||||
const {
|
||||
visible: apiKeyVisible,
|
||||
hideModal: hideApiKeyModal,
|
||||
showModal: showApiKeyModal,
|
||||
} = useSetModalState();
|
||||
|
||||
const onApiKeySavingOk = useCallback(
|
||||
async (postBody: ApiKeyPostBody, isVerify = false) => {
|
||||
if (!isVerify) {
|
||||
setSaveLoading(true);
|
||||
}
|
||||
const apiKey: string = postBody.api_key || '';
|
||||
|
||||
let region: string | undefined;
|
||||
if (savingParams.llm_factory === LLMFactory.SILICONFLOW) {
|
||||
const baseUrl = postBody.base_url;
|
||||
if (baseUrl) {
|
||||
try {
|
||||
const parsed = new URL(baseUrl);
|
||||
const host = parsed.hostname.toLowerCase();
|
||||
if (
|
||||
host === 'api.siliconflow.com' ||
|
||||
host.endsWith('.api.siliconflow.com')
|
||||
) {
|
||||
region = 'intl';
|
||||
}
|
||||
} catch {
|
||||
// ignore invalid URL
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use dedicated verify API for verification
|
||||
if (isVerify) {
|
||||
const res = await verifyConnection(
|
||||
savingParams.llm_factory,
|
||||
postBody.api_key,
|
||||
postBody.base_url,
|
||||
region,
|
||||
);
|
||||
return res;
|
||||
}
|
||||
|
||||
const req: IAddProviderInstanceRequestBody = {
|
||||
instance_name:
|
||||
postBody.instance_name || savingParams.instance_name || '',
|
||||
llm_factory: savingParams.llm_factory,
|
||||
llm_name: savingParams.llm_name || '',
|
||||
model_type: savingParams.model_type || '',
|
||||
api_key: apiKey,
|
||||
api_base: postBody.base_url || '',
|
||||
max_tokens: 0,
|
||||
...(region ? { region } : {}),
|
||||
};
|
||||
|
||||
const ret = await submitProviderInstance(req, isVerify);
|
||||
if (!isVerify) {
|
||||
setSaveLoading(false);
|
||||
if (ret.code === 0) {
|
||||
hideApiKeyModal();
|
||||
setEditMode(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
[hideApiKeyModal, submitProviderInstance, savingParams, verifyConnection],
|
||||
);
|
||||
|
||||
const onShowApiKeyModal = useCallback(
|
||||
(savingParams: SavingParamsState, isEdit = false) => {
|
||||
setSavingParams(savingParams);
|
||||
setEditMode(isEdit);
|
||||
showApiKeyModal();
|
||||
},
|
||||
[showApiKeyModal, setSavingParams],
|
||||
);
|
||||
|
||||
return {
|
||||
saveApiKeyLoading: saveLoading,
|
||||
initialApiKey: '',
|
||||
llmFactory: savingParams.llm_factory,
|
||||
editMode,
|
||||
onApiKeySavingOk,
|
||||
apiKeyVisible,
|
||||
hideApiKeyModal,
|
||||
showApiKeyModal: onShowApiKeyModal,
|
||||
};
|
||||
};
|
||||
|
||||
export const useSubmitOllama = () => {
|
||||
const [selectedLlmFactory, setSelectedLlmFactory] = useState<string>('');
|
||||
const [editMode, setEditMode] = useState(false);
|
||||
const [initialValues, setInitialValues] = useState<
|
||||
Partial<IAddProviderInstanceRequestBody> & { provider_order?: string }
|
||||
>();
|
||||
const [saveLoading, setSaveLoading] = useState(false);
|
||||
const submitProviderInstance = useSubmitProviderInstance();
|
||||
const {
|
||||
visible: llmAddingVisible,
|
||||
hideModal: hideLlmAddingModal,
|
||||
showModal: showLlmAddingModal,
|
||||
} = useSetModalState();
|
||||
|
||||
const onLlmAddingOk = useCallback(
|
||||
async (payload: IAddProviderInstanceRequestBody, isVerify = false) => {
|
||||
if (!isVerify) {
|
||||
setSaveLoading(true);
|
||||
}
|
||||
const cleanedPayload = { ...payload };
|
||||
// if (
|
||||
// !cleanedPayload.api_key ||
|
||||
// (typeof cleanedPayload.api_key === 'string' &&
|
||||
// cleanedPayload.api_key.trim() === '')
|
||||
// ) {
|
||||
// delete cleanedPayload.api_key;
|
||||
// }
|
||||
|
||||
const ret = await submitProviderInstance(cleanedPayload, isVerify);
|
||||
if (!isVerify) {
|
||||
setSaveLoading(false);
|
||||
if (ret.code === 0) {
|
||||
hideLlmAddingModal();
|
||||
setEditMode(false);
|
||||
setInitialValues(undefined);
|
||||
}
|
||||
}
|
||||
if (isVerify) {
|
||||
let res = {} as VerifyResult;
|
||||
if (ret.data?.success) {
|
||||
res = {
|
||||
isValid: true,
|
||||
logs: ret.data?.message,
|
||||
};
|
||||
} else {
|
||||
res = {
|
||||
isValid: false,
|
||||
logs: ret.data?.message,
|
||||
};
|
||||
}
|
||||
return res;
|
||||
}
|
||||
},
|
||||
[hideLlmAddingModal, submitProviderInstance, setSaveLoading],
|
||||
);
|
||||
|
||||
const handleShowLlmAddingModal = (
|
||||
llmFactory: string,
|
||||
isEdit = false,
|
||||
modelData?: any,
|
||||
detailedData?: any,
|
||||
) => {
|
||||
setSelectedLlmFactory(llmFactory);
|
||||
setEditMode(isEdit);
|
||||
|
||||
if (isEdit && detailedData) {
|
||||
const initialVals = {
|
||||
instance_name:
|
||||
detailedData.instance_name || getRealModelName(detailedData.name),
|
||||
llm_name: getRealModelName(detailedData.name),
|
||||
model_type: detailedData.type,
|
||||
api_base: detailedData.api_base || '',
|
||||
max_tokens: detailedData.max_tokens || 8192,
|
||||
api_key: '',
|
||||
is_tools: detailedData.is_tools || false,
|
||||
};
|
||||
setInitialValues(initialVals);
|
||||
} else {
|
||||
setInitialValues(undefined);
|
||||
}
|
||||
showLlmAddingModal();
|
||||
};
|
||||
|
||||
return {
|
||||
llmAddingLoading: saveLoading,
|
||||
editMode,
|
||||
initialValues,
|
||||
onLlmAddingOk,
|
||||
llmAddingVisible,
|
||||
hideLlmAddingModal,
|
||||
showLlmAddingModal: handleShowLlmAddingModal,
|
||||
selectedLlmFactory,
|
||||
};
|
||||
};
|
||||
|
||||
export const useSubmitVolcEngine = () => {
|
||||
const [saveLoading, setSaveLoading] = useState(false);
|
||||
const submitProviderInstance = useSubmitProviderInstance();
|
||||
const {
|
||||
visible: volcAddingVisible,
|
||||
hideModal: hideVolcAddingModal,
|
||||
showModal: showVolcAddingModal,
|
||||
} = useSetModalState();
|
||||
|
||||
const onVolcAddingOk = useCallback(
|
||||
async (payload: IAddProviderInstanceRequestBody, isVerify = false) => {
|
||||
if (!isVerify) {
|
||||
setSaveLoading(true);
|
||||
}
|
||||
const ret = await submitProviderInstance(payload, isVerify);
|
||||
if (!isVerify) {
|
||||
setSaveLoading(false);
|
||||
if (ret.code === 0) {
|
||||
hideVolcAddingModal();
|
||||
}
|
||||
}
|
||||
if (isVerify) {
|
||||
let res = {} as VerifyResult;
|
||||
if (ret.data?.success) {
|
||||
res = {
|
||||
isValid: true,
|
||||
logs: ret.data?.message,
|
||||
};
|
||||
} else {
|
||||
res = {
|
||||
isValid: false,
|
||||
logs: ret.data?.message,
|
||||
};
|
||||
}
|
||||
return res;
|
||||
}
|
||||
},
|
||||
[hideVolcAddingModal, submitProviderInstance, setSaveLoading],
|
||||
);
|
||||
|
||||
return {
|
||||
volcAddingLoading: saveLoading,
|
||||
onVolcAddingOk,
|
||||
volcAddingVisible,
|
||||
hideVolcAddingModal,
|
||||
showVolcAddingModal,
|
||||
};
|
||||
};
|
||||
|
||||
export const useSubmitTencentCloud = () => {
|
||||
const [saveLoading, setSaveLoading] = useState(false);
|
||||
const submitProviderInstance = useSubmitProviderInstance();
|
||||
const {
|
||||
visible: TencentCloudAddingVisible,
|
||||
hideModal: hideTencentCloudAddingModal,
|
||||
showModal: showTencentCloudAddingModal,
|
||||
} = useSetModalState();
|
||||
|
||||
const onTencentCloudAddingOk = useCallback(
|
||||
async (payload: IAddProviderInstanceRequestBody, isVerify = false) => {
|
||||
if (!isVerify) {
|
||||
setSaveLoading(true);
|
||||
}
|
||||
const ret = await submitProviderInstance(payload, isVerify);
|
||||
if (!isVerify) {
|
||||
setSaveLoading(false);
|
||||
if (ret.code === 0) {
|
||||
hideTencentCloudAddingModal();
|
||||
}
|
||||
}
|
||||
if (isVerify) {
|
||||
let res = {} as VerifyResult;
|
||||
if (ret.data?.success) {
|
||||
res = {
|
||||
isValid: true,
|
||||
logs: ret.data?.message,
|
||||
};
|
||||
} else {
|
||||
res = {
|
||||
isValid: false,
|
||||
logs: ret.data?.message,
|
||||
};
|
||||
}
|
||||
return res;
|
||||
}
|
||||
},
|
||||
[hideTencentCloudAddingModal, submitProviderInstance, setSaveLoading],
|
||||
);
|
||||
|
||||
return {
|
||||
TencentCloudAddingLoading: saveLoading,
|
||||
onTencentCloudAddingOk,
|
||||
TencentCloudAddingVisible,
|
||||
hideTencentCloudAddingModal,
|
||||
showTencentCloudAddingModal,
|
||||
};
|
||||
};
|
||||
|
||||
export const useSubmitSpark = () => {
|
||||
const [saveLoading, setSaveLoading] = useState(false);
|
||||
const submitProviderInstance = useSubmitProviderInstance();
|
||||
const {
|
||||
visible: SparkAddingVisible,
|
||||
hideModal: hideSparkAddingModal,
|
||||
showModal: showSparkAddingModal,
|
||||
} = useSetModalState();
|
||||
|
||||
const onSparkAddingOk = useCallback(
|
||||
async (payload: IAddProviderInstanceRequestBody, isVerify = false) => {
|
||||
if (!isVerify) {
|
||||
setSaveLoading(true);
|
||||
}
|
||||
const ret = await submitProviderInstance(payload, isVerify);
|
||||
if (!isVerify) {
|
||||
setSaveLoading(false);
|
||||
if (ret.code === 0) {
|
||||
hideSparkAddingModal();
|
||||
}
|
||||
}
|
||||
if (isVerify) {
|
||||
let res = {} as VerifyResult;
|
||||
if (ret.data?.success) {
|
||||
res = {
|
||||
isValid: true,
|
||||
logs: ret.data?.message,
|
||||
};
|
||||
} else {
|
||||
res = {
|
||||
isValid: false,
|
||||
logs: ret.data?.message,
|
||||
};
|
||||
}
|
||||
return res;
|
||||
}
|
||||
},
|
||||
[hideSparkAddingModal, submitProviderInstance, setSaveLoading],
|
||||
);
|
||||
|
||||
return {
|
||||
SparkAddingLoading: saveLoading,
|
||||
onSparkAddingOk,
|
||||
SparkAddingVisible,
|
||||
hideSparkAddingModal,
|
||||
showSparkAddingModal,
|
||||
};
|
||||
};
|
||||
|
||||
export const useSubmityiyan = () => {
|
||||
const [saveLoading, setSaveLoading] = useState(false);
|
||||
const submitProviderInstance = useSubmitProviderInstance();
|
||||
const {
|
||||
visible: yiyanAddingVisible,
|
||||
hideModal: hideyiyanAddingModal,
|
||||
showModal: showyiyanAddingModal,
|
||||
} = useSetModalState();
|
||||
|
||||
const onyiyanAddingOk = useCallback(
|
||||
async (payload: IAddProviderInstanceRequestBody, isVerify = false) => {
|
||||
if (!isVerify) {
|
||||
setSaveLoading(true);
|
||||
}
|
||||
const ret = await submitProviderInstance(payload, isVerify);
|
||||
if (!isVerify) {
|
||||
setSaveLoading(false);
|
||||
if (ret.code === 0) {
|
||||
hideyiyanAddingModal();
|
||||
}
|
||||
}
|
||||
if (isVerify) {
|
||||
let res = {} as VerifyResult;
|
||||
if (ret.data?.success) {
|
||||
res = {
|
||||
isValid: true,
|
||||
logs: ret.data?.message,
|
||||
};
|
||||
} else {
|
||||
res = {
|
||||
isValid: false,
|
||||
logs: ret.data?.message,
|
||||
};
|
||||
}
|
||||
return res;
|
||||
}
|
||||
},
|
||||
[hideyiyanAddingModal, submitProviderInstance, setSaveLoading],
|
||||
);
|
||||
|
||||
return {
|
||||
yiyanAddingLoading: saveLoading,
|
||||
onyiyanAddingOk,
|
||||
yiyanAddingVisible,
|
||||
hideyiyanAddingModal,
|
||||
showyiyanAddingModal,
|
||||
};
|
||||
};
|
||||
|
||||
export const useSubmitFishAudio = () => {
|
||||
const [saveLoading, setSaveLoading] = useState(false);
|
||||
const submitProviderInstance = useSubmitProviderInstance();
|
||||
const {
|
||||
visible: FishAudioAddingVisible,
|
||||
hideModal: hideFishAudioAddingModal,
|
||||
showModal: showFishAudioAddingModal,
|
||||
} = useSetModalState();
|
||||
|
||||
const onFishAudioAddingOk = useCallback(
|
||||
async (payload: IAddProviderInstanceRequestBody, isVerify = false) => {
|
||||
if (!isVerify) {
|
||||
setSaveLoading(true);
|
||||
}
|
||||
const ret = await submitProviderInstance(payload, isVerify);
|
||||
if (!isVerify) {
|
||||
setSaveLoading(false);
|
||||
if (ret.code === 0) {
|
||||
hideFishAudioAddingModal();
|
||||
}
|
||||
}
|
||||
if (isVerify) {
|
||||
let res = {} as VerifyResult;
|
||||
if (ret.data?.success) {
|
||||
res = {
|
||||
isValid: true,
|
||||
logs: ret.data?.message,
|
||||
};
|
||||
} else {
|
||||
res = {
|
||||
isValid: false,
|
||||
logs: ret.data?.message,
|
||||
};
|
||||
}
|
||||
return res;
|
||||
}
|
||||
},
|
||||
[hideFishAudioAddingModal, submitProviderInstance, setSaveLoading],
|
||||
);
|
||||
|
||||
return {
|
||||
FishAudioAddingLoading: saveLoading,
|
||||
onFishAudioAddingOk,
|
||||
FishAudioAddingVisible,
|
||||
hideFishAudioAddingModal,
|
||||
showFishAudioAddingModal,
|
||||
};
|
||||
};
|
||||
|
||||
export const useSubmitGoogle = () => {
|
||||
const [saveLoading, setSaveLoading] = useState(false);
|
||||
const submitProviderInstance = useSubmitProviderInstance();
|
||||
const {
|
||||
visible: GoogleAddingVisible,
|
||||
hideModal: hideGoogleAddingModal,
|
||||
showModal: showGoogleAddingModal,
|
||||
} = useSetModalState();
|
||||
|
||||
const onGoogleAddingOk = useCallback(
|
||||
async (payload: IAddProviderInstanceRequestBody, isVerify = false) => {
|
||||
if (!isVerify) {
|
||||
setSaveLoading(true);
|
||||
}
|
||||
const ret = await submitProviderInstance(payload, isVerify);
|
||||
if (!isVerify) {
|
||||
setSaveLoading(false);
|
||||
if (ret.code === 0) {
|
||||
hideGoogleAddingModal();
|
||||
}
|
||||
}
|
||||
if (isVerify) {
|
||||
let res = {} as VerifyResult;
|
||||
if (ret.data?.success) {
|
||||
res = {
|
||||
isValid: true,
|
||||
logs: ret.data?.message,
|
||||
};
|
||||
} else {
|
||||
res = {
|
||||
isValid: false,
|
||||
logs: ret.data?.message,
|
||||
};
|
||||
}
|
||||
return res;
|
||||
}
|
||||
},
|
||||
[hideGoogleAddingModal, submitProviderInstance, setSaveLoading],
|
||||
);
|
||||
|
||||
return {
|
||||
GoogleAddingLoading: saveLoading,
|
||||
onGoogleAddingOk,
|
||||
GoogleAddingVisible,
|
||||
hideGoogleAddingModal,
|
||||
showGoogleAddingModal,
|
||||
};
|
||||
};
|
||||
// ============ Hooks for the 4 retained special modals ============
|
||||
// Bedrock / MinerU / PaddleOCR / OpenDataLoader are not yet merged into ProviderModal
|
||||
|
||||
export const useSubmitBedrock = () => {
|
||||
const [saveLoading, setSaveLoading] = useState(false);
|
||||
const submitProviderInstance = useSubmitProviderInstance();
|
||||
const verifyConnection = useVerifyConnection();
|
||||
const {
|
||||
visible: bedrockAddingVisible,
|
||||
hideModal: hideBedrockAddingModal,
|
||||
@@ -620,30 +145,45 @@ export const useSubmitBedrock = () => {
|
||||
if (!isVerify) {
|
||||
setSaveLoading(true);
|
||||
}
|
||||
const ret = await submitProviderInstance(payload, isVerify);
|
||||
if (!isVerify) {
|
||||
setSaveLoading(false);
|
||||
if (ret.code === 0) {
|
||||
hideBedrockAddingModal();
|
||||
}
|
||||
}
|
||||
if (isVerify) {
|
||||
let res = {} as VerifyResult;
|
||||
if (ret.data?.success) {
|
||||
res = {
|
||||
isValid: true,
|
||||
logs: ret.data?.message,
|
||||
};
|
||||
} else {
|
||||
res = {
|
||||
isValid: false,
|
||||
logs: ret.data?.message,
|
||||
};
|
||||
}
|
||||
return res;
|
||||
const legacyPayload = payload as any;
|
||||
const modelType = Array.isArray(legacyPayload.model_type)
|
||||
? (legacyPayload.model_type as string[])
|
||||
: legacyPayload.model_type
|
||||
? [legacyPayload.model_type as string]
|
||||
: [];
|
||||
const apiKey = JSON.stringify({
|
||||
auth_mode: legacyPayload.auth_mode,
|
||||
bedrock_ak: legacyPayload.bedrock_ak,
|
||||
bedrock_sk: legacyPayload.bedrock_sk,
|
||||
aws_role_arn: legacyPayload.aws_role_arn,
|
||||
});
|
||||
return verifyConnection(
|
||||
payload.llm_factory as string,
|
||||
apiKey,
|
||||
legacyPayload.bedrock_region,
|
||||
undefined,
|
||||
[
|
||||
{
|
||||
model_type: modelType,
|
||||
model_name: (legacyPayload.model_name as string) ?? '',
|
||||
max_tokens: (legacyPayload.max_tokens as number) ?? 0,
|
||||
},
|
||||
],
|
||||
);
|
||||
}
|
||||
const ret = await submitProviderInstance(payload, false);
|
||||
setSaveLoading(false);
|
||||
if (ret.code === 0) {
|
||||
hideBedrockAddingModal();
|
||||
}
|
||||
},
|
||||
[hideBedrockAddingModal, submitProviderInstance, setSaveLoading],
|
||||
[
|
||||
hideBedrockAddingModal,
|
||||
submitProviderInstance,
|
||||
setSaveLoading,
|
||||
verifyConnection,
|
||||
],
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -655,260 +195,13 @@ export const useSubmitBedrock = () => {
|
||||
};
|
||||
};
|
||||
|
||||
export const useSubmitAzure = () => {
|
||||
const [saveLoading, setSaveLoading] = useState(false);
|
||||
const submitProviderInstance = useSubmitProviderInstance();
|
||||
const {
|
||||
visible: AzureAddingVisible,
|
||||
hideModal: hideAzureAddingModal,
|
||||
showModal: showAzureAddingModal,
|
||||
} = useSetModalState();
|
||||
|
||||
const onAzureAddingOk = useCallback(
|
||||
async (payload: IAddProviderInstanceRequestBody, isVerify = false) => {
|
||||
if (!isVerify) {
|
||||
setSaveLoading(true);
|
||||
}
|
||||
const ret = await submitProviderInstance(payload, isVerify);
|
||||
if (!isVerify) {
|
||||
setSaveLoading(false);
|
||||
if (ret.code === 0) {
|
||||
hideAzureAddingModal();
|
||||
}
|
||||
}
|
||||
if (isVerify) {
|
||||
let res = {} as VerifyResult;
|
||||
if (ret.data?.success) {
|
||||
res = {
|
||||
isValid: true,
|
||||
logs: ret.data?.message,
|
||||
};
|
||||
} else {
|
||||
res = {
|
||||
isValid: false,
|
||||
logs: ret.data?.message,
|
||||
};
|
||||
}
|
||||
return res;
|
||||
}
|
||||
},
|
||||
[hideAzureAddingModal, submitProviderInstance, setSaveLoading],
|
||||
);
|
||||
|
||||
return {
|
||||
AzureAddingLoading: saveLoading,
|
||||
onAzureAddingOk,
|
||||
AzureAddingVisible,
|
||||
hideAzureAddingModal,
|
||||
showAzureAddingModal,
|
||||
};
|
||||
};
|
||||
|
||||
export const useSubmitMinerU = () => {
|
||||
const [saveLoading, setSaveLoading] = useState(false);
|
||||
const submitProviderInstance = useSubmitProviderInstance();
|
||||
const {
|
||||
visible: mineruVisible,
|
||||
hideModal: hideMineruModal,
|
||||
showModal: showMineruModal,
|
||||
} = useSetModalState();
|
||||
|
||||
const onMineruOk = useCallback(
|
||||
async (
|
||||
payload: MinerUFormValues & { instance_name: string },
|
||||
isVerify = false,
|
||||
) => {
|
||||
if (!isVerify) {
|
||||
setSaveLoading(true);
|
||||
}
|
||||
const cfg: any = {
|
||||
...payload,
|
||||
mineru_delete_output:
|
||||
(payload.mineru_delete_output ?? true) ? '1' : '0',
|
||||
};
|
||||
delete cfg.instance_name;
|
||||
if (payload.mineru_backend !== 'vlm-http-client') {
|
||||
delete cfg.mineru_server_url;
|
||||
}
|
||||
const req: IAddProviderInstanceRequestBody = {
|
||||
instance_name: payload.instance_name,
|
||||
llm_factory: LLMFactory.MinerU,
|
||||
llm_name: payload.llm_name,
|
||||
model_type: 'ocr',
|
||||
api_key: cfg,
|
||||
api_base: '',
|
||||
max_tokens: 0,
|
||||
};
|
||||
const ret = await submitProviderInstance(req, isVerify);
|
||||
if (!isVerify) {
|
||||
setSaveLoading(false);
|
||||
if (ret.code === 0) {
|
||||
hideMineruModal();
|
||||
}
|
||||
}
|
||||
if (isVerify) {
|
||||
let res = {} as VerifyResult;
|
||||
if (ret.data?.success) {
|
||||
res = {
|
||||
isValid: true,
|
||||
logs: ret.data?.message,
|
||||
};
|
||||
} else {
|
||||
res = {
|
||||
isValid: false,
|
||||
logs: ret.data?.message,
|
||||
};
|
||||
}
|
||||
return res;
|
||||
}
|
||||
},
|
||||
[submitProviderInstance, hideMineruModal, setSaveLoading],
|
||||
);
|
||||
|
||||
return {
|
||||
mineruVisible,
|
||||
hideMineruModal,
|
||||
showMineruModal,
|
||||
onMineruOk,
|
||||
mineruLoading: saveLoading,
|
||||
};
|
||||
};
|
||||
|
||||
export const useSubmitPaddleOCR = () => {
|
||||
const [saveLoading, setSaveLoading] = useState(false);
|
||||
const submitProviderInstance = useSubmitProviderInstance();
|
||||
const {
|
||||
visible: paddleocrVisible,
|
||||
hideModal: hidePaddleOCRModal,
|
||||
showModal: showPaddleOCRModal,
|
||||
} = useSetModalState();
|
||||
|
||||
const onPaddleOCROk = useCallback(
|
||||
async (payload: any, isVerify = false) => {
|
||||
if (!isVerify) {
|
||||
setSaveLoading(true);
|
||||
}
|
||||
const cfg: any = {
|
||||
...payload,
|
||||
};
|
||||
delete cfg.instance_name;
|
||||
const req: IAddProviderInstanceRequestBody = {
|
||||
instance_name: payload.instance_name,
|
||||
llm_factory: LLMFactory.PaddleOCR,
|
||||
llm_name: payload.llm_name,
|
||||
model_type: 'ocr',
|
||||
api_key: cfg,
|
||||
api_base: '',
|
||||
max_tokens: 0,
|
||||
};
|
||||
const ret = await submitProviderInstance(req, isVerify);
|
||||
if (!isVerify) {
|
||||
setSaveLoading(false);
|
||||
if (ret.code === 0) {
|
||||
hidePaddleOCRModal();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (isVerify) {
|
||||
let res = {} as VerifyResult;
|
||||
if (ret.data?.success) {
|
||||
res = {
|
||||
isValid: true,
|
||||
logs: ret.data?.message,
|
||||
};
|
||||
} else {
|
||||
res = {
|
||||
isValid: false,
|
||||
logs: ret.data?.message,
|
||||
};
|
||||
}
|
||||
return res;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
[submitProviderInstance, hidePaddleOCRModal, setSaveLoading],
|
||||
);
|
||||
|
||||
return {
|
||||
paddleocrVisible,
|
||||
hidePaddleOCRModal,
|
||||
showPaddleOCRModal,
|
||||
onPaddleOCROk,
|
||||
paddleocrLoading: saveLoading,
|
||||
};
|
||||
};
|
||||
|
||||
export const useSubmitOpenDataLoader = () => {
|
||||
const [saveLoading, setSaveLoading] = useState(false);
|
||||
const submitProviderInstance = useSubmitProviderInstance();
|
||||
const {
|
||||
visible: opendataloaderVisible,
|
||||
hideModal: hideOpenDataLoaderModal,
|
||||
showModal: showOpenDataLoaderModal,
|
||||
} = useSetModalState();
|
||||
|
||||
const onOpenDataLoaderOk = useCallback(
|
||||
async (payload: any, isVerify = false) => {
|
||||
if (!isVerify) {
|
||||
setSaveLoading(true);
|
||||
}
|
||||
const cfg: any = { ...payload };
|
||||
delete cfg.instance_name;
|
||||
const req: IAddProviderInstanceRequestBody = {
|
||||
instance_name: payload.instance_name,
|
||||
llm_factory: LLMFactory.OpenDataLoader,
|
||||
llm_name: payload.llm_name,
|
||||
model_type: 'ocr',
|
||||
api_key: cfg,
|
||||
api_base: '',
|
||||
max_tokens: 0,
|
||||
};
|
||||
const ret = await submitProviderInstance(req, isVerify);
|
||||
if (!isVerify) {
|
||||
setSaveLoading(false);
|
||||
if (ret.code === 0) {
|
||||
hideOpenDataLoaderModal();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (isVerify) {
|
||||
return {
|
||||
isValid: !!ret.data?.success,
|
||||
logs: ret.data?.message,
|
||||
} as VerifyResult;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
[submitProviderInstance, hideOpenDataLoaderModal, setSaveLoading],
|
||||
);
|
||||
|
||||
return {
|
||||
opendataloaderVisible,
|
||||
hideOpenDataLoaderModal,
|
||||
showOpenDataLoaderModal,
|
||||
onOpenDataLoaderOk,
|
||||
opendataloaderLoading: saveLoading,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Wraps the verify callback: provides a unified call with isVerify=true for the Verify button
|
||||
*/
|
||||
export const useVerifySettings = ({
|
||||
onVerify,
|
||||
}: {
|
||||
onVerify:
|
||||
| ((
|
||||
postBody: ApiKeyPostBody,
|
||||
isVerify?: boolean,
|
||||
) => Promise<VerifyResult | undefined>)
|
||||
| ((
|
||||
payload: IAddProviderInstanceRequestBody,
|
||||
isVerify?: boolean,
|
||||
) => Promise<VerifyResult | undefined>)
|
||||
| ((
|
||||
payload: MinerUFormValues,
|
||||
isVerify?: boolean,
|
||||
) => Promise<VerifyResult | undefined>)
|
||||
| ((payload: any, isVerify?: boolean) => Promise<boolean | VerifyResult>)
|
||||
| (() => void);
|
||||
onVerify: (postBody: any, isVerify?: boolean) => Promise<any>;
|
||||
}) => {
|
||||
const onApiKeyVerifying = useCallback(
|
||||
async (postBody: any) => {
|
||||
|
||||
@@ -1,111 +1,28 @@
|
||||
import Spotlight from '@/components/spotlight';
|
||||
import { LLMFactory } from '@/constants/llm';
|
||||
// import { LlmItem, useFetchMyLlmListDetailed } from '@/hooks/use-llm-request';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import {
|
||||
useAddInstanceModel,
|
||||
useAddProviderInstance,
|
||||
useFetchAvailableProviders,
|
||||
useVerifyProviderConnection,
|
||||
} from '@/hooks/use-llm-request';
|
||||
import { IInstanceModel, IProviderInstance } from '@/interfaces/database/llm';
|
||||
import {
|
||||
IAddProviderInstanceRequestBody,
|
||||
IModelInfo,
|
||||
} from '@/interfaces/request/llm';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { isLocalLlmFactory } from '../utils';
|
||||
import SystemSetting from './components/system-setting';
|
||||
import { AvailableModels } from './components/un-add-model';
|
||||
import { UsedModel } from './components/used-model';
|
||||
import {
|
||||
useSubmitApiKey,
|
||||
useSubmitAzure,
|
||||
useSubmitBedrock,
|
||||
useSubmitFishAudio,
|
||||
useSubmitGoogle,
|
||||
useSubmitMinerU,
|
||||
useSubmitOllama,
|
||||
useSubmitOpenDataLoader,
|
||||
useSubmitPaddleOCR,
|
||||
useSubmitSpark,
|
||||
useSubmitTencentCloud,
|
||||
useSubmitVolcEngine,
|
||||
useSubmityiyan,
|
||||
useVerifySettings,
|
||||
} from './hooks';
|
||||
import ApiKeyModal from './modal/api-key-modal';
|
||||
import AzureOpenAIModal from './modal/azure-openai-modal';
|
||||
import { useSubmitBedrock } from './hooks';
|
||||
import BedrockModal from './modal/bedrock-modal';
|
||||
import FishAudioModal from './modal/fish-audio-modal';
|
||||
import GoogleModal from './modal/google-modal';
|
||||
import MinerUModal from './modal/mineru-modal';
|
||||
import TencentCloudModal from './modal/next-tencent-modal';
|
||||
import OllamaModal from './modal/ollama-modal';
|
||||
import OpenDataLoaderModal from './modal/opendataloader-modal';
|
||||
import PaddleOCRModal from './modal/paddleocr-modal';
|
||||
import SparkModal from './modal/spark-modal';
|
||||
import VolcEngineModal from './modal/volcengine-modal';
|
||||
import YiyanModal from './modal/yiyan-modal';
|
||||
import ProviderModal, { IViewModeOkPayload } from './modal/provider-modal';
|
||||
import { splitProviderPayload } from './payload-utils';
|
||||
|
||||
const ModelProviders = () => {
|
||||
// const { data: detailedLlmList } = useFetchMyLlmListDetailed();
|
||||
const {
|
||||
saveApiKeyLoading,
|
||||
initialApiKey,
|
||||
llmFactory,
|
||||
editMode,
|
||||
onApiKeySavingOk,
|
||||
apiKeyVisible,
|
||||
hideApiKeyModal,
|
||||
showApiKeyModal,
|
||||
} = useSubmitApiKey();
|
||||
const {
|
||||
llmAddingVisible,
|
||||
hideLlmAddingModal,
|
||||
showLlmAddingModal,
|
||||
onLlmAddingOk,
|
||||
llmAddingLoading,
|
||||
editMode: llmEditMode,
|
||||
initialValues: llmInitialValues,
|
||||
selectedLlmFactory,
|
||||
} = useSubmitOllama();
|
||||
|
||||
const {
|
||||
volcAddingVisible,
|
||||
hideVolcAddingModal,
|
||||
showVolcAddingModal,
|
||||
onVolcAddingOk,
|
||||
volcAddingLoading,
|
||||
} = useSubmitVolcEngine();
|
||||
|
||||
const {
|
||||
GoogleAddingVisible,
|
||||
hideGoogleAddingModal,
|
||||
showGoogleAddingModal,
|
||||
onGoogleAddingOk,
|
||||
GoogleAddingLoading,
|
||||
} = useSubmitGoogle();
|
||||
|
||||
const {
|
||||
TencentCloudAddingVisible,
|
||||
hideTencentCloudAddingModal,
|
||||
showTencentCloudAddingModal,
|
||||
onTencentCloudAddingOk,
|
||||
TencentCloudAddingLoading,
|
||||
} = useSubmitTencentCloud();
|
||||
|
||||
const {
|
||||
SparkAddingVisible,
|
||||
hideSparkAddingModal,
|
||||
showSparkAddingModal,
|
||||
onSparkAddingOk,
|
||||
SparkAddingLoading,
|
||||
} = useSubmitSpark();
|
||||
|
||||
const {
|
||||
yiyanAddingVisible,
|
||||
hideyiyanAddingModal,
|
||||
showyiyanAddingModal,
|
||||
onyiyanAddingOk,
|
||||
yiyanAddingLoading,
|
||||
} = useSubmityiyan();
|
||||
|
||||
const {
|
||||
FishAudioAddingVisible,
|
||||
hideFishAudioAddingModal,
|
||||
showFishAudioAddingModal,
|
||||
onFishAudioAddingOk,
|
||||
FishAudioAddingLoading,
|
||||
} = useSubmitFishAudio();
|
||||
|
||||
// 4 retained special modals
|
||||
const {
|
||||
bedrockAddingLoading,
|
||||
onBedrockAddingOk,
|
||||
@@ -114,299 +31,354 @@ const ModelProviders = () => {
|
||||
showBedrockAddingModal,
|
||||
} = useSubmitBedrock();
|
||||
|
||||
const {
|
||||
AzureAddingVisible,
|
||||
hideAzureAddingModal,
|
||||
showAzureAddingModal,
|
||||
onAzureAddingOk,
|
||||
AzureAddingLoading,
|
||||
} = useSubmitAzure();
|
||||
// Unified ProviderModal state
|
||||
const [providerVisible, setProviderVisible] = useState(false);
|
||||
const [currentLlmFactory, setCurrentLlmFactory] = useState<string>('');
|
||||
const [providerLoading, setProviderLoading] = useState(false);
|
||||
|
||||
const {
|
||||
mineruVisible,
|
||||
hideMineruModal,
|
||||
showMineruModal,
|
||||
onMineruOk,
|
||||
mineruLoading,
|
||||
} = useSubmitMinerU();
|
||||
// viewMode (edit-models) state: when true, ProviderModal opens in
|
||||
// read-only mode for everything except the model-related fields.
|
||||
// `viewModeInitialValues` carries the existing instance + model data.
|
||||
const [viewMode, setViewMode] = useState(false);
|
||||
const [viewModeInitialValues, setViewModeInitialValues] = useState<
|
||||
Record<string, any> | undefined
|
||||
>(undefined);
|
||||
|
||||
const {
|
||||
paddleocrVisible,
|
||||
hidePaddleOCRModal,
|
||||
showPaddleOCRModal,
|
||||
onPaddleOCROk,
|
||||
paddleocrLoading,
|
||||
} = useSubmitPaddleOCR();
|
||||
// ProviderModal submission logic: calls addProviderInstance + addInstanceModel
|
||||
const { addProviderInstance } = useAddProviderInstance();
|
||||
const { addInstanceModel } = useAddInstanceModel();
|
||||
const { verifyProviderConnection } = useVerifyProviderConnection();
|
||||
const { data: availableProviders } = useFetchAvailableProviders();
|
||||
|
||||
const {
|
||||
opendataloaderVisible,
|
||||
hideOpenDataLoaderModal,
|
||||
showOpenDataLoaderModal,
|
||||
onOpenDataLoaderOk,
|
||||
opendataloaderLoading,
|
||||
} = useSubmitOpenDataLoader();
|
||||
// Convert IAvailableProvider.url to baseUrlOptions
|
||||
// IAvailableProvider.url looks like { default?: string; cn?: string; intl?: string; ... }
|
||||
// Mapped to [{ value: 'https://...', regionKey: 'default', label: <span>https://...<span>default</span></span> }, ...]
|
||||
// `regionKey` carries the original key so the modal can map the currently
|
||||
// selected URL back to its key for the `region` submit field.
|
||||
const buildBaseUrlOptions = useCallback(
|
||||
(urlObj?: Record<string, string | undefined>) => {
|
||||
if (!urlObj) return undefined;
|
||||
const options = Object.keys(urlObj)
|
||||
.filter((k) => !!urlObj[k])
|
||||
.map((k) => {
|
||||
const v = urlObj[k] as string;
|
||||
// if (k === 'default') {
|
||||
// return { value: v, label: v };
|
||||
// }
|
||||
return {
|
||||
value: v,
|
||||
regionKey: k,
|
||||
label: (
|
||||
<div className="flex justify-between items-center gap-2">
|
||||
<span className="truncate">{v}</span>
|
||||
<span className="text-xs text-text-secondary bg-bg-card px-2 py-0.5 rounded-sm shrink-0">
|
||||
{k}
|
||||
</span>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
});
|
||||
return options.length > 0 ? options : undefined;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
// baseUrlOptions for the current factory (looked up from availableProviders)
|
||||
const currentProvider = useMemo(
|
||||
() =>
|
||||
currentLlmFactory
|
||||
? availableProviders.find((p) => p.name === currentLlmFactory)
|
||||
: undefined,
|
||||
[availableProviders, currentLlmFactory],
|
||||
);
|
||||
const currentBaseUrlOptions = useMemo(
|
||||
() => buildBaseUrlOptions(currentProvider?.url),
|
||||
[buildBaseUrlOptions, currentProvider],
|
||||
);
|
||||
|
||||
const handleProviderOk = useCallback(
|
||||
async (payload: IAddProviderInstanceRequestBody, isVerify = false) => {
|
||||
if (!isVerify) setProviderLoading(true);
|
||||
try {
|
||||
if (isVerify) {
|
||||
// Verify mode: call verify API
|
||||
const ret = await addProviderInstance({ ...payload, verify: true });
|
||||
return ret;
|
||||
}
|
||||
// Normal submission
|
||||
const { instancePayload, modelPayload } = splitProviderPayload(payload);
|
||||
const hasModelPayload =
|
||||
!!modelPayload.model_name && !!modelPayload.model_type;
|
||||
const instanceRet = await addProviderInstance({
|
||||
...instancePayload,
|
||||
llm_factory: payload.llm_factory,
|
||||
instance_name: payload.instance_name,
|
||||
} as IAddProviderInstanceRequestBody);
|
||||
if (instanceRet.code !== 0) {
|
||||
return instanceRet;
|
||||
}
|
||||
// When model information has been submitted nested in the instance via model_info
|
||||
// (e.g., VolcEngine / LocalLLM), addInstanceModel is no longer called separately;
|
||||
// close the modal directly.
|
||||
if (!hasModelPayload) {
|
||||
setProviderVisible(false);
|
||||
return instanceRet;
|
||||
}
|
||||
const modelRet = await addInstanceModel({
|
||||
provider_name: payload.llm_factory,
|
||||
instance_name: payload.instance_name,
|
||||
...modelPayload,
|
||||
});
|
||||
if (modelRet.code === 0) {
|
||||
setProviderVisible(false);
|
||||
}
|
||||
return modelRet;
|
||||
} finally {
|
||||
if (!isVerify) setProviderLoading(false);
|
||||
}
|
||||
},
|
||||
[addProviderInstance, addInstanceModel],
|
||||
);
|
||||
|
||||
const handleProviderVerify = useCallback(
|
||||
async (params: any) => {
|
||||
// ProviderModal's handleVerify flattens verifyArgs onto params
|
||||
// verifyArgs comes from config.verifyTransform, fields are apiKey/baseUrl/region/modelInfo
|
||||
const apiKey = params.apiKey ?? params.api_key ?? params._apiKey ?? '';
|
||||
const baseUrl = params.baseUrl ?? params.base_url ?? params._baseUrl;
|
||||
const region = params.region ?? params._region;
|
||||
const modelInfo =
|
||||
params.modelInfo ?? params.model_info ?? params._modelInfo;
|
||||
const ret = await verifyProviderConnection({
|
||||
provider_name: params.llm_factory ?? currentLlmFactory,
|
||||
api_key: apiKey,
|
||||
base_url: baseUrl,
|
||||
region: region,
|
||||
model_info: modelInfo,
|
||||
});
|
||||
if (ret.code === 0) {
|
||||
return { isValid: true, logs: ret.message };
|
||||
}
|
||||
return { isValid: false, logs: ret.message };
|
||||
},
|
||||
[verifyProviderConnection, currentLlmFactory],
|
||||
);
|
||||
|
||||
const ModalMap = useMemo(
|
||||
() => ({
|
||||
[LLMFactory.Bedrock]: showBedrockAddingModal,
|
||||
[LLMFactory.VolcEngine]: showVolcAddingModal,
|
||||
[LLMFactory.XunFeiSpark]: showSparkAddingModal,
|
||||
[LLMFactory.BaiduYiYan]: showyiyanAddingModal,
|
||||
[LLMFactory.FishAudio]: showFishAudioAddingModal,
|
||||
[LLMFactory.TencentCloud]: showTencentCloudAddingModal,
|
||||
[LLMFactory.GoogleCloud]: showGoogleAddingModal,
|
||||
[LLMFactory.AzureOpenAI]: showAzureAddingModal,
|
||||
[LLMFactory.MinerU]: showMineruModal,
|
||||
[LLMFactory.PaddleOCR]: showPaddleOCRModal,
|
||||
[LLMFactory.OpenDataLoader]: showOpenDataLoaderModal,
|
||||
[LLMFactory.VolcEngine]: () => {
|
||||
setCurrentLlmFactory(LLMFactory.VolcEngine);
|
||||
setProviderVisible(true);
|
||||
},
|
||||
[LLMFactory.XunFeiSpark]: () => {
|
||||
setCurrentLlmFactory(LLMFactory.XunFeiSpark);
|
||||
setProviderVisible(true);
|
||||
},
|
||||
[LLMFactory.BaiduYiYan]: () => {
|
||||
setCurrentLlmFactory(LLMFactory.BaiduYiYan);
|
||||
setProviderVisible(true);
|
||||
},
|
||||
[LLMFactory.FishAudio]: () => {
|
||||
setCurrentLlmFactory(LLMFactory.FishAudio);
|
||||
setProviderVisible(true);
|
||||
},
|
||||
[LLMFactory.TencentCloud]: () => {
|
||||
setCurrentLlmFactory(LLMFactory.TencentCloud);
|
||||
setProviderVisible(true);
|
||||
},
|
||||
[LLMFactory.GoogleCloud]: () => {
|
||||
setCurrentLlmFactory(LLMFactory.GoogleCloud);
|
||||
setProviderVisible(true);
|
||||
},
|
||||
[LLMFactory.AzureOpenAI]: () => {
|
||||
setCurrentLlmFactory(LLMFactory.AzureOpenAI);
|
||||
setProviderVisible(true);
|
||||
},
|
||||
[LLMFactory.MinerU]: () => {
|
||||
setCurrentLlmFactory(LLMFactory.MinerU);
|
||||
setProviderVisible(true);
|
||||
},
|
||||
[LLMFactory.PaddleOCR]: () => {
|
||||
setCurrentLlmFactory(LLMFactory.PaddleOCR);
|
||||
setProviderVisible(true);
|
||||
},
|
||||
[LLMFactory.OpenDataLoader]: () => {
|
||||
setCurrentLlmFactory(LLMFactory.OpenDataLoader);
|
||||
setProviderVisible(true);
|
||||
},
|
||||
}),
|
||||
[
|
||||
showBedrockAddingModal,
|
||||
showVolcAddingModal,
|
||||
showSparkAddingModal,
|
||||
showyiyanAddingModal,
|
||||
showFishAudioAddingModal,
|
||||
showTencentCloudAddingModal,
|
||||
showGoogleAddingModal,
|
||||
showAzureAddingModal,
|
||||
showMineruModal,
|
||||
showPaddleOCRModal,
|
||||
showOpenDataLoaderModal,
|
||||
],
|
||||
[showBedrockAddingModal],
|
||||
);
|
||||
|
||||
const handleAddModel = useCallback(
|
||||
(llmFactory: string) => {
|
||||
console.log('handleAddModel', llmFactory);
|
||||
if (isLocalLlmFactory(llmFactory)) {
|
||||
showLlmAddingModal(llmFactory);
|
||||
setCurrentLlmFactory(llmFactory);
|
||||
setProviderVisible(true);
|
||||
} else if (llmFactory in ModalMap) {
|
||||
ModalMap[llmFactory as keyof typeof ModalMap]();
|
||||
} else {
|
||||
showApiKeyModal({ llm_factory: llmFactory });
|
||||
setCurrentLlmFactory(llmFactory);
|
||||
setProviderVisible(true);
|
||||
}
|
||||
},
|
||||
[showApiKeyModal, showLlmAddingModal, ModalMap],
|
||||
[ModalMap],
|
||||
);
|
||||
|
||||
// const handleEditModel = useCallback(
|
||||
// (model: any, factory: LlmItem) => {
|
||||
// if (factory) {
|
||||
// const detailedFactory = detailedLlmList[factory.name];
|
||||
// const detailedModel = detailedFactory?.llm?.find(
|
||||
// (m: any) => m.name === model.name,
|
||||
// );
|
||||
// Open the ProviderModal in viewMode (read-only) for an existing
|
||||
// instance so the user can edit its model list. The instance's
|
||||
// `api_key`, `baseUrl` and `model_info` are passed as initial values;
|
||||
// the list picker uses `model_info` to pre-check the already-added
|
||||
// models.
|
||||
const handleEditInstance = useCallback(
|
||||
(
|
||||
providerName: string,
|
||||
instance: IProviderInstance,
|
||||
models: IInstanceModel[],
|
||||
) => {
|
||||
setCurrentLlmFactory(providerName);
|
||||
const modelInfos: IModelInfo[] = models.map((m) => ({
|
||||
model_name: m.name,
|
||||
model_type: m.model_type,
|
||||
max_tokens: m.max_tokens ?? 0,
|
||||
}));
|
||||
// For non-LIST_MODEL_PROVIDERS, the modal renders model_name /
|
||||
// model_type / max_tokens / is_tools as form fields, so seed
|
||||
// them from the first existing model to match what the user sees
|
||||
// in the instance list.
|
||||
const firstModel = models[0];
|
||||
setViewModeInitialValues({
|
||||
instance_name: instance.instance_name,
|
||||
api_key: instance.api_key,
|
||||
// baseUrl is only present when the showProviderInstance endpoint
|
||||
// returned it; pass it as both `base_url` and `api_base` so it
|
||||
// fills the form field regardless of which name the provider
|
||||
// config uses.
|
||||
...(instance.base_url
|
||||
? { base_url: instance.base_url, api_base: instance.base_url }
|
||||
: {}),
|
||||
...(firstModel
|
||||
? {
|
||||
model_name: firstModel.name,
|
||||
model_type: firstModel.model_type,
|
||||
max_tokens: firstModel.max_tokens,
|
||||
}
|
||||
: {}),
|
||||
model_info: modelInfos,
|
||||
});
|
||||
setViewMode(true);
|
||||
setProviderVisible(true);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
// const editData = {
|
||||
// llm_factory: factory.name,
|
||||
// llm_name: model.name,
|
||||
// model_type: model.type,
|
||||
// };
|
||||
// viewMode save handler: receives the list of selected models (or
|
||||
// the editable model fields for non-LIST_MODEL_PROVIDERS) from the
|
||||
// modal and adds them via `addInstanceModel`. Does NOT call
|
||||
// `addProviderInstance` because the instance itself is unchanged.
|
||||
const handleViewModeOk = useCallback(
|
||||
async (payload: IViewModeOkPayload) => {
|
||||
setProviderLoading(true);
|
||||
try {
|
||||
if (payload.modelInfos.length > 0) {
|
||||
// LIST_MODEL_PROVIDERS: full sync — call addInstanceModel for
|
||||
// every selected model. The backend is idempotent so re-adding
|
||||
// an already-present model is a no-op.
|
||||
for (const model of payload.modelInfos) {
|
||||
const modelType = Array.isArray(model.model_type)
|
||||
? model.model_type
|
||||
: model.model_type
|
||||
? [model.model_type as string]
|
||||
: [];
|
||||
const ret = await addInstanceModel({
|
||||
provider_name: payload.llmFactory,
|
||||
instance_name: payload.instanceName,
|
||||
model_name: model.model_name,
|
||||
model_type: modelType,
|
||||
max_tokens: model.max_tokens ?? 0,
|
||||
...(model.extra ? { extra: model.extra } : {}),
|
||||
});
|
||||
if (ret.code !== 0) {
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
} else if (payload.formValues) {
|
||||
// Non-LIST_MODEL_PROVIDERS: add/update the single model
|
||||
// described by the form values.
|
||||
const fv = payload.formValues;
|
||||
const modelType = Array.isArray(fv.model_type)
|
||||
? fv.model_type
|
||||
: fv.model_type
|
||||
? [fv.model_type as string]
|
||||
: [];
|
||||
const ret = await addInstanceModel({
|
||||
provider_name: payload.llmFactory,
|
||||
instance_name: payload.instanceName,
|
||||
model_name: fv.model_name,
|
||||
model_type: modelType,
|
||||
max_tokens: fv.max_tokens ?? 0,
|
||||
...(fv.is_tools !== undefined
|
||||
? { extra: { is_tools: !!fv.is_tools } }
|
||||
: {}),
|
||||
});
|
||||
if (ret.code !== 0) {
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
setProviderVisible(false);
|
||||
} finally {
|
||||
setProviderLoading(false);
|
||||
}
|
||||
},
|
||||
[addInstanceModel],
|
||||
);
|
||||
|
||||
// if (isLocalLlmFactory(factory.name)) {
|
||||
// showLlmAddingModal(factory.name, true, editData, detailedModel);
|
||||
// } else if (factory.name in ModalMap) {
|
||||
// ModalMap[factory.name as keyof typeof ModalMap]();
|
||||
// } else {
|
||||
// showApiKeyModal(editData, true);
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
// [showApiKeyModal, showLlmAddingModal, ModalMap, detailedLlmList],
|
||||
// );
|
||||
|
||||
const handleOk = useMemo(() => {
|
||||
if (apiKeyVisible) {
|
||||
return onApiKeySavingOk;
|
||||
}
|
||||
if (llmAddingVisible) {
|
||||
return onLlmAddingOk;
|
||||
}
|
||||
if (volcAddingVisible) {
|
||||
return onVolcAddingOk;
|
||||
}
|
||||
if (TencentCloudAddingVisible) {
|
||||
return onTencentCloudAddingOk;
|
||||
}
|
||||
if (SparkAddingVisible) {
|
||||
return onSparkAddingOk;
|
||||
}
|
||||
if (yiyanAddingVisible) {
|
||||
return onyiyanAddingOk;
|
||||
}
|
||||
if (FishAudioAddingVisible) {
|
||||
return onFishAudioAddingOk;
|
||||
}
|
||||
if (bedrockAddingVisible) {
|
||||
return onBedrockAddingOk;
|
||||
}
|
||||
if (AzureAddingVisible) {
|
||||
return onAzureAddingOk;
|
||||
}
|
||||
if (mineruVisible) {
|
||||
return onMineruOk;
|
||||
}
|
||||
if (paddleocrVisible) {
|
||||
return onPaddleOCROk;
|
||||
}
|
||||
if (opendataloaderVisible) {
|
||||
return onOpenDataLoaderOk;
|
||||
}
|
||||
if (GoogleAddingVisible) {
|
||||
return onGoogleAddingOk;
|
||||
}
|
||||
return () => {};
|
||||
}, [
|
||||
GoogleAddingVisible,
|
||||
onGoogleAddingOk,
|
||||
apiKeyVisible,
|
||||
onApiKeySavingOk,
|
||||
llmAddingVisible,
|
||||
onLlmAddingOk,
|
||||
volcAddingVisible,
|
||||
onVolcAddingOk,
|
||||
TencentCloudAddingVisible,
|
||||
onTencentCloudAddingOk,
|
||||
SparkAddingVisible,
|
||||
onSparkAddingOk,
|
||||
yiyanAddingVisible,
|
||||
onyiyanAddingOk,
|
||||
FishAudioAddingVisible,
|
||||
onFishAudioAddingOk,
|
||||
bedrockAddingVisible,
|
||||
onBedrockAddingOk,
|
||||
AzureAddingVisible,
|
||||
onAzureAddingOk,
|
||||
mineruVisible,
|
||||
onMineruOk,
|
||||
paddleocrVisible,
|
||||
onPaddleOCROk,
|
||||
opendataloaderVisible,
|
||||
onOpenDataLoaderOk,
|
||||
]);
|
||||
|
||||
const { onApiKeyVerifying } = useVerifySettings({
|
||||
onVerify: handleOk,
|
||||
});
|
||||
// Closing the modal also clears the viewMode flag so the next open
|
||||
// starts in the default (add) mode.
|
||||
const hideProviderModal = useCallback(() => {
|
||||
setProviderVisible(false);
|
||||
setViewMode(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex w-full border-[0.5px] border-border-button rounded-lg relative ">
|
||||
<Spotlight />
|
||||
<section className="flex flex-col gap-4 w-3/5 px-5 border-r-[0.5px] border-border-button overflow-auto scrollbar-auto">
|
||||
<SystemSetting />
|
||||
<UsedModel handleAddModel={handleAddModel} />
|
||||
<UsedModel
|
||||
handleAddModel={handleAddModel}
|
||||
onEditInstance={handleEditInstance}
|
||||
/>
|
||||
</section>
|
||||
<section className="flex flex-col w-2/5 overflow-auto scrollbar-auto">
|
||||
<AvailableModels handleAddModel={handleAddModel} />
|
||||
</section>
|
||||
<ApiKeyModal
|
||||
visible={apiKeyVisible}
|
||||
hideModal={hideApiKeyModal}
|
||||
loading={saveApiKeyLoading}
|
||||
initialValue={initialApiKey}
|
||||
editMode={editMode}
|
||||
onOk={onApiKeySavingOk}
|
||||
onVerify={onApiKeyVerifying}
|
||||
llmFactory={llmFactory}
|
||||
></ApiKeyModal>
|
||||
{llmAddingVisible && (
|
||||
<OllamaModal
|
||||
visible={llmAddingVisible}
|
||||
hideModal={hideLlmAddingModal}
|
||||
onOk={onLlmAddingOk}
|
||||
loading={llmAddingLoading}
|
||||
editMode={llmEditMode}
|
||||
initialValues={llmInitialValues}
|
||||
llmFactory={selectedLlmFactory}
|
||||
onVerify={onApiKeyVerifying}
|
||||
></OllamaModal>
|
||||
)}
|
||||
<VolcEngineModal
|
||||
visible={volcAddingVisible}
|
||||
hideModal={hideVolcAddingModal}
|
||||
onOk={onVolcAddingOk}
|
||||
loading={volcAddingLoading}
|
||||
llmFactory={LLMFactory.VolcEngine}
|
||||
onVerify={onApiKeyVerifying}
|
||||
></VolcEngineModal>
|
||||
<GoogleModal
|
||||
visible={GoogleAddingVisible}
|
||||
hideModal={hideGoogleAddingModal}
|
||||
onOk={onGoogleAddingOk}
|
||||
loading={GoogleAddingLoading}
|
||||
llmFactory={LLMFactory.GoogleCloud}
|
||||
onVerify={onApiKeyVerifying}
|
||||
></GoogleModal>
|
||||
<TencentCloudModal
|
||||
visible={TencentCloudAddingVisible}
|
||||
hideModal={hideTencentCloudAddingModal}
|
||||
onOk={onTencentCloudAddingOk}
|
||||
loading={TencentCloudAddingLoading}
|
||||
llmFactory={LLMFactory.TencentCloud}
|
||||
onVerify={onApiKeyVerifying}
|
||||
></TencentCloudModal>
|
||||
<SparkModal
|
||||
visible={SparkAddingVisible}
|
||||
hideModal={hideSparkAddingModal}
|
||||
onOk={onSparkAddingOk}
|
||||
loading={SparkAddingLoading}
|
||||
llmFactory={LLMFactory.XunFeiSpark}
|
||||
onVerify={onApiKeyVerifying}
|
||||
></SparkModal>
|
||||
<YiyanModal
|
||||
visible={yiyanAddingVisible}
|
||||
hideModal={hideyiyanAddingModal}
|
||||
onOk={onyiyanAddingOk}
|
||||
loading={yiyanAddingLoading}
|
||||
llmFactory={LLMFactory.BaiduYiYan}
|
||||
onVerify={onApiKeyVerifying}
|
||||
></YiyanModal>
|
||||
<FishAudioModal
|
||||
visible={FishAudioAddingVisible}
|
||||
hideModal={hideFishAudioAddingModal}
|
||||
onOk={onFishAudioAddingOk}
|
||||
loading={FishAudioAddingLoading}
|
||||
llmFactory={LLMFactory.FishAudio}
|
||||
onVerify={onApiKeyVerifying}
|
||||
></FishAudioModal>
|
||||
|
||||
{/* Unified ProviderModal (replaces 9 independent modals) */}
|
||||
<ProviderModal
|
||||
visible={providerVisible}
|
||||
hideModal={hideProviderModal}
|
||||
llmFactory={currentLlmFactory}
|
||||
loading={providerLoading}
|
||||
viewMode={viewMode}
|
||||
initialValues={viewModeInitialValues}
|
||||
baseUrlOptions={currentBaseUrlOptions as any}
|
||||
onOk={handleProviderOk}
|
||||
onVerify={handleProviderVerify}
|
||||
onViewModeOk={handleViewModeOk}
|
||||
/>
|
||||
|
||||
<BedrockModal
|
||||
visible={bedrockAddingVisible}
|
||||
hideModal={hideBedrockAddingModal}
|
||||
onOk={onBedrockAddingOk}
|
||||
loading={bedrockAddingLoading}
|
||||
llmFactory={LLMFactory.Bedrock}
|
||||
onVerify={onApiKeyVerifying}
|
||||
onVerify={async () => ({ isValid: null, logs: '' })}
|
||||
></BedrockModal>
|
||||
<AzureOpenAIModal
|
||||
visible={AzureAddingVisible}
|
||||
hideModal={hideAzureAddingModal}
|
||||
onOk={onAzureAddingOk}
|
||||
loading={AzureAddingLoading}
|
||||
llmFactory={LLMFactory.AzureOpenAI}
|
||||
onVerify={onApiKeyVerifying}
|
||||
></AzureOpenAIModal>
|
||||
<MinerUModal
|
||||
visible={mineruVisible}
|
||||
hideModal={hideMineruModal}
|
||||
onOk={onMineruOk}
|
||||
loading={mineruLoading}
|
||||
onVerify={onApiKeyVerifying}
|
||||
></MinerUModal>
|
||||
<PaddleOCRModal
|
||||
visible={paddleocrVisible}
|
||||
hideModal={hidePaddleOCRModal}
|
||||
onOk={onPaddleOCROk}
|
||||
loading={paddleocrLoading}
|
||||
onVerify={onApiKeyVerifying}
|
||||
></PaddleOCRModal>
|
||||
<OpenDataLoaderModal
|
||||
visible={opendataloaderVisible}
|
||||
hideModal={hideOpenDataLoaderModal}
|
||||
onOk={onOpenDataLoaderOk}
|
||||
loading={opendataloaderLoading}
|
||||
onVerify={onApiKeyVerifying}
|
||||
></OpenDataLoaderModal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModelProviders;
|
||||
|
||||
@@ -1,195 +0,0 @@
|
||||
import { IModalManagerChildrenProps } from '@/components/modal-manager';
|
||||
import { RAGFlowFormItem } from '@/components/ragflow-form';
|
||||
import { Form } from '@/components/ui/form';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Modal } from '@/components/ui/modal/modal';
|
||||
import { LLMFactory } from '@/constants/llm';
|
||||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
import { KeyboardEventHandler, useCallback, useEffect } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { ApiKeyPostBody } from '../../../interface';
|
||||
import { LLMHeader } from '../../components/llm-header';
|
||||
import { VerifyResult } from '../../hooks';
|
||||
import VerifyButton from '../verify-button';
|
||||
|
||||
interface IProps extends Omit<IModalManagerChildrenProps, 'showModal'> {
|
||||
loading: boolean;
|
||||
initialValue: string;
|
||||
llmFactory: string;
|
||||
editMode?: boolean;
|
||||
onOk: (postBody: ApiKeyPostBody) => void;
|
||||
onVerify: (
|
||||
postBody: any,
|
||||
) => Promise<boolean | void | VerifyResult | undefined>;
|
||||
showModal?(): void;
|
||||
}
|
||||
|
||||
type FieldType = {
|
||||
instance_name?: string;
|
||||
api_key?: string;
|
||||
base_url?: string;
|
||||
group_id?: string;
|
||||
};
|
||||
|
||||
const modelsWithBaseUrl = [
|
||||
LLMFactory.OpenAI,
|
||||
LLMFactory.AzureOpenAI,
|
||||
LLMFactory.TongYiQianWen,
|
||||
LLMFactory.MiniMax,
|
||||
LLMFactory.SILICONFLOW,
|
||||
];
|
||||
|
||||
const ApiKeyModal = ({
|
||||
visible,
|
||||
hideModal,
|
||||
llmFactory,
|
||||
loading,
|
||||
initialValue,
|
||||
// editMode = false,
|
||||
onOk,
|
||||
onVerify,
|
||||
}: IProps) => {
|
||||
const form = useForm<FieldType>();
|
||||
const { t } = useTranslate('setting');
|
||||
|
||||
const handleOk = useCallback(async () => {
|
||||
await form.handleSubmit((values) => onOk(values as ApiKeyPostBody))();
|
||||
}, [form, onOk]);
|
||||
|
||||
const handleKeyDown: KeyboardEventHandler<HTMLInputElement> = useCallback(
|
||||
async (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
await handleOk();
|
||||
}
|
||||
},
|
||||
[handleOk],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
form.setValue('api_key', initialValue);
|
||||
} else {
|
||||
form.reset();
|
||||
}
|
||||
}, [initialValue, form, visible]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={<LLMHeader name={llmFactory} />}
|
||||
open={visible}
|
||||
onOpenChange={(open) => !open && hideModal()}
|
||||
onOk={handleOk}
|
||||
onCancel={hideModal}
|
||||
confirmLoading={loading}
|
||||
okText={t('save')}
|
||||
cancelText={t('cancel')}
|
||||
className="!w-[600px]"
|
||||
testId="apikey-modal"
|
||||
okButtonTestId="apikey-save"
|
||||
>
|
||||
<Form {...form}>
|
||||
<div className="space-y-4 py-4">
|
||||
<RAGFlowFormItem
|
||||
name="instance_name"
|
||||
label={t('instanceName')}
|
||||
tooltip={t('instanceNameTip')}
|
||||
rules={{ required: t('instanceNameMessage') }}
|
||||
required
|
||||
labelClassName="text-sm font-medium text-text-secondary"
|
||||
>
|
||||
{(field) => (
|
||||
<Input
|
||||
{...field}
|
||||
placeholder={t('instanceNameMessage')}
|
||||
onKeyDown={handleKeyDown}
|
||||
className="w-full"
|
||||
/>
|
||||
)}
|
||||
</RAGFlowFormItem>
|
||||
|
||||
<RAGFlowFormItem
|
||||
name="api_key"
|
||||
label={t('apiKey')}
|
||||
rules={{ required: t('apiKeyMessage') }}
|
||||
required
|
||||
labelClassName="text-sm font-medium text-text-secondary"
|
||||
>
|
||||
{(field) => (
|
||||
<Input
|
||||
{...field}
|
||||
data-testid="apikey-input"
|
||||
onKeyDown={handleKeyDown}
|
||||
className="w-full"
|
||||
/>
|
||||
)}
|
||||
</RAGFlowFormItem>
|
||||
|
||||
{modelsWithBaseUrl.some((x) => x === llmFactory) && (
|
||||
<RAGFlowFormItem
|
||||
name="base_url"
|
||||
label={t('baseUrl')}
|
||||
tooltip={
|
||||
llmFactory === LLMFactory.MiniMax
|
||||
? t('minimaxBaseUrlTip')
|
||||
: llmFactory === LLMFactory.TongYiQianWen
|
||||
? t('tongyiBaseUrlTip')
|
||||
: llmFactory === LLMFactory.SILICONFLOW
|
||||
? t('siliconBaseUrlTip')
|
||||
: t('baseUrlTip')
|
||||
}
|
||||
labelClassName="text-sm font-medium text-text-primary"
|
||||
>
|
||||
{(field) => (
|
||||
<Input
|
||||
{...field}
|
||||
placeholder={
|
||||
llmFactory === LLMFactory.TongYiQianWen
|
||||
? t('tongyiBaseUrlPlaceholder')
|
||||
: llmFactory === LLMFactory.MiniMax
|
||||
? t('minimaxBaseUrlPlaceholder')
|
||||
: llmFactory === LLMFactory.SILICONFLOW
|
||||
? 'https://api.siliconflow.cn/v1'
|
||||
: 'https://api.openai.com/v1'
|
||||
}
|
||||
onKeyDown={handleKeyDown}
|
||||
className="w-full"
|
||||
/>
|
||||
)}
|
||||
</RAGFlowFormItem>
|
||||
)}
|
||||
|
||||
{llmFactory?.toLowerCase() === 'Anthropic'.toLowerCase() && (
|
||||
<RAGFlowFormItem
|
||||
name="base_url"
|
||||
label={t('baseUrl')}
|
||||
labelClassName="text-sm font-medium text-text-primary"
|
||||
>
|
||||
{(field) => (
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="https://api.anthropic.com/v1"
|
||||
onKeyDown={handleKeyDown}
|
||||
className="w-full"
|
||||
/>
|
||||
)}
|
||||
</RAGFlowFormItem>
|
||||
)}
|
||||
|
||||
{llmFactory?.toLowerCase() === 'Minimax'.toLowerCase() && (
|
||||
<RAGFlowFormItem
|
||||
name="group_id"
|
||||
label="Group ID"
|
||||
labelClassName="text-sm font-medium text-text-primary"
|
||||
>
|
||||
{(field) => <Input {...field} className="w-full" />}
|
||||
</RAGFlowFormItem>
|
||||
)}
|
||||
|
||||
<VerifyButton onVerify={onVerify} />
|
||||
</div>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ApiKeyModal;
|
||||
@@ -1,225 +0,0 @@
|
||||
import {
|
||||
DynamicForm,
|
||||
DynamicFormRef,
|
||||
FormFieldConfig,
|
||||
FormFieldType,
|
||||
} from '@/components/dynamic-form';
|
||||
import { Modal } from '@/components/ui/modal/modal';
|
||||
import { useCommonTranslation, useTranslate } from '@/hooks/common-hooks';
|
||||
import { useBuildModelTypeOptions } from '@/hooks/logic-hooks/use-build-options';
|
||||
import { IModalProps } from '@/interfaces/common';
|
||||
import { IAddProviderInstanceRequestBody } from '@/interfaces/request/llm';
|
||||
import {
|
||||
useFetchInstanceNameSet,
|
||||
useHideWhenInstanceExists,
|
||||
VerifyResult,
|
||||
} from '@/pages/user-setting/setting-model/hooks';
|
||||
import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { FieldValues } from 'react-hook-form';
|
||||
import { LLMHeader } from '../../components/llm-header';
|
||||
import VerifyButton from '../../modal/verify-button';
|
||||
|
||||
const AzureOpenAIModal = ({
|
||||
visible,
|
||||
hideModal,
|
||||
onOk,
|
||||
onVerify,
|
||||
loading,
|
||||
llmFactory,
|
||||
}: IModalProps<IAddProviderInstanceRequestBody> & {
|
||||
llmFactory: string;
|
||||
onVerify?: (
|
||||
postBody: any,
|
||||
) => Promise<boolean | void | VerifyResult | undefined>;
|
||||
}) => {
|
||||
const { t } = useTranslate('setting');
|
||||
const { t: tg } = useCommonTranslation();
|
||||
const { buildModelTypeOptions } = useBuildModelTypeOptions();
|
||||
const formRef = useRef<DynamicFormRef>(null);
|
||||
const { instanceNameSet } = useFetchInstanceNameSet(llmFactory);
|
||||
|
||||
const hideWhenInstanceExists = useHideWhenInstanceExists(instanceNameSet);
|
||||
|
||||
const fields: FormFieldConfig[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
name: 'instance_name',
|
||||
label: t('instanceName'),
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: t('instanceNameMessage'),
|
||||
tooltip: t('instanceNameTip'),
|
||||
validation: {
|
||||
message: t('instanceNameMessage'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'model_type',
|
||||
label: t('modelType'),
|
||||
type: FormFieldType.MultiSelect,
|
||||
required: true,
|
||||
options: buildModelTypeOptions(['chat', 'embedding', 'image2text']),
|
||||
defaultValue: ['embedding'],
|
||||
},
|
||||
{
|
||||
name: 'api_base',
|
||||
label: t('addLlmBaseUrl'),
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: t('baseUrlNameMessage'),
|
||||
validation: {
|
||||
message: t('baseUrlNameMessage'),
|
||||
},
|
||||
shouldRender: hideWhenInstanceExists,
|
||||
},
|
||||
{
|
||||
name: 'api_key',
|
||||
label: t('apiKey'),
|
||||
type: FormFieldType.Text,
|
||||
required: false,
|
||||
placeholder: t('apiKeyMessage'),
|
||||
shouldRender: hideWhenInstanceExists,
|
||||
},
|
||||
{
|
||||
name: 'llm_name',
|
||||
label: t('modelName'),
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: t('modelNameMessage'),
|
||||
defaultValue: 'gpt-3.5-turbo',
|
||||
validation: {
|
||||
message: t('modelNameMessage'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'api_version',
|
||||
label: t('apiVersion'),
|
||||
type: FormFieldType.Text,
|
||||
required: false,
|
||||
placeholder: t('apiVersionMessage'),
|
||||
defaultValue: '2024-02-01',
|
||||
},
|
||||
{
|
||||
name: 'max_tokens',
|
||||
label: t('maxTokens'),
|
||||
type: FormFieldType.Number,
|
||||
required: true,
|
||||
placeholder: t('maxTokensTip'),
|
||||
validation: {
|
||||
min: 0,
|
||||
message: t('maxTokensMessage'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'vision',
|
||||
label: t('vision'),
|
||||
type: FormFieldType.Switch,
|
||||
defaultValue: false,
|
||||
dependencies: ['model_type'],
|
||||
shouldRender: (formValues: any) => {
|
||||
const modelType = formValues?.model_type;
|
||||
if (Array.isArray(modelType)) {
|
||||
return modelType.includes('chat');
|
||||
}
|
||||
return modelType === 'chat';
|
||||
},
|
||||
},
|
||||
],
|
||||
[t, buildModelTypeOptions, hideWhenInstanceExists],
|
||||
);
|
||||
|
||||
const handleOk = async (values?: FieldValues) => {
|
||||
if (!values) return;
|
||||
|
||||
const modelType = values.model_type.map((t: string) =>
|
||||
t === 'chat' && values.vision ? 'image2text' : t,
|
||||
);
|
||||
|
||||
const data: IAddProviderInstanceRequestBody & { api_version?: string } = {
|
||||
instance_name: values.instance_name as string,
|
||||
llm_factory: llmFactory,
|
||||
llm_name: values.llm_name as string,
|
||||
model_type: modelType,
|
||||
api_base: values.api_base as string,
|
||||
api_key: values.api_key as string | undefined,
|
||||
max_tokens: values.max_tokens as number,
|
||||
api_version: values.api_version as string,
|
||||
};
|
||||
|
||||
await onOk?.(data);
|
||||
};
|
||||
|
||||
const verifyParamsFunc = useCallback(() => {
|
||||
const values = formRef.current?.getValues();
|
||||
return {
|
||||
llm_factory: llmFactory,
|
||||
model_type: values.model_type.map((t: string) =>
|
||||
t === 'chat' && values.vision ? 'image2text' : t,
|
||||
),
|
||||
};
|
||||
}, [llmFactory]);
|
||||
|
||||
const handleVerify = useCallback(
|
||||
async (params: any) => {
|
||||
const verifyParams = verifyParamsFunc();
|
||||
const res = await onVerify?.({ ...params, ...verifyParams });
|
||||
return (res || { isValid: null, logs: '' }) as VerifyResult;
|
||||
},
|
||||
[verifyParamsFunc, onVerify],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible) {
|
||||
formRef.current?.reset();
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={<LLMHeader name={llmFactory} />}
|
||||
open={visible || false}
|
||||
onOpenChange={(open) => !open && hideModal?.()}
|
||||
maskClosable={false}
|
||||
footer={<div className="p-4"></div>}
|
||||
>
|
||||
<DynamicForm.Root
|
||||
fields={fields}
|
||||
onSubmit={(data) => {
|
||||
console.log(data);
|
||||
}}
|
||||
ref={formRef}
|
||||
defaultValues={
|
||||
{
|
||||
instance_name: '',
|
||||
model_type: ['embedding'],
|
||||
llm_name: 'gpt-3.5-turbo',
|
||||
api_version: '2024-02-01',
|
||||
vision: false,
|
||||
max_tokens: 8192,
|
||||
} as FieldValues
|
||||
}
|
||||
labelClassName="font-normal"
|
||||
>
|
||||
<>
|
||||
{onVerify && <VerifyButton onVerify={handleVerify} />}
|
||||
<div className="absolute bottom-0 right-0 left-0 flex items-center justify-end w-full gap-2 py-6 px-6">
|
||||
<DynamicForm.CancelButton
|
||||
handleCancel={() => {
|
||||
hideModal?.();
|
||||
}}
|
||||
/>
|
||||
<DynamicForm.SavingButton
|
||||
submitLoading={loading || false}
|
||||
buttonText={tg('ok')}
|
||||
submitFunc={(values: FieldValues) => {
|
||||
handleOk(values);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
</DynamicForm.Root>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(AzureOpenAIModal);
|
||||
@@ -1,182 +0,0 @@
|
||||
import {
|
||||
DynamicForm,
|
||||
DynamicFormRef,
|
||||
FormFieldConfig,
|
||||
FormFieldType,
|
||||
} from '@/components/dynamic-form';
|
||||
import { Modal } from '@/components/ui/modal/modal';
|
||||
import { useCommonTranslation, useTranslate } from '@/hooks/common-hooks';
|
||||
import { useBuildModelTypeOptions } from '@/hooks/logic-hooks/use-build-options';
|
||||
import { IModalProps } from '@/interfaces/common';
|
||||
import { IAddProviderInstanceRequestBody } from '@/interfaces/request/llm';
|
||||
import {
|
||||
useFetchInstanceNameSet,
|
||||
useHideWhenInstanceExists,
|
||||
VerifyResult,
|
||||
} from '@/pages/user-setting/setting-model/hooks';
|
||||
import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { FieldValues } from 'react-hook-form';
|
||||
import { LLMHeader } from '../../components/llm-header';
|
||||
import VerifyButton from '../../modal/verify-button';
|
||||
|
||||
const FishAudioModal = ({
|
||||
visible,
|
||||
hideModal,
|
||||
onOk,
|
||||
onVerify,
|
||||
loading,
|
||||
llmFactory,
|
||||
}: IModalProps<IAddProviderInstanceRequestBody> & {
|
||||
llmFactory: string;
|
||||
onVerify?: (
|
||||
postBody: any,
|
||||
) => Promise<boolean | void | VerifyResult | undefined>;
|
||||
}) => {
|
||||
const { t } = useTranslate('setting');
|
||||
const { t: tc } = useCommonTranslation();
|
||||
const { buildModelTypeOptions } = useBuildModelTypeOptions();
|
||||
const formRef = useRef<DynamicFormRef>(null);
|
||||
const { instanceNameSet } = useFetchInstanceNameSet(llmFactory);
|
||||
|
||||
const hideWhenInstanceExists = useHideWhenInstanceExists(instanceNameSet);
|
||||
|
||||
const fields: FormFieldConfig[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
name: 'instance_name',
|
||||
label: t('instanceName'),
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: t('instanceNameMessage'),
|
||||
tooltip: t('instanceNameTip'),
|
||||
validation: { message: t('instanceNameMessage') },
|
||||
},
|
||||
{
|
||||
name: 'model_type',
|
||||
label: t('modelType'),
|
||||
type: FormFieldType.MultiSelect,
|
||||
required: true,
|
||||
options: buildModelTypeOptions(['tts']),
|
||||
defaultValue: ['tts'],
|
||||
},
|
||||
{
|
||||
name: 'llm_name',
|
||||
label: t('modelName'),
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: t('FishAudioModelNameMessage'),
|
||||
validation: { message: t('FishAudioModelNameMessage') },
|
||||
},
|
||||
{
|
||||
name: 'fish_audio_ak',
|
||||
label: t('addFishAudioAK'),
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: t('FishAudioAKMessage'),
|
||||
validation: { message: t('FishAudioAKMessage') },
|
||||
shouldRender: hideWhenInstanceExists,
|
||||
},
|
||||
{
|
||||
name: 'fish_audio_refid',
|
||||
label: t('addFishAudioRefID'),
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: t('FishAudioRefIDMessage'),
|
||||
validation: { message: t('FishAudioRefIDMessage') },
|
||||
shouldRender: hideWhenInstanceExists,
|
||||
},
|
||||
{
|
||||
name: 'max_tokens',
|
||||
label: t('maxTokens'),
|
||||
type: FormFieldType.Number,
|
||||
required: true,
|
||||
placeholder: t('maxTokensTip'),
|
||||
validation: {
|
||||
min: 0,
|
||||
message: t('maxTokensInvalidMessage'),
|
||||
},
|
||||
},
|
||||
],
|
||||
[t, buildModelTypeOptions, hideWhenInstanceExists],
|
||||
);
|
||||
|
||||
const handleOk = async (values?: FieldValues) => {
|
||||
if (!values) return;
|
||||
|
||||
const data: IAddProviderInstanceRequestBody & {
|
||||
fish_audio_ak: string;
|
||||
fish_audio_refid: string;
|
||||
} = {
|
||||
instance_name: values.instance_name as string,
|
||||
llm_factory: llmFactory,
|
||||
llm_name: values.llm_name as string,
|
||||
model_type: values.model_type,
|
||||
fish_audio_ak: values.fish_audio_ak,
|
||||
fish_audio_refid: values.fish_audio_refid,
|
||||
max_tokens: values.max_tokens as number,
|
||||
};
|
||||
|
||||
await onOk?.(data);
|
||||
};
|
||||
|
||||
const handleVerify = useCallback(
|
||||
async (params: any) => {
|
||||
const res = await onVerify?.({ ...params, llm_factory: llmFactory });
|
||||
return (res || { isValid: null, logs: '' }) as VerifyResult;
|
||||
},
|
||||
[llmFactory, onVerify],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible) {
|
||||
formRef.current?.reset();
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={<LLMHeader name={llmFactory} />}
|
||||
open={visible || false}
|
||||
onOpenChange={(open) => !open && hideModal?.()}
|
||||
maskClosable={false}
|
||||
footerClassName="py-1"
|
||||
footer={<div className="py-0"></div>}
|
||||
>
|
||||
<DynamicForm.Root
|
||||
fields={fields}
|
||||
onSubmit={(data) => console.log(data)}
|
||||
ref={formRef}
|
||||
defaultValues={{
|
||||
instance_name: '',
|
||||
model_type: ['tts'],
|
||||
max_tokens: 8192,
|
||||
}}
|
||||
labelClassName="font-normal"
|
||||
>
|
||||
{onVerify && (
|
||||
<VerifyButton onVerify={handleVerify} isAbsolute={false} />
|
||||
)}
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<a
|
||||
href="https://fish.audio"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-sm text-text-secondary hover:text-primary"
|
||||
>
|
||||
{t('FishAudioLink')}
|
||||
</a>
|
||||
<div className="flex gap-2">
|
||||
<DynamicForm.CancelButton handleCancel={() => hideModal?.()} />
|
||||
<DynamicForm.SavingButton
|
||||
submitLoading={loading || false}
|
||||
buttonText={tc('ok')}
|
||||
submitFunc={(values: FieldValues) => handleOk(values)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</DynamicForm.Root>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(FishAudioModal);
|
||||
@@ -1,210 +0,0 @@
|
||||
import {
|
||||
DynamicForm,
|
||||
DynamicFormRef,
|
||||
FormFieldConfig,
|
||||
FormFieldType,
|
||||
} from '@/components/dynamic-form';
|
||||
import { Modal } from '@/components/ui/modal/modal';
|
||||
import { useCommonTranslation, useTranslate } from '@/hooks/common-hooks';
|
||||
import { useBuildModelTypeOptions } from '@/hooks/logic-hooks/use-build-options';
|
||||
import { IModalProps } from '@/interfaces/common';
|
||||
import { IAddProviderInstanceRequestBody } from '@/interfaces/request/llm';
|
||||
import {
|
||||
useFetchInstanceNameSet,
|
||||
useHideWhenInstanceExists,
|
||||
VerifyResult,
|
||||
} from '@/pages/user-setting/setting-model/hooks';
|
||||
import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { FieldValues } from 'react-hook-form';
|
||||
import { LLMHeader } from '../../components/llm-header';
|
||||
import VerifyButton from '../../modal/verify-button';
|
||||
|
||||
const GoogleModal = ({
|
||||
visible,
|
||||
hideModal,
|
||||
onOk,
|
||||
onVerify,
|
||||
loading,
|
||||
llmFactory,
|
||||
}: IModalProps<IAddProviderInstanceRequestBody> & {
|
||||
llmFactory: string;
|
||||
onVerify?: (
|
||||
postBody: any,
|
||||
) => Promise<boolean | void | VerifyResult | undefined>;
|
||||
}) => {
|
||||
const { t } = useTranslate('setting');
|
||||
const { t: tc } = useCommonTranslation();
|
||||
const { buildModelTypeOptions } = useBuildModelTypeOptions();
|
||||
const { instanceNameSet } = useFetchInstanceNameSet(llmFactory);
|
||||
const formRef = useRef<DynamicFormRef>(null);
|
||||
|
||||
const hideWhenInstanceExists = useHideWhenInstanceExists(instanceNameSet);
|
||||
|
||||
const fields: FormFieldConfig[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
name: 'instance_name',
|
||||
label: t('instanceName'),
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: t('instanceNameMessage'),
|
||||
tooltip: t('instanceNameTip'),
|
||||
validation: { message: t('instanceNameMessage') },
|
||||
},
|
||||
{
|
||||
name: 'model_type',
|
||||
label: t('modelType'),
|
||||
type: FormFieldType.MultiSelect,
|
||||
required: true,
|
||||
options: buildModelTypeOptions(['chat', 'image2text']),
|
||||
defaultValue: ['chat'],
|
||||
},
|
||||
{
|
||||
name: 'llm_name',
|
||||
label: t('modelID'),
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: t('GoogleModelIDMessage'),
|
||||
validation: {
|
||||
message: t('GoogleModelIDMessage'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'google_project_id',
|
||||
label: t('addGoogleProjectID'),
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: t('GoogleProjectIDMessage'),
|
||||
validation: {
|
||||
message: t('GoogleProjectIDMessage'),
|
||||
},
|
||||
shouldRender: hideWhenInstanceExists,
|
||||
},
|
||||
{
|
||||
name: 'google_region',
|
||||
label: t('addGoogleRegion'),
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: t('GoogleRegionMessage'),
|
||||
validation: {
|
||||
message: t('GoogleRegionMessage'),
|
||||
},
|
||||
shouldRender: hideWhenInstanceExists,
|
||||
},
|
||||
{
|
||||
name: 'google_service_account_key',
|
||||
label: t('addGoogleServiceAccountKey'),
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: t('GoogleServiceAccountKeyMessage'),
|
||||
validation: {
|
||||
message: t('GoogleServiceAccountKeyMessage'),
|
||||
},
|
||||
shouldRender: hideWhenInstanceExists,
|
||||
},
|
||||
{
|
||||
name: 'max_tokens',
|
||||
label: t('maxTokens'),
|
||||
type: FormFieldType.Number,
|
||||
required: true,
|
||||
placeholder: t('maxTokensTip'),
|
||||
validation: {
|
||||
min: 0,
|
||||
message: t('maxTokensMinMessage'),
|
||||
},
|
||||
customValidate: (value: any) => {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
return t('maxTokensMessage');
|
||||
}
|
||||
if (value < 0) {
|
||||
return t('maxTokensMinMessage');
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
],
|
||||
[t, buildModelTypeOptions, hideWhenInstanceExists],
|
||||
);
|
||||
|
||||
const handleOk = async (values?: FieldValues) => {
|
||||
if (!values) return;
|
||||
|
||||
const data = {
|
||||
instance_name: values.instance_name as string,
|
||||
llm_factory: llmFactory,
|
||||
model_type: values.model_type,
|
||||
llm_name: values.llm_name,
|
||||
google_project_id: values.google_project_id,
|
||||
google_region: values.google_region,
|
||||
google_service_account_key: values.google_service_account_key,
|
||||
max_tokens: values.max_tokens,
|
||||
} as IAddProviderInstanceRequestBody;
|
||||
|
||||
await onOk?.(data);
|
||||
};
|
||||
|
||||
const verifyParamsFunc = useCallback(() => {
|
||||
return {
|
||||
llm_factory: llmFactory,
|
||||
};
|
||||
}, [llmFactory]);
|
||||
|
||||
const handleVerify = useCallback(
|
||||
async (params: any) => {
|
||||
const verifyParams = verifyParamsFunc();
|
||||
const res = await onVerify?.({ ...params, ...verifyParams });
|
||||
return (res || { isValid: null, logs: '' }) as VerifyResult;
|
||||
},
|
||||
[verifyParamsFunc, onVerify],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible) {
|
||||
formRef.current?.reset();
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={<LLMHeader name={llmFactory} />}
|
||||
open={visible || false}
|
||||
onOpenChange={(open) => !open && hideModal?.()}
|
||||
maskClosable={false}
|
||||
footer={<div className="p-4"></div>}
|
||||
>
|
||||
<DynamicForm.Root
|
||||
fields={fields}
|
||||
onSubmit={() => {
|
||||
// Form submission is handled by SavingButton
|
||||
}}
|
||||
ref={formRef}
|
||||
defaultValues={
|
||||
{
|
||||
instance_name: '',
|
||||
model_type: ['chat'],
|
||||
max_tokens: 8192,
|
||||
} as FieldValues
|
||||
}
|
||||
labelClassName="font-normal"
|
||||
>
|
||||
{onVerify && <VerifyButton onVerify={handleVerify} />}
|
||||
<div className="absolute bottom-0 right-0 left-0 flex items-center justify-end w-full gap-2 py-6 px-6">
|
||||
<DynamicForm.CancelButton
|
||||
handleCancel={() => {
|
||||
hideModal?.();
|
||||
}}
|
||||
/>
|
||||
<DynamicForm.SavingButton
|
||||
submitLoading={loading || false}
|
||||
buttonText={tc('ok')}
|
||||
submitFunc={(values: FieldValues) => {
|
||||
handleOk(values);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</DynamicForm.Root>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(GoogleModal);
|
||||
@@ -1,200 +0,0 @@
|
||||
import { RAGFlowFormItem } from '@/components/ragflow-form';
|
||||
import { ButtonLoading } from '@/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Form } from '@/components/ui/form';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { RAGFlowSelect } from '@/components/ui/select';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { LLMFactory } from '@/constants/llm';
|
||||
import { IModalProps } from '@/interfaces/common';
|
||||
import { VerifyResult } from '@/pages/user-setting/setting-model/hooks';
|
||||
import { buildOptions } from '@/utils/form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { t } from 'i18next';
|
||||
import { memo, useEffect } from 'react';
|
||||
import { useForm, useWatch } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { z } from 'zod';
|
||||
import { LLMHeader } from '../../components/llm-header';
|
||||
import VerifyButton from '../verify-button';
|
||||
|
||||
const FormSchema = z.object({
|
||||
instance_name: z.string().min(1, {
|
||||
message: t('setting.instanceNameMessage'),
|
||||
}),
|
||||
llm_name: z.string().min(1, {
|
||||
message: t('setting.mineru.modelNameRequired'),
|
||||
}),
|
||||
mineru_apiserver: z.string().url(),
|
||||
mineru_output_dir: z.string().optional(),
|
||||
mineru_backend: z.enum([
|
||||
'pipeline',
|
||||
'vlm-transformers',
|
||||
'vlm-vllm-engine',
|
||||
'vlm-http-client',
|
||||
'vlm-mlx-engine',
|
||||
'vlm-vllm-async-engine',
|
||||
'vlm-lmdeploy-engine',
|
||||
]),
|
||||
mineru_server_url: z.string().url().optional(),
|
||||
mineru_delete_output: z.boolean(),
|
||||
});
|
||||
|
||||
export type MinerUFormValues = z.infer<typeof FormSchema>;
|
||||
|
||||
const MinerUModal = ({
|
||||
visible,
|
||||
hideModal,
|
||||
onOk,
|
||||
onVerify,
|
||||
loading,
|
||||
}: IModalProps<MinerUFormValues> & {
|
||||
onVerify?: (
|
||||
postBody: any,
|
||||
) => Promise<boolean | void | VerifyResult | undefined>;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const backendOptions = buildOptions([
|
||||
'pipeline',
|
||||
'vlm-transformers',
|
||||
'vlm-vllm-engine',
|
||||
'vlm-http-client',
|
||||
'vlm-mlx-engine',
|
||||
'vlm-vllm-async-engine',
|
||||
'vlm-lmdeploy-engine',
|
||||
]);
|
||||
|
||||
const form = useForm<MinerUFormValues>({
|
||||
resolver: zodResolver(FormSchema),
|
||||
defaultValues: {
|
||||
instance_name: '',
|
||||
mineru_backend: 'pipeline',
|
||||
mineru_delete_output: true,
|
||||
},
|
||||
});
|
||||
|
||||
const backend = useWatch({
|
||||
control: form.control,
|
||||
name: 'mineru_backend',
|
||||
});
|
||||
|
||||
const handleOk = async (values: MinerUFormValues) => {
|
||||
const ret = await onOk?.(values as any);
|
||||
if (ret) {
|
||||
hideModal?.();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible) {
|
||||
form.reset();
|
||||
}
|
||||
}, [visible, form]);
|
||||
|
||||
return (
|
||||
<Dialog open={visible} onOpenChange={hideModal}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<LLMHeader name={LLMFactory.MinerU} />
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(handleOk)}
|
||||
className="space-y-6"
|
||||
id="mineru-form"
|
||||
>
|
||||
<RAGFlowFormItem
|
||||
name="instance_name"
|
||||
label={t('setting.instanceName')}
|
||||
tooltip={t('setting.instanceNameTip')}
|
||||
required
|
||||
>
|
||||
<Input placeholder={t('setting.instanceNameMessage')} />
|
||||
</RAGFlowFormItem>
|
||||
<RAGFlowFormItem
|
||||
name="llm_name"
|
||||
label={t('setting.modelName')}
|
||||
required
|
||||
>
|
||||
<Input placeholder="mineru-from-env-1" />
|
||||
</RAGFlowFormItem>
|
||||
<RAGFlowFormItem
|
||||
name="mineru_apiserver"
|
||||
label={t('setting.mineru.apiserver')}
|
||||
required
|
||||
>
|
||||
<Input placeholder="http://host.docker.internal:9987" />
|
||||
</RAGFlowFormItem>
|
||||
<RAGFlowFormItem
|
||||
name="mineru_output_dir"
|
||||
label={t('setting.mineru.outputDir')}
|
||||
>
|
||||
<Input placeholder="/tmp/mineru" />
|
||||
</RAGFlowFormItem>
|
||||
<RAGFlowFormItem
|
||||
name="mineru_backend"
|
||||
label={t('setting.mineru.backend')}
|
||||
>
|
||||
{(field) => (
|
||||
<RAGFlowSelect
|
||||
value={field.value}
|
||||
onChange={(value) => {
|
||||
field.onChange(value);
|
||||
if (value !== 'vlm-http-client') {
|
||||
form.setValue('mineru_server_url', undefined);
|
||||
}
|
||||
}}
|
||||
options={backendOptions}
|
||||
placeholder={t('setting.mineru.selectBackend')}
|
||||
/>
|
||||
)}
|
||||
</RAGFlowFormItem>
|
||||
{backend === 'vlm-http-client' && (
|
||||
<RAGFlowFormItem
|
||||
name="mineru_server_url"
|
||||
label={t('setting.mineru.serverUrl')}
|
||||
>
|
||||
<Input placeholder="http://your-vllm-server:30000" />
|
||||
</RAGFlowFormItem>
|
||||
)}
|
||||
<RAGFlowFormItem
|
||||
name="mineru_delete_output"
|
||||
label={t('setting.mineru.deleteOutput')}
|
||||
labelClassName="!mb-0"
|
||||
>
|
||||
{(field) => (
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
)}
|
||||
</RAGFlowFormItem>
|
||||
{onVerify && (
|
||||
<VerifyButton
|
||||
onVerify={onVerify as (postBody: any) => Promise<VerifyResult>}
|
||||
/>
|
||||
)}
|
||||
</form>
|
||||
</Form>
|
||||
<DialogFooter>
|
||||
<div className="flex gap-2">
|
||||
<ButtonLoading type="submit" form="mineru-form" loading={loading}>
|
||||
{t('common.save', 'Save')}
|
||||
</ButtonLoading>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(MinerUModal);
|
||||
@@ -1,211 +0,0 @@
|
||||
import {
|
||||
DynamicForm,
|
||||
DynamicFormRef,
|
||||
FormFieldConfig,
|
||||
FormFieldType,
|
||||
} from '@/components/dynamic-form';
|
||||
import { Modal } from '@/components/ui/modal/modal';
|
||||
import { useCommonTranslation, useTranslate } from '@/hooks/common-hooks';
|
||||
import { useBuildModelTypeOptions } from '@/hooks/logic-hooks/use-build-options';
|
||||
import { IModalProps } from '@/interfaces/common';
|
||||
import { IAddProviderInstanceRequestBody } from '@/interfaces/request/llm';
|
||||
import {
|
||||
useFetchInstanceNameSet,
|
||||
useHideWhenInstanceExists,
|
||||
VerifyResult,
|
||||
} from '@/pages/user-setting/setting-model/hooks';
|
||||
import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { FieldValues } from 'react-hook-form';
|
||||
import { LLMHeader } from '../../components/llm-header';
|
||||
import VerifyButton from '../../modal/verify-button';
|
||||
|
||||
const TencentCloudModal = ({
|
||||
visible,
|
||||
hideModal,
|
||||
onOk,
|
||||
onVerify,
|
||||
loading,
|
||||
llmFactory,
|
||||
}: IModalProps<Omit<IAddProviderInstanceRequestBody, 'max_tokens'>> & {
|
||||
llmFactory: string;
|
||||
onVerify?: (
|
||||
postBody: any,
|
||||
) => Promise<boolean | void | VerifyResult | undefined>;
|
||||
}) => {
|
||||
const { t } = useTranslate('setting');
|
||||
const { t: tc } = useCommonTranslation();
|
||||
const { buildModelTypeOptions } = useBuildModelTypeOptions();
|
||||
const formRef = useRef<DynamicFormRef>(null);
|
||||
const { instanceNameSet } = useFetchInstanceNameSet(llmFactory);
|
||||
|
||||
const hideWhenInstanceExists = useHideWhenInstanceExists(instanceNameSet);
|
||||
|
||||
const fields: FormFieldConfig[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
name: 'instance_name',
|
||||
label: t('instanceName'),
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: t('instanceNameMessage'),
|
||||
tooltip: t('instanceNameTip'),
|
||||
validation: { message: t('instanceNameMessage') },
|
||||
},
|
||||
{
|
||||
name: 'model_type',
|
||||
label: t('modelType'),
|
||||
type: FormFieldType.MultiSelect,
|
||||
required: true,
|
||||
options: buildModelTypeOptions(['speech2text']),
|
||||
defaultValue: ['speech2text'],
|
||||
},
|
||||
{
|
||||
name: 'llm_name',
|
||||
label: t('modelName'),
|
||||
type: FormFieldType.Select,
|
||||
required: true,
|
||||
options: [
|
||||
{ label: '16k_zh', value: '16k_zh' },
|
||||
{ label: '16k_zh_large', value: '16k_zh_large' },
|
||||
{ label: '16k_multi_lang', value: '16k_multi_lang' },
|
||||
{ label: '16k_zh_dialect', value: '16k_zh_dialect' },
|
||||
{ label: '16k_en', value: '16k_en' },
|
||||
{ label: '16k_yue', value: '16k_yue' },
|
||||
{ label: '16k_zh-PY', value: '16k_zh-PY' },
|
||||
{ label: '16k_ja', value: '16k_ja' },
|
||||
{ label: '16k_ko', value: '16k_ko' },
|
||||
{ label: '16k_vi', value: '16k_vi' },
|
||||
{ label: '16k_ms', value: '16k_ms' },
|
||||
{ label: '16k_id', value: '16k_id' },
|
||||
{ label: '16k_fil', value: '16k_fil' },
|
||||
{ label: '16k_th', value: '16k_th' },
|
||||
{ label: '16k_pt', value: '16k_pt' },
|
||||
{ label: '16k_tr', value: '16k_tr' },
|
||||
{ label: '16k_ar', value: '16k_ar' },
|
||||
{ label: '16k_es', value: '16k_es' },
|
||||
{ label: '16k_hi', value: '16k_hi' },
|
||||
{ label: '16k_fr', value: '16k_fr' },
|
||||
{ label: '16k_zh_medical', value: '16k_zh_medical' },
|
||||
{ label: '16k_de', value: '16k_de' },
|
||||
],
|
||||
defaultValue: '16k_zh',
|
||||
validation: {
|
||||
message: t('SparkModelNameMessage'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'TencentCloud_sid',
|
||||
label: t('addTencentCloudSID'),
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: t('TencentCloudSIDMessage'),
|
||||
validation: {
|
||||
message: t('TencentCloudSIDMessage'),
|
||||
},
|
||||
shouldRender: hideWhenInstanceExists,
|
||||
},
|
||||
{
|
||||
name: 'TencentCloud_sk',
|
||||
label: t('addTencentCloudSK'),
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: t('TencentCloudSKMessage'),
|
||||
validation: {
|
||||
message: t('TencentCloudSKMessage'),
|
||||
},
|
||||
shouldRender: hideWhenInstanceExists,
|
||||
},
|
||||
],
|
||||
[t, buildModelTypeOptions, hideWhenInstanceExists],
|
||||
);
|
||||
|
||||
const handleOk = async (values?: FieldValues) => {
|
||||
if (!values) return;
|
||||
|
||||
const data = {
|
||||
instance_name: values.instance_name as string,
|
||||
model_type: values.model_type,
|
||||
llm_name: values.llm_name as string,
|
||||
TencentCloud_sid: values.TencentCloud_sid as string,
|
||||
TencentCloud_sk: values.TencentCloud_sk as string,
|
||||
llm_factory: llmFactory,
|
||||
} as Omit<IAddProviderInstanceRequestBody, 'max_tokens'>;
|
||||
|
||||
await onOk?.(data);
|
||||
};
|
||||
|
||||
const verifyParamsFunc = useCallback(() => {
|
||||
return {
|
||||
llm_factory: llmFactory,
|
||||
};
|
||||
}, [llmFactory]);
|
||||
|
||||
const handleVerify = useCallback(
|
||||
async (params: any) => {
|
||||
const verifyParams = verifyParamsFunc();
|
||||
const res = await onVerify?.({ ...params, ...verifyParams });
|
||||
return (res || { isValid: null, logs: '' }) as VerifyResult;
|
||||
},
|
||||
[verifyParamsFunc, onVerify],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible) {
|
||||
formRef.current?.reset();
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={<LLMHeader name={llmFactory} />}
|
||||
open={visible || false}
|
||||
onOpenChange={(open) => !open && hideModal?.()}
|
||||
maskClosable={false}
|
||||
footer={<div className="p-4"></div>}
|
||||
>
|
||||
<DynamicForm.Root
|
||||
fields={fields}
|
||||
onSubmit={() => {}}
|
||||
ref={formRef}
|
||||
defaultValues={
|
||||
{
|
||||
instance_name: '',
|
||||
model_type: ['speech2text'],
|
||||
llm_name: '16k_zh',
|
||||
} as FieldValues
|
||||
}
|
||||
labelClassName="font-normal"
|
||||
>
|
||||
{onVerify && (
|
||||
<VerifyButton onVerify={handleVerify} isAbsolute={false} />
|
||||
)}
|
||||
<div className="absolute bottom-0 right-0 left-0 flex items-center justify-between w-full py-6 px-6">
|
||||
<a
|
||||
href="https://cloud.tencent.com/document/api/1093/37823"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
{t('TencentCloudLink')}
|
||||
</a>
|
||||
<div className="flex gap-2">
|
||||
<DynamicForm.CancelButton
|
||||
handleCancel={() => {
|
||||
hideModal?.();
|
||||
}}
|
||||
/>
|
||||
<DynamicForm.SavingButton
|
||||
submitLoading={loading || false}
|
||||
buttonText={tc('ok')}
|
||||
submitFunc={(values: FieldValues) => {
|
||||
handleOk(values);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</DynamicForm.Root>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(TencentCloudModal);
|
||||
@@ -1,384 +0,0 @@
|
||||
import {
|
||||
DynamicForm,
|
||||
DynamicFormRef,
|
||||
FormFieldConfig,
|
||||
FormFieldType,
|
||||
} from '@/components/dynamic-form';
|
||||
import { Modal } from '@/components/ui/modal/modal';
|
||||
import { LLMFactory } from '@/constants/llm';
|
||||
import { useCommonTranslation, useTranslate } from '@/hooks/common-hooks';
|
||||
import { useBuildModelTypeOptions } from '@/hooks/logic-hooks/use-build-options';
|
||||
import { IModalProps } from '@/interfaces/common';
|
||||
import { IAddProviderInstanceRequestBody } from '@/interfaces/request/llm';
|
||||
import {
|
||||
useFetchInstanceNameSet,
|
||||
useHideWhenInstanceExists,
|
||||
VerifyResult,
|
||||
} from '@/pages/user-setting/setting-model/hooks';
|
||||
import { memo, useCallback, useMemo, useRef } from 'react';
|
||||
import { FieldValues } from 'react-hook-form';
|
||||
import { LLMHeader } from '../../components/llm-header';
|
||||
import VerifyButton from '../../modal/verify-button';
|
||||
|
||||
const llmFactoryToUrlMap: Partial<Record<LLMFactory, string>> = {
|
||||
[LLMFactory.Ollama]:
|
||||
'https://github.com/infiniflow/ragflow/blob/main/docs/guides/models/deploy_local_llm.mdx',
|
||||
[LLMFactory.Xinference]:
|
||||
'https://inference.readthedocs.io/en/latest/user_guide',
|
||||
[LLMFactory.ModelScope]:
|
||||
'https://www.modelscope.cn/docs/model-service/API-Inference/intro',
|
||||
[LLMFactory.LocalAI]: 'https://localai.io/docs/getting-started/models/',
|
||||
[LLMFactory.LMStudio]: 'https://lmstudio.ai/docs/basics',
|
||||
[LLMFactory.OpenAiAPICompatible]:
|
||||
'https://platform.openai.com/docs/models/gpt-4',
|
||||
[LLMFactory.RAGcon]: 'https://www.ragcon.ai/erste-schritte-mit-ragflow/',
|
||||
[LLMFactory.TogetherAI]: 'https://docs.together.ai/docs/deployment-options',
|
||||
[LLMFactory.Replicate]: 'https://replicate.com/docs/topics/deployments',
|
||||
[LLMFactory.OpenRouter]: 'https://openrouter.ai/docs',
|
||||
[LLMFactory.HuggingFace]:
|
||||
'https://huggingface.co/docs/text-embeddings-inference/quick_tour',
|
||||
[LLMFactory.GPUStack]: 'https://docs.gpustack.ai/latest/quickstart',
|
||||
[LLMFactory.VLLM]: 'https://docs.vllm.ai/en/latest/',
|
||||
[LLMFactory.TokenPony]: 'https://docs.tokenpony.cn/#/',
|
||||
};
|
||||
|
||||
function buildModelTypesWithVision(
|
||||
modelType: string[] | string,
|
||||
vision = false,
|
||||
): string[] {
|
||||
const modelTypeArray = Array.isArray(modelType) ? modelType : [modelType];
|
||||
|
||||
if (modelTypeArray.includes('chat') && vision) {
|
||||
return [...modelTypeArray, 'image2text'];
|
||||
}
|
||||
|
||||
return modelTypeArray;
|
||||
}
|
||||
|
||||
const OllamaModal = ({
|
||||
visible,
|
||||
hideModal,
|
||||
onOk,
|
||||
onVerify,
|
||||
loading,
|
||||
llmFactory,
|
||||
editMode = false,
|
||||
initialValues,
|
||||
}: IModalProps<
|
||||
Partial<IAddProviderInstanceRequestBody> & { provider_order?: string }
|
||||
> & {
|
||||
llmFactory: string;
|
||||
editMode?: boolean;
|
||||
onVerify?: (
|
||||
postBody: any,
|
||||
) => Promise<boolean | void | VerifyResult | undefined>;
|
||||
}) => {
|
||||
const { t } = useTranslate('setting');
|
||||
const { t: tc } = useCommonTranslation();
|
||||
const { buildModelTypeOptions } = useBuildModelTypeOptions();
|
||||
const formRef = useRef<DynamicFormRef>(null);
|
||||
const { instanceNameSet } = useFetchInstanceNameSet(llmFactory);
|
||||
|
||||
const hideWhenInstanceExists = useHideWhenInstanceExists(instanceNameSet);
|
||||
|
||||
const optionsMap: Partial<
|
||||
Record<LLMFactory, { label: string; value: string }[]>
|
||||
> & {
|
||||
Default: { label: string; value: string }[];
|
||||
} = {
|
||||
[LLMFactory.HuggingFace]: buildModelTypeOptions([
|
||||
'embedding',
|
||||
'chat',
|
||||
'rerank',
|
||||
]),
|
||||
[LLMFactory.LMStudio]: buildModelTypeOptions([
|
||||
'chat',
|
||||
'embedding',
|
||||
'image2text',
|
||||
]),
|
||||
[LLMFactory.Xinference]: buildModelTypeOptions([
|
||||
'chat',
|
||||
'embedding',
|
||||
'rerank',
|
||||
'image2text',
|
||||
'speech2text',
|
||||
'tts',
|
||||
]),
|
||||
[LLMFactory.RAGcon]: buildModelTypeOptions([
|
||||
'chat',
|
||||
'embedding',
|
||||
'rerank',
|
||||
'image2text',
|
||||
'speech2text',
|
||||
'tts',
|
||||
]),
|
||||
[LLMFactory.ModelScope]: buildModelTypeOptions(['chat']),
|
||||
[LLMFactory.GPUStack]: buildModelTypeOptions([
|
||||
'chat',
|
||||
'embedding',
|
||||
'rerank',
|
||||
'speech2text',
|
||||
'tts',
|
||||
]),
|
||||
[LLMFactory.OpenRouter]: buildModelTypeOptions(['chat', 'image2text']),
|
||||
Default: buildModelTypeOptions([
|
||||
'chat',
|
||||
'embedding',
|
||||
'rerank',
|
||||
'image2text',
|
||||
]),
|
||||
};
|
||||
|
||||
const url =
|
||||
llmFactoryToUrlMap[llmFactory as LLMFactory] ||
|
||||
'https://github.com/infiniflow/ragflow/blob/main/docs/guides/models/deploy_local_llm.mdx';
|
||||
|
||||
const fields = useMemo<FormFieldConfig[]>(() => {
|
||||
const getOptions = (factory: string) => {
|
||||
return optionsMap[factory as LLMFactory] || optionsMap.Default;
|
||||
};
|
||||
const defaultToolCallEnabled = initialValues?.is_tools ?? false;
|
||||
|
||||
const baseFields: FormFieldConfig[] = [
|
||||
{
|
||||
name: 'instance_name',
|
||||
label: t('instanceName'),
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: t('instanceNameMessage'),
|
||||
tooltip: t('instanceNameTip'),
|
||||
validation: {
|
||||
message: t('instanceNameMessage'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'model_type',
|
||||
label: t('modelType'),
|
||||
type: FormFieldType.MultiSelect,
|
||||
required: true,
|
||||
options: getOptions(llmFactory),
|
||||
},
|
||||
{
|
||||
name: 'llm_name',
|
||||
label: t(llmFactory === 'Xinference' ? 'modelUid' : 'modelName'),
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: t('modelNameMessage'),
|
||||
validation: {
|
||||
message: t('modelNameMessage'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'api_base',
|
||||
label: t('addLlmBaseUrl'),
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: t('baseUrlNameMessage'),
|
||||
validation: {
|
||||
message: t('baseUrlNameMessage'),
|
||||
},
|
||||
shouldRender: hideWhenInstanceExists,
|
||||
},
|
||||
{
|
||||
name: 'api_key',
|
||||
label: t('apiKey'),
|
||||
type: FormFieldType.Text,
|
||||
required: false,
|
||||
placeholder: t('apiKeyMessage'),
|
||||
shouldRender: hideWhenInstanceExists,
|
||||
},
|
||||
{
|
||||
name: 'max_tokens',
|
||||
label: t('maxTokens'),
|
||||
type: FormFieldType.Number,
|
||||
required: true,
|
||||
placeholder: t('maxTokensTip'),
|
||||
validation: {
|
||||
message: t('maxTokensMessage'),
|
||||
},
|
||||
customValidate: (value: any) => {
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
if (typeof value !== 'number') {
|
||||
return t('maxTokensInvalidMessage');
|
||||
}
|
||||
if (value < 0) {
|
||||
return t('maxTokensMinMessage');
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
baseFields.push({
|
||||
name: 'is_tools',
|
||||
label: t('enableToolCall'),
|
||||
type: FormFieldType.Switch,
|
||||
required: false,
|
||||
dependencies: ['model_type'],
|
||||
shouldRender: (formValues: any) => {
|
||||
const modelType = formValues?.model_type;
|
||||
if (Array.isArray(modelType)) {
|
||||
return modelType.includes('chat') || modelType.includes('image2text');
|
||||
}
|
||||
return modelType === 'chat' || modelType === 'image2text';
|
||||
},
|
||||
tooltip: t('enableToolCallTip'),
|
||||
defaultValue: defaultToolCallEnabled,
|
||||
});
|
||||
|
||||
// Add provider_order field only for OpenRouter
|
||||
if (llmFactory === 'OpenRouter') {
|
||||
baseFields.push({
|
||||
name: 'provider_order',
|
||||
label: 'Provider Order',
|
||||
type: FormFieldType.Text,
|
||||
required: false,
|
||||
tooltip: 'Comma-separated provider list, e.g. Groq,Fireworks',
|
||||
placeholder: 'Groq,Fireworks',
|
||||
});
|
||||
}
|
||||
|
||||
// Add vision switch (conditional on model_type === 'chat')
|
||||
baseFields.push({
|
||||
name: 'vision',
|
||||
label: t('vision'),
|
||||
type: FormFieldType.Switch,
|
||||
required: false,
|
||||
dependencies: ['model_type'],
|
||||
shouldRender: (formValues: any) => {
|
||||
const modelType = formValues?.model_type;
|
||||
if (Array.isArray(modelType)) {
|
||||
return modelType.includes('chat');
|
||||
}
|
||||
return modelType === 'chat';
|
||||
},
|
||||
});
|
||||
|
||||
return baseFields;
|
||||
}, [llmFactory, t, hideWhenInstanceExists, initialValues?.is_tools]);
|
||||
|
||||
const defaultValues: FieldValues = useMemo(() => {
|
||||
if (editMode && initialValues) {
|
||||
return {
|
||||
instance_name: initialValues.instance_name || '',
|
||||
llm_name: initialValues.llm_name || '',
|
||||
model_type: initialValues.model_type
|
||||
? initialValues.model_type
|
||||
: ['chat'],
|
||||
api_base: initialValues.api_base || '',
|
||||
max_tokens: initialValues.max_tokens || 8192,
|
||||
api_key: '',
|
||||
vision: initialValues.model_type === 'image2text',
|
||||
provider_order: initialValues.provider_order || '',
|
||||
is_tools: initialValues.is_tools || false,
|
||||
};
|
||||
}
|
||||
return {
|
||||
instance_name: '',
|
||||
model_type: [
|
||||
llmFactory === LLMFactory.Ollama || llmFactory === LLMFactory.VLLM
|
||||
? 'chat'
|
||||
: llmFactory in optionsMap
|
||||
? optionsMap[llmFactory as LLMFactory]?.at(0)?.value
|
||||
: 'embedding',
|
||||
],
|
||||
vision: false,
|
||||
is_tools: false,
|
||||
max_tokens: 8192,
|
||||
};
|
||||
}, [editMode, initialValues, llmFactory]);
|
||||
|
||||
const handleOk = async (values?: FieldValues) => {
|
||||
if (!values) return;
|
||||
|
||||
const modelTypeArray: string[] = Array.isArray(values.model_type)
|
||||
? values.model_type
|
||||
: [values.model_type];
|
||||
const supportsToolCall =
|
||||
modelTypeArray.includes('chat') || modelTypeArray.includes('image2text');
|
||||
|
||||
const data: IAddProviderInstanceRequestBody & { provider_order?: string } =
|
||||
{
|
||||
instance_name: values.instance_name as string,
|
||||
llm_factory: llmFactory,
|
||||
llm_name: values.llm_name as string,
|
||||
model_type: buildModelTypesWithVision(values.model_type, values.vision),
|
||||
api_base: values.api_base as string,
|
||||
api_key: values.api_key as string,
|
||||
max_tokens: values.max_tokens as number,
|
||||
};
|
||||
if (supportsToolCall) {
|
||||
data.is_tools = Boolean(values.is_tools);
|
||||
}
|
||||
|
||||
// Add provider_order only if it exists (for OpenRouter)
|
||||
if (values.provider_order) {
|
||||
data.provider_order = values.provider_order as string;
|
||||
}
|
||||
|
||||
await onOk?.(data);
|
||||
};
|
||||
|
||||
const verifyParamsFunc = useCallback(() => {
|
||||
const values = formRef.current?.getValues();
|
||||
return {
|
||||
llm_factory: llmFactory,
|
||||
model_type: buildModelTypesWithVision(values.model_type, values.vision),
|
||||
};
|
||||
}, [llmFactory]);
|
||||
|
||||
const handleVerify = useCallback(
|
||||
async (params: any) => {
|
||||
const verifyParams = verifyParamsFunc();
|
||||
const res = await onVerify?.({ ...params, ...verifyParams });
|
||||
return (res || { isValid: null, logs: '' }) as VerifyResult;
|
||||
},
|
||||
[verifyParamsFunc, onVerify],
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={<LLMHeader name={llmFactory} />}
|
||||
open={visible || false}
|
||||
onOpenChange={(open) => !open && hideModal?.()}
|
||||
maskClosable={false}
|
||||
footer={<></>}
|
||||
footerClassName="py-1"
|
||||
>
|
||||
<DynamicForm.Root
|
||||
key={`${visible}-${llmFactory}`}
|
||||
fields={fields}
|
||||
ref={formRef}
|
||||
onSubmit={() => {}}
|
||||
defaultValues={defaultValues}
|
||||
labelClassName="font-normal"
|
||||
>
|
||||
{onVerify && (
|
||||
<VerifyButton onVerify={handleVerify} isAbsolute={false} />
|
||||
)}
|
||||
<div className="flex items-center justify-between w-full gap-2 ">
|
||||
<a href={url} target="_blank" rel="noreferrer" className="text-sm">
|
||||
{t('ollamaLink', { name: llmFactory })}
|
||||
</a>
|
||||
<div className="flex gap-2">
|
||||
<DynamicForm.CancelButton
|
||||
handleCancel={() => {
|
||||
hideModal?.();
|
||||
}}
|
||||
/>
|
||||
<DynamicForm.SavingButton
|
||||
submitLoading={loading || false}
|
||||
buttonText={tc('ok')}
|
||||
submitFunc={(values: FieldValues) => {
|
||||
handleOk(values);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</DynamicForm.Root>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(OllamaModal);
|
||||
@@ -1,156 +0,0 @@
|
||||
import { RAGFlowFormItem } from '@/components/ragflow-form';
|
||||
import { Button, ButtonLoading } from '@/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Form } from '@/components/ui/form';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { LLMFactory } from '@/constants/llm';
|
||||
import { VerifyResult } from '@/pages/user-setting/setting-model/hooks';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { memo, useEffect, useMemo } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { z } from 'zod';
|
||||
import { LLMHeader } from '../../components/llm-header';
|
||||
import VerifyButton from '../verify-button';
|
||||
|
||||
export type OpenDataLoaderFormValues = {
|
||||
instance_name: string;
|
||||
llm_name: string;
|
||||
opendataloader_apiserver: string;
|
||||
opendataloader_api_key?: string;
|
||||
};
|
||||
|
||||
export interface IModalProps<T> {
|
||||
visible: boolean;
|
||||
hideModal: () => void;
|
||||
onOk?: (data: T) => Promise<boolean>;
|
||||
onVerify?: (
|
||||
postBody: any,
|
||||
) => Promise<boolean | void | VerifyResult | undefined>;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
const OpenDataLoaderModal = ({
|
||||
visible,
|
||||
hideModal,
|
||||
onOk,
|
||||
onVerify,
|
||||
loading,
|
||||
}: IModalProps<OpenDataLoaderFormValues>) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const FormSchema = useMemo(
|
||||
() =>
|
||||
z.object({
|
||||
instance_name: z.string().min(1, {
|
||||
message: t('setting.instanceNameMessage'),
|
||||
}),
|
||||
llm_name: z.string().min(1, {
|
||||
message: t('setting.modelNameMessage'),
|
||||
}),
|
||||
opendataloader_apiserver: z.string().min(1, {
|
||||
message: t('setting.apiServerMessage'),
|
||||
}),
|
||||
opendataloader_api_key: z.string().optional(),
|
||||
}),
|
||||
[t],
|
||||
);
|
||||
|
||||
const form = useForm<OpenDataLoaderFormValues>({
|
||||
resolver: zodResolver(FormSchema),
|
||||
defaultValues: {
|
||||
instance_name: '',
|
||||
opendataloader_apiserver: '',
|
||||
opendataloader_api_key: '',
|
||||
},
|
||||
});
|
||||
|
||||
const handleOk = async (values: OpenDataLoaderFormValues) => {
|
||||
const ret = await onOk?.(values as any);
|
||||
if (ret) {
|
||||
hideModal?.();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible) {
|
||||
form.reset();
|
||||
}
|
||||
}, [visible, form]);
|
||||
|
||||
return (
|
||||
<Dialog open={visible} onOpenChange={hideModal}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<LLMHeader name={LLMFactory.OpenDataLoader} />
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(handleOk)}
|
||||
className="space-y-6"
|
||||
id="opendataloader-form"
|
||||
>
|
||||
<RAGFlowFormItem
|
||||
name="instance_name"
|
||||
label={t('setting.instanceName')}
|
||||
tooltip={t('setting.instanceNameTip')}
|
||||
required
|
||||
>
|
||||
<Input placeholder={t('setting.instanceNameMessage')} />
|
||||
</RAGFlowFormItem>
|
||||
<RAGFlowFormItem
|
||||
name="llm_name"
|
||||
label={t('setting.modelName')}
|
||||
required
|
||||
>
|
||||
<Input placeholder="my-opendataloader" />
|
||||
</RAGFlowFormItem>
|
||||
<RAGFlowFormItem
|
||||
name="opendataloader_apiserver"
|
||||
label={t('setting.baseUrl')}
|
||||
required
|
||||
>
|
||||
<Input placeholder="http://your-opendataloader-service:9383" />
|
||||
</RAGFlowFormItem>
|
||||
<RAGFlowFormItem
|
||||
name="opendataloader_api_key"
|
||||
label={t('setting.apiKey')}
|
||||
>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder={t('setting.apiKeyPlaceholder')}
|
||||
/>
|
||||
</RAGFlowFormItem>
|
||||
{onVerify && (
|
||||
<VerifyButton
|
||||
onVerify={onVerify as (postBody: any) => Promise<VerifyResult>}
|
||||
/>
|
||||
)}
|
||||
</form>
|
||||
</Form>
|
||||
<DialogFooter className="flex justify-end space-x-2">
|
||||
<Button type="button" variant="secondary" onClick={hideModal}>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<ButtonLoading
|
||||
type="submit"
|
||||
form="opendataloader-form"
|
||||
loading={loading}
|
||||
>
|
||||
{t('common.add')}
|
||||
</ButtonLoading>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(OpenDataLoaderModal);
|
||||
@@ -1,168 +0,0 @@
|
||||
import { RAGFlowFormItem } from '@/components/ragflow-form';
|
||||
import { Button, ButtonLoading } from '@/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Form } from '@/components/ui/form';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { RAGFlowSelect, RAGFlowSelectOptionType } from '@/components/ui/select';
|
||||
import { LLMFactory } from '@/constants/llm';
|
||||
import { VerifyResult } from '@/pages/user-setting/setting-model/hooks';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { t } from 'i18next';
|
||||
import { memo, useEffect } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { z } from 'zod';
|
||||
import { LLMHeader } from '../../components/llm-header';
|
||||
import VerifyButton from '../verify-button';
|
||||
|
||||
const FormSchema = z.object({
|
||||
instance_name: z.string().min(1, {
|
||||
message: t('setting.instanceNameMessage'),
|
||||
}),
|
||||
llm_name: z.string().min(1, {
|
||||
message: t('setting.paddleocr.modelNameRequired'),
|
||||
}),
|
||||
paddleocr_api_url: z.string().min(1, {
|
||||
message: t('setting.paddleocr.apiUrlRequired'),
|
||||
}),
|
||||
paddleocr_access_token: z.string().optional(),
|
||||
paddleocr_algorithm: z.string().default('PaddleOCR-VL'),
|
||||
});
|
||||
|
||||
export type PaddleOCRFormValues = z.infer<typeof FormSchema>;
|
||||
|
||||
export interface IModalProps<T> {
|
||||
visible: boolean;
|
||||
hideModal: () => void;
|
||||
onOk?: (data: T) => Promise<boolean>;
|
||||
onVerify?: (
|
||||
postBody: any,
|
||||
) => Promise<boolean | void | VerifyResult | undefined>;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
const algorithmOptions: RAGFlowSelectOptionType[] = [
|
||||
{ label: 'PaddleOCR-VL-1.5', value: 'PaddleOCR-VL-1.5' },
|
||||
{ label: 'PaddleOCR-VL', value: 'PaddleOCR-VL' },
|
||||
{ label: 'PP-OCRv5', value: 'PP-OCRv5' },
|
||||
{ label: 'PP-StructureV3', value: 'PP-StructureV3' },
|
||||
];
|
||||
|
||||
const PaddleOCRModal = ({
|
||||
visible,
|
||||
hideModal,
|
||||
onOk,
|
||||
onVerify,
|
||||
loading,
|
||||
}: IModalProps<PaddleOCRFormValues>) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const form = useForm<PaddleOCRFormValues>({
|
||||
resolver: zodResolver(FormSchema),
|
||||
defaultValues: {
|
||||
instance_name: '',
|
||||
paddleocr_algorithm: 'PaddleOCR-VL',
|
||||
},
|
||||
});
|
||||
|
||||
const handleOk = async (values: PaddleOCRFormValues) => {
|
||||
const ret = await onOk?.(values as any);
|
||||
if (ret) {
|
||||
hideModal?.();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible) {
|
||||
form.reset();
|
||||
}
|
||||
}, [visible, form]);
|
||||
|
||||
return (
|
||||
<Dialog open={visible} onOpenChange={hideModal}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<LLMHeader name={LLMFactory.PaddleOCR} />
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(handleOk)}
|
||||
className="space-y-6"
|
||||
id="paddleocr-form"
|
||||
>
|
||||
<RAGFlowFormItem
|
||||
name="instance_name"
|
||||
label={t('setting.instanceName')}
|
||||
tooltip={t('setting.instanceNameTip')}
|
||||
required
|
||||
>
|
||||
<Input placeholder={t('setting.instanceNameMessage')} />
|
||||
</RAGFlowFormItem>
|
||||
<RAGFlowFormItem
|
||||
name="llm_name"
|
||||
label={t('setting.modelName')}
|
||||
required
|
||||
>
|
||||
<Input
|
||||
placeholder={t('setting.paddleocr.modelNamePlaceholder')}
|
||||
/>
|
||||
</RAGFlowFormItem>
|
||||
<RAGFlowFormItem
|
||||
name="paddleocr_api_url"
|
||||
label={t('setting.paddleocr.apiUrl')}
|
||||
required
|
||||
>
|
||||
<Input placeholder={t('setting.paddleocr.apiUrlPlaceholder')} />
|
||||
</RAGFlowFormItem>
|
||||
<RAGFlowFormItem
|
||||
name="paddleocr_access_token"
|
||||
label={t('setting.paddleocr.accessToken')}
|
||||
>
|
||||
<Input
|
||||
placeholder={t('setting.paddleocr.accessTokenPlaceholder')}
|
||||
/>
|
||||
</RAGFlowFormItem>
|
||||
<RAGFlowFormItem
|
||||
name="paddleocr_algorithm"
|
||||
label={t('setting.paddleocr.algorithm')}
|
||||
>
|
||||
{(field) => (
|
||||
<RAGFlowSelect
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
options={algorithmOptions}
|
||||
placeholder={t('setting.paddleocr.selectAlgorithm')}
|
||||
/>
|
||||
)}
|
||||
</RAGFlowFormItem>
|
||||
{onVerify && (
|
||||
<VerifyButton
|
||||
onVerify={onVerify as (postBody: any) => Promise<VerifyResult>}
|
||||
/>
|
||||
)}
|
||||
<DialogFooter>
|
||||
<div className="flex justify-end space-x-2">
|
||||
<Button type="button" onClick={hideModal} variant={'outline'}>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<ButtonLoading type="submit" loading={loading}>
|
||||
{t('common.ok')}
|
||||
</ButtonLoading>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(PaddleOCRModal);
|
||||
@@ -0,0 +1,49 @@
|
||||
import { LLMFactory } from '@/constants/llm';
|
||||
|
||||
/**
|
||||
* Provider factories that opt into the "List Models" picker UI.
|
||||
*
|
||||
* For these factories, the modal hides the traditional model_name,
|
||||
* model_type, max_tokens, and is_tools form fields and instead shows a
|
||||
* "List Models" button that fetches available models from the provider's
|
||||
* `/providers/<factory>/models` endpoint. The user can multi-select models
|
||||
* from the response; each selected model is converted to an `IModelInfo`
|
||||
* entry and submitted as `model_info`.
|
||||
*
|
||||
* For all other factories the picker is hidden and the form renders the
|
||||
* 4 model_* fields directly.
|
||||
*/
|
||||
export const LIST_MODEL_PROVIDERS = new Set<string>([
|
||||
LLMFactory.HuggingFace,
|
||||
LLMFactory.Ollama,
|
||||
LLMFactory.GoogleCloud,
|
||||
LLMFactory.OpenRouter,
|
||||
LLMFactory.VLLM,
|
||||
LLMFactory.OpenAiAPICompatible,
|
||||
LLMFactory.LMStudio,
|
||||
LLMFactory.VolcEngine,
|
||||
LLMFactory.Xinference,
|
||||
LLMFactory.LocalAI,
|
||||
LLMFactory.BaiduYiYan,
|
||||
LLMFactory.TencentCloud,
|
||||
LLMFactory.XunFeiSpark,
|
||||
LLMFactory.GPUStack,
|
||||
LLMFactory.FishAudio,
|
||||
LLMFactory.MinerU,
|
||||
LLMFactory.PaddleOCR,
|
||||
]);
|
||||
|
||||
/**
|
||||
* The set of form-field names that are owned by the list-models picker
|
||||
* (not registered in the dynamic form when the picker is active).
|
||||
*
|
||||
* Doubles as the whitelist of fields that remain editable in viewMode —
|
||||
* in viewMode every other field is disabled so only model-related edits
|
||||
* are possible.
|
||||
*/
|
||||
export const LIST_MODEL_FIELD_NAMES = new Set<string>([
|
||||
'model_name',
|
||||
'model_type',
|
||||
'max_tokens',
|
||||
'is_tools',
|
||||
]);
|
||||
@@ -0,0 +1,82 @@
|
||||
import { FormFieldType } from '@/components/dynamic-form';
|
||||
import { LLMFactory } from '@/constants/llm';
|
||||
import type { ProviderConfig } from '../types';
|
||||
|
||||
/**
|
||||
* Generic ApiKey configuration (used for factories not in ProviderConfigMap)
|
||||
*/
|
||||
export const GenericApiKeyConfig: ProviderConfig = {
|
||||
llmFactory: '__generic__',
|
||||
title: 'API Key',
|
||||
fields: [
|
||||
{
|
||||
name: 'instance_name',
|
||||
label: 'instanceName',
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: 'instanceNameMessage',
|
||||
tooltip: 'instanceNameTip',
|
||||
validation: { message: 'instanceNameMessage' },
|
||||
},
|
||||
{
|
||||
name: 'api_key',
|
||||
label: 'apiKey',
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: 'apiKeyMessage',
|
||||
validation: { message: 'apiKeyMessage' },
|
||||
},
|
||||
{
|
||||
name: 'base_url',
|
||||
label: 'baseUrl',
|
||||
type: 'inputSelect',
|
||||
required: false,
|
||||
tooltip: (factory) => {
|
||||
if (factory === LLMFactory.MiniMax) return 'minimaxBaseUrlTip';
|
||||
if (factory === LLMFactory.TongYiQianWen) return 'tongyiBaseUrlTip';
|
||||
if (factory === LLMFactory.SILICONFLOW) return 'siliconBaseUrlTip';
|
||||
return 'baseUrlTip';
|
||||
},
|
||||
placeholder: (factory) => {
|
||||
if (factory === LLMFactory.MiniMax) return 'minimaxBaseUrlPlaceholder';
|
||||
if (factory === LLMFactory.TongYiQianWen)
|
||||
return 'tongyiBaseUrlPlaceholder';
|
||||
if (factory === LLMFactory.SILICONFLOW)
|
||||
return 'siliconflowBaseUrlPlaceholder';
|
||||
if (factory?.toLowerCase() === 'Anthropic')
|
||||
return 'anthropicBaseUrlPlaceholder';
|
||||
return 'openaiBaseUrlPlaceholder';
|
||||
},
|
||||
shouldRender: 'showBaseUrl',
|
||||
},
|
||||
{
|
||||
name: 'group_id',
|
||||
label: 'groupId',
|
||||
type: FormFieldType.Text,
|
||||
required: false,
|
||||
shouldRender: 'showGroupId',
|
||||
},
|
||||
],
|
||||
verifyTransform: (values) => ({
|
||||
apiKey: values.api_key,
|
||||
baseUrl: values.base_url,
|
||||
}),
|
||||
submitTransform: (values) => ({
|
||||
instance_name: values.instance_name,
|
||||
api_key: values.api_key,
|
||||
api_base: values.base_url || '',
|
||||
group_id: values.group_id,
|
||||
max_tokens: 0,
|
||||
}),
|
||||
};
|
||||
|
||||
/**
|
||||
* List of factories supporting base_url (used for the generic ApiKey modal)
|
||||
*/
|
||||
export const FACTORIES_WITH_BASE_URL = [
|
||||
LLMFactory.OpenAI,
|
||||
LLMFactory.AzureOpenAI,
|
||||
LLMFactory.TongYiQianWen,
|
||||
LLMFactory.MiniMax,
|
||||
LLMFactory.SILICONFLOW,
|
||||
];
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { ProviderConfig } from '../types';
|
||||
import { GenericApiKeyConfig } from './generic-api-key-config';
|
||||
import { LocalLlmConfigs } from './local-llm-configs';
|
||||
import { ProviderConfigMap } from './provider-config-map';
|
||||
|
||||
/**
|
||||
* Get the configuration for the given factory
|
||||
* First look up in ProviderConfigMap, then LocalLlmConfigs, finally fall back to GenericApiKeyConfig
|
||||
*/
|
||||
export function getProviderConfig(llmFactory: string): ProviderConfig {
|
||||
// Check whether it is a special factory (11 in ModalMap)
|
||||
// Among which AzureOpenAI/VolcEngine/GoogleCloud/TencentCloud/XunFeiSpark/BaiduYiYan/FishAudio are in ProviderConfigMap
|
||||
// Bedrock/MinerU/PaddleOCR/OpenDataLoader are out of the merge scope and use the original modal
|
||||
|
||||
if (ProviderConfigMap[llmFactory]) {
|
||||
return ProviderConfigMap[llmFactory];
|
||||
}
|
||||
|
||||
if (LocalLlmConfigs[llmFactory]) {
|
||||
return LocalLlmConfigs[llmFactory];
|
||||
}
|
||||
|
||||
// Generic ApiKey modal
|
||||
return {
|
||||
...GenericApiKeyConfig,
|
||||
llmFactory,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
// Public entry point for the field-config folder.
|
||||
// Preserves the previous `./field-config` import path used by provider-modal/index.tsx.
|
||||
|
||||
export { FACTORIES_WITH_BASE_URL } from './generic-api-key-config';
|
||||
export { getProviderConfig } from './get-provider-config';
|
||||
@@ -0,0 +1,202 @@
|
||||
import { FormFieldType } from '@/components/dynamic-form';
|
||||
import { LLMFactory } from '@/constants/llm';
|
||||
import type { FieldConfig, ProviderConfig } from '../types';
|
||||
import { capitalize } from './utils';
|
||||
|
||||
/**
|
||||
* Factory configuration for local/compatible factories
|
||||
* Used for scenarios after OllamaModal merge
|
||||
*/
|
||||
export const LocalLlmConfigs: Record<string, ProviderConfig> = {
|
||||
[LLMFactory.Ollama]: buildLocalConfig(LLMFactory.Ollama, 'Ollama', [
|
||||
'chat',
|
||||
'embedding',
|
||||
'rerank',
|
||||
'image2text',
|
||||
]),
|
||||
[LLMFactory.Xinference]: buildLocalConfig(
|
||||
LLMFactory.Xinference,
|
||||
'Xinference',
|
||||
['chat', 'embedding', 'rerank', 'image2text', 'speech2text', 'tts'],
|
||||
'modelUid',
|
||||
),
|
||||
[LLMFactory.ModelScope]: buildLocalConfig(
|
||||
LLMFactory.ModelScope,
|
||||
'ModelScope',
|
||||
['chat'],
|
||||
),
|
||||
[LLMFactory.LocalAI]: buildLocalConfig(LLMFactory.LocalAI, 'LocalAI', [
|
||||
'chat',
|
||||
'embedding',
|
||||
'rerank',
|
||||
'image2text',
|
||||
]),
|
||||
[LLMFactory.LMStudio]: buildLocalConfig(LLMFactory.LMStudio, 'LMStudio', [
|
||||
'chat',
|
||||
'embedding',
|
||||
'image2text',
|
||||
]),
|
||||
[LLMFactory.OpenAiAPICompatible]: buildLocalConfig(
|
||||
LLMFactory.OpenAiAPICompatible,
|
||||
'OpenAiAPICompatible',
|
||||
['chat', 'embedding', 'rerank', 'image2text'],
|
||||
),
|
||||
[LLMFactory.RAGcon]: buildLocalConfig(LLMFactory.RAGcon, 'RAGcon', [
|
||||
'chat',
|
||||
'embedding',
|
||||
'rerank',
|
||||
'image2text',
|
||||
'speech2text',
|
||||
'tts',
|
||||
]),
|
||||
[LLMFactory.TogetherAI]: buildLocalConfig(
|
||||
LLMFactory.TogetherAI,
|
||||
'TogetherAI',
|
||||
['chat', 'embedding', 'rerank', 'image2text'],
|
||||
),
|
||||
[LLMFactory.Replicate]: buildLocalConfig(LLMFactory.Replicate, 'Replicate', [
|
||||
'chat',
|
||||
'embedding',
|
||||
'rerank',
|
||||
'image2text',
|
||||
]),
|
||||
[LLMFactory.OpenRouter]: buildLocalConfig(
|
||||
LLMFactory.OpenRouter,
|
||||
'OpenRouter',
|
||||
['chat', 'image2text'],
|
||||
undefined,
|
||||
true,
|
||||
),
|
||||
[LLMFactory.HuggingFace]: buildLocalConfig(
|
||||
LLMFactory.HuggingFace,
|
||||
'HuggingFace',
|
||||
['embedding', 'chat', 'rerank'],
|
||||
),
|
||||
[LLMFactory.GPUStack]: buildLocalConfig(LLMFactory.GPUStack, 'GPUStack', [
|
||||
'chat',
|
||||
'embedding',
|
||||
'rerank',
|
||||
'speech2text',
|
||||
'tts',
|
||||
]),
|
||||
[LLMFactory.VLLM]: buildLocalConfig(LLMFactory.VLLM, 'VLLM', [
|
||||
'chat',
|
||||
'embedding',
|
||||
'rerank',
|
||||
'image2text',
|
||||
]),
|
||||
[LLMFactory.TokenPony]: buildLocalConfig(LLMFactory.TokenPony, 'TokenPony', [
|
||||
'chat',
|
||||
'embedding',
|
||||
'rerank',
|
||||
'image2text',
|
||||
]),
|
||||
};
|
||||
|
||||
/**
|
||||
* Build the default configuration for local factories
|
||||
*/
|
||||
function buildLocalConfig(
|
||||
llmFactory: string,
|
||||
title: string,
|
||||
modelTypes: string[],
|
||||
modelNameLabel?: string,
|
||||
addProviderOrder = false,
|
||||
): ProviderConfig {
|
||||
const fields: FieldConfig[] = [
|
||||
{
|
||||
name: 'instance_name',
|
||||
label: 'instanceName',
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: 'instanceNameMessage',
|
||||
tooltip: 'instanceNameTip',
|
||||
},
|
||||
{
|
||||
name: 'model_type',
|
||||
label: 'modelType',
|
||||
type: FormFieldType.MultiSelect,
|
||||
required: true,
|
||||
options: modelTypes.map((t) => ({ label: capitalize(t), value: t })),
|
||||
},
|
||||
{
|
||||
name: 'model_name',
|
||||
label: modelNameLabel ?? 'modelName',
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: 'modelNameMessage',
|
||||
},
|
||||
{
|
||||
name: 'base_url',
|
||||
label: 'addLlmBaseUrl',
|
||||
type: 'inputSelect',
|
||||
required: true,
|
||||
placeholder: 'baseUrlNameMessage',
|
||||
shouldRender: 'hideWhenInstanceExists',
|
||||
},
|
||||
{
|
||||
name: 'api_key',
|
||||
label: 'apiKey',
|
||||
type: FormFieldType.Text,
|
||||
required: false,
|
||||
placeholder: 'apiKeyMessage',
|
||||
shouldRender: 'hideWhenInstanceExists',
|
||||
},
|
||||
{
|
||||
name: 'max_tokens',
|
||||
label: 'maxTokens',
|
||||
type: FormFieldType.Number,
|
||||
required: true,
|
||||
placeholder: 'maxTokensTip',
|
||||
defaultValue: 8192,
|
||||
validation: { min: 0, message: 'maxTokensMessage' },
|
||||
},
|
||||
{
|
||||
name: 'is_tools',
|
||||
label: 'enableToolCall',
|
||||
type: FormFieldType.Switch,
|
||||
required: false,
|
||||
shouldRender: 'modelTypeSupportsToolCall',
|
||||
defaultValue: false,
|
||||
},
|
||||
];
|
||||
|
||||
if (addProviderOrder) {
|
||||
fields.push({
|
||||
name: 'provider_order',
|
||||
label: 'providerOrder',
|
||||
type: FormFieldType.Text,
|
||||
required: false,
|
||||
});
|
||||
}
|
||||
|
||||
fields.push({
|
||||
name: 'vision',
|
||||
label: 'vision',
|
||||
type: FormFieldType.Switch,
|
||||
required: false,
|
||||
defaultValue: false,
|
||||
shouldRender: 'modelTypeIncludesChat',
|
||||
});
|
||||
|
||||
return {
|
||||
llmFactory,
|
||||
title,
|
||||
fields,
|
||||
verifyTransform: (values, modelInfo) => ({
|
||||
apiKey: values.api_key || '',
|
||||
baseUrl: values.base_url,
|
||||
modelInfo,
|
||||
}),
|
||||
submitTransform: (values, modelInfo) => ({
|
||||
instance_name: values.instance_name,
|
||||
llm_factory: llmFactory,
|
||||
model_info: modelInfo,
|
||||
api_base: values.base_url,
|
||||
api_key: values.api_key,
|
||||
...(values.provider_order
|
||||
? { provider_order: values.provider_order }
|
||||
: {}),
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,873 @@
|
||||
import { FormFieldType } from '@/components/dynamic-form';
|
||||
import { LLMFactory } from '@/constants/llm';
|
||||
import type { ProviderConfig } from '../types';
|
||||
|
||||
/**
|
||||
* Factory configuration mapping table
|
||||
* key: LLMFactory value
|
||||
* value: ProviderConfig
|
||||
*/
|
||||
export const ProviderConfigMap: Record<string, ProviderConfig> = {
|
||||
// ============ Azure OpenAI ============
|
||||
[LLMFactory.AzureOpenAI]: {
|
||||
llmFactory: LLMFactory.AzureOpenAI,
|
||||
title: 'Azure OpenAI',
|
||||
fields: [
|
||||
{
|
||||
name: 'instance_name',
|
||||
label: 'instanceName',
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: 'instanceNameMessage',
|
||||
tooltip: 'instanceNameTip',
|
||||
validation: { message: 'instanceNameMessage' },
|
||||
},
|
||||
{
|
||||
name: 'model_type',
|
||||
label: 'modelType',
|
||||
type: FormFieldType.MultiSelect,
|
||||
required: true,
|
||||
options: [
|
||||
{ label: 'Chat', value: 'chat' },
|
||||
{ label: 'Embedding', value: 'embedding' },
|
||||
{ label: 'Image2Text', value: 'image2text' },
|
||||
],
|
||||
defaultValue: ['embedding'],
|
||||
},
|
||||
{
|
||||
name: 'api_base',
|
||||
label: 'addLlmBaseUrl',
|
||||
type: 'inputSelect',
|
||||
required: true,
|
||||
placeholder: 'baseUrlNameMessage',
|
||||
shouldRender: 'hideWhenInstanceExists',
|
||||
validation: { message: 'baseUrlNameMessage' },
|
||||
},
|
||||
{
|
||||
name: 'api_key',
|
||||
label: 'apiKey',
|
||||
type: FormFieldType.Text,
|
||||
required: false,
|
||||
placeholder: 'apiKeyMessage',
|
||||
shouldRender: 'hideWhenInstanceExists',
|
||||
validation: { message: 'apiKeyMessage' },
|
||||
},
|
||||
{
|
||||
name: 'model_name',
|
||||
label: 'modelName',
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: 'modelNameMessage',
|
||||
defaultValue: 'gpt-3.5-turbo',
|
||||
validation: { message: 'modelNameMessage' },
|
||||
},
|
||||
{
|
||||
name: 'api_version',
|
||||
label: 'apiVersion',
|
||||
type: FormFieldType.Text,
|
||||
required: false,
|
||||
placeholder: 'apiVersionMessage',
|
||||
defaultValue: '2024-02-01',
|
||||
},
|
||||
{
|
||||
name: 'max_tokens',
|
||||
label: 'maxTokens',
|
||||
type: FormFieldType.Number,
|
||||
required: true,
|
||||
placeholder: 'maxTokensTip',
|
||||
defaultValue: 8192,
|
||||
validation: { min: 0, message: 'maxTokensMessage' },
|
||||
},
|
||||
{
|
||||
name: 'vision',
|
||||
label: 'vision',
|
||||
type: FormFieldType.Switch,
|
||||
defaultValue: false,
|
||||
shouldRender: 'modelTypeIncludesChat',
|
||||
},
|
||||
],
|
||||
verifyTransform: (values, modelInfo) => ({
|
||||
apiKey: values.api_key,
|
||||
baseUrl: values.api_base,
|
||||
modelInfo,
|
||||
}),
|
||||
submitTransform: (values, modelInfo) => ({
|
||||
instance_name: values.instance_name,
|
||||
llm_factory: LLMFactory.AzureOpenAI,
|
||||
api_base: values.api_base,
|
||||
api_key: values.api_key,
|
||||
api_version: values.api_version,
|
||||
model_info: modelInfo,
|
||||
}),
|
||||
},
|
||||
|
||||
// ============ VolcEngine ============
|
||||
[LLMFactory.VolcEngine]: {
|
||||
llmFactory: LLMFactory.VolcEngine,
|
||||
title: 'VolcEngine',
|
||||
docLink: 'https://www.volcengine.com/docs/82379/1302008',
|
||||
docLinkI18nKey: 'ollamaLink',
|
||||
fields: [
|
||||
{
|
||||
name: 'instance_name',
|
||||
label: 'instanceName',
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: 'instanceNameMessage',
|
||||
tooltip: 'instanceNameTip',
|
||||
validation: { message: 'instanceNameMessage' },
|
||||
},
|
||||
{
|
||||
name: 'model_type',
|
||||
label: 'modelType',
|
||||
type: FormFieldType.MultiSelect,
|
||||
required: true,
|
||||
options: [
|
||||
{ label: 'Chat', value: 'chat' },
|
||||
{ label: 'Embedding', value: 'embedding' },
|
||||
{ label: 'Image2Text', value: 'image2text' },
|
||||
],
|
||||
defaultValue: ['chat'],
|
||||
},
|
||||
// {
|
||||
// name: 'model_name',
|
||||
// label: 'modelName',
|
||||
// type: 'text',
|
||||
// required: true,
|
||||
// placeholder: 'volcModelNameMessage',
|
||||
// validation: { message: 'volcModelNameMessage' },
|
||||
// },
|
||||
{
|
||||
name: 'model_name',
|
||||
label: 'addEndpointID',
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: 'endpointIDMessage',
|
||||
shouldRender: 'hideWhenInstanceExists',
|
||||
validation: { message: 'endpointIDMessage' },
|
||||
},
|
||||
{
|
||||
name: 'api_key',
|
||||
label: 'addArkApiKey',
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: 'ArkApiKeyMessage',
|
||||
shouldRender: 'hideWhenInstanceExists',
|
||||
validation: { message: 'ArkApiKeyMessage' },
|
||||
},
|
||||
{
|
||||
name: 'max_tokens',
|
||||
label: 'maxTokens',
|
||||
type: FormFieldType.Number,
|
||||
required: true,
|
||||
placeholder: 'maxTokensTip',
|
||||
defaultValue: 8192,
|
||||
validation: { min: 0 },
|
||||
},
|
||||
],
|
||||
verifyTransform: (values, modelInfo) => ({
|
||||
apiKey: JSON.stringify({
|
||||
ark_api_key: values.api_key,
|
||||
endpoint_id: values.endpoint_id,
|
||||
}),
|
||||
modelInfo,
|
||||
}),
|
||||
submitTransform: (values, modelInfo) => ({
|
||||
instance_name: values.instance_name,
|
||||
llm_factory: LLMFactory.VolcEngine,
|
||||
endpoint_id: values.endpoint_id,
|
||||
ark_api_key: values.api_key,
|
||||
model_info: modelInfo,
|
||||
}),
|
||||
},
|
||||
|
||||
// ============ Google Cloud ============
|
||||
[LLMFactory.GoogleCloud]: {
|
||||
llmFactory: LLMFactory.GoogleCloud,
|
||||
title: 'Google Cloud',
|
||||
fields: [
|
||||
{
|
||||
name: 'instance_name',
|
||||
label: 'instanceName',
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: 'instanceNameMessage',
|
||||
tooltip: 'instanceNameTip',
|
||||
validation: { message: 'instanceNameMessage' },
|
||||
},
|
||||
{
|
||||
name: 'model_type',
|
||||
label: 'modelType',
|
||||
type: FormFieldType.MultiSelect,
|
||||
required: true,
|
||||
options: [
|
||||
{ label: 'Chat', value: 'chat' },
|
||||
{ label: 'Image2Text', value: 'image2text' },
|
||||
],
|
||||
defaultValue: ['chat'],
|
||||
},
|
||||
{
|
||||
name: 'model_name',
|
||||
label: 'modelID',
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: 'GoogleModelIDMessage',
|
||||
validation: { message: 'GoogleModelIDMessage' },
|
||||
},
|
||||
{
|
||||
name: 'google_project_id',
|
||||
label: 'addGoogleProjectID',
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: 'GoogleProjectIDMessage',
|
||||
shouldRender: 'hideWhenInstanceExists',
|
||||
validation: { message: 'GoogleProjectIDMessage' },
|
||||
},
|
||||
{
|
||||
name: 'google_region',
|
||||
label: 'addGoogleRegion',
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: 'GoogleRegionMessage',
|
||||
shouldRender: 'hideWhenInstanceExists',
|
||||
validation: { message: 'GoogleRegionMessage' },
|
||||
},
|
||||
{
|
||||
name: 'google_service_account_key',
|
||||
label: 'addGoogleServiceAccountKey',
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: 'GoogleServiceAccountKeyMessage',
|
||||
shouldRender: 'hideWhenInstanceExists',
|
||||
validation: { message: 'GoogleServiceAccountKeyMessage' },
|
||||
},
|
||||
{
|
||||
name: 'max_tokens',
|
||||
label: 'maxTokens',
|
||||
type: FormFieldType.Number,
|
||||
required: true,
|
||||
placeholder: 'maxTokensTip',
|
||||
defaultValue: 8192,
|
||||
validation: { min: 0, message: 'maxTokensMinMessage' },
|
||||
},
|
||||
],
|
||||
verifyTransform: (values, modelInfo) => ({
|
||||
apiKey: JSON.stringify({
|
||||
google_project_id: values.google_project_id,
|
||||
google_region: values.google_region,
|
||||
google_service_account_key: values.google_service_account_key,
|
||||
}),
|
||||
modelInfo,
|
||||
}),
|
||||
submitTransform: (values, modelInfo) => ({
|
||||
instance_name: values.instance_name,
|
||||
llm_factory: LLMFactory.GoogleCloud,
|
||||
google_project_id: values.google_project_id,
|
||||
google_region: values.google_region,
|
||||
google_service_account_key: values.google_service_account_key,
|
||||
model_info: modelInfo,
|
||||
}),
|
||||
},
|
||||
|
||||
// ============ Tencent Cloud ============
|
||||
[LLMFactory.TencentCloud]: {
|
||||
llmFactory: LLMFactory.TencentCloud,
|
||||
title: 'Tencent Cloud',
|
||||
docLink: 'https://cloud.tencent.com/document/api/1093/37823',
|
||||
docLinkI18nKey: 'TencentCloudLink',
|
||||
fields: [
|
||||
{
|
||||
name: 'instance_name',
|
||||
label: 'instanceName',
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: 'instanceNameMessage',
|
||||
tooltip: 'instanceNameTip',
|
||||
validation: { message: 'instanceNameMessage' },
|
||||
},
|
||||
{
|
||||
name: 'model_type',
|
||||
label: 'modelType',
|
||||
type: FormFieldType.MultiSelect,
|
||||
required: true,
|
||||
options: [{ label: 'Speech2Text', value: 'speech2text' }],
|
||||
defaultValue: ['speech2text'],
|
||||
},
|
||||
{
|
||||
name: 'model_name',
|
||||
label: 'modelName',
|
||||
type: FormFieldType.Select,
|
||||
required: true,
|
||||
options: [
|
||||
{ label: '16k_zh', value: '16k_zh' },
|
||||
{ label: '16k_zh_large', value: '16k_zh_large' },
|
||||
{ label: '16k_multi_lang', value: '16k_multi_lang' },
|
||||
{ label: '16k_zh_dialect', value: '16k_zh_dialect' },
|
||||
{ label: '16k_en', value: '16k_en' },
|
||||
{ label: '16k_yue', value: '16k_yue' },
|
||||
{ label: '16k_zh-PY', value: '16k_zh-PY' },
|
||||
{ label: '16k_ja', value: '16k_ja' },
|
||||
{ label: '16k_ko', value: '16k_ko' },
|
||||
{ label: '16k_vi', value: '16k_vi' },
|
||||
{ label: '16k_ms', value: '16k_ms' },
|
||||
{ label: '16k_id', value: '16k_id' },
|
||||
{ label: '16k_fil', value: '16k_fil' },
|
||||
{ label: '16k_th', value: '16k_th' },
|
||||
{ label: '16k_pt', value: '16k_pt' },
|
||||
{ label: '16k_tr', value: '16k_tr' },
|
||||
{ label: '16k_ar', value: '16k_ar' },
|
||||
{ label: '16k_es', value: '16k_es' },
|
||||
{ label: '16k_hi', value: '16k_hi' },
|
||||
{ label: '16k_fr', value: '16k_fr' },
|
||||
{ label: '16k_zh_medical', value: '16k_zh_medical' },
|
||||
{ label: '16k_de', value: '16k_de' },
|
||||
],
|
||||
defaultValue: '16k_zh',
|
||||
validation: { message: 'modelNameMessage' },
|
||||
},
|
||||
{
|
||||
name: 'TencentCloud_sid',
|
||||
label: 'addTencentCloudSID',
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: 'TencentCloudSIDMessage',
|
||||
shouldRender: 'hideWhenInstanceExists',
|
||||
validation: { message: 'TencentCloudSIDMessage' },
|
||||
},
|
||||
{
|
||||
name: 'TencentCloud_sk',
|
||||
label: 'addTencentCloudSK',
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: 'TencentCloudSKMessage',
|
||||
shouldRender: 'hideWhenInstanceExists',
|
||||
validation: { message: 'TencentCloudSKMessage' },
|
||||
},
|
||||
],
|
||||
verifyTransform: (values, modelInfo) => ({
|
||||
apiKey: JSON.stringify({
|
||||
TencentCloud_sid: values.TencentCloud_sid,
|
||||
TencentCloud_sk: values.TencentCloud_sk,
|
||||
}),
|
||||
modelInfo,
|
||||
}),
|
||||
submitTransform: (values, modelInfo) => ({
|
||||
instance_name: values.instance_name,
|
||||
llm_factory: LLMFactory.TencentCloud,
|
||||
TencentCloud_sid: values.TencentCloud_sid,
|
||||
TencentCloud_sk: values.TencentCloud_sk,
|
||||
model_info: modelInfo,
|
||||
}),
|
||||
},
|
||||
|
||||
// ============ XunFei Spark ============
|
||||
[LLMFactory.XunFeiSpark]: {
|
||||
llmFactory: LLMFactory.XunFeiSpark,
|
||||
title: 'XunFei Spark',
|
||||
fields: [
|
||||
{
|
||||
name: 'instance_name',
|
||||
label: 'instanceName',
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: 'instanceNameMessage',
|
||||
tooltip: 'instanceNameTip',
|
||||
validation: { message: 'instanceNameMessage' },
|
||||
},
|
||||
{
|
||||
name: 'model_type',
|
||||
label: 'modelType',
|
||||
type: FormFieldType.MultiSelect,
|
||||
required: true,
|
||||
options: [
|
||||
{ label: 'Chat', value: 'chat' },
|
||||
{ label: 'TTS', value: 'tts' },
|
||||
],
|
||||
defaultValue: ['chat'],
|
||||
},
|
||||
{
|
||||
name: 'model_name',
|
||||
label: 'modelName',
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: 'modelNameMessage',
|
||||
validation: { message: 'modelNameMessage' },
|
||||
},
|
||||
{
|
||||
name: 'spark_api_password',
|
||||
label: 'addSparkAPIPassword',
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: 'SparkAPIPasswordMessage',
|
||||
shouldRender: 'hideWhenInstanceExists',
|
||||
validation: { message: 'SparkAPIPasswordMessage' },
|
||||
},
|
||||
{
|
||||
name: 'spark_app_id',
|
||||
label: 'addSparkAPPID',
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: 'SparkAPPIDMessage',
|
||||
shouldRender: 'modelTypeIncludesTtsAndNotExists',
|
||||
validation: { message: 'SparkAPPIDMessage' },
|
||||
},
|
||||
{
|
||||
name: 'spark_api_secret',
|
||||
label: 'addSparkAPISecret',
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: 'SparkAPISecretMessage',
|
||||
shouldRender: 'modelTypeIncludesTtsAndNotExists',
|
||||
validation: { message: 'SparkAPISecretMessage' },
|
||||
},
|
||||
{
|
||||
name: 'spark_api_key',
|
||||
label: 'addSparkAPIKey',
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: 'SparkAPIKeyMessage',
|
||||
shouldRender: 'modelTypeIncludesTtsAndNotExists',
|
||||
validation: { message: 'SparkAPIKeyMessage' },
|
||||
},
|
||||
{
|
||||
name: 'max_tokens',
|
||||
label: 'maxTokens',
|
||||
type: FormFieldType.Number,
|
||||
required: true,
|
||||
placeholder: 'maxTokensTip',
|
||||
defaultValue: 8192,
|
||||
validation: { min: 0, message: 'maxTokensInvalidMessage' },
|
||||
},
|
||||
],
|
||||
verifyTransform: (values, modelInfo) => ({
|
||||
apiKey: JSON.stringify({
|
||||
spark_api_password: values.spark_api_password,
|
||||
spark_app_id: values.spark_app_id,
|
||||
spark_api_secret: values.spark_api_secret,
|
||||
spark_api_key: values.spark_api_key,
|
||||
}),
|
||||
modelInfo,
|
||||
}),
|
||||
submitTransform: (values, modelInfo) => ({
|
||||
instance_name: values.instance_name,
|
||||
llm_factory: LLMFactory.XunFeiSpark,
|
||||
model_info: modelInfo,
|
||||
}),
|
||||
},
|
||||
|
||||
// ============ Baidu YiYan ============
|
||||
[LLMFactory.BaiduYiYan]: {
|
||||
llmFactory: LLMFactory.BaiduYiYan,
|
||||
title: 'Baidu YiYan',
|
||||
fields: [
|
||||
{
|
||||
name: 'instance_name',
|
||||
label: 'instanceName',
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: 'instanceNameMessage',
|
||||
tooltip: 'instanceNameTip',
|
||||
validation: { message: 'instanceNameMessage' },
|
||||
},
|
||||
{
|
||||
name: 'model_type',
|
||||
label: 'modelType',
|
||||
type: FormFieldType.MultiSelect,
|
||||
required: true,
|
||||
options: [
|
||||
{ label: 'Chat', value: 'chat' },
|
||||
{ label: 'Embedding', value: 'embedding' },
|
||||
{ label: 'Rerank', value: 'rerank' },
|
||||
],
|
||||
defaultValue: ['chat'],
|
||||
},
|
||||
{
|
||||
name: 'model_name',
|
||||
label: 'modelName',
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: 'yiyanModelNameMessage',
|
||||
validation: { message: 'yiyanModelNameMessage' },
|
||||
},
|
||||
{
|
||||
name: 'yiyan_ak',
|
||||
label: 'addyiyanAK',
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: 'yiyanAKMessage',
|
||||
shouldRender: 'hideWhenInstanceExists',
|
||||
validation: { message: 'yiyanAKMessage' },
|
||||
},
|
||||
{
|
||||
name: 'yiyan_sk',
|
||||
label: 'addyiyanSK',
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: 'yiyanSKMessage',
|
||||
shouldRender: 'hideWhenInstanceExists',
|
||||
validation: { message: 'yiyanSKMessage' },
|
||||
},
|
||||
{
|
||||
name: 'max_tokens',
|
||||
label: 'maxTokens',
|
||||
type: FormFieldType.Number,
|
||||
required: true,
|
||||
placeholder: 'maxTokensTip',
|
||||
defaultValue: 8192,
|
||||
validation: { min: 0 },
|
||||
},
|
||||
],
|
||||
verifyTransform: (values, modelInfo) => ({
|
||||
apiKey: JSON.stringify({
|
||||
yiyan_ak: values.yiyan_ak,
|
||||
yiyan_sk: values.yiyan_sk,
|
||||
}),
|
||||
modelInfo,
|
||||
}),
|
||||
submitTransform: (values, modelInfo) => ({
|
||||
instance_name: values.instance_name,
|
||||
llm_factory: LLMFactory.BaiduYiYan,
|
||||
api_key: {
|
||||
yiyan_ak: values.yiyan_ak,
|
||||
yiyan_sk: values.yiyan_sk,
|
||||
},
|
||||
model_info: modelInfo,
|
||||
}),
|
||||
},
|
||||
|
||||
// ============ Fish Audio ============
|
||||
[LLMFactory.FishAudio]: {
|
||||
llmFactory: LLMFactory.FishAudio,
|
||||
title: 'Fish Audio',
|
||||
docLink: 'https://fish.audio',
|
||||
docLinkI18nKey: 'FishAudioLink',
|
||||
fields: [
|
||||
{
|
||||
name: 'instance_name',
|
||||
label: 'instanceName',
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: 'instanceNameMessage',
|
||||
tooltip: 'instanceNameTip',
|
||||
validation: { message: 'instanceNameMessage' },
|
||||
},
|
||||
{
|
||||
name: 'model_type',
|
||||
label: 'modelType',
|
||||
type: FormFieldType.MultiSelect,
|
||||
required: true,
|
||||
options: [{ label: 'TTS', value: 'tts' }],
|
||||
defaultValue: ['tts'],
|
||||
},
|
||||
{
|
||||
name: 'model_name',
|
||||
label: 'modelName',
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: 'FishAudioModelNameMessage',
|
||||
validation: { message: 'FishAudioModelNameMessage' },
|
||||
},
|
||||
{
|
||||
name: 'fish_audio_ak',
|
||||
label: 'addFishAudioAK',
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: 'FishAudioAKMessage',
|
||||
shouldRender: 'hideWhenInstanceExists',
|
||||
validation: { message: 'FishAudioAKMessage' },
|
||||
},
|
||||
{
|
||||
name: 'fish_audio_refid',
|
||||
label: 'addFishAudioRefID',
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: 'FishAudioRefIDMessage',
|
||||
shouldRender: 'hideWhenInstanceExists',
|
||||
validation: { message: 'FishAudioRefIDMessage' },
|
||||
},
|
||||
{
|
||||
name: 'max_tokens',
|
||||
label: 'maxTokens',
|
||||
type: FormFieldType.Number,
|
||||
required: true,
|
||||
placeholder: 'maxTokensTip',
|
||||
defaultValue: 8192,
|
||||
validation: { min: 0, message: 'maxTokensInvalidMessage' },
|
||||
},
|
||||
],
|
||||
verifyTransform: (values, modelInfo) => ({
|
||||
apiKey: JSON.stringify({
|
||||
fish_audio_ak: values.fish_audio_ak,
|
||||
fish_audio_refid: values.fish_audio_refid,
|
||||
}),
|
||||
modelInfo,
|
||||
}),
|
||||
submitTransform: (values, modelInfo) => ({
|
||||
instance_name: values.instance_name,
|
||||
llm_factory: LLMFactory.FishAudio,
|
||||
fish_audio_ak: values.fish_audio_ak,
|
||||
fish_audio_refid: values.fish_audio_refid,
|
||||
model_info: modelInfo,
|
||||
}),
|
||||
},
|
||||
|
||||
// ============ OpenDataLoader ============
|
||||
[LLMFactory.OpenDataLoader]: {
|
||||
llmFactory: LLMFactory.OpenDataLoader,
|
||||
title: 'OpenDataLoader',
|
||||
fields: [
|
||||
{
|
||||
name: 'instance_name',
|
||||
label: 'instanceName',
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: 'instanceNameMessage',
|
||||
tooltip: 'instanceNameTip',
|
||||
validation: { message: 'instanceNameMessage' },
|
||||
},
|
||||
{
|
||||
name: 'model_name',
|
||||
label: 'modelName',
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: 'modelNameMessage',
|
||||
validation: { message: 'modelNameMessage' },
|
||||
},
|
||||
{
|
||||
name: 'opendataloader_apiserver',
|
||||
label: 'opendataloaderApiserver',
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: 'opendataloaderApiserverPlaceholder',
|
||||
validation: { message: 'opendataloaderApiserverMessage' },
|
||||
},
|
||||
{
|
||||
name: 'opendataloader_api_key',
|
||||
label: 'apiKey',
|
||||
type: FormFieldType.Text,
|
||||
required: false,
|
||||
placeholder: 'apiKeyPlaceholder',
|
||||
},
|
||||
],
|
||||
verifyTransform: (values, modelInfo) => {
|
||||
const cfg: Record<string, any> = {};
|
||||
if (values.opendataloader_apiserver) {
|
||||
cfg.opendataloader_apiserver = values.opendataloader_apiserver;
|
||||
}
|
||||
if (values.opendataloader_api_key) {
|
||||
cfg.opendataloader_api_key = values.opendataloader_api_key;
|
||||
}
|
||||
return {
|
||||
apiKey: JSON.stringify(cfg),
|
||||
baseUrl: values.opendataloader_apiserver,
|
||||
modelInfo,
|
||||
};
|
||||
},
|
||||
submitTransform: (values, modelInfo) => {
|
||||
const cfg: Record<string, any> = {};
|
||||
if (values.opendataloader_apiserver) {
|
||||
cfg.opendataloader_apiserver = values.opendataloader_apiserver;
|
||||
}
|
||||
if (values.opendataloader_api_key) {
|
||||
cfg.opendataloader_api_key = values.opendataloader_api_key;
|
||||
}
|
||||
return {
|
||||
instance_name: values.instance_name,
|
||||
llm_factory: LLMFactory.OpenDataLoader,
|
||||
api_key: JSON.stringify(cfg),
|
||||
api_base: '',
|
||||
model_info: modelInfo,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
// ============ PaddleOCR ============
|
||||
[LLMFactory.PaddleOCR]: {
|
||||
llmFactory: LLMFactory.PaddleOCR,
|
||||
title: 'PaddleOCR',
|
||||
fields: [
|
||||
{
|
||||
name: 'instance_name',
|
||||
label: 'instanceName',
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: 'instanceNameMessage',
|
||||
tooltip: 'instanceNameTip',
|
||||
validation: { message: 'instanceNameMessage' },
|
||||
},
|
||||
{
|
||||
name: 'model_name',
|
||||
label: 'modelName',
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: 'modelNameMessage',
|
||||
validation: { message: 'modelNameMessage' },
|
||||
},
|
||||
{
|
||||
name: 'paddleocr_api_url',
|
||||
label: 'paddleocrApiUrl',
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: 'paddleocrApiUrlPlaceholder',
|
||||
validation: { message: 'paddleocrApiUrlMessage' },
|
||||
},
|
||||
{
|
||||
name: 'paddleocr_access_token',
|
||||
label: 'paddleocrAccessToken',
|
||||
type: FormFieldType.Text,
|
||||
required: false,
|
||||
placeholder: 'paddleocrAccessTokenPlaceholder',
|
||||
validation: { message: 'paddleocrAccessTokenMessage' },
|
||||
},
|
||||
{
|
||||
name: 'paddleocr_algorithm',
|
||||
label: 'paddleocrAlgorithm',
|
||||
type: FormFieldType.Select,
|
||||
required: false,
|
||||
defaultValue: 'PaddleOCR-VL',
|
||||
placeholder: 'paddleocrSelectAlgorithm',
|
||||
options: [
|
||||
{ label: 'PaddleOCR-VL-1.5', value: 'PaddleOCR-VL-1.5' },
|
||||
{ label: 'PaddleOCR-VL', value: 'PaddleOCR-VL' },
|
||||
{ label: 'PP-OCRv5', value: 'PP-OCRv5' },
|
||||
{ label: 'PP-StructureV3', value: 'PP-StructureV3' },
|
||||
],
|
||||
},
|
||||
],
|
||||
verifyTransform: (values, modelInfo) => {
|
||||
const cfg: Record<string, any> = {};
|
||||
if (values.paddleocr_api_url)
|
||||
cfg.paddleocr_api_url = values.paddleocr_api_url;
|
||||
if (values.paddleocr_access_token)
|
||||
cfg.paddleocr_access_token = values.paddleocr_access_token;
|
||||
if (values.paddleocr_algorithm)
|
||||
cfg.paddleocr_algorithm = values.paddleocr_algorithm;
|
||||
return {
|
||||
apiKey: JSON.stringify(cfg),
|
||||
baseUrl: values.paddleocr_api_url,
|
||||
modelInfo,
|
||||
};
|
||||
},
|
||||
submitTransform: (values, modelInfo) => {
|
||||
const cfg: Record<string, any> = {};
|
||||
if (values.paddleocr_api_url)
|
||||
cfg.paddleocr_api_url = values.paddleocr_api_url;
|
||||
if (values.paddleocr_access_token)
|
||||
cfg.paddleocr_access_token = values.paddleocr_access_token;
|
||||
if (values.paddleocr_algorithm)
|
||||
cfg.paddleocr_algorithm = values.paddleocr_algorithm;
|
||||
return {
|
||||
instance_name: values.instance_name,
|
||||
llm_factory: LLMFactory.PaddleOCR,
|
||||
api_key: JSON.stringify(cfg),
|
||||
api_base: '',
|
||||
model_info: modelInfo,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
// ============ MinerU ============
|
||||
[LLMFactory.MinerU]: {
|
||||
llmFactory: LLMFactory.MinerU,
|
||||
title: 'MinerU',
|
||||
fields: [
|
||||
{
|
||||
name: 'instance_name',
|
||||
label: 'instanceName',
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: 'instanceNameMessage',
|
||||
tooltip: 'instanceNameTip',
|
||||
validation: { message: 'instanceNameMessage' },
|
||||
},
|
||||
{
|
||||
name: 'model_name',
|
||||
label: 'modelName',
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: 'modelNameMessage',
|
||||
validation: { message: 'modelNameMessage' },
|
||||
},
|
||||
{
|
||||
name: 'mineru_apiserver',
|
||||
label: 'mineruApiserver',
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: 'mineruApiserverPlaceholder',
|
||||
validation: { message: 'mineruApiserverMessage' },
|
||||
},
|
||||
{
|
||||
name: 'mineru_output_dir',
|
||||
label: 'mineruOutputDir',
|
||||
type: FormFieldType.Text,
|
||||
required: false,
|
||||
placeholder: 'mineruOutputDirPlaceholder',
|
||||
},
|
||||
{
|
||||
name: 'mineru_backend',
|
||||
label: 'mineruBackend',
|
||||
type: FormFieldType.Select,
|
||||
required: true,
|
||||
defaultValue: 'pipeline',
|
||||
placeholder: 'mineruSelectBackend',
|
||||
options: [
|
||||
{ label: 'pipeline', value: 'pipeline' },
|
||||
{ label: 'vlm-transformers', value: 'vlm-transformers' },
|
||||
{ label: 'vlm-vllm-engine', value: 'vlm-vllm-engine' },
|
||||
{ label: 'vlm-http-client', value: 'vlm-http-client' },
|
||||
{ label: 'vlm-mlx-engine', value: 'vlm-mlx-engine' },
|
||||
{ label: 'vlm-vllm-async-engine', value: 'vlm-vllm-async-engine' },
|
||||
{ label: 'vlm-lmdeploy-engine', value: 'vlm-lmdeploy-engine' },
|
||||
],
|
||||
validation: { message: 'mineruBackendMessage' },
|
||||
},
|
||||
{
|
||||
name: 'mineru_server_url',
|
||||
label: 'mineruServerUrl',
|
||||
type: FormFieldType.Text,
|
||||
required: false,
|
||||
placeholder: 'mineruServerUrlPlaceholder',
|
||||
shouldRender: (values: any) =>
|
||||
values?.mineru_backend === 'vlm-http-client',
|
||||
validation: { message: 'mineruServerUrlMessage' },
|
||||
},
|
||||
{
|
||||
name: 'mineru_delete_output',
|
||||
label: 'mineruDeleteOutput',
|
||||
type: FormFieldType.Switch,
|
||||
required: false,
|
||||
defaultValue: true,
|
||||
},
|
||||
],
|
||||
verifyTransform: (values, modelInfo) => {
|
||||
const cfg: Record<string, any> = { ...values };
|
||||
delete cfg.instance_name;
|
||||
delete cfg.model_name;
|
||||
cfg.mineru_delete_output = values.mineru_delete_output ? '1' : '0';
|
||||
if (values.mineru_backend !== 'vlm-http-client') {
|
||||
delete cfg.mineru_server_url;
|
||||
}
|
||||
return {
|
||||
apiKey: JSON.stringify(cfg),
|
||||
baseUrl: values.mineru_apiserver,
|
||||
modelInfo,
|
||||
};
|
||||
},
|
||||
submitTransform: (values, modelInfo) => {
|
||||
const cfg: Record<string, any> = { ...values };
|
||||
delete cfg.instance_name;
|
||||
delete cfg.model_name;
|
||||
cfg.mineru_delete_output = values.mineru_delete_output ? '1' : '0';
|
||||
if (values.mineru_backend !== 'vlm-http-client') {
|
||||
delete cfg.mineru_server_url;
|
||||
}
|
||||
return {
|
||||
instance_name: values.instance_name,
|
||||
llm_factory: LLMFactory.MinerU,
|
||||
api_key: JSON.stringify(cfg),
|
||||
api_base: '',
|
||||
model_info: modelInfo,
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Capitalize the first letter of a string
|
||||
*/
|
||||
export function capitalize(s: string): string {
|
||||
return s.charAt(0).toUpperCase() + s.slice(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* When model_type contains chat and vision=true, automatically add image2text
|
||||
*/
|
||||
export function applyChatToImage2Text(
|
||||
modelType: string[] | string | undefined,
|
||||
vision?: boolean,
|
||||
): string[] {
|
||||
const arr = Array.isArray(modelType)
|
||||
? modelType
|
||||
: modelType
|
||||
? [modelType]
|
||||
: [];
|
||||
if (arr.includes('chat') && vision) {
|
||||
return [...arr, 'image2text'];
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export { useListModelsOptions } from './use-list-models-options';
|
||||
export { useListModelsPicker } from './use-list-models-picker';
|
||||
export { useProviderFields } from './use-provider-fields';
|
||||
export { useProviderModalActions } from './use-provider-modal-actions';
|
||||
@@ -0,0 +1,103 @@
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
import { IProviderModelItem } from '@/interfaces/request/llm';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
interface UseListModelsOptionsParams {
|
||||
models: IProviderModelItem[];
|
||||
selectedModelItems: IProviderModelItem[];
|
||||
allSelected: boolean;
|
||||
handleSelectModel: (model: IProviderModelItem) => void;
|
||||
handleToggleAll: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build ToggleList options from the fetched model list. The first item is
|
||||
* a sentinel "All models" row that toggles the full selection.
|
||||
*
|
||||
* Why the Checkbox uses `onClick` (not `onCheckedChange`):
|
||||
* Radix Checkbox calls `event.stopPropagation()` internally on its onClick
|
||||
* when the Checkbox lives inside a form, so the row's `onClick` (attached
|
||||
* by ToggleList) never fires when the user clicks the Checkbox itself.
|
||||
* To make the Checkbox click toggle selection, we handle it in our own
|
||||
* `onClick` and re-stop propagation to (a) prevent the row's onClick from
|
||||
* double-firing and (b) make Radix's CheckboxBubbleInput dispatch a
|
||||
* non-bubbling synthetic click on its hidden form input — without this,
|
||||
* the dispatched click would bubble back to the row and re-trigger the
|
||||
* toggle, causing "Maximum update depth exceeded".
|
||||
*/
|
||||
export const useListModelsOptions = ({
|
||||
models,
|
||||
selectedModelItems,
|
||||
allSelected,
|
||||
handleSelectModel,
|
||||
handleToggleAll,
|
||||
}: UseListModelsOptionsParams) => {
|
||||
const { t } = useTranslate('setting');
|
||||
|
||||
return useMemo(() => {
|
||||
const allOption = {
|
||||
value: null as string | null,
|
||||
label: (
|
||||
<div className="flex justify-between items-center gap-2 w-full">
|
||||
<div className="flex-1 min-w-0 flex gap-1 items-center">
|
||||
<div className="font-medium truncate">{t('allModels')}</div>
|
||||
</div>
|
||||
<Checkbox
|
||||
checked={allSelected}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleToggleAll();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
onClick: () => handleToggleAll(),
|
||||
};
|
||||
|
||||
const modelOptions = models.map((m) => {
|
||||
const checked = selectedModelItems.some((s) => s.name === m.name);
|
||||
return {
|
||||
value: m.name,
|
||||
label: (
|
||||
<div className="flex justify-between items-center gap-2 w-full">
|
||||
<div className="flex-1 min-w-0 flex gap-1 items-center">
|
||||
<div className="font-medium truncate">{m.name}</div>
|
||||
{m.model_types &&
|
||||
m.model_types.map((type) => {
|
||||
return (
|
||||
<div
|
||||
key={type}
|
||||
className="text-xs text-text-secondary truncate bg-bg-card rounded-md px-2 py-1"
|
||||
>
|
||||
{type}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<Checkbox
|
||||
checked={checked}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleSelectModel(m);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
onClick: () => handleSelectModel(m),
|
||||
};
|
||||
});
|
||||
if (modelOptions?.length) {
|
||||
return [allOption, ...modelOptions];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}, [
|
||||
models,
|
||||
selectedModelItems,
|
||||
handleSelectModel,
|
||||
allSelected,
|
||||
handleToggleAll,
|
||||
t,
|
||||
]);
|
||||
};
|
||||
@@ -0,0 +1,275 @@
|
||||
import { DynamicFormRef } from '@/components/dynamic-form';
|
||||
import { useListProviderModels } from '@/hooks/use-llm-request';
|
||||
import { IModelInfo, IProviderModelItem } from '@/interfaces/request/llm';
|
||||
import {
|
||||
RefObject,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import type { ProviderConfig } from '../types';
|
||||
|
||||
// Derive is_tools from a model descriptor's `features` array. A model is
|
||||
// considered tool-capable if it advertises either `tool_call` or
|
||||
// `function_call`. Returns `undefined` when the model has no features info.
|
||||
const getIsToolsFromFeatures = (
|
||||
features: IProviderModelItem['features'],
|
||||
): boolean | undefined => {
|
||||
if (!Array.isArray(features)) return undefined;
|
||||
return features.includes('tool_call') || features.includes('function_call');
|
||||
};
|
||||
|
||||
// Map a fetched list-model item to the request-side IModelInfo shape.
|
||||
// Per-model extras (such as is_tools) live under the `extra` object so the
|
||||
// addProviderInstance API receives them in the shape the backend expects.
|
||||
const toIModelInfo = (item: IProviderModelItem): IModelInfo => {
|
||||
const is_tools = getIsToolsFromFeatures(item.features);
|
||||
return {
|
||||
model_name: item.name,
|
||||
model_type: item.model_types ?? [],
|
||||
max_tokens: item.max_tokens ?? 0,
|
||||
...(is_tools !== undefined ? { extra: { is_tools } } : {}),
|
||||
};
|
||||
};
|
||||
|
||||
// Compare the model_type sets from an initial IModelInfo entry and a
|
||||
// freshly fetched list item. Tolerates string-vs-array differences so a
|
||||
// legacy single-value `model_type` still matches the list's array shape.
|
||||
const modelTypesMatch = (
|
||||
initial: string | string[] | undefined,
|
||||
fetched: string[] | undefined,
|
||||
): boolean => {
|
||||
const a = Array.isArray(initial) ? initial : initial ? [initial] : [];
|
||||
const b = Array.isArray(fetched) ? fetched : [];
|
||||
if (a.length !== b.length) return false;
|
||||
const sb = new Set(b);
|
||||
return a.every((t) => sb.has(t));
|
||||
};
|
||||
|
||||
interface UseListModelsPickerParams {
|
||||
visible: boolean;
|
||||
hasModelNameField: boolean;
|
||||
editMode?: boolean;
|
||||
viewMode?: boolean;
|
||||
initialValues?: Record<string, any>;
|
||||
llmFactory: string;
|
||||
config: ProviderConfig;
|
||||
formRef: RefObject<DynamicFormRef>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Owns all state for the "List Models" picker:
|
||||
* - fetched model catalog (`models`) and loading flag
|
||||
* - currently checked items (`selectedModelItems`)
|
||||
* - derived `modelInfoList` payload used at verify/submit time
|
||||
* - `allSelected` flag and the all/none/individual toggle handlers
|
||||
* - the API fetch (with edit-mode pre-check seeding) and modal-reset effect
|
||||
*
|
||||
* The picker lives entirely in component state — no form fields are touched.
|
||||
* A reentrancy ref guards against Radix Checkbox's double-click dispatch
|
||||
* (CheckboxBubbleInput re-fires onClick inside a form).
|
||||
*/
|
||||
export const useListModelsPicker = ({
|
||||
visible,
|
||||
hasModelNameField,
|
||||
editMode,
|
||||
viewMode,
|
||||
initialValues,
|
||||
llmFactory,
|
||||
config,
|
||||
formRef,
|
||||
}: UseListModelsPickerParams) => {
|
||||
const [models, setModels] = useState<IProviderModelItem[]>([]);
|
||||
const [listLoading, setListLoading] = useState(false);
|
||||
// Items the user has checked in the picker. Carries the full descriptor
|
||||
// (including `features`) so we can derive is_tools per model at submit time.
|
||||
const [selectedModelItems, setSelectedModelItems] = useState<
|
||||
IProviderModelItem[]
|
||||
>([]);
|
||||
// Edit-mode seed: the model_info array stored on the existing instance.
|
||||
// Used to pre-check list items once the model list is fetched.
|
||||
const initialModelInfoRef = useRef<IModelInfo[] | null>(null);
|
||||
const { listProviderModels } = useListProviderModels();
|
||||
|
||||
// Reentrancy guard for selection toggles. When a Checkbox inside the form is
|
||||
// clicked, Radix's CheckboxBubbleInput dispatches a synthetic click on its
|
||||
// hidden form input that bubbles up to the row's onClick — so each user
|
||||
// click fires the toggle handler twice in the same tick. Calling setState
|
||||
// twice (especially for the all/none toggle below) trips React's
|
||||
// "Maximum update depth exceeded" guard. The ref short-circuits the second
|
||||
// call and is released on the next macrotask.
|
||||
const selectionLockRef = useRef(false);
|
||||
|
||||
// Derived: the model_info array passed to verify/submit. One entry per
|
||||
// checked list item, with is_tools derived from the model's `features`.
|
||||
const modelInfoList: IModelInfo[] = useMemo(
|
||||
() => selectedModelItems.map(toIModelInfo),
|
||||
[selectedModelItems],
|
||||
);
|
||||
|
||||
// "All models" is checked when every fetched model is selected.
|
||||
const allSelected = useMemo(
|
||||
() => models.length > 0 && selectedModelItems.length === models.length,
|
||||
[models.length, selectedModelItems.length],
|
||||
);
|
||||
|
||||
// Capture the initial model_info array (edit mode or viewMode) so we
|
||||
// can match it against the fetched list once it arrives and pre-check
|
||||
// the right items.
|
||||
useEffect(() => {
|
||||
if (!visible) return;
|
||||
initialModelInfoRef.current = null;
|
||||
if ((editMode || viewMode) && initialValues) {
|
||||
const initial = (initialValues as any).model_info;
|
||||
if (Array.isArray(initial)) {
|
||||
initialModelInfoRef.current = initial as IModelInfo[];
|
||||
} else if (
|
||||
(initialValues as any).model_name &&
|
||||
(initialValues as any).model_type
|
||||
) {
|
||||
// Legacy fallback: a single-model payload may still arrive as flat
|
||||
// fields. Wrap it so the matcher below has a uniform input shape.
|
||||
initialModelInfoRef.current = [
|
||||
{
|
||||
model_name: (initialValues as any).model_name,
|
||||
model_type: (initialValues as any).model_type,
|
||||
max_tokens: (initialValues as any).max_tokens ?? 0,
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [visible, editMode, viewMode]);
|
||||
|
||||
// Triggered by ToggleList's onOpenChange — fires the API call with the
|
||||
// same payload shape as verifyProviderConnection. Caches the result in
|
||||
// component state so subsequent opens don't re-fetch. In edit mode, the
|
||||
// fetched list is then matched against `initialModelInfoRef` so any model
|
||||
// that was already configured is pre-checked.
|
||||
const handleListOpenChange = useCallback(
|
||||
async (open: boolean) => {
|
||||
if (!open || !hasModelNameField) return;
|
||||
if (models.length > 0 || listLoading) return;
|
||||
setListLoading(true);
|
||||
try {
|
||||
const values = (formRef.current?.getValues() || {}) as Record<
|
||||
string,
|
||||
any
|
||||
>;
|
||||
// Reuse the verifyTransform to build the request payload — it
|
||||
// already knows how to flatten provider-specific auth (api_key,
|
||||
// base_url, region, model_info). Pass the current modelInfoList
|
||||
// (empty on first load, populated on re-opens) so the backend
|
||||
// sees an array shape consistent with the verify/submit payloads.
|
||||
const verifyArgs = config.verifyTransform
|
||||
? config.verifyTransform(values, modelInfoList)
|
||||
: { apiKey: values.api_key ?? '', baseUrl: values.base_url };
|
||||
const res = await listProviderModels({
|
||||
provider_name: llmFactory,
|
||||
api_key: (verifyArgs as any).apiKey ?? '',
|
||||
base_url: (verifyArgs as any).baseUrl,
|
||||
region: (verifyArgs as any).region,
|
||||
model_info: (verifyArgs as any).modelInfo ?? modelInfoList,
|
||||
});
|
||||
if (res?.code === 0 && Array.isArray(res.data)) {
|
||||
setModels(res.data);
|
||||
// Edit-mode pre-check: match the initial model_info entries
|
||||
// against the freshly fetched list (by name + model_type set)
|
||||
// and seed `selectedModelItems`.
|
||||
const seed = initialModelInfoRef.current;
|
||||
if (seed && seed.length > 0) {
|
||||
const matched = res.data.filter((m: IProviderModelItem) =>
|
||||
seed.some(
|
||||
(s) =>
|
||||
s.model_name === m.name &&
|
||||
modelTypesMatch(s.model_type, m.model_types),
|
||||
),
|
||||
);
|
||||
if (matched.length > 0) {
|
||||
setSelectedModelItems(matched);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch provider models:', err);
|
||||
} finally {
|
||||
setListLoading(false);
|
||||
}
|
||||
},
|
||||
[
|
||||
hasModelNameField,
|
||||
models.length,
|
||||
listLoading,
|
||||
config,
|
||||
listProviderModels,
|
||||
llmFactory,
|
||||
modelInfoList,
|
||||
formRef,
|
||||
],
|
||||
);
|
||||
|
||||
// Toggling a list item: add it to `selectedModelItems` if absent, remove
|
||||
// it otherwise. No form fields are touched — selection lives entirely
|
||||
// in component state and is surfaced as `modelInfoList` at verify/submit.
|
||||
const handleSelectModel = useCallback((model: IProviderModelItem) => {
|
||||
if (selectionLockRef.current) return;
|
||||
selectionLockRef.current = true;
|
||||
setSelectedModelItems((prev) => {
|
||||
const idx = prev.findIndex((p) => p.name === model.name);
|
||||
if (idx >= 0) {
|
||||
const next = prev.slice();
|
||||
next.splice(idx, 1);
|
||||
return next;
|
||||
}
|
||||
return [...prev, model];
|
||||
});
|
||||
setTimeout(() => {
|
||||
selectionLockRef.current = false;
|
||||
}, 0);
|
||||
}, []);
|
||||
|
||||
// Toggling the "All models" row: select every model when none/all are
|
||||
// unselected, otherwise clear the selection. Mirrors the per-item toggle
|
||||
// semantics so the UI stays consistent (re-clicking all = empty).
|
||||
// The reentrancy guard here is critical: without it, the second call sees
|
||||
// `prev.length === models.length` (from the first call's `models.slice()`)
|
||||
// and returns `[]`, producing two setState calls per click and triggering
|
||||
// the "Maximum update depth exceeded" error.
|
||||
const handleToggleAll = useCallback(() => {
|
||||
if (selectionLockRef.current) return;
|
||||
selectionLockRef.current = true;
|
||||
setSelectedModelItems((prev) => {
|
||||
if (prev.length === models.length) {
|
||||
return [];
|
||||
}
|
||||
return models.slice();
|
||||
});
|
||||
setTimeout(() => {
|
||||
selectionLockRef.current = false;
|
||||
}, 0);
|
||||
}, [models]);
|
||||
|
||||
// Reset everything when the modal is closed.
|
||||
useEffect(() => {
|
||||
if (!visible) {
|
||||
formRef.current?.reset();
|
||||
setModels([]);
|
||||
setSelectedModelItems([]);
|
||||
setListLoading(false);
|
||||
initialModelInfoRef.current = null;
|
||||
}
|
||||
}, [visible, formRef]);
|
||||
|
||||
return {
|
||||
models,
|
||||
listLoading,
|
||||
selectedModelItems,
|
||||
modelInfoList,
|
||||
allSelected,
|
||||
handleListOpenChange,
|
||||
handleSelectModel,
|
||||
handleToggleAll,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,297 @@
|
||||
import { FormFieldConfig, FormFieldType } from '@/components/dynamic-form';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { InputSelect } from '@/components/ui/input-select';
|
||||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
import { useMemo } from 'react';
|
||||
import { ControllerRenderProps, FieldValues } from 'react-hook-form';
|
||||
import { LIST_MODEL_FIELD_NAMES, LIST_MODEL_PROVIDERS } from '../constants';
|
||||
import { FACTORIES_WITH_BASE_URL, getProviderConfig } from '../field-config';
|
||||
import type { FieldConfig, SelectOption } from '../types';
|
||||
|
||||
interface UseProviderFieldsParams {
|
||||
llmFactory: string;
|
||||
editMode?: boolean;
|
||||
viewMode?: boolean;
|
||||
initialValues?: Record<string, any>;
|
||||
baseUrlOptions?: SelectOption[];
|
||||
hideWhenInstanceExists: (values: Record<string, any>) => boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a text value that may be a static i18n key or a factory-aware
|
||||
* resolver, then translate it via `t`. Used for `placeholder` and `tooltip`
|
||||
* so that a single FieldConfig entry can render different text per provider
|
||||
* (e.g. the generic `base_url` field has a Minimax-specific tooltip).
|
||||
*/
|
||||
const resolveText = (
|
||||
val: string | ((factory: string) => string) | undefined,
|
||||
factory: string,
|
||||
t: (key: string) => string,
|
||||
): string | undefined => {
|
||||
if (!val) return undefined;
|
||||
const key = typeof val === 'function' ? val(factory) : val;
|
||||
return t(key);
|
||||
};
|
||||
|
||||
/** Set value by nested path (supports paths like 'model_info.model_type'). */
|
||||
const setNestedValue = (obj: any, path: string, value: any) => {
|
||||
const keys = path.split('.');
|
||||
let current = obj;
|
||||
for (let i = 0; i < keys.length - 1; i++) {
|
||||
const key = keys[i];
|
||||
if (!current[key]) {
|
||||
current[key] = {};
|
||||
}
|
||||
current = current[key];
|
||||
}
|
||||
current[keys[keys.length - 1]] = value;
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds the form field config, default values, and doc-link text for the
|
||||
* Provider modal. Handles:
|
||||
* - Per-provider text resolution (placeholder / tooltip).
|
||||
* - shouldRender token → predicate resolution.
|
||||
* - Hiding the 4 model_* fields when the list-models picker is active.
|
||||
* - Disabling non-model fields in viewMode.
|
||||
* - Custom inputSelect rendering (Input + dropdown of suggestions).
|
||||
*/
|
||||
export const useProviderFields = ({
|
||||
llmFactory,
|
||||
editMode,
|
||||
viewMode,
|
||||
initialValues,
|
||||
baseUrlOptions,
|
||||
hideWhenInstanceExists,
|
||||
}: UseProviderFieldsParams) => {
|
||||
const { t } = useTranslate('setting');
|
||||
|
||||
const config = useMemo(() => getProviderConfig(llmFactory), [llmFactory]);
|
||||
|
||||
// Whether this factory should render the "List Models" picker. Only the
|
||||
// providers listed in `LIST_MODEL_PROVIDERS` opt into the picker; all
|
||||
// others keep the traditional model_name / model_type / max_tokens /
|
||||
// is_tools form fields.
|
||||
const hasModelNameField = useMemo(
|
||||
() => LIST_MODEL_PROVIDERS.has(llmFactory),
|
||||
[llmFactory],
|
||||
);
|
||||
|
||||
// Resolve the shouldRender string token to an actual predicate.
|
||||
const resolveShouldRender = useMemo(() => {
|
||||
return (sr: FieldConfig['shouldRender']) => {
|
||||
if (!sr) return undefined;
|
||||
if (typeof sr === 'function') return sr;
|
||||
|
||||
switch (sr) {
|
||||
case 'hideWhenInstanceExists':
|
||||
return hideWhenInstanceExists;
|
||||
case 'modelTypeIncludesChat':
|
||||
return (values: any) => {
|
||||
const mt = values?.model_type;
|
||||
if (Array.isArray(mt)) return mt.includes('chat');
|
||||
return mt === 'chat';
|
||||
};
|
||||
case 'modelTypeSupportsToolCall':
|
||||
return (values: any) => {
|
||||
const mt = values?.model_type;
|
||||
if (Array.isArray(mt)) {
|
||||
return mt.includes('chat') || mt.includes('image2text');
|
||||
}
|
||||
return mt === 'chat' || mt === 'image2text';
|
||||
};
|
||||
case 'modelTypeIncludesTtsAndNotExists':
|
||||
return (values: any) => {
|
||||
if (!hideWhenInstanceExists(values)) return false;
|
||||
const mt = values?.model_type;
|
||||
if (Array.isArray(mt)) return mt.includes('tts');
|
||||
return mt === 'tts';
|
||||
};
|
||||
case 'showBaseUrl':
|
||||
return () =>
|
||||
FACTORIES_WITH_BASE_URL.some((x) => x === llmFactory) ||
|
||||
llmFactory?.toLowerCase() === 'Anthropic'.toLowerCase();
|
||||
case 'showGroupId':
|
||||
return () => llmFactory?.toLowerCase() === 'Minimax'.toLowerCase();
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
}, [hideWhenInstanceExists, llmFactory]);
|
||||
|
||||
// For each inputSelect field, build a URL → regionKey map from its
|
||||
// options (either inline `field.options` or the shared `baseUrlOptions`).
|
||||
// The map is used both to pick the "default" key's URL as the form's
|
||||
// initial value and (downstream, in useProviderModalActions) to derive
|
||||
// the `region` submit field from the user's currently selected URL.
|
||||
const baseUrlRegionMaps = useMemo(() => {
|
||||
const maps: Record<string, Map<string, string>> = {};
|
||||
config.fields.forEach((field) => {
|
||||
if (field.type !== 'inputSelect') return;
|
||||
const options =
|
||||
field.options && field.options.length > 0
|
||||
? field.options
|
||||
: (baseUrlOptions ?? []);
|
||||
const urlMap = new Map<string, string>();
|
||||
options.forEach((opt) => {
|
||||
if (opt.regionKey) {
|
||||
urlMap.set(opt.value, opt.regionKey);
|
||||
}
|
||||
});
|
||||
if (urlMap.size > 0) {
|
||||
maps[field.name] = urlMap;
|
||||
}
|
||||
});
|
||||
return maps;
|
||||
}, [config.fields, baseUrlOptions]);
|
||||
|
||||
// Convert FieldConfig to FormFieldConfig (for use by DynamicForm)
|
||||
const fields: FormFieldConfig[] = useMemo(() => {
|
||||
const res = config.fields
|
||||
.filter(
|
||||
// When the list-models picker is active, the 4 model_* fields are
|
||||
// owned by the picker (component state) and must not be registered
|
||||
// in the dynamic form.
|
||||
(field) =>
|
||||
!hasModelNameField || !LIST_MODEL_FIELD_NAMES.has(field.name),
|
||||
)
|
||||
.map((field) => {
|
||||
const placeholderText = resolveText(field.placeholder, llmFactory, t);
|
||||
const tooltipText = resolveText(field.tooltip, llmFactory, t);
|
||||
const validation = field.validation
|
||||
? {
|
||||
...field.validation,
|
||||
message: field.validation.message
|
||||
? t(field.validation.message)
|
||||
: undefined,
|
||||
}
|
||||
: undefined;
|
||||
const baseField: Omit<FormFieldConfig, 'type'> = {
|
||||
name: field.name,
|
||||
label: t(field.label),
|
||||
required: field.required,
|
||||
hidden: false,
|
||||
placeholder: placeholderText,
|
||||
tooltip: tooltipText,
|
||||
options: field.options?.map((o) => ({
|
||||
label: o.label as string,
|
||||
value: o.value,
|
||||
})) as any,
|
||||
defaultValue: field.defaultValue,
|
||||
validation,
|
||||
shouldRender: resolveShouldRender(field.shouldRender),
|
||||
// In viewMode, only the model-related fields are editable.
|
||||
// All other fields (instance_name, api_key, base_url, etc.)
|
||||
// are rendered as disabled.
|
||||
disabled: !!viewMode && !LIST_MODEL_FIELD_NAMES.has(field.name),
|
||||
dependencies:
|
||||
field.shouldRender === 'modelTypeIncludesChat' ||
|
||||
field.shouldRender === 'modelTypeSupportsToolCall' ||
|
||||
field.shouldRender === 'modelTypeIncludesTtsAndNotExists'
|
||||
? ['model_type']
|
||||
: ['model_type', 'instance_name'].includes(field.name)
|
||||
? ['model_type', 'instance_name']
|
||||
: undefined,
|
||||
};
|
||||
|
||||
// inputSelect type: use the InputSelect component, options come from baseUrlOptions
|
||||
if (field.type === 'inputSelect') {
|
||||
const inputSelectOptions: SelectOption[] =
|
||||
field.options && field.options.length > 0
|
||||
? field.options
|
||||
: (baseUrlOptions ?? []);
|
||||
return {
|
||||
...baseField,
|
||||
type: FormFieldType.Custom,
|
||||
options: inputSelectOptions as any,
|
||||
render: (fieldProps: ControllerRenderProps) => {
|
||||
return inputSelectOptions.length > 0 ? (
|
||||
<InputSelect
|
||||
{...fieldProps}
|
||||
value={(fieldProps.value ?? '') as string}
|
||||
onChange={(value) => fieldProps.onChange(value)}
|
||||
options={inputSelectOptions as any}
|
||||
placeholder={placeholderText}
|
||||
/>
|
||||
) : (
|
||||
<Input {...fieldProps} placeholder={placeholderText} />
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Other types use the enum value directly from the field config
|
||||
return { ...baseField, type: field.type };
|
||||
});
|
||||
return res;
|
||||
}, [
|
||||
config.fields,
|
||||
resolveShouldRender,
|
||||
t,
|
||||
baseUrlOptions,
|
||||
llmFactory,
|
||||
hasModelNameField,
|
||||
viewMode,
|
||||
]);
|
||||
|
||||
const defaultValues: FieldValues = useMemo(() => {
|
||||
// In editMode or viewMode, seed the form with the supplied
|
||||
// `initialValues` so the user sees the existing instance/model data.
|
||||
if ((editMode || viewMode) && initialValues) {
|
||||
return initialValues as FieldValues;
|
||||
}
|
||||
const result: FieldValues = {};
|
||||
config.fields.forEach((f) => {
|
||||
if (f.defaultValue !== undefined) {
|
||||
setNestedValue(result, f.name, f.defaultValue);
|
||||
return;
|
||||
}
|
||||
// For inputSelect fields, default the form to the option whose
|
||||
// original key in the URL object is 'default' (e.g.
|
||||
// `availableProviders.url.default`). If no such option exists,
|
||||
// leave the field empty so the user picks one explicitly.
|
||||
if (f.type === 'inputSelect') {
|
||||
const urlMap = baseUrlRegionMaps[f.name];
|
||||
if (urlMap) {
|
||||
for (const [url, regionKey] of urlMap.entries()) {
|
||||
if (regionKey === 'default') {
|
||||
setNestedValue(result, f.name, url);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
// For configurations without a default model_type, assign an empty array (multi-select field)
|
||||
const mtField = config.fields.find((f) => f.name === 'model_type');
|
||||
if (mtField && mtField.defaultValue === undefined) {
|
||||
setNestedValue(result, 'model_type', []);
|
||||
}
|
||||
return result;
|
||||
}, [
|
||||
editMode,
|
||||
viewMode,
|
||||
initialValues,
|
||||
config.fields,
|
||||
llmFactory,
|
||||
baseUrlRegionMaps,
|
||||
]);
|
||||
|
||||
// Documentation link text (rendered at the bottom of the modal)
|
||||
const docLinkText = useMemo(() => {
|
||||
if (config.docLinkText) return config.docLinkText;
|
||||
if (config.docLinkI18nKey) {
|
||||
return t(config.docLinkI18nKey, { name: llmFactory });
|
||||
}
|
||||
return null;
|
||||
}, [config, llmFactory, t]);
|
||||
|
||||
return {
|
||||
config,
|
||||
fields,
|
||||
defaultValues,
|
||||
docLinkText,
|
||||
hasModelNameField,
|
||||
baseUrlRegionMaps,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,170 @@
|
||||
import { DynamicFormRef } from '@/components/dynamic-form';
|
||||
import { IModelInfo } from '@/interfaces/request/llm';
|
||||
import { VerifyResult } from '@/pages/user-setting/setting-model/hooks';
|
||||
import { RefObject, useCallback } from 'react';
|
||||
import { FieldValues } from 'react-hook-form';
|
||||
import type {
|
||||
IViewModeOkPayload,
|
||||
ProviderConfig,
|
||||
ProviderModalProps,
|
||||
} from '../types';
|
||||
|
||||
type ActionParams = {
|
||||
config: ProviderConfig;
|
||||
viewMode?: boolean;
|
||||
hasModelNameField: boolean;
|
||||
llmFactory: string;
|
||||
initialValues?: Record<string, any>;
|
||||
modelInfoList: IModelInfo[];
|
||||
formRef: RefObject<DynamicFormRef>;
|
||||
/**
|
||||
* URL → regionKey map for each inputSelect field (built in
|
||||
* `useProviderFields`). Used to derive the `region` submit field
|
||||
* from the user's currently selected base URL.
|
||||
*/
|
||||
baseUrlRegionMaps?: Record<string, Map<string, string>>;
|
||||
onOk: ProviderModalProps['onOk'];
|
||||
onVerify: ProviderModalProps['onVerify'];
|
||||
onViewModeOk: ProviderModalProps['onViewModeOk'];
|
||||
};
|
||||
|
||||
/**
|
||||
* Look up the `region` key (e.g. 'default', 'intl', 'cn') for the
|
||||
* currently selected base URL of any inputSelect field. Returns
|
||||
* `undefined` when no inputSelect field has a value, or when that
|
||||
* value does not match any option's URL — in those cases the caller
|
||||
* should leave `region` unset.
|
||||
*/
|
||||
const resolveRegionFromValues = (
|
||||
values: Record<string, any> | undefined,
|
||||
baseUrlRegionMaps?: Record<string, Map<string, string>>,
|
||||
): string | undefined => {
|
||||
if (!values || !baseUrlRegionMaps) return undefined;
|
||||
for (const fieldName of Object.keys(baseUrlRegionMaps)) {
|
||||
const url = values[fieldName];
|
||||
if (typeof url !== 'string' || url === '') continue;
|
||||
const regionKey = baseUrlRegionMaps[fieldName].get(url);
|
||||
if (regionKey !== undefined) {
|
||||
return regionKey;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Build the two outbound handlers for the Provider modal:
|
||||
*
|
||||
* - `handleVerify` reads current form values, runs them through the
|
||||
* provider's `verifyTransform`, and forwards the result to `onVerify`.
|
||||
* Returns a `VerifyResult` (the VerifyButton consumes the shape).
|
||||
*
|
||||
* - `handleSubmit` has two paths:
|
||||
* 1. viewMode → invoke `onViewModeOk` with either the picker's selected
|
||||
* models (LIST_MODEL_PROVIDERS) or the editable form values
|
||||
* (non-LIST_MODEL_PROVIDERS). The instance itself is not re-saved.
|
||||
* 2. normal mode → run values through `submitTransform` (when present)
|
||||
* and forward to `onOk`.
|
||||
*
|
||||
* Both paths inject a `region` field derived from the currently selected
|
||||
* base URL whenever the field is an inputSelect (see `baseUrlRegionMaps`).
|
||||
*/
|
||||
export const useProviderModalActions = ({
|
||||
config,
|
||||
viewMode,
|
||||
hasModelNameField,
|
||||
llmFactory,
|
||||
initialValues,
|
||||
modelInfoList,
|
||||
formRef,
|
||||
baseUrlRegionMaps,
|
||||
onOk,
|
||||
onVerify,
|
||||
onViewModeOk,
|
||||
}: ActionParams) => {
|
||||
const handleVerify = useCallback(
|
||||
async (params: any) => {
|
||||
const values = formRef.current?.getValues() || params;
|
||||
if (!config.verifyTransform) {
|
||||
return { isValid: null, logs: '' } as VerifyResult;
|
||||
}
|
||||
const verifyArgs = config.verifyTransform(values, modelInfoList);
|
||||
const region = resolveRegionFromValues(values, baseUrlRegionMaps);
|
||||
if (region !== undefined) {
|
||||
verifyArgs.region = region;
|
||||
}
|
||||
const res = await onVerify({ ...params, ...verifyArgs });
|
||||
return (res || { isValid: null, logs: '' }) as VerifyResult;
|
||||
},
|
||||
[config, onVerify, modelInfoList, formRef, baseUrlRegionMaps],
|
||||
);
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
async (values?: FieldValues) => {
|
||||
if (!values) return;
|
||||
|
||||
// viewMode: only add/update models. The instance itself is not
|
||||
// re-saved because all instance-level fields are disabled. The
|
||||
// parent receives the selected models (or the model-related form
|
||||
// values for non-list-model providers) via `onViewModeOk`.
|
||||
if (viewMode) {
|
||||
if (!onViewModeOk) {
|
||||
// No viewMode handler provided — nothing to save, just close
|
||||
// (the modal's own hideModal flow handles closing).
|
||||
return;
|
||||
}
|
||||
const instanceName = String(
|
||||
(initialValues as any)?.instance_name ?? '',
|
||||
);
|
||||
const payload: IViewModeOkPayload = hasModelNameField
|
||||
? {
|
||||
instanceName,
|
||||
llmFactory,
|
||||
modelInfos: modelInfoList,
|
||||
}
|
||||
: {
|
||||
instanceName,
|
||||
llmFactory,
|
||||
modelInfos: [],
|
||||
formValues: values as Record<string, any>,
|
||||
};
|
||||
await onViewModeOk(payload);
|
||||
return;
|
||||
}
|
||||
|
||||
const transformed = (
|
||||
config.submitTransform
|
||||
? config.submitTransform(values as Record<string, any>, modelInfoList)
|
||||
: values
|
||||
) as Record<string, any>;
|
||||
const region = resolveRegionFromValues(
|
||||
values as Record<string, any>,
|
||||
baseUrlRegionMaps,
|
||||
);
|
||||
if (region !== undefined) {
|
||||
transformed.region = region;
|
||||
}
|
||||
// Always include `llm_factory` in the submitted payload. Some
|
||||
// providers' submitTransforms (e.g. GenericApiKeyConfig) omit it,
|
||||
// but the parent uses it to build the request URL
|
||||
// (`/api/v1/providers/${llm_factory}/instances`); without it the
|
||||
// URL becomes `/providers/undefined/instances`.
|
||||
if (!transformed.llm_factory) {
|
||||
transformed.llm_factory = llmFactory;
|
||||
}
|
||||
await onOk?.(transformed, false);
|
||||
},
|
||||
[
|
||||
config,
|
||||
onOk,
|
||||
onViewModeOk,
|
||||
modelInfoList,
|
||||
viewMode,
|
||||
hasModelNameField,
|
||||
llmFactory,
|
||||
initialValues,
|
||||
baseUrlRegionMaps,
|
||||
],
|
||||
);
|
||||
|
||||
return { handleVerify, handleSubmit };
|
||||
};
|
||||
@@ -0,0 +1,199 @@
|
||||
import { DynamicForm, DynamicFormRef } from '@/components/dynamic-form';
|
||||
import { Modal } from '@/components/ui/modal/modal';
|
||||
import { ToggleList } from '@/components/ui/toggle-list';
|
||||
import { useCommonTranslation, useTranslate } from '@/hooks/common-hooks';
|
||||
import {
|
||||
useFetchInstanceNameSet,
|
||||
useHideWhenInstanceExists,
|
||||
} from '@/pages/user-setting/setting-model/hooks';
|
||||
import { memo, useRef } from 'react';
|
||||
import { FieldValues } from 'react-hook-form';
|
||||
import { LLMHeader } from '../../components/llm-header';
|
||||
import VerifyButton from '../verify-button';
|
||||
import {
|
||||
useListModelsOptions,
|
||||
useListModelsPicker,
|
||||
useProviderFields,
|
||||
useProviderModalActions,
|
||||
} from './hooks';
|
||||
import type { ProviderModalProps } from './types';
|
||||
|
||||
const ProviderModal = ({
|
||||
visible,
|
||||
hideModal,
|
||||
llmFactory,
|
||||
loading,
|
||||
editMode,
|
||||
viewMode,
|
||||
initialValues,
|
||||
baseUrlOptions,
|
||||
onOk,
|
||||
onVerify,
|
||||
onViewModeOk,
|
||||
}: ProviderModalProps) => {
|
||||
const { t } = useTranslate('setting');
|
||||
const { t: tc } = useCommonTranslation();
|
||||
const formRef = useRef<DynamicFormRef>(null);
|
||||
const { instanceNameSet } = useFetchInstanceNameSet(llmFactory);
|
||||
const hideWhenInstanceExists = useHideWhenInstanceExists(instanceNameSet);
|
||||
|
||||
// Field config, default values, doc link, and the LIST_MODEL_PROVIDERS
|
||||
// flag are all derived from the current llmFactory / mode / initialValues.
|
||||
// `baseUrlRegionMaps` is forwarded to the actions hook so the modal can
|
||||
// populate the `region` submit field from the currently selected base URL.
|
||||
const {
|
||||
config,
|
||||
fields,
|
||||
defaultValues,
|
||||
docLinkText,
|
||||
hasModelNameField,
|
||||
baseUrlRegionMaps,
|
||||
} = useProviderFields({
|
||||
llmFactory,
|
||||
editMode,
|
||||
viewMode,
|
||||
initialValues,
|
||||
baseUrlOptions,
|
||||
hideWhenInstanceExists,
|
||||
});
|
||||
|
||||
// Owns the "List Models" picker state and lifecycle. When
|
||||
// `hasModelNameField` is false the picker is hidden and this hook is
|
||||
// effectively idle (no fetch, no selection state in use).
|
||||
const {
|
||||
models,
|
||||
listLoading,
|
||||
selectedModelItems,
|
||||
modelInfoList,
|
||||
allSelected,
|
||||
handleListOpenChange,
|
||||
handleSelectModel,
|
||||
handleToggleAll,
|
||||
} = useListModelsPicker({
|
||||
visible,
|
||||
hasModelNameField,
|
||||
editMode,
|
||||
viewMode,
|
||||
initialValues,
|
||||
llmFactory,
|
||||
config,
|
||||
formRef,
|
||||
});
|
||||
|
||||
// Render-only: turn the fetched model list into ToggleList options with
|
||||
// the "All models" sentinel row at the top.
|
||||
const listModelsOptions = useListModelsOptions({
|
||||
models,
|
||||
selectedModelItems,
|
||||
allSelected,
|
||||
handleSelectModel,
|
||||
handleToggleAll,
|
||||
});
|
||||
|
||||
// Submit and verify handlers — branch on viewMode and on whether the
|
||||
// picker owns the model fields.
|
||||
const { handleVerify, handleSubmit } = useProviderModalActions({
|
||||
config,
|
||||
viewMode,
|
||||
hasModelNameField,
|
||||
llmFactory,
|
||||
initialValues,
|
||||
modelInfoList,
|
||||
formRef,
|
||||
baseUrlRegionMaps,
|
||||
onOk,
|
||||
onVerify,
|
||||
onViewModeOk,
|
||||
});
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={<LLMHeader name={llmFactory} />}
|
||||
open={visible || false}
|
||||
onOpenChange={(open) => !open && hideModal?.()}
|
||||
maskClosable={false}
|
||||
footer={<div className="p-4"></div>}
|
||||
>
|
||||
<DynamicForm.Root
|
||||
key={`${visible}-${llmFactory}`}
|
||||
fields={fields}
|
||||
onSubmit={() => {
|
||||
// The actual submission is handled by SavingButton
|
||||
}}
|
||||
ref={formRef}
|
||||
defaultValues={defaultValues}
|
||||
labelClassName="font-normal"
|
||||
>
|
||||
{hasModelNameField && (
|
||||
<ToggleList
|
||||
className="w-full"
|
||||
buttonClassName="self-end"
|
||||
// searchable
|
||||
btnText={hasModelNameField ? t('listModels') : 'Select an option'}
|
||||
options={
|
||||
hasModelNameField
|
||||
? listModelsOptions
|
||||
: listModelsOptions.length > 0
|
||||
? listModelsOptions
|
||||
: []
|
||||
}
|
||||
searchPlaceholder={t('listModelsSearchPlaceholder')}
|
||||
emptyText={t('listModelsEmpty')}
|
||||
searchLoading={listLoading}
|
||||
onOpenChange={handleListOpenChange}
|
||||
maxHeight={400}
|
||||
closeOnOutsideClick
|
||||
/>
|
||||
)}
|
||||
|
||||
<VerifyButton onVerify={handleVerify} />
|
||||
|
||||
<div
|
||||
className={
|
||||
docLinkText
|
||||
? 'absolute bottom-0 right-0 left-0 flex items-center justify-between w-full py-6 px-6'
|
||||
: 'absolute bottom-0 right-0 left-0 flex items-center justify-end w-full gap-2 py-6 px-6'
|
||||
}
|
||||
>
|
||||
{docLinkText && config.docLink && (
|
||||
<a
|
||||
href={config.docLink}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-primary hover:underline ml-24"
|
||||
>
|
||||
{docLinkText}
|
||||
</a>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2">
|
||||
<DynamicForm.CancelButton
|
||||
handleCancel={() => {
|
||||
hideModal?.();
|
||||
}}
|
||||
/>
|
||||
<DynamicForm.SavingButton
|
||||
submitLoading={loading || false}
|
||||
buttonText={tc('ok')}
|
||||
submitFunc={(values: FieldValues) => {
|
||||
handleSubmit(values);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</DynamicForm.Root>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ProviderModal);
|
||||
|
||||
// Export field configurations (for use by other modules)
|
||||
export { FACTORIES_WITH_BASE_URL, getProviderConfig } from './field-config';
|
||||
export type {
|
||||
FieldConfig,
|
||||
IViewModeOkPayload,
|
||||
ProviderConfig,
|
||||
ProviderModalProps,
|
||||
ShouldRenderToken,
|
||||
} from './types';
|
||||
@@ -0,0 +1,194 @@
|
||||
import { FormFieldType } from '@/components/dynamic-form';
|
||||
import type { IModelInfo } from '@/interfaces/request/llm';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
/**
|
||||
* Form field types.
|
||||
* - `FormFieldType.*` values map 1:1 to DynamicForm's field types.
|
||||
* - `'inputSelect'` is a project-specific token: a text input combined with a
|
||||
* dropdown of suggested values. The ProviderModal resolves it into
|
||||
* `FormFieldType.Custom` with a custom `render` function.
|
||||
*/
|
||||
export type FieldType = FormFieldType | 'inputSelect';
|
||||
|
||||
/**
|
||||
* String tokens for shouldRender
|
||||
* The component resolves these into actual functions based on runtime context (instanceNameSet, etc.)
|
||||
*/
|
||||
export type ShouldRenderToken =
|
||||
| 'hideWhenInstanceExists'
|
||||
| 'modelTypeIncludesChat'
|
||||
| 'modelTypeSupportsToolCall'
|
||||
| 'modelTypeIncludesTtsAndNotExists'
|
||||
| 'showBaseUrl'
|
||||
| 'showGroupId';
|
||||
|
||||
/**
|
||||
* Option label can be a string or ReactNode (used for rich-text labels in InputSelect).
|
||||
* `regionKey` is the original key from the provider's `url` object (e.g. 'default',
|
||||
* 'intl', 'cn'). It is preserved on the option so that the modal can map the
|
||||
* currently selected URL back to its key for the `region` submit field.
|
||||
*/
|
||||
export type SelectOption = {
|
||||
label: string | ReactNode;
|
||||
value: string;
|
||||
regionKey?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolver for a text value that may differ by factory (provider).
|
||||
* Use when a shared FieldConfig entry needs different i18n keys per provider
|
||||
* (e.g. the generic `base_url` field renders different tooltip / placeholder
|
||||
* for Minimax, TongYiQianWen, SILICONFLOW, etc.).
|
||||
*/
|
||||
export type FactoryTextResolver = (llmFactory: string) => string;
|
||||
|
||||
/**
|
||||
* Field config: defines the presentation and behavior of a form field
|
||||
*/
|
||||
export interface FieldConfig {
|
||||
/** Field name (supports nested paths, e.g. 'model_info.model_type') */
|
||||
name: string;
|
||||
/** Label i18n key */
|
||||
label: string;
|
||||
/** Field type */
|
||||
type: FieldType;
|
||||
/** Whether the field is required */
|
||||
required?: boolean;
|
||||
/**
|
||||
* Placeholder i18n key. May be a static key, or a function that takes the
|
||||
* current `llmFactory` and returns the key (for per-provider placeholders).
|
||||
*/
|
||||
placeholder?: string | FactoryTextResolver;
|
||||
/**
|
||||
* Tooltip i18n key. May be a static key, or a function that takes the
|
||||
* current `llmFactory` and returns the key (for per-provider tooltips).
|
||||
*/
|
||||
tooltip?: string | FactoryTextResolver;
|
||||
/** Options (used for select/multiSelect/inputSelect) */
|
||||
options?: SelectOption[];
|
||||
/** Default value */
|
||||
defaultValue?: any;
|
||||
/**
|
||||
* Validation rules.
|
||||
* `message` is treated as an i18n key by the ProviderModal and translated
|
||||
* via `t()` at field-build time. In `Number` fields, `min` / `max` bound
|
||||
* the value; the message is shown when the bound is violated.
|
||||
*/
|
||||
validation?: {
|
||||
min?: number;
|
||||
max?: number;
|
||||
message?: string;
|
||||
};
|
||||
/**
|
||||
* Conditional rendering: returns true to show the field
|
||||
* @param values current form values
|
||||
*/
|
||||
shouldRender?: ((values: Record<string, any>) => boolean) | ShouldRenderToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provider config: defines the full behavior of a LLM provider modal
|
||||
*/
|
||||
export interface ProviderConfig {
|
||||
/** Corresponding LLMFactory value (also used as the field-config key) */
|
||||
llmFactory: string;
|
||||
/** Modal title */
|
||||
title: string;
|
||||
/** Field list (in render order) */
|
||||
fields: FieldConfig[];
|
||||
/**
|
||||
* Transform form values into verify API parameters
|
||||
* Used to construct api_key / base_url / region / model_info when the Verify button is clicked.
|
||||
* `modelInfo` is the array of currently selected models from the list-models picker
|
||||
* (one entry per checked list item). Providers without a list-models picker can ignore it.
|
||||
*/
|
||||
verifyTransform?: (
|
||||
values: Record<string, any>,
|
||||
modelInfo: IModelInfo[],
|
||||
) => {
|
||||
apiKey: string;
|
||||
baseUrl?: string;
|
||||
region?: string;
|
||||
modelInfo?: IModelInfo[];
|
||||
};
|
||||
/**
|
||||
* Transform form values into submit API parameters.
|
||||
* Used to handle special field name mapping (e.g. volcengine's endpoint_id -> ark_api_key).
|
||||
* `modelInfo` is the array of currently selected models from the list-models picker
|
||||
* (one entry per checked list item). Providers without a list-models picker can ignore it.
|
||||
*/
|
||||
submitTransform?: (
|
||||
values: Record<string, any>,
|
||||
modelInfo: IModelInfo[],
|
||||
) => Record<string, any>;
|
||||
/**
|
||||
* Optional link at the bottom of the modal
|
||||
* e.g. the official documentation link for Ollama-family providers
|
||||
*/
|
||||
docLink?: string;
|
||||
/**
|
||||
* i18n key for the docLink text (optional)
|
||||
* e.g. 'ollamaLink'; the { name: llmFactory } variable is passed in
|
||||
*/
|
||||
docLinkI18nKey?: string;
|
||||
/**
|
||||
* Custom docLink text (optional, takes precedence over docLinkI18nKey)
|
||||
*/
|
||||
docLinkText?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Payload for the viewMode save callback. The modal calls `onViewModeOk`
|
||||
* (when provided) instead of `onOk` whenever `viewMode` is true.
|
||||
*
|
||||
* - `instanceName` is the pre-existing instance's name (taken from
|
||||
* `initialValues.instance_name`).
|
||||
* - `llmFactory` is the current provider/factory.
|
||||
* - For LIST_MODEL_PROVIDERS, `modelInfos` carries the full list of
|
||||
* currently checked models in the picker (one IModelInfo per checked
|
||||
* item) and `formValues` is undefined.
|
||||
* - For non-LIST_MODEL_PROVIDERS, the picker is hidden so `modelInfos`
|
||||
* is empty and `formValues` carries the editable model-related form
|
||||
* values (model_name, model_type, max_tokens, is_tools).
|
||||
*/
|
||||
export interface IViewModeOkPayload {
|
||||
instanceName: string;
|
||||
llmFactory: string;
|
||||
modelInfos: IModelInfo[];
|
||||
formValues?: Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* ProviderModal component props
|
||||
*/
|
||||
export interface ProviderModalProps {
|
||||
visible: boolean;
|
||||
hideModal: () => void;
|
||||
llmFactory: string;
|
||||
loading: boolean;
|
||||
editMode?: boolean;
|
||||
/**
|
||||
* Read-only "edit models" mode: opens the modal pre-filled with an
|
||||
* existing instance's data and only allows editing the model-related
|
||||
* fields (model_name, model_type, max_tokens, is_tools) plus the
|
||||
* list-models picker (when applicable). All other fields are disabled.
|
||||
* On save, only `addInstanceModel` is invoked (not `addProviderInstance`).
|
||||
*/
|
||||
viewMode?: boolean;
|
||||
initialValues?: Record<string, any>;
|
||||
/**
|
||||
* Base URL options for the input+select combo (from IAvailableProvider.url)
|
||||
* Used by base_url/api_base fields of type inputSelect
|
||||
*/
|
||||
baseUrlOptions?: SelectOption[];
|
||||
onOk: (payload: any, isVerify?: boolean) => Promise<any>;
|
||||
onVerify: (payload: any) => Promise<any>;
|
||||
/**
|
||||
* Save handler used when `viewMode` is true. The modal calls this with
|
||||
* the list of selected models (LIST_MODEL_PROVIDERS) or the editable
|
||||
* model-related form values (non-LIST_MODEL_PROVIDERS). If omitted,
|
||||
* the modal falls back to `onOk` and submits the standard payload.
|
||||
*/
|
||||
onViewModeOk?: (payload: IViewModeOkPayload) => Promise<any>;
|
||||
}
|
||||
@@ -1,234 +0,0 @@
|
||||
import {
|
||||
DynamicForm,
|
||||
DynamicFormRef,
|
||||
FormFieldConfig,
|
||||
FormFieldType,
|
||||
} from '@/components/dynamic-form';
|
||||
import { Modal } from '@/components/ui/modal/modal';
|
||||
import { useCommonTranslation, useTranslate } from '@/hooks/common-hooks';
|
||||
import { useBuildModelTypeOptions } from '@/hooks/logic-hooks/use-build-options';
|
||||
import { IModalProps } from '@/interfaces/common';
|
||||
import { IAddProviderInstanceRequestBody } from '@/interfaces/request/llm';
|
||||
import {
|
||||
useFetchInstanceNameSet,
|
||||
useHideWhenInstanceExists,
|
||||
VerifyResult,
|
||||
} from '@/pages/user-setting/setting-model/hooks';
|
||||
import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { FieldValues } from 'react-hook-form';
|
||||
import { LLMHeader } from '../../components/llm-header';
|
||||
import VerifyButton from '../../modal/verify-button';
|
||||
|
||||
const SparkModal = ({
|
||||
visible,
|
||||
hideModal,
|
||||
onOk,
|
||||
onVerify,
|
||||
loading,
|
||||
llmFactory,
|
||||
}: IModalProps<IAddProviderInstanceRequestBody> & {
|
||||
llmFactory: string;
|
||||
onVerify?: (
|
||||
postBody: any,
|
||||
) => Promise<boolean | void | VerifyResult | undefined>;
|
||||
}) => {
|
||||
const { t } = useTranslate('setting');
|
||||
const { t: tc } = useCommonTranslation();
|
||||
const { buildModelTypeOptions } = useBuildModelTypeOptions();
|
||||
const formRef = useRef<DynamicFormRef>(null);
|
||||
const { instanceNameSet } = useFetchInstanceNameSet(llmFactory);
|
||||
|
||||
const hideWhenInstanceExists = useHideWhenInstanceExists(instanceNameSet);
|
||||
|
||||
const fields: FormFieldConfig[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
name: 'instance_name',
|
||||
label: t('instanceName'),
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: t('instanceNameMessage'),
|
||||
tooltip: t('instanceNameTip'),
|
||||
validation: { message: t('instanceNameMessage') },
|
||||
},
|
||||
{
|
||||
name: 'model_type',
|
||||
label: t('modelType'),
|
||||
type: FormFieldType.MultiSelect,
|
||||
required: true,
|
||||
options: buildModelTypeOptions(['chat', 'tts']),
|
||||
defaultValue: ['chat'],
|
||||
},
|
||||
{
|
||||
name: 'llm_name',
|
||||
label: t('modelName'),
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: t('modelNameMessage'),
|
||||
validation: {
|
||||
message: t('SparkModelNameMessage'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'spark_api_password',
|
||||
label: t('addSparkAPIPassword'),
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: t('SparkAPIPasswordMessage'),
|
||||
validation: {
|
||||
message: t('SparkAPIPasswordMessage'),
|
||||
},
|
||||
shouldRender: hideWhenInstanceExists,
|
||||
},
|
||||
{
|
||||
name: 'spark_app_id',
|
||||
label: t('addSparkAPPID'),
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: t('SparkAPPIDMessage'),
|
||||
validation: {
|
||||
message: t('SparkAPPIDMessage'),
|
||||
},
|
||||
dependencies: ['model_type', 'instance_name'],
|
||||
shouldRender: (formValues: any) => {
|
||||
if (!hideWhenInstanceExists(formValues)) return false;
|
||||
const modelType = formValues?.model_type;
|
||||
if (Array.isArray(modelType)) {
|
||||
return modelType.includes('tts');
|
||||
}
|
||||
return modelType === 'tts';
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'spark_api_secret',
|
||||
label: t('addSparkAPISecret'),
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: t('SparkAPISecretMessage'),
|
||||
validation: {
|
||||
message: t('SparkAPISecretMessage'),
|
||||
},
|
||||
dependencies: ['model_type', 'instance_name'],
|
||||
shouldRender: (formValues: any) => {
|
||||
if (!hideWhenInstanceExists(formValues)) return false;
|
||||
const modelType = formValues?.model_type;
|
||||
if (Array.isArray(modelType)) {
|
||||
return modelType.includes('tts');
|
||||
}
|
||||
return modelType === 'tts';
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'spark_api_key',
|
||||
label: t('addSparkAPIKey'),
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: t('SparkAPIKeyMessage'),
|
||||
validation: {
|
||||
message: t('SparkAPIKeyMessage'),
|
||||
},
|
||||
dependencies: ['model_type', 'instance_name'],
|
||||
shouldRender: (formValues: any) => {
|
||||
if (!hideWhenInstanceExists(formValues)) return false;
|
||||
const modelType = formValues?.model_type;
|
||||
if (Array.isArray(modelType)) {
|
||||
return modelType.includes('tts');
|
||||
}
|
||||
return modelType === 'tts';
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'max_tokens',
|
||||
label: t('maxTokens'),
|
||||
type: FormFieldType.Number,
|
||||
required: true,
|
||||
placeholder: t('maxTokensTip'),
|
||||
validation: {
|
||||
min: 0,
|
||||
message: t('maxTokensInvalidMessage'),
|
||||
},
|
||||
},
|
||||
],
|
||||
[t, buildModelTypeOptions, hideWhenInstanceExists],
|
||||
);
|
||||
|
||||
const handleOk = async (values?: FieldValues) => {
|
||||
if (!values) return;
|
||||
|
||||
const data = {
|
||||
instance_name: values.instance_name as string,
|
||||
model_type: values.model_type,
|
||||
llm_factory: llmFactory,
|
||||
max_tokens: values.max_tokens,
|
||||
};
|
||||
|
||||
await onOk?.(data as IAddProviderInstanceRequestBody);
|
||||
};
|
||||
|
||||
const verifyParamsFunc = useCallback(() => {
|
||||
const values = formRef.current?.getValues();
|
||||
return {
|
||||
llm_factory: llmFactory,
|
||||
model_type: values.model_type,
|
||||
};
|
||||
}, [llmFactory]);
|
||||
|
||||
const handleVerify = useCallback(
|
||||
async (params: any) => {
|
||||
const verifyParams = verifyParamsFunc();
|
||||
const res = await onVerify?.({ ...params, ...verifyParams });
|
||||
return (res || { isValid: null, logs: '' }) as VerifyResult;
|
||||
},
|
||||
[verifyParamsFunc, onVerify],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible) {
|
||||
formRef.current?.reset();
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={<LLMHeader name={llmFactory} />}
|
||||
open={visible || false}
|
||||
onOpenChange={(open) => !open && hideModal?.()}
|
||||
maskClosable={false}
|
||||
footer={<div className="p-4"></div>}
|
||||
>
|
||||
<DynamicForm.Root
|
||||
fields={fields}
|
||||
onSubmit={(data) => {
|
||||
console.log(data);
|
||||
}}
|
||||
ref={formRef}
|
||||
defaultValues={
|
||||
{
|
||||
instance_name: '',
|
||||
model_type: ['chat'],
|
||||
max_tokens: 8192,
|
||||
} as FieldValues
|
||||
}
|
||||
labelClassName="font-normal"
|
||||
>
|
||||
{onVerify && <VerifyButton onVerify={handleVerify} />}
|
||||
<div className="absolute bottom-0 right-0 left-0 flex items-center justify-end w-full gap-2 py-6 px-6">
|
||||
<DynamicForm.CancelButton
|
||||
handleCancel={() => {
|
||||
hideModal?.();
|
||||
}}
|
||||
/>
|
||||
<DynamicForm.SavingButton
|
||||
submitLoading={loading || false}
|
||||
buttonText={tc('ok')}
|
||||
submitFunc={(values: FieldValues) => {
|
||||
handleOk(values);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</DynamicForm.Root>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(SparkModal);
|
||||
@@ -12,12 +12,14 @@ interface IVerifyButton {
|
||||
onVerify: (params: any) => Promise<VerifyResult>;
|
||||
isAbsolute?: boolean;
|
||||
params?: any;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const VerifyButton: React.FC<IVerifyButton> = ({
|
||||
onVerify,
|
||||
isAbsolute = true,
|
||||
params,
|
||||
className,
|
||||
}) => {
|
||||
const { t, i18n } = useTranslate('setting');
|
||||
const isArabic = (i18n.resolvedLanguage || i18n.language || '')
|
||||
@@ -82,6 +84,7 @@ const VerifyButton: React.FC<IVerifyButton> = ({
|
||||
!isAbsolute || (verifyResult && verifyResult.isValid === false)
|
||||
? 'flex flex-col gap-5 w-full '
|
||||
: `absolute bottom-6 z-[100] ${isArabic ? 'right-6' : 'left-6'}`,
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="flex gap-2 items-center">
|
||||
|
||||
@@ -1,197 +0,0 @@
|
||||
import {
|
||||
DynamicForm,
|
||||
DynamicFormRef,
|
||||
FormFieldConfig,
|
||||
FormFieldType,
|
||||
} from '@/components/dynamic-form';
|
||||
import { Modal } from '@/components/ui/modal/modal';
|
||||
import { useCommonTranslation, useTranslate } from '@/hooks/common-hooks';
|
||||
import { useBuildModelTypeOptions } from '@/hooks/logic-hooks/use-build-options';
|
||||
import { IModalProps } from '@/interfaces/common';
|
||||
import { IAddProviderInstanceRequestBody } from '@/interfaces/request/llm';
|
||||
import {
|
||||
useFetchInstanceNameSet,
|
||||
useHideWhenInstanceExists,
|
||||
VerifyResult,
|
||||
} from '@/pages/user-setting/setting-model/hooks';
|
||||
import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { FieldValues } from 'react-hook-form';
|
||||
import { LLMHeader } from '../../components/llm-header';
|
||||
import VerifyButton from '../../modal/verify-button';
|
||||
|
||||
type VolcEngineLlmRequest = IAddProviderInstanceRequestBody & {
|
||||
endpoint_id: string;
|
||||
ark_api_key: string;
|
||||
};
|
||||
|
||||
const VolcEngineModal = ({
|
||||
visible,
|
||||
hideModal,
|
||||
onOk,
|
||||
onVerify,
|
||||
loading,
|
||||
llmFactory,
|
||||
}: IModalProps<IAddProviderInstanceRequestBody> & {
|
||||
llmFactory: string;
|
||||
onVerify?: (
|
||||
postBody: any,
|
||||
) => Promise<boolean | void | VerifyResult | undefined>;
|
||||
}) => {
|
||||
const { t } = useTranslate('setting');
|
||||
const { t: tc } = useCommonTranslation();
|
||||
const { buildModelTypeOptions } = useBuildModelTypeOptions();
|
||||
const formRef = useRef<DynamicFormRef>(null);
|
||||
const { instanceNameSet } = useFetchInstanceNameSet(llmFactory);
|
||||
|
||||
const hideWhenInstanceExists = useHideWhenInstanceExists(instanceNameSet);
|
||||
|
||||
const fields: FormFieldConfig[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
name: 'instance_name',
|
||||
label: t('instanceName'),
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: t('instanceNameMessage'),
|
||||
tooltip: t('instanceNameTip'),
|
||||
validation: { message: t('instanceNameMessage') },
|
||||
},
|
||||
{
|
||||
name: 'model_type',
|
||||
label: t('modelType'),
|
||||
type: FormFieldType.MultiSelect,
|
||||
required: true,
|
||||
options: buildModelTypeOptions(['chat', 'embedding', 'image2text']),
|
||||
defaultValue: ['chat'],
|
||||
},
|
||||
{
|
||||
name: 'llm_name',
|
||||
label: t('modelName'),
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: t('volcModelNameMessage'),
|
||||
},
|
||||
{
|
||||
name: 'endpoint_id',
|
||||
label: t('addEndpointID'),
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: t('endpointIDMessage'),
|
||||
shouldRender: hideWhenInstanceExists,
|
||||
},
|
||||
{
|
||||
name: 'ark_api_key',
|
||||
label: t('addArkApiKey'),
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: t('ArkApiKeyMessage'),
|
||||
shouldRender: hideWhenInstanceExists,
|
||||
},
|
||||
{
|
||||
name: 'max_tokens',
|
||||
label: t('maxTokens'),
|
||||
type: FormFieldType.Number,
|
||||
required: true,
|
||||
placeholder: t('maxTokensTip'),
|
||||
validation: {
|
||||
min: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
[t, buildModelTypeOptions, hideWhenInstanceExists],
|
||||
);
|
||||
|
||||
const handleOk = async (values?: FieldValues) => {
|
||||
if (!values) return;
|
||||
|
||||
const data: VolcEngineLlmRequest = {
|
||||
instance_name: values.instance_name as string,
|
||||
llm_factory: llmFactory,
|
||||
llm_name: values.llm_name as string,
|
||||
model_type: values.model_type,
|
||||
endpoint_id: values.endpoint_id as string,
|
||||
ark_api_key: values.ark_api_key as string,
|
||||
max_tokens: values.max_tokens as number,
|
||||
};
|
||||
|
||||
await onOk?.(data);
|
||||
};
|
||||
|
||||
const verifyParamsFunc = useCallback(() => {
|
||||
const values = formRef.current?.getValues();
|
||||
return {
|
||||
llm_factory: llmFactory,
|
||||
model_type: values.model_type,
|
||||
};
|
||||
}, [llmFactory]);
|
||||
|
||||
const handleVerify = useCallback(
|
||||
async (params: any) => {
|
||||
const verifyParams = verifyParamsFunc();
|
||||
const res = await onVerify?.({ ...params, ...verifyParams });
|
||||
return (res || { isValid: null, logs: '' }) as VerifyResult;
|
||||
},
|
||||
[verifyParamsFunc, onVerify],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible) {
|
||||
formRef.current?.reset();
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={<LLMHeader name={llmFactory} />}
|
||||
open={visible || false}
|
||||
onOpenChange={(open) => !open && hideModal?.()}
|
||||
maskClosable={false}
|
||||
footer={<div className="p-4"></div>}
|
||||
>
|
||||
<DynamicForm.Root
|
||||
fields={fields}
|
||||
onSubmit={(data) => {
|
||||
console.log(data);
|
||||
}}
|
||||
ref={formRef}
|
||||
defaultValues={
|
||||
{
|
||||
instance_name: '',
|
||||
model_type: ['chat'],
|
||||
max_tokens: 8192,
|
||||
} as FieldValues
|
||||
}
|
||||
labelClassName="font-normal"
|
||||
>
|
||||
{onVerify && (
|
||||
<VerifyButton onVerify={handleVerify} isAbsolute={false} />
|
||||
)}
|
||||
<div className="absolute bottom-0 right-0 left-0 flex items-center justify-between w-full py-6 px-6">
|
||||
<a
|
||||
href="https://www.volcengine.com/docs/82379/1302008"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{t('ollamaLink', { name: llmFactory })}
|
||||
</a>
|
||||
<div className="flex gap-2">
|
||||
<DynamicForm.CancelButton
|
||||
handleCancel={() => {
|
||||
hideModal?.();
|
||||
}}
|
||||
/>
|
||||
<DynamicForm.SavingButton
|
||||
submitLoading={loading || false}
|
||||
buttonText={tc('ok')}
|
||||
submitFunc={(values: FieldValues) => {
|
||||
handleOk(values);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</DynamicForm.Root>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(VolcEngineModal);
|
||||
@@ -1,188 +0,0 @@
|
||||
import {
|
||||
DynamicForm,
|
||||
DynamicFormRef,
|
||||
FormFieldConfig,
|
||||
FormFieldType,
|
||||
} from '@/components/dynamic-form';
|
||||
import { Modal } from '@/components/ui/modal/modal';
|
||||
import { useCommonTranslation, useTranslate } from '@/hooks/common-hooks';
|
||||
import { useBuildModelTypeOptions } from '@/hooks/logic-hooks/use-build-options';
|
||||
import { IModalProps } from '@/interfaces/common';
|
||||
import { IAddProviderInstanceRequestBody } from '@/interfaces/request/llm';
|
||||
import {
|
||||
useFetchInstanceNameSet,
|
||||
useHideWhenInstanceExists,
|
||||
VerifyResult,
|
||||
} from '@/pages/user-setting/setting-model/hooks';
|
||||
import { memo, useCallback, useMemo, useRef } from 'react';
|
||||
import { FieldValues } from 'react-hook-form';
|
||||
import { LLMHeader } from '../../components/llm-header';
|
||||
import VerifyButton from '../../modal/verify-button';
|
||||
|
||||
const YiyanModal = ({
|
||||
visible,
|
||||
hideModal,
|
||||
onOk,
|
||||
onVerify,
|
||||
loading,
|
||||
llmFactory,
|
||||
}: IModalProps<IAddProviderInstanceRequestBody> & {
|
||||
llmFactory: string;
|
||||
onVerify?: (
|
||||
postBody: any,
|
||||
) => Promise<boolean | void | VerifyResult | undefined>;
|
||||
}) => {
|
||||
const { t } = useTranslate('setting');
|
||||
const { t: tc } = useCommonTranslation();
|
||||
const { buildModelTypeOptions } = useBuildModelTypeOptions();
|
||||
const formRef = useRef<DynamicFormRef>(null);
|
||||
const { instanceNameSet } = useFetchInstanceNameSet(llmFactory);
|
||||
|
||||
const hideWhenInstanceExists = useHideWhenInstanceExists(instanceNameSet);
|
||||
|
||||
const fields = useMemo<FormFieldConfig[]>(
|
||||
() => [
|
||||
{
|
||||
name: 'instance_name',
|
||||
label: t('instanceName'),
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: t('instanceNameMessage'),
|
||||
tooltip: t('instanceNameTip'),
|
||||
validation: { message: t('instanceNameMessage') },
|
||||
},
|
||||
{
|
||||
name: 'model_type',
|
||||
label: t('modelType'),
|
||||
type: FormFieldType.MultiSelect,
|
||||
required: true,
|
||||
options: buildModelTypeOptions(['chat', 'embedding', 'rerank']),
|
||||
defaultValue: ['chat'],
|
||||
},
|
||||
{
|
||||
name: 'llm_name',
|
||||
label: t('modelName'),
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: t('yiyanModelNameMessage'),
|
||||
},
|
||||
{
|
||||
name: 'yiyan_ak',
|
||||
label: t('addyiyanAK'),
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: t('yiyanAKMessage'),
|
||||
shouldRender: hideWhenInstanceExists,
|
||||
},
|
||||
{
|
||||
name: 'yiyan_sk',
|
||||
label: t('addyiyanSK'),
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: t('yiyanSKMessage'),
|
||||
shouldRender: hideWhenInstanceExists,
|
||||
},
|
||||
{
|
||||
name: 'max_tokens',
|
||||
label: t('maxTokens'),
|
||||
type: FormFieldType.Number,
|
||||
required: true,
|
||||
placeholder: t('maxTokensTip'),
|
||||
validation: {
|
||||
min: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
[t, buildModelTypeOptions, hideWhenInstanceExists],
|
||||
);
|
||||
|
||||
const handleOk = async (values?: FieldValues) => {
|
||||
if (!values) return;
|
||||
|
||||
const data: IAddProviderInstanceRequestBody = {
|
||||
instance_name: values.instance_name as string,
|
||||
llm_factory: llmFactory,
|
||||
llm_name: values.llm_name as string,
|
||||
model_type: values.model_type,
|
||||
api_key: {
|
||||
yiyan_ak: values.yiyan_ak,
|
||||
yiyan_sk: values.yiyan_sk,
|
||||
},
|
||||
max_tokens: values.max_tokens as number,
|
||||
};
|
||||
|
||||
await onOk?.(data);
|
||||
};
|
||||
|
||||
const verifyParamsFunc = useCallback(() => {
|
||||
const values = formRef.current?.getValues();
|
||||
return {
|
||||
llm_factory: llmFactory,
|
||||
llm_name: values.llm_name as string,
|
||||
model_type: values.model_type,
|
||||
api_key: {
|
||||
yiyan_ak: values.yiyan_ak,
|
||||
yiyan_sk: values.yiyan_sk,
|
||||
},
|
||||
max_tokens: values.max_tokens as number,
|
||||
};
|
||||
}, [llmFactory]);
|
||||
|
||||
const handleVerify = useCallback(
|
||||
async (params: any) => {
|
||||
const verifyParams = verifyParamsFunc();
|
||||
const res = await onVerify?.({ ...params, ...verifyParams });
|
||||
return (res || { isValid: null, logs: '' }) as VerifyResult;
|
||||
},
|
||||
[verifyParamsFunc, onVerify],
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={<LLMHeader name={llmFactory} />}
|
||||
open={visible || false}
|
||||
onOpenChange={(open) => !open && hideModal?.()}
|
||||
maskClosable={false}
|
||||
// footer={<div className="p-4"></div>}
|
||||
footer={<></>}
|
||||
footerClassName="pb-10"
|
||||
>
|
||||
<DynamicForm.Root
|
||||
key={`${visible}-${llmFactory}`}
|
||||
fields={fields}
|
||||
ref={formRef}
|
||||
onSubmit={(data) => {
|
||||
console.log(data);
|
||||
}}
|
||||
defaultValues={
|
||||
{
|
||||
instance_name: '',
|
||||
model_type: ['chat'],
|
||||
max_tokens: 8192,
|
||||
} as FieldValues
|
||||
}
|
||||
labelClassName="font-normal"
|
||||
>
|
||||
<div>
|
||||
{onVerify && <VerifyButton onVerify={handleVerify} />}
|
||||
<div className="absolute bottom-0 right-0 left-0 flex items-center justify-end w-full gap-2 py-6 px-6">
|
||||
<DynamicForm.CancelButton
|
||||
handleCancel={() => {
|
||||
hideModal?.();
|
||||
}}
|
||||
/>
|
||||
<DynamicForm.SavingButton
|
||||
submitLoading={loading || false}
|
||||
buttonText={tc('ok')}
|
||||
submitFunc={(values: FieldValues) => {
|
||||
handleOk(values);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</DynamicForm.Root>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(YiyanModal);
|
||||
@@ -18,6 +18,7 @@ const INSTANCE_RESERVED_KEYS = new Set([
|
||||
'base_url',
|
||||
'region',
|
||||
'verify',
|
||||
'model_info',
|
||||
]);
|
||||
|
||||
export const MODEL_EXTRA_KEYS = new Set([
|
||||
@@ -91,6 +92,7 @@ export const splitProviderPayload = (payload: FlatPayload): SplitResult => {
|
||||
api_key: collectApiKeyExtras(payload),
|
||||
base_url: (payload.base_url ?? payload.api_base) as string | undefined,
|
||||
region: (payload.region as string | undefined) || 'default',
|
||||
model_info: payload.model_info,
|
||||
};
|
||||
|
||||
const modelExtra = collectModelExtras(payload);
|
||||
|
||||
@@ -8,6 +8,7 @@ const {
|
||||
addProvider,
|
||||
addProviderInstance,
|
||||
verifyProviderConnection,
|
||||
listProviderModels,
|
||||
listProviderInstances,
|
||||
listInstanceModels,
|
||||
showProviderInstance,
|
||||
@@ -45,6 +46,10 @@ const methods = {
|
||||
url: verifyProviderConnection,
|
||||
method: 'post',
|
||||
},
|
||||
listProviderModels: {
|
||||
url: listProviderModels,
|
||||
method: 'get',
|
||||
},
|
||||
listProviderInstances: {
|
||||
url: listProviderInstances,
|
||||
method: 'get',
|
||||
|
||||
@@ -32,6 +32,8 @@ export default {
|
||||
`${restAPIv1}/providers/${llm_factory}/instances`,
|
||||
verifyProviderConnection: ({ provider_name }: { provider_name: string }) =>
|
||||
`${restAPIv1}/providers/${provider_name}/connection`,
|
||||
listProviderModels: ({ provider_name }: { provider_name: string }) =>
|
||||
`${restAPIv1}/providers/${provider_name}/models`,
|
||||
listProviderInstances: ({ provider_name }: { provider_name: string }) =>
|
||||
`${restAPIv1}/providers/${provider_name}/instances`,
|
||||
listInstanceModels: ({
|
||||
|
||||
Reference in New Issue
Block a user