refactor: update installed app component to handle missing params and improve type safety (#27331)

This commit is contained in:
GuanMu
2025-10-27 14:38:58 +08:00
committed by GitHub
parent f06025a342
commit 43bcf40f80
49 changed files with 531 additions and 302 deletions

View File

@@ -2,14 +2,14 @@ import React from 'react'
import Main from '@/app/components/explore/installed-app'
export type IInstalledAppProps = {
params: {
params?: Promise<{
appId: string
}
}>
}
// Using Next.js page convention for async server components
async function InstalledApp({ params }: IInstalledAppProps) {
const appId = (await params).appId
const { appId } = await (params ?? Promise.reject(new Error('Missing params')))
return (
<Main id={appId} />
)

View File

@@ -0,0 +1,3 @@
export const OAUTH_AUTHORIZE_PENDING_KEY = 'oauth_authorize_pending'
export const REDIRECT_URL_KEY = 'oauth_redirect_url'
export const OAUTH_AUTHORIZE_PENDING_TTL = 60 * 3

View File

@@ -19,11 +19,11 @@ import {
} from '@remixicon/react'
import dayjs from 'dayjs'
import { useIsLogin } from '@/service/use-common'
export const OAUTH_AUTHORIZE_PENDING_KEY = 'oauth_authorize_pending'
export const REDIRECT_URL_KEY = 'oauth_redirect_url'
const OAUTH_AUTHORIZE_PENDING_TTL = 60 * 3
import {
OAUTH_AUTHORIZE_PENDING_KEY,
OAUTH_AUTHORIZE_PENDING_TTL,
REDIRECT_URL_KEY,
} from './constants'
function setItemWithExpiry(key: string, value: string, ttl: number) {
const item = {

View File

@@ -44,7 +44,7 @@ import { appDefaultIconBackground } from '@/config'
import type { PublishWorkflowParams } from '@/types/workflow'
import { useAppWhiteListSubjects, useGetUserCanAccessApp } from '@/service/access-control'
import { AccessMode } from '@/models/access-control'
import { fetchAppDetail } from '@/service/apps'
import { fetchAppDetailDirect } from '@/service/apps'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
@@ -162,11 +162,16 @@ const AppPublisher = ({
}
}, [appDetail?.id])
const handleAccessControlUpdate = useCallback(() => {
fetchAppDetail({ url: '/apps', id: appDetail!.id }).then((res) => {
const handleAccessControlUpdate = useCallback(async () => {
if (!appDetail)
return
try {
const res = await fetchAppDetailDirect({ url: '/apps', id: appDetail.id })
setAppDetail(res)
}
finally {
setShowAppAccessControl(false)
})
}
}, [appDetail, setAppDetail])
const [embeddingModalOpen, setEmbeddingModalOpen] = useState(false)

View File

@@ -14,7 +14,8 @@ import { TransferMethod } from '@/app/components/base/chat/types'
import { useEventEmitterContextContext } from '@/context/event-emitter'
import { useProviderContext } from '@/context/provider-context'
import { useFeatures } from '@/app/components/base/features/hooks'
import { noop } from 'lodash-es'
import { cloneDeep, noop } from 'lodash-es'
import { DEFAULT_CHAT_PROMPT_CONFIG, DEFAULT_COMPLETION_PROMPT_CONFIG } from '@/config'
type TextGenerationItemProps = {
modelAndParameter: ModelAndParameter
@@ -50,8 +51,8 @@ const TextGenerationItem: FC<TextGenerationItemProps> = ({
const config: TextGenerationConfig = {
pre_prompt: !isAdvancedMode ? modelConfig.configs.prompt_template : '',
prompt_type: promptMode,
chat_prompt_config: isAdvancedMode ? chatPromptConfig : {},
completion_prompt_config: isAdvancedMode ? completionPromptConfig : {},
chat_prompt_config: isAdvancedMode ? chatPromptConfig : cloneDeep(DEFAULT_CHAT_PROMPT_CONFIG),
completion_prompt_config: isAdvancedMode ? completionPromptConfig : cloneDeep(DEFAULT_COMPLETION_PROMPT_CONFIG),
user_input_form: promptVariablesToUserInputsForm(modelConfig.configs.prompt_variables),
dataset_query_variable: contextVar || '',
// features
@@ -74,6 +75,7 @@ const TextGenerationItem: FC<TextGenerationItemProps> = ({
datasets: [...postDatasets],
} as any,
},
system_parameters: modelConfig.system_parameters,
}
const {
completion,

View File

@@ -6,7 +6,7 @@ import {
import Chat from '@/app/components/base/chat/chat'
import { useChat } from '@/app/components/base/chat/chat/hooks'
import { useDebugConfigurationContext } from '@/context/debug-configuration'
import type { ChatConfig, ChatItem, ChatItemInTree, OnSend } from '@/app/components/base/chat/types'
import type { ChatConfig, ChatItem, OnSend } from '@/app/components/base/chat/types'
import { useProviderContext } from '@/context/provider-context'
import {
fetchConversationMessages,
@@ -126,7 +126,7 @@ const DebugWithSingleModel = (
)
}, [appId, chatList, checkCanSend, completionParams, config, handleSend, inputs, modelConfig.mode, modelConfig.model_id, modelConfig.provider, textGenerationModelList])
const doRegenerate = useCallback((chatItem: ChatItemInTree, editedQuestion?: { message: string, files?: FileEntity[] }) => {
const doRegenerate = useCallback((chatItem: ChatItem, editedQuestion?: { message: string, files?: FileEntity[] }) => {
const question = editedQuestion ? chatItem : chatList.find(item => item.id === chatItem.parentMessageId)!
const parentAnswer = chatList.find(item => item.id === question.parentMessageId)
doSend(editedQuestion ? editedQuestion.message : question.content,

View File

@@ -12,12 +12,15 @@ import type {
ChatConfig,
ChatItem,
} from '@/app/components/base/chat/types'
import cloneDeep from 'lodash-es/cloneDeep'
import {
AgentStrategy,
} from '@/types/app'
import { SupportUploadFileTypes } from '@/app/components/workflow/types'
import { promptVariablesToUserInputsForm } from '@/utils/model-config'
import { useDebugConfigurationContext } from '@/context/debug-configuration'
import { useEventEmitterContextContext } from '@/context/event-emitter'
import { DEFAULT_CHAT_PROMPT_CONFIG, DEFAULT_COMPLETION_PROMPT_CONFIG } from '@/config'
export const useDebugWithSingleOrMultipleModel = (appId: string) => {
const localeDebugWithSingleOrMultipleModelConfigs = localStorage.getItem('app-debug-with-single-or-multiple-models')
@@ -95,16 +98,14 @@ export const useConfigFromDebugContext = () => {
const config: ChatConfig = {
pre_prompt: !isAdvancedMode ? modelConfig.configs.prompt_template : '',
prompt_type: promptMode,
chat_prompt_config: isAdvancedMode ? chatPromptConfig : {},
completion_prompt_config: isAdvancedMode ? completionPromptConfig : {},
chat_prompt_config: isAdvancedMode ? chatPromptConfig : cloneDeep(DEFAULT_CHAT_PROMPT_CONFIG),
completion_prompt_config: isAdvancedMode ? completionPromptConfig : cloneDeep(DEFAULT_COMPLETION_PROMPT_CONFIG),
user_input_form: promptVariablesToUserInputsForm(modelConfig.configs.prompt_variables),
dataset_query_variable: contextVar || '',
opening_statement: introduction,
more_like_this: {
enabled: false,
},
more_like_this: modelConfig.more_like_this ?? { enabled: false },
suggested_questions: openingSuggestedQuestions,
suggested_questions_after_answer: suggestedQuestionsAfterAnswerConfig,
suggested_questions_after_answer: suggestedQuestionsAfterAnswerConfig ?? { enabled: false },
text_to_speech: textToSpeechConfig,
speech_to_text: speechToTextConfig,
retriever_resource: citationConfig,
@@ -121,8 +122,13 @@ export const useConfigFromDebugContext = () => {
},
file_upload: {
image: visionConfig,
allowed_file_upload_methods: visionConfig.transfer_methods ?? [],
allowed_file_types: [SupportUploadFileTypes.image],
max_length: visionConfig.number_limits ?? 0,
number_limits: visionConfig.number_limits,
},
annotation_reply: annotationConfig,
system_parameters: modelConfig.system_parameters,
supportAnnotation: true,
appId,

View File

@@ -3,6 +3,7 @@ import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import React, { useCallback, useEffect, useRef, useState } from 'react'
import { produce, setAutoFreeze } from 'immer'
import cloneDeep from 'lodash-es/cloneDeep'
import { useBoolean } from 'ahooks'
import {
RiAddLine,
@@ -36,7 +37,7 @@ import ActionButton, { ActionButtonState } from '@/app/components/base/action-bu
import type { ModelConfig as BackendModelConfig, VisionFile, VisionSettings } from '@/types/app'
import { formatBooleanInputs, promptVariablesToUserInputsForm } from '@/utils/model-config'
import TextGeneration from '@/app/components/app/text-generate/item'
import { IS_CE_EDITION } from '@/config'
import { DEFAULT_CHAT_PROMPT_CONFIG, DEFAULT_COMPLETION_PROMPT_CONFIG, IS_CE_EDITION } from '@/config'
import type { Inputs } from '@/models/debug'
import { useDefaultModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { ModelFeatureEnum, ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
@@ -90,6 +91,7 @@ const Debug: FC<IDebug> = ({
completionParams,
hasSetContextVar,
datasetConfigs,
externalDataToolsConfig,
} = useContext(ConfigContext)
const { eventEmitter } = useEventEmitterContextContext()
const { data: text2speechDefaultModel } = useDefaultModel(ModelTypeEnum.textEmbedding)
@@ -223,8 +225,8 @@ const Debug: FC<IDebug> = ({
const postModelConfig: BackendModelConfig = {
pre_prompt: !isAdvancedMode ? modelConfig.configs.prompt_template : '',
prompt_type: promptMode,
chat_prompt_config: {},
completion_prompt_config: {},
chat_prompt_config: isAdvancedMode ? chatPromptConfig : cloneDeep(DEFAULT_CHAT_PROMPT_CONFIG),
completion_prompt_config: isAdvancedMode ? completionPromptConfig : cloneDeep(DEFAULT_COMPLETION_PROMPT_CONFIG),
user_input_form: promptVariablesToUserInputsForm(modelConfig.configs.prompt_variables),
dataset_query_variable: contextVar || '',
dataset_configs: {
@@ -251,11 +253,8 @@ const Debug: FC<IDebug> = ({
suggested_questions_after_answer: suggestedQuestionsAfterAnswerConfig,
speech_to_text: speechToTextConfig,
retriever_resource: citationConfig,
}
if (isAdvancedMode) {
postModelConfig.chat_prompt_config = chatPromptConfig
postModelConfig.completion_prompt_config = completionPromptConfig
system_parameters: modelConfig.system_parameters,
external_data_tools: externalDataToolsConfig,
}
const data: Record<string, any> = {

View File

@@ -36,14 +36,14 @@ import type {
} from '@/models/debug'
import type { ExternalDataTool } from '@/models/common'
import type { DataSet } from '@/models/datasets'
import type { ModelConfig as BackendModelConfig, VisionSettings } from '@/types/app'
import type { ModelConfig as BackendModelConfig, UserInputFormItem, VisionSettings } from '@/types/app'
import ConfigContext from '@/context/debug-configuration'
import Config from '@/app/components/app/configuration/config'
import Debug from '@/app/components/app/configuration/debug'
import Confirm from '@/app/components/base/confirm'
import { ModelFeatureEnum, ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { ToastContext } from '@/app/components/base/toast'
import { fetchAppDetail, updateAppModelConfig } from '@/service/apps'
import { fetchAppDetailDirect, updateAppModelConfig } from '@/service/apps'
import { promptVariablesToUserInputsForm, userInputsFormToPromptVariables } from '@/utils/model-config'
import { fetchDatasets } from '@/service/datasets'
import { useProviderContext } from '@/context/provider-context'
@@ -186,6 +186,8 @@ const Configuration: FC = () => {
prompt_template: '',
prompt_variables: [] as PromptVariable[],
},
chat_prompt_config: clone(DEFAULT_CHAT_PROMPT_CONFIG),
completion_prompt_config: clone(DEFAULT_COMPLETION_PROMPT_CONFIG),
more_like_this: null,
opening_statement: '',
suggested_questions: [],
@@ -196,6 +198,14 @@ const Configuration: FC = () => {
suggested_questions_after_answer: null,
retriever_resource: null,
annotation_reply: null,
external_data_tools: [],
system_parameters: {
audio_file_size_limit: 0,
file_size_limit: 0,
image_file_size_limit: 0,
video_file_size_limit: 0,
workflow_file_upload_limit: 0,
},
dataSets: [],
agentConfig: DEFAULT_AGENT_SETTING,
})
@@ -543,169 +553,169 @@ const Configuration: FC = () => {
})
}
setCollectionList(collectionList)
fetchAppDetail({ url: '/apps', id: appId }).then(async (res: any) => {
setMode(res.mode)
const modelConfig = res.model_config
const promptMode = modelConfig.prompt_type === PromptMode.advanced ? PromptMode.advanced : PromptMode.simple
doSetPromptMode(promptMode)
if (promptMode === PromptMode.advanced) {
if (modelConfig.chat_prompt_config && modelConfig.chat_prompt_config.prompt.length > 0)
setChatPromptConfig(modelConfig.chat_prompt_config)
else
setChatPromptConfig(clone(DEFAULT_CHAT_PROMPT_CONFIG))
setCompletionPromptConfig(modelConfig.completion_prompt_config || clone(DEFAULT_COMPLETION_PROMPT_CONFIG) as any)
setCanReturnToSimpleMode(false)
}
const res = await fetchAppDetailDirect({ url: '/apps', id: appId })
setMode(res.mode)
const modelConfig = res.model_config as BackendModelConfig
const promptMode = modelConfig.prompt_type === PromptMode.advanced ? PromptMode.advanced : PromptMode.simple
doSetPromptMode(promptMode)
if (promptMode === PromptMode.advanced) {
if (modelConfig.chat_prompt_config && modelConfig.chat_prompt_config.prompt.length > 0)
setChatPromptConfig(modelConfig.chat_prompt_config)
else
setChatPromptConfig(clone(DEFAULT_CHAT_PROMPT_CONFIG))
setCompletionPromptConfig(modelConfig.completion_prompt_config || clone(DEFAULT_COMPLETION_PROMPT_CONFIG) as any)
setCanReturnToSimpleMode(false)
}
const model = res.model_config.model
const model = modelConfig.model
let datasets: any = null
let datasets: any = null
// old dataset struct
if (modelConfig.agent_mode?.tools?.find(({ dataset }: any) => dataset?.enabled))
datasets = modelConfig.agent_mode?.tools.filter(({ dataset }: any) => dataset?.enabled)
if (modelConfig.agent_mode?.tools?.find(({ dataset }: any) => dataset?.enabled))
datasets = modelConfig.agent_mode?.tools.filter(({ dataset }: any) => dataset?.enabled)
// new dataset struct
else if (modelConfig.dataset_configs.datasets?.datasets?.length > 0)
datasets = modelConfig.dataset_configs?.datasets?.datasets
else if (modelConfig.dataset_configs.datasets?.datasets?.length > 0)
datasets = modelConfig.dataset_configs?.datasets?.datasets
if (dataSets && datasets?.length && datasets?.length > 0) {
const { data: dataSetsWithDetail } = await fetchDatasets({ url: '/datasets', params: { page: 1, ids: datasets.map(({ dataset }: any) => dataset.id) } })
datasets = dataSetsWithDetail
setDataSets(datasets)
}
if (dataSets && datasets?.length && datasets?.length > 0) {
const { data: dataSetsWithDetail } = await fetchDatasets({ url: '/datasets', params: { page: 1, ids: datasets.map(({ dataset }: any) => dataset.id) } })
datasets = dataSetsWithDetail
setDataSets(datasets)
}
setIntroduction(modelConfig.opening_statement)
setSuggestedQuestions(modelConfig.suggested_questions || [])
if (modelConfig.more_like_this)
setMoreLikeThisConfig(modelConfig.more_like_this)
setIntroduction(modelConfig.opening_statement)
setSuggestedQuestions(modelConfig.suggested_questions || [])
if (modelConfig.more_like_this)
setMoreLikeThisConfig(modelConfig.more_like_this)
if (modelConfig.suggested_questions_after_answer)
setSuggestedQuestionsAfterAnswerConfig(modelConfig.suggested_questions_after_answer)
if (modelConfig.suggested_questions_after_answer)
setSuggestedQuestionsAfterAnswerConfig(modelConfig.suggested_questions_after_answer)
if (modelConfig.speech_to_text)
setSpeechToTextConfig(modelConfig.speech_to_text)
if (modelConfig.speech_to_text)
setSpeechToTextConfig(modelConfig.speech_to_text)
if (modelConfig.text_to_speech)
setTextToSpeechConfig(modelConfig.text_to_speech)
if (modelConfig.text_to_speech)
setTextToSpeechConfig(modelConfig.text_to_speech)
if (modelConfig.retriever_resource)
setCitationConfig(modelConfig.retriever_resource)
if (modelConfig.retriever_resource)
setCitationConfig(modelConfig.retriever_resource)
if (modelConfig.annotation_reply) {
let annotationConfig = modelConfig.annotation_reply
if (modelConfig.annotation_reply.enabled) {
annotationConfig = {
...modelConfig.annotation_reply,
embedding_model: {
...modelConfig.annotation_reply.embedding_model,
embedding_provider_name: correctModelProvider(modelConfig.annotation_reply.embedding_model.embedding_provider_name),
},
}
if (modelConfig.annotation_reply) {
let annotationConfig = modelConfig.annotation_reply
if (modelConfig.annotation_reply.enabled) {
annotationConfig = {
...modelConfig.annotation_reply,
embedding_model: {
...modelConfig.annotation_reply.embedding_model,
embedding_provider_name: correctModelProvider(modelConfig.annotation_reply.embedding_model.embedding_provider_name),
},
}
setAnnotationConfig(annotationConfig, true)
}
setAnnotationConfig(annotationConfig, true)
}
if (modelConfig.sensitive_word_avoidance)
setModerationConfig(modelConfig.sensitive_word_avoidance)
if (modelConfig.sensitive_word_avoidance)
setModerationConfig(modelConfig.sensitive_word_avoidance)
if (modelConfig.external_data_tools)
setExternalDataToolsConfig(modelConfig.external_data_tools)
if (modelConfig.external_data_tools)
setExternalDataToolsConfig(modelConfig.external_data_tools)
const config = {
modelConfig: {
provider: correctModelProvider(model.provider),
model_id: model.name,
mode: model.mode,
configs: {
prompt_template: modelConfig.pre_prompt || '',
prompt_variables: userInputsFormToPromptVariables(
[
...modelConfig.user_input_form,
...(
modelConfig.external_data_tools?.length
? modelConfig.external_data_tools.map((item: any) => {
return {
external_data_tool: {
variable: item.variable as string,
label: item.label as string,
enabled: item.enabled,
type: item.type as string,
config: item.config,
required: true,
icon: item.icon,
icon_background: item.icon_background,
},
}
})
: []
),
],
modelConfig.dataset_query_variable,
),
},
more_like_this: modelConfig.more_like_this,
opening_statement: modelConfig.opening_statement,
suggested_questions: modelConfig.suggested_questions,
sensitive_word_avoidance: modelConfig.sensitive_word_avoidance,
speech_to_text: modelConfig.speech_to_text,
text_to_speech: modelConfig.text_to_speech,
file_upload: modelConfig.file_upload,
suggested_questions_after_answer: modelConfig.suggested_questions_after_answer,
retriever_resource: modelConfig.retriever_resource,
annotation_reply: modelConfig.annotation_reply,
external_data_tools: modelConfig.external_data_tools,
dataSets: datasets || [],
agentConfig: res.mode === 'agent-chat' ? {
max_iteration: DEFAULT_AGENT_SETTING.max_iteration,
...modelConfig.agent_mode,
const config: PublishConfig = {
modelConfig: {
provider: correctModelProvider(model.provider),
model_id: model.name,
mode: model.mode,
configs: {
prompt_template: modelConfig.pre_prompt || '',
prompt_variables: userInputsFormToPromptVariables(
([
...modelConfig.user_input_form,
...(
modelConfig.external_data_tools?.length
? modelConfig.external_data_tools.map((item: any) => {
return {
external_data_tool: {
variable: item.variable as string,
label: item.label as string,
enabled: item.enabled,
type: item.type as string,
config: item.config,
required: true,
icon: item.icon,
icon_background: item.icon_background,
},
}
})
: []
),
]) as unknown as UserInputFormItem[],
modelConfig.dataset_query_variable,
),
},
more_like_this: modelConfig.more_like_this ?? { enabled: false },
opening_statement: modelConfig.opening_statement,
suggested_questions: modelConfig.suggested_questions ?? [],
sensitive_word_avoidance: modelConfig.sensitive_word_avoidance,
speech_to_text: modelConfig.speech_to_text,
text_to_speech: modelConfig.text_to_speech,
file_upload: modelConfig.file_upload ?? null,
suggested_questions_after_answer: modelConfig.suggested_questions_after_answer ?? { enabled: false },
retriever_resource: modelConfig.retriever_resource,
annotation_reply: modelConfig.annotation_reply ?? null,
external_data_tools: modelConfig.external_data_tools ?? [],
system_parameters: modelConfig.system_parameters,
dataSets: datasets || [],
agentConfig: res.mode === 'agent-chat' ? {
max_iteration: DEFAULT_AGENT_SETTING.max_iteration,
...modelConfig.agent_mode,
// remove dataset
enabled: true, // modelConfig.agent_mode?.enabled is not correct. old app: the value of app with dataset's is always true
tools: modelConfig.agent_mode?.tools.filter((tool: any) => {
return !tool.dataset
}).map((tool: any) => {
const toolInCollectionList = collectionList.find(c => tool.provider_id === c.id)
return {
...tool,
isDeleted: res.deleted_tools?.some((deletedTool: any) => deletedTool.id === tool.id && deletedTool.tool_name === tool.tool_name),
notAuthor: toolInCollectionList?.is_team_authorization === false,
...(tool.provider_type === 'builtin' ? {
provider_id: correctToolProvider(tool.provider_name, !!toolInCollectionList),
provider_name: correctToolProvider(tool.provider_name, !!toolInCollectionList),
} : {}),
}
}),
} : DEFAULT_AGENT_SETTING,
},
completionParams: model.completion_params,
}
enabled: true, // modelConfig.agent_mode?.enabled is not correct. old app: the value of app with dataset's is always true
tools: (modelConfig.agent_mode?.tools ?? []).filter((tool: any) => {
return !tool.dataset
}).map((tool: any) => {
const toolInCollectionList = collectionList.find(c => tool.provider_id === c.id)
return {
...tool,
isDeleted: res.deleted_tools?.some((deletedTool: any) => deletedTool.id === tool.id && deletedTool.tool_name === tool.tool_name) ?? false,
notAuthor: toolInCollectionList?.is_team_authorization === false,
...(tool.provider_type === 'builtin' ? {
provider_id: correctToolProvider(tool.provider_name, !!toolInCollectionList),
provider_name: correctToolProvider(tool.provider_name, !!toolInCollectionList),
} : {}),
}
}),
strategy: modelConfig.agent_mode?.strategy ?? AgentStrategy.react,
} : DEFAULT_AGENT_SETTING,
},
completionParams: model.completion_params,
}
if (modelConfig.file_upload)
handleSetVisionConfig(modelConfig.file_upload.image, true)
if (modelConfig.file_upload)
handleSetVisionConfig(modelConfig.file_upload.image, true)
syncToPublishedConfig(config)
setPublishedConfig(config)
const retrievalConfig = getMultipleRetrievalConfig({
...modelConfig.dataset_configs,
reranking_model: modelConfig.dataset_configs.reranking_model && {
provider: modelConfig.dataset_configs.reranking_model.reranking_provider_name,
model: modelConfig.dataset_configs.reranking_model.reranking_model_name,
},
}, datasets, datasets, {
provider: currentRerankProvider?.provider,
model: currentRerankModel?.model,
})
setDatasetConfigs({
retrieval_model: RETRIEVE_TYPE.multiWay,
...modelConfig.dataset_configs,
...retrievalConfig,
...(retrievalConfig.reranking_model ? {
reranking_model: {
reranking_model_name: retrievalConfig.reranking_model.model,
reranking_provider_name: correctModelProvider(retrievalConfig.reranking_model.provider),
},
} : {}),
})
setHasFetchedDetail(true)
syncToPublishedConfig(config)
setPublishedConfig(config)
const retrievalConfig = getMultipleRetrievalConfig({
...modelConfig.dataset_configs,
reranking_model: modelConfig.dataset_configs.reranking_model && {
provider: modelConfig.dataset_configs.reranking_model.reranking_provider_name,
model: modelConfig.dataset_configs.reranking_model.reranking_model_name,
},
}, datasets, datasets, {
provider: currentRerankProvider?.provider,
model: currentRerankModel?.model,
})
setDatasetConfigs({
...modelConfig.dataset_configs,
...retrievalConfig,
...(retrievalConfig.reranking_model ? {
reranking_model: {
reranking_model_name: retrievalConfig.reranking_model.model,
reranking_provider_name: correctModelProvider(retrievalConfig.reranking_model.provider),
},
} : {}),
} as DatasetConfigs)
setHasFetchedDetail(true)
})()
}, [appId])
@@ -780,8 +790,8 @@ const Configuration: FC = () => {
// Simple Mode prompt
pre_prompt: !isAdvancedMode ? promptTemplate : '',
prompt_type: promptMode,
chat_prompt_config: {},
completion_prompt_config: {},
chat_prompt_config: isAdvancedMode ? chatPromptConfig : clone(DEFAULT_CHAT_PROMPT_CONFIG),
completion_prompt_config: isAdvancedMode ? completionPromptConfig : clone(DEFAULT_COMPLETION_PROMPT_CONFIG),
user_input_form: promptVariablesToUserInputsForm(promptVariables),
dataset_query_variable: contextVar || '',
// features
@@ -798,6 +808,7 @@ const Configuration: FC = () => {
...modelConfig.agentConfig,
strategy: isFunctionCall ? AgentStrategy.functionCall : AgentStrategy.react,
},
external_data_tools: externalDataToolsConfig,
model: {
provider: modelAndParameter?.provider || modelConfig.provider,
name: modelId,
@@ -810,11 +821,7 @@ const Configuration: FC = () => {
datasets: [...postDatasets],
} as any,
},
}
if (isAdvancedMode) {
data.chat_prompt_config = chatPromptConfig
data.completion_prompt_config = completionPromptConfig
system_parameters: modelConfig.system_parameters,
}
await updateAppModelConfig({ url: `/apps/${appId}/model-config`, body: data })

View File

@@ -3,7 +3,6 @@ import Chat from '../chat'
import type {
ChatConfig,
ChatItem,
ChatItemInTree,
OnSend,
} from '../types'
import { useChat } from '../chat/hooks'
@@ -149,7 +148,7 @@ const ChatWrapper = () => {
)
}, [chatList, handleNewConversationCompleted, handleSend, currentConversationId, currentConversationInputs, newConversationInputs, isInstalledApp, appId])
const doRegenerate = useCallback((chatItem: ChatItemInTree, editedQuestion?: { message: string, files?: FileEntity[] }) => {
const doRegenerate = useCallback((chatItem: ChatItem, editedQuestion?: { message: string, files?: FileEntity[] }) => {
const question = editedQuestion ? chatItem : chatList.find(item => item.id === chatItem.parentMessageId)!
const parentAnswer = chatList.find(item => item.id === question.parentMessageId)
doSend(editedQuestion ? editedQuestion.message : question.content,

View File

@@ -3,7 +3,6 @@ import Chat from '../chat'
import type {
ChatConfig,
ChatItem,
ChatItemInTree,
OnSend,
} from '../types'
import { useChat } from '../chat/hooks'
@@ -147,7 +146,7 @@ const ChatWrapper = () => {
)
}, [currentConversationId, currentConversationInputs, newConversationInputs, chatList, handleSend, isInstalledApp, appId, handleNewConversationCompleted])
const doRegenerate = useCallback((chatItem: ChatItemInTree, editedQuestion?: { message: string, files?: FileEntity[] }) => {
const doRegenerate = useCallback((chatItem: ChatItem, editedQuestion?: { message: string, files?: FileEntity[] }) => {
const question = editedQuestion ? chatItem : chatList.find(item => item.id === chatItem.parentMessageId)!
const parentAnswer = chatList.find(item => item.id === question.parentMessageId)
doSend(editedQuestion ? editedQuestion.message : question.content,

View File

@@ -85,7 +85,7 @@ export type OnSend = {
(message: string, files: FileEntity[] | undefined, isRegenerate: boolean, lastAnswer?: ChatItem | null): void
}
export type OnRegenerate = (chatItem: ChatItem) => void
export type OnRegenerate = (chatItem: ChatItem, editedQuestion?: { message: string; files?: FileEntity[] }) => void
export type Callback = {
onSuccess: () => void

View File

@@ -32,6 +32,7 @@ const meta = {
},
args: {
show: false,
children: null,
},
} satisfies Meta<typeof ContentDialog>
@@ -92,6 +93,9 @@ const DemoWrapper = (props: Props) => {
}
export const Default: Story = {
args: {
children: null,
},
render: args => <DemoWrapper {...args} />,
}
@@ -99,6 +103,7 @@ export const NarrowPanel: Story = {
render: args => <DemoWrapper {...args} />,
args: {
className: 'max-w-[420px]',
children: null,
},
parameters: {
docs: {

View File

@@ -3,6 +3,7 @@ import { fireEvent, render, screen } from '@testing-library/react'
import TimePicker from './index'
import dayjs from '../utils/dayjs'
import { isDayjsObject } from '../utils/dayjs'
import type { TimePickerProps } from '../types'
jest.mock('react-i18next', () => ({
useTranslation: () => ({
@@ -30,9 +31,10 @@ jest.mock('./options', () => () => <div data-testid="time-options" />)
jest.mock('./header', () => () => <div data-testid="time-header" />)
describe('TimePicker', () => {
const baseProps = {
const baseProps: Pick<TimePickerProps, 'onChange' | 'onClear' | 'value'> = {
onChange: jest.fn(),
onClear: jest.fn(),
value: undefined,
}
beforeEach(() => {

View File

@@ -150,7 +150,7 @@ export const toDayjs = (value: string | Dayjs | undefined, options: ToDayjsOptio
if (format) {
const parsedWithFormat = tzName
? dayjs.tz(trimmed, format, tzName, true)
? dayjs(trimmed, format, true).tz(tzName, true)
: dayjs(trimmed, format, true)
if (parsedWithFormat.isValid())
return parsedWithFormat
@@ -191,7 +191,7 @@ export const toDayjs = (value: string | Dayjs | undefined, options: ToDayjsOptio
const candidateFormats = formats ?? COMMON_PARSE_FORMATS
for (const fmt of candidateFormats) {
const parsed = tzName
? dayjs.tz(trimmed, fmt, tzName, true)
? dayjs(trimmed, fmt, true).tz(tzName, true)
: dayjs(trimmed, fmt, true)
if (parsed.isValid())
return parsed

View File

@@ -47,6 +47,7 @@ const meta = {
args: {
title: 'Manage API Keys',
show: false,
children: null,
},
} satisfies Meta<typeof Dialog>
@@ -102,6 +103,7 @@ export const Default: Story = {
</button>
</>
),
children: null,
},
}
@@ -110,6 +112,7 @@ export const WithoutFooter: Story = {
args: {
footer: undefined,
title: 'Read-only summary',
children: null,
},
parameters: {
docs: {
@@ -140,6 +143,7 @@ export const CustomStyling: Story = {
</div>
</>
),
children: null,
},
parameters: {
docs: {

View File

@@ -42,7 +42,7 @@ export type FormOption = {
icon?: string
}
export type AnyValidators = FieldValidators<any, any, any, any, any, any, any, any, any, any>
export type AnyValidators = FieldValidators<any, any, any, any, any, any, any, any, any, any, any, any>
export type FormSchema = {
type: FormTypeEnum

View File

@@ -1,6 +1,7 @@
import React, { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useChatContext } from '../chat/chat/context'
import cn from '@/utils/classnames'
const hasEndThink = (children: any): boolean => {
if (typeof children === 'string')
@@ -40,7 +41,7 @@ const useThinkTimer = (children: any) => {
const [startTime] = useState(() => Date.now())
const [elapsedTime, setElapsedTime] = useState(0)
const [isComplete, setIsComplete] = useState(false)
const timerRef = useRef<NodeJS.Timeout>()
const timerRef = useRef<NodeJS.Timeout | null>(null)
useEffect(() => {
if (isComplete) return
@@ -63,16 +64,26 @@ const useThinkTimer = (children: any) => {
return { elapsedTime, isComplete }
}
const ThinkBlock = ({ children, ...props }: React.ComponentProps<'details'>) => {
type ThinkBlockProps = React.ComponentProps<'details'> & {
'data-think'?: boolean
}
const ThinkBlock = ({ children, ...props }: ThinkBlockProps) => {
const { elapsedTime, isComplete } = useThinkTimer(children)
const displayContent = removeEndThink(children)
const { t } = useTranslation()
const { 'data-think': isThink = false, className, open, ...rest } = props
if (!(props['data-think'] ?? false))
if (!isThink)
return (<details {...props}>{children}</details>)
return (
<details {...(!isComplete && { open: true })} className="group">
<details
{...rest}
data-think={isThink}
className={cn('group', className)}
open={isComplete ? open : true}
>
<summary className="flex cursor-pointer select-none list-none items-center whitespace-nowrap pl-2 font-bold text-text-secondary">
<div className="flex shrink-0 items-center">
<svg

View File

@@ -45,6 +45,7 @@ const meta = {
hideCloseBtn: false,
onClose: () => console.log('close'),
onConfirm: () => console.log('confirm'),
children: null,
},
} satisfies Meta<typeof ModalLikeWrap>
@@ -68,6 +69,9 @@ export const Default: Story = {
<BaseContent />
</ModalLikeWrap>
),
args: {
children: null,
},
}
export const WithBackLink: Story = {
@@ -90,6 +94,7 @@ export const WithBackLink: Story = {
),
args: {
title: 'Select metadata type',
children: null,
},
parameters: {
docs: {
@@ -114,6 +119,7 @@ export const CustomWidth: Story = {
),
args: {
title: 'Advanced configuration',
children: null,
},
parameters: {
docs: {

View File

@@ -1,5 +1,5 @@
import { Popover, PopoverButton, PopoverPanel, Transition } from '@headlessui/react'
import { Fragment, cloneElement, useRef } from 'react'
import { Fragment, cloneElement, isValidElement, useRef } from 'react'
import cn from '@/utils/classnames'
export type HtmlContentProps = {
@@ -103,15 +103,17 @@ export default function CustomPopover({
})
}
>
{cloneElement(htmlContent as React.ReactElement, {
open,
onClose: close,
...(manualClose
? {
onClick: close,
}
: {}),
})}
{isValidElement(htmlContent)
? cloneElement(htmlContent as React.ReactElement<HtmlContentProps>, {
open,
onClose: close,
...(manualClose
? {
onClick: close,
}
: {}),
})
: htmlContent}
</div>
)}
</PopoverPanel>

View File

@@ -125,7 +125,7 @@ export const PortalToFollowElemTrigger = (
children,
asChild = false,
...props
}: React.HTMLProps<HTMLElement> & { ref?: React.RefObject<HTMLElement>, asChild?: boolean },
}: React.HTMLProps<HTMLElement> & { ref?: React.RefObject<HTMLElement | null>, asChild?: boolean },
) => {
const context = usePortalToFollowElemContext()
const childrenRef = (children as any).props?.ref
@@ -133,12 +133,13 @@ export const PortalToFollowElemTrigger = (
// `asChild` allows the user to pass any element as the anchor
if (asChild && React.isValidElement(children)) {
const childProps = (children.props ?? {}) as Record<string, unknown>
return React.cloneElement(
children,
context.getReferenceProps({
ref,
...props,
...children.props,
...childProps,
'data-state': context.open ? 'open' : 'closed',
} as React.HTMLProps<HTMLElement>),
)
@@ -164,7 +165,7 @@ export const PortalToFollowElemContent = (
style,
...props
}: React.HTMLProps<HTMLDivElement> & {
ref?: React.RefObject<HTMLDivElement>;
ref?: React.RefObject<HTMLDivElement | null>;
},
) => {
const context = usePortalToFollowElemContext()

View File

@@ -35,7 +35,7 @@ import { DELETE_QUERY_BLOCK_COMMAND } from './plugins/query-block'
import type { CustomTextNode } from './plugins/custom-text/node'
import { registerLexicalTextEntity } from './utils'
export type UseSelectOrDeleteHandler = (nodeKey: string, command?: LexicalCommand<undefined>) => [RefObject<HTMLDivElement>, boolean]
export type UseSelectOrDeleteHandler = (nodeKey: string, command?: LexicalCommand<undefined>) => [RefObject<HTMLDivElement | null>, boolean]
export const useSelectOrDelete: UseSelectOrDeleteHandler = (nodeKey: string, command?: LexicalCommand<undefined>) => {
const ref = useRef<HTMLDivElement>(null)
const [editor] = useLexicalComposerContext()
@@ -110,7 +110,7 @@ export const useSelectOrDelete: UseSelectOrDeleteHandler = (nodeKey: string, com
return [ref, isSelected]
}
export type UseTriggerHandler = () => [RefObject<HTMLDivElement>, boolean, Dispatch<SetStateAction<boolean>>]
export type UseTriggerHandler = () => [RefObject<HTMLDivElement | null>, boolean, Dispatch<SetStateAction<boolean>>]
export const useTrigger: UseTriggerHandler = () => {
const triggerRef = useRef<HTMLDivElement>(null)
const [open, setOpen] = useState(false)

View File

@@ -1,4 +1,5 @@
import { memo } from 'react'
import type { ReactNode } from 'react'
import { useTranslation } from 'react-i18next'
import cn from '@/utils/classnames'
@@ -8,7 +9,7 @@ const Placeholder = ({
className,
}: {
compact?: boolean
value?: string | JSX.Element
value?: ReactNode
className?: string
}) => {
const { t } = useTranslation()

View File

@@ -14,13 +14,19 @@ export const convertToMp3 = (recorder: any) => {
const { channels, sampleRate } = wav
const mp3enc = new lamejs.Mp3Encoder(channels, sampleRate, 128)
const result = recorder.getChannelData()
const buffer = []
const buffer: BlobPart[] = []
const leftData = result.left && new Int16Array(result.left.buffer, 0, result.left.byteLength / 2)
const rightData = result.right && new Int16Array(result.right.buffer, 0, result.right.byteLength / 2)
const remaining = leftData.length + (rightData ? rightData.length : 0)
const maxSamples = 1152
const toArrayBuffer = (bytes: Int8Array) => {
const arrayBuffer = new ArrayBuffer(bytes.length)
new Uint8Array(arrayBuffer).set(bytes)
return arrayBuffer
}
for (let i = 0; i < remaining; i += maxSamples) {
const left = leftData.subarray(i, i + maxSamples)
let right = null
@@ -35,13 +41,13 @@ export const convertToMp3 = (recorder: any) => {
}
if (mp3buf.length > 0)
buffer.push(mp3buf)
buffer.push(toArrayBuffer(mp3buf))
}
const enc = mp3enc.flush()
if (enc.length > 0)
buffer.push(enc)
buffer.push(toArrayBuffer(enc))
return new Blob(buffer, { type: 'audio/mp3' })
}

View File

@@ -32,7 +32,6 @@ const Pricing: FC<PricingProps> = ({
const [planRange, setPlanRange] = React.useState<PlanRange>(PlanRange.monthly)
const [currentCategory, setCurrentCategory] = useState<Category>(CategoryEnum.CLOUD)
const canPay = isCurrentWorkspaceManager
useKeyPress(['esc'], onCancel)
const pricingPageLanguage = useGetPricingPageLanguage()

View File

@@ -6,7 +6,7 @@ import SelfHostedPlanItem from './self-hosted-plan-item'
type PlansProps = {
plan: {
type: BasicPlan
type: Plan
usage: UsagePlanInfo
total: UsagePlanInfo
}
@@ -21,6 +21,7 @@ const Plans = ({
planRange,
canPay,
}: PlansProps) => {
const currentPlanType: BasicPlan = plan.type === Plan.enterprise ? Plan.team : plan.type
return (
<div className='flex w-full justify-center border-t border-divider-accent px-10'>
<div className='flex max-w-[1680px] grow border-x border-divider-accent'>
@@ -28,21 +29,21 @@ const Plans = ({
currentPlan === 'cloud' && (
<>
<CloudPlanItem
currentPlan={plan.type}
currentPlan={currentPlanType}
plan={Plan.sandbox}
planRange={planRange}
canPay={canPay}
/>
<Divider type='vertical' className='mx-0 shrink-0 bg-divider-accent' />
<CloudPlanItem
currentPlan={plan.type}
currentPlan={currentPlanType}
plan={Plan.professional}
planRange={planRange}
canPay={canPay}
/>
<Divider type='vertical' className='mx-0 shrink-0 bg-divider-accent' />
<CloudPlanItem
currentPlan={plan.type}
currentPlan={currentPlanType}
plan={Plan.team}
planRange={planRange}
canPay={canPay}

View File

@@ -18,7 +18,13 @@ import DocumentFileIcon from '../../common/document-file-icon'
import cn from '@/utils/classnames'
import { FieldInfo } from '@/app/components/datasets/documents/detail/metadata'
import Button from '@/app/components/base/button'
import type { FullDocumentDetail, IndexingStatusResponse, ProcessRuleResponse } from '@/models/datasets'
import type {
DataSourceInfo,
FullDocumentDetail,
IndexingStatusResponse,
LegacyDataSourceInfo,
ProcessRuleResponse,
} from '@/models/datasets'
import { fetchIndexingStatusBatch as doFetchIndexingStatus, fetchProcessRule } from '@/service/datasets'
import { DataSourceType, ProcessMode } from '@/models/datasets'
import NotionIcon from '@/app/components/base/notion-icon'
@@ -241,10 +247,16 @@ const EmbeddingProcess: FC<Props> = ({ datasetId, batchId, documents = [], index
return doc?.data_source_type as DataSourceType
}
const isLegacyDataSourceInfo = (info: DataSourceInfo): info is LegacyDataSourceInfo => {
return info != null && typeof (info as LegacyDataSourceInfo).upload_file === 'object'
}
const getIcon = (id: string) => {
const doc = documents.find(document => document.id === id)
return doc?.data_source_info.notion_page_icon
const info = doc?.data_source_info
if (info && isLegacyDataSourceInfo(info))
return info.notion_page_icon
return undefined
}
const isSourceEmbedding = (detail: IndexingStatusResponse) =>
['indexing', 'splitting', 'parsing', 'cleaning', 'waiting'].includes(detail.indexing_status || '')

View File

@@ -105,6 +105,8 @@ const FileUploader = ({
return isValidType && isValidSize
}, [fileUploadConfig, notify, t, ACCEPTS])
type UploadResult = Awaited<ReturnType<typeof upload>>
const fileUpload = useCallback(async (fileItem: FileItem): Promise<FileItem> => {
const formData = new FormData()
formData.append('file', fileItem.file)

View File

@@ -121,6 +121,8 @@ const LocalFile = ({
return isValidType && isValidSize
}, [fileUploadConfig, notify, t, ACCEPTS])
type UploadResult = Awaited<ReturnType<typeof upload>>
const fileUpload = useCallback(async (fileItem: FileItem): Promise<FileItem> => {
const formData = new FormData()
formData.append('file', fileItem.file)
@@ -136,10 +138,14 @@ const LocalFile = ({
data: formData,
onprogress: onProgress,
}, false, undefined, '?source=datasets')
.then((res: File) => {
const completeFile = {
.then((res: UploadResult) => {
const updatedFile = Object.assign({}, fileItem.file, {
id: res.id,
...(res as Partial<File>),
}) as File
const completeFile: FileItem = {
fileID: fileItem.fileID,
file: res,
file: updatedFile,
progress: -1,
}
const index = fileListRef.current.findIndex(item => item.fileID === fileItem.fileID)

View File

@@ -38,6 +38,8 @@ const CSVUploader: FC<Props> = ({
file_size_limit: 15,
}, [fileUploadConfigResponse])
type UploadResult = Awaited<ReturnType<typeof upload>>
const fileUpload = useCallback(async (fileItem: FileItem): Promise<FileItem> => {
fileItem.progress = 0
@@ -58,10 +60,14 @@ const CSVUploader: FC<Props> = ({
data: formData,
onprogress: onProgress,
}, false, undefined, '?source=datasets')
.then((res: File) => {
const completeFile = {
.then((res: UploadResult) => {
const updatedFile = Object.assign({}, fileItem.file, {
id: res.id,
...(res as Partial<File>),
}) as File
const completeFile: FileItem = {
fileID: fileItem.fileID,
file: res,
file: updatedFile,
progress: 100,
}
updateFile(completeFile)

View File

@@ -17,7 +17,7 @@ import Divider from '@/app/components/base/divider'
import Loading from '@/app/components/base/loading'
import Toast from '@/app/components/base/toast'
import { ChunkingMode } from '@/models/datasets'
import type { FileItem } from '@/models/datasets'
import type { DataSourceInfo, FileItem, LegacyDataSourceInfo } from '@/models/datasets'
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
import FloatRightContainer from '@/app/components/base/float-right-container'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
@@ -109,6 +109,18 @@ const DocumentDetail: FC<DocumentDetailProps> = ({ datasetId, documentId }) => {
const embedding = ['queuing', 'indexing', 'paused'].includes((documentDetail?.display_status || '').toLowerCase())
const isLegacyDataSourceInfo = (info?: DataSourceInfo): info is LegacyDataSourceInfo => {
return !!info && 'upload_file' in info
}
const documentUploadFile = useMemo(() => {
if (!documentDetail?.data_source_info)
return undefined
if (isLegacyDataSourceInfo(documentDetail.data_source_info))
return documentDetail.data_source_info.upload_file
return undefined
}, [documentDetail?.data_source_info])
const invalidChunkList = useInvalid(useSegmentListKey)
const invalidChildChunkList = useInvalid(useChildSegmentListKey)
const invalidDocumentList = useInvalidDocumentList(datasetId)
@@ -153,7 +165,7 @@ const DocumentDetail: FC<DocumentDetailProps> = ({ datasetId, documentId }) => {
</div>
<DocumentTitle
datasetId={datasetId}
extension={documentDetail?.data_source_info?.upload_file?.extension}
extension={documentUploadFile?.extension}
name={documentDetail?.name}
wrapperCls='mr-2'
parent_mode={parentMode}

View File

@@ -131,9 +131,15 @@ type IMetadataProps = {
onUpdate: () => void
}
type MetadataState = {
documentType?: DocType | ''
metadata: Record<string, string>
}
const Metadata: FC<IMetadataProps> = ({ docDetail, loading, onUpdate }) => {
const { doc_metadata = {} } = docDetail || {}
const doc_type = docDetail?.doc_type || ''
const rawDocType = docDetail?.doc_type ?? ''
const doc_type = rawDocType === 'others' ? '' : rawDocType
const { t } = useTranslation()
const metadataMap = useMetadataMap()
@@ -143,18 +149,16 @@ const Metadata: FC<IMetadataProps> = ({ docDetail, loading, onUpdate }) => {
const businessDocCategoryMap = useBusinessDocCategories()
const [editStatus, setEditStatus] = useState(!doc_type) // if no documentType, in editing status by default
// the initial values are according to the documentType
const [metadataParams, setMetadataParams] = useState<{
documentType?: DocType | ''
metadata: { [key: string]: string }
}>(
const [metadataParams, setMetadataParams] = useState<MetadataState>(
doc_type
? {
documentType: doc_type,
metadata: doc_metadata || {},
documentType: doc_type as DocType,
metadata: (doc_metadata || {}) as Record<string, string>,
}
: { metadata: {} })
: { metadata: {} },
)
const [showDocTypes, setShowDocTypes] = useState(!doc_type) // whether show doc types
const [tempDocType, setTempDocType] = useState<DocType | undefined | ''>('') // for remember icon click
const [tempDocType, setTempDocType] = useState<DocType | ''>('') // for remember icon click
const [saveLoading, setSaveLoading] = useState(false)
const { notify } = useContext(ToastContext)
@@ -165,13 +169,13 @@ const Metadata: FC<IMetadataProps> = ({ docDetail, loading, onUpdate }) => {
if (docDetail?.doc_type) {
setEditStatus(false)
setShowDocTypes(false)
setTempDocType(docDetail?.doc_type)
setTempDocType(doc_type as DocType | '')
setMetadataParams({
documentType: docDetail?.doc_type,
metadata: docDetail?.doc_metadata || {},
documentType: doc_type as DocType | '',
metadata: (docDetail?.doc_metadata || {}) as Record<string, string>,
})
}
}, [docDetail?.doc_type])
}, [docDetail?.doc_type, docDetail?.doc_metadata, doc_type])
// confirm doc type
const confirmDocType = () => {
@@ -179,7 +183,7 @@ const Metadata: FC<IMetadataProps> = ({ docDetail, loading, onUpdate }) => {
return
setMetadataParams({
documentType: tempDocType,
metadata: tempDocType === metadataParams.documentType ? metadataParams.metadata : {}, // change doc type, clear metadata
metadata: tempDocType === metadataParams.documentType ? metadataParams.metadata : {} as Record<string, string>, // change doc type, clear metadata
})
setEditStatus(true)
setShowDocTypes(false)
@@ -187,7 +191,7 @@ const Metadata: FC<IMetadataProps> = ({ docDetail, loading, onUpdate }) => {
// cancel doc type
const cancelDocType = () => {
setTempDocType(metadataParams.documentType)
setTempDocType(metadataParams.documentType ?? '')
setEditStatus(true)
setShowDocTypes(false)
}
@@ -209,7 +213,7 @@ const Metadata: FC<IMetadataProps> = ({ docDetail, loading, onUpdate }) => {
<span className={s.title}>{t('datasetDocuments.metadata.docTypeChangeTitle')}</span>
<span className={s.changeTip}>{t('datasetDocuments.metadata.docTypeSelectWarning')}</span>
</>}
<Radio.Group value={tempDocType ?? documentType} onChange={setTempDocType} className={s.radioGroup}>
<Radio.Group value={tempDocType ?? documentType ?? ''} onChange={setTempDocType} className={s.radioGroup}>
{CUSTOMIZABLE_DOC_TYPES.map((type, index) => {
const currValue = tempDocType ?? documentType
return <Radio key={index} value={type} className={`${s.radio} ${currValue === type ? 'shadow-none' : ''}`}>

View File

@@ -4,7 +4,17 @@ import { useBoolean } from 'ahooks'
import { useContext } from 'use-context-selector'
import { useRouter } from 'next/navigation'
import DatasetDetailContext from '@/context/dataset-detail'
import type { CrawlOptions, CustomFile, DataSourceType } from '@/models/datasets'
import type {
CrawlOptions,
CustomFile,
DataSourceInfo,
DataSourceType,
LegacyDataSourceInfo,
LocalFileInfo,
OnlineDocumentInfo,
WebsiteCrawlInfo,
} from '@/models/datasets'
import type { DataSourceProvider } from '@/models/common'
import Loading from '@/app/components/base/loading'
import StepTwo from '@/app/components/datasets/create/step-two'
import AccountSetting from '@/app/components/header/account-setting'
@@ -42,15 +52,78 @@ const DocumentSettings = ({ datasetId, documentId }: DocumentSettingsProps) => {
params: { metadata: 'without' },
})
const dataSourceInfo = documentDetail?.data_source_info
const isLegacyDataSourceInfo = (info: DataSourceInfo | undefined): info is LegacyDataSourceInfo => {
return !!info && 'upload_file' in info
}
const isWebsiteCrawlInfo = (info: DataSourceInfo | undefined): info is WebsiteCrawlInfo => {
return !!info && 'source_url' in info && 'title' in info
}
const isOnlineDocumentInfo = (info: DataSourceInfo | undefined): info is OnlineDocumentInfo => {
return !!info && 'page' in info
}
const isLocalFileInfo = (info: DataSourceInfo | undefined): info is LocalFileInfo => {
return !!info && 'related_id' in info && 'transfer_method' in info
}
const legacyInfo = isLegacyDataSourceInfo(dataSourceInfo) ? dataSourceInfo : undefined
const websiteInfo = isWebsiteCrawlInfo(dataSourceInfo) ? dataSourceInfo : undefined
const onlineDocumentInfo = isOnlineDocumentInfo(dataSourceInfo) ? dataSourceInfo : undefined
const localFileInfo = isLocalFileInfo(dataSourceInfo) ? dataSourceInfo : undefined
const currentPage = useMemo(() => {
return {
workspace_id: documentDetail?.data_source_info.notion_workspace_id,
page_id: documentDetail?.data_source_info.notion_page_id,
page_name: documentDetail?.name,
page_icon: documentDetail?.data_source_info.notion_page_icon,
type: documentDetail?.data_source_type,
if (legacyInfo) {
return {
workspace_id: legacyInfo.notion_workspace_id ?? '',
page_id: legacyInfo.notion_page_id ?? '',
page_name: documentDetail?.name,
page_icon: legacyInfo.notion_page_icon,
type: documentDetail?.data_source_type,
}
}
}, [documentDetail])
if (onlineDocumentInfo) {
return {
workspace_id: onlineDocumentInfo.workspace_id,
page_id: onlineDocumentInfo.page.page_id,
page_name: onlineDocumentInfo.page.page_name,
page_icon: onlineDocumentInfo.page.page_icon,
type: onlineDocumentInfo.page.type,
}
}
return undefined
}, [documentDetail?.data_source_type, documentDetail?.name, legacyInfo, onlineDocumentInfo])
const files = useMemo<CustomFile[]>(() => {
if (legacyInfo?.upload_file)
return [legacyInfo.upload_file as CustomFile]
if (localFileInfo) {
const { related_id, name, extension } = localFileInfo
return [{
id: related_id,
name,
extension,
} as unknown as CustomFile]
}
return []
}, [legacyInfo?.upload_file, localFileInfo])
const websitePages = useMemo(() => {
if (!websiteInfo)
return []
return [{
title: websiteInfo.title,
source_url: websiteInfo.source_url,
content: websiteInfo.content,
description: websiteInfo.description,
}]
}, [websiteInfo])
const crawlOptions = (dataSourceInfo && typeof dataSourceInfo === 'object' && 'includes' in dataSourceInfo && 'excludes' in dataSourceInfo)
? dataSourceInfo as unknown as CrawlOptions
: undefined
const websiteCrawlProvider = (websiteInfo?.provider ?? legacyInfo?.provider) as DataSourceProvider | undefined
const websiteCrawlJobId = websiteInfo?.job_id ?? legacyInfo?.job_id
if (error)
return <AppUnavailable code={500} unknownReason={t('datasetCreation.error.unavailable') as string} />
@@ -65,22 +138,16 @@ const DocumentSettings = ({ datasetId, documentId }: DocumentSettingsProps) => {
onSetting={showSetAPIKey}
datasetId={datasetId}
dataSourceType={documentDetail.data_source_type as DataSourceType}
notionPages={[currentPage as unknown as NotionPage]}
websitePages={[
{
title: documentDetail.name,
source_url: documentDetail.data_source_info?.url,
content: '',
description: '',
},
]}
websiteCrawlProvider={documentDetail.data_source_info?.provider}
websiteCrawlJobId={documentDetail.data_source_info?.job_id}
crawlOptions={documentDetail.data_source_info as unknown as CrawlOptions}
notionPages={currentPage ? [currentPage as unknown as NotionPage] : []}
notionCredentialId={legacyInfo?.credential_id || onlineDocumentInfo?.credential_id || ''}
websitePages={websitePages}
websiteCrawlProvider={websiteCrawlProvider}
websiteCrawlJobId={websiteCrawlJobId || ''}
crawlOptions={crawlOptions}
indexingType={indexingTechnique}
isSetting
documentDetail={documentDetail}
files={[documentDetail.data_source_info.upload_file as CustomFile]}
files={files}
onSave={saveHandler}
onCancel={cancelHandler}
/>

View File

@@ -5,6 +5,7 @@ import type {
Model,
ModelItem,
} from '../declarations'
import type { ModelFeatureEnum } from '../declarations'
import { useCurrentProviderAndModel } from '../hooks'
import ModelTrigger from './model-trigger'
import EmptyTrigger from './empty-trigger'
@@ -24,7 +25,7 @@ type ModelSelectorProps = {
popupClassName?: string
onSelect?: (model: DefaultModel) => void
readonly?: boolean
scopeFeatures?: string[]
scopeFeatures?: ModelFeatureEnum[]
deprecatedClassName?: string
showDeprecatedWarnIcon?: boolean
}

View File

@@ -22,7 +22,7 @@ type PopupProps = {
defaultModel?: DefaultModel
modelList: Model[]
onSelect: (provider: string, model: ModelItem) => void
scopeFeatures?: string[]
scopeFeatures?: ModelFeatureEnum[]
onHide: () => void
}
const Popup: FC<PopupProps> = ({

View File

@@ -5,15 +5,17 @@ import { isEmpty } from 'lodash-es'
export const pluginManifestToCardPluginProps = (pluginManifest: PluginDeclaration): Plugin => {
return {
plugin_id: pluginManifest.plugin_unique_identifier,
type: pluginManifest.category,
type: pluginManifest.category as Plugin['type'],
category: pluginManifest.category,
name: pluginManifest.name,
version: pluginManifest.version,
latest_version: '',
latest_package_identifier: '',
org: pluginManifest.author,
author: pluginManifest.author,
label: pluginManifest.label,
brief: pluginManifest.description,
description: pluginManifest.description,
icon: pluginManifest.icon,
verified: pluginManifest.verified,
introduction: '',
@@ -22,14 +24,17 @@ export const pluginManifestToCardPluginProps = (pluginManifest: PluginDeclaratio
endpoint: {
settings: [],
},
tags: [],
tags: pluginManifest.tags.map(tag => ({ name: tag })),
badges: [],
verification: { authorized_category: 'langgenius' },
from: 'package',
}
}
export const pluginManifestInMarketToPluginProps = (pluginManifest: PluginManifestInMarket): Plugin => {
return {
plugin_id: pluginManifest.plugin_unique_identifier,
type: pluginManifest.category,
type: pluginManifest.category as Plugin['type'],
category: pluginManifest.category,
name: pluginManifest.name,
version: pluginManifest.latest_version,
@@ -38,6 +43,7 @@ export const pluginManifestInMarketToPluginProps = (pluginManifest: PluginManife
org: pluginManifest.org,
label: pluginManifest.label,
brief: pluginManifest.brief,
description: pluginManifest.brief,
icon: pluginManifest.icon,
verified: true,
introduction: pluginManifest.introduction,
@@ -49,6 +55,7 @@ export const pluginManifestInMarketToPluginProps = (pluginManifest: PluginManife
tags: [],
badges: pluginManifest.badges,
verification: isEmpty(pluginManifest.verification) ? { authorized_category: 'langgenius' } : pluginManifest.verification,
from: pluginManifest.from,
}
}

View File

@@ -50,7 +50,7 @@ const EndpointModal: FC<Props> = ({
// Fix: Process boolean fields to ensure they are sent as proper boolean values
const processedCredential = { ...tempCredential }
formSchemas.forEach((field) => {
formSchemas.forEach((field: any) => {
if (field.type === 'boolean' && processedCredential[field.name] !== undefined) {
const value = processedCredential[field.name]
if (typeof value === 'string')

View File

@@ -7,6 +7,7 @@ import { useTranslation } from 'react-i18next'
import type {
DefaultModel,
FormValue,
ModelFeatureEnum,
} from '@/app/components/header/account-setting/model-provider-page/declarations'
import { ModelStatusEnum, ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import ModelSelector from '@/app/components/header/account-setting/model-provider-page/model-selector'
@@ -57,7 +58,7 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
const { isAPIKeySet } = useProviderContext()
const [open, setOpen] = useState(false)
const scopeArray = scope.split('&')
const scopeFeatures = useMemo(() => {
const scopeFeatures = useMemo((): ModelFeatureEnum[] => {
if (scopeArray.includes('all'))
return []
return scopeArray.filter(item => ![
@@ -67,7 +68,7 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
ModelTypeEnum.moderation,
ModelTypeEnum.speech2text,
ModelTypeEnum.tts,
].includes(item as ModelTypeEnum))
].includes(item as ModelTypeEnum)).map(item => item as ModelFeatureEnum)
}, [scopeArray])
const { data: textGenerationList } = useModelList(ModelTypeEnum.textGeneration)

View File

@@ -9,6 +9,7 @@ import I18n from '@/context/i18n'
import { getLanguage } from '@/i18n-config/language'
import { useStore as useLabelStore } from '@/app/components/tools/labels/store'
import { fetchLabelList } from '@/service/tools'
import { renderI18nObject } from '@/i18n-config'
type Props = {
value: string
@@ -55,14 +56,24 @@ const Category = ({
<Apps02 className='mr-2 h-4 w-4 shrink-0' />
{t('tools.type.all')}
</div>
{labelList.map(label => (
<div key={label.name} title={label.label[language]} className={cn('mb-0.5 flex cursor-pointer items-center overflow-hidden truncate rounded-lg p-1 pl-3 text-sm leading-5 text-gray-700 hover:bg-white', value === label.name && '!bg-white font-medium !text-primary-600')} onClick={() => onSelect(label.name)}>
<div className='mr-2 h-4 w-4 shrink-0'>
<Icon active={value === label.name} svgString={label.icon} />
{labelList.map((label) => {
const labelText = typeof label.label === 'string'
? label.label
: (label.label ? renderI18nObject(label.label, language) : '')
return (
<div
key={label.name}
title={labelText}
className={cn('mb-0.5 flex cursor-pointer items-center overflow-hidden truncate rounded-lg p-1 pl-3 text-sm leading-5 text-gray-700 hover:bg-white', value === label.name && '!bg-white font-medium !text-primary-600')}
onClick={() => onSelect(label.name)}
>
<div className='mr-2 h-4 w-4 shrink-0'>
<Icon active={value === label.name} svgString={label.icon || ''} />
</div>
{labelText}
</div>
{label.label[language]}
</div>
))}
)
})}
</div>
)
}

View File

@@ -10,6 +10,7 @@ import {
} from '@remixicon/react'
import { useMount } from 'ahooks'
import type { Collection, CustomCollectionBackend, Tool } from '../types'
import type { CollectionType } from '../types'
import Type from './type'
import Category from './category'
import Tools from './tools'
@@ -129,7 +130,7 @@ const AddToolModal: FC<Props> = ({
const nexModelConfig = produce(modelConfig, (draft: ModelConfig) => {
draft.agentConfig.tools.push({
provider_id: collection.id || collection.name,
provider_type: collection.type,
provider_type: collection.type as CollectionType,
provider_name: collection.name,
tool_name: tool.name,
tool_label: tool.label[locale] || tool.label[locale.replaceAll('-', '_')],

View File

@@ -23,6 +23,14 @@ import type { Tool } from '@/app/components/tools/types'
import { CollectionType } from '@/app/components/tools/types'
import type { AgentTool } from '@/types/app'
import { MAX_TOOLS_NUM } from '@/config'
import type { TypeWithI18N } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { renderI18nObject } from '@/i18n-config'
const resolveI18nText = (value: TypeWithI18N | string | undefined, language: string): string => {
if (!value)
return ''
return typeof value === 'string' ? value : renderI18nObject(value, language)
}
type ToolsProps = {
showWorkflowEmpty: boolean
@@ -53,7 +61,7 @@ const Blocks = ({
className='group mb-1 last-of-type:mb-0'
>
<div className='flex h-[22px] w-full items-center justify-between pl-3 pr-1 text-xs font-medium text-gray-500'>
{toolWithProvider.label[language]}
{resolveI18nText(toolWithProvider.label, language)}
<a className='hidden cursor-pointer items-center group-hover:flex' href={`${basePath}/tools?category=${toolWithProvider.type}`} target='_blank'>{t('tools.addToolModal.manageInTools')}<ArrowUpRight className='ml-0.5 h-3 w-3' /></a>
</div>
{list.map((tool) => {
@@ -62,7 +70,7 @@ const Blocks = ({
return ''
return tool.labels.map((name) => {
const label = labelList.find(item => item.name === name)
return label?.label[language]
return resolveI18nText(label?.label, language)
}).filter(Boolean).join(', ')
})()
const added = !!addedTools?.find(v => v.provider_id === toolWithProvider.id && v.provider_type === toolWithProvider.type && v.tool_name === tool.name)
@@ -79,8 +87,8 @@ const Blocks = ({
type={BlockEnum.Tool}
toolIcon={toolWithProvider.icon}
/>
<div className='mb-1 text-sm leading-5 text-gray-900'>{tool.label[language]}</div>
<div className='text-xs leading-[18px] text-gray-700'>{tool.description[language]}</div>
<div className='mb-1 text-sm leading-5 text-gray-900'>{resolveI18nText(tool.label, language)}</div>
<div className='text-xs leading-[18px] text-gray-700'>{resolveI18nText(tool.description, language)}</div>
{tool.labels?.length > 0 && (
<div className='mt-1 flex shrink-0 items-center'>
<div className='relative flex w-full items-center gap-1 rounded-md py-1 text-gray-500' title={labelContent}>
@@ -98,7 +106,7 @@ const Blocks = ({
type={BlockEnum.Tool}
toolIcon={toolWithProvider.icon}
/>
<div className={cn('grow truncate text-sm text-gray-900', needAuth && 'opacity-30')}>{tool.label[language]}</div>
<div className={cn('grow truncate text-sm text-gray-900', needAuth && 'opacity-30')}>{resolveI18nText(tool.label, language)}</div>
{!needAuth && added && (
<div className='flex items-center gap-1 rounded-[6px] border border-gray-100 bg-white px-2 py-[3px] text-xs font-medium leading-[18px] text-gray-300'>
<Check className='h-3 w-3' />

View File

@@ -12,7 +12,7 @@ import ConversationVariableModal from './conversation-variable-modal'
import { useChat } from './hooks'
import type { ChatWrapperRefType } from './index'
import Chat from '@/app/components/base/chat/chat'
import type { ChatItem, ChatItemInTree, OnSend } from '@/app/components/base/chat/types'
import type { ChatItem, OnSend } from '@/app/components/base/chat/types'
import { useFeatures } from '@/app/components/base/features/hooks'
import {
fetchSuggestedQuestions,
@@ -117,7 +117,7 @@ const ChatWrapper = (
)
}, [handleSend, workflowStore, conversationId, chatList, appDetail])
const doRegenerate = useCallback((chatItem: ChatItemInTree, editedQuestion?: { message: string, files?: FileEntity[] }) => {
const doRegenerate = useCallback((chatItem: ChatItem, editedQuestion?: { message: string, files?: FileEntity[] }) => {
const question = editedQuestion ? chatItem : chatList.find(item => item.id === chatItem.parentMessageId)!
const parentAnswer = chatList.find(item => item.id === question.parentMessageId)
doSend(editedQuestion ? editedQuestion.message : question.content,

View File

@@ -4,18 +4,18 @@ import { cloneDeep } from 'lodash-es'
import type {
Edge,
Node,
} from '../types'
} from '@/app/components/workflow/types'
import {
BlockEnum,
} from '../types'
} from '@/app/components/workflow/types'
import {
CUSTOM_NODE,
NODE_LAYOUT_HORIZONTAL_PADDING,
NODE_LAYOUT_VERTICAL_PADDING,
} from '../constants'
import { CUSTOM_ITERATION_START_NODE } from '../nodes/iteration-start/constants'
import { CUSTOM_LOOP_START_NODE } from '../nodes/loop-start/constants'
import type { CaseItem, IfElseNodeType } from '../nodes/if-else/types'
} from '@/app/components/workflow/constants'
import { CUSTOM_ITERATION_START_NODE } from '@/app/components/workflow/nodes/iteration-start/constants'
import { CUSTOM_LOOP_START_NODE } from '@/app/components/workflow/nodes/loop-start/constants'
import type { CaseItem, IfElseNodeType } from '@/app/components/workflow/nodes/if-else/types'
// Although the file name refers to Dagre, the implementation now relies on ELK's layered algorithm.
// Keep the export signatures unchanged to minimise the blast radius while we migrate the layout stack.

View File

@@ -1,7 +1,7 @@
export * from './node'
export * from './edge'
export * from './workflow-init'
export * from './layout'
export * from './elk-layout'
export * from './common'
export * from './tool'
export * from './workflow'

View File

@@ -1,4 +1,4 @@
import { OAUTH_AUTHORIZE_PENDING_KEY, REDIRECT_URL_KEY } from '@/app/account/oauth/authorize/page'
import { OAUTH_AUTHORIZE_PENDING_KEY, REDIRECT_URL_KEY } from '@/app/account/oauth/authorize/constants'
import dayjs from 'dayjs'
import type { ReadonlyURLSearchParams } from 'next/navigation'

View File

@@ -210,6 +210,8 @@ const DebugConfigurationContext = createContext<IDebugConfiguration>({
prompt_template: '',
prompt_variables: [],
},
chat_prompt_config: DEFAULT_CHAT_PROMPT_CONFIG,
completion_prompt_config: DEFAULT_COMPLETION_PROMPT_CONFIG,
more_like_this: null,
opening_statement: '',
suggested_questions: [],
@@ -220,6 +222,14 @@ const DebugConfigurationContext = createContext<IDebugConfiguration>({
suggested_questions_after_answer: null,
retriever_resource: null,
annotation_reply: null,
external_data_tools: [],
system_parameters: {
audio_file_size_limit: 0,
file_size_limit: 0,
image_file_size_limit: 0,
video_file_size_limit: 0,
workflow_file_upload_limit: 0,
},
dataSets: [],
agentConfig: DEFAULT_AGENT_SETTING,
},

View File

@@ -344,6 +344,8 @@ export type WebsiteCrawlInfo = {
description: string
source_url: string
title: string
provider?: string
job_id?: string
}
export type OnlineDocumentInfo = {

View File

@@ -9,6 +9,7 @@ import type {
MetadataFilteringModeEnum,
} from '@/app/components/workflow/nodes/knowledge-retrieval/types'
import type { ModelConfig as NodeModelConfig } from '@/app/components/workflow/types'
import type { ExternalDataTool } from '@/models/common'
export type Inputs = Record<string, string | number | object | boolean>
export enum PromptMode {
@@ -133,6 +134,8 @@ export type ModelConfig = {
model_id: string
mode: ModelModeType
configs: PromptConfig
chat_prompt_config?: ChatPromptConfig | null
completion_prompt_config?: CompletionPromptConfig | null
opening_statement: string | null
more_like_this: MoreLikeThisConfig | null
suggested_questions: string[] | null
@@ -143,6 +146,14 @@ export type ModelConfig = {
retriever_resource: RetrieverResourceConfig | null
sensitive_word_avoidance: ModerationConfig | null
annotation_reply: AnnotationReplyConfig | null
external_data_tools?: ExternalDataTool[] | null
system_parameters: {
audio_file_size_limit: number
file_size_limit: number
image_file_size_limit: number
video_file_size_limit: number
workflow_file_upload_limit: number
}
dataSets: any[]
agentConfig: AgentConfig
}

View File

@@ -8,6 +8,7 @@ import type {
} from '@/models/datasets'
import type { UploadFileSetting } from '@/app/components/workflow/types'
import type { AccessMode } from '@/models/access-control'
import type { ExternalDataTool } from '@/models/common'
export enum Theme {
light = 'light',
@@ -206,12 +207,12 @@ export type ModelConfig = {
suggested_questions?: string[]
pre_prompt: string
prompt_type: PromptMode
chat_prompt_config: ChatPromptConfig | {}
completion_prompt_config: CompletionPromptConfig | {}
chat_prompt_config?: ChatPromptConfig | null
completion_prompt_config?: CompletionPromptConfig | null
user_input_form: UserInputFormItem[]
dataset_query_variable?: string
more_like_this: {
enabled?: boolean
enabled: boolean
}
suggested_questions_after_answer: {
enabled: boolean
@@ -237,12 +238,20 @@ export type ModelConfig = {
strategy?: AgentStrategy
tools: ToolItem[]
}
external_data_tools?: ExternalDataTool[]
model: Model
dataset_configs: DatasetConfigs
file_upload?: {
image: VisionSettings
} & UploadFileSetting
files?: VisionFile[]
system_parameters: {
audio_file_size_limit: number
file_size_limit: number
image_file_size_limit: number
video_file_size_limit: number
workflow_file_upload_limit: number
}
created_at?: number
updated_at?: number
}
@@ -360,6 +369,7 @@ export type App = {
updated_at: number
updated_by?: string
}
deleted_tools?: Array<{ id: string; tool_name: string }>
/** access control */
access_mode: AccessMode
max_active_requests?: number | null