From c3b1c05b01d7c336d435a8e7952827ff96745e69 Mon Sep 17 00:00:00 2001 From: Tian1996 <40369357+Tian1996@users.noreply.github.com> Date: Mon, 15 Jun 2026 13:37:08 +0800 Subject: [PATCH 1/7] Support natural language devcontainer prompts --- main.py | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/main.py b/main.py index 2e6410d..4587307 100644 --- a/main.py +++ b/main.py @@ -1,13 +1,15 @@ import logging import os import json +from urllib.parse import quote_plus from datetime import datetime from fasthtml.common import * from dotenv import load_dotenv from supabase_client import supabase from helpers.openai_helpers import setup_azure_openai, setup_instructor -from helpers.github_helpers import fetch_repo_context, check_url_exists +from helpers.github_helpers import fetch_repo_context, check_url_exists, is_valid_github_url +from helpers.natural_language_helpers import build_project_description_context, project_description_cache_key from helpers.devcontainer_helpers import generate_devcontainer_json, validate_devcontainer_json from helpers.token_helpers import count_tokens, truncate_to_token_limit from models import DevContainer @@ -105,19 +107,27 @@ async def get(): async def post(repo_url: str, regenerate: bool = False): logging.info(f"Generating devcontainer.json for: {repo_url}") - # Normalize the repo_url by stripping trailing slashes - repo_url = repo_url.rstrip('/') - try: - exists, existing_record = check_url_exists(repo_url) + project_input = repo_url.strip() + if is_valid_github_url(project_input): + repo_url = project_input.rstrip('/') + cache_key = repo_url + repo_context, existing_devcontainer, devcontainer_url = fetch_repo_context(repo_url) + logging.info(f"Fetched repo context. Existing devcontainer: {'Yes' if existing_devcontainer else 'No'}") + logging.info(f"Devcontainer URL: {devcontainer_url}") + else: + repo_url = project_input + cache_key = project_description_cache_key(project_input) + repo_context = build_project_description_context(project_input) + existing_devcontainer = None + devcontainer_url = None + logging.info("Using natural language project description as generation context.") + + exists, existing_record = check_url_exists(cache_key) logging.info(f"URL check result: exists={exists}, existing_record={existing_record}") - repo_context, existing_devcontainer, devcontainer_url = fetch_repo_context(repo_url) - logging.info(f"Fetched repo context. Existing devcontainer: {'Yes' if existing_devcontainer else 'No'}") - logging.info(f"Devcontainer URL: {devcontainer_url}") - if exists and not regenerate: - logging.info(f"URL already exists in database. Returning existing devcontainer_json for: {repo_url}") + logging.info(f"Input already exists in database. Returning existing devcontainer_json for: {cache_key}") devcontainer_json = existing_record['devcontainer_json'] generated = existing_record['generated'] source = "database" @@ -143,7 +153,7 @@ async def post(repo_url: str, regenerate: bool = False): embedding_json = None new_devcontainer = DevContainer( - url=repo_url, + url=cache_key, devcontainer_json=devcontainer_json, devcontainer_url=devcontainer_url, repo_context=repo_context, @@ -176,7 +186,7 @@ async def post(repo_url: str, regenerate: bool = False): Button( Img(cls="w-4 h-4", src="assets/icons/regenerate.svg", alt="Regenerate"), cls="icon-button regenerate-button", - hx_post=f"/generate?regenerate=true&repo_url={repo_url}", + hx_post=f"/generate?regenerate=true&repo_url={quote_plus(repo_url)}", hx_target="#result", hx_indicator="#action-text", title="Regenerate", @@ -207,4 +217,4 @@ async def get(fname:str, ext:str): if __name__ == "__main__": logging.info("Starting FastHTML app...") - serve() \ No newline at end of file + serve() From 0e1e610439899d748701231057e8790f76cee0ee Mon Sep 17 00:00:00 2001 From: Tian1996 <40369357+Tian1996@users.noreply.github.com> Date: Mon, 15 Jun 2026 13:38:55 +0800 Subject: [PATCH 2/7] Add natural language project helpers --- helpers/natural_language_helpers.py | 38 +++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 helpers/natural_language_helpers.py diff --git a/helpers/natural_language_helpers.py b/helpers/natural_language_helpers.py new file mode 100644 index 0000000..705e671 --- /dev/null +++ b/helpers/natural_language_helpers.py @@ -0,0 +1,38 @@ +import hashlib +import re + + +GITHUB_REPO_URL_PATTERN = re.compile(r"^https?://github\.com/[\w-]+/[\w.-]+/?$") +URL_PATTERN = re.compile(r"^[a-z][a-z0-9+.-]*://", re.IGNORECASE) + + +def normalize_project_description(description: str) -> str: + lines = [" ".join(line.split()) for line in description.strip().splitlines()] + return "\n".join(line for line in lines if line) + + +def is_project_description(value: str) -> bool: + description = normalize_project_description(value) + if len(description) < 10: + return False + if GITHUB_REPO_URL_PATTERN.match(description): + return False + return URL_PATTERN.match(description) is None + + +def build_project_description_context(description: str) -> str: + description = normalize_project_description(description) + if not is_project_description(description): + raise ValueError("Please enter a GitHub repository URL or describe the project you want to generate.") + + return ( + "<>\n" + f"{description}\n" + "<>" + ) + + +def project_description_cache_key(description: str) -> str: + description = normalize_project_description(description) + digest = hashlib.sha256(description.encode("utf-8")).hexdigest() + return f"description:{digest}" From 66353ac2cf2ce7fd648f469f4705588a76935dba Mon Sep 17 00:00:00 2001 From: Tian1996 <40369357+Tian1996@users.noreply.github.com> Date: Mon, 15 Jun 2026 13:39:16 +0800 Subject: [PATCH 3/7] Pass context source to devcontainer prompt --- helpers/devcontainer_helpers.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/helpers/devcontainer_helpers.py b/helpers/devcontainer_helpers.py index ef4f34c..0ebe147 100644 --- a/helpers/devcontainer_helpers.py +++ b/helpers/devcontainer_helpers.py @@ -81,7 +81,8 @@ def generate_devcontainer_json(instructor_client, repo_url, repo_context, devcon template_data = { "repo_url": repo_url, "repo_context": truncated_context, - "existing_devcontainer": existing_devcontainer + "existing_devcontainer": existing_devcontainer, + "context_source": "a project description" if "<>" in repo_context else "a GitHub repository", } prompt = process_template("prompts/devcontainer.jinja", template_data) @@ -136,4 +137,4 @@ def save_devcontainer(new_devcontainer): return result.data[0] if result.data else None except Exception as e: logging.error(f"Error saving devcontainer to Supabase: {str(e)}") - raise \ No newline at end of file + raise From 69bb7bce2f350785732c53ac4ec0de3a83cf9aca Mon Sep 17 00:00:00 2001 From: Tian1996 <40369357+Tian1996@users.noreply.github.com> Date: Mon, 15 Jun 2026 13:39:19 +0800 Subject: [PATCH 4/7] Support project description prompt context --- prompts/devcontainer.jinja | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/prompts/devcontainer.jinja b/prompts/devcontainer.jinja index 43d2469..b488fa3 100644 --- a/prompts/devcontainer.jinja +++ b/prompts/devcontainer.jinja @@ -1,4 +1,4 @@ -Given the following context from a GitHub repository: +Given the following context from {{ context_source }}: {{ repo_context }} @@ -78,4 +78,4 @@ Here's an example of a devcontainer.json: } ``` -Your goal is to deliver the most logical, secure, efficient, and well-documented devcontainer.json file. \ No newline at end of file +Your goal is to deliver the most logical, secure, efficient, and well-documented devcontainer.json file. From 1e54002bb9d349e23f79dcdf7026ec5ec35f9a11 Mon Sep 17 00:00:00 2001 From: Tian1996 <40369357+Tian1996@users.noreply.github.com> Date: Mon, 15 Jun 2026 13:39:23 +0800 Subject: [PATCH 5/7] Update copy for project descriptions --- content.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/content.py b/content.py index 71ac0b1..5119d04 100644 --- a/content.py +++ b/content.py @@ -8,7 +8,7 @@ def hero_section(): Div( H1("AI-Powered Dev Environment Setup", cls="text-3xl font-bold text-center"), H2("From GitHub to Ready-to-Code in Seconds"), - P("Paste your GitHub URL and get a custom devcontainer.json to simplify your development.", cls="text-lg text-center mt-4"), + P("Paste a GitHub URL or describe a project to get a custom devcontainer.json.", cls="text-lg text-center mt-4"), cls="container mx-auto px-4 py-16" ), cls="bg-gray-100", @@ -19,7 +19,7 @@ def generator_section(): return Section( Form( Group( - Input(type="text", name="repo_url", placeholder="Paste your Github repo URL, or select a repo to get started", cls="form-input", list="repo-list"), + Input(type="text", name="repo_url", placeholder="Paste a GitHub repo URL or describe a project", cls="form-input", list="repo-list"), Datalist( Option(value="https://github.com/devcontainers/templates"), Option(value="https://github.com/JetBrains/devcontainers-examples"), @@ -258,4 +258,4 @@ def manifesto_page(): ), footer_section() ) - ) \ No newline at end of file + ) From 868f76bd6d55fe90c98f6a36f623ddf7ef8e6941 Mon Sep 17 00:00:00 2001 From: Tian1996 <40369357+Tian1996@users.noreply.github.com> Date: Mon, 15 Jun 2026 13:39:27 +0800 Subject: [PATCH 6/7] Allow project descriptions in generator form --- js/main.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/js/main.js b/js/main.js index b80975d..a18c867 100644 --- a/js/main.js +++ b/js/main.js @@ -123,19 +123,27 @@ function isValidGithubUrl(url) { return pattern.test(url); } +function looksLikeUrl(value) { + return /^[a-z][a-z0-9+.-]*:\/\//i.test(value); +} + +function isValidProjectDescription(value) { + return value.trim().length >= 10 && !isValidGithubUrl(value.trim()) && !looksLikeUrl(value.trim()); +} + function validateRepoUrl() { const input = document.querySelector('input[name="repo_url"]'); const errorDiv = document.getElementById('url-error'); const generateButton = document.getElementById('generate-button'); if (input.value.trim() === '') { - errorDiv.textContent = 'Please enter a GitHub repository URL.'; + errorDiv.textContent = 'Please enter a GitHub repository URL or project description.'; generateButton.disabled = true; return false; } - if (!isValidGithubUrl(input.value)) { - errorDiv.textContent = 'Please enter a valid GitHub repository URL.'; + if (!isValidGithubUrl(input.value) && !isValidProjectDescription(input.value)) { + errorDiv.textContent = 'Please enter a GitHub repository URL or describe the project in plain text.'; generateButton.disabled = true; return false; } @@ -164,4 +172,3 @@ document.addEventListener('DOMContentLoaded', function() { initializeButtons(); setupObserver(); }); - From 8ac1f14d23b1d1951112ffb6653b73203962e41b Mon Sep 17 00:00:00 2001 From: Tian1996 <40369357+Tian1996@users.noreply.github.com> Date: Mon, 15 Jun 2026 13:39:30 +0800 Subject: [PATCH 7/7] Add tests for natural language helpers --- tests/test_natural_language_helpers.py | 43 ++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 tests/test_natural_language_helpers.py diff --git a/tests/test_natural_language_helpers.py b/tests/test_natural_language_helpers.py new file mode 100644 index 0000000..fd185eb --- /dev/null +++ b/tests/test_natural_language_helpers.py @@ -0,0 +1,43 @@ +import hashlib +import unittest + +from helpers.natural_language_helpers import ( + build_project_description_context, + is_project_description, + normalize_project_description, + project_description_cache_key, +) + + +class NaturalLanguageHelpersTest(unittest.TestCase): + def test_normalize_project_description_collapses_line_spacing(self): + description = " Python FastAPI service \n\n with Postgres and Redis " + + self.assertEqual( + normalize_project_description(description), + "Python FastAPI service\nwith Postgres and Redis", + ) + + def test_detects_descriptions_without_accepting_urls(self): + self.assertTrue(is_project_description("Node.js API with Redis and background workers")) + self.assertFalse(is_project_description("https://github.com/daytonaio/daytona")) + self.assertFalse(is_project_description("https://example.com/not-a-github-repo")) + self.assertFalse(is_project_description("tiny")) + + def test_build_project_description_context_uses_expected_sections(self): + context = build_project_description_context("Ruby on Rails app with PostgreSQL") + + self.assertIn("<>", context) + self.assertIn("Ruby on Rails app with PostgreSQL", context) + self.assertIn("<>", context) + + def test_project_description_cache_key_is_stable_for_normalized_text(self): + description = "Python FastAPI service\nwith Redis" + normalized = "Python FastAPI service\nwith Redis" + expected = hashlib.sha256(normalized.encode("utf-8")).hexdigest() + + self.assertEqual(project_description_cache_key(description), f"description:{expected}") + + +if __name__ == "__main__": + unittest.main()