├── .env ├── .env.template ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── CODEOWNERS ├── README.md ├── README_CN.md ├── app ├── Sidebar │ ├── index.css │ └── index.tsx ├── actions │ └── publish.ts ├── api │ ├── chat-o1 │ │ └── route.ts │ ├── chat │ │ └── route.ts │ └── sandbox │ │ └── route.ts ├── favicon.ico ├── globals.css ├── layout.tsx ├── page.tsx └── providers.tsx ├── components.json ├── components ├── DeleteConfirmation │ ├── index.css │ └── index.tsx ├── Dialog │ ├── Add │ │ ├── index.css │ │ └── index.tsx │ ├── img │ │ └── config_s.svg │ ├── index.css │ └── index.tsx ├── auth-dialog.tsx ├── auth-form.tsx ├── chat-input.tsx ├── chat-picker.tsx ├── chat-settings.tsx ├── chat.css ├── chat.tsx ├── code-theme.css ├── code-view.tsx ├── deploy-dialog.tsx ├── fragment-code.tsx ├── fragment-interpreter.tsx ├── fragment-preview.tsx ├── fragment-web.tsx ├── logo.tsx ├── navbar.tsx ├── preview.tsx └── ui │ ├── alert.tsx │ ├── avatar.tsx │ ├── button.tsx │ ├── card.tsx │ ├── copy-button.tsx │ ├── dialog.tsx │ ├── dropdown-menu.tsx │ ├── input.tsx │ ├── label.tsx │ ├── select.tsx │ ├── separator.tsx │ ├── skeleton.tsx │ ├── tabs.tsx │ ├── textarea.tsx │ ├── theme-toggle.tsx │ ├── toast.tsx │ ├── toaster.tsx │ ├── tooltip.tsx │ └── use-toast.ts ├── docs ├── docs │ ├── assets │ │ ├── add-mcp-server.png │ │ ├── code-login.gif │ │ ├── fast-mcp-weather.gif │ │ ├── fast-mcp-weather.png │ │ ├── favicon.ico │ │ ├── image-to-UI-preview.png │ │ ├── image-to-UI.gif │ │ ├── image-to-UI.png │ │ ├── landing-page.png │ │ ├── logo.svg │ │ ├── mcp-server.gif │ │ ├── text-to-UI-preview.png │ │ ├── text-to-UI.gif │ │ └── text-to-UI.png │ ├── en │ │ ├── contribute.md │ │ ├── generate-code.md │ │ ├── get-started.md │ │ ├── index.md │ │ └── work-with-mcp.md │ ├── javascripts │ │ └── feedback.js │ ├── overrides │ │ └── main.html │ └── zh │ │ ├── contribute.md │ │ ├── generate-code.md │ │ ├── get-started.md │ │ ├── index.md │ │ └── work-with-mcp.md └── mkdocs.yml ├── lib ├── api.ts ├── auth.ts ├── duration.ts ├── messages.ts ├── models copy.json ├── models.json ├── models.ts ├── prompt.ts ├── ratelimit.ts ├── request.ts ├── schema.ts ├── supabase.ts ├── templates.json ├── templates.ts ├── types.ts └── utils.ts ├── middleware.ts ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── preview.png ├── public └── thirdparty │ ├── logo.png │ ├── logo.svg │ ├── logo2.png │ ├── logos │ ├── anthropic.svg │ ├── azure.svg │ ├── fireworks.svg │ ├── fireworksai.svg │ ├── google.svg │ ├── groq.svg │ ├── mistral.svg │ ├── ollama.svg │ ├── openai.svg │ ├── togetherai.svg │ ├── vertex.svg │ └── xai.svg │ └── templates │ ├── code-interpreter-v1.svg │ ├── gradio-developer.svg │ ├── nextjs-developer.svg │ ├── streamlit-developer.svg │ └── vue-developer.svg ├── sandbox-templates ├── gradio-developer │ ├── app.py │ ├── e2b.Dockerfile │ └── e2b.toml ├── nextjs-developer │ ├── _app.tsx │ ├── compile_page.sh │ ├── e2b.Dockerfile │ └── e2b.toml ├── streamlit-developer │ ├── app.py │ ├── e2b.Dockerfile │ └── e2b.toml └── vue-developer │ ├── e2b.Dockerfile │ ├── e2b.toml │ └── nuxt.config.ts ├── tailwind.config.ts └── tsconfig.json /.env: -------------------------------------------------------------------------------- 1 | # API URL 2 | NEXT_PUBLIC_API_URL= 3 | 4 | # Get your E2B API key here https://e2b.dev/docs/getting-started/api-key 5 | E2B_API_KEY= 6 | 7 | # Get your Azure API key here https://learn.microsoft.com/en-us/azure/ai-services/openai/how-to/create-resource?tabs=portal 8 | AZURE_API_KEY= 9 | 10 | # Get your Anthropic API key here https://console.anthropic.com 11 | ANTHROPIC_API_KEY= -------------------------------------------------------------------------------- /.env.template: -------------------------------------------------------------------------------- 1 | # API URL 2 | NEXT_PUBLIC_API_URL= 3 | 4 | # Get your E2B API key here https://e2b.dev/docs/getting-started/api-key 5 | E2B_API_KEY= 6 | 7 | # Get your Azure API key here https://learn.microsoft.com/en-us/azure/ai-services/openai/how-to/create-resource?tabs=portal 8 | AZURE_API_KEY= 9 | 10 | # Get your Anthropic API key here https://console.anthropic.com 11 | ANTHROPIC_API_KEY= -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | 38 | # static doc files 39 | docs/site/ -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": false, 4 | "plugins": ["@trivago/prettier-plugin-sort-imports"] 5 | } 6 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # These owners will be the default owners for everything in 2 | # the repo. Unless a later match takes precedence, 3 | * @mishushakov 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EFFLUX 2 | 3 | [English](./README.md) | [简体中文](./README_CN.md) 4 | 5 | ## What's Efflux? 6 | 7 | Efflux, the next-generation AI interaction platform, is a powerful, lightweight, and highly flexible framework that seamlessly integrates state-of-the-art LLMs (large language models), generative front-end technologies, and MCP (Model Context Protocols) servers. It redefines the way AI-driven applications are deployed and scaled, offering unparalleled efficiency and adaptability. 8 | 9 | In essence, Efflux can serve as: 10 | 11 | * **An LLM-powered chatbot** that engages in natural conversations with users. 12 | 13 | * **A text-to-artifact tool** that helps developers create code snippets effortlessly —— simply by describing their ideas. Efflux can also render the generated UI code in real time, allowing you to immediately test and iterate. 14 | 15 | * **A ready-to-use MCP (Model Context Protocol) host**, unlocking your LLMs' potential and expanding more capabilities by enabling wider data access and integrating custom tools, including but not limited to database interaction and business intelligence. 16 | 17 | 18 | ## Online Demo 19 | Experience Efflux in action through our [online demo](http://47.236.204.213:3000/login). 20 | 21 | 22 | ## Features 23 | 24 | - Based on Next.js 14 (App Router, Server Actions), shadcn/ui, TailwindCSS, Vercel AI SDK. 25 | - Uses the [E2B SDK](https://github.com/e2b-dev/code-interpreter) by [E2B](https://e2b.dev) to securely execute code generated by AI. 26 | - Streaming in the UI. 27 | - Can install and use any package from npm, pip. 28 | 29 | 30 | ## Get started 31 | 32 | 33 | ### 1. Clone the repository 34 | 35 | In your terminal: 36 | 37 | ``` 38 | git clone https://github.com/isoftstone-data-intelligence-ai/efflux-frontend.git 39 | ``` 40 | 41 | ### 2. Install the dependencies 42 | 43 | Enter the repository: 44 | 45 | ``` 46 | cd efflux-frontend 47 | ``` 48 | 49 | Run the following to install the required dependencies: 50 | 51 | ``` 52 | npm i 53 | ``` 54 | 55 | ### 3. Set the environment variables 56 | 57 | Create a `.env.local` file and set the following: 58 | 59 | ```sh 60 | # Get your API key here - https://e2b.dev/ 61 | E2B_API_KEY="your-e2b-api-key" 62 | 63 | # Get your Azure API key here https://learn.microsoft.com/en-us/azure/ai-services/openai/how-to/create-resource?tabs=portal 64 | AZURE_API_KEY= 65 | 66 | # API URL 67 | NEXT_PUBLIC_API_URL= 68 | 69 | # OpenAI API Key 70 | OPENAI_API_KEY= 71 | 72 | # Other providers 73 | ANTHROPIC_API_KEY= 74 | GROQ_API_KEY= 75 | FIREWORKS_API_KEY= 76 | TOGETHER_API_KEY= 77 | GOOGLE_AI_API_KEY= 78 | GOOGLE_VERTEX_CREDENTIALS= 79 | MISTRAL_API_KEY= 80 | XAI_API_KEY= 81 | 82 | ### Optional env vars 83 | 84 | # Domain of the site 85 | NEXT_PUBLIC_SITE_URL= 86 | 87 | # Disabling API key and base URL input in the chat 88 | NEXT_PUBLIC_NO_API_KEY_INPUT= 89 | NEXT_PUBLIC_NO_BASE_URL_INPUT= 90 | 91 | # Rate limit 92 | RATE_LIMIT_MAX_REQUESTS= 93 | RATE_LIMIT_WINDOW= 94 | 95 | # Vercel/Upstash KV (short URLs, rate limiting) 96 | KV_REST_API_URL= 97 | KV_REST_API_TOKEN= 98 | 99 | # Supabase (auth) 100 | SUPABASE_URL= 101 | SUPABASE_ANON_KEY= 102 | 103 | # PostHog (analytics) 104 | NEXT_PUBLIC_POSTHOG_KEY= 105 | NEXT_PUBLIC_POSTHOG_HOST= 106 | ``` 107 | 108 | ### 4. Start the development server 109 | 110 | ``` 111 | npm run dev 112 | ``` 113 | 114 | ### 5. Build the web app 115 | 116 | ``` 117 | npm run build 118 | ``` 119 | 120 | 121 | ## Documentation 122 | 123 | For more information and guidance, check out [Efflux Docs](https://jun-ma.github.io/efflux-frontend/). -------------------------------------------------------------------------------- /README_CN.md: -------------------------------------------------------------------------------- 1 | # EFFLUX 2 | 3 | [English](./README.md) | [简体中文](./README_CN.md) 4 | 5 | ## Efflux是什么? 6 | 7 | Efflux 是一款新一代 AI 交互平台 —— 一个强大、轻量且高度灵活的框架,可无缝集成最先进的大语言模型(LLMs)、生成式前端技术以及 MCP(模型上下文协议)服务器。它重新定义了 AI 驱动应用的部署和扩展方式,可以通过接入海量社区工具,构建普惠AI生态。 8 | 9 | Efflux 可以是: 10 | 11 | * **基于 LLM 的聊天机器人**,能够与用户进行自然语言对话。 12 | 13 | * **文本到组件(Text-to-Artifact)生成工具**,帮助开发者轻松创建代码片段 —— 只需描述你的想法即可。Efflux 能实时渲染生成的 UI 代码,让你能够立即测试和迭代。 14 | 15 | * **开箱即用的 MCP(模型上下文协议)主机**,通过更广泛的数据访问和集成自定义工具释放 LLM 潜力。 16 | 17 | 18 | ## 在线演示 19 | 20 | 您可以通过访问[在线演示](http://47.236.204.213:3000/login)来体验Efflux的功能。 21 | 22 | 23 | ## 特性 24 | 25 | - 基于 Next.js 14 (App Router, Server Actions)、shadcn/ui、TailwindCSS 和 Vercel AI SDK 构建 26 | - 使用 [E2B](https://e2b.dev) 开发的 [E2B SDK](https://github.com/e2b-dev/code-interpreter) 来安全执行 AI 生成的代码 27 | - UI 流式响应 28 | - 支持安装和使用任何 npm、pip 包 29 | 30 | ## 快速开始 31 | 32 | ### 1. 克隆仓库 33 | 34 | 在终端中执行: 35 | 36 | ``` 37 | git clone https://github.com/isoftstone-data-intelligence-ai/efflux-frontend.git 38 | ``` 39 | 40 | ### 2. 安装依赖 41 | 42 | 进入项目目录: 43 | 44 | ``` 45 | cd efflux-frontend 46 | ``` 47 | 48 | 运行以下命令安装所需依赖: 49 | 50 | ``` 51 | npm i 52 | ``` 53 | 54 | ### 3. 配置环境变量 55 | 56 | 创建 `.env.local` 文件并设置以下环境变量: 57 | 58 | ```sh 59 | # 在此获取 E2B API 密钥 - https://e2b.dev/ 60 | E2B_API_KEY="your-e2b-api-key" 61 | 62 | # 在此获取 Azure API 密钥 https://learn.microsoft.com/en-us/azure/ai-services/openai/how-to/create-resource?tabs=portal 63 | AZURE_API_KEY= 64 | 65 | # API 地址 66 | NEXT_PUBLIC_API_URL= 67 | 68 | # OpenAI API 密钥 69 | OPENAI_API_KEY= 70 | 71 | # 其他服务提供商 72 | ANTHROPIC_API_KEY= 73 | GROQ_API_KEY= 74 | FIREWORKS_API_KEY= 75 | TOGETHER_API_KEY= 76 | GOOGLE_AI_API_KEY= 77 | GOOGLE_VERTEX_CREDENTIALS= 78 | MISTRAL_API_KEY= 79 | XAI_API_KEY= 80 | 81 | ### 可选环境变量 82 | 83 | # 站点域名 84 | NEXT_PUBLIC_SITE_URL= 85 | 86 | # 禁用聊天中的 API 密钥和基础 URL 输入 87 | NEXT_PUBLIC_NO_API_KEY_INPUT= 88 | NEXT_PUBLIC_NO_BASE_URL_INPUT= 89 | 90 | # 速率限制 91 | RATE_LIMIT_MAX_REQUESTS= 92 | RATE_LIMIT_WINDOW= 93 | 94 | # Vercel/Upstash KV(短 URL、速率限制) 95 | KV_REST_API_URL= 96 | KV_REST_API_TOKEN= 97 | 98 | # Supabase(认证) 99 | SUPABASE_URL= 100 | SUPABASE_ANON_KEY= 101 | 102 | # PostHog(分析) 103 | NEXT_PUBLIC_POSTHOG_KEY= 104 | NEXT_PUBLIC_POSTHOG_HOST= 105 | ``` 106 | 107 | ### 4. 启动开发服务器 108 | 109 | ``` 110 | npm run dev 111 | ``` 112 | 113 | ### 5. 构建网页应用 114 | 115 | ``` 116 | npm run build 117 | ``` 118 | 119 | 120 | ## 文档 121 | 122 | 有关更多信息和指导,请查看 [Efflux Docs](http://localhost:8080/efflux-frontend/zh/)。 -------------------------------------------------------------------------------- /app/Sidebar/index.css: -------------------------------------------------------------------------------- 1 | .sidebar-wrapper .drawer-overlay { 2 | background-color: rgba(0, 0, 0, 0.4); 3 | position: fixed; 4 | inset: 0; 5 | animation: overlayShow 150ms cubic-bezier(0.16, 1, 0.3, 1); 6 | z-index: 9998; 7 | } 8 | 9 | .sidebar-wrapper .drawer-content { 10 | position: fixed; 11 | top: 0; 12 | left: 0; 13 | bottom: 0; 14 | box-shadow: 2px 0 8px rgba(0, 0, 0, 0.1); 15 | animation: drawerSlideIn 150ms cubic-bezier(0.16, 1, 0.3, 1); 16 | z-index: 9999; 17 | } 18 | 19 | .sidebar-wrapper button.trigger-button { 20 | position: fixed; 21 | left: 0; 22 | top: 50%; 23 | transform: translateY(-50%); 24 | border-left: none; 25 | padding: 8px 4px; 26 | border-radius: 0 4px 4px 0; 27 | cursor: pointer; 28 | z-index: 9997; 29 | transition: all 0.2s ease; 30 | } 31 | 32 | .sidebar-wrapper button.close-button { 33 | padding: 8px; 34 | border-radius: 4px; 35 | cursor: pointer; 36 | display: flex; 37 | align-items: center; 38 | justify-content: center; 39 | transition: all 0.2s ease; 40 | } 41 | 42 | /* Light Theme */ 43 | .sidebar-wrapper .light { 44 | background-color: #ffffff; 45 | color: #18181B; 46 | } 47 | 48 | .sidebar-wrapper button.trigger-button.light { 49 | background-color: #ffffff; 50 | border: 1px solid #e5e7eb; 51 | border-left: none; 52 | color: #6b7280; 53 | } 54 | 55 | .sidebar-wrapper button.trigger-button.light:hover { 56 | background-color: #f3f4f6; 57 | color: #374151; 58 | } 59 | 60 | .sidebar-wrapper button.close-button.light { 61 | color: #6b7280; 62 | } 63 | 64 | .sidebar-wrapper button.close-button.light:hover { 65 | background-color: #f3f4f6; 66 | color: #374151; 67 | } 68 | 69 | .sidebar-wrapper .light .border-t { 70 | border-color: #e5e7eb; 71 | } 72 | 73 | .sidebar-wrapper .light h2 { 74 | color: #6b7280; 75 | } 76 | 77 | .sidebar-wrapper .light button { 78 | background-color: #f3f4f6; 79 | color: #374151; 80 | } 81 | 82 | .sidebar-wrapper .light button:hover { 83 | background-color: #e5e7eb; 84 | } 85 | 86 | .sidebar-wrapper .light i { 87 | color: #6b7280; 88 | } 89 | 90 | .sidebar-wrapper .light a { 91 | color: #6b7280; 92 | } 93 | 94 | .sidebar-wrapper .light a:hover { 95 | color: #374151; 96 | } 97 | 98 | /* Dark Theme */ 99 | .sidebar-wrapper .dark { 100 | background-color: #18181B; 101 | color: #ffffff; 102 | } 103 | 104 | .sidebar-wrapper button.trigger-button.dark { 105 | background-color: #18181B; 106 | border: 1px solid #27272a; 107 | border-left: none; 108 | color: #d4d4d8; 109 | } 110 | 111 | .sidebar-wrapper button.trigger-button.dark:hover { 112 | background-color: #27272a; 113 | color: #ffffff; 114 | } 115 | 116 | .sidebar-wrapper button.close-button.dark { 117 | color: #d4d4d8; 118 | } 119 | 120 | .sidebar-wrapper button.close-button.dark:hover { 121 | background-color: #27272a; 122 | color: #ffffff; 123 | } 124 | 125 | .sidebar-wrapper .dark .border-t { 126 | border-color: #27272a; 127 | } 128 | 129 | .sidebar-wrapper .dark h2 { 130 | color: #d4d4d8; 131 | } 132 | 133 | .sidebar-wrapper .dark button { 134 | background-color: #27272a; 135 | color: #ffffff; 136 | } 137 | 138 | .sidebar-wrapper .dark button:hover { 139 | background-color: #3f3f46; 140 | } 141 | 142 | .sidebar-wrapper .dark i { 143 | color: #d4d4d8; 144 | } 145 | 146 | .sidebar-wrapper .dark a { 147 | color: #d4d4d8; 148 | } 149 | 150 | .sidebar-wrapper .dark a:hover { 151 | color: #ffffff; 152 | } 153 | 154 | /* Dark theme text colors */ 155 | .sidebar-wrapper .dark .text-black { 156 | color: #ffffff; 157 | } 158 | 159 | .sidebar-wrapper .dark .text-gray-500, 160 | .sidebar-wrapper .dark .text-gray-600, 161 | .sidebar-wrapper .dark .text-gray-800 { 162 | color: #d4d4d8; 163 | } 164 | 165 | /* Selected chat in dark theme */ 166 | .sidebar-wrapper .dark .selected-chat { 167 | background-color: #d4d4d8 !important; 168 | } 169 | 170 | .sidebar-wrapper .dark .selected-chat span { 171 | color: #18181B !important; 172 | } 173 | 174 | /* Preserve New Chat button styles */ 175 | .sidebar-wrapper .dark button.bg-gray-100 { 176 | background-color: #27272a; 177 | color: #ffffff; 178 | } 179 | 180 | .sidebar-wrapper .dark button.bg-gray-100:hover { 181 | background-color: #3f3f46; 182 | } 183 | 184 | @keyframes overlayShow { 185 | from { 186 | opacity: 0; 187 | } 188 | to { 189 | opacity: 1; 190 | } 191 | } 192 | 193 | @keyframes drawerSlideIn { 194 | from { 195 | transform: translateX(-100%); 196 | } 197 | to { 198 | transform: translateX(0); 199 | } 200 | } -------------------------------------------------------------------------------- /app/Sidebar/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import * as Dialog from '@radix-ui/react-dialog' 3 | import './index.css' 4 | import { getChatList } from '@/lib/api' 5 | 6 | 7 | export default class extends React.Component { 8 | state = { 9 | open: false, 10 | selectedChat: null, 11 | selectedItem: null, 12 | chats: [] 13 | }; 14 | 15 | componentDidMount = async () => { 16 | this.getList((data) => { 17 | var selectedChat = window.localStorage.getItem('selectedChat') 18 | var selectedItem = null 19 | data.forEach((item) => { 20 | if (String(item.id) == selectedChat) { 21 | selectedItem = item 22 | } 23 | }) 24 | 25 | var obj = { 26 | selectedChat: selectedChat ? parseInt(selectedChat) : null, 27 | selectedItem: selectedItem, 28 | } 29 | 30 | if(selectedItem){ 31 | this.props.setMessages(selectedItem.chat_messages) 32 | } 33 | 34 | this.setState(obj) 35 | }) 36 | } 37 | 38 | getList = async (back = () => { }) => { 39 | var rs = await getChatList({ userId: 1 }) 40 | if (rs.data?.code == 200) { 41 | var data = rs.data.data 42 | this.setState({ chats: data }, () => { 43 | back(data) 44 | }) 45 | } 46 | } 47 | 48 | newChat = () => { 49 | this.setState({ selectedChat: null, selectedItem: null }); 50 | this.setState({ open: false }); 51 | this.props.onAdd() 52 | } 53 | handleChatSelect = (chatId: string) => { 54 | var { chats } = this.state 55 | var obj = {} 56 | chats.forEach((item) => { 57 | if (item.id == chatId) obj = item 58 | }) 59 | this.setState({ selectedChat: chatId, selectedItem: obj }); 60 | window.localStorage.setItem('selectedChat', chatId) 61 | this.props.setMessages(obj.chat_messages) 62 | }; 63 | 64 | render() { 65 | const { open, selectedChat, chats } = this.state; 66 | const { theme = 'light', disabled } = this.props; 67 | 68 | return ( 69 |
70 | { this.setState({ open: val }) }}> 71 | 72 | 77 | 78 | 79 | 80 | Navigation Menu 81 | 82 | Navigation menu for accessing different sections of the application 83 | 84 | 85 |
86 |
87 |
88 |
Chat
89 | 90 | 91 | 92 | 93 | 94 |
95 | 98 |
99 | 100 |
101 | 102 |
103 |

