mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-21 10:50:14 +01:00
✋ feat: Stop Sequences for Conversations & Presets (#2536)
* feat: `stop` conversation parameter * feat: Tag primitive * feat: dynamic tags * refactor: update tag styling * feat: add stop sequences to OpenAI settings * fix(Presentation): prevent `SidePanel` re-renders that flicker side panel * refactor: use stop placeholder * feat: type and schema update for `stop` and `TPreset` in generation param related types * refactor: pass conversation to dynamic settings * refactor(OpenAIClient): remove default handling for `modelOptions.stop` * docs: fix Google AI Setup formatting * feat: current_model * docs: WIP update * fix(ChatRoute): prevent default preset override before `hasSetConversation.current` becomes true by including latest conversation state as template * docs: update docs with more info on `stop` * chore: bump config_version * refactor: CURRENT_MODEL handling
This commit is contained in:
parent
4121818124
commit
099aa9dead
29 changed files with 690 additions and 93 deletions
|
|
@ -98,6 +98,7 @@ export default function HeaderOptions() {
|
|||
>
|
||||
<div className="px-4 py-4">
|
||||
<EndpointSettings
|
||||
className="[&::-webkit-scrollbar]:w-2"
|
||||
conversation={conversation}
|
||||
setOption={setOption}
|
||||
isMultiChat={true}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useEffect } from 'react';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { FileSources } from 'librechat-data-provider';
|
||||
import type { ExtendedFile } from '~/common';
|
||||
|
|
@ -49,12 +49,16 @@ export default function Presentation({
|
|||
}, [mutateAsync]);
|
||||
|
||||
const isActive = canDrop && isOver;
|
||||
const resizableLayout = localStorage.getItem('react-resizable-panels:layout');
|
||||
const collapsedPanels = localStorage.getItem('react-resizable-panels:collapsed');
|
||||
|
||||
const defaultLayout = resizableLayout ? JSON.parse(resizableLayout) : undefined;
|
||||
const defaultCollapsed = collapsedPanels ? JSON.parse(collapsedPanels) : undefined;
|
||||
const fullCollapse = localStorage.getItem('fullPanelCollapse') === 'true';
|
||||
const defaultLayout = useMemo(() => {
|
||||
const resizableLayout = localStorage.getItem('react-resizable-panels:layout');
|
||||
return resizableLayout ? JSON.parse(resizableLayout) : undefined;
|
||||
}, []);
|
||||
const defaultCollapsed = useMemo(() => {
|
||||
const collapsedPanels = localStorage.getItem('react-resizable-panels:collapsed');
|
||||
return collapsedPanels ? JSON.parse(collapsedPanels) : undefined;
|
||||
}, []);
|
||||
const fullCollapse = useMemo(() => localStorage.getItem('fullPanelCollapse') === 'true', []);
|
||||
|
||||
const layout = () => (
|
||||
<div className="transition-width relative flex h-full w-full flex-1 flex-col items-stretch overflow-hidden bg-white pt-0 dark:bg-gray-800">
|
||||
|
|
|
|||
|
|
@ -27,9 +27,7 @@ export default function Settings({
|
|||
|
||||
if (OptionComponent) {
|
||||
return (
|
||||
<div
|
||||
className={cn('hide-scrollbar h-[500px] overflow-y-auto md:mb-2 md:h-[350px]', className)}
|
||||
>
|
||||
<div className={cn('h-[500px] overflow-y-auto md:mb-2 md:h-[350px]', className)}>
|
||||
<OptionComponent
|
||||
conversation={conversation}
|
||||
setOption={setOption}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,12 @@
|
|||
import { useMemo } from 'react';
|
||||
import TextareaAutosize from 'react-textarea-autosize';
|
||||
import { ImageDetail, imageDetailNumeric, imageDetailValue } from 'librechat-data-provider';
|
||||
import * as InputNumberPrimitive from 'rc-input-number';
|
||||
import {
|
||||
EModelEndpoint,
|
||||
ImageDetail,
|
||||
imageDetailNumeric,
|
||||
imageDetailValue,
|
||||
} from 'librechat-data-provider';
|
||||
import {
|
||||
Input,
|
||||
Label,
|
||||
|
|
@ -10,12 +17,15 @@ import {
|
|||
SelectDropDown,
|
||||
HoverCardTrigger,
|
||||
} from '~/components/ui';
|
||||
import { cn, defaultTextProps, optionText, removeFocusOutlines } from '~/utils/';
|
||||
import { cn, defaultTextProps, optionText, removeFocusOutlines } from '~/utils';
|
||||
import { DynamicTags } from '~/components/SidePanel/Parameters';
|
||||
import { useLocalize, useDebouncedInput } from '~/hooks';
|
||||
import type { TModelSelectProps } from '~/common';
|
||||
import OptionHover from './OptionHover';
|
||||
import { ESide } from '~/common';
|
||||
|
||||
type OnInputNumberChange = InputNumberPrimitive.InputNumberProps['onChange'];
|
||||
|
||||
export default function Settings({ conversation, setOption, models, readonly }: TModelSelectProps) {
|
||||
const localize = useLocalize();
|
||||
const {
|
||||
|
|
@ -62,6 +72,12 @@ export default function Settings({ conversation, setOption, models, readonly }:
|
|||
initialValue: presP,
|
||||
});
|
||||
|
||||
const optionEndpoint = useMemo(() => endpointType ?? endpoint, [endpoint, endpointType]);
|
||||
const isOpenAI = useMemo(
|
||||
() => optionEndpoint === EModelEndpoint.openAI || optionEndpoint === EModelEndpoint.azureOpenAI,
|
||||
[optionEndpoint],
|
||||
);
|
||||
|
||||
if (!conversation) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -70,8 +86,6 @@ export default function Settings({ conversation, setOption, models, readonly }:
|
|||
const setResendFiles = setOption('resendFiles');
|
||||
const setImageDetail = setOption('imageDetail');
|
||||
|
||||
const optionEndpoint = endpointType ?? endpoint;
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-5 gap-6">
|
||||
<div className="col-span-5 flex flex-col items-center justify-start gap-6 sm:col-span-3">
|
||||
|
|
@ -120,6 +134,22 @@ export default function Settings({ conversation, setOption, models, readonly }:
|
|||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid w-full items-start gap-2">
|
||||
<DynamicTags
|
||||
settingKey="stop"
|
||||
setOption={setOption}
|
||||
label="com_endpoint_stop"
|
||||
labelCode={true}
|
||||
description="com_endpoint_openai_stop"
|
||||
descriptionCode={true}
|
||||
placeholder="com_endpoint_stop_placeholder"
|
||||
placeholderCode={true}
|
||||
descriptionSide="right"
|
||||
maxTags={isOpenAI ? 4 : undefined}
|
||||
conversation={conversation}
|
||||
readonly={readonly}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-5 flex flex-col items-center justify-start gap-6 px-3 sm:col-span-2">
|
||||
<HoverCard openDelay={300}>
|
||||
|
|
@ -133,9 +163,10 @@ export default function Settings({ conversation, setOption, models, readonly }:
|
|||
</Label>
|
||||
<InputNumber
|
||||
id="temp-int"
|
||||
stringMode={false}
|
||||
disabled={readonly}
|
||||
value={temperatureValue as number}
|
||||
onChange={setTemperature}
|
||||
onChange={setTemperature as OnInputNumberChange}
|
||||
max={2}
|
||||
min={0}
|
||||
step={0.01}
|
||||
|
|
|
|||
|
|
@ -20,9 +20,10 @@ function DynamicCheckbox({
|
|||
showDefault = true,
|
||||
labelCode,
|
||||
descriptionCode,
|
||||
conversation,
|
||||
}: DynamicSettingProps) {
|
||||
const localize = useLocalize();
|
||||
const { conversation = { conversationId: null }, preset } = useChatContext();
|
||||
const { preset } = useChatContext();
|
||||
const [inputValue, setInputValue] = useState<boolean>(!!(defaultValue as boolean | undefined));
|
||||
|
||||
const selectedValue = useMemo(() => {
|
||||
|
|
|
|||
|
|
@ -22,9 +22,10 @@ function DynamicDropdown({
|
|||
showDefault = true,
|
||||
labelCode,
|
||||
descriptionCode,
|
||||
conversation,
|
||||
}: DynamicSettingProps) {
|
||||
const localize = useLocalize();
|
||||
const { conversation = { conversationId: null }, preset } = useChatContext();
|
||||
const { preset } = useChatContext();
|
||||
const [inputValue, setInputValue] = useState<string | null>(null);
|
||||
|
||||
const selectedValue = useMemo(() => {
|
||||
|
|
|
|||
|
|
@ -22,9 +22,10 @@ function DynamicInput({
|
|||
labelCode,
|
||||
descriptionCode,
|
||||
placeholderCode,
|
||||
conversation,
|
||||
}: DynamicSettingProps) {
|
||||
const localize = useLocalize();
|
||||
const { conversation = { conversationId: null }, preset } = useChatContext();
|
||||
const { preset } = useChatContext();
|
||||
|
||||
const [setInputValue, inputValue] = useDebouncedInput<string | null>({
|
||||
optionKey: optionType !== OptionTypes.Custom ? settingKey : undefined,
|
||||
|
|
|
|||
|
|
@ -23,9 +23,10 @@ function DynamicSlider({
|
|||
includeInput = true,
|
||||
labelCode,
|
||||
descriptionCode,
|
||||
conversation,
|
||||
}: DynamicSettingProps) {
|
||||
const localize = useLocalize();
|
||||
const { conversation = { conversationId: null }, preset } = useChatContext();
|
||||
const { preset } = useChatContext();
|
||||
const isEnum = useMemo(() => !range && options && options.length > 0, [options, range]);
|
||||
|
||||
const [setInputValue, inputValue] = useDebouncedInput<string | number>({
|
||||
|
|
|
|||
|
|
@ -19,9 +19,10 @@ function DynamicSwitch({
|
|||
showDefault = true,
|
||||
labelCode,
|
||||
descriptionCode,
|
||||
conversation,
|
||||
}: DynamicSettingProps) {
|
||||
const localize = useLocalize();
|
||||
const { conversation = { conversationId: null }, preset } = useChatContext();
|
||||
const { preset } = useChatContext();
|
||||
const [inputValue, setInputValue] = useState<boolean>(!!(defaultValue as boolean | undefined));
|
||||
useParameterEffects({
|
||||
preset,
|
||||
|
|
|
|||
193
client/src/components/SidePanel/Parameters/DynamicTags.tsx
Normal file
193
client/src/components/SidePanel/Parameters/DynamicTags.tsx
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
// client/src/components/SidePanel/Parameters/DynamicTags.tsx
|
||||
import { useState, useMemo, useCallback, useRef } from 'react';
|
||||
import { OptionTypes } from 'librechat-data-provider';
|
||||
import type { DynamicSettingProps } from 'librechat-data-provider';
|
||||
import { Label, Input, HoverCard, HoverCardTrigger, Tag } from '~/components/ui';
|
||||
import { useChatContext, useToastContext } from '~/Providers';
|
||||
import { useLocalize, useParameterEffects } from '~/hooks';
|
||||
import { cn, defaultTextProps } from '~/utils';
|
||||
import OptionHover from './OptionHover';
|
||||
import { ESide } from '~/common';
|
||||
|
||||
function DynamicTags({
|
||||
label,
|
||||
settingKey,
|
||||
defaultValue = [],
|
||||
description,
|
||||
columnSpan,
|
||||
setOption,
|
||||
optionType,
|
||||
placeholder,
|
||||
readonly = false,
|
||||
showDefault = true,
|
||||
labelCode,
|
||||
descriptionCode,
|
||||
placeholderCode,
|
||||
descriptionSide = ESide.Left,
|
||||
conversation,
|
||||
minTags,
|
||||
maxTags,
|
||||
}: DynamicSettingProps) {
|
||||
const localize = useLocalize();
|
||||
const { preset } = useChatContext();
|
||||
const { showToast } = useToastContext();
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [tagText, setTagText] = useState<string>('');
|
||||
const [tags, setTags] = useState<string[] | undefined>(
|
||||
(defaultValue as string[] | undefined) ?? [],
|
||||
);
|
||||
|
||||
const updateState = useCallback(
|
||||
(update: string[]) => {
|
||||
if (optionType === OptionTypes.Custom) {
|
||||
// TODO: custom logic, add to payload but not to conversation
|
||||
setTags(update);
|
||||
return;
|
||||
}
|
||||
setOption(settingKey)(update);
|
||||
},
|
||||
[optionType, setOption, settingKey],
|
||||
);
|
||||
|
||||
const onTagClick = useCallback(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [inputRef]);
|
||||
|
||||
const currentTags: string[] | undefined = useMemo(() => {
|
||||
if (optionType === OptionTypes.Custom) {
|
||||
// TODO: custom logic, add to payload but not to conversation
|
||||
return tags;
|
||||
}
|
||||
|
||||
if (!conversation?.[settingKey]) {
|
||||
return defaultValue ?? [];
|
||||
}
|
||||
|
||||
return conversation?.[settingKey];
|
||||
}, [conversation, defaultValue, optionType, settingKey, tags]);
|
||||
|
||||
const onTagRemove = useCallback(
|
||||
(indexToRemove: number) => {
|
||||
if (!currentTags) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (minTags && currentTags.length <= minTags) {
|
||||
showToast({
|
||||
message: localize('com_ui_min_tags', minTags + ''),
|
||||
status: 'warning',
|
||||
});
|
||||
return;
|
||||
}
|
||||
const update = currentTags.filter((_, index) => index !== indexToRemove);
|
||||
updateState(update);
|
||||
},
|
||||
[localize, minTags, currentTags, showToast, updateState],
|
||||
);
|
||||
|
||||
const onTagAdd = useCallback(() => {
|
||||
if (!tagText) {
|
||||
return;
|
||||
}
|
||||
|
||||
let update = [...(currentTags ?? []), tagText];
|
||||
if (maxTags && update.length > maxTags) {
|
||||
showToast({
|
||||
message: localize('com_ui_max_tags', maxTags + ''),
|
||||
status: 'warning',
|
||||
});
|
||||
update = update.slice(-maxTags);
|
||||
}
|
||||
updateState(update);
|
||||
setTagText('');
|
||||
}, [tagText, currentTags, updateState, maxTags, showToast, localize]);
|
||||
|
||||
useParameterEffects({
|
||||
preset,
|
||||
settingKey,
|
||||
defaultValue: typeof defaultValue === 'undefined' ? [] : defaultValue,
|
||||
inputValue: tags,
|
||||
setInputValue: setTags,
|
||||
preventDelayedUpdate: true,
|
||||
conversation,
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex flex-col items-center justify-start gap-6 ${
|
||||
columnSpan ? `col-span-${columnSpan}` : 'col-span-full'
|
||||
}`}
|
||||
>
|
||||
<HoverCard openDelay={300}>
|
||||
<HoverCardTrigger className="grid w-full items-center gap-2">
|
||||
<div className="flex w-full justify-between">
|
||||
<Label
|
||||
htmlFor={`${settingKey}-dynamic-input`}
|
||||
className="text-left text-sm font-medium"
|
||||
>
|
||||
{labelCode ? localize(label ?? '') || label : label ?? settingKey}{' '}
|
||||
{showDefault && (
|
||||
<small className="opacity-40">
|
||||
(
|
||||
{typeof defaultValue === 'undefined' || !(defaultValue as string)?.length
|
||||
? localize('com_endpoint_default_blank')
|
||||
: `${localize('com_endpoint_default')}: ${defaultValue}`}
|
||||
)
|
||||
</small>
|
||||
)}
|
||||
</Label>
|
||||
</div>
|
||||
<div>
|
||||
<div className="bg-muted mb-2 flex flex-wrap gap-1 break-all rounded-lg">
|
||||
{currentTags?.map((tag: string, index: number) => (
|
||||
<Tag
|
||||
key={`${tag}-${index}`}
|
||||
label={tag}
|
||||
onClick={onTagClick}
|
||||
onRemove={() => {
|
||||
onTagRemove(index);
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
<Input
|
||||
ref={inputRef}
|
||||
id={`${settingKey}-dynamic-input`}
|
||||
disabled={readonly}
|
||||
value={tagText}
|
||||
onKeyDown={(e) => {
|
||||
if (!currentTags) {
|
||||
return;
|
||||
}
|
||||
if (e.key === 'Backspace' && !tagText) {
|
||||
onTagRemove(currentTags.length - 1);
|
||||
}
|
||||
if (e.key === 'Enter') {
|
||||
onTagAdd();
|
||||
}
|
||||
}}
|
||||
onChange={(e) => setTagText(e.target.value)}
|
||||
placeholder={
|
||||
placeholderCode ? localize(placeholder ?? '') || placeholder : placeholder
|
||||
}
|
||||
className={cn(defaultTextProps, 'flex h-10 max-h-10 px-3 py-2')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</HoverCardTrigger>
|
||||
{description && (
|
||||
<OptionHover
|
||||
description={descriptionCode ? localize(description) || description : description}
|
||||
side={descriptionSide as ESide}
|
||||
/>
|
||||
)}
|
||||
</HoverCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DynamicTags;
|
||||
|
|
@ -22,9 +22,10 @@ function DynamicTextarea({
|
|||
labelCode,
|
||||
descriptionCode,
|
||||
placeholderCode,
|
||||
conversation,
|
||||
}: DynamicSettingProps) {
|
||||
const localize = useLocalize();
|
||||
const { conversation = { conversationId: null }, preset } = useChatContext();
|
||||
const { preset } = useChatContext();
|
||||
|
||||
const [setInputValue, inputValue] = useDebouncedInput<string | null>({
|
||||
optionKey: optionType !== OptionTypes.Custom ? settingKey : undefined,
|
||||
|
|
|
|||
|
|
@ -5,12 +5,16 @@ import type {
|
|||
SettingsConfiguration,
|
||||
} from 'librechat-data-provider';
|
||||
import { useSetIndexOptions } from '~/hooks';
|
||||
import DynamicDropdown from './DynamicDropdown';
|
||||
import DynamicCheckbox from './DynamicCheckbox';
|
||||
import DynamicTextarea from './DynamicTextarea';
|
||||
import DynamicSlider from './DynamicSlider';
|
||||
import DynamicSwitch from './DynamicSwitch';
|
||||
import DynamicInput from './DynamicInput';
|
||||
import { useChatContext } from '~/Providers';
|
||||
import {
|
||||
DynamicDropdown,
|
||||
DynamicCheckbox,
|
||||
DynamicTextarea,
|
||||
DynamicSlider,
|
||||
DynamicSwitch,
|
||||
DynamicInput,
|
||||
DynamicTags,
|
||||
} from './';
|
||||
|
||||
const settingsConfiguration: SettingsConfiguration = [
|
||||
{
|
||||
|
|
@ -129,6 +133,22 @@ const settingsConfiguration: SettingsConfiguration = [
|
|||
showDefault: false,
|
||||
columnSpan: 2,
|
||||
},
|
||||
{
|
||||
key: 'stop',
|
||||
label: 'com_endpoint_stop',
|
||||
labelCode: true,
|
||||
description: 'com_endpoint_openai_stop',
|
||||
descriptionCode: true,
|
||||
placeholder: 'com_endpoint_stop_placeholder',
|
||||
placeholderCode: true,
|
||||
type: 'array',
|
||||
default: [],
|
||||
component: 'tags',
|
||||
optionType: 'conversation',
|
||||
columnSpan: 4,
|
||||
minTags: 1,
|
||||
maxTags: 4,
|
||||
},
|
||||
];
|
||||
|
||||
const componentMapping: Record<ComponentTypes, React.ComponentType<DynamicSettingProps>> = {
|
||||
|
|
@ -138,9 +158,11 @@ const componentMapping: Record<ComponentTypes, React.ComponentType<DynamicSettin
|
|||
[ComponentTypes.Textarea]: DynamicTextarea,
|
||||
[ComponentTypes.Input]: DynamicInput,
|
||||
[ComponentTypes.Checkbox]: DynamicCheckbox,
|
||||
[ComponentTypes.Tags]: DynamicTags,
|
||||
};
|
||||
|
||||
export default function Parameters() {
|
||||
const { conversation } = useChatContext();
|
||||
const { setOption } = useSetIndexOptions();
|
||||
|
||||
const temperature = settingsConfiguration.find(
|
||||
|
|
@ -173,6 +195,10 @@ export default function Parameters() {
|
|||
const Input = componentMapping[chatGptLabel.component];
|
||||
const { key: inputKey, default: inputDefault, ...inputSettings } = chatGptLabel;
|
||||
|
||||
const stop = settingsConfiguration.find((setting) => setting.key === 'stop') as SettingDefinition;
|
||||
const Tags = componentMapping[stop.component];
|
||||
const { key: stopKey, default: stopDefault, ...stopSettings } = stop;
|
||||
|
||||
return (
|
||||
<div className="h-auto max-w-full overflow-x-hidden p-3">
|
||||
<div className="grid grid-cols-4 gap-6">
|
||||
|
|
@ -184,30 +210,42 @@ export default function Parameters() {
|
|||
defaultValue={inputDefault}
|
||||
{...inputSettings}
|
||||
setOption={setOption}
|
||||
conversation={conversation}
|
||||
/>
|
||||
<Textarea
|
||||
settingKey={textareaKey}
|
||||
defaultValue={textareaDefault}
|
||||
{...textareaSettings}
|
||||
setOption={setOption}
|
||||
conversation={conversation}
|
||||
/>
|
||||
<TempComponent
|
||||
settingKey={temp}
|
||||
defaultValue={tempDefault}
|
||||
{...tempSettings}
|
||||
setOption={setOption}
|
||||
conversation={conversation}
|
||||
/>
|
||||
<Switch
|
||||
settingKey={switchKey}
|
||||
defaultValue={switchDefault}
|
||||
{...switchSettings}
|
||||
setOption={setOption}
|
||||
conversation={conversation}
|
||||
/>
|
||||
<DetailComponent
|
||||
settingKey={detail}
|
||||
defaultValue={detailDefault}
|
||||
{...detailSettings}
|
||||
setOption={setOption}
|
||||
conversation={conversation}
|
||||
/>
|
||||
<Tags
|
||||
settingKey={stopKey}
|
||||
defaultValue={stopDefault}
|
||||
{...stopSettings}
|
||||
setOption={setOption}
|
||||
conversation={conversation}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
7
client/src/components/SidePanel/Parameters/index.ts
Normal file
7
client/src/components/SidePanel/Parameters/index.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export { default as DynamicDropdown } from './DynamicDropdown';
|
||||
export { default as DynamicCheckbox } from './DynamicCheckbox';
|
||||
export { default as DynamicTextarea } from './DynamicTextarea';
|
||||
export { default as DynamicSlider } from './DynamicSlider';
|
||||
export { default as DynamicSwitch } from './DynamicSwitch';
|
||||
export { default as DynamicInput } from './DynamicInput';
|
||||
export { default as DynamicTags } from './DynamicTags';
|
||||
43
client/src/components/ui/Tag.tsx
Normal file
43
client/src/components/ui/Tag.tsx
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import * as React from 'react';
|
||||
import { X } from 'lucide-react';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
type TagProps = React.ComponentPropsWithoutRef<'div'> & {
|
||||
label: string;
|
||||
labelClassName?: string;
|
||||
CancelButton?: React.ReactNode;
|
||||
onRemove: (e: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
};
|
||||
|
||||
const TagPrimitiveRoot = React.forwardRef<HTMLDivElement, TagProps>(
|
||||
({ CancelButton, label, onRemove, className = '', labelClassName = '', ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
{...props}
|
||||
className={cn(
|
||||
'flex max-h-8 items-center overflow-y-hidden rounded rounded-3xl border-2 border-green-600 bg-green-600/20 text-sm text-xs text-white',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className={cn('ml-1 whitespace-pre-wrap px-2 py-1', labelClassName)}>{label}</div>
|
||||
{CancelButton ? (
|
||||
CancelButton
|
||||
) : (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onRemove(e);
|
||||
}}
|
||||
className="rounded-full bg-green-600/50"
|
||||
aria-label={`Remove ${label}`}
|
||||
>
|
||||
<X className="m-[1.5px] p-1" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
);
|
||||
|
||||
TagPrimitiveRoot.displayName = 'Tag';
|
||||
|
||||
export const Tag = React.memo(TagPrimitiveRoot);
|
||||
|
|
@ -16,6 +16,7 @@ export * from './Separator';
|
|||
export * from './Switch';
|
||||
export * from './Table';
|
||||
export * from './Tabs';
|
||||
export * from './Tag';
|
||||
export * from './Templates';
|
||||
export * from './Textarea';
|
||||
export * from './TextareaAutosize';
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue