Merge remote-tracking branch 'origin/main' into feat/collaboration

This commit is contained in:
lyzno1
2025-10-17 19:33:40 +08:00
204 changed files with 1539 additions and 1021 deletions

View File

@ -100,7 +100,10 @@ export default function MailAndPasswordAuth({ isEmailSetup }: MailAndPasswordAut
})
}
}
catch (e: any) {
if (e.code === 'authentication_failed')
Toast.notify({ type: 'error', message: e.message })
}
finally {
setIsLoading(false)
}

View File

@ -1,9 +1,11 @@
'use client'
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import { RiCloseLine } from '@remixicon/react'
import { RiCloseLine, RiPlayLargeLine } from '@remixicon/react'
import Run from '@/app/components/workflow/run'
import { useStore } from '@/app/components/app/store'
import TooltipPlus from '@/app/components/base/tooltip'
import { useRouter } from 'next/navigation'
type ILogDetail = {
runID: string
@ -13,13 +15,34 @@ type ILogDetail = {
const DetailPanel: FC<ILogDetail> = ({ runID, onClose }) => {
const { t } = useTranslation()
const appDetail = useStore(state => state.appDetail)
const router = useRouter()
const handleReplay = () => {
if (!appDetail?.id) return
router.push(`/app/${appDetail.id}/workflow?replayRunId=${runID}`)
}
return (
<div className='relative flex grow flex-col pt-3'>
<span className='absolute right-3 top-4 z-20 cursor-pointer p-1' onClick={onClose}>
<RiCloseLine className='h-4 w-4 text-text-tertiary' />
</span>
<h1 className='system-xl-semibold shrink-0 px-4 py-1 text-text-primary'>{t('appLog.runDetail.workflowTitle')}</h1>
<div className='flex items-center bg-components-panel-bg'>
<h1 className='system-xl-semibold shrink-0 px-4 py-1 text-text-primary'>{t('appLog.runDetail.workflowTitle')}</h1>
<TooltipPlus
popupContent={t('appLog.runDetail.testWithParams')}
popupClassName='rounded-xl'
>
<button
type='button'
className='mr-1 flex h-6 w-6 items-center justify-center rounded-md hover:bg-state-base-hover'
aria-label={t('appLog.runDetail.testWithParams')}
onClick={handleReplay}
>
<RiPlayLargeLine className='h-4 w-4 text-text-tertiary' />
</button>
</TooltipPlus>
</div>
<Run
runDetailUrl={runID ? `/apps/${appDetail?.id}/workflow-runs/${runID}` : ''}
tracingListUrl={runID ? `/apps/${appDetail?.id}/workflow-runs/${runID}/node-executions` : ''}

View File

@ -14,16 +14,6 @@ import Divider from '@/app/components/base/divider'
import { searchEmoji } from '@/utils/emoji'
import cn from '@/utils/classnames'
declare global {
// eslint-disable-next-line ts/no-namespace
namespace JSX {
// eslint-disable-next-line ts/consistent-type-definitions
interface IntrinsicElements {
'em-emoji': React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>
}
}
}
init({ data })
const backgroundColors = [

View File

@ -32,7 +32,7 @@ const TopKItem: FC<Props> = ({
}) => {
const { t } = useTranslation()
const handleParamChange = (key: string, value: number) => {
let notOutRangeValue = Number.parseFloat(value.toFixed(2))
let notOutRangeValue = Number.parseInt(value.toFixed(0))
notOutRangeValue = Math.max(VALUE_LIMIT.min, notOutRangeValue)
notOutRangeValue = Math.min(VALUE_LIMIT.max, notOutRangeValue)
onChange(key, notOutRangeValue)

View File

@ -25,8 +25,8 @@ export type TextareaProps = {
destructive?: boolean
styleCss?: CSSProperties
ref?: React.Ref<HTMLTextAreaElement>
onFocus?: () => void
onBlur?: () => void
onFocus?: React.FocusEventHandler<HTMLTextAreaElement>
onBlur?: React.FocusEventHandler<HTMLTextAreaElement>
} & React.TextareaHTMLAttributes<HTMLTextAreaElement> & VariantProps<typeof textareaVariants>
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(

View File

@ -1,6 +1,7 @@
'use client'
import {
useEffect,
useMemo,
} from 'react'
import {
@ -23,9 +24,14 @@ import {
WorkflowContextProvider,
} from '@/app/components/workflow/context'
import type { InjectWorkflowStoreSliceFn } from '@/app/components/workflow/store'
import { useWorkflowStore } from '@/app/components/workflow/store'
import { createWorkflowSlice } from './store/workflow/workflow-slice'
import WorkflowAppMain from './components/workflow-main'
import { collaborationManager } from '@/app/components/workflow/collaboration/core/collaboration-manager'
import { useSearchParams } from 'next/navigation'
import { fetchRunDetail } from '@/service/log'
import { useGetRunAndTraceUrl } from './hooks/use-get-run-and-trace-url'
const WorkflowAppWithAdditionalContext = () => {
const {
@ -53,6 +59,71 @@ const WorkflowAppWithAdditionalContext = () => {
return []
}, [data])
const searchParams = useSearchParams()
const workflowStore = useWorkflowStore()
const { getWorkflowRunAndTraceUrl } = useGetRunAndTraceUrl()
const replayRunId = searchParams.get('replayRunId')
useEffect(() => {
if (!replayRunId)
return
const { runUrl } = getWorkflowRunAndTraceUrl(replayRunId)
if (!runUrl)
return
fetchRunDetail(runUrl).then((res) => {
const { setInputs, setShowInputsPanel, setShowDebugAndPreviewPanel } = workflowStore.getState()
const rawInputs = res.inputs
let parsedInputs: Record<string, unknown> | null = null
if (typeof rawInputs === 'string') {
try {
const maybeParsed = JSON.parse(rawInputs) as unknown
if (maybeParsed && typeof maybeParsed === 'object' && !Array.isArray(maybeParsed))
parsedInputs = maybeParsed as Record<string, unknown>
}
catch (error) {
console.error('Failed to parse workflow run inputs', error)
}
}
else if (rawInputs && typeof rawInputs === 'object' && !Array.isArray(rawInputs)) {
parsedInputs = rawInputs as Record<string, unknown>
}
if (!parsedInputs)
return
const userInputs: Record<string, string> = {}
Object.entries(parsedInputs).forEach(([key, value]) => {
if (key.startsWith('sys.'))
return
if (value == null) {
userInputs[key] = ''
return
}
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
userInputs[key] = value
return
}
try {
userInputs[key] = JSON.stringify(value)
}
catch {
userInputs[key] = String(value)
}
})
if (!Object.keys(userInputs).length)
return
setInputs(userInputs)
setShowInputsPanel(true)
setShowDebugAndPreviewPanel(true)
})
}, [replayRunId, workflowStore, getWorkflowRunAndTraceUrl])
if (!data || isLoading || isLoadingCurrentWorkspace || !currentWorkspace.id) {
return (
<div className='relative flex h-full w-full items-center justify-center'>

View File

@ -234,6 +234,9 @@ const ConditionItem = ({
draft.varType = resolvedVarType
draft.value = resolvedVarType === VarType.boolean ? false : ''
draft.comparison_operator = getOperators(resolvedVarType)[0]
delete draft.key
delete draft.sub_variable_condition
delete draft.numberVarType
setTimeout(() => setControlPromptEditorRerenderKey(Date.now()))
})
doUpdateCondition(newCondition)

View File

@ -1,8 +1,8 @@
import { memo } from 'react'
import { memo, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import Tooltip from '@/app/components/base/tooltip'
import Input from '@/app/components/base/input'
import Switch from '@/app/components/base/switch'
import { InputNumber } from '@/app/components/base/input-number'
export type TopKAndScoreThresholdProps = {
topK: number
@ -14,6 +14,24 @@ export type TopKAndScoreThresholdProps = {
readonly?: boolean
hiddenScoreThreshold?: boolean
}
const maxTopK = (() => {
const configValue = Number.parseInt(globalThis.document?.body?.getAttribute('data-public-top-k-max-value') || '', 10)
if (configValue && !isNaN(configValue))
return configValue
return 10
})()
const TOP_K_VALUE_LIMIT = {
amount: 1,
min: 1,
max: maxTopK,
}
const SCORE_THRESHOLD_VALUE_LIMIT = {
step: 0.01,
min: 0,
max: 1,
}
const TopKAndScoreThreshold = ({
topK,
onTopKChange,
@ -25,18 +43,18 @@ const TopKAndScoreThreshold = ({
hiddenScoreThreshold,
}: TopKAndScoreThresholdProps) => {
const { t } = useTranslation()
const handleTopKChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = Number(e.target.value)
if (Number.isNaN(value))
return
onTopKChange?.(value)
}
const handleTopKChange = useCallback((value: number) => {
let notOutRangeValue = Number.parseInt(value.toFixed(0))
notOutRangeValue = Math.max(TOP_K_VALUE_LIMIT.min, notOutRangeValue)
notOutRangeValue = Math.min(TOP_K_VALUE_LIMIT.max, notOutRangeValue)
onTopKChange?.(notOutRangeValue)
}, [onTopKChange])
const handleScoreThresholdChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = Number(e.target.value)
if (Number.isNaN(value))
return
onScoreThresholdChange?.(value)
const handleScoreThresholdChange = (value: number) => {
let notOutRangeValue = Number.parseFloat(value.toFixed(2))
notOutRangeValue = Math.max(SCORE_THRESHOLD_VALUE_LIMIT.min, notOutRangeValue)
notOutRangeValue = Math.min(SCORE_THRESHOLD_VALUE_LIMIT.max, notOutRangeValue)
onScoreThresholdChange?.(notOutRangeValue)
}
return (
@ -49,11 +67,13 @@ const TopKAndScoreThreshold = ({
popupContent={t('appDebug.datasetConfig.top_kTip')}
/>
</div>
<Input
<InputNumber
disabled={readonly}
type='number'
{...TOP_K_VALUE_LIMIT}
size='regular'
value={topK}
onChange={handleTopKChange}
disabled={readonly}
/>
</div>
{
@ -74,11 +94,13 @@ const TopKAndScoreThreshold = ({
popupContent={t('appDebug.datasetConfig.score_thresholdTip')}
/>
</div>
<Input
<InputNumber
disabled={readonly || !isScoreThresholdEnabled}
type='number'
{...SCORE_THRESHOLD_VALUE_LIMIT}
size='regular'
value={scoreThreshold}
onChange={handleScoreThresholdChange}
disabled={readonly || !isScoreThresholdEnabled}
/>
</div>
)

View File

@ -18,7 +18,7 @@ type ConditionNumberProps = {
nodesOutputVars: NodeOutPutVar[]
availableNodes: Node[]
isCommonVariable?: boolean
commonVariables: { name: string, type: string }[]
commonVariables: { name: string; type: string; value: string }[]
} & ConditionValueMethodProps
const ConditionNumber = ({
value,

View File

@ -18,7 +18,7 @@ type ConditionStringProps = {
nodesOutputVars: NodeOutPutVar[]
availableNodes: Node[]
isCommonVariable?: boolean
commonVariables: { name: string, type: string }[]
commonVariables: { name: string; type: string; value: string }[]
} & ConditionValueMethodProps
const ConditionString = ({
value,

View File

@ -128,6 +128,6 @@ export type MetadataShape = {
availableNumberVars?: NodeOutPutVar[]
availableNumberNodesWithParent?: Node[]
isCommonVariable?: boolean
availableCommonStringVars?: { name: string; type: string; }[]
availableCommonNumberVars?: { name: string; type: string; }[]
availableCommonStringVars?: { name: string; type: string; value: string }[]
availableCommonNumberVars?: { name: string; type: string; value: string }[]
}

View File

@ -24,7 +24,7 @@ const JsonImporter: FC<JsonImporterProps> = ({
const [open, setOpen] = useState(false)
const [json, setJson] = useState('')
const [parseError, setParseError] = useState<any>(null)
const importBtnRef = useRef<HTMLButtonElement>(null)
const importBtnRef = useRef<HTMLElement>(null)
const advancedEditing = useVisualEditorStore(state => state.advancedEditing)
const isAddingNewField = useVisualEditorStore(state => state.isAddingNewField)
const { emit } = useMittContext()

View File

@ -18,7 +18,7 @@ type VisualEditorProviderProps = {
export const VisualEditorContext = createContext<VisualEditorContextType>(null)
export const VisualEditorContextProvider = ({ children }: VisualEditorProviderProps) => {
const storeRef = useRef<VisualEditorStore>()
const storeRef = useRef<VisualEditorStore | null>(null)
if (!storeRef.current)
storeRef.current = createVisualEditorStore()

View File

@ -23,7 +23,7 @@ const useConfig = (id: string, payload: LLMNodeType) => {
const { nodesReadOnly: readOnly } = useNodesReadOnly()
const isChatMode = useIsChatMode()
const defaultConfig = useStore(s => s.nodesDefaultConfigs)[payload.type]
const defaultConfig = useStore(s => s.nodesDefaultConfigs)?.[payload.type]
const [defaultRolePrefix, setDefaultRolePrefix] = useState<{ user: string; assistant: string }>({ user: '', assistant: '' })
const { inputs, setInputs: doSetInputs } = useNodeCrud<LLMNodeType>(id, payload)
const inputRef = useRef(inputs)

View File

@ -10,7 +10,7 @@ export const checkNodeValid = (_payload: LLMNodeType) => {
export const getFieldType = (field: Field) => {
const { type, items } = field
if(field.schemaType === 'file') return 'file'
if(field.schemaType === 'file') return Type.file
if (type !== Type.array || !items)
return type

View File

@ -196,6 +196,9 @@ const ConditionItem = ({
draft.varType = varItem.type
draft.value = ''
draft.comparison_operator = getOperators(varItem.type)[0]
delete draft.key
delete draft.sub_variable_condition
delete draft.numberVarType
})
doUpdateCondition(newCondition)
setOpen(false)

View File

@ -9,7 +9,10 @@ import BlockSelector from '../../../../block-selector'
import type { Param, ParamType } from '../../types'
import cn from '@/utils/classnames'
import { useStore } from '@/app/components/workflow/store'
import type { ToolDefaultValue } from '@/app/components/workflow/block-selector/types'
import type {
DataSourceDefaultValue,
ToolDefaultValue,
} from '@/app/components/workflow/block-selector/types'
import type { ToolParameter } from '@/app/components/tools/types'
import { CollectionType } from '@/app/components/tools/types'
import type { BlockEnum } from '@/app/components/workflow/types'
@ -43,8 +46,11 @@ const ImportFromTool: FC<Props> = ({
const customTools = useStore(s => s.customTools)
const workflowTools = useStore(s => s.workflowTools)
const handleSelectTool = useCallback((_type: BlockEnum, toolInfo?: ToolDefaultValue) => {
const { provider_id, provider_type, tool_name } = toolInfo!
const handleSelectTool = useCallback((_type: BlockEnum, toolInfo?: ToolDefaultValue | DataSourceDefaultValue) => {
if (!toolInfo || 'datasource_name' in toolInfo)
return
const { provider_id, provider_type, tool_name } = toolInfo
const currentTools = (() => {
switch (provider_type) {
case CollectionType.builtIn:

View File

@ -27,7 +27,7 @@ const useConfig = (id: string, payload: ParameterExtractorNodeType) => {
const { handleOutVarRenameChange } = useWorkflow()
const isChatMode = useIsChatMode()
const defaultConfig = useStore(s => s.nodesDefaultConfigs)[payload.type]
const defaultConfig = useStore(s => s.nodesDefaultConfigs)?.[payload.type]
const [defaultRolePrefix, setDefaultRolePrefix] = useState<{ user: string; assistant: string }>({ user: '', assistant: '' })
const { inputs, setInputs: doSetInputs } = useNodeCrud<ParameterExtractorNodeType>(id, payload)

View File

@ -20,7 +20,7 @@ const useConfig = (id: string, payload: QuestionClassifierNodeType) => {
const updateNodeInternals = useUpdateNodeInternals()
const { nodesReadOnly: readOnly } = useNodesReadOnly()
const isChatMode = useIsChatMode()
const defaultConfig = useStore(s => s.nodesDefaultConfigs)[payload.type]
const defaultConfig = useStore(s => s.nodesDefaultConfigs)?.[payload.type]
const { getBeforeNodesInSameBranch } = useWorkflow()
const startNode = getBeforeNodesInSameBranch(id).find(node => node.data.type === BlockEnum.Start)
const startNodeId = startNode?.id

View File

@ -13,7 +13,7 @@ import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use
const useConfig = (id: string, payload: TemplateTransformNodeType) => {
const { nodesReadOnly: readOnly } = useNodesReadOnly()
const defaultConfig = useStore(s => s.nodesDefaultConfigs)[payload.type]
const defaultConfig = useStore(s => s.nodesDefaultConfigs)?.[payload.type]
const { inputs, setInputs: doSetInputs } = useNodeCrud<TemplateTransformNodeType>(id, payload)
const inputsRef = useRef(inputs)

View File

@ -9,7 +9,7 @@ import Button from '@/app/components/base/button'
import type { AgentLogItemWithChildren } from '@/types/workflow'
type AgentLogNavMoreProps = {
options: { id: string; label: string }[]
options: AgentLogItemWithChildren[]
onShowAgentOrToolLog: (detail?: AgentLogItemWithChildren) => void
}
const AgentLogNavMore = ({
@ -41,10 +41,10 @@ const AgentLogNavMore = ({
{
options.map(option => (
<div
key={option.id}
key={option.message_id}
className='system-md-regular flex h-8 cursor-pointer items-center rounded-lg px-2 text-text-secondary hover:bg-state-base-hover'
onClick={() => {
onShowAgentOrToolLog(option as AgentLogItemWithChildren)
onShowAgentOrToolLog(option)
setOpen(false)
}}
>

View File

@ -4,8 +4,8 @@ import type {
} from '@/app/components/workflow/types'
export type FormSliceShape = {
inputs: Record<string, string>
setInputs: (inputs: Record<string, string>) => void
inputs: Record<string, string | number | boolean>
setInputs: (inputs: Record<string, string | number | boolean>) => void
files: RunFile[]
setFiles: (files: RunFile[]) => void
}

View File

@ -23,8 +23,10 @@ import {
} from '../node-handle'
import ErrorHandleOnNode from '../error-handle-on-node'
type NodeChildElement = ReactElement<Partial<NodeProps>>
type NodeCardProps = NodeProps & {
children?: ReactElement
children?: NodeChildElement
}
const BaseCard = ({

View File

@ -242,7 +242,7 @@ const DebugConfigurationContext = createContext<IDebugConfiguration>({
},
datasetConfigsRef: {
current: null,
},
} as unknown as RefObject<DatasetConfigs>,
setDatasetConfigs: noop,
hasSetContextVar: false,
isShowVisionConfig: false,

5
web/global.d.ts vendored
View File

@ -1,3 +1,6 @@
import './types/i18n'
import './types/jsx'
declare module 'lamejs';
declare module 'lamejs/src/js/MPEGMode';
declare module 'lamejs/src/js/Lame';
@ -9,4 +12,4 @@ declare module '*.mdx' {
export default MDXComponent
}
import './types/i18n'
export {}

View File

@ -1,5 +1,6 @@
'use client'
import { useEffect } from 'react'
import { validateRedirectUrl } from '@/utils/urlValidation'
export const useOAuthCallback = () => {
useEffect(() => {
@ -18,6 +19,7 @@ export const openOAuthPopup = (url: string, callback: () => void) => {
const left = window.screenX + (window.outerWidth - width) / 2
const top = window.screenY + (window.outerHeight - height) / 2
validateRedirectUrl(url)
const popup = window.open(
url,
'OAuth',

View File

@ -83,6 +83,7 @@ const translation = {
workflowTitle: 'Protokolldetail',
fileListLabel: 'Details zur Datei',
fileListDetail: 'Detail',
testWithParams: 'Test mit Parametern',
},
promptLog: 'Prompt-Protokoll',
agentLog: 'Agentenprotokoll',

View File

@ -83,6 +83,7 @@ const translation = {
workflowTitle: 'Log Detail',
fileListLabel: 'File Details',
fileListDetail: 'Detail',
testWithParams: 'Test With Params',
},
promptLog: 'Prompt Log',
agentLog: 'Agent Log',

View File

@ -82,6 +82,7 @@ const translation = {
workflowTitle: 'Detalle del Registro',
fileListLabel: 'Detalles del archivo',
fileListDetail: 'Detalle',
testWithParams: 'Prueba con parámetros',
},
promptLog: 'Registro de Indicación',
agentLog: 'Registro de Agente',

View File

@ -82,6 +82,7 @@ const translation = {
workflowTitle: 'جزئیات لاگ',
fileListLabel: 'جزئیات فایل',
fileListDetail: 'جزئیات',
testWithParams: 'تست با پارامترها',
},
promptLog: 'لاگ درخواست',
agentLog: 'لاگ عامل',

View File

@ -82,6 +82,7 @@ const translation = {
workflowTitle: 'Détail du journal',
fileListDetail: 'Détail',
fileListLabel: 'Détails du fichier',
testWithParams: 'Test avec paramètres',
},
promptLog: 'Journal de consigne',
agentLog: 'Journal des agents',

View File

@ -84,6 +84,7 @@ const translation = {
workflowTitle: 'लॉग विवरण',
fileListDetail: 'विस्तार',
fileListLabel: 'फ़ाइल विवरण',
testWithParams: 'पैरामीटर्स के साथ परीक्षण',
},
promptLog: 'प्रॉम्प्ट लॉग',
agentLog: 'एजेंट लॉग',

View File

@ -74,6 +74,7 @@ const translation = {
workflowTitle: 'Log Detail',
title: 'Log Percakapan',
fileListLabel: 'Rincian File',
testWithParams: 'Uji Dengan Param',
},
agentLogDetail: {
iterations: 'Iterasi',

View File

@ -86,6 +86,7 @@ const translation = {
workflowTitle: 'Dettagli Registro',
fileListDetail: 'Dettaglio',
fileListLabel: 'Dettagli del file',
testWithParams: 'Test con parametri',
},
promptLog: 'Registro Prompt',
agentLog: 'Registro Agente',

View File

@ -83,6 +83,7 @@ const translation = {
workflowTitle: 'ログの詳細',
fileListLabel: 'ファイルの詳細',
fileListDetail: '詳細',
testWithParams: 'パラメータ付きテスト',
},
promptLog: 'プロンプトログ',
agentLog: 'エージェントログ',

View File

@ -83,6 +83,7 @@ const translation = {
workflowTitle: '로그 세부 정보',
fileListDetail: '세부',
fileListLabel: '파일 세부 정보',
testWithParams: '매개변수로 테스트',
},
promptLog: '프롬프트 로그',
agentLog: '에이전트 로그',

View File

@ -86,6 +86,7 @@ const translation = {
workflowTitle: 'Szczegół dziennika',
fileListDetail: 'Detal',
fileListLabel: 'Szczegóły pliku',
testWithParams: 'Test z parametrami',
},
promptLog: 'Dziennik monitów',
agentLog: 'Dziennik agenta',

View File

@ -82,6 +82,7 @@ const translation = {
workflowTitle: 'Detalhes do Registro',
fileListLabel: 'Detalhes do arquivo',
fileListDetail: 'Detalhe',
testWithParams: 'Teste com parâmetros',
},
promptLog: 'Registro de Prompt',
agentLog: 'Registro do agente',

View File

@ -82,6 +82,7 @@ const translation = {
workflowTitle: 'Detalii jurnal',
fileListDetail: 'Amănunt',
fileListLabel: 'Detalii fișier',
testWithParams: 'Test cu parametri',
},
promptLog: 'Jurnal prompt',
agentLog: 'Jurnal agent',

View File

@ -82,6 +82,7 @@ const translation = {
workflowTitle: 'Подробная информация о журнале',
fileListLabel: 'Сведения о файле',
fileListDetail: 'Подробность',
testWithParams: 'Тест с параметрами',
},
promptLog: 'Журнал подсказок',
agentLog: 'Журнал агента',

View File

@ -82,6 +82,7 @@ const translation = {
workflowTitle: 'Podrobnosti dnevnika',
fileListDetail: 'Podrobnosti',
fileListLabel: 'Podrobnosti o datoteki',
testWithParams: 'Preizkus s parametri',
},
promptLog: 'Dnevnik PROMPT-ov',
agentLog: 'Dnevnik pomočnika',

View File

@ -82,6 +82,7 @@ const translation = {
workflowTitle: 'รายละเอียดบันทึก',
fileListDetail: 'รายละเอียด',
fileListLabel: 'รายละเอียดไฟล์',
testWithParams: 'ทดสอบด้วยพารามิเตอร์',
},
promptLog: 'บันทึกพร้อมท์',
agentLog: 'บันทึกตัวแทน',

View File

@ -82,6 +82,7 @@ const translation = {
workflowTitle: 'Günlük Detayı',
fileListDetail: 'Ayrıntı',
fileListLabel: 'Dosya Detayları',
testWithParams: 'Parametrelerle Test',
},
promptLog: 'Prompt Günlüğü',
agentLog: 'Agent Günlüğü',

View File

@ -82,6 +82,7 @@ const translation = {
workflowTitle: 'Деталі Журналу',
fileListDetail: 'Деталь',
fileListLabel: 'Подробиці файлу',
testWithParams: 'Тест з параметрами',
},
promptLog: 'Журнал Запитань',
agentLog: 'Журнал агента',

View File

@ -82,6 +82,7 @@ const translation = {
workflowTitle: 'Chi tiết nhật ký',
fileListDetail: 'Chi tiết',
fileListLabel: 'Chi tiết tệp',
testWithParams: 'Kiểm tra với các tham số',
},
promptLog: 'Nhật ký lời nhắc',
viewLog: 'Xem nhật ký',

View File

@ -83,6 +83,7 @@ const translation = {
workflowTitle: '日志详情',
fileListLabel: '文件详情',
fileListDetail: '详情',
testWithParams: '按此参数测试',
},
promptLog: 'Prompt 日志',
agentLog: 'Agent 日志',

View File

@ -82,6 +82,7 @@ const translation = {
workflowTitle: '日誌詳情',
fileListDetail: '細節',
fileListLabel: '檔詳細資訊',
testWithParams: '使用參數測試',
},
promptLog: 'Prompt 日誌',
agentLog: 'Agent 日誌',

View File

@ -146,7 +146,7 @@
"@babel/core": "^7.28.3",
"@chromatic-com/storybook": "^3.1.0",
"@eslint-react/eslint-plugin": "^1.15.0",
"@happy-dom/jest-environment": "^20.0.0",
"@happy-dom/jest-environment": "^20.0.2",
"@mdx-js/loader": "^3.1.0",
"@mdx-js/react": "^3.1.0",
"@next/bundle-analyzer": "15.5.4",

18
web/pnpm-lock.yaml generated
View File

@ -351,8 +351,8 @@ importers:
specifier: ^1.15.0
version: 1.52.3(eslint@9.36.0(jiti@2.6.1))(ts-api-utils@2.1.0(typescript@5.8.3))(typescript@5.8.3)
'@happy-dom/jest-environment':
specifier: ^20.0.0
version: 20.0.0(@jest/environment@29.7.0)(@jest/fake-timers@29.7.0)(@jest/types@29.6.3)(jest-mock@29.7.0)(jest-util@29.7.0)
specifier: ^20.0.2
version: 20.0.4(@jest/environment@29.7.0)(@jest/fake-timers@29.7.0)(@jest/types@29.6.3)(jest-mock@29.7.0)(jest-util@29.7.0)
'@mdx-js/loader':
specifier: ^3.1.0
version: 3.1.0(acorn@8.15.0)(webpack@5.100.2(esbuild@0.25.0)(uglify-js@3.19.3))
@ -1649,8 +1649,8 @@ packages:
'@formatjs/intl-localematcher@0.5.10':
resolution: {integrity: sha512-af3qATX+m4Rnd9+wHcjJ4w2ijq+rAVP3CCinJQvFv1kgSu1W6jypUmvleJxcewdxmutM8dmIRZFxO/IQBZmP2Q==}
'@happy-dom/jest-environment@20.0.0':
resolution: {integrity: sha512-dUyMDNJzPDFopSDyzKdbeYs8z9B4jLj9kXnru8TjYdGeLsQKf+6r0lq/9T2XVcu04QFxXMykt64A+KjsaJTaNA==}
'@happy-dom/jest-environment@20.0.4':
resolution: {integrity: sha512-75OcYtjO+jqxWiYiXvbwR8JZITX1/8iAjRSRpZ/rNjO6UnYebwX6HdI91Ix09xYZEO1X/xOof6HX1EiZnrgnXA==}
engines: {node: '>=20.0.0'}
peerDependencies:
'@jest/environment': '>=25.0.0'
@ -5544,8 +5544,8 @@ packages:
hachure-fill@0.5.2:
resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==}
happy-dom@20.0.0:
resolution: {integrity: sha512-GkWnwIFxVGCf2raNrxImLo397RdGhLapj5cT3R2PT7FwL62Ze1DROhzmYW7+J3p9105DYMVenEejEbnq5wA37w==}
happy-dom@20.0.4:
resolution: {integrity: sha512-WxFtvnij6G64/MtMimnZhF0nKx3LUQKc20zjATD6tKiqOykUwQkd+2FW/DZBAFNjk4oWh0xdv/HBleGJmSY/Iw==}
engines: {node: '>=20.0.0'}
has-flag@4.0.0:
@ -10122,12 +10122,12 @@ snapshots:
dependencies:
tslib: 2.8.1
'@happy-dom/jest-environment@20.0.0(@jest/environment@29.7.0)(@jest/fake-timers@29.7.0)(@jest/types@29.6.3)(jest-mock@29.7.0)(jest-util@29.7.0)':
'@happy-dom/jest-environment@20.0.4(@jest/environment@29.7.0)(@jest/fake-timers@29.7.0)(@jest/types@29.6.3)(jest-mock@29.7.0)(jest-util@29.7.0)':
dependencies:
'@jest/environment': 29.7.0
'@jest/fake-timers': 29.7.0
'@jest/types': 29.6.3
happy-dom: 20.0.0
happy-dom: 20.0.4
jest-mock: 29.7.0
jest-util: 29.7.0
@ -14730,7 +14730,7 @@ snapshots:
hachure-fill@0.5.2: {}
happy-dom@20.0.0:
happy-dom@20.0.4:
dependencies:
'@types/node': 20.19.20
'@types/whatwg-mimetype': 3.0.2

View File

@ -324,7 +324,7 @@ const baseFetch = base
type UploadOptions = {
xhr: XMLHttpRequest
method: string
method?: string
url?: string
headers?: Record<string, string>
data: FormData

13
web/types/jsx.d.ts vendored Normal file
View File

@ -0,0 +1,13 @@
// TypeScript type definitions for custom JSX elements
// Custom JSX elements for emoji-mart web components
import 'react'
declare module 'react' {
namespace JSX {
// eslint-disable-next-line ts/consistent-type-definitions
interface IntrinsicElements {
'em-emoji': React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>
}
}
}

View File

@ -0,0 +1,23 @@
/**
* Validates that a URL is safe for redirection.
* Only allows HTTP and HTTPS protocols to prevent XSS attacks.
*
* @param url - The URL string to validate
* @throws Error if the URL has an unsafe protocol
*/
export function validateRedirectUrl(url: string): void {
try {
const parsedUrl = new URL(url)
if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:')
throw new Error('Authorization URL must be HTTP or HTTPS')
}
catch (error) {
if (
error instanceof Error
&& error.message === 'Authorization URL must be HTTP or HTTPS'
)
throw error
// If URL parsing fails, it's also invalid
throw new Error(`Invalid URL: ${url}`)
}
}