;
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 ;
10 | }
11 |
12 | export function ClickPromptIcon({ width = 32, height = 32 }) {
13 | return ;
14 | }
15 |
16 | export function ClickPromptSmall({ width = 32, height = 32 }) {
17 | return ;
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 |
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 |
51 |
52 | );
53 | }
54 |
--------------------------------------------------------------------------------
/src/assets/clickprompt-light.svg:
--------------------------------------------------------------------------------
1 |
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 |
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 |
36 |
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 | [](https://github.com/prompt-engineering/chat-flow/actions/workflows/ci.yaml)
4 | 
5 |
6 | Screenshots:
7 |
8 | 
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 | |
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 | |
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 | |
66 | {flexRender(cell.column.columnDef.cell, cell.getContext())}
67 | |
68 | );
69 | })}
70 |
71 | ))}
72 |
73 |
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 | [](https://github.com/prompt-engineering/chat-flow/actions/workflows/ci.yaml)
4 | 
5 | [](https://discord.gg/FSWXq4DmEj)
6 |
7 | Screenshots:
8 |
9 | 
10 |
11 | English | [简体中文](./README.zh-CN.md)
12 |
13 | Online Demo: https://prompt.phodal.com/
14 |
15 | Join us:
16 |
17 | [](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 |
55 |
56 |
57 |
58 | Hidden Execute
59 |
60 |
61 |
62 |
63 | Markdown Editor
64 |
65 |
66 |
67 |
68 | Cached Response Regex
69 |
75 |
76 |
77 |
78 |
79 |
80 | );
81 | }
82 |
83 | const width = 320;
84 |
85 | const TextNodeStyle = styled.div`
86 | min-height: 50px;
87 | width: ${width}px;
88 | border: 1px solid #555;
89 | border-radius: 5px;
90 | background: white;
91 | font-family: jetbrains-mono, "JetBrains Mono", monospace;
92 | `;
93 |
94 | const CardTitle = styled.div`
95 | display: block;
96 | height: 32px;
97 | line-height: 32px;
98 | width: ${width - 2}px;
99 | background: #eee;
100 |
101 | border-top-left-radius: 5px;
102 | border-top-right-radius: 5px;
103 |
104 | border-bottom-width: 1px;
105 | border-bottom-style: solid;
106 | border-color: #555555;
107 | font-size: 14px;
108 | text-align: center;
109 | font-weight: bold;
110 | `;
111 |
112 | const StyledForm = styled.form`
113 | padding: 10px;
114 | `;
115 |
116 | export default StepNode;
117 |
--------------------------------------------------------------------------------
/src/layout/NavBar.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {
3 | Box,
4 | Flex,
5 | Heading,
6 | IconButton,
7 | Link as NavLink,
8 | Menu,
9 | MenuButton,
10 | MenuItem,
11 | MenuList,
12 | Spacer,
13 | } from "@/components/ChakraUI";
14 | import { ChevronDownIcon, ExternalLinkIcon, HamburgerIcon } from "@/components/ChakraUI/icons";
15 | import Link from "next/link";
16 | import { GITHUB_URL } from "@/configs/constants";
17 | import LocaleSwitcher from "@/components/LocaleSwitcher";
18 | import { getAppData } from "@/i18n";
19 |
20 | export default async function NavBar({ locale }: { locale: string }) {
21 | const { pathname } = await getAppData();
22 |
23 | const NavList = [
24 | {
25 | title: "Flow",
26 | url: `/click-flow/`,
27 | },
28 | {
29 | title: "ChatApp",
30 | url: `/chatgpt/`,
31 | },
32 | {
33 | title: "Flow Editor",
34 | url: `/flow-editor/`,
35 | },
36 | ];
37 |
38 | return (
39 |
40 |
41 |
42 | ChatFlow
43 |
44 |
45 | {NavList.map((nav: any) => {
46 | // 如果当前导航项有子菜单,则呈现为下拉菜单
47 | if (nav?.children) {
48 | return (
49 |
64 | );
65 | } else {
66 | // 否则呈现为单独的链接
67 | return (
68 |
69 |
70 | {nav.title}
71 |
72 |
73 | );
74 | }
75 | })}
76 |
77 |
78 |
79 |
80 |
81 | GitHub
82 |
83 |
117 |
118 | );
119 | }
120 |
--------------------------------------------------------------------------------
/src/assets/icons/gpt.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/pages/api/chatgpt/conversation.ts:
--------------------------------------------------------------------------------
1 | import { NextApiHandler } from "next";
2 | import { getUser } from "@/uitls/user.util";
3 | import {
4 | changeConversationName,
5 | createConversation,
6 | deleteAllConversationsByUserId,
7 | deleteConversation,
8 | getAllConversionsByUserId,
9 | } from "@/storage/planetscale";
10 |
11 | export type RequestCreateConversation = {
12 | action: "create_conversation";
13 | name: string;
14 | };
15 | export type ResponseCreateConversation = Awaited>;
16 |
17 | export type RequestDeleteConversation = {
18 | action: "delete_conversation";
19 | conversation_id: number;
20 | };
21 | export type ResponseDeleteConversation = Awaited>;
22 |
23 | export type RequestDeleteAllConversation = {
24 | action: "delete_all_conversations";
25 | };
26 | export type ResponseDeleteAllConversation = {
27 | message?: string;
28 | error?: string;
29 | };
30 |
31 | export type RequestGetConversations = {
32 | action: "get_conversations";
33 | };
34 | export type ResponseGetConversations = Awaited>;
35 |
36 | // change name
37 | export type RequestChangeConversationName = {
38 | action: "change_conversation_name";
39 | conversation_id: number;
40 | name: string;
41 | };
42 | export type ResponseChangeConversationName = Awaited>;
43 |
44 | type RequestType =
45 | | RequestCreateConversation
46 | | RequestDeleteConversation
47 | | RequestDeleteAllConversation
48 | | RequestGetConversations
49 | | RequestChangeConversationName;
50 |
51 | const hander: NextApiHandler = async (req, res) => {
52 | const user = await getUser(req, res);
53 | if (!user) {
54 | return;
55 | }
56 |
57 | if (req.method !== "POST" || !req.body) {
58 | res.status(400).json({ error: "Invalid request" });
59 | return;
60 | }
61 |
62 | const body: RequestType = typeof req.body === "string" ? JSON.parse(req.body) : req.body;
63 | switch (body.action) {
64 | case "create_conversation": {
65 | const { name } = body;
66 | if (!name) {
67 | res.status(400).json({ error: "Name is required" });
68 | return;
69 | }
70 |
71 | const conversation = await createConversation({
72 | name,
73 | user_id: user.id as number,
74 | });
75 |
76 | return res.status(200).json(conversation);
77 | }
78 | case "delete_conversation": {
79 | const { conversation_id } = body;
80 | if (!conversation_id) {
81 | res.status(400).json({ error: "Conversation id is required" });
82 | return;
83 | }
84 | const conversation = await deleteConversation(conversation_id);
85 | return res.status(200).json(conversation);
86 | }
87 | case "delete_all_conversations": {
88 | try {
89 | await deleteAllConversationsByUserId(user.id as number);
90 | return res.status(200).json({
91 | message: "Delete all conversation successfully",
92 | });
93 | } catch (e) {
94 | return res.status(400).json({
95 | error: "Delete all conversation failed: " + JSON.stringify(e),
96 | });
97 | }
98 | }
99 | case "get_conversations": {
100 | const conversations = await getAllConversionsByUserId(user.id as number);
101 | return res.status(200).json(conversations);
102 | }
103 | case "change_conversation_name": {
104 | const { conversation_id, name } = body;
105 |
106 | if (!conversation_id) {
107 | res.status(400).json({ error: "Conversation id is required" });
108 | return;
109 | }
110 | if (!name) {
111 | res.status(400).json({ error: "Name is required" });
112 | return;
113 | }
114 | const conversation = await changeConversationName(conversation_id, name);
115 | return res.status(200).json(conversation);
116 | }
117 | default: {
118 | return res.status(400).json({ error: "Invalid actions" });
119 | }
120 | }
121 | };
122 | export default hander;
123 |
--------------------------------------------------------------------------------
/__tests__/flows/step-converter.test.ts:
--------------------------------------------------------------------------------
1 | import "@testing-library/jest-dom";
2 | import { flowToYaml } from "@/app/[lang]/flow-editor/StepConverter";
3 |
4 | describe("Step To Yaml", () => {
5 | it("simple step", () => {
6 | const nodes = [
7 | {
8 | id: "db7a9443-04c1-4880-8331-d6a4dd9267ad",
9 | type: "stepNode",
10 | position: { x: 425, y: 157 },
11 | data: {
12 | label: "stepNode node",
13 | step: {
14 | name: "Demos",
15 | ask: "",
16 | response: "",
17 | hiddenExecute: false,
18 | markdownEditor: false,
19 | cachedResponseRegex: "",
20 | values: {},
21 | preActions: [],
22 | postActions: [],
23 | },
24 | },
25 | width: 320,
26 | height: 422,
27 | selected: false,
28 | dragging: false,
29 | },
30 | {
31 | id: "f9f5cb5f-863f-4d33-879c-c87050730be0",
32 | position: { x: 948.7994746059545, y: 283.8586690017513 },
33 | data: {
34 | label: "Node f9f5cb5f-863f-4d33-879c-c87050730be0",
35 | step: {
36 | name: "4324234",
37 | ask: "234234",
38 | response: "",
39 | hiddenExecute: false,
40 | markdownEditor: false,
41 | cachedResponseRegex: "",
42 | values: {},
43 | preActions: [],
44 | postActions: [],
45 | },
46 | },
47 | type: "stepNode",
48 | width: 320,
49 | height: 422,
50 | selected: false,
51 | dragging: false,
52 | },
53 | {
54 | id: "ac6b0896-4bc5-4516-91a3-21a1363b658c",
55 | position: { x: 1360.241194711708, y: 375.91867226821165 },
56 | data: {
57 | label: "Node ac6b0896-4bc5-4516-91a3-21a1363b658c",
58 | step: {
59 | name: "4324234",
60 | ask: "32423423",
61 | response: "",
62 | hiddenExecute: false,
63 | markdownEditor: false,
64 | cachedResponseRegex: "",
65 | values: {},
66 | preActions: [],
67 | postActions: [],
68 | },
69 | },
70 | type: "stepNode",
71 | width: 320,
72 | height: 422,
73 | selected: false,
74 | dragging: false,
75 | },
76 | ];
77 | const edges = [
78 | {
79 | id: "f9f5cb5f-863f-4d33-879c-c87050730be0",
80 | source: "db7a9443-04c1-4880-8331-d6a4dd9267ad",
81 | target: "f9f5cb5f-863f-4d33-879c-c87050730be0",
82 | },
83 | {
84 | id: "ac6b0896-4bc5-4516-91a3-21a1363b658c",
85 | source: "f9f5cb5f-863f-4d33-879c-c87050730be0",
86 | target: "ac6b0896-4bc5-4516-91a3-21a1363b658c",
87 | },
88 | ];
89 |
90 | const yaml = flowToYaml(nodes, edges);
91 | expect(yaml).toBe(`explain: |
92 | digraph G {
93 | "db7a9443-04c1-4880-8331-d6a4dd9267ad"[label="Demos", flowType = "interactive"]
94 | "f9f5cb5f-863f-4d33-879c-c87050730be0"[label="4324234", flowType = "interactive"]
95 | "ac6b0896-4bc5-4516-91a3-21a1363b658c"[label="4324234", flowType = "interactive"]
96 | "db7a9443-04c1-4880-8331-d6a4dd9267ad" -> "f9f5cb5f-863f-4d33-879c-c87050730be0"
97 | "f9f5cb5f-863f-4d33-879c-c87050730be0" -> "ac6b0896-4bc5-4516-91a3-21a1363b658c"
98 | }
99 | steps:
100 | - name: Demos
101 | ask: ''
102 | response: ''
103 | hiddenExecute: false
104 | markdownEditor: false
105 | cachedResponseRegex: ''
106 | values: {}
107 | preActions: []
108 | postActions: []
109 | - name: '4324234'
110 | ask: '234234'
111 | response: ''
112 | hiddenExecute: false
113 | markdownEditor: false
114 | cachedResponseRegex: ''
115 | values: {}
116 | preActions: []
117 | postActions: []
118 | - name: '4324234'
119 | ask: '32423423'
120 | response: ''
121 | hiddenExecute: false
122 | markdownEditor: false
123 | cachedResponseRegex: ''
124 | values: {}
125 | preActions: []
126 | postActions: []
127 | `);
128 | });
129 | });
130 |
--------------------------------------------------------------------------------
/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | // This is your Prisma schema file,
2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema
3 |
4 | generator client {
5 | provider = "prisma-client-js"
6 | }
7 |
8 | datasource db {
9 | provider = "mysql"
10 | url = env("DATABASE_URL")
11 | referentialIntegrity = "prisma"
12 | }
13 | // create table chats
14 | // (
15 | // id int auto_increment comment 'Chat ID'
16 | // primary key,
17 | // conversation_id bigint unsigned not null comment 'Conversation ID that the chat belongs to',
18 | // role varchar(10) not null comment 'The role of the author of this message. ChatCompletionRequestMessageRoleEnum',
19 | // content varchar(4096) charset utf8 not null comment 'The contents of the message',
20 | // name varchar(512) charset utf8 null comment 'The name of the user in a multi-user chat',
21 | // created_at datetime default CURRENT_TIMESTAMP not null,
22 | // constraint id
23 | // unique (id)
24 | // );
25 | model chats {
26 | id Int @id @default(autoincrement())
27 | conversation_id Int
28 | role String
29 | content String @db.VarChar(4096)
30 | name String? @db.VarChar(512)
31 | created_at DateTime @default(now())
32 | }
33 |
34 | // create table conversations
35 | // (
36 | // id bigint unsigned auto_increment comment 'Conversation ID'
37 | // primary key,
38 | // user_id bigint unsigned not null comment 'User ID that the conversation belongs to',
39 | // name varchar(255) charset utf8 default 'Default name' not null invisible comment 'conversation name CAN DULICATED',
40 | // deleted tinyint(1) default 0 not null comment 'is conversation has been deleted or not',
41 | // created_at datetime default CURRENT_TIMESTAMP not null
42 | // );
43 | model conversations {
44 | id Int @id @default(autoincrement())
45 | user_id Int
46 | name String @default("Default name")
47 | deleted Boolean @default(false)
48 | created_at DateTime @default(now())
49 | }
50 |
51 | // -- for example, a user can save a custom field with the name "story:123" and the value "blablablabla"
52 | // -- story:123 is the type_name:id => type_value
53 | // create table custom_field
54 | // (
55 | // -- type id is a unique id for each custom field
56 | // id bigint unsigned auto_increment comment 'Custom Field ID'
57 | // primary key,
58 | // user_id bigint unsigned not null comment 'User ID that the custom field belongs to',
59 | // type_id bigint unsigned not null comment 'custom type id',
60 | // type_name varchar(255) charset utf8 default 'Default name' not null invisible comment 'custom field name',
61 | // type_value varchar(32768) charset utf8 default 'Default value' not null invisible comment 'custom field value',
62 | // created_at datetime default CURRENT_TIMESTAMP not null
63 | // );
64 |
65 | model custom_field {
66 | id Int @id @default(autoincrement())
67 | user_id Int
68 | type_id Int
69 | type_name String @default("Default name")
70 | type_value String @default("Default value")
71 | created_at DateTime @default(now())
72 | }
73 |
74 | // create table users
75 | // (
76 | // id bigint unsigned auto_increment comment 'User ID',
77 | // key_hashed varchar(64) not null comment 'hash of openai key',
78 | // iv varchar(32) not null comment 'iv of openai key',
79 | // key_encrypted varchar(255) not null comment 'openai key, but it''s encrypted',
80 | // deleted tinyint default 0 not null comment 'is user has been deleted or not',
81 | // created_at datetime default CURRENT_TIMESTAMP not null,
82 | // primary key (id, key_hashed),
83 | // constraint id
84 | // unique (id)
85 | // );
86 | model users {
87 | id Int @id @default(autoincrement())
88 | key_hashed String
89 | iv String
90 | key_encrypted String
91 | deleted Boolean @default(false)
92 | created_at DateTime @default(now())
93 | }
94 |
--------------------------------------------------------------------------------
/src/components/markdown/Mermaid.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import "client-only";
4 | import React, { useCallback, useEffect, useRef, useState } from "react";
5 | import svgPanZoom from "svg-pan-zoom";
6 | import { Button, Flex } from "@chakra-ui/react";
7 | import mermaid from "mermaid";
8 |
9 | let currentId = 0;
10 | const uuid = () => `mermaid-${(currentId++).toString()}`;
11 |
12 | function downloadBlob(blob: Blob, filename: string) {
13 | const objectUrl = URL.createObjectURL(blob);
14 |
15 | const link = document.createElement("a");
16 | link.href = objectUrl;
17 | link.download = filename;
18 | document.body.appendChild(link);
19 | link.click();
20 | document.body.removeChild(link);
21 |
22 | setTimeout(() => URL.revokeObjectURL(objectUrl), 5000);
23 | }
24 |
25 | export default function Mermaid({ graphDefinition }: { graphDefinition: string }) {
26 | const [instance, setInstance] = useState(null);
27 | const enableZoom = useCallback(() => {
28 | instance?.enablePan();
29 | instance?.enableZoom();
30 | }, [instance]);
31 |
32 | const disableZoom = useCallback(() => {
33 | instance?.disablePan();
34 | instance?.disableZoom();
35 | }, [instance]);
36 |
37 | const resetZoom = useCallback(() => {
38 | instance?.fit();
39 | instance?.center();
40 | }, [instance]);
41 |
42 | const ref = useRef(null);
43 | const [hasError, setHasError] = React.useState(false);
44 | const currentId = uuid();
45 |
46 | const downloadSVG = useCallback(() => {
47 | const svg = ref.current!.innerHTML;
48 | const blob = new Blob([svg], { type: "image/svg+xml" });
49 | downloadBlob(blob, `myimage.svg`);
50 | }, []);
51 |
52 | useEffect(() => {
53 | if (!ref.current || !graphDefinition) return;
54 | mermaid.initialize({
55 | startOnLoad: false,
56 | });
57 |
58 | mermaid.mermaidAPI
59 | .render(currentId, graphDefinition)
60 | .then(({ svg, bindFunctions }) => {
61 | ref.current!.innerHTML = svg;
62 | bindFunctions?.(ref.current!);
63 |
64 | setInstance(() => {
65 | const instance = svgPanZoom(ref.current!.querySelector("svg")!);
66 | instance.fit();
67 | instance.center();
68 | instance.disablePan();
69 | instance.disableZoom();
70 | return instance;
71 | });
72 | })
73 | .catch((e) => {
74 | console.info(e);
75 |
76 | // NOTE(CGQAQ): there's a bug in mermaid will always throw an error:
77 | // Error: Diagram error not found.
78 | // we need to check if the svg is rendered.
79 | // if rendered, we can ignore the error.
80 | // ref: https://github.com/mermaid-js/mermaid/issues/4140
81 | if (ref.current?.querySelector("svg") == null) {
82 | setHasError(true);
83 | }
84 | });
85 | }, [graphDefinition]);
86 |
87 | useEffect(() => {
88 | const handleSpaceDown = (e: KeyboardEvent) => {
89 | if (e.code === "Space" && !e.repeat) {
90 | e.preventDefault();
91 | enableZoom();
92 | }
93 | };
94 |
95 | const handleSpaceUp = (e: KeyboardEvent) => {
96 | if (e.code === "Space" && !e.repeat) {
97 | disableZoom();
98 | }
99 | };
100 | document.addEventListener("keydown", handleSpaceDown);
101 | document.addEventListener("keyup", handleSpaceUp);
102 |
103 | return () => {
104 | document.removeEventListener("keydown", handleSpaceDown);
105 | document.removeEventListener("keyup", handleSpaceUp);
106 | };
107 | }, [enableZoom, disableZoom]);
108 |
109 | if (hasError || !graphDefinition) return {graphDefinition};
110 | return (
111 | <>
112 |
113 | * hold space to pan & zoom
114 |
115 | {
118 | ref.current?.querySelector("svg")?.setPointerCapture(event.pointerId);
119 | }}
120 | >
121 |
122 |
123 |
124 |
125 | >
126 | );
127 | }
128 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chat-flow",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "prepare": "husky install",
7 | "dev": "npm run prepare:data && cross-env NODE_ENV='development' next dev",
8 | "build": "next build",
9 | "postbuild": "next-sitemap",
10 | "start": "npm run dev",
11 | "lint": "next lint",
12 | "lint:fix": "next lint --fix",
13 | "format": "prettier --check . -u",
14 | "format:fix": "prettier --write . -u",
15 | "postinstall": "npm run prepare:data",
16 | "prepare:env": "npx vercel link && npx vercel env pull .env.local",
17 | "prepare:data": "node gen/generate-chatgpt-by-category.js",
18 | "test": "jest",
19 | "test:watch": "jest --watch"
20 | },
21 | "dependencies": {
22 | "@chakra-ui/icons": "^2.0.17",
23 | "@chakra-ui/react": "^2.5.1",
24 | "@chakra-ui/spinner": "^2.0.13",
25 | "@chakra-ui/system": "^2.5.1",
26 | "@emotion/react": "^11.10.6",
27 | "@emotion/styled": "^11.10.6",
28 | "@formatjs/intl-localematcher": "^0.2.32",
29 | "@planetscale/database": "^1.6.0",
30 | "@prisma/client": "^4.11.0",
31 | "@remirror/pm": "^2.0.4",
32 | "@remirror/react": "^2.0.27",
33 | "@remirror/react-editors": "^1.0.27",
34 | "@tanstack/react-table": "^8.7.9",
35 | "@types/jsonpath-plus": "^5.0.2",
36 | "@vercel/analytics": "^0.1.11",
37 | "autosize": "^6.0.1",
38 | "chakra-ui-markdown-renderer": "^4.1.0",
39 | "client-only": "^0.0.1",
40 | "dagre": "^0.8.5",
41 | "dotparser": "^1.1.1",
42 | "encoding": "^0.1.13",
43 | "expr-eval": "^2.0.2",
44 | "formik": "^2.2.9",
45 | "framer-motion": "^10.0.1",
46 | "jsonpath-plus": "^7.2.0",
47 | "kysely": "^0.23.5",
48 | "kysely-planetscale": "^1.3.0",
49 | "lodash-es": "^4.17.21",
50 | "mermaid": "^10.0.2",
51 | "negotiator": "^0.6.3",
52 | "next": "^13.2.4",
53 | "next-sitemap": "^4.0.2",
54 | "node-fetch": "^2",
55 | "openai": "^3.2.1",
56 | "react": "18.2.0",
57 | "react-color": "^2.19.3",
58 | "react-copy-to-clipboard": "^5.1.0",
59 | "react-dom": "18.2.0",
60 | "react-json-view": "^1.21.3",
61 | "react-markdown": "^8.0.5",
62 | "react-spinners": "^0.13.8",
63 | "react-syntax-highlighter": "^15.5.0",
64 | "reactflow": "^11.6.0",
65 | "remark": "^14.0.2",
66 | "remark-gfm": "^3.0.1",
67 | "remirror": "^2.0.26",
68 | "rxjs": "^7.8.0",
69 | "server-only": "^0.0.1",
70 | "sharp": "^0.31.3",
71 | "svg-pan-zoom": "^3.6.1",
72 | "typescript": "4.9.5",
73 | "unist-util-select": "^4.0.3",
74 | "use-debounce": "^9.0.3",
75 | "uuid": "^9.0.0",
76 | "zustand": "^4.3.6"
77 | },
78 | "devDependencies": {
79 | "@svgr/webpack": "^6.5.1",
80 | "@testing-library/jest-dom": "^5.16.5",
81 | "@testing-library/react": "^14.0.0",
82 | "@types/autosize": "^4.0.1",
83 | "@types/dagre": "^0.7.48",
84 | "@types/js-yaml": "^4.0.5",
85 | "@types/jsonpath": "^0.2.0",
86 | "@types/lodash-es": "^4.17.6",
87 | "@types/negotiator": "^0.6.1",
88 | "@types/node": "18.14.5",
89 | "@types/node-fetch": "^2.6.2",
90 | "@types/papaparse": "^5.3.7",
91 | "@types/react": "18.0.28",
92 | "@types/react-color": "^3.0.6",
93 | "@types/react-copy-to-clipboard": "^5.0.4",
94 | "@types/react-dom": "18.0.11",
95 | "@types/react-syntax-highlighter": "^15.5.6",
96 | "@types/tunnel": "^0.0.3",
97 | "@typescript-eslint/eslint-plugin": "^5.54.1",
98 | "autoprefixer": "^10.4.13",
99 | "cross-env": "^7.0.3",
100 | "eslint": "8.35.0",
101 | "eslint-config-next": "13.2.3",
102 | "eslint-config-prettier": "^8.6.0",
103 | "eslint-plugin-prettier": "^4.2.1",
104 | "husky": "^8.0.3",
105 | "jest": "^29.4.3",
106 | "jest-environment-jsdom": "^29.4.3",
107 | "js-yaml": "^4.1.0",
108 | "lint-staged": "^13.1.2",
109 | "postcss": "^8.4.21",
110 | "prettier": "^2.8.4",
111 | "prisma": "^4.11.0",
112 | "tailwindcss": "^3.2.7",
113 | "tunnel": "^0.0.6",
114 | "walkdir": "^0.4.1",
115 | "yaml-loader": "^0.8.0"
116 | },
117 | "overrides": {
118 | "react-json-view": {
119 | "react": "$react",
120 | "react-dom": "$react-dom"
121 | },
122 | "flux": {
123 | "react": "$react",
124 | "react-dom": "$react-dom"
125 | }
126 | },
127 | "engines": {
128 | "npm": ">=8.11.0",
129 | "node": ">=16.19.0"
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/src/app/[lang]/click-flow/[id]/StartlingStepPage.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React, { useEffect } from "react";
4 | import {
5 | Box,
6 | Breadcrumb,
7 | BreadcrumbItem,
8 | BreadcrumbLink,
9 | Flex,
10 | Heading,
11 | SimpleGrid,
12 | Container,
13 | } from "@/components/ChakraUI";
14 | import StartlingStepDetail from "@/app/[lang]/click-flow/[id]/StartlingStepDetail";
15 | import { StartlingFlow } from "@/flows/types/click-flow";
16 | import FlowExplain from "../../../../flows/explain/FlowExplain";
17 | import { ReplService } from "@/flows/unitmesh/ReplService";
18 | import { webSocket } from "rxjs/webSocket";
19 | import { WebSocketSubject } from "rxjs/internal/observable/dom/WebSocketSubject";
20 | import styled from "@emotion/styled";
21 |
22 | type StepPageProps = {
23 | flow: StartlingFlow;
24 | id: string;
25 | i18n: GeneralI18nProps;
26 | };
27 |
28 | function StartlingStepPage({ flow, id, i18n }: StepPageProps) {
29 | const [conversationId, setConversationId] = React.useState(undefined);
30 | const [cachedValue, setCachedValue] = React.useState>({});
31 |
32 | const [currentStep, setCurrentStep] = React.useState(0);
33 |
34 | const [replService, setReplService] = React.useState(undefined);
35 | useEffect(() => {
36 | if (flow.replService) {
37 | try {
38 | const host = process.env.REPL_SERVER ? process.env.REPL_SERVER : "127.0.0.1:8080";
39 | const subject = webSocket(`ws://${host}/repl`);
40 |
41 | setReplService(new ReplService(subject as WebSocketSubject));
42 | } catch (e) {
43 | console.error("Failed to create repl service", e);
44 | }
45 | }
46 | }, []);
47 |
48 | const bottomAnchor = React.useRef(null);
49 | useEffect(() => {
50 | if (!flow.stepGuide) {
51 | return;
52 | }
53 |
54 | let counter = 0;
55 | const id = setInterval(() => {
56 | if (bottomAnchor.current) {
57 | bottomAnchor.current.scrollIntoView({ behavior: "auto" });
58 | }
59 |
60 | counter++;
61 | // animation delay 500ms
62 | if (counter > 5) {
63 | clearInterval(id);
64 | }
65 | }, 100);
66 |
67 | return () => {
68 | clearInterval(id);
69 | };
70 | }, [currentStep]);
71 |
72 | const dict = i18n.i18n.dict;
73 |
74 | const updateCached = (index: number, value: any) => {
75 | setCachedValue((prev) => ({ ...prev, [index]: value }));
76 | };
77 |
78 | const updateConversationId = (conversationId: number) => {
79 | setConversationId(conversationId);
80 | };
81 |
82 | return (
83 |
84 | {flow && (
85 | <>
86 |
87 |
88 |
89 |
90 | {dict["by-each-step-samples"]}
91 |
92 |
93 | {flow.name}
94 |
95 |
96 |
97 |
98 | {flow.name}
99 |
100 | {flow.explain && (
101 |
102 |
103 |
104 | )}
105 |
106 |
107 | {flow.steps.map(
108 | (step, index) =>
109 | (index <= currentStep || !flow.stepGuide) /** show all if stepGuide is falsey */ && (
110 | setCurrentStep(index + 1)}
121 | />
122 | ),
123 | )}
124 |
125 |
126 |
127 |
128 | >
129 | )}
130 |
131 | );
132 | }
133 |
134 | const StyledHeading = styled(Heading)`
135 | padding: 1rem 0;
136 | text-align: center;
137 | `;
138 |
139 | export default StartlingStepPage;
140 |
--------------------------------------------------------------------------------
/src/storage/planetscale.ts:
--------------------------------------------------------------------------------
1 | import { Kysely } from "kysely";
2 | import { PlanetScaleDialect } from "kysely-planetscale";
3 | import { cache } from "react";
4 |
5 | enum NumBool {
6 | True = 1,
7 | False = 0,
8 | }
9 |
10 | interface UserTable {
11 | id?: number;
12 | key_hashed: string;
13 | iv: string;
14 | key_encrypted: string;
15 | deleted?: NumBool;
16 | created_at?: string;
17 | }
18 |
19 | interface ConversationTable {
20 | id?: number;
21 | user_id: number;
22 | name: string;
23 | deleted?: NumBool;
24 | created_at?: string;
25 | }
26 |
27 | interface ChatTable {
28 | id?: number;
29 | conversation_id: number;
30 | role: string; // line 14
31 | content: string;
32 | name?: string;
33 | created_at?: string;
34 | }
35 |
36 | interface Database {
37 | users: UserTable;
38 | conversations: ConversationTable;
39 | chats: ChatTable;
40 | }
41 |
42 | export const queryBuilder = new Kysely({
43 | dialect: new PlanetScaleDialect({
44 | url: process.env.DATABASE_URL,
45 | }),
46 | });
47 |
48 | export const getAllConversionsByUserId = cache(async (userId: number) => {
49 | return queryBuilder
50 | .selectFrom("conversations")
51 | .select(["conversations.id", "conversations.user_id", "conversations.name", "conversations.created_at"])
52 | .where((qb) => qb.where("conversations.user_id", "=", userId).where("conversations.deleted", "=", 0))
53 | .orderBy("created_at", "desc")
54 | .limit(100)
55 | .execute();
56 | });
57 |
58 | export const changeConversationName = cache(async (conversationId: number, name: string) => {
59 | return queryBuilder
60 | .updateTable("conversations")
61 | .set({
62 | name,
63 | })
64 | .where("conversations.id", "=", conversationId)
65 | .execute();
66 | });
67 |
68 | export const getAllChatsInsideConversation = cache(async (conversationId: number) => {
69 | return queryBuilder
70 | .selectFrom("chats")
71 | .selectAll()
72 | .where("chats.conversation_id", "=", conversationId)
73 | .limit(100)
74 | .execute();
75 | });
76 |
77 | export const isValidUser = cache(async (keyHashed: string) => {
78 | return queryBuilder
79 | .selectFrom("users")
80 | .select("users.key_hashed")
81 | .where("users.deleted", "=", 0)
82 | .where("users.key_hashed", "=", keyHashed)
83 | .limit(1)
84 | .execute()
85 | .then((users) => users.length === 1);
86 | });
87 |
88 | export const createUser = cache(async (data: Pick) => {
89 | return queryBuilder.insertInto("users").values(data).execute();
90 | });
91 |
92 | export const createConversation = cache(async (data: Pick) => {
93 | const r = await queryBuilder.insertInto("conversations").values(data).executeTakeFirst();
94 |
95 | if (!r) {
96 | return null;
97 | }
98 |
99 | return queryBuilder
100 | .selectFrom("conversations")
101 | .selectAll()
102 | .where("conversations.id", "=", Number(r.insertId))
103 | .limit(1)
104 | .executeTakeFirst();
105 | });
106 |
107 | export const createChat = cache(async (data: Pick[]) => {
108 | return queryBuilder.insertInto("chats").values(data).execute();
109 | });
110 |
111 | export const getChatById = cache(async (chatId: number) => {
112 | return queryBuilder.selectFrom("chats").selectAll().where("chats.id", "=", chatId).limit(1).executeTakeFirst();
113 | });
114 |
115 | export const deleteConversation = cache(async (conversationId: number) => {
116 | return queryBuilder
117 | .updateTable("conversations")
118 | .set({
119 | deleted: 1,
120 | })
121 | .where("conversations.id", "=", conversationId)
122 | .execute();
123 | });
124 |
125 | export const deleteAllConversationsByUserId = cache(async (userId: number) => {
126 | return queryBuilder
127 | .updateTable("conversations")
128 | .set({
129 | deleted: 1,
130 | })
131 | .where("conversations.user_id", "=", userId)
132 | .execute();
133 | });
134 |
135 | export const getUserByKeyHashed = cache(async (keyHashed: string) => {
136 | const result = await queryBuilder
137 | .selectFrom("users")
138 | .selectAll()
139 | .where("users.key_hashed", "=", keyHashed)
140 | .limit(1)
141 | .execute();
142 |
143 | if (result.length !== 1) {
144 | return null;
145 | }
146 |
147 | return result[0];
148 | });
149 |
150 | // function generateDateTime() {
151 | // const date = new Date();
152 | // const padZero = (num: number) => num.toString().padStart(2, "0");
153 | // const datetime = `${date.getFullYear()}-${padZero(date.getMonth() + 1)}-${padZero(date.getDate())} ${padZero(
154 | // date.getHours(),
155 | // )}:${padZero(date.getMinutes())}:${padZero(date.getSeconds())}`;
156 | // return datetime;
157 | // }
158 |
--------------------------------------------------------------------------------
/src/pages/api/chatgpt/chat.ts:
--------------------------------------------------------------------------------
1 | import { NextApiHandler, NextApiResponse } from "next";
2 | import type { ChatCompletionRequestMessage, CreateChatCompletionResponse } from "openai";
3 | import type { OpenAIApi } from "openai";
4 | import { decryptKey } from "@/uitls/crypto.util";
5 | import { createChat, getAllChatsInsideConversation, createConversation } from "@/storage/planetscale";
6 | import { getChatClient } from "@/uitls/openapi.util";
7 | import { getUser } from "@/uitls/user.util";
8 | import { CHAT_COMPLETION_CONFIG } from "@/configs/constants";
9 |
10 | export type RequestSend = {
11 | action: "send";
12 | conversation_id: number;
13 | messages: ChatCompletionRequestMessage[];
14 | };
15 | export type ResponseSend = Awaited>;
16 |
17 | export type RequestGetChats = {
18 | action: "get_chats";
19 | conversation_id: number;
20 | };
21 | export type ResponseGetChats = Awaited>;
22 |
23 | type RequestBody = RequestSend | RequestGetChats;
24 |
25 | const handler: NextApiHandler = async (req, res) => {
26 | const user = await getUser(req, res);
27 | if (!user) {
28 | return;
29 | }
30 |
31 | const chatClient = await getChatClient(user.key_hashed, decryptKey(user.key_encrypted, user.iv));
32 |
33 | if (req.method === "POST" && req.body) {
34 | const body: RequestBody = typeof req.body === "string" ? JSON.parse(req.body) : req.body;
35 |
36 | switch (body.action) {
37 | case "send": {
38 | let conversation_id: number | undefined | null = body.conversation_id;
39 | // if no conversation.ts exists, create new one as default, elsewise `create Chat` will throw error
40 | if (!conversation_id) {
41 | const defaultConvesation = await createConversation({
42 | user_id: user.id as number,
43 | name: "Default Conversation name",
44 | });
45 | conversation_id = defaultConvesation?.id;
46 | }
47 | if (conversation_id == null) {
48 | res.status(400).json({ error: "No conversation_id found" });
49 | return;
50 | }
51 |
52 | const chats = await getAllChatsInsideConversation(conversation_id);
53 | await sendMsgs({
54 | res,
55 | client: chatClient,
56 | conversation_id: conversation_id,
57 | msgs: chats.map(
58 | (it) => ({ role: it.role, content: it.content, name: it.name } as ChatCompletionRequestMessage),
59 | ),
60 | newMsgs: body.messages,
61 | });
62 | return;
63 | }
64 |
65 | case "get_chats": {
66 | const chats = await getAllChatsInsideConversation(body.conversation_id);
67 |
68 | res.status(200).json(chats);
69 | return;
70 | }
71 |
72 | default:
73 | res.status(400).json(`Not supported action of ${(body as any)?.action}`);
74 | return;
75 | }
76 | } else {
77 | res.status(404).json({ error: "Not found" });
78 | return;
79 | }
80 | };
81 | export default handler;
82 |
83 | async function sendMsgs({
84 | res,
85 | client,
86 | conversation_id,
87 | msgs,
88 | newMsgs,
89 | }: {
90 | res: NextApiResponse;
91 | client: OpenAIApi;
92 | conversation_id: number;
93 | msgs: ChatCompletionRequestMessage[];
94 | newMsgs: ChatCompletionRequestMessage[];
95 | }) {
96 | try {
97 | const messages = [...msgs, ...newMsgs].map((it) => ({ ...it, name: it.name ?? undefined }));
98 | const response = await client.createChatCompletion({
99 | ...CHAT_COMPLETION_CONFIG,
100 | messages,
101 | });
102 | if (response.status !== 200) {
103 | res.status(response.status).json({ error: response.statusText });
104 | return;
105 | }
106 |
107 | const { choices } = response.data as CreateChatCompletionResponse;
108 | if (choices.length === 0 || !choices[0].message) {
109 | res.status(500).json({ error: "No response from OpenAI" });
110 | return;
111 | }
112 |
113 | // add response to newMsgs
114 | messages.push({ ...choices[0].message, name: undefined });
115 |
116 | const needToSave = newMsgs.concat(choices[0].message).map((it) => ({ ...it, conversation_id }));
117 | // save to database
118 | const result = await createChat(needToSave);
119 | if (!result) {
120 | res.status(500).json({ error: "Cannot save to database" });
121 | return;
122 | }
123 |
124 | return res.status(200).json([choices[0].message] as unknown as ResponseSend);
125 | } catch (e: any) {
126 | let msg = e.message;
127 | if (e.code === "ETIMEDOUT") {
128 | msg = "Request api was timeout, pls confirm your network worked";
129 | } else if (e.response && e.response.data) {
130 | msg = e.response.data.error;
131 | }
132 | res.status(500).json({ error: msg });
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/src/assets/chatgpt/flow/unit-mesh-unit-server.yml:
--------------------------------------------------------------------------------
1 | name: UnitMesh - UnitServer Demo
2 | category: Development
3 | author: Phodal Huang
4 | description: REPL Service for ClickFlow
5 | explain: |
6 | digraph G {
7 | 0[flowType = "prompt"]
8 | 1[flowType = "prompt"]
9 | 2[flowType = "prompt"]
10 | 3[flowType = "prompt"]
11 |
12 | 0 -> 1
13 | 1 -> 2
14 | 1 -> 3
15 | }
16 |
17 | stepGuide: false
18 | replService: true
19 | steps:
20 | - name: Setup REPL
21 | hiddenExecute: true
22 | ask: |
23 | 首先,你需要 Setup 一下 FlowRepl: [https://github.com/prompt-engineering/unit-server](https://github.com/prompt-engineering/unit-server)
24 |
25 | 然后,启动对应的 REPL 服务,例如:
26 |
27 | - Kotlin Repl
28 | - TypeScript Repl
29 |
30 | 然后,Run 一下下面的代码:
31 |
32 | ```kotlin
33 | var x = 1
34 | x + 10086
35 | ```
36 |
37 | 如果返回的是 `{"resultValue":"10087","className":"java.lang.Integer"}` 那就说明你的 REPL 服务已经可以正常工作了。
38 | - name: Generate Code
39 | markdownEditor: true
40 | ask: |
41 | 请帮我使用 Ktor + Kotlin + Exposed 实现一个用户注册的 RESTful API,要求如下:
42 |
43 | 1. 涉及到数据库的地方,请直接使用 Database.connect。
44 | 2. 只返回核心逻辑,并写在 Server 类里,我要部署在 Serverless 服务器里。
45 | 3. 请使用 Kotlin DSL 的方式编写代码。
46 | 4. 不返回其它的无关代码,如:注释、依赖、import 等。
47 |
48 | 最后,你只返回类的代码,返回格式如下:
49 |
50 | ```kotlin
51 | class Server : KotlessAWS() {
52 | override fun prepare(app: Application) {
53 | Database.connect("jdbc:h2:mem:test", driver = "org.h2.Driver", user = "root", password = "")
54 | transaction {
55 | SchemaUtils.create(Users)
56 | }
57 |
58 | app.routing {
59 | {{{}}}
60 | }
61 | }
62 | }
63 | ```
64 | - name: Kotlin Ktor Sample
65 | hiddenExecute: true
66 | ask: |
67 | 下面的代码是 AI 生成的,不过好像有点问题:
68 |
69 | ```kotlin
70 | %use kotless, ktor, exposed
71 |
72 | data class User(val id: Int, val username: String)
73 |
74 | class Server : KotlessAWS() {
75 | override fun prepare(app: Application) {
76 | Database.connect("jdbc:h2:mem:test", driver = "org.h2.Driver")
77 |
78 | transaction {
79 | SchemaUtils.create(Users)
80 | }
81 |
82 | app.routing {
83 | get("/users") {
84 | val users = transaction {
85 | Users.selectAll().map {
86 | User(it[Users.id], it[Users.username])
87 | }
88 | }
89 | call.respond(users)
90 | }
91 | post("/register") {
92 | val user = call.receive()
93 | val id = transaction {
94 | // Insert the new user into the database
95 | Users.insert {
96 | it[username] = user.username
97 | } get Users.id
98 | }
99 |
100 | val newUser = User(id, user.username)
101 | call.respond(newUser)
102 | }
103 | }
104 | }
105 | }
106 |
107 | object Users : org.jetbrains.exposed.sql.Table("users") {
108 | val id = integer("id").autoIncrement()
109 | val username = varchar("username", 50).uniqueIndex()
110 |
111 | override val primaryKey = PrimaryKey(id, name = "PK_User_ID")
112 | }
113 | ```
114 |
115 | - name: Kotlin Spring Sample
116 | hiddenExecute: true
117 | ask: |
118 | ```kotlin
119 | %use spring, kotless
120 |
121 | @RestController
122 | class SampleController {
123 | @GetMapping("/hello")
124 | fun helloKotlin(): String {
125 | return "hello world"
126 | }
127 | }
128 | ```
129 | - name: React Hello, World
130 | hiddenExecute: true
131 | ask: |
132 | ```typescript
133 | import React, {useState, useEffect} from "react";
134 | import ReactDom, {createRoot} from "react-dom/client";
135 |
136 | const root = ReactDOM.createRoot(document.getElementById('root'));
137 | root.render(Hello, world!
);
138 | ```
139 | - name: React Sample
140 | hiddenExecute: true
141 | ask: |
142 | ```typescript
143 | import React, {useState, useEffect} from "react";
144 | import ReactDom, {createRoot} from "react-dom/client";
145 |
146 | function Root() {
147 | const [tick, setTick] = useState(0);
148 |
149 | useEffect(() => {
150 | const id = setInterval(() => {
151 | setTick(it => it+1);
152 | }, 1000);
153 |
154 | return () => clearInterval(id);
155 | }, []);
156 |
157 | return <>
158 | Hello world #{tick}
159 | >;
160 | }
161 |
162 | const root = ReactDOM.createRoot(document.getElementById('root'));
163 | root.render();
164 | ```
165 |
--------------------------------------------------------------------------------
/src/app/api/chatgpt/stream/route.ts:
--------------------------------------------------------------------------------
1 | import { CHAT_COMPLETION_CONFIG } from "@/configs/constants";
2 | import { createChat, createConversation, getAllChatsInsideConversation } from "@/storage/planetscale";
3 | import { decryptKey } from "@/uitls/crypto.util";
4 | import { getChatClient } from "@/uitls/openapi.util";
5 | import { getUser, User } from "@/uitls/user.edge.util";
6 | import { ChatCompletionRequestMessage, ChatCompletionRequestMessageRoleEnum } from "openai";
7 |
8 | export async function POST(request: Request, response: Response) {
9 | // TODO mixin?
10 | const user = await getUser();
11 | if (!user || !(user as User)?.id) {
12 | return user;
13 | }
14 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
15 | // @ts-ignore
16 | const { id: user_id, key_hashed, key_encrypted, iv } = user as User;
17 | const body = await request.json();
18 | let conversation_id: number | undefined | null = body.conversation_id;
19 | // if no conversation.ts exists, create new one as default, elsewise `create Chat` will throw error
20 | if (!conversation_id) {
21 | const defaultConvesation = await createConversation({
22 | user_id,
23 | name: "Default Conversation name",
24 | });
25 | conversation_id = defaultConvesation?.id;
26 | }
27 | if (conversation_id == null) {
28 | return new Response(JSON.stringify({ error: "No conversation_id found" }), {
29 | status: 400,
30 | });
31 | }
32 | const chatClient = await getChatClient(key_hashed, decryptKey(key_encrypted, iv));
33 | const chats = await getAllChatsInsideConversation(conversation_id);
34 | const msgs = chats.map(
35 | (it) => ({ role: it.role, content: it.content, name: it.name } as ChatCompletionRequestMessage),
36 | );
37 | const newMsgs = body.messages;
38 | try {
39 | const messages = [...msgs, ...newMsgs].map((it) => ({ ...it, name: it.name ?? undefined }));
40 | const response = await chatClient.createChatCompletion(
41 | {
42 | ...CHAT_COMPLETION_CONFIG,
43 | messages,
44 | stream: true,
45 | },
46 | { responseType: "stream" },
47 | );
48 | if (response.status !== 200) {
49 | return new Response(JSON.stringify({ error: response.statusText }), {
50 | status: response.status,
51 | });
52 | }
53 | let controller: any;
54 | const encoder = new TextEncoder();
55 | const stream = new ReadableStream({
56 | async start(_) {
57 | controller = _;
58 | },
59 | });
60 | let msg = "",
61 | role: ChatCompletionRequestMessageRoleEnum;
62 | // FIXME add typescript type for res
63 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
64 | // @ts-ignore
65 | response.data.on("data", async (data: BufferSource | undefined) => {
66 | if (data) {
67 | const dataStr = data.toString();
68 | controller.enqueue(encoder.encode(dataStr));
69 | // for save chat history
70 | const lines = dataStr.split("\n").filter((line) => line.trim() !== "");
71 | for (const line of lines) {
72 | const message = line.replace(/^data: /, "");
73 | if (message === "[DONE]") {
74 | controller.close();
75 | // add response to newMsgs
76 | const _newMsg = { content: msg, role };
77 | messages.push({ ..._newMsg, name: undefined });
78 | const needToSave = newMsgs.concat(_newMsg).map((it: any) => ({ ...it, conversation_id }));
79 | try {
80 | // save to database
81 | const result = await createChat(needToSave);
82 | if (!result) {
83 | // TODO logging
84 | }
85 | } catch (error) {
86 | console.error("save to database error", error);
87 | }
88 | } else {
89 | try {
90 | const parsed = JSON.parse(message).choices[0].delta;
91 | if (parsed.role) {
92 | role = parsed.role;
93 | }
94 | if (parsed.content) {
95 | msg += parsed.content;
96 | }
97 | } catch (error) {
98 | console.error("Could not JSON parse stream message", message, error);
99 | }
100 | }
101 | }
102 | }
103 | });
104 |
105 | return new Response(stream, {
106 | headers: {
107 | "Content-Type": "text/event-stream;",
108 | "Cache-Control": "no-cache",
109 | Connection: "keep-alive",
110 | },
111 | });
112 | } catch (e: any) {
113 | if (e.response?.status) {
114 | e.response.data.on("data", (data: BufferSource | undefined) => {
115 | return new Response(JSON.stringify({ error: data?.toString() }), {
116 | status: 500,
117 | });
118 | });
119 | } else {
120 | let msg = e.message;
121 | if (e.code === "ETIMEDOUT") {
122 | msg = "Request api was timeout, pls confirm your network worked";
123 | }
124 | return new Response(JSON.stringify({ error: msg }), {
125 | status: 500,
126 | });
127 | }
128 | }
129 | }
130 |
--------------------------------------------------------------------------------