From dee58f064c9cabdc693a572539e6eb15485d8d22 Mon Sep 17 00:00:00 2001 From: raks4 Date: Fri, 6 Feb 2026 00:01:22 +0530 Subject: [PATCH 1/5] containerized for docker deployment --- Dockerfile | 35 ++++++++ backend/backend/settings.py | 8 +- backend/backend/urls.py | 6 +- backend/generator/views.py | 173 +++++++++++++++++++----------------- backend/requirements.txt | 7 ++ backend/stream_test.py | 22 +++++ docker-compose.yml | 24 +++++ frontend/package.json | 1 + frontend/src/App.js | 72 ++++++++++----- 9 files changed, 240 insertions(+), 108 deletions(-) create mode 100644 Dockerfile create mode 100644 backend/requirements.txt create mode 100644 backend/stream_test.py create mode 100644 docker-compose.yml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a934da3 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,35 @@ +# Stage 1: build frontend +FROM node:18 AS frontend-build +WORKDIR /app/frontend +COPY frontend/package*.json ./ +# Use npm install to avoid CI lockfile/peer-deps failures during image build +RUN npm install --legacy-peer-deps +COPY frontend/ . +RUN npm run build + +# Stage 2: build python image +FROM python:3.11-slim +ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 +WORKDIR /app + +# system deps if needed +RUN apt-get update && apt-get install -y build-essential curl && rm -rf /var/lib/apt/lists/* + +# copy requirements and install +COPY backend/requirements.txt /app/requirements.txt +RUN pip install --no-cache-dir -r /app/requirements.txt + +# copy backend app +COPY backend/ /app/backend + +# copy frontend build into Django static locations +# Place index.html under backend/static and the built JS/CSS under STATIC_ROOT +COPY --from=frontend-build /app/frontend/build/index.html /app/backend/static/index.html +COPY --from=frontend-build /app/frontend/build/static /app/backend/staticfiles + +WORKDIR /app/backend +# run collectstatic to pick up any additional static files +RUN python manage.py collectstatic --noinput + +EXPOSE 8000 +CMD ["gunicorn","backend.wsgi:application","--bind","0.0.0.0:8000","--workers","2","--threads","8","--worker-class","gthread","--timeout","0","--keep-alive","75","--access-logfile","-","--error-logfile","-"] diff --git a/backend/backend/settings.py b/backend/backend/settings.py index e76ff91..bdf13dc 100644 --- a/backend/backend/settings.py +++ b/backend/backend/settings.py @@ -45,6 +45,7 @@ MIDDLEWARE = [ "corsheaders.middleware.CorsMiddleware", 'django.middleware.security.SecurityMiddleware', + "whitenoise.middleware.WhiteNoiseMiddleware", 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', @@ -58,7 +59,7 @@ TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], + 'DIRS': [BASE_DIR / 'static'], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ @@ -118,7 +119,10 @@ # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/6.0/howto/static-files/ -STATIC_URL = 'static/' +STATIC_URL = '/static/' +STATIC_ROOT = BASE_DIR / "staticfiles" +STATICFILES_DIRS = [BASE_DIR / 'static'] +STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage" CORS_ALLOW_ALL_ORIGINS = True diff --git a/backend/backend/urls.py b/backend/backend/urls.py index 7c3fb55..a0cd1a0 100644 --- a/backend/backend/urls.py +++ b/backend/backend/urls.py @@ -15,9 +15,13 @@ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin -from django.urls import path, include +from django.urls import path, include, re_path +from django.views.generic import TemplateView urlpatterns = [ path('admin/', admin.site.urls), path("api/", include("generator.urls")), + # Serve the React app's index.html on all other non-API/static/admin routes + # Exclude paths that should be handled by static files or other services (static/, api/, admin/) + re_path(r'^(?!static/|api/|admin/).*$', TemplateView.as_view(template_name='index.html')), ] diff --git a/backend/generator/views.py b/backend/generator/views.py index ae9a36e..f8748d9 100644 --- a/backend/generator/views.py +++ b/backend/generator/views.py @@ -1,118 +1,126 @@ -from django.shortcuts import render -import requests from rest_framework.decorators import api_view from rest_framework.response import Response -from django.http import FileResponse -from .pdf_generator import create_pdf -from django.http import StreamingHttpResponse -import json +from django.http import StreamingHttpResponse, FileResponse import requests +import json +import os +import re +from .pdf_generator import create_pdf -OLLAMA_URL = "http://localhost:11434/api/generate" +OLLAMA_URL = os.environ.get("OLLAMA_URL", "http://localhost:11434/api/generate") +OLLAMA_MODEL = os.environ.get("OLLAMA_MODEL", "qwen2.5-coder:7b") -def classify_input(text): - check_prompt = f""" -You are a strict classifier for a programming assistant. +# ---------- SIMPLE CODE DETECTOR (FAST) ---------- +def is_likely_code(text: str) -> bool: + indicators = [ + "def ", "class ", "import ", "from ", "{", "}", ";", + "print(", "console.log", "function ", "#include" + ] + text = text.lower() + return any(i in text for i in indicators) -Your task: -Decide if the input is related to software, programming, coding, frameworks, tools, or computer science. -Treat these as PROGRAMMING: -languages (python, java, c++, js) -frameworks (react, django, node, spring, flask) -concepts (api, database, array, algorithm, recursion, oops) -errors, debugging, coding doubts +# ---------- MAIN STREAMING ENDPOINT ---------- +@api_view(["POST"]) +def generate_documentation(request): + user_input = request.data.get("code", "").strip() -Categories (return ONLY ONE word): + if not user_input: + return Response("Empty input", status=400) -CODE -PROGRAMMING_QUESTION -NON_TECH + is_code = is_likely_code(user_input) -Examples: -"What is React?" -> PROGRAMMING_QUESTION -"Explain Python list" -> PROGRAMMING_QUESTION -"print('hello')" -> CODE -"who is prime minister" -> NON_TECH -"weather today" -> NON_TECH + prompt = ( + f"""You are a professional software documentation writer. +Explain the following code in clear structured Markdown. +Use headings and fenced code blocks. -Input: -{text} +Code: +{user_input} """ + if is_code + else + f"""You are a helpful programming tutor. +Answer clearly in Markdown with examples. +Question: +{user_input} +""" + ) payload = { - "model": "qwen2.5-coder:7b", - "prompt": check_prompt, - "stream": False + "model": OLLAMA_MODEL, + "prompt": prompt, + "stream": True, } - try: - res = requests.post(OLLAMA_URL, json=payload, timeout=120) - return res.json()["response"].strip().upper() - except: - return "NON_TECH" + import time + def stream(): + last_sent = time.time() -@api_view(["POST"]) -def generate_documentation(request): + try: + # 🚀 force headers flush + yield " " - user_input = request.data.get("code", "") + r = requests.post( + OLLAMA_URL, + json=payload, + stream=True, + timeout=None + ) - if not user_input.strip(): - return Response({"documentation": "Empty input provided."}) + if r.status_code != 200: + yield f"\nModel error: {r.text}" + return - category = classify_input(user_input) + code_block_open = False - # CODE → documentation - if "CODE" in category: - prompt = f""" -You are a professional software documentation writer. -Explain the code in clear structured markdown format. + for line in r.iter_lines(decode_unicode=True): + now = time.time() -Code: -{user_input} -""" + # 🫀 KEEP-ALIVE every 1 second + if now - last_sent > 1: + yield "\n" + last_sent = now - # PROGRAMMING QUESTION → answer - elif "PROGRAMMING_QUESTION" in category: - prompt = f""" -You are a helpful programming tutor. -Answer clearly with examples. + if not line: + continue -Question: -{user_input} -""" + try: + data = json.loads(line) + except json.JSONDecodeError: + continue - # NON TECH → reject - else: - return Response({ - "documentation": "I only answer programming related queries." - }) + chunk = data.get("response") + if not chunk: + continue - payload = { - "model": "qwen2.5-coder:7b", - "prompt": prompt, - "stream": True - } + if "\npython\n" in chunk: + chunk = chunk.replace("\npython\n", "\n```python\n") + code_block_open = True - def stream(): - try: - response = requests.post(OLLAMA_URL, json=payload, stream=True, timeout=600) + yield chunk + last_sent = time.time() + + if data.get("done") and code_block_open: + yield "\n```" - for line in response.iter_lines(): - if line: - data = json.loads(line.decode("utf-8")) - if "response" in data: - yield data["response"] + except Exception as e: + yield f"\nError: {str(e)}" - except: - yield "\nModel not responding. Check Ollama." - return StreamingHttpResponse(stream(), content_type="text/plain") + response = StreamingHttpResponse( + stream(), + content_type="text/markdown; charset=utf-8" + ) + response["Cache-Control"] = "no-cache" + response["X-Accel-Buffering"] = "no" + return response +# ---------- PDF DOWNLOAD ---------- @api_view(["POST"]) def download_pdf(request): docs = request.data.get("docs", "") @@ -121,4 +129,7 @@ def download_pdf(request): return Response({"error": "No documentation provided."}) filename = create_pdf(docs) - return FileResponse(open(filename, "rb"), as_attachment=True) + response = FileResponse(open(filename, "rb"), content_type="application/pdf") + response["Content-Disposition"] = f'attachment; filename="{filename}"' + return response + diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..624fde3 --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,7 @@ +Django +djangorestframework +django-cors-headers +requests +reportlab +gunicorn +whitenoise \ No newline at end of file diff --git a/backend/stream_test.py b/backend/stream_test.py new file mode 100644 index 0000000..6f970f8 --- /dev/null +++ b/backend/stream_test.py @@ -0,0 +1,22 @@ +import requests +import sys + +url = "http://localhost:8000/api/generate/" +json = { + "code": "print(\"hello world\")", + "max_output_tokens": 256 +} + +try: + r = requests.post(url, json=json, stream=True, timeout=60) + print("STATUS", r.status_code) + r.raise_for_status() + for chunk in r.iter_content(chunk_size=None): + if chunk: + try: + s = chunk.decode() + except Exception: + s = str(chunk) + print(s, end='', flush=True) +except Exception as e: + print("ERROR", e) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..8ce3090 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,24 @@ +version: "3.8" +services: + ollama: + image: ollama/ollama:latest # replace with trusted image if needed + container_name: ollama + ports: + - "11434:11434" + volumes: + - ollama_data:/var/lib/ollama + + docgen: + build: . + image: rakshaknaik/docgen:latest + environment: + - OLLAMA_URL=http://ollama:11434/api/generate + - OLLAMA_MODEL=qwen2.5-coder:7b + - DJANGO_SETTINGS_MODULE=backend.settings + depends_on: + - ollama + ports: + - "8000:8000" + +volumes: + ollama_data: \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index c645289..dac8a76 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -7,6 +7,7 @@ "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^13.5.0", + "axios": "^1.13.4", "framer-motion": "^12.31.0", "lucide-react": "^0.563.0", "react": "^19.2.4", diff --git a/frontend/src/App.js b/frontend/src/App.js index 9e7f455..aeabf17 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -23,36 +23,54 @@ export default function App() { const [loading, setLoading] = useState(false); const generateDocs = async () => { - setDocs(""); - setLoading(true); + setDocs(""); + setLoading(true); - const response = await fetch("http://127.0.0.1:8000/api/generate/", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ code }), - }); - - const reader = response.body.getReader(); - const decoder = new TextDecoder("utf-8"); - - while (true) { - const { value, done } = await reader.read(); - if (done) break; - - const chunk = decoder.decode(value); - setDocs(prev => prev + chunk); - } + try { + const response = await fetch("/api/generate/", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ code }), + }); + + if (!response.ok) { + const err = await response.text(); + throw new Error(err || "Server error"); + } + + const reader = response.body.getReader(); + const decoder = new TextDecoder("utf-8"); + + try { + while (true) { + const { value, done } = await reader.read(); + if (done) break; + + if (value) { + const chunk = decoder.decode(value, { stream: true }); + setDocs(prev => prev + chunk); + } + } +} catch (streamErr) { + console.warn("Stream interrupted:", streamErr); + // IMPORTANT: don't throw — allow partial docs +} - setLoading(false); -}; + } catch (err) { + console.error("Generate failed:", err); + alert("Error generating documentation: " + (err.message || err)); + } finally { + setLoading(false); + } + }; const downloadPDF = async () => { try { const res = await axios.post( - "http://127.0.0.1:8000/api/pdf/", + "/api/pdf/", { docs }, { responseType: "blob" } ); @@ -169,6 +187,7 @@ export default function App() { {/* BUTTON */}
- - - {/* Toggle link */} -

