Compare commits

..

9 Commits

Author SHA1 Message Date
FFXN
7dfe615613 feat: Add "type" field to PipelineRecommendedPlugin model; Add query param "type" to recommended-plugins api. (#29684)
Some checks failed
Build and Push API & Web / build (api, DIFY_API_IMAGE_NAME, linux/amd64, build-api-amd64) (push) Has been cancelled
Build and Push API & Web / build (api, DIFY_API_IMAGE_NAME, linux/arm64, build-api-arm64) (push) Has been cancelled
Build and Push API & Web / build (web, DIFY_WEB_IMAGE_NAME, linux/amd64, build-web-amd64) (push) Has been cancelled
Build and Push API & Web / build (web, DIFY_WEB_IMAGE_NAME, linux/arm64, build-web-arm64) (push) Has been cancelled
Build and Push API & Web / create-manifest (api, DIFY_API_IMAGE_NAME, merge-api-images) (push) Has been cancelled
Build and Push API & Web / create-manifest (web, DIFY_WEB_IMAGE_NAME, merge-web-images) (push) Has been cancelled
2025-12-15 18:19:54 +08:00
FFXN
a1a3fa0283 Add "type" field to PipelineRecommendedPlugin model; Add query param "type" to recommended-plugins api. 2025-12-15 16:44:32 +08:00
FFXN
ff7344f3d3 Add "type" field to PipelineRecommendedPlugin model; Add query param "type" to recommended-plugins api. 2025-12-15 16:38:44 +08:00
FFXN
bcd33be22a Add "type" field to PipelineRecommendedPlugin model; Add query param "type" to recommended-plugins api. 2025-12-15 16:33:06 +08:00
hjlarry
991f31f195 log missing trigger icon
Some checks failed
Build and Push API & Web / build (api, DIFY_API_IMAGE_NAME, linux/amd64, build-api-amd64) (push) Has been cancelled
Build and Push API & Web / build (api, DIFY_API_IMAGE_NAME, linux/arm64, build-api-arm64) (push) Has been cancelled
Build and Push API & Web / build (web, DIFY_WEB_IMAGE_NAME, linux/amd64, build-web-amd64) (push) Has been cancelled
Build and Push API & Web / build (web, DIFY_WEB_IMAGE_NAME, linux/arm64, build-web-arm64) (push) Has been cancelled
Build and Push API & Web / create-manifest (api, DIFY_API_IMAGE_NAME, merge-api-images) (push) Has been cancelled
Build and Push API & Web / create-manifest (web, DIFY_WEB_IMAGE_NAME, merge-web-images) (push) Has been cancelled
2025-12-10 09:40:16 +08:00
yyh
f8b10c2272 Refactor apps service toward TanStack Query (#29004) 2025-12-02 15:18:33 +08:00
carribean
369892634d [Bugfix] Fixed an issue with UUID type queries in MySQL databases (#28941) 2025-12-02 14:37:23 +08:00
yyh
8e5cb86409 Stop showing slash commands in general Go to Anything search (#29012) 2025-12-02 14:24:21 +08:00
Gritty_dev
a85afe4d07 feat: complete test script of plugin manager (#28967)
Some checks failed
autofix.ci / autofix (push) Has been cancelled
Build and Push API & Web / build (api, DIFY_API_IMAGE_NAME, linux/amd64, build-api-amd64) (push) Has been cancelled
Build and Push API & Web / build (api, DIFY_API_IMAGE_NAME, linux/arm64, build-api-arm64) (push) Has been cancelled
Build and Push API & Web / build (web, DIFY_WEB_IMAGE_NAME, linux/amd64, build-web-amd64) (push) Has been cancelled
Build and Push API & Web / build (web, DIFY_WEB_IMAGE_NAME, linux/arm64, build-web-arm64) (push) Has been cancelled
Build and Push API & Web / create-manifest (api, DIFY_API_IMAGE_NAME, merge-api-images) (push) Has been cancelled
Build and Push API & Web / create-manifest (web, DIFY_WEB_IMAGE_NAME, merge-web-images) (push) Has been cancelled
Main CI Pipeline / Check Changed Files (push) Has been cancelled
Main CI Pipeline / API Tests (push) Has been cancelled
Main CI Pipeline / Web Tests (push) Has been cancelled
Main CI Pipeline / Style Check (push) Has been cancelled
Main CI Pipeline / VDB Tests (push) Has been cancelled
Main CI Pipeline / DB Migration Test (push) Has been cancelled
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2025-12-02 11:25:08 +08:00
127 changed files with 2756 additions and 4658 deletions

View File

@@ -1004,6 +1004,11 @@ class RagPipelineRecommendedPluginApi(Resource):
@login_required
@account_initialization_required
def get(self):
parser = reqparse.RequestParser()
parser.add_argument('type', type=str, location='args', required=False, default='all')
args = parser.parse_args()
type = args["type"]
rag_pipeline_service = RagPipelineService()
recommended_plugins = rag_pipeline_service.get_recommended_plugins()
recommended_plugins = rag_pipeline_service.get_recommended_plugins(type)
return recommended_plugins

View File

@@ -0,0 +1,64 @@
"""Alter table pipeline_recommended_plugins add column type
Revision ID: 6bb0832495f0
Revises: 7bb281b7a422
Create Date: 2025-12-15 16:14:38.482072
"""
from alembic import op
import models as models
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = '6bb0832495f0'
down_revision = '7bb281b7a422'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('app_triggers', schema=None) as batch_op:
batch_op.alter_column('provider_name',
existing_type=sa.VARCHAR(length=255),
nullable=False,
existing_server_default=sa.text("''::character varying"))
with op.batch_alter_table('operation_logs', schema=None) as batch_op:
batch_op.alter_column('content',
existing_type=postgresql.JSON(astext_type=sa.Text()),
nullable=False)
with op.batch_alter_table('pipeline_recommended_plugins', schema=None) as batch_op:
batch_op.add_column(sa.Column('type', sa.String(length=50), nullable=True))
with op.batch_alter_table('providers', schema=None) as batch_op:
batch_op.alter_column('quota_used',
existing_type=sa.BIGINT(),
nullable=False)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('providers', schema=None) as batch_op:
batch_op.alter_column('quota_used',
existing_type=sa.BIGINT(),
nullable=True)
with op.batch_alter_table('pipeline_recommended_plugins', schema=None) as batch_op:
batch_op.drop_column('type')
with op.batch_alter_table('operation_logs', schema=None) as batch_op:
batch_op.alter_column('content',
existing_type=postgresql.JSON(astext_type=sa.Text()),
nullable=True)
with op.batch_alter_table('app_triggers', schema=None) as batch_op:
batch_op.alter_column('provider_name',
existing_type=sa.VARCHAR(length=255),
nullable=True,
existing_server_default=sa.text("''::character varying"))
# ### end Alembic commands ###

View File

@@ -1458,6 +1458,7 @@ class PipelineRecommendedPlugin(TypeBase):
)
plugin_id: Mapped[str] = mapped_column(LongText, nullable=False)
provider_name: Mapped[str] = mapped_column(LongText, nullable=False)
type: Mapped[str] = mapped_column(sa.String(50), nullable=True)
position: Mapped[int] = mapped_column(sa.Integer, nullable=False, default=0)
active: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, default=True)
created_at: Mapped[datetime] = mapped_column(

View File

@@ -19,7 +19,7 @@ class StringUUID(TypeDecorator[uuid.UUID | str | None]):
def process_bind_param(self, value: uuid.UUID | str | None, dialect: Dialect) -> str | None:
if value is None:
return value
elif dialect.name == "postgresql":
elif dialect.name in ["postgresql", "mysql"]:
return str(value)
else:
if isinstance(value, uuid.UUID):

View File

@@ -907,19 +907,29 @@ class WorkflowNodeExecutionModel(Base): # This model is expected to have `offlo
@property
def extras(self) -> dict[str, Any]:
from core.tools.tool_manager import ToolManager
from core.trigger.trigger_manager import TriggerManager
extras: dict[str, Any] = {}
if self.execution_metadata_dict:
if self.node_type == NodeType.TOOL and "tool_info" in self.execution_metadata_dict:
tool_info: dict[str, Any] = self.execution_metadata_dict["tool_info"]
execution_metadata = self.execution_metadata_dict
if execution_metadata:
if self.node_type == NodeType.TOOL and "tool_info" in execution_metadata:
tool_info: dict[str, Any] = execution_metadata["tool_info"]
extras["icon"] = ToolManager.get_tool_icon(
tenant_id=self.tenant_id,
provider_type=tool_info["provider_type"],
provider_id=tool_info["provider_id"],
)
elif self.node_type == NodeType.DATASOURCE and "datasource_info" in self.execution_metadata_dict:
datasource_info = self.execution_metadata_dict["datasource_info"]
elif self.node_type == NodeType.DATASOURCE and "datasource_info" in execution_metadata:
datasource_info = execution_metadata["datasource_info"]
extras["icon"] = datasource_info.get("icon")
elif self.node_type == NodeType.TRIGGER_PLUGIN and "trigger_info" in execution_metadata:
trigger_info = execution_metadata["trigger_info"] or {}
provider_id = trigger_info.get("provider_id")
if provider_id:
extras["icon"] = TriggerManager.get_trigger_plugin_icon(
tenant_id=self.tenant_id,
provider_id=provider_id,
)
return extras
def _get_offload_by_type(self, type_: ExecutionOffLoadType) -> Optional["WorkflowNodeExecutionOffload"]:

View File

@@ -1248,12 +1248,14 @@ class RagPipelineService:
session.commit()
return workflow_node_execution_db_model
def get_recommended_plugins(self) -> dict:
def get_recommended_plugins(self, type: str) -> dict:
# Query active recommended plugins
query = db.session.query(PipelineRecommendedPlugin).where(PipelineRecommendedPlugin.active == True)
if type and type != "all":
query = query.where(PipelineRecommendedPlugin.type == type)
pipeline_recommended_plugins = (
db.session.query(PipelineRecommendedPlugin)
.where(PipelineRecommendedPlugin.active == True)
.order_by(PipelineRecommendedPlugin.position.asc())
query.order_by(PipelineRecommendedPlugin.position.asc())
.all()
)

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,5 @@
'use client'
import { useState } from 'react'
import useSWR from 'swr'
import { useTranslation } from 'react-i18next'
import {
RiGraduationCapFill,
@@ -23,8 +22,9 @@ import PremiumBadge from '@/app/components/base/premium-badge'
import { useGlobalPublicStore } from '@/context/global-public-context'
import EmailChangeModal from './email-change-modal'
import { validPassword } from '@/config'
import { fetchAppList } from '@/service/apps'
import type { App } from '@/types/app'
import { useAppList } from '@/service/use-apps'
const titleClassName = `
system-sm-semibold text-text-secondary
@@ -36,7 +36,7 @@ const descriptionClassName = `
export default function AccountPage() {
const { t } = useTranslation()
const { systemFeatures } = useGlobalPublicStore()
const { data: appList } = useSWR({ url: '/apps', params: { page: 1, limit: 100, name: '' } }, fetchAppList)
const { data: appList } = useAppList({ page: 1, limit: 100, name: '' })
const apps = appList?.data || []
const { mutateUserProfile, userProfile } = useAppContext()
const { isEducationAccount } = useProviderContext()

View File

@@ -52,7 +52,6 @@ export type IGetAutomaticResProps = {
editorId?: string
currentPrompt?: string
isBasicMode?: boolean
hideTryIt?: boolean
}
const TryLabel: FC<{
@@ -81,7 +80,6 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
currentPrompt,
isBasicMode,
onFinished,
hideTryIt,
}) => {
const { t } = useTranslation()
const localModel = localStorage.getItem('auto-gen-model')
@@ -307,7 +305,7 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
hideDebugWithMultipleModel
/>
</div>
{isBasicMode && !hideTryIt && (
{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>

View File

@@ -3,7 +3,6 @@ import type { FC } from 'react'
import React from 'react'
import ReactECharts from 'echarts-for-react'
import type { EChartsOption } from 'echarts'
import useSWR from 'swr'
import type { Dayjs } from 'dayjs'
import dayjs from 'dayjs'
import { get } from 'lodash-es'
@@ -13,7 +12,20 @@ import { formatNumber } from '@/utils/format'
import Basic from '@/app/components/app-sidebar/basic'
import Loading from '@/app/components/base/loading'
import type { AppDailyConversationsResponse, AppDailyEndUsersResponse, AppDailyMessagesResponse, AppTokenCostsResponse } from '@/models/app'
import { getAppDailyConversations, getAppDailyEndUsers, getAppDailyMessages, getAppStatistics, getAppTokenCosts, getWorkflowDailyConversations } from '@/service/apps'
import {
useAppAverageResponseTime,
useAppAverageSessionInteractions,
useAppDailyConversations,
useAppDailyEndUsers,
useAppDailyMessages,
useAppSatisfactionRate,
useAppTokenCosts,
useAppTokensPerSecond,
useWorkflowAverageInteractions,
useWorkflowDailyConversations,
useWorkflowDailyTerminals,
useWorkflowTokenCosts,
} from '@/service/use-apps'
const valueFormatter = (v: string | number) => v
const COLOR_TYPE_MAP = {
@@ -272,8 +284,8 @@ const getDefaultChartData = ({ start, end, key = 'count' }: { start: string; end
export const MessagesChart: FC<IBizChartProps> = ({ id, period }) => {
const { t } = useTranslation()
const { data: response } = useSWR({ url: `/apps/${id}/statistics/daily-messages`, params: period.query }, getAppDailyMessages)
if (!response)
const { data: response, isLoading } = useAppDailyMessages(id, period.query)
if (isLoading || !response)
return <Loading />
const noDataFlag = !response.data || response.data.length === 0
return <Chart
@@ -286,8 +298,8 @@ export const MessagesChart: FC<IBizChartProps> = ({ id, period }) => {
export const ConversationsChart: FC<IBizChartProps> = ({ id, period }) => {
const { t } = useTranslation()
const { data: response } = useSWR({ url: `/apps/${id}/statistics/daily-conversations`, params: period.query }, getAppDailyConversations)
if (!response)
const { data: response, isLoading } = useAppDailyConversations(id, period.query)
if (isLoading || !response)
return <Loading />
const noDataFlag = !response.data || response.data.length === 0
return <Chart
@@ -301,8 +313,8 @@ export const ConversationsChart: FC<IBizChartProps> = ({ id, period }) => {
export const EndUsersChart: FC<IBizChartProps> = ({ id, period }) => {
const { t } = useTranslation()
const { data: response } = useSWR({ url: `/apps/${id}/statistics/daily-end-users`, id, params: period.query }, getAppDailyEndUsers)
if (!response)
const { data: response, isLoading } = useAppDailyEndUsers(id, period.query)
if (isLoading || !response)
return <Loading />
const noDataFlag = !response.data || response.data.length === 0
return <Chart
@@ -315,8 +327,8 @@ export const EndUsersChart: FC<IBizChartProps> = ({ id, period }) => {
export const AvgSessionInteractions: FC<IBizChartProps> = ({ id, period }) => {
const { t } = useTranslation()
const { data: response } = useSWR({ url: `/apps/${id}/statistics/average-session-interactions`, params: period.query }, getAppStatistics)
if (!response)
const { data: response, isLoading } = useAppAverageSessionInteractions(id, period.query)
if (isLoading || !response)
return <Loading />
const noDataFlag = !response.data || response.data.length === 0
return <Chart
@@ -331,8 +343,8 @@ export const AvgSessionInteractions: FC<IBizChartProps> = ({ id, period }) => {
export const AvgResponseTime: FC<IBizChartProps> = ({ id, period }) => {
const { t } = useTranslation()
const { data: response } = useSWR({ url: `/apps/${id}/statistics/average-response-time`, params: period.query }, getAppStatistics)
if (!response)
const { data: response, isLoading } = useAppAverageResponseTime(id, period.query)
if (isLoading || !response)
return <Loading />
const noDataFlag = !response.data || response.data.length === 0
return <Chart
@@ -348,8 +360,8 @@ export const AvgResponseTime: FC<IBizChartProps> = ({ id, period }) => {
export const TokenPerSecond: FC<IBizChartProps> = ({ id, period }) => {
const { t } = useTranslation()
const { data: response } = useSWR({ url: `/apps/${id}/statistics/tokens-per-second`, params: period.query }, getAppStatistics)
if (!response)
const { data: response, isLoading } = useAppTokensPerSecond(id, period.query)
if (isLoading || !response)
return <Loading />
const noDataFlag = !response.data || response.data.length === 0
return <Chart
@@ -366,8 +378,8 @@ export const TokenPerSecond: FC<IBizChartProps> = ({ id, period }) => {
export const UserSatisfactionRate: FC<IBizChartProps> = ({ id, period }) => {
const { t } = useTranslation()
const { data: response } = useSWR({ url: `/apps/${id}/statistics/user-satisfaction-rate`, params: period.query }, getAppStatistics)
if (!response)
const { data: response, isLoading } = useAppSatisfactionRate(id, period.query)
if (isLoading || !response)
return <Loading />
const noDataFlag = !response.data || response.data.length === 0
return <Chart
@@ -384,8 +396,8 @@ export const UserSatisfactionRate: FC<IBizChartProps> = ({ id, period }) => {
export const CostChart: FC<IBizChartProps> = ({ id, period }) => {
const { t } = useTranslation()
const { data: response } = useSWR({ url: `/apps/${id}/statistics/token-costs`, params: period.query }, getAppTokenCosts)
if (!response)
const { data: response, isLoading } = useAppTokenCosts(id, period.query)
if (isLoading || !response)
return <Loading />
const noDataFlag = !response.data || response.data.length === 0
return <Chart
@@ -398,8 +410,8 @@ export const CostChart: FC<IBizChartProps> = ({ id, period }) => {
export const WorkflowMessagesChart: FC<IBizChartProps> = ({ id, period }) => {
const { t } = useTranslation()
const { data: response } = useSWR({ url: `/apps/${id}/workflow/statistics/daily-conversations`, params: period.query }, getWorkflowDailyConversations)
if (!response)
const { data: response, isLoading } = useWorkflowDailyConversations(id, period.query)
if (isLoading || !response)
return <Loading />
const noDataFlag = !response.data || response.data.length === 0
return <Chart
@@ -414,8 +426,8 @@ export const WorkflowMessagesChart: FC<IBizChartProps> = ({ id, period }) => {
export const WorkflowDailyTerminalsChart: FC<IBizChartProps> = ({ id, period }) => {
const { t } = useTranslation()
const { data: response } = useSWR({ url: `/apps/${id}/workflow/statistics/daily-terminals`, id, params: period.query }, getAppDailyEndUsers)
if (!response)
const { data: response, isLoading } = useWorkflowDailyTerminals(id, period.query)
if (isLoading || !response)
return <Loading />
const noDataFlag = !response.data || response.data.length === 0
return <Chart
@@ -429,8 +441,8 @@ export const WorkflowDailyTerminalsChart: FC<IBizChartProps> = ({ id, period })
export const WorkflowCostChart: FC<IBizChartProps> = ({ id, period }) => {
const { t } = useTranslation()
const { data: response } = useSWR({ url: `/apps/${id}/workflow/statistics/token-costs`, params: period.query }, getAppTokenCosts)
if (!response)
const { data: response, isLoading } = useWorkflowTokenCosts(id, period.query)
if (isLoading || !response)
return <Loading />
const noDataFlag = !response.data || response.data.length === 0
return <Chart
@@ -443,8 +455,8 @@ export const WorkflowCostChart: FC<IBizChartProps> = ({ id, period }) => {
export const AvgUserInteractions: FC<IBizChartProps> = ({ id, period }) => {
const { t } = useTranslation()
const { data: response } = useSWR({ url: `/apps/${id}/workflow/statistics/average-app-interactions`, params: period.query }, getAppStatistics)
if (!response)
const { data: response, isLoading } = useWorkflowAverageInteractions(id, period.query)
if (isLoading || !response)
return <Loading />
const noDataFlag = !response.data || response.data.length === 0
return <Chart

View File

@@ -23,7 +23,7 @@ const Empty = () => {
return (
<>
<DefaultCards />
<div className='absolute inset-0 z-20 flex items-center justify-center bg-gradient-to-t from-background-body to-transparent pointer-events-none'>
<div className='pointer-events-none absolute inset-0 z-20 flex items-center justify-center bg-gradient-to-t from-background-body to-transparent'>
<span className='system-md-medium text-text-tertiary'>
{t('app.newApp.noAppsFound')}
</span>

View File

@@ -4,7 +4,6 @@ import { useCallback, useEffect, useRef, useState } from 'react'
import {
useRouter,
} from 'next/navigation'
import useSWRInfinite from 'swr/infinite'
import { useTranslation } from 'react-i18next'
import { useDebounceFn } from 'ahooks'
import {
@@ -19,8 +18,6 @@ import AppCard from './app-card'
import NewAppCard from './new-app-card'
import useAppsQueryState from './hooks/use-apps-query-state'
import { useDSLDragDrop } from './hooks/use-dsl-drag-drop'
import type { AppListResponse } from '@/models/app'
import { fetchAppList } from '@/service/apps'
import { useAppContext } from '@/context/app-context'
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
import { CheckModal } from '@/hooks/use-pay'
@@ -35,6 +32,7 @@ import Empty from './empty'
import Footer from './footer'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { AppModeEnum } from '@/types/app'
import { useInfiniteAppList } from '@/service/use-apps'
const TagManagementModal = dynamic(() => import('@/app/components/base/tag-management'), {
ssr: false,
@@ -43,30 +41,6 @@ const CreateFromDSLModal = dynamic(() => import('@/app/components/app/create-fro
ssr: false,
})
const getKey = (
pageIndex: number,
previousPageData: AppListResponse,
activeTab: string,
isCreatedByMe: boolean,
tags: string[],
keywords: string,
) => {
if (!pageIndex || previousPageData.has_more) {
const params: any = { url: 'apps', params: { page: pageIndex + 1, limit: 30, name: keywords, is_created_by_me: isCreatedByMe } }
if (activeTab !== 'all')
params.params.mode = activeTab
else
delete params.params.mode
if (tags.length)
params.params.tag_ids = tags
return params
}
return null
}
const List = () => {
const { t } = useTranslation()
const { systemFeatures } = useGlobalPublicStore()
@@ -102,16 +76,24 @@ const List = () => {
enabled: isCurrentWorkspaceEditor,
})
const { data, isLoading, error, setSize, mutate } = useSWRInfinite(
(pageIndex: number, previousPageData: AppListResponse) => getKey(pageIndex, previousPageData, activeTab, isCreatedByMe, tagIDs, searchKeywords),
fetchAppList,
{
revalidateFirstPage: true,
shouldRetryOnError: false,
dedupingInterval: 500,
errorRetryCount: 3,
},
)
const appListQueryParams = {
page: 1,
limit: 30,
name: searchKeywords,
tag_ids: tagIDs,
is_created_by_me: isCreatedByMe,
...(activeTab !== 'all' ? { mode: activeTab as AppModeEnum } : {}),
}
const {
data,
isLoading,
isFetchingNextPage,
fetchNextPage,
hasNextPage,
error,
refetch,
} = useInfiniteAppList(appListQueryParams, { enabled: !isCurrentWorkspaceDatasetOperator })
const anchorRef = useRef<HTMLDivElement>(null)
const options = [
@@ -126,9 +108,9 @@ const List = () => {
useEffect(() => {
if (localStorage.getItem(NEED_REFRESH_APP_LIST_KEY) === '1') {
localStorage.removeItem(NEED_REFRESH_APP_LIST_KEY)
mutate()
refetch()
}
}, [mutate, t])
}, [refetch])
useEffect(() => {
if (isCurrentWorkspaceDatasetOperator)
@@ -136,7 +118,9 @@ const List = () => {
}, [router, isCurrentWorkspaceDatasetOperator])
useEffect(() => {
const hasMore = data?.at(-1)?.has_more ?? true
if (isCurrentWorkspaceDatasetOperator)
return
const hasMore = hasNextPage ?? true
let observer: IntersectionObserver | undefined
if (error) {
@@ -151,8 +135,8 @@ const List = () => {
const dynamicMargin = Math.max(100, Math.min(containerHeight * 0.2, 200)) // Clamps to 100-200px range, using 20% of container height as the base value
observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && !isLoading && !error && hasMore)
setSize((size: number) => size + 1)
if (entries[0].isIntersecting && !isLoading && !isFetchingNextPage && !error && hasMore)
fetchNextPage()
}, {
root: containerRef.current,
rootMargin: `${dynamicMargin}px`,
@@ -161,7 +145,7 @@ const List = () => {
observer.observe(anchorRef.current)
}
return () => observer?.disconnect()
}, [isLoading, setSize, data, error])
}, [isLoading, isFetchingNextPage, fetchNextPage, error, hasNextPage, isCurrentWorkspaceDatasetOperator])
const { run: handleSearch } = useDebounceFn(() => {
setSearchKeywords(keywords)
@@ -185,6 +169,9 @@ const List = () => {
setQuery(prev => ({ ...prev, isCreatedByMe: newValue }))
}, [isCreatedByMe, setQuery])
const pages = data?.pages ?? []
const hasAnyApp = (pages[0]?.total ?? 0) > 0
return (
<>
<div ref={containerRef} className='relative flex h-0 shrink-0 grow flex-col overflow-y-auto bg-background-body'>
@@ -217,17 +204,17 @@ const List = () => {
/>
</div>
</div>
{(data && data[0].total > 0)
{hasAnyApp
? <div className='relative grid grow grid-cols-1 content-start gap-4 px-12 pt-2 sm:grid-cols-1 md:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-5 2k:grid-cols-6'>
{isCurrentWorkspaceEditor
&& <NewAppCard ref={newAppCardRef} onSuccess={mutate} selectedAppType={activeTab} />}
{data.map(({ data: apps }) => apps.map(app => (
<AppCard key={app.id} app={app} onRefresh={mutate} />
&& <NewAppCard ref={newAppCardRef} onSuccess={refetch} selectedAppType={activeTab} />}
{pages.map(({ data: apps }) => apps.map(app => (
<AppCard key={app.id} app={app} onRefresh={refetch} />
)))}
</div>
: <div className='relative grid grow grid-cols-1 content-start gap-4 overflow-hidden px-12 pt-2 sm:grid-cols-1 md:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-5 2k:grid-cols-6'>
{isCurrentWorkspaceEditor
&& <NewAppCard ref={newAppCardRef} className='z-10' onSuccess={mutate} selectedAppType={activeTab} />}
&& <NewAppCard ref={newAppCardRef} className='z-10' onSuccess={refetch} selectedAppType={activeTab} />}
<Empty />
</div>}
@@ -261,7 +248,7 @@ const List = () => {
onSuccess={() => {
setShowCreateFromDSLModal(false)
setDroppedDSLFile(undefined)
mutate()
refetch()
}}
droppedFile={droppedDSLFile}
/>

View File

@@ -7,7 +7,6 @@ import type {
ChatConfig,
ChatItemInTree,
Feedback,
Memory,
} from '../types'
import type { ThemeBuilder } from '../embedded-chatbot/theme/theme-context'
import type {
@@ -61,14 +60,6 @@ export type ChatWithHistoryContextValue = {
name?: string
avatar_url?: string
}
showChatMemory?: boolean
setShowChatMemory: (state: boolean) => void
memoryList: Memory[]
clearAllMemory: () => void
updateMemory: (memory: Memory, content: string) => void
resetDefault: (memory: Memory) => void
clearAllUpdateVersion: (memory: Memory) => void
switchMemoryVersion: (memory: Memory, version: string) => void
}
export const ChatWithHistoryContext = createContext<ChatWithHistoryContextValue>({
@@ -104,13 +95,5 @@ export const ChatWithHistoryContext = createContext<ChatWithHistoryContextValue>
setCurrentConversationInputs: noop,
allInputsHidden: false,
initUserVariables: {},
showChatMemory: false,
setShowChatMemory: noop,
memoryList: [],
clearAllMemory: noop,
updateMemory: noop,
resetDefault: noop,
clearAllUpdateVersion: noop,
switchMemoryVersion: noop,
})
export const useChatWithHistoryContext = () => useContext(ChatWithHistoryContext)

View File

@@ -28,8 +28,6 @@ const HeaderInMobile = () => {
handleRenameConversation,
conversationRenaming,
inputsForms,
showChatMemory,
setShowChatMemory,
} = useChatWithHistoryContext()
const { t } = useTranslation()
const isPin = pinnedConversationList.some(item => item.id === currentConversationId)
@@ -62,9 +60,6 @@ const HeaderInMobile = () => {
if (showRename)
handleRenameConversation(showRename.id, newName, { onSuccess: handleCancelRename })
}, [showRename, handleRenameConversation, handleCancelRename])
const handleChatMemoryToggle = useCallback(() => {
setShowChatMemory(!showChatMemory)
}, [setShowChatMemory, showChatMemory])
const [showSidebar, setShowSidebar] = useState(false)
const [showChatSettings, setShowChatSettings] = useState(false)
@@ -103,7 +98,6 @@ const HeaderInMobile = () => {
)}
</div>
<MobileOperationDropdown
handleChatMemoryToggle={handleChatMemoryToggle}
handleResetChat={handleNewConversation}
handleViewChatSettings={() => setShowChatSettings(true)}
hideViewChatSettings={inputsForms.length < 1}

View File

@@ -1,11 +1,10 @@
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import {
RiEditBoxLine,
RiLayoutRight2Line,
RiResetLeftLine,
} from '@remixicon/react'
import { Memory } from '@/app/components/base/icons/src/vender/line/others'
import { useTranslation } from 'react-i18next'
import {
useChatWithHistoryContext,
} from '../context'
@@ -35,8 +34,6 @@ const Header = () => {
sidebarCollapseState,
handleSidebarCollapse,
isResponding,
showChatMemory,
setShowChatMemory,
} = useChatWithHistoryContext()
const { t } = useTranslation()
const isSidebarCollapsed = sidebarCollapseState
@@ -73,10 +70,6 @@ const Header = () => {
handleRenameConversation(showRename.id, newName, { onSuccess: handleCancelRename })
}, [showRename, handleRenameConversation, handleCancelRename])
const handleChatMemoryToggle = useCallback(() => {
setShowChatMemory(!showChatMemory)
}, [setShowChatMemory, showChatMemory])
return (
<>
<div className='flex h-14 shrink-0 items-center justify-between p-3'>
@@ -144,15 +137,6 @@ const Header = () => {
{currentConversationId && inputsForms.length > 0 && (
<ViewFormDropdown />
)}
{currentConversationId && (
<Tooltip
popupContent={t('share.chat.memory.actionButton')}
>
<ActionButton size='l' state={showChatMemory ? ActionButtonState.Active : ActionButtonState.Default} onClick={handleChatMemoryToggle}>
<Memory className='h-[18px] w-[18px]' />
</ActionButton>
</Tooltip>
)}
</div>
</div>
{!!showConfirm && (

View File

@@ -9,14 +9,12 @@ import ActionButton, { ActionButtonState } from '@/app/components/base/action-bu
type Props = {
handleResetChat: () => void
handleViewChatSettings: () => void
handleChatMemoryToggle?: () => void
hideViewChatSettings?: boolean
}
const MobileOperationDropdown = ({
handleResetChat,
handleViewChatSettings,
handleChatMemoryToggle,
hideViewChatSettings = false,
}: Props) => {
const { t } = useTranslation()
@@ -46,9 +44,6 @@ const MobileOperationDropdown = ({
<div className='system-md-regular flex cursor-pointer items-center space-x-1 rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover' onClick={handleResetChat}>
<span className='grow'>{t('share.chat.resetChat')}</span>
</div>
<div className='system-md-regular flex cursor-pointer items-center space-x-1 rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover' onClick={handleChatMemoryToggle}>
<span className='grow'>{t('share.chat.memory.actionButton')}</span>
</div>
{!hideViewChatSettings && (
<div className='system-md-regular flex cursor-pointer items-center space-x-1 rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover' onClick={handleViewChatSettings}>
<span className='grow'>{t('share.chat.viewChatSettings')}</span>

View File

@@ -21,11 +21,8 @@ import { addFileInfos, sortAgentSorts } from '../../../tools/utils'
import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils'
import {
delConversation,
deleteMemory,
editMemory,
fetchChatList,
fetchConversations,
fetchMemories,
generationConversationName,
pinConversation,
renameConversation,
@@ -44,9 +41,6 @@ import { InputVarType } from '@/app/components/workflow/types'
import { TransferMethod } from '@/types/app'
import { noop } from 'lodash-es'
import { useWebAppStore } from '@/context/web-app-context'
import type { Memory } from '@/app/components/base/chat/types'
import { mockMemoryList } from '@/app/components/base/chat/chat-with-history/memory/mock'
function getFormattedChatList(messages: any[]) {
const newChatList: ChatItem[] = []
@@ -532,61 +526,6 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
notify({ type: 'success', message: t('common.api.success') })
}, [isInstalledApp, appId, t, notify])
const [showChatMemory, setShowChatMemory] = useState(false)
const [memoryList, setMemoryList] = useState<Memory[]>(mockMemoryList)
const getMemoryList = useCallback(async (currentConversationId: string) => {
const memories = await fetchMemories(currentConversationId, '', '', isInstalledApp, appId)
setMemoryList(memories)
}, [isInstalledApp, appId])
const clearAllMemory = useCallback(async () => {
await deleteMemory('', isInstalledApp, appId)
notify({ type: 'success', message: t('common.api.success') })
getMemoryList(currentConversationId)
}, [currentConversationId, getMemoryList])
const resetDefault = useCallback(async (memory: Memory) => {
try {
await editMemory(memory.spec.id, memory.spec.template, isInstalledApp, appId)
getMemoryList(currentConversationId)
}
catch (error) {
console.error('Failed to reset memory:', error)
}
}, [currentConversationId, getMemoryList, isInstalledApp, appId])
const clearAllUpdateVersion = useCallback(async (memory: Memory) => {
await deleteMemory(memory.spec.id, isInstalledApp, appId)
notify({ type: 'success', message: t('common.api.success') })
getMemoryList(currentConversationId)
}, [currentConversationId, getMemoryList])
const switchMemoryVersion = useCallback(async (memory: Memory, version: string) => {
const memories = await fetchMemories(currentConversationId, memory.spec.id, version, isInstalledApp, appId)
const newMemory = memories[0]
const newList = produce(memoryList, (draft) => {
const index = draft.findIndex(item => item.spec.id === memory.spec.id)
if (index !== -1)
draft[index] = newMemory
})
setMemoryList(newList)
}, [memoryList, currentConversationId, isInstalledApp, appId])
const updateMemory = useCallback(async (memory: Memory, content: string) => {
try {
await editMemory(memory.spec.id, content, isInstalledApp, appId)
getMemoryList(currentConversationId)
}
catch (error) {
console.error('Failed to reset memory:', error)
}
}, [getMemoryList, currentConversationId, isInstalledApp, appId])
useEffect(() => {
getMemoryList(currentConversationId)
}, [currentConversationId, getMemoryList])
return {
isInstalledApp,
appId,
@@ -633,13 +572,5 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
setCurrentConversationInputs,
allInputsHidden,
initUserVariables,
showChatMemory,
setShowChatMemory,
memoryList,
clearAllMemory,
updateMemory,
resetDefault,
clearAllUpdateVersion,
switchMemoryVersion,
}
}

View File

@@ -14,7 +14,6 @@ import Sidebar from './sidebar'
import Header from './header'
import HeaderInMobile from './header-in-mobile'
import ChatWrapper from './chat-wrapper'
import MemoryPanel from './memory'
import type { InstalledApp } from '@/models/explore'
import Loading from '@/app/components/base/loading'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
@@ -34,14 +33,6 @@ const ChatWithHistory: FC<ChatWithHistoryProps> = ({
isMobile,
themeBuilder,
sidebarCollapseState,
showChatMemory,
setShowChatMemory,
memoryList,
clearAllMemory,
updateMemory,
resetDefault,
clearAllUpdateVersion,
switchMemoryVersion,
} = useChatWithHistoryContext()
const isSidebarCollapsed = sidebarCollapseState
const customConfig = appData?.custom_config
@@ -77,7 +68,7 @@ const ChatWithHistory: FC<ChatWithHistoryProps> = ({
{isMobile && (
<HeaderInMobile />
)}
<div className={cn('relative flex grow p-2', isMobile && 'h-[calc(100%_-_56px)] p-0')}>
<div className={cn('relative grow p-2', isMobile && 'h-[calc(100%_-_56px)] p-0')}>
{isSidebarCollapsed && (
<div
className={cn(
@@ -90,11 +81,7 @@ const ChatWithHistory: FC<ChatWithHistoryProps> = ({
<Sidebar isPanel panelVisible={showSidePanel} />
</div>
)}
<div className={cn(
'flex h-full grow flex-col overflow-hidden border-[0,5px] border-components-panel-border-subtle bg-chatbot-bg',
isMobile ? 'rounded-t-2xl' : 'rounded-2xl',
showChatMemory && !isMobile && 'mr-1',
)}>
<div className={cn('flex h-full flex-col overflow-hidden border-[0,5px] border-components-panel-border-subtle bg-chatbot-bg', isMobile ? 'rounded-t-2xl' : 'rounded-2xl')}>
{!isMobile && <Header />}
{appChatListDataLoading && (
<Loading type='app' />
@@ -103,38 +90,6 @@ const ChatWithHistory: FC<ChatWithHistoryProps> = ({
<ChatWrapper key={chatShouldReloadKey} />
)}
</div>
{!isMobile && (
<MemoryPanel
isMobile={isMobile}
showChatMemory={showChatMemory}
setShowChatMemory={setShowChatMemory}
memoryList={memoryList}
clearAllMemory={clearAllMemory}
updateMemory={updateMemory}
resetDefault={resetDefault}
clearAllUpdateVersion={clearAllUpdateVersion}
switchMemoryVersion={switchMemoryVersion}
/>
)}
{isMobile && showChatMemory && (
<div className='fixed inset-0 z-50 flex flex-row-reverse bg-background-overlay p-1 backdrop-blur-sm'
onClick={() => setShowChatMemory(false)}
>
<div className='flex h-full w-[360px] rounded-xl shadow-lg' onClick={e => e.stopPropagation()}>
<MemoryPanel
isMobile={isMobile}
showChatMemory={showChatMemory}
setShowChatMemory={setShowChatMemory}
memoryList={memoryList}
clearAllMemory={clearAllMemory}
updateMemory={updateMemory}
resetDefault={resetDefault}
clearAllUpdateVersion={clearAllUpdateVersion}
switchMemoryVersion={switchMemoryVersion}
/>
</div>
</div>
)}
</div>
</div>
)
@@ -190,14 +145,6 @@ const ChatWithHistoryWrap: FC<ChatWithHistoryWrapProps> = ({
setCurrentConversationInputs,
allInputsHidden,
initUserVariables,
showChatMemory,
setShowChatMemory,
memoryList,
clearAllMemory,
updateMemory,
resetDefault,
clearAllUpdateVersion,
switchMemoryVersion,
} = useChatWithHistory(installedAppInfo)
return (
@@ -241,14 +188,6 @@ const ChatWithHistoryWrap: FC<ChatWithHistoryWrapProps> = ({
setCurrentConversationInputs,
allInputsHidden,
initUserVariables,
showChatMemory,
setShowChatMemory,
memoryList,
clearAllMemory,
updateMemory,
resetDefault,
clearAllUpdateVersion,
switchMemoryVersion,
}}>
<ChatWithHistory className={className} />
</ChatWithHistoryContext.Provider>

View File

@@ -1,128 +0,0 @@
'use client'
import React, { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { RiCloseLine } from '@remixicon/react'
import { Memory } from '@/app/components/base/icons/src/vender/line/others'
import Modal from '@/app/components/base/modal'
import ActionButton from '@/app/components/base/action-button'
import Badge from '@/app/components/base/badge'
import Button from '@/app/components/base/button'
import Textarea from '@/app/components/base/textarea'
import Divider from '@/app/components/base/divider'
import Toast from '@/app/components/base/toast'
import type { Memory as MemoryItem } from '@/app/components/base/chat/types'
import { noop } from 'lodash-es'
import cn from '@/utils/classnames'
type Props = {
memory: MemoryItem
show: boolean
onConfirm: (info: MemoryItem, content: string) => Promise<void>
onHide: () => void
isMobile?: boolean
}
const MemoryEditModal = ({
memory,
show = false,
onConfirm,
onHide,
isMobile,
}: Props) => {
const { t } = useTranslation()
const [content, setContent] = React.useState(memory.value)
const versionTag = useMemo(() => {
const res = `${t('share.chat.memory.updateVersion.update')} ${memory.version}`
if (memory.edited_by_user)
return `${res} · ${t('share.chat.memory.updateVersion.edited')}`
return res
}, [memory.version, t])
const reset = () => {
setContent(memory.value)
}
const submit = () => {
if (!content.trim()) {
Toast.notify({ type: 'error', message: 'content is required' })
return
}
onConfirm(memory, content)
onHide()
}
if (isMobile) {
return (
<div className='fixed inset-0 z-50 flex flex-col bg-background-overlay pt-3 backdrop-blur-sm'
onClick={onHide}
>
<div className='relative flex w-full grow flex-col rounded-t-xl bg-components-panel-bg shadow-xl' onClick={e => e.stopPropagation()}>
<div className='absolute right-4 top-4 cursor-pointer p-2'>
<ActionButton onClick={onHide}>
<RiCloseLine className='h-5 w-5' />
</ActionButton>
</div>
<div className='p-4 pb-3'>
<div className='title-2xl-semi-bold mb-2 text-text-primary'>{t('share.chat.memory.editTitle')}</div>
<div className='flex items-center gap-1 pb-1 pt-2'>
<Memory className='h-4 w-4 shrink-0 text-util-colors-teal-teal-700' />
<div className='system-sm-semibold truncate text-text-primary'>{memory.spec.name}</div>
{memory.version > 1 && <Badge text={versionTag} className='!h-4' />}
</div>
</div>
<div className='grow px-4'>
<Textarea
className='h-full'
value={content}
onChange={e => setContent(e.target.value)}
/>
</div>
<div className='flex flex-row-reverse items-center p-4'>
<Button className='ml-2' variant='primary' onClick={submit}>{t('share.chat.memory.operations.save')}</Button>
<Button className='ml-3' onClick={onHide}>{t('share.chat.memory.operations.cancel')}</Button>
<Divider type='vertical' className='!mx-0 !h-4' />
<Button className='mr-3' onClick={reset}>{t('share.chat.memory.operations.reset')}</Button>
</div>
</div>
</div>
)
}
return (
<Modal
isShow={show}
onClose={noop}
className={cn('relative !max-w-[800px]', 'p-0')}
>
<div className='absolute right-5 top-5 cursor-pointer p-2'>
<ActionButton onClick={onHide}>
<RiCloseLine className='h-5 w-5' />
</ActionButton>
</div>
<div className='p-6 pb-3'>
<div className='title-2xl-semi-bold mb-2 text-text-primary'>{t('share.chat.memory.editTitle')}</div>
<div className='flex items-center gap-1 pb-1 pt-2'>
<Memory className='h-4 w-4 shrink-0 text-util-colors-teal-teal-700' />
<div className='system-sm-semibold truncate text-text-primary'>{memory.spec.name}</div>
{memory.version > 1 && <Badge text={versionTag} />}
</div>
</div>
<div className='px-6'>
<Textarea
className='h-[562px]'
value={content}
onChange={e => setContent(e.target.value)}
/>
</div>
<div className='flex flex-row-reverse items-center p-6 pt-5'>
<Button className='ml-2' variant='primary' onClick={submit}>{t('share.chat.memory.operations.save')}</Button>
<Button className='ml-3' onClick={onHide}>{t('share.chat.memory.operations.cancel')}</Button>
<Divider type='vertical' className='!mx-0 !h-4' />
<Button className='mr-3' onClick={reset}>{t('share.chat.memory.operations.reset')}</Button>
</div>
</Modal>
)
}
export default MemoryEditModal

View File

@@ -1,127 +0,0 @@
import React, { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import {
RiArrowDownSLine,
RiArrowUpSLine,
} from '@remixicon/react'
import { Memory } from '@/app/components/base/icons/src/vender/line/others'
import ActionButton from '@/app/components/base/action-button'
import Badge from '@/app/components/base/badge'
import Indicator from '@/app/components/header/indicator'
import Operation from './operation'
import MemoryEditModal from './edit-modal'
import type { Memory as MemoryItem } from '@/app/components/base/chat/types'
import cn from '@/utils/classnames'
type Props = {
isMobile?: boolean
memory: MemoryItem
updateMemory: (memory: MemoryItem, content: string) => void
resetDefault: (memory: MemoryItem) => void
clearAllUpdateVersion: (memory: MemoryItem) => void
switchMemoryVersion: (memory: MemoryItem, version: string) => void
}
const MemoryCard: React.FC<Props> = ({
isMobile,
memory,
updateMemory,
resetDefault,
clearAllUpdateVersion,
switchMemoryVersion,
}) => {
const { t } = useTranslation()
const [isHovering, setIsHovering] = React.useState(false)
const [showEditModal, setShowEditModal] = React.useState(false)
const versionTag = useMemo(() => {
const res = `${t('share.chat.memory.updateVersion.update')} ${memory.version}`
if (memory.edited_by_user)
return `${res} · ${t('share.chat.memory.updateVersion.edited')}`
return res
}, [memory.version, t])
const isLatest = useMemo(() => {
if (memory.conversation_metadata)
return memory.conversation_metadata.visible_count === memory.spec.preserved_turns
return true
}, [memory])
const waitMergeCount = useMemo(() => {
if (memory.conversation_metadata)
return memory.conversation_metadata.visible_count - memory.spec.preserved_turns
return 0
}, [memory])
const prevVersion = () => {
if (memory.version > 1)
switchMemoryVersion(memory, (memory.version - 1).toString())
}
const nextVersion = () => {
switchMemoryVersion(memory, (memory.version + 1).toString())
}
return (
<>
<div
className={cn('group mb-1 rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg shadow-xs hover:bg-components-panel-on-panel-item-bg-hover hover:shadow-md')}
onMouseEnter={() => setIsHovering(true)}
onMouseLeave={() => setIsHovering(false)}
>
<div className='relative flex items-end justify-between pb-1 pl-4 pr-2 pt-2'>
<div className='flex items-center gap-1 pb-1 pt-2'>
<Memory className='h-4 w-4 shrink-0 text-util-colors-teal-teal-700' />
<div className='system-sm-semibold truncate text-text-primary'>{memory.spec.name}</div>
{memory.version > 1 && <Badge text={versionTag} className='!h-4' />}
</div>
{isHovering && (
<div className='hover:bg-components-actionbar-bg-hover absolute bottom-0 right-2 flex items-center gap-0.5 rounded-lg border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 shadow-md'>
<ActionButton onClick={prevVersion}><RiArrowUpSLine className='h-4 w-4' /></ActionButton>
<ActionButton onClick={nextVersion}><RiArrowDownSLine className='h-4 w-4' /></ActionButton>
<Operation
memory={memory}
onEdit={() => {
setShowEditModal(true)
setIsHovering(false)
}}
resetDefault={resetDefault}
clearAllUpdateVersion={clearAllUpdateVersion}
switchMemoryVersion={switchMemoryVersion}
/>
</div>
)}
</div>
<div className='system-xs-regular line-clamp-[12] px-4 pb-2 pt-1 text-text-tertiary'>{memory.value}</div>
{isLatest && (
<div className='flex items-center gap-1 rounded-b-xl border-t-[0.5px] border-divider-subtle bg-background-default-subtle px-4 py-3 group-hover:bg-components-panel-on-panel-item-bg-hover'>
<div className='system-xs-regular text-text-tertiary'>{t('share.chat.memory.latestVersion')}</div>
<Indicator color='green' />
</div>
)}
{!isLatest && (
<div className='flex items-center gap-1 rounded-b-xl border-t-[0.5px] border-divider-subtle bg-background-default-subtle px-4 py-3 group-hover:bg-components-panel-on-panel-item-bg-hover'>
<div className='system-xs-regular text-text-tertiary'>{t('share.chat.memory.notLatestVersion', { num: waitMergeCount })}</div>
<Indicator color='orange' />
</div>
)}
</div>
{showEditModal && (
<MemoryEditModal
isMobile={isMobile}
show={showEditModal}
memory={memory}
onConfirm={async (info, content) => {
await updateMemory(info, content)
setShowEditModal(false)
}}
onHide={() => setShowEditModal(false)}
/>
)}
</>
)
}
export default MemoryCard

View File

@@ -1,88 +0,0 @@
'use client'
import type { FC } from 'react'
import React, { useCallback, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { RiCheckLine, RiMoreFill } from '@remixicon/react'
import ActionButton from '@/app/components/base/action-button'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import Divider from '@/app/components/base/divider'
import type { Memory } from '@/app/components/base/chat/types'
import cn from '@/utils/classnames'
type Props = {
memory: Memory
onEdit: () => void
resetDefault: (memory: Memory) => void
clearAllUpdateVersion: (memory: Memory) => void
switchMemoryVersion: (memory: Memory, version: string) => void
}
const OperationDropdown: FC<Props> = ({
memory,
onEdit,
resetDefault,
clearAllUpdateVersion,
switchMemoryVersion,
}) => {
const { t } = useTranslation()
const [open, doSetOpen] = useState(false)
const openRef = useRef(open)
const setOpen = useCallback((v: boolean) => {
doSetOpen(v)
openRef.current = v
}, [doSetOpen])
const handleTrigger = useCallback(() => {
setOpen(!openRef.current)
}, [setOpen])
return (
<PortalToFollowElem
open={open}
onOpenChange={setOpen}
placement='bottom-end'
offset={{
mainAxis: 4,
crossAxis: 4,
}}
>
<PortalToFollowElemTrigger onClick={handleTrigger}>
<div>
<ActionButton className={cn(open && 'bg-state-base-hover')}>
<RiMoreFill className='h-4 w-4' />
</ActionButton>
</div>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-50'>
<div className='w-[220px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-sm'>
<div className='p-1'>
<div className='system-md-regular cursor-pointer rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover' onClick={onEdit}>{t('share.chat.memory.operations.edit')}</div>
<div className='system-md-regular cursor-pointer rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover' onClick={() => resetDefault(memory)}>{t('share.chat.memory.operations.reset')}</div>
<div className='system-md-regular cursor-pointer rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-destructive-hover hover:text-text-destructive' onClick={() => clearAllUpdateVersion(memory)}>{t('share.chat.memory.operations.clear')}</div>
</div>
<Divider className='!my-0 !h-px bg-divider-subtle' />
<div className='px-1 py-2'>
<div className='system-xs-medium-uppercase px-3 pb-0.5 pt-1 text-text-tertiary'>{t('share.chat.memory.updateVersion.title')}</div>
<div className='system-md-regular flex cursor-pointer items-center gap-1 rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover'>
{t('share.chat.memory.operations.edit')}
<RiCheckLine className='h-4 w-4 text-text-accent' />
</div>
<div className='system-md-regular flex cursor-pointer items-center gap-1 rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover'>
{t('share.chat.memory.operations.edit')}
<RiCheckLine className='h-4 w-4 text-text-accent' />
</div>
<div className='system-md-regular flex cursor-pointer items-center gap-1 rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover'>
{t('share.chat.memory.operations.edit')}
<RiCheckLine className='h-4 w-4 text-text-accent' />
</div>
</div>
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
)
}
export default React.memo(OperationDropdown)

View File

@@ -1,80 +0,0 @@
import React from 'react'
import { useTranslation } from 'react-i18next'
import {
RiCloseLine,
RiDeleteBinLine,
} from '@remixicon/react'
import ActionButton from '@/app/components/base/action-button'
import Button from '@/app/components/base/button'
import MemoryCard from './card'
import cn from '@/utils/classnames'
import type { Memory } from '@/app/components/base/chat/types'
type Props = {
isMobile?: boolean
showChatMemory?: boolean
setShowChatMemory: (show: boolean) => void
memoryList: Memory[]
clearAllMemory: () => void
updateMemory: (memory: Memory, content: string) => void
resetDefault: (memory: Memory) => void
clearAllUpdateVersion: (memory: Memory) => void
switchMemoryVersion: (memory: Memory, version: string) => void
}
const MemoryPanel: React.FC<Props> = ({
isMobile,
showChatMemory,
setShowChatMemory,
memoryList,
clearAllMemory,
updateMemory,
resetDefault,
clearAllUpdateVersion,
switchMemoryVersion,
}) => {
const { t } = useTranslation()
return (
<div className={cn(
'flex h-full w-[360px] shrink-0 flex-col rounded-2xl border-[0.5px] border-components-panel-border-subtle bg-chatbot-bg transition-all ease-in-out',
showChatMemory ? 'w-[360px]' : 'w-0 opacity-0',
)}>
<div className='flex shrink-0 items-center border-b-[0.5px] border-components-panel-border-subtle pl-4 pr-3.5 pt-2'>
<div className='system-md-semibold-uppercase grow py-3 text-text-primary'>{t('share.chat.memory.title')}</div>
<ActionButton size='l' onClick={() => setShowChatMemory(false)}>
<RiCloseLine className='h-[18px] w-[18px]' />
</ActionButton>
</div>
<div className='h-0 grow overflow-y-auto px-3 pt-2'>
{memoryList.map(memory => (
<MemoryCard
key={memory.spec.id}
isMobile={isMobile}
memory={memory}
updateMemory={updateMemory}
resetDefault={resetDefault}
clearAllUpdateVersion={clearAllUpdateVersion}
switchMemoryVersion={switchMemoryVersion}
/>
))}
{memoryList.length > 0 && (
<div className='flex items-center justify-center'>
<Button variant='ghost' onClick={clearAllMemory}>
<RiDeleteBinLine className='mr-1 h-3.5 w-3.5' />
{t('share.chat.memory.clearAll')}
</Button>
</div>
)}
{memoryList.length === 0 && (
<div className='system-xs-regular flex items-center justify-center py-2 text-text-tertiary'>
{t('share.chat.memory.empty')}
</div>
)}
</div>
</div>
)
}
export default MemoryPanel

View File

@@ -1,96 +0,0 @@
import type { Memory as MemoryItem } from '@/app/components/base/chat/types'
export const mockMemoryList: MemoryItem[] = [
{
tenant_id: 'user-tenant-id',
value: `Learning Goal: [What you\'re studying]
Current Level: [Beginner/Intermediate/Advanced]
Learning Style: [Visual, hands-on, theoretical, etc.]
Progress: [Topics mastered, current focus]
Preferred Pace: [Fast/moderate/slow explanations]
Background: [Relevant experience or education]
Time Constraints: [Available study time]`,
app_id: 'user-app-id',
conversation_id: '',
version: 1,
edited_by_user: false,
conversation_metadata: {
type: 'mutable_visible_window',
visible_count: 5,
},
spec: {
id: 'learning_companion',
name: 'Learning Companion',
description: 'A companion to help with learning goals',
template: 'no zuo no die why you try', // default value
instruction: 'enjoy yourself',
scope: 'app', // app or node
term: 'session', // session or persistent
strategy: 'on_turns',
update_turns: 3,
preserved_turns: 5,
schedule_mode: 'sync', // sync or async
end_user_visible: true,
end_user_editable: true,
},
},
{
tenant_id: 'user-tenant-id',
value: `Research Topic: [Your research topic]
Current Progress: [Literature review, experiments, etc.]
Challenges: [What you\'re struggling with]
Goals: [Short-term and long-term research goals]`,
app_id: 'user-app-id',
conversation_id: '',
version: 1,
edited_by_user: false,
conversation_metadata: {
type: 'mutable_visible_window',
visible_count: 5,
},
spec: {
id: 'research_partner',
name: 'research_partner',
description: 'A companion to help with research goals',
template: 'no zuo no die why you try', // default value
instruction: 'enjoy yourself',
scope: 'app', // app or node
term: 'session', // session or persistent
strategy: 'on_turns',
update_turns: 3,
preserved_turns: 3,
schedule_mode: 'sync', // sync or async
end_user_visible: true,
end_user_editable: false,
},
},
{
tenant_id: 'user-tenant-id',
value: `Code Context: [Brief description of the codebase]
Current Issues: [Bugs, technical debt, etc.]
Goals: [Features to implement, improvements to make]`,
app_id: 'user-app-id',
conversation_id: '',
version: 3,
edited_by_user: true,
conversation_metadata: {
type: 'mutable_visible_window',
visible_count: 5,
},
spec: {
id: 'code_partner',
name: 'code_partner',
description: 'A companion to help with code-related tasks',
template: 'no zuo no die why you try', // default value
instruction: 'enjoy yourself',
scope: 'app', // app or node
term: 'session', // session or persistent
strategy: 'on_turns',
update_turns: 3,
preserved_turns: 5,
schedule_mode: 'sync', // sync or async
end_user_visible: true,
end_user_editable: true,
},
},
]

View File

@@ -6,7 +6,6 @@ import type {
ChatConfig,
ChatItem,
Feedback,
Memory,
} from '../types'
import type { ThemeBuilder } from './theme/theme-context'
import type {
@@ -54,14 +53,6 @@ export type EmbeddedChatbotContextValue = {
name?: string
avatar_url?: string
}
showChatMemory?: boolean
setShowChatMemory: (state: boolean) => void
memoryList: Memory[]
clearAllMemory: () => void
updateMemory: (memory: Memory, content: string) => void
resetDefault: (memory: Memory) => void
clearAllUpdateVersion: (memory: Memory) => void
switchMemoryVersion: (memory: Memory, version: string) => void
}
export const EmbeddedChatbotContext = createContext<EmbeddedChatbotContextValue>({
@@ -95,13 +86,5 @@ export const EmbeddedChatbotContext = createContext<EmbeddedChatbotContextValue>
setCurrentConversationInputs: noop,
allInputsHidden: false,
initUserVariables: {},
showChatMemory: false,
setShowChatMemory: noop,
memoryList: [],
clearAllMemory: noop,
updateMemory: noop,
resetDefault: noop,
clearAllUpdateVersion: noop,
switchMemoryVersion: noop,
})
export const useEmbeddedChatbotContext = () => useContext(EmbeddedChatbotContext)

View File

@@ -7,9 +7,8 @@ import { CssTransform } from '../theme/utils'
import {
useEmbeddedChatbotContext,
} from '../context'
import { Memory } from '@/app/components/base/icons/src/vender/line/others'
import Tooltip from '@/app/components/base/tooltip'
import ActionButton, { ActionButtonState } from '@/app/components/base/action-button'
import ActionButton from '@/app/components/base/action-button'
import Divider from '@/app/components/base/divider'
import ViewFormDropdown from '@/app/components/base/chat/embedded-chatbot/inputs-form/view-form-dropdown'
import DifyLogo from '@/app/components/base/logo/dify-logo'
@@ -37,8 +36,6 @@ const Header: FC<IHeaderProps> = ({
appData,
currentConversationId,
inputsForms,
showChatMemory,
setShowChatMemory,
allInputsHidden,
} = useEmbeddedChatbotContext()
@@ -80,10 +77,6 @@ const Header: FC<IHeaderProps> = ({
}, parentOrigin)
}, [isIframe, parentOrigin, showToggleExpandButton, expanded])
const handleChatMemoryToggle = useCallback(() => {
setShowChatMemory(!showChatMemory)
}, [setShowChatMemory, showChatMemory])
if (!isMobile) {
return (
<div className='flex h-14 shrink-0 items-center justify-end p-3'>
@@ -135,15 +128,6 @@ const Header: FC<IHeaderProps> = ({
{currentConversationId && inputsForms.length > 0 && !allInputsHidden && (
<ViewFormDropdown />
)}
{currentConversationId && (
<Tooltip
popupContent={t('share.chat.memory.actionButton')}
>
<ActionButton size='l' state={showChatMemory ? ActionButtonState.Active : ActionButtonState.Default} onClick={handleChatMemoryToggle}>
<Memory className='h-[18px] w-[18px]' />
</ActionButton>
</Tooltip>
)}
</div>
</div>
)
@@ -191,15 +175,6 @@ const Header: FC<IHeaderProps> = ({
{currentConversationId && inputsForms.length > 0 && !allInputsHidden && (
<ViewFormDropdown iconColor={theme?.colorPathOnHeader} />
)}
{currentConversationId && (
<Tooltip
popupContent={t('share.chat.memory.actionButton')}
>
<ActionButton size='l' onClick={handleChatMemoryToggle}>
<Memory className={cn('h-[18px] w-[18px]', theme?.colorPathOnHeader)} />
</ActionButton>
</Tooltip>
)}
</div>
</div>
)

View File

@@ -18,11 +18,8 @@ import { CONVERSATION_ID_INFO } from '../constants'
import { buildChatItemTree, getProcessedInputsFromUrlParams, getProcessedSystemVariablesFromUrlParams, getProcessedUserVariablesFromUrlParams } from '../utils'
import { getProcessedFilesFromResponse } from '../../file-uploader/utils'
import {
deleteMemory,
editMemory,
fetchChatList,
fetchConversations,
fetchMemories,
generationConversationName,
updateFeedback,
} from '@/service/share'
@@ -36,7 +33,6 @@ import { InputVarType } from '@/app/components/workflow/types'
import { TransferMethod } from '@/types/app'
import { addFileInfos, sortAgentSorts } from '@/app/components/tools/utils'
import { noop } from 'lodash-es'
import type { Memory } from '@/app/components/base/chat/types'
import { useWebAppStore } from '@/context/web-app-context'
function getFormattedChatList(messages: any[]) {
@@ -391,61 +387,6 @@ export const useEmbeddedChatbot = () => {
notify({ type: 'success', message: t('common.api.success') })
}, [isInstalledApp, appId, t, notify])
const [showChatMemory, setShowChatMemory] = useState(false)
const [memoryList, setMemoryList] = useState<Memory[]>([])
const getMemoryList = useCallback(async (currentConversationId: string) => {
const memories = await fetchMemories(currentConversationId, '', '', isInstalledApp, appId)
setMemoryList(memories)
}, [isInstalledApp, appId])
const clearAllMemory = useCallback(async () => {
await deleteMemory('', isInstalledApp, appId)
notify({ type: 'success', message: t('common.api.success') })
getMemoryList(currentConversationId)
}, [currentConversationId, getMemoryList])
const resetDefault = useCallback(async (memory: Memory) => {
try {
await editMemory(memory.spec.id, memory.spec.template, isInstalledApp, appId)
getMemoryList(currentConversationId)
}
catch (error) {
console.error('Failed to reset memory:', error)
}
}, [currentConversationId, getMemoryList, isInstalledApp, appId])
const clearAllUpdateVersion = useCallback(async (memory: Memory) => {
await deleteMemory(memory.spec.id, isInstalledApp, appId)
notify({ type: 'success', message: t('common.api.success') })
getMemoryList(currentConversationId)
}, [currentConversationId, getMemoryList])
const switchMemoryVersion = useCallback(async (memory: Memory, version: string) => {
const memories = await fetchMemories(currentConversationId, memory.spec.id, version, isInstalledApp, appId)
const newMemory = memories[0]
const newList = produce(memoryList, (draft) => {
const index = draft.findIndex(item => item.spec.id === memory.spec.id)
if (index !== -1)
draft[index] = newMemory
})
setMemoryList(newList)
}, [memoryList, currentConversationId, isInstalledApp, appId])
const updateMemory = useCallback(async (memory: Memory, content: string) => {
try {
await editMemory(memory.spec.id, content, isInstalledApp, appId)
getMemoryList(currentConversationId)
}
catch (error) {
console.error('Failed to reset memory:', error)
}
}, [getMemoryList, currentConversationId, isInstalledApp, appId])
useEffect(() => {
getMemoryList(currentConversationId)
}, [currentConversationId, getMemoryList])
return {
isInstalledApp,
allowResetChat,
@@ -485,13 +426,5 @@ export const useEmbeddedChatbot = () => {
setCurrentConversationInputs,
allInputsHidden,
initUserVariables,
showChatMemory,
setShowChatMemory,
memoryList,
clearAllMemory,
updateMemory,
resetDefault,
clearAllUpdateVersion,
switchMemoryVersion,
}
}

View File

@@ -16,7 +16,6 @@ import Loading from '@/app/components/base/loading'
import LogoHeader from '@/app/components/base/logo/logo-embedded-chat-header'
import Header from '@/app/components/base/chat/embedded-chatbot/header'
import ChatWrapper from '@/app/components/base/chat/embedded-chatbot/chat-wrapper'
import MemoryPanel from '@/app/components/base/chat/chat-with-history/memory'
import DifyLogo from '@/app/components/base/logo/dify-logo'
import cn from '@/utils/classnames'
import useDocumentTitle from '@/hooks/use-document-title'
@@ -31,14 +30,6 @@ const Chatbot = () => {
chatShouldReloadKey,
handleNewConversation,
themeBuilder,
showChatMemory,
setShowChatMemory,
memoryList,
clearAllMemory,
updateMemory,
resetDefault,
clearAllUpdateVersion,
switchMemoryVersion,
} = useEmbeddedChatbotContext()
const { t } = useTranslation()
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
@@ -99,25 +90,6 @@ const Chatbot = () => {
)}
</div>
)}
{showChatMemory && (
<div className='fixed inset-0 z-50 flex flex-row-reverse bg-background-overlay p-1 backdrop-blur-sm'
onClick={() => setShowChatMemory(false)}
>
<div className='flex h-full w-[360px] rounded-xl shadow-lg' onClick={e => e.stopPropagation()}>
<MemoryPanel
isMobile={isMobile}
showChatMemory={showChatMemory}
setShowChatMemory={setShowChatMemory}
memoryList={memoryList}
clearAllMemory={clearAllMemory}
updateMemory={updateMemory}
resetDefault={resetDefault}
clearAllUpdateVersion={clearAllUpdateVersion}
switchMemoryVersion={switchMemoryVersion}
/>
</div>
</div>
)}
</div>
)
}
@@ -159,14 +131,6 @@ const EmbeddedChatbotWrapper = () => {
setCurrentConversationInputs,
allInputsHidden,
initUserVariables,
showChatMemory,
setShowChatMemory,
memoryList,
clearAllMemory,
updateMemory,
resetDefault,
clearAllUpdateVersion,
switchMemoryVersion,
} = useEmbeddedChatbot()
return <EmbeddedChatbotContext.Provider value={{
@@ -203,14 +167,6 @@ const EmbeddedChatbotWrapper = () => {
setCurrentConversationInputs,
allInputsHidden,
initUserVariables,
showChatMemory,
setShowChatMemory,
memoryList,
clearAllMemory,
updateMemory,
resetDefault,
clearAllUpdateVersion,
switchMemoryVersion,
}}>
<Chatbot />
</EmbeddedChatbotContext.Provider>

View File

@@ -95,36 +95,3 @@ export type Feedback = {
rating: 'like' | 'dislike' | null
content?: string | null
}
export type MemorySpec = {
id: string
name: string
description: string
template: string // default value
instruction: string
scope: string // app or node
term: string // session or persistent
strategy: string
update_turns: number
preserved_turns: number
schedule_mode: string // sync or async
end_user_visible: boolean
end_user_editable: boolean
}
export type ConversationMetaData = {
type: string // mutable_visible_window
visible_count: number // visible_count - preserved_turns = N messages waiting merged
}
export type Memory = {
tenant_id: string
value: string
app_id: string
conversation_id?: string
node_id?: string
version: number
edited_by_user: boolean
conversation_metadata?: ConversationMetaData
spec: MemorySpec
}

View File

@@ -1,5 +1,4 @@
'use client'
import useSWR from 'swr'
import { produce } from 'immer'
import React, { Fragment } from 'react'
import { usePathname } from 'next/navigation'
@@ -9,7 +8,6 @@ import { Listbox, ListboxButton, ListboxOption, ListboxOptions, Transition } fro
import { CheckIcon, ChevronDownIcon } from '@heroicons/react/20/solid'
import { useFeatures, useFeaturesStore } from '@/app/components/base/features/hooks'
import type { Item } from '@/app/components/base/select'
import { fetchAppVoices } from '@/service/apps'
import Tooltip from '@/app/components/base/tooltip'
import Switch from '@/app/components/base/switch'
import AudioBtn from '@/app/components/base/audio-btn'
@@ -17,6 +15,7 @@ import { languages } from '@/i18n-config/language'
import { TtsAutoPlay } from '@/types/app'
import type { OnFeaturesChange } from '@/app/components/base/features/types'
import classNames from '@/utils/classnames'
import { useAppVoices } from '@/service/use-apps'
type VoiceParamConfigProps = {
onClose: () => void
@@ -39,7 +38,7 @@ const VoiceParamConfig = ({
const localLanguagePlaceholder = languageItem?.name || t('common.placeholder.select')
const language = languageItem?.value
const voiceItems = useSWR({ appId, language }, fetchAppVoices).data
const { data: voiceItems } = useAppVoices(appId, language)
let voiceItem = voiceItems?.find(item => item.value === text2speech?.voice)
if (voiceItems && !voiceItem)
voiceItem = voiceItems[0]

View File

@@ -9,12 +9,7 @@ import Tooltip from '@/app/components/base/tooltip'
import { useRenderI18nObject } from '@/hooks/use-i18n'
import { useTriggerPluginDynamicOptions } from '@/service/use-triggers'
import cn from '@/utils/classnames'
import {
RiArrowDownSFill,
RiDraftLine,
RiExternalLinkLine,
RiInputField,
} from '@remixicon/react'
import { RiExternalLinkLine } from '@remixicon/react'
import type { AnyFieldApi } from '@tanstack/react-form'
import { useStore } from '@tanstack/react-form'
import {
@@ -24,19 +19,6 @@ import {
useMemo,
} from 'react'
import { useTranslation } from 'react-i18next'
import Textarea from '@/app/components/base/textarea'
import PromptEditor from '@/app/components/base/prompt-editor'
import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal'
import ObjectValueList from '@/app/components/workflow/panel/chat-variable-panel/components/object-value-list'
import ArrayValueList from '@/app/components/workflow/panel/chat-variable-panel/components/array-value-list'
import ArrayBooleanValueList from '@/app/components/workflow/panel/chat-variable-panel/components/array-bool-list'
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
import Button from '@/app/components/base/button'
import PromptGeneratorBtn from '@/app/components/workflow/nodes/llm/components/prompt-generator-btn'
import Slider from '@/app/components/base/slider'
import Switch from '../../../switch'
import NodeSelector from '@/app/components/workflow/panel/chat-variable-panel/components/node-selector'
const getExtraProps = (type: FormTypeEnum) => {
switch (type) {
@@ -118,6 +100,7 @@ const BaseField = ({
options,
labelClassName: formLabelClassName,
disabled: formSchemaDisabled,
type: formItemType,
dynamicSelectParams,
multiple = false,
tooltip,
@@ -125,14 +108,7 @@ const BaseField = ({
description,
url,
help,
type: typeOrFn,
fieldClassName: formFieldClassName,
inputContainerClassName: formInputContainerClassName,
inputClassName: formInputClassName,
selfFormProps,
onChange: formOnChange,
} = formSchema
const formItemType = typeof typeOrFn === 'function' ? typeOrFn(field.form) : typeOrFn
const disabled = propsDisabled || formSchemaDisabled
const [translatedLabel, translatedPlaceholder, translatedTooltip, translatedDescription, translatedHelp] = useMemo(() => {
@@ -172,8 +148,7 @@ const BaseField = ({
return true
return option.show_on.every((condition) => {
const conditionValue = watchedValues[condition.variable]
return Array.isArray(condition.value) ? condition.value.includes(conditionValue) : conditionValue === condition.value
return watchedValues[condition.variable] === condition.value
})
}).map((option) => {
return {
@@ -205,67 +180,21 @@ const BaseField = ({
}))
}, [dynamicOptionsData, renderI18nObject])
const booleanRadioValue = useMemo(() => {
if (value === null || value === undefined)
return undefined
return value ? 1 : 0
}, [value])
const handleChange = useCallback((value: any) => {
if (disabled)
return
field.handleChange(value)
formOnChange?.(field.form, value)
onChange?.(field.name, value)
}, [field, formOnChange, onChange, disabled])
const selfProps = typeof selfFormProps === 'function' ? selfFormProps(field.form) : selfFormProps
}, [field, onChange])
return (
<>
{
selfProps?.withTopDivider && (
<div className='h-px w-full bg-divider-subtle' />
)
}
<div className={cn(fieldClassName, formFieldClassName)}>
<div
className={cn(formItemType === FormTypeEnum.collapse && 'cursor-pointer', labelClassName, formLabelClassName)}
onClick={() => {
if (formItemType === FormTypeEnum.collapse)
handleChange(!value)
}}
>
<div className={cn(fieldClassName)}>
<div className={cn(labelClassName, formLabelClassName)}>
{translatedLabel}
{
required && !isValidElement(label) && (
<span className='ml-1 text-text-destructive-secondary'>*</span>
)
}
{
formItemType === FormTypeEnum.collapse && (
<RiArrowDownSFill
className={cn(
'h-4 w-4 text-text-quaternary',
!value && '-rotate-90',
)}
/>
)
}
{
formItemType === FormTypeEnum.editMode && (
<Button
variant='ghost'
size='small'
className='text-text-tertiary'
onClick={() => handleChange(!value)}
>
{value ? <RiInputField className='mr-1 h-3.5 w-3.5' /> : <RiDraftLine className='mr-1 h-3.5 w-3.5' />}
{selfProps?.editModeLabel}
</Button>
)
}
{tooltip && (
<Tooltip
popupContent={<div className='w-[200px]'>{translatedTooltip}</div>}
@@ -273,9 +202,9 @@ const BaseField = ({
/>
)}
</div>
<div className={cn(inputContainerClassName, formInputContainerClassName)}>
<div className={cn(inputContainerClassName)}>
{
!selfProps?.withSlider && [FormTypeEnum.textInput, FormTypeEnum.secretInput, FormTypeEnum.textNumber].includes(formItemType) && (
[FormTypeEnum.textInput, FormTypeEnum.secretInput, FormTypeEnum.textNumber].includes(formItemType) && (
<Input
id={field.name}
name={field.name}
@@ -292,34 +221,6 @@ const BaseField = ({
/>
)
}
{
formItemType === FormTypeEnum.textNumber && selfProps?.withSlider && (
<div className='flex items-center space-x-2'>
<Slider
min={selfProps?.sliderMin}
max={selfProps?.sliderMax}
step={selfProps?.sliderStep}
value={value}
onChange={handleChange}
className={cn(selfProps.sliderClassName)}
trackClassName={cn(selfProps.sliderTrackClassName)}
thumbClassName={cn(selfProps.sliderThumbClassName)}
/>
<Input
id={field.name}
name={field.name}
type='number'
className={cn('', inputClassName, formInputClassName)}
wrapperClassName={cn(selfProps.inputWrapperClassName)}
value={value || ''}
onChange={e => handleChange(e.target.value)}
onBlur={field.handleBlur}
disabled={disabled}
placeholder={translatedPlaceholder}
/>
</div>
)
}
{
formItemType === FormTypeEnum.select && !multiple && (
<PureSelect
@@ -375,16 +276,15 @@ const BaseField = ({
<div
key={option.value}
className={cn(
'system-sm-regular hover:bg-components-option-card-option-hover-bg hover:border-components-option-card-option-hover-border flex h-8 flex-[1] grow cursor-pointer items-center justify-center rounded-lg border border-components-option-card-option-border bg-components-option-card-option-bg p-2 text-text-secondary',
'system-sm-regular hover:bg-components-option-card-option-hover-bg hover:border-components-option-card-option-hover-border flex h-8 flex-[1] grow cursor-pointer items-center justify-center gap-2 rounded-lg border border-components-option-card-option-border bg-components-option-card-option-bg p-2 text-text-secondary',
value === option.value && 'border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg text-text-primary shadow-xs',
disabled && 'cursor-not-allowed opacity-50',
inputClassName,
formInputClassName,
)}
onClick={() => handleChange(option.value)}
onClick={() => !disabled && handleChange(option.value)}
>
{
selfProps?.showRadioUI && (
formSchema.showRadioUI && (
<RadioE
className='mr-2'
isChecked={value === option.value}
@@ -398,151 +298,18 @@ const BaseField = ({
</div>
)
}
{
formItemType === FormTypeEnum.textareaInput && (
<Textarea
className={cn(
'min-h-[80px]',
inputClassName,
formInputClassName,
)}
value={value}
placeholder={translatedPlaceholder}
onChange={e => handleChange(e.target.value)}
onBlur={field.handleBlur}
disabled={disabled}
/>
)
}
{
formItemType === FormTypeEnum.promptInput && (
<div className={cn(
'relative rounded-lg bg-components-input-bg-normal p-2',
formInputContainerClassName,
)}>
{
selfProps?.enablePromptGenerator && (
<PromptGeneratorBtn
nodeId={selfProps?.nodeId}
editorId={selfProps?.editorId}
className='absolute right-0 top-[-26px]'
onGenerated={handleChange}
modelConfig={selfProps?.modelConfig}
currentPrompt={value}
isBasicMode={selfProps?.isBasicMode}
/>
)
}
<PromptEditor
value={value}
onChange={handleChange}
onBlur={field.handleBlur}
editable={!disabled}
placeholder={translatedPlaceholder || selfProps?.placeholder}
className={cn(
'min-h-[80px]',
inputClassName,
formInputClassName,
)}
/>
</div>
)
}
{
formItemType === FormTypeEnum.objectList && (
<ObjectValueList
list={value}
onChange={handleChange}
/>
)
}
{
formItemType === FormTypeEnum.arrayList && (
<ArrayValueList
isString={selfProps?.isString}
list={value}
onChange={handleChange}
/>
)
}
{
formItemType === FormTypeEnum.booleanList && (
<ArrayBooleanValueList
list={value}
onChange={handleChange}
/>
)
}
{
formItemType === FormTypeEnum.jsonInput && (
<div className='w-full rounded-[10px] bg-components-input-bg-normal py-2 pl-3 pr-1' style={{ height: selfProps?.editorMinHeight }}>
<CodeEditor
isExpand
noWrapper
language={CodeLanguage.json}
value={value}
placeholder={<div className='whitespace-pre'>{selfProps?.placeholder as string}</div>}
onChange={handleChange}
/>
</div>
)
}
{
formItemType === FormTypeEnum.modelSelector && (
<ModelParameterModal
popupClassName='!w-[387px]'
modelId={value?.name}
provider={value?.provider}
setModel={({ modelId, mode, provider }) => {
handleChange({
mode,
provider,
name: modelId,
completion_params: value?.completion_params,
})
}}
completionParams={value?.completion_params}
onCompletionParamsChange={(params) => {
handleChange({
...value,
completion_params: params,
})
}}
readonly={disabled}
isAdvancedMode
isInWorkflow
hideDebugWithMultipleModel
/>
)
}
{
formItemType === FormTypeEnum.nodeSelector && (
<NodeSelector
value={value}
onChange={handleChange}
/>
)
}
{
formItemType === FormTypeEnum.boolean && (
<Radio.Group
className={cn('flex w-full items-center space-x-1', inputClassName, formInputClassName)}
value={booleanRadioValue}
onChange={handleChange}
className='flex w-fit items-center'
value={value}
onChange={v => field.handleChange(v)}
>
<Radio value={1} className='m-0 h-7 flex-1 justify-center p-0'>True</Radio>
<Radio value={0} className='m-0 h-7 flex-1 justify-center p-0'>False</Radio>
<Radio value={true} className='!mr-1'>True</Radio>
<Radio value={false}>False</Radio>
</Radio.Group>
)
}
{
formItemType === FormTypeEnum.switch && (
<Switch
defaultValue={value}
onChange={handleChange}
/>
)
}
{fieldState?.validateStatus && [FormItemValidateStatusEnum.Error, FormItemValidateStatusEnum.Warning].includes(fieldState?.validateStatus) && (
<div className={cn(
'system-xs-regular mt-1 px-0 py-[2px]',
@@ -572,11 +339,6 @@ const BaseField = ({
</a>
)
}
{
selfProps?.withBottomDivider && (
<div className='h-px w-full bg-divider-subtle' />
)
}
</>
)

View File

@@ -5,7 +5,6 @@ import {
useMemo,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import type {
AnyFieldApi,
AnyFormApi,
@@ -32,7 +31,6 @@ import {
useGetFormValues,
useGetValidators,
} from '@/app/components/base/form/hooks'
import { Button } from '@/app/components/base/button'
export type BaseFormProps = {
formSchemas?: FormSchema[]
@@ -41,7 +39,6 @@ export type BaseFormProps = {
ref?: FormRef
disabled?: boolean
formFromProps?: AnyFormApi
onCancel?: () => void
onChange?: (field: string, value: any) => void
onSubmit?: (e: React.FormEvent<HTMLFormElement>) => void
preventDefaultSubmit?: boolean
@@ -58,12 +55,10 @@ const BaseForm = ({
ref,
disabled,
formFromProps,
onCancel,
onChange,
onSubmit,
preventDefaultSubmit = false,
}: BaseFormProps) => {
const { t } = useTranslation()
const initialDefaultValues = useMemo(() => {
if (defaultValues)
return defaultValues
@@ -87,22 +82,8 @@ const BaseForm = ({
const result: Record<string, any> = {}
formSchemas.forEach((schema) => {
const { show_on } = schema
const showOn = typeof show_on === 'function' ? show_on(form) : show_on
if (showOn?.length) {
showOn?.forEach((condition) => {
result[condition.variable] = s.values[condition.variable]
})
}
})
return result
})
const moreOnValues = useStore(form.store, (s: any) => {
const result: Record<string, any> = {}
formSchemas.forEach((schema) => {
const { more_on } = schema
const moreOn = typeof more_on === 'function' ? more_on(form) : more_on
if (moreOn?.length) {
moreOn?.forEach((condition) => {
if (show_on?.length) {
show_on.forEach((condition) => {
result[condition.variable] = s.values[condition.variable]
})
}
@@ -154,18 +135,11 @@ const BaseForm = ({
const formSchema = formSchemas?.find(schema => schema.name === field.name)
if (formSchema) {
const { more_on = [] } = formSchema
const moreOn = typeof more_on === 'function' ? more_on(form) : more_on
const more = (moreOn || []).every((condition) => {
const conditionValue = moreOnValues[condition.variable]
return Array.isArray(condition.value) ? condition.value.includes(conditionValue) : conditionValue === condition.value
})
return (
<BaseField
field={field}
formSchema={formSchema}
fieldClassName={cn(fieldClassName ?? formSchema.fieldClassName, !more ? 'absolute top-[-9999px]' : '')}
fieldClassName={fieldClassName ?? formSchema.fieldClassName}
labelClassName={labelClassName ?? formSchema.labelClassName}
inputContainerClassName={inputContainerClassName}
inputClassName={inputClassName}
@@ -177,7 +151,7 @@ const BaseForm = ({
}
return null
}, [formSchemas, fieldClassName, labelClassName, inputContainerClassName, inputClassName, disabled, onChange, moreOnValues, fieldStates])
}, [formSchemas, fieldClassName, labelClassName, inputContainerClassName, inputClassName, disabled, onChange, fieldStates])
const renderFieldWrapper = useCallback((formSchema: FormSchema) => {
const validators = getValidators(formSchema)
@@ -185,10 +159,10 @@ const BaseForm = ({
name,
show_on = [],
} = formSchema
const showOn = typeof show_on === 'function' ? show_on(form) : show_on
const show = (showOn || []).every((condition) => {
const show = show_on?.every((condition) => {
const conditionValue = showOnValues[condition.variable]
return Array.isArray(condition.value) ? condition.value.includes(conditionValue) : conditionValue === condition.value
return conditionValue === condition.value
})
if (!show)
@@ -222,28 +196,6 @@ const BaseForm = ({
onSubmit={handleSubmit}
>
{formSchemas.map(renderFieldWrapper)}
{
onSubmit && (
<div className='flex justify-end space-x-2'>
{
onCancel && (
<Button
variant='secondary'
onClick={onCancel}
>
{t('common.operation.cancel')}
</Button>
)
}
<Button
variant='primary'
onClick={() => onSubmit(form.getValues())}
>
{t('common.operation.save')}
</Button>
</div>
)
}
</form>
)
}

View File

@@ -1,25 +0,0 @@
import { memo } from 'react'
import { BaseForm } from '../../components/base'
import type { BaseFormProps } from '../../components/base'
const VariableForm = ({
formSchemas = [],
defaultValues,
ref,
formFromProps,
...rest
}: BaseFormProps) => {
return (
<BaseForm
ref={ref}
formSchemas={formSchemas}
defaultValues={defaultValues}
formClassName='space-y-3'
labelClassName='h-6 flex items-center mb-1 system-sm-medium text-text-secondary'
formFromProps={formFromProps}
{...rest}
/>
)
}
export default memo(VariableForm)

View File

@@ -15,14 +15,13 @@ export const useCheckValidated = (form: AnyFormApi, FormSchemas: FormSchema[]) =
const errorArray = Object.keys(fields).reduce((acc: string[], key: string) => {
const currentSchema = FormSchemas.find(schema => schema.name === key)
const { show_on = [] } = currentSchema || {}
const showOn = typeof show_on === 'function' ? show_on(form) : show_on
const showOnValues = (showOn || []).reduce((acc, condition) => {
const showOnValues = show_on.reduce((acc, condition) => {
acc[condition.variable] = values[condition.variable]
return acc
}, {} as Record<string, any>)
const show = (showOn || []).every((condition) => {
const show = show_on?.every((condition) => {
const conditionValue = showOnValues[condition.variable]
return Array.isArray(condition.value) ? condition.value.includes(conditionValue) : conditionValue === condition.value
return conditionValue === condition.value
})
const errors: any[] = show ? fields[key].errors : []

View File

@@ -16,7 +16,7 @@ export type TypeWithI18N<T = string> = {
export type FormShowOnObject = {
variable: string
value: string | string[]
value: string
}
export enum FormTypeEnum {
@@ -33,17 +33,7 @@ export enum FormTypeEnum {
multiToolSelector = 'array[tools]',
appSelector = 'app-selector',
dynamicSelect = 'dynamic-select',
textareaInput = 'textarea-input',
promptInput = 'prompt-input',
objectList = 'object-list',
arrayList = 'array-list',
jsonInput = 'json-input',
collapse = 'collapse',
editMode = 'edit-mode',
boolean = 'boolean',
booleanList = 'boolean-list',
switch = 'switch',
nodeSelector = 'node-selector', // used in memory variable form
}
export type FormOption = {
@@ -63,7 +53,7 @@ export enum FormItemValidateStatusEnum {
}
export type FormSchema = {
type: FormTypeEnum | ((form: AnyFormApi) => FormTypeEnum)
type: FormTypeEnum
name: string
label: string | ReactNode | TypeWithI18N | Record<Locale, string>
required: boolean
@@ -71,20 +61,15 @@ export type FormSchema = {
default?: any
description?: string | TypeWithI18N | Record<Locale, string>
tooltip?: string | TypeWithI18N | Record<Locale, string>
show_on?: FormShowOnObject[] | ((form: AnyFormApi) => FormShowOnObject[])
more_on?: FormShowOnObject[] | ((form: AnyFormApi) => FormShowOnObject[])
show_on?: FormShowOnObject[]
url?: string
scope?: string
help?: string | TypeWithI18N | Record<Locale, string>
placeholder?: string | TypeWithI18N | Record<Locale, string>
options?: FormOption[]
fieldClassName?: string
labelClassName?: string
inputContainerClassName?: string
inputClassName?: string
fieldClassName?: string
validators?: AnyValidators
selfFormProps?: ((form: AnyFormApi) => Record<string, any>) | Record<string, any>
onChange?: (form: AnyFormApi, v: any) => void
showRadioUI?: boolean
disabled?: boolean
showCopy?: boolean

View File

@@ -1,3 +0,0 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.678 1.6502C11.1023 1.50885 11.5679 1.56398 11.9473 1.80108L14.2947 3.26885C14.7333 3.54295 15 4.02396 15 4.54107V5.62505L15.9001 6.30035C16.2777 6.58359 16.5 7.02807 16.5 7.50005V9.75005C16.5 10.222 16.2777 10.6665 15.9001 10.9498L15 11.6251V13.4598C14.9999 13.9767 14.7336 14.4572 14.2954 14.7313L11.9473 16.199C11.6152 16.4066 11.217 16.4748 10.8384 16.3939L10.678 16.3499L9 15.7903L7.32202 16.3499C6.89768 16.4913 6.43213 16.4362 6.05273 16.199L3.70532 14.7313C3.2672 14.4572 3.00013 13.9768 3 13.4598V11.6251L2.09985 10.9498C1.72225 10.6665 1.5 10.222 1.5 9.75005V7.50005C1.50004 7.02809 1.72231 6.5836 2.09985 6.30035L3 5.62505V4.54107C3.00005 4.02394 3.26679 3.54294 3.70532 3.26885L6.05273 1.80108C6.43204 1.56403 6.89766 1.50884 7.32202 1.6502L9 2.20977L10.678 1.6502ZM9.75 3.54058V5.68951L10.8625 6.80206C10.9863 6.76904 11.1159 6.75005 11.25 6.75005C12.0784 6.75005 12.75 7.42165 12.75 8.25005C12.75 9.07848 12.0784 9.75005 11.25 9.75005C10.4216 9.75005 9.75 9.07848 9.75 8.25005C9.75001 8.11594 9.76898 7.98631 9.802 7.8626L8.68945 6.75005C8.40829 6.46885 8.25003 6.08736 8.25 5.68951V3.54058L6.84814 3.0733L4.5 4.54107V5.62505C4.5 6.09705 4.27767 6.54146 3.90015 6.82476L3 7.50005V9.75005L3.90015 10.4253C4.27764 10.7086 4.49996 11.1531 4.5 11.6251V13.459L6.84814 14.9268L8.25 14.4588V12.3106L7.13672 11.1973C7.01316 11.2303 6.88394 11.2501 6.75 11.2501C5.92157 11.2501 5.25 10.5785 5.25 9.75005C5.25003 8.92165 5.92159 8.25005 6.75 8.25005C7.57841 8.25005 8.24997 8.92165 8.25 9.75005C8.25 9.88396 8.23019 10.0132 8.19727 10.1368L9.31055 11.2501C9.59176 11.5313 9.74996 11.9128 9.75 12.3106V14.4588L11.1519 14.9268L13.5 13.459V11.6251C13.5 11.153 13.7224 10.7086 14.0999 10.4253L15 9.75005V7.50005L14.0999 6.82476C13.7224 6.54147 13.5 6.09707 13.5 5.62505V4.54107L11.1519 3.0733L9.75 3.54058Z" fill="#354052"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -1,28 +0,0 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "18",
"height": "18",
"viewBox": "0 0 18 18",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"fill-rule": "evenodd",
"clip-rule": "evenodd",
"d": "M10.678 1.6502C11.1023 1.50885 11.5679 1.56398 11.9473 1.80108L14.2947 3.26885C14.7333 3.54295 15 4.02396 15 4.54107V5.62505L15.9001 6.30035C16.2777 6.58359 16.5 7.02807 16.5 7.50005V9.75005C16.5 10.222 16.2777 10.6665 15.9001 10.9498L15 11.6251V13.4598C14.9999 13.9767 14.7336 14.4572 14.2954 14.7313L11.9473 16.199C11.6152 16.4066 11.217 16.4748 10.8384 16.3939L10.678 16.3499L9 15.7903L7.32202 16.3499C6.89768 16.4913 6.43213 16.4362 6.05273 16.199L3.70532 14.7313C3.2672 14.4572 3.00013 13.9768 3 13.4598V11.6251L2.09985 10.9498C1.72225 10.6665 1.5 10.222 1.5 9.75005V7.50005C1.50004 7.02809 1.72231 6.5836 2.09985 6.30035L3 5.62505V4.54107C3.00005 4.02394 3.26679 3.54294 3.70532 3.26885L6.05273 1.80108C6.43204 1.56403 6.89766 1.50884 7.32202 1.6502L9 2.20977L10.678 1.6502ZM9.75 3.54058V5.68951L10.8625 6.80206C10.9863 6.76904 11.1159 6.75005 11.25 6.75005C12.0784 6.75005 12.75 7.42165 12.75 8.25005C12.75 9.07848 12.0784 9.75005 11.25 9.75005C10.4216 9.75005 9.75 9.07848 9.75 8.25005C9.75001 8.11594 9.76898 7.98631 9.802 7.8626L8.68945 6.75005C8.40829 6.46885 8.25003 6.08736 8.25 5.68951V3.54058L6.84814 3.0733L4.5 4.54107V5.62505C4.5 6.09705 4.27767 6.54146 3.90015 6.82476L3 7.50005V9.75005L3.90015 10.4253C4.27764 10.7086 4.49996 11.1531 4.5 11.6251V13.459L6.84814 14.9268L8.25 14.4588V12.3106L7.13672 11.1973C7.01316 11.2303 6.88394 11.2501 6.75 11.2501C5.92157 11.2501 5.25 10.5785 5.25 9.75005C5.25003 8.92165 5.92159 8.25005 6.75 8.25005C7.57841 8.25005 8.24997 8.92165 8.25 9.75005C8.25 9.88396 8.23019 10.0132 8.19727 10.1368L9.31055 11.2501C9.59176 11.5313 9.74996 11.9128 9.75 12.3106V14.4588L11.1519 14.9268L13.5 13.459V11.6251C13.5 11.153 13.7224 10.7086 14.0999 10.4253L15 9.75005V7.50005L14.0999 6.82476C13.7224 6.54147 13.5 6.09707 13.5 5.62505V4.54107L11.1519 3.0733L9.75 3.54058Z",
"fill": "currentColor"
},
"children": []
}
]
},
"name": "Memory"
}

View File

@@ -1,20 +0,0 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './Memory.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 = 'Memory'
export default Icon

View File

@@ -6,6 +6,5 @@ export { default as GlobalVariable } from './GlobalVariable'
export { default as Icon3Dots } from './Icon3Dots'
export { default as LongArrowLeft } from './LongArrowLeft'
export { default as LongArrowRight } from './LongArrowRight'
export { default as Memory } from './Memory'
export { default as SearchMenu } from './SearchMenu'
export { default as Tools } from './Tools'

View File

@@ -10,7 +10,6 @@ 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'
export const UPDATE_WORKFLOW_VARIABLES_EVENT_EMITTER = 'prompt-editor-workflow-variables-block-update-variables'
export const checkHasContextBlock = (text: string) => {
if (!text)

View File

@@ -61,8 +61,6 @@ import { VariableValueBlockNode } from './plugins/variable-value-block/node'
import { CustomTextNode } from './plugins/custom-text/node'
import OnBlurBlock from './plugins/on-blur-or-focus-block'
import UpdateBlock from './plugins/update-block'
import MemoryPopupPlugin from './plugins/memory-popup-plugin'
import { textToEditorState } from './utils'
import type {
ContextBlockType,
@@ -78,11 +76,9 @@ import type {
import {
UPDATE_DATASETS_EVENT_EMITTER,
UPDATE_HISTORY_EVENT_EMITTER,
UPDATE_WORKFLOW_VARIABLES_EVENT_EMITTER,
} from './constants'
import { useEventEmitterContextContext } from '@/context/event-emitter'
import cn from '@/utils/classnames'
import PromptEditorProvider from './store/provider'
export type PromptEditorProps = {
instanceId?: string
@@ -178,13 +174,6 @@ const PromptEditor: FC<PromptEditorProps> = ({
payload: historyBlock?.history,
} as any)
}, [eventEmitter, historyBlock?.history])
useEffect(() => {
eventEmitter?.emit({
type: UPDATE_WORKFLOW_VARIABLES_EVENT_EMITTER,
payload: workflowVariableBlock?.variables,
instanceId,
} as any)
}, [eventEmitter, workflowVariableBlock?.variables])
return (
<LexicalComposer initialConfig={{ ...initialConfig, editable }}>
@@ -209,12 +198,6 @@ const PromptEditor: FC<PromptEditorProps> = ({
}
ErrorBoundary={LexicalErrorBoundary}
/>
{workflowVariableBlock?.show && workflowVariableBlock?.isMemorySupported && (
<MemoryPopupPlugin
instanceId={instanceId}
memoryVariables={workflowVariableBlock?.variables?.find(v => v.nodeId === 'memory_block')?.vars || []}
/>
)}
<ComponentPickerBlock
triggerString='/'
contextBlock={contextBlock}
@@ -320,12 +303,4 @@ const PromptEditor: FC<PromptEditorProps> = ({
)
}
const PromptEditorWithProvider = ({ instanceId, ...props }: PromptEditorProps) => {
return (
<PromptEditorProvider instanceId={instanceId}>
<PromptEditor {...props} instanceId={instanceId} />
</PromptEditorProvider>
)
}
export default PromptEditorWithProvider
export default PromptEditor

View File

@@ -283,19 +283,7 @@ export const useOptions = (
const workflowVariableOptions = useMemo(() => {
if (!workflowVariableBlockType?.show)
return []
let res = workflowVariableBlockType.variables || []
if (!workflowVariableBlockType.isMemorySupported) {
res = res.map((v) => {
if (v.nodeId === 'conversation') {
return {
...v,
vars: v.vars.filter(vv => !vv.variable.startsWith('memory_block.')),
}
}
return v
})
}
const res = workflowVariableBlockType.variables || []
if(errorMessageBlockType?.show && res.findIndex(v => v.nodeId === 'error_message') === -1) {
res.unshift({
nodeId: 'error_message',

View File

@@ -1,273 +0,0 @@
import {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react'
import { createPortal } from 'react-dom'
import { useTranslation } from 'react-i18next'
import {
autoUpdate,
flip,
offset,
shift,
size,
useFloating,
} from '@floating-ui/react'
import {
RiAddLine,
} from '@remixicon/react'
import { Memory } from '@/app/components/base/icons/src/vender/line/others'
import {
$getSelection,
$isRangeSelection,
} from 'lexical'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { useEventEmitterContextContext } from '@/context/event-emitter'
import { MEMORY_POPUP_SHOW_BY_EVENT_EMITTER, MEMORY_VAR_CREATED_BY_MODAL_BY_EVENT_EMITTER, MEMORY_VAR_MODAL_SHOW_BY_EVENT_EMITTER } from '@/app/components/workflow/nodes/_base/components/prompt/type'
import Divider from '@/app/components/base/divider'
import VariableIcon from '@/app/components/workflow/nodes/_base/components/variable/variable-label/base/variable-icon'
import type {
Var,
} from '@/app/components/workflow/types'
import { INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND } from '../workflow-variable-block'
import cn from '@/utils/classnames'
export type MemoryPopupProps = {
className?: string
container?: Element | null
instanceId?: string
memoryVariables: Var[]
}
export default function MemoryPopupPlugin({
className,
container,
instanceId,
memoryVariables,
}: MemoryPopupProps) {
const { t } = useTranslation()
const [editor] = useLexicalComposerContext()
const { eventEmitter } = useEventEmitterContextContext()
const [open, setOpen] = useState(false)
const portalRef = useRef<HTMLDivElement | null>(null)
const lastSelectionRef = useRef<Range | null>(null)
const containerEl = useMemo(() => container ?? (typeof document !== 'undefined' ? document.body : null), [container])
const useContainer = !!containerEl && containerEl !== document.body
const memoryVarInNode = memoryVariables.filter(memoryVariable => memoryVariable.memoryVariableNodeId)
const memoryVarInApp = memoryVariables.filter(memoryVariable => !memoryVariable.memoryVariableNodeId)
const { refs, floatingStyles, isPositioned } = useFloating({
placement: 'bottom-start',
middleware: [
offset(0), // fix hide cursor
shift({
padding: 8,
altBoundary: true,
}),
flip(),
size({
apply({ availableWidth, availableHeight, elements }) {
Object.assign(elements.floating.style, {
maxWidth: `${Math.min(400, availableWidth)}px`,
maxHeight: `${Math.min(300, availableHeight)}px`,
overflow: 'auto',
})
},
padding: 8,
}),
],
whileElementsMounted: autoUpdate,
})
const openPortal = useCallback(() => {
const domSelection = window.getSelection()
let range: Range | null = null
if (domSelection && domSelection.rangeCount > 0)
range = domSelection.getRangeAt(0).cloneRange()
else
range = lastSelectionRef.current
if (range) {
const rects = range.getClientRects()
let rect: DOMRect | null = null
if (rects && rects.length)
rect = rects[rects.length - 1]
else
rect = range.getBoundingClientRect()
if (rect.width === 0 && rect.height === 0) {
const root = editor.getRootElement()
if (root) {
const sc = range.startContainer
const node = sc.nodeType === Node.ELEMENT_NODE
? sc as Element
: (sc.parentElement || root)
rect = node.getBoundingClientRect()
if (rect.width === 0 && rect.height === 0)
rect = root.getBoundingClientRect()
}
}
if (rect && !(rect.top === 0 && rect.left === 0 && rect.width === 0 && rect.height === 0)) {
const virtualEl = {
getBoundingClientRect() {
return rect!
},
}
refs.setReference(virtualEl as Element)
}
}
setOpen(true)
}, [setOpen])
const closePortal = useCallback(() => {
setOpen(false)
}, [setOpen])
const handleSelectVariable = useCallback((variable: string[]) => {
editor.dispatchCommand(INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND, variable)
closePortal()
}, [editor, closePortal])
const handleCreate = useCallback(() => {
eventEmitter?.emit({ type: MEMORY_VAR_MODAL_SHOW_BY_EVENT_EMITTER, instanceId } as any)
closePortal()
}, [eventEmitter, instanceId, closePortal])
eventEmitter?.useSubscription((v: any) => {
if (v.type === MEMORY_POPUP_SHOW_BY_EVENT_EMITTER && v.instanceId === instanceId)
openPortal()
})
eventEmitter?.useSubscription((v: any) => {
if (v.type === MEMORY_VAR_CREATED_BY_MODAL_BY_EVENT_EMITTER && v.instanceId === instanceId)
handleSelectVariable(v.variable)
})
useEffect(() => {
return editor.registerUpdateListener(({ editorState }) => {
editorState.read(() => {
const selection = $getSelection()
if ($isRangeSelection(selection)) {
const domSelection = window.getSelection()
if (domSelection && domSelection.rangeCount > 0)
lastSelectionRef.current = domSelection.getRangeAt(0).cloneRange()
}
})
})
}, [editor])
useEffect(() => {
if (!open)
return
const onMouseDown = (e: MouseEvent) => {
if (!portalRef.current)
return
if (!portalRef.current.contains(e.target as Node))
closePortal()
}
document.addEventListener('mousedown', onMouseDown, false)
return () => document.removeEventListener('mousedown', onMouseDown, false)
}, [open, closePortal])
if (!open || !containerEl)
return null
return createPortal(
<div className='h-0 w-0'>
<div
ref={(node) => {
portalRef.current = node
refs.setFloating(node)
}}
className={cn(
useContainer ? '' : 'z-[999999]',
'absolute rounded-xl shadow-lg backdrop-blur-sm',
className,
)}
style={{
...floatingStyles,
visibility: isPositioned ? 'visible' : 'hidden',
}}
>
<div className='w-[261px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur'>
{memoryVarInNode.length > 0 && (
<>
<div className='flex items-center gap-1 pb-1 pt-2.5'>
<Divider className='!h-px !w-3 bg-divider-subtle' />
<div className='system-2xs-medium-uppercase shrink-0 text-text-tertiary'>{t('workflow.nodes.llm.memory.currentNodeLabel')}</div>
<Divider className='!h-px grow bg-divider-subtle' />
</div>
<div className='p-1'>
{memoryVarInNode.map(variable => (
<div
key={variable.variable}
className='flex cursor-pointer items-center gap-1 rounded-md px-3 py-1 hover:bg-state-base-hover'
onClick={() => handleSelectVariable(['memory_block', variable.variable])}
>
<VariableIcon
variables={['memory_block', '']}
className='text-util-colors-teal-teal-700'
/>
<div title={variable.memoryVariableName} className='system-sm-medium shrink-0 truncate text-text-secondary'>{variable.memoryVariableName}</div>
</div>
))}
</div>
</>
)}
{memoryVarInApp.length > 0 && (
<>
<div className='flex items-center gap-1 pb-1 pt-2.5'>
<Divider className='!h-px !w-3 bg-divider-subtle' />
<div className='system-2xs-medium-uppercase shrink-0 text-text-tertiary'>{t('workflow.nodes.llm.memory.conversationScopeLabel')}</div>
<Divider className='!h-px grow bg-divider-subtle' />
</div>
<div className='p-1'>
{memoryVarInApp.map(variable => (
<div
key={variable.variable}
className='flex cursor-pointer items-center gap-1 rounded-md px-3 py-1 hover:bg-state-base-hover'
onClick={() => handleSelectVariable(['memory_block', variable.variable])}
>
<VariableIcon
variables={['memory_block', '']}
className='text-util-colors-teal-teal-700'
/>
<div title={variable.variable} className='system-sm-medium shrink-0 truncate text-text-secondary'>{variable.memoryVariableName}</div>
</div>
))}
</div>
</>
)}
{!memoryVarInNode.length && !memoryVarInApp.length && (
<div className='p-2'>
<div className='flex flex-col gap-2 rounded-[10px] bg-workflow-process-bg p-4'>
<div className='flex h-10 w-10 items-center justify-center rounded-lg border-[0.5px] border-components-card-border bg-components-card-bg shadow-lg backdrop-blur-sm'>
<Memory className='h-5 w-5 text-util-colors-teal-teal-700' />
</div>
<div className='system-sm-medium text-text-secondary'>{t('workflow.nodes.llm.memory.emptyState')}</div>
</div>
</div>
)}
<div className='system-xs-medium flex cursor-pointer items-center gap-1 border-t border-divider-subtle px-4 py-2 text-text-accent-light-mode-only' onClick={handleCreate}>
<RiAddLine className='h-4 w-4' />
<div>{t('workflow.nodes.llm.memory.createButton')}</div>
</div>
</div>
</div>
</div>,
containerEl,
)
}

View File

@@ -1,11 +1,8 @@
import {
$insertNodes,
} from 'lexical'
import { $insertNodes } from 'lexical'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { textToEditorState } from '../utils'
import { CustomTextNode } from './custom-text/node'
import { CLEAR_HIDE_MENU_TIMEOUT } from './workflow-variable-block'
import { MEMORY_POPUP_SHOW_BY_EVENT_EMITTER } from '@/app/components/workflow/nodes/_base/components/prompt/type'
import { useEventEmitterContextContext } from '@/context/event-emitter'
export const PROMPT_EDITOR_UPDATE_VALUE_BY_EVENT_EMITTER = 'PROMPT_EDITOR_UPDATE_VALUE_BY_EVENT_EMITTER'
@@ -39,18 +36,6 @@ const UpdateBlock = ({
}
})
eventEmitter?.useSubscription((v: any) => {
if (v.type === MEMORY_POPUP_SHOW_BY_EVENT_EMITTER && v.instanceId === instanceId) {
editor.focus()
editor.update(() => {
const textNode = new CustomTextNode('')
$insertNodes([textNode])
editor.dispatchCommand(CLEAR_HIDE_MENU_TIMEOUT, undefined)
})
}
})
return null
}

View File

@@ -19,27 +19,23 @@ import {
DELETE_WORKFLOW_VARIABLE_BLOCK_COMMAND,
UPDATE_WORKFLOW_NODES_MAP,
} from './index'
import { isConversationVar, isENV, isGlobalVar, isMemoryVariable, isRagVariableVar, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
import { isConversationVar, isENV, isGlobalVar, isRagVariableVar, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
import Tooltip from '@/app/components/base/tooltip'
import { isExceptionVariable } from '@/app/components/workflow/utils'
import VarFullPathPanel from '@/app/components/workflow/nodes/_base/components/variable/var-full-path-panel'
import { Type } from '@/app/components/workflow/nodes/llm/types'
import type {
NodeOutPutVar,
ValueSelector,
} from '@/app/components/workflow/types'
import type { ValueSelector, Var } from '@/app/components/workflow/types'
import {
VariableLabelInEditor,
} from '@/app/components/workflow/nodes/_base/components/variable/variable-label'
import { useEventEmitterContextContext } from '@/context/event-emitter'
import { UPDATE_WORKFLOW_VARIABLES_EVENT_EMITTER } from '../../constants'
import { usePromptEditorStore } from '../../store/store'
type WorkflowVariableBlockComponentProps = {
nodeKey: string
variables: string[]
workflowNodesMap: WorkflowNodesMap
availableVariables: NodeOutPutVar[]
environmentVariables?: Var[]
conversationVariables?: Var[]
ragVariables?: Var[]
getVarType?: (payload: {
nodeId: string,
valueSelector: ValueSelector,
@@ -51,27 +47,12 @@ const WorkflowVariableBlockComponent = ({
variables,
workflowNodesMap = {},
getVarType,
availableVariables: initialAvailableVariables,
environmentVariables,
conversationVariables,
ragVariables,
}: WorkflowVariableBlockComponentProps) => {
const { t } = useTranslation()
const [editor] = useLexicalComposerContext()
const instanceId = usePromptEditorStore(s => s.instanceId)
const { eventEmitter } = useEventEmitterContextContext()
const [availableVariables, setAvailableVariables] = useState<NodeOutPutVar[]>(initialAvailableVariables)
eventEmitter?.useSubscription((v: any) => {
if (v?.type === UPDATE_WORKFLOW_VARIABLES_EVENT_EMITTER && instanceId && v.instanceId === instanceId)
setAvailableVariables(v.payload)
})
const environmentVariables = availableVariables?.find(v => v.nodeId === 'env')?.vars || []
const conversationVariables = availableVariables?.find(v => v.nodeId === 'conversation')?.vars || []
const memoryVariables = conversationVariables?.filter(v => v.variable.startsWith('memory_block.'))
const ragVariables = availableVariables?.reduce<any[]>((acc, curr) => {
if (curr.nodeId === 'rag')
acc.push(...curr.vars)
else
acc.push(...curr.vars.filter(v => v.isRagVariable))
return acc
}, [])
const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_WORKFLOW_VARIABLE_BLOCK_COMMAND)
const variablesLength = variables.length
const isRagVar = isRagVariableVar(variables)
@@ -91,23 +72,21 @@ const WorkflowVariableBlockComponent = ({
let variableValid = true
const isEnv = isENV(variables)
const isChatVar = isConversationVar(variables)
const isMemoryVar = isMemoryVariable(variables)
const isGlobal = isGlobalVar(variables)
if (isGlobal)
return true
if (isEnv) {
variableValid
= environmentVariables.some(v => v.variable === `${variables?.[0] ?? ''}.${variables?.[1] ?? ''}`)
if (environmentVariables)
variableValid = environmentVariables.some(v => v.variable === `${variables?.[0] ?? ''}.${variables?.[1] ?? ''}`)
}
else if (isChatVar) {
variableValid = conversationVariables.some(v => v.variable === `${variables?.[0] ?? ''}.${variables?.[1] ?? ''}`)
}
else if (isMemoryVar) {
variableValid = memoryVariables.some(v => v.variable === `${variables?.[0] ?? ''}.${variables?.[1] ?? ''}`)
if (conversationVariables)
variableValid = conversationVariables.some(v => v.variable === `${variables?.[0] ?? ''}.${variables?.[1] ?? ''}`)
}
else if (isRagVar) {
variableValid = ragVariables.some(v => v.variable === `${variables?.[0] ?? ''}.${variables?.[1] ?? ''}.${variables?.[2] ?? ''}`)
if (ragVariables)
variableValid = ragVariables.some(v => v.variable === `${variables?.[0] ?? ''}.${variables?.[1] ?? ''}.${variables?.[2] ?? ''}`)
}
else {
variableValid = !!node
@@ -155,28 +134,11 @@ const WorkflowVariableBlockComponent = ({
})
}, [node, reactflow, store])
const memoriedVariables = useMemo(() => {
if (variables[0] === 'memory_block') {
const currentMemoryVariable = memoryVariables?.find(v => v.variable === variables.join('.'))
if (currentMemoryVariable && currentMemoryVariable.memoryVariableName) {
return [
'memory_block',
currentMemoryVariable.memoryVariableName,
]
}
return variables
}
return variables
}, [memoryVariables, variables])
const Item = (
<VariableLabelInEditor
nodeType={node?.type}
nodeTitle={node?.title}
variables={memoriedVariables}
variables={variables}
onClick={(e) => {
e.stopPropagation()
handleVariableJump()
@@ -190,7 +152,7 @@ const WorkflowVariableBlockComponent = ({
)
if (!node)
return <div>{Item}</div>
return Item
return (
<Tooltip
@@ -198,10 +160,10 @@ const WorkflowVariableBlockComponent = ({
popupContent={
<VarFullPathPanel
nodeName={node.title}
path={memoriedVariables.slice(1)}
path={variables.slice(1)}
varType={getVarType ? getVarType({
nodeId: memoriedVariables[0],
valueSelector: memoriedVariables,
nodeId: variables[0],
valueSelector: variables,
}) : Type.string}
nodeType={node?.type}
/>}

View File

@@ -32,7 +32,6 @@ const WorkflowVariableBlock = memo(({
onInsert,
onDelete,
getVarType,
variables: originalVariables,
}: WorkflowVariableBlockType) => {
const [editor] = useLexicalComposerContext()
@@ -51,7 +50,7 @@ const WorkflowVariableBlock = memo(({
INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND,
(variables: string[]) => {
editor.dispatchCommand(CLEAR_HIDE_MENU_TIMEOUT, undefined)
const workflowVariableBlockNode = $createWorkflowVariableBlockNode(variables, workflowNodesMap, getVarType, originalVariables || [])
const workflowVariableBlockNode = $createWorkflowVariableBlockNode(variables, workflowNodesMap, getVarType)
$insertNodes([workflowVariableBlockNode])
if (onInsert)

View File

@@ -3,7 +3,7 @@ import { DecoratorNode } from 'lexical'
import type { WorkflowVariableBlockType } from '../../types'
import WorkflowVariableBlockComponent from './component'
import type { GetVarType } from '../../types'
import type { NodeOutPutVar } from '@/app/components/workflow/types'
import type { Var } from '@/app/components/workflow/types'
export type WorkflowNodesMap = WorkflowVariableBlockType['workflowNodesMap']
@@ -11,40 +11,40 @@ export type SerializedNode = SerializedLexicalNode & {
variables: string[]
workflowNodesMap: WorkflowNodesMap
getVarType?: GetVarType
availableVariables?: NodeOutPutVar[]
environmentVariables?: Var[]
conversationVariables?: Var[]
ragVariables?: Var[]
}
export class WorkflowVariableBlockNode extends DecoratorNode<React.JSX.Element> {
__variables: string[]
__workflowNodesMap: WorkflowNodesMap
__getVarType?: GetVarType
__availableVariables?: NodeOutPutVar[]
__environmentVariables?: Var[]
__conversationVariables?: Var[]
__ragVariables?: Var[]
static getType(): string {
return 'workflow-variable-block'
}
static clone(node: WorkflowVariableBlockNode): WorkflowVariableBlockNode {
return new WorkflowVariableBlockNode(node.__variables, node.__workflowNodesMap, node.__getVarType, node.__key, node.__availableVariables)
return new WorkflowVariableBlockNode(node.__variables, node.__workflowNodesMap, node.__getVarType, node.__key, node.__environmentVariables, node.__conversationVariables, node.__ragVariables)
}
isInline(): boolean {
return true
}
constructor(
variables: string[],
workflowNodesMap: WorkflowNodesMap,
getVarType: any,
key?: NodeKey,
availableVariables?: NodeOutPutVar[],
) {
constructor(variables: string[], workflowNodesMap: WorkflowNodesMap, getVarType: any, key?: NodeKey, environmentVariables?: Var[], conversationVariables?: Var[], ragVariables?: Var[]) {
super(key)
this.__variables = variables
this.__workflowNodesMap = workflowNodesMap
this.__getVarType = getVarType
this.__availableVariables = availableVariables
this.__environmentVariables = environmentVariables
this.__conversationVariables = conversationVariables
this.__ragVariables = ragVariables
}
createDOM(): HTMLElement {
@@ -64,13 +64,15 @@ export class WorkflowVariableBlockNode extends DecoratorNode<React.JSX.Element>
variables={this.__variables}
workflowNodesMap={this.__workflowNodesMap}
getVarType={this.__getVarType!}
availableVariables={this.__availableVariables || []}
environmentVariables={this.__environmentVariables}
conversationVariables={this.__conversationVariables}
ragVariables={this.__ragVariables}
/>
)
}
static importJSON(serializedNode: SerializedNode): WorkflowVariableBlockNode {
const node = $createWorkflowVariableBlockNode(serializedNode.variables, serializedNode.workflowNodesMap, serializedNode.getVarType, serializedNode.availableVariables)
const node = $createWorkflowVariableBlockNode(serializedNode.variables, serializedNode.workflowNodesMap, serializedNode.getVarType, serializedNode.environmentVariables, serializedNode.conversationVariables, serializedNode.ragVariables)
return node
}
@@ -82,7 +84,9 @@ export class WorkflowVariableBlockNode extends DecoratorNode<React.JSX.Element>
variables: this.getVariables(),
workflowNodesMap: this.getWorkflowNodesMap(),
getVarType: this.getVarType(),
availableVariables: this.getAvailableVariables(),
environmentVariables: this.getEnvironmentVariables(),
conversationVariables: this.getConversationVariables(),
ragVariables: this.getRagVariables(),
}
}
@@ -101,17 +105,27 @@ export class WorkflowVariableBlockNode extends DecoratorNode<React.JSX.Element>
return self.__getVarType
}
getAvailableVariables(): NodeOutPutVar[] {
getEnvironmentVariables(): any {
const self = this.getLatest()
return self.__availableVariables || []
return self.__environmentVariables
}
getConversationVariables(): any {
const self = this.getLatest()
return self.__conversationVariables
}
getRagVariables(): any {
const self = this.getLatest()
return self.__ragVariables
}
getTextContent(): string {
return `{{#${this.getVariables().join('.')}#}}`
}
}
export function $createWorkflowVariableBlockNode(variables: string[], workflowNodesMap: WorkflowNodesMap, getVarType?: GetVarType, availableVariables?: NodeOutPutVar[]): WorkflowVariableBlockNode {
return new WorkflowVariableBlockNode(variables, workflowNodesMap, getVarType, undefined, availableVariables)
export function $createWorkflowVariableBlockNode(variables: string[], workflowNodesMap: WorkflowNodesMap, getVarType?: GetVarType, environmentVariables?: Var[], conversationVariables?: Var[], ragVariables?: Var[]): WorkflowVariableBlockNode {
return new WorkflowVariableBlockNode(variables, workflowNodesMap, getVarType, undefined, environmentVariables, conversationVariables, ragVariables)
}
export function $isWorkflowVariableBlockNode(

View File

@@ -21,6 +21,13 @@ const WorkflowVariableBlockReplacementBlock = ({
variables,
}: WorkflowVariableBlockType) => {
const [editor] = useLexicalComposerContext()
const ragVariables = variables?.reduce<any[]>((acc, curr) => {
if (curr.nodeId === 'rag')
acc.push(...curr.vars)
else
acc.push(...curr.vars.filter(v => v.isRagVariable))
return acc
}, [])
useEffect(() => {
if (!editor.hasNodes([WorkflowVariableBlockNode]))
@@ -32,7 +39,7 @@ const WorkflowVariableBlockReplacementBlock = ({
onInsert()
const nodePathString = textNode.getTextContent().slice(3, -3)
return $applyNodeReplacement($createWorkflowVariableBlockNode(nodePathString.split('.'), workflowNodesMap, getVarType, variables))
return $applyNodeReplacement($createWorkflowVariableBlockNode(nodePathString.split('.'), workflowNodesMap, getVarType, variables?.find(o => o.nodeId === 'env')?.vars || [], variables?.find(o => o.nodeId === 'conversation')?.vars || [], ragVariables))
}, [onInsert, workflowNodesMap, getVarType, variables])
const getMatch = useCallback((text: string) => {

View File

@@ -1,31 +0,0 @@
import { createContext, useRef } from 'react'
import { createPromptEditorStore } from './store'
type PromptEditorStoreApi = ReturnType<typeof createPromptEditorStore>
type PromptEditorContextType = PromptEditorStoreApi | undefined
export const PromptEditorContext = createContext<PromptEditorContextType>(undefined)
type PromptEditorProviderProps = {
instanceId?: string
children: React.ReactNode
}
const PromptEditorProvider = ({
instanceId,
children,
}: PromptEditorProviderProps) => {
const storeRef = useRef<PromptEditorStoreApi>(undefined)
if (!storeRef.current)
storeRef.current = createPromptEditorStore({ instanceId })
return (
<PromptEditorContext.Provider value={storeRef.current!}>
{children}
</PromptEditorContext.Provider>
)
}
export default PromptEditorProvider

View File

@@ -1,24 +0,0 @@
import { useContext } from 'react'
import { createStore, useStore } from 'zustand'
import { PromptEditorContext } from './provider'
type PromptEditorStoreProps = {
instanceId?: string
}
type PromptEditorStore = {
instanceId?: string
}
export const createPromptEditorStore = ({ instanceId }: PromptEditorStoreProps) => {
return createStore<PromptEditorStore>(() => ({
instanceId,
}))
}
export const usePromptEditorStore = <T>(selector: (state: PromptEditorStore) => T): T => {
const store = useContext(PromptEditorContext)
if (!store)
throw new Error('Missing PromptEditorContext.Provider in the tree')
return useStore(store, selector)
}

View File

@@ -71,7 +71,6 @@ export type WorkflowVariableBlockType = {
getVarType?: GetVarType
showManageInputField?: boolean
onManageInputField?: () => void
isMemorySupported?: boolean
}
export type MenuTextMatch = {

View File

@@ -5,7 +5,7 @@ import {
import { useTranslation } from 'react-i18next'
import { RiDeleteBinLine } from '@remixicon/react'
import { PlusIcon, XMarkIcon } from '@heroicons/react/20/solid'
import useSWR, { useSWRConfig } from 'swr'
import useSWR from 'swr'
import SecretKeyGenerateModal from './secret-key-generate'
import s from './style.module.css'
import ActionButton from '@/app/components/base/action-button'
@@ -15,7 +15,6 @@ import CopyFeedback from '@/app/components/base/copy-feedback'
import {
createApikey as createAppApikey,
delApikey as delAppApikey,
fetchApiKeysList as fetchAppApiKeysList,
} from '@/service/apps'
import {
createApikey as createDatasetApikey,
@@ -27,6 +26,7 @@ import Loading from '@/app/components/base/loading'
import Confirm from '@/app/components/base/confirm'
import useTimestamp from '@/hooks/use-timestamp'
import { useAppContext } from '@/context/app-context'
import { useAppApiKeys, useInvalidateAppApiKeys } from '@/service/use-apps'
type ISecretKeyModalProps = {
isShow: boolean
@@ -45,12 +45,14 @@ const SecretKeyModal = ({
const [showConfirmDelete, setShowConfirmDelete] = useState(false)
const [isVisible, setVisible] = useState(false)
const [newKey, setNewKey] = useState<CreateApiKeyResponse | undefined>(undefined)
const { mutate } = useSWRConfig()
const commonParams = appId
? { url: `/apps/${appId}/api-keys`, params: {} }
: { url: '/datasets/api-keys', params: {} }
const fetchApiKeysList = appId ? fetchAppApiKeysList : fetchDatasetApiKeysList
const { data: apiKeysList } = useSWR(commonParams, fetchApiKeysList)
const invalidateAppApiKeys = useInvalidateAppApiKeys()
const { data: appApiKeys, isLoading: isAppApiKeysLoading } = useAppApiKeys(appId, { enabled: !!appId && isShow })
const { data: datasetApiKeys, isLoading: isDatasetApiKeysLoading, mutate: mutateDatasetApiKeys } = useSWR(
!appId && isShow ? { url: '/datasets/api-keys', params: {} } : null,
fetchDatasetApiKeysList,
)
const apiKeysList = appId ? appApiKeys : datasetApiKeys
const isApiKeysLoading = appId ? isAppApiKeysLoading : isDatasetApiKeysLoading
const [delKeyID, setDelKeyId] = useState('')
@@ -64,7 +66,10 @@ const SecretKeyModal = ({
? { url: `/apps/${appId}/api-keys/${delKeyID}`, params: {} }
: { url: `/datasets/api-keys/${delKeyID}`, params: {} }
await delApikey(params)
mutate(commonParams)
if (appId)
invalidateAppApiKeys(appId)
else
mutateDatasetApiKeys()
}
const onCreate = async () => {
@@ -75,7 +80,10 @@ const SecretKeyModal = ({
const res = await createApikey(params)
setVisible(true)
setNewKey(res)
mutate(commonParams)
if (appId)
invalidateAppApiKeys(appId)
else
mutateDatasetApiKeys()
}
const generateToken = (token: string) => {
@@ -88,7 +96,7 @@ const SecretKeyModal = ({
<XMarkIcon className="h-6 w-6 cursor-pointer text-text-tertiary" onClick={onClose} />
</div>
<p className='mt-1 shrink-0 text-[13px] font-normal leading-5 text-text-tertiary'>{t('appApi.apiKeyModal.apiSecretKeyTips')}</p>
{!apiKeysList && <div className='mt-4'><Loading /></div>}
{isApiKeysLoading && <div className='mt-4'><Loading /></div>}
{
!!apiKeysList?.data?.length && (
<div className='mt-4 flex grow flex-col overflow-hidden'>

View File

@@ -214,8 +214,12 @@ export const searchAnything = async (
actionItem?: ActionItem,
dynamicActions?: Record<string, ActionItem>,
): Promise<SearchResult[]> => {
const trimmedQuery = query.trim()
if (actionItem) {
const searchTerm = query.replace(actionItem.key, '').replace(actionItem.shortcut, '').trim()
const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
const prefixPattern = new RegExp(`^(${escapeRegExp(actionItem.key)}|${escapeRegExp(actionItem.shortcut)})\\s*`)
const searchTerm = trimmedQuery.replace(prefixPattern, '').trim()
try {
return await actionItem.search(query, searchTerm, locale)
}
@@ -225,10 +229,12 @@ export const searchAnything = async (
}
}
if (query.startsWith('@') || query.startsWith('/'))
if (trimmedQuery.startsWith('@') || trimmedQuery.startsWith('/'))
return []
const globalSearchActions = Object.values(dynamicActions || Actions)
// Exclude slash commands from general search results
.filter(action => action.key !== '/')
// Use Promise.allSettled to handle partial failures gracefully
const searchPromises = globalSearchActions.map(async (action) => {

View File

@@ -177,31 +177,42 @@ const GotoAnything: FC<Props> = ({
}
}, [router])
const dedupedResults = useMemo(() => {
const seen = new Set<string>()
return searchResults.filter((result) => {
const key = `${result.type}-${result.id}`
if (seen.has(key))
return false
seen.add(key)
return true
})
}, [searchResults])
// Group results by type
const groupedResults = useMemo(() => searchResults.reduce((acc, result) => {
const groupedResults = useMemo(() => dedupedResults.reduce((acc, result) => {
if (!acc[result.type])
acc[result.type] = []
acc[result.type].push(result)
return acc
}, {} as { [key: string]: SearchResult[] }),
[searchResults])
[dedupedResults])
useEffect(() => {
if (isCommandsMode)
return
if (!searchResults.length)
if (!dedupedResults.length)
return
const currentValueExists = searchResults.some(result => `${result.type}-${result.id}` === cmdVal)
const currentValueExists = dedupedResults.some(result => `${result.type}-${result.id}` === cmdVal)
if (!currentValueExists)
setCmdVal(`${searchResults[0].type}-${searchResults[0].id}`)
}, [isCommandsMode, searchResults, cmdVal])
setCmdVal(`${dedupedResults[0].type}-${dedupedResults[0].id}`)
}, [isCommandsMode, dedupedResults, cmdVal])
const emptyResult = useMemo(() => {
if (searchResults.length || !searchQuery.trim() || isLoading || isCommandsMode)
if (dedupedResults.length || !searchQuery.trim() || isLoading || isCommandsMode)
return null
const isCommandSearch = searchMode !== 'general'
@@ -246,7 +257,7 @@ const GotoAnything: FC<Props> = ({
</div>
</div>
)
}, [searchResults, searchQuery, Actions, searchMode, isLoading, isError, isCommandsMode])
}, [dedupedResults, searchQuery, Actions, searchMode, isLoading, isError, isCommandsMode])
const defaultUI = useMemo(() => {
if (searchQuery.trim())
@@ -430,14 +441,14 @@ const GotoAnything: FC<Props> = ({
{/* Always show footer to prevent height jumping */}
<div className='border-t border-divider-subtle bg-components-panel-bg-blur px-4 py-2 text-xs text-text-tertiary'>
<div className='flex min-h-[16px] items-center justify-between'>
{(!!searchResults.length || isError) ? (
{(!!dedupedResults.length || isError) ? (
<>
<span>
{isError ? (
<span className='text-red-500'>{t('app.gotoAnything.someServicesUnavailable')}</span>
) : (
<>
{t('app.gotoAnything.resultCount', { count: searchResults.length })}
{t('app.gotoAnything.resultCount', { count: dedupedResults.length })}
{searchMode !== 'general' && (
<span className='ml-2 opacity-60'>
{t('app.gotoAnything.inScope', { scope: searchMode.replace('@', '') })}

View File

@@ -129,8 +129,8 @@ export const useProviderCredentialsAndLoadBalancing = (
// as ([Record<string, string | boolean | undefined> | undefined, ModelLoadBalancingConfig | undefined])
}
export const useModelList = (type: ModelTypeEnum, enabled?: boolean) => {
const { data, mutate, isLoading } = useSWR(enabled ? `/workspaces/current/models/model-types/${type}` : null, fetchModelList)
export const useModelList = (type: ModelTypeEnum) => {
const { data, mutate, isLoading } = useSWR(`/workspaces/current/models/model-types/${type}`, fetchModelList)
return {
data: data?.data || [],
@@ -139,8 +139,8 @@ export const useModelList = (type: ModelTypeEnum, enabled?: boolean) => {
}
}
export const useDefaultModel = (type: ModelTypeEnum, enabled?: boolean) => {
const { data, mutate, isLoading } = useSWR(enabled ? `/workspaces/current/default-model?model_type=${type}` : null, fetchDefaultModal)
export const useDefaultModel = (type: ModelTypeEnum) => {
const { data, mutate, isLoading } = useSWR(`/workspaces/current/default-model?model_type=${type}`, fetchDefaultModal)
return {
data: data?.data,

View File

@@ -3,7 +3,6 @@
import { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useParams } from 'next/navigation'
import useSWRInfinite from 'swr/infinite'
import { flatten } from 'lodash-es'
import { produce } from 'immer'
import {
@@ -12,33 +11,13 @@ import {
} from '@remixicon/react'
import Nav from '../nav'
import type { NavItem } from '../nav/nav-selector'
import { fetchAppList } from '@/service/apps'
import CreateAppTemplateDialog from '@/app/components/app/create-app-dialog'
import CreateAppModal from '@/app/components/app/create-app-modal'
import CreateFromDSLModal from '@/app/components/app/create-from-dsl-modal'
import type { AppListResponse } from '@/models/app'
import { useAppContext } from '@/context/app-context'
import { useStore as useAppStore } from '@/app/components/app/store'
import { AppModeEnum } from '@/types/app'
const getKey = (
pageIndex: number,
previousPageData: AppListResponse,
activeTab: string,
keywords: string,
) => {
if (!pageIndex || previousPageData.has_more) {
const params: any = { url: 'apps', params: { page: pageIndex + 1, limit: 30, name: keywords } }
if (activeTab !== 'all')
params.params.mode = activeTab
else
delete params.params.mode
return params
}
return null
}
import { useInfiniteAppList } from '@/service/use-apps'
const AppNav = () => {
const { t } = useTranslation()
@@ -50,17 +29,21 @@ const AppNav = () => {
const [showCreateFromDSLModal, setShowCreateFromDSLModal] = useState(false)
const [navItems, setNavItems] = useState<NavItem[]>([])
const { data: appsData, setSize, mutate } = useSWRInfinite(
appId
? (pageIndex: number, previousPageData: AppListResponse) => getKey(pageIndex, previousPageData, 'all', '')
: () => null,
fetchAppList,
{ revalidateFirstPage: false },
)
const {
data: appsData,
fetchNextPage,
hasNextPage,
refetch,
} = useInfiniteAppList({
page: 1,
limit: 30,
name: '',
}, { enabled: !!appId })
const handleLoadMore = useCallback(() => {
setSize(size => size + 1)
}, [setSize])
if (hasNextPage)
fetchNextPage()
}, [fetchNextPage, hasNextPage])
const openModal = (state: string) => {
if (state === 'blank')
@@ -73,7 +56,7 @@ const AppNav = () => {
useEffect(() => {
if (appsData) {
const appItems = flatten(appsData?.map(appData => appData.data))
const appItems = flatten((appsData.pages ?? []).map(appData => appData.data))
const navItems = appItems.map((app) => {
const link = ((isCurrentWorkspaceEditor, app) => {
if (!isCurrentWorkspaceEditor) {
@@ -132,17 +115,17 @@ const AppNav = () => {
<CreateAppModal
show={showNewAppDialog}
onClose={() => setShowNewAppDialog(false)}
onSuccess={() => mutate()}
onSuccess={() => refetch()}
/>
<CreateAppTemplateDialog
show={showNewAppTemplateDialog}
onClose={() => setShowNewAppTemplateDialog(false)}
onSuccess={() => mutate()}
onSuccess={() => refetch()}
/>
<CreateFromDSLModal
show={showCreateFromDSLModal}
onClose={() => setShowCreateFromDSLModal(false)}
onSuccess={() => mutate()}
onSuccess={() => refetch()}
/>
</>
)

View File

@@ -15,32 +15,10 @@ import type {
OffsetOptions,
Placement,
} from '@floating-ui/react'
import useSWRInfinite from 'swr/infinite'
import { fetchAppList } from '@/service/apps'
import type { AppListResponse } from '@/models/app'
import { useInfiniteAppList } from '@/service/use-apps'
const PAGE_SIZE = 20
const getKey = (
pageIndex: number,
previousPageData: AppListResponse,
searchText: string,
) => {
if (pageIndex === 0 || (previousPageData && previousPageData.has_more)) {
const params: any = {
url: 'apps',
params: {
page: pageIndex + 1,
limit: PAGE_SIZE,
name: searchText,
},
}
return params
}
return null
}
type Props = {
value?: {
app_id: string
@@ -72,30 +50,32 @@ const AppSelector: FC<Props> = ({
const [searchText, setSearchText] = useState('')
const [isLoadingMore, setIsLoadingMore] = useState(false)
const { data, isLoading, setSize } = useSWRInfinite(
(pageIndex: number, previousPageData: AppListResponse) => getKey(pageIndex, previousPageData, searchText),
fetchAppList,
{
revalidateFirstPage: true,
shouldRetryOnError: false,
dedupingInterval: 500,
errorRetryCount: 3,
},
)
const {
data,
isLoading,
isFetchingNextPage,
fetchNextPage,
hasNextPage,
} = useInfiniteAppList({
page: 1,
limit: PAGE_SIZE,
name: searchText,
})
const pages = data?.pages ?? []
const displayedApps = useMemo(() => {
if (!data) return []
return data.flatMap(({ data: apps }) => apps)
}, [data])
if (!pages.length) return []
return pages.flatMap(({ data: apps }) => apps)
}, [pages])
const hasMore = data?.at(-1)?.has_more ?? true
const hasMore = hasNextPage ?? true
const handleLoadMore = useCallback(async () => {
if (isLoadingMore || !hasMore) return
if (isLoadingMore || isFetchingNextPage || !hasMore) return
setIsLoadingMore(true)
try {
await setSize((size: number) => size + 1)
await fetchNextPage()
}
finally {
// Add a small delay to ensure state updates are complete
@@ -103,7 +83,7 @@ const AppSelector: FC<Props> = ({
setIsLoadingMore(false)
}, 300)
}
}, [isLoadingMore, hasMore, setSize])
}, [isLoadingMore, isFetchingNextPage, hasMore, fetchNextPage])
const handleTriggerClick = () => {
if (disabled) return
@@ -185,7 +165,7 @@ const AppSelector: FC<Props> = ({
onSelect={handleSelectApp}
scope={scope || 'all'}
apps={displayedApps}
isLoading={isLoading || isLoadingMore}
isLoading={isLoading || isLoadingMore || isFetchingNextPage}
hasMore={hasMore}
onLoadMore={handleLoadMore}
searchText={searchText}

View File

@@ -19,7 +19,6 @@ import {
useWorkflowStartRun,
} from '../hooks'
import { useWorkflowStore } from '@/app/components/workflow/store'
import { useFormatMemoryVariables } from '@/app/components/workflow/hooks'
type WorkflowMainProps = Pick<WorkflowProps, 'nodes' | 'edges' | 'viewport'>
const WorkflowMain = ({
@@ -29,14 +28,12 @@ const WorkflowMain = ({
}: WorkflowMainProps) => {
const featuresStore = useFeaturesStore()
const workflowStore = useWorkflowStore()
const { formatMemoryVariables } = useFormatMemoryVariables()
const handleWorkflowDataUpdate = useCallback((payload: any) => {
const {
features,
conversation_variables,
environment_variables,
memory_blocks,
} = payload
if (features && featuresStore) {
const { setFeatures } = featuresStore.getState()
@@ -51,11 +48,7 @@ const WorkflowMain = ({
const { setEnvironmentVariables } = workflowStore.getState()
setEnvironmentVariables(environment_variables)
}
if (memory_blocks) {
const { setMemoryVariables } = workflowStore.getState()
setMemoryVariables(formatMemoryVariables(memory_blocks))
}
}, [featuresStore, workflowStore, formatMemoryVariables])
}, [featuresStore, workflowStore])
const {
doSyncWorkflowDraft,

View File

@@ -27,7 +27,6 @@ export const useNodesSyncDraft = () => {
const {
appId,
conversationVariables,
memoryVariables,
environmentVariables,
syncWorkflowDraftHash,
isWorkflowDataLoaded,
@@ -75,13 +74,6 @@ export const useNodesSyncDraft = () => {
},
environment_variables: environmentVariables,
conversation_variables: conversationVariables,
memory_blocks: memoryVariables.map(({ value_type, scope, more, node_id, ...rest }) => {
return {
...rest,
node_id: scope === 'node' ? node_id : undefined,
scope,
}
}),
hash: syncWorkflowDraftHash,
},
}

View File

@@ -18,7 +18,6 @@ import {
import type { FetchWorkflowDraftResponse } from '@/types/workflow'
import { useWorkflowConfig } from '@/service/use-workflow'
import type { FileUploadConfigResponse } from '@/models/common'
import { useFormatMemoryVariables } from '@/app/components/workflow/hooks'
import type { Edge, Node } from '@/app/components/workflow/types'
import { BlockEnum } from '@/app/components/workflow/types'
import { AppModeEnum } from '@/types/app'
@@ -55,7 +54,6 @@ export const useWorkflowInit = () => {
data: fileUploadConfigResponse,
isLoading: isFileUploadConfigLoading,
} = useWorkflowConfig('/files/upload', handleUpdateWorkflowFileUploadConfig)
const { formatMemoryVariables } = useFormatMemoryVariables()
const handleGetInitialWorkflowData = useCallback(async () => {
try {
@@ -68,7 +66,6 @@ export const useWorkflowInit = () => {
}, {} as Record<string, string>),
environmentVariables: res.environment_variables?.map(env => env.value_type === 'secret' ? { ...env, value: '[__HIDDEN__]' } : env) || [],
conversationVariables: res.conversation_variables || [],
memoryVariables: formatMemoryVariables((res.memory_blocks || [])),
isWorkflowDataLoaded: true,
})
setSyncWorkflowDraftHash(res.hash)

View File

@@ -3,12 +3,10 @@ import { useWorkflowStore } from '@/app/components/workflow/store'
import { fetchWorkflowDraft } from '@/service/workflow'
import type { WorkflowDataUpdater } from '@/app/components/workflow/types'
import { useWorkflowUpdate } from '@/app/components/workflow/hooks'
import { useFormatMemoryVariables } from '@/app/components/workflow/hooks'
export const useWorkflowRefreshDraft = () => {
const workflowStore = useWorkflowStore()
const { handleUpdateWorkflowCanvas } = useWorkflowUpdate()
const { formatMemoryVariables } = useFormatMemoryVariables()
const handleRefreshWorkflowDraft = useCallback(() => {
const {
@@ -18,7 +16,6 @@ export const useWorkflowRefreshDraft = () => {
setEnvironmentVariables,
setEnvSecrets,
setConversationVariables,
setMemoryVariables,
setIsWorkflowDataLoaded,
isWorkflowDataLoaded,
debouncedSyncWorkflowDraft,
@@ -47,7 +44,6 @@ export const useWorkflowRefreshDraft = () => {
}, {} as Record<string, string>))
setEnvironmentVariables(response.environment_variables?.map(env => env.value_type === 'secret' ? { ...env, value: '[__HIDDEN__]' } : env) || [])
setConversationVariables(response.conversation_variables || [])
setMemoryVariables(formatMemoryVariables((response.memory_blocks || [])))
setIsWorkflowDataLoaded(true)
})
.catch(() => {
@@ -57,7 +53,7 @@ export const useWorkflowRefreshDraft = () => {
.finally(() => {
setIsSyncingWorkflowDraft(false)
})
}, [handleUpdateWorkflowCanvas, workflowStore, formatMemoryVariables])
}, [handleUpdateWorkflowCanvas, workflowStore])
return {
handleRefreshWorkflowDraft,

View File

@@ -603,7 +603,6 @@ export const useWorkflowRun = () => {
baseSseOptions.onTTSEnd,
baseSseOptions.onTextReplace,
baseSseOptions.onAgentLog,
baseSseOptions.onMemoryUpdate,
baseSseOptions.onDataSourceNodeProcessing,
baseSseOptions.onDataSourceNodeCompleted,
baseSseOptions.onDataSourceNodeError,

View File

@@ -22,6 +22,5 @@ export * from './use-DSL'
export * from './use-inspect-vars-crud'
export * from './use-set-workflow-vars-with-value'
export * from './use-workflow-search'
export * from './use-memory-variable'
export * from './use-auto-generate-webhook-url'
export * from './use-serial-async-callback'

View File

@@ -51,7 +51,7 @@ export const useSetWorkflowVarsWithValue = ({
const { getNodes } = store.getState()
const nodeArr = getNodes()
const allNodesOutputVars = toNodeOutputVars(nodeArr, false, () => true, [], [], [], [], passedInAllPluginInfoList || allPluginInfoList, passedInSchemaTypeDefinitions || schemaTypeDefinitions)
const allNodesOutputVars = toNodeOutputVars(nodeArr, false, () => true, [], [], [], passedInAllPluginInfoList || allPluginInfoList, passedInSchemaTypeDefinitions || schemaTypeDefinitions)
const nodesKeyValue: Record<string, Node> = {}
nodeArr.forEach((node) => {

View File

@@ -132,7 +132,7 @@ export const useInspectVarsCrudCommon = ({
mcpTools: mcpTools || [],
dataSourceList: dataSourceList || [],
}
const currentNodeOutputVars = toNodeOutputVars([currentNode], false, () => true, [], [], [], [], allPluginInfoList, schemaTypeDefinitions)
const currentNodeOutputVars = toNodeOutputVars([currentNode], false, () => true, [], [], [], allPluginInfoList, schemaTypeDefinitions)
const vars = await fetchNodeInspectVars(flowType, flowId, nodeId)
const varsWithSchemaType = vars.map((varItem) => {
const schemaType = currentNodeOutputVars[0]?.vars.find(v => v.variable === varItem.name)?.schemaType || ''

View File

@@ -1,50 +0,0 @@
import {
useCallback,
} from 'react'
import {
useStore,
useWorkflowStore,
} from '@/app/components/workflow/store'
import { ChatVarType } from '@/app/components/workflow/panel/chat-variable-panel/type'
import type { MemoryVariable } from '@/app/components/workflow/types'
export const useMemoryVariable = () => {
const workflowStore = useWorkflowStore()
const setMemoryVariables = useStore(s => s.setMemoryVariables)
const handleAddMemoryVariable = useCallback((memoryVariable: MemoryVariable) => {
const { memoryVariables } = workflowStore.getState()
setMemoryVariables([memoryVariable, ...memoryVariables])
}, [setMemoryVariables, workflowStore])
const handleUpdateMemoryVariable = useCallback((memoryVariable: MemoryVariable) => {
const { memoryVariables } = workflowStore.getState()
setMemoryVariables(memoryVariables.map(v => v.id === memoryVariable.id ? memoryVariable : v))
}, [setMemoryVariables, workflowStore])
const handleDeleteMemoryVariable = useCallback((memoryVariable: MemoryVariable) => {
const { memoryVariables } = workflowStore.getState()
setMemoryVariables(memoryVariables.filter(v => v.id !== memoryVariable.id))
}, [setMemoryVariables, workflowStore])
return {
handleAddMemoryVariable,
handleUpdateMemoryVariable,
handleDeleteMemoryVariable,
}
}
export const useFormatMemoryVariables = () => {
const formatMemoryVariables = useCallback((memoryVariables: MemoryVariable[]) => {
return memoryVariables.map((v) => {
return {
...v,
value_type: ChatVarType.Memory,
}
})
}, [])
return {
formatMemoryVariables,
}
}

View File

@@ -36,7 +36,6 @@ export const useWorkflowVariables = () => {
filterVar,
hideEnv,
hideChatVar,
conversationVariablesFirst,
}: {
parentNode?: Node | null
beforeNodes: Node[]
@@ -44,12 +43,10 @@ export const useWorkflowVariables = () => {
filterVar: (payload: Var, selector: ValueSelector) => boolean
hideEnv?: boolean
hideChatVar?: boolean
conversationVariablesFirst?: boolean
}): NodeOutPutVar[] => {
const {
conversationVariables,
environmentVariables,
memoryVariables,
ragPipelineVariables,
dataSourceList,
} = workflowStore.getState()
@@ -60,7 +57,6 @@ export const useWorkflowVariables = () => {
isChatMode,
environmentVariables: hideEnv ? [] : environmentVariables,
conversationVariables: (isChatMode && !hideChatVar) ? conversationVariables : [],
memoryVariables: isChatMode ? memoryVariables : [],
ragVariables: ragPipelineVariables,
filterVar,
allPluginInfoList: {
@@ -71,7 +67,6 @@ export const useWorkflowVariables = () => {
dataSourceList: dataSourceList || [],
},
schemaTypeDefinitions,
conversationVariablesFirst,
})
}, [t, workflowStore, schemaTypeDefinitions, buildInTools, customTools, workflowTools, mcpTools])

View File

@@ -13,7 +13,6 @@ type CollapseProps = {
onCollapse?: (collapsed: boolean) => void
operations?: ReactNode
hideCollapseIcon?: boolean
triggerClassName?: string
}
const Collapse = ({
disabled,
@@ -23,7 +22,6 @@ const Collapse = ({
onCollapse,
operations,
hideCollapseIcon,
triggerClassName,
}: CollapseProps) => {
const [collapsedLocal, setCollapsedLocal] = useState(true)
const collapsedMerged = collapsed !== undefined ? collapsed : collapsedLocal
@@ -44,7 +42,7 @@ const Collapse = ({
<>
<div className='group/collapse flex items-center'>
<div
className={cn('ml-4 flex grow items-center', triggerClassName)}
className='ml-4 flex grow items-center'
onClick={() => {
if (!disabled) {
setCollapsedLocal(!collapsedMerged)

View File

@@ -1,27 +0,0 @@
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import { Memory } from '@/app/components/base/icons/src/vender/line/others'
type Props = {
onAddMemory: () => void
}
const AddMemoryButton = ({ onAddMemory }: Props) => {
const { t } = useTranslation()
return (
<div className='ml-1.5 mt-2.5'>
<Button
variant='ghost'
size='small'
className='text-text-tertiary'
onClick={onAddMemory}
>
<Memory className='h-3.5 w-3.5' />
<span className='ml-1'>{t('workflow.nodes.llm.memory.addButton')}</span>
</Button>
</div>
)
}
export default AddMemoryButton

View File

@@ -36,9 +36,6 @@ import Switch from '@/app/components/base/switch'
import { Jinja } from '@/app/components/base/icons/src/vender/workflow'
import { useStore } from '@/app/components/workflow/store'
import { useWorkflowVariableType } from '@/app/components/workflow/hooks'
import AddMemoryButton from './add-memory-button'
import { MEMORY_POPUP_SHOW_BY_EVENT_EMITTER } from './type'
import MemoryCreateButton from '@/app/components/workflow/nodes/llm/components/memory-system/memory-create-button'
type Props = {
className?: string
@@ -78,11 +75,10 @@ type Props = {
titleTooltip?: ReactNode
inputClassName?: string
editorContainerClassName?: string
placeholder?: string | React.JSX.Element
placeholder?: string
placeholderClassName?: string
titleClassName?: string
required?: boolean
isMemorySupported?: boolean
}
const Editor: FC<Props> = ({
@@ -122,7 +118,6 @@ const Editor: FC<Props> = ({
titleClassName,
editorContainerClassName,
required,
isMemorySupported,
}) => {
const { t } = useTranslation()
const { eventEmitter } = useEventEmitterContextContext()
@@ -158,182 +153,170 @@ const Editor: FC<Props> = ({
const pipelineId = useStore(s => s.pipelineId)
const setShowInputFieldPanel = useStore(s => s.setShowInputFieldPanel)
const handleAddMemory = () => {
setFocus()
eventEmitter?.emit({ type: MEMORY_POPUP_SHOW_BY_EVENT_EMITTER, instanceId } as any)
}
return (
<>
<Wrap className={cn(className, wrapClassName)} style={wrapStyle} isInNode isExpand={isExpand}>
<div ref={ref} className={cn(isFocus ? (gradientBorder && 'bg-gradient-to-r from-components-input-border-active-prompt-1 to-components-input-border-active-prompt-2') : 'bg-transparent', isExpand && 'h-full', '!rounded-[9px] p-0.5', containerClassName)}>
<div className={cn(isFocus ? 'bg-background-default' : 'bg-components-input-bg-normal', isExpand && 'flex h-full flex-col', 'rounded-lg', containerClassName)}>
<div className={cn('flex items-center justify-between pl-3 pr-2 pt-1', headerClassName)}>
<div className='flex gap-2'>
<div className={cn('text-xs font-semibold uppercase leading-4 text-text-secondary', titleClassName)}>{title} {required && <span className='text-text-destructive'>*</span>}</div>
{titleTooltip && <Tooltip popupContent={titleTooltip} />}
</div>
<div className='flex items-center'>
<div className='text-xs font-medium leading-[18px] text-text-tertiary'>{value?.length || 0}</div>
{isSupportPromptGenerator && (
<PromptGeneratorBtn
nodeId={nodeId!}
editorId={editorId}
className='ml-[5px]'
onGenerated={onGenerated}
modelConfig={modelConfig}
currentPrompt={value}
/>
)}
<div className='ml-2 mr-2 h-3 w-px bg-divider-regular'></div>
{/* Operations */}
<div className='flex items-center space-x-[2px]'>
{isSupportJinja && (
<Tooltip
popupContent={
<div>
<div>{t('workflow.common.enableJinja')}</div>
<a className='text-text-accent' target='_blank' href='https://jinja.palletsprojects.com/en/2.10.x/'>{t('workflow.common.learnMore')}</a>
</div>
}
>
<div className={cn(editionType === EditionType.jinja2 && 'border-components-button-ghost-bg-hover bg-components-button-ghost-bg-hover', 'flex h-[22px] items-center space-x-0.5 rounded-[5px] border border-transparent px-1.5 hover:border-components-button-ghost-bg-hover')}>
<Jinja className='h-3 w-6 text-text-quaternary' />
<Switch
size='sm'
defaultValue={editionType === EditionType.jinja2}
onChange={(checked) => {
onEditionTypeChange?.(checked ? EditionType.jinja2 : EditionType.basic)
}}
/>
</div>
</Tooltip>
)}
{!readOnly && (
<Tooltip
popupContent={`${t('workflow.common.insertVarTip')}`}
>
<ActionButton onClick={handleInsertVariable}>
<Variable02 className='h-4 w-4' />
</ActionButton>
</Tooltip>
)}
{showRemove && (
<ActionButton onClick={onRemove}>
<RiDeleteBinLine className='h-4 w-4' />
</ActionButton>
)}
{!isCopied
? (
<ActionButton onClick={handleCopy}>
<Copy className='h-4 w-4' />
</ActionButton>
)
: (
<ActionButton>
<CopyCheck className='h-4 w-4' />
</ActionButton>
)
}
<ToggleExpandBtn isExpand={isExpand} onExpandChange={setIsExpand} />
</div>
</div>
<Wrap className={cn(className, wrapClassName)} style={wrapStyle} isInNode isExpand={isExpand}>
<div ref={ref} className={cn(isFocus ? (gradientBorder && 'bg-gradient-to-r from-components-input-border-active-prompt-1 to-components-input-border-active-prompt-2') : 'bg-transparent', isExpand && 'h-full', '!rounded-[9px] p-0.5', containerClassName)}>
<div className={cn(isFocus ? 'bg-background-default' : 'bg-components-input-bg-normal', isExpand && 'flex h-full flex-col', 'rounded-lg', containerClassName)}>
<div className={cn('flex items-center justify-between pl-3 pr-2 pt-1', headerClassName)}>
<div className='flex gap-2'>
<div className={cn('text-xs font-semibold uppercase leading-4 text-text-secondary', titleClassName)}>{title} {required && <span className='text-text-destructive'>*</span>}</div>
{titleTooltip && <Tooltip popupContent={titleTooltip} />}
</div>
<div className='flex items-center'>
<div className='text-xs font-medium leading-[18px] text-text-tertiary'>{value?.length || 0}</div>
{isSupportPromptGenerator && (
<PromptGeneratorBtn
nodeId={nodeId!}
editorId={editorId}
className='ml-[5px]'
onGenerated={onGenerated}
modelConfig={modelConfig}
currentPrompt={value}
/>
)}
{/* Min: 80 Max: 560. Header: 24 */}
<div className={cn('pb-2', isExpand && 'flex grow flex-col', isMemorySupported && isFocus && 'pb-1.5')}>
{!(isSupportJinja && editionType === EditionType.jinja2)
? (
<>
<div className={cn(isExpand ? 'grow' : 'max-h-[536px]', 'relative min-h-[56px] overflow-y-auto px-3', editorContainerClassName)}>
<PromptEditor
key={controlPromptEditorRerenderKey}
placeholder={placeholder}
placeholderClassName={placeholderClassName}
instanceId={instanceId}
compact
className={cn('min-h-[56px]', inputClassName)}
style={isExpand ? { height: editorExpandHeight - 5 } : {}}
value={value}
contextBlock={{
show: justVar ? false : isShowContext,
selectable: !hasSetBlockStatus?.context,
canNotAddContext: true,
<div className='ml-2 mr-2 h-3 w-px bg-divider-regular'></div>
{/* Operations */}
<div className='flex items-center space-x-[2px]'>
{isSupportJinja && (
<Tooltip
popupContent={
<div>
<div>{t('workflow.common.enableJinja')}</div>
<a className='text-text-accent' target='_blank' href='https://jinja.palletsprojects.com/en/2.10.x/'>{t('workflow.common.learnMore')}</a>
</div>
}
>
<div className={cn(editionType === EditionType.jinja2 && 'border-components-button-ghost-bg-hover bg-components-button-ghost-bg-hover', 'flex h-[22px] items-center space-x-0.5 rounded-[5px] border border-transparent px-1.5 hover:border-components-button-ghost-bg-hover')}>
<Jinja className='h-3 w-6 text-text-quaternary' />
<Switch
size='sm'
defaultValue={editionType === EditionType.jinja2}
onChange={(checked) => {
onEditionTypeChange?.(checked ? EditionType.jinja2 : EditionType.basic)
}}
historyBlock={{
show: justVar ? false : isShowHistory,
selectable: !hasSetBlockStatus?.history,
history: {
user: 'Human',
assistant: 'Assistant',
},
}}
queryBlock={{
show: false, // use [sys.query] instead of query block
selectable: false,
}}
workflowVariableBlock={{
show: true,
variables: nodesOutputVars || [],
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),
showManageInputField: !!pipelineId,
onManageInputField: () => setShowInputFieldPanel?.(true),
isMemorySupported,
}}
onChange={onChange}
onBlur={setBlur}
onFocus={setFocus}
editable={!readOnly}
isSupportFileVar={isSupportFileVar}
/>
{/* to patch Editor not support dynamic change editable status */}
{readOnly && <div className='absolute inset-0 z-10'></div>}
</div>
{isMemorySupported && <AddMemoryButton onAddMemory={handleAddMemory} />}
</>
)
: (
<div className={cn(isExpand ? 'grow' : 'max-h-[536px]', 'relative min-h-[56px] overflow-y-auto px-3', editorContainerClassName)}>
<CodeEditor
availableVars={nodesOutputVars || []}
varList={varList}
onAddVar={handleAddVariable}
isInNode
readOnly={readOnly}
language={CodeLanguage.python3}
value={value}
onChange={onChange}
noWrapper
isExpand={isExpand}
className={inputClassName}
/>
</div>
</Tooltip>
)}
{!readOnly && (
<Tooltip
popupContent={`${t('workflow.common.insertVarTip')}`}
>
<ActionButton onClick={handleInsertVariable}>
<Variable02 className='h-4 w-4' />
</ActionButton>
</Tooltip>
)}
{showRemove && (
<ActionButton onClick={onRemove}>
<RiDeleteBinLine className='h-4 w-4' />
</ActionButton>
)}
{!isCopied
? (
<ActionButton onClick={handleCopy}>
<Copy className='h-4 w-4' />
</ActionButton>
)
: (
<ActionButton>
<CopyCheck className='h-4 w-4' />
</ActionButton>
)
}
<ToggleExpandBtn isExpand={isExpand} onExpandChange={setIsExpand} />
</div>
</div>
</div>
{/* Min: 80 Max: 560. Header: 24 */}
<div className={cn('pb-2', isExpand && 'flex grow flex-col')}>
{!(isSupportJinja && editionType === EditionType.jinja2)
? (
<div className={cn(isExpand ? 'grow' : 'max-h-[536px]', 'relative min-h-[56px] overflow-y-auto px-3', editorContainerClassName)}>
<PromptEditor
key={controlPromptEditorRerenderKey}
placeholder={placeholder}
placeholderClassName={placeholderClassName}
instanceId={instanceId}
compact
className={cn('min-h-[56px]', inputClassName)}
style={isExpand ? { height: editorExpandHeight - 5 } : {}}
value={value}
contextBlock={{
show: justVar ? false : isShowContext,
selectable: !hasSetBlockStatus?.context,
canNotAddContext: true,
}}
historyBlock={{
show: justVar ? false : isShowHistory,
selectable: !hasSetBlockStatus?.history,
history: {
user: 'Human',
assistant: 'Assistant',
},
}}
queryBlock={{
show: false, // use [sys.query] instead of query block
selectable: false,
}}
workflowVariableBlock={{
show: true,
variables: nodesOutputVars || [],
getVarType: getVarType as any,
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),
showManageInputField: !!pipelineId,
onManageInputField: () => setShowInputFieldPanel?.(true),
}}
onChange={onChange}
onBlur={setBlur}
onFocus={setFocus}
editable={!readOnly}
isSupportFileVar={isSupportFileVar}
/>
{/* to patch Editor not support dynamic change editable status */}
{readOnly && <div className='absolute inset-0 z-10'></div>}
</div>
)
: (
<div className={cn(isExpand ? 'grow' : 'max-h-[536px]', 'relative min-h-[56px] overflow-y-auto px-3', editorContainerClassName)}>
<CodeEditor
availableVars={nodesOutputVars || []}
varList={varList}
onAddVar={handleAddVariable}
isInNode
readOnly={readOnly}
language={CodeLanguage.python3}
value={value}
onChange={onChange}
noWrapper
isExpand={isExpand}
className={inputClassName}
/>
</div>
)}
</div>
</div>
</Wrap>
{isMemorySupported && <MemoryCreateButton nodeId={nodeId || ''} instanceId={instanceId} hideTrigger />}
</>
</div>
</Wrap>
)
}

View File

@@ -1,3 +0,0 @@
export const MEMORY_POPUP_SHOW_BY_EVENT_EMITTER = 'MEMORY_POPUP_SHOW_BY_EVENT_EMITTER'
export const MEMORY_VAR_MODAL_SHOW_BY_EVENT_EMITTER = 'MEMORY_VAR_MODAL_SHOW_BY_EVENT_EMITTER'
export const MEMORY_VAR_CREATED_BY_MODAL_BY_EVENT_EMITTER = 'MEMORY_VAR_CREATED_BY_MODAL_BY_EVENT_EMITTER'

View File

@@ -39,7 +39,6 @@ import type {
import type { VariableAssignerNodeType } from '@/app/components/workflow/nodes/variable-assigner/types'
import type { Field as StructField } from '@/app/components/workflow/nodes/llm/types'
import type { RAGPipelineVariable } from '@/models/pipeline'
import type { MemoryVariable } from '@/app/components/workflow/types'
import type { WebhookTriggerNodeType } from '@/app/components/workflow/nodes/trigger-webhook/types'
import type { PluginTriggerNodeType } from '@/app/components/workflow/nodes/trigger-plugin/types'
import PluginTriggerNodeDefault from '@/app/components/workflow/nodes/trigger-plugin/default'
@@ -87,17 +86,13 @@ export const isConversationVar = (valueSelector: ValueSelector) => {
return valueSelector[0] === 'conversation'
}
export const isMemoryVariable = (valueSelector: ValueSelector) => {
return valueSelector[0] === 'memory_block'
}
export const isRagVariableVar = (valueSelector: ValueSelector) => {
if (!valueSelector) return false
return valueSelector[0] === 'rag'
}
export const isSpecialVar = (prefix: string): boolean => {
return ['sys', 'env', 'conversation', 'memory_block', 'rag'].includes(prefix)
return ['sys', 'env', 'conversation', 'rag'].includes(prefix)
}
export const hasValidChildren = (children: any): boolean => {
@@ -649,25 +644,13 @@ const formatItem = (
}
case 'conversation': {
res.vars = [
...data.memoryVarList.map((memoryVar: MemoryVariable) => {
return {
variable: `memory_block.${memoryVar.node_id ? `${memoryVar.node_id}_` : ''}${memoryVar.id}`,
type: 'memory_block',
description: '',
isMemoryVariable: true,
memoryVariableName: memoryVar.name,
memoryVariableNodeId: memoryVar.node_id,
}
}) as Var[],
...data.chatVarList.map((chatVar: ConversationVariable) => {
return {
variable: `conversation.${chatVar.name}`,
type: chatVar.value_type,
description: chatVar.description,
}
}) as Var[],
]
res.vars = data.chatVarList.map((chatVar: ConversationVariable) => {
return {
variable: `conversation.${chatVar.name}`,
type: chatVar.value_type,
description: chatVar.description,
}
}) as Var[]
break
}
@@ -715,13 +698,11 @@ const formatItem = (
(() => {
const variableArr = v.variable.split('.')
const [first] = variableArr
if (isSpecialVar(first)) return variableArr
return [...selector, ...variableArr]
})(),
)
if (isCurrentMatched) return true
const isFile = v.type === VarType.file
@@ -796,11 +777,9 @@ export const toNodeOutputVars = (
filterVar = (_payload: Var, _selector: ValueSelector) => true,
environmentVariables: EnvironmentVariable[] = [],
conversationVariables: ConversationVariable[] = [],
memoryVariables: MemoryVariable[] = [],
ragVariables: RAGPipelineVariable[] = [],
allPluginInfoList: Record<string, ToolWithProvider[]>,
schemaTypeDefinitions?: SchemaTypeDefinition[],
conversationVariablesFirst: boolean = false,
): NodeOutPutVar[] => {
// ENV_NODE data format
const ENV_NODE = {
@@ -817,7 +796,6 @@ export const toNodeOutputVars = (
data: {
title: 'CONVERSATION',
type: 'conversation',
memoryVarList: memoryVariables,
chatVarList: conversationVariables,
},
}
@@ -1017,7 +995,6 @@ export const getVarType = ({
isConstant,
environmentVariables = [],
conversationVariables = [],
memoryVariables = [],
ragVariables = [],
allPluginInfoList,
schemaTypeDefinitions,
@@ -1032,7 +1009,6 @@ export const getVarType = ({
isConstant?: boolean;
environmentVariables?: EnvironmentVariable[];
conversationVariables?: ConversationVariable[];
memoryVariables?: MemoryVariable[];
ragVariables?: RAGPipelineVariable[];
allPluginInfoList: Record<string, ToolWithProvider[]>;
schemaTypeDefinitions?: SchemaTypeDefinition[];
@@ -1046,7 +1022,6 @@ export const getVarType = ({
undefined,
environmentVariables,
conversationVariables,
memoryVariables,
ragVariables,
allPluginInfoList,
schemaTypeDefinitions,
@@ -1175,12 +1150,10 @@ export const toNodeAvailableVars = ({
isChatMode,
environmentVariables,
conversationVariables,
memoryVariables,
ragVariables,
filterVar,
allPluginInfoList,
schemaTypeDefinitions,
conversationVariablesFirst,
}: {
parentNode?: Node | null;
t?: any;
@@ -1191,14 +1164,11 @@ export const toNodeAvailableVars = ({
environmentVariables?: EnvironmentVariable[];
// chat var
conversationVariables?: ConversationVariable[];
// memory variables
memoryVariables?: MemoryVariable[];
// rag variables
ragVariables?: RAGPipelineVariable[];
filterVar: (payload: Var, selector: ValueSelector) => boolean;
allPluginInfoList: Record<string, ToolWithProvider[]>;
schemaTypeDefinitions?: SchemaTypeDefinition[];
conversationVariablesFirst?: boolean
}): NodeOutPutVar[] => {
const beforeNodesOutputVars = toNodeOutputVars(
beforeNodes,
@@ -1206,11 +1176,9 @@ export const toNodeAvailableVars = ({
filterVar,
environmentVariables,
conversationVariables,
memoryVariables,
ragVariables,
allPluginInfoList,
schemaTypeDefinitions,
conversationVariablesFirst,
)
const isInIteration = parentNode?.data.type === BlockEnum.Iteration
if (isInIteration) {
@@ -1223,7 +1191,6 @@ export const toNodeAvailableVars = ({
isChatMode,
environmentVariables,
conversationVariables,
memoryVariables,
allPluginInfoList,
schemaTypeDefinitions,
})

View File

@@ -65,7 +65,6 @@ const Item: FC<ItemProps> = ({
const isSys = itemData.variable.startsWith('sys.')
const isEnv = itemData.variable.startsWith('env.')
const isChatVar = itemData.variable.startsWith('conversation.')
const isMemoryVar = itemData.variable.startsWith('memory_block')
const isRagVariable = itemData.isRagVariable
const flatVarIcon = useMemo(() => {
if (!isFlat)
@@ -153,7 +152,7 @@ const Item: FC<ItemProps> = ({
if (isFlat) {
onChange([itemData.variable], itemData)
}
else if (isSys || isEnv || isChatVar || isMemoryVar || isRagVariable) { // system variable | environment variable | conversation variable
else if (isSys || isEnv || isChatVar || isRagVariable) { // system variable | environment variable | conversation variable
onChange([...objPath, ...itemData.variable.split('.')], itemData)
}
else {
@@ -163,16 +162,10 @@ const Item: FC<ItemProps> = ({
const variableCategory = useMemo(() => {
if (isEnv) return 'environment'
if (isChatVar) return 'conversation'
if (isMemoryVar) return 'memory_block'
if (isLoopVar) return 'loop'
if (isRagVariable) return 'rag'
return 'system'
}, [isEnv, isChatVar, isMemoryVar, isSys, isLoopVar, isRagVariable])
const variableType = useMemo(() => {
if (itemData.type === 'memory_block')
return 'memory'
return itemData.type
}, [itemData.type])
}, [isEnv, isChatVar, isSys, isLoopVar, isRagVariable])
return (
<PortalToFollowElem
open={open}
@@ -200,7 +193,7 @@ const Item: FC<ItemProps> = ({
/>}
{isFlat && flatVarIcon}
{!isEnv && !isChatVar && !isMemoryVar && !isRagVariable && (
{!isEnv && !isChatVar && !isRagVariable && (
<div title={itemData.variable} className='system-sm-medium ml-1 w-0 grow truncate text-text-secondary'>{varName}</div>
)}
{isEnv && (
@@ -209,15 +202,11 @@ const Item: FC<ItemProps> = ({
{isChatVar && (
<div title={itemData.des} className='system-sm-medium ml-1 w-0 grow truncate text-text-secondary'>{itemData.variable.replace('conversation.', '')}</div>
)}
{isMemoryVar && (
<div title={itemData.memoryVariableName} className='system-sm-medium ml-1 w-0 grow truncate text-text-secondary'>{itemData.memoryVariableName}</div>
)
}
{isRagVariable && (
<div title={itemData.des} className='system-sm-medium ml-1 w-0 grow truncate text-text-secondary'>{itemData.variable.split('.').slice(-1)[0]}</div>
)}
</div>
<div className='ml-1 shrink-0 text-xs font-normal capitalize text-text-tertiary'>{(preferSchemaType && itemData.schemaType) ? itemData.schemaType : variableType}</div>
<div className='ml-1 shrink-0 text-xs font-normal capitalize text-text-tertiary'>{(preferSchemaType && itemData.schemaType) ? itemData.schemaType : itemData.type}</div>
{
(isObj || isStructureOutput) && (
<ChevronRight className={cn('ml-0.5 h-3 w-3 text-text-quaternary', isHovering && 'text-text-tertiary')} />

View File

@@ -22,7 +22,6 @@ const VariableLabel = ({
errorMsg,
onClick,
isExceptionVariable,
isMemoryVariable,
ref,
notShowFullPath,
rightSlot,

View File

@@ -1,13 +1,12 @@
import { useMemo } from 'react'
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
import { BubbleX, Env, GlobalVariable, Memory } from '@/app/components/base/icons/src/vender/line/others'
import { BubbleX, Env, GlobalVariable } from '@/app/components/base/icons/src/vender/line/others'
import { Loop } from '@/app/components/base/icons/src/vender/workflow'
import { InputField } from '@/app/components/base/icons/src/vender/pipeline'
import {
isConversationVar,
isENV,
isGlobalVar,
isMemoryVariable,
isRagVariableVar,
isSystemVar,
} from '../utils'
@@ -24,9 +23,6 @@ export const useVarIcon = (variables: string[], variableCategory?: VarInInspectT
if (isENV(variables) || variableCategory === VarInInspectType.environment || variableCategory === 'environment')
return Env
if (isMemoryVariable(variables) || variableCategory === VarInInspectType.memory || variableCategory === 'memory_block')
return Memory
if (isConversationVar(variables) || variableCategory === VarInInspectType.conversation || variableCategory === 'conversation')
return BubbleX
@@ -50,9 +46,6 @@ export const useVarColor = (variables: string[], isExceptionVariable?: boolean,
if (isConversationVar(variables) || variableCategory === VarInInspectType.conversation || variableCategory === 'conversation')
return 'text-util-colors-teal-teal-700'
if (isMemoryVariable(variables) || variableCategory === VarInInspectType.memory || variableCategory === 'memory_block')
return 'text-util-colors-teal-teal-700'
if (isGlobalVar(variables) || variableCategory === VarInInspectType.system)
return 'text-util-colors-orange-orange-600'
@@ -112,15 +105,6 @@ export const useVarBgColorInEditor = (variables: string[], hasError?: boolean) =
}
}
if (isMemoryVariable(variables)) {
return {
hoverBorderColor: 'hover:border-util-colors-teal-teal-100',
hoverBgColor: 'hover:bg-util-colors-teal-teal-50',
selectedBorderColor: 'border-util-colors-teal-teal-600',
selectedBgColor: 'bg-util-colors-teal-teal-50',
}
}
return {
hoverBorderColor: 'hover:border-state-accent-alt',
hoverBgColor: 'hover:bg-state-accent-hover',

View File

@@ -13,7 +13,6 @@ export type VariablePayload = {
onClick?: (e: React.MouseEvent<HTMLDivElement>) => void
errorMsg?: string
isExceptionVariable?: boolean
isMemoryVariable?: boolean
ref?: React.Ref<HTMLDivElement>
notShowFullPath?: boolean
rightSlot?: ReactNode

View File

@@ -15,8 +15,6 @@ type Params = {
hideChatVar?: boolean
filterVar: (payload: Var, selector: ValueSelector) => boolean
passedInAvailableNodes?: Node[]
conversationVariablesFirst?: boolean
isMemorySupported?: boolean
}
// TODO: loop type?
@@ -26,8 +24,6 @@ const useAvailableVarList = (nodeId: string, {
hideEnv,
hideChatVar,
passedInAvailableNodes,
conversationVariablesFirst,
isMemorySupported,
}: Params = {
onlyLeafNodeVar: false,
filterVar: () => true,
@@ -67,35 +63,13 @@ const useAvailableVarList = (nodeId: string, {
})
}
}
const nodeAvailableVars = getNodeAvailableVars({
const availableVars = [...getNodeAvailableVars({
parentNode: iterationNode,
beforeNodes: availableNodes,
isChatMode,
filterVar,
hideEnv,
hideChatVar,
conversationVariablesFirst,
})
const availableVars = [...nodeAvailableVars.map((availableVar) => {
if (availableVar.nodeId === 'conversation') {
return {
...availableVar,
vars: availableVar.vars.filter((v) => {
if (!v.isMemoryVariable)
return true
if (!isMemorySupported)
return false
if (!v.memoryVariableNodeId)
return true
return v.memoryVariableNodeId === nodeId
}),
}
}
return availableVar
}), ...dataSourceRagVars]
return {

View File

@@ -1,5 +1,3 @@
import { useCallback } from 'react'
import { useStoreApi } from 'reactflow'
import { useNodeDataUpdate } from '@/app/components/workflow/hooks'
import type { CommonNodeType } from '@/app/components/workflow/types'
const useNodeCrud = <T>(id: string, data: CommonNodeType<T>) => {
@@ -18,28 +16,4 @@ const useNodeCrud = <T>(id: string, data: CommonNodeType<T>) => {
}
}
export const useNodeUpdate = (id: string) => {
const store = useStoreApi()
const { handleNodeDataUpdateWithSyncDraft } = useNodeDataUpdate()
const getNodeData = useCallback(() => {
const { getNodes } = store.getState()
const nodes = getNodes()
return nodes.find(node => node.id === id)
}, [store, id])
const handleNodeDataUpdate = useCallback((data: any) => {
handleNodeDataUpdateWithSyncDraft({
id,
data,
})
}, [id, handleNodeDataUpdateWithSyncDraft])
return {
getNodeData,
handleNodeDataUpdate,
}
}
export default useNodeCrud

View File

@@ -145,7 +145,6 @@ const useOneStepRun = <T>({
const { t } = useTranslation()
const { getBeforeNodesInSameBranch, getBeforeNodesInSameBranchIncludeParent } = useWorkflow() as any
const conversationVariables = useStore(s => s.conversationVariables)
const memoryVariables = useStore(s => s.memoryVariables)
const isChatMode = useIsChatMode()
const isIteration = data.type === BlockEnum.Iteration
const isLoop = data.type === BlockEnum.Loop
@@ -174,7 +173,7 @@ const useOneStepRun = <T>({
dataSourceList: dataSourceList || [],
}
const allOutputVars = toNodeOutputVars(availableNodes, isChatMode, undefined, undefined, conversationVariables, memoryVariables, [], allPluginInfoList, schemaTypeDefinitions)
const allOutputVars = toNodeOutputVars(availableNodes, isChatMode, undefined, undefined, conversationVariables, [], allPluginInfoList, schemaTypeDefinitions)
const targetVar = allOutputVars.find(item => isSystem ? !!item.isStartNode : item.nodeId === valueSelector[0])
if (!targetVar)
return undefined

View File

@@ -58,7 +58,7 @@ const useConfig = (id: string, payload: IterationNodeType) => {
mcpTools: mcpTools || [],
dataSourceList: dataSourceList || [],
}
const childrenNodeVars = toNodeOutputVars(iterationChildrenNodes, isChatMode, undefined, [], [], [], [], allPluginInfoList)
const childrenNodeVars = toNodeOutputVars(iterationChildrenNodes, isChatMode, undefined, [], [], [], allPluginInfoList)
const handleOutputVarChange = useCallback((output: ValueSelector | string, _varKindType: VarKindType, varInfo?: Var) => {
if (isEqual(inputs.output_selector, output as ValueSelector))

View File

@@ -39,7 +39,6 @@ type Props = {
varList: Variable[]
handleAddVariable: (payload: any) => void
modelConfig?: ModelConfig
isMemorySupported?: boolean
}
const roleOptions = [
@@ -82,7 +81,6 @@ const ConfigPromptItem: FC<Props> = ({
varList,
handleAddVariable,
modelConfig,
isMemorySupported,
}) => {
const { t } = useTranslation()
const workflowStore = useWorkflowStore()
@@ -148,17 +146,6 @@ const ConfigPromptItem: FC<Props> = ({
varList={varList}
handleAddVariable={handleAddVariable}
isSupportFileVar
placeholder={
<>
<div>
{t(`${i18nPrefix}.promptEditorPlaceholder1`)}
</div>
<div>
{t(`${i18nPrefix}.promptEditorPlaceholder2`)}
</div>
</>
}
isMemorySupported={isMemorySupported}
/>
)
}

View File

@@ -34,7 +34,6 @@ type Props = {
varList?: Variable[]
handleAddVariable: (payload: any) => void
modelConfig: ModelConfig
isMemorySupported?: boolean
}
const ConfigPrompt: FC<Props> = ({
@@ -50,7 +49,6 @@ const ConfigPrompt: FC<Props> = ({
varList = [],
handleAddVariable,
modelConfig,
isMemorySupported,
}) => {
const { t } = useTranslation()
const workflowStore = useWorkflowStore()
@@ -75,8 +73,6 @@ const ConfigPrompt: FC<Props> = ({
} = useAvailableVarList(nodeId, {
onlyLeafNodeVar: false,
filterVar,
conversationVariablesFirst: true,
isMemorySupported,
})
const handleChatModePromptChange = useCallback((index: number) => {
@@ -208,7 +204,6 @@ const ConfigPrompt: FC<Props> = ({
varList={varList}
handleAddVariable={handleAddVariable}
modelConfig={modelConfig}
isMemorySupported={isMemorySupported}
/>
</div>
)
@@ -245,7 +240,6 @@ const ConfigPrompt: FC<Props> = ({
handleAddVariable={handleAddVariable}
onGenerated={handleGenerated}
modelConfig={modelConfig}
isMemorySupported={isMemorySupported}
/>
</div>
)}

View File

@@ -1,116 +0,0 @@
import {
memo,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import {
RiDeleteBinLine,
RiEditLine,
} from '@remixicon/react'
import type { Memory } from '@/app/components/workflow/types'
import Badge from '@/app/components/base/badge'
import ActionButton from '@/app/components/base/action-button'
import { useMemoryVariables } from './hooks/use-memory-variables'
import Confirm from '@/app/components/base/confirm'
import { Memory as MemoryIcon } from '@/app/components/base/icons/src/vender/line/others'
import VariableModal from '@/app/components/workflow/panel/chat-variable-panel/components/variable-modal'
import cn from '@/utils/classnames'
type BlockMemoryProps = {
id: string
payload: Memory
}
const BlockMemory = ({ id }: BlockMemoryProps) => {
const { t } = useTranslation()
const [destructiveItemId, setDestructiveItemId] = useState<string | undefined>(undefined)
const {
memoryVariablesInUsed,
editMemoryVariable,
handleSetEditMemoryVariable,
handleEdit,
handleDelete,
handleDeleteConfirm,
cacheForDeleteMemoryVariable,
setCacheForDeleteMemoryVariable,
} = useMemoryVariables(id)
if (!memoryVariablesInUsed?.length) {
return (
<div className='system-xs-regular mt-2 flex items-center justify-center rounded-[10px] bg-background-section p-3 text-text-tertiary'>
{t('workflow.nodes.common.memory.block.empty')}
</div>
)
}
return (
<>
<div className='mt-2 space-y-1'>
{
memoryVariablesInUsed.map(memoryVariable => (
<div
key={memoryVariable.id}
className={cn(
'group flex h-8 items-center space-x-1 rounded-lg border-[0.5px] border-components-panel-border-subtle bg-components-panel-on-panel-item-bg pl-2 pr-1 shadow-xs',
destructiveItemId === memoryVariable.id && 'border border-state-destructive-solid bg-state-destructive-hover',
)}>
<MemoryIcon className='h-4 w-4 text-util-colors-teal-teal-700' />
<div
title={memoryVariable.name}
className='system-sm-medium grow truncate text-text-secondary'
>
{memoryVariable.name}
</div>
<Badge className={cn('shrink-0 group-hover:hidden', editMemoryVariable?.id === memoryVariable.id && 'hidden')}>
{memoryVariable.term}
</Badge>
<ActionButton
className={cn(
'hidden shrink-0 group-hover:inline-flex',
editMemoryVariable?.id === memoryVariable.id && 'inline-flex bg-state-base-hover text-text-secondary',
)}
size='m'
onClick={() => handleSetEditMemoryVariable(memoryVariable.id)}
>
<RiEditLine className='h-4 w-4 text-text-tertiary' />
</ActionButton>
<ActionButton
className={cn(
'hidden shrink-0 bg-transparent hover:bg-transparent hover:text-text-destructive group-hover:inline-flex',
editMemoryVariable?.id === memoryVariable.id && 'inline-flex',
)}
size='m'
onClick={() => handleDelete(memoryVariable)}
onMouseOver={() => setDestructiveItemId(memoryVariable.id)}
onMouseOut={() => setDestructiveItemId(undefined)}
>
<RiDeleteBinLine className={cn('h-4 w-4', destructiveItemId === memoryVariable.id && 'text-text-destructive')} />
</ActionButton>
</div>
))
}
</div>
{
!!cacheForDeleteMemoryVariable && (
<Confirm
isShow
onCancel={() => setCacheForDeleteMemoryVariable(undefined)}
onConfirm={() => handleDeleteConfirm(cacheForDeleteMemoryVariable.id)}
title={t('workflow.nodes.common.memory.deleteNodeMemoryVariableConfirmTitle', { name: cacheForDeleteMemoryVariable.name })}
content={t('workflow.nodes.common.memory.deleteNodeMemoryVariableConfirmDesc')}
/>
)
}
{
!!editMemoryVariable && (
<VariableModal
chatVar={editMemoryVariable}
onClose={() => handleSetEditMemoryVariable(undefined)}
onSave={handleEdit}
nodeScopeMemoryVariable={{ nodeId: id }}
/>
)
}
</>
)
}
export default memo(BlockMemory)

View File

@@ -1,20 +0,0 @@
import { useCallback } from 'react'
import type { MemoryVariable } from '@/app/components/workflow/types'
import { useNodeUpdate } from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
import { findUsedVarNodes } from '@/app/components/workflow/nodes/_base/components/variable/utils'
export const useMemoryUsedDetector = (nodeId: string) => {
const { getNodeData } = useNodeUpdate(nodeId)
const getMemoryUsedDetector = useCallback((chatVar: MemoryVariable) => {
const nodeData = getNodeData()!
const valueSelector = ['memory_block', chatVar.node_id ? `${chatVar.node_id}_${chatVar.id}` : chatVar.id]
return findUsedVarNodes(
valueSelector,
[nodeData],
)
}, [getNodeData])
return {
getMemoryUsedDetector,
}
}

View File

@@ -1,77 +0,0 @@
import {
useCallback,
useMemo,
useState,
} from 'react'
import {
useStore,
useWorkflowStore,
} from '@/app/components/workflow/store'
import type { MemoryVariable } from '@/app/components/workflow/types'
import { useNodesSyncDraft } from '@/app/components/workflow/hooks/use-nodes-sync-draft'
import { useMemoryUsedDetector } from './use-memory-used-detector'
export const useMemoryVariables = (nodeId: string) => {
const workflowStore = useWorkflowStore()
const memoryVariables = useStore(s => s.memoryVariables)
const [editMemoryVariable, setEditMemoryVariable] = useState<MemoryVariable | undefined>(undefined)
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
const { getMemoryUsedDetector } = useMemoryUsedDetector(nodeId)
const [cacheForDeleteMemoryVariable, setCacheForDeleteMemoryVariable] = useState<MemoryVariable | undefined>(undefined)
const memoryVariablesInUsed = useMemo(() => {
return memoryVariables.filter(variable => variable.node_id === nodeId)
}, [memoryVariables, nodeId])
const handleSave = useCallback((newMemoryVar: MemoryVariable) => {
const { memoryVariables, setMemoryVariables } = workflowStore.getState()
const newList = [newMemoryVar, ...memoryVariables]
setMemoryVariables(newList)
handleSyncWorkflowDraft()
}, [handleSyncWorkflowDraft, workflowStore])
const handleSetEditMemoryVariable = (memoryVariableId?: string) => {
if (!memoryVariableId) {
setEditMemoryVariable(undefined)
return
}
const memoryVariable = memoryVariables.find(variable => variable.id === memoryVariableId)
setEditMemoryVariable(memoryVariable)
}
const handleEdit = (memoryVariable: MemoryVariable) => {
const { memoryVariables, setMemoryVariables } = workflowStore.getState()
const newList = memoryVariables.map(variable => variable.id === memoryVariable.id ? memoryVariable : variable)
setMemoryVariables(newList)
handleSyncWorkflowDraft()
}
const handleDeleteConfirm = (memoryVariableId?: string) => {
const { memoryVariables, setMemoryVariables } = workflowStore.getState()
const newList = memoryVariables.filter(variable => variable.id !== memoryVariableId)
setMemoryVariables(newList)
handleSyncWorkflowDraft()
setCacheForDeleteMemoryVariable(undefined)
}
const handleDelete = (memoryVariable: MemoryVariable) => {
const effectedNodes = getMemoryUsedDetector(memoryVariable)
if (effectedNodes.length > 0) {
setCacheForDeleteMemoryVariable(memoryVariable)
return
}
handleDeleteConfirm(memoryVariable.id)
}
return {
memoryVariablesInUsed,
handleDelete,
handleSave,
handleSetEditMemoryVariable,
handleEdit,
editMemoryVariable,
handleDeleteConfirm,
cacheForDeleteMemoryVariable,
setCacheForDeleteMemoryVariable,
}
}

View File

@@ -1,136 +0,0 @@
import {
useCallback,
useMemo,
useState,
} from 'react'
import type { LLMNodeType } from '../../../types'
import { useNodeUpdate } from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
import {
MEMORY_DEFAULT,
} from '../linear-memory'
import type { Memory } from '@/app/components/workflow/types'
import { MemoryMode } from '@/app/components/workflow/types'
import { useWorkflowStore } from '@/app/components/workflow/store'
import { useMemoryUsedDetector } from './use-memory-used-detector'
export const useMemory = (
id: string,
data: LLMNodeType,
) => {
const workflowStore = useWorkflowStore()
const { memory } = data
const initCollapsed = useMemo(() => {
if (!memory?.enabled)
return true
return false
}, [memory])
const [collapsed, setCollapsed] = useState(initCollapsed)
const {
getNodeData,
handleNodeDataUpdate,
} = useNodeUpdate(id)
const [showTipsWhenMemoryModeBlockToLinear, setShowTipsWhenMemoryModeBlockToLinear] = useState(false)
const { getMemoryUsedDetector } = useMemoryUsedDetector(id)
const handleMemoryTypeChange = useCallback((value: string) => {
const nodeData = getNodeData()
const { memory: memoryData = {} as Memory } = nodeData?.data as LLMNodeType
if (value === MemoryMode.disabled) {
setCollapsed(true)
handleNodeDataUpdate({
memory: {
...memoryData,
enabled: false,
mode: '',
},
})
}
if (value === MemoryMode.linear) {
setCollapsed(false)
handleNodeDataUpdate({
memory: {
...memoryData,
enabled: true,
mode: MemoryMode.linear,
window: memoryData?.window || MEMORY_DEFAULT.window,
query_prompt_template: memoryData?.query_prompt_template || MEMORY_DEFAULT.query_prompt_template,
},
})
}
if (value === MemoryMode.block) {
setCollapsed(false)
handleNodeDataUpdate({
memory: {
...memoryData,
enabled: true,
mode: MemoryMode.block,
block_id: memoryData?.block_id || [],
query_prompt_template: memoryData?.query_prompt_template || MEMORY_DEFAULT.query_prompt_template,
},
})
}
setShowTipsWhenMemoryModeBlockToLinear(false)
}, [getNodeData, handleNodeDataUpdate])
const handleMemoryTypeChangeBefore = useCallback((value: string) => {
const nodeData = getNodeData()
const { memory: memoryData = {} as Memory } = nodeData?.data as LLMNodeType
const { memoryVariables } = workflowStore.getState()
if (memoryData.mode === MemoryMode.block && value === MemoryMode.linear && nodeData) {
const globalMemoryVariables = memoryVariables.filter(variable => variable.scope === 'app')
const currentNodeMemoryVariables = memoryVariables.filter(variable => variable.node_id === id)
const allMemoryVariables = [...globalMemoryVariables, ...currentNodeMemoryVariables]
for (const variable of allMemoryVariables) {
const effectedNodes = getMemoryUsedDetector(variable)
if (effectedNodes.length > 0) {
setShowTipsWhenMemoryModeBlockToLinear(true)
return
}
}
handleMemoryTypeChange(value)
}
else {
handleMemoryTypeChange(value)
}
}, [getNodeData, workflowStore, handleMemoryTypeChange])
const handleUpdateMemory = useCallback((memory: Memory) => {
handleNodeDataUpdate({
memory,
})
}, [handleNodeDataUpdate])
const memoryType = useMemo(() => {
if (!memory)
return MemoryMode.disabled
if (!('enabled' in memory))
return MemoryMode.linear
if (memory.enabled) {
if (memory.mode === MemoryMode.linear)
return MemoryMode.linear
if (memory.mode === MemoryMode.block)
return MemoryMode.block
}
else {
return MemoryMode.disabled
}
}, [memory])
return {
collapsed,
setCollapsed,
handleMemoryTypeChange,
handleMemoryTypeChangeBefore,
memoryType,
handleUpdateMemory,
showTipsWhenMemoryModeBlockToLinear,
setShowTipsWhenMemoryModeBlockToLinear,
}
}

View File

@@ -1,139 +0,0 @@
import {
memo,
useCallback,
} from 'react'
import { useTranslation } from 'react-i18next'
import { RiAddLine } from '@remixicon/react'
import Collapse from '@/app/components/workflow/nodes/_base/components/collapse'
import type {
Node,
} from '@/app/components/workflow/types'
import Divider from '@/app/components/base/divider'
import Tooltip from '@/app/components/base/tooltip'
import MemoryCreateButton from './memory-create-button'
import MemorySelector from './memory-selector'
import LinearMemory from './linear-memory'
import type { Memory } from '@/app/components/workflow/types'
import type { LLMNodeType } from '../../types'
import { useMemory } from './hooks/use-memory'
import Split from '@/app/components/workflow/nodes/_base/components/split'
import { MemoryMode } from '@/app/components/workflow/types'
import BlockMemory from './block-memory'
import { ActionButton } from '@/app/components/base/action-button'
import cn from '@/utils/classnames'
import Confirm from '@/app/components/base/confirm'
type MemoryProps = Pick<Node, 'id' | 'data'> & {
readonly?: boolean
canSetRoleName?: boolean
}
const MemorySystem = ({
id,
data,
readonly,
canSetRoleName,
}: MemoryProps) => {
const { t } = useTranslation()
const { memory } = data as LLMNodeType
const {
collapsed,
setCollapsed,
handleMemoryTypeChange,
handleMemoryTypeChangeBefore,
memoryType,
handleUpdateMemory,
showTipsWhenMemoryModeBlockToLinear,
setShowTipsWhenMemoryModeBlockToLinear,
} = useMemory(id, data as LLMNodeType)
const renderTrigger = useCallback((open?: boolean) => {
return (
<ActionButton className={cn('shrink-0', open && 'bg-state-base-hover')}>
<RiAddLine className='h-4 w-4' />
</ActionButton>
)
}, [])
return (
<>
<div className=''>
<Collapse
disabled={!memory?.enabled}
collapsed={collapsed}
onCollapse={setCollapsed}
hideCollapseIcon
triggerClassName='ml-0 system-sm-semibold-uppercase'
trigger={
collapseIcon => (
<div className='flex grow items-center justify-between'>
<div className='flex items-center'>
<div className='system-sm-semibold-uppercase mr-0.5 text-text-secondary'>
{t('workflow.nodes.common.memory.memory')}
</div>
<Tooltip
popupContent={t('workflow.nodes.common.memory.memoryTip')}
triggerClassName='w-4 h-4'
/>
{collapseIcon}
{
memoryType === MemoryMode.block && (
<>
<Divider type='vertical' className='!ml-1.5 !mr-1 h-3 !w-px bg-divider-regular' />
<div onClick={e => e.stopPropagation()}>
<MemoryCreateButton
nodeId={id}
renderTrigger={renderTrigger}
/>
</div>
</>
)
}
</div>
<MemorySelector
value={memoryType}
onSelected={handleMemoryTypeChangeBefore}
readonly={readonly}
/>
</div>
)}
>
<>
{
memoryType === MemoryMode.linear && !collapsed && (
<LinearMemory
className='mt-2'
payload={memory as Memory}
onChange={handleUpdateMemory}
readonly={readonly}
canSetRoleName={canSetRoleName}
/>
)
}
{
memoryType === MemoryMode.block && !collapsed && (
<BlockMemory id={id} payload={memory as Memory} />
)
}
</>
</Collapse>
<Split className='mt-4' />
</div>
{
showTipsWhenMemoryModeBlockToLinear && (
<Confirm
isShow
type='info'
showCancel={false}
onCancel={() => setShowTipsWhenMemoryModeBlockToLinear(false)}
onConfirm={() => handleMemoryTypeChange(MemoryMode.linear)}
confirmText={t('workflow.nodes.common.memory.toLinearConfirmButton')}
title={t('workflow.nodes.common.memory.toLinearConfirmTitle')}
content={t('workflow.nodes.common.memory.toLinearConfirmDesc')}
/>
)
}
</>
)
}
export default memo(MemorySystem)

View File

@@ -1,182 +0,0 @@
import {
memo,
useCallback,
} from 'react'
import { produce } from 'immer'
import { useTranslation } from 'react-i18next'
import Switch from '@/app/components/base/switch'
import Slider from '@/app/components/base/slider'
import Input from '@/app/components/base/input'
import type { Memory } from '@/app/components/workflow/types'
import { MemoryRole } from '@/app/components/workflow/types'
import cn from '@/utils/classnames'
const WINDOW_SIZE_MIN = 1
const WINDOW_SIZE_MAX = 100
export const WINDOW_SIZE_DEFAULT = 50
export const MEMORY_DEFAULT: Memory = {
window: { enabled: false, size: WINDOW_SIZE_DEFAULT },
query_prompt_template: '{{#sys.query#}}\n\n{{#sys.files#}}',
}
type RoleItemProps = {
readonly?: boolean
title: string
value: string
onChange: (value: string) => void
}
const RoleItem = ({
readonly,
title,
value,
onChange,
}: RoleItemProps) => {
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
onChange(e.target.value)
}, [onChange])
return (
<div className='flex items-center justify-between'>
<div className='text-[13px] font-normal text-text-secondary'>{title}</div>
<Input
readOnly={readonly}
value={value}
onChange={handleChange}
className='h-8 w-[200px]'
type='text' />
</div>
)
}
type LinearMemoryProps = {
payload: Memory
readonly?: boolean
onChange: (payload: Memory) => void
canSetRoleName?: boolean
className?: string
}
const LinearMemory = ({
payload,
readonly,
onChange,
canSetRoleName,
className,
}: LinearMemoryProps) => {
const i18nPrefix = 'workflow.nodes.common.memory'
const { t } = useTranslation()
const handleWindowEnabledChange = useCallback((enabled: boolean) => {
const newPayload = produce(payload || MEMORY_DEFAULT, (draft) => {
if (!draft.window)
draft.window = { enabled: false, size: WINDOW_SIZE_DEFAULT }
draft.window.enabled = enabled
})
onChange(newPayload)
}, [payload, onChange])
const handleWindowSizeChange = useCallback((size: number | string) => {
const newPayload = produce(payload || MEMORY_DEFAULT, (draft) => {
if (!draft.window)
draft.window = { enabled: true, size: WINDOW_SIZE_DEFAULT }
let limitedSize: null | string | number = size
if (limitedSize === '') {
limitedSize = null
}
else {
limitedSize = Number.parseInt(limitedSize as string, 10)
if (Number.isNaN(limitedSize))
limitedSize = WINDOW_SIZE_DEFAULT
if (limitedSize < WINDOW_SIZE_MIN)
limitedSize = WINDOW_SIZE_MIN
if (limitedSize > WINDOW_SIZE_MAX)
limitedSize = WINDOW_SIZE_MAX
}
draft.window.size = limitedSize as number
})
onChange(newPayload)
}, [payload, onChange])
const handleBlur = useCallback(() => {
if (!payload)
return
if (payload.window.size === '' || payload.window.size === null)
handleWindowSizeChange(WINDOW_SIZE_DEFAULT)
}, [handleWindowSizeChange, payload])
const handleRolePrefixChange = useCallback((role: MemoryRole) => {
return (value: string) => {
const newPayload = produce(payload || MEMORY_DEFAULT, (draft) => {
if (!draft.role_prefix) {
draft.role_prefix = {
user: '',
assistant: '',
}
}
draft.role_prefix[role] = value
})
onChange(newPayload)
}
}, [payload, onChange])
return (
<>
<div className={cn('flex justify-between', className)}>
<div className='flex h-8 items-center space-x-2'>
<Switch
defaultValue={payload?.window?.enabled}
onChange={handleWindowEnabledChange}
size='md'
disabled={readonly}
/>
<div className='system-xs-medium-uppercase text-text-tertiary'>{t(`${i18nPrefix}.windowSize`)}</div>
</div>
<div className='flex h-8 items-center space-x-2'>
<Slider
className='w-[144px]'
value={(payload.window?.size || WINDOW_SIZE_DEFAULT) as number}
min={WINDOW_SIZE_MIN}
max={WINDOW_SIZE_MAX}
step={1}
onChange={handleWindowSizeChange}
disabled={readonly || !payload.window?.enabled}
/>
<Input
value={(payload.window?.size || WINDOW_SIZE_DEFAULT) as number}
wrapperClassName='w-12'
className='appearance-none pr-0'
type='number'
min={WINDOW_SIZE_MIN}
max={WINDOW_SIZE_MAX}
step={1}
onChange={e => handleWindowSizeChange(e.target.value)}
onBlur={handleBlur}
disabled={readonly || !payload.window?.enabled}
/>
</div>
</div>
{canSetRoleName && (
<div className='mt-4'>
<div className='text-xs font-medium uppercase leading-6 text-text-tertiary'>{t(`${i18nPrefix}.conversationRoleName`)}</div>
<div className='mt-1 space-y-2'>
<RoleItem
readonly={!!readonly}
title={t(`${i18nPrefix}.user`)}
value={payload.role_prefix?.user || ''}
onChange={handleRolePrefixChange(MemoryRole.user)}
/>
<RoleItem
readonly={!!readonly}
title={t(`${i18nPrefix}.assistant`)}
value={payload.role_prefix?.assistant || ''}
onChange={handleRolePrefixChange(MemoryRole.assistant)}
/>
</div>
</div>
)}
</>
)
}
export default memo(LinearMemory)

View File

@@ -1,70 +0,0 @@
import { useCallback, useState } from 'react'
import VariableModal from '@/app/components/workflow/panel/chat-variable-panel/components/variable-modal'
import type { OffsetOptions, Placement } from '@floating-ui/react'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import type { MemoryVariable } from '@/app/components/workflow/types'
import { useEventEmitterContextContext } from '@/context/event-emitter'
import { MEMORY_VAR_CREATED_BY_MODAL_BY_EVENT_EMITTER, MEMORY_VAR_MODAL_SHOW_BY_EVENT_EMITTER } from '@/app/components/workflow/nodes/_base/components/prompt/type'
import { useMemoryVariables } from './hooks/use-memory-variables'
type Props = {
placement?: Placement
offset?: number | OffsetOptions
hideTrigger?: boolean
instanceId?: string
nodeId: string
renderTrigger?: (open?: boolean) => React.ReactNode
}
const MemoryCreateButton = ({
placement,
offset,
instanceId,
nodeId,
renderTrigger = () => <div></div>,
}: Props) => {
const { eventEmitter } = useEventEmitterContextContext()
const [open, setOpen] = useState(false)
const { handleSave: handleSaveMemoryVariables } = useMemoryVariables(nodeId)
const handleSave = useCallback((newMemoryVar: MemoryVariable) => {
handleSaveMemoryVariables(newMemoryVar)
if (instanceId)
eventEmitter?.emit({ type: MEMORY_VAR_CREATED_BY_MODAL_BY_EVENT_EMITTER, instanceId, variable: ['memory', newMemoryVar.name] } as any)
}, [handleSaveMemoryVariables, eventEmitter, instanceId])
eventEmitter?.useSubscription((v: any) => {
if (v.type === MEMORY_VAR_MODAL_SHOW_BY_EVENT_EMITTER && v.instanceId === instanceId)
setOpen(true)
})
return (
<>
<PortalToFollowElem
open={open}
onOpenChange={setOpen}
placement={placement || 'left'}
offset={offset}
>
<PortalToFollowElemTrigger onClick={() => setOpen(v => !v)}>
{renderTrigger?.(open)}
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[11]'>
<VariableModal
onSave={handleSave}
onClose={() => {
setOpen(false)
}}
nodeScopeMemoryVariable={nodeId ? { nodeId } : undefined}
/>
</PortalToFollowElemContent>
</PortalToFollowElem>
</>
)
}
export default MemoryCreateButton

View File

@@ -1,107 +0,0 @@
import {
memo,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import {
RiArrowDownSLine,
RiCheckLine,
} from '@remixicon/react'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import Button from '@/app/components/base/button'
import { MemoryMode } from '@/app/components/workflow/types'
type MemorySelectorProps = {
value?: string
onSelected: (value: string) => void
readonly?: boolean
}
const MemorySelector = ({
value,
onSelected,
readonly,
}: MemorySelectorProps) => {
const { t } = useTranslation()
const [open, setOpen] = useState(false)
const options = [
{
value: MemoryMode.disabled,
label: t('workflow.nodes.common.memory.disabled.title'),
description: t('workflow.nodes.common.memory.disabled.desc'),
},
{
value: MemoryMode.linear,
label: t('workflow.nodes.common.memory.linear.title'),
description: t('workflow.nodes.common.memory.linear.desc'),
},
{
value: MemoryMode.block,
label: t('workflow.nodes.common.memory.block.title'),
description: t('workflow.nodes.common.memory.block.desc'),
},
]
const selectedOption = options.find(option => option.value === value)
return (
<PortalToFollowElem
open={open}
onOpenChange={setOpen}
placement='bottom-end'
offset={4}
>
<PortalToFollowElemTrigger onClick={(e) => {
if (readonly)
return
e.stopPropagation()
e.nativeEvent.stopImmediatePropagation()
setOpen(v => !v)
}}>
<Button
size='small'
disabled={readonly}
>
{selectedOption?.label || t('workflow.nodes.common.memory.disabled.title')}
<RiArrowDownSLine className='h-3.5 w-3.5' />
</Button>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[11]'>
<div className='w-[280px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg'>
{
options.map(option => (
<div
key={option.value}
className='flex cursor-pointer rounded-lg p-2 pr-3 hover:bg-state-base-hover'
onClick={(e) => {
if (readonly)
return
e.stopPropagation()
e.nativeEvent.stopImmediatePropagation()
onSelected(option.value)
setOpen(false)
}}
>
<div className='mr-1 w-4 shrink-0'>
{
value === option.value && (
<RiCheckLine className='h-4 w-4 text-text-accent' />
)
}
</div>
<div className='grow'>
<div className='system-sm-semibold mb-0.5 text-text-secondary'>{option.label}</div>
<div className='system-xs-regular text-text-tertiary'>{option.description}</div>
</div>
</div>
))
}
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
)
}
export default memo(MemorySelector)

View File

@@ -18,7 +18,6 @@ type Props = {
nodeId: string
editorId?: string
currentPrompt?: string
isBasicMode?: boolean
}
const PromptGeneratorBtn: FC<Props> = ({
@@ -27,7 +26,6 @@ const PromptGeneratorBtn: FC<Props> = ({
nodeId,
editorId,
currentPrompt,
isBasicMode,
}) => {
const [showAutomatic, { setTrue: showAutomaticTrue, setFalse: showAutomaticFalse }] = useBoolean(false)
const handleAutomaticRes = useCallback((res: GenRes) => {
@@ -52,8 +50,6 @@ const PromptGeneratorBtn: FC<Props> = ({
nodeId={nodeId}
editorId={editorId}
currentPrompt={currentPrompt}
isBasicMode={isBasicMode}
hideTryIt
/>
)}
</div>

View File

@@ -1,6 +1,7 @@
import type { FC } from 'react'
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import MemoryConfig from '../_base/components/memory-config'
import VarReferencePicker from '../_base/components/variable/var-reference-picker'
import ConfigVision from '../_base/components/config-vision'
import useConfig from './use-config'
@@ -21,9 +22,6 @@ import Switch from '@/app/components/base/switch'
import { RiAlertFill, RiQuestionLine } from '@remixicon/react'
import { fetchAndMergeValidCompletionParams } from '@/utils/completion-params'
import Toast from '@/app/components/base/toast'
import MemorySystem from './components/memory-system'
import { useMemory } from './components/memory-system/hooks/use-memory'
import { MemoryMode } from '@/app/components/workflow/types'
const i18nPrefix = 'workflow.nodes.llm'
@@ -55,6 +53,7 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
handleVarListChange,
handleVarNameChange,
handleSyeQueryChange,
handleMemoryChange,
handleVisionResolutionEnabledChange,
handleVisionResolutionChange,
isModelSupportStructuredOutput,
@@ -65,7 +64,6 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
filterJinja2InputVar,
handleReasoningFormatChange,
} = useConfig(id, data)
const { memoryType } = useMemory(id, data)
const model = inputs.model
@@ -153,7 +151,6 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
varList={inputs.prompt_config?.jinja2_variables || []}
handleAddVariable={handleAddVariable}
modelConfig={model}
isMemorySupported={memoryType === MemoryMode.block}
/>
)}
@@ -177,7 +174,7 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
)}
{/* Memory put place examples. */}
{isChatMode && isChatModel && (memoryType === MemoryMode.linear || memoryType === MemoryMode.block) && (
{isChatMode && isChatModel && !!inputs.memory && (
<div className='mt-4'>
<div className='flex h-8 items-center justify-between rounded-lg bg-components-input-bg-normal pl-3 pr-2'>
<div className='flex items-center space-x-1'>
@@ -201,7 +198,7 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
triggerClassName='w-4 h-4'
/>
</div>}
value={inputs.memory?.query_prompt_template || '{{#sys.query#}}'}
value={inputs.memory.query_prompt_template || '{{#sys.query#}}'}
onChange={handleSyeQueryChange}
readOnly={readOnly}
isShowContext={false}
@@ -211,10 +208,9 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
nodesOutputVars={availableVars}
availableNodes={availableNodesWithParent}
isSupportFileVar
instanceId={`${id}-chat-workflow-llm-prompt-editor-user`}
/>
{inputs.memory?.query_prompt_template && !inputs.memory.query_prompt_template.includes('{{#sys.query#}}') && (
{inputs.memory.query_prompt_template && !inputs.memory.query_prompt_template.includes('{{#sys.query#}}') && (
<div className='text-xs font-normal leading-[18px] text-[#DC6803]'>{t(`${i18nPrefix}.sysQueryInUser`)}</div>
)}
</div>
@@ -224,10 +220,11 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
{/* Memory */}
{isChatMode && (
<>
<MemorySystem
id={id}
data={data}
<Split />
<MemoryConfig
readonly={readOnly}
config={{ data: inputs.memory }}
onChange={handleMemoryChange}
canSetRoleName={isCompletionModel}
/>
</>

View File

@@ -308,11 +308,11 @@ const useConfig = (id: string, payload: LLMNodeType) => {
}, [setInputs, deleteNodeInspectorVars, id])
const filterInputVar = useCallback((varPayload: Var) => {
return [VarType.number, VarType.string, VarType.secret, VarType.arrayString, VarType.arrayNumber, VarType.file, VarType.arrayFile, VarType.memory].includes(varPayload.type)
return [VarType.number, VarType.string, VarType.secret, VarType.arrayString, VarType.arrayNumber, VarType.file, VarType.arrayFile].includes(varPayload.type)
}, [])
const filterJinja2InputVar = useCallback((varPayload: Var) => {
return [VarType.number, VarType.string, VarType.secret, VarType.arrayString, VarType.arrayNumber, VarType.arrayBoolean, VarType.arrayObject, VarType.object, VarType.array, VarType.boolean, VarType.memory].includes(varPayload.type)
return [VarType.number, VarType.string, VarType.secret, VarType.arrayString, VarType.arrayNumber, VarType.arrayBoolean, VarType.arrayObject, VarType.object, VarType.array, VarType.boolean].includes(varPayload.type)
}, [])
const filterMemoryPromptVar = useCallback((varPayload: Var) => {

View File

@@ -26,7 +26,7 @@ import {
arrayObjectPlaceholder,
arrayStringPlaceholder,
objectPlaceholder,
} from '@/app/components/workflow/panel/chat-variable-panel/constants'
} from '@/app/components/workflow/panel/chat-variable-panel/utils'
import ArrayBoolList from '@/app/components/workflow/panel/chat-variable-panel/components/array-bool-list'
type FormItemProps = {

View File

@@ -38,7 +38,7 @@ const useConfig = (id: string, payload: LoopNodeType) => {
const { nodesReadOnly: readOnly } = useNodesReadOnly()
const isChatMode = useIsChatMode()
const conversationVariables = useStore(s => s.conversationVariables)
const memoryVariables = useStore(s => s.memoryVariables)
const { inputs, setInputs } = useNodeCrud<LoopNodeType>(id, payload)
const inputsRef = useRef(inputs)
const handleInputsChange = useCallback((newInputs: LoopNodeType) => {
@@ -65,7 +65,7 @@ const useConfig = (id: string, payload: LoopNodeType) => {
mcpTools: mcpTools || [],
dataSourceList: dataSourceList || [],
}
const childrenNodeVars = toNodeOutputVars(loopChildrenNodes, isChatMode, undefined, [], conversationVariables, memoryVariables, [], allPluginInfoList)
const childrenNodeVars = toNodeOutputVars(loopChildrenNodes, isChatMode, undefined, [], conversationVariables, [], allPluginInfoList)
const {
getIsVarFileAttribute,

View File

@@ -11,7 +11,7 @@ const VALID_PARAMETER_TYPES: readonly VarType[] = [
] as const
// Type display name mappings
const TYPE_DISPLAY_NAMES: Record<Exclude<VarType, VarType.memory>, string> = {
const TYPE_DISPLAY_NAMES: Record<VarType, string> = {
[VarType.string]: 'String',
[VarType.number]: 'Number',
[VarType.boolean]: 'Boolean',
@@ -91,7 +91,7 @@ export const normalizeParameterType = (input: string | undefined | null): VarTyp
/**
* Gets display name for parameter types in UI components
*/
export const getParameterTypeDisplayName = (type: Exclude<VarType, VarType.memory>): string => {
export const getParameterTypeDisplayName = (type: VarType): string => {
return TYPE_DISPLAY_NAMES[type]
}
@@ -119,7 +119,7 @@ export const createParameterTypeOptions = (contentType?: string) => {
const availableTypes = getAvailableParameterTypes(contentType)
return availableTypes.map(type => ({
name: getParameterTypeDisplayName(type as Exclude<VarType, VarType.memory>),
name: getParameterTypeDisplayName(type),
value: type,
}))
}

View File

@@ -52,7 +52,7 @@ const ArrayValueList: FC<Props> = ({
<div className='flex items-center space-x-1' key={index}>
<Input
placeholder={t('workflow.chatVariable.modal.arrayValue') || ''}
value={list[index] || ''}
value={list[index]}
onChange={handleNameChange(index)}
type={isString ? 'text' : 'number'}
/>

View File

@@ -1,79 +0,0 @@
'use client'
import type { FC } from 'react'
import React, { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import {
RiArrowDownSLine,
RiCheckLine,
} from '@remixicon/react'
import { useShallow } from 'zustand/react/shallow'
import {
useStore,
} from 'reactflow'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import BlockIcon from '@/app/components/workflow/block-icon'
import { BlockEnum } from '@/app/components/workflow/types'
import cn from '@/utils/classnames'
type Props = {
value: string
onChange: (value: string) => void
nodeType?: BlockEnum
}
const NodeSelector: FC<Props> = ({
value,
onChange,
nodeType = BlockEnum.LLM,
}) => {
const { t } = useTranslation()
const [open, setOpen] = useState(false)
const filteredNodes = useStore(useShallow((s) => {
const nodes = [...s.nodeInternals.values()]
return nodes.filter(node => node.data?.type === nodeType)
}))
const currentNode = useMemo(() => filteredNodes.find(node => node.id === value), [filteredNodes, value])
return (
<PortalToFollowElem
open={open}
onOpenChange={setOpen}
placement='bottom-end'
offset={4}
>
<PortalToFollowElemTrigger onClick={() => setOpen(v => !v)}>
{currentNode && (
<div className={cn('flex h-8 w-[208px] cursor-pointer items-center rounded-lg bg-components-input-bg-normal px-2 py-1 pl-3 hover:bg-state-base-hover-alt', open && 'bg-state-base-hover-alt')}>
<BlockIcon className={cn('mr-1.5 h-4 w-4 shrink-0')} type={currentNode.data?.type} />
<div className='system-sm-regular grow truncate text-components-input-text-filled'>{currentNode.data?.title}</div>
</div>
)}
{!currentNode && (
<div className={cn('flex h-8 w-[208px] cursor-pointer items-center gap-1 rounded-lg bg-components-input-bg-normal px-2 py-1 pl-3 hover:bg-state-base-hover-alt', open && 'bg-state-base-hover-alt')}>
<div className='system-sm-regular grow truncate text-components-input-text-placeholder'>{t('workflow.chatVariable.modal.selectNode')}</div>
<RiArrowDownSLine className='h-4 w-4 text-text-quaternary' />
</div>
)}
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[25]'>
<div className='w-[209px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg p-1 shadow-lg'>
{filteredNodes.map(node => (
<div key={node.id} className='flex cursor-pointer items-center rounded-lg px-2 py-1 hover:bg-state-base-hover' onClick={() => onChange(node.id)}>
<BlockIcon className={cn('mr-2 shrink-0')} type={node.data?.type} />
<div className='system-sm-medium grow text-text-secondary'>{node.data?.title}</div>
{currentNode?.id === node.id && (
<RiCheckLine className='h-4 w-4 shrink-0 text-text-accent' />
)}
</div>
))}
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
)
}
export default React.memo(NodeSelector)

View File

@@ -1,54 +0,0 @@
import { memo } from 'react'
import { useStore } from 'reactflow'
import VariableItem from './variable-item'
import BlockIcon from '@/app/components/workflow/block-icon'
import { BlockEnum } from '@/app/components/workflow/types'
import type { MemoryVariable } from '@/app/components/workflow/types'
type VariableItemWithNodeProps = {
nodeId: string
memoryVariables: MemoryVariable[]
onEdit: (memoryVariable: MemoryVariable) => void
onDelete: (memoryVariable: MemoryVariable) => void
currentVarId?: string
}
const VariableItemWithNode = ({
nodeId,
memoryVariables,
onEdit,
onDelete,
currentVarId,
}: VariableItemWithNodeProps) => {
const currentNode = useStore(s => s.nodeInternals.get(nodeId))
if (!currentNode) return null
return (
<div className='space-y-1 py-1'>
<div className='mb-1 flex items-center'>
<BlockIcon className='mr-1.5 shrink-0' type={BlockEnum.LLM} />
<div
className='system-sm-medium grow truncate text-text-secondary'
title={currentNode?.data.title}
>
{currentNode?.data.title}
</div>
</div>
{
memoryVariables.map(memoryVariable => (
<VariableItem
key={memoryVariable.id}
item={memoryVariable}
onEdit={onEdit}
onDelete={onDelete}
scope='node'
term={memoryVariable.term}
currentVarId={currentVarId}
/>
))
}
</div>
)
}
export default memo(VariableItemWithNode)

View File

@@ -1,87 +1,47 @@
import { memo, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { memo, useState } from 'react'
import { capitalize } from 'lodash-es'
import { RiDeleteBinLine, RiEditLine } from '@remixicon/react'
import {
BubbleX,
Memory,
} from '@/app/components/base/icons/src/vender/line/others'
import type { ConversationVariable, MemoryVariable } from '@/app/components/workflow/types'
import { BubbleX } from '@/app/components/base/icons/src/vender/line/others'
import type { ConversationVariable } from '@/app/components/workflow/types'
import cn from '@/utils/classnames'
import { ChatVarType } from '../type'
import Badge from '@/app/components/base/badge'
type VariableItemProps = {
item: ConversationVariable | MemoryVariable
onEdit: (item: ConversationVariable | MemoryVariable) => void
onDelete: (item: ConversationVariable | MemoryVariable) => void
scope?: string
term?: string
currentVarId?: string
item: ConversationVariable
onEdit: (item: ConversationVariable) => void
onDelete: (item: ConversationVariable) => void
}
const VariableItem = ({
item,
onEdit,
onDelete,
scope,
term,
currentVarId,
}: VariableItemProps) => {
const { t } = useTranslation()
const [destructive, setDestructive] = useState(false)
const valueType = useMemo(() => {
if (item.value_type === ChatVarType.Memory)
return 'memory'
return item.value_type
}, [item.value_type])
return (
<div className={cn(
'radius-md mb-1 border border-components-panel-border-subtle bg-components-panel-on-panel-item-bg px-2.5 py-2 shadow-xs hover:bg-components-panel-on-panel-item-bg-hover',
destructive && 'border-state-destructive-border hover:bg-state-destructive-hover',
)}>
<div className='group flex items-center justify-between'>
<div className='flex items-center justify-between'>
<div className='flex grow items-center gap-1'>
{
item.value_type === ChatVarType.Memory && (
<Memory className='h-4 w-4 text-util-colors-teal-teal-700' />
)
}
{
item.value_type !== ChatVarType.Memory && (
<BubbleX className='h-4 w-4 text-util-colors-teal-teal-700' />
)
}
<BubbleX className='h-4 w-4 text-util-colors-teal-teal-700' />
<div className='system-sm-medium text-text-primary'>{item.name}</div>
<div className='system-xs-medium text-text-tertiary'>{capitalize(valueType)}</div>
<div className='system-xs-medium text-text-tertiary'>{capitalize(item.value_type)}</div>
</div>
<div className='flex shrink-0 items-center gap-1 text-text-tertiary'>
<div className={cn('radius-md hidden cursor-pointer p-1 hover:bg-state-base-hover hover:text-text-secondary group-hover:block', currentVarId === item.id && 'block bg-state-base-hover text-text-secondary')}>
<div className='radius-md cursor-pointer p-1 hover:bg-state-base-hover hover:text-text-secondary'>
<RiEditLine className='h-4 w-4' onClick={() => onEdit(item)}/>
</div>
<div
className={cn('radius-md hidden cursor-pointer p-1 hover:bg-state-destructive-hover hover:text-text-destructive group-hover:block', currentVarId === item.id && 'block')}
className='radius-md cursor-pointer p-1 hover:bg-state-destructive-hover hover:text-text-destructive'
onMouseOver={() => setDestructive(true)}
onMouseOut={() => setDestructive(false)}
>
<RiDeleteBinLine className='h-4 w-4' onClick={() => onDelete(item)}/>
</div>
<div className={cn('flex h-6 items-center gap-0.5 group-hover:hidden', currentVarId === item.id && 'hidden')}>
{scope === 'app' && <Badge text={'conv'} />}
{term && <Badge text={term} />}
</div>
</div>
</div>
{
'description' in item && item.description && (
<div className='system-xs-regular truncate text-text-tertiary'>{item.description}</div>
)
}
{
scope === 'app' && (
<div className='system-xs-regular truncate text-text-tertiary'>{t('workflow.chatVariable.appScopeText')}</div>
)
}
<div className='system-xs-regular truncate text-text-tertiary'>{item.description}</div>
</div>
)
}

View File

@@ -9,15 +9,15 @@ import {
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import type { ConversationVariable, MemoryVariable } from '@/app/components/workflow/types'
import type { ConversationVariable } from '@/app/components/workflow/types'
type Props = {
open: boolean
setOpen: (value: React.SetStateAction<boolean>) => void
showTip: boolean
chatVar?: ConversationVariable | MemoryVariable
chatVar?: ConversationVariable
onClose: () => void
onSave: (env: ConversationVariable | MemoryVariable) => void
onSave: (env: ConversationVariable) => void
}
const VariableModalTrigger = ({

Some files were not shown because too many files have changed in this diff Show More