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:
@@ -16,6 +16,9 @@ OPENAI_BASE_URL=https://api.openai.com/v1
|
||||
# 通用key。可以是 openai 的也可以是 oneapi 的。
|
||||
# 此处逻辑:优先走 ONEAPI_URL,如果填写了 ONEAPI_URL,key 也需要是 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-
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "app",
|
||||
"version": "4.8.12",
|
||||
"version": "4.8.13",
|
||||
"private": false,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
|
||||
@@ -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 |
4
projects/app/public/icon/resizer.svg
Normal file
4
projects/app/public/icon/resizer.svg
Normal 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 |
207
projects/app/public/imgs/app/visionModel.svg
Normal file
207
projects/app/public/imgs/app/visionModel.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 48 KiB |
24
projects/app/src/components/Markdown/codeBlock/Iframe.tsx
Normal file
24
projects/app/src/components/Markdown/codeBlock/Iframe.tsx
Normal 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;
|
||||
@@ -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;
|
||||
34
projects/app/src/components/Markdown/hooks.ts
Normal file
34
projects/app/src/components/Markdown/hooks.ts
Normal 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
|
||||
};
|
||||
};
|
||||
@@ -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}>
|
||||
|
||||
@@ -5,7 +5,8 @@ export enum CodeClassNameEnum {
|
||||
echarts = 'echarts',
|
||||
quote = 'quote',
|
||||
files = 'files',
|
||||
latex = 'latex'
|
||||
latex = 'latex',
|
||||
iframe = 'iframe'
|
||||
}
|
||||
|
||||
function htmlTableToLatex(html: string) {
|
||||
|
||||
@@ -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={''}
|
||||
|
||||
@@ -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'}
|
||||
|
||||
@@ -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'}>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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'}
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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'}
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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={''}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
</>
|
||||
|
||||
@@ -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} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>;
|
||||
};
|
||||
|
||||
@@ -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'}
|
||||
|
||||
@@ -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?.();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')} />
|
||||
)}
|
||||
|
||||
@@ -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}¤tTab=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}¤tTab=dataCard&collectionId=${quoteItem.collectionId}`}
|
||||
>
|
||||
{t('common:core.dataset.Go Dataset')}
|
||||
<MyIcon name={'common/rightArrowLight'} w={'10px'} />
|
||||
</Link>
|
||||
)}
|
||||
</Flex>
|
||||
</MyBox>
|
||||
|
||||
{editInputData && (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -61,6 +61,7 @@ const DefaultPermissionList = ({
|
||||
}
|
||||
}}
|
||||
fontSize={styles?.fontSize}
|
||||
fontWeight={styles?.fontWeight}
|
||||
/>
|
||||
</Box>
|
||||
<ConfirmModal />
|
||||
|
||||
@@ -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'}
|
||||
|
||||
@@ -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>)}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -180,6 +180,9 @@ const DetailLogsModal = ({
|
||||
chatConfig={chat?.app?.chatConfig}
|
||||
appId={appId}
|
||||
chatId={chatId}
|
||||
chatType="log"
|
||||
showRawSource
|
||||
showNodeStatus
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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'}
|
||||
|
||||
@@ -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'}>
|
||||
|
||||
@@ -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'}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'}
|
||||
|
||||
@@ -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'}>
|
||||
|
||||
@@ -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'}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
})}
|
||||
|
||||
@@ -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={''}
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -387,6 +387,7 @@ const RenderList = React.memo(function RenderList({
|
||||
isInvalid={errors && Object.keys(errors).includes(input.key)}
|
||||
onChange={onChange}
|
||||
input={input}
|
||||
setUploading={() => {}}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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 }) => (
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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'}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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 && (
|
||||
<>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}) => {
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user