- {isRegister ? "Already have an account? " : "Don't have an account? "} - -

-
- - - {/* RIGHT SIDE - INFO WITH GEOMETRIC PATTERN */} -
- {/* Geometric Pattern Background */} -
- - - - - - - - -
- - {/* Large Circles - Decorative */} -
-
-
- -
-

AI Document Generator

-

- Transform your code into professional documentation automatically. -

- -
-
-
-
-
-
-

Code Analysis

-

Automatically analyze and document your codebase

-
-
- -
-
-
-
-
-

Export Options

-

Download as PDF or DOCX format

-
-
- -
-
-
-
-
-

History Tracking

-

Access all your generated documents anytime

-
-
-
-
-
- - ); + // --- COLORS --- + const isDark = theme === "dark"; + const bgMain = isDark ? "bg-[#18181b]" : "bg-[#e5e7eb]"; + const bgCard = isDark ? "bg-[#27272a]" : "bg-white"; + const bgSidebar = isDark ? "bg-[#1f1f22]" : "bg-[#f3f4f6]"; + const textMain = isDark ? "text-gray-100" : "text-black"; + const textSub = isDark ? "text-gray-400" : "text-gray-600"; + const border = isDark ? "border-[#3f3f46]" : "border-gray-300"; + + // Adjusted Blue (Lower grade/Less neon) + const primaryBtn = "bg-slate-700 hover:bg-slate-600 text-white shadow-none"; + + // --- RENDER --- + if (!token) return ; return ( -<<<<<<< HEAD
@@ -361,82 +249,179 @@ export default function App() {
{/* SIDEBAR */} -======= -
- {/* ANIMATED BACKGROUND */} -
- - {/* TOP HEADER */} - - - {/* HISTORY MODAL */} ->>>>>>> 3141ede1a6758e9a7abcaf25f4761788104e3f38 {showHistory && ( - <> - setShowHistory(false)} className="fixed inset-0 bg-black/50 z-40 top-14" /> - -
- Document History - -
-
+ + {/* Header with Clickable Logo */} +
+ + +
+ + {/* New Project */} +
+ +
+ + {/* History List */} +
{history.map(doc => ( -
loadHistoryItem(doc)} className={`p-3 rounded-lg cursor-pointer ${colors.hover} transition border ${currentDocId === doc.id ? 'border-blue-500 bg-blue-500/5' : 'border-transparent'}`}> -
{doc.topic}
-
- {new Date(doc.created_at).toLocaleDateString()} - +
loadDoc(doc)} + className={`p-3 rounded-lg border cursor-pointer hover:bg-gray-200 dark:hover:bg-white/5 transition + ${currentDocId === doc.id ? `border-blue-500 ring-1 ring-blue-500 ${isDark ? 'bg-blue-900/20' : 'bg-blue-50'}` : `border-transparent`} + `} + > +
{doc.topic || "Untitled Doc"}
+
+ {new Date(doc.created_at).toLocaleDateString()} + +
-
))} -
-
- -
- - +
+ + {/* User Profile */} +
+
setView("profile")} className="flex items-center gap-3 cursor-pointer hover:opacity-80"> +
{userData.username[0]}
+
+
{userData.username}
+
{connection.toUpperCase()}
+
+ +
+
+
)} -
- {/* LEFT PANEL - CODE INPUT */} -
-
- Code Input - -
- - {/* Prompts */} -
-
-
Paste or describe your code
-
- Paste code snippets or upload files (.py, .js, .cpp, .java) to generate comprehensive documentation. -
-
+ {/* MAIN LAYOUT: Vertical Flex Column */} +
+ + {/* 1. NAVBAR (Fixed Height, Flex None) */} +
+
+ {!showHistory && ( + + )} +

