diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..9e1bac6 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,41 @@ +# 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 + +# lightweight ASGI server + async HTTP client for streaming +RUN pip install --no-cache-dir "uvicorn[standard]" httpx + +# 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 +ENV DJANGO_SETTINGS_MODULE=backend.settings +RUN mkdir -p /app/backend/staticfiles +RUN python manage.py collectstatic --noinput + +EXPOSE 8000 +# Run as ASGI server for non-blocking streaming (uvicorn + uvloop + httptools from uvicorn[standard]) +CMD ["uvicorn", "backend.asgi:application", "--host", "0.0.0.0", "--port", "8000", "--workers", "1", "--loop", "uvloop", "--http", "httptools"] diff --git a/backend/backend/settings.py b/backend/backend/settings.py index 05b0861..33ecd8a 100644 --- a/backend/backend/settings.py +++ b/backend/backend/settings.py @@ -31,6 +31,7 @@ "corsheaders.middleware.CorsMiddleware", 'django.middleware.security.SecurityMiddleware', + "whitenoise.middleware.WhiteNoiseMiddleware", 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', @@ -47,7 +48,7 @@ TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], + 'DIRS': [BASE_DIR / 'static'], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ @@ -89,6 +90,8 @@ # ---------------- STATIC ---------------- STATIC_URL = 'static/' +STATIC_ROOT = BASE_DIR / 'staticfiles' +STATICFILES_DIRS = [BASE_DIR / 'static'] # ===================================================== diff --git a/backend/backend/urls.py b/backend/backend/urls.py index 2cb62b2..aad7b86 100644 --- a/backend/backend/urls.py +++ b/backend/backend/urls.py @@ -1,7 +1,11 @@ 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')), # MUST be 'api/' + # Serve React app at root and for any non-API/admin path (client-side routing) + path('', TemplateView.as_view(template_name='index.html'), name='home'), + re_path(r'^(?!admin/|api/).*$', TemplateView.as_view(template_name='index.html')), ] diff --git a/backend/db.sqlite3 b/backend/db.sqlite3 index 0c9e158..4d96022 100644 Binary files a/backend/db.sqlite3 and b/backend/db.sqlite3 differ diff --git a/backend/generator/views.py b/backend/generator/views.py index bbd3203..33a2dd7 100644 --- a/backend/generator/views.py +++ b/backend/generator/views.py @@ -188,7 +188,7 @@ def generate_documentation(request): user_model = request.data.get("model", "qwen2.5-coder:3b") ALLOWED_MODELS = [ - "phi3:mini", + "phi3:latest", "qwen2.5-coder:3b", "qwen2.5-coder:7b" ] @@ -251,4 +251,4 @@ def download_pdf(request): @api_view(["POST"]) def download_docx(request): docs = request.data.get("docs", "") - return FileResponse(create_docx(docs), as_attachment=True, filename="Doc.docx") + return FileResponse(create_docx(docs), as_attachment=True, filename="Doc.docx") \ No newline at end of file diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..0a93fbc --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,10 @@ +Django +djangorestframework +django-cors-headers +requests +reportlab +gunicorn +whitenoise +djangorestframework-simplejwt +wikipedia +python-docx \ 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/backend/users/urls.py b/backend/users/urls.py index 49de864..3939f45 100644 --- a/backend/users/urls.py +++ b/backend/users/urls.py @@ -1,9 +1,11 @@ from django.urls import path -from .views import register, login_user, logout_user, profile - +from .views import register, login_user, logout_user, profile,login_view +from rest_framework_simplejwt.views import TokenRefreshView urlpatterns = [ path("register/", register), path("login/", login_user), path("logout/", logout_user), path("profile/", profile), + path("api/login/", login_view), + path("api/token/refresh/", TokenRefreshView.as_view()), ] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e76eb7c --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,30 @@ +version: "3.8" +services: + ollama: + image: ollama/ollama:latest # replace with trusted image if needed + container_name: ollama + ports: + - "11434:11434" + volumes: + - ollama_data:/root/.ollama + restart: unless-stopped + + docgen: + build: + context: . + dockerfile: Dockerfile + image: rakshaknaik/docgen:latest + environment: + - OLLAMA_URL=http://ollama:11434/api/generate + - OLLAMA_MODEL=qwen2.5-coder:3b + - DJANGO_SETTINGS_MODULE=backend.settings + depends_on: + - ollama + ports: + - "8000:8000" + restart: unless-stopped + volumes: + - ./backend/db.sqlite3:/app/backend/db.sqlite3 + +volumes: + ollama_data: \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 797cc30..7fa81a2 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -12,6 +12,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", @@ -4876,6 +4877,33 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.4.tgz", + "integrity": "sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axios/node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -14839,6 +14867,12 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/psl": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index cb21aa8..d7e1993 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 e34e582..a56034a 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -3,13 +3,16 @@ import axios from "axios"; import { motion, AnimatePresence } from "framer-motion"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; +import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; +import { vscDarkPlus, coy } from 'react-syntax-highlighter/dist/esm/styles/prism'; import { - Cpu, FileText, Download, Wand2, - Copy, Upload, Wifi, WifiOff, Trash2, LogOut, Menu, X, StopCircle, - Sun, Moon, PlusCircle, Loader2 + FileText, Download, Wand2, ChevronDown, ChevronUp, + Copy, Upload, Wifi, WifiOff, Trash2, LogOut, Menu, X, StopCircle, + Sun, Moon, PlusCircle, Loader2, Settings, User, Layout, Code2, + Paperclip, Info, Mail } from "lucide-react"; -// API CONFIG +// --- API CONFIG --- const API_BASE = "http://127.0.0.1:8000/api/"; const API = axios.create({ baseURL: API_BASE }); @@ -21,76 +24,82 @@ API.interceptors.request.use((config) => { // --- MODELS --- const MODELS = [ - { id: "phi3:mini", label: "Fast" }, - { id: "qwen2.5-coder:3b", label: "Balanced" }, - { id: "qwen2.5-coder:7b", label: "Thinking" } + { id: "phi3:latest", label: "Fast" }, + { id: "qwen2.5-coder:3b", label: "Balanced" }, + { id: "qwen2.5-coder:7b", label: "Thinking" } ]; +// --- APP COMPONENT --- export default function App() { - // --- STATE --- + // Global const [token, setToken] = useState(localStorage.getItem("token")); - const [username, setUsername] = useState("User"); + const [theme, setTheme] = useState(localStorage.getItem("theme") || "light"); + const [view, setView] = useState("home"); + // Workspace const [code, setCode] = useState(""); const [docs, setDocs] = useState(""); const [history, setHistory] = useState([]); const [currentDocId, setCurrentDocId] = useState(null); + + // --- UPDATED DEFAULT TO BALANCED --- const [model, setModel] = useState("qwen2.5-coder:3b"); - + const [loading, setLoading] = useState(false); const [connection, setConnection] = useState("checking"); const [abortController, setAbortController] = useState(null); - - const [showHistory, setShowHistory] = useState(false); - const [showProfile, setShowProfile] = useState(false); - const [theme, setTheme] = useState("dark"); + + // UI Layout + const [showHistory, setShowHistory] = useState(true); const [isInputMinimized, setIsInputMinimized] = useState(false); - const outputRef = useRef(null); - const textareaRef = useRef(null); + + // User Data + const [userData, setUserData] = useState({ username: "Guest" }); // --- INIT --- + useEffect(() => { + document.documentElement.className = theme; + localStorage.setItem("theme", theme); + }, [theme]); + useEffect(() => { checkConnection(); - const interval = setInterval(checkConnection, 10000); if (token) { fetchUser(); fetchHistory(); } + const interval = setInterval(checkConnection, 15000); return () => clearInterval(interval); }, [token]); - // --- RESIZE INPUT --- - useEffect(() => { - if (textareaRef.current) { - textareaRef.current.style.height = isInputMinimized ? "40px" : "auto"; - if (!isInputMinimized) textareaRef.current.style.height = Math.min(textareaRef.current.scrollHeight, 200) + "px"; - } - }, [code, isInputMinimized]); - - // --- AUTH --- - const handleAuthSubmit = async (user, pass, isRegister) => { + // --- ACTIONS --- + const handleAuth = async (type, data) => { try { - const endpoint = isRegister ? "register/" : "login/"; - const res = await axios.post(`${API_BASE}${endpoint}`, { username: user, password: pass }); - - if (isRegister) { - alert("Registered successfully! Please login."); - setIsRegister(false); + const endpoint = type === "register" ? "register/" : "login/"; + const res = await axios.post(`${API_BASE}${endpoint}`, data); + + if (type === "register") { + alert("Account created! Please login."); + return true; } else { if (res.data.access) { localStorage.setItem("token", res.data.access); setToken(res.data.access); - checkConnection(); - setTimeout(() => { fetchUser(); fetchHistory(); }, 50); - } else { - alert("Login failed: No token received"); + return true; } } } catch (err) { - console.error(err); - alert(err.response?.data?.error || "Auth Failed"); + alert(err.response?.data?.error || "Connection Error"); + return false; } }; - // --- CORE LOGIC --- + const logout = () => { + localStorage.removeItem("token"); + setToken(null); + setHistory([]); + setView("home"); + }; + + // --- GENERATION LOGIC WITH SCROLL FIX --- const generateDocs = async () => { if (!code.trim()) return; setDocs(""); @@ -115,9 +124,9 @@ export default function App() { while (true) { const { value, done } = await reader.read(); if (done) break; - - let chunk = decoder.decode(value, { stream: true }); - + + let chunk = decoder.decode(value, {stream: true}); + if (isFirstChunk) { const match = chunk.match(/^\{"id":\s*(\d+)\}\n/); if (match) { @@ -128,53 +137,72 @@ export default function App() { } isFirstChunk = false; } - - // --- IMPROVED SCROLL HANDLING --- + + // --- SCROLL FIX START --- const container = outputRef.current; let shouldAutoScroll = false; - + if (container) { - const { scrollTop, scrollHeight, clientHeight } = container; - // Only auto-scroll if user is near the bottom (within 150px) - if (scrollHeight - scrollTop - clientHeight < 150) { - shouldAutoScroll = true; - } + const { scrollTop, scrollHeight, clientHeight } = container; + if (scrollHeight - scrollTop - clientHeight < 150) { + shouldAutoScroll = true; + } } setDocs(prev => prev + chunk); - // Auto-scroll only if user was near bottom if (shouldAutoScroll && container) { - setTimeout(() => { - container.scrollTop = container.scrollHeight; - }, 0); + setTimeout(() => { + container.scrollTop = container.scrollHeight; + }, 0); } + // --- SCROLL FIX END --- } - } catch (e) { - if (e.name !== 'AbortError') console.error("Gen Error:", e); + } catch (e) { + if (e.name !== 'AbortError') console.error(e); } setLoading(false); fetchHistory(); }; - const handleNewChat = () => { - setDocs(""); setCode(""); setCurrentDocId(null); setLoading(false); - if (abortController) abortController.abort(); - setShowHistory(false); + const stopGeneration = () => { + if(abortController) abortController.abort(); + setLoading(false); + }; + + const loadDoc = (doc) => { + if (loading) return; + setCurrentDocId(doc.id); + setDocs(doc.content); + setView("home"); }; - const stopGeneration = () => { - if (abortController) { abortController.abort(); setLoading(false); fetchHistory(); } + const deleteDoc = async (id, e) => { + e.stopPropagation(); + if (window.confirm("Delete this document?")) { + await API.delete(`history/${id}/delete/`); + if(currentDocId === id) { + setDocs(""); + setCurrentDocId(null); + } + fetchHistory(); + } }; - const loadHistoryItem = (doc) => { - setCurrentDocId(doc.id); - setDocs(doc.content); - setShowHistory(false); + const handleFileUpload = (e) => { + const file = e.target.files[0]; + if (!file) return; + const reader = new FileReader(); + reader.onload = (ev) => setCode(ev.target.result); + reader.readAsText(file); }; - // --- HELPERS --- + // --- API --- + const fetchUser = async () => { try { const res = await API.get("user/"); setUserData(res.data); } catch {} }; + const fetchHistory = async () => { try { const res = await API.get("history/"); setHistory(res.data); } catch {} }; + const checkConnection = async () => { try { await fetch(`${API_BASE}status/`); setConnection("online"); } catch { setConnection("offline"); } }; const downloadFile = async (type) => { + if(!docs) return; try { const res = await API.post(type === 'pdf' ? "pdf/" : "docx/", { docs }, { responseType: "blob" }); const url = window.URL.createObjectURL(new Blob([res.data])); @@ -182,372 +210,405 @@ export default function App() { link.href = url; link.download = `Documentation.${type}`; link.click(); - } catch { alert("Download Failed"); } + } catch { alert("Download failed."); } }; - const logout = () => { localStorage.removeItem("token"); setToken(null); setHistory([]); }; - const fetchUser = async () => { try { const res = await API.get("user/"); setUsername(res.data.username); } catch { } }; - const fetchHistory = async () => { try { const res = await API.get("history/"); setHistory(res.data); } catch { } }; - const checkConnection = async () => { try { const res = await fetch(`${API_BASE}status/`); const d = await res.json(); setConnection(d.online ? "online" : "offline"); } catch { setConnection("offline"); } }; - const deleteDoc = async (id, e) => { e.stopPropagation(); if (window.confirm("Delete?")) { await API.delete(`history/${id}/delete/`); fetchHistory(); } }; - const handleFileUpload = (e) => { const f = e.target.files[0]; if (f) { const r = new FileReader(); r.onload = ev => setCode(ev.target.result); r.readAsText(f); } }; - const toggleTheme = () => setTheme(theme === "dark" ? "light" : "dark"); - - const colors = { - bg: theme === "dark" ? "bg-[#0a0a0a]" : "bg-[#f5f5f5]", - card: theme === "dark" ? "bg-[#1a1a1a]" : "bg-white", - sidebar: theme === "dark" ? "bg-[#141414]" : "bg-[#fafafa]", - text: theme === "dark" ? "text-gray-300" : "text-gray-800", - textSecondary: theme === "dark" ? "text-gray-500" : "text-gray-500", - border: theme === "dark" ? "border-[#2a2a2a]" : "border-gray-200", - input: theme === "dark" ? "bg-[#1a1a1a] text-gray-300" : "bg-white text-gray-800", - hover: theme === "dark" ? "hover:bg-[#2a2a2a]" : "hover:bg-gray-100", - toolbar: theme === "dark" ? "bg-[#1a1a1a]/80" : "bg-white/80" - }; - - // --- RENDER LOGIN --- - const [isRegister, setIsRegister] = useState(false); - - if (!token) return ( -
- {/* LEFT SIDE - LOGIN FORM */} -
-
- {/* Logo */} -
-
DocGen
-
- - {/* Heading */} -