Recent Chats

104 |
105 | {chats.map(chat => ( 106 |
this.handleChatSelect(chat.id)} 109 | className={`relative group py-2 px-4 rounded-md text-sm font-medium flex justify-between items-center cursor-pointer ${selectedChat === chat.id ? 'bg-gray-100 selected-chat' : '' 110 | }`} 111 | > 112 | {chat.summary} 113 |
114 | ))} 115 |
116 |
117 |
118 |
119 |
120 |
121 | ) 122 | } 123 | } -------------------------------------------------------------------------------- /app/actions/publish.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { Duration, ms } from '@/lib/duration' 4 | import { Sandbox } from '@e2b/code-interpreter' 5 | import { kv } from '@vercel/kv' 6 | import { customAlphabet } from 'nanoid' 7 | 8 | const nanoid = customAlphabet('1234567890abcdef', 7) 9 | 10 | export async function publish( 11 | url: string, 12 | sbxId: string, 13 | duration: Duration, 14 | apiKey: string | undefined, 15 | ) { 16 | const expiration = ms(duration) 17 | await Sandbox.setTimeout(sbxId, expiration, { apiKey }) 18 | 19 | if (process.env.KV_REST_API_URL && process.env.KV_REST_API_TOKEN) { 20 | const id = nanoid() 21 | await kv.set(`fragment:${id}`, url, { px: expiration }) 22 | 23 | return { 24 | url: process.env.NEXT_PUBLIC_SITE_URL 25 | ? `https://${process.env.NEXT_PUBLIC_SITE_URL}/s/${id}` 26 | : `/s/${id}`, 27 | } 28 | } 29 | 30 | return { 31 | url, 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/api/chat-o1/route.ts: -------------------------------------------------------------------------------- 1 | import { Duration } from '@/lib/duration' 2 | import { getModelClient } from '@/lib/models' 3 | import { LLMModel, LLMModelConfig } from '@/lib/models' 4 | import { toPrompt } from '@/lib/prompt' 5 | import ratelimit from '@/lib/ratelimit' 6 | import { fragmentSchema as schema } from '@/lib/schema' 7 | import { Templates, templatesToPrompt } from '@/lib/templates' 8 | import { openai } from '@ai-sdk/openai' 9 | import { streamObject, LanguageModel, CoreMessage, generateText } from 'ai' 10 | 11 | export const maxDuration = 60 12 | 13 | const rateLimitMaxRequests = process.env.RATE_LIMIT_MAX_REQUESTS 14 | ? parseInt(process.env.RATE_LIMIT_MAX_REQUESTS) 15 | : 10 16 | const ratelimitWindow = process.env.RATE_LIMIT_WINDOW 17 | ? (process.env.RATE_LIMIT_WINDOW as Duration) 18 | : '1d' 19 | 20 | export async function POST(req: Request) { 21 | const { 22 | messages, 23 | userID, 24 | template, 25 | model, 26 | config, 27 | }: { 28 | messages: CoreMessage[] 29 | userID: string 30 | template: Templates 31 | model: LLMModel 32 | config: LLMModelConfig 33 | } = await req.json() 34 | 35 | const limit = !config.apiKey 36 | ? await ratelimit( 37 | userID, 38 | rateLimitMaxRequests, 39 | ratelimitWindow, 40 | ) 41 | : false 42 | 43 | if (limit) { 44 | return new Response('You have reached your request limit for the day.', { 45 | status: 429, 46 | headers: { 47 | 'X-RateLimit-Limit': limit.amount.toString(), 48 | 'X-RateLimit-Remaining': limit.remaining.toString(), 49 | 'X-RateLimit-Reset': limit.reset.toString(), 50 | }, 51 | }) 52 | } 53 | 54 | console.log('userID', userID) 55 | // console.log('template', template) 56 | console.log('model', model) 57 | // console.log('config', config) 58 | 59 | const { model: modelNameString, apiKey: modelApiKey, ...modelParams } = config 60 | const modelClient = getModelClient(model, config) 61 | 62 | messages.unshift({ 63 | role: 'user', 64 | content: toPrompt(template), 65 | }) 66 | 67 | const { text } = await generateText({ 68 | model: modelClient as LanguageModel, 69 | messages, 70 | ...modelParams, 71 | }) 72 | 73 | const stream = await streamObject({ 74 | model: openai('gpt-4o-mini') as LanguageModel, 75 | schema, 76 | system: `Please extract as required by the schema from the response. You can use one of the following templates:\n${templatesToPrompt(template)}`, 77 | prompt: text, 78 | ...modelParams, 79 | }) 80 | 81 | return stream.toTextStreamResponse() 82 | } 83 | -------------------------------------------------------------------------------- /app/api/chat/route.ts: -------------------------------------------------------------------------------- 1 | import { Duration } from '@/lib/duration' 2 | import { getModelClient, getDefaultMode } from '@/lib/models' 3 | import { LLMModel, LLMModelConfig } from '@/lib/models' 4 | import { toPrompt } from '@/lib/prompt' 5 | import ratelimit from '@/lib/ratelimit' 6 | import { fragmentSchema as schema } from '@/lib/schema' 7 | import { Templates } from '@/lib/templates' 8 | import { streamObject, LanguageModel, CoreMessage } from 'ai' 9 | 10 | export const maxDuration = 60 11 | 12 | const rateLimitMaxRequests = process.env.RATE_LIMIT_MAX_REQUESTS 13 | ? parseInt(process.env.RATE_LIMIT_MAX_REQUESTS) 14 | : 10 15 | const ratelimitWindow = process.env.RATE_LIMIT_WINDOW 16 | ? (process.env.RATE_LIMIT_WINDOW as Duration) 17 | : '1d' 18 | 19 | export async function POST(req: Request) { 20 | const { 21 | messages, 22 | userID, 23 | template, 24 | model, 25 | config, 26 | }: { 27 | messages: CoreMessage[] 28 | userID: string 29 | template: Templates 30 | model: LLMModel 31 | config: LLMModelConfig 32 | } = await req.json() 33 | 34 | const limit = !config.apiKey 35 | ? await ratelimit( 36 | userID, 37 | rateLimitMaxRequests, 38 | ratelimitWindow, 39 | ) 40 | : false 41 | 42 | if (limit) { 43 | return new Response('You have reached your request limit for the day.', { 44 | status: 429, 45 | headers: { 46 | 'X-RateLimit-Limit': limit.amount.toString(), 47 | 'X-RateLimit-Remaining': limit.remaining.toString(), 48 | 'X-RateLimit-Reset': limit.reset.toString(), 49 | }, 50 | }) 51 | } 52 | console.log('---------------------------------') 53 | console.log('userID', userID) 54 | console.log('model', model) 55 | // console.log(JSON.stringify({ 56 | // schema, 57 | // system: toPrompt(template), 58 | // messages, 59 | // mode: getDefaultMode(model), 60 | // })) 61 | 62 | 63 | const { model: modelNameString, apiKey: modelApiKey, ...modelParams } = config 64 | const modelClient = getModelClient(model, config) 65 | 66 | // 相当于用固定的 关键词 去让大模型生成代码 67 | const stream = await streamObject({ 68 | model: modelClient as LanguageModel, 69 | schema, 70 | system: toPrompt(template), 71 | messages, 72 | mode: getDefaultMode(model), 73 | ...modelParams, 74 | }) 75 | 76 | return stream.toTextStreamResponse() 77 | } 78 | -------------------------------------------------------------------------------- /app/api/sandbox/route.ts: -------------------------------------------------------------------------------- 1 | import { FragmentSchema } from '@/lib/schema' 2 | import { ExecutionResultInterpreter, ExecutionResultWeb } from '@/lib/types' 3 | import { Sandbox } from '@e2b/code-interpreter' 4 | 5 | const sandboxTimeout = 10 * 60 * 1000 // 10 minute in ms 6 | 7 | export const maxDuration = 60 8 | 9 | export async function POST(req: Request) { 10 | const { 11 | fragment, 12 | userID, 13 | apiKey, 14 | }: { fragment: FragmentSchema; userID: string; apiKey?: string } = 15 | await req.json() 16 | console.log('fragment', fragment) 17 | console.log('userID', userID) 18 | // console.log('apiKey', apiKey) 19 | 20 | // Create a interpreter or a sandbox 21 | const sbx = await Sandbox.create(fragment.template, { 22 | metadata: { template: fragment.template, userID: userID }, 23 | timeoutMs: sandboxTimeout, 24 | apiKey, 25 | }) 26 | 27 | // Install packages 28 | if (fragment.has_additional_dependencies) { 29 | await sbx.commands.run(fragment.install_dependencies_command) 30 | console.log( 31 | `Installed dependencies: ${fragment.additional_dependencies.join(', ')} in sandbox ${sbx.sandboxId}`, 32 | ) 33 | } 34 | 35 | // Copy code to fs 36 | if (fragment.code && Array.isArray(fragment.code)) { 37 | fragment.code.forEach(async (file) => { 38 | await sbx.files.write(file.file_path, file.file_content) 39 | console.log(`Copied file to ${file.file_path} in ${sbx.sandboxId}`) 40 | }) 41 | } else { 42 | await sbx.files.write(fragment.file_path, fragment.code) 43 | console.log(`Copied file to ${fragment.file_path} in ${sbx.sandboxId}`) 44 | } 45 | 46 | // Execute code or return a URL to the running sandbox 47 | if (fragment.template === 'code-interpreter-v1') { 48 | const { logs, error, results } = await sbx.runCode(fragment.code || '') 49 | 50 | return new Response( 51 | JSON.stringify({ 52 | sbxId: sbx?.sandboxId, 53 | template: fragment.template, 54 | stdout: logs.stdout, 55 | stderr: logs.stderr, 56 | runtimeError: error, 57 | cellResults: results, 58 | } as ExecutionResultInterpreter), 59 | ) 60 | } 61 | 62 | return new Response( 63 | JSON.stringify({ 64 | sbxId: sbx?.sandboxId, 65 | template: fragment.template, 66 | url: `https://${sbx?.getHost(fragment.port || 80)}`, 67 | } as ExecutionResultWeb), 68 | ) 69 | } 70 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/isoftstone-data-intelligence-ai/efflux-frontend/1b941cc4ed746995a952e72cd8afa4c86eb4eab5/app/favicon.ico -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 240 10% 3.9%; 9 | --card: 0 0% 100%; 10 | --card-foreground: 240 10% 3.9%; 11 | --popover: 0 0% 100%; 12 | --popover-foreground: 240 10% 3.9%; 13 | --primary: 240 5.9% 10%; 14 | --primary-foreground: 0 0% 98%; 15 | --secondary: 240 4.8% 95.9%; 16 | --secondary-foreground: 240 5.9% 10%; 17 | --muted: 240 4.8% 95.9%; 18 | --muted-foreground: 240 3.8% 46.1%; 19 | --accent: 240 4.8% 95.9%; 20 | --accent-foreground: 240 5.9% 10%; 21 | --destructive: 0 84.2% 60.2%; 22 | --destructive-foreground: 0 0% 98%; 23 | --border: 240 5.9% 90%; 24 | --input: 240 5.9% 90%; 25 | --ring: 240 10% 3.9%; 26 | --chart-1: 12 76% 61%; 27 | --chart-2: 173 58% 39%; 28 | --chart-3: 197 37% 24%; 29 | --chart-4: 43 74% 66%; 30 | --chart-5: 27 87% 67%; 31 | --radius: 0.75rem; 32 | } 33 | 34 | .dark { 35 | --background: 240, 6%, 10%; 36 | --foreground: 0 0% 98%; 37 | --card: 240 10% 3.9%; 38 | --card-foreground: 0 0% 98%; 39 | --popover: 240, 5%, 13%; 40 | --popover-foreground: 0 0% 98%; 41 | --primary: 0 0% 98%; 42 | --primary-foreground: 240 5.9% 10%; 43 | --secondary: 240 3.7% 15.9%; 44 | --secondary-foreground: 0 0% 98%; 45 | --muted: 240 3.7% 15.9%; 46 | --muted-foreground: 240 5% 64.9%; 47 | --accent: 240 3.7% 15.9%; 48 | --accent-foreground: 0 0% 98%; 49 | --destructive: 0 62.8% 30.6%; 50 | --destructive-foreground: 0 0% 98%; 51 | --border: 270, 2%, 19%; 52 | --input: 240 3.7% 15.9%; 53 | --ring: 0, 0%, 100%, 0.1; 54 | --chart-1: 220 70% 50%; 55 | --chart-2: 160 60% 45%; 56 | --chart-3: 30 80% 55%; 57 | --chart-4: 280 65% 60%; 58 | --chart-5: 340 75% 55%; 59 | } 60 | } 61 | 62 | @layer base { 63 | * { 64 | @apply border-border; 65 | } 66 | body { 67 | @apply bg-background text-foreground; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import './globals.css' 2 | import { PostHogProvider, ThemeProvider } from './providers' 3 | import { Toaster } from '@/components/ui/toaster' 4 | import type { Metadata } from 'next' 5 | import { Inter } from 'next/font/google' 6 | 7 | const inter = Inter({ subsets: ['latin'] }) 8 | 9 | export const metadata: Metadata = { 10 | title: 'EFFLUX', 11 | description: "Open-source version of Anthropic's Artifacts", 12 | } 13 | 14 | export default function RootLayout({ 15 | children, 16 | }: Readonly<{ 17 | children: React.ReactNode 18 | }>) { 19 | return ( 20 | 21 | 22 | 23 | 29 | {children} 30 | 31 | 32 | 33 | 34 | 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /app/providers.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { ThemeProvider as NextThemesProvider } from 'next-themes' 4 | import { type ThemeProviderProps } from 'next-themes/dist/types' 5 | import posthog from 'posthog-js' 6 | import { PostHogProvider as PostHogProviderJS } from 'posthog-js/react' 7 | 8 | if (typeof window !== 'undefined' && process.env.NEXT_PUBLIC_ENABLE_POSTHOG) { 9 | posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY ?? '', { 10 | api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST, 11 | person_profiles: 'identified_only', 12 | session_recording: { 13 | recordCrossOriginIframes: true, 14 | } 15 | }) 16 | } 17 | 18 | export function PostHogProvider({ children }: { children: React.ReactNode }) { 19 | return process.env.NEXT_PUBLIC_ENABLE_POSTHOG ? ( 20 | {children} 21 | ) : ( 22 | children 23 | ) 24 | } 25 | 26 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) { 27 | return {children} 28 | } 29 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /components/DeleteConfirmation/index.css: -------------------------------------------------------------------------------- 1 | .delete-confirmation-overlay { 2 | background-color: rgba(0, 0, 0, 0.5); 3 | position: fixed; 4 | inset: 0; 5 | animation: overlayShow 150ms cubic-bezier(0.16, 1, 0.3, 1); 6 | z-index: 999999; 7 | } 8 | 9 | .delete-confirmation-content { 10 | background-color: white; 11 | border-radius: 6px; 12 | box-shadow: 0px 10px 38px -10px rgba(22, 23, 24, 0.35), 0px 10px 20px -15px rgba(22, 23, 24, 0.2); 13 | position: fixed; 14 | top: 50%; 15 | left: 50%; 16 | transform: translate(-50%, -50%); 17 | width: 90vw; 18 | max-width: 400px; 19 | max-height: 85vh; 20 | padding: 24px; 21 | animation: contentShow 150ms cubic-bezier(0.16, 1, 0.3, 1); 22 | z-index: 999999; 23 | } 24 | 25 | .delete-confirmation-title { 26 | font-size: 18px; 27 | font-weight: 600; 28 | color: #1a1a1a; 29 | margin-bottom: 8px; 30 | } 31 | 32 | .delete-confirmation-description { 33 | font-size: 14px; 34 | color: #666; 35 | margin-bottom: 24px; 36 | line-height: 1.5; 37 | } 38 | 39 | .delete-confirmation-buttons { 40 | display: flex; 41 | gap: 12px; 42 | justify-content: flex-end; 43 | } 44 | 45 | .delete-confirmation-button { 46 | border-radius: 4px; 47 | padding: 8px 16px; 48 | font-size: 14px; 49 | font-weight: 500; 50 | cursor: pointer; 51 | border: none; 52 | transition: background-color 0.2s ease; 53 | } 54 | 55 | .delete-confirmation-button.cancel { 56 | background-color: #f5f5f5; 57 | color: #666; 58 | } 59 | 60 | .delete-confirmation-button.cancel:hover { 61 | background-color: #e8e8e8; 62 | } 63 | 64 | .delete-confirmation-button.confirm { 65 | background-color: #dc2626; 66 | color: white; 67 | } 68 | 69 | .delete-confirmation-button.confirm:hover { 70 | background-color: #b91c1c; 71 | } 72 | 73 | @keyframes overlayShow { 74 | from { 75 | opacity: 0; 76 | } 77 | to { 78 | opacity: 1; 79 | } 80 | } 81 | 82 | @keyframes contentShow { 83 | from { 84 | opacity: 0; 85 | transform: translate(-50%, -48%) scale(0.96); 86 | } 87 | to { 88 | opacity: 1; 89 | transform: translate(-50%, -50%) scale(1); 90 | } 91 | } -------------------------------------------------------------------------------- /components/DeleteConfirmation/index.tsx: -------------------------------------------------------------------------------- 1 | import * as AlertDialog from '@radix-ui/react-alert-dialog'; 2 | import './index.css'; 3 | 4 | interface DeleteConfirmationProps { 5 | open: boolean; 6 | onOpenChange: (open: boolean) => void; 7 | onConfirm: () => void; 8 | title?: string; 9 | description?: string; 10 | cancelText?: string; 11 | confirmText?: string; 12 | } 13 | 14 | const DeleteConfirmation = ({ 15 | open, 16 | onOpenChange, 17 | onConfirm, 18 | title = 'Confirm Delete', 19 | description = 'This action will permanently delete this item. Do you want to continue?', 20 | cancelText = 'Cancel', 21 | confirmText = 'Delete' 22 | }: DeleteConfirmationProps) => { 23 | return ( 24 | 25 | 26 | 27 | 28 | 29 | {title} 30 | 31 | 32 | {description} 33 | 34 |
35 | 36 | 39 | 40 | 41 | 47 | 48 |
49 |
50 |
51 |
52 | ); 53 | } 54 | 55 | export default DeleteConfirmation -------------------------------------------------------------------------------- /components/Dialog/Add/index.css: -------------------------------------------------------------------------------- 1 | .dialog-overlay { 2 | background-color: rgba(0, 0, 0, 0.5); 3 | position: fixed; 4 | inset: 0; 5 | animation: overlayShow 150ms cubic-bezier(0.16, 1, 0.3, 1); 6 | z-index: 999999; 7 | } 8 | 9 | .dialog-content { 10 | background-color: white; 11 | border-radius: 6px; 12 | box-shadow: 0px 10px 38px -10px rgba(22, 23, 24, 0.35), 13 | 0px 10px 20px -15px rgba(22, 23, 24, 0.2); 14 | position: fixed; 15 | top: 50%; 16 | left: 50%; 17 | transform: translate(-50%, -50%); 18 | width: 90vw; 19 | max-width: 450px; 20 | max-height: 85vh; 21 | padding: 24px; 22 | animation: contentShow 150ms cubic-bezier(0.16, 1, 0.3, 1); 23 | z-index: 999999; 24 | } 25 | 26 | .dialog-title { 27 | font-size: 18px; 28 | font-weight: 600; 29 | color: #1a1a1a; 30 | margin-bottom: 20px; 31 | } 32 | 33 | .form-root { 34 | display: flex; 35 | flex-direction: column; 36 | gap: 20px; 37 | } 38 | 39 | .form-field { 40 | display: flex; 41 | flex-direction: column; 42 | gap: 8px; 43 | } 44 | 45 | .field-row { 46 | display: flex; 47 | justify-content: space-between; 48 | align-items: center; 49 | } 50 | 51 | .form-label { 52 | font-size: 14px; 53 | font-weight: 500; 54 | color: #1a1a1a; 55 | } 56 | 57 | .form-message { 58 | font-size: 13px; 59 | color: #ef4444; 60 | opacity: 0; 61 | } 62 | 63 | .form-field[data-invalid] .form-message { 64 | opacity: 1; 65 | } 66 | 67 | .form-input, 68 | .form-textarea { 69 | width: 100%; 70 | border: 1px solid #e5e7eb; 71 | border-radius: 4px; 72 | padding: 8px 12px; 73 | font-size: 14px; 74 | color: #1a1a1a; 75 | background: white; 76 | transition: all 0.2s; 77 | } 78 | 79 | .form-input:focus, 80 | .form-textarea:focus { 81 | outline: none; 82 | border-color: #3b82f6; 83 | box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1); 84 | } 85 | 86 | .form-field[data-invalid] .form-input, 87 | .form-field[data-invalid] .form-textarea { 88 | border-color: #ef4444; 89 | } 90 | 91 | .form-buttons { 92 | display: flex; 93 | justify-content: flex-end; 94 | gap: 12px; 95 | margin-top: 8px; 96 | } 97 | 98 | .button { 99 | padding: 8px 16px; 100 | border-radius: 4px; 101 | font-size: 14px; 102 | font-weight: 500; 103 | cursor: pointer; 104 | transition: all 0.2s; 105 | border: none; 106 | } 107 | 108 | .button.cancel { 109 | background-color: #f3f4f6; 110 | color: #4b5563; 111 | } 112 | 113 | .button.cancel:hover { 114 | background-color: #e5e7eb; 115 | } 116 | 117 | .button.submit { 118 | background-color: #3b82f6; 119 | color: white; 120 | } 121 | 122 | .button.submit:hover { 123 | background-color: #2563eb; 124 | } 125 | 126 | @keyframes overlayShow { 127 | from { 128 | opacity: 0; 129 | } 130 | to { 131 | opacity: 1; 132 | } 133 | } 134 | 135 | @keyframes contentShow { 136 | from { 137 | opacity: 0; 138 | transform: translate(-50%, -48%) scale(0.96); 139 | } 140 | to { 141 | opacity: 1; 142 | transform: translate(-50%, -50%) scale(1); 143 | } 144 | } -------------------------------------------------------------------------------- /components/Dialog/img/config_s.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/Dialog/index.css: -------------------------------------------------------------------------------- 1 | 2 | .overlay { 3 | position: fixed; 4 | top: 0; 5 | left: 0; 6 | right: 0; 7 | bottom: 0; 8 | background: rgba(0, 0, 0, 0.5); 9 | z-index: 1000; 10 | } 11 | 12 | .content { 13 | position: fixed; 14 | top: 50%; 15 | left: 50%; 16 | transform: translate(-50%, -50%); 17 | background: white; 18 | padding: 20px; 19 | border-radius: 8px; 20 | z-index: 1001; 21 | width: 300px; 22 | } 23 | -------------------------------------------------------------------------------- /components/auth-dialog.tsx: -------------------------------------------------------------------------------- 1 | import AuthForm from './auth-form' 2 | import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog' 3 | import { AuthViewType } from '@/lib/auth' 4 | import { VisuallyHidden } from '@radix-ui/react-visually-hidden' 5 | import { SupabaseClient } from '@supabase/supabase-js' 6 | 7 | export function AuthDialog({ 8 | open, 9 | setOpen, 10 | supabase, 11 | view, 12 | }: { 13 | open: boolean 14 | setOpen: (open: boolean) => void 15 | supabase: SupabaseClient 16 | view: AuthViewType 17 | }) { 18 | return ( 19 | 20 | 21 | 22 | Sign in to E2B 23 | 24 | 25 | 26 | 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /components/auth-form.tsx: -------------------------------------------------------------------------------- 1 | import Logo from './logo' 2 | import { AuthViewType } from '@/lib/auth' 3 | import { Auth } from '@supabase/auth-ui-react' 4 | import { ThemeSupa } from '@supabase/auth-ui-shared' 5 | import { SupabaseClient } from '@supabase/supabase-js' 6 | 7 | function AuthForm({ 8 | supabase, 9 | view = 'sign_in', 10 | }: { 11 | supabase: SupabaseClient 12 | view: AuthViewType 13 | }) { 14 | return ( 15 |
16 |

17 |
18 | 19 |
20 | Sign in to Fragments 21 |

22 |
23 | 66 |
67 |
68 | ) 69 | } 70 | 71 | export default AuthForm 72 | -------------------------------------------------------------------------------- /components/chat.css: -------------------------------------------------------------------------------- 1 | .msgByUse{ 2 | align-self: flex-end; 3 | } -------------------------------------------------------------------------------- /components/chat.tsx: -------------------------------------------------------------------------------- 1 | import { Message } from '@/lib/messages' 2 | import { FragmentSchema } from '@/lib/schema' 3 | import { ExecutionResult } from '@/lib/types' 4 | import { DeepPartial } from 'ai' 5 | import { Loader2Icon, LoaderIcon, Terminal } from 'lucide-react' 6 | import { useEffect } from 'react' 7 | import ReactMarkdown from 'react-markdown' 8 | import './chat.css' 9 | 10 | export function Chat({ 11 | messages, 12 | isLoading, 13 | setCurrentPreview, 14 | }: { 15 | messages: Message[] 16 | isLoading: boolean 17 | setCurrentPreview: (preview: { 18 | fragment: DeepPartial | undefined 19 | result: ExecutionResult | undefined 20 | }) => void 21 | }) { 22 | useEffect(() => { 23 | const chatContainer = document.getElementById('chat-container') 24 | if (chatContainer) { 25 | chatContainer.scrollTop = chatContainer.scrollHeight 26 | } 27 | 28 | }, [JSON.stringify(messages)]) 29 | 30 | return ( 31 |
35 | {messages.map((message: Message, index: number) => { 36 | return ( 37 | ( 38 |
43 | {message.content.map((content, id) => { 44 | if (content.type === 'image') { 45 | return ( 46 | fragment 52 | ) 53 | } 54 | if(message.role == 'user'){ 55 | return content.text 56 | } 57 | if (content.type === 'text') { 58 | return ( 59 |
{content.text}
60 | ) 61 | } 62 | })} 63 | 64 | {/* 生成代码区域 */} 65 | {message.object && ( 66 |
68 | setCurrentPreview({ 69 | fragment: message.object, 70 | result: message.result, 71 | }) 72 | } 73 | className="py-2 pl-2 w-full md:w-max flex items-center border rounded-xl select-none hover:bg-white dark:hover:bg-white/5 hover:cursor-pointer" 74 | > 75 |
76 | 77 |
78 |
79 | 80 | {message.object.title} 81 | 82 | 83 | Click to see fragment 84 | 85 |
86 |
87 | )} 88 |
89 | ) 90 | ) 91 | })} 92 | {isLoading && ( 93 |
94 | 95 | Generating... 96 |
97 | )} 98 |
99 | ) 100 | } 101 | -------------------------------------------------------------------------------- /components/code-theme.css: -------------------------------------------------------------------------------- 1 | /* Prism.js GitHub Dark Theme */ 2 | 3 | code[class*='language-'], 4 | pre[class*='language-'] { 5 | font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Mono', 6 | 'Droid Sans Mono', 'Source Code Pro', monospace; 7 | text-align: left; 8 | white-space: pre; 9 | word-spacing: normal; 10 | word-break: normal; 11 | word-wrap: normal; 12 | line-height: 1.5; 13 | tab-size: 4; 14 | hyphens: none; 15 | } 16 | 17 | code[class*='language-'], 18 | pre[class*='language-'] { 19 | color: #24292e; 20 | } 21 | 22 | .token.comment, 23 | .token.prolog, 24 | .token.doctype, 25 | .token.cdata { 26 | color: #6a737d; 27 | } 28 | 29 | .token.punctuation { 30 | color: #24292e; 31 | } 32 | 33 | .token.namespace { 34 | opacity: 0.7; 35 | } 36 | 37 | .token.property, 38 | .token.tag, 39 | .token.boolean, 40 | .token.number, 41 | .token.constant, 42 | .token.symbol { 43 | color: #005cc5; 44 | } 45 | 46 | .token.selector, 47 | .token.attr-name, 48 | .token.string, 49 | .token.char, 50 | .token.builtin { 51 | color: #032f62; 52 | } 53 | 54 | .token.operator, 55 | .token.entity, 56 | .token.url, 57 | .language-css .token.string, 58 | .style .token.string { 59 | color: #d73a49; 60 | background: transparent; 61 | } 62 | 63 | .token.atrule, 64 | .token.attr-value, 65 | .token.keyword { 66 | color: #d73a49; 67 | } 68 | 69 | .token.function, 70 | .token.class-name { 71 | color: #6f42c1; 72 | } 73 | 74 | .token.regex, 75 | .token.important, 76 | .token.variable { 77 | color: #e36209; 78 | } 79 | 80 | .token.important, 81 | .token.bold { 82 | font-weight: bold; 83 | } 84 | 85 | .token.italic { 86 | font-style: italic; 87 | } 88 | 89 | .token.entity { 90 | cursor: help; 91 | } 92 | 93 | /* Dark */ 94 | .dark code[class*='language-'], 95 | .dark pre[class*='language-'] { 96 | color: #e1e4e8; 97 | } 98 | 99 | .dark .token.comment, 100 | .dark .token.prolog, 101 | .dark .token.doctype, 102 | .dark .token.cdata { 103 | color: #6a737d; /* comment */ 104 | } 105 | 106 | .dark .token.punctuation { 107 | color: #e1e4e8; /* editor.foreground */ 108 | } 109 | 110 | .dark .token.namespace { 111 | opacity: 0.7; 112 | } 113 | 114 | .dark .token.property, 115 | .dark .token.tag, 116 | .dark .token.boolean, 117 | .dark .token.number, 118 | .dark .token.constant, 119 | .dark .token.symbol, 120 | .dark .token.deleted { 121 | color: #79b8ff; /* constant, entity.name.constant, variable.other.constant */ 122 | } 123 | 124 | .dark .token.selector, 125 | .dark .token.attr-name, 126 | .dark .token.string, 127 | .dark .token.char, 128 | .dark .token.builtin, 129 | .dark .token.inserted { 130 | color: #9ecbff; /* string */ 131 | } 132 | 133 | .dark .token.operator, 134 | .dark .token.entity, 135 | .dark .token.url, 136 | .dark .language-css .token.string, 137 | .dark .style .token.string { 138 | color: #e1e4e8; /* editor.foreground */ 139 | } 140 | 141 | .dark .token.atrule, 142 | .dark .token.attr-value, 143 | .dark .token.keyword { 144 | color: #f97583; /* keyword */ 145 | } 146 | 147 | .dark .token.function, 148 | .dark .token.class-name { 149 | color: #b392f0; /* entity, entity.name */ 150 | } 151 | 152 | .dark .token.regex, 153 | .dark .token.important, 154 | .dark .token.variable { 155 | color: #ffab70; /* variable */ 156 | } 157 | 158 | .dark .token.important, 159 | .dark .token.bold { 160 | font-weight: bold; 161 | } 162 | 163 | .dark .token.italic { 164 | font-style: italic; 165 | } 166 | 167 | .dark .token.entity { 168 | cursor: help; 169 | } 170 | -------------------------------------------------------------------------------- /components/code-view.tsx: -------------------------------------------------------------------------------- 1 | // import "prismjs/plugins/line-numbers/prism-line-numbers.js"; 2 | // import "prismjs/plugins/line-numbers/prism-line-numbers.css"; 3 | import './code-theme.css' 4 | import Prism from 'prismjs' 5 | import 'prismjs/components/prism-javascript' 6 | import 'prismjs/components/prism-jsx' 7 | import 'prismjs/components/prism-python' 8 | import 'prismjs/components/prism-tsx' 9 | import 'prismjs/components/prism-typescript' 10 | import { useEffect } from 'react' 11 | 12 | export function CodeView({ code, lang }: { code: string; lang: string }) { 13 | useEffect(() => { 14 | Prism.highlightAll() 15 | }, [code]) 16 | 17 | return ( 18 |
27 |       {code}
28 |     
29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /components/deploy-dialog.tsx: -------------------------------------------------------------------------------- 1 | import Logo from './logo' 2 | import { CopyButton } from './ui/copy-button' 3 | import { 4 | Select, 5 | SelectContent, 6 | SelectGroup, 7 | SelectItem, 8 | SelectLabel, 9 | SelectTrigger, 10 | SelectValue, 11 | } from './ui/select' 12 | import { publish } from '@/app/actions/publish' 13 | import { Button } from '@/components/ui/button' 14 | import { 15 | DropdownMenu, 16 | DropdownMenuContent, 17 | DropdownMenuTrigger, 18 | } from '@/components/ui/dropdown-menu' 19 | import { Input } from '@/components/ui/input' 20 | import { Duration } from '@/lib/duration' 21 | import { usePostHog } from 'posthog-js/react' 22 | import { useEffect, useState } from 'react' 23 | 24 | export function DeployDialog({ 25 | url, 26 | sbxId, 27 | apiKey, 28 | }: { 29 | url: string 30 | sbxId: string 31 | apiKey: string | undefined 32 | }) { 33 | const posthog = usePostHog() 34 | 35 | const [publishedURL, setPublishedURL] = useState(null) 36 | const [duration, setDuration] = useState(null) 37 | 38 | useEffect(() => { 39 | setPublishedURL(null) 40 | }, [url]) 41 | 42 | async function publishURL(e: React.FormEvent) { 43 | e.preventDefault() 44 | const { url: publishedURL } = await publish( 45 | url, 46 | sbxId, 47 | duration as Duration, 48 | apiKey, 49 | ) 50 | setPublishedURL(publishedURL) 51 | posthog.capture('publish_url', { 52 | url: publishedURL, 53 | }) 54 | } 55 | 56 | return ( 57 | 58 | {/* 59 | 63 | */} 64 | 65 |
Deploy to E2B
66 |
67 | Deploying the fragment will make it publicly accessible to others via 68 | link. 69 |
70 |
71 | The fragment will be available up until the expiration date you choose 72 | and you'll be billed based on our{' '} 73 | 78 | Compute pricing 79 | 80 | . 81 |
82 |
83 | All new accounts receive $100 worth of compute credits. Upgrade to{' '} 84 | 89 | Pro tier 90 | {' '} 91 | for longer expiration. 92 |
93 |
94 | {publishedURL ? ( 95 |
96 | 97 | 98 |
99 | ) : ( 100 | 115 | )} 116 | 123 |
124 |
125 |
126 | ) 127 | } 128 | -------------------------------------------------------------------------------- /components/fragment-code.tsx: -------------------------------------------------------------------------------- 1 | import { CodeView } from './code-view' 2 | import { Button } from './ui/button' 3 | import { CopyButton } from './ui/copy-button' 4 | import { 5 | Tooltip, 6 | TooltipContent, 7 | TooltipProvider, 8 | TooltipTrigger, 9 | } from '@/components/ui/tooltip' 10 | import { Download, FileText } from 'lucide-react' 11 | import { useState } from 'react' 12 | 13 | export function FragmentCode({ 14 | files, 15 | }: { 16 | files: { name: string; content: string }[] 17 | }) { 18 | const [currentFile, setCurrentFile] = useState(files[0].name) 19 | const currentFileContent = files.find( 20 | (file) => file.name === currentFile, 21 | )?.content 22 | 23 | function download(filename: string, content: string) { 24 | const blob = new Blob([content], { type: 'text/plain' }) 25 | const url = window.URL.createObjectURL(blob) 26 | const a = document.createElement('a') 27 | a.style.display = 'none' 28 | a.href = url 29 | a.download = filename 30 | document.body.appendChild(a) 31 | a.click() 32 | window.URL.revokeObjectURL(url) 33 | document.body.removeChild(a) 34 | } 35 | 36 | return ( 37 |
38 |
39 |
40 | {files.map((file) => ( 41 |
setCurrentFile(file.name)} 47 | > 48 | 49 | {file.name} 50 |
51 | ))} 52 |
53 |
54 | 55 | 56 | 57 | 61 | 62 | Copy 63 | 64 | 65 | 66 | 67 | 68 | 78 | 79 | Download 80 | 81 | 82 |
83 |
84 |
85 | 89 |
90 |
91 | ) 92 | } 93 | -------------------------------------------------------------------------------- /components/fragment-interpreter.tsx: -------------------------------------------------------------------------------- 1 | import { Alert, AlertTitle, AlertDescription } from '@/components/ui/alert' 2 | import { ExecutionResultInterpreter } from '@/lib/types' 3 | import { Terminal } from 'lucide-react' 4 | import Image from 'next/image' 5 | 6 | function LogsOutput({ 7 | stdout, 8 | stderr, 9 | }: { 10 | stdout: string[] 11 | stderr: string[] 12 | }) { 13 | if (stdout.length === 0 && stderr.length === 0) return null 14 | 15 | return ( 16 |
17 | {stdout && 18 | stdout.length > 0 && 19 | stdout.map((out: string, index: number) => ( 20 |
21 |             {out}
22 |           
23 | ))} 24 | {stderr && 25 | stderr.length > 0 && 26 | stderr.map((err: string, index: number) => ( 27 |
28 |             {err}
29 |           
30 | ))} 31 |
32 | ) 33 | } 34 | 35 | export function FragmentInterpreter({ 36 | result, 37 | }: { 38 | result: ExecutionResultInterpreter 39 | }) { 40 | const { cellResults, stdout, stderr, runtimeError } = result 41 | 42 | // The AI-generated code experienced runtime error 43 | if (runtimeError) { 44 | const { name, value, traceback } = runtimeError 45 | return ( 46 |
47 | 48 | 49 | 50 | {name}: {value} 51 | 52 | 53 | {traceback} 54 | 55 | 56 |
57 | ) 58 | } 59 | 60 | // Cell results can contain text, pdfs, images, and code (html, latex, json) 61 | // TODO: Show all results 62 | // TODO: Check other formats than `png` 63 | if (cellResults.length > 0) { 64 | const imgInBase64 = cellResults[0].png 65 | return ( 66 |
67 |
68 | result 74 |
75 | 76 |
77 | ) 78 | } 79 | 80 | // No cell results, but there is stdout or stderr 81 | if (stdout.length > 0 || stderr.length > 0) { 82 | return 83 | } 84 | 85 | return No output or logs 86 | } 87 | -------------------------------------------------------------------------------- /components/fragment-preview.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { FragmentInterpreter } from './fragment-interpreter' 4 | import { FragmentWeb } from './fragment-web' 5 | import { ExecutionResult } from '@/lib/types' 6 | 7 | export function FragmentPreview({ result }: { result: ExecutionResult }) { 8 | if (result.template === 'code-interpreter-v1') { 9 | return 10 | } 11 | 12 | return 13 | } 14 | -------------------------------------------------------------------------------- /components/fragment-web.tsx: -------------------------------------------------------------------------------- 1 | import { CopyButton } from './ui/copy-button' 2 | import { Button } from '@/components/ui/button' 3 | import { 4 | Tooltip, 5 | TooltipContent, 6 | TooltipProvider, 7 | TooltipTrigger, 8 | } from '@/components/ui/tooltip' 9 | import { ExecutionResultWeb } from '@/lib/types' 10 | import { RotateCw } from 'lucide-react' 11 | import { useState } from 'react' 12 | 13 | export function FragmentWeb({ result }: { result: ExecutionResultWeb }) { 14 | const [iframeKey, setIframeKey] = useState(0) 15 | if (!result) return null 16 | 17 | function refreshIframe() { 18 | setIframeKey((prevKey) => prevKey + 1) 19 | } 20 | 21 | return ( 22 |
23 |