├── .env.example ├── .eslintrc.cjs ├── .gitignore ├── LICENSE ├── README.md ├── backend ├── Dockerfile ├── app │ ├── __init__.py │ ├── core │ │ └── limiter.py │ ├── main.py │ ├── prompts.py │ ├── routers │ │ ├── generate.py │ │ └── modify.py │ ├── services │ │ ├── claude_service.py │ │ ├── github_service.py │ │ ├── o1_mini_openai_service.py │ │ ├── o3_mini_openai_service.py │ │ └── o3_mini_openrouter_service.py │ └── utils │ │ └── format_message.py ├── deploy.sh ├── entrypoint.sh ├── nginx │ ├── api.conf │ └── setup_nginx.sh └── requirements.txt ├── components.json ├── docker-compose.yml ├── docs └── readme_img.png ├── drizzle.config.ts ├── drizzle ├── 0000_jittery_robin_chapel.sql ├── 0001_lush_mercury.sql └── meta │ ├── 0000_snapshot.json │ ├── 0001_snapshot.json │ └── _journal.json ├── next.config.js ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── prettier.config.js ├── public └── favicon.ico ├── src ├── app │ ├── [username] │ │ └── [repo] │ │ │ └── page.tsx │ ├── _actions │ │ ├── cache.ts │ │ ├── github.ts │ │ └── repo.ts │ ├── layout.tsx │ ├── page.tsx │ └── providers.tsx ├── components │ ├── action-button.tsx │ ├── api-key-button.tsx │ ├── api-key-dialog.tsx │ ├── copy-button.tsx │ ├── customization-dropdown.tsx │ ├── export-dropdown.tsx │ ├── footer.tsx │ ├── header.tsx │ ├── hero.tsx │ ├── loading-animation.tsx │ ├── loading.tsx │ ├── main-card.tsx │ ├── mermaid-diagram.tsx │ ├── private-repos-dialog.tsx │ └── ui │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── dialog.tsx │ │ ├── input.tsx │ │ ├── progress.tsx │ │ ├── switch.tsx │ │ ├── textarea.tsx │ │ └── tooltip.tsx ├── env.js ├── hooks │ └── useDiagram.ts ├── lib │ ├── exampleRepos.ts │ ├── fetch-backend.ts │ └── utils.ts ├── server │ └── db │ │ ├── index.ts │ │ └── schema.ts └── styles │ └── globals.css ├── start-database.sh ├── start-services.sh ├── stop-services.sh ├── tailwind.config.ts ├── tsconfig.json └── update_ec2.sh /.env.example: -------------------------------------------------------------------------------- 1 | POSTGRES_URL="postgresql://postgres:password@localhost:5432/solidityVisualizer" 2 | NEXT_PUBLIC_API_DEV_URL=http://localhost:8000 3 | NEXT_PUBLIC_API_PROD_URL= 4 | 5 | OPENAI_API_KEY= 6 | 7 | # OPTIONAL: providing your own GitHub PAT increases rate limits from 60/hr to 5000/hr to the GitHub API 8 | GITHUB_PAT= 9 | 10 | # old implementation 11 | # OPENROUTER_API_KEY= 12 | # ANTHROPIC_API_KEY= -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import("eslint").Linter.Config} */ 2 | const config = { 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "project": true 6 | }, 7 | "plugins": [ 8 | "@typescript-eslint", 9 | "drizzle" 10 | ], 11 | "extends": [ 12 | "next/core-web-vitals", 13 | "plugin:@typescript-eslint/recommended-type-checked", 14 | "plugin:@typescript-eslint/stylistic-type-checked" 15 | ], 16 | "rules": { 17 | "@typescript-eslint/array-type": "off", 18 | "@typescript-eslint/consistent-type-definitions": "off", 19 | "@typescript-eslint/consistent-type-imports": [ 20 | "warn", 21 | { 22 | "prefer": "type-imports", 23 | "fixStyle": "inline-type-imports" 24 | } 25 | ], 26 | "@typescript-eslint/no-unused-vars": [ 27 | "warn", 28 | { 29 | "argsIgnorePattern": "^_" 30 | } 31 | ], 32 | "@typescript-eslint/require-await": "off", 33 | "@typescript-eslint/no-misused-promises": [ 34 | "error", 35 | { 36 | "checksVoidReturn": { 37 | "attributes": false 38 | } 39 | } 40 | ], 41 | "drizzle/enforce-delete-with-where": [ 42 | "error", 43 | { 44 | "drizzleObjectName": [ 45 | "db", 46 | "ctx.db" 47 | ] 48 | } 49 | ], 50 | "drizzle/enforce-update-with-where": [ 51 | "error", 52 | { 53 | "drizzleObjectName": [ 54 | "db", 55 | "ctx.db" 56 | ] 57 | } 58 | ] 59 | } 60 | } 61 | module.exports = config; -------------------------------------------------------------------------------- /.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 | 8 | # testing 9 | /coverage 10 | 11 | # database 12 | /prisma/db.sqlite 13 | /prisma/db.sqlite-journal 14 | db.sqlite 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | next-env.d.ts 20 | 21 | # production 22 | /build 23 | 24 | # misc 25 | .DS_Store 26 | *.pem 27 | 28 | # debug 29 | npm-debug.log* 30 | yarn-debug.log* 31 | yarn-error.log* 32 | .pnpm-debug.log* 33 | 34 | # local env files 35 | # do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables 36 | .env 37 | .env*.local 38 | .env-e 39 | 40 | # vercel 41 | .vercel 42 | 43 | # typescript 44 | *.tsbuildinfo 45 | 46 | # idea files 47 | .idea 48 | 49 | __pycache__/ 50 | venv 51 | 52 | # vscode 53 | .vscode/ 54 | 55 | # cursor 56 | .cursor/ 57 | 58 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Ahmed Khaleel 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Image](./docs/readme_img.png "Solidity Visualizer Front Page")](https://solidityVisualizer.com/) 2 | 3 | ![License](https://img.shields.io/badge/license-MIT-blue.svg) 4 | [![Kofi](https://img.shields.io/badge/Kofi-F16061.svg?logo=ko-fi&logoColor=white)](https://ko-fi.com/VGabriel45) 5 | 6 | # Solidity Visualizer 7 | 8 | Visualize any Solidity project. 9 | 10 | ## ⚙️ Tech Stack 11 | 12 | - **Frontend**: Next.js, TypeScript, Tailwind CSS, ShadCN 13 | - **Backend**: FastAPI, Python, Server Actions 14 | - **Database**: PostgreSQL (with Drizzle ORM) 15 | - **AI**: OpenAI o3-mini, Claude 3.5 Sonnet 16 | - **Deployment**: Vercel (Frontend), EC2 (Backend) 17 | - **CI/CD**: GitHub Actions 18 | - **Analytics**: PostHog, Api-Analytics 19 | 20 | ## 🤔 About 21 | 22 | This project was created to help Solidity developers explore and understand Blockchain projects faster and better, personally I have been in the position where I need to understand a new project but quickly realized the codebase is too massive for me to dig through manually, not to mention that Solidity smart contracts are not always the easiest to understand. 23 | 24 | Please follow the instructions to run this locally, this is a demo app and it is not deployed anyhwere yet. 25 | 26 | ## 🔒 How to diagram private repositories 27 | 28 | You can simply click on "Private Repos" in the header and follow the instructions by providing a GitHub personal access token with the `repo` scope. 29 | 30 | You can also self-host this app locally (backend separated as well!) with the steps below. 31 | 32 | ## 🛠️ Self-hosting / Local Development 33 | 34 | 1. Clone the repository 35 | 36 | ```bash 37 | git clone https://github.com/VGabriel45/solidityVisualizer.git 38 | cd solidityVisualizer 39 | ``` 40 | 41 | 2. Install dependencies 42 | 43 | ```bash 44 | pnpm i 45 | ``` 46 | 47 | 3. Set up environment variables (create .env) 48 | 49 | ```bash 50 | cp .env.example .env 51 | ``` 52 | 53 | Then edit the `.env` file with your Anthropic API key and optional GitHub personal access token. 54 | 55 | 4. Run backend 56 | 57 | ```bash 58 | docker compose up --build -d 59 | ``` 60 | 61 | Logs available at `docker-compose logs -f` 62 | The FastAPI server will be available at `localhost:8000` 63 | 64 | 5. Start local database 65 | 66 | ```bash 67 | chmod +x start-database.sh 68 | ./start-database.sh 69 | ``` 70 | 71 | When prompted to generate a random password, input yes. 72 | The Postgres database will start in a container at `localhost:5432` 73 | 74 | 6. Initialize the database schema 75 | 76 | ```bash 77 | pnpm db:push 78 | ``` 79 | 80 | You can view and interact with the database using `pnpm db:studio` 81 | 82 | 7. Run Frontend 83 | 84 | ```bash 85 | pnpm dev 86 | ``` 87 | 88 | You can now access the website at `localhost:3000` and edit the rate limits defined in `backend/app/routers/generate.py` in the generate function decorator. 89 | 90 | ## Contributing 91 | 92 | Contributions are welcome! Please feel free to submit a Pull Request. 93 | 94 | ## Acknowledgements 95 | 96 | Shoutout to [Romain Courtois](https://github.com/cyclotruc)'s [Gitingest](https://gitingest.com/) for inspiration and styling 97 | 98 | ## Support the Project 99 | 100 | Solidity Visualizer is an open-source tool. If you find it useful for your work, consider supporting the ongoing development. 101 | 102 | ### Buy Me a Coffee 103 | 104 | Buy Me a Coffee at ko-fi.com 105 | 106 | ### Crypto Donations 107 | `0x373aa015cEeE5dca6740E5003433142894Bf5E0c` 108 | 109 | ### Other Ways to Support 110 | 111 | - ⭐ Star this repository 112 | - 🔀 Share with others who might find it useful 113 | - 🐛 Report bugs and suggest features 114 | - 💻 Contribute to the codebase 115 | 116 | Your support helps make Solidity Visualizer better for everyone in the blockchain community! Thank you! -------------------------------------------------------------------------------- /backend/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use Python 3.12 slim image for smaller size 2 | FROM python:3.12-slim 3 | 4 | # Set working directory 5 | WORKDIR /app 6 | 7 | # Copy requirements first to leverage Docker cache 8 | COPY requirements.txt . 9 | 10 | # Install dependencies 11 | RUN pip install --no-cache-dir -r requirements.txt 12 | 13 | # Copy application code 14 | COPY . . 15 | 16 | # Create and set permissions for entrypoint script 17 | COPY entrypoint.sh /app/ 18 | RUN chmod +x /app/entrypoint.sh && \ 19 | # Ensure the script uses Unix line endings 20 | sed -i 's/\r$//' /app/entrypoint.sh && \ 21 | # Double check permissions 22 | ls -la /app/entrypoint.sh 23 | 24 | # Expose port 25 | EXPOSE 8000 26 | 27 | # Use entrypoint script 28 | CMD ["/bin/bash", "/app/entrypoint.sh"] 29 | -------------------------------------------------------------------------------- /backend/app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VGabriel45/SolidityVisualizer/1c41ac49c6a09ec7cf721b3a94b929c607121239/backend/app/__init__.py -------------------------------------------------------------------------------- /backend/app/core/limiter.py: -------------------------------------------------------------------------------- 1 | from slowapi import Limiter 2 | from slowapi.util import get_remote_address 3 | 4 | limiter = Limiter(key_func=get_remote_address) 5 | -------------------------------------------------------------------------------- /backend/app/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, Request 2 | from fastapi.middleware.cors import CORSMiddleware 3 | from slowapi import _rate_limit_exceeded_handler 4 | from slowapi.errors import RateLimitExceeded 5 | from app.routers import generate, modify 6 | from app.core.limiter import limiter 7 | from typing import cast 8 | from starlette.exceptions import ExceptionMiddleware 9 | from api_analytics.fastapi import Analytics 10 | import os 11 | 12 | 13 | app = FastAPI() 14 | 15 | 16 | origins = [ 17 | "https://solidityvisualizer.com", 18 | "http://solidityvisualizer.com" 19 | ] 20 | 21 | app.add_middleware( 22 | CORSMiddleware, 23 | allow_origins=origins, 24 | allow_credentials=True, 25 | allow_methods=["GET", "POST"], 26 | allow_headers=["*"], 27 | ) 28 | 29 | API_ANALYTICS_KEY = os.getenv("API_ANALYTICS_KEY") 30 | if API_ANALYTICS_KEY: 31 | app.add_middleware(Analytics, api_key=API_ANALYTICS_KEY) 32 | 33 | app.state.limiter = limiter 34 | app.add_exception_handler( 35 | RateLimitExceeded, cast(ExceptionMiddleware, _rate_limit_exceeded_handler) 36 | ) 37 | 38 | app.include_router(generate.router) 39 | app.include_router(modify.router) 40 | 41 | 42 | @app.get("/") 43 | # @limiter.limit("100/day") 44 | async def root(request: Request): 45 | return {"message": "Hello from Solidity Visualizer API!"} 46 | -------------------------------------------------------------------------------- /backend/app/prompts.py: -------------------------------------------------------------------------------- 1 | SYSTEM_FIRST_PROMPT = """ 2 | You are tasked with explaining to a principal blockchain engineer how to draw the best and most accurate system design diagram / architecture of a Solidity smart contract project. This explanation should be tailored to the specific project's purpose and structure. To accomplish this, you will be provided with two key pieces of information: 3 | 4 | 1. The complete and entire file tree of the project including all directory and file names, which will be enclosed in tags in the users message. 5 | 6 | 2. The README file of the project, which will be enclosed in tags in the users message. 7 | 8 | Analyze these components carefully, as they will provide crucial information about the smart contract architecture and purpose. Follow these steps to create an explanation for the blockchain engineer: 9 | 10 | 1. Identify the project type and purpose: 11 | - Examine the file structure and README to determine the type of smart contract system (e.g., DeFi protocol, NFT marketplace, DAO, token contract). 12 | - Look for key indicators in the README, such as project description, features, or use cases. 13 | - Identify the blockchain networks it's designed for (e.g., Ethereum, Polygon, BSC). 14 | 15 | 2. Analyze the smart contract structure: 16 | - Pay attention to contract files and their organization (e.g., "contracts/", "interfaces/", "libraries/"). 17 | - Identify patterns in the contract structure that might indicate architectural choices (e.g., proxy patterns, inheritance hierarchies). 18 | - Note any deployment scripts, test files, or configuration files. 19 | - Look for important Solidity patterns like libraries, interfaces, and abstract contracts. 20 | 21 | 3. Examine the README for additional insights: 22 | - Look for sections describing the contract architecture, dependencies, or technical stack. 23 | - Check for any diagrams or explanations of the system's components. 24 | - Note any important protocol integrations or external contract dependencies. 25 | 26 | 4. Based on your analysis, explain how to create a system design diagram that accurately represents the smart contract architecture. Include the following points: 27 | 28 | a. Identify the main components of the system: 29 | - Core contracts and their purposes 30 | - External contract integrations 31 | - Protocol interfaces 32 | - Access control and admin functions 33 | - Token flows and interactions 34 | b. Determine the relationships and inheritance between contracts 35 | c. Highlight any important architectural patterns or design principles used (e.g., upgradability patterns, access control) 36 | d. Include relevant dependencies, oracles, or external protocols that interact with the system 37 | 38 | 5. Provide guidelines for tailoring the diagram to the specific smart contract type: 39 | - For DeFi protocols, emphasize token flows, liquidity pools, and protocol integrations 40 | - For NFT systems, focus on minting, marketplace functions, and metadata handling 41 | - For DAOs, highlight governance mechanisms and treasury management 42 | - For token contracts, show token economics and distribution mechanisms 43 | 44 | 6. Instruct the blockchain engineer to include the following elements in the diagram: 45 | - Clear labels for each contract and component 46 | - Directional arrows to show inheritance, calls, and token flows 47 | - Color coding or shapes to distinguish between different types of contracts (e.g., core contracts, interfaces, libraries) 48 | - Important state variables and key functions that define the protocol's behavior 49 | 50 | 7. NOTE: Emphasize the importance of being very detailed and capturing the essential smart contract elements. Focus on security-critical components and access control flows. Don't overthink it too much, simply separating the contracts into logical components is best. 51 | 52 | Present your explanation and instructions within tags, ensuring that you tailor your advice to the specific smart contract project based on the provided file tree and README content. 53 | """ 54 | 55 | SYSTEM_SECOND_PROMPT = """ 56 | You are tasked with mapping key components of a smart contract system to their corresponding files and directories in a project's file structure. You will be provided with a detailed explanation of the contract architecture and a file tree of the project. 57 | 58 | First, carefully read the system design explanation which will be enclosed in tags in the users message. 59 | 60 | Then, examine the file tree of the project which will be enclosed in tags in the users message. 61 | 62 | Your task is to analyze the smart contract architecture explanation and identify key contracts, interfaces, and libraries mentioned. Then, try your best to map these components to what you believe could be their corresponding Solidity files and directories in the provided file tree. 63 | 64 | Guidelines: 65 | 1. Focus on major contracts and components described in the system design. 66 | 2. Look for .sol files and contract-related directories that clearly correspond to these components. 67 | 3. Include both contract directories and specific Solidity files when relevant. 68 | 4. Pay special attention to: 69 | - Core contract implementations 70 | - Interfaces and abstract contracts 71 | - Libraries and utility contracts 72 | - Test files that demonstrate component interaction 73 | 5. If a component doesn't have a clear corresponding file or directory, simply dont include it in the map. 74 | 75 | Now, provide your final answer in the following format: 76 | 77 | 78 | 1. [Contract/Component Name]: [File/Directory Path] 79 | 2. [Contract/Component Name]: [File/Directory Path] 80 | [Continue for all identified components] 81 | 82 | 83 | Remember to be as specific as possible in your mappings, only use what is given to you from the file tree, and to strictly follow the components mentioned in the explanation. 84 | """ 85 | 86 | SYSTEM_THIRD_PROMPT = """ 87 | You are tasked with creating a sequence diagram using Mermaid.js based on a detailed explanation of a smart contract system. Your goal is to accurately represent the interactions and flow between different components of the smart contract project. 88 | 89 | The detailed explanation of the design will be enclosed in tags in the users message. 90 | 91 | Also, sourced from the explanation, as a bonus, a few of the identified components have been mapped to their paths in the project file tree, whether it is a directory or file which will be enclosed in tags in the users message. 92 | 93 | STRICT SYNTAX RULES (YOU MUST FOLLOW THESE EXACTLY): 94 | 1. Start with exactly: `sequenceDiagram` 95 | 96 | 2. Participant Definitions: 97 | - Must declare all participants at the start 98 | - Format: `participant ParticipantName` 99 | - Example: `participant Contract` 100 | - Example: `participant User` 101 | - BAD: `participant Contract-1` (no special characters) 102 | 103 | 3. Message Types: 104 | - Solid line with arrow: `->>` (synchronous request) 105 | - Dashed line with arrow: `-->>` (asynchronous response) 106 | - Solid line with cross: `-x` (failed request) 107 | - Dashed line with cross: `--x` (failed response) 108 | - Example: `Contract ->> Proxy: deploy()` 109 | - Example: `DB -->> Service: userData` 110 | - BAD: `Contract->Proxy` (wrong arrow syntax) 111 | - BAD: `Contract => Proxy` (invalid syntax) 112 | 113 | 4. Activation and Deactivation: 114 | - Activate: `activate ParticipantName` 115 | - Deactivate: `deactivate ParticipantName` 116 | - Example: 117 | ``` 118 | Contract ->> Proxy: call() 119 | activate Proxy 120 | Proxy -->> Contract: result 121 | deactivate Proxy 122 | ``` 123 | 124 | 5. Control Structures: 125 | - Alt blocks (if/else): 126 | ``` 127 | alt Condition 128 | Contract ->> Proxy: success() 129 | else 130 | Contract ->> Logger: error() 131 | end 132 | ``` 133 | - Optional blocks: 134 | ``` 135 | opt Condition 136 | Contract ->> Logger: log() 137 | end 138 | ``` 139 | - Loop blocks: 140 | ``` 141 | loop Retry Logic 142 | Contract ->> Service: retry() 143 | end 144 | ``` 145 | 146 | 6. Notes: 147 | - Use only **two participants** in `Note over X,Y:` (no more). ✅ IMPORTANT! 148 | - Right of participant: `Note right of ParticipantName: text` 149 | - Left of participant: `Note left of ParticipantName: text` 150 | - Over participants: `Note over Participant1,Participant2: text` 151 | - Example: `Note right of Contract: Deploys new instance` 152 | - Do not use `\n` for line breaks in notes — use `
` instead. ✅ 153 | - Avoid `style` or `classDef` blocks — they are **not supported** in `sequenceDiagram`. 154 | 155 | HERE'S A COMPLETE EXAMPLE OF PROPER SYNTAX AND FLOW: 156 | ```mermaid 157 | sequenceDiagram 158 | %% Participants 159 | participant User 160 | participant Frontend 161 | participant AuthService 162 | participant DB 163 | participant EmailService 164 | 165 | %% Initial interaction 166 | User ->> Frontend: click "Sign Up" 167 | activate Frontend 168 | 169 | %% Form submission 170 | Frontend ->> AuthService: createUser(name, email, password) 171 | deactivate Frontend 172 | activate AuthService 173 | 174 | %% Backend validation 175 | AuthService ->> AuthService: validateInput() 176 | AuthService ->> DB: checkIfUserExists(email) 177 | activate DB 178 | DB -->> AuthService: userNotFound 179 | deactivate DB 180 | 181 | %% Create user 182 | AuthService ->> DB: insertUser(data) 183 | activate DB 184 | DB -->> AuthService: userId 185 | deactivate DB 186 | 187 | %% Send welcome email 188 | AuthService ->> EmailService: sendWelcomeEmail(email) 189 | activate EmailService 190 | EmailService -->> AuthService: emailSent 191 | deactivate EmailService 192 | 193 | %% Auth token creation 194 | AuthService ->> AuthService: generateJWT(userId) 195 | AuthService -->> Frontend: token, userData 196 | deactivate AuthService 197 | activate Frontend 198 | 199 | %% Frontend confirmation 200 | Frontend ->> User: show success screen 201 | deactivate Frontend 202 | 203 | %% Conditionals 204 | alt EmailService Down 205 | activate AuthService 206 | AuthService ->> AuthService: logError("email failed") 207 | deactivate AuthService 208 | else Email sent successfully 209 | activate AuthService 210 | AuthService ->> DB: updateUser(emailSent=true) 211 | activate DB 212 | DB -->> AuthService: OK 213 | deactivate DB 214 | deactivate AuthService 215 | end 216 | 217 | %% Optional block 218 | opt Remember Me 219 | activate Frontend 220 | Frontend ->> LocalStorage: store token 221 | deactivate Frontend 222 | end 223 | 224 | %% Looping flow 225 | loop Retry on Failure (max 3 times) 226 | activate Frontend 227 | Frontend ->> AuthService: createUser(...) 228 | deactivate Frontend 229 | activate AuthService 230 | AuthService ->> DB: checkIfUserExists(...) 231 | DB -->> AuthService: ... 232 | deactivate AuthService 233 | end 234 | 235 | %% Note annotations 236 | Note right of User: Enters name, email and password 237 | Note left of AuthService: Handles all signup logic 238 | Note over EmailService, DB: Sends confirmation and updates user record 239 | 240 | %% Styling (not officially supported in sequenceDiagram yet but shown for completeness) 241 | %% classDef service fill:#bbf,stroke:#333,stroke-width:2px 242 | %% class AuthService,EmailService,DB service 243 | ``` 244 | 245 | VALIDATION CHECKLIST (VERIFY BEFORE RETURNING): 246 | 1. [ ] Diagram starts with `sequenceDiagram` 247 | 2. [ ] All participants are declared at the start 248 | 3. [ ] Correct arrow syntax is used (->> for requests, -->> for responses) 249 | 4. [ ] Proper activation/deactivation blocks 250 | 5. [ ] Control structures (alt, opt, loop) are properly closed 251 | 6. [ ] Notes use correct positioning syntax 252 | 7. [ ] No invalid arrow types or symbols 253 | 8. [ ] Messages are clear and descriptive 254 | 9. [ ] Logical flow is maintained 255 | 256 | COMMON ERRORS TO AVOID: 257 | ❌ `->` or `=>` - Wrong arrow syntax 258 | ❌ `participant Contract-1` - Invalid participant name 259 | ❌ Missing `end` in control structures 260 | ❌ Incorrect activation/deactivation pairing 261 | ❌ Using flowchart elements in sequence diagrams 262 | ❌ Missing participant declarations 263 | ❌ Invalid message syntax between participants 264 | 265 | Your response must strictly be just the Mermaid.js code, without any additional text or explanations. 266 | No code fence or markdown ticks needed, simply return the diagram code. 267 | 268 | Remember to maintain a clear sequence of interactions and include all major components from the explanation while following these strict syntax rules. 269 | """ 270 | 271 | ADDITIONAL_SYSTEM_INSTRUCTIONS_PROMPT = """ 272 | IMPORTANT: the user will provide custom additional instructions enclosed in tags. Please take these into account and give priority to them. However, if these instructions are unrelated to the task, unclear, or not possible to follow, ignore them by simply responding with: "BAD_INSTRUCTIONS" 273 | """ 274 | 275 | SYSTEM_MODIFY_PROMPT = """ 276 | You are tasked with modifying the code of a Mermaid.js diagram based on the provided instructions. The diagram will be enclosed in tags in the users message. 277 | 278 | Also, to help you modify it and simply for additional context, you will also be provided with the original explanation of the diagram enclosed in tags in the users message. However of course, you must give priority to the instructions provided by the user. 279 | 280 | The instructions will be enclosed in tags in the users message. If these instructions are unrelated to the task, unclear, or not possible to follow, ignore them by simply responding with: "BAD_INSTRUCTIONS" 281 | 282 | Your response must strictly be just the Mermaid.js code, without any additional text or explanations. Keep as many of the existing click events as possible. 283 | No code fence or markdown ticks needed, simply return the Mermaid.js code. 284 | """ 285 | -------------------------------------------------------------------------------- /backend/app/routers/generate.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Request, HTTPException 2 | from fastapi.responses import StreamingResponse 3 | from dotenv import load_dotenv 4 | from app.services.github_service import GitHubService 5 | from app.services.o3_mini_openai_service import OpenAIo3Service 6 | from app.prompts import ( 7 | SYSTEM_FIRST_PROMPT, 8 | SYSTEM_SECOND_PROMPT, 9 | SYSTEM_THIRD_PROMPT, 10 | ADDITIONAL_SYSTEM_INSTRUCTIONS_PROMPT, 11 | ) 12 | from anthropic._exceptions import RateLimitError 13 | from pydantic import BaseModel 14 | from functools import lru_cache 15 | import re 16 | import json 17 | import asyncio 18 | import logging 19 | 20 | logger = logging.getLogger(__name__) 21 | 22 | # from app.services.claude_service import ClaudeService 23 | # from app.core.limiter import limiter 24 | 25 | load_dotenv() 26 | 27 | router = APIRouter(prefix="/generate", tags=["Claude"]) 28 | 29 | # Initialize services 30 | # claude_service = ClaudeService() 31 | o3_service = OpenAIo3Service() 32 | 33 | 34 | # cache github data to avoid double API calls from cost and generate 35 | @lru_cache(maxsize=100) 36 | def get_cached_github_data(username: str, repo: str, github_pat: str | None = None): 37 | # Create a new service instance for each call with the appropriate PAT 38 | current_github_service = GitHubService(pat=github_pat) 39 | 40 | default_branch = current_github_service.get_default_branch(username, repo) 41 | if not default_branch: 42 | default_branch = "main" # fallback value 43 | 44 | file_tree = current_github_service.get_github_file_paths_as_list(username, repo) 45 | readme = current_github_service.get_github_readme(username, repo) 46 | 47 | return {"default_branch": default_branch, "file_tree": file_tree, "readme": readme} 48 | 49 | 50 | class ApiRequest(BaseModel): 51 | username: str 52 | repo: str 53 | instructions: str = "" 54 | api_key: str | None = None 55 | github_pat: str | None = None 56 | 57 | 58 | @router.post("/cost") 59 | # @limiter.limit("5/minute") # TEMP: disable rate limit for growth?? 60 | async def get_generation_cost(request: Request, body: ApiRequest): 61 | try: 62 | logger.info(f"Calculating generation cost for repository: {body.username}/{body.repo}") 63 | # Get file tree and README content 64 | github_data = get_cached_github_data(body.username, body.repo, body.github_pat) 65 | file_tree = github_data["file_tree"] 66 | readme = github_data["readme"] 67 | 68 | # Calculate combined token count 69 | # file_tree_tokens = claude_service.count_tokens(file_tree) 70 | # readme_tokens = claude_service.count_tokens(readme) 71 | 72 | file_tree_tokens = o3_service.count_tokens(file_tree) 73 | readme_tokens = o3_service.count_tokens(readme) 74 | 75 | logger.info(f"Token counts - File tree: {file_tree_tokens}, README: {readme_tokens}") 76 | 77 | # CLAUDE: Calculate approximate cost 78 | # Input cost: $3 per 1M tokens ($0.000003 per token) 79 | # Output cost: $15 per 1M tokens ($0.000015 per token) 80 | # input_cost = ((file_tree_tokens * 2 + readme_tokens) + 3000) * 0.000003 81 | # output_cost = 3500 * 0.000015 82 | # estimated_cost = input_cost + output_cost 83 | 84 | # Input cost: $1.1 per 1M tokens ($0.0000011 per token) 85 | # Output cost: $4.4 per 1M tokens ($0.0000044 per token) 86 | input_cost = ((file_tree_tokens * 2 + readme_tokens) + 3000) * 0.0000011 87 | output_cost = ( 88 | 8000 * 0.0000044 89 | ) # 8k just based on what I've seen (reasoning is expensive) 90 | estimated_cost = input_cost + output_cost 91 | 92 | # Format as currency string 93 | cost_string = f"${estimated_cost:.2f} USD" 94 | logger.info(f"Estimated cost: {cost_string}") 95 | return {"cost": cost_string} 96 | except Exception as e: 97 | logger.error(f"Error calculating generation cost: {str(e)}", exc_info=True) 98 | return {"error": str(e)} 99 | 100 | 101 | def process_click_events(diagram: str, username: str, repo: str, branch: str) -> str: 102 | """ 103 | Process click events in Mermaid diagram to include full GitHub URLs. 104 | Detects if path is file or directory and uses appropriate URL format. 105 | """ 106 | 107 | def replace_path(match): 108 | # Extract the path from the click event 109 | path = match.group(2).strip("\"'") 110 | 111 | # Determine if path is likely a file (has extension) or directory 112 | is_file = "." in path.split("/")[-1] 113 | 114 | # Construct GitHub URL 115 | base_url = f"https://github.com/{username}/{repo}" 116 | path_type = "blob" if is_file else "tree" 117 | full_url = f"{base_url}/{path_type}/{branch}/{path}" 118 | 119 | # Return the full click event with the new URL 120 | return f'click {match.group(1)} "{full_url}"' 121 | 122 | # Match click events: click ComponentName "path/to/something" 123 | click_pattern = r'click ([^\s"]+)\s+"([^"]+)"' 124 | return re.sub(click_pattern, replace_path, diagram) 125 | 126 | 127 | @router.post("/stream") 128 | async def generate_stream(request: Request, body: ApiRequest): 129 | try: 130 | logger.info(f"Starting generation for repository: {body.username}/{body.repo}") 131 | 132 | # Initial validation checks 133 | if len(body.instructions) > 1000: 134 | logger.warning("Instructions exceed maximum length") 135 | return {"error": "Instructions exceed maximum length of 1000 characters"} 136 | 137 | if body.repo in [ 138 | "fastapi", 139 | "streamlit", 140 | "flask", 141 | "api-analytics", 142 | "monkeytype", 143 | ]: 144 | logger.warning(f"Attempted to regenerate example repo: {body.repo}") 145 | return {"error": "Example repos cannot be regenerated"} 146 | 147 | async def event_generator(): 148 | try: 149 | # Get cached github data 150 | logger.info("Fetching GitHub data") 151 | github_data = get_cached_github_data( 152 | body.username, body.repo, body.github_pat 153 | ) 154 | default_branch = github_data["default_branch"] 155 | file_tree = github_data["file_tree"] 156 | readme = github_data["readme"] 157 | 158 | logger.info(f"Successfully fetched GitHub data. Default branch: {default_branch}") 159 | 160 | # Send initial status 161 | yield f"data: {json.dumps({'status': 'started', 'message': 'Starting generation process...'})}\n\n" 162 | await asyncio.sleep(0.1) 163 | 164 | # Token count check 165 | combined_content = f"{file_tree}\n{readme}" 166 | token_count = o3_service.count_tokens(combined_content) 167 | logger.info(f"Total token count: {token_count}") 168 | 169 | # Require API key for all operations 170 | if not body.api_key: 171 | logger.warning("No API key provided") 172 | yield f"data: {json.dumps({'error': 'An OpenAI API key is required to generate diagrams. Please provide your API key to continue.'})}\n\n" 173 | return 174 | elif token_count > 195000: 175 | logger.warning(f"Token count ({token_count}) exceeds maximum limit") 176 | yield f"data: {json.dumps({'error': f'Repository is too large (>195k tokens) for analysis. OpenAI o3-mini\'s max context length is 200k tokens. Current size: {token_count} tokens.'})}\n\n" 177 | return 178 | 179 | # Prepare prompts 180 | first_system_prompt = SYSTEM_FIRST_PROMPT 181 | third_system_prompt = SYSTEM_THIRD_PROMPT 182 | if body.instructions: 183 | first_system_prompt = ( 184 | first_system_prompt 185 | + "\n" 186 | + ADDITIONAL_SYSTEM_INSTRUCTIONS_PROMPT 187 | ) 188 | third_system_prompt = ( 189 | third_system_prompt 190 | + "\n" 191 | + ADDITIONAL_SYSTEM_INSTRUCTIONS_PROMPT 192 | ) 193 | 194 | # Phase 1: Get explanation 195 | yield f"data: {json.dumps({'status': 'explanation_sent', 'message': 'Sending explanation request to o3-mini...'})}\n\n" 196 | await asyncio.sleep(0.1) 197 | yield f"data: {json.dumps({'status': 'explanation', 'message': 'Analyzing repository structure...'})}\n\n" 198 | explanation = "" 199 | async for chunk in o3_service.call_o3_api_stream( 200 | system_prompt=first_system_prompt, 201 | data={ 202 | "file_tree": file_tree, 203 | "readme": readme, 204 | "instructions": body.instructions, 205 | }, 206 | api_key=body.api_key, 207 | reasoning_effort="medium", 208 | ): 209 | explanation += chunk 210 | yield f"data: {json.dumps({'status': 'explanation_chunk', 'chunk': chunk})}\n\n" 211 | 212 | if "BAD_INSTRUCTIONS" in explanation: 213 | yield f"data: {json.dumps({'error': 'Invalid or unclear instructions provided'})}\n\n" 214 | return 215 | 216 | # Phase 2: Get component mapping 217 | yield f"data: {json.dumps({'status': 'mapping_sent', 'message': 'Sending component mapping request to o3-mini...'})}\n\n" 218 | await asyncio.sleep(0.1) 219 | yield f"data: {json.dumps({'status': 'mapping', 'message': 'Creating component mapping...'})}\n\n" 220 | full_second_response = "" 221 | async for chunk in o3_service.call_o3_api_stream( 222 | system_prompt=SYSTEM_SECOND_PROMPT, 223 | data={"explanation": explanation, "file_tree": file_tree}, 224 | api_key=body.api_key, 225 | reasoning_effort="low", 226 | ): 227 | full_second_response += chunk 228 | yield f"data: {json.dumps({'status': 'mapping_chunk', 'chunk': chunk})}\n\n" 229 | 230 | # i dont think i need this anymore? but keep it here for now 231 | # Extract component mapping 232 | start_tag = "" 233 | end_tag = "" 234 | component_mapping_text = full_second_response[ 235 | full_second_response.find(start_tag) : full_second_response.find( 236 | end_tag 237 | ) 238 | ] 239 | 240 | # Phase 3: Generate Mermaid diagram 241 | yield f"data: {json.dumps({'status': 'diagram_sent', 'message': 'Sending diagram generation request to o3-mini...'})}\n\n" 242 | await asyncio.sleep(0.1) 243 | yield f"data: {json.dumps({'status': 'diagram', 'message': 'Generating diagram...'})}\n\n" 244 | mermaid_code = "" 245 | async for chunk in o3_service.call_o3_api_stream( 246 | system_prompt=third_system_prompt, 247 | data={ 248 | "explanation": explanation, 249 | "component_mapping": component_mapping_text, 250 | "instructions": body.instructions, 251 | }, 252 | api_key=body.api_key, 253 | reasoning_effort="medium", 254 | ): 255 | mermaid_code += chunk 256 | yield f"data: {json.dumps({'status': 'diagram_chunk', 'chunk': chunk})}\n\n" 257 | 258 | # Process final diagram 259 | mermaid_code = mermaid_code.replace("```mermaid", "").replace("```", "") 260 | if "BAD_INSTRUCTIONS" in mermaid_code: 261 | yield f"data: {json.dumps({'error': 'Invalid or unclear instructions provided'})}\n\n" 262 | return 263 | 264 | processed_diagram = process_click_events( 265 | mermaid_code, body.username, body.repo, default_branch 266 | ) 267 | 268 | # Send final result 269 | yield f"data: {json.dumps({ 270 | 'status': 'complete', 271 | 'diagram': processed_diagram, 272 | 'explanation': explanation, 273 | 'mapping': component_mapping_text 274 | })}\n\n" 275 | 276 | except Exception as e: 277 | logger.error(f"Error in event generator: {str(e)}", exc_info=True) 278 | yield f"data: {json.dumps({'error': str(e)})}\n\n" 279 | 280 | return StreamingResponse( 281 | event_generator(), 282 | media_type="text/event-stream", 283 | headers={ 284 | "X-Accel-Buffering": "no", # Hint to Nginx 285 | "Cache-Control": "no-cache", 286 | "Connection": "keep-alive", 287 | }, 288 | ) 289 | except Exception as e: 290 | logger.error(f"Error in generate stream endpoint: {str(e)}", exc_info=True) 291 | return {"error": str(e)} 292 | -------------------------------------------------------------------------------- /backend/app/routers/modify.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Request, HTTPException 2 | from dotenv import load_dotenv 3 | 4 | # from app.services.claude_service import ClaudeService 5 | # from app.core.limiter import limiter 6 | from anthropic._exceptions import RateLimitError 7 | from app.prompts import SYSTEM_MODIFY_PROMPT 8 | from pydantic import BaseModel 9 | from app.services.o1_mini_openai_service import OpenAIO1Service 10 | 11 | 12 | load_dotenv() 13 | 14 | router = APIRouter(prefix="/modify", tags=["Claude"]) 15 | 16 | # Initialize services 17 | # claude_service = ClaudeService() 18 | o1_service = OpenAIO1Service() 19 | 20 | 21 | # Define the request body model 22 | 23 | 24 | class ModifyRequest(BaseModel): 25 | instructions: str 26 | current_diagram: str 27 | repo: str 28 | username: str 29 | explanation: str 30 | 31 | 32 | @router.post("") 33 | # @limiter.limit("2/minute;10/day") 34 | async def modify(request: Request, body: ModifyRequest): 35 | try: 36 | # Check instructions length 37 | if not body.instructions or not body.current_diagram: 38 | return {"error": "Instructions and/or current diagram are required"} 39 | elif ( 40 | len(body.instructions) > 1000 or len(body.current_diagram) > 100000 41 | ): # just being safe 42 | return {"error": "Instructions exceed maximum length of 1000 characters"} 43 | 44 | if body.repo in [ 45 | "fastapi", 46 | "streamlit", 47 | "flask", 48 | "api-analytics", 49 | "monkeytype", 50 | ]: 51 | return {"error": "Example repos cannot be modified"} 52 | 53 | # modified_mermaid_code = claude_service.call_claude_api( 54 | # system_prompt=SYSTEM_MODIFY_PROMPT, 55 | # data={ 56 | # "instructions": body.instructions, 57 | # "explanation": body.explanation, 58 | # "diagram": body.current_diagram, 59 | # }, 60 | # ) 61 | 62 | modified_mermaid_code = o1_service.call_o1_api( 63 | system_prompt=SYSTEM_MODIFY_PROMPT, 64 | data={ 65 | "instructions": body.instructions, 66 | "explanation": body.explanation, 67 | "diagram": body.current_diagram, 68 | }, 69 | ) 70 | 71 | # Check for BAD_INSTRUCTIONS response 72 | if "BAD_INSTRUCTIONS" in modified_mermaid_code: 73 | return {"error": "Invalid or unclear instructions provided"} 74 | 75 | return {"diagram": modified_mermaid_code} 76 | except RateLimitError as e: 77 | raise HTTPException( 78 | status_code=429, 79 | detail="Service is currently experiencing high demand. Please try again in a few minutes.", 80 | ) 81 | except Exception as e: 82 | return {"error": str(e)} 83 | -------------------------------------------------------------------------------- /backend/app/services/claude_service.py: -------------------------------------------------------------------------------- 1 | from anthropic import Anthropic 2 | from dotenv import load_dotenv 3 | from app.utils.format_message import format_user_message 4 | 5 | load_dotenv() 6 | 7 | 8 | class ClaudeService: 9 | def __init__(self): 10 | self.default_client = Anthropic() 11 | 12 | def call_claude_api( 13 | self, system_prompt: str, data: dict, api_key: str | None = None 14 | ) -> str: 15 | """ 16 | Makes an API call to Claude and returns the response. 17 | 18 | Args: 19 | system_prompt (str): The instruction/system prompt 20 | data (dict): Dictionary of variables to format into the user message 21 | api_key (str | None): Optional custom API key 22 | 23 | Returns: 24 | str: Claude's response text 25 | """ 26 | # Create the user message with the data 27 | user_message = format_user_message(data) 28 | 29 | # Use custom client if API key provided, otherwise use default 30 | client = Anthropic(api_key=api_key) if api_key else self.default_client 31 | 32 | message = client.messages.create( 33 | model="claude-3-5-sonnet-latest", 34 | max_tokens=4096, 35 | temperature=0, 36 | system=system_prompt, 37 | messages=[ 38 | {"role": "user", "content": [{"type": "text", "text": user_message}]} 39 | ], 40 | ) 41 | return message.content[0].text # type: ignore 42 | 43 | def count_tokens(self, prompt: str) -> int: 44 | """ 45 | Counts the number of tokens in a prompt. 46 | 47 | Args: 48 | prompt (str): The prompt to count tokens for 49 | 50 | Returns: 51 | int: Number of input tokens 52 | """ 53 | response = self.default_client.messages.count_tokens( 54 | model="claude-3-5-sonnet-latest", 55 | messages=[{"role": "user", "content": prompt}], 56 | ) 57 | return response.input_tokens 58 | -------------------------------------------------------------------------------- /backend/app/services/github_service.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import jwt 3 | import time 4 | from datetime import datetime, timedelta 5 | from dotenv import load_dotenv 6 | import os 7 | import logging 8 | 9 | # Configure logging 10 | logging.basicConfig(level=logging.INFO) 11 | logger = logging.getLogger(__name__) 12 | 13 | load_dotenv() 14 | 15 | 16 | class GitHubService: 17 | def __init__(self, pat: str | None = None): 18 | # Try app authentication first 19 | self.client_id = os.getenv("GITHUB_CLIENT_ID") 20 | self.private_key = os.getenv("GITHUB_PRIVATE_KEY") 21 | self.installation_id = os.getenv("GITHUB_INSTALLATION_ID") 22 | 23 | # Use provided PAT if available, otherwise fallback to env PAT 24 | self.github_token = pat or os.getenv("GITHUB_PAT") 25 | 26 | # If no credentials are provided, warn about rate limits 27 | if ( 28 | not all([self.client_id, self.private_key, self.installation_id]) 29 | and not self.github_token 30 | ): 31 | logger.warning("No GitHub credentials provided. Using unauthenticated requests with rate limit of 60 requests/hour.") 32 | 33 | self.access_token = None 34 | self.token_expires_at = None 35 | 36 | # autopep8: off 37 | def _generate_jwt(self): 38 | now = int(time.time()) 39 | payload = { 40 | "iat": now, 41 | "exp": now + (10 * 60), # 10 minutes 42 | "iss": self.client_id, 43 | } 44 | # Convert PEM string format to proper newlines 45 | return jwt.encode(payload, self.private_key, algorithm="RS256") # type: ignore 46 | 47 | # autopep8: on 48 | 49 | def _get_installation_token(self): 50 | if self.access_token and self.token_expires_at > datetime.now(): # type: ignore 51 | return self.access_token 52 | 53 | jwt_token = self._generate_jwt() 54 | response = requests.post( 55 | f"https://api.github.com/app/installations/{ 56 | self.installation_id}/access_tokens", 57 | headers={ 58 | "Authorization": f"Bearer {jwt_token}", 59 | "Accept": "application/vnd.github+json", 60 | }, 61 | ) 62 | data = response.json() 63 | self.access_token = data["token"] 64 | self.token_expires_at = datetime.now() + timedelta(hours=1) 65 | return self.access_token 66 | 67 | def _get_headers(self): 68 | # If no credentials are available, return basic headers 69 | if ( 70 | not all([self.client_id, self.private_key, self.installation_id]) 71 | and not self.github_token 72 | ): 73 | return {"Accept": "application/vnd.github+json"} 74 | 75 | # Use PAT if available 76 | if self.github_token: 77 | return { 78 | "Authorization": f"token {self.github_token}", 79 | "Accept": "application/vnd.github+json", 80 | } 81 | 82 | # Otherwise use app authentication 83 | token = self._get_installation_token() 84 | return { 85 | "Authorization": f"Bearer {token}", 86 | "Accept": "application/vnd.github+json", 87 | "X-GitHub-Api-Version": "2022-11-28", 88 | } 89 | 90 | def _check_repository_exists(self, username, repo): 91 | """ 92 | Check if the repository exists using the GitHub API. 93 | """ 94 | api_url = f"https://api.github.com/repos/{username}/{repo}" 95 | logger.info(f"Checking repository existence: {api_url}") 96 | response = requests.get(api_url, headers=self._get_headers()) 97 | 98 | if response.status_code == 404: 99 | logger.error(f"Repository not found: {username}/{repo}") 100 | raise ValueError("Repository not found.") 101 | elif response.status_code != 200: 102 | logger.error(f"Failed to check repository: {response.status_code}, {response.json()}") 103 | raise Exception( 104 | f"Failed to check repository: {response.status_code}, {response.json()}" 105 | ) 106 | logger.info(f"Repository exists: {username}/{repo}") 107 | 108 | def get_default_branch(self, username, repo): 109 | """Get the default branch of the repository.""" 110 | api_url = f"https://api.github.com/repos/{username}/{repo}" 111 | logger.info(f"Fetching default branch for: {username}/{repo}") 112 | response = requests.get(api_url, headers=self._get_headers()) 113 | 114 | if response.status_code == 200: 115 | default_branch = response.json().get("default_branch") 116 | logger.info(f"Default branch for {username}/{repo}: {default_branch}") 117 | return default_branch 118 | logger.warning(f"Could not fetch default branch for {username}/{repo}. Status code: {response.status_code}") 119 | return None 120 | 121 | def get_github_file_paths_as_list(self, username, repo): 122 | """ 123 | Fetches the file tree of an open-source GitHub repository, 124 | excluding static files and generated code. 125 | 126 | Args: 127 | username (str): The GitHub username or organization name 128 | repo (str): The repository name 129 | 130 | Returns: 131 | str: A filtered and formatted string of file paths in the repository, one per line. 132 | """ 133 | logger.info(f"Attempting to fetch file tree for {username}/{repo}") 134 | 135 | def should_include_file(path): 136 | # Patterns to exclude 137 | excluded_patterns = [ 138 | # Dependencies 139 | "node_modules/", 140 | "vendor/", 141 | "venv/", 142 | # Compiled files 143 | ".min.", 144 | ".pyc", 145 | ".pyo", 146 | ".pyd", 147 | ".so", 148 | ".dll", 149 | ".class", 150 | # Asset files 151 | ".jpg", 152 | ".jpeg", 153 | ".png", 154 | ".gif", 155 | ".ico", 156 | ".svg", 157 | ".ttf", 158 | ".woff", 159 | ".webp", 160 | # Cache and temporary files 161 | "__pycache__/", 162 | ".cache/", 163 | ".tmp/", 164 | # Lock files and logs 165 | "yarn.lock", 166 | "poetry.lock", 167 | "*.log", 168 | # Configuration files 169 | ".vscode/", 170 | ".idea/", 171 | ] 172 | 173 | return not any(pattern in path.lower() for pattern in excluded_patterns) 174 | 175 | # Try to get the default branch first 176 | branch = self.get_default_branch(username, repo) 177 | if branch: 178 | api_url = f"https://api.github.com/repos/{username}/{repo}/git/trees/{branch}?recursive=1" 179 | logger.info(f"Fetching file tree using default branch {branch}: {api_url}") 180 | response = requests.get(api_url, headers=self._get_headers()) 181 | 182 | if response.status_code == 200: 183 | data = response.json() 184 | if "tree" in data: 185 | logger.info(f"Successfully fetched file tree using default branch {branch}") 186 | paths = [ 187 | item["path"] 188 | for item in data["tree"] 189 | if should_include_file(item["path"]) 190 | ] 191 | return "\n".join(paths) 192 | else: 193 | logger.warning(f"Failed to fetch file tree using default branch {branch}. Status: {response.status_code}") 194 | logger.debug(f"Response content: {response.text}") 195 | 196 | # If default branch didn't work or wasn't found, try common branch names 197 | for branch in ["main", "master"]: 198 | api_url = f"https://api.github.com/repos/{username}/{repo}/git/trees/{branch}?recursive=1" 199 | logger.info(f"Trying alternative branch {branch}: {api_url}") 200 | response = requests.get(api_url, headers=self._get_headers()) 201 | 202 | if response.status_code == 200: 203 | data = response.json() 204 | if "tree" in data: 205 | logger.info(f"Successfully fetched file tree using branch {branch}") 206 | paths = [ 207 | item["path"] 208 | for item in data["tree"] 209 | if should_include_file(item["path"]) 210 | ] 211 | return "\n".join(paths) 212 | else: 213 | logger.warning(f"Failed to fetch file tree using branch {branch}. Status: {response.status_code}") 214 | logger.debug(f"Response content: {response.text}") 215 | 216 | logger.error(f"Could not fetch repository file tree for {username}/{repo}") 217 | logger.error("Tried default branch and common branches (main, master)") 218 | raise ValueError( 219 | "Could not fetch repository file tree. Repository might not exist, be empty or private." 220 | ) 221 | 222 | def get_github_readme(self, username, repo): 223 | """ 224 | Fetches the README contents of an open-source GitHub repository. 225 | 226 | Args: 227 | username (str): The GitHub username or organization name 228 | repo (str): The repository name 229 | 230 | Returns: 231 | str: The contents of the README file. 232 | 233 | Raises: 234 | ValueError: If repository does not exist or has no README. 235 | Exception: For other unexpected API errors. 236 | """ 237 | # First check if the repository exists 238 | logger.info(f"Attempting to fetch README for {username}/{repo}") 239 | self._check_repository_exists(username, repo) 240 | 241 | # Then attempt to fetch the README 242 | api_url = f"https://api.github.com/repos/{username}/{repo}/readme" 243 | logger.info(f"Fetching README: {api_url}") 244 | response = requests.get(api_url, headers=self._get_headers()) 245 | 246 | if response.status_code == 404: 247 | logger.error(f"No README found for {username}/{repo}") 248 | raise ValueError("No README found for the specified repository.") 249 | elif response.status_code != 200: 250 | logger.error(f"Failed to fetch README: {response.status_code}, {response.json()}") 251 | raise Exception( 252 | f"Failed to fetch README: {response.status_code}, {response.json()}" 253 | ) 254 | 255 | data = response.json() 256 | readme_content = requests.get(data["download_url"]).text 257 | logger.info(f"Successfully fetched README for {username}/{repo}") 258 | return readme_content 259 | -------------------------------------------------------------------------------- /backend/app/services/o1_mini_openai_service.py: -------------------------------------------------------------------------------- 1 | from openai import OpenAI 2 | from dotenv import load_dotenv 3 | from app.utils.format_message import format_user_message 4 | import tiktoken 5 | import os 6 | import aiohttp 7 | import json 8 | from typing import AsyncGenerator 9 | 10 | load_dotenv() 11 | 12 | 13 | class OpenAIO1Service: 14 | def __init__(self): 15 | self.default_client = OpenAI( 16 | api_key=os.getenv("OPENAI_API_KEY"), 17 | ) 18 | self.encoding = tiktoken.get_encoding("o200k_base") # Encoder for OpenAI models 19 | self.base_url = "https://api.openai.com/v1/chat/completions" 20 | 21 | def call_o1_api( 22 | self, 23 | system_prompt: str, 24 | data: dict, 25 | api_key: str | None = None, 26 | ) -> str: 27 | """ 28 | Makes an API call to OpenAI o1-mini and returns the response. 29 | 30 | Args: 31 | system_prompt (str): The instruction/system prompt 32 | data (dict): Dictionary of variables to format into the user message 33 | api_key (str | None): Optional custom API key 34 | 35 | Returns: 36 | str: o1-mini's response text 37 | """ 38 | # Create the user message with the data 39 | user_message = format_user_message(data) 40 | 41 | # Use custom client if API key provided, otherwise use default 42 | client = OpenAI(api_key=api_key) if api_key else self.default_client 43 | 44 | try: 45 | print( 46 | f"Making non-streaming API call to o1-mini with API key: {'custom key' if api_key else 'default key'}" 47 | ) 48 | 49 | completion = client.chat.completions.create( 50 | model="o1-mini", 51 | messages=[ 52 | {"role": "system", "content": system_prompt}, 53 | {"role": "user", "content": user_message}, 54 | ], 55 | max_completion_tokens=12000, # Adjust as needed 56 | temperature=0.2, 57 | ) 58 | 59 | print("API call completed successfully") 60 | 61 | if completion.choices[0].message.content is None: 62 | raise ValueError("No content returned from OpenAI o1-mini") 63 | 64 | return completion.choices[0].message.content 65 | 66 | except Exception as e: 67 | print(f"Error in OpenAI o1-mini API call: {str(e)}") 68 | raise 69 | 70 | async def call_o1_api_stream( 71 | self, 72 | system_prompt: str, 73 | data: dict, 74 | api_key: str | None = None, 75 | ) -> AsyncGenerator[str, None]: 76 | """ 77 | Makes a streaming API call to OpenAI o1-mini and yields the responses. 78 | 79 | Args: 80 | system_prompt (str): The instruction/system prompt 81 | data (dict): Dictionary of variables to format into the user message 82 | api_key (str | None): Optional custom API key 83 | 84 | Yields: 85 | str: Chunks of o1-mini's response text 86 | """ 87 | # Create the user message with the data 88 | user_message = format_user_message(data) 89 | 90 | headers = { 91 | "Content-Type": "application/json", 92 | "Authorization": f"Bearer {api_key or self.default_client.api_key}", 93 | } 94 | 95 | payload = { 96 | "model": "o1-mini", 97 | "messages": [ 98 | { 99 | "role": "user", 100 | "content": f""" 101 | 102 | {system_prompt} 103 | 104 | 105 | {user_message} 106 | 107 | """, 108 | }, 109 | ], 110 | "max_completion_tokens": 12000, 111 | "stream": True, 112 | } 113 | 114 | try: 115 | async with aiohttp.ClientSession() as session: 116 | async with session.post( 117 | self.base_url, headers=headers, json=payload 118 | ) as response: 119 | 120 | if response.status != 200: 121 | error_text = await response.text() 122 | print(f"Error response: {error_text}") 123 | raise ValueError( 124 | f"OpenAI API returned status code {response.status}: {error_text}" 125 | ) 126 | 127 | line_count = 0 128 | async for line in response.content: 129 | line = line.decode("utf-8").strip() 130 | if not line: 131 | continue 132 | 133 | line_count += 1 134 | 135 | if line.startswith("data: "): 136 | if line == "data: [DONE]": 137 | break 138 | try: 139 | data = json.loads(line[6:]) 140 | content = ( 141 | data.get("choices", [{}])[0] 142 | .get("delta", {}) 143 | .get("content") 144 | ) 145 | if content: 146 | yield content 147 | except json.JSONDecodeError as e: 148 | print(f"JSON decode error: {e} for line: {line}") 149 | continue 150 | 151 | if line_count == 0: 152 | print("Warning: No lines received in stream response") 153 | 154 | except aiohttp.ClientError as e: 155 | print(f"Connection error: {str(e)}") 156 | raise ValueError(f"Failed to connect to OpenAI API: {str(e)}") 157 | except Exception as e: 158 | print(f"Unexpected error in streaming API call: {str(e)}") 159 | raise 160 | 161 | def count_tokens(self, prompt: str) -> int: 162 | """ 163 | Counts the number of tokens in a prompt. 164 | 165 | Args: 166 | prompt (str): The prompt to count tokens for 167 | 168 | Returns: 169 | int: Estimated number of input tokens 170 | """ 171 | num_tokens = len(self.encoding.encode(prompt)) 172 | return num_tokens 173 | -------------------------------------------------------------------------------- /backend/app/services/o3_mini_openai_service.py: -------------------------------------------------------------------------------- 1 | from openai import OpenAI 2 | from dotenv import load_dotenv 3 | from app.utils.format_message import format_user_message 4 | import tiktoken 5 | import os 6 | import aiohttp 7 | import json 8 | from typing import AsyncGenerator, Literal 9 | 10 | load_dotenv() 11 | 12 | 13 | class OpenAIo3Service: 14 | def __init__(self): 15 | self.default_client = OpenAI( 16 | api_key=os.getenv("OPENAI_API_KEY"), 17 | ) 18 | self.encoding = tiktoken.get_encoding("o200k_base") # Encoder for OpenAI models 19 | self.base_url = "https://api.openai.com/v1/chat/completions" 20 | 21 | def call_o3_api( 22 | self, 23 | system_prompt: str, 24 | data: dict, 25 | api_key: str | None = None, 26 | reasoning_effort: Literal["low", "medium", "high"] = "low", 27 | ) -> str: 28 | """ 29 | Makes an API call to OpenAI o3-mini and returns the response. 30 | 31 | Args: 32 | system_prompt (str): The instruction/system prompt 33 | data (dict): Dictionary of variables to format into the user message 34 | api_key (str | None): Optional custom API key 35 | 36 | Returns: 37 | str: o3-mini's response text 38 | """ 39 | # Create the user message with the data 40 | user_message = format_user_message(data) 41 | 42 | # Use custom client if API key provided, otherwise use default 43 | client = OpenAI(api_key=api_key) if api_key else self.default_client 44 | 45 | try: 46 | print( 47 | f"Making non-streaming API call to o3-mini with API key: {'custom key' if api_key else 'default key'}" 48 | ) 49 | 50 | completion = client.chat.completions.create( 51 | model="o3-mini", 52 | messages=[ 53 | {"role": "system", "content": system_prompt}, 54 | {"role": "user", "content": user_message}, 55 | ], 56 | max_completion_tokens=12000, # Adjust as needed 57 | temperature=0.2, 58 | reasoning_effort=reasoning_effort, 59 | ) 60 | 61 | print("API call completed successfully") 62 | 63 | if completion.choices[0].message.content is None: 64 | raise ValueError("No content returned from OpenAI o3-mini") 65 | 66 | return completion.choices[0].message.content 67 | 68 | except Exception as e: 69 | print(f"Error in OpenAI o3-mini API call: {str(e)}") 70 | raise 71 | 72 | async def call_o3_api_stream( 73 | self, 74 | system_prompt: str, 75 | data: dict, 76 | api_key: str | None = None, 77 | reasoning_effort: Literal["low", "medium", "high"] = "low", 78 | ) -> AsyncGenerator[str, None]: 79 | """ 80 | Makes a streaming API call to OpenAI o3-mini and yields the responses. 81 | 82 | Args: 83 | system_prompt (str): The instruction/system prompt 84 | data (dict): Dictionary of variables to format into the user message 85 | api_key (str | None): Optional custom API key 86 | 87 | Yields: 88 | str: Chunks of o3-mini's response text 89 | """ 90 | # Create the user message with the data 91 | user_message = format_user_message(data) 92 | 93 | headers = { 94 | "Content-Type": "application/json", 95 | "Authorization": f"Bearer {api_key or self.default_client.api_key}", 96 | } 97 | 98 | payload = { 99 | "model": "o3-mini", 100 | "messages": [ 101 | {"role": "system", "content": system_prompt}, 102 | {"role": "user", "content": user_message}, 103 | ], 104 | "max_completion_tokens": 12000, 105 | "stream": True, 106 | "reasoning_effort": reasoning_effort, 107 | } 108 | 109 | try: 110 | async with aiohttp.ClientSession() as session: 111 | async with session.post( 112 | self.base_url, headers=headers, json=payload 113 | ) as response: 114 | 115 | if response.status != 200: 116 | error_text = await response.text() 117 | print(f"Error response: {error_text}") 118 | raise ValueError( 119 | f"OpenAI API returned status code {response.status}: {error_text}" 120 | ) 121 | 122 | line_count = 0 123 | async for line in response.content: 124 | line = line.decode("utf-8").strip() 125 | if not line: 126 | continue 127 | 128 | line_count += 1 129 | 130 | if line.startswith("data: "): 131 | if line == "data: [DONE]": 132 | break 133 | try: 134 | data = json.loads(line[6:]) 135 | content = ( 136 | data.get("choices", [{}])[0] 137 | .get("delta", {}) 138 | .get("content") 139 | ) 140 | if content: 141 | yield content 142 | except json.JSONDecodeError as e: 143 | print(f"JSON decode error: {e} for line: {line}") 144 | continue 145 | 146 | if line_count == 0: 147 | print("Warning: No lines received in stream response") 148 | 149 | except aiohttp.ClientError as e: 150 | print(f"Connection error: {str(e)}") 151 | raise ValueError(f"Failed to connect to OpenAI API: {str(e)}") 152 | except Exception as e: 153 | print(f"Unexpected error in streaming API call: {str(e)}") 154 | raise 155 | 156 | def count_tokens(self, prompt: str) -> int: 157 | """ 158 | Counts the number of tokens in a prompt. 159 | 160 | Args: 161 | prompt (str): The prompt to count tokens for 162 | 163 | Returns: 164 | int: Estimated number of input tokens 165 | """ 166 | num_tokens = len(self.encoding.encode(prompt)) 167 | return num_tokens 168 | -------------------------------------------------------------------------------- /backend/app/services/o3_mini_openrouter_service.py: -------------------------------------------------------------------------------- 1 | from openai import OpenAI 2 | from dotenv import load_dotenv 3 | from app.utils.format_message import format_user_message 4 | import tiktoken 5 | import os 6 | import aiohttp 7 | import json 8 | from typing import Literal, AsyncGenerator 9 | 10 | load_dotenv() 11 | 12 | 13 | class OpenRouterO3Service: 14 | def __init__(self): 15 | self.default_client = OpenAI( 16 | base_url="https://openrouter.ai/api/v1", 17 | api_key=os.getenv("OPENROUTER_API_KEY"), 18 | ) 19 | self.encoding = tiktoken.get_encoding("o200k_base") 20 | self.base_url = "https://openrouter.ai/api/v1/chat/completions" 21 | 22 | def call_o3_api( 23 | self, 24 | system_prompt: str, 25 | data: dict, 26 | api_key: str | None = None, 27 | reasoning_effort: Literal["low", "medium", "high"] = "low", 28 | ) -> str: 29 | """ 30 | Makes an API call to OpenRouter O3 and returns the response. 31 | 32 | Args: 33 | system_prompt (str): The instruction/system prompt 34 | data (dict): Dictionary of variables to format into the user message 35 | api_key (str | None): Optional custom API key 36 | 37 | Returns: 38 | str: O3's response text 39 | """ 40 | # Create the user message with the data 41 | user_message = format_user_message(data) 42 | 43 | # Use custom client if API key provided, otherwise use default 44 | client = ( 45 | OpenAI(base_url="https://openrouter.ai/api/v1", api_key=api_key) 46 | if api_key 47 | else self.default_client 48 | ) 49 | 50 | completion = client.chat.completions.create( 51 | extra_headers={ 52 | "HTTP-Referer": "https://solidityVisualizer.com", # Optional. Site URL for rankings on openrouter.ai. 53 | "X-Title": "solidityVisualizer", # Optional. Site title for rankings on openrouter.ai. 54 | }, 55 | model="openai/o3-mini", # Can be configured as needed 56 | reasoning_effort=reasoning_effort, # Can be adjusted based on needs 57 | messages=[ 58 | {"role": "system", "content": system_prompt}, 59 | {"role": "user", "content": user_message}, 60 | ], 61 | max_completion_tokens=12000, # Adjust as needed 62 | temperature=0.2, 63 | ) 64 | 65 | if completion.choices[0].message.content is None: 66 | raise ValueError("No content returned from OpenRouter O3") 67 | 68 | return completion.choices[0].message.content 69 | 70 | async def call_o3_api_stream( 71 | self, 72 | system_prompt: str, 73 | data: dict, 74 | api_key: str | None = None, 75 | reasoning_effort: Literal["low", "medium", "high"] = "low", 76 | ) -> AsyncGenerator[str, None]: 77 | """ 78 | Makes a streaming API call to OpenRouter O3 and yields the responses. 79 | 80 | Args: 81 | system_prompt (str): The instruction/system prompt 82 | data (dict): Dictionary of variables to format into the user message 83 | api_key (str | None): Optional custom API key 84 | 85 | Yields: 86 | str: Chunks of O3's response text 87 | """ 88 | # Create the user message with the data 89 | user_message = format_user_message(data) 90 | 91 | headers = { 92 | "HTTP-Referer": "https://solidityVisualizer.com", 93 | "X-Title": "solidityVisualizer", 94 | "Authorization": f"Bearer {api_key or self.default_client.api_key}", 95 | "Content-Type": "application/json", 96 | } 97 | 98 | payload = { 99 | "model": "openai/o3-mini", 100 | "messages": [ 101 | {"role": "system", "content": system_prompt}, 102 | {"role": "user", "content": user_message}, 103 | ], 104 | "max_tokens": 12000, 105 | "temperature": 0.2, 106 | "stream": True, 107 | "reasoning_effort": reasoning_effort, 108 | } 109 | 110 | buffer = "" 111 | async with aiohttp.ClientSession() as session: 112 | async with session.post( 113 | self.base_url, headers=headers, json=payload 114 | ) as response: 115 | async for line in response.content: 116 | line = line.decode("utf-8").strip() 117 | if line.startswith("data: "): 118 | if line == "data: [DONE]": 119 | break 120 | try: 121 | data = json.loads(line[6:]) 122 | if ( 123 | content := data.get("choices", [{}])[0] 124 | .get("delta", {}) 125 | .get("content") 126 | ): 127 | yield content 128 | except json.JSONDecodeError: 129 | # Skip any non-JSON lines (like the OPENROUTER PROCESSING comments) 130 | continue 131 | 132 | def count_tokens(self, prompt: str) -> int: 133 | """ 134 | Counts the number of tokens in a prompt. 135 | Note: This is a rough estimate as OpenRouter may not provide direct token counting. 136 | 137 | Args: 138 | prompt (str): The prompt to count tokens for 139 | 140 | Returns: 141 | int: Estimated number of input tokens 142 | """ 143 | num_tokens = len(self.encoding.encode(prompt)) 144 | return num_tokens 145 | -------------------------------------------------------------------------------- /backend/app/utils/format_message.py: -------------------------------------------------------------------------------- 1 | def format_user_message(data: dict[str, str]) -> str: 2 | """ 3 | Formats a dictionary of data into a structured user message with XML-style tags. 4 | 5 | Args: 6 | data (dict[str, str]): Dictionary of key-value pairs to format 7 | 8 | Returns: 9 | str: Formatted message with each key-value pair wrapped in appropriate tags 10 | """ 11 | parts = [] 12 | for key, value in data.items(): 13 | # Map keys to their XML-style tags 14 | if key == "file_tree": 15 | parts.append(f"\n{value}\n") 16 | elif key == "readme": 17 | parts.append(f"\n{value}\n") 18 | elif key == "explanation": 19 | parts.append(f"\n{value}\n") 20 | elif key == "component_mapping": 21 | parts.append(f"\n{value}\n") 22 | elif key == "instructions": 23 | parts.append(f"\n{value}\n") 24 | elif key == "diagram": 25 | parts.append(f"\n{value}\n") 26 | 27 | return "\n\n".join(parts) 28 | -------------------------------------------------------------------------------- /backend/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Exit on any error 4 | set -e 5 | 6 | # Navigate to project directory (one level up from backend) 7 | cd .. 8 | 9 | # Pull latest changes 10 | git pull origin main 11 | 12 | # Build and restart containers with production environment 13 | docker-compose down 14 | ENVIRONMENT=production docker-compose up --build -d 15 | 16 | # Remove unused images 17 | docker image prune -f 18 | 19 | # Show logs only if --logs flag is passed 20 | if [ "$1" == "--logs" ]; then 21 | docker-compose logs -f 22 | else 23 | echo "Deployment complete! Run 'docker-compose logs -f' to view logs" 24 | fi -------------------------------------------------------------------------------- /backend/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Current ENVIRONMENT: $ENVIRONMENT" 4 | 5 | if [ "$ENVIRONMENT" = "development" ]; then 6 | echo "Starting in development mode with hot reload..." 7 | exec uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload 8 | elif [ "$ENVIRONMENT" = "production" ]; then 9 | echo "Starting in production mode with multiple workers..." 10 | exec uvicorn app.main:app \ 11 | --host 0.0.0.0 \ 12 | --port 8000 \ 13 | --timeout-keep-alive 300 \ 14 | --workers 2 \ 15 | --loop uvloop \ 16 | --http httptools 17 | else 18 | echo "ENVIRONMENT must be set to either 'development' or 'production'" 19 | exit 1 20 | fi -------------------------------------------------------------------------------- /backend/nginx/api.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name api.solidityvisualizer.com; 4 | 5 | # Redirect HTTP to HTTPS 6 | location / { 7 | return 301 https://$host$request_uri; 8 | } 9 | } 10 | 11 | server { 12 | listen 443 ssl; 13 | server_name api.solidityvisualizer.com; 14 | 15 | ssl_certificate /etc/letsencrypt/live/api.solidityvisualizer.com/fullchain.pem; 16 | ssl_certificate_key /etc/letsencrypt/live/api.solidityvisualizer.com/privkey.pem; 17 | 18 | location / { 19 | proxy_pass http://localhost:8000; 20 | proxy_set_header Host $host; 21 | proxy_set_header X-Real-IP $remote_addr; 22 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 23 | proxy_set_header X-Forwarded-Proto $scheme; 24 | 25 | # Required headers for SSE (Server-Sent Events) 26 | proxy_buffering off; 27 | proxy_cache off; 28 | proxy_set_header Connection ''; 29 | proxy_http_version 1.1; 30 | } 31 | } -------------------------------------------------------------------------------- /backend/nginx/setup_nginx.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Exit on any error 4 | set -e 5 | 6 | # Check if running as root 7 | if [ "$EUID" -ne 0 ]; then 8 | echo "Please run as root or with sudo" 9 | exit 1 10 | fi 11 | 12 | # Copy Nginx configuration 13 | echo "Copying Nginx configuration..." 14 | cp "$(dirname "$0")/api.conf" /etc/nginx/sites-available/api 15 | ln -sf /etc/nginx/sites-available/api /etc/nginx/sites-enabled/ 16 | 17 | # Test Nginx configuration 18 | echo "Testing Nginx configuration..." 19 | nginx -t 20 | 21 | # Reload Nginx 22 | echo "Reloading Nginx..." 23 | systemctl reload nginx 24 | 25 | echo "Nginx configuration updated successfully!" -------------------------------------------------------------------------------- /backend/requirements.txt: -------------------------------------------------------------------------------- 1 | aiohappyeyeballs==2.4.6 2 | aiohttp==3.11.12 3 | aiosignal==1.3.2 4 | annotated-types==0.7.0 5 | anthropic==0.42.0 6 | anyio==4.7.0 7 | api-analytics==1.2.5 8 | attrs==25.1.0 9 | certifi==2024.12.14 10 | cffi==1.17.1 11 | charset-normalizer==3.4.0 12 | click==8.1.7 13 | cryptography==44.0.0 14 | Deprecated==1.2.15 15 | distro==1.9.0 16 | dnspython==2.7.0 17 | email_validator==2.2.0 18 | fastapi==0.115.6 19 | fastapi-cli==0.0.6 20 | frozenlist==1.5.0 21 | h11==0.14.0 22 | httpcore==1.0.7 23 | httptools==0.6.4 24 | httpx==0.28.1 25 | idna==3.10 26 | Jinja2==3.1.4 27 | jiter==0.8.2 28 | limits==3.14.1 29 | markdown-it-py==3.0.0 30 | MarkupSafe==3.0.2 31 | mdurl==0.1.2 32 | multidict==6.1.0 33 | openai==1.61.1 34 | packaging==24.2 35 | propcache==0.2.1 36 | pycparser==2.22 37 | pydantic==2.10.3 38 | pydantic_core==2.27.1 39 | Pygments==2.18.0 40 | PyJWT==2.10.1 41 | python-dotenv==1.0.1 42 | python-multipart==0.0.19 43 | PyYAML==6.0.2 44 | regex==2024.11.6 45 | requests==2.32.3 46 | rich==13.9.4 47 | rich-toolkit==0.12.0 48 | shellingham==1.5.4 49 | slowapi==0.1.9 50 | sniffio==1.3.1 51 | starlette==0.41.3 52 | tiktoken==0.8.0 53 | tqdm==4.67.1 54 | typer==0.15.1 55 | typing_extensions==4.12.2 56 | urllib3==2.2.3 57 | uvicorn==0.34.0 58 | uvloop==0.21.0 59 | watchfiles==1.0.3 60 | websockets==14.1 61 | wrapt==1.17.0 62 | yarl==1.18.3 63 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/styles/globals.css", 9 | "baseColor": "neutral", 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 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | api: 3 | build: 4 | context: ./backend 5 | dockerfile: Dockerfile 6 | ports: 7 | - "8000:8000" 8 | volumes: 9 | - ./backend:/app 10 | env_file: 11 | - .env 12 | environment: 13 | - ENVIRONMENT=${ENVIRONMENT:-development} # Default to development if not set 14 | restart: unless-stopped 15 | -------------------------------------------------------------------------------- /docs/readme_img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VGabriel45/SolidityVisualizer/1c41ac49c6a09ec7cf721b3a94b929c607121239/docs/readme_img.png -------------------------------------------------------------------------------- /drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import { type Config } from "drizzle-kit"; 2 | 3 | import { env } from "~/env"; 4 | 5 | export default { 6 | schema: "./src/server/db/schema.ts", 7 | dialect: "postgresql", 8 | dbCredentials: { 9 | url: env.POSTGRES_URL, 10 | }, 11 | tablesFilter: ["solidityVisualizer_*"], 12 | } satisfies Config; 13 | -------------------------------------------------------------------------------- /drizzle/0000_jittery_robin_chapel.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS "solidityAnalyzer_diagram_cache" ( 2 | "username" varchar(256) NOT NULL, 3 | "repo" varchar(256) NOT NULL, 4 | "diagram" varchar(10000) NOT NULL, 5 | "explanation" varchar(10000) DEFAULT 'No explanation provided' NOT NULL, 6 | "created_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, 7 | "updated_at" timestamp with time zone, 8 | "used_own_key" boolean DEFAULT false, 9 | CONSTRAINT "solidityAnalyzer_diagram_cache_username_repo_pk" PRIMARY KEY("username","repo") 10 | ); 11 | -------------------------------------------------------------------------------- /drizzle/0001_lush_mercury.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS "solidityVisualizer_diagram_cache" ( 2 | "username" varchar(256) NOT NULL, 3 | "repo" varchar(256) NOT NULL, 4 | "diagram" varchar(10000) NOT NULL, 5 | "explanation" varchar(10000) DEFAULT 'No explanation provided' NOT NULL, 6 | "created_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, 7 | "updated_at" timestamp with time zone, 8 | "used_own_key" boolean DEFAULT false, 9 | CONSTRAINT "solidityVisualizer_diagram_cache_username_repo_pk" PRIMARY KEY("username","repo") 10 | ); 11 | --> statement-breakpoint 12 | DROP TABLE "solidityAnalyzer_diagram_cache"; -------------------------------------------------------------------------------- /drizzle/meta/0000_snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "f73259e5-dd6b-4a21-bac5-7a0e012f11df", 3 | "prevId": "00000000-0000-0000-0000-000000000000", 4 | "version": "7", 5 | "dialect": "postgresql", 6 | "tables": { 7 | "public.solidityAnalyzer_diagram_cache": { 8 | "name": "solidityAnalyzer_diagram_cache", 9 | "schema": "", 10 | "columns": { 11 | "username": { 12 | "name": "username", 13 | "type": "varchar(256)", 14 | "primaryKey": false, 15 | "notNull": true 16 | }, 17 | "repo": { 18 | "name": "repo", 19 | "type": "varchar(256)", 20 | "primaryKey": false, 21 | "notNull": true 22 | }, 23 | "diagram": { 24 | "name": "diagram", 25 | "type": "varchar(10000)", 26 | "primaryKey": false, 27 | "notNull": true 28 | }, 29 | "explanation": { 30 | "name": "explanation", 31 | "type": "varchar(10000)", 32 | "primaryKey": false, 33 | "notNull": true, 34 | "default": "'No explanation provided'" 35 | }, 36 | "created_at": { 37 | "name": "created_at", 38 | "type": "timestamp with time zone", 39 | "primaryKey": false, 40 | "notNull": true, 41 | "default": "CURRENT_TIMESTAMP" 42 | }, 43 | "updated_at": { 44 | "name": "updated_at", 45 | "type": "timestamp with time zone", 46 | "primaryKey": false, 47 | "notNull": false 48 | }, 49 | "used_own_key": { 50 | "name": "used_own_key", 51 | "type": "boolean", 52 | "primaryKey": false, 53 | "notNull": false, 54 | "default": false 55 | } 56 | }, 57 | "indexes": {}, 58 | "foreignKeys": {}, 59 | "compositePrimaryKeys": { 60 | "solidityAnalyzer_diagram_cache_username_repo_pk": { 61 | "name": "solidityAnalyzer_diagram_cache_username_repo_pk", 62 | "columns": [ 63 | "username", 64 | "repo" 65 | ] 66 | } 67 | }, 68 | "uniqueConstraints": {} 69 | } 70 | }, 71 | "enums": {}, 72 | "schemas": {}, 73 | "sequences": {}, 74 | "_meta": { 75 | "columns": {}, 76 | "schemas": {}, 77 | "tables": {} 78 | } 79 | } -------------------------------------------------------------------------------- /drizzle/meta/0001_snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "533450c3-c9b2-4918-9914-0788522b5486", 3 | "prevId": "f73259e5-dd6b-4a21-bac5-7a0e012f11df", 4 | "version": "7", 5 | "dialect": "postgresql", 6 | "tables": { 7 | "public.solidityVisualizer_diagram_cache": { 8 | "name": "solidityVisualizer_diagram_cache", 9 | "schema": "", 10 | "columns": { 11 | "username": { 12 | "name": "username", 13 | "type": "varchar(256)", 14 | "primaryKey": false, 15 | "notNull": true 16 | }, 17 | "repo": { 18 | "name": "repo", 19 | "type": "varchar(256)", 20 | "primaryKey": false, 21 | "notNull": true 22 | }, 23 | "diagram": { 24 | "name": "diagram", 25 | "type": "varchar(10000)", 26 | "primaryKey": false, 27 | "notNull": true 28 | }, 29 | "explanation": { 30 | "name": "explanation", 31 | "type": "varchar(10000)", 32 | "primaryKey": false, 33 | "notNull": true, 34 | "default": "'No explanation provided'" 35 | }, 36 | "created_at": { 37 | "name": "created_at", 38 | "type": "timestamp with time zone", 39 | "primaryKey": false, 40 | "notNull": true, 41 | "default": "CURRENT_TIMESTAMP" 42 | }, 43 | "updated_at": { 44 | "name": "updated_at", 45 | "type": "timestamp with time zone", 46 | "primaryKey": false, 47 | "notNull": false 48 | }, 49 | "used_own_key": { 50 | "name": "used_own_key", 51 | "type": "boolean", 52 | "primaryKey": false, 53 | "notNull": false, 54 | "default": false 55 | } 56 | }, 57 | "indexes": {}, 58 | "foreignKeys": {}, 59 | "compositePrimaryKeys": { 60 | "solidityVisualizer_diagram_cache_username_repo_pk": { 61 | "name": "solidityVisualizer_diagram_cache_username_repo_pk", 62 | "columns": [ 63 | "username", 64 | "repo" 65 | ] 66 | } 67 | }, 68 | "uniqueConstraints": {} 69 | } 70 | }, 71 | "enums": {}, 72 | "schemas": {}, 73 | "sequences": {}, 74 | "_meta": { 75 | "columns": {}, 76 | "schemas": {}, 77 | "tables": {} 78 | } 79 | } -------------------------------------------------------------------------------- /drizzle/meta/_journal.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "7", 3 | "dialect": "postgresql", 4 | "entries": [ 5 | { 6 | "idx": 0, 7 | "version": "7", 8 | "when": 1743445628031, 9 | "tag": "0000_jittery_robin_chapel", 10 | "breakpoints": true 11 | }, 12 | { 13 | "idx": 1, 14 | "version": "7", 15 | "when": 1743446339371, 16 | "tag": "0001_lush_mercury", 17 | "breakpoints": true 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially useful 3 | * for Docker builds. 4 | */ 5 | import "./src/env.js"; 6 | 7 | /** @type {import("next").NextConfig} */ 8 | const config = { 9 | reactStrictMode: false, 10 | experimental: { 11 | serverActions: { 12 | bodySizeLimit: '10mb' 13 | }, 14 | }, 15 | async rewrites() { 16 | return [ 17 | { 18 | source: "/ingest/static/:path*", 19 | destination: "https://us-assets.i.posthog.com/static/:path*", 20 | }, 21 | { 22 | source: "/ingest/:path*", 23 | destination: "https://us.i.posthog.com/:path*", 24 | }, 25 | { 26 | source: "/ingest/decide", 27 | destination: "https://us.i.posthog.com/decide", 28 | }, 29 | ]; 30 | }, 31 | // This is required to support PostHog trailing slash API requests 32 | skipTrailingSlashRedirect: true, 33 | }; 34 | 35 | export default config; 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "solidityVisualizer", 3 | "version": "0.1.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "build": "next build", 8 | "check": "next lint && tsc --noEmit", 9 | "db:generate": "drizzle-kit generate", 10 | "db:migrate": "drizzle-kit migrate", 11 | "db:push": "drizzle-kit push", 12 | "db:studio": "drizzle-kit studio", 13 | "dev": "next dev --turbo", 14 | "lint": "next lint", 15 | "lint:fix": "next lint --fix", 16 | "preview": "next build && next start", 17 | "start": "next start", 18 | "typecheck": "tsc --noEmit", 19 | "format:write": "prettier --write \"**/*.{ts,tsx,js,jsx,mdx}\" --cache", 20 | "format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,mdx}\" --cache" 21 | }, 22 | "dependencies": { 23 | "@neondatabase/serverless": "^0.10.4", 24 | "@radix-ui/react-dialog": "^1.1.4", 25 | "@radix-ui/react-progress": "^1.1.1", 26 | "@radix-ui/react-slot": "^1.1.1", 27 | "@radix-ui/react-switch": "^1.1.3", 28 | "@radix-ui/react-tooltip": "^1.1.6", 29 | "@t3-oss/env-nextjs": "^0.10.1", 30 | "@vercel/analytics": "^1.5.0", 31 | "class-variance-authority": "^0.7.1", 32 | "clsx": "^2.1.1", 33 | "dotenv": "^16.4.7", 34 | "drizzle-orm": "^0.33.0", 35 | "geist": "^1.3.0", 36 | "ldrs": "^1.0.2", 37 | "lucide-react": "^0.468.0", 38 | "mermaid": "^11.4.1", 39 | "next": "^15.0.1", 40 | "postgres": "^3.4.4", 41 | "posthog-js": "^1.203.1", 42 | "react": "^18.3.1", 43 | "react-dom": "^18.3.1", 44 | "react-icons": "^5.4.0", 45 | "svg-pan-zoom": "^3.6.2", 46 | "tailwind-merge": "^2.5.5", 47 | "tailwindcss-animate": "^1.0.7", 48 | "zod": "^3.23.3" 49 | }, 50 | "devDependencies": { 51 | "@types/eslint": "^8.56.10", 52 | "@types/node": "^20.14.10", 53 | "@types/react": "^18.3.3", 54 | "@types/react-dom": "^18.3.0", 55 | "@types/svg-pan-zoom": "^3.4.0", 56 | "@typescript-eslint/eslint-plugin": "^8.1.0", 57 | "@typescript-eslint/parser": "^8.1.0", 58 | "drizzle-kit": "^0.24.0", 59 | "eslint": "^8.57.0", 60 | "eslint-config-next": "^15.0.1", 61 | "eslint-plugin-drizzle": "^0.2.3", 62 | "postcss": "^8.4.39", 63 | "prettier": "^3.3.2", 64 | "prettier-plugin-tailwindcss": "^0.6.5", 65 | "tailwind-scrollbar": "^4.0.0", 66 | "tailwindcss": "^3.4.3", 67 | "typescript": "^5.5.3" 68 | }, 69 | "ct3aMetadata": { 70 | "initVersion": "7.38.1" 71 | }, 72 | "packageManager": "pnpm@9.13.0" 73 | } 74 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('prettier').Config & import('prettier-plugin-tailwindcss').PluginOptions} */ 2 | export default { 3 | plugins: ["prettier-plugin-tailwindcss"], 4 | }; 5 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VGabriel45/SolidityVisualizer/1c41ac49c6a09ec7cf721b3a94b929c607121239/public/favicon.ico -------------------------------------------------------------------------------- /src/app/[username]/[repo]/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useParams } from "next/navigation"; 4 | import MainCard from "~/components/main-card"; 5 | import Loading from "~/components/loading"; 6 | import MermaidChart from "~/components/mermaid-diagram"; 7 | import { useDiagram } from "~/hooks/useDiagram"; 8 | import { ApiKeyDialog } from "~/components/api-key-dialog"; 9 | import { ApiKeyButton } from "~/components/api-key-button"; 10 | import { useState } from "react"; 11 | 12 | export default function Repo() { 13 | const [zoomingEnabled, setZoomingEnabled] = useState(false); 14 | const params = useParams<{ username: string; repo: string }>(); 15 | const { 16 | diagram, 17 | error, 18 | loading, 19 | lastGenerated, 20 | cost, 21 | showApiKeyDialog, 22 | handleModify, 23 | handleRegenerate, 24 | handleCopy, 25 | handleApiKeySubmit, 26 | handleCloseApiKeyDialog, 27 | handleOpenApiKeyDialog, 28 | handleExportImage, 29 | state, 30 | } = useDiagram(params.username.toLowerCase(), params.repo.toLowerCase()); 31 | 32 | return ( 33 |
34 |
35 | setZoomingEnabled(!zoomingEnabled)} 47 | loading={loading} 48 | /> 49 |
50 |
51 | {loading ? ( 52 | 59 | ) : error || state.error ? ( 60 |
61 |

62 | {error || state.error} 63 |

64 | {(error?.includes("API key") || 65 | state.error?.includes("API key")) && ( 66 |
67 | 68 |
69 | )} 70 |
71 | ) : ( 72 |
73 | 74 |
75 | )} 76 |
77 | 78 | 83 |
84 | ); 85 | } 86 | -------------------------------------------------------------------------------- /src/app/_actions/cache.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { db } from "~/server/db"; 4 | import { eq, and } from "drizzle-orm"; 5 | import { diagramCache } from "~/server/db/schema"; 6 | import { sql } from "drizzle-orm"; 7 | 8 | export async function testDatabaseConnection() { 9 | 'use server'; 10 | try { 11 | console.log('SERVER: Testing database connection...'); 12 | const result = await db.execute(sql`SELECT 1 as test`); 13 | console.log('SERVER: Database connection successful:', result); 14 | return true; 15 | } catch (error) { 16 | console.error('SERVER: Database connection failed:', error); 17 | return false; 18 | } 19 | } 20 | 21 | export async function getCachedDiagram(username: string, repo: string) { 22 | 'use server'; 23 | try { 24 | console.log('SERVER: Fetching cached diagram for:', { username, repo }); 25 | const cached = await db 26 | .select() 27 | .from(diagramCache) 28 | .where( 29 | and(eq(diagramCache.username, username), eq(diagramCache.repo, repo)), 30 | ) 31 | .limit(1); 32 | 33 | return cached[0]?.diagram ?? null; 34 | } catch (error) { 35 | console.error('SERVER: Error fetching cached diagram:', error); 36 | return null; 37 | } 38 | } 39 | 40 | export async function getCachedExplanation(username: string, repo: string) { 41 | try { 42 | const cached = await db 43 | .select() 44 | .from(diagramCache) 45 | .where( 46 | and(eq(diagramCache.username, username), eq(diagramCache.repo, repo)), 47 | ) 48 | .limit(1); 49 | 50 | return cached[0]?.explanation ?? null; 51 | } catch (error) { 52 | console.error("Error fetching cached explanation:", error); 53 | return null; 54 | } 55 | } 56 | 57 | export async function cacheDiagramAndExplanation( 58 | username: string, 59 | repo: string, 60 | diagram: string, 61 | explanation: string, 62 | usedOwnKey = false, 63 | ) { 64 | 'use server'; 65 | console.log('SERVER: Starting cache operation'); 66 | try { 67 | console.log('SERVER: Attempting to cache diagram:', { 68 | username, 69 | repo, 70 | usedOwnKey, 71 | diagramLength: diagram.length, 72 | explanationLength: explanation.length 73 | }); 74 | 75 | const isConnected = await testDatabaseConnection(); 76 | if (!isConnected) { 77 | throw new Error('Database connection failed'); 78 | } 79 | 80 | await db 81 | .insert(diagramCache) 82 | .values({ 83 | username, 84 | repo, 85 | diagram, 86 | explanation, 87 | usedOwnKey, 88 | }) 89 | .onConflictDoUpdate({ 90 | target: [diagramCache.username, diagramCache.repo], 91 | set: { 92 | diagram, 93 | explanation, 94 | usedOwnKey, 95 | updatedAt: new Date(), 96 | }, 97 | }); 98 | console.log('SERVER: Successfully cached diagram'); 99 | return true; 100 | } catch (error) { 101 | console.error('SERVER: Error caching diagram:', error); 102 | console.error('SERVER: Database URL:', process.env.POSTGRES_URL?.replace(/:[^:@]*@/, ':****@')); 103 | throw error; 104 | } 105 | } 106 | 107 | export async function getDiagramStats() { 108 | try { 109 | const stats = await db 110 | .select({ 111 | totalDiagrams: sql`COUNT(*)`, 112 | ownKeyUsers: sql`COUNT(CASE WHEN ${diagramCache.usedOwnKey} = true THEN 1 END)`, 113 | freeUsers: sql`COUNT(CASE WHEN ${diagramCache.usedOwnKey} = false THEN 1 END)`, 114 | }) 115 | .from(diagramCache); 116 | 117 | return stats[0]; 118 | } catch (error) { 119 | console.error("Error getting diagram stats:", error); 120 | return null; 121 | } 122 | } 123 | 124 | // Test database connection on module load 125 | console.log('SERVER: Testing database connection on module load'); 126 | void testDatabaseConnection(); 127 | -------------------------------------------------------------------------------- /src/app/_actions/github.ts: -------------------------------------------------------------------------------- 1 | import { cache } from "react"; 2 | 3 | interface GitHubResponse { 4 | stargazers_count: number; 5 | } 6 | 7 | export const getStarCount = cache(async () => { 8 | try { 9 | const response = await fetch( 10 | "https://api.github.com/repos/VGabriel45/SolidityVisualizer", 11 | { 12 | headers: { 13 | Accept: "application/vnd.github.v3+json", 14 | }, 15 | next: { 16 | revalidate: 300, // Cache for 5 minutes 17 | }, 18 | }, 19 | ); 20 | 21 | if (!response.ok) { 22 | throw new Error("Failed to fetch star count"); 23 | } 24 | 25 | const data = (await response.json()) as GitHubResponse; 26 | return data.stargazers_count; 27 | } catch (error) { 28 | console.error("Error fetching star count:", error); 29 | return null; 30 | } 31 | }); 32 | -------------------------------------------------------------------------------- /src/app/_actions/repo.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { db } from "~/server/db"; 4 | import { eq, and } from "drizzle-orm"; 5 | import { diagramCache } from "~/server/db/schema"; 6 | 7 | export async function getLastGeneratedDate(username: string, repo: string) { 8 | const result = await db 9 | .select() 10 | .from(diagramCache) 11 | .where( 12 | and(eq(diagramCache.username, username), eq(diagramCache.repo, repo)), 13 | ); 14 | 15 | return result[0]?.updatedAt; 16 | } 17 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import "~/styles/globals.css"; 2 | 3 | import { GeistSans } from "geist/font/sans"; 4 | import { type Metadata } from "next"; 5 | import { Header } from "~/components/header"; 6 | import { Footer } from "~/components/footer"; 7 | import { CSPostHogProvider } from "./providers"; 8 | import { Analytics } from "@vercel/analytics/react" 9 | 10 | export const metadata: Metadata = { 11 | title: "Solidity Visualizer", 12 | description: 13 | "Turn any Solidity smart contract repository into an interactive diagram for visualization in seconds.", 14 | metadataBase: new URL("https://solidityVisualizer.com"), 15 | keywords: [ 16 | "solidity", 17 | "smart contracts", 18 | "ethereum", 19 | "blockchain", 20 | "smart contract diagram", 21 | "solidity diagram generator", 22 | "solidity diagram tool", 23 | "solidity diagram maker", 24 | "solidity diagram creator", 25 | "diagram", 26 | "repository", 27 | "visualization", 28 | "code structure", 29 | "system design", 30 | "smart contract architecture", 31 | "smart contract design", 32 | "blockchain engineering", 33 | "blockchain development", 34 | "defi", 35 | "web3", 36 | "open source", 37 | "open source software", 38 | "solidity visualizer", 39 | ], 40 | authors: [ 41 | { name: "Ahmed Khaleel", url: "https://github.com/VGabriel45" }, 42 | ], 43 | creator: "Ahmed Khaleel", 44 | openGraph: { 45 | type: "website", 46 | locale: "en_US", 47 | url: "https://solidityVisualizer.com", 48 | title: "Solidity Visualizer - Smart Contract Diagrams in Seconds", 49 | description: 50 | "Turn any Solidity smart contract repository into an interactive diagram for visualization.", 51 | siteName: "Solidity Visualizer", 52 | images: [ 53 | { 54 | url: "/og-image.png", 55 | width: 1200, 56 | height: 630, 57 | alt: "Solidity Visualizer - Smart Contract Visualization Tool", 58 | }, 59 | ], 60 | }, 61 | robots: { 62 | index: true, 63 | follow: true, 64 | googleBot: { 65 | index: true, 66 | follow: true, 67 | "max-snippet": -1, 68 | }, 69 | }, 70 | }; 71 | 72 | export default function RootLayout({ 73 | children, 74 | }: Readonly<{ children: React.ReactNode }>) { 75 | return ( 76 | 77 | 78 | 79 |
80 |
{children}
81 |