├── i18n ├── en-US │ ├── $.json │ ├── _chatgpt.json │ ├── chatgpt-samples │ │ └── $.json │ ├── click-flow │ │ └── $.json │ ├── _.json │ └── _click-flow.json ├── zh-CN │ ├── $.json │ ├── _chatgpt.json │ ├── click-flow │ │ └── $.json │ ├── _.json │ └── _click-flow.json └── README.md ├── src ├── assets │ ├── chatgpt │ │ └── flow │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── writting.yml │ │ │ ├── domain-driven-design.yml │ │ │ ├── design-software-system.yml │ │ │ ├── user-story.yml │ │ │ └── unit-mesh-unit-server.yml │ ├── images │ │ └── content.png │ ├── icons │ │ ├── message.svg │ │ ├── new-chat.svg │ │ ├── send.svg │ │ ├── logout.svg │ │ ├── trashcan.svg │ │ └── gpt.svg │ ├── clickprompt-small.svg │ └── clickprompt-light.svg ├── components │ ├── ChakraUI │ │ ├── icons.ts │ │ ├── index.ts │ │ └── Provider.tsx │ ├── chatgpt │ │ ├── AiBlock.tsx │ │ ├── HumanBlock.tsx │ │ ├── ChatGPTApp.tsx │ │ └── LoginPage.tsx │ ├── markdown │ │ ├── MermaidWrapper.tsx │ │ └── Mermaid.tsx │ ├── UnitRuntime │ │ ├── renderer │ │ │ ├── UnitServerRenderer.tsx │ │ │ └── ReactRenderer.tsx │ │ ├── UnitResultDispatcher.tsx │ │ └── UnitRenderer.tsx │ ├── ClickPrompt │ │ ├── Button.shared.tsx │ │ ├── LoggingDrawer.tsx │ │ ├── ClickPromptButton.tsx │ │ └── ExecutePromptButton.tsx │ ├── Highlight.tsx │ ├── CustomIcon.tsx │ ├── CopyComponent.tsx │ ├── LocaleSwitcher.tsx │ ├── DataTable │ │ └── DataTable.tsx │ └── SimpleColorPicker.tsx ├── app │ ├── [lang] │ │ ├── [...not_found] │ │ │ └── page.ts │ │ ├── flow-editor │ │ │ ├── page.tsx │ │ │ └── StepConverter.ts │ │ ├── click-flow │ │ │ ├── page.tsx │ │ │ ├── [id] │ │ │ │ ├── page.tsx │ │ │ │ ├── AskRenderer.tsx │ │ │ │ └── StartlingStepPage.tsx │ │ │ └── page.client.tsx │ │ ├── page.tsx │ │ ├── not-found.tsx │ │ ├── chatgpt │ │ │ └── page.tsx │ │ └── layout.tsx │ ├── globals.css │ └── api │ │ └── chatgpt │ │ └── stream │ │ └── route.ts ├── i18n │ ├── pagePath.ts │ ├── en-US.ts │ ├── zh-CN.ts │ └── index.ts ├── flows │ ├── actions │ │ ├── open-action.ts │ │ └── api-action.ts │ ├── types │ │ ├── click-flow.ts │ │ ├── flow-action.ts │ │ └── flow-step.ts │ ├── components │ │ ├── FlowMarkdownWrapper.tsx │ │ ├── PostComponentDispatcher.tsx │ │ ├── SettingHeaderConfig.tsx │ │ ├── ProcessDispatcher.tsx │ │ ├── PostFlowAction.tsx │ │ ├── PreFlowAction.tsx │ │ ├── FlowMarkdownEditor.tsx │ │ └── SharedFlowAction.tsx │ ├── flow-components │ │ └── JsonViewer.tsx │ ├── pre-action-dispatcher.ts │ ├── unitmesh │ │ ├── ascode.ts │ │ └── ReplService.ts │ ├── post-action-dispatcher.ts │ ├── flow-functions │ │ ├── codeFromMarkdown.ts │ │ ├── jsonPath.ts │ │ └── math.ts │ ├── react-flow-nodes │ │ ├── InteractiveNode.tsx │ │ ├── PromptNode.tsx │ │ └── StepNode.tsx │ ├── explain │ │ └── FlowExplain.tsx │ └── store.ts ├── configs │ ├── constants.ts │ └── next-seo-config.ts ├── storage │ ├── webstorage.ts │ └── planetscale.ts ├── uitls │ ├── user.edge.util.ts │ ├── user.util.ts │ ├── openapi.util.ts │ └── crypto.util.ts ├── pages │ └── api │ │ ├── chatgpt │ │ ├── verify.ts │ │ ├── user.ts │ │ ├── conversation.ts │ │ └── chat.ts │ │ └── action │ │ └── proxy.ts ├── types.d.ts ├── api │ ├── user.ts │ ├── chat.ts │ └── conversation.ts ├── data-processor │ └── explain-parser.ts ├── middleware.ts └── layout │ └── NavBar.tsx ├── vercel.json ├── docs ├── screenshot.jpeg ├── TRANSLATING.md └── CONTRIBUTING.md ├── public ├── favicon │ ├── favicon.ico │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon-192x192.png │ └── favicon-512x512.png ├── sitemap.xml └── robots.txt ├── postcss.config.js ├── .husky └── pre-push ├── .prettierignore ├── next-sitemap.config.js ├── scripts └── gen-enc.js ├── .prettierrc.json ├── .env.example ├── .eslintrc.json ├── tailwind.config.js ├── .vscode ├── settings.json └── launch.json ├── .gitignore ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ └── ci.yaml ├── .editorconfig ├── next.config.js ├── jest.config.js ├── tsconfig.json ├── __tests__ ├── flows │ ├── flow-authorization.test.ts │ ├── jsonPath.test.ts │ ├── math.test.ts │ └── step-converter.test.ts ├── step-detail.test.ts └── explain-parser.test.ts ├── LICENSE ├── gen └── generate-chatgpt-by-category.js ├── README.zh-CN.md ├── README.md ├── prisma └── schema.prisma └── package.json /i18n/en-US/$.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /i18n/en-US/_chatgpt.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /i18n/en-US/chatgpt-samples/$.json: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /i18n/zh-CN/$.json: -------------------------------------------------------------------------------- 1 | { 2 | } 3 | -------------------------------------------------------------------------------- /i18n/zh-CN/_chatgpt.json: -------------------------------------------------------------------------------- 1 | {} 2 | 3 | -------------------------------------------------------------------------------- /src/assets/chatgpt/flow/.gitignore: -------------------------------------------------------------------------------- 1 | index.json 2 | -------------------------------------------------------------------------------- /i18n/zh-CN/click-flow/$.json: -------------------------------------------------------------------------------- 1 | { 2 | "by-each-step-samples": "逐步运行" 3 | } 4 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "github": { 3 | "silent": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /i18n/en-US/click-flow/$.json: -------------------------------------------------------------------------------- 1 | { 2 | "by-each-step-samples": "Flow samples" 3 | } 4 | -------------------------------------------------------------------------------- /src/components/ChakraUI/icons.ts: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | export * from "@chakra-ui/icons"; 4 | -------------------------------------------------------------------------------- /i18n/zh-CN/_.json: -------------------------------------------------------------------------------- 1 | { 2 | "create-new-steps": "创建新的 逐步运行", 3 | "view-here": "详细展开" 4 | } 5 | -------------------------------------------------------------------------------- /docs/screenshot.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prompt-engineering/chat-flow/HEAD/docs/screenshot.jpeg -------------------------------------------------------------------------------- /i18n/en-US/_.json: -------------------------------------------------------------------------------- 1 | { 2 | "create-new-steps": "Create new Flow", 3 | "view-here": "View here" 4 | } 5 | -------------------------------------------------------------------------------- /i18n/zh-CN/_click-flow.json: -------------------------------------------------------------------------------- 1 | { 2 | "create-new-steps": "创建新的 逐步运行", 3 | "view-here": "详细展开" 4 | } 5 | -------------------------------------------------------------------------------- /i18n/en-US/_click-flow.json: -------------------------------------------------------------------------------- 1 | { 2 | "create-new-steps": "Create new Flow", 3 | "view-here": "View here" 4 | } 5 | -------------------------------------------------------------------------------- /public/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prompt-engineering/chat-flow/HEAD/public/favicon/favicon.ico -------------------------------------------------------------------------------- /src/assets/images/content.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prompt-engineering/chat-flow/HEAD/src/assets/images/content.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prompt-engineering/chat-flow/HEAD/public/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prompt-engineering/chat-flow/HEAD/public/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon/favicon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prompt-engineering/chat-flow/HEAD/public/favicon/favicon-192x192.png -------------------------------------------------------------------------------- /public/favicon/favicon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prompt-engineering/chat-flow/HEAD/public/favicon/favicon-512x512.png -------------------------------------------------------------------------------- /public/sitemap.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /docs/TRANSLATING.md: -------------------------------------------------------------------------------- 1 | # 🌐 i18n Guide 2 | 3 | Thanks for your interest in helping us translate ClickPrompt! 4 | 5 | TODO 6 | -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | # TODO(CGQAQ): uncomment this when next 13.2.4 came out 5 | # npm run lint; 6 | npm run format; 7 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore artifacts: 2 | build 3 | coverage 4 | 5 | .next 6 | 7 | dist 8 | node_modules 9 | public 10 | 11 | .vscode 12 | .idea 13 | 14 | *.json 15 | CNAME 16 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # * 2 | User-agent: * 3 | Allow: / 4 | 5 | # Host 6 | Host: https://www.clickprompt.org/ 7 | 8 | # Sitemaps 9 | Sitemap: https://www.clickprompt.org/sitemap.xml 10 | -------------------------------------------------------------------------------- /src/app/[lang]/[...not_found]/page.ts: -------------------------------------------------------------------------------- 1 | /// https://stackoverflow.com/a/75625136 2 | 3 | import { notFound } from "next/navigation"; 4 | 5 | export default function NotFoundCatchAll() { 6 | notFound(); 7 | return null; 8 | } 9 | -------------------------------------------------------------------------------- /src/i18n/pagePath.ts: -------------------------------------------------------------------------------- 1 | export const hadChildRoutes = ["click-flow", "chatgpt-samples"]; 2 | 3 | export const pages = ["/", "/chatgpt/", "/click-flow/", "/click-flow/$"] as const; 4 | 5 | export type PagePath = (typeof pages)[number]; 6 | -------------------------------------------------------------------------------- /next-sitemap.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next-sitemap').IConfig} */ 2 | module.exports = { 3 | siteUrl: process.env.SITE_URL || "https://www.clickprompt.org/", 4 | generateRobotsTxt: true, // (optional) 5 | // ...other options 6 | }; 7 | -------------------------------------------------------------------------------- /src/flows/actions/open-action.ts: -------------------------------------------------------------------------------- 1 | import { OpenAction } from "@/flows/types/flow-action"; 2 | 3 | export async function openAction(openAction: OpenAction) { 4 | window.open(openAction.scheme); 5 | return { 6 | success: true, 7 | }; 8 | } 9 | -------------------------------------------------------------------------------- /src/components/chatgpt/AiBlock.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import styled from "@emotion/styled"; 4 | import { Flex } from "@/components/ChakraUI"; 5 | 6 | export const AiBlock = styled(Flex)` 7 | background-color: #fff; 8 | padding: 1rem; 9 | `; 10 | -------------------------------------------------------------------------------- /scripts/gen-enc.js: -------------------------------------------------------------------------------- 1 | // node scripts/gen-enc.js 1234567890 2 | // read secret from command line 3 | let secret = process.argv[2]; 4 | // create key from secret 5 | let key = require("node:crypto").createHash("sha256").update(String(secret)).digest("base64").substr(0, 32); 6 | 7 | console.log(key); 8 | -------------------------------------------------------------------------------- /src/assets/icons/message.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/assets/icons/new-chat.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/components/chatgpt/HumanBlock.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import styled from "@emotion/styled"; 4 | import { Flex } from "@/components/ChakraUI"; 5 | 6 | export const HumanBlock = styled(Flex)` 7 | background-color: rgba(247, 247, 248); 8 | border-color: rgba(0, 0, 0, 0.1); 9 | padding: 1rem; 10 | `; 11 | -------------------------------------------------------------------------------- /src/assets/icons/send.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/flows/types/click-flow.ts: -------------------------------------------------------------------------------- 1 | import { FlowStep } from "@/flows/types/flow-step"; 2 | 3 | export type StartlingFlow = { 4 | name: string; 5 | category: string; 6 | author: string; 7 | explain?: string; 8 | description: string; 9 | steps: FlowStep[]; 10 | stepGuide?: boolean; 11 | replService?: boolean; 12 | }; 13 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/prettierrc", 3 | "trailingComma": "all", 4 | "tabWidth": 2, 5 | "semi": true, 6 | "singleQuote": false, 7 | "jsxSingleQuote": true, 8 | "endOfLine": "lf", 9 | "printWidth": 120, 10 | "bracketSpacing": true, 11 | "arrowParens": "always", 12 | "useTabs": false 13 | } 14 | -------------------------------------------------------------------------------- /src/components/markdown/MermaidWrapper.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from "next/dynamic"; 2 | import React from "react"; 3 | 4 | export default function MermaidWrapper({ graphDefinition }: { graphDefinition: string }) { 5 | const MermaidDynamic = dynamic(() => import("./Mermaid"), { ssr: false }); 6 | return ; 7 | } 8 | -------------------------------------------------------------------------------- /src/assets/icons/logout.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/flows/components/FlowMarkdownWrapper.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from "next/dynamic"; 2 | import React from "react"; 3 | 4 | export function FlowMarkdownWrapper({ text, onChange }: { text: string; onChange: (text: string) => void }) { 5 | const FlowMarkdownEditor = dynamic(() => import("./FlowMarkdownEditor"), { ssr: false }); 6 | return ; 7 | } 8 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # fake enc on local dev 2 | # ```nodejs 3 | # const { createHash } = require('node:crypto'); 4 | # const enc = createHash('sha256').update(String()).digest('base64').substring(0, 32); 5 | # ``` 6 | ENC_KEY= 7 | # can get free db from https://auth.planetscale.com/sign-up 8 | DATABASE_URL= 9 | # the websocket server: https://github.com/prompt-engineering/ChatREPL 10 | REPL_SERVER=127.0.0.1:8080 11 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "plugins": [ 4 | "@typescript-eslint" 5 | ], 6 | "extends": [ 7 | "next/core-web-vitals", 8 | "plugin:@typescript-eslint/recommended", 9 | "prettier" 10 | ], 11 | "rules": { 12 | "react-hooks/exhaustive-deps": "off", 13 | "no-console": "off", 14 | "@typescript-eslint/no-unused-vars": "off", 15 | "@typescript-eslint/no-explicit-any": "off" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/assets/icons/trashcan.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/app/[lang]/flow-editor/page.tsx: -------------------------------------------------------------------------------- 1 | import "server-only"; 2 | 3 | import FlowEditor from "./page.client"; 4 | import { getAppData } from "@/i18n"; 5 | 6 | export default async function Page() { 7 | const { locale, pathname, i18n } = await getAppData(); 8 | const i18nProps: GeneralI18nProps = { 9 | locale, 10 | pathname, 11 | i18n: { 12 | dict: i18n.dict, 13 | }, 14 | }; 15 | 16 | return ; 17 | } 18 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ["./src/**/*.{js,ts,jsx,tsx}"], 4 | theme: { 5 | extend: { 6 | colors: { 7 | // generate by https://huemint.com/website-magazine/ 8 | white: "#ffffff", 9 | // Pohutukawa 10 | black: "#16245d", 11 | light: "#F8F4EA", 12 | blue: "#0A5CD6", 13 | }, 14 | }, 15 | }, 16 | plugins: [], 17 | }; 18 | -------------------------------------------------------------------------------- /src/app/[lang]/click-flow/page.tsx: -------------------------------------------------------------------------------- 1 | import "server-only"; 2 | 3 | import StartlingByEachStepList from "./page.client"; 4 | import { getAppData } from "@/i18n"; 5 | 6 | export default async function Page() { 7 | const { locale, pathname, i18n } = await getAppData(); 8 | const i18nProps: GeneralI18nProps = { 9 | locale, 10 | pathname, 11 | i18n: { 12 | dict: i18n.dict, 13 | }, 14 | }; 15 | 16 | return ; 17 | } 18 | -------------------------------------------------------------------------------- /src/components/UnitRuntime/renderer/UnitServerRenderer.tsx: -------------------------------------------------------------------------------- 1 | import { ReplResult } from "@/flows/unitmesh/ascode"; 2 | import { Link, Text } from "@chakra-ui/react"; 3 | import React from "react"; 4 | 5 | export function UnitServerRenderer(result: ReplResult) { 6 | const url = (result.content as any)["url"]; 7 | 8 | return ( 9 | 10 | Online URL:{" "} 11 | 12 | {url} 13 | 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /src/app/[lang]/page.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { getAppData } from "@/i18n"; 3 | import StartlingByEachStepList from "@/app/[lang]/click-flow/page.client"; 4 | 5 | async function Page() { 6 | const { locale, pathname, i18n } = await getAppData(); 7 | const i18nProps: GeneralI18nProps = { 8 | locale, 9 | pathname, 10 | i18n: { 11 | dict: i18n.dict, 12 | }, 13 | }; 14 | 15 | return ; 16 | } 17 | 18 | export default Page; 19 | -------------------------------------------------------------------------------- /src/flows/flow-components/JsonViewer.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from "next/dynamic"; 2 | 3 | const DynamicReactJson = dynamic(import("react-json-view"), { ssr: false }); 4 | 5 | export function JsonViewer({ json }: { json: object }) { 6 | return ( 7 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/components/ChakraUI/index.ts: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | export { 4 | Avatar, 5 | Box, 6 | Flex, 7 | Heading, 8 | Spacer, 9 | Tooltip, 10 | Link, 11 | Breadcrumb, 12 | BreadcrumbItem, 13 | BreadcrumbLink, 14 | Button, 15 | Stack, 16 | Text, 17 | IconButton, 18 | Menu, 19 | MenuButton, 20 | MenuItem, 21 | MenuList, 22 | Input, 23 | Container, 24 | SimpleGrid, 25 | Card, 26 | CardBody, 27 | CardHeader, 28 | AlertIcon, 29 | AlertTitle, 30 | Alert, 31 | } from "@chakra-ui/react"; 32 | -------------------------------------------------------------------------------- /src/app/[lang]/not-found.tsx: -------------------------------------------------------------------------------- 1 | /// https://stackoverflow.com/a/75625136 2 | 3 | import Link from "next/link"; 4 | 5 | export default function NotFound() { 6 | return ( 7 |
8 |

nOT foUnD – 404!

9 |
10 | 11 | Go back to Home 12 | 13 |
14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /src/flows/pre-action-dispatcher.ts: -------------------------------------------------------------------------------- 1 | import { FlowAction } from "@/flows/types/flow-action"; 2 | import { apiProxy } from "@/flows/actions/api-action"; 3 | 4 | export async function preActionDispatcher(action: FlowAction) { 5 | console.log("preActionDispatcher", action); 6 | switch (action.type) { 7 | case "api": 8 | if (action.api) return await apiProxy(action.api!); 9 | break; 10 | default: 11 | console.log("Unknown actions type"); 12 | } 13 | 14 | return { 15 | success: false, 16 | error: "Unknown actions type", 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /src/components/ChakraUI/Provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import { ChakraProvider, extendTheme } from "@chakra-ui/react"; 5 | 6 | export const Provider = ({ children }: { children: React.ReactNode }) => { 7 | const theme = extendTheme({ 8 | components: { 9 | Drawer: { 10 | sizes: { 11 | "2xl": { dialog: { maxW: "8xl" } }, 12 | }, 13 | }, 14 | }, 15 | }); 16 | 17 | return ( 18 | 19 | {children} 20 | 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /src/flows/components/PostComponentDispatcher.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { ActionPostComponent } from "@/flows/types/flow-action"; 4 | import { JsonViewer } from "@/flows/flow-components/JsonViewer"; 5 | 6 | export function PostComponentDispatcher(components: ActionPostComponent[], result: any) { 7 | return ( 8 | <> 9 | {components.map((component, index) => { 10 | switch (component.name) { 11 | case "JsonViewer": 12 | return ; 13 | default: 14 | return
Not found
; 15 | } 16 | })} 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "typescript.enablePromptUseWorkspaceTsdk": true, 4 | "stylelint.validate": [ 5 | "css", 6 | "less", 7 | "scss", 8 | "postcss", 9 | "react", 10 | ], 11 | "editor.codeActionsOnSave": { 12 | "source.fixAll.eslint": true 13 | }, 14 | "eslint.validate": [ 15 | "javascript", 16 | "typescript" 17 | ], 18 | "editor.formatOnSave": true, 19 | "editor.defaultFormatter": "esbenp.prettier-vscode", 20 | "javascript.format.insertSpaceAfterOpeningAndBeforeClosingEmptyBraces": false, 21 | "typescript.format.insertSpaceAfterOpeningAndBeforeClosingEmptyBraces": false, 22 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | pnpm-lock.yaml 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /.swc/ 15 | /out/ 16 | 17 | # production 18 | /build 19 | 20 | # misc 21 | .DS_Store 22 | *.pem 23 | 24 | # debug 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | .pnpm-debug.log* 29 | 30 | # local env files 31 | .env*.local 32 | 33 | # vercel 34 | .vercel 35 | 36 | # typescript 37 | *.tsbuildinfo 38 | next-env.d.ts 39 | .idea 40 | 41 | /dist 42 | public/sitemap-0.xml 43 | src/assets/resources/**/*.json 44 | 45 | .vercel 46 | .env 47 | -------------------------------------------------------------------------------- /src/components/ClickPrompt/Button.shared.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Image from "next/image"; 4 | import React, { MouseEventHandler } from "react"; 5 | import styled from "@emotion/styled"; 6 | 7 | export type ButtonSize = "sm" | "md" | "lg"; 8 | 9 | export const StyledBird = styled(Image)` 10 | position: absolute; 11 | top: -20px; 12 | right: -20px; 13 | `; 14 | 15 | export const StyledPromptButton = styled.div` 16 | position: relative; 17 | width: min-content; 18 | `; 19 | 20 | export type CPButtonProps = { 21 | loading?: boolean; 22 | onClick?: MouseEventHandler; 23 | size?: ButtonSize; 24 | text: string; 25 | children?: React.ReactNode; 26 | [key: string]: any; 27 | }; 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /src/configs/constants.ts: -------------------------------------------------------------------------------- 1 | export const SITE_TITLE = "ClickPrompt"; 2 | export const SITE_URL = "https://www.clickprompt.org/"; 3 | export const SITE_LOCALE_COOKIE = "CLICKPROMPT_LOCALE"; 4 | export const SITE_USER_COOKIE = "CLICKPROMPT_USER"; 5 | export const GITHUB_URL = "https://github.com/prompt-engineering/chat-flow"; 6 | export const CP_GITHUB_ASSETS = `${GITHUB_URL}/tree/master/src/assets/`; 7 | export const SITE_INTERNAL_HEADER_URL = "$$$x-url"; 8 | export const SITE_INTERNAL_HEADER_PATHNAME = "$$$x-pathname"; 9 | export const SITE_INTERNAL_HEADER_LOCALE = "$$$x-locale"; 10 | export const CHAT_COMPLETION_CONFIG = { 11 | model: "gpt-3.5-turbo", 12 | temperature: 0.5, 13 | max_tokens: 512, 14 | }; 15 | -------------------------------------------------------------------------------- /src/flows/unitmesh/ascode.ts: -------------------------------------------------------------------------------- 1 | export enum MsgType { 2 | None = "none", 3 | ERROR = "error", 4 | RUNNING = "running", 5 | UNIT_SERVER = "unit_server", 6 | REACT_BUNDLE = "react_bundle", 7 | } 8 | 9 | export interface ReplResult { 10 | id: number; 11 | resultValue: string; 12 | className: string; 13 | msgType: MsgType; 14 | content: UnitServerContent | ErrorContent | ReactBundleContent; 15 | } 16 | 17 | export interface ReactBundleContent { 18 | react: string; 19 | reactDom: string; 20 | thirdParty: string[]; 21 | } 22 | 23 | export interface ErrorContent { 24 | exception: string; 25 | message: string; 26 | } 27 | 28 | export interface UnitServerContent { 29 | url: string; 30 | } 31 | -------------------------------------------------------------------------------- /src/flows/post-action-dispatcher.ts: -------------------------------------------------------------------------------- 1 | import { ActionResult, FlowAction } from "@/flows/types/flow-action"; 2 | import { apiProxy } from "@/flows/actions/api-action"; 3 | import { openAction } from "@/flows/actions/open-action"; 4 | 5 | export async function postActionDispatcher(action: FlowAction, content: string): Promise { 6 | switch (action.type) { 7 | case "api": 8 | if (action.api) await apiProxy(action.api!, content); 9 | break; 10 | case "open": 11 | if (action.open) await openAction(action.open!); 12 | break; 13 | default: 14 | console.log("Unknown actions type"); 15 | } 16 | 17 | return { 18 | success: false, 19 | error: "Unknown actions type", 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /src/storage/webstorage.ts: -------------------------------------------------------------------------------- 1 | type WebStorageType = "localStorage" | "sessionStorage"; 2 | 3 | export class WebStorage { 4 | type: WebStorageType = "localStorage"; 5 | name: string; 6 | 7 | constructor(name: string, type?: WebStorageType) { 8 | this.name = name; 9 | type && (this.type = type); 10 | } 11 | 12 | get storage(): Storage { 13 | return window[this.type]; 14 | } 15 | 16 | get() { 17 | try { 18 | return JSON.parse(this.storage.getItem(this.name) ?? "") as T; 19 | } catch (e) { 20 | return null; 21 | } 22 | } 23 | 24 | set(value: T) { 25 | this.storage.setItem(this.name, JSON.stringify(value)); 26 | } 27 | 28 | remove() { 29 | this.storage.removeItem(this.name); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/flows/components/SettingHeaderConfig.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { AuthKeyValues } from "@/flows/types/flow-action"; 3 | 4 | export function parseConfigures(values: AuthKeyValues): AuthKeyValues { 5 | const regex = /\${{.*}}/g; 6 | const configures = values 7 | .map((v) => { 8 | const match = v.value.match(regex); 9 | if (match) { 10 | return { 11 | key: v.key, 12 | value: match[0], 13 | }; 14 | } 15 | }) 16 | .filter((v) => v != undefined); 17 | 18 | return configures as AuthKeyValues; 19 | } 20 | 21 | type ConfigProps = { 22 | configures: AuthKeyValues; 23 | }; 24 | 25 | function SettingHeaderConfig(props: ConfigProps) { 26 | return
; 27 | } 28 | 29 | export default SettingHeaderConfig; 30 | -------------------------------------------------------------------------------- /src/i18n/en-US.ts: -------------------------------------------------------------------------------- 1 | import type { PagePath } from "./pagePath"; 2 | 3 | import _global from "@i18n/en-US/$.json"; 4 | import _index from "@i18n/en-US/_.json"; 5 | import _chatgpt from "@i18n/en-US/_chatgpt.json"; 6 | import _chatgptStartlingByEachStep from "@i18n/en-US/_click-flow.json"; 7 | import _chatgptStartlingByEachStepDetail from "@i18n/en-US/click-flow/$.json"; 8 | 9 | export type GlobalKey = keyof typeof _global; 10 | const pages = { 11 | "/": _index, 12 | "/chatgpt/": _chatgpt, 13 | "/click-flow/": _chatgptStartlingByEachStep, 14 | "/click-flow/$": _chatgptStartlingByEachStepDetail, 15 | } satisfies Record; 16 | export type PageKey

= keyof (typeof pages)[P]; 17 | 18 | const i18nDataEnUS = { 19 | "*": _global, 20 | ...pages, 21 | }; 22 | export default i18nDataEnUS; 23 | -------------------------------------------------------------------------------- /src/i18n/zh-CN.ts: -------------------------------------------------------------------------------- 1 | import type { PagePath } from "./pagePath"; 2 | 3 | import _global from "@i18n/zh-CN/$.json"; 4 | import _index from "@i18n/zh-CN/_.json"; 5 | import _chatgpt from "@i18n/zh-CN/_chatgpt.json"; 6 | import _chatgptStartlingByEachStep from "@i18n/zh-CN/_click-flow.json"; 7 | import _chatgptStartlingByEachStepDetail from "@i18n/zh-CN/click-flow/$.json"; 8 | 9 | export type GlobalKey = keyof typeof _global; 10 | const pages = { 11 | "/": _index, 12 | "/chatgpt/": _chatgpt, 13 | "/click-flow/": _chatgptStartlingByEachStep, 14 | "/click-flow/$": _chatgptStartlingByEachStepDetail, 15 | } satisfies Record; 16 | export type PageKey

= keyof (typeof pages)[P]; 17 | 18 | const i18nDataZhCN = { 19 | "*": _global, 20 | ...pages, 21 | }; 22 | export default i18nDataZhCN; 23 | -------------------------------------------------------------------------------- /src/components/Highlight.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | /** 4 | * Hight light keywords in paragraph 5 | */ 6 | export default function Highlight({ value, keyword }: { value: string; keyword: string }) { 7 | if (!(value != undefined && keyword != undefined && value.length > 0 && keyword.length > 0)) { 8 | return value; 9 | } 10 | const regex = new RegExp(keyword, "gi"); 11 | 12 | return value 13 | .split(regex) 14 | .reduce((acc: any, part: string, i: number) => { 15 | if (i === 0) { 16 | return [part]; 17 | } 18 | return acc.concat( 19 | 20 | {keyword} 21 | , 22 | part, 23 | ); 24 | }, []) 25 | .map((part: React.ReactNode, i: number) => {part}); 26 | } 27 | -------------------------------------------------------------------------------- /src/flows/components/ProcessDispatcher.tsx: -------------------------------------------------------------------------------- 1 | import { ActionProcess } from "@/flows/types/flow-action"; 2 | import { jsonPath } from "@/flows/flow-functions/jsonPath"; 3 | 4 | export function processDispatcher(postProcesses: ActionProcess[], data: any) { 5 | //iterator postProcesses and set to result 6 | let result = data; 7 | postProcesses.forEach((process) => { 8 | switch (process.function) { 9 | case "jsonPath": 10 | if (!process.args || process.args.length < 2) { 11 | throw new Error("jsonPath function need 2 arguments"); 12 | } 13 | result = jsonPath(result, process.args[0], process.args[1]); 14 | break; 15 | case "fromMarkdown": 16 | break; 17 | case "toMarkdown": 18 | break; 19 | default: 20 | break; 21 | } 22 | }); 23 | 24 | return result; 25 | } 26 | -------------------------------------------------------------------------------- /src/components/CustomIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Image from "next/image"; 3 | 4 | import chatgptLogo from "@/assets/images/chatgpt-logo.svg?url"; 5 | import clickPromptLogo from "@/assets/clickprompt-light.svg?url"; 6 | import clickPromptSmall from "@/assets/clickprompt-small.svg?url"; 7 | 8 | export function ChatGptIcon({ width = 32, height = 32 }) { 9 | return ChatGPT Logo; 10 | } 11 | 12 | export function ClickPromptIcon({ width = 32, height = 32 }) { 13 | return ClickPrompt Logo; 14 | } 15 | 16 | export function ClickPromptSmall({ width = 32, height = 32 }) { 17 | return ClickPrompt Logo; 18 | } 19 | -------------------------------------------------------------------------------- /src/components/chatgpt/ChatGPTApp.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ChatRoom } from "@/components/chatgpt/ChatRoom"; 4 | import { LoginPage } from "@/components/chatgpt/LoginPage"; 5 | import React, { useEffect, useState } from "react"; 6 | 7 | type ChatGPTAppProps = { 8 | loggedIn?: boolean; 9 | updateLoginStatus?: (loggedIn: boolean) => void; 10 | initMessage?: string; 11 | }; 12 | export const ChatGPTApp = ({ loggedIn, initMessage, updateLoginStatus }: ChatGPTAppProps) => { 13 | const [isLoggedIn, setIsLoggedIn] = useState(loggedIn ?? false); 14 | 15 | useEffect(() => { 16 | if (updateLoginStatus) { 17 | updateLoginStatus(isLoggedIn); 18 | } 19 | }, [isLoggedIn]); 20 | 21 | return isLoggedIn ? ( 22 | 23 | ) : ( 24 | 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | max_line_length = 120 11 | trim_trailing_whitespace = true 12 | 13 | [*.markdown] 14 | trim_trailing_whitespace = false 15 | 16 | # Matches multiple files with brace expansion notation 17 | # Set default charset 18 | [*.{js, py, ts, tsx, html, css, scss, json}] 19 | charset = utf-8 20 | indent_style = space 21 | indent_size = 2 22 | 23 | # 4 space indentation 24 | [*.py] 25 | indent_style = space 26 | indent_size = 4 27 | 28 | # Tab indentation (no size specified) 29 | [Makefile] 30 | indent_style = tab 31 | 32 | # Matches the exact files either package.json or .travis.yml 33 | [{package.json,.travis.yml}] 34 | indent_style = space 35 | indent_size = 2 36 | -------------------------------------------------------------------------------- /src/components/UnitRuntime/UnitResultDispatcher.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { MsgType, ReactBundleContent, ReplResult } from "@/flows/unitmesh/ascode"; 4 | import { Textarea } from "@chakra-ui/react"; 5 | import ReactRenderer from "./renderer/ReactRenderer"; 6 | import { UnitServerRenderer } from "@/components/UnitRuntime/renderer/UnitServerRenderer"; 7 | 8 | export function UnitResultDispatcher(result: ReplResult) { 9 | const isReturnUrl = result.content && result.content.hasOwnProperty("url"); 10 | if (isReturnUrl) { 11 | return UnitServerRenderer(result); 12 | } 13 | 14 | if (result.msgType == MsgType.REACT_BUNDLE) { 15 | return ( 16 |

17 | 18 |
19 | ); 20 | } 21 | 22 | return ; 23 | } 24 | -------------------------------------------------------------------------------- /src/uitls/user.edge.util.ts: -------------------------------------------------------------------------------- 1 | import { getUserByKeyHashed } from "@/storage/planetscale"; 2 | import { SITE_USER_COOKIE } from "@/configs/constants"; 3 | import { cookies } from "next/headers"; 4 | 5 | export type User = Awaited>; 6 | export async function getUser(): Promise { 7 | const cookieStore = cookies(); 8 | const keyHashed = cookieStore.get(SITE_USER_COOKIE); 9 | if (!keyHashed) { 10 | return new Response(JSON.stringify({ error: "You're not logged in yet!" }), { 11 | status: 400, 12 | }); 13 | } 14 | 15 | const user = await getUserByKeyHashed(keyHashed.value); 16 | if (!user) { 17 | return new Response(JSON.stringify({ error: "Your login session has been expired!" }), { 18 | status: 400, 19 | headers: { "Set-Cookie": `${SITE_USER_COOKIE}=; Max-Age=0; HttpOnly; Path=/;` }, 20 | }); 21 | } 22 | return user; 23 | } 24 | -------------------------------------------------------------------------------- /src/uitls/user.util.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from "next"; 2 | import { getUserByKeyHashed } from "@/storage/planetscale"; 3 | import { SITE_USER_COOKIE } from "@/configs/constants"; 4 | 5 | export type User = Awaited>; 6 | export async function getUser(req: NextApiRequest, res: NextApiResponse): Promise { 7 | const keyHashed = req.cookies[SITE_USER_COOKIE]; 8 | if (!keyHashed) { 9 | res.status(400).json({ error: "You're not logged in yet!" }); 10 | return null; 11 | } 12 | 13 | const user = await getUserByKeyHashed(keyHashed); 14 | if (!user) { 15 | kickOutUser(res); 16 | res.status(400).json({ error: "Your login session has been expired!" }); 17 | return null; 18 | } 19 | return user; 20 | } 21 | 22 | export function kickOutUser(res: NextApiResponse) { 23 | res.setHeader("Set-Cookie", `${SITE_USER_COOKIE}=; Max-Age=0; HttpOnly; Path=/;`); 24 | } 25 | -------------------------------------------------------------------------------- /src/app/[lang]/chatgpt/page.tsx: -------------------------------------------------------------------------------- 1 | import "server-only"; 2 | 3 | import React from "react"; 4 | import { cookies } from "next/headers"; 5 | import { SITE_USER_COOKIE } from "@/configs/constants"; 6 | import { ChatGPTApp } from "@/components/chatgpt/ChatGPTApp"; 7 | import * as UserAPI from "@/api/user"; 8 | import { Container } from "@/components/ChakraUI"; 9 | 10 | export default async function ChatGPTPage() { 11 | const hashedKey = cookies().get(SITE_USER_COOKIE)?.value as string; 12 | 13 | let isLogin: boolean; 14 | try { 15 | isLogin = await UserAPI.isLoggedIn(hashedKey); 16 | } catch (e) { 17 | console.error(e); 18 | isLogin = false; 19 | } 20 | 21 | return ( 22 | 23 |
24 | 25 |
26 |
27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | 28 | - OS: [e.g. iOS] 29 | - Browser [e.g. chrome, safari] 30 | - Version [e.g. 22] 31 | 32 | **Smartphone (please complete the following information):** 33 | 34 | - Device: [e.g. iPhone6] 35 | - OS: [e.g. iOS8.1] 36 | - Browser [e.g. stock browser, safari] 37 | - Version [e.g. 22] 38 | 39 | **Additional context** 40 | Add any other context about the problem here. 41 | -------------------------------------------------------------------------------- /src/pages/api/chatgpt/verify.ts: -------------------------------------------------------------------------------- 1 | import { NextApiHandler } from "next"; 2 | import { SITE_USER_COOKIE } from "@/configs/constants"; 3 | import { isValidUser } from "@/storage/planetscale"; 4 | 5 | // verify login state 6 | const handler: NextApiHandler = async (req, res) => { 7 | if (req.method !== "POST") { 8 | res.status(404).json({ error: "Not found" }); 9 | return; 10 | } 11 | const keyHashed = req.body.length > 10 ? req.body : req.cookies[SITE_USER_COOKIE] ?? ""; 12 | 13 | if (!keyHashed) { 14 | res.status(200).json({ message: "You're not logged in yet!", loggedIn: false }); 15 | return; 16 | } 17 | 18 | const isValid = isValidUser(keyHashed); 19 | if (!isValid) { 20 | res.setHeader("Set-Cookie", `${SITE_USER_COOKIE}=; Max-Age=0; HttpOnly; Path=/;`); 21 | res.status(200).json({ message: "Your login session has been expired!", loggedIn: false }); 22 | return; 23 | } 24 | 25 | return res.status(200).json({ message: "You're logged in!", loggedIn: true }); 26 | }; 27 | 28 | export default handler; 29 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | experimental: { 5 | appDir: true, 6 | // TODO https://beta.nextjs.org/docs/configuring/typescript#statically-typed-links 7 | // typedRoutes: true, 8 | }, 9 | trailingSlash: true, 10 | transpilePackages: ["react-syntax-highlighter"], 11 | images: { 12 | domains: ["prompt-engineering.github.io"], 13 | }, 14 | webpack: (config, options) => { 15 | config.module.rules.push({ 16 | test: /\.yml/, 17 | use: "yaml-loader", 18 | }); 19 | 20 | config.module.rules.push({ 21 | test: /\.svg$/i, 22 | type: "asset", 23 | resourceQuery: /url/, // *.svg?url 24 | }); 25 | 26 | config.module.rules.push({ 27 | test: /\.svg$/i, 28 | issuer: /\.[jt]sx?$/, 29 | resourceQuery: { not: [/url/] }, // exclude react component if *.svg?url 30 | use: ["@svgr/webpack"], 31 | }); 32 | 33 | return config; 34 | }, 35 | }; 36 | 37 | module.exports = nextConfig; 38 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // jest.config.js 2 | const nextJest = require("next/jest"); 3 | 4 | const createJestConfig = nextJest({ 5 | // Provide the path to your Next.js app to load next.config.js and .env files in your test environment 6 | dir: "./", 7 | }); 8 | 9 | // Add any custom config to be passed to Jest 10 | /** @type {import("jest").Config} */ 11 | const customJestConfig = { 12 | // Add more setup options before each test is run 13 | // setupFilesAfterEnv: ['/jest.setup.js'], 14 | testEnvironment: "jest-environment-jsdom", 15 | moduleNameMapper: { 16 | "^jsonpath-plus": require.resolve("jsonpath-plus"), 17 | unified: require.resolve("unified"), 18 | "^lodash-es$": "lodash", 19 | "^@/(.*)": "/src/$1", 20 | }, 21 | transformIgnorePatterns: ["node_modules/(?!(unified)/)", "/node_modules/", "^.+\\.module\\.(css|sass|scss)$"], 22 | }; 23 | 24 | // createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async 25 | module.exports = createJestConfig(customJestConfig); 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseUrl": ".", // This has to be specified if "paths" is. 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "target": "ES2020", 6 | "lib": [ 7 | "dom", 8 | "dom.iterable", 9 | "ES2021.String", 10 | "esnext" 11 | ], 12 | "allowJs": true, 13 | "skipLibCheck": true, 14 | "strict": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "noEmit": true, 17 | "esModuleInterop": true, 18 | "module": "esnext", 19 | "moduleResolution": "node", 20 | "resolveJsonModule": true, 21 | "isolatedModules": true, 22 | "jsx": "preserve", 23 | "incremental": true, 24 | "plugins": [ 25 | { 26 | "name": "next" 27 | } 28 | ], 29 | "paths": { 30 | "@/*": [ 31 | "./src/*" 32 | ], 33 | "@i18n/*": [ 34 | "./i18n/*" 35 | ] 36 | } 37 | }, 38 | "include": [ 39 | "next-env.d.ts", 40 | "**/*.ts", 41 | "**/*.tsx", 42 | ".next/types/**/*.ts" 43 | ], 44 | "exclude": [ 45 | "node_modules" 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /src/flows/actions/api-action.ts: -------------------------------------------------------------------------------- 1 | import { ApiAction } from "@/flows/types/flow-action"; 2 | import fetch from "node-fetch"; 3 | 4 | export async function apiProxy(apiAction: ApiAction, body?: string) { 5 | return await postApiAction(apiAction, body!); 6 | } 7 | 8 | async function postApiAction(apiAction: ApiAction, content: string) { 9 | // todo: show config for token, when user click on the actions 10 | const { url, method, headers, body } = apiAction; 11 | const response = await fetch(`/api/action/proxy`, { 12 | method: "POST", 13 | headers: { 14 | "Content-Type": "application/json", 15 | }, 16 | body: JSON.stringify({ 17 | url, 18 | method, 19 | headers, 20 | body: body, 21 | }).replace('"$$response$$"', JSON.stringify(content)), 22 | }); 23 | 24 | if (response.ok) { 25 | const body = await response.json(); 26 | return { 27 | success: true, 28 | result: body, 29 | }; 30 | } else { 31 | return { 32 | success: false, 33 | error: await response.text(), 34 | }; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | build: 11 | name: Build & Test 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | node-version: ["lts/gallium", "lts/hydrogen", "current"] 16 | steps: 17 | - name: Checkout 🛎️ 18 | uses: actions/checkout@v3 19 | with: 20 | persist-credentials: false 21 | 22 | - uses: actions/setup-node@v3 23 | with: 24 | node-version: 16 25 | 26 | - run: npm ci 27 | 28 | - run: npm run test 29 | 30 | - run: npm run build --if-present 31 | lint: 32 | name: format and lint 33 | runs-on: ubuntu-latest 34 | steps: 35 | - name: Checkout 🛎️ 36 | uses: actions/checkout@v3 37 | with: 38 | persist-credentials: false 39 | 40 | - uses: actions/setup-node@v3 41 | with: 42 | node-version: 16 43 | - run: npm ci 44 | 45 | - run: npm run format 46 | 47 | - run: npm run lint 48 | -------------------------------------------------------------------------------- /__tests__/flows/flow-authorization.test.ts: -------------------------------------------------------------------------------- 1 | import "@testing-library/jest-dom"; 2 | import { parseConfigures } from "@/flows/components/SettingHeaderConfig"; 3 | 4 | describe("Flow Authorization", () => { 5 | it("parse", () => { 6 | expect( 7 | parseConfigures([ 8 | { 9 | key: "Authorization", 10 | value: " $${{ GITHUB_TOKEN }}", 11 | }, 12 | ]), 13 | ).toEqual([{ key: "Authorization", value: "${{ GITHUB_TOKEN }}" }]); 14 | expect( 15 | parseConfigures([ 16 | { 17 | key: "Authorization", 18 | value: "{{ GITHUB_TOKEN }}", 19 | }, 20 | ]), 21 | ).toEqual([]); 22 | }); 23 | 24 | it("parse two values", () => { 25 | expect( 26 | parseConfigures([ 27 | { 28 | key: "Accept", 29 | value: "application/vnd.github+json", 30 | }, 31 | { 32 | key: "Authorization", 33 | value: " $${{ GITHUB_TOKEN }}", 34 | }, 35 | ]), 36 | ).toEqual([{ key: "Authorization", value: "${{ GITHUB_TOKEN }}" }]); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Next.js: debug server-side", 9 | "type": "node-terminal", 10 | "request": "launch", 11 | "command": "npm run dev" 12 | }, 13 | { 14 | "name": "Next.js: debug client-side", 15 | "type": "chrome", 16 | "request": "launch", 17 | "url": "http://localhost:3000" 18 | }, 19 | { 20 | "name": "Next.js: debug full stack", 21 | "type": "node-terminal", 22 | "request": "launch", 23 | "command": "npm run dev", 24 | "serverReadyAction": { 25 | "pattern": "started server on .+, url: (https?://.+)", 26 | "uriFormat": "%s", 27 | "action": "debugWithChrome" 28 | } 29 | } 30 | ] 31 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Prompt Engineering 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /src/components/ClickPrompt/LoggingDrawer.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Drawer, DrawerBody, DrawerCloseButton, DrawerContent, DrawerOverlay } from "@chakra-ui/react"; 4 | import { ChatGPTApp } from "@/components/chatgpt/ChatGPTApp"; 5 | import React from "react"; 6 | import { CPButtonProps } from "@/components/ClickPrompt/Button.shared"; 7 | 8 | export function LoggingDrawer( 9 | isOpen: boolean, 10 | handleClose: () => void, 11 | isLoggedIn: boolean, 12 | props: CPButtonProps, 13 | updateStatus?: (loggedIn: boolean) => void, 14 | ) { 15 | return ( 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 |
24 |
25 |
26 |
27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /__tests__/flows/jsonPath.test.ts: -------------------------------------------------------------------------------- 1 | import { jsonPath } from "@/flows/flow-functions/jsonPath"; 2 | import "@testing-library/jest-dom"; 3 | 4 | describe("Json Parse for function", () => { 5 | it("parse", () => { 6 | const cities = [ 7 | { name: "London", population: 8615246 }, 8 | { name: "Berlin", population: 3517424 }, 9 | { name: "Madrid", population: 3165235 }, 10 | { name: "Rome", population: 2870528 }, 11 | ]; 12 | 13 | const names = jsonPath(cities, "$..name", ["name"]); 14 | expect(names.length).toEqual(4); 15 | expect(names[0]).toEqual({ name: "London" }); 16 | }); 17 | 18 | it("match name and population", () => { 19 | const cities = [ 20 | { name: "London", population: 8615246 }, 21 | { name: "Berlin", population: 3517424 }, 22 | { name: "Madrid", population: 3165235 }, 23 | { name: "Rome", population: 2870528 }, 24 | ]; 25 | 26 | const result = jsonPath(cities, "$..[name,population]", ["name", "population"]); 27 | expect(result.length).toEqual(4); 28 | expect(result[0]).toEqual({ name: "London", population: 8615246 }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /__tests__/flows/math.test.ts: -------------------------------------------------------------------------------- 1 | import "@testing-library/jest-dom"; 2 | import { math } from "@/flows/flow-functions/math"; 3 | 4 | describe("Math Evaluator", () => { 5 | it("simple eval", () => { 6 | expect(math("value + 1", 1)).toEqual(2); 7 | }); 8 | 9 | it("with object", () => { 10 | const demoObject = { 11 | x: 123, 12 | y: 456, 13 | }; 14 | 15 | expect(math("value.x + 3", demoObject, "x")).toEqual({ 16 | x: 126, 17 | y: 456, 18 | }); 19 | }); 20 | 21 | it("with array", () => { 22 | const demoArray = [1, 2, 3, 4, 5]; 23 | const result = math("value + 1", demoArray); 24 | expect(result).toEqual([2, 3, 4, 5, 6]); 25 | }); 26 | 27 | it("with object array", () => { 28 | const demoArray = [ 29 | { 30 | x: 1, 31 | y: 2, 32 | }, 33 | { 34 | x: 3, 35 | y: 4, 36 | }, 37 | ]; 38 | const result = math("value.x + 1", demoArray, "x"); 39 | expect(result).toEqual([ 40 | { 41 | x: 2, 42 | y: 2, 43 | }, 44 | { 45 | x: 4, 46 | y: 4, 47 | }, 48 | ]); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /src/components/CopyComponent.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { CopyToClipboard } from "react-copy-to-clipboard"; 4 | import { CopyIcon } from "@chakra-ui/icons"; 5 | import React from "react"; 6 | import { Tooltip, useToast } from "@chakra-ui/react"; 7 | 8 | type CopyProps = { 9 | value: string; 10 | boxSize?: number; 11 | className?: string; 12 | children?: React.ReactNode; 13 | }; 14 | 15 | function CopyComponent({ value, className = "", children, boxSize = 8 }: CopyProps) { 16 | const toast = useToast(); 17 | return ( 18 |
19 | { 22 | toast({ 23 | title: "Copied to clipboard", 24 | position: "top", 25 | status: "success", 26 | }); 27 | }} 28 | > 29 |
30 | {children ? children : ""} 31 | 32 | 33 | 34 |
35 |
36 |
37 | ); 38 | } 39 | 40 | export default CopyComponent; 41 | -------------------------------------------------------------------------------- /__tests__/step-detail.test.ts: -------------------------------------------------------------------------------- 1 | import "@testing-library/jest-dom"; 2 | import { fillStepWithValued } from "@/flows/types/flow-step"; 3 | 4 | describe("Step Valued", () => { 5 | it("fillStepWithValued", () => { 6 | let step = { 7 | name: "分析需求,编写用户故事", 8 | ask: "story: $$placeholder$$", 9 | cachedResponseRegex: "/.*/", 10 | values: { 11 | placeholder: "用户通过主菜单进入“权限管理”模块,选择“账号管理”Tab页,可以看到“新增账号”按钮。", 12 | }, 13 | preActions: [], 14 | postActions: [], 15 | }; 16 | const result = fillStepWithValued(step, {}); 17 | expect(result.replaced).toEqual(true); 18 | expect(result.ask).toEqual( 19 | "story: 用户通过主菜单进入“权限管理”模块,选择“账号管理”Tab页,可以看到“新增账号”按钮。", 20 | ); 21 | }); 22 | 23 | it("fillStepWithValued with cached", () => { 24 | const step = { 25 | name: "分析需求,编写用户故事", 26 | ask: "story: $$response:1$$", 27 | cachedResponseRegex: "/.*/", 28 | values: {}, 29 | preActions: [], 30 | postActions: [], 31 | }; 32 | const result = fillStepWithValued(step, { 33 | 1: "Cached Value", 34 | }); 35 | expect(result.replaced).toEqual(true); 36 | expect(result.ask).toEqual("story: Cached Value"); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /src/flows/flow-functions/codeFromMarkdown.ts: -------------------------------------------------------------------------------- 1 | import { unified } from "unified"; 2 | import remarkParse from "remark-parse"; 3 | import { Node } from "unist"; 4 | 5 | /** 6 | * Parses a markdown file and returns the code blocks 7 | * @param markdown 8 | * @returns {CodeBlock[]} code blocks 9 | * @example 10 | * const markdown = ` 11 | * # Title 12 | * 13 | * \`\`\`js 14 | * const a = 1; 15 | * \`\`\` 16 | * 17 | * \`\`\`js 18 | * const b = 2; 19 | * \`\`\` 20 | * `; 21 | * 22 | * const codeBlocks = getCodeBlocksFromMarkdown(markdown); 23 | * // codeBlocks = ["const a = 1;", "const b = 2;"] 24 | * 25 | */ 26 | export async function codeFromMarkdown(markdown: string): Promise { 27 | const ast = await unified().use(remarkParse).parse(markdown); 28 | 29 | const codeBlocks: CodeBlock[] = []; 30 | 31 | ast.children.forEach((node) => { 32 | if (node.type === "code") { 33 | const codeNode = node as Node & { lang: string; value: string }; 34 | codeBlocks.push({ 35 | lang: codeNode.lang, 36 | code: codeNode.value, 37 | }); 38 | } 39 | }); 40 | 41 | return codeBlocks; 42 | } 43 | 44 | export type CodeBlock = { 45 | lang: string; 46 | code: string; 47 | }; 48 | -------------------------------------------------------------------------------- /src/flows/types/flow-action.ts: -------------------------------------------------------------------------------- 1 | export type FlowAction = { 2 | name: string; 3 | type: FlowActionType; 4 | api?: ApiAction; 5 | open?: OpenAction; 6 | // the function after execute the api action 7 | postProcess?: ActionProcess[]; 8 | // the function before execute the api action 9 | preProcess?: ActionProcess[]; 10 | postComponents?: ActionPostComponent[]; 11 | }; 12 | 13 | export type ActionProcess = { 14 | function: "jsonPath" | "fromMarkdown" | "toMarkdown"; 15 | args?: any[]; 16 | outputVar?: string; 17 | }; 18 | 19 | export type ActionPostComponent = { 20 | name: "JsonViewer" | "MarkdownViewer"; 21 | args?: string; 22 | }; 23 | 24 | export type FlowActionType = "api" | "open"; 25 | export type ApiAction = { 26 | url: string; 27 | method: string; 28 | headers: AuthKeyValues; 29 | body: string; 30 | }; 31 | 32 | export type AuthKeyValues = { 33 | key: string; 34 | value: string; 35 | }[]; 36 | 37 | export type OpenAction = { 38 | scheme: string; 39 | }; 40 | 41 | export type ActionResult = ActionSuccess | ActionError; 42 | export type ActionSuccess = { 43 | success: true; 44 | result?: any; 45 | }; 46 | export type ActionError = { 47 | success: false; 48 | error: string; 49 | }; 50 | -------------------------------------------------------------------------------- /src/types.d.ts: -------------------------------------------------------------------------------- 1 | declare module "color-name-list" { 2 | interface ColorName { 3 | name: string; 4 | hex: string; 5 | } 6 | const colorNameList: ColorName[]; 7 | export = colorNameList; 8 | } 9 | 10 | declare module "nearest-color" { 11 | interface RGB { 12 | r: number; 13 | g: number; 14 | b: number; 15 | } 16 | interface ColorSpec { 17 | name: string; 18 | value: string; 19 | rgb: RGB; 20 | distance: number; 21 | } 22 | interface ColorMatcher extends NearestColor { 23 | (needle: RGB | string): ColorSpec; 24 | or: (alternateColors: string[] | Record) => ColorMatcher; 25 | } 26 | 27 | interface NearestColor { 28 | (needle: RGB | string, colors?: ColorSpec[]): string; 29 | from: (availableColors: string[] | Record) => ColorMatcher; 30 | } 31 | 32 | const nearestColor: NearestColor; 33 | 34 | export default nearestColor; 35 | } 36 | 37 | declare module "*.svg?url" { 38 | const content: string; 39 | export default content; 40 | } 41 | 42 | type GeneralI18nProps = { 43 | i18n: { 44 | dict: import("@/i18n/index").AppData["i18n"]["dict"]; 45 | }; 46 | locale: import("@/i18n").SupportedLocale; 47 | pathname: string; 48 | }; 49 | -------------------------------------------------------------------------------- /src/app/[lang]/click-flow/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { notFound } from "next/navigation"; 3 | import { StartlingFlow } from "@/flows/types/click-flow"; 4 | import StartlingStepPage from "@/app/[lang]/click-flow/[id]/StartlingStepPage"; 5 | import { getAppData } from "@/i18n"; 6 | 7 | const getSampleNames = async () => { 8 | const index = await import("@/assets/chatgpt/flow/index.json").then((mod) => mod.default); 9 | return index.map((item) => item.path.split(".").slice(0, -1).join(".")); 10 | }; 11 | 12 | async function StepDetailPage({ params }: { params: { id: string } }) { 13 | const { locale, pathname, i18n } = await getAppData(); 14 | const i18nProps: GeneralI18nProps = { 15 | locale, 16 | pathname, 17 | i18n: { 18 | dict: i18n.dict, 19 | }, 20 | }; 21 | 22 | const names = await getSampleNames(); 23 | if (!names.includes(params.id)) { 24 | notFound(); 25 | } 26 | 27 | const flow: StartlingFlow = await import(`@/assets/chatgpt/flow/${params.id}.yml`).then((mod) => mod.default); 28 | 29 | if (!flow) { 30 | notFound(); 31 | } 32 | 33 | return <>{flow && }; 34 | } 35 | 36 | export default StepDetailPage; 37 | -------------------------------------------------------------------------------- /gen/generate-chatgpt-by-category.js: -------------------------------------------------------------------------------- 1 | // 1. convert resources in src/assets/chatgpt/category/*.yml to json 2 | // 2. generate src/assets/chatgpt/category.json 3 | // the yaml file is like this: 4 | // ```yml 5 | // name: 6 | // zh-cn: 编程 7 | // en-us: Programming 8 | // category: Programming 9 | // samples: 10 | // - name: name 11 | // ask: string 12 | // response: string 13 | // ``` 14 | const fs = require("node:fs"); 15 | const yaml = require("js-yaml"); 16 | const path = require("node:path"); 17 | const walkdir = require("walkdir"); 18 | 19 | function generateBySteps() { 20 | const stepsDir = path.join(__dirname, "../src/assets/chatgpt/flow"); 21 | const stepsFile = path.join(stepsDir, "index.json"); 22 | 23 | const files = walkdir.sync(stepsDir, { no_recurse: true }); 24 | const index = files 25 | .filter((f) => f.endsWith(".yml")) 26 | .map((f) => { 27 | const content = fs.readFileSync(f, "utf8"); 28 | const doc = yaml.load(content); 29 | const { name, category, description, steps, author } = doc; 30 | return { name, category, description, steps, author, path: path.relative(stepsDir, f) }; 31 | }); 32 | 33 | fs.writeFileSync(stepsFile, JSON.stringify(index, null, 2)); 34 | } 35 | 36 | generateBySteps(); 37 | -------------------------------------------------------------------------------- /src/assets/chatgpt/flow/README.md: -------------------------------------------------------------------------------- 1 | # ChatGPT StartlingByEachStep 2 | 3 | Simple DSL with components: 4 | 5 | ``` 6 | - $$placeholder$$: placeholder 7 | - $$response:1$$: means the second response, because array index start from 0 8 | ``` 9 | 10 | DataStructure: 11 | 12 | ```yaml 13 | name: Interactive User Journey 14 | category: Development 15 | author: Phodal Huang 16 | description: In this example, we will design a user journey for online shopping. 17 | explain: | 18 | digraph G { 19 | 0[flowType = "prompt"] 20 | 1[flowType = "prompt,interactive"] 21 | 2[flowType = "prompt,interactive"] 22 | 3[flowType = "prompt,interactive"] 23 | 4[flowType = "prompt,interactive"] 24 | 0 -> 1 25 | 1 -> 2 26 | 1 -> 3 27 | 1 -> 4 28 | } 29 | 30 | steps: 31 | - name: 设计用户旅程 32 | ask: | 33 | design a user journal for $$placeholder$$ 34 | values: 35 | placeholder: online shopping 36 | cachedResponseRegex: .* 37 | ``` 38 | 39 | for explain: 40 | 41 | 1. we use Graphviz to generate the graph, you can use [Graphviz Online](https://dreampuf.github.io/GraphvizOnline/) to generate the graph. 42 | 2. number is the step index, and the `custom` is the step type, `prompt` is the normal step, `interactive` is the interactive step. 43 | -------------------------------------------------------------------------------- /src/uitls/openapi.util.ts: -------------------------------------------------------------------------------- 1 | import { Configuration, OpenAIApi, type ConfigurationParameters } from "openai"; 2 | async function getConfig(apiKey: string) { 3 | const baseConf: ConfigurationParameters = { 4 | apiKey, 5 | }; 6 | // FIXME now just for development 7 | if (process.env.NODE_ENV === "development" && process.env.PROXY_HOST && process.env.PROXY_PORT) { 8 | const { httpsOverHttp } = await import("tunnel"); 9 | const tunnel = httpsOverHttp({ 10 | proxy: { 11 | host: process.env.PROXY_HOST, 12 | port: process.env.PROXY_PORT as unknown as number, 13 | }, 14 | }); 15 | baseConf.baseOptions = { 16 | httpsAgent: tunnel, 17 | proxy: false, 18 | }; 19 | } 20 | return baseConf; 21 | } 22 | 23 | async function createNewOpenAIApi(apiKey: string) { 24 | const conf = await getConfig(apiKey); 25 | const configuration = new Configuration(conf); 26 | 27 | return new OpenAIApi(configuration); 28 | } 29 | 30 | const chatClients = new Map(); 31 | 32 | export async function getChatClient(keyHashed: string, apiKey: string) { 33 | const chatClient = chatClients.get(keyHashed) || (await createNewOpenAIApi(apiKey)); 34 | chatClients.set(keyHashed, chatClient); 35 | return chatClient; 36 | } 37 | -------------------------------------------------------------------------------- /src/flows/flow-functions/jsonPath.ts: -------------------------------------------------------------------------------- 1 | import { JSONPath } from "jsonpath-plus"; 2 | 3 | /** 4 | * Get the value of the JSONPath from the JSON object. 5 | * @param json JSON object to extract value from. 6 | * @param path JSONPath to match in the object. 7 | * @param keys Array of keys to extract from the matched object. 8 | * @returns Array of values extracted from the matched objects. 9 | * 10 | * @example 11 | * const json = [ 12 | * { name: "London", "population": 8615246 }, 13 | * { name: "Berlin", "population": 3517424 }, 14 | * { name: "Madrid", "population": 3165235 }, 15 | * { name: "Rome", "population": 2870528 } 16 | * ]; 17 | * 18 | * const path = "$..name"; 19 | * const keys = ["name"]; 20 | * 21 | * // Returns [{ name: "London" }, { name: "Berlin" }, { name: "Madrid" }, { name: "Rome" }] 22 | * const result = jsonPath(json, path, keys); 23 | */ 24 | export function jsonPath(json: object, path: string, keys: string[]) { 25 | const flatValues = JSONPath({ path, json }); 26 | const result = []; 27 | 28 | for (let i = 0; i < flatValues.length; i += keys.length) { 29 | const obj: any = {}; 30 | for (let j = 0; j < keys.length; j++) { 31 | obj[keys[j]] = flatValues[i + j]; 32 | } 33 | result.push(obj); 34 | } 35 | 36 | return result; 37 | } 38 | -------------------------------------------------------------------------------- /src/flows/react-flow-nodes/InteractiveNode.tsx: -------------------------------------------------------------------------------- 1 | import styled from "@emotion/styled"; 2 | import React from "react"; 3 | import { Handle, Position } from "reactflow"; 4 | 5 | type TextNodeProps = { 6 | isConnectable: boolean; 7 | data: { label: string }; 8 | }; 9 | 10 | function InteractiveNode(props: TextNodeProps) { 11 | const { isConnectable } = props; 12 | 13 | return ( 14 | 15 | 16 | {props.data.label} 17 | 18 | 19 | ); 20 | } 21 | 22 | const TextNodeStyle = styled.div` 23 | font-family: jetbrains-mono, "JetBrains Mono", monospace; 24 | display: -ms-flexbox; 25 | display: -webkit-flex; 26 | display: flex; 27 | 28 | -ms-flex-align: center; 29 | -webkit-align-items: center; 30 | -webkit-box-align: center; 31 | align-items: center; 32 | 33 | min-height: 50px; 34 | width: 120px; 35 | border: 2px solid #555; 36 | padding: 4px; 37 | border-radius: 5px; 38 | background: white; 39 | `; 40 | 41 | const Title = styled.div` 42 | display: block; 43 | width: 100%; 44 | font-size: 12px; 45 | text-align: center; 46 | `; 47 | 48 | export default InteractiveNode; 49 | -------------------------------------------------------------------------------- /src/assets/clickprompt-small.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/assets/chatgpt/flow/writting.yml: -------------------------------------------------------------------------------- 1 | name: 写作 2 | category: Development 3 | author: Phodal Huang 4 | description: Flow for writting. 5 | explain: | 6 | digraph G { 7 | 0[flowType = "interactive"] 8 | 1[flowType = "interactive"] 9 | 2[flowType = "interactive"] 10 | 3[flowType = "interactive"] 11 | 4[flowType = "interactive"] 12 | 5[flowType = "interactive"] 13 | 0 -> 1 14 | 1 -> 2 15 | 2 -> 3 16 | 2 -> 4 17 | 2 -> 5 18 | } 19 | 20 | steps: 21 | - name: 思路扩展 22 | ask: 我想写一篇文章,主题围绕于:$$placeholder$$,有什么合适的方向?只返回合适的方向。 23 | markdownEditor: true 24 | values: 25 | placeholder: ChatGPT 与内容创作? 26 | cachedResponseRegex: 27 | - name: 继续思考 28 | ask: 有创意一点的呢? 29 | markdownEditor: true 30 | cachedResponseRegex: 31 | - name: 合适的标题 32 | ask: 围绕于 """$$placeholder$$""",帮我想 10 个合适的 10 个相关的标题 33 | markdownEditor: true 34 | values: 35 | placeholder: 利用 ChatGPT 进行内容创作的协作 ChatGPT,如何作为一个协作平台,让多个内容创作者共同创作一篇文章或一个视频 36 | cachedResponseRegex: 37 | - name: 设计大纲 38 | ask: 帮我围绕上这个标题 """$$placeholder$$""",设计一个大纲吧? 39 | markdownEditor: true 40 | values: 41 | placeholder: 协作的力量:ChatGPT 如何彻底改变内容创作 42 | cachedResponseRegex: .* 43 | - name: 编写内容 44 | ask: 现在,围绕于这个大纲 """$$response:3$$""",帮我写一篇文章吧? 45 | markdownEditor: true 46 | cachedResponseRegex: 47 | - name: 写总结 48 | ask: 好的,现在,帮我写一下文章的总结。 49 | markdownEditor: true 50 | cachedResponseRegex: 51 | -------------------------------------------------------------------------------- /docs/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributor Manual 2 | 3 | We welcome contributions of any size and skill level. As an open source project, we believe in giving back to our contributors and are happy to help with guidance on PRs, technical writing, and turning any feature idea into a reality. 4 | 5 | > **Tip for new contributors:** 6 | > Take a look at [https://github.com/firstcontributions/first-contributions](https://github.com/firstcontributions/first-contributions) for helpful information on contributing 7 | 8 | ## Quick Guide 9 | 10 | ### Prerequisite 11 | 12 | ```shell 13 | node: ">=16.0.0" 14 | npm: "^8.11.0" 15 | # otherwise, your build will fail 16 | ``` 17 | 18 | ### Setting up your local repo 19 | 20 | ```shell 21 | git clone && cd ... 22 | npm install 23 | npm run build 24 | ``` 25 | 26 | ### Development 27 | 28 | ```shell 29 | # starts a file-watching, live-reloading dev script for active development 30 | npm run dev 31 | # build the entire project, one time. 32 | npm run build 33 | ``` 34 | 35 | ### Running tests 36 | 37 | ```shell 38 | # run this in the top-level project root to run all tests 39 | npm run test 40 | ``` 41 | 42 | ### Making a Pull Request 43 | 44 | You can run the following commands before making a Pull Request 45 | 46 | ```shell 47 | # format with fix 48 | npm run format:fix 49 | # lint with fix 50 | npm run lint:fix 51 | ``` 52 | 53 | ## Code Structure 54 | 55 | TODO 56 | 57 | ## Translation 58 | 59 | See [i18n guide](TRANSLATING.md) 60 | -------------------------------------------------------------------------------- /__tests__/explain-parser.test.ts: -------------------------------------------------------------------------------- 1 | import "@testing-library/jest-dom"; 2 | import { explainParser, graphToFlow } from "@/data-processor/explain-parser"; 3 | 4 | describe("StableDiffusion Prompt Parser", () => { 5 | it("parse", () => { 6 | let str = ` 7 | digraph G { 8 | 1[flowType = "prompt"] 9 | 2[flowType = "prompt,interactive"] 10 | 3[flowType = "prompt,interactive"] 11 | 4[flowType = "prompt,interactive"] 12 | 5[flowType = "prompt,interactive"] 13 | 1 -> 2 -> 3 14 | 2 -> 4 15 | 2 -> 5 16 | }`; 17 | let graph = explainParser(str); 18 | expect(graph.nodes().length).toEqual(5); 19 | expect(graph.edges().length).toEqual(3); 20 | }); 21 | 22 | it("graphToFlow", () => { 23 | let str = ` 24 | digraph G { 25 | 1[flowType = "prompt"] 26 | 2[flowType = "prompt,interactive"] 27 | 3[flowType = "prompt,interactive"] 28 | 4[flowType = "prompt,interactive"] 29 | 5[flowType = "prompt,interactive"] 30 | 1 -> 2 -> 3 31 | 2 -> 4 32 | 2 -> 5 33 | }`; 34 | let graph = explainParser(str); 35 | let flows = graphToFlow(graph); 36 | 37 | expect(flows.nodes.length).toEqual(5); 38 | 39 | expect(flows.nodes[0].height).toEqual(50); 40 | expect(flows.nodes[0].width).toEqual(120); 41 | expect(flows.nodes[0].position.x).toEqual(60); 42 | expect(flows.nodes[0].position.y).toEqual(75); 43 | expect(flows.nodes[0].data).toEqual({ flowType: "prompt" }); 44 | 45 | expect(flows.edges.length).toEqual(3); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /src/components/UnitRuntime/UnitRenderer.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useState } from "react"; 2 | import { Button, Flex, Text } from "@chakra-ui/react"; 3 | 4 | import { ReplService } from "@/flows/unitmesh/ReplService"; 5 | import { ReplResult } from "@/flows/unitmesh/ascode"; 6 | import { UnitResultDispatcher } from "@/components/UnitRuntime/UnitResultDispatcher"; 7 | 8 | export function UnitRenderer({ code, repl, index }: { code: string; repl: ReplService; index?: number }) { 9 | const [result, setResult] = useState(undefined); 10 | const [isRunning, setIsRunning] = useState(false); 11 | const [error, setError] = useState(undefined); 12 | 13 | repl.getSubject().subscribe({ 14 | next: (msg: ReplResult) => { 15 | if (msg.id == index) { 16 | setResult(msg); 17 | setIsRunning(false); 18 | } 19 | }, 20 | error: () => { 21 | setError("Error"); 22 | setIsRunning(false); 23 | }, 24 | complete: () => { 25 | setIsRunning(false); 26 | }, 27 | }); 28 | 29 | const runShell = useCallback(() => { 30 | setIsRunning(true); 31 | repl.eval(code, index ?? 0); 32 | }, [setIsRunning, repl]); 33 | 34 | return ( 35 | 36 | 37 | {isRunning && Running...} 38 | {result && UnitResultDispatcher(result)} 39 | {error && {error}} 40 | 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /src/uitls/crypto.util.ts: -------------------------------------------------------------------------------- 1 | import { createCipheriv, createDecipheriv, randomBytes, createHash } from "node:crypto"; 2 | 3 | if (!process.env["ENC_KEY"]) { 4 | // for skip CI 5 | // throw Error("No secret key env in the server."); 6 | console.log("No secret key env in the server."); 7 | } 8 | 9 | const hasher = createHash("sha256"); 10 | const secret = process.env["ENC_KEY"] || ""; 11 | function genIV() { 12 | return Buffer.from(randomBytes(16)); 13 | } 14 | 15 | function encrypt(data: string, secret: string, iv: Buffer) { 16 | const cipher = createCipheriv("aes-256-cbc", secret, iv); 17 | let encrypted = cipher.update(data, "utf8", "hex"); 18 | encrypted += cipher.final("hex"); 19 | return encrypted; 20 | } 21 | 22 | function decrypt(encrypted: string, secret: string, iv: string) { 23 | const ivBuffer = Buffer.from(iv, "hex"); 24 | const decipher = createDecipheriv("aes-256-cbc", secret, ivBuffer); 25 | let decrypted = decipher.update(encrypted, "hex", "utf8"); 26 | decrypted += decipher.final("utf8"); 27 | return decrypted; 28 | } 29 | 30 | export function hashedKey(key: string) { 31 | return hasher.copy().update(key).digest().toString("hex"); 32 | } 33 | 34 | export function encryptedKey(key: string) { 35 | const iv = genIV(); 36 | const key_encrypted = encrypt(key, secret, iv); 37 | return { 38 | iv, 39 | key_encrypted, 40 | }; 41 | } 42 | 43 | export function decryptKey(encryptedKey: string, iv: string) { 44 | return decrypt(encryptedKey, secret, iv); 45 | } 46 | -------------------------------------------------------------------------------- /src/configs/next-seo-config.ts: -------------------------------------------------------------------------------- 1 | export const NEXT_SEO_DEFAULT = { 2 | title: "ClickPrompt - Streamline your prompt design", 3 | description: 4 | "ClickPrompt 是一款专为Prompt编写者设计的工具,它支持多种基于Prompt的AI应用,例如Stable Diffusion、ChatGPT和GitHub Copilot等。使用ClickPrompt,您可以轻松地查看、分享和一键运行这些模型,同时提供在线的Prompt生成器,使用户能够根据自己的需求轻松创建符合要求的Prompt,并与其他人分享", 5 | openGraph: { 6 | type: "website", 7 | locale: "zh_CN", 8 | url: "https://www.clickprompt.org/", 9 | title: "ClickPrompt - Streamline your prompt design", 10 | description: 11 | "ClickPrompt 是一款专为Prompt编写者设计的工具,它支持多种基于Prompt的AI应用,例如Stable Diffusion、ChatGPT和GitHub Copilot等。使用ClickPrompt,您可以轻松地查看、分享和一键运行这些模型,同时提供在线的Prompt生成器,使用户能够根据自己的需求轻松创建符合要求的Prompt,并与其他人分享", 12 | images: [ 13 | { 14 | url: "/favicon/favicon-16x16.png", 15 | width: 16, 16 | height: 16, 17 | alt: "ClickPrompt", 18 | type: "image/jpeg", 19 | }, 20 | { 21 | url: "/favicon/favicon-32x32.png", 22 | width: 32, 23 | height: 32, 24 | alt: "ClickPrompt", 25 | type: "image/jpeg", 26 | }, 27 | { 28 | url: "/favicon/favicon-192x192.png", 29 | width: 192, 30 | height: 192, 31 | alt: "ClickPrompt", 32 | type: "image/jpeg", 33 | }, 34 | { 35 | url: "/favicon/favicon-512x512.png", 36 | width: 512, 37 | height: 512, 38 | alt: "ClickPrompt", 39 | type: "image/jpeg", 40 | }, 41 | ], 42 | siteName: "ClickPrompt", 43 | }, 44 | }; 45 | -------------------------------------------------------------------------------- /src/pages/api/action/proxy.ts: -------------------------------------------------------------------------------- 1 | import { NextApiHandler } from "next"; 2 | import fetch from "node-fetch"; 3 | 4 | export type ApiAction = { 5 | url: string; 6 | method: string; 7 | headers: { 8 | key: string; 9 | value: string; 10 | }[]; 11 | body: string; 12 | }; 13 | 14 | const handler: NextApiHandler = async (req, res) => { 15 | if (req.method !== "POST" || !req.body) { 16 | res.status(400).json({ error: "Invalid request" }); 17 | return; 18 | } 19 | const { url, method, headers, body } = req.body as ApiAction; 20 | 21 | const proxy_body: any = typeof body === "string" ? JSON.parse(body) : body; 22 | 23 | const browserHeaders: Record = headers.reduce( 24 | (acc, { key, value }) => ({ 25 | ...acc, 26 | [key]: value, 27 | }), 28 | {}, 29 | ); 30 | 31 | // ignore body when method is GET 32 | if (method === "GET") { 33 | delete browserHeaders["body"]; 34 | } 35 | 36 | const response = await fetch(url, { 37 | method, 38 | headers: browserHeaders, 39 | body: JSON.stringify(proxy_body), 40 | }); 41 | 42 | console.log("create proxy request: ", method, url, browserHeaders); 43 | console.log("proxy response: ", response.status, response.statusText); 44 | 45 | if (response.ok) { 46 | const body = await response.json(); 47 | return res.status(response.status).json(body); 48 | } else { 49 | return res.status(response.status).json({ 50 | error: await response.json(), 51 | }); 52 | } 53 | }; 54 | 55 | export default handler; 56 | -------------------------------------------------------------------------------- /src/app/[lang]/layout.tsx: -------------------------------------------------------------------------------- 1 | import "@/app/globals.css"; 2 | import React from "react"; 3 | import NavBar from "@/layout/NavBar"; 4 | import { Provider } from "@/components/ChakraUI/Provider"; 5 | import { Analytics } from "@vercel/analytics/react"; 6 | 7 | type RootLayoutProps = { 8 | params: { 9 | lang: string; 10 | }; 11 | children: React.ReactNode; 12 | }; 13 | export default function RootLayout({ params, children }: RootLayoutProps) { 14 | const { lang } = params; 15 | 16 | return ( 17 | 18 | 19 | 20 | 21 | 22 | ChatFlow, personalize your ChatGPT workflows and build the road to automation。 23 | 27 | 28 | 29 | 30 | 31 | {/* https://github.com/vercel/next.js/issues/42292 */} 32 |
33 | {/* @ts-expect-error Async Server Component */} 34 | 35 |
36 | {children} 37 |
38 | 39 | 40 | 41 | 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer utilities { 6 | /* good looking scrollbar */ 7 | .overflow-container::-webkit-scrollbar { 8 | width: 8px; 9 | } 10 | 11 | .overflow-container::-webkit-scrollbar-track { 12 | background: #f1f1f1; 13 | } 14 | 15 | .overflow-container::-webkit-scrollbar-thumb { 16 | background: #888; 17 | } 18 | 19 | .overflow-container::-webkit-scrollbar-thumb:hover { 20 | background: #555; 21 | } 22 | } 23 | 24 | #root { 25 | margin: 0 auto; 26 | text-align: center; 27 | } 28 | 29 | code { 30 | text-shadow: none !important; 31 | } 32 | 33 | .logo { 34 | height: 6em; 35 | padding: 1.5em; 36 | will-change: filter; 37 | transition: filter 300ms; 38 | } 39 | .logo:hover { 40 | filter: drop-shadow(0 0 2em #646cffaa); 41 | } 42 | .logo.react:hover { 43 | filter: drop-shadow(0 0 2em #61dafbaa); 44 | } 45 | 46 | @keyframes logo-spin { 47 | from { 48 | transform: rotate(0deg); 49 | } 50 | to { 51 | transform: rotate(360deg); 52 | } 53 | } 54 | 55 | @media (prefers-reduced-motion: no-preference) { 56 | a:nth-of-type(2) .logo { 57 | animation: logo-spin infinite 20s linear; 58 | } 59 | } 60 | 61 | .card { 62 | padding: 2em; 63 | } 64 | 65 | .read-the-docs { 66 | color: #888; 67 | } 68 | 69 | ul, 70 | ul li, 71 | p { 72 | text-align: left; 73 | } 74 | /* custom grid-cols */ 75 | .grid-cols-\[1rem_1fr\] { 76 | grid-template-columns: 1rem 1fr; 77 | } 78 | .grid-cols-\[200px_1fr\] { 79 | grid-template-columns: 200px 1fr; 80 | } 81 | -------------------------------------------------------------------------------- /i18n/README.md: -------------------------------------------------------------------------------- 1 | # i18n files 2 | 3 | Inside this folder, the first folder level is locale code such as `en-US`, and in it has A LOT of json files the naming convention is: 4 | 5 | - Global data is in the `$.json` file. 6 | - For specific page data: 7 | - index page is corresponding to `_.json` file 8 | - other pages just use pathname without trailing slash and locale segment, and replace all `/` with `_`(cause in some filesystem `/` is illegal charactor in pathname). such as `_foo.json` for `/foo/`, `_foo_bar.json` for `/foo/bar/` . I think you get the idea. 9 | 10 | # HOW TO USE IN RSC(React server component) 11 | 12 | ```typescript jsx 13 | // page.server.tsx 14 | import { getAppData } from "@/i18n"; 15 | import CSC from "./component.client.tsx"; 16 | 17 | async function RscFoo() { 18 | // ... 19 | const { locale, pathname, i18n } = await getAppData(); 20 | const t = i18n.tFactory("/"); 21 | // t is a function takes key and give you value in the json file 22 | t("title"); // will be "Streamline your prompt design" 23 | 24 | // you can also access global data by 25 | const g = i18n.g; 26 | 27 | const i18nProps: GeneralI18nProps = { 28 | locale, 29 | pathname, 30 | i18n: { 31 | dict: i18n.dict, 32 | }, 33 | }; 34 | 35 | // use i18n in CSC (client side component) 36 | return ; 37 | // ... 38 | } 39 | ``` 40 | 41 | ```typescript jsx 42 | // component.client.tsx 43 | "use client"; 44 | 45 | export default function CSC({ i18n }: GeneralI18nProps) { 46 | const { dict } = i18n; 47 | 48 | // use dict like plain object here 49 | } 50 | ``` 51 | -------------------------------------------------------------------------------- /src/flows/react-flow-nodes/PromptNode.tsx: -------------------------------------------------------------------------------- 1 | import styled from "@emotion/styled"; 2 | import React from "react"; 3 | import { Handle, Position } from "reactflow"; 4 | 5 | type TextNodeProps = { 6 | isConnectable: boolean; 7 | data: { label: string }; 8 | }; 9 | 10 | function PromptNode(props: TextNodeProps) { 11 | const { isConnectable } = props; 12 | 13 | return ( 14 | 15 | 16 | Prompt 17 | {props.data.label} 18 | 19 | 20 | ); 21 | } 22 | 23 | const TextNodeStyle = styled.div` 24 | min-height: 50px; 25 | width: 120px; 26 | border: 1px solid #555; 27 | border-radius: 5px; 28 | background: white; 29 | font-family: jetbrains-mono, "JetBrains Mono", monospace; 30 | `; 31 | 32 | const CardTitle = styled.div` 33 | display: block; 34 | height: 20px; 35 | // 120px - 1px * 2; 36 | width: 118px; 37 | background: #eee; 38 | 39 | border-top-left-radius: 5px; 40 | border-top-right-radius: 5px; 41 | 42 | border-bottom-width: 1px; 43 | border-bottom-style: solid; 44 | border-color: #555555; 45 | font-size: 10px; 46 | text-align: center; 47 | font-weight: bold; 48 | `; 49 | 50 | const Title = styled.div` 51 | padding: 0 2px; 52 | border-color: #eee; 53 | display: block; 54 | width: 120px; 55 | overflow-y: auto; 56 | font-size: 12px; 57 | text-align: center; 58 | `; 59 | 60 | export default PromptNode; 61 | -------------------------------------------------------------------------------- /src/flows/types/flow-step.ts: -------------------------------------------------------------------------------- 1 | import { FlowAction } from "@/flows/types/flow-action"; 2 | 3 | export type FlowStep = { 4 | name: string; 5 | ask: string; 6 | hiddenExecute?: boolean; 7 | response?: string; 8 | markdownEditor?: boolean; 9 | cachedResponseRegex: string; 10 | values: Record; 11 | preActions: FlowAction[]; 12 | postActions: FlowAction[]; 13 | }; 14 | 15 | export function fillStepWithValued( 16 | step: FlowStep, 17 | cachedValue: Record, 18 | ): { replaced: boolean; ask: string } { 19 | const regex = new RegExp(/\$\$([a-zA-Z0-9_]+)\$\$/); 20 | let newValue = step.ask; 21 | let isChanged = false; 22 | // 2. find $$placeholder$$ in step.ask 23 | if (step.ask && step.values) { 24 | const matched = step.ask.match(regex); 25 | if (matched) { 26 | // 1. replace $$placeholder$$ with step.values.placeholder 27 | const placeholder = matched[1]; 28 | const value = step.values[placeholder]; 29 | if (value) { 30 | isChanged = true; 31 | newValue = step.ask.replace(regex, value); 32 | } 33 | } 34 | } 35 | 36 | // 3. find value in cachedValue, format: $$response:1$$ 37 | if (step.ask && cachedValue) { 38 | const regex = new RegExp(/\$\$response:([0-9]+)\$\$/); 39 | const matched = step.ask.match(regex); 40 | if (matched) { 41 | const index = parseInt(matched[1]); 42 | const value = cachedValue[index]; 43 | if (value) { 44 | isChanged = true; 45 | newValue = step.ask.replace(regex, value); 46 | } 47 | } 48 | } 49 | 50 | return { replaced: isChanged, ask: newValue }; 51 | } 52 | -------------------------------------------------------------------------------- /src/flows/components/PostFlowAction.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { FlowAction } from "@/flows/types/flow-action"; 4 | import { postActionDispatcher } from "@/flows/post-action-dispatcher"; 5 | import SharedFlowAction from "@/flows/components/SharedFlowAction"; 6 | import { PostComponentDispatcher } from "@/flows/components/PostComponentDispatcher"; 7 | import { processDispatcher } from "@/flows/components/ProcessDispatcher"; 8 | 9 | type ActionProps = { action: FlowAction; response: string }; 10 | 11 | function PostFlowAction({ action, response }: ActionProps) { 12 | const [isShowPostComponent, setIsShowPostComponent] = React.useState(false); 13 | const [result, setResult] = React.useState(null); 14 | 15 | const postComponents = action.postComponents; 16 | const hasPostComponent = postComponents?.length && postComponents?.length > 0; 17 | 18 | const handleSubmit = (modifiedAction: FlowAction) => { 19 | postActionDispatcher(modifiedAction, response).then((r) => { 20 | if (r.success) { 21 | // handle in here 22 | let newResult = r.result; 23 | if (action.postProcess) { 24 | newResult = processDispatcher(action.postProcess, r.result); 25 | } 26 | setResult(newResult); 27 | if (hasPostComponent) { 28 | setIsShowPostComponent(true); 29 | } 30 | } 31 | }); 32 | }; 33 | 34 | return ( 35 | <> 36 | 37 | {isShowPostComponent && hasPostComponent && PostComponentDispatcher(postComponents, result)} 38 | 39 | ); 40 | } 41 | 42 | export default PostFlowAction; 43 | -------------------------------------------------------------------------------- /src/flows/components/PreFlowAction.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Flex } from "@chakra-ui/react"; 3 | 4 | import { FlowAction } from "@/flows/types/flow-action"; 5 | import { PostComponentDispatcher } from "@/flows/components/PostComponentDispatcher"; 6 | import SharedFlowAction from "./SharedFlowAction"; 7 | import { preActionDispatcher } from "../pre-action-dispatcher"; 8 | import { processDispatcher } from "./ProcessDispatcher"; 9 | 10 | type ActionProps = { action: FlowAction; onResponse?: (value: any) => void }; 11 | 12 | function PreFlowAction({ action }: ActionProps) { 13 | const [isShowPostComponent, setIsShowPostComponent] = React.useState(false); 14 | const [result, setResult] = React.useState(null); 15 | 16 | const postComponents = action.postComponents; 17 | const hasPostComponent = postComponents?.length && postComponents?.length > 0; 18 | 19 | const handleSubmit = (modifiedAction: FlowAction) => { 20 | preActionDispatcher(modifiedAction).then((r) => { 21 | if (r.success) { 22 | let newResult = r.result; 23 | if (action.postProcess) { 24 | newResult = processDispatcher(action.postProcess, r.result); 25 | } 26 | setResult(newResult); 27 | if (hasPostComponent) { 28 | setIsShowPostComponent(true); 29 | } 30 | } 31 | }); 32 | }; 33 | 34 | return ( 35 | 36 | 37 | {isShowPostComponent && hasPostComponent && PostComponentDispatcher(postComponents, result)} 38 | 39 | ); 40 | } 41 | 42 | export default PreFlowAction; 43 | -------------------------------------------------------------------------------- /src/components/UnitRuntime/renderer/ReactRenderer.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from "react"; 2 | import { ReactBundleContent } from "@/flows/unitmesh/ascode"; 3 | 4 | type ReactRendererParams = { code: string; bundle_scripts: ReactBundleContent }; 5 | 6 | function ReactRenderer({ code, bundle_scripts }: ReactRendererParams) { 7 | const iframe$ = useRef(null); 8 | 9 | useEffect(() => { 10 | if (iframe$.current && code) { 11 | const ifr = iframe$.current; 12 | const reactLoaderScript = document.createElement("script"); 13 | const reactDomLoaderScript = document.createElement("script"); 14 | reactLoaderScript.src = bundle_scripts.react; 15 | reactDomLoaderScript.src = bundle_scripts.reactDom; 16 | 17 | // create div#root 18 | const rootDom = document.createElement("div"); 19 | rootDom.id = "root"; 20 | ifr?.contentDocument?.body.append(rootDom); 21 | 22 | const script = document.createElement("script"); 23 | script.innerHTML = code; 24 | reactLoaderScript.onload = () => { 25 | reactDomLoaderScript.onload = () => { 26 | ifr?.contentDocument?.body.append(script); 27 | }; 28 | 29 | ifr?.contentDocument?.body.append(reactDomLoaderScript); 30 | }; 31 | ifr?.contentDocument?.body.append(reactLoaderScript); 32 | 33 | return () => { 34 | if (ifr?.contentDocument?.body) { 35 | ifr.contentDocument.body.innerHTML = ""; 36 | } 37 | }; 38 | } 39 | }, [code]); 40 | 41 | return ; 42 | } 43 | 44 | export default ReactRenderer; 45 | -------------------------------------------------------------------------------- /src/flows/flow-functions/math.ts: -------------------------------------------------------------------------------- 1 | import { Parser } from "expr-eval"; 2 | import { isArray } from "lodash-es"; 3 | 4 | /** 5 | * Math evaluator 6 | * @param expr, the expression to evaluate 7 | * @param value, if the value is an array, evaluate the expression for each element, if the value is an object, evaluate the expression for the object 8 | * @param updatePropKey, if the value is an object, update the value of the key 9 | * 10 | * @example 11 | * const expr = "value.x + 1"; 12 | * const value = { x: 1 }; 13 | * const propKeys = ["x"]; 14 | * 15 | * const result = math(expr, value, propKeys); 16 | * // result = { x: 2 } 17 | */ 18 | export const math = (expr: string, value: any, updatePropKey?: string) => { 19 | if (isArray(value)) { 20 | return value.map((v) => exprMath(v, updatePropKey, expr)); 21 | } 22 | 23 | return exprMath(value, updatePropKey, expr); 24 | }; 25 | 26 | function executeEval(expression: string, value: any) { 27 | const parser = new Parser(); 28 | try { 29 | const expr = parser.parse(expression); 30 | return expr.evaluate({ value }); 31 | } catch (e) { 32 | console.log(e); 33 | return value; 34 | } 35 | } 36 | 37 | function exprObjectMath(value: any, updatePropKey: string | undefined, expr: string) { 38 | const newValue = value; 39 | if (updatePropKey) { 40 | newValue[updatePropKey] = executeEval(expr, value); 41 | } 42 | 43 | return newValue; 44 | } 45 | 46 | function exprMath(value: any, updatePropKey: string | undefined, expr: string) { 47 | if (typeof value === "object") { 48 | return exprObjectMath(value, updatePropKey, expr); 49 | } 50 | 51 | return executeEval(expr, value); 52 | } 53 | -------------------------------------------------------------------------------- /src/api/user.ts: -------------------------------------------------------------------------------- 1 | import fetch from "node-fetch"; 2 | import { SITE_INTERNAL_HEADER_URL } from "@/configs/constants"; 3 | 4 | export async function logout() { 5 | const response = await fetch("/api/chatgpt/user", { 6 | method: "POST", 7 | body: JSON.stringify({ 8 | action: "logout", 9 | }), 10 | }); 11 | return response.json(); 12 | } 13 | 14 | export async function login(key: string) { 15 | const response = await fetch("/api/chatgpt/user", { 16 | method: "POST", 17 | body: JSON.stringify({ 18 | action: "login", 19 | key, 20 | }), 21 | }).then((it) => it.json()); 22 | 23 | if ((response as any).error) { 24 | alert("Error(login): " + JSON.stringify((response as any).error)); 25 | return; 26 | } 27 | 28 | return response; 29 | } 30 | 31 | export async function isLoggedIn(hashedKey?: string) { 32 | if (typeof window !== "undefined" && typeof document !== "undefined") { 33 | // Client-side 34 | const response = await fetch("/api/chatgpt/verify", { 35 | method: "POST", 36 | body: hashedKey ?? "NOPE", 37 | }).then((it) => it.json()); 38 | 39 | return (response as any).loggedIn; 40 | } 41 | 42 | const { headers } = await import("next/headers"); 43 | const urlStr = headers().get(SITE_INTERNAL_HEADER_URL) as string; 44 | // Propagate cookies to the API route 45 | const headersPropagated = { cookie: headers().get("cookie") as string }; 46 | const response = await fetch(new URL("/api/chatgpt/verify", new URL(urlStr)), { 47 | method: "POST", 48 | body: hashedKey ?? "NOPE", 49 | headers: headersPropagated, 50 | redirect: "follow", 51 | }).then((it) => it.json()); 52 | return (response as any).loggedIn; 53 | } 54 | -------------------------------------------------------------------------------- /src/components/LocaleSwitcher.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { SITE_LOCALE_COOKIE } from "@/configs/constants"; 4 | import { Box, Menu, MenuButton, MenuList, MenuItem } from "@/components/ChakraUI"; 5 | import { ChevronDownIcon } from "@/components/ChakraUI/icons"; 6 | 7 | const options = [ 8 | { 9 | value: "zh-CN", 10 | label: "中文", 11 | }, 12 | { 13 | value: "en-US", 14 | label: "English", 15 | }, 16 | ]; 17 | export default function LocaleSwitcher({ locale }: { locale: string }) { 18 | const classZh = locale === "zh-CN" ? "text-blue-500" : "text-gray-500"; 19 | const classEn = locale === "en-US" ? "text-blue-500" : "text-gray-500"; 20 | function setEn() { 21 | document.cookie = `${SITE_LOCALE_COOKIE}=en-US;path=/;max-age=31536000;`; 22 | window.location.reload(); 23 | } 24 | 25 | function setZh() { 26 | document.cookie = `${SITE_LOCALE_COOKIE}=zh-CN;path=/;max-age=31536000;`; 27 | window.location.reload(); 28 | } 29 | 30 | return ( 31 |
32 | 33 | 34 | {locale === "zh-CN" ? "中文" : "English"} 35 | 36 | 37 | 38 | {options.map((child) => ( 39 | (child.value === "zh-CN" ? setZh() : setEn())} 43 | > 44 | 45 | {child.label} 46 | 47 | 48 | ))} 49 | 50 | 51 |
52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /src/assets/clickprompt-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/flows/explain/FlowExplain.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactFlow, { Background, Controls } from "reactflow"; 3 | import { Edge, Node } from "@reactflow/core/dist/esm/types"; 4 | import "reactflow/dist/style.css"; 5 | 6 | import InteractiveNode from "@/flows/react-flow-nodes/InteractiveNode"; 7 | import { explainParser, graphToFlow } from "@/data-processor/explain-parser"; 8 | import { StartlingFlow } from "@/flows/types/click-flow"; 9 | import PromptNode from "@/flows/react-flow-nodes/PromptNode"; 10 | 11 | type StepExplainProps = { 12 | step: StartlingFlow; 13 | }; 14 | 15 | type NodeInfo = { 16 | id: string; 17 | label: string | undefined; 18 | width: number; 19 | height: number; 20 | position: { x: number; y: number }; 21 | }; 22 | 23 | const nodeTypes = { interactive: InteractiveNode, prompt: PromptNode }; 24 | 25 | function FlowExplain(props: StepExplainProps) { 26 | const graph = explainParser(props.step.explain || ""); 27 | const flowGraph = graphToFlow(graph); 28 | 29 | function getLabel(node: NodeInfo) { 30 | const id = parseInt(node.id) || 0; 31 | return props.step.steps[id]?.name || ""; 32 | } 33 | 34 | const initialNodes: Node[] = flowGraph.nodes.map((node) => { 35 | return { 36 | id: node.id, 37 | data: { label: getLabel(node) }, 38 | position: node.position, 39 | type: node.data?.flowType || "prompt", 40 | }; 41 | }); 42 | 43 | const initialEdges: Edge[] = flowGraph.edges.map((edge) => { 44 | return { id: edge.id, source: edge.source, target: edge.target, type: "step" }; 45 | }); 46 | 47 | return ( 48 |
49 | 50 | 51 | 52 | 53 |
54 | ); 55 | } 56 | 57 | export default FlowExplain; 58 | -------------------------------------------------------------------------------- /src/flows/components/FlowMarkdownEditor.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { useCallback } from "react"; 4 | import styled from "@emotion/styled"; 5 | import { useDocChanged, useHelpers, useKeymap } from "@remirror/react"; 6 | import { MarkdownEditor } from "@remirror/react-editors/markdown"; 7 | import { KeyBindingProps } from "@remirror/core-types/dist-types/core-types"; 8 | 9 | const hooks = [ 10 | () => { 11 | const { getJSON } = useHelpers(); 12 | 13 | const handleSaveShortcut = useCallback( 14 | ({ state }: KeyBindingProps) => { 15 | console.log(`Save to backend: ${JSON.stringify(getJSON(state))}`); 16 | 17 | return true; 18 | }, 19 | [getJSON], 20 | ); 21 | 22 | useKeymap("Mod-s", handleSaveShortcut); 23 | }, 24 | ]; 25 | 26 | export const OnTextChange = ({ onChange }: { onChange: (html: string) => void }): null => { 27 | const { getMarkdown } = useHelpers(); 28 | 29 | useDocChanged( 30 | useCallback( 31 | ({ state }) => { 32 | const string = getMarkdown(state); 33 | onChange(string); 34 | }, 35 | [onChange, getMarkdown], 36 | ), 37 | ); 38 | 39 | return null; 40 | }; 41 | 42 | /** 43 | * @deprecated: Don't direct use this, Use instead 44 | */ 45 | const FlowMarkdownEditor = ({ text, onChange }: { text: string; onChange: (text: string) => void }) => { 46 | const valueChange = (value: string) => { 47 | onChange(value || ""); 48 | }; 49 | 50 | return ( 51 | 52 | 53 | valueChange(text)} /> 54 | 55 | 56 | ); 57 | }; 58 | 59 | const StyledMarkdownContainer = styled.div` 60 | background: #fff; 61 | width: 100%; 62 | `; 63 | 64 | export default FlowMarkdownEditor; 65 | -------------------------------------------------------------------------------- /src/flows/unitmesh/ReplService.ts: -------------------------------------------------------------------------------- 1 | import { Subject } from "rxjs"; 2 | import { WebSocketSubject } from "rxjs/internal/observable/dom/WebSocketSubject"; 3 | 4 | import { ReplResult } from "./ascode"; 5 | 6 | export class ReplService { 7 | private subject: WebSocketSubject; 8 | private idSubjectMap: Record> = {}; 9 | private codes: Record = {}; 10 | private indexId = 0; 11 | private runningCodeIds: number[] = []; 12 | 13 | private runAllSub = new Subject(); 14 | private isRunAll = false; 15 | 16 | constructor(subject: WebSocketSubject) { 17 | this.subject = subject; 18 | 19 | // eslint-disable-next-line @typescript-eslint/no-this-alias 20 | const that = this; 21 | this.subject.subscribe({ 22 | next: (msg: ReplResult) => { 23 | if (that.idSubjectMap[msg.id] != null) { 24 | const sub: Subject = that.idSubjectMap[msg.id]; 25 | sub.next(msg); 26 | } 27 | 28 | const isRunAll = that.runningCodeIds.length > 0; 29 | if (isRunAll) { 30 | that.runningCodeIds.forEach((item, index) => { 31 | if (item == msg.id) that.runningCodeIds.splice(index, 1); 32 | }); 33 | 34 | if (that.isRunAll && that.runningCodeIds.length == 0) { 35 | that.isRunAll = false; 36 | that.runAllSub.next("done"); 37 | } 38 | } 39 | }, 40 | error: (err) => { 41 | console.error(err); 42 | }, 43 | complete: () => { 44 | console.log("complete"); 45 | }, 46 | }); 47 | } 48 | 49 | getSubject() { 50 | return this.subject; 51 | } 52 | 53 | register() { 54 | this.indexId += 1; 55 | const subject = new Subject(); 56 | this.idSubjectMap[this.indexId] = subject; 57 | return { 58 | id: this.indexId, 59 | subject, 60 | }; 61 | } 62 | 63 | eval(code: string, id: number) { 64 | this.subject.next({ code: code, id: id }); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/assets/chatgpt/flow/domain-driven-design.yml: -------------------------------------------------------------------------------- 1 | name: Domain Driven Design 2 | description: Domain Driven Design 3 | category: Development 4 | author: ClickPrompt Team 5 | explain: | 6 | digraph G { 7 | 0[flowType = "prompt"] 8 | 1[flowType = "interactive"] 9 | 2[flowType = "interactive"] 10 | 3[flowType = "interactive"] 11 | 4[flowType = "interactive"] 12 | 5[flowType = "interactive"] 13 | 6[flowType = "interactive"] 14 | 0 -> 1 15 | 1 -> 2 16 | 1 -> 3 17 | 1 -> 4 18 | 1 -> 5 19 | 1 -> 6 20 | } 21 | steps: 22 | - name: 定义 DDD 步骤 23 | ask: | 24 | 我们来定义一下 DDD 游戏的步骤,一共有 6 个步骤,步骤如下: 25 | 26 | """ 27 | 28 | 第一步. 拆解场景。分析特定领域的所有商业活动,并将其拆解出每个场景。 29 | 30 | 第二步. 场景与过程分析。选定一个场景,并使用 "{名词}已{动词}" 的形式描述过程中所有发生的事件,其中的名词是过程中的实体,其中的动词是实体相关的行为。 31 | 32 | 第三步. 针对场景建模。基于统一语言和拆解出的场景进行建模,以实现 DDD 设计与代码实现的双向绑定。 33 | 34 | 第四步. 持续建模。回到第一步,选择未完成的场景。你要重复第一到第四步,直到所有的场景完成。 35 | 36 | 第五步. 围绕模型生成子域。对模型进行分类,以划定不同的子域,需要列出所有的模型包含英语翻译。 37 | 38 | 第六步. API 生成。对于每一个子域,生成其对应的 RESTful API,并以表格的形式展现这些 API。 39 | 40 | """ 41 | 42 | 需要注意的是,当我说 """ddd 第 {} 步: {}""" 则表示进行第几步的分析,如 """ddd 第一步 : 博客系统""" 表示只对博客系统进行 DDD 第一步分析。我发的是 """ddd : { }""",则表示按 6 个步骤分析整个系统。明白这个游戏怎么玩了吗? 43 | cachedResponseRegex: 44 | - name: 设计在线博客 45 | ask: ddd $$placeholder$$ 46 | values: 47 | placeholder: 在线博客系统 48 | 49 | - name: 第二步:场景与过程分析 50 | ask: | 51 | ddd 第二步: $$placeholder$$ 52 | values: 53 | placeholder: 在线博客系统 54 | 55 | - name: 第三步:针对场景建模 56 | ask: | 57 | ddd 第三步: $$placeholder$$ 58 | values: 59 | placeholder: 在线博客系统 60 | 61 | - name: 第四步:持续建模 62 | ask: | 63 | ddd 第四步: $$placeholder$$ 64 | values: 65 | placeholder: 在线博客系统 66 | 67 | - name: 第五步:围绕模型生成子域 68 | ask: | 69 | ddd 第五步: $$placeholder$$ 70 | values: 71 | placeholder: 在线博客系统 72 | 73 | - name: 第六步:设计 API 74 | ask: | 75 | ddd 第六步: $$placeholder$$ 76 | values: 77 | placeholder: 在线博客系统 78 | -------------------------------------------------------------------------------- /src/pages/api/chatgpt/user.ts: -------------------------------------------------------------------------------- 1 | import { NextApiHandler } from "next"; 2 | import { SITE_USER_COOKIE } from "@/configs/constants"; 3 | import { createUser, isValidUser } from "@/storage/planetscale"; 4 | import { encryptedKey, hashedKey } from "@/uitls/crypto.util"; 5 | 6 | // type Request = { 7 | // actions: "login" | "logout"; 8 | // key?: string; 9 | // }; 10 | 11 | type Response = { 12 | message?: string; 13 | error?: string; 14 | }; 15 | 16 | const handler: NextApiHandler = async (req, res) => { 17 | if (!(req.method === "POST" && req.body)) { 18 | res.status(404).json({ error: "Not found" }); 19 | return; 20 | } 21 | 22 | const userIdInCookie = req.cookies[SITE_USER_COOKIE]; 23 | const { key, action } = typeof req.body === "string" ? JSON.parse(req.body) : req.body; 24 | 25 | if (!action) { 26 | res.status(400).json({ error: "No query provided" }); 27 | return; 28 | } 29 | 30 | switch (action) { 31 | case "login": 32 | if (key) { 33 | const key_hashed = hashedKey(key); 34 | 35 | if (!(await isValidUser(key_hashed))) { 36 | const { iv, key_encrypted } = encryptedKey(key); 37 | await createUser({ 38 | iv: iv.toString("hex"), 39 | key_hashed, 40 | key_encrypted, 41 | }); 42 | } 43 | 44 | res.setHeader("Set-Cookie", `${SITE_USER_COOKIE}=${key_hashed}; Max-Age=3600; HttpOnly; Path=/;`); 45 | return res.status(200).json({ message: "Logged in" } as Response); 46 | } else { 47 | return res.status(400).json({ error: "No key provided" } as Response); 48 | } 49 | case "logout": 50 | if (!userIdInCookie) { 51 | return res.status(200).json({ error: "You're not logged in yet!" } as Response); 52 | } 53 | 54 | res.setHeader("Set-Cookie", `${SITE_USER_COOKIE}=; Max-Age=0; HttpOnly; Path=/;`); 55 | return res.status(200).json({ message: "Logged out" } as Response); 56 | default: 57 | return res.status(400).json({ error: "Unknown actions" } as Response); 58 | } 59 | }; 60 | export default handler; 61 | -------------------------------------------------------------------------------- /src/data-processor/explain-parser.ts: -------------------------------------------------------------------------------- 1 | import parse from "dotparser"; 2 | import dagre, { graphlib } from "dagre"; 3 | import Graph = graphlib.Graph; 4 | 5 | export const explainParser = (str: string) => { 6 | const ast = parse(str); 7 | const graph = new Graph(); 8 | 9 | graph.setGraph({ 10 | rankdir: "LR", 11 | }); 12 | graph.setDefaultEdgeLabel(() => ({})); 13 | 14 | const children = ast[0].children; 15 | 16 | const nodes = children.filter((item: any) => item.type === "node_stmt"); 17 | const edges = children.filter((item: any) => item.type === "edge_stmt"); 18 | 19 | nodes.forEach((node: any) => { 20 | const data = node.attr_list.reduce((acc: any, item: any) => { 21 | acc[item.id] = item.eq; 22 | return acc; 23 | }, {}); 24 | graph.setNode(node.node_id.id, { label: node.node_id.id, width: 120, height: 50, data }); 25 | }); 26 | 27 | edges.forEach((edge: any) => { 28 | graph.setEdge(edge.edge_list[0].id, edge.edge_list[1].id); 29 | }); 30 | 31 | dagre.layout(graph); 32 | return graph; 33 | }; 34 | 35 | type CustomFieldData = { 36 | flowType?: string; 37 | }; 38 | 39 | type FlowGraph = { 40 | nodes: { 41 | id: string; 42 | label: string | undefined; 43 | width: number; 44 | height: number; 45 | position: { 46 | x: number; 47 | y: number; 48 | }; 49 | data?: CustomFieldData; 50 | }[]; 51 | edges: { 52 | id: string; 53 | source: string; 54 | target: string; 55 | }[]; 56 | }; 57 | 58 | export function graphToFlow(graph: Graph): FlowGraph { 59 | const nodes = graph.nodes().map((nodeStr) => { 60 | const node = graph.node(nodeStr); 61 | const { label, width, height, x, y } = node; 62 | let data = {}; 63 | if (node.hasOwnProperty("data")) { 64 | data = (node as any)["data"]; 65 | } 66 | 67 | return { id: nodeStr, label, width, height, position: { x, y }, data }; 68 | }); 69 | const edges = graph.edges().map((edge) => { 70 | const { v, w } = edge; 71 | return { id: `${v}-${w}`, source: v, target: w }; 72 | }); 73 | 74 | return { nodes, edges }; 75 | } 76 | -------------------------------------------------------------------------------- /src/flows/components/SharedFlowAction.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { Fragment } from "react"; 4 | import { useFormik } from "formik"; 5 | import { Button, FormControl, Grid, Input } from "@chakra-ui/react"; 6 | 7 | import { AuthKeyValues, FlowAction } from "@/flows/types/flow-action"; 8 | import { parseConfigures } from "@/flows/components/SettingHeaderConfig"; 9 | 10 | type ActionProps = { action: FlowAction; onSubmit?: (modifiedAction: FlowAction) => void }; 11 | 12 | function SharedFlowAction({ action, onSubmit }: ActionProps) { 13 | let conf: AuthKeyValues = []; 14 | if (action.api?.headers) { 15 | conf = parseConfigures(action.api?.headers); 16 | } 17 | 18 | const formik = useFormik({ 19 | initialValues: conf.reduce((acc: any, field) => { 20 | acc[field.key] = field.value; 21 | return acc; 22 | }, {}), 23 | onSubmit: (values) => { 24 | if (conf.length > 0 && action.api?.headers) { 25 | action.api!.headers.map((header) => { 26 | if (values[header.key]) { 27 | header.value = values[header.key]; 28 | } 29 | }); 30 | } 31 | 32 | onSubmit?.(action); 33 | }, 34 | }); 35 | 36 | return ( 37 |
38 | 39 | {conf.length > 0 && 40 | conf.map((keyValue, index) => ( 41 | 42 | 43 | 44 | { 46 | const inputValue = event.target.value; 47 | formik.setFieldValue(keyValue.key, inputValue); 48 | }} 49 | /> 50 | 51 | 52 | ))} 53 | 54 | 57 | 58 |
59 | ); 60 | } 61 | 62 | export default SharedFlowAction; 63 | -------------------------------------------------------------------------------- /src/app/[lang]/click-flow/[id]/AskRenderer.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from "react"; 2 | import { Box, Textarea } from "@chakra-ui/react"; 3 | import SimpleMarkdown from "@/components/markdown/SimpleMarkdown"; 4 | import autosize from "autosize"; 5 | import styled from "@emotion/styled"; 6 | import { fillStepWithValued, FlowStep } from "@/flows/types/flow-step"; 7 | import { ReplService } from "@/flows/unitmesh/ReplService"; 8 | import { FlowMarkdownWrapper } from "@/flows/components/FlowMarkdownWrapper"; 9 | 10 | type AskRendererProps = { 11 | step: FlowStep; 12 | index?: number; 13 | onAskUpdate: (ask: string) => void; 14 | cachedValue: Record; 15 | replService?: ReplService | undefined; 16 | }; 17 | 18 | export function AskRenderer({ step, onAskUpdate, cachedValue, replService, index }: AskRendererProps) { 19 | const askTask = fillStepWithValued(step, cachedValue); 20 | const [value, setValue] = React.useState(askTask.ask); 21 | const ref = useRef(null); 22 | 23 | useEffect(() => { 24 | setValue(askTask.ask); 25 | onAskUpdate(askTask.ask); 26 | }, [askTask.ask, setValue]); 27 | 28 | useEffect(() => { 29 | if (ref.current) { 30 | autosize(ref.current); 31 | return () => { 32 | if (ref.current) autosize.destroy(ref.current); 33 | }; 34 | } 35 | }, []); 36 | 37 | if (step.markdownEditor) { 38 | return ( 39 | { 42 | setValue(text); 43 | onAskUpdate(text); 44 | }} 45 | /> 46 | ); 47 | } 48 | 49 | if (askTask.replaced) { 50 | return ( 51 | { 56 | setValue(event.target.value); 57 | onAskUpdate(event.target.value); 58 | }} 59 | /> 60 | ); 61 | } 62 | 63 | return ; 64 | } 65 | 66 | const StyledTextarea = styled(Textarea)` 67 | background: #fff; 68 | `; 69 | -------------------------------------------------------------------------------- /src/components/chatgpt/LoginPage.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Button, Input } from "@/components/ChakraUI"; 4 | import React, { Dispatch, SetStateAction } from "react"; 5 | import * as UserApi from "@/api/user"; 6 | 7 | export const LoginPage = ({ setIsLoggedIn }: { setIsLoggedIn: Dispatch> }) => { 8 | const [openAiKey, setOpenAiKey] = React.useState(""); 9 | 10 | async function login(key: string) { 11 | if (key.length === 0) { 12 | alert("Please enter your OpenAI API key first."); 13 | return; 14 | } 15 | 16 | const data = await UserApi.login(key); 17 | if (data) { 18 | setIsLoggedIn(true); 19 | } else { 20 | alert("Login failed. Please check your API key."); 21 | setIsLoggedIn(false); 22 | } 23 | } 24 | 25 | return ( 26 |
27 |

ChatGPT

28 |

You need to login first use your own key.

29 |
30 |
31 | 1. Sign up for the   32 | 33 | OpenAI Platform. 34 | 35 |
36 |
37 | 2. Create a new secret key in   38 | 39 | Settings → API keys. 40 | 41 |
42 |
3. Copy and paste your API key here:
43 |
44 |
45 | setOpenAiKey(ev.target.value)} 49 | > 50 | 58 |
59 |
60 | ); 61 | }; 62 | -------------------------------------------------------------------------------- /src/middleware.ts: -------------------------------------------------------------------------------- 1 | import { NextMiddleware, NextResponse } from "next/server"; 2 | import { SupportedLocales, getLocale, replaceRouteLocale, getLocaleFromPath, SupportedLocale } from "@/i18n"; 3 | import { 4 | SITE_INTERNAL_HEADER_LOCALE, 5 | SITE_INTERNAL_HEADER_PATHNAME, 6 | SITE_INTERNAL_HEADER_URL, 7 | SITE_LOCALE_COOKIE, 8 | } from "@/configs/constants"; 9 | 10 | export const middleware: NextMiddleware = (request) => { 11 | // Check if there is any supported locale in the pathname 12 | const pathname = request.nextUrl.pathname; 13 | const pathnameIsMissingLocale = SupportedLocales.every( 14 | (locale) => !pathname.startsWith(`/${locale}/`) && pathname !== `/${locale}`, 15 | ); 16 | 17 | let locale = getLocale(request.headers); 18 | 19 | const cookie = request.cookies.get(SITE_LOCALE_COOKIE)?.value; 20 | // If there is a cookie, and it is a supported locale, use it 21 | if (SupportedLocales.includes(cookie as unknown as SupportedLocale)) { 22 | locale = cookie as unknown as SupportedLocale; 23 | } 24 | 25 | // Redirect if there is no locale 26 | if (pathnameIsMissingLocale) { 27 | // e.g. incoming request is /products 28 | // The new URL is now /en-US/products 29 | return NextResponse.redirect(new URL(`/${locale}/${pathname}`, request.url)); 30 | } else if (getLocaleFromPath(pathname) !== locale) { 31 | return NextResponse.redirect(new URL(replaceRouteLocale(pathname, locale), request.url)); 32 | } 33 | 34 | // ref: https://github.com/vercel/next.js/issues/43704#issuecomment-1411186664 35 | // for server component to access url and pathname 36 | // Store current request url in a custom header, which you can read later 37 | const requestHeaders = new Headers(request.headers); 38 | requestHeaders.set(SITE_INTERNAL_HEADER_URL, request.url); 39 | requestHeaders.set(SITE_INTERNAL_HEADER_PATHNAME, request.nextUrl.pathname); 40 | requestHeaders.set(SITE_INTERNAL_HEADER_LOCALE, locale); 41 | 42 | return NextResponse.next({ 43 | request: { 44 | // Apply new request headers 45 | headers: requestHeaders, 46 | }, 47 | }); 48 | }; 49 | 50 | export const config = { 51 | matcher: [ 52 | // Skip all internal paths (_next) 53 | "/((?!_next|favicon|api).*)", 54 | // Optional: only run on root (/) URL 55 | // '/' 56 | ], 57 | }; 58 | -------------------------------------------------------------------------------- /src/api/chat.ts: -------------------------------------------------------------------------------- 1 | import { RequestGetChats, RequestSend, ResponseGetChats, ResponseSend } from "@/pages/api/chatgpt/chat"; 2 | import nodeFetch from "node-fetch"; 3 | 4 | export async function getChatsByConversationId(conversationId: number) { 5 | const response = await nodeFetch("/api/chatgpt/chat", { 6 | method: "POST", 7 | body: JSON.stringify({ 8 | action: "get_chats", 9 | conversation_id: conversationId, 10 | } as RequestGetChats), 11 | }); 12 | const data = (await response.json()) as ResponseGetChats; 13 | if (!response.ok) { 14 | alert("Error: " + JSON.stringify((data as any).error)); 15 | return null; 16 | } 17 | 18 | if (!data) { 19 | alert("Error(getChatsByConversationId): sOmeTHiNg wEnT wRoNg"); 20 | return null; 21 | } 22 | 23 | return data; 24 | } 25 | 26 | export async function sendMessage(conversageId: number, message: string, name?: string) { 27 | const response = await nodeFetch("/api/chatgpt/chat", { 28 | method: "POST", 29 | body: JSON.stringify({ 30 | action: "send", 31 | conversation_id: conversageId, 32 | messages: [ 33 | { 34 | role: "user", 35 | content: message, 36 | name: name ?? undefined, 37 | }, 38 | ], 39 | } as RequestSend), 40 | }); 41 | const data = (await response.json()) as ResponseSend; 42 | if (!response.ok) { 43 | alert("Error: " + JSON.stringify((data as any).error)); 44 | return; 45 | } 46 | if (data == null) { 47 | alert("Error: sOmeTHiNg wEnT wRoNg"); 48 | return; 49 | } 50 | 51 | return data; 52 | } 53 | 54 | export async function sendMsgWithStreamRes(conversageId: number, message: string, name?: string) { 55 | const response = await fetch("/api/chatgpt/stream", { 56 | method: "POST", 57 | headers: { Accept: "text/event-stream" }, 58 | body: JSON.stringify({ 59 | action: "send_stream", 60 | conversation_id: conversageId, 61 | messages: [ 62 | { 63 | role: "user", 64 | content: message, 65 | name: name ?? undefined, 66 | }, 67 | ], 68 | }), 69 | }); 70 | 71 | if (!response.ok) { 72 | alert("Error: " + response.statusText); 73 | return; 74 | } 75 | if (response.body == null) { 76 | alert("Error: sOmeTHiNg wEnT wRoNg"); 77 | return; 78 | } 79 | return response.body; 80 | } 81 | -------------------------------------------------------------------------------- /src/flows/store.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | import { 3 | addEdge, 4 | applyEdgeChanges, 5 | applyNodeChanges, 6 | Connection, 7 | Edge, 8 | EdgeChange, 9 | Node, 10 | NodeChange, 11 | OnConnect, 12 | OnEdgesChange, 13 | OnNodesChange, 14 | } from "reactflow"; 15 | import { FlowStep } from "@/flows/types/flow-step"; 16 | import { persist, createJSONStorage } from "zustand/middleware"; 17 | 18 | export type NodeData = { 19 | step: FlowStep; 20 | }; 21 | 22 | export type RFState = { 23 | nodes: Node[]; 24 | edges: Edge[]; 25 | onNodesChange: OnNodesChange; 26 | onEdgesChange: OnEdgesChange; 27 | onConnect: OnConnect; 28 | updateNodeStep: (nodeId: string, step: FlowStep) => void; 29 | }; 30 | 31 | // this is our useStore hook that we can use in our components to get parts of the store and call actions 32 | const useRfStore = create( 33 | persist( 34 | (set, get: any) => ({ 35 | nodes: [], 36 | edges: [], 37 | addNode(node: Node) { 38 | set({ 39 | nodes: [...get().nodes, node], 40 | }); 41 | }, 42 | addEdge(edge: Edge) { 43 | set({ 44 | edges: [...get().edges, edge], 45 | }); 46 | }, 47 | onNodesChange: (changes: NodeChange[]) => { 48 | set({ 49 | nodes: applyNodeChanges(changes, get().nodes), 50 | }); 51 | }, 52 | onEdgesChange: (changes: EdgeChange[]) => { 53 | set({ 54 | edges: applyEdgeChanges(changes, get().edges), 55 | }); 56 | }, 57 | onConnect: (connection: Connection) => { 58 | set({ 59 | edges: addEdge(connection, get().edges), 60 | }); 61 | }, 62 | updateNodeStep: (nodeId: string, step: FlowStep) => { 63 | set({ 64 | nodes: get().nodes.map((node: any) => { 65 | if (node.id === nodeId) { 66 | // it's important to create a new object here, to inform React Flow about the cahnges 67 | node.data = { ...node.data, step }; 68 | } 69 | 70 | return node; 71 | }), 72 | }); 73 | }, 74 | }), 75 | { 76 | name: "flow", 77 | partialize: (state: any) => ({ 78 | nodes: state.nodes, 79 | edges: state.edges, 80 | }), 81 | }, 82 | ), 83 | ); 84 | 85 | export default useRfStore; 86 | -------------------------------------------------------------------------------- /src/app/[lang]/click-flow/page.client.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import { 5 | Alert, 6 | AlertIcon, 7 | AlertTitle, 8 | Box, 9 | Button, 10 | Card, 11 | CardBody, 12 | CardFooter, 13 | CardHeader, 14 | Flex, 15 | Heading, 16 | SimpleGrid, 17 | Stack, 18 | Text, 19 | Link as NavLink, 20 | Container, 21 | } from "@chakra-ui/react"; 22 | import { ExternalLinkIcon } from "@chakra-ui/icons"; 23 | import samples from "@/assets/chatgpt/flow/index.json"; 24 | import SimpleMarkdown from "@/components/markdown/SimpleMarkdown"; 25 | import Link from "next/link"; 26 | import { CP_GITHUB_ASSETS } from "@/configs/constants"; 27 | 28 | function StartlingByEachStepList({ i18n }: GeneralI18nProps) { 29 | const chatgptLink = `${CP_GITHUB_ASSETS}/chatgpt`; 30 | const dict = i18n.dict; 31 | 32 | return ( 33 | 34 | 35 | 36 | {dict["create-new-steps"]}: 37 | 38 | Pull Request 39 | 40 | 41 | {samples.length > 0 && ( 42 | 43 | {samples.map((sample, index) => ( 44 | 45 | 46 | {sample.name} 47 | {sample.author} 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | ))} 67 | 68 | )} 69 | 70 | ); 71 | } 72 | 73 | export default StartlingByEachStepList; 74 | -------------------------------------------------------------------------------- /README.zh-CN.md: -------------------------------------------------------------------------------- 1 | # ChatFlow - 打造个性化 ChatGPT 流程,构建自动化之路 2 | 3 | [![ci](https://github.com/prompt-engineering/chat-flow/actions/workflows/ci.yaml/badge.svg)](https://github.com/prompt-engineering/chat-flow/actions/workflows/ci.yaml) 4 | ![GitHub](https://img.shields.io/github/license/prompt-engineering/chat-flow) 5 | 6 | Screenshots: 7 | 8 | ![](docs/screenshot.jpeg) 9 | 10 | Online Demo: https://prompt.phodal.com/ 11 | 12 | [English](./README.md) | 简体中文 13 | 14 | # 部署 15 | 16 | ## 在 Vercel 上部署 ChatFlow,使用 Planetscale 17 | 18 | 按照以下步骤,在 Vercel 上部署 ChatFlow,使用由 Planetscale 提供的无服务器 MySQL 数据库: 19 | 20 | 1. 从 GitHub 克隆 [ChatFlow 模板](https://github.com/prompt-engineering/chat-flow)。 21 | 2. 创建 Vercel 帐户,并将其连接到 GitHub 帐户。 22 | 3. 创建 [Planetscale](https://app.planetscale.com) 帐户。 23 | 4. 设置 Planetscale 数据库: 24 | 1. 使用 `pscale auth login` 登录 Planetscale 帐户。 25 | 2. 使用 `pscale password create ` 创建密码。 26 | 3. 使用 `npx prisma db push` 将数据库推送到 Planetscale。 27 | 5. 配置 Vercel 环境: 28 | - 设置 Planetscale 数据库的 URL:`DATABASE_URL='mysql://{user}:{password}@{host}/{db}?ssl={"rejectUnauthorized":false&sslcert=/etc/ssl/certs/ca-certificates.crt}'`。 29 | - 使用 `node scripts/gen-enc.js` 生成加密密钥,并将其设置为 `ENC_KEY`。 30 | 31 | 完成这些步骤后,您的 ChatFlow 将在 Vercel 上部署,并使用 Planetscale 的 Serverless MySQL 数据库。 32 | 33 | ## 本地搭建 34 | 35 | 1. 从 GitHub 克隆 [ChatFlow 模板](https://github.com/prompt-engineering/chat-flow)。 36 | 2. 暂时仍依赖 Planetscale 服务,按照上小节注册,并配置`DATABASE_URL`到.env 文件。 37 | 3. 执行 `npm install`。 38 | 4. 使用 `node scripts/gen-enc.js` 生成加密密钥,在 `.env` 文件中配置 `ENC_KEY=***` 的形式。(PS:`.env` 文件可以从 env.template 复制过去) 39 | 5. 直接运行 `npm run dev` 就可以使用了。 40 | 41 | # Create new Flow 42 | 43 | - examples: see in: [src/assets/chatgpt/flow](src/assets/chatgpt/flow) 44 | - all type defines: [src/flows/types](src/flows/types) 45 | 46 | # Development 47 | 48 | Technical documentation: 49 | 50 | - Flowchart 51 | - DotParser, parse dot file to graph data 52 | - dagre, layout graph data 53 | - ReactFlow, render graph data 54 | - Flow Functions 55 | - jsonpath-plus, parse jsonpath 56 | - expr-eval, parse expression 57 | - Flow Components 58 | - JsonViewer, render json data 59 | - DataTable, render table data 60 | - Flow Editor 61 | - ReactFlow, render graph data 62 | - Repl Server 63 | - Rx.js, handle websocket 64 | - Others 65 | - MarkdownViewer, render markdown data 66 | - MermaidViewer, render mermaid data 67 | 68 | ## LICENSE 69 | 70 | This code is distributed under the MIT license. See [LICENSE](./LICENSE) in this directory. 71 | -------------------------------------------------------------------------------- /src/components/ClickPrompt/ClickPromptButton.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { useState } from "react"; 4 | import { Box, Text, Tooltip, useDisclosure } from "@chakra-ui/react"; 5 | import { Button } from "@/components/ChakraUI"; 6 | import { BeatLoader } from "react-spinners"; 7 | import { ClickPromptSmall } from "@/components/CustomIcon"; 8 | import clickPromptLogo from "@/assets/clickprompt-light.svg?url"; 9 | import { CPButtonProps, StyledBird, StyledPromptButton } from "@/components/ClickPrompt/Button.shared"; 10 | import { LoggingDrawer } from "@/components/ClickPrompt/LoggingDrawer"; 11 | import * as UserAPI from "@/api/user"; 12 | 13 | export type ClickPromptBirdParams = { width?: number; height?: number }; 14 | 15 | export function ClickPromptBird(props: ClickPromptBirdParams) { 16 | const width = props.width || 38; 17 | const height = props.height || 32; 18 | 19 | return ; 20 | } 21 | 22 | export function ClickPromptButton(props: CPButtonProps) { 23 | const [isLoading, setIsLoading] = useState(props.loading); 24 | const [isLoggedIn, setIsLoggedIn] = useState(false); 25 | const { isOpen, onOpen, onClose } = useDisclosure(); 26 | 27 | const handleClick = async (event: any) => { 28 | setIsLoading(true); 29 | const isLoggedIn = await UserAPI.isLoggedIn(); 30 | setIsLoggedIn(isLoggedIn); 31 | onOpen(); 32 | props.onClick && props.onClick(event); 33 | }; 34 | 35 | const handleClose = () => { 36 | setIsLoading(false); 37 | onClose(); 38 | }; 39 | 40 | function NormalSize() { 41 | return ( 42 | 43 | 48 | 49 | 50 | ); 51 | } 52 | 53 | function SmallSize() { 54 | return ( 55 | 61 | ); 62 | } 63 | 64 | return ( 65 | 66 | {props.size !== "sm" && } 67 | {props.size === "sm" && } 68 | 69 | {LoggingDrawer(isOpen, handleClose, isLoggedIn, props)} 70 | 71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /src/assets/chatgpt/flow/design-software-system.yml: -------------------------------------------------------------------------------- 1 | name: 软件系统设计 2 | category: Development 3 | author: Phodal Huang 4 | description: 在这个示例中,我们将会使用 ChatGPT 分析系统,编写软件系统设计。 5 | explain: | 6 | digraph G { 7 | 0[flowType = "prompt"] 8 | 1[flowType = "interactive"] 9 | 2[flowType = "interactive"] 10 | 3[flowType = "interactive"] 11 | 4[flowType = "interactive"] 12 | 0 -> 1 13 | 1 -> 2 14 | 1 -> 3 15 | 1 -> 4 16 | } 17 | 18 | steps: 19 | - name: 创建系统设计 "函数" 20 | ask: | 21 | 我们来设计一个流程,名为: system,其用于软件系统设计。我们会把设计分为两部分: 22 | 23 | 第一部分,当我用 "design:{}" 发给你需求时,你需要: 24 | 25 | 1. 分析所有潜在的对应场景,分析用户旅程。 26 | 2. 使用 Mermaid 绘制 User Journey Diagram,并只返回 Mermaid 的 User Journey Diagram 代码,最后返回示例如: 27 | 28 | ```mermaid 29 | journey 30 | title My working day 31 | section Go to work 32 | Make tea: 5: Me 33 | Go upstairs: 3: Me 34 | Do work: 1: Me, Cat 35 | section Go home 36 | Go downstairs: 5: Me 37 | Sit down: 5: Me 38 | ``` 39 | 40 | 第二部分,我会用 "system({}):{}" 的形式发给你设计需求,示例:"system("API"): 博客系统",表示上面格式中的 API 部分。要求如下: 41 | 42 | 1. 你需要考虑围绕这一类型系统的所有场景。 43 | 2. 使用如下的 DSL 格式来描述系统: 44 | 45 | ``` 46 | System("BlogSystem") { 47 | Entities { 48 | Blog { title: string, ..., comments: [Comment]? }, 49 | Comment { ...} 50 | } 51 | Operation { 52 | Ops("CreateBlog", { 53 | in: { title: string, description: string }, 54 | out: { id: number } 55 | pre: title is unique and (title.length > 5 && title.length < 120) 56 | post: id is not null 57 | }) 58 | } 59 | API { 60 | Route(path: String, method: HttpMethod operation: Operation) 61 | } 62 | } 63 | ``` 64 | 65 | 明白吗?明白就返回:OK。 66 | cachedResponseRegex: 67 | - name: 设计用户旅程 68 | ask: | 69 | design: $$placeholder$$, 使用 Mermaid 绘制 User Journey Diagram,并只返回 Mermaid 的 User Journey Diagram 代码,返回格式如:"""```mermaid journey{}"""。 70 | values: 71 | placeholder: 在线博客系统 72 | cachedResponseRegex: .* 73 | - name: 分析系统,绘制 Entities 图 74 | ask: | 75 | system("Entities"): $$placeholder$$,返回 Mermaid 类图。 76 | values: 77 | placeholder: 在线博客系统 78 | cachedResponseRegex: .* 79 | - name: 分析系统,绘制 Operation 图 80 | ask: | 81 | system("Operation"): $$placeholder$$,只返回 Operation 部分。 82 | values: 83 | placeholder: 在线博客系统 84 | cachedResponseRegex: .* 85 | - name: 分析系统,绘制 API 表格 86 | ask: | 87 | system("API"): $$placeholder$$,只返回 API 部分,并使用表格绘制。 88 | values: 89 | placeholder: 在线博客系统 90 | cachedResponseRegex: .* 91 | -------------------------------------------------------------------------------- /src/assets/chatgpt/flow/user-story.yml: -------------------------------------------------------------------------------- 1 | name: 模糊的需求到代码骨架 2 | category: Development 3 | author: Phodal Huang 4 | description: 这个 "逐步运行" 交互示例将会展示,如何结合 ChatGPT 分析需求,编写用户故事?分析用户故事,编写测试用例?分析用户故事,编写代码? 5 | explain: | 6 | digraph G { 7 | 0[flowType = "prompt"] 8 | 1[flowType = "interactive"] 9 | 2[flowType = "interactive"] 10 | 3[flowType = "interactive"] 11 | 4[flowType = "interactive"] 12 | 0 -> 1 13 | 1 -> 2 14 | 1 -> 3 15 | 1 -> 4 16 | } 17 | 18 | steps: 19 | - name: 创建需求游戏 20 | ask: | 21 | 我们来玩一个名为 story 的游戏,在这个游戏里,我会给你一个模糊的需求,你需要: 22 | 23 | 1. 分析需求,并使用用户故事和 Invest 原则编写用户故事卡,但是不需要返回给我。 24 | 2. 尽可能写清楚用户故事的验收条件,验收条件 Given When Then 的表达方式,但是不需要返回给我。 25 | 3. 最后返回用户故事的标题,内容,验收条件,格式如下: 26 | 27 | """ 28 | 29 | 标题:{} 30 | 31 | 内容:{} 32 | 33 | 验收条件: 34 | 35 | 1. AC01 {} 36 | - When {} 37 | - Then {} 38 | 2. AC02 {} 39 | - When {} 40 | - Then {} 41 | 42 | """ 43 | 44 | 当我说 """story: {}""" ,咱们开始游戏。知道这个游戏怎么玩吗?知道的话,请只回复:OK 45 | cachedResponseRegex: 46 | - name: 分析需求,编写用户故事 47 | ask: | 48 | story: $$placeholder$$ 49 | cachedResponseRegex: .* 50 | values: 51 | placeholder: | 52 | 用户通过主菜单进入“权限管理”模块,选择“账号管理”Tab页,可以看到“新增账号”按钮。 53 | 点击“新增账号”按钮,系统弹出新增账号窗口(可能还会写一句“背景置灰”)。 54 | 用户可在窗口中填写姓名、登录邮箱…… 55 | 若用户未填写必填字段,则点击“确认”时给出错误提醒“请完成所有必填字段的填写!” 56 | 点击“确认”按钮后弹出二次确认窗口,二次确认信息为“确认创建该账号?账号一旦创建成功即会邮件通知对应用户”。用户再次选择“确认”则系统创建账号,若用户选择“取消”则返回填写账号窗口。 57 | 58 | - name: Mermaid 绘制流程图 59 | ask: | 60 | 我会给你一个模糊的需求,你需要: 61 | 62 | 1. 分析和完善需求,但是不需要返回结果给我。 63 | 2. 使用 Mermaid 绘制时序图,但是不需要返回给我。 64 | 3. 最后,只返回 Mermaid 代码,如:"""```mermaid graph {}""",只返回 Mermaid 代码。 65 | 66 | 需求,如下: 67 | 68 | """ 69 | $$response:1$$ 70 | """ 71 | - name: 分析用户故事,编写测试用例 72 | ask: | 73 | 我会给你一个需求,你需要: 74 | 75 | 1. 分析需求,但是不需要返回结果给我。 76 | 2. 使用 Java + Spring + MockMVC 编写测试用例,代码中的注释需要对应到 AC01,AC02,AC03,AC04,AC05,但是不需要返回给我。 77 | 3. 最后,只返回 Java 代码,只返回 Java 代码。 78 | 79 | 需求,如下: 80 | 81 | """ 82 | $$response:1$$ 83 | """ 84 | cachedResponseRegex: 85 | - name: 分析用户故事,编写功能代码 86 | ask: | 87 | 我给你一个需求,你需要分析需求,使用 Java + Spring 编写 API,要求如下: 88 | 89 | 1. 去除不需要的 UI 交互代码,只返回对应的代码。 90 | 2. 在方法中用注释写明如何实现。 91 | 3. 最后,你返回给我的只有代码,格式如下: 92 | 93 | ```java 94 | // {} 95 | @PostMapping({}) 96 | public void main(String args[]) 97 | { 98 | // {} 99 | } 100 | ``` 101 | 102 | 需求,如下: 103 | 104 | """ 105 | $$response:1$$ 106 | """ 107 | cachedResponseRegex: 108 | -------------------------------------------------------------------------------- /src/components/DataTable/DataTable.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { chakra, Table, Tbody, Td, Th, Thead, Tr } from "@chakra-ui/react"; 3 | import { TriangleDownIcon, TriangleUpIcon } from "@chakra-ui/icons"; 4 | import { 5 | ColumnDef, 6 | flexRender, 7 | getCoreRowModel, 8 | getSortedRowModel, 9 | SortingState, 10 | useReactTable, 11 | } from "@tanstack/react-table"; 12 | 13 | export type DataTableProps = { 14 | data: Data[]; 15 | columns: ColumnDef[]; 16 | }; 17 | 18 | export function DataTable({ data, columns }: DataTableProps) { 19 | const [sorting, setSorting] = useState([]); 20 | const table = useReactTable({ 21 | columns, 22 | data, 23 | getCoreRowModel: getCoreRowModel(), 24 | onSortingChange: setSorting, 25 | getSortedRowModel: getSortedRowModel(), 26 | state: { 27 | sorting, 28 | }, 29 | }); 30 | 31 | return ( 32 | 33 | 34 | {table.getHeaderGroups().map((headerGroup) => ( 35 | 36 | {headerGroup.headers.map((header) => { 37 | // see https://tanstack.com/table/v8/docs/api/core/column-def#meta to type this correctly 38 | const meta: any = header.column.columnDef.meta; 39 | return ( 40 | 53 | ); 54 | })} 55 | 56 | ))} 57 | 58 | 59 | {table.getRowModel().rows.map((row) => ( 60 | 61 | {row.getVisibleCells().map((cell) => { 62 | // see https://tanstack.com/table/v8/docs/api/core/column-def#meta to type this correctly 63 | const meta: any = cell.column.columnDef.meta; 64 | return ( 65 | 68 | ); 69 | })} 70 | 71 | ))} 72 | 73 |
41 | {flexRender(header.column.columnDef.header, header.getContext())} 42 | 43 | 44 | {header.column.getIsSorted() ? ( 45 | header.column.getIsSorted() === "desc" ? ( 46 | 47 | ) : ( 48 | 49 | ) 50 | ) : null} 51 | 52 |
66 | {flexRender(cell.column.columnDef.cell, cell.getContext())} 67 |
74 | ); 75 | } 76 | -------------------------------------------------------------------------------- /src/components/SimpleColorPicker.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { ChromePicker, ColorResult } from "react-color"; 3 | import styled from "@emotion/styled"; 4 | import nearestColor from "nearest-color"; 5 | import colorNameList from "color-name-list"; 6 | 7 | type SimpleColorProps = { 8 | initColor?: string; 9 | updateColor?: (color: string) => void; 10 | }; 11 | 12 | const colorNameMap: Record = colorNameList.reduce( 13 | (o, { name, hex }) => Object.assign(o, { [name]: hex }), 14 | {}, 15 | ); 16 | const nearest = nearestColor.from(colorNameMap); 17 | const hexToRgbString = (hex: string) => { 18 | const { rgb } = nearest(hex); 19 | return `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`; 20 | }; 21 | const defaultColor = "rgb(255, 255, 255)"; 22 | 23 | function SimpleColorPicker(props: SimpleColorProps) { 24 | const [color, setColor] = useState(defaultColor); 25 | const [displayColorPicker, setDisplayColorPicker] = useState(false); 26 | 27 | useEffect(() => { 28 | const initColor = props.initColor && colorNameMap[props.initColor.replace(/ color$/, "")]; 29 | setColor(initColor ? hexToRgbString(initColor) : defaultColor); 30 | }, [props.initColor]); 31 | 32 | const handleClick = () => { 33 | setDisplayColorPicker(!displayColorPicker); 34 | }; 35 | 36 | const handleClose = () => { 37 | setDisplayColorPicker(false); 38 | }; 39 | 40 | const handleChange = (color: ColorResult) => { 41 | const newColor = `rgba(${color.rgb.r}, ${color.rgb.g}, ${color.rgb.b}, ${color.rgb.a})`; 42 | setColor(newColor); 43 | if (props.updateColor) { 44 | const colorName = nearest(color.hex).name; 45 | // we should add color after the color name, so the StableDiffusion can parse it 46 | props.updateColor(colorName + " color"); 47 | } 48 | }; 49 | 50 | return ( 51 | <> 52 | 53 | 54 | 55 | {displayColorPicker && ( 56 | 57 | 58 | 59 | 60 | )} 61 | 62 | ); 63 | } 64 | 65 | const StyleColor = styled.div` 66 | width: 16px; 67 | height: 14px; 68 | border-radius: 2px; 69 | background: ${(props) => props.color}; 70 | `; 71 | 72 | const Swatch = styled.div` 73 | display: inline-block; 74 | padding: 1px; 75 | top: 4px; 76 | left: 4px; 77 | position: relative; 78 | background: #fff; 79 | border-radius: 1px; 80 | box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1); 81 | cursor: pointer; 82 | `; 83 | 84 | const StylePopover = styled.div` 85 | position: absolute; 86 | z-index: 2; 87 | `; 88 | 89 | const StyleCover = styled.div` 90 | position: fixed; 91 | top: 0; 92 | right: 0; 93 | bottom: 0; 94 | left: 0; 95 | `; 96 | 97 | export default SimpleColorPicker; 98 | -------------------------------------------------------------------------------- /src/api/conversation.ts: -------------------------------------------------------------------------------- 1 | import fetch from "node-fetch"; 2 | import { 3 | RequestChangeConversationName, 4 | RequestCreateConversation, 5 | RequestDeleteAllConversation, 6 | RequestDeleteConversation, 7 | ResponseCreateConversation, 8 | ResponseDeleteAllConversation, 9 | } from "@/pages/api/chatgpt/conversation"; 10 | 11 | export async function createConversation(name?: string) { 12 | const response = await fetch("/api/chatgpt/conversation", { 13 | method: "POST", 14 | body: JSON.stringify({ 15 | action: "create_conversation", 16 | name: name ?? "Default name", 17 | } as RequestCreateConversation), 18 | }); 19 | const data = (await response.json()) as ResponseCreateConversation; 20 | if (!response.ok) { 21 | alert("Error(createConversation): " + JSON.stringify((data as any).error)); 22 | return; 23 | } 24 | 25 | if (data == null) { 26 | alert("Error(createConversation): sOmeTHiNg wEnT wRoNg"); 27 | return; 28 | } 29 | 30 | return data; 31 | } 32 | 33 | export async function changeConversationName(conversationId: number, name: string) { 34 | const response = await fetch("/api/chatgpt/conversation", { 35 | method: "POST", 36 | body: JSON.stringify({ 37 | action: "change_conversation_name", 38 | conversation_id: conversationId, 39 | name: name ?? "Default name", 40 | } as RequestChangeConversationName), 41 | }); 42 | const data = (await response.json()) as ResponseCreateConversation; 43 | if (!response.ok) { 44 | alert("Error: " + JSON.stringify((data as any).error)); 45 | return; 46 | } 47 | 48 | if (!data) { 49 | alert("Error(changeConversationName): sOmeTHiNg wEnT wRoNg"); 50 | return; 51 | } 52 | 53 | return data; 54 | } 55 | 56 | export async function deleteConversation(conversationId: number) { 57 | const response = await fetch("/api/chatgpt/conversation", { 58 | method: "POST", 59 | body: JSON.stringify({ 60 | action: "delete_conversation", 61 | conversation_id: conversationId, 62 | } as RequestDeleteConversation), 63 | }); 64 | const data = (await response.json()) as ResponseCreateConversation; 65 | if (!response.ok) { 66 | alert("Error: " + JSON.stringify((data as any).error)); 67 | return; 68 | } 69 | 70 | if (!data) { 71 | alert("Error(deleteConversation): sOmeTHiNg wEnT wRoNg"); 72 | return; 73 | } 74 | 75 | return data; 76 | } 77 | 78 | export async function deleteAllConversations() { 79 | const response = await fetch("/api/chatgpt/conversation", { 80 | method: "POST", 81 | body: JSON.stringify({ 82 | action: "delete_all_conversations", 83 | } as RequestDeleteAllConversation), 84 | }); 85 | const data = (await response.json()) as ResponseDeleteAllConversation; 86 | if (!response.ok) { 87 | alert("Error: " + JSON.stringify((data as any).error)); 88 | return; 89 | } 90 | 91 | if (data.error) { 92 | alert("Error(deleteAllConversation): sOmeTHiNg wEnT wRoNg: " + data.error); 93 | return; 94 | } 95 | 96 | return data; 97 | } 98 | -------------------------------------------------------------------------------- /src/app/[lang]/flow-editor/StepConverter.ts: -------------------------------------------------------------------------------- 1 | import { Edge, Node } from "reactflow"; 2 | import { FlowStep } from "@/flows/types/flow-step"; 3 | import yml from "js-yaml"; 4 | 5 | /** 6 | * convert following data: 7 | * ```json 8 | * "nodes":[{"id":"db7a9443-04c1-4880-8331-d6a4dd9267ad","type":"stepNode","position":{"x":425,"y":157},"data":{"label":"stepNode node","step":{"name":"Demos","ask":"","response":"","hiddenExecute":false,"markdownEditor":false,"cachedResponseRegex":"","values":{},"preActions":[],"postActions":[]}},"width":320,"height":422,"selected":false,"dragging":false},{"id":"f9f5cb5f-863f-4d33-879c-c87050730be0","position":{"x":948.7994746059545,"y":283.8586690017513},"data":{"label":"Node f9f5cb5f-863f-4d33-879c-c87050730be0","step":{"name":"4324234","ask":"234234","response":"","hiddenExecute":false,"markdownEditor":false,"cachedResponseRegex":"","values":{},"preActions":[],"postActions":[]}},"type":"stepNode","width":320,"height":422,"selected":false,"dragging":false},{"id":"ac6b0896-4bc5-4516-91a3-21a1363b658c","position":{"x":1360.241194711708,"y":375.91867226821165},"data":{"label":"Node ac6b0896-4bc5-4516-91a3-21a1363b658c","step":{"name":"4324234","ask":"32423423","response":"","hiddenExecute":false,"markdownEditor":false,"cachedResponseRegex":"","values":{},"preActions":[],"postActions":[]}},"type":"stepNode","width":320,"height":422,"selected":false,"dragging":false}] 9 | * "edges":[{"id":"f9f5cb5f-863f-4d33-879c-c87050730be0","source":"db7a9443-04c1-4880-8331-d6a4dd9267ad","target":"f9f5cb5f-863f-4d33-879c-c87050730be0"},{"id":"ac6b0896-4bc5-4516-91a3-21a1363b658c","source":"f9f5cb5f-863f-4d33-879c-c87050730be0","target":"ac6b0896-4bc5-4516-91a3-21a1363b658c"}] 10 | * ``` 11 | * 1. edges will convert to Graphviz dot format, bind to `explain` variable, flowType is `interactive`, like: 12 | * ```dot 13 | * digraph G { 14 | * "db7a9443-04c1-4880-8331-d6a4dd9267ad"[flowType = "interactive"] 15 | * ... 16 | * "db7a9443-04c1-4880-8331-d6a4dd9267ad" -> "f9f5cb5f-863f-4d33-879c-c87050730be0" 17 | * ... 18 | * } 19 | * ``` 20 | * 2. nodes will convert to `FlowStep` type, to be yaml format, like: 21 | * 22 | * ```yaml 23 | * steps: 24 | * - name: Demos 25 | * ask: '' 26 | * response: '' 27 | * hiddenExecute: false 28 | * markdownEditor: false 29 | * cachedResponseRegex: '' 30 | * values: {} 31 | * preActions: [] 32 | * postActions: [] 33 | * ``` 34 | * 3. combined with `explain` variable, will generate yaml format: 35 | * 36 | * ``` 37 | * explain: | 38 | * steps: [] 39 | * ``` 40 | */ 41 | export function flowToYaml(nodes: Node[], edges: Edge[]) { 42 | let explain = "digraph G {\n"; 43 | const steps: FlowStep[] = []; 44 | 45 | nodes.forEach((node) => { 46 | const step: FlowStep = node.data.step; 47 | explain += ` "${node.id}"[label="${step.name}", flowType = "interactive"]\n`; 48 | steps.push(step); 49 | }); 50 | 51 | edges.forEach((edge) => { 52 | explain += ` "${edge.source}" -> "${edge.target}"\n`; 53 | }); 54 | 55 | explain += "}\n"; 56 | 57 | const yamlOutput = yml.dump({ explain, steps: steps }); 58 | 59 | return yamlOutput; 60 | } 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ChatFlow - Personalize your ChatGPT workflows and build the road to automation 2 | 3 | [![ci](https://github.com/prompt-engineering/chat-flow/actions/workflows/ci.yaml/badge.svg)](https://github.com/prompt-engineering/chat-flow/actions/workflows/ci.yaml) 4 | ![GitHub](https://img.shields.io/github/license/prompt-engineering/chat-flow) 5 | [![Discord](https://img.shields.io/discord/1082563233593966612)](https://discord.gg/FSWXq4DmEj) 6 | 7 | Screenshots: 8 | 9 | ![](docs/screenshot.jpeg) 10 | 11 | English | [简体中文](./README.zh-CN.md) 12 | 13 | Online Demo: https://prompt.phodal.com/ 14 | 15 | Join us: 16 | 17 | [![Chat Server](https://img.shields.io/badge/chat-discord-7289da.svg)](https://discord.gg/FSWXq4DmEj) 18 | 19 | # Deploy 20 | 21 | ## Deploy ChatFlow on Vercel with Planetscale 22 | 23 | Follow these steps to deploy ChatFlow on Vercel with a serverless MySQL database provided by Planetscale: 24 | 25 | 1. Clone the [ChatFlow template](https://github.com/prompt-engineering/chat-flow) from GitHub. 26 | 2. Create a Vercel account and connect it to your GitHub account. 27 | 3. Create a [Planetscale](https://app.planetscale.com) account. 28 | 4. Set up your Planetscale database: 29 | 1. Log in to your Planetscale account with `pscale auth login`. 30 | 2. Create a password with `pscale password create `. 31 | 3. Push your database to Planetscale with `npx prisma db push`. 32 | 5. Configure your Vercel environment: 33 | - Set `DATABASE_URL` to your Planetscale database URL. 34 | - Generate an encryption key with `node scripts/gen-enc.js` and set it as `ENC_KEY`. 35 | 36 | With these steps completed, your ChatFlow will be deployed on Vercel with a Planetscale serverless MySQL database. 37 | 38 | ## Local Usage 39 | 40 | 1. Clone the [ChatFlow template](https://github.com/prompt-engineering/chat-flow) from GitHub. 41 | 2. Dependencies on Planetscale services still exist temporarily. Please register as mentioned in the previous section and configure `DATABASE_URL` in the `.env` file. 42 | 3. Run `npm install`. 43 | 4. Generate an encryption key using `node scripts/gen-enc.js` and configure it in the `.env` file in the format `ENC_KEY=***`. (Note: You can copy the `.env` file from env.template) 44 | 5. You can now use the application by running `npm run dev`. 45 | 46 | # Create new Flow 47 | 48 | - examples: see in: [src/assets/chatgpt/flow](src/assets/chatgpt/flow) 49 | - all type defines: [src/flows/types](src/flows/types) 50 | 51 | # Development 52 | 53 | Technical documentation: 54 | 55 | - Flowchart 56 | - DotParser, parse dot file to graph data 57 | - dagre, layout graph data 58 | - ReactFlow, render graph data 59 | - Flow Functions 60 | - jsonpath-plus, parse jsonpath 61 | - expr-eval, parse expression 62 | - Flow Components 63 | - JsonViewer, render json data 64 | - DataTable, render table data 65 | - Flow Editor 66 | - ReactFlow, render graph data 67 | - Repl Server 68 | - Rx.js, handle websocket 69 | - Others 70 | - MarkdownViewer, render markdown data 71 | - MermaidViewer, render mermaid data 72 | 73 | ## LICENSE 74 | 75 | This code is distributed under the MIT license. See [LICENSE](./LICENSE) in this directory. 76 | -------------------------------------------------------------------------------- /src/components/ClickPrompt/ExecutePromptButton.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { MouseEventHandler, useEffect, useState } from "react"; 4 | import { Text, useDisclosure } from "@chakra-ui/react"; 5 | import * as UserAPI from "@/api/user"; 6 | import { ResponseCreateConversation } from "@/pages/api/chatgpt/conversation"; 7 | import { createConversation } from "@/api/conversation"; 8 | import { sendMessage } from "@/api/chat"; 9 | import { ResponseSend } from "@/pages/api/chatgpt/chat"; 10 | import { Box, Button } from "@/components/ChakraUI"; 11 | import { BeatLoader } from "react-spinners"; 12 | import { ClickPromptBird } from "@/components/ClickPrompt/ClickPromptButton"; 13 | import { ButtonSize, StyledPromptButton } from "./Button.shared"; 14 | import { LoggingDrawer } from "@/components/ClickPrompt/LoggingDrawer"; 15 | 16 | export type ExecButtonProps = { 17 | loading?: boolean; 18 | onClick?: MouseEventHandler; 19 | name: string; 20 | text: string; 21 | size?: ButtonSize; 22 | children?: React.ReactNode; 23 | handleResponse?: (response: ResponseSend) => void; 24 | conversationId?: number; 25 | updateConversationId?: (conversationId: number) => void; 26 | }; 27 | 28 | function ExecutePromptButton(props: ExecButtonProps) { 29 | const [isLoading, setIsLoading] = useState(props.loading); 30 | const { isOpen, onOpen, onClose } = useDisclosure(); 31 | const [hasLogin, setHasLogin] = useState(false); 32 | 33 | const handleClick = async () => { 34 | setIsLoading(true); 35 | 36 | try { 37 | const isLoggedIn = await UserAPI.isLoggedIn(); 38 | if (!isLoggedIn) { 39 | onOpen(); 40 | setIsLoading(false); 41 | return; 42 | } 43 | } catch (e) { 44 | console.log(e); 45 | setHasLogin(false); 46 | } 47 | 48 | let conversationId = props.conversationId; 49 | if (!props.conversationId) { 50 | const conversation: ResponseCreateConversation = await createConversation(); 51 | if (!conversation) { 52 | return; 53 | } 54 | 55 | conversationId = conversation.id as number; 56 | props.updateConversationId ? props.updateConversationId(conversationId) : null; 57 | } 58 | 59 | if (conversationId) { 60 | const response: any = await sendMessage(conversationId, props.text); 61 | if (response && props.handleResponse) { 62 | props.handleResponse(response as ResponseSend); 63 | } 64 | } 65 | 66 | setIsLoading(false); 67 | }; 68 | 69 | useEffect(() => { 70 | console.log(`hasLogin: ${hasLogin}`); 71 | if (hasLogin) { 72 | onClose(); 73 | } 74 | }, [hasLogin]); 75 | 76 | const handleClose = () => { 77 | onClose(); 78 | }; 79 | 80 | const updateLoginStatus = (status: boolean) => { 81 | if (status) { 82 | setHasLogin(true); 83 | onClose(); 84 | } 85 | }; 86 | 87 | return ( 88 | <> 89 | 90 | 91 | 96 | 97 | 98 | 99 | {!hasLogin && LoggingDrawer(isOpen, handleClose, hasLogin, props, updateLoginStatus)} 100 | 101 | ); 102 | } 103 | 104 | export default ExecutePromptButton; 105 | -------------------------------------------------------------------------------- /src/i18n/index.ts: -------------------------------------------------------------------------------- 1 | import { match } from "@formatjs/intl-localematcher"; 2 | import Negotiator from "negotiator"; 3 | 4 | const dictionaries = { 5 | "en-US": () => import("./en-US").then((module) => module.default), 6 | "zh-CN": () => import("./zh-CN").then((module) => module.default), 7 | }; 8 | 9 | export type SupportedLocale = keyof typeof dictionaries; 10 | export const SupportedLocales = Object.keys(dictionaries) as SupportedLocale[]; 11 | export const DefaultLocale: SupportedLocale = "zh-CN"; 12 | 13 | export function stripLocaleInPath(pathname: string): PagePath { 14 | const splits = pathname.split("/"); 15 | const locale = splits[1]; 16 | 17 | let striped: PagePath; 18 | if (SupportedLocales.includes(locale as SupportedLocale)) { 19 | striped = pathname.replace(`/${locale}`, "") as PagePath; 20 | } else { 21 | striped = pathname as PagePath; 22 | } 23 | 24 | // todo: we read to read routes from Next.js 25 | if (splits.length == 5 && hadChildRoutes.includes(splits[2])) { 26 | striped = `/${splits[2]}/$` as PagePath; 27 | } 28 | 29 | return striped; 30 | } 31 | 32 | export function getLocaleFromPath(pathname: string): SupportedLocale { 33 | const locale = pathname.split("/")[1]; 34 | if (SupportedLocales.includes(locale as SupportedLocale)) { 35 | return locale as SupportedLocale; 36 | } 37 | 38 | return DefaultLocale; 39 | } 40 | 41 | export function replaceRouteLocale(pathname: string, locale: SupportedLocale): string { 42 | const currentLocale = pathname.split("/")[1]; 43 | if (SupportedLocales.includes(currentLocale as SupportedLocale)) { 44 | return pathname.replace(`/${currentLocale}`, `/${locale}`); 45 | } 46 | 47 | return `/${locale}${pathname}`; 48 | } 49 | 50 | export function getLocale(headers: Headers): SupportedLocale { 51 | const languages = new Negotiator({ 52 | headers: [...headers].reduce((pre: Record, [key, value]) => { 53 | pre[key] = value; 54 | return pre; 55 | }, {}), 56 | }).languages(); 57 | 58 | let locale: SupportedLocale; 59 | try { 60 | locale = match(languages, SupportedLocales, DefaultLocale) as SupportedLocale; 61 | } catch (error) { 62 | locale = DefaultLocale; 63 | } 64 | 65 | return locale; 66 | } 67 | 68 | import type { GlobalKey as GlobalKeyEnUS, PageKey as PageKeyEnUS } from "./en-US"; 69 | import type { GlobalKey as GlobalKeyZhCN, PageKey as PageKeyZhCN } from "./zh-CN"; 70 | 71 | export type AppData = { 72 | i18n: { 73 | g: (key: GlobalKeyEnUS | GlobalKeyZhCN) => string; 74 | tFactory:

(path: P) => (key: PageKeyEnUS

| PageKeyZhCN

) => string; 75 | dict: Record; 76 | }; 77 | pathname: string; 78 | locale: SupportedLocale; 79 | }; 80 | export type AppDataI18n = AppData["i18n"]; 81 | 82 | import { SITE_INTERNAL_HEADER_LOCALE, SITE_INTERNAL_HEADER_PATHNAME } from "@/configs/constants"; 83 | import { hadChildRoutes, PagePath } from "./pagePath"; 84 | 85 | export async function getAppData(): Promise { 86 | let pathname: PagePath = "/"; 87 | let locale = DefaultLocale; 88 | 89 | try { 90 | const { headers } = await import("next/headers"); 91 | pathname = (headers().get(SITE_INTERNAL_HEADER_PATHNAME) || "/") as PagePath; 92 | locale = headers().get(SITE_INTERNAL_HEADER_LOCALE) as SupportedLocale; 93 | } catch (error) { 94 | console.log(error); 95 | } 96 | 97 | const dictionary = dictionaries[locale] ?? dictionaries[DefaultLocale]; 98 | const stripedPathname = stripLocaleInPath(pathname); 99 | return dictionary().then((module) => ({ 100 | i18n: { 101 | g: (key) => module["*"][key], 102 | tFactory: (_) => (key) => (module[stripedPathname] as any)[key as any] as any, 103 | dict: module[stripedPathname], 104 | }, 105 | pathname: stripedPathname, 106 | locale, 107 | })); 108 | } 109 | -------------------------------------------------------------------------------- /src/flows/react-flow-nodes/StepNode.tsx: -------------------------------------------------------------------------------- 1 | import styled from "@emotion/styled"; 2 | import React from "react"; 3 | import { Handle, Position } from "reactflow"; 4 | import { FormControl, FormLabel, Switch, Input, Button, Textarea } from "@chakra-ui/react"; 5 | import { useFormik } from "formik"; 6 | import useRfStore from "../store"; 7 | import { FlowStep } from "@/flows/types/flow-step"; 8 | 9 | type TextNodeProps = { 10 | isConnectable: boolean; 11 | id: string; 12 | data: { label: string; step?: FlowStep }; 13 | }; 14 | 15 | function StepNode(props: TextNodeProps) { 16 | const updateNode = useRfStore((state) => state.updateNodeStep); 17 | 18 | const { isConnectable } = props; 19 | const defaultValue: FlowStep = props.data.step 20 | ? props.data.step 21 | : { 22 | name: "", 23 | ask: "", 24 | response: "", 25 | hiddenExecute: false, 26 | markdownEditor: false, 27 | cachedResponseRegex: "", 28 | values: {}, 29 | preActions: [], 30 | postActions: [], 31 | }; 32 | 33 | const formik = useFormik({ 34 | initialValues: defaultValue, 35 | onSubmit: (values) => { 36 | // we config to onChange to trigger this method 37 | updateNode(props.id, values); 38 | }, 39 | }); 40 | 41 | return ( 42 | 43 | 44 | {formik.values.name.length > 0 ? formik.values.name : "Step"} 45 | 46 | 47 | 48 | Step Name 49 | 50 | 51 | 52 | 53 | Ask 54 |