├── src ├── components │ ├── Chat │ │ ├── const.ts │ │ ├── types.ts │ │ ├── index.ts │ │ ├── store │ │ │ ├── index.ts │ │ │ ├── __snapshots__ │ │ │ │ └── store.test.ts.snap │ │ │ ├── initialState.ts │ │ │ ├── selectors.ts │ │ │ └── messageReducer.test.ts │ │ └── MessageModal.tsx │ ├── DndKit │ │ ├── index.ts │ │ ├── Draggable.tsx │ │ └── DaggingOverlay.tsx │ ├── DraggablePanel │ │ └── index.ts │ ├── FlowRunPanel │ │ └── style.ts │ ├── Settings │ │ ├── style.ts │ │ └── index.tsx │ ├── SearchBar.tsx │ ├── ModuleLoading.tsx │ ├── Logo │ │ ├── index.tsx │ │ └── Logo.tsx │ ├── EditableText.tsx │ ├── Markdown │ │ ├── CodeBlock.tsx │ │ └── index.tsx │ ├── Highlighter │ │ ├── Prism │ │ │ └── index.tsx │ │ ├── index.tsx │ │ ├── style.ts │ │ └── Highlighter.tsx │ ├── FlowInputRender │ │ └── index.tsx │ ├── Header │ │ ├── ThemeIcon.tsx │ │ └── index.tsx │ ├── Swatches │ │ └── index.tsx │ ├── IconAction.tsx │ ├── AgentAvatar │ │ └── index.tsx │ ├── CopyButton │ │ └── index.tsx │ ├── MessageInput.tsx │ └── ControlInput.tsx ├── const │ └── fetch.ts ├── utils │ ├── constant.ts │ ├── uploadFIle.ts │ ├── time.ts │ ├── genChatMessages.ts │ ├── colorUtils.ts │ ├── filter.ts │ ├── StringTemplate.ts │ ├── VersionController.ts │ ├── VersionController.test.ts │ └── compass.ts ├── pages │ ├── index.page.tsx │ ├── mask │ │ ├── components │ │ │ └── AgentAvatar │ │ │ │ ├── index.tsx │ │ │ │ └── EmojiPicker.tsx │ │ ├── layout.tsx │ │ └── new.page.tsx │ ├── flow │ │ ├── components │ │ │ ├── nodes │ │ │ │ ├── types.ts │ │ │ │ ├── String │ │ │ │ │ └── index.ts │ │ │ │ ├── Embeddings │ │ │ │ │ └── index.ts │ │ │ │ ├── FileRead │ │ │ │ │ └── index.tsx │ │ │ │ ├── NetWork │ │ │ │ │ └── index.tsx │ │ │ │ ├── DingDingBot │ │ │ │ │ └── index.tsx │ │ │ │ ├── index.ts │ │ │ │ ├── VoyQuery │ │ │ │ │ └── index.ts │ │ │ │ └── AITask │ │ │ │ │ └── index.tsx │ │ │ ├── Terminal │ │ │ │ └── style.ts │ │ │ ├── Panel │ │ │ │ ├── NodeManager │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── SymbolList │ │ │ │ │ │ └── index.tsx │ │ │ │ └── index.tsx │ │ │ ├── InputSchemaRender │ │ │ │ ├── Var.tsx │ │ │ │ ├── TextAreaInput.tsx │ │ │ │ ├── VariableHandle.tsx │ │ │ │ └── TextVariableHandle.tsx │ │ │ ├── DefaultPreview │ │ │ │ └── index.tsx │ │ │ └── FlowView │ │ │ │ └── useDragAndDrop.ts │ │ ├── index.page.tsx │ │ └── [id].page.tsx │ ├── runner │ │ ├── index.page.tsx │ │ ├── components │ │ │ └── FlowSelect │ │ │ │ └── index.tsx │ │ └── layout.tsx │ ├── _app.page.tsx │ ├── api │ │ ├── chain.api.ts │ │ ├── db.ts │ │ ├── sdserve.api.ts │ │ ├── openai.api.ts │ │ ├── proxy.api.ts │ │ ├── auth │ │ │ └── [...nextauth].api.ts │ │ ├── dingbot.api.ts │ │ ├── embeddings.api.ts │ │ ├── github.api.ts │ │ └── workflow.api.ts │ ├── zhizhi │ │ └── color │ │ │ └── index.module.css │ ├── schema │ │ └── layout.tsx │ └── _document.page.tsx ├── store │ ├── index.ts │ ├── session │ │ ├── selectors.ts │ │ ├── slices │ │ │ ├── agent │ │ │ │ ├── index.ts │ │ │ │ ├── selectors │ │ │ │ │ ├── index.ts │ │ │ │ │ └── agent.ts │ │ │ │ ├── initialState.ts │ │ │ │ └── reducers │ │ │ │ │ └── agents.ts │ │ │ └── chat │ │ │ │ ├── index.ts │ │ │ │ ├── selectors │ │ │ │ ├── index.ts │ │ │ │ └── list.ts │ │ │ │ ├── initialState.ts │ │ │ │ └── reducers │ │ │ │ ├── chats.ts │ │ │ │ └── sessionTree.ts │ │ ├── store.ts │ │ ├── initialState.ts │ │ └── index.ts │ ├── masks │ │ ├── typing.ts │ │ └── index.ts │ ├── flow │ │ ├── action.ts │ │ ├── index.ts │ │ ├── initialState.ts │ │ ├── selectors │ │ │ └── index.ts │ │ └── reducers │ │ │ └── flows.ts │ ├── settings.ts │ ├── middleware │ │ └── createHashStorage.ts │ ├── useHighlight.ts │ └── chat │ │ └── index.ts ├── types │ ├── flow │ │ ├── index.ts │ │ ├── node │ │ │ ├── index.ts │ │ │ ├── sdTask.ts │ │ │ └── aiTask.ts │ │ ├── workflow.ts │ │ └── symbols.ts │ ├── index.ts │ ├── meta.ts │ ├── langchain.ts │ ├── request.ts │ ├── exportConfig.ts │ ├── sessions.ts │ ├── agent.ts │ └── chat.ts ├── helpers │ ├── flow │ │ ├── index.ts │ │ ├── node.ts │ │ ├── nodeTree.ts │ │ ├── create.ts │ │ └── workflow.ts │ ├── url.ts │ ├── agent.ts │ ├── prompt.ts │ └── prompt.test.ts ├── runStroe │ └── index.ts ├── services │ ├── url.ts │ ├── networkServe.ts │ ├── dingdingbotServe.ts │ ├── embeddings.ts │ ├── langChain.ts │ ├── server.ts │ ├── sdServe.ts │ ├── chatModel.ts │ ├── mask.ts │ └── workflow.ts ├── hooks │ ├── useCopied.ts │ └── useImportAndExport.ts ├── features │ ├── Sidebar │ │ └── style.ts │ └── FolderPanel │ │ └── index.tsx ├── prompts │ ├── chat.ts │ └── agent.ts ├── theme │ ├── customToken.ts │ ├── antd.ts │ └── customStylish.ts └── layout │ ├── FlowLayout │ ├── Menu │ │ ├── index.tsx │ │ ├── Header.tsx │ │ └── FlowList │ │ │ ├── index.tsx │ │ │ ├── Actions.tsx │ │ │ └── FlowItem.tsx │ └── index.tsx │ └── style.ts ├── commitlint.config.js ├── .releaserc.js ├── public ├── favicon.png ├── icons │ ├── icon-192x192.png │ ├── icon-256x256.png │ └── icon-512x512.png.png ├── manifest.json └── dingding.svg ├── typing.d.ts ├── .husky ├── pre-commit └── commit-msg ├── vercel.json ├── .eslintrc.js ├── next-env.d.ts ├── mock ├── messages.ts └── response.ts ├── test.serve.js ├── .stylelintrc.js ├── .prettierignore ├── vitest.config.ts ├── .prettierrc.js ├── s.sql ├── tests └── setup.ts ├── .gitignore ├── tsconfig.json ├── .github └── workflows │ ├── test.yml │ └── release.yml └── next.config.js /src/components/Chat/const.ts: -------------------------------------------------------------------------------- 1 | export const LOADING_FLAT = '...'; 2 | -------------------------------------------------------------------------------- /src/const/fetch.ts: -------------------------------------------------------------------------------- 1 | export const OPENAI_SERVICE_ERROR_CODE = 555; 2 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['gitmoji'], 3 | }; 4 | -------------------------------------------------------------------------------- /src/utils/constant.ts: -------------------------------------------------------------------------------- 1 | export enum StoreKey { 2 | Mask = 'mask-store', 3 | } 4 | -------------------------------------------------------------------------------- /.releaserc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['semantic-release-config-gitmoji'], 3 | }; 4 | -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tech-flow-org/TechFlow/HEAD/public/favicon.png -------------------------------------------------------------------------------- /src/pages/index.page.tsx: -------------------------------------------------------------------------------- 1 | import Flow from './flow/index.page'; 2 | 3 | export default Flow; 4 | -------------------------------------------------------------------------------- /typing.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'dingtalk-robot-sdk'; 2 | declare module 'pdfjs-dist/build/pdf'; 3 | -------------------------------------------------------------------------------- /src/components/DndKit/index.ts: -------------------------------------------------------------------------------- 1 | export * from './DaggingOverlay'; 2 | export * from './Draggable'; 3 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | export * from './chat'; 2 | export * from './session'; 3 | export * from './settings'; 4 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx --no-install lint-staged 5 | -------------------------------------------------------------------------------- /public/icons/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tech-flow-org/TechFlow/HEAD/public/icons/icon-192x192.png -------------------------------------------------------------------------------- /public/icons/icon-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tech-flow-org/TechFlow/HEAD/public/icons/icon-256x256.png -------------------------------------------------------------------------------- /src/types/flow/index.ts: -------------------------------------------------------------------------------- 1 | export * from './node'; 2 | export * from './symbols'; 3 | export * from './workflow'; 4 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx --no -- commitlint --edit ${1} 5 | -------------------------------------------------------------------------------- /public/icons/icon-512x512.png.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tech-flow-org/TechFlow/HEAD/public/icons/icon-512x512.png.png -------------------------------------------------------------------------------- /src/types/flow/node/index.ts: -------------------------------------------------------------------------------- 1 | export * from './aiTask'; 2 | 3 | export interface StringNodeContent { 4 | text: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/store/session/selectors.ts: -------------------------------------------------------------------------------- 1 | export { agentSelectors } from './slices/agent'; 2 | export { chatSelectors } from './slices/chat'; 3 | -------------------------------------------------------------------------------- /src/store/session/slices/agent/index.ts: -------------------------------------------------------------------------------- 1 | export * from './action'; 2 | export * from './initialState'; 3 | export * from './selectors'; 4 | -------------------------------------------------------------------------------- /src/store/session/slices/chat/index.ts: -------------------------------------------------------------------------------- 1 | export * from './action'; 2 | export * from './initialState'; 3 | export * from './selectors'; 4 | -------------------------------------------------------------------------------- /src/helpers/flow/index.ts: -------------------------------------------------------------------------------- 1 | export * from './create'; 2 | export * from './node'; 3 | export * from './nodeTree'; 4 | export * from './workflow'; 5 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "crons": [{ "path": "/api/dingding/zhiban", "schedule": "0 10 * * 1" }], 3 | "git": { 4 | "deploymentEnabled": {} 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | const config = require('@umijs/lint/dist/config/eslint'); 2 | 3 | module.exports = { 4 | ...config, 5 | extends: ['plugin:@next/next/recommended'], 6 | }; 7 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './agent'; 2 | export * from './chat'; 3 | export * from './exportConfig'; 4 | export * from './request'; 5 | export * from './sessions'; 6 | -------------------------------------------------------------------------------- /src/components/DraggablePanel/index.ts: -------------------------------------------------------------------------------- 1 | export type { Position } from 'react-rnd' 2 | export { Draggable as DraggablePanel } from './DraggablePanel' 3 | export type { DraggablePanelProps } from './DraggablePanel' 4 | -------------------------------------------------------------------------------- /src/store/masks/typing.ts: -------------------------------------------------------------------------------- 1 | import { Mask, ModelConfig } from '../mask'; 2 | 3 | export type BuiltinMask = Omit & { 4 | builtin: boolean; 5 | modelConfig: Partial; 6 | }; 7 | -------------------------------------------------------------------------------- /src/components/Chat/types.ts: -------------------------------------------------------------------------------- 1 | import { ChatAgent, ChatContext } from '@/types'; 2 | 3 | export interface InternalChatContext extends Omit { 4 | // 智能体 5 | agent: Omit; 6 | } 7 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /src/components/Chat/index.ts: -------------------------------------------------------------------------------- 1 | export { ChatMessageList } from './ChatMessageList'; 2 | export { EditableMessage } from './EditableMessage'; 3 | export { default as MessageModal } from './MessageModal'; 4 | export { messagesReducer } from './store'; 5 | -------------------------------------------------------------------------------- /src/store/session/slices/chat/selectors/index.ts: -------------------------------------------------------------------------------- 1 | import { chatListSel, currentChatContextSel, sessionTreeSel } from './list'; 2 | 3 | export const chatSelectors = { 4 | currentChat: currentChatContextSel, 5 | chatList: chatListSel, 6 | sessionTree: sessionTreeSel, 7 | }; 8 | -------------------------------------------------------------------------------- /mock/messages.ts: -------------------------------------------------------------------------------- 1 | export const userMessageWithModify = [ 2 | { 3 | role: 'user', 4 | content: '你好', 5 | }, 6 | { 7 | role: 'user', 8 | content: '你是谁', 9 | }, 10 | { 11 | role: 'assistant', 12 | content: '我是一名人工智能助理,可以回答您的问题和提供帮助。', 13 | }, 14 | ]; 15 | -------------------------------------------------------------------------------- /src/utils/uploadFIle.ts: -------------------------------------------------------------------------------- 1 | export const createUploadImageHandler = 2 | (onUploadImage: (base64: string) => void) => (file: any) => { 3 | const reader = new FileReader(); 4 | reader.readAsDataURL(file); 5 | reader.onload = () => { 6 | onUploadImage(String(reader.result)); 7 | }; 8 | }; 9 | -------------------------------------------------------------------------------- /src/runStroe/index.ts: -------------------------------------------------------------------------------- 1 | const { program } = require('commander'); 2 | 3 | program.version('1.0.0'); 4 | 5 | program 6 | .command('hello ') 7 | .description('Say hello to someone') 8 | .action((name: string) => { 9 | console.log(`Hello, ${name}!`); 10 | }); 11 | 12 | program.parse(process.argv); 13 | -------------------------------------------------------------------------------- /src/types/flow/node/sdTask.ts: -------------------------------------------------------------------------------- 1 | export type SDTaskType = { 2 | model?: string; 3 | size?: 'landing' | 'avatar' | '4:3'; 4 | mode?: string; 5 | output?: string; 6 | width?: number; 7 | height?: number; 8 | hr_scale?: number; 9 | enable_hr?: boolean; 10 | negative_prompt?: string; 11 | prompt?: string; 12 | }; 13 | -------------------------------------------------------------------------------- /test.serve.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | 3 | const app = express(); 4 | app.use(express.json()); 5 | app.post('/api/data', (req, res) => { 6 | console.log('api/data', req.body); 7 | res.json({ name: '服务发送成功' }); 8 | }); 9 | 10 | app.listen(8001, () => { 11 | console.log('listening on port 8001'); 12 | }); 13 | -------------------------------------------------------------------------------- /src/pages/mask/components/AgentAvatar/index.tsx: -------------------------------------------------------------------------------- 1 | import { Flexbox } from 'react-layout-kit'; 2 | 3 | import EmojiPicker from './EmojiPicker'; 4 | 5 | export const AgentAvatar: React.FC> = (props) => { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | }; 12 | -------------------------------------------------------------------------------- /.stylelintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['stylelint-config-recommended', 'stylelint-config-clean-order'], 3 | files: ['*.js', '*.jsx', '*.ts', '*.tsx'], 4 | plugins: ['stylelint-order'], 5 | customSyntax: 'postcss-styled-syntax', 6 | rules: { 7 | 'no-empty-source': null, 8 | 'no-invalid-double-slash-comments': null, 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /src/pages/flow/components/nodes/types.ts: -------------------------------------------------------------------------------- 1 | import { AITaskContent, OutputNodeContent } from '@/types/flow'; 2 | import { BasicFlowNodeProps } from '@ant-design/pro-flow-editor'; 3 | 4 | export type AITaskNodeProps = BasicFlowNodeProps; 5 | 6 | export type StringNodeProps = BasicFlowNodeProps; 7 | 8 | export type OutputNodeProps = BasicFlowNodeProps; 9 | -------------------------------------------------------------------------------- /src/types/meta.ts: -------------------------------------------------------------------------------- 1 | export interface MetaData { 2 | /** 3 | * 名称 4 | * @description 可选参数,如果不传则使用默认名称 5 | */ 6 | title?: string; 7 | description?: string; 8 | /** 9 | * 角色头像 10 | * @description 可选参数,如果不传则使用默认头像 11 | */ 12 | avatar?: string; 13 | /** 14 | * 角色头像背景色 15 | * @description 可选参数,如果不传则使用默认背景色 16 | */ 17 | avatarBackground?: string; 18 | } 19 | -------------------------------------------------------------------------------- /src/types/langchain.ts: -------------------------------------------------------------------------------- 1 | import { ChatMessage } from './chat'; 2 | 3 | export interface LangChainParams { 4 | llm: { 5 | model: string; 6 | temperature: number; 7 | top_p?: number; 8 | frequency_penalty?: number; 9 | max_tokens?: number; 10 | }; 11 | 12 | /** 13 | * @title 聊天信息列表 14 | */ 15 | prompts: ChatMessage[]; 16 | vars: Record; 17 | } 18 | -------------------------------------------------------------------------------- /src/types/request.ts: -------------------------------------------------------------------------------- 1 | import { ChatMessage } from './chat'; 2 | 3 | /** 4 | * 请求数据类型 5 | */ 6 | export interface OpenAIRequestParams { 7 | /** 8 | * @title 系统角色 9 | * @type string 10 | */ 11 | systemRole: string; 12 | /** 13 | * @title 消息内容 14 | * @type string 15 | */ 16 | message?: string; 17 | /** 18 | * 中间的聊天记录 19 | */ 20 | messages?: ChatMessage[]; 21 | } 22 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/*.svg 2 | .umi 3 | .umi-production 4 | /dist 5 | .dockerignore 6 | .DS_Store 7 | .eslintignore 8 | *.png 9 | *.jpg 10 | *.webp 11 | *.toml 12 | *.py 13 | docker 14 | .editorconfig 15 | Dockerfile* 16 | .gitignore 17 | .prettierignore 18 | LICENSE 19 | .eslintcache 20 | *.lock 21 | yarn-error.log 22 | .idea 23 | .husky 24 | .npmrc 25 | .env.local 26 | .next 27 | __snapshots__ 28 | .snap -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | setupFiles: './tests/setup.ts', 6 | environment: 'jsdom', 7 | globals: true, 8 | alias: { 9 | '@': './src', 10 | }, 11 | coverage: { 12 | reporter: ['text', 'json', 'lcov', 'text-summary'], 13 | provider: 'v8', 14 | }, 15 | }, 16 | }); 17 | -------------------------------------------------------------------------------- /src/components/FlowRunPanel/style.ts: -------------------------------------------------------------------------------- 1 | import { createStyles } from 'antd-style'; 2 | 3 | export const NOTIFICATION_PRIMARY = 'notification-primary-info'; 4 | 5 | export const useStyles = createStyles(({ css, token }) => ({ 6 | code: css` 7 | font-family: ${token.fontFamilyCode}; 8 | background: ${token.colorFillQuaternary}; 9 | text-wrap: balance; 10 | padding: 8px; 11 | `, 12 | })); 13 | -------------------------------------------------------------------------------- /src/pages/flow/components/Terminal/style.ts: -------------------------------------------------------------------------------- 1 | import { createStyles } from 'antd-style'; 2 | 3 | export const NOTIFICATION_PRIMARY = 'notification-primary-info'; 4 | 5 | export const useStyles = createStyles(({ css, token }) => ({ 6 | code: css` 7 | font-family: ${token.fontFamilyCode}; 8 | background: ${token.colorFillQuaternary}; 9 | text-wrap: balance; 10 | padding: 8px; 11 | `, 12 | })); 13 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 100, 3 | singleQuote: true, 4 | trailingComma: 'all', 5 | proseWrap: 'never', 6 | endOfLine: 'lf', 7 | overrides: [{ files: '.prettierrc', options: { parser: 'json' } }], 8 | plugins: [ 9 | require.resolve('prettier-plugin-packagejson'), 10 | require.resolve('prettier-plugin-organize-imports'), 11 | ], 12 | pluginSearchDirs: false, 13 | }; 14 | -------------------------------------------------------------------------------- /src/utils/time.ts: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs'; 2 | import 'dayjs/locale/zh-cn'; 3 | dayjs.locale('zh-cn'); 4 | 5 | export const getChatItemTime = (updateAt: number) => { 6 | const time = dayjs(updateAt); 7 | const diff = dayjs().day() - time.day(); 8 | 9 | if (time.isSame(dayjs(), 'day')) return time.format('HH:mm'); 10 | 11 | if (diff === 1) return '昨天'; 12 | 13 | return time.format('MM-DD'); 14 | }; 15 | -------------------------------------------------------------------------------- /src/components/Settings/style.ts: -------------------------------------------------------------------------------- 1 | import { createStyles } from 'antd-style'; 2 | 3 | export const useStyles = createStyles(({ css, token, prefixCls }) => ({ 4 | actions: css` 5 | height: 40px; 6 | border: 1px solid ${token.colorBorder} !important; 7 | box-shadow: none; 8 | `, 9 | upload: css` 10 | width: 100%; 11 | .${prefixCls}-upload { 12 | width: 100%; 13 | } 14 | `, 15 | })); 16 | -------------------------------------------------------------------------------- /src/services/url.ts: -------------------------------------------------------------------------------- 1 | const isDev = process.env.NODE_ENV === 'development'; 2 | 3 | const prefix = isDev ? '-dev' : ''; 4 | 5 | export const URLS = { 6 | openai: '/api/openai' + prefix, 7 | chain: '/api/chain' + prefix, 8 | sd: '/api/sd/text2img', 9 | mask: '/api/mask', 10 | workflow: '/api/workflow', 11 | embeddings: '/api/embeddings' + prefix, 12 | network: '/api/network/proxy', 13 | dingdingbot: '/api/dingding/send', 14 | }; 15 | -------------------------------------------------------------------------------- /src/pages/flow/components/Panel/NodeManager/index.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from 'react'; 2 | import { Flexbox } from 'react-layout-kit'; 3 | 4 | import SymbolList from './SymbolList'; 5 | 6 | const NodeManager = memo(() => { 7 | return ( 8 | 9 |
10 | 11 |
12 |
13 | ); 14 | }); 15 | 16 | export default NodeManager; 17 | -------------------------------------------------------------------------------- /src/store/session/slices/agent/selectors/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | agentListSel, 3 | currentAgentSel, 4 | currentAgentTitleSel, 5 | currentAgentWithFlowSel, 6 | getAgentById, 7 | } from './agent'; 8 | 9 | export const agentSelectors = { 10 | currentAgent: currentAgentSel, 11 | currentAgentWithFlow: currentAgentWithFlowSel, 12 | agentList: agentListSel, 13 | currentAgentSlicedTitle: currentAgentTitleSel, 14 | getAgentById, 15 | }; 16 | -------------------------------------------------------------------------------- /src/components/SearchBar.tsx: -------------------------------------------------------------------------------- 1 | import { SearchOutlined } from '@ant-design/icons'; 2 | import { Input, InputProps } from 'antd'; 3 | import { memo } from 'react'; 4 | 5 | export const SearchBar = memo(({ value, onChange, style }: InputProps) => ( 6 | } 8 | allowClear 9 | value={value} 10 | placeholder="搜索" 11 | style={{ ...style, borderColor: 'transparent' }} 12 | onChange={onChange} 13 | /> 14 | )); 15 | -------------------------------------------------------------------------------- /src/pages/flow/components/InputSchemaRender/Var.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from 'react'; 2 | 3 | import { VariableHandle } from './VariableHandle'; 4 | 5 | export const VarShow = memo(({ id, ...props }: { id: string; value: string }) => { 6 | return ( 7 |
8 | 12 | {props.value} 13 |
14 | ); 15 | }); 16 | -------------------------------------------------------------------------------- /s.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE mask ( 2 | id varchar(128) primary key not null, 3 | name varchar(255), 4 | avatar varchar(255), 5 | context varchar(4096), 6 | lang varchar(255), 7 | model_config varchar(255), 8 | user_id varchar(255), 9 | created_at TIMESTAMP DEFAULT NOW() 10 | ); 11 | 12 | CREATE TABLE workflow ( 13 | id varchar(128) primary key not null, 14 | workflow text, 15 | user_id varchar(255), 16 | created_at TIMESTAMP DEFAULT NOW() 17 | ); -------------------------------------------------------------------------------- /src/pages/runner/index.page.tsx: -------------------------------------------------------------------------------- 1 | import { useSettings } from '@/store'; 2 | import type { NextPage } from 'next'; 3 | import { memo, useEffect } from 'react'; 4 | import RunnerLayout from './layout'; 5 | 6 | const Runner: NextPage = () => { 7 | useEffect(() => { 8 | useSettings.setState({ sidebarKey: 'runner' }); 9 | }, []); 10 | 11 | return ( 12 | 13 |
14 | 15 | ); 16 | }; 17 | 18 | export default memo(Runner); 19 | -------------------------------------------------------------------------------- /src/services/networkServe.ts: -------------------------------------------------------------------------------- 1 | import { URLS } from '@/services/url'; 2 | import { OutputNodeContent } from '@/types/flow'; 3 | 4 | /** 5 | * 专门用于 FlowChain 的 fetch 6 | */ 7 | export const fetchNetworkServe = (params: OutputNodeContent) => 8 | window 9 | .fetch(URLS.network, { 10 | method: 'POST', 11 | headers: { 12 | 'Content-Type': 'application/json', 13 | }, 14 | body: JSON.stringify(params), 15 | }) 16 | .then((res) => res.json()); 17 | -------------------------------------------------------------------------------- /src/services/dingdingbotServe.ts: -------------------------------------------------------------------------------- 1 | import { URLS } from '@/services/url'; 2 | import { OutputNodeContent } from '@/types/flow'; 3 | 4 | /** 5 | * 专门用于 FlowChain 的 fetch 6 | */ 7 | export const fetchDingDingBotServe = (params: OutputNodeContent) => 8 | window 9 | .fetch(URLS.dingdingbot, { 10 | method: 'POST', 11 | headers: { 12 | 'Content-Type': 'application/json', 13 | }, 14 | body: JSON.stringify(params), 15 | }) 16 | .then((res) => res.json()); 17 | -------------------------------------------------------------------------------- /src/pages/_app.page.tsx: -------------------------------------------------------------------------------- 1 | import Layout from '@/layout'; 2 | import { Analytics } from '@vercel/analytics/react'; 3 | import { SessionProvider } from 'next-auth/react'; 4 | import type { AppProps } from 'next/app'; 5 | 6 | function MyApp({ Component, pageProps }: AppProps) { 7 | return ( 8 | 9 | 10 | 11 | 12 | 13 | 14 | ); 15 | } 16 | 17 | export default MyApp; 18 | -------------------------------------------------------------------------------- /src/pages/api/chain.api.ts: -------------------------------------------------------------------------------- 1 | import { LangChainParams } from '@/types/langchain'; 2 | import { LangChainStream } from './LangChainStream'; 3 | 4 | if (!process.env.OPENAI_API_KEY) { 5 | throw new Error('Missing env var from OpenAI'); 6 | } 7 | 8 | export const config = { 9 | runtime: 'edge', 10 | }; 11 | 12 | export default async function handler(request: Request) { 13 | const payload = (await request.json()) as LangChainParams; 14 | return new Response(LangChainStream(payload)); 15 | } 16 | -------------------------------------------------------------------------------- /src/services/embeddings.ts: -------------------------------------------------------------------------------- 1 | import { DocumentsLoadPayload } from '@/pages/api/embeddings.api'; 2 | import { URLS } from '@/services/url'; 3 | 4 | /** 5 | * 专门用于 FlowChain 的 fetch 6 | */ 7 | export const fetchEmbeddings = (params: DocumentsLoadPayload) => 8 | window 9 | .fetch(URLS.embeddings, { 10 | method: 'POST', 11 | headers: { 12 | 'Content-Type': 'application/json', 13 | }, 14 | body: JSON.stringify(params), 15 | }) 16 | .then((res) => res.json()); 17 | -------------------------------------------------------------------------------- /src/store/session/slices/chat/initialState.ts: -------------------------------------------------------------------------------- 1 | import { ChatContextMap } from '@/types'; 2 | 3 | export interface SessionChatState { 4 | /** 5 | * @title 聊天上下文映射 6 | * @description 用于记录每个用户与每个角色的聊天上下文 7 | */ 8 | chats: ChatContextMap; 9 | 10 | summarizingTitle: boolean; 11 | summarizingDescription: boolean; 12 | } 13 | 14 | export const initialChatState: SessionChatState = { 15 | chats: {}, 16 | // loading 中间态 17 | summarizingTitle: false, 18 | summarizingDescription: false, 19 | }; 20 | -------------------------------------------------------------------------------- /src/utils/genChatMessages.ts: -------------------------------------------------------------------------------- 1 | import { ChatMessage, OpenAIRequestParams } from '@/types'; 2 | 3 | export const genChatMessages = ({ 4 | systemRole, 5 | messages = [], 6 | message, 7 | }: Partial): ChatMessage[] => 8 | [ 9 | systemRole ? { role: 'system', content: systemRole } : null, 10 | ...messages.filter((i) => i).map((m) => ({ role: m.role, content: m.content })), 11 | message ? { role: 'user', content: message } : null, 12 | ].filter(Boolean) as ChatMessage[]; 13 | -------------------------------------------------------------------------------- /src/pages/api/db.ts: -------------------------------------------------------------------------------- 1 | import { createKysely } from '@vercel/postgres-kysely'; 2 | 3 | export type MaskDataBase = Partial<{ 4 | id?: string; 5 | name?: string; 6 | user_id?: string; 7 | avatar: string; 8 | context?: string; 9 | model_config?: string; 10 | }>; 11 | 12 | export type WorkFlowDataBase = Partial<{ 13 | id?: string; 14 | user_id?: string; 15 | workflow?: string; 16 | }>; 17 | 18 | interface Database { 19 | mask: MaskDataBase; 20 | workflow: WorkFlowDataBase; 21 | } 22 | 23 | export const dataBase = createKysely(); 24 | -------------------------------------------------------------------------------- /src/services/langChain.ts: -------------------------------------------------------------------------------- 1 | import { URLS } from '@/services/url'; 2 | import { LangChainParams } from '@/types/langchain'; 3 | import { fetchAIFactory } from '@/utils/fetch'; 4 | 5 | /** 6 | * 专门用于 FlowChain 的 fetch 7 | */ 8 | export const fetchLangChain = fetchAIFactory( 9 | (params: LangChainParams, signal?: AbortSignal | undefined) => 10 | fetch(URLS.chain, { 11 | method: 'POST', 12 | headers: { 13 | 'Content-Type': 'application/json', 14 | }, 15 | body: JSON.stringify(params), 16 | signal, 17 | }), 18 | ); 19 | -------------------------------------------------------------------------------- /src/components/ModuleLoading.tsx: -------------------------------------------------------------------------------- 1 | import { Loading3QuartersOutlined } from '@ant-design/icons'; 2 | import { Spin } from 'antd'; 3 | import { useTheme } from 'antd-style'; 4 | import { Center } from 'react-layout-kit'; 5 | 6 | export default () => { 7 | const theme = useTheme(); 8 | 9 | return ( 10 |
11 | } 16 | /> 17 |
18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /src/hooks/useCopied.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useMemo, useState } from 'react'; 2 | 3 | export const useCopied = () => { 4 | const [copied, setCopy] = useState(false); 5 | 6 | useEffect(() => { 7 | if (!copied) return; 8 | 9 | const timer = setTimeout(() => { 10 | setCopy(false); 11 | }, 2000); 12 | 13 | return () => { 14 | clearTimeout(timer); 15 | }; 16 | }, [copied]); 17 | 18 | const setCopied = useCallback(() => setCopy(true), []); 19 | 20 | return useMemo(() => ({ copied, setCopied }), [copied]); 21 | }; 22 | -------------------------------------------------------------------------------- /src/store/session/store.ts: -------------------------------------------------------------------------------- 1 | import { StateCreator } from 'zustand/vanilla'; 2 | 3 | import { SessionState, initialState } from './initialState'; 4 | import { AgentAction, createAgentSlice } from './slices/agent'; 5 | import { ChatAction, createChatSlice } from './slices/chat'; 6 | 7 | export type SessionStore = ChatAction & AgentAction & SessionState; 8 | 9 | export const createStore: StateCreator = ( 10 | ...params 11 | ) => ({ 12 | ...initialState, 13 | ...createAgentSlice(...params), 14 | ...createChatSlice(...params), 15 | }); 16 | -------------------------------------------------------------------------------- /src/pages/runner/components/FlowSelect/index.tsx: -------------------------------------------------------------------------------- 1 | import { flowSelectors, useFlowStore } from '@/store/flow'; 2 | import { Select, SelectProps } from 'antd'; 3 | import { isEqual } from 'lodash-es'; 4 | 5 | export const FlowSelect: React.FC = (props) => { 6 | const list = useFlowStore(flowSelectors.flowMetaList, isEqual); 7 | return ( 8 | } 27 | allowClear 28 | value={keywords} 29 | placeholder="搜索" 30 | style={{ borderColor: 'transparent' }} 31 | onChange={(e) => useFlowStore.setState({ keywords: e.target.value })} 32 | /> 33 | 34 |
46 | } 47 | > 48 |
49 | 55 |
56 | 57 | ); 58 | }; 59 | 60 | export default EmojiPicker; 61 | -------------------------------------------------------------------------------- /src/pages/flow/components/nodes/DingDingBot/index.tsx: -------------------------------------------------------------------------------- 1 | import { fetchDingDingBotServe } from '@/services/dingdingbotServe'; 2 | import { DingDingBotNodeContent, SymbolMasterDefinition } from '@/types/flow'; 3 | import lodashGet from 'lodash.get'; 4 | 5 | export const DingDingBotSymbol: SymbolMasterDefinition = { 6 | id: 'dingdingbot', 7 | title: '钉钉机器人', 8 | group: '输出节点', 9 | avatar: 'https://techflow.antdigital.dev/dingding.svg', 10 | description: '将接受到的结果以 md 格式发送给钉钉', 11 | defaultContent: { 12 | url: 'https://oapi.dingtalk.com/robot/send?access_token=xxxx', 13 | data: '## ${name} \n ${result}', 14 | title: '钉钉机器人', 15 | }, 16 | schema: { 17 | url: { 18 | type: 'input', 19 | component: 'Input', 20 | title: '钉钉机器人 hooks 地址', 21 | }, 22 | data: { 23 | type: 'input', 24 | component: 'InputArea', 25 | title: 'Markdown', 26 | }, 27 | title: { 28 | type: 'input', 29 | component: 'InputArea', 30 | title: 'Markdown 标题', 31 | }, 32 | }, 33 | run: async (node, vars, { updateParams }) => { 34 | const title = node?.title!.replace(/\{(.+?)\}/g, (match, p1) => { 35 | return lodashGet(vars, p1, match); 36 | }); 37 | const data = node?.data!.replace(/\{(.+?)\}/g, (match, p1) => { 38 | return lodashGet(vars, p1, match); 39 | }); 40 | 41 | const params = { 42 | ...node, 43 | title, 44 | data, 45 | output: undefined, 46 | params: undefined, 47 | }; 48 | updateParams(params); 49 | const res = (await fetchDingDingBotServe(params)) as unknown as { 50 | message: string; 51 | }; 52 | return { 53 | type: 'json', 54 | output: JSON.stringify(res, null, 2), 55 | }; 56 | }, 57 | }; 58 | -------------------------------------------------------------------------------- /src/helpers/flow/workflow.ts: -------------------------------------------------------------------------------- 1 | import { initialFlow } from '@/store/flow/initialState'; 2 | import { 3 | OutputNodeContent, 4 | ResultVariable, 5 | Workflow, 6 | WorkflowMap, 7 | WorkflowMeta, 8 | } from '@/types/flow'; 9 | import { StringTemplate } from '@/utils/StringTemplate'; 10 | import { IFlowBasicNode } from '@ant-design/pro-flow-editor'; 11 | 12 | export const getFlowMeta = (flow: Workflow): WorkflowMeta => { 13 | const { id, createAt, updateAt, meta } = flow; 14 | 15 | return { id, createAt, updateAt, ...meta }; 16 | }; 17 | 18 | export const getSafeFlow = (flows: WorkflowMap, id?: string | null): Workflow => { 19 | if (!id || !flows[id]) return initialFlow; 20 | 21 | return flows[id]; 22 | }; 23 | 24 | export const getSafeFlowMeta = (flows: WorkflowMap, id?: string | null): WorkflowMeta => { 25 | if (!id || !flows[id]) return initialFlow; 26 | 27 | return getFlowMeta(flows[id]); 28 | }; 29 | 30 | export const getResultVariables = (flow: Workflow) => { 31 | const nodes = Object.values(flow.flattenNodes).filter((i) => i.type === 'result'); 32 | 33 | return new StringTemplate(flow.outputTemplate).variableNames.map((v) => { 34 | const variable: ResultVariable = { name: v }; 35 | const node = nodes.find( 36 | (e: IFlowBasicNode) => v === e.data.content.variable, 37 | ); 38 | 39 | // 有节点的情况下,查找source id 40 | if (node) { 41 | variable.nodeId = node.id; 42 | 43 | const edges = Object.values(flow.flattenEdges || {}); 44 | const edge = edges.find((e) => e.target === node.id); 45 | 46 | if (edge) { 47 | variable.sourceId = edge.source; 48 | variable.contentKey = edge.sourceHandle ? edge.sourceHandle : undefined; 49 | } 50 | } 51 | 52 | return variable; 53 | }); 54 | }; 55 | -------------------------------------------------------------------------------- /src/pages/flow/components/nodes/index.ts: -------------------------------------------------------------------------------- 1 | import { SymbolMasterDefinition } from '@/types/flow'; 2 | import { DefaultPreview } from '../DefaultPreview'; 3 | import { DefaultRender } from '../DefaultRender'; 4 | import { AITaskSymbol } from './AITask'; 5 | import { DingDingBotSymbol } from './DingDingBot'; 6 | import { EmbeddingsSymbol } from './Embeddings'; 7 | import { FileReadSymbol } from './FileRead'; 8 | import { NetworkSymbol } from './NetWork'; 9 | import { SDTaskSymbol } from './SD'; 10 | import { StringSymbol } from './String'; 11 | import { VoyQuerySymbol } from './VoyQuery'; 12 | 13 | export const SYMBOL_NODE_LIST = [ 14 | StringSymbol, 15 | AITaskSymbol, 16 | SDTaskSymbol, 17 | NetworkSymbol, 18 | DingDingBotSymbol, 19 | EmbeddingsSymbol, 20 | FileReadSymbol, 21 | VoyQuerySymbol, 22 | ]; 23 | 24 | export const SymbolNodeMasterTypes = Object.fromEntries( 25 | SYMBOL_NODE_LIST.map((item) => [item.id, item.preview || DefaultPreview]).filter(Boolean), 26 | ); 27 | 28 | export const FlowNodeRenderType = Object.fromEntries( 29 | SYMBOL_NODE_LIST.map((item) => [item.id, item.render || DefaultRender]).filter(Boolean), 30 | ); 31 | 32 | export const SymbolNodeRunMap = SYMBOL_NODE_LIST.reduce((pre, current) => { 33 | pre[current.id] = current.run; 34 | return pre; 35 | }, {} as Record['run']>); 36 | 37 | export const SymbolNodeRenderMap = SYMBOL_NODE_LIST.reduce((pre, current) => { 38 | pre[current.id] = current.outputRender; 39 | return pre; 40 | }, {} as Record['outputRender']>); 41 | 42 | export const SymbolSchemaRenderMap = SYMBOL_NODE_LIST.reduce((pre, current) => { 43 | pre[current.id] = current.schema; 44 | return pre; 45 | }, {} as Record['schema']>); 46 | -------------------------------------------------------------------------------- /src/store/flow/selectors/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getFlowMeta, 3 | getFlowNodeById, 4 | getNodeContentById, 5 | getResultVariables, 6 | getSafeFlow, 7 | getSafeFlowNodeById, 8 | getSourceDataOfNode, 9 | } from '@/helpers/flow'; 10 | import { FlowStore } from '@/store/flow'; 11 | import { AITaskContent } from '@/types/flow'; 12 | import { IFlowBasicNode } from '@ant-design/pro-flow-editor'; 13 | 14 | export const flowSelectors = { 15 | currentFlow: (s: FlowStore) => s.flows[s.activeId || ''], 16 | currentFlowMeta: (s: FlowStore) => getSafeFlow(s.flows, s.activeId).meta, 17 | currentFlowSafe: (s: FlowStore) => getSafeFlow(s.flows, s.activeId), 18 | 19 | flowList: (s: FlowStore) => Object.values(s.flows), 20 | flowMetaList: (s: FlowStore) => Object.values(s.flows).map(getFlowMeta), 21 | 22 | getCurrentEdges: (s: FlowStore) => Object.values(flowSelectors.currentFlowSafe(s).flattenEdges), 23 | getNodeById: (id: string) => (s: FlowStore) => { 24 | const flow = flowSelectors.currentFlow(s); 25 | return getFlowNodeById(flow, id); 26 | }, 27 | 28 | getNodeByIdSafe: 29 | (id: string) => 30 | (s: FlowStore): IFlowBasicNode => { 31 | const flow = flowSelectors.currentFlowSafe(s); 32 | return getSafeFlowNodeById(flow, id) as IFlowBasicNode; 33 | }, 34 | 35 | getNodeContentById: 36 | (id: string) => 37 | (s: FlowStore): T => 38 | getNodeContentById(flowSelectors.currentFlowSafe(s), id) as T, 39 | 40 | getResultVariables: (s: FlowStore) => { 41 | return getResultVariables(flowSelectors.currentFlowSafe(s)); 42 | }, 43 | 44 | getSourceDataOfNode: (id: string) => (s: FlowStore) => { 45 | const flow = flowSelectors.currentFlowSafe(s); 46 | return getSourceDataOfNode(id, flow.flattenNodes, flow.flattenEdges); 47 | }, 48 | }; 49 | -------------------------------------------------------------------------------- /src/pages/flow/components/InputSchemaRender/VariableHandle.tsx: -------------------------------------------------------------------------------- 1 | import { getInputVariablesFromMessages } from '@/helpers/prompt'; 2 | import { ChatMessage } from '@/types'; 3 | import { useNodeFieldStyles } from '@ant-design/pro-flow-editor'; 4 | import { Tooltip } from 'antd'; 5 | import { createStyles } from 'antd-style'; 6 | import isEqual from 'fast-deep-equal'; 7 | import { memo } from 'react'; 8 | import { Flexbox } from 'react-layout-kit'; 9 | import { Handle, Position } from 'reactflow'; 10 | 11 | const useStyles = createStyles(({ css }) => ({ 12 | handleContainer: css` 13 | position: absolute; 14 | left: -12px; 15 | padding-top: 8px; 16 | `, 17 | segment: css` 18 | font-weight: normal; 19 | `, 20 | handle: css` 21 | position: relative; 22 | top: 0; 23 | left: 0; 24 | transform: none; 25 | `, 26 | })); 27 | 28 | interface VariableHandleProps { 29 | chatMessages: ChatMessage[]; 30 | handleId: string; 31 | } 32 | export const VariableHandle = memo(({ handleId, chatMessages = [] }) => { 33 | const { styles, cx } = useStyles(); 34 | const { styles: fieldStyles } = useNodeFieldStyles(); 35 | 36 | const inputVariables = getInputVariablesFromMessages(chatMessages); 37 | 38 | return ( 39 | 40 | {inputVariables 41 | .filter((i) => !i.includes(' ')) 42 | .map( 43 | (i, index) => 44 | i && ( 45 | 46 | 52 | 53 | ), 54 | )} 55 | 56 | ); 57 | }, isEqual); 58 | -------------------------------------------------------------------------------- /src/features/FolderPanel/index.tsx: -------------------------------------------------------------------------------- 1 | import { createStyles } from 'antd-style'; 2 | import isEqual from 'fast-deep-equal'; 3 | import { PropsWithChildren, useState } from 'react'; 4 | import { shallow } from 'zustand/shallow'; 5 | 6 | import { DraggablePanel } from '@/components/DraggablePanel'; 7 | import { useSettings } from '@/store'; 8 | 9 | export const useStyles = createStyles(({ css, token }) => ({ 10 | panel: css` 11 | height: 100vh; 12 | color: ${token.colorTextSecondary}; 13 | `, 14 | })); 15 | 16 | export default ({ children }: PropsWithChildren) => { 17 | const { styles } = useStyles(); 18 | const [sessionsWidth, sessionExpandable] = useSettings( 19 | (s) => [s.sessionsWidth, s.sessionExpandable], 20 | shallow, 21 | ); 22 | const [tmpWidth, setWidth] = useState(sessionsWidth || 280); 23 | if (tmpWidth !== sessionsWidth) setWidth(sessionsWidth); 24 | 25 | return ( 26 | { 33 | if (!size) return; 34 | 35 | const nextWidth = typeof size.width === 'string' ? parseInt(size.width) : size.width; 36 | 37 | if (isEqual(nextWidth, sessionsWidth)) return; 38 | 39 | setWidth(nextWidth); 40 | useSettings.setState({ sessionsWidth: nextWidth }); 41 | }} 42 | isExpand={sessionExpandable} 43 | onExpandChange={(expand) => { 44 | useSettings.setState({ 45 | sessionsWidth: expand ? tmpWidth : 0, 46 | sessionExpandable: expand, 47 | }); 48 | }} 49 | className={styles.panel} 50 | style={{ 51 | transition: 'all 0.2s ease-in-out', 52 | }} 53 | > 54 | {children} 55 | 56 | ); 57 | }; 58 | -------------------------------------------------------------------------------- /src/store/session/slices/agent/reducers/agents.ts: -------------------------------------------------------------------------------- 1 | import { ChatAgent, ChatAgentMap } from '@/types'; 2 | import { produce } from 'immer'; 3 | import { Md5 } from 'ts-md5'; 4 | import { v4 as uuid } from 'uuid'; 5 | 6 | interface AddAgentAction { 7 | type: 'addAgent'; 8 | id?: string; 9 | title?: string; 10 | content: string; 11 | avatar?: string; 12 | } 13 | interface RemoveAgentAction { 14 | type: 'removeAgent'; 15 | id: string; 16 | } 17 | interface UpdateAgentData { 18 | type: 'updateAgentData'; 19 | id: string; 20 | key: keyof Omit; 21 | value: any; 22 | } 23 | 24 | export type AgentDispatch = AddAgentAction | RemoveAgentAction | UpdateAgentData; 25 | 26 | export const agentsReducer = (state: ChatAgentMap, payload: AgentDispatch): ChatAgentMap => { 27 | switch (payload.type) { 28 | case 'addAgent': 29 | return produce(state, (draft) => { 30 | const { avatar, id, content, title } = payload; 31 | const newAgent: ChatAgent = { 32 | id: id ?? uuid(), 33 | title, 34 | content, 35 | hash: Md5.hashStr(content), 36 | avatar, 37 | model: 'gpt-3.5-turbo', 38 | }; 39 | 40 | draft[newAgent.id] = newAgent; 41 | }); 42 | 43 | case 'removeAgent': 44 | return produce(state, (draftState) => { 45 | delete draftState[payload.id]; 46 | }); 47 | 48 | case 'updateAgentData': 49 | return produce(state, (draft) => { 50 | const { id, key, value } = payload; 51 | const agent = draft[id]; 52 | if (!agent) return; 53 | 54 | agent[key] = value as never; 55 | 56 | if (key === 'content') { 57 | agent.hash = Md5.hashStr(value); 58 | } 59 | 60 | agent.updateAt = Date.now(); 61 | }); 62 | 63 | default: 64 | throw Error('不存在的 type,请检查代码实现...'); 65 | } 66 | }; 67 | -------------------------------------------------------------------------------- /src/pages/flow/components/InputSchemaRender/TextVariableHandle.tsx: -------------------------------------------------------------------------------- 1 | import { getInputVariablesFromMessages } from '@/helpers/prompt'; 2 | import { useNodeFieldStyles } from '@ant-design/pro-flow-editor'; 3 | import { Tooltip } from 'antd'; 4 | import { createStyles } from 'antd-style'; 5 | import isEqual from 'fast-deep-equal'; 6 | import { memo } from 'react'; 7 | import { Flexbox } from 'react-layout-kit'; 8 | import { Handle, Position } from 'reactflow'; 9 | 10 | const useStyles = createStyles(({ css }) => ({ 11 | handleContainer: css` 12 | position: absolute; 13 | left: -12px; 14 | padding-top: 8px; 15 | `, 16 | segment: css` 17 | font-weight: normal; 18 | `, 19 | handle: css` 20 | position: relative; 21 | top: 0; 22 | left: 0; 23 | transform: none; 24 | `, 25 | })); 26 | 27 | interface VariableHandleProps { 28 | chatMessages: string[]; 29 | handleId: string; 30 | } 31 | export const TextVariableHandle = memo(({ handleId, chatMessages = [] }) => { 32 | const { styles, cx } = useStyles(); 33 | const { styles: fieldStyles } = useNodeFieldStyles(); 34 | 35 | const inputVariables = getInputVariablesFromMessages( 36 | [...chatMessages].map((i) => { 37 | return { 38 | role: 'user', 39 | content: i, 40 | }; 41 | }), 42 | ); 43 | 44 | return ( 45 | 46 | {inputVariables 47 | .filter((i) => !i.includes(' ')) 48 | .map( 49 | (i, index) => 50 | i && ( 51 | 52 | 58 | 59 | ), 60 | )} 61 | 62 | ); 63 | }, isEqual); 64 | -------------------------------------------------------------------------------- /src/components/AgentAvatar/index.tsx: -------------------------------------------------------------------------------- 1 | import { Avatar, AvatarProps } from 'antd'; 2 | import { createStyles } from 'antd-style'; 3 | import { FC } from 'react'; 4 | import { Center } from 'react-layout-kit'; 5 | 6 | const useStyles = createStyles(({ css, token }) => ({ 7 | container: css` 8 | cursor: pointer; 9 | 10 | > * { 11 | cursor: pointer; 12 | } 13 | `, 14 | emoji: css` 15 | border: 1px solid ${token.colorBorder}; 16 | `, 17 | })); 18 | 19 | interface AgentAvatarProps { 20 | avatar?: string; 21 | title?: string; 22 | size?: number; 23 | shape?: AvatarProps['shape']; 24 | background?: string; 25 | } 26 | 27 | const AgentAvatar: FC = ({ 28 | avatar, 29 | title, 30 | size = 40, 31 | shape = 'circle', 32 | background, 33 | }) => { 34 | const { styles, theme } = useStyles(); 35 | 36 | const backgroundColor = background ?? theme.colorBgContainer; 37 | 38 | const isImage = avatar && ['/', 'http', 'data:'].some((i) => avatar.startsWith(i)); 39 | 40 | return ( 41 |
51 | {!avatar ? ( 52 | 53 | {title?.slice(0, 2)} 54 | 55 | ) : isImage ? ( 56 | 57 | ) : ( 58 |
68 | {avatar} 69 |
70 | )} 71 |
72 | ); 73 | }; 74 | 75 | export default AgentAvatar; 76 | -------------------------------------------------------------------------------- /src/layout/FlowLayout/Menu/FlowList/index.tsx: -------------------------------------------------------------------------------- 1 | import { Empty } from 'antd'; 2 | import isEqual from 'fast-deep-equal'; 3 | import Link from 'next/link'; 4 | import { memo, useEffect } from 'react'; 5 | import { Flexbox } from 'react-layout-kit'; 6 | import { shallow } from 'zustand/shallow'; 7 | 8 | import { flowSelectors, useFlowStore } from '@/store/flow'; 9 | import { useSession } from 'next-auth/react'; 10 | import FlowItem from './FlowItem'; 11 | 12 | const FlowList: React.FC<{ 13 | prefixPath: string; 14 | }> = memo((props) => { 15 | const [activeId, isEmpty, loading, queryFlowListForServer, setLoading] = useFlowStore( 16 | (s) => [ 17 | s.activeId, 18 | flowSelectors.flowMetaList(s).length === 0, 19 | s.loading, 20 | s.queryFlowListForServer, 21 | (loading: boolean) => { 22 | s.loading = loading; 23 | }, 24 | ], 25 | shallow, 26 | ); 27 | 28 | const session = useSession(); 29 | 30 | useEffect(() => { 31 | if (session.status.toLowerCase() === 'authenticated') { 32 | setLoading(true); 33 | queryFlowListForServer().finally(() => setLoading(false)); 34 | } 35 | }, [session.status.toLowerCase()]); 36 | 37 | const list = useFlowStore(flowSelectors.flowMetaList, isEqual); 38 | 39 | const { prefixPath = '/flow' } = props; 40 | 41 | return isEmpty ? ( 42 | 47 | ) : ( 48 | <> 49 | {list.map(({ id }) => ( 50 | 51 | 52 | 58 | 59 | 60 | ))} 61 | 62 | ); 63 | }); 64 | 65 | export default FlowList; 66 | -------------------------------------------------------------------------------- /src/layout/style.ts: -------------------------------------------------------------------------------- 1 | import { createGlobalStyle, createStyles } from 'antd-style'; 2 | import { rgba } from 'polished'; 3 | 4 | export const NOTIFICATION_PRIMARY = 'notification-primary-info'; 5 | 6 | export const useStyles = createStyles(({ css, token }) => ({ 7 | bg: css` 8 | overflow-y: scroll; 9 | display: flex; 10 | flex-direction: column; 11 | align-items: center; 12 | 13 | height: 100%; 14 | 15 | background: ${token.colorBgLayout}; 16 | background-image: linear-gradient( 17 | 180deg, 18 | ${token.colorBgContainer} 0%, 19 | rgba(255, 255, 255, 0) 20% 20 | ); 21 | 22 | :has(#ChatLayout, #FlowLayout) { 23 | overflow: hidden; 24 | } 25 | `, 26 | })); 27 | 28 | export const GlobalStyle = createGlobalStyle` 29 | 30 | #__next { 31 | height: 100%; 32 | } 33 | 34 | p { 35 | margin-bottom: 0; 36 | } 37 | 38 | li { 39 | display: block; 40 | } 41 | .ant-popover { 42 | z-index: 1100; 43 | } 44 | 45 | 46 | /* 定义滚动槽的样式 */ 47 | ::-webkit-scrollbar { 48 | width: 6px; 49 | height: 6px; 50 | margin-right: 4px; 51 | background-color: transparent; /* 定义滚动槽的背景色 */ 52 | 53 | &-thumb { 54 | background-color: ${({ theme }) => theme.colorFill}; /* 定义滚动块的背景色 */ 55 | border-radius: 4px; /* 定义滚动块的圆角半径 */ 56 | } 57 | 58 | &-corner { 59 | display: none; 60 | } 61 | } 62 | 63 | .ant-notification .ant-notification-notice.${NOTIFICATION_PRIMARY} { 64 | background: ${(p) => p.theme.colorPrimary}; 65 | box-shadow: 0 6px 16px 0 ${({ theme }) => rgba(theme.colorPrimary, 0.1)}, 66 | 0 3px 6px -4px ${({ theme }) => rgba(theme.colorPrimary, 0.2)}, 67 | 0 9px 28px 8px ${({ theme }) => rgba(theme.colorPrimary, 0.1)}; 68 | 69 | .anticon { 70 | color: ${(p) => p.theme.colorTextLightSolid} 71 | } 72 | 73 | .ant-notification-notice-message { 74 | margin-bottom: 0; 75 | padding-right: 0; 76 | color: ${(p) => p.theme.colorTextLightSolid}; 77 | } 78 | } 79 | 80 | `; 81 | -------------------------------------------------------------------------------- /src/pages/_document.page.tsx: -------------------------------------------------------------------------------- 1 | import { extractStaticStyle, StyleProvider } from 'antd-style'; 2 | import Document, { DocumentContext, Head, Html, Main, NextScript } from 'next/document'; 3 | 4 | class MyDocument extends Document { 5 | static async getInitialProps(ctx: DocumentContext) { 6 | const page = await ctx.renderPage({ 7 | enhanceApp: (App) => (props) => 8 | ( 9 | 10 | 11 | 12 | ), 13 | }); 14 | 15 | const styles = extractStaticStyle(page.html).map((item) => item.style); 16 | 17 | const initialProps = await Document.getInitialProps(ctx); 18 | 19 | return { 20 | ...initialProps, 21 | styles: ( 22 | <> 23 | {initialProps.styles} 24 | {styles} 25 | 26 | ), 27 | }; 28 | } 29 | 30 | render() { 31 | return ( 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 |
49 | 50 | 51 | 52 | ); 53 | } 54 | } 55 | 56 | export default MyDocument; 57 | -------------------------------------------------------------------------------- /src/store/session/slices/chat/reducers/chats.ts: -------------------------------------------------------------------------------- 1 | import { ChatContext, ChatContextMap } from '@/types'; 2 | import { produce } from 'immer'; 3 | 4 | /** 5 | * @title 添加会话 6 | */ 7 | interface AddChats { 8 | /** 9 | * @param type - 操作类型 10 | * @default 'addChat' 11 | */ 12 | type: 'addChat'; 13 | /** 14 | * @param session - 会话信息 15 | */ 16 | chat: ChatContext; 17 | } 18 | 19 | /** 20 | * @title 更新会话聊天上下文 21 | */ 22 | interface UpdateSessionChatContext { 23 | /** 24 | * @param type - 操作类型 25 | * @default 'updateSessionChatContext' 26 | */ 27 | type: 'updateSessionChatContext'; 28 | /** 29 | * 会话 ID 30 | */ 31 | id: string; 32 | /** 33 | * @param key - 聊天上下文的键值 34 | */ 35 | key: keyof ChatContext; 36 | /** 37 | * @param value - 聊天上下文的值 38 | */ 39 | value: any; 40 | } 41 | 42 | export type HistoryDispatch = 43 | | AddChats 44 | | UpdateSessionChatContext 45 | | { type: 'removeChat'; id: string } 46 | | { type: 'updateChatTitle'; id: string; title: string } 47 | | { type: 'switchChat'; id: string }; 48 | 49 | export const sessionsReducer = ( 50 | state: ChatContextMap, 51 | payload: HistoryDispatch, 52 | ): ChatContextMap => { 53 | switch (payload.type) { 54 | case 'addChat': 55 | return produce(state, (draft) => { 56 | draft[payload.chat.id] = payload.chat; 57 | }); 58 | 59 | case 'removeChat': 60 | return produce(state, (draft) => { 61 | delete draft[payload.id]; 62 | }); 63 | 64 | case 'updateChatTitle': 65 | return produce(state, (draft) => { 66 | const chat = draft[payload.id]; 67 | if (!chat) return; 68 | 69 | chat.title = payload.title; 70 | }); 71 | 72 | case 'updateSessionChatContext': 73 | return produce(state, (draft) => { 74 | const chat = draft[payload.id]; 75 | if (!chat) return; 76 | 77 | // @ts-ignore 78 | chat[payload.key] = payload.value; 79 | }); 80 | 81 | default: 82 | return state; 83 | } 84 | }; 85 | -------------------------------------------------------------------------------- /src/layout/FlowLayout/Menu/FlowList/Actions.tsx: -------------------------------------------------------------------------------- 1 | import { DeleteOutlined, EditOutlined, MoreOutlined } from '@ant-design/icons'; 2 | import { App, Dropdown } from 'antd'; 3 | import { FC, memo } from 'react'; 4 | import { Flexbox } from 'react-layout-kit'; 5 | 6 | import { IconAction } from '@/components/IconAction'; 7 | import { useFlowStore } from '@/store/flow'; 8 | import { shallow } from 'zustand/shallow'; 9 | 10 | interface ActionsProps { 11 | visible: boolean; 12 | id: string; 13 | } 14 | 15 | const Actions: FC = memo(({ visible, id }) => { 16 | const { modal } = App.useApp(); 17 | 18 | const [removeFlow] = useFlowStore((s) => [s.removeFlow], shallow); 19 | 20 | return ( 21 | 22 | {id === 'default' ? null : ( 23 | <> 24 | } /> 25 | 26 | , 32 | label: '删除任务流', 33 | key: 'delete', 34 | danger: true, 35 | onClick: () => { 36 | modal.confirm({ 37 | type: 'warning', 38 | title: '确认删除任务流?', 39 | content: '删除该角色的同时和该角色的会话断开关联', 40 | centered: true, 41 | okButtonProps: { danger: true }, 42 | okText: '删除', 43 | cancelText: '我再想想', 44 | onOk: () => { 45 | removeFlow(id); 46 | }, 47 | }); 48 | }, 49 | }, 50 | ].filter(Boolean), 51 | }} 52 | > 53 | } /> 54 | 55 | 56 | )} 57 | 58 | ); 59 | }); 60 | 61 | export default Actions; 62 | -------------------------------------------------------------------------------- /src/store/useHighlight.ts: -------------------------------------------------------------------------------- 1 | import { getHighlighter, Highlighter, Theme } from 'shiki-es'; 2 | import { shallow } from 'zustand/shallow'; 3 | import { createWithEqualityFn } from 'zustand/traditional'; 4 | 5 | export interface ShikiSyntaxTheme { 6 | /** 7 | * @title 暗色模式主题 8 | */ 9 | dark: Theme; 10 | /** 11 | * @title 亮色模式主题 12 | */ 13 | light: Theme; 14 | } 15 | 16 | const THEME: ShikiSyntaxTheme = { 17 | dark: 'one-dark-pro', 18 | light: 'github-light', 19 | }; 20 | 21 | export const languageMap = [ 22 | 'javascript', 23 | 'js', 24 | 'jsx', 25 | 'json', 26 | 'markdown', 27 | 'md', 28 | 'less', 29 | 'css', 30 | 'typescript', 31 | 'ts', 32 | 'tsx', 33 | 'diff', 34 | 'bash', 35 | ] as const; 36 | 37 | /** 38 | * @title 代码高亮的存储对象 39 | */ 40 | interface Store { 41 | /** 42 | * @title 高亮器对象 43 | */ 44 | highlighter?: Highlighter; 45 | /** 46 | * @title 初始化高亮器对象 47 | * @returns 初始化 Promise 对象 48 | */ 49 | initHighlighter: () => Promise; 50 | /** 51 | * @title 将代码转化为 HTML 字符串 52 | * @param text - 代码文本 53 | * @param language - 代码语言 54 | * @param isDarkMode - 是否为暗黑模式 55 | * @returns HTML 字符串 56 | */ 57 | codeToHtml: (text: string, language: string, isDarkMode: boolean) => string; 58 | } 59 | 60 | export const useHighlight = createWithEqualityFn( 61 | (set, get) => ({ 62 | highlighter: undefined, 63 | initHighlighter: async () => { 64 | if (!get().highlighter) { 65 | const highlighter = await getHighlighter({ 66 | langs: languageMap as any, 67 | themes: Object.values(THEME), 68 | }); 69 | set({ highlighter }); 70 | } 71 | }, 72 | 73 | codeToHtml: (text, language, isDarkMode) => { 74 | const { highlighter } = get(); 75 | if (!highlighter) return ''; 76 | 77 | try { 78 | return highlighter?.codeToHtml(text, { 79 | lang: language, 80 | theme: isDarkMode ? THEME.dark : THEME.light, 81 | }); 82 | } catch (e) { 83 | return text; 84 | } 85 | }, 86 | }), 87 | shallow, 88 | ); 89 | -------------------------------------------------------------------------------- /src/store/flow/reducers/flows.ts: -------------------------------------------------------------------------------- 1 | import { Workflow, WorkflowMap } from '@/types/flow'; 2 | import { produce } from 'immer'; 3 | 4 | export type FlowsDispatch = 5 | | { type: 'addFlow'; flow: Workflow } 6 | | { type: 'deleteFlow'; id: string } 7 | | { type: 'updateFlow'; id: string; flow: Partial; updateDate?: boolean } 8 | | { 9 | type: 'updateFlowState'; 10 | id: string; 11 | state: Partial; 12 | updateDate?: boolean; 13 | } 14 | | { 15 | type: 'updateFlowMeta'; 16 | id: string; 17 | meta: Partial; 18 | }; 19 | 20 | export const flowsReducer = (state: WorkflowMap, payload: FlowsDispatch): WorkflowMap => { 21 | switch (payload.type) { 22 | case 'addFlow': 23 | return produce(state, (draftState) => { 24 | const { flow } = payload; 25 | draftState[flow.id] = payload.flow; 26 | }); 27 | 28 | case 'deleteFlow': 29 | return produce(state, (draftState) => { 30 | delete draftState[payload.id]; 31 | }); 32 | 33 | case 'updateFlow': 34 | return produce(state, (draftState) => { 35 | const flow = draftState[payload.id]; 36 | delete payload.flow.id; 37 | if (flow) { 38 | Object.assign( 39 | flow, 40 | payload.flow, 41 | payload.updateDate !== false ? { updateAt: Date.now() } : {}, 42 | ); 43 | } 44 | }); 45 | 46 | case 'updateFlowState': 47 | return produce(state, (draftState) => { 48 | const flow = draftState[payload.id]; 49 | if (!flow) return; 50 | 51 | flow.state = { ...flow.state, ...payload.state }; 52 | 53 | if (payload.updateDate !== false) { 54 | flow.updateAt = Date.now(); 55 | } 56 | }); 57 | 58 | case 'updateFlowMeta': 59 | return produce(state, (draftState) => { 60 | const flow = draftState[payload.id]; 61 | if (!flow) return; 62 | 63 | flow.meta = { ...flow.meta, ...payload.meta }; 64 | 65 | flow.updateAt = Date.now(); 66 | }); 67 | 68 | default: 69 | return state; 70 | } 71 | }; 72 | -------------------------------------------------------------------------------- /src/components/CopyButton/index.tsx: -------------------------------------------------------------------------------- 1 | import { useCopied } from '@/hooks/useCopied'; 2 | import { CheckOutlined, CopyOutlined } from '@ant-design/icons'; 3 | import { Button, Tooltip, TooltipProps } from 'antd'; 4 | import { useTheme } from 'antd-style'; 5 | import { ButtonSize, ButtonType } from 'antd/es/button'; 6 | import copy from 'copy-to-clipboard'; 7 | import { ReactNode } from 'react'; 8 | 9 | /** 10 | * @title 复制按钮属性 11 | */ 12 | interface CopyButtonProps { 13 | /** 14 | * @title 复制的内容 15 | */ 16 | content: string; 17 | /** 18 | * @title 自定义类名 19 | */ 20 | className?: string; 21 | /** 22 | * @title Tooltip 提示框位置 23 | * @enum ['top', 'left', 'right', 'bottom', 'topLeft', 'topRight', 'bottomLeft', 'bottomRight', 'leftTop', 'leftBottom', 'rightTop', 'rightBottom'] 24 | * @enumNames ['上', '左', '右', '下', '左上', '右上', '左下', '右下', '左上', '左下', '右上', '右下'] 25 | * @default 'top' 26 | */ 27 | placement?: TooltipProps['placement']; 28 | type?: ButtonType; 29 | size?: ButtonSize; 30 | children?: ReactNode; 31 | render?: (props: { handleCopy: () => void }) => ReactNode; 32 | } 33 | 34 | const CopyButton = ({ 35 | content, 36 | className, 37 | placement = 'right', 38 | type, 39 | size, 40 | render, 41 | }: CopyButtonProps) => { 42 | const { copied, setCopied } = useCopied(); 43 | 44 | const theme = useTheme(); 45 | 46 | const handleCopy = () => { 47 | copy(content); 48 | setCopied(); 49 | }; 50 | 51 | const children = render ? ( 52 | render({ handleCopy }) 53 | ) : ( 54 | 79 | 80 | 83 | 84 | )} 85 | 86 | 87 | ); 88 | }; 89 | 90 | export default MessageInput; 91 | -------------------------------------------------------------------------------- /src/components/Highlighter/Highlighter.tsx: -------------------------------------------------------------------------------- 1 | import { Loading3QuartersOutlined as Loading } from '@ant-design/icons'; 2 | import { memo } from 'react'; 3 | import { Center } from 'react-layout-kit'; 4 | import { shallow } from 'zustand/shallow'; 5 | 6 | import { useHighlight } from '@/store/useHighlight'; 7 | 8 | import { useSettings } from '@/store/settings'; 9 | import { Prism } from './Prism'; 10 | 11 | import { createStyles } from 'antd-style'; 12 | import type { HighlighterProps } from './index'; 13 | 14 | const useStyles = createStyles(({ css, token, cx, prefixCls }) => { 15 | const prefix = `${prefixCls}-highlighter`; 16 | 17 | return { 18 | shiki: cx( 19 | `${prefix}-shiki`, 20 | css` 21 | .shiki { 22 | overflow-x: scroll; 23 | 24 | .line { 25 | font-family: 'Fira Code', 'Fira Mono', Menlo, Consolas, 'DejaVu Sans Mono', monospace; 26 | } 27 | } 28 | `, 29 | ), 30 | prism: css` 31 | pre { 32 | overflow-x: scroll; 33 | } 34 | `, 35 | 36 | loading: css` 37 | position: absolute; 38 | top: 8px; 39 | right: 12px; 40 | color: ${token.colorTextTertiary}; 41 | `, 42 | }; 43 | }); 44 | 45 | type SyntaxHighlighterProps = Pick; 46 | 47 | const SyntaxHighlighter = memo(({ children, language }) => { 48 | const { styles, theme } = useStyles(); 49 | const appearance = useSettings((s) => s.appearance); 50 | const isDarkMode = appearance === 'dark'; 51 | 52 | const [codeToHtml, isLoading] = useHighlight((s) => [s.codeToHtml, !s.highlighter], shallow); 53 | 54 | return ( 55 | <> 56 | {isLoading ? ( 57 |
58 | 59 | {children} 60 | 61 |
62 | ) : ( 63 |
69 | )} 70 | 71 | {isLoading && ( 72 |
73 | 74 | shiki 着色器准备中... 75 |
76 | )} 77 | 78 | ); 79 | }); 80 | 81 | export default SyntaxHighlighter; 82 | -------------------------------------------------------------------------------- /src/components/Chat/MessageModal.tsx: -------------------------------------------------------------------------------- 1 | import { AimOutlined } from '@ant-design/icons'; 2 | import { Modal } from 'antd'; 3 | import { createStyles } from 'antd-style'; 4 | import { memo } from 'react'; 5 | import { Flexbox } from 'react-layout-kit'; 6 | import useControlledState from 'use-merge-value'; 7 | 8 | import Markdown from '@/components/Markdown'; 9 | import MessageInput from '@/components/MessageInput'; 10 | 11 | const useStyles = createStyles(({ css, prefixCls }) => ({ 12 | modal: css` 13 | height: 70%; 14 | .${prefixCls}-modal-header { 15 | margin-bottom: 24px; 16 | } 17 | `, 18 | body: css` 19 | overflow-y: scroll; 20 | max-height: 70vh; 21 | `, 22 | })); 23 | 24 | interface MessageModalProps { 25 | open?: boolean; 26 | onOpenChange?: (open: boolean) => void; 27 | editing?: boolean; 28 | onEditingChange?: (editing: boolean) => void; 29 | onChange: (text: string) => void; 30 | value: string; 31 | } 32 | 33 | const MessageModal = memo( 34 | ({ editing, open, onOpenChange, onEditingChange, value, onChange }) => { 35 | const { styles } = useStyles(); 36 | 37 | const [isEdit, setTyping] = useControlledState(false, { 38 | value: editing, 39 | onChange: onEditingChange, 40 | }); 41 | 42 | const [expand, setExpand] = useControlledState(false, { 43 | value: open, 44 | onChange: onOpenChange, 45 | }); 46 | 47 | return ( 48 | setExpand(false)} 52 | okText={'编辑'} 53 | onOk={() => { 54 | setTyping(true); 55 | }} 56 | footer={isEdit ? null : undefined} 57 | cancelText={'关闭'} 58 | title={ 59 | 60 | 61 | 提示词 62 | 63 | } 64 | className={styles.modal} 65 | > 66 | {isEdit ? ( 67 | { 69 | setTyping(false); 70 | onChange?.(text); 71 | }} 72 | onCancel={() => setTyping(false)} 73 | defaultValue={value} 74 | height={400} 75 | /> 76 | ) : ( 77 | {value} 78 | )} 79 | 80 | ); 81 | }, 82 | ); 83 | 84 | export default MessageModal; 85 | -------------------------------------------------------------------------------- /src/pages/api/workflow.api.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next'; 2 | import { getServerSession } from 'next-auth'; 3 | import { authOptions } from './auth/[...nextauth].api'; 4 | import { WorkFlowDataBase, dataBase } from './db'; 5 | 6 | export default async function handler(request: NextApiRequest, response: NextApiResponse) { 7 | const session = await getServerSession(request, response, authOptions); 8 | if (!session?.user?.email) { 9 | return response.status(200).json({ 10 | success: false, 11 | data: [], 12 | error: 'Not logged in', 13 | }); 14 | } 15 | if (request.method === 'GET') { 16 | try { 17 | const workflowList = await dataBase 18 | .selectFrom('workflow') 19 | .select(['id', 'workflow']) 20 | .where('workflow.user_id', '=', session?.user?.email) 21 | .execute(); 22 | 23 | response.status(200).json({ 24 | success: true, 25 | data: workflowList, 26 | }); 27 | } catch (error) { 28 | return response.status(200).json({ 29 | success: false, 30 | data: [], 31 | error: 'serve error' + error, 32 | }); 33 | } 34 | return; 35 | } 36 | 37 | if (request.method === 'POST') { 38 | const workflow = request.body as WorkFlowDataBase; 39 | workflow.user_id = session?.user?.email; 40 | await dataBase.insertInto('workflow').values(workflow).execute(); 41 | return response.status(200).json({ 42 | success: true, 43 | data: workflow, 44 | }); 45 | return; 46 | } 47 | 48 | if (request.method === 'DELETE') { 49 | const workflow = request.query as WorkFlowDataBase; 50 | await dataBase 51 | .deleteFrom('workflow') 52 | .where('id', '=', workflow.id?.toString() || '') 53 | .execute(); 54 | return response.status(200).json({ 55 | success: true, 56 | data: {}, 57 | }); 58 | return; 59 | } 60 | 61 | if (request.method === 'PUT') { 62 | const workflow = request.body as WorkFlowDataBase; 63 | const result = await dataBase 64 | .updateTable('workflow') 65 | .set(workflow) 66 | .where('id', '=', workflow.id?.toString() || '') 67 | .execute(); 68 | return response.status(200).json({ 69 | success: true, 70 | data: { result }, 71 | }); 72 | return; 73 | } 74 | 75 | return response.status(200).json({ 76 | success: false, 77 | data: [], 78 | }); 79 | } 80 | -------------------------------------------------------------------------------- /src/store/chat/index.ts: -------------------------------------------------------------------------------- 1 | import { produce } from 'immer'; 2 | import { optionalDevtools } from 'zustand-utils'; 3 | import { createWithEqualityFn } from 'zustand/traditional'; 4 | import { StateCreator } from 'zustand/vanilla'; 5 | 6 | import { initialAgent } from '@/store/session/initialState'; 7 | import { ChatAgent, ChatContext, ChatMessage } from '@/types'; 8 | import { Compressor } from '@/utils/compass'; 9 | import { genChatMessages } from '@/utils/genChatMessages'; 10 | import { shallow } from 'zustand/shallow'; 11 | 12 | interface ChatState { 13 | agent: ChatAgent; 14 | chat: ChatContext; 15 | } 16 | 17 | export const initChat: ChatContext = { 18 | id: 'new', 19 | messages: [], 20 | updateAt: -1, 21 | createAt: -1, 22 | }; 23 | 24 | export const initialState: ChatState = { 25 | chat: initChat, 26 | agent: initialAgent, 27 | }; 28 | 29 | interface ChatStore extends ChatState { 30 | genCompassedMessages: () => string; 31 | resetChat: () => void; 32 | updateAgentContent: (content: string) => void; 33 | setMessages: (messages: ChatMessage[]) => void; 34 | } 35 | 36 | const createStore: StateCreator = (set, get) => ({ 37 | ...initialState, 38 | genCompassedMessages: () => { 39 | const { agent, chat } = get(); 40 | 41 | const compassedMsg = genChatMessages({ messages: chat.messages, systemRole: agent.content }); 42 | 43 | return `/share?messages=${Compressor.compress(JSON.stringify(compassedMsg))}`; 44 | }, 45 | 46 | updateAgentContent: (content) => { 47 | set( 48 | produce((draft) => { 49 | draft.agent.content = content; 50 | }), 51 | ); 52 | }, 53 | 54 | setMessages: (messages) => { 55 | set( 56 | produce((draft) => { 57 | draft.chat.messages = messages; 58 | }), 59 | false, 60 | 'setMessages', 61 | ); 62 | }, 63 | 64 | resetChat: () => { 65 | set(initialState); 66 | }, 67 | }); 68 | 69 | export const useChatStore = createWithEqualityFn()( 70 | optionalDevtools(true)( 71 | createStore, 72 | // persist(createStore, { 73 | // name: 'CHAT_URL', 74 | // partialize: ((s: ChatState) => ({ 75 | // systemRole: s.agent.content, 76 | // })) as any, 77 | // storage: createHashStorage(), 78 | // }), 79 | { name: 'TechFlow_CHAT' }, 80 | ), 81 | shallow, 82 | ); 83 | 84 | export const modelSel = (s: ChatStore) => s.agent.model; 85 | -------------------------------------------------------------------------------- /src/components/Settings/index.tsx: -------------------------------------------------------------------------------- 1 | import { DownloadOutlined, UploadOutlined } from '@ant-design/icons'; 2 | import { Button, Form, Segmented, Upload } from 'antd'; 3 | import { useEffect } from 'react'; 4 | import { Flexbox } from 'react-layout-kit'; 5 | 6 | import { useImportAndExport } from '@/hooks/useImportAndExport'; 7 | import { useSettings } from '@/store/settings'; 8 | import { useStyles } from './style'; 9 | 10 | export default () => { 11 | const settings = useSettings(); 12 | const { styles } = useStyles(); 13 | const [form] = Form.useForm(); 14 | const { exportConfigFile, handleImport } = useImportAndExport(); 15 | 16 | useEffect(() => { 17 | form.setFieldsValue(settings); 18 | }, [settings]); 19 | 20 | return ( 21 | 22 |
{ 25 | useSettings.setState(changedValues); 26 | }} 27 | labelCol={{ span: 8 }} 28 | wrapperCol={{ span: 16 }} 29 | style={{ textAlign: 'right' }} 30 | > 31 | 32 | 38 | 39 | 40 | 46 | 47 |
48 | 49 | 50 | 56 | 65 | 66 | 67 | 76 | 77 | 78 |
79 | ); 80 | }; 81 | -------------------------------------------------------------------------------- /src/components/Logo/Logo.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image'; 2 | 3 | export const logoImg = 4 | 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAACXBIWXMAAAsTAAALEwEAmpwYAAAGqElEQVR4nO2aaWwUZRzG3z3b3VJaQdxZowT9IKhRv5jSxBZTI4liNI24aiQ2JdBru73ZXWhtCxJQEVGUI51dguEwaW0KBEoPeh/bu93dVlqR0s609Yga9QNNINC/eacH3c47uy3b7UJ2n+SXvHmPvP/nmZmd6UwR8ssvv/zyy6+lV36tFO3qfYUDt31K+qZgcVZHhzirAyZpb8N9yFckTWnIkaY0gAO6hmzkK5LHl5vlCeXgQHy5GfmKAmOLPwjcWgKzUcSWaJAvKSD23A+KmELABGy9cAb5miSZbRpxZjtgcBv5miRpFo0kzQJT+GAAukbN9B0At5GvSZ5UrZEl1wBGrqv2xQAqNbLESsDgNnoYtdL8Z7CKZt5X0+whimbOqmm2aL6ovr1uWflFH2BweyFrub1M7FeUmdHgGpbeeT5I1SZmp9rE/qs2seBNKJr9R00zRlQEkiXxrjr1WxBlYsq9bZwHzZSpC8aUnnUPIFKbmGKvmxUMgS3yqH+qYGSz1026wsxGeywANc20ed2gS5gWj5h/0sw8rqaZCe8bdHUZMBOrTg5Rix6AyjwS5XVz84QqYF9d2uufZmHFNzdAuW8AAvf2exTl/p9hxeFBFwGMbF78AMyMRuCa4wpDRvuSotjbj093cgBmRrNkAQQfuAbIYPUKyw9c80AA+SBGRutmZLAXI4N9FBntt3Hi+CiTNpNl20Ck755DF4gyWkCU2jAJbu/oJMwTYn7rZTk2Yk2zzsjbnAfOS++7nDenyrI+hfS2dmSwwVwC914lbsYVi4uboQNEKXUQkt8EL5wf4gjJa+T6uDGHuSQWsr5LIICrvPqnaEXGnjVk88aeNaIdPWMifQ+QUHz6E3EzcWaHI6lNEJrXCG+zdyH6V+DA7dDcRm6MN9/N9aSacK1CPkT67lFk7FzN8y/a0VWLExVCsaePHEBGmyPaanjp3NBM8dO8WHKDG+PNd3M9MYA9fYI+OLK6GhAC0axTv2uDKKsTnKHY00sOIL3FkcRKCKsc4xnAfXiMN9/N9eQAep16waDMzoh7Rz+z/fPpF5RCKHbbiZtJ0podSSyHsMpRgoFRbow33831xAB22516wYgy2/bfCyC99bQ4oxWcoci3kgNIbXIkoUzYQEIZf76b64kB5FudesGIMlpOzQQgSW/+TpJuAWco83uIm/E+ccWXQlgFwUDFKDfGm+/melJNuFZXfiTplsMzAYjTm7a4OjWVed3kAHR1jsRdhLCKEYKBEW6MN9/N9cQA8rpdXmri1OYPZ/0IVgRJUhrGJCmNIIQyl3zPnX6zO8P2C/DyRf6vOO7DY7z5bq4nBpDbJeiDQ9c4guI7Hd8eSZLr3pEm192V6uqBRNAnneQAtFWOxF2C1QebeQZWH2wCWfwl/nw315NqwrUK+ZDq6u9IdPVvEZ+FxNraj6XamnFpci3MJSinnRxA4hVH4stBFlsMz56wwpsD4xzrzFaujxubO9/N9cQActp59U9xU6yt2eL0aVieWL1Wqq06K0uqGpdpq2GaoBzyQ0dgchXIEyocibsM8q0lII8pnAS340r584SY5/qAZIEzIKdtpm6OpKpxaVL1GXnylWfQvJVhUUgTK8PlSVXR+CtO6H77IdJmwdktII8vI3B5smgMbhPnOMP1+uXZrcQAQoz1A4qYH0EZU3gXe8BekKf+HFYdHQRFciUExJUuKQpdJVDHbhADCDXUWII+Og0Yt427fiEyGcLyXc0QmFQGAdsvepTARHzkmwXNc2fArnqLIqYIMGixRJnYN4Q2fNAINtR3Bmy7AAHbzv+3eAGcGH7O28bmiyKtZhD/35Esoax/Ub8KUSZmxNvmXEHRw2NSXc3E1G2vcPECQNxlkOdtg6545Mv+uunnfXG6xfk9f6FadeSPZWqaHXxwjz7DiPXdN6defPyOtH3L0GJLRTPPUzTz130VSTNAHR8E6tgvZI4PCr7qnsep/7c0t+/6zKtzvU2LPKXHTjBPq01M64KLPNIPT3xWD2sN38M6/UkHcB8eo44OLNx8wXCvJLePnTFvsJUiTZHE85/KzWy02sQWUiaGoWj2ltNCC4ZAdagHXnsvBjZt2kQEj6m+7uHmOj/a7C38Y/fo0WGLct9AG9ppm7j30cTWgYydIehB1caNG8EZbn4xOrsoj72eVFRUFDjjvowb7K1op/119DAoIiLiTmRkJJCIiNwwgQz2IpcYbWeQwXYQGa1J3Mech0nh4eH0+vXr74SHh8NspvoKvF2fX3755ZdffvmFfEb/A8o6QTLJzyjvAAAAAElFTkSuQmCC'; 5 | export const LogoImage = () => {'TechFlow'}; 6 | -------------------------------------------------------------------------------- /src/types/flow/symbols.ts: -------------------------------------------------------------------------------- 1 | import { IFlowBasicNode } from '@ant-design/pro-flow-editor'; 2 | import { FC } from 'react'; 3 | import { XYPosition } from 'reactflow'; 4 | 5 | export interface OutputNodeContent { 6 | params?: Record; 7 | data: string; 8 | url: string; 9 | outputType?: 'text' | 'img' | 'json'; 10 | variable?: string; 11 | output?: string; 12 | } 13 | 14 | export type DingDingBotNodeContent = { 15 | title?: string; 16 | data: string; 17 | url: string; 18 | }; 19 | 20 | export type FileReadNodeContent = { 21 | file: string; 22 | type: 'pdf' | 'txt' | 'csv'; 23 | }; 24 | 25 | type ActionType = { 26 | // 更新当前 节点 的 loading 的状态 27 | updateLoading: (loading: boolean) => void; 28 | // 当前节点实例 29 | node: IFlowBasicNode; 30 | // 停止的 AbortController 实例 31 | abortController?: AbortController; 32 | // 更新当前节点的参数 33 | updateParams: (params: Record) => void; 34 | // 更新当前节点的输出 35 | updateOutput: (output: { output: string; type: OutputNodeContent['outputType'] }) => void; 36 | }; 37 | 38 | export type OnCreateNode = ( 39 | node: { id: string; position: XYPosition; type: string }, 40 | active: { source: 'agent' | 'symbol' } & Record, 41 | ) => T; 42 | 43 | export interface SymbolMasterDefinition { 44 | id: string; 45 | title: string; 46 | description: string; 47 | group: string; 48 | avatar: string; 49 | preview?: FC; 50 | render?: FC; 51 | defaultContent: Content; 52 | schema: Record< 53 | string, 54 | { 55 | type: 'input' | 'output'; 56 | title: string; 57 | valueContainer?: boolean; 58 | hideContainer?: boolean; 59 | valueKey?: string[]; 60 | component?: 61 | | 'Input' 62 | | 'Var' 63 | | 'VarList' 64 | | 'Segmented' 65 | | 'InputArea' 66 | | 'SystemRole' 67 | | 'TaskPromptsInput' 68 | | 'Upload'; 69 | options?: { 70 | label: string; 71 | value: string; 72 | }[]; 73 | handles?: { 74 | source?: true | string; 75 | target?: true | string; 76 | }; 77 | } 78 | >; 79 | onCreateNode?: OnCreateNode>; 80 | run?: ( 81 | content: Content, 82 | vars: Record, 83 | options: ActionType, 84 | ) => Promise<{ 85 | type: OutputNodeContent['outputType']; 86 | output: string; 87 | code?: string; 88 | message?: string; 89 | }>; 90 | outputRender?: (output: string, node: Content) => React.ReactNode; 91 | } 92 | -------------------------------------------------------------------------------- /src/pages/flow/components/FlowView/useDragAndDrop.ts: -------------------------------------------------------------------------------- 1 | import { createNode } from '@/helpers/flow'; 2 | import { useFlowStore } from '@/store/flow'; 3 | import { IFlowBasicNode } from '@ant-design/pro-flow-editor'; 4 | import { useDndMonitor } from '@dnd-kit/core'; 5 | import { useTheme } from 'antd-style'; 6 | import { PointerEvent, useState } from 'react'; 7 | import { useReactFlow } from 'reactflow'; 8 | import { v4 as uuid } from 'uuid'; 9 | import { shallow } from 'zustand/shallow'; 10 | import { SYMBOL_NODE_LIST } from '../nodes'; 11 | 12 | export const useDropNodeOnCanvas = () => { 13 | const [addNode] = useFlowStore((s) => { 14 | return [s.editor.addNode]; 15 | }, shallow); 16 | 17 | const instance = useReactFlow(); 18 | 19 | const [isOver, setOver] = useState(false); 20 | const theme = useTheme(); 21 | 22 | useDndMonitor({ 23 | onDragMove({ over, activatorEvent, delta }) { 24 | const event = activatorEvent as unknown as PointerEvent; 25 | setOver((event.clientX + delta.x || 0) > (over?.rect.left || 0)); 26 | }, 27 | onDragCancel() { 28 | setOver(false); 29 | }, 30 | onDragEnd({ over, activatorEvent, active, delta }) { 31 | if (!over) return; 32 | const event = activatorEvent as unknown as PointerEvent; 33 | 34 | const left = event.clientX + delta.x - over.rect.left; 35 | const top = event.clientY + delta.y - over.rect.top; 36 | 37 | const zoom = instance.getZoom(); 38 | const position = instance.project({ 39 | // 让 x 位置在节点中间 40 | x: left - (theme.aiTaskNodeWidth / 2) * zoom, 41 | // 在 drag 部分对应为 40 42 | y: top - 80 * zoom, 43 | }); 44 | 45 | const id = uuid(); 46 | // 基于 type 查找对应的 Symbol 类型 47 | const symbolNode = SYMBOL_NODE_LIST.find((item) => item.id === active.data.current?.type); 48 | if (symbolNode) { 49 | const node: IFlowBasicNode = symbolNode.onCreateNode 50 | ? // 如果有定义 onCreateNode ,则使用 onCreateNode 创建节点 51 | symbolNode.onCreateNode( 52 | { id, position, type: symbolNode.id }, 53 | active.data.current as any, 54 | ) 55 | : // 否则使用默认的创建节点方法 56 | createNode({ id, position, type: symbolNode.id }, symbolNode.defaultContent, { 57 | title: symbolNode.title, 58 | description: symbolNode.description, 59 | avatar: symbolNode.avatar, 60 | }); 61 | 62 | addNode(node); 63 | } 64 | setOver(false); 65 | }, 66 | }); 67 | 68 | return isOver; 69 | }; 70 | -------------------------------------------------------------------------------- /src/utils/VersionController.test.ts: -------------------------------------------------------------------------------- 1 | import { Migration, MigrationData, VersionController } from './VersionController'; 2 | 3 | class TestMigration0 implements Migration { 4 | version = 0; 5 | 6 | migrate(data: MigrationData): MigrationData { 7 | return data; 8 | } 9 | } 10 | class TestMigration1 implements Migration { 11 | version = 1; 12 | 13 | migrate(data: MigrationData): MigrationData { 14 | return { 15 | state: { 16 | ...data.state, 17 | value1: data.state.value * 2, 18 | }, 19 | version: this.version, 20 | }; 21 | } 22 | } 23 | 24 | class TestMigration2 implements Migration { 25 | version = 2; 26 | 27 | migrate(data: MigrationData): MigrationData { 28 | return { 29 | state: { 30 | ...data.state, 31 | value2: data.state.value1 * 2, 32 | }, 33 | version: this.version, 34 | }; 35 | } 36 | } 37 | 38 | describe('VersionController', () => { 39 | let migrations; 40 | let versionController: VersionController; 41 | 42 | beforeEach(() => { 43 | migrations = [TestMigration0, TestMigration1, TestMigration2]; 44 | versionController = new VersionController(migrations); 45 | }); 46 | 47 | it('should instantiate with sorted migrations', () => { 48 | expect(versionController['migrations'][0].version).toBe(0); 49 | expect(versionController['migrations'][1].version).toBe(1); 50 | expect(versionController['migrations'][2].version).toBe(2); 51 | }); 52 | 53 | it('should throw error if data version is undefined', () => { 54 | const data = { 55 | state: { value: 10 }, 56 | }; 57 | 58 | expect(() => versionController.migrate(data as any)).toThrow( 59 | '导入数据缺少版本号,请检查文件后重试', 60 | ); 61 | }); 62 | 63 | it('should migrate data correctly through multiple versions', () => { 64 | const data: MigrationData = { 65 | state: { value: 10 }, 66 | version: 0, 67 | }; 68 | 69 | const migratedData = versionController.migrate(data); 70 | 71 | expect(migratedData).toEqual({ 72 | state: { value: 10, value1: 20, value2: 40 }, 73 | version: 3, 74 | }); 75 | }); 76 | 77 | it('should migrate data correctly if starting from a specific version', () => { 78 | const data: MigrationData = { 79 | state: { value: 10, value1: 20 }, 80 | version: 1, 81 | }; 82 | 83 | const migratedData = versionController.migrate(data); 84 | 85 | expect(migratedData).toEqual({ 86 | state: { value: 10, value1: 20, value2: 40 }, 87 | version: 3, 88 | }); 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /src/store/session/slices/chat/reducers/sessionTree.ts: -------------------------------------------------------------------------------- 1 | import { SessionTree, SessionTreeNode } from '@/types'; 2 | import { produce } from 'immer'; 3 | 4 | export type SessionTreeDispatch = 5 | | { type: 'addNode'; node: SessionTreeNode } 6 | | { type: 'deleteNode'; agentId: string } 7 | | { type: 'moveNode'; fromIndex: number; toIndex: number } 8 | | { type: 'addChat'; agentId: string; chat: string } 9 | | { type: 'deleteChat'; chatId: string } 10 | | { type: 'sortChat'; agentId: string; chatIndex: number; toIndex: number }; 11 | 12 | export const sessionTreeReducer = ( 13 | state: SessionTree, 14 | action: SessionTreeDispatch, 15 | ): SessionTree => { 16 | switch (action.type) { 17 | case 'addNode': 18 | return produce(state, (draftState) => { 19 | const timestamp = Date.now(); 20 | draftState.unshift({ 21 | ...action.node, 22 | createAt: timestamp, 23 | updateAt: timestamp, 24 | }); 25 | }); 26 | 27 | case 'deleteNode': 28 | return state.filter((node) => node.agentId !== action.agentId); 29 | 30 | case 'moveNode': 31 | return produce(state, (draftState) => { 32 | const [removed] = draftState.splice(action.fromIndex, 1); 33 | draftState.splice(action.toIndex, 0, removed); 34 | }); 35 | 36 | case 'addChat': 37 | return produce(state, (draftState) => { 38 | const nodeIndex = draftState.findIndex((node) => node.agentId === action.agentId); 39 | if (nodeIndex !== -1) { 40 | draftState[nodeIndex].chats.push(action.chat); 41 | draftState[nodeIndex].updateAt = Date.now(); 42 | } 43 | }); 44 | 45 | case 'deleteChat': 46 | return produce(state, (draftState) => { 47 | draftState.forEach((node) => { 48 | const nextChats = node.chats.filter((c) => c !== action.chatId); 49 | if (nextChats.length !== node.chats.length) { 50 | node.chats = nextChats; 51 | node.updateAt = Date.now(); 52 | } 53 | }); 54 | }); 55 | 56 | case 'sortChat': 57 | return produce(state, (draftState) => { 58 | const nodeIndex = draftState.findIndex((node) => node.agentId === action.agentId); 59 | if (nodeIndex !== -1) { 60 | const [removed] = draftState[nodeIndex].chats.splice(action.chatIndex, 1); 61 | draftState[nodeIndex].chats.splice(action.toIndex, 0, removed); 62 | draftState[nodeIndex].updateAt = Date.now(); 63 | } 64 | }); 65 | 66 | default: 67 | return state; 68 | } 69 | }; 70 | -------------------------------------------------------------------------------- /src/pages/flow/components/Panel/NodeManager/SymbolList/index.tsx: -------------------------------------------------------------------------------- 1 | import { useDndMonitor } from '@dnd-kit/core'; 2 | import { useState } from 'react'; 3 | import { Flexbox } from 'react-layout-kit'; 4 | 5 | import AgentAvatar from '@/components/AgentAvatar'; 6 | import CardListItem from '@/components/CardListItem'; 7 | import { Draggable, DragOverlay } from '@/components/DndKit'; 8 | import { Typography } from 'antd'; 9 | import { useTheme } from 'antd-style'; 10 | import { SYMBOL_NODE_LIST, SymbolNodeMasterTypes } from '../../../nodes'; 11 | 12 | const SymbolList = () => { 13 | const theme = useTheme(); 14 | const [draggingId, setDraggingId] = useState(null); 15 | 16 | useDndMonitor({ 17 | onDragStart: (event) => { 18 | if (event.active.data.current?.source !== 'symbol') return; 19 | setDraggingId(event.active.id as string); 20 | }, 21 | onDragEnd: () => { 22 | setDraggingId(null); 23 | }, 24 | }); 25 | 26 | const SymbolMaster = SymbolNodeMasterTypes[draggingId as keyof typeof SymbolNodeMasterTypes]; 27 | 28 | const groupList = SYMBOL_NODE_LIST.reduce((pre, current) => { 29 | const group = current.group || '默认节点'; 30 | if (!pre[group]) { 31 | pre[group] = []; 32 | } 33 | pre[group].push(current); 34 | return pre; 35 | }, {} as Record); 36 | 37 | return ( 38 | 39 | {Object.keys(groupList).map((key) => { 40 | const list = groupList[key]?.map((i) => { 41 | return ( 42 | 48 | } 52 | description={i.description} 53 | style={{ color: theme.colorText }} 54 | /> 55 | 56 | ); 57 | }); 58 | return ( 59 | <> 60 | 61 | {key} 62 | 63 | {list} 64 | 65 | ); 66 | })} 67 | {draggingId && SymbolMaster ? ( 68 | 69 | 70 | 71 | ) : null} 72 | 73 | ); 74 | }; 75 | 76 | export default SymbolList; 77 | -------------------------------------------------------------------------------- /src/utils/compass.ts: -------------------------------------------------------------------------------- 1 | import brotliPromise from 'brotli-wasm'; 2 | 3 | /** 4 | * @title 字符串压缩器 5 | */ 6 | export class StrCompressor { 7 | /** 8 | * @ignore 9 | */ 10 | private instance!: { 11 | decompress(buf: Uint8Array): Uint8Array; 12 | compress(buf: Uint8Array, options?: any): Uint8Array; 13 | }; 14 | 15 | async init(): Promise { 16 | this.instance = await brotliPromise; // Import is async in browsers due to wasm requirements! 17 | } 18 | 19 | /** 20 | * @title 压缩字符串 21 | * @param str - 要压缩的字符串 22 | * @returns 压缩后的字符串 23 | */ 24 | compress(str: string): string { 25 | const input = new TextEncoder().encode(str); 26 | 27 | const compressedData = this.instance.compress(input); 28 | 29 | return this.urlSafeBase64Encode(compressedData); 30 | } 31 | 32 | /** 33 | * @title 解压缩字符串 34 | * @param str - 要解压缩的字符串 35 | * @returns 解压缩后的字符串 36 | */ 37 | decompress(str: string): string { 38 | const compressedData = this.urlSafeBase64Decode(str); 39 | 40 | const decompressedData = this.instance.decompress(compressedData); 41 | 42 | return new TextDecoder().decode(decompressedData); 43 | } 44 | 45 | /** 46 | * @title 异步压缩字符串 47 | * @param str - 要压缩的字符串 48 | * @returns Promise 49 | */ 50 | async compressAsync(str: string) { 51 | const brotli = await brotliPromise; 52 | 53 | const input = new TextEncoder().encode(str); 54 | 55 | const compressedData = brotli.compress(input); 56 | 57 | return this.urlSafeBase64Encode(compressedData); 58 | } 59 | 60 | /** 61 | * @title 异步解压缩字符串 62 | * @param str - 要解压缩的字符串 63 | * @returns Promise 64 | */ 65 | async decompressAsync(str: string) { 66 | const brotli = await brotliPromise; 67 | 68 | const compressedData = this.urlSafeBase64Decode(str); 69 | 70 | const decompressedData = brotli.decompress(compressedData); 71 | 72 | return new TextDecoder().decode(decompressedData); 73 | } 74 | 75 | private urlSafeBase64Encode = (data: Uint8Array): string => { 76 | const base64Str = btoa(String.fromCharCode(...data)); 77 | return base64Str.replaceAll('+', '_0_').replaceAll('/', '_').replace(/=+$/, ''); 78 | }; 79 | 80 | private urlSafeBase64Decode = (data: string): Uint8Array => { 81 | let after = data.replaceAll('_0_', '+').replaceAll('_', '/'); 82 | while (after.length % 4) { 83 | after += '='; 84 | } 85 | 86 | return new Uint8Array( 87 | atob(after) 88 | .split('') 89 | .map((c) => c.charCodeAt(0)), 90 | ); 91 | }; 92 | } 93 | 94 | export const Compressor = new StrCompressor(); 95 | -------------------------------------------------------------------------------- /src/layout/FlowLayout/Menu/FlowList/FlowItem.tsx: -------------------------------------------------------------------------------- 1 | import { createStyles } from 'antd-style'; 2 | import isEqual from 'fast-deep-equal'; 3 | import { FC, memo, useState } from 'react'; 4 | 5 | import AgentAvatar from '@/components/AgentAvatar'; 6 | import CardListItem from '@/components/CardListItem'; 7 | 8 | import { IconAction } from '@/components/IconAction'; 9 | import { getSafeFlowMeta } from '@/helpers/flow'; 10 | import { useFlowStore } from '@/store/flow'; 11 | import { DeleteOutlined } from '@ant-design/icons'; 12 | import { Popconfirm } from 'antd'; 13 | 14 | const useStyles = createStyles(({ css }) => { 15 | return { 16 | time: css` 17 | align-self: flex-start; 18 | `, 19 | }; 20 | }); 21 | 22 | interface FlowItemProps { 23 | active: boolean; 24 | id: string; 25 | loading?: boolean; 26 | } 27 | 28 | const FlowItem: FC = memo(({ id, active, loading }) => { 29 | const { styles, theme } = useStyles(); 30 | const { title, description, avatar, avatarBackground, updateAt } = useFlowStore( 31 | (s) => getSafeFlowMeta(s.flows, id), 32 | isEqual, 33 | ); 34 | 35 | const removeFlow = useFlowStore((s) => s.removeFlow); 36 | 37 | const [isHover, setHovered] = useState(false); 38 | 39 | const displayTitle = title || description || '默认角色'; 40 | 41 | const showAction = isHover || active; 42 | return ( 43 | 58 | } 59 | onHoverChange={(hover) => { 60 | setHovered(hover); 61 | }} 62 | showAction={showAction} 63 | renderActions={ 64 | { 69 | removeFlow(id); 70 | }} 71 | > 72 | { 75 | e.stopPropagation(); 76 | e.preventDefault(); 77 | }} 78 | icon={} 79 | /> 80 | 81 | } 82 | style={{ 83 | color: theme.colorText, 84 | alignItems: 'center', 85 | }} 86 | /> 87 | ); 88 | }); 89 | 90 | export default FlowItem; 91 | -------------------------------------------------------------------------------- /src/hooks/useImportAndExport.ts: -------------------------------------------------------------------------------- 1 | import isEqual from 'fast-deep-equal'; 2 | import { useMemo } from 'react'; 3 | import { shallow } from 'zustand/shallow'; 4 | 5 | import { notification } from '@/layout'; 6 | import { useSessionStore, useSettings } from '@/store'; 7 | import { useFlowStore } from '@/store/flow'; 8 | import { ConfigFile, ConfigSettings } from '@/types'; 9 | 10 | export const useImportAndExport = () => { 11 | const [chats, agents, importChatSessions] = useSessionStore( 12 | (s) => [s.chats, s.agents, s.importChatSessions], 13 | shallow, 14 | ); 15 | const [flows] = useFlowStore((s) => [s.flows], shallow); 16 | const settings = useSettings( 17 | (s): ConfigSettings => ({ 18 | avatar: s.avatar, 19 | fontSize: s.fontSize, 20 | contentWidth: s.contentWidth, 21 | }), 22 | isEqual, 23 | ); 24 | 25 | const importSettings = useSettings((s) => s.importSettings); 26 | 27 | // 将 入参转换为 配置文件格式 28 | const config: ConfigFile = { 29 | state: { chats, agents, settings, flows }, 30 | version: 3.0, 31 | }; 32 | 33 | const exportConfigFile = () => { 34 | // 创建一个 Blob 对象 35 | const blob = new Blob([JSON.stringify(config)], { type: 'application/json' }); 36 | 37 | // 创建一个 URL 对象,用于下载 38 | const url = URL.createObjectURL(blob); 39 | 40 | // 创建一个 元素,设置下载链接和文件名 41 | const a = document.createElement('a'); 42 | a.href = url; 43 | a.download = `TechFlow_Config_v3.0.json`; 44 | 45 | // 触发 元素的点击事件,开始下载 46 | document.body.appendChild(a); 47 | a.click(); 48 | 49 | // 下载完成后,清除 URL 对象 50 | URL.revokeObjectURL(url); 51 | document.body.removeChild(a); 52 | }; 53 | 54 | const handleImport = (info: any) => { 55 | const reader = new FileReader(); 56 | //读取完文件之后的回调函数 57 | reader.onloadend = function (evt) { 58 | const fileString = evt.target?.result; 59 | const fileJson = fileString as string; 60 | 61 | try { 62 | const { state } = JSON.parse(fileJson); 63 | if (isEqual(config, state)) return; 64 | 65 | importChatSessions(state); 66 | importSettings(state.settings); 67 | 68 | // TODO:未来看是否需要收 import 一个方法到 flowStore 中 69 | useFlowStore.setState({ flows: state.flows }); 70 | } catch (e) { 71 | notification.error({ 72 | message: '导入失败', 73 | description: `出错原因: ${(e as Error).message}`, 74 | }); 75 | } 76 | }; 77 | 78 | //@ts-ignore file 类型不明确 79 | reader.readAsText(info.file.originFileObj, 'UTF-8'); 80 | }; 81 | 82 | return useMemo(() => ({ exportConfigFile, handleImport }), [chats, agents, settings]); 83 | }; 84 | -------------------------------------------------------------------------------- /src/pages/flow/components/nodes/AITask/index.tsx: -------------------------------------------------------------------------------- 1 | import { createAITaskContent, createNode } from '@/helpers/flow'; 2 | import { fetchLangChain } from '@/services/langChain'; 3 | import { initAITaskContent } from '@/store/flow/initialState'; 4 | import { ALL_MODELS } from '@/store/mask'; 5 | import { ChatAgent } from '@/types'; 6 | import { AITaskContent, SymbolMasterDefinition } from '@/types/flow'; 7 | import { LangChainParams } from '@/types/langchain'; 8 | import { genChatMessages } from '@/utils/genChatMessages'; 9 | 10 | export const AITaskSymbol: SymbolMasterDefinition = { 11 | id: 'aiTask', 12 | title: '大模型节点', 13 | group: 'AI 节点', 14 | avatar: '🤖', 15 | description: '使用大模型处理任务', 16 | defaultContent: initAITaskContent, 17 | schema: { 18 | model: { 19 | type: 'input', 20 | valueKey: ['llm', 'model'], 21 | component: 'Segmented', 22 | title: '模型', 23 | options: ALL_MODELS.map((model) => ({ 24 | label: model.name, 25 | value: model.name, 26 | })), 27 | valueContainer: false, 28 | }, 29 | systemRole: { 30 | type: 'input', 31 | hideContainer: true, 32 | component: 'SystemRole', 33 | title: '角色定义', 34 | valueContainer: false, 35 | }, 36 | input: { 37 | type: 'input', 38 | component: 'TaskPromptsInput', 39 | title: '运行输入', 40 | hideContainer: true, 41 | valueContainer: false, 42 | }, 43 | }, 44 | onCreateNode: (node, activeData) => { 45 | if (activeData?.source === 'agent') { 46 | const agent = activeData as unknown as ChatAgent; 47 | return createNode( 48 | node, 49 | createAITaskContent({ 50 | llm: { model: 'gpt-3.5-turbo' }, 51 | systemRole: agent.content, 52 | }), 53 | agent, 54 | ); 55 | } 56 | return createNode(node, initAITaskContent, { title: 'AI 节点' }); 57 | }, 58 | run: async (node, vars, action) => { 59 | const prompts = genChatMessages({ 60 | systemRole: node.systemRole, 61 | messages: node.input, 62 | }); 63 | const request: LangChainParams = { 64 | llm: { model: 'gpt3.5-turbo', temperature: 0.6 }, 65 | prompts, 66 | vars, 67 | }; 68 | action.updateParams(request); 69 | let output = ''; 70 | await fetchLangChain({ 71 | params: request, 72 | onMessageHandle: (text) => { 73 | output += text; 74 | action.updateOutput({ 75 | output, 76 | type: 'text', 77 | }); 78 | }, 79 | onLoadingChange: (loading) => { 80 | action.updateLoading(loading); 81 | }, 82 | abortController: action.abortController, 83 | }); 84 | 85 | return { 86 | type: 'text', 87 | output, 88 | }; 89 | }, 90 | }; 91 | -------------------------------------------------------------------------------- /src/components/ControlInput.tsx: -------------------------------------------------------------------------------- 1 | import { Button, ConfigProvider, Input, InputProps, InputRef } from 'antd'; 2 | import { memo, useCallback, useEffect, useRef, useState } from 'react'; 3 | 4 | export interface ControlInputProps extends Omit { 5 | onChange?: (value: string) => void; 6 | onValueChanging?: (value: string) => void; 7 | value?: string; 8 | onChangeEnd?: (value: string) => void; 9 | } 10 | 11 | export const ControlInput = memo( 12 | ({ value, onChange, onValueChanging, onChangeEnd, ...props }) => { 13 | const [input, setInput] = useState(value || ''); 14 | const inputRef = useRef(null); 15 | const isChineseInput = useRef(false); 16 | 17 | const isFocusing = useRef(false); 18 | 19 | const updateValue = useCallback(() => { 20 | onChange?.(input); 21 | }, [input]); 22 | 23 | useEffect(() => { 24 | if (typeof value !== 'undefined') setInput(value); 25 | }, [value]); 26 | 27 | return ( 28 | { 33 | isChineseInput.current = true; 34 | }} 35 | onCompositionEnd={() => { 36 | isChineseInput.current = false; 37 | }} 38 | onFocus={() => { 39 | isFocusing.current = true; 40 | }} 41 | onBlur={() => { 42 | isFocusing.current = false; 43 | onChangeEnd?.(input); 44 | }} 45 | onChange={(e) => { 46 | setInput(e.target.value); 47 | onValueChanging?.(e.target.value); 48 | }} 49 | onPressEnter={(e) => { 50 | if (!e.shiftKey && !isChineseInput.current) { 51 | e.preventDefault(); 52 | updateValue(); 53 | isFocusing.current = false; 54 | onChangeEnd?.(input); 55 | } 56 | }} 57 | suffix={ 58 | value === input ? ( 59 | 60 | ) : ( 61 | 62 | 72 | 82 | 83 | ) 84 | } 85 | /> 86 | ); 87 | }, 88 | ); 89 | --------------------------------------------------------------------------------