├── .assets ├── header.png └── preview.png ├── frontend ├── public │ ├── bg.mp4 │ ├── suisse.ttf │ ├── xspace.ttf │ ├── logo-black.png │ ├── logo-white.png │ ├── vue-logo.png │ ├── django-logo.png │ ├── express-logo.png │ ├── nextjs-logo.png │ ├── december-logo.png │ ├── vercel.svg │ ├── window.svg │ ├── file.svg │ ├── globe.svg │ └── next.svg ├── src │ ├── app │ │ ├── favicon.ico │ │ ├── page.tsx │ │ ├── community │ │ │ ├── page.tsx │ │ │ └── components │ │ │ │ └── CommunityPage.tsx │ │ ├── editor │ │ │ ├── components │ │ │ │ ├── Sidebar.tsx │ │ │ │ ├── Icon.tsx │ │ │ │ ├── FileTree.tsx │ │ │ │ └── Code.tsx │ │ │ ├── utils │ │ │ │ ├── index.tsx │ │ │ │ ├── FileManager.tsx │ │ │ │ └── Code.tsx │ │ │ ├── IDE.tsx │ │ │ └── CodeEditor.tsx │ │ ├── projects │ │ │ ├── [containerId] │ │ │ │ └── page.tsx │ │ │ └── components │ │ │ │ ├── ProjectsPage.tsx │ │ │ │ ├── ProjectsLayout.tsx │ │ │ │ ├── CreateProjectCard.tsx │ │ │ │ ├── TemplatesSection.tsx │ │ │ │ ├── TopNavigation.tsx │ │ │ │ ├── LivePreview.tsx │ │ │ │ ├── ProjectCard.tsx │ │ │ │ ├── ProjectsGrid.tsx │ │ │ │ └── ProjectPromptInterface.tsx │ │ ├── create │ │ │ └── components │ │ │ │ ├── ChatSidebar.tsx │ │ │ │ ├── CryptoPlaceholder.tsx │ │ │ │ ├── TopNavigation.tsx │ │ │ │ ├── CreateDashboard.tsx │ │ │ │ └── ChatInput.tsx │ │ ├── globals.css │ │ └── layout.tsx │ └── lib │ │ └── backend │ │ └── api.ts ├── postcss.config.mjs ├── next.config.ts ├── tsconfig.json ├── package.json └── README.md ├── .github └── ISSUE_TEMPLATE │ ├── custom.md │ ├── bug_report.md │ └── feature_request.md ├── backend ├── README.md ├── package.json ├── src │ ├── Dockerfile │ ├── services │ │ ├── package.ts │ │ ├── export.ts │ │ ├── llm.ts │ │ ├── docker.ts │ │ └── file.ts │ ├── index.ts │ └── routes │ │ ├── chat.ts │ │ └── containers.ts └── tsconfig.json ├── start.sh ├── config.ts ├── LICENCE ├── .gitignore ├── README.md └── CONTRIBUTING.md /.assets/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ntegrals/december/HEAD/.assets/header.png -------------------------------------------------------------------------------- /.assets/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ntegrals/december/HEAD/.assets/preview.png -------------------------------------------------------------------------------- /frontend/public/bg.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ntegrals/december/HEAD/frontend/public/bg.mp4 -------------------------------------------------------------------------------- /frontend/public/suisse.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ntegrals/december/HEAD/frontend/public/suisse.ttf -------------------------------------------------------------------------------- /frontend/public/xspace.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ntegrals/december/HEAD/frontend/public/xspace.ttf -------------------------------------------------------------------------------- /frontend/public/logo-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ntegrals/december/HEAD/frontend/public/logo-black.png -------------------------------------------------------------------------------- /frontend/public/logo-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ntegrals/december/HEAD/frontend/public/logo-white.png -------------------------------------------------------------------------------- /frontend/public/vue-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ntegrals/december/HEAD/frontend/public/vue-logo.png -------------------------------------------------------------------------------- /frontend/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ntegrals/december/HEAD/frontend/src/app/favicon.ico -------------------------------------------------------------------------------- /frontend/public/django-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ntegrals/december/HEAD/frontend/public/django-logo.png -------------------------------------------------------------------------------- /frontend/public/express-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ntegrals/december/HEAD/frontend/public/express-logo.png -------------------------------------------------------------------------------- /frontend/public/nextjs-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ntegrals/december/HEAD/frontend/public/nextjs-logo.png -------------------------------------------------------------------------------- /frontend/public/december-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ntegrals/december/HEAD/frontend/public/december-logo.png -------------------------------------------------------------------------------- /frontend/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: ["@tailwindcss/postcss"], 3 | }; 4 | 5 | export default config; 6 | -------------------------------------------------------------------------------- /frontend/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/custom.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Custom issue template 3 | about: Describe this issue template's purpose here. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | -------------------------------------------------------------------------------- /frontend/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { ProjectsPage } from "./projects/components/ProjectsPage"; 2 | 3 | export default function Projects() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /frontend/src/app/community/page.tsx: -------------------------------------------------------------------------------- 1 | import { CommunityPage } from "./components/CommunityPage"; 2 | 3 | export default function Community() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /frontend/next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | devIndicators: false, 6 | }; 7 | 8 | export default nextConfig; 9 | -------------------------------------------------------------------------------- /backend/README.md: -------------------------------------------------------------------------------- 1 | # backend 2 | 3 | To install dependencies: 4 | 5 | ```bash 6 | bun install 7 | ``` 8 | 9 | To run: 10 | 11 | ```bash 12 | bun run index.ts 13 | ``` 14 | 15 | This project was created using `bun init` in bun v1.2.5. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. 16 | -------------------------------------------------------------------------------- /frontend/src/app/editor/components/Sidebar.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | 3 | export const Sidebar = ({ children }: { children: ReactNode }) => { 4 | return ( 5 | 8 | ); 9 | }; 10 | 11 | export default Sidebar; 12 | -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Starting backend server..." 4 | 5 | # copy config file to backend directory 6 | cp config.ts backend/config.ts 7 | cd backend && bun src/index.ts & 8 | 9 | echo "Starting frontend server..." 10 | cd frontend && bun dev & 11 | 12 | echo "Both servers are starting..." 13 | echo "Press Ctrl+C to stop both servers" 14 | 15 | wait -------------------------------------------------------------------------------- /frontend/public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/app/projects/[containerId]/page.tsx: -------------------------------------------------------------------------------- 1 | import { WorkspaceDashboard } from "../components/WorkspaceDashboard"; 2 | 3 | interface ContainerPageProps { 4 | params: Promise<{ 5 | containerId: string; 6 | }>; 7 | } 8 | 9 | export default async function ContainerPage({ params }: ContainerPageProps) { 10 | const { containerId } = await params; 11 | 12 | return ; 13 | } 14 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "module": "index.ts", 4 | "type": "module", 5 | "private": true, 6 | "devDependencies": { 7 | "@types/bun": "latest", 8 | "@types/express": "^5.0.2" 9 | }, 10 | "peerDependencies": { 11 | "typescript": "^5" 12 | }, 13 | "dependencies": { 14 | "@types/dockerode": "^3.3.39", 15 | "dockerode": "^4.0.6", 16 | "express": "^5.1.0", 17 | "openai": "^5.0.1", 18 | "uuid": "^11.1.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /backend/src/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine 2 | 3 | RUN apk add --no-cache curl bash 4 | 5 | RUN curl -fsSL https://bun.sh/install | bash 6 | ENV PATH="/root/.bun/bin:$PATH" 7 | 8 | WORKDIR /app 9 | 10 | # Clone the December Next.js template repository 11 | RUN curl -L https://github.com/ntegrals/december-nextjs-template/archive/main.tar.gz | tar -xz && \ 12 | mv december-nextjs-template-main my-nextjs-app 13 | 14 | WORKDIR /app/my-nextjs-app 15 | 16 | RUN bun install 17 | 18 | EXPOSE 3001 19 | 20 | CMD ["bun", "dev"] -------------------------------------------------------------------------------- /config.ts: -------------------------------------------------------------------------------- 1 | // Make sure to replace the values with your actual API key and model 2 | 3 | // USING ANTHROPIC CLAUDE SONNET 4 is strongly recommended for best results 4 | 5 | export const config = { 6 | aiSdk: { 7 | // The base URL for the AI SDK, leave blank for e.g. openai 8 | baseUrl: "https://openrouter.ai/api/v1", 9 | 10 | // Your API key for provider, if using Ollama enter "ollama" here 11 | apiKey: "sk-or-v1-824...", 12 | 13 | // The model to use, e.g., "gpt-4", "gpt-3.5-turbo", or "ollama/llama2" 14 | model: "anthropic/claude-sonnet-4", 15 | }, 16 | } as const; 17 | -------------------------------------------------------------------------------- /frontend/src/app/editor/utils/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { buildFileTree, Directory } from "./FileManager"; 3 | 4 | export const useFilesFromSandbox = ( 5 | id: string, 6 | callback: (dir: Directory) => void 7 | ) => { 8 | React.useEffect(() => { 9 | fetch("https://codesandbox.io/api/v1/sandboxes/" + id) 10 | .then((response) => response.json()) 11 | .then(({ data }) => { 12 | const rootDir = buildFileTree(data); 13 | callback(rootDir); 14 | }); 15 | // eslint-disable-next-line react-hooks/exhaustive-deps 16 | }, []); 17 | }; 18 | -------------------------------------------------------------------------------- /backend/src/services/package.ts: -------------------------------------------------------------------------------- 1 | import { exec } from "child_process"; 2 | import { promisify } from "util"; 3 | 4 | const execAsync = promisify(exec); 5 | const BASE_PATH = "/app/my-nextjs-app"; 6 | 7 | export async function addDependency( 8 | containerId: string, 9 | packageName: string, 10 | isDev: boolean = false 11 | ): Promise { 12 | const devFlag = isDev ? "--dev" : ""; 13 | const addCommand = 14 | `docker exec -w ${BASE_PATH} ${containerId} bun add ${packageName} ${devFlag}`.trim(); 15 | 16 | const { stdout, stderr } = await execAsync(addCommand); 17 | return stdout || stderr; 18 | } 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create an issue to help us fix bugs 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // Environment setup & latest features 4 | "lib": ["esnext"], 5 | "target": "ESNext", 6 | "module": "ESNext", 7 | "moduleDetection": "force", 8 | "jsx": "react-jsx", 9 | "allowJs": true, 10 | 11 | // Bundler mode 12 | "moduleResolution": "bundler", 13 | "allowImportingTsExtensions": true, 14 | "verbatimModuleSyntax": true, 15 | "noEmit": true, 16 | 17 | // Best practices 18 | "strict": true, 19 | "skipLibCheck": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "noUncheckedIndexedAccess": true, 22 | 23 | // Some stricter flags (disabled by default) 24 | "noUnusedLocals": false, 25 | "noUnusedParameters": false, 26 | "noPropertyAccessFromIndexSignature": false 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@monaco-editor/react": "^4.7.0", 13 | "lucide-react": "^0.511.0", 14 | "next": "15.3.2", 15 | "react": "^19.0.0", 16 | "react-dom": "^19.0.0", 17 | "react-hot-toast": "^2.5.2", 18 | "react-icons": "^5.5.0" 19 | }, 20 | "devDependencies": { 21 | "typescript": "^5", 22 | "@types/node": "^20", 23 | "@types/react": "^19", 24 | "@types/react-dom": "^19", 25 | "@tailwindcss/postcss": "^4", 26 | "tailwindcss": "^4", 27 | "eslint": "^9", 28 | "eslint-config-next": "15.3.2", 29 | "@eslint/eslintrc": "^3" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /backend/src/index.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import chatRoutes from "./routes/chat"; 3 | import containerRoutes from "./routes/containers"; 4 | 5 | const app = express(); 6 | 7 | app.use((req, res, next) => { 8 | res.header("Access-Control-Allow-Origin", "*"); 9 | res.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"); 10 | res.header( 11 | "Access-Control-Allow-Headers", 12 | "Origin, X-Requested-With, Content-Type, Accept, Authorization" 13 | ); 14 | 15 | if (req.method === "OPTIONS") { 16 | res.sendStatus(200); 17 | } else { 18 | next(); 19 | } 20 | }); 21 | 22 | app.use(express.json({ limit: "50mb" })); 23 | app.use(express.urlencoded({ limit: "50mb", extended: true })); 24 | 25 | app.use("/containers", containerRoutes); 26 | app.use("/chat", chatRoutes); 27 | 28 | const PORT = process.env.PORT || 4000; 29 | app.listen(PORT, () => { 30 | console.log(`Docker Container API running on port ${PORT}`); 31 | }); 32 | 33 | export default app; 34 | -------------------------------------------------------------------------------- /frontend/public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2025] [Julian Schoen] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /frontend/src/app/create/components/ChatSidebar.tsx: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | 3 | import { ChatInput } from "./ChatInput"; 4 | import { ChatMessage } from "./ChatMessage"; 5 | 6 | export const ChatSidebar = ({ 7 | messages, 8 | inputValue, 9 | setInputValue, 10 | onSendMessage, 11 | messagesEndRef, 12 | textareaRef, 13 | onKeyDown, 14 | formatMessageContent, 15 | }) => { 16 | return ( 17 |
18 |
19 |
20 | {messages.map((message) => ( 21 | 26 | ))} 27 |
28 |
29 |
30 | 31 | 38 |
39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies (bun install) 2 | node_modules 3 | 4 | # output 5 | out 6 | dist 7 | *.tgz 8 | 9 | # code coverage 10 | coverage 11 | *.lcov 12 | 13 | # logs 14 | logs 15 | _.log 16 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 17 | 18 | # dotenv environment variable files 19 | .env 20 | .env.development.local 21 | .env.test.local 22 | .env.production.local 23 | .env.local 24 | 25 | # caches 26 | .eslintcache 27 | .cache 28 | *.tsbuildinfo 29 | 30 | # IntelliJ based IDEs 31 | .idea 32 | 33 | # Finder (MacOS) folder config 34 | .DS_Store 35 | 36 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 37 | 38 | # dependencies 39 | /node_modules 40 | /.pnp 41 | .pnp.* 42 | .yarn/* 43 | !.yarn/patches 44 | !.yarn/plugins 45 | !.yarn/releases 46 | !.yarn/versions 47 | 48 | # testing 49 | /coverage 50 | 51 | # next.js 52 | /.next/ 53 | /out/ 54 | .next 55 | 56 | # production 57 | /build 58 | 59 | # misc 60 | .DS_Store 61 | *.pem 62 | 63 | # debug 64 | npm-debug.log* 65 | yarn-debug.log* 66 | yarn-error.log* 67 | .pnpm-debug.log* 68 | 69 | # env files (can opt-in for committing if needed) 70 | .env* 71 | 72 | # vercel 73 | .vercel 74 | 75 | # typescript 76 | *.tsbuildinfo 77 | next-env.d.ts 78 | -------------------------------------------------------------------------------- /frontend/src/app/editor/IDE.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Editor from "@monaco-editor/react"; 4 | 5 | export default function IDE() { 6 | const handleSubmit = async () => {}; 7 | 8 | return ( 9 |
10 |
11 |
12 |
13 | 16 | 21 |
22 |
23 |
24 |
25 | 31 |
32 |
33 |
34 |
35 |
36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /frontend/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/app/editor/components/Icon.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | import { AiFillFileText } from "react-icons/ai"; 3 | import { FcFile, FcFolder, FcOpenedFolder, FcPicture } from "react-icons/fc"; 4 | import { 5 | SiCss3, 6 | SiHtml5, 7 | SiJavascript, 8 | SiJson, 9 | SiTypescript, 10 | } from "react-icons/si"; 11 | 12 | function getIconHelper() { 13 | const cache = new Map(); 14 | cache.set("js", ); 15 | cache.set("jsx", ); 16 | cache.set("ts", ); 17 | cache.set("tsx", ); 18 | cache.set("css", ); 19 | cache.set("json", ); 20 | cache.set("html", ); 21 | cache.set("png", ); 22 | cache.set("jpg", ); 23 | cache.set("ico", ); 24 | cache.set("txt", ); 25 | cache.set("closedDirectory", ); 26 | cache.set("openDirectory", ); 27 | return function (extension: string, name: string): ReactNode { 28 | if (cache.has(extension)) return cache.get(extension); 29 | else if (cache.has(name)) return cache.get(name); 30 | else return ; 31 | }; 32 | } 33 | 34 | export const getIcon = getIconHelper(); 35 | -------------------------------------------------------------------------------- /frontend/src/app/projects/components/ProjectsPage.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState } from "react"; 4 | import { ProjectPromptInterface } from "./ProjectPromptInterface"; 5 | import { ProjectsGrid } from "./ProjectsGrid"; 6 | import { ProjectsLayout } from "./ProjectsLayout"; 7 | import { TemplatesSection } from "./TemplatesSection"; 8 | 9 | export const ProjectsPage = () => { 10 | const [selectedTemplate, setSelectedTemplate] = useState("Next.js"); 11 | 12 | const handleTemplateSelect = (template: any) => { 13 | setSelectedTemplate(template.name); 14 | }; 15 | 16 | return ( 17 | 18 | 22 | 26 |
27 |
28 |
29 |

30 | Your Projects 31 |

32 |

33 | Manage and access your December projects. 34 |

35 |
36 |
37 | 38 |
39 |
40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /frontend/src/app/create/components/CryptoPlaceholder.tsx: -------------------------------------------------------------------------------- 1 | export const CryptoPlaceholder = () => { 2 | return ( 3 |
4 |
5 |
6 |
7 | 12 | 17 | 18 |
19 |

20 | Crypto Dashboard 21 |

22 |

23 | Your crypto trading interface will be rendered here 24 |

25 |
26 |

• Real-time market data

27 |

• Interactive trading charts

28 |

• Portfolio management

29 |

• Price alerts & notifications

30 |
31 |
32 |
33 |
34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. 37 | -------------------------------------------------------------------------------- /backend/src/services/export.ts: -------------------------------------------------------------------------------- 1 | import { exec } from "child_process"; 2 | import fs from "fs/promises"; 3 | import path from "path"; 4 | import { promisify } from "util"; 5 | 6 | const execAsync = promisify(exec); 7 | 8 | export async function exportContainerCode( 9 | containerId: string 10 | ): Promise { 11 | const tempDir = `/tmp/export-${containerId}-${Date.now()}`; 12 | const zipPath = `${tempDir}.zip`; 13 | 14 | try { 15 | await fs.mkdir(tempDir, { recursive: true }); 16 | 17 | const copyCommand = `docker cp ${containerId}:/app/my-nextjs-app/. ${tempDir}/`; 18 | await execAsync(copyCommand); 19 | 20 | const nodeModulesPath = path.join(tempDir, "node_modules"); 21 | const nextPath = path.join(tempDir, ".next"); 22 | 23 | try { 24 | await fs.rm(nodeModulesPath, { recursive: true, force: true }); 25 | } catch {} 26 | 27 | try { 28 | await fs.rm(nextPath, { recursive: true, force: true }); 29 | } catch {} 30 | 31 | const zipCommand = `cd ${tempDir} && zip -r ${zipPath} . -x "*.DS_Store"`; 32 | await execAsync(zipCommand); 33 | 34 | const zipBuffer = await fs.readFile(zipPath); 35 | 36 | await fs.rm(tempDir, { recursive: true, force: true }); 37 | await fs.rm(zipPath, { force: true }); 38 | 39 | return zipBuffer; 40 | } catch (error) { 41 | try { 42 | await fs.rm(tempDir, { recursive: true, force: true }); 43 | await fs.rm(zipPath, { force: true }); 44 | } catch {} 45 | 46 | throw new Error( 47 | `Export failed: ${ 48 | error instanceof Error ? error.message : "Unknown error" 49 | }` 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /frontend/src/app/globals.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | 3 | @font-face { 4 | font-family: "XSpace"; 5 | src: url("/xspace.ttf") format("truetype"); 6 | font-weight: normal; 7 | font-style: normal; 8 | font-display: swap; 9 | } 10 | 11 | @font-face { 12 | font-family: "Suisse"; 13 | src: url("/suisse.ttf") format("truetype"); 14 | font-weight: normal; 15 | font-style: normal; 16 | font-display: swap; 17 | } 18 | 19 | :root { 20 | --background: #ffffff; 21 | --foreground: #171717; 22 | } 23 | 24 | @theme inline { 25 | --color-background: var(--background); 26 | --color-foreground: var(--foreground); 27 | --font-sans: var(--font-geist-sans); 28 | --font-mono: var(--font-geist-mono); 29 | } 30 | 31 | @media (prefers-color-scheme: dark) { 32 | :root { 33 | --background: #0a0a0a; 34 | --foreground: #ededed; 35 | } 36 | } 37 | 38 | body { 39 | background: var(--background); 40 | color: var(--foreground); 41 | font-family: Arial, Helvetica, sans-serif; 42 | } 43 | 44 | .custom-scrollbar::-webkit-scrollbar { 45 | height: 6px; 46 | width: 6px; 47 | } 48 | 49 | .custom-scrollbar::-webkit-scrollbar-track { 50 | background: rgba(30, 30, 46, 0.4); 51 | border-radius: 8px; 52 | margin: 0 10px; 53 | } 54 | 55 | .custom-scrollbar::-webkit-scrollbar-thumb { 56 | background: #333333; 57 | border-radius: 8px; 58 | transition: all 0.3s ease; 59 | } 60 | 61 | .custom-scrollbar::-webkit-scrollbar-thumb:hover { 62 | background: #555555; 63 | } 64 | 65 | .custom-scrollbar { 66 | scrollbar-width: thin; 67 | scrollbar-color: #333333 transparent; 68 | } 69 | 70 | .custom-scrollbar:not(:hover)::-webkit-scrollbar-thumb { 71 | background: rgba(51, 51, 51, 0.5); 72 | } 73 | -------------------------------------------------------------------------------- /frontend/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Geist, Geist_Mono } from "next/font/google"; 3 | import { Toaster } from "react-hot-toast"; 4 | import "./globals.css"; 5 | 6 | const geistSans = Geist({ 7 | variable: "--font-geist-sans", 8 | subsets: ["latin"], 9 | }); 10 | 11 | const geistMono = Geist_Mono({ 12 | variable: "--font-geist-mono", 13 | subsets: ["latin"], 14 | }); 15 | 16 | export const metadata: Metadata = { 17 | title: "December - Your Personal Full Stack Engineer", 18 | description: 19 | "Idea to app in seconds, with your personal full stack engineer. Build, deploy, and manage containerized applications with AI assistance.", 20 | keywords: [ 21 | "AI", 22 | "full stack", 23 | "development", 24 | "containers", 25 | "Next.js", 26 | "deployment", 27 | "coding assistant", 28 | ], 29 | authors: [{ name: "December" }], 30 | creator: "December", 31 | publisher: "December", 32 | openGraph: { 33 | title: "December - Your Personal Full Stack Engineer", 34 | description: 35 | "Idea to app in seconds, with your personal full stack engineer", 36 | type: "website", 37 | locale: "en_US", 38 | }, 39 | twitter: { 40 | card: "summary_large_image", 41 | title: "December - Your Personal Full Stack Engineer", 42 | description: 43 | "Idea to app in seconds, with your personal full stack engineer", 44 | }, 45 | robots: { 46 | index: true, 47 | follow: true, 48 | }, 49 | viewport: "width=device-width, initial-scale=1", 50 | }; 51 | 52 | export default function RootLayout({ 53 | children, 54 | }: Readonly<{ 55 | children: React.ReactNode; 56 | }>) { 57 | return ( 58 | 59 | 63 | 73 | {children} 74 | 75 | 76 | ); 77 | } 78 | -------------------------------------------------------------------------------- /backend/src/routes/chat.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import * as llmService from "../services/llm"; 3 | 4 | const router = express.Router(); 5 | 6 | //@ts-ignore 7 | router.post("/:containerId/messages", async (req, res) => { 8 | const { containerId } = req.params; 9 | const { message, attachments = [], stream = false } = req.body; 10 | 11 | if (!message || typeof message !== "string") { 12 | return res.status(400).json({ 13 | success: false, 14 | error: "Message is required", 15 | }); 16 | } 17 | 18 | try { 19 | if (stream) { 20 | res.setHeader("Content-Type", "text/event-stream"); 21 | res.setHeader("Cache-Control", "no-cache"); 22 | res.setHeader("Connection", "keep-alive"); 23 | res.setHeader("Access-Control-Allow-Origin", "*"); 24 | 25 | const messageStream = llmService.sendMessageStream( 26 | containerId, 27 | message, 28 | attachments 29 | ); 30 | 31 | for await (const chunk of messageStream) { 32 | res.write(`data: ${JSON.stringify(chunk)}\n\n`); 33 | } 34 | 35 | res.write("data: [DONE]\n\n"); 36 | res.end(); 37 | } else { 38 | const { userMessage, assistantMessage } = await llmService.sendMessage( 39 | containerId, 40 | message, 41 | attachments 42 | ); 43 | 44 | res.json({ 45 | success: true, 46 | userMessage, 47 | assistantMessage, 48 | }); 49 | } 50 | } catch (error) { 51 | console.log(error); 52 | if (stream) { 53 | res.write( 54 | `data: ${JSON.stringify({ 55 | type: "error", 56 | data: { 57 | error: error instanceof Error ? error.message : "Unknown error", 58 | }, 59 | })}\n\n` 60 | ); 61 | res.end(); 62 | } else { 63 | res.status(500).json({ 64 | success: false, 65 | error: error instanceof Error ? error.message : "Unknown error", 66 | }); 67 | } 68 | } 69 | }); 70 | 71 | router.get("/:containerId/messages", async (req, res) => { 72 | const { containerId } = req.params; 73 | 74 | try { 75 | const session = llmService.getOrCreateChatSession(containerId); 76 | 77 | res.json({ 78 | success: true, 79 | messages: session.messages, 80 | sessionId: session.id, 81 | }); 82 | } catch (error) { 83 | res.status(500).json({ 84 | success: false, 85 | error: error instanceof Error ? error.message : "Unknown error", 86 | }); 87 | } 88 | }); 89 | 90 | export default router; 91 | -------------------------------------------------------------------------------- /frontend/src/app/create/components/TopNavigation.tsx: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | import { 3 | ChevronLeft, 4 | ExternalLink, 5 | Globe, 6 | Menu, 7 | Monitor, 8 | RefreshCw, 9 | Smartphone, 10 | } from "lucide-react"; 11 | import { useState } from "react"; 12 | 13 | export const TopNavigation = ({ sidebarOpen, setSidebarOpen }) => { 14 | const [isDesktopView, setIsDesktopView] = useState(true); 15 | return ( 16 |
17 | 65 |
66 | ); 67 | }; 68 | -------------------------------------------------------------------------------- /frontend/src/app/projects/components/ProjectsLayout.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Link from "next/link"; 4 | import { ReactNode } from "react"; 5 | 6 | interface ProjectsLayoutProps { 7 | children: ReactNode; 8 | } 9 | 10 | export const ProjectsLayout = ({ children }: ProjectsLayoutProps) => { 11 | return ( 12 |
13 |
14 | 23 |
24 |
25 | 26 |
27 | 52 | 53 |
{children}
54 | 55 | 73 |
74 |
75 | ); 76 | }; 77 | -------------------------------------------------------------------------------- /frontend/src/app/editor/utils/FileManager.tsx: -------------------------------------------------------------------------------- 1 | export enum Type { 2 | FILE, 3 | DIRECTORY, 4 | DUMMY, 5 | } 6 | 7 | interface CommonProps { 8 | id: string; 9 | type: Type; 10 | name: string; 11 | parentId: string | undefined; 12 | depth: number; 13 | } 14 | 15 | export interface File extends CommonProps { 16 | content: string; 17 | path?: string; 18 | } 19 | 20 | export interface Directory extends CommonProps { 21 | files: File[]; 22 | dirs: Directory[]; 23 | } 24 | 25 | export function buildFileTree(data: any): Directory { 26 | const dirs = [...data.directories]; 27 | const files = [...data.modules]; 28 | const cache = new Map(); 29 | 30 | let rootDir: Directory = { 31 | id: "0", 32 | name: "root", 33 | parentId: undefined, 34 | type: Type.DIRECTORY, 35 | depth: 0, 36 | dirs: [], 37 | files: [], 38 | }; 39 | 40 | dirs.forEach((item) => { 41 | let dir: Directory = { 42 | id: item.shortid, 43 | name: item.title, 44 | parentId: item.directory_shortid === null ? "0" : item.directory_shortid, 45 | type: Type.DIRECTORY, 46 | depth: 0, 47 | dirs: [], 48 | files: [], 49 | }; 50 | 51 | cache.set(dir.id, dir); 52 | }); 53 | 54 | files.forEach((item) => { 55 | let file: File = { 56 | id: item.shortid, 57 | name: item.title, 58 | parentId: item.directory_shortid === null ? "0" : item.directory_shortid, 59 | type: Type.FILE, 60 | depth: 0, 61 | content: item.code, 62 | }; 63 | cache.set(file.id, file); 64 | }); 65 | 66 | cache.forEach((value, key) => { 67 | if (value.parentId === "0") { 68 | if (value.type === Type.DIRECTORY) rootDir.dirs.push(value as Directory); 69 | else rootDir.files.push(value as File); 70 | } else { 71 | const parentDir = cache.get(value.parentId as string) as Directory; 72 | if (value.type === Type.DIRECTORY) 73 | parentDir.dirs.push(value as Directory); 74 | else parentDir.files.push(value as File); 75 | } 76 | }); 77 | 78 | getDepth(rootDir, 0); 79 | 80 | return rootDir; 81 | } 82 | 83 | function getDepth(rootDir: Directory, curDepth: number) { 84 | rootDir.files.forEach((file) => { 85 | file.depth = curDepth + 1; 86 | }); 87 | rootDir.dirs.forEach((dir) => { 88 | dir.depth = curDepth + 1; 89 | getDepth(dir, curDepth + 1); 90 | }); 91 | } 92 | 93 | export function findFileByName( 94 | rootDir: Directory, 95 | filename: string 96 | ): File | undefined { 97 | let targetFile: File | undefined = undefined; 98 | 99 | function findFile(rootDir: Directory, filename: string) { 100 | rootDir.files.forEach((file) => { 101 | if (file.name === filename) { 102 | targetFile = file; 103 | return; 104 | } 105 | }); 106 | rootDir.dirs.forEach((dir) => { 107 | findFile(dir, filename); 108 | }); 109 | } 110 | 111 | findFile(rootDir, filename); 112 | return targetFile; 113 | } 114 | 115 | export function sortDir(l: Directory, r: Directory) { 116 | return l.name.localeCompare(r.name); 117 | } 118 | 119 | export function sortFile(l: File, r: File) { 120 | return l.name.localeCompare(r.name); 121 | } 122 | -------------------------------------------------------------------------------- /frontend/src/app/editor/utils/Code.tsx: -------------------------------------------------------------------------------- 1 | import Editor from "@monaco-editor/react"; 2 | import { File } from "../utils/FileManager"; 3 | 4 | interface CodeProps { 5 | selectedFile: File | undefined; 6 | onChange: (value: string | undefined) => void; 7 | } 8 | 9 | export const Code = ({ selectedFile, onChange }: CodeProps) => { 10 | const handleEditorDidMount = (editor: any, monaco: any) => { 11 | monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions({ 12 | noSemanticValidation: true, 13 | noSyntaxValidation: true, 14 | noSuggestionDiagnostics: true, 15 | }); 16 | 17 | monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({ 18 | noSemanticValidation: true, 19 | noSyntaxValidation: true, 20 | noSuggestionDiagnostics: true, 21 | }); 22 | 23 | monaco.languages.html.htmlDefaults.setOptions({ 24 | validate: false, 25 | }); 26 | 27 | monaco.languages.css.cssDefaults.setOptions({ 28 | validate: false, 29 | }); 30 | 31 | monaco.languages.json.jsonDefaults.setDiagnosticsOptions({ 32 | validate: false, 33 | }); 34 | 35 | monaco.editor.defineTheme("no-errors", { 36 | base: "vs-dark", 37 | inherit: true, 38 | rules: [], 39 | colors: { 40 | "editorError.foreground": "#00000000", 41 | "editorError.background": "#00000000", 42 | "editorWarning.foreground": "#00000000", 43 | "editorWarning.background": "#00000000", 44 | "editorInfo.foreground": "#00000000", 45 | "editorInfo.background": "#00000000", 46 | "editorSquiggles.error": "#00000000", 47 | "editorSquiggles.warning": "#00000000", 48 | "editorSquiggles.info": "#00000000", 49 | }, 50 | }); 51 | 52 | monaco.editor.setTheme("no-errors"); 53 | }; 54 | 55 | if (!selectedFile) { 56 | return ( 57 |
58 |
59 |
No file selected
60 |
61 | Select a file from the sidebar to start editing 62 |
63 |
64 |
65 | ); 66 | } 67 | 68 | let language = selectedFile.name.split(".").pop(); 69 | 70 | if (language === "js" || language === "jsx") language = "javascript"; 71 | else if (language === "ts" || language === "tsx") language = "typescript"; 72 | 73 | return ( 74 |
75 | 101 |
102 | ); 103 | }; 104 | -------------------------------------------------------------------------------- /frontend/src/app/projects/components/CreateProjectCard.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Plus } from "lucide-react"; 4 | import { useState } from "react"; 5 | import { createContainer } from "../../../lib/backend/api"; 6 | 7 | interface CreateProjectCardProps { 8 | onProjectCreated: () => void; 9 | } 10 | 11 | export const CreateProjectCard = ({ 12 | onProjectCreated, 13 | }: CreateProjectCardProps) => { 14 | const [isCreating, setIsCreating] = useState(false); 15 | 16 | const handleCreateProject = async () => { 17 | setIsCreating(true); 18 | try { 19 | await createContainer(); 20 | onProjectCreated(); 21 | } catch (error) { 22 | console.error("Failed to create project:", error); 23 | } finally { 24 | setIsCreating(false); 25 | } 26 | }; 27 | 28 | return ( 29 |
33 |
34 | 35 |
36 | {isCreating ? ( 37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | 46 | Creating project... (the first time will take a bit longer due 47 | to container setup) 48 | 49 | 50 | Setting up your container 51 | 52 |
53 |
54 | ) : ( 55 |
56 |
57 |
58 | 59 |
60 |
61 |
62 |
63 | 64 | Create New Project 65 | 66 | 67 | Start a new Next.js application 68 |
69 | 70 | with Docker containerization 71 | 72 |
73 |
74 |
75 | )} 76 |
77 |
78 | ); 79 | }; 80 | -------------------------------------------------------------------------------- /frontend/src/app/editor/components/FileTree.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { Directory, File, sortDir, sortFile } from "../utils/FileManager"; 3 | import { getIcon } from "./Icon"; 4 | 5 | interface FileTreeProps { 6 | rootDir: Directory; 7 | selectedFile: File | undefined; 8 | onSelect: (file: File) => void; 9 | } 10 | 11 | export const FileTree = (props: FileTreeProps) => { 12 | return ; 13 | }; 14 | 15 | interface SubTreeProps { 16 | directory: Directory; 17 | selectedFile: File | undefined; 18 | onSelect: (file: File) => void; 19 | } 20 | 21 | const SubTree = (props: SubTreeProps) => { 22 | return ( 23 |
24 | {props.directory.dirs.sort(sortDir).map((dir) => ( 25 | 26 | 31 | 32 | ))} 33 | {props.directory.files.sort(sortFile).map((file) => ( 34 | 35 | props.onSelect(file)} 39 | /> 40 | 41 | ))} 42 |
43 | ); 44 | }; 45 | 46 | const FileDiv = ({ 47 | file, 48 | icon, 49 | selectedFile, 50 | onClick, 51 | }: { 52 | file: File | Directory; 53 | icon?: string; 54 | selectedFile: File | undefined; 55 | onClick: () => void; 56 | }) => { 57 | const isSelected = (selectedFile && selectedFile.id === file.id) as boolean; 58 | const depth = file.depth; 59 | 60 | return ( 61 |
68 | 69 | {file.name} 70 |
71 | ); 72 | }; 73 | 74 | const DirDiv = ({ 75 | directory, 76 | selectedFile, 77 | onSelect, 78 | }: { 79 | directory: Directory; 80 | selectedFile: File | undefined; 81 | onSelect: (file: File) => void; 82 | }) => { 83 | let defaultOpen = false; 84 | if (selectedFile) defaultOpen = isChildSelected(directory, selectedFile); 85 | const [open, setOpen] = useState(defaultOpen); 86 | 87 | return ( 88 | <> 89 | setOpen(!open)} 94 | /> 95 | {open ? ( 96 | 101 | ) : null} 102 | 103 | ); 104 | }; 105 | 106 | const isChildSelected = (directory: Directory, selectedFile: File) => { 107 | let res: boolean = false; 108 | 109 | function isChild(dir: Directory, file: File) { 110 | if (selectedFile.parentId === dir.id) { 111 | res = true; 112 | return; 113 | } 114 | if (selectedFile.parentId === "0") { 115 | res = false; 116 | return; 117 | } 118 | dir.dirs.forEach((item) => { 119 | isChild(item, file); 120 | }); 121 | } 122 | 123 | isChild(directory, selectedFile); 124 | return res; 125 | }; 126 | 127 | const FileIcon = ({ 128 | extension, 129 | name, 130 | }: { 131 | name?: string; 132 | extension?: string; 133 | }) => { 134 | let icon = getIcon(extension || "", name || ""); 135 | return ( 136 | {icon} 137 | ); 138 | }; 139 | -------------------------------------------------------------------------------- /frontend/src/app/projects/components/TemplatesSection.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | interface Template { 4 | id: string; 5 | name: string; 6 | description: string; 7 | icon: string; 8 | category: "template"; 9 | gradient?: string; 10 | } 11 | 12 | interface TemplatesSectionProps { 13 | selectedTemplate: string; 14 | onTemplateSelect: (template: Template) => void; 15 | } 16 | 17 | export const TemplatesSection = ({ 18 | selectedTemplate, 19 | onTemplateSelect, 20 | }: TemplatesSectionProps) => { 21 | const templates: Template[] = [ 22 | { 23 | id: "nextjs", 24 | name: "Next.js", 25 | description: "Build full-stack React apps with Next.js", 26 | icon: "/nextjs-logo.png", 27 | category: "template", 28 | gradient: "from-black to-gray-800", 29 | }, 30 | { 31 | id: "express-react", 32 | name: "Express & React", 33 | description: "Node.js backend with React frontend", 34 | icon: "/express-logo.png", 35 | category: "template", 36 | gradient: "from-gray-700 to-gray-900", 37 | }, 38 | { 39 | id: "express-vue", 40 | name: "Express & Vue", 41 | description: "Node.js backend with Vue.js frontend", 42 | icon: "/vue-logo.png", 43 | category: "template", 44 | gradient: "from-green-600 to-emerald-800", 45 | }, 46 | { 47 | id: "django", 48 | name: "Django", 49 | description: "High-level Python web framework", 50 | icon: "/django-logo.png", 51 | category: "template", 52 | gradient: "from-blue-600 to-indigo-800", 53 | }, 54 | ]; 55 | 56 | const handleTemplateSelect = (template: Template) => { 57 | onTemplateSelect(template); 58 | }; 59 | 60 | return ( 61 |
62 |
63 |
64 |

Templates

65 |

66 | Get started instantly with popular frameworks and tools. 67 |

68 |
69 |
70 | 71 |
72 | {templates.map((template) => ( 73 | 106 | ))} 107 |
108 |
109 | ); 110 | }; 111 | -------------------------------------------------------------------------------- /frontend/src/app/editor/components/Code.tsx: -------------------------------------------------------------------------------- 1 | import Editor from "@monaco-editor/react"; 2 | import { File } from "../utils/FileManager"; 3 | 4 | interface CodeProps { 5 | selectedFile: File | undefined; 6 | onChange: (value: string | undefined) => void; 7 | } 8 | 9 | export const Code = ({ selectedFile, onChange }: CodeProps) => { 10 | const handleEditorDidMount = (editor: any, monaco: any) => { 11 | monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions({ 12 | noSemanticValidation: true, 13 | noSyntaxValidation: true, 14 | noSuggestionDiagnostics: true, 15 | }); 16 | 17 | monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({ 18 | noSemanticValidation: true, 19 | noSyntaxValidation: true, 20 | noSuggestionDiagnostics: true, 21 | }); 22 | 23 | monaco.languages.html.htmlDefaults.setOptions({ 24 | validate: false, 25 | }); 26 | 27 | monaco.languages.css.cssDefaults.setOptions({ 28 | validate: false, 29 | }); 30 | 31 | monaco.languages.json.jsonDefaults.setDiagnosticsOptions({ 32 | validate: false, 33 | }); 34 | 35 | monaco.editor.defineTheme("no-errors", { 36 | base: "vs-dark", 37 | inherit: true, 38 | rules: [], 39 | colors: { 40 | "editorError.foreground": "#00000000", 41 | "editorError.background": "#00000000", 42 | "editorWarning.foreground": "#00000000", 43 | "editorWarning.background": "#00000000", 44 | "editorInfo.foreground": "#00000000", 45 | "editorInfo.background": "#00000000", 46 | "editorSquiggles.error": "#00000000", 47 | "editorSquiggles.warning": "#00000000", 48 | "editorSquiggles.info": "#00000000", 49 | }, 50 | }); 51 | 52 | monaco.editor.setTheme("no-errors"); 53 | }; 54 | 55 | if (!selectedFile) { 56 | return ( 57 |
58 |
59 |
No file selected
60 |
61 | Select a file from the sidebar to start editing 62 |
63 |
64 |
65 | ); 66 | } 67 | 68 | let language = selectedFile.name.split(".").pop(); 69 | 70 | if (language === "js" || language === "jsx") language = "javascript"; 71 | else if (language === "ts" || language === "tsx") language = "typescript"; 72 | 73 | return ( 74 |
75 |
76 |
77 | 111 |
112 |
113 |
114 | ); 115 | }; 116 | -------------------------------------------------------------------------------- /frontend/src/app/create/components/CreateDashboard.tsx: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | "use client"; 3 | 4 | import { useEffect, useRef, useState } from "react"; 5 | import { ChatSidebar } from "./ChatSidebar"; 6 | import { CryptoPlaceholder } from "./CryptoPlaceholder"; 7 | import { TopNavigation } from "./TopNavigation"; 8 | 9 | export const CreateDashboard = () => { 10 | const [messages, setMessages] = useState([ 11 | { 12 | id: 1, 13 | type: "user", 14 | content: "what is this project about?", 15 | timestamp: "15:20 on May 26, 2025", 16 | }, 17 | { 18 | id: 2, 19 | type: "bot", 20 | content: `This is a **Crypto Dashboard** web application that provides a comprehensive view of cryptocurrency market data and portfolio tracking. Here's what the project includes: 21 | 22 | ## Main Features: 23 | 1. **Market Statistics** - Displays key crypto market metrics and data 24 | 2. **Interactive Charts** - Features a TradingView widget showing Bitcoin price charts with professional trading tools 25 | 3. **Portfolio Performance** - Shows Bitcoin performance over the last 6 months using a custom line chart built with Recharts 26 | 4. **Cryptocurrency List** - Displays a list of various cryptocurrencies with their current data 27 | 28 | ## Technical Stack: 29 | - **React** with TypeScript for the frontend 30 | - **Tailwind CSS** for styling with a dark theme 31 | - **Shadcn/UI** components for consistent UI elements 32 | - **TanStack Query** for data fetching and caching 33 | - **Recharts** for custom chart visualizations 34 | - **TradingView Widget** for professional trading charts 35 | - **CoinGecko API** for real-time cryptocurrency data 36 | 37 | ## Design: 38 | The dashboard features a modern dark theme with a glassmorphism design (glass-card styling), purple accent colors, and smooth animations. The layout is responsive and organized in a grid system that adapts to different screen sizes. 39 | 40 | The app fetches live Bitcoin price data and displays it in multiple formats - both as a professional trading chart and as a simplified performance chart showing the 6-month trend.`, 41 | timestamp: "15:20 on May 26, 2025", 42 | }, 43 | ]); 44 | 45 | const [inputValue, setInputValue] = useState(""); 46 | const [sidebarOpen, setSidebarOpen] = useState(true); 47 | const messagesEndRef = useRef(null); 48 | const textareaRef = useRef(null); 49 | 50 | const scrollToBottom = () => { 51 | messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); 52 | }; 53 | 54 | useEffect(() => { 55 | scrollToBottom(); 56 | }, [messages]); 57 | 58 | const handleSendMessage = () => { 59 | if (!inputValue.trim()) return; 60 | 61 | const newMessage = { 62 | id: messages.length + 1, 63 | type: "user", 64 | content: inputValue, 65 | timestamp: new Date().toLocaleString(), 66 | }; 67 | 68 | setMessages((prev) => [...prev, newMessage]); 69 | setInputValue(""); 70 | 71 | setTimeout(() => { 72 | const botResponse = { 73 | id: messages.length + 2, 74 | type: "bot", 75 | content: 76 | "This is a mock response. In a real application, this would connect to your backend API or AI service.", 77 | timestamp: new Date().toLocaleString(), 78 | }; 79 | setMessages((prev) => [...prev, botResponse]); 80 | }, 1000); 81 | }; 82 | 83 | const handleTextareaKeyDown = (e) => { 84 | if (e.key === "Enter" && !e.shiftKey) { 85 | e.preventDefault(); 86 | handleSendMessage(); 87 | } 88 | }; 89 | 90 | const formatMessageContent = (content) => { 91 | return content.split("\n").map((line, index) => { 92 | if (line.startsWith("## ")) { 93 | return ( 94 |

95 | {line.substring(3)} 96 |

97 | ); 98 | } 99 | if (line.startsWith("# ")) { 100 | return ( 101 |

102 | {line.substring(2)} 103 |

104 | ); 105 | } 106 | if (line.startsWith("- ")) { 107 | return ( 108 |
  • 109 | {line.substring(2)} 110 |
  • 111 | ); 112 | } 113 | if (line.match(/^\d+\./)) { 114 | const match = line.match(/^(\d+\.)\s*(.*)$/); 115 | return ( 116 |
  • 117 | {match[2]} 118 |
  • 119 | ); 120 | } 121 | if (line.includes("**") && line.includes("**")) { 122 | const parts = line.split("**"); 123 | return ( 124 |

    125 | {parts.map((part, i) => 126 | i % 2 === 1 ? {part} : part 127 | )} 128 |

    129 | ); 130 | } 131 | return line ? ( 132 |

    133 | {line} 134 |

    135 | ) : ( 136 |
    137 | ); 138 | }); 139 | }; 140 | 141 | return ( 142 |
    143 |
    144 | 148 | 149 |
    150 | {sidebarOpen && ( 151 | 161 | )} 162 | 163 | 164 |
    165 |
    166 |
    167 | ); 168 | }; 169 | 170 | export default CreateDashboard; 171 | -------------------------------------------------------------------------------- /frontend/src/app/projects/components/TopNavigation.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | ChevronLeft, 5 | Code2, 6 | ExternalLink, 7 | Eye, 8 | Globe, 9 | Menu, 10 | Monitor, 11 | RefreshCw, 12 | Smartphone, 13 | } from "lucide-react"; 14 | import { useEffect, useState } from "react"; 15 | 16 | interface TopNavigationProps { 17 | sidebarOpen: boolean; 18 | setSidebarOpen: (open: boolean) => void; 19 | viewMode?: "preview" | "editor"; 20 | setViewMode?: (mode: "preview" | "editor") => void; 21 | isDesktopView?: boolean; 22 | setIsDesktopView?: (isDesktop: boolean) => void; 23 | containerId?: string; 24 | } 25 | 26 | export const TopNavigation = ({ 27 | sidebarOpen, 28 | setSidebarOpen, 29 | viewMode, 30 | setViewMode, 31 | isDesktopView = true, 32 | setIsDesktopView, 33 | containerId, 34 | }: TopNavigationProps) => { 35 | const [containerUrl, setContainerUrl] = useState(null); 36 | 37 | useEffect(() => { 38 | if (containerId) { 39 | const fetchContainerUrl = async () => { 40 | try { 41 | const response = await fetch(`http://localhost:4000/containers`); 42 | const data = await response.json(); 43 | if (data.success) { 44 | const container = data.containers.find( 45 | (c: any) => c.id === containerId 46 | ); 47 | if (container && container.url) { 48 | setContainerUrl(container.url); 49 | } 50 | } 51 | } catch (error) { 52 | console.error("Error fetching container URL:", error); 53 | } 54 | }; 55 | 56 | fetchContainerUrl(); 57 | const interval = setInterval(fetchContainerUrl, 10000); 58 | return () => clearInterval(interval); 59 | } 60 | }, [containerId]); 61 | 62 | const handleRefresh = () => { 63 | const iframe = document.querySelector("iframe"); 64 | if (iframe) { 65 | iframe.src = iframe.src; 66 | } 67 | }; 68 | 69 | const handleExternalLink = () => { 70 | if (containerUrl) { 71 | window.open(containerUrl, "_blank", "noopener,noreferrer"); 72 | } 73 | }; 74 | 75 | return ( 76 |
    77 |
    78 |
    79 | 91 |
    92 | 93 |
    97 |
    98 | 105 | 112 |
    113 | 114 |
    115 | {setIsDesktopView && ( 116 | 126 | )} 127 | 128 | {viewMode && setViewMode && ( 129 |
    130 |
    137 | 138 | 149 | 160 |
    161 | )} 162 |
    163 | 164 |
    165 | 169 |
    170 |
    171 |
    172 |
    173 | ); 174 | }; 175 | -------------------------------------------------------------------------------- /frontend/src/lib/backend/api.ts: -------------------------------------------------------------------------------- 1 | const API_BASE_URL = "http://localhost:4000"; 2 | 3 | export interface Container { 4 | id: string; 5 | name: string; 6 | status: string; 7 | image: string; 8 | created: string; 9 | assignedPort: number | null; 10 | url: string | null; 11 | ports: Array<{ 12 | private: number; 13 | public: number; 14 | type: string; 15 | }>; 16 | labels: Record; 17 | } 18 | 19 | export interface Message { 20 | id: string; 21 | role: "user" | "assistant"; 22 | content: string; 23 | timestamp: string; 24 | attachments?: Attachment[]; 25 | } 26 | 27 | export interface Attachment { 28 | type: "image" | "document"; 29 | data: string; 30 | name: string; 31 | mimeType: string; 32 | size: number; 33 | } 34 | 35 | export interface ApiResponse { 36 | success: boolean; 37 | data?: T; 38 | error?: string; 39 | } 40 | 41 | export interface CreateContainerResponse { 42 | containerId: string; 43 | container: { 44 | id: string; 45 | containerId: string; 46 | status: string; 47 | port: number; 48 | url: string; 49 | createdAt: string; 50 | type: string; 51 | }; 52 | } 53 | 54 | export interface StartContainerResponse { 55 | containerId: string; 56 | port: number; 57 | url: string; 58 | status: string; 59 | message: string; 60 | } 61 | 62 | export interface StopContainerResponse { 63 | containerId: string; 64 | status: string; 65 | message: string; 66 | } 67 | 68 | export interface DeleteContainerResponse { 69 | containerId: string; 70 | message: string; 71 | } 72 | 73 | export interface ChatResponse { 74 | success: boolean; 75 | userMessage: Message; 76 | assistantMessage: Message; 77 | } 78 | 79 | export interface ChatHistoryResponse { 80 | success: boolean; 81 | messages: Message[]; 82 | sessionId: string; 83 | } 84 | 85 | async function fetchApi( 86 | endpoint: string, 87 | options?: RequestInit 88 | ): Promise { 89 | const response = await fetch(`${API_BASE_URL}${endpoint}`, { 90 | headers: { 91 | "Content-Type": "application/json", 92 | ...options?.headers, 93 | }, 94 | ...options, 95 | }); 96 | 97 | if (!response.ok) { 98 | throw new Error(`API request failed: ${response.statusText}`); 99 | } 100 | 101 | return response.json(); 102 | } 103 | 104 | export async function getContainers(): Promise { 105 | const response = await fetchApi<{ 106 | success: boolean; 107 | containers: Container[]; 108 | }>("/containers"); 109 | return response.containers; 110 | } 111 | 112 | export async function createContainer(): Promise { 113 | const response = await fetchApi< 114 | { success: boolean } & CreateContainerResponse 115 | >("/containers/create", { method: "POST" }); 116 | return response; 117 | } 118 | 119 | export async function startContainer( 120 | containerId: string 121 | ): Promise { 122 | const response = await fetchApi< 123 | { success: boolean } & StartContainerResponse 124 | >(`/containers/${containerId}/start`, { method: "POST" }); 125 | return response; 126 | } 127 | 128 | export async function stopContainer( 129 | containerId: string 130 | ): Promise { 131 | const response = await fetchApi<{ success: boolean } & StopContainerResponse>( 132 | `/containers/${containerId}/stop`, 133 | { method: "POST" } 134 | ); 135 | return response; 136 | } 137 | 138 | export async function deleteContainer( 139 | containerId: string 140 | ): Promise { 141 | const response = await fetchApi< 142 | { success: boolean } & DeleteContainerResponse 143 | >(`/containers/${containerId}`, { method: "DELETE" }); 144 | return response; 145 | } 146 | 147 | export async function sendChatMessage( 148 | containerId: string, 149 | message: string, 150 | attachments?: any[] 151 | ): Promise { 152 | const response = await fetchApi( 153 | `/chat/${containerId}/messages`, 154 | { 155 | method: "POST", 156 | body: JSON.stringify({ message, attachments }), 157 | } 158 | ); 159 | return response; 160 | } 161 | 162 | export function sendChatMessageStream( 163 | containerId: string, 164 | message: string, 165 | attachments: any[] = [], 166 | onMessage: (data: any) => void, 167 | onError?: (error: string) => void, 168 | onComplete?: () => void 169 | ): () => void { 170 | let abortController = new AbortController(); 171 | 172 | fetch(`${API_BASE_URL}/chat/${containerId}/messages`, { 173 | method: "POST", 174 | headers: { 175 | "Content-Type": "application/json", 176 | }, 177 | body: JSON.stringify({ message, attachments, stream: true }), 178 | signal: abortController.signal, 179 | }) 180 | .then(async (response) => { 181 | if (!response.ok) { 182 | throw new Error(`HTTP ${response.status}: ${response.statusText}`); 183 | } 184 | 185 | const reader = response.body?.getReader(); 186 | if (!reader) throw new Error("No reader available"); 187 | 188 | const decoder = new TextDecoder(); 189 | let buffer = ""; 190 | 191 | try { 192 | while (true) { 193 | const { done, value } = await reader.read(); 194 | if (done) break; 195 | 196 | buffer += decoder.decode(value, { stream: true }); 197 | const lines = buffer.split("\n"); 198 | buffer = lines.pop() || ""; 199 | 200 | for (const line of lines) { 201 | if (line.startsWith("data: ")) { 202 | const data = line.slice(6).trim(); 203 | if (data === "[DONE]") { 204 | onComplete?.(); 205 | return; 206 | } 207 | if (data) { 208 | try { 209 | const parsed = JSON.parse(data); 210 | onMessage(parsed); 211 | } catch (e) { 212 | console.error("Failed to parse SSE data:", data, e); 213 | } 214 | } 215 | } 216 | } 217 | } 218 | } catch (error) { 219 | if (error instanceof Error && error.name === "AbortError") { 220 | return; 221 | } 222 | throw error; 223 | } 224 | }) 225 | .catch((error) => { 226 | if (error instanceof Error && error.name === "AbortError") { 227 | return; 228 | } 229 | console.error("Stream error:", error); 230 | onError?.(error.message || "Connection error"); 231 | }); 232 | 233 | return () => { 234 | abortController.abort(); 235 | }; 236 | } 237 | 238 | export async function getChatHistory( 239 | containerId: string 240 | ): Promise { 241 | const response = await fetchApi( 242 | `/chat/${containerId}/messages` 243 | ); 244 | return response; 245 | } 246 | -------------------------------------------------------------------------------- /frontend/src/app/community/components/CommunityPage.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Star } from "lucide-react"; 4 | import Link from "next/link"; 5 | 6 | interface CommunityProject { 7 | id: string; 8 | name: string; 9 | description: string; 10 | icon: string; 11 | gradient: string; 12 | featured?: boolean; 13 | } 14 | 15 | export const CommunityPage = () => { 16 | const featuredProjects: CommunityProject[] = [ 17 | { 18 | id: "background-paths", 19 | name: "Background Paths", 20 | description: 21 | "Beautiful animated background patterns with customizable colors and shapes", 22 | icon: "🎨", 23 | gradient: "from-purple-600 to-pink-600", 24 | featured: true, 25 | }, 26 | { 27 | id: "flowers-saints", 28 | name: "Flowers & Saints", 29 | description: "Creative agency portfolio with stunning visual effects", 30 | icon: "🌸", 31 | gradient: "from-blue-600 to-purple-600", 32 | featured: true, 33 | }, 34 | { 35 | id: "crypto-dashboard", 36 | name: "Crypto Dashboard", 37 | description: "Real-time cryptocurrency trading dashboard with charts", 38 | icon: "📊", 39 | gradient: "from-green-500 to-emerald-600", 40 | featured: true, 41 | }, 42 | ]; 43 | 44 | const handleProjectSelect = (project: CommunityProject) => { 45 | console.log("Selected project:", project); 46 | }; 47 | 48 | return ( 49 |
    50 |
    51 |
    52 |
    53 | 54 |
    55 | 86 | 87 |
    88 |
    89 |
    90 |

    91 | Community Projects 92 |

    93 |

    94 | Discover amazing projects built by the December community. Get 95 | inspired, fork, and build upon the work of others. 96 |

    97 |
    98 | 99 |
    100 |

    101 | Featured Projects 102 |

    103 |
    104 | {featuredProjects.map((project) => ( 105 |
    handleProjectSelect(project)} 109 | > 110 |
    111 | 112 | 113 | Featured 114 | 115 |
    116 | 117 |
    120 |
    121 |
    122 | {project.icon} 123 |
    124 |
    125 |
    126 | 127 |
    128 |
    129 |

    130 | {project.name} 131 |

    132 |
    133 |

    134 | {project.description} 135 |

    136 |
    137 |
    138 | ))} 139 |
    140 |
    141 |
    142 |
    143 | 144 | 162 |
    163 |
    164 | ); 165 | }; 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 |
    4 | 5 |

    Say hi to December ☃️

    6 | 7 |

    8 | December is an open-source alternative to AI-powered development platforms like Loveable, Replit, and Bolt that you can run locally with your own API keys, ensuring complete privacy and significant cost savings. 9 |
    10 |
    11 | December lets you build full-stack applications from simple text prompts using AI. 12 |
    13 |
    14 | Get started 15 | · 16 | Report Bug 17 | · 18 | Request Feature 19 | 20 |

    21 |
    22 | 23 | December Preview 24 | 25 | 26 | ## Features 27 | 28 | ✅ AI-powered project creation from natural language prompts 29 | ✅ Containerized Next.js applications with Docker 30 | ✅ Live preview with mobile and desktop views 31 | ✅ Full-featured Monaco code editor with file management 32 | ✅ Real-time chat assistant for development help 33 | ✅ Project export and deployment capabilities 34 | 35 | ## Roadmap 36 | 37 | 🔄 LLM streaming support 38 | 🔄 Document & image attachments 39 | 🔄 Improved fault tolerance 40 | 🔄 Comprehensive test coverage 41 | 🔄 Multi-framework support (beyond Next.js) 42 | 43 | ## Get started 44 | 45 | 1. Clone the repo 46 | 47 | ```sh 48 | git clone https://github.com/ntegrals/december 49 | ``` 50 | 51 | 2. Get an API Key from any OpenAI sdk compatible provider (e.g. OpenAI, Claude, Ollama, OpenRouter, etc.) and set it in the `config.ts` file. 52 | 53 | The start.sh script will automatically copy over the file into the backend folder. 54 | 55 | I highly recommend using Sonnet-4 from Anthropic as it is the best coding model available right now. 56 | 57 | ```sh 58 | baseUrl: "https://openrouter.ai/api/v1", 59 | 60 | apiKey: 61 | "sk-...", 62 | 63 | model: "anthropic/claude-sonnet-4", 64 | temperature: 0.7, 65 | ``` 66 | 67 | 3. Install docker (Docker Desktop is the easiest way to get started) 68 | 69 | - [Docker Desktop for Mac](https://www.docker.com/products/docker-desktop/) 70 | - [Docker Desktop for Windows](https://www.docker.com/products/docker-desktop/) 71 | - [Docker Engine for Linux](https://docs.docker.com/engine/install/) 72 | 73 | Make sure you have Docker running and the Docker CLI installed before proceeding. 74 | 75 | 4. Run the start script to set up the environment 76 | 77 | ```sh 78 | sh start.sh 79 | ``` 80 | 81 | 5. The application will start in development mode, and you can access it at [http://localhost:3000](http://localhost:3000). 82 | 83 | The backend will run on port 4000, and the frontend will run on port 3000. 84 | 85 | You can now start building your applications with December! 🥳 86 | 87 | 90 | 91 | ## Motivation 92 | 93 | AI-powered development platforms have revolutionized how we build applications. They allow developers to go from idea to working application in seconds, but most solutions are closed-source or require expensive subscriptions. 94 | 95 | Until recently, building a local alternative that matched the speed and capabilities of platforms like Loveable, Replit, or Bolt seemed challenging. The recent advances in AI and containerization technologies have made it possible to build a fast, local development environment that gives you full control over your code and API usage. 96 | 97 | I would love for this repo to become the go-to place for people who want to run their own AI-powered development environment. I've been working on this project for a while now and I'm really excited to share it with you. 98 | 99 | ## Why run December locally? 100 | 101 | Building applications shouldn't require expensive subscriptions or sacrificing your privacy. December gives you the power of platforms like Loveable, Replit, and Bolt without the downsides: 102 | 103 | - **Full Control & Privacy** - Your code, ideas, and projects never leave your machine. No cloud storage, no data mining, no vendor lock-in 104 | - **Your API Keys, Your Costs** - Use your own OpenAI API key and pay only for what you use. No monthly subscriptions or usage limits imposed by third parties 105 | - **Complete Feature Access** - No paywalls, premium tiers, or artificial limitations. Every feature is available from day one 106 | 107 | Most cloud-based AI development platforms charge $20-100+ per month while limiting your usage and storing your intellectual property on their servers. With December, a $5 OpenAI API credit can generate dozens of complete applications, and you keep full ownership of everything you create. 108 | 109 | The local-first approach means you can work offline, modify the platform itself, and never worry about service outages or policy changes affecting your projects. Your development environment evolves with your needs, not a company's business model. 110 | 111 | December proves that you don't need to choose between powerful AI assistance and maintaining control over your work. Run it locally, use your own API keys, and build without boundaries. 112 | 113 | ## Contact 114 | 115 | Hi! Thanks for checking out and using this project. If you are interested in discussing your project, require mentorship, consider hiring me, or just wanna chat - I'm happy to talk. 116 | 117 | You can send me an email to get in touch: j.schoen@mail.com or message me on Twitter: [@julianschoen](https://twitter.com/julianschoen) 118 | 119 | Thanks and have an awesome day 👋 120 | 121 | ## Disclaimer 122 | 123 | December, is an experimental application and is provided "as-is" without any warranty, express or implied. By using this software, you agree to assume all risks associated with its use, including but not limited to data loss, system failure, or any other issues that may arise. 124 | 125 | The developers and contributors of this project do not accept any responsibility or liability for any losses, damages, or other consequences that may occur as a result of using this software. You are solely responsible for any decisions and actions taken based on the information provided by December. 126 | 127 | Please note that the use of the large language models can be expensive due to its token usage. By utilizing this project, you acknowledge that you are responsible for monitoring and managing your own token usage and the associated costs. It is highly recommended to check your API usage regularly and set up any necessary limits or alerts to prevent unexpected charges. 128 | 129 | By using December, you agree to indemnify, defend, and hold harmless the developers, contributors, and any affiliated parties from and against any and all claims, damages, losses, liabilities, costs, and expenses (including reasonable attorneys' fees) arising from your use of this software or your violation of these terms. 130 | 131 | 132 | 133 | ## License 134 | 135 | Distributed under the MIT License. See `LICENSE` for more information. 136 | -------------------------------------------------------------------------------- /backend/src/services/llm.ts: -------------------------------------------------------------------------------- 1 | import OpenAI from "openai"; 2 | import { config } from "../../config"; 3 | import prompt from "../utils/prompt.txt"; 4 | import * as dockerService from "./docker"; 5 | import * as fileService from "./file"; 6 | 7 | const openai = new OpenAI({ 8 | apiKey: config.aiSdk.apiKey, 9 | baseURL: config.aiSdk.baseUrl || "https://api.openai.com/v1", 10 | }); 11 | 12 | export interface Message { 13 | id: string; 14 | role: "user" | "assistant"; 15 | content: string; 16 | timestamp: string; 17 | attachments?: Attachment[]; 18 | } 19 | 20 | export interface Attachment { 21 | type: "image" | "document"; 22 | data: string; 23 | name: string; 24 | mimeType: string; 25 | size: number; 26 | } 27 | 28 | export interface ChatSession { 29 | id: string; 30 | containerId: string; 31 | messages: Message[]; 32 | createdAt: string; 33 | updatedAt: string; 34 | } 35 | 36 | const chatSessions = new Map(); 37 | 38 | export async function createChatSession( 39 | containerId: string 40 | ): Promise { 41 | const sessionId = `${containerId}-${Date.now()}`; 42 | const session: ChatSession = { 43 | id: sessionId, 44 | containerId, 45 | messages: [], 46 | createdAt: new Date().toISOString(), 47 | updatedAt: new Date().toISOString(), 48 | }; 49 | 50 | chatSessions.set(sessionId, session); 51 | return session; 52 | } 53 | 54 | export function getChatSession(sessionId: string): ChatSession | undefined { 55 | return chatSessions.get(sessionId); 56 | } 57 | 58 | export function getOrCreateChatSession(containerId: string): ChatSession { 59 | const existingSession = Array.from(chatSessions.values()).find( 60 | (session) => session.containerId === containerId 61 | ); 62 | 63 | if (existingSession) { 64 | return existingSession; 65 | } 66 | 67 | const sessionId = `${containerId}-${Date.now()}`; 68 | const session: ChatSession = { 69 | id: sessionId, 70 | containerId, 71 | messages: [], 72 | createdAt: new Date().toISOString(), 73 | updatedAt: new Date().toISOString(), 74 | }; 75 | 76 | chatSessions.set(sessionId, session); 77 | return session; 78 | } 79 | 80 | function buildMessageContent( 81 | message: string, 82 | attachments: Attachment[] = [] 83 | ): any[] { 84 | const content: any[] = [{ type: "text", text: message }]; 85 | 86 | for (const attachment of attachments) { 87 | if (attachment.type === "image") { 88 | content.push({ 89 | type: "image_url", 90 | image_url: { 91 | url: `data:${attachment.mimeType};base64,${attachment.data}`, 92 | }, 93 | }); 94 | } else if (attachment.type === "document") { 95 | const decodedText = Buffer.from(attachment.data, "base64").toString( 96 | "utf-8" 97 | ); 98 | content.push({ 99 | type: "text", 100 | text: `\n\nDocument "${attachment.name}" content:\n${decodedText}`, 101 | }); 102 | } 103 | } 104 | 105 | return content; 106 | } 107 | 108 | export async function sendMessage( 109 | containerId: string, 110 | userMessage: string, 111 | attachments: Attachment[] = [] 112 | ): Promise<{ userMessage: Message; assistantMessage: Message }> { 113 | const session = getOrCreateChatSession(containerId); 114 | 115 | const userMsg: Message = { 116 | id: `user-${Date.now()}`, 117 | role: "user", 118 | content: userMessage, 119 | timestamp: new Date().toISOString(), 120 | attachments: attachments.length > 0 ? attachments : undefined, 121 | }; 122 | 123 | session.messages.push(userMsg); 124 | 125 | const fileContentTree = await fileService.getFileContentTree( 126 | dockerService.docker, 127 | containerId 128 | ); 129 | 130 | const codeContext = JSON.stringify(fileContentTree, null, 2); 131 | 132 | const systemPrompt = `${prompt} 133 | 134 | Current codebase structure and content: 135 | ${codeContext}`; 136 | 137 | const openaiMessages = [ 138 | { role: "system" as const, content: systemPrompt }, 139 | ...session.messages.map((msg) => ({ 140 | role: msg.role as "user" | "assistant", 141 | content: 142 | msg.role === "user" && msg.attachments 143 | ? buildMessageContent(msg.content, msg.attachments) 144 | : msg.content, 145 | })), 146 | ]; 147 | 148 | const completion = await openai.chat.completions.create({ 149 | model: config.aiSdk.model, 150 | messages: openaiMessages, 151 | //@ts-ignore 152 | temperature: config.aiSdk.temperature, 153 | }); 154 | 155 | const assistantContent = 156 | completion.choices[0]?.message?.content || 157 | "Sorry, I could not generate a response."; 158 | 159 | const assistantMsg: Message = { 160 | id: `assistant-${Date.now()}`, 161 | role: "assistant", 162 | content: assistantContent, 163 | timestamp: new Date().toISOString(), 164 | }; 165 | 166 | session.messages.push(assistantMsg); 167 | session.updatedAt = new Date().toISOString(); 168 | 169 | return { 170 | userMessage: userMsg, 171 | assistantMessage: assistantMsg, 172 | }; 173 | } 174 | 175 | export async function* sendMessageStream( 176 | containerId: string, 177 | userMessage: string, 178 | attachments: Attachment[] = [] 179 | ): AsyncGenerator<{ type: "user" | "assistant" | "done"; data: any }> { 180 | const session = getOrCreateChatSession(containerId); 181 | 182 | const userMsg: Message = { 183 | id: `user-${Date.now()}`, 184 | role: "user", 185 | content: userMessage, 186 | timestamp: new Date().toISOString(), 187 | attachments: attachments.length > 0 ? attachments : undefined, 188 | }; 189 | 190 | session.messages.push(userMsg); 191 | yield { type: "user", data: userMsg }; 192 | 193 | const fileContentTree = await fileService.getFileContentTree( 194 | dockerService.docker, 195 | containerId 196 | ); 197 | 198 | const codeContext = JSON.stringify(fileContentTree, null, 2); 199 | 200 | const systemPrompt = `${prompt} 201 | 202 | Current codebase structure and content: 203 | ${codeContext}`; 204 | 205 | const openaiMessages = [ 206 | { role: "system" as const, content: systemPrompt }, 207 | ...session.messages.map((msg) => ({ 208 | role: msg.role as "user" | "assistant", 209 | content: 210 | msg.role === "user" && msg.attachments 211 | ? buildMessageContent(msg.content, msg.attachments) 212 | : msg.content, 213 | })), 214 | ]; 215 | 216 | const assistantId = `assistant-${Date.now()}`; 217 | let assistantContent = ""; 218 | 219 | const stream = await openai.chat.completions.create({ 220 | model: config.aiSdk.model, 221 | messages: openaiMessages, 222 | //@ts-ignore 223 | temperature: config.aiSdk.temperature, 224 | stream: true, 225 | }); 226 | 227 | for await (const chunk of stream) { 228 | const delta = chunk.choices[0]?.delta; 229 | if (delta?.content) { 230 | assistantContent += delta.content; 231 | yield { 232 | type: "assistant", 233 | data: { 234 | id: assistantId, 235 | role: "assistant", 236 | content: assistantContent, 237 | timestamp: new Date().toISOString(), 238 | }, 239 | }; 240 | } 241 | } 242 | 243 | const finalAssistantMsg: Message = { 244 | id: assistantId, 245 | role: "assistant", 246 | content: assistantContent, 247 | timestamp: new Date().toISOString(), 248 | }; 249 | 250 | session.messages.push(finalAssistantMsg); 251 | session.updatedAt = new Date().toISOString(); 252 | 253 | yield { type: "done", data: finalAssistantMsg }; 254 | } 255 | -------------------------------------------------------------------------------- /frontend/src/app/projects/components/LivePreview.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect, useState } from "react"; 4 | import { Container, getContainers } from "../../../lib/backend/api"; 5 | 6 | interface LivePreviewProps { 7 | containerId: string; 8 | isDesktopView?: boolean; 9 | } 10 | 11 | export const LivePreview = ({ 12 | containerId, 13 | isDesktopView = true, 14 | }: LivePreviewProps) => { 15 | const [container, setContainer] = useState(null); 16 | const [isLoading, setIsLoading] = useState(true); 17 | const [error, setError] = useState(null); 18 | 19 | useEffect(() => { 20 | const fetchContainer = async () => { 21 | try { 22 | setError(null); 23 | const containers = await getContainers(); 24 | const foundContainer = containers.find((c) => c.id === containerId); 25 | 26 | if (!foundContainer) { 27 | setError("Container not found"); 28 | return; 29 | } 30 | 31 | setContainer(foundContainer); 32 | } catch (err) { 33 | setError( 34 | err instanceof Error ? err.message : "Failed to fetch container" 35 | ); 36 | } finally { 37 | setIsLoading(false); 38 | } 39 | }; 40 | 41 | fetchContainer(); 42 | const interval = setInterval(fetchContainer, 5000); 43 | return () => clearInterval(interval); 44 | }, [containerId]); 45 | 46 | if (isLoading) { 47 | return ( 48 |
    49 |
    50 |
    51 |
    52 | Loading preview... 53 |
    54 |
    55 | ); 56 | } 57 | 58 | if (error) { 59 | return ( 60 |
    61 |
    62 |
    63 |
    64 | Preview Error 65 |
    66 |
    {error}
    67 |
    68 |
    69 | ); 70 | } 71 | 72 | if (!container) { 73 | return ( 74 |
    75 |
    76 |
    77 |
    Container not found
    78 |
    79 |
    80 | ); 81 | } 82 | 83 | if (container.status !== "running" || !container.url) { 84 | return ( 85 |
    86 |
    87 |
    88 |
    89 | 94 | 99 | 100 |
    101 |

    102 | Container Not Running 103 |

    104 |

    105 | Start the container to see the live preview 106 |

    107 |
    108 | Status: {container.status} 109 |
    110 |
    111 |
    112 | ); 113 | } 114 | 115 | const previewContainer = ( 116 |
    117 |
    118 |
    119 |
    120 |
    121 |
    122 |
    123 |
    124 |
    125 | {container.url} 126 |
    127 |
    128 |
    129 | Live 130 |
    131 |
    132 |