├── .env.template ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── CODEOWNERS ├── LICENSE ├── README.md ├── app ├── actions │ ├── publish.ts │ └── validate-email.ts ├── api │ ├── chat │ │ └── route.ts │ └── sandbox │ │ └── route.ts ├── favicon.ico ├── globals.css ├── layout.tsx ├── page.tsx └── providers.tsx ├── components.json ├── components ├── auth-dialog.tsx ├── auth.tsx ├── chat-input.tsx ├── chat-picker.tsx ├── chat-settings.tsx ├── chat.tsx ├── code-theme.css ├── code-view.tsx ├── deploy-dialog.tsx ├── fragment-code.tsx ├── fragment-interpreter.tsx ├── fragment-preview.tsx ├── fragment-web.tsx ├── icons.tsx ├── logo.tsx ├── navbar.tsx ├── preview.tsx ├── repo-banner.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 ├── lib ├── auth.ts ├── duration.ts ├── messages.ts ├── models.json ├── models.ts ├── prompt.ts ├── ratelimit.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 ├── public └── thirdparty │ ├── logos │ ├── anthropic.svg │ ├── deepseek.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 ├── readme-assets ├── fragments-dark.png └── fragments-light.png ├── 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.template: -------------------------------------------------------------------------------- 1 | # Get your API key here - https://e2b.dev/ 2 | E2B_API_KEY= 3 | 4 | # OpenAI API Key 5 | OPENAI_API_KEY= 6 | 7 | # Other providers 8 | ANTHROPIC_API_KEY= 9 | GROQ_API_KEY= 10 | FIREWORKS_API_KEY= 11 | TOGETHER_API_KEY= 12 | GOOGLE_AI_API_KEY= 13 | GOOGLE_VERTEX_CREDENTIALS= 14 | MISTRAL_API_KEY= 15 | XAI_API_KEY= 16 | 17 | ### Optional env vars 18 | 19 | # Domain of the site 20 | NEXT_PUBLIC_SITE_URL= 21 | 22 | # Rate limit 23 | RATE_LIMIT_MAX_REQUESTS= 24 | RATE_LIMIT_WINDOW= 25 | 26 | # Vercel/Upstash KV (short URLs, rate limiting) 27 | KV_REST_API_URL= 28 | KV_REST_API_TOKEN= 29 | 30 | # Supabase (auth) 31 | SUPABASE_URL= 32 | SUPABASE_ANON_KEY= 33 | 34 | # PostHog (analytics) 35 | NEXT_PUBLIC_POSTHOG_KEY= 36 | NEXT_PUBLIC_POSTHOG_HOST= 37 | 38 | ### Disabling functionality (when uncommented) 39 | 40 | # Disable API key and base URL input in the chat 41 | # NEXT_PUBLIC_NO_API_KEY_INPUT= 42 | # NEXT_PUBLIC_NO_BASE_URL_INPUT= 43 | 44 | # Hide local models from the list of available models 45 | # NEXT_PUBLIC_HIDE_LOCAL_MODELS= 46 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 @ben-fornefeld @mlejva 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![E2B Fragments Preview Light](/readme-assets/fragments-light.png#gh-light-mode-only) 2 | ![E2B Fragments Preview Dark](/readme-assets/fragments-dark.png#gh-dark-mode-only) 3 | 4 | # Fragments by E2B 5 | 6 | This is an open-source version of apps like [Anthropic's Claude Artifacts](https://www.anthropic.com/news/claude-3-5-sonnet), Vercel [v0](https://v0.dev), or [GPT Engineer](https://gptengineer.app). 7 | 8 | Powered by the [E2B SDK](https://github.com/e2b-dev/code-interpreter). 9 | 10 | [→ Try on fragments.e2b.dev](https://fragments.e2b.dev) 11 | 12 | ## Features 13 | 14 | - Based on Next.js 14 (App Router, Server Actions), shadcn/ui, TailwindCSS, Vercel AI SDK. 15 | - Uses the [E2B SDK](https://github.com/e2b-dev/code-interpreter) by [E2B](https://e2b.dev) to securely execute code generated by AI. 16 | - Streaming in the UI. 17 | - Can install and use any package from npm, pip. 18 | - Supported stacks ([add your own](#adding-custom-personas)): 19 | - 🔸 Python interpreter 20 | - 🔸 Next.js 21 | - 🔸 Vue.js 22 | - 🔸 Streamlit 23 | - 🔸 Gradio 24 | - Supported LLM Providers ([add your own](#adding-custom-llm-models)): 25 | - 🔸 OpenAI 26 | - 🔸 Anthropic 27 | - 🔸 Google AI 28 | - 🔸 Mistral 29 | - 🔸 Groq 30 | - 🔸 Fireworks 31 | - 🔸 Together AI 32 | - 🔸 Ollama 33 | 34 | **Make sure to give us a star!** 35 | 36 | Screenshot 2024-04-20 at 22 13 32 37 | 38 | ## Get started 39 | 40 | ### Prerequisites 41 | 42 | - [git](https://git-scm.com) 43 | - Recent version of [Node.js](https://nodejs.org) and npm package manager 44 | - [E2B API Key](https://e2b.dev) 45 | - LLM Provider API Key 46 | 47 | ### 1. Clone the repository 48 | 49 | In your terminal: 50 | 51 | ``` 52 | git clone https://github.com/e2b-dev/fragments.git 53 | ``` 54 | 55 | ### 2. Install the dependencies 56 | 57 | Enter the repository: 58 | 59 | ``` 60 | cd fragments 61 | ``` 62 | 63 | Run the following to install the required dependencies: 64 | 65 | ``` 66 | npm i 67 | ``` 68 | 69 | ### 3. Set the environment variables 70 | 71 | Create a `.env.local` file and set the following: 72 | 73 | ```sh 74 | # Get your API key here - https://e2b.dev/ 75 | E2B_API_KEY="your-e2b-api-key" 76 | 77 | # OpenAI API Key 78 | OPENAI_API_KEY= 79 | 80 | # Other providers 81 | ANTHROPIC_API_KEY= 82 | GROQ_API_KEY= 83 | FIREWORKS_API_KEY= 84 | TOGETHER_API_KEY= 85 | GOOGLE_AI_API_KEY= 86 | GOOGLE_VERTEX_CREDENTIALS= 87 | MISTRAL_API_KEY= 88 | XAI_API_KEY= 89 | 90 | ### Optional env vars 91 | 92 | # Domain of the site 93 | NEXT_PUBLIC_SITE_URL= 94 | 95 | # Rate limit 96 | RATE_LIMIT_MAX_REQUESTS= 97 | RATE_LIMIT_WINDOW= 98 | 99 | # Vercel/Upstash KV (short URLs, rate limiting) 100 | KV_REST_API_URL= 101 | KV_REST_API_TOKEN= 102 | 103 | # Supabase (auth) 104 | SUPABASE_URL= 105 | SUPABASE_ANON_KEY= 106 | 107 | # PostHog (analytics) 108 | NEXT_PUBLIC_POSTHOG_KEY= 109 | NEXT_PUBLIC_POSTHOG_HOST= 110 | 111 | ### Disabling functionality (when uncommented) 112 | 113 | # Disable API key and base URL input in the chat 114 | # NEXT_PUBLIC_NO_API_KEY_INPUT= 115 | # NEXT_PUBLIC_NO_BASE_URL_INPUT= 116 | 117 | # Hide local models from the list of available models 118 | # NEXT_PUBLIC_HIDE_LOCAL_MODELS= 119 | ``` 120 | 121 | ### 4. Start the development server 122 | 123 | ``` 124 | npm run dev 125 | ``` 126 | 127 | ### 5. Build the web app 128 | 129 | ``` 130 | npm run build 131 | ``` 132 | 133 | ## Customize 134 | 135 | ### Adding custom personas 136 | 137 | 1. Make sure [E2B CLI](https://e2b.dev/docs/cli) is installed and you're logged in. 138 | 139 | 2. Add a new folder under [sandbox-templates/](sandbox-templates/) 140 | 141 | 3. Initialize a new template using E2B CLI: 142 | 143 | ``` 144 | e2b template init 145 | ``` 146 | 147 | This will create a new file called `e2b.Dockerfile`. 148 | 149 | 4. Adjust the `e2b.Dockerfile` 150 | 151 | Here's an example streamlit template: 152 | 153 | ```Dockerfile 154 | # You can use most Debian-based base images 155 | FROM python:3.19-slim 156 | 157 | RUN pip3 install --no-cache-dir streamlit pandas numpy matplotlib requests seaborn plotly 158 | 159 | # Copy the code to the container 160 | WORKDIR /home/user 161 | COPY . /home/user 162 | ``` 163 | 164 | 5. Specify a custom start command in `e2b.toml`: 165 | 166 | ```toml 167 | start_cmd = "cd /home/user && streamlit run app.py" 168 | ``` 169 | 170 | 6. Deploy the template with the E2B CLI 171 | 172 | ``` 173 | e2b template build --name 174 | ``` 175 | 176 | After the build has finished, you should get the following message: 177 | 178 | ``` 179 | ✅ Building sandbox template finished. 180 | ``` 181 | 182 | 7. Open [lib/templates.json](lib/templates.json) in your code editor. 183 | 184 | Add your new template to the list. Here's an example for Streamlit: 185 | 186 | ```json 187 | "streamlit-developer": { 188 | "name": "Streamlit developer", 189 | "lib": [ 190 | "streamlit", 191 | "pandas", 192 | "numpy", 193 | "matplotlib", 194 | "request", 195 | "seaborn", 196 | "plotly" 197 | ], 198 | "file": "app.py", 199 | "instructions": "A streamlit app that reloads automatically.", 200 | "port": 8501 // can be null 201 | }, 202 | ``` 203 | 204 | Provide a template id (as key), name, list of dependencies, entrypoint and a port (optional). You can also add additional instructions that will be given to the LLM. 205 | 206 | 4. Optionally, add a new logo under [public/thirdparty/templates](public/thirdparty/templates) 207 | 208 | ### Adding custom LLM models 209 | 210 | 1. Open [lib/models.json](lib/models.ts) in your code editor. 211 | 212 | 2. Add a new entry to the models list: 213 | 214 | ```json 215 | { 216 | "id": "mistral-large", 217 | "name": "Mistral Large", 218 | "provider": "Ollama", 219 | "providerId": "ollama" 220 | } 221 | ``` 222 | 223 | Where id is the model id, name is the model name (visible in the UI), provider is the provider name and providerId is the provider tag (see [adding providers](#adding-custom-llm-providers) below). 224 | 225 | ### Adding custom LLM providers 226 | 227 | 1. Open [lib/models.ts](lib/models.ts) in your code editor. 228 | 229 | 2. Add a new entry to the `providerConfigs` list: 230 | 231 | Example for fireworks: 232 | 233 | ```ts 234 | fireworks: () => createOpenAI({ apiKey: apiKey || process.env.FIREWORKS_API_KEY, baseURL: baseURL || 'https://api.fireworks.ai/inference/v1' })(modelNameString), 235 | ``` 236 | 237 | 3. Optionally, adjust the default structured output mode in the `getDefaultMode` function: 238 | 239 | ```ts 240 | if (providerId === 'fireworks') { 241 | return 'json' 242 | } 243 | ``` 244 | 245 | 4. Optionally, add a new logo under [public/thirdparty/logos](public/thirdparty/logos) 246 | 247 | ## Contributing 248 | 249 | As an open-source project, we welcome contributions from the community. If you are experiencing any bugs or want to add some improvements, please feel free to open an issue or pull request. 250 | -------------------------------------------------------------------------------- /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 | teamID: string | undefined, 15 | accessToken: string | undefined, 16 | ) { 17 | const expiration = ms(duration) 18 | await Sandbox.setTimeout(sbxId, expiration, { 19 | ...(teamID && accessToken 20 | ? { 21 | headers: { 22 | 'X-Supabase-Team': teamID, 23 | 'X-Supabase-Token': accessToken, 24 | }, 25 | } 26 | : {}), 27 | }) 28 | 29 | if (process.env.KV_REST_API_URL && process.env.KV_REST_API_TOKEN) { 30 | const id = nanoid() 31 | await kv.set(`fragment:${id}`, url, { px: expiration }) 32 | 33 | return { 34 | url: process.env.NEXT_PUBLIC_SITE_URL 35 | ? `https://${process.env.NEXT_PUBLIC_SITE_URL}/s/${id}` 36 | : `/s/${id}`, 37 | } 38 | } 39 | 40 | return { 41 | url, 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/actions/validate-email.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | export type EmailValidationResponse = { 4 | address: string 5 | status: string 6 | sub_status: string 7 | free_email: boolean 8 | account: string 9 | domain: string 10 | mx_found: boolean 11 | did_you_mean: string | null 12 | domain_age_days: string | null 13 | active_in_days: string | null 14 | smtp_provider: string | null 15 | mx_record: string | null 16 | firstname: string | null 17 | lastname: string | null 18 | gender: string | null 19 | country: string | null 20 | region: string | null 21 | city: string | null 22 | zipcode: string | null 23 | processed_at: string 24 | } 25 | 26 | export async function validateEmail(email: string): Promise { 27 | if (!process.env.ZEROBOUNCE_API_KEY) { 28 | return true 29 | } 30 | 31 | const response = await fetch( 32 | `https://api.zerobounce.net/v2/validate?api_key=${process.env.ZEROBOUNCE_API_KEY}&email=${email}&ip_address=`, 33 | ) 34 | 35 | const responseData = await response.json() 36 | 37 | const data = { 38 | ...responseData, 39 | mx_found: 40 | responseData.mx_found === 'true' 41 | ? true 42 | : responseData.mx_found === 'false' 43 | ? false 44 | : responseData.mx_found, 45 | } as EmailValidationResponse 46 | 47 | switch (data.status) { 48 | case 'invalid': 49 | case 'spamtrap': 50 | case 'abuse': 51 | case 'do_not_mail': 52 | return false 53 | } 54 | 55 | return true 56 | } 57 | -------------------------------------------------------------------------------- /app/api/chat/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 } 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 | teamID, 24 | template, 25 | model, 26 | config, 27 | }: { 28 | messages: CoreMessage[] 29 | userID: string | undefined 30 | teamID: string | undefined 31 | template: Templates 32 | model: LLMModel 33 | config: LLMModelConfig 34 | } = await req.json() 35 | 36 | const limit = !config.apiKey 37 | ? await ratelimit( 38 | req.headers.get('x-forwarded-for'), 39 | rateLimitMaxRequests, 40 | ratelimitWindow, 41 | ) 42 | : false 43 | 44 | if (limit) { 45 | return new Response('You have reached your request limit for the day.', { 46 | status: 429, 47 | headers: { 48 | 'X-RateLimit-Limit': limit.amount.toString(), 49 | 'X-RateLimit-Remaining': limit.remaining.toString(), 50 | 'X-RateLimit-Reset': limit.reset.toString(), 51 | }, 52 | }) 53 | } 54 | 55 | console.log('userID', userID) 56 | console.log('teamID', teamID) 57 | // console.log('template', template) 58 | console.log('model', model) 59 | // console.log('config', config) 60 | 61 | const { model: modelNameString, apiKey: modelApiKey, ...modelParams } = config 62 | const modelClient = getModelClient(model, config) 63 | 64 | try { 65 | const stream = await streamObject({ 66 | model: modelClient as LanguageModel, 67 | schema, 68 | system: toPrompt(template), 69 | messages, 70 | maxRetries: 0, // do not retry on errors 71 | ...modelParams, 72 | }) 73 | 74 | return stream.toTextStreamResponse() 75 | } catch (error: any) { 76 | const isRateLimitError = 77 | error && (error.statusCode === 429 || error.message.includes('limit')) 78 | const isOverloadedError = 79 | error && (error.statusCode === 529 || error.statusCode === 503) 80 | const isAccessDeniedError = 81 | error && (error.statusCode === 403 || error.statusCode === 401) 82 | 83 | if (isRateLimitError) { 84 | return new Response( 85 | 'The provider is currently unavailable due to request limit. Try using your own API key.', 86 | { 87 | status: 429, 88 | }, 89 | ) 90 | } 91 | 92 | if (isOverloadedError) { 93 | return new Response( 94 | 'The provider is currently unavailable. Please try again later.', 95 | { 96 | status: 529, 97 | }, 98 | ) 99 | } 100 | 101 | if (isAccessDeniedError) { 102 | return new Response( 103 | 'Access denied. Please make sure your API key is valid.', 104 | { 105 | status: 403, 106 | }, 107 | ) 108 | } 109 | 110 | console.error('Error:', error) 111 | 112 | return new Response( 113 | 'An unexpected error has occurred. Please try again later.', 114 | { 115 | status: 500, 116 | }, 117 | ) 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /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 | teamID, 14 | accessToken, 15 | }: { 16 | fragment: FragmentSchema 17 | userID: string | undefined 18 | teamID: string | undefined 19 | accessToken: string | undefined 20 | } = await req.json() 21 | console.log('fragment', fragment) 22 | console.log('userID', userID) 23 | // console.log('apiKey', apiKey) 24 | 25 | // Create an interpreter or a sandbox 26 | const sbx = await Sandbox.create(fragment.template, { 27 | metadata: { 28 | template: fragment.template, 29 | userID: userID ?? '', 30 | teamID: teamID ?? '', 31 | }, 32 | timeoutMs: sandboxTimeout, 33 | ...(teamID && accessToken 34 | ? { 35 | headers: { 36 | 'X-Supabase-Team': teamID, 37 | 'X-Supabase-Token': accessToken, 38 | }, 39 | } 40 | : {}), 41 | }) 42 | 43 | // Install packages 44 | if (fragment.has_additional_dependencies) { 45 | await sbx.commands.run(fragment.install_dependencies_command) 46 | console.log( 47 | `Installed dependencies: ${fragment.additional_dependencies.join(', ')} in sandbox ${sbx.sandboxId}`, 48 | ) 49 | } 50 | 51 | // Copy code to fs 52 | if (fragment.code && Array.isArray(fragment.code)) { 53 | fragment.code.forEach(async (file) => { 54 | await sbx.files.write(file.file_path, file.file_content) 55 | console.log(`Copied file to ${file.file_path} in ${sbx.sandboxId}`) 56 | }) 57 | } else { 58 | await sbx.files.write(fragment.file_path, fragment.code) 59 | console.log(`Copied file to ${fragment.file_path} in ${sbx.sandboxId}`) 60 | } 61 | 62 | // Execute code or return a URL to the running sandbox 63 | if (fragment.template === 'code-interpreter-v1') { 64 | const { logs, error, results } = await sbx.runCode(fragment.code || '') 65 | 66 | return new Response( 67 | JSON.stringify({ 68 | sbxId: sbx?.sandboxId, 69 | template: fragment.template, 70 | stdout: logs.stdout, 71 | stderr: logs.stderr, 72 | runtimeError: error, 73 | cellResults: results, 74 | } as ExecutionResultInterpreter), 75 | ) 76 | } 77 | 78 | return new Response( 79 | JSON.stringify({ 80 | sbxId: sbx?.sandboxId, 81 | template: fragment.template, 82 | url: `https://${sbx?.getHost(fragment.port || 80)}`, 83 | } as ExecutionResultWeb), 84 | ) 85 | } 86 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/e2b-dev/fragments/0b97dcbcd38e0a2d42dbdd6da60013e223c8ce2f/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 { Analytics } from '@vercel/analytics/next' 5 | import type { Metadata } from 'next' 6 | import { Inter } from 'next/font/google' 7 | 8 | const inter = Inter({ subsets: ['latin'] }) 9 | 10 | export const metadata: Metadata = { 11 | title: 'Fragments by E2B', 12 | description: "Open-source version of Anthropic's Artifacts", 13 | } 14 | 15 | export default function RootLayout({ 16 | children, 17 | }: Readonly<{ 18 | children: React.ReactNode 19 | }>) { 20 | return ( 21 | 22 | 23 | 24 | 30 | {children} 31 | 32 | 33 | 34 | 35 | 36 | 37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { ViewType } from '@/components/auth' 4 | import { AuthDialog } from '@/components/auth-dialog' 5 | import { Chat } from '@/components/chat' 6 | import { ChatInput } from '@/components/chat-input' 7 | import { ChatPicker } from '@/components/chat-picker' 8 | import { ChatSettings } from '@/components/chat-settings' 9 | import { NavBar } from '@/components/navbar' 10 | import { Preview } from '@/components/preview' 11 | import { useAuth } from '@/lib/auth' 12 | import { Message, toAISDKMessages, toMessageImage } from '@/lib/messages' 13 | import { LLMModelConfig } from '@/lib/models' 14 | import modelsList from '@/lib/models.json' 15 | import { FragmentSchema, fragmentSchema as schema } from '@/lib/schema' 16 | import { supabase } from '@/lib/supabase' 17 | import templates, { TemplateId } from '@/lib/templates' 18 | import { ExecutionResult } from '@/lib/types' 19 | import { DeepPartial } from 'ai' 20 | import { experimental_useObject as useObject } from 'ai/react' 21 | import { usePostHog } from 'posthog-js/react' 22 | import { SetStateAction, useEffect, useState } from 'react' 23 | import { useLocalStorage } from 'usehooks-ts' 24 | 25 | export default function Home() { 26 | const [chatInput, setChatInput] = useLocalStorage('chat', '') 27 | const [files, setFiles] = useState([]) 28 | const [selectedTemplate, setSelectedTemplate] = useState<'auto' | TemplateId>( 29 | 'auto', 30 | ) 31 | const [languageModel, setLanguageModel] = useLocalStorage( 32 | 'languageModel', 33 | { 34 | model: 'claude-3-5-sonnet-latest', 35 | }, 36 | ) 37 | 38 | const posthog = usePostHog() 39 | 40 | const [result, setResult] = useState() 41 | const [messages, setMessages] = useState([]) 42 | const [fragment, setFragment] = useState>() 43 | const [currentTab, setCurrentTab] = useState<'code' | 'fragment'>('code') 44 | const [isPreviewLoading, setIsPreviewLoading] = useState(false) 45 | const [isAuthDialogOpen, setAuthDialog] = useState(false) 46 | const [authView, setAuthView] = useState('sign_in') 47 | const [isRateLimited, setIsRateLimited] = useState(false) 48 | const [errorMessage, setErrorMessage] = useState('') 49 | const { session, userTeam } = useAuth(setAuthDialog, setAuthView) 50 | 51 | const filteredModels = modelsList.models.filter((model) => { 52 | if (process.env.NEXT_PUBLIC_HIDE_LOCAL_MODELS) { 53 | return model.providerId !== 'ollama' 54 | } 55 | return true 56 | }) 57 | 58 | const currentModel = filteredModels.find( 59 | (model) => model.id === languageModel.model, 60 | ) 61 | const currentTemplate = 62 | selectedTemplate === 'auto' 63 | ? templates 64 | : { [selectedTemplate]: templates[selectedTemplate] } 65 | const lastMessage = messages[messages.length - 1] 66 | 67 | const { object, submit, isLoading, stop, error } = useObject({ 68 | api: '/api/chat', 69 | schema, 70 | onError: (error) => { 71 | console.error('Error submitting request:', error) 72 | if (error.message.includes('limit')) { 73 | setIsRateLimited(true) 74 | } 75 | 76 | setErrorMessage(error.message) 77 | }, 78 | onFinish: async ({ object: fragment, error }) => { 79 | if (!error) { 80 | // send it to /api/sandbox 81 | console.log('fragment', fragment) 82 | setIsPreviewLoading(true) 83 | posthog.capture('fragment_generated', { 84 | template: fragment?.template, 85 | }) 86 | 87 | const response = await fetch('/api/sandbox', { 88 | method: 'POST', 89 | body: JSON.stringify({ 90 | fragment, 91 | userID: session?.user?.id, 92 | teamID: userTeam?.id, 93 | accessToken: session?.access_token, 94 | }), 95 | }) 96 | 97 | const result = await response.json() 98 | console.log('result', result) 99 | posthog.capture('sandbox_created', { url: result.url }) 100 | 101 | setResult(result) 102 | setCurrentPreview({ fragment, result }) 103 | setMessage({ result }) 104 | setCurrentTab('fragment') 105 | setIsPreviewLoading(false) 106 | } 107 | }, 108 | }) 109 | 110 | useEffect(() => { 111 | if (object) { 112 | setFragment(object) 113 | const content: Message['content'] = [ 114 | { type: 'text', text: object.commentary || '' }, 115 | { type: 'code', text: object.code || '' }, 116 | ] 117 | 118 | if (!lastMessage || lastMessage.role !== 'assistant') { 119 | addMessage({ 120 | role: 'assistant', 121 | content, 122 | object, 123 | }) 124 | } 125 | 126 | if (lastMessage && lastMessage.role === 'assistant') { 127 | setMessage({ 128 | content, 129 | object, 130 | }) 131 | } 132 | } 133 | }, [object]) 134 | 135 | useEffect(() => { 136 | if (error) stop() 137 | }, [error]) 138 | 139 | function setMessage(message: Partial, index?: number) { 140 | setMessages((previousMessages) => { 141 | const updatedMessages = [...previousMessages] 142 | updatedMessages[index ?? previousMessages.length - 1] = { 143 | ...previousMessages[index ?? previousMessages.length - 1], 144 | ...message, 145 | } 146 | 147 | return updatedMessages 148 | }) 149 | } 150 | 151 | async function handleSubmitAuth(e: React.FormEvent) { 152 | e.preventDefault() 153 | 154 | if (!session) { 155 | return setAuthDialog(true) 156 | } 157 | 158 | if (isLoading) { 159 | stop() 160 | } 161 | 162 | const content: Message['content'] = [{ type: 'text', text: chatInput }] 163 | const images = await toMessageImage(files) 164 | 165 | if (images.length > 0) { 166 | images.forEach((image) => { 167 | content.push({ type: 'image', image }) 168 | }) 169 | } 170 | 171 | const updatedMessages = addMessage({ 172 | role: 'user', 173 | content, 174 | }) 175 | 176 | submit({ 177 | userID: session?.user?.id, 178 | teamID: userTeam?.id, 179 | messages: toAISDKMessages(updatedMessages), 180 | template: currentTemplate, 181 | model: currentModel, 182 | config: languageModel, 183 | }) 184 | 185 | setChatInput('') 186 | setFiles([]) 187 | setCurrentTab('code') 188 | 189 | posthog.capture('chat_submit', { 190 | template: selectedTemplate, 191 | model: languageModel.model, 192 | }) 193 | } 194 | 195 | function retry() { 196 | submit({ 197 | userID: session?.user?.id, 198 | teamID: userTeam?.id, 199 | messages: toAISDKMessages(messages), 200 | template: currentTemplate, 201 | model: currentModel, 202 | config: languageModel, 203 | }) 204 | } 205 | 206 | function addMessage(message: Message) { 207 | setMessages((previousMessages) => [...previousMessages, message]) 208 | return [...messages, message] 209 | } 210 | 211 | function handleSaveInputChange(e: React.ChangeEvent) { 212 | setChatInput(e.target.value) 213 | } 214 | 215 | function handleFileChange(change: SetStateAction) { 216 | setFiles(change) 217 | } 218 | 219 | function logout() { 220 | supabase 221 | ? supabase.auth.signOut() 222 | : console.warn('Supabase is not initialized') 223 | } 224 | 225 | function handleLanguageModelChange(e: LLMModelConfig) { 226 | setLanguageModel({ ...languageModel, ...e }) 227 | } 228 | 229 | function handleSocialClick(target: 'github' | 'x' | 'discord') { 230 | if (target === 'github') { 231 | window.open('https://github.com/e2b-dev/fragments', '_blank') 232 | } else if (target === 'x') { 233 | window.open('https://x.com/e2b_dev', '_blank') 234 | } else if (target === 'discord') { 235 | window.open('https://discord.gg/U7KEcGErtQ', '_blank') 236 | } 237 | 238 | posthog.capture(`${target}_click`) 239 | } 240 | 241 | function handleClearChat() { 242 | stop() 243 | setChatInput('') 244 | setFiles([]) 245 | setMessages([]) 246 | setFragment(undefined) 247 | setResult(undefined) 248 | setCurrentTab('code') 249 | setIsPreviewLoading(false) 250 | } 251 | 252 | function setCurrentPreview(preview: { 253 | fragment: DeepPartial | undefined 254 | result: ExecutionResult | undefined 255 | }) { 256 | setFragment(preview.fragment) 257 | setResult(preview.result) 258 | } 259 | 260 | function handleUndo() { 261 | setMessages((previousMessages) => [...previousMessages.slice(0, -2)]) 262 | setCurrentPreview({ fragment: undefined, result: undefined }) 263 | } 264 | 265 | return ( 266 |
267 | {supabase && ( 268 | 274 | )} 275 |
276 |
279 | setAuthDialog(true)} 282 | signOut={logout} 283 | onSocialClick={handleSocialClick} 284 | onClear={handleClearChat} 285 | canClear={messages.length > 0} 286 | canUndo={messages.length > 1 && !isLoading} 287 | onUndo={handleUndo} 288 | /> 289 | 294 | 308 | 316 | 322 | 323 |
324 | setFragment(undefined)} 334 | /> 335 |
336 |
337 | ) 338 | } 339 | -------------------------------------------------------------------------------- /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/auth-dialog.tsx: -------------------------------------------------------------------------------- 1 | import Auth, { ViewType } from './auth' 2 | import Logo from './logo' 3 | import { validateEmail } from '@/app/actions/validate-email' 4 | import { 5 | Dialog, 6 | DialogContent, 7 | DialogTitle, 8 | DialogDescription, 9 | } from '@/components/ui/dialog' 10 | import { VisuallyHidden } from '@radix-ui/react-visually-hidden' 11 | import { SupabaseClient } from '@supabase/supabase-js' 12 | 13 | export function AuthDialog({ 14 | open, 15 | setOpen, 16 | supabase, 17 | view, 18 | }: { 19 | open: boolean 20 | setOpen: (open: boolean) => void 21 | supabase: SupabaseClient 22 | view: ViewType 23 | }) { 24 | return ( 25 | 26 | 27 | 28 | Sign in to Fragments 29 | 30 | Sign in or create an account to access Fragments 31 | 32 | 33 |
34 |

35 |
36 | 37 |
38 | Sign in to Fragments 39 |

40 |
41 | 51 |
52 |
53 |
54 |
55 | ) 56 | } 57 | -------------------------------------------------------------------------------- /components/chat-input.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { RepoBanner } from './repo-banner' 4 | import { Button } from '@/components/ui/button' 5 | import { 6 | Tooltip, 7 | TooltipContent, 8 | TooltipProvider, 9 | TooltipTrigger, 10 | } from '@/components/ui/tooltip' 11 | import { isFileInArray } from '@/lib/utils' 12 | import { ArrowUp, Paperclip, Square, X } from 'lucide-react' 13 | import { SetStateAction, useEffect, useMemo, useState } from 'react' 14 | import TextareaAutosize from 'react-textarea-autosize' 15 | 16 | export function ChatInput({ 17 | retry, 18 | isErrored, 19 | errorMessage, 20 | isLoading, 21 | isRateLimited, 22 | stop, 23 | input, 24 | handleInputChange, 25 | handleSubmit, 26 | isMultiModal, 27 | files, 28 | handleFileChange, 29 | children, 30 | }: { 31 | retry: () => void 32 | isErrored: boolean 33 | errorMessage: string 34 | isLoading: boolean 35 | isRateLimited: boolean 36 | stop: () => void 37 | input: string 38 | handleInputChange: (e: React.ChangeEvent) => void 39 | handleSubmit: (e: React.FormEvent) => void 40 | isMultiModal: boolean 41 | files: File[] 42 | handleFileChange: (change: SetStateAction) => void 43 | children: React.ReactNode 44 | }) { 45 | function handleFileInput(e: React.ChangeEvent) { 46 | handleFileChange((prev) => { 47 | const newFiles = Array.from(e.target.files || []) 48 | const uniqueFiles = newFiles.filter((file) => !isFileInArray(file, prev)) 49 | return [...prev, ...uniqueFiles] 50 | }) 51 | } 52 | 53 | function handleFileRemove(file: File) { 54 | handleFileChange((prev) => prev.filter((f) => f !== file)) 55 | } 56 | 57 | function handlePaste(e: React.ClipboardEvent) { 58 | const items = Array.from(e.clipboardData.items) 59 | 60 | for (const item of items) { 61 | if (item.type.indexOf('image') !== -1) { 62 | e.preventDefault() 63 | 64 | const file = item.getAsFile() 65 | if (file) { 66 | handleFileChange((prev) => { 67 | if (!isFileInArray(file, prev)) { 68 | return [...prev, file] 69 | } 70 | return prev 71 | }) 72 | } 73 | } 74 | } 75 | } 76 | 77 | const [dragActive, setDragActive] = useState(false) 78 | 79 | function handleDrag(e: React.DragEvent) { 80 | e.preventDefault() 81 | e.stopPropagation() 82 | if (e.type === 'dragenter' || e.type === 'dragover') { 83 | setDragActive(true) 84 | } else if (e.type === 'dragleave') { 85 | setDragActive(false) 86 | } 87 | } 88 | 89 | function handleDrop(e: React.DragEvent) { 90 | e.preventDefault() 91 | e.stopPropagation() 92 | setDragActive(false) 93 | 94 | const droppedFiles = Array.from(e.dataTransfer.files).filter((file) => 95 | file.type.startsWith('image/'), 96 | ) 97 | 98 | if (droppedFiles.length > 0) { 99 | handleFileChange((prev) => { 100 | const uniqueFiles = droppedFiles.filter( 101 | (file) => !isFileInArray(file, prev), 102 | ) 103 | return [...prev, ...uniqueFiles] 104 | }) 105 | } 106 | } 107 | 108 | const filePreview = useMemo(() => { 109 | if (files.length === 0) return null 110 | return Array.from(files).map((file) => { 111 | return ( 112 |
113 | handleFileRemove(file)} 115 | className="absolute top-[-8] right-[-8] bg-muted rounded-full p-1" 116 | > 117 | 118 | 119 | {file.name} 124 |
125 | ) 126 | }) 127 | }, [files]) 128 | 129 | function onEnter(e: React.KeyboardEvent) { 130 | if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) { 131 | e.preventDefault() 132 | if (e.currentTarget.checkValidity()) { 133 | handleSubmit(e) 134 | } else { 135 | e.currentTarget.reportValidity() 136 | } 137 | } 138 | } 139 | 140 | useEffect(() => { 141 | if (!isMultiModal) { 142 | handleFileChange([]) 143 | } 144 | }, [isMultiModal]) 145 | 146 | return ( 147 |
156 | {isErrored && ( 157 |
164 | {errorMessage} 165 | 173 |
174 | )} 175 |
176 | 177 |
184 |
{children}
185 | 197 |
198 | 207 |
208 | 209 | 210 | 211 | 224 | 225 | Add attachments 226 | 227 | 228 | {files.length > 0 && filePreview} 229 |
230 |
231 | {!isLoading ? ( 232 | 233 | 234 | 235 | 244 | 245 | Send message 246 | 247 | 248 | ) : ( 249 | 250 | 251 | 252 | 263 | 264 | Stop generation 265 | 266 | 267 | )} 268 |
269 |
270 |
271 |
272 |

273 | Fragments is an open-source project made by{' '} 274 | 275 | ✶ E2B 276 | 277 |

278 |
279 | ) 280 | } 281 | -------------------------------------------------------------------------------- /components/chat-picker.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Select, 3 | SelectContent, 4 | SelectGroup, 5 | SelectItem, 6 | SelectLabel, 7 | SelectTrigger, 8 | SelectValue, 9 | } from '@/components/ui/select' 10 | import { LLMModel, LLMModelConfig } from '@/lib/models' 11 | import { TemplateId, Templates } from '@/lib/templates' 12 | import 'core-js/features/object/group-by.js' 13 | import { Sparkles } from 'lucide-react' 14 | import Image from 'next/image' 15 | 16 | export function ChatPicker({ 17 | templates, 18 | selectedTemplate, 19 | onSelectedTemplateChange, 20 | models, 21 | languageModel, 22 | onLanguageModelChange, 23 | }: { 24 | templates: Templates 25 | selectedTemplate: 'auto' | TemplateId 26 | onSelectedTemplateChange: (template: 'auto' | TemplateId) => void 27 | models: LLMModel[] 28 | languageModel: LLMModelConfig 29 | onLanguageModelChange: (config: LLMModelConfig) => void 30 | }) { 31 | return ( 32 |
33 |
34 | 72 |
73 |
74 | 106 |
107 |
108 | ) 109 | } 110 | -------------------------------------------------------------------------------- /components/chat-settings.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from './ui/button' 2 | import { 3 | DropdownMenu, 4 | DropdownMenuContent, 5 | DropdownMenuSeparator, 6 | DropdownMenuTrigger, 7 | } from './ui/dropdown-menu' 8 | import { Input } from './ui/input' 9 | import { Label } from './ui/label' 10 | import { 11 | Tooltip, 12 | TooltipContent, 13 | TooltipProvider, 14 | TooltipTrigger, 15 | } from './ui/tooltip' 16 | import { LLMModelConfig } from '@/lib/models' 17 | import { Settings2 } from 'lucide-react' 18 | 19 | export function ChatSettings({ 20 | apiKeyConfigurable, 21 | baseURLConfigurable, 22 | languageModel, 23 | onLanguageModelChange, 24 | }: { 25 | apiKeyConfigurable: boolean 26 | baseURLConfigurable: boolean 27 | languageModel: LLMModelConfig 28 | onLanguageModelChange: (model: LLMModelConfig) => void 29 | }) { 30 | return ( 31 | 32 | 33 | 34 | 35 | 36 | 39 | 40 | 41 | LLM settings 42 | 43 | 44 | 45 | {apiKeyConfigurable && ( 46 | <> 47 |
48 | 49 | 56 | onLanguageModelChange({ 57 | apiKey: 58 | e.target.value.length > 0 ? e.target.value : undefined, 59 | }) 60 | } 61 | className="text-sm" 62 | /> 63 |
64 | 65 | 66 | )} 67 | {baseURLConfigurable && ( 68 | <> 69 |
70 | 71 | 78 | onLanguageModelChange({ 79 | baseURL: 80 | e.target.value.length > 0 ? e.target.value : undefined, 81 | }) 82 | } 83 | className="text-sm" 84 | /> 85 |
86 | 87 | 88 | )} 89 |
90 | Parameters 91 |
92 | 93 | Output tokens 94 | 95 | 104 | onLanguageModelChange({ 105 | maxTokens: parseFloat(e.target.value) || undefined, 106 | }) 107 | } 108 | /> 109 |
110 |
111 | 112 | Temperature 113 | 114 | 123 | onLanguageModelChange({ 124 | temperature: parseFloat(e.target.value) || undefined, 125 | }) 126 | } 127 | /> 128 |
129 |
130 | Top P 131 | 140 | onLanguageModelChange({ 141 | topP: parseFloat(e.target.value) || undefined, 142 | }) 143 | } 144 | /> 145 |
146 |
147 | Top K 148 | 157 | onLanguageModelChange({ 158 | topK: parseFloat(e.target.value) || undefined, 159 | }) 160 | } 161 | /> 162 |
163 |
164 | 165 | Frequence penalty 166 | 167 | 176 | onLanguageModelChange({ 177 | frequencyPenalty: parseFloat(e.target.value) || undefined, 178 | }) 179 | } 180 | /> 181 |
182 |
183 | 184 | Presence penalty 185 | 186 | 195 | onLanguageModelChange({ 196 | presencePenalty: parseFloat(e.target.value) || undefined, 197 | }) 198 | } 199 | /> 200 |
201 |
202 |
203 |
204 | ) 205 | } 206 | -------------------------------------------------------------------------------- /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 { LoaderIcon, Terminal } from 'lucide-react' 6 | import { useEffect } from 'react' 7 | 8 | export function Chat({ 9 | messages, 10 | isLoading, 11 | setCurrentPreview, 12 | }: { 13 | messages: Message[] 14 | isLoading: boolean 15 | setCurrentPreview: (preview: { 16 | fragment: DeepPartial | undefined 17 | result: ExecutionResult | undefined 18 | }) => void 19 | }) { 20 | useEffect(() => { 21 | const chatContainer = document.getElementById('chat-container') 22 | if (chatContainer) { 23 | chatContainer.scrollTop = chatContainer.scrollHeight 24 | } 25 | }, [JSON.stringify(messages)]) 26 | 27 | return ( 28 |
32 | {messages.map((message: Message, index: number) => ( 33 |
37 | {message.content.map((content, id) => { 38 | if (content.type === 'text') { 39 | return content.text 40 | } 41 | if (content.type === 'image') { 42 | return ( 43 | fragment 49 | ) 50 | } 51 | })} 52 | {message.object && ( 53 |
55 | setCurrentPreview({ 56 | fragment: message.object, 57 | result: message.result, 58 | }) 59 | } 60 | 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" 61 | > 62 |
63 | 64 |
65 |
66 | 67 | {message.object.title} 68 | 69 | 70 | Click to see fragment 71 | 72 |
73 |
74 | )} 75 |
76 | ))} 77 | {isLoading && ( 78 |
79 | 80 | Generating... 81 |
82 | )} 83 |
84 | ) 85 | } 86 | -------------------------------------------------------------------------------- /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 | teamID, 28 | accessToken, 29 | }: { 30 | url: string 31 | sbxId: string 32 | teamID: string | undefined 33 | accessToken: string | undefined 34 | }) { 35 | const posthog = usePostHog() 36 | 37 | const [publishedURL, setPublishedURL] = useState(null) 38 | const [duration, setDuration] = useState(null) 39 | 40 | useEffect(() => { 41 | setPublishedURL(null) 42 | }, [url]) 43 | 44 | async function publishURL(e: React.FormEvent) { 45 | e.preventDefault() 46 | const { url: publishedURL } = await publish( 47 | url, 48 | sbxId, 49 | duration as Duration, 50 | teamID, 51 | accessToken, 52 | ) 53 | setPublishedURL(publishedURL) 54 | posthog.capture('publish_url', { 55 | url: publishedURL, 56 | }) 57 | } 58 | 59 | return ( 60 | 61 | 62 | 66 | 67 | 68 |
Deploy to E2B
69 |
70 | Deploying the fragment will make it publicly accessible to others via 71 | link. 72 |
73 |
74 | The fragment will be available up until the expiration date you choose 75 | and you'll be billed based on our{' '} 76 | 81 | Compute pricing 82 | 83 | . 84 |
85 |
86 | All new accounts receive $100 worth of compute credits. Upgrade to{' '} 87 | 92 | Pro tier 93 | {' '} 94 | for longer expiration. 95 |
96 |
97 | {publishedURL ? ( 98 |
99 | 100 | 101 |
102 | ) : ( 103 | 118 | )} 119 | 126 |
127 |
128 |
129 | ) 130 | } 131 | -------------------------------------------------------------------------------- /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 |