mirror of
https://github.com/langgenius/dify.git
synced 2026-05-06 02:18:08 +08:00
Merge branch 'main' into feat/model-auth
This commit is contained in:
@ -6,7 +6,7 @@ LABEL maintainer="takatost@gmail.com"
|
||||
# RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
|
||||
|
||||
RUN apk add --no-cache tzdata
|
||||
RUN npm install -g pnpm@10.13.1
|
||||
RUN corepack enable
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
|
||||
@ -19,6 +19,9 @@ WORKDIR /app/web
|
||||
COPY package.json .
|
||||
COPY pnpm-lock.yaml .
|
||||
|
||||
# Use packageManager from package.json
|
||||
RUN corepack install
|
||||
|
||||
# if you located in China, you can use taobao registry to speed up
|
||||
# RUN pnpm install --frozen-lockfile --registry https://registry.npmmirror.com/
|
||||
|
||||
|
||||
@ -37,7 +37,7 @@ describe('CommandSelector', () => {
|
||||
},
|
||||
knowledge: {
|
||||
key: '@knowledge',
|
||||
shortcut: '@knowledge',
|
||||
shortcut: '@kb',
|
||||
title: 'Search Knowledge',
|
||||
description: 'Search knowledge bases',
|
||||
search: jest.fn(),
|
||||
@ -75,7 +75,7 @@ describe('CommandSelector', () => {
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('command-item-@app')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('command-item-@knowledge')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('command-item-@kb')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('command-item-@plugin')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('command-item-@node')).toBeInTheDocument()
|
||||
})
|
||||
@ -90,7 +90,7 @@ describe('CommandSelector', () => {
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('command-item-@app')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('command-item-@knowledge')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('command-item-@kb')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('command-item-@plugin')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('command-item-@node')).toBeInTheDocument()
|
||||
})
|
||||
@ -107,7 +107,7 @@ describe('CommandSelector', () => {
|
||||
)
|
||||
|
||||
expect(screen.queryByTestId('command-item-@app')).not.toBeInTheDocument()
|
||||
expect(screen.getByTestId('command-item-@knowledge')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('command-item-@kb')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('command-item-@plugin')).not.toBeInTheDocument()
|
||||
expect(screen.queryByTestId('command-item-@node')).not.toBeInTheDocument()
|
||||
})
|
||||
@ -122,7 +122,7 @@ describe('CommandSelector', () => {
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('command-item-@app')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('command-item-@knowledge')).not.toBeInTheDocument()
|
||||
expect(screen.queryByTestId('command-item-@kb')).not.toBeInTheDocument()
|
||||
expect(screen.getByTestId('command-item-@plugin')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('command-item-@node')).not.toBeInTheDocument()
|
||||
})
|
||||
@ -137,7 +137,7 @@ describe('CommandSelector', () => {
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('command-item-@app')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('command-item-@knowledge')).not.toBeInTheDocument()
|
||||
expect(screen.queryByTestId('command-item-@kb')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should match partial strings', () => {
|
||||
@ -145,14 +145,14 @@ describe('CommandSelector', () => {
|
||||
<CommandSelector
|
||||
actions={mockActions}
|
||||
onCommandSelect={mockOnCommandSelect}
|
||||
searchFilter="nowl"
|
||||
searchFilter="od"
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.queryByTestId('command-item-@app')).not.toBeInTheDocument()
|
||||
expect(screen.getByTestId('command-item-@knowledge')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('command-item-@kb')).not.toBeInTheDocument()
|
||||
expect(screen.queryByTestId('command-item-@plugin')).not.toBeInTheDocument()
|
||||
expect(screen.queryByTestId('command-item-@node')).not.toBeInTheDocument()
|
||||
expect(screen.getByTestId('command-item-@node')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -167,7 +167,7 @@ describe('CommandSelector', () => {
|
||||
)
|
||||
|
||||
expect(screen.queryByTestId('command-item-@app')).not.toBeInTheDocument()
|
||||
expect(screen.queryByTestId('command-item-@knowledge')).not.toBeInTheDocument()
|
||||
expect(screen.queryByTestId('command-item-@kb')).not.toBeInTheDocument()
|
||||
expect(screen.queryByTestId('command-item-@plugin')).not.toBeInTheDocument()
|
||||
expect(screen.queryByTestId('command-item-@node')).not.toBeInTheDocument()
|
||||
|
||||
@ -210,7 +210,7 @@ describe('CommandSelector', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(mockOnCommandValueChange).toHaveBeenCalledWith('@knowledge')
|
||||
expect(mockOnCommandValueChange).toHaveBeenCalledWith('@kb')
|
||||
})
|
||||
|
||||
it('should not call onCommandValueChange if current value still exists', () => {
|
||||
@ -246,10 +246,10 @@ describe('CommandSelector', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
const knowledgeItem = screen.getByTestId('command-item-@knowledge')
|
||||
const knowledgeItem = screen.getByTestId('command-item-@kb')
|
||||
fireEvent.click(knowledgeItem)
|
||||
|
||||
expect(mockOnCommandSelect).toHaveBeenCalledWith('@knowledge')
|
||||
expect(mockOnCommandSelect).toHaveBeenCalledWith('@kb')
|
||||
})
|
||||
})
|
||||
|
||||
@ -276,7 +276,7 @@ describe('CommandSelector', () => {
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('command-item-@app')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('command-item-@knowledge')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('command-item-@kb')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('command-item-@plugin')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('command-item-@node')).toBeInTheDocument()
|
||||
})
|
||||
@ -312,7 +312,7 @@ describe('CommandSelector', () => {
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('command-item-@app')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('command-item-@knowledge')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('command-item-@kb')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('command-item-@plugin')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('command-item-@node')).toBeInTheDocument()
|
||||
})
|
||||
@ -326,7 +326,7 @@ describe('CommandSelector', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('command-item-@knowledge')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('command-item-@kb')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('command-item-@app')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -3,7 +3,7 @@ import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import AppCard from '@/app/components/app/overview/appCard'
|
||||
import AppCard from '@/app/components/app/overview/app-card'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import MCPServiceCard from '@/app/components/tools/mcp/mcp-service-card'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
@ -17,7 +17,7 @@ import type { App } from '@/types/app'
|
||||
import type { UpdateAppSiteCodeResponse } from '@/models/app'
|
||||
import { asyncRunSafe } from '@/utils'
|
||||
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
||||
import type { IAppCardProps } from '@/app/components/app/overview/appCard'
|
||||
import type { IAppCardProps } from '@/app/components/app/overview/app-card'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
|
||||
export type ICardViewProps = {
|
||||
@ -3,8 +3,8 @@ import React, { useState } from 'react'
|
||||
import dayjs from 'dayjs'
|
||||
import quarterOfYear from 'dayjs/plugin/quarterOfYear'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { PeriodParams } from '@/app/components/app/overview/appChart'
|
||||
import { AvgResponseTime, AvgSessionInteractions, AvgUserInteractions, ConversationsChart, CostChart, EndUsersChart, MessagesChart, TokenPerSecond, UserSatisfactionRate, WorkflowCostChart, WorkflowDailyTerminalsChart, WorkflowMessagesChart } from '@/app/components/app/overview/appChart'
|
||||
import type { PeriodParams } from '@/app/components/app/overview/app-chart'
|
||||
import { AvgResponseTime, AvgSessionInteractions, AvgUserInteractions, ConversationsChart, CostChart, EndUsersChart, MessagesChart, TokenPerSecond, UserSatisfactionRate, WorkflowCostChart, WorkflowDailyTerminalsChart, WorkflowMessagesChart } from '@/app/components/app/overview/app-chart'
|
||||
import type { Item } from '@/app/components/base/select'
|
||||
import { SimpleSelect } from '@/app/components/base/select'
|
||||
import { TIME_PERIOD_MAPPING } from '@/app/components/app/log/filter'
|
||||
@ -54,6 +54,7 @@ export default function ChartView({ appId, headerRight }: IChartViewProps) {
|
||||
<SimpleSelect
|
||||
items={Object.entries(TIME_PERIOD_MAPPING).map(([k, v]) => ({ value: k, name: t(`appLog.filter.period.${v.name}`) }))}
|
||||
className='mt-0 !w-40'
|
||||
notClearable={true}
|
||||
onSelect={(item) => {
|
||||
const id = item.value
|
||||
const value = TIME_PERIOD_MAPPING[id]?.value ?? '-1'
|
||||
@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import ChartView from './chartView'
|
||||
import ChartView from './chart-view'
|
||||
import TracingPanel from './tracing/panel'
|
||||
import ApikeyInfoPanel from '@/app/components/app/overview/apikey-info-panel'
|
||||
|
||||
|
||||
@ -216,8 +216,8 @@ const DatasetCard = ({
|
||||
}
|
||||
btnClassName={open =>
|
||||
cn(
|
||||
open ? '!bg-black/5 !shadow-none' : '!bg-transparent',
|
||||
'h-8 w-8 rounded-md border-none !p-2 hover:!bg-black/5',
|
||||
open ? '!bg-state-base-hover !shadow-none' : '!bg-transparent',
|
||||
'h-8 w-8 rounded-md border-none !p-2 hover:!bg-state-base-hover',
|
||||
)
|
||||
}
|
||||
className={'!z-20 h-fit !w-[128px]'}
|
||||
|
||||
@ -13,14 +13,14 @@ const Header = () => {
|
||||
const router = useRouter()
|
||||
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
|
||||
|
||||
const back = useCallback(() => {
|
||||
router.back()
|
||||
const goToStudio = useCallback(() => {
|
||||
router.push('/apps')
|
||||
}, [router])
|
||||
|
||||
return (
|
||||
<div className='flex flex-1 items-center justify-between px-4'>
|
||||
<div className='flex items-center gap-3'>
|
||||
<div className='flex cursor-pointer items-center' onClick={back}>
|
||||
<div className='flex cursor-pointer items-center' onClick={goToStudio}>
|
||||
{systemFeatures.branding.enabled && systemFeatures.branding.login_page_logo
|
||||
? <img
|
||||
src={systemFeatures.branding.login_page_logo}
|
||||
@ -33,7 +33,7 @@ const Header = () => {
|
||||
<p className='title-3xl-semi-bold relative mt-[-2px] text-text-primary'>{t('common.account.account')}</p>
|
||||
</div>
|
||||
<div className='flex shrink-0 items-center gap-3'>
|
||||
<Button className='system-sm-medium gap-2 px-3 py-2' onClick={back}>
|
||||
<Button className='system-sm-medium gap-2 px-3 py-2' onClick={goToStudio}>
|
||||
<RiRobot2Line className='h-4 w-4' />
|
||||
<p>{t('common.account.studio')}</p>
|
||||
<RiArrowRightUpLine className='h-4 w-4' />
|
||||
|
||||
@ -25,7 +25,7 @@ import type { EnvironmentVariable } from '@/app/components/workflow/types'
|
||||
import { fetchWorkflowDraft } from '@/service/workflow'
|
||||
import ContentDialog from '@/app/components/base/content-dialog'
|
||||
import Button from '@/app/components/base/button'
|
||||
import CardView from '@/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/cardView'
|
||||
import CardView from '@/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view'
|
||||
import Divider from '../base/divider'
|
||||
import type { Operation } from './app-operations'
|
||||
import AppOperations from './app-operations'
|
||||
|
||||
@ -13,7 +13,7 @@ import Tooltip from '@/app/components/base/tooltip'
|
||||
import { AppType } from '@/types/app'
|
||||
import { getNewVar, getVars } from '@/utils/var'
|
||||
import AutomaticBtn from '@/app/components/app/configuration/config/automatic/automatic-btn'
|
||||
import type { AutomaticRes } from '@/service/debug'
|
||||
import type { GenRes } from '@/service/debug'
|
||||
import GetAutomaticResModal from '@/app/components/app/configuration/config/automatic/get-automatic-res'
|
||||
import PromptEditor from '@/app/components/base/prompt-editor'
|
||||
import ConfigContext from '@/context/debug-configuration'
|
||||
@ -61,6 +61,7 @@ const Prompt: FC<ISimplePromptInput> = ({
|
||||
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
const {
|
||||
appId,
|
||||
modelConfig,
|
||||
dataSets,
|
||||
setModelConfig,
|
||||
@ -139,21 +140,21 @@ const Prompt: FC<ISimplePromptInput> = ({
|
||||
}
|
||||
|
||||
const [showAutomatic, { setTrue: showAutomaticTrue, setFalse: showAutomaticFalse }] = useBoolean(false)
|
||||
const handleAutomaticRes = (res: AutomaticRes) => {
|
||||
const handleAutomaticRes = (res: GenRes) => {
|
||||
// put eventEmitter in first place to prevent overwrite the configs.prompt_variables.But another problem is that prompt won't hight the prompt_variables.
|
||||
eventEmitter?.emit({
|
||||
type: PROMPT_EDITOR_UPDATE_VALUE_BY_EVENT_EMITTER,
|
||||
payload: res.prompt,
|
||||
payload: res.modified,
|
||||
} as any)
|
||||
const newModelConfig = produce(modelConfig, (draft) => {
|
||||
draft.configs.prompt_template = res.prompt
|
||||
draft.configs.prompt_variables = res.variables.map(key => ({ key, name: key, type: 'string', required: true }))
|
||||
draft.configs.prompt_template = res.modified
|
||||
draft.configs.prompt_variables = (res.variables || []).map(key => ({ key, name: key, type: 'string', required: true }))
|
||||
})
|
||||
setModelConfig(newModelConfig)
|
||||
setPrevPromptConfig(modelConfig.configs)
|
||||
|
||||
if (mode !== AppType.completion) {
|
||||
setIntroduction(res.opening_statement)
|
||||
setIntroduction(res.opening_statement || '')
|
||||
const newFeatures = produce(features, (draft) => {
|
||||
draft.opening = {
|
||||
...draft.opening,
|
||||
@ -272,10 +273,13 @@ const Prompt: FC<ISimplePromptInput> = ({
|
||||
|
||||
{showAutomatic && (
|
||||
<GetAutomaticResModal
|
||||
flowId={appId}
|
||||
mode={mode as AppType}
|
||||
isShow={showAutomatic}
|
||||
onClose={showAutomaticFalse}
|
||||
onFinished={handleAutomaticRes}
|
||||
currentPrompt={promptTemplate}
|
||||
isBasicMode
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { useBoolean, useSessionStorageState } from 'ahooks'
|
||||
import {
|
||||
RiDatabase2Line,
|
||||
RiFileExcel2Line,
|
||||
@ -14,24 +14,18 @@ import {
|
||||
RiTranslate,
|
||||
RiUser2Line,
|
||||
} from '@remixicon/react'
|
||||
import cn from 'classnames'
|
||||
import s from './style.module.css'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { generateRule } from '@/service/debug'
|
||||
import ConfigPrompt from '@/app/components/app/configuration/config-prompt'
|
||||
import { generateBasicAppFistTimeRule, generateRule } from '@/service/debug'
|
||||
import type { CompletionParams, Model } from '@/types/app'
|
||||
import { AppType } from '@/types/app'
|
||||
import ConfigVar from '@/app/components/app/configuration/config-var'
|
||||
import GroupName from '@/app/components/app/configuration/base/group-name'
|
||||
import type { AppType } from '@/types/app'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
import { LoveMessage } from '@/app/components/base/icons/src/vender/features'
|
||||
|
||||
// type
|
||||
import type { AutomaticRes } from '@/service/debug'
|
||||
import type { GenRes } from '@/service/debug'
|
||||
import { Generator } from '@/app/components/base/icons/src/vender/other'
|
||||
import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal'
|
||||
|
||||
@ -39,13 +33,25 @@ import { ModelTypeEnum } from '@/app/components/header/account-setting/model-pro
|
||||
import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import type { ModelModeType } from '@/types/app'
|
||||
import type { FormValue } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import InstructionEditorInWorkflow from './instruction-editor-in-workflow'
|
||||
import InstructionEditorInBasic from './instruction-editor'
|
||||
import { GeneratorType } from './types'
|
||||
import Result from './result'
|
||||
import useGenData from './use-gen-data'
|
||||
import IdeaOutput from './idea-output'
|
||||
import ResPlaceholder from './res-placeholder'
|
||||
import { useGenerateRuleTemplate } from '@/service/use-apps'
|
||||
|
||||
const i18nPrefix = 'appDebug.generate'
|
||||
export type IGetAutomaticResProps = {
|
||||
mode: AppType
|
||||
isShow: boolean
|
||||
onClose: () => void
|
||||
onFinished: (res: AutomaticRes) => void
|
||||
isInLLMNode?: boolean
|
||||
onFinished: (res: GenRes) => void
|
||||
flowId?: string
|
||||
nodeId?: string
|
||||
currentPrompt?: string
|
||||
isBasicMode?: boolean
|
||||
}
|
||||
|
||||
const TryLabel: FC<{
|
||||
@ -68,7 +74,10 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
|
||||
mode,
|
||||
isShow,
|
||||
onClose,
|
||||
isInLLMNode,
|
||||
flowId,
|
||||
nodeId,
|
||||
currentPrompt,
|
||||
isBasicMode,
|
||||
onFinished,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
@ -123,13 +132,27 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
|
||||
},
|
||||
]
|
||||
|
||||
const [instruction, setInstruction] = useState<string>('')
|
||||
const [instructionFromSessionStorage, setInstruction] = useSessionStorageState<string>(`improve-instruction-${flowId}${isBasicMode ? '' : `-${nodeId}`}`)
|
||||
const instruction = instructionFromSessionStorage || ''
|
||||
const [ideaOutput, setIdeaOutput] = useState<string>('')
|
||||
|
||||
const [editorKey, setEditorKey] = useState(`${flowId}-0`)
|
||||
const handleChooseTemplate = useCallback((key: string) => {
|
||||
return () => {
|
||||
const template = t(`appDebug.generate.template.${key}.instruction`)
|
||||
setInstruction(template)
|
||||
setEditorKey(`${flowId}-${Date.now()}`)
|
||||
}
|
||||
}, [t])
|
||||
|
||||
const { data: instructionTemplate } = useGenerateRuleTemplate(GeneratorType.prompt, isBasicMode)
|
||||
useEffect(() => {
|
||||
if (!instruction && instructionTemplate)
|
||||
setInstruction(instructionTemplate.data)
|
||||
|
||||
setEditorKey(`${flowId}-${Date.now()}`)
|
||||
}, [instructionTemplate])
|
||||
|
||||
const isValid = () => {
|
||||
if (instruction.trim() === '') {
|
||||
Toast.notify({
|
||||
@ -143,7 +166,10 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
|
||||
return true
|
||||
}
|
||||
const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false)
|
||||
const [res, setRes] = useState<AutomaticRes | null>(null)
|
||||
const storageKey = `${flowId}${isBasicMode ? '' : `-${nodeId}`}`
|
||||
const { addVersion, current, currentVersionIndex, setCurrentVersionIndex, versions } = useGenData({
|
||||
storageKey,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (defaultModel) {
|
||||
@ -170,16 +196,6 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
|
||||
</div>
|
||||
)
|
||||
|
||||
const renderNoData = (
|
||||
<div className='flex h-full w-0 grow flex-col items-center justify-center space-y-3 px-8'>
|
||||
<Generator className='h-14 w-14 text-text-tertiary' />
|
||||
<div className='text-center text-[13px] font-normal leading-5 text-text-tertiary'>
|
||||
<div>{t('appDebug.generate.noDataLine1')}</div>
|
||||
<div>{t('appDebug.generate.noDataLine2')}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
const handleModelChange = useCallback((newValue: { modelId: string; provider: string; mode?: string; features?: string[] }) => {
|
||||
const newModel = {
|
||||
...model,
|
||||
@ -207,28 +223,59 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
|
||||
return
|
||||
setLoadingTrue()
|
||||
try {
|
||||
const { error, ...res } = await generateRule({
|
||||
instruction,
|
||||
model_config: model,
|
||||
no_variable: !!isInLLMNode,
|
||||
})
|
||||
setRes(res)
|
||||
if (error) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: error,
|
||||
let apiRes: GenRes
|
||||
let hasError = false
|
||||
if (isBasicMode || !currentPrompt) {
|
||||
const { error, ...res } = await generateBasicAppFistTimeRule({
|
||||
instruction,
|
||||
model_config: model,
|
||||
no_variable: false,
|
||||
})
|
||||
apiRes = {
|
||||
...res,
|
||||
modified: res.prompt,
|
||||
} as GenRes
|
||||
if (error) {
|
||||
hasError = true
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: error,
|
||||
})
|
||||
}
|
||||
}
|
||||
else {
|
||||
const { error, ...res } = await generateRule({
|
||||
flow_id: flowId,
|
||||
node_id: nodeId,
|
||||
current: currentPrompt,
|
||||
instruction,
|
||||
ideal_output: ideaOutput,
|
||||
model_config: model,
|
||||
})
|
||||
apiRes = res
|
||||
if (error) {
|
||||
hasError = true
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: error,
|
||||
})
|
||||
}
|
||||
}
|
||||
if (!hasError)
|
||||
addVersion(apiRes)
|
||||
}
|
||||
finally {
|
||||
setLoadingFalse()
|
||||
}
|
||||
}
|
||||
|
||||
const [showConfirmOverwrite, setShowConfirmOverwrite] = React.useState(false)
|
||||
const [isShowConfirmOverwrite, {
|
||||
setTrue: showConfirmOverwrite,
|
||||
setFalse: hideShowConfirmOverwrite,
|
||||
}] = useBoolean(false)
|
||||
|
||||
const isShowAutoPromptResPlaceholder = () => {
|
||||
return !isLoading && !res
|
||||
return !isLoading && !current
|
||||
}
|
||||
|
||||
return (
|
||||
@ -236,15 +283,14 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
|
||||
isShow={isShow}
|
||||
onClose={onClose}
|
||||
className='min-w-[1140px] !p-0'
|
||||
closable
|
||||
>
|
||||
<div className='flex h-[680px] flex-wrap'>
|
||||
<div className='h-full w-[570px] shrink-0 overflow-y-auto border-r border-divider-regular p-6'>
|
||||
<div className='mb-8'>
|
||||
<div className='mb-5'>
|
||||
<div className={`text-lg font-bold leading-[28px] ${s.textGradient}`}>{t('appDebug.generate.title')}</div>
|
||||
<div className='mt-1 text-[13px] font-normal text-text-tertiary'>{t('appDebug.generate.description')}</div>
|
||||
</div>
|
||||
<div className='mb-8'>
|
||||
<div>
|
||||
<ModelParameterModal
|
||||
popupClassName='!w-[520px]'
|
||||
portalToFollowElemContentClassName='z-[1000]'
|
||||
@ -258,116 +304,99 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
|
||||
hideDebugWithMultipleModel
|
||||
/>
|
||||
</div>
|
||||
<div >
|
||||
<div className='flex items-center'>
|
||||
<div className='mr-3 shrink-0 text-xs font-semibold uppercase leading-[18px] text-text-tertiary'>{t('appDebug.generate.tryIt')}</div>
|
||||
<div className='h-px grow' style={{
|
||||
background: 'linear-gradient(to right, rgba(243, 244, 246, 1), rgba(243, 244, 246, 0))',
|
||||
}}></div>
|
||||
</div>
|
||||
<div className='flex flex-wrap'>
|
||||
{tryList.map(item => (
|
||||
<TryLabel
|
||||
key={item.key}
|
||||
Icon={item.icon}
|
||||
text={t(`appDebug.generate.template.${item.key}.name`)}
|
||||
onClick={handleChooseTemplate(item.key)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{/* inputs */}
|
||||
<div className='mt-6'>
|
||||
<div className='text-[0px]'>
|
||||
<div className='mb-2 text-sm font-medium leading-5 text-text-primary'>{t('appDebug.generate.instruction')}</div>
|
||||
<Textarea
|
||||
className="h-[200px] resize-none"
|
||||
placeholder={t('appDebug.generate.instructionPlaceHolder') as string}
|
||||
value={instruction}
|
||||
onChange={e => setInstruction(e.target.value)} />
|
||||
{isBasicMode && (
|
||||
<div className='mt-4'>
|
||||
<div className='flex items-center'>
|
||||
<div className='mr-3 shrink-0 text-xs font-semibold uppercase leading-[18px] text-text-tertiary'>{t('appDebug.generate.tryIt')}</div>
|
||||
<div className='h-px grow' style={{
|
||||
background: 'linear-gradient(to right, rgba(243, 244, 246, 1), rgba(243, 244, 246, 0))',
|
||||
}}></div>
|
||||
</div>
|
||||
<div className='flex flex-wrap'>
|
||||
{tryList.map(item => (
|
||||
<TryLabel
|
||||
key={item.key}
|
||||
Icon={item.icon}
|
||||
text={t(`appDebug.generate.template.${item.key}.name`)}
|
||||
onClick={handleChooseTemplate(item.key)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className='mt-5 flex justify-end'>
|
||||
{/* inputs */}
|
||||
<div className='mt-4'>
|
||||
<div>
|
||||
<div className='system-sm-semibold-uppercase mb-1.5 text-text-secondary'>{t('appDebug.generate.instruction')}</div>
|
||||
{isBasicMode ? (
|
||||
<InstructionEditorInBasic
|
||||
editorKey={editorKey}
|
||||
generatorType={GeneratorType.prompt}
|
||||
value={instruction}
|
||||
onChange={setInstruction}
|
||||
availableVars={[]}
|
||||
availableNodes={[]}
|
||||
isShowCurrentBlock={!!currentPrompt}
|
||||
isShowLastRunBlock={false}
|
||||
/>
|
||||
) : (
|
||||
<InstructionEditorInWorkflow
|
||||
editorKey={editorKey}
|
||||
generatorType={GeneratorType.prompt}
|
||||
value={instruction}
|
||||
onChange={setInstruction}
|
||||
nodeId={nodeId || ''}
|
||||
isShowCurrentBlock={!!currentPrompt}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<IdeaOutput
|
||||
value={ideaOutput}
|
||||
onChange={setIdeaOutput}
|
||||
/>
|
||||
|
||||
<div className='mt-7 flex justify-end space-x-2'>
|
||||
<Button onClick={onClose}>{t(`${i18nPrefix}.dismiss`)}</Button>
|
||||
<Button
|
||||
className='flex space-x-1'
|
||||
variant='primary'
|
||||
onClick={onGenerate}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<Generator className='h-4 w-4 text-white' />
|
||||
<span className='text-xs font-semibold text-white'>{t('appDebug.generate.generate')}</span>
|
||||
<Generator className='h-4 w-4' />
|
||||
<span className='text-xs font-semibold'>{t('appDebug.generate.generate')}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{(!isLoading && res) && (
|
||||
<div className='h-full w-0 grow p-6 pb-0'>
|
||||
<div className='mb-3 shrink-0 text-base font-semibold leading-[160%] text-text-secondary'>{t('appDebug.generate.resTitle')}</div>
|
||||
<div className={cn('max-h-[555px] overflow-y-auto', !isInLLMNode && 'pb-2')}>
|
||||
<ConfigPrompt
|
||||
mode={mode}
|
||||
promptTemplate={res?.prompt || ''}
|
||||
promptVariables={[]}
|
||||
readonly
|
||||
noTitle={isInLLMNode}
|
||||
gradientBorder
|
||||
editorHeight={isInLLMNode ? 524 : 0}
|
||||
noResize={isInLLMNode}
|
||||
/>
|
||||
{!isInLLMNode && (
|
||||
<>
|
||||
{(res?.variables?.length && res?.variables?.length > 0)
|
||||
? (
|
||||
<ConfigVar
|
||||
promptVariables={res?.variables.map(key => ({ key, name: key, type: 'string', required: true })) || []}
|
||||
readonly
|
||||
/>
|
||||
)
|
||||
: ''}
|
||||
|
||||
{(mode !== AppType.completion && res?.opening_statement) && (
|
||||
<div className='mt-7'>
|
||||
<GroupName name={t('appDebug.feature.groupChat.title')} />
|
||||
<div
|
||||
className='mb-1 rounded-xl border-l-[0.5px] border-t-[0.5px] border-effects-highlight bg-background-section-burn p-3'
|
||||
>
|
||||
<div className='mb-2 flex items-center gap-2'>
|
||||
<div className='shrink-0 rounded-lg border-[0.5px] border-divider-subtle bg-util-colors-blue-light-blue-light-500 p-1 shadow-xs'>
|
||||
<LoveMessage className='h-4 w-4 text-text-primary-on-surface' />
|
||||
</div>
|
||||
<div className='system-sm-semibold flex grow items-center text-text-secondary'>
|
||||
{t('appDebug.feature.conversationOpener.title')}
|
||||
</div>
|
||||
</div>
|
||||
<div className='system-xs-regular min-h-8 text-text-tertiary'>{res.opening_statement}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className='flex justify-end bg-background-default py-4'>
|
||||
<Button onClick={onClose}>{t('common.operation.cancel')}</Button>
|
||||
<Button variant='primary' className='ml-2' onClick={() => {
|
||||
setShowConfirmOverwrite(true)
|
||||
}}>{t('appDebug.generate.apply')}</Button>
|
||||
</div>
|
||||
{(!isLoading && current) && (
|
||||
<div className='h-full w-0 grow bg-background-default-subtle p-6 pb-0'>
|
||||
<Result
|
||||
current={current!}
|
||||
isBasicMode={isBasicMode}
|
||||
nodeId={nodeId!}
|
||||
currentVersionIndex={currentVersionIndex || 0}
|
||||
setCurrentVersionIndex={setCurrentVersionIndex}
|
||||
versions={versions || []}
|
||||
onApply={showConfirmOverwrite}
|
||||
generatorType={GeneratorType.prompt}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{isLoading && renderLoading}
|
||||
{isShowAutoPromptResPlaceholder() && renderNoData}
|
||||
{showConfirmOverwrite && (
|
||||
{isShowAutoPromptResPlaceholder() && <ResPlaceholder />}
|
||||
{isShowConfirmOverwrite && (
|
||||
<Confirm
|
||||
title={t('appDebug.generate.overwriteTitle')}
|
||||
content={t('appDebug.generate.overwriteMessage')}
|
||||
isShow={showConfirmOverwrite}
|
||||
isShow
|
||||
onConfirm={() => {
|
||||
setShowConfirmOverwrite(false)
|
||||
onFinished(res!)
|
||||
hideShowConfirmOverwrite()
|
||||
onFinished(current!)
|
||||
}}
|
||||
onCancel={() => setShowConfirmOverwrite(false)}
|
||||
onCancel={hideShowConfirmOverwrite}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -0,0 +1,48 @@
|
||||
'use client'
|
||||
import { ArrowDownRoundFill } from '@/app/components/base/icons/src/vender/solid/general'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import cn from '@/utils/classnames'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const i18nPrefix = 'appDebug.generate'
|
||||
|
||||
type Props = {
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
}
|
||||
|
||||
const IdeaOutput: FC<Props> = ({
|
||||
value,
|
||||
onChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [isFoldIdeaOutput, {
|
||||
toggle: toggleFoldIdeaOutput,
|
||||
}] = useBoolean(true)
|
||||
|
||||
return (
|
||||
<div className='mt-4 text-[0px]'>
|
||||
<div
|
||||
className='mb-1.5 flex cursor-pointer items-center text-sm font-medium leading-5 text-text-primary'
|
||||
onClick={toggleFoldIdeaOutput}
|
||||
>
|
||||
<div className='system-sm-semibold-uppercase mr-1 text-text-secondary'>{t(`${i18nPrefix}.idealOutput`)}</div>
|
||||
<div className='system-xs-regular text-text-tertiary'>({t(`${i18nPrefix}.optional`)})</div>
|
||||
<ArrowDownRoundFill className={cn('size text-text-quaternary', isFoldIdeaOutput && 'relative top-[1px] rotate-[-90deg]')} />
|
||||
</div>
|
||||
{!isFoldIdeaOutput && (
|
||||
<Textarea
|
||||
className="h-[80px]"
|
||||
placeholder={t(`${i18nPrefix}.idealOutputPlaceholder`)}
|
||||
value={value}
|
||||
onChange={e => onChange(e.target.value)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(IdeaOutput)
|
||||
@ -0,0 +1,58 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import type { GeneratorType } from './types'
|
||||
import type { ValueSelector, Var } from '@/app/components/workflow/types'
|
||||
import { VarType } from '@/app/components/workflow/types'
|
||||
import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list'
|
||||
import InstructionEditor from './instruction-editor'
|
||||
import { useWorkflowVariableType } from '@/app/components/workflow/hooks'
|
||||
import { useWorkflowStore } from '@/app/components/workflow/store'
|
||||
|
||||
type Props = {
|
||||
nodeId: string
|
||||
value: string
|
||||
editorKey: string
|
||||
onChange: (text: string) => void
|
||||
generatorType: GeneratorType
|
||||
isShowCurrentBlock: boolean
|
||||
}
|
||||
|
||||
const InstructionEditorInWorkflow: FC<Props> = ({
|
||||
nodeId,
|
||||
value,
|
||||
editorKey,
|
||||
onChange,
|
||||
generatorType,
|
||||
isShowCurrentBlock,
|
||||
}) => {
|
||||
const workflowStore = useWorkflowStore()
|
||||
const filterVar = useCallback((payload: Var, selector: ValueSelector) => {
|
||||
const { nodesWithInspectVars } = workflowStore.getState()
|
||||
const nodeId = selector?.[0]
|
||||
return !!nodesWithInspectVars.find(node => node.nodeId === nodeId) && payload.type !== VarType.file && payload.type !== VarType.arrayFile
|
||||
}, [workflowStore])
|
||||
const {
|
||||
availableVars,
|
||||
availableNodes,
|
||||
} = useAvailableVarList(nodeId, {
|
||||
onlyLeafNodeVar: false,
|
||||
filterVar,
|
||||
})
|
||||
const getVarType = useWorkflowVariableType()
|
||||
|
||||
return (
|
||||
<InstructionEditor
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
editorKey={editorKey}
|
||||
generatorType={generatorType}
|
||||
availableVars={availableVars}
|
||||
availableNodes={availableNodes}
|
||||
getVarType={getVarType}
|
||||
isShowCurrentBlock={isShowCurrentBlock}
|
||||
isShowLastRunBlock
|
||||
/>
|
||||
)
|
||||
}
|
||||
export default React.memo(InstructionEditorInWorkflow)
|
||||
@ -0,0 +1,117 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import PromptEditor from '@/app/components/base/prompt-editor'
|
||||
import type { GeneratorType } from './types'
|
||||
import cn from '@/utils/classnames'
|
||||
import type { Node, NodeOutPutVar, ValueSelector } from '@/app/components/workflow/types'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Type } from '@/app/components/workflow/nodes/llm/types'
|
||||
import { PROMPT_EDITOR_INSERT_QUICKLY } from '@/app/components/base/prompt-editor/plugins/update-block'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
|
||||
type Props = {
|
||||
editorKey: string
|
||||
value: string
|
||||
onChange: (text: string) => void
|
||||
generatorType: GeneratorType
|
||||
availableVars: NodeOutPutVar[]
|
||||
availableNodes: Node[]
|
||||
getVarType?: (params: {
|
||||
nodeId: string,
|
||||
valueSelector: ValueSelector,
|
||||
}) => Type
|
||||
isShowCurrentBlock: boolean
|
||||
isShowLastRunBlock: boolean
|
||||
}
|
||||
|
||||
const i18nPrefix = 'appDebug.generate'
|
||||
|
||||
const InstructionEditor: FC<Props> = ({
|
||||
editorKey,
|
||||
generatorType,
|
||||
value,
|
||||
onChange,
|
||||
availableVars,
|
||||
availableNodes,
|
||||
getVarType = () => Type.string,
|
||||
isShowCurrentBlock,
|
||||
isShowLastRunBlock,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
|
||||
const isCode = generatorType === 'code'
|
||||
const placeholder = isCode ? <div className='system-sm-regular whitespace-break-spaces !leading-6 text-text-placeholder'>
|
||||
{t(`${i18nPrefix}.codeGenInstructionPlaceHolderLine`)}
|
||||
</div> : (
|
||||
<div className='system-sm-regular text-text-placeholder'>
|
||||
<div className='leading-6'>{t(`${i18nPrefix}.instructionPlaceHolderTitle`)}</div>
|
||||
<div className='mt-2'>
|
||||
<div>{t(`${i18nPrefix}.instructionPlaceHolderLine1`)}</div>
|
||||
<div>{t(`${i18nPrefix}.instructionPlaceHolderLine2`)}</div>
|
||||
<div>{t(`${i18nPrefix}.instructionPlaceHolderLine3`)}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
const handleInsertVariable = () => {
|
||||
eventEmitter?.emit({ type: PROMPT_EDITOR_INSERT_QUICKLY, instanceId: editorKey } as any)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='relative'>
|
||||
<PromptEditor
|
||||
wrapperClassName='border !border-components-input-bg-normal bg-components-input-bg-normal hover:!border-components-input-bg-hover rounded-[10px] px-4 pt-3'
|
||||
key={editorKey}
|
||||
instanceId={editorKey}
|
||||
placeholder={placeholder}
|
||||
placeholderClassName='px-4 pt-3'
|
||||
className={cn('min-h-[240px] pb-8')}
|
||||
value={value}
|
||||
workflowVariableBlock={{
|
||||
show: true,
|
||||
variables: availableVars,
|
||||
getVarType,
|
||||
workflowNodesMap: availableNodes.reduce((acc, node) => {
|
||||
acc[node.id] = {
|
||||
title: node.data.title,
|
||||
type: node.data.type,
|
||||
width: node.width,
|
||||
height: node.height,
|
||||
position: node.position,
|
||||
}
|
||||
if (node.data.type === BlockEnum.Start) {
|
||||
acc.sys = {
|
||||
title: t('workflow.blocks.start'),
|
||||
type: BlockEnum.Start,
|
||||
}
|
||||
}
|
||||
return acc
|
||||
}, {} as any),
|
||||
}}
|
||||
currentBlock={{
|
||||
show: isShowCurrentBlock,
|
||||
generatorType,
|
||||
}}
|
||||
errorMessageBlock={{
|
||||
show: isCode,
|
||||
}}
|
||||
lastRunBlock={{
|
||||
show: isShowLastRunBlock,
|
||||
}}
|
||||
onChange={onChange}
|
||||
editable
|
||||
isSupportFileVar={false}
|
||||
/>
|
||||
<div className='system-xs-regular absolute bottom-0 left-4 flex h-8 items-center space-x-0.5 text-components-input-text-placeholder'>
|
||||
<span>{t('appDebug.generate.press')}</span>
|
||||
<span className='system-kbd flex h-4 w-3.5 items-center justify-center rounded-[4px] bg-components-kbd-bg-gray text-text-placeholder'>/</span>
|
||||
<span>{t('appDebug.generate.to')}</span>
|
||||
<span onClick={handleInsertVariable} className='!ml-1 cursor-pointer hover:border-b hover:border-dotted hover:border-text-tertiary hover:text-text-tertiary'>{t('appDebug.generate.insertContext')}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(InstructionEditor)
|
||||
@ -0,0 +1,55 @@
|
||||
'use client'
|
||||
import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import PromptRes from './prompt-res'
|
||||
import { Type } from '@/app/components/workflow/nodes/llm/types'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type Props = {
|
||||
value: string
|
||||
nodeId: string
|
||||
}
|
||||
|
||||
const PromptResInWorkflow: FC<Props> = ({
|
||||
value,
|
||||
nodeId,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
availableVars,
|
||||
availableNodes,
|
||||
} = useAvailableVarList(nodeId, {
|
||||
onlyLeafNodeVar: false,
|
||||
filterVar: _payload => true,
|
||||
})
|
||||
return (
|
||||
<PromptRes
|
||||
value={value}
|
||||
workflowVariableBlock={{
|
||||
show: true,
|
||||
variables: availableVars || [],
|
||||
getVarType: () => Type.string,
|
||||
workflowNodesMap: availableNodes.reduce((acc, node) => {
|
||||
acc[node.id] = {
|
||||
title: node.data.title,
|
||||
type: node.data.type,
|
||||
width: node.width,
|
||||
height: node.height,
|
||||
position: node.position,
|
||||
}
|
||||
if (node.data.type === BlockEnum.Start) {
|
||||
acc.sys = {
|
||||
title: t('workflow.blocks.start'),
|
||||
type: BlockEnum.Start,
|
||||
}
|
||||
}
|
||||
return acc
|
||||
}, {} as any),
|
||||
}}
|
||||
>
|
||||
</PromptRes>
|
||||
)
|
||||
}
|
||||
export default React.memo(PromptResInWorkflow)
|
||||
@ -0,0 +1,31 @@
|
||||
'use client'
|
||||
import PromptEditor from '@/app/components/base/prompt-editor'
|
||||
import type { WorkflowVariableBlockType } from '@/app/components/base/prompt-editor/types'
|
||||
import type { FC } from 'react'
|
||||
import React, { useEffect } from 'react'
|
||||
|
||||
type Props = {
|
||||
value: string
|
||||
workflowVariableBlock: WorkflowVariableBlockType
|
||||
}
|
||||
|
||||
const keyIdPrefix = 'prompt-res-editor'
|
||||
const PromptRes: FC<Props> = ({
|
||||
value,
|
||||
workflowVariableBlock,
|
||||
}) => {
|
||||
const [editorKey, setEditorKey] = React.useState<string>('keyIdPrefix-0')
|
||||
useEffect(() => {
|
||||
setEditorKey(`${keyIdPrefix}-${Date.now()}`)
|
||||
}, [value])
|
||||
return (
|
||||
<PromptEditor
|
||||
key={editorKey}
|
||||
value={value}
|
||||
editable={false}
|
||||
className='h-full bg-transparent pt-0'
|
||||
workflowVariableBlock={workflowVariableBlock}
|
||||
/>
|
||||
)
|
||||
}
|
||||
export default React.memo(PromptRes)
|
||||
@ -0,0 +1,54 @@
|
||||
import { RiArrowDownSLine, RiSparklingFill } from '@remixicon/react'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import React from 'react'
|
||||
import cn from '@/utils/classnames'
|
||||
import { Markdown } from '@/app/components/base/markdown'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import s from './style.module.css'
|
||||
|
||||
type Props = {
|
||||
message: string
|
||||
className?: string
|
||||
}
|
||||
const PromptToast = ({
|
||||
message,
|
||||
className,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const [isFold, {
|
||||
toggle: toggleFold,
|
||||
}] = useBoolean(false)
|
||||
// const message = `
|
||||
// list1list1list1list1list1list1list1list1list1list1list1list1list1list1list1list1list1list1list1list1list1list1list1list1list1list1list1list1list1list1
|
||||
// # h1
|
||||
// **strong text** ~~strikethrough~~
|
||||
|
||||
// * list1list1list1list1list1list1list1list1list1list1list1list1list1list1list1
|
||||
// * list2
|
||||
|
||||
// xxxx
|
||||
|
||||
// ## h2
|
||||
// \`\`\`python
|
||||
// print('Hello, World!')
|
||||
// \`\`\`
|
||||
// `
|
||||
return (
|
||||
<div className={cn('rounded-xl border-[0.5px] border-components-panel-border bg-background-section-burn pl-4 shadow-xs', className)}>
|
||||
<div className='my-3 flex h-4 items-center justify-between pr-3'>
|
||||
<div className='flex items-center space-x-1'>
|
||||
<RiSparklingFill className='size-3.5 text-components-input-border-active-prompt-1' />
|
||||
<span className={cn(s.optimizationNoteText, 'system-xs-semibold-uppercase')}>{t('appDebug.generate.optimizationNote')}</span>
|
||||
</div>
|
||||
<RiArrowDownSLine className={cn('size-4 cursor-pointer text-text-tertiary', isFold && 'rotate-[-90deg]')} onClick={toggleFold} />
|
||||
</div>
|
||||
{!isFold && (
|
||||
<div className='pb-4 pr-4'>
|
||||
<Markdown className="!text-sm" content={message} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PromptToast
|
||||
@ -0,0 +1,18 @@
|
||||
'use client'
|
||||
import { Generator } from '@/app/components/base/icons/src/vender/other'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const ResPlaceholder: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div className='flex h-full w-0 grow flex-col items-center justify-center space-y-3 px-8'>
|
||||
<Generator className='size-8 text-text-quaternary' />
|
||||
<div className='text-center text-[13px] font-normal leading-5 text-text-tertiary'>
|
||||
<div>{t('appDebug.generate.newNoDataLine1')}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(ResPlaceholder)
|
||||
@ -0,0 +1,97 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { GeneratorType } from './types'
|
||||
import PromptToast from './prompt-toast'
|
||||
import Button from '@/app/components/base/button'
|
||||
import VersionSelector from './version-selector'
|
||||
import type { GenRes } from '@/service/debug'
|
||||
import { RiClipboardLine } from '@remixicon/react'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import CodeEditor from '@/app/components/workflow/nodes/llm/components/json-schema-config-modal/code-editor'
|
||||
import PromptRes from './prompt-res'
|
||||
import PromptResInWorkflow from './prompt-res-in-workflow'
|
||||
|
||||
type Props = {
|
||||
isBasicMode?: boolean
|
||||
nodeId?: string
|
||||
current: GenRes
|
||||
currentVersionIndex: number
|
||||
setCurrentVersionIndex: (index: number) => void
|
||||
versions: GenRes[]
|
||||
onApply: () => void
|
||||
generatorType: GeneratorType
|
||||
}
|
||||
|
||||
const Result: FC<Props> = ({
|
||||
isBasicMode,
|
||||
nodeId,
|
||||
current,
|
||||
currentVersionIndex,
|
||||
setCurrentVersionIndex,
|
||||
versions,
|
||||
onApply,
|
||||
generatorType,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const isGeneratorPrompt = generatorType === GeneratorType.prompt
|
||||
|
||||
return (
|
||||
<div className='flex h-full flex-col'>
|
||||
<div className='mb-3 flex shrink-0 items-center justify-between'>
|
||||
<div>
|
||||
<div className='shrink-0 text-base font-semibold leading-[160%] text-text-secondary'>{t('appDebug.generate.resTitle')}</div>
|
||||
<VersionSelector
|
||||
versionLen={versions.length}
|
||||
value={currentVersionIndex}
|
||||
onChange={setCurrentVersionIndex}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex items-center space-x-2'>
|
||||
<Button className='px-2' onClick={() => {
|
||||
copy(current.modified)
|
||||
Toast.notify({ type: 'success', message: t('common.actionMsg.copySuccessfully') })
|
||||
}}>
|
||||
<RiClipboardLine className='h-4 w-4 text-text-secondary' />
|
||||
</Button>
|
||||
<Button variant='primary' onClick={onApply}>
|
||||
{t('appDebug.generate.apply')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex grow flex-col overflow-y-auto'>
|
||||
{
|
||||
current?.message && (
|
||||
<PromptToast message={current.message} className='mb-3 shrink-0' />
|
||||
)
|
||||
}
|
||||
<div className='grow pb-6'>
|
||||
{isGeneratorPrompt ? (
|
||||
isBasicMode ? (
|
||||
<PromptRes
|
||||
value={current?.modified}
|
||||
workflowVariableBlock={{
|
||||
show: false,
|
||||
}}
|
||||
/>
|
||||
) : (<PromptResInWorkflow
|
||||
value={current?.modified || ''}
|
||||
nodeId={nodeId!}
|
||||
/>)
|
||||
) : (
|
||||
<CodeEditor
|
||||
editorWrapperClassName='h-full'
|
||||
className='bg-transparent pt-0'
|
||||
value={current?.modified}
|
||||
readOnly
|
||||
hideTopMenu
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(Result)
|
||||
@ -3,5 +3,11 @@
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.optimizationNoteText {
|
||||
background: linear-gradient(263deg, rgba(21, 90, 239, 0.95) -20.92%, rgba(11, 165, 236, 0.95) 87.04%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
@ -0,0 +1,4 @@
|
||||
export enum GeneratorType {
|
||||
prompt = 'prompt',
|
||||
code = 'code',
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
import type { GenRes } from '@/service/debug'
|
||||
import { useSessionStorageState } from 'ahooks'
|
||||
import { useCallback } from 'react'
|
||||
|
||||
type Params = {
|
||||
storageKey: string
|
||||
}
|
||||
const keyPrefix = 'gen-data-'
|
||||
const useGenData = ({ storageKey }: Params) => {
|
||||
const [versions, setVersions] = useSessionStorageState<GenRes[]>(`${keyPrefix}${storageKey}-versions`, {
|
||||
defaultValue: [],
|
||||
})
|
||||
|
||||
const [currentVersionIndex, setCurrentVersionIndex] = useSessionStorageState<number>(`${keyPrefix}${storageKey}-version-index`, {
|
||||
defaultValue: 0,
|
||||
})
|
||||
|
||||
const current = versions?.[currentVersionIndex || 0]
|
||||
|
||||
const addVersion = useCallback((version: GenRes) => {
|
||||
setCurrentVersionIndex(() => versions?.length || 0)
|
||||
setVersions((prev) => {
|
||||
return [...prev!, version]
|
||||
})
|
||||
}, [setVersions, setCurrentVersionIndex, versions?.length])
|
||||
|
||||
return {
|
||||
versions,
|
||||
addVersion,
|
||||
currentVersionIndex,
|
||||
setCurrentVersionIndex,
|
||||
current,
|
||||
}
|
||||
}
|
||||
|
||||
export default useGenData
|
||||
@ -0,0 +1,103 @@
|
||||
import React, { useCallback } from 'react'
|
||||
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import cn from '@/utils/classnames'
|
||||
import { RiArrowDownSLine, RiCheckLine } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type Option = {
|
||||
label: string
|
||||
value: number
|
||||
}
|
||||
|
||||
type VersionSelectorProps = {
|
||||
versionLen: number;
|
||||
value: number;
|
||||
onChange: (index: number) => void;
|
||||
}
|
||||
|
||||
const VersionSelector: React.FC<VersionSelectorProps> = ({ versionLen, value, onChange }) => {
|
||||
const { t } = useTranslation()
|
||||
const [isOpen, {
|
||||
setFalse: handleOpenFalse,
|
||||
toggle: handleOpenToggle,
|
||||
set: handleOpenSet,
|
||||
}] = useBoolean(false)
|
||||
|
||||
const moreThanOneVersion = versionLen > 1
|
||||
const handleOpen = useCallback((value: boolean) => {
|
||||
if (moreThanOneVersion)
|
||||
handleOpenSet(value)
|
||||
}, [moreThanOneVersion, handleOpenToggle])
|
||||
const handleToggle = useCallback(() => {
|
||||
if (moreThanOneVersion)
|
||||
handleOpenToggle()
|
||||
}, [moreThanOneVersion, handleOpenToggle])
|
||||
|
||||
const versions = Array.from({ length: versionLen }, (_, index) => ({
|
||||
label: `${t('appDebug.generate.version')} ${index + 1}${index === versionLen - 1 ? ` · ${t('appDebug.generate.latest')}` : ''}`,
|
||||
value: index,
|
||||
}))
|
||||
|
||||
const isLatest = value === versionLen - 1
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
placement={'bottom-start'}
|
||||
offset={{
|
||||
mainAxis: 4,
|
||||
crossAxis: -12,
|
||||
}}
|
||||
open={isOpen}
|
||||
onOpenChange={handleOpen}
|
||||
>
|
||||
<PortalToFollowElemTrigger
|
||||
onClick={handleToggle}
|
||||
asChild
|
||||
>
|
||||
|
||||
<div className={cn('system-xs-medium flex items-center text-text-tertiary', isOpen && 'text-text-secondary', moreThanOneVersion && 'cursor-pointer')}>
|
||||
<div>{t('appDebug.generate.version')} {value + 1}{isLatest && ` · ${t('appDebug.generate.latest')}`}</div>
|
||||
{moreThanOneVersion && <RiArrowDownSLine className='size-3 ' />}
|
||||
</div>
|
||||
</PortalToFollowElemTrigger >
|
||||
<PortalToFollowElemContent className={cn(
|
||||
'z-[99]',
|
||||
)}>
|
||||
<div
|
||||
className={cn(
|
||||
'w-[208px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg',
|
||||
)}
|
||||
>
|
||||
<div className={cn('system-xs-medium-uppercase flex h-[22px] items-center px-3 pl-3 text-text-tertiary')}>
|
||||
{t('appDebug.generate.versions')}
|
||||
</div>
|
||||
{
|
||||
versions.map(option => (
|
||||
<div
|
||||
key={option.value}
|
||||
className={cn(
|
||||
'system-sm-medium flex h-7 cursor-pointer items-center rounded-lg px-2 text-text-secondary hover:bg-state-base-hover',
|
||||
)}
|
||||
title={option.label}
|
||||
onClick={() => {
|
||||
onChange(option.value)
|
||||
handleOpenFalse()
|
||||
}}
|
||||
>
|
||||
<div className='mr-1 grow truncate px-1 pl-1'>
|
||||
{option.label}
|
||||
</div>
|
||||
{
|
||||
value === option.value && <RiCheckLine className='h-4 w-4 shrink-0 text-text-accent' />
|
||||
}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem >
|
||||
)
|
||||
}
|
||||
|
||||
export default VersionSelector
|
||||
@ -1,16 +1,13 @@
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback, useEffect } from 'react'
|
||||
import cn from 'classnames'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import useBoolean from 'ahooks/lib/useBoolean'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ConfigPrompt from '../../config-prompt'
|
||||
import { languageMap } from '../../../../workflow/nodes/_base/components/editor/code-editor/index'
|
||||
import { generateRuleCode } from '@/service/debug'
|
||||
import type { CodeGenRes } from '@/service/debug'
|
||||
import { generateRule } from '@/service/debug'
|
||||
import type { GenRes } from '@/service/debug'
|
||||
import type { ModelModeType } from '@/types/app'
|
||||
import type { AppType, CompletionParams, Model } from '@/types/app'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { Generator } from '@/app/components/base/icons/src/vender/other'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
@ -21,17 +18,33 @@ import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/com
|
||||
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal'
|
||||
import type { FormValue } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import IdeaOutput from '../automatic/idea-output'
|
||||
import { GeneratorType } from '../automatic/types'
|
||||
import InstructionEditor from '../automatic/instruction-editor-in-workflow'
|
||||
import useGenData from '../automatic/use-gen-data'
|
||||
import Result from '../automatic/result'
|
||||
import ResPlaceholder from '../automatic/res-placeholder'
|
||||
import { useGenerateRuleTemplate } from '@/service/use-apps'
|
||||
import { useSessionStorageState } from 'ahooks'
|
||||
import s from '../automatic/style.module.css'
|
||||
|
||||
const i18nPrefix = 'appDebug.generate'
|
||||
export type IGetCodeGeneratorResProps = {
|
||||
flowId: string
|
||||
nodeId: string
|
||||
currentCode?: string
|
||||
mode: AppType
|
||||
isShow: boolean
|
||||
codeLanguages: CodeLanguage
|
||||
onClose: () => void
|
||||
onFinished: (res: CodeGenRes) => void
|
||||
onFinished: (res: GenRes) => void
|
||||
}
|
||||
|
||||
export const GetCodeGeneratorResModal: FC<IGetCodeGeneratorResProps> = (
|
||||
{
|
||||
flowId,
|
||||
nodeId,
|
||||
currentCode,
|
||||
mode,
|
||||
isShow,
|
||||
codeLanguages,
|
||||
@ -61,9 +74,25 @@ export const GetCodeGeneratorResModal: FC<IGetCodeGeneratorResProps> = (
|
||||
const {
|
||||
defaultModel,
|
||||
} = useModelListAndDefaultModelAndCurrentProviderAndModel(ModelTypeEnum.textGeneration)
|
||||
const [instruction, setInstruction] = React.useState<string>('')
|
||||
const [instructionFromSessionStorage, setInstruction] = useSessionStorageState<string>(`improve-instruction-${flowId}-${nodeId}`)
|
||||
const instruction = instructionFromSessionStorage || ''
|
||||
|
||||
const [ideaOutput, setIdeaOutput] = useState<string>('')
|
||||
|
||||
const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false)
|
||||
const [res, setRes] = React.useState<CodeGenRes | null>(null)
|
||||
const storageKey = `${flowId}-${nodeId}`
|
||||
const { addVersion, current, currentVersionIndex, setCurrentVersionIndex, versions } = useGenData({
|
||||
storageKey,
|
||||
})
|
||||
const [editorKey, setEditorKey] = useState(`${flowId}-0`)
|
||||
const { data: instructionTemplate } = useGenerateRuleTemplate(GeneratorType.code)
|
||||
useEffect(() => {
|
||||
if (!instruction && instructionTemplate)
|
||||
setInstruction(instructionTemplate.data)
|
||||
|
||||
setEditorKey(`${flowId}-${Date.now()}`)
|
||||
}, [instructionTemplate])
|
||||
|
||||
const isValid = () => {
|
||||
if (instruction.trim() === '') {
|
||||
Toast.notify({
|
||||
@ -97,7 +126,6 @@ export const GetCodeGeneratorResModal: FC<IGetCodeGeneratorResProps> = (
|
||||
localStorage.setItem('auto-gen-model', JSON.stringify(newModel))
|
||||
}, [model, setModel])
|
||||
|
||||
const isInLLMNode = true
|
||||
const onGenerate = async () => {
|
||||
if (!isValid())
|
||||
return
|
||||
@ -105,25 +133,37 @@ export const GetCodeGeneratorResModal: FC<IGetCodeGeneratorResProps> = (
|
||||
return
|
||||
setLoadingTrue()
|
||||
try {
|
||||
const { error, ...res } = await generateRuleCode({
|
||||
const { error, ...res } = await generateRule({
|
||||
flow_id: flowId,
|
||||
node_id: nodeId,
|
||||
current: currentCode,
|
||||
instruction,
|
||||
model_config: model,
|
||||
no_variable: !!isInLLMNode,
|
||||
code_language: languageMap[codeLanguages] || 'javascript',
|
||||
ideal_output: ideaOutput,
|
||||
language: languageMap[codeLanguages] || 'javascript',
|
||||
})
|
||||
setRes(res)
|
||||
if(!currentCode)
|
||||
res.modified = (res as any).code
|
||||
|
||||
if (error) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: error,
|
||||
})
|
||||
}
|
||||
else {
|
||||
addVersion(res)
|
||||
}
|
||||
}
|
||||
finally {
|
||||
setLoadingFalse()
|
||||
}
|
||||
}
|
||||
const [showConfirmOverwrite, setShowConfirmOverwrite] = React.useState(false)
|
||||
|
||||
const [isShowConfirmOverwrite, {
|
||||
setTrue: showConfirmOverwrite,
|
||||
setFalse: hideShowConfirmOverwrite,
|
||||
}] = useBoolean(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (defaultModel) {
|
||||
@ -155,30 +195,20 @@ export const GetCodeGeneratorResModal: FC<IGetCodeGeneratorResProps> = (
|
||||
<div className='text-[13px] text-text-tertiary'>{t('appDebug.codegen.loading')}</div>
|
||||
</div>
|
||||
)
|
||||
const renderNoData = (
|
||||
<div className='flex h-full w-0 grow flex-col items-center justify-center space-y-3 px-8'>
|
||||
<Generator className='h-14 w-14 text-text-tertiary' />
|
||||
<div className='text-center text-[13px] font-normal leading-5 text-text-tertiary'>
|
||||
<div>{t('appDebug.codegen.noDataLine1')}</div>
|
||||
<div>{t('appDebug.codegen.noDataLine2')}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isShow={isShow}
|
||||
onClose={onClose}
|
||||
className='min-w-[1140px] !p-0'
|
||||
closable
|
||||
>
|
||||
<div className='relative flex h-[680px] flex-wrap'>
|
||||
<div className='h-full w-[570px] shrink-0 overflow-y-auto border-r border-divider-regular p-8'>
|
||||
<div className='mb-8'>
|
||||
<div className={'text-lg font-bold leading-[28px] text-text-primary'}>{t('appDebug.codegen.title')}</div>
|
||||
<div className='h-full w-[570px] shrink-0 overflow-y-auto border-r border-divider-regular p-6'>
|
||||
<div className='mb-5'>
|
||||
<div className={`text-lg font-bold leading-[28px] ${s.textGradient}`}>{t('appDebug.codegen.title')}</div>
|
||||
<div className='mt-1 text-[13px] font-normal text-text-tertiary'>{t('appDebug.codegen.description')}</div>
|
||||
</div>
|
||||
<div className='mb-8'>
|
||||
<div className='mb-4'>
|
||||
<ModelParameterModal
|
||||
popupClassName='!w-[520px]'
|
||||
portalToFollowElemContentClassName='z-[1000]'
|
||||
@ -194,84 +224,60 @@ export const GetCodeGeneratorResModal: FC<IGetCodeGeneratorResProps> = (
|
||||
</div>
|
||||
<div>
|
||||
<div className='text-[0px]'>
|
||||
<div className='mb-2 text-sm font-medium leading-5 text-text-primary'>{t('appDebug.codegen.instruction')}</div>
|
||||
<Textarea
|
||||
className="h-[200px] resize-none"
|
||||
placeholder={t('appDebug.codegen.instructionPlaceholder') || ''}
|
||||
<div className='system-sm-semibold-uppercase mb-1.5 text-text-secondary'>{t('appDebug.codegen.instruction')}</div>
|
||||
<InstructionEditor
|
||||
editorKey={editorKey}
|
||||
value={instruction}
|
||||
onChange={e => setInstruction(e.target.value)}
|
||||
onChange={setInstruction}
|
||||
nodeId={nodeId}
|
||||
generatorType={GeneratorType.code}
|
||||
isShowCurrentBlock={!!currentCode}
|
||||
/>
|
||||
</div>
|
||||
<IdeaOutput
|
||||
value={ideaOutput}
|
||||
onChange={setIdeaOutput}
|
||||
/>
|
||||
|
||||
<div className='mt-5 flex justify-end'>
|
||||
<div className='mt-7 flex justify-end space-x-2'>
|
||||
<Button onClick={onClose}>{t(`${i18nPrefix}.dismiss`)}</Button>
|
||||
<Button
|
||||
className='flex space-x-1'
|
||||
variant='primary'
|
||||
onClick={onGenerate}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<Generator className='h-4 w-4 text-white' />
|
||||
<span className='text-xs font-semibold text-white'>{t('appDebug.codegen.generate')}</span>
|
||||
<Generator className='h-4 w-4' />
|
||||
<span className='text-xs font-semibold '>{t('appDebug.codegen.generate')}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{isLoading && renderLoading}
|
||||
{!isLoading && !res && renderNoData}
|
||||
{(!isLoading && res) && (
|
||||
<div className='h-full w-0 grow p-6 pb-0'>
|
||||
<div className='mb-3 shrink-0 text-base font-semibold leading-[160%] text-text-secondary'>{t('appDebug.codegen.resTitle')}</div>
|
||||
<div className={cn('max-h-[555px] overflow-y-auto', !isInLLMNode && 'pb-2')}>
|
||||
<ConfigPrompt
|
||||
mode={mode}
|
||||
promptTemplate={res?.code || ''}
|
||||
promptVariables={[]}
|
||||
readonly
|
||||
noTitle={isInLLMNode}
|
||||
gradientBorder
|
||||
editorHeight={isInLLMNode ? 524 : 0}
|
||||
noResize={isInLLMNode}
|
||||
/>
|
||||
{!isInLLMNode && (
|
||||
<>
|
||||
{res?.code && (
|
||||
<div className='mt-4'>
|
||||
<h3 className='mb-2 text-sm font-medium text-text-primary'>{t('appDebug.codegen.generatedCode')}</h3>
|
||||
<pre className='overflow-x-auto rounded-lg bg-gray-50 p-4'>
|
||||
<code className={`language-${res.language}`}>
|
||||
{res.code}
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
{res?.error && (
|
||||
<div className='mt-4 rounded-lg bg-red-50 p-4'>
|
||||
<p className='text-sm text-red-600'>{res.error}</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className='flex justify-end bg-background-default py-4'>
|
||||
<Button onClick={onClose}>{t('common.operation.cancel')}</Button>
|
||||
<Button variant='primary' className='ml-2' onClick={() => {
|
||||
setShowConfirmOverwrite(true)
|
||||
}}>{t('appDebug.codegen.apply')}</Button>
|
||||
</div>
|
||||
{!isLoading && !current && <ResPlaceholder />}
|
||||
{(!isLoading && current) && (
|
||||
<div className='h-full w-0 grow bg-background-default-subtle p-6 pb-0'>
|
||||
<Result
|
||||
current={current!}
|
||||
currentVersionIndex={currentVersionIndex || 0}
|
||||
setCurrentVersionIndex={setCurrentVersionIndex}
|
||||
versions={versions || []}
|
||||
onApply={showConfirmOverwrite}
|
||||
generatorType={GeneratorType.code}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{showConfirmOverwrite && (
|
||||
{isShowConfirmOverwrite && (
|
||||
<Confirm
|
||||
title={t('appDebug.codegen.overwriteConfirmTitle')}
|
||||
content={t('appDebug.codegen.overwriteConfirmMessage')}
|
||||
isShow={showConfirmOverwrite}
|
||||
isShow
|
||||
onConfirm={() => {
|
||||
setShowConfirmOverwrite(false)
|
||||
onFinished(res!)
|
||||
hideShowConfirmOverwrite()
|
||||
onFinished(current!)
|
||||
}}
|
||||
onCancel={() => setShowConfirmOverwrite(false)}
|
||||
onCancel={hideShowConfirmOverwrite}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
@ -1,43 +0,0 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import GroupName from '../../base/group-name'
|
||||
import TextToSpeech from '../chat-group/text-to-speech'
|
||||
import MoreLikeThis from './more-like-this'
|
||||
|
||||
/*
|
||||
* Include
|
||||
* 1. More like this
|
||||
*/
|
||||
|
||||
type ExperienceGroupProps = {
|
||||
isShowTextToSpeech: boolean
|
||||
isShowMoreLike: boolean
|
||||
}
|
||||
|
||||
const ExperienceEnhanceGroup: FC<ExperienceGroupProps> = ({
|
||||
isShowTextToSpeech,
|
||||
isShowMoreLike,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className='mt-7'>
|
||||
<GroupName name={t('appDebug.feature.groupExperience.title')}/>
|
||||
<div className='space-y-3'>
|
||||
{
|
||||
isShowMoreLike && (
|
||||
<MoreLikeThis/>
|
||||
)
|
||||
}
|
||||
{
|
||||
isShowTextToSpeech && (
|
||||
<TextToSpeech/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(ExperienceEnhanceGroup)
|
||||
@ -1,51 +0,0 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { XMarkIcon } from '@heroicons/react/24/outline'
|
||||
import { useLocalStorageState } from 'ahooks'
|
||||
import MoreLikeThisIcon from '../../../base/icons/more-like-this-icon'
|
||||
import Panel from '@/app/components/app/configuration/base/feature-panel'
|
||||
|
||||
const GENERATE_NUM = 1
|
||||
|
||||
const warningIcon = (
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M6.40616 0.834307C6.14751 0.719294 5.85222 0.719294 5.59356 0.834307C5.3938 0.923133 5.26403 1.07959 5.17373 1.20708C5.08495 1.33242 4.9899 1.49664 4.88536 1.67723L0.751783 8.81705C0.646828 8.9983 0.551451 9.16302 0.486781 9.3028C0.421056 9.44487 0.349754 9.63584 0.372478 9.85381C0.401884 10.1359 0.549654 10.3922 0.779012 10.5589C0.956259 10.6878 1.15726 10.7218 1.31314 10.7361C1.46651 10.7501 1.65684 10.7501 1.86628 10.7501H10.1334C10.3429 10.7501 10.5332 10.7501 10.6866 10.7361C10.8425 10.7218 11.0435 10.6878 11.2207 10.5589C11.4501 10.3922 11.5978 10.1359 11.6272 9.85381C11.65 9.63584 11.5787 9.44487 11.5129 9.3028C11.4483 9.16303 11.3529 8.99833 11.248 8.81709L7.11436 1.67722C7.00983 1.49663 6.91477 1.33242 6.82599 1.20708C6.73569 1.07959 6.60593 0.923133 6.40616 0.834307ZM6.49988 4.50012C6.49988 4.22398 6.27602 4.00012 5.99988 4.00012C5.72374 4.00012 5.49988 4.22398 5.49988 4.50012V6.50012C5.49988 6.77626 5.72374 7.00012 5.99988 7.00012C6.27602 7.00012 6.49988 6.77626 6.49988 6.50012V4.50012ZM5.99988 8.00012C5.72374 8.00012 5.49988 8.22398 5.49988 8.50012C5.49988 8.77626 5.72374 9.00012 5.99988 9.00012H6.00488C6.28102 9.00012 6.50488 8.77626 6.50488 8.50012C6.50488 8.22398 6.28102 8.00012 6.00488 8.00012H5.99988Z" fill="#F79009" />
|
||||
</svg>
|
||||
|
||||
)
|
||||
const MoreLikeThis: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [isHideTip, setIsHideTip] = useLocalStorageState('isHideMoreLikeThisTip', {
|
||||
defaultValue: false,
|
||||
})
|
||||
|
||||
const headerRight = (
|
||||
<div className='text-xs text-gray-500'>{t('appDebug.feature.moreLikeThis.generateNumTip')} {GENERATE_NUM}</div>
|
||||
)
|
||||
return (
|
||||
<Panel
|
||||
className='mt-4'
|
||||
title={t('appDebug.feature.moreLikeThis.title')}
|
||||
headerIcon={<MoreLikeThisIcon />}
|
||||
headerRight={headerRight}
|
||||
noBodySpacing
|
||||
>
|
||||
{!isHideTip && (
|
||||
<div className='flex h-9 items-center justify-between rounded-b-xl bg-[#FFFAEB] px-3 text-xs text-gray-700'>
|
||||
<div className='flex items-center space-x-2'>
|
||||
<div>{warningIcon}</div>
|
||||
<div>{t('appDebug.feature.moreLikeThis.tip')}</div>
|
||||
</div>
|
||||
<div className='flex h-4 w-4 cursor-pointer items-center justify-center' onClick={() => setIsHideTip(true)}>
|
||||
<XMarkIcon className="h-3 w-3" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</Panel>
|
||||
)
|
||||
}
|
||||
export default React.memo(MoreLikeThis)
|
||||
@ -8,7 +8,6 @@ import {
|
||||
} from '@heroicons/react/24/outline'
|
||||
import { RiCloseLine, RiEditFill } from '@remixicon/react'
|
||||
import { get } from 'lodash-es'
|
||||
import InfiniteScroll from 'react-infinite-scroll-component'
|
||||
import dayjs from 'dayjs'
|
||||
import utc from 'dayjs/plugin/utc'
|
||||
import timezone from 'dayjs/plugin/timezone'
|
||||
@ -111,7 +110,8 @@ const statusTdRender = (statusCount: StatusCount) => {
|
||||
|
||||
const getFormattedChatList = (messages: ChatMessage[], conversationId: string, timezone: string, format: string) => {
|
||||
const newChatList: IChatItem[] = []
|
||||
messages.forEach((item: ChatMessage) => {
|
||||
try {
|
||||
messages.forEach((item: ChatMessage) => {
|
||||
const questionFiles = item.message_files?.filter((file: any) => file.belongs_to === 'user') || []
|
||||
newChatList.push({
|
||||
id: `question-${item.id}`,
|
||||
@ -178,7 +178,13 @@ const getFormattedChatList = (messages: ChatMessage[], conversationId: string, t
|
||||
parentMessageId: `question-${item.id}`,
|
||||
})
|
||||
})
|
||||
return newChatList
|
||||
|
||||
return newChatList
|
||||
}
|
||||
catch (error) {
|
||||
console.error('getFormattedChatList processing failed:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
type IDetailPanel = {
|
||||
@ -188,6 +194,9 @@ type IDetailPanel = {
|
||||
}
|
||||
|
||||
function DetailPanel({ detail, onFeedback }: IDetailPanel) {
|
||||
const MIN_ITEMS_FOR_SCROLL_LOADING = 8
|
||||
const SCROLL_THRESHOLD_PX = 50
|
||||
const SCROLL_DEBOUNCE_MS = 200
|
||||
const { userProfile: { timezone } } = useAppContext()
|
||||
const { formatTime } = useTimestamp()
|
||||
const { onClose, appDetail } = useContext(DrawerContext)
|
||||
@ -204,13 +213,19 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
|
||||
const { t } = useTranslation()
|
||||
const [hasMore, setHasMore] = useState(true)
|
||||
const [varValues, setVarValues] = useState<Record<string, string>>({})
|
||||
const isLoadingRef = useRef(false)
|
||||
|
||||
const [allChatItems, setAllChatItems] = useState<IChatItem[]>([])
|
||||
const [chatItemTree, setChatItemTree] = useState<ChatItemInTree[]>([])
|
||||
const [threadChatItems, setThreadChatItems] = useState<IChatItem[]>([])
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
if (isLoadingRef.current)
|
||||
return
|
||||
|
||||
try {
|
||||
isLoadingRef.current = true
|
||||
|
||||
if (!hasMore)
|
||||
return
|
||||
|
||||
@ -218,8 +233,11 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
|
||||
conversation_id: detail.id,
|
||||
limit: 10,
|
||||
}
|
||||
if (allChatItems[0]?.id)
|
||||
params.first_id = allChatItems[0]?.id.replace('question-', '')
|
||||
// Use the oldest answer item ID for pagination
|
||||
const answerItems = allChatItems.filter(item => item.isAnswer)
|
||||
const oldestAnswerItem = answerItems[answerItems.length - 1]
|
||||
if (oldestAnswerItem?.id)
|
||||
params.first_id = oldestAnswerItem.id
|
||||
const messageRes = await fetchChatMessages({
|
||||
url: `/apps/${appDetail?.id}/chat-messages`,
|
||||
params,
|
||||
@ -249,15 +267,20 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
|
||||
}
|
||||
setChatItemTree(tree)
|
||||
|
||||
setThreadChatItems(getThreadMessages(tree, newAllChatItems.at(-1)?.id))
|
||||
const lastMessageId = newAllChatItems.length > 0 ? newAllChatItems[newAllChatItems.length - 1].id : undefined
|
||||
setThreadChatItems(getThreadMessages(tree, lastMessageId))
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err)
|
||||
console.error('fetchData execution failed:', err)
|
||||
}
|
||||
finally {
|
||||
isLoadingRef.current = false
|
||||
}
|
||||
}, [allChatItems, detail.id, hasMore, timezone, t, appDetail, detail?.model_config?.configs?.introduction])
|
||||
|
||||
const switchSibling = useCallback((siblingMessageId: string) => {
|
||||
setThreadChatItems(getThreadMessages(chatItemTree, siblingMessageId))
|
||||
const newThreadChatItems = getThreadMessages(chatItemTree, siblingMessageId)
|
||||
setThreadChatItems(newThreadChatItems)
|
||||
}, [chatItemTree])
|
||||
|
||||
const handleAnnotationEdited = useCallback((query: string, answer: string, index: number) => {
|
||||
@ -344,13 +367,217 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
|
||||
|
||||
const fetchInitiated = useRef(false)
|
||||
|
||||
// Only load initial messages, don't auto-load more
|
||||
useEffect(() => {
|
||||
if (appDetail?.id && detail.id && appDetail?.mode !== 'completion' && !fetchInitiated.current) {
|
||||
// Mark as initialized, but don't auto-load more messages
|
||||
fetchInitiated.current = true
|
||||
// Still call fetchData to get initial messages
|
||||
fetchData()
|
||||
}
|
||||
}, [appDetail?.id, detail.id, appDetail?.mode, fetchData])
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
const loadMoreMessages = useCallback(async () => {
|
||||
if (isLoading || !hasMore || !appDetail?.id || !detail.id)
|
||||
return
|
||||
|
||||
setIsLoading(true)
|
||||
|
||||
try {
|
||||
const params: ChatMessagesRequest = {
|
||||
conversation_id: detail.id,
|
||||
limit: 10,
|
||||
}
|
||||
|
||||
// Use the earliest response item as the first_id
|
||||
const answerItems = allChatItems.filter(item => item.isAnswer)
|
||||
const oldestAnswerItem = answerItems[answerItems.length - 1]
|
||||
if (oldestAnswerItem?.id) {
|
||||
params.first_id = oldestAnswerItem.id
|
||||
}
|
||||
else if (allChatItems.length > 0 && allChatItems[0]?.id) {
|
||||
const firstId = allChatItems[0].id.replace('question-', '').replace('answer-', '')
|
||||
params.first_id = firstId
|
||||
}
|
||||
|
||||
const messageRes = await fetchChatMessages({
|
||||
url: `/apps/${appDetail.id}/chat-messages`,
|
||||
params,
|
||||
})
|
||||
|
||||
if (!messageRes.data || messageRes.data.length === 0) {
|
||||
setHasMore(false)
|
||||
return
|
||||
}
|
||||
|
||||
if (messageRes.data.length > 0) {
|
||||
const varValues = messageRes.data.at(-1)!.inputs
|
||||
setVarValues(varValues)
|
||||
}
|
||||
|
||||
setHasMore(messageRes.has_more)
|
||||
|
||||
const newItems = getFormattedChatList(
|
||||
messageRes.data,
|
||||
detail.id,
|
||||
timezone!,
|
||||
t('appLog.dateTimeFormat') as string,
|
||||
)
|
||||
|
||||
// Check for duplicate messages
|
||||
const existingIds = new Set(allChatItems.map(item => item.id))
|
||||
const uniqueNewItems = newItems.filter(item => !existingIds.has(item.id))
|
||||
|
||||
if (uniqueNewItems.length === 0) {
|
||||
if (allChatItems.length > 1) {
|
||||
const nextId = allChatItems[1].id.replace('question-', '').replace('answer-', '')
|
||||
|
||||
const retryParams = {
|
||||
...params,
|
||||
first_id: nextId,
|
||||
}
|
||||
|
||||
const retryRes = await fetchChatMessages({
|
||||
url: `/apps/${appDetail.id}/chat-messages`,
|
||||
params: retryParams,
|
||||
})
|
||||
|
||||
if (retryRes.data && retryRes.data.length > 0) {
|
||||
const retryItems = getFormattedChatList(
|
||||
retryRes.data,
|
||||
detail.id,
|
||||
timezone!,
|
||||
t('appLog.dateTimeFormat') as string,
|
||||
)
|
||||
|
||||
const retryUniqueItems = retryItems.filter(item => !existingIds.has(item.id))
|
||||
if (retryUniqueItems.length > 0) {
|
||||
const newAllChatItems = [
|
||||
...retryUniqueItems,
|
||||
...allChatItems,
|
||||
]
|
||||
|
||||
setAllChatItems(newAllChatItems)
|
||||
|
||||
let tree = buildChatItemTree(newAllChatItems)
|
||||
if (retryRes.has_more === false && detail?.model_config?.configs?.introduction) {
|
||||
tree = [{
|
||||
id: 'introduction',
|
||||
isAnswer: true,
|
||||
isOpeningStatement: true,
|
||||
content: detail?.model_config?.configs?.introduction ?? 'hello',
|
||||
feedbackDisabled: true,
|
||||
children: tree,
|
||||
}]
|
||||
}
|
||||
setChatItemTree(tree)
|
||||
setHasMore(retryRes.has_more)
|
||||
setThreadChatItems(getThreadMessages(tree, newAllChatItems.at(-1)?.id))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const newAllChatItems = [
|
||||
...uniqueNewItems,
|
||||
...allChatItems,
|
||||
]
|
||||
|
||||
setAllChatItems(newAllChatItems)
|
||||
|
||||
let tree = buildChatItemTree(newAllChatItems)
|
||||
if (messageRes.has_more === false && detail?.model_config?.configs?.introduction) {
|
||||
tree = [{
|
||||
id: 'introduction',
|
||||
isAnswer: true,
|
||||
isOpeningStatement: true,
|
||||
content: detail?.model_config?.configs?.introduction ?? 'hello',
|
||||
feedbackDisabled: true,
|
||||
children: tree,
|
||||
}]
|
||||
}
|
||||
setChatItemTree(tree)
|
||||
|
||||
setThreadChatItems(getThreadMessages(tree, newAllChatItems.at(-1)?.id))
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
setHasMore(false)
|
||||
}
|
||||
finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}, [allChatItems, detail.id, hasMore, isLoading, timezone, t, appDetail])
|
||||
|
||||
useEffect(() => {
|
||||
const scrollableDiv = document.getElementById('scrollableDiv')
|
||||
const outerDiv = scrollableDiv?.parentElement
|
||||
const chatContainer = document.querySelector('.mx-1.mb-1.grow.overflow-auto') as HTMLElement
|
||||
|
||||
let scrollContainer: HTMLElement | null = null
|
||||
|
||||
if (outerDiv && outerDiv.scrollHeight > outerDiv.clientHeight) {
|
||||
scrollContainer = outerDiv
|
||||
}
|
||||
else if (scrollableDiv && scrollableDiv.scrollHeight > scrollableDiv.clientHeight) {
|
||||
scrollContainer = scrollableDiv
|
||||
}
|
||||
else if (chatContainer && chatContainer.scrollHeight > chatContainer.clientHeight) {
|
||||
scrollContainer = chatContainer
|
||||
}
|
||||
else {
|
||||
const possibleContainers = document.querySelectorAll('.overflow-auto, .overflow-y-auto')
|
||||
for (let i = 0; i < possibleContainers.length; i++) {
|
||||
const container = possibleContainers[i] as HTMLElement
|
||||
if (container.scrollHeight > container.clientHeight) {
|
||||
scrollContainer = container
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!scrollContainer)
|
||||
return
|
||||
|
||||
let lastLoadTime = 0
|
||||
const throttleDelay = 200
|
||||
|
||||
const handleScroll = () => {
|
||||
const currentScrollTop = scrollContainer!.scrollTop
|
||||
const scrollHeight = scrollContainer!.scrollHeight
|
||||
const clientHeight = scrollContainer!.clientHeight
|
||||
|
||||
const distanceFromTop = currentScrollTop
|
||||
const distanceFromBottom = scrollHeight - currentScrollTop - clientHeight
|
||||
|
||||
const now = Date.now()
|
||||
|
||||
const isNearTop = distanceFromTop < 30
|
||||
// eslint-disable-next-line sonarjs/no-unused-vars
|
||||
const _distanceFromBottom = distanceFromBottom < 30
|
||||
if (isNearTop && hasMore && !isLoading && (now - lastLoadTime > throttleDelay)) {
|
||||
lastLoadTime = now
|
||||
loadMoreMessages()
|
||||
}
|
||||
}
|
||||
|
||||
scrollContainer.addEventListener('scroll', handleScroll, { passive: true })
|
||||
|
||||
const handleWheel = (e: WheelEvent) => {
|
||||
if (e.deltaY < 0)
|
||||
handleScroll()
|
||||
}
|
||||
scrollContainer.addEventListener('wheel', handleWheel, { passive: true })
|
||||
|
||||
return () => {
|
||||
scrollContainer!.removeEventListener('scroll', handleScroll)
|
||||
scrollContainer!.removeEventListener('wheel', handleWheel)
|
||||
}
|
||||
}, [hasMore, isLoading, loadMoreMessages])
|
||||
|
||||
const isChatMode = appDetail?.mode !== 'completion'
|
||||
const isAdvanced = appDetail?.mode === 'advanced-chat'
|
||||
|
||||
@ -378,6 +605,36 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
|
||||
return () => cancelAnimationFrame(raf)
|
||||
}, [])
|
||||
|
||||
// Add scroll listener to ensure loading is triggered
|
||||
useEffect(() => {
|
||||
if (threadChatItems.length >= MIN_ITEMS_FOR_SCROLL_LOADING && hasMore) {
|
||||
const scrollableDiv = document.getElementById('scrollableDiv')
|
||||
|
||||
if (scrollableDiv) {
|
||||
let loadingTimeout: NodeJS.Timeout | null = null
|
||||
|
||||
const handleScroll = () => {
|
||||
const { scrollTop } = scrollableDiv
|
||||
|
||||
// Trigger loading when scrolling near the top
|
||||
if (scrollTop < SCROLL_THRESHOLD_PX && !isLoadingRef.current) {
|
||||
if (loadingTimeout)
|
||||
clearTimeout(loadingTimeout)
|
||||
|
||||
loadingTimeout = setTimeout(fetchData, SCROLL_DEBOUNCE_MS) // 200ms debounce
|
||||
}
|
||||
}
|
||||
|
||||
scrollableDiv.addEventListener('scroll', handleScroll)
|
||||
return () => {
|
||||
scrollableDiv.removeEventListener('scroll', handleScroll)
|
||||
if (loadingTimeout)
|
||||
clearTimeout(loadingTimeout)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [threadChatItems.length, hasMore, fetchData])
|
||||
|
||||
return (
|
||||
<div ref={ref} className='flex h-full flex-col rounded-xl border-[0.5px] border-components-panel-border'>
|
||||
{/* Panel Header */}
|
||||
@ -439,8 +696,8 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
|
||||
siteInfo={null}
|
||||
/>
|
||||
</div>
|
||||
: threadChatItems.length < 8
|
||||
? <div className="mb-4 pt-4">
|
||||
: threadChatItems.length < MIN_ITEMS_FOR_SCROLL_LOADING ? (
|
||||
<div className="mb-4 pt-4">
|
||||
<Chat
|
||||
config={{
|
||||
appId: appDetail?.id,
|
||||
@ -466,35 +723,27 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
|
||||
switchSibling={switchSibling}
|
||||
/>
|
||||
</div>
|
||||
: <div
|
||||
) : (
|
||||
<div
|
||||
className="py-4"
|
||||
id="scrollableDiv"
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column-reverse',
|
||||
height: '100%',
|
||||
overflow: 'auto',
|
||||
}}>
|
||||
{/* Put the scroll bar always on the bottom */}
|
||||
<InfiniteScroll
|
||||
scrollableTarget="scrollableDiv"
|
||||
dataLength={threadChatItems.length}
|
||||
next={fetchData}
|
||||
hasMore={hasMore}
|
||||
loader={<div className='system-xs-regular text-center text-text-tertiary'>{t('appLog.detail.loading')}...</div>}
|
||||
// endMessage={<div className='text-center'>Nothing more to show</div>}
|
||||
// below props only if you need pull down functionality
|
||||
refreshFunction={fetchData}
|
||||
pullDownToRefresh
|
||||
pullDownToRefreshThreshold={50}
|
||||
// pullDownToRefreshContent={
|
||||
// <div className='text-center'>Pull down to refresh</div>
|
||||
// }
|
||||
// releaseToRefreshContent={
|
||||
// <div className='text-center'>Release to refresh</div>
|
||||
// }
|
||||
// To put endMessage and loader to the top.
|
||||
style={{ display: 'flex', flexDirection: 'column-reverse' }}
|
||||
inverse={true}
|
||||
>
|
||||
<div className="flex w-full flex-col-reverse" style={{ position: 'relative' }}>
|
||||
{/* Loading state indicator - only shown when loading */}
|
||||
{hasMore && isLoading && (
|
||||
<div className="sticky left-0 right-0 top-0 z-10 bg-primary-50/40 py-3 text-center">
|
||||
<div className='system-xs-regular text-text-tertiary'>
|
||||
{t('appLog.detail.loading')}...
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Chat
|
||||
config={{
|
||||
appId: appDetail?.id,
|
||||
@ -519,8 +768,9 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
|
||||
chatContainerInnerClassName='px-3'
|
||||
switchSibling={switchSibling}
|
||||
/>
|
||||
</InfiniteScroll>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{showMessageLogModal && (
|
||||
|
||||
@ -35,7 +35,7 @@ import type { AppDetailResponse } from '@/models/app'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import type { AppSSO } from '@/types/app'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import { fetchAppDetail } from '@/service/apps'
|
||||
import { fetchAppDetailDirect } from '@/service/apps'
|
||||
import { AccessMode } from '@/models/access-control'
|
||||
import AccessControl from '../app-access-control'
|
||||
import { useAppWhiteListSubjects } from '@/service/access-control'
|
||||
@ -161,11 +161,15 @@ function AppCard({
|
||||
return
|
||||
setShowAccessControl(true)
|
||||
}, [appDetail])
|
||||
const handleAccessControlUpdate = useCallback(() => {
|
||||
fetchAppDetail({ url: '/apps', id: appDetail!.id }).then((res) => {
|
||||
const handleAccessControlUpdate = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetchAppDetailDirect({ url: '/apps', id: appDetail!.id })
|
||||
setAppDetail(res)
|
||||
setShowAccessControl(false)
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Failed to fetch app detail:', error)
|
||||
}
|
||||
}, [appDetail, setAppDetail])
|
||||
|
||||
return (
|
||||
@ -123,7 +123,7 @@ const Chart: React.FC<IChartProps> = ({
|
||||
dimensions: ['date', yField],
|
||||
source: statistics,
|
||||
},
|
||||
grid: { top: 8, right: 36, bottom: 0, left: 0, containLabel: true },
|
||||
grid: { top: 8, right: 36, bottom: 10, left: 25, containLabel: true },
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
position: 'top',
|
||||
@ -165,7 +165,7 @@ const Chart: React.FC<IChartProps> = ({
|
||||
lineStyle: {
|
||||
color: COMMON_COLOR_MAP.splitLineDark,
|
||||
},
|
||||
interval(index, value) {
|
||||
interval(_index, value) {
|
||||
return !!value
|
||||
},
|
||||
},
|
||||
@ -242,7 +242,7 @@ const Chart: React.FC<IChartProps> = ({
|
||||
? ''
|
||||
: <span>{t('appOverview.analysis.tokenUsage.consumed')} Tokens<span className='text-sm'>
|
||||
<span className='ml-1 text-text-tertiary'>(</span>
|
||||
<span className='text-orange-400'>~{sum(statistics.map(item => Number.parseFloat(get(item, 'total_price', '0')))).toLocaleString('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 4 })}</span>
|
||||
<span className='text-orange-400'>~{sum(statistics.map(item => Number.parseFloat(String(get(item, 'total_price', '0'))))).toLocaleString('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 4 })}</span>
|
||||
<span className='text-text-tertiary'>)</span>
|
||||
</span></span>}
|
||||
textStyle={{ main: `!text-3xl !font-normal ${sumData === 0 ? '!text-text-quaternary' : ''}` }} />
|
||||
@ -268,7 +268,7 @@ export const MessagesChart: FC<IBizChartProps> = ({ id, period }) => {
|
||||
const noDataFlag = !response.data || response.data.length === 0
|
||||
return <Chart
|
||||
basicInfo={{ title: t('appOverview.analysis.totalMessages.title'), explanation: t('appOverview.analysis.totalMessages.explanation'), timePeriod: period.name }}
|
||||
chartData={!noDataFlag ? response : { data: getDefaultChartData(period.query ?? defaultPeriod) }}
|
||||
chartData={!noDataFlag ? response : { data: getDefaultChartData(period.query ?? defaultPeriod) } as any}
|
||||
chartType='messages'
|
||||
{...(noDataFlag && { yMax: 500 })}
|
||||
/>
|
||||
@ -282,7 +282,7 @@ export const ConversationsChart: FC<IBizChartProps> = ({ id, period }) => {
|
||||
const noDataFlag = !response.data || response.data.length === 0
|
||||
return <Chart
|
||||
basicInfo={{ title: t('appOverview.analysis.totalConversations.title'), explanation: t('appOverview.analysis.totalConversations.explanation'), timePeriod: period.name }}
|
||||
chartData={!noDataFlag ? response : { data: getDefaultChartData(period.query ?? defaultPeriod) }}
|
||||
chartData={!noDataFlag ? response : { data: getDefaultChartData(period.query ?? defaultPeriod) } as any}
|
||||
chartType='conversations'
|
||||
{...(noDataFlag && { yMax: 500 })}
|
||||
/>
|
||||
@ -297,7 +297,7 @@ export const EndUsersChart: FC<IBizChartProps> = ({ id, period }) => {
|
||||
const noDataFlag = !response.data || response.data.length === 0
|
||||
return <Chart
|
||||
basicInfo={{ title: t('appOverview.analysis.activeUsers.title'), explanation: t('appOverview.analysis.activeUsers.explanation'), timePeriod: period.name }}
|
||||
chartData={!noDataFlag ? response : { data: getDefaultChartData(period.query ?? defaultPeriod) }}
|
||||
chartData={!noDataFlag ? response : { data: getDefaultChartData(period.query ?? defaultPeriod) } as any}
|
||||
chartType='endUsers'
|
||||
{...(noDataFlag && { yMax: 500 })}
|
||||
/>
|
||||
@ -380,7 +380,7 @@ export const CostChart: FC<IBizChartProps> = ({ id, period }) => {
|
||||
const noDataFlag = !response.data || response.data.length === 0
|
||||
return <Chart
|
||||
basicInfo={{ title: t('appOverview.analysis.tokenUsage.title'), explanation: t('appOverview.analysis.tokenUsage.explanation'), timePeriod: period.name }}
|
||||
chartData={!noDataFlag ? response : { data: getDefaultChartData(period.query ?? defaultPeriod) }}
|
||||
chartData={!noDataFlag ? response : { data: getDefaultChartData(period.query ?? defaultPeriod) } as any}
|
||||
chartType='costs'
|
||||
{...(noDataFlag && { yMax: 100 })}
|
||||
/>
|
||||
@ -394,7 +394,7 @@ export const WorkflowMessagesChart: FC<IBizChartProps> = ({ id, period }) => {
|
||||
const noDataFlag = !response.data || response.data.length === 0
|
||||
return <Chart
|
||||
basicInfo={{ title: t('appOverview.analysis.totalMessages.title'), explanation: t('appOverview.analysis.totalMessages.explanation'), timePeriod: period.name }}
|
||||
chartData={!noDataFlag ? response : { data: getDefaultChartData({ ...(period.query ?? defaultPeriod), key: 'runs' }) }}
|
||||
chartData={!noDataFlag ? response : { data: getDefaultChartData({ ...(period.query ?? defaultPeriod), key: 'runs' }) } as any}
|
||||
chartType='conversations'
|
||||
valueKey='runs'
|
||||
{...(noDataFlag && { yMax: 500 })}
|
||||
@ -410,7 +410,7 @@ export const WorkflowDailyTerminalsChart: FC<IBizChartProps> = ({ id, period })
|
||||
const noDataFlag = !response.data || response.data.length === 0
|
||||
return <Chart
|
||||
basicInfo={{ title: t('appOverview.analysis.activeUsers.title'), explanation: t('appOverview.analysis.activeUsers.explanation'), timePeriod: period.name }}
|
||||
chartData={!noDataFlag ? response : { data: getDefaultChartData(period.query ?? defaultPeriod) }}
|
||||
chartData={!noDataFlag ? response : { data: getDefaultChartData(period.query ?? defaultPeriod) } as any}
|
||||
chartType='endUsers'
|
||||
{...(noDataFlag && { yMax: 500 })}
|
||||
/>
|
||||
@ -425,7 +425,7 @@ export const WorkflowCostChart: FC<IBizChartProps> = ({ id, period }) => {
|
||||
const noDataFlag = !response.data || response.data.length === 0
|
||||
return <Chart
|
||||
basicInfo={{ title: t('appOverview.analysis.tokenUsage.title'), explanation: t('appOverview.analysis.tokenUsage.explanation'), timePeriod: period.name }}
|
||||
chartData={!noDataFlag ? response : { data: getDefaultChartData(period.query ?? defaultPeriod) }}
|
||||
chartData={!noDataFlag ? response : { data: getDefaultChartData(period.query ?? defaultPeriod) } as any}
|
||||
chartType='workflowCosts'
|
||||
{...(noDataFlag && { yMax: 100 })}
|
||||
/>
|
||||
@ -18,3 +18,13 @@
|
||||
.pluginInstallIcon {
|
||||
background-image: url(../assets/chromeplugin-install.svg);
|
||||
}
|
||||
|
||||
:global(html[data-theme="dark"]) .iframeIcon,
|
||||
:global(html[data-theme="dark"]) .scriptsIcon,
|
||||
:global(html[data-theme="dark"]) .chromePluginIcon {
|
||||
filter: invert(0.86) hue-rotate(180deg) saturate(0.5) brightness(0.95);
|
||||
}
|
||||
|
||||
:global(html[data-theme="dark"]) .pluginInstallIcon {
|
||||
filter: invert(0.9);
|
||||
}
|
||||
|
||||
@ -407,8 +407,8 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
||||
}
|
||||
btnClassName={open =>
|
||||
cn(
|
||||
open ? '!bg-black/5 !shadow-none' : '!bg-transparent',
|
||||
'h-8 w-8 rounded-md border-none !p-2 hover:!bg-black/5',
|
||||
open ? '!bg-state-base-hover !shadow-none' : '!bg-transparent',
|
||||
'h-8 w-8 rounded-md border-none !p-2 hover:!bg-state-base-hover',
|
||||
)
|
||||
}
|
||||
popupClassName={
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import s from './style.module.css'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
export type ILoadingAnimProps = {
|
||||
type: 'text' | 'avatar'
|
||||
@ -11,7 +12,7 @@ const LoadingAnim: FC<ILoadingAnimProps> = ({
|
||||
type,
|
||||
}) => {
|
||||
return (
|
||||
<div className={`${s['dot-flashing']} ${s[type]}`}></div>
|
||||
<div className={cn(s['dot-flashing'], s[type])} />
|
||||
)
|
||||
}
|
||||
export default React.memo(LoadingAnim)
|
||||
|
||||
@ -8,6 +8,7 @@ import Modal from '@/app/components/base/modal'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import ConfirmAddVar from '@/app/components/app/configuration/config-prompt/confirm-add-var'
|
||||
import PromptEditor from '@/app/components/base/prompt-editor'
|
||||
import type { OpeningStatement } from '@/app/components/base/features/types'
|
||||
import { getInputKeys } from '@/app/components/base/block-input'
|
||||
import type { PromptVariable } from '@/models/debug'
|
||||
@ -101,7 +102,7 @@ const OpeningSettingModal = ({
|
||||
<div>·</div>
|
||||
<div>{tempSuggestedQuestions.length}/{MAX_QUESTION_NUM}</div>
|
||||
</div>
|
||||
<Divider bgStyle='gradient' className='ml-3 h-px w-0 grow'/>
|
||||
<Divider bgStyle='gradient' className='ml-3 h-px w-0 grow' />
|
||||
</div>
|
||||
<ReactSortable
|
||||
className="space-y-1"
|
||||
@ -178,19 +179,32 @@ const OpeningSettingModal = ({
|
||||
>
|
||||
<div className='mb-6 flex items-center justify-between'>
|
||||
<div className='title-2xl-semi-bold text-text-primary'>{t('appDebug.feature.conversationOpener.title')}</div>
|
||||
<div className='cursor-pointer p-1' onClick={onCancel}><RiCloseLine className='h-4 w-4 text-text-tertiary'/></div>
|
||||
<div className='cursor-pointer p-1' onClick={onCancel}><RiCloseLine className='h-4 w-4 text-text-tertiary' /></div>
|
||||
</div>
|
||||
<div className='mb-8 flex gap-2'>
|
||||
<div className='mt-1.5 h-8 w-8 shrink-0 rounded-lg border-components-panel-border bg-util-colors-orange-dark-orange-dark-500 p-1.5'>
|
||||
<RiAsterisk className='h-5 w-5 text-text-primary-on-surface' />
|
||||
</div>
|
||||
<div className='grow rounded-2xl border-t border-divider-subtle bg-chat-bubble-bg p-3 shadow-xs'>
|
||||
<textarea
|
||||
<PromptEditor
|
||||
value={tempValue}
|
||||
rows={3}
|
||||
onChange={e => setTempValue(e.target.value)}
|
||||
className="system-md-regular w-full border-0 bg-transparent px-0 text-text-secondary focus:outline-none"
|
||||
onChange={setTempValue}
|
||||
placeholder={t('appDebug.openingStatement.placeholder') as string}
|
||||
variableBlock={{
|
||||
show: true,
|
||||
variables: [
|
||||
// Prompt variables
|
||||
...promptVariables.map(item => ({
|
||||
name: item.name || item.key,
|
||||
value: item.key,
|
||||
})),
|
||||
// Workflow variables
|
||||
...workflowVariables.map(item => ({
|
||||
name: item.variable,
|
||||
value: item.variable,
|
||||
})),
|
||||
],
|
||||
}}
|
||||
/>
|
||||
{renderQuestions()}
|
||||
</div>
|
||||
|
||||
@ -0,0 +1,6 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 3C12.5523 3 13 3.44772 13 4C13 4.55228 12.5523 5 12 5H5.59962C5.30336 5 5.14096 5.00122 5.02443 5.01074C5.01998 5.01111 5.01573 5.01135 5.01173 5.01172C5.01136 5.01572 5.01112 5.01996 5.01076 5.02441C5.00124 5.14095 5.00001 5.30334 5.00001 5.59961V18.4004C5.00001 18.6967 5.00124 18.8591 5.01076 18.9756C5.01109 18.9797 5.01139 18.9836 5.01173 18.9873C5.01575 18.9877 5.01995 18.9889 5.02443 18.9893C5.14096 18.9988 5.30336 19 5.59962 19H18.4004C18.6967 19 18.8591 18.9988 18.9756 18.9893C18.9797 18.9889 18.9836 18.9876 18.9873 18.9873C18.9877 18.9836 18.9889 18.9797 18.9893 18.9756C18.9988 18.8591 19 18.6967 19 18.4004V12C19 11.4477 19.4477 11 20 11C20.5523 11 21 11.4477 21 12V18.4004C21 18.6638 21.0011 18.9219 20.9834 19.1387C20.9647 19.3672 20.9205 19.6369 20.7822 19.9082C20.5905 20.2845 20.2845 20.5905 19.9082 20.7822C19.6369 20.9205 19.3672 20.9647 19.1387 20.9834C18.9219 21.0011 18.6638 21 18.4004 21H5.59962C5.33625 21 5.07815 21.0011 4.86134 20.9834C4.63284 20.9647 4.36307 20.9204 4.09181 20.7822C3.71579 20.5906 3.40963 20.2847 3.21779 19.9082C3.07958 19.6369 3.03531 19.3672 3.01662 19.1387C2.9989 18.9219 3.00001 18.6638 3.00001 18.4004V5.59961C3.00001 5.33623 2.9989 5.07814 3.01662 4.86133C3.03531 4.63283 3.07958 4.36305 3.21779 4.0918C3.40953 3.71555 3.71557 3.40952 4.09181 3.21777C4.36306 3.07957 4.63285 3.0353 4.86134 3.0166C5.07815 2.99889 5.33624 3 5.59962 3H12Z" fill="black"/>
|
||||
<path d="M13.293 9.29297C13.6835 8.90244 14.3165 8.90244 14.707 9.29297L16.707 11.293C17.0975 11.6835 17.0975 12.3165 16.707 12.707L14.707 14.707C14.3165 15.0975 13.6835 15.0975 13.293 14.707C12.9025 14.3165 12.9025 13.6835 13.293 13.293L14.586 12L13.293 10.707C12.9025 10.3165 12.9025 9.68349 13.293 9.29297Z" fill="black"/>
|
||||
<path d="M9.29298 9.29297C9.68351 8.90244 10.3165 8.90244 10.707 9.29297C11.0975 9.6835 11.0975 10.3165 10.707 10.707L9.41408 12L10.707 13.293L10.7754 13.3691C11.0957 13.7619 11.0731 14.3409 10.707 14.707C10.341 15.0731 9.76192 15.0957 9.36915 14.7754L9.29298 14.707L7.29298 12.707C6.90246 12.3165 6.90246 11.6835 7.29298 11.293L9.29298 9.29297Z" fill="black"/>
|
||||
<path d="M18.501 2C18.7487 2.00048 18.9564 2.18654 18.9844 2.43262C19.1561 3.94883 20.0165 4.87694 21.5557 5.01367C21.8075 5.03614 22.0003 5.24816 22 5.50098C21.9995 5.75356 21.8063 5.96437 21.5547 5.98633C20.0372 6.11768 19.1176 7.03726 18.9863 8.55469C18.9644 8.80629 18.7536 8.99948 18.501 9C18.2483 9.00028 18.0362 8.80743 18.0137 8.55566C17.877 7.01647 16.9488 6.15613 15.4326 5.98438C15.1866 5.95634 15.0006 5.74863 15 5.50098C14.9997 5.25311 15.1855 5.04417 15.4317 5.01563C16.9697 4.83823 17.8382 3.96966 18.0156 2.43164C18.0442 2.18545 18.2531 1.99975 18.501 2Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
@ -0,0 +1,6 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.3691 3.22448C15.7619 2.90422 16.3409 2.92682 16.707 3.29284L20.707 7.29284C21.0975 7.68334 21.0975 8.31637 20.707 8.7069L8.70703 20.7069C8.5195 20.8944 8.26521 20.9999 8 20.9999H4C3.44771 20.9999 3 20.5522 3 19.9999V15.9999L3.00488 15.9012C3.0276 15.6723 3.12886 15.4569 3.29297 15.2928L15.293 3.29284L15.3691 3.22448ZM5 16.4139V18.9999H7.58593L18.5859 7.99987L16 5.41394L5 16.4139Z" fill="black"/>
|
||||
<path d="M18.2451 15.1581C18.3502 14.9478 18.6498 14.9478 18.7549 15.1581L19.4082 16.4637C19.4358 16.5189 19.4809 16.5641 19.5361 16.5917L20.8428 17.245C21.0525 17.3502 21.0524 17.6495 20.8428 17.7548L19.5361 18.4081C19.4809 18.4357 19.4358 18.4808 19.4082 18.536L18.7549 19.8426C18.6497 20.0525 18.3503 20.0525 18.2451 19.8426L17.5918 18.536C17.5642 18.4808 17.5191 18.4357 17.4639 18.4081L16.1572 17.7548C15.9476 17.6495 15.9475 17.3502 16.1572 17.245L17.4639 16.5917C17.5191 16.5641 17.5642 16.5189 17.5918 16.4637L18.2451 15.1581Z" fill="black"/>
|
||||
<path d="M4.24511 5.15808C4.35024 4.94783 4.64975 4.94783 4.75488 5.15808L5.4082 6.46374C5.4358 6.51895 5.48092 6.56406 5.53613 6.59167L6.84277 7.24499C7.05248 7.3502 7.05238 7.64946 6.84277 7.75476L5.53613 8.40808C5.48092 8.43568 5.4358 8.4808 5.4082 8.53601L4.75488 9.84265C4.64966 10.0525 4.35033 10.0525 4.24511 9.84265L3.59179 8.53601C3.56418 8.4808 3.51907 8.43568 3.46386 8.40808L2.15722 7.75476C1.94764 7.64945 1.94755 7.35021 2.15722 7.24499L3.46386 6.59167C3.51907 6.56406 3.56418 6.51895 3.59179 6.46374L4.24511 5.15808Z" fill="black"/>
|
||||
<path d="M8.73535 2.16394C8.84432 1.94601 9.15568 1.94601 9.26465 2.16394L9.74414 3.12292C9.77275 3.18014 9.81973 3.22712 9.87695 3.25573L10.8369 3.73522C11.0546 3.84424 11.0545 4.15544 10.8369 4.26452L9.87695 4.74401C9.81973 4.77262 9.77275 4.81961 9.74414 4.87683L9.26465 5.83679C9.15565 6.05458 8.84435 6.05457 8.73535 5.83679L8.25586 4.87683C8.22725 4.81961 8.18026 4.77262 8.12304 4.74401L7.16308 4.26452C6.94547 4.15546 6.94539 3.84422 7.16308 3.73522L8.12304 3.25573C8.18026 3.22712 8.22725 3.18014 8.25586 3.12292L8.73535 2.16394Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
@ -0,0 +1,53 @@
|
||||
{
|
||||
"icon": {
|
||||
"type": "element",
|
||||
"isRootNode": true,
|
||||
"name": "svg",
|
||||
"attributes": {
|
||||
"width": "24",
|
||||
"height": "24",
|
||||
"viewBox": "0 0 24 24",
|
||||
"fill": "none",
|
||||
"xmlns": "http://www.w3.org/2000/svg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"d": "M12 3C12.5523 3 13 3.44772 13 4C13 4.55228 12.5523 5 12 5H5.59962C5.30336 5 5.14096 5.00122 5.02443 5.01074C5.01998 5.01111 5.01573 5.01135 5.01173 5.01172C5.01136 5.01572 5.01112 5.01996 5.01076 5.02441C5.00124 5.14095 5.00001 5.30334 5.00001 5.59961V18.4004C5.00001 18.6967 5.00124 18.8591 5.01076 18.9756C5.01109 18.9797 5.01139 18.9836 5.01173 18.9873C5.01575 18.9877 5.01995 18.9889 5.02443 18.9893C5.14096 18.9988 5.30336 19 5.59962 19H18.4004C18.6967 19 18.8591 18.9988 18.9756 18.9893C18.9797 18.9889 18.9836 18.9876 18.9873 18.9873C18.9877 18.9836 18.9889 18.9797 18.9893 18.9756C18.9988 18.8591 19 18.6967 19 18.4004V12C19 11.4477 19.4477 11 20 11C20.5523 11 21 11.4477 21 12V18.4004C21 18.6638 21.0011 18.9219 20.9834 19.1387C20.9647 19.3672 20.9205 19.6369 20.7822 19.9082C20.5905 20.2845 20.2845 20.5905 19.9082 20.7822C19.6369 20.9205 19.3672 20.9647 19.1387 20.9834C18.9219 21.0011 18.6638 21 18.4004 21H5.59962C5.33625 21 5.07815 21.0011 4.86134 20.9834C4.63284 20.9647 4.36307 20.9204 4.09181 20.7822C3.71579 20.5906 3.40963 20.2847 3.21779 19.9082C3.07958 19.6369 3.03531 19.3672 3.01662 19.1387C2.9989 18.9219 3.00001 18.6638 3.00001 18.4004V5.59961C3.00001 5.33623 2.9989 5.07814 3.01662 4.86133C3.03531 4.63283 3.07958 4.36305 3.21779 4.0918C3.40953 3.71555 3.71557 3.40952 4.09181 3.21777C4.36306 3.07957 4.63285 3.0353 4.86134 3.0166C5.07815 2.99889 5.33624 3 5.59962 3H12Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"d": "M13.293 9.29297C13.6835 8.90244 14.3165 8.90244 14.707 9.29297L16.707 11.293C17.0975 11.6835 17.0975 12.3165 16.707 12.707L14.707 14.707C14.3165 15.0975 13.6835 15.0975 13.293 14.707C12.9025 14.3165 12.9025 13.6835 13.293 13.293L14.586 12L13.293 10.707C12.9025 10.3165 12.9025 9.68349 13.293 9.29297Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"d": "M9.29298 9.29297C9.68351 8.90244 10.3165 8.90244 10.707 9.29297C11.0975 9.6835 11.0975 10.3165 10.707 10.707L9.41408 12L10.707 13.293L10.7754 13.3691C11.0957 13.7619 11.0731 14.3409 10.707 14.707C10.341 15.0731 9.76192 15.0957 9.36915 14.7754L9.29298 14.707L7.29298 12.707C6.90246 12.3165 6.90246 11.6835 7.29298 11.293L9.29298 9.29297Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"d": "M18.501 2C18.7487 2.00048 18.9564 2.18654 18.9844 2.43262C19.1561 3.94883 20.0165 4.87694 21.5557 5.01367C21.8075 5.03614 22.0003 5.24816 22 5.50098C21.9995 5.75356 21.8063 5.96437 21.5547 5.98633C20.0372 6.11768 19.1176 7.03726 18.9863 8.55469C18.9644 8.80629 18.7536 8.99948 18.501 9C18.2483 9.00028 18.0362 8.80743 18.0137 8.55566C17.877 7.01647 16.9488 6.15613 15.4326 5.98438C15.1866 5.95634 15.0006 5.74863 15 5.50098C14.9997 5.25311 15.1855 5.04417 15.4317 5.01563C16.9697 4.83823 17.8382 3.96966 18.0156 2.43164C18.0442 2.18545 18.2531 1.99975 18.501 2Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "CodeAssistant"
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import * as React from 'react'
|
||||
import data from './CodeAssistant.json'
|
||||
import IconBase from '@/app/components/base/icons/IconBase'
|
||||
import type { IconData } from '@/app/components/base/icons/IconBase'
|
||||
|
||||
const Icon = (
|
||||
{
|
||||
ref,
|
||||
...props
|
||||
}: React.SVGProps<SVGSVGElement> & {
|
||||
ref?: React.RefObject<React.MutableRefObject<HTMLOrSVGElement>>;
|
||||
},
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />
|
||||
|
||||
Icon.displayName = 'CodeAssistant'
|
||||
|
||||
export default Icon
|
||||
@ -0,0 +1,55 @@
|
||||
{
|
||||
"icon": {
|
||||
"type": "element",
|
||||
"isRootNode": true,
|
||||
"name": "svg",
|
||||
"attributes": {
|
||||
"width": "24",
|
||||
"height": "24",
|
||||
"viewBox": "0 0 24 24",
|
||||
"fill": "none",
|
||||
"xmlns": "http://www.w3.org/2000/svg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"fill-rule": "evenodd",
|
||||
"clip-rule": "evenodd",
|
||||
"d": "M15.3691 3.22448C15.7619 2.90422 16.3409 2.92682 16.707 3.29284L20.707 7.29284C21.0975 7.68334 21.0975 8.31637 20.707 8.7069L8.70703 20.7069C8.5195 20.8944 8.26521 20.9999 8 20.9999H4C3.44771 20.9999 3 20.5522 3 19.9999V15.9999L3.00488 15.9012C3.0276 15.6723 3.12886 15.4569 3.29297 15.2928L15.293 3.29284L15.3691 3.22448ZM5 16.4139V18.9999H7.58593L18.5859 7.99987L16 5.41394L5 16.4139Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"d": "M18.2451 15.1581C18.3502 14.9478 18.6498 14.9478 18.7549 15.1581L19.4082 16.4637C19.4358 16.5189 19.4809 16.5641 19.5361 16.5917L20.8428 17.245C21.0525 17.3502 21.0524 17.6495 20.8428 17.7548L19.5361 18.4081C19.4809 18.4357 19.4358 18.4808 19.4082 18.536L18.7549 19.8426C18.6497 20.0525 18.3503 20.0525 18.2451 19.8426L17.5918 18.536C17.5642 18.4808 17.5191 18.4357 17.4639 18.4081L16.1572 17.7548C15.9476 17.6495 15.9475 17.3502 16.1572 17.245L17.4639 16.5917C17.5191 16.5641 17.5642 16.5189 17.5918 16.4637L18.2451 15.1581Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"d": "M4.24511 5.15808C4.35024 4.94783 4.64975 4.94783 4.75488 5.15808L5.4082 6.46374C5.4358 6.51895 5.48092 6.56406 5.53613 6.59167L6.84277 7.24499C7.05248 7.3502 7.05238 7.64946 6.84277 7.75476L5.53613 8.40808C5.48092 8.43568 5.4358 8.4808 5.4082 8.53601L4.75488 9.84265C4.64966 10.0525 4.35033 10.0525 4.24511 9.84265L3.59179 8.53601C3.56418 8.4808 3.51907 8.43568 3.46386 8.40808L2.15722 7.75476C1.94764 7.64945 1.94755 7.35021 2.15722 7.24499L3.46386 6.59167C3.51907 6.56406 3.56418 6.51895 3.59179 6.46374L4.24511 5.15808Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"d": "M8.73535 2.16394C8.84432 1.94601 9.15568 1.94601 9.26465 2.16394L9.74414 3.12292C9.77275 3.18014 9.81973 3.22712 9.87695 3.25573L10.8369 3.73522C11.0546 3.84424 11.0545 4.15544 10.8369 4.26452L9.87695 4.74401C9.81973 4.77262 9.77275 4.81961 9.74414 4.87683L9.26465 5.83679C9.15565 6.05458 8.84435 6.05457 8.73535 5.83679L8.25586 4.87683C8.22725 4.81961 8.18026 4.77262 8.12304 4.74401L7.16308 4.26452C6.94547 4.15546 6.94539 3.84422 7.16308 3.73522L8.12304 3.25573C8.18026 3.22712 8.22725 3.18014 8.25586 3.12292L8.73535 2.16394Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "MagicEdit"
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import * as React from 'react'
|
||||
import data from './MagicEdit.json'
|
||||
import IconBase from '@/app/components/base/icons/IconBase'
|
||||
import type { IconData } from '@/app/components/base/icons/IconBase'
|
||||
|
||||
const Icon = (
|
||||
{
|
||||
ref,
|
||||
...props
|
||||
}: React.SVGProps<SVGSVGElement> & {
|
||||
ref?: React.RefObject<React.MutableRefObject<HTMLOrSVGElement>>;
|
||||
},
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />
|
||||
|
||||
Icon.displayName = 'MagicEdit'
|
||||
|
||||
export default Icon
|
||||
@ -3,6 +3,7 @@ export { default as Bookmark } from './Bookmark'
|
||||
export { default as CheckDone01 } from './CheckDone01'
|
||||
export { default as Check } from './Check'
|
||||
export { default as ChecklistSquare } from './ChecklistSquare'
|
||||
export { default as CodeAssistant } from './CodeAssistant'
|
||||
export { default as DotsGrid } from './DotsGrid'
|
||||
export { default as Edit02 } from './Edit02'
|
||||
export { default as Edit04 } from './Edit04'
|
||||
@ -14,6 +15,7 @@ export { default as LinkExternal02 } from './LinkExternal02'
|
||||
export { default as LogIn04 } from './LogIn04'
|
||||
export { default as LogOut01 } from './LogOut01'
|
||||
export { default as LogOut04 } from './LogOut04'
|
||||
export { default as MagicEdit } from './MagicEdit'
|
||||
export { default as Menu01 } from './Menu01'
|
||||
export { default as Pin01 } from './Pin01'
|
||||
export { default as Pin02 } from './Pin02'
|
||||
|
||||
@ -117,7 +117,7 @@ const Flowchart = React.forwardRef((props: {
|
||||
const [isInitialized, setIsInitialized] = useState(false)
|
||||
const [currentTheme, setCurrentTheme] = useState<'light' | 'dark'>(props.theme || 'light')
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const chartId = useRef(`mermaid-chart-${Math.random().toString(36).substr(2, 9)}`).current
|
||||
const chartId = useRef(`mermaid-chart-${Math.random().toString(36).slice(2, 11)}`).current
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const renderTimeoutRef = useRef<NodeJS.Timeout>()
|
||||
const [errMsg, setErrMsg] = useState('')
|
||||
|
||||
@ -52,7 +52,7 @@ export default function Modal({
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
||||
<TransitionChild>
|
||||
<DialogPanel className={classNames(
|
||||
'w-full max-w-[480px] rounded-2xl bg-components-panel-bg p-6 text-left align-middle shadow-xl transition-all',
|
||||
'relative w-full max-w-[480px] rounded-2xl bg-components-panel-bg p-6 text-left align-middle shadow-xl transition-all',
|
||||
overflowVisible ? 'overflow-visible' : 'overflow-hidden',
|
||||
'duration-100 ease-in data-[closed]:scale-95 data-[closed]:opacity-0',
|
||||
'data-[enter]:scale-100 data-[enter]:opacity-100',
|
||||
|
||||
@ -3,6 +3,10 @@ import { SupportUploadFileTypes, type ValueSelector } from '../../workflow/types
|
||||
export const CONTEXT_PLACEHOLDER_TEXT = '{{#context#}}'
|
||||
export const HISTORY_PLACEHOLDER_TEXT = '{{#histories#}}'
|
||||
export const QUERY_PLACEHOLDER_TEXT = '{{#query#}}'
|
||||
export const CURRENT_PLACEHOLDER_TEXT = '{{#current#}}'
|
||||
export const ERROR_MESSAGE_PLACEHOLDER_TEXT = '{{#error_message#}}'
|
||||
export const LAST_RUN_PLACEHOLDER_TEXT = '{{#last_run#}}'
|
||||
|
||||
export const PRE_PROMPT_PLACEHOLDER_TEXT = '{{#pre_prompt#}}'
|
||||
export const UPDATE_DATASETS_EVENT_EMITTER = 'prompt-editor-context-block-update-datasets'
|
||||
export const UPDATE_HISTORY_EVENT_EMITTER = 'prompt-editor-history-block-update-role'
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import type { FC } from 'react'
|
||||
import { useEffect } from 'react'
|
||||
import React, { useEffect } from 'react'
|
||||
import type {
|
||||
EditorState,
|
||||
} from 'lexical'
|
||||
@ -39,6 +39,22 @@ import {
|
||||
WorkflowVariableBlockNode,
|
||||
WorkflowVariableBlockReplacementBlock,
|
||||
} from './plugins/workflow-variable-block'
|
||||
import {
|
||||
CurrentBlock,
|
||||
CurrentBlockNode,
|
||||
CurrentBlockReplacementBlock,
|
||||
} from './plugins/current-block'
|
||||
import {
|
||||
ErrorMessageBlock,
|
||||
ErrorMessageBlockNode,
|
||||
ErrorMessageBlockReplacementBlock,
|
||||
} from './plugins/error-message-block'
|
||||
import {
|
||||
LastRunBlock,
|
||||
LastRunBlockNode,
|
||||
LastRunReplacementBlock,
|
||||
} from './plugins/last-run-block'
|
||||
|
||||
import VariableBlock from './plugins/variable-block'
|
||||
import VariableValueBlock from './plugins/variable-value-block'
|
||||
import { VariableValueBlockNode } from './plugins/variable-value-block/node'
|
||||
@ -48,8 +64,11 @@ import UpdateBlock from './plugins/update-block'
|
||||
import { textToEditorState } from './utils'
|
||||
import type {
|
||||
ContextBlockType,
|
||||
CurrentBlockType,
|
||||
ErrorMessageBlockType,
|
||||
ExternalToolBlockType,
|
||||
HistoryBlockType,
|
||||
LastRunBlockType,
|
||||
QueryBlockType,
|
||||
VariableBlockType,
|
||||
WorkflowVariableBlockType,
|
||||
@ -66,7 +85,7 @@ export type PromptEditorProps = {
|
||||
compact?: boolean
|
||||
wrapperClassName?: string
|
||||
className?: string
|
||||
placeholder?: string | JSX.Element
|
||||
placeholder?: string | React.JSX.Element
|
||||
placeholderClassName?: string
|
||||
style?: React.CSSProperties
|
||||
value?: string
|
||||
@ -80,6 +99,9 @@ export type PromptEditorProps = {
|
||||
variableBlock?: VariableBlockType
|
||||
externalToolBlock?: ExternalToolBlockType
|
||||
workflowVariableBlock?: WorkflowVariableBlockType
|
||||
currentBlock?: CurrentBlockType
|
||||
errorMessageBlock?: ErrorMessageBlockType
|
||||
lastRunBlock?: LastRunBlockType
|
||||
isSupportFileVar?: boolean
|
||||
}
|
||||
|
||||
@ -102,6 +124,9 @@ const PromptEditor: FC<PromptEditorProps> = ({
|
||||
variableBlock,
|
||||
externalToolBlock,
|
||||
workflowVariableBlock,
|
||||
currentBlock,
|
||||
errorMessageBlock,
|
||||
lastRunBlock,
|
||||
isSupportFileVar,
|
||||
}) => {
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
@ -119,6 +144,9 @@ const PromptEditor: FC<PromptEditorProps> = ({
|
||||
QueryBlockNode,
|
||||
WorkflowVariableBlockNode,
|
||||
VariableValueBlockNode,
|
||||
CurrentBlockNode,
|
||||
ErrorMessageBlockNode,
|
||||
LastRunBlockNode, // LastRunBlockNode is used for error message block replacement
|
||||
],
|
||||
editorState: textToEditorState(value || ''),
|
||||
onError: (error: Error) => {
|
||||
@ -178,6 +206,9 @@ const PromptEditor: FC<PromptEditorProps> = ({
|
||||
variableBlock={variableBlock}
|
||||
externalToolBlock={externalToolBlock}
|
||||
workflowVariableBlock={workflowVariableBlock}
|
||||
currentBlock={currentBlock}
|
||||
errorMessageBlock={errorMessageBlock}
|
||||
lastRunBlock={lastRunBlock}
|
||||
isSupportFileVar={isSupportFileVar}
|
||||
/>
|
||||
<ComponentPickerBlock
|
||||
@ -188,6 +219,9 @@ const PromptEditor: FC<PromptEditorProps> = ({
|
||||
variableBlock={variableBlock}
|
||||
externalToolBlock={externalToolBlock}
|
||||
workflowVariableBlock={workflowVariableBlock}
|
||||
currentBlock={currentBlock}
|
||||
errorMessageBlock={errorMessageBlock}
|
||||
lastRunBlock={lastRunBlock}
|
||||
isSupportFileVar={isSupportFileVar}
|
||||
/>
|
||||
{
|
||||
@ -230,6 +264,35 @@ const PromptEditor: FC<PromptEditorProps> = ({
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
currentBlock?.show && (
|
||||
<>
|
||||
<CurrentBlock {...currentBlock} />
|
||||
<CurrentBlockReplacementBlock {...currentBlock} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
errorMessageBlock?.show && (
|
||||
<>
|
||||
<ErrorMessageBlock {...errorMessageBlock} />
|
||||
<ErrorMessageBlockReplacementBlock {...errorMessageBlock} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
lastRunBlock?.show && (
|
||||
<>
|
||||
<LastRunBlock {...lastRunBlock} />
|
||||
<LastRunReplacementBlock {...lastRunBlock} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
isSupportFileVar && (
|
||||
<VariableValueBlock />
|
||||
)
|
||||
}
|
||||
<OnChangePlugin onChange={handleEditorChange} />
|
||||
<OnBlurBlock onBlur={onBlur} onFocus={onFocus} />
|
||||
<UpdateBlock instanceId={instanceId} />
|
||||
|
||||
@ -4,8 +4,11 @@ import { $insertNodes } from 'lexical'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import type {
|
||||
ContextBlockType,
|
||||
CurrentBlockType,
|
||||
ErrorMessageBlockType,
|
||||
ExternalToolBlockType,
|
||||
HistoryBlockType,
|
||||
LastRunBlockType,
|
||||
QueryBlockType,
|
||||
VariableBlockType,
|
||||
WorkflowVariableBlockType,
|
||||
@ -27,6 +30,7 @@ import { BracketsX } from '@/app/components/base/icons/src/vender/line/developme
|
||||
import { UserEdit02 } from '@/app/components/base/icons/src/vender/solid/users'
|
||||
import { ArrowUpRight } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import { VarType } from '@/app/components/workflow/types'
|
||||
|
||||
export const usePromptOptions = (
|
||||
contextBlock?: ContextBlockType,
|
||||
@ -267,17 +271,61 @@ export const useOptions = (
|
||||
variableBlock?: VariableBlockType,
|
||||
externalToolBlockType?: ExternalToolBlockType,
|
||||
workflowVariableBlockType?: WorkflowVariableBlockType,
|
||||
currentBlockType?: CurrentBlockType,
|
||||
errorMessageBlockType?: ErrorMessageBlockType,
|
||||
lastRunBlockType?: LastRunBlockType,
|
||||
queryString?: string,
|
||||
) => {
|
||||
const promptOptions = usePromptOptions(contextBlock, queryBlock, historyBlock)
|
||||
const variableOptions = useVariableOptions(variableBlock, queryString)
|
||||
const externalToolOptions = useExternalToolOptions(externalToolBlockType, queryString)
|
||||
|
||||
const workflowVariableOptions = useMemo(() => {
|
||||
if (!workflowVariableBlockType?.show)
|
||||
return []
|
||||
|
||||
return workflowVariableBlockType.variables || []
|
||||
}, [workflowVariableBlockType])
|
||||
const res = workflowVariableBlockType.variables || []
|
||||
if(errorMessageBlockType?.show && res.findIndex(v => v.nodeId === 'error_message') === -1) {
|
||||
res.unshift({
|
||||
nodeId: 'error_message',
|
||||
title: 'error_message',
|
||||
isFlat: true,
|
||||
vars: [
|
||||
{
|
||||
variable: 'error_message',
|
||||
type: VarType.string,
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
if(lastRunBlockType?.show && res.findIndex(v => v.nodeId === 'last_run') === -1) {
|
||||
res.unshift({
|
||||
nodeId: 'last_run',
|
||||
title: 'last_run',
|
||||
isFlat: true,
|
||||
vars: [
|
||||
{
|
||||
variable: 'last_run',
|
||||
type: VarType.object,
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
if(currentBlockType?.show && res.findIndex(v => v.nodeId === 'current') === -1) {
|
||||
const title = currentBlockType.generatorType === 'prompt' ? 'current_prompt' : 'current_code'
|
||||
res.unshift({
|
||||
nodeId: 'current',
|
||||
title,
|
||||
isFlat: true,
|
||||
vars: [
|
||||
{
|
||||
variable: 'current',
|
||||
type: VarType.string,
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
return res
|
||||
}, [workflowVariableBlockType?.show, workflowVariableBlockType?.variables, errorMessageBlockType?.show, lastRunBlockType?.show, currentBlockType?.show, currentBlockType?.generatorType])
|
||||
|
||||
return useMemo(() => {
|
||||
return {
|
||||
|
||||
@ -17,8 +17,11 @@ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext
|
||||
import { LexicalTypeaheadMenuPlugin } from '@lexical/react/LexicalTypeaheadMenuPlugin'
|
||||
import type {
|
||||
ContextBlockType,
|
||||
CurrentBlockType,
|
||||
ErrorMessageBlockType,
|
||||
ExternalToolBlockType,
|
||||
HistoryBlockType,
|
||||
LastRunBlockType,
|
||||
QueryBlockType,
|
||||
VariableBlockType,
|
||||
WorkflowVariableBlockType,
|
||||
@ -32,6 +35,10 @@ import type { PickerBlockMenuOption } from './menu'
|
||||
import VarReferenceVars from '@/app/components/workflow/nodes/_base/components/variable/var-reference-vars'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import { KEY_ESCAPE_COMMAND } from 'lexical'
|
||||
import { INSERT_CURRENT_BLOCK_COMMAND } from '../current-block'
|
||||
import { GeneratorType } from '@/app/components/app/configuration/config/automatic/types'
|
||||
import { INSERT_ERROR_MESSAGE_BLOCK_COMMAND } from '../error-message-block'
|
||||
import { INSERT_LAST_RUN_BLOCK_COMMAND } from '../last-run-block'
|
||||
|
||||
type ComponentPickerProps = {
|
||||
triggerString: string
|
||||
@ -41,6 +48,9 @@ type ComponentPickerProps = {
|
||||
variableBlock?: VariableBlockType
|
||||
externalToolBlock?: ExternalToolBlockType
|
||||
workflowVariableBlock?: WorkflowVariableBlockType
|
||||
currentBlock?: CurrentBlockType
|
||||
errorMessageBlock?: ErrorMessageBlockType
|
||||
lastRunBlock?: LastRunBlockType
|
||||
isSupportFileVar?: boolean
|
||||
}
|
||||
const ComponentPicker = ({
|
||||
@ -51,6 +61,9 @@ const ComponentPicker = ({
|
||||
variableBlock,
|
||||
externalToolBlock,
|
||||
workflowVariableBlock,
|
||||
currentBlock,
|
||||
errorMessageBlock,
|
||||
lastRunBlock,
|
||||
isSupportFileVar,
|
||||
}: ComponentPickerProps) => {
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
@ -87,6 +100,9 @@ const ComponentPicker = ({
|
||||
variableBlock,
|
||||
externalToolBlock,
|
||||
workflowVariableBlock,
|
||||
currentBlock,
|
||||
errorMessageBlock,
|
||||
lastRunBlock,
|
||||
)
|
||||
|
||||
const onSelectOption = useCallback(
|
||||
@ -112,12 +128,23 @@ const ComponentPicker = ({
|
||||
if (needRemove)
|
||||
needRemove.remove()
|
||||
})
|
||||
|
||||
if (variables[1] === 'sys.query' || variables[1] === 'sys.files')
|
||||
const isFlat = variables.length === 1
|
||||
if(isFlat) {
|
||||
const varName = variables[0]
|
||||
if(varName === 'current')
|
||||
editor.dispatchCommand(INSERT_CURRENT_BLOCK_COMMAND, currentBlock?.generatorType)
|
||||
else if (varName === 'error_message')
|
||||
editor.dispatchCommand(INSERT_ERROR_MESSAGE_BLOCK_COMMAND, null)
|
||||
else if (varName === 'last_run')
|
||||
editor.dispatchCommand(INSERT_LAST_RUN_BLOCK_COMMAND, null)
|
||||
}
|
||||
else if (variables[1] === 'sys.query' || variables[1] === 'sys.files') {
|
||||
editor.dispatchCommand(INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND, [variables[1]])
|
||||
else
|
||||
}
|
||||
else {
|
||||
editor.dispatchCommand(INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND, variables)
|
||||
}, [editor, checkForTriggerMatch, triggerString])
|
||||
}
|
||||
}, [editor, currentBlock?.generatorType, checkForTriggerMatch, triggerString])
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
const escapeEvent = new KeyboardEvent('keydown', { key: 'Escape' })
|
||||
@ -166,6 +193,7 @@ const ComponentPicker = ({
|
||||
onClose={handleClose}
|
||||
onBlur={handleClose}
|
||||
autoFocus={false}
|
||||
isInCodeGeneratorInstructionEditor={currentBlock?.generatorType === GeneratorType.code}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
@ -206,7 +234,7 @@ const ComponentPicker = ({
|
||||
}
|
||||
</>
|
||||
)
|
||||
}, [allFlattenOptions.length, workflowVariableBlock?.show, refs, isPositioned, floatingStyles, queryString, workflowVariableOptions, handleSelectWorkflowVariable, handleClose, isSupportFileVar])
|
||||
}, [allFlattenOptions.length, workflowVariableBlock?.show, floatingStyles, isPositioned, refs, workflowVariableOptions, isSupportFileVar, handleClose, currentBlock?.generatorType, handleSelectWorkflowVariable, queryString])
|
||||
|
||||
return (
|
||||
<LexicalTypeaheadMenuPlugin
|
||||
|
||||
@ -0,0 +1,44 @@
|
||||
import { type FC, useEffect } from 'react'
|
||||
import { GeneratorType } from '@/app/components/app/configuration/config/automatic/types'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { useSelectOrDelete } from '../../hooks'
|
||||
import { CurrentBlockNode, DELETE_CURRENT_BLOCK_COMMAND } from '.'
|
||||
import cn from '@/utils/classnames'
|
||||
import { CodeAssistant, MagicEdit } from '../../../icons/src/vender/line/general'
|
||||
|
||||
type CurrentBlockComponentProps = {
|
||||
nodeKey: string
|
||||
generatorType: GeneratorType
|
||||
}
|
||||
|
||||
const CurrentBlockComponent: FC<CurrentBlockComponentProps> = ({
|
||||
nodeKey,
|
||||
generatorType,
|
||||
}) => {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_CURRENT_BLOCK_COMMAND)
|
||||
|
||||
const Icon = generatorType === GeneratorType.prompt ? MagicEdit : CodeAssistant
|
||||
useEffect(() => {
|
||||
if (!editor.hasNodes([CurrentBlockNode]))
|
||||
throw new Error('WorkflowVariableBlockPlugin: WorkflowVariableBlock not registered on editor')
|
||||
}, [editor])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'group/wrap relative mx-0.5 flex h-[18px] select-none items-center rounded-[5px] border pl-0.5 pr-[3px] text-util-colors-violet-violet-600 hover:border-state-accent-solid hover:bg-state-accent-hover',
|
||||
isSelected ? ' border-state-accent-solid bg-state-accent-hover' : ' border-components-panel-border-subtle bg-components-badge-white-to-dark',
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
}}
|
||||
ref={ref}
|
||||
>
|
||||
<Icon className='mr-0.5 h-[14px] w-[14px]' />
|
||||
<div className='text-xs font-medium'>{generatorType === GeneratorType.prompt ? 'current_prompt' : 'current_code'}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CurrentBlockComponent
|
||||
@ -0,0 +1,62 @@
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
} from 'react'
|
||||
import { $applyNodeReplacement } from 'lexical'
|
||||
import { mergeRegister } from '@lexical/utils'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { decoratorTransform } from '../../utils'
|
||||
import { CURRENT_PLACEHOLDER_TEXT } from '../../constants'
|
||||
import type { CurrentBlockType } from '../../types'
|
||||
import {
|
||||
$createCurrentBlockNode,
|
||||
CurrentBlockNode,
|
||||
} from './node'
|
||||
import { CustomTextNode } from '../custom-text/node'
|
||||
|
||||
const REGEX = new RegExp(CURRENT_PLACEHOLDER_TEXT)
|
||||
|
||||
const CurrentBlockReplacementBlock = ({
|
||||
generatorType,
|
||||
onInsert,
|
||||
}: CurrentBlockType) => {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor.hasNodes([CurrentBlockNode]))
|
||||
throw new Error('CurrentBlockNodePlugin: CurrentBlockNode not registered on editor')
|
||||
}, [editor])
|
||||
|
||||
const createCurrentBlockNode = useCallback((): CurrentBlockNode => {
|
||||
if (onInsert)
|
||||
onInsert()
|
||||
return $applyNodeReplacement($createCurrentBlockNode(generatorType))
|
||||
}, [onInsert, generatorType])
|
||||
|
||||
const getMatch = useCallback((text: string) => {
|
||||
const matchArr = REGEX.exec(text)
|
||||
|
||||
if (matchArr === null)
|
||||
return null
|
||||
|
||||
const startOffset = matchArr.index
|
||||
const endOffset = startOffset + CURRENT_PLACEHOLDER_TEXT.length
|
||||
return {
|
||||
end: endOffset,
|
||||
start: startOffset,
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
REGEX.lastIndex = 0
|
||||
return mergeRegister(
|
||||
editor.registerNodeTransform(CustomTextNode, textNode => decoratorTransform(textNode, getMatch, createCurrentBlockNode)),
|
||||
)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export default memo(CurrentBlockReplacementBlock)
|
||||
@ -0,0 +1,66 @@
|
||||
import {
|
||||
memo,
|
||||
useEffect,
|
||||
} from 'react'
|
||||
import {
|
||||
$insertNodes,
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
createCommand,
|
||||
} from 'lexical'
|
||||
import { mergeRegister } from '@lexical/utils'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import type { CurrentBlockType } from '../../types'
|
||||
import {
|
||||
$createCurrentBlockNode,
|
||||
CurrentBlockNode,
|
||||
} from './node'
|
||||
|
||||
export const INSERT_CURRENT_BLOCK_COMMAND = createCommand('INSERT_CURRENT_BLOCK_COMMAND')
|
||||
export const DELETE_CURRENT_BLOCK_COMMAND = createCommand('DELETE_CURRENT_BLOCK_COMMAND')
|
||||
|
||||
const CurrentBlock = memo(({
|
||||
generatorType,
|
||||
onInsert,
|
||||
onDelete,
|
||||
}: CurrentBlockType) => {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor.hasNodes([CurrentBlockNode]))
|
||||
throw new Error('CURRENTBlockPlugin: CURRENTBlock not registered on editor')
|
||||
|
||||
return mergeRegister(
|
||||
editor.registerCommand(
|
||||
INSERT_CURRENT_BLOCK_COMMAND,
|
||||
() => {
|
||||
const currentBlockNode = $createCurrentBlockNode(generatorType)
|
||||
|
||||
$insertNodes([currentBlockNode])
|
||||
|
||||
if (onInsert)
|
||||
onInsert()
|
||||
|
||||
return true
|
||||
},
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
),
|
||||
editor.registerCommand(
|
||||
DELETE_CURRENT_BLOCK_COMMAND,
|
||||
() => {
|
||||
if (onDelete)
|
||||
onDelete()
|
||||
|
||||
return true
|
||||
},
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
),
|
||||
)
|
||||
}, [editor, generatorType, onDelete, onInsert])
|
||||
|
||||
return null
|
||||
})
|
||||
CurrentBlock.displayName = 'CurrentBlock'
|
||||
|
||||
export { CurrentBlock }
|
||||
export { CurrentBlockNode } from './node'
|
||||
export { default as CurrentBlockReplacementBlock } from './current-block-replacement-block'
|
||||
@ -0,0 +1,78 @@
|
||||
import type { LexicalNode, NodeKey, SerializedLexicalNode } from 'lexical'
|
||||
import { DecoratorNode } from 'lexical'
|
||||
import CurrentBlockComponent from './component'
|
||||
import type { GeneratorType } from '@/app/components/app/configuration/config/automatic/types'
|
||||
|
||||
export type SerializedNode = SerializedLexicalNode & { generatorType: GeneratorType; }
|
||||
|
||||
export class CurrentBlockNode extends DecoratorNode<React.JSX.Element> {
|
||||
__generatorType: GeneratorType
|
||||
static getType(): string {
|
||||
return 'current-block'
|
||||
}
|
||||
|
||||
static clone(node: CurrentBlockNode): CurrentBlockNode {
|
||||
return new CurrentBlockNode(node.__generatorType, node.getKey())
|
||||
}
|
||||
|
||||
isInline(): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
constructor(generatorType: GeneratorType, key?: NodeKey) {
|
||||
super(key)
|
||||
|
||||
this.__generatorType = generatorType
|
||||
}
|
||||
|
||||
createDOM(): HTMLElement {
|
||||
const div = document.createElement('div')
|
||||
div.classList.add('inline-flex', 'items-center', 'align-middle')
|
||||
return div
|
||||
}
|
||||
|
||||
updateDOM(): false {
|
||||
return false
|
||||
}
|
||||
|
||||
decorate(): React.JSX.Element {
|
||||
return (
|
||||
<CurrentBlockComponent
|
||||
nodeKey={this.getKey()}
|
||||
generatorType={this.getGeneratorType()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
getGeneratorType(): GeneratorType {
|
||||
const self = this.getLatest()
|
||||
return self.__generatorType
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedNode): CurrentBlockNode {
|
||||
const node = $createCurrentBlockNode(serializedNode.generatorType)
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
exportJSON(): SerializedNode {
|
||||
return {
|
||||
type: 'current-block',
|
||||
version: 1,
|
||||
generatorType: this.getGeneratorType(),
|
||||
}
|
||||
}
|
||||
|
||||
getTextContent(): string {
|
||||
return '{{#current#}}'
|
||||
}
|
||||
}
|
||||
export function $createCurrentBlockNode(type: GeneratorType): CurrentBlockNode {
|
||||
return new CurrentBlockNode(type)
|
||||
}
|
||||
|
||||
export function $isCurrentBlockNode(
|
||||
node: CurrentBlockNode | LexicalNode | null | undefined,
|
||||
): boolean {
|
||||
return node instanceof CurrentBlockNode
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
import { type FC, useEffect } from 'react'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { useSelectOrDelete } from '../../hooks'
|
||||
import { DELETE_ERROR_MESSAGE_COMMAND, ErrorMessageBlockNode } from '.'
|
||||
import cn from '@/utils/classnames'
|
||||
import { Variable02 } from '../../../icons/src/vender/solid/development'
|
||||
|
||||
type Props = {
|
||||
nodeKey: string
|
||||
}
|
||||
|
||||
const ErrorMessageBlockComponent: FC<Props> = ({
|
||||
nodeKey,
|
||||
}) => {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_ERROR_MESSAGE_COMMAND)
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor.hasNodes([ErrorMessageBlockNode]))
|
||||
throw new Error('WorkflowVariableBlockPlugin: WorkflowVariableBlock not registered on editor')
|
||||
}, [editor])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'group/wrap relative mx-0.5 flex h-[18px] select-none items-center rounded-[5px] border pl-0.5 pr-[3px] text-util-colors-orange-dark-orange-dark-600 hover:border-state-accent-solid hover:bg-state-accent-hover',
|
||||
isSelected ? ' border-state-accent-solid bg-state-accent-hover' : ' border-components-panel-border-subtle bg-components-badge-white-to-dark',
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
}}
|
||||
ref={ref}
|
||||
>
|
||||
<Variable02 className='mr-0.5 h-[14px] w-[14px]' />
|
||||
<div className='text-xs font-medium'>error_message</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ErrorMessageBlockComponent
|
||||
@ -0,0 +1,61 @@
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
} from 'react'
|
||||
import { $applyNodeReplacement } from 'lexical'
|
||||
import { mergeRegister } from '@lexical/utils'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { decoratorTransform } from '../../utils'
|
||||
import { ERROR_MESSAGE_PLACEHOLDER_TEXT } from '../../constants'
|
||||
import type { ErrorMessageBlockType } from '../../types'
|
||||
import {
|
||||
$createErrorMessageBlockNode,
|
||||
ErrorMessageBlockNode,
|
||||
} from './node'
|
||||
import { CustomTextNode } from '../custom-text/node'
|
||||
|
||||
const REGEX = new RegExp(ERROR_MESSAGE_PLACEHOLDER_TEXT)
|
||||
|
||||
const ErrorMessageBlockReplacementBlock = ({
|
||||
onInsert,
|
||||
}: ErrorMessageBlockType) => {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor.hasNodes([ErrorMessageBlockNode]))
|
||||
throw new Error('ErrorMessageBlockNodePlugin: ErrorMessageBlockNode not registered on editor')
|
||||
}, [editor])
|
||||
|
||||
const createErrorMessageBlockNode = useCallback((): ErrorMessageBlockNode => {
|
||||
if (onInsert)
|
||||
onInsert()
|
||||
return $applyNodeReplacement($createErrorMessageBlockNode())
|
||||
}, [onInsert])
|
||||
|
||||
const getMatch = useCallback((text: string) => {
|
||||
const matchArr = REGEX.exec(text)
|
||||
|
||||
if (matchArr === null)
|
||||
return null
|
||||
|
||||
const startOffset = matchArr.index
|
||||
const endOffset = startOffset + ERROR_MESSAGE_PLACEHOLDER_TEXT.length
|
||||
return {
|
||||
end: endOffset,
|
||||
start: startOffset,
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
REGEX.lastIndex = 0
|
||||
return mergeRegister(
|
||||
editor.registerNodeTransform(CustomTextNode, textNode => decoratorTransform(textNode, getMatch, createErrorMessageBlockNode)),
|
||||
)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export default memo(ErrorMessageBlockReplacementBlock)
|
||||
@ -0,0 +1,65 @@
|
||||
import {
|
||||
memo,
|
||||
useEffect,
|
||||
} from 'react'
|
||||
import {
|
||||
$insertNodes,
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
createCommand,
|
||||
} from 'lexical'
|
||||
import { mergeRegister } from '@lexical/utils'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import type { ErrorMessageBlockType } from '../../types'
|
||||
import {
|
||||
$createErrorMessageBlockNode,
|
||||
ErrorMessageBlockNode,
|
||||
} from './node'
|
||||
|
||||
export const INSERT_ERROR_MESSAGE_BLOCK_COMMAND = createCommand('INSERT_ERROR_MESSAGE_BLOCK_COMMAND')
|
||||
export const DELETE_ERROR_MESSAGE_COMMAND = createCommand('DELETE_ERROR_MESSAGE_COMMAND')
|
||||
|
||||
const ErrorMessageBlock = memo(({
|
||||
onInsert,
|
||||
onDelete,
|
||||
}: ErrorMessageBlockType) => {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor.hasNodes([ErrorMessageBlockNode]))
|
||||
throw new Error('ERROR_MESSAGEBlockPlugin: ERROR_MESSAGEBlock not registered on editor')
|
||||
|
||||
return mergeRegister(
|
||||
editor.registerCommand(
|
||||
INSERT_ERROR_MESSAGE_BLOCK_COMMAND,
|
||||
() => {
|
||||
const Node = $createErrorMessageBlockNode()
|
||||
|
||||
$insertNodes([Node])
|
||||
|
||||
if (onInsert)
|
||||
onInsert()
|
||||
|
||||
return true
|
||||
},
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
),
|
||||
editor.registerCommand(
|
||||
DELETE_ERROR_MESSAGE_COMMAND,
|
||||
() => {
|
||||
if (onDelete)
|
||||
onDelete()
|
||||
|
||||
return true
|
||||
},
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
),
|
||||
)
|
||||
}, [editor, onDelete, onInsert])
|
||||
|
||||
return null
|
||||
})
|
||||
ErrorMessageBlock.displayName = 'ErrorMessageBlock'
|
||||
|
||||
export { ErrorMessageBlock }
|
||||
export { ErrorMessageBlockNode } from './node'
|
||||
export { default as ErrorMessageBlockReplacementBlock } from './error-message-block-replacement-block'
|
||||
@ -0,0 +1,67 @@
|
||||
import type { LexicalNode, NodeKey, SerializedLexicalNode } from 'lexical'
|
||||
import { DecoratorNode } from 'lexical'
|
||||
import ErrorMessageBlockComponent from './component'
|
||||
|
||||
export type SerializedNode = SerializedLexicalNode
|
||||
|
||||
export class ErrorMessageBlockNode extends DecoratorNode<React.JSX.Element> {
|
||||
static getType(): string {
|
||||
return 'error-message-block'
|
||||
}
|
||||
|
||||
static clone(node: ErrorMessageBlockNode): ErrorMessageBlockNode {
|
||||
return new ErrorMessageBlockNode(node.getKey())
|
||||
}
|
||||
|
||||
isInline(): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
constructor(key?: NodeKey) {
|
||||
super(key)
|
||||
}
|
||||
|
||||
createDOM(): HTMLElement {
|
||||
const div = document.createElement('div')
|
||||
div.classList.add('inline-flex', 'items-center', 'align-middle')
|
||||
return div
|
||||
}
|
||||
|
||||
updateDOM(): false {
|
||||
return false
|
||||
}
|
||||
|
||||
decorate(): React.JSX.Element {
|
||||
return (
|
||||
<ErrorMessageBlockComponent
|
||||
nodeKey={this.getKey()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
static importJSON(): ErrorMessageBlockNode {
|
||||
const node = $createErrorMessageBlockNode()
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
exportJSON(): SerializedNode {
|
||||
return {
|
||||
type: 'error-message-block',
|
||||
version: 1,
|
||||
}
|
||||
}
|
||||
|
||||
getTextContent(): string {
|
||||
return '{{#error_message#}}'
|
||||
}
|
||||
}
|
||||
export function $createErrorMessageBlockNode(): ErrorMessageBlockNode {
|
||||
return new ErrorMessageBlockNode()
|
||||
}
|
||||
|
||||
export function $isErrorMessageBlockNode(
|
||||
node: ErrorMessageBlockNode | LexicalNode | null | undefined,
|
||||
): boolean {
|
||||
return node instanceof ErrorMessageBlockNode
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
import { type FC, useEffect } from 'react'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { useSelectOrDelete } from '../../hooks'
|
||||
import { DELETE_LAST_RUN_COMMAND, LastRunBlockNode } from '.'
|
||||
import cn from '@/utils/classnames'
|
||||
import { Variable02 } from '../../../icons/src/vender/solid/development'
|
||||
|
||||
type Props = {
|
||||
nodeKey: string
|
||||
}
|
||||
|
||||
const LastRunBlockComponent: FC<Props> = ({
|
||||
nodeKey,
|
||||
}) => {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_LAST_RUN_COMMAND)
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor.hasNodes([LastRunBlockNode]))
|
||||
throw new Error('WorkflowVariableBlockPlugin: WorkflowVariableBlock not registered on editor')
|
||||
}, [editor])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'group/wrap relative mx-0.5 flex h-[18px] select-none items-center rounded-[5px] border pl-0.5 pr-[3px] text-text-accent hover:border-state-accent-solid hover:bg-state-accent-hover',
|
||||
isSelected ? ' border-state-accent-solid bg-state-accent-hover' : ' border-components-panel-border-subtle bg-components-badge-white-to-dark',
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
}}
|
||||
ref={ref}
|
||||
>
|
||||
<Variable02 className='mr-0.5 h-[14px] w-[14px]' />
|
||||
<div className='text-xs font-medium'>last_run</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default LastRunBlockComponent
|
||||
@ -0,0 +1,65 @@
|
||||
import {
|
||||
memo,
|
||||
useEffect,
|
||||
} from 'react'
|
||||
import {
|
||||
$insertNodes,
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
createCommand,
|
||||
} from 'lexical'
|
||||
import { mergeRegister } from '@lexical/utils'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import type { LastRunBlockType } from '../../types'
|
||||
import {
|
||||
$createLastRunBlockNode,
|
||||
LastRunBlockNode,
|
||||
} from './node'
|
||||
|
||||
export const INSERT_LAST_RUN_BLOCK_COMMAND = createCommand('INSERT_LAST_RUN_BLOCK_COMMAND')
|
||||
export const DELETE_LAST_RUN_COMMAND = createCommand('DELETE_LAST_RUN_COMMAND')
|
||||
|
||||
const LastRunBlock = memo(({
|
||||
onInsert,
|
||||
onDelete,
|
||||
}: LastRunBlockType) => {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor.hasNodes([LastRunBlockNode]))
|
||||
throw new Error('Last_RunBlockPlugin: Last_RunBlock not registered on editor')
|
||||
|
||||
return mergeRegister(
|
||||
editor.registerCommand(
|
||||
INSERT_LAST_RUN_BLOCK_COMMAND,
|
||||
() => {
|
||||
const Node = $createLastRunBlockNode()
|
||||
|
||||
$insertNodes([Node])
|
||||
|
||||
if (onInsert)
|
||||
onInsert()
|
||||
|
||||
return true
|
||||
},
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
),
|
||||
editor.registerCommand(
|
||||
DELETE_LAST_RUN_COMMAND,
|
||||
() => {
|
||||
if (onDelete)
|
||||
onDelete()
|
||||
|
||||
return true
|
||||
},
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
),
|
||||
)
|
||||
}, [editor, onDelete, onInsert])
|
||||
|
||||
return null
|
||||
})
|
||||
LastRunBlock.displayName = 'LastRunBlock'
|
||||
|
||||
export { LastRunBlock }
|
||||
export { LastRunBlockNode } from './node'
|
||||
export { default as LastRunReplacementBlock } from './last-run-block-replacement-block'
|
||||
@ -0,0 +1,61 @@
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
} from 'react'
|
||||
import { $applyNodeReplacement } from 'lexical'
|
||||
import { mergeRegister } from '@lexical/utils'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { decoratorTransform } from '../../utils'
|
||||
import { LAST_RUN_PLACEHOLDER_TEXT } from '../../constants'
|
||||
import type { LastRunBlockType } from '../../types'
|
||||
import {
|
||||
$createLastRunBlockNode,
|
||||
LastRunBlockNode,
|
||||
} from './node'
|
||||
import { CustomTextNode } from '../custom-text/node'
|
||||
|
||||
const REGEX = new RegExp(LAST_RUN_PLACEHOLDER_TEXT)
|
||||
|
||||
const LastRunReplacementBlock = ({
|
||||
onInsert,
|
||||
}: LastRunBlockType) => {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor.hasNodes([LastRunBlockNode]))
|
||||
throw new Error('LastRunMessageBlockNodePlugin: LastRunMessageBlockNode not registered on editor')
|
||||
}, [editor])
|
||||
|
||||
const createLastRunBlockNode = useCallback((): LastRunBlockNode => {
|
||||
if (onInsert)
|
||||
onInsert()
|
||||
return $applyNodeReplacement($createLastRunBlockNode())
|
||||
}, [onInsert])
|
||||
|
||||
const getMatch = useCallback((text: string) => {
|
||||
const matchArr = REGEX.exec(text)
|
||||
|
||||
if (matchArr === null)
|
||||
return null
|
||||
|
||||
const startOffset = matchArr.index
|
||||
const endOffset = startOffset + LAST_RUN_PLACEHOLDER_TEXT.length
|
||||
return {
|
||||
end: endOffset,
|
||||
start: startOffset,
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
REGEX.lastIndex = 0
|
||||
return mergeRegister(
|
||||
editor.registerNodeTransform(CustomTextNode, textNode => decoratorTransform(textNode, getMatch, createLastRunBlockNode)),
|
||||
)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export default memo(LastRunReplacementBlock)
|
||||
@ -0,0 +1,67 @@
|
||||
import type { LexicalNode, NodeKey, SerializedLexicalNode } from 'lexical'
|
||||
import { DecoratorNode } from 'lexical'
|
||||
import LastRunBlockComponent from './component'
|
||||
|
||||
export type SerializedNode = SerializedLexicalNode
|
||||
|
||||
export class LastRunBlockNode extends DecoratorNode<React.JSX.Element> {
|
||||
static getType(): string {
|
||||
return 'last-run-block'
|
||||
}
|
||||
|
||||
static clone(node: LastRunBlockNode): LastRunBlockNode {
|
||||
return new LastRunBlockNode(node.getKey())
|
||||
}
|
||||
|
||||
isInline(): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
constructor(key?: NodeKey) {
|
||||
super(key)
|
||||
}
|
||||
|
||||
createDOM(): HTMLElement {
|
||||
const div = document.createElement('div')
|
||||
div.classList.add('inline-flex', 'items-center', 'align-middle')
|
||||
return div
|
||||
}
|
||||
|
||||
updateDOM(): false {
|
||||
return false
|
||||
}
|
||||
|
||||
decorate(): React.JSX.Element {
|
||||
return (
|
||||
<LastRunBlockComponent
|
||||
nodeKey={this.getKey()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
static importJSON(): LastRunBlockNode {
|
||||
const node = $createLastRunBlockNode()
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
exportJSON(): SerializedNode {
|
||||
return {
|
||||
type: 'last-run-block',
|
||||
version: 1,
|
||||
}
|
||||
}
|
||||
|
||||
getTextContent(): string {
|
||||
return '{{#last_run#}}'
|
||||
}
|
||||
}
|
||||
export function $createLastRunBlockNode(): LastRunBlockNode {
|
||||
return new LastRunBlockNode()
|
||||
}
|
||||
|
||||
export function $isLastRunBlockNode(
|
||||
node: LastRunBlockNode | LexicalNode | null | undefined,
|
||||
): boolean {
|
||||
return node instanceof LastRunBlockNode
|
||||
}
|
||||
@ -1,3 +1,4 @@
|
||||
import type { GeneratorType } from '../../app/configuration/config/automatic/types'
|
||||
import type { Type } from '../../workflow/nodes/llm/types'
|
||||
import type { Dataset } from './plugins/context-block'
|
||||
import type { RoleName } from './plugins/history-block'
|
||||
@ -75,3 +76,22 @@ export type MenuTextMatch = {
|
||||
matchingString: string
|
||||
replaceableString: string
|
||||
}
|
||||
|
||||
export type CurrentBlockType = {
|
||||
show?: boolean
|
||||
generatorType: GeneratorType
|
||||
onInsert?: () => void
|
||||
onDelete?: () => void
|
||||
}
|
||||
|
||||
export type ErrorMessageBlockType = {
|
||||
show?: boolean
|
||||
onInsert?: () => void
|
||||
onDelete?: () => void
|
||||
}
|
||||
|
||||
export type LastRunBlockType = {
|
||||
show?: boolean
|
||||
onInsert?: () => void
|
||||
onDelete?: () => void
|
||||
}
|
||||
|
||||
@ -259,7 +259,7 @@ function getFullMatchOffset(
|
||||
): number {
|
||||
let triggerOffset = offset
|
||||
for (let i = triggerOffset; i <= entryText.length; i++) {
|
||||
if (documentText.substr(-i) === entryText.substr(0, i))
|
||||
if (documentText.slice(-i) === entryText.slice(0, i))
|
||||
triggerOffset = i
|
||||
}
|
||||
return triggerOffset
|
||||
|
||||
@ -192,7 +192,6 @@ const SimpleSelect: FC<ISelectProps> = ({
|
||||
const localPlaceholder = placeholder || t('common.placeholder.select')
|
||||
|
||||
const [selectedItem, setSelectedItem] = useState<Item | null>(null)
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
let defaultSelect = null
|
||||
@ -215,88 +214,83 @@ const SimpleSelect: FC<ISelectProps> = ({
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className={classNames('group/simple-select relative h-9', wrapperClassName)}>
|
||||
{renderTrigger && <ListboxButton className='w-full'>{renderTrigger(selectedItem)}</ListboxButton>}
|
||||
{!renderTrigger && (
|
||||
<ListboxButton onClick={() => {
|
||||
// get data-open, use setTimeout to ensure the attribute is set
|
||||
setTimeout(() => {
|
||||
if (listboxRef.current) {
|
||||
const isOpen = listboxRef.current.getAttribute('data-open') !== null
|
||||
setOpen(isOpen)
|
||||
onOpenChange?.(isOpen)
|
||||
}
|
||||
})
|
||||
}} className={classNames(`flex h-full w-full items-center rounded-lg border-0 bg-components-input-bg-normal pl-3 pr-10 focus-visible:bg-state-base-hover-alt focus-visible:outline-none group-hover/simple-select:bg-state-base-hover-alt sm:text-sm sm:leading-6 ${disabled ? 'cursor-not-allowed' : 'cursor-pointer'}`, className)}>
|
||||
<span className={classNames('system-sm-regular block truncate text-left text-components-input-text-filled', !selectedItem?.name && 'text-components-input-text-placeholder')}>{selectedItem?.name ?? localPlaceholder}</span>
|
||||
<span className="absolute inset-y-0 right-0 flex items-center pr-2">
|
||||
{isLoading ? <RiLoader4Line className='h-3.5 w-3.5 animate-spin text-text-secondary' />
|
||||
: (selectedItem && !notClearable)
|
||||
? (
|
||||
<XMarkIcon
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setSelectedItem(null)
|
||||
onSelect({ name: '', value: '' })
|
||||
}}
|
||||
className="h-4 w-4 cursor-pointer text-text-quaternary"
|
||||
aria-hidden="false"
|
||||
/>
|
||||
)
|
||||
: (
|
||||
open ? (
|
||||
<ChevronUpIcon
|
||||
className="h-4 w-4 text-text-quaternary group-hover/simple-select:text-text-secondary"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : (
|
||||
<ChevronDownIcon
|
||||
className="h-4 w-4 text-text-quaternary group-hover/simple-select:text-text-secondary"
|
||||
aria-hidden="true"
|
||||
{({ open }) => (
|
||||
<div className={classNames('group/simple-select relative h-9', wrapperClassName)}>
|
||||
{renderTrigger && <ListboxButton className='w-full'>{renderTrigger(selectedItem)}</ListboxButton>}
|
||||
{!renderTrigger && (
|
||||
<ListboxButton onClick={() => {
|
||||
onOpenChange?.(open)
|
||||
}} className={classNames(`flex h-full w-full items-center rounded-lg border-0 bg-components-input-bg-normal pl-3 pr-10 focus-visible:bg-state-base-hover-alt focus-visible:outline-none group-hover/simple-select:bg-state-base-hover-alt sm:text-sm sm:leading-6 ${disabled ? 'cursor-not-allowed' : 'cursor-pointer'}`, className)}>
|
||||
<span className={classNames('system-sm-regular block truncate text-left text-components-input-text-filled', !selectedItem?.name && 'text-components-input-text-placeholder')}>{selectedItem?.name ?? localPlaceholder}</span>
|
||||
<span className="absolute inset-y-0 right-0 flex items-center pr-2">
|
||||
{isLoading ? <RiLoader4Line className='h-3.5 w-3.5 animate-spin text-text-secondary' />
|
||||
: (selectedItem && !notClearable)
|
||||
? (
|
||||
<XMarkIcon
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setSelectedItem(null)
|
||||
onSelect({ name: '', value: '' })
|
||||
}}
|
||||
className="h-4 w-4 cursor-pointer text-text-quaternary"
|
||||
aria-hidden="false"
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</span>
|
||||
</ListboxButton>
|
||||
)}
|
||||
: (
|
||||
open ? (
|
||||
<ChevronUpIcon
|
||||
className="h-4 w-4 text-text-quaternary group-hover/simple-select:text-text-secondary"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : (
|
||||
<ChevronDownIcon
|
||||
className="h-4 w-4 text-text-quaternary group-hover/simple-select:text-text-secondary"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</span>
|
||||
</ListboxButton>
|
||||
)}
|
||||
|
||||
{(!disabled) && (
|
||||
<ListboxOptions className={classNames('absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur px-1 py-1 text-base shadow-lg backdrop-blur-sm focus:outline-none sm:text-sm', optionWrapClassName)}>
|
||||
{items.map((item: Item) => (
|
||||
<ListboxOption
|
||||
key={item.value}
|
||||
className={
|
||||
classNames(
|
||||
'relative cursor-pointer select-none rounded-lg py-2 pl-3 pr-9 text-text-secondary hover:bg-state-base-hover',
|
||||
optionClassName,
|
||||
)
|
||||
}
|
||||
value={item}
|
||||
disabled={disabled}
|
||||
>
|
||||
{({ /* active, */ selected }) => (
|
||||
<>
|
||||
{renderOption
|
||||
? renderOption({ item, selected })
|
||||
: (<>
|
||||
<span className={classNames('block', selected && 'font-normal')}>{item.name}</span>
|
||||
{selected && !hideChecked && (
|
||||
<span
|
||||
className={classNames(
|
||||
'absolute inset-y-0 right-0 flex items-center pr-4 text-text-accent',
|
||||
)}
|
||||
>
|
||||
<RiCheckLine className="h-4 w-4" aria-hidden="true" />
|
||||
</span>
|
||||
)}
|
||||
</>)}
|
||||
</>
|
||||
)}
|
||||
</ListboxOption>
|
||||
))}
|
||||
</ListboxOptions>
|
||||
)}
|
||||
</div>
|
||||
{(!disabled) && (
|
||||
<ListboxOptions className={classNames('absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur px-1 py-1 text-base shadow-lg backdrop-blur-sm focus:outline-none sm:text-sm', optionWrapClassName)}>
|
||||
{items.map((item: Item) => (
|
||||
<ListboxOption
|
||||
key={item.value}
|
||||
className={
|
||||
classNames(
|
||||
'relative cursor-pointer select-none rounded-lg py-2 pl-3 pr-9 text-text-secondary hover:bg-state-base-hover',
|
||||
optionClassName,
|
||||
)
|
||||
}
|
||||
value={item}
|
||||
disabled={disabled}
|
||||
>
|
||||
{({ /* active, */ selected }) => (
|
||||
<>
|
||||
{renderOption
|
||||
? renderOption({ item, selected })
|
||||
: (<>
|
||||
<span className={classNames('block', selected && 'font-normal')}>{item.name}</span>
|
||||
{selected && !hideChecked && (
|
||||
<span
|
||||
className={classNames(
|
||||
'absolute inset-y-0 right-0 flex items-center pr-4 text-text-accent',
|
||||
)}
|
||||
>
|
||||
<RiCheckLine className="h-4 w-4" aria-hidden="true" />
|
||||
</span>
|
||||
)}
|
||||
</>)}
|
||||
</>
|
||||
)}
|
||||
</ListboxOption>
|
||||
))}
|
||||
</ListboxOptions>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Listbox>
|
||||
)
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@ const Tag = ({ text, className }: { text: string; className?: string }) => {
|
||||
return (
|
||||
<div className={cn('inline-flex items-center gap-x-0.5', className)}>
|
||||
<span className='text-xs font-medium text-text-quaternary'>#</span>
|
||||
<span className='line-clamp-1 max-w-12 shrink-0 text-xs text-text-tertiary'>{text}</span>
|
||||
<span className='max-w-12 shrink-0 truncate text-xs text-text-tertiary'>{text}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -301,6 +301,7 @@ export const OperationAction: FC<{
|
||||
<Tooltip
|
||||
popupContent={t('datasetDocuments.list.action.download')}
|
||||
popupClassName='text-text-secondary system-xs-medium'
|
||||
needsDelay={false}
|
||||
>
|
||||
<button
|
||||
className={cn('mr-2 cursor-pointer rounded-lg',
|
||||
@ -311,9 +312,9 @@ export const OperationAction: FC<{
|
||||
downloadDocument.mutateAsync({
|
||||
datasetId,
|
||||
documentId: detail.id,
|
||||
}).then((response) => {
|
||||
if (response.download_url)
|
||||
window.location.href = response.download_url
|
||||
}).then((response) => {
|
||||
if (response.download_url)
|
||||
window.location.href = response.download_url
|
||||
}).catch((error) => {
|
||||
console.error(error)
|
||||
notify({ type: 'error', message: t('common.actionMsg.downloadFailed') })
|
||||
@ -326,6 +327,7 @@ export const OperationAction: FC<{
|
||||
<Tooltip
|
||||
popupContent={t('datasetDocuments.list.action.settings')}
|
||||
popupClassName='text-text-secondary system-xs-medium'
|
||||
needsDelay={false}
|
||||
>
|
||||
<button
|
||||
className={cn('mr-2 cursor-pointer rounded-lg',
|
||||
@ -526,7 +528,7 @@ const DocumentList: FC<IDocumentListProps> = ({
|
||||
const result = aValue.localeCompare(bValue)
|
||||
return sortOrder === 'asc' ? result : -result
|
||||
}
|
||||
else {
|
||||
else {
|
||||
const result = aValue - bValue
|
||||
return sortOrder === 'asc' ? result : -result
|
||||
}
|
||||
@ -539,7 +541,7 @@ const DocumentList: FC<IDocumentListProps> = ({
|
||||
if (sortField === field) {
|
||||
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')
|
||||
}
|
||||
else {
|
||||
else {
|
||||
setSortField(field)
|
||||
setSortOrder('desc')
|
||||
}
|
||||
|
||||
@ -137,7 +137,7 @@ const HitTestingPage: FC<Props> = ({ datasetId }: Props) => {
|
||||
<>
|
||||
<div className='grow overflow-y-auto'>
|
||||
<table className={'w-full border-collapse border-0 text-[13px] leading-4 text-text-secondary '}>
|
||||
<thead className='sticky top-0 h-7 text-xs font-medium uppercase leading-7 text-text-tertiary'>
|
||||
<thead className='sticky top-0 h-7 text-xs font-medium uppercase leading-7 text-text-tertiary backdrop-blur-[5px]'>
|
||||
<tr>
|
||||
<td className='w-[128px] rounded-l-lg bg-background-section-burn pl-3'>{t('datasetHitTesting.table.header.source')}</td>
|
||||
<td className='bg-background-section-burn'>{t('datasetHitTesting.table.header.text')}</td>
|
||||
|
||||
@ -36,7 +36,7 @@ const Category: FC<ICategoryProps> = ({
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={cn(className, 'flex flex-wrap space-x-1 text-[13px]')}>
|
||||
<div className={cn(className, 'flex flex-wrap gap-1 text-[13px]')}>
|
||||
<div
|
||||
className={itemClassName(isAllCategories)}
|
||||
onClick={() => onChange(allCategoriesEn)}
|
||||
|
||||
@ -2,11 +2,11 @@ export type CommandHandler = (args?: Record<string, any>) => void | Promise<void
|
||||
|
||||
const handlers = new Map<string, CommandHandler>()
|
||||
|
||||
export const registerCommand = (name: string, handler: CommandHandler) => {
|
||||
const registerCommand = (name: string, handler: CommandHandler) => {
|
||||
handlers.set(name, handler)
|
||||
}
|
||||
|
||||
export const unregisterCommand = (name: string) => {
|
||||
const unregisterCommand = (name: string) => {
|
||||
handlers.delete(name)
|
||||
}
|
||||
|
||||
15
web/app/components/goto-anything/actions/commands/index.ts
Normal file
15
web/app/components/goto-anything/actions/commands/index.ts
Normal file
@ -0,0 +1,15 @@
|
||||
// Command system exports
|
||||
export { slashAction } from './slash'
|
||||
export { registerSlashCommands, unregisterSlashCommands, SlashCommandProvider } from './slash'
|
||||
|
||||
// Command registry system (for extending with custom commands)
|
||||
export { slashCommandRegistry, SlashCommandRegistry } from './registry'
|
||||
export type { SlashCommandHandler } from './types'
|
||||
|
||||
// Command bus (for extending with custom commands)
|
||||
export {
|
||||
executeCommand,
|
||||
registerCommands,
|
||||
unregisterCommands,
|
||||
type CommandHandler,
|
||||
} from './command-bus'
|
||||
@ -0,0 +1,53 @@
|
||||
import type { SlashCommandHandler } from './types'
|
||||
import type { CommandSearchResult } from '../types'
|
||||
import { languages } from '@/i18n-config/language'
|
||||
import i18n from '@/i18n-config/i18next-config'
|
||||
import { registerCommands, unregisterCommands } from './command-bus'
|
||||
|
||||
// Language dependency types
|
||||
type LanguageDeps = {
|
||||
setLocale?: (locale: string) => Promise<void>
|
||||
}
|
||||
|
||||
const buildLanguageCommands = (query: string): CommandSearchResult[] => {
|
||||
const q = query.toLowerCase()
|
||||
const list = languages.filter(item => item.supported && (
|
||||
!q || item.name.toLowerCase().includes(q) || String(item.value).toLowerCase().includes(q)
|
||||
))
|
||||
return list.map(item => ({
|
||||
id: `lang-${item.value}`,
|
||||
title: item.name,
|
||||
description: i18n.t('app.gotoAnything.actions.languageChangeDesc'),
|
||||
type: 'command' as const,
|
||||
data: { command: 'i18n.set', args: { locale: item.value } },
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Language command handler
|
||||
* Integrates UI building, search, and registration logic
|
||||
*/
|
||||
export const languageCommand: SlashCommandHandler<LanguageDeps> = {
|
||||
name: 'language',
|
||||
aliases: ['lang'],
|
||||
description: 'Switch between different languages',
|
||||
|
||||
async search(args: string, _locale: string = 'en') {
|
||||
// Return language options directly, regardless of parameters
|
||||
return buildLanguageCommands(args)
|
||||
},
|
||||
|
||||
register(deps: LanguageDeps) {
|
||||
registerCommands({
|
||||
'i18n.set': async (args) => {
|
||||
const locale = args?.locale
|
||||
if (locale)
|
||||
await deps.setLocale?.(locale)
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
unregister() {
|
||||
unregisterCommands(['i18n.set'])
|
||||
},
|
||||
}
|
||||
233
web/app/components/goto-anything/actions/commands/registry.ts
Normal file
233
web/app/components/goto-anything/actions/commands/registry.ts
Normal file
@ -0,0 +1,233 @@
|
||||
import type { SlashCommandHandler } from './types'
|
||||
import type { CommandSearchResult } from '../types'
|
||||
|
||||
/**
|
||||
* Slash Command Registry System
|
||||
* Responsible for managing registration, lookup, and search of all slash commands
|
||||
*/
|
||||
export class SlashCommandRegistry {
|
||||
private commands = new Map<string, SlashCommandHandler>()
|
||||
private commandDeps = new Map<string, any>()
|
||||
|
||||
/**
|
||||
* Register command handler
|
||||
*/
|
||||
register<TDeps = any>(handler: SlashCommandHandler<TDeps>, deps?: TDeps) {
|
||||
// Register main command name
|
||||
this.commands.set(handler.name, handler)
|
||||
|
||||
// Register aliases
|
||||
if (handler.aliases) {
|
||||
handler.aliases.forEach((alias) => {
|
||||
this.commands.set(alias, handler)
|
||||
})
|
||||
}
|
||||
|
||||
// Store dependencies and call registration method
|
||||
if (deps) {
|
||||
this.commandDeps.set(handler.name, deps)
|
||||
handler.register?.(deps)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister command
|
||||
*/
|
||||
unregister(name: string) {
|
||||
const handler = this.commands.get(name)
|
||||
if (handler) {
|
||||
// Call the command's unregister method
|
||||
handler.unregister?.()
|
||||
|
||||
// Remove dependencies
|
||||
this.commandDeps.delete(handler.name)
|
||||
|
||||
// Remove main command name
|
||||
this.commands.delete(handler.name)
|
||||
|
||||
// Remove all aliases
|
||||
if (handler.aliases) {
|
||||
handler.aliases.forEach((alias) => {
|
||||
this.commands.delete(alias)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find command handler
|
||||
*/
|
||||
findCommand(commandName: string): SlashCommandHandler | undefined {
|
||||
return this.commands.get(commandName)
|
||||
}
|
||||
|
||||
/**
|
||||
* Smart partial command matching
|
||||
* Prioritize alias matching, then match command name prefix
|
||||
*/
|
||||
private findBestPartialMatch(partialName: string): SlashCommandHandler | undefined {
|
||||
const lowerPartial = partialName.toLowerCase()
|
||||
|
||||
// First check if any alias starts with this
|
||||
const aliasMatch = this.findHandlerByAliasPrefix(lowerPartial)
|
||||
if (aliasMatch)
|
||||
return aliasMatch
|
||||
|
||||
// Then check if command name starts with this
|
||||
return this.findHandlerByNamePrefix(lowerPartial)
|
||||
}
|
||||
|
||||
/**
|
||||
* Find handler by alias prefix
|
||||
*/
|
||||
private findHandlerByAliasPrefix(prefix: string): SlashCommandHandler | undefined {
|
||||
for (const handler of this.getAllCommands()) {
|
||||
if (handler.aliases?.some(alias => alias.toLowerCase().startsWith(prefix)))
|
||||
return handler
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Find handler by name prefix
|
||||
*/
|
||||
private findHandlerByNamePrefix(prefix: string): SlashCommandHandler | undefined {
|
||||
return this.getAllCommands().find(handler =>
|
||||
handler.name.toLowerCase().startsWith(prefix),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered commands (deduplicated)
|
||||
*/
|
||||
getAllCommands(): SlashCommandHandler[] {
|
||||
const uniqueCommands = new Map<string, SlashCommandHandler>()
|
||||
this.commands.forEach((handler) => {
|
||||
uniqueCommands.set(handler.name, handler)
|
||||
})
|
||||
return Array.from(uniqueCommands.values())
|
||||
}
|
||||
|
||||
/**
|
||||
* Search commands
|
||||
* @param query Full query (e.g., "/theme dark" or "/lang en")
|
||||
* @param locale Current language
|
||||
*/
|
||||
async search(query: string, locale: string = 'en'): Promise<CommandSearchResult[]> {
|
||||
const trimmed = query.trim()
|
||||
|
||||
// Handle root level search "/"
|
||||
if (trimmed === '/' || !trimmed.replace('/', '').trim())
|
||||
return await this.getRootCommands()
|
||||
|
||||
// Parse command and arguments
|
||||
const afterSlash = trimmed.substring(1).trim()
|
||||
const spaceIndex = afterSlash.indexOf(' ')
|
||||
const commandName = spaceIndex === -1 ? afterSlash : afterSlash.substring(0, spaceIndex)
|
||||
const args = spaceIndex === -1 ? '' : afterSlash.substring(spaceIndex + 1).trim()
|
||||
|
||||
// First try exact match
|
||||
let handler = this.findCommand(commandName)
|
||||
if (handler) {
|
||||
try {
|
||||
return await handler.search(args, locale)
|
||||
}
|
||||
catch (error) {
|
||||
console.warn(`Command search failed for ${commandName}:`, error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
// If no exact match, try smart partial matching
|
||||
handler = this.findBestPartialMatch(commandName)
|
||||
if (handler) {
|
||||
try {
|
||||
return await handler.search(args, locale)
|
||||
}
|
||||
catch (error) {
|
||||
console.warn(`Command search failed for ${handler.name}:`, error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
// Finally perform fuzzy search
|
||||
return this.fuzzySearchCommands(afterSlash)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get root level command list
|
||||
*/
|
||||
private async getRootCommands(): Promise<CommandSearchResult[]> {
|
||||
const results: CommandSearchResult[] = []
|
||||
|
||||
// Generate a root level item for each command
|
||||
for (const handler of this.getAllCommands()) {
|
||||
results.push({
|
||||
id: `root-${handler.name}`,
|
||||
title: `/${handler.name}`,
|
||||
description: handler.description,
|
||||
type: 'command' as const,
|
||||
data: {
|
||||
command: `root.${handler.name}`,
|
||||
args: { name: handler.name },
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* Fuzzy search commands
|
||||
*/
|
||||
private fuzzySearchCommands(query: string): CommandSearchResult[] {
|
||||
const lowercaseQuery = query.toLowerCase()
|
||||
const matches: CommandSearchResult[] = []
|
||||
|
||||
this.getAllCommands().forEach((handler) => {
|
||||
// Check if command name matches
|
||||
if (handler.name.toLowerCase().includes(lowercaseQuery)) {
|
||||
matches.push({
|
||||
id: `fuzzy-${handler.name}`,
|
||||
title: `/${handler.name}`,
|
||||
description: handler.description,
|
||||
type: 'command' as const,
|
||||
data: {
|
||||
command: `root.${handler.name}`,
|
||||
args: { name: handler.name },
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Check if aliases match
|
||||
if (handler.aliases) {
|
||||
handler.aliases.forEach((alias) => {
|
||||
if (alias.toLowerCase().includes(lowercaseQuery)) {
|
||||
matches.push({
|
||||
id: `fuzzy-${alias}`,
|
||||
title: `/${alias}`,
|
||||
description: `${handler.description} (alias for /${handler.name})`,
|
||||
type: 'command' as const,
|
||||
data: {
|
||||
command: `root.${handler.name}`,
|
||||
args: { name: handler.name },
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return matches
|
||||
}
|
||||
|
||||
/**
|
||||
* Get command dependencies
|
||||
*/
|
||||
getCommandDependencies(commandName: string): any {
|
||||
return this.commandDeps.get(commandName)
|
||||
}
|
||||
}
|
||||
|
||||
// Global registry instance
|
||||
export const slashCommandRegistry = new SlashCommandRegistry()
|
||||
52
web/app/components/goto-anything/actions/commands/slash.tsx
Normal file
52
web/app/components/goto-anything/actions/commands/slash.tsx
Normal file
@ -0,0 +1,52 @@
|
||||
'use client'
|
||||
import { useEffect } from 'react'
|
||||
import type { ActionItem } from '../types'
|
||||
import { slashCommandRegistry } from './registry'
|
||||
import { executeCommand } from './command-bus'
|
||||
import { useTheme } from 'next-themes'
|
||||
import { setLocaleOnClient } from '@/i18n-config'
|
||||
import { themeCommand } from './theme'
|
||||
import { languageCommand } from './language'
|
||||
import i18n from '@/i18n-config/i18next-config'
|
||||
|
||||
export const slashAction: ActionItem = {
|
||||
key: '/',
|
||||
shortcut: '/',
|
||||
title: i18n.t('app.gotoAnything.actions.slashTitle'),
|
||||
description: i18n.t('app.gotoAnything.actions.slashDesc'),
|
||||
action: (result) => {
|
||||
if (result.type !== 'command') return
|
||||
const { command, args } = result.data
|
||||
executeCommand(command, args)
|
||||
},
|
||||
search: async (query, _searchTerm = '') => {
|
||||
// Delegate all search logic to the command registry system
|
||||
return slashCommandRegistry.search(query, i18n.language)
|
||||
},
|
||||
}
|
||||
|
||||
// Register/unregister default handlers for slash commands with external dependencies.
|
||||
export const registerSlashCommands = (deps: Record<string, any>) => {
|
||||
// Register command handlers to the registry system with their respective dependencies
|
||||
slashCommandRegistry.register(themeCommand, { setTheme: deps.setTheme })
|
||||
slashCommandRegistry.register(languageCommand, { setLocale: deps.setLocale })
|
||||
}
|
||||
|
||||
export const unregisterSlashCommands = () => {
|
||||
// Remove command handlers from registry system (automatically calls each command's unregister method)
|
||||
slashCommandRegistry.unregister('theme')
|
||||
slashCommandRegistry.unregister('language')
|
||||
}
|
||||
|
||||
export const SlashCommandProvider = () => {
|
||||
const theme = useTheme()
|
||||
useEffect(() => {
|
||||
registerSlashCommands({
|
||||
setTheme: theme.setTheme,
|
||||
setLocale: setLocaleOnClient,
|
||||
})
|
||||
return () => unregisterSlashCommands()
|
||||
}, [theme.setTheme])
|
||||
|
||||
return null
|
||||
}
|
||||
@ -1,7 +1,15 @@
|
||||
import type { CommandSearchResult } from './types'
|
||||
import type { SlashCommandHandler } from './types'
|
||||
import type { CommandSearchResult } from '../types'
|
||||
import type { ReactNode } from 'react'
|
||||
import { RiComputerLine, RiMoonLine, RiPaletteLine, RiSunLine } from '@remixicon/react'
|
||||
import React from 'react'
|
||||
import { RiComputerLine, RiMoonLine, RiSunLine } from '@remixicon/react'
|
||||
import i18n from '@/i18n-config/i18next-config'
|
||||
import { registerCommands, unregisterCommands } from './command-bus'
|
||||
|
||||
// Theme dependency types
|
||||
type ThemeDeps = {
|
||||
setTheme?: (value: 'light' | 'dark' | 'system') => void
|
||||
}
|
||||
|
||||
const THEME_ITEMS: { id: 'light' | 'dark' | 'system'; titleKey: string; descKey: string; icon: ReactNode }[] = [
|
||||
{
|
||||
@ -24,7 +32,7 @@ const THEME_ITEMS: { id: 'light' | 'dark' | 'system'; titleKey: string; descKey:
|
||||
},
|
||||
]
|
||||
|
||||
export const buildThemeCommands = (query: string, locale?: string): CommandSearchResult[] => {
|
||||
const buildThemeCommands = (query: string, locale?: string): CommandSearchResult[] => {
|
||||
const q = query.toLowerCase()
|
||||
const list = THEME_ITEMS.filter(item =>
|
||||
!q
|
||||
@ -45,17 +53,28 @@ export const buildThemeCommands = (query: string, locale?: string): CommandSearc
|
||||
}))
|
||||
}
|
||||
|
||||
export const buildThemeRootItem = (): CommandSearchResult => {
|
||||
return {
|
||||
id: 'category-theme',
|
||||
title: i18n.t('app.gotoAnything.actions.themeCategoryTitle'),
|
||||
description: i18n.t('app.gotoAnything.actions.themeCategoryDesc'),
|
||||
type: 'command',
|
||||
icon: (
|
||||
<div className='flex h-6 w-6 items-center justify-center rounded-md border-[0.5px] border-divider-regular bg-components-panel-bg'>
|
||||
<RiPaletteLine className='h-4 w-4 text-text-tertiary' />
|
||||
</div>
|
||||
),
|
||||
data: { command: 'nav.search', args: { query: '@run theme ' } },
|
||||
}
|
||||
/**
|
||||
* Theme command handler
|
||||
* Integrates UI building, search, and registration logic
|
||||
*/
|
||||
export const themeCommand: SlashCommandHandler<ThemeDeps> = {
|
||||
name: 'theme',
|
||||
description: 'Switch between light and dark themes',
|
||||
|
||||
async search(args: string, locale: string = 'en') {
|
||||
// Return theme options directly, regardless of parameters
|
||||
return buildThemeCommands(args, locale)
|
||||
},
|
||||
|
||||
register(deps: ThemeDeps) {
|
||||
registerCommands({
|
||||
'theme.set': async (args) => {
|
||||
deps.setTheme?.(args?.value)
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
unregister() {
|
||||
unregisterCommands(['theme.set'])
|
||||
},
|
||||
}
|
||||
33
web/app/components/goto-anything/actions/commands/types.ts
Normal file
33
web/app/components/goto-anything/actions/commands/types.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import type { CommandSearchResult } from '../types'
|
||||
|
||||
/**
|
||||
* Slash command handler interface
|
||||
* Each slash command should implement this interface
|
||||
*/
|
||||
export type SlashCommandHandler<TDeps = any> = {
|
||||
/** Command name (e.g., 'theme', 'language') */
|
||||
name: string
|
||||
|
||||
/** Command alias list (e.g., ['lang'] for language) */
|
||||
aliases?: string[]
|
||||
|
||||
/** Command description */
|
||||
description: string
|
||||
|
||||
/**
|
||||
* Search command results
|
||||
* @param args Command arguments (part after removing command name)
|
||||
* @param locale Current language
|
||||
*/
|
||||
search: (args: string, locale?: string) => Promise<CommandSearchResult[]>
|
||||
|
||||
/**
|
||||
* Called when registering command, passing external dependencies
|
||||
*/
|
||||
register?: (deps: TDeps) => void
|
||||
|
||||
/**
|
||||
* Called when unregistering command
|
||||
*/
|
||||
unregister?: () => void
|
||||
}
|
||||
@ -1,15 +1,180 @@
|
||||
/**
|
||||
* Goto Anything - Action System
|
||||
*
|
||||
* This file defines the action registry for the goto-anything search system.
|
||||
* Actions handle different types of searches: apps, knowledge bases, plugins, workflow nodes, and commands.
|
||||
*
|
||||
* ## How to Add a New Slash Command
|
||||
*
|
||||
* 1. **Create Command Handler File** (in `./commands/` directory):
|
||||
* ```typescript
|
||||
* // commands/my-command.ts
|
||||
* import type { SlashCommandHandler } from './types'
|
||||
* import type { CommandSearchResult } from '../types'
|
||||
* import { registerCommands, unregisterCommands } from './command-bus'
|
||||
*
|
||||
* interface MyCommandDeps {
|
||||
* myService?: (data: any) => Promise<void>
|
||||
* }
|
||||
*
|
||||
* export const myCommand: SlashCommandHandler<MyCommandDeps> = {
|
||||
* name: 'mycommand',
|
||||
* aliases: ['mc'], // Optional aliases
|
||||
* description: 'My custom command description',
|
||||
*
|
||||
* async search(args: string, locale: string = 'en') {
|
||||
* // Return search results based on args
|
||||
* return [{
|
||||
* id: 'my-result',
|
||||
* title: 'My Command Result',
|
||||
* description: 'Description of the result',
|
||||
* type: 'command' as const,
|
||||
* data: { command: 'my.action', args: { value: args } }
|
||||
* }]
|
||||
* },
|
||||
*
|
||||
* register(deps: MyCommandDeps) {
|
||||
* registerCommands({
|
||||
* 'my.action': async (args) => {
|
||||
* await deps.myService?.(args?.value)
|
||||
* }
|
||||
* })
|
||||
* },
|
||||
*
|
||||
* unregister() {
|
||||
* unregisterCommands(['my.action'])
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* **Example for Self-Contained Command (no external dependencies):**
|
||||
* ```typescript
|
||||
* // commands/calculator-command.ts
|
||||
* export const calculatorCommand: SlashCommandHandler = {
|
||||
* name: 'calc',
|
||||
* aliases: ['calculator'],
|
||||
* description: 'Simple calculator',
|
||||
*
|
||||
* async search(args: string) {
|
||||
* if (!args.trim()) return []
|
||||
* try {
|
||||
* // Safe math evaluation (implement proper parser in real use)
|
||||
* const result = Function('"use strict"; return (' + args + ')')()
|
||||
* return [{
|
||||
* id: 'calc-result',
|
||||
* title: `${args} = ${result}`,
|
||||
* description: 'Calculator result',
|
||||
* type: 'command' as const,
|
||||
* data: { command: 'calc.copy', args: { result: result.toString() } }
|
||||
* }]
|
||||
* } catch {
|
||||
* return [{
|
||||
* id: 'calc-error',
|
||||
* title: 'Invalid expression',
|
||||
* description: 'Please enter a valid math expression',
|
||||
* type: 'command' as const,
|
||||
* data: { command: 'calc.noop', args: {} }
|
||||
* }]
|
||||
* }
|
||||
* },
|
||||
*
|
||||
* register() {
|
||||
* registerCommands({
|
||||
* 'calc.copy': (args) => navigator.clipboard.writeText(args.result),
|
||||
* 'calc.noop': () => {} // No operation
|
||||
* })
|
||||
* },
|
||||
*
|
||||
* unregister() {
|
||||
* unregisterCommands(['calc.copy', 'calc.noop'])
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* 2. **Register Command** (in `./commands/slash.tsx`):
|
||||
* ```typescript
|
||||
* import { myCommand } from './my-command'
|
||||
* import { calculatorCommand } from './calculator-command' // For self-contained commands
|
||||
*
|
||||
* export const registerSlashCommands = (deps: Record<string, any>) => {
|
||||
* slashCommandRegistry.register(themeCommand, { setTheme: deps.setTheme })
|
||||
* slashCommandRegistry.register(languageCommand, { setLocale: deps.setLocale })
|
||||
* slashCommandRegistry.register(myCommand, { myService: deps.myService }) // With dependencies
|
||||
* slashCommandRegistry.register(calculatorCommand) // Self-contained, no dependencies
|
||||
* }
|
||||
*
|
||||
* export const unregisterSlashCommands = () => {
|
||||
* slashCommandRegistry.unregister('theme')
|
||||
* slashCommandRegistry.unregister('language')
|
||||
* slashCommandRegistry.unregister('mycommand')
|
||||
* slashCommandRegistry.unregister('calc') // Add this line
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* 3. **Update SlashCommandProvider** (in `./commands/slash.tsx`):
|
||||
* ```typescript
|
||||
* export const SlashCommandProvider = () => {
|
||||
* const theme = useTheme()
|
||||
* const myService = useMyService() // Add external dependency if needed
|
||||
*
|
||||
* useEffect(() => {
|
||||
* registerSlashCommands({
|
||||
* setTheme: theme.setTheme, // Required for theme command
|
||||
* setLocale: setLocaleOnClient, // Required for language command
|
||||
* myService: myService, // Required for your custom command
|
||||
* // Note: calculatorCommand doesn't need dependencies, so not listed here
|
||||
* })
|
||||
* return () => unregisterSlashCommands()
|
||||
* }, [theme.setTheme, myService]) // Update dependency array for all dynamic deps
|
||||
*
|
||||
* return null
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* **Note:** Self-contained commands (like calculator) don't require dependencies but are
|
||||
* still registered through the same system for consistent lifecycle management.
|
||||
*
|
||||
* 4. **Usage**: Users can now type `/mycommand` or `/mc` to use your command
|
||||
*
|
||||
* ## Command System Architecture
|
||||
* - Commands are registered via `SlashCommandRegistry`
|
||||
* - Each command is self-contained with its own dependencies
|
||||
* - Commands support aliases for easier access
|
||||
* - Command execution is handled by the command bus system
|
||||
* - All commands should be registered through `SlashCommandProvider` for consistent lifecycle management
|
||||
*
|
||||
* ## Command Types
|
||||
* **Commands with External Dependencies:**
|
||||
* - Require external services, APIs, or React hooks
|
||||
* - Must provide dependencies in `SlashCommandProvider`
|
||||
* - Example: theme commands (needs useTheme), API commands (needs service)
|
||||
*
|
||||
* **Self-Contained Commands:**
|
||||
* - Pure logic operations, no external dependencies
|
||||
* - Still recommended to register through `SlashCommandProvider` for consistency
|
||||
* - Example: calculator, text manipulation commands
|
||||
*
|
||||
* ## Available Actions
|
||||
* - `@app` - Search applications
|
||||
* - `@knowledge` / `@kb` - Search knowledge bases
|
||||
* - `@plugin` - Search plugins
|
||||
* - `@node` - Search workflow nodes (workflow pages only)
|
||||
* - `/` - Execute slash commands (theme, language, etc.)
|
||||
*/
|
||||
|
||||
import { appAction } from './app'
|
||||
import { knowledgeAction } from './knowledge'
|
||||
import { pluginAction } from './plugin'
|
||||
import { workflowNodesAction } from './workflow-nodes'
|
||||
import type { ActionItem, SearchResult } from './types'
|
||||
import { commandAction } from './run'
|
||||
import { slashAction } from './commands'
|
||||
|
||||
export const Actions = {
|
||||
slash: slashAction,
|
||||
app: appAction,
|
||||
knowledge: knowledgeAction,
|
||||
plugin: pluginAction,
|
||||
run: commandAction,
|
||||
node: workflowNodesAction,
|
||||
}
|
||||
|
||||
@ -29,11 +194,13 @@ export const searchAnything = async (
|
||||
}
|
||||
}
|
||||
|
||||
if (query.startsWith('@'))
|
||||
if (query.startsWith('@') || query.startsWith('/'))
|
||||
return []
|
||||
|
||||
const globalSearchActions = Object.values(Actions)
|
||||
|
||||
// Use Promise.allSettled to handle partial failures gracefully
|
||||
const searchPromises = Object.values(Actions).map(async (action) => {
|
||||
const searchPromises = globalSearchActions.map(async (action) => {
|
||||
try {
|
||||
const results = await action.search(query, query, locale)
|
||||
return { success: true, data: results, actionType: action.key }
|
||||
@ -54,7 +221,7 @@ export const searchAnything = async (
|
||||
allResults.push(...result.value.data)
|
||||
}
|
||||
else {
|
||||
const actionKey = Object.values(Actions)[index]?.key || 'unknown'
|
||||
const actionKey = globalSearchActions[index]?.key || 'unknown'
|
||||
failedActions.push(actionKey)
|
||||
}
|
||||
})
|
||||
@ -67,10 +234,15 @@ export const searchAnything = async (
|
||||
|
||||
export const matchAction = (query: string, actions: Record<string, ActionItem>) => {
|
||||
return Object.values(actions).find((action) => {
|
||||
// Special handling for slash commands to allow direct /theme, /lang
|
||||
if (action.key === '/')
|
||||
return query.startsWith('/')
|
||||
|
||||
const reg = new RegExp(`^(${action.key}|${action.shortcut})(?:\\s|$)`)
|
||||
return reg.test(query)
|
||||
})
|
||||
}
|
||||
|
||||
export * from './types'
|
||||
export * from './commands'
|
||||
export { appAction, knowledgeAction, pluginAction, workflowNodesAction }
|
||||
|
||||
@ -1,33 +0,0 @@
|
||||
import type { CommandSearchResult } from './types'
|
||||
import { languages } from '@/i18n-config/language'
|
||||
import { RiTranslate } from '@remixicon/react'
|
||||
import i18n from '@/i18n-config/i18next-config'
|
||||
|
||||
export const buildLanguageCommands = (query: string): CommandSearchResult[] => {
|
||||
const q = query.toLowerCase()
|
||||
const list = languages.filter(item => item.supported && (
|
||||
!q || item.name.toLowerCase().includes(q) || String(item.value).toLowerCase().includes(q)
|
||||
))
|
||||
return list.map(item => ({
|
||||
id: `lang-${item.value}`,
|
||||
title: item.name,
|
||||
description: i18n.t('app.gotoAnything.actions.languageChangeDesc'),
|
||||
type: 'command' as const,
|
||||
data: { command: 'i18n.set', args: { locale: item.value } },
|
||||
}))
|
||||
}
|
||||
|
||||
export const buildLanguageRootItem = (): CommandSearchResult => {
|
||||
return {
|
||||
id: 'category-language',
|
||||
title: i18n.t('app.gotoAnything.actions.languageCategoryTitle'),
|
||||
description: i18n.t('app.gotoAnything.actions.languageCategoryDesc'),
|
||||
type: 'command',
|
||||
icon: (
|
||||
<div className='flex h-6 w-6 items-center justify-center rounded-md border-[0.5px] border-divider-regular bg-components-panel-bg'>
|
||||
<RiTranslate className='h-4 w-4 text-text-tertiary' />
|
||||
</div>
|
||||
),
|
||||
data: { command: 'nav.search', args: { query: '@run language ' } },
|
||||
}
|
||||
}
|
||||
@ -1,97 +0,0 @@
|
||||
'use client'
|
||||
import { useEffect } from 'react'
|
||||
import type { ActionItem, CommandSearchResult } from './types'
|
||||
import { buildLanguageCommands, buildLanguageRootItem } from './run-language'
|
||||
import { buildThemeCommands, buildThemeRootItem } from './run-theme'
|
||||
import i18n from '@/i18n-config/i18next-config'
|
||||
import { executeCommand, registerCommands, unregisterCommands } from './command-bus'
|
||||
import { useTheme } from 'next-themes'
|
||||
import { setLocaleOnClient } from '@/i18n-config'
|
||||
|
||||
const rootParser = (query: string): CommandSearchResult[] => {
|
||||
const q = query.toLowerCase()
|
||||
const items: CommandSearchResult[] = []
|
||||
if (!q || 'theme'.includes(q))
|
||||
items.push(buildThemeRootItem())
|
||||
if (!q || 'language'.includes(q) || 'lang'.includes(q))
|
||||
items.push(buildLanguageRootItem())
|
||||
return items
|
||||
}
|
||||
|
||||
type RunContext = {
|
||||
setTheme?: (value: 'light' | 'dark' | 'system') => void
|
||||
setLocale?: (locale: string) => Promise<void>
|
||||
search?: (query: string) => void
|
||||
}
|
||||
|
||||
export const commandAction: ActionItem = {
|
||||
key: '@run',
|
||||
shortcut: '@run',
|
||||
title: i18n.t('app.gotoAnything.actions.runTitle'),
|
||||
description: i18n.t('app.gotoAnything.actions.runDesc'),
|
||||
action: (result) => {
|
||||
if (result.type !== 'command') return
|
||||
const { command, args } = result.data
|
||||
if (command === 'theme.set') {
|
||||
executeCommand('theme.set', args)
|
||||
return
|
||||
}
|
||||
if (command === 'i18n.set') {
|
||||
executeCommand('i18n.set', args)
|
||||
return
|
||||
}
|
||||
if (command === 'nav.search')
|
||||
executeCommand('nav.search', args)
|
||||
},
|
||||
search: async (_, searchTerm = '') => {
|
||||
const q = searchTerm.trim()
|
||||
if (q.startsWith('theme'))
|
||||
return buildThemeCommands(q.replace(/^theme\s*/, ''), i18n.language)
|
||||
if (q.startsWith('language') || q.startsWith('lang'))
|
||||
return buildLanguageCommands(q.replace(/^(language|lang)\s*/, ''))
|
||||
|
||||
// root categories
|
||||
return rootParser(q)
|
||||
},
|
||||
}
|
||||
|
||||
// Register/unregister default handlers for @run commands with external dependencies.
|
||||
export const registerRunCommands = (deps: {
|
||||
setTheme?: (value: 'light' | 'dark' | 'system') => void
|
||||
setLocale?: (locale: string) => Promise<void>
|
||||
search?: (query: string) => void
|
||||
}) => {
|
||||
registerCommands({
|
||||
'theme.set': async (args) => {
|
||||
deps.setTheme?.(args?.value)
|
||||
},
|
||||
'i18n.set': async (args) => {
|
||||
const locale = args?.locale
|
||||
if (locale)
|
||||
await deps.setLocale?.(locale)
|
||||
},
|
||||
'nav.search': (args) => {
|
||||
const q = args?.query
|
||||
if (q)
|
||||
deps.search?.(q)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const unregisterRunCommands = () => {
|
||||
unregisterCommands(['theme.set', 'i18n.set', 'nav.search'])
|
||||
}
|
||||
|
||||
export const RunCommandProvider = ({ onNavSearch }: { onNavSearch?: (q: string) => void }) => {
|
||||
const theme = useTheme()
|
||||
useEffect(() => {
|
||||
registerRunCommands({
|
||||
setTheme: theme.setTheme,
|
||||
setLocale: setLocaleOnClient,
|
||||
search: onNavSearch,
|
||||
})
|
||||
return () => unregisterRunCommands()
|
||||
}, [theme.setTheme, onNavSearch])
|
||||
|
||||
return null
|
||||
}
|
||||
@ -44,7 +44,7 @@ export type CommandSearchResult = {
|
||||
export type SearchResult = AppSearchResult | PluginSearchResult | KnowledgeSearchResult | WorkflowNodeSearchResult | CommandSearchResult
|
||||
|
||||
export type ActionItem = {
|
||||
key: '@app' | '@knowledge' | '@plugin' | '@node' | '@run'
|
||||
key: '@app' | '@knowledge' | '@plugin' | '@node' | '/'
|
||||
shortcut: string
|
||||
title: string | TypeWithI18N
|
||||
description: string
|
||||
|
||||
@ -20,7 +20,6 @@ const CommandSelector: FC<Props> = ({ actions, onCommandSelect, searchFilter, co
|
||||
return true
|
||||
const filterLower = searchFilter.toLowerCase()
|
||||
return action.shortcut.toLowerCase().includes(filterLower)
|
||||
|| action.key.toLowerCase().includes(filterLower)
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
@ -61,7 +60,7 @@ const CommandSelector: FC<Props> = ({ actions, onCommandSelect, searchFilter, co
|
||||
className="flex cursor-pointer items-center rounded-md
|
||||
p-2.5
|
||||
transition-all
|
||||
duration-150 hover:bg-state-base-hover-alt aria-[selected=true]:bg-state-base-hover"
|
||||
duration-150 hover:bg-state-base-hover aria-[selected=true]:bg-state-base-hover-alt"
|
||||
onSelect={() => onCommandSelect(action.shortcut)}
|
||||
>
|
||||
<span className="min-w-[4.5rem] text-left font-mono text-xs text-text-tertiary">
|
||||
@ -70,10 +69,10 @@ const CommandSelector: FC<Props> = ({ actions, onCommandSelect, searchFilter, co
|
||||
<span className="ml-3 text-sm text-text-secondary">
|
||||
{(() => {
|
||||
const keyMap: Record<string, string> = {
|
||||
'/': 'app.gotoAnything.actions.slashDesc',
|
||||
'@app': 'app.gotoAnything.actions.searchApplicationsDesc',
|
||||
'@plugin': 'app.gotoAnything.actions.searchPluginsDesc',
|
||||
'@knowledge': 'app.gotoAnything.actions.searchKnowledgeBasesDesc',
|
||||
'@run': 'app.gotoAnything.actions.runDesc',
|
||||
'@node': 'app.gotoAnything.actions.searchWorkflowNodesDesc',
|
||||
}
|
||||
return t(keyMap[action.key])
|
||||
|
||||
@ -18,7 +18,7 @@ import InstallFromMarketplace from '../plugins/install-plugin/install-from-marke
|
||||
import type { Plugin } from '../plugins/types'
|
||||
import { Command } from 'cmdk'
|
||||
import CommandSelector from './command-selector'
|
||||
import { RunCommandProvider } from './actions/run'
|
||||
import { SlashCommandProvider } from './actions/commands'
|
||||
|
||||
type Props = {
|
||||
onHide?: () => void
|
||||
@ -32,13 +32,9 @@ const GotoAnything: FC<Props> = ({
|
||||
const { t } = useTranslation()
|
||||
const [show, setShow] = useState<boolean>(false)
|
||||
const [searchQuery, setSearchQuery] = useState<string>('')
|
||||
const [cmdVal, setCmdVal] = useState<string>('')
|
||||
const [cmdVal, setCmdVal] = useState<string>('_')
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
const handleNavSearch = useCallback((q: string) => {
|
||||
setShow(true)
|
||||
setSearchQuery(q)
|
||||
requestAnimationFrame(() => inputRef.current?.focus())
|
||||
}, [])
|
||||
|
||||
// Filter actions based on context
|
||||
const Actions = useMemo(() => {
|
||||
// Create a filtered copy of actions based on current page context
|
||||
@ -47,9 +43,8 @@ const GotoAnything: FC<Props> = ({
|
||||
return AllActions
|
||||
}
|
||||
else {
|
||||
// Exclude node action on non-workflow pages
|
||||
const { app, knowledge, plugin, run } = AllActions
|
||||
return { app, knowledge, plugin, run }
|
||||
const { app, knowledge, plugin, slash } = AllActions
|
||||
return { app, knowledge, plugin, slash }
|
||||
}
|
||||
}, [isWorkflowPage])
|
||||
|
||||
@ -87,14 +82,18 @@ const GotoAnything: FC<Props> = ({
|
||||
wait: 300,
|
||||
})
|
||||
|
||||
const isCommandsMode = searchQuery.trim() === '@' || (searchQuery.trim().startsWith('@') && !matchAction(searchQuery.trim(), Actions))
|
||||
const isCommandsMode = searchQuery.trim() === '@' || searchQuery.trim() === '/'
|
||||
|| (searchQuery.trim().startsWith('@') && !matchAction(searchQuery.trim(), Actions))
|
||||
|| (searchQuery.trim().startsWith('/') && !matchAction(searchQuery.trim(), Actions))
|
||||
|
||||
const searchMode = useMemo(() => {
|
||||
if (isCommandsMode) return 'commands'
|
||||
|
||||
const query = searchQueryDebouncedValue.toLowerCase()
|
||||
const action = matchAction(query, Actions)
|
||||
return action ? action.key : 'general'
|
||||
return action
|
||||
? (action.key === '/' ? '@command' : action.key)
|
||||
: 'general'
|
||||
}, [searchQueryDebouncedValue, Actions, isCommandsMode])
|
||||
|
||||
const { data: searchResults = [], isLoading, isError, error } = useQuery(
|
||||
@ -119,9 +118,14 @@ const GotoAnything: FC<Props> = ({
|
||||
},
|
||||
)
|
||||
|
||||
// Prevent automatic selection of the first option when cmdVal is not set
|
||||
const clearSelection = () => {
|
||||
setCmdVal('_')
|
||||
}
|
||||
|
||||
const handleCommandSelect = useCallback((commandKey: string) => {
|
||||
setSearchQuery(`${commandKey} `)
|
||||
setCmdVal('')
|
||||
clearSelection()
|
||||
setTimeout(() => {
|
||||
inputRef.current?.focus()
|
||||
}, 0)
|
||||
@ -134,7 +138,8 @@ const GotoAnything: FC<Props> = ({
|
||||
|
||||
switch (result.type) {
|
||||
case 'command': {
|
||||
const action = Object.values(Actions).find(a => a.key === '@run')
|
||||
// Execute slash commands
|
||||
const action = Actions.slash
|
||||
action?.action?.(result)
|
||||
break
|
||||
}
|
||||
@ -202,7 +207,7 @@ const GotoAnything: FC<Props> = ({
|
||||
</div>
|
||||
<div className='mt-1 text-xs text-text-quaternary'>
|
||||
{isCommandSearch
|
||||
? t('app.gotoAnything.emptyState.tryDifferentTerm', { mode: searchMode })
|
||||
? t('app.gotoAnything.emptyState.tryDifferentTerm')
|
||||
: t('app.gotoAnything.emptyState.trySpecificSearch', { shortcuts: Object.values(Actions).map(action => action.shortcut).join(', ') })
|
||||
}
|
||||
</div>
|
||||
@ -232,18 +237,17 @@ const GotoAnything: FC<Props> = ({
|
||||
inputRef.current?.focus()
|
||||
})
|
||||
}
|
||||
return () => {
|
||||
setCmdVal('')
|
||||
}
|
||||
}, [show])
|
||||
|
||||
return (
|
||||
<>
|
||||
<SlashCommandProvider />
|
||||
<Modal
|
||||
isShow={show}
|
||||
onClose={() => {
|
||||
setShow(false)
|
||||
setSearchQuery('')
|
||||
clearSelection()
|
||||
onHide?.()
|
||||
}}
|
||||
closable={false}
|
||||
@ -266,8 +270,8 @@ const GotoAnything: FC<Props> = ({
|
||||
placeholder={t('app.gotoAnything.searchPlaceholder')}
|
||||
onChange={(e) => {
|
||||
setSearchQuery(e.target.value)
|
||||
if (!e.target.value.startsWith('@'))
|
||||
setCmdVal('')
|
||||
if (!e.target.value.startsWith('@') && !e.target.value.startsWith('/'))
|
||||
clearSelection()
|
||||
}}
|
||||
className='flex-1 !border-0 !bg-transparent !shadow-none'
|
||||
wrapperClassName='flex-1 !border-0 !bg-transparent'
|
||||
@ -320,40 +324,41 @@ const GotoAnything: FC<Props> = ({
|
||||
/>
|
||||
) : (
|
||||
Object.entries(groupedResults).map(([type, results], groupIndex) => (
|
||||
<Command.Group key={groupIndex} heading={(() => {
|
||||
const typeMap: Record<string, string> = {
|
||||
'app': 'app.gotoAnything.groups.apps',
|
||||
'plugin': 'app.gotoAnything.groups.plugins',
|
||||
'knowledge': 'app.gotoAnything.groups.knowledgeBases',
|
||||
'workflow-node': 'app.gotoAnything.groups.workflowNodes',
|
||||
}
|
||||
return t(typeMap[type] || `${type}s`)
|
||||
})()} className='p-2 capitalize text-text-secondary'>
|
||||
{results.map(result => (
|
||||
<Command.Item
|
||||
key={`${result.type}-${result.id}`}
|
||||
value={result.title}
|
||||
className='flex cursor-pointer items-center gap-3 rounded-md p-3 will-change-[background-color] hover:bg-state-base-hover-alt aria-[selected=true]:bg-state-base-hover data-[selected=true]:bg-state-base-hover'
|
||||
onSelect={() => handleNavigate(result)}
|
||||
>
|
||||
{result.icon}
|
||||
<div className='min-w-0 flex-1'>
|
||||
<div className='truncate font-medium text-text-secondary'>
|
||||
{result.title}
|
||||
</div>
|
||||
{result.description && (
|
||||
<div className='mt-0.5 truncate text-xs text-text-quaternary'>
|
||||
{result.description}
|
||||
<Command.Group key={groupIndex} heading={(() => {
|
||||
const typeMap: Record<string, string> = {
|
||||
'app': 'app.gotoAnything.groups.apps',
|
||||
'plugin': 'app.gotoAnything.groups.plugins',
|
||||
'knowledge': 'app.gotoAnything.groups.knowledgeBases',
|
||||
'workflow-node': 'app.gotoAnything.groups.workflowNodes',
|
||||
'command': 'app.gotoAnything.groups.commands',
|
||||
}
|
||||
return t(typeMap[type] || `${type}s`)
|
||||
})()} className='p-2 capitalize text-text-secondary'>
|
||||
{results.map(result => (
|
||||
<Command.Item
|
||||
key={`${result.type}-${result.id}`}
|
||||
value={result.title}
|
||||
className='flex cursor-pointer items-center gap-3 rounded-md p-3 will-change-[background-color] aria-[selected=true]:bg-state-base-hover data-[selected=true]:bg-state-base-hover'
|
||||
onSelect={() => handleNavigate(result)}
|
||||
>
|
||||
{result.icon}
|
||||
<div className='min-w-0 flex-1'>
|
||||
<div className='truncate font-medium text-text-secondary'>
|
||||
{result.title}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className='text-xs capitalize text-text-quaternary'>
|
||||
{result.type}
|
||||
</div>
|
||||
</Command.Item>
|
||||
))}
|
||||
</Command.Group>
|
||||
))
|
||||
{result.description && (
|
||||
<div className='mt-0.5 truncate text-xs text-text-quaternary'>
|
||||
{result.description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className='text-xs capitalize text-text-quaternary'>
|
||||
{result.type}
|
||||
</div>
|
||||
</Command.Item>
|
||||
))}
|
||||
</Command.Group>
|
||||
))
|
||||
)}
|
||||
{!isCommandsMode && emptyResult}
|
||||
{!isCommandsMode && defaultUI}
|
||||
@ -372,7 +377,7 @@ const GotoAnything: FC<Props> = ({
|
||||
{t('app.gotoAnything.resultCount', { count: searchResults.length })}
|
||||
{searchMode !== 'general' && (
|
||||
<span className='ml-2 opacity-60'>
|
||||
{t('app.gotoAnything.inScope', { scope: searchMode.replace('@', '') })}
|
||||
{t('app.gotoAnything.inScope', { scope: searchMode.replace('@', '') })}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
@ -391,7 +396,6 @@ const GotoAnything: FC<Props> = ({
|
||||
</div>
|
||||
|
||||
</Modal>
|
||||
<RunCommandProvider onNavSearch={handleNavSearch} />
|
||||
{
|
||||
activePlugin && (
|
||||
<InstallFromMarketplace
|
||||
|
||||
@ -42,7 +42,7 @@ export default function Support() {
|
||||
>
|
||||
<MenuItems
|
||||
className={cn(
|
||||
`absolute top-[1px] z-10 max-h-[70vh] w-[216px] origin-top-right -translate-x-full divide-y divide-divider-subtle overflow-y-scroll
|
||||
`absolute top-[1px] z-10 max-h-[70vh] w-[216px] origin-top-right -translate-x-full divide-y divide-divider-subtle overflow-y-auto
|
||||
rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-[5px] focus:outline-none
|
||||
`,
|
||||
)}
|
||||
|
||||
@ -5,7 +5,7 @@ import type { GithubRepo } from '@/models/common'
|
||||
import { RiLoader2Line } from '@remixicon/react'
|
||||
|
||||
const defaultData = {
|
||||
stargazers_count: 98570,
|
||||
stargazers_count: 110918,
|
||||
}
|
||||
|
||||
const getStar = async () => {
|
||||
|
||||
@ -45,6 +45,7 @@ import { convertUTCDaySecondsToLocalSeconds, timeOfDayToDayjs } from '../referen
|
||||
import useReferenceSetting from '../plugin-page/use-reference-setting'
|
||||
import { AUTO_UPDATE_MODE } from '../reference-setting-modal/auto-update-setting/types'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
|
||||
const i18nPrefix = 'plugin.action'
|
||||
|
||||
@ -69,6 +70,7 @@ const DetailHeader = ({
|
||||
const { setShowUpdatePluginModal } = useModalContext()
|
||||
const { refreshModelProviders } = useProviderContext()
|
||||
const invalidateAllToolProviders = useInvalidateAllToolProviders()
|
||||
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
|
||||
|
||||
const {
|
||||
installation_id,
|
||||
@ -122,6 +124,8 @@ const DetailHeader = ({
|
||||
const { referenceSetting } = useReferenceSetting()
|
||||
const { auto_upgrade: autoUpgradeInfo } = referenceSetting || {}
|
||||
const isAutoUpgradeEnabled = useMemo(() => {
|
||||
if (!enable_marketplace)
|
||||
return false
|
||||
if (!autoUpgradeInfo || !isFromMarketplace)
|
||||
return false
|
||||
if(autoUpgradeInfo.strategy_setting === 'disabled')
|
||||
|
||||
@ -10,6 +10,7 @@ import { PermissionType } from '@/app/components/plugins/types'
|
||||
import type { AutoUpdateConfig } from './auto-update-setting/types'
|
||||
import AutoUpdateSetting from './auto-update-setting'
|
||||
import { defaultValue as autoUpdateDefaultValue } from './auto-update-setting/config'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import Label from './label'
|
||||
|
||||
const i18nPrefix = 'plugin.privilege'
|
||||
@ -28,6 +29,7 @@ const PluginSettingModal: FC<Props> = ({
|
||||
const { auto_upgrade: autoUpdateConfig, permission: privilege } = payload || {}
|
||||
const [tempPrivilege, setTempPrivilege] = useState<Permissions>(privilege)
|
||||
const [tempAutoUpdateConfig, setTempAutoUpdateConfig] = useState<AutoUpdateConfig>(autoUpdateConfig || autoUpdateDefaultValue)
|
||||
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
|
||||
const handlePrivilegeChange = useCallback((key: string) => {
|
||||
return (value: PermissionType) => {
|
||||
setTempPrivilege({
|
||||
@ -77,8 +79,11 @@ const PluginSettingModal: FC<Props> = ({
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<AutoUpdateSetting payload={tempAutoUpdateConfig} onChange={setTempAutoUpdateConfig} />
|
||||
{
|
||||
enable_marketplace && (
|
||||
<AutoUpdateSetting payload={tempAutoUpdateConfig} onChange={setTempAutoUpdateConfig} />
|
||||
)
|
||||
}
|
||||
<div className='flex h-[76px] items-center justify-end gap-2 self-stretch p-6 pt-5'>
|
||||
<Button
|
||||
className='min-w-[72px]'
|
||||
|
||||
@ -79,6 +79,9 @@ const SwrInitializer = ({
|
||||
<SWRConfig value={{
|
||||
shouldRetryOnError: false,
|
||||
revalidateOnFocus: false,
|
||||
dedupingInterval: 60000,
|
||||
focusThrottleInterval: 5000,
|
||||
provider: () => new Map(),
|
||||
}}>
|
||||
{children}
|
||||
</SWRConfig>
|
||||
|
||||
15
web/app/components/tools/labels/store.ts
Normal file
15
web/app/components/tools/labels/store.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { create } from 'zustand'
|
||||
import type { Label } from './constant'
|
||||
|
||||
type State = {
|
||||
labelList: Label[]
|
||||
}
|
||||
|
||||
type Action = {
|
||||
setLabelList: (labelList?: Label[]) => void
|
||||
}
|
||||
|
||||
export const useStore = create<State & Action>(set => ({
|
||||
labelList: [],
|
||||
setLabelList: labelList => set(() => ({ labelList })),
|
||||
}))
|
||||
@ -3,7 +3,7 @@ import {
|
||||
useCallback,
|
||||
useMemo,
|
||||
} from 'react'
|
||||
import { useStore as useReactflowStore } from 'reactflow'
|
||||
import { useEdges, useNodes, useStore as useReactflowStore } from 'reactflow'
|
||||
import { RiApps2AddLine } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
@ -11,6 +11,7 @@ import {
|
||||
useWorkflowStore,
|
||||
} from '@/app/components/workflow/store'
|
||||
import {
|
||||
useChecklist,
|
||||
useChecklistBeforePublish,
|
||||
useNodesReadOnly,
|
||||
useNodesSyncDraft,
|
||||
@ -18,6 +19,10 @@ import {
|
||||
import Button from '@/app/components/base/button'
|
||||
import AppPublisher from '@/app/components/app/app-publisher'
|
||||
import { useFeatures } from '@/app/components/base/features/hooks'
|
||||
import type {
|
||||
CommonEdgeType,
|
||||
CommonNodeType,
|
||||
} from '@/app/components/workflow/types'
|
||||
import {
|
||||
BlockEnum,
|
||||
InputVarType,
|
||||
@ -92,8 +97,19 @@ const FeaturesTrigger = () => {
|
||||
}
|
||||
}, [appID, setAppDetail])
|
||||
const { mutateAsync: publishWorkflow } = usePublishWorkflow(appID!)
|
||||
const nodes = useNodes<CommonNodeType>()
|
||||
const edges = useEdges<CommonEdgeType>()
|
||||
const needWarningNodes = useChecklist(nodes, edges)
|
||||
|
||||
const updatePublishedWorkflow = useInvalidateAppWorkflow()
|
||||
const onPublish = useCallback(async (params?: PublishWorkflowParams) => {
|
||||
// First check if there are any items in the checklist
|
||||
if (needWarningNodes.length > 0) {
|
||||
notify({ type: 'error', message: t('workflow.panel.checklistTip') })
|
||||
throw new Error('Checklist has unresolved items')
|
||||
}
|
||||
|
||||
// Then perform the detailed validation
|
||||
if (await handleCheckBeforePublish()) {
|
||||
const res = await publishWorkflow({
|
||||
title: params?.title || '',
|
||||
@ -111,7 +127,7 @@ const FeaturesTrigger = () => {
|
||||
else {
|
||||
throw new Error('Checklist failed')
|
||||
}
|
||||
}, [handleCheckBeforePublish, publishWorkflow, notify, t, updatePublishedWorkflow, appID, updateAppDetail, workflowStore, resetWorkflowVersionHistory])
|
||||
}, [needWarningNodes, handleCheckBeforePublish, publishWorkflow, notify, t, updatePublishedWorkflow, appID, updateAppDetail, workflowStore, resetWorkflowVersionHistory])
|
||||
|
||||
const onPublisherToggle = useCallback((state: boolean) => {
|
||||
if (state)
|
||||
|
||||
@ -5,6 +5,7 @@ export const useConfigsMap = () => {
|
||||
const appId = useStore(s => s.appId)
|
||||
return useMemo(() => {
|
||||
return {
|
||||
flowId: appId,
|
||||
conversationVarsUrl: `apps/${appId}/workflows/draft/conversation-variables`,
|
||||
systemVarsUrl: `apps/${appId}/workflows/draft/system-variables`,
|
||||
}
|
||||
|
||||
@ -35,7 +35,6 @@ export const useWorkflowRun = () => {
|
||||
const invalidAllLastRun = useInvalidAllLastRun(appId as string)
|
||||
const configsMap = useConfigsMap()
|
||||
const { fetchInspectVars } = useSetWorkflowVarsWithValue({
|
||||
flowId: appId as string,
|
||||
...configsMap,
|
||||
})
|
||||
|
||||
|
||||
@ -49,6 +49,7 @@ type CommonHooksFnMap = {
|
||||
resetConversationVar: (varId: string) => Promise<void>
|
||||
invalidateConversationVarValues: () => void
|
||||
configsMap?: {
|
||||
flowId: string
|
||||
conversationVarsUrl: string
|
||||
systemVarsUrl: string
|
||||
}
|
||||
|
||||
@ -10,6 +10,7 @@ import type {
|
||||
} from '@/app/components/workflow/types'
|
||||
import { useIsChatMode } from './use-workflow'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import type { Type } from '../nodes/llm/types'
|
||||
|
||||
export const useWorkflowVariables = () => {
|
||||
const { t } = useTranslation()
|
||||
@ -106,7 +107,7 @@ export const useWorkflowVariableType = () => {
|
||||
isChatMode,
|
||||
isConstant: false,
|
||||
})
|
||||
return type
|
||||
return type as unknown as Type
|
||||
}
|
||||
|
||||
return getVarType
|
||||
|
||||
@ -259,11 +259,11 @@ export const useWorkflow = () => {
|
||||
|
||||
const handleOutVarRenameChange = useCallback((nodeId: string, oldValeSelector: ValueSelector, newVarSelector: ValueSelector) => {
|
||||
const { getNodes, setNodes } = store.getState()
|
||||
const afterNodes = getAfterNodesInSameBranch(nodeId)
|
||||
const effectNodes = findUsedVarNodes(oldValeSelector, afterNodes)
|
||||
if (effectNodes.length > 0) {
|
||||
const newNodes = getNodes().map((node) => {
|
||||
if (effectNodes.find(n => n.id === node.id))
|
||||
const allNodes = getNodes()
|
||||
const affectedNodes = findUsedVarNodes(oldValeSelector, allNodes)
|
||||
if (affectedNodes.length > 0) {
|
||||
const newNodes = allNodes.map((node) => {
|
||||
if (affectedNodes.find(n => n.id === node.id))
|
||||
return updateNodeVars(node, oldValeSelector, newVarSelector)
|
||||
|
||||
return node
|
||||
|
||||
@ -87,6 +87,7 @@ export const AgentStrategy = memo((props: AgentStrategyProps) => {
|
||||
headerClassName='bg-transparent px-0 text-text-secondary system-sm-semibold-uppercase'
|
||||
containerBackgroundClassName='bg-transparent'
|
||||
gradientBorder={false}
|
||||
nodeId={nodeId}
|
||||
isSupportPromptGenerator={!!def.auto_generate?.type}
|
||||
titleTooltip={schema.tooltip && renderI18nObject(schema.tooltip)}
|
||||
editorContainerClassName='px-0'
|
||||
|
||||
@ -7,25 +7,32 @@ import type { CodeLanguage } from '../../code/types'
|
||||
import { Generator } from '@/app/components/base/icons/src/vender/other'
|
||||
import { ActionButton } from '@/app/components/base/action-button'
|
||||
import { AppType } from '@/types/app'
|
||||
import type { CodeGenRes } from '@/service/debug'
|
||||
import type { GenRes } from '@/service/debug'
|
||||
import { GetCodeGeneratorResModal } from '@/app/components/app/configuration/config/code-generator/get-code-generator-res'
|
||||
import { useHooksStore } from '../../../hooks-store'
|
||||
|
||||
type Props = {
|
||||
nodeId: string
|
||||
currentCode?: string
|
||||
className?: string
|
||||
onGenerated?: (prompt: string) => void
|
||||
codeLanguages: CodeLanguage
|
||||
}
|
||||
|
||||
const CodeGenerateBtn: FC<Props> = ({
|
||||
nodeId,
|
||||
currentCode,
|
||||
className,
|
||||
codeLanguages,
|
||||
onGenerated,
|
||||
}) => {
|
||||
const [showAutomatic, { setTrue: showAutomaticTrue, setFalse: showAutomaticFalse }] = useBoolean(false)
|
||||
const handleAutomaticRes = useCallback((res: CodeGenRes) => {
|
||||
onGenerated?.(res.code)
|
||||
const handleAutomaticRes = useCallback((res: GenRes) => {
|
||||
onGenerated?.(res.modified)
|
||||
showAutomaticFalse()
|
||||
}, [onGenerated, showAutomaticFalse])
|
||||
const configsMap = useHooksStore(s => s.configsMap)
|
||||
|
||||
return (
|
||||
<div className={cn(className)}>
|
||||
<ActionButton
|
||||
@ -40,6 +47,9 @@ const CodeGenerateBtn: FC<Props> = ({
|
||||
codeLanguages={codeLanguages}
|
||||
onClose={showAutomaticFalse}
|
||||
onFinished={handleAutomaticRes}
|
||||
flowId={configsMap?.flowId || ''}
|
||||
nodeId={nodeId}
|
||||
currentCode={currentCode}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -16,8 +16,10 @@ import useToggleExpend from '@/app/components/workflow/nodes/_base/hooks/use-tog
|
||||
import type { FileEntity } from '@/app/components/base/file-uploader/types'
|
||||
import FileListInLog from '@/app/components/base/file-uploader/file-list-in-log'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import type { Node, NodeOutPutVar } from '@/app/components/workflow/types'
|
||||
|
||||
type Props = {
|
||||
nodeId?: string
|
||||
className?: string
|
||||
title: React.JSX.Element | string
|
||||
headerRight?: React.JSX.Element
|
||||
@ -35,9 +37,12 @@ type Props = {
|
||||
showFileList?: boolean
|
||||
showCodeGenerator?: boolean
|
||||
tip?: React.JSX.Element
|
||||
nodesOutputVars?: NodeOutPutVar[]
|
||||
availableNodes?: Node[]
|
||||
}
|
||||
|
||||
const Base: FC<Props> = ({
|
||||
nodeId,
|
||||
className,
|
||||
title,
|
||||
headerRight,
|
||||
@ -86,7 +91,12 @@ const Base: FC<Props> = ({
|
||||
{headerRight}
|
||||
{showCodeGenerator && codeLanguages && (
|
||||
<div className='ml-1'>
|
||||
<CodeGeneratorButton onGenerated={onGenerated} codeLanguages={codeLanguages} />
|
||||
<CodeGeneratorButton
|
||||
onGenerated={onGenerated}
|
||||
codeLanguages={codeLanguages}
|
||||
currentCode={value}
|
||||
nodeId={nodeId!}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<ActionButton className='ml-1' onClick={handleCopy}>
|
||||
|
||||
@ -20,6 +20,7 @@ loader.config({ paths: { vs: `${basePath}/vs` } })
|
||||
const CODE_EDITOR_LINE_HEIGHT = 18
|
||||
|
||||
export type Props = {
|
||||
nodeId?: string
|
||||
value?: string | object
|
||||
placeholder?: React.JSX.Element | string
|
||||
onChange?: (value: string) => void
|
||||
@ -47,6 +48,7 @@ export const languageMap = {
|
||||
}
|
||||
|
||||
const CodeEditor: FC<Props> = ({
|
||||
nodeId,
|
||||
value = '',
|
||||
placeholder = '',
|
||||
onChange = noop,
|
||||
@ -175,6 +177,7 @@ const CodeEditor: FC<Props> = ({
|
||||
</div>
|
||||
: (
|
||||
<Base
|
||||
nodeId={nodeId}
|
||||
className='relative'
|
||||
title={title}
|
||||
value={outPutValue}
|
||||
|
||||
@ -41,6 +41,7 @@ type Props = {
|
||||
className?: string
|
||||
headerClassName?: string
|
||||
instanceId?: string
|
||||
nodeId?: string
|
||||
title: string | React.JSX.Element
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
@ -83,6 +84,7 @@ const Editor: FC<Props> = ({
|
||||
className,
|
||||
headerClassName,
|
||||
instanceId,
|
||||
nodeId,
|
||||
title,
|
||||
value,
|
||||
onChange,
|
||||
@ -159,7 +161,13 @@ const Editor: FC<Props> = ({
|
||||
<div className='flex items-center'>
|
||||
<div className='text-xs font-medium leading-[18px] text-text-tertiary'>{value?.length || 0}</div>
|
||||
{isSupportPromptGenerator && (
|
||||
<PromptGeneratorBtn className='ml-[5px]' onGenerated={onGenerated} modelConfig={modelConfig} />
|
||||
<PromptGeneratorBtn
|
||||
nodeId={nodeId!}
|
||||
className='ml-[5px]'
|
||||
onGenerated={onGenerated}
|
||||
modelConfig={modelConfig}
|
||||
currentPrompt={value}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className='ml-2 mr-2 h-3 w-px bg-divider-regular'></div>
|
||||
|
||||
@ -1022,7 +1022,15 @@ export const getNodeUsedVars = (node: Node): ValueSelector[] => {
|
||||
res = (data as IfElseNodeType).conditions?.map((c) => {
|
||||
return c.variable_selector || []
|
||||
}) || []
|
||||
res.push(...((data as IfElseNodeType).cases || []).flatMap(c => (c.conditions || [])).map(c => c.variable_selector || []))
|
||||
res.push(...((data as IfElseNodeType).cases || []).flatMap(c => (c.conditions || [])).flatMap((c) => {
|
||||
const selectors: ValueSelector[] = []
|
||||
if (c.variable_selector)
|
||||
selectors.push(c.variable_selector)
|
||||
// Handle sub-variable conditions
|
||||
if (c.sub_variable_condition && c.sub_variable_condition.conditions)
|
||||
selectors.push(...c.sub_variable_condition.conditions.map(subC => subC.variable_selector || []).filter(sel => sel.length > 0))
|
||||
return selectors
|
||||
}))
|
||||
break
|
||||
}
|
||||
case BlockEnum.Code: {
|
||||
@ -1259,6 +1267,26 @@ export const updateNodeVars = (oldNode: Node, oldVarSelector: ValueSelector, new
|
||||
return c
|
||||
})
|
||||
}
|
||||
if (payload.cases) {
|
||||
payload.cases = payload.cases.map((caseItem) => {
|
||||
if (caseItem.conditions) {
|
||||
caseItem.conditions = caseItem.conditions.map((c) => {
|
||||
if (c.variable_selector?.join('.') === oldVarSelector.join('.'))
|
||||
c.variable_selector = newVarSelector
|
||||
// Handle sub-variable conditions
|
||||
if (c.sub_variable_condition && c.sub_variable_condition.conditions) {
|
||||
c.sub_variable_condition.conditions = c.sub_variable_condition.conditions.map((subC) => {
|
||||
if (subC.variable_selector?.join('.') === oldVarSelector.join('.'))
|
||||
subC.variable_selector = newVarSelector
|
||||
return subC
|
||||
})
|
||||
}
|
||||
return c
|
||||
})
|
||||
}
|
||||
return caseItem
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
case BlockEnum.Code: {
|
||||
|
||||
@ -20,7 +20,9 @@ import { varTypeToStructType } from './utils'
|
||||
import type { Field } from '@/app/components/workflow/nodes/llm/types'
|
||||
import { FILE_STRUCT } from '@/app/components/workflow/constants'
|
||||
import { noop } from 'lodash-es'
|
||||
import { CodeAssistant, MagicEdit } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import { VariableIconWithColor } from '@/app/components/workflow/nodes/_base/components/variable/variable-label'
|
||||
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
|
||||
type ObjectChildrenProps = {
|
||||
nodeId: string
|
||||
@ -44,7 +46,10 @@ type ItemProps = {
|
||||
isSupportFileVar?: boolean
|
||||
isException?: boolean
|
||||
isLoopVar?: boolean
|
||||
isFlat?: boolean
|
||||
isInCodeGeneratorInstructionEditor?: boolean
|
||||
zIndex?: number
|
||||
className?: string
|
||||
}
|
||||
|
||||
const objVarTypes = [VarType.object, VarType.file]
|
||||
@ -59,7 +64,10 @@ const Item: FC<ItemProps> = ({
|
||||
isSupportFileVar,
|
||||
isException,
|
||||
isLoopVar,
|
||||
isFlat,
|
||||
isInCodeGeneratorInstructionEditor,
|
||||
zIndex,
|
||||
className,
|
||||
}) => {
|
||||
const isStructureOutput = itemData.type === VarType.object && (itemData.children as StructuredOutput)?.schema?.properties
|
||||
const isFile = itemData.type === VarType.file && !isStructureOutput
|
||||
@ -67,6 +75,29 @@ const Item: FC<ItemProps> = ({
|
||||
const isSys = itemData.variable.startsWith('sys.')
|
||||
const isEnv = itemData.variable.startsWith('env.')
|
||||
const isChatVar = itemData.variable.startsWith('conversation.')
|
||||
const flatVarIcon = useMemo(() => {
|
||||
if (!isFlat)
|
||||
return null
|
||||
const variable = itemData.variable
|
||||
let Icon
|
||||
switch (variable) {
|
||||
case 'current':
|
||||
Icon = isInCodeGeneratorInstructionEditor ? CodeAssistant : MagicEdit
|
||||
return <Icon className='h-3.5 w-3.5 shrink-0 text-util-colors-violet-violet-600' />
|
||||
case 'error_message':
|
||||
return <Variable02 className='h-3.5 w-3.5 shrink-0 text-util-colors-orange-dark-orange-dark-600' />
|
||||
default:
|
||||
return <Variable02 className='h-3.5 w-3.5 shrink-0 text-text-accent' />
|
||||
}
|
||||
}, [isFlat, isInCodeGeneratorInstructionEditor, itemData.variable])
|
||||
|
||||
const varName = useMemo(() => {
|
||||
if (!isFlat)
|
||||
return itemData.variable
|
||||
if (itemData.variable === 'current')
|
||||
return isInCodeGeneratorInstructionEditor ? 'current_code' : 'current_prompt'
|
||||
return itemData.variable
|
||||
}, [isFlat, isInCodeGeneratorInstructionEditor, itemData.variable])
|
||||
|
||||
const objStructuredOutput: StructuredOutput | null = useMemo(() => {
|
||||
if (!isObj) return null
|
||||
@ -122,7 +153,10 @@ const Item: FC<ItemProps> = ({
|
||||
if (!isSupportFileVar && isFile)
|
||||
return
|
||||
|
||||
if (isSys || isEnv || isChatVar) { // system variable | environment variable | conversation variable
|
||||
if (isFlat) {
|
||||
onChange([itemData.variable], itemData)
|
||||
}
|
||||
else if (isSys || isEnv || isChatVar) { // system variable | environment variable | conversation variable
|
||||
onChange([...objPath, ...itemData.variable.split('.')], itemData)
|
||||
}
|
||||
else {
|
||||
@ -147,18 +181,22 @@ const Item: FC<ItemProps> = ({
|
||||
className={cn(
|
||||
(isObj || isStructureOutput) ? ' pr-1' : 'pr-[18px]',
|
||||
isHovering && ((isObj || isStructureOutput) ? 'bg-components-panel-on-panel-item-bg-hover' : 'bg-state-base-hover'),
|
||||
'relative flex h-6 w-full cursor-pointer items-center rounded-md pl-3')
|
||||
'relative flex h-6 w-full cursor-pointer items-center rounded-md pl-3',
|
||||
className,
|
||||
)
|
||||
}
|
||||
onClick={handleChosen}
|
||||
onMouseDown={e => e.preventDefault()}
|
||||
>
|
||||
<div className='flex w-0 grow items-center'>
|
||||
<VariableIconWithColor
|
||||
{!isFlat && <VariableIconWithColor
|
||||
variableCategory={variableCategory}
|
||||
isExceptionVariable={isException}
|
||||
/>
|
||||
/>}
|
||||
{isFlat && flatVarIcon}
|
||||
|
||||
{!isEnv && !isChatVar && (
|
||||
<div title={itemData.variable} className='system-sm-medium ml-1 w-0 grow truncate text-text-secondary'>{itemData.variable}</div>
|
||||
<div title={itemData.variable} className='system-sm-medium ml-1 w-0 grow truncate text-text-secondary'>{varName}</div>
|
||||
)}
|
||||
{isEnv && (
|
||||
<div title={itemData.variable} className='system-sm-medium ml-1 w-0 grow truncate text-text-secondary'>{itemData.variable.replace('env.', '')}</div>
|
||||
@ -264,6 +302,7 @@ type Props = {
|
||||
onClose?: () => void
|
||||
onBlur?: () => void
|
||||
zIndex?: number
|
||||
isInCodeGeneratorInstructionEditor?: boolean
|
||||
autoFocus?: boolean
|
||||
}
|
||||
const VarReferenceVars: FC<Props> = ({
|
||||
@ -277,6 +316,7 @@ const VarReferenceVars: FC<Props> = ({
|
||||
onClose,
|
||||
onBlur,
|
||||
zIndex,
|
||||
isInCodeGeneratorInstructionEditor,
|
||||
autoFocus = true,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
@ -319,7 +359,7 @@ const VarReferenceVars: FC<Props> = ({
|
||||
{
|
||||
!hideSearch && (
|
||||
<>
|
||||
<div className={cn('var-search-input-wrapper mx-2 mb-1 mt-2', searchBoxClassName)} onClick={e => e.stopPropagation()}>
|
||||
<div className={cn('var-search-input-wrapper mx-2 mb-2 mt-2', searchBoxClassName)} onClick={e => e.stopPropagation()}>
|
||||
<Input
|
||||
className='var-search-input'
|
||||
showLeftIcon
|
||||
@ -345,11 +385,13 @@ const VarReferenceVars: FC<Props> = ({
|
||||
|
||||
{
|
||||
filteredVars.map((item, i) => (
|
||||
<div key={i}>
|
||||
<div
|
||||
className='system-xs-medium-uppercase truncate px-3 leading-[22px] text-text-tertiary'
|
||||
title={item.title}
|
||||
>{item.title}</div>
|
||||
<div key={i} className={cn(!item.isFlat && 'mt-3', i === 0 && item.isFlat && 'mt-2')}>
|
||||
{!item.isFlat && (
|
||||
<div
|
||||
className='system-xs-medium-uppercase truncate px-3 leading-[22px] text-text-tertiary'
|
||||
title={item.title}
|
||||
>{item.title}</div>
|
||||
)}
|
||||
{item.vars.map((v, j) => (
|
||||
<Item
|
||||
key={j}
|
||||
@ -362,13 +404,22 @@ const VarReferenceVars: FC<Props> = ({
|
||||
isSupportFileVar={isSupportFileVar}
|
||||
isException={v.isException}
|
||||
isLoopVar={item.isLoop}
|
||||
isFlat={item.isFlat}
|
||||
isInCodeGeneratorInstructionEditor={isInCodeGeneratorInstructionEditor}
|
||||
zIndex={zIndex}
|
||||
/>
|
||||
))}
|
||||
{item.isFlat && !filteredVars[i + 1]?.isFlat && !!filteredVars.find(item => !item.isFlat) && (
|
||||
<div className='relative mt-[14px] flex items-center space-x-1'>
|
||||
<div className='h-0 w-3 shrink-0 border border-divider-subtle'></div>
|
||||
<div className='system-2xs-semibold-uppercase text-text-tertiary'>{t('workflow.debug.lastOutput')}</div>
|
||||
<div className='h-0 shrink-0 grow border border-divider-subtle'></div>
|
||||
</div>
|
||||
)}
|
||||
</div>))
|
||||
}
|
||||
</div>
|
||||
: <div className='pl-3 text-xs font-medium uppercase leading-[18px] text-gray-500'>{t('workflow.common.noVar')}</div>}
|
||||
: <div className='mt-2 pl-3 text-xs font-medium uppercase leading-[18px] text-gray-500'>{t('workflow.common.noVar')}</div>}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,18 +1,19 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
type Params = {
|
||||
ref: React.RefObject<HTMLDivElement>
|
||||
ref?: React.RefObject<HTMLDivElement | null>
|
||||
hasFooter?: boolean
|
||||
isInNode?: boolean
|
||||
}
|
||||
|
||||
const useToggleExpend = ({ ref, hasFooter = true, isInNode }: Params) => {
|
||||
const [isExpand, setIsExpand] = useState(false)
|
||||
const [wrapHeight, setWrapHeight] = useState(ref.current?.clientHeight)
|
||||
const [wrapHeight, setWrapHeight] = useState(ref?.current?.clientHeight)
|
||||
const editorExpandHeight = isExpand ? wrapHeight! - (hasFooter ? 56 : 29) : 0
|
||||
useEffect(() => {
|
||||
if (!ref?.current) return
|
||||
setWrapHeight(ref.current?.clientHeight)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isExpand])
|
||||
|
||||
const wrapClassName = (() => {
|
||||
|
||||
@ -89,6 +89,7 @@ const Panel: FC<NodePanelProps<CodeNodeType>> = ({
|
||||
</Field>
|
||||
<Split />
|
||||
<CodeEditor
|
||||
nodeId={id}
|
||||
isInNode
|
||||
readOnly={readOnly}
|
||||
title={
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { uniqueId } from 'lodash-es'
|
||||
import React, { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { ModelConfig, PromptItem, Variable } from '../../../types'
|
||||
import { EditionType } from '../../../types'
|
||||
@ -14,11 +13,13 @@ import { PromptRole } from '@/models/debug'
|
||||
const i18nPrefix = 'workflow.nodes.llm'
|
||||
|
||||
type Props = {
|
||||
instanceId: string
|
||||
className?: string
|
||||
headerClassName?: string
|
||||
canNotChooseSystemRole?: boolean
|
||||
readOnly: boolean
|
||||
id: string
|
||||
nodeId: string
|
||||
canRemove: boolean
|
||||
isChatModel: boolean
|
||||
isChatApp: boolean
|
||||
@ -58,11 +59,13 @@ const roleOptions = [
|
||||
const roleOptionsWithoutSystemRole = roleOptions.filter(item => item.value !== PromptRole.system)
|
||||
|
||||
const ConfigPromptItem: FC<Props> = ({
|
||||
instanceId,
|
||||
className,
|
||||
headerClassName,
|
||||
canNotChooseSystemRole,
|
||||
readOnly,
|
||||
id,
|
||||
nodeId,
|
||||
canRemove,
|
||||
handleChatModeMessageRoleChange,
|
||||
isChatModel,
|
||||
@ -84,10 +87,6 @@ const ConfigPromptItem: FC<Props> = ({
|
||||
const {
|
||||
setControlPromptEditorRerenderKey,
|
||||
} = workflowStore.getState()
|
||||
const [instanceId, setInstanceId] = useState(uniqueId())
|
||||
useEffect(() => {
|
||||
setInstanceId(`${id}-${uniqueId()}`)
|
||||
}, [id])
|
||||
|
||||
const handleGenerated = useCallback((prompt: string) => {
|
||||
onPromptChange(prompt)
|
||||
@ -136,6 +135,7 @@ const ConfigPromptItem: FC<Props> = ({
|
||||
hasSetBlockStatus={hasSetBlockStatus}
|
||||
nodesOutputVars={availableVars}
|
||||
availableNodes={availableNodes}
|
||||
nodeId={nodeId}
|
||||
isSupportPromptGenerator={payload.role === PromptRole.system}
|
||||
onGenerated={handleGenerated}
|
||||
modelConfig={modelConfig}
|
||||
|
||||
@ -182,12 +182,14 @@ const ConfigPrompt: FC<Props> = ({
|
||||
<div key={item.id || index} className='group relative'>
|
||||
{canDrag && <DragHandle className='absolute left-[-14px] top-2 hidden h-3.5 w-3.5 text-text-quaternary group-hover:block' />}
|
||||
<ConfigPromptItem
|
||||
instanceId={item.role === PromptRole.system ? `${nodeId}-chat-workflow-llm-prompt-editor` : `${nodeId}-chat-workflow-llm-prompt-editor-${index}`}
|
||||
className={cn(canDrag && 'handle')}
|
||||
headerClassName={cn(canDrag && 'cursor-grab')}
|
||||
canNotChooseSystemRole={!canChooseSystemRole}
|
||||
canRemove={payload.length > 1 && !(index === 0 && item.role === PromptRole.system)}
|
||||
readOnly={readOnly}
|
||||
id={item.id!}
|
||||
nodeId={nodeId}
|
||||
handleChatModeMessageRoleChange={handleChatModeMessageRoleChange(index)}
|
||||
isChatModel={isChatModel}
|
||||
isChatApp={isChatApp}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user