├── .env.example ├── .eslintignore ├── .eslintrc.json ├── .github └── workflows │ ├── deploy.yml │ └── release.yml ├── .gitignore ├── .gitmodules ├── .node-version ├── .npmrc ├── .prettierignore ├── .prettierrc ├── .storybook ├── main.ts └── preview.ts ├── .vscode └── settings.json ├── CNAME ├── CONTRIBUTION.md ├── README.md ├── README.zh.md ├── docs ├── Motivation.mdx ├── architecture.svg ├── logo.svg ├── research.md └── test-doc.md ├── jest.config.ts ├── jest.preset.js ├── local_server ├── .gitignore ├── Cargo.toml ├── README.md ├── _fixtures │ └── header.docx ├── project.json └── src │ ├── app_state.rs │ ├── doc_indexes │ ├── doc_indexes.rs │ ├── doc_reader.rs │ ├── doc_schema.rs │ └── mod.rs │ ├── doc_split │ ├── document_type.rs │ ├── html_splitter.rs │ ├── markdown_splitter.rs │ ├── mod.rs │ ├── office_splitter.rs │ ├── pdf_splitter.rs │ ├── split.rs │ └── splitter.rs │ ├── document_handler.rs │ ├── infra │ ├── dir_watcher.rs │ ├── file_walker.rs │ └── mod.rs │ ├── main.rs │ └── scraper │ ├── LICENSE │ ├── article.rs │ ├── chunk.rs │ ├── mod.rs │ └── text_range.rs ├── nx.json ├── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── tsconfig.base.json └── web ├── converter ├── package.json ├── tsconfig.json ├── tsconfig.node.json └── vite.config.mts ├── core ├── .eslintrc.cjs ├── .gitignore ├── README.md ├── lib │ ├── editor │ │ ├── action │ │ │ ├── AiActionExecutor.ts │ │ │ ├── BuiltinFunctionExecutor.ts │ │ │ ├── PromptCompiler.ts │ │ │ └── custom-editor-commands.ts │ │ ├── components │ │ │ ├── base │ │ │ │ ├── select.tsx │ │ │ │ └── slider.tsx │ │ │ ├── settings.tsx │ │ │ ├── sidebar.tsx │ │ │ └── ui-select.tsx │ │ ├── defs │ │ │ ├── custom-action.type.ts │ │ │ └── type-options.type.ts │ │ ├── editor.css │ │ ├── extensions │ │ │ ├── advice │ │ │ │ ├── advice-extension.ts │ │ │ │ ├── advice-manager.ts │ │ │ │ ├── advice-view.tsx │ │ │ │ └── advice.ts │ │ │ ├── inline-completion │ │ │ │ └── inline-completion.tsx │ │ │ ├── quick-box │ │ │ │ ├── quick-box-extension.ts │ │ │ │ ├── quick-box-view-wrapper.tsx │ │ │ │ └── quick-box-view.tsx │ │ │ └── slash-command │ │ │ │ ├── slash-extension.ts │ │ │ │ └── slash-view.tsx │ │ ├── hooks │ │ │ └── useEditorContentChange.tsx │ │ ├── live-editor.tsx │ │ ├── menu │ │ │ ├── menu-bubble.tsx │ │ │ ├── toolbar-ai-dropdown.tsx │ │ │ └── toolbar-menu.tsx │ │ ├── prompts │ │ │ ├── TemplateRender.ts │ │ │ ├── article-prompts.ts │ │ │ ├── prompts-manager.ts │ │ │ └── requirements-prompts.ts │ │ └── typeing.d.ts │ └── main.ts ├── package.json ├── postcss.config.js ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.node.json └── vite.config.mts ├── llmapi ├── package.json ├── src │ ├── ernie.ts │ ├── hunyuan.ts │ ├── index.ts │ ├── minimax.ts │ ├── qwen.ts │ ├── resource.ts │ ├── streaming.ts │ ├── util.ts │ └── vyro.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.mts ├── native-wrapper ├── .gitignore ├── .vscode │ └── extensions.json ├── README.md ├── index.html ├── package.json ├── public │ ├── tauri.svg │ └── vite.svg ├── src-tauri │ ├── .gitignore │ ├── Cargo.lock │ ├── Cargo.toml │ ├── build.rs │ ├── icons │ │ ├── 128x128.png │ │ ├── 128x128@2x.png │ │ ├── 32x32.png │ │ ├── Square107x107Logo.png │ │ ├── Square142x142Logo.png │ │ ├── Square150x150Logo.png │ │ ├── Square284x284Logo.png │ │ ├── Square30x30Logo.png │ │ ├── Square310x310Logo.png │ │ ├── Square44x44Logo.png │ │ ├── Square71x71Logo.png │ │ ├── Square89x89Logo.png │ │ ├── StoreLogo.png │ │ ├── icon.icns │ │ ├── icon.ico │ │ └── icon.png │ ├── src │ │ └── main.rs │ └── tauri.conf.json ├── src │ ├── App.css │ ├── App.tsx │ ├── assets │ │ └── preact.svg │ ├── main.tsx │ ├── styles.css │ └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts └── studio ├── .eslintrc.json ├── .gitignore ├── .nvmrc ├── app └── api │ ├── completion │ ├── hunyuan │ │ ├── ai.ts │ │ └── route.ts │ ├── minimax │ │ ├── ai.ts │ │ └── route.ts │ ├── mock │ │ └── route.ts │ ├── openai │ │ ├── ai.ts │ │ └── route.ts │ ├── qwen │ │ ├── ai.ts │ │ └── route.ts │ ├── route.ts │ └── yiyan │ │ ├── ai.ts │ │ └── route.ts │ ├── hello │ └── route.ts │ └── images │ └── generate │ ├── qwen │ └── route.ts │ └── route.ts ├── components └── .gitkeep ├── i18n └── i18n.ts ├── index.d.ts ├── jest.config.ts ├── next.config.ci.js ├── next.config.js ├── package.json ├── pages ├── _app.tsx └── index.tsx ├── postcss.config.js ├── public ├── .gitkeep ├── .nojekyll └── favicon.ico ├── styles ├── Home.module.css ├── editor-styles.css └── global.css ├── tailwind.config.ts ├── tsconfig.json ├── tsconfig.spec.json └── types └── llm-model.type.ts /.env.example: -------------------------------------------------------------------------------- 1 | # 文心一言 飞浆平台分发 2 | # See https://aistudio.baidu.com/index/accessToken 3 | AISTUDIO_ACCESS_TOKEN="xxxxxxxxxxxxxxxxxxxxxxxxxxxx" 4 | 5 | # 通义千问 6 | # See https://help.aliyun.com/zh/dashscope/developer-reference/activate-dashscope-and-create-an-api-key 7 | QWEN_API_KEY="sk-xxxxxxxxxxxxxxxxxxxxx" 8 | 9 | # OpenAI 10 | # See https://platform.openai.com/account/api-keys 11 | OPENAI_API_KEY="sk-xxxxxxxxxxxxxxxxxxxxx" 12 | 13 | # Minimax 14 | # See https://api.minimax.chat/user-center/basic-information/interface-key 15 | MINIMAX_API_ORG="xxxxxxxx" 16 | MINIMAX_API_KEY="eyJhxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" 17 | 18 | # Imagine Art 19 | # see https://platform.imagine.art/dashboard 20 | VYRO_API_KEY="vk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" 21 | 22 | # 腾讯混元大模型 23 | # see https://console.cloud.tencent.com/cam/capi 24 | HUNYUAN_APP_ID="xxxxxx" 25 | HUNYUAN_SECRET_ID="AKIDxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" 26 | HUNYUAN_SECRET_KEY="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" 27 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": ["**/*"], 4 | "plugins": ["@nx"], 5 | "overrides": [ 6 | { 7 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 8 | "rules": { 9 | "@nx/enforce-module-boundaries": [ 10 | "error", 11 | { 12 | "enforceBuildableLibDependency": true, 13 | "allow": [], 14 | "depConstraints": [ 15 | { 16 | "sourceTag": "*", 17 | "onlyDependOnLibsWithTags": ["*"] 18 | } 19 | ] 20 | } 21 | ] 22 | } 23 | }, 24 | { 25 | "files": ["*.ts", "*.tsx"], 26 | "extends": ["plugin:@nx/typescript"], 27 | "rules": {} 28 | }, 29 | { 30 | "files": ["*.js", "*.jsx"], 31 | "extends": ["plugin:@nx/javascript"], 32 | "rules": {} 33 | }, 34 | { 35 | "files": ["*.spec.ts", "*.spec.tsx", "*.spec.js", "*.spec.jsx"], 36 | "env": { 37 | "jest": true 38 | }, 39 | "rules": {} 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | # Sample workflow for building and deploying a Next.js site to GitHub Pages 2 | # 3 | # To get started with Next.js see: https://nextjs.org/docs/getting-started 4 | # 5 | name: GitHub Pages Deploy 6 | 7 | on: 8 | # Runs on pushes targeting the default branch 9 | push: 10 | branches: [ "master" ] 11 | 12 | # Allows you to run this workflow manually from the Actions tab 13 | workflow_dispatch: 14 | 15 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 16 | permissions: 17 | contents: read 18 | pages: write 19 | id-token: write 20 | 21 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 22 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 23 | concurrency: 24 | group: "pages" 25 | cancel-in-progress: false 26 | 27 | jobs: 28 | # Build job 29 | build: 30 | runs-on: ubuntu-latest 31 | steps: 32 | - uses: pnpm/action-setup@v4 33 | with: 34 | version: 9 35 | - name: Checkout 36 | uses: actions/checkout@v4 37 | 38 | - name: Override config 39 | run: | 40 | cp -f web/studio/next.config.ci.js web/studio/next.config.js 41 | 42 | - name: Install dependencies 43 | run: pnpm install --frozen-lockfile 44 | 45 | - name: Build 46 | run: | 47 | pnpm run build-studio 48 | 49 | - name: Upload artifact 50 | uses: actions/upload-pages-artifact@v3 51 | with: 52 | path: ./web/studio/out 53 | 54 | # Deployment job 55 | deploy: 56 | environment: 57 | name: github-pages 58 | url: ${{ steps.deployment.outputs.page_url }} 59 | runs-on: ubuntu-latest 60 | needs: build 61 | steps: 62 | - name: Deploy to GitHub Pages 63 | id: deployment 64 | uses: actions/deploy-pages@v4 65 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: LocalServer Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | publish: 13 | name: Publish for ${{ matrix.os }} 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | matrix: 17 | include: 18 | - os: ubuntu-latest 19 | artifact_name: b3_server 20 | asset_name: b3_server-linux 21 | - os: windows-latest 22 | artifact_name: b3_server.exe 23 | asset_name: b3_server-windows.exe 24 | - os: macos-latest 25 | artifact_name: b3_server 26 | asset_name: b3_server-macos 27 | 28 | steps: 29 | - uses: actions/checkout@v4 30 | 31 | - name: Build Server 32 | run: cargo build --release --manifest-path=local_server/Cargo.toml 33 | 34 | - name: Upload binaries to release 35 | uses: svenstaro/upload-release-action@v2 36 | with: 37 | repo_token: ${{ secrets.GITHUB_TOKEN }} 38 | file: local_server/target/release/${{ matrix.artifact_name }} 39 | asset_name: ${{ matrix.asset_name }} 40 | tag: ${{ github.ref }} 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | 3 | .nx/installation 4 | .nx/cache 5 | .env 6 | .idea 7 | .next 8 | next-env.d.ts 9 | out 10 | dist 11 | dist-types 12 | 13 | .nx -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "local_server/model"] 2 | path = local_server/model 3 | url = https://github.com/unit-mesh/mini-embedding 4 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 18.x.x -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | # pnpm 2 | node-linker=isolated 3 | shamefully-hoist=true 4 | # hoist-pattern[]=*!@radix-ui/* 5 | # hoist-pattern[]=*!@tiptap/* 6 | # public-hoist-pattern[]=*!@radix-ui/* 7 | # public-hoist-pattern[]=*!@tiptap/* 8 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Add files here to ignore them from prettier formatting 2 | /dist 3 | /coverage 4 | /.nx/cache -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from "@storybook/nextjs"; 2 | 3 | const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin'); 4 | 5 | const config: StorybookConfig = { 6 | stories: [ 7 | "../src/**/*.mdx", 8 | "../docs/**/*.mdx", 9 | "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)" 10 | ], 11 | addons: [ 12 | "@storybook/addon-links", 13 | "@storybook/addon-essentials", 14 | "@storybook/addon-onboarding", 15 | "@storybook/addon-interactions", 16 | ], 17 | framework: { 18 | name: "@storybook/nextjs", 19 | options: {}, 20 | }, 21 | docs: { 22 | autodocs: "tag", 23 | }, 24 | webpackFinal: async (config, { configType }) => { 25 | // @ts-ignore 26 | config.resolve.plugins = [new TsconfigPathsPlugin()]; 27 | return config; 28 | } 29 | }; 30 | export default config; 31 | -------------------------------------------------------------------------------- /.storybook/preview.ts: -------------------------------------------------------------------------------- 1 | import type { Preview } from "@storybook/react"; 2 | import '@/i18n/i18n'; 3 | import '../styles/global.css'; 4 | 5 | const preview: Preview = { 6 | parameters: { 7 | actions: { argTypesRegex: "^on[A-Z].*" }, 8 | controls: { 9 | matchers: { 10 | color: /(background|color)$/i, 11 | date: /Date$/i, 12 | }, 13 | }, 14 | }, 15 | }; 16 | 17 | export default preview; 18 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "git.ignoreLimitWarning": true 3 | } -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | editor.unitmesh.cc -------------------------------------------------------------------------------- /CONTRIBUTION.md: -------------------------------------------------------------------------------- 1 | First of all, *any* contribution is welcome. If you have any idea, suggestion, bug report, or you want to contribute with code, please feel free to do so. 2 | 3 | If you want to contribute with code, please follow these steps: 4 | 1. Fork the repository. 5 | 2. Create a new branch with a descriptive name of your contribution. 6 | 3. Commit and push your changes to your branch. 7 | 4. Create a pull request. Please, be descriptive with your contribution. 8 | 9 | This project is a monorepo, that means that it contains multiple packages. specifically, it contains two packages: 10 | 1. "@studio-b3/web-core" located in the `web/core` directory. 11 | 2. "@studio-b3/web-studio" located in the `web/studio` directory. 12 | 13 | The first package is the core of the project, it contains the core editor react component. The second package is the studio, it contains the studio logic and the studio components that are built on top of the core. 14 | 15 | This project uses NX to manage the monorepo. If you want to know more about NX, please visit their [website](https://nx.dev/). We have a few scripts that you can use to develop the project as well in the root `package.json` file. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | logo 3 |

4 |

AutoDev Editor(formerly Studio B3)

5 | 6 |

7 | 8 | Deploy 9 | 10 | 11 | npm 12 | 13 |

