mirror of
https://github.com/langgenius/dify.git
synced 2026-05-31 22:26:19 +08:00
Compare commits
69 Commits
deploy/tri
...
feat/memor
| Author | SHA1 | Date | |
|---|---|---|---|
| 4e037d14d1 | |||
| 08ca4e3e8b | |||
| 0998fab282 | |||
| eb536956ef | |||
| 338e10424c | |||
| c2ce2be102 | |||
| b7311bf1d3 | |||
| 4050ed8c8e | |||
| 2f0076166a | |||
| 896a29b836 | |||
| 7cef0fff89 | |||
| 9d310fed92 | |||
| 2944f831e2 | |||
| 10f5270320 | |||
| 38d27100ac | |||
| 9efbfdda55 | |||
| 7e2d16a518 | |||
| 6b2d460023 | |||
| 1cf7007fb4 | |||
| 828b9c6a93 | |||
| c8188274a2 | |||
| d1d419e2b0 | |||
| e90086c2d2 | |||
| 1c3ff179f8 | |||
| 8a348615bf | |||
| 2a27d81553 | |||
| 6b6ab5e034 | |||
| 50a7d93ddc | |||
| 61e4bc6b17 | |||
| 0e545d7be5 | |||
| eddc39cd0a | |||
| efcaa2bbbd | |||
| 05c05bb6d0 | |||
| 57624979e4 | |||
| 7e9375ce7e | |||
| 0b1445aed5 | |||
| f6623423dd | |||
| 77b7bf9d47 | |||
| b0dd7d303d | |||
| f8097b20c8 | |||
| 257e43e2c2 | |||
| 55f146f527 | |||
| 2cd9d1066f | |||
| 612112c919 | |||
| df502e0d79 | |||
| 27d6fee1ed | |||
| 35c116906e | |||
| e9685a38cb | |||
| a7156244f7 | |||
| 01f0ee339e | |||
| 158da1ce6e | |||
| 761ad336e9 | |||
| 239e15eac6 | |||
| 2acee44071 | |||
| 944b2ff22d | |||
| a5dfc09e90 | |||
| 8266dc1dcc | |||
| edeea0a8b9 | |||
| dfc17f3b08 | |||
| bca0b7c087 | |||
| 985becbc41 | |||
| c90ab89323 | |||
| 8547d4b1ff | |||
| fbcfebbba1 | |||
| f827f8dc63 | |||
| fef9907af3 | |||
| e3b7f8afdd | |||
| dceac39078 | |||
| 58fd2f7a2f |
@ -1004,11 +1004,6 @@ class RagPipelineRecommendedPluginApi(Resource):
|
|||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
def get(self):
|
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()
|
rag_pipeline_service = RagPipelineService()
|
||||||
recommended_plugins = rag_pipeline_service.get_recommended_plugins(type)
|
recommended_plugins = rag_pipeline_service.get_recommended_plugins()
|
||||||
return recommended_plugins
|
return recommended_plugins
|
||||||
|
|||||||
@ -1,64 +0,0 @@
|
|||||||
"""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 ###
|
|
||||||
@ -1458,7 +1458,6 @@ class PipelineRecommendedPlugin(TypeBase):
|
|||||||
)
|
)
|
||||||
plugin_id: Mapped[str] = mapped_column(LongText, nullable=False)
|
plugin_id: Mapped[str] = mapped_column(LongText, nullable=False)
|
||||||
provider_name: 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)
|
position: Mapped[int] = mapped_column(sa.Integer, nullable=False, default=0)
|
||||||
active: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, default=True)
|
active: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, default=True)
|
||||||
created_at: Mapped[datetime] = mapped_column(
|
created_at: Mapped[datetime] = mapped_column(
|
||||||
|
|||||||
@ -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:
|
def process_bind_param(self, value: uuid.UUID | str | None, dialect: Dialect) -> str | None:
|
||||||
if value is None:
|
if value is None:
|
||||||
return value
|
return value
|
||||||
elif dialect.name in ["postgresql", "mysql"]:
|
elif dialect.name == "postgresql":
|
||||||
return str(value)
|
return str(value)
|
||||||
else:
|
else:
|
||||||
if isinstance(value, uuid.UUID):
|
if isinstance(value, uuid.UUID):
|
||||||
|
|||||||
@ -907,29 +907,19 @@ class WorkflowNodeExecutionModel(Base): # This model is expected to have `offlo
|
|||||||
@property
|
@property
|
||||||
def extras(self) -> dict[str, Any]:
|
def extras(self) -> dict[str, Any]:
|
||||||
from core.tools.tool_manager import ToolManager
|
from core.tools.tool_manager import ToolManager
|
||||||
from core.trigger.trigger_manager import TriggerManager
|
|
||||||
|
|
||||||
extras: dict[str, Any] = {}
|
extras: dict[str, Any] = {}
|
||||||
execution_metadata = self.execution_metadata_dict
|
if self.execution_metadata_dict:
|
||||||
if execution_metadata:
|
if self.node_type == NodeType.TOOL and "tool_info" in self.execution_metadata_dict:
|
||||||
if self.node_type == NodeType.TOOL and "tool_info" in execution_metadata:
|
tool_info: dict[str, Any] = self.execution_metadata_dict["tool_info"]
|
||||||
tool_info: dict[str, Any] = execution_metadata["tool_info"]
|
|
||||||
extras["icon"] = ToolManager.get_tool_icon(
|
extras["icon"] = ToolManager.get_tool_icon(
|
||||||
tenant_id=self.tenant_id,
|
tenant_id=self.tenant_id,
|
||||||
provider_type=tool_info["provider_type"],
|
provider_type=tool_info["provider_type"],
|
||||||
provider_id=tool_info["provider_id"],
|
provider_id=tool_info["provider_id"],
|
||||||
)
|
)
|
||||||
elif self.node_type == NodeType.DATASOURCE and "datasource_info" in execution_metadata:
|
elif self.node_type == NodeType.DATASOURCE and "datasource_info" in self.execution_metadata_dict:
|
||||||
datasource_info = execution_metadata["datasource_info"]
|
datasource_info = self.execution_metadata_dict["datasource_info"]
|
||||||
extras["icon"] = datasource_info.get("icon")
|
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
|
return extras
|
||||||
|
|
||||||
def _get_offload_by_type(self, type_: ExecutionOffLoadType) -> Optional["WorkflowNodeExecutionOffload"]:
|
def _get_offload_by_type(self, type_: ExecutionOffLoadType) -> Optional["WorkflowNodeExecutionOffload"]:
|
||||||
|
|||||||
@ -1248,14 +1248,12 @@ class RagPipelineService:
|
|||||||
session.commit()
|
session.commit()
|
||||||
return workflow_node_execution_db_model
|
return workflow_node_execution_db_model
|
||||||
|
|
||||||
def get_recommended_plugins(self, type: str) -> dict:
|
def get_recommended_plugins(self) -> dict:
|
||||||
# Query active recommended plugins
|
# 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 = (
|
pipeline_recommended_plugins = (
|
||||||
query.order_by(PipelineRecommendedPlugin.position.asc())
|
db.session.query(PipelineRecommendedPlugin)
|
||||||
|
.where(PipelineRecommendedPlugin.active == True)
|
||||||
|
.order_by(PipelineRecommendedPlugin.position.asc())
|
||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
import useSWR from 'swr'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import {
|
import {
|
||||||
RiGraduationCapFill,
|
RiGraduationCapFill,
|
||||||
@ -22,9 +23,8 @@ import PremiumBadge from '@/app/components/base/premium-badge'
|
|||||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||||
import EmailChangeModal from './email-change-modal'
|
import EmailChangeModal from './email-change-modal'
|
||||||
import { validPassword } from '@/config'
|
import { validPassword } from '@/config'
|
||||||
|
import { fetchAppList } from '@/service/apps'
|
||||||
import type { App } from '@/types/app'
|
import type { App } from '@/types/app'
|
||||||
import { useAppList } from '@/service/use-apps'
|
|
||||||
|
|
||||||
const titleClassName = `
|
const titleClassName = `
|
||||||
system-sm-semibold text-text-secondary
|
system-sm-semibold text-text-secondary
|
||||||
@ -36,7 +36,7 @@ const descriptionClassName = `
|
|||||||
export default function AccountPage() {
|
export default function AccountPage() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { systemFeatures } = useGlobalPublicStore()
|
const { systemFeatures } = useGlobalPublicStore()
|
||||||
const { data: appList } = useAppList({ page: 1, limit: 100, name: '' })
|
const { data: appList } = useSWR({ url: '/apps', params: { page: 1, limit: 100, name: '' } }, fetchAppList)
|
||||||
const apps = appList?.data || []
|
const apps = appList?.data || []
|
||||||
const { mutateUserProfile, userProfile } = useAppContext()
|
const { mutateUserProfile, userProfile } = useAppContext()
|
||||||
const { isEducationAccount } = useProviderContext()
|
const { isEducationAccount } = useProviderContext()
|
||||||
|
|||||||
@ -52,6 +52,7 @@ export type IGetAutomaticResProps = {
|
|||||||
editorId?: string
|
editorId?: string
|
||||||
currentPrompt?: string
|
currentPrompt?: string
|
||||||
isBasicMode?: boolean
|
isBasicMode?: boolean
|
||||||
|
hideTryIt?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const TryLabel: FC<{
|
const TryLabel: FC<{
|
||||||
@ -80,6 +81,7 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
|
|||||||
currentPrompt,
|
currentPrompt,
|
||||||
isBasicMode,
|
isBasicMode,
|
||||||
onFinished,
|
onFinished,
|
||||||
|
hideTryIt,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const localModel = localStorage.getItem('auto-gen-model')
|
const localModel = localStorage.getItem('auto-gen-model')
|
||||||
@ -305,7 +307,7 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
|
|||||||
hideDebugWithMultipleModel
|
hideDebugWithMultipleModel
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{isBasicMode && (
|
{isBasicMode && !hideTryIt && (
|
||||||
<div className='mt-4'>
|
<div className='mt-4'>
|
||||||
<div className='flex items-center'>
|
<div className='flex items-center'>
|
||||||
<div className='mr-3 shrink-0 text-xs font-semibold uppercase leading-[18px] text-text-tertiary'>{t('appDebug.generate.tryIt')}</div>
|
<div className='mr-3 shrink-0 text-xs font-semibold uppercase leading-[18px] text-text-tertiary'>{t('appDebug.generate.tryIt')}</div>
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import type { FC } from 'react'
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import ReactECharts from 'echarts-for-react'
|
import ReactECharts from 'echarts-for-react'
|
||||||
import type { EChartsOption } from 'echarts'
|
import type { EChartsOption } from 'echarts'
|
||||||
|
import useSWR from 'swr'
|
||||||
import type { Dayjs } from 'dayjs'
|
import type { Dayjs } from 'dayjs'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { get } from 'lodash-es'
|
import { get } from 'lodash-es'
|
||||||
@ -12,20 +13,7 @@ import { formatNumber } from '@/utils/format'
|
|||||||
import Basic from '@/app/components/app-sidebar/basic'
|
import Basic from '@/app/components/app-sidebar/basic'
|
||||||
import Loading from '@/app/components/base/loading'
|
import Loading from '@/app/components/base/loading'
|
||||||
import type { AppDailyConversationsResponse, AppDailyEndUsersResponse, AppDailyMessagesResponse, AppTokenCostsResponse } from '@/models/app'
|
import type { AppDailyConversationsResponse, AppDailyEndUsersResponse, AppDailyMessagesResponse, AppTokenCostsResponse } from '@/models/app'
|
||||||
import {
|
import { getAppDailyConversations, getAppDailyEndUsers, getAppDailyMessages, getAppStatistics, getAppTokenCosts, getWorkflowDailyConversations } from '@/service/apps'
|
||||||
useAppAverageResponseTime,
|
|
||||||
useAppAverageSessionInteractions,
|
|
||||||
useAppDailyConversations,
|
|
||||||
useAppDailyEndUsers,
|
|
||||||
useAppDailyMessages,
|
|
||||||
useAppSatisfactionRate,
|
|
||||||
useAppTokenCosts,
|
|
||||||
useAppTokensPerSecond,
|
|
||||||
useWorkflowAverageInteractions,
|
|
||||||
useWorkflowDailyConversations,
|
|
||||||
useWorkflowDailyTerminals,
|
|
||||||
useWorkflowTokenCosts,
|
|
||||||
} from '@/service/use-apps'
|
|
||||||
const valueFormatter = (v: string | number) => v
|
const valueFormatter = (v: string | number) => v
|
||||||
|
|
||||||
const COLOR_TYPE_MAP = {
|
const COLOR_TYPE_MAP = {
|
||||||
@ -284,8 +272,8 @@ const getDefaultChartData = ({ start, end, key = 'count' }: { start: string; end
|
|||||||
|
|
||||||
export const MessagesChart: FC<IBizChartProps> = ({ id, period }) => {
|
export const MessagesChart: FC<IBizChartProps> = ({ id, period }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { data: response, isLoading } = useAppDailyMessages(id, period.query)
|
const { data: response } = useSWR({ url: `/apps/${id}/statistics/daily-messages`, params: period.query }, getAppDailyMessages)
|
||||||
if (isLoading || !response)
|
if (!response)
|
||||||
return <Loading />
|
return <Loading />
|
||||||
const noDataFlag = !response.data || response.data.length === 0
|
const noDataFlag = !response.data || response.data.length === 0
|
||||||
return <Chart
|
return <Chart
|
||||||
@ -298,8 +286,8 @@ export const MessagesChart: FC<IBizChartProps> = ({ id, period }) => {
|
|||||||
|
|
||||||
export const ConversationsChart: FC<IBizChartProps> = ({ id, period }) => {
|
export const ConversationsChart: FC<IBizChartProps> = ({ id, period }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { data: response, isLoading } = useAppDailyConversations(id, period.query)
|
const { data: response } = useSWR({ url: `/apps/${id}/statistics/daily-conversations`, params: period.query }, getAppDailyConversations)
|
||||||
if (isLoading || !response)
|
if (!response)
|
||||||
return <Loading />
|
return <Loading />
|
||||||
const noDataFlag = !response.data || response.data.length === 0
|
const noDataFlag = !response.data || response.data.length === 0
|
||||||
return <Chart
|
return <Chart
|
||||||
@ -313,8 +301,8 @@ export const ConversationsChart: FC<IBizChartProps> = ({ id, period }) => {
|
|||||||
export const EndUsersChart: FC<IBizChartProps> = ({ id, period }) => {
|
export const EndUsersChart: FC<IBizChartProps> = ({ id, period }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const { data: response, isLoading } = useAppDailyEndUsers(id, period.query)
|
const { data: response } = useSWR({ url: `/apps/${id}/statistics/daily-end-users`, id, params: period.query }, getAppDailyEndUsers)
|
||||||
if (isLoading || !response)
|
if (!response)
|
||||||
return <Loading />
|
return <Loading />
|
||||||
const noDataFlag = !response.data || response.data.length === 0
|
const noDataFlag = !response.data || response.data.length === 0
|
||||||
return <Chart
|
return <Chart
|
||||||
@ -327,8 +315,8 @@ export const EndUsersChart: FC<IBizChartProps> = ({ id, period }) => {
|
|||||||
|
|
||||||
export const AvgSessionInteractions: FC<IBizChartProps> = ({ id, period }) => {
|
export const AvgSessionInteractions: FC<IBizChartProps> = ({ id, period }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { data: response, isLoading } = useAppAverageSessionInteractions(id, period.query)
|
const { data: response } = useSWR({ url: `/apps/${id}/statistics/average-session-interactions`, params: period.query }, getAppStatistics)
|
||||||
if (isLoading || !response)
|
if (!response)
|
||||||
return <Loading />
|
return <Loading />
|
||||||
const noDataFlag = !response.data || response.data.length === 0
|
const noDataFlag = !response.data || response.data.length === 0
|
||||||
return <Chart
|
return <Chart
|
||||||
@ -343,8 +331,8 @@ export const AvgSessionInteractions: FC<IBizChartProps> = ({ id, period }) => {
|
|||||||
|
|
||||||
export const AvgResponseTime: FC<IBizChartProps> = ({ id, period }) => {
|
export const AvgResponseTime: FC<IBizChartProps> = ({ id, period }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { data: response, isLoading } = useAppAverageResponseTime(id, period.query)
|
const { data: response } = useSWR({ url: `/apps/${id}/statistics/average-response-time`, params: period.query }, getAppStatistics)
|
||||||
if (isLoading || !response)
|
if (!response)
|
||||||
return <Loading />
|
return <Loading />
|
||||||
const noDataFlag = !response.data || response.data.length === 0
|
const noDataFlag = !response.data || response.data.length === 0
|
||||||
return <Chart
|
return <Chart
|
||||||
@ -360,8 +348,8 @@ export const AvgResponseTime: FC<IBizChartProps> = ({ id, period }) => {
|
|||||||
|
|
||||||
export const TokenPerSecond: FC<IBizChartProps> = ({ id, period }) => {
|
export const TokenPerSecond: FC<IBizChartProps> = ({ id, period }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { data: response, isLoading } = useAppTokensPerSecond(id, period.query)
|
const { data: response } = useSWR({ url: `/apps/${id}/statistics/tokens-per-second`, params: period.query }, getAppStatistics)
|
||||||
if (isLoading || !response)
|
if (!response)
|
||||||
return <Loading />
|
return <Loading />
|
||||||
const noDataFlag = !response.data || response.data.length === 0
|
const noDataFlag = !response.data || response.data.length === 0
|
||||||
return <Chart
|
return <Chart
|
||||||
@ -378,8 +366,8 @@ export const TokenPerSecond: FC<IBizChartProps> = ({ id, period }) => {
|
|||||||
|
|
||||||
export const UserSatisfactionRate: FC<IBizChartProps> = ({ id, period }) => {
|
export const UserSatisfactionRate: FC<IBizChartProps> = ({ id, period }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { data: response, isLoading } = useAppSatisfactionRate(id, period.query)
|
const { data: response } = useSWR({ url: `/apps/${id}/statistics/user-satisfaction-rate`, params: period.query }, getAppStatistics)
|
||||||
if (isLoading || !response)
|
if (!response)
|
||||||
return <Loading />
|
return <Loading />
|
||||||
const noDataFlag = !response.data || response.data.length === 0
|
const noDataFlag = !response.data || response.data.length === 0
|
||||||
return <Chart
|
return <Chart
|
||||||
@ -396,8 +384,8 @@ export const UserSatisfactionRate: FC<IBizChartProps> = ({ id, period }) => {
|
|||||||
export const CostChart: FC<IBizChartProps> = ({ id, period }) => {
|
export const CostChart: FC<IBizChartProps> = ({ id, period }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const { data: response, isLoading } = useAppTokenCosts(id, period.query)
|
const { data: response } = useSWR({ url: `/apps/${id}/statistics/token-costs`, params: period.query }, getAppTokenCosts)
|
||||||
if (isLoading || !response)
|
if (!response)
|
||||||
return <Loading />
|
return <Loading />
|
||||||
const noDataFlag = !response.data || response.data.length === 0
|
const noDataFlag = !response.data || response.data.length === 0
|
||||||
return <Chart
|
return <Chart
|
||||||
@ -410,8 +398,8 @@ export const CostChart: FC<IBizChartProps> = ({ id, period }) => {
|
|||||||
|
|
||||||
export const WorkflowMessagesChart: FC<IBizChartProps> = ({ id, period }) => {
|
export const WorkflowMessagesChart: FC<IBizChartProps> = ({ id, period }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { data: response, isLoading } = useWorkflowDailyConversations(id, period.query)
|
const { data: response } = useSWR({ url: `/apps/${id}/workflow/statistics/daily-conversations`, params: period.query }, getWorkflowDailyConversations)
|
||||||
if (isLoading || !response)
|
if (!response)
|
||||||
return <Loading />
|
return <Loading />
|
||||||
const noDataFlag = !response.data || response.data.length === 0
|
const noDataFlag = !response.data || response.data.length === 0
|
||||||
return <Chart
|
return <Chart
|
||||||
@ -426,8 +414,8 @@ export const WorkflowMessagesChart: FC<IBizChartProps> = ({ id, period }) => {
|
|||||||
export const WorkflowDailyTerminalsChart: FC<IBizChartProps> = ({ id, period }) => {
|
export const WorkflowDailyTerminalsChart: FC<IBizChartProps> = ({ id, period }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const { data: response, isLoading } = useWorkflowDailyTerminals(id, period.query)
|
const { data: response } = useSWR({ url: `/apps/${id}/workflow/statistics/daily-terminals`, id, params: period.query }, getAppDailyEndUsers)
|
||||||
if (isLoading || !response)
|
if (!response)
|
||||||
return <Loading />
|
return <Loading />
|
||||||
const noDataFlag = !response.data || response.data.length === 0
|
const noDataFlag = !response.data || response.data.length === 0
|
||||||
return <Chart
|
return <Chart
|
||||||
@ -441,8 +429,8 @@ export const WorkflowDailyTerminalsChart: FC<IBizChartProps> = ({ id, period })
|
|||||||
export const WorkflowCostChart: FC<IBizChartProps> = ({ id, period }) => {
|
export const WorkflowCostChart: FC<IBizChartProps> = ({ id, period }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const { data: response, isLoading } = useWorkflowTokenCosts(id, period.query)
|
const { data: response } = useSWR({ url: `/apps/${id}/workflow/statistics/token-costs`, params: period.query }, getAppTokenCosts)
|
||||||
if (isLoading || !response)
|
if (!response)
|
||||||
return <Loading />
|
return <Loading />
|
||||||
const noDataFlag = !response.data || response.data.length === 0
|
const noDataFlag = !response.data || response.data.length === 0
|
||||||
return <Chart
|
return <Chart
|
||||||
@ -455,8 +443,8 @@ export const WorkflowCostChart: FC<IBizChartProps> = ({ id, period }) => {
|
|||||||
|
|
||||||
export const AvgUserInteractions: FC<IBizChartProps> = ({ id, period }) => {
|
export const AvgUserInteractions: FC<IBizChartProps> = ({ id, period }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { data: response, isLoading } = useWorkflowAverageInteractions(id, period.query)
|
const { data: response } = useSWR({ url: `/apps/${id}/workflow/statistics/average-app-interactions`, params: period.query }, getAppStatistics)
|
||||||
if (isLoading || !response)
|
if (!response)
|
||||||
return <Loading />
|
return <Loading />
|
||||||
const noDataFlag = !response.data || response.data.length === 0
|
const noDataFlag = !response.data || response.data.length === 0
|
||||||
return <Chart
|
return <Chart
|
||||||
|
|||||||
@ -23,7 +23,7 @@ const Empty = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DefaultCards />
|
<DefaultCards />
|
||||||
<div className='pointer-events-none absolute inset-0 z-20 flex items-center justify-center bg-gradient-to-t from-background-body to-transparent'>
|
<div className='absolute inset-0 z-20 flex items-center justify-center bg-gradient-to-t from-background-body to-transparent pointer-events-none'>
|
||||||
<span className='system-md-medium text-text-tertiary'>
|
<span className='system-md-medium text-text-tertiary'>
|
||||||
{t('app.newApp.noAppsFound')}
|
{t('app.newApp.noAppsFound')}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { useCallback, useEffect, useRef, useState } from 'react'
|
|||||||
import {
|
import {
|
||||||
useRouter,
|
useRouter,
|
||||||
} from 'next/navigation'
|
} from 'next/navigation'
|
||||||
|
import useSWRInfinite from 'swr/infinite'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useDebounceFn } from 'ahooks'
|
import { useDebounceFn } from 'ahooks'
|
||||||
import {
|
import {
|
||||||
@ -18,6 +19,8 @@ import AppCard from './app-card'
|
|||||||
import NewAppCard from './new-app-card'
|
import NewAppCard from './new-app-card'
|
||||||
import useAppsQueryState from './hooks/use-apps-query-state'
|
import useAppsQueryState from './hooks/use-apps-query-state'
|
||||||
import { useDSLDragDrop } from './hooks/use-dsl-drag-drop'
|
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 { useAppContext } from '@/context/app-context'
|
||||||
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
||||||
import { CheckModal } from '@/hooks/use-pay'
|
import { CheckModal } from '@/hooks/use-pay'
|
||||||
@ -32,7 +35,6 @@ import Empty from './empty'
|
|||||||
import Footer from './footer'
|
import Footer from './footer'
|
||||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||||
import { AppModeEnum } from '@/types/app'
|
import { AppModeEnum } from '@/types/app'
|
||||||
import { useInfiniteAppList } from '@/service/use-apps'
|
|
||||||
|
|
||||||
const TagManagementModal = dynamic(() => import('@/app/components/base/tag-management'), {
|
const TagManagementModal = dynamic(() => import('@/app/components/base/tag-management'), {
|
||||||
ssr: false,
|
ssr: false,
|
||||||
@ -41,6 +43,30 @@ const CreateFromDSLModal = dynamic(() => import('@/app/components/app/create-fro
|
|||||||
ssr: false,
|
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 List = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { systemFeatures } = useGlobalPublicStore()
|
const { systemFeatures } = useGlobalPublicStore()
|
||||||
@ -76,24 +102,16 @@ const List = () => {
|
|||||||
enabled: isCurrentWorkspaceEditor,
|
enabled: isCurrentWorkspaceEditor,
|
||||||
})
|
})
|
||||||
|
|
||||||
const appListQueryParams = {
|
const { data, isLoading, error, setSize, mutate } = useSWRInfinite(
|
||||||
page: 1,
|
(pageIndex: number, previousPageData: AppListResponse) => getKey(pageIndex, previousPageData, activeTab, isCreatedByMe, tagIDs, searchKeywords),
|
||||||
limit: 30,
|
fetchAppList,
|
||||||
name: searchKeywords,
|
{
|
||||||
tag_ids: tagIDs,
|
revalidateFirstPage: true,
|
||||||
is_created_by_me: isCreatedByMe,
|
shouldRetryOnError: false,
|
||||||
...(activeTab !== 'all' ? { mode: activeTab as AppModeEnum } : {}),
|
dedupingInterval: 500,
|
||||||
}
|
errorRetryCount: 3,
|
||||||
|
},
|
||||||
const {
|
)
|
||||||
data,
|
|
||||||
isLoading,
|
|
||||||
isFetchingNextPage,
|
|
||||||
fetchNextPage,
|
|
||||||
hasNextPage,
|
|
||||||
error,
|
|
||||||
refetch,
|
|
||||||
} = useInfiniteAppList(appListQueryParams, { enabled: !isCurrentWorkspaceDatasetOperator })
|
|
||||||
|
|
||||||
const anchorRef = useRef<HTMLDivElement>(null)
|
const anchorRef = useRef<HTMLDivElement>(null)
|
||||||
const options = [
|
const options = [
|
||||||
@ -108,9 +126,9 @@ const List = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (localStorage.getItem(NEED_REFRESH_APP_LIST_KEY) === '1') {
|
if (localStorage.getItem(NEED_REFRESH_APP_LIST_KEY) === '1') {
|
||||||
localStorage.removeItem(NEED_REFRESH_APP_LIST_KEY)
|
localStorage.removeItem(NEED_REFRESH_APP_LIST_KEY)
|
||||||
refetch()
|
mutate()
|
||||||
}
|
}
|
||||||
}, [refetch])
|
}, [mutate, t])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isCurrentWorkspaceDatasetOperator)
|
if (isCurrentWorkspaceDatasetOperator)
|
||||||
@ -118,9 +136,7 @@ const List = () => {
|
|||||||
}, [router, isCurrentWorkspaceDatasetOperator])
|
}, [router, isCurrentWorkspaceDatasetOperator])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isCurrentWorkspaceDatasetOperator)
|
const hasMore = data?.at(-1)?.has_more ?? true
|
||||||
return
|
|
||||||
const hasMore = hasNextPage ?? true
|
|
||||||
let observer: IntersectionObserver | undefined
|
let observer: IntersectionObserver | undefined
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
@ -135,8 +151,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
|
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) => {
|
observer = new IntersectionObserver((entries) => {
|
||||||
if (entries[0].isIntersecting && !isLoading && !isFetchingNextPage && !error && hasMore)
|
if (entries[0].isIntersecting && !isLoading && !error && hasMore)
|
||||||
fetchNextPage()
|
setSize((size: number) => size + 1)
|
||||||
}, {
|
}, {
|
||||||
root: containerRef.current,
|
root: containerRef.current,
|
||||||
rootMargin: `${dynamicMargin}px`,
|
rootMargin: `${dynamicMargin}px`,
|
||||||
@ -145,7 +161,7 @@ const List = () => {
|
|||||||
observer.observe(anchorRef.current)
|
observer.observe(anchorRef.current)
|
||||||
}
|
}
|
||||||
return () => observer?.disconnect()
|
return () => observer?.disconnect()
|
||||||
}, [isLoading, isFetchingNextPage, fetchNextPage, error, hasNextPage, isCurrentWorkspaceDatasetOperator])
|
}, [isLoading, setSize, data, error])
|
||||||
|
|
||||||
const { run: handleSearch } = useDebounceFn(() => {
|
const { run: handleSearch } = useDebounceFn(() => {
|
||||||
setSearchKeywords(keywords)
|
setSearchKeywords(keywords)
|
||||||
@ -169,9 +185,6 @@ const List = () => {
|
|||||||
setQuery(prev => ({ ...prev, isCreatedByMe: newValue }))
|
setQuery(prev => ({ ...prev, isCreatedByMe: newValue }))
|
||||||
}, [isCreatedByMe, setQuery])
|
}, [isCreatedByMe, setQuery])
|
||||||
|
|
||||||
const pages = data?.pages ?? []
|
|
||||||
const hasAnyApp = (pages[0]?.total ?? 0) > 0
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div ref={containerRef} className='relative flex h-0 shrink-0 grow flex-col overflow-y-auto bg-background-body'>
|
<div ref={containerRef} className='relative flex h-0 shrink-0 grow flex-col overflow-y-auto bg-background-body'>
|
||||||
@ -204,17 +217,17 @@ const List = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{hasAnyApp
|
{(data && data[0].total > 0)
|
||||||
? <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'>
|
? <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
|
{isCurrentWorkspaceEditor
|
||||||
&& <NewAppCard ref={newAppCardRef} onSuccess={refetch} selectedAppType={activeTab} />}
|
&& <NewAppCard ref={newAppCardRef} onSuccess={mutate} selectedAppType={activeTab} />}
|
||||||
{pages.map(({ data: apps }) => apps.map(app => (
|
{data.map(({ data: apps }) => apps.map(app => (
|
||||||
<AppCard key={app.id} app={app} onRefresh={refetch} />
|
<AppCard key={app.id} app={app} onRefresh={mutate} />
|
||||||
)))}
|
)))}
|
||||||
</div>
|
</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'>
|
: <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
|
{isCurrentWorkspaceEditor
|
||||||
&& <NewAppCard ref={newAppCardRef} className='z-10' onSuccess={refetch} selectedAppType={activeTab} />}
|
&& <NewAppCard ref={newAppCardRef} className='z-10' onSuccess={mutate} selectedAppType={activeTab} />}
|
||||||
<Empty />
|
<Empty />
|
||||||
</div>}
|
</div>}
|
||||||
|
|
||||||
@ -248,7 +261,7 @@ const List = () => {
|
|||||||
onSuccess={() => {
|
onSuccess={() => {
|
||||||
setShowCreateFromDSLModal(false)
|
setShowCreateFromDSLModal(false)
|
||||||
setDroppedDSLFile(undefined)
|
setDroppedDSLFile(undefined)
|
||||||
refetch()
|
mutate()
|
||||||
}}
|
}}
|
||||||
droppedFile={droppedDSLFile}
|
droppedFile={droppedDSLFile}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import type {
|
|||||||
ChatConfig,
|
ChatConfig,
|
||||||
ChatItemInTree,
|
ChatItemInTree,
|
||||||
Feedback,
|
Feedback,
|
||||||
|
Memory,
|
||||||
} from '../types'
|
} from '../types'
|
||||||
import type { ThemeBuilder } from '../embedded-chatbot/theme/theme-context'
|
import type { ThemeBuilder } from '../embedded-chatbot/theme/theme-context'
|
||||||
import type {
|
import type {
|
||||||
@ -60,6 +61,14 @@ export type ChatWithHistoryContextValue = {
|
|||||||
name?: string
|
name?: string
|
||||||
avatar_url?: 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>({
|
export const ChatWithHistoryContext = createContext<ChatWithHistoryContextValue>({
|
||||||
@ -95,5 +104,13 @@ export const ChatWithHistoryContext = createContext<ChatWithHistoryContextValue>
|
|||||||
setCurrentConversationInputs: noop,
|
setCurrentConversationInputs: noop,
|
||||||
allInputsHidden: false,
|
allInputsHidden: false,
|
||||||
initUserVariables: {},
|
initUserVariables: {},
|
||||||
|
showChatMemory: false,
|
||||||
|
setShowChatMemory: noop,
|
||||||
|
memoryList: [],
|
||||||
|
clearAllMemory: noop,
|
||||||
|
updateMemory: noop,
|
||||||
|
resetDefault: noop,
|
||||||
|
clearAllUpdateVersion: noop,
|
||||||
|
switchMemoryVersion: noop,
|
||||||
})
|
})
|
||||||
export const useChatWithHistoryContext = () => useContext(ChatWithHistoryContext)
|
export const useChatWithHistoryContext = () => useContext(ChatWithHistoryContext)
|
||||||
|
|||||||
@ -28,6 +28,8 @@ const HeaderInMobile = () => {
|
|||||||
handleRenameConversation,
|
handleRenameConversation,
|
||||||
conversationRenaming,
|
conversationRenaming,
|
||||||
inputsForms,
|
inputsForms,
|
||||||
|
showChatMemory,
|
||||||
|
setShowChatMemory,
|
||||||
} = useChatWithHistoryContext()
|
} = useChatWithHistoryContext()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const isPin = pinnedConversationList.some(item => item.id === currentConversationId)
|
const isPin = pinnedConversationList.some(item => item.id === currentConversationId)
|
||||||
@ -60,6 +62,9 @@ const HeaderInMobile = () => {
|
|||||||
if (showRename)
|
if (showRename)
|
||||||
handleRenameConversation(showRename.id, newName, { onSuccess: handleCancelRename })
|
handleRenameConversation(showRename.id, newName, { onSuccess: handleCancelRename })
|
||||||
}, [showRename, handleRenameConversation, handleCancelRename])
|
}, [showRename, handleRenameConversation, handleCancelRename])
|
||||||
|
const handleChatMemoryToggle = useCallback(() => {
|
||||||
|
setShowChatMemory(!showChatMemory)
|
||||||
|
}, [setShowChatMemory, showChatMemory])
|
||||||
const [showSidebar, setShowSidebar] = useState(false)
|
const [showSidebar, setShowSidebar] = useState(false)
|
||||||
const [showChatSettings, setShowChatSettings] = useState(false)
|
const [showChatSettings, setShowChatSettings] = useState(false)
|
||||||
|
|
||||||
@ -98,6 +103,7 @@ const HeaderInMobile = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<MobileOperationDropdown
|
<MobileOperationDropdown
|
||||||
|
handleChatMemoryToggle={handleChatMemoryToggle}
|
||||||
handleResetChat={handleNewConversation}
|
handleResetChat={handleNewConversation}
|
||||||
handleViewChatSettings={() => setShowChatSettings(true)}
|
handleViewChatSettings={() => setShowChatSettings(true)}
|
||||||
hideViewChatSettings={inputsForms.length < 1}
|
hideViewChatSettings={inputsForms.length < 1}
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
import { useCallback, useState } from 'react'
|
import { useCallback, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import {
|
import {
|
||||||
RiEditBoxLine,
|
RiEditBoxLine,
|
||||||
RiLayoutRight2Line,
|
RiLayoutRight2Line,
|
||||||
RiResetLeftLine,
|
RiResetLeftLine,
|
||||||
} from '@remixicon/react'
|
} from '@remixicon/react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { Memory } from '@/app/components/base/icons/src/vender/line/others'
|
||||||
import {
|
import {
|
||||||
useChatWithHistoryContext,
|
useChatWithHistoryContext,
|
||||||
} from '../context'
|
} from '../context'
|
||||||
@ -34,6 +35,8 @@ const Header = () => {
|
|||||||
sidebarCollapseState,
|
sidebarCollapseState,
|
||||||
handleSidebarCollapse,
|
handleSidebarCollapse,
|
||||||
isResponding,
|
isResponding,
|
||||||
|
showChatMemory,
|
||||||
|
setShowChatMemory,
|
||||||
} = useChatWithHistoryContext()
|
} = useChatWithHistoryContext()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const isSidebarCollapsed = sidebarCollapseState
|
const isSidebarCollapsed = sidebarCollapseState
|
||||||
@ -70,6 +73,10 @@ const Header = () => {
|
|||||||
handleRenameConversation(showRename.id, newName, { onSuccess: handleCancelRename })
|
handleRenameConversation(showRename.id, newName, { onSuccess: handleCancelRename })
|
||||||
}, [showRename, handleRenameConversation, handleCancelRename])
|
}, [showRename, handleRenameConversation, handleCancelRename])
|
||||||
|
|
||||||
|
const handleChatMemoryToggle = useCallback(() => {
|
||||||
|
setShowChatMemory(!showChatMemory)
|
||||||
|
}, [setShowChatMemory, showChatMemory])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className='flex h-14 shrink-0 items-center justify-between p-3'>
|
<div className='flex h-14 shrink-0 items-center justify-between p-3'>
|
||||||
@ -137,6 +144,15 @@ const Header = () => {
|
|||||||
{currentConversationId && inputsForms.length > 0 && (
|
{currentConversationId && inputsForms.length > 0 && (
|
||||||
<ViewFormDropdown />
|
<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>
|
||||||
</div>
|
</div>
|
||||||
{!!showConfirm && (
|
{!!showConfirm && (
|
||||||
|
|||||||
@ -9,12 +9,14 @@ import ActionButton, { ActionButtonState } from '@/app/components/base/action-bu
|
|||||||
type Props = {
|
type Props = {
|
||||||
handleResetChat: () => void
|
handleResetChat: () => void
|
||||||
handleViewChatSettings: () => void
|
handleViewChatSettings: () => void
|
||||||
|
handleChatMemoryToggle?: () => void
|
||||||
hideViewChatSettings?: boolean
|
hideViewChatSettings?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const MobileOperationDropdown = ({
|
const MobileOperationDropdown = ({
|
||||||
handleResetChat,
|
handleResetChat,
|
||||||
handleViewChatSettings,
|
handleViewChatSettings,
|
||||||
|
handleChatMemoryToggle,
|
||||||
hideViewChatSettings = false,
|
hideViewChatSettings = false,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
@ -44,6 +46,9 @@ 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}>
|
<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>
|
<span className='grow'>{t('share.chat.resetChat')}</span>
|
||||||
</div>
|
</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 && (
|
{!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}>
|
<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>
|
<span className='grow'>{t('share.chat.viewChatSettings')}</span>
|
||||||
|
|||||||
@ -21,8 +21,11 @@ import { addFileInfos, sortAgentSorts } from '../../../tools/utils'
|
|||||||
import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils'
|
import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils'
|
||||||
import {
|
import {
|
||||||
delConversation,
|
delConversation,
|
||||||
|
deleteMemory,
|
||||||
|
editMemory,
|
||||||
fetchChatList,
|
fetchChatList,
|
||||||
fetchConversations,
|
fetchConversations,
|
||||||
|
fetchMemories,
|
||||||
generationConversationName,
|
generationConversationName,
|
||||||
pinConversation,
|
pinConversation,
|
||||||
renameConversation,
|
renameConversation,
|
||||||
@ -41,6 +44,9 @@ import { InputVarType } from '@/app/components/workflow/types'
|
|||||||
import { TransferMethod } from '@/types/app'
|
import { TransferMethod } from '@/types/app'
|
||||||
import { noop } from 'lodash-es'
|
import { noop } from 'lodash-es'
|
||||||
import { useWebAppStore } from '@/context/web-app-context'
|
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[]) {
|
function getFormattedChatList(messages: any[]) {
|
||||||
const newChatList: ChatItem[] = []
|
const newChatList: ChatItem[] = []
|
||||||
@ -526,6 +532,61 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
|||||||
notify({ type: 'success', message: t('common.api.success') })
|
notify({ type: 'success', message: t('common.api.success') })
|
||||||
}, [isInstalledApp, appId, t, notify])
|
}, [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 {
|
return {
|
||||||
isInstalledApp,
|
isInstalledApp,
|
||||||
appId,
|
appId,
|
||||||
@ -572,5 +633,13 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
|||||||
setCurrentConversationInputs,
|
setCurrentConversationInputs,
|
||||||
allInputsHidden,
|
allInputsHidden,
|
||||||
initUserVariables,
|
initUserVariables,
|
||||||
|
showChatMemory,
|
||||||
|
setShowChatMemory,
|
||||||
|
memoryList,
|
||||||
|
clearAllMemory,
|
||||||
|
updateMemory,
|
||||||
|
resetDefault,
|
||||||
|
clearAllUpdateVersion,
|
||||||
|
switchMemoryVersion,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import Sidebar from './sidebar'
|
|||||||
import Header from './header'
|
import Header from './header'
|
||||||
import HeaderInMobile from './header-in-mobile'
|
import HeaderInMobile from './header-in-mobile'
|
||||||
import ChatWrapper from './chat-wrapper'
|
import ChatWrapper from './chat-wrapper'
|
||||||
|
import MemoryPanel from './memory'
|
||||||
import type { InstalledApp } from '@/models/explore'
|
import type { InstalledApp } from '@/models/explore'
|
||||||
import Loading from '@/app/components/base/loading'
|
import Loading from '@/app/components/base/loading'
|
||||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||||
@ -33,6 +34,14 @@ const ChatWithHistory: FC<ChatWithHistoryProps> = ({
|
|||||||
isMobile,
|
isMobile,
|
||||||
themeBuilder,
|
themeBuilder,
|
||||||
sidebarCollapseState,
|
sidebarCollapseState,
|
||||||
|
showChatMemory,
|
||||||
|
setShowChatMemory,
|
||||||
|
memoryList,
|
||||||
|
clearAllMemory,
|
||||||
|
updateMemory,
|
||||||
|
resetDefault,
|
||||||
|
clearAllUpdateVersion,
|
||||||
|
switchMemoryVersion,
|
||||||
} = useChatWithHistoryContext()
|
} = useChatWithHistoryContext()
|
||||||
const isSidebarCollapsed = sidebarCollapseState
|
const isSidebarCollapsed = sidebarCollapseState
|
||||||
const customConfig = appData?.custom_config
|
const customConfig = appData?.custom_config
|
||||||
@ -68,7 +77,7 @@ const ChatWithHistory: FC<ChatWithHistoryProps> = ({
|
|||||||
{isMobile && (
|
{isMobile && (
|
||||||
<HeaderInMobile />
|
<HeaderInMobile />
|
||||||
)}
|
)}
|
||||||
<div className={cn('relative grow p-2', isMobile && 'h-[calc(100%_-_56px)] p-0')}>
|
<div className={cn('relative flex grow p-2', isMobile && 'h-[calc(100%_-_56px)] p-0')}>
|
||||||
{isSidebarCollapsed && (
|
{isSidebarCollapsed && (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
@ -81,7 +90,11 @@ const ChatWithHistory: FC<ChatWithHistoryProps> = ({
|
|||||||
<Sidebar isPanel panelVisible={showSidePanel} />
|
<Sidebar isPanel panelVisible={showSidePanel} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<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')}>
|
<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',
|
||||||
|
)}>
|
||||||
{!isMobile && <Header />}
|
{!isMobile && <Header />}
|
||||||
{appChatListDataLoading && (
|
{appChatListDataLoading && (
|
||||||
<Loading type='app' />
|
<Loading type='app' />
|
||||||
@ -90,6 +103,38 @@ const ChatWithHistory: FC<ChatWithHistoryProps> = ({
|
|||||||
<ChatWrapper key={chatShouldReloadKey} />
|
<ChatWrapper key={chatShouldReloadKey} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@ -145,6 +190,14 @@ const ChatWithHistoryWrap: FC<ChatWithHistoryWrapProps> = ({
|
|||||||
setCurrentConversationInputs,
|
setCurrentConversationInputs,
|
||||||
allInputsHidden,
|
allInputsHidden,
|
||||||
initUserVariables,
|
initUserVariables,
|
||||||
|
showChatMemory,
|
||||||
|
setShowChatMemory,
|
||||||
|
memoryList,
|
||||||
|
clearAllMemory,
|
||||||
|
updateMemory,
|
||||||
|
resetDefault,
|
||||||
|
clearAllUpdateVersion,
|
||||||
|
switchMemoryVersion,
|
||||||
} = useChatWithHistory(installedAppInfo)
|
} = useChatWithHistory(installedAppInfo)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -188,6 +241,14 @@ const ChatWithHistoryWrap: FC<ChatWithHistoryWrapProps> = ({
|
|||||||
setCurrentConversationInputs,
|
setCurrentConversationInputs,
|
||||||
allInputsHidden,
|
allInputsHidden,
|
||||||
initUserVariables,
|
initUserVariables,
|
||||||
|
showChatMemory,
|
||||||
|
setShowChatMemory,
|
||||||
|
memoryList,
|
||||||
|
clearAllMemory,
|
||||||
|
updateMemory,
|
||||||
|
resetDefault,
|
||||||
|
clearAllUpdateVersion,
|
||||||
|
switchMemoryVersion,
|
||||||
}}>
|
}}>
|
||||||
<ChatWithHistory className={className} />
|
<ChatWithHistory className={className} />
|
||||||
</ChatWithHistoryContext.Provider>
|
</ChatWithHistoryContext.Provider>
|
||||||
|
|||||||
@ -0,0 +1,128 @@
|
|||||||
|
'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
|
||||||
@ -0,0 +1,127 @@
|
|||||||
|
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
|
||||||
@ -0,0 +1,88 @@
|
|||||||
|
'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)
|
||||||
@ -0,0 +1,80 @@
|
|||||||
|
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
|
||||||
@ -0,0 +1,96 @@
|
|||||||
|
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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
@ -6,6 +6,7 @@ import type {
|
|||||||
ChatConfig,
|
ChatConfig,
|
||||||
ChatItem,
|
ChatItem,
|
||||||
Feedback,
|
Feedback,
|
||||||
|
Memory,
|
||||||
} from '../types'
|
} from '../types'
|
||||||
import type { ThemeBuilder } from './theme/theme-context'
|
import type { ThemeBuilder } from './theme/theme-context'
|
||||||
import type {
|
import type {
|
||||||
@ -53,6 +54,14 @@ export type EmbeddedChatbotContextValue = {
|
|||||||
name?: string
|
name?: string
|
||||||
avatar_url?: 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>({
|
export const EmbeddedChatbotContext = createContext<EmbeddedChatbotContextValue>({
|
||||||
@ -86,5 +95,13 @@ export const EmbeddedChatbotContext = createContext<EmbeddedChatbotContextValue>
|
|||||||
setCurrentConversationInputs: noop,
|
setCurrentConversationInputs: noop,
|
||||||
allInputsHidden: false,
|
allInputsHidden: false,
|
||||||
initUserVariables: {},
|
initUserVariables: {},
|
||||||
|
showChatMemory: false,
|
||||||
|
setShowChatMemory: noop,
|
||||||
|
memoryList: [],
|
||||||
|
clearAllMemory: noop,
|
||||||
|
updateMemory: noop,
|
||||||
|
resetDefault: noop,
|
||||||
|
clearAllUpdateVersion: noop,
|
||||||
|
switchMemoryVersion: noop,
|
||||||
})
|
})
|
||||||
export const useEmbeddedChatbotContext = () => useContext(EmbeddedChatbotContext)
|
export const useEmbeddedChatbotContext = () => useContext(EmbeddedChatbotContext)
|
||||||
|
|||||||
@ -7,8 +7,9 @@ import { CssTransform } from '../theme/utils'
|
|||||||
import {
|
import {
|
||||||
useEmbeddedChatbotContext,
|
useEmbeddedChatbotContext,
|
||||||
} from '../context'
|
} from '../context'
|
||||||
|
import { Memory } from '@/app/components/base/icons/src/vender/line/others'
|
||||||
import Tooltip from '@/app/components/base/tooltip'
|
import Tooltip from '@/app/components/base/tooltip'
|
||||||
import ActionButton from '@/app/components/base/action-button'
|
import ActionButton, { ActionButtonState } from '@/app/components/base/action-button'
|
||||||
import Divider from '@/app/components/base/divider'
|
import Divider from '@/app/components/base/divider'
|
||||||
import ViewFormDropdown from '@/app/components/base/chat/embedded-chatbot/inputs-form/view-form-dropdown'
|
import ViewFormDropdown from '@/app/components/base/chat/embedded-chatbot/inputs-form/view-form-dropdown'
|
||||||
import DifyLogo from '@/app/components/base/logo/dify-logo'
|
import DifyLogo from '@/app/components/base/logo/dify-logo'
|
||||||
@ -36,6 +37,8 @@ const Header: FC<IHeaderProps> = ({
|
|||||||
appData,
|
appData,
|
||||||
currentConversationId,
|
currentConversationId,
|
||||||
inputsForms,
|
inputsForms,
|
||||||
|
showChatMemory,
|
||||||
|
setShowChatMemory,
|
||||||
allInputsHidden,
|
allInputsHidden,
|
||||||
} = useEmbeddedChatbotContext()
|
} = useEmbeddedChatbotContext()
|
||||||
|
|
||||||
@ -77,6 +80,10 @@ const Header: FC<IHeaderProps> = ({
|
|||||||
}, parentOrigin)
|
}, parentOrigin)
|
||||||
}, [isIframe, parentOrigin, showToggleExpandButton, expanded])
|
}, [isIframe, parentOrigin, showToggleExpandButton, expanded])
|
||||||
|
|
||||||
|
const handleChatMemoryToggle = useCallback(() => {
|
||||||
|
setShowChatMemory(!showChatMemory)
|
||||||
|
}, [setShowChatMemory, showChatMemory])
|
||||||
|
|
||||||
if (!isMobile) {
|
if (!isMobile) {
|
||||||
return (
|
return (
|
||||||
<div className='flex h-14 shrink-0 items-center justify-end p-3'>
|
<div className='flex h-14 shrink-0 items-center justify-end p-3'>
|
||||||
@ -128,6 +135,15 @@ const Header: FC<IHeaderProps> = ({
|
|||||||
{currentConversationId && inputsForms.length > 0 && !allInputsHidden && (
|
{currentConversationId && inputsForms.length > 0 && !allInputsHidden && (
|
||||||
<ViewFormDropdown />
|
<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>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@ -175,6 +191,15 @@ const Header: FC<IHeaderProps> = ({
|
|||||||
{currentConversationId && inputsForms.length > 0 && !allInputsHidden && (
|
{currentConversationId && inputsForms.length > 0 && !allInputsHidden && (
|
||||||
<ViewFormDropdown iconColor={theme?.colorPathOnHeader} />
|
<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>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -18,8 +18,11 @@ import { CONVERSATION_ID_INFO } from '../constants'
|
|||||||
import { buildChatItemTree, getProcessedInputsFromUrlParams, getProcessedSystemVariablesFromUrlParams, getProcessedUserVariablesFromUrlParams } from '../utils'
|
import { buildChatItemTree, getProcessedInputsFromUrlParams, getProcessedSystemVariablesFromUrlParams, getProcessedUserVariablesFromUrlParams } from '../utils'
|
||||||
import { getProcessedFilesFromResponse } from '../../file-uploader/utils'
|
import { getProcessedFilesFromResponse } from '../../file-uploader/utils'
|
||||||
import {
|
import {
|
||||||
|
deleteMemory,
|
||||||
|
editMemory,
|
||||||
fetchChatList,
|
fetchChatList,
|
||||||
fetchConversations,
|
fetchConversations,
|
||||||
|
fetchMemories,
|
||||||
generationConversationName,
|
generationConversationName,
|
||||||
updateFeedback,
|
updateFeedback,
|
||||||
} from '@/service/share'
|
} from '@/service/share'
|
||||||
@ -33,6 +36,7 @@ import { InputVarType } from '@/app/components/workflow/types'
|
|||||||
import { TransferMethod } from '@/types/app'
|
import { TransferMethod } from '@/types/app'
|
||||||
import { addFileInfos, sortAgentSorts } from '@/app/components/tools/utils'
|
import { addFileInfos, sortAgentSorts } from '@/app/components/tools/utils'
|
||||||
import { noop } from 'lodash-es'
|
import { noop } from 'lodash-es'
|
||||||
|
import type { Memory } from '@/app/components/base/chat/types'
|
||||||
import { useWebAppStore } from '@/context/web-app-context'
|
import { useWebAppStore } from '@/context/web-app-context'
|
||||||
|
|
||||||
function getFormattedChatList(messages: any[]) {
|
function getFormattedChatList(messages: any[]) {
|
||||||
@ -387,6 +391,61 @@ export const useEmbeddedChatbot = () => {
|
|||||||
notify({ type: 'success', message: t('common.api.success') })
|
notify({ type: 'success', message: t('common.api.success') })
|
||||||
}, [isInstalledApp, appId, t, notify])
|
}, [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 {
|
return {
|
||||||
isInstalledApp,
|
isInstalledApp,
|
||||||
allowResetChat,
|
allowResetChat,
|
||||||
@ -426,5 +485,13 @@ export const useEmbeddedChatbot = () => {
|
|||||||
setCurrentConversationInputs,
|
setCurrentConversationInputs,
|
||||||
allInputsHidden,
|
allInputsHidden,
|
||||||
initUserVariables,
|
initUserVariables,
|
||||||
|
showChatMemory,
|
||||||
|
setShowChatMemory,
|
||||||
|
memoryList,
|
||||||
|
clearAllMemory,
|
||||||
|
updateMemory,
|
||||||
|
resetDefault,
|
||||||
|
clearAllUpdateVersion,
|
||||||
|
switchMemoryVersion,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import Loading from '@/app/components/base/loading'
|
|||||||
import LogoHeader from '@/app/components/base/logo/logo-embedded-chat-header'
|
import LogoHeader from '@/app/components/base/logo/logo-embedded-chat-header'
|
||||||
import Header from '@/app/components/base/chat/embedded-chatbot/header'
|
import Header from '@/app/components/base/chat/embedded-chatbot/header'
|
||||||
import ChatWrapper from '@/app/components/base/chat/embedded-chatbot/chat-wrapper'
|
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 DifyLogo from '@/app/components/base/logo/dify-logo'
|
||||||
import cn from '@/utils/classnames'
|
import cn from '@/utils/classnames'
|
||||||
import useDocumentTitle from '@/hooks/use-document-title'
|
import useDocumentTitle from '@/hooks/use-document-title'
|
||||||
@ -30,6 +31,14 @@ const Chatbot = () => {
|
|||||||
chatShouldReloadKey,
|
chatShouldReloadKey,
|
||||||
handleNewConversation,
|
handleNewConversation,
|
||||||
themeBuilder,
|
themeBuilder,
|
||||||
|
showChatMemory,
|
||||||
|
setShowChatMemory,
|
||||||
|
memoryList,
|
||||||
|
clearAllMemory,
|
||||||
|
updateMemory,
|
||||||
|
resetDefault,
|
||||||
|
clearAllUpdateVersion,
|
||||||
|
switchMemoryVersion,
|
||||||
} = useEmbeddedChatbotContext()
|
} = useEmbeddedChatbotContext()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
|
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
|
||||||
@ -90,6 +99,25 @@ const Chatbot = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -131,6 +159,14 @@ const EmbeddedChatbotWrapper = () => {
|
|||||||
setCurrentConversationInputs,
|
setCurrentConversationInputs,
|
||||||
allInputsHidden,
|
allInputsHidden,
|
||||||
initUserVariables,
|
initUserVariables,
|
||||||
|
showChatMemory,
|
||||||
|
setShowChatMemory,
|
||||||
|
memoryList,
|
||||||
|
clearAllMemory,
|
||||||
|
updateMemory,
|
||||||
|
resetDefault,
|
||||||
|
clearAllUpdateVersion,
|
||||||
|
switchMemoryVersion,
|
||||||
} = useEmbeddedChatbot()
|
} = useEmbeddedChatbot()
|
||||||
|
|
||||||
return <EmbeddedChatbotContext.Provider value={{
|
return <EmbeddedChatbotContext.Provider value={{
|
||||||
@ -167,6 +203,14 @@ const EmbeddedChatbotWrapper = () => {
|
|||||||
setCurrentConversationInputs,
|
setCurrentConversationInputs,
|
||||||
allInputsHidden,
|
allInputsHidden,
|
||||||
initUserVariables,
|
initUserVariables,
|
||||||
|
showChatMemory,
|
||||||
|
setShowChatMemory,
|
||||||
|
memoryList,
|
||||||
|
clearAllMemory,
|
||||||
|
updateMemory,
|
||||||
|
resetDefault,
|
||||||
|
clearAllUpdateVersion,
|
||||||
|
switchMemoryVersion,
|
||||||
}}>
|
}}>
|
||||||
<Chatbot />
|
<Chatbot />
|
||||||
</EmbeddedChatbotContext.Provider>
|
</EmbeddedChatbotContext.Provider>
|
||||||
|
|||||||
@ -95,3 +95,36 @@ export type Feedback = {
|
|||||||
rating: 'like' | 'dislike' | null
|
rating: 'like' | 'dislike' | null
|
||||||
content?: string | 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
|
||||||
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
import useSWR from 'swr'
|
||||||
import { produce } from 'immer'
|
import { produce } from 'immer'
|
||||||
import React, { Fragment } from 'react'
|
import React, { Fragment } from 'react'
|
||||||
import { usePathname } from 'next/navigation'
|
import { usePathname } from 'next/navigation'
|
||||||
@ -8,6 +9,7 @@ import { Listbox, ListboxButton, ListboxOption, ListboxOptions, Transition } fro
|
|||||||
import { CheckIcon, ChevronDownIcon } from '@heroicons/react/20/solid'
|
import { CheckIcon, ChevronDownIcon } from '@heroicons/react/20/solid'
|
||||||
import { useFeatures, useFeaturesStore } from '@/app/components/base/features/hooks'
|
import { useFeatures, useFeaturesStore } from '@/app/components/base/features/hooks'
|
||||||
import type { Item } from '@/app/components/base/select'
|
import type { Item } from '@/app/components/base/select'
|
||||||
|
import { fetchAppVoices } from '@/service/apps'
|
||||||
import Tooltip from '@/app/components/base/tooltip'
|
import Tooltip from '@/app/components/base/tooltip'
|
||||||
import Switch from '@/app/components/base/switch'
|
import Switch from '@/app/components/base/switch'
|
||||||
import AudioBtn from '@/app/components/base/audio-btn'
|
import AudioBtn from '@/app/components/base/audio-btn'
|
||||||
@ -15,7 +17,6 @@ import { languages } from '@/i18n-config/language'
|
|||||||
import { TtsAutoPlay } from '@/types/app'
|
import { TtsAutoPlay } from '@/types/app'
|
||||||
import type { OnFeaturesChange } from '@/app/components/base/features/types'
|
import type { OnFeaturesChange } from '@/app/components/base/features/types'
|
||||||
import classNames from '@/utils/classnames'
|
import classNames from '@/utils/classnames'
|
||||||
import { useAppVoices } from '@/service/use-apps'
|
|
||||||
|
|
||||||
type VoiceParamConfigProps = {
|
type VoiceParamConfigProps = {
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
@ -38,7 +39,7 @@ const VoiceParamConfig = ({
|
|||||||
const localLanguagePlaceholder = languageItem?.name || t('common.placeholder.select')
|
const localLanguagePlaceholder = languageItem?.name || t('common.placeholder.select')
|
||||||
|
|
||||||
const language = languageItem?.value
|
const language = languageItem?.value
|
||||||
const { data: voiceItems } = useAppVoices(appId, language)
|
const voiceItems = useSWR({ appId, language }, fetchAppVoices).data
|
||||||
let voiceItem = voiceItems?.find(item => item.value === text2speech?.voice)
|
let voiceItem = voiceItems?.find(item => item.value === text2speech?.voice)
|
||||||
if (voiceItems && !voiceItem)
|
if (voiceItems && !voiceItem)
|
||||||
voiceItem = voiceItems[0]
|
voiceItem = voiceItems[0]
|
||||||
|
|||||||
@ -9,7 +9,12 @@ import Tooltip from '@/app/components/base/tooltip'
|
|||||||
import { useRenderI18nObject } from '@/hooks/use-i18n'
|
import { useRenderI18nObject } from '@/hooks/use-i18n'
|
||||||
import { useTriggerPluginDynamicOptions } from '@/service/use-triggers'
|
import { useTriggerPluginDynamicOptions } from '@/service/use-triggers'
|
||||||
import cn from '@/utils/classnames'
|
import cn from '@/utils/classnames'
|
||||||
import { RiExternalLinkLine } from '@remixicon/react'
|
import {
|
||||||
|
RiArrowDownSFill,
|
||||||
|
RiDraftLine,
|
||||||
|
RiExternalLinkLine,
|
||||||
|
RiInputField,
|
||||||
|
} from '@remixicon/react'
|
||||||
import type { AnyFieldApi } from '@tanstack/react-form'
|
import type { AnyFieldApi } from '@tanstack/react-form'
|
||||||
import { useStore } from '@tanstack/react-form'
|
import { useStore } from '@tanstack/react-form'
|
||||||
import {
|
import {
|
||||||
@ -19,6 +24,19 @@ import {
|
|||||||
useMemo,
|
useMemo,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
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) => {
|
const getExtraProps = (type: FormTypeEnum) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@ -100,7 +118,6 @@ const BaseField = ({
|
|||||||
options,
|
options,
|
||||||
labelClassName: formLabelClassName,
|
labelClassName: formLabelClassName,
|
||||||
disabled: formSchemaDisabled,
|
disabled: formSchemaDisabled,
|
||||||
type: formItemType,
|
|
||||||
dynamicSelectParams,
|
dynamicSelectParams,
|
||||||
multiple = false,
|
multiple = false,
|
||||||
tooltip,
|
tooltip,
|
||||||
@ -108,7 +125,14 @@ const BaseField = ({
|
|||||||
description,
|
description,
|
||||||
url,
|
url,
|
||||||
help,
|
help,
|
||||||
|
type: typeOrFn,
|
||||||
|
fieldClassName: formFieldClassName,
|
||||||
|
inputContainerClassName: formInputContainerClassName,
|
||||||
|
inputClassName: formInputClassName,
|
||||||
|
selfFormProps,
|
||||||
|
onChange: formOnChange,
|
||||||
} = formSchema
|
} = formSchema
|
||||||
|
const formItemType = typeof typeOrFn === 'function' ? typeOrFn(field.form) : typeOrFn
|
||||||
const disabled = propsDisabled || formSchemaDisabled
|
const disabled = propsDisabled || formSchemaDisabled
|
||||||
|
|
||||||
const [translatedLabel, translatedPlaceholder, translatedTooltip, translatedDescription, translatedHelp] = useMemo(() => {
|
const [translatedLabel, translatedPlaceholder, translatedTooltip, translatedDescription, translatedHelp] = useMemo(() => {
|
||||||
@ -148,7 +172,8 @@ const BaseField = ({
|
|||||||
return true
|
return true
|
||||||
|
|
||||||
return option.show_on.every((condition) => {
|
return option.show_on.every((condition) => {
|
||||||
return watchedValues[condition.variable] === condition.value
|
const conditionValue = watchedValues[condition.variable]
|
||||||
|
return Array.isArray(condition.value) ? condition.value.includes(conditionValue) : conditionValue === condition.value
|
||||||
})
|
})
|
||||||
}).map((option) => {
|
}).map((option) => {
|
||||||
return {
|
return {
|
||||||
@ -180,21 +205,67 @@ const BaseField = ({
|
|||||||
}))
|
}))
|
||||||
}, [dynamicOptionsData, renderI18nObject])
|
}, [dynamicOptionsData, renderI18nObject])
|
||||||
|
|
||||||
|
const booleanRadioValue = useMemo(() => {
|
||||||
|
if (value === null || value === undefined)
|
||||||
|
return undefined
|
||||||
|
return value ? 1 : 0
|
||||||
|
}, [value])
|
||||||
|
|
||||||
const handleChange = useCallback((value: any) => {
|
const handleChange = useCallback((value: any) => {
|
||||||
|
if (disabled)
|
||||||
|
return
|
||||||
|
|
||||||
field.handleChange(value)
|
field.handleChange(value)
|
||||||
|
formOnChange?.(field.form, value)
|
||||||
onChange?.(field.name, value)
|
onChange?.(field.name, value)
|
||||||
}, [field, onChange])
|
}, [field, formOnChange, onChange, disabled])
|
||||||
|
|
||||||
|
const selfProps = typeof selfFormProps === 'function' ? selfFormProps(field.form) : selfFormProps
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={cn(fieldClassName)}>
|
{
|
||||||
<div className={cn(labelClassName, formLabelClassName)}>
|
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)
|
||||||
|
}}
|
||||||
|
>
|
||||||
{translatedLabel}
|
{translatedLabel}
|
||||||
{
|
{
|
||||||
required && !isValidElement(label) && (
|
required && !isValidElement(label) && (
|
||||||
<span className='ml-1 text-text-destructive-secondary'>*</span>
|
<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 && (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
popupContent={<div className='w-[200px]'>{translatedTooltip}</div>}
|
popupContent={<div className='w-[200px]'>{translatedTooltip}</div>}
|
||||||
@ -202,9 +273,9 @@ const BaseField = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className={cn(inputContainerClassName)}>
|
<div className={cn(inputContainerClassName, formInputContainerClassName)}>
|
||||||
{
|
{
|
||||||
[FormTypeEnum.textInput, FormTypeEnum.secretInput, FormTypeEnum.textNumber].includes(formItemType) && (
|
!selfProps?.withSlider && [FormTypeEnum.textInput, FormTypeEnum.secretInput, FormTypeEnum.textNumber].includes(formItemType) && (
|
||||||
<Input
|
<Input
|
||||||
id={field.name}
|
id={field.name}
|
||||||
name={field.name}
|
name={field.name}
|
||||||
@ -221,6 +292,34 @@ 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 && (
|
formItemType === FormTypeEnum.select && !multiple && (
|
||||||
<PureSelect
|
<PureSelect
|
||||||
@ -276,15 +375,16 @@ const BaseField = ({
|
|||||||
<div
|
<div
|
||||||
key={option.value}
|
key={option.value}
|
||||||
className={cn(
|
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 gap-2 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 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',
|
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',
|
disabled && 'cursor-not-allowed opacity-50',
|
||||||
inputClassName,
|
inputClassName,
|
||||||
|
formInputClassName,
|
||||||
)}
|
)}
|
||||||
onClick={() => !disabled && handleChange(option.value)}
|
onClick={() => handleChange(option.value)}
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
formSchema.showRadioUI && (
|
selfProps?.showRadioUI && (
|
||||||
<RadioE
|
<RadioE
|
||||||
className='mr-2'
|
className='mr-2'
|
||||||
isChecked={value === option.value}
|
isChecked={value === option.value}
|
||||||
@ -298,18 +398,151 @@ const BaseField = ({
|
|||||||
</div>
|
</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 && (
|
formItemType === FormTypeEnum.boolean && (
|
||||||
<Radio.Group
|
<Radio.Group
|
||||||
className='flex w-fit items-center'
|
className={cn('flex w-full items-center space-x-1', inputClassName, formInputClassName)}
|
||||||
value={value}
|
value={booleanRadioValue}
|
||||||
onChange={v => field.handleChange(v)}
|
onChange={handleChange}
|
||||||
>
|
>
|
||||||
<Radio value={true} className='!mr-1'>True</Radio>
|
<Radio value={1} className='m-0 h-7 flex-1 justify-center p-0'>True</Radio>
|
||||||
<Radio value={false}>False</Radio>
|
<Radio value={0} className='m-0 h-7 flex-1 justify-center p-0'>False</Radio>
|
||||||
</Radio.Group>
|
</Radio.Group>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
formItemType === FormTypeEnum.switch && (
|
||||||
|
<Switch
|
||||||
|
defaultValue={value}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
{fieldState?.validateStatus && [FormItemValidateStatusEnum.Error, FormItemValidateStatusEnum.Warning].includes(fieldState?.validateStatus) && (
|
{fieldState?.validateStatus && [FormItemValidateStatusEnum.Error, FormItemValidateStatusEnum.Warning].includes(fieldState?.validateStatus) && (
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
'system-xs-regular mt-1 px-0 py-[2px]',
|
'system-xs-regular mt-1 px-0 py-[2px]',
|
||||||
@ -339,6 +572,11 @@ const BaseField = ({
|
|||||||
</a>
|
</a>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
selfProps?.withBottomDivider && (
|
||||||
|
<div className='h-px w-full bg-divider-subtle' />
|
||||||
|
)
|
||||||
|
}
|
||||||
</>
|
</>
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import {
|
|||||||
useMemo,
|
useMemo,
|
||||||
useState,
|
useState,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import type {
|
import type {
|
||||||
AnyFieldApi,
|
AnyFieldApi,
|
||||||
AnyFormApi,
|
AnyFormApi,
|
||||||
@ -31,6 +32,7 @@ import {
|
|||||||
useGetFormValues,
|
useGetFormValues,
|
||||||
useGetValidators,
|
useGetValidators,
|
||||||
} from '@/app/components/base/form/hooks'
|
} from '@/app/components/base/form/hooks'
|
||||||
|
import { Button } from '@/app/components/base/button'
|
||||||
|
|
||||||
export type BaseFormProps = {
|
export type BaseFormProps = {
|
||||||
formSchemas?: FormSchema[]
|
formSchemas?: FormSchema[]
|
||||||
@ -39,6 +41,7 @@ export type BaseFormProps = {
|
|||||||
ref?: FormRef
|
ref?: FormRef
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
formFromProps?: AnyFormApi
|
formFromProps?: AnyFormApi
|
||||||
|
onCancel?: () => void
|
||||||
onChange?: (field: string, value: any) => void
|
onChange?: (field: string, value: any) => void
|
||||||
onSubmit?: (e: React.FormEvent<HTMLFormElement>) => void
|
onSubmit?: (e: React.FormEvent<HTMLFormElement>) => void
|
||||||
preventDefaultSubmit?: boolean
|
preventDefaultSubmit?: boolean
|
||||||
@ -55,10 +58,12 @@ const BaseForm = ({
|
|||||||
ref,
|
ref,
|
||||||
disabled,
|
disabled,
|
||||||
formFromProps,
|
formFromProps,
|
||||||
|
onCancel,
|
||||||
onChange,
|
onChange,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
preventDefaultSubmit = false,
|
preventDefaultSubmit = false,
|
||||||
}: BaseFormProps) => {
|
}: BaseFormProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
const initialDefaultValues = useMemo(() => {
|
const initialDefaultValues = useMemo(() => {
|
||||||
if (defaultValues)
|
if (defaultValues)
|
||||||
return defaultValues
|
return defaultValues
|
||||||
@ -82,8 +87,22 @@ const BaseForm = ({
|
|||||||
const result: Record<string, any> = {}
|
const result: Record<string, any> = {}
|
||||||
formSchemas.forEach((schema) => {
|
formSchemas.forEach((schema) => {
|
||||||
const { show_on } = schema
|
const { show_on } = schema
|
||||||
if (show_on?.length) {
|
const showOn = typeof show_on === 'function' ? show_on(form) : show_on
|
||||||
show_on.forEach((condition) => {
|
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) => {
|
||||||
result[condition.variable] = s.values[condition.variable]
|
result[condition.variable] = s.values[condition.variable]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -135,11 +154,18 @@ const BaseForm = ({
|
|||||||
const formSchema = formSchemas?.find(schema => schema.name === field.name)
|
const formSchema = formSchemas?.find(schema => schema.name === field.name)
|
||||||
|
|
||||||
if (formSchema) {
|
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 (
|
return (
|
||||||
<BaseField
|
<BaseField
|
||||||
field={field}
|
field={field}
|
||||||
formSchema={formSchema}
|
formSchema={formSchema}
|
||||||
fieldClassName={fieldClassName ?? formSchema.fieldClassName}
|
fieldClassName={cn(fieldClassName ?? formSchema.fieldClassName, !more ? 'absolute top-[-9999px]' : '')}
|
||||||
labelClassName={labelClassName ?? formSchema.labelClassName}
|
labelClassName={labelClassName ?? formSchema.labelClassName}
|
||||||
inputContainerClassName={inputContainerClassName}
|
inputContainerClassName={inputContainerClassName}
|
||||||
inputClassName={inputClassName}
|
inputClassName={inputClassName}
|
||||||
@ -151,7 +177,7 @@ const BaseForm = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}, [formSchemas, fieldClassName, labelClassName, inputContainerClassName, inputClassName, disabled, onChange, fieldStates])
|
}, [formSchemas, fieldClassName, labelClassName, inputContainerClassName, inputClassName, disabled, onChange, moreOnValues, fieldStates])
|
||||||
|
|
||||||
const renderFieldWrapper = useCallback((formSchema: FormSchema) => {
|
const renderFieldWrapper = useCallback((formSchema: FormSchema) => {
|
||||||
const validators = getValidators(formSchema)
|
const validators = getValidators(formSchema)
|
||||||
@ -159,10 +185,10 @@ const BaseForm = ({
|
|||||||
name,
|
name,
|
||||||
show_on = [],
|
show_on = [],
|
||||||
} = formSchema
|
} = formSchema
|
||||||
|
const showOn = typeof show_on === 'function' ? show_on(form) : show_on
|
||||||
const show = show_on?.every((condition) => {
|
const show = (showOn || []).every((condition) => {
|
||||||
const conditionValue = showOnValues[condition.variable]
|
const conditionValue = showOnValues[condition.variable]
|
||||||
return conditionValue === condition.value
|
return Array.isArray(condition.value) ? condition.value.includes(conditionValue) : conditionValue === condition.value
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!show)
|
if (!show)
|
||||||
@ -196,6 +222,28 @@ const BaseForm = ({
|
|||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
>
|
>
|
||||||
{formSchemas.map(renderFieldWrapper)}
|
{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>
|
</form>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,25 @@
|
|||||||
|
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)
|
||||||
@ -15,13 +15,14 @@ export const useCheckValidated = (form: AnyFormApi, FormSchemas: FormSchema[]) =
|
|||||||
const errorArray = Object.keys(fields).reduce((acc: string[], key: string) => {
|
const errorArray = Object.keys(fields).reduce((acc: string[], key: string) => {
|
||||||
const currentSchema = FormSchemas.find(schema => schema.name === key)
|
const currentSchema = FormSchemas.find(schema => schema.name === key)
|
||||||
const { show_on = [] } = currentSchema || {}
|
const { show_on = [] } = currentSchema || {}
|
||||||
const showOnValues = show_on.reduce((acc, condition) => {
|
const showOn = typeof show_on === 'function' ? show_on(form) : show_on
|
||||||
|
const showOnValues = (showOn || []).reduce((acc, condition) => {
|
||||||
acc[condition.variable] = values[condition.variable]
|
acc[condition.variable] = values[condition.variable]
|
||||||
return acc
|
return acc
|
||||||
}, {} as Record<string, any>)
|
}, {} as Record<string, any>)
|
||||||
const show = show_on?.every((condition) => {
|
const show = (showOn || []).every((condition) => {
|
||||||
const conditionValue = showOnValues[condition.variable]
|
const conditionValue = showOnValues[condition.variable]
|
||||||
return conditionValue === condition.value
|
return Array.isArray(condition.value) ? condition.value.includes(conditionValue) : conditionValue === condition.value
|
||||||
})
|
})
|
||||||
const errors: any[] = show ? fields[key].errors : []
|
const errors: any[] = show ? fields[key].errors : []
|
||||||
|
|
||||||
|
|||||||
@ -16,7 +16,7 @@ export type TypeWithI18N<T = string> = {
|
|||||||
|
|
||||||
export type FormShowOnObject = {
|
export type FormShowOnObject = {
|
||||||
variable: string
|
variable: string
|
||||||
value: string
|
value: string | string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum FormTypeEnum {
|
export enum FormTypeEnum {
|
||||||
@ -33,7 +33,17 @@ export enum FormTypeEnum {
|
|||||||
multiToolSelector = 'array[tools]',
|
multiToolSelector = 'array[tools]',
|
||||||
appSelector = 'app-selector',
|
appSelector = 'app-selector',
|
||||||
dynamicSelect = 'dynamic-select',
|
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',
|
boolean = 'boolean',
|
||||||
|
booleanList = 'boolean-list',
|
||||||
|
switch = 'switch',
|
||||||
|
nodeSelector = 'node-selector', // used in memory variable form
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FormOption = {
|
export type FormOption = {
|
||||||
@ -53,7 +63,7 @@ export enum FormItemValidateStatusEnum {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type FormSchema = {
|
export type FormSchema = {
|
||||||
type: FormTypeEnum
|
type: FormTypeEnum | ((form: AnyFormApi) => FormTypeEnum)
|
||||||
name: string
|
name: string
|
||||||
label: string | ReactNode | TypeWithI18N | Record<Locale, string>
|
label: string | ReactNode | TypeWithI18N | Record<Locale, string>
|
||||||
required: boolean
|
required: boolean
|
||||||
@ -61,15 +71,20 @@ export type FormSchema = {
|
|||||||
default?: any
|
default?: any
|
||||||
description?: string | TypeWithI18N | Record<Locale, string>
|
description?: string | TypeWithI18N | Record<Locale, string>
|
||||||
tooltip?: string | TypeWithI18N | Record<Locale, string>
|
tooltip?: string | TypeWithI18N | Record<Locale, string>
|
||||||
show_on?: FormShowOnObject[]
|
show_on?: FormShowOnObject[] | ((form: AnyFormApi) => FormShowOnObject[])
|
||||||
|
more_on?: FormShowOnObject[] | ((form: AnyFormApi) => FormShowOnObject[])
|
||||||
url?: string
|
url?: string
|
||||||
scope?: string
|
scope?: string
|
||||||
help?: string | TypeWithI18N | Record<Locale, string>
|
help?: string | TypeWithI18N | Record<Locale, string>
|
||||||
placeholder?: string | TypeWithI18N | Record<Locale, string>
|
placeholder?: string | TypeWithI18N | Record<Locale, string>
|
||||||
options?: FormOption[]
|
options?: FormOption[]
|
||||||
labelClassName?: string
|
|
||||||
fieldClassName?: string
|
fieldClassName?: string
|
||||||
|
labelClassName?: string
|
||||||
|
inputContainerClassName?: string
|
||||||
|
inputClassName?: string
|
||||||
validators?: AnyValidators
|
validators?: AnyValidators
|
||||||
|
selfFormProps?: ((form: AnyFormApi) => Record<string, any>) | Record<string, any>
|
||||||
|
onChange?: (form: AnyFormApi, v: any) => void
|
||||||
showRadioUI?: boolean
|
showRadioUI?: boolean
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
showCopy?: boolean
|
showCopy?: boolean
|
||||||
|
|||||||
@ -0,0 +1,3 @@
|
|||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 1.9 KiB |
@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
// 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
|
||||||
@ -6,5 +6,6 @@ export { default as GlobalVariable } from './GlobalVariable'
|
|||||||
export { default as Icon3Dots } from './Icon3Dots'
|
export { default as Icon3Dots } from './Icon3Dots'
|
||||||
export { default as LongArrowLeft } from './LongArrowLeft'
|
export { default as LongArrowLeft } from './LongArrowLeft'
|
||||||
export { default as LongArrowRight } from './LongArrowRight'
|
export { default as LongArrowRight } from './LongArrowRight'
|
||||||
|
export { default as Memory } from './Memory'
|
||||||
export { default as SearchMenu } from './SearchMenu'
|
export { default as SearchMenu } from './SearchMenu'
|
||||||
export { default as Tools } from './Tools'
|
export { default as Tools } from './Tools'
|
||||||
|
|||||||
@ -10,6 +10,7 @@ export const LAST_RUN_PLACEHOLDER_TEXT = '{{#last_run#}}'
|
|||||||
export const PRE_PROMPT_PLACEHOLDER_TEXT = '{{#pre_prompt#}}'
|
export const PRE_PROMPT_PLACEHOLDER_TEXT = '{{#pre_prompt#}}'
|
||||||
export const UPDATE_DATASETS_EVENT_EMITTER = 'prompt-editor-context-block-update-datasets'
|
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_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) => {
|
export const checkHasContextBlock = (text: string) => {
|
||||||
if (!text)
|
if (!text)
|
||||||
|
|||||||
@ -61,6 +61,8 @@ import { VariableValueBlockNode } from './plugins/variable-value-block/node'
|
|||||||
import { CustomTextNode } from './plugins/custom-text/node'
|
import { CustomTextNode } from './plugins/custom-text/node'
|
||||||
import OnBlurBlock from './plugins/on-blur-or-focus-block'
|
import OnBlurBlock from './plugins/on-blur-or-focus-block'
|
||||||
import UpdateBlock from './plugins/update-block'
|
import UpdateBlock from './plugins/update-block'
|
||||||
|
import MemoryPopupPlugin from './plugins/memory-popup-plugin'
|
||||||
|
|
||||||
import { textToEditorState } from './utils'
|
import { textToEditorState } from './utils'
|
||||||
import type {
|
import type {
|
||||||
ContextBlockType,
|
ContextBlockType,
|
||||||
@ -76,9 +78,11 @@ import type {
|
|||||||
import {
|
import {
|
||||||
UPDATE_DATASETS_EVENT_EMITTER,
|
UPDATE_DATASETS_EVENT_EMITTER,
|
||||||
UPDATE_HISTORY_EVENT_EMITTER,
|
UPDATE_HISTORY_EVENT_EMITTER,
|
||||||
|
UPDATE_WORKFLOW_VARIABLES_EVENT_EMITTER,
|
||||||
} from './constants'
|
} from './constants'
|
||||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||||
import cn from '@/utils/classnames'
|
import cn from '@/utils/classnames'
|
||||||
|
import PromptEditorProvider from './store/provider'
|
||||||
|
|
||||||
export type PromptEditorProps = {
|
export type PromptEditorProps = {
|
||||||
instanceId?: string
|
instanceId?: string
|
||||||
@ -174,6 +178,13 @@ const PromptEditor: FC<PromptEditorProps> = ({
|
|||||||
payload: historyBlock?.history,
|
payload: historyBlock?.history,
|
||||||
} as any)
|
} as any)
|
||||||
}, [eventEmitter, historyBlock?.history])
|
}, [eventEmitter, historyBlock?.history])
|
||||||
|
useEffect(() => {
|
||||||
|
eventEmitter?.emit({
|
||||||
|
type: UPDATE_WORKFLOW_VARIABLES_EVENT_EMITTER,
|
||||||
|
payload: workflowVariableBlock?.variables,
|
||||||
|
instanceId,
|
||||||
|
} as any)
|
||||||
|
}, [eventEmitter, workflowVariableBlock?.variables])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LexicalComposer initialConfig={{ ...initialConfig, editable }}>
|
<LexicalComposer initialConfig={{ ...initialConfig, editable }}>
|
||||||
@ -198,6 +209,12 @@ const PromptEditor: FC<PromptEditorProps> = ({
|
|||||||
}
|
}
|
||||||
ErrorBoundary={LexicalErrorBoundary}
|
ErrorBoundary={LexicalErrorBoundary}
|
||||||
/>
|
/>
|
||||||
|
{workflowVariableBlock?.show && workflowVariableBlock?.isMemorySupported && (
|
||||||
|
<MemoryPopupPlugin
|
||||||
|
instanceId={instanceId}
|
||||||
|
memoryVariables={workflowVariableBlock?.variables?.find(v => v.nodeId === 'memory_block')?.vars || []}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<ComponentPickerBlock
|
<ComponentPickerBlock
|
||||||
triggerString='/'
|
triggerString='/'
|
||||||
contextBlock={contextBlock}
|
contextBlock={contextBlock}
|
||||||
@ -303,4 +320,12 @@ const PromptEditor: FC<PromptEditorProps> = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default PromptEditor
|
const PromptEditorWithProvider = ({ instanceId, ...props }: PromptEditorProps) => {
|
||||||
|
return (
|
||||||
|
<PromptEditorProvider instanceId={instanceId}>
|
||||||
|
<PromptEditor {...props} instanceId={instanceId} />
|
||||||
|
</PromptEditorProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PromptEditorWithProvider
|
||||||
|
|||||||
@ -283,7 +283,19 @@ export const useOptions = (
|
|||||||
const workflowVariableOptions = useMemo(() => {
|
const workflowVariableOptions = useMemo(() => {
|
||||||
if (!workflowVariableBlockType?.show)
|
if (!workflowVariableBlockType?.show)
|
||||||
return []
|
return []
|
||||||
const res = workflowVariableBlockType.variables || []
|
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
|
||||||
|
})
|
||||||
|
}
|
||||||
if(errorMessageBlockType?.show && res.findIndex(v => v.nodeId === 'error_message') === -1) {
|
if(errorMessageBlockType?.show && res.findIndex(v => v.nodeId === 'error_message') === -1) {
|
||||||
res.unshift({
|
res.unshift({
|
||||||
nodeId: 'error_message',
|
nodeId: 'error_message',
|
||||||
|
|||||||
@ -0,0 +1,273 @@
|
|||||||
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -1,8 +1,11 @@
|
|||||||
import { $insertNodes } from 'lexical'
|
import {
|
||||||
|
$insertNodes,
|
||||||
|
} from 'lexical'
|
||||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||||
import { textToEditorState } from '../utils'
|
import { textToEditorState } from '../utils'
|
||||||
import { CustomTextNode } from './custom-text/node'
|
import { CustomTextNode } from './custom-text/node'
|
||||||
import { CLEAR_HIDE_MENU_TIMEOUT } from './workflow-variable-block'
|
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'
|
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||||
|
|
||||||
export const PROMPT_EDITOR_UPDATE_VALUE_BY_EVENT_EMITTER = 'PROMPT_EDITOR_UPDATE_VALUE_BY_EVENT_EMITTER'
|
export const PROMPT_EDITOR_UPDATE_VALUE_BY_EVENT_EMITTER = 'PROMPT_EDITOR_UPDATE_VALUE_BY_EVENT_EMITTER'
|
||||||
@ -36,6 +39,18 @@ 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
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -19,23 +19,27 @@ import {
|
|||||||
DELETE_WORKFLOW_VARIABLE_BLOCK_COMMAND,
|
DELETE_WORKFLOW_VARIABLE_BLOCK_COMMAND,
|
||||||
UPDATE_WORKFLOW_NODES_MAP,
|
UPDATE_WORKFLOW_NODES_MAP,
|
||||||
} from './index'
|
} from './index'
|
||||||
import { isConversationVar, isENV, isGlobalVar, isRagVariableVar, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
|
import { isConversationVar, isENV, isGlobalVar, isMemoryVariable, isRagVariableVar, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
|
||||||
import Tooltip from '@/app/components/base/tooltip'
|
import Tooltip from '@/app/components/base/tooltip'
|
||||||
import { isExceptionVariable } from '@/app/components/workflow/utils'
|
import { isExceptionVariable } from '@/app/components/workflow/utils'
|
||||||
import VarFullPathPanel from '@/app/components/workflow/nodes/_base/components/variable/var-full-path-panel'
|
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 } from '@/app/components/workflow/nodes/llm/types'
|
||||||
import type { ValueSelector, Var } from '@/app/components/workflow/types'
|
import type {
|
||||||
|
NodeOutPutVar,
|
||||||
|
ValueSelector,
|
||||||
|
} from '@/app/components/workflow/types'
|
||||||
import {
|
import {
|
||||||
VariableLabelInEditor,
|
VariableLabelInEditor,
|
||||||
} from '@/app/components/workflow/nodes/_base/components/variable/variable-label'
|
} 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 = {
|
type WorkflowVariableBlockComponentProps = {
|
||||||
nodeKey: string
|
nodeKey: string
|
||||||
variables: string[]
|
variables: string[]
|
||||||
workflowNodesMap: WorkflowNodesMap
|
workflowNodesMap: WorkflowNodesMap
|
||||||
environmentVariables?: Var[]
|
availableVariables: NodeOutPutVar[]
|
||||||
conversationVariables?: Var[]
|
|
||||||
ragVariables?: Var[]
|
|
||||||
getVarType?: (payload: {
|
getVarType?: (payload: {
|
||||||
nodeId: string,
|
nodeId: string,
|
||||||
valueSelector: ValueSelector,
|
valueSelector: ValueSelector,
|
||||||
@ -47,12 +51,27 @@ const WorkflowVariableBlockComponent = ({
|
|||||||
variables,
|
variables,
|
||||||
workflowNodesMap = {},
|
workflowNodesMap = {},
|
||||||
getVarType,
|
getVarType,
|
||||||
environmentVariables,
|
availableVariables: initialAvailableVariables,
|
||||||
conversationVariables,
|
|
||||||
ragVariables,
|
|
||||||
}: WorkflowVariableBlockComponentProps) => {
|
}: WorkflowVariableBlockComponentProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [editor] = useLexicalComposerContext()
|
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 [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_WORKFLOW_VARIABLE_BLOCK_COMMAND)
|
||||||
const variablesLength = variables.length
|
const variablesLength = variables.length
|
||||||
const isRagVar = isRagVariableVar(variables)
|
const isRagVar = isRagVariableVar(variables)
|
||||||
@ -72,21 +91,23 @@ const WorkflowVariableBlockComponent = ({
|
|||||||
let variableValid = true
|
let variableValid = true
|
||||||
const isEnv = isENV(variables)
|
const isEnv = isENV(variables)
|
||||||
const isChatVar = isConversationVar(variables)
|
const isChatVar = isConversationVar(variables)
|
||||||
|
const isMemoryVar = isMemoryVariable(variables)
|
||||||
const isGlobal = isGlobalVar(variables)
|
const isGlobal = isGlobalVar(variables)
|
||||||
if (isGlobal)
|
if (isGlobal)
|
||||||
return true
|
return true
|
||||||
|
|
||||||
if (isEnv) {
|
if (isEnv) {
|
||||||
if (environmentVariables)
|
variableValid
|
||||||
variableValid = environmentVariables.some(v => v.variable === `${variables?.[0] ?? ''}.${variables?.[1] ?? ''}`)
|
= environmentVariables.some(v => v.variable === `${variables?.[0] ?? ''}.${variables?.[1] ?? ''}`)
|
||||||
}
|
}
|
||||||
else if (isChatVar) {
|
else if (isChatVar) {
|
||||||
if (conversationVariables)
|
variableValid = conversationVariables.some(v => v.variable === `${variables?.[0] ?? ''}.${variables?.[1] ?? ''}`)
|
||||||
variableValid = conversationVariables.some(v => v.variable === `${variables?.[0] ?? ''}.${variables?.[1] ?? ''}`)
|
}
|
||||||
|
else if (isMemoryVar) {
|
||||||
|
variableValid = memoryVariables.some(v => v.variable === `${variables?.[0] ?? ''}.${variables?.[1] ?? ''}`)
|
||||||
}
|
}
|
||||||
else if (isRagVar) {
|
else if (isRagVar) {
|
||||||
if (ragVariables)
|
variableValid = ragVariables.some(v => v.variable === `${variables?.[0] ?? ''}.${variables?.[1] ?? ''}.${variables?.[2] ?? ''}`)
|
||||||
variableValid = ragVariables.some(v => v.variable === `${variables?.[0] ?? ''}.${variables?.[1] ?? ''}.${variables?.[2] ?? ''}`)
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
variableValid = !!node
|
variableValid = !!node
|
||||||
@ -134,11 +155,28 @@ const WorkflowVariableBlockComponent = ({
|
|||||||
})
|
})
|
||||||
}, [node, reactflow, store])
|
}, [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 = (
|
const Item = (
|
||||||
<VariableLabelInEditor
|
<VariableLabelInEditor
|
||||||
nodeType={node?.type}
|
nodeType={node?.type}
|
||||||
nodeTitle={node?.title}
|
nodeTitle={node?.title}
|
||||||
variables={variables}
|
variables={memoriedVariables}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
handleVariableJump()
|
handleVariableJump()
|
||||||
@ -152,7 +190,7 @@ const WorkflowVariableBlockComponent = ({
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (!node)
|
if (!node)
|
||||||
return Item
|
return <div>{Item}</div>
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
@ -160,10 +198,10 @@ const WorkflowVariableBlockComponent = ({
|
|||||||
popupContent={
|
popupContent={
|
||||||
<VarFullPathPanel
|
<VarFullPathPanel
|
||||||
nodeName={node.title}
|
nodeName={node.title}
|
||||||
path={variables.slice(1)}
|
path={memoriedVariables.slice(1)}
|
||||||
varType={getVarType ? getVarType({
|
varType={getVarType ? getVarType({
|
||||||
nodeId: variables[0],
|
nodeId: memoriedVariables[0],
|
||||||
valueSelector: variables,
|
valueSelector: memoriedVariables,
|
||||||
}) : Type.string}
|
}) : Type.string}
|
||||||
nodeType={node?.type}
|
nodeType={node?.type}
|
||||||
/>}
|
/>}
|
||||||
|
|||||||
@ -32,6 +32,7 @@ const WorkflowVariableBlock = memo(({
|
|||||||
onInsert,
|
onInsert,
|
||||||
onDelete,
|
onDelete,
|
||||||
getVarType,
|
getVarType,
|
||||||
|
variables: originalVariables,
|
||||||
}: WorkflowVariableBlockType) => {
|
}: WorkflowVariableBlockType) => {
|
||||||
const [editor] = useLexicalComposerContext()
|
const [editor] = useLexicalComposerContext()
|
||||||
|
|
||||||
@ -50,7 +51,7 @@ const WorkflowVariableBlock = memo(({
|
|||||||
INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND,
|
INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND,
|
||||||
(variables: string[]) => {
|
(variables: string[]) => {
|
||||||
editor.dispatchCommand(CLEAR_HIDE_MENU_TIMEOUT, undefined)
|
editor.dispatchCommand(CLEAR_HIDE_MENU_TIMEOUT, undefined)
|
||||||
const workflowVariableBlockNode = $createWorkflowVariableBlockNode(variables, workflowNodesMap, getVarType)
|
const workflowVariableBlockNode = $createWorkflowVariableBlockNode(variables, workflowNodesMap, getVarType, originalVariables || [])
|
||||||
|
|
||||||
$insertNodes([workflowVariableBlockNode])
|
$insertNodes([workflowVariableBlockNode])
|
||||||
if (onInsert)
|
if (onInsert)
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { DecoratorNode } from 'lexical'
|
|||||||
import type { WorkflowVariableBlockType } from '../../types'
|
import type { WorkflowVariableBlockType } from '../../types'
|
||||||
import WorkflowVariableBlockComponent from './component'
|
import WorkflowVariableBlockComponent from './component'
|
||||||
import type { GetVarType } from '../../types'
|
import type { GetVarType } from '../../types'
|
||||||
import type { Var } from '@/app/components/workflow/types'
|
import type { NodeOutPutVar } from '@/app/components/workflow/types'
|
||||||
|
|
||||||
export type WorkflowNodesMap = WorkflowVariableBlockType['workflowNodesMap']
|
export type WorkflowNodesMap = WorkflowVariableBlockType['workflowNodesMap']
|
||||||
|
|
||||||
@ -11,40 +11,40 @@ export type SerializedNode = SerializedLexicalNode & {
|
|||||||
variables: string[]
|
variables: string[]
|
||||||
workflowNodesMap: WorkflowNodesMap
|
workflowNodesMap: WorkflowNodesMap
|
||||||
getVarType?: GetVarType
|
getVarType?: GetVarType
|
||||||
environmentVariables?: Var[]
|
availableVariables?: NodeOutPutVar[]
|
||||||
conversationVariables?: Var[]
|
|
||||||
ragVariables?: Var[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class WorkflowVariableBlockNode extends DecoratorNode<React.JSX.Element> {
|
export class WorkflowVariableBlockNode extends DecoratorNode<React.JSX.Element> {
|
||||||
__variables: string[]
|
__variables: string[]
|
||||||
__workflowNodesMap: WorkflowNodesMap
|
__workflowNodesMap: WorkflowNodesMap
|
||||||
__getVarType?: GetVarType
|
__getVarType?: GetVarType
|
||||||
__environmentVariables?: Var[]
|
__availableVariables?: NodeOutPutVar[]
|
||||||
__conversationVariables?: Var[]
|
|
||||||
__ragVariables?: Var[]
|
|
||||||
|
|
||||||
static getType(): string {
|
static getType(): string {
|
||||||
return 'workflow-variable-block'
|
return 'workflow-variable-block'
|
||||||
}
|
}
|
||||||
|
|
||||||
static clone(node: WorkflowVariableBlockNode): WorkflowVariableBlockNode {
|
static clone(node: WorkflowVariableBlockNode): WorkflowVariableBlockNode {
|
||||||
return new WorkflowVariableBlockNode(node.__variables, node.__workflowNodesMap, node.__getVarType, node.__key, node.__environmentVariables, node.__conversationVariables, node.__ragVariables)
|
return new WorkflowVariableBlockNode(node.__variables, node.__workflowNodesMap, node.__getVarType, node.__key, node.__availableVariables)
|
||||||
}
|
}
|
||||||
|
|
||||||
isInline(): boolean {
|
isInline(): boolean {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(variables: string[], workflowNodesMap: WorkflowNodesMap, getVarType: any, key?: NodeKey, environmentVariables?: Var[], conversationVariables?: Var[], ragVariables?: Var[]) {
|
constructor(
|
||||||
|
variables: string[],
|
||||||
|
workflowNodesMap: WorkflowNodesMap,
|
||||||
|
getVarType: any,
|
||||||
|
key?: NodeKey,
|
||||||
|
availableVariables?: NodeOutPutVar[],
|
||||||
|
) {
|
||||||
super(key)
|
super(key)
|
||||||
|
|
||||||
this.__variables = variables
|
this.__variables = variables
|
||||||
this.__workflowNodesMap = workflowNodesMap
|
this.__workflowNodesMap = workflowNodesMap
|
||||||
this.__getVarType = getVarType
|
this.__getVarType = getVarType
|
||||||
this.__environmentVariables = environmentVariables
|
this.__availableVariables = availableVariables
|
||||||
this.__conversationVariables = conversationVariables
|
|
||||||
this.__ragVariables = ragVariables
|
|
||||||
}
|
}
|
||||||
|
|
||||||
createDOM(): HTMLElement {
|
createDOM(): HTMLElement {
|
||||||
@ -64,15 +64,13 @@ export class WorkflowVariableBlockNode extends DecoratorNode<React.JSX.Element>
|
|||||||
variables={this.__variables}
|
variables={this.__variables}
|
||||||
workflowNodesMap={this.__workflowNodesMap}
|
workflowNodesMap={this.__workflowNodesMap}
|
||||||
getVarType={this.__getVarType!}
|
getVarType={this.__getVarType!}
|
||||||
environmentVariables={this.__environmentVariables}
|
availableVariables={this.__availableVariables || []}
|
||||||
conversationVariables={this.__conversationVariables}
|
|
||||||
ragVariables={this.__ragVariables}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
static importJSON(serializedNode: SerializedNode): WorkflowVariableBlockNode {
|
static importJSON(serializedNode: SerializedNode): WorkflowVariableBlockNode {
|
||||||
const node = $createWorkflowVariableBlockNode(serializedNode.variables, serializedNode.workflowNodesMap, serializedNode.getVarType, serializedNode.environmentVariables, serializedNode.conversationVariables, serializedNode.ragVariables)
|
const node = $createWorkflowVariableBlockNode(serializedNode.variables, serializedNode.workflowNodesMap, serializedNode.getVarType, serializedNode.availableVariables)
|
||||||
|
|
||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
@ -84,9 +82,7 @@ export class WorkflowVariableBlockNode extends DecoratorNode<React.JSX.Element>
|
|||||||
variables: this.getVariables(),
|
variables: this.getVariables(),
|
||||||
workflowNodesMap: this.getWorkflowNodesMap(),
|
workflowNodesMap: this.getWorkflowNodesMap(),
|
||||||
getVarType: this.getVarType(),
|
getVarType: this.getVarType(),
|
||||||
environmentVariables: this.getEnvironmentVariables(),
|
availableVariables: this.getAvailableVariables(),
|
||||||
conversationVariables: this.getConversationVariables(),
|
|
||||||
ragVariables: this.getRagVariables(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,27 +101,17 @@ export class WorkflowVariableBlockNode extends DecoratorNode<React.JSX.Element>
|
|||||||
return self.__getVarType
|
return self.__getVarType
|
||||||
}
|
}
|
||||||
|
|
||||||
getEnvironmentVariables(): any {
|
getAvailableVariables(): NodeOutPutVar[] {
|
||||||
const self = this.getLatest()
|
const self = this.getLatest()
|
||||||
return self.__environmentVariables
|
return self.__availableVariables || []
|
||||||
}
|
|
||||||
|
|
||||||
getConversationVariables(): any {
|
|
||||||
const self = this.getLatest()
|
|
||||||
return self.__conversationVariables
|
|
||||||
}
|
|
||||||
|
|
||||||
getRagVariables(): any {
|
|
||||||
const self = this.getLatest()
|
|
||||||
return self.__ragVariables
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getTextContent(): string {
|
getTextContent(): string {
|
||||||
return `{{#${this.getVariables().join('.')}#}}`
|
return `{{#${this.getVariables().join('.')}#}}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export function $createWorkflowVariableBlockNode(variables: string[], workflowNodesMap: WorkflowNodesMap, getVarType?: GetVarType, environmentVariables?: Var[], conversationVariables?: Var[], ragVariables?: Var[]): WorkflowVariableBlockNode {
|
export function $createWorkflowVariableBlockNode(variables: string[], workflowNodesMap: WorkflowNodesMap, getVarType?: GetVarType, availableVariables?: NodeOutPutVar[]): WorkflowVariableBlockNode {
|
||||||
return new WorkflowVariableBlockNode(variables, workflowNodesMap, getVarType, undefined, environmentVariables, conversationVariables, ragVariables)
|
return new WorkflowVariableBlockNode(variables, workflowNodesMap, getVarType, undefined, availableVariables)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function $isWorkflowVariableBlockNode(
|
export function $isWorkflowVariableBlockNode(
|
||||||
|
|||||||
@ -21,13 +21,6 @@ const WorkflowVariableBlockReplacementBlock = ({
|
|||||||
variables,
|
variables,
|
||||||
}: WorkflowVariableBlockType) => {
|
}: WorkflowVariableBlockType) => {
|
||||||
const [editor] = useLexicalComposerContext()
|
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(() => {
|
useEffect(() => {
|
||||||
if (!editor.hasNodes([WorkflowVariableBlockNode]))
|
if (!editor.hasNodes([WorkflowVariableBlockNode]))
|
||||||
@ -39,7 +32,7 @@ const WorkflowVariableBlockReplacementBlock = ({
|
|||||||
onInsert()
|
onInsert()
|
||||||
|
|
||||||
const nodePathString = textNode.getTextContent().slice(3, -3)
|
const nodePathString = textNode.getTextContent().slice(3, -3)
|
||||||
return $applyNodeReplacement($createWorkflowVariableBlockNode(nodePathString.split('.'), workflowNodesMap, getVarType, variables?.find(o => o.nodeId === 'env')?.vars || [], variables?.find(o => o.nodeId === 'conversation')?.vars || [], ragVariables))
|
return $applyNodeReplacement($createWorkflowVariableBlockNode(nodePathString.split('.'), workflowNodesMap, getVarType, variables))
|
||||||
}, [onInsert, workflowNodesMap, getVarType, variables])
|
}, [onInsert, workflowNodesMap, getVarType, variables])
|
||||||
|
|
||||||
const getMatch = useCallback((text: string) => {
|
const getMatch = useCallback((text: string) => {
|
||||||
|
|||||||
31
web/app/components/base/prompt-editor/store/provider.tsx
Normal file
31
web/app/components/base/prompt-editor/store/provider.tsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
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
|
||||||
24
web/app/components/base/prompt-editor/store/store.ts
Normal file
24
web/app/components/base/prompt-editor/store/store.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
@ -71,6 +71,7 @@ export type WorkflowVariableBlockType = {
|
|||||||
getVarType?: GetVarType
|
getVarType?: GetVarType
|
||||||
showManageInputField?: boolean
|
showManageInputField?: boolean
|
||||||
onManageInputField?: () => void
|
onManageInputField?: () => void
|
||||||
|
isMemorySupported?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MenuTextMatch = {
|
export type MenuTextMatch = {
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import {
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { RiDeleteBinLine } from '@remixicon/react'
|
import { RiDeleteBinLine } from '@remixicon/react'
|
||||||
import { PlusIcon, XMarkIcon } from '@heroicons/react/20/solid'
|
import { PlusIcon, XMarkIcon } from '@heroicons/react/20/solid'
|
||||||
import useSWR from 'swr'
|
import useSWR, { useSWRConfig } from 'swr'
|
||||||
import SecretKeyGenerateModal from './secret-key-generate'
|
import SecretKeyGenerateModal from './secret-key-generate'
|
||||||
import s from './style.module.css'
|
import s from './style.module.css'
|
||||||
import ActionButton from '@/app/components/base/action-button'
|
import ActionButton from '@/app/components/base/action-button'
|
||||||
@ -15,6 +15,7 @@ import CopyFeedback from '@/app/components/base/copy-feedback'
|
|||||||
import {
|
import {
|
||||||
createApikey as createAppApikey,
|
createApikey as createAppApikey,
|
||||||
delApikey as delAppApikey,
|
delApikey as delAppApikey,
|
||||||
|
fetchApiKeysList as fetchAppApiKeysList,
|
||||||
} from '@/service/apps'
|
} from '@/service/apps'
|
||||||
import {
|
import {
|
||||||
createApikey as createDatasetApikey,
|
createApikey as createDatasetApikey,
|
||||||
@ -26,7 +27,6 @@ import Loading from '@/app/components/base/loading'
|
|||||||
import Confirm from '@/app/components/base/confirm'
|
import Confirm from '@/app/components/base/confirm'
|
||||||
import useTimestamp from '@/hooks/use-timestamp'
|
import useTimestamp from '@/hooks/use-timestamp'
|
||||||
import { useAppContext } from '@/context/app-context'
|
import { useAppContext } from '@/context/app-context'
|
||||||
import { useAppApiKeys, useInvalidateAppApiKeys } from '@/service/use-apps'
|
|
||||||
|
|
||||||
type ISecretKeyModalProps = {
|
type ISecretKeyModalProps = {
|
||||||
isShow: boolean
|
isShow: boolean
|
||||||
@ -45,14 +45,12 @@ const SecretKeyModal = ({
|
|||||||
const [showConfirmDelete, setShowConfirmDelete] = useState(false)
|
const [showConfirmDelete, setShowConfirmDelete] = useState(false)
|
||||||
const [isVisible, setVisible] = useState(false)
|
const [isVisible, setVisible] = useState(false)
|
||||||
const [newKey, setNewKey] = useState<CreateApiKeyResponse | undefined>(undefined)
|
const [newKey, setNewKey] = useState<CreateApiKeyResponse | undefined>(undefined)
|
||||||
const invalidateAppApiKeys = useInvalidateAppApiKeys()
|
const { mutate } = useSWRConfig()
|
||||||
const { data: appApiKeys, isLoading: isAppApiKeysLoading } = useAppApiKeys(appId, { enabled: !!appId && isShow })
|
const commonParams = appId
|
||||||
const { data: datasetApiKeys, isLoading: isDatasetApiKeysLoading, mutate: mutateDatasetApiKeys } = useSWR(
|
? { url: `/apps/${appId}/api-keys`, params: {} }
|
||||||
!appId && isShow ? { url: '/datasets/api-keys', params: {} } : null,
|
: { url: '/datasets/api-keys', params: {} }
|
||||||
fetchDatasetApiKeysList,
|
const fetchApiKeysList = appId ? fetchAppApiKeysList : fetchDatasetApiKeysList
|
||||||
)
|
const { data: apiKeysList } = useSWR(commonParams, fetchApiKeysList)
|
||||||
const apiKeysList = appId ? appApiKeys : datasetApiKeys
|
|
||||||
const isApiKeysLoading = appId ? isAppApiKeysLoading : isDatasetApiKeysLoading
|
|
||||||
|
|
||||||
const [delKeyID, setDelKeyId] = useState('')
|
const [delKeyID, setDelKeyId] = useState('')
|
||||||
|
|
||||||
@ -66,10 +64,7 @@ const SecretKeyModal = ({
|
|||||||
? { url: `/apps/${appId}/api-keys/${delKeyID}`, params: {} }
|
? { url: `/apps/${appId}/api-keys/${delKeyID}`, params: {} }
|
||||||
: { url: `/datasets/api-keys/${delKeyID}`, params: {} }
|
: { url: `/datasets/api-keys/${delKeyID}`, params: {} }
|
||||||
await delApikey(params)
|
await delApikey(params)
|
||||||
if (appId)
|
mutate(commonParams)
|
||||||
invalidateAppApiKeys(appId)
|
|
||||||
else
|
|
||||||
mutateDatasetApiKeys()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const onCreate = async () => {
|
const onCreate = async () => {
|
||||||
@ -80,10 +75,7 @@ const SecretKeyModal = ({
|
|||||||
const res = await createApikey(params)
|
const res = await createApikey(params)
|
||||||
setVisible(true)
|
setVisible(true)
|
||||||
setNewKey(res)
|
setNewKey(res)
|
||||||
if (appId)
|
mutate(commonParams)
|
||||||
invalidateAppApiKeys(appId)
|
|
||||||
else
|
|
||||||
mutateDatasetApiKeys()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const generateToken = (token: string) => {
|
const generateToken = (token: string) => {
|
||||||
@ -96,7 +88,7 @@ const SecretKeyModal = ({
|
|||||||
<XMarkIcon className="h-6 w-6 cursor-pointer text-text-tertiary" onClick={onClose} />
|
<XMarkIcon className="h-6 w-6 cursor-pointer text-text-tertiary" onClick={onClose} />
|
||||||
</div>
|
</div>
|
||||||
<p className='mt-1 shrink-0 text-[13px] font-normal leading-5 text-text-tertiary'>{t('appApi.apiKeyModal.apiSecretKeyTips')}</p>
|
<p className='mt-1 shrink-0 text-[13px] font-normal leading-5 text-text-tertiary'>{t('appApi.apiKeyModal.apiSecretKeyTips')}</p>
|
||||||
{isApiKeysLoading && <div className='mt-4'><Loading /></div>}
|
{!apiKeysList && <div className='mt-4'><Loading /></div>}
|
||||||
{
|
{
|
||||||
!!apiKeysList?.data?.length && (
|
!!apiKeysList?.data?.length && (
|
||||||
<div className='mt-4 flex grow flex-col overflow-hidden'>
|
<div className='mt-4 flex grow flex-col overflow-hidden'>
|
||||||
|
|||||||
@ -214,12 +214,8 @@ export const searchAnything = async (
|
|||||||
actionItem?: ActionItem,
|
actionItem?: ActionItem,
|
||||||
dynamicActions?: Record<string, ActionItem>,
|
dynamicActions?: Record<string, ActionItem>,
|
||||||
): Promise<SearchResult[]> => {
|
): Promise<SearchResult[]> => {
|
||||||
const trimmedQuery = query.trim()
|
|
||||||
|
|
||||||
if (actionItem) {
|
if (actionItem) {
|
||||||
const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
const searchTerm = query.replace(actionItem.key, '').replace(actionItem.shortcut, '').trim()
|
||||||
const prefixPattern = new RegExp(`^(${escapeRegExp(actionItem.key)}|${escapeRegExp(actionItem.shortcut)})\\s*`)
|
|
||||||
const searchTerm = trimmedQuery.replace(prefixPattern, '').trim()
|
|
||||||
try {
|
try {
|
||||||
return await actionItem.search(query, searchTerm, locale)
|
return await actionItem.search(query, searchTerm, locale)
|
||||||
}
|
}
|
||||||
@ -229,12 +225,10 @@ export const searchAnything = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (trimmedQuery.startsWith('@') || trimmedQuery.startsWith('/'))
|
if (query.startsWith('@') || query.startsWith('/'))
|
||||||
return []
|
return []
|
||||||
|
|
||||||
const globalSearchActions = Object.values(dynamicActions || Actions)
|
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
|
// Use Promise.allSettled to handle partial failures gracefully
|
||||||
const searchPromises = globalSearchActions.map(async (action) => {
|
const searchPromises = globalSearchActions.map(async (action) => {
|
||||||
|
|||||||
@ -177,42 +177,31 @@ const GotoAnything: FC<Props> = ({
|
|||||||
}
|
}
|
||||||
}, [router])
|
}, [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
|
// Group results by type
|
||||||
const groupedResults = useMemo(() => dedupedResults.reduce((acc, result) => {
|
const groupedResults = useMemo(() => searchResults.reduce((acc, result) => {
|
||||||
if (!acc[result.type])
|
if (!acc[result.type])
|
||||||
acc[result.type] = []
|
acc[result.type] = []
|
||||||
|
|
||||||
acc[result.type].push(result)
|
acc[result.type].push(result)
|
||||||
return acc
|
return acc
|
||||||
}, {} as { [key: string]: SearchResult[] }),
|
}, {} as { [key: string]: SearchResult[] }),
|
||||||
[dedupedResults])
|
[searchResults])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isCommandsMode)
|
if (isCommandsMode)
|
||||||
return
|
return
|
||||||
|
|
||||||
if (!dedupedResults.length)
|
if (!searchResults.length)
|
||||||
return
|
return
|
||||||
|
|
||||||
const currentValueExists = dedupedResults.some(result => `${result.type}-${result.id}` === cmdVal)
|
const currentValueExists = searchResults.some(result => `${result.type}-${result.id}` === cmdVal)
|
||||||
|
|
||||||
if (!currentValueExists)
|
if (!currentValueExists)
|
||||||
setCmdVal(`${dedupedResults[0].type}-${dedupedResults[0].id}`)
|
setCmdVal(`${searchResults[0].type}-${searchResults[0].id}`)
|
||||||
}, [isCommandsMode, dedupedResults, cmdVal])
|
}, [isCommandsMode, searchResults, cmdVal])
|
||||||
|
|
||||||
const emptyResult = useMemo(() => {
|
const emptyResult = useMemo(() => {
|
||||||
if (dedupedResults.length || !searchQuery.trim() || isLoading || isCommandsMode)
|
if (searchResults.length || !searchQuery.trim() || isLoading || isCommandsMode)
|
||||||
return null
|
return null
|
||||||
|
|
||||||
const isCommandSearch = searchMode !== 'general'
|
const isCommandSearch = searchMode !== 'general'
|
||||||
@ -257,7 +246,7 @@ const GotoAnything: FC<Props> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}, [dedupedResults, searchQuery, Actions, searchMode, isLoading, isError, isCommandsMode])
|
}, [searchResults, searchQuery, Actions, searchMode, isLoading, isError, isCommandsMode])
|
||||||
|
|
||||||
const defaultUI = useMemo(() => {
|
const defaultUI = useMemo(() => {
|
||||||
if (searchQuery.trim())
|
if (searchQuery.trim())
|
||||||
@ -441,14 +430,14 @@ const GotoAnything: FC<Props> = ({
|
|||||||
{/* Always show footer to prevent height jumping */}
|
{/* 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='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'>
|
<div className='flex min-h-[16px] items-center justify-between'>
|
||||||
{(!!dedupedResults.length || isError) ? (
|
{(!!searchResults.length || isError) ? (
|
||||||
<>
|
<>
|
||||||
<span>
|
<span>
|
||||||
{isError ? (
|
{isError ? (
|
||||||
<span className='text-red-500'>{t('app.gotoAnything.someServicesUnavailable')}</span>
|
<span className='text-red-500'>{t('app.gotoAnything.someServicesUnavailable')}</span>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{t('app.gotoAnything.resultCount', { count: dedupedResults.length })}
|
{t('app.gotoAnything.resultCount', { count: searchResults.length })}
|
||||||
{searchMode !== 'general' && (
|
{searchMode !== 'general' && (
|
||||||
<span className='ml-2 opacity-60'>
|
<span className='ml-2 opacity-60'>
|
||||||
{t('app.gotoAnything.inScope', { scope: searchMode.replace('@', '') })}
|
{t('app.gotoAnything.inScope', { scope: searchMode.replace('@', '') })}
|
||||||
|
|||||||
@ -129,8 +129,8 @@ export const useProviderCredentialsAndLoadBalancing = (
|
|||||||
// as ([Record<string, string | boolean | undefined> | undefined, ModelLoadBalancingConfig | undefined])
|
// as ([Record<string, string | boolean | undefined> | undefined, ModelLoadBalancingConfig | undefined])
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useModelList = (type: ModelTypeEnum) => {
|
export const useModelList = (type: ModelTypeEnum, enabled?: boolean) => {
|
||||||
const { data, mutate, isLoading } = useSWR(`/workspaces/current/models/model-types/${type}`, fetchModelList)
|
const { data, mutate, isLoading } = useSWR(enabled ? `/workspaces/current/models/model-types/${type}` : null, fetchModelList)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: data?.data || [],
|
data: data?.data || [],
|
||||||
@ -139,8 +139,8 @@ export const useModelList = (type: ModelTypeEnum) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useDefaultModel = (type: ModelTypeEnum) => {
|
export const useDefaultModel = (type: ModelTypeEnum, enabled?: boolean) => {
|
||||||
const { data, mutate, isLoading } = useSWR(`/workspaces/current/default-model?model_type=${type}`, fetchDefaultModal)
|
const { data, mutate, isLoading } = useSWR(enabled ? `/workspaces/current/default-model?model_type=${type}` : null, fetchDefaultModal)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: data?.data,
|
data: data?.data,
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useParams } from 'next/navigation'
|
import { useParams } from 'next/navigation'
|
||||||
|
import useSWRInfinite from 'swr/infinite'
|
||||||
import { flatten } from 'lodash-es'
|
import { flatten } from 'lodash-es'
|
||||||
import { produce } from 'immer'
|
import { produce } from 'immer'
|
||||||
import {
|
import {
|
||||||
@ -11,13 +12,33 @@ import {
|
|||||||
} from '@remixicon/react'
|
} from '@remixicon/react'
|
||||||
import Nav from '../nav'
|
import Nav from '../nav'
|
||||||
import type { NavItem } from '../nav/nav-selector'
|
import type { NavItem } from '../nav/nav-selector'
|
||||||
|
import { fetchAppList } from '@/service/apps'
|
||||||
import CreateAppTemplateDialog from '@/app/components/app/create-app-dialog'
|
import CreateAppTemplateDialog from '@/app/components/app/create-app-dialog'
|
||||||
import CreateAppModal from '@/app/components/app/create-app-modal'
|
import CreateAppModal from '@/app/components/app/create-app-modal'
|
||||||
import CreateFromDSLModal from '@/app/components/app/create-from-dsl-modal'
|
import CreateFromDSLModal from '@/app/components/app/create-from-dsl-modal'
|
||||||
|
import type { AppListResponse } from '@/models/app'
|
||||||
import { useAppContext } from '@/context/app-context'
|
import { useAppContext } from '@/context/app-context'
|
||||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||||
import { AppModeEnum } from '@/types/app'
|
import { AppModeEnum } from '@/types/app'
|
||||||
import { useInfiniteAppList } from '@/service/use-apps'
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
const AppNav = () => {
|
const AppNav = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
@ -29,21 +50,17 @@ const AppNav = () => {
|
|||||||
const [showCreateFromDSLModal, setShowCreateFromDSLModal] = useState(false)
|
const [showCreateFromDSLModal, setShowCreateFromDSLModal] = useState(false)
|
||||||
const [navItems, setNavItems] = useState<NavItem[]>([])
|
const [navItems, setNavItems] = useState<NavItem[]>([])
|
||||||
|
|
||||||
const {
|
const { data: appsData, setSize, mutate } = useSWRInfinite(
|
||||||
data: appsData,
|
appId
|
||||||
fetchNextPage,
|
? (pageIndex: number, previousPageData: AppListResponse) => getKey(pageIndex, previousPageData, 'all', '')
|
||||||
hasNextPage,
|
: () => null,
|
||||||
refetch,
|
fetchAppList,
|
||||||
} = useInfiniteAppList({
|
{ revalidateFirstPage: false },
|
||||||
page: 1,
|
)
|
||||||
limit: 30,
|
|
||||||
name: '',
|
|
||||||
}, { enabled: !!appId })
|
|
||||||
|
|
||||||
const handleLoadMore = useCallback(() => {
|
const handleLoadMore = useCallback(() => {
|
||||||
if (hasNextPage)
|
setSize(size => size + 1)
|
||||||
fetchNextPage()
|
}, [setSize])
|
||||||
}, [fetchNextPage, hasNextPage])
|
|
||||||
|
|
||||||
const openModal = (state: string) => {
|
const openModal = (state: string) => {
|
||||||
if (state === 'blank')
|
if (state === 'blank')
|
||||||
@ -56,7 +73,7 @@ const AppNav = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (appsData) {
|
if (appsData) {
|
||||||
const appItems = flatten((appsData.pages ?? []).map(appData => appData.data))
|
const appItems = flatten(appsData?.map(appData => appData.data))
|
||||||
const navItems = appItems.map((app) => {
|
const navItems = appItems.map((app) => {
|
||||||
const link = ((isCurrentWorkspaceEditor, app) => {
|
const link = ((isCurrentWorkspaceEditor, app) => {
|
||||||
if (!isCurrentWorkspaceEditor) {
|
if (!isCurrentWorkspaceEditor) {
|
||||||
@ -115,17 +132,17 @@ const AppNav = () => {
|
|||||||
<CreateAppModal
|
<CreateAppModal
|
||||||
show={showNewAppDialog}
|
show={showNewAppDialog}
|
||||||
onClose={() => setShowNewAppDialog(false)}
|
onClose={() => setShowNewAppDialog(false)}
|
||||||
onSuccess={() => refetch()}
|
onSuccess={() => mutate()}
|
||||||
/>
|
/>
|
||||||
<CreateAppTemplateDialog
|
<CreateAppTemplateDialog
|
||||||
show={showNewAppTemplateDialog}
|
show={showNewAppTemplateDialog}
|
||||||
onClose={() => setShowNewAppTemplateDialog(false)}
|
onClose={() => setShowNewAppTemplateDialog(false)}
|
||||||
onSuccess={() => refetch()}
|
onSuccess={() => mutate()}
|
||||||
/>
|
/>
|
||||||
<CreateFromDSLModal
|
<CreateFromDSLModal
|
||||||
show={showCreateFromDSLModal}
|
show={showCreateFromDSLModal}
|
||||||
onClose={() => setShowCreateFromDSLModal(false)}
|
onClose={() => setShowCreateFromDSLModal(false)}
|
||||||
onSuccess={() => refetch()}
|
onSuccess={() => mutate()}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -15,10 +15,32 @@ import type {
|
|||||||
OffsetOptions,
|
OffsetOptions,
|
||||||
Placement,
|
Placement,
|
||||||
} from '@floating-ui/react'
|
} from '@floating-ui/react'
|
||||||
import { useInfiniteAppList } from '@/service/use-apps'
|
import useSWRInfinite from 'swr/infinite'
|
||||||
|
import { fetchAppList } from '@/service/apps'
|
||||||
|
import type { AppListResponse } from '@/models/app'
|
||||||
|
|
||||||
const PAGE_SIZE = 20
|
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 = {
|
type Props = {
|
||||||
value?: {
|
value?: {
|
||||||
app_id: string
|
app_id: string
|
||||||
@ -50,32 +72,30 @@ const AppSelector: FC<Props> = ({
|
|||||||
const [searchText, setSearchText] = useState('')
|
const [searchText, setSearchText] = useState('')
|
||||||
const [isLoadingMore, setIsLoadingMore] = useState(false)
|
const [isLoadingMore, setIsLoadingMore] = useState(false)
|
||||||
|
|
||||||
const {
|
const { data, isLoading, setSize } = useSWRInfinite(
|
||||||
data,
|
(pageIndex: number, previousPageData: AppListResponse) => getKey(pageIndex, previousPageData, searchText),
|
||||||
isLoading,
|
fetchAppList,
|
||||||
isFetchingNextPage,
|
{
|
||||||
fetchNextPage,
|
revalidateFirstPage: true,
|
||||||
hasNextPage,
|
shouldRetryOnError: false,
|
||||||
} = useInfiniteAppList({
|
dedupingInterval: 500,
|
||||||
page: 1,
|
errorRetryCount: 3,
|
||||||
limit: PAGE_SIZE,
|
},
|
||||||
name: searchText,
|
)
|
||||||
})
|
|
||||||
|
|
||||||
const pages = data?.pages ?? []
|
|
||||||
const displayedApps = useMemo(() => {
|
const displayedApps = useMemo(() => {
|
||||||
if (!pages.length) return []
|
if (!data) return []
|
||||||
return pages.flatMap(({ data: apps }) => apps)
|
return data.flatMap(({ data: apps }) => apps)
|
||||||
}, [pages])
|
}, [data])
|
||||||
|
|
||||||
const hasMore = hasNextPage ?? true
|
const hasMore = data?.at(-1)?.has_more ?? true
|
||||||
|
|
||||||
const handleLoadMore = useCallback(async () => {
|
const handleLoadMore = useCallback(async () => {
|
||||||
if (isLoadingMore || isFetchingNextPage || !hasMore) return
|
if (isLoadingMore || !hasMore) return
|
||||||
|
|
||||||
setIsLoadingMore(true)
|
setIsLoadingMore(true)
|
||||||
try {
|
try {
|
||||||
await fetchNextPage()
|
await setSize((size: number) => size + 1)
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
// Add a small delay to ensure state updates are complete
|
// Add a small delay to ensure state updates are complete
|
||||||
@ -83,7 +103,7 @@ const AppSelector: FC<Props> = ({
|
|||||||
setIsLoadingMore(false)
|
setIsLoadingMore(false)
|
||||||
}, 300)
|
}, 300)
|
||||||
}
|
}
|
||||||
}, [isLoadingMore, isFetchingNextPage, hasMore, fetchNextPage])
|
}, [isLoadingMore, hasMore, setSize])
|
||||||
|
|
||||||
const handleTriggerClick = () => {
|
const handleTriggerClick = () => {
|
||||||
if (disabled) return
|
if (disabled) return
|
||||||
@ -165,7 +185,7 @@ const AppSelector: FC<Props> = ({
|
|||||||
onSelect={handleSelectApp}
|
onSelect={handleSelectApp}
|
||||||
scope={scope || 'all'}
|
scope={scope || 'all'}
|
||||||
apps={displayedApps}
|
apps={displayedApps}
|
||||||
isLoading={isLoading || isLoadingMore || isFetchingNextPage}
|
isLoading={isLoading || isLoadingMore}
|
||||||
hasMore={hasMore}
|
hasMore={hasMore}
|
||||||
onLoadMore={handleLoadMore}
|
onLoadMore={handleLoadMore}
|
||||||
searchText={searchText}
|
searchText={searchText}
|
||||||
|
|||||||
@ -19,6 +19,7 @@ import {
|
|||||||
useWorkflowStartRun,
|
useWorkflowStartRun,
|
||||||
} from '../hooks'
|
} from '../hooks'
|
||||||
import { useWorkflowStore } from '@/app/components/workflow/store'
|
import { useWorkflowStore } from '@/app/components/workflow/store'
|
||||||
|
import { useFormatMemoryVariables } from '@/app/components/workflow/hooks'
|
||||||
|
|
||||||
type WorkflowMainProps = Pick<WorkflowProps, 'nodes' | 'edges' | 'viewport'>
|
type WorkflowMainProps = Pick<WorkflowProps, 'nodes' | 'edges' | 'viewport'>
|
||||||
const WorkflowMain = ({
|
const WorkflowMain = ({
|
||||||
@ -28,12 +29,14 @@ const WorkflowMain = ({
|
|||||||
}: WorkflowMainProps) => {
|
}: WorkflowMainProps) => {
|
||||||
const featuresStore = useFeaturesStore()
|
const featuresStore = useFeaturesStore()
|
||||||
const workflowStore = useWorkflowStore()
|
const workflowStore = useWorkflowStore()
|
||||||
|
const { formatMemoryVariables } = useFormatMemoryVariables()
|
||||||
|
|
||||||
const handleWorkflowDataUpdate = useCallback((payload: any) => {
|
const handleWorkflowDataUpdate = useCallback((payload: any) => {
|
||||||
const {
|
const {
|
||||||
features,
|
features,
|
||||||
conversation_variables,
|
conversation_variables,
|
||||||
environment_variables,
|
environment_variables,
|
||||||
|
memory_blocks,
|
||||||
} = payload
|
} = payload
|
||||||
if (features && featuresStore) {
|
if (features && featuresStore) {
|
||||||
const { setFeatures } = featuresStore.getState()
|
const { setFeatures } = featuresStore.getState()
|
||||||
@ -48,7 +51,11 @@ const WorkflowMain = ({
|
|||||||
const { setEnvironmentVariables } = workflowStore.getState()
|
const { setEnvironmentVariables } = workflowStore.getState()
|
||||||
setEnvironmentVariables(environment_variables)
|
setEnvironmentVariables(environment_variables)
|
||||||
}
|
}
|
||||||
}, [featuresStore, workflowStore])
|
if (memory_blocks) {
|
||||||
|
const { setMemoryVariables } = workflowStore.getState()
|
||||||
|
setMemoryVariables(formatMemoryVariables(memory_blocks))
|
||||||
|
}
|
||||||
|
}, [featuresStore, workflowStore, formatMemoryVariables])
|
||||||
|
|
||||||
const {
|
const {
|
||||||
doSyncWorkflowDraft,
|
doSyncWorkflowDraft,
|
||||||
|
|||||||
@ -27,6 +27,7 @@ export const useNodesSyncDraft = () => {
|
|||||||
const {
|
const {
|
||||||
appId,
|
appId,
|
||||||
conversationVariables,
|
conversationVariables,
|
||||||
|
memoryVariables,
|
||||||
environmentVariables,
|
environmentVariables,
|
||||||
syncWorkflowDraftHash,
|
syncWorkflowDraftHash,
|
||||||
isWorkflowDataLoaded,
|
isWorkflowDataLoaded,
|
||||||
@ -74,6 +75,13 @@ export const useNodesSyncDraft = () => {
|
|||||||
},
|
},
|
||||||
environment_variables: environmentVariables,
|
environment_variables: environmentVariables,
|
||||||
conversation_variables: conversationVariables,
|
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,
|
hash: syncWorkflowDraftHash,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import {
|
|||||||
import type { FetchWorkflowDraftResponse } from '@/types/workflow'
|
import type { FetchWorkflowDraftResponse } from '@/types/workflow'
|
||||||
import { useWorkflowConfig } from '@/service/use-workflow'
|
import { useWorkflowConfig } from '@/service/use-workflow'
|
||||||
import type { FileUploadConfigResponse } from '@/models/common'
|
import type { FileUploadConfigResponse } from '@/models/common'
|
||||||
|
import { useFormatMemoryVariables } from '@/app/components/workflow/hooks'
|
||||||
import type { Edge, Node } from '@/app/components/workflow/types'
|
import type { Edge, Node } from '@/app/components/workflow/types'
|
||||||
import { BlockEnum } from '@/app/components/workflow/types'
|
import { BlockEnum } from '@/app/components/workflow/types'
|
||||||
import { AppModeEnum } from '@/types/app'
|
import { AppModeEnum } from '@/types/app'
|
||||||
@ -54,6 +55,7 @@ export const useWorkflowInit = () => {
|
|||||||
data: fileUploadConfigResponse,
|
data: fileUploadConfigResponse,
|
||||||
isLoading: isFileUploadConfigLoading,
|
isLoading: isFileUploadConfigLoading,
|
||||||
} = useWorkflowConfig('/files/upload', handleUpdateWorkflowFileUploadConfig)
|
} = useWorkflowConfig('/files/upload', handleUpdateWorkflowFileUploadConfig)
|
||||||
|
const { formatMemoryVariables } = useFormatMemoryVariables()
|
||||||
|
|
||||||
const handleGetInitialWorkflowData = useCallback(async () => {
|
const handleGetInitialWorkflowData = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@ -66,6 +68,7 @@ export const useWorkflowInit = () => {
|
|||||||
}, {} as Record<string, string>),
|
}, {} as Record<string, string>),
|
||||||
environmentVariables: res.environment_variables?.map(env => env.value_type === 'secret' ? { ...env, value: '[__HIDDEN__]' } : env) || [],
|
environmentVariables: res.environment_variables?.map(env => env.value_type === 'secret' ? { ...env, value: '[__HIDDEN__]' } : env) || [],
|
||||||
conversationVariables: res.conversation_variables || [],
|
conversationVariables: res.conversation_variables || [],
|
||||||
|
memoryVariables: formatMemoryVariables((res.memory_blocks || [])),
|
||||||
isWorkflowDataLoaded: true,
|
isWorkflowDataLoaded: true,
|
||||||
})
|
})
|
||||||
setSyncWorkflowDraftHash(res.hash)
|
setSyncWorkflowDraftHash(res.hash)
|
||||||
|
|||||||
@ -3,10 +3,12 @@ import { useWorkflowStore } from '@/app/components/workflow/store'
|
|||||||
import { fetchWorkflowDraft } from '@/service/workflow'
|
import { fetchWorkflowDraft } from '@/service/workflow'
|
||||||
import type { WorkflowDataUpdater } from '@/app/components/workflow/types'
|
import type { WorkflowDataUpdater } from '@/app/components/workflow/types'
|
||||||
import { useWorkflowUpdate } from '@/app/components/workflow/hooks'
|
import { useWorkflowUpdate } from '@/app/components/workflow/hooks'
|
||||||
|
import { useFormatMemoryVariables } from '@/app/components/workflow/hooks'
|
||||||
|
|
||||||
export const useWorkflowRefreshDraft = () => {
|
export const useWorkflowRefreshDraft = () => {
|
||||||
const workflowStore = useWorkflowStore()
|
const workflowStore = useWorkflowStore()
|
||||||
const { handleUpdateWorkflowCanvas } = useWorkflowUpdate()
|
const { handleUpdateWorkflowCanvas } = useWorkflowUpdate()
|
||||||
|
const { formatMemoryVariables } = useFormatMemoryVariables()
|
||||||
|
|
||||||
const handleRefreshWorkflowDraft = useCallback(() => {
|
const handleRefreshWorkflowDraft = useCallback(() => {
|
||||||
const {
|
const {
|
||||||
@ -16,6 +18,7 @@ export const useWorkflowRefreshDraft = () => {
|
|||||||
setEnvironmentVariables,
|
setEnvironmentVariables,
|
||||||
setEnvSecrets,
|
setEnvSecrets,
|
||||||
setConversationVariables,
|
setConversationVariables,
|
||||||
|
setMemoryVariables,
|
||||||
setIsWorkflowDataLoaded,
|
setIsWorkflowDataLoaded,
|
||||||
isWorkflowDataLoaded,
|
isWorkflowDataLoaded,
|
||||||
debouncedSyncWorkflowDraft,
|
debouncedSyncWorkflowDraft,
|
||||||
@ -44,6 +47,7 @@ export const useWorkflowRefreshDraft = () => {
|
|||||||
}, {} as Record<string, string>))
|
}, {} as Record<string, string>))
|
||||||
setEnvironmentVariables(response.environment_variables?.map(env => env.value_type === 'secret' ? { ...env, value: '[__HIDDEN__]' } : env) || [])
|
setEnvironmentVariables(response.environment_variables?.map(env => env.value_type === 'secret' ? { ...env, value: '[__HIDDEN__]' } : env) || [])
|
||||||
setConversationVariables(response.conversation_variables || [])
|
setConversationVariables(response.conversation_variables || [])
|
||||||
|
setMemoryVariables(formatMemoryVariables((response.memory_blocks || [])))
|
||||||
setIsWorkflowDataLoaded(true)
|
setIsWorkflowDataLoaded(true)
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
@ -53,7 +57,7 @@ export const useWorkflowRefreshDraft = () => {
|
|||||||
.finally(() => {
|
.finally(() => {
|
||||||
setIsSyncingWorkflowDraft(false)
|
setIsSyncingWorkflowDraft(false)
|
||||||
})
|
})
|
||||||
}, [handleUpdateWorkflowCanvas, workflowStore])
|
}, [handleUpdateWorkflowCanvas, workflowStore, formatMemoryVariables])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
handleRefreshWorkflowDraft,
|
handleRefreshWorkflowDraft,
|
||||||
|
|||||||
@ -603,6 +603,7 @@ export const useWorkflowRun = () => {
|
|||||||
baseSseOptions.onTTSEnd,
|
baseSseOptions.onTTSEnd,
|
||||||
baseSseOptions.onTextReplace,
|
baseSseOptions.onTextReplace,
|
||||||
baseSseOptions.onAgentLog,
|
baseSseOptions.onAgentLog,
|
||||||
|
baseSseOptions.onMemoryUpdate,
|
||||||
baseSseOptions.onDataSourceNodeProcessing,
|
baseSseOptions.onDataSourceNodeProcessing,
|
||||||
baseSseOptions.onDataSourceNodeCompleted,
|
baseSseOptions.onDataSourceNodeCompleted,
|
||||||
baseSseOptions.onDataSourceNodeError,
|
baseSseOptions.onDataSourceNodeError,
|
||||||
|
|||||||
@ -22,5 +22,6 @@ export * from './use-DSL'
|
|||||||
export * from './use-inspect-vars-crud'
|
export * from './use-inspect-vars-crud'
|
||||||
export * from './use-set-workflow-vars-with-value'
|
export * from './use-set-workflow-vars-with-value'
|
||||||
export * from './use-workflow-search'
|
export * from './use-workflow-search'
|
||||||
|
export * from './use-memory-variable'
|
||||||
export * from './use-auto-generate-webhook-url'
|
export * from './use-auto-generate-webhook-url'
|
||||||
export * from './use-serial-async-callback'
|
export * from './use-serial-async-callback'
|
||||||
|
|||||||
@ -51,7 +51,7 @@ export const useSetWorkflowVarsWithValue = ({
|
|||||||
const { getNodes } = store.getState()
|
const { getNodes } = store.getState()
|
||||||
|
|
||||||
const nodeArr = getNodes()
|
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> = {}
|
const nodesKeyValue: Record<string, Node> = {}
|
||||||
nodeArr.forEach((node) => {
|
nodeArr.forEach((node) => {
|
||||||
|
|||||||
@ -132,7 +132,7 @@ export const useInspectVarsCrudCommon = ({
|
|||||||
mcpTools: mcpTools || [],
|
mcpTools: mcpTools || [],
|
||||||
dataSourceList: dataSourceList || [],
|
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 vars = await fetchNodeInspectVars(flowType, flowId, nodeId)
|
||||||
const varsWithSchemaType = vars.map((varItem) => {
|
const varsWithSchemaType = vars.map((varItem) => {
|
||||||
const schemaType = currentNodeOutputVars[0]?.vars.find(v => v.variable === varItem.name)?.schemaType || ''
|
const schemaType = currentNodeOutputVars[0]?.vars.find(v => v.variable === varItem.name)?.schemaType || ''
|
||||||
|
|||||||
50
web/app/components/workflow/hooks/use-memory-variable.ts
Normal file
50
web/app/components/workflow/hooks/use-memory-variable.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -36,6 +36,7 @@ export const useWorkflowVariables = () => {
|
|||||||
filterVar,
|
filterVar,
|
||||||
hideEnv,
|
hideEnv,
|
||||||
hideChatVar,
|
hideChatVar,
|
||||||
|
conversationVariablesFirst,
|
||||||
}: {
|
}: {
|
||||||
parentNode?: Node | null
|
parentNode?: Node | null
|
||||||
beforeNodes: Node[]
|
beforeNodes: Node[]
|
||||||
@ -43,10 +44,12 @@ export const useWorkflowVariables = () => {
|
|||||||
filterVar: (payload: Var, selector: ValueSelector) => boolean
|
filterVar: (payload: Var, selector: ValueSelector) => boolean
|
||||||
hideEnv?: boolean
|
hideEnv?: boolean
|
||||||
hideChatVar?: boolean
|
hideChatVar?: boolean
|
||||||
|
conversationVariablesFirst?: boolean
|
||||||
}): NodeOutPutVar[] => {
|
}): NodeOutPutVar[] => {
|
||||||
const {
|
const {
|
||||||
conversationVariables,
|
conversationVariables,
|
||||||
environmentVariables,
|
environmentVariables,
|
||||||
|
memoryVariables,
|
||||||
ragPipelineVariables,
|
ragPipelineVariables,
|
||||||
dataSourceList,
|
dataSourceList,
|
||||||
} = workflowStore.getState()
|
} = workflowStore.getState()
|
||||||
@ -57,6 +60,7 @@ export const useWorkflowVariables = () => {
|
|||||||
isChatMode,
|
isChatMode,
|
||||||
environmentVariables: hideEnv ? [] : environmentVariables,
|
environmentVariables: hideEnv ? [] : environmentVariables,
|
||||||
conversationVariables: (isChatMode && !hideChatVar) ? conversationVariables : [],
|
conversationVariables: (isChatMode && !hideChatVar) ? conversationVariables : [],
|
||||||
|
memoryVariables: isChatMode ? memoryVariables : [],
|
||||||
ragVariables: ragPipelineVariables,
|
ragVariables: ragPipelineVariables,
|
||||||
filterVar,
|
filterVar,
|
||||||
allPluginInfoList: {
|
allPluginInfoList: {
|
||||||
@ -67,6 +71,7 @@ export const useWorkflowVariables = () => {
|
|||||||
dataSourceList: dataSourceList || [],
|
dataSourceList: dataSourceList || [],
|
||||||
},
|
},
|
||||||
schemaTypeDefinitions,
|
schemaTypeDefinitions,
|
||||||
|
conversationVariablesFirst,
|
||||||
})
|
})
|
||||||
}, [t, workflowStore, schemaTypeDefinitions, buildInTools, customTools, workflowTools, mcpTools])
|
}, [t, workflowStore, schemaTypeDefinitions, buildInTools, customTools, workflowTools, mcpTools])
|
||||||
|
|
||||||
|
|||||||
@ -13,6 +13,7 @@ type CollapseProps = {
|
|||||||
onCollapse?: (collapsed: boolean) => void
|
onCollapse?: (collapsed: boolean) => void
|
||||||
operations?: ReactNode
|
operations?: ReactNode
|
||||||
hideCollapseIcon?: boolean
|
hideCollapseIcon?: boolean
|
||||||
|
triggerClassName?: string
|
||||||
}
|
}
|
||||||
const Collapse = ({
|
const Collapse = ({
|
||||||
disabled,
|
disabled,
|
||||||
@ -22,6 +23,7 @@ const Collapse = ({
|
|||||||
onCollapse,
|
onCollapse,
|
||||||
operations,
|
operations,
|
||||||
hideCollapseIcon,
|
hideCollapseIcon,
|
||||||
|
triggerClassName,
|
||||||
}: CollapseProps) => {
|
}: CollapseProps) => {
|
||||||
const [collapsedLocal, setCollapsedLocal] = useState(true)
|
const [collapsedLocal, setCollapsedLocal] = useState(true)
|
||||||
const collapsedMerged = collapsed !== undefined ? collapsed : collapsedLocal
|
const collapsedMerged = collapsed !== undefined ? collapsed : collapsedLocal
|
||||||
@ -42,7 +44,7 @@ const Collapse = ({
|
|||||||
<>
|
<>
|
||||||
<div className='group/collapse flex items-center'>
|
<div className='group/collapse flex items-center'>
|
||||||
<div
|
<div
|
||||||
className='ml-4 flex grow items-center'
|
className={cn('ml-4 flex grow items-center', triggerClassName)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!disabled) {
|
if (!disabled) {
|
||||||
setCollapsedLocal(!collapsedMerged)
|
setCollapsedLocal(!collapsedMerged)
|
||||||
|
|||||||
@ -0,0 +1,27 @@
|
|||||||
|
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
|
||||||
@ -36,6 +36,9 @@ import Switch from '@/app/components/base/switch'
|
|||||||
import { Jinja } from '@/app/components/base/icons/src/vender/workflow'
|
import { Jinja } from '@/app/components/base/icons/src/vender/workflow'
|
||||||
import { useStore } from '@/app/components/workflow/store'
|
import { useStore } from '@/app/components/workflow/store'
|
||||||
import { useWorkflowVariableType } from '@/app/components/workflow/hooks'
|
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 = {
|
type Props = {
|
||||||
className?: string
|
className?: string
|
||||||
@ -75,10 +78,11 @@ type Props = {
|
|||||||
titleTooltip?: ReactNode
|
titleTooltip?: ReactNode
|
||||||
inputClassName?: string
|
inputClassName?: string
|
||||||
editorContainerClassName?: string
|
editorContainerClassName?: string
|
||||||
placeholder?: string
|
placeholder?: string | React.JSX.Element
|
||||||
placeholderClassName?: string
|
placeholderClassName?: string
|
||||||
titleClassName?: string
|
titleClassName?: string
|
||||||
required?: boolean
|
required?: boolean
|
||||||
|
isMemorySupported?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const Editor: FC<Props> = ({
|
const Editor: FC<Props> = ({
|
||||||
@ -118,6 +122,7 @@ const Editor: FC<Props> = ({
|
|||||||
titleClassName,
|
titleClassName,
|
||||||
editorContainerClassName,
|
editorContainerClassName,
|
||||||
required,
|
required,
|
||||||
|
isMemorySupported,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { eventEmitter } = useEventEmitterContextContext()
|
const { eventEmitter } = useEventEmitterContextContext()
|
||||||
@ -153,170 +158,182 @@ const Editor: FC<Props> = ({
|
|||||||
const pipelineId = useStore(s => s.pipelineId)
|
const pipelineId = useStore(s => s.pipelineId)
|
||||||
const setShowInputFieldPanel = useStore(s => s.setShowInputFieldPanel)
|
const setShowInputFieldPanel = useStore(s => s.setShowInputFieldPanel)
|
||||||
|
|
||||||
|
const handleAddMemory = () => {
|
||||||
|
setFocus()
|
||||||
|
eventEmitter?.emit({ type: MEMORY_POPUP_SHOW_BY_EVENT_EMITTER, instanceId } as any)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
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)}>
|
<Wrap className={cn(className, wrapClassName)} style={wrapStyle} isInNode isExpand={isExpand}>
|
||||||
<div className={cn(isFocus ? 'bg-background-default' : 'bg-components-input-bg-normal', isExpand && 'flex h-full flex-col', 'rounded-lg', containerClassName)}>
|
<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('flex items-center justify-between pl-3 pr-2 pt-1', headerClassName)}>
|
<div className={cn(isFocus ? 'bg-background-default' : 'bg-components-input-bg-normal', isExpand && 'flex h-full flex-col', 'rounded-lg', containerClassName)}>
|
||||||
<div className='flex gap-2'>
|
<div className={cn('flex items-center justify-between pl-3 pr-2 pt-1', headerClassName)}>
|
||||||
<div className={cn('text-xs font-semibold uppercase leading-4 text-text-secondary', titleClassName)}>{title} {required && <span className='text-text-destructive'>*</span>}</div>
|
<div className='flex gap-2'>
|
||||||
{titleTooltip && <Tooltip popupContent={titleTooltip} />}
|
<div className={cn('text-xs font-semibold uppercase leading-4 text-text-secondary', titleClassName)}>{title} {required && <span className='text-text-destructive'>*</span>}</div>
|
||||||
</div>
|
{titleTooltip && <Tooltip popupContent={titleTooltip} />}
|
||||||
<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>
|
||||||
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Min: 80 Max: 560. Header: 24 */}
|
{/* Min: 80 Max: 560. Header: 24 */}
|
||||||
<div className={cn('pb-2', isExpand && 'flex grow flex-col')}>
|
<div className={cn('pb-2', isExpand && 'flex grow flex-col', isMemorySupported && isFocus && 'pb-1.5')}>
|
||||||
{!(isSupportJinja && editionType === EditionType.jinja2)
|
{!(isSupportJinja && editionType === EditionType.jinja2)
|
||||||
? (
|
? (
|
||||||
<div className={cn(isExpand ? 'grow' : 'max-h-[536px]', 'relative min-h-[56px] overflow-y-auto px-3', editorContainerClassName)}>
|
<>
|
||||||
<PromptEditor
|
<div className={cn(isExpand ? 'grow' : 'max-h-[536px]', 'relative min-h-[56px] overflow-y-auto px-3', editorContainerClassName)}>
|
||||||
key={controlPromptEditorRerenderKey}
|
<PromptEditor
|
||||||
placeholder={placeholder}
|
key={controlPromptEditorRerenderKey}
|
||||||
placeholderClassName={placeholderClassName}
|
placeholder={placeholder}
|
||||||
instanceId={instanceId}
|
placeholderClassName={placeholderClassName}
|
||||||
compact
|
instanceId={instanceId}
|
||||||
className={cn('min-h-[56px]', inputClassName)}
|
compact
|
||||||
style={isExpand ? { height: editorExpandHeight - 5 } : {}}
|
className={cn('min-h-[56px]', inputClassName)}
|
||||||
value={value}
|
style={isExpand ? { height: editorExpandHeight - 5 } : {}}
|
||||||
contextBlock={{
|
value={value}
|
||||||
show: justVar ? false : isShowContext,
|
contextBlock={{
|
||||||
selectable: !hasSetBlockStatus?.context,
|
show: justVar ? false : isShowContext,
|
||||||
canNotAddContext: true,
|
selectable: !hasSetBlockStatus?.context,
|
||||||
}}
|
canNotAddContext: true,
|
||||||
historyBlock={{
|
}}
|
||||||
show: justVar ? false : isShowHistory,
|
historyBlock={{
|
||||||
selectable: !hasSetBlockStatus?.history,
|
show: justVar ? false : isShowHistory,
|
||||||
history: {
|
selectable: !hasSetBlockStatus?.history,
|
||||||
user: 'Human',
|
history: {
|
||||||
assistant: 'Assistant',
|
user: 'Human',
|
||||||
},
|
assistant: 'Assistant',
|
||||||
}}
|
},
|
||||||
queryBlock={{
|
}}
|
||||||
show: false, // use [sys.query] instead of query block
|
queryBlock={{
|
||||||
selectable: false,
|
show: false, // use [sys.query] instead of query block
|
||||||
}}
|
selectable: false,
|
||||||
workflowVariableBlock={{
|
}}
|
||||||
show: true,
|
workflowVariableBlock={{
|
||||||
variables: nodesOutputVars || [],
|
show: true,
|
||||||
getVarType: getVarType as any,
|
variables: nodesOutputVars || [],
|
||||||
workflowNodesMap: availableNodes.reduce((acc, node) => {
|
getVarType,
|
||||||
acc[node.id] = {
|
workflowNodesMap: availableNodes.reduce((acc, node) => {
|
||||||
title: node.data.title,
|
acc[node.id] = {
|
||||||
type: node.data.type,
|
title: node.data.title,
|
||||||
width: node.width,
|
type: node.data.type,
|
||||||
height: node.height,
|
width: node.width,
|
||||||
position: node.position,
|
height: node.height,
|
||||||
}
|
position: node.position,
|
||||||
if (node.data.type === BlockEnum.Start) {
|
}
|
||||||
acc.sys = {
|
if (node.data.type === BlockEnum.Start) {
|
||||||
title: t('workflow.blocks.start'),
|
acc.sys = {
|
||||||
type: BlockEnum.Start,
|
title: t('workflow.blocks.start'),
|
||||||
}
|
type: BlockEnum.Start,
|
||||||
}
|
}
|
||||||
return acc
|
}
|
||||||
}, {} as any),
|
return acc
|
||||||
showManageInputField: !!pipelineId,
|
}, {} as any),
|
||||||
onManageInputField: () => setShowInputFieldPanel?.(true),
|
showManageInputField: !!pipelineId,
|
||||||
}}
|
onManageInputField: () => setShowInputFieldPanel?.(true),
|
||||||
onChange={onChange}
|
isMemorySupported,
|
||||||
onBlur={setBlur}
|
}}
|
||||||
onFocus={setFocus}
|
onChange={onChange}
|
||||||
editable={!readOnly}
|
onBlur={setBlur}
|
||||||
isSupportFileVar={isSupportFileVar}
|
onFocus={setFocus}
|
||||||
/>
|
editable={!readOnly}
|
||||||
{/* to patch Editor not support dynamic change editable status */}
|
isSupportFileVar={isSupportFileVar}
|
||||||
{readOnly && <div className='absolute inset-0 z-10'></div>}
|
/>
|
||||||
</div>
|
{/* 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)}>
|
{isMemorySupported && <AddMemoryButton onAddMemory={handleAddMemory} />}
|
||||||
<CodeEditor
|
</>
|
||||||
availableVars={nodesOutputVars || []}
|
)
|
||||||
varList={varList}
|
: (
|
||||||
onAddVar={handleAddVariable}
|
<div className={cn(isExpand ? 'grow' : 'max-h-[536px]', 'relative min-h-[56px] overflow-y-auto px-3', editorContainerClassName)}>
|
||||||
isInNode
|
<CodeEditor
|
||||||
readOnly={readOnly}
|
availableVars={nodesOutputVars || []}
|
||||||
language={CodeLanguage.python3}
|
varList={varList}
|
||||||
value={value}
|
onAddVar={handleAddVariable}
|
||||||
onChange={onChange}
|
isInNode
|
||||||
noWrapper
|
readOnly={readOnly}
|
||||||
isExpand={isExpand}
|
language={CodeLanguage.python3}
|
||||||
className={inputClassName}
|
value={value}
|
||||||
/>
|
onChange={onChange}
|
||||||
</div>
|
noWrapper
|
||||||
)}
|
isExpand={isExpand}
|
||||||
|
className={inputClassName}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Wrap>
|
||||||
</Wrap>
|
{isMemorySupported && <MemoryCreateButton nodeId={nodeId || ''} instanceId={instanceId} hideTrigger />}
|
||||||
|
</>
|
||||||
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,3 @@
|
|||||||
|
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'
|
||||||
@ -39,6 +39,7 @@ import type {
|
|||||||
import type { VariableAssignerNodeType } from '@/app/components/workflow/nodes/variable-assigner/types'
|
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 { Field as StructField } from '@/app/components/workflow/nodes/llm/types'
|
||||||
import type { RAGPipelineVariable } from '@/models/pipeline'
|
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 { WebhookTriggerNodeType } from '@/app/components/workflow/nodes/trigger-webhook/types'
|
||||||
import type { PluginTriggerNodeType } from '@/app/components/workflow/nodes/trigger-plugin/types'
|
import type { PluginTriggerNodeType } from '@/app/components/workflow/nodes/trigger-plugin/types'
|
||||||
import PluginTriggerNodeDefault from '@/app/components/workflow/nodes/trigger-plugin/default'
|
import PluginTriggerNodeDefault from '@/app/components/workflow/nodes/trigger-plugin/default'
|
||||||
@ -86,13 +87,17 @@ export const isConversationVar = (valueSelector: ValueSelector) => {
|
|||||||
return valueSelector[0] === 'conversation'
|
return valueSelector[0] === 'conversation'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const isMemoryVariable = (valueSelector: ValueSelector) => {
|
||||||
|
return valueSelector[0] === 'memory_block'
|
||||||
|
}
|
||||||
|
|
||||||
export const isRagVariableVar = (valueSelector: ValueSelector) => {
|
export const isRagVariableVar = (valueSelector: ValueSelector) => {
|
||||||
if (!valueSelector) return false
|
if (!valueSelector) return false
|
||||||
return valueSelector[0] === 'rag'
|
return valueSelector[0] === 'rag'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const isSpecialVar = (prefix: string): boolean => {
|
export const isSpecialVar = (prefix: string): boolean => {
|
||||||
return ['sys', 'env', 'conversation', 'rag'].includes(prefix)
|
return ['sys', 'env', 'conversation', 'memory_block', 'rag'].includes(prefix)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const hasValidChildren = (children: any): boolean => {
|
export const hasValidChildren = (children: any): boolean => {
|
||||||
@ -644,13 +649,25 @@ const formatItem = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
case 'conversation': {
|
case 'conversation': {
|
||||||
res.vars = data.chatVarList.map((chatVar: ConversationVariable) => {
|
res.vars = [
|
||||||
return {
|
...data.memoryVarList.map((memoryVar: MemoryVariable) => {
|
||||||
variable: `conversation.${chatVar.name}`,
|
return {
|
||||||
type: chatVar.value_type,
|
variable: `memory_block.${memoryVar.node_id ? `${memoryVar.node_id}_` : ''}${memoryVar.id}`,
|
||||||
description: chatVar.description,
|
type: 'memory_block',
|
||||||
}
|
description: '',
|
||||||
}) as Var[]
|
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[],
|
||||||
|
]
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -698,11 +715,13 @@ const formatItem = (
|
|||||||
(() => {
|
(() => {
|
||||||
const variableArr = v.variable.split('.')
|
const variableArr = v.variable.split('.')
|
||||||
const [first] = variableArr
|
const [first] = variableArr
|
||||||
|
|
||||||
if (isSpecialVar(first)) return variableArr
|
if (isSpecialVar(first)) return variableArr
|
||||||
|
|
||||||
return [...selector, ...variableArr]
|
return [...selector, ...variableArr]
|
||||||
})(),
|
})(),
|
||||||
)
|
)
|
||||||
|
|
||||||
if (isCurrentMatched) return true
|
if (isCurrentMatched) return true
|
||||||
|
|
||||||
const isFile = v.type === VarType.file
|
const isFile = v.type === VarType.file
|
||||||
@ -777,9 +796,11 @@ export const toNodeOutputVars = (
|
|||||||
filterVar = (_payload: Var, _selector: ValueSelector) => true,
|
filterVar = (_payload: Var, _selector: ValueSelector) => true,
|
||||||
environmentVariables: EnvironmentVariable[] = [],
|
environmentVariables: EnvironmentVariable[] = [],
|
||||||
conversationVariables: ConversationVariable[] = [],
|
conversationVariables: ConversationVariable[] = [],
|
||||||
|
memoryVariables: MemoryVariable[] = [],
|
||||||
ragVariables: RAGPipelineVariable[] = [],
|
ragVariables: RAGPipelineVariable[] = [],
|
||||||
allPluginInfoList: Record<string, ToolWithProvider[]>,
|
allPluginInfoList: Record<string, ToolWithProvider[]>,
|
||||||
schemaTypeDefinitions?: SchemaTypeDefinition[],
|
schemaTypeDefinitions?: SchemaTypeDefinition[],
|
||||||
|
conversationVariablesFirst: boolean = false,
|
||||||
): NodeOutPutVar[] => {
|
): NodeOutPutVar[] => {
|
||||||
// ENV_NODE data format
|
// ENV_NODE data format
|
||||||
const ENV_NODE = {
|
const ENV_NODE = {
|
||||||
@ -796,6 +817,7 @@ export const toNodeOutputVars = (
|
|||||||
data: {
|
data: {
|
||||||
title: 'CONVERSATION',
|
title: 'CONVERSATION',
|
||||||
type: 'conversation',
|
type: 'conversation',
|
||||||
|
memoryVarList: memoryVariables,
|
||||||
chatVarList: conversationVariables,
|
chatVarList: conversationVariables,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -995,6 +1017,7 @@ export const getVarType = ({
|
|||||||
isConstant,
|
isConstant,
|
||||||
environmentVariables = [],
|
environmentVariables = [],
|
||||||
conversationVariables = [],
|
conversationVariables = [],
|
||||||
|
memoryVariables = [],
|
||||||
ragVariables = [],
|
ragVariables = [],
|
||||||
allPluginInfoList,
|
allPluginInfoList,
|
||||||
schemaTypeDefinitions,
|
schemaTypeDefinitions,
|
||||||
@ -1009,6 +1032,7 @@ export const getVarType = ({
|
|||||||
isConstant?: boolean;
|
isConstant?: boolean;
|
||||||
environmentVariables?: EnvironmentVariable[];
|
environmentVariables?: EnvironmentVariable[];
|
||||||
conversationVariables?: ConversationVariable[];
|
conversationVariables?: ConversationVariable[];
|
||||||
|
memoryVariables?: MemoryVariable[];
|
||||||
ragVariables?: RAGPipelineVariable[];
|
ragVariables?: RAGPipelineVariable[];
|
||||||
allPluginInfoList: Record<string, ToolWithProvider[]>;
|
allPluginInfoList: Record<string, ToolWithProvider[]>;
|
||||||
schemaTypeDefinitions?: SchemaTypeDefinition[];
|
schemaTypeDefinitions?: SchemaTypeDefinition[];
|
||||||
@ -1022,6 +1046,7 @@ export const getVarType = ({
|
|||||||
undefined,
|
undefined,
|
||||||
environmentVariables,
|
environmentVariables,
|
||||||
conversationVariables,
|
conversationVariables,
|
||||||
|
memoryVariables,
|
||||||
ragVariables,
|
ragVariables,
|
||||||
allPluginInfoList,
|
allPluginInfoList,
|
||||||
schemaTypeDefinitions,
|
schemaTypeDefinitions,
|
||||||
@ -1150,10 +1175,12 @@ export const toNodeAvailableVars = ({
|
|||||||
isChatMode,
|
isChatMode,
|
||||||
environmentVariables,
|
environmentVariables,
|
||||||
conversationVariables,
|
conversationVariables,
|
||||||
|
memoryVariables,
|
||||||
ragVariables,
|
ragVariables,
|
||||||
filterVar,
|
filterVar,
|
||||||
allPluginInfoList,
|
allPluginInfoList,
|
||||||
schemaTypeDefinitions,
|
schemaTypeDefinitions,
|
||||||
|
conversationVariablesFirst,
|
||||||
}: {
|
}: {
|
||||||
parentNode?: Node | null;
|
parentNode?: Node | null;
|
||||||
t?: any;
|
t?: any;
|
||||||
@ -1164,11 +1191,14 @@ export const toNodeAvailableVars = ({
|
|||||||
environmentVariables?: EnvironmentVariable[];
|
environmentVariables?: EnvironmentVariable[];
|
||||||
// chat var
|
// chat var
|
||||||
conversationVariables?: ConversationVariable[];
|
conversationVariables?: ConversationVariable[];
|
||||||
|
// memory variables
|
||||||
|
memoryVariables?: MemoryVariable[];
|
||||||
// rag variables
|
// rag variables
|
||||||
ragVariables?: RAGPipelineVariable[];
|
ragVariables?: RAGPipelineVariable[];
|
||||||
filterVar: (payload: Var, selector: ValueSelector) => boolean;
|
filterVar: (payload: Var, selector: ValueSelector) => boolean;
|
||||||
allPluginInfoList: Record<string, ToolWithProvider[]>;
|
allPluginInfoList: Record<string, ToolWithProvider[]>;
|
||||||
schemaTypeDefinitions?: SchemaTypeDefinition[];
|
schemaTypeDefinitions?: SchemaTypeDefinition[];
|
||||||
|
conversationVariablesFirst?: boolean
|
||||||
}): NodeOutPutVar[] => {
|
}): NodeOutPutVar[] => {
|
||||||
const beforeNodesOutputVars = toNodeOutputVars(
|
const beforeNodesOutputVars = toNodeOutputVars(
|
||||||
beforeNodes,
|
beforeNodes,
|
||||||
@ -1176,9 +1206,11 @@ export const toNodeAvailableVars = ({
|
|||||||
filterVar,
|
filterVar,
|
||||||
environmentVariables,
|
environmentVariables,
|
||||||
conversationVariables,
|
conversationVariables,
|
||||||
|
memoryVariables,
|
||||||
ragVariables,
|
ragVariables,
|
||||||
allPluginInfoList,
|
allPluginInfoList,
|
||||||
schemaTypeDefinitions,
|
schemaTypeDefinitions,
|
||||||
|
conversationVariablesFirst,
|
||||||
)
|
)
|
||||||
const isInIteration = parentNode?.data.type === BlockEnum.Iteration
|
const isInIteration = parentNode?.data.type === BlockEnum.Iteration
|
||||||
if (isInIteration) {
|
if (isInIteration) {
|
||||||
@ -1191,6 +1223,7 @@ export const toNodeAvailableVars = ({
|
|||||||
isChatMode,
|
isChatMode,
|
||||||
environmentVariables,
|
environmentVariables,
|
||||||
conversationVariables,
|
conversationVariables,
|
||||||
|
memoryVariables,
|
||||||
allPluginInfoList,
|
allPluginInfoList,
|
||||||
schemaTypeDefinitions,
|
schemaTypeDefinitions,
|
||||||
})
|
})
|
||||||
|
|||||||
@ -65,6 +65,7 @@ const Item: FC<ItemProps> = ({
|
|||||||
const isSys = itemData.variable.startsWith('sys.')
|
const isSys = itemData.variable.startsWith('sys.')
|
||||||
const isEnv = itemData.variable.startsWith('env.')
|
const isEnv = itemData.variable.startsWith('env.')
|
||||||
const isChatVar = itemData.variable.startsWith('conversation.')
|
const isChatVar = itemData.variable.startsWith('conversation.')
|
||||||
|
const isMemoryVar = itemData.variable.startsWith('memory_block')
|
||||||
const isRagVariable = itemData.isRagVariable
|
const isRagVariable = itemData.isRagVariable
|
||||||
const flatVarIcon = useMemo(() => {
|
const flatVarIcon = useMemo(() => {
|
||||||
if (!isFlat)
|
if (!isFlat)
|
||||||
@ -152,7 +153,7 @@ const Item: FC<ItemProps> = ({
|
|||||||
if (isFlat) {
|
if (isFlat) {
|
||||||
onChange([itemData.variable], itemData)
|
onChange([itemData.variable], itemData)
|
||||||
}
|
}
|
||||||
else if (isSys || isEnv || isChatVar || isRagVariable) { // system variable | environment variable | conversation variable
|
else if (isSys || isEnv || isChatVar || isMemoryVar || isRagVariable) { // system variable | environment variable | conversation variable
|
||||||
onChange([...objPath, ...itemData.variable.split('.')], itemData)
|
onChange([...objPath, ...itemData.variable.split('.')], itemData)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@ -162,10 +163,16 @@ const Item: FC<ItemProps> = ({
|
|||||||
const variableCategory = useMemo(() => {
|
const variableCategory = useMemo(() => {
|
||||||
if (isEnv) return 'environment'
|
if (isEnv) return 'environment'
|
||||||
if (isChatVar) return 'conversation'
|
if (isChatVar) return 'conversation'
|
||||||
|
if (isMemoryVar) return 'memory_block'
|
||||||
if (isLoopVar) return 'loop'
|
if (isLoopVar) return 'loop'
|
||||||
if (isRagVariable) return 'rag'
|
if (isRagVariable) return 'rag'
|
||||||
return 'system'
|
return 'system'
|
||||||
}, [isEnv, isChatVar, isSys, isLoopVar, isRagVariable])
|
}, [isEnv, isChatVar, isMemoryVar, isSys, isLoopVar, isRagVariable])
|
||||||
|
const variableType = useMemo(() => {
|
||||||
|
if (itemData.type === 'memory_block')
|
||||||
|
return 'memory'
|
||||||
|
return itemData.type
|
||||||
|
}, [itemData.type])
|
||||||
return (
|
return (
|
||||||
<PortalToFollowElem
|
<PortalToFollowElem
|
||||||
open={open}
|
open={open}
|
||||||
@ -193,7 +200,7 @@ const Item: FC<ItemProps> = ({
|
|||||||
/>}
|
/>}
|
||||||
{isFlat && flatVarIcon}
|
{isFlat && flatVarIcon}
|
||||||
|
|
||||||
{!isEnv && !isChatVar && !isRagVariable && (
|
{!isEnv && !isChatVar && !isMemoryVar && !isRagVariable && (
|
||||||
<div title={itemData.variable} className='system-sm-medium ml-1 w-0 grow truncate text-text-secondary'>{varName}</div>
|
<div title={itemData.variable} className='system-sm-medium ml-1 w-0 grow truncate text-text-secondary'>{varName}</div>
|
||||||
)}
|
)}
|
||||||
{isEnv && (
|
{isEnv && (
|
||||||
@ -202,11 +209,15 @@ const Item: FC<ItemProps> = ({
|
|||||||
{isChatVar && (
|
{isChatVar && (
|
||||||
<div title={itemData.des} className='system-sm-medium ml-1 w-0 grow truncate text-text-secondary'>{itemData.variable.replace('conversation.', '')}</div>
|
<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 && (
|
{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 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>
|
||||||
<div className='ml-1 shrink-0 text-xs font-normal capitalize text-text-tertiary'>{(preferSchemaType && itemData.schemaType) ? itemData.schemaType : itemData.type}</div>
|
<div className='ml-1 shrink-0 text-xs font-normal capitalize text-text-tertiary'>{(preferSchemaType && itemData.schemaType) ? itemData.schemaType : variableType}</div>
|
||||||
{
|
{
|
||||||
(isObj || isStructureOutput) && (
|
(isObj || isStructureOutput) && (
|
||||||
<ChevronRight className={cn('ml-0.5 h-3 w-3 text-text-quaternary', isHovering && 'text-text-tertiary')} />
|
<ChevronRight className={cn('ml-0.5 h-3 w-3 text-text-quaternary', isHovering && 'text-text-tertiary')} />
|
||||||
|
|||||||
@ -22,6 +22,7 @@ const VariableLabel = ({
|
|||||||
errorMsg,
|
errorMsg,
|
||||||
onClick,
|
onClick,
|
||||||
isExceptionVariable,
|
isExceptionVariable,
|
||||||
|
isMemoryVariable,
|
||||||
ref,
|
ref,
|
||||||
notShowFullPath,
|
notShowFullPath,
|
||||||
rightSlot,
|
rightSlot,
|
||||||
|
|||||||
@ -1,12 +1,13 @@
|
|||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
||||||
import { BubbleX, Env, GlobalVariable } from '@/app/components/base/icons/src/vender/line/others'
|
import { BubbleX, Env, GlobalVariable, Memory } from '@/app/components/base/icons/src/vender/line/others'
|
||||||
import { Loop } from '@/app/components/base/icons/src/vender/workflow'
|
import { Loop } from '@/app/components/base/icons/src/vender/workflow'
|
||||||
import { InputField } from '@/app/components/base/icons/src/vender/pipeline'
|
import { InputField } from '@/app/components/base/icons/src/vender/pipeline'
|
||||||
import {
|
import {
|
||||||
isConversationVar,
|
isConversationVar,
|
||||||
isENV,
|
isENV,
|
||||||
isGlobalVar,
|
isGlobalVar,
|
||||||
|
isMemoryVariable,
|
||||||
isRagVariableVar,
|
isRagVariableVar,
|
||||||
isSystemVar,
|
isSystemVar,
|
||||||
} from '../utils'
|
} from '../utils'
|
||||||
@ -23,6 +24,9 @@ export const useVarIcon = (variables: string[], variableCategory?: VarInInspectT
|
|||||||
if (isENV(variables) || variableCategory === VarInInspectType.environment || variableCategory === 'environment')
|
if (isENV(variables) || variableCategory === VarInInspectType.environment || variableCategory === 'environment')
|
||||||
return Env
|
return Env
|
||||||
|
|
||||||
|
if (isMemoryVariable(variables) || variableCategory === VarInInspectType.memory || variableCategory === 'memory_block')
|
||||||
|
return Memory
|
||||||
|
|
||||||
if (isConversationVar(variables) || variableCategory === VarInInspectType.conversation || variableCategory === 'conversation')
|
if (isConversationVar(variables) || variableCategory === VarInInspectType.conversation || variableCategory === 'conversation')
|
||||||
return BubbleX
|
return BubbleX
|
||||||
|
|
||||||
@ -46,6 +50,9 @@ export const useVarColor = (variables: string[], isExceptionVariable?: boolean,
|
|||||||
if (isConversationVar(variables) || variableCategory === VarInInspectType.conversation || variableCategory === 'conversation')
|
if (isConversationVar(variables) || variableCategory === VarInInspectType.conversation || variableCategory === 'conversation')
|
||||||
return 'text-util-colors-teal-teal-700'
|
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)
|
if (isGlobalVar(variables) || variableCategory === VarInInspectType.system)
|
||||||
return 'text-util-colors-orange-orange-600'
|
return 'text-util-colors-orange-orange-600'
|
||||||
|
|
||||||
@ -105,6 +112,15 @@ 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 {
|
return {
|
||||||
hoverBorderColor: 'hover:border-state-accent-alt',
|
hoverBorderColor: 'hover:border-state-accent-alt',
|
||||||
hoverBgColor: 'hover:bg-state-accent-hover',
|
hoverBgColor: 'hover:bg-state-accent-hover',
|
||||||
|
|||||||
@ -13,6 +13,7 @@ export type VariablePayload = {
|
|||||||
onClick?: (e: React.MouseEvent<HTMLDivElement>) => void
|
onClick?: (e: React.MouseEvent<HTMLDivElement>) => void
|
||||||
errorMsg?: string
|
errorMsg?: string
|
||||||
isExceptionVariable?: boolean
|
isExceptionVariable?: boolean
|
||||||
|
isMemoryVariable?: boolean
|
||||||
ref?: React.Ref<HTMLDivElement>
|
ref?: React.Ref<HTMLDivElement>
|
||||||
notShowFullPath?: boolean
|
notShowFullPath?: boolean
|
||||||
rightSlot?: ReactNode
|
rightSlot?: ReactNode
|
||||||
|
|||||||
@ -15,6 +15,8 @@ type Params = {
|
|||||||
hideChatVar?: boolean
|
hideChatVar?: boolean
|
||||||
filterVar: (payload: Var, selector: ValueSelector) => boolean
|
filterVar: (payload: Var, selector: ValueSelector) => boolean
|
||||||
passedInAvailableNodes?: Node[]
|
passedInAvailableNodes?: Node[]
|
||||||
|
conversationVariablesFirst?: boolean
|
||||||
|
isMemorySupported?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: loop type?
|
// TODO: loop type?
|
||||||
@ -24,6 +26,8 @@ const useAvailableVarList = (nodeId: string, {
|
|||||||
hideEnv,
|
hideEnv,
|
||||||
hideChatVar,
|
hideChatVar,
|
||||||
passedInAvailableNodes,
|
passedInAvailableNodes,
|
||||||
|
conversationVariablesFirst,
|
||||||
|
isMemorySupported,
|
||||||
}: Params = {
|
}: Params = {
|
||||||
onlyLeafNodeVar: false,
|
onlyLeafNodeVar: false,
|
||||||
filterVar: () => true,
|
filterVar: () => true,
|
||||||
@ -63,13 +67,35 @@ const useAvailableVarList = (nodeId: string, {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const availableVars = [...getNodeAvailableVars({
|
const nodeAvailableVars = getNodeAvailableVars({
|
||||||
parentNode: iterationNode,
|
parentNode: iterationNode,
|
||||||
beforeNodes: availableNodes,
|
beforeNodes: availableNodes,
|
||||||
isChatMode,
|
isChatMode,
|
||||||
filterVar,
|
filterVar,
|
||||||
hideEnv,
|
hideEnv,
|
||||||
hideChatVar,
|
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]
|
}), ...dataSourceRagVars]
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import { useCallback } from 'react'
|
||||||
|
import { useStoreApi } from 'reactflow'
|
||||||
import { useNodeDataUpdate } from '@/app/components/workflow/hooks'
|
import { useNodeDataUpdate } from '@/app/components/workflow/hooks'
|
||||||
import type { CommonNodeType } from '@/app/components/workflow/types'
|
import type { CommonNodeType } from '@/app/components/workflow/types'
|
||||||
const useNodeCrud = <T>(id: string, data: CommonNodeType<T>) => {
|
const useNodeCrud = <T>(id: string, data: CommonNodeType<T>) => {
|
||||||
@ -16,4 +18,28 @@ 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
|
export default useNodeCrud
|
||||||
|
|||||||
@ -145,6 +145,7 @@ const useOneStepRun = <T>({
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { getBeforeNodesInSameBranch, getBeforeNodesInSameBranchIncludeParent } = useWorkflow() as any
|
const { getBeforeNodesInSameBranch, getBeforeNodesInSameBranchIncludeParent } = useWorkflow() as any
|
||||||
const conversationVariables = useStore(s => s.conversationVariables)
|
const conversationVariables = useStore(s => s.conversationVariables)
|
||||||
|
const memoryVariables = useStore(s => s.memoryVariables)
|
||||||
const isChatMode = useIsChatMode()
|
const isChatMode = useIsChatMode()
|
||||||
const isIteration = data.type === BlockEnum.Iteration
|
const isIteration = data.type === BlockEnum.Iteration
|
||||||
const isLoop = data.type === BlockEnum.Loop
|
const isLoop = data.type === BlockEnum.Loop
|
||||||
@ -173,7 +174,7 @@ const useOneStepRun = <T>({
|
|||||||
dataSourceList: dataSourceList || [],
|
dataSourceList: dataSourceList || [],
|
||||||
}
|
}
|
||||||
|
|
||||||
const allOutputVars = toNodeOutputVars(availableNodes, isChatMode, undefined, undefined, conversationVariables, [], allPluginInfoList, schemaTypeDefinitions)
|
const allOutputVars = toNodeOutputVars(availableNodes, isChatMode, undefined, undefined, conversationVariables, memoryVariables, [], allPluginInfoList, schemaTypeDefinitions)
|
||||||
const targetVar = allOutputVars.find(item => isSystem ? !!item.isStartNode : item.nodeId === valueSelector[0])
|
const targetVar = allOutputVars.find(item => isSystem ? !!item.isStartNode : item.nodeId === valueSelector[0])
|
||||||
if (!targetVar)
|
if (!targetVar)
|
||||||
return undefined
|
return undefined
|
||||||
|
|||||||
@ -58,7 +58,7 @@ const useConfig = (id: string, payload: IterationNodeType) => {
|
|||||||
mcpTools: mcpTools || [],
|
mcpTools: mcpTools || [],
|
||||||
dataSourceList: dataSourceList || [],
|
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) => {
|
const handleOutputVarChange = useCallback((output: ValueSelector | string, _varKindType: VarKindType, varInfo?: Var) => {
|
||||||
if (isEqual(inputs.output_selector, output as ValueSelector))
|
if (isEqual(inputs.output_selector, output as ValueSelector))
|
||||||
|
|||||||
@ -39,6 +39,7 @@ type Props = {
|
|||||||
varList: Variable[]
|
varList: Variable[]
|
||||||
handleAddVariable: (payload: any) => void
|
handleAddVariable: (payload: any) => void
|
||||||
modelConfig?: ModelConfig
|
modelConfig?: ModelConfig
|
||||||
|
isMemorySupported?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const roleOptions = [
|
const roleOptions = [
|
||||||
@ -81,6 +82,7 @@ const ConfigPromptItem: FC<Props> = ({
|
|||||||
varList,
|
varList,
|
||||||
handleAddVariable,
|
handleAddVariable,
|
||||||
modelConfig,
|
modelConfig,
|
||||||
|
isMemorySupported,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const workflowStore = useWorkflowStore()
|
const workflowStore = useWorkflowStore()
|
||||||
@ -146,6 +148,17 @@ const ConfigPromptItem: FC<Props> = ({
|
|||||||
varList={varList}
|
varList={varList}
|
||||||
handleAddVariable={handleAddVariable}
|
handleAddVariable={handleAddVariable}
|
||||||
isSupportFileVar
|
isSupportFileVar
|
||||||
|
placeholder={
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
{t(`${i18nPrefix}.promptEditorPlaceholder1`)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{t(`${i18nPrefix}.promptEditorPlaceholder2`)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
isMemorySupported={isMemorySupported}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -34,6 +34,7 @@ type Props = {
|
|||||||
varList?: Variable[]
|
varList?: Variable[]
|
||||||
handleAddVariable: (payload: any) => void
|
handleAddVariable: (payload: any) => void
|
||||||
modelConfig: ModelConfig
|
modelConfig: ModelConfig
|
||||||
|
isMemorySupported?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const ConfigPrompt: FC<Props> = ({
|
const ConfigPrompt: FC<Props> = ({
|
||||||
@ -49,6 +50,7 @@ const ConfigPrompt: FC<Props> = ({
|
|||||||
varList = [],
|
varList = [],
|
||||||
handleAddVariable,
|
handleAddVariable,
|
||||||
modelConfig,
|
modelConfig,
|
||||||
|
isMemorySupported,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const workflowStore = useWorkflowStore()
|
const workflowStore = useWorkflowStore()
|
||||||
@ -73,6 +75,8 @@ const ConfigPrompt: FC<Props> = ({
|
|||||||
} = useAvailableVarList(nodeId, {
|
} = useAvailableVarList(nodeId, {
|
||||||
onlyLeafNodeVar: false,
|
onlyLeafNodeVar: false,
|
||||||
filterVar,
|
filterVar,
|
||||||
|
conversationVariablesFirst: true,
|
||||||
|
isMemorySupported,
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleChatModePromptChange = useCallback((index: number) => {
|
const handleChatModePromptChange = useCallback((index: number) => {
|
||||||
@ -204,6 +208,7 @@ const ConfigPrompt: FC<Props> = ({
|
|||||||
varList={varList}
|
varList={varList}
|
||||||
handleAddVariable={handleAddVariable}
|
handleAddVariable={handleAddVariable}
|
||||||
modelConfig={modelConfig}
|
modelConfig={modelConfig}
|
||||||
|
isMemorySupported={isMemorySupported}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@ -240,6 +245,7 @@ const ConfigPrompt: FC<Props> = ({
|
|||||||
handleAddVariable={handleAddVariable}
|
handleAddVariable={handleAddVariable}
|
||||||
onGenerated={handleGenerated}
|
onGenerated={handleGenerated}
|
||||||
modelConfig={modelConfig}
|
modelConfig={modelConfig}
|
||||||
|
isMemorySupported={isMemorySupported}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -0,0 +1,116 @@
|
|||||||
|
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)
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,77 @@
|
|||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,136 @@
|
|||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,139 @@
|
|||||||
|
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)
|
||||||
@ -0,0 +1,182 @@
|
|||||||
|
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)
|
||||||
@ -0,0 +1,70 @@
|
|||||||
|
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
|
||||||
@ -0,0 +1,107 @@
|
|||||||
|
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)
|
||||||
@ -18,6 +18,7 @@ type Props = {
|
|||||||
nodeId: string
|
nodeId: string
|
||||||
editorId?: string
|
editorId?: string
|
||||||
currentPrompt?: string
|
currentPrompt?: string
|
||||||
|
isBasicMode?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const PromptGeneratorBtn: FC<Props> = ({
|
const PromptGeneratorBtn: FC<Props> = ({
|
||||||
@ -26,6 +27,7 @@ const PromptGeneratorBtn: FC<Props> = ({
|
|||||||
nodeId,
|
nodeId,
|
||||||
editorId,
|
editorId,
|
||||||
currentPrompt,
|
currentPrompt,
|
||||||
|
isBasicMode,
|
||||||
}) => {
|
}) => {
|
||||||
const [showAutomatic, { setTrue: showAutomaticTrue, setFalse: showAutomaticFalse }] = useBoolean(false)
|
const [showAutomatic, { setTrue: showAutomaticTrue, setFalse: showAutomaticFalse }] = useBoolean(false)
|
||||||
const handleAutomaticRes = useCallback((res: GenRes) => {
|
const handleAutomaticRes = useCallback((res: GenRes) => {
|
||||||
@ -50,6 +52,8 @@ const PromptGeneratorBtn: FC<Props> = ({
|
|||||||
nodeId={nodeId}
|
nodeId={nodeId}
|
||||||
editorId={editorId}
|
editorId={editorId}
|
||||||
currentPrompt={currentPrompt}
|
currentPrompt={currentPrompt}
|
||||||
|
isBasicMode={isBasicMode}
|
||||||
|
hideTryIt
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import type { FC } from 'react'
|
import type { FC } from 'react'
|
||||||
import React, { useCallback } from 'react'
|
import React, { useCallback } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import MemoryConfig from '../_base/components/memory-config'
|
|
||||||
import VarReferencePicker from '../_base/components/variable/var-reference-picker'
|
import VarReferencePicker from '../_base/components/variable/var-reference-picker'
|
||||||
import ConfigVision from '../_base/components/config-vision'
|
import ConfigVision from '../_base/components/config-vision'
|
||||||
import useConfig from './use-config'
|
import useConfig from './use-config'
|
||||||
@ -22,6 +21,9 @@ import Switch from '@/app/components/base/switch'
|
|||||||
import { RiAlertFill, RiQuestionLine } from '@remixicon/react'
|
import { RiAlertFill, RiQuestionLine } from '@remixicon/react'
|
||||||
import { fetchAndMergeValidCompletionParams } from '@/utils/completion-params'
|
import { fetchAndMergeValidCompletionParams } from '@/utils/completion-params'
|
||||||
import Toast from '@/app/components/base/toast'
|
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'
|
const i18nPrefix = 'workflow.nodes.llm'
|
||||||
|
|
||||||
@ -53,7 +55,6 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
|
|||||||
handleVarListChange,
|
handleVarListChange,
|
||||||
handleVarNameChange,
|
handleVarNameChange,
|
||||||
handleSyeQueryChange,
|
handleSyeQueryChange,
|
||||||
handleMemoryChange,
|
|
||||||
handleVisionResolutionEnabledChange,
|
handleVisionResolutionEnabledChange,
|
||||||
handleVisionResolutionChange,
|
handleVisionResolutionChange,
|
||||||
isModelSupportStructuredOutput,
|
isModelSupportStructuredOutput,
|
||||||
@ -64,6 +65,7 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
|
|||||||
filterJinja2InputVar,
|
filterJinja2InputVar,
|
||||||
handleReasoningFormatChange,
|
handleReasoningFormatChange,
|
||||||
} = useConfig(id, data)
|
} = useConfig(id, data)
|
||||||
|
const { memoryType } = useMemory(id, data)
|
||||||
|
|
||||||
const model = inputs.model
|
const model = inputs.model
|
||||||
|
|
||||||
@ -151,6 +153,7 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
|
|||||||
varList={inputs.prompt_config?.jinja2_variables || []}
|
varList={inputs.prompt_config?.jinja2_variables || []}
|
||||||
handleAddVariable={handleAddVariable}
|
handleAddVariable={handleAddVariable}
|
||||||
modelConfig={model}
|
modelConfig={model}
|
||||||
|
isMemorySupported={memoryType === MemoryMode.block}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -174,7 +177,7 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Memory put place examples. */}
|
{/* Memory put place examples. */}
|
||||||
{isChatMode && isChatModel && !!inputs.memory && (
|
{isChatMode && isChatModel && (memoryType === MemoryMode.linear || memoryType === MemoryMode.block) && (
|
||||||
<div className='mt-4'>
|
<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 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'>
|
<div className='flex items-center space-x-1'>
|
||||||
@ -198,7 +201,7 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
|
|||||||
triggerClassName='w-4 h-4'
|
triggerClassName='w-4 h-4'
|
||||||
/>
|
/>
|
||||||
</div>}
|
</div>}
|
||||||
value={inputs.memory.query_prompt_template || '{{#sys.query#}}'}
|
value={inputs.memory?.query_prompt_template || '{{#sys.query#}}'}
|
||||||
onChange={handleSyeQueryChange}
|
onChange={handleSyeQueryChange}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
isShowContext={false}
|
isShowContext={false}
|
||||||
@ -208,9 +211,10 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
|
|||||||
nodesOutputVars={availableVars}
|
nodesOutputVars={availableVars}
|
||||||
availableNodes={availableNodesWithParent}
|
availableNodes={availableNodesWithParent}
|
||||||
isSupportFileVar
|
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 className='text-xs font-normal leading-[18px] text-[#DC6803]'>{t(`${i18nPrefix}.sysQueryInUser`)}</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -220,11 +224,10 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
|
|||||||
{/* Memory */}
|
{/* Memory */}
|
||||||
{isChatMode && (
|
{isChatMode && (
|
||||||
<>
|
<>
|
||||||
<Split />
|
<MemorySystem
|
||||||
<MemoryConfig
|
id={id}
|
||||||
|
data={data}
|
||||||
readonly={readOnly}
|
readonly={readOnly}
|
||||||
config={{ data: inputs.memory }}
|
|
||||||
onChange={handleMemoryChange}
|
|
||||||
canSetRoleName={isCompletionModel}
|
canSetRoleName={isCompletionModel}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -308,11 +308,11 @@ const useConfig = (id: string, payload: LLMNodeType) => {
|
|||||||
}, [setInputs, deleteNodeInspectorVars, id])
|
}, [setInputs, deleteNodeInspectorVars, id])
|
||||||
|
|
||||||
const filterInputVar = useCallback((varPayload: Var) => {
|
const filterInputVar = useCallback((varPayload: Var) => {
|
||||||
return [VarType.number, VarType.string, VarType.secret, VarType.arrayString, VarType.arrayNumber, VarType.file, VarType.arrayFile].includes(varPayload.type)
|
return [VarType.number, VarType.string, VarType.secret, VarType.arrayString, VarType.arrayNumber, VarType.file, VarType.arrayFile, VarType.memory].includes(varPayload.type)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const filterJinja2InputVar = useCallback((varPayload: Var) => {
|
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].includes(varPayload.type)
|
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)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const filterMemoryPromptVar = useCallback((varPayload: Var) => {
|
const filterMemoryPromptVar = useCallback((varPayload: Var) => {
|
||||||
|
|||||||
@ -26,7 +26,7 @@ import {
|
|||||||
arrayObjectPlaceholder,
|
arrayObjectPlaceholder,
|
||||||
arrayStringPlaceholder,
|
arrayStringPlaceholder,
|
||||||
objectPlaceholder,
|
objectPlaceholder,
|
||||||
} from '@/app/components/workflow/panel/chat-variable-panel/utils'
|
} from '@/app/components/workflow/panel/chat-variable-panel/constants'
|
||||||
import ArrayBoolList from '@/app/components/workflow/panel/chat-variable-panel/components/array-bool-list'
|
import ArrayBoolList from '@/app/components/workflow/panel/chat-variable-panel/components/array-bool-list'
|
||||||
|
|
||||||
type FormItemProps = {
|
type FormItemProps = {
|
||||||
|
|||||||
@ -38,7 +38,7 @@ const useConfig = (id: string, payload: LoopNodeType) => {
|
|||||||
const { nodesReadOnly: readOnly } = useNodesReadOnly()
|
const { nodesReadOnly: readOnly } = useNodesReadOnly()
|
||||||
const isChatMode = useIsChatMode()
|
const isChatMode = useIsChatMode()
|
||||||
const conversationVariables = useStore(s => s.conversationVariables)
|
const conversationVariables = useStore(s => s.conversationVariables)
|
||||||
|
const memoryVariables = useStore(s => s.memoryVariables)
|
||||||
const { inputs, setInputs } = useNodeCrud<LoopNodeType>(id, payload)
|
const { inputs, setInputs } = useNodeCrud<LoopNodeType>(id, payload)
|
||||||
const inputsRef = useRef(inputs)
|
const inputsRef = useRef(inputs)
|
||||||
const handleInputsChange = useCallback((newInputs: LoopNodeType) => {
|
const handleInputsChange = useCallback((newInputs: LoopNodeType) => {
|
||||||
@ -65,7 +65,7 @@ const useConfig = (id: string, payload: LoopNodeType) => {
|
|||||||
mcpTools: mcpTools || [],
|
mcpTools: mcpTools || [],
|
||||||
dataSourceList: dataSourceList || [],
|
dataSourceList: dataSourceList || [],
|
||||||
}
|
}
|
||||||
const childrenNodeVars = toNodeOutputVars(loopChildrenNodes, isChatMode, undefined, [], conversationVariables, [], allPluginInfoList)
|
const childrenNodeVars = toNodeOutputVars(loopChildrenNodes, isChatMode, undefined, [], conversationVariables, memoryVariables, [], allPluginInfoList)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
getIsVarFileAttribute,
|
getIsVarFileAttribute,
|
||||||
|
|||||||
@ -11,7 +11,7 @@ const VALID_PARAMETER_TYPES: readonly VarType[] = [
|
|||||||
] as const
|
] as const
|
||||||
|
|
||||||
// Type display name mappings
|
// Type display name mappings
|
||||||
const TYPE_DISPLAY_NAMES: Record<VarType, string> = {
|
const TYPE_DISPLAY_NAMES: Record<Exclude<VarType, VarType.memory>, string> = {
|
||||||
[VarType.string]: 'String',
|
[VarType.string]: 'String',
|
||||||
[VarType.number]: 'Number',
|
[VarType.number]: 'Number',
|
||||||
[VarType.boolean]: 'Boolean',
|
[VarType.boolean]: 'Boolean',
|
||||||
@ -91,7 +91,7 @@ export const normalizeParameterType = (input: string | undefined | null): VarTyp
|
|||||||
/**
|
/**
|
||||||
* Gets display name for parameter types in UI components
|
* Gets display name for parameter types in UI components
|
||||||
*/
|
*/
|
||||||
export const getParameterTypeDisplayName = (type: VarType): string => {
|
export const getParameterTypeDisplayName = (type: Exclude<VarType, VarType.memory>): string => {
|
||||||
return TYPE_DISPLAY_NAMES[type]
|
return TYPE_DISPLAY_NAMES[type]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -119,7 +119,7 @@ export const createParameterTypeOptions = (contentType?: string) => {
|
|||||||
const availableTypes = getAvailableParameterTypes(contentType)
|
const availableTypes = getAvailableParameterTypes(contentType)
|
||||||
|
|
||||||
return availableTypes.map(type => ({
|
return availableTypes.map(type => ({
|
||||||
name: getParameterTypeDisplayName(type),
|
name: getParameterTypeDisplayName(type as Exclude<VarType, VarType.memory>),
|
||||||
value: type,
|
value: type,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -52,7 +52,7 @@ const ArrayValueList: FC<Props> = ({
|
|||||||
<div className='flex items-center space-x-1' key={index}>
|
<div className='flex items-center space-x-1' key={index}>
|
||||||
<Input
|
<Input
|
||||||
placeholder={t('workflow.chatVariable.modal.arrayValue') || ''}
|
placeholder={t('workflow.chatVariable.modal.arrayValue') || ''}
|
||||||
value={list[index]}
|
value={list[index] || ''}
|
||||||
onChange={handleNameChange(index)}
|
onChange={handleNameChange(index)}
|
||||||
type={isString ? 'text' : 'number'}
|
type={isString ? 'text' : 'number'}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -0,0 +1,79 @@
|
|||||||
|
'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)
|
||||||
@ -0,0 +1,54 @@
|
|||||||
|
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)
|
||||||
@ -1,47 +1,87 @@
|
|||||||
import { memo, useState } from 'react'
|
import { memo, useMemo, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import { capitalize } from 'lodash-es'
|
import { capitalize } from 'lodash-es'
|
||||||
import { RiDeleteBinLine, RiEditLine } from '@remixicon/react'
|
import { RiDeleteBinLine, RiEditLine } from '@remixicon/react'
|
||||||
import { BubbleX } from '@/app/components/base/icons/src/vender/line/others'
|
import {
|
||||||
import type { ConversationVariable } from '@/app/components/workflow/types'
|
BubbleX,
|
||||||
|
Memory,
|
||||||
|
} from '@/app/components/base/icons/src/vender/line/others'
|
||||||
|
import type { ConversationVariable, MemoryVariable } from '@/app/components/workflow/types'
|
||||||
import cn from '@/utils/classnames'
|
import cn from '@/utils/classnames'
|
||||||
|
import { ChatVarType } from '../type'
|
||||||
|
import Badge from '@/app/components/base/badge'
|
||||||
|
|
||||||
type VariableItemProps = {
|
type VariableItemProps = {
|
||||||
item: ConversationVariable
|
item: ConversationVariable | MemoryVariable
|
||||||
onEdit: (item: ConversationVariable) => void
|
onEdit: (item: ConversationVariable | MemoryVariable) => void
|
||||||
onDelete: (item: ConversationVariable) => void
|
onDelete: (item: ConversationVariable | MemoryVariable) => void
|
||||||
|
scope?: string
|
||||||
|
term?: string
|
||||||
|
currentVarId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const VariableItem = ({
|
const VariableItem = ({
|
||||||
item,
|
item,
|
||||||
onEdit,
|
onEdit,
|
||||||
onDelete,
|
onDelete,
|
||||||
|
scope,
|
||||||
|
term,
|
||||||
|
currentVarId,
|
||||||
}: VariableItemProps) => {
|
}: VariableItemProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
const [destructive, setDestructive] = useState(false)
|
const [destructive, setDestructive] = useState(false)
|
||||||
|
const valueType = useMemo(() => {
|
||||||
|
if (item.value_type === ChatVarType.Memory)
|
||||||
|
return 'memory'
|
||||||
|
return item.value_type
|
||||||
|
}, [item.value_type])
|
||||||
return (
|
return (
|
||||||
<div className={cn(
|
<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',
|
'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',
|
destructive && 'border-state-destructive-border hover:bg-state-destructive-hover',
|
||||||
)}>
|
)}>
|
||||||
<div className='flex items-center justify-between'>
|
<div className='group flex items-center justify-between'>
|
||||||
<div className='flex grow items-center gap-1'>
|
<div className='flex grow items-center gap-1'>
|
||||||
<BubbleX className='h-4 w-4 text-util-colors-teal-teal-700' />
|
{
|
||||||
|
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' />
|
||||||
|
)
|
||||||
|
}
|
||||||
<div className='system-sm-medium text-text-primary'>{item.name}</div>
|
<div className='system-sm-medium text-text-primary'>{item.name}</div>
|
||||||
<div className='system-xs-medium text-text-tertiary'>{capitalize(item.value_type)}</div>
|
<div className='system-xs-medium text-text-tertiary'>{capitalize(valueType)}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex shrink-0 items-center gap-1 text-text-tertiary'>
|
<div className='flex shrink-0 items-center gap-1 text-text-tertiary'>
|
||||||
<div className='radius-md cursor-pointer p-1 hover:bg-state-base-hover hover:text-text-secondary'>
|
<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')}>
|
||||||
<RiEditLine className='h-4 w-4' onClick={() => onEdit(item)}/>
|
<RiEditLine className='h-4 w-4' onClick={() => onEdit(item)}/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className='radius-md cursor-pointer p-1 hover:bg-state-destructive-hover hover:text-text-destructive'
|
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')}
|
||||||
onMouseOver={() => setDestructive(true)}
|
onMouseOver={() => setDestructive(true)}
|
||||||
onMouseOut={() => setDestructive(false)}
|
onMouseOut={() => setDestructive(false)}
|
||||||
>
|
>
|
||||||
<RiDeleteBinLine className='h-4 w-4' onClick={() => onDelete(item)}/>
|
<RiDeleteBinLine className='h-4 w-4' onClick={() => onDelete(item)}/>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
<div className='system-xs-regular truncate text-text-tertiary'>{item.description}</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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,15 +9,15 @@ import {
|
|||||||
PortalToFollowElemContent,
|
PortalToFollowElemContent,
|
||||||
PortalToFollowElemTrigger,
|
PortalToFollowElemTrigger,
|
||||||
} from '@/app/components/base/portal-to-follow-elem'
|
} from '@/app/components/base/portal-to-follow-elem'
|
||||||
import type { ConversationVariable } from '@/app/components/workflow/types'
|
import type { ConversationVariable, MemoryVariable } from '@/app/components/workflow/types'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
open: boolean
|
open: boolean
|
||||||
setOpen: (value: React.SetStateAction<boolean>) => void
|
setOpen: (value: React.SetStateAction<boolean>) => void
|
||||||
showTip: boolean
|
showTip: boolean
|
||||||
chatVar?: ConversationVariable
|
chatVar?: ConversationVariable | MemoryVariable
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
onSave: (env: ConversationVariable) => void
|
onSave: (env: ConversationVariable | MemoryVariable) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const VariableModalTrigger = ({
|
const VariableModalTrigger = ({
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user