4.8.13 feature (#3118)

* chore(ui): login page & workflow page (#3046)

* login page & number input & multirow select & llm select

* workflow

* adjust nodes

* New file upload (#3058)

* feat: toolNode aiNode readFileNode adapt new version

* update docker-compose

* update tip

* feat: adapt new file version

* perf: file input

* fix: ts

* feat: add chat history time label (#3024)

* feat:add chat and logs time

* feat: add chat history time label

* code perf

* code perf

---------

Co-authored-by: 勤劳上班的卑微小张 <jiazhan.zhang@ggimage.com>

* add chatType (#3060)

* pref: slow query of full text search (#3044)

* Adapt findLast api;perf: markdown zh format. (#3066)

* perf: context code

* fix: adapt findLast api

* perf: commercial plugin run error

* perf: markdown zh format

* perf: dockerfile proxy (#3067)

* fix ui (#3065)

* fix ui

* fix

* feat: support array reference multi-select (#3041)

* feat: support array reference multi-select

* fix build

* fix

* fix loop multi-select

* adjust condition

* fix get value

* array and non-array conversion

* fix plugin input

* merge func

* feat: iframe code block;perf: workflow selector type (#3076)

* feat: iframe code block

* perf: workflow selector type

* node pluginoutput check (#3074)

* feat: View will move when workflow check error;fix: ui refresh error when continuous file upload (#3077)

* fix: plugin output check

* fix: ui refresh error when continuous file upload

* feat: View will move when workflow check error

* add dispatch try catch (#3075)

* perf: workflow context split (#3083)

* perf: workflow context split

* perf: context

* 4.8.13 test (#3085)

* perf: workflow node ui

* chat iframe url

* feat: support sub route config (#3071)

* feat: support sub route config

* dockerfile

* fix upload

* delete unused code

* 4.8.13 test (#3087)

* fix: image expired

* fix: datacard navbar ui

* perf: build action

* fix: workflow file upload refresh (#3088)

* fix: http tool response (#3097)

* loop node dynamic height (#3092)

* loop node dynamic height

* fix

* fix

* feat: support push chat log (#3093)

* feat: custom uid/metadata

* to: custom info

* fix: chat push latest

* feat: add chat log envs

* refactor: move timer to pushChatLog

* fix: using precise log

---------

Co-authored-by: Finley Ge <m13203533462@163.com>

* 4.8.13 test (#3098)

* perf: loop node refresh

* rename context

* comment

* fix: ts

* perf: push chat log

* array reference check & node ui (#3100)

* feat: loop start add index (#3101)

* feat: loop start add index

* update doc

* 4.8.13 test (#3102)

* fix: loop index;edge parent check

* perf: reference invalid check

* fix: ts

* fix: plugin select files and ai response check (#3104)

* fix: plugin select files and ai response check

* perf: text editor selector;tool call tip;remove invalid image url;

* perf: select file

* perf: drop files

* feat: source id prefix env (#3103)

* 4.8.13 test (#3106)

* perf: select file

* perf: drop files

* perf: env template

* 4.8.13 test (#3107)

* perf: select file

* perf: drop files

* fix: imple mode adapt files

* perf: push chat log (#3109)

* fix: share page load title error (#3111)

* 4.8.13 perf (#3112)

* fix: share page load title error

* update file input doc

* perf: auto add file urls

* perf: auto ser loop node offset height

* 4.8.13 test (#3117)

* perf: plugin

* updat eaction

* feat: add more share config (#3120)

* feat: add more share config

* add i18n en

* fix: missing subroute (#3121)

* perf: outlink config (#3128)

* update action

* perf: outlink config

* fix: ts (#3129)

* 更新 docSite 文档内容 (#3131)

* fix: null pointer (#3130)

* fix: null pointer

* perf: not input text

* update doc url

* perf: outlink default value (#3134)

* update doc (#3136)

* 4.8.13 test (#3137)

* update doc

* perf: completions chat api

* Restore docSite content based on upstream/4.8.13-dev (#3138)

* Restore docSite content based on upstream/4.8.13-dev

* 4813.md缺少更正

* update doc (#3141)

---------

Co-authored-by: heheer <heheer@sealos.io>
Co-authored-by: papapatrick <109422393+Patrickill@users.noreply.github.com>
Co-authored-by: 勤劳上班的卑微小张 <jiazhan.zhang@ggimage.com>
Co-authored-by: Finley Ge <32237950+FinleyGe@users.noreply.github.com>
Co-authored-by: a.e. <49438478+I-Info@users.noreply.github.com>
Co-authored-by: Finley Ge <m13203533462@163.com>
Co-authored-by: Jiangween <145003935+Jiangween@users.noreply.github.com>
This commit is contained in:
Archer
2024-11-13 11:29:53 +08:00
committed by GitHub
parent e1f5483432
commit e9d52ada73
449 changed files with 7626 additions and 4180 deletions

View File

@@ -16,6 +16,9 @@ OPENAI_BASE_URL=https://api.openai.com/v1
# 通用key。可以是 openai 的也可以是 oneapi 的。
# 此处逻辑:优先走 ONEAPI_URL如果填写了 ONEAPI_URLkey 也需要是 ONEAPI 的 key
CHAT_API_KEY=sk-xxxx
# 是否将图片转成 base64 传递给模型,本地开发和内网环境使用共有模型时候需要设置为 true
MULTIPLE_DATA_TO_BASE64=true
# mongo 数据库连接参数,本地开发连接远程数据库时,可能需要增加 directConnection=true 参数,才能连接上。
MONGODB_URI=mongodb://username:password@0.0.0.0:27017/fastgpt?authSource=admin
@@ -32,6 +35,8 @@ SANDBOX_URL=http://localhost:3001
PRO_URL=
# 页面的地址,用于自动补全相对路径资源的 domain
FE_DOMAIN=http://localhost:3000
# 二级路由,需要打包时候就确定
# NEXT_PUBLIC_BASE_URL=/fastai
# 日志等级: debug, info, warn, error
LOG_LEVEL=debug
@@ -41,4 +46,12 @@ STORE_LOG_LEVEL=warn
# 工作流最大运行次数,避免极端的死循环情况
WORKFLOW_MAX_RUN_TIMES=500
# 循环最大运行次数,避免极端的死循环情况
WORKFLOW_MAX_LOOP_TIMES=50
WORKFLOW_MAX_LOOP_TIMES=50
# 对话日志推送服务
# # 日志服务地址
# CHAT_LOG_URL=http://localhost:8080
# # 日志推送间隔
# CHAT_LOG_INTERVAL=10000
# # 日志来源ID前缀
# CHAT_LOG_SOURCE_ID_PREFIX=fastgpt-

View File

@@ -6,8 +6,6 @@ ARG proxy
RUN [ -z "$proxy" ] || sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories
RUN apk add --no-cache libc6-compat && npm install -g pnpm@9.4.0
# if proxy exists, set proxy
RUN [ -z "$proxy" ] || pnpm config set registry https://registry.npmmirror.com
# copy packages and one project
COPY pnpm-lock.yaml pnpm-workspace.yaml .npmrc ./
@@ -16,13 +14,19 @@ COPY ./projects/app/package.json ./projects/app/package.json
RUN [ -f pnpm-lock.yaml ] || (echo "Lockfile not found." && exit 1)
RUN pnpm i
# if proxy exists, set proxy
RUN if [ -z "$proxy" ]; then \
pnpm i; \
else \
pnpm i --registry=https://registry.npmmirror.com; \
fi
# --------- builder -----------
FROM node:20.14.0-alpine AS builder
WORKDIR /app
ARG proxy
ARG base_url
# copy common node_modules and one project node_modules
COPY package.json pnpm-workspace.yaml .npmrc tsconfig.json ./
@@ -36,6 +40,7 @@ RUN [ -z "$proxy" ] || sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /
RUN apk add --no-cache libc6-compat && npm install -g pnpm@9.4.0
ENV NODE_OPTIONS="--max-old-space-size=4096"
ENV NEXT_PUBLIC_BASE_URL=$base_url
RUN pnpm --filter=app build
# --------- runner -----------
@@ -43,6 +48,7 @@ FROM node:20.14.0-alpine AS runner
WORKDIR /app
ARG proxy
ARG base_url
# create user and use it
RUN addgroup --system --gid 1001 nodejs
@@ -78,6 +84,7 @@ RUN chown -R nextjs:nodejs /app/data
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
ENV PORT=3000
ENV NEXT_PUBLIC_BASE_URL=$base_url
EXPOSE 3000

View File

@@ -6,6 +6,7 @@ const isDev = process.env.NODE_ENV === 'development';
/** @type {import('next').NextConfig} */
const nextConfig = {
basePath: process.env.NEXT_PUBLIC_BASE_URL,
i18n,
output: 'standalone',
reactStrictMode: isDev ? false : true,

View File

@@ -1,6 +1,6 @@
{
"name": "app",
"version": "4.8.12",
"version": "4.8.13",
"private": false,
"scripts": {
"dev": "next dev",

View File

@@ -1,12 +1,37 @@
<svg width="1041" height="1348" viewBox="0 0 1041 1348" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M340.837 0.33933L681.068 0.338989V0.455643C684.032 0.378397 686.999 0.339702 689.967 0.339702C735.961 0.3397 781.504 9.62899 823.997 27.6772C866.49 45.7254 905.099 72.1791 937.622 105.528C970.144 138.877 995.942 178.467 1013.54 222.04C1031.14 265.612 1040.2 312.312 1040.2 359.474L340.836 359.474L340.836 1347.84C296.157 1347.84 251.914 1338.55 210.636 1320.49C169.357 1302.43 131.85 1275.95 100.257 1242.58C68.6636 1209.21 43.6023 1169.59 26.5041 1125.99C11.3834 1087.43 2.75216 1046.42 0.957956 1004.81H0.605869L0.605897 368.098H0.70363C0.105752 341.831 2.23741 315.443 7.14306 289.411C20.2709 219.745 52.6748 155.754 100.257 105.528C147.839 55.3017 208.462 21.0975 274.461 7.24017C296.426 2.62833 318.657 0.339101 340.837 0.33933Z" fill="url(#paint0_linear_1172_228)"/>
<path d="M633.639 904.645H513.029V576.37H635.422V576.377C678.161 576.607 720.454 585.093 759.951 601.37C799.997 617.874 836.384 642.064 867.033 672.559C897.683 703.054 921.996 739.257 938.583 779.101C955.171 818.944 963.709 861.648 963.709 904.775H633.639V904.645Z" fill="url(#paint1_linear_1172_228)"/>
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21.5938 10.9C21.5938 9.45229 21.1475 8.03706 20.3113 6.8333C19.4751 5.62954 18.2866 4.69132 16.8961 4.13729C15.5056 3.58326 13.9755 3.4383 12.4993 3.72075C11.0231 4.00319 9.66715 4.70034 8.60288 5.72406C7.53861 6.74777 6.81384 8.05206 6.52021 9.47199C6.22658 10.8919 6.37728 12.3637 6.95326 13.7013C7.52923 15.0388 8.50462 16.182 9.75606 16.9864C11.0075 17.7907 12.4788 18.22 13.9839 18.22V10.9H21.5938Z" fill="url(#paint0_linear_0_5622)"/>
<path d="M29.6263 10.9C29.6263 9.93877 29.4498 8.98691 29.1069 8.09882C28.764 7.21072 28.2613 6.40377 27.6277 5.72405C26.9941 5.04433 26.2418 4.50515 25.414 4.13729C24.5861 3.76943 23.6987 3.58009 22.8027 3.58009C21.9066 3.58009 21.0192 3.76943 20.1913 4.13729C19.3635 4.50515 18.6112 5.04434 17.9776 5.72406C17.344 6.40378 16.8413 7.21072 16.4984 8.09882C16.1555 8.98692 15.979 9.93877 15.979 10.9L29.6263 10.9Z" fill="url(#paint1_linear_0_5622)"/>
<path d="M28.3745 22.634C28.3745 21.8174 28.2342 21.0087 27.9617 20.2543C27.6892 19.4998 27.2898 18.8143 26.7863 18.2368C26.2828 17.6594 25.685 17.2013 25.0272 16.8888C24.3693 16.5763 23.6642 16.4155 22.9521 16.4155V22.634H28.3745Z" fill="url(#paint2_linear_0_5622)"/>
<path d="M13.9836 20.3912C12.9843 20.3912 11.9947 20.5635 11.0715 20.8985C10.1482 21.2334 9.30926 21.7242 8.60262 22.3431C7.89597 22.9619 7.33543 23.6966 6.95299 24.5051C6.57056 25.3137 6.37372 26.1803 6.37372 27.0554C6.37372 27.9306 6.57056 28.7972 6.95299 29.6057C7.33543 30.4143 7.89597 31.1489 8.60262 31.7678C9.30926 32.3866 10.1482 32.8775 11.0715 33.2124C11.9947 33.5473 12.9843 33.7197 13.9836 33.7197L13.9836 20.3912Z" fill="url(#paint3_linear_0_5622)"/>
<path d="M13.9837 10.6101L13.9837 26.9823L6.3736 26.9823L6.3736 10.6101H13.9837Z" fill="url(#paint4_linear_0_5622)"/>
<path d="M23.0327 10.8988L13.8973 10.8988L13.8973 3.58008L23.0327 3.58008V10.8988Z" fill="url(#paint5_linear_0_5622)"/>
<path d="M23.0327 22.6316H17.9771V16.4155L23.0327 16.4155V22.6316Z" fill="url(#paint6_linear_0_5622)"/>
<defs>
<linearGradient id="paint0_linear_1172_228" x1="520.404" y1="0.338989" x2="520.404" y2="1347.84" gradientUnits="userSpaceOnUse">
<linearGradient id="paint0_linear_0_5622" x1="18" y1="3.58008" x2="18" y2="33.7197" gradientUnits="userSpaceOnUse">
<stop stop-color="#326DFF"/>
<stop offset="1" stop-color="#8EAEFF"/>
</linearGradient>
<linearGradient id="paint1_linear_1172_228" x1="738.369" y1="576.37" x2="738.369" y2="904.775" gradientUnits="userSpaceOnUse">
<linearGradient id="paint1_linear_0_5622" x1="18" y1="3.58008" x2="18" y2="33.7197" gradientUnits="userSpaceOnUse">
<stop stop-color="#326DFF"/>
<stop offset="1" stop-color="#8EAEFF"/>
</linearGradient>
<linearGradient id="paint2_linear_0_5622" x1="18" y1="3.58008" x2="18" y2="33.7197" gradientUnits="userSpaceOnUse">
<stop stop-color="#326DFF"/>
<stop offset="1" stop-color="#8EAEFF"/>
</linearGradient>
<linearGradient id="paint3_linear_0_5622" x1="18" y1="3.58008" x2="18" y2="33.7197" gradientUnits="userSpaceOnUse">
<stop stop-color="#326DFF"/>
<stop offset="1" stop-color="#8EAEFF"/>
</linearGradient>
<linearGradient id="paint4_linear_0_5622" x1="18" y1="3.58008" x2="18" y2="33.7197" gradientUnits="userSpaceOnUse">
<stop stop-color="#326DFF"/>
<stop offset="1" stop-color="#8EAEFF"/>
</linearGradient>
<linearGradient id="paint5_linear_0_5622" x1="18" y1="3.58008" x2="18" y2="33.7197" gradientUnits="userSpaceOnUse">
<stop stop-color="#326DFF"/>
<stop offset="1" stop-color="#8EAEFF"/>
</linearGradient>
<linearGradient id="paint6_linear_0_5622" x1="18" y1="3.58008" x2="18" y2="33.7197" gradientUnits="userSpaceOnUse">
<stop stop-color="#326DFF"/>
<stop offset="1" stop-color="#8EAEFF"/>
</linearGradient>

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -0,0 +1,4 @@
<svg width="12" height="11" viewBox="0 0 12 11" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.45093 7.94055L8.82385 0.567627" stroke="#8A95A7" stroke-linecap="round"/>
<path d="M4.8418 7.953L8.82373 3.97107" stroke="#8A95A7" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 271 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 48 KiB

View File

@@ -0,0 +1,24 @@
import React from 'react';
import { Box } from '@chakra-ui/react';
import { useMarkdownWidth } from '../hooks';
const IframeBlock = ({ code }: { code: string }) => {
const { width, Ref } = useMarkdownWidth();
return (
<Box w={width} ref={Ref}>
<iframe
src={code}
sandbox="allow-scripts allow-forms allow-popups allow-downloads allow-presentation allow-storage-access-by-user-activation"
referrerPolicy="no-referrer"
style={{
width: '100%',
height: '100%',
minHeight: '70vh',
border: 'none'
}}
/>
</Box>
);
};
export default IframeBlock;

View File

@@ -0,0 +1,25 @@
import React, { useEffect, useRef, useCallback, useState } from 'react';
import { Box } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useMarkdownWidth } from '../hooks';
const MermaidBlock = ({ code }: { code: string }) => {
const { width, Ref } = useMarkdownWidth();
return (
<Box w={width} ref={Ref}>
<iframe
src={code}
sandbox="allow-scripts allow-forms allow-popups allow-downloads allow-presentation allow-storage-access-by-user-activation"
referrerPolicy="no-referrer"
style={{
width: '100%',
height: '100%',
minHeight: '70vh',
border: 'none'
}}
/>
</Box>
);
};
export default MermaidBlock;

View File

@@ -0,0 +1,34 @@
import { useScreen } from '@fastgpt/web/hooks/useScreen';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
import { useCallback, useEffect, useRef, useState } from 'react';
export const useMarkdownWidth = () => {
const Ref = useRef<HTMLDivElement>(null);
const [width, setWidth] = useState(400);
const { screenWidth } = useScreen();
const { isPc } = useSystem();
const findMarkdownDom = useCallback(() => {
if (!Ref.current) return;
// 一直找到 parent = markdown 的元素
let parent = Ref.current?.parentElement;
while (parent && !parent.className.includes('chat-box-card')) {
parent = parent.parentElement;
}
const ChatItemDom = parent?.parentElement;
const clientWidth = ChatItemDom?.clientWidth ? ChatItemDom.clientWidth - (isPc ? 90 : 60) : 500;
setWidth(clientWidth);
return parent?.parentElement;
}, [isPc]);
useEffect(() => {
findMarkdownDom();
}, [findMarkdownDom, screenWidth, Ref.current]);
return {
Ref,
width
};
};

View File

@@ -22,6 +22,7 @@ const CodeLight = dynamic(() => import('./CodeLight'), { ssr: false });
const MermaidCodeBlock = dynamic(() => import('./img/MermaidCodeBlock'), { ssr: false });
const MdImage = dynamic(() => import('./img/Image'), { ssr: false });
const EChartsCodeBlock = dynamic(() => import('./img/EChartsCodeBlock'), { ssr: false });
const IframeCodeBlock = dynamic(() => import('./codeBlock/Iframe'), { ssr: false });
const ChatGuide = dynamic(() => import('./chat/Guide'), { ssr: false });
const QuestionGuide = dynamic(() => import('./chat/QuestionGuide'), { ssr: false });
@@ -29,11 +30,13 @@ const QuestionGuide = dynamic(() => import('./chat/QuestionGuide'), { ssr: false
const Markdown = ({
source = '',
showAnimation = false,
isDisabled = false
isDisabled = false,
forbidZhFormat = false
}: {
source?: string;
showAnimation?: boolean;
isDisabled?: boolean;
forbidZhFormat?: boolean;
}) => {
const components = useMemo<any>(
() => ({
@@ -46,15 +49,35 @@ const Markdown = ({
);
const formatSource = useMemo(() => {
const formatSource = source
if (showAnimation || forbidZhFormat) return source;
// 保护 URL 格式https://, http://, /api/xxx
const urlPlaceholders: string[] = [];
const textWithProtectedUrls = source.replace(
/(https?:\/\/[^\s<]+[^<.,:;"')\]\s]|\/api\/[^\s]+)(?=\s|$)/g,
(match) => {
urlPlaceholders.push(match);
return `__URL_${urlPlaceholders.length - 1}__`;
}
);
// 处理中文与英文数字之间的分词
const textWithSpaces = textWithProtectedUrls
.replace(
/([\u4e00-\u9fa5\u3000-\u303f])([a-zA-Z0-9])|([a-zA-Z0-9])([\u4e00-\u9fa5\u3000-\u303f])/g,
'$1$3 $2$4'
) // Chinese and english chars separated by space
)
// 处理引用标记
.replace(/\n*(\[QUOTE SIGN\]\(.*\))/g, '$1');
return formatSource;
}, [source]);
// 还原 URL
const finalText = textWithSpaces.replace(
/__URL_(\d+)__/g,
(_, index) => urlPlaceholders[parseInt(index)]
);
return finalText;
}, [forbidZhFormat, showAnimation, source]);
const urlTransform = useCallback((val: string) => {
return val;
@@ -101,6 +124,9 @@ function Code(e: any) {
if (codeType === CodeClassNameEnum.echarts) {
return <EChartsCodeBlock code={strChildren} />;
}
if (codeType === CodeClassNameEnum.iframe) {
return <IframeCodeBlock code={strChildren} />;
}
return (
<CodeLight className={className} codeBlock={codeBlock} match={match}>

View File

@@ -5,7 +5,8 @@ export enum CodeClassNameEnum {
echarts = 'echarts',
quote = 'quote',
files = 'files',
latex = 'latex'
latex = 'latex',
iframe = 'iframe'
}
function htmlTableToLatex(html: string) {

View File

@@ -1,5 +1,6 @@
import React, { useState } from 'react';
import { Image, Skeleton, ImageProps } from '@chakra-ui/react';
import { Skeleton, ImageProps } from '@chakra-ui/react';
import CustomImage from '@fastgpt/web/components/common/Image/MyImage';
export const MyImage = (props: ImageProps) => {
const [isLoading, setIsLoading] = useState(true);
@@ -13,7 +14,7 @@ export const MyImage = (props: ImageProps) => {
justifyContent={'center'}
my={1}
>
<Image
<CustomImage
display={'inline-block'}
borderRadius={'md'}
alt={''}

View File

@@ -82,7 +82,7 @@ const AIChatSettingsModal = ({
{t('common:core.ai.AI settings')}
{feConfigs?.docUrl && (
<Link
href={getDocPath('/docs/course/ai_settings/')}
href={getDocPath('/docs/guide/course/ai_settings/')}
target={'_blank'}
ml={1}
textDecoration={'underline'}

View File

@@ -68,7 +68,9 @@ const SettingLLMModel = ({
<Button
w={'100%'}
justifyContent={'flex-start'}
variant={'whiteBase'}
variant={'whitePrimaryOutline'}
size={'lg'}
fontSize={'sm'}
bg={bg}
_active={{
transform: 'none'
@@ -81,8 +83,9 @@ const SettingLLMModel = ({
w={'18px'}
/>
}
rightIcon={<MyIcon name={'common/select'} w={'1rem'} />}
pl={4}
rightIcon={<MyIcon name={'common/select'} w={'1.2rem'} color={'myGray.500'} />}
px={3}
pr={2}
onClick={onOpenAIChatSetting}
>
<Box flex={1} textAlign={'left'}>

View File

@@ -59,7 +59,9 @@ const FileSelect = ({
return (
<Flex alignItems={'center'}>
<MyIcon name={'core/app/simpleMode/file'} mr={2} w={'20px'} />
<FormLabel {...labelStyle}>{t('app:file_upload')}</FormLabel>
<FormLabel color={'myGray.600'} {...labelStyle}>
{t('app:file_upload')}
</FormLabel>
<ChatFunctionTip type={'file'} />
<Box flex={1} />
<MyTooltip label={t('app:config_file_upload')}>
@@ -68,6 +70,7 @@ const FileSelect = ({
iconSpacing={1}
size={'sm'}
mr={'-5px'}
color={'myGray.600'}
onClick={onOpen}
>
{formLabel}

View File

@@ -87,7 +87,7 @@ const InputGuideConfig = ({
<Flex alignItems={'center'}>
<MyIcon name={'core/app/inputGuides'} mr={2} w={'20px'} />
<Flex alignItems={'center'}>
<FormLabel>{chatT('input_guide')}</FormLabel>
<FormLabel color={'myGray.600'}>{chatT('input_guide')}</FormLabel>
<ChatFunctionTip type={'inputGuide'} />
</Flex>
<Box flex={1} />
@@ -97,6 +97,7 @@ const InputGuideConfig = ({
iconSpacing={1}
size={'sm'}
mr={'-5px'}
color={'myGray.600'}
onClick={onOpen}
>
{formLabel}
@@ -145,7 +146,7 @@ const InputGuideConfig = ({
<Flex mt={8} alignItems={'center'}>
<FormLabel>{chatT('custom_input_guide_url')}</FormLabel>
<Flex
onClick={() => window.open(getDocPath('/docs/course/chat_input_guide'))}
onClick={() => window.open(getDocPath('/docs/guide/course/chat_input_guide/'))}
color={'primary.700'}
alignItems={'center'}
cursor={'pointer'}

View File

@@ -11,7 +11,7 @@ const QGSwitch = (props: SwitchProps) => {
return (
<Flex alignItems={'center'}>
<MyIcon name={'core/chat/QGFill'} mr={2} w={'20px'} />
<FormLabel>{t('common:core.app.Question Guide')}</FormLabel>
<FormLabel color={'myGray.600'}>{t('common:core.app.Question Guide')}</FormLabel>
<ChatFunctionTip type={'nextQuestion'} />
<Box flex={1} />
<Switch {...props} />

View File

@@ -270,7 +270,7 @@ const ScheduledTriggerConfig = ({
<Flex alignItems={'center'}>
<MyIcon name={'core/app/schedulePlan'} w={'20px'} />
<HStack ml={2} flex={1} spacing={1}>
<FormLabel>{t('common:core.app.Interval timer run')}</FormLabel>
<FormLabel color={'myGray.600'}>{t('common:core.app.Interval timer run')}</FormLabel>
<QuestionTip label={t('common:core.app.Interval timer tip')} />
</HStack>
<MyTooltip label={t('common:core.app.Config schedule plan')}>
@@ -279,6 +279,7 @@ const ScheduledTriggerConfig = ({
iconSpacing={1}
size={'sm'}
mr={'-5px'}
color={'myGray.600'}
onClick={onOpen}
>
{formatLabel}

View File

@@ -13,6 +13,7 @@ import MySelect from '@fastgpt/web/components/common/MySelect';
import { defaultTTSConfig } from '@fastgpt/global/core/app/constants';
import ChatFunctionTip from './Tip';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import MyImage from '@fastgpt/web/components/common/Image/MyImage';
const TTSSelect = ({
value = defaultTTSConfig,
@@ -82,7 +83,7 @@ const TTSSelect = ({
return (
<Flex alignItems={'center'}>
<MyIcon name={'core/app/simpleMode/tts'} mr={2} w={'20px'} />
<FormLabel>{t('common:core.app.TTS')}</FormLabel>
<FormLabel color={'myGray.600'}>{t('common:core.app.TTS')}</FormLabel>
<ChatFunctionTip type={'tts'} />
<Box flex={1} />
<MyTooltip label={t('common:core.app.Select TTS')}>
@@ -92,6 +93,7 @@ const TTSSelect = ({
size={'sm'}
mr={'-5px'}
onClick={onOpen}
color={'myGray.600'}
>
{formLabel}
</Button>
@@ -132,7 +134,7 @@ const TTSSelect = ({
<Flex mt={10} justifyContent={'end'}>
{audioPlaying ? (
<Flex>
<Image src="/icon/speaking.gif" w={'24px'} alt={''} />
<MyImage src="/icon/speaking.gif" w={'24px'} alt={''} />
<Button
ml={2}
variant={'grayBase'}

View File

@@ -1,5 +1,5 @@
import { useI18n } from '@/web/context/I18n';
import { Box, Flex, Image } from '@chakra-ui/react';
import { Box, Flex } from '@chakra-ui/react';
import MyImage from '@fastgpt/web/components/common/Image/MyImage';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import { useTranslation } from 'next-i18next';
import React, { useRef } from 'react';
@@ -58,8 +58,8 @@ const ChatFunctionTip = ({ type }: { type: `${FnTypeEnum}` }) => {
[FnTypeEnum.visionModel]: {
icon: '/imgs/app/question.svg',
title: t('app:vision_model_title'),
desc: t('app:llm_use_vision_tip'),
imgUrl: '/imgs/app/visionModel.png'
desc: t('app:open_vision_function_tip'),
imgUrl: '/imgs/app/visionModel.svg'
},
[FnTypeEnum.instruction]: {
icon: '/imgs/app/help.svg',
@@ -77,7 +77,7 @@ const ChatFunctionTip = ({ type }: { type: `${FnTypeEnum}` }) => {
label={
<Box pt={2}>
<Flex alignItems={'flex-start'}>
<Image src={data.icon} w={'36px'} alt={''} />
<MyImage src={data.icon} w={'36px'} alt={''} />
<Box ml={3}>
<Box fontWeight="bold">{data.title}</Box>
<Box fontSize={'xs'} color={'myGray.500'}>
@@ -85,7 +85,7 @@ const ChatFunctionTip = ({ type }: { type: `${FnTypeEnum}` }) => {
</Box>
</Box>
</Flex>
<Image src={data.imgUrl} w={'100%'} minH={['auto', '250px']} mt={2} alt={''} />
<MyImage src={data.imgUrl} w={'100%'} minH={['auto', '250px']} mt={2} alt={''} />
</Box>
}
/>

View File

@@ -65,10 +65,6 @@ const VariableEdit = ({
const { setValue, reset, watch, getValues } = form;
const value = getValues();
const type = watch('type');
const valueType = watch('valueType');
const max = watch('max');
const min = watch('min');
const defaultValue = watch('defaultValue');
const inputTypeList = useMemo(
() =>
@@ -173,7 +169,9 @@ const VariableEdit = ({
{/* Row box */}
<Flex alignItems={'center'}>
<MyIcon name={'core/app/simpleMode/variable'} w={'20px'} />
<FormLabel ml={2}>{t('common:core.module.Variable')}</FormLabel>
<FormLabel ml={2} color={'myGray.600'}>
{t('common:core.module.Variable')}
</FormLabel>
<ChatFunctionTip type={'variable'} />
<Box flex={1} />
<Button
@@ -181,6 +179,7 @@ const VariableEdit = ({
leftIcon={<SmallAddIcon />}
iconSpacing={1}
size={'sm'}
color={'myGray.600'}
mr={'-5px'}
onClick={() => {
reset(addVariable());
@@ -193,60 +192,83 @@ const VariableEdit = ({
{formatVariables.length > 0 && (
<Box mt={2} borderRadius={'md'} overflow={'hidden'} borderWidth={'1px'} borderBottom="none">
<TableContainer>
<Table>
<Thead>
<Table bg={'white'}>
<Thead h={8}>
<Tr>
<Th
borderRadius={'none !important'}
fontSize={'mini'}
bg={'myGray.50'}
p={0}
px={4}
fontWeight={'medium'}
>
{t('workflow:Variable_name')}
</Th>
<Th fontSize={'mini'} bg={'myGray.50'} p={0} px={4} fontWeight={'medium'}>
{t('common:common.Require Input')}
</Th>
<Th
fontSize={'mini'}
borderRadius={'none !important'}
w={'18px !important'}
bg={'myGray.50'}
p={0}
/>
<Th fontSize={'mini'}>{t('workflow:Variable_name')}</Th>
<Th fontSize={'mini'}>{t('app:global_variables_desc')}</Th>
<Th fontSize={'mini'}>{t('common:common.Require Input')}</Th>
<Th fontSize={'mini'} borderRadius={'none !important'}></Th>
px={4}
fontWeight={'medium'}
>
{t('common:common.Operation')}
</Th>
</Tr>
</Thead>
<Tbody>
{formatVariables.map((item) => (
<Tr key={item.id}>
<Td p={0} pl={3}>
<MyIcon name={item.icon as any} w={'16px'} color={'myGray.500'} />
</Td>
<Td>{item.key}</Td>
<Td
maxW={'200px'}
fontSize={'sm'}
whiteSpace={'pre-wrap'}
wordBreak={'break-all'}
px={0}
p={0}
px={4}
h={8}
color={'myGray.900'}
fontSize={'mini'}
fontWeight={'medium'}
>
{item.description || '-'}
<Flex alignItems={'center'}>
<MyIcon name={item.icon as any} w={'16px'} color={'myGray.400'} mr={2} />
{item.key}
</Flex>
</Td>
<Td>{item.required ? '✔' : '-'}</Td>
<Td>
<MyIcon
mr={3}
name={'common/settingLight'}
w={'16px'}
cursor={'pointer'}
onClick={() => {
const formattedItem = {
...item,
list: item.enums || []
};
reset(formattedItem);
}}
/>
<MyIcon
name={'delete'}
w={'16px'}
cursor={'pointer'}
onClick={() =>
onChange(variables.filter((variable) => variable.id !== item.id))
}
/>
<Td p={0} px={4} h={8} color={'myGray.900'} fontSize={'mini'}>
<Flex alignItems={'center'}>
{item.required ? (
<MyIcon name={'check'} w={'16px'} color={'myGray.900'} mr={2} />
) : (
''
)}
</Flex>
</Td>
<Td p={0} px={4} h={8} color={'myGray.600'} fontSize={'mini'}>
<Flex alignItems={'center'}>
<MyIcon
mr={3}
name={'common/settingLight'}
w={'16px'}
cursor={'pointer'}
onClick={() => {
const formattedItem = {
...item,
list: item.enums || []
};
reset(formattedItem);
}}
/>
<MyIcon
name={'delete'}
w={'16px'}
cursor={'pointer'}
onClick={() =>
onChange(variables.filter((variable) => variable.id !== item.id))
}
/>
</Flex>
</Td>
</Tr>
))}
@@ -337,11 +359,7 @@ const VariableEdit = ({
type={'variable'}
isEdit={!!value.key}
inputType={type}
valueType={valueType}
defaultValue={defaultValue}
defaultValueType={defaultValueType}
max={max}
min={min}
onClose={() => reset({})}
onSubmitSuccess={onSubmitSuccess}
onSubmitError={onSubmitError}

View File

@@ -13,17 +13,20 @@ const WelcomeTextConfig = (props: TextareaProps) => {
<>
<Flex alignItems={'center'}>
<MyIcon name={'core/app/simpleMode/chat'} w={'20px'} />
<FormLabel ml={2}>{t('common:core.app.Welcome Text')}</FormLabel>
<FormLabel ml={2} color={'myGray.600'}>
{t('common:core.app.Welcome Text')}
</FormLabel>
<ChatFunctionTip type={'welcome'} />
</Flex>
<MyTextarea
className="nowheel"
iconSrc={'core/app/simpleMode/chat'}
title={t('common:core.app.Welcome Text')}
mt={2}
mt={1.5}
rows={6}
fontSize={'sm'}
bg={'myGray.50'}
bg={'white'}
minW={'384px'}
placeholder={t('common:core.app.tip.welcomeTextTip')}
autoHeight
minH={100}

View File

@@ -34,7 +34,7 @@ const WhisperConfig = ({
return (
<Flex alignItems={'center'}>
<MyIcon name={'core/app/simpleMode/whisper'} mr={2} w={'20px'} />
<FormLabel>{t('common:core.app.Whisper')}</FormLabel>
<FormLabel color={'myGray.600'}>{t('common:core.app.Whisper')}</FormLabel>
<Box flex={1} />
<MyTooltip label={t('common:core.app.Config whisper')}>
<Button
@@ -42,6 +42,7 @@ const WhisperConfig = ({
iconSpacing={1}
size={'sm'}
mr={'-5px'}
color={'myGray.600'}
onClick={onOpen}
>
{formLabel}

View File

@@ -8,7 +8,7 @@ import MyIcon from '@fastgpt/web/components/common/Icon';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { ChatBoxInputFormType, ChatBoxInputType, SendPromptFnType } from '../type';
import { textareaMinH } from '../constants';
import { UseFormReturn } from 'react-hook-form';
import { useFieldArray, UseFormReturn } from 'react-hook-form';
import { ChatBoxContext } from '../Provider';
import dynamic from 'next/dynamic';
import { useContextSelector } from 'use-context-selector';
@@ -17,6 +17,7 @@ import { documentFileType } from '@fastgpt/global/common/file/constants';
import FilePreview from '../../components/FilePreview';
import { useFileUpload } from '../hooks/useFileUpload';
import ComplianceTip from '@/components/common/ComplianceTip/index';
import { useToast } from '@fastgpt/web/hooks/useToast';
const InputGuideBox = dynamic(() => import('./InputGuideBox'));
@@ -44,6 +45,7 @@ const ChatInput = ({
}) => {
const { isPc } = useSystem();
const { t } = useTranslation();
const { toast } = useToast();
const { setValue, watch, control } = chatForm;
const inputValue = watch('input');
@@ -58,6 +60,10 @@ const ChatInput = ({
fileSelectConfig
} = useContextSelector(ChatBoxContext, (v) => v);
const fileCtrl = useFieldArray({
control,
name: 'files'
});
const {
File,
onOpenSelectFile,
@@ -69,15 +75,15 @@ const ChatInput = ({
showSelectFile,
showSelectImg,
removeFiles,
replaceFiles
replaceFiles,
hasFileUploading
} = useFileUpload({
outLinkAuthData,
chatId: chatId || '',
fileSelectConfig,
control
fileCtrl
});
const havInput = !!inputValue || fileList.length > 0;
const hasFileUploading = fileList.some((item) => !item.url);
const canSendMessage = havInput && !hasFileUploading;
// Upload files
@@ -202,7 +208,7 @@ const ChatInput = ({
<MyTooltip label={selectFileLabel}>
<MyIcon name={selectFileIcon as any} w={'18px'} color={'myGray.600'} />
</MyTooltip>
<File onSelect={(files) => onSelectFile({ files, fileList })} />
<File onSelect={(files) => onSelectFile({ files })} />
</Flex>
)}
@@ -278,9 +284,10 @@ const ChatInput = ({
.filter((file) => {
return file && fileTypeFilter(file);
}) as File[];
onSelectFile({ files, fileList });
onSelectFile({ files });
if (files.length > 0) {
e.preventDefault();
e.stopPropagation();
}
}
@@ -431,7 +438,36 @@ const ChatInput = ({
);
return (
<Box m={['0 auto', '10px auto']} w={'100%'} maxW={['auto', 'min(800px, 100%)']} px={[0, 5]}>
<Box
m={['0 auto', '10px auto']}
w={'100%'}
maxW={['auto', 'min(800px, 100%)']}
px={[0, 5]}
onDragOver={(e) => e.preventDefault()}
onDrop={(e) => {
e.preventDefault();
if (!(showSelectFile || showSelectImg)) return;
const files = Array.from(e.dataTransfer.files);
const droppedFiles = files.filter((file) => fileTypeFilter(file));
if (droppedFiles.length > 0) {
onSelectFile({ files: droppedFiles });
}
const invalidFileName = files
.filter((file) => !fileTypeFilter(file))
.map((file) => file.name)
.join(', ');
if (invalidFileName) {
toast({
status: 'warning',
title: t('chat:unsupported_file_type'),
description: invalidFileName
});
}
}}
>
<Box
pt={fileList.length > 0 ? '0' : ['14px', '18px']}
pb={['14px', '18px']}
@@ -468,7 +504,7 @@ const ChatInput = ({
{RenderTranslateLoading}
{/* file preview */}
<Box px={[2, 4]}>
<Box px={[1, 3]}>
<FilePreview fileList={fileList} removeFiles={removeFiles} />
</Box>

View File

@@ -34,6 +34,9 @@ export type ChatProviderProps = OutLinkChatAuthProps & {
// not chat test params
chatId?: string;
chatType: 'log' | 'chat' | 'share' | 'team';
showRawSource: boolean;
showNodeStatus: boolean;
};
type useChatStoreType = OutLinkChatAuthProps &
@@ -137,7 +140,9 @@ const Provider = ({
chatHistories,
setChatHistories,
variablesForm,
chatType = 'chat',
showRawSource,
showNodeStatus,
chatConfig = {},
children,
...props
@@ -239,7 +244,10 @@ const Provider = ({
chatInputGuide,
outLinkAuthData,
variablesForm,
getHistoryResponseData
getHistoryResponseData,
chatType,
showRawSource,
showNodeStatus
};
return <ChatBoxContext.Provider value={value}>{children}</ChatBoxContext.Provider>;

View File

@@ -1,5 +1,5 @@
import { useCopyData } from '@/web/common/hooks/useCopyData';
import { Flex, FlexProps, Image, css, useTheme } from '@chakra-ui/react';
import { Flex, FlexProps, css, useTheme } from '@chakra-ui/react';
import { ChatSiteItemType } from '@fastgpt/global/core/chat/type';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import React, { useMemo } from 'react';
@@ -9,6 +9,7 @@ import { formatChatValue2InputType } from '../utils';
import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
import { ChatBoxContext } from '../Provider';
import { useContextSelector } from 'use-context-selector';
import MyImage from '@fastgpt/web/components/common/Image/MyImage';
export type ChatControllerProps = {
isLastChild: boolean;
@@ -124,7 +125,7 @@ const ChatController = ({
onClick={cancelAudio}
/>
</MyTooltip>
<Image
<MyImage
src="/icon/speaking.gif"
w={'23px'}
alt={''}

View File

@@ -1,5 +1,5 @@
import { Box, BoxProps, Card, Flex } from '@chakra-ui/react';
import React, { useMemo } from 'react';
import React, { useMemo, useRef } from 'react';
import ChatController, { type ChatControllerProps } from './ChatController';
import ChatAvatar from './ChatAvatar';
import { MessageCardStyle } from '../constants';
@@ -22,6 +22,9 @@ import { useTranslation } from 'next-i18next';
import { AIChatItemValueItemType, ChatItemValueItemType } from '@fastgpt/global/core/chat/type';
import { CodeClassNameEnum } from '@/components/Markdown/utils';
import { isEqual } from 'lodash';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
import { formatTimeToChatItemTime } from '@fastgpt/global/common/string/time';
import dayjs from 'dayjs';
const colorMap = {
[ChatStatusEnum.loading]: {
@@ -110,8 +113,10 @@ const AIContentCard = React.memo(function AIContentCard({
const ChatItem = (props: Props) => {
const { type, avatar, statusBoxData, children, isLastChild, questionGuides = [], chat } = props;
const styleMap: BoxProps =
type === ChatRoleEnum.Human
const { isPc } = useSystem();
const styleMap: BoxProps = {
...(type === ChatRoleEnum.Human
? {
order: 0,
borderRadius: '8px 0 8px 8px',
@@ -125,10 +130,17 @@ const ChatItem = (props: Props) => {
justifyContent: 'flex-start',
textAlign: 'left',
bg: 'myGray.50'
};
}),
fontSize: 'mini',
fontWeight: '400',
color: 'myGray.500'
};
const { t } = useTranslation();
const isChatting = useContextSelector(ChatBoxContext, (v) => v.isChatting);
const chatType = useContextSelector(ChatBoxContext, (v) => v.chatType);
const showNodeStatus = useContextSelector(ChatBoxContext, (v) => v.showNodeStatus);
const isChatLog = chatType === 'log';
const { copyData } = useCopyData();
@@ -196,18 +208,38 @@ const ChatItem = (props: Props) => {
}, [chat.obj, chat.value, isChatting]);
return (
<>
<Box
_hover={{
'& .time-label': {
display: 'block'
}
}}
>
{/* control icon */}
<Flex w={'100%'} alignItems={'center'} gap={2} justifyContent={styleMap.justifyContent}>
<Flex w={'100%'} alignItems={'flex-end'} gap={2} justifyContent={styleMap.justifyContent}>
{isChatting && type === ChatRoleEnum.AI && isLastChild ? null : (
<Box order={styleMap.order} ml={styleMap.ml}>
<Flex order={styleMap.order} ml={styleMap.ml} align={'center'} gap={'0.62rem'}>
{chat.time && (isPc || isChatLog) && (
<Box
order={type === ChatRoleEnum.AI ? 2 : 0}
className={'time-label'}
fontSize={styleMap.fontSize}
color={styleMap.color}
fontWeight={styleMap.fontWeight}
display={isChatLog ? 'block' : 'none'}
>
{t(formatTimeToChatItemTime(chat.time) as any, {
time: dayjs(chat.time).format('HH:mm')
}).replace('#', ':')}
</Box>
)}
<ChatController {...props} isLastChild={isLastChild} />
</Box>
</Flex>
)}
<ChatAvatar src={avatar} type={type} />
{/* Workflow status */}
{!!chatStatusMap && statusBoxData && isLastChild && (
{!!chatStatusMap && statusBoxData && isLastChild && showNodeStatus && (
<Flex
alignItems={'center'}
px={3}
@@ -290,7 +322,7 @@ const ChatItem = (props: Props) => {
</Card>
</Box>
))}
</>
</Box>
);
};

View File

@@ -54,6 +54,7 @@ const ContextModal = ({ onClose, dataId }: { onClose: () => void; dataId: string
border={'base'}
_notLast={{ mb: 2 }}
position={'relative'}
bg={i % 2 === 0 ? 'white' : 'myGray.50'}
>
<Box fontWeight={'bold'}>{item.obj}</Box>
<Box>{item.value}</Box>

View File

@@ -6,16 +6,19 @@ import { useTranslation } from 'next-i18next';
import type { SearchDataResponseItemType } from '@fastgpt/global/core/dataset/type';
import QuoteItem from '@/components/core/dataset/QuoteItem';
import RawSourceBox from '@/components/core/dataset/RawSourceBox';
import { getWebReqUrl } from '@fastgpt/web/common/system/utils';
const QuoteModal = ({
rawSearch = [],
onClose,
showDetail,
canEditDataset,
showRawSource,
metadata
}: {
rawSearch: SearchDataResponseItemType[];
onClose: () => void;
showDetail: boolean;
canEditDataset: boolean;
showRawSource: boolean;
metadata?: {
collectionId: string;
sourceId?: string;
@@ -42,13 +45,13 @@ const QuoteModal = ({
h={['90vh', '80vh']}
isCentered
minW={['90vw', '600px']}
iconSrc={!!metadata ? undefined : '/imgs/modal/quote.svg'}
iconSrc={!!metadata ? undefined : getWebReqUrl('/imgs/modal/quote.svg')}
title={
<Box>
{metadata ? (
<RawSourceBox {...metadata} canView={showDetail} />
<RawSourceBox {...metadata} canView={showRawSource} />
) : (
<>{t('core.chat.Quote Amount', { amount: rawSearch.length })}</>
<>{t('common:core.chat.Quote Amount', { amount: rawSearch.length })}</>
)}
<Box fontSize={'xs'} color={'myGray.500'} fontWeight={'normal'}>
{t('common:core.chat.quote.Quote Tip')}
@@ -57,7 +60,11 @@ const QuoteModal = ({
}
>
<ModalBody>
<QuoteList rawSearch={filterResults} showDetail={showDetail} />
<QuoteList
rawSearch={filterResults}
canEditDataset={canEditDataset}
canViewSource={showRawSource}
/>
</ModalBody>
</MyModal>
</>
@@ -68,10 +75,12 @@ export default QuoteModal;
export const QuoteList = React.memo(function QuoteList({
rawSearch = [],
showDetail
canEditDataset,
canViewSource
}: {
rawSearch: SearchDataResponseItemType[];
showDetail: boolean;
canEditDataset: boolean;
canViewSource: boolean;
}) {
const theme = useTheme();
@@ -88,7 +97,11 @@ export const QuoteList = React.memo(function QuoteList({
_hover={{ '& .hover-data': { display: 'flex' } }}
bg={i % 2 === 0 ? 'white' : 'myWhite.500'}
>
<QuoteItem quoteItem={item} canViewSource={showDetail} linkToDataset={showDetail} />
<QuoteItem
quoteItem={item}
canViewSource={canViewSource}
canEditDataset={canEditDataset}
/>
</Box>
))}
</>

View File

@@ -7,12 +7,13 @@ import MyTag from '@fastgpt/web/components/common/Tag/index';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { getSourceNameIcon } from '@fastgpt/global/core/dataset/utils';
import ChatBoxDivider from '@/components/core/chat/Divider';
import { strIsLink } from '@fastgpt/global/common/string/tools';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
import { ChatSiteItemType } from '@fastgpt/global/core/chat/type';
import { addStatisticalDataToHistoryItem } from '@/global/core/chat/utils';
import { useSize } from 'ahooks';
import { useContextSelector } from 'use-context-selector';
import { ChatBoxContext } from '../Provider';
const QuoteModal = dynamic(() => import('./QuoteModal'));
const ContextModal = dynamic(() => import('./ContextModal'));
@@ -20,11 +21,9 @@ const WholeResponseModal = dynamic(() => import('../../../components/WholeRespon
const ResponseTags = ({
showTags,
showDetail,
historyItem
}: {
showTags: boolean;
showDetail: boolean;
historyItem: ChatSiteItemType;
}) => {
const { isPc } = useSystem();
@@ -37,6 +36,7 @@ const ResponseTags = ({
totalRunningTime: runningTime = 0,
historyPreviewLength = 0
} = useMemo(() => addStatisticalDataToHistoryItem(historyItem), [historyItem]);
const [quoteModalData, setQuoteModalData] = useState<{
rawSearch: SearchDataResponseItemType[];
metadata?: {
@@ -47,6 +47,10 @@ const ResponseTags = ({
}>();
const [quoteFolded, setQuoteFolded] = useState<boolean>(true);
const chatType = useContextSelector(ChatBoxContext, (v) => v.chatType);
const showRawSource = useContextSelector(ChatBoxContext, (v) => v.showRawSource);
const notSharePage = useMemo(() => chatType !== 'share', [chatType]);
const {
isOpen: isOpenWholeModal,
onOpen: onOpenWholeModal,
@@ -77,13 +81,20 @@ const ResponseTags = ({
sourceName: item.sourceName,
sourceId: item.sourceId,
icon: getSourceNameIcon({ sourceId: item.sourceId, sourceName: item.sourceName }),
canReadQuote: showDetail || strIsLink(item.sourceId),
collectionId: item.collectionId
}));
}, [quoteList, showDetail]);
}, [quoteList]);
const notEmptyTags =
quoteList.length > 0 ||
(llmModuleAccount === 1 && notSharePage) ||
(llmModuleAccount > 1 && notSharePage) ||
(isPc && runningTime > 0) ||
notSharePage;
return !showTags ? null : (
<>
{/* quote */}
{sourceList.length > 0 && (
<>
<Flex justifyContent={'space-between'} alignItems={'center'}>
@@ -176,7 +187,8 @@ const ResponseTags = ({
</Flex>
</>
)}
{showDetail && (
{notEmptyTags && (
<Flex alignItems={'center'} mt={3} flexWrap={'wrap'} gap={2}>
{quoteList.length > 0 && (
<MyTooltip label={t('chat:view_citations')}>
@@ -190,7 +202,7 @@ const ResponseTags = ({
</MyTag>
</MyTooltip>
)}
{llmModuleAccount === 1 && (
{llmModuleAccount === 1 && notSharePage && (
<>
{historyPreviewLength > 0 && (
<MyTooltip label={t('chat:click_contextual_preview')}>
@@ -206,12 +218,11 @@ const ResponseTags = ({
)}
</>
)}
{llmModuleAccount > 1 && (
{llmModuleAccount > 1 && notSharePage && (
<MyTag type="borderSolid" colorSchema="blue">
{t('chat:multiple_AI_conversations')}
</MyTag>
)}
{isPc && runningTime > 0 && (
<MyTooltip label={t('chat:module_runtime_and')}>
<MyTag colorSchema="purple" type="borderSolid" cursor={'default'}>
@@ -219,29 +230,32 @@ const ResponseTags = ({
</MyTag>
</MyTooltip>
)}
<MyTooltip label={t('common:core.chat.response.Read complete response tips')}>
<MyTag
colorSchema="gray"
type="borderSolid"
cursor={'pointer'}
onClick={onOpenWholeModal}
>
{t('common:core.chat.response.Read complete response')}
</MyTag>
</MyTooltip>
{notSharePage && (
<MyTooltip label={t('common:core.chat.response.Read complete response tips')}>
<MyTag
colorSchema="gray"
type="borderSolid"
cursor={'pointer'}
onClick={onOpenWholeModal}
>
{t('common:core.chat.response.Read complete response')}
</MyTag>
</MyTooltip>
)}
</Flex>
)}
{!!quoteModalData && (
<QuoteModal
{...quoteModalData}
showDetail={showDetail}
canEditDataset={notSharePage}
showRawSource={showRawSource}
onClose={() => setQuoteModalData(undefined)}
/>
)}
{isOpenContextModal && <ContextModal dataId={dataId} onClose={onCloseContextModal} />}
{isOpenWholeModal && (
<WholeResponseModal dataId={dataId} showDetail={showDetail} onClose={onCloseWholeModal} />
)}
{isOpenWholeModal && <WholeResponseModal dataId={dataId} onClose={onCloseWholeModal} />}
</>
);
};

View File

@@ -24,6 +24,7 @@ import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import { useDeepCompareEffect } from 'ahooks';
import { VariableItemType } from '@fastgpt/global/core/app/type';
import MyTextarea from '@/components/common/Textarea/MyTextarea';
import MyNumberInput from '@fastgpt/web/components/common/Input/NumberInput';
export const VariableInputItem = ({
item,
@@ -64,14 +65,14 @@ export const VariableInputItem = ({
minH={40}
maxH={160}
bg={'myGray.50'}
{...register(item.key, {
{...register(`variables.${item.key}`, {
required: item.required
})}
/>
)}
{item.type === VariableInputEnum.textarea && (
<Textarea
{...register(item.key, {
{...register(`variables.${item.key}`, {
required: item.required
})}
rows={5}
@@ -82,9 +83,9 @@ export const VariableInputItem = ({
{item.type === VariableInputEnum.select && (
<Controller
key={item.key}
key={`variables.${item.key}`}
control={control}
name={item.key}
name={`variables.${item.key}`}
rules={{ required: item.required }}
render={({ field: { ref, value } }) => {
return (
@@ -96,7 +97,7 @@ export const VariableInputItem = ({
value: item.value
}))}
value={value}
onchange={(e) => setValue(item.key, e)}
onchange={(e) => setValue(`variables.${item.key}`, e)}
/>
);
}}
@@ -104,27 +105,19 @@ export const VariableInputItem = ({
)}
{item.type === VariableInputEnum.numberInput && (
<Controller
key={item.key}
key={`variables.${item.key}`}
control={control}
name={item.key}
name={`variables.${item.key}`}
rules={{ required: item.required, min: item.min, max: item.max }}
render={({ field: { ref, value, onChange } }) => (
<NumberInput
render={({ field: { value, onChange } }) => (
<MyNumberInput
step={1}
min={item.min}
max={item.max}
bg={'white'}
rounded={'md'}
clampValueOnBlur={false}
value={value}
onChange={(valueString) => onChange(Number(valueString))}
>
<NumberInputField ref={ref} bg={'white'} />
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>
onChange={onChange}
/>
)}
/>
)}

View File

@@ -22,7 +22,7 @@ const WelcomeBox = ({ welcomeText }: { welcomeText: string }) => {
bg={'white'}
boxShadow={'0 0 8px rgba(0,0,0,0.15)'}
>
<Markdown source={`~~~guide \n${welcomeText}`} />
<Markdown source={`~~~guide \n${welcomeText}`} forbidZhFormat />
</Card>
</Box>
</Box>

View File

@@ -9,21 +9,22 @@ import { getFileIcon } from '@fastgpt/global/common/file/icon';
import { formatFileSize } from '@fastgpt/global/common/file/tools';
import { clone } from 'lodash';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { Control, useFieldArray } from 'react-hook-form';
import { UseFieldArrayReturn } from 'react-hook-form';
import { ChatBoxInputFormType, UserInputFileItemType } from '../type';
import { AppFileSelectConfigType } from '@fastgpt/global/core/app/type';
import { documentFileType } from '@fastgpt/global/common/file/constants';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat';
interface UseFileUploadOptions {
outLinkAuthData: any;
type UseFileUploadOptions = {
outLinkAuthData: OutLinkChatAuthProps;
chatId: string;
fileSelectConfig: AppFileSelectConfigType;
control: Control<ChatBoxInputFormType, any>;
}
fileCtrl: UseFieldArrayReturn<ChatBoxInputFormType, 'files', 'id'>;
};
export const useFileUpload = (props: UseFileUploadOptions) => {
const { outLinkAuthData, chatId, fileSelectConfig, control } = props;
const { outLinkAuthData, chatId, fileSelectConfig, fileCtrl } = props;
const { toast } = useToast();
const { t } = useTranslation();
const { feConfigs } = useSystemStore();
@@ -32,16 +33,16 @@ export const useFileUpload = (props: UseFileUploadOptions) => {
update: updateFiles,
remove: removeFiles,
fields: fileList,
replace: replaceFiles
} = useFieldArray({
control: control,
name: 'files'
});
replace: replaceFiles,
append: appendFiles
} = fileCtrl;
const hasFileUploading = fileList.some((item) => !item.url);
const showSelectFile = fileSelectConfig?.canSelectFile;
const showSelectImg = fileSelectConfig?.canSelectImg;
const maxSelectFiles = fileSelectConfig?.maxFiles ?? 10;
const maxSize = (feConfigs?.uploadFileMaxSize || 1024) * 1024 * 1024; // nkb
const canSelectFileAmount = maxSelectFiles - fileList.length;
const { icon: selectFileIcon, label: selectFileLabel } = useMemo(() => {
if (showSelectFile && showSelectImg) {
@@ -66,11 +67,11 @@ export const useFileUpload = (props: UseFileUploadOptions) => {
const { File, onOpen: onOpenSelectFile } = useSelectFile({
fileType: `${showSelectImg ? 'image/*,' : ''} ${showSelectFile ? documentFileType : ''}`,
multiple: true,
maxCount: maxSelectFiles
maxCount: canSelectFileAmount
});
const onSelectFile = useCallback(
async ({ files, fileList }: { files: File[]; fileList: UserInputFileItemType[] }) => {
async ({ files }: { files: File[] }) => {
if (!files || files.length === 0) {
return [];
}
@@ -129,22 +130,11 @@ export const useFileUpload = (props: UseFileUploadOptions) => {
)
);
// Document, image
const concatFileList = clone(
fileList.concat(loadFiles).sort((a, b) => {
if (a.type === ChatFileTypeEnum.image && b.type === ChatFileTypeEnum.file) {
return 1;
} else if (a.type === ChatFileTypeEnum.file && b.type === ChatFileTypeEnum.image) {
return -1;
}
return 0;
})
);
replaceFiles(concatFileList);
appendFiles(loadFiles);
return loadFiles;
},
[maxSelectFiles, replaceFiles, toast, t, maxSize]
[maxSelectFiles, appendFiles, toast, t, maxSize]
);
const uploadFiles = async () => {
@@ -198,10 +188,23 @@ export const useFileUpload = (props: UseFileUploadOptions) => {
removeFiles(errorFileIndex);
};
const sortFileList = useMemo(() => {
// Sort: Document, image
const sortResult = clone(fileList).sort((a, b) => {
if (a.type === ChatFileTypeEnum.image && b.type === ChatFileTypeEnum.file) {
return 1;
} else if (a.type === ChatFileTypeEnum.file && b.type === ChatFileTypeEnum.image) {
return -1;
}
return 0;
});
return sortResult;
}, [fileList]);
return {
File,
onOpenSelectFile,
fileList,
fileList: sortFileList,
onSelectFile,
uploadFiles,
selectFileIcon,
@@ -209,6 +212,7 @@ export const useFileUpload = (props: UseFileUploadOptions) => {
showSelectFile,
showSelectImg,
removeFiles,
replaceFiles
replaceFiles,
hasFileUploading
};
};

View File

@@ -67,6 +67,9 @@ import { useSystem } from '@fastgpt/web/hooks/useSystem';
import { useCreation, useMemoizedFn, useThrottleFn } from 'ahooks';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { mergeChatResponseData } from '@fastgpt/global/core/chat/utils';
import { formatTimeToChatItemTime } from '@fastgpt/global/common/string/time';
import dayjs from 'dayjs';
import { getWebReqUrl } from '@fastgpt/web/common/system/utils';
const ResponseTags = dynamic(() => import('./components/ResponseTags'));
const FeedbackModal = dynamic(() => import('./components/FeedbackModal'));
@@ -108,6 +111,18 @@ type Props = OutLinkChatAuthProps &
onDelMessage?: (e: { contentId: string }) => void;
};
const ChatTimeBox = ({ time }: { time: Date }) => {
const { t } = useTranslation();
return (
<Box w={'100%'} fontSize={'mini'} textAlign={'center'} color={'myGray.500'} fontWeight={'400'}>
{t(formatTimeToChatItemTime(time) as any, {
time: dayjs(time).format('HH#mm')
}).replace('#', ':')}
</Box>
);
};
const ChatBox = (
{
feedbackType = FeedbackTypeEnum.hidden,
@@ -393,7 +408,7 @@ const ChatBox = (
isInteractivePrompt = false
}) => {
variablesForm.handleSubmit(
async (variables) => {
async ({ variables }) => {
if (!onStartChat) return;
if (isChatting) {
toast({
@@ -436,6 +451,7 @@ const ChatBox = (
{
dataId: getNanoid(24),
obj: ChatRoleEnum.Human,
time: new Date(),
value: [
...files.map((file) => ({
type: ChatItemValueTypeEnum.file,
@@ -510,6 +526,12 @@ const ChatBox = (
generatingMessage: (e) => generatingMessage({ ...e, autoTTSResponse }),
variables: requestVariables
});
if (responseData?.[responseData.length - 1]?.error) {
toast({
title: t(responseData[responseData.length - 1].error?.message),
status: 'error'
});
}
isNewChatReplace.current = isNewChat;
@@ -521,6 +543,7 @@ const ChatBox = (
return {
...item,
status: ChatStatusEnum.finish,
time: new Date(),
responseData: item.responseData
? mergeChatResponseData([...item.responseData, ...responseData])
: responseData
@@ -543,6 +566,7 @@ const ChatBox = (
// tts audio
autoTTSResponse && splitText2Audio(responseText, true);
} catch (err: any) {
console.log(err);
toast({
title: t(getErrText(err, 'core.chat.error.Chat error') as any),
status: 'error',
@@ -562,6 +586,7 @@ const ChatBox = (
if (index !== state.length - 1) return item;
return {
...item,
time: new Date(),
status: ChatStatusEnum.finish
};
})
@@ -877,88 +902,97 @@ const ChatBox = (
{/* chat history */}
<Box id={'history'}>
{chatHistories.map((item, index) => (
<Box key={item.dataId} py={5}>
{item.obj === ChatRoleEnum.Human && (
<ChatItem
type={item.obj}
avatar={userAvatar}
chat={item}
onRetry={retryInput(item.dataId)}
onDelete={delOneMessage(item.dataId)}
isLastChild={index === chatHistories.length - 1}
/>
)}
{item.obj === ChatRoleEnum.AI && (
<>
<>
{/* 并且时间和上一条的time相差超过十分钟 */}
{index !== 0 &&
item.time &&
chatHistories[index - 1].time !== undefined &&
new Date(item.time).getTime() -
new Date(chatHistories[index - 1].time!).getTime() >
10 * 60 * 1000 && <ChatTimeBox time={item.time} />}
<Box key={item.dataId} py={6}>
{item.obj === ChatRoleEnum.Human && (
<ChatItem
type={item.obj}
avatar={appAvatar}
avatar={userAvatar}
chat={item}
onRetry={retryInput(item.dataId)}
onDelete={delOneMessage(item.dataId)}
isLastChild={index === chatHistories.length - 1}
{...{
showVoiceIcon,
shareId,
outLinkUid,
teamId,
teamToken,
statusBoxData,
questionGuides,
onMark: onMark(
item,
formatChatValue2InputType(chatHistories[index - 1]?.value)?.text
),
onAddUserLike: onAddUserLike(item),
onCloseUserLike: onCloseUserLike(item),
onAddUserDislike: onAddUserDislike(item),
onReadUserDislike: onReadUserDislike(item)
}}
>
<ResponseTags
showTags={index !== chatHistories.length - 1 || !isChatting}
showDetail={!shareId && !teamId}
historyItem={item}
/>
/>
)}
{item.obj === ChatRoleEnum.AI && (
<>
<ChatItem
type={item.obj}
avatar={appAvatar}
chat={item}
isLastChild={index === chatHistories.length - 1}
{...{
showVoiceIcon,
shareId,
outLinkUid,
teamId,
teamToken,
statusBoxData,
questionGuides,
onMark: onMark(
item,
formatChatValue2InputType(chatHistories[index - 1]?.value)?.text
),
onAddUserLike: onAddUserLike(item),
onCloseUserLike: onCloseUserLike(item),
onAddUserDislike: onAddUserDislike(item),
onReadUserDislike: onReadUserDislike(item)
}}
>
<ResponseTags
showTags={index !== chatHistories.length - 1 || !isChatting}
historyItem={item}
/>
{/* custom feedback */}
{item.customFeedbacks && item.customFeedbacks.length > 0 && (
<Box>
<ChatBoxDivider
icon={'core/app/customFeedback'}
text={t('common:core.app.feedback.Custom feedback')}
/>
{item.customFeedbacks.map((text, i) => (
<Box key={i}>
<MyTooltip
label={t('common:core.app.feedback.close custom feedback')}
>
<Checkbox
onChange={onCloseCustomFeedback(item, i)}
icon={<MyIcon name={'common/check'} w={'12px'} />}
{/* custom feedback */}
{item.customFeedbacks && item.customFeedbacks.length > 0 && (
<Box>
<ChatBoxDivider
icon={'core/app/customFeedback'}
text={t('common:core.app.feedback.Custom feedback')}
/>
{item.customFeedbacks.map((text, i) => (
<Box key={i}>
<MyTooltip
label={t('common:core.app.feedback.close custom feedback')}
>
{text}
</Checkbox>
</MyTooltip>
</Box>
))}
</Box>
)}
{/* admin mark content */}
{showMarkIcon && item.adminFeedback && (
<Box fontSize={'sm'}>
<ChatBoxDivider
icon="core/app/markLight"
text={t('common:core.chat.Admin Mark Content')}
/>
<Box whiteSpace={'pre-wrap'}>
<Box color={'black'}>{item.adminFeedback.q}</Box>
<Box color={'myGray.600'}>{item.adminFeedback.a}</Box>
<Checkbox
onChange={onCloseCustomFeedback(item, i)}
icon={<MyIcon name={'common/check'} w={'12px'} />}
>
{text}
</Checkbox>
</MyTooltip>
</Box>
))}
</Box>
</Box>
)}
</ChatItem>
</>
)}
</Box>
)}
{/* admin mark content */}
{showMarkIcon && item.adminFeedback && (
<Box fontSize={'sm'}>
<ChatBoxDivider
icon="core/app/markLight"
text={t('common:core.chat.Admin Mark Content')}
/>
<Box whiteSpace={'pre-wrap'}>
<Box color={'black'}>{item.adminFeedback.q}</Box>
<Box color={'myGray.600'}>{item.adminFeedback.a}</Box>
</Box>
</Box>
)}
</ChatItem>
</>
)}
</Box>
</>
))}
</Box>
</Box>
@@ -996,7 +1030,7 @@ const ChatBox = (
return (
<Flex flexDirection={'column'} h={'100%'} position={'relative'}>
<Script src="/js/html2pdf.bundle.min.js" strategy="lazyOnload"></Script>
<Script src={getWebReqUrl('/js/html2pdf.bundle.min.js')} strategy="lazyOnload"></Script>
{/* chat box container */}
{RenderRecords}
{/* message input */}

View File

@@ -20,9 +20,9 @@ export type UserInputFileItemType = {
export type ChatBoxInputFormType = {
input: string;
files: UserInputFileItemType[];
files: UserInputFileItemType[]; // global files
chatStarted: boolean;
[key: string]: any;
variables: Record<string, any>;
};
export type ChatBoxInputType = {

View File

@@ -1,5 +1,5 @@
import React, { useCallback, useEffect, useMemo } from 'react';
import { Controller } from 'react-hook-form';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Controller, useFieldArray } from 'react-hook-form';
import RenderPluginInput from './renderPluginInput';
import { Box, Button, Flex } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
@@ -14,7 +14,8 @@ import { useFileUpload } from '../../ChatBox/hooks/useFileUpload';
import FilePreview from '../../components/FilePreview';
import { UserChatItemValueItemType } from '@fastgpt/global/core/chat/type';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import { ChatBoxInputFormType, UserInputFileItemType } from '../../ChatBox/type';
import { ChatBoxInputFormType } from '../../ChatBox/type';
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io';
const RenderInput = () => {
const { t } = useTranslation();
@@ -29,9 +30,7 @@ const RenderInput = () => {
isChatting,
chatConfig,
chatId,
outLinkAuthData,
restartInputStore,
setRestartInputStore
outLinkAuthData
} = useContextSelector(PluginRunContext, (v) => v);
const {
@@ -42,6 +41,11 @@ const RenderInput = () => {
formState: { errors }
} = variablesForm;
/* ===> Global files(abandon) */
const fileCtrl = useFieldArray({
control: variablesForm.control,
name: 'files'
});
const {
File,
onOpenSelectFile,
@@ -52,46 +56,76 @@ const RenderInput = () => {
showSelectFile,
showSelectImg,
removeFiles,
replaceFiles
hasFileUploading
} = useFileUpload({
outLinkAuthData,
chatId: chatId || '',
fileSelectConfig: chatConfig?.fileSelectConfig,
control
fileCtrl
});
const isDisabledInput = histories.length > 0;
useRequest2(uploadFiles, {
manual: false,
errorToast: t('common:upload_file_error'),
refreshDeps: [fileList, outLinkAuthData, chatId]
});
/* Global files(abandon) <=== */
const [restartData, setRestartData] = useState<ChatBoxInputFormType>();
const onClickNewChat = useCallback(
(e: ChatBoxInputFormType, files: UserInputFileItemType[] = []) => {
setRestartInputStore({
...e,
files
});
(e: ChatBoxInputFormType) => {
setRestartData(e);
onNewChat?.();
},
[onNewChat, setRestartInputStore]
[onNewChat, setRestartData]
);
// Get plugin input components
const formatPluginInputs = useMemo(() => {
if (histories.length === 0) return pluginInputs;
try {
const historyValue = histories[0]?.value as UserChatItemValueItemType[];
const inputValueString = historyValue.find((item) => item.type === 'text')?.text?.content;
if (!inputValueString) return pluginInputs;
return JSON.parse(inputValueString) as FlowNodeInputItemType[];
} catch (error) {
console.error('Failed to parse input value:', error);
return pluginInputs;
}
}, [histories, pluginInputs]);
// Reset input value
useEffect(() => {
// Set last run value
if (!isDisabledInput && restartInputStore) {
reset(restartInputStore);
// Set config default value
if (histories.length === 0) {
if (restartData) {
reset(restartData);
setRestartData(undefined);
return;
}
const defaultFormValues = formatPluginInputs.reduce(
(acc, input) => {
acc[input.key] = input.defaultValue;
return acc;
},
{} as Record<string, any>
);
reset({
files: [],
variables: defaultFormValues
});
return;
}
// Set history to default value
const historyVariables = (() => {
const historyValue = histories[0]?.value as UserChatItemValueItemType[];
if (!historyValue) return undefined;
const defaultFormValues = pluginInputs.reduce(
(acc, input) => {
acc[input.key] = input.defaultValue;
return acc;
},
{} as Record<string, any>
);
const historyFormValues = (() => {
if (!isDisabledInput) return undefined;
const historyValue = histories[0].value;
try {
const inputValueString = historyValue.find((item) => item.type === 'text')?.text?.content;
return (
@@ -115,32 +149,25 @@ const RenderInput = () => {
return undefined;
}
})();
// Parse history file
const historyFileList = (() => {
if (!isDisabledInput) return [];
const historyValue = histories[0].value as UserChatItemValueItemType[];
return historyValue.filter((item) => item.type === 'file').map((item) => item.file);
const historyValue = histories[0]?.value as UserChatItemValueItemType[];
return historyValue?.filter((item) => item.type === 'file').map((item) => item.file);
})();
reset({
...(historyFormValues || defaultFormValues),
variables: historyVariables,
files: historyFileList
});
}, [getValues, histories, isDisabledInput, pluginInputs, replaceFiles, reset, restartInputStore]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [histories]);
const hasFileUploading = useMemo(() => {
return fileList.some((item) => !item.url);
}, [fileList]);
const [uploading, setUploading] = useState(false);
useRequest2(uploadFiles, {
manual: false,
errorToast: t('common:upload_file_error'),
refreshDeps: [fileList, outLinkAuthData, chatId]
});
const fileUploading = uploading || hasFileUploading;
return (
<>
<Box>
{/* instruction */}
{chatConfig?.instruction && (
<Box
@@ -155,7 +182,7 @@ const RenderInput = () => {
<Markdown source={chatConfig.instruction} />
</Box>
)}
{/* file select */}
{/* file select(Abandoned) */}
{(showSelectFile || showSelectImg) && (
<Box mb={5}>
<Flex alignItems={'center'}>
@@ -175,7 +202,7 @@ const RenderInput = () => {
{t('chat:select')}
</Button>
)}
<File onSelect={(files) => onSelectFile({ files, fileList })} />
<File onSelect={(files) => onSelectFile({ files })} />
</Flex>
<FilePreview
fileList={fileList}
@@ -184,12 +211,12 @@ const RenderInput = () => {
</Box>
)}
{/* Filed */}
{pluginInputs.map((input) => {
{formatPluginInputs.map((input) => {
return (
<Controller
key={input.key}
key={`variables.${input.key}`}
control={control}
name={input.key}
name={`variables.${input.key}`}
rules={{
validate: (value) => {
if (!input.required) return true;
@@ -207,6 +234,7 @@ const RenderInput = () => {
isDisabled={isDisabledInput}
isInvalid={errors && Object.keys(errors).includes(input.key)}
input={input}
setUploading={setUploading}
/>
);
}}
@@ -217,13 +245,14 @@ const RenderInput = () => {
{onStartChat && onNewChat && (
<Flex justifyContent={'end'} mt={8}>
<Button
isLoading={isChatting || hasFileUploading}
isLoading={isChatting}
isDisabled={fileUploading}
onClick={() => {
handleSubmit((e) => {
if (isDisabledInput) {
onClickNewChat(e, fileList);
onClickNewChat(e);
} else {
onSubmit(e, fileList);
onSubmit(e);
}
})();
}}
@@ -232,7 +261,7 @@ const RenderInput = () => {
</Button>
</Flex>
)}
</>
</Box>
);
};

View File

@@ -13,7 +13,7 @@ const RenderResponseDetail = () => {
<>{t('chat:in_progress')}</>
) : (
<Box flex={'1 0 0'} h={'100%'} overflow={'auto'}>
<ResponseBox useMobile={true} response={responseData} showDetail={true} />
<ResponseBox useMobile={true} response={responseData} />
</Box>
);
};

View File

@@ -1,36 +1,150 @@
import {
Box,
Flex,
NumberDecrementStepper,
NumberIncrementStepper,
NumberInput,
NumberInputField,
NumberInputStepper,
Switch,
Textarea
} from '@chakra-ui/react';
import { Box, Button, Flex, Switch, Textarea } from '@chakra-ui/react';
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import MySelect from '@fastgpt/web/components/common/MySelect';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import { useTranslation } from 'next-i18next';
import dynamic from 'next/dynamic';
import { useFileUpload } from '../../ChatBox/hooks/useFileUpload';
import { useContextSelector } from 'use-context-selector';
import { PluginRunContext } from '../context';
import MyIcon from '@fastgpt/web/components/common/Icon';
import FilePreview from '../../components/FilePreview';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useEffect, useMemo } from 'react';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import { useFieldArray } from 'react-hook-form';
import MyNumberInput from '@fastgpt/web/components/common/Input/NumberInput';
import { isEqual } from 'lodash';
const JsonEditor = dynamic(() => import('@fastgpt/web/components/common/Textarea/JsonEditor'));
const FileSelector = ({
input,
setUploading,
onChange,
value
}: {
input: FlowNodeInputItemType;
setUploading: React.Dispatch<React.SetStateAction<boolean>>;
onChange: (...event: any[]) => void;
value: any;
}) => {
const { t } = useTranslation();
const { variablesForm, histories, chatId, outLinkAuthData } = useContextSelector(
PluginRunContext,
(v) => v
);
const fileCtrl = useFieldArray({
control: variablesForm.control,
name: `variables.${input.key}`
});
const {
File,
fileList,
selectFileIcon,
uploadFiles,
onOpenSelectFile,
onSelectFile,
removeFiles,
replaceFiles,
hasFileUploading
} = useFileUpload({
outLinkAuthData,
chatId: chatId || '',
fileSelectConfig: {
canSelectFile: input.canSelectFile ?? true,
canSelectImg: input.canSelectImg ?? false,
maxFiles: input.maxFiles ?? 5
},
// @ts-ignore
fileCtrl
});
useEffect(() => {
if (!Array.isArray(value)) {
replaceFiles([]);
return;
}
// compare file names and update if different
const valueFileNames = value.map((item) => item.name);
const currentFileNames = fileList.map((item) => item.name);
if (!isEqual(valueFileNames, currentFileNames)) {
replaceFiles(value);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [value]);
const isDisabledInput = histories.length > 0;
useRequest2(uploadFiles, {
manual: false,
errorToast: t('common:upload_file_error'),
refreshDeps: [fileList, outLinkAuthData, chatId]
});
useEffect(() => {
setUploading(hasFileUploading);
onChange(
fileList.map((item) => ({
type: item.type,
name: item.name,
url: item.url,
icon: item.icon
}))
);
}, [fileList, hasFileUploading, onChange, setUploading]);
return (
<>
<Flex alignItems={'center'}>
<Box position={'relative'}>
{input.required && (
<Box position={'absolute'} left={-2} top={'-1px'} color={'red.600'}>
*
</Box>
)}
<FormLabel fontWeight={'500'}>{t(input.label as any)}</FormLabel>
</Box>
{input.description && <QuestionTip ml={2} label={t(input.description as any)} />}
<Box flex={1} />
{/* 有历史记录,说明是已经跑过了,不能再新增了 */}
<Button
isDisabled={histories.length !== 0}
leftIcon={<MyIcon name={selectFileIcon as any} w={'16px'} />}
variant={'whiteBase'}
onClick={() => {
onOpenSelectFile();
}}
>
{t('chat:select')}
</Button>
</Flex>
<FilePreview fileList={fileList} removeFiles={isDisabledInput ? undefined : removeFiles} />
{fileList.length === 0 && <EmptyTip py={0} mt={3} text={t('chat:not_select_file')} />}
<File onSelect={(files) => onSelectFile({ files })} />
</>
);
};
const RenderPluginInput = ({
value,
onChange,
isDisabled,
isInvalid,
input
input,
setUploading
}: {
value: any;
onChange: () => void;
onChange: (...event: any[]) => void;
isDisabled?: boolean;
isInvalid: boolean;
input: FlowNodeInputItemType;
setUploading: React.Dispatch<React.SetStateAction<boolean>>;
}) => {
const { t } = useTranslation();
const inputType = input.renderTypeList[0];
@@ -44,6 +158,12 @@ const RenderPluginInput = ({
<MySelect list={input.list} value={value} onchange={onChange} isDisabled={isDisabled} />
);
}
if (inputType === FlowNodeInputTypeEnum.fileSelect) {
return (
<FileSelector onChange={onChange} input={input} setUploading={setUploading} value={value} />
);
}
if (input.valueType === WorkflowIOValueTypeEnum.string) {
return (
<Textarea
@@ -59,20 +179,17 @@ const RenderPluginInput = ({
}
if (input.valueType === WorkflowIOValueTypeEnum.number) {
return (
<NumberInput
<MyNumberInput
step={1}
min={input.min}
max={input.max}
bg={'myGray.50'}
isDisabled={isDisabled}
isInvalid={isInvalid}
>
<NumberInputField value={value} onChange={onChange} defaultValue={input.defaultValue} />
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>
value={value}
onChange={onChange}
defaultValue={input.defaultValue}
/>
);
}
if (input.valueType === WorkflowIOValueTypeEnum.boolean) {
@@ -100,22 +217,26 @@ const RenderPluginInput = ({
);
})();
return !!render ? (
<Box _notLast={{ mb: 4 }} px={1}>
<Flex alignItems={'center'} mb={1}>
<Box position={'relative'}>
{input.required && (
<Box position={'absolute'} left={-2} top={'-1px'} color={'red.600'}>
*
</Box>
)}
{t(input.label as any)}
</Box>
{input.description && <QuestionTip ml={2} label={t(input.description as any)} />}
</Flex>
return (
<Box _notLast={{ mb: 4 }}>
{/* label */}
{inputType !== FlowNodeInputTypeEnum.fileSelect && (
<Flex alignItems={'center'} mb={1}>
<Box position={'relative'}>
{input.required && (
<Box position={'absolute'} left={-2} top={'-1px'} color={'red.600'}>
*
</Box>
)}
<FormLabel fontWeight={'500'}>{t(input.label as any)}</FormLabel>
</Box>
{input.description && <QuestionTip ml={2} label={t(input.description as any)} />}
</Flex>
)}
{render}
</Box>
) : null;
);
};
export default RenderPluginInput;

View File

@@ -1,4 +1,4 @@
import React, { ReactNode, useCallback, useMemo, useRef, useState } from 'react';
import React, { ReactNode, useCallback, useMemo, useRef } from 'react';
import { createContext } from 'use-context-selector';
import { PluginRunBoxProps } from './type';
import {
@@ -8,7 +8,6 @@ import {
} from '@fastgpt/global/core/chat/type';
import { FieldValues, useForm } from 'react-hook-form';
import { PluginRunBoxTabEnum } from './constants';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { getNanoid } from '@fastgpt/global/common/string/tools';
import { ChatItemValueTypeEnum, ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
@@ -16,17 +15,16 @@ import { generatingMessageProps } from '../type';
import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants';
import { useTranslation } from 'next-i18next';
import { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat';
import { ChatBoxInputFormType, UserInputFileItemType } from '../ChatBox/type';
import { ChatBoxInputFormType } from '../ChatBox/type';
import { chats2GPTMessages } from '@fastgpt/global/core/chat/adapt';
import { getPluginRunUserQuery } from '@fastgpt/global/core/workflow/utils';
import { cloneDeep } from 'lodash';
type PluginRunContextType = OutLinkChatAuthProps &
PluginRunBoxProps & {
isChatting: boolean;
onSubmit: (e: ChatBoxInputFormType, files?: UserInputFileItemType[]) => Promise<any>;
onSubmit: (e: ChatBoxInputFormType) => Promise<any>;
outLinkAuthData: OutLinkChatAuthProps;
restartInputStore?: ChatBoxInputFormType;
setRestartInputStore: React.Dispatch<React.SetStateAction<ChatBoxInputFormType | undefined>>;
};
export const PluginRunContext = createContext<PluginRunContextType>({
@@ -59,8 +57,6 @@ const PluginRunContextProvider = ({
}: PluginRunBoxProps & { children: ReactNode }) => {
const { pluginInputs, onStartChat, setHistories, histories, setTab } = props;
const [restartInputStore, setRestartInputStore] = useState<ChatBoxInputFormType>();
const { toast } = useToast();
const chatController = useRef(new AbortController());
const { t } = useTranslation();
@@ -80,9 +76,7 @@ const PluginRunContextProvider = ({
);
const variablesForm = useForm<ChatBoxInputFormType>({
defaultValues: {
files: []
}
defaultValues: {}
});
const generatingMessage = useCallback(
@@ -179,8 +173,8 @@ const PluginRunContextProvider = ({
[histories]
);
const { runAsync: onSubmit } = useRequest2(
async (e: ChatBoxInputFormType, files?: UserInputFileItemType[]) => {
const onSubmit = useCallback(
async ({ variables, files }: ChatBoxInputFormType) => {
if (!onStartChat) return;
if (isChatting) {
toast({
@@ -199,7 +193,7 @@ const PluginRunContextProvider = ({
{
...getPluginRunUserQuery({
pluginInputs,
variables: e,
variables,
files: files as RuntimeUserPromptType['files']
}),
status: 'finish'
@@ -233,12 +227,33 @@ const PluginRunContextProvider = ({
});
try {
// Remove files icon
const formatVariables = cloneDeep(variables);
for (const key in formatVariables) {
if (Array.isArray(formatVariables[key])) {
formatVariables[key].forEach((item) => {
if (item.url && item.icon) {
delete item.icon;
}
});
}
}
const { responseData } = await onStartChat({
messages: messages,
messages,
controller: chatController.current,
generatingMessage,
variables: e
variables: {
files,
...formatVariables
}
});
if (responseData?.[responseData.length - 1]?.error) {
toast({
title: responseData[responseData.length - 1].error?.message,
status: 'error'
});
}
setHistories((state) =>
state.map((item, index) => {
@@ -262,7 +277,18 @@ const PluginRunContextProvider = ({
})
);
}
}
},
[
abortRequest,
generatingMessage,
isChatting,
onStartChat,
pluginInputs,
setHistories,
setTab,
t,
toast
]
);
const contextValue: PluginRunContextType = {
@@ -270,9 +296,7 @@ const PluginRunContextProvider = ({
isChatting,
onSubmit,
outLinkAuthData,
variablesForm,
restartInputStore,
setRestartInputStore
variablesForm
};
return <PluginRunContext.Provider value={contextValue}>{children}</PluginRunContext.Provider>;
};

View File

@@ -6,6 +6,7 @@ import MyBox from '@fastgpt/web/components/common/MyBox';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { ChatFileTypeEnum } from '@fastgpt/global/core/chat/constants';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
import MyImage from '@fastgpt/web/components/common/Image/MyImage';
const RenderFilePreview = ({
fileList,
@@ -18,13 +19,12 @@ const RenderFilePreview = ({
return fileList.length > 0 ? (
<Flex
maxH={'250px'}
overflowY={'auto'}
overflow={'visible'}
wrap={'wrap'}
pt={3}
userSelect={'none'}
mb={fileList.length > 0 ? 2 : 0}
pr={0.5}
gap={'6px'}
>
{fileList.map((item, index) => {
const isFile = item.type === ChatFileTypeEnum.file;
@@ -33,11 +33,8 @@ const RenderFilePreview = ({
<MyBox
key={index}
maxW={isFile ? 56 : 14}
w={isFile ? '50%' : '12.5%'}
w={isFile ? 'calc(50% - 3px)' : '12.5%'}
aspectRatio={isFile ? 4 : 1}
pr={1.5}
pb={1.5}
mb={0.5}
>
<Box
border={'sm'}
@@ -74,7 +71,7 @@ const RenderFilePreview = ({
/>
)}
{isImage && (
<Image
<MyImage
alt={'img'}
src={item.icon}
w={'full'}

View File

@@ -28,13 +28,24 @@ export const useChat = (params?: { chatId?: string; appId: string; type?: GetCha
// Reset to empty input
const data = variablesForm.getValues();
for (const key in data) {
data[key] = '';
// Reset the old variables to empty
const resetVariables: Record<string, any> = {};
for (const key in data.variables) {
resetVariables[key] = (() => {
if (Array.isArray(data.variables[key])) {
return [];
}
return '';
})();
}
variablesForm.reset({
...data,
...variables
variables: {
...resetVariables,
...variables
}
});
},
[variablesForm]
@@ -42,8 +53,8 @@ export const useChat = (params?: { chatId?: string; appId: string; type?: GetCha
const clearChatRecords = useCallback(() => {
const data = variablesForm.getValues();
for (const key in data) {
variablesForm.setValue(key, '');
for (const key in data.variables) {
variablesForm.setValue(`variables.${key}`, '');
}
ChatBoxRef.current?.restartChat?.();

View File

@@ -39,6 +39,7 @@ import { useTranslation } from 'react-i18next';
import { Controller, useForm } from 'react-hook-form';
import MySelect from '@fastgpt/web/components/common/MySelect';
import MyTextarea from '@/components/common/Textarea/MyTextarea';
import MyNumberInput from '@fastgpt/web/components/common/Input/NumberInput';
type props = {
value: UserChatItemValueItemType | AIChatItemValueItemType;
@@ -244,25 +245,15 @@ const RenderUserFormInteractive = React.memo(function RenderFormInput({
/>
)}
{input.type === FlowNodeInputTypeEnum.numberInput && (
<NumberInput
step={1}
<MyNumberInput
min={input.min}
max={input.max}
isDisabled={interactive.params.submitted}
bg={'white'}
rounded={'md'}
>
<NumberInputField
bg={'white'}
{...register(input.label, {
required: input.required
})}
/>
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>
register={register}
name={input.label}
isRequired={input.required}
/>
)}
{input.type === FlowNodeInputTypeEnum.select && (
<Controller

View File

@@ -31,12 +31,10 @@ type sideTabItemType = {
/* Per response value */
export const WholeResponseContent = ({
activeModule,
hideTabs,
showDetail
hideTabs
}: {
activeModule: ChatHistoryItemResType;
hideTabs?: boolean;
showDetail: boolean;
}) => {
const { t } = useTranslation();
@@ -233,10 +231,14 @@ export const WholeResponseContent = ({
{activeModule.quoteList && activeModule.quoteList.length > 0 && (
<Row
label={t('common:core.chat.response.module quoteList')}
rawDom={<QuoteList showDetail={showDetail} rawSearch={activeModule.quoteList} />}
rawDom={<QuoteList canEditDataset canViewSource rawSearch={activeModule.quoteList} />}
/>
)}
</>
{/* dataset concat */}
<>
<Row label={t('chat:response.dataset_concat_length')} value={activeModule?.concatLength} />
</>
{/* classify question */}
<>
<Row
@@ -527,12 +529,10 @@ const SideTabItem = ({
/* Modal main container */
export const ResponseBox = React.memo(function ResponseBox({
response,
showDetail,
hideTabs = false,
useMobile = false
}: {
response: ChatHistoryItemResType[];
showDetail: boolean;
hideTabs?: boolean;
useMobile?: boolean;
}) {
@@ -655,11 +655,7 @@ export const ResponseBox = React.memo(function ResponseBox({
</Box>
</Box>
<Box flex={'5 0 0'} w={0} height={'100%'}>
<WholeResponseContent
activeModule={activeModule}
hideTabs={hideTabs}
showDetail={showDetail}
/>
<WholeResponseContent activeModule={activeModule} hideTabs={hideTabs} />
</Box>
</Flex>
) : (
@@ -719,11 +715,7 @@ export const ResponseBox = React.memo(function ResponseBox({
</Box>
</Flex>
<Box flex={'1 0 0'}>
<WholeResponseContent
activeModule={activeModule}
hideTabs={hideTabs}
showDetail={showDetail}
/>
<WholeResponseContent activeModule={activeModule} hideTabs={hideTabs} />
</Box>
</Flex>
)}
@@ -733,15 +725,7 @@ export const ResponseBox = React.memo(function ResponseBox({
);
});
const WholeResponseModal = ({
showDetail,
onClose,
dataId
}: {
showDetail: boolean;
onClose: () => void;
dataId: string;
}) => {
const WholeResponseModal = ({ onClose, dataId }: { onClose: () => void; dataId: string }) => {
const { t } = useTranslation();
const { getHistoryResponseData } = useContextSelector(ChatBoxContext, (v) => v);
@@ -770,7 +754,7 @@ const WholeResponseModal = ({
}
>
{!!response?.length ? (
<ResponseBox response={response} showDetail={showDetail} />
<ResponseBox response={response} />
) : (
<EmptyTip text={t('chat:no_workflow_response')} />
)}

View File

@@ -45,11 +45,11 @@ const scoreTheme: Record<
const QuoteItem = ({
quoteItem,
canViewSource,
linkToDataset
canEditDataset
}: {
quoteItem: SearchDataResponseItemType;
canViewSource?: boolean;
linkToDataset?: boolean;
canEditDataset?: boolean;
}) => {
const { t } = useTranslation();
const [editInputData, setEditInputData] = useState<{ dataId: string; collectionId: string }>();
@@ -110,89 +110,64 @@ const QuoteItem = ({
>
<Flex alignItems={'center'} mb={3} flexWrap={'wrap'} gap={3}>
{score?.primaryScore && (
<>
{canViewSource ? (
<MyTooltip label={t(SearchScoreTypeMap[score.primaryScore.type]?.desc as any)}>
<Flex
px={'12px'}
py={'5px'}
borderRadius={'md'}
color={'primary.700'}
bg={'primary.50'}
borderWidth={'1px'}
borderColor={'primary.200'}
alignItems={'center'}
fontSize={'sm'}
>
<Box>#{score.primaryScore.index + 1}</Box>
<Box
borderRightColor={'primary.700'}
borderRightWidth={'1px'}
h={'14px'}
mx={2}
/>
<Box>
{t(SearchScoreTypeMap[score.primaryScore.type]?.label as any)}
{SearchScoreTypeMap[score.primaryScore.type]?.showScore
? ` ${score.primaryScore.value.toFixed(4)}`
: ''}
</Box>
</Flex>
</MyTooltip>
) : (
<Flex
px={'12px'}
py={'1px'}
mr={4}
borderRadius={'md'}
color={'primary.700'}
bg={'primary.50'}
borderWidth={'1px'}
borderColor={'primary.200'}
alignItems={'center'}
fontSize={'sm'}
>
<Box>#{score.primaryScore.index + 1}</Box>
</Flex>
)}
</>
)}
{canViewSource &&
score.secondaryScore.map((item, i) => (
<MyTooltip key={item.type} label={t(SearchScoreTypeMap[item.type]?.desc as any)}>
<Box fontSize={'xs'}>
<Flex alignItems={'flex-start'} lineHeight={1.2} mb={1}>
<Box
px={'5px'}
borderWidth={'1px'}
borderRadius={'sm'}
mr={'2px'}
{...(scoreTheme[i] && scoreTheme[i])}
>
<Box transform={'scale(0.9)'}>#{item.index + 1}</Box>
</Box>
<Box transform={'scale(0.9)'}>
{t(SearchScoreTypeMap[item.type]?.label as any)}: {item.value.toFixed(4)}
</Box>
</Flex>
<Box h={'4px'}>
{SearchScoreTypeMap[item.type]?.showScore && (
<Progress
value={item.value * 100}
h={'4px'}
w={'100%'}
size="sm"
borderRadius={'20px'}
{...(scoreTheme[i] && {
colorScheme: scoreTheme[i].colorScheme
})}
bg="#E8EBF0"
/>
)}
</Box>
<MyTooltip label={t(SearchScoreTypeMap[score.primaryScore.type]?.desc as any)}>
<Flex
px={'12px'}
py={'5px'}
borderRadius={'md'}
color={'primary.700'}
bg={'primary.50'}
borderWidth={'1px'}
borderColor={'primary.200'}
alignItems={'center'}
fontSize={'sm'}
>
<Box>#{score.primaryScore.index + 1}</Box>
<Box borderRightColor={'primary.700'} borderRightWidth={'1px'} h={'14px'} mx={2} />
<Box>
{t(SearchScoreTypeMap[score.primaryScore.type]?.label as any)}
{SearchScoreTypeMap[score.primaryScore.type]?.showScore
? ` ${score.primaryScore.value.toFixed(4)}`
: ''}
</Box>
</MyTooltip>
))}
</Flex>
</MyTooltip>
)}
{score.secondaryScore.map((item, i) => (
<MyTooltip key={item.type} label={t(SearchScoreTypeMap[item.type]?.desc as any)}>
<Box fontSize={'xs'}>
<Flex alignItems={'flex-start'} lineHeight={1.2} mb={1}>
<Box
px={'5px'}
borderWidth={'1px'}
borderRadius={'sm'}
mr={'2px'}
{...(scoreTheme[i] && scoreTheme[i])}
>
<Box transform={'scale(0.9)'}>#{item.index + 1}</Box>
</Box>
<Box transform={'scale(0.9)'}>
{t(SearchScoreTypeMap[item.type]?.label as any)}: {item.value.toFixed(4)}
</Box>
</Flex>
<Box h={'4px'}>
{SearchScoreTypeMap[item.type]?.showScore && (
<Progress
value={item.value * 100}
h={'4px'}
w={'100%'}
size="sm"
borderRadius={'20px'}
{...(scoreTheme[i] && {
colorScheme: scoreTheme[i].colorScheme
})}
bg="#E8EBF0"
/>
)}
</Box>
</Box>
</MyTooltip>
))}
</Flex>
<Box flex={'1 0 0'}>
@@ -200,73 +175,71 @@ const QuoteItem = ({
<Box color={'myGray.600'}>{quoteItem.a}</Box>
</Box>
{canViewSource && (
<Flex
alignItems={'center'}
flexWrap={'wrap'}
mt={3}
gap={4}
color={'myGray.500'}
fontSize={'xs'}
>
<MyTooltip label={t('common:core.dataset.Quote Length')}>
<Flex alignItems={'center'}>
<MyIcon name="common/text/t" w={'14px'} mr={1} color={'myGray.500'} />
{quoteItem.q.length + (quoteItem.a?.length || 0)}
</Flex>
</MyTooltip>
<RawSourceBox
fontWeight={'bold'}
color={'black'}
collectionId={quoteItem.collectionId}
sourceName={quoteItem.sourceName}
sourceId={quoteItem.sourceId}
canView={canViewSource}
/>
<Box flex={1} />
{quoteItem.id && (
<MyTooltip label={t('common:core.dataset.data.Edit')}>
<Box
className="hover-data"
visibility={'hidden'}
display={'flex'}
alignItems={'center'}
justifyContent={'center'}
>
<MyIcon
name={'edit'}
w={['16px', '18px']}
h={['16px', '18px']}
cursor={'pointer'}
color={'myGray.600'}
_hover={{
color: 'primary.600'
}}
onClick={() =>
setEditInputData({
dataId: quoteItem.id,
collectionId: quoteItem.collectionId
})
}
/>
</Box>
</MyTooltip>
)}
{linkToDataset && (
<Link
as={NextLink}
<Flex
alignItems={'center'}
flexWrap={'wrap'}
mt={3}
gap={4}
color={'myGray.500'}
fontSize={'xs'}
>
<MyTooltip label={t('common:core.dataset.Quote Length')}>
<Flex alignItems={'center'}>
<MyIcon name="common/text/t" w={'14px'} mr={1} color={'myGray.500'} />
{quoteItem.q.length + (quoteItem.a?.length || 0)}
</Flex>
</MyTooltip>
<RawSourceBox
fontWeight={'bold'}
color={'black'}
collectionId={quoteItem.collectionId}
sourceName={quoteItem.sourceName}
sourceId={quoteItem.sourceId}
canView={canViewSource}
/>
<Box flex={1} />
{quoteItem.id && canEditDataset && (
<MyTooltip label={t('common:core.dataset.data.Edit')}>
<Box
className="hover-data"
visibility={'hidden'}
display={'flex'}
alignItems={'center'}
color={'primary.500'}
href={`/dataset/detail?datasetId=${quoteItem.datasetId}&currentTab=dataCard&collectionId=${quoteItem.collectionId}`}
justifyContent={'center'}
>
{t('common:core.dataset.Go Dataset')}
<MyIcon name={'common/rightArrowLight'} w={'10px'} />
</Link>
)}
</Flex>
)}
<MyIcon
name={'edit'}
w={['16px', '18px']}
h={['16px', '18px']}
cursor={'pointer'}
color={'myGray.600'}
_hover={{
color: 'primary.600'
}}
onClick={() =>
setEditInputData({
dataId: quoteItem.id,
collectionId: quoteItem.collectionId
})
}
/>
</Box>
</MyTooltip>
)}
{canEditDataset && (
<Link
as={NextLink}
className="hover-data"
visibility={'hidden'}
alignItems={'center'}
color={'primary.500'}
href={`/dataset/detail?datasetId=${quoteItem.datasetId}&currentTab=dataCard&collectionId=${quoteItem.collectionId}`}
>
{t('common:core.dataset.Go Dataset')}
<MyIcon name={'common/rightArrowLight'} w={'10px'} />
</Link>
)}
</Flex>
</MyBox>
{editInputData && (

View File

@@ -6,19 +6,23 @@ import { getCollectionSourceAndOpen } from '@/web/core/dataset/hooks/readCollect
import { getSourceNameIcon } from '@fastgpt/global/core/dataset/utils';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useI18n } from '@/web/context/I18n';
import { ShareChatAuthProps } from '@fastgpt/global/support/permission/chat';
type Props = BoxProps & {
sourceName?: string;
collectionId: string;
sourceId?: string;
canView?: boolean;
};
type Props = BoxProps &
ShareChatAuthProps & {
sourceName?: string;
collectionId: string;
sourceId?: string;
canView?: boolean;
};
const RawSourceBox = ({
sourceId,
collectionId,
sourceName = '',
canView = true,
shareId,
outLinkUid,
...props
}: Props) => {
const { t } = useTranslation();
@@ -27,7 +31,11 @@ const RawSourceBox = ({
const canPreview = !!sourceId && canView;
const icon = useMemo(() => getSourceNameIcon({ sourceId, sourceName }), [sourceId, sourceName]);
const read = getCollectionSourceAndOpen(collectionId);
const read = getCollectionSourceAndOpen({
collectionId,
shareId,
outLinkUid
});
return (
<MyTooltip

View File

@@ -101,7 +101,7 @@ const LafAccountModal = ({
<Box fontSize={'sm'} color={'myGray.500'}>
<Box>{t('common:support.user.Laf account intro')}</Box>
<Box textDecoration={'underline'}>
<Link href={getDocPath('/docs/workflow/modules/laf/')} isExternal>
<Link href={getDocPath('/docs/guide/workbench/workflow/laf/')} isExternal>
{t('common:support.user.Laf account course')}
</Link>
</Box>

View File

@@ -61,6 +61,7 @@ const DefaultPermissionList = ({
}
}}
fontSize={styles?.fontSize}
fontWeight={styles?.fontWeight}
/>
</Box>
<ConfirmModal />

View File

@@ -1,5 +1,6 @@
import { getCaptchaPic } from '@/web/support/user/api';
import { Button, Input, Image, ModalBody, ModalFooter, Skeleton } from '@chakra-ui/react';
import MyImage from '@fastgpt/web/components/common/Image/MyImage';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next';
@@ -42,7 +43,7 @@ const SendCodeAuthModal = ({
justifyContent={'center'}
my={1}
>
<Image
<MyImage
borderRadius={'md'}
w={'100%'}
h={'200px'}

View File

@@ -13,6 +13,7 @@ import '@/web/styles/reset.scss';
import NextHead from '@/components/common/NextHead';
import { ReactElement, useEffect } from 'react';
import { NextPage } from 'next';
import { getWebReqUrl } from '@fastgpt/web/common/system/utils';
type NextPageWithLayout = NextPage & {
setLayout?: (page: ReactElement) => JSX.Element;
@@ -49,7 +50,7 @@ function App({ Component, pageProps }: AppPropsWithLayout) {
process.env.SYSTEM_DESCRIPTION ||
`${title}${t('app:intro')}`
}
icon={feConfigs?.favicon || process.env.SYSTEM_FAVICON}
icon={getWebReqUrl(feConfigs?.favicon || process.env.SYSTEM_FAVICON)}
/>
{scripts?.map((item, i) => <Script key={i} strategy="lazyOnload" {...item}></Script>)}

View File

@@ -9,7 +9,6 @@ import {
Link,
Progress,
Grid,
Image,
BoxProps
} from '@chakra-ui/react';
import { useForm } from 'react-hook-form';
@@ -45,6 +44,7 @@ import StandardPlanContentList from '@/components/support/wallet/StandardPlanCon
import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
import MyImage from '@fastgpt/web/components/common/Image/MyImage';
const StandDetailModal = dynamic(() => import('./standardDetailModal'));
const TeamMenu = dynamic(() => import('@/components/support/user/team/TeamMenu'));
@@ -653,7 +653,7 @@ const Other = ({ onOpenContact }: { onOpenContact: () => void }) => {
onClick={onOpenLaf}
fontSize={'sm'}
>
<Image src="/imgs/workflow/laf.png" w={'18px'} alt="laf" />
<MyImage src="/imgs/workflow/laf.png" w={'18px'} alt="laf" />
<Box ml={2} flex={1}>
{'laf' + t('common:navbar.Account')}
</Box>

View File

@@ -43,7 +43,7 @@ const InformTable = () => {
<Flex alignItems={'center'}>
<Box fontWeight={'bold'}>{item.title}</Box>
<Box ml={2} color={'myGray.500'} flex={'1 0 0'}>
({formatTimeToChatTime(item.time)})
({t(formatTimeToChatTime(item.time) as any).replace('#', ':')})
</Box>
{!item.read && (
<Button

View File

@@ -13,6 +13,7 @@ import { serviceSideProps } from '@/web/common/utils/i18n';
import { useTranslation } from 'next-i18next';
import Script from 'next/script';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
import { getWebReqUrl } from '@fastgpt/web/common/system/utils';
const Promotion = dynamic(() => import('./components/Promotion'));
const UsageTable = dynamic(() => import('./components/UsageTable'));
@@ -128,7 +129,7 @@ const Account = ({ currentTab }: { currentTab: TabEnum }) => {
return (
<>
<Script src="/js/qrcode.min.js" strategy="lazyOnload"></Script>
<Script src={getWebReqUrl('/js/qrcode.min.js')} strategy="lazyOnload"></Script>
<PageContainer>
<Flex flexDirection={['column', 'row']} h={'100%'} pt={[4, 0]}>
{isPc ? (

View File

@@ -17,16 +17,8 @@ async function handler(
req: ApiRequestProps<getHistoriesBody, getHistoriesQuery>,
res: ApiResponseType<any>
): Promise<PaginationResponse<getHistoriesResponse>> {
const {
appId,
shareId,
outLinkUid,
teamId,
teamToken,
offset,
pageSize,
source = ChatSourceEnum.online
} = req.body as getHistoriesBody;
const { appId, shareId, outLinkUid, teamId, teamToken, offset, pageSize, source } =
req.body as getHistoriesBody;
const match = await (async () => {
if (shareId && outLinkUid) {
@@ -35,7 +27,6 @@ async function handler(
return {
shareId,
outLinkUid: uid,
source: ChatSourceEnum.share,
updateTime: {
$gte: new Date(new Date().setDate(new Date().getDate() - 30))
}
@@ -55,7 +46,7 @@ async function handler(
return {
tmbId,
appId,
source: source
source
};
}
})();

View File

@@ -9,7 +9,7 @@ import { getChatItems } from '@fastgpt/service/core/chat/controller';
import { authChatCrud } from '@/service/support/permission/auth/chat';
import { MongoApp } from '@fastgpt/service/core/app/schema';
import { AppErrEnum } from '@fastgpt/global/common/error/code/app';
import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
import { ChatItemValueTypeEnum, ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
import { filterPublicNodeResponseData } from '@fastgpt/global/core/chat/utils';
import { authOutLink } from '@/service/support/permission/auth/outLink';
import { GetChatTypeEnum } from '@/global/core/chat/constants';
@@ -67,13 +67,11 @@ async function handler(
})();
const fieldMap = {
[GetChatTypeEnum.normal]: `dataId obj value adminFeedback userBadFeedback userGoodFeedback ${
[GetChatTypeEnum.normal]: `dataId obj value adminFeedback userBadFeedback userGoodFeedback time ${
DispatchNodeResponseKeyEnum.nodeResponse
} ${loadCustomFeedbacks ? 'customFeedbacks' : ''}`,
[GetChatTypeEnum.outLink]: `dataId obj value userGoodFeedback userBadFeedback adminFeedback ${
shareChat?.responseDetail || isPlugin ? `${DispatchNodeResponseKeyEnum.nodeResponse}` : ''
} `,
[GetChatTypeEnum.team]: `dataId obj value userGoodFeedback userBadFeedback adminFeedback ${DispatchNodeResponseKeyEnum.nodeResponse}`
[GetChatTypeEnum.outLink]: `dataId obj value userGoodFeedback userBadFeedback adminFeedback time ${DispatchNodeResponseKeyEnum.nodeResponse}`,
[GetChatTypeEnum.team]: `dataId obj value userGoodFeedback userBadFeedback adminFeedback time ${DispatchNodeResponseKeyEnum.nodeResponse}`
};
const { total, histories } = await getChatItems({
@@ -85,10 +83,14 @@ async function handler(
});
// Remove important information
if (type === 'outLink' && app.type !== AppTypeEnum.plugin) {
if (shareChat && app.type !== AppTypeEnum.plugin) {
histories.forEach((item) => {
if (item.obj === ChatRoleEnum.AI) {
item.responseData = filterPublicNodeResponseData({ flowResponses: item.responseData });
if (shareChat.showNodeStatus === false) {
item.value = item.value.filter((v) => v.type !== ChatItemValueTypeEnum.tool);
}
}
});
}

View File

@@ -2,7 +2,6 @@ import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import type { InitChatResponse, InitOutLinkChatProps } from '@/global/core/chat/api.d';
import { getGuideModule, getAppChatConfig } from '@fastgpt/global/core/workflow/utils';
import { getChatModelNameListByModules } from '@/service/core/app/workflow';
import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema';
import { authOutLink } from '@/service/support/permission/auth/outLink';
import { MongoApp } from '@fastgpt/service/core/app/schema';
@@ -54,7 +53,6 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
storeWelcomeText: chat?.welcomeText,
isPublicFetch: false
}),
chatModels: getChatModelNameListByModules(nodes),
name: app.name,
avatar: app.avatar,
intro: app.intro,

View File

@@ -5,12 +5,13 @@ import { DatasetCollectionTypeEnum } from '@fastgpt/global/core/dataset/constant
import { createFileToken } from '@fastgpt/service/support/permission/controller';
import { BucketNameEnum, ReadFileBaseUrl } from '@fastgpt/global/common/file/constants';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import { ShareChatAuthProps } from '@fastgpt/global/support/permission/chat';
export type readCollectionSourceQuery = {
export type readCollectionSourceQuery = {};
export type readCollectionSourceBody = {
collectionId: string;
};
export type readCollectionSourceBody = {};
} & ShareChatAuthProps;
export type readCollectionSourceResponse = {
type: 'url';
@@ -24,7 +25,7 @@ async function handler(
req,
authToken: true,
authApiKey: true,
collectionId: req.query.collectionId,
collectionId: req.body.collectionId,
per: ReadPermissionVal
});

View File

@@ -24,7 +24,7 @@ export type OutLinkUpdateResponse = {};
async function handler(
req: ApiRequestProps<OutLinkUpdateBody, OutLinkUpdateQuery>
): Promise<OutLinkUpdateResponse> {
const { _id, name, responseDetail, limit, app } = req.body;
const { _id, name, responseDetail, limit, app, showRawSource, showNodeStatus } = req.body;
if (!_id) {
return Promise.reject(CommonErrEnum.missingParams);
@@ -35,6 +35,8 @@ async function handler(
await MongoOutLink.findByIdAndUpdate(_id, {
name,
responseDetail,
showRawSource,
showNodeStatus,
limit,
app
});

View File

@@ -63,6 +63,8 @@ import { getPluginInputsFromStoreNodes } from '@fastgpt/global/core/app/plugin/u
type FastGptWebChatProps = {
chatId?: string; // undefined: get histories from messages, '': new chat, 'xxxxx': get histories from db
appId?: string;
customUid?: string; // non-undefined: will be the priority provider for the logger.
metadata?: Record<string, any>;
};
export type Props = ChatCompletionCreateParams &
@@ -81,10 +83,12 @@ type AuthResponseType = {
user: UserModelSchema;
app: AppSchema;
responseDetail?: boolean;
showNodeStatus?: boolean;
authType: `${AuthUserTypeEnum}`;
apikey?: string;
canWrite: boolean;
outLinkUserId?: string;
sourceName?: string;
};
async function handler(req: NextApiRequest, res: NextApiResponse) {
@@ -99,6 +103,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
let {
chatId,
appId,
customUid,
// share chat
shareId,
outLinkUid,
@@ -110,7 +115,8 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
detail = false,
messages = [],
variables = {},
responseChatItemId = getNanoid()
responseChatItemId = getNanoid(),
metadata
} = req.body as Props;
const originIp = requestIp.getClientIp(req);
@@ -122,7 +128,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
throw new Error('messages is not array');
}
/*
/*
Web params: chatId + [Human]
API params: chatId + [Human]
API params: [histories, Human]
@@ -139,41 +145,52 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
return JSON.stringify(variables);
})();
/*
/*
1. auth app permission
2. auth balance
3. get app
4. parse outLink token
*/
const { teamId, tmbId, user, app, responseDetail, authType, apikey, canWrite, outLinkUserId } =
await (async () => {
// share chat
if (shareId && outLinkUid) {
return authShareChat({
shareId,
outLinkUid,
chatId,
ip: originIp,
question: startHookText
});
}
// team space chat
if (spaceTeamId && appId && teamToken) {
return authTeamSpaceChat({
teamId: spaceTeamId,
teamToken,
appId,
chatId
});
}
/* parse req: api or token */
return authHeaderRequest({
req,
const {
teamId,
tmbId,
user,
app,
responseDetail,
authType,
sourceName,
apikey,
canWrite,
outLinkUserId = customUid,
showNodeStatus
} = await (async () => {
// share chat
if (shareId && outLinkUid) {
return authShareChat({
shareId,
outLinkUid,
chatId,
ip: originIp,
question: startHookText
});
}
// team space chat
if (spaceTeamId && appId && teamToken) {
return authTeamSpaceChat({
teamId: spaceTeamId,
teamToken,
appId,
chatId
});
})();
}
/* parse req: api or token */
return authHeaderRequest({
req,
appId,
chatId
});
})();
const isPlugin = app.type === AppTypeEnum.plugin;
// Check message type
@@ -241,7 +258,8 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
res,
detail,
streamResponse: stream,
id: chatId
id: chatId,
showNodeStatus
});
/* start flow controller */
@@ -330,10 +348,11 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
newTitle,
shareId,
outLinkUid: outLinkUserId,
source,
source: sourceName || source,
content: [userQuestion, aiResponse],
metadata: {
originIp
originIp,
...metadata
}
});
}
@@ -445,7 +464,7 @@ const authShareChat = async ({
shareId: string;
chatId?: string;
}): Promise<AuthResponseType> => {
const { teamId, tmbId, user, appId, authType, responseDetail, uid } =
const { teamId, tmbId, user, appId, authType, responseDetail, showNodeStatus, uid, sourceName } =
await authOutLinkChatStart(data);
const app = await MongoApp.findById(appId).lean();
@@ -460,6 +479,7 @@ const authShareChat = async ({
}
return {
sourceName,
teamId,
tmbId,
user,
@@ -468,7 +488,8 @@ const authShareChat = async ({
apikey: '',
authType,
canWrite: false,
outLinkUserId: uid
outLinkUserId: uid,
showNodeStatus
};
};
const authTeamSpaceChat = async ({
@@ -527,6 +548,7 @@ const authHeaderRequest = async ({
teamId,
tmbId,
authType,
sourceName,
apikey
} = await authCert({
req,
@@ -593,6 +615,7 @@ const authHeaderRequest = async ({
responseDetail: true,
apikey,
authType,
sourceName,
canWrite: true
};
};

View File

@@ -180,6 +180,9 @@ const DetailLogsModal = ({
chatConfig={chat?.app?.chatConfig}
appId={appId}
chatId={chatId}
chatType="log"
showRawSource
showNodeStatus
/>
)}
</Box>

View File

@@ -129,15 +129,16 @@ const Logs = () => {
onClick={() => setDetailLogsId(item.id)}
>
<Td>
<Box>{t(ChatSourceMap[item.source]?.name || ('UnKnow' as any))}</Box>
{/* @ts-ignore */}
<Box>{t(ChatSourceMap[item.source]?.name) || item.source}</Box>
<Box color={'myGray.500'}>{dayjs(item.time).format('YYYY/MM/DD HH:mm')}</Box>
</Td>
<Td>
<Box>
{item.source === 'share' ? (
{!!item.outLinkUid ? (
item.outLinkUid
) : (
<Tag key={item._id} type={'fill'} colorSchema="white">
<HStack>
<Avatar
src={teamMembers.find((v) => v.tmbId === item.tmbId)?.avatar}
w="1.25rem"
@@ -145,7 +146,7 @@ const Logs = () => {
<Box fontSize={'sm'} ml={1}>
{teamMembers.find((v) => v.tmbId === item.tmbId)?.memberName}
</Box>
</Tag>
</HStack>
)}
</Box>
</Td>

View File

@@ -29,6 +29,11 @@ import { useDebounceEffect } from 'ahooks';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import SaveButton from '../Workflow/components/SaveButton';
import PublishHistories from '../PublishHistoriesSlider';
import {
WorkflowNodeEdgeContext,
WorkflowInitContext
} from '../WorkflowComponents/context/workflowInitContext';
import { WorkflowEventContext } from '../WorkflowComponents/context/workflowEventContext';
const Header = () => {
const { t } = useTranslation();
@@ -44,27 +49,31 @@ const Header = () => {
onClose: onCloseBackConfirm
} = useDisclosure();
const nodes = useContextSelector(WorkflowInitContext, (v) => v.nodes);
const edges = useContextSelector(WorkflowNodeEdgeContext, (v) => v.edges);
const {
flowData2StoreData,
flowData2StoreDataAndCheck,
setWorkflowTestData,
setShowHistoryModal,
showHistoryModal,
nodes,
edges,
past,
future,
setPast,
onSwitchTmpVersion,
onSwitchCloudVersion
} = useContextSelector(WorkflowContext, (v) => v);
const showHistoryModal = useContextSelector(WorkflowEventContext, (v) => v.showHistoryModal);
const setShowHistoryModal = useContextSelector(
WorkflowEventContext,
(v) => v.setShowHistoryModal
);
const { lastAppListRouteType } = useSystemStore();
const [isPublished, setIsPublished] = useState(false);
useDebounceEffect(
() => {
const savedSnapshot =
future.findLast((snapshot) => snapshot.isSaved) ||
[...future].reverse().find((snapshot) => snapshot.isSaved) ||
past.find((snapshot) => snapshot.isSaved);
const val = compareSnapshot(
@@ -145,7 +154,6 @@ const Header = () => {
)}
<Flex
mt={[2, 0]}
py={3}
pl={[2, 4]}
pr={[2, 6]}
borderBottom={'base'}
@@ -163,12 +171,20 @@ const Header = () => {
})}
>
{/* back */}
<MyIcon
name={'common/leftArrowLight'}
w={'1.75rem'}
cursor={'pointer'}
onClick={isPublished ? onBack : onOpenBackConfirm}
/>
<Box
_hover={{
bg: 'myGray.200'
}}
p={0.5}
borderRadius={'sm'}
>
<MyIcon
name={'common/leftArrowLight'}
w={6}
cursor={'pointer'}
onClick={isPublished ? onBack : onOpenBackConfirm}
/>
</Box>
{/* app info */}
<Box ml={1}>

View File

@@ -2,7 +2,7 @@ import React from 'react';
import { pluginSystemModuleTemplates } from '@fastgpt/global/core/workflow/template/constants';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { v1Workflow2V2 } from '@/web/core/workflow/adapt';
import WorkflowContextProvider, { WorkflowContext } from '../WorkflowComponents/context';
import { ReactFlowCustomProvider, WorkflowContext } from '../WorkflowComponents/context';
import { useContextSelector } from 'use-context-selector';
import { AppContext, TabEnum } from '../context';
import { useMount } from 'ahooks';
@@ -13,13 +13,15 @@ import dynamic from 'next/dynamic';
import { cloneDeep } from 'lodash';
import Flow from '../WorkflowComponents/Flow';
import { t } from 'i18next';
import { useTranslation } from 'next-i18next';
const Logs = dynamic(() => import('../Logs/index'));
const PublishChannel = dynamic(() => import('../Publish'));
const WorkflowEdit = () => {
const { appDetail, currentTab } = useContextSelector(AppContext, (e) => e);
const isV2Workflow = appDetail?.version === 'v2';
const { t } = useTranslation();
const { openConfirm, ConfirmModal } = useConfirm({
showCancel: false,
@@ -64,9 +66,9 @@ const WorkflowEdit = () => {
const Render = () => {
return (
<WorkflowContextProvider basicNodeTemplates={pluginSystemModuleTemplates}>
<ReactFlowCustomProvider templates={pluginSystemModuleTemplates}>
<WorkflowEdit />
</WorkflowContextProvider>
</ReactFlowCustomProvider>
);
};

View File

@@ -90,7 +90,10 @@ const FeiShuEditModal = ({
<Box color="myGray.600">{t('publish:feishu_api')}</Box>
{feConfigs?.docUrl && (
<Link
href={feConfigs.openAPIDocUrl || getDocPath('/docs/course/feishu')}
href={
feConfigs.openAPIDocUrl ||
getDocPath('/docs/use-cases/external-integration/feishu/')
}
target={'_blank'}
ml={2}
color={'primary.500'}

View File

@@ -73,7 +73,10 @@ const FeiShu = ({ appId }: { appId: string }) => {
</Box>
{feConfigs?.docUrl && (
<Link
href={feConfigs.openAPIDocUrl || getDocPath('/docs/course/feishu')}
href={
feConfigs.openAPIDocUrl ||
getDocPath('/docs/use-cases/external-integration/feishu/')
}
target={'_blank'}
color={'primary.500'}
fontSize={'sm'}
@@ -147,7 +150,7 @@ const FeiShu = ({ appId }: { appId: string }) => {
)}
<Td>
{item.lastTime
? t(formatTimeToChatTime(item.lastTime) as any)
? t(formatTimeToChatTime(item.lastTime) as any).replace('#', ':')
: t('common:common.Un used')}
</Td>
<Td display={'flex'} alignItems={'center'}>

View File

@@ -2,7 +2,7 @@ import { OutLinkSchema } from '@fastgpt/global/support/outLink/type';
import React, { useCallback, useState } from 'react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useTranslation } from 'next-i18next';
import { Box, Flex, FlexProps, Grid, Image, ModalBody, Switch, useTheme } from '@chakra-ui/react';
import { Box, Flex, FlexProps, Grid, ModalBody, Switch, useTheme } from '@chakra-ui/react';
import MyRadio from '@/components/common/MyRadio';
import { useForm } from 'react-hook-form';
import MyIcon from '@fastgpt/web/components/common/Icon';
@@ -10,6 +10,7 @@ import { useCopyData } from '@/web/common/hooks/useCopyData';
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
import { fileToBase64 } from '@/web/common/file/utils';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import MyImage from '@fastgpt/web/components/common/Image/MyImage';
enum UsingWayEnum {
link = 'link',
@@ -29,15 +30,15 @@ const SelectUsingWayModal = ({ share, onClose }: { share: OutLinkSchema; onClose
const VariableTypeList = [
{
title: <Image src={'/imgs/outlink/link.svg'} alt={''} />,
title: <MyImage src={'/imgs/outlink/link.svg'} alt={''} />,
value: UsingWayEnum.link
},
{
title: <Image src={'/imgs/outlink/iframe.svg'} alt={''} />,
title: <MyImage src={'/imgs/outlink/iframe.svg'} alt={''} />,
value: UsingWayEnum.iframe
},
{
title: <Image src={'/imgs/outlink/script.svg'} alt={''} />,
title: <MyImage src={'/imgs/outlink/script.svg'} alt={''} />,
value: UsingWayEnum.script
}
];
@@ -162,7 +163,7 @@ console.log("Chat box loaded")
</Flex>
<Flex {...gridItemStyle}>
<Box flex={1}>{t('common:core.app.outLink.Script Open Icon')}</Box>
<Image
<MyImage
src={getValues('scriptOpenIcon')}
alt={''}
w={'20px'}
@@ -173,7 +174,7 @@ console.log("Chat box loaded")
</Flex>
<Flex {...gridItemStyle}>
<Box flex={1}>{t('common:core.app.outLink.Script Close Icon')}</Box>
<Image
<MyImage
src={getValues('scriptCloseIcon')}
alt={''}
w={'20px'}

View File

@@ -32,7 +32,6 @@ import { useCopyData } from '@/web/common/hooks/useCopyData';
import { useForm } from 'react-hook-form';
import { defaultOutLinkForm } from '@/web/core/app/constants';
import type { OutLinkEditType, OutLinkSchema } from '@fastgpt/global/support/outLink/type.d';
import { useRequest } from '@/web/common/hooks/useRequest';
import { PublishChannelEnum } from '@fastgpt/global/support/outLink/constant';
import { useTranslation } from 'next-i18next';
import { useToast } from '@fastgpt/web/hooks/useToast';
@@ -48,6 +47,7 @@ import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import MyBox from '@fastgpt/web/components/common/MyBox';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
const SelectUsingWayModal = dynamic(() => import('./SelectUsingWayModal'));
@@ -150,7 +150,9 @@ const Share = ({ appId }: { appId: string; type: PublishChannelEnum }) => {
</>
)}
<Td>
{item.lastTime ? formatTimeToChatTime(item.lastTime) : t('common:common.Un used')}
{item.lastTime
? t(formatTimeToChatTime(item.lastTime) as any).replace('#', ':')
: t('common:common.Un used')}
</Td>
<Td display={'flex'} alignItems={'center'}>
<Button
@@ -181,7 +183,9 @@ const Share = ({ appId }: { appId: string; type: PublishChannelEnum }) => {
setEditLinkData({
_id: item._id,
name: item.name,
responseDetail: item.responseDetail,
responseDetail: item.responseDetail ?? false,
showRawSource: item.showRawSource ?? false,
showNodeStatus: item.showNodeStatus ?? false,
limit: item.limit
})
},
@@ -270,27 +274,30 @@ function EditLinkModal({
const {
register,
setValue,
watch,
handleSubmit: submitShareChat
} = useForm({
defaultValues: defaultData
});
const responseDetail = watch('responseDetail');
const showRawSource = watch('showRawSource');
const isEdit = useMemo(() => !!defaultData._id, [defaultData]);
const { mutate: onclickCreate, isLoading: creating } = useRequest({
mutationFn: async (e: OutLinkEditType) =>
const { runAsync: onclickCreate, loading: creating } = useRequest2(
async (e: OutLinkEditType) =>
createShareChat({
...e,
appId,
type
}),
errorToast: t('common:common.Create Failed'),
onSuccess: onCreate
});
const { mutate: onclickUpdate, isLoading: updating } = useRequest({
mutationFn: (e: OutLinkEditType) => {
return putShareChat(e);
},
{
errorToast: t('common:common.Create Failed'),
onSuccess: onCreate
}
);
const { runAsync: onclickUpdate, loading: updating } = useRequest2(putShareChat, {
errorToast: t('common:common.Update Failed'),
onSuccess: onEdit
});
@@ -300,101 +307,133 @@ function EditLinkModal({
isOpen={true}
iconSrc="/imgs/modal/shareFill.svg"
title={isEdit ? publishT('edit_link') : publishT('create_link')}
maxW={['90vw', '700px']}
w={'100%'}
h={['90vh', 'auto']}
>
<ModalBody>
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 90px'}>{t('common:Name')}</FormLabel>
<Input
placeholder={publishT('link_name')}
maxLength={20}
{...register('name', {
required: t('common:common.name_is_empty') || 'name_is_empty'
})}
/>
</Flex>
{feConfigs?.isPlus && (
<>
<Flex alignItems={'center'} mt={4}>
<FormLabel flex={'0 0 90px'} alignItems={'center'}>
{t('common:common.Expired Time')}
</FormLabel>
<Input
type="datetime-local"
defaultValue={
defaultData.limit?.expiredTime
? dayjs(defaultData.limit?.expiredTime).format('YYYY-MM-DDTHH:mm')
: ''
}
onChange={(e) => {
setValue('limit.expiredTime', new Date(e.target.value));
}}
/>
</Flex>
<Flex alignItems={'center'} mt={4}>
<Flex flex={'0 0 90px'} alignItems={'center'}>
<FormLabel>QPM</FormLabel>
<QuestionTip ml={1} label={publishT('qpm_tips' || '')}></QuestionTip>
</Flex>
<Input
max={1000}
{...register('limit.QPM', {
min: 0,
max: 1000,
valueAsNumber: true,
required: publishT('qpm_is_empty') || ''
})}
/>
</Flex>
<Flex alignItems={'center'} mt={4}>
<Flex flex={'0 0 90px'} alignItems={'center'}>
<FormLabel>{t('common:support.outlink.Max usage points')}</FormLabel>
<QuestionTip
ml={1}
label={t('common:support.outlink.Max usage points tip')}
></QuestionTip>
</Flex>
<Input
{...register('limit.maxUsagePoints', {
min: -1,
max: 10000000,
valueAsNumber: true,
required: true
})}
/>
</Flex>
<Flex alignItems={'center'} mt={4}>
<Flex flex={'0 0 90px'} alignItems={'center'}>
<FormLabel>{publishT('token_auth')}</FormLabel>
<QuestionTip ml={1} label={publishT('token_auth_tips') || ''}></QuestionTip>
</Flex>
<Input
placeholder={publishT('token_auth_tips') || ''}
fontSize={'sm'}
{...register('limit.hookUrl')}
/>
</Flex>
<Link
href={getDocPath('/docs/development/openapi/share')}
target={'_blank'}
fontSize={'xs'}
color={'myGray.500'}
>
{publishT('token_auth_use_cases')}
</Link>
</>
)}
<Flex alignItems={'center'} mt={4}>
<Flex flex={'0 0 90px'} alignItems={'center'}>
<FormLabel>{t('common:support.outlink.share.Response Quote')}</FormLabel>
<QuestionTip
ml={1}
label={t('support.outlink.share.Response Quote tips' || '')}
></QuestionTip>
<ModalBody
p={6}
display={['block', 'flex']}
flex={['1 0 0', 'auto']}
overflow={'auto'}
gap={4}
>
<Box pr={[0, 4]} flex={1} borderRight={['0px', '1px']} borderColor={['', 'myGray.150']}>
<Box fontSize={'sm'} fontWeight={'500'} color={'myGray.600'}>
{t('publish:basic_info')}
</Box>
<Flex alignItems={'center'} mt={4}>
<FormLabel flex={'0 0 90px'}>{t('common:Name')}</FormLabel>
<Input
placeholder={publishT('link_name')}
maxLength={20}
{...register('name', {
required: t('common:common.name_is_empty') || 'name_is_empty'
})}
/>
</Flex>
<Switch {...register('responseDetail')} />
</Flex>
{feConfigs?.isPlus && (
<>
<Flex alignItems={'center'} mt={4}>
<FormLabel flex={'0 0 90px'} alignItems={'center'}>
{t('common:common.Expired Time')}
</FormLabel>
<Input
type="datetime-local"
defaultValue={
defaultData.limit?.expiredTime
? dayjs(defaultData.limit?.expiredTime).format('YYYY-MM-DDTHH:mm')
: ''
}
onChange={(e) => {
setValue('limit.expiredTime', new Date(e.target.value));
}}
/>
</Flex>
<Flex alignItems={'center'} mt={4}>
<Flex flex={'0 0 90px'} alignItems={'center'}>
<FormLabel>QPM</FormLabel>
<QuestionTip ml={1} label={publishT('qpm_tips' || '')}></QuestionTip>
</Flex>
<Input
max={1000}
{...register('limit.QPM', {
min: 0,
max: 1000,
valueAsNumber: true,
required: publishT('qpm_is_empty') || ''
})}
/>
</Flex>
<Flex alignItems={'center'} mt={4}>
<Flex flex={'0 0 90px'} alignItems={'center'}>
<FormLabel>{t('common:support.outlink.Max usage points')}</FormLabel>
<QuestionTip
ml={1}
label={t('common:support.outlink.Max usage points tip')}
></QuestionTip>
</Flex>
<Input
{...register('limit.maxUsagePoints', {
min: -1,
max: 10000000,
valueAsNumber: true,
required: true
})}
/>
</Flex>
<Flex alignItems={'center'} mt={4}>
<Flex flex={'0 0 90px'} alignItems={'center'}>
<FormLabel>{publishT('token_auth')}</FormLabel>
<QuestionTip ml={1} label={publishT('token_auth_tips') || ''}></QuestionTip>
</Flex>
<Input
placeholder={publishT('token_auth_tips') || ''}
fontSize={'sm'}
{...register('limit.hookUrl')}
/>
</Flex>
<Link
href={getDocPath('/docs/development/openapi/share')}
target={'_blank'}
fontSize={'xs'}
color={'myGray.500'}
>
{publishT('token_auth_use_cases')}
</Link>
</>
)}
</Box>
<Box flex={1} pt={[6, 0]}>
<Box fontSize={'sm'} fontWeight={'500'} color={'myGray.600'}>
{t('publish:private_config')}
</Box>
<Flex alignItems={'center'} mt={4} justify={'space-between'} height={'36px'}>
<FormLabel>{t('publish:show_node')}</FormLabel>
<Switch {...register('showNodeStatus')} />
</Flex>
<Flex alignItems={'center'} mt={4} justify={'space-between'} height={'36px'}>
<Flex alignItems={'center'}>
<FormLabel>{t('common:support.outlink.share.Response Quote')}</FormLabel>
<QuestionTip
ml={1}
label={t('common:support.outlink.share.Response Quote tips' || '')}
></QuestionTip>
</Flex>
<Switch {...register('responseDetail')} isChecked={responseDetail} />
</Flex>
{/* <Flex alignItems={'center'} mt={4} justify={'space-between'} height={'36px'}>
<Flex alignItems={'center'}>
<FormLabel>{t('common:support.outlink.share.show_complete_quote')}</FormLabel>
<QuestionTip
ml={1}
label={t('common:support.outlink.share.show_complete_quote_tips' || '')}
></QuestionTip>
</Flex>
<Switch {...register('showRawSource')} isChecked={showRawSource} />
</Flex> */}
</Box>
</ModalBody>
<ModalFooter>

View File

@@ -96,7 +96,10 @@ const OffiAccountEditModal = ({
<Box color="myGray.600">{t('publish:official_account.params')}</Box>
{feConfigs?.docUrl && (
<Link
href={feConfigs.openAPIDocUrl || getDocPath('/docs/course/official_account')}
href={
feConfigs.openAPIDocUrl ||
getDocPath('/docs/use-cases/external-integration/official_account/')
}
target={'_blank'}
ml={2}
color={'primary.500'}

View File

@@ -75,7 +75,10 @@ const OffiAccount = ({ appId }: { appId: string }) => {
{feConfigs?.docUrl && (
<Link
href={feConfigs.openAPIDocUrl || getDocPath('/docs/course/official_account')}
href={
feConfigs.openAPIDocUrl ||
getDocPath('/docs/use-cases/external-integration/official_account/')
}
target={'_blank'}
ml={2}
color={'primary.500'}
@@ -150,7 +153,7 @@ const OffiAccount = ({ appId }: { appId: string }) => {
)}
<Td>
{item.lastTime
? t(formatTimeToChatTime(item.lastTime) as any)
? t(formatTimeToChatTime(item.lastTime) as any).replace('#', ':')
: t('common:common.Un used')}
</Td>
<Td display={'flex'} alignItems={'center'}>

View File

@@ -126,7 +126,7 @@ const Wecom = ({ appId }: { appId: string }) => {
)}
<Td>
{item.lastTime
? t(formatTimeToChatTime(item.lastTime) as any)
? t(formatTimeToChatTime(item.lastTime) as any).replace('#', ':')
: t('common:common.Un used')}
</Td>
<Td display={'flex'} alignItems={'center'}>

View File

@@ -3,6 +3,7 @@ import { Box, Image, Flex, ModalBody } from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useTranslation } from 'next-i18next';
import MyImage from '@fastgpt/web/components/common/Image/MyImage';
export type ShowShareLinkModalProps = {
shareLink: string;
@@ -40,7 +41,7 @@ function ShowShareLinkModal({ shareLink, onClose, img }: ShowShareLinkModalProps
</Box>
</Box>
<Box mt="4" borderRadius="0.5rem" border="1px" borderStyle="solid" borderColor="myGray.200">
<Image src={img} borderRadius="0.5rem" alt="" />
<MyImage src={img} borderRadius="0.5rem" alt="" />
</Box>
</ModalBody>
</MyModal>

View File

@@ -51,6 +51,7 @@ const RouteTab = () => {
px={2}
py={0.5}
fontWeight={'medium'}
borderRadius={'sm'}
{...(currentTab === tab.id
? {
color: 'primary.700'
@@ -59,8 +60,7 @@ const RouteTab = () => {
color: 'myGray.600',
cursor: 'pointer',
_hover: {
bg: 'myGray.200',
borderRadius: 'md'
bg: 'myGray.200'
},
onClick: () => setCurrentTab(tab.id)
})}

View File

@@ -84,7 +84,7 @@ const AppCard = () => {
>
{appDetail.intro || t('common:core.app.tip.Add a intro to app')}
</Box>
<HStack alignItems={'flex-end'}>
<HStack alignItems={'center'}>
<Button
size={['sm', 'md']}
variant={'whitePrimary'}
@@ -107,7 +107,7 @@ const AppCard = () => {
<MyMenu
Button={
<IconButton
variant={'whiteBase'}
variant={'whitePrimary'}
size={['smSquare', 'mdSquare']}
icon={<MyIcon name={'more'} w={'1rem'} />}
aria-label={''}

View File

@@ -43,9 +43,6 @@ const TTSSelect = dynamic(() => import('@/components/core/app/TTSSelect'));
const QGSwitch = dynamic(() => import('@/components/core/app/QGSwitch'));
const WhisperConfig = dynamic(() => import('@/components/core/app/WhisperConfig'));
const InputGuideConfig = dynamic(() => import('@/components/core/app/InputGuideConfig'));
const ScheduledTriggerConfig = dynamic(
() => import('@/components/core/app/ScheduledTriggerConfig')
);
const WelcomeTextConfig = dynamic(() => import('@/components/core/app/WelcomeTextConfig'));
const FileSelectConfig = dynamic(() => import('@/components/core/app/FileSelect'));
@@ -455,22 +452,6 @@ const EditForm = ({
}}
/>
</Box>
{/* timer trigger */}
<Box {...BoxStyles} borderBottom={'none'}>
<ScheduledTriggerConfig
value={appForm.chatConfig.scheduledTriggerConfig}
onChange={(e) => {
setAppForm((state) => ({
...state,
chatConfig: {
...state.chatConfig,
scheduledTriggerConfig: e
}
}));
}}
/>
</Box>
</Box>
{isOpenDatasetSelect && (

View File

@@ -387,6 +387,7 @@ const RenderList = React.memo(function RenderList({
isInvalid={errors && Object.keys(errors).includes(input.key)}
onChange={onChange}
input={input}
setUploading={() => {}}
/>
);
}}

View File

@@ -29,6 +29,11 @@ import { useDebounceEffect } from 'ahooks';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import SaveButton from './components/SaveButton';
import PublishHistories from '../PublishHistoriesSlider';
import {
WorkflowNodeEdgeContext,
WorkflowInitContext
} from '../WorkflowComponents/context/workflowInitContext';
import { WorkflowEventContext } from '../WorkflowComponents/context/workflowEventContext';
const Header = () => {
const { t } = useTranslation();
@@ -48,20 +53,23 @@ const Header = () => {
onClose: onCloseBackConfirm
} = useDisclosure();
const nodes = useContextSelector(WorkflowInitContext, (v) => v.nodes);
const edges = useContextSelector(WorkflowNodeEdgeContext, (v) => v.edges);
const {
flowData2StoreData,
flowData2StoreDataAndCheck,
setWorkflowTestData,
setShowHistoryModal,
showHistoryModal,
nodes,
edges,
past,
future,
setPast,
onSwitchTmpVersion,
onSwitchCloudVersion
} = useContextSelector(WorkflowContext, (v) => v);
const showHistoryModal = useContextSelector(WorkflowEventContext, (v) => v.showHistoryModal);
const setShowHistoryModal = useContextSelector(
WorkflowEventContext,
(v) => v.setShowHistoryModal
);
const { lastAppListRouteType } = useSystemStore();
@@ -70,7 +78,7 @@ const Header = () => {
useDebounceEffect(
() => {
const savedSnapshot =
future.findLast((snapshot) => snapshot.isSaved) ||
[...future].reverse().find((snapshot) => snapshot.isSaved) ||
past.find((snapshot) => snapshot.isSaved);
const val = compareSnapshot(
@@ -151,7 +159,6 @@ const Header = () => {
)}
<Flex
mt={[2, 0]}
py={3}
pl={[2, 4]}
pr={[2, 6]}
borderBottom={'base'}
@@ -169,12 +176,20 @@ const Header = () => {
})}
>
{/* back */}
<MyIcon
name={'common/leftArrowLight'}
w={'1.75rem'}
cursor={'pointer'}
onClick={isPublished ? onBack : onOpenBackConfirm}
/>
<Box
_hover={{
bg: 'myGray.200'
}}
p={0.5}
borderRadius={'sm'}
>
<MyIcon
name={'common/leftArrowLight'}
w={6}
cursor={'pointer'}
onClick={isPublished ? onBack : onOpenBackConfirm}
/>
</Box>
{/* app info */}
<Box ml={1}>

View File

@@ -11,15 +11,18 @@ import { Flex } from '@chakra-ui/react';
import { workflowBoxStyles } from '../constants';
import dynamic from 'next/dynamic';
import { cloneDeep } from 'lodash';
import { useTranslation } from 'next-i18next';
import Flow from '../WorkflowComponents/Flow';
import { t } from 'i18next';
import { ReactFlowCustomProvider } from '../WorkflowComponents/context/index';
const Logs = dynamic(() => import('../Logs/index'));
const PublishChannel = dynamic(() => import('../Publish'));
const WorkflowEdit = () => {
const { appDetail, currentTab } = useContextSelector(AppContext, (e) => e);
const isV2Workflow = appDetail?.version === 'v2';
const { t } = useTranslation();
const { openConfirm, ConfirmModal } = useConfirm({
showCancel: false,
@@ -64,9 +67,9 @@ const WorkflowEdit = () => {
const Render = () => {
return (
<WorkflowContextProvider basicNodeTemplates={appSystemModuleTemplates}>
<ReactFlowCustomProvider templates={appSystemModuleTemplates}>
<WorkflowEdit />
</WorkflowContextProvider>
</ReactFlowCustomProvider>
);
};

View File

@@ -4,18 +4,18 @@ import { useContextSelector } from 'use-context-selector';
import { AppContext, TabEnum } from '../context';
import { useTranslation } from 'next-i18next';
import Avatar from '@fastgpt/web/components/common/Avatar';
import MyMenu from '@fastgpt/web/components/common/MyMenu';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { WorkflowContext } from './context';
import { filterSensitiveNodesData } from '@/web/core/workflow/utils';
import dynamic from 'next/dynamic';
import { useCopyData } from '@/web/common/hooks/useCopyData';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import MyTag from '@fastgpt/web/components/common/Tag/index';
import { publishStatusStyle } from '../constants';
import MyPopover from '@fastgpt/web/components/common/MyPopover';
import { fileDownload } from '@/web/common/file/utils';
import { AppChatConfigType } from '@fastgpt/global/core/app/type';
import MyBox from '@fastgpt/web/components/common/MyBox';
import { useSystemStore } from '@/web/common/system/useSystemStore';
const ImportSettings = dynamic(() => import('./Flow/ImportSettings'));
@@ -31,83 +31,115 @@ const AppCard = ({
const { appDetail, onOpenInfoEdit, onOpenTeamTagModal, onDelApp, currentTab } =
useContextSelector(AppContext, (v) => v);
const { showHistoryModal } = useContextSelector(WorkflowContext, (v) => v);
const { isOpen: isOpenImport, onOpen: onOpenImport, onClose: onCloseImport } = useDisclosure();
const InfoMenu = useCallback(
({ children }: { children: React.ReactNode }) => {
return (
<MyMenu
width={150}
Button={children}
menuList={[
{
children: [
{
icon: 'edit',
label: t('app:edit_info'),
onClick: onOpenInfoEdit
},
{
icon: 'support/team/key',
label: t('common:common.Role'),
onClick: onOpenInfoEdit
}
]
},
...(!showHistoryModal && currentTab === TabEnum.appEdit
? [
{
children: [
{
label: t('app:import_configs'),
icon: 'common/importLight',
onClick: onOpenImport
},
{
label: ExportPopover({
chatConfig: appDetail.chatConfig,
appName: appDetail.name
}),
menuItemStyles: {
p: 0,
cursor: 'default'
}
}
]
}
]
: []),
...(appDetail.permission.hasWritePer && feConfigs?.show_team_chat
? [
{
children: [
{
icon: 'support/team/memberLight',
label: t('common:common.Team Tags Set'),
onClick: onOpenTeamTagModal
}
]
}
]
: []),
...(appDetail.permission.isOwner
? [
{
children: [
{
type: 'danger' as 'danger',
icon: 'delete',
label: t('common:common.Delete'),
onClick: onDelApp
}
]
}
]
: [])
]}
/>
<MyPopover
placement={'bottom-end'}
hasArrow={false}
offset={[2, 4]}
w={'116px'}
trigger={'hover'}
Trigger={children}
>
{({ onClose }) => (
<Box p={1.5}>
<MyBox
display={'flex'}
size={'md'}
px={1}
py={1.5}
rounded={'4px'}
_hover={{ color: 'primary.600', bg: 'rgba(17, 24, 36, 0.05)' }}
cursor={'pointer'}
onClick={onOpenInfoEdit}
>
<MyIcon name={'edit'} w={'16px'} mr={2} />
<Box fontSize={'sm'}>{t('app:edit_info')}</Box>
</MyBox>
<MyBox
display={'flex'}
size={'md'}
px={1}
py={1.5}
rounded={'4px'}
_hover={{ color: 'primary.600', bg: 'rgba(17, 24, 36, 0.05)' }}
cursor={'pointer'}
onClick={onOpenInfoEdit}
>
<MyIcon name={'support/team/key'} w={'16px'} mr={2} />
<Box fontSize={'sm'}>{t('app:Role_setting')}</Box>
</MyBox>
<Box w={'full'} h={'1px'} bg={'myGray.200'} my={1} />
<MyBox
display={'flex'}
size={'md'}
px={1}
py={1.5}
rounded={'4px'}
_hover={{ color: 'primary.600', bg: 'rgba(17, 24, 36, 0.05)' }}
cursor={'pointer'}
onClick={onOpenImport}
>
<MyIcon name={'common/importLight'} w={'16px'} mr={2} />
<Box fontSize={'sm'}>{t('app:import_configs')}</Box>
</MyBox>
<MyBox
display={'flex'}
size={'md'}
px={1}
py={1.5}
rounded={'4px'}
_hover={{ color: 'primary.600', bg: 'rgba(17, 24, 36, 0.05)' }}
cursor={'pointer'}
>
{ExportPopover({
chatConfig: appDetail.chatConfig,
appName: appDetail.name
})}
</MyBox>
<Box w={'full'} h={'1px'} bg={'myGray.200'} my={1} />
{appDetail.permission.hasWritePer && feConfigs?.show_team_chat && (
<>
<MyBox
display={'flex'}
size={'md'}
px={1}
py={1.5}
rounded={'4px'}
_hover={{ color: 'primary.600', bg: 'rgba(17, 24, 36, 0.05)' }}
cursor={'pointer'}
onClick={onOpenTeamTagModal}
>
<MyIcon name={'core/dataset/tag'} w={'16px'} mr={2} />
<Box fontSize={'sm'}>{t('app:Team_Tags')}</Box>
</MyBox>
<Box w={'full'} h={'1px'} bg={'myGray.200'} my={1} />
</>
)}
{appDetail.permission.isOwner && (
<MyBox
display={'flex'}
size={'md'}
px={1}
py={1.5}
rounded={'4px'}
color={'red.600'}
_hover={{ bg: 'rgba(17, 24, 36, 0.05)' }}
cursor={'pointer'}
onClick={onDelApp}
>
<MyIcon name={'delete'} w={'16px'} mr={2} />
<Box fontSize={'sm'}>{t('common:common.Delete')}</Box>
</MyBox>
)}
</Box>
)}
</MyPopover>
);
},
[
@@ -117,7 +149,6 @@ const AppCard = ({
appDetail.permission.isOwner,
currentTab,
feConfigs?.show_team_chat,
showHistoryModal,
onDelApp,
onOpenImport,
onOpenInfoEdit,
@@ -129,21 +160,26 @@ const AppCard = ({
const Render = useMemo(() => {
return (
<HStack>
<InfoMenu>
<Avatar src={appDetail.avatar} w={'1.75rem'} borderRadius={'md'} />
</InfoMenu>
<Avatar src={appDetail.avatar} w={'1.75rem'} borderRadius={'md'} />
<Box>
<InfoMenu>
<HStack spacing={1} cursor={'pointer'}>
<HStack
spacing={1}
cursor={'pointer'}
pl={1}
ml={-1}
borderRadius={'xs'}
_hover={{ bg: 'myGray.150' }}
>
<Box color={'myGray.900'}>{appDetail.name}</Box>
<MyIcon name={'common/select'} w={'1rem'} />
<MyIcon name={'common/select'} w={'1rem'} color={'myGray.500'} />
</HStack>
</InfoMenu>
{showSaveStatus && (
<Flex alignItems={'center'} h={'20px'} fontSize={'mini'} lineHeight={1}>
<Flex alignItems={'center'} fontSize={'mini'} lineHeight={1}>
<MyTag
py={0}
px={0}
px={1}
showDot
bg={'transparent'}
colorSchema={
@@ -211,15 +247,19 @@ function ExportPopover({
return (
<MyPopover
placement={'right-start'}
offset={[0, 0]}
offset={[0, 20]}
hasArrow
trigger={'hover'}
w={'8.6rem'}
Trigger={
<Flex align={'center'} w={'100%'} py={2} px={3}>
<Avatar src={'export'} borderRadius={'sm'} w={'1rem'} mr={3} />
{t('app:export_configs')}
</Flex>
// <Flex align={'center'} w={'100%'} py={2} px={3}>
// <Avatar src={'export'} borderRadius={'sm'} w={'1rem'} mr={3} />
// {t('app:export_configs')}
// </Flex>
<MyBox display={'flex'} size={'md'} rounded={'4px'} cursor={'pointer'}>
<MyIcon name={'export'} w={'16px'} mr={2} />
<Box fontSize={'sm'}>{t('app:export_configs')}</Box>
</MyBox>
}
>
{({ onClose }) => (

View File

@@ -50,6 +50,7 @@ import { useUserStore } from '@/web/support/user/useUserStore';
import { LoopStartNode } from '@fastgpt/global/core/workflow/template/system/loop/loopStart';
import { LoopEndNode } from '@fastgpt/global/core/workflow/template/system/loop/loopEnd';
import { NodeInputKeyEnum, NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { WorkflowNodeEdgeContext } from '../context/workflowInitContext';
type ModuleTemplateListProps = {
isOpen: boolean;
@@ -79,10 +80,10 @@ const NodeTemplatesModal = ({ isOpen, onClose }: ModuleTemplateListProps) => {
const [parentId, setParentId] = useState<ParentIdType>('');
const [searchKey, setSearchKey] = useState('');
const { feConfigs } = useSystemStore();
const { basicNodeTemplates, hasToolNode, nodeList, appId } = useContextSelector(
WorkflowContext,
(v) => v
);
const basicNodeTemplates = useContextSelector(WorkflowContext, (v) => v.basicNodeTemplates);
const hasToolNode = useContextSelector(WorkflowContext, (v) => v.hasToolNode);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const appId = useContextSelector(WorkflowContext, (v) => v.appId);
const { data: members = [] } = useRequest2(loadAndGetTeamMembers, {
manual: !feConfigs.isPlus
@@ -217,105 +218,120 @@ const NodeTemplatesModal = ({ isOpen, onClose }: ModuleTemplateListProps) => {
}
);
const Render = useMemo(() => {
return (
<>
<Box
zIndex={2}
display={isOpen ? 'block' : 'none'}
position={'absolute'}
top={0}
left={0}
bottom={0}
w={`${sliderWidth}px`}
maxW={'100%'}
onClick={onClose}
fontSize={'sm'}
/>
<MyBox
isLoading={isLoading}
display={'flex'}
zIndex={3}
flexDirection={'column'}
position={'absolute'}
top={'10px'}
left={0}
pt={'20px'}
pb={4}
h={isOpen ? 'calc(100% - 20px)' : '0'}
w={isOpen ? ['100%', `${sliderWidth}px`] : '0'}
bg={'white'}
boxShadow={'3px 0 20px rgba(0,0,0,0.2)'}
borderRadius={'0 20px 20px 0'}
transition={'.2s ease'}
userSelect={'none'}
overflow={isOpen ? 'none' : 'hidden'}
>
{/* Header */}
<Box px={'5'} mb={3} whiteSpace={'nowrap'} overflow={'hidden'}>
{/* Tabs */}
<Flex flex={'1 0 0'} alignItems={'center'} gap={3}>
<Box flex={'1 0 0'}>
<FillRowTabs
list={[
{
icon: 'core/modules/basicNode',
label: t('common:core.module.template.Basic Node'),
value: TemplateTypeEnum.basic
},
{
icon: 'core/modules/systemPlugin',
label: t('common:core.module.template.System Plugin'),
value: TemplateTypeEnum.systemPlugin
},
{
icon: 'core/modules/teamPlugin',
label: t('common:core.module.template.Team app'),
value: TemplateTypeEnum.teamPlugin
}
]}
width={'100%'}
py={'5px'}
value={templateType}
onChange={(e) => {
loadNodeTemplates({
type: e as TemplateTypeEnum,
parentId: ''
});
}}
/>
</Box>
{/* close icon */}
<IconButton
size={'sm'}
icon={<MyIcon name={'common/backFill'} w={'14px'} color={'myGray.700'} />}
borderColor={'myGray.300'}
variant={'grayBase'}
aria-label={''}
onClick={onClose}
return (
<>
<Box
zIndex={2}
display={isOpen ? 'block' : 'none'}
position={'absolute'}
top={0}
left={0}
bottom={0}
w={`${sliderWidth}px`}
maxW={'100%'}
onClick={onClose}
fontSize={'sm'}
/>
<MyBox
isLoading={isLoading}
display={'flex'}
zIndex={3}
flexDirection={'column'}
position={'absolute'}
top={'10px'}
left={0}
pt={'20px'}
pb={4}
h={isOpen ? 'calc(100% - 20px)' : '0'}
w={isOpen ? ['100%', `${sliderWidth}px`] : '0'}
bg={'white'}
boxShadow={'3px 0 20px rgba(0,0,0,0.2)'}
borderRadius={'0 20px 20px 0'}
transition={'.2s ease'}
userSelect={'none'}
overflow={isOpen ? 'none' : 'hidden'}
>
{/* Header */}
<Box px={'5'} mb={3} whiteSpace={'nowrap'} overflow={'hidden'}>
{/* Tabs */}
<Flex flex={'1 0 0'} alignItems={'center'} gap={3}>
<Box flex={'1 0 0'}>
<FillRowTabs
list={[
{
icon: 'core/modules/basicNode',
label: t('common:core.module.template.Basic Node'),
value: TemplateTypeEnum.basic
},
{
icon: 'core/modules/systemPlugin',
label: t('common:core.module.template.System Plugin'),
value: TemplateTypeEnum.systemPlugin
},
{
icon: 'core/modules/teamPlugin',
label: t('common:core.module.template.Team app'),
value: TemplateTypeEnum.teamPlugin
}
]}
width={'100%'}
py={'5px'}
value={templateType}
onChange={(e) => {
loadNodeTemplates({
type: e as TemplateTypeEnum,
parentId: ''
});
}}
/>
</Flex>
{/* Search */}
{(templateType === TemplateTypeEnum.teamPlugin ||
templateType === TemplateTypeEnum.systemPlugin) && (
<Flex mt={2} alignItems={'center'} h={10}>
<InputGroup mr={4} h={'full'}>
<InputLeftElement h={'full'} alignItems={'center'} display={'flex'}>
<MyIcon name={'common/searchLight'} w={'16px'} color={'myGray.500'} ml={3} />
</InputLeftElement>
<Input
h={'full'}
bg={'myGray.50'}
placeholder={
templateType === TemplateTypeEnum.teamPlugin
? t('common:plugin.Search_app')
: t('common:plugin.Search plugin')
}
onChange={(e) => setSearchKey(e.target.value)}
/>
</InputGroup>
<Box flex={1} />
{templateType === TemplateTypeEnum.teamPlugin && (
</Box>
{/* close icon */}
<IconButton
size={'sm'}
icon={<MyIcon name={'common/backFill'} w={'14px'} color={'myGray.700'} />}
borderColor={'myGray.300'}
variant={'grayBase'}
aria-label={''}
onClick={onClose}
/>
</Flex>
{/* Search */}
{(templateType === TemplateTypeEnum.teamPlugin ||
templateType === TemplateTypeEnum.systemPlugin) && (
<Flex mt={2} alignItems={'center'} h={10}>
<InputGroup mr={4} h={'full'}>
<InputLeftElement h={'full'} alignItems={'center'} display={'flex'}>
<MyIcon name={'common/searchLight'} w={'16px'} color={'myGray.500'} ml={3} />
</InputLeftElement>
<Input
h={'full'}
bg={'myGray.50'}
placeholder={
templateType === TemplateTypeEnum.teamPlugin
? t('common:plugin.Search_app')
: t('common:plugin.Search plugin')
}
onChange={(e) => setSearchKey(e.target.value)}
/>
</InputGroup>
<Box flex={1} />
{templateType === TemplateTypeEnum.teamPlugin && (
<Flex
alignItems={'center'}
cursor={'pointer'}
_hover={{
color: 'primary.600'
}}
fontSize={'sm'}
onClick={() => router.push('/app/list')}
gap={1}
>
<Box>{t('common:create')}</Box>
<MyIcon name={'common/rightArrowLight'} w={'0.8rem'} />
</Flex>
)}
{templateType === TemplateTypeEnum.systemPlugin &&
feConfigs.systemPluginCourseUrl && (
<Flex
alignItems={'center'}
cursor={'pointer'}
@@ -323,68 +339,35 @@ const NodeTemplatesModal = ({ isOpen, onClose }: ModuleTemplateListProps) => {
color: 'primary.600'
}}
fontSize={'sm'}
onClick={() => router.push('/app/list')}
onClick={() => window.open(feConfigs.systemPluginCourseUrl)}
gap={1}
>
<Box>{t('common:create')}</Box>
<Box>{t('common:plugin.contribute')}</Box>
<MyIcon name={'common/rightArrowLight'} w={'0.8rem'} />
</Flex>
)}
{templateType === TemplateTypeEnum.systemPlugin &&
feConfigs.systemPluginCourseUrl && (
<Flex
alignItems={'center'}
cursor={'pointer'}
_hover={{
color: 'primary.600'
}}
fontSize={'sm'}
onClick={() => window.open(feConfigs.systemPluginCourseUrl)}
gap={1}
>
<Box>{t('common:plugin.contribute')}</Box>
<MyIcon name={'common/rightArrowLight'} w={'0.8rem'} />
</Flex>
)}
</Flex>
)}
{/* paths */}
{(templateType === TemplateTypeEnum.teamPlugin ||
templateType === TemplateTypeEnum.systemPlugin) &&
!searchKey &&
parentId && (
<Flex alignItems={'center'} mt={2}>
<FolderPath paths={paths} FirstPathDom={null} onClick={onUpdateParentId} />
</Flex>
)}
{/* paths */}
{(templateType === TemplateTypeEnum.teamPlugin ||
templateType === TemplateTypeEnum.systemPlugin) &&
!searchKey &&
parentId && (
<Flex alignItems={'center'} mt={2}>
<FolderPath paths={paths} FirstPathDom={null} onClick={onUpdateParentId} />
</Flex>
)}
</Box>
<RenderList
templates={templates}
type={templateType}
onClose={onClose}
parentId={parentId}
setParentId={onUpdateParentId}
/>
</MyBox>
</>
);
}, [
isOpen,
onClose,
isLoading,
t,
templateType,
feConfigs.systemPluginCourseUrl,
searchKey,
parentId,
paths,
onUpdateParentId,
templates,
loadNodeTemplates,
router
]);
return Render;
</Box>
<RenderList
templates={templates}
type={templateType}
onClose={onClose}
parentId={parentId}
setParentId={onUpdateParentId}
/>
</MyBox>
</>
);
};
export default React.memo(NodeTemplatesModal);
@@ -403,9 +386,11 @@ const RenderList = React.memo(function RenderList({
const isSystemPlugin = type === TemplateTypeEnum.systemPlugin;
const { screenToFlowPosition } = useReactFlow();
const { toast } = useToast();
const { reactFlowWrapper, setNodes, nodeList } = useContextSelector(WorkflowContext, (v) => v);
const { computedNewNodeName } = useWorkflowUtils();
const { toast } = useToast();
const setNodes = useContextSelector(WorkflowNodeEdgeContext, (v) => v.setNodes);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const formatTemplates = useMemo<NodeTemplateListType>(() => {
const copy: NodeTemplateListType = cloneDeep(workflowNodeTemplateList);
@@ -426,8 +411,6 @@ const RenderList = React.memo(function RenderList({
template: NodeTemplateListItemType;
position: XYPosition;
}) => {
if (!reactFlowWrapper?.current) return;
// Load template node
const templateNode = await (async () => {
try {
@@ -465,7 +448,8 @@ const RenderList = React.memo(function RenderList({
// Add default values to some inputs
const defaultValueMap: Record<string, any> = {
[NodeInputKeyEnum.userChatInput]: undefined
[NodeInputKeyEnum.userChatInput]: undefined,
[NodeInputKeyEnum.fileUrlList]: undefined
};
nodeList.forEach((node) => {
if (node.flowNodeType === FlowNodeTypeEnum.workflowStart) {
@@ -473,6 +457,9 @@ const RenderList = React.memo(function RenderList({
node.nodeId,
NodeOutputKeyEnum.userChatInput
];
defaultValueMap[NodeInputKeyEnum.fileUrlList] = [
[node.nodeId, NodeOutputKeyEnum.userFiles]
];
}
});
@@ -535,16 +522,7 @@ const RenderList = React.memo(function RenderList({
return newState;
});
},
[
reactFlowWrapper,
screenToFlowPosition,
nodeList,
computedNewNodeName,
t,
setNodes,
setLoading,
toast
]
[screenToFlowPosition, nodeList, computedNewNodeName, t, setNodes, setLoading, toast]
);
const gridStyle = useMemo(() => {
@@ -567,133 +545,118 @@ const RenderList = React.memo(function RenderList({
};
}, [type]);
const Render = useMemo(() => {
return templates.length === 0 ? (
<EmptyTip text={t('app:module.No Modules')} />
) : (
<Box flex={'1 0 0'} overflow={'overlay'} px={'5'}>
<Box mx={'auto'}>
{formatTemplates.map((item, i) => (
<Box
key={item.type}
css={css({
span: {
display: 'block'
}
})}
_notLast={{ mb: 5 }}
>
{item.label && formatTemplates.length > 1 && (
<Flex>
<Box fontSize={'sm'} mb={3} fontWeight={'500'} flex={1} color={'myGray.900'}>
{t(item.label as any)}
</Box>
</Flex>
)}
return templates.length === 0 ? (
<EmptyTip text={t('app:module.No Modules')} />
) : (
<Box flex={'1 0 0'} overflow={'overlay'} px={'5'}>
<Box mx={'auto'}>
{formatTemplates.map((item, i) => (
<Box
key={item.type}
css={css({
span: {
display: 'block'
}
})}
_notLast={{ mb: 5 }}
>
{item.label && formatTemplates.length > 1 && (
<Flex>
<Box fontSize={'sm'} mb={3} fontWeight={'500'} flex={1} color={'myGray.900'}>
{t(item.label as any)}
</Box>
</Flex>
)}
<Grid gridTemplateColumns={gridStyle.gridTemplateColumns} rowGap={2}>
{item.list.map((template) => (
<MyTooltip
key={template.id}
placement={'right'}
label={
<Box py={2}>
<Flex alignItems={'center'}>
<Avatar
src={template.avatar}
w={'1.75rem'}
objectFit={'contain'}
borderRadius={'sm'}
/>
<Box fontWeight={'bold'} ml={3} color={'myGray.900'}>
{t(template.name as any)}
</Box>
</Flex>
<Box mt={2} color={'myGray.500'}>
{t(template.intro as any) || t('common:core.workflow.Not intro')}
</Box>
{isSystemPlugin && <CostTooltip cost={template.currentCost} />}
</Box>
}
>
<Flex
alignItems={'center'}
py={gridStyle.py}
px={3}
cursor={'pointer'}
_hover={{ bg: 'myWhite.600' }}
borderRadius={'sm'}
draggable={!template.isFolder}
onDragEnd={(e) => {
if (e.clientX < sliderWidth) return;
onAddNode({
template,
position: { x: e.clientX, y: e.clientY }
});
}}
onClick={(e) => {
if (template.isFolder) {
return setParentId(template.id);
}
if (isPc) {
return onAddNode({
template,
position: { x: sliderWidth * 1.5, y: 200 }
});
}
onAddNode({
template,
position: { x: e.clientX, y: e.clientY }
});
onClose();
}}
>
<Avatar
src={template.avatar}
w={gridStyle.avatarSize}
objectFit={'contain'}
borderRadius={'sm'}
/>
<Box ml={3} flex={'1'}>
<Box color={'myGray.900'} fontWeight={'500'} fontSize={'sm'} flex={'1 0 0'}>
<Grid gridTemplateColumns={gridStyle.gridTemplateColumns} rowGap={2}>
{item.list.map((template) => (
<MyTooltip
key={template.id}
placement={'right'}
label={
<Box py={2}>
<Flex alignItems={'center'}>
<Avatar
src={template.avatar}
w={'1.75rem'}
objectFit={'contain'}
borderRadius={'sm'}
/>
<Box fontWeight={'bold'} ml={3} color={'myGray.900'}>
{t(template.name as any)}
</Box>
{gridStyle.authorInName && template.author !== undefined && (
<Box fontSize={'xs'} mt={0.5} color={'myGray.500'}>
{`by ${template.author || feConfigs.systemTitle}`}
</Box>
)}
</Flex>
<Box mt={2} color={'myGray.500'}>
{t(template.intro as any) || t('common:core.workflow.Not intro')}
</Box>
{gridStyle.authorInRight && template.authorAvatar && template.author && (
<HStack spacing={1} maxW={'120px'}>
<Avatar src={template.authorAvatar} w={'1rem'} borderRadius={'50%'} />
<Box fontSize={'xs'} className="textEllipsis">
{template.author}
</Box>
</HStack>
{isSystemPlugin && <CostTooltip cost={template.currentCost} />}
</Box>
}
>
<Flex
alignItems={'center'}
py={gridStyle.py}
px={3}
cursor={'pointer'}
_hover={{ bg: 'myWhite.600' }}
borderRadius={'sm'}
draggable={!template.isFolder}
onDragEnd={(e) => {
if (e.clientX < sliderWidth) return;
onAddNode({
template,
position: { x: e.clientX, y: e.clientY }
});
}}
onClick={(e) => {
if (template.isFolder) {
return setParentId(template.id);
}
if (isPc) {
return onAddNode({
template,
position: { x: sliderWidth * 1.5, y: 200 }
});
}
onAddNode({
template,
position: { x: e.clientX, y: e.clientY }
});
onClose();
}}
>
<Avatar
src={template.avatar}
w={gridStyle.avatarSize}
objectFit={'contain'}
borderRadius={'sm'}
/>
<Box ml={3} flex={'1'}>
<Box color={'myGray.900'} fontWeight={'500'} fontSize={'sm'} flex={'1 0 0'}>
{t(template.name as any)}
</Box>
{gridStyle.authorInName && template.author !== undefined && (
<Box fontSize={'xs'} mt={0.5} color={'myGray.500'}>
{`by ${template.author || feConfigs.systemTitle}`}
</Box>
)}
</Flex>
</MyTooltip>
))}
</Grid>
</Box>
))}
</Box>
</Box>
);
}, [
feConfigs.systemTitle,
formatTemplates,
gridStyle,
isPc,
isSystemPlugin,
onAddNode,
onClose,
setParentId,
t,
templates.length
]);
</Box>
return Render;
{gridStyle.authorInRight && template.authorAvatar && template.author && (
<HStack spacing={1} maxW={'120px'}>
<Avatar src={template.authorAvatar} w={'1rem'} borderRadius={'50%'} />
<Box fontSize={'xs'} className="textEllipsis">
{template.author}
</Box>
</HStack>
)}
</Flex>
</MyTooltip>
))}
</Grid>
</Box>
))}
</Box>
</Box>
);
});

View File

@@ -6,12 +6,15 @@ import { NodeOutputKeyEnum, RuntimeEdgeStatusEnum } from '@fastgpt/global/core/w
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../context';
import { useThrottleEffect } from 'ahooks';
import { WorkflowNodeEdgeContext, WorkflowInitContext } from '../../context/workflowInitContext';
import { WorkflowEventContext } from '../../context/workflowEventContext';
const ButtonEdge = (props: EdgeProps) => {
const { nodes, nodeList, onEdgesChange, workflowDebugData, hoverEdgeId } = useContextSelector(
WorkflowContext,
(v) => v
);
const nodes = useContextSelector(WorkflowInitContext, (v) => v.nodes);
const onEdgesChange = useContextSelector(WorkflowNodeEdgeContext, (v) => v.onEdgesChange);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const workflowDebugData = useContextSelector(WorkflowContext, (v) => v.workflowDebugData);
const hoverEdgeId = useContextSelector(WorkflowEventContext, (v) => v.hoverEdgeId);
const {
id,
@@ -31,9 +34,12 @@ const ButtonEdge = (props: EdgeProps) => {
// If parentNode is folded, the edge will not be displayed
const parentNode = useMemo(() => {
return nodeList.find(
(node) => (node.nodeId === source || node.nodeId === target) && node.parentNodeId
);
for (const node of nodeList) {
if ((node.nodeId === source || node.nodeId === target) && node.parentNodeId) {
return nodeList.find((parent) => parent.nodeId === node.parentNodeId);
}
}
return undefined;
}, [nodeList, source, target]);
const defaultZIndex = useMemo(
@@ -116,13 +122,13 @@ const ButtonEdge = (props: EdgeProps) => {
(edge) => edge.sourceHandle === sourceHandleId && edge.targetHandle === targetHandleId
);
if (!targetEdge) {
if (highlightEdge) return '#3370ff';
if (highlightEdge) return '#487FFF';
return '#94B5FF';
}
// debug mode
const colorMap = {
[RuntimeEdgeStatusEnum.active]: '#39CC83',
[RuntimeEdgeStatusEnum.active]: '#487FFF',
[RuntimeEdgeStatusEnum.waiting]: '#5E8FFF',
[RuntimeEdgeStatusEnum.skipped]: '#8A95A7'
};
@@ -154,10 +160,10 @@ const ButtonEdge = (props: EdgeProps) => {
position={'absolute'}
transform={`translate(-55%, -50%) translate(${labelX}px,${labelY}px)`}
pointerEvents={'all'}
w={'17px'}
h={'17px'}
w={'18px'}
h={'18px'}
bg={'white'}
borderRadius={'17px'}
borderRadius={'18px'}
cursor={'pointer'}
zIndex={defaultZIndex + 1000}
onClick={() => onDelConnect(id)}

View File

@@ -6,10 +6,8 @@ const Container = ({ children, ...props }: BoxProps) => {
return (
<Flex
flexDirection={'column'}
px={4}
mx={2}
mb={2}
py={'10px'}
mx={3}
p={4}
position={'relative'}
bg={'myGray.50'}
border={'1px solid #F0F1F6'}

View File

@@ -7,76 +7,76 @@ import { CommentNode } from '@fastgpt/global/core/workflow/template/system/comme
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../context';
import { useReactFlow } from 'reactflow';
import { WorkflowNodeEdgeContext } from '../../context/workflowInitContext';
import { WorkflowEventContext } from '../../context/workflowEventContext';
type ContextMenuProps = {
top: number;
left: number;
};
const ContextMenu = ({ top, left }: ContextMenuProps) => {
const ContextMenu = () => {
const { t } = useTranslation();
const setNodes = useContextSelector(WorkflowContext, (ctx) => ctx.setNodes);
const setMenu = useContextSelector(WorkflowContext, (ctx) => ctx.setMenu);
const setNodes = useContextSelector(WorkflowNodeEdgeContext, (v) => v.setNodes);
const menu = useContextSelector(WorkflowEventContext, (v) => v.menu);
const setMenu = useContextSelector(WorkflowEventContext, (ctx) => ctx.setMenu);
const { screenToFlowPosition } = useReactFlow();
const newNode = nodeTemplate2FlowNode({
template: CommentNode,
position: screenToFlowPosition({ x: left, y: top }),
position: screenToFlowPosition({ x: menu?.left ?? 0, y: menu?.top ?? 0 }),
t
});
return (
<Box position="relative">
<Box
position="absolute"
top={`${top - 6}px`}
left={`${left + 10}px`}
width={0}
height={0}
borderLeft="6px solid transparent"
borderRight="6px solid transparent"
borderBottom="6px solid white"
zIndex={2}
filter="drop-shadow(0px -1px 2px rgba(0, 0, 0, 0.1))"
/>
<Flex
position={'absolute'}
top={top}
left={left}
bg={'white'}
w={'120px'}
height={9}
p={1}
rounded={'md'}
boxShadow={'0px 2px 4px 0px #A1A7B340'}
className="context-menu"
alignItems={'center'}
color={'myGray.600'}
cursor={'pointer'}
_hover={{
color: 'primary.500'
}}
onClick={() => {
setMenu(null);
setNodes((state) => {
const newState = state
.map((node) => ({
...node,
selected: false
}))
// @ts-ignore
.concat(newNode);
return newState;
});
}}
zIndex={1}
>
<MyIcon name="comment" w={'16px'} h={'16px'} ml={1} />
<Box fontSize={'12px'} fontWeight={'500'} ml={1.5}>
{t('workflow:context_menu.add_comment')}
</Box>
</Flex>
</Box>
!!menu && (
<Box position="relative">
<Box
position="absolute"
top={`${menu.top - 6}px`}
left={`${menu.left + 10}px`}
width={0}
height={0}
borderLeft="6px solid transparent"
borderRight="6px solid transparent"
borderBottom="6px solid white"
zIndex={2}
filter="drop-shadow(0px -1px 2px rgba(0, 0, 0, 0.1))"
/>
<Flex
position={'absolute'}
top={menu.top}
left={menu.left}
bg={'white'}
w={'120px'}
height={9}
p={1}
rounded={'md'}
boxShadow={'0px 2px 4px 0px #A1A7B340'}
className="context-menu"
alignItems={'center'}
color={'myGray.600'}
cursor={'pointer'}
_hover={{
color: 'primary.500'
}}
onClick={() => {
setMenu(null);
setNodes((state) => {
const newState = state
.map((node) => ({
...node,
selected: false
}))
// @ts-ignore
.concat(newNode);
return newState;
});
}}
zIndex={1}
>
<MyIcon name="comment" w={'16px'} h={'16px'} ml={1} />
<Box fontSize={'12px'} fontWeight={'500'} ml={1.5}>
{t('workflow:context_menu.add_comment')}
</Box>
</Flex>
</Box>
)
);
};

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useMemo } from 'react';
import React, { useCallback, useEffect, useMemo } from 'react';
import {
Background,
ControlButton,
@@ -15,8 +15,9 @@ import MyIcon from '@fastgpt/web/components/common/Icon';
import { Box } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import styles from './index.module.scss';
import { maxZoom, minZoom } from '../index';
import { maxZoom, minZoom } from '../../constants';
import { useKeyPress } from 'ahooks';
import { WorkflowEventContext } from '../../context/workflowEventContext';
const buttonStyle = {
border: 'none',
@@ -27,31 +28,41 @@ const buttonStyle = {
const FlowController = React.memo(function FlowController() {
const { fitView, zoomIn, zoomOut } = useReactFlow();
const { zoom } = useViewport();
const {
undo,
redo,
canRedo,
canUndo,
workflowControlMode,
setWorkflowControlMode,
mouseInCanvas,
nodeList
} = useContextSelector(WorkflowContext, (v) => v);
const undo = useContextSelector(WorkflowContext, (v) => v.undo);
const redo = useContextSelector(WorkflowContext, (v) => v.redo);
const canRedo = useContextSelector(WorkflowContext, (v) => v.canRedo);
const canUndo = useContextSelector(WorkflowContext, (v) => v.canUndo);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const workflowControlMode = useContextSelector(
WorkflowEventContext,
(v) => v.workflowControlMode
);
const setWorkflowControlMode = useContextSelector(
WorkflowEventContext,
(v) => v.setWorkflowControlMode
);
const mouseInCanvas = useContextSelector(WorkflowEventContext, (v) => v.mouseInCanvas);
const { t } = useTranslation();
const isMac = !window ? false : window.navigator.userAgent.toLocaleLowerCase().includes('mac');
// Controller shortcut key
useKeyPress(['ctrl.z', 'meta.z'], (e) => {
useKeyPress(['ctrl.z', 'meta.z', 'ctrl.shift.z', 'meta.shift.z', 'ctrl.y', 'meta.y'], (e) => {
e.preventDefault();
e.stopPropagation();
if (!mouseInCanvas) return;
undo();
});
useKeyPress(['ctrl.shift.z', 'meta.shift.z', 'ctrl.y', 'meta.y'], (e) => {
if (!mouseInCanvas) return;
redo();
const isRedo = (e.key.toLowerCase() === 'z' && e.shiftKey) || e.key.toLowerCase() === 'y';
if (isRedo) {
redo();
} else {
undo();
}
});
useKeyPress(['ctrl.add', 'meta.add', 'ctrl.equalsign', 'meta.equalsign'], (e) => {
e.preventDefault();
e.stopPropagation();
if (!mouseInCanvas) return;
zoomIn();
});

View File

@@ -1,10 +1,9 @@
import React from 'react';
import { Box, StackProps, HStack } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
const IOTitle = ({ text, ...props }: { text?: 'Input' | 'Output' | string } & StackProps) => {
return (
<HStack fontSize={'md'} alignItems={'center'} fontWeight={'medium'} mb={3} {...props}>
<HStack fontSize={'md'} alignItems={'center'} fontWeight={'medium'} mb={4} {...props}>
<Box w={'3px'} h={'14px'} borderRadius={'13px'} bg={'primary.600'} />
<Box color={'myGray.900'}>{text}</Box>
</HStack>

View File

@@ -1,7 +1,7 @@
import { storeNodes2RuntimeNodes } from '@fastgpt/global/core/workflow/runtime/utils';
import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node';
import { RuntimeEdgeItemType, StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
import { useCallback, useState, useMemo, useEffect } from 'react';
import { useCallback, useState, useMemo } from 'react';
import { checkWorkflowNodeAndConnection } from '@/web/core/workflow/utils';
import { useTranslation } from 'next-i18next';
import { useToast } from '@fastgpt/web/hooks/useToast';
@@ -27,13 +27,14 @@ import {
} from '@fastgpt/global/core/workflow/constants';
import { checkInputIsReference } from '@fastgpt/global/core/workflow/utils';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext, getWorkflowStore } from '../../context';
import { WorkflowContext } from '../../context';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { AppContext } from '../../../context';
import { VariableInputItem } from '@/components/core/chat/ChatContainer/ChatBox/components/VariableInput';
import LightRowTabs from '@fastgpt/web/components/common/Tabs/LightRowTabs';
import MyTextarea from '@/components/common/Textarea/MyTextarea';
import { WorkflowNodeEdgeContext } from '../../context/workflowInitContext';
const MyRightDrawer = dynamic(
() => import('@fastgpt/web/components/common/MyDrawer/MyRightDrawer')
@@ -49,9 +50,10 @@ export const useDebug = () => {
const { t } = useTranslation();
const { toast } = useToast();
const setNodes = useContextSelector(WorkflowContext, (v) => v.setNodes);
const setNodes = useContextSelector(WorkflowNodeEdgeContext, (v) => v.setNodes);
const getNodes = useContextSelector(WorkflowNodeEdgeContext, (v) => v.getNodes);
const edges = useContextSelector(WorkflowNodeEdgeContext, (v) => v.edges);
const onUpdateNodeError = useContextSelector(WorkflowContext, (v) => v.onUpdateNodeError);
const edges = useContextSelector(WorkflowContext, (v) => v.edges);
const onStartNodeDebug = useContextSelector(WorkflowContext, (v) => v.onStartNodeDebug);
const appDetail = useContextSelector(AppContext, (v) => v.appDetail);
@@ -76,7 +78,7 @@ export const useDebug = () => {
const [runtimeEdges, setRuntimeEdges] = useState<RuntimeEdgeItemType[]>();
const flowData2StoreDataAndCheck = useCallback(async () => {
const { nodes } = await getWorkflowStore();
const nodes = getNodes();
const checkResults = checkWorkflowNodeAndConnection({ nodes, edges });
if (!checkResults) {

View File

@@ -5,14 +5,18 @@ import { useTranslation } from 'next-i18next';
import { Node, useKeyPress } from 'reactflow';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext, getWorkflowStore } from '../../context';
import { useWorkflowUtils } from './useUtils';
import { useKeyPress as useKeyPressEffect } from 'ahooks';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { WorkflowNodeEdgeContext } from '../../context/workflowInitContext';
import { WorkflowEventContext } from '../../context/workflowEventContext';
export const useKeyboard = () => {
const { t } = useTranslation();
const { setNodes, mouseInCanvas } = useContextSelector(WorkflowContext, (v) => v);
const getNodes = useContextSelector(WorkflowNodeEdgeContext, (v) => v.getNodes);
const setNodes = useContextSelector(WorkflowNodeEdgeContext, (v) => v.setNodes);
const mouseInCanvas = useContextSelector(WorkflowEventContext, (v) => v.mouseInCanvas);
const { copyData } = useCopyData();
const { computedNewNodeName } = useWorkflowUtils();
@@ -33,14 +37,14 @@ export const useKeyboard = () => {
const onCopy = useCallback(async () => {
if (hasInputtingElement()) return;
const { nodes } = await getWorkflowStore();
const nodes = getNodes();
const selectedNodes = nodes.filter(
(node) => node.selected && !node.data?.isError && node.data?.unique !== true
);
if (selectedNodes.length === 0) return;
copyData(JSON.stringify(selectedNodes), t('common:core.workflow.Copy node'));
}, [copyData, hasInputtingElement, t]);
}, [copyData, getNodes, hasInputtingElement, t]);
const onParse = useCallback(async () => {
if (hasInputtingElement()) return;

View File

@@ -24,13 +24,17 @@ import { useKeyboard } from './useKeyboard';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../context';
import { THelperLine } from '@fastgpt/global/core/workflow/type';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { useMemoizedFn } from 'ahooks';
import { NodeInputKeyEnum, NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { useDebounceEffect, useMemoizedFn } from 'ahooks';
import {
Input_Template_Node_Height,
Input_Template_Node_Width
} from '@fastgpt/global/core/workflow/template/input';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node';
import { WorkflowNodeEdgeContext, WorkflowInitContext } from '../../context/workflowInitContext';
import { formatTime2YMDHMS } from '@fastgpt/global/common/string/time';
import { AppContext } from '../../../context';
import { WorkflowEventContext } from '../../context/workflowEventContext';
/*
Compute helper lines for snapping nodes to each other
@@ -271,26 +275,48 @@ export const useWorkflow = () => {
const { toast } = useToast();
const { t } = useTranslation();
const { isDowningCtrl } = useKeyboard();
const {
setConnectingEdge,
edges,
nodes,
nodeList,
onNodesChange,
setEdges,
onChangeNode,
onEdgesChange,
setHoverEdgeId,
setMenu
} = useContextSelector(WorkflowContext, (v) => v);
const appDetail = useContextSelector(AppContext, (e) => e.appDetail);
const nodes = useContextSelector(WorkflowInitContext, (state) => state.nodes);
const onNodesChange = useContextSelector(WorkflowNodeEdgeContext, (state) => state.onNodesChange);
const edges = useContextSelector(WorkflowNodeEdgeContext, (state) => state.edges);
const setEdges = useContextSelector(WorkflowNodeEdgeContext, (v) => v.setEdges);
const onEdgesChange = useContextSelector(WorkflowNodeEdgeContext, (v) => v.onEdgesChange);
const { setConnectingEdge, nodeList, onChangeNode, pushPastSnapshot } = useContextSelector(
WorkflowContext,
(v) => v
);
const setHoverEdgeId = useContextSelector(WorkflowEventContext, (v) => v.setHoverEdgeId);
const setMenu = useContextSelector(WorkflowEventContext, (v) => v.setMenu);
const { getIntersectingNodes } = useReactFlow();
const { isDowningCtrl } = useKeyboard();
// Loop node size and position
const resetParentNodeSizeAndPosition = useMemoizedFn((rect: Rect, parentId: string) => {
const width = rect.width + 110 > 900 ? rect.width + 110 : 900;
const height = rect.height + 380 > 900 ? rect.height + 380 : 900;
const resetParentNodeSizeAndPosition = useMemoizedFn((parentId: string) => {
const { childNodes, loopNode } = nodes.reduce(
(acc, node) => {
if (node.data.parentNodeId === parentId) {
acc.childNodes.push(node);
}
if (node.id === parentId) {
acc.loopNode = node;
}
return acc;
},
{ childNodes: [] as Node[], loopNode: undefined as Node<FlowNodeItemType> | undefined }
);
if (!loopNode) return;
const rect = getNodesBounds(childNodes);
// Calculate parent node size with minimum width/height constraints
const width = Math.max(rect.width + 80, 840);
const height = Math.max(rect.height + 80, 600);
const offsetHeight =
loopNode.data.inputs.find((input) => input.key === NodeInputKeyEnum.loopNodeInputHeight)
?.value ?? 83;
// Update parentNode size and position
onChangeNode({
@@ -311,15 +337,14 @@ export const useWorkflow = () => {
value: height
}
});
// Update parentNode position
onNodesChange([
{
id: parentId,
type: 'position',
position: {
x: rect.x - 50,
y: rect.y - 280
x: rect.x - 70,
y: rect.y - offsetHeight - 240
}
}
]);
@@ -369,15 +394,6 @@ export const useWorkflow = () => {
// Check if a node is placed on top of a loop node
const checkNodeOverLoopNode = useMemoizedFn((node: Node) => {
if (!node) return;
// 获取所有与当前节点相交的节点
const intersections = getIntersectingNodes(node);
// 获取所有与当前节点相交的节点中,类型为 loop 的节点且它不能是折叠状态
const parentNode = intersections.find(
(item) => !item.data.isFolded && item.type === FlowNodeTypeEnum.loop
);
const unSupportedTypes = [
FlowNodeTypeEnum.workflowStart,
FlowNodeTypeEnum.loop,
@@ -386,7 +402,16 @@ export const useWorkflow = () => {
FlowNodeTypeEnum.systemConfig
];
if (parentNode && !node.data.parentNodeId) {
if (!node || node.data.parentNodeId) return;
// 获取所有与当前节点相交的节点
const intersections = getIntersectingNodes(node);
// 获取所有与当前节点相交的节点中,类型为 loop 的节点且它不能是折叠状态
const parentNode = intersections.find(
(item) => !item.data.isFolded && item.type === FlowNodeTypeEnum.loop
);
if (parentNode) {
if (unSupportedTypes.includes(node.type as FlowNodeTypeEnum)) {
return toast({
status: 'warning',
@@ -404,10 +429,6 @@ export const useWorkflow = () => {
setEdges((state) =>
state.filter((edge) => edge.source !== node.id && edge.target !== node.id)
);
const childNodes = [...nodes.filter((n) => n.data.parentNodeId === parentNode.id), node];
const rect = getNodesBounds(childNodes);
resetParentNodeSizeAndPosition(rect, parentNode.id);
}
});
@@ -462,7 +483,7 @@ export const useWorkflow = () => {
const childNodes = nodes.filter((n) => n.data.parentNodeId === parentId);
checkNodeHelpLine(change, childNodes);
resetParentNodeSizeAndPosition(getNodesBounds(childNodes), parentId);
resetParentNodeSizeAndPosition(parentId);
}
// If node is parent node, move parent node and child nodes
else if (parentNode[node.data.flowNodeType]) {
@@ -591,8 +612,36 @@ export const useWorkflow = () => {
state
)
);
// Add default input
const node = nodeList.find((n) => n.nodeId === connect.target);
if (!node) return;
// 1. Add file input
if (
node.flowNodeType === FlowNodeTypeEnum.chatNode ||
node.flowNodeType === FlowNodeTypeEnum.tools ||
node.flowNodeType === FlowNodeTypeEnum.appModule
) {
const input = node.inputs.find((i) => i.key === NodeInputKeyEnum.fileUrlList);
if (input && (!input?.value || input.value.length === 0)) {
const workflowStartNode = nodeList.find(
(n) => n.flowNodeType === FlowNodeTypeEnum.workflowStart
);
if (!workflowStartNode) return;
onChangeNode({
nodeId: node.nodeId,
type: 'updateInput',
key: NodeInputKeyEnum.fileUrlList,
value: {
...input,
value: [[workflowStartNode.nodeId, NodeOutputKeyEnum.userFiles]]
}
});
}
}
},
[setEdges]
[nodeList, onChangeNode, setEdges]
);
const customOnConnect = useCallback(
(connect: Connection) => {
@@ -642,6 +691,23 @@ export const useWorkflow = () => {
setMenu(null);
}, [setMenu]);
// Watch
// Auto save snapshot
useDebounceEffect(
() => {
if (nodes.length === 0 || !appDetail.chatConfig) return;
pushPastSnapshot({
pastNodes: nodes,
pastEdges: edges,
customTitle: formatTime2YMDHMS(new Date()),
chatConfig: appDetail.chatConfig
});
},
[nodes, edges, appDetail.chatConfig],
{ wait: 500 }
);
return {
handleNodesChange,
handleEdgeChange,
@@ -655,7 +721,8 @@ export const useWorkflow = () => {
helperLineVertical,
onNodeDragStop,
onPaneContextMenu,
onPaneClick
onPaneClick,
resetParentNodeSizeAndPosition
};
};

View File

@@ -1,5 +1,5 @@
import React from 'react';
import ReactFlow, { NodeProps, ReactFlowProvider, SelectionMode } from 'reactflow';
import ReactFlow, { NodeProps, SelectionMode } from 'reactflow';
import { Box, IconButton, useDisclosure } from '@chakra-ui/react';
import { SmallCloseIcon } from '@chakra-ui/icons';
import { EDGE_TYPE, FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
@@ -11,16 +11,14 @@ import NodeTemplatesModal from './NodeTemplatesModal';
import 'reactflow/dist/style.css';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node.d';
import { connectionLineStyle, defaultEdgeOptions } from '../constants';
import { connectionLineStyle, defaultEdgeOptions, maxZoom, minZoom } from '../constants';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../context';
import { useWorkflow } from './hooks/useWorkflow';
import HelperLines from './components/HelperLines';
import FlowController from './components/FlowController';
import ContextMenu from './components/ContextMenu';
export const minZoom = 0.1;
export const maxZoom = 1.5;
import { WorkflowNodeEdgeContext, WorkflowInitContext } from '../context/workflowInitContext';
import { WorkflowEventContext } from '../context/workflowEventContext';
const NodeSimple = dynamic(() => import('./nodes/NodeSimple'));
const nodeTypes: Record<FlowNodeTypeEnum, any> = {
@@ -66,9 +64,12 @@ const edgeTypes = {
};
const Workflow = () => {
const { nodes, edges, menu, reactFlowWrapper, workflowControlMode } = useContextSelector(
WorkflowContext,
(v) => v
const nodes = useContextSelector(WorkflowInitContext, (v) => v.nodes);
const edges = useContextSelector(WorkflowNodeEdgeContext, (v) => v.edges);
const reactFlowWrapper = useContextSelector(WorkflowEventContext, (v) => v.reactFlowWrapper);
const workflowControlMode = useContextSelector(
WorkflowEventContext,
(v) => v.workflowControlMode
);
const {
@@ -125,7 +126,7 @@ const Workflow = () => {
<NodeTemplatesModal isOpen={isOpenTemplate} onClose={onCloseTemplate} />
</>
{menu && <ContextMenu {...menu} />}
<ContextMenu />
<ReactFlow
ref={reactFlowWrapper}
fitView
@@ -169,12 +170,4 @@ const Workflow = () => {
);
};
const Render = () => {
return (
<ReactFlowProvider>
<Workflow />
</ReactFlowProvider>
);
};
export default React.memo(Render);
export default React.memo(Workflow);

View File

@@ -5,7 +5,7 @@
*/
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node';
import React, { useEffect, useMemo } from 'react';
import React, { useEffect, useMemo, useRef } from 'react';
import { Background, NodeProps } from 'reactflow';
import NodeCard from '../render/NodeCard';
import Container from '../../components/Container';
@@ -15,28 +15,101 @@ import RenderInput from '../render/RenderInput';
import { Box } from '@chakra-ui/react';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import RenderOutput from '../render/RenderOutput';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { Input_Template_Children_Node_List } from '@fastgpt/global/core/workflow/template/input';
import {
ArrayTypeMap,
NodeInputKeyEnum,
VARIABLE_NODE_ID,
WorkflowIOValueTypeEnum
} from '@fastgpt/global/core/workflow/constants';
import {
Input_Template_Children_Node_List,
Input_Template_LOOP_NODE_OFFSET
} from '@fastgpt/global/core/workflow/template/input';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../../context';
import { getWorkflowGlobalVariables } from '@/web/core/workflow/utils';
import { AppContext } from '../../../../context';
import { isValidArrayReferenceValue } from '@fastgpt/global/core/workflow/utils';
import { ReferenceArrayValueType } from '@fastgpt/global/core/workflow/type/io';
import { useWorkflow } from '../../hooks/useWorkflow';
import { useSize } from 'ahooks';
const NodeLoop = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { t } = useTranslation();
const { nodeId, inputs, outputs, isFolded } = data;
const { onChangeNode, nodeList } = useContextSelector(WorkflowContext, (v) => v);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const appDetail = useContextSelector(AppContext, (v) => v.appDetail);
const { nodeWidth, nodeHeight } = useMemo(() => {
const { resetParentNodeSizeAndPosition } = useWorkflow();
const {
nodeWidth,
nodeHeight,
loopInputArray,
loopNodeInputHeight = Input_Template_LOOP_NODE_OFFSET
} = useMemo(() => {
return {
nodeWidth: inputs.find((input) => input.key === NodeInputKeyEnum.nodeWidth)?.value,
nodeHeight: inputs.find((input) => input.key === NodeInputKeyEnum.nodeHeight)?.value
nodeHeight: inputs.find((input) => input.key === NodeInputKeyEnum.nodeHeight)?.value,
loopInputArray: inputs.find((input) => input.key === NodeInputKeyEnum.loopInputArray),
loopNodeInputHeight: inputs.find(
(input) => input.key === NodeInputKeyEnum.loopNodeInputHeight
)
};
}, [inputs]);
// Update array input type
// Computed the reference value type
const newValueType = useMemo(() => {
if (!loopInputArray) return WorkflowIOValueTypeEnum.arrayAny;
const value = loopInputArray.value as ReferenceArrayValueType;
if (
!value ||
value.length === 0 ||
!isValidArrayReferenceValue(
value,
nodeList.map((node) => node.nodeId)
)
)
return WorkflowIOValueTypeEnum.arrayAny;
const globalVariables = getWorkflowGlobalVariables({
nodes: nodeList,
chatConfig: appDetail.chatConfig
});
const valueType = ((value) => {
if (value?.[0] === VARIABLE_NODE_ID) {
return globalVariables.find((item) => item.key === value[1])?.valueType;
} else {
const node = nodeList.find((node) => node.nodeId === value?.[0]);
const output = node?.outputs.find((output) => output.id === value?.[1]);
return output?.valueType;
}
})(value[0]);
return ArrayTypeMap[valueType as keyof typeof ArrayTypeMap] ?? WorkflowIOValueTypeEnum.arrayAny;
}, [appDetail.chatConfig, loopInputArray, nodeList]);
useEffect(() => {
if (!loopInputArray) return;
onChangeNode({
nodeId,
type: 'updateInput',
key: NodeInputKeyEnum.loopInputArray,
value: {
...loopInputArray,
valueType: newValueType
}
});
}, [newValueType]);
// Update childrenNodeIdList
const childrenNodeIdList = useMemo(() => {
return JSON.stringify(
nodeList.filter((node) => node.parentNodeId === nodeId).map((node) => node.nodeId)
);
}, [nodeId, nodeList]);
useEffect(() => {
onChangeNode({
nodeId,
@@ -47,31 +120,52 @@ const NodeLoop = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
value: JSON.parse(childrenNodeIdList)
}
});
resetParentNodeSizeAndPosition(nodeId);
}, [childrenNodeIdList]);
// Update loop node offset value
const inputBoxRef = useRef<HTMLDivElement>(null);
const size = useSize(inputBoxRef);
useEffect(() => {
if (!size?.height) return;
onChangeNode({
nodeId,
type: 'replaceInput',
key: NodeInputKeyEnum.loopNodeInputHeight,
value: {
...loopNodeInputHeight,
value: size.height
}
});
setTimeout(() => {
resetParentNodeSizeAndPosition(nodeId);
}, 50);
}, [size?.height]);
const Render = useMemo(() => {
return (
<NodeCard
selected={selected}
maxW="full"
{...(!isFolded && {
minW: '900px',
minH: '900px',
w: nodeWidth,
h: nodeHeight
})}
menuForbid={{ copy: true }}
{...data}
>
<NodeCard selected={selected} maxW="full" menuForbid={{ copy: true }} {...data}>
<Container position={'relative'} flex={1}>
<IOTitle text={t('common:common.Input')} />
<Box mb={6} maxW={'360'}>
<Box mb={6} maxW={'500px'} ref={inputBoxRef}>
<RenderInput nodeId={nodeId} flowInputList={inputs} />
</Box>
<FormLabel required fontWeight={'medium'} mb={3} color={'myGray.600'}>
{t('workflow:loop_body')}
</FormLabel>
<Box flex={1} position={'relative'} border={'base'} bg={'myGray.100'} rounded={'8px'}>
<Box
flex={1}
position={'relative'}
border={'base'}
bg={'myGray.100'}
rounded={'8px'}
{...(!isFolded && {
minW: nodeWidth,
minH: nodeHeight
})}
>
<Background />
</Box>
</Container>
@@ -80,7 +174,7 @@ const NodeLoop = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
</Container>
</NodeCard>
);
}, [selected, nodeWidth, nodeHeight, data, t, nodeId, inputs, outputs]);
}, [selected, isFolded, nodeWidth, nodeHeight, data, t, nodeId, inputs, outputs]);
return Render;
};

View File

@@ -80,7 +80,7 @@ const NodeLoopEnd = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
debug: true
}}
>
<Box px={4} pb={4}>
<Box px={4} pb={4} pt={2}>
{inputItem && <Reference item={inputItem} nodeId={nodeId} />}
</Box>
</NodeCard>

View File

@@ -8,24 +8,27 @@ import { WorkflowContext } from '../../../context';
import {
NodeInputKeyEnum,
NodeOutputKeyEnum,
toolValueTypeList,
WorkflowIOValueTypeEnum
} from '@fastgpt/global/core/workflow/constants';
import { Box, Flex, Table, TableContainer, Tbody, Td, Th, Thead, Tr } from '@chakra-ui/react';
import React, { useEffect, useMemo } from 'react';
import { FlowNodeOutputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import {
FlowNodeOutputTypeEnum,
FlowValueTypeMap
} from '@fastgpt/global/core/workflow/node/constant';
import MyIcon from '@fastgpt/web/components/common/Icon';
const typeMap = {
[WorkflowIOValueTypeEnum.arrayString]: WorkflowIOValueTypeEnum.string,
[WorkflowIOValueTypeEnum.arrayNumber]: WorkflowIOValueTypeEnum.number,
[WorkflowIOValueTypeEnum.arrayBoolean]: WorkflowIOValueTypeEnum.boolean,
[WorkflowIOValueTypeEnum.arrayObject]: WorkflowIOValueTypeEnum.object,
[WorkflowIOValueTypeEnum.arrayAny]: WorkflowIOValueTypeEnum.any
[WorkflowIOValueTypeEnum.arrayObject]: WorkflowIOValueTypeEnum.object
};
const NodeLoopStart = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { t } = useTranslation();
const { nodeId } = data;
const { nodeId, outputs } = data;
const { nodeList, onChangeNode } = useContextSelector(WorkflowContext, (v) => v);
const loopStartNode = useMemo(
@@ -39,12 +42,7 @@ const NodeLoopStart = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const parentArrayInput = parentNode?.inputs.find(
(input) => input.key === NodeInputKeyEnum.loopInputArray
);
return parentArrayInput?.value
? (nodeList
.find((node) => node.nodeId === parentArrayInput?.value[0])
?.outputs.find((output) => output.id === parentArrayInput?.value[1])
?.valueType as keyof typeof typeMap)
: undefined;
return typeMap[parentArrayInput?.valueType as keyof typeof typeMap];
}, [loopStartNode?.parentNodeId, nodeList]);
// Auth update loopStartInput output
@@ -71,7 +69,7 @@ const NodeLoopStart = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
key: NodeOutputKeyEnum.loopStartInput,
label: t('workflow:Array_element'),
type: FlowNodeOutputTypeEnum.static,
valueType: typeMap[loopItemInputType as keyof typeof typeMap]
valueType: loopItemInputType
}
});
}
@@ -83,7 +81,7 @@ const NodeLoopStart = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
key: NodeOutputKeyEnum.loopStartInput,
value: {
...loopArrayOutput,
valueType: typeMap[loopItemInputType as keyof typeof typeMap]
valueType: loopItemInputType
}
});
}
@@ -100,23 +98,21 @@ const NodeLoopStart = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
debug: true
}}
>
<Box px={4} w={'420px'} h={'116px'}>
{!loopItemInputType ? (
<EmptyTip text={t('workflow:loop_start_tip')} py={0} mt={4} iconSize={'32px'} />
) : (
<Box bg={'white'} borderRadius={'md'} overflow={'hidden'} border={'base'}>
<TableContainer>
<Table bg={'white'}>
<Thead>
<Tr>
<Th borderBottomLeftRadius={'none !important'}>
{t('workflow:Variable_name')}
</Th>
<Th>{t('common:core.workflow.Value type')}</Th>
</Tr>
</Thead>
<Tbody>
<Tr>
<Box px={4} pt={2} w={'420px'}>
<Box bg={'white'} borderRadius={'md'} overflow={'hidden'} border={'base'}>
<TableContainer>
<Table bg={'white'}>
<Thead>
<Tr>
<Th borderBottomLeftRadius={'none !important'}>
{t('workflow:Variable_name')}
</Th>
<Th>{t('common:core.workflow.Value type')}</Th>
</Tr>
</Thead>
<Tbody>
{outputs.map((output) => (
<Tr key={output.id}>
<Td>
<Flex alignItems={'center'}>
<MyIcon
@@ -125,20 +121,20 @@ const NodeLoopStart = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
mr={1}
color={'primary.600'}
/>
{t('workflow:Array_element')}
{t(output.label as any)}
</Flex>
</Td>
<Td>{typeMap[loopItemInputType]}</Td>
{output.valueType && <Td>{FlowValueTypeMap[output.valueType]?.label}</Td>}
</Tr>
</Tbody>
</Table>
</TableContainer>
</Box>
)}
))}
</Tbody>
</Table>
</TableContainer>
</Box>
</Box>
</NodeCard>
);
}, [data, loopItemInputType, selected, t]);
}, [data, outputs, selected, t]);
return Render;
};

View File

@@ -16,7 +16,7 @@ const NodeAnswer = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { isTool, commonInputs } = splitToolInputs(inputs, nodeId);
return (
<NodeCard minW={'400px'} selected={selected} {...data}>
<NodeCard selected={selected} {...data}>
<Container>
{isTool && (
<>

View File

@@ -99,7 +99,7 @@ const NodeCQNode = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
nodeId={nodeId}
handleId={getHandleId(nodeId, 'source', item.key)}
position={Position.Right}
translate={[26, 0]}
translate={[34, 0]}
/>
</Box>
</Box>

View File

@@ -7,14 +7,13 @@ import RenderInput from './render/RenderInput';
import { Box, Button, Flex, HStack } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import { SmallAddIcon } from '@chakra-ui/icons';
import { NodeInputKeyEnum, VARIABLE_NODE_ID } from '@fastgpt/global/core/workflow/constants';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { getOneQuoteInputTemplate } from '@fastgpt/global/core/workflow/template/system/datasetConcat';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import MySlider from '@/components/Slider';
import {
FlowNodeInputItemType,
ReferenceValueProps
ReferenceItemValueType
} from '@fastgpt/global/core/workflow/type/io.d';
import RenderOutput from './render/RenderOutput';
import IOTitle from '../components/IOTitle';
@@ -24,94 +23,13 @@ import { ReferSelector, useReference } from './render/RenderInput/templates/Refe
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import ValueTypeLabel from './render/ValueTypeLabel';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { isWorkflowStartOutput } from '@fastgpt/global/core/workflow/template/system/workflowStart';
import { getWebLLMModel } from '@/web/common/system/utils';
import { useMemoizedFn } from 'ahooks';
const NodeDatasetConcat = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { t } = useTranslation();
const { llmModelList } = useSystemStore();
const { nodeId, inputs, outputs } = data;
const { nodeList, onChangeNode } = useContextSelector(WorkflowContext, (v) => v);
const Reference = useMemoizedFn(
({ nodeId, inputChildren }: { nodeId: string; inputChildren: FlowNodeInputItemType }) => {
const { t } = useTranslation();
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const { referenceList, formatValue } = useReference({
nodeId,
valueType: inputChildren.valueType,
value: inputChildren.value
});
const onSelect = useCallback(
(e: ReferenceValueProps) => {
const workflowStartNode = nodeList.find(
(node) => node.flowNodeType === FlowNodeTypeEnum.workflowStart
);
onChangeNode({
nodeId,
type: 'replaceInput',
key: inputChildren.key,
value: {
...inputChildren,
value:
e[0] === workflowStartNode?.id && !isWorkflowStartOutput(e[1])
? [VARIABLE_NODE_ID, e[1]]
: e
}
});
},
[inputChildren, nodeId, nodeList, onChangeNode]
);
const onDel = useCallback(() => {
onChangeNode({
nodeId,
type: 'delInput',
key: inputChildren.key
});
}, [inputChildren.key, nodeId, onChangeNode]);
return (
<>
<Flex alignItems={'center'} mb={1}>
<FormLabel required={inputChildren.required}>{t(inputChildren.label as any)}</FormLabel>
{/* value */}
<ValueTypeLabel
valueType={inputChildren.valueType}
valueDesc={inputChildren.valueDesc}
/>
<MyIcon
className="delete"
name={'delete'}
w={'14px'}
color={'myGray.500'}
cursor={'pointer'}
ml={2}
_hover={{ color: 'red.600' }}
onClick={onDel}
/>
</Flex>
<ReferSelector
placeholder={t(
(inputChildren.referencePlaceholder as any) ||
t('common:core.module.Dataset quote.select')
)}
list={referenceList}
value={formatValue}
onSelect={onSelect}
/>
</>
);
}
);
const CustomComponent = useMemo(() => {
const quoteList = inputs.filter((item) => item.canEdit);
const tokenLimit = (() => {
@@ -184,7 +102,7 @@ const NodeDatasetConcat = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
<Box mt={2}>
{quoteList.map((children) => (
<Box key={children.key} _notLast={{ mb: 3 }}>
<Reference nodeId={nodeId} inputChildren={children} />
<VariableSelector nodeId={nodeId} inputChildren={children} />
</Box>
))}
</Box>
@@ -192,7 +110,7 @@ const NodeDatasetConcat = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
);
}
};
}, [Reference, inputs, nodeId, nodeList, onChangeNode, t, llmModelList]);
}, [inputs, nodeId, nodeList, onChangeNode, t]);
const Render = useMemo(() => {
return (
@@ -212,3 +130,75 @@ const NodeDatasetConcat = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
return Render;
};
export default React.memo(NodeDatasetConcat);
const VariableSelector = ({
nodeId,
inputChildren
}: {
nodeId: string;
inputChildren: FlowNodeInputItemType;
}) => {
const { t } = useTranslation();
const { onChangeNode } = useContextSelector(WorkflowContext, (v) => v);
const { referenceList } = useReference({
nodeId,
valueType: inputChildren.valueType
});
const onSelect = useCallback(
(e?: ReferenceItemValueType) => {
if (!e) return;
onChangeNode({
nodeId,
type: 'replaceInput',
key: inputChildren.key,
value: {
...inputChildren,
value: e
}
});
},
[inputChildren, nodeId, onChangeNode]
);
const onDel = useCallback(() => {
onChangeNode({
nodeId,
type: 'delInput',
key: inputChildren.key
});
}, [inputChildren.key, nodeId, onChangeNode]);
return (
<>
<Flex alignItems={'center'} mb={1}>
<FormLabel required={inputChildren.required}>{t(inputChildren.label as any)}</FormLabel>
{/* value */}
<ValueTypeLabel valueType={inputChildren.valueType} valueDesc={inputChildren.valueDesc} />
<MyIcon
className="delete"
name={'delete'}
w={'14px'}
color={'myGray.500'}
cursor={'pointer'}
ml={2}
_hover={{ color: 'red.600' }}
onClick={onDel}
/>
</Flex>
<ReferSelector
placeholder={t(
(inputChildren.referencePlaceholder as any) ||
t('common:core.module.Dataset quote.select')
)}
list={referenceList}
value={inputChildren.value}
onSelect={onSelect}
isArray={false}
/>
</>
);
};

View File

@@ -60,7 +60,8 @@ const NodeExtract = ({ data }: NodeProps<FlowNodeItemType>) => {
</Box>
<Button
size={'sm'}
variant={'whitePrimary'}
variant={'ghost'}
color={'myGray.600'}
leftIcon={<AddIcon fontSize={'10px'} />}
onClick={() => setEditExtractField(defaultField)}
>
@@ -78,12 +79,10 @@ const NodeExtract = ({ data }: NodeProps<FlowNodeItemType>) => {
<Table bg={'white'}>
<Thead>
<Tr>
<Th bg={'myGray.50'} borderRadius={'none !important'}>
{t('common:item_name')}
</Th>
<Th bg={'myGray.50'}>{t('common:item_description')}</Th>
<Th bg={'myGray.50'}>{t('common:required')}</Th>
<Th bg={'myGray.50'} borderRadius={'none !important'}></Th>
<Th borderRadius={'none !important'}>{t('common:item_name')}</Th>
<Th>{t('common:item_description')}</Th>
<Th>{t('common:required')}</Th>
<Th borderRadius={'none !important'}></Th>
</Tr>
</Thead>
<Tbody>

View File

@@ -46,11 +46,6 @@ const InputFormEditModal = ({
const inputType = watch('type') || FlowNodeInputTypeEnum.input;
const maxLength = watch('maxLength');
const max = watch('max');
const min = watch('min');
const defaultInputValue = watch('defaultValue');
const inputTypeList = [
{
icon: 'core/workflow/inputType/input',
@@ -187,14 +182,9 @@ const InputFormEditModal = ({
type={'formInput'}
isEdit={isEdit}
inputType={inputType}
maxLength={maxLength}
max={max}
min={min}
defaultValue={defaultInputValue}
onClose={onClose}
onSubmitSuccess={onSubmitSuccess}
onSubmitError={onSubmitError}
valueType={defaultValueType}
/>
</Flex>
</MyModal>

View File

@@ -49,6 +49,7 @@ import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { getEditorVariables } from '../../../utils';
import PromptEditor from '@fastgpt/web/components/common/Textarea/PromptEditor';
import { WorkflowNodeEdgeContext } from '../../../context/workflowInitContext';
const CurlImportModal = dynamic(() => import('./CurlImportModal'));
const defaultFormBody = {
@@ -80,9 +81,10 @@ const RenderHttpMethodAndUrl = React.memo(function RenderHttpMethodAndUrl({
}) {
const { t } = useTranslation();
const { toast } = useToast();
const edges = useContextSelector(WorkflowNodeEdgeContext, (v) => v.edges);
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const edges = useContextSelector(WorkflowContext, (v) => v.edges);
const { appDetail } = useContextSelector(AppContext, (v) => v);
const { isOpen: isOpenCurl, onOpen: onOpenCurl, onClose: onCloseCurl } = useDisclosure();
@@ -256,8 +258,9 @@ export function RenderHttpProps({
}) {
const { t } = useTranslation();
const [selectedTab, setSelectedTab] = useState(TabEnum.params);
const edges = useContextSelector(WorkflowNodeEdgeContext, (v) => v.edges);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const edges = useContextSelector(WorkflowContext, (v) => v.edges);
const { appDetail } = useContextSelector(AppContext, (v) => v);

View File

@@ -7,7 +7,7 @@ import Container from '../../components/Container';
import { MinusIcon, SmallAddIcon } from '@chakra-ui/icons';
import { IfElseListItemType } from '@fastgpt/global/core/workflow/template/system/ifElse/type';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { ReferenceValueProps } from '@fastgpt/global/core/workflow/type/io';
import { ReferenceItemValueType } from '@fastgpt/global/core/workflow/type/io';
import { useTranslation } from 'next-i18next';
import { ReferSelector, useReference } from '../render/RenderInput/templates/Reference';
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
@@ -62,6 +62,7 @@ const ListItem = ({
position={'relative'}
transform={snapshot.isDragging ? `scale(${getZoom()})` : ''}
transformOrigin={'top left'}
mb={2}
>
<Container w={snapshot.isDragging ? '' : 'full'} className="nodrag">
<Flex mb={4} alignItems={'center'}>
@@ -122,7 +123,7 @@ const ListItem = ({
<Flex gap={2} mb={2} alignItems={'center'}>
{/* variable reference */}
<Box minW={'250px'}>
<Reference
<VariableSelector
nodeId={nodeId}
variable={item.variable}
onSelect={(e) => {
@@ -255,7 +256,6 @@ const ListItem = ({
}}
variant={'link'}
leftIcon={<SmallAddIcon />}
my={3}
color={'primary.600'}
>
{t('common:core.module.input.add')}
@@ -266,7 +266,7 @@ const ListItem = ({
nodeId={nodeId}
handleId={handleId}
position={Position.Right}
translate={[18, 0]}
translate={[5, 0]}
/>
)}
</Flex>
@@ -302,29 +302,29 @@ const ListItem = ({
export default React.memo(ListItem);
const Reference = ({
const VariableSelector = ({
nodeId,
variable,
onSelect
}: {
nodeId: string;
variable?: ReferenceValueProps;
onSelect: (e: ReferenceValueProps) => void;
variable?: ReferenceItemValueType;
onSelect: (e?: ReferenceItemValueType) => void;
}) => {
const { t } = useTranslation();
const { referenceList, formatValue } = useReference({
const { referenceList } = useReference({
nodeId,
valueType: WorkflowIOValueTypeEnum.any,
value: variable
valueType: WorkflowIOValueTypeEnum.any
});
return (
<ReferSelector
placeholder={t('common:select_reference_variable')}
list={referenceList}
value={formatValue}
value={variable}
onSelect={onSelect}
isArray={false}
/>
);
};
@@ -336,7 +336,7 @@ const ConditionSelect = ({
onSelect
}: {
condition?: VariableConditionEnum;
variable?: ReferenceValueProps;
variable?: ReferenceItemValueType;
onSelect: (e: VariableConditionEnum) => void;
}) => {
const { t } = useTranslation();
@@ -414,7 +414,7 @@ const ConditionValueInput = ({
onChange
}: {
value?: string;
variable?: ReferenceValueProps;
variable?: ReferenceItemValueType;
condition?: VariableConditionEnum;
onChange: (e: string) => void;
}) => {

View File

@@ -48,7 +48,7 @@ const NodeIfElse = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
return (
<NodeCard selected={selected} maxW={'1000px'} {...data}>
<Box px={4} cursor={'default'}>
<Flex flexDirection={'column'} cursor={'default'}>
<DndDrag<IfElseListItemType>
onDragEndCb={(list: IfElseListItemType[]) => onUpdateIfElseList(list)}
dataList={ifElseList}
@@ -98,12 +98,12 @@ const NodeIfElse = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
nodeId={nodeId}
handleId={elseHandleId}
position={Position.Right}
translate={[26, 0]}
translate={[18, 0]}
/>
</Flex>
</Container>
</Box>
<Box py={3} px={6}>
</Flex>
<Box py={3} px={4}>
<Button
variant={'whiteBase'}
w={'full'}

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