{view}

+
+ +
+ + + +
+ +
+
+ + {/* 2. SCROLLABLE CONTENT AREA (Flex 1) */} +
+ + {view === 'home' && ( + <> + {/* Documentation Output */} +
+ {!docs && !loading ? ( +
+ +

Generate Professional Documentation

+

+ Select your required model, specify requirement or paste your source code below, and let DocGen craft comprehensive documentation for you. +

+
+ ) : ( +
+
+ + + +
+ +
+
    , + ol: ({node, ...props}) =>
      , + li: ({node, ...props}) =>
    1. , + h1: ({node, ...props}) =>

      , + h2: ({node, ...props}) =>

      , + p: ({node, ...props}) =>

      , + code({node, inline, className, children, ...props}) { + const match = /language-(\w+)/.exec(className || '') + return !inline && match ? ( +

      + + {String(children).replace(/\n$/, '')} + +
      + ) : ( + + {children} + + ) + } + }} + > + {docs} + + {loading && } +

+
+ )} +
-
-
How it works
-
- AI analyzes your code structure, functions, and logic to create documentation with sections like purpose, parameters, and usage examples. -
-
+ {/* Input Controls */} +
+
+
+
+ + +
+ +
-<<<<<<< HEAD {!isInputMinimized && (
@@ -532,149 +517,98 @@ export default function App() {

Contact Us

docgenindia@gmail.com
-======= - {/* CODE TEXTAREA */} -
-
- -
- - -
-
-