mirror of
https://github.com/langgenius/dify.git
synced 2026-05-02 00:18:03 +08:00
Merge branch 'feat/queue-based-graph-engine' into feat/rag-2
# Conflicts: # api/core/memory/token_buffer_memory.py # api/core/rag/extractor/notion_extractor.py # api/core/repositories/sqlalchemy_workflow_node_execution_repository.py # api/core/variables/variables.py # api/core/workflow/graph/graph.py # api/core/workflow/graph_engine/entities/event.py # api/services/dataset_service.py # web/app/components/app-sidebar/index.tsx # web/app/components/base/tag-management/selector.tsx # web/app/components/base/toast/index.tsx # web/app/components/datasets/create/website/index.tsx # web/app/components/datasets/create/website/jina-reader/base/options-wrap.tsx # web/app/components/workflow/header/version-history-button.tsx # web/app/components/workflow/hooks/use-inspect-vars-crud-common.ts # web/app/components/workflow/hooks/use-workflow-interactions.ts # web/app/components/workflow/panel/version-history-panel/index.tsx # web/service/base.ts
This commit is contained in:
@ -45,8 +45,8 @@ const ICON_MAP = {
|
||||
</div>,
|
||||
dataset: <AppIcon innerIcon={DatasetSvg} className='!border-[0.5px] !border-indigo-100 !bg-indigo-25' />,
|
||||
webapp: <div className='rounded-lg border-[0.5px] border-divider-subtle bg-util-colors-blue-brand-blue-brand-500 p-1 shadow-md'>
|
||||
<WindowCursor className='h-4 w-4 text-text-primary-on-surface' />
|
||||
</div>,
|
||||
<WindowCursor className='h-4 w-4 text-text-primary-on-surface' />
|
||||
</div>,
|
||||
notion: <AppIcon innerIcon={NotionSvg} className='!border-[0.5px] !border-indigo-100 !bg-white' />,
|
||||
}
|
||||
|
||||
|
||||
@ -121,7 +121,7 @@ describe('Sidebar Animation Issues Reproduction', () => {
|
||||
}))
|
||||
})
|
||||
|
||||
describe('Issue #1: Toggle Button Position Movement - FIXED', () => {
|
||||
describe('Issue #1: Toggle Button Position Movement - FIXED', () => {
|
||||
it('should verify consistent padding prevents button position shift', () => {
|
||||
let expanded = false
|
||||
const handleToggle = () => {
|
||||
|
||||
@ -84,7 +84,7 @@ const Annotation: FC<Props> = (props) => {
|
||||
setList(data as AnnotationItem[])
|
||||
setTotal(total)
|
||||
}
|
||||
finally {
|
||||
finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,19 +16,26 @@ const VarHighlight: FC<IVarHighlightProps> = ({
|
||||
return (
|
||||
<div
|
||||
key={name}
|
||||
className={`${s.item} ${className} mb-2 flex h-5 items-center justify-center rounded-md px-1 text-xs font-medium text-primary-600`}
|
||||
className={`${s.item} ${className} mb-2 inline-flex h-5 items-center justify-center rounded-md px-1 text-xs font-medium text-primary-600`}
|
||||
>
|
||||
<span className='opacity-60'>{'{{'}</span>
|
||||
<span>{name}</span>
|
||||
<span className='opacity-60'>{'}}'}</span>
|
||||
<span className='opacity-60'>{'{{'}</span><span>{name}</span><span className='opacity-60'>{'}}'}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// DEPRECATED: This function is vulnerable to XSS attacks and should not be used
|
||||
// Use the VarHighlight React component instead
|
||||
export const varHighlightHTML = ({ name, className = '' }: IVarHighlightProps) => {
|
||||
const escapedName = name
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
|
||||
const html = `<div class="${s.item} ${className} inline-flex mb-2 items-center justify-center px-1 rounded-md h-5 text-xs font-medium text-primary-600">
|
||||
<span class='opacity-60'>{{</span>
|
||||
<span>${name}</span>
|
||||
<span>${escapedName}</span>
|
||||
<span class='opacity-60'>}}</span>
|
||||
</div>`
|
||||
return html
|
||||
|
||||
@ -52,13 +52,13 @@ const TypeSelector: FC<Props> = ({
|
||||
>
|
||||
<div className='flex items-center'>
|
||||
<InputVarTypeIcon type={selectedItem?.value as InputVarType} className='size-4 shrink-0 text-text-secondary' />
|
||||
<span
|
||||
className={`
|
||||
<span
|
||||
className={`
|
||||
ml-1.5 ${!selectedItem?.name && 'text-components-input-text-placeholder'}
|
||||
`}
|
||||
>
|
||||
{selectedItem?.name}
|
||||
</span>
|
||||
>
|
||||
{selectedItem?.name}
|
||||
</span>
|
||||
</div>
|
||||
<div className='flex items-center space-x-1'>
|
||||
<Badge uppercase={false}>{inputVarTypeToVarType(selectedItem?.value as InputVarType)}</Badge>
|
||||
|
||||
@ -18,7 +18,7 @@ import s from './style.module.css'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { generateBasicAppFistTimeRule, generateRule } from '@/service/debug'
|
||||
import { generateBasicAppFirstTimeRule, generateRule } from '@/service/debug'
|
||||
import type { CompletionParams, Model } from '@/types/app'
|
||||
import type { AppType } from '@/types/app'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
@ -226,7 +226,7 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
|
||||
let apiRes: GenRes
|
||||
let hasError = false
|
||||
if (isBasicMode || !currentPrompt) {
|
||||
const { error, ...res } = await generateBasicAppFistTimeRule({
|
||||
const { error, ...res } = await generateBasicAppFirstTimeRule({
|
||||
instruction,
|
||||
model_config: model,
|
||||
no_variable: false,
|
||||
|
||||
@ -175,7 +175,6 @@ const ConfigContent: FC<Props> = ({
|
||||
...datasetConfigs,
|
||||
reranking_enable: enable,
|
||||
})
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentRerankModel, datasetConfigs, onChange])
|
||||
|
||||
return (
|
||||
|
||||
@ -57,10 +57,10 @@ const ChatUserInput = ({
|
||||
>
|
||||
<div>
|
||||
{type !== 'checkbox' && (
|
||||
<div className='system-sm-semibold mb-1 flex h-6 items-center gap-1 text-text-secondary'>
|
||||
<div className='truncate'>{name || key}</div>
|
||||
{!required && <span className='system-xs-regular text-text-tertiary'>{t('workflow.panel.optional')}</span>}
|
||||
</div>
|
||||
<div className='system-sm-semibold mb-1 flex h-6 items-center gap-1 text-text-secondary'>
|
||||
<div className='truncate'>{name || key}</div>
|
||||
{!required && <span className='system-xs-regular text-text-tertiary'>{t('workflow.panel.optional')}</span>}
|
||||
</div>
|
||||
)}
|
||||
<div className='grow'>
|
||||
{type === 'string' && (
|
||||
|
||||
@ -112,72 +112,72 @@ const getFormattedChatList = (messages: ChatMessage[], conversationId: string, t
|
||||
const newChatList: IChatItem[] = []
|
||||
try {
|
||||
messages.forEach((item: ChatMessage) => {
|
||||
const questionFiles = item.message_files?.filter((file: any) => file.belongs_to === 'user') || []
|
||||
newChatList.push({
|
||||
id: `question-${item.id}`,
|
||||
content: item.inputs.query || item.inputs.default_input || item.query, // text generation: item.inputs.query; chat: item.query
|
||||
isAnswer: false,
|
||||
message_files: getProcessedFilesFromResponse(questionFiles.map((item: any) => ({ ...item, related_id: item.id }))),
|
||||
parentMessageId: item.parent_message_id || undefined,
|
||||
})
|
||||
const questionFiles = item.message_files?.filter((file: any) => file.belongs_to === 'user') || []
|
||||
newChatList.push({
|
||||
id: `question-${item.id}`,
|
||||
content: item.inputs.query || item.inputs.default_input || item.query, // text generation: item.inputs.query; chat: item.query
|
||||
isAnswer: false,
|
||||
message_files: getProcessedFilesFromResponse(questionFiles.map((item: any) => ({ ...item, related_id: item.id }))),
|
||||
parentMessageId: item.parent_message_id || undefined,
|
||||
})
|
||||
|
||||
const answerFiles = item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || []
|
||||
newChatList.push({
|
||||
id: item.id,
|
||||
content: item.answer,
|
||||
agent_thoughts: addFileInfos(item.agent_thoughts ? sortAgentSorts(item.agent_thoughts) : item.agent_thoughts, item.message_files),
|
||||
feedback: item.feedbacks.find(item => item.from_source === 'user'), // user feedback
|
||||
adminFeedback: item.feedbacks.find(item => item.from_source === 'admin'), // admin feedback
|
||||
feedbackDisabled: false,
|
||||
isAnswer: true,
|
||||
message_files: getProcessedFilesFromResponse(answerFiles.map((item: any) => ({ ...item, related_id: item.id }))),
|
||||
log: [
|
||||
...item.message,
|
||||
...(item.message[item.message.length - 1]?.role !== 'assistant'
|
||||
? [
|
||||
{
|
||||
role: 'assistant',
|
||||
text: item.answer,
|
||||
files: item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [],
|
||||
},
|
||||
]
|
||||
: []),
|
||||
] as IChatItem['log'],
|
||||
workflow_run_id: item.workflow_run_id,
|
||||
conversationId,
|
||||
input: {
|
||||
inputs: item.inputs,
|
||||
query: item.query,
|
||||
},
|
||||
more: {
|
||||
time: dayjs.unix(item.created_at).tz(timezone).format(format),
|
||||
tokens: item.answer_tokens + item.message_tokens,
|
||||
latency: item.provider_response_latency.toFixed(2),
|
||||
},
|
||||
citation: item.metadata?.retriever_resources,
|
||||
annotation: (() => {
|
||||
if (item.annotation_hit_history) {
|
||||
return {
|
||||
id: item.annotation_hit_history.annotation_id,
|
||||
authorName: item.annotation_hit_history.annotation_create_account?.name || 'N/A',
|
||||
created_at: item.annotation_hit_history.created_at,
|
||||
const answerFiles = item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || []
|
||||
newChatList.push({
|
||||
id: item.id,
|
||||
content: item.answer,
|
||||
agent_thoughts: addFileInfos(item.agent_thoughts ? sortAgentSorts(item.agent_thoughts) : item.agent_thoughts, item.message_files),
|
||||
feedback: item.feedbacks.find(item => item.from_source === 'user'), // user feedback
|
||||
adminFeedback: item.feedbacks.find(item => item.from_source === 'admin'), // admin feedback
|
||||
feedbackDisabled: false,
|
||||
isAnswer: true,
|
||||
message_files: getProcessedFilesFromResponse(answerFiles.map((item: any) => ({ ...item, related_id: item.id }))),
|
||||
log: [
|
||||
...item.message,
|
||||
...(item.message[item.message.length - 1]?.role !== 'assistant'
|
||||
? [
|
||||
{
|
||||
role: 'assistant',
|
||||
text: item.answer,
|
||||
files: item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [],
|
||||
},
|
||||
]
|
||||
: []),
|
||||
] as IChatItem['log'],
|
||||
workflow_run_id: item.workflow_run_id,
|
||||
conversationId,
|
||||
input: {
|
||||
inputs: item.inputs,
|
||||
query: item.query,
|
||||
},
|
||||
more: {
|
||||
time: dayjs.unix(item.created_at).tz(timezone).format(format),
|
||||
tokens: item.answer_tokens + item.message_tokens,
|
||||
latency: item.provider_response_latency.toFixed(2),
|
||||
},
|
||||
citation: item.metadata?.retriever_resources,
|
||||
annotation: (() => {
|
||||
if (item.annotation_hit_history) {
|
||||
return {
|
||||
id: item.annotation_hit_history.annotation_id,
|
||||
authorName: item.annotation_hit_history.annotation_create_account?.name || 'N/A',
|
||||
created_at: item.annotation_hit_history.created_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (item.annotation) {
|
||||
return {
|
||||
id: item.annotation.id,
|
||||
authorName: item.annotation.account.name,
|
||||
logAnnotation: item.annotation,
|
||||
created_at: 0,
|
||||
if (item.annotation) {
|
||||
return {
|
||||
id: item.annotation.id,
|
||||
authorName: item.annotation.account.name,
|
||||
logAnnotation: item.annotation,
|
||||
created_at: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined
|
||||
})(),
|
||||
parentMessageId: `question-${item.id}`,
|
||||
return undefined
|
||||
})(),
|
||||
parentMessageId: `question-${item.id}`,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
return newChatList
|
||||
}
|
||||
@ -503,7 +503,7 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
|
||||
|
||||
setThreadChatItems(getThreadMessages(tree, newAllChatItems.at(-1)?.id))
|
||||
}
|
||||
catch (error) {
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
setHasMore(false)
|
||||
}
|
||||
@ -522,7 +522,7 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
|
||||
if (outerDiv && outerDiv.scrollHeight > outerDiv.clientHeight) {
|
||||
scrollContainer = outerDiv
|
||||
}
|
||||
else if (scrollableDiv && scrollableDiv.scrollHeight > scrollableDiv.clientHeight) {
|
||||
else if (scrollableDiv && scrollableDiv.scrollHeight > scrollableDiv.clientHeight) {
|
||||
scrollContainer = scrollableDiv
|
||||
}
|
||||
else if (chatContainer && chatContainer.scrollHeight > chatContainer.clientHeight) {
|
||||
|
||||
@ -167,7 +167,7 @@ function AppCard({
|
||||
setAppDetail(res)
|
||||
setShowAccessControl(false)
|
||||
}
|
||||
catch (error) {
|
||||
catch (error) {
|
||||
console.error('Failed to fetch app detail:', error)
|
||||
}
|
||||
}, [appDetail, setAppDetail])
|
||||
|
||||
@ -40,12 +40,12 @@ const OPTION_MAP = {
|
||||
`<script>
|
||||
window.difyChatbotConfig = {
|
||||
token: '${token}'${isTestEnv
|
||||
? `,
|
||||
? `,
|
||||
isDev: true`
|
||||
: ''}${IS_CE_EDITION
|
||||
? `,
|
||||
: ''}${IS_CE_EDITION
|
||||
? `,
|
||||
baseUrl: '${url}${basePath}'`
|
||||
: ''},
|
||||
: ''},
|
||||
inputs: {
|
||||
// You can define the inputs from the Start node here
|
||||
// key is the variable name
|
||||
|
||||
@ -240,7 +240,7 @@ const SettingsModal: FC<ISettingsModalProps> = ({
|
||||
<Link href={docLink('/guides/application-publishing/launch-your-webapp-quickly/README', {
|
||||
'zh-Hans': '/guides/application-publishing/launch-your-webapp-quickly/readme',
|
||||
})}
|
||||
target='_blank' rel='noopener noreferrer' className='text-text-accent'>{t('common.operation.learnMore')}</Link>
|
||||
target='_blank' rel='noopener noreferrer' className='text-text-accent'>{t('common.operation.learnMore')}</Link>
|
||||
</div>
|
||||
</div>
|
||||
{/* form body */}
|
||||
|
||||
@ -43,6 +43,7 @@ const Filter: FC<IFilterProps> = ({ queryParams, setQueryParams }: IFilterProps)
|
||||
{ value: 'succeeded', name: 'Success' },
|
||||
{ value: 'failed', name: 'Fail' },
|
||||
{ value: 'stopped', name: 'Stop' },
|
||||
{ value: 'partial-succeeded', name: 'Partial Success' },
|
||||
]}
|
||||
/>
|
||||
<Chip
|
||||
|
||||
@ -68,7 +68,7 @@ const getKey = (
|
||||
|
||||
const List = () => {
|
||||
const { t } = useTranslation()
|
||||
const { systemFeatures } = useGlobalPublicStore()
|
||||
const { systemFeatures } = useGlobalPublicStore()
|
||||
const router = useRouter()
|
||||
const { isCurrentWorkspaceEditor, isCurrentWorkspaceDatasetOperator } = useAppContext()
|
||||
const showTagManagementModal = useTagStore(s => s.showTagManagementModal)
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
import type { ChangeEvent, FC } from 'react'
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { varHighlightHTML } from '../../app/configuration/base/var-highlight'
|
||||
import VarHighlight from '../../app/configuration/base/var-highlight'
|
||||
import Toast from '../toast'
|
||||
import classNames from '@/utils/classnames'
|
||||
import { checkKeys } from '@/utils/var'
|
||||
@ -66,11 +66,24 @@ const BlockInput: FC<IBlockInputProps> = ({
|
||||
'block-input--editing': isEditing,
|
||||
})
|
||||
|
||||
const coloredContent = (currentValue || '')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(regex, varHighlightHTML({ name: '$1' })) // `<span class="${highLightClassName}">{{$1}}</span>`
|
||||
.replace(/\n/g, '<br />')
|
||||
const renderSafeContent = (value: string) => {
|
||||
const parts = value.split(/(\{\{[^}]+\}\}|\n)/g)
|
||||
return parts.map((part, index) => {
|
||||
const variableMatch = part.match(/^\{\{([^}]+)\}\}$/)
|
||||
if (variableMatch) {
|
||||
return (
|
||||
<VarHighlight
|
||||
key={`var-${index}`}
|
||||
name={variableMatch[1]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
if (part === '\n')
|
||||
return <br key={`br-${index}`} />
|
||||
|
||||
return <span key={`text-${index}`}>{part}</span>
|
||||
})
|
||||
}
|
||||
|
||||
// Not use useCallback. That will cause out callback get old data.
|
||||
const handleSubmit = (value: string) => {
|
||||
@ -96,11 +109,11 @@ const BlockInput: FC<IBlockInputProps> = ({
|
||||
|
||||
// Prevent rerendering caused cursor to jump to the start of the contentEditable element
|
||||
const TextAreaContentView = () => {
|
||||
return <div
|
||||
className={classNames(style, className)}
|
||||
dangerouslySetInnerHTML={{ __html: coloredContent }}
|
||||
suppressContentEditableWarning={true}
|
||||
/>
|
||||
return (
|
||||
<div className={classNames(style, className)}>
|
||||
{renderSafeContent(currentValue || '')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const placeholder = ''
|
||||
|
||||
@ -508,7 +508,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
||||
}, [mutateAppConversationData, handleConversationIdInfoChange])
|
||||
|
||||
const handleFeedback = useCallback(async (messageId: string, feedback: Feedback) => {
|
||||
await updateFeedback({ url: `/messages/${messageId}/feedbacks`, body: { rating: feedback.rating } }, isInstalledApp, appId)
|
||||
await updateFeedback({ url: `/messages/${messageId}/feedbacks`, body: { rating: feedback.rating, content: feedback.content } }, isInstalledApp, appId)
|
||||
notify({ type: 'success', message: t('common.api.success') })
|
||||
}, [isInstalledApp, appId, t, notify])
|
||||
|
||||
|
||||
@ -20,6 +20,8 @@ import EditReplyModal from '@/app/components/app/annotation/edit-annotation-moda
|
||||
import Log from '@/app/components/base/chat/chat/log'
|
||||
import ActionButton, { ActionButtonState } from '@/app/components/base/action-button'
|
||||
import NewAudioButton from '@/app/components/base/new-audio-button'
|
||||
import Modal from '@/app/components/base/modal/modal'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type OperationProps = {
|
||||
@ -32,6 +34,7 @@ type OperationProps = {
|
||||
hasWorkflowProcess: boolean
|
||||
noChatInput?: boolean
|
||||
}
|
||||
|
||||
const Operation: FC<OperationProps> = ({
|
||||
item,
|
||||
question,
|
||||
@ -52,6 +55,8 @@ const Operation: FC<OperationProps> = ({
|
||||
onRegenerate,
|
||||
} = useChatContext()
|
||||
const [isShowReplyModal, setIsShowReplyModal] = useState(false)
|
||||
const [isShowFeedbackModal, setIsShowFeedbackModal] = useState(false)
|
||||
const [feedbackContent, setFeedbackContent] = useState('')
|
||||
const {
|
||||
id,
|
||||
isOpeningStatement,
|
||||
@ -70,14 +75,29 @@ const Operation: FC<OperationProps> = ({
|
||||
return messageContent
|
||||
}, [agent_thoughts, messageContent])
|
||||
|
||||
const handleFeedback = async (rating: 'like' | 'dislike' | null) => {
|
||||
const handleFeedback = async (rating: 'like' | 'dislike' | null, content?: string) => {
|
||||
if (!config?.supportFeedback || !onFeedback)
|
||||
return
|
||||
|
||||
await onFeedback?.(id, { rating })
|
||||
await onFeedback?.(id, { rating, content })
|
||||
setLocalFeedback({ rating })
|
||||
}
|
||||
|
||||
const handleThumbsDown = () => {
|
||||
setIsShowFeedbackModal(true)
|
||||
}
|
||||
|
||||
const handleFeedbackSubmit = async () => {
|
||||
await handleFeedback('dislike', feedbackContent)
|
||||
setFeedbackContent('')
|
||||
setIsShowFeedbackModal(false)
|
||||
}
|
||||
|
||||
const handleFeedbackCancel = () => {
|
||||
setFeedbackContent('')
|
||||
setIsShowFeedbackModal(false)
|
||||
}
|
||||
|
||||
const operationWidth = useMemo(() => {
|
||||
let width = 0
|
||||
if (!isOpeningStatement)
|
||||
@ -153,7 +173,7 @@ const Operation: FC<OperationProps> = ({
|
||||
<ActionButton onClick={() => handleFeedback('like')}>
|
||||
<RiThumbUpLine className='h-4 w-4' />
|
||||
</ActionButton>
|
||||
<ActionButton onClick={() => handleFeedback('dislike')}>
|
||||
<ActionButton onClick={handleThumbsDown}>
|
||||
<RiThumbDownLine className='h-4 w-4' />
|
||||
</ActionButton>
|
||||
</>
|
||||
@ -188,6 +208,32 @@ const Operation: FC<OperationProps> = ({
|
||||
createdAt={annotation?.created_at}
|
||||
onRemove={() => onAnnotationRemoved?.(index)}
|
||||
/>
|
||||
{isShowFeedbackModal && (
|
||||
<Modal
|
||||
title={t('common.feedback.title') || 'Provide Feedback'}
|
||||
subTitle={t('common.feedback.subtitle') || 'Please tell us what went wrong with this response'}
|
||||
onClose={handleFeedbackCancel}
|
||||
onConfirm={handleFeedbackSubmit}
|
||||
onCancel={handleFeedbackCancel}
|
||||
confirmButtonText={t('common.operation.submit') || 'Submit'}
|
||||
cancelButtonText={t('common.operation.cancel') || 'Cancel'}
|
||||
>
|
||||
<div className='space-y-3'>
|
||||
<div>
|
||||
<label className='system-sm-semibold mb-2 block text-text-secondary'>
|
||||
{t('common.feedback.content') || 'Feedback Content'}
|
||||
</label>
|
||||
<Textarea
|
||||
value={feedbackContent}
|
||||
onChange={e => setFeedbackContent(e.target.value)}
|
||||
placeholder={t('common.feedback.placeholder') || 'Please describe what went wrong or how we can improve...'}
|
||||
rows={4}
|
||||
className='w-full'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -390,7 +390,7 @@ export const useEmbeddedChatbot = () => {
|
||||
}, [mutateAppConversationData, handleConversationIdInfoChange])
|
||||
|
||||
const handleFeedback = useCallback(async (messageId: string, feedback: Feedback) => {
|
||||
await updateFeedback({ url: `/messages/${messageId}/feedbacks`, body: { rating: feedback.rating } }, isInstalledApp, appId)
|
||||
await updateFeedback({ url: `/messages/${messageId}/feedbacks`, body: { rating: feedback.rating, content: feedback.content } }, isInstalledApp, appId)
|
||||
notify({ type: 'success', message: t('common.api.success') })
|
||||
}, [isInstalledApp, appId, t, notify])
|
||||
|
||||
|
||||
@ -46,12 +46,12 @@ const InputsFormContent = ({ showTip }: Props) => {
|
||||
{visibleInputsForms.map(form => (
|
||||
<div key={form.variable} className='space-y-1'>
|
||||
{form.type !== InputVarType.checkbox && (
|
||||
<div className='flex h-6 items-center gap-1'>
|
||||
<div className='system-md-semibold text-text-secondary'>{form.label}</div>
|
||||
{!form.required && (
|
||||
<div className='system-xs-regular text-text-tertiary'>{t('appDebug.variableTable.optional')}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className='flex h-6 items-center gap-1'>
|
||||
<div className='system-md-semibold text-text-secondary'>{form.label}</div>
|
||||
{!form.required && (
|
||||
<div className='system-xs-regular text-text-tertiary'>{t('appDebug.variableTable.optional')}</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{form.type === InputVarType.textInput && (
|
||||
<Input
|
||||
|
||||
@ -93,4 +93,5 @@ export type Callback = {
|
||||
|
||||
export type Feedback = {
|
||||
rating: 'like' | 'dislike' | null
|
||||
content?: string | null
|
||||
}
|
||||
|
||||
@ -131,7 +131,7 @@ export const formatDateForOutput = (date: Dayjs, includeTime: boolean = false, l
|
||||
// Output format with time
|
||||
return date.format('YYYY-MM-DDTHH:mm:ss.SSSZ')
|
||||
}
|
||||
else {
|
||||
else {
|
||||
// Date-only output format without timezone
|
||||
return date.format('YYYY-MM-DD')
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import {
|
||||
isValidElement,
|
||||
memo,
|
||||
useCallback,
|
||||
useMemo,
|
||||
} from 'react'
|
||||
import { RiExternalLinkLine } from '@remixicon/react'
|
||||
@ -12,6 +13,7 @@ import PureSelect from '@/app/components/base/select/pure'
|
||||
import type { FormSchema } from '@/app/components/base/form/types'
|
||||
import { FormTypeEnum } from '@/app/components/base/form/types'
|
||||
import { useRenderI18nObject } from '@/hooks/use-i18n'
|
||||
import Radio from '@/app/components/base/radio'
|
||||
import RadioE from '@/app/components/base/radio/ui'
|
||||
|
||||
export type BaseFieldProps = {
|
||||
@ -22,6 +24,7 @@ export type BaseFieldProps = {
|
||||
formSchema: FormSchema
|
||||
field: AnyFieldApi
|
||||
disabled?: boolean
|
||||
onChange?: (field: string, value: any) => void
|
||||
}
|
||||
const BaseField = ({
|
||||
fieldClassName,
|
||||
@ -31,6 +34,7 @@ const BaseField = ({
|
||||
formSchema,
|
||||
field,
|
||||
disabled: propsDisabled,
|
||||
onChange,
|
||||
}: BaseFieldProps) => {
|
||||
const renderI18nObject = useRenderI18nObject()
|
||||
const {
|
||||
@ -39,7 +43,6 @@ const BaseField = ({
|
||||
placeholder,
|
||||
options,
|
||||
labelClassName: formLabelClassName,
|
||||
show_on = [],
|
||||
disabled: formSchemaDisabled,
|
||||
} = formSchema
|
||||
const disabled = propsDisabled || formSchemaDisabled
|
||||
@ -89,21 +92,11 @@ const BaseField = ({
|
||||
}) || []
|
||||
}, [options, renderI18nObject, optionValues])
|
||||
const value = useStore(field.form.store, s => s.values[field.name])
|
||||
const values = useStore(field.form.store, (s) => {
|
||||
return show_on.reduce((acc, condition) => {
|
||||
acc[condition.variable] = s.values[condition.variable]
|
||||
return acc
|
||||
}, {} as Record<string, any>)
|
||||
})
|
||||
const show = useMemo(() => {
|
||||
return show_on.every((condition) => {
|
||||
const conditionValue = values[condition.variable]
|
||||
return conditionValue === condition.value
|
||||
})
|
||||
}, [values, show_on])
|
||||
|
||||
if (!show)
|
||||
return null
|
||||
const handleChange = useCallback((value: any) => {
|
||||
field.handleChange(value)
|
||||
onChange?.(field.name, value)
|
||||
}, [field, onChange])
|
||||
|
||||
return (
|
||||
<div className={cn(fieldClassName)}>
|
||||
@ -123,7 +116,9 @@ const BaseField = ({
|
||||
name={field.name}
|
||||
className={cn(inputClassName)}
|
||||
value={value || ''}
|
||||
onChange={e => field.handleChange(e.target.value)}
|
||||
onChange={(e) => {
|
||||
handleChange(e.target.value)
|
||||
}}
|
||||
onBlur={field.handleBlur}
|
||||
disabled={disabled}
|
||||
placeholder={memorizedPlaceholder}
|
||||
@ -138,10 +133,11 @@ const BaseField = ({
|
||||
type='password'
|
||||
className={cn(inputClassName)}
|
||||
value={value || ''}
|
||||
onChange={e => field.handleChange(e.target.value)}
|
||||
onChange={e => handleChange(e.target.value)}
|
||||
onBlur={field.handleBlur}
|
||||
disabled={disabled}
|
||||
placeholder={memorizedPlaceholder}
|
||||
autoComplete={'new-password'}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@ -153,7 +149,7 @@ const BaseField = ({
|
||||
type='number'
|
||||
className={cn(inputClassName)}
|
||||
value={value || ''}
|
||||
onChange={e => field.handleChange(e.target.value)}
|
||||
onChange={e => handleChange(e.target.value)}
|
||||
onBlur={field.handleBlur}
|
||||
disabled={disabled}
|
||||
placeholder={memorizedPlaceholder}
|
||||
@ -164,11 +160,14 @@ const BaseField = ({
|
||||
formSchema.type === FormTypeEnum.select && (
|
||||
<PureSelect
|
||||
value={value}
|
||||
onChange={v => field.handleChange(v)}
|
||||
onChange={v => handleChange(v)}
|
||||
disabled={disabled}
|
||||
placeholder={memorizedPlaceholder}
|
||||
options={memorizedOptions}
|
||||
triggerPopupSameWidth
|
||||
popupProps={{
|
||||
className: 'max-h-[320px] overflow-y-auto',
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@ -187,7 +186,7 @@ const BaseField = ({
|
||||
disabled && 'cursor-not-allowed opacity-50',
|
||||
inputClassName,
|
||||
)}
|
||||
onClick={() => !disabled && field.handleChange(option.value)}
|
||||
onClick={() => !disabled && handleChange(option.value)}
|
||||
>
|
||||
{
|
||||
formSchema.showRadioUI && (
|
||||
@ -204,6 +203,18 @@ const BaseField = ({
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
formSchema.type === FormTypeEnum.boolean && (
|
||||
<Radio.Group
|
||||
className='flex w-fit items-center'
|
||||
value={value}
|
||||
onChange={v => field.handleChange(v)}
|
||||
>
|
||||
<Radio value={true} className='!mr-1'>True</Radio>
|
||||
<Radio value={false}>False</Radio>
|
||||
</Radio.Group>
|
||||
)
|
||||
}
|
||||
{
|
||||
formSchema.url && (
|
||||
<a
|
||||
|
||||
@ -8,7 +8,10 @@ import type {
|
||||
AnyFieldApi,
|
||||
AnyFormApi,
|
||||
} from '@tanstack/react-form'
|
||||
import { useForm } from '@tanstack/react-form'
|
||||
import {
|
||||
useForm,
|
||||
useStore,
|
||||
} from '@tanstack/react-form'
|
||||
import type {
|
||||
FormRef,
|
||||
FormSchema,
|
||||
@ -32,6 +35,7 @@ export type BaseFormProps = {
|
||||
ref?: FormRef
|
||||
disabled?: boolean
|
||||
formFromProps?: AnyFormApi
|
||||
onChange?: (field: string, value: any) => void
|
||||
} & Pick<BaseFieldProps, 'fieldClassName' | 'labelClassName' | 'inputContainerClassName' | 'inputClassName'>
|
||||
|
||||
const BaseForm = ({
|
||||
@ -45,6 +49,7 @@ const BaseForm = ({
|
||||
ref,
|
||||
disabled,
|
||||
formFromProps,
|
||||
onChange,
|
||||
}: BaseFormProps) => {
|
||||
const initialDefaultValues = useMemo(() => {
|
||||
if (defaultValues)
|
||||
@ -63,6 +68,19 @@ const BaseForm = ({
|
||||
const { getFormValues } = useGetFormValues(form, formSchemas)
|
||||
const { getValidators } = useGetValidators()
|
||||
|
||||
const showOnValues = useStore(form.store, (s: any) => {
|
||||
const result: Record<string, any> = {}
|
||||
formSchemas.forEach((schema) => {
|
||||
const { show_on } = schema
|
||||
if (show_on?.length) {
|
||||
show_on.forEach((condition) => {
|
||||
result[condition.variable] = s.values[condition.variable]
|
||||
})
|
||||
}
|
||||
})
|
||||
return result
|
||||
})
|
||||
|
||||
useImperativeHandle(ref, () => {
|
||||
return {
|
||||
getForm() {
|
||||
@ -87,19 +105,29 @@ const BaseForm = ({
|
||||
inputContainerClassName={inputContainerClassName}
|
||||
inputClassName={inputClassName}
|
||||
disabled={disabled}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}, [formSchemas, fieldClassName, labelClassName, inputContainerClassName, inputClassName, disabled])
|
||||
}, [formSchemas, fieldClassName, labelClassName, inputContainerClassName, inputClassName, disabled, onChange])
|
||||
|
||||
const renderFieldWrapper = useCallback((formSchema: FormSchema) => {
|
||||
const validators = getValidators(formSchema)
|
||||
const {
|
||||
name,
|
||||
show_on = [],
|
||||
} = formSchema
|
||||
|
||||
const show = show_on?.every((condition) => {
|
||||
const conditionValue = showOnValues[condition.variable]
|
||||
return conditionValue === condition.value
|
||||
})
|
||||
|
||||
if (!show)
|
||||
return null
|
||||
|
||||
return (
|
||||
<form.Field
|
||||
key={name}
|
||||
@ -109,7 +137,7 @@ const BaseForm = ({
|
||||
{renderField}
|
||||
</form.Field>
|
||||
)
|
||||
}, [renderField, form, getValidators])
|
||||
}, [renderField, form, getValidators, showOnValues])
|
||||
|
||||
if (!formSchemas?.length)
|
||||
return null
|
||||
|
||||
@ -32,6 +32,7 @@ export enum FormTypeEnum {
|
||||
multiToolSelector = 'array[tools]',
|
||||
appSelector = 'app-selector',
|
||||
dynamicSelect = 'dynamic-select',
|
||||
boolean = 'boolean',
|
||||
}
|
||||
|
||||
export type FormOption = {
|
||||
@ -69,10 +70,10 @@ export type GetValuesOptions = {
|
||||
needCheckValidatedValues?: boolean
|
||||
}
|
||||
export type FormRefObject = {
|
||||
getForm: () => AnyFormApi
|
||||
getFormValues: (obj: GetValuesOptions) => {
|
||||
values: Record<string, any>
|
||||
isCheckValidated: boolean
|
||||
}
|
||||
getForm: () => AnyFormApi
|
||||
getFormValues: (obj: GetValuesOptions) => {
|
||||
values: Record<string, any>
|
||||
isCheckValidated: boolean
|
||||
}
|
||||
}
|
||||
export type FormRef = ForwardedRef<FormRefObject>
|
||||
|
||||
@ -529,9 +529,9 @@ const Flowchart = React.forwardRef((props: {
|
||||
{isLoading && !svgString && (
|
||||
<div className='px-[26px] py-4'>
|
||||
<LoadingAnim type='text'/>
|
||||
<div className="mt-2 text-sm text-gray-500">
|
||||
{t('common.wait_for_completion', 'Waiting for diagram code to complete...')}
|
||||
</div>
|
||||
<div className="mt-2 text-sm text-gray-500">
|
||||
{t('common.wait_for_completion', 'Waiting for diagram code to complete...')}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import clsx from 'clsx'
|
||||
import cn from 'classnames'
|
||||
import usePagination from './hook'
|
||||
import type {
|
||||
ButtonProps,
|
||||
@ -45,7 +45,7 @@ export const PrevButton = ({
|
||||
<as.type
|
||||
{...buttonProps}
|
||||
{...as.props}
|
||||
className={clsx(className, as.props.className)}
|
||||
className={cn(className, as.props.className)}
|
||||
onClick={() => previous()}
|
||||
tabIndex={disabled ? '-1' : 0}
|
||||
disabled={disabled}
|
||||
@ -80,7 +80,7 @@ export const NextButton = ({
|
||||
<as.type
|
||||
{...buttonProps}
|
||||
{...as.props}
|
||||
className={clsx(className, as.props.className)}
|
||||
className={cn(className, as.props.className)}
|
||||
onClick={() => next()}
|
||||
tabIndex={disabled ? '-1' : 0}
|
||||
disabled={disabled}
|
||||
@ -132,7 +132,7 @@ export const PageButton = ({
|
||||
<li key={page}>
|
||||
<as.type
|
||||
data-testid={
|
||||
clsx({
|
||||
cn({
|
||||
[`${dataTestIdActive}`]:
|
||||
dataTestIdActive && pagination.currentPage + 1 === page,
|
||||
[`${dataTestIdInactive}-${page}`]:
|
||||
@ -145,7 +145,7 @@ export const PageButton = ({
|
||||
pagination.setCurrentPage(page - 1)
|
||||
}}
|
||||
onClick={() => pagination.setCurrentPage(page - 1)}
|
||||
className={clsx(
|
||||
className={cn(
|
||||
className,
|
||||
pagination.currentPage + 1 === page
|
||||
? activeClassName
|
||||
|
||||
@ -20,9 +20,9 @@ const CurrentBlockComponent: FC<CurrentBlockComponentProps> = ({
|
||||
|
||||
const Icon = generatorType === GeneratorType.prompt ? MagicEdit : CodeAssistant
|
||||
useEffect(() => {
|
||||
if (!editor.hasNodes([CurrentBlockNode]))
|
||||
throw new Error('WorkflowVariableBlockPlugin: WorkflowVariableBlock not registered on editor')
|
||||
}, [editor])
|
||||
if (!editor.hasNodes([CurrentBlockNode]))
|
||||
throw new Error('WorkflowVariableBlockPlugin: WorkflowVariableBlock not registered on editor')
|
||||
}, [editor])
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@ -16,9 +16,9 @@ const ErrorMessageBlockComponent: FC<Props> = ({
|
||||
const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_ERROR_MESSAGE_COMMAND)
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor.hasNodes([ErrorMessageBlockNode]))
|
||||
throw new Error('WorkflowVariableBlockPlugin: WorkflowVariableBlock not registered on editor')
|
||||
}, [editor])
|
||||
if (!editor.hasNodes([ErrorMessageBlockNode]))
|
||||
throw new Error('WorkflowVariableBlockPlugin: WorkflowVariableBlock not registered on editor')
|
||||
}, [editor])
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@ -16,9 +16,9 @@ const LastRunBlockComponent: FC<Props> = ({
|
||||
const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_LAST_RUN_COMMAND)
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor.hasNodes([LastRunBlockNode]))
|
||||
throw new Error('WorkflowVariableBlockPlugin: WorkflowVariableBlock not registered on editor')
|
||||
}, [editor])
|
||||
if (!editor.hasNodes([LastRunBlockNode]))
|
||||
throw new Error('WorkflowVariableBlockPlugin: WorkflowVariableBlock not registered on editor')
|
||||
}, [editor])
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@ -5,7 +5,7 @@ import cn from '@/utils/classnames'
|
||||
|
||||
export type TRadioGroupProps = {
|
||||
children?: ReactNode | ReactNode[]
|
||||
value?: string | number
|
||||
value?: string | number | boolean
|
||||
className?: string
|
||||
onChange?: (value: any) => void
|
||||
}
|
||||
|
||||
@ -10,7 +10,7 @@ export type IRadioProps = {
|
||||
labelClassName?: string
|
||||
children?: string | ReactNode
|
||||
checked?: boolean
|
||||
value?: string | number
|
||||
value?: string | number | boolean
|
||||
disabled?: boolean
|
||||
onChange?: (e?: IRadioProps['value']) => void
|
||||
}
|
||||
|
||||
@ -219,36 +219,36 @@ const SimpleSelect: FC<ISelectProps> = ({
|
||||
{renderTrigger && <ListboxButton className='w-full'>{renderTrigger(selectedItem)}</ListboxButton>}
|
||||
{!renderTrigger && (
|
||||
<ListboxButton onClick={() => {
|
||||
onOpenChange?.(open)
|
||||
onOpenChange?.(open)
|
||||
}} className={classNames(`flex h-full w-full items-center rounded-lg border-0 bg-components-input-bg-normal pl-3 pr-10 focus-visible:bg-state-base-hover-alt focus-visible:outline-none group-hover/simple-select:bg-state-base-hover-alt sm:text-sm sm:leading-6 ${disabled ? 'cursor-not-allowed' : 'cursor-pointer'}`, className)}>
|
||||
<span className={classNames('system-sm-regular block truncate text-left text-components-input-text-filled', !selectedItem?.name && 'text-components-input-text-placeholder')}>{selectedItem?.name ?? localPlaceholder}</span>
|
||||
<span className="absolute inset-y-0 right-0 flex items-center pr-2">
|
||||
{isLoading ? <RiLoader4Line className='h-3.5 w-3.5 animate-spin text-text-secondary' />
|
||||
: (selectedItem && !notClearable)
|
||||
? (
|
||||
<XMarkIcon
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setSelectedItem(null)
|
||||
onSelect({ name: '', value: '' })
|
||||
}}
|
||||
className="h-4 w-4 cursor-pointer text-text-quaternary"
|
||||
aria-hidden="false"
|
||||
/>
|
||||
)
|
||||
: (
|
||||
open ? (
|
||||
<ChevronUpIcon
|
||||
className="h-4 w-4 text-text-quaternary group-hover/simple-select:text-text-secondary"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : (
|
||||
<ChevronDownIcon
|
||||
className="h-4 w-4 text-text-quaternary group-hover/simple-select:text-text-secondary"
|
||||
aria-hidden="true"
|
||||
: (selectedItem && !notClearable)
|
||||
? (
|
||||
<XMarkIcon
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setSelectedItem(null)
|
||||
onSelect({ name: '', value: '' })
|
||||
}}
|
||||
className="h-4 w-4 cursor-pointer text-text-quaternary"
|
||||
aria-hidden="false"
|
||||
/>
|
||||
)
|
||||
)}
|
||||
: (
|
||||
open ? (
|
||||
<ChevronUpIcon
|
||||
className="h-4 w-4 text-text-quaternary group-hover/simple-select:text-text-secondary"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : (
|
||||
<ChevronDownIcon
|
||||
className="h-4 w-4 text-text-quaternary group-hover/simple-select:text-text-secondary"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</span>
|
||||
</ListboxButton>
|
||||
)}
|
||||
|
||||
@ -36,7 +36,7 @@ export default function LocaleSigninSelect({
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<MenuItems className="absolute right-0 z-10 mt-2 w-[200px] origin-top-right divide-y divide-divider-regular rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg focus:outline-none">
|
||||
<div className="px-1 py-1 ">
|
||||
<div className="max-h-96 overflow-y-auto px-1 py-1 [mask-image:linear-gradient(to_bottom,transparent_0px,black_8px,black_calc(100%-8px),transparent_100%)]">
|
||||
{items.map((item) => {
|
||||
return <MenuItem key={item.value}>
|
||||
<button
|
||||
|
||||
@ -29,7 +29,6 @@ const OptionsWrap: FC<Props> = ({
|
||||
useEffect(() => {
|
||||
if (controlFoldOptions)
|
||||
foldHide()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [controlFoldOptions])
|
||||
return (
|
||||
<div className={cn(className, !fold ? 'mb-0' : 'mb-3')}>
|
||||
|
||||
@ -161,7 +161,7 @@ const CSVUploader: FC<Props> = ({
|
||||
const fileChangeHandle = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const currentFile = e.target.files?.[0]
|
||||
if (!isValid(currentFile))
|
||||
return
|
||||
return
|
||||
|
||||
initialUpload(currentFile)
|
||||
}
|
||||
|
||||
@ -24,9 +24,9 @@ const InfoPanel = () => {
|
||||
{t('dataset.connectDatasetIntro.content.end')}
|
||||
</span>
|
||||
<a className='system-sm-regular self-stretch text-text-accent'
|
||||
href={docLink('/guides/knowledge-base/connect-external-knowledge-base')}
|
||||
target='_blank'
|
||||
rel="noopener noreferrer">
|
||||
href={docLink('/guides/knowledge-base/connect-external-knowledge-base')}
|
||||
target='_blank'
|
||||
rel="noopener noreferrer">
|
||||
{t('dataset.connectDatasetIntro.learnMore')}
|
||||
</a>
|
||||
</p>
|
||||
|
||||
@ -28,7 +28,7 @@ const ExternalKnowledgeBaseCreate: React.FC<ExternalKnowledgeBaseCreateProps> =
|
||||
external_knowledge_api_id: '',
|
||||
external_knowledge_id: '',
|
||||
external_retrieval_model: {
|
||||
top_k: 2,
|
||||
top_k: 4,
|
||||
score_threshold: 0.5,
|
||||
score_threshold_enabled: false,
|
||||
},
|
||||
|
||||
@ -49,7 +49,7 @@ const TextAreaWithButton = ({
|
||||
const { t } = useTranslation()
|
||||
const [isSettingsOpen, setIsSettingsOpen] = useState(false)
|
||||
const [externalRetrievalSettings, setExternalRetrievalSettings] = useState({
|
||||
top_k: 2,
|
||||
top_k: 4,
|
||||
score_threshold: 0.5,
|
||||
score_threshold_enabled: false,
|
||||
})
|
||||
|
||||
@ -28,7 +28,6 @@ const useEditDatasetMetadata = ({
|
||||
showEditModal()
|
||||
localStorage.removeItem(isShowManageMetadataLocalStorageKey)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
const { data: datasetMetaData } = useDatasetMetaData(datasetId)
|
||||
|
||||
@ -132,7 +132,7 @@ export class SlashCommandRegistry {
|
||||
try {
|
||||
return await handler.search(args, locale)
|
||||
}
|
||||
catch (error) {
|
||||
catch (error) {
|
||||
console.warn(`Command search failed for ${commandName}:`, error)
|
||||
return []
|
||||
}
|
||||
@ -144,7 +144,7 @@ export class SlashCommandRegistry {
|
||||
try {
|
||||
return await handler.search(args, locale)
|
||||
}
|
||||
catch (error) {
|
||||
catch (error) {
|
||||
console.warn(`Command search failed for ${handler.name}:`, error)
|
||||
return []
|
||||
}
|
||||
|
||||
@ -205,7 +205,7 @@ export const searchAnything = async (
|
||||
const results = await action.search(query, query, locale)
|
||||
return { success: true, data: results, actionType: action.key }
|
||||
}
|
||||
catch (error) {
|
||||
catch (error) {
|
||||
console.warn(`Search failed for ${action.key}:`, error)
|
||||
return { success: false, data: [], actionType: action.key, error }
|
||||
}
|
||||
@ -220,7 +220,7 @@ export const searchAnything = async (
|
||||
if (result.status === 'fulfilled' && result.value.success) {
|
||||
allResults.push(...result.value.data)
|
||||
}
|
||||
else {
|
||||
else {
|
||||
const actionKey = globalSearchActions[index]?.key || 'unknown'
|
||||
failedActions.push(actionKey)
|
||||
}
|
||||
|
||||
@ -166,7 +166,7 @@ const GotoAnything: FC<Props> = ({
|
||||
acc[result.type].push(result)
|
||||
return acc
|
||||
}, {} as { [key: string]: SearchResult[] }),
|
||||
[searchResults])
|
||||
[searchResults])
|
||||
|
||||
const emptyResult = useMemo(() => {
|
||||
if (searchResults.length || !searchQuery.trim() || isLoading || isCommandsMode)
|
||||
|
||||
@ -32,7 +32,6 @@ const DataSourceWebsite: FC<Props> = ({ provider }) => {
|
||||
|
||||
useEffect(() => {
|
||||
checkSetApiKey()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
const [configTarget, setConfigTarget] = useState<DataSourceProvider | null>(null)
|
||||
|
||||
@ -100,7 +100,7 @@ const InviteModal = ({
|
||||
<div className='mb-2 text-sm font-medium text-text-primary'>{t('common.members.email')}</div>
|
||||
<div className='mb-8 flex h-36 flex-col items-stretch'>
|
||||
<ReactMultiEmail
|
||||
className={cn('w-full border-components-input-border-active !bg-components-input-bg-normal px-3 pt-2 outline-none',
|
||||
className={cn('h-full w-full border-components-input-border-active !bg-components-input-bg-normal px-3 pt-2 outline-none',
|
||||
'appearance-none overflow-y-auto rounded-lg text-sm !text-text-primary',
|
||||
)}
|
||||
autoFocus
|
||||
|
||||
@ -199,6 +199,7 @@ export type CustomModelCredential = CustomModel & {
|
||||
credentials?: Record<string, any>
|
||||
available_model_credentials?: Credential[]
|
||||
current_credential_id?: string
|
||||
current_credential_name?: string
|
||||
}
|
||||
|
||||
export type CredentialWithModel = Credential & {
|
||||
@ -236,6 +237,10 @@ export type ModelProvider = {
|
||||
current_credential_name?: string
|
||||
available_credentials?: Credential[]
|
||||
custom_models?: CustomModelCredential[]
|
||||
can_added_models?: {
|
||||
model: string
|
||||
model_type: ModelTypeEnum
|
||||
}[]
|
||||
}
|
||||
system_configuration: {
|
||||
enabled: boolean
|
||||
@ -323,3 +328,10 @@ export type ModelCredential = {
|
||||
current_credential_id?: string
|
||||
current_credential_name?: string
|
||||
}
|
||||
|
||||
export enum ModelModalModeEnum {
|
||||
configProviderCredential = 'config-provider-credential',
|
||||
configCustomModel = 'config-custom-model',
|
||||
addCustomModelToModelList = 'add-custom-model-to-model-list',
|
||||
configModelCredential = 'config-model-credential',
|
||||
}
|
||||
|
||||
@ -13,6 +13,7 @@ import type {
|
||||
DefaultModel,
|
||||
DefaultModelResponse,
|
||||
Model,
|
||||
ModelModalModeEnum,
|
||||
ModelProvider,
|
||||
ModelTypeEnum,
|
||||
} from './declarations'
|
||||
@ -348,29 +349,31 @@ export const useRefreshModel = () => {
|
||||
|
||||
export const useModelModalHandler = () => {
|
||||
const setShowModelModal = useModalContextSelector(state => state.setShowModelModal)
|
||||
const { handleRefreshModel } = useRefreshModel()
|
||||
|
||||
return (
|
||||
provider: ModelProvider,
|
||||
configurationMethod: ConfigurationMethodEnum,
|
||||
CustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields,
|
||||
isModelCredential?: boolean,
|
||||
credential?: Credential,
|
||||
model?: CustomModel,
|
||||
onUpdate?: () => void,
|
||||
extra: {
|
||||
isModelCredential?: boolean,
|
||||
credential?: Credential,
|
||||
model?: CustomModel,
|
||||
onUpdate?: (newPayload: any, formValues?: Record<string, any>) => void,
|
||||
mode?: ModelModalModeEnum,
|
||||
} = {},
|
||||
) => {
|
||||
setShowModelModal({
|
||||
payload: {
|
||||
currentProvider: provider,
|
||||
currentConfigurationMethod: configurationMethod,
|
||||
currentCustomConfigurationModelFixedFields: CustomConfigurationModelFixedFields,
|
||||
isModelCredential,
|
||||
credential,
|
||||
model,
|
||||
isModelCredential: extra.isModelCredential,
|
||||
credential: extra.credential,
|
||||
model: extra.model,
|
||||
mode: extra.mode,
|
||||
},
|
||||
onSaveCallback: () => {
|
||||
handleRefreshModel(provider, configurationMethod, CustomConfigurationModelFixedFields)
|
||||
onUpdate?.()
|
||||
onSaveCallback: (newPayload, formValues) => {
|
||||
extra.onUpdate?.(newPayload, formValues)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useMemo,
|
||||
} from 'react'
|
||||
import { RiAddLine } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -9,20 +8,22 @@ import { Authorized } from '@/app/components/header/account-setting/model-provid
|
||||
import cn from '@/utils/classnames'
|
||||
import type {
|
||||
Credential,
|
||||
CustomConfigurationModelFixedFields,
|
||||
CustomModelCredential,
|
||||
ModelCredential,
|
||||
ModelProvider,
|
||||
} from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { ConfigurationMethodEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { ConfigurationMethodEnum, ModelModalModeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
|
||||
type AddCredentialInLoadBalancingProps = {
|
||||
provider: ModelProvider
|
||||
model: CustomModelCredential
|
||||
configurationMethod: ConfigurationMethodEnum
|
||||
currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields
|
||||
modelCredential: ModelCredential
|
||||
onSelectCredential: (credential: Credential) => void
|
||||
onUpdate?: () => void
|
||||
onUpdate?: (payload?: any, formValues?: Record<string, any>) => void
|
||||
onRemove?: (credentialId: string) => void
|
||||
}
|
||||
const AddCredentialInLoadBalancing = ({
|
||||
provider,
|
||||
@ -31,41 +32,17 @@ const AddCredentialInLoadBalancing = ({
|
||||
modelCredential,
|
||||
onSelectCredential,
|
||||
onUpdate,
|
||||
onRemove,
|
||||
}: AddCredentialInLoadBalancingProps) => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
available_credentials,
|
||||
} = modelCredential
|
||||
const customModel = configurationMethod === ConfigurationMethodEnum.customizableModel
|
||||
const isCustomModel = configurationMethod === ConfigurationMethodEnum.customizableModel
|
||||
const notAllowCustomCredential = provider.allow_custom_token === false
|
||||
|
||||
const ButtonComponent = useMemo(() => {
|
||||
const Item = (
|
||||
<div className={cn(
|
||||
'system-sm-medium flex h-8 items-center rounded-lg px-3 text-text-accent hover:bg-state-base-hover',
|
||||
notAllowCustomCredential && 'cursor-not-allowed opacity-50',
|
||||
)}>
|
||||
<RiAddLine className='mr-2 h-4 w-4' />
|
||||
{
|
||||
customModel
|
||||
? t('common.modelProvider.auth.addCredential')
|
||||
: t('common.modelProvider.auth.addApiKey')
|
||||
}
|
||||
</div>
|
||||
)
|
||||
|
||||
if (notAllowCustomCredential) {
|
||||
return (
|
||||
<Tooltip
|
||||
asChild
|
||||
popupContent={t('plugin.auth.credentialUnavailable')}
|
||||
>
|
||||
{Item}
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
return Item
|
||||
}, [notAllowCustomCredential, t, customModel])
|
||||
const handleUpdate = useCallback((payload?: any, formValues?: Record<string, any>) => {
|
||||
onUpdate?.(payload, formValues)
|
||||
}, [onUpdate])
|
||||
|
||||
const renderTrigger = useCallback((open?: boolean) => {
|
||||
const Item = (
|
||||
@ -74,40 +51,40 @@ const AddCredentialInLoadBalancing = ({
|
||||
open && 'bg-state-base-hover',
|
||||
)}>
|
||||
<RiAddLine className='mr-2 h-4 w-4' />
|
||||
{
|
||||
customModel
|
||||
? t('common.modelProvider.auth.addCredential')
|
||||
: t('common.modelProvider.auth.addApiKey')
|
||||
}
|
||||
{t('common.modelProvider.auth.addCredential')}
|
||||
</div>
|
||||
)
|
||||
|
||||
return Item
|
||||
}, [t, customModel])
|
||||
|
||||
if (!available_credentials?.length)
|
||||
return ButtonComponent
|
||||
}, [t, isCustomModel])
|
||||
|
||||
return (
|
||||
<Authorized
|
||||
provider={provider}
|
||||
renderTrigger={renderTrigger}
|
||||
authParams={{
|
||||
isModelCredential: isCustomModel,
|
||||
mode: ModelModalModeEnum.configModelCredential,
|
||||
onUpdate: handleUpdate,
|
||||
onRemove,
|
||||
}}
|
||||
triggerOnlyOpenModal={!available_credentials?.length && !notAllowCustomCredential}
|
||||
items={[
|
||||
{
|
||||
title: customModel ? t('common.modelProvider.auth.modelCredentials') : t('common.modelProvider.auth.apiKeys'),
|
||||
model: customModel ? model : undefined,
|
||||
title: isCustomModel ? '' : t('common.modelProvider.auth.apiKeys'),
|
||||
model: isCustomModel ? model : undefined,
|
||||
credentials: available_credentials ?? [],
|
||||
},
|
||||
]}
|
||||
showModelTitle={!isCustomModel}
|
||||
configurationMethod={configurationMethod}
|
||||
currentCustomConfigurationModelFixedFields={customModel ? {
|
||||
currentCustomConfigurationModelFixedFields={isCustomModel ? {
|
||||
__model_name: model.model,
|
||||
__model_type: model.model_type,
|
||||
} : undefined}
|
||||
onItemClick={onSelectCredential}
|
||||
placement='bottom-start'
|
||||
onUpdate={onUpdate}
|
||||
isModelCredential={customModel}
|
||||
popupTitle={isCustomModel ? t('common.modelProvider.auth.modelCredentials') : ''}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,32 +1,39 @@
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiAddCircleFill,
|
||||
RiAddLine,
|
||||
} from '@remixicon/react'
|
||||
import {
|
||||
Button,
|
||||
} from '@/app/components/base/button'
|
||||
import type {
|
||||
ConfigurationMethodEnum,
|
||||
CustomConfigurationModelFixedFields,
|
||||
ModelProvider,
|
||||
} from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { ConfigurationMethodEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import Authorized from './authorized'
|
||||
import {
|
||||
useAuth,
|
||||
useCustomModels,
|
||||
} from './hooks'
|
||||
import { ModelModalModeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import cn from '@/utils/classnames'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import ModelIcon from '../model-icon'
|
||||
import { useCanAddedModels } from './hooks/use-custom-models'
|
||||
import { useAuth } from './hooks/use-auth'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
|
||||
type AddCustomModelProps = {
|
||||
provider: ModelProvider,
|
||||
configurationMethod: ConfigurationMethodEnum,
|
||||
currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields,
|
||||
open?: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
}
|
||||
const AddCustomModel = ({
|
||||
provider,
|
||||
@ -34,44 +41,32 @@ const AddCustomModel = ({
|
||||
currentCustomConfigurationModelFixedFields,
|
||||
}: AddCustomModelProps) => {
|
||||
const { t } = useTranslation()
|
||||
const customModels = useCustomModels(provider)
|
||||
const noModels = !customModels.length
|
||||
const [open, setOpen] = useState(false)
|
||||
const canAddedModels = useCanAddedModels(provider)
|
||||
const noModels = !canAddedModels.length
|
||||
const {
|
||||
handleOpenModal,
|
||||
} = useAuth(provider, configurationMethod, currentCustomConfigurationModelFixedFields, true)
|
||||
handleOpenModal: handleOpenModalForAddNewCustomModel,
|
||||
} = useAuth(
|
||||
provider,
|
||||
configurationMethod,
|
||||
currentCustomConfigurationModelFixedFields,
|
||||
{
|
||||
isModelCredential: true,
|
||||
mode: ModelModalModeEnum.configCustomModel,
|
||||
},
|
||||
)
|
||||
const {
|
||||
handleOpenModal: handleOpenModalForAddCustomModelToModelList,
|
||||
} = useAuth(
|
||||
provider,
|
||||
configurationMethod,
|
||||
currentCustomConfigurationModelFixedFields,
|
||||
{
|
||||
isModelCredential: true,
|
||||
mode: ModelModalModeEnum.addCustomModelToModelList,
|
||||
},
|
||||
)
|
||||
const notAllowCustomCredential = provider.allow_custom_token === false
|
||||
const handleClick = useCallback(() => {
|
||||
if (notAllowCustomCredential)
|
||||
return
|
||||
|
||||
handleOpenModal()
|
||||
}, [handleOpenModal, notAllowCustomCredential])
|
||||
const ButtonComponent = useMemo(() => {
|
||||
const Item = (
|
||||
<Button
|
||||
variant='ghost-accent'
|
||||
size='small'
|
||||
onClick={handleClick}
|
||||
className={cn(
|
||||
notAllowCustomCredential && 'cursor-not-allowed opacity-50',
|
||||
)}
|
||||
>
|
||||
<RiAddCircleFill className='mr-1 h-3.5 w-3.5' />
|
||||
{t('common.modelProvider.addModel')}
|
||||
</Button>
|
||||
)
|
||||
if (notAllowCustomCredential) {
|
||||
return (
|
||||
<Tooltip
|
||||
asChild
|
||||
popupContent={t('plugin.auth.credentialUnavailable')}
|
||||
>
|
||||
{Item}
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
return Item
|
||||
}, [handleClick, notAllowCustomCredential, t])
|
||||
|
||||
const renderTrigger = useCallback((open?: boolean) => {
|
||||
const Item = (
|
||||
@ -79,32 +74,93 @@ const AddCustomModel = ({
|
||||
variant='ghost'
|
||||
size='small'
|
||||
className={cn(
|
||||
'text-text-tertiary',
|
||||
open && 'bg-components-button-ghost-bg-hover',
|
||||
notAllowCustomCredential && !!noModels && 'cursor-not-allowed opacity-50',
|
||||
)}
|
||||
>
|
||||
<RiAddCircleFill className='mr-1 h-3.5 w-3.5' />
|
||||
{t('common.modelProvider.addModel')}
|
||||
</Button>
|
||||
)
|
||||
if (notAllowCustomCredential && !!noModels) {
|
||||
return (
|
||||
<Tooltip asChild popupContent={t('plugin.auth.credentialUnavailable')}>
|
||||
{Item}
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
return Item
|
||||
}, [t])
|
||||
|
||||
if (noModels)
|
||||
return ButtonComponent
|
||||
}, [t, notAllowCustomCredential, noModels])
|
||||
|
||||
return (
|
||||
<Authorized
|
||||
provider={provider}
|
||||
configurationMethod={ConfigurationMethodEnum.customizableModel}
|
||||
items={customModels.map(model => ({
|
||||
model,
|
||||
credentials: model.available_model_credentials ?? [],
|
||||
}))}
|
||||
renderTrigger={renderTrigger}
|
||||
isModelCredential
|
||||
enableAddModelCredential
|
||||
bottomAddModelCredentialText={t('common.modelProvider.auth.addNewModel')}
|
||||
/>
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement='bottom-end'
|
||||
offset={{
|
||||
mainAxis: 4,
|
||||
crossAxis: 0,
|
||||
}}
|
||||
>
|
||||
<PortalToFollowElemTrigger onClick={() => {
|
||||
if (noModels) {
|
||||
if (notAllowCustomCredential)
|
||||
return
|
||||
handleOpenModalForAddNewCustomModel()
|
||||
return
|
||||
}
|
||||
|
||||
setOpen(prev => !prev)
|
||||
}}>
|
||||
{renderTrigger(open)}
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-[100]'>
|
||||
<div className='w-[320px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg'>
|
||||
<div className='max-h-[304px] overflow-y-auto p-1'>
|
||||
{
|
||||
canAddedModels.map(model => (
|
||||
<div
|
||||
key={model.model}
|
||||
className='flex h-8 cursor-pointer items-center rounded-lg px-2 hover:bg-state-base-hover'
|
||||
onClick={() => {
|
||||
handleOpenModalForAddCustomModelToModelList(undefined, model)
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<ModelIcon
|
||||
className='mr-1 h-5 w-5 shrink-0'
|
||||
iconClassName='h-5 w-5'
|
||||
provider={provider}
|
||||
modelName={model.model}
|
||||
/>
|
||||
<div
|
||||
className='system-md-regular grow truncate text-text-primary'
|
||||
title={model.model}
|
||||
>
|
||||
{model.model}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
{
|
||||
!notAllowCustomCredential && (
|
||||
<div
|
||||
className='system-xs-medium flex cursor-pointer items-center border-t border-t-divider-subtle p-3 text-text-accent-light-mode-only'
|
||||
onClick={() => {
|
||||
handleOpenModalForAddNewCustomModel()
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<RiAddLine className='mr-1 h-4 w-4' />
|
||||
{t('common.modelProvider.auth.addNewModel')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -2,18 +2,17 @@ import {
|
||||
memo,
|
||||
useCallback,
|
||||
} from 'react'
|
||||
import { RiAddLine } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import CredentialItem from './credential-item'
|
||||
import type {
|
||||
Credential,
|
||||
CustomModel,
|
||||
CustomModelCredential,
|
||||
ModelProvider,
|
||||
} from '../../declarations'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import ModelIcon from '../../model-icon'
|
||||
|
||||
type AuthorizedItemProps = {
|
||||
provider: ModelProvider
|
||||
model?: CustomModelCredential
|
||||
title?: string
|
||||
disabled?: boolean
|
||||
@ -25,8 +24,12 @@ type AuthorizedItemProps = {
|
||||
onItemClick?: (credential: Credential, model?: CustomModel) => void
|
||||
enableAddModelCredential?: boolean
|
||||
notAllowCustomCredential?: boolean
|
||||
showModelTitle?: boolean
|
||||
disableDeleteButShowAction?: boolean
|
||||
disableDeleteTip?: string
|
||||
}
|
||||
export const AuthorizedItem = ({
|
||||
provider,
|
||||
model,
|
||||
title,
|
||||
credentials,
|
||||
@ -36,10 +39,10 @@ export const AuthorizedItem = ({
|
||||
showItemSelectedIcon,
|
||||
selectedCredentialId,
|
||||
onItemClick,
|
||||
enableAddModelCredential,
|
||||
notAllowCustomCredential,
|
||||
showModelTitle,
|
||||
disableDeleteButShowAction,
|
||||
disableDeleteTip,
|
||||
}: AuthorizedItemProps) => {
|
||||
const { t } = useTranslation()
|
||||
const handleEdit = useCallback((credential?: Credential) => {
|
||||
onEdit?.(credential, model)
|
||||
}, [onEdit, model])
|
||||
@ -52,34 +55,29 @@ export const AuthorizedItem = ({
|
||||
|
||||
return (
|
||||
<div className='p-1'>
|
||||
<div
|
||||
className='flex h-9 items-center'
|
||||
>
|
||||
<div className='h-5 w-5 shrink-0'></div>
|
||||
<div
|
||||
className='system-md-medium mx-1 grow truncate text-text-primary'
|
||||
title={title ?? model?.model}
|
||||
>
|
||||
{title ?? model?.model}
|
||||
</div>
|
||||
{
|
||||
enableAddModelCredential && !notAllowCustomCredential && (
|
||||
<Tooltip
|
||||
asChild
|
||||
popupContent={t('common.modelProvider.auth.addModelCredential')}
|
||||
{
|
||||
showModelTitle && (
|
||||
<div
|
||||
className='flex h-9 items-center px-2'
|
||||
>
|
||||
{
|
||||
model?.model && (
|
||||
<ModelIcon
|
||||
className='mr-1 h-5 w-5 shrink-0'
|
||||
provider={provider}
|
||||
modelName={model.model}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<div
|
||||
className='system-md-medium mx-1 grow truncate text-text-primary'
|
||||
title={title ?? model?.model}
|
||||
>
|
||||
<Button
|
||||
className='h-6 w-6 shrink-0 rounded-full p-0'
|
||||
size='small'
|
||||
variant='secondary-accent'
|
||||
onClick={() => handleEdit?.()}
|
||||
>
|
||||
<RiAddLine className='h-4 w-4' />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{title ?? model?.model}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
credentials.map(credential => (
|
||||
<CredentialItem
|
||||
@ -91,6 +89,8 @@ export const AuthorizedItem = ({
|
||||
showSelectedIcon={showItemSelectedIcon}
|
||||
selectedCredentialId={selectedCredentialId}
|
||||
onItemClick={handleItemClick}
|
||||
disableDeleteButShowAction={disableDeleteButShowAction}
|
||||
disableDeleteTip={disableDeleteTip}
|
||||
/>
|
||||
))
|
||||
}
|
||||
|
||||
@ -24,6 +24,8 @@ type CredentialItemProps = {
|
||||
disableRename?: boolean
|
||||
disableEdit?: boolean
|
||||
disableDelete?: boolean
|
||||
disableDeleteButShowAction?: boolean
|
||||
disableDeleteTip?: string
|
||||
showSelectedIcon?: boolean
|
||||
selectedCredentialId?: string
|
||||
}
|
||||
@ -36,6 +38,8 @@ const CredentialItem = ({
|
||||
disableRename,
|
||||
disableEdit,
|
||||
disableDelete,
|
||||
disableDeleteButShowAction,
|
||||
disableDeleteTip,
|
||||
showSelectedIcon,
|
||||
selectedCredentialId,
|
||||
}: CredentialItemProps) => {
|
||||
@ -43,6 +47,9 @@ const CredentialItem = ({
|
||||
const showAction = useMemo(() => {
|
||||
return !(disableRename && disableEdit && disableDelete)
|
||||
}, [disableRename, disableEdit, disableDelete])
|
||||
const disableDeleteWhenSelected = useMemo(() => {
|
||||
return disableDeleteButShowAction && selectedCredentialId === credential.credential_id
|
||||
}, [disableDeleteButShowAction, selectedCredentialId, credential.credential_id])
|
||||
|
||||
const Item = (
|
||||
<div
|
||||
@ -104,16 +111,21 @@ const CredentialItem = ({
|
||||
}
|
||||
{
|
||||
!disableDelete && !credential.from_enterprise && (
|
||||
<Tooltip popupContent={t('common.operation.delete')}>
|
||||
<Tooltip popupContent={disableDeleteWhenSelected ? disableDeleteTip : t('common.operation.delete')}>
|
||||
<ActionButton
|
||||
className='hover:bg-transparent'
|
||||
disabled={disabled}
|
||||
onClick={(e) => {
|
||||
if (disabled || disableDeleteWhenSelected)
|
||||
return
|
||||
e.stopPropagation()
|
||||
onDelete?.(credential)
|
||||
}}
|
||||
>
|
||||
<RiDeleteBinLine className='h-4 w-4 text-text-tertiary hover:text-text-destructive' />
|
||||
<RiDeleteBinLine className={cn(
|
||||
'h-4 w-4 text-text-tertiary',
|
||||
!disableDeleteWhenSelected && 'hover:text-text-destructive',
|
||||
disableDeleteWhenSelected && 'opacity-50',
|
||||
)} />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
)
|
||||
|
||||
@ -1,12 +1,11 @@
|
||||
import {
|
||||
Fragment,
|
||||
memo,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import {
|
||||
RiAddLine,
|
||||
RiEqualizer2Line,
|
||||
} from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
@ -25,6 +24,7 @@ import type {
|
||||
Credential,
|
||||
CustomConfigurationModelFixedFields,
|
||||
CustomModel,
|
||||
ModelModalModeEnum,
|
||||
ModelProvider,
|
||||
} from '../../declarations'
|
||||
import { useAuth } from '../hooks'
|
||||
@ -34,15 +34,20 @@ type AuthorizedProps = {
|
||||
provider: ModelProvider,
|
||||
configurationMethod: ConfigurationMethodEnum,
|
||||
currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields,
|
||||
isModelCredential?: boolean
|
||||
authParams?: {
|
||||
isModelCredential?: boolean
|
||||
onUpdate?: (newPayload?: any, formValues?: Record<string, any>) => void
|
||||
onRemove?: (credentialId: string) => void
|
||||
mode?: ModelModalModeEnum
|
||||
}
|
||||
items: {
|
||||
title?: string
|
||||
model?: CustomModel
|
||||
selectedCredential?: Credential
|
||||
credentials: Credential[]
|
||||
}[]
|
||||
selectedCredential?: Credential
|
||||
disabled?: boolean
|
||||
renderTrigger?: (open?: boolean) => React.ReactNode
|
||||
renderTrigger: (open?: boolean) => React.ReactNode
|
||||
isOpen?: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
offset?: PortalToFollowElemOptions['offset']
|
||||
@ -50,18 +55,22 @@ type AuthorizedProps = {
|
||||
triggerPopupSameWidth?: boolean
|
||||
popupClassName?: string
|
||||
showItemSelectedIcon?: boolean
|
||||
onUpdate?: () => void
|
||||
onItemClick?: (credential: Credential, model?: CustomModel) => void
|
||||
enableAddModelCredential?: boolean
|
||||
bottomAddModelCredentialText?: string
|
||||
triggerOnlyOpenModal?: boolean
|
||||
hideAddAction?: boolean
|
||||
disableItemClick?: boolean
|
||||
popupTitle?: string
|
||||
showModelTitle?: boolean
|
||||
disableDeleteButShowAction?: boolean
|
||||
disableDeleteTip?: string
|
||||
}
|
||||
const Authorized = ({
|
||||
provider,
|
||||
configurationMethod,
|
||||
currentCustomConfigurationModelFixedFields,
|
||||
items,
|
||||
isModelCredential,
|
||||
selectedCredential,
|
||||
authParams,
|
||||
disabled,
|
||||
renderTrigger,
|
||||
isOpen,
|
||||
@ -71,10 +80,14 @@ const Authorized = ({
|
||||
triggerPopupSameWidth = false,
|
||||
popupClassName,
|
||||
showItemSelectedIcon,
|
||||
onUpdate,
|
||||
onItemClick,
|
||||
enableAddModelCredential,
|
||||
bottomAddModelCredentialText,
|
||||
triggerOnlyOpenModal,
|
||||
hideAddAction,
|
||||
disableItemClick,
|
||||
popupTitle,
|
||||
showModelTitle,
|
||||
disableDeleteButShowAction,
|
||||
disableDeleteTip,
|
||||
}: AuthorizedProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [isLocalOpen, setIsLocalOpen] = useState(false)
|
||||
@ -85,6 +98,12 @@ const Authorized = ({
|
||||
|
||||
setIsLocalOpen(open)
|
||||
}, [onOpenChange])
|
||||
const {
|
||||
isModelCredential,
|
||||
onUpdate,
|
||||
onRemove,
|
||||
mode,
|
||||
} = authParams || {}
|
||||
const {
|
||||
openConfirmDelete,
|
||||
closeConfirmDelete,
|
||||
@ -93,7 +112,17 @@ const Authorized = ({
|
||||
handleConfirmDelete,
|
||||
deleteCredentialId,
|
||||
handleOpenModal,
|
||||
} = useAuth(provider, configurationMethod, currentCustomConfigurationModelFixedFields, isModelCredential, onUpdate)
|
||||
} = useAuth(
|
||||
provider,
|
||||
configurationMethod,
|
||||
currentCustomConfigurationModelFixedFields,
|
||||
{
|
||||
isModelCredential,
|
||||
onUpdate,
|
||||
onRemove,
|
||||
mode,
|
||||
},
|
||||
)
|
||||
|
||||
const handleEdit = useCallback((credential?: Credential, model?: CustomModel) => {
|
||||
handleOpenModal(credential, model)
|
||||
@ -101,28 +130,18 @@ const Authorized = ({
|
||||
}, [handleOpenModal, setMergedIsOpen])
|
||||
|
||||
const handleItemClick = useCallback((credential: Credential, model?: CustomModel) => {
|
||||
if (disableItemClick)
|
||||
return
|
||||
|
||||
if (onItemClick)
|
||||
onItemClick(credential, model)
|
||||
else
|
||||
handleActiveCredential(credential, model)
|
||||
|
||||
setMergedIsOpen(false)
|
||||
}, [handleActiveCredential, onItemClick, setMergedIsOpen])
|
||||
}, [handleActiveCredential, onItemClick, setMergedIsOpen, disableItemClick])
|
||||
const notAllowCustomCredential = provider.allow_custom_token === false
|
||||
|
||||
const Trigger = useMemo(() => {
|
||||
const Item = (
|
||||
<Button
|
||||
className='grow'
|
||||
size='small'
|
||||
>
|
||||
<RiEqualizer2Line className='mr-1 h-3.5 w-3.5' />
|
||||
{t('common.operation.config')}
|
||||
</Button>
|
||||
)
|
||||
return Item
|
||||
}, [t])
|
||||
|
||||
return (
|
||||
<>
|
||||
<PortalToFollowElem
|
||||
@ -134,63 +153,79 @@ const Authorized = ({
|
||||
>
|
||||
<PortalToFollowElemTrigger
|
||||
onClick={() => {
|
||||
if (triggerOnlyOpenModal) {
|
||||
handleOpenModal()
|
||||
return
|
||||
}
|
||||
|
||||
setMergedIsOpen(!mergedIsOpen)
|
||||
}}
|
||||
asChild
|
||||
>
|
||||
{
|
||||
renderTrigger
|
||||
? renderTrigger(mergedIsOpen)
|
||||
: Trigger
|
||||
}
|
||||
{renderTrigger(mergedIsOpen)}
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-[100]'>
|
||||
<div className={cn(
|
||||
'w-[360px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg',
|
||||
'w-[360px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-[5px]',
|
||||
popupClassName,
|
||||
)}>
|
||||
{
|
||||
popupTitle && (
|
||||
<div className='system-xs-medium px-3 pb-0.5 pt-[10px] text-text-tertiary'>
|
||||
{popupTitle}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div className='max-h-[304px] overflow-y-auto'>
|
||||
{
|
||||
items.map((item, index) => (
|
||||
<AuthorizedItem
|
||||
key={index}
|
||||
title={item.title}
|
||||
model={item.model}
|
||||
credentials={item.credentials}
|
||||
disabled={disabled}
|
||||
onDelete={openConfirmDelete}
|
||||
onEdit={handleEdit}
|
||||
showItemSelectedIcon={showItemSelectedIcon}
|
||||
selectedCredentialId={selectedCredential?.credential_id}
|
||||
onItemClick={handleItemClick}
|
||||
enableAddModelCredential={enableAddModelCredential}
|
||||
notAllowCustomCredential={notAllowCustomCredential}
|
||||
/>
|
||||
<Fragment key={index}>
|
||||
<AuthorizedItem
|
||||
provider={provider}
|
||||
title={item.title}
|
||||
model={item.model}
|
||||
credentials={item.credentials}
|
||||
disabled={disabled}
|
||||
onDelete={openConfirmDelete}
|
||||
disableDeleteButShowAction={disableDeleteButShowAction}
|
||||
disableDeleteTip={disableDeleteTip}
|
||||
onEdit={handleEdit}
|
||||
showItemSelectedIcon={showItemSelectedIcon}
|
||||
selectedCredentialId={item.selectedCredential?.credential_id}
|
||||
onItemClick={handleItemClick}
|
||||
showModelTitle={showModelTitle}
|
||||
/>
|
||||
{
|
||||
index !== items.length - 1 && (
|
||||
<div className='h-[1px] bg-divider-subtle'></div>
|
||||
)
|
||||
}
|
||||
</Fragment>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
<div className='h-[1px] bg-divider-subtle'></div>
|
||||
{
|
||||
isModelCredential && !notAllowCustomCredential && (
|
||||
isModelCredential && !notAllowCustomCredential && !hideAddAction && (
|
||||
<div
|
||||
onClick={() => handleEdit(
|
||||
undefined,
|
||||
currentCustomConfigurationModelFixedFields
|
||||
? {
|
||||
model: currentCustomConfigurationModelFixedFields.__model_name,
|
||||
model_type: currentCustomConfigurationModelFixedFields.__model_type,
|
||||
}
|
||||
: undefined,
|
||||
? {
|
||||
model: currentCustomConfigurationModelFixedFields.__model_name,
|
||||
model_type: currentCustomConfigurationModelFixedFields.__model_type,
|
||||
}
|
||||
: undefined,
|
||||
)}
|
||||
className='system-xs-medium flex h-[30px] cursor-pointer items-center px-3 text-text-accent-light-mode-only'
|
||||
className='system-xs-medium flex h-[40px] cursor-pointer items-center px-3 text-text-accent-light-mode-only'
|
||||
>
|
||||
<RiAddLine className='mr-1 h-4 w-4' />
|
||||
{bottomAddModelCredentialText ?? t('common.modelProvider.auth.addModelCredential')}
|
||||
{t('common.modelProvider.auth.addModelCredential')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
!isModelCredential && !notAllowCustomCredential && (
|
||||
!isModelCredential && !notAllowCustomCredential && !hideAddAction && (
|
||||
<div className='p-2'>
|
||||
<Button
|
||||
onClick={() => handleEdit()}
|
||||
|
||||
@ -25,7 +25,7 @@ const ConfigModel = ({
|
||||
if (loadBalancingInvalid) {
|
||||
return (
|
||||
<div
|
||||
className='system-2xs-medium-uppercase relative flex h-[18px] items-center rounded-[5px] border border-text-warning bg-components-badge-bg-dimm px-1.5 text-text-warning'
|
||||
className='system-2xs-medium-uppercase relative flex h-[18px] cursor-pointer items-center rounded-[5px] border border-text-warning bg-components-badge-bg-dimm px-1.5 text-text-warning'
|
||||
onClick={onClick}
|
||||
>
|
||||
<RiScales3Line className='mr-0.5 h-3 w-3' />
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useMemo,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
@ -16,24 +15,18 @@ import type {
|
||||
} from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { ConfigurationMethodEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import Authorized from './authorized'
|
||||
import { useAuth, useCredentialStatus } from './hooks'
|
||||
import { useCredentialStatus } from './hooks'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type ConfigProviderProps = {
|
||||
provider: ModelProvider,
|
||||
configurationMethod: ConfigurationMethodEnum,
|
||||
currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields,
|
||||
}
|
||||
const ConfigProvider = ({
|
||||
provider,
|
||||
configurationMethod,
|
||||
currentCustomConfigurationModelFixedFields,
|
||||
}: ConfigProviderProps) => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
handleOpenModal,
|
||||
} = useAuth(provider, configurationMethod, currentCustomConfigurationModelFixedFields)
|
||||
const {
|
||||
hasCredential,
|
||||
authorized,
|
||||
@ -42,23 +35,20 @@ const ConfigProvider = ({
|
||||
available_credentials,
|
||||
} = useCredentialStatus(provider)
|
||||
const notAllowCustomCredential = provider.allow_custom_token === false
|
||||
const handleClick = useCallback(() => {
|
||||
if (!hasCredential && !notAllowCustomCredential)
|
||||
handleOpenModal()
|
||||
}, [handleOpenModal, hasCredential, notAllowCustomCredential])
|
||||
const ButtonComponent = useMemo(() => {
|
||||
|
||||
const renderTrigger = useCallback(() => {
|
||||
const Item = (
|
||||
<Button
|
||||
className={cn('grow', notAllowCustomCredential && 'cursor-not-allowed opacity-50')}
|
||||
className='grow'
|
||||
size='small'
|
||||
onClick={handleClick}
|
||||
variant={!authorized ? 'secondary-accent' : 'secondary'}
|
||||
>
|
||||
<RiEqualizer2Line className='mr-1 h-3.5 w-3.5' />
|
||||
{t('common.operation.setup')}
|
||||
{hasCredential && t('common.operation.config')}
|
||||
{!hasCredential && t('common.operation.setup')}
|
||||
</Button>
|
||||
)
|
||||
if (notAllowCustomCredential) {
|
||||
if (notAllowCustomCredential && !hasCredential) {
|
||||
return (
|
||||
<Tooltip
|
||||
asChild
|
||||
@ -69,26 +59,27 @@ const ConfigProvider = ({
|
||||
)
|
||||
}
|
||||
return Item
|
||||
}, [handleClick, authorized, notAllowCustomCredential, t])
|
||||
|
||||
if (!hasCredential)
|
||||
return ButtonComponent
|
||||
}, [authorized, hasCredential, notAllowCustomCredential, t])
|
||||
|
||||
return (
|
||||
<Authorized
|
||||
provider={provider}
|
||||
configurationMethod={ConfigurationMethodEnum.predefinedModel}
|
||||
currentCustomConfigurationModelFixedFields={currentCustomConfigurationModelFixedFields}
|
||||
items={[
|
||||
{
|
||||
title: t('common.modelProvider.auth.apiKeys'),
|
||||
credentials: available_credentials ?? [],
|
||||
selectedCredential: {
|
||||
credential_id: current_credential_id ?? '',
|
||||
credential_name: current_credential_name ?? '',
|
||||
},
|
||||
},
|
||||
]}
|
||||
selectedCredential={{
|
||||
credential_id: current_credential_id ?? '',
|
||||
credential_name: current_credential_name ?? '',
|
||||
}}
|
||||
showItemSelectedIcon
|
||||
showModelTitle
|
||||
renderTrigger={renderTrigger}
|
||||
triggerOnlyOpenModal={!hasCredential && !notAllowCustomCredential}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@ -0,0 +1,115 @@
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiAddLine,
|
||||
RiArrowDownSLine,
|
||||
} from '@remixicon/react'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import type { Credential } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import CredentialItem from './authorized/credential-item'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
|
||||
type CredentialSelectorProps = {
|
||||
selectedCredential?: Credential & { addNewCredential?: boolean }
|
||||
credentials: Credential[]
|
||||
onSelect: (credential: Credential & { addNewCredential?: boolean }) => void
|
||||
disabled?: boolean
|
||||
notAllowAddNewCredential?: boolean
|
||||
}
|
||||
const CredentialSelector = ({
|
||||
selectedCredential,
|
||||
credentials,
|
||||
onSelect,
|
||||
disabled,
|
||||
notAllowAddNewCredential,
|
||||
}: CredentialSelectorProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
const handleSelect = useCallback((credential: Credential & { addNewCredential?: boolean }) => {
|
||||
setOpen(false)
|
||||
onSelect(credential)
|
||||
}, [onSelect])
|
||||
const handleAddNewCredential = useCallback(() => {
|
||||
handleSelect({
|
||||
credential_id: '__add_new_credential',
|
||||
addNewCredential: true,
|
||||
credential_name: t('common.modelProvider.auth.addNewModelCredential'),
|
||||
})
|
||||
}, [handleSelect, t])
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
triggerPopupSameWidth
|
||||
>
|
||||
<PortalToFollowElemTrigger asChild onClick={() => !disabled && setOpen(v => !v)}>
|
||||
<div className='system-sm-regular flex h-8 w-full items-center justify-between rounded-lg bg-components-input-bg-normal px-2'>
|
||||
{
|
||||
selectedCredential && (
|
||||
<div className='flex items-center'>
|
||||
{
|
||||
!selectedCredential.addNewCredential && <Indicator className='ml-1 mr-2 shrink-0' />
|
||||
}
|
||||
<div className='system-sm-regular truncate text-components-input-text-filled' title={selectedCredential.credential_name}>{selectedCredential.credential_name}</div>
|
||||
{
|
||||
selectedCredential.from_enterprise && (
|
||||
<Badge className='shrink-0'>Enterprise</Badge>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
!selectedCredential && (
|
||||
<div className='system-sm-regular grow truncate text-components-input-text-placeholder'>{t('common.modelProvider.auth.selectModelCredential')}</div>
|
||||
)
|
||||
}
|
||||
<RiArrowDownSLine className='h-4 w-4 text-text-quaternary' />
|
||||
</div>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-[100]'>
|
||||
<div className='border-ccomponents-panel-border rounded-xl border-[0.5px] bg-components-panel-bg-blur shadow-lg'>
|
||||
<div className='max-h-[320px] overflow-y-auto p-1'>
|
||||
{
|
||||
credentials.map(credential => (
|
||||
<CredentialItem
|
||||
key={credential.credential_id}
|
||||
credential={credential}
|
||||
disableDelete
|
||||
disableEdit
|
||||
disableRename
|
||||
onItemClick={handleSelect}
|
||||
showSelectedIcon
|
||||
selectedCredentialId={selectedCredential?.credential_id}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
{
|
||||
!notAllowAddNewCredential && (
|
||||
<div
|
||||
className='system-xs-medium flex h-10 cursor-pointer items-center border-t border-t-divider-subtle px-7 text-text-accent-light-mode-only'
|
||||
onClick={handleAddNewCredential}
|
||||
>
|
||||
<RiAddLine className='mr-1 h-4 w-4' />
|
||||
{t('common.modelProvider.auth.addNewModelCredential')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(CredentialSelector)
|
||||
@ -17,7 +17,7 @@ import type {
|
||||
|
||||
export const useGetCredential = (provider: string, isModelCredential?: boolean, credentialId?: string, model?: CustomModel, configFrom?: string) => {
|
||||
const providerData = useGetProviderCredential(!isModelCredential && !!credentialId, provider, credentialId)
|
||||
const modelData = useGetModelCredential(!!isModelCredential && !!credentialId, provider, credentialId, model?.model, model?.model_type, configFrom)
|
||||
const modelData = useGetModelCredential(!!isModelCredential && (!!credentialId || !!model), provider, credentialId, model?.model, model?.model_type, configFrom)
|
||||
return isModelCredential ? modelData : providerData
|
||||
}
|
||||
|
||||
|
||||
@ -11,20 +11,32 @@ import type {
|
||||
Credential,
|
||||
CustomConfigurationModelFixedFields,
|
||||
CustomModel,
|
||||
ModelModalModeEnum,
|
||||
ModelProvider,
|
||||
} from '../../declarations'
|
||||
import {
|
||||
useModelModalHandler,
|
||||
useRefreshModel,
|
||||
} from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import { useDeleteModel } from '@/service/use-models'
|
||||
|
||||
export const useAuth = (
|
||||
provider: ModelProvider,
|
||||
configurationMethod: ConfigurationMethodEnum,
|
||||
currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields,
|
||||
isModelCredential?: boolean,
|
||||
onUpdate?: () => void,
|
||||
extra: {
|
||||
isModelCredential?: boolean,
|
||||
onUpdate?: (newPayload?: any, formValues?: Record<string, any>) => void,
|
||||
onRemove?: (credentialId: string) => void,
|
||||
mode?: ModelModalModeEnum,
|
||||
} = {},
|
||||
) => {
|
||||
const {
|
||||
isModelCredential,
|
||||
onUpdate,
|
||||
onRemove,
|
||||
mode,
|
||||
} = extra
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useToastContext()
|
||||
const {
|
||||
@ -33,22 +45,30 @@ export const useAuth = (
|
||||
getEditCredentialService,
|
||||
getAddCredentialService,
|
||||
} = useAuthService(provider.provider)
|
||||
const { mutateAsync: deleteModelService } = useDeleteModel(provider.provider)
|
||||
const handleOpenModelModal = useModelModalHandler()
|
||||
const { handleRefreshModel } = useRefreshModel()
|
||||
const pendingOperationCredentialId = useRef<string | null>(null)
|
||||
const pendingOperationModel = useRef<CustomModel | null>(null)
|
||||
const [deleteCredentialId, setDeleteCredentialId] = useState<string | null>(null)
|
||||
const handleSetDeleteCredentialId = useCallback((credentialId: string | null) => {
|
||||
setDeleteCredentialId(credentialId)
|
||||
pendingOperationCredentialId.current = credentialId
|
||||
}, [])
|
||||
const pendingOperationModel = useRef<CustomModel | null>(null)
|
||||
const [deleteModel, setDeleteModel] = useState<CustomModel | null>(null)
|
||||
const handleSetDeleteModel = useCallback((model: CustomModel | null) => {
|
||||
setDeleteModel(model)
|
||||
pendingOperationModel.current = model
|
||||
}, [])
|
||||
const openConfirmDelete = useCallback((credential?: Credential, model?: CustomModel) => {
|
||||
if (credential)
|
||||
pendingOperationCredentialId.current = credential.credential_id
|
||||
handleSetDeleteCredentialId(credential.credential_id)
|
||||
if (model)
|
||||
pendingOperationModel.current = model
|
||||
|
||||
setDeleteCredentialId(pendingOperationCredentialId.current)
|
||||
handleSetDeleteModel(model)
|
||||
}, [])
|
||||
const closeConfirmDelete = useCallback(() => {
|
||||
setDeleteCredentialId(null)
|
||||
pendingOperationCredentialId.current = null
|
||||
handleSetDeleteCredentialId(null)
|
||||
handleSetDeleteModel(null)
|
||||
}, [])
|
||||
const [doingAction, setDoingAction] = useState(false)
|
||||
const doingActionRef = useRef(doingAction)
|
||||
@ -70,45 +90,49 @@ export const useAuth = (
|
||||
type: 'success',
|
||||
message: t('common.api.actionSuccess'),
|
||||
})
|
||||
onUpdate?.()
|
||||
handleRefreshModel(provider, configurationMethod, undefined)
|
||||
}
|
||||
finally {
|
||||
handleSetDoingAction(false)
|
||||
}
|
||||
}, [getActiveCredentialService, onUpdate, notify, t, handleSetDoingAction])
|
||||
}, [getActiveCredentialService, notify, t, handleSetDoingAction])
|
||||
const handleConfirmDelete = useCallback(async () => {
|
||||
if (doingActionRef.current)
|
||||
return
|
||||
if (!pendingOperationCredentialId.current) {
|
||||
setDeleteCredentialId(null)
|
||||
if (!pendingOperationCredentialId.current && !pendingOperationModel.current) {
|
||||
closeConfirmDelete()
|
||||
return
|
||||
}
|
||||
try {
|
||||
handleSetDoingAction(true)
|
||||
await getDeleteCredentialService(!!isModelCredential)({
|
||||
credential_id: pendingOperationCredentialId.current,
|
||||
model: pendingOperationModel.current?.model,
|
||||
model_type: pendingOperationModel.current?.model_type,
|
||||
})
|
||||
let payload: any = {}
|
||||
if (pendingOperationCredentialId.current) {
|
||||
payload = {
|
||||
credential_id: pendingOperationCredentialId.current,
|
||||
model: pendingOperationModel.current?.model,
|
||||
model_type: pendingOperationModel.current?.model_type,
|
||||
}
|
||||
await getDeleteCredentialService(!!isModelCredential)(payload)
|
||||
}
|
||||
if (!pendingOperationCredentialId.current && pendingOperationModel.current) {
|
||||
payload = {
|
||||
model: pendingOperationModel.current.model,
|
||||
model_type: pendingOperationModel.current.model_type,
|
||||
}
|
||||
await deleteModelService(payload)
|
||||
}
|
||||
notify({
|
||||
type: 'success',
|
||||
message: t('common.api.actionSuccess'),
|
||||
})
|
||||
onUpdate?.()
|
||||
handleRefreshModel(provider, configurationMethod, undefined)
|
||||
setDeleteCredentialId(null)
|
||||
pendingOperationCredentialId.current = null
|
||||
pendingOperationModel.current = null
|
||||
onRemove?.(pendingOperationCredentialId.current ?? '')
|
||||
closeConfirmDelete()
|
||||
}
|
||||
finally {
|
||||
handleSetDoingAction(false)
|
||||
}
|
||||
}, [onUpdate, notify, t, handleSetDoingAction, getDeleteCredentialService, isModelCredential])
|
||||
const handleAddCredential = useCallback((model?: CustomModel) => {
|
||||
if (model)
|
||||
pendingOperationModel.current = model
|
||||
}, [])
|
||||
}, [notify, t, handleSetDoingAction, getDeleteCredentialService, isModelCredential, closeConfirmDelete, handleRefreshModel, provider, configurationMethod, deleteModelService])
|
||||
const handleSaveCredential = useCallback(async (payload: Record<string, any>) => {
|
||||
if (doingActionRef.current)
|
||||
return
|
||||
@ -123,24 +147,35 @@ export const useAuth = (
|
||||
|
||||
if (res.result === 'success') {
|
||||
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
|
||||
onUpdate?.()
|
||||
handleRefreshModel(provider, configurationMethod, undefined)
|
||||
}
|
||||
}
|
||||
finally {
|
||||
handleSetDoingAction(false)
|
||||
}
|
||||
}, [onUpdate, notify, t, handleSetDoingAction, getEditCredentialService, getAddCredentialService])
|
||||
}, [notify, t, handleSetDoingAction, getEditCredentialService, getAddCredentialService])
|
||||
const handleOpenModal = useCallback((credential?: Credential, model?: CustomModel) => {
|
||||
handleOpenModelModal(
|
||||
provider,
|
||||
configurationMethod,
|
||||
currentCustomConfigurationModelFixedFields,
|
||||
isModelCredential,
|
||||
credential,
|
||||
model,
|
||||
onUpdate,
|
||||
{
|
||||
isModelCredential,
|
||||
credential,
|
||||
model,
|
||||
onUpdate,
|
||||
mode,
|
||||
},
|
||||
)
|
||||
}, [handleOpenModelModal, provider, configurationMethod, currentCustomConfigurationModelFixedFields, isModelCredential, onUpdate])
|
||||
}, [
|
||||
handleOpenModelModal,
|
||||
provider,
|
||||
configurationMethod,
|
||||
currentCustomConfigurationModelFixedFields,
|
||||
isModelCredential,
|
||||
onUpdate,
|
||||
mode,
|
||||
])
|
||||
|
||||
return {
|
||||
pendingOperationCredentialId,
|
||||
@ -150,8 +185,8 @@ export const useAuth = (
|
||||
doingAction,
|
||||
handleActiveCredential,
|
||||
handleConfirmDelete,
|
||||
handleAddCredential,
|
||||
deleteCredentialId,
|
||||
deleteModel,
|
||||
handleSaveCredential,
|
||||
handleOpenModal,
|
||||
}
|
||||
|
||||
@ -7,3 +7,9 @@ export const useCustomModels = (provider: ModelProvider) => {
|
||||
|
||||
return custom_models || []
|
||||
}
|
||||
|
||||
export const useCanAddedModels = (provider: ModelProvider) => {
|
||||
const { can_added_models } = provider.custom_configuration
|
||||
|
||||
return can_added_models || []
|
||||
}
|
||||
|
||||
@ -3,7 +3,6 @@ import { useTranslation } from 'react-i18next'
|
||||
import type {
|
||||
Credential,
|
||||
CustomModelCredential,
|
||||
ModelLoadBalancingConfig,
|
||||
ModelProvider,
|
||||
} from '../../declarations'
|
||||
import {
|
||||
@ -18,7 +17,6 @@ export const useModelFormSchemas = (
|
||||
credentials?: Record<string, any>,
|
||||
credential?: Credential,
|
||||
model?: CustomModelCredential,
|
||||
draftConfig?: ModelLoadBalancingConfig,
|
||||
) => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
@ -27,26 +25,15 @@ export const useModelFormSchemas = (
|
||||
model_credential_schema,
|
||||
} = provider
|
||||
const formSchemas = useMemo(() => {
|
||||
const modelTypeSchema = genModelTypeFormSchema(supported_model_types)
|
||||
const modelNameSchema = genModelNameFormSchema(model_credential_schema?.model)
|
||||
if (!!model) {
|
||||
modelTypeSchema.disabled = true
|
||||
modelNameSchema.disabled = true
|
||||
}
|
||||
return providerFormSchemaPredefined
|
||||
? provider_credential_schema.credential_form_schemas
|
||||
: [
|
||||
modelTypeSchema,
|
||||
modelNameSchema,
|
||||
...(draftConfig?.enabled ? [] : model_credential_schema.credential_form_schemas),
|
||||
]
|
||||
: model_credential_schema.credential_form_schemas
|
||||
}, [
|
||||
providerFormSchemaPredefined,
|
||||
provider_credential_schema?.credential_form_schemas,
|
||||
supported_model_types,
|
||||
model_credential_schema?.credential_form_schemas,
|
||||
model_credential_schema?.model,
|
||||
draftConfig?.enabled,
|
||||
model,
|
||||
])
|
||||
|
||||
@ -55,7 +42,7 @@ export const useModelFormSchemas = (
|
||||
type: FormTypeEnum.textInput,
|
||||
variable: '__authorization_name__',
|
||||
label: t('plugin.auth.authorizationName'),
|
||||
required: true,
|
||||
required: false,
|
||||
}
|
||||
|
||||
return [
|
||||
@ -65,7 +52,10 @@ export const useModelFormSchemas = (
|
||||
}, [formSchemas, t])
|
||||
|
||||
const formValues = useMemo(() => {
|
||||
let result = {}
|
||||
let result: any = {}
|
||||
formSchemas.forEach((schema) => {
|
||||
result[schema.variable] = schema.default
|
||||
})
|
||||
if (credential) {
|
||||
result = { ...result, __authorization_name__: credential?.credential_name }
|
||||
if (credentials)
|
||||
@ -74,10 +64,35 @@ export const useModelFormSchemas = (
|
||||
if (model)
|
||||
result = { ...result, __model_name: model?.model, __model_type: model?.model_type }
|
||||
return result
|
||||
}, [credentials, credential, model])
|
||||
}, [credentials, credential, model, formSchemas])
|
||||
|
||||
const modelNameAndTypeFormSchemas = useMemo(() => {
|
||||
if (providerFormSchemaPredefined)
|
||||
return []
|
||||
|
||||
const modelNameSchema = genModelNameFormSchema(model_credential_schema?.model)
|
||||
const modelTypeSchema = genModelTypeFormSchema(supported_model_types)
|
||||
return [
|
||||
modelNameSchema,
|
||||
modelTypeSchema,
|
||||
]
|
||||
}, [supported_model_types, model_credential_schema?.model, providerFormSchemaPredefined])
|
||||
|
||||
const modelNameAndTypeFormValues = useMemo(() => {
|
||||
let result = {}
|
||||
if (providerFormSchemaPredefined)
|
||||
return result
|
||||
|
||||
if (model)
|
||||
result = { ...result, __model_name: model?.model, __model_type: model?.model_type }
|
||||
|
||||
return result
|
||||
}, [model, providerFormSchemaPredefined])
|
||||
|
||||
return {
|
||||
formSchemas: formSchemasWithAuthorizationName,
|
||||
formValues,
|
||||
modelNameAndTypeFormSchemas,
|
||||
modelNameAndTypeFormValues,
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,3 +4,5 @@ export { default as AddCredentialInLoadBalancing } from './add-credential-in-loa
|
||||
export { default as AddCustomModel } from './add-custom-model'
|
||||
export { default as ConfigProvider } from './config-provider'
|
||||
export { default as ConfigModel } from './config-model'
|
||||
export { default as ManageCustomModelCredentials } from './manage-custom-model-credentials'
|
||||
export { default as CredentialSelector } from './credential-selector'
|
||||
|
||||
@ -0,0 +1,82 @@
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
Button,
|
||||
} from '@/app/components/base/button'
|
||||
import type {
|
||||
CustomConfigurationModelFixedFields,
|
||||
ModelProvider,
|
||||
} from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import {
|
||||
ConfigurationMethodEnum,
|
||||
ModelModalModeEnum,
|
||||
} from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import Authorized from './authorized'
|
||||
import {
|
||||
useCustomModels,
|
||||
} from './hooks'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type ManageCustomModelCredentialsProps = {
|
||||
provider: ModelProvider,
|
||||
currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields,
|
||||
}
|
||||
const ManageCustomModelCredentials = ({
|
||||
provider,
|
||||
currentCustomConfigurationModelFixedFields,
|
||||
}: ManageCustomModelCredentialsProps) => {
|
||||
const { t } = useTranslation()
|
||||
const customModels = useCustomModels(provider)
|
||||
const noModels = !customModels.length
|
||||
|
||||
const renderTrigger = useCallback((open?: boolean) => {
|
||||
const Item = (
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='small'
|
||||
className={cn(
|
||||
'mr-0.5 text-text-tertiary',
|
||||
open && 'bg-components-button-ghost-bg-hover',
|
||||
)}
|
||||
>
|
||||
{t('common.modelProvider.auth.manageCredentials')}
|
||||
</Button>
|
||||
)
|
||||
return Item
|
||||
}, [t])
|
||||
|
||||
if (noModels)
|
||||
return null
|
||||
|
||||
return (
|
||||
<Authorized
|
||||
provider={provider}
|
||||
configurationMethod={ConfigurationMethodEnum.customizableModel}
|
||||
currentCustomConfigurationModelFixedFields={currentCustomConfigurationModelFixedFields}
|
||||
items={customModels.map(model => ({
|
||||
model,
|
||||
credentials: model.available_model_credentials ?? [],
|
||||
selectedCredential: model.current_credential_id ? {
|
||||
credential_id: model.current_credential_id,
|
||||
credential_name: model.current_credential_name,
|
||||
} : undefined,
|
||||
}))}
|
||||
renderTrigger={renderTrigger}
|
||||
authParams={{
|
||||
isModelCredential: true,
|
||||
mode: ModelModalModeEnum.configModelCredential,
|
||||
}}
|
||||
hideAddAction
|
||||
disableItemClick
|
||||
popupTitle={t('common.modelProvider.auth.customModelCredentials')}
|
||||
showModelTitle
|
||||
disableDeleteButShowAction
|
||||
disableDeleteTip={t('common.modelProvider.auth.customModelCredentialsDeleteTip')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(ManageCustomModelCredentials)
|
||||
@ -13,7 +13,7 @@ import type {
|
||||
CustomModel,
|
||||
ModelProvider,
|
||||
} from '../declarations'
|
||||
import { ConfigurationMethodEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { ConfigurationMethodEnum, ModelModalModeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import cn from '@/utils/classnames'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
@ -24,6 +24,8 @@ type SwitchCredentialInLoadBalancingProps = {
|
||||
credentials?: Credential[]
|
||||
customModelCredential?: Credential
|
||||
setCustomModelCredential: Dispatch<SetStateAction<Credential | undefined>>
|
||||
onUpdate?: (payload?: any, formValues?: Record<string, any>) => void
|
||||
onRemove?: (credentialId: string) => void
|
||||
}
|
||||
const SwitchCredentialInLoadBalancing = ({
|
||||
provider,
|
||||
@ -31,6 +33,8 @@ const SwitchCredentialInLoadBalancing = ({
|
||||
customModelCredential,
|
||||
setCustomModelCredential,
|
||||
credentials,
|
||||
onUpdate,
|
||||
onRemove,
|
||||
}: SwitchCredentialInLoadBalancingProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
@ -94,27 +98,31 @@ const SwitchCredentialInLoadBalancing = ({
|
||||
<Authorized
|
||||
provider={provider}
|
||||
configurationMethod={ConfigurationMethodEnum.customizableModel}
|
||||
currentCustomConfigurationModelFixedFields={model ? {
|
||||
__model_name: model.model,
|
||||
__model_type: model.model_type,
|
||||
} : undefined}
|
||||
authParams={{
|
||||
isModelCredential: true,
|
||||
mode: ModelModalModeEnum.configModelCredential,
|
||||
onUpdate,
|
||||
onRemove,
|
||||
}}
|
||||
items={[
|
||||
{
|
||||
title: t('common.modelProvider.auth.modelCredentials'),
|
||||
model,
|
||||
credentials: credentials || [],
|
||||
selectedCredential: customModelCredential ? {
|
||||
credential_id: customModelCredential?.credential_id || '',
|
||||
credential_name: customModelCredential?.credential_name || '',
|
||||
} : undefined,
|
||||
},
|
||||
]}
|
||||
renderTrigger={renderTrigger}
|
||||
onItemClick={handleItemClick}
|
||||
isModelCredential
|
||||
enableAddModelCredential
|
||||
bottomAddModelCredentialText={t('common.modelProvider.auth.addModelCredential')}
|
||||
selectedCredential={
|
||||
customModelCredential
|
||||
? {
|
||||
credential_id: customModelCredential?.credential_id || '',
|
||||
credential_name: customModelCredential?.credential_name || '',
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
showItemSelectedIcon
|
||||
popupTitle={t('common.modelProvider.auth.modelCredentials')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@ -284,11 +284,11 @@ function Form<
|
||||
</div>
|
||||
<Radio.Group
|
||||
className='flex items-center'
|
||||
value={value[variable] === null ? undefined : (value[variable] ? 1 : 0)}
|
||||
onChange={val => handleFormChange(variable, val === 1)}
|
||||
value={value[variable]}
|
||||
onChange={val => handleFormChange(variable, val)}
|
||||
>
|
||||
<Radio value={1} className='!mr-1'>True</Radio>
|
||||
<Radio value={0}>False</Radio>
|
||||
<Radio value={true} className='!mr-1'>True</Radio>
|
||||
<Radio value={false}>False</Radio>
|
||||
</Radio.Group>
|
||||
</div>
|
||||
{fieldMoreInfo?.(formSchema)}
|
||||
|
||||
@ -0,0 +1,16 @@
|
||||
import { render } from '@testing-library/react'
|
||||
import Input from './Input'
|
||||
|
||||
test('Input renders correctly as password type with no autocomplete', () => {
|
||||
const { asFragment, getByPlaceholderText } = render(
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="API Key"
|
||||
onChange={jest.fn()}
|
||||
/>,
|
||||
)
|
||||
const input = getByPlaceholderText('API Key')
|
||||
expect(input).toHaveAttribute('type', 'password')
|
||||
expect(input).not.toHaveAttribute('autocomplete')
|
||||
expect(asFragment()).toMatchSnapshot()
|
||||
})
|
||||
@ -13,6 +13,7 @@ type InputProps = {
|
||||
min?: number
|
||||
max?: number
|
||||
}
|
||||
|
||||
const Input: FC<InputProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
@ -32,23 +33,23 @@ const Input: FC<InputProps> = ({
|
||||
onChange(`${min}`)
|
||||
return
|
||||
}
|
||||
|
||||
if (!isNaN(maxNum) && Number.parseFloat(v) > maxNum)
|
||||
onChange(`${max}`)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='relative'>
|
||||
<input
|
||||
autoComplete="new-password"
|
||||
tabIndex={0}
|
||||
// Do not set autoComplete for security - prevents browser from storing sensitive API keys
|
||||
className={`
|
||||
block h-8 w-full appearance-none rounded-lg border border-transparent bg-components-input-bg-normal px-3 text-sm
|
||||
text-components-input-text-filled caret-primary-600 outline-none
|
||||
placeholder:text-sm placeholder:text-text-tertiary
|
||||
hover:border-components-input-border-hover hover:bg-components-input-bg-hover focus:border-components-input-border-active
|
||||
focus:bg-components-input-bg-active focus:shadow-xs
|
||||
${validated && 'pr-[30px]'}
|
||||
${className}
|
||||
${validated ? 'pr-[30px]' : ''}
|
||||
${className || ''}
|
||||
`}
|
||||
placeholder={placeholder || ''}
|
||||
onChange={e => onChange(e.target.value)}
|
||||
@ -60,13 +61,11 @@ const Input: FC<InputProps> = ({
|
||||
min={min}
|
||||
max={max}
|
||||
/>
|
||||
{
|
||||
validated && (
|
||||
<div className='absolute right-2.5 top-2.5'>
|
||||
<CheckCircle className='h-4 w-4 text-[#039855]' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{validated && (
|
||||
<div className='absolute right-2.5 top-2.5'>
|
||||
<CheckCircle className='h-4 w-4 text-[#039855]' />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -0,0 +1,24 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Input renders correctly as password type with no autocomplete 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="relative"
|
||||
>
|
||||
<input
|
||||
class="
|
||||
block h-8 w-full appearance-none rounded-lg border border-transparent bg-components-input-bg-normal px-3 text-sm
|
||||
text-components-input-text-filled caret-primary-600 outline-none
|
||||
placeholder:text-sm placeholder:text-text-tertiary
|
||||
hover:border-components-input-border-hover hover:bg-components-input-bg-hover focus:border-components-input-border-active
|
||||
focus:bg-components-input-bg-active focus:shadow-xs
|
||||
|
||||
|
||||
"
|
||||
placeholder="API Key"
|
||||
tabindex="0"
|
||||
type="password"
|
||||
/>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
@ -2,8 +2,10 @@ import type { FC } from 'react'
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -14,6 +16,7 @@ import type {
|
||||
import {
|
||||
ConfigurationMethodEnum,
|
||||
FormTypeEnum,
|
||||
ModelModalModeEnum,
|
||||
} from '../declarations'
|
||||
import {
|
||||
useLanguage,
|
||||
@ -45,16 +48,19 @@ import {
|
||||
import ModelIcon from '@/app/components/header/account-setting/model-provider-page/model-icon'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
import { useRenderI18nObject } from '@/hooks/use-i18n'
|
||||
import { CredentialSelector } from '../model-auth'
|
||||
|
||||
type ModelModalProps = {
|
||||
provider: ModelProvider
|
||||
configurateMethod: ConfigurationMethodEnum
|
||||
currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields
|
||||
onCancel: () => void
|
||||
onSave: () => void
|
||||
onSave: (formValues?: Record<string, any>) => void
|
||||
onRemove: (formValues?: Record<string, any>) => void
|
||||
model?: CustomModel
|
||||
credential?: Credential
|
||||
isModelCredential?: boolean
|
||||
mode?: ModelModalModeEnum
|
||||
}
|
||||
|
||||
const ModelModal: FC<ModelModalProps> = ({
|
||||
@ -66,6 +72,7 @@ const ModelModal: FC<ModelModalProps> = ({
|
||||
model,
|
||||
credential,
|
||||
isModelCredential,
|
||||
mode = ModelModalModeEnum.configProviderCredential,
|
||||
}) => {
|
||||
const renderI18nObject = useRenderI18nObject()
|
||||
const providerFormSchemaPredefined = configurateMethod === ConfigurationMethodEnum.predefinedModel
|
||||
@ -80,40 +87,88 @@ const ModelModal: FC<ModelModalProps> = ({
|
||||
closeConfirmDelete,
|
||||
openConfirmDelete,
|
||||
doingAction,
|
||||
} = useAuth(provider, configurateMethod, currentCustomConfigurationModelFixedFields, isModelCredential, onSave)
|
||||
handleActiveCredential,
|
||||
} = useAuth(
|
||||
provider,
|
||||
configurateMethod,
|
||||
currentCustomConfigurationModelFixedFields,
|
||||
{
|
||||
isModelCredential,
|
||||
mode,
|
||||
},
|
||||
)
|
||||
const {
|
||||
credentials: formSchemasValue,
|
||||
available_credentials,
|
||||
} = credentialData as any
|
||||
|
||||
const { isCurrentWorkspaceManager } = useAppContext()
|
||||
const isEditMode = !!formSchemasValue && isCurrentWorkspaceManager
|
||||
const { t } = useTranslation()
|
||||
const language = useLanguage()
|
||||
const {
|
||||
formSchemas,
|
||||
formValues,
|
||||
modelNameAndTypeFormSchemas,
|
||||
modelNameAndTypeFormValues,
|
||||
} = useModelFormSchemas(provider, providerFormSchemaPredefined, formSchemasValue, credential, model)
|
||||
const formRef = useRef<FormRefObject>(null)
|
||||
const formRef1 = useRef<FormRefObject>(null)
|
||||
const [selectedCredential, setSelectedCredential] = useState<Credential & { addNewCredential?: boolean } | undefined>()
|
||||
const formRef2 = useRef<FormRefObject>(null)
|
||||
const isEditMode = !!Object.keys(formValues).filter((key) => {
|
||||
return key !== '__model_name' && key !== '__model_type'
|
||||
}).length && isCurrentWorkspaceManager
|
||||
|
||||
const handleSave = useCallback(async () => {
|
||||
if (mode === ModelModalModeEnum.addCustomModelToModelList && selectedCredential && !selectedCredential?.addNewCredential) {
|
||||
handleActiveCredential(selectedCredential, model)
|
||||
onCancel()
|
||||
return
|
||||
}
|
||||
|
||||
let modelNameAndTypeIsCheckValidated = true
|
||||
let modelNameAndTypeValues: Record<string, any> = {}
|
||||
|
||||
if (mode === ModelModalModeEnum.configCustomModel) {
|
||||
const formResult = formRef1.current?.getFormValues({
|
||||
needCheckValidatedValues: true,
|
||||
}) || { isCheckValidated: false, values: {} }
|
||||
modelNameAndTypeIsCheckValidated = formResult.isCheckValidated
|
||||
modelNameAndTypeValues = formResult.values
|
||||
}
|
||||
|
||||
if (mode === ModelModalModeEnum.configModelCredential && model) {
|
||||
modelNameAndTypeValues = {
|
||||
__model_name: model.model,
|
||||
__model_type: model.model_type,
|
||||
}
|
||||
}
|
||||
|
||||
if (mode === ModelModalModeEnum.addCustomModelToModelList && selectedCredential?.addNewCredential && model) {
|
||||
modelNameAndTypeValues = {
|
||||
__model_name: model.model,
|
||||
__model_type: model.model_type,
|
||||
}
|
||||
}
|
||||
const {
|
||||
isCheckValidated,
|
||||
values,
|
||||
} = formRef.current?.getFormValues({
|
||||
} = formRef2.current?.getFormValues({
|
||||
needCheckValidatedValues: true,
|
||||
needTransformWhenSecretFieldIsPristine: true,
|
||||
}) || { isCheckValidated: false, values: {} }
|
||||
if (!isCheckValidated)
|
||||
if (!isCheckValidated || !modelNameAndTypeIsCheckValidated)
|
||||
return
|
||||
|
||||
const {
|
||||
__authorization_name__,
|
||||
__model_name,
|
||||
__model_type,
|
||||
} = modelNameAndTypeValues
|
||||
const {
|
||||
__authorization_name__,
|
||||
...rest
|
||||
} = values
|
||||
if (__model_name && __model_type) {
|
||||
handleSaveCredential({
|
||||
if (__model_name && __model_type && __authorization_name__) {
|
||||
await handleSaveCredential({
|
||||
credential_id: credential?.credential_id,
|
||||
credentials: rest,
|
||||
name: __authorization_name__,
|
||||
@ -122,41 +177,33 @@ const ModelModal: FC<ModelModalProps> = ({
|
||||
})
|
||||
}
|
||||
else {
|
||||
handleSaveCredential({
|
||||
await handleSaveCredential({
|
||||
credential_id: credential?.credential_id,
|
||||
credentials: rest,
|
||||
name: __authorization_name__,
|
||||
})
|
||||
}
|
||||
}, [handleSaveCredential, credential?.credential_id, model])
|
||||
onSave(values)
|
||||
}, [handleSaveCredential, credential?.credential_id, model, onSave, mode, selectedCredential, handleActiveCredential])
|
||||
|
||||
const modalTitle = useMemo(() => {
|
||||
if (!providerFormSchemaPredefined && !model) {
|
||||
return (
|
||||
<div className='flex items-center'>
|
||||
<ModelIcon
|
||||
className='mr-2 h-10 w-10 shrink-0'
|
||||
iconClassName='h-10 w-10'
|
||||
provider={provider}
|
||||
/>
|
||||
<div>
|
||||
<div className='system-xs-medium-uppercase text-text-tertiary'>{t('common.modelProvider.auth.apiKeyModal.addModel')}</div>
|
||||
<div className='system-md-semibold text-text-primary'>{renderI18nObject(provider.label)}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
let label = t('common.modelProvider.auth.apiKeyModal.title')
|
||||
|
||||
if (model)
|
||||
label = t('common.modelProvider.auth.addModelCredential')
|
||||
if (mode === ModelModalModeEnum.configCustomModel || mode === ModelModalModeEnum.addCustomModelToModelList)
|
||||
label = t('common.modelProvider.auth.addModel')
|
||||
if (mode === ModelModalModeEnum.configModelCredential) {
|
||||
if (credential)
|
||||
label = t('common.modelProvider.auth.editModelCredential')
|
||||
else
|
||||
label = t('common.modelProvider.auth.addModelCredential')
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='title-2xl-semi-bold text-text-primary'>
|
||||
{label}
|
||||
</div>
|
||||
)
|
||||
}, [providerFormSchemaPredefined, t, model, renderI18nObject])
|
||||
}, [t, mode, credential])
|
||||
|
||||
const modalDesc = useMemo(() => {
|
||||
if (providerFormSchemaPredefined) {
|
||||
@ -171,7 +218,18 @@ const ModelModal: FC<ModelModalProps> = ({
|
||||
}, [providerFormSchemaPredefined, t])
|
||||
|
||||
const modalModel = useMemo(() => {
|
||||
if (model) {
|
||||
if (mode === ModelModalModeEnum.configCustomModel) {
|
||||
return (
|
||||
<div className='mt-2 flex items-center'>
|
||||
<ModelIcon
|
||||
className='mr-2 h-4 w-4 shrink-0'
|
||||
provider={provider}
|
||||
/>
|
||||
<div className='system-md-regular mr-1 text-text-secondary'>{renderI18nObject(provider.label)}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (model && (mode === ModelModalModeEnum.configModelCredential || mode === ModelModalModeEnum.addCustomModelToModelList)) {
|
||||
return (
|
||||
<div className='mt-2 flex items-center'>
|
||||
<ModelIcon
|
||||
@ -186,7 +244,52 @@ const ModelModal: FC<ModelModalProps> = ({
|
||||
}
|
||||
|
||||
return null
|
||||
}, [model, provider])
|
||||
}, [model, provider, mode, renderI18nObject])
|
||||
|
||||
const showCredentialLabel = useMemo(() => {
|
||||
if (mode === ModelModalModeEnum.configCustomModel)
|
||||
return true
|
||||
if (mode === ModelModalModeEnum.addCustomModelToModelList)
|
||||
return selectedCredential?.addNewCredential
|
||||
}, [mode, selectedCredential])
|
||||
const showCredentialForm = useMemo(() => {
|
||||
if (mode !== ModelModalModeEnum.addCustomModelToModelList)
|
||||
return true
|
||||
return selectedCredential?.addNewCredential
|
||||
}, [mode, selectedCredential])
|
||||
const saveButtonText = useMemo(() => {
|
||||
if (mode === ModelModalModeEnum.addCustomModelToModelList || mode === ModelModalModeEnum.configCustomModel)
|
||||
return t('common.operation.add')
|
||||
return t('common.operation.save')
|
||||
}, [mode, t])
|
||||
|
||||
const handleDeleteCredential = useCallback(() => {
|
||||
handleConfirmDelete()
|
||||
onCancel()
|
||||
}, [handleConfirmDelete])
|
||||
|
||||
const handleModelNameAndTypeChange = useCallback((field: string, value: any) => {
|
||||
const {
|
||||
getForm,
|
||||
} = formRef2.current as FormRefObject || {}
|
||||
if (getForm())
|
||||
getForm()?.setFieldValue(field, value)
|
||||
}, [])
|
||||
const notAllowCustomCredential = provider.allow_custom_token === false
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
event.stopPropagation()
|
||||
onCancel()
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', handleKeyDown, true)
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown, true)
|
||||
}
|
||||
}, [onCancel])
|
||||
|
||||
return (
|
||||
<PortalToFollowElem open>
|
||||
@ -199,100 +302,132 @@ const ModelModal: FC<ModelModalProps> = ({
|
||||
>
|
||||
<RiCloseLine className='h-4 w-4 text-text-tertiary' />
|
||||
</div>
|
||||
<div className='px-6 pt-6'>
|
||||
<div className='pb-3'>
|
||||
{modalTitle}
|
||||
{modalDesc}
|
||||
{modalModel}
|
||||
</div>
|
||||
|
||||
<div className='max-h-[calc(100vh-320px)] overflow-y-auto'>
|
||||
{
|
||||
isLoading && (
|
||||
<div className='flex items-center justify-center'>
|
||||
<Loading />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
!isLoading && (
|
||||
<AuthForm
|
||||
formSchemas={formSchemas.map((formSchema) => {
|
||||
return {
|
||||
...formSchema,
|
||||
name: formSchema.variable,
|
||||
showRadioUI: formSchema.type === FormTypeEnum.radio,
|
||||
}
|
||||
}) as FormSchema[]}
|
||||
defaultValues={formValues}
|
||||
inputClassName='justify-start'
|
||||
ref={formRef}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className='sticky bottom-0 -mx-2 mt-2 flex flex-wrap items-center justify-between gap-y-2 bg-components-panel-bg px-2 pb-6 pt-4'>
|
||||
{
|
||||
(provider.help && (provider.help.title || provider.help.url))
|
||||
? (
|
||||
<a
|
||||
href={provider.help?.url[language] || provider.help?.url.en_US}
|
||||
target='_blank' rel='noopener noreferrer'
|
||||
className='inline-flex items-center text-xs text-primary-600'
|
||||
onClick={e => !provider.help.url && e.preventDefault()}
|
||||
>
|
||||
{provider.help.title?.[language] || provider.help.url[language] || provider.help.title?.en_US || provider.help.url.en_US}
|
||||
<LinkExternal02 className='ml-1 h-3 w-3' />
|
||||
</a>
|
||||
)
|
||||
: <div />
|
||||
}
|
||||
<div>
|
||||
{
|
||||
isEditMode && (
|
||||
<Button
|
||||
variant='warning'
|
||||
size='large'
|
||||
className='mr-2'
|
||||
onClick={() => openConfirmDelete(credential, model)}
|
||||
>
|
||||
{t('common.operation.remove')}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
<Button
|
||||
size='large'
|
||||
className='mr-2'
|
||||
onClick={onCancel}
|
||||
>
|
||||
{t('common.operation.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
size='large'
|
||||
variant='primary'
|
||||
onClick={handleSave}
|
||||
disabled={isLoading || doingAction}
|
||||
>
|
||||
{t('common.operation.save')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className='p-6 pb-3'>
|
||||
{modalTitle}
|
||||
{modalDesc}
|
||||
{modalModel}
|
||||
</div>
|
||||
<div className='border-t-[0.5px] border-t-divider-regular'>
|
||||
<div className='flex items-center justify-center rounded-b-2xl bg-background-section-burn py-3 text-xs text-text-tertiary'>
|
||||
<Lock01 className='mr-1 h-3 w-3 text-text-tertiary' />
|
||||
{t('common.modelProvider.encrypted.front')}
|
||||
<a
|
||||
className='mx-1 text-text-accent'
|
||||
target='_blank' rel='noopener noreferrer'
|
||||
href='https://pycryptodome.readthedocs.io/en/latest/src/cipher/oaep.html'
|
||||
<div className='max-h-[calc(100vh-320px)] overflow-y-auto px-6 py-3'>
|
||||
{
|
||||
mode === ModelModalModeEnum.configCustomModel && (
|
||||
<AuthForm
|
||||
formSchemas={modelNameAndTypeFormSchemas.map((formSchema) => {
|
||||
return {
|
||||
...formSchema,
|
||||
name: formSchema.variable,
|
||||
}
|
||||
}) as FormSchema[]}
|
||||
defaultValues={modelNameAndTypeFormValues}
|
||||
inputClassName='justify-start'
|
||||
ref={formRef1}
|
||||
onChange={handleModelNameAndTypeChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
mode === ModelModalModeEnum.addCustomModelToModelList && (
|
||||
<CredentialSelector
|
||||
credentials={available_credentials || []}
|
||||
onSelect={setSelectedCredential}
|
||||
selectedCredential={selectedCredential}
|
||||
disabled={isLoading}
|
||||
notAllowAddNewCredential={notAllowCustomCredential}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
showCredentialLabel && (
|
||||
<div className='system-xs-medium-uppercase mb-3 mt-6 flex items-center text-text-tertiary'>
|
||||
{t('common.modelProvider.auth.modelCredential')}
|
||||
<div className='ml-2 h-px grow bg-gradient-to-r from-divider-regular to-background-gradient-mask-transparent' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
isLoading && (
|
||||
<div className='mt-3 flex items-center justify-center'>
|
||||
<Loading />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
!isLoading
|
||||
&& showCredentialForm
|
||||
&& (
|
||||
<AuthForm
|
||||
formSchemas={formSchemas.map((formSchema) => {
|
||||
return {
|
||||
...formSchema,
|
||||
name: formSchema.variable,
|
||||
showRadioUI: formSchema.type === FormTypeEnum.radio,
|
||||
}
|
||||
}) as FormSchema[]}
|
||||
defaultValues={formValues}
|
||||
inputClassName='justify-start'
|
||||
ref={formRef2}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div className='flex justify-between p-6 pt-5'>
|
||||
{
|
||||
(provider.help && (provider.help.title || provider.help.url))
|
||||
? (
|
||||
<a
|
||||
href={provider.help?.url[language] || provider.help?.url.en_US}
|
||||
target='_blank' rel='noopener noreferrer'
|
||||
className='system-xs-regular mt-2 inline-flex items-center text-text-accent'
|
||||
onClick={e => !provider.help.url && e.preventDefault()}
|
||||
>
|
||||
{provider.help.title?.[language] || provider.help.url[language] || provider.help.title?.en_US || provider.help.url.en_US}
|
||||
<LinkExternal02 className='ml-1 h-3 w-3' />
|
||||
</a>
|
||||
)
|
||||
: <div />
|
||||
}
|
||||
<div className='flex items-center justify-end space-x-2'>
|
||||
{
|
||||
isEditMode && (
|
||||
<Button
|
||||
variant='warning'
|
||||
onClick={() => openConfirmDelete(credential, model)}
|
||||
>
|
||||
{t('common.operation.remove')}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
<Button
|
||||
onClick={onCancel}
|
||||
>
|
||||
PKCS1_OAEP
|
||||
</a>
|
||||
{t('common.modelProvider.encrypted.back')}
|
||||
{t('common.operation.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
variant='primary'
|
||||
onClick={handleSave}
|
||||
disabled={isLoading || doingAction}
|
||||
>
|
||||
{saveButtonText}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
(mode === ModelModalModeEnum.configCustomModel || mode === ModelModalModeEnum.configProviderCredential) && (
|
||||
<div className='border-t-[0.5px] border-t-divider-regular'>
|
||||
<div className='flex items-center justify-center rounded-b-2xl bg-background-section-burn py-3 text-xs text-text-tertiary'>
|
||||
<Lock01 className='mr-1 h-3 w-3 text-text-tertiary' />
|
||||
{t('common.modelProvider.encrypted.front')}
|
||||
<a
|
||||
className='mx-1 text-text-accent'
|
||||
target='_blank' rel='noopener noreferrer'
|
||||
href='https://pycryptodome.readthedocs.io/en/latest/src/cipher/oaep.html'
|
||||
>
|
||||
PKCS1_OAEP
|
||||
</a>
|
||||
{t('common.modelProvider.encrypted.back')}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{
|
||||
deleteCredentialId && (
|
||||
@ -301,7 +436,7 @@ const ModelModal: FC<ModelModalProps> = ({
|
||||
title={t('common.modelProvider.confirmDelete')}
|
||||
isDisabled={doingAction}
|
||||
onCancel={closeConfirmDelete}
|
||||
onConfirm={handleConfirmDelete}
|
||||
onConfirm={handleDeleteCredential}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@ -91,8 +91,8 @@ const ParameterItem: FC<ParameterItemProps> = ({
|
||||
numberInputRef.current!.value = `${num}`
|
||||
}
|
||||
|
||||
const handleRadioChange = (v: number) => {
|
||||
handleInputChange(v === 1)
|
||||
const handleRadioChange = (v: boolean) => {
|
||||
handleInputChange(v)
|
||||
}
|
||||
|
||||
const handleStringInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
@ -187,11 +187,11 @@ const ParameterItem: FC<ParameterItemProps> = ({
|
||||
return (
|
||||
<Radio.Group
|
||||
className='flex w-[178px] items-center'
|
||||
value={renderValue ? 1 : 0}
|
||||
value={renderValue as boolean}
|
||||
onChange={handleRadioChange}
|
||||
>
|
||||
<Radio value={1} className='w-[83px]'>True</Radio>
|
||||
<Radio value={0} className='w-[83px]'>False</Radio>
|
||||
<Radio value={true} className='w-[83px]'>True</Radio>
|
||||
<Radio value={false} className='w-[83px]'>False</Radio>
|
||||
</Radio.Group>
|
||||
)
|
||||
}
|
||||
|
||||
@ -111,7 +111,6 @@ const CredentialPanel = ({
|
||||
<div className='flex items-center gap-0.5'>
|
||||
<ConfigProvider
|
||||
provider={provider}
|
||||
configurationMethod={ConfigurationMethodEnum.predefinedModel}
|
||||
/>
|
||||
{
|
||||
systemConfig.enabled && isCustomConfigured && (
|
||||
|
||||
@ -25,7 +25,10 @@ import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import { IS_CE_EDITION } from '@/config'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import cn from '@/utils/classnames'
|
||||
import { AddCustomModel } from '@/app/components/header/account-setting/model-provider-page/model-auth'
|
||||
import {
|
||||
AddCustomModel,
|
||||
ManageCustomModelCredentials,
|
||||
} from '@/app/components/header/account-setting/model-provider-page/model-auth'
|
||||
|
||||
export const UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST = 'UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST'
|
||||
type ProviderAddedCardProps = {
|
||||
@ -155,10 +158,17 @@ const ProviderAddedCard: FC<ProviderAddedCardProps> = ({
|
||||
)}
|
||||
{
|
||||
configurationMethods.includes(ConfigurationMethodEnum.customizableModel) && isCurrentWorkspaceManager && (
|
||||
<AddCustomModel
|
||||
provider={provider}
|
||||
configurationMethod={ConfigurationMethodEnum.customizableModel}
|
||||
/>
|
||||
<div className='flex grow justify-end'>
|
||||
<ManageCustomModelCredentials
|
||||
provider={provider}
|
||||
currentCustomConfigurationModelFixedFields={undefined}
|
||||
/>
|
||||
<AddCustomModel
|
||||
provider={provider}
|
||||
configurationMethod={ConfigurationMethodEnum.customizableModel}
|
||||
currentCustomConfigurationModelFixedFields={undefined}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
@ -16,7 +16,10 @@ import {
|
||||
import ModelListItem from './model-list-item'
|
||||
import { useModalContextSelector } from '@/context/modal-context'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { AddCustomModel } from '@/app/components/header/account-setting/model-provider-page/model-auth'
|
||||
import {
|
||||
AddCustomModel,
|
||||
ManageCustomModelCredentials,
|
||||
} from '@/app/components/header/account-setting/model-provider-page/model-auth'
|
||||
|
||||
type ModelListProps = {
|
||||
provider: ModelProvider
|
||||
@ -67,6 +70,10 @@ const ModelList: FC<ModelListProps> = ({
|
||||
{
|
||||
isConfigurable && isCurrentWorkspaceManager && (
|
||||
<div className='flex grow justify-end'>
|
||||
<ManageCustomModelCredentials
|
||||
provider={provider}
|
||||
currentCustomConfigurationModelFixedFields={undefined}
|
||||
/>
|
||||
<AddCustomModel
|
||||
provider={provider}
|
||||
configurationMethod={ConfigurationMethodEnum.customizableModel}
|
||||
|
||||
@ -2,8 +2,7 @@ import type { Dispatch, SetStateAction } from 'react'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiDeleteBinLine,
|
||||
RiEqualizer2Line,
|
||||
RiIndeterminateCircleLine,
|
||||
} from '@remixicon/react'
|
||||
import type {
|
||||
Credential,
|
||||
@ -28,7 +27,6 @@ import GridMask from '@/app/components/base/grid-mask'
|
||||
import { useProviderContextSelector } from '@/context/provider-context'
|
||||
import { IS_CE_EDITION } from '@/config'
|
||||
import { AddCredentialInLoadBalancing } from '@/app/components/header/account-setting/model-provider-page/model-auth'
|
||||
import { useModelModalHandler } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import Badge from '@/app/components/base/badge/index'
|
||||
|
||||
export type ModelLoadBalancingConfigsProps = {
|
||||
@ -40,7 +38,8 @@ export type ModelLoadBalancingConfigsProps = {
|
||||
withSwitch?: boolean
|
||||
className?: string
|
||||
modelCredential: ModelCredential
|
||||
onUpdate?: () => void
|
||||
onUpdate?: (payload?: any, formValues?: Record<string, any>) => void
|
||||
onRemove?: (credentialId: string) => void
|
||||
model: CustomModelCredential
|
||||
}
|
||||
|
||||
@ -55,11 +54,11 @@ const ModelLoadBalancingConfigs = ({
|
||||
className,
|
||||
modelCredential,
|
||||
onUpdate,
|
||||
onRemove,
|
||||
}: ModelLoadBalancingConfigsProps) => {
|
||||
const { t } = useTranslation()
|
||||
const providerFormSchemaPredefined = configurationMethod === ConfigurationMethodEnum.predefinedModel
|
||||
const modelLoadBalancingEnabled = useProviderContextSelector(state => state.modelLoadBalancingEnabled)
|
||||
const handleOpenModal = useModelModalHandler()
|
||||
|
||||
const updateConfigEntry = useCallback(
|
||||
(
|
||||
@ -130,6 +129,17 @@ const ModelLoadBalancingConfigs = ({
|
||||
return draftConfig.configs
|
||||
}, [draftConfig])
|
||||
|
||||
const handleUpdate = useCallback((payload?: any, formValues?: Record<string, any>) => {
|
||||
onUpdate?.(payload, formValues)
|
||||
}, [onUpdate])
|
||||
|
||||
const handleRemove = useCallback((credentialId: string) => {
|
||||
const index = draftConfig?.configs.findIndex(item => item.credential_id === credentialId && item.name !== '__inherit__')
|
||||
if (index && index > -1)
|
||||
updateConfigEntry(index, () => undefined)
|
||||
onRemove?.(credentialId)
|
||||
}, [draftConfig?.configs, updateConfigEntry, onRemove])
|
||||
|
||||
if (!draftConfig)
|
||||
return null
|
||||
|
||||
@ -190,7 +200,7 @@ const ModelLoadBalancingConfigs = ({
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<div className='mr-1 text-[13px]'>
|
||||
<div className='mr-1 text-[13px] text-text-secondary'>
|
||||
{isProviderManaged ? t('common.modelProvider.defaultConfig') : config.name}
|
||||
</div>
|
||||
{isProviderManaged && providerFormSchemaPredefined && (
|
||||
@ -206,34 +216,14 @@ const ModelLoadBalancingConfigs = ({
|
||||
{!isProviderManaged && (
|
||||
<>
|
||||
<div className='flex items-center gap-1 opacity-0 transition-opacity group-hover:opacity-100'>
|
||||
{
|
||||
config.credential_id && !credential?.not_allowed_to_use && !credential?.from_enterprise && (
|
||||
<span
|
||||
className='flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg bg-components-button-secondary-bg text-text-tertiary transition-colors hover:bg-components-button-secondary-bg-hover'
|
||||
onClick={() => {
|
||||
handleOpenModal(
|
||||
provider,
|
||||
configurationMethod,
|
||||
currentCustomConfigurationModelFixedFields,
|
||||
configurationMethod === ConfigurationMethodEnum.customizableModel,
|
||||
(config.credential_id && config.name) ? {
|
||||
credential_id: config.credential_id,
|
||||
credential_name: config.name,
|
||||
} : undefined,
|
||||
model,
|
||||
)
|
||||
}}
|
||||
>
|
||||
<RiEqualizer2Line className='h-4 w-4' />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
<span
|
||||
className='flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg bg-components-button-secondary-bg text-text-tertiary transition-colors hover:bg-components-button-secondary-bg-hover'
|
||||
onClick={() => updateConfigEntry(index, () => undefined)}
|
||||
>
|
||||
<RiDeleteBinLine className='h-4 w-4' />
|
||||
</span>
|
||||
<Tooltip popupContent={t('common.operation.remove')}>
|
||||
<span
|
||||
className='flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg bg-components-button-secondary-bg text-text-tertiary transition-colors hover:bg-components-button-secondary-bg-hover'
|
||||
onClick={() => updateConfigEntry(index, () => undefined)}
|
||||
>
|
||||
<RiIndeterminateCircleLine className='h-4 w-4' />
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
@ -261,7 +251,8 @@ const ModelLoadBalancingConfigs = ({
|
||||
configurationMethod={configurationMethod}
|
||||
modelCredential={modelCredential}
|
||||
onSelectCredential={addConfigEntry}
|
||||
onUpdate={onUpdate}
|
||||
onUpdate={handleUpdate}
|
||||
onRemove={handleRemove}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -2,6 +2,7 @@ import { memo, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type {
|
||||
Credential,
|
||||
CustomConfigurationModelFixedFields,
|
||||
ModelItem,
|
||||
ModelLoadBalancingConfig,
|
||||
ModelLoadBalancingConfigEntry,
|
||||
@ -24,10 +25,14 @@ import {
|
||||
useGetModelCredential,
|
||||
useUpdateModelLoadBalancingConfig,
|
||||
} from '@/service/use-models'
|
||||
import { useAuth } from '../model-auth/hooks/use-auth'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
import { useRefreshModel } from '../hooks'
|
||||
|
||||
export type ModelLoadBalancingModalProps = {
|
||||
provider: ModelProvider
|
||||
configurateMethod: ConfigurationMethodEnum
|
||||
currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields
|
||||
model: ModelItem
|
||||
credential?: Credential
|
||||
open?: boolean
|
||||
@ -39,6 +44,7 @@ export type ModelLoadBalancingModalProps = {
|
||||
const ModelLoadBalancingModal = ({
|
||||
provider,
|
||||
configurateMethod,
|
||||
currentCustomConfigurationModelFixedFields,
|
||||
model,
|
||||
credential,
|
||||
open = false,
|
||||
@ -47,7 +53,20 @@ const ModelLoadBalancingModal = ({
|
||||
}: ModelLoadBalancingModalProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useToastContext()
|
||||
|
||||
const {
|
||||
doingAction,
|
||||
deleteModel,
|
||||
openConfirmDelete,
|
||||
closeConfirmDelete,
|
||||
handleConfirmDelete,
|
||||
} = useAuth(
|
||||
provider,
|
||||
configurateMethod,
|
||||
currentCustomConfigurationModelFixedFields,
|
||||
{
|
||||
isModelCredential: true,
|
||||
},
|
||||
)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const providerFormSchemaPredefined = configurateMethod === ConfigurationMethodEnum.predefinedModel
|
||||
const configFrom = providerFormSchemaPredefined ? 'predefined-model' : 'custom-model'
|
||||
@ -121,6 +140,7 @@ const ModelLoadBalancingModal = ({
|
||||
}
|
||||
}, [current_credential_id, current_credential_name])
|
||||
const [customModelCredential, setCustomModelCredential] = useState<Credential | undefined>(initialCustomModelCredential)
|
||||
const { handleRefreshModel } = useRefreshModel()
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
@ -139,6 +159,7 @@ const ModelLoadBalancingModal = ({
|
||||
)
|
||||
if (res.result === 'success') {
|
||||
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
|
||||
handleRefreshModel(provider, configurateMethod, currentCustomConfigurationModelFixedFields)
|
||||
onSave?.(provider.provider)
|
||||
onClose?.()
|
||||
}
|
||||
@ -147,120 +168,208 @@ const ModelLoadBalancingModal = ({
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
const handleDeleteModel = useCallback(async () => {
|
||||
await handleConfirmDelete()
|
||||
onClose?.()
|
||||
}, [handleConfirmDelete, onClose])
|
||||
|
||||
const handleUpdate = useCallback(async (payload?: any, formValues?: Record<string, any>) => {
|
||||
const result = await refetch()
|
||||
const available_credentials = result.data?.available_credentials || []
|
||||
const credentialName = formValues?.__authorization_name__
|
||||
const modelCredential = payload?.credential
|
||||
|
||||
if (!available_credentials.length) {
|
||||
onClose?.()
|
||||
return
|
||||
}
|
||||
|
||||
if (!modelCredential) {
|
||||
const currentCredential = available_credentials.find(c => c.credential_name === credentialName)
|
||||
if (currentCredential) {
|
||||
setDraftConfig((prev: any) => {
|
||||
if (!prev)
|
||||
return prev
|
||||
return {
|
||||
...prev,
|
||||
configs: [...prev.configs, {
|
||||
credential_id: currentCredential.credential_id,
|
||||
enabled: true,
|
||||
name: currentCredential.credential_name,
|
||||
}],
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
else {
|
||||
setDraftConfig((prev) => {
|
||||
if (!prev)
|
||||
return prev
|
||||
const newConfigs = [...prev.configs]
|
||||
const prevIndex = newConfigs.findIndex(item => item.credential_id === modelCredential.credential_id && item.name !== '__inherit__')
|
||||
const newIndex = available_credentials.findIndex(c => c.credential_id === modelCredential.credential_id)
|
||||
|
||||
if (newIndex > -1 && prevIndex > -1)
|
||||
newConfigs[prevIndex].name = available_credentials[newIndex].credential_name || ''
|
||||
|
||||
return {
|
||||
...prev,
|
||||
configs: newConfigs,
|
||||
}
|
||||
})
|
||||
}
|
||||
}, [refetch, credential])
|
||||
|
||||
const handleUpdateWhenSwitchCredential = useCallback(async () => {
|
||||
const result = await refetch()
|
||||
const available_credentials = result.data?.available_credentials || []
|
||||
if (!available_credentials.length)
|
||||
onClose?.()
|
||||
}, [refetch, onClose])
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isShow={Boolean(model) && open}
|
||||
onClose={onClose}
|
||||
className='w-[640px] max-w-none px-8 pt-8'
|
||||
title={
|
||||
<div className='pb-3 font-semibold'>
|
||||
<div className='h-[30px]'>{
|
||||
draftConfig?.enabled
|
||||
? t('common.modelProvider.auth.configLoadBalancing')
|
||||
: t('common.modelProvider.auth.configModel')
|
||||
}</div>
|
||||
{Boolean(model) && (
|
||||
<div className='flex h-5 items-center'>
|
||||
<ModelIcon
|
||||
className='mr-2 shrink-0'
|
||||
provider={provider}
|
||||
modelName={model!.model}
|
||||
/>
|
||||
<ModelName
|
||||
className='system-md-regular grow text-text-secondary'
|
||||
modelItem={model!}
|
||||
showModelType
|
||||
showMode
|
||||
showContextSize
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{!draftConfig
|
||||
? <Loading type='area' />
|
||||
: (
|
||||
<>
|
||||
<div className='py-2'>
|
||||
<div
|
||||
className={classNames(
|
||||
'min-h-16 rounded-xl border bg-components-panel-bg transition-colors',
|
||||
draftConfig.enabled ? 'cursor-pointer border-components-panel-border' : 'cursor-default border-util-colors-blue-blue-600',
|
||||
)}
|
||||
onClick={draftConfig.enabled ? () => toggleModalBalancing(false) : undefined}
|
||||
>
|
||||
<div className='flex select-none items-center gap-2 px-[15px] py-3'>
|
||||
<div className='flex h-8 w-8 shrink-0 grow-0 items-center justify-center rounded-lg border border-components-card-border bg-components-card-bg'>
|
||||
{Boolean(model) && (
|
||||
<ModelIcon className='shrink-0' provider={provider} modelName={model!.model} />
|
||||
)}
|
||||
</div>
|
||||
<div className='grow'>
|
||||
<div className='text-sm text-text-secondary'>{
|
||||
providerFormSchemaPredefined
|
||||
? t('common.modelProvider.auth.providerManaged')
|
||||
: t('common.modelProvider.auth.specifyModelCredential')
|
||||
}</div>
|
||||
<div className='text-xs text-text-tertiary'>{
|
||||
providerFormSchemaPredefined
|
||||
? t('common.modelProvider.auth.providerManagedTip')
|
||||
: t('common.modelProvider.auth.specifyModelCredentialTip')
|
||||
}</div>
|
||||
<>
|
||||
<Modal
|
||||
isShow={Boolean(model) && open}
|
||||
onClose={onClose}
|
||||
className='w-[640px] max-w-none px-8 pt-8'
|
||||
title={
|
||||
<div className='pb-3 font-semibold'>
|
||||
<div className='h-[30px]'>{
|
||||
draftConfig?.enabled
|
||||
? t('common.modelProvider.auth.configLoadBalancing')
|
||||
: t('common.modelProvider.auth.configModel')
|
||||
}</div>
|
||||
{Boolean(model) && (
|
||||
<div className='flex h-5 items-center'>
|
||||
<ModelIcon
|
||||
className='mr-2 shrink-0'
|
||||
provider={provider}
|
||||
modelName={model!.model}
|
||||
/>
|
||||
<ModelName
|
||||
className='system-md-regular grow text-text-secondary'
|
||||
modelItem={model!}
|
||||
showModelType
|
||||
showMode
|
||||
showContextSize
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{!draftConfig
|
||||
? <Loading type='area' />
|
||||
: (
|
||||
<>
|
||||
<div className='py-2'>
|
||||
<div
|
||||
className={classNames(
|
||||
'min-h-16 rounded-xl border bg-components-panel-bg transition-colors',
|
||||
draftConfig.enabled ? 'cursor-pointer border-components-panel-border' : 'cursor-default border-util-colors-blue-blue-600',
|
||||
)}
|
||||
onClick={draftConfig.enabled ? () => toggleModalBalancing(false) : undefined}
|
||||
>
|
||||
<div className='flex select-none items-center gap-2 px-[15px] py-3'>
|
||||
<div className='flex h-8 w-8 shrink-0 grow-0 items-center justify-center rounded-lg border border-components-card-border bg-components-card-bg'>
|
||||
{Boolean(model) && (
|
||||
<ModelIcon className='shrink-0' provider={provider} modelName={model!.model} />
|
||||
)}
|
||||
</div>
|
||||
<div className='grow'>
|
||||
<div className='text-sm text-text-secondary'>{
|
||||
providerFormSchemaPredefined
|
||||
? t('common.modelProvider.auth.providerManaged')
|
||||
: t('common.modelProvider.auth.specifyModelCredential')
|
||||
}</div>
|
||||
<div className='text-xs text-text-tertiary'>{
|
||||
providerFormSchemaPredefined
|
||||
? t('common.modelProvider.auth.providerManagedTip')
|
||||
: t('common.modelProvider.auth.specifyModelCredentialTip')
|
||||
}</div>
|
||||
</div>
|
||||
{
|
||||
!providerFormSchemaPredefined && (
|
||||
<SwitchCredentialInLoadBalancing
|
||||
provider={provider}
|
||||
customModelCredential={customModelCredential ?? initialCustomModelCredential}
|
||||
setCustomModelCredential={setCustomModelCredential}
|
||||
model={model}
|
||||
credentials={available_credentials}
|
||||
onUpdate={handleUpdateWhenSwitchCredential}
|
||||
onRemove={handleUpdateWhenSwitchCredential}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
modelCredential && (
|
||||
<ModelLoadBalancingConfigs {...{
|
||||
draftConfig,
|
||||
setDraftConfig,
|
||||
provider,
|
||||
currentCustomConfigurationModelFixedFields: {
|
||||
__model_name: model.model,
|
||||
__model_type: model.model_type,
|
||||
},
|
||||
configurationMethod: model.fetch_from,
|
||||
className: 'mt-2',
|
||||
modelCredential,
|
||||
onUpdate: handleUpdate,
|
||||
onRemove: handleUpdateWhenSwitchCredential,
|
||||
model: {
|
||||
model: model.model,
|
||||
model_type: model.model_type,
|
||||
},
|
||||
}} />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className='mt-6 flex items-center justify-between gap-2'>
|
||||
<div>
|
||||
{
|
||||
!providerFormSchemaPredefined && (
|
||||
<SwitchCredentialInLoadBalancing
|
||||
provider={provider}
|
||||
customModelCredential={initialCustomModelCredential ?? customModelCredential}
|
||||
setCustomModelCredential={setCustomModelCredential}
|
||||
model={model}
|
||||
credentials={available_credentials}
|
||||
/>
|
||||
<Button
|
||||
onClick={() => openConfirmDelete(undefined, { model: model.model, model_type: model.model_type })}
|
||||
className='text-components-button-destructive-secondary-text'
|
||||
>
|
||||
{t('common.modelProvider.auth.removeModel')}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div className='space-x-2'>
|
||||
<Button onClick={onClose}>{t('common.operation.cancel')}</Button>
|
||||
<Button
|
||||
variant='primary'
|
||||
onClick={handleSave}
|
||||
disabled={
|
||||
loading
|
||||
|| (draftConfig?.enabled && (draftConfig?.configs.filter(config => config.enabled).length ?? 0) < 2)
|
||||
|| isLoading
|
||||
}
|
||||
>{t('common.operation.save')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
modelCredential && (
|
||||
<ModelLoadBalancingConfigs {...{
|
||||
draftConfig,
|
||||
setDraftConfig,
|
||||
provider,
|
||||
currentCustomConfigurationModelFixedFields: {
|
||||
__model_name: model.model,
|
||||
__model_type: model.model_type,
|
||||
},
|
||||
configurationMethod: model.fetch_from,
|
||||
className: 'mt-2',
|
||||
modelCredential,
|
||||
onUpdate: refetch,
|
||||
model: {
|
||||
model: model.model,
|
||||
model_type: model.model_type,
|
||||
},
|
||||
}} />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className='mt-6 flex items-center justify-end gap-2'>
|
||||
<Button onClick={onClose}>{t('common.operation.cancel')}</Button>
|
||||
<Button
|
||||
variant='primary'
|
||||
onClick={handleSave}
|
||||
disabled={
|
||||
loading
|
||||
|| (draftConfig?.enabled && (draftConfig?.configs.filter(config => config.enabled).length ?? 0) < 2)
|
||||
|| isLoading
|
||||
}
|
||||
>{t('common.operation.save')}</Button>
|
||||
</div>
|
||||
</>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</Modal >
|
||||
{
|
||||
deleteModel && (
|
||||
<Confirm
|
||||
isShow
|
||||
title={t('common.modelProvider.confirmDelete')}
|
||||
onCancel={closeConfirmDelete}
|
||||
onConfirm={handleDeleteModel}
|
||||
isDisabled={doingAction}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</Modal >
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -161,7 +161,7 @@ export const modelTypeFormat = (modelType: ModelTypeEnum) => {
|
||||
|
||||
export const genModelTypeFormSchema = (modelTypes: ModelTypeEnum[]) => {
|
||||
return {
|
||||
type: FormTypeEnum.radio,
|
||||
type: FormTypeEnum.select,
|
||||
label: {
|
||||
zh_Hans: '模型类型',
|
||||
en_US: 'Model Type',
|
||||
|
||||
@ -41,13 +41,10 @@ const Item: FC<Props> = ({
|
||||
onFetchedPayload(payload)
|
||||
setPayload({ ...payload, from: dependency.type })
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [data])
|
||||
useEffect(() => {
|
||||
if (error)
|
||||
onFetchError()
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [error])
|
||||
if (!payload) return <Loading />
|
||||
return (
|
||||
|
||||
@ -131,7 +131,6 @@ const InstallByDSLList: ForwardRefRenderFunction<ExposeRefs, Props> = ({
|
||||
if (failedIndex.length > 0)
|
||||
setErrorIndexes([...errorIndexes, ...failedIndex])
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isFetchingMarketplaceDataById])
|
||||
|
||||
useEffect(() => {
|
||||
@ -156,15 +155,12 @@ const InstallByDSLList: ForwardRefRenderFunction<ExposeRefs, Props> = ({
|
||||
if (failedIndex.length > 0)
|
||||
setErrorIndexes([...errorIndexes, ...failedIndex])
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isFetchingDataByMeta])
|
||||
|
||||
useEffect(() => {
|
||||
// get info all failed
|
||||
if (infoByMetaError || infoByIdError)
|
||||
setErrorIndexes([...errorIndexes, ...marketPlaceInDSLIndex])
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [infoByMetaError, infoByIdError])
|
||||
|
||||
const isLoadedAllData = (plugins.filter(p => !!p).length + errorIndexes.length) === allPlugins.length
|
||||
@ -189,8 +185,6 @@ const InstallByDSLList: ForwardRefRenderFunction<ExposeRefs, Props> = ({
|
||||
useEffect(() => {
|
||||
if (isLoadedAllData && installedInfo)
|
||||
onLoadedAllPlugin(installedInfo!)
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isLoadedAllData, installedInfo])
|
||||
|
||||
const handleSelect = useCallback((index: number) => {
|
||||
|
||||
@ -61,7 +61,6 @@ const Loaded: React.FC<LoadedProps> = ({
|
||||
useEffect(() => {
|
||||
if (hasInstalled && uniqueIdentifier === installedInfoPayload.uniqueIdentifier)
|
||||
onInstalled()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [hasInstalled])
|
||||
|
||||
const handleInstall = async () => {
|
||||
|
||||
@ -55,7 +55,6 @@ const Uploading: FC<Props> = ({
|
||||
|
||||
React.useEffect(() => {
|
||||
handleUpload()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
return (
|
||||
<>
|
||||
|
||||
@ -175,7 +175,6 @@ export const MarketplaceContextProvider = ({
|
||||
})
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [queryPlugins, queryMarketplaceCollectionsAndPlugins, isSuccess, exclude])
|
||||
|
||||
const handleQueryMarketplaceCollectionsAndPlugins = useCallback(() => {
|
||||
|
||||
@ -155,9 +155,9 @@ const Authorized = ({
|
||||
}, [setPluginDefaultCredential, onUpdate, notify, t, handleSetDoingAction])
|
||||
const { mutateAsync: updatePluginCredential } = useUpdatePluginCredentialHook(pluginPayload)
|
||||
const handleRename = useCallback(async (payload: {
|
||||
credential_id: string
|
||||
name: string
|
||||
}) => {
|
||||
credential_id: string
|
||||
name: string
|
||||
}) => {
|
||||
if (doingActionRef.current)
|
||||
return
|
||||
try {
|
||||
@ -306,17 +306,17 @@ const Authorized = ({
|
||||
!notAllowCustomCredential && (
|
||||
<>
|
||||
<div className='h-[1px] bg-divider-subtle'></div>
|
||||
<div className='p-2'>
|
||||
<Authorize
|
||||
pluginPayload={pluginPayload}
|
||||
theme='secondary'
|
||||
showDivider={false}
|
||||
canOAuth={canOAuth}
|
||||
canApiKey={canApiKey}
|
||||
disabled={disabled}
|
||||
onUpdate={onUpdate}
|
||||
/>
|
||||
</div>
|
||||
<div className='p-2'>
|
||||
<Authorize
|
||||
pluginPayload={pluginPayload}
|
||||
theme='secondary'
|
||||
showDivider={false}
|
||||
canOAuth={canOAuth}
|
||||
canApiKey={canApiKey}
|
||||
disabled={disabled}
|
||||
onUpdate={onUpdate}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -97,7 +97,7 @@ const AppSelector: FC<Props> = ({
|
||||
try {
|
||||
await setSize((size: number) => size + 1)
|
||||
}
|
||||
finally {
|
||||
finally {
|
||||
// Add a small delay to ensure state updates are complete
|
||||
setTimeout(() => {
|
||||
setIsLoadingMore(false)
|
||||
|
||||
@ -61,7 +61,7 @@ const DetailHeader = ({
|
||||
onUpdate,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const { userProfile: { timezone } } = useAppContext()
|
||||
const { userProfile: { timezone } } = useAppContext()
|
||||
|
||||
const { theme } = useTheme()
|
||||
const locale = useGetLanguage()
|
||||
|
||||
@ -55,9 +55,9 @@ const EndpointModal: FC<Props> = ({
|
||||
const value = processedCredential[field.name]
|
||||
if (typeof value === 'string')
|
||||
processedCredential[field.name] = value === 'true' || value === '1' || value === 'True'
|
||||
else if (typeof value === 'number')
|
||||
else if (typeof value === 'number')
|
||||
processedCredential[field.name] = value === 1
|
||||
else if (typeof value === 'boolean')
|
||||
else if (typeof value === 'boolean')
|
||||
processedCredential[field.name] = value
|
||||
}
|
||||
})
|
||||
|
||||
@ -45,7 +45,7 @@ const MultipleToolSelector = ({
|
||||
canChooseMCPTool,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const { data: mcpTools } = useAllMCPTools()
|
||||
const { data: mcpTools } = useAllMCPTools()
|
||||
const enabledCount = value.filter((item) => {
|
||||
const isMCPTool = mcpTools?.find(tool => tool.id === item.provider_name)
|
||||
if(isMCPTool)
|
||||
|
||||
@ -222,12 +222,12 @@ const ReasoningConfigForm: React.FC<Props> = ({
|
||||
{t('workflow.nodes.agent.clickToViewParameterSchema')}
|
||||
</div>}
|
||||
asChild={false}>
|
||||
<div
|
||||
className='ml-0.5 cursor-pointer rounded-[4px] p-px text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary'
|
||||
onClick={() => showSchema(input_schema as SchemaRoot, label[language] || label.en_US)}
|
||||
>
|
||||
<RiBracesLine className='size-3.5'/>
|
||||
</div>
|
||||
<div
|
||||
className='ml-0.5 cursor-pointer rounded-[4px] p-px text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary'
|
||||
onClick={() => showSchema(input_schema as SchemaRoot, label[language] || label.en_US)}
|
||||
>
|
||||
<RiBracesLine className='size-3.5'/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
|
||||
@ -98,7 +98,6 @@ const Action: FC<Props> = ({
|
||||
hideDeleteConfirm()
|
||||
onDelete()
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [installationId, onDelete])
|
||||
return (
|
||||
<div className='flex space-x-1'>
|
||||
|
||||
@ -101,19 +101,19 @@ const AutoUpdateSetting: FC<Props> = ({
|
||||
|
||||
const renderTimePickerTrigger = useCallback(({ inputElem, onClick, isOpen }: TriggerParams) => {
|
||||
return (
|
||||
<div
|
||||
className='group float-right flex h-8 w-[160px] cursor-pointer items-center justify-between rounded-lg bg-components-input-bg-normal px-2 hover:bg-state-base-hover-alt'
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className='flex w-0 grow items-center gap-x-1'>
|
||||
<RiTimeLine className={cn(
|
||||
'h-4 w-4 shrink-0 text-text-tertiary',
|
||||
isOpen ? 'text-text-secondary' : 'group-hover:text-text-secondary',
|
||||
)} />
|
||||
{inputElem}
|
||||
</div>
|
||||
<div className='system-sm-regular text-text-tertiary'>{convertTimezoneToOffsetStr(timezone)}</div>
|
||||
<div
|
||||
className='group float-right flex h-8 w-[160px] cursor-pointer items-center justify-between rounded-lg bg-components-input-bg-normal px-2 hover:bg-state-base-hover-alt'
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className='flex w-0 grow items-center gap-x-1'>
|
||||
<RiTimeLine className={cn(
|
||||
'h-4 w-4 shrink-0 text-text-tertiary',
|
||||
isOpen ? 'text-text-secondary' : 'group-hover:text-text-secondary',
|
||||
)} />
|
||||
{inputElem}
|
||||
</div>
|
||||
<div className='system-sm-regular text-text-tertiary'>{convertTimezoneToOffsetStr(timezone)}</div>
|
||||
</div>
|
||||
)
|
||||
}, [timezone])
|
||||
|
||||
|
||||
@ -28,10 +28,10 @@ export const dayjsToTimeOfDay = (date?: Dayjs): number => {
|
||||
}
|
||||
|
||||
export const convertUTCDaySecondsToLocalSeconds = (utcDaySeconds: number, localTimezone: string): number => {
|
||||
const utcDayStart = dayjs().utc().startOf('day')
|
||||
const utcTargetTime = utcDayStart.add(utcDaySeconds, 'second')
|
||||
const localTargetTime = utcTargetTime.tz(localTimezone)
|
||||
const localDayStart = localTargetTime.startOf('day')
|
||||
const secondsInLocalDay = localTargetTime.diff(localDayStart, 'second')
|
||||
return secondsInLocalDay
|
||||
const utcDayStart = dayjs().utc().startOf('day')
|
||||
const utcTargetTime = utcDayStart.add(utcDaySeconds, 'second')
|
||||
const localTargetTime = utcTargetTime.tz(localTimezone)
|
||||
const localDayStart = localTargetTime.startOf('day')
|
||||
const secondsInLocalDay = localTargetTime.diff(localDayStart, 'second')
|
||||
return secondsInLocalDay
|
||||
}
|
||||
|
||||
@ -20,7 +20,7 @@ const DowngradeWarningModal = ({
|
||||
<div className='flex flex-col items-start gap-2 self-stretch'>
|
||||
<div className='title-2xl-semi-bold text-text-primary'>{t(`${i18nPrefix}.title`)}</div>
|
||||
<div className='system-md-regular text-text-secondary'>
|
||||
{t(`${i18nPrefix}.description`)}
|
||||
{t(`${i18nPrefix}.description`)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='mt-9 flex items-start justify-end space-x-2 self-stretch'>
|
||||
|
||||
@ -136,47 +136,47 @@ const UpdatePluginModal: FC<Props> = ({
|
||||
onExcludeAndDowngrade={handleExcludeAndDownload}
|
||||
/>
|
||||
)}
|
||||
{!doShowDowngradeWarningModal && (
|
||||
<>
|
||||
<div className='system-md-regular mb-2 mt-3 text-text-secondary'>
|
||||
{t(`${i18nPrefix}.description`)}
|
||||
</div>
|
||||
<div className='flex flex-wrap content-start items-start gap-1 self-stretch rounded-2xl bg-background-section-burn p-2'>
|
||||
<Card
|
||||
installed={uploadStep === UploadStep.installed}
|
||||
payload={pluginManifestToCardPluginProps({
|
||||
...originalPackageInfo.payload,
|
||||
icon: icon!,
|
||||
})}
|
||||
className='w-full'
|
||||
titleLeft={
|
||||
<>
|
||||
<Badge className='mx-1' size="s" state={BadgeState.Warning}>
|
||||
{`${originalPackageInfo.payload.version} -> ${targetPackageInfo.version}`}
|
||||
</Badge>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex items-center justify-end gap-2 self-stretch pt-5'>
|
||||
{uploadStep === UploadStep.notStarted && (
|
||||
{!doShowDowngradeWarningModal && (
|
||||
<>
|
||||
<div className='system-md-regular mb-2 mt-3 text-text-secondary'>
|
||||
{t(`${i18nPrefix}.description`)}
|
||||
</div>
|
||||
<div className='flex flex-wrap content-start items-start gap-1 self-stretch rounded-2xl bg-background-section-burn p-2'>
|
||||
<Card
|
||||
installed={uploadStep === UploadStep.installed}
|
||||
payload={pluginManifestToCardPluginProps({
|
||||
...originalPackageInfo.payload,
|
||||
icon: icon!,
|
||||
})}
|
||||
className='w-full'
|
||||
titleLeft={
|
||||
<>
|
||||
<Badge className='mx-1' size="s" state={BadgeState.Warning}>
|
||||
{`${originalPackageInfo.payload.version} -> ${targetPackageInfo.version}`}
|
||||
</Badge>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex items-center justify-end gap-2 self-stretch pt-5'>
|
||||
{uploadStep === UploadStep.notStarted && (
|
||||
<Button
|
||||
onClick={handleCancel}
|
||||
>
|
||||
{t('common.operation.cancel')}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
onClick={handleCancel}
|
||||
variant='primary'
|
||||
loading={uploadStep === UploadStep.upgrading}
|
||||
onClick={handleConfirm}
|
||||
disabled={uploadStep === UploadStep.upgrading}
|
||||
>
|
||||
{t('common.operation.cancel')}
|
||||
{configBtnText}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant='primary'
|
||||
loading={uploadStep === UploadStep.upgrading}
|
||||
onClick={handleConfirm}
|
||||
disabled={uploadStep === UploadStep.upgrading}
|
||||
>
|
||||
{configBtnText}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
</Modal>
|
||||
)
|
||||
|
||||
@ -102,7 +102,7 @@ const Result: FC<IResultProps> = ({
|
||||
})
|
||||
|
||||
const handleFeedback = async (feedback: FeedbackType) => {
|
||||
await updateFeedback({ url: `/messages/${messageId}/feedbacks`, body: { rating: feedback.rating } }, isInstalledApp, installedAppInfo?.id)
|
||||
await updateFeedback({ url: `/messages/${messageId}/feedbacks`, body: { rating: feedback.rating, content: feedback.content } }, isInstalledApp, installedAppInfo?.id)
|
||||
setFeedback(feedback)
|
||||
}
|
||||
|
||||
|
||||
@ -141,7 +141,6 @@ const MCPDetailContent: FC<Props> = ({
|
||||
useEffect(() => {
|
||||
if (isTriggerAuthorize)
|
||||
handleAuthorize()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
if (!detail)
|
||||
|
||||
@ -209,9 +209,9 @@ function MCPServiceCard({
|
||||
>
|
||||
|
||||
<div className="flex items-center justify-center gap-[1px]">
|
||||
<RiEditLine className="h-3.5 w-3.5" />
|
||||
<div className="system-xs-medium px-[3px] text-text-tertiary">{serverPublished ? t('tools.mcp.server.edit') : t('tools.mcp.server.addDescription')}</div>
|
||||
</div>
|
||||
<RiEditLine className="h-3.5 w-3.5" />
|
||||
<div className="system-xs-medium px-[3px] text-text-tertiary">{serverPublished ? t('tools.mcp.server.edit') : t('tools.mcp.server.addDescription')}</div>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -111,7 +111,7 @@ const ConfigCredential: FC<Props> = ({
|
||||
<Button onClick={onRemove}>{t('common.operation.remove')}</Button>
|
||||
)
|
||||
}
|
||||
< div className='flex space-x-2'>
|
||||
<div className='flex space-x-2'>
|
||||
<Button onClick={onCancel}>{t('common.operation.cancel')}</Button>
|
||||
<Button loading={isLoading || isSaving} disabled={isLoading || isSaving} variant='primary' onClick={handleSave}>{t('common.operation.save')}</Button>
|
||||
</div>
|
||||
|
||||
@ -70,9 +70,9 @@ export const addDefaultValue = (value: Record<string, any>, formSchemas: { varia
|
||||
if (formSchema.type === 'boolean' && itemValue !== undefined && itemValue !== null && itemValue !== '') {
|
||||
if (typeof itemValue === 'string')
|
||||
newValues[formSchema.variable] = itemValue === 'true' || itemValue === '1' || itemValue === 'True'
|
||||
else if (typeof itemValue === 'number')
|
||||
else if (typeof itemValue === 'number')
|
||||
newValues[formSchema.variable] = itemValue === 1
|
||||
else if (typeof itemValue === 'boolean')
|
||||
else if (typeof itemValue === 'boolean')
|
||||
newValues[formSchema.variable] = itemValue
|
||||
}
|
||||
})
|
||||
@ -162,13 +162,13 @@ export const getConfiguredValue = (value: Record<string, any>, formSchemas: { va
|
||||
}
|
||||
|
||||
const getVarKindType = (type: FormTypeEnum) => {
|
||||
if (type === FormTypeEnum.file || type === FormTypeEnum.files)
|
||||
return VarKindType.variable
|
||||
if (type === FormTypeEnum.select || type === FormTypeEnum.boolean || type === FormTypeEnum.textNumber)
|
||||
return VarKindType.constant
|
||||
if (type === FormTypeEnum.textInput || type === FormTypeEnum.secretInput)
|
||||
return VarKindType.mixed
|
||||
}
|
||||
if (type === FormTypeEnum.file || type === FormTypeEnum.files)
|
||||
return VarKindType.variable
|
||||
if (type === FormTypeEnum.select || type === FormTypeEnum.boolean || type === FormTypeEnum.textNumber)
|
||||
return VarKindType.constant
|
||||
if (type === FormTypeEnum.textInput || type === FormTypeEnum.secretInput)
|
||||
return VarKindType.mixed
|
||||
}
|
||||
|
||||
export const generateAgentToolValue = (value: Record<string, any>, formSchemas: { variable: string; default?: any; type: string }[], isReasoning = false) => {
|
||||
const newValues = {} as any
|
||||
|
||||
@ -94,7 +94,6 @@ export const useWorkflowInit = () => {
|
||||
|
||||
useEffect(() => {
|
||||
handleGetInitialWorkflowData()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
const handleFetchPreloadData = useCallback(async () => {
|
||||
|
||||
@ -60,7 +60,6 @@ const OperationDropdown: FC<Props> = ({
|
||||
downloadFile({ data: blob, fileName })
|
||||
setNeedDownload(false)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [blob])
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
|
||||
@ -56,7 +56,6 @@ const List = forwardRef<ListRef, ListProps>(({
|
||||
|
||||
useEffect(() => {
|
||||
handleScroll()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [list])
|
||||
|
||||
const handleHeadClick = () => {
|
||||
|
||||
@ -40,7 +40,6 @@ const DatasetsDetailProvider: FC<DatasetsDetailProviderProps> = ({
|
||||
}, [])
|
||||
if (allDatasetIds.length === 0) return
|
||||
updateDatasetsDetail(allDatasetIds)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
return (
|
||||
|
||||
@ -81,26 +81,26 @@ const HeaderInRestoring = ({
|
||||
<RestoringTitle />
|
||||
</div>
|
||||
<div className=' flex items-center justify-end gap-x-2'>
|
||||
<Button
|
||||
onClick={handleRestore}
|
||||
disabled={!currentVersion || currentVersion.version === WorkflowVersion.Draft}
|
||||
variant='primary'
|
||||
className={cn(
|
||||
theme === 'dark' && 'rounded-lg border border-black/5 bg-white/10 backdrop-blur-sm',
|
||||
)}
|
||||
>
|
||||
{t('workflow.common.restore')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleCancelRestore}
|
||||
className={cn(
|
||||
'text-components-button-secondary-accent-text',
|
||||
theme === 'dark' && 'rounded-lg border border-black/5 bg-white/10 backdrop-blur-sm',
|
||||
)}
|
||||
>
|
||||
<div className='flex items-center gap-x-0.5'>
|
||||
<RiHistoryLine className='h-4 w-4' />
|
||||
<span className='px-0.5'>{t('workflow.common.exitVersions')}</span>
|
||||
<Button
|
||||
onClick={handleRestore}
|
||||
disabled={!currentVersion || currentVersion.version === WorkflowVersion.Draft}
|
||||
variant='primary'
|
||||
className={cn(
|
||||
theme === 'dark' && 'rounded-lg border border-black/5 bg-white/10 backdrop-blur-sm',
|
||||
)}
|
||||
>
|
||||
{t('workflow.common.restore')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleCancelRestore}
|
||||
className={cn(
|
||||
'text-components-button-secondary-accent-text',
|
||||
theme === 'dark' && 'rounded-lg border border-black/5 bg-white/10 backdrop-blur-sm',
|
||||
)}
|
||||
>
|
||||
<div className='flex items-center gap-x-0.5'>
|
||||
<RiHistoryLine className='h-4 w-4' />
|
||||
<span className='px-0.5'>{t('workflow.common.exitVersions')}</span>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user