├── .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 |
3 |
4 | AutoDev Editor(formerly Studio B3)
5 |
6 |
7 |
8 |
9 |
10 |
11 |
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 |
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 |
3 |
4 | AutoDev Editor(formerly Studio B3)
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
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": "[](https://github.com/unit-mesh/3b/actions/workflows/deploy.yml)\r \r ",
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 | {
72 | AdviceManager.getInstance().removeAdvice(advice.id)
73 | editor.commands.unsetAdvice(advice.id)
74 | editor.commands.focus()
75 | }}
76 | >
77 | Reject
78 |
79 | {
82 | const originalSelection = editor.state.selection;
83 | const markdownParser = new MarkdownParser(editor, {
84 | html: true,
85 | });
86 | const markdownNode = markdownParser.parse(advice.content)
87 |
88 | editor.chain().focus()?.deleteRange({
89 | from: originalSelection.from,
90 | to: editor.state.selection.to
91 | }).run();
92 |
93 | editor.chain().insertContentAt(editor.state.selection, markdownNode).run();
94 |
95 | setActiveId(null)
96 | editor.commands.unsetAdvice(advice.id)
97 | AdviceManager.getInstance().removeAdvice(advice.id)
98 | editor.commands.focus()
99 | }}
100 | >
101 | Accept
102 |
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 | {
117 | editor.commands.cancelAi();
118 | }}
119 | >
120 | Cancel esc
121 |
122 | {
124 | editor.commands.callAi(editor.getText() || "");
125 | }}
126 | >
127 | Go
128 |
129 |
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 &&
62 | Ask AI
63 |
64 |
65 | }
66 |
67 |
68 |
69 |
70 |
71 |
72 | {isOpen && (
73 | {smartMenus?.map((menu, index) => {
74 | return
75 | {
78 | setIsOpen(false);
79 | setLoading(true);
80 |
81 | if (!menu.action) {
82 | const text = await editor.commands?.callLlm(menu)
83 | setLoading(false);
84 |
85 | const newComment = newAdvice(text || '');
86 | editor.commands?.setAdvice(newComment.id);
87 | editor.commands?.setAdviceCommand(newComment);
88 | } else {
89 | await menu.action?.(editor);
90 | setLoading(false);
91 | }
92 |
93 | editor.view?.focus();
94 | }}
95 | >
96 | {menu.name}
97 |
98 | ;
99 | })}
100 |
101 | {menus?.map((menu, index) => {
102 | return
103 | {
106 | event.preventDefault();
107 | setIsOpen(false);
108 | await editor.chain().callLlm(menu);
109 | editor.view?.focus();
110 | }}
111 | >
112 | {menu.name}
113 |
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 |
24 | AI
25 |
26 |
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 |
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 |
--------------------------------------------------------------------------------