feat: stop word model setting (#4113)

* feat: stop word model setting

* chore: update test tag input

* chore: handle UI when no stop word

* chore: fix types of value tag input
This commit is contained in:
Faisal Amir 2024-11-25 21:17:16 +07:00 committed by GitHub
parent a5acaf0556
commit 314cb03693
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 208 additions and 38 deletions

View File

@ -12,7 +12,7 @@ export type SettingComponentProps = {
export type ConfigType = 'runtime' | 'setting'
export type ControllerType = 'slider' | 'checkbox' | 'input'
export type ControllerType = 'slider' | 'checkbox' | 'input' | 'tag'
export type InputType = 'password' | 'text' | 'email' | 'number' | 'tel' | 'url'
@ -22,7 +22,7 @@ export type InputAction = InputActionsTuple[number]
export type InputComponentProps = {
placeholder: string
value: string
value: string | string[]
type?: InputType
textAlign?: 'left' | 'right'
inputActions?: InputAction[]

View File

@ -4,7 +4,10 @@ import SettingComponentBuilder from '@/containers/ModelSetting/SettingComponent'
type Props = {
componentData: SettingComponentProps[]
onValueChanged: (key: string, value: string | number | boolean) => void
onValueChanged: (
key: string,
value: string | number | boolean | string[]
) => void
disabled?: boolean
}

View File

@ -19,28 +19,30 @@ const ModelConfigInput = ({
description,
placeholder,
onValueChanged,
}: Props) => (
<div className="flex flex-col">
<div className="mb-2 flex items-center gap-x-2">
<p className="font-medium">{title}</p>
<Tooltip
trigger={
<InfoIcon
size={16}
className="flex-shrink-0 text-[hsla(var(--text-secondary))]"
/>
}
content={description}
}: Props) => {
return (
<div className="flex flex-col">
<div className="mb-2 flex items-center gap-x-2">
<p className="font-medium">{title}</p>
<Tooltip
trigger={
<InfoIcon
size={16}
className="flex-shrink-0 text-[hsla(var(--text-secondary))]"
/>
}
content={description}
/>
</div>
<TextArea
placeholder={placeholder}
onChange={(e) => onValueChanged?.(e.target.value)}
autoResize
value={value}
disabled={disabled}
/>
</div>
<TextArea
placeholder={placeholder}
onChange={(e) => onValueChanged?.(e.target.value)}
autoResize
value={value}
disabled={disabled}
/>
</div>
)
)
}
export default ModelConfigInput

View File

@ -8,11 +8,15 @@ import {
import Checkbox from '@/containers/Checkbox'
import ModelConfigInput from '@/containers/ModelConfigInput'
import SliderRightPanel from '@/containers/SliderRightPanel'
import TagInput from '@/containers/TagInput'
type Props = {
componentProps: SettingComponentProps[]
disabled?: boolean
onValueUpdated: (key: string, value: string | number | boolean) => void
onValueUpdated: (
key: string,
value: string | number | boolean | string[]
) => void
}
const SettingComponent: React.FC<Props> = ({
@ -53,7 +57,24 @@ const SettingComponent: React.FC<Props> = ({
name={data.key}
description={data.description}
placeholder={placeholder}
value={textValue}
value={textValue as string}
onValueChanged={(value) => onValueUpdated(data.key, value)}
/>
)
}
case 'tag': {
const { placeholder, value: textValue } =
data.controllerProps as InputComponentProps
return (
<TagInput
title={data.title}
disabled={disabled}
key={data.key}
name={data.key}
description={data.description}
placeholder={placeholder}
value={textValue as string[]}
onValueChanged={(value) => onValueUpdated(data.key, value)}
/>
)

View File

@ -6,7 +6,10 @@ import SettingComponentBuilder from './SettingComponent'
type Props = {
componentProps: SettingComponentProps[]
onValueChanged: (key: string, value: string | number | boolean) => void
onValueChanged: (
key: string,
value: string | number | boolean | string[]
) => void
disabled?: boolean
}

View File

@ -0,0 +1,50 @@
import React from 'react'
import { render, fireEvent } from '@testing-library/react'
import TagInput from './index' // Adjust the import path as necessary
import '@testing-library/jest-dom'
describe('TagInput Component', () => {
let props: any
beforeEach(() => {
props = {
title: 'Tags',
name: 'tag-input',
description: 'Add your tags',
placeholder: 'Enter a tag',
value: ['tag1', 'tag2'],
onValueChanged: jest.fn(),
}
})
it('renders correctly', () => {
const { getByText, getByPlaceholderText } = render(<TagInput {...props} />)
expect(getByText('Tags')).toBeInTheDocument()
expect(getByText('tag1')).toBeInTheDocument()
expect(getByText('tag2')).toBeInTheDocument()
expect(getByPlaceholderText('Enter a tag')).toBeInTheDocument()
})
it('calls onValueChanged when a new tag is added', () => {
const { getByPlaceholderText } = render(<TagInput {...props} />)
const input = getByPlaceholderText('Enter a tag')
fireEvent.change(input, { target: { value: 'tag3' } })
fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' })
expect(props.onValueChanged).toHaveBeenCalledWith(
expect.arrayContaining(['tag1', 'tag2', 'tag3'])
)
})
it('calls onValueChanged when a tag is removed', () => {
const { getAllByRole } = render(<TagInput {...props} />)
const removeButton = getAllByRole('button')[0] // Click on the first remove button
fireEvent.click(removeButton)
expect(props.onValueChanged).toHaveBeenCalledWith(
expect.arrayContaining(['tag2'])
)
})
})

View File

@ -0,0 +1,85 @@
import { useState } from 'react'
import { Badge, Input, Tooltip } from '@janhq/joi'
import { InfoIcon, XIcon } from 'lucide-react'
type Props = {
title: string
disabled?: boolean
name: string
description: string
placeholder: string
value: string[]
onValueChanged?: (e: string | number | boolean | string[]) => void
}
const TagInput = ({
title,
disabled = false,
value,
description,
placeholder,
onValueChanged,
}: Props) => {
const [pendingDataPoint, setPendingDataPoint] = useState('')
const addPendingDataPoint = () => {
if (pendingDataPoint) {
const newDataPoints = new Set([...value, pendingDataPoint])
onValueChanged && onValueChanged(Array.from(newDataPoints))
setPendingDataPoint('')
}
}
return (
<div className="flex flex-col">
<div className="mb-2 flex items-center gap-x-2">
<p className="font-medium">{title}</p>
<Tooltip
trigger={
<InfoIcon
size={16}
className="flex-shrink-0 text-[hsla(var(--text-secondary))]"
/>
}
content={description}
/>
</div>
<Input
value={pendingDataPoint}
disabled={disabled}
onChange={(e) => setPendingDataPoint(e.target.value)}
placeholder={placeholder}
className="w-full"
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === 'Tab') {
e.preventDefault()
addPendingDataPoint()
}
}}
/>
{value.length > 0 && (
<div className="mt-2 flex min-h-[2.5rem] flex-wrap items-center gap-2 overflow-y-auto">
{value.map((item, idx) => (
<Badge key={idx} theme="secondary">
{item}
<button
type="button"
className="ml-1.5 w-3 bg-transparent"
onClick={() => {
onValueChanged &&
onValueChanged(value.filter((i) => i !== item))
}}
>
<XIcon className="w-3" />
</button>
</Badge>
))}
</div>
)}
</div>
)
}
export default TagInput

View File

@ -86,7 +86,7 @@ const LocalServerRightPanel = () => {
}, [currentModelSettingParams, setLocalAPIserverModelParams])
const onValueChanged = useCallback(
(key: string, value: string | number | boolean) => {
(key: string, value: string | number | boolean | string[]) => {
setCurrentModelSettingParams((prevParams) => ({
...prevParams,
[key]: value,

View File

@ -44,7 +44,7 @@ const ExtensionSetting = () => {
const onValueChanged = async (
key: string,
value: string | number | boolean
value: string | number | boolean | string[]
) => {
// find the key in settings state, update it and set the state back
const newSettings = settings.map((setting) => {

View File

@ -51,7 +51,7 @@ const SettingDetailTextInputItem = ({
}, [])
const copy = useCallback(() => {
navigator.clipboard.writeText(value)
navigator.clipboard.writeText(value as string)
if (value.length > 0) {
setCopied(true)
}

View File

@ -5,7 +5,10 @@ import SettingDetailToggleItem from './SettingDetailToggleItem'
type Props = {
componentProps: SettingComponentProps[]
onValueUpdated: (key: string, value: string | number | boolean) => void
onValueUpdated: (
key: string,
value: string | number | boolean | string[]
) => void
}
const SettingDetailItem = ({ componentProps, onValueUpdated }: Props) => {

View File

@ -24,7 +24,7 @@ const AssistantSetting: React.FC<Props> = ({ componentData }) => {
const setEngineParamsUpdate = useSetAtom(engineParamsUpdateAtom)
const onValueChanged = useCallback(
(key: string, value: string | number | boolean) => {
(key: string, value: string | number | boolean | string[]) => {
if (!activeThread) return
const shouldReloadModel =
componentData.find((x) => x.key === key)?.requireModelReload ?? false

View File

@ -26,7 +26,7 @@ const PromptTemplateSetting: React.FC<Props> = ({ componentData }) => {
const setEngineParamsUpdate = useSetAtom(engineParamsUpdateAtom)
const onValueChanged = useCallback(
(key: string, value: string | number | boolean) => {
(key: string, value: string | number | boolean | string[]) => {
if (!activeThread) return
setEngineParamsUpdate(true)

View File

@ -173,7 +173,7 @@ const ThreadRightPanel = () => {
}, 300)
const onValueChanged = useCallback(
(key: string, value: string | number | boolean) => {
(key: string, value: string | number | boolean | string[]) => {
if (!activeThread) {
return
}

View File

@ -60,11 +60,14 @@ export const getConfigurationsData = (
componentSetting.controllerProps.placeholder = placeholder
} else if ('checkbox' === componentSetting.controllerType) {
const checked = keySetting as boolean
if ('value' in componentSetting.controllerProps)
componentSetting.controllerProps.value = checked
} else if ('tag' === componentSetting.controllerType) {
if ('value' in componentSetting.controllerProps)
componentSetting.controllerProps.value = keySetting as string
}
componentData.push(componentSetting)
})
return componentData
}

View File

@ -17,10 +17,10 @@ export const presetConfiguration: Record<string, SettingComponentProps> = {
key: 'stop',
title: 'Stop',
description: `Defines specific tokens or phrases that signal the model to stop producing further output, allowing you to control the length and coherence of the output.`,
controllerType: 'input',
controllerType: 'tag',
controllerProps: {
placeholder: 'Stop',
value: '',
placeholder: 'Enter stop words',
value: [''],
},
requireModelReload: false,
configType: 'runtime',