14 | 15 | Chinese version: [中文版](./README.zh.md) 16 | 17 | > AutoDev Editor(formerly Studio B3) (B-3 Bomber) is a sophisticated editor designed for content creation, catering to various formats such as 18 | > blogs, articles, user stories, and more. 19 | 20 | Mission: Our primary goal is to create an editor similar to [AutoDev](https://github.com/unit-mesh/auto-dev). 21 | Additionally, we aim to share insights from the article 22 | titled [Why Chatbots Are Not the Future](https://wattenberger.com/thoughts/boo-chatbots). Our vision includes delivering 23 | a writing experience akin to [Copilot for Docs](https://githubnext.com/projects/copilot-for-docs/) in documentation. 24 | 25 | About name: In the documentary "10 Years with Hayao Miyazaki" the esteemed artist (宫崎骏, 宮﨑駿/みやざきはやお) 26 | chooses a 3B 27 | pencil, deeming conventional ones too inflexible for his creative process. Let us pay homage to his lofty ideals. 28 | 29 |

30 | architecture diagram 31 |

32 | 33 | Roadmap: see [Roadmap](https://github.com/unit-mesh/3b/issues/1) 34 | 35 | Online Demo: [https://editor.unitmesh.cc/](https://editor.unitmesh.cc/) 36 | 37 | Demo Videos: [开源 AI 原生编辑器 AutoDev Editor(formerly Studio B3)](https://www.bilibili.com/video/BV1E64y1j7hJ/) 38 | 39 | ## Quick Start 40 | 41 | See in [web/core](web/core/README.md) 42 | 43 | ## Features 44 | 45 | - Immersive generation. Provides an immersive content generation experience, supporting various formats to allow users 46 | to create content comprehensively. 47 | - Local AI capability. Integration of local AI capabilities, such as semantic search, to enhance the editor's 48 | intelligent search and recommendation functions. 49 | - Custom action. Allowing users to define variables and other elements for more flexible and tailored content 50 | generation. 51 | - Full lifecycle AI. Including interactive tools like the Bubble Menu, Slash Command, Quick Insert, to enhance user 52 | experience in editing, searching, and navigation. 53 | 54 | ## Design Principle 55 | 56 | - **Intelligent Embedding**: Integrate artificial intelligence deeply with the user interface, ensuring that AI models 57 | are cleverly introduced at various positions in the editor to achieve a more intuitive and intelligent user 58 | interaction experience. 59 | - **Local Optimization**: Pursue an efficient and smooth writing experience by introducing local inference models, which 60 | operate within the user's local environment. This includes localized enhancements such as semantic search, local 61 | syntax checking, text prediction, etc. 62 | - **Context Flexibility**: Introduce a context API, providing users with custom prompts and predefined contexts, 63 | allowing for more flexible shaping of the editing environment. Through flexible context management, users gain better 64 | control over AI-generated content. 65 | 66 | ### [Facets as Composable Extension Points](https://marijnhaverbeke.nl/blog/facets.html) 67 | 68 | * Composition: Multiple extensions attaching to a given extension point must have their effects combined in a 69 | predictable way. 70 | * Precedence: In cases where combining effects is order-sensitive, it must be easy to reason about and control the order 71 | of the extensions. 72 | * Grouping: Many extensions will need to attach to a number of extension points, or even pull in other extensions that 73 | they depend on. 74 | * Change: The effect produced by extensions may depend on other aspects of the system state, or be explicitly 75 | reconfigured. 76 | 77 | ## Refs 78 | 79 | ### Tiptap Editor extensions 80 | 81 | App: 82 | 83 | - [Gitlab](https://gitlab.com/gitlab-org/gitlab/-/tree/master/app/assets/javascripts/content_editor/extensions) 84 | 85 | Editor: 86 | 87 | - [https://github.com/fantasticit/magic-editor](https://github.com/fantasticit/magic-editor) 88 | - [Think Editor's Tiptap extensions](https://github.com/fantasticit/think/tree/main/packages/client/src/tiptap/core/extensions) 89 | 90 | Similar project: 91 | 92 | - [JetBrains Grazie](https://lp.jetbrains.com/grazie-for-software-teams/) 93 | 94 | ## License 95 | 96 | TrackChange based on: [TrackChangeExtension](https://github.com/chenyuncai/tiptap-track-change-extension) 97 | 98 | This code is distributed under the MIT license. See `LICENSE` in this directory. 99 | -------------------------------------------------------------------------------- /README.zh.md: -------------------------------------------------------------------------------- 1 |

2 | logo 3 |

4 |

AutoDev Editor(formerly Studio B3)

5 | 6 |

7 | 8 | Deploy 9 | 10 | 11 | npm 12 | 13 | 14 | GitHub release (with filter) 15 | 16 |

17 | 18 | > AutoDev Editor(formerly Studio B3) 是一个为内容创作设计的高级编辑器,适用于各种格式,如博客、文章、用户故事等。 19 | 20 | 在纪录片《宫崎骏:十载同行》中,这位备受尊敬的艺术家宫崎骏选择了一支 3B 铅笔,认为传统的铅笔在他的创作过程中过于僵硬。让我们向他的崇高理念致敬。 21 | 22 | 待办事项:查看 [路线图](https://github.com/unit-mesh/3b/issues/1) 23 | 24 | 在线演示:[https://editor.unitmesh.cc/](https://editor.unitmesh.cc/) 25 | 26 | 演示视频: [开源 AI 原生编辑器 AutoDev Editor(formerly Studio B3)](https://www.bilibili.com/video/BV1E64y1j7hJ/) 27 | 28 | ## 特点 29 | 30 | - 沉浸式生成。提供沉浸式的内容生成体验,支持各种格式,使用户能够全面创作内容。 31 | - 本地AI能力。集成本地AI能力,如语义搜索,以增强编辑器的智能搜索和推荐功能。 32 | - 自定义操作。允许用户定义变量和其他元素,实现更灵活、定制化的内容生成。 33 | - 完整生命周期的AI。包括Bubble菜单、Slash命令、Quick Insert等交互工具,以增强用户在编辑、搜索和导航中的体验。 34 | 35 | ## 设计原则 36 | 37 | - **智能嵌入:** 将人工智能与用户界面深度融合,确保在编辑器的各个界面位置巧妙地引入AI模型,以实现更直观、智能的用户交互体验。 38 | - **本地优化:** 通过引入本地推理模型,追求在用户本地环境下提供高效、流畅的写作体验。这包括语义搜索、本地语法检查、文本预测等本地化增强功能。 39 | - **上下文灵活性:** 引入上下文API,为用户提供自定义提示和预定义上下文等工具,以实现对编辑环境更灵活的塑造。通过灵活的上下文管理,用户能够更好地掌控AI生成的内容。 40 | 41 | 中文版:[中文版](./README.zh.md) 42 | 43 | ### [可组合扩展点的外观](https://marijnhaverbeke.nl/blog/facets.html) 44 | 45 | * 组合:附加到给定扩展点的多个扩展必须以可预测的方式结合其效果。 46 | * 优先级:在组合效果与顺序敏感的情况下,必须轻松理解和控制扩展的顺序。 47 | * 分组:许多扩展将需要附加到多个扩展点,甚至拉入它们依赖的其他扩展。 48 | * 变更:扩展产生的效果可能取决于系统状态的其他方面,或者被明确地重新配置。 49 | 50 | ## 用法 51 | 52 | ### 自定义菜单示例 53 | 54 | ```typescript 55 | const BubbleMenu: PromptAction[] = [ 56 | { 57 | name: 'Polish', 58 | i18Name: true, 59 | template: `You are an assistant helping to polish sentence. Output in markdown format. \n ###${DefinedVariable.SELECTION}###`, 60 | facetType: FacetType.BUBBLE_MENU, 61 | outputForm: OutputForm.STREAMING, 62 | }, 63 | { 64 | name: 'Similar Chunk', 65 | i18Name: true, 66 | template: `You are an assistant helping to find similar content. Output in markdown format. \n ###${DefinedVariable.SELECTION}###`, 67 | facetType: FacetType.BUBBLE_MENU, 68 | outputForm: OutputForm.STREAMING, 69 | }, 70 | { 71 | name: 'Simplify Content', 72 | i18Name: true, 73 | template: `You are an assistant helping to simplify content. Output in markdown format. \n ###${DefinedVariable.SELECTION}###`, 74 | facetType: FacetType.BUBBLE_MENU, 75 | outputForm: OutputForm.STREAMING, 76 | changeForm: ChangeForm.DIFF, 77 | }, 78 | ]; 79 | ``` 80 | 81 | ## 引用: 82 | 83 | ### Tiptap编辑器扩展 84 | 85 | App: 86 | 87 | - [Gitlab](https://gitlab.com/gitlab-org/gitlab/-/tree/master/app/assets/javascripts/content_editor/extensions) 88 | 89 | Editor: 90 | 91 | - [https://github.com/fantasticit/magic-editor](https://github.com/fantasticit/magic-editor) 92 | - [Think Editor's Tiptap extensions](https://github.com/fantasticit/think/tree/main/packages/client/src/tiptap/core/extensions) 93 | 94 | ## 许可证 95 | 96 | 基于TrackChange:[TrackChangeExtension](https://github.com/chenyuncai/tiptap-track-change-extension) 97 | 98 | 本代码采用MIT许可证分发。请参阅此目录中的`LICENSE`。 99 | -------------------------------------------------------------------------------- /docs/Motivation.mdx: -------------------------------------------------------------------------------- 1 | # Motivation 2 | 3 | Our goal is to create an `Immersive Creative Writing Experience`, inspired by the exploration conducted 4 | in the realms of AutoDev and IDE. 5 | 6 | We are building an AI-native text editor to delve into the world of immersive creative expression. 7 | This editor is tailored for various document scenarios such as requirements writing, architectural documentation, 8 | and more, with the aim of accelerating the daily work of diverse roles in software development. 9 | 10 | -------------------------------------------------------------------------------- /docs/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | B 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /docs/test-doc.md: -------------------------------------------------------------------------------- 1 | # 需求 2 | 3 | 4 | > 获取同业头条资讯 5 | 6 | ``` 7 | ### 获取财经头条资讯### 8 | 9 | **1. 产品简介** 10 | 11 | 本产品是一个面向金融行业从业人员的新闻资讯获取应用。主要目标是提供实时、精准的同业头条新闻,帮助用户更好地了解行业动态,把握市场趋势。 12 | 13 | **2. 需求流程图** 14 | 15 | 以下是我们产品的核心功能流程: 16 | 17 | ![获取同业头条资讯流程图] 18 | 19 | **3. 数据项描述** 20 | 21 | 我们的产品需要从各种来源获取新闻数据,包括但不限于: 22 | 23 | * 各大金融资讯网站 24 | * 社交媒体平台(如微博、微信) 25 | * 专业金融博客和论坛 26 | * 直接从相关公司获取的官方公告和新闻 27 | 28 | 以下是数据项的详细描述: 29 | 30 | | 数据项 | 描述 | 示例 | 31 | | --- | --- | --- | 32 | | 标题 | 新闻标题是数据项中的主要元素,需要简洁、明了地概括新闻内容。 | “某银行发布2023年第一季度财报” | 33 | | 发布时间 | 记录新闻的发布时间,帮助用户判断新闻的新鲜程度。 | “2023-04-25” | 34 | | 来源 | 记录新闻的来源,帮助用户判断新闻的可信度。 | “某金融资讯网” | 35 | | 内容 | 新闻的详细内容,需要能够全面、准确地传达新闻信息。 | “某银行2023年第一季度净利润增长10%” | 36 | | 分类 | 对新闻进行分类,帮助用户更好地理解和筛选新闻。 | “金融市场、银行业” | 37 | | 关键词 | 对新闻内容的关键词进行提取,帮助用户快速了解新闻主题。 | “净利润、增长” | 38 | 39 | **4. 验收条件** 40 | 41 | 以下是我们的产品验收条件: 42 | 43 | * 用户界面需要简洁明了,易于操作。 44 | * 产品需要能够实时获取并更新同业头条新闻。 45 | * 产品需要能够对获取的新闻进行智能分类和关键词提取。 46 | * 产品需要能够根据用户的阅读习惯和偏好推荐相关新闻。 47 | ``` 48 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import { getJestProjects } from '@nx/jest'; 2 | 3 | export default { 4 | projects: getJestProjects(), 5 | }; 6 | -------------------------------------------------------------------------------- /jest.preset.js: -------------------------------------------------------------------------------- 1 | const nxPreset = require('@nx/jest/preset').default; 2 | 3 | module.exports = { ...nxPreset }; 4 | -------------------------------------------------------------------------------- /local_server/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | bin/ 5 | pkg/ 6 | wasm-pack.log 7 | .idea 8 | !bin/*.rs 9 | *.db 10 | testdocs 11 | -------------------------------------------------------------------------------- /local_server/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "b3_server" 3 | version = "0.2.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | actix-web = "4" 10 | actix-cors = "0.6.4" 11 | #tokio = { version = "1.34.0", features = ["macros", "process", "rt", "rt-multi-thread", "io-std", "io-util", "sync", "fs"] } 12 | 13 | uuid = "1.6.1" 14 | 15 | # core 16 | flume = "0.11.0" 17 | ignore = "=0.4.20" 18 | 19 | anyhow = "1.0.75" 20 | 21 | # misc 22 | serde = { version = "1.0", features = ["derive"] } 23 | serde_json = "1.0.108" 24 | 25 | # embed 26 | enfer_core = "0.1.2" 27 | 28 | # code-nav 29 | tree-sitter = "0.20.10" 30 | tree-sitter-javascript = "0.20.1" 31 | tree-sitter-python = "=0.20.2" 32 | tree-sitter-rust = "0.20.4" 33 | tree-sitter-typescript = "0.20.2" 34 | tree-sitter-java = { git = "https://github.com/tree-sitter/tree-sitter-java", tag = "v0.20.0" } 35 | 36 | # the documentation splitter 37 | tree-sitter-md = "0.1.7" 38 | # chunks spliting 39 | unicode-segmentation = "1.10.1" 40 | 41 | # doc scraper 42 | select = "0.6" 43 | url = "2.5.0" 44 | reqwest = { version = "0.11.20", features = ["rustls-tls-webpki-roots", "cookies", "gzip"], default-features = false } 45 | reqwest-eventsource = "0.5.0" 46 | 47 | # document splitter 48 | docx-rs = "0.4.7" 49 | 50 | # storage 51 | #sqlx = { version = "0.7.3", features = ["sqlite", "migrate", "runtime-tokio-rustls", "chrono", "uuid"] } 52 | # search engine Tantivy? 53 | tantivy = { version = "0.22.0", features = ["mmap"] } 54 | # Chinese support 55 | tantivy-jieba = "0.10.0" 56 | tantivy-columnar = "0.3.0" 57 | 58 | tracing = "0.1.37" 59 | tracing-subscriber = { version = "0.3.17", features = ["env-filter", "registry"] } 60 | tracing-appender = "0.2.2" 61 | 62 | # watch dir 63 | notify-debouncer-mini = { version = "0.4.1", default-features = false } 64 | 65 | #misc 66 | directories = "5.0.1" 67 | -------------------------------------------------------------------------------- /local_server/README.md: -------------------------------------------------------------------------------- 1 | # Local Document Server 2 | 3 | - [ ] Watch local directory for document changes 4 | - [x] use Local directory 5 | - [ ] Embedding the document, code 6 | - [ ] Markdown Documentation Splitter: [TreeSitter-markdown](https://github.com/MDeiml/tree-sitter-markdown) 7 | - [x] Office, like Word Document Splitter: [docx-rs](https://github.com/bokuweb/docx-rs) 8 | - [x] reader 9 | spike: [Document File Text Extractor](https://github.com/anvie/dotext), [docx](https://github.com/PoiScript/docx-rs), [OOXML](https://github.com/zitsen/ooxml-rs), 10 | - [ ] Code Splitter: TreeSitter, 11 | like [Bloop](https://github.com/BloopAI/bloop/tree/main/server/bleep/src/intelligence/language) 12 | - [ ] Web Scrapper 13 | - [ ] Extract text from web page, 14 | like: [scraper](https://github.com/BloopAI/bloop/tree/main/server/bleep/src/scraper) 15 | - [ ] Document version control 16 | - [x] Vector Search: InMemory 17 | - [FANN](https://github.com/fennel-ai/fann) - [FANN: Vector Search in 200 Lines of Rust](https://fennel.ai/blog/vector-search-in-200-lines-of-rust/) 18 | - [tinyvector](https://github.com/m1guelpf/tinyvector) 19 | - [x] Search document by semantic 20 | - [ ] Embedding Search engine by [tantivy](https://github.com/quickwit-oss/tantivy) 21 | 22 | ## HTTP API design 23 | 24 | ### Embedding Document/Code CRUD 25 | 26 | CREATE: `POST /api/embedding-document` 27 | 28 | ```json 29 | { 30 | "name": "README.md", 31 | "uri": "file:///path/to/README.md", 32 | "type": "markdown", 33 | "content": "..." 34 | } 35 | ``` 36 | 37 | READ: `GET /api/embedding-document/:id` 38 | 39 | ```json 40 | { 41 | "id": "xxx-xxxx-xxx", 42 | "uri": "http://localhost:8080/api/embedding-document/xxx-xxxx-xxx", 43 | "name": "README.md", 44 | "content": "...", 45 | "type": "markdown", 46 | "chunks": [ 47 | { 48 | "id": "xxx-xxxx-xxx", 49 | "text": "...", 50 | "embedding": "..." 51 | }, 52 | { 53 | "id": "xxx-xxxx-xxx", 54 | "text": "...", 55 | "embedding": "..." 56 | } 57 | ] 58 | } 59 | ``` 60 | 61 | SEARCH: `GET /api/embedding-document/search?q=...` 62 | 63 | ```json 64 | { 65 | "results": [ 66 | { 67 | "id": "xxx-xxxx-xxx", 68 | "name": "README.md", 69 | "uri": "http://localhost:8080/api/embedding-document/xxx-xxxx-xxx", 70 | "content": "...", 71 | "type": "markdown", 72 | "chunks": [ 73 | { 74 | "id": "xxx-xxxx-xxx", 75 | "text": "...", 76 | "embedding": "..." 77 | }, 78 | { 79 | "id": "xxx-xxxx-xxx", 80 | "text": "...", 81 | "embedding": "..." 82 | } 83 | ] 84 | } 85 | ] 86 | } 87 | ``` 88 | 89 | UPDATE: `PUT /api/embedding-document/:id` 90 | 91 | ```json 92 | { 93 | "name": "README.md", 94 | "uri": "file:///path/to/README.md", 95 | "content": "..." 96 | } 97 | ``` 98 | 99 | DELETE: `DELETE /api/embedding-document/:id` 100 | 101 | ### Web Scrapper 102 | 103 | CREATE: `POST /api/web-scrapper` 104 | 105 | ```json 106 | { 107 | "url": "https://www.example.com" 108 | } 109 | ``` 110 | 111 | returns `embedding-document` object 112 | 113 | DELETE: `DELETE /api/web-scrapper/:id` 114 | 115 | REFRESH: `POST /api/web-scrapper/:id/refresh` 116 | -------------------------------------------------------------------------------- /local_server/_fixtures/header.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unit-mesh/autodev-editor/d6b82047072dd7344e6378a22c280bd374bff9ab/local_server/_fixtures/header.docx -------------------------------------------------------------------------------- /local_server/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "local_server", 3 | "$schema": "../.nx/installation/node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "local-server/src", 5 | "targets": { 6 | "build": { 7 | "executor": "nx:run-commands", 8 | "options": { 9 | "cwd": "local_server", 10 | "commands": [ 11 | "cargo build --release" 12 | ] 13 | } 14 | }, 15 | "test": { 16 | "executor": "nx:run-commands", 17 | "options": { 18 | "cwd": "local_server", 19 | "commands": [ 20 | "cargo test" 21 | ] 22 | } 23 | }, 24 | "lint": { 25 | "executor": "nx:run-commands", 26 | "options": { 27 | "cwd": "local_server", 28 | "commands": [ 29 | "cargo lint" 30 | ] 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /local_server/src/app_state.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use inference_core::{InMemoryEmbeddingStore, Semantic}; 4 | use crate::doc_indexes::doc_indexes::DocIndexes; 5 | 6 | pub struct AppState { 7 | pub semantic: Arc, 8 | pub storage: Arc, 9 | pub indexes: Arc 10 | } 11 | -------------------------------------------------------------------------------- /local_server/src/doc_indexes/doc_indexes.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::path::{Path, PathBuf}; 3 | 4 | use anyhow::Context; 5 | use tantivy::schema::Schema; 6 | 7 | use crate::doc_indexes::doc_schema::DocumentFile; 8 | 9 | pub struct DocIndexes { 10 | pub doc: tantivy::Index, 11 | } 12 | 13 | impl DocIndexes { 14 | pub(crate) fn new() -> Self { 15 | let threads = std::thread::available_parallelism().unwrap().get(); 16 | let index_dir = default_index_dir(); 17 | 18 | let path = index_dir.join("doc"); 19 | let index = Self::init_index(DocumentFile::new().schema, path.as_ref(), threads).unwrap(); 20 | 21 | Self { 22 | doc: index 23 | } 24 | } 25 | 26 | fn init_index(schema: Schema, path: &Path, threads: usize) -> anyhow::Result { 27 | fs::create_dir_all(path).context("failed to create index dir")?; 28 | 29 | let mut index = 30 | tantivy::Index::open_or_create(tantivy::directory::MmapDirectory::open(path)?, schema)?; 31 | let tokenizer = tantivy_jieba::JiebaTokenizer {}; 32 | 33 | index.set_multithread_executor(threads)?; 34 | index 35 | .tokenizers() 36 | .register("jieba", tokenizer); 37 | 38 | Ok(index) 39 | } 40 | } 41 | 42 | /// Returns the default index directory for the current platform. 43 | /// 44 | /// | Platform | Default index directory | 45 | /// | --- | --- | 46 | /// | macOS | ~/Library/Application Support/org.unitmesh.b3 | 47 | /// | Linux | ~/.config/b3 | 48 | /// | Windows | C:\Users\\AppData\Roaming\unitmesh\b3\config | 49 | /// 50 | fn default_index_dir() -> PathBuf { 51 | match directories::ProjectDirs::from("org", "unitmesh", "b3") { 52 | Some(dirs) => dirs.data_dir().to_owned(), 53 | None => "b3_index".into(), 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /local_server/src/doc_indexes/doc_reader.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | use crate::doc_split::document_type::DocumentType; 3 | use crate::doc_split::office_splitter::OfficeSplitter; 4 | 5 | pub struct DocReader {} 6 | 7 | impl DocReader { 8 | pub fn read(path: &PathBuf) -> Result { 9 | let extension = path.extension() 10 | .ok_or(anyhow::anyhow!("No extension found for file: {:?}", path))? 11 | .to_str() 12 | .ok_or(anyhow::anyhow!("Failed to convert extension to string: {:?}", path))?; 13 | 14 | let document_type = match DocumentType::of(extension) { 15 | None => return Ok(String::new()), 16 | Some(doc) => doc 17 | }; 18 | 19 | return match document_type { 20 | DocumentType::MD | DocumentType::TXT => { 21 | std::fs::read_to_string(path).map_err(|e| e.into()) 22 | } 23 | DocumentType::PDF => { 24 | Ok("".to_string()) 25 | } 26 | DocumentType::HTML => { 27 | Ok("".to_string()) 28 | } 29 | DocumentType::DOC | DocumentType::XLS | DocumentType::PPT => { 30 | OfficeSplitter::read(path) 31 | } 32 | }; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /local_server/src/doc_indexes/doc_schema.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use tantivy::doc; 4 | use tantivy::schema::{Field, Schema, SchemaBuilder}; 5 | 6 | #[derive(Clone)] 7 | pub struct DocumentFile { 8 | pub(super) schema: Schema, 9 | pub unique_hash: Field, 10 | /// Path to the root of the repo on disk 11 | pub repo_disk_path: Field, 12 | /// Path to the file, relative to the repo root 13 | pub relative_path: Field, 14 | pub title: Field, 15 | pub content: Field, 16 | } 17 | 18 | impl Default for DocumentFile { 19 | fn default() -> Self { 20 | Self::new() 21 | } 22 | } 23 | 24 | impl DocumentFile { 25 | pub fn new() -> Self { 26 | let mut builder = SchemaBuilder::new(); 27 | let unique_hash = builder.add_text_field("unique_hash", tantivy::schema::TEXT | tantivy::schema::STORED); 28 | 29 | let repo_disk_path = builder.add_text_field("repo_disk_path", tantivy::schema::TEXT | tantivy::schema::STORED); 30 | let relative_path = builder.add_text_field("relative_path", tantivy::schema::TEXT | tantivy::schema::STORED); 31 | 32 | let title = builder.add_text_field("title", tantivy::schema::TEXT | tantivy::schema::STORED); 33 | let content = builder.add_text_field("content", tantivy::schema::TEXT | tantivy::schema::STORED); 34 | 35 | Self { 36 | unique_hash, 37 | repo_disk_path, 38 | relative_path, 39 | title, 40 | content, 41 | schema: builder.build(), 42 | } 43 | } 44 | 45 | pub fn build_document(&self, path: &PathBuf) -> Option { 46 | // todo: read file content by types 47 | None 48 | } 49 | } -------------------------------------------------------------------------------- /local_server/src/doc_indexes/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod doc_schema; 2 | pub mod doc_indexes; 3 | pub mod doc_reader; -------------------------------------------------------------------------------- /local_server/src/doc_split/document_type.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, PartialEq)] 2 | pub enum DocumentType { 3 | TXT, 4 | PDF, 5 | HTML, 6 | DOC, 7 | XLS, 8 | MD, 9 | PPT, 10 | } 11 | 12 | impl DocumentType { 13 | fn supported_extensions(&self) -> Vec<&'static str> { 14 | match self { 15 | DocumentType::TXT => vec!["txt"], 16 | DocumentType::PDF => vec!["pdf"], 17 | DocumentType::HTML => vec!["html", "htm", "xhtml"], 18 | DocumentType::DOC => vec!["doc", "docx"], 19 | DocumentType::XLS => vec!["xls", "xlsx"], 20 | DocumentType::MD => vec!["md", "markdown"], 21 | DocumentType::PPT => vec!["ppt", "pptx"], 22 | } 23 | } 24 | 25 | pub fn of(file_name: &str) -> Option { 26 | for document_type in [ 27 | DocumentType::TXT, 28 | DocumentType::PDF, 29 | DocumentType::HTML, 30 | DocumentType::DOC, 31 | DocumentType::XLS, 32 | DocumentType::MD, 33 | DocumentType::PPT, 34 | ] { 35 | for supported_extension in document_type.supported_extensions() { 36 | if file_name.ends_with(supported_extension) { 37 | return Some(document_type); 38 | } 39 | } 40 | } 41 | 42 | None 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /local_server/src/doc_split/html_splitter.rs: -------------------------------------------------------------------------------- 1 | use inference_core::Document; 2 | 3 | use crate::doc_split::splitter::{SplitOptions, TextSplitter}; 4 | 5 | pub struct HtmlSplitter {} 6 | 7 | impl TextSplitter for HtmlSplitter { 8 | fn split(text: &str, options: &SplitOptions) -> Vec { 9 | let document = Document::from(text.to_string()); 10 | 11 | // todo: implement it 12 | 13 | let mut documents: Vec = vec![]; 14 | 15 | documents 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /local_server/src/doc_split/markdown_splitter.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use inference_core::Document; 4 | 5 | use crate::doc_split::splitter::{SplitOptions, Splitter}; 6 | 7 | pub struct MarkdownSplitter {} 8 | 9 | impl Splitter for MarkdownSplitter { 10 | fn split(path: &PathBuf, options: &SplitOptions) -> Vec { 11 | println!("MarkdownSplitter::split() not implemented yet"); 12 | vec![] 13 | } 14 | } -------------------------------------------------------------------------------- /local_server/src/doc_split/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod document_type; 2 | pub mod split; 3 | pub mod office_splitter; 4 | pub mod pdf_splitter; 5 | pub mod markdown_splitter; 6 | pub mod splitter; 7 | pub mod html_splitter; 8 | -------------------------------------------------------------------------------- /local_server/src/doc_split/office_splitter.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::fs::File; 3 | use std::io::Read; 4 | use std::path::PathBuf; 5 | use std::ptr; 6 | 7 | use docx_rs::{DocumentChild, ParagraphChild, read_docx, RunChild}; 8 | use inference_core::{Document, Metadata}; 9 | use tracing::error; 10 | use unicode_segmentation::UnicodeSegmentation; 11 | 12 | use crate::doc_split::splitter::{SplitOptions, Splitter}; 13 | 14 | pub struct OfficeSplitter {} 15 | 16 | impl Splitter for OfficeSplitter { 17 | fn split(path: &PathBuf, options: &SplitOptions) -> Vec { 18 | let mut documents: Vec = vec![]; 19 | let document = Self::read(path).expect("docx_to_markdown error"); 20 | let pure_file_name = path.file_stem().unwrap().to_str().unwrap(); 21 | let mut map = HashMap::new(); 22 | map.insert("file_name".to_string(), pure_file_name.to_string()); 23 | map.insert("file_path".to_string(), path.to_str().unwrap().to_string()); 24 | 25 | let metadata: Metadata = Metadata { 26 | metadata: map, 27 | }; 28 | 29 | let buf_size = options.chunk_size * 4; 30 | let mut buffer = String::with_capacity(buf_size); 31 | for word in document.split_sentence_bounds() { 32 | if buffer.len() + word.len() <= buf_size { 33 | buffer.push_str(word); 34 | } else { 35 | documents.push(Document::from_with_metadata(buffer.clone(), metadata.clone())); 36 | for i in buffer.len() .. buf_size { 37 | unsafe{ ptr::write(buffer.as_mut_ptr().add(i), 0x20); }; 38 | } 39 | buffer.clear(); 40 | } 41 | } 42 | 43 | documents 44 | } 45 | } 46 | 47 | impl OfficeSplitter { 48 | pub(crate) fn read(path: &PathBuf) -> Result { 49 | let mut file = File::open(path)?; 50 | 51 | let mut buf = vec![]; 52 | file.read_to_end(&mut buf)?; 53 | 54 | let mut text = String::new(); 55 | 56 | match read_docx(&*buf) { 57 | Ok(content) => { 58 | content.document.children.iter().for_each(|child| { 59 | match child { 60 | DocumentChild::Paragraph(para) => { 61 | let heading = match ¶.property.style { 62 | None => "", 63 | Some(style) => { 64 | match style.val.as_str() { 65 | "Heading1" => "# ", 66 | "Heading2" => "## ", 67 | "Heading3" => "### ", 68 | "Heading4" => "#### ", 69 | "Heading5" => "##### ", 70 | "Heading6" => "###### ", 71 | _ => "" 72 | } 73 | } 74 | }; 75 | 76 | let mut para_text = String::new(); 77 | para.children.iter().for_each(|child| { 78 | match child { 79 | ParagraphChild::Run(run) => { 80 | para_text += &run.children.iter().map(|child| { 81 | match child { 82 | RunChild::Text(text) => text.text.clone(), 83 | _ => String::new(), 84 | } 85 | }).collect::(); 86 | } 87 | _ => {} 88 | } 89 | }); 90 | 91 | text = format!("{}{}{}\n", text, heading, para_text); 92 | } 93 | _ => {} 94 | } 95 | }); 96 | } 97 | Err(err) => { 98 | error!("read_docx error: {:?}", err); 99 | } 100 | } 101 | 102 | Ok(text) 103 | } 104 | } 105 | 106 | #[cfg(test)] 107 | mod tests { 108 | use std::path::PathBuf; 109 | 110 | use crate::doc_split::office_splitter::OfficeSplitter; 111 | use crate::infra::file_walker::FileWalker; 112 | 113 | #[test] 114 | fn test_word_splitter() { 115 | let testdir = PathBuf::from("_fixtures").join("header.docx"); 116 | let files = FileWalker::index_directory(testdir); 117 | 118 | let file = files.first().unwrap(); 119 | let documents = OfficeSplitter::read(file); 120 | 121 | assert_eq!(documents, "# Heading 1 122 | 123 | ## Heading 2 124 | 125 | Normal Context 126 | 127 | 128 | "); 129 | } 130 | } -------------------------------------------------------------------------------- /local_server/src/doc_split/pdf_splitter.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use inference_core::Document; 4 | 5 | use crate::doc_split::splitter::{SplitOptions, Splitter}; 6 | 7 | pub struct PdfSplitter {} 8 | 9 | impl Splitter for PdfSplitter { 10 | fn split(path: &PathBuf, options: &SplitOptions) -> Vec { 11 | println!("PdfSplitter::split() not implemented yet"); 12 | vec![] 13 | } 14 | } -------------------------------------------------------------------------------- /local_server/src/doc_split/split.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use inference_core::Document; 4 | 5 | use crate::doc_split::document_type::DocumentType; 6 | use crate::doc_split::splitter::{SplitOptions, Splitter}; 7 | use crate::doc_split::office_splitter::OfficeSplitter; 8 | 9 | /** 10 | * Split a document into multiple documents which will according to the document type. 11 | */ 12 | pub fn split(path: &PathBuf, options: &SplitOptions) -> Option> { 13 | let path_buf = path.clone(); 14 | 15 | let filename = path_buf.file_name()?.to_str()?; 16 | let document_type = DocumentType::of(filename)?; 17 | 18 | let documents = match document_type { 19 | DocumentType::TXT => vec![], 20 | DocumentType::PDF => vec![], 21 | DocumentType::HTML => vec![], 22 | DocumentType::MD => vec![], 23 | DocumentType::DOC => OfficeSplitter::split(path, options), 24 | DocumentType::XLS => OfficeSplitter::split(path, options), 25 | DocumentType::PPT => OfficeSplitter::split(path, options), 26 | }; 27 | 28 | Some(documents) 29 | } 30 | 31 | #[cfg(test)] 32 | mod tests { 33 | use std::path::PathBuf; 34 | 35 | use crate::doc_split::split::split; 36 | use crate::doc_split::splitter::SplitOptions; 37 | use crate::infra::file_walker::FileWalker; 38 | 39 | #[test] 40 | fn test_doc_splitter() { 41 | let testdir = PathBuf::from("testdocs"); 42 | let files = FileWalker::index_directory(testdir); 43 | 44 | let options = SplitOptions::default(); 45 | for file in files { 46 | split(&file, &options); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /local_server/src/doc_split/splitter.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | use inference_core::Document; 3 | 4 | pub trait Splitter { 5 | /** 6 | * Split a document into multiple documents by chunk size. 7 | */ 8 | fn split(path: &PathBuf, options: &SplitOptions) -> Vec; 9 | } 10 | 11 | pub trait TextSplitter { 12 | /** 13 | * Split a document into multiple documents by chunk size. 14 | */ 15 | fn split(text: &str, options: &SplitOptions) -> Vec; 16 | } 17 | 18 | pub struct SplitOptions { 19 | pub chunk_size: usize, 20 | } 21 | 22 | impl Default for SplitOptions { 23 | fn default() -> Self { 24 | SplitOptions { 25 | chunk_size: 256, 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /local_server/src/document_handler.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use actix_web::{get, HttpResponse, post, Responder, web}; 3 | use actix_web::http::header::ContentType; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | use crate::app_state::AppState; 7 | 8 | #[post("/api/embedding-document")] 9 | async fn create_embedding_document( 10 | form: web::Form, 11 | _data: web::Data, 12 | ) -> impl Responder { 13 | let response = serde_json::to_string(&form).unwrap(); 14 | 15 | HttpResponse::Created() 16 | .content_type(ContentType::json()) 17 | .body(response) 18 | } 19 | 20 | #[get("/api/embedding-document/search")] 21 | async fn search_embedding_document( 22 | query: web::Query, 23 | data: web::Data, 24 | ) -> impl Responder { 25 | let embedding = data.semantic.embed(&query.q).unwrap(); 26 | let document_match = data.storage.find_relevant(embedding, 5, 0.0); 27 | 28 | let documents: Vec = document_match 29 | .into_iter() 30 | .map(|doc| DocumentResult { 31 | id: doc.embedding_id, 32 | score: doc.score, 33 | text: doc.embedded.text, 34 | metadata: doc.embedded.metadata.metadata, 35 | }) 36 | .collect(); 37 | 38 | let response = serde_json::to_string(&documents).unwrap(); 39 | 40 | HttpResponse::Ok() 41 | .content_type(ContentType::json()) 42 | .body(response) 43 | } 44 | 45 | #[derive(Serialize, Deserialize)] 46 | pub struct DocumentResult { 47 | pub id: String, 48 | pub score: f32, 49 | pub text: String, 50 | pub metadata: HashMap, 51 | } 52 | 53 | #[derive(Serialize, Deserialize)] 54 | pub struct ReqDocument { 55 | pub name: String, 56 | pub uri: String, 57 | #[serde(rename = "type")] 58 | pub doc_type: String, 59 | pub content: String, 60 | } 61 | 62 | #[derive(Serialize, Deserialize)] 63 | pub struct SearchQuery { 64 | pub q: String, 65 | } 66 | -------------------------------------------------------------------------------- /local_server/src/infra/dir_watcher.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | ops::Not, 3 | time::Duration, 4 | }; 5 | use std::path::PathBuf; 6 | 7 | use flume::Sender; 8 | use notify_debouncer_mini::{Config, DebounceEventResult, Debouncer, new_debouncer_opt, notify, notify::RecommendedWatcher}; 9 | use notify_debouncer_mini::notify::RecursiveMode; 10 | use tracing::{debug, error, warn}; 11 | 12 | fn watch_dir(tx: Sender<()>, disk_path: PathBuf) -> Option> { 13 | let mut debouncer = debounced_events(tx); 14 | debouncer 15 | .watcher() 16 | .watch(&disk_path, RecursiveMode::Recursive) 17 | .map_err(|e| { 18 | let d = disk_path.display(); 19 | warn!(error = %e, path = %d, "path does not exist anymore"); 20 | }) 21 | .ok()?; 22 | 23 | return Some(debouncer); 24 | } 25 | 26 | fn debounced_events(tx: flume::Sender<()>) -> Debouncer { 27 | let notify_config = notify::Config::default().with_compare_contents(true); 28 | 29 | let config = Config::default() 30 | .with_timeout(Duration::from_secs(5)) 31 | .with_notify_config(notify_config) 32 | ; 33 | 34 | new_debouncer_opt(config, move |event: DebounceEventResult| match event { 35 | Ok(events) if events.is_empty().not() => { 36 | if let Err(e) = tx.send(()) { 37 | error!("{e}"); 38 | } 39 | } 40 | Ok(_) => debug!("no events received from debouncer"), 41 | Err(err) => { 42 | error!(?err, "repository monitoring"); 43 | } 44 | }).unwrap() 45 | } 46 | -------------------------------------------------------------------------------- /local_server/src/infra/file_walker.rs: -------------------------------------------------------------------------------- 1 | use std::fs::canonicalize; 2 | use std::path::{Path, PathBuf}; 3 | 4 | use tracing::warn; 5 | 6 | pub struct FileWalker { 7 | file_list: Vec, 8 | } 9 | 10 | impl FileWalker { 11 | pub fn index_directory(dir: impl AsRef) -> Vec { 12 | let walker = ignore::WalkBuilder::new(&dir) 13 | .standard_filters(true) 14 | .hidden(false) 15 | .build(); 16 | 17 | let file_list = walker 18 | .filter_map(|de| match de { 19 | Ok(de) => Some(de), 20 | Err(err) => { 21 | warn!(%err, "access failure; skipping"); 22 | None 23 | } 24 | }) 25 | .filter(|de| !de.path().strip_prefix(&dir).unwrap().starts_with(".git")) 26 | .filter_map(|de| canonicalize(de.into_path()).ok()) 27 | .collect(); 28 | 29 | file_list 30 | } 31 | } -------------------------------------------------------------------------------- /local_server/src/infra/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod dir_watcher; 2 | pub mod file_walker; -------------------------------------------------------------------------------- /local_server/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | use std::sync::Arc; 3 | 4 | use actix_cors::Cors; 5 | use actix_web::{App, http, HttpServer, web}; 6 | use actix_web::web::Data; 7 | use inference_core::{Document, init_semantic_with_path, InMemoryEmbeddingStore}; 8 | use uuid::Uuid; 9 | 10 | use app_state::AppState; 11 | use crate::doc_indexes::doc_indexes::DocIndexes; 12 | use crate::doc_indexes::doc_schema::DocumentFile; 13 | 14 | use crate::doc_split::split::split; 15 | use crate::doc_split::splitter::SplitOptions; 16 | use crate::document_handler::{create_embedding_document, search_embedding_document}; 17 | use crate::infra::file_walker::FileWalker; 18 | 19 | pub mod scraper; 20 | mod document_handler; 21 | pub mod app_state; 22 | pub mod infra; 23 | pub mod doc_split; 24 | pub mod doc_indexes; 25 | 26 | #[actix_web::main] 27 | async fn main() -> std::io::Result<()> { 28 | let app_state = create_app_state(); 29 | 30 | HttpServer::new(move || { 31 | App::new() 32 | .wrap(Cors::default() 33 | .allowed_origin("https://editor.unitmesh.cc") 34 | .allowed_origin("http://localhost:3000") 35 | .allowed_methods(vec!["GET", "POST"]) 36 | .allowed_headers(vec![http::header::AUTHORIZATION, http::header::ACCEPT]) 37 | .allowed_header(http::header::CONTENT_TYPE) 38 | .max_age(3600) 39 | ) 40 | .app_data(app_state.clone()) 41 | .service(create_embedding_document) 42 | .service(search_embedding_document) 43 | }) 44 | .bind(("127.0.0.1", 8080))? 45 | .run() 46 | .await 47 | } 48 | 49 | fn create_app_state() -> Data { 50 | let semantic = init_semantic_with_path("model/model.onnx", "model/tokenizer.json") 51 | .expect("Failed to initialize semantic"); 52 | 53 | let dir = PathBuf::from("testdocs"); 54 | let files = FileWalker::index_directory(dir); 55 | 56 | let embedding_store = InMemoryEmbeddingStore::new(); 57 | let options = SplitOptions::default(); 58 | let indexes = DocIndexes::new(); 59 | let mut index_writer = indexes.doc.writer(50_000_000).unwrap(); 60 | 61 | files.iter().for_each(|file| { 62 | // DocumentFile::build_document(&file)?.map(|doc| { 63 | // index_writer.add_document(doc).expect("TODO: panic message"); 64 | // }); 65 | 66 | if let Some(docs) = split(&file, &options) { 67 | docs.iter().for_each(|doc| { 68 | let embedding = semantic.embed(&doc.text).unwrap(); 69 | let mut document = doc.clone(); 70 | document.vector = embedding.clone(); 71 | 72 | let uuid = Uuid::new_v4().to_string(); 73 | embedding_store.add(uuid, embedding, document); 74 | }); 75 | } 76 | }); 77 | 78 | let app_state = web::Data::new(AppState { 79 | semantic, 80 | storage: Arc::new(embedding_store), 81 | indexes: Arc::new(indexes), 82 | }); 83 | 84 | app_state 85 | } -------------------------------------------------------------------------------- /local_server/src/scraper/article.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::time::Duration; 3 | 4 | use anyhow::Result; 5 | use reqwest::IntoUrl; 6 | use select::document::Document; 7 | use url::Url; 8 | 9 | #[derive(Debug)] 10 | pub struct Article { 11 | pub url: Url, 12 | pub doc: Document, 13 | pub content: ArticleContent<'static>, 14 | pub language: &'static str, 15 | } 16 | 17 | impl Article { 18 | pub fn builder(url: T) -> Result { 19 | ArticleBuilder::new(url) 20 | } 21 | } 22 | 23 | 24 | #[derive(Debug, Clone)] 25 | pub struct ArticleContent<'a> { 26 | pub title: Option>, 27 | pub icon: Option>, 28 | pub language: Option<&'a str>, 29 | pub description: Option>, 30 | pub text: Option>, 31 | } 32 | 33 | pub struct ArticleBuilder { 34 | url: Option, 35 | timeout: Option, 36 | language: Option, 37 | browser_user_agent: Option, 38 | } 39 | 40 | impl ArticleBuilder { 41 | fn new(url: T) -> Result { 42 | let url = url.into_url()?; 43 | 44 | Ok(ArticleBuilder { 45 | url: Some(url), 46 | timeout: None, 47 | language: None, 48 | browser_user_agent: None, 49 | }) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /local_server/src/scraper/chunk.rs: -------------------------------------------------------------------------------- 1 | use tree_sitter::{Node, Parser}; 2 | use crate::scraper::text_range::TextRange; 3 | 4 | pub struct Section<'s> { 5 | pub data: &'s str, 6 | pub ancestry: Vec<&'s str>, 7 | pub header: Option<&'s str>, 8 | pub section_range: TextRange, 9 | pub node_range: TextRange, 10 | } 11 | 12 | impl<'a> Section<'a> { 13 | pub fn ancestry_str(&self) -> String { 14 | self.ancestry.join(" > ") 15 | } 16 | 17 | /// may not be idempotent 18 | pub fn ancestry_from_str(s: &str) -> Vec<&str> { 19 | s.split(" > ").filter(|h| !h.is_empty()).collect() 20 | } 21 | } 22 | 23 | // - collect non-section child-nodes for the current node 24 | // - these form a single chunk to be embedded 25 | // - repeat above on every section child-node 26 | const MAX_DEPTH: usize = 1; 27 | 28 | pub fn sectionize<'s, 'b>( 29 | start_node: &'b Node, 30 | sections: &'b mut Vec>, 31 | mut ancestry: Vec<&'s str>, 32 | depth: usize, 33 | src: &'s str, 34 | ) { 35 | let mut cursor = start_node.walk(); 36 | 37 | // discover section and non-section nodes among direct child nodes 38 | let (section_nodes, non_section_nodes): (Vec<_>, Vec<_>) = start_node 39 | .named_children(&mut cursor) 40 | .partition(|child| child.kind() == "section"); 41 | 42 | // extract header of start_node 43 | let own_header = non_section_nodes 44 | .iter() 45 | .find(|child| child.kind() == "atx_heading") 46 | .map(|child| src[child.byte_range()].trim()); 47 | 48 | // do not sectionize after h4 49 | if depth > MAX_DEPTH { 50 | sections.push(Section { 51 | data: &src[start_node.byte_range()], 52 | ancestry: ancestry.clone(), 53 | header: own_header, 54 | section_range: start_node.range().into(), 55 | node_range: start_node.range().into(), 56 | }); 57 | return; 58 | } 59 | 60 | // collect ranges of all non-section nodes 61 | let own_section_range = non_section_nodes 62 | .into_iter() 63 | .map(|node| node.range()) 64 | .reduce(cover); 65 | 66 | if let Some(r) = own_section_range { 67 | sections.push(Section { 68 | data: &src[r.start_byte..r.end_byte], 69 | ancestry: ancestry.clone(), 70 | section_range: r.into(), 71 | node_range: start_node.range().into(), 72 | header: own_header, 73 | }); 74 | } 75 | 76 | // add current header to ancestry and recurse 77 | if let Some(h) = own_header { 78 | ancestry.push(h.trim()); 79 | } 80 | 81 | for sub_section in section_nodes { 82 | sectionize(&sub_section, sections, ancestry.clone(), depth + 1, src); 83 | } 84 | } 85 | 86 | fn cover(a: tree_sitter::Range, b: tree_sitter::Range) -> tree_sitter::Range { 87 | let start_byte = a.start_byte.min(b.start_byte); 88 | let end_byte = a.end_byte.max(b.end_byte); 89 | let start_point = a.start_point.min(b.start_point); 90 | let end_point = a.end_point.max(b.end_point); 91 | 92 | tree_sitter::Range { 93 | start_byte, 94 | end_byte, 95 | start_point, 96 | end_point, 97 | } 98 | } 99 | 100 | pub fn by_section(src: &str) -> Vec> { 101 | let mut parser = Parser::new(); 102 | parser.set_language(tree_sitter_md::language()).unwrap(); 103 | 104 | let tree = parser.parse(src.as_bytes(), None).unwrap(); 105 | let root_node = tree.root_node(); 106 | 107 | let mut sections = Vec::new(); 108 | sectionize(&root_node, &mut sections, vec![], 0, src); 109 | 110 | sections 111 | } 112 | 113 | 114 | // test 115 | #[cfg(test)] 116 | mod tests { 117 | use super::*; 118 | 119 | #[test] 120 | fn test_sectionize() { 121 | let src = r#" 122 | # hello world 123 | 124 | ## hello world 125 | 126 | ### hello world 127 | 128 | ## Block 2 129 | 130 | "#; 131 | 132 | let sections = by_section(src); 133 | 134 | assert_eq!(sections.len(), 3); 135 | assert_eq!(sections[0].data, "# hello world\n"); 136 | assert_eq!(sections[1].data, "## hello world\n\n### hello world\n\n"); 137 | assert_eq!(sections[2].data, "## Block 2\n\n"); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /local_server/src/scraper/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod chunk; 2 | pub mod text_range; 3 | 4 | pub mod article; -------------------------------------------------------------------------------- /local_server/src/scraper/text_range.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::{Ord, Ordering}; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | /// A singular position in a text document 6 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] 7 | pub struct Point { 8 | /// The byte index 9 | pub byte: usize, 10 | 11 | /// 0-indexed line number 12 | pub line: usize, 13 | 14 | /// Position within the line 15 | pub column: usize, 16 | } 17 | 18 | impl PartialOrd for Point { 19 | fn partial_cmp(&self, other: &Self) -> Option { 20 | Some(self.cmp(other)) 21 | } 22 | } 23 | 24 | impl Ord for Point { 25 | fn cmp(&self, other: &Self) -> Ordering { 26 | self.byte.cmp(&other.byte) 27 | } 28 | } 29 | 30 | impl Point { 31 | pub fn new(byte: usize, line: usize, column: usize) -> Self { 32 | Self { byte, line, column } 33 | } 34 | 35 | pub fn from_byte(byte: usize, line_end_indices: &[u32]) -> Self { 36 | let line = line_end_indices 37 | .iter() 38 | .position(|&line_end_byte| (line_end_byte as usize) > byte) 39 | .unwrap_or(0); 40 | 41 | let column = line 42 | .checked_sub(1) 43 | .and_then(|idx| line_end_indices.get(idx)) 44 | .map(|&prev_line_end| byte.saturating_sub(prev_line_end as usize)) 45 | .unwrap_or(byte); 46 | 47 | Self::new(byte, line, column) 48 | } 49 | } 50 | 51 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] 52 | pub struct TextRange { 53 | pub start: Point, 54 | pub end: Point, 55 | } 56 | 57 | impl PartialOrd for TextRange { 58 | fn partial_cmp(&self, other: &Self) -> Option { 59 | Some(self.cmp(other)) 60 | } 61 | } 62 | 63 | impl Ord for TextRange { 64 | fn cmp(&self, other: &Self) -> Ordering { 65 | let compare_start_byte = self.start.byte.cmp(&other.start.byte); 66 | let compare_size = self.size().cmp(&other.size()); 67 | 68 | compare_start_byte.then(compare_size) 69 | } 70 | } 71 | 72 | impl TextRange { 73 | pub fn new(start: Point, end: Point) -> Self { 74 | assert!(start <= end); 75 | Self { start, end } 76 | } 77 | 78 | pub fn contains(&self, other: &TextRange) -> bool { 79 | // (self.start ... [other.start ... other.end] ... self.end) 80 | self.start <= other.start && other.end <= self.end 81 | } 82 | 83 | #[allow(unused)] 84 | pub fn contains_strict(&self, other: TextRange) -> bool { 85 | // (self.start ... (other.start ... other.end) ... self.end) 86 | self.start < other.start && other.end <= self.end 87 | } 88 | 89 | pub fn size(&self) -> usize { 90 | self.end.byte.saturating_sub(self.start.byte) 91 | } 92 | 93 | pub fn from_byte_range(range: std::ops::Range, line_end_indices: &[u32]) -> Self { 94 | let start = Point::from_byte(range.start, line_end_indices); 95 | let end = Point::from_byte(range.end, line_end_indices); 96 | Self::new(start, end) 97 | } 98 | } 99 | 100 | impl From for TextRange { 101 | fn from(r: tree_sitter::Range) -> Self { 102 | Self { 103 | start: Point { 104 | byte: r.start_byte, 105 | line: r.start_point.row, 106 | column: r.start_point.column, 107 | }, 108 | end: Point { 109 | byte: r.end_byte, 110 | line: r.end_point.row, 111 | column: r.end_point.column, 112 | }, 113 | } 114 | } 115 | } 116 | 117 | impl From for std::ops::Range { 118 | fn from(r: TextRange) -> std::ops::Range { 119 | r.start.byte..r.end.byte 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /nx.json: -------------------------------------------------------------------------------- 1 | { 2 | "targetDefaults": { 3 | "build": { 4 | "cache": true, 5 | "inputs": ["production", "^production"], 6 | "dependsOn": ["^build"] 7 | }, 8 | "dev": { 9 | "dependsOn": ["^build"] 10 | }, 11 | "lint": { 12 | "cache": true, 13 | "inputs": [ 14 | "default", 15 | "{workspaceRoot}/.eslintrc.json", 16 | "{workspaceRoot}/.eslintignore", 17 | "{workspaceRoot}/eslint.config.js" 18 | ] 19 | }, 20 | "test": { 21 | "cache": true 22 | } 23 | }, 24 | "installation": { 25 | "version": "17.1.3" 26 | }, 27 | "namedInputs": { 28 | "default": ["{projectRoot}/**/*", "sharedGlobals"], 29 | "sharedGlobals": [], 30 | "production": [ 31 | "default", 32 | "!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)", 33 | "!{projectRoot}/tsconfig.spec.json", 34 | "!{projectRoot}/jest.config.[jt]s", 35 | "!{projectRoot}/src/test-setup.[jt]s", 36 | "!{projectRoot}/test-setup.[jt]s", 37 | "!{projectRoot}/.eslintrc.json", 38 | "!{projectRoot}/eslint.config.js" 39 | ] 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "studio-b3", 3 | "description": "[![Deploy](https://github.com/unit-mesh/3b/actions/workflows/deploy.yml/badge.svg)](https://github.com/unit-mesh/3b/actions/workflows/deploy.yml)\r ![npm](https://img.shields.io/npm/v/b3-editor)\r ![GitHub release (with filter)](https://img.shields.io/github/v/release/unit-mesh/b3)", 4 | "directories": { 5 | "doc": "docs" 6 | }, 7 | "version": "0.2.0", 8 | "scripts": { 9 | "serve-llmapi": "pnpm -F @studio-b3/llmapi watch", 10 | "serve-core": "pnpm -F @studio-b3/web-core watch", 11 | "build-core": "nx build @studio-b3/web-core", 12 | "serve-studio": "nx dev @studio-b3/web-studio", 13 | "build-studio": "nx build @studio-b3/web-studio", 14 | "build-native": "nx tauri:build @studio-b3/web-native-wrapper" 15 | }, 16 | "packageManager": "pnpm@8.0.0", 17 | "devDependencies": { 18 | "@nrwl/cli": "^15.9.3", 19 | "@types/node": "20.9.4", 20 | "@types/react": "18.2.38", 21 | "@types/react-dom": "18.2.17", 22 | "@typescript-eslint/eslint-plugin": "^6.10.0", 23 | "@typescript-eslint/parser": "^6.10.0", 24 | "@vitejs/plugin-react": "^4.2.0", 25 | "eslint": "^8.53.0", 26 | "eslint-config-next": "14.0.3", 27 | "eslint-config-prettier": "^9.0.0", 28 | "eslint-define-config": "^2.0.0", 29 | "eslint-plugin-import": "2.27.5", 30 | "eslint-plugin-jsx-a11y": "6.7.1", 31 | "eslint-plugin-react": "7.32.2", 32 | "eslint-plugin-react-hooks": "^4.6.0", 33 | "eslint-plugin-react-refresh": "^0.4.4", 34 | "eslint-plugin-storybook": "^0.6.15", 35 | "nx": "^17.2.0", 36 | "prettier": "^2.6.2", 37 | "react": "^18.2.0", 38 | "react-dom": "^18.2.0", 39 | "ts-node": "10.9.1", 40 | "typescript": "^5.2.2", 41 | "vite": "^5.0.0", 42 | "vite-plugin-checker": "^0.6.2", 43 | "vite-plugin-dts": "^3.6.4", 44 | "vite-plugin-externalize-deps": "^0.8.0", 45 | "vite-plugin-lib-inject-css": "^1.3.0" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'web/*' -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": {}, 3 | } -------------------------------------------------------------------------------- /web/converter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@studio-b3/converter", 3 | "version": "0.0.2", 4 | "type": "module", 5 | "main": "dist/index.mjs", 6 | "types": "dist-types/index.d.ts", 7 | "exports": { 8 | ".": { 9 | "import": "./dist/index.mjs" 10 | }, 11 | "./package.json": "./package.json" 12 | }, 13 | "typesVersions": { 14 | "*": { 15 | "*": [ 16 | "./dist-types/index.d.ts", 17 | "./dist-types/*" 18 | ] 19 | } 20 | }, 21 | "sideEffects": false, 22 | "files": [ 23 | "dist", 24 | "dist-types", 25 | "src" 26 | ], 27 | "scripts": { 28 | "watch": "vite build --watch", 29 | "build": "vite build", 30 | "lint": "eslint . --ext ts,tsx,.cjs --report-unused-disable-directives --max-warnings 0", 31 | "lint:fix": "eslint . --ext .ts,.cjs --fix --fix-type [problem,suggestion]" 32 | }, 33 | "dependencies": { 34 | 35 | }, 36 | "private": false, 37 | "devDependencies": {} 38 | } 39 | -------------------------------------------------------------------------------- /web/converter/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": [ 6 | "ES2020" 7 | ], 8 | "module": "ESNext", 9 | "skipLibCheck": true, 10 | /* Bundler mode */ 11 | "moduleResolution": "bundler", 12 | "allowImportingTsExtensions": true, 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "noEmit": true, 16 | /* Linting */ 17 | // "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true 21 | }, 22 | "include": [ 23 | "src" 24 | ], 25 | "references": [ 26 | { 27 | "path": "./tsconfig.node.json" 28 | } 29 | ] 30 | } -------------------------------------------------------------------------------- /web/converter/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.mts"] 10 | } 11 | -------------------------------------------------------------------------------- /web/converter/vite.config.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import checker from 'vite-plugin-checker'; 3 | import dts from 'vite-plugin-dts'; 4 | import { externalizeDeps } from 'vite-plugin-externalize-deps'; 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig({ 8 | plugins: [ 9 | checker({ 10 | typescript: true, 11 | }), 12 | externalizeDeps(), 13 | dts({ 14 | outDir: './dist-types', 15 | }), 16 | ], 17 | build: { 18 | copyPublicDir: false, 19 | lib: { 20 | entry: 'src/index.ts', 21 | formats: ['es'], 22 | }, 23 | rollupOptions: { 24 | output: { 25 | dir: 'dist', 26 | exports: 'named', 27 | entryFileNames: '[name].mjs', 28 | chunkFileNames: '[name].mjs', 29 | }, 30 | }, 31 | }, 32 | }); 33 | -------------------------------------------------------------------------------- /web/core/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const { defineConfig } = require('eslint-define-config') 3 | 4 | module.exports = defineConfig({ 5 | root: true, 6 | env: { 7 | node: true, 8 | browser: true, 9 | es2022: true 10 | }, 11 | extends: [ 12 | 'eslint:recommended', 13 | ], 14 | plugins: ['import'], 15 | parserOptions: { 16 | sourceType: 'module', 17 | ecmaVersion: 'latest', 18 | }, 19 | ignorePatterns: ['dist', 'dist-types'], 20 | overrides: [ 21 | { 22 | files: ['*.ts', '*.tsx'], // Added *.tsx 23 | extends: [ 24 | 'eslint:recommended', 25 | 'plugin:@typescript-eslint/recommended', 26 | 'plugin:react-hooks/recommended', 27 | 'plugin:react/recommended' // Added React plugin 28 | ], 29 | plugins: ['@typescript-eslint', 'import', 'react-refresh', 'react'], 30 | parser: '@typescript-eslint/parser', 31 | parserOptions: { 32 | ecmaVersion: 'latest', 33 | sourceType: 'module', 34 | ecmaFeatures: { 35 | jsx: true 36 | }, 37 | project: './tsconfig.json' // Add reference to your tsconfig 38 | }, 39 | settings: { 40 | react: { 41 | version: 'detect' 42 | } 43 | }, 44 | rules: { 45 | 'react-refresh/only-export-components': [ 46 | 'warn', 47 | { allowConstantExport: true }, 48 | ], 49 | '@typescript-eslint/explicit-function-return-type': 'off', 50 | '@typescript-eslint/no-explicit-any': 'off' 51 | }, 52 | }, 53 | { 54 | files: ['test', '__test__', '*.{spec,test}.ts'], 55 | rules: { 56 | '@typescript-eslint/no-var-requires': 'off', 57 | "@typescript-eslint/no-unused-vars": "off", 58 | 'tsdoc/syntax': 'off', 59 | }, 60 | }, 61 | ] 62 | }) 63 | -------------------------------------------------------------------------------- /web/core/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /web/core/lib/editor/action/AiActionExecutor.ts: -------------------------------------------------------------------------------- 1 | import { Editor } from '@tiptap/core'; 2 | import { OutputForm, PromptAction } from '@/editor/defs/custom-action.type'; 3 | import { actionPosition, PromptCompiler } from '@/editor/action/PromptCompiler'; 4 | import { MarkdownParser } from '@/../node_modules/tiptap-markdown/src/parse/MarkdownParser'; 5 | import { BuiltinFunctionExecutor } from '@/editor/action/BuiltinFunctionExecutor'; 6 | 7 | export class AiActionExecutor { 8 | editor: Editor; 9 | endpointUrl: string = '/api/completion'; 10 | markdownParser: MarkdownParser; 11 | 12 | constructor() { 13 | } 14 | 15 | setEditor(editor: Editor) { 16 | if (editor == null) { 17 | console.error('editor is null, will not set it'); 18 | return; 19 | } 20 | 21 | this.markdownParser = new MarkdownParser(editor, {}); 22 | this.editor = editor; 23 | } 24 | 25 | setEndpointUrl(url: string) { 26 | this.endpointUrl = url; 27 | } 28 | 29 | 30 | /** 31 | * TODO: will according the {@link PromptAction.useModel} to return the endpoint in future 32 | * @param action 33 | */ 34 | endpoint(action: PromptAction) { 35 | return this.endpointUrl; 36 | } 37 | 38 | async handleStreaming(action: PromptAction, prompt: string) { 39 | this.editor.setEditable(false); 40 | const originalSelection = this.editor.state.selection; 41 | 42 | const response = await fetch(this.endpoint(action), { 43 | method: 'POST', 44 | body: JSON.stringify({ prompt: prompt }), 45 | headers: { Accept: 'text/event-stream' } 46 | }); 47 | 48 | let allText = ''; 49 | let buffer = ''; 50 | console.info(response.body) 51 | await response.body?.pipeThrough(new TextDecoderStream()).pipeTo( 52 | new WritableStream({ 53 | write: (chunk) => { 54 | allText += chunk; 55 | buffer = buffer.concat(chunk); 56 | 57 | console.info('buffer', buffer); 58 | 59 | if (buffer.includes('\n')) { 60 | const pos = actionPosition(action, this.editor.state.selection); 61 | this.editor.chain().focus()?.insertContentAt(pos, buffer).run(); 62 | 63 | // insert new line 64 | const posInfo = actionPosition(action, this.editor.state.selection); 65 | this.editor.chain().focus()?.insertContentAt(posInfo, '\n').run(); 66 | 67 | buffer = ''; 68 | } 69 | } 70 | }) 71 | ); 72 | 73 | if (buffer.length > 0) { 74 | const pos = actionPosition(action, this.editor.state.selection); 75 | this.editor.chain().focus()?.insertContentAt(pos, buffer).run(); 76 | } 77 | 78 | if (this.editor == null) { 79 | console.error('editor is not, can not insert content'); 80 | return; 81 | } 82 | 83 | const pos = actionPosition(action, this.editor.state.selection); 84 | this.editor.chain().focus()?.insertContentAt(pos, buffer).run(); 85 | 86 | const markdownNode = this.markdownParser.parse(allText); 87 | 88 | this.editor.chain().focus()?.deleteRange({ 89 | from: originalSelection.from, 90 | to: this.editor.state.selection.to 91 | }).run(); 92 | 93 | this.editor.chain().insertContentAt(this.editor.state.selection, markdownNode).run(); 94 | 95 | this.editor.setEditable(true); 96 | } 97 | 98 | async handleTextOrDiff(action: PromptAction, prompt: string): Promise { 99 | this.editor.setEditable(false); 100 | 101 | const response = await fetch(this.endpoint(action), { 102 | method: 'POST', 103 | body: JSON.stringify({ prompt: prompt, stream: false }) 104 | }); 105 | 106 | const text = await response.text(); 107 | this.editor.setEditable(true); 108 | 109 | return text; 110 | } 111 | 112 | async handleDefault(action: PromptAction, prompt: string) { 113 | this.editor.setEditable(false); 114 | const response = await fetch(this.endpoint(action), { 115 | method: 'POST', 116 | body: JSON.stringify({ prompt: prompt }) 117 | }); 118 | 119 | const msg = await response.text(); 120 | const posInfo = actionPosition(action, this.editor.state.selection); 121 | const node = this.markdownParser.parse(msg); 122 | this.editor.chain().focus()?.insertContentAt(posInfo, node).run(); 123 | 124 | this.editor.setEditable(true); 125 | 126 | return msg; 127 | } 128 | 129 | async execute(action: PromptAction) { 130 | console.info('execute action', action); 131 | if (action.builtinFunction) { 132 | const executor = new BuiltinFunctionExecutor(this.editor); 133 | return await executor.execute(action); 134 | } 135 | 136 | const actionExecutor = new PromptCompiler(action, this.editor); 137 | actionExecutor.compile(); 138 | 139 | const prompt = action.compiledTemplate; 140 | 141 | if (prompt == null) { 142 | throw Error('template is not been compiled yet! compile it first'); 143 | } 144 | 145 | console.info('compiledTemplate: \n\n', prompt); 146 | 147 | switch (action.outputForm) { 148 | case OutputForm.STREAMING: 149 | await this.handleStreaming(action, prompt); 150 | return undefined; 151 | 152 | case OutputForm.DIFF: 153 | case OutputForm.TEXT: 154 | return await this.handleTextOrDiff(action, prompt); 155 | 156 | default: 157 | return this.handleDefault(action, prompt); 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /web/core/lib/editor/action/BuiltinFunctionExecutor.ts: -------------------------------------------------------------------------------- 1 | import { Editor } from "@tiptap/core"; 2 | import { BuiltInFunc, PromptAction } from "@/editor/defs/custom-action.type"; 3 | 4 | export class BuiltinFunctionExecutor { 5 | private editor: Editor; 6 | 7 | constructor(editor: Editor) { 8 | this.editor = editor; 9 | } 10 | 11 | async execute(action: PromptAction) { 12 | switch (action.builtinFunction) { 13 | case BuiltInFunc.GRAMMAR_CHECK: 14 | break; 15 | case BuiltInFunc.SIMILAR_CHUNKS: 16 | return this.searchSimilarChunks(action); 17 | case BuiltInFunc.SPELLING_CHECK: 18 | break; 19 | case BuiltInFunc.WIKI_SUMMARY: 20 | break; 21 | case BuiltInFunc.RELATED_CHUNKS: 22 | break; 23 | } 24 | } 25 | 26 | private searchSimilarChunks(action: PromptAction) { 27 | return new Promise((resolve, reject) => { 28 | const selection = this.editor.state.selection; 29 | const query = this.editor.state.doc.textBetween(selection.from, selection.to); 30 | 31 | const response = fetch(`http://127.0.0.1:8080/api/embedding-document/search?q=${query}`, { 32 | method: "GET", 33 | }) 34 | .then(res => res.json()) 35 | .then(data => { 36 | console.log(data); 37 | }) 38 | 39 | return undefined; 40 | }); 41 | } 42 | } -------------------------------------------------------------------------------- /web/core/lib/editor/action/PromptCompiler.ts: -------------------------------------------------------------------------------- 1 | import { ChangeForm, DefinedVariable, PromptAction } from "@/editor/defs/custom-action.type"; 2 | import { Editor, Range } from "@tiptap/core"; 3 | import { Selection } from "prosemirror-state"; 4 | import { PromptsManager } from "@/editor/prompts/prompts-manager"; 5 | 6 | export class PromptCompiler { 7 | private action: PromptAction; 8 | private editor: Editor; 9 | 10 | constructor(action: PromptAction, editor: Editor) { 11 | this.action = action; 12 | this.editor = editor; 13 | } 14 | 15 | compile() { 16 | const promptManager = PromptsManager.getInstance(); 17 | const state = this.editor.state; 18 | 19 | const range = actionPosition(this.action, state.selection); 20 | const selection = state.doc.textBetween(range.from, range.to); 21 | const beforeCursor = state.doc.textBetween(0, range.to); 22 | const afterCursor = state.doc.textBetween(range.to, state.doc.nodeSize - 2); 23 | const all = this.editor.getText(); 24 | let title = ''; 25 | 26 | state.doc.descendants((node) => { 27 | if (node.type.name === 'heading' && node.attrs.level === 1) { 28 | title = node.textContent; 29 | } 30 | }); 31 | 32 | const similarChunks = ""; 33 | 34 | const context = { 35 | [DefinedVariable.BEFORE_CURSOR]: beforeCursor, 36 | [DefinedVariable.AFTER_CURSOR]: afterCursor, 37 | [DefinedVariable.SELECTION]: selection, 38 | [DefinedVariable.ALL]: all, 39 | [DefinedVariable.SIMILAR_CHUNKS]: similarChunks, 40 | [DefinedVariable.TITLE]: title, 41 | }; 42 | console.info("variable context", context); 43 | this.action.compiledTemplate = promptManager.compile(this.action.template, context); 44 | } 45 | } 46 | 47 | export function actionPosition(action: PromptAction, selection: Selection): Range { 48 | let posInfo: Range; 49 | switch (action.changeForm) { 50 | case ChangeForm.INSERT: 51 | posInfo = { 52 | from: selection.to, 53 | to: selection.to 54 | }; 55 | break; 56 | case ChangeForm.REPLACE: 57 | posInfo = { 58 | from: selection.from, 59 | to: selection.to 60 | }; 61 | break; 62 | case ChangeForm.DIFF: 63 | posInfo = { 64 | from: selection.from, 65 | to: selection.to 66 | }; 67 | break; 68 | default: 69 | posInfo = { 70 | from: selection.from, 71 | to: selection.to 72 | }; 73 | } 74 | 75 | return posInfo; 76 | } 77 | -------------------------------------------------------------------------------- /web/core/lib/editor/action/custom-editor-commands.ts: -------------------------------------------------------------------------------- 1 | import { Commands, Extension } from '@tiptap/react'; 2 | import { Editor } from '@tiptap/core'; 3 | import { Transaction } from 'prosemirror-state'; 4 | import { DefinedVariable, FacetType, OutputForm, PromptAction } from '@/editor/defs/custom-action.type'; 5 | import { PromptsManager } from '@/editor/prompts/prompts-manager'; 6 | import { ARTICLE_TYPE_OPTIONS, TypeOptions } from '@/editor/defs/type-options.type'; 7 | import { AiActionExecutor } from '@/editor/action/AiActionExecutor'; 8 | 9 | declare module '@tiptap/core' { 10 | interface Commands { 11 | callLlm: { 12 | callLlm: (action: PromptAction) => string | undefined; 13 | }; 14 | getAiActions: { 15 | getAiActions: (facet: FacetType) => PromptAction[]; 16 | }; 17 | callQuickAction: { 18 | callQuickAction: (text: string) => ReturnType; 19 | } 20 | runAiAction: { 21 | runAiAction: (action: PromptAction) => ReturnType; 22 | }; 23 | replaceRange: { 24 | replaceRange: (text: string) => ReturnType; 25 | } 26 | setBackgroundContext: () => ReturnType, 27 | getArticleType: { 28 | getArticleType: () => TypeOptions, 29 | } 30 | setArticleType: { 31 | setArticleType: (articleType: TypeOptions) => ReturnType 32 | }, 33 | } 34 | } 35 | 36 | let articleType = ARTICLE_TYPE_OPTIONS[0]; 37 | 38 | export const CustomEditorCommands = ( 39 | actionHandler: AiActionExecutor, 40 | promptsManager: PromptsManager = PromptsManager.getInstance() 41 | ) => { 42 | return Extension.create({ 43 | name: 'commandFunctions', 44 | 45 | // @ts-ignore 46 | addCommands: () => { 47 | return { 48 | getArticleType: 49 | () => 50 | ({ tr }: { tr: Transaction }) => { 51 | return articleType; 52 | }, 53 | setArticleType: 54 | (type: TypeOptions) => 55 | ({ editor, tr, dispatch }: { editor: Editor, tr: Transaction, dispatch: any }) => { 56 | articleType = type; 57 | }, 58 | 59 | callLlm: 60 | (action: PromptAction) => 61 | async ({ tr, commands, editor }: { tr: Transaction; commands: Commands, editor: Editor }) => { 62 | actionHandler.setEditor(editor); 63 | return await actionHandler.execute(action); 64 | }, 65 | getAiActions: 66 | (facet: FacetType) => 67 | ({ editor }: { editor: Editor }) => { 68 | const articleType = editor.commands.getArticleType(); 69 | return promptsManager.getActions(facet, articleType); 70 | }, 71 | runAiAction: 72 | (action: PromptAction) => 73 | ({ editor }: { editor: Editor }) => { 74 | if (action.action) { 75 | action.action(editor).then(() => {}); 76 | return; 77 | } else { 78 | editor.commands.callLlm(action); 79 | } 80 | }, 81 | callQuickAction: 82 | (text: string) => 83 | ({ editor }: { editor: Editor }) => { 84 | editor.setEditable(false); 85 | editor.commands.callLlm({ 86 | name: text, 87 | template: `You are an assistant to help user write article. Here is user command:` + text + `\n Here is some content ###Article title: {{${DefinedVariable.TITLE}}}, Before Content: {{${DefinedVariable.BEFORE_CURSOR}}}###`, 88 | facetType: FacetType.QUICK_INSERT, 89 | outputForm: OutputForm.STREAMING 90 | }); 91 | 92 | editor.setEditable(true); 93 | }, 94 | replaceRange: 95 | (text: string) => 96 | ({ editor, tr, dispatch }: { editor: Editor, tr: Transaction, dispatch: any }) => { 97 | const { from, to } = editor.state.selection; 98 | tr.replaceRangeWith(from, to, editor.state.schema.text(text)); 99 | dispatch(tr); 100 | }, 101 | setBackgroundContext: 102 | (context: string) => 103 | ({ editor }: { editor: Editor }) => { 104 | promptsManager.saveBackgroundContext(context); 105 | } 106 | }; 107 | } 108 | }); 109 | }; 110 | -------------------------------------------------------------------------------- /web/core/lib/editor/components/base/slider.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as SliderPrimitive from '@radix-ui/react-slider'; 3 | 4 | import { clsx } from 'clsx'; 5 | 6 | export const Slider = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >(({ className, ...props }, ref) => ( 10 | 18 | 19 | 20 | 21 | 22 | 23 | )); 24 | 25 | Slider.displayName = SliderPrimitive.Root.displayName; 26 | -------------------------------------------------------------------------------- /web/core/lib/editor/components/ui-select.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import * as SelectPrimitive from '@radix-ui/react-select'; 3 | import { 4 | CheckIcon, 5 | ChevronDownIcon, 6 | ChevronUpIcon, 7 | } from '@radix-ui/react-icons'; 8 | 9 | interface BeSelectProps { 10 | children: React.ReactNode; 11 | [key: string]: any; 12 | } 13 | 14 | export const BeSelect = React.forwardRef( 15 | (props, forwardedRef) => { 16 | const { children, ...rest } = props; 17 | 18 | return ( 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | {children} 32 | 33 | 34 | 35 | 36 | 37 | 38 | ); 39 | } 40 | ); 41 | 42 | export const BeSelectItem: React.FC = React.forwardRef( 43 | ({ children, ...props }, forwardedRef) => { 44 | return ( 45 | 46 | {children} 47 | 48 | 49 | 50 | 51 | ); 52 | } 53 | ); 54 | -------------------------------------------------------------------------------- /web/core/lib/editor/defs/custom-action.type.ts: -------------------------------------------------------------------------------- 1 | import { Editor } from '@tiptap/core'; 2 | 3 | export enum FacetType { 4 | TOOLBAR_MENU = 0, 5 | BUBBLE_MENU = 1, 6 | SLASH_COMMAND = 2, 7 | /** 8 | * the 9 | */ 10 | QUICK_INSERT = 3, 11 | } 12 | 13 | export enum OutputForm { 14 | /** 15 | * Append the output to the document, not streaming 16 | */ 17 | NORMAL = 0, 18 | /** 19 | * Streaming the output to the document 20 | */ 21 | STREAMING = 1, 22 | /** 23 | * Show the difference between the selected text and the output 24 | */ 25 | DIFF = 2, 26 | /** 27 | * Show the output in which is a popup 28 | */ 29 | NOTIFICATION = 3, 30 | /** 31 | * Side suggestion box 32 | */ 33 | SIDE_BOX = 4, 34 | /** 35 | * Await all 36 | */ 37 | TEXT = 5, 38 | } 39 | 40 | export enum ChangeForm { 41 | /** 42 | * Insert the output to the document 43 | */ 44 | INSERT = 0, 45 | /** 46 | * Replace the selected text with the output 47 | */ 48 | REPLACE = 1, 49 | /** 50 | * Show the difference between the selected text and the output 51 | */ 52 | DIFF = 2, 53 | } 54 | 55 | export enum BuiltInFunc { 56 | SIMILAR_CHUNKS = "SIMILAR_CHUNKS", 57 | RELATED_CHUNKS = "RELATED_CHUNKS", 58 | GRAMMAR_CHECK = "GRAMMAR_CHECK", 59 | SPELLING_CHECK = "SPELLING_CHECK", 60 | WIKI_SUMMARY = "WIKI_SUMMARY", 61 | } 62 | 63 | export enum DefinedVariable { 64 | /** 65 | * The base context, i.e. the document 66 | * 基础上下文,即文档的富余背景 67 | */ 68 | BASE_CONTEXT = "base_context", 69 | /** 70 | * Temporary context, i.e. the background in sidebar 71 | * 临时上下文,即 sidebar 中的背景 72 | */ 73 | TEMP_CONTEXT = "temp_context", 74 | /** 75 | * All the text content before the cursor 76 | * 光标前的所有文本内容 77 | */ 78 | BEFORE_CURSOR = "before_cursor", 79 | /** 80 | * All the text content after the cursor 81 | * 光标后的所有文本内容 82 | */ 83 | AFTER_CURSOR = "after_cursor", 84 | /** 85 | * The selected text 86 | * 选中的文本 87 | */ 88 | SELECTION = "selection", 89 | /** 90 | * All text in the document 91 | * 文档中的所有文本 92 | */ 93 | ALL = "all", 94 | /** 95 | * Similar chunks of the selected text 96 | * 选中文本的相似块 97 | */ 98 | SIMILAR_CHUNKS = "similar_chunks", 99 | /** 100 | * Related chunks of the selected text 101 | * 选中文本的相关块 102 | */ 103 | RELATED_CHUNKS = "related_chunks", 104 | /** 105 | * Title of the document 106 | */ 107 | TITLE = "title", 108 | } 109 | 110 | export enum SourceType { 111 | BEFORE_CURSOR = "BEFORE_CURSOR", 112 | SELECTION = "SELECTION", 113 | } 114 | 115 | export interface PromptAction { 116 | /** 117 | * Name of the action, will be displayed in the menu. 118 | * If i18Name is true, then it will be translated by i18n, so we suggest use `{{` and `}}` inside the name. 119 | * For example: 120 | * ```ts 121 | * name: '{{Continue writing}}' 122 | * i18Name: true 123 | * ``` 124 | */ 125 | name: string; 126 | /** 127 | * Use i18n to translate the prompt name 128 | */ 129 | i18Name?: boolean; 130 | /** 131 | * Template is a handlebars template, for example: 132 | * 133 | * ```handlebars 134 | * You are in {{TEMP_CONTEXT}} and your selection is {{SELECTION}} 135 | * ``` 136 | */ 137 | template: string; 138 | 139 | /** 140 | * Final result that compiled using handlebars engine from [template] 141 | */ 142 | compiledTemplate?: string; 143 | /** 144 | * Use builtin function to execute the prompt 145 | */ 146 | builtinFunction?: BuiltInFunc; 147 | /** 148 | * The type of the facet, like toolbar menu, bubble menu, context menu, slash command, quick insert 149 | */ 150 | facetType: FacetType; 151 | /** 152 | * the output form of the prompt, like streaming, normal, chat, inside box, notification 153 | */ 154 | outputForm: OutputForm; 155 | /** 156 | * The change form of the prompt, like insert, replace, diff 157 | */ 158 | changeForm?: ChangeForm; 159 | /** 160 | * The higher the number, the higher the priority, will be placed higher in the menu 161 | */ 162 | priority?: number; 163 | /** 164 | * The icon of the prompt, will be displayed in the menu 165 | */ 166 | icon?: never; 167 | /** 168 | * The description of the prompt, will be displayed in the menu 169 | */ 170 | description?: string; 171 | /** 172 | * Used LLM model, like openai, gpt3, gpt2, etc. 173 | */ 174 | useModel?: string; 175 | /** 176 | * Condition to show the prompt 177 | */ 178 | condition?: string; // maybe use a function instead ? 179 | /** 180 | * Menu Action 181 | */ 182 | action?: (editor: Editor) => Promise; 183 | } 184 | 185 | -------------------------------------------------------------------------------- /web/core/lib/editor/defs/type-options.type.ts: -------------------------------------------------------------------------------- 1 | export interface TypeOptions { 2 | value: string; 3 | label: string; 4 | } 5 | 6 | export const ARTICLE_TYPE_OPTIONS: TypeOptions[] = [ 7 | { value: 'article', label: '文章' }, 8 | { value: 'requirements', label: '需求文档' }, 9 | ]; 10 | -------------------------------------------------------------------------------- /web/core/lib/editor/extensions/advice/advice-extension.ts: -------------------------------------------------------------------------------- 1 | // This code based on https://github.com/sereneinserenade/tiptap-comment-extension which is licensed under MIT License 2 | import { Mark as PMMark } from "@tiptap/pm/model"; 3 | import { CommandProps, Mark, mergeAttributes, Range } from "@tiptap/react"; 4 | import { Advice } from "./advice"; 5 | 6 | declare module "@tiptap/core" { 7 | interface Commands { 8 | advice: { 9 | setAdviceCommand: (newComment: Advice) => ReturnType; 10 | setAdvice: (commentId: string) => ReturnType; 11 | unsetAdvice: (commentId: string) => ReturnType; 12 | updateAdvice: (commentId: string, newComment: Advice) => ReturnType; 13 | }; 14 | } 15 | } 16 | 17 | export interface MarkWithRange { 18 | mark: PMMark; 19 | range: Range; 20 | } 21 | 22 | export interface CommentOptions { 23 | setAdviceCommand: (comment: Advice) => void; 24 | HTMLAttributes: Record; 25 | onAdviceActivated: (commentId: string | null) => void; 26 | } 27 | 28 | export interface CommentStorage { 29 | activeAdviceId: string | null; 30 | } 31 | 32 | const EXTENSION_NAME = "advice"; 33 | 34 | // https://dev.to/sereneinserenade/how-i-implemented-google-docs-like-commenting-in-tiptap-k2k 35 | export const AdviceExtension = Mark.create({ 36 | name: EXTENSION_NAME, 37 | 38 | addOptions() { 39 | return { 40 | setAdviceCommand: (comment: Advice) => { 41 | }, 42 | HTMLAttributes: {}, 43 | onAdviceActivated: () => { 44 | }, 45 | }; 46 | }, 47 | 48 | addAttributes() { 49 | return { 50 | commentId: { 51 | default: null, 52 | parseHTML: (el: HTMLElement) => (el as HTMLSpanElement).getAttribute("data-advice-id"), 53 | renderHTML: (attrs) => ({ "data-advice-id": attrs.commentId }), 54 | }, 55 | }; 56 | }, 57 | 58 | parseHTML() { 59 | return [ 60 | { 61 | tag: "span[data-advice-id]", 62 | getAttrs: (el: HTMLElement) => 63 | !!(el as HTMLSpanElement).getAttribute("data-advice-id")?.trim() && 64 | null, 65 | }, 66 | ]; 67 | }, 68 | 69 | renderHTML({ HTMLAttributes }: { 70 | HTMLAttributes: Record 71 | }) { 72 | return ["span", mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0,]; 73 | }, 74 | 75 | onSelectionUpdate() { 76 | const { $from } = this.editor.state.selection; 77 | const marks = $from.marks(); 78 | 79 | if (!marks.length) { 80 | this.storage.activeAdviceId = null; 81 | this.options.onAdviceActivated(this.storage.activeAdviceId); 82 | return; 83 | } 84 | 85 | const adviceMark = this.editor.schema.marks.advice; 86 | const activeCommentMark = marks.find((mark) => mark.type === adviceMark); 87 | this.storage.activeAdviceId = activeCommentMark?.attrs.commentId || null; 88 | this.options.onAdviceActivated(this.storage.activeAdviceId); 89 | }, 90 | 91 | addStorage() { 92 | return { 93 | activeAdviceId: null, 94 | }; 95 | }, 96 | 97 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 98 | // @ts-expect-error 99 | addCommands() { 100 | return { 101 | setAdviceCommand: (comment: Advice) => ({ commands }: CommandProps) => { 102 | this.options.setAdviceCommand(comment); 103 | }, 104 | setAdvice: (commentId) => ({ commands }) => { 105 | if (!commentId) return false; 106 | 107 | commands.setMark("advice", { commentId }); 108 | return true; 109 | }, 110 | unsetAdvice: (commentId) => ({ tr, dispatch }) => { 111 | if (!commentId) return false; 112 | 113 | const commentMarksWithRange: MarkWithRange[] = []; 114 | 115 | tr.doc.descendants((node, pos) => { 116 | const commentMark = node.marks.find( 117 | (mark) => 118 | mark.type.name === "advice" && 119 | mark.attrs.commentId === commentId 120 | ); 121 | 122 | if (!commentMark) return; 123 | 124 | commentMarksWithRange.push({ 125 | mark: commentMark, 126 | range: { 127 | from: pos, 128 | to: pos + node.nodeSize, 129 | }, 130 | }); 131 | }); 132 | 133 | commentMarksWithRange.forEach(({ mark, range }) => { 134 | tr.removeMark(range.from, range.to, mark); 135 | }); 136 | 137 | return dispatch?.(tr); 138 | }, 139 | }; 140 | }, 141 | }); 142 | -------------------------------------------------------------------------------- /web/core/lib/editor/extensions/advice/advice-manager.ts: -------------------------------------------------------------------------------- 1 | import { Advice } from "@/editor/extensions/advice/advice"; 2 | 3 | type EventHandler = (data: any) => void; 4 | 5 | export class AdviceManager { 6 | private static instance: AdviceManager; 7 | 8 | static getInstance(): AdviceManager { 9 | if (!AdviceManager.instance) { 10 | AdviceManager.instance = new AdviceManager(); 11 | } 12 | return AdviceManager.instance; 13 | } 14 | 15 | private constructor() {} 16 | 17 | private advices: Record = {}; 18 | 19 | // pub sub 20 | private handlers: Record = {}; 21 | 22 | on(event: string, handler: EventHandler) { 23 | if (!this.handlers[event]) { 24 | this.handlers[event] = []; 25 | } 26 | this.handlers[event].push(handler); 27 | } 28 | 29 | emit(event: string, data: any) { 30 | if (this.handlers[event]) { 31 | this.handlers[event].forEach((handler) => handler(data)); 32 | } 33 | } 34 | 35 | addAdvice(advice: Advice) { 36 | this.advices[advice.id] = advice; 37 | this.emit('add', advice); 38 | } 39 | 40 | getAdvice(id: string) { 41 | return this.advices[id]; 42 | } 43 | 44 | setActiveId(id: string) { 45 | this.emit('active', id); 46 | } 47 | 48 | onActiveIdChange(handler: EventHandler) { 49 | this.on('active', handler); 50 | } 51 | 52 | updateAdvice(id: string, data: Advice) { 53 | this.advices[id] = { 54 | ...this.advices[id], 55 | ...data, 56 | }; 57 | } 58 | 59 | updateAdvices(data: Advice[]) { 60 | Object.keys(data).forEach((id) => { 61 | // @ts-ignore 62 | this.updateAdvice(id, data[id]); 63 | }); 64 | } 65 | 66 | getAdvices(): Advice[] { 67 | return Object.values(this.advices); 68 | } 69 | 70 | removeAdvice(id: string) { 71 | delete this.advices[id]; 72 | this.emit('remove', id); 73 | } 74 | } -------------------------------------------------------------------------------- /web/core/lib/editor/extensions/advice/advice-view.tsx: -------------------------------------------------------------------------------- 1 | import { Editor } from "@tiptap/core" 2 | import React, { useEffect, useRef, useState } from "react"; 3 | import { AdviceManager } from "@/editor/extensions/advice/advice-manager"; 4 | import { Advice } from "@/editor/extensions/advice/advice"; 5 | import { MarkdownParser } from '@/../node_modules/tiptap-markdown/src/parse/MarkdownParser'; 6 | 7 | export interface AdviceViewProps { 8 | editor: Editor 9 | } 10 | 11 | // based on : https://github.com/sereneinserenade/tiptap-comment-extension/blob/d8ad0d01e98ac416e69f27ab237467b782076c16/demos/react/src/components/Tiptap.tsx 12 | export const AdviceView = ({ editor }: AdviceViewProps) => { 13 | const [advices, setAdvices] = useState([]) 14 | const [activeCommentId, setActiveId] = useState(null) 15 | const advicesSectionRef = useRef(null) 16 | 17 | const focusAdviceWithActiveId = (id: string) => { 18 | if (!advicesSectionRef.current) return 19 | const adviceInput = advicesSectionRef.current.querySelector(`p#${id}`) 20 | if (!adviceInput) return 21 | adviceInput.scrollIntoView({ 22 | behavior: 'smooth', 23 | block: 'center', 24 | inline: 'center' 25 | }) 26 | } 27 | 28 | useEffect(() => { 29 | AdviceManager.getInstance().on('add', (advice) => { 30 | setAdvices(AdviceManager.getInstance().getAdvices()); 31 | setActiveId(advice.id); 32 | setTimeout(() => { 33 | focusAdviceWithActiveId(advice.id); 34 | }); 35 | }); 36 | 37 | AdviceManager.getInstance().on('remove', (advice) => { 38 | setAdvices(AdviceManager.getInstance().getAdvices()); 39 | }); 40 | 41 | AdviceManager.getInstance().onActiveIdChange((id) => { 42 | setActiveId(id); 43 | setTimeout(() => { 44 | focusAdviceWithActiveId(id); 45 | }); 46 | }); 47 | }, []); 48 | 49 | return
52 | {advices.length ? (advices.map(advice => ( 53 |
57 | 58 | AI Assistant 59 | 60 | {advice.createdAt.toLocaleDateString()} 61 | 62 | 63 | 64 |

{advice.content || ''}

67 | 68 |
69 | 79 | 103 |
104 |
105 | )) 106 | ) : (No advices yet) 107 | } 108 |
109 | } 110 | -------------------------------------------------------------------------------- /web/core/lib/editor/extensions/advice/advice.ts: -------------------------------------------------------------------------------- 1 | import { v4 } from 'uuid' 2 | 3 | export interface Advice { 4 | id: string 5 | content: string 6 | replies: Advice[] 7 | createdAt: Date 8 | } 9 | 10 | export const newAdvice = (content: string): Advice => { 11 | if (typeof content !== "string") { 12 | console.log("content is: typeof content", typeof content); 13 | } 14 | 15 | return { 16 | id: `a${v4()}a`, 17 | content, 18 | replies: [], 19 | createdAt: new Date() 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /web/core/lib/editor/extensions/inline-completion/inline-completion.tsx: -------------------------------------------------------------------------------- 1 | import { NodeViewWrapper, ReactNodeViewRenderer } from '@tiptap/react'; 2 | import { Node, Editor } from '@tiptap/core'; 3 | import React, { useEffect, useRef } from 'react'; 4 | import { KeyboardIcon } from "@radix-ui/react-icons"; 5 | 6 | declare module '@tiptap/core' { 7 | interface Commands { 8 | commands: { 9 | triggerInlineCompletion: () => ReturnType, 10 | completeInlineCompletion: () => ReturnType, 11 | cancelInlineCompletion: () => ReturnType, 12 | } 13 | } 14 | } 15 | 16 | const extensionName = "inline-completion"; 17 | export const InlineCompletion = Node.create({ 18 | name: extensionName, 19 | group: "block", 20 | defining: true, 21 | isolating: true, 22 | hasTrigger: false, 23 | content: "text*", 24 | addOptions() { 25 | return { 26 | HTMLAttributes: { 27 | class: "inline-completion", 28 | }, 29 | } 30 | }, 31 | addKeyboardShortcuts() { 32 | return { 33 | "Mod-\\": (): boolean => { 34 | // @ts-ignore 35 | this.hasTrigger = true 36 | this.editor.commands.triggerInlineCompletion() 37 | return true 38 | }, 39 | "Tab": (): boolean => { 40 | // @ts-ignore 41 | this.hasTrigger = false 42 | this.editor.commands.completeInlineCompletion() 43 | return true 44 | }, 45 | "`": (): boolean => { 46 | // @ts-ignore 47 | if (!this.hasTrigger) { 48 | return false 49 | } 50 | // @ts-ignore 51 | this.hasTrigger = false 52 | this.editor.commands.completeInlineCompletion() 53 | return true 54 | }, 55 | Escape: (): boolean => { 56 | // @ts-ignore 57 | if (!this.hasTrigger) { 58 | return false 59 | } 60 | // @ts-ignore 61 | this.hasTrigger = false 62 | this.editor.commands.cancelInlineCompletion(); 63 | return true 64 | }, 65 | } 66 | }, 67 | // @ts-ignore 68 | addCommands() { 69 | return { 70 | triggerInlineCompletion: (options: any) => ({ commands }: { commands: any }) => { 71 | return commands.insertContent({ 72 | type: this.name, 73 | attrs: options, 74 | }) 75 | }, 76 | completeInlineCompletion: (options: any) => ({ commands, tr }: { commands: any, tr: any }) => { 77 | const pos = this.editor.view.state.selection.$anchor.pos; 78 | // commands.deleteNode(this.name) 79 | commands.insertContentAt(pos, "done completion") 80 | 81 | try { 82 | tr.doc.descendants((node, pos) => { 83 | if (node.type.name == this.name) { 84 | commands.deleteRange({ from: pos, to: pos + node.nodeSize }) 85 | return false; 86 | } 87 | }) 88 | } catch (e) { 89 | console.log(e) 90 | } 91 | }, 92 | cancelInlineCompletion: (options: any) => ({ commands }: { commands: any }) => { 93 | commands.deleteNode(this.name) 94 | } 95 | } 96 | }, 97 | addNodeView() { 98 | return ReactNodeViewRenderer(InlineCompletionView); 99 | }, 100 | }); 101 | 102 | const InlineCompletionView = (props?: { editor: Editor }) => { 103 | const $container = useRef(); 104 | 105 | // handle for esc 106 | useEffect(() => { 107 | const handleKeyDown = (event: KeyboardEvent) => { 108 | if (event.key === "Escape") { 109 | props?.editor?.commands.cancelInlineCompletion(); 110 | } 111 | }; 112 | 113 | document.addEventListener("keydown", handleKeyDown); 114 | 115 | return () => { 116 | document.removeEventListener("keydown", handleKeyDown); 117 | }; 118 | }, [props?.editor?.commands]); 119 | 120 | return ( 121 | 122 | type ` to completion 123 | 124 | ); 125 | }; 126 | 127 | -------------------------------------------------------------------------------- /web/core/lib/editor/extensions/quick-box/quick-box-extension.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * license: MIT 3 | * author: Tiptap 4 | * origin: https://github.com/ueberdosis/tiptap/blob/develop/packages/extension-code-block/src/code-block.ts 5 | */ 6 | import { 7 | RawCommands, 8 | ReactNodeViewRenderer, 9 | } from "@tiptap/react"; 10 | import { Node } from "@tiptap/core"; 11 | 12 | import QuickBoxViewWrapper from "./quick-box-view-wrapper"; 13 | 14 | const extensionName = "quick-command"; 15 | 16 | declare module "@tiptap/core" { 17 | interface Commands { 18 | setAiBlock: { 19 | setAiBlock: (attributes: Record) => ReturnType; 20 | }; 21 | toggleAiBlock: { 22 | toggleAiBlock: (attributes: Record) => ReturnType; 23 | }; 24 | enableEnter: { 25 | enableEnter: () => ReturnType; 26 | }; 27 | } 28 | } 29 | 30 | export const createQuickBox = () => { 31 | let isEditInChild = false; 32 | 33 | return Node.create({ 34 | name: extensionName, 35 | group: "block", 36 | defining: true, 37 | content: "text*", 38 | 39 | addCommands(): Partial { 40 | return { 41 | setAiBlock: 42 | (attributes) => 43 | ({ commands }) => { 44 | return commands.setNode(this.name, attributes); 45 | }, 46 | toggleAiBlock: 47 | (attributes) => 48 | ({ commands }) => { 49 | return commands.toggleNode(this.name, "paragraph", attributes); 50 | }, 51 | enableEnter: 52 | () => 53 | ({ commands, editor }) => { 54 | isEditInChild = false; 55 | commands.focus(); 56 | editor.setEditable(true); 57 | return true; 58 | }, 59 | }; 60 | }, 61 | addNodeView: function () { 62 | return ReactNodeViewRenderer(QuickBoxViewWrapper); 63 | }, 64 | addKeyboardShortcuts() { 65 | return { 66 | "Mod-/": (): boolean => { 67 | isEditInChild = true; 68 | this.editor.commands.insertContent([ 69 | { 70 | type: "paragraph", 71 | content: [], 72 | }, 73 | ]); 74 | this.editor.setEditable(false); 75 | return this.editor.commands.toggleAiBlock({}); 76 | }, 77 | Backspace: () => { 78 | const { empty, $anchor } = this.editor.state.selection; 79 | const isAtStart = $anchor.pos === 1; 80 | 81 | if (!empty || $anchor.parent.type.name !== this.name) { 82 | return false; 83 | } 84 | 85 | if (isAtStart || !$anchor.parent.textContent.length) { 86 | return !!this.editor.commands.clearNodes(); 87 | } 88 | 89 | return false; 90 | }, 91 | Escape: (): boolean => { 92 | if (isEditInChild) { 93 | this.editor.setEditable(true); 94 | return this.editor.commands.focus(); 95 | } 96 | 97 | return false; 98 | }, 99 | Enter: ({ editor }): boolean => { 100 | if (isEditInChild) return true; 101 | 102 | if (!this.options.exitOnTripleEnter) { 103 | return false; 104 | } 105 | 106 | const { state } = editor; 107 | const { selection } = state; 108 | const { $from, empty } = selection; 109 | 110 | if (!empty || $from.parent.type !== this.type) { 111 | return false; 112 | } 113 | 114 | const isAtEnd = $from.parentOffset === $from.parent.nodeSize - 2; 115 | const endsWithDoubleNewline = 116 | $from.parent.textContent.endsWith("\n\n"); 117 | 118 | if (!isAtEnd || !endsWithDoubleNewline) { 119 | return false; 120 | } 121 | 122 | return editor 123 | .chain() 124 | .command(({ tr }) => { 125 | tr.delete($from.pos - 2, $from.pos); 126 | 127 | return true; 128 | }) 129 | .exitCode() 130 | .run(); 131 | }, 132 | }; 133 | }, 134 | }); 135 | }; 136 | -------------------------------------------------------------------------------- /web/core/lib/editor/extensions/quick-box/quick-box-view-wrapper.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from "react"; 2 | import { NodeViewWrapper } from "@tiptap/react"; 3 | import { QuickBoxView } from "./quick-box-view"; 4 | import { Editor } from "@tiptap/core"; 5 | 6 | const QuickBoxViewWrapper = (props?: { editor: Editor }) => { 7 | const $container = useRef(); 8 | 9 | return ( 10 | 11 | { 14 | props?.editor?.commands.toggleAiBlock({}); 15 | props?.editor?.commands.enableEnter(); 16 | }} 17 | go={(content: string) => { 18 | props?.editor?.commands.enableEnter(); 19 | props?.editor?.commands.toggleAiBlock({}); 20 | props?.editor?.commands.callQuickAction(content); 21 | }}/> 22 | 23 | ); 24 | }; 25 | 26 | export default QuickBoxViewWrapper; 27 | -------------------------------------------------------------------------------- /web/core/lib/editor/extensions/quick-box/quick-box-view.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { CommandProps, EditorContent, Extension, useEditor, } from "@tiptap/react"; 3 | import StarterKit from "@tiptap/starter-kit"; 4 | import { EnterIcon } from "@radix-ui/react-icons"; 5 | 6 | import { BeSelect, BeSelectItem } from "@/editor/components/ui-select"; 7 | 8 | declare module "@tiptap/core" { 9 | interface Commands { 10 | callAi: { 11 | callAi: (text: string) => ReturnType; 12 | }; 13 | 14 | cancelAi: { 15 | cancelAi: () => ReturnType; 16 | }; 17 | } 18 | } 19 | 20 | export type AiBlockEditorProps = { 21 | content: string; 22 | cancel: () => void; 23 | go: (content: string) => void; 24 | }; 25 | 26 | export const QuickBoxView = ({ content, cancel, go }: AiBlockEditorProps) => { 27 | const ActionBar = Extension.create({ 28 | name: "actionBar", 29 | 30 | addCommands: () => ({ 31 | callAi: 32 | (text: string) => 33 | ({ commands }: CommandProps) => { 34 | go(text); 35 | 36 | return true; 37 | }, 38 | cancelAi: 39 | () => 40 | ({ commands }: CommandProps) => { 41 | cancel(); 42 | 43 | return false; 44 | }, 45 | }), 46 | addKeyboardShortcuts() { 47 | return { 48 | "Mod-Enter": () => { 49 | this.editor.commands.callAi(this.editor.getText() || ""); 50 | this.editor.view?.focus(); 51 | return true; 52 | }, 53 | Escape: () => { 54 | this.editor.commands.cancelAi(); 55 | return true; 56 | }, 57 | }; 58 | }, 59 | }); 60 | 61 | const extensions: any = [StarterKit, ActionBar]; 62 | const editor = useEditor({ 63 | extensions, 64 | content: content, 65 | editorProps: { 66 | attributes: { 67 | class: "prose ai-block-editor-inner", 68 | }, 69 | }, 70 | }); 71 | 72 | useEffect(() => { 73 | if (editor) { 74 | setTimeout(() => { 75 | if (editor) { 76 | editor.view.focus(); 77 | } 78 | }, 100); 79 | } 80 | }, [editor]); 81 | 82 | useEffect(() => { 83 | const keyDownHandler = (event: KeyboardEvent) => { 84 | if (event.key === "Enter") { 85 | event.preventDefault(); 86 | editor?.commands?.newlineInCode(); 87 | editor?.view?.focus(); 88 | } 89 | if (event.key === "Escape") { 90 | event.preventDefault(); 91 | editor?.commands?.cancelAi(); 92 | } 93 | }; 94 | 95 | document.addEventListener("keydown", keyDownHandler); 96 | return () => { 97 | document.removeEventListener("keydown", keyDownHandler); 98 | }; 99 | }, [editor]); 100 | 101 | return ( 102 |
103 | 104 | {editor && ( 105 |
106 |
107 | 108 |
109 | Text 110 | Image 111 |
112 |
113 |
114 |
115 | 122 | 130 |
131 |
132 | )} 133 |
134 | ); 135 | }; 136 | -------------------------------------------------------------------------------- /web/core/lib/editor/extensions/slash-command/slash-extension.ts: -------------------------------------------------------------------------------- 1 | import { ReactRenderer } from '@tiptap/react'; 2 | import { Node } from '@tiptap/core'; 3 | import { Suggestion } from '@tiptap/suggestion'; 4 | import tippy, { Instance } from 'tippy.js'; 5 | import SlashView from './slash-view'; 6 | import { PluginKey } from '@tiptap/pm/state'; 7 | import { FacetType } from '@/editor/defs/custom-action.type'; 8 | import { PromptsManager } from '@/editor/prompts/prompts-manager'; 9 | 10 | export const createSlashExtension = (promptsManager: PromptsManager = PromptsManager.getInstance()) => { 11 | const extensionName = 'ai-slash'; 12 | 13 | return Node.create({ 14 | name: "slash-command", 15 | addOptions() { 16 | return { 17 | char: "/", 18 | pluginKey: "slash", 19 | }; 20 | }, 21 | addProseMirrorPlugins() { 22 | return [ 23 | Suggestion({ 24 | editor: this.editor, 25 | char: this.options.char, 26 | pluginKey: new PluginKey(this.options.pluginKey), 27 | 28 | command: ({ editor, props }) => { 29 | const { state, dispatch } = editor.view; 30 | const { $head, $from } = state.selection; 31 | 32 | const end = $from.pos; 33 | const from = $head?.nodeBefore?.text 34 | ? end - 35 | $head.nodeBefore.text.substring( 36 | $head.nodeBefore.text.indexOf("/"), 37 | ).length 38 | : $from.start(); 39 | 40 | const tr = state.tr.deleteRange(from, end); 41 | dispatch(tr); 42 | editor.commands.runAiAction(props); 43 | editor?.view?.focus(); 44 | }, 45 | items: () => { 46 | const articleType = this.editor.commands.getArticleType(); 47 | return (promptsManager.getActions(FacetType.SLASH_COMMAND, articleType) || []); 48 | }, 49 | render: () => { 50 | let component: ReactRenderer; 51 | let popup: Instance[]; 52 | let isEditable: boolean; 53 | 54 | return { 55 | onStart: (props) => { 56 | isEditable = props.editor.isEditable; 57 | if (!isEditable) return; 58 | 59 | component = new ReactRenderer(SlashView, { 60 | props, 61 | editor: props.editor, 62 | }); 63 | 64 | popup = tippy("body", { 65 | getReferenceClientRect: 66 | props.clientRect || 67 | (() => props.editor.storage[extensionName].rect), 68 | appendTo: () => document.body, 69 | content: component.element, 70 | showOnCreate: true, 71 | interactive: true, 72 | trigger: "manual", 73 | placement: "bottom-start", 74 | }); 75 | }, 76 | 77 | onUpdate(props) { 78 | if (!isEditable) return; 79 | 80 | component.updateProps(props); 81 | if (props.editor?.storage[extensionName]) { 82 | props.editor.storage[extensionName].rect = props.clientRect!(); 83 | } 84 | 85 | popup[0].setProps({ 86 | getReferenceClientRect: props.clientRect, 87 | }); 88 | }, 89 | 90 | onKeyDown(props) { 91 | if (!isEditable) return; 92 | 93 | if (props.event.key === "Escape") { 94 | popup[0].hide(); 95 | return true; 96 | } 97 | return (component.ref as SlashView).onKeyDown(props); 98 | }, 99 | 100 | onExit() { 101 | if (!isEditable) return; 102 | popup && popup[0].destroy(); 103 | component.destroy(); 104 | }, 105 | }; 106 | }, 107 | }), 108 | ]; 109 | }, 110 | }); 111 | }; 112 | -------------------------------------------------------------------------------- /web/core/lib/editor/extensions/slash-command/slash-view.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * based on: 3 | * https://github.com/ueberdosis/tiptap/issues/1508 4 | * MIT License https://github.com/fantasticit/think/blob/main/packages/client/src/tiptap/core/extensions/slash.ts#L11 5 | * https://github.com/fantasticit/magic-editor/blob/main/src/extensions/slash/slash-menu-view.tsx#L68 6 | */ 7 | import React, { ElementRef, RefObject } from "react"; 8 | import i18next from "i18next"; 9 | 10 | class SlashView extends React.Component< 11 | { items: any; command: any }, 12 | { selectedIndex: number } 13 | > { 14 | $container: RefObject; 15 | 16 | constructor(props: { items: any; command: any }) { 17 | super(props); 18 | this.$container = React.createRef(); 19 | this.state = { 20 | selectedIndex: 0, 21 | }; 22 | } 23 | 24 | selectItem = (index: number) => { 25 | const { items, command } = this.props; 26 | const selectedCommand = items[index]; 27 | 28 | if (selectedCommand) { 29 | command(selectedCommand); 30 | } 31 | }; 32 | 33 | upHandler = () => { 34 | const { items } = this.props; 35 | this.setState((prevState: { selectedIndex: number }) => ({ 36 | selectedIndex: 37 | (prevState.selectedIndex + items.length - 1) % items.length, 38 | })); 39 | }; 40 | 41 | downHandler = () => { 42 | const { items } = this.props; 43 | this.setState((prevState: { selectedIndex: number }) => ({ 44 | selectedIndex: (prevState.selectedIndex + 1) % items.length, 45 | })); 46 | }; 47 | 48 | enterHandler = () => { 49 | this.selectItem(this.state.selectedIndex); 50 | }; 51 | 52 | componentDidMount() { 53 | this.setState({ selectedIndex: 0 }); 54 | } 55 | 56 | componentDidUpdate(prevProps: { items: any }) { 57 | if (prevProps.items !== this.props.items) { 58 | this.setState({ selectedIndex: 0 }); 59 | } 60 | 61 | const { selectedIndex } = this.state; 62 | if (!Number.isNaN(selectedIndex + 1)) { 63 | const el = this.$container?.current?.querySelector( 64 | `.slash-menu-item:nth-of-type(${selectedIndex + 1})`, 65 | ); 66 | el && el.scrollIntoView({ behavior: "smooth", scrollMode: "if-needed" }); 67 | } 68 | } 69 | 70 | onKeyDown = ({ event }: { event: KeyboardEvent }) => { 71 | if (event.key === "ArrowUp") { 72 | this.upHandler(); 73 | return true; 74 | } 75 | 76 | if (event.key === "ArrowDown") { 77 | this.downHandler(); 78 | return true; 79 | } 80 | 81 | if (event.key === "Enter") { 82 | this.enterHandler(); 83 | return true; 84 | } 85 | 86 | return false; 87 | }; 88 | 89 | render() { 90 | const { items } = this.props; 91 | const { selectedIndex } = this.state; 92 | 93 | return ( 94 |
95 | {items.map(({ name, i18Name }: any, idx: number) => ( 96 |
  • this.selectItem(idx)} 99 | className={ 100 | selectedIndex === idx 101 | ? "is-active DropdownMenuItem" 102 | : "DropdownMenuItem" 103 | } 104 | > 105 | {i18Name ? i18next.t(name) : name} 106 |
  • 107 | ))} 108 |
    109 | ); 110 | } 111 | } 112 | 113 | export default SlashView; 114 | -------------------------------------------------------------------------------- /web/core/lib/editor/hooks/useEditorContentChange.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { Editor } from "@tiptap/core"; 3 | 4 | export function useEditorContentChange(editor: Editor, callback: () => void) { 5 | useEffect(() => { 6 | editor.on("update", callback); 7 | 8 | return () => { 9 | editor.off("update", callback); 10 | }; 11 | }, [callback, editor]); 12 | } -------------------------------------------------------------------------------- /web/core/lib/editor/live-editor.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | 3 | import { EditorContent, useEditor } from '@tiptap/react'; 4 | import { Color } from '@tiptap/extension-color'; 5 | import ListItem from '@tiptap/extension-list-item'; 6 | import TextStyle from '@tiptap/extension-text-style'; 7 | import StarterKit from '@tiptap/starter-kit'; 8 | import { CharacterCount } from '@tiptap/extension-character-count'; 9 | import { Table } from '@tiptap/extension-table'; 10 | import { TableRow } from '@tiptap/extension-table-row'; 11 | import { TableCell } from '@tiptap/extension-table-cell'; 12 | import { TableHeader } from '@tiptap/extension-table-header'; 13 | 14 | import MarkdownIt from 'markdown-it'; 15 | import { useTranslation } from 'react-i18next'; 16 | import { useDebounce } from 'use-debounce'; 17 | import { Theme } from '@radix-ui/themes'; 18 | 19 | import './editor.css'; 20 | 21 | import { MenuBubble } from '@/editor/menu/menu-bubble'; 22 | import { createSlashExtension } from '@/editor/extensions/slash-command/slash-extension.ts'; 23 | import { createQuickBox } from '@/editor/extensions/quick-box/quick-box-extension'; 24 | import { AdviceExtension } from '@/editor/extensions/advice/advice-extension'; 25 | import { ToolbarMenu } from '@/editor/menu/toolbar-menu.tsx'; 26 | import { CustomEditorCommands } from '@/editor/action/custom-editor-commands.ts'; 27 | import { Sidebar } from '@/editor/components/sidebar.tsx'; 28 | import { Advice } from '@/editor/extensions/advice/advice'; 29 | import { AdviceManager } from '@/editor/extensions/advice/advice-manager'; 30 | import { AdviceView } from '@/editor/extensions/advice/advice-view'; 31 | import { Settings } from '@/editor/components/settings'; 32 | import { PromptsManager } from '@/editor/prompts/prompts-manager.ts'; 33 | import { AiActionExecutor } from '@/editor/action/AiActionExecutor.ts'; 34 | 35 | const md = new MarkdownIt(); 36 | 37 | export const setupExtensions = (promptsManager: PromptsManager, actionExecutor: AiActionExecutor) => { 38 | return [ 39 | // we define all commands here 40 | CustomEditorCommands(actionExecutor, promptsManager,), 41 | AdviceExtension.configure({ 42 | HTMLAttributes: { 43 | class: 'my-advice' 44 | }, 45 | setAdviceCommand: (advice: Advice) => { 46 | AdviceManager.getInstance().addAdvice(advice); 47 | }, 48 | onAdviceActivated: (adviceId) => { 49 | if (adviceId) AdviceManager.getInstance().setActiveId(adviceId); 50 | } 51 | }), 52 | // InlineCompletion, 53 | StarterKit.configure({ 54 | bulletList: { 55 | keepMarks: true, 56 | keepAttributes: false 57 | }, 58 | orderedList: { 59 | keepMarks: true, 60 | keepAttributes: false 61 | } 62 | }), 63 | createSlashExtension(promptsManager), 64 | createQuickBox(), 65 | CharacterCount.configure({}), 66 | Color.configure({ types: [TextStyle.name, ListItem.name] }), 67 | // @ts-ignore 68 | TextStyle.configure({ types: [ListItem.name] }), 69 | Table, 70 | TableRow, 71 | TableCell, 72 | TableHeader 73 | ]; 74 | }; 75 | 76 | const LiveEditor = () => { 77 | const { t } = useTranslation(); 78 | 79 | const actionExecutor = new AiActionExecutor(); 80 | const editor = useEditor({ 81 | extensions: setupExtensions(PromptsManager.getInstance(), actionExecutor), 82 | content: md.render(t('Editor Placeholder')), 83 | editorProps: { 84 | attributes: { 85 | class: 'prose lg:prose-xl bb-editor-inner' 86 | } 87 | } 88 | }); 89 | 90 | const [debouncedEditor] = useDebounce(editor?.state.doc.content, 2000); 91 | useEffect(() => { 92 | if (debouncedEditor) { 93 | localStorage.setItem('editor', JSON.stringify(editor.getJSON())); 94 | } 95 | }, [debouncedEditor]); 96 | 97 | useEffect(() => { 98 | const content = localStorage.getItem('editor'); 99 | if (content) { 100 | try { 101 | editor?.commands?.setContent(JSON.parse(content)); 102 | } catch (e) { 103 | editor?.commands?.setContent(md.render(t('Editor Placeholder'))); 104 | console.error(e); 105 | } 106 | } 107 | }, [editor]); 108 | 109 | return 110 |
    111 | {editor &&
    } 112 | 113 |
    114 | {editor && } 115 | {editor && } 116 |
    117 | 118 |
    {editor && }
    119 | 120 | {editor &&
    121 |

    {editor.storage.characterCount.characters()} characters

    122 |

    {editor.storage.characterCount.words()} words

    123 |
    124 | } 125 |
    126 |
    127 | {editor && } 128 |
    129 |
    ; 130 | }; 131 | 132 | export default LiveEditor; 133 | -------------------------------------------------------------------------------- /web/core/lib/editor/menu/menu-bubble.tsx: -------------------------------------------------------------------------------- 1 | import { BubbleMenu } from '@tiptap/react'; 2 | import React, { useEffect } from 'react'; 3 | import { Editor } from '@tiptap/core'; 4 | import { BookmarkIcon, CookieIcon } from '@radix-ui/react-icons'; 5 | import { Button } from '@radix-ui/themes'; 6 | import { 7 | ChangeForm, 8 | DefinedVariable, 9 | FacetType, 10 | OutputForm, 11 | PromptAction 12 | } from '@/editor/defs/custom-action.type'; 13 | import { newAdvice } from '@/editor/extensions/advice/advice'; 14 | import { ToolbarMenu } from '@/editor/menu/toolbar-menu'; 15 | import { BounceLoader } from 'react-spinners'; 16 | 17 | function innerSmartActions() : PromptAction[] { 18 | const innerSmartMenus: PromptAction[] = []; 19 | 20 | innerSmartMenus.push({ 21 | name: '扩写', 22 | template: `根据如下的内容扩写,只返回三句,限 100 字以内。###\${${DefinedVariable.SELECTION}}###。`, 23 | facetType: FacetType.BUBBLE_MENU, 24 | changeForm: ChangeForm.DIFF, 25 | outputForm: OutputForm.TEXT 26 | }); 27 | 28 | innerSmartMenus.push({ 29 | name: '润色', 30 | template: `请润色并重写如下的内容:###\${${DefinedVariable.SELECTION}}###`, 31 | facetType: FacetType.BUBBLE_MENU, 32 | changeForm: ChangeForm.DIFF, 33 | outputForm: OutputForm.TEXT 34 | }); 35 | 36 | return innerSmartMenus; 37 | } 38 | 39 | export const MenuBubble = ({ editor, customActions } : { 40 | editor: Editor, 41 | customActions?: PromptAction[] 42 | }) => { 43 | const [loading, setLoading] = React.useState(false); 44 | const [isOpen, setIsOpen] = React.useState(false); 45 | 46 | const [smartMenus, setSmartMenus] = React.useState(customActions ? innerSmartActions().concat(customActions) : innerSmartActions); 47 | const [menus, setMenus] = React.useState([]); 48 | 49 | useEffect(() => { 50 | setMenus(editor?.commands?.getAiActions(FacetType.BUBBLE_MENU) || []); 51 | }, [editor, isOpen]); 52 | 53 | const handleToggle = React.useCallback(() => { 54 | setIsOpen(!isOpen); 55 | }, [isOpen]); 56 | 57 | return 58 |
    59 |
    60 | {loading && } 61 | {!loading && 65 | } 66 |
    67 |
    68 | 69 |
    70 |
    71 |
    72 | {isOpen && (
      73 | {smartMenus?.map((menu, index) => { 74 | return
    • 75 | 98 |
    • ; 99 | })} 100 | 101 | {menus?.map((menu, index) => { 102 | return
    • 103 | 114 |
    • ; 115 | })} 116 |
    117 | )} 118 |
    119 |
    ; 120 | }; 121 | -------------------------------------------------------------------------------- /web/core/lib/editor/menu/toolbar-ai-dropdown.tsx: -------------------------------------------------------------------------------- 1 | import { CookieIcon } from "@radix-ui/react-icons"; 2 | import React, { useEffect } from "react"; 3 | import { Editor } from "@tiptap/core"; 4 | import { Button, DropdownMenu } from "@radix-ui/themes"; 5 | 6 | import { FacetType } from "@/editor/defs/custom-action.type"; 7 | 8 | export const ToolbarAiDropdown = ({ editor }: { 9 | editor: Editor 10 | }) => { 11 | const [menus, setMenus] = React.useState([]); 12 | 13 | useEffect(() => { 14 | setMenus(editor?.commands?.getAiActions(FacetType.TOOLBAR_MENU)); 15 | }, [editor]); 16 | 17 | return ( 18 | { 19 | let aiActions = editor?.commands?.getAiActions(FacetType.TOOLBAR_MENU); 20 | setMenus(aiActions) 21 | }}> 22 | 23 | 27 | 28 | 29 | {menus?.map((menu, index) => { 30 | return { 34 | editor.chain().callLlm(menu); 35 | }} 36 | > 37 | {menu.name} 38 | 39 | })} 40 | 41 | 42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /web/core/lib/editor/menu/toolbar-menu.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | // TODO: Fix this 3 | 4 | import { 5 | ActivityLogIcon, 6 | CodeIcon, 7 | DividerHorizontalIcon, 8 | FontBoldIcon, 9 | FontItalicIcon, 10 | ListBulletIcon, 11 | QuoteIcon, 12 | StrikethroughIcon, 13 | TextIcon 14 | } from '@radix-ui/react-icons' 15 | import React from 'react' 16 | import * as ToggleGroup from '@radix-ui/react-toggle-group' 17 | import { Editor } from "@tiptap/core" 18 | import { Theme } from '@radix-ui/themes' 19 | import { ToolbarAiDropdown } from '@/editor/menu/toolbar-ai-dropdown' 20 | 21 | export interface ToolbarProps { 22 | editor: Editor 23 | isBubbleMenu?: boolean // 是否是气泡菜单 24 | className?: string 25 | } 26 | 27 | export const ToolbarMenu = ({ editor, isBubbleMenu = false, className }: ToolbarProps) => { 28 | return 29 | 35 | editor.chain().focus().toggleBold().run()} 37 | disabled={!editor.can().chain().focus().toggleBold().run()} 38 | className={editor.isActive('bold') ? 'is-active toggle-group-item' : 'toggle-group-item'} 39 | value="left" aria-label="Left aligned" 40 | > 41 | 42 | 43 | editor.chain().focus().toggleItalic().run()} 45 | disabled={!editor.can().chain().focus().toggleItalic().run()} 46 | className={editor.isActive('italic') ? 'is-active toggle-group-item' : 'toggle-group-item'} 47 | value="center" aria-label="Center aligned" 48 | > 49 | 50 | 51 | editor.chain().focus().toggleStrike().run()} 53 | disabled={!editor.can().chain().focus().toggleStrike().run()} 54 | className={editor.isActive('strike') ? 'is-active toggle-group-item' : 'toggle-group-item'} 55 | value="center" aria-label="Center aligned" 56 | > 57 | 58 | 59 | editor.chain().focus().toggleCode().run()} 61 | disabled={!editor.can().chain().focus().toggleCode().run()} 62 | className={editor.isActive('code') ? 'is-active toggle-group-item' : 'toggle-group-item'} 63 | value="center" aria-label="Center aligned" 64 | > 65 | 66 | 67 | editor.chain().focus().setParagraph().run()} 69 | className={editor.isActive('paragraph') ? 'is-active toggle-group-item' : 'toggle-group-item'} 70 | value="center" aria-label="Center aligned" 71 | > 72 | 73 | 74 | editor.chain().focus().toggleHeading({ level: 1 }).run()} 76 | className={editor.isActive('heading', { level: 1 }) ? 'is-active toggle-group-item' : 'toggle-group-item'} 77 | value="center" aria-label="Center aligned" 78 | > 79 | H1 80 | 81 | editor.chain().focus().toggleHeading({ level: 2 }).run()} 83 | className={editor.isActive('heading', { level: 2 }) ? 'is-active toggle-group-item' : 'toggle-group-item'} 84 | value="center" aria-label="Center aligned" 85 | > 86 | H2 87 | 88 | editor.chain().focus().toggleHeading({ level: 3 }).run()} 90 | className={editor.isActive('heading', { level: 3 }) ? 'is-active toggle-group-item' : 'toggle-group-item'} 91 | value="center" aria-label="Center aligned" 92 | > 93 | H3 94 | 95 | editor.chain().focus().toggleHeading({ level: 4 }).run()} 97 | className={editor.isActive('heading', { level: 4 }) ? 'is-active toggle-group-item' : 'toggle-group-item'} 98 | value="center" aria-label="Center aligned" 99 | > 100 | H4 101 | 102 | editor.chain().focus().toggleBulletList().run()} 104 | className={editor.isActive('bulletList') ? 'is-active toggle-group-item' : 'toggle-group-item'} 105 | value="center" aria-label="Center aligned" 106 | > 107 | 108 | 109 | editor.chain().focus().toggleOrderedList().run()} 111 | className={editor.isActive('orderedList') ? 'is-active toggle-group-item' : 'toggle-group-item'} 112 | value="center" aria-label="Center aligned" 113 | > 114 | 115 | 116 | editor.chain().focus().toggleCodeBlock().run()} 118 | className={editor.isActive('codeBlock') ? 'is-active toggle-group-item' : 'toggle-group-item'} 119 | value="center" aria-label="Center aligned" 120 | > 121 | 122 | 123 | editor.chain().focus().toggleBlockquote().run()} 125 | className={editor.isActive('blockquote') ? 'is-active toggle-group-item' : 'toggle-group-item'} 126 | value="center" aria-label="Center aligned" 127 | > 128 | 129 | 130 | editor.chain().focus().setHorizontalRule().run()} 132 | className={'toggle-group-item'} 133 | value="center" aria-label="Center aligned" 134 | > 135 | 136 | 137 | 138 | { !isBubbleMenu && <> 139 |
    140 | 141 | } 142 | 143 | 144 | } 145 | -------------------------------------------------------------------------------- /web/core/lib/editor/prompts/TemplateRender.ts: -------------------------------------------------------------------------------- 1 | export function render(template: string, data: Record): string { 2 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 3 | // @ts-expect-error 4 | return template.replace(/\$\{([\s\S]+?)\}/g, (match, p1) => { 5 | const keys = p1.trim().split('.'); 6 | let value = data; 7 | for (const key of keys) { 8 | value = value[key]; 9 | if (value === undefined) { 10 | return ''; 11 | } 12 | } 13 | return value; 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /web/core/lib/editor/prompts/article-prompts.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DefinedVariable, 3 | FacetType, 4 | OutputForm, 5 | PromptAction, 6 | } from "@/editor/defs/custom-action.type"; 7 | 8 | export const ToolbarMenuPrompts: PromptAction[] = [ 9 | { 10 | name: 'Generate Outline', 11 | i18Name: true, 12 | template: `You are an assistant helping a user to generate an outline. Output in markdown format. ###\${${DefinedVariable.BEFORE_CURSOR}}###`, 13 | facetType: FacetType.TOOLBAR_MENU, 14 | outputForm: OutputForm.STREAMING, 15 | }, 16 | { 17 | name: 'Continue writing', 18 | i18Name: true, 19 | template: `You are an assistant helping a user write a document. Output how the document continues, no more than 3 sentences. ###\${${DefinedVariable.BEFORE_CURSOR}}###`, 20 | facetType: FacetType.TOOLBAR_MENU, 21 | outputForm: OutputForm.STREAMING, 22 | }, 23 | { 24 | name: 'Help Me Write', 25 | i18Name: true, 26 | template: ` You are an assistant helping a user write more content in a document based on a prompt. Output in markdown format. ###\${${DefinedVariable.BEFORE_CURSOR}}###`, 27 | facetType: FacetType.TOOLBAR_MENU, 28 | outputForm: OutputForm.STREAMING, 29 | }, 30 | { 31 | name: 'Spelling and Grammar', 32 | i18Name: true, 33 | template: `You are an assistant helping a user to check spelling and grammar. Output in markdown format. ###\${${DefinedVariable.BEFORE_CURSOR}}###`, 34 | facetType: FacetType.TOOLBAR_MENU, 35 | outputForm: OutputForm.STREAMING, 36 | }, 37 | ]; 38 | 39 | export const BubbleMenuPrompts: PromptAction[] = []; 40 | 41 | export const SlashCommandsPrompts: PromptAction[] = [ 42 | { 43 | name: 'Summarize', 44 | i18Name: true, 45 | template: `You are an assistant helping to summarize a article. Output in markdown format. \n ###\${${DefinedVariable.BEFORE_CURSOR}}###`, 46 | facetType: FacetType.SLASH_COMMAND, 47 | outputForm: OutputForm.STREAMING 48 | }, 49 | { 50 | name: 'Continue writing', 51 | i18Name: true, 52 | template: `You are an assistant helping a user write a document. Output how the document continues, no more than 3 sentences. ###\${${DefinedVariable.BEFORE_CURSOR}}###`, 53 | facetType: FacetType.SLASH_COMMAND, 54 | outputForm: OutputForm.STREAMING, 55 | } 56 | ] 57 | 58 | const ArticlePrompts: PromptAction[] = [ 59 | ToolbarMenuPrompts, 60 | BubbleMenuPrompts, 61 | SlashCommandsPrompts 62 | ].flat(); 63 | 64 | export default ArticlePrompts; 65 | -------------------------------------------------------------------------------- /web/core/lib/editor/prompts/prompts-manager.ts: -------------------------------------------------------------------------------- 1 | import i18next from 'i18next'; 2 | import { DefinedVariable, FacetType, PromptAction } from '@/editor/defs/custom-action.type'; 3 | import ArticlePrompts from '@/editor/prompts/article-prompts'; 4 | import { TypeOptions } from '@/editor/defs/type-options.type'; 5 | import RequirementsPrompts from '@/editor/prompts/requirements-prompts'; 6 | import { render } from '@/editor/prompts/TemplateRender.ts'; 7 | 8 | export class PromptsManager { 9 | private constructor() { 10 | } 11 | 12 | private static instance: PromptsManager; 13 | 14 | public static getInstance(): PromptsManager { 15 | if (!PromptsManager.instance) { 16 | PromptsManager.instance = new PromptsManager(); 17 | } 18 | 19 | return PromptsManager.instance; 20 | } 21 | 22 | actionsMap = { 23 | "article": ArticlePrompts, 24 | "requirements": RequirementsPrompts 25 | } 26 | 27 | getActions(type: FacetType, articleType: TypeOptions): PromptAction[] { 28 | let typedPrompts: PromptAction[] = [] 29 | 30 | if (articleType?.value) { 31 | typedPrompts = this.actionsMap[articleType.value] 32 | } 33 | 34 | const actions = typedPrompts.filter(prompt => prompt.facetType === type); 35 | return actions.map(prompt => { 36 | if (prompt.i18Name) { 37 | prompt.name = i18next.t(prompt.name) 38 | } 39 | 40 | return prompt 41 | }) 42 | } 43 | 44 | variableList(): string[] { 45 | return Object.values(DefinedVariable); 46 | } 47 | 48 | updateActionsMap(articleType: string, prompts: PromptAction[]) { 49 | this.actionsMap[articleType] = prompts 50 | } 51 | 52 | compile(string: string, data: object) { 53 | const template = render(string, data) 54 | console.log(template) 55 | return template 56 | } 57 | 58 | saveBackgroundContext(context: string) { 59 | (this as any).backgroundContext = context 60 | } 61 | } 62 | 63 | -------------------------------------------------------------------------------- /web/core/lib/editor/prompts/requirements-prompts.ts: -------------------------------------------------------------------------------- 1 | import { ChangeForm, DefinedVariable, FacetType, OutputForm, PromptAction } from "@/editor/defs/custom-action.type"; 2 | 3 | const ToolbarMenuPrompts: PromptAction[] = [ 4 | { 5 | name: 'Generate Requirements', 6 | i18Name: true, 7 | template: `你是一个产品经理。请编写一个 ###{{${DefinedVariable.TITLE}}}###的需求文档大纲。大纲包含:产品简介、需求流程图、数据项描述、验收条件。`, 8 | facetType: FacetType.TOOLBAR_MENU, 9 | outputForm: OutputForm.STREAMING, 10 | } 11 | ]; 12 | 13 | const BubbleMenuPrompts: PromptAction[] = [ 14 | { 15 | name: '细化需求', 16 | i18Name: true, 17 | template: `你是一个产品经理。请细化这些需求 \n ###{{${DefinedVariable.SELECTION}}}###`, 18 | facetType: FacetType.BUBBLE_MENU, 19 | outputForm: OutputForm.STREAMING, 20 | changeForm: ChangeForm.DIFF, 21 | } 22 | ]; 23 | 24 | const SlashCommandsPrompts: PromptAction[] = [ 25 | { 26 | name: '需求细化', 27 | i18Name: true, 28 | template: `You are an assistant helping to summarize a article. Output in markdown format. \n ###{{${DefinedVariable.BEFORE_CURSOR}}}###`, 29 | facetType: FacetType.SLASH_COMMAND, 30 | outputForm: OutputForm.STREAMING 31 | }, 32 | { 33 | name: '你是一个测试专家。生成验收条件', 34 | i18Name: true, 35 | template: `使用的表格形式生成验收条件: 36 | 1、用例编号:A(产品项目名)—B(用例属性)—C(测试需求标识)—D(编号数字) 37 | 2、测试模块:测试用例对应的功能模块 38 | 3、测试标题:概括描述测试用例关注点 39 | 4、重要级别:高、中、低三个级别,高级别测试用例一般用在冒烟测试阶段 40 | 5、预置条件:执行该用例的先决条件 41 | 6、测试数据:测试输入 42 | 7、操作步骤:需明确给出每一个步骤的详细描述 43 | 8、预期结果:(最重要部分) 44 | 45 | 。需求信息: ###{{${DefinedVariable.BEFORE_CURSOR}}}###`, 46 | facetType: FacetType.SLASH_COMMAND, 47 | outputForm: OutputForm.STREAMING 48 | }, 49 | { 50 | name: '生成流程图', 51 | i18Name: true, 52 | template: `你是一个产品经理。根据业务信息,使用 PlantUML 绘制流程图。\n ###{{${DefinedVariable.ALL}}}###`, 53 | facetType: FacetType.SLASH_COMMAND, 54 | outputForm: OutputForm.STREAMING 55 | }, 56 | { 57 | name: '生成需求大纲', 58 | i18Name: true, 59 | template: `你是一个产品经理。请编写一个 ###{{${DefinedVariable.TITLE}}}###的需求文档大纲。大纲包含:产品简介、需求流程图、数据项描述、验收条件`, 60 | facetType: FacetType.SLASH_COMMAND, 61 | outputForm: OutputForm.STREAMING, 62 | } 63 | ] 64 | 65 | const RequirementsPrompts: PromptAction[] = [ 66 | ToolbarMenuPrompts, 67 | BubbleMenuPrompts, 68 | SlashCommandsPrompts 69 | ].flat(); 70 | 71 | export default RequirementsPrompts; 72 | -------------------------------------------------------------------------------- /web/core/lib/editor/typeing.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.module.css" { 2 | const classes: { [key: string]: string }; 3 | export default classes; 4 | } -------------------------------------------------------------------------------- /web/core/lib/main.ts: -------------------------------------------------------------------------------- 1 | export * from '@/editor/live-editor'; 2 | 3 | export { default as LiveEditor } from '@/editor/live-editor'; 4 | export { setupExtensions } from '@/editor/live-editor'; 5 | export { ToolbarMenu } from '@/editor/menu/toolbar-menu'; 6 | export { PromptsManager } from '@/editor/prompts/prompts-manager.ts'; 7 | export { InlineCompletion } from '@/editor/extensions/inline-completion/inline-completion'; 8 | export { MenuBubble } from '@/editor/menu/menu-bubble'; 9 | export { createSlashExtension } from '@/editor/extensions/slash-command/slash-extension.ts'; 10 | export { createQuickBox } from '@/editor/extensions/quick-box/quick-box-extension'; 11 | export { AdviceExtension } from '@/editor/extensions/advice/advice-extension'; 12 | export { CustomEditorCommands } from '@/editor/action/custom-editor-commands.ts'; 13 | export { Sidebar } from '@/editor/components/sidebar.tsx'; 14 | export { AdviceManager } from '@/editor/extensions/advice/advice-manager'; 15 | export { AdviceView } from '@/editor/extensions/advice/advice-view'; 16 | export { newAdvice } from '@/editor/extensions/advice/advice'; 17 | export type { Advice } from '@/editor/extensions/advice/advice'; 18 | export { Settings } from '@/editor/components/settings'; 19 | export { AiActionExecutor } from '@/editor/action/AiActionExecutor.ts'; 20 | export { OutputForm } from '@/editor/defs/custom-action.type'; 21 | export { actionPosition, PromptCompiler } from '@/editor/action/PromptCompiler'; 22 | export { BuiltinFunctionExecutor } from '@/editor/action/BuiltinFunctionExecutor'; 23 | export { 24 | default as ArticlePrompts, ToolbarMenuPrompts, BubbleMenuPrompts, SlashCommandsPrompts 25 | } from '@/editor/prompts/article-prompts'; 26 | 27 | /// export all types 28 | export * from '@/editor/defs/custom-action.type'; 29 | -------------------------------------------------------------------------------- /web/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@studio-b3/web-core", 3 | "version": "0.9.1", 4 | "type": "module", 5 | "main": "dist/main.js", 6 | "types": "dist-types/main.d.ts", 7 | "files": [ 8 | "dist", 9 | "dist-types", 10 | "lib" 11 | ], 12 | "exports": { 13 | ".": { 14 | "import": "./dist/main.js", 15 | "types": "./dist-types/main.d.ts" 16 | }, 17 | "./package.json": "./package.json", 18 | "./style.css": "./dist/assets/main.css" 19 | }, 20 | "typesVersions": { 21 | "*": { 22 | "*": [ 23 | "./dist-types/main.d.ts", 24 | "./dist-types/*" 25 | ] 26 | } 27 | }, 28 | "sideEffects": [ 29 | "**/*.css" 30 | ], 31 | "scripts": { 32 | "prepublishOnly": "npm run build", 33 | "watch": "vite build --watch", 34 | "build": "vite build", 35 | "lint": "eslint . --ext ts,tsx,.cjs --report-unused-disable-directives --max-warnings 0", 36 | "lint:fix": "eslint . --ext .ts,.cjs --fix --fix-type [problem,suggestion]", 37 | "fmt": "prettier --write \"**/*.{ts,.cjs,json,,md}\" --log-level warn" 38 | }, 39 | "peerDependencies": { 40 | "react": "^18.2.0", 41 | "react-dom": "^18.2.0" 42 | }, 43 | "resolutions": { 44 | "@tiptap/core": "^2.1.12", 45 | "@tiptap/react": "^2.1.12" 46 | }, 47 | "dependencies": { 48 | "@radix-ui/colors": "^3.0.0", 49 | "@radix-ui/popper": "^0.1.0", 50 | "@radix-ui/react-accordion": "^1.1.2", 51 | "@radix-ui/react-dialog": "^1.0.5", 52 | "@radix-ui/react-dropdown-menu": "^2.0.6", 53 | "@radix-ui/react-icons": "^1.3.0", 54 | "@radix-ui/react-popover": "^1.0.7", 55 | "@radix-ui/react-select": "^2.0.0", 56 | "@radix-ui/react-tabs": "^1.0.4", 57 | "@radix-ui/react-toggle": "^1.0.3", 58 | "@radix-ui/react-toggle-group": "^1.0.4", 59 | "@radix-ui/themes": "^2.0.1", 60 | "@tiptap/core": "2.1.12", 61 | "@tiptap/extension-blockquote": "^2.1.12", 62 | "@tiptap/extension-bold": "^2.1.12", 63 | "@tiptap/extension-bubble-menu": "^2.1.12", 64 | "@tiptap/extension-bullet-list": "^2.1.12", 65 | "@tiptap/extension-character-count": "^2.1.12", 66 | "@tiptap/extension-code": "^2.1.12", 67 | "@tiptap/extension-code-block-lowlight": "^2.1.12", 68 | "@tiptap/extension-collaboration": "^2.1.12", 69 | "@tiptap/extension-collaboration-cursor": "^2.1.12", 70 | "@tiptap/extension-color": "^2.1.12", 71 | "@tiptap/extension-document": "2.1.12", 72 | "@tiptap/extension-dropcursor": "^2.1.12", 73 | "@tiptap/extension-floating-menu": "^2.1.12", 74 | "@tiptap/extension-gapcursor": "^2.1.12", 75 | "@tiptap/extension-hard-break": "^2.1.12", 76 | "@tiptap/extension-highlight": "^2.1.12", 77 | "@tiptap/extension-horizontal-rule": "^2.1.12", 78 | "@tiptap/extension-image": "^2.1.12", 79 | "@tiptap/extension-italic": "^2.1.12", 80 | "@tiptap/extension-link": "^2.1.12", 81 | "@tiptap/extension-list-item": "^2.1.12", 82 | "@tiptap/extension-ordered-list": "^2.1.12", 83 | "@tiptap/extension-paragraph": "^2.1.12", 84 | "@tiptap/extension-placeholder": "^2.1.12", 85 | "@tiptap/extension-strike": "^2.1.12", 86 | "@tiptap/extension-subscript": "^2.1.12", 87 | "@tiptap/extension-superscript": "^2.1.12", 88 | "@tiptap/extension-table": "^2.1.13", 89 | "@tiptap/extension-table-cell": "^2.1.13", 90 | "@tiptap/extension-table-header": "^2.1.13", 91 | "@tiptap/extension-table-row": "^2.1.13", 92 | "@tiptap/extension-task-item": "^2.1.12", 93 | "@tiptap/extension-task-list": "^2.1.12", 94 | "@tiptap/extension-text": "^2.1.12", 95 | "@tiptap/extension-text-style": "^2.1.12", 96 | "@tiptap/extension-typography": "^2.1.12", 97 | "@tiptap/pm": "^2.1.13", 98 | "@tiptap/react": "^2.1.12", 99 | "@tiptap/starter-kit": "^2.1.12", 100 | "@tiptap/suggestion": "^2.1.12", 101 | "clsx": "^2.0.0", 102 | "i18next": "^23.7.6", 103 | "i18next-browser-languagedetector": "^7.2.0", 104 | "markdown-it": "^13.0.2", 105 | "prosemirror-changeset": "^2.2.1", 106 | "prosemirror-keymap": "^1.2.2", 107 | "prosemirror-view": "^1.32.4", 108 | "react": "^18.2.0", 109 | "react-dom": "^18.2.0", 110 | "react-i18next": "^13.5.0", 111 | "react-select": "^5.8.0", 112 | "react-spinners": "^0.13.8", 113 | "scroll-into-view-if-needed": "^3.1.0", 114 | "tippy.js": "^6.3.7", 115 | "tiptap-markdown": "^0.8.7", 116 | "use-debounce": "^10.0.0", 117 | "uuid": "^9.0.1" 118 | }, 119 | "devDependencies": { 120 | "@types/prosemirror-keymap": "^1.2.0", 121 | "@types/prosemirror-model": "^1.17.0", 122 | "@types/prosemirror-view": "^1.24.0", 123 | "autoprefixer": "^10.0.1", 124 | "postcss": "8.4.31", 125 | "postcss-import": "^15.1.0", 126 | "postcss-nesting": "^12.0.1", 127 | "tailwindcss": "^3.3.0" 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /web/core/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /web/core/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ['./lib/**/*.{js,jsx,ts,tsx}'], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [], 8 | }; 9 | -------------------------------------------------------------------------------- /web/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "@/*": ["./lib/*"] 6 | }, 7 | "target": "ES2020", 8 | "useDefineForClassFields": true, 9 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 10 | "module": "ESNext", 11 | "skipLibCheck": true, 12 | /* Bundler mode */ 13 | "moduleResolution": "bundler", 14 | "allowImportingTsExtensions": true, 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "noEmit": true, 18 | "jsx": "react-jsx", 19 | /* Linting */ 20 | // "strict": true, 21 | // "noUnusedLocals": true, 22 | // "noUnusedParameters": true, 23 | "noFallthroughCasesInSwitch": true 24 | }, 25 | "include": ["lib"], 26 | "references": [ 27 | { 28 | "path": "./tsconfig.node.json" 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /web/core/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.mts"] 10 | } 11 | -------------------------------------------------------------------------------- /web/core/vite.config.mts: -------------------------------------------------------------------------------- 1 | // https://dev.to/receter/how-to-create-a-react-component-library-using-vites-library-mode-4lma 2 | 3 | import { defineConfig } from 'vite'; 4 | import react from '@vitejs/plugin-react'; 5 | import { resolve } from 'node:path'; 6 | import { libInjectCss } from 'vite-plugin-lib-inject-css'; 7 | import checker from 'vite-plugin-checker'; 8 | import dts from 'vite-plugin-dts'; 9 | import { externalizeDeps } from 'vite-plugin-externalize-deps'; 10 | 11 | // https://vitejs.dev/config/ 12 | export default defineConfig({ 13 | resolve: { 14 | alias: { 15 | '@': resolve(__dirname, 'lib'), 16 | }, 17 | }, 18 | plugins: [ 19 | checker({ 20 | typescript: true, 21 | }), 22 | externalizeDeps(), 23 | dts({ 24 | outDir: './dist-types', 25 | rollupTypes: true 26 | }), 27 | libInjectCss(), 28 | react(), 29 | ], 30 | build: { 31 | copyPublicDir: false, 32 | lib: { 33 | entry: resolve(__dirname, 'lib/main.ts'), 34 | formats: ['es'], 35 | }, 36 | rollupOptions: { 37 | output: { 38 | dir: 'dist', 39 | assetFileNames: 'assets/[name][extname]', 40 | entryFileNames: '[name].js', 41 | }, 42 | }, 43 | }, 44 | }); 45 | -------------------------------------------------------------------------------- /web/llmapi/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@studio-b3/llmapi", 3 | "version": "0.0.2", 4 | "type": "module", 5 | "main": "dist/index.mjs", 6 | "types": "dist-types/index.d.ts", 7 | "exports": { 8 | ".": { 9 | "import": "./dist/index.mjs" 10 | }, 11 | "./package.json": "./package.json" 12 | }, 13 | "typesVersions": { 14 | "*": { 15 | "*": [ 16 | "./dist-types/index.d.ts", 17 | "./dist-types/*" 18 | ] 19 | } 20 | }, 21 | "sideEffects": false, 22 | "files": [ 23 | "dist", 24 | "dist-types", 25 | "src" 26 | ], 27 | "scripts": { 28 | "watch": "vite build --watch", 29 | "build": "vite build", 30 | "lint": "eslint . --ext ts,tsx,.cjs --report-unused-disable-directives --max-warnings 0", 31 | "lint:fix": "eslint . --ext .ts,.cjs --fix --fix-type [problem,suggestion]" 32 | }, 33 | "dependencies": { 34 | "openai": "^4.20.0" 35 | }, 36 | "private": false, 37 | "devDependencies": {} 38 | } 39 | -------------------------------------------------------------------------------- /web/llmapi/src/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | OpenAI, 3 | OpenAIError, 4 | APIError, 5 | APIConnectionError, 6 | APIConnectionTimeoutError, 7 | APIUserAbortError, 8 | NotFoundError, 9 | ConflictError, 10 | RateLimitError, 11 | BadRequestError, 12 | AuthenticationError, 13 | InternalServerError, 14 | PermissionDeniedError, 15 | UnprocessableEntityError, 16 | } from 'openai'; 17 | 18 | export { ErnieAI, type ErnieAIOptions } from './ernie'; 19 | export { HunYuanAI, type HunYuanAIOptions } from './hunyuan'; 20 | export { MinimaxAI, type MinimaxAIOptions } from './minimax'; 21 | export { QWenAI, type QWenAIOptions } from './qwen'; 22 | export { VYroAI, type VYroAIOptions } from './vyro'; 23 | 24 | export * from './resource'; 25 | export * from './streaming'; 26 | export * from './util'; 27 | -------------------------------------------------------------------------------- /web/llmapi/src/resource.ts: -------------------------------------------------------------------------------- 1 | import { APIClient } from "openai/core"; 2 | 3 | export class APIResource { 4 | protected _client: Client; 5 | 6 | constructor(client: Client) { 7 | this._client = client; 8 | } 9 | } -------------------------------------------------------------------------------- /web/llmapi/src/util.ts: -------------------------------------------------------------------------------- 1 | export function ensureArray(value: T | T[]): T[] { 2 | return Array.isArray(value) ? value : [value]; 3 | } 4 | -------------------------------------------------------------------------------- /web/llmapi/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": [ 6 | "ES2020" 7 | ], 8 | "module": "ESNext", 9 | "skipLibCheck": true, 10 | /* Bundler mode */ 11 | "moduleResolution": "bundler", 12 | "allowImportingTsExtensions": true, 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "noEmit": true, 16 | /* Linting */ 17 | // "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true 21 | }, 22 | "include": [ 23 | "src" 24 | ], 25 | "references": [ 26 | { 27 | "path": "./tsconfig.node.json" 28 | } 29 | ] 30 | } -------------------------------------------------------------------------------- /web/llmapi/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.mts"] 10 | } 11 | -------------------------------------------------------------------------------- /web/llmapi/vite.config.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import checker from 'vite-plugin-checker'; 3 | import dts from 'vite-plugin-dts'; 4 | import { externalizeDeps } from 'vite-plugin-externalize-deps'; 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig({ 8 | plugins: [ 9 | checker({ 10 | typescript: true, 11 | }), 12 | externalizeDeps(), 13 | dts({ 14 | outDir: './dist-types', 15 | }), 16 | ], 17 | build: { 18 | copyPublicDir: false, 19 | lib: { 20 | entry: 'src/index.ts', 21 | formats: ['es'], 22 | }, 23 | rollupOptions: { 24 | output: { 25 | dir: 'dist', 26 | exports: 'named', 27 | entryFileNames: '[name].mjs', 28 | chunkFileNames: '[name].mjs', 29 | }, 30 | }, 31 | }, 32 | }); 33 | -------------------------------------------------------------------------------- /web/native-wrapper/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /web/native-wrapper/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"] 3 | } 4 | -------------------------------------------------------------------------------- /web/native-wrapper/README.md: -------------------------------------------------------------------------------- 1 | # Tauri + Preact + Typescript 2 | 3 | This template should help get you started developing with Tauri, Preact and Typescript in Vite. 4 | 5 | ## Recommended IDE Setup 6 | 7 | - [VS Code](https://code.visualstudio.com/) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) 8 | -------------------------------------------------------------------------------- /web/native-wrapper/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Tauri + Peact + TS 8 | 9 | 10 | 11 |
    12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /web/native-wrapper/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@studio-b3/web-native-wrapper", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview", 10 | "tauri": "tauri", 11 | "tauri:build": "tauri build" 12 | }, 13 | "dependencies": { 14 | "preact": "^10.16.0", 15 | "@tauri-apps/api": "^1.6.0" 16 | }, 17 | "devDependencies": { 18 | "@preact/preset-vite": "^2.5.0", 19 | "typescript": "^5.0.2", 20 | "vite": "5.2.6", 21 | "@tauri-apps/cli": "^1.5.6" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /web/native-wrapper/public/tauri.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /web/native-wrapper/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/native-wrapper/src-tauri/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | -------------------------------------------------------------------------------- /web/native-wrapper/src-tauri/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "native-wrapper" 3 | version = "0.0.0" 4 | description = "A Tauri App" 5 | authors = ["you"] 6 | license = "" 7 | repository = "" 8 | edition = "2021" 9 | 10 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 11 | 12 | [build-dependencies] 13 | tauri-build = { version = "1.8.0", features = [] } 14 | 15 | [dependencies] 16 | tauri = { version = "1.8.0", features = ["shell-open"] } 17 | serde = { version = "1.0", features = ["derive"] } 18 | serde_json = "1.0" 19 | 20 | [features] 21 | # this feature is used for production builds or when `devPath` points to the filesystem 22 | # DO NOT REMOVE!! 23 | custom-protocol = ["tauri/custom-protocol"] 24 | -------------------------------------------------------------------------------- /web/native-wrapper/src-tauri/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | tauri_build::build() 3 | } 4 | -------------------------------------------------------------------------------- /web/native-wrapper/src-tauri/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unit-mesh/autodev-editor/d6b82047072dd7344e6378a22c280bd374bff9ab/web/native-wrapper/src-tauri/icons/128x128.png -------------------------------------------------------------------------------- /web/native-wrapper/src-tauri/icons/128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unit-mesh/autodev-editor/d6b82047072dd7344e6378a22c280bd374bff9ab/web/native-wrapper/src-tauri/icons/128x128@2x.png -------------------------------------------------------------------------------- /web/native-wrapper/src-tauri/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unit-mesh/autodev-editor/d6b82047072dd7344e6378a22c280bd374bff9ab/web/native-wrapper/src-tauri/icons/32x32.png -------------------------------------------------------------------------------- /web/native-wrapper/src-tauri/icons/Square107x107Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unit-mesh/autodev-editor/d6b82047072dd7344e6378a22c280bd374bff9ab/web/native-wrapper/src-tauri/icons/Square107x107Logo.png -------------------------------------------------------------------------------- /web/native-wrapper/src-tauri/icons/Square142x142Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unit-mesh/autodev-editor/d6b82047072dd7344e6378a22c280bd374bff9ab/web/native-wrapper/src-tauri/icons/Square142x142Logo.png -------------------------------------------------------------------------------- /web/native-wrapper/src-tauri/icons/Square150x150Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unit-mesh/autodev-editor/d6b82047072dd7344e6378a22c280bd374bff9ab/web/native-wrapper/src-tauri/icons/Square150x150Logo.png -------------------------------------------------------------------------------- /web/native-wrapper/src-tauri/icons/Square284x284Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unit-mesh/autodev-editor/d6b82047072dd7344e6378a22c280bd374bff9ab/web/native-wrapper/src-tauri/icons/Square284x284Logo.png -------------------------------------------------------------------------------- /web/native-wrapper/src-tauri/icons/Square30x30Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unit-mesh/autodev-editor/d6b82047072dd7344e6378a22c280bd374bff9ab/web/native-wrapper/src-tauri/icons/Square30x30Logo.png -------------------------------------------------------------------------------- /web/native-wrapper/src-tauri/icons/Square310x310Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unit-mesh/autodev-editor/d6b82047072dd7344e6378a22c280bd374bff9ab/web/native-wrapper/src-tauri/icons/Square310x310Logo.png -------------------------------------------------------------------------------- /web/native-wrapper/src-tauri/icons/Square44x44Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unit-mesh/autodev-editor/d6b82047072dd7344e6378a22c280bd374bff9ab/web/native-wrapper/src-tauri/icons/Square44x44Logo.png -------------------------------------------------------------------------------- /web/native-wrapper/src-tauri/icons/Square71x71Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unit-mesh/autodev-editor/d6b82047072dd7344e6378a22c280bd374bff9ab/web/native-wrapper/src-tauri/icons/Square71x71Logo.png -------------------------------------------------------------------------------- /web/native-wrapper/src-tauri/icons/Square89x89Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unit-mesh/autodev-editor/d6b82047072dd7344e6378a22c280bd374bff9ab/web/native-wrapper/src-tauri/icons/Square89x89Logo.png -------------------------------------------------------------------------------- /web/native-wrapper/src-tauri/icons/StoreLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unit-mesh/autodev-editor/d6b82047072dd7344e6378a22c280bd374bff9ab/web/native-wrapper/src-tauri/icons/StoreLogo.png -------------------------------------------------------------------------------- /web/native-wrapper/src-tauri/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unit-mesh/autodev-editor/d6b82047072dd7344e6378a22c280bd374bff9ab/web/native-wrapper/src-tauri/icons/icon.icns -------------------------------------------------------------------------------- /web/native-wrapper/src-tauri/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unit-mesh/autodev-editor/d6b82047072dd7344e6378a22c280bd374bff9ab/web/native-wrapper/src-tauri/icons/icon.ico -------------------------------------------------------------------------------- /web/native-wrapper/src-tauri/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unit-mesh/autodev-editor/d6b82047072dd7344e6378a22c280bd374bff9ab/web/native-wrapper/src-tauri/icons/icon.png -------------------------------------------------------------------------------- /web/native-wrapper/src-tauri/src/main.rs: -------------------------------------------------------------------------------- 1 | // Prevents additional console window on Windows in release, DO NOT REMOVE!! 2 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] 3 | 4 | use tauri::{Manager, Size, LogicalSize}; 5 | 6 | // Learn more about Tauri commands at https://tauri.app/v1/guides/features/command 7 | #[tauri::command] 8 | fn greet(name: &str) -> String { 9 | format!("Hello, {}! You've been greeted from Rust!", name) 10 | } 11 | 12 | fn main() { 13 | tauri::Builder::default() 14 | .invoke_handler(tauri::generate_handler![greet]) 15 | .setup(|app| { 16 | let window = app.get_window("main").unwrap(); 17 | window.eval("window.location.replace('https://editor.unitmesh.cc/')").unwrap(); 18 | window.set_size(Size::Logical(LogicalSize{width: 1680f64, height: 900f64})).unwrap(); 19 | Ok(()) 20 | }) 21 | .run(tauri::generate_context!()) 22 | .expect("error while running tauri application"); 23 | } 24 | -------------------------------------------------------------------------------- /web/native-wrapper/src-tauri/tauri.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "build": { 3 | "beforeDevCommand": "pnpm dev", 4 | "beforeBuildCommand": "pnpm build", 5 | "devPath": "http://localhost:1420", 6 | "distDir": "../dist" 7 | }, 8 | "package": { 9 | "productName": "native-wrapper", 10 | "version": "0.0.0" 11 | }, 12 | "tauri": { 13 | "allowlist": { 14 | "all": false, 15 | "shell": { 16 | "all": false, 17 | "open": true 18 | } 19 | }, 20 | "bundle": { 21 | "active": true, 22 | "targets": "all", 23 | "identifier": "cc.unitmesh.editor", 24 | "icon": [ 25 | "icons/32x32.png", 26 | "icons/128x128.png", 27 | "icons/128x128@2x.png", 28 | "icons/icon.icns", 29 | "icons/icon.ico" 30 | ] 31 | }, 32 | "security": { 33 | "csp": null 34 | }, 35 | "windows": [ 36 | { 37 | "fullscreen": false, 38 | "resizable": true, 39 | "title": "native-wrapper", 40 | "width": 800, 41 | "height": 600 42 | } 43 | ] 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /web/native-wrapper/src/App.css: -------------------------------------------------------------------------------- 1 | .logo.vite:hover { 2 | filter: drop-shadow(0 0 2em #747bff); 3 | } 4 | 5 | .logo.preact:hover { 6 | filter: drop-shadow(0 0 2em #673ab8); 7 | } 8 | -------------------------------------------------------------------------------- /web/native-wrapper/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "preact/hooks"; 2 | import preactLogo from "./assets/preact.svg"; 3 | import { invoke } from "@tauri-apps/api/tauri"; 4 | import "./App.css"; 5 | 6 | function App() { 7 | const [greetMsg, setGreetMsg] = useState(""); 8 | const [name, setName] = useState(""); 9 | 10 | async function greet() { 11 | // Learn more about Tauri commands at https://tauri.app/v1/guides/features/command 12 | setGreetMsg(await invoke("greet", { name })); 13 | } 14 | 15 | return ( 16 |
    17 |

    Welcome to Tauri!

    18 | 19 | 30 | 31 |

    Click on the Tauri, Vite, and Preact logos to learn more.

    32 | 33 |
    { 36 | e.preventDefault(); 37 | greet(); 38 | }} 39 | > 40 | setName(e.currentTarget.value)} 43 | placeholder="Enter a name..." 44 | /> 45 | 46 |
    47 | 48 |

    {greetMsg}

    49 |
    50 | ); 51 | } 52 | 53 | export default App; 54 | -------------------------------------------------------------------------------- /web/native-wrapper/src/assets/preact.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /web/native-wrapper/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { render } from "preact"; 2 | import App from "./App"; 3 | import "./styles.css"; 4 | 5 | render(, document.getElementById("root")!); 6 | -------------------------------------------------------------------------------- /web/native-wrapper/src/styles.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 3 | font-size: 16px; 4 | line-height: 24px; 5 | font-weight: 400; 6 | 7 | color: #0f0f0f; 8 | background-color: #f6f6f6; 9 | 10 | font-synthesis: none; 11 | text-rendering: optimizeLegibility; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | -webkit-text-size-adjust: 100%; 15 | } 16 | 17 | .container { 18 | margin: 0; 19 | padding-top: 10vh; 20 | display: flex; 21 | flex-direction: column; 22 | justify-content: center; 23 | text-align: center; 24 | } 25 | 26 | .logo { 27 | height: 6em; 28 | padding: 1.5em; 29 | will-change: filter; 30 | transition: 0.75s; 31 | } 32 | 33 | .logo.tauri:hover { 34 | filter: drop-shadow(0 0 2em #24c8db); 35 | } 36 | 37 | .row { 38 | display: flex; 39 | justify-content: center; 40 | } 41 | 42 | a { 43 | font-weight: 500; 44 | color: #646cff; 45 | text-decoration: inherit; 46 | } 47 | 48 | a:hover { 49 | color: #535bf2; 50 | } 51 | 52 | h1 { 53 | text-align: center; 54 | } 55 | 56 | input, 57 | button { 58 | border-radius: 8px; 59 | border: 1px solid transparent; 60 | padding: 0.6em 1.2em; 61 | font-size: 1em; 62 | font-weight: 500; 63 | font-family: inherit; 64 | color: #0f0f0f; 65 | background-color: #ffffff; 66 | transition: border-color 0.25s; 67 | box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2); 68 | } 69 | 70 | button { 71 | cursor: pointer; 72 | } 73 | 74 | button:hover { 75 | border-color: #396cd8; 76 | } 77 | button:active { 78 | border-color: #396cd8; 79 | background-color: #e8e8e8; 80 | } 81 | 82 | input, 83 | button { 84 | outline: none; 85 | } 86 | 87 | #greet-input { 88 | margin-right: 5px; 89 | } 90 | 91 | @media (prefers-color-scheme: dark) { 92 | :root { 93 | color: #f6f6f6; 94 | background-color: #2f2f2f; 95 | } 96 | 97 | a:hover { 98 | color: #24c8db; 99 | } 100 | 101 | input, 102 | button { 103 | color: #ffffff; 104 | background-color: #0f0f0f98; 105 | } 106 | button:active { 107 | background-color: #0f0f0f69; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /web/native-wrapper/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /web/native-wrapper/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | "jsxImportSource": "preact", 17 | 18 | /* Linting */ 19 | "strict": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "noFallthroughCasesInSwitch": true 23 | }, 24 | "include": ["src"], 25 | "references": [{ "path": "./tsconfig.node.json" }] 26 | } 27 | -------------------------------------------------------------------------------- /web/native-wrapper/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /web/native-wrapper/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import preact from "@preact/preset-vite"; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig(async () => ({ 6 | plugins: [preact()], 7 | 8 | // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` 9 | // 10 | // 1. prevent vite from obscuring rust errors 11 | clearScreen: false, 12 | // 2. tauri expects a fixed port, fail if that port is not available 13 | server: { 14 | port: 1420, 15 | strictPort: true, 16 | } 17 | })); 18 | -------------------------------------------------------------------------------- /web/studio/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "plugin:@nx/react-typescript", 4 | "next", 5 | "next/core-web-vitals", 6 | "../../.eslintrc.json" 7 | ], 8 | "ignorePatterns": [ 9 | "!**/*", 10 | ".next/**/*" 11 | ], 12 | "overrides": [ 13 | { 14 | "files": [ 15 | "*.*" 16 | ], 17 | "rules": { 18 | "@next/next/no-html-link-for-pages": "off" 19 | } 20 | }, 21 | { 22 | "files": [ 23 | "*.*" 24 | ], 25 | "rules": { 26 | "@typescript-eslint/no-explicit-any": "off" 27 | } 28 | }, 29 | { 30 | "files": [ 31 | "*.ts", 32 | "*.tsx", 33 | "*.js", 34 | "*.jsx" 35 | ], 36 | "rules": { 37 | "@next/next/no-html-link-for-pages": [ 38 | "error", 39 | "web/core/pages" 40 | ] 41 | } 42 | }, 43 | { 44 | "files": [ 45 | "*.ts", 46 | "*.tsx" 47 | ], 48 | "rules": {} 49 | }, 50 | { 51 | "files": [ 52 | "*.js", 53 | "*.jsx" 54 | ], 55 | "rules": {} 56 | }, 57 | { 58 | "files": [ 59 | "*.spec.ts", 60 | "*.spec.tsx", 61 | "*.spec.js", 62 | "*.spec.jsx" 63 | ], 64 | "env": { 65 | "jest": true 66 | } 67 | } 68 | ] 69 | } -------------------------------------------------------------------------------- /web/studio/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /.next 3 | /next-env.d.ts 4 | /out -------------------------------------------------------------------------------- /web/studio/.nvmrc: -------------------------------------------------------------------------------- 1 | 18 2 | -------------------------------------------------------------------------------- /web/studio/app/api/completion/hunyuan/ai.ts: -------------------------------------------------------------------------------- 1 | import OpenAI from 'openai'; 2 | import { HunYuanAI } from '@studio-b3/llmapi'; 3 | 4 | const api = new HunYuanAI({ 5 | // see https://console.cloud.tencent.com/cam/capi 6 | appId: process.env.HUNYUAN_APP_ID, 7 | secretId: process.env.HUNYUAN_SECRET_ID, 8 | secretKey: process.env.HUNYUAN_SECRET_KEY 9 | }); 10 | 11 | export type AskParams = { 12 | model?: HunYuanAI.ChatModel; 13 | prompt: string; 14 | system?: string; 15 | temperature?: number; 16 | presence_penalty?: number; 17 | max_tokens?: number; 18 | }; 19 | 20 | export function askAI({ 21 | model = 'hunyuan', 22 | prompt, 23 | system, 24 | temperature = 0.9, 25 | ...rest 26 | }: AskParams) { 27 | const messages: OpenAI.ChatCompletionMessageParam[] = [ 28 | { 29 | role: 'user', 30 | content: prompt, 31 | }, 32 | ]; 33 | 34 | if (system) { 35 | messages.unshift({ 36 | role: 'system', 37 | content: system, 38 | }); 39 | } 40 | 41 | return api.chat.completions.create({ 42 | ...rest, 43 | stream: true, 44 | model, 45 | temperature, 46 | messages, 47 | }); 48 | } 49 | -------------------------------------------------------------------------------- /web/studio/app/api/completion/hunyuan/route.ts: -------------------------------------------------------------------------------- 1 | import { OpenAIStream, StreamingTextResponse } from "ai"; 2 | 3 | import { askAI } from './ai' 4 | 5 | // export const runtime = 'edge'; 6 | 7 | export async function POST(req: Request) { 8 | const { prompt } = await req.json(); 9 | 10 | const response = await askAI({ 11 | prompt 12 | }); 13 | 14 | // Convert the response into a friendly text-stream 15 | const stream = OpenAIStream(response); 16 | 17 | // Respond with the stream 18 | return new StreamingTextResponse(stream); 19 | } 20 | -------------------------------------------------------------------------------- /web/studio/app/api/completion/minimax/ai.ts: -------------------------------------------------------------------------------- 1 | import OpenAI from 'openai'; 2 | import { MinimaxAI } from '@studio-b3/llmapi'; 3 | 4 | // See https://api.minimax.chat/user-center/basic-information/interface-key 5 | const api = new MinimaxAI({ 6 | orgId: process.env.MINIMAX_API_ORG, 7 | apiKey: process.env.MINIMAX_API_KEY, 8 | }); 9 | 10 | export type AskParams = { 11 | model?: MinimaxAI.ChatModel; 12 | prompt: string; 13 | system?: string; 14 | temperature?: number; 15 | presence_penalty?: number; 16 | max_tokens?: number; 17 | }; 18 | 19 | export function askAI({ 20 | model = 'abab5-chat', 21 | prompt, 22 | system, 23 | temperature = 0.6, 24 | ...rest 25 | }: AskParams) { 26 | const messages: OpenAI.ChatCompletionMessageParam[] = [ 27 | { 28 | role: 'user', 29 | content: prompt, 30 | }, 31 | ]; 32 | 33 | if (system) { 34 | messages.unshift({ 35 | role: 'system', 36 | content: system, 37 | }); 38 | } 39 | 40 | return api.chat.completions.create({ 41 | ...rest, 42 | stream: true, 43 | model, 44 | temperature, 45 | messages, 46 | }); 47 | } 48 | -------------------------------------------------------------------------------- /web/studio/app/api/completion/minimax/route.ts: -------------------------------------------------------------------------------- 1 | import { OpenAIStream, StreamingTextResponse } from 'ai'; 2 | 3 | import { askAI } from './ai'; 4 | 5 | // export const runtime = 'edge'; 6 | 7 | export async function POST(req: Request) { 8 | const response = await askAI(await req.json()); 9 | 10 | // Convert the response into a friendly text-stream 11 | const stream = OpenAIStream(response); 12 | 13 | // Respond with the stream 14 | return new StreamingTextResponse(stream); 15 | } 16 | -------------------------------------------------------------------------------- /web/studio/app/api/completion/mock/route.ts: -------------------------------------------------------------------------------- 1 | import { OpenAIStream, StreamingTextResponse } from 'ai'; 2 | 3 | export async function POST(req: Request) { 4 | const responseData = { 5 | 'choices': [ 6 | { 7 | 'finish_reason': 'stop', 8 | 'index': 0, 9 | 'message': { 10 | 'content': 'The 2020 World Series was played in Texas at Globe Life Field in Arlington.', 11 | 'role': 'assistant' 12 | } 13 | } 14 | ], 15 | 'created': 1677664795, 16 | 'id': 'chatcmpl-7QyqpwdfhqwajicIEznoc6Q47XAyW', 17 | 'model': 'gpt-3.5-turbo-0613', 18 | 'object': 'chat.completion' 19 | }; 20 | 21 | const response = new Response(JSON.stringify(responseData), { 22 | headers: { 'Content-Type': 'application/json' } 23 | }); 24 | 25 | const stream = OpenAIStream(response); 26 | return new StreamingTextResponse(stream); 27 | } -------------------------------------------------------------------------------- /web/studio/app/api/completion/openai/ai.ts: -------------------------------------------------------------------------------- 1 | import OpenAI from 'openai'; 2 | 3 | // Create an OpenAI API client (that's edge friendly!) 4 | const api = new OpenAI({ 5 | apiKey: process.env.OPENAI_API_KEY || '', 6 | }); 7 | 8 | export type AskParams = { 9 | model?: OpenAI.ChatCompletionCreateParams['model']; 10 | prompt: string; 11 | system?: string; 12 | temperature?: number; 13 | presence_penalty?: number; 14 | max_tokens?: number; 15 | }; 16 | 17 | export function askAI({ 18 | model = 'gpt-3.5-turbo', 19 | prompt, 20 | system, 21 | temperature = 0.6, 22 | ...rest 23 | }: AskParams) { 24 | const messages: OpenAI.ChatCompletionMessageParam[] = [ 25 | { 26 | role: 'user', 27 | content: prompt, 28 | }, 29 | ]; 30 | 31 | if (system) { 32 | messages.unshift({ 33 | role: 'system', 34 | content: system, 35 | }); 36 | } 37 | 38 | return api.chat.completions.create({ 39 | ...rest, 40 | stream: true, 41 | model, 42 | temperature, 43 | messages, 44 | }); 45 | } 46 | -------------------------------------------------------------------------------- /web/studio/app/api/completion/openai/route.ts: -------------------------------------------------------------------------------- 1 | import { OpenAIStream, StreamingTextResponse } from 'ai'; 2 | 3 | import { askAI } from './ai' 4 | 5 | // Set the runtime to edge for best performance 6 | // export const runtime = 'edge'; 7 | 8 | export async function POST(req: Request) { 9 | const { prompt } = await req.json(); 10 | 11 | // Ask OpenAI for a streaming completion given the prompt 12 | const response = await askAI({ 13 | prompt 14 | }); 15 | // Convert the response into a friendly text-stream 16 | const stream = OpenAIStream(response); 17 | // Respond with the stream 18 | return new StreamingTextResponse(stream); 19 | } -------------------------------------------------------------------------------- /web/studio/app/api/completion/qwen/ai.ts: -------------------------------------------------------------------------------- 1 | import OpenAI from 'openai'; 2 | import { QWenAI } from '@studio-b3/llmapi'; 3 | 4 | const api = new QWenAI({ 5 | // https://help.aliyun.com/zh/dashscope/developer-reference/activate-dashscope-and-create-an-api-key 6 | apiKey: process.env.QWEN_API_KEY || '', 7 | }); 8 | 9 | export type AskParams = { 10 | model?: QWenAI.ChatModel; 11 | prompt: string; 12 | system?: string; 13 | temperature?: number; 14 | presence_penalty?: number; 15 | max_tokens?: number; 16 | }; 17 | 18 | export function askAI({ 19 | model = 'qwen-max', 20 | prompt, 21 | system, 22 | temperature = 0.9, 23 | ...rest 24 | }: AskParams) { 25 | const messages: OpenAI.ChatCompletionMessageParam[] = [ 26 | { 27 | role: 'user', 28 | content: prompt, 29 | }, 30 | ]; 31 | 32 | if (system) { 33 | messages.unshift({ 34 | role: 'system', 35 | content: system, 36 | }); 37 | } 38 | 39 | return api.chat.completions.create({ 40 | ...rest, 41 | stream: true, 42 | model, 43 | temperature, 44 | messages, 45 | }); 46 | } 47 | -------------------------------------------------------------------------------- /web/studio/app/api/completion/qwen/route.ts: -------------------------------------------------------------------------------- 1 | import { OpenAIStream, StreamingTextResponse } from "ai"; 2 | 3 | import { askAI } from './ai' 4 | 5 | // export const runtime = 'edge'; 6 | 7 | export async function POST(req: Request) { 8 | const { prompt } = await req.json(); 9 | 10 | const response = await askAI({ 11 | prompt 12 | }); 13 | 14 | // Convert the response into a friendly text-stream 15 | const stream = OpenAIStream(response); 16 | 17 | // Respond with the stream 18 | return new StreamingTextResponse(stream); 19 | } 20 | -------------------------------------------------------------------------------- /web/studio/app/api/completion/route.ts: -------------------------------------------------------------------------------- 1 | import { OpenAIStream, StreamingTextResponse } from 'ai'; 2 | 3 | import * as minimax from './minimax/ai'; 4 | import * as openai from './openai/ai'; 5 | import * as qwen from './qwen/ai'; 6 | import * as yiyan from './yiyan/ai'; 7 | import * as hunyuan from './hunyuan/ai'; 8 | 9 | export type AskParams = 10 | | minimax.AskParams 11 | | openai.AskParams 12 | | qwen.AskParams 13 | | yiyan.AskParams; 14 | 15 | const handlers = [ 16 | { 17 | match: /abab/, 18 | handle: minimax.askAI, 19 | }, 20 | { 21 | match: /qwen/, 22 | handle: qwen.askAI, 23 | }, 24 | { 25 | match: /ernie/, 26 | handle: yiyan.askAI, 27 | }, 28 | { 29 | match: /hunyuan/, 30 | handle: hunyuan.askAI, 31 | }, 32 | ]; 33 | 34 | const fallback = openai.askAI; 35 | 36 | function askAI(params: AskParams) { 37 | const model = params.model; 38 | 39 | if (!model) return fallback(params); 40 | 41 | const matches = handlers.find((h) => h.match.test(model)); 42 | const handle = matches?.handle || fallback; 43 | 44 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 45 | // @ts-expect-error 46 | return handle(params); 47 | } 48 | 49 | export async function POST(req: Request) { 50 | const response = await askAI(await req.json()); 51 | 52 | const stream = OpenAIStream(response); 53 | 54 | return new StreamingTextResponse(stream); 55 | } 56 | -------------------------------------------------------------------------------- /web/studio/app/api/completion/yiyan/ai.ts: -------------------------------------------------------------------------------- 1 | import OpenAI from 'openai'; 2 | import { ErnieAI } from '@studio-b3/llmapi'; 3 | 4 | const api = new ErnieAI({ 5 | // 访问令牌通过编程对 AI Studio ⽤户进⾏身份验证 6 | // https://aistudio.baidu.com/index/accessToken 7 | token: process.env.AISTUDIO_ACCESS_TOKEN || '', 8 | }); 9 | 10 | export type AskParams = { 11 | model?: ErnieAI.ChatModel; 12 | prompt: string; 13 | system?: string; 14 | temperature?: number; 15 | presence_penalty?: number; 16 | max_tokens?: number; 17 | }; 18 | 19 | export function askAI({ 20 | model = 'ernie-bot', 21 | prompt, 22 | system, 23 | temperature = 0.6, 24 | ...rest 25 | }: AskParams) { 26 | const messages: OpenAI.ChatCompletionMessageParam[] = [ 27 | { 28 | role: 'user', 29 | content: prompt, 30 | }, 31 | ]; 32 | 33 | if (system) { 34 | messages.unshift({ 35 | role: 'system', 36 | content: system, 37 | }); 38 | } 39 | 40 | return api.chat.completions.create({ 41 | ...rest, 42 | stream: true, 43 | model, 44 | temperature, 45 | messages, 46 | }); 47 | } 48 | -------------------------------------------------------------------------------- /web/studio/app/api/completion/yiyan/route.ts: -------------------------------------------------------------------------------- 1 | import { OpenAIStream, StreamingTextResponse } from 'ai'; 2 | 3 | import { askAI } from './ai' 4 | 5 | // export const runtime = 'edge'; 6 | 7 | export async function POST(req: Request) { 8 | const { prompt } = await req.json(); 9 | 10 | const response = await askAI({ 11 | prompt 12 | }); 13 | 14 | const stream = OpenAIStream(response); 15 | return new StreamingTextResponse(stream); 16 | } 17 | -------------------------------------------------------------------------------- /web/studio/app/api/hello/route.ts: -------------------------------------------------------------------------------- 1 | export async function GET(request: Request) { 2 | return new Response('Hello, from API!'); 3 | } 4 | -------------------------------------------------------------------------------- /web/studio/app/api/images/generate/qwen/route.ts: -------------------------------------------------------------------------------- 1 | import { QWenAI } from '@studio-b3/llmapi'; 2 | 3 | // 通义千问 API 4 | // see https://platform.imagine.art/dashboard 5 | const api = new QWenAI({ 6 | apiKey: process.env.QWEN_API_KEY || '', 7 | }); 8 | 9 | // export const runtime = 'edge'; 10 | 11 | export async function POST(req: Request) { 12 | const { prompt } = await req.json(); 13 | 14 | // See https://help.aliyun.com/zh/dashscope/developer-reference/api-details-9 15 | const response = await api.images.generate({ 16 | model: 'wanx-v1', 17 | prompt: prompt, 18 | n: 1, 19 | size: '1024*1024' 20 | }); 21 | 22 | // Respond with the stream 23 | return new Response(JSON.stringify(response), { 24 | headers: { 25 | 'Content-Type': 'application/json', 26 | }, 27 | }); 28 | } 29 | -------------------------------------------------------------------------------- /web/studio/app/api/images/generate/route.ts: -------------------------------------------------------------------------------- 1 | import { VYroAI } from '@studio-b3/llmapi'; 2 | 3 | // Imagine Art 4 | // see https://platform.imagine.art/dashboard 5 | const api = new VYroAI({ 6 | apiKey: process.env.VYRO_API_KEY || '', 7 | apiType: process.env.VYRO_API_TYPE || '', 8 | }); 9 | 10 | // export const runtime = 'edge'; 11 | 12 | export async function POST(req: Request) { 13 | const { prompt } = await req.json(); 14 | 15 | const response = await api.images.generate({ 16 | model: 'imagine-v5', 17 | prompt: prompt, 18 | }); 19 | 20 | // TODO 目前只支持单图 21 | const image = response.data[0].binary!; 22 | 23 | // Respond with the stream 24 | return new Response(image as globalThis.ReadableStream, { 25 | headers: { 26 | 'Content-Type': 'image/png', 27 | }, 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /web/studio/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unit-mesh/autodev-editor/d6b82047072dd7344e6378a22c280bd374bff9ab/web/studio/components/.gitkeep -------------------------------------------------------------------------------- /web/studio/i18n/i18n.ts: -------------------------------------------------------------------------------- 1 | import i18n from 'i18next' 2 | import { initReactI18next } from 'react-i18next' 3 | import LanguageDetector from 'i18next-browser-languagedetector' 4 | 5 | const mainPlaceholder = `# AutoDev Editor(formerly Studio B3) 6 | 7 | Hi there, B3 is editor for Unit Mesh architecture paradigms, the next-gen software architecture. 8 | 9 | 1. Click toolbar's AI button to trigger AI commands. 10 | 2. Press \`/\` to trigger AI commands. 11 | 3. Press \`Control\` + \`/\` (Windows/Linux) or \`Command\` + \`/\` (macOS) to show custom AI input box. 12 | 4. Select text and see the select-relative bubble menu. 13 | 5. Press \`Control\` + \`\\\` (Windows/Linux) or \`Command\` + \`\\\` to trigger inline completion. 14 | 15 | Scenarios: professional article, blog, user stories, daily reports, weekly reports, etc. 16 | 17 | ## Inline AI 18 | 19 | > Testing grammar and spellings, select long text to see the menu. 20 | 21 | 永和九年,岁在癸丑,暮春之初,会于会稽山阴之兰亭,修禊事也。群贤毕至,少长咸集。此地有崇山峻岭,茂林修竹;又有清流激湍,映带左右,引以为流觞曲水, 22 | 列坐其次。虽无丝竹管弦之盛,一觞一咏,亦足以畅叙幽情。 23 | 24 | 是日也,天朗气清,惠风和畅。仰观宇宙之大,俯察品类之盛,所以游目骋怀,足以极视听之娱,信可乐也。 25 | 26 | 夫人之相与,俯仰一世,或取诸怀抱,悟言一室之内;或因寄所托,放浪形骸之外。虽趣舍万殊,静躁不同,当其欣于所遇,暂得于己,快然自足,不知老之将至。 27 | 及其所之既倦,情随事迁,感慨系之矣。向之所欣,俯仰之间,已为陈迹,犹不能不以之兴怀。况修短随化,终期于尽。古人云:“死生亦大矣。”岂不痛哉! 28 | 29 | 每览昔人兴感之由,若合一契,未尝不临文嗟悼,不能喻之于怀。固知一死生为虚诞,齐彭殇为妄作。后之视今,亦犹今之视昔。悲夫!故列叙时人,录其所述, 30 | 虽世殊事异,所以兴怀,其致一也。后之览者,亦将有感于斯文。 31 | 32 | ` 33 | 34 | const zhPlaceholder = `# B3 编辑器 35 | 36 | B3 是一个 AI 原生文本编辑器,适合于 Unit Mesh 架构范式的编辑器,以探索下一代软件架构。 37 | 38 | 1. 点击工具栏上的 AI 按钮以触发 AI 指令。 39 | 2. 按 \`/\` 键以触发 AI 指令。 40 | 3. 按 \`Control\` + \`/\`(Windows/Linux)或 \`Command\` + \`/\`(macOS)以显示自定义 AI 输入框。 41 | 4. 选择文本并查看选择相对泡泡菜单。 42 | 5. 按 \`Control\` + \`\\\`(Windows / Linux)或 \`Command\` + \`\\\` 以触发行内补全。 43 | 44 | 适用场景:专业领域文章、博客、用户故事、日报、周报等。 45 | 46 | ## 内联 AI 支持 47 | 48 | > 测试语法和拼写,选择长文本以查看菜单。 49 | 50 | 永和九年,岁在癸丑,暮春之初,会于会稽山阴之兰亭,修禊事也。群贤毕至,少长咸集。此地有崇山峻岭,茂林修竹;又有清流激湍,映带左右,引以为流觞曲水, 51 | 列坐其次。虽无丝竹管弦之盛,一觞一咏,亦足以畅叙幽情。 52 | 53 | 是日也,天朗气清,惠风和畅。仰观宇宙之大,俯察品类之盛,所以游目骋怀,足以极视听之娱,信可乐也。 54 | 55 | 夫人之相与,俯仰一世,或取诸怀抱,悟言一室之内;或因寄所托,放浪形骸之外。虽趣舍万殊,静躁不同,当其欣于所遇,暂得于己,快然自足,不知老之将至。 56 | 及其所之既倦,情随事迁,感慨系之矣。向之所欣,俯仰之间,已为陈迹,犹不能不以之兴怀。况修短随化,终期于尽。古人云:“死生亦大矣。”岂不痛哉! 57 | 58 | 每览昔人兴感之由,若合一契,未尝不临文嗟悼,不能喻之于怀。固知一死生为虚诞,齐彭殇为妄作。后之视今,亦犹今之视昔。悲夫!故列叙时人,录其所述, 59 | 虽世殊事异,所以兴怀,其致一也。后之览者,亦将有感于斯文。 60 | 61 | ` 62 | 63 | // the translations 64 | // (tip move them in a JSON file and import them, 65 | // or even better, manage them separated from your code: https://react.i18next.com/guides/multiple-translation-files) 66 | const resources = { 67 | en: { 68 | translation: { 69 | 'Editor Placeholder': mainPlaceholder, 70 | 'Custom Related Resource Link': 'Custom Related Resource Link', 71 | 'Article Context': 'Article Context', 72 | 'Grammarly': 'Grammarly', 73 | 'Text Prediction': 'Text Prediction', 74 | 'Text Similarity': 'Text Similarity', 75 | 'Web Search': 'Web Search', 76 | 'Model Setting': 'Model Setting', 77 | 'Continue writing': 'Continue writing', 78 | 'Help Me Write': 'Help Me Write', 79 | 'Spelling and Grammar': 'Spelling and Grammar', 80 | 'Summarize': 'Summarize', 81 | 'Polish': 'Polish', 82 | 'Similar Chunk': 'Similar Content Chunk', 83 | 'Simplify Content': 'Simplify Content', 84 | 'Translate': 'Translate', 85 | 'Generate Outline': 'Generate Outline', 86 | 'Look up': 'Look up', 87 | } 88 | }, 89 | zh: { 90 | translation: { 91 | 'Editor Placeholder': zhPlaceholder, 92 | 'Custom Related Resource Link': '自定义相关资源链接', 93 | 'Article Context': '文章背景', 94 | 'Grammarly': '语法检查', 95 | 'Text Prediction': '文本预测', 96 | 'Text Similarity': '文本相似度', 97 | 'Web Search': '网页搜索', 98 | 'Model Setting': '模型设置', 99 | 'Continue writing': '续写', 100 | 'Help Me Write': '帮助我写作', 101 | 'Spelling and Grammar': '拼写和语法检查', 102 | 'Summarize': '生成摘要', 103 | 'Polish': '润色', 104 | 'Similar Chunk': '相似内容块', 105 | 'Simplify Content': '精简内容', 106 | 'Translate': '翻译', 107 | 'Generate Outline': '生成大纲', 108 | 'Look up': '检索', 109 | } 110 | } 111 | } 112 | 113 | i18n 114 | .use(initReactI18next) // passes i18n down to react-i18next 115 | .use(LanguageDetector) 116 | .init({ 117 | resources, 118 | fallbackLng: 'en', 119 | // debug: true, 120 | // lng: 'zh', // language to use, more information here: https://www.i18next.com/overview/configuration-options#languages-namespaces-resources 121 | // you can use the i18n.changeLanguage function to change the language manually: https://www.i18next.com/overview/api#changelanguage 122 | // if you're using a language detector, do not define the lng option 123 | 124 | interpolation: { 125 | escapeValue: false // react already safes from xss 126 | } 127 | }, (err, t) => { 128 | if (err) return console.log('something went wrong loading', err) 129 | console.log('i18n loaded successfully') 130 | }) 131 | 132 | export default i18n 133 | -------------------------------------------------------------------------------- /web/studio/index.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | declare module '*.svg' { 3 | const content: any; 4 | export const ReactComponent: any; 5 | export default content; 6 | } 7 | -------------------------------------------------------------------------------- /web/studio/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'core', 4 | preset: '../../jest.preset.js', 5 | transform: { 6 | '^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nx/react/plugins/jest', 7 | '^.+\\.[tj]sx?$': ['babel-jest', { presets: ['@nx/next/babel'] }], 8 | }, 9 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], 10 | coverageDirectory: '../../coverage/web/core', 11 | }; 12 | -------------------------------------------------------------------------------- /web/studio/next.config.ci.js: -------------------------------------------------------------------------------- 1 | const nextConfig = { 2 | output: 'export', 3 | }; 4 | 5 | 6 | module.exports = nextConfig; 7 | -------------------------------------------------------------------------------- /web/studio/next.config.js: -------------------------------------------------------------------------------- 1 | const nextConfig = {}; 2 | 3 | module.exports = nextConfig; 4 | -------------------------------------------------------------------------------- /web/studio/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@studio-b3/web-studio", 3 | "version": "0.0.2", 4 | "license": "MPL-2.0", 5 | "contributors": [ 6 | { 7 | "name": "CGQAQ", 8 | "email": "m.jason.liu@outlook.com" 9 | }, 10 | { 11 | "name": "Phodal", 12 | "email": "h@phodal.com" 13 | } 14 | ], 15 | "scripts": { 16 | "build": "next build", 17 | "dev": "next dev", 18 | "lint": "eslint --ext .js,.jsx,.ts,.tsx .", 19 | "lint:fix": "eslint --fix --ext .js,.jsx,.ts,.tsx .", 20 | "start": "next start", 21 | "storybook": "start-storybook -p 6006" 22 | }, 23 | "dependencies": { 24 | "@studio-b3/web-core": "workspace:^", 25 | "@studio-b3/llmapi": "workspace:^", 26 | "ai": "^2.2.25", 27 | "next": "^14", 28 | "openai": "^4.20.0", 29 | "react": "18.2.0", 30 | "react-dom": "18.2.0" 31 | }, 32 | "devDependencies": { 33 | "@nx/eslint": "17.1.3", 34 | "@nx/eslint-plugin": "17.1.3", 35 | "@nx/jest": "17.1.3", 36 | "@nx/js": "17.1.3", 37 | "@nx/next": "^17.1.3", 38 | "@nx/react": "17.1.3", 39 | "@storybook/addon-essentials": "^7.5.3", 40 | "@storybook/addon-interactions": "^7.5.3", 41 | "@storybook/addon-links": "^7.5.3", 42 | "@storybook/addon-onboarding": "^1.0.8", 43 | "@storybook/blocks": "^7.5.3", 44 | "@storybook/nextjs": "^7.5.3", 45 | "@storybook/react": "^7.5.3", 46 | "@storybook/testing-library": "^0.2.2", 47 | "@swc-node/register": "~1.6.7", 48 | "@swc/core": "~1.3.85", 49 | "@tailwindcss/typography": "^0.5.10", 50 | "@testing-library/jest-dom": "^6.1.5", 51 | "@testing-library/react": "^14.1.2", 52 | "@types/jest": "^29.4.0", 53 | "@types/markdown-it": "^13.0.7", 54 | "@types/node": "20.9.4", 55 | "@types/react": "18.2.38", 56 | "@types/react-dom": "18.2.17", 57 | "@types/uuid": "^9.0.7", 58 | "@typescript-eslint/eslint-plugin": "^6.9.1", 59 | "@typescript-eslint/parser": "^6.9.1", 60 | "autoprefixer": "^10.0.1", 61 | "babel-jest": "^29.4.1", 62 | "eslint": "^8", 63 | "eslint-config-next": "14.0.3", 64 | "eslint-config-prettier": "^9.0.0", 65 | "eslint-plugin-import": "2.27.5", 66 | "eslint-plugin-jsx-a11y": "6.7.1", 67 | "eslint-plugin-react": "7.32.2", 68 | "eslint-plugin-react-hooks": "4.6.0", 69 | "eslint-plugin-storybook": "^0.6.15", 70 | "jest": "^29.7.0", 71 | "jest-environment-jsdom": "^29.7.0", 72 | "postcss": "^8", 73 | "prettier": "^2.6.2", 74 | "storybook": "^7.5.3", 75 | "tailwindcss": "^3.3.0", 76 | "ts-jest": "^29.1.0", 77 | "ts-node": "10.9.1", 78 | "tsconfig-paths-webpack-plugin": "^4.1.0", 79 | "typescript": "5.3.2" 80 | }, 81 | "engines": { 82 | "node": ">=18" 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /web/studio/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import "../styles/global.css" 2 | 3 | export default function MyApp({ Component, pageProps }: any) { 4 | return 5 | } 6 | -------------------------------------------------------------------------------- /web/studio/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head' 2 | 3 | import "../styles/editor-styles.css" 4 | import styles from '../styles/Home.module.css' 5 | import {LiveEditor} from '@studio-b3/web-core' 6 | import '@/i18n/i18n'; 7 | 8 | export default function Home() { 9 | return ( 10 |
    11 | 12 | AutoDev Editor(formerly Studio B3) - all you need is editor! 13 | 14 | 15 | 16 |
    17 |
    18 | 19 |
    20 |
    21 |
    22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /web/studio/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /web/studio/public/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unit-mesh/autodev-editor/d6b82047072dd7344e6378a22c280bd374bff9ab/web/studio/public/.gitkeep -------------------------------------------------------------------------------- /web/studio/public/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unit-mesh/autodev-editor/d6b82047072dd7344e6378a22c280bd374bff9ab/web/studio/public/.nojekyll -------------------------------------------------------------------------------- /web/studio/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unit-mesh/autodev-editor/d6b82047072dd7344e6378a22c280bd374bff9ab/web/studio/public/favicon.ico -------------------------------------------------------------------------------- /web/studio/styles/Home.module.css: -------------------------------------------------------------------------------- 1 | button.setting { 2 | width: 48px; 3 | height: 48px; 4 | color: var(--gray-12); 5 | } 6 | 7 | button.setting svg { 8 | height: 48px; 9 | width: 48px; 10 | } -------------------------------------------------------------------------------- /web/studio/styles/editor-styles.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unit-mesh/autodev-editor/d6b82047072dd7344e6378a22c280bd374bff9ab/web/studio/styles/editor-styles.css -------------------------------------------------------------------------------- /web/studio/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type {Config} from 'tailwindcss' 2 | 3 | const config: Config = { 4 | content: [ 5 | './pages/**/*.{js,ts,jsx,tsx,mdx}', 6 | './components/**/*.{js,ts,jsx,tsx,mdx}', 7 | './app/**/*.{js,ts,jsx,tsx,mdx}', 8 | ], 9 | theme: { 10 | extend: { 11 | backgroundImage: { 12 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', 13 | 'gradient-conic': 14 | 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', 15 | }, 16 | }, 17 | }, 18 | plugins: [ 19 | require('@tailwindcss/typography'), 20 | ], 21 | } 22 | export default config 23 | -------------------------------------------------------------------------------- /web/studio/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "baseUrl": ".", 4 | "compilerOptions": { 5 | "paths": { 6 | "@/*": [ 7 | "./*" 8 | ] 9 | }, 10 | "jsx": "preserve", 11 | "allowJs": true, 12 | "esModuleInterop": true, 13 | "allowSyntheticDefaultImports": true, 14 | "strict": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "noEmit": true, 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "incremental": true, 20 | "plugins": [ 21 | { 22 | "name": "next" 23 | } 24 | ], 25 | "types": [ 26 | "jest", 27 | "node" 28 | ], 29 | "lib": [ 30 | "dom", 31 | "dom.iterable", 32 | "esnext" 33 | ], 34 | "skipLibCheck": true, 35 | "module": "esnext", 36 | "moduleResolution": "node" 37 | }, 38 | "include": [ 39 | "**/*.ts", 40 | "**/*.tsx", 41 | "**/*.js", 42 | "**/*.jsx", 43 | "../../web/core/.next/types/**/*.ts", 44 | "../../dist/web/core/.next/types/**/*.ts", 45 | "next-env.d.ts", 46 | "../../web/studio/.next/types/**/*.ts", 47 | "../../dist/web/studio/.next/types/**/*.ts", 48 | ".next/types/**/*.ts" 49 | ], 50 | "exclude": [ 51 | "node_modules", 52 | "jest.config.ts", 53 | "src/**/*.spec.ts", 54 | "src/**/*.test.ts" 55 | ] 56 | } 57 | -------------------------------------------------------------------------------- /web/studio/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"], 7 | "jsx": "react" 8 | }, 9 | "include": [ 10 | "jest.config.ts", 11 | "src/**/*.test.ts", 12 | "src/**/*.spec.ts", 13 | "src/**/*.test.tsx", 14 | "src/**/*.spec.tsx", 15 | "src/**/*.test.js", 16 | "src/**/*.spec.js", 17 | "src/**/*.test.jsx", 18 | "src/**/*.spec.jsx", 19 | "src/**/*.d.ts" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /web/studio/types/llm-model.type.ts: -------------------------------------------------------------------------------- 1 | export enum OPENAI_MODEL { 2 | CHATGPT_35_TURBO = 'chatgpt-3.5-turbo', 3 | CHATGPT_35 = 'chatgpt-3.5', 4 | CHATGPT_4 = 'chatgpt-4', 5 | } 6 | 7 | // aka Yiyan 8 | export enum ERNIEBOT { 9 | ERNIEBOT = 'ernie-bot', 10 | } 11 | 12 | export const LlmModelType = { 13 | OPENAI: OPENAI_MODEL, 14 | ERNIEBOT: ERNIEBOT, 15 | } 16 | --------------------------------------------------------------------------------