{isRegister ? "Sign up" : "Sign in"}

-

{isRegister ? "Create an account to get started" : "Enter your credentials to continue"}

- - {/* Form */} -
{ - e.preventDefault(); - const user = e.target[0].value; - const pass = e.target[1].value; - handleAuthSubmit(user, pass, isRegister); - }} className="space-y-4"> -
- - -
- -
- - -
- - {!isRegister && ( -
- -
- )} - - -
- - {/* 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 ( -
- {/* ANIMATED BACKGROUND */} -
- - {/* TOP HEADER */} - - {/* HISTORY MODAL */} + {/* SIDEBAR */} {showHistory && ( - <> - setShowHistory(false)} className="fixed inset-0 bg-black/50 z-40 top-14" /> - -
- Document History - -
-
- {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()} - -
-
- ))} -
-
- -
-
- - )} -
+ +
-
- {/* 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. -
+ {/* New Project */} +
+
-
-
How it works
-
- AI analyzes your code structure, functions, and logic to create documentation with sections like purpose, parameters, and usage examples. -
+ {/* History List */} +
+ {history.map(doc => ( +
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()} + +
+
+ ))}
- {/* CODE TEXTAREA */} -
-
- -
- - + {/* User Profile */} +
+
setView("profile")} className="flex items-center gap-3 cursor-pointer hover:opacity-80"> +
{userData.username[0]}
+
+
{userData.username}
+
{connection.toUpperCase()}
+
+
-
-