Spaces:
Sleeping
Sleeping
Mohammed AL Sarraj commited on
Commit ·
950dcd2
0
Parent(s):
initial deploy
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .dockerignore +4 -0
- .env.example +5 -0
- Dockerfile +7 -0
- app/__init__.py +42 -0
- app/__pycache__/__init__.cpython-314.pyc +0 -0
- app/core/__init__.py +0 -0
- app/core/__pycache__/__init__.cpython-314.pyc +0 -0
- app/core/__pycache__/ai.cpython-314.pyc +0 -0
- app/core/ai.py +219 -0
- app/core/file_reader.py +99 -0
- app/home/__init__.py +0 -0
- app/home/__pycache__/__init__.cpython-314.pyc +0 -0
- app/home/__pycache__/routes.cpython-314.pyc +0 -0
- app/home/routes.py +8 -0
- app/home/templates/home/index.html +152 -0
- app/templates/base.html +121 -0
- app/tools/__init__.py +0 -0
- app/tools/__pycache__/__init__.cpython-314.pyc +0 -0
- app/tools/changelog_ai/__init__.py +0 -0
- app/tools/changelog_ai/__pycache__/__init__.cpython-314.pyc +0 -0
- app/tools/changelog_ai/__pycache__/changelog.cpython-314.pyc +0 -0
- app/tools/changelog_ai/__pycache__/routes.cpython-314.pyc +0 -0
- app/tools/changelog_ai/changelog.py +94 -0
- app/tools/changelog_ai/routes.py +32 -0
- app/tools/changelog_ai/templates/changelog_ai/index.html +726 -0
- app/tools/doc_forge/__init__.py +0 -0
- app/tools/doc_forge/__pycache__/__init__.cpython-314.pyc +0 -0
- app/tools/doc_forge/__pycache__/db.cpython-314.pyc +0 -0
- app/tools/doc_forge/__pycache__/doc_generator.cpython-314.pyc +0 -0
- app/tools/doc_forge/__pycache__/github_fetcher.cpython-314.pyc +0 -0
- app/tools/doc_forge/__pycache__/routes.cpython-314.pyc +0 -0
- app/tools/doc_forge/db.py +88 -0
- app/tools/doc_forge/doc_generator.py +127 -0
- app/tools/doc_forge/github_fetcher.py +116 -0
- app/tools/doc_forge/routes.py +121 -0
- app/tools/doc_forge/templates/doc_forge/docs.html +417 -0
- app/tools/doc_forge/templates/doc_forge/index.html +430 -0
- app/tools/git_narrator/__init__.py +0 -0
- app/tools/git_narrator/__pycache__/__init__.cpython-314.pyc +0 -0
- app/tools/git_narrator/__pycache__/narrator.cpython-314.pyc +0 -0
- app/tools/git_narrator/__pycache__/routes.cpython-314.pyc +0 -0
- app/tools/git_narrator/narrator.py +112 -0
- app/tools/git_narrator/routes.py +22 -0
- app/tools/git_narrator/templates/git_narrator/index.html +509 -0
- app/tools/schema_detective/__init__.py +0 -0
- app/tools/schema_detective/__pycache__/__init__.cpython-314.pyc +0 -0
- app/tools/schema_detective/__pycache__/detective.cpython-314.pyc +0 -0
- app/tools/schema_detective/__pycache__/routes.cpython-314.pyc +0 -0
- app/tools/schema_detective/detective.py +252 -0
- app/tools/schema_detective/routes.py +40 -0
.dockerignore
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__pycache__
|
| 2 |
+
*.pyc
|
| 3 |
+
.env
|
| 4 |
+
*.db
|
.env.example
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
GROQ_API_KEY=
|
| 2 |
+
CEREBRAS_API_KEY=
|
| 3 |
+
OPENROUTER_API_KEY=
|
| 4 |
+
MISTRAL_API_KEY=
|
| 5 |
+
SECRET_KEY=change-me
|
Dockerfile
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.12-slim
|
| 2 |
+
WORKDIR /app
|
| 3 |
+
COPY requirements.txt .
|
| 4 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 5 |
+
COPY . .
|
| 6 |
+
EXPOSE 7860
|
| 7 |
+
CMD ["gunicorn", "wsgi:app", "--bind", "0.0.0.0:7860", "--workers", "2", "--timeout", "120", "--access-logfile", "-"]
|
app/__init__.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""DevKit — Flask app factory."""
|
| 2 |
+
import os
|
| 3 |
+
from flask import Flask
|
| 4 |
+
from flask_wtf.csrf import CSRFProtect
|
| 5 |
+
|
| 6 |
+
_csrf = CSRFProtect()
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def create_app():
|
| 10 |
+
app = Flask(__name__, template_folder="templates")
|
| 11 |
+
app.config["SECRET_KEY"] = os.environ.get("SECRET_KEY", "dev-devkit-2026")
|
| 12 |
+
app.config["MAX_CONTENT_LENGTH"] = 30 * 1024 * 1024
|
| 13 |
+
_csrf.init_app(app)
|
| 14 |
+
|
| 15 |
+
from app.home.routes import bp as home_bp
|
| 16 |
+
app.register_blueprint(home_bp)
|
| 17 |
+
|
| 18 |
+
from app.tools.schema_detective.routes import bp as schema_detective_bp
|
| 19 |
+
app.register_blueprint(schema_detective_bp, url_prefix='/schema-detective')
|
| 20 |
+
|
| 21 |
+
from app.tools.test_forge.routes import bp as test_forge_bp
|
| 22 |
+
app.register_blueprint(test_forge_bp, url_prefix='/test-forge')
|
| 23 |
+
|
| 24 |
+
from app.tools.sql_whisperer.routes import bp as sql_whisperer_bp
|
| 25 |
+
app.register_blueprint(sql_whisperer_bp, url_prefix='/sql-whisperer')
|
| 26 |
+
|
| 27 |
+
from app.tools.doc_forge.routes import bp as doc_forge_bp
|
| 28 |
+
app.register_blueprint(doc_forge_bp, url_prefix='/doc-forge')
|
| 29 |
+
|
| 30 |
+
from app.tools.changelog_ai.routes import bp as changelog_ai_bp
|
| 31 |
+
app.register_blueprint(changelog_ai_bp, url_prefix='/changelog-ai')
|
| 32 |
+
|
| 33 |
+
from app.tools.git_narrator.routes import bp as git_narrator_bp
|
| 34 |
+
app.register_blueprint(git_narrator_bp, url_prefix='/git-narrator')
|
| 35 |
+
|
| 36 |
+
from flask import jsonify
|
| 37 |
+
@app.errorhandler(Exception)
|
| 38 |
+
def _handle_exc(e):
|
| 39 |
+
code = getattr(e, "code", 500)
|
| 40 |
+
return jsonify({"error": str(e)}), code
|
| 41 |
+
|
| 42 |
+
return app
|
app/__pycache__/__init__.cpython-314.pyc
ADDED
|
Binary file (2.23 kB). View file
|
|
|
app/core/__init__.py
ADDED
|
File without changes
|
app/core/__pycache__/__init__.cpython-314.pyc
ADDED
|
Binary file (162 Bytes). View file
|
|
|
app/core/__pycache__/ai.cpython-314.pyc
ADDED
|
Binary file (11.2 kB). View file
|
|
|
app/core/ai.py
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Multi-provider AI engine. Runtime chain: Groq -> Cerebras -> OpenRouter -> Mistral -> Ollama."""
|
| 2 |
+
import json, logging, os, re, requests
|
| 3 |
+
|
| 4 |
+
logger = logging.getLogger(__name__)
|
| 5 |
+
_OLLAMA_BASE = "http://localhost:11434"
|
| 6 |
+
|
| 7 |
+
_PROVIDER_URLS = {
|
| 8 |
+
"groq": "https://api.groq.com/openai/v1/chat/completions",
|
| 9 |
+
"cerebras": "https://api.cerebras.ai/v1/chat/completions",
|
| 10 |
+
"openrouter": "https://openrouter.ai/api/v1/chat/completions",
|
| 11 |
+
"mistral": "https://api.mistral.ai/v1/chat/completions",
|
| 12 |
+
"openai": "https://api.openai.com/v1/chat/completions",
|
| 13 |
+
}
|
| 14 |
+
_FREE_MODELS = {
|
| 15 |
+
"groq": "llama-3.1-8b-instant",
|
| 16 |
+
"cerebras": "llama3.1-8b",
|
| 17 |
+
"openrouter": "google/gemma-3-12b-it:free",
|
| 18 |
+
"mistral": "mistral-small-latest",
|
| 19 |
+
}
|
| 20 |
+
_PREMIUM_MODELS = {
|
| 21 |
+
"groq": "llama-3.3-70b-versatile",
|
| 22 |
+
"cerebras": "qwen-3-235b-a22b-instruct-2507",
|
| 23 |
+
"openrouter": "google/gemma-3-27b-it:free",
|
| 24 |
+
"mistral": "mistral-medium-latest",
|
| 25 |
+
"openai": "gpt-4o-mini",
|
| 26 |
+
}
|
| 27 |
+
_CHAIN_CFG = [
|
| 28 |
+
{"name": "groq", "key_env": "GROQ_API_KEY", "timeout": 30, "extra": {}},
|
| 29 |
+
{"name": "cerebras", "key_env": "CEREBRAS_API_KEY", "timeout": 30, "extra": {}},
|
| 30 |
+
{"name": "openrouter", "key_env": "OPENROUTER_API_KEY", "timeout": 45,
|
| 31 |
+
"extra": {"HTTP-Referer": "https://github.com/Moealsarraj", "X-Title": "AI Tools"}},
|
| 32 |
+
{"name": "mistral", "key_env": "MISTRAL_API_KEY", "timeout": 40, "extra": {}},
|
| 33 |
+
]
|
| 34 |
+
|
| 35 |
+
# Build the runtime provider list — all providers with valid keys
|
| 36 |
+
_PROVIDERS = []
|
| 37 |
+
for _p in _CHAIN_CFG:
|
| 38 |
+
_k = os.environ.get(_p["key_env"], "")
|
| 39 |
+
if _k:
|
| 40 |
+
_PROVIDERS.append({
|
| 41 |
+
"name": _p["name"],
|
| 42 |
+
"url": _PROVIDER_URLS[_p["name"]],
|
| 43 |
+
"model": _FREE_MODELS[_p["name"]],
|
| 44 |
+
"key": _k,
|
| 45 |
+
"timeout": _p["timeout"],
|
| 46 |
+
"extra": _p["extra"],
|
| 47 |
+
})
|
| 48 |
+
|
| 49 |
+
# Ollama fallback
|
| 50 |
+
_OLLAMA_PROVIDER = None
|
| 51 |
+
try:
|
| 52 |
+
_r = requests.get(f"{_OLLAMA_BASE}/api/tags", timeout=3)
|
| 53 |
+
if _r.status_code == 200:
|
| 54 |
+
_installed = [m["name"] for m in _r.json().get("models", [])]
|
| 55 |
+
if _installed:
|
| 56 |
+
_OLLAMA_PROVIDER = {"name": "ollama", "model": _installed[0]}
|
| 57 |
+
except Exception:
|
| 58 |
+
pass
|
| 59 |
+
|
| 60 |
+
_AI_AVAILABLE = bool(_PROVIDERS or _OLLAMA_PROVIDER)
|
| 61 |
+
|
| 62 |
+
_RE_THINK = re.compile(r"<think>.*?</think>", re.DOTALL)
|
| 63 |
+
_RE_OPEN = re.compile(r"^```[a-z]*\n?", re.MULTILINE)
|
| 64 |
+
_RE_CLOSE = re.compile(r"\n?```$", re.MULTILINE)
|
| 65 |
+
|
| 66 |
+
def _clean(raw: str) -> str:
|
| 67 |
+
raw = _RE_THINK.sub("", raw).strip()
|
| 68 |
+
raw = _RE_OPEN.sub("", raw)
|
| 69 |
+
return _RE_CLOSE.sub("", raw).strip()
|
| 70 |
+
|
| 71 |
+
def _post_openai(url, key, model, messages, max_tokens, extra_headers, timeout=60):
|
| 72 |
+
headers = {"Authorization": f"Bearer {key}", "Content-Type": "application/json"}
|
| 73 |
+
headers.update(extra_headers)
|
| 74 |
+
r = requests.post(url, headers=headers,
|
| 75 |
+
json={"model": model, "messages": messages, "max_tokens": max_tokens},
|
| 76 |
+
timeout=timeout)
|
| 77 |
+
r.raise_for_status()
|
| 78 |
+
return _clean(r.json()["choices"][0]["message"]["content"])
|
| 79 |
+
|
| 80 |
+
def call_ai(messages: list, system: str = "", max_tokens: int = 2048,
|
| 81 |
+
api_key_row: dict | None = None) -> str:
|
| 82 |
+
if system:
|
| 83 |
+
messages = [{"role": "system", "content": system}] + messages
|
| 84 |
+
# Custom API key path (used by e.g. Wasit/Amin integrations)
|
| 85 |
+
if api_key_row:
|
| 86 |
+
provider = api_key_row.get("provider", "openai")
|
| 87 |
+
key = api_key_row["key"]
|
| 88 |
+
url = api_key_row.get("url") or _PROVIDER_URLS.get(provider, "")
|
| 89 |
+
model = api_key_row.get("model") or _PREMIUM_MODELS.get(provider, "gpt-4o-mini")
|
| 90 |
+
if not url:
|
| 91 |
+
raise ValueError(f"No endpoint known for provider {provider!r}")
|
| 92 |
+
if provider == "claude":
|
| 93 |
+
r = requests.post("https://api.anthropic.com/v1/messages",
|
| 94 |
+
headers={"x-api-key": key, "anthropic-version": "2023-06-01",
|
| 95 |
+
"content-type": "application/json"},
|
| 96 |
+
json={"model": "claude-sonnet-4-6", "max_tokens": max_tokens, "messages": messages},
|
| 97 |
+
timeout=60)
|
| 98 |
+
r.raise_for_status()
|
| 99 |
+
return _clean(r.json()["content"][0]["text"])
|
| 100 |
+
return _post_openai(url, key, model, messages, max_tokens, {})
|
| 101 |
+
if not _AI_AVAILABLE:
|
| 102 |
+
raise RuntimeError("No AI provider. Set GROQ_API_KEY or similar in .env")
|
| 103 |
+
# Ollama-only path
|
| 104 |
+
if not _PROVIDERS and _OLLAMA_PROVIDER:
|
| 105 |
+
r = requests.post(f"{_OLLAMA_BASE}/api/chat",
|
| 106 |
+
json={"model": _OLLAMA_PROVIDER["model"], "messages": messages, "stream": False},
|
| 107 |
+
timeout=120)
|
| 108 |
+
r.raise_for_status()
|
| 109 |
+
return _clean(r.json()["message"]["content"])
|
| 110 |
+
# Runtime chain: try each provider, fall back on 429 or transient errors
|
| 111 |
+
last_exc = None
|
| 112 |
+
for prov in _PROVIDERS:
|
| 113 |
+
try:
|
| 114 |
+
return _post_openai(
|
| 115 |
+
prov["url"], prov["key"], prov["model"],
|
| 116 |
+
messages, max_tokens, prov["extra"], prov["timeout"]
|
| 117 |
+
)
|
| 118 |
+
except requests.exceptions.HTTPError as e:
|
| 119 |
+
status = e.response.status_code if e.response is not None else 0
|
| 120 |
+
if status in (429, 503, 502):
|
| 121 |
+
logger.debug("Provider %s returned %s, trying next", prov["name"], status)
|
| 122 |
+
last_exc = e
|
| 123 |
+
continue
|
| 124 |
+
raise
|
| 125 |
+
except (requests.exceptions.ConnectionError,
|
| 126 |
+
requests.exceptions.Timeout) as e:
|
| 127 |
+
last_exc = e
|
| 128 |
+
continue
|
| 129 |
+
# Try Ollama as last resort
|
| 130 |
+
if _OLLAMA_PROVIDER:
|
| 131 |
+
r = requests.post(f"{_OLLAMA_BASE}/api/chat",
|
| 132 |
+
json={"model": _OLLAMA_PROVIDER["model"], "messages": messages, "stream": False},
|
| 133 |
+
timeout=120)
|
| 134 |
+
r.raise_for_status()
|
| 135 |
+
return _clean(r.json()["message"]["content"])
|
| 136 |
+
raise last_exc or RuntimeError("All AI providers failed or rate-limited")
|
| 137 |
+
|
| 138 |
+
def _repair_json(text: str) -> str:
|
| 139 |
+
"""Escape literal control characters inside JSON string values."""
|
| 140 |
+
result = []
|
| 141 |
+
in_str = False
|
| 142 |
+
esc = False
|
| 143 |
+
for c in text:
|
| 144 |
+
if esc:
|
| 145 |
+
result.append(c)
|
| 146 |
+
esc = False
|
| 147 |
+
continue
|
| 148 |
+
if c == '\\' and in_str:
|
| 149 |
+
result.append(c)
|
| 150 |
+
esc = True
|
| 151 |
+
continue
|
| 152 |
+
if c == '"':
|
| 153 |
+
in_str = not in_str
|
| 154 |
+
result.append(c)
|
| 155 |
+
continue
|
| 156 |
+
if in_str and c == '\n':
|
| 157 |
+
result.append('\\n')
|
| 158 |
+
continue
|
| 159 |
+
if in_str and c == '\r':
|
| 160 |
+
result.append('\\r')
|
| 161 |
+
continue
|
| 162 |
+
if in_str and c == '\t':
|
| 163 |
+
result.append('\\t')
|
| 164 |
+
continue
|
| 165 |
+
result.append(c)
|
| 166 |
+
return ''.join(result)
|
| 167 |
+
|
| 168 |
+
def _extract_json(raw: str):
|
| 169 |
+
"""Try progressively harder to extract valid JSON from raw text."""
|
| 170 |
+
raw = raw.strip()
|
| 171 |
+
# Direct parse
|
| 172 |
+
try:
|
| 173 |
+
return json.loads(raw)
|
| 174 |
+
except json.JSONDecodeError:
|
| 175 |
+
pass
|
| 176 |
+
# Repair literal newlines inside strings then retry
|
| 177 |
+
repaired = _repair_json(raw)
|
| 178 |
+
try:
|
| 179 |
+
return json.loads(repaired)
|
| 180 |
+
except json.JSONDecodeError:
|
| 181 |
+
pass
|
| 182 |
+
# Find first { or [ then walk to find matching closer
|
| 183 |
+
for source in (repaired, raw):
|
| 184 |
+
for start_ch, end_ch in [('{', '}'), ('[', ']')]:
|
| 185 |
+
idx = source.find(start_ch)
|
| 186 |
+
if idx == -1:
|
| 187 |
+
continue
|
| 188 |
+
depth = 0
|
| 189 |
+
in_str = False
|
| 190 |
+
esc = False
|
| 191 |
+
for i in range(idx, len(source)):
|
| 192 |
+
c = source[i]
|
| 193 |
+
if esc:
|
| 194 |
+
esc = False
|
| 195 |
+
continue
|
| 196 |
+
if c == '\\' and in_str:
|
| 197 |
+
esc = True
|
| 198 |
+
continue
|
| 199 |
+
if c == '"':
|
| 200 |
+
in_str = not in_str
|
| 201 |
+
continue
|
| 202 |
+
if in_str:
|
| 203 |
+
continue
|
| 204 |
+
if c == start_ch:
|
| 205 |
+
depth += 1
|
| 206 |
+
elif c == end_ch:
|
| 207 |
+
depth -= 1
|
| 208 |
+
if depth == 0:
|
| 209 |
+
candidate = source[idx:i+1]
|
| 210 |
+
try:
|
| 211 |
+
return json.loads(candidate)
|
| 212 |
+
except json.JSONDecodeError:
|
| 213 |
+
break
|
| 214 |
+
raise ValueError(f"AI returned non-JSON: {raw[:200]}")
|
| 215 |
+
|
| 216 |
+
def call_ai_json(messages: list, system: str = "", max_tokens: int = 2048,
|
| 217 |
+
api_key_row: dict | None = None) -> dict | list:
|
| 218 |
+
raw = call_ai(messages, system=system, max_tokens=max_tokens, api_key_row=api_key_row)
|
| 219 |
+
return _extract_json(raw)
|
app/core/file_reader.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""File text extractor — supports .docx, .pdf, .txt.
|
| 2 |
+
|
| 3 |
+
Reusable: copy this file to any Flask project's app/core/ directory.
|
| 4 |
+
Dependencies: pypdf>=4.0 (for PDF support — add to requirements.txt)
|
| 5 |
+
DOCX and TXT use Python built-ins only (no extra packages needed).
|
| 6 |
+
"""
|
| 7 |
+
import io
|
| 8 |
+
import zipfile
|
| 9 |
+
import xml.etree.ElementTree as ET
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
|
| 12 |
+
ALLOWED_EXTENSIONS = {".pdf", ".docx", ".txt"}
|
| 13 |
+
MAX_FILE_SIZE = 10 * 1024 * 1024 # 10 MB
|
| 14 |
+
|
| 15 |
+
_WORD_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def extract_text(file_storage) -> str:
|
| 19 |
+
"""Extract plain text from a Werkzeug FileStorage object.
|
| 20 |
+
|
| 21 |
+
Supports .pdf, .docx, .txt files up to 10 MB.
|
| 22 |
+
Returns extracted text as a string.
|
| 23 |
+
Raises ValueError for unsupported types, oversized files, or parse errors.
|
| 24 |
+
"""
|
| 25 |
+
filename = file_storage.filename or ""
|
| 26 |
+
ext = Path(filename).suffix.lower()
|
| 27 |
+
|
| 28 |
+
if ext not in ALLOWED_EXTENSIONS:
|
| 29 |
+
raise ValueError(
|
| 30 |
+
f"Unsupported file type '{ext or '(none)'}'. Allowed: PDF, DOCX, TXT"
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
data = file_storage.read()
|
| 34 |
+
if len(data) > MAX_FILE_SIZE:
|
| 35 |
+
raise ValueError("File too large (max 10 MB)")
|
| 36 |
+
if not data:
|
| 37 |
+
raise ValueError("File is empty")
|
| 38 |
+
|
| 39 |
+
if ext == ".txt":
|
| 40 |
+
return data.decode("utf-8", errors="replace").strip()
|
| 41 |
+
if ext == ".docx":
|
| 42 |
+
return _read_docx(io.BytesIO(data))
|
| 43 |
+
if ext == ".pdf":
|
| 44 |
+
return _read_pdf(io.BytesIO(data))
|
| 45 |
+
|
| 46 |
+
raise ValueError(f"Unhandled extension: {ext}")
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def _read_docx(stream: io.BytesIO) -> str:
|
| 50 |
+
"""Extract text from a .docx file using built-in zipfile + xml.etree (no deps)."""
|
| 51 |
+
try:
|
| 52 |
+
with zipfile.ZipFile(stream) as z:
|
| 53 |
+
with z.open("word/document.xml") as f:
|
| 54 |
+
tree = ET.parse(f)
|
| 55 |
+
except (zipfile.BadZipFile, KeyError) as exc:
|
| 56 |
+
raise ValueError(f"Could not read Word document: {exc}")
|
| 57 |
+
|
| 58 |
+
root = tree.getroot()
|
| 59 |
+
paragraphs = []
|
| 60 |
+
for para in root.iter(f"{{{_WORD_NS}}}p"):
|
| 61 |
+
# Collect all text runs, preserving spaces
|
| 62 |
+
parts = []
|
| 63 |
+
for node in para.iter():
|
| 64 |
+
if node.tag == f"{{{_WORD_NS}}}t" and node.text:
|
| 65 |
+
parts.append(node.text)
|
| 66 |
+
elif node.tag == f"{{{_WORD_NS}}}br":
|
| 67 |
+
parts.append("\n")
|
| 68 |
+
text = "".join(parts).strip()
|
| 69 |
+
if text:
|
| 70 |
+
paragraphs.append(text)
|
| 71 |
+
|
| 72 |
+
text = "\n\n".join(paragraphs)
|
| 73 |
+
if not text.strip():
|
| 74 |
+
raise ValueError("No readable text found in the Word document")
|
| 75 |
+
return text
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def _read_pdf(stream: io.BytesIO) -> str:
|
| 79 |
+
"""Extract text from a PDF using pypdf."""
|
| 80 |
+
try:
|
| 81 |
+
from pypdf import PdfReader
|
| 82 |
+
except ImportError:
|
| 83 |
+
raise ValueError("pypdf not installed — run: pip install pypdf")
|
| 84 |
+
|
| 85 |
+
try:
|
| 86 |
+
reader = PdfReader(stream)
|
| 87 |
+
except Exception as exc:
|
| 88 |
+
raise ValueError(f"Could not read PDF: {exc}")
|
| 89 |
+
|
| 90 |
+
pages = []
|
| 91 |
+
for page in reader.pages:
|
| 92 |
+
text = page.extract_text() or ""
|
| 93 |
+
if text.strip():
|
| 94 |
+
pages.append(text.strip())
|
| 95 |
+
|
| 96 |
+
text = "\n\n".join(pages)
|
| 97 |
+
if not text.strip():
|
| 98 |
+
raise ValueError("No readable text found in the PDF (may be image-based)")
|
| 99 |
+
return text
|
app/home/__init__.py
ADDED
|
File without changes
|
app/home/__pycache__/__init__.cpython-314.pyc
ADDED
|
Binary file (162 Bytes). View file
|
|
|
app/home/__pycache__/routes.cpython-314.pyc
ADDED
|
Binary file (565 Bytes). View file
|
|
|
app/home/routes.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""DevKit landing page."""
|
| 2 |
+
from flask import Blueprint, render_template
|
| 3 |
+
|
| 4 |
+
bp = Blueprint("home", __name__, template_folder="templates")
|
| 5 |
+
|
| 6 |
+
@bp.route("/")
|
| 7 |
+
def index():
|
| 8 |
+
return render_template("home/index.html")
|
app/home/templates/home/index.html
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
{% block title %}DevKit — Developer Toolkit{% endblock %}
|
| 3 |
+
|
| 4 |
+
{% block content %}
|
| 5 |
+
<div class="flex flex-col h-screen overflow-hidden">
|
| 6 |
+
|
| 7 |
+
<!-- Nav: same h-14 pattern as all tools -->
|
| 8 |
+
<nav class="flex items-center justify-between w-full px-6 py-2 bg-slate-50 h-14 z-50 border-b border-slate-200 shrink-0">
|
| 9 |
+
<div class="flex items-center gap-3">
|
| 10 |
+
<span class="w-8 h-8 rounded-lg bg-primary flex items-center justify-center">
|
| 11 |
+
<span class="material-symbols-outlined text-on-primary text-lg" style="font-variation-settings:'FILL' 1;">terminal</span>
|
| 12 |
+
</span>
|
| 13 |
+
<span class="text-lg font-semibold text-on-surface">DevKit</span>
|
| 14 |
+
</div>
|
| 15 |
+
<span class="text-xs font-semibold text-on-surface-variant bg-surface-container px-3 py-1.5 rounded-full">6 tools</span>
|
| 16 |
+
</nav>
|
| 17 |
+
|
| 18 |
+
<div class="flex flex-1 overflow-hidden">
|
| 19 |
+
|
| 20 |
+
<!-- Sidebar: same w-64 bg-slate-100 pattern as test-forge / agent-builder -->
|
| 21 |
+
<aside class="flex flex-col h-full w-64 p-4 gap-4 bg-slate-100 border-r border-slate-200 shrink-0 overflow-y-auto">
|
| 22 |
+
<div>
|
| 23 |
+
<p class="text-xs font-bold text-on-surface-variant uppercase tracking-widest mb-3">Categories</p>
|
| 24 |
+
<div class="space-y-1">
|
| 25 |
+
<div class="flex items-center gap-3 px-3 py-2 rounded-lg bg-white shadow-sm text-primary cursor-default">
|
| 26 |
+
<span class="material-symbols-outlined text-base">storage</span>
|
| 27 |
+
<span class="text-sm font-medium">Database</span>
|
| 28 |
+
</div>
|
| 29 |
+
<div class="flex items-center gap-3 px-3 py-2 rounded-lg text-on-surface-variant hover:bg-slate-200/60 transition-colors cursor-default">
|
| 30 |
+
<span class="material-symbols-outlined text-base">science</span>
|
| 31 |
+
<span class="text-sm font-medium">Code Quality</span>
|
| 32 |
+
</div>
|
| 33 |
+
<div class="flex items-center gap-3 px-3 py-2 rounded-lg text-on-surface-variant hover:bg-slate-200/60 transition-colors cursor-default">
|
| 34 |
+
<span class="material-symbols-outlined text-base">article</span>
|
| 35 |
+
<span class="text-sm font-medium">Documentation</span>
|
| 36 |
+
</div>
|
| 37 |
+
<div class="flex items-center gap-3 px-3 py-2 rounded-lg text-on-surface-variant hover:bg-slate-200/60 transition-colors cursor-default">
|
| 38 |
+
<span class="material-symbols-outlined text-base">history</span>
|
| 39 |
+
<span class="text-sm font-medium">Version History</span>
|
| 40 |
+
</div>
|
| 41 |
+
</div>
|
| 42 |
+
</div>
|
| 43 |
+
</aside>
|
| 44 |
+
|
| 45 |
+
<!-- Main workspace -->
|
| 46 |
+
<main class="flex-1 flex flex-col overflow-hidden bg-surface">
|
| 47 |
+
|
| 48 |
+
<!-- Action bar: matches the h-14 workspace bars inside tools -->
|
| 49 |
+
<div class="h-14 flex items-center justify-between px-6 bg-surface-container-low border-b border-slate-200 shrink-0">
|
| 50 |
+
<span class="text-xs font-bold text-on-surface-variant uppercase tracking-widest">Workspace</span>
|
| 51 |
+
<span class="text-xs text-outline">No signup required · Free</span>
|
| 52 |
+
</div>
|
| 53 |
+
|
| 54 |
+
<div class="flex-1 overflow-y-auto p-8">
|
| 55 |
+
|
| 56 |
+
<!-- Hero -->
|
| 57 |
+
<div class="mb-8">
|
| 58 |
+
<span class="text-[11px] font-semibold text-primary uppercase tracking-widest block mb-1">DevKit</span>
|
| 59 |
+
<h1 class="text-2xl font-bold text-on-surface tracking-tight">Developer Toolkit</h1>
|
| 60 |
+
<p class="text-sm text-on-surface-variant mt-2 max-w-xl leading-relaxed">Six tools for the day-to-day developer workflow — database auditing, SQL generation, test suites, documentation, and version history.</p>
|
| 61 |
+
</div>
|
| 62 |
+
|
| 63 |
+
<!-- Tool cards -->
|
| 64 |
+
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
|
| 65 |
+
<a href="/schema-detective/" class="group block bg-surface-container-lowest rounded-xl overflow-hidden border border-outline-variant/10 shadow-sm hover:shadow-md hover:-translate-y-0.5 transition-all">
|
| 66 |
+
<div class="h-11 flex items-center gap-3 px-4 bg-surface-container-low border-b border-slate-200/70 shrink-0">
|
| 67 |
+
<span class="w-6 h-6 rounded-md bg-primary flex items-center justify-center shrink-0">
|
| 68 |
+
<span class="material-symbols-outlined text-on-primary" style="font-size:14px;line-height:1;font-variation-settings:'FILL' 1;">table_view</span>
|
| 69 |
+
</span>
|
| 70 |
+
<span class="text-sm font-semibold text-on-surface">Schema Detective</span>
|
| 71 |
+
<span class="ml-auto flex items-center gap-0.5 text-xs font-medium text-outline group-hover:text-primary transition-colors">Open<span class="material-symbols-outlined text-sm leading-none ml-0.5">arrow_forward</span></span>
|
| 72 |
+
</div>
|
| 73 |
+
<div class="p-4">
|
| 74 |
+
<p class="text-sm text-on-surface-variant leading-relaxed">Audit database schemas for vulnerabilities, missing indexes, and design flaws.</p>
|
| 75 |
+
</div>
|
| 76 |
+
</a>
|
| 77 |
+
<a href="/test-forge/" class="group block bg-surface-container-lowest rounded-xl overflow-hidden border border-outline-variant/10 shadow-sm hover:shadow-md hover:-translate-y-0.5 transition-all">
|
| 78 |
+
<div class="h-11 flex items-center gap-3 px-4 bg-surface-container-low border-b border-slate-200/70 shrink-0">
|
| 79 |
+
<span class="w-6 h-6 rounded-md bg-primary flex items-center justify-center shrink-0">
|
| 80 |
+
<span class="material-symbols-outlined text-on-primary" style="font-size:14px;line-height:1;font-variation-settings:'FILL' 1;">science</span>
|
| 81 |
+
</span>
|
| 82 |
+
<span class="text-sm font-semibold text-on-surface">Test Forge</span>
|
| 83 |
+
<span class="ml-auto flex items-center gap-0.5 text-xs font-medium text-outline group-hover:text-primary transition-colors">Open<span class="material-symbols-outlined text-sm leading-none ml-0.5">arrow_forward</span></span>
|
| 84 |
+
</div>
|
| 85 |
+
<div class="p-4">
|
| 86 |
+
<p class="text-sm text-on-surface-variant leading-relaxed">Generate complete, runnable test suites across 8 frameworks from any source code.</p>
|
| 87 |
+
</div>
|
| 88 |
+
</a>
|
| 89 |
+
<a href="/sql-whisperer/" class="group block bg-surface-container-lowest rounded-xl overflow-hidden border border-outline-variant/10 shadow-sm hover:shadow-md hover:-translate-y-0.5 transition-all">
|
| 90 |
+
<div class="h-11 flex items-center gap-3 px-4 bg-surface-container-low border-b border-slate-200/70 shrink-0">
|
| 91 |
+
<span class="w-6 h-6 rounded-md bg-primary flex items-center justify-center shrink-0">
|
| 92 |
+
<span class="material-symbols-outlined text-on-primary" style="font-size:14px;line-height:1;font-variation-settings:'FILL' 1;">data_object</span>
|
| 93 |
+
</span>
|
| 94 |
+
<span class="text-sm font-semibold text-on-surface">SQL Whisperer</span>
|
| 95 |
+
<span class="ml-auto flex items-center gap-0.5 text-xs font-medium text-outline group-hover:text-primary transition-colors">Open<span class="material-symbols-outlined text-sm leading-none ml-0.5">arrow_forward</span></span>
|
| 96 |
+
</div>
|
| 97 |
+
<div class="p-4">
|
| 98 |
+
<p class="text-sm text-on-surface-variant leading-relaxed">Convert plain-English questions to SQL for PostgreSQL, MySQL, SQLite, and more.</p>
|
| 99 |
+
</div>
|
| 100 |
+
</a>
|
| 101 |
+
<a href="/doc-forge/" class="group block bg-surface-container-lowest rounded-xl overflow-hidden border border-outline-variant/10 shadow-sm hover:shadow-md hover:-translate-y-0.5 transition-all">
|
| 102 |
+
<div class="h-11 flex items-center gap-3 px-4 bg-surface-container-low border-b border-slate-200/70 shrink-0">
|
| 103 |
+
<span class="w-6 h-6 rounded-md bg-primary flex items-center justify-center shrink-0">
|
| 104 |
+
<span class="material-symbols-outlined text-on-primary" style="font-size:14px;line-height:1;font-variation-settings:'FILL' 1;">article</span>
|
| 105 |
+
</span>
|
| 106 |
+
<span class="text-sm font-semibold text-on-surface">Doc Forge</span>
|
| 107 |
+
<span class="ml-auto flex items-center gap-0.5 text-xs font-medium text-outline group-hover:text-primary transition-colors">Open<span class="material-symbols-outlined text-sm leading-none ml-0.5">arrow_forward</span></span>
|
| 108 |
+
</div>
|
| 109 |
+
<div class="p-4">
|
| 110 |
+
<p class="text-sm text-on-surface-variant leading-relaxed">Generate README, architecture overviews, and API docs from any GitHub repo.</p>
|
| 111 |
+
</div>
|
| 112 |
+
</a>
|
| 113 |
+
<a href="/changelog-ai/" class="group block bg-surface-container-lowest rounded-xl overflow-hidden border border-outline-variant/10 shadow-sm hover:shadow-md hover:-translate-y-0.5 transition-all">
|
| 114 |
+
<div class="h-11 flex items-center gap-3 px-4 bg-surface-container-low border-b border-slate-200/70 shrink-0">
|
| 115 |
+
<span class="w-6 h-6 rounded-md bg-primary flex items-center justify-center shrink-0">
|
| 116 |
+
<span class="material-symbols-outlined text-on-primary" style="font-size:14px;line-height:1;font-variation-settings:'FILL' 1;">update</span>
|
| 117 |
+
</span>
|
| 118 |
+
<span class="text-sm font-semibold text-on-surface">Changelog AI</span>
|
| 119 |
+
<span class="ml-auto flex items-center gap-0.5 text-xs font-medium text-outline group-hover:text-primary transition-colors">Open<span class="material-symbols-outlined text-sm leading-none ml-0.5">arrow_forward</span></span>
|
| 120 |
+
</div>
|
| 121 |
+
<div class="p-4">
|
| 122 |
+
<p class="text-sm text-on-surface-variant leading-relaxed">Turn commit history into changelogs for developers, users, or executives.</p>
|
| 123 |
+
</div>
|
| 124 |
+
</a>
|
| 125 |
+
<a href="/git-narrator/" class="group block bg-surface-container-lowest rounded-xl overflow-hidden border border-outline-variant/10 shadow-sm hover:shadow-md hover:-translate-y-0.5 transition-all">
|
| 126 |
+
<div class="h-11 flex items-center gap-3 px-4 bg-surface-container-low border-b border-slate-200/70 shrink-0">
|
| 127 |
+
<span class="w-6 h-6 rounded-md bg-primary flex items-center justify-center shrink-0">
|
| 128 |
+
<span class="material-symbols-outlined text-on-primary" style="font-size:14px;line-height:1;font-variation-settings:'FILL' 1;">history</span>
|
| 129 |
+
</span>
|
| 130 |
+
<span class="text-sm font-semibold text-on-surface">Git Narrator</span>
|
| 131 |
+
<span class="ml-auto flex items-center gap-0.5 text-xs font-medium text-outline group-hover:text-primary transition-colors">Open<span class="material-symbols-outlined text-sm leading-none ml-0.5">arrow_forward</span></span>
|
| 132 |
+
</div>
|
| 133 |
+
<div class="p-4">
|
| 134 |
+
<p class="text-sm text-on-surface-variant leading-relaxed">Transform raw git logs into readable project narratives and milestone timelines.</p>
|
| 135 |
+
</div>
|
| 136 |
+
</a>
|
| 137 |
+
</div>
|
| 138 |
+
|
| 139 |
+
</div>
|
| 140 |
+
</main>
|
| 141 |
+
</div>
|
| 142 |
+
|
| 143 |
+
<!-- Footer: same h-8 status bar as tools -->
|
| 144 |
+
<footer class="h-8 bg-surface-container-highest flex items-center justify-between px-6 text-[11px] font-medium text-on-surface-variant border-t border-slate-200 shrink-0">
|
| 145 |
+
<div class="flex items-center gap-4">
|
| 146 |
+
<div class="flex items-center gap-1.5"><div class="w-1.5 h-1.5 rounded-full bg-green-500"></div><span>DevKit</span></div>
|
| 147 |
+
</div>
|
| 148 |
+
<span>6 Tools · Free</span>
|
| 149 |
+
</footer>
|
| 150 |
+
|
| 151 |
+
</div>
|
| 152 |
+
{% endblock %}
|
app/templates/base.html
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html class="light" lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8"/>
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
| 6 |
+
<title>{% block title %}Competitive Intel{% endblock %}</title>
|
| 7 |
+
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
| 8 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"/>
|
| 9 |
+
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
| 10 |
+
<script id="tailwind-config">
|
| 11 |
+
tailwind.config = {
|
| 12 |
+
darkMode: "class",
|
| 13 |
+
theme: {
|
| 14 |
+
extend: {
|
| 15 |
+
colors: {
|
| 16 |
+
"error-container": "#fe8983",
|
| 17 |
+
"on-primary-fixed-variant": "#005bb0",
|
| 18 |
+
"on-secondary-fixed-variant": "#505d68",
|
| 19 |
+
"surface-bright": "#f8f9fa",
|
| 20 |
+
"surface": "#f8f9fa",
|
| 21 |
+
"surface-tint": "#005db5",
|
| 22 |
+
"primary": "#005db5",
|
| 23 |
+
"on-surface-variant": "#586064",
|
| 24 |
+
"primary-fixed-dim": "#bfd5ff",
|
| 25 |
+
"inverse-surface": "#0c0f10",
|
| 26 |
+
"on-tertiary": "#fbf7ff",
|
| 27 |
+
"background": "#f8f9fa",
|
| 28 |
+
"secondary": "#54616b",
|
| 29 |
+
"on-secondary-fixed": "#34414b",
|
| 30 |
+
"on-background": "#2b3437",
|
| 31 |
+
"surface-container": "#eaeff1",
|
| 32 |
+
"tertiary-dim": "#51516c",
|
| 33 |
+
"on-primary": "#f6f7ff",
|
| 34 |
+
"secondary-container": "#d7e4f1",
|
| 35 |
+
"tertiary": "#5d5c78",
|
| 36 |
+
"primary-dim": "#0052a0",
|
| 37 |
+
"on-error": "#fff7f6",
|
| 38 |
+
"surface-container-lowest": "#ffffff",
|
| 39 |
+
"outline-variant": "#abb3b7",
|
| 40 |
+
"on-secondary": "#f5f9ff",
|
| 41 |
+
"inverse-primary": "#5f9efb",
|
| 42 |
+
"tertiary-fixed-dim": "#cbc9e9",
|
| 43 |
+
"outline": "#737c7f",
|
| 44 |
+
"on-secondary-container": "#46535e",
|
| 45 |
+
"primary-container": "#d6e3ff",
|
| 46 |
+
"error-dim": "#4e0309",
|
| 47 |
+
"error": "#9f403d",
|
| 48 |
+
"secondary-fixed": "#d7e4f1",
|
| 49 |
+
"primary-fixed": "#d6e3ff",
|
| 50 |
+
"inverse-on-surface": "#9b9d9e",
|
| 51 |
+
"on-tertiary-container": "#4a4a65",
|
| 52 |
+
"tertiary-fixed": "#d9d7f8",
|
| 53 |
+
"on-tertiary-fixed-variant": "#54546f",
|
| 54 |
+
"on-surface": "#2b3437",
|
| 55 |
+
"on-primary-fixed": "#003f7d",
|
| 56 |
+
"on-primary-container": "#00519e",
|
| 57 |
+
"on-tertiary-fixed": "#383751",
|
| 58 |
+
"surface-container-low": "#f1f4f6",
|
| 59 |
+
"surface-dim": "#d1dce0",
|
| 60 |
+
"secondary-fixed-dim": "#c9d6e3",
|
| 61 |
+
"on-error-container": "#752121",
|
| 62 |
+
"surface-container-high": "#e3e9ec",
|
| 63 |
+
"surface-variant": "#dbe4e7",
|
| 64 |
+
"tertiary-container": "#d9d7f8",
|
| 65 |
+
"surface-container-highest": "#dbe4e7",
|
| 66 |
+
"secondary-dim": "#48555f"
|
| 67 |
+
},
|
| 68 |
+
borderRadius: {
|
| 69 |
+
"DEFAULT": "0.25rem",
|
| 70 |
+
"lg": "0.5rem",
|
| 71 |
+
"xl": "0.75rem",
|
| 72 |
+
"full": "9999px"
|
| 73 |
+
},
|
| 74 |
+
fontFamily: {
|
| 75 |
+
"headline": ["Inter", "sans-serif"],
|
| 76 |
+
"body": ["Inter", "sans-serif"],
|
| 77 |
+
"label": ["Inter", "sans-serif"]
|
| 78 |
+
}
|
| 79 |
+
}
|
| 80 |
+
}
|
| 81 |
+
}
|
| 82 |
+
</script>
|
| 83 |
+
<style>
|
| 84 |
+
body { font-family: 'Inter', sans-serif; }
|
| 85 |
+
.material-symbols-outlined {
|
| 86 |
+
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
| 87 |
+
vertical-align: middle;
|
| 88 |
+
}
|
| 89 |
+
.glass-effect { background: rgba(248,249,250,0.8); backdrop-filter: blur(12px); }
|
| 90 |
+
.ghost-border { outline: 1px solid rgba(171,179,183,0.15); }
|
| 91 |
+
::-webkit-scrollbar { width: 6px; }
|
| 92 |
+
::-webkit-scrollbar-track { background: transparent; }
|
| 93 |
+
::-webkit-scrollbar-thumb { background: #abb3b7; border-radius: 10px; }
|
| 94 |
+
|
| 95 |
+
/* Code editor (test-forge, schema-detective) */
|
| 96 |
+
.code-editor { font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'Consolas', monospace; }
|
| 97 |
+
.editor-bg { background: #1e1e2e; color: #cdd6f4; }
|
| 98 |
+
/* Arabic text (arabic-bench) */
|
| 99 |
+
.arabic-text { font-family: 'Segoe UI', Tahoma, Arial, sans-serif; }
|
| 100 |
+
/* Scrollbar utilities */
|
| 101 |
+
.no-scrollbar::-webkit-scrollbar { display: none; }
|
| 102 |
+
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
|
| 103 |
+
.custom-scrollbar::-webkit-scrollbar { width: 6px; }
|
| 104 |
+
.custom-scrollbar::-webkit-scrollbar-track { background: transparent; }
|
| 105 |
+
.custom-scrollbar::-webkit-scrollbar-thumb { background: #abb3b7; border-radius: 10px; }
|
| 106 |
+
/* Prompt-bench iOS overrides — scoped so they only affect prompt-bench internals */
|
| 107 |
+
.toolbar-blur { background: rgba(242,242,247,0.85); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); }
|
| 108 |
+
.btn-primary { display:inline-flex;align-items:center;gap:6px;padding:6px 14px;background:#007AFF;color:#fff;border-radius:8px;font-size:13px;font-weight:600;border:none;cursor:pointer; }
|
| 109 |
+
.btn-secondary{ display:inline-flex;align-items:center;gap:4px;padding:5px 10px;background:rgba(0,0,0,0.06);color:#3C3C43;border-radius:7px;font-size:12px;font-weight:500;border:none;cursor:pointer; }
|
| 110 |
+
.section-label{ font-size:11px;font-weight:600;color:#636366;text-transform:uppercase;letter-spacing:.06em; }
|
| 111 |
+
.sys-label { color: #1C1C1E; }
|
| 112 |
+
.text-sys-label { color: #1C1C1E; }
|
| 113 |
+
</style>
|
| 114 |
+
{% block extra_head %}{% endblock %}
|
| 115 |
+
<meta name="csrf-token" content="{{ csrf_token() }}"/>
|
| 116 |
+
</head>
|
| 117 |
+
<body class="bg-surface text-on-surface selection:bg-primary-container selection:text-on-primary-container">
|
| 118 |
+
{% block content %}{% endblock %}
|
| 119 |
+
{% block extra_scripts %}{% endblock %}
|
| 120 |
+
</body>
|
| 121 |
+
</html>
|
app/tools/__init__.py
ADDED
|
File without changes
|
app/tools/__pycache__/__init__.cpython-314.pyc
ADDED
|
Binary file (163 Bytes). View file
|
|
|
app/tools/changelog_ai/__init__.py
ADDED
|
File without changes
|
app/tools/changelog_ai/__pycache__/__init__.cpython-314.pyc
ADDED
|
Binary file (176 Bytes). View file
|
|
|
app/tools/changelog_ai/__pycache__/changelog.cpython-314.pyc
ADDED
|
Binary file (4.37 kB). View file
|
|
|
app/tools/changelog_ai/__pycache__/routes.cpython-314.pyc
ADDED
|
Binary file (2.13 kB). View file
|
|
|
app/tools/changelog_ai/changelog.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Changelog.ai — transforms raw git commits into polished release notes."""
|
| 2 |
+
from app.core.ai import call_ai_json
|
| 3 |
+
|
| 4 |
+
_LANG_NAMES = {
|
| 5 |
+
"en": "English", "ar": "Arabic", "fr": "French", "es": "Spanish",
|
| 6 |
+
"de": "German", "zh": "Chinese (Simplified)", "ja": "Japanese",
|
| 7 |
+
"pt": "Portuguese", "ru": "Russian", "tr": "Turkish",
|
| 8 |
+
"ko": "Korean", "nl": "Dutch", "it": "Italian", "pl": "Polish",
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
_SYSTEM = """You are an expert technical writer who specializes in software release communications.
|
| 12 |
+
You transform raw git commit history into clear, well-structured changelogs.
|
| 13 |
+
You adapt your tone precisely to the target audience without over-explaining.
|
| 14 |
+
Return ONLY valid JSON — no markdown fences, no preamble."""
|
| 15 |
+
|
| 16 |
+
_AUDIENCE_NOTES = {
|
| 17 |
+
"Developer": (
|
| 18 |
+
"Write for software engineers. Use precise technical language. "
|
| 19 |
+
"Reference module names, APIs, and implementation details. "
|
| 20 |
+
"Include all commit types: feat, fix, perf, refactor. "
|
| 21 |
+
"Descriptions should be concise and technically accurate. "
|
| 22 |
+
"Skip chore/docs commits unless they have real developer impact."
|
| 23 |
+
),
|
| 24 |
+
"User": (
|
| 25 |
+
"Write for end users with no technical background. "
|
| 26 |
+
"Translate technical changes into plain English benefits. "
|
| 27 |
+
"Focus on what users can NOW DO or what problems are SOLVED. "
|
| 28 |
+
"Only include feat and fix sections — no internal refactors, no chores. "
|
| 29 |
+
"Use active voice, 'You can now...', 'We fixed...' language."
|
| 30 |
+
),
|
| 31 |
+
"Executive": (
|
| 32 |
+
"Write for C-suite and business stakeholders. "
|
| 33 |
+
"Focus exclusively on business impact, risk reduction, and strategic value. "
|
| 34 |
+
"Be brief: one punchy sentence per item maximum. "
|
| 35 |
+
"Skip low-level fixes unless they had business/availability impact. "
|
| 36 |
+
"Frame everything in terms of outcome, not implementation."
|
| 37 |
+
),
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
_PROMPT_TMPL = """Transform the following git commits into a polished changelog.
|
| 41 |
+
|
| 42 |
+
TARGET AUDIENCE: {audience}
|
| 43 |
+
AUDIENCE INSTRUCTIONS: {audience_notes}
|
| 44 |
+
|
| 45 |
+
GIT COMMITS / RAW CHANGES:
|
| 46 |
+
---
|
| 47 |
+
{commits}
|
| 48 |
+
---
|
| 49 |
+
|
| 50 |
+
Return a JSON object with EXACTLY these keys:
|
| 51 |
+
{{
|
| 52 |
+
"sections": [
|
| 53 |
+
{{
|
| 54 |
+
"type": "<exactly one of: features | fixes | improvements | breaking>",
|
| 55 |
+
"items": [
|
| 56 |
+
{{
|
| 57 |
+
"title": "<short, clear title — max 8 words>",
|
| 58 |
+
"description": "<1-2 sentence description adapted to the target audience>"
|
| 59 |
+
}}
|
| 60 |
+
]
|
| 61 |
+
}}
|
| 62 |
+
]
|
| 63 |
+
}}
|
| 64 |
+
|
| 65 |
+
Rules:
|
| 66 |
+
- sections: only include types that have actual items; omit empty sections
|
| 67 |
+
- type ordering: breaking first, then features, then improvements, then fixes
|
| 68 |
+
- breaking: only if a commit explicitly breaks backward compatibility
|
| 69 |
+
- improvements: performance, refactors, dependency bumps with user impact
|
| 70 |
+
- Filter chore/doc-only commits unless they carry meaningful impact for this audience
|
| 71 |
+
- Each item title must be sentence-case and not end with a period
|
| 72 |
+
- Minimum 1 item per section; group related commits into one item when appropriate"""
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def generate_changelog(commits: str, audience: str, language: str = "en") -> dict:
|
| 76 |
+
"""Generate a polished changelog from raw git commits."""
|
| 77 |
+
audience_notes = _AUDIENCE_NOTES.get(audience, _AUDIENCE_NOTES["Developer"])
|
| 78 |
+
lang_name = _LANG_NAMES.get(language, "English")
|
| 79 |
+
lang_instruction = (
|
| 80 |
+
f"\n\nIMPORTANT: Write ALL text values in the JSON response in {lang_name}. "
|
| 81 |
+
"This includes all item titles and descriptions. "
|
| 82 |
+
"The JSON keys (type, sections, items, title, description) must remain in English."
|
| 83 |
+
) if language != "en" else ""
|
| 84 |
+
prompt = _PROMPT_TMPL.format(
|
| 85 |
+
audience=audience,
|
| 86 |
+
audience_notes=audience_notes,
|
| 87 |
+
commits=commits[:6000],
|
| 88 |
+
) + lang_instruction
|
| 89 |
+
result = call_ai_json(
|
| 90 |
+
[{"role": "user", "content": prompt}],
|
| 91 |
+
system=_SYSTEM,
|
| 92 |
+
max_tokens=2048,
|
| 93 |
+
)
|
| 94 |
+
return result or {}
|
app/tools/changelog_ai/routes.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Changelog.ai routes."""
|
| 2 |
+
from flask import Blueprint, render_template, request, jsonify
|
| 3 |
+
from .changelog import generate_changelog
|
| 4 |
+
|
| 5 |
+
bp = Blueprint("changelog_ai", __name__, template_folder="templates")
|
| 6 |
+
|
| 7 |
+
SUPPORTED_AUDIENCES = {"Developer", "User", "Executive"}
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
@bp.route("/")
|
| 11 |
+
def index():
|
| 12 |
+
return render_template("changelog_ai/index.html")
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
@bp.route("/api/generate", methods=["POST"])
|
| 16 |
+
def api_generate():
|
| 17 |
+
body = request.get_json(silent=True) or {}
|
| 18 |
+
commits = (body.get("commits") or "").strip()
|
| 19 |
+
audience = (body.get("audience") or "Developer").strip()
|
| 20 |
+
language = (body.get("language") or "en").strip()
|
| 21 |
+
|
| 22 |
+
if not commits:
|
| 23 |
+
return jsonify({"error": "Paste some commits or change notes first"}), 400
|
| 24 |
+
if len(commits) < 20:
|
| 25 |
+
return jsonify({"error": "Too short — paste at least a few commit messages"}), 400
|
| 26 |
+
if audience not in SUPPORTED_AUDIENCES:
|
| 27 |
+
return jsonify({"error": f"Unsupported audience: {audience}"}), 400
|
| 28 |
+
|
| 29 |
+
result = generate_changelog(commits, audience, language=language)
|
| 30 |
+
if not result:
|
| 31 |
+
return jsonify({"error": "AI failed to generate changelog — please try again"}), 502
|
| 32 |
+
return jsonify(result)
|
app/tools/changelog_ai/templates/changelog_ai/index.html
ADDED
|
@@ -0,0 +1,726 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
{% block title %}Changelog.ai — AI Release Notes Generator{% endblock %}
|
| 3 |
+
|
| 4 |
+
{% block content %}
|
| 5 |
+
<!-- Top Nav -->
|
| 6 |
+
<header class="flex justify-between items-center px-6 w-full sticky top-0 z-50 bg-white/80 backdrop-blur-md h-16 border-b border-slate-200/60">
|
| 7 |
+
<div class="flex items-center gap-6">
|
| 8 |
+
<span class="text-lg font-semibold text-on-surface tracking-tight">Changelog.ai</span>
|
| 9 |
+
<!-- Audience mode selector in nav -->
|
| 10 |
+
<div class="hidden md:flex gap-1 h-full items-end pb-0">
|
| 11 |
+
<button data-nav-audience="Developer"
|
| 12 |
+
class="nav-audience-btn px-4 pb-4 pt-4 text-sm font-medium text-on-surface-variant hover:text-on-surface transition-colors border-b-2 border-transparent">Developer</button>
|
| 13 |
+
<button data-nav-audience="User"
|
| 14 |
+
class="nav-audience-btn px-4 pb-4 pt-4 text-sm font-semibold text-blue-700 border-b-2 border-blue-700">User</button>
|
| 15 |
+
<button data-nav-audience="Executive"
|
| 16 |
+
class="nav-audience-btn px-4 pb-4 pt-4 text-sm font-medium text-on-surface-variant hover:text-on-surface transition-colors border-b-2 border-transparent">Executive</button>
|
| 17 |
+
</div>
|
| 18 |
+
</div>
|
| 19 |
+
<div class="flex items-center gap-3">
|
| 20 |
+
<button id="btn-download-pdf"
|
| 21 |
+
class="hidden bg-white border border-slate-200 hover:border-slate-300 text-on-surface px-4 py-2 rounded-lg text-sm font-medium transition-all shadow-sm flex items-center gap-2">
|
| 22 |
+
<span class="material-symbols-outlined text-sm" style="font-size:16px">picture_as_pdf</span>
|
| 23 |
+
Download PDF
|
| 24 |
+
</button>
|
| 25 |
+
<button id="btn-copy-md"
|
| 26 |
+
class="bg-primary hover:bg-primary-dim text-on-primary px-4 py-2 rounded-lg text-sm font-medium transition-all shadow-sm flex items-center gap-2">
|
| 27 |
+
<span id="copy-icon" class="material-symbols-outlined text-sm" style="font-size:16px">download</span>
|
| 28 |
+
<span id="copy-label">Copy Markdown</span>
|
| 29 |
+
</button>
|
| 30 |
+
</div>
|
| 31 |
+
</header>
|
| 32 |
+
|
| 33 |
+
<div class="flex h-[calc(100vh-4rem)] overflow-hidden">
|
| 34 |
+
|
| 35 |
+
<!-- Sidebar -->
|
| 36 |
+
<aside class="flex flex-col gap-2 p-4 h-full w-56 border-r border-slate-200/60 bg-slate-50 shrink-0">
|
| 37 |
+
<div class="flex items-center gap-3 px-2 mb-4">
|
| 38 |
+
<div class="w-8 h-8 rounded bg-primary flex items-center justify-center text-on-primary font-bold text-xs shadow-inner">CL</div>
|
| 39 |
+
<div>
|
| 40 |
+
<span class="text-sm font-semibold text-on-surface">Changelog.ai</span>
|
| 41 |
+
<span class="text-[10px] text-on-surface-variant uppercase tracking-wider font-bold block">Release Notes</span>
|
| 42 |
+
</div>
|
| 43 |
+
</div>
|
| 44 |
+
<nav class="flex-1 space-y-1">
|
| 45 |
+
<a class="flex items-center gap-3 px-3 py-2 bg-white text-blue-700 shadow-sm rounded-lg font-medium text-sm" href="#">
|
| 46 |
+
<span class="material-symbols-outlined" style="font-size:18px">history</span> Generator
|
| 47 |
+
</a>
|
| 48 |
+
</nav>
|
| 49 |
+
</aside>
|
| 50 |
+
|
| 51 |
+
<!-- Main Workspace -->
|
| 52 |
+
<main class="flex-1 flex overflow-hidden bg-surface">
|
| 53 |
+
|
| 54 |
+
<!-- Left Panel: Raw Input -->
|
| 55 |
+
<section class="w-2/5 p-5 border-r border-slate-200/60 flex flex-col gap-3 bg-white/50 shrink-0">
|
| 56 |
+
<div>
|
| 57 |
+
<span class="text-[10px] font-semibold text-on-surface-variant uppercase tracking-widest block mb-1">Source Input</span>
|
| 58 |
+
<h2 class="text-xl font-semibold text-on-surface">Raw Git History</h2>
|
| 59 |
+
</div>
|
| 60 |
+
|
| 61 |
+
<!-- Version + Date row -->
|
| 62 |
+
<div class="grid grid-cols-2 gap-3">
|
| 63 |
+
<div>
|
| 64 |
+
<label class="text-[10px] font-semibold text-on-surface-variant uppercase tracking-widest block mb-1">Version</label>
|
| 65 |
+
<input id="version-input" type="text" placeholder="v1.0.0"
|
| 66 |
+
class="w-full bg-white border border-slate-200 rounded-lg px-3 py-2 text-sm text-on-surface focus:ring-2 focus:ring-primary/20 outline-none">
|
| 67 |
+
</div>
|
| 68 |
+
<div>
|
| 69 |
+
<label class="text-[10px] font-semibold text-on-surface-variant uppercase tracking-widest block mb-1">Date</label>
|
| 70 |
+
<input id="date-input" type="date"
|
| 71 |
+
class="w-full bg-white border border-slate-200 rounded-lg px-3 py-2 text-sm text-on-surface focus:ring-2 focus:ring-primary/20 outline-none">
|
| 72 |
+
</div>
|
| 73 |
+
</div>
|
| 74 |
+
|
| 75 |
+
<!-- Language selector -->
|
| 76 |
+
<div>
|
| 77 |
+
<label class="text-[10px] font-semibold text-on-surface-variant uppercase tracking-widest block mb-1">Output Language</label>
|
| 78 |
+
<select id="lang-select"
|
| 79 |
+
class="w-full bg-white border border-slate-200 rounded-lg px-3 py-2 text-sm text-on-surface focus:ring-2 focus:ring-primary/20 outline-none">
|
| 80 |
+
<option value="en">English</option>
|
| 81 |
+
<option value="ar">Arabic</option>
|
| 82 |
+
<option value="fr">French</option>
|
| 83 |
+
<option value="es">Spanish</option>
|
| 84 |
+
<option value="de">German</option>
|
| 85 |
+
<option value="zh">Chinese (Simplified)</option>
|
| 86 |
+
<option value="ja">Japanese</option>
|
| 87 |
+
<option value="pt">Portuguese</option>
|
| 88 |
+
<option value="ru">Russian</option>
|
| 89 |
+
<option value="tr">Turkish</option>
|
| 90 |
+
<option value="ko">Korean</option>
|
| 91 |
+
<option value="nl">Dutch</option>
|
| 92 |
+
<option value="it">Italian</option>
|
| 93 |
+
<option value="pl">Polish</option>
|
| 94 |
+
</select>
|
| 95 |
+
</div>
|
| 96 |
+
|
| 97 |
+
<!-- Commits textarea -->
|
| 98 |
+
<div class="flex-1 relative">
|
| 99 |
+
<textarea id="commits-input"
|
| 100 |
+
class="w-full h-full bg-surface-container-lowest border-none ghost-border rounded-xl p-4 font-mono text-sm text-on-surface-variant focus:ring-2 focus:ring-primary/10 resize-none shadow-sm"
|
| 101 |
+
spellcheck="false"
|
| 102 |
+
placeholder="feat(api): add auth endpoints (f2a3b4)
|
| 103 |
+
fix(worker): resolve memory leak in background worker (9a8b7c)
|
| 104 |
+
perf(db): optimize query latency (d4e5f6)
|
| 105 |
+
feat(ui): implement new dashboard layouts (1a2b3c)
|
| 106 |
+
feat(auth): sso integration with Okta (5e6f7g)
|
| 107 |
+
chore(deps): bump tailwindcss from 3.0 to 3.4
|
| 108 |
+
docs: update readme with deployment steps
|
| 109 |
+
refactor: clean up legacy charting utility"></textarea>
|
| 110 |
+
</div>
|
| 111 |
+
|
| 112 |
+
<div class="flex items-center gap-2">
|
| 113 |
+
<button id="btn-generate"
|
| 114 |
+
class="flex-1 bg-primary hover:bg-primary-dim text-on-primary py-3 rounded-xl font-semibold flex items-center justify-center gap-3 shadow-lg shadow-primary/20 transition-all active:scale-[0.98]">
|
| 115 |
+
<span class="material-symbols-outlined">auto_awesome</span>
|
| 116 |
+
Generate Polished Changelog
|
| 117 |
+
</button>
|
| 118 |
+
<button id="btn-try-demo"
|
| 119 |
+
class="shrink-0 bg-white border border-slate-200 hover:border-primary hover:text-primary text-on-surface-variant px-4 py-3 rounded-xl text-sm font-medium transition-all flex items-center gap-2 shadow-sm"
|
| 120 |
+
title="Load a sample to try the generator">
|
| 121 |
+
<span class="material-symbols-outlined" style="font-size:16px">play_circle</span>
|
| 122 |
+
Try a demo
|
| 123 |
+
</button>
|
| 124 |
+
</div>
|
| 125 |
+
</section>
|
| 126 |
+
|
| 127 |
+
<!-- Right Panel: Preview -->
|
| 128 |
+
<section class="flex-1 p-6 overflow-y-auto bg-surface-container-low flex flex-col gap-5">
|
| 129 |
+
|
| 130 |
+
<!-- Audience Toggle Pills -->
|
| 131 |
+
<div class="flex items-center justify-center shrink-0">
|
| 132 |
+
<div class="bg-surface-container-high p-1 rounded-xl flex gap-1">
|
| 133 |
+
<button data-pill-audience="Developer"
|
| 134 |
+
class="pill-btn px-4 py-2 text-sm font-medium rounded-lg text-on-surface-variant hover:text-on-surface transition-all">Developer</button>
|
| 135 |
+
<button data-pill-audience="User"
|
| 136 |
+
class="pill-btn px-6 py-2 text-sm font-semibold rounded-lg bg-white text-primary shadow-sm">User</button>
|
| 137 |
+
<button data-pill-audience="Executive"
|
| 138 |
+
class="pill-btn px-4 py-2 text-sm font-medium rounded-lg text-on-surface-variant hover:text-on-surface transition-all">Executive</button>
|
| 139 |
+
</div>
|
| 140 |
+
</div>
|
| 141 |
+
|
| 142 |
+
<!-- States -->
|
| 143 |
+
<div id="empty-state" class="flex-1 flex flex-col items-center justify-center text-center text-on-surface-variant">
|
| 144 |
+
<span class="material-symbols-outlined text-5xl text-outline mb-4">auto_awesome</span>
|
| 145 |
+
<p class="text-sm font-medium">Paste your commits and click Generate</p>
|
| 146 |
+
<p class="text-xs text-outline mt-1">Set version, audience, and language, then generate</p>
|
| 147 |
+
</div>
|
| 148 |
+
|
| 149 |
+
<div id="loading-state" class="hidden flex-1 flex flex-col items-center justify-center gap-4">
|
| 150 |
+
<div class="w-10 h-10 border-2 border-primary border-t-transparent rounded-full animate-spin"></div>
|
| 151 |
+
<p class="text-sm text-on-surface-variant">Generating changelog…</p>
|
| 152 |
+
</div>
|
| 153 |
+
|
| 154 |
+
<div id="error-state" class="hidden flex-1 flex flex-col items-center justify-center text-center">
|
| 155 |
+
<span class="material-symbols-outlined text-error text-4xl mb-3">error</span>
|
| 156 |
+
<p id="error-msg" class="text-sm text-error font-medium px-8"></p>
|
| 157 |
+
</div>
|
| 158 |
+
|
| 159 |
+
<!-- GitHub-style Preview Card -->
|
| 160 |
+
<div id="results-state" class="hidden max-w-3xl mx-auto w-full flex flex-col gap-4">
|
| 161 |
+
<div class="bg-white rounded-2xl shadow-sm border border-slate-200/50 p-8">
|
| 162 |
+
<!-- Release Header -->
|
| 163 |
+
<header class="mb-8 border-b border-slate-100 pb-6">
|
| 164 |
+
<div class="flex items-start justify-between mb-2">
|
| 165 |
+
<div>
|
| 166 |
+
<h1 id="release-title" class="text-3xl font-bold text-on-surface tracking-tight">Latest Release</h1>
|
| 167 |
+
<p id="release-meta" class="text-sm text-on-surface-variant mt-1"></p>
|
| 168 |
+
</div>
|
| 169 |
+
<span id="audience-badge" class="bg-secondary-container text-on-secondary-container text-xs font-bold px-3 py-1 rounded-full mt-1">User</span>
|
| 170 |
+
</div>
|
| 171 |
+
</header>
|
| 172 |
+
<!-- Sections rendered by JS -->
|
| 173 |
+
<div id="changelog-sections" class="space-y-8"></div>
|
| 174 |
+
</div>
|
| 175 |
+
|
| 176 |
+
<!-- Bento stats -->
|
| 177 |
+
<div class="grid grid-cols-3 gap-3">
|
| 178 |
+
<div class="col-span-2 bg-white/60 p-5 rounded-2xl ghost-border flex items-center gap-4">
|
| 179 |
+
<div class="p-3 bg-primary-container rounded-xl">
|
| 180 |
+
<span class="material-symbols-outlined text-primary" style="font-size:20px">checklist</span>
|
| 181 |
+
</div>
|
| 182 |
+
<div>
|
| 183 |
+
<p class="text-xs font-bold text-on-surface-variant uppercase tracking-wider">Changes</p>
|
| 184 |
+
<p id="stat-changes" class="text-lg font-bold text-on-surface">—</p>
|
| 185 |
+
</div>
|
| 186 |
+
</div>
|
| 187 |
+
<div class="bg-white/60 p-5 rounded-2xl ghost-border flex flex-col justify-center">
|
| 188 |
+
<p class="text-xs font-bold text-on-surface-variant uppercase tracking-wider mb-1">Audience</p>
|
| 189 |
+
<p id="stat-audience" class="text-xl font-bold text-on-surface">—</p>
|
| 190 |
+
</div>
|
| 191 |
+
</div>
|
| 192 |
+
</div>
|
| 193 |
+
|
| 194 |
+
</section>
|
| 195 |
+
</main>
|
| 196 |
+
</div>
|
| 197 |
+
{% endblock %}
|
| 198 |
+
|
| 199 |
+
{% block extra_scripts %}
|
| 200 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js"></script>
|
| 201 |
+
<script>
|
| 202 |
+
(function () {
|
| 203 |
+
const CSRF = document.querySelector('meta[name="csrf-token"]').content;
|
| 204 |
+
|
| 205 |
+
let selectedAudience = 'User';
|
| 206 |
+
let lastResult = null;
|
| 207 |
+
|
| 208 |
+
const commitsInput = document.getElementById('commits-input');
|
| 209 |
+
const versionInput = document.getElementById('version-input');
|
| 210 |
+
const dateInput = document.getElementById('date-input');
|
| 211 |
+
const langSelect = document.getElementById('lang-select');
|
| 212 |
+
const btnGenerate = document.getElementById('btn-generate');
|
| 213 |
+
const btnCopyMd = document.getElementById('btn-copy-md');
|
| 214 |
+
const btnDownloadPdf = document.getElementById('btn-download-pdf');
|
| 215 |
+
const copyIcon = document.getElementById('copy-icon');
|
| 216 |
+
const copyLabel = document.getElementById('copy-label');
|
| 217 |
+
|
| 218 |
+
const emptyState = document.getElementById('empty-state');
|
| 219 |
+
const loadingState = document.getElementById('loading-state');
|
| 220 |
+
const errorState = document.getElementById('error-state');
|
| 221 |
+
const resultsState = document.getElementById('results-state');
|
| 222 |
+
const errorMsg = document.getElementById('error-msg');
|
| 223 |
+
const changelogSections = document.getElementById('changelog-sections');
|
| 224 |
+
const audienceBadge = document.getElementById('audience-badge');
|
| 225 |
+
const releaseTitle = document.getElementById('release-title');
|
| 226 |
+
const releaseMeta = document.getElementById('release-meta');
|
| 227 |
+
const statChanges = document.getElementById('stat-changes');
|
| 228 |
+
const statAudience = document.getElementById('stat-audience');
|
| 229 |
+
|
| 230 |
+
// Set default date to today
|
| 231 |
+
dateInput.valueAsDate = new Date();
|
| 232 |
+
|
| 233 |
+
const SECTION_META = {
|
| 234 |
+
features: { emoji: '\u2728', label: 'New Features', bg: 'bg-blue-50', dot: 'bg-blue-500', pdfColor: [59, 130, 246] },
|
| 235 |
+
fixes: { emoji: '\uD83D\uDC1B', label: 'Bug Fixes', bg: 'bg-red-50', dot: 'bg-red-400', pdfColor: [239, 68, 68] },
|
| 236 |
+
improvements: { emoji: '\uD83D\uDE80', label: 'Improvements',bg: 'bg-emerald-50', dot: 'bg-emerald-500', pdfColor: [16, 185, 129] },
|
| 237 |
+
breaking: { emoji: '\u26A0\uFE0F', label: 'Breaking Changes', bg: 'bg-amber-50', dot: 'bg-amber-500', pdfColor: [245, 158, 11] },
|
| 238 |
+
};
|
| 239 |
+
|
| 240 |
+
const SECTION_LABELS = {
|
| 241 |
+
en: { features: 'New Features', fixes: 'Bug Fixes', improvements: 'Improvements', breaking: 'Breaking Changes' },
|
| 242 |
+
ar: { features: '\u0645\u064a\u0632\u0627\u062a \u062c\u062f\u064a\u062f\u0629', fixes: '\u0625\u0635\u0644\u0627\u062d \u0627\u0644\u0623\u062e\u0637\u0627\u0621', improvements: '\u062a\u062d\u0633\u064a\u0646\u0627\u062a', breaking: '\u062a\u063a\u064a\u064a\u0631\u0627\u062a \u062c\u0630\u0631\u064a\u0629' },
|
| 243 |
+
fr: { features: 'Nouvelles fonctionnalit\u00e9s', fixes: 'Correctifs', improvements: 'Am\u00e9liorations', breaking: 'Changements majeurs' },
|
| 244 |
+
es: { features: 'Nuevas funciones', fixes: 'Correcciones', improvements: 'Mejoras', breaking: 'Cambios importantes' },
|
| 245 |
+
de: { features: 'Neue Funktionen', fixes: 'Fehlerbehebungen', improvements: 'Verbesserungen', breaking: 'Grundlegende \u00c4nderungen' },
|
| 246 |
+
zh: { features: '\u65b0\u529f\u80fd', fixes: '\u9519\u8bef\u4fee\u590d', improvements: '\u6539\u8fdb', breaking: '\u91cd\u5927\u53d8\u66f4' },
|
| 247 |
+
ja: { features: '\u65b0\u6a5f\u80fd', fixes: '\u30d0\u30b0\u4fee\u6b63', improvements: '\u6539\u5584', breaking: '\u7834\u58ca\u7684\u5909\u66f4' },
|
| 248 |
+
pt: { features: 'Novos recursos', fixes: 'Corre\u00e7\u00f5es', improvements: 'Melhorias', breaking: 'Mudan\u00e7as importantes' },
|
| 249 |
+
ru: { features: '\u041d\u043e\u0432\u044b\u0435 \u0444\u0443\u043d\u043a\u0446\u0438\u0438', fixes: '\u0418\u0441\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u043e\u0448\u0438\u0431\u043e\u043a', improvements: '\u0423\u043b\u0443\u0447\u0448\u0435\u043d\u0438\u044f', breaking: '\u041a\u0440\u0438\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0435 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f' },
|
| 250 |
+
tr: { features: 'Yeni \u00f6zellikler', fixes: 'Hata d\u00fczeltmeleri', improvements: '\u0130yile\u015ftirmeler', breaking: 'B\u00fcy\u00fck de\u011fi\u015fiklikler' },
|
| 251 |
+
ko: { features: '\uc0c8\ub85c\uc6b4 \uae30\ub2a5', fixes: '\ubc84\uadf8 \uc218\uc815', improvements: '\uac1c\uc120 \uc0ac\ud56d', breaking: '\uc8fc\uc694 \ubcc0\uacbd \uc0ac\ud56d' },
|
| 252 |
+
nl: { features: 'Nieuwe functies', fixes: 'Bugfixes', improvements: 'Verbeteringen', breaking: 'Grote wijzigingen' },
|
| 253 |
+
it: { features: 'Nuove funzionalit\u00e0', fixes: 'Correzioni di bug', improvements: 'Miglioramenti', breaking: 'Modifiche importanti' },
|
| 254 |
+
pl: { features: 'Nowe funkcje', fixes: 'Poprawki b\u0142\u0119d\u00f3w', improvements: 'Ulepszenia', breaking: 'Prze\u0142omowe zmiany' },
|
| 255 |
+
};
|
| 256 |
+
|
| 257 |
+
function getSectionLabel(type) {
|
| 258 |
+
const lang = langSelect.value;
|
| 259 |
+
const labels = SECTION_LABELS[lang] || SECTION_LABELS['en'];
|
| 260 |
+
return labels[type] || SECTION_META[type]?.label || type;
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
const RTL_LANGS = new Set(['ar']);
|
| 264 |
+
function isRtl() { return RTL_LANGS.has(langSelect.value); }
|
| 265 |
+
|
| 266 |
+
// html2pdf screenshots the DOM, so all languages/scripts render correctly.
|
| 267 |
+
|
| 268 |
+
// ── Audience sync ─────────────────────────────────────────────────────────
|
| 269 |
+
function setAudience(aud) {
|
| 270 |
+
selectedAudience = aud;
|
| 271 |
+
document.querySelectorAll('.nav-audience-btn').forEach(b => {
|
| 272 |
+
const on = b.dataset.navAudience === aud;
|
| 273 |
+
b.className = on
|
| 274 |
+
? 'nav-audience-btn px-4 pb-4 pt-4 text-sm font-semibold text-blue-700 border-b-2 border-blue-700'
|
| 275 |
+
: 'nav-audience-btn px-4 pb-4 pt-4 text-sm font-medium text-on-surface-variant hover:text-on-surface transition-colors border-b-2 border-transparent';
|
| 276 |
+
});
|
| 277 |
+
document.querySelectorAll('.pill-btn').forEach(b => {
|
| 278 |
+
const on = b.dataset.pillAudience === aud;
|
| 279 |
+
b.className = on
|
| 280 |
+
? 'pill-btn px-6 py-2 text-sm font-semibold rounded-lg bg-white text-primary shadow-sm'
|
| 281 |
+
: 'pill-btn px-4 py-2 text-sm font-medium rounded-lg text-on-surface-variant hover:text-on-surface transition-all';
|
| 282 |
+
});
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
document.querySelectorAll('.nav-audience-btn').forEach(b =>
|
| 286 |
+
b.addEventListener('click', () => setAudience(b.dataset.navAudience)));
|
| 287 |
+
document.querySelectorAll('.pill-btn').forEach(b =>
|
| 288 |
+
b.addEventListener('click', () => setAudience(b.dataset.pillAudience)));
|
| 289 |
+
|
| 290 |
+
setAudience('User');
|
| 291 |
+
|
| 292 |
+
// ── Generate ─────────────────────────────────────────────────────────────
|
| 293 |
+
btnGenerate.addEventListener('click', generate);
|
| 294 |
+
|
| 295 |
+
async function generate() {
|
| 296 |
+
const commits = commitsInput.value.trim();
|
| 297 |
+
if (!commits) { showError('Paste some commit messages first.'); return; }
|
| 298 |
+
|
| 299 |
+
showState('loading');
|
| 300 |
+
btnDownloadPdf.classList.add('hidden');
|
| 301 |
+
|
| 302 |
+
try {
|
| 303 |
+
const res = await fetch('/api/generate', {
|
| 304 |
+
method: 'POST',
|
| 305 |
+
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': CSRF },
|
| 306 |
+
body: JSON.stringify({
|
| 307 |
+
commits,
|
| 308 |
+
audience: selectedAudience,
|
| 309 |
+
language: langSelect.value,
|
| 310 |
+
})
|
| 311 |
+
});
|
| 312 |
+
const data = await res.json();
|
| 313 |
+
if (!res.ok) { showError(data.error || 'Generation failed — try again.'); return; }
|
| 314 |
+
lastResult = data;
|
| 315 |
+
renderChangelog(data);
|
| 316 |
+
showState('results');
|
| 317 |
+
btnDownloadPdf.classList.remove('hidden');
|
| 318 |
+
} catch (_) {
|
| 319 |
+
showError('Network error — is the server running?');
|
| 320 |
+
}
|
| 321 |
+
}
|
| 322 |
+
|
| 323 |
+
// ── Render changelog ──────────────────────────────────────────────────────
|
| 324 |
+
function renderChangelog(data) {
|
| 325 |
+
changelogSections.textContent = '';
|
| 326 |
+
const sections = data.sections || [];
|
| 327 |
+
let totalItems = 0;
|
| 328 |
+
|
| 329 |
+
// Apply RTL direction to the whole preview
|
| 330 |
+
resultsState.setAttribute('dir', isRtl() ? 'rtl' : 'ltr');
|
| 331 |
+
|
| 332 |
+
// Release header
|
| 333 |
+
const ver = versionInput.value.trim();
|
| 334 |
+
const date = dateInput.value;
|
| 335 |
+
releaseTitle.textContent = ver ? ver : 'Latest Release';
|
| 336 |
+
const parts = [];
|
| 337 |
+
if (date) {
|
| 338 |
+
const d = new Date(date + 'T00:00:00');
|
| 339 |
+
parts.push(d.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }));
|
| 340 |
+
}
|
| 341 |
+
releaseMeta.textContent = parts.join(' · ');
|
| 342 |
+
|
| 343 |
+
sections.forEach(section => {
|
| 344 |
+
const meta = SECTION_META[section.type] || SECTION_META.improvements;
|
| 345 |
+
const items = section.items || [];
|
| 346 |
+
if (!items.length) return;
|
| 347 |
+
totalItems += items.length;
|
| 348 |
+
|
| 349 |
+
const sectionEl = document.createElement('section');
|
| 350 |
+
|
| 351 |
+
const headerRow = document.createElement('div');
|
| 352 |
+
headerRow.className = 'flex items-center gap-3 mb-4';
|
| 353 |
+
|
| 354 |
+
const emojiBox = document.createElement('span');
|
| 355 |
+
emojiBox.className = 'w-9 h-9 rounded-xl ' + meta.bg + ' flex items-center justify-center text-lg';
|
| 356 |
+
emojiBox.textContent = meta.emoji;
|
| 357 |
+
|
| 358 |
+
const title = document.createElement('h3');
|
| 359 |
+
title.className = 'text-base font-bold text-on-surface';
|
| 360 |
+
title.textContent = getSectionLabel(section.type);
|
| 361 |
+
|
| 362 |
+
const countBadge = document.createElement('span');
|
| 363 |
+
countBadge.className = 'ml-auto text-xs font-semibold text-on-surface-variant bg-slate-100 px-2 py-0.5 rounded-full';
|
| 364 |
+
countBadge.textContent = items.length + ' item' + (items.length !== 1 ? 's' : '');
|
| 365 |
+
|
| 366 |
+
headerRow.appendChild(emojiBox);
|
| 367 |
+
headerRow.appendChild(title);
|
| 368 |
+
headerRow.appendChild(countBadge);
|
| 369 |
+
sectionEl.appendChild(headerRow);
|
| 370 |
+
|
| 371 |
+
const box = document.createElement('div');
|
| 372 |
+
box.className = 'bg-slate-50/50 rounded-xl p-5 ghost-border';
|
| 373 |
+
const ul = document.createElement('ul');
|
| 374 |
+
ul.className = 'space-y-4';
|
| 375 |
+
|
| 376 |
+
items.forEach(item => {
|
| 377 |
+
const li = document.createElement('li');
|
| 378 |
+
li.className = 'flex items-start gap-3';
|
| 379 |
+
|
| 380 |
+
const dot = document.createElement('div');
|
| 381 |
+
dot.className = 'w-1.5 h-1.5 rounded-full mt-2 shrink-0 ' + meta.dot;
|
| 382 |
+
|
| 383 |
+
const textBlock = document.createElement('div');
|
| 384 |
+
const itemTitle = document.createElement('p');
|
| 385 |
+
itemTitle.className = 'font-semibold text-sm text-on-surface';
|
| 386 |
+
itemTitle.textContent = item.title || '';
|
| 387 |
+
|
| 388 |
+
const itemDesc = document.createElement('p');
|
| 389 |
+
itemDesc.className = 'text-xs text-on-surface-variant mt-0.5 leading-relaxed';
|
| 390 |
+
itemDesc.textContent = item.description || '';
|
| 391 |
+
|
| 392 |
+
textBlock.appendChild(itemTitle);
|
| 393 |
+
if (item.description) textBlock.appendChild(itemDesc);
|
| 394 |
+
|
| 395 |
+
li.appendChild(dot);
|
| 396 |
+
li.appendChild(textBlock);
|
| 397 |
+
ul.appendChild(li);
|
| 398 |
+
});
|
| 399 |
+
|
| 400 |
+
box.appendChild(ul);
|
| 401 |
+
sectionEl.appendChild(box);
|
| 402 |
+
changelogSections.appendChild(sectionEl);
|
| 403 |
+
});
|
| 404 |
+
|
| 405 |
+
audienceBadge.textContent = selectedAudience;
|
| 406 |
+
statChanges.textContent = totalItems + ' item' + (totalItems !== 1 ? 's' : '');
|
| 407 |
+
statAudience.textContent = selectedAudience;
|
| 408 |
+
}
|
| 409 |
+
|
| 410 |
+
// ── Copy Markdown ─────────────────────────────────────────────────────────
|
| 411 |
+
btnCopyMd.addEventListener('click', () => {
|
| 412 |
+
if (!lastResult) return;
|
| 413 |
+
const ver = versionInput.value.trim();
|
| 414 |
+
const date = dateInput.value;
|
| 415 |
+
const lines = [];
|
| 416 |
+
if (ver) lines.push('# ' + ver, '');
|
| 417 |
+
if (date) lines.push('_Released ' + date + '_', '');
|
| 418 |
+
lines.push('**Audience: ' + selectedAudience + '**', '');
|
| 419 |
+
(lastResult.sections || []).forEach(section => {
|
| 420 |
+
const meta = SECTION_META[section.type] || SECTION_META.improvements;
|
| 421 |
+
lines.push('## ' + meta.emoji + ' ' + getSectionLabel(section.type));
|
| 422 |
+
lines.push('');
|
| 423 |
+
(section.items || []).forEach(item => {
|
| 424 |
+
lines.push('- **' + (item.title || '') + '** — ' + (item.description || ''));
|
| 425 |
+
});
|
| 426 |
+
lines.push('');
|
| 427 |
+
});
|
| 428 |
+
navigator.clipboard.writeText(lines.join('\n')).then(() => {
|
| 429 |
+
copyIcon.textContent = 'check';
|
| 430 |
+
copyLabel.textContent = 'Copied!';
|
| 431 |
+
setTimeout(() => {
|
| 432 |
+
copyIcon.textContent = 'download';
|
| 433 |
+
copyLabel.textContent = 'Copy Markdown';
|
| 434 |
+
}, 2000);
|
| 435 |
+
});
|
| 436 |
+
});
|
| 437 |
+
|
| 438 |
+
// ── PDF Export ────────────────────────────────────────────────────────────
|
| 439 |
+
btnDownloadPdf.addEventListener('click', downloadPDF);
|
| 440 |
+
|
| 441 |
+
function downloadPDF() {
|
| 442 |
+
if (!lastResult) return;
|
| 443 |
+
btnDownloadPdf.disabled = true;
|
| 444 |
+
btnDownloadPdf.textContent = 'Generating…';
|
| 445 |
+
const el = document.getElementById('results-state');
|
| 446 |
+
const ver = versionInput.value.trim();
|
| 447 |
+
const fname = (ver ? ver.replace(/\s+/g, '-') : 'changelog') + '-release-notes.pdf';
|
| 448 |
+
html2pdf().set({
|
| 449 |
+
margin: 10,
|
| 450 |
+
filename: fname,
|
| 451 |
+
image: { type: 'jpeg', quality: 0.98 },
|
| 452 |
+
html2canvas: { scale: 2, useCORS: true, logging: false, backgroundColor: '#f8fafc' },
|
| 453 |
+
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' },
|
| 454 |
+
pagebreak: { mode: ['avoid-all', 'css'] },
|
| 455 |
+
}).from(el).save().then(() => {
|
| 456 |
+
btnDownloadPdf.disabled = false;
|
| 457 |
+
btnDownloadPdf.textContent = 'Download PDF';
|
| 458 |
+
});
|
| 459 |
+
}
|
| 460 |
+
/* legacy jsPDF body
|
| 461 |
+
function downloadPDF_legacy() {
|
| 462 |
+
const { jsPDF } = window.jspdf;
|
| 463 |
+
const doc = new jsPDF('p', 'mm', 'a4');
|
| 464 |
+
const PW = 210, PH = 297, M = 14;
|
| 465 |
+
const CW = PW - M * 2;
|
| 466 |
+
|
| 467 |
+
const NAVY = [30, 41, 82];
|
| 468 |
+
const BLUE = [59, 130, 246];
|
| 469 |
+
const SLATE = [100, 116, 139];
|
| 470 |
+
const LIGHT = [241, 245, 249];
|
| 471 |
+
const WHITE = [255, 255, 255];
|
| 472 |
+
const DARK = [15, 23, 42];
|
| 473 |
+
|
| 474 |
+
const ver = versionInput.value.trim();
|
| 475 |
+
const date = dateInput.value;
|
| 476 |
+
let totalPages = 1;
|
| 477 |
+
let y = 0;
|
| 478 |
+
|
| 479 |
+
function addPage() {
|
| 480 |
+
doc.addPage();
|
| 481 |
+
totalPages++;
|
| 482 |
+
y = 20;
|
| 483 |
+
drawPageFooter();
|
| 484 |
+
}
|
| 485 |
+
|
| 486 |
+
function checkY(needed) {
|
| 487 |
+
if (y + needed > PH - 20) addPage();
|
| 488 |
+
}
|
| 489 |
+
|
| 490 |
+
function drawPageFooter() {
|
| 491 |
+
doc.setFillColor(...LIGHT);
|
| 492 |
+
doc.rect(0, PH - 12, PW, 12, 'F');
|
| 493 |
+
doc.setFontSize(7);
|
| 494 |
+
doc.setTextColor(...SLATE);
|
| 495 |
+
doc.text('Generated by Changelog.ai', M, PH - 5);
|
| 496 |
+
doc.text('Page ' + doc.getCurrentPageInfo().pageNumber, PW - M, PH - 5, { align: 'right' });
|
| 497 |
+
}
|
| 498 |
+
|
| 499 |
+
// ── Header ──────────────────────────────────────────────────────────────
|
| 500 |
+
doc.setFillColor(...NAVY);
|
| 501 |
+
doc.rect(0, 0, PW, 30, 'F');
|
| 502 |
+
doc.setFillColor(...BLUE);
|
| 503 |
+
doc.rect(0, 30, PW, 2.5, 'F');
|
| 504 |
+
|
| 505 |
+
doc.setTextColor(...WHITE);
|
| 506 |
+
doc.setFontSize(18);
|
| 507 |
+
doc.setFont('helvetica', 'bold');
|
| 508 |
+
doc.text('RELEASE NOTES', M, 19);
|
| 509 |
+
|
| 510 |
+
if (ver) {
|
| 511 |
+
doc.setFontSize(12);
|
| 512 |
+
doc.setFont('helvetica', 'normal');
|
| 513 |
+
doc.text(ver, PW - M, 19, { align: 'right' });
|
| 514 |
+
}
|
| 515 |
+
|
| 516 |
+
y = 40;
|
| 517 |
+
|
| 518 |
+
// ── Meta row ────────────────────────────────────────────────────────────
|
| 519 |
+
doc.setFontSize(9);
|
| 520 |
+
doc.setFont('helvetica', 'normal');
|
| 521 |
+
doc.setTextColor(...SLATE);
|
| 522 |
+
if (date) {
|
| 523 |
+
const d = new Date(date + 'T00:00:00');
|
| 524 |
+
const dateStr = d.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });
|
| 525 |
+
doc.text(dateStr, M, y);
|
| 526 |
+
}
|
| 527 |
+
doc.setFillColor(...BLUE);
|
| 528 |
+
doc.roundedRect(PW - M - 32, y - 4.5, 32, 6.5, 1.5, 1.5, 'F');
|
| 529 |
+
doc.setTextColor(...WHITE);
|
| 530 |
+
doc.setFontSize(7.5);
|
| 531 |
+
doc.setFont('helvetica', 'bold');
|
| 532 |
+
doc.text(selectedAudience.toUpperCase(), PW - M - 16, y - 0.2, { align: 'center' });
|
| 533 |
+
|
| 534 |
+
y += 10;
|
| 535 |
+
|
| 536 |
+
// ── Sections ─────────────────────────────────────────────────────────────
|
| 537 |
+
const rtl = isRtl();
|
| 538 |
+
const textX = rtl ? M + CW - 4 : M + 4;
|
| 539 |
+
const textAlign = rtl ? 'right' : 'left';
|
| 540 |
+
const itemX = rtl ? M + CW - 8 : M + 8;
|
| 541 |
+
const itemW = CW - 14;
|
| 542 |
+
const dotX = rtl ? M + CW - 3 : M + 3;
|
| 543 |
+
const countX = rtl ? M + 4 : M + CW - 4;
|
| 544 |
+
const countAlign = rtl ? 'left' : 'right';
|
| 545 |
+
const divX1 = rtl ? M : M + 6;
|
| 546 |
+
const divX2 = rtl ? M + CW - 6 : M + CW;
|
| 547 |
+
|
| 548 |
+
const sections = lastResult.sections || [];
|
| 549 |
+
sections.forEach(section => {
|
| 550 |
+
const meta = SECTION_META[section.type] || SECTION_META.improvements;
|
| 551 |
+
const items = section.items || [];
|
| 552 |
+
if (!items.length) return;
|
| 553 |
+
|
| 554 |
+
checkY(18);
|
| 555 |
+
|
| 556 |
+
// Section header band
|
| 557 |
+
const clr = meta.pdfColor || BLUE;
|
| 558 |
+
doc.setFillColor(clr[0] + Math.round((255 - clr[0]) * 0.88),
|
| 559 |
+
clr[1] + Math.round((255 - clr[1]) * 0.88),
|
| 560 |
+
clr[2] + Math.round((255 - clr[2]) * 0.88));
|
| 561 |
+
doc.roundedRect(M, y, CW, 9, 1.5, 1.5, 'F');
|
| 562 |
+
|
| 563 |
+
doc.setFontSize(9.5);
|
| 564 |
+
doc.setFont('helvetica', 'bold');
|
| 565 |
+
doc.setTextColor(...DARK);
|
| 566 |
+
doc.text(meta.emoji + ' ' + getSectionLabel(section.type), textX, y + 6.2, { align: textAlign });
|
| 567 |
+
|
| 568 |
+
const countStr = items.length + ' item' + (items.length !== 1 ? 's' : '');
|
| 569 |
+
doc.setFontSize(7.5);
|
| 570 |
+
doc.setFont('helvetica', 'normal');
|
| 571 |
+
doc.setTextColor(...SLATE);
|
| 572 |
+
doc.text(countStr, countX, y + 6.2, { align: countAlign });
|
| 573 |
+
|
| 574 |
+
y += 12;
|
| 575 |
+
|
| 576 |
+
items.forEach(item => {
|
| 577 |
+
const titleLines = doc.splitTextToSize(item.title || '', itemW);
|
| 578 |
+
const descLines = item.description
|
| 579 |
+
? doc.splitTextToSize(item.description, itemW)
|
| 580 |
+
: [];
|
| 581 |
+
const blockH = 4 + titleLines.length * 5 + (descLines.length > 0 ? 2 + descLines.length * 4.2 : 0) + 4;
|
| 582 |
+
checkY(blockH + 2);
|
| 583 |
+
|
| 584 |
+
// Dot
|
| 585 |
+
doc.setFillColor(...clr);
|
| 586 |
+
doc.circle(dotX, y + 2.8, 1, 'F');
|
| 587 |
+
|
| 588 |
+
// Title
|
| 589 |
+
doc.setFontSize(9);
|
| 590 |
+
doc.setFont('helvetica', 'bold');
|
| 591 |
+
doc.setTextColor(...DARK);
|
| 592 |
+
doc.text(titleLines, itemX, y + 4.5, { align: textAlign });
|
| 593 |
+
y += titleLines.length * 5;
|
| 594 |
+
|
| 595 |
+
// Description
|
| 596 |
+
if (descLines.length) {
|
| 597 |
+
doc.setFontSize(8);
|
| 598 |
+
doc.setFont('helvetica', 'normal');
|
| 599 |
+
doc.setTextColor(...SLATE);
|
| 600 |
+
doc.text(descLines, itemX, y + 3.5, { align: textAlign });
|
| 601 |
+
y += descLines.length * 4.2 + 2;
|
| 602 |
+
}
|
| 603 |
+
|
| 604 |
+
y += 4;
|
| 605 |
+
|
| 606 |
+
// Divider
|
| 607 |
+
doc.setDrawColor(226, 232, 240);
|
| 608 |
+
doc.setLineWidth(0.3);
|
| 609 |
+
doc.line(divX1, y, divX2, y);
|
| 610 |
+
y += 3;
|
| 611 |
+
});
|
| 612 |
+
|
| 613 |
+
y += 4;
|
| 614 |
+
});
|
| 615 |
+
|
| 616 |
+
// ── Stats footer area ────────────────────────────────────────────────────
|
| 617 |
+
checkY(24);
|
| 618 |
+
y += 2;
|
| 619 |
+
doc.setFillColor(...LIGHT);
|
| 620 |
+
doc.roundedRect(M, y, CW, 18, 2, 2, 'F');
|
| 621 |
+
|
| 622 |
+
let totalItems = 0;
|
| 623 |
+
sections.forEach(s => { totalItems += (s.items || []).length; });
|
| 624 |
+
|
| 625 |
+
doc.setFontSize(8);
|
| 626 |
+
doc.setFont('helvetica', 'bold');
|
| 627 |
+
doc.setTextColor(...SLATE);
|
| 628 |
+
doc.text('TOTAL CHANGES', M + 8, y + 7);
|
| 629 |
+
doc.setFontSize(13);
|
| 630 |
+
doc.setTextColor(...DARK);
|
| 631 |
+
doc.text(String(totalItems), M + 8, y + 14);
|
| 632 |
+
|
| 633 |
+
doc.setFontSize(8);
|
| 634 |
+
doc.setFont('helvetica', 'bold');
|
| 635 |
+
doc.setTextColor(...SLATE);
|
| 636 |
+
doc.text('AUDIENCE', PW / 2, y + 7, { align: 'center' });
|
| 637 |
+
doc.setFontSize(10);
|
| 638 |
+
doc.setTextColor(...DARK);
|
| 639 |
+
doc.setFont('helvetica', 'normal');
|
| 640 |
+
doc.text(selectedAudience, PW / 2, y + 14, { align: 'center' });
|
| 641 |
+
|
| 642 |
+
if (ver) {
|
| 643 |
+
doc.setFontSize(8);
|
| 644 |
+
doc.setFont('helvetica', 'bold');
|
| 645 |
+
doc.setTextColor(...SLATE);
|
| 646 |
+
doc.text('VERSION', M + CW - 8, y + 7, { align: 'right' });
|
| 647 |
+
doc.setFontSize(10);
|
| 648 |
+
doc.setFont('helvetica', 'normal');
|
| 649 |
+
doc.setTextColor(...DARK);
|
| 650 |
+
doc.text(ver, M + CW - 8, y + 14, { align: 'right' });
|
| 651 |
+
}
|
| 652 |
+
|
| 653 |
+
// Footer on first page
|
| 654 |
+
drawPageFooter();
|
| 655 |
+
|
| 656 |
+
const fname2 = (ver ? ver.replace(/\s+/g, '-') : 'changelog') + '-release-notes.pdf';
|
| 657 |
+
doc.save(fname2);
|
| 658 |
+
}*/
|
| 659 |
+
|
| 660 |
+
// ── State helpers ─────────────────────────────────────────────────────────
|
| 661 |
+
function showState(state) {
|
| 662 |
+
[emptyState, loadingState, errorState, resultsState].forEach(el => el.classList.add('hidden'));
|
| 663 |
+
const map = { empty: emptyState, loading: loadingState, error: errorState, results: resultsState };
|
| 664 |
+
if (map[state]) map[state].classList.remove('hidden');
|
| 665 |
+
}
|
| 666 |
+
|
| 667 |
+
function showError(msg) {
|
| 668 |
+
errorMsg.textContent = msg;
|
| 669 |
+
showState('error');
|
| 670 |
+
}
|
| 671 |
+
|
| 672 |
+
// ── Demo loader ───────────────────────────────────────────────────────────
|
| 673 |
+
const _DEMOS = [
|
| 674 |
+
{
|
| 675 |
+
version: 'v2.4.0',
|
| 676 |
+
commits: `- feat: add AI-powered auto-complete to search bar
|
| 677 |
+
- feat: support bulk export to CSV and Excel
|
| 678 |
+
- feat: dark mode toggle in user settings
|
| 679 |
+
- fix: dashboard charts not loading on Safari
|
| 680 |
+
- fix: email notifications sent twice on password reset
|
| 681 |
+
- fix: file uploads failing for files over 50MB
|
| 682 |
+
- perf: reduced page load time by 40% via lazy loading
|
| 683 |
+
- security: patched XSS vulnerability in comment fields`,
|
| 684 |
+
},
|
| 685 |
+
{
|
| 686 |
+
version: 'v3.1.2',
|
| 687 |
+
commits: `- feat: offline mode — browse and save content without internet
|
| 688 |
+
- feat: biometric login (Face ID / fingerprint)
|
| 689 |
+
- feat: share to WhatsApp and Instagram Stories
|
| 690 |
+
- fix: app crashing on Android 12 when opening camera
|
| 691 |
+
- fix: notifications not appearing on iOS 17
|
| 692 |
+
- fix: search results showing duplicates
|
| 693 |
+
- improvement: 60% faster image loading
|
| 694 |
+
- improvement: reduced battery drain by 25%`,
|
| 695 |
+
},
|
| 696 |
+
{
|
| 697 |
+
version: 'v1.8.0',
|
| 698 |
+
commits: `- feat: Claude 3 and GPT-4o model support
|
| 699 |
+
- feat: streaming responses with real-time token display
|
| 700 |
+
- feat: team workspaces with shared prompt libraries
|
| 701 |
+
- feat: version history for all prompts
|
| 702 |
+
- fix: API timeout errors on long responses
|
| 703 |
+
- fix: syntax highlighting broken for Python 3.12
|
| 704 |
+
- fix: keyboard shortcuts not working on Windows
|
| 705 |
+
- breaking: removed deprecated v1 API endpoints
|
| 706 |
+
- docs: updated quickstart guide and API reference`,
|
| 707 |
+
},
|
| 708 |
+
];
|
| 709 |
+
|
| 710 |
+
let _demoIndex = 0;
|
| 711 |
+
|
| 712 |
+
function loadDemo() {
|
| 713 |
+
const demo = _DEMOS[_demoIndex % _DEMOS.length];
|
| 714 |
+
_demoIndex++;
|
| 715 |
+
commitsInput.value = demo.commits;
|
| 716 |
+
versionInput.value = demo.version;
|
| 717 |
+
// Trigger generate
|
| 718 |
+
generate();
|
| 719 |
+
}
|
| 720 |
+
|
| 721 |
+
window.loadDemo = loadDemo;
|
| 722 |
+
|
| 723 |
+
document.getElementById('btn-try-demo').addEventListener('click', loadDemo);
|
| 724 |
+
})();
|
| 725 |
+
</script>
|
| 726 |
+
{% endblock %}
|
app/tools/doc_forge/__init__.py
ADDED
|
File without changes
|
app/tools/doc_forge/__pycache__/__init__.cpython-314.pyc
ADDED
|
Binary file (173 Bytes). View file
|
|
|
app/tools/doc_forge/__pycache__/db.cpython-314.pyc
ADDED
|
Binary file (5.29 kB). View file
|
|
|
app/tools/doc_forge/__pycache__/doc_generator.cpython-314.pyc
ADDED
|
Binary file (7.55 kB). View file
|
|
|
app/tools/doc_forge/__pycache__/github_fetcher.cpython-314.pyc
ADDED
|
Binary file (7.84 kB). View file
|
|
|
app/tools/doc_forge/__pycache__/routes.cpython-314.pyc
ADDED
|
Binary file (6.31 kB). View file
|
|
|
app/tools/doc_forge/db.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""SQLite cache for generated docs."""
|
| 2 |
+
import json
|
| 3 |
+
import sqlite3
|
| 4 |
+
import os
|
| 5 |
+
|
| 6 |
+
_SCHEMA = """
|
| 7 |
+
PRAGMA journal_mode=WAL;
|
| 8 |
+
PRAGMA foreign_keys=ON;
|
| 9 |
+
|
| 10 |
+
CREATE TABLE IF NOT EXISTS repos (
|
| 11 |
+
id INTEGER PRIMARY KEY,
|
| 12 |
+
owner TEXT NOT NULL,
|
| 13 |
+
repo TEXT NOT NULL,
|
| 14 |
+
info TEXT NOT NULL, -- JSON
|
| 15 |
+
tree TEXT NOT NULL, -- JSON array
|
| 16 |
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
| 17 |
+
UNIQUE(owner, repo)
|
| 18 |
+
);
|
| 19 |
+
|
| 20 |
+
CREATE TABLE IF NOT EXISTS docs (
|
| 21 |
+
id INTEGER PRIMARY KEY,
|
| 22 |
+
repo_id INTEGER NOT NULL REFERENCES repos(id) ON DELETE CASCADE,
|
| 23 |
+
doc_type TEXT NOT NULL, -- readme | architecture | api
|
| 24 |
+
content TEXT NOT NULL, -- JSON
|
| 25 |
+
generated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
| 26 |
+
UNIQUE(repo_id, doc_type)
|
| 27 |
+
);
|
| 28 |
+
|
| 29 |
+
CREATE INDEX IF NOT EXISTS idx_docs_repo ON docs(repo_id);
|
| 30 |
+
"""
|
| 31 |
+
|
| 32 |
+
def get_db(path: str) -> sqlite3.Connection:
|
| 33 |
+
con = sqlite3.connect(path)
|
| 34 |
+
con.row_factory = sqlite3.Row
|
| 35 |
+
con.executescript(_SCHEMA)
|
| 36 |
+
con.commit()
|
| 37 |
+
return con
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def upsert_repo(db, owner: str, repo: str, info: dict, tree: list) -> int:
|
| 41 |
+
db.execute("""
|
| 42 |
+
INSERT INTO repos (owner, repo, info, tree)
|
| 43 |
+
VALUES (?, ?, ?, ?)
|
| 44 |
+
ON CONFLICT(owner, repo) DO UPDATE SET
|
| 45 |
+
info=excluded.info, tree=excluded.tree, created_at=datetime('now')
|
| 46 |
+
""", (owner, repo, json.dumps(info), json.dumps(tree)))
|
| 47 |
+
db.commit()
|
| 48 |
+
row = db.execute("SELECT id FROM repos WHERE owner=? AND repo=?",
|
| 49 |
+
(owner, repo)).fetchone()
|
| 50 |
+
return row["id"]
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def upsert_doc(db, repo_id: int, doc_type: str, content: dict):
|
| 54 |
+
db.execute("""
|
| 55 |
+
INSERT INTO docs (repo_id, doc_type, content)
|
| 56 |
+
VALUES (?, ?, ?)
|
| 57 |
+
ON CONFLICT(repo_id, doc_type) DO UPDATE SET
|
| 58 |
+
content=excluded.content, generated_at=datetime('now')
|
| 59 |
+
""", (repo_id, doc_type, json.dumps(content)))
|
| 60 |
+
db.commit()
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def get_docs(db, owner: str, repo: str) -> dict | None:
|
| 64 |
+
row = db.execute("SELECT id FROM repos WHERE owner=? AND repo=?",
|
| 65 |
+
(owner, repo)).fetchone()
|
| 66 |
+
if not row:
|
| 67 |
+
return None
|
| 68 |
+
docs = db.execute("SELECT doc_type, content FROM docs WHERE repo_id=?",
|
| 69 |
+
(row["id"],)).fetchall()
|
| 70 |
+
if not docs:
|
| 71 |
+
return None
|
| 72 |
+
result = {}
|
| 73 |
+
for d in docs:
|
| 74 |
+
result[d["doc_type"]] = json.loads(d["content"])
|
| 75 |
+
return result
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def list_recent(db, limit: int = 10) -> list:
|
| 79 |
+
rows = db.execute("""
|
| 80 |
+
SELECT r.owner, r.repo, r.info, r.created_at,
|
| 81 |
+
COUNT(d.id) as doc_count
|
| 82 |
+
FROM repos r LEFT JOIN docs d ON d.repo_id = r.id
|
| 83 |
+
GROUP BY r.id ORDER BY r.created_at DESC LIMIT ?
|
| 84 |
+
""", (limit,)).fetchall()
|
| 85 |
+
return [{"owner": r["owner"], "repo": r["repo"],
|
| 86 |
+
"info": json.loads(r["info"]),
|
| 87 |
+
"created_at": r["created_at"], "doc_count": r["doc_count"]}
|
| 88 |
+
for r in rows]
|
app/tools/doc_forge/doc_generator.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""AI-powered documentation generator for DocForge."""
|
| 2 |
+
import json
|
| 3 |
+
from app.core.ai import call_ai, call_ai_json
|
| 4 |
+
|
| 5 |
+
_README_SYSTEM = """You are a senior technical writer and developer advocate.
|
| 6 |
+
Generate a comprehensive, beautiful GitHub README.md for the given repository.
|
| 7 |
+
Output ONLY the raw Markdown content — no JSON, no preamble, no code fences.
|
| 8 |
+
README must include: title with badges, description, features list, installation,
|
| 9 |
+
usage with examples, configuration, contributing guide, and license section.
|
| 10 |
+
Use real emoji sparingly. Make it genuinely useful, not generic."""
|
| 11 |
+
|
| 12 |
+
_README_META_SYSTEM = """You are a technical analyst. Given a repository description, return ONLY valid JSON.
|
| 13 |
+
Return a JSON object with EXACTLY these keys:
|
| 14 |
+
{"summary": "2-3 sentence plain English summary", "tech_stack": ["tech1", "tech2"], "key_features": ["feature 1", "feature 2"], "complexity": "beginner|intermediate|advanced"}
|
| 15 |
+
No markdown fences, no preamble."""
|
| 16 |
+
|
| 17 |
+
_ARCH_SYSTEM = """You are a software architect. Analyze the repository and write a clear architecture document.
|
| 18 |
+
Output ONLY raw Markdown — no JSON, no preamble, no code fences around the whole document.
|
| 19 |
+
Structure your response with these sections:
|
| 20 |
+
## Architecture Overview
|
| 21 |
+
(2-3 paragraphs explaining the overall design)
|
| 22 |
+
## Key Components
|
| 23 |
+
(bullet list: component name — file path — what it does)
|
| 24 |
+
## Data Flow
|
| 25 |
+
(numbered steps describing how data moves through the system)
|
| 26 |
+
## Mermaid Diagram
|
| 27 |
+
(a ```mermaid code block with a graph LR or flowchart diagram)"""
|
| 28 |
+
|
| 29 |
+
_ARCH_META_SYSTEM = """You are a technical analyst. Return ONLY valid JSON — no markdown, no preamble.
|
| 30 |
+
{"components": [{"name": "X", "role": "Y", "file": "path/to/file"}], "mermaid": "graph LR\\n A --> B"}"""
|
| 31 |
+
|
| 32 |
+
_API_SYSTEM = """You are a technical writer. Extract and document all API endpoints,
|
| 33 |
+
functions, and classes from the code files provided.
|
| 34 |
+
Output ONLY raw Markdown — no JSON, no preamble.
|
| 35 |
+
Structure with these sections:
|
| 36 |
+
## API Endpoints
|
| 37 |
+
(table: Method | Path | Description | Returns)
|
| 38 |
+
## Functions
|
| 39 |
+
(### FunctionName signature, then description and params as a bullet list)
|
| 40 |
+
## Classes
|
| 41 |
+
(### ClassName, then description and method list)"""
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def _build_context(repo_info: dict, tree: list[str], files: dict[str, str]) -> str:
|
| 45 |
+
ctx = f"Repository: {repo_info['full_name']}\n"
|
| 46 |
+
ctx += f"Description: {repo_info.get('description', 'No description')}\n"
|
| 47 |
+
ctx += f"Primary language: {repo_info.get('language', 'Unknown')}\n"
|
| 48 |
+
ctx += f"Stars: {repo_info.get('stars', 0)} Forks: {repo_info.get('forks', 0)}\n"
|
| 49 |
+
if repo_info.get("topics"):
|
| 50 |
+
ctx += f"Topics: {', '.join(repo_info['topics'])}\n"
|
| 51 |
+
ctx += f"\nFile tree ({len(tree)} files, showing first 30):\n"
|
| 52 |
+
ctx += "\n".join(f" {p}" for p in tree[:30])
|
| 53 |
+
ctx += "\n\nKey file contents:\n"
|
| 54 |
+
for path, content in list(files.items())[:5]:
|
| 55 |
+
ctx += f"\n--- {path} ---\n{content[:1500]}\n"
|
| 56 |
+
# Hard cap: Groq llama-3.1-8b has ~8k token context; keep prompt under ~12k chars
|
| 57 |
+
return ctx[:12000]
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def generate_readme(repo_info: dict, tree: list[str],
|
| 61 |
+
files: dict[str, str], api_key_row=None) -> dict:
|
| 62 |
+
ctx = _build_context(repo_info, tree, files)
|
| 63 |
+
# Generate README as plain text (more reliable than embedding in JSON)
|
| 64 |
+
readme_text = call_ai(
|
| 65 |
+
[{"role": "user", "content": f"Generate a README.md for this repository:\n\n{ctx}"}],
|
| 66 |
+
system=_README_SYSTEM,
|
| 67 |
+
max_tokens=2048,
|
| 68 |
+
api_key_row=api_key_row,
|
| 69 |
+
)
|
| 70 |
+
# Generate metadata as simple JSON
|
| 71 |
+
try:
|
| 72 |
+
meta = call_ai_json(
|
| 73 |
+
[{"role": "user", "content": f"Analyze this repository and return metadata JSON:\n{ctx[:3000]}"}],
|
| 74 |
+
system=_README_META_SYSTEM,
|
| 75 |
+
max_tokens=512,
|
| 76 |
+
api_key_row=api_key_row,
|
| 77 |
+
)
|
| 78 |
+
if not isinstance(meta, dict):
|
| 79 |
+
meta = {}
|
| 80 |
+
except Exception:
|
| 81 |
+
meta = {}
|
| 82 |
+
return {
|
| 83 |
+
"readme": readme_text,
|
| 84 |
+
"summary": meta.get("summary", ""),
|
| 85 |
+
"tech_stack": meta.get("tech_stack", []),
|
| 86 |
+
"key_features": meta.get("key_features", []),
|
| 87 |
+
"complexity": meta.get("complexity", "intermediate"),
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def generate_architecture(repo_info: dict, tree: list[str],
|
| 92 |
+
files: dict[str, str], api_key_row=None) -> dict:
|
| 93 |
+
ctx = _build_context(repo_info, tree, files)
|
| 94 |
+
overview_md = call_ai(
|
| 95 |
+
[{"role": "user", "content": f"Write an architecture document for this repository:\n\n{ctx}"}],
|
| 96 |
+
system=_ARCH_SYSTEM,
|
| 97 |
+
max_tokens=2048,
|
| 98 |
+
api_key_row=api_key_row,
|
| 99 |
+
)
|
| 100 |
+
try:
|
| 101 |
+
meta = call_ai_json(
|
| 102 |
+
[{"role": "user", "content": f"List the key components and a Mermaid diagram for this repo:\n{ctx[:3000]}"}],
|
| 103 |
+
system=_ARCH_META_SYSTEM,
|
| 104 |
+
max_tokens=1024,
|
| 105 |
+
api_key_row=api_key_row,
|
| 106 |
+
)
|
| 107 |
+
if not isinstance(meta, dict):
|
| 108 |
+
meta = {}
|
| 109 |
+
except Exception:
|
| 110 |
+
meta = {}
|
| 111 |
+
return {
|
| 112 |
+
"overview": overview_md,
|
| 113 |
+
"components": meta.get("components", []),
|
| 114 |
+
"mermaid": meta.get("mermaid", ""),
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
def generate_api_docs(repo_info: dict, tree: list[str],
|
| 119 |
+
files: dict[str, str], api_key_row=None) -> dict:
|
| 120 |
+
ctx = _build_context(repo_info, tree, files)
|
| 121 |
+
api_md = call_ai(
|
| 122 |
+
[{"role": "user", "content": f"Document the API, functions, and classes from this codebase:\n\n{ctx}"}],
|
| 123 |
+
system=_API_SYSTEM,
|
| 124 |
+
max_tokens=2048,
|
| 125 |
+
api_key_row=api_key_row,
|
| 126 |
+
)
|
| 127 |
+
return {"content": api_md}
|
app/tools/doc_forge/github_fetcher.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Fetches repository structure and key file contents from GitHub API."""
|
| 2 |
+
import os
|
| 3 |
+
import re
|
| 4 |
+
import requests
|
| 5 |
+
|
| 6 |
+
_GITHUB_API = "https://api.github.com"
|
| 7 |
+
_SKIP_DIRS = {".git", "node_modules", "__pycache__", ".venv", "venv",
|
| 8 |
+
"dist", "build", ".next", ".nuxt", "coverage", "htmlcov"}
|
| 9 |
+
_CODE_EXTS = {".py", ".js", ".ts", ".jsx", ".tsx", ".go", ".rs", ".java",
|
| 10 |
+
".rb", ".php", ".cs", ".cpp", ".c", ".h", ".swift", ".kt"}
|
| 11 |
+
_DOC_EXTS = {".md", ".rst", ".txt", ".yaml", ".yml", ".toml", ".json"}
|
| 12 |
+
_PRIORITY = ["README.md", "readme.md", "README.rst", "main.py", "app.py",
|
| 13 |
+
"index.js", "index.ts", "main.go", "src/main.rs", "setup.py",
|
| 14 |
+
"pyproject.toml", "package.json", "go.mod", "Cargo.toml"]
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def _headers():
|
| 18 |
+
token = os.environ.get("GITHUB_TOKEN", "")
|
| 19 |
+
h = {"Accept": "application/vnd.github.v3+json"}
|
| 20 |
+
if token:
|
| 21 |
+
h["Authorization"] = f"Bearer {token}"
|
| 22 |
+
return h
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def parse_repo_url(url: str) -> tuple[str, str]:
|
| 26 |
+
"""Return (owner, repo) from a GitHub URL or owner/repo string."""
|
| 27 |
+
url = url.strip().rstrip("/")
|
| 28 |
+
# Match github.com/owner/repo — ignore /tree/, /blob/, /issues/ etc.
|
| 29 |
+
m = re.search(r"github\.com/([^/]+)/([^/?#\s]+)", url)
|
| 30 |
+
if m:
|
| 31 |
+
repo = m.group(2)
|
| 32 |
+
if repo.endswith(".git"):
|
| 33 |
+
repo = repo[:-4]
|
| 34 |
+
return m.group(1), repo
|
| 35 |
+
# Plain "owner/repo" shorthand
|
| 36 |
+
parts = url.split("/")
|
| 37 |
+
if len(parts) == 2 and parts[0] and parts[1]:
|
| 38 |
+
return parts[0], parts[1]
|
| 39 |
+
raise ValueError(f"Cannot parse GitHub URL: {url!r}")
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def get_repo_info(owner: str, repo: str) -> dict:
|
| 43 |
+
r = requests.get(f"{_GITHUB_API}/repos/{owner}/{repo}",
|
| 44 |
+
headers=_headers(), timeout=15)
|
| 45 |
+
r.raise_for_status()
|
| 46 |
+
d = r.json()
|
| 47 |
+
return {
|
| 48 |
+
"full_name": d.get("full_name", ""),
|
| 49 |
+
"description": d.get("description", ""),
|
| 50 |
+
"language": d.get("language", ""),
|
| 51 |
+
"stars": d.get("stargazers_count", 0),
|
| 52 |
+
"forks": d.get("forks_count", 0),
|
| 53 |
+
"topics": d.get("topics", []),
|
| 54 |
+
"default_branch": d.get("default_branch", "main"),
|
| 55 |
+
"url": d.get("html_url", ""),
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def get_file_tree(owner: str, repo: str, branch: str = "main",
|
| 60 |
+
max_files: int = 150) -> list[str]:
|
| 61 |
+
"""Return flat list of file paths, priority files first."""
|
| 62 |
+
r = requests.get(
|
| 63 |
+
f"{_GITHUB_API}/repos/{owner}/{repo}/git/trees/{branch}?recursive=1",
|
| 64 |
+
headers=_headers(), timeout=20)
|
| 65 |
+
if r.status_code == 404:
|
| 66 |
+
# Try main vs master
|
| 67 |
+
alt = "master" if branch == "main" else "main"
|
| 68 |
+
r = requests.get(
|
| 69 |
+
f"{_GITHUB_API}/repos/{owner}/{repo}/git/trees/{alt}?recursive=1",
|
| 70 |
+
headers=_headers(), timeout=20)
|
| 71 |
+
r.raise_for_status()
|
| 72 |
+
blobs = [item["path"] for item in r.json().get("tree", [])
|
| 73 |
+
if item["type"] == "blob"
|
| 74 |
+
and not any(seg in _SKIP_DIRS for seg in item["path"].split("/"))]
|
| 75 |
+
|
| 76 |
+
# Sort: priority first, then code, then docs, then rest
|
| 77 |
+
def rank(p):
|
| 78 |
+
name = p.split("/")[-1]
|
| 79 |
+
if p in _PRIORITY or name in _PRIORITY:
|
| 80 |
+
return 0
|
| 81 |
+
ext = os.path.splitext(p)[1].lower()
|
| 82 |
+
if ext in _CODE_EXTS:
|
| 83 |
+
return 1
|
| 84 |
+
if ext in _DOC_EXTS:
|
| 85 |
+
return 2
|
| 86 |
+
return 3
|
| 87 |
+
|
| 88 |
+
return sorted(blobs, key=rank)[:max_files]
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def fetch_file(owner: str, repo: str, path: str, branch: str = "main") -> str:
|
| 92 |
+
"""Fetch raw content of a single file (max 50KB)."""
|
| 93 |
+
r = requests.get(
|
| 94 |
+
f"https://raw.githubusercontent.com/{owner}/{repo}/{branch}/{path}",
|
| 95 |
+
headers=_headers(), timeout=15)
|
| 96 |
+
if r.status_code != 200:
|
| 97 |
+
return ""
|
| 98 |
+
text = r.text
|
| 99 |
+
return text[:50_000] # cap at 50KB per file
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def fetch_key_files(owner: str, repo: str, tree: list[str],
|
| 103 |
+
branch: str = "main", max_chars: int = 60_000) -> dict[str, str]:
|
| 104 |
+
"""Fetch the most important files up to max_chars total."""
|
| 105 |
+
results: dict[str, str] = {}
|
| 106 |
+
total = 0
|
| 107 |
+
# Always try priority files first
|
| 108 |
+
ordered = [p for p in _PRIORITY if p in tree] + [p for p in tree if p not in _PRIORITY]
|
| 109 |
+
for path in ordered:
|
| 110 |
+
if total >= max_chars:
|
| 111 |
+
break
|
| 112 |
+
content = fetch_file(owner, repo, path, branch)
|
| 113 |
+
if content:
|
| 114 |
+
results[path] = content
|
| 115 |
+
total += len(content)
|
| 116 |
+
return results
|
app/tools/doc_forge/routes.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""DocForge routes — generate full documentation for any GitHub repo."""
|
| 2 |
+
import json
|
| 3 |
+
import os
|
| 4 |
+
from flask import (Blueprint, render_template, request, jsonify,
|
| 5 |
+
current_app, Response)
|
| 6 |
+
from .github_fetcher import (parse_repo_url, get_repo_info,
|
| 7 |
+
get_file_tree, fetch_key_files)
|
| 8 |
+
from .doc_generator import (generate_readme, generate_architecture,
|
| 9 |
+
generate_api_docs)
|
| 10 |
+
from .db import get_db, upsert_repo, upsert_doc, get_docs, list_recent
|
| 11 |
+
|
| 12 |
+
bp = Blueprint("doc_forge", __name__, template_folder="templates")
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def _db():
|
| 16 |
+
db_path = os.path.join(os.path.dirname(current_app.root_path), "docforge.db")
|
| 17 |
+
return get_db(db_path)
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
# ── Pages ──────────────────────────────────────────────────────────────────────
|
| 21 |
+
|
| 22 |
+
@bp.route("/")
|
| 23 |
+
def index():
|
| 24 |
+
db = _db()
|
| 25 |
+
recent = list_recent(db)
|
| 26 |
+
return render_template("doc_forge/index.html", recent=recent)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
@bp.route("/docs/<owner>/<repo>")
|
| 30 |
+
def docs_view(owner, repo):
|
| 31 |
+
db = _db()
|
| 32 |
+
data = get_docs(db, owner, repo)
|
| 33 |
+
if not data:
|
| 34 |
+
return render_template("doc_forge/index.html", recent=list_recent(db),
|
| 35 |
+
error=f"No docs found for {owner}/{repo}. Generate them first.")
|
| 36 |
+
return render_template("doc_forge/docs.html", owner=owner, repo=repo, data=data)
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
# ── API ────────────────────────────────────────────────────────────────────────
|
| 40 |
+
|
| 41 |
+
@bp.route("/api/analyze", methods=["POST"])
|
| 42 |
+
def analyze():
|
| 43 |
+
"""Step 1: fetch repo metadata + file tree. Returns info without generating docs."""
|
| 44 |
+
body = request.get_json(silent=True) or {}
|
| 45 |
+
url = (body.get("url") or "").strip()
|
| 46 |
+
if not url:
|
| 47 |
+
return jsonify({"error": "repo_url is required"}), 400
|
| 48 |
+
try:
|
| 49 |
+
owner, repo = parse_repo_url(url)
|
| 50 |
+
info = get_repo_info(owner, repo)
|
| 51 |
+
tree = get_file_tree(owner, repo, info["default_branch"])
|
| 52 |
+
return jsonify({
|
| 53 |
+
"owner": owner, "repo": repo,
|
| 54 |
+
"info": info, "file_count": len(tree),
|
| 55 |
+
"tree_preview": tree[:20],
|
| 56 |
+
})
|
| 57 |
+
except Exception as exc:
|
| 58 |
+
return jsonify({"error": str(exc)}), 400
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
@bp.route("/api/generate", methods=["POST"])
|
| 62 |
+
def generate():
|
| 63 |
+
"""Step 2: generate all docs for a repo."""
|
| 64 |
+
body = request.get_json(silent=True) or {}
|
| 65 |
+
url = (body.get("url") or "").strip()
|
| 66 |
+
types = body.get("types", ["readme", "architecture", "api"])
|
| 67 |
+
if not url:
|
| 68 |
+
return jsonify({"error": "url is required"}), 400
|
| 69 |
+
try:
|
| 70 |
+
owner, repo = parse_repo_url(url)
|
| 71 |
+
info = get_repo_info(owner, repo)
|
| 72 |
+
tree = get_file_tree(owner, repo, info["default_branch"])
|
| 73 |
+
files = fetch_key_files(owner, repo, tree, info["default_branch"])
|
| 74 |
+
db = _db()
|
| 75 |
+
repo_id = upsert_repo(db, owner, repo, info, tree)
|
| 76 |
+
results = {}
|
| 77 |
+
|
| 78 |
+
if "readme" in types:
|
| 79 |
+
readme = generate_readme(info, tree, files)
|
| 80 |
+
upsert_doc(db, repo_id, "readme", readme)
|
| 81 |
+
results["readme"] = readme
|
| 82 |
+
|
| 83 |
+
if "architecture" in types:
|
| 84 |
+
arch = generate_architecture(info, tree, files)
|
| 85 |
+
upsert_doc(db, repo_id, "architecture", arch)
|
| 86 |
+
results["architecture"] = arch
|
| 87 |
+
|
| 88 |
+
if "api" in types:
|
| 89 |
+
api_docs = generate_api_docs(info, tree, files)
|
| 90 |
+
upsert_doc(db, repo_id, "api", api_docs)
|
| 91 |
+
results["api"] = api_docs
|
| 92 |
+
|
| 93 |
+
return jsonify({"owner": owner, "repo": repo, "info": info, "docs": results})
|
| 94 |
+
except Exception as exc:
|
| 95 |
+
return jsonify({"error": str(exc)}), 500
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
@bp.route("/api/download/<owner>/<repo>/<doc_type>")
|
| 99 |
+
def download(owner, repo, doc_type):
|
| 100 |
+
"""Download a generated doc as a file."""
|
| 101 |
+
db = _db()
|
| 102 |
+
data = get_docs(db, owner, repo)
|
| 103 |
+
if not data or doc_type not in data:
|
| 104 |
+
return jsonify({"error": "not found"}), 404
|
| 105 |
+
content = data[doc_type]
|
| 106 |
+
if doc_type == "readme":
|
| 107 |
+
text = content.get("readme", "")
|
| 108 |
+
filename = "README.md"
|
| 109 |
+
mime = "text/markdown"
|
| 110 |
+
else:
|
| 111 |
+
text = json.dumps(content, indent=2, ensure_ascii=False)
|
| 112 |
+
filename = f"{doc_type}.json"
|
| 113 |
+
mime = "application/json"
|
| 114 |
+
return Response(text, mimetype=mime,
|
| 115 |
+
headers={"Content-Disposition": f'attachment; filename="{filename}"'})
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
@bp.route("/api/recent")
|
| 119 |
+
def recent():
|
| 120 |
+
db = _db()
|
| 121 |
+
return jsonify(list_recent(db))
|
app/tools/doc_forge/templates/doc_forge/docs.html
ADDED
|
@@ -0,0 +1,417 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block title %}{{ owner }}/{{ repo }} — DocForge{% endblock %}
|
| 4 |
+
|
| 5 |
+
{% block extra_head %}
|
| 6 |
+
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
| 7 |
+
<script src="https://cdn.jsdelivr.net/npm/dompurify@3/dist/purify.min.js"></script>
|
| 8 |
+
<script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
|
| 9 |
+
<style>
|
| 10 |
+
body { padding-top: 0; }
|
| 11 |
+
.sidebar-link { display: flex; align-items: center; gap: 0.75rem; color: #424753; padding: 0.75rem 1rem; font-size: 0.875rem; transition: all 0.15s; border-radius: 0.5rem; }
|
| 12 |
+
.sidebar-link:hover { color: #171c22; background: rgba(0,0,0,0.04); }
|
| 13 |
+
.sidebar-link.active{ color: #0051ae; border-left: 3px solid #0051ae; padding-left: calc(1rem - 3px); background: rgba(0,81,174,0.08); border-radius: 0.5rem; }
|
| 14 |
+
.tab-btn { padding: 0.625rem 1.25rem; font-size: 0.875rem; font-weight: 600; color: #424753; border-bottom: 2px solid transparent; transition: all 0.15s; cursor: pointer; background: none; border-top: none; border-left: none; border-right: none; }
|
| 15 |
+
.tab-btn.active { color: #0051ae; border-bottom-color: #0051ae; }
|
| 16 |
+
.tab-btn:hover:not(.active) { color: #171c22; }
|
| 17 |
+
.tab-panel { display: none; }
|
| 18 |
+
.tab-panel.active { display: block; }
|
| 19 |
+
.prose-doc h1, .prose-doc h2, .prose-doc h3 { font-weight: 700; color: #171c22; margin: 1.5rem 0 0.75rem; }
|
| 20 |
+
.prose-doc h1 { font-size: 1.75rem; }
|
| 21 |
+
.prose-doc h2 { font-size: 1.375rem; }
|
| 22 |
+
.prose-doc h3 { font-size: 1.125rem; }
|
| 23 |
+
.prose-doc p { color: #424753; line-height: 1.75; margin-bottom: 1rem; }
|
| 24 |
+
.prose-doc ul { list-style: disc; padding-left: 1.5rem; margin-bottom: 1rem; color: #424753; }
|
| 25 |
+
.prose-doc ol { list-style: decimal; padding-left: 1.5rem; margin-bottom: 1rem; color: #424753; }
|
| 26 |
+
.prose-doc li { margin-bottom: 0.25rem; line-height: 1.75; }
|
| 27 |
+
.prose-doc code { background: #eaeef6; padding: 0.125rem 0.375rem; border-radius: 0.25rem; font-family: 'JetBrains Mono', monospace; font-size: 0.8125rem; color: #0051ae; }
|
| 28 |
+
.prose-doc pre { background: #2c3137; color: #e2e8f0; padding: 1.25rem; border-radius: 0.5rem; overflow-x: auto; margin: 1rem 0; font-family: 'JetBrains Mono', monospace; font-size: 0.8125rem; line-height: 1.6; }
|
| 29 |
+
.prose-doc pre code { background: none; color: inherit; padding: 0; }
|
| 30 |
+
.prose-doc blockquote { border-left: 3px solid #0051ae; padding-left: 1rem; color: #424753; font-style: italic; margin: 1rem 0; }
|
| 31 |
+
.prose-doc a { color: #0051ae; text-decoration: underline; text-underline-offset: 2px; }
|
| 32 |
+
.prose-doc table { width: 100%; border-collapse: collapse; margin: 1rem 0; }
|
| 33 |
+
.prose-doc th { background: #e4e8f0; padding: 0.75rem 1rem; text-align: left; font-size: 0.875rem; font-weight: 700; color: #171c22; }
|
| 34 |
+
.prose-doc td { padding: 0.75rem 1rem; font-size: 0.875rem; color: #424753; border-bottom: 1px solid #f0f4fc; }
|
| 35 |
+
.mermaid { background: #f7f9ff; padding: 1.5rem; border-radius: 0.5rem; text-align: center; }
|
| 36 |
+
</style>
|
| 37 |
+
{% endblock %}
|
| 38 |
+
|
| 39 |
+
{% block content %}
|
| 40 |
+
<div class="flex h-screen overflow-hidden pt-12">
|
| 41 |
+
|
| 42 |
+
{# ── Sidebar ───────────────────────────────────────────────────────────────── #}
|
| 43 |
+
<aside class="w-64 bg-surface-container-low border-r border-outline-variant/40 flex-shrink-0 flex flex-col overflow-y-auto">
|
| 44 |
+
<div class="px-5 py-5 border-b border-outline-variant/40">
|
| 45 |
+
<div class="flex items-center gap-3">
|
| 46 |
+
<div class="w-10 h-10 bg-primary rounded-lg flex items-center justify-center text-white font-bold text-lg">
|
| 47 |
+
{{ repo[0].upper() }}
|
| 48 |
+
</div>
|
| 49 |
+
<div class="min-w-0">
|
| 50 |
+
<h3 class="text-on-surface text-sm font-semibold leading-tight truncate">{{ repo }}</h3>
|
| 51 |
+
<p class="text-on-surface-variant text-xs font-mono truncate">{{ owner }}</p>
|
| 52 |
+
</div>
|
| 53 |
+
</div>
|
| 54 |
+
</div>
|
| 55 |
+
|
| 56 |
+
<nav class="flex-1 px-2 py-4 space-y-0.5">
|
| 57 |
+
<a href="/" class="sidebar-link">
|
| 58 |
+
<span class="material-symbols-outlined text-[20px]">home</span>
|
| 59 |
+
<span>Home</span>
|
| 60 |
+
</a>
|
| 61 |
+
{% if data.readme %}
|
| 62 |
+
<a href="#" onclick="switchTab('readme'); return false;" class="sidebar-link active" id="nav-readme">
|
| 63 |
+
<span class="material-symbols-outlined text-[20px]">description</span>
|
| 64 |
+
<span>README</span>
|
| 65 |
+
</a>
|
| 66 |
+
{% endif %}
|
| 67 |
+
{% if data.architecture %}
|
| 68 |
+
<a href="#" onclick="switchTab('architecture'); return false;" class="sidebar-link" id="nav-architecture">
|
| 69 |
+
<span class="material-symbols-outlined text-[20px]">account_tree</span>
|
| 70 |
+
<span>Architecture</span>
|
| 71 |
+
</a>
|
| 72 |
+
{% endif %}
|
| 73 |
+
{% if data.api %}
|
| 74 |
+
<a href="#" onclick="switchTab('api'); return false;" class="sidebar-link" id="nav-api">
|
| 75 |
+
<span class="material-symbols-outlined text-[20px]">api</span>
|
| 76 |
+
<span>API Docs</span>
|
| 77 |
+
</a>
|
| 78 |
+
{% endif %}
|
| 79 |
+
</nav>
|
| 80 |
+
|
| 81 |
+
<div class="p-4 border-t border-outline-variant/40 space-y-1">
|
| 82 |
+
<a href="https://github.com/{{ owner }}/{{ repo }}" target="_blank"
|
| 83 |
+
class="flex items-center gap-2 text-on-surface-variant hover:text-primary text-xs py-2 px-2 transition-colors rounded-lg">
|
| 84 |
+
<span class="material-symbols-outlined text-base">open_in_new</span>
|
| 85 |
+
View on GitHub
|
| 86 |
+
</a>
|
| 87 |
+
<a href="/" class="flex items-center gap-2 text-on-surface-variant hover:text-primary text-xs py-2 px-2 transition-colors rounded-lg">
|
| 88 |
+
<span class="material-symbols-outlined text-base">add</span>
|
| 89 |
+
New Repository
|
| 90 |
+
</a>
|
| 91 |
+
</div>
|
| 92 |
+
</aside>
|
| 93 |
+
|
| 94 |
+
{# ── Main Content ──────────────────────────────────────────────────────────── #}
|
| 95 |
+
<main class="flex-1 overflow-y-auto bg-background">
|
| 96 |
+
<div class="max-w-4xl mx-auto p-8">
|
| 97 |
+
|
| 98 |
+
{# Breadcrumb #}
|
| 99 |
+
<nav class="flex items-center gap-2 text-xs font-mono text-on-surface-variant mb-4">
|
| 100 |
+
<a href="/" class="hover:text-primary transition-colors">docforge</a>
|
| 101 |
+
<span class="material-symbols-outlined text-[12px]">chevron_right</span>
|
| 102 |
+
<span>{{ owner }}</span>
|
| 103 |
+
<span class="material-symbols-outlined text-[12px]">chevron_right</span>
|
| 104 |
+
<span class="text-primary font-bold">{{ repo }}</span>
|
| 105 |
+
</nav>
|
| 106 |
+
|
| 107 |
+
{# Page header #}
|
| 108 |
+
<header class="flex justify-between items-start mb-8 gap-4 flex-wrap">
|
| 109 |
+
<div>
|
| 110 |
+
<h1 class="text-3xl font-extrabold text-on-surface tracking-tight">{{ owner }}/{{ repo }}</h1>
|
| 111 |
+
{% if data.readme and data.readme.summary %}
|
| 112 |
+
<p class="text-on-surface-variant mt-2 max-w-2xl">{{ data.readme.summary }}</p>
|
| 113 |
+
{% endif %}
|
| 114 |
+
<div class="flex flex-wrap gap-2 mt-3">
|
| 115 |
+
{% if data.readme and data.readme.tech_stack %}
|
| 116 |
+
{% for tech in data.readme.tech_stack[:6] %}
|
| 117 |
+
<span class="px-2 py-0.5 rounded bg-secondary-container text-on-secondary-container text-[11px] font-mono">{{ tech }}</span>
|
| 118 |
+
{% endfor %}
|
| 119 |
+
{% endif %}
|
| 120 |
+
{% if data.readme and data.readme.complexity %}
|
| 121 |
+
<span class="px-2 py-0.5 rounded bg-tertiary-fixed text-on-tertiary-fixed text-[11px] font-mono">{{ data.readme.complexity }}</span>
|
| 122 |
+
{% endif %}
|
| 123 |
+
</div>
|
| 124 |
+
</div>
|
| 125 |
+
<div class="flex gap-2 flex-shrink-0">
|
| 126 |
+
<button onclick="copyCurrentTab()"
|
| 127 |
+
class="bg-surface-container-high text-on-surface px-4 py-2 rounded-lg text-sm font-medium flex items-center gap-2 hover:bg-surface-container-highest transition-colors cursor-pointer">
|
| 128 |
+
<span id="copy-icon" class="material-symbols-outlined text-lg">content_copy</span>
|
| 129 |
+
Copy
|
| 130 |
+
</button>
|
| 131 |
+
<button onclick="downloadCurrentTab()"
|
| 132 |
+
class="bg-gradient-to-r from-primary to-primary-container text-on-primary px-5 py-2 rounded-lg text-sm font-semibold flex items-center gap-2 shadow-lg shadow-primary/20 hover:-translate-y-0.5 transition-all cursor-pointer">
|
| 133 |
+
<span class="material-symbols-outlined text-lg">download</span>
|
| 134 |
+
Download
|
| 135 |
+
</button>
|
| 136 |
+
</div>
|
| 137 |
+
</header>
|
| 138 |
+
|
| 139 |
+
{# Tabs #}
|
| 140 |
+
<div class="flex gap-0 border-b border-outline-variant/30 mb-8">
|
| 141 |
+
{% if data.readme %}
|
| 142 |
+
<button class="tab-btn active" id="tab-readme" onclick="switchTab('readme')">README</button>
|
| 143 |
+
{% endif %}
|
| 144 |
+
{% if data.architecture %}
|
| 145 |
+
<button class="tab-btn" id="tab-architecture" onclick="switchTab('architecture')">Architecture</button>
|
| 146 |
+
{% endif %}
|
| 147 |
+
{% if data.api %}
|
| 148 |
+
<button class="tab-btn" id="tab-api" onclick="switchTab('api')">API Docs</button>
|
| 149 |
+
{% endif %}
|
| 150 |
+
</div>
|
| 151 |
+
|
| 152 |
+
{# ── README Panel ──────────────────────────────────────────────────────── #}
|
| 153 |
+
{% if data.readme %}
|
| 154 |
+
<div class="tab-panel active" id="panel-readme">
|
| 155 |
+
<div class="bg-surface-container-lowest rounded-xl p-8">
|
| 156 |
+
<div class="prose-doc" id="readme-content"></div>
|
| 157 |
+
</div>
|
| 158 |
+
{% if data.readme.key_features %}
|
| 159 |
+
<div class="mt-6 grid grid-cols-1 sm:grid-cols-2 gap-4">
|
| 160 |
+
{% for feat in data.readme.key_features[:4] %}
|
| 161 |
+
<div class="bg-surface-container-lowest p-5 rounded-xl flex items-start gap-3">
|
| 162 |
+
<span class="material-symbols-outlined text-primary text-xl mt-0.5 flex-shrink-0">check_circle</span>
|
| 163 |
+
<p class="text-sm text-on-surface-variant">{{ feat | e }}</p>
|
| 164 |
+
</div>
|
| 165 |
+
{% endfor %}
|
| 166 |
+
</div>
|
| 167 |
+
{% endif %}
|
| 168 |
+
</div>
|
| 169 |
+
{% endif %}
|
| 170 |
+
|
| 171 |
+
{# ── Architecture Panel ────────────────────────────────────────────────── #}
|
| 172 |
+
{% if data.architecture %}
|
| 173 |
+
<div class="tab-panel" id="panel-architecture">
|
| 174 |
+
{% if data.architecture.overview %}
|
| 175 |
+
<div class="bg-surface-container-lowest rounded-xl p-8 mb-6">
|
| 176 |
+
<h2 class="text-xl font-bold text-on-surface mb-4 flex items-center gap-3">
|
| 177 |
+
<span class="w-1.5 h-5 bg-primary rounded-full flex-shrink-0"></span>
|
| 178 |
+
Overview
|
| 179 |
+
</h2>
|
| 180 |
+
<div id="arch-overview" class="prose-doc"></div>
|
| 181 |
+
<script>document.getElementById('arch-overview').innerHTML = DOMPurify.sanitize(marked.parse({{ data.architecture.overview | tojson }}));</script>
|
| 182 |
+
</div>
|
| 183 |
+
{% endif %}
|
| 184 |
+
|
| 185 |
+
{% if data.architecture.mermaid %}
|
| 186 |
+
<div class="bg-surface-container-lowest rounded-xl p-8 mb-6">
|
| 187 |
+
<h2 class="text-xl font-bold text-on-surface mb-6 flex items-center gap-3">
|
| 188 |
+
<span class="w-1.5 h-5 bg-primary-container rounded-full flex-shrink-0"></span>
|
| 189 |
+
System Diagram
|
| 190 |
+
</h2>
|
| 191 |
+
<div class="mermaid">{{ data.architecture.mermaid | e }}</div>
|
| 192 |
+
</div>
|
| 193 |
+
{% endif %}
|
| 194 |
+
|
| 195 |
+
{% if data.architecture.components %}
|
| 196 |
+
<div class="bg-surface-container-lowest rounded-xl p-8 mb-6">
|
| 197 |
+
<h2 class="text-xl font-bold text-on-surface mb-6 flex items-center gap-3">
|
| 198 |
+
<span class="w-1.5 h-5 bg-tertiary rounded-full flex-shrink-0"></span>
|
| 199 |
+
Components
|
| 200 |
+
</h2>
|
| 201 |
+
<div class="space-y-4">
|
| 202 |
+
{% for comp in data.architecture.components %}
|
| 203 |
+
<div class="bg-surface-container-low p-5 rounded-xl">
|
| 204 |
+
<h3 class="font-bold text-on-surface text-sm mb-1 font-mono">
|
| 205 |
+
{{ (comp.name if comp is mapping else comp) | e }}
|
| 206 |
+
</h3>
|
| 207 |
+
{% if comp is mapping and comp.description %}
|
| 208 |
+
<p class="text-sm text-on-surface-variant">{{ comp.description | e }}</p>
|
| 209 |
+
{% endif %}
|
| 210 |
+
{% if comp is mapping and comp.responsibilities %}
|
| 211 |
+
<ul class="mt-2 space-y-1">
|
| 212 |
+
{% for r in comp.responsibilities %}
|
| 213 |
+
<li class="text-xs text-on-surface-variant flex items-start gap-2">
|
| 214 |
+
<span class="material-symbols-outlined text-outline text-sm mt-0.5 flex-shrink-0">arrow_right</span>
|
| 215 |
+
{{ r | e }}
|
| 216 |
+
</li>
|
| 217 |
+
{% endfor %}
|
| 218 |
+
</ul>
|
| 219 |
+
{% endif %}
|
| 220 |
+
</div>
|
| 221 |
+
{% endfor %}
|
| 222 |
+
</div>
|
| 223 |
+
</div>
|
| 224 |
+
{% endif %}
|
| 225 |
+
|
| 226 |
+
{% if data.architecture.data_flow %}
|
| 227 |
+
<div class="bg-surface-container-lowest rounded-xl p-8">
|
| 228 |
+
<h2 class="text-xl font-bold text-on-surface mb-4 flex items-center gap-3">
|
| 229 |
+
<span class="w-1.5 h-5 bg-secondary rounded-full flex-shrink-0"></span>
|
| 230 |
+
Data Flow
|
| 231 |
+
</h2>
|
| 232 |
+
<p class="text-on-surface-variant leading-relaxed">{{ data.architecture.data_flow | e }}</p>
|
| 233 |
+
</div>
|
| 234 |
+
{% endif %}
|
| 235 |
+
</div>
|
| 236 |
+
{% endif %}
|
| 237 |
+
|
| 238 |
+
{# ── API Panel ────────────────────────────────────────────────────────── #}
|
| 239 |
+
{% if data.api %}
|
| 240 |
+
<div class="tab-panel" id="panel-api">
|
| 241 |
+
|
| 242 |
+
{% if data.api.endpoints %}
|
| 243 |
+
<div class="bg-surface-container-lowest rounded-xl p-8 mb-6">
|
| 244 |
+
<h2 class="text-xl font-bold text-on-surface mb-6 flex items-center gap-3">
|
| 245 |
+
<span class="w-1.5 h-5 bg-primary rounded-full flex-shrink-0"></span>
|
| 246 |
+
Endpoints
|
| 247 |
+
</h2>
|
| 248 |
+
<div class="overflow-hidden rounded-xl bg-surface-container-low">
|
| 249 |
+
<table class="w-full text-left">
|
| 250 |
+
<thead class="bg-surface-container-high">
|
| 251 |
+
<tr>
|
| 252 |
+
<th class="px-5 py-3 text-xs font-bold text-on-surface uppercase tracking-wide">Method</th>
|
| 253 |
+
<th class="px-5 py-3 text-xs font-bold text-on-surface uppercase tracking-wide">Path</th>
|
| 254 |
+
<th class="px-5 py-3 text-xs font-bold text-on-surface uppercase tracking-wide">Description</th>
|
| 255 |
+
</tr>
|
| 256 |
+
</thead>
|
| 257 |
+
<tbody>
|
| 258 |
+
{% for ep in data.api.endpoints %}
|
| 259 |
+
{% set method = ep.method if ep is mapping else 'GET' %}
|
| 260 |
+
<tr class="hover:bg-surface-container-highest transition-colors">
|
| 261 |
+
<td class="px-5 py-3">
|
| 262 |
+
<span class="font-mono text-xs font-bold
|
| 263 |
+
{% if method == 'GET' %}text-green-600
|
| 264 |
+
{% elif method == 'POST' %}text-blue-600
|
| 265 |
+
{% elif method in ('PUT','PATCH') %}text-purple-600
|
| 266 |
+
{% elif method == 'DELETE' %}text-red-600
|
| 267 |
+
{% else %}text-on-surface{% endif %}">
|
| 268 |
+
{{ method | e }}
|
| 269 |
+
</span>
|
| 270 |
+
</td>
|
| 271 |
+
<td class="px-5 py-3 font-mono text-xs text-on-surface">
|
| 272 |
+
{{ (ep.path if ep is mapping else ep) | e }}
|
| 273 |
+
</td>
|
| 274 |
+
<td class="px-5 py-3 text-sm text-on-surface-variant">
|
| 275 |
+
{{ (ep.description if ep is mapping else '') | e }}
|
| 276 |
+
</td>
|
| 277 |
+
</tr>
|
| 278 |
+
{% endfor %}
|
| 279 |
+
</tbody>
|
| 280 |
+
</table>
|
| 281 |
+
</div>
|
| 282 |
+
</div>
|
| 283 |
+
{% endif %}
|
| 284 |
+
|
| 285 |
+
{% if data.api.functions %}
|
| 286 |
+
<div class="bg-surface-container-lowest rounded-xl p-8 mb-6">
|
| 287 |
+
<h2 class="text-xl font-bold text-on-surface mb-6 flex items-center gap-3">
|
| 288 |
+
<span class="w-1.5 h-5 bg-primary-container rounded-full flex-shrink-0"></span>
|
| 289 |
+
Functions
|
| 290 |
+
</h2>
|
| 291 |
+
<div class="space-y-4">
|
| 292 |
+
{% for fn in data.api.functions %}
|
| 293 |
+
<div class="bg-surface-container-low p-5 rounded-xl">
|
| 294 |
+
<p class="font-mono text-sm font-bold text-on-surface mb-1">
|
| 295 |
+
{{ (fn.name if fn is mapping else fn) | e }}{% if fn is mapping and fn.signature %}({{ fn.signature | e }}){% endif %}
|
| 296 |
+
</p>
|
| 297 |
+
{% if fn is mapping and fn.description %}
|
| 298 |
+
<p class="text-sm text-on-surface-variant">{{ fn.description | e }}</p>
|
| 299 |
+
{% endif %}
|
| 300 |
+
{% if fn is mapping and fn.returns %}
|
| 301 |
+
<p class="text-xs text-outline mt-2 font-mono">→ {{ fn.returns | e }}</p>
|
| 302 |
+
{% endif %}
|
| 303 |
+
</div>
|
| 304 |
+
{% endfor %}
|
| 305 |
+
</div>
|
| 306 |
+
</div>
|
| 307 |
+
{% endif %}
|
| 308 |
+
|
| 309 |
+
{% if data.api.classes %}
|
| 310 |
+
<div class="bg-surface-container-lowest rounded-xl p-8">
|
| 311 |
+
<h2 class="text-xl font-bold text-on-surface mb-6 flex items-center gap-3">
|
| 312 |
+
<span class="w-1.5 h-5 bg-tertiary rounded-full flex-shrink-0"></span>
|
| 313 |
+
Classes
|
| 314 |
+
</h2>
|
| 315 |
+
<div class="space-y-4">
|
| 316 |
+
{% for cls in data.api.classes %}
|
| 317 |
+
<div class="bg-surface-container-low p-5 rounded-xl">
|
| 318 |
+
<p class="font-mono text-sm font-bold text-on-surface mb-1">
|
| 319 |
+
class {{ (cls.name if cls is mapping else cls) | e }}
|
| 320 |
+
</p>
|
| 321 |
+
{% if cls is mapping and cls.description %}
|
| 322 |
+
<p class="text-sm text-on-surface-variant">{{ cls.description | e }}</p>
|
| 323 |
+
{% endif %}
|
| 324 |
+
</div>
|
| 325 |
+
{% endfor %}
|
| 326 |
+
</div>
|
| 327 |
+
</div>
|
| 328 |
+
{% endif %}
|
| 329 |
+
|
| 330 |
+
{% if not data.api.endpoints and not data.api.functions and not data.api.classes %}
|
| 331 |
+
{% if data.api.content %}
|
| 332 |
+
<div class="bg-surface-container-lowest rounded-xl p-8">
|
| 333 |
+
<div id="api-md-content" class="prose-doc"></div>
|
| 334 |
+
</div>
|
| 335 |
+
<script>
|
| 336 |
+
document.getElementById('api-md-content').innerHTML =
|
| 337 |
+
DOMPurify.sanitize(marked.parse({{ data.api.content | tojson }}));
|
| 338 |
+
</script>
|
| 339 |
+
{% else %}
|
| 340 |
+
<div class="bg-surface-container-lowest rounded-xl p-8 text-center">
|
| 341 |
+
<span class="material-symbols-outlined text-outline text-4xl">api</span>
|
| 342 |
+
<p class="text-on-surface-variant mt-4">No API elements detected in this repository.</p>
|
| 343 |
+
</div>
|
| 344 |
+
{% endif %}
|
| 345 |
+
{% endif %}
|
| 346 |
+
</div>
|
| 347 |
+
{% endif %}
|
| 348 |
+
|
| 349 |
+
<div class="mt-12 pt-8 flex justify-between items-center text-sm text-on-surface-variant">
|
| 350 |
+
<a href="/" class="flex items-center gap-2 hover:text-primary transition-colors">
|
| 351 |
+
<span class="material-symbols-outlined text-base">arrow_back</span>
|
| 352 |
+
Back to DocForge
|
| 353 |
+
</a>
|
| 354 |
+
<a href="https://github.com/{{ owner }}/{{ repo }}" target="_blank"
|
| 355 |
+
class="flex items-center gap-2 hover:text-primary transition-colors">
|
| 356 |
+
View on GitHub
|
| 357 |
+
<span class="material-symbols-outlined text-base">open_in_new</span>
|
| 358 |
+
</a>
|
| 359 |
+
</div>
|
| 360 |
+
</div>
|
| 361 |
+
</main>
|
| 362 |
+
</div>
|
| 363 |
+
{% endblock %}
|
| 364 |
+
|
| 365 |
+
{% block extra_scripts %}
|
| 366 |
+
<script>
|
| 367 |
+
mermaid.initialize({ startOnLoad: true, theme: 'neutral', securityLevel: 'loose' });
|
| 368 |
+
|
| 369 |
+
let _currentTab = '{% if data.readme %}readme{% elif data.architecture %}architecture{% else %}api{% endif %}';
|
| 370 |
+
|
| 371 |
+
// Render README via marked + DOMPurify
|
| 372 |
+
(function renderReadme() {
|
| 373 |
+
const el = document.getElementById('readme-content');
|
| 374 |
+
if (!el) return;
|
| 375 |
+
const raw = {{ (data.readme.readme if data.readme else '') | tojson }};
|
| 376 |
+
const html = DOMPurify.sanitize(marked.parse(raw));
|
| 377 |
+
el.setHTML ? el.setHTML(html) : (el.textContent = '', el.insertAdjacentHTML('afterbegin', DOMPurify.sanitize(html)));
|
| 378 |
+
})();
|
| 379 |
+
|
| 380 |
+
function switchTab(name) {
|
| 381 |
+
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
|
| 382 |
+
const panel = document.getElementById('panel-' + name);
|
| 383 |
+
if (panel) panel.classList.add('active');
|
| 384 |
+
|
| 385 |
+
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
| 386 |
+
const btn = document.getElementById('tab-' + name);
|
| 387 |
+
if (btn) btn.classList.add('active');
|
| 388 |
+
|
| 389 |
+
document.querySelectorAll('.sidebar-link').forEach(l => l.classList.remove('active'));
|
| 390 |
+
const nav = document.getElementById('nav-' + name);
|
| 391 |
+
if (nav) nav.classList.add('active');
|
| 392 |
+
|
| 393 |
+
_currentTab = name;
|
| 394 |
+
if (name === 'architecture') mermaid.run({ querySelector: '.mermaid' });
|
| 395 |
+
}
|
| 396 |
+
|
| 397 |
+
function copyCurrentTab() {
|
| 398 |
+
let text = '';
|
| 399 |
+
if (_currentTab === 'readme') {
|
| 400 |
+
text = {{ (data.readme.readme if data.readme else '') | tojson }};
|
| 401 |
+
} else if (_currentTab === 'architecture') {
|
| 402 |
+
text = JSON.stringify({{ (data.architecture or {}) | tojson }}, null, 2);
|
| 403 |
+
} else if (_currentTab === 'api') {
|
| 404 |
+
text = JSON.stringify({{ (data.api or {}) | tojson }}, null, 2);
|
| 405 |
+
}
|
| 406 |
+
navigator.clipboard.writeText(text).then(() => {
|
| 407 |
+
const icon = document.getElementById('copy-icon');
|
| 408 |
+
icon.textContent = 'check';
|
| 409 |
+
setTimeout(() => { icon.textContent = 'content_copy'; }, 1800);
|
| 410 |
+
});
|
| 411 |
+
}
|
| 412 |
+
|
| 413 |
+
function downloadCurrentTab() {
|
| 414 |
+
window.location.href = '/api/download/{{ owner }}/{{ repo }}/' + _currentTab;
|
| 415 |
+
}
|
| 416 |
+
</script>
|
| 417 |
+
{% endblock %}
|
app/tools/doc_forge/templates/doc_forge/index.html
ADDED
|
@@ -0,0 +1,430 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block title %}DocForge — AI Documentation Engine{% endblock %}
|
| 4 |
+
|
| 5 |
+
{% block extra_head %}
|
| 6 |
+
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
| 7 |
+
{% endblock %}
|
| 8 |
+
|
| 9 |
+
{% block content %}
|
| 10 |
+
<main class="pt-16 pb-20 overflow-x-hidden">
|
| 11 |
+
|
| 12 |
+
{# ── Error Banner ──────────────────────────────────────────────────────────── #}
|
| 13 |
+
{% if error %}
|
| 14 |
+
<div class="max-w-7xl mx-auto px-6 pt-6">
|
| 15 |
+
<div class="flex items-center gap-3 p-4 bg-error-container rounded-xl text-on-error-container text-sm font-medium">
|
| 16 |
+
<span class="material-symbols-outlined text-error">error</span>
|
| 17 |
+
{{ error }}
|
| 18 |
+
</div>
|
| 19 |
+
</div>
|
| 20 |
+
{% endif %}
|
| 21 |
+
|
| 22 |
+
{# ── Hero ──────────────────────────────────────────────────────────────────── #}
|
| 23 |
+
<section class="max-w-7xl mx-auto px-6 py-16 lg:py-24 flex flex-col items-center text-center">
|
| 24 |
+
<div class="mb-4 inline-flex items-center gap-2 px-3 py-1 rounded-full bg-secondary-container text-on-secondary-container text-xs font-semibold tracking-wide">
|
| 25 |
+
<span class="material-symbols-outlined text-base">auto_awesome</span>
|
| 26 |
+
AI-POWERED DOCUMENTATION ENGINE
|
| 27 |
+
</div>
|
| 28 |
+
<h1 class="text-5xl lg:text-7xl font-extrabold tracking-tighter text-on-surface mb-6 max-w-4xl leading-[1.1]">
|
| 29 |
+
Document any GitHub repo
|
| 30 |
+
<span class="bg-gradient-to-r from-primary to-primary-container bg-clip-text text-transparent">instantly.</span>
|
| 31 |
+
</h1>
|
| 32 |
+
<p class="text-lg text-on-surface-variant max-w-2xl mb-12 font-medium">
|
| 33 |
+
Transform complex codebases into human-readable READMEs, architecture maps, and API references in seconds. Just paste a URL.
|
| 34 |
+
</p>
|
| 35 |
+
|
| 36 |
+
{# ── URL Input ─────────────────────────────────────────────────────────────── #}
|
| 37 |
+
<div id="url-cluster" class="w-full max-w-2xl p-2 bg-surface-container-lowest rounded-xl shadow-2xl shadow-primary/5 flex flex-col sm:flex-row gap-2 transition-all focus-within:ring-4 focus-within:ring-primary/10">
|
| 38 |
+
<div class="flex-1 flex items-center px-4 gap-3">
|
| 39 |
+
<span class="material-symbols-outlined text-outline">link</span>
|
| 40 |
+
<input id="repo-url"
|
| 41 |
+
class="w-full bg-transparent border-none focus:ring-0 font-mono text-sm placeholder:text-outline/50 py-3 outline-none"
|
| 42 |
+
placeholder="https://github.com/username/repository"
|
| 43 |
+
type="text"
|
| 44 |
+
autocomplete="off"/>
|
| 45 |
+
</div>
|
| 46 |
+
<button id="analyze-btn"
|
| 47 |
+
onclick="analyzeRepo()"
|
| 48 |
+
class="bg-gradient-to-b from-primary to-primary-container text-on-primary px-8 py-3 rounded-lg font-bold text-sm tracking-tight hover:brightness-110 active:scale-[0.98] transition-all shadow-lg shadow-primary/20 flex items-center gap-2 justify-center">
|
| 49 |
+
<span id="analyze-icon" class="material-symbols-outlined text-base">search</span>
|
| 50 |
+
<span id="analyze-label">Analyze</span>
|
| 51 |
+
</button>
|
| 52 |
+
</div>
|
| 53 |
+
<div class="mt-6 flex flex-wrap justify-center items-center gap-6 text-xs font-mono text-outline uppercase tracking-widest">
|
| 54 |
+
<div class="flex items-center gap-2"><span class="material-symbols-outlined text-sm">check_circle</span> No Login Required</div>
|
| 55 |
+
<div class="flex items-center gap-2"><span class="material-symbols-outlined text-sm">check_circle</span> Instant Results</div>
|
| 56 |
+
<div class="flex items-center gap-2"><span class="material-symbols-outlined text-sm">check_circle</span> Free & Open Source</div>
|
| 57 |
+
</div>
|
| 58 |
+
|
| 59 |
+
{# ── Repo Preview Card (hidden until analyze) ──────────────────────────────── #}
|
| 60 |
+
<div id="repo-card" class="hidden w-full max-w-2xl mt-8 bg-surface-container-lowest rounded-xl p-6 text-left shadow-sm">
|
| 61 |
+
<div class="flex items-start justify-between gap-4 mb-6">
|
| 62 |
+
<div>
|
| 63 |
+
<a id="rc-link" href="#" target="_blank" class="text-xl font-bold text-on-surface hover:text-primary transition-colors font-mono"><span id="rc-name"></span></a>
|
| 64 |
+
<p id="rc-desc" class="text-sm text-on-surface-variant mt-1 leading-relaxed"></p>
|
| 65 |
+
</div>
|
| 66 |
+
<div id="rc-lang-badge" class="px-2 py-1 rounded bg-secondary-container text-on-secondary-container text-xs font-mono whitespace-nowrap"></div>
|
| 67 |
+
</div>
|
| 68 |
+
<div class="flex items-center gap-6 text-sm text-on-surface-variant mb-6">
|
| 69 |
+
<span class="flex items-center gap-1.5"><span class="material-symbols-outlined text-base">star</span> <span id="rc-stars"></span></span>
|
| 70 |
+
<span class="flex items-center gap-1.5"><span class="material-symbols-outlined text-base">fork_right</span> <span id="rc-forks"></span></span>
|
| 71 |
+
<span class="flex items-center gap-1.5"><span class="material-symbols-outlined text-base">folder</span> <span id="rc-files"></span> files</span>
|
| 72 |
+
</div>
|
| 73 |
+
|
| 74 |
+
{# Doc type checkboxes #}
|
| 75 |
+
<p class="text-xs font-bold uppercase tracking-widest text-on-surface-variant mb-3">Generate</p>
|
| 76 |
+
<div class="flex gap-3 mb-6 flex-wrap">
|
| 77 |
+
<label class="flex items-center gap-2 px-4 py-2 rounded-lg bg-surface-container cursor-pointer hover:bg-surface-container-high transition-colors text-sm font-medium">
|
| 78 |
+
<input type="checkbox" id="chk-readme" checked class="accent-primary rounded"> README
|
| 79 |
+
</label>
|
| 80 |
+
<label class="flex items-center gap-2 px-4 py-2 rounded-lg bg-surface-container cursor-pointer hover:bg-surface-container-high transition-colors text-sm font-medium">
|
| 81 |
+
<input type="checkbox" id="chk-architecture" checked class="accent-primary rounded"> Architecture
|
| 82 |
+
</label>
|
| 83 |
+
<label class="flex items-center gap-2 px-4 py-2 rounded-lg bg-surface-container cursor-pointer hover:bg-surface-container-high transition-colors text-sm font-medium">
|
| 84 |
+
<input type="checkbox" id="chk-api" checked class="accent-primary rounded"> API Docs
|
| 85 |
+
</label>
|
| 86 |
+
</div>
|
| 87 |
+
|
| 88 |
+
<button id="generate-btn"
|
| 89 |
+
onclick="generateDocs()"
|
| 90 |
+
class="w-full bg-gradient-to-b from-primary to-primary-container text-on-primary py-3 rounded-lg font-bold text-sm tracking-tight hover:brightness-110 active:scale-[0.98] transition-all shadow-lg shadow-primary/20 flex items-center justify-center gap-2">
|
| 91 |
+
<span class="material-symbols-outlined text-base">auto_awesome</span>
|
| 92 |
+
Generate Documentation
|
| 93 |
+
</button>
|
| 94 |
+
</div>
|
| 95 |
+
|
| 96 |
+
{# ── Progress indicator (hidden until generating) ─────────────────────────── #}
|
| 97 |
+
<div id="progress-card" class="hidden w-full max-w-2xl mt-8 bg-surface-container-lowest rounded-xl p-8 text-center">
|
| 98 |
+
<div class="flex flex-col items-center gap-4">
|
| 99 |
+
<div class="w-12 h-12 rounded-xl bg-primary-fixed flex items-center justify-center animate-pulse">
|
| 100 |
+
<span class="material-symbols-outlined text-primary text-2xl">psychology</span>
|
| 101 |
+
</div>
|
| 102 |
+
<p class="font-bold text-on-surface">Analyzing codebase…</p>
|
| 103 |
+
<p id="progress-msg" class="text-sm text-on-surface-variant font-mono">Fetching repository structure</p>
|
| 104 |
+
<div class="w-full bg-surface-container-high rounded-full h-1.5 overflow-hidden">
|
| 105 |
+
<div id="progress-bar" class="h-full bg-gradient-to-r from-primary to-primary-container rounded-full transition-all duration-500" style="width: 0%"></div>
|
| 106 |
+
</div>
|
| 107 |
+
</div>
|
| 108 |
+
</div>
|
| 109 |
+
</section>
|
| 110 |
+
|
| 111 |
+
{# ── Bento: Features ──────────────────────────────────────────────────────── #}
|
| 112 |
+
<section class="max-w-7xl mx-auto px-6 py-12">
|
| 113 |
+
<div class="grid grid-cols-1 md:grid-cols-3 grid-rows-2 gap-4 md:h-[560px]">
|
| 114 |
+
|
| 115 |
+
{# Main card #}
|
| 116 |
+
<div class="md:col-span-2 md:row-span-2 bg-surface-container-lowest p-10 rounded-xl relative overflow-hidden group">
|
| 117 |
+
<div class="relative z-10 h-full flex flex-col">
|
| 118 |
+
<div class="w-12 h-12 bg-primary-fixed rounded-lg flex items-center justify-center mb-6">
|
| 119 |
+
<span class="material-symbols-outlined text-primary">psychology</span>
|
| 120 |
+
</div>
|
| 121 |
+
<h3 class="text-3xl font-bold tracking-tight mb-4 text-on-surface">Understand any codebase in seconds.</h3>
|
| 122 |
+
<p class="text-on-surface-variant max-w-sm mb-8 font-medium">
|
| 123 |
+
DocForge parses file trees, function relationships, and logic flows to explain
|
| 124 |
+
<span class="italic text-on-surface">why</span> code works — not just what it does.
|
| 125 |
+
</p>
|
| 126 |
+
<div class="mt-auto pt-8 flex items-center justify-between">
|
| 127 |
+
<div class="flex items-center gap-2 text-xs font-mono text-outline">
|
| 128 |
+
<span class="material-symbols-outlined text-sm">token</span>
|
| 129 |
+
Multi-provider AI backbone
|
| 130 |
+
</div>
|
| 131 |
+
</div>
|
| 132 |
+
</div>
|
| 133 |
+
<div class="absolute right-0 bottom-0 w-2/3 h-2/3 opacity-30 group-hover:opacity-50 transition-opacity">
|
| 134 |
+
<div class="bg-gradient-to-br from-primary/20 to-transparent w-full h-full rounded-tl-[100px]"></div>
|
| 135 |
+
</div>
|
| 136 |
+
<div class="absolute top-10 right-10 flex flex-col gap-2 scale-75 opacity-40 group-hover:opacity-90 transition-all group-hover:scale-100 origin-top-right">
|
| 137 |
+
<div class="bg-surface-container-highest px-3 py-1.5 rounded-lg text-[10px] font-mono">PARSING TREE... [DONE]</div>
|
| 138 |
+
<div class="bg-surface-container-highest px-3 py-1.5 rounded-lg text-[10px] font-mono">MAPPING DEPS... [DONE]</div>
|
| 139 |
+
<div class="bg-surface-container-highest px-3 py-1.5 rounded-lg text-[10px] font-mono">GENERATING DOCS... [OK]</div>
|
| 140 |
+
</div>
|
| 141 |
+
</div>
|
| 142 |
+
|
| 143 |
+
{# Architecture card #}
|
| 144 |
+
<div class="bg-surface-container-low p-8 rounded-xl flex flex-col justify-between">
|
| 145 |
+
<div>
|
| 146 |
+
<div class="w-10 h-10 bg-tertiary-fixed rounded-lg flex items-center justify-center mb-4">
|
| 147 |
+
<span class="material-symbols-outlined text-tertiary">account_tree</span>
|
| 148 |
+
</div>
|
| 149 |
+
<h4 class="text-xl font-bold tracking-tight mb-2">Visual Architecture Maps.</h4>
|
| 150 |
+
<p class="text-sm text-on-surface-variant leading-relaxed">
|
| 151 |
+
Auto-generated Mermaid diagrams show how data flows through the system.
|
| 152 |
+
</p>
|
| 153 |
+
</div>
|
| 154 |
+
<div class="mt-6 p-3 bg-surface-container-lowest rounded-lg flex items-center gap-3">
|
| 155 |
+
<span class="material-symbols-outlined text-primary text-sm">schema</span>
|
| 156 |
+
<span class="text-[10px] font-mono uppercase tracking-widest text-outline">Mermaid · Flowchart · ER</span>
|
| 157 |
+
</div>
|
| 158 |
+
</div>
|
| 159 |
+
|
| 160 |
+
{# Export card #}
|
| 161 |
+
<div class="bg-inverse-surface p-8 rounded-xl flex flex-col justify-between">
|
| 162 |
+
<div>
|
| 163 |
+
<div class="w-10 h-10 bg-slate-700 rounded-lg flex items-center justify-center mb-4">
|
| 164 |
+
<span class="material-symbols-outlined text-white">data_object</span>
|
| 165 |
+
</div>
|
| 166 |
+
<h4 class="text-xl font-bold tracking-tight text-white mb-2">Export Ready.</h4>
|
| 167 |
+
<p class="text-sm text-slate-400 leading-relaxed">
|
| 168 |
+
Download README.md, architecture.json, or api.json directly to your project.
|
| 169 |
+
</p>
|
| 170 |
+
</div>
|
| 171 |
+
<div class="mt-4 flex gap-2 items-center">
|
| 172 |
+
<div class="flex-1 h-1 bg-slate-700 rounded-full overflow-hidden">
|
| 173 |
+
<div class="w-[85%] h-full bg-primary rounded-full"></div>
|
| 174 |
+
</div>
|
| 175 |
+
<span class="text-[10px] font-mono text-slate-500">MD · JSON</span>
|
| 176 |
+
</div>
|
| 177 |
+
</div>
|
| 178 |
+
</div>
|
| 179 |
+
</section>
|
| 180 |
+
|
| 181 |
+
{# ── Feature Spotlight ────────────────────────────────────────────────────── #}
|
| 182 |
+
<section class="max-w-7xl mx-auto px-6 py-20">
|
| 183 |
+
<div class="flex flex-col lg:flex-row items-center gap-16">
|
| 184 |
+
<div class="w-full lg:w-1/2">
|
| 185 |
+
<h2 class="text-4xl font-extrabold tracking-tighter mb-6 text-on-surface">The complete technical ledger for any project.</h2>
|
| 186 |
+
<div class="space-y-8">
|
| 187 |
+
<div class="flex gap-4">
|
| 188 |
+
<div class="flex-shrink-0 w-6 h-6 rounded-full bg-primary/10 flex items-center justify-center">
|
| 189 |
+
<span class="material-symbols-outlined text-sm text-primary">description</span>
|
| 190 |
+
</div>
|
| 191 |
+
<div>
|
| 192 |
+
<h5 class="font-bold text-on-surface">AI-Written README</h5>
|
| 193 |
+
<p class="text-sm text-on-surface-variant">A complete, professional README with setup instructions, features, and badges — generated from your code.</p>
|
| 194 |
+
</div>
|
| 195 |
+
</div>
|
| 196 |
+
<div class="flex gap-4">
|
| 197 |
+
<div class="flex-shrink-0 w-6 h-6 rounded-full bg-primary/10 flex items-center justify-center">
|
| 198 |
+
<span class="material-symbols-outlined text-sm text-primary">account_tree</span>
|
| 199 |
+
</div>
|
| 200 |
+
<div>
|
| 201 |
+
<h5 class="font-bold text-on-surface">Architecture Blueprint</h5>
|
| 202 |
+
<p class="text-sm text-on-surface-variant">Component maps, data flow diagrams, and a Mermaid chart you can drop straight into documentation.</p>
|
| 203 |
+
</div>
|
| 204 |
+
</div>
|
| 205 |
+
<div class="flex gap-4">
|
| 206 |
+
<div class="flex-shrink-0 w-6 h-6 rounded-full bg-primary/10 flex items-center justify-center">
|
| 207 |
+
<span class="material-symbols-outlined text-sm text-primary">api</span>
|
| 208 |
+
</div>
|
| 209 |
+
<div>
|
| 210 |
+
<h5 class="font-bold text-on-surface">API Reference</h5>
|
| 211 |
+
<p class="text-sm text-on-surface-variant">Auto-detected endpoints, functions, and classes with parameters and return types documented.</p>
|
| 212 |
+
</div>
|
| 213 |
+
</div>
|
| 214 |
+
</div>
|
| 215 |
+
</div>
|
| 216 |
+
|
| 217 |
+
{# Mock doc preview #}
|
| 218 |
+
<div class="w-full lg:w-1/2 bg-surface-container-low rounded-xl p-4 relative">
|
| 219 |
+
<div class="bg-surface-container-lowest rounded-lg overflow-hidden shadow-2xl">
|
| 220 |
+
<div class="bg-inverse-surface px-4 py-2 flex items-center justify-between">
|
| 221 |
+
<div class="flex gap-1.5">
|
| 222 |
+
<div class="w-2.5 h-2.5 rounded-full bg-red-500/50"></div>
|
| 223 |
+
<div class="w-2.5 h-2.5 rounded-full bg-yellow-500/50"></div>
|
| 224 |
+
<div class="w-2.5 h-2.5 rounded-full bg-green-500/50"></div>
|
| 225 |
+
</div>
|
| 226 |
+
<span class="text-[10px] text-slate-500 font-mono">docforge.ai/docs/owner/repo</span>
|
| 227 |
+
</div>
|
| 228 |
+
<div class="p-6">
|
| 229 |
+
<div class="flex items-center gap-3 mb-6">
|
| 230 |
+
<div class="w-10 h-10 rounded-full bg-primary flex items-center justify-center">
|
| 231 |
+
<span class="material-symbols-outlined text-white text-xl">auto_stories</span>
|
| 232 |
+
</div>
|
| 233 |
+
<div>
|
| 234 |
+
<h6 class="text-xs font-mono text-outline uppercase tracking-tighter">Documentation Preview</h6>
|
| 235 |
+
<p class="font-bold text-on-surface">README.md</p>
|
| 236 |
+
</div>
|
| 237 |
+
</div>
|
| 238 |
+
<div class="space-y-3">
|
| 239 |
+
<div class="h-2 w-[40%] bg-surface-container rounded-full"></div>
|
| 240 |
+
<div class="h-2 w-[90%] bg-surface-container rounded-full"></div>
|
| 241 |
+
<div class="h-2 w-[70%] bg-surface-container rounded-full"></div>
|
| 242 |
+
<div class="pt-4 flex flex-wrap gap-2">
|
| 243 |
+
<div class="px-2 py-1 rounded bg-secondary-container text-on-secondary-container text-[10px] font-mono">Python</div>
|
| 244 |
+
<div class="px-2 py-1 rounded bg-tertiary-fixed text-on-tertiary-fixed text-[10px] font-mono">Flask</div>
|
| 245 |
+
<div class="px-2 py-1 rounded bg-primary-fixed text-on-primary-fixed text-[10px] font-mono">MIT</div>
|
| 246 |
+
</div>
|
| 247 |
+
</div>
|
| 248 |
+
</div>
|
| 249 |
+
</div>
|
| 250 |
+
<div class="absolute -z-10 -right-4 -bottom-4 w-full h-full bg-primary/5 rounded-xl blur-3xl"></div>
|
| 251 |
+
</div>
|
| 252 |
+
</div>
|
| 253 |
+
</section>
|
| 254 |
+
|
| 255 |
+
{# ── Recent Repos ─────────────────────────────────────────────────────────── #}
|
| 256 |
+
{% if recent %}
|
| 257 |
+
<section class="max-w-7xl mx-auto px-6 py-12">
|
| 258 |
+
<h2 class="text-2xl font-bold tracking-tight text-on-surface mb-8">Recent Repositories</h2>
|
| 259 |
+
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
| 260 |
+
{% for r in recent %}
|
| 261 |
+
<a href="/docs/{{ r.owner }}/{{ r.repo }}"
|
| 262 |
+
class="group block bg-surface-container-lowest p-6 rounded-xl hover:bg-surface-container transition-colors">
|
| 263 |
+
<div class="flex items-start justify-between mb-3">
|
| 264 |
+
<div>
|
| 265 |
+
<p class="font-mono text-xs text-outline mb-1">{{ r.owner }}</p>
|
| 266 |
+
<h3 class="font-bold text-on-surface group-hover:text-primary transition-colors">{{ r.repo }}</h3>
|
| 267 |
+
</div>
|
| 268 |
+
{% if r.info.language %}
|
| 269 |
+
<span class="px-2 py-0.5 rounded bg-secondary-container text-on-secondary-container text-[10px] font-mono">{{ r.info.language }}</span>
|
| 270 |
+
{% endif %}
|
| 271 |
+
</div>
|
| 272 |
+
{% if r.info.description %}
|
| 273 |
+
<p class="text-sm text-on-surface-variant line-clamp-2 mb-4">{{ r.info.description }}</p>
|
| 274 |
+
{% endif %}
|
| 275 |
+
<div class="flex items-center gap-4 text-xs text-outline">
|
| 276 |
+
<span class="flex items-center gap-1"><span class="material-symbols-outlined text-sm">star</span> {{ r.info.stars }}</span>
|
| 277 |
+
<span class="flex items-center gap-1"><span class="material-symbols-outlined text-sm">description</span> {{ r.doc_count }} docs</span>
|
| 278 |
+
</div>
|
| 279 |
+
</a>
|
| 280 |
+
{% endfor %}
|
| 281 |
+
</div>
|
| 282 |
+
</section>
|
| 283 |
+
{% endif %}
|
| 284 |
+
|
| 285 |
+
{# ── CTA ──────────────────────────────────────────────────────────────────── #}
|
| 286 |
+
<section class="max-w-5xl mx-auto px-6 py-20">
|
| 287 |
+
<div class="bg-surface-container-lowest rounded-xl p-12 lg:p-20 text-center relative overflow-hidden">
|
| 288 |
+
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-primary via-primary-container to-primary"></div>
|
| 289 |
+
<h2 class="text-4xl font-extrabold tracking-tight mb-4 text-on-surface">Ready to document your code?</h2>
|
| 290 |
+
<p class="text-on-surface-variant mb-10 max-w-xl mx-auto">Paste any public GitHub URL above and get a full documentation suite in under a minute.</p>
|
| 291 |
+
<button onclick="document.getElementById('repo-url').focus(); window.scrollTo({top: 0, behavior: 'smooth'})"
|
| 292 |
+
class="bg-gradient-to-b from-primary to-primary-container text-on-primary px-10 py-4 rounded-lg font-bold text-base shadow-xl shadow-primary/20 hover:scale-105 transition-transform">
|
| 293 |
+
Get Started Free
|
| 294 |
+
</button>
|
| 295 |
+
</div>
|
| 296 |
+
</section>
|
| 297 |
+
|
| 298 |
+
</main>
|
| 299 |
+
|
| 300 |
+
{# ── Footer ───────────────────────────────────────────────────────────────── #}
|
| 301 |
+
<footer class="bg-surface-container-lowest py-12">
|
| 302 |
+
<div class="max-w-7xl mx-auto px-6 flex flex-col md:flex-row justify-between items-center gap-8">
|
| 303 |
+
<div class="flex flex-col items-center md:items-start">
|
| 304 |
+
<span class="text-lg font-bold tracking-tight text-on-surface mb-1">DocForge</span>
|
| 305 |
+
<p class="text-xs text-outline font-medium">AI-powered documentation for every GitHub repository.</p>
|
| 306 |
+
</div>
|
| 307 |
+
<div class="flex gap-8">
|
| 308 |
+
<a href="https://github.com/Moealsarraj/doc-forge" target="_blank" class="text-sm font-medium text-on-surface-variant hover:text-primary transition-colors">GitHub</a>
|
| 309 |
+
</div>
|
| 310 |
+
</div>
|
| 311 |
+
</footer>
|
| 312 |
+
{% endblock %}
|
| 313 |
+
|
| 314 |
+
{% block extra_scripts %}
|
| 315 |
+
<script>
|
| 316 |
+
// CSRF helper
|
| 317 |
+
const _csrf = document.querySelector('meta[name="csrf-token"]')?.content || '';
|
| 318 |
+
function _post(url, body) {
|
| 319 |
+
return fetch(url, {
|
| 320 |
+
method: 'POST',
|
| 321 |
+
headers: {'Content-Type': 'application/json', 'X-CSRFToken': _csrf},
|
| 322 |
+
body: JSON.stringify(body)
|
| 323 |
+
});
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
// State
|
| 327 |
+
let _currentOwner = null;
|
| 328 |
+
let _currentRepo = null;
|
| 329 |
+
|
| 330 |
+
function setProgress(pct, msg) {
|
| 331 |
+
document.getElementById('progress-bar').style.width = pct + '%';
|
| 332 |
+
document.getElementById('progress-msg').textContent = msg;
|
| 333 |
+
}
|
| 334 |
+
|
| 335 |
+
async function analyzeRepo() {
|
| 336 |
+
const url = document.getElementById('repo-url').value.trim();
|
| 337 |
+
if (!url) return;
|
| 338 |
+
|
| 339 |
+
// Show progress
|
| 340 |
+
document.getElementById('repo-card').classList.add('hidden');
|
| 341 |
+
document.getElementById('progress-card').classList.remove('hidden');
|
| 342 |
+
const btn = document.getElementById('analyze-btn');
|
| 343 |
+
btn.disabled = true;
|
| 344 |
+
document.getElementById('analyze-label').textContent = 'Analyzing…';
|
| 345 |
+
document.getElementById('analyze-icon').textContent = 'hourglass_top';
|
| 346 |
+
setProgress(10, 'Connecting to GitHub API…');
|
| 347 |
+
|
| 348 |
+
try {
|
| 349 |
+
setProgress(30, 'Fetching repository metadata…');
|
| 350 |
+
const res = await _post('/api/analyze', {url});
|
| 351 |
+
const data = await res.json();
|
| 352 |
+
if (!res.ok) throw new Error(data.error || 'Analysis failed');
|
| 353 |
+
|
| 354 |
+
setProgress(100, 'Done!');
|
| 355 |
+
_currentOwner = data.owner;
|
| 356 |
+
_currentRepo = data.repo;
|
| 357 |
+
|
| 358 |
+
// Populate card
|
| 359 |
+
const info = data.info;
|
| 360 |
+
document.getElementById('rc-name').textContent = info.full_name;
|
| 361 |
+
document.getElementById('rc-link').href = info.url;
|
| 362 |
+
document.getElementById('rc-desc').textContent = info.description || 'No description provided.';
|
| 363 |
+
document.getElementById('rc-lang-badge').textContent = info.language || 'Unknown';
|
| 364 |
+
document.getElementById('rc-stars').textContent = (info.stars || 0).toLocaleString() + ' stars';
|
| 365 |
+
document.getElementById('rc-forks').textContent = (info.forks || 0).toLocaleString() + ' forks';
|
| 366 |
+
document.getElementById('rc-files').textContent = data.file_count;
|
| 367 |
+
|
| 368 |
+
setTimeout(() => {
|
| 369 |
+
document.getElementById('progress-card').classList.add('hidden');
|
| 370 |
+
document.getElementById('repo-card').classList.remove('hidden');
|
| 371 |
+
}, 400);
|
| 372 |
+
} catch (err) {
|
| 373 |
+
document.getElementById('progress-card').classList.add('hidden');
|
| 374 |
+
alert('Error: ' + err.message);
|
| 375 |
+
} finally {
|
| 376 |
+
btn.disabled = false;
|
| 377 |
+
document.getElementById('analyze-label').textContent = 'Analyze';
|
| 378 |
+
document.getElementById('analyze-icon').textContent = 'search';
|
| 379 |
+
}
|
| 380 |
+
}
|
| 381 |
+
|
| 382 |
+
async function generateDocs() {
|
| 383 |
+
if (!_currentOwner) return;
|
| 384 |
+
const url = document.getElementById('repo-url').value.trim();
|
| 385 |
+
const types = [];
|
| 386 |
+
if (document.getElementById('chk-readme').checked) types.push('readme');
|
| 387 |
+
if (document.getElementById('chk-architecture').checked) types.push('architecture');
|
| 388 |
+
if (document.getElementById('chk-api').checked) types.push('api');
|
| 389 |
+
if (!types.length) { alert('Select at least one doc type.'); return; }
|
| 390 |
+
|
| 391 |
+
document.getElementById('repo-card').classList.add('hidden');
|
| 392 |
+
document.getElementById('progress-card').classList.remove('hidden');
|
| 393 |
+
setProgress(5, 'Starting AI generation…');
|
| 394 |
+
|
| 395 |
+
const steps = [
|
| 396 |
+
[20, 'Fetching key source files…'],
|
| 397 |
+
[45, 'Generating README…'],
|
| 398 |
+
[65, 'Mapping architecture…'],
|
| 399 |
+
[80, 'Documenting API…'],
|
| 400 |
+
[92, 'Saving to database…']
|
| 401 |
+
];
|
| 402 |
+
let si = 0;
|
| 403 |
+
const ticker = setInterval(() => {
|
| 404 |
+
if (si < steps.length) { setProgress(...steps[si++]); }
|
| 405 |
+
}, 1800);
|
| 406 |
+
|
| 407 |
+
try {
|
| 408 |
+
const res = await _post('/api/generate', {url, types});
|
| 409 |
+
const data = await res.json();
|
| 410 |
+
clearInterval(ticker);
|
| 411 |
+
if (!res.ok) throw new Error(data.error || 'Generation failed');
|
| 412 |
+
|
| 413 |
+
setProgress(100, 'Complete! Redirecting…');
|
| 414 |
+
setTimeout(() => {
|
| 415 |
+
window.location.href = `/docs/${data.owner}/${data.repo}`;
|
| 416 |
+
}, 600);
|
| 417 |
+
} catch (err) {
|
| 418 |
+
clearInterval(ticker);
|
| 419 |
+
document.getElementById('progress-card').classList.add('hidden');
|
| 420 |
+
document.getElementById('repo-card').classList.remove('hidden');
|
| 421 |
+
alert('Error: ' + err.message);
|
| 422 |
+
}
|
| 423 |
+
}
|
| 424 |
+
|
| 425 |
+
// Allow Enter key in input
|
| 426 |
+
document.getElementById('repo-url').addEventListener('keydown', e => {
|
| 427 |
+
if (e.key === 'Enter') analyzeRepo();
|
| 428 |
+
});
|
| 429 |
+
</script>
|
| 430 |
+
{% endblock %}
|
app/tools/git_narrator/__init__.py
ADDED
|
File without changes
|
app/tools/git_narrator/__pycache__/__init__.cpython-314.pyc
ADDED
|
Binary file (176 Bytes). View file
|
|
|
app/tools/git_narrator/__pycache__/narrator.cpython-314.pyc
ADDED
|
Binary file (5.85 kB). View file
|
|
|
app/tools/git_narrator/__pycache__/routes.cpython-314.pyc
ADDED
|
Binary file (1.51 kB). View file
|
|
|
app/tools/git_narrator/narrator.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Git history narrator — turns raw commits into editorial prose."""
|
| 2 |
+
import os
|
| 3 |
+
import re
|
| 4 |
+
import requests
|
| 5 |
+
from app.core.ai import call_ai_json
|
| 6 |
+
|
| 7 |
+
_GITHUB_API = "https://api.github.com"
|
| 8 |
+
_SYSTEM = """You are a senior engineering writer and technical storyteller.
|
| 9 |
+
You transform dry git commit logs into compelling, editorial-quality engineering narratives.
|
| 10 |
+
Write like a thoughtful tech lead preparing a sprint retrospective for the whole company.
|
| 11 |
+
Be specific about what was built, avoid jargon where plain language works, and surface the human story behind the code.
|
| 12 |
+
Return ONLY valid JSON — no markdown fences."""
|
| 13 |
+
|
| 14 |
+
_PROMPT_TMPL = """Narrate the following git history into an editorial engineering report.
|
| 15 |
+
|
| 16 |
+
GIT HISTORY:
|
| 17 |
+
---
|
| 18 |
+
{log}
|
| 19 |
+
---
|
| 20 |
+
|
| 21 |
+
Return a JSON object with EXACTLY these keys:
|
| 22 |
+
{{
|
| 23 |
+
"period_label": "<inferred time period, e.g. 'Sprint 42' or 'Week of Jan 15, 2025'>",
|
| 24 |
+
"highlights": [
|
| 25 |
+
{{
|
| 26 |
+
"title": "<punchy title for the biggest story>",
|
| 27 |
+
"narrative": "<2-3 paragraph editorial story — what changed, why it matters, what comes next>",
|
| 28 |
+
"key_commit": "<most important commit message verbatim>",
|
| 29 |
+
"impact": "<one sentence business impact>"
|
| 30 |
+
}}
|
| 31 |
+
],
|
| 32 |
+
"tech_debt": [
|
| 33 |
+
{{
|
| 34 |
+
"icon": "<material symbol name, e.g. cleaning_services|speed|bug_report|tune>",
|
| 35 |
+
"title": "<short title>",
|
| 36 |
+
"description": "<2-3 sentences on what was fixed/refactored and the measurable benefit>"
|
| 37 |
+
}}
|
| 38 |
+
],
|
| 39 |
+
"milestones": [
|
| 40 |
+
{{
|
| 41 |
+
"status": "<shipped|in_progress|planned>",
|
| 42 |
+
"title": "<milestone name>",
|
| 43 |
+
"narrative": "<editorial paragraph on this milestone>",
|
| 44 |
+
"contributors": ["<name or handle>"]
|
| 45 |
+
}}
|
| 46 |
+
],
|
| 47 |
+
"commits": [
|
| 48 |
+
{{
|
| 49 |
+
"hash": "<7-char hash or empty string>",
|
| 50 |
+
"message": "<commit message>",
|
| 51 |
+
"author": "<author name or handle>",
|
| 52 |
+
"time": "<relative time e.g. '2 hours ago'>",
|
| 53 |
+
"type": "<feat|fix|docs|refactor|chore|test|perf>"
|
| 54 |
+
}}
|
| 55 |
+
],
|
| 56 |
+
"summary_stats": {{
|
| 57 |
+
"total_commits": <integer>,
|
| 58 |
+
"contributors": <integer>,
|
| 59 |
+
"features": <integer>,
|
| 60 |
+
"fixes": <integer>
|
| 61 |
+
}}
|
| 62 |
+
}}
|
| 63 |
+
|
| 64 |
+
Aim for 2-3 highlights, 2-3 tech_debt items, 2-3 milestones.
|
| 65 |
+
If the log is sparse, extrapolate intelligently from what's there.
|
| 66 |
+
commits should list ALL commits from the log (max 20)."""
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def _fetch_github_commits(owner: str, repo: str, limit: int = 30) -> str:
|
| 70 |
+
"""Fetch recent commits from GitHub and format as git log text."""
|
| 71 |
+
token = os.environ.get("GITHUB_TOKEN", "")
|
| 72 |
+
headers = {"Accept": "application/vnd.github.v3+json"}
|
| 73 |
+
if token:
|
| 74 |
+
headers["Authorization"] = f"Bearer {token}"
|
| 75 |
+
r = requests.get(f"{_GITHUB_API}/repos/{owner}/{repo}/commits?per_page={limit}",
|
| 76 |
+
headers=headers, timeout=20)
|
| 77 |
+
r.raise_for_status()
|
| 78 |
+
lines = []
|
| 79 |
+
for c in r.json():
|
| 80 |
+
sha = c["sha"][:7]
|
| 81 |
+
msg = c["commit"]["message"].split("\n")[0]
|
| 82 |
+
name = c["commit"]["author"]["name"]
|
| 83 |
+
date = c["commit"]["author"]["date"]
|
| 84 |
+
lines.append(f"commit {sha}\nAuthor: {name}\nDate: {date}\n\n {msg}\n")
|
| 85 |
+
return "\n".join(lines)
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def _parse_github_url(text: str):
|
| 89 |
+
"""Return (owner, repo) if text looks like a GitHub URL, else None."""
|
| 90 |
+
m = re.search(r"github\.com/([^/\s]+)/([^/\s]+?)(?:\.git)?(?:\s|$)", text)
|
| 91 |
+
if m:
|
| 92 |
+
return m.group(1), m.group(2)
|
| 93 |
+
parts = text.strip().split("/")
|
| 94 |
+
if len(parts) == 2 and " " not in text:
|
| 95 |
+
return parts[0], parts[1]
|
| 96 |
+
return None
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
def narrate(raw_input: str) -> dict:
|
| 100 |
+
"""Narrate a git history. raw_input can be a GitHub URL or raw git log text."""
|
| 101 |
+
parsed = _parse_github_url(raw_input.strip())
|
| 102 |
+
if parsed:
|
| 103 |
+
try:
|
| 104 |
+
owner, repo = parsed
|
| 105 |
+
log = _fetch_github_commits(owner, repo)
|
| 106 |
+
except Exception as e:
|
| 107 |
+
log = raw_input # fallback to treating as raw log
|
| 108 |
+
else:
|
| 109 |
+
log = raw_input
|
| 110 |
+
|
| 111 |
+
prompt = _PROMPT_TMPL.format(log=log[:8000])
|
| 112 |
+
return call_ai_json([{"role": "user", "content": prompt}], system=_SYSTEM) or {}
|
app/tools/git_narrator/routes.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Git Narrator routes."""
|
| 2 |
+
from flask import Blueprint, render_template, request, jsonify
|
| 3 |
+
from .narrator import narrate
|
| 4 |
+
|
| 5 |
+
bp = Blueprint("git_narrator", __name__, template_folder="templates")
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
@bp.route("/")
|
| 9 |
+
def index():
|
| 10 |
+
return render_template("git_narrator/index.html")
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
@bp.route("/api/narrate", methods=["POST"])
|
| 14 |
+
def api_narrate():
|
| 15 |
+
body = request.get_json(silent=True) or {}
|
| 16 |
+
raw = (body.get("content") or "").strip()
|
| 17 |
+
if not raw:
|
| 18 |
+
return jsonify({"error": "content is required (GitHub URL or git log text)"}), 400
|
| 19 |
+
if len(raw) < 10:
|
| 20 |
+
return jsonify({"error": "Input too short — paste a URL or git log"}), 400
|
| 21 |
+
result = narrate(raw)
|
| 22 |
+
return jsonify(result)
|
app/tools/git_narrator/templates/git_narrator/index.html
ADDED
|
@@ -0,0 +1,509 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
{% block title %}Git Narrator — Engineering Stories{% endblock %}
|
| 3 |
+
|
| 4 |
+
{% block content %}
|
| 5 |
+
|
| 6 |
+
{# ── Toolbar ─────────────────────────────────────────────────────────────────── #}
|
| 7 |
+
<header class="toolbar-blur fixed top-0 w-full z-50 flex items-center justify-between px-5 h-12 flex-shrink-0"
|
| 8 |
+
style="border-bottom:1px solid rgba(240,246,252,0.08);">
|
| 9 |
+
<div class="flex items-center gap-3">
|
| 10 |
+
<div class="flex items-center gap-2">
|
| 11 |
+
<span class="material-symbols-outlined text-[16px]" style="color:#58A6FF;">auto_stories</span>
|
| 12 |
+
<span class="font-semibold text-gd-text text-sm" style="letter-spacing:-0.02em;">Git Narrator</span>
|
| 13 |
+
</div>
|
| 14 |
+
<span style="color:rgba(240,246,252,0.12);">|</span>
|
| 15 |
+
<span class="section-label" style="color:#484F58;">Engineering Stories</span>
|
| 16 |
+
</div>
|
| 17 |
+
<div class="flex items-center gap-2">
|
| 18 |
+
<span id="period-chip" class="hidden pill" style="background:rgba(88,166,255,0.12); color:#58A6FF;"></span>
|
| 19 |
+
<button id="copy-btn" class="hidden btn-ghost text-xs">
|
| 20 |
+
<span id="copy-icon" class="material-symbols-outlined text-[13px]">content_copy</span>
|
| 21 |
+
Copy
|
| 22 |
+
</button>
|
| 23 |
+
<button id="narrate-btn" onclick="doNarrate()" class="btn-primary">
|
| 24 |
+
<span class="material-symbols-outlined text-[14px]">auto_awesome</span>
|
| 25 |
+
<span id="narrate-label">Narrate History</span>
|
| 26 |
+
</button>
|
| 27 |
+
</div>
|
| 28 |
+
</header>
|
| 29 |
+
|
| 30 |
+
{# ── Body ─────────────────────────────────────────────────────────────────────── #}
|
| 31 |
+
<div class="flex flex-1 pt-12 overflow-hidden">
|
| 32 |
+
|
| 33 |
+
{# ── Left: Terminal input zone ───────────────────────────────────────────────── #}
|
| 34 |
+
<aside class="w-[320px] flex-shrink-0 flex flex-col overflow-y-auto"
|
| 35 |
+
style="background:#0D1117; border-right:1px solid rgba(240,246,252,0.07);">
|
| 36 |
+
|
| 37 |
+
{# Panel label #}
|
| 38 |
+
<div class="flex items-center gap-2 px-4 py-3"
|
| 39 |
+
style="border-bottom:1px solid rgba(240,246,252,0.06);">
|
| 40 |
+
<span class="w-1.5 h-1.5 rounded-full" style="background:#3FB950;"></span>
|
| 41 |
+
<span class="section-label">Source</span>
|
| 42 |
+
</div>
|
| 43 |
+
|
| 44 |
+
{# Prompt hint #}
|
| 45 |
+
<div class="px-4 pt-3 pb-1.5 flex items-center gap-2">
|
| 46 |
+
<span class="text-[11px] font-mono" style="color:#3FB950;">$</span>
|
| 47 |
+
<span class="text-[10px] font-mono" style="color:#484F58;">paste url or git log below</span>
|
| 48 |
+
</div>
|
| 49 |
+
|
| 50 |
+
{# Input area #}
|
| 51 |
+
<div class="px-3 pb-3">
|
| 52 |
+
<div class="rounded-lg overflow-hidden"
|
| 53 |
+
style="background:#161B22; border:1px solid rgba(240,246,252,0.08);
|
| 54 |
+
box-shadow:inset 0 2px 8px rgba(0,0,0,0.3);">
|
| 55 |
+
<textarea id="log-input" class="input-terminal" style="height:200px;"
|
| 56 |
+
placeholder="https://github.com/org/repo # or raw git log: a3f1b2c feat: add OAuth2 login d4e5f6a fix: resolve race condition 7b8c9d0 docs: update API reference …"></textarea>
|
| 57 |
+
</div>
|
| 58 |
+
</div>
|
| 59 |
+
|
| 60 |
+
{# Run button #}
|
| 61 |
+
<div class="px-3 pb-4">
|
| 62 |
+
<button onclick="doNarrate()"
|
| 63 |
+
class="w-full flex items-center justify-center gap-2 py-2.5 rounded-lg font-mono text-[11px] font-semibold transition-all"
|
| 64 |
+
style="background:#238636; color:#fff; border:1px solid rgba(63,185,80,0.3); box-shadow:0 1px 4px rgba(0,0,0,0.4);">
|
| 65 |
+
<span class="material-symbols-outlined text-[14px]">auto_awesome</span>
|
| 66 |
+
narrate_history()
|
| 67 |
+
</button>
|
| 68 |
+
</div>
|
| 69 |
+
|
| 70 |
+
{# Divider #}
|
| 71 |
+
<div style="height:1px; background:rgba(240,246,252,0.06);"></div>
|
| 72 |
+
|
| 73 |
+
{# Stats #}
|
| 74 |
+
<div id="stats-area" class="hidden flex flex-col gap-2.5 px-3 pt-3">
|
| 75 |
+
<div class="flex items-center gap-1.5 px-1">
|
| 76 |
+
<span class="text-[11px] font-mono" style="color:#3FB950;">$</span>
|
| 77 |
+
<span class="section-label">git shortlog -sn</span>
|
| 78 |
+
</div>
|
| 79 |
+
<div class="grid grid-cols-2 gap-1.5">
|
| 80 |
+
<div class="rounded-lg p-3 text-center" style="background:#161B22; border:1px solid rgba(240,246,252,0.06);">
|
| 81 |
+
<p id="stat-commits" class="text-lg font-bold font-mono" style="color:#58A6FF;">—</p>
|
| 82 |
+
<p class="section-label mt-0.5">Commits</p>
|
| 83 |
+
</div>
|
| 84 |
+
<div class="rounded-lg p-3 text-center" style="background:#161B22; border:1px solid rgba(240,246,252,0.06);">
|
| 85 |
+
<p id="stat-contributors" class="text-lg font-bold font-mono" style="color:#3FB950;">—</p>
|
| 86 |
+
<p class="section-label mt-0.5">Authors</p>
|
| 87 |
+
</div>
|
| 88 |
+
<div class="rounded-lg p-3 text-center" style="background:#161B22; border:1px solid rgba(240,246,252,0.06);">
|
| 89 |
+
<p id="stat-features" class="text-lg font-bold font-mono" style="color:#D2A8FF;">—</p>
|
| 90 |
+
<p class="section-label mt-0.5">Features</p>
|
| 91 |
+
</div>
|
| 92 |
+
<div class="rounded-lg p-3 text-center" style="background:#161B22; border:1px solid rgba(240,246,252,0.06);">
|
| 93 |
+
<p id="stat-fixes" class="text-lg font-bold font-mono" style="color:#E3B341;">—</p>
|
| 94 |
+
<p class="section-label mt-0.5">Fixes</p>
|
| 95 |
+
</div>
|
| 96 |
+
</div>
|
| 97 |
+
</div>
|
| 98 |
+
|
| 99 |
+
{# Commits log #}
|
| 100 |
+
<div id="commits-area" class="hidden flex flex-col gap-1 px-3 pt-3 pb-4">
|
| 101 |
+
<div class="flex items-center gap-1.5 px-1 mb-1">
|
| 102 |
+
<span class="text-[11px] font-mono" style="color:#3FB950;">$</span>
|
| 103 |
+
<span class="section-label">git log --oneline</span>
|
| 104 |
+
</div>
|
| 105 |
+
<div id="commits-list" class="flex flex-col"></div>
|
| 106 |
+
</div>
|
| 107 |
+
|
| 108 |
+
</aside>
|
| 109 |
+
|
| 110 |
+
{# ── Right: Narrative output ─────────────────────────────────────────────────── #}
|
| 111 |
+
<main class="flex-1 overflow-y-auto" style="background:#161B22;">
|
| 112 |
+
|
| 113 |
+
{# Empty state #}
|
| 114 |
+
<div id="empty-state" class="h-full flex flex-col items-center justify-center text-center select-none px-8">
|
| 115 |
+
<div class="w-14 h-14 rounded-2xl flex items-center justify-center mb-5"
|
| 116 |
+
style="background:#1C2128; border:1px solid rgba(240,246,252,0.08);">
|
| 117 |
+
<span class="material-symbols-outlined text-2xl" style="color:#484F58;">auto_stories</span>
|
| 118 |
+
</div>
|
| 119 |
+
<p class="text-sm font-semibold text-gd-text mb-2" style="letter-spacing:-0.02em;">The Narrative</p>
|
| 120 |
+
<p class="text-xs text-gd-text-2 max-w-xs leading-relaxed">
|
| 121 |
+
Paste a GitHub URL or git log on the left. The AI writes your commit history as an editorial engineering report.
|
| 122 |
+
</p>
|
| 123 |
+
<div class="mt-5 flex flex-wrap justify-center gap-1.5">
|
| 124 |
+
{% for tag in ['Highlights', 'Tech Debt', 'Milestones', 'Contributors', 'Impact'] %}
|
| 125 |
+
<span class="pill" style="background:#1C2128; color:#484F58; border:1px solid rgba(240,246,252,0.06); font-size:9px;">{{ tag }}</span>
|
| 126 |
+
{% endfor %}
|
| 127 |
+
</div>
|
| 128 |
+
</div>
|
| 129 |
+
|
| 130 |
+
{# Loading state #}
|
| 131 |
+
<div id="loading-state" class="hidden h-full flex flex-col items-center justify-center text-center px-8">
|
| 132 |
+
<div class="w-14 h-14 rounded-2xl flex items-center justify-center mb-5"
|
| 133 |
+
style="background:rgba(88,166,255,0.08); border:1px solid rgba(88,166,255,0.15);">
|
| 134 |
+
<span class="material-symbols-outlined text-2xl" style="color:#58A6FF; animation:pulse 1.4s ease-in-out infinite;">psychology</span>
|
| 135 |
+
</div>
|
| 136 |
+
<p class="text-sm font-semibold text-gd-text mb-1.5" style="letter-spacing:-0.02em;">Writing the narrative…</p>
|
| 137 |
+
<p id="loading-msg" class="text-xs font-mono text-gd-text-3">Parsing commit history</p>
|
| 138 |
+
<div class="w-48 h-0.5 rounded-full overflow-hidden mt-5"
|
| 139 |
+
style="background:rgba(240,246,252,0.06);">
|
| 140 |
+
<div id="loading-bar" class="h-full rounded-full transition-all duration-700"
|
| 141 |
+
style="width:5%; background:#58A6FF;"></div>
|
| 142 |
+
</div>
|
| 143 |
+
</div>
|
| 144 |
+
|
| 145 |
+
{# Narrative output #}
|
| 146 |
+
<div id="narrative-output" class="hidden max-w-2xl mx-auto px-8 py-8 flex flex-col gap-10 pb-12">
|
| 147 |
+
|
| 148 |
+
{# Report header #}
|
| 149 |
+
<div class="flex items-end justify-between">
|
| 150 |
+
<div>
|
| 151 |
+
<p class="section-label mb-2">Engineering Report</p>
|
| 152 |
+
<h1 class="text-2xl font-bold text-gd-text" style="letter-spacing:-0.03em;">The Narrative</h1>
|
| 153 |
+
</div>
|
| 154 |
+
<span id="period-label-inline" class="text-xs font-mono text-gd-text-3 pb-1"></span>
|
| 155 |
+
</div>
|
| 156 |
+
|
| 157 |
+
<div style="height:1px; background:rgba(240,246,252,0.07);"></div>
|
| 158 |
+
|
| 159 |
+
<div id="highlights-section"></div>
|
| 160 |
+
<div id="tech-debt-section"></div>
|
| 161 |
+
<div id="milestones-section"></div>
|
| 162 |
+
|
| 163 |
+
</div>
|
| 164 |
+
</main>
|
| 165 |
+
</div>
|
| 166 |
+
|
| 167 |
+
{# ── Footer ─────────────────────────────────────────────────────────────────── #}
|
| 168 |
+
<footer class="toolbar-blur fixed bottom-0 w-full h-9 flex items-center justify-between px-5 flex-shrink-0"
|
| 169 |
+
style="border-top:1px solid rgba(240,246,252,0.07);">
|
| 170 |
+
<span class="section-label">Git Narrator v1.0</span>
|
| 171 |
+
<div class="flex items-center gap-1.5">
|
| 172 |
+
<span id="footer-dot" class="w-1.5 h-1.5 rounded-full" style="background:#3FB950;"></span>
|
| 173 |
+
<span id="footer-status" class="section-label" style="color:#3FB950;">Ready</span>
|
| 174 |
+
</div>
|
| 175 |
+
</footer>
|
| 176 |
+
|
| 177 |
+
<style>
|
| 178 |
+
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.4} }
|
| 179 |
+
</style>
|
| 180 |
+
|
| 181 |
+
{% endblock %}
|
| 182 |
+
|
| 183 |
+
{% block extra_scripts %}
|
| 184 |
+
<script>
|
| 185 |
+
const _csrf = document.querySelector('meta[name="csrf-token"]')?.content || '';
|
| 186 |
+
let _narrativeText = '';
|
| 187 |
+
|
| 188 |
+
function setStatus(msg, color='#3FB950') {
|
| 189 |
+
document.getElementById('footer-dot').style.background = color;
|
| 190 |
+
document.getElementById('footer-status').style.color = color;
|
| 191 |
+
document.getElementById('footer-status').textContent = msg;
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
async function doNarrate() {
|
| 195 |
+
const raw = document.getElementById('log-input').value.trim();
|
| 196 |
+
if (!raw) { document.getElementById('log-input').focus(); return; }
|
| 197 |
+
|
| 198 |
+
document.getElementById('empty-state').classList.add('hidden');
|
| 199 |
+
document.getElementById('narrative-output').classList.add('hidden');
|
| 200 |
+
document.getElementById('loading-state').classList.remove('hidden');
|
| 201 |
+
document.getElementById('stats-area').classList.add('hidden');
|
| 202 |
+
document.getElementById('commits-area').classList.add('hidden');
|
| 203 |
+
document.getElementById('copy-btn').classList.add('hidden');
|
| 204 |
+
document.getElementById('period-chip').classList.add('hidden');
|
| 205 |
+
|
| 206 |
+
const btn = document.getElementById('narrate-btn');
|
| 207 |
+
btn.disabled = true;
|
| 208 |
+
document.getElementById('narrate-label').textContent = 'Writing…';
|
| 209 |
+
setStatus('Processing…', '#E3B341');
|
| 210 |
+
|
| 211 |
+
let pct = 5;
|
| 212 |
+
const msgs = ['Parsing commit history…','Identifying key themes…','Writing highlights…','Building the narrative…'];
|
| 213 |
+
let mi = 0;
|
| 214 |
+
const ticker = setInterval(() => {
|
| 215 |
+
pct = Math.min(pct + 12, 88);
|
| 216 |
+
document.getElementById('loading-bar').style.width = pct + '%';
|
| 217 |
+
document.getElementById('loading-msg').textContent = msgs[mi++ % msgs.length];
|
| 218 |
+
}, 1400);
|
| 219 |
+
|
| 220 |
+
try {
|
| 221 |
+
const res = await fetch('/api/narrate', {
|
| 222 |
+
method: 'POST',
|
| 223 |
+
headers: {'Content-Type':'application/json','X-CSRFToken':_csrf},
|
| 224 |
+
body: JSON.stringify({content: raw})
|
| 225 |
+
});
|
| 226 |
+
const data = await res.json();
|
| 227 |
+
clearInterval(ticker);
|
| 228 |
+
document.getElementById('loading-bar').style.width = '100%';
|
| 229 |
+
if (!res.ok) throw new Error(data.error || 'Narration failed');
|
| 230 |
+
renderNarrative(data);
|
| 231 |
+
setStatus('Done');
|
| 232 |
+
} catch(err) {
|
| 233 |
+
clearInterval(ticker);
|
| 234 |
+
document.getElementById('loading-state').classList.add('hidden');
|
| 235 |
+
document.getElementById('empty-state').classList.remove('hidden');
|
| 236 |
+
setStatus('Error', '#F85149');
|
| 237 |
+
alert('Error: ' + err.message);
|
| 238 |
+
} finally {
|
| 239 |
+
btn.disabled = false;
|
| 240 |
+
document.getElementById('narrate-label').textContent = 'Narrate History';
|
| 241 |
+
}
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
function renderNarrative(data) {
|
| 245 |
+
document.getElementById('loading-state').classList.add('hidden');
|
| 246 |
+
|
| 247 |
+
const periodText = data.period_label || '';
|
| 248 |
+
document.getElementById('period-chip').textContent = periodText;
|
| 249 |
+
document.getElementById('period-chip').classList.toggle('hidden', !periodText);
|
| 250 |
+
document.getElementById('period-label-inline').textContent = periodText;
|
| 251 |
+
|
| 252 |
+
// Stats
|
| 253 |
+
const ss = data.summary_stats || {};
|
| 254 |
+
document.getElementById('stat-commits').textContent = ss.total_commits || '—';
|
| 255 |
+
document.getElementById('stat-contributors').textContent = ss.contributors || '—';
|
| 256 |
+
document.getElementById('stat-features').textContent = ss.features || '—';
|
| 257 |
+
document.getElementById('stat-fixes').textContent = ss.fixes || '—';
|
| 258 |
+
document.getElementById('stats-area').classList.remove('hidden');
|
| 259 |
+
|
| 260 |
+
// Commits
|
| 261 |
+
const cl = document.getElementById('commits-list');
|
| 262 |
+
while (cl.firstChild) cl.removeChild(cl.firstChild);
|
| 263 |
+
(data.commits || []).slice(0, 8).forEach(c => {
|
| 264 |
+
const row = document.createElement('div');
|
| 265 |
+
row.className = 'flex items-start gap-2 py-1.5 font-mono';
|
| 266 |
+
row.style.cssText = 'border-bottom:1px solid rgba(240,246,252,0.04);';
|
| 267 |
+
|
| 268 |
+
const hash = document.createElement('span');
|
| 269 |
+
hash.className = 'text-[10px] flex-shrink-0 mt-0.5';
|
| 270 |
+
hash.style.color = '#58A6FF';
|
| 271 |
+
hash.textContent = (c.hash || '???????').slice(0, 7);
|
| 272 |
+
|
| 273 |
+
const info = document.createElement('div');
|
| 274 |
+
info.className = 'min-w-0';
|
| 275 |
+
const msg = document.createElement('p');
|
| 276 |
+
msg.className = 'text-[11px] truncate';
|
| 277 |
+
msg.style.color = '#C9D1D9';
|
| 278 |
+
msg.textContent = c.message || '';
|
| 279 |
+
const meta = document.createElement('p');
|
| 280 |
+
meta.className = 'text-[9px] mt-0.5';
|
| 281 |
+
meta.style.color = '#484F58';
|
| 282 |
+
meta.textContent = [c.time, c.author ? '@'+c.author : ''].filter(Boolean).join(' · ');
|
| 283 |
+
info.append(msg, meta);
|
| 284 |
+
row.append(hash, info);
|
| 285 |
+
cl.appendChild(row);
|
| 286 |
+
});
|
| 287 |
+
document.getElementById('commits-area').classList.remove('hidden');
|
| 288 |
+
|
| 289 |
+
// ── Highlights ─────────────────────────────────────────────────────────────
|
| 290 |
+
const hs = document.getElementById('highlights-section');
|
| 291 |
+
while (hs.firstChild) hs.removeChild(hs.firstChild);
|
| 292 |
+
if ((data.highlights || []).length) {
|
| 293 |
+
hs.appendChild(makeSectionHeader("Week's Highlights", 'auto_awesome', '#58A6FF'));
|
| 294 |
+
const grid = document.createElement('div');
|
| 295 |
+
grid.className = 'flex flex-col gap-3 mt-4';
|
| 296 |
+
|
| 297 |
+
(data.highlights || []).forEach(h => {
|
| 298 |
+
const card = document.createElement('div');
|
| 299 |
+
card.className = 'card p-5';
|
| 300 |
+
card.style.borderLeftColor = '#58A6FF';
|
| 301 |
+
card.style.borderLeftWidth = '3px';
|
| 302 |
+
|
| 303 |
+
const title = document.createElement('h3');
|
| 304 |
+
title.className = 'text-sm font-semibold text-gd-text mb-2';
|
| 305 |
+
title.style.letterSpacing = '-0.01em';
|
| 306 |
+
title.textContent = h.title || '';
|
| 307 |
+
|
| 308 |
+
const narrative = document.createElement('p');
|
| 309 |
+
narrative.className = 'text-xs leading-relaxed text-gd-text-2';
|
| 310 |
+
narrative.textContent = h.narrative || '';
|
| 311 |
+
|
| 312 |
+
card.append(title, narrative);
|
| 313 |
+
|
| 314 |
+
if (h.impact) {
|
| 315 |
+
const impact = document.createElement('div');
|
| 316 |
+
impact.className = 'flex items-center gap-1.5 mt-3';
|
| 317 |
+
const icon = document.createElement('span');
|
| 318 |
+
icon.className = 'material-symbols-outlined text-[13px]';
|
| 319 |
+
icon.style.color = '#3FB950';
|
| 320 |
+
icon.textContent = 'trending_up';
|
| 321 |
+
const txt = document.createElement('span');
|
| 322 |
+
txt.className = 'text-[11px] font-medium';
|
| 323 |
+
txt.style.color = '#3FB950';
|
| 324 |
+
txt.textContent = h.impact;
|
| 325 |
+
impact.append(icon, txt);
|
| 326 |
+
card.appendChild(impact);
|
| 327 |
+
}
|
| 328 |
+
|
| 329 |
+
if (h.key_commit) {
|
| 330 |
+
const pre = document.createElement('pre');
|
| 331 |
+
pre.className = 'font-mono text-[10px] leading-relaxed whitespace-pre-wrap mt-3 p-3 rounded-lg';
|
| 332 |
+
pre.style.cssText = 'background:#0D1117; color:#484F58; border:1px solid rgba(240,246,252,0.06);';
|
| 333 |
+
pre.textContent = h.key_commit;
|
| 334 |
+
card.appendChild(pre);
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
grid.appendChild(card);
|
| 338 |
+
});
|
| 339 |
+
hs.appendChild(grid);
|
| 340 |
+
}
|
| 341 |
+
|
| 342 |
+
// ── Tech Debt ──────────────────────────────────────────────────────────────
|
| 343 |
+
const td = document.getElementById('tech-debt-section');
|
| 344 |
+
while (td.firstChild) td.removeChild(td.firstChild);
|
| 345 |
+
if ((data.tech_debt || []).length) {
|
| 346 |
+
td.appendChild(makeSectionHeader('Technical Debt Addressed', 'build', '#E3B341'));
|
| 347 |
+
const grid = document.createElement('div');
|
| 348 |
+
grid.className = 'grid grid-cols-2 gap-3 mt-4';
|
| 349 |
+
|
| 350 |
+
(data.tech_debt || []).forEach(item => {
|
| 351 |
+
const card = document.createElement('div');
|
| 352 |
+
card.className = 'card p-4';
|
| 353 |
+
|
| 354 |
+
const top = document.createElement('div');
|
| 355 |
+
top.className = 'flex items-center gap-2 mb-2';
|
| 356 |
+
const icon = document.createElement('span');
|
| 357 |
+
icon.className = 'material-symbols-outlined text-[15px]';
|
| 358 |
+
icon.style.color = '#E3B341';
|
| 359 |
+
icon.textContent = item.icon || 'build';
|
| 360 |
+
const h4 = document.createElement('p');
|
| 361 |
+
h4.className = 'text-xs font-semibold text-gd-text';
|
| 362 |
+
h4.textContent = item.title || '';
|
| 363 |
+
top.append(icon, h4);
|
| 364 |
+
|
| 365 |
+
const desc = document.createElement('p');
|
| 366 |
+
desc.className = 'text-xs leading-relaxed text-gd-text-2';
|
| 367 |
+
desc.textContent = item.description || '';
|
| 368 |
+
|
| 369 |
+
card.append(top, desc);
|
| 370 |
+
grid.appendChild(card);
|
| 371 |
+
});
|
| 372 |
+
td.appendChild(grid);
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
+
// ── Milestones ─────────────────────────────────────────────────────────────
|
| 376 |
+
const ms = document.getElementById('milestones-section');
|
| 377 |
+
while (ms.firstChild) ms.removeChild(ms.firstChild);
|
| 378 |
+
if ((data.milestones || []).length) {
|
| 379 |
+
ms.appendChild(makeSectionHeader('Feature Milestones', 'flag', '#58A6FF'));
|
| 380 |
+
|
| 381 |
+
const timeline = document.createElement('div');
|
| 382 |
+
timeline.className = 'relative pl-8 flex flex-col gap-6 mt-4';
|
| 383 |
+
const line = document.createElement('div');
|
| 384 |
+
line.className = 'absolute left-[9px] top-2 bottom-2 w-px';
|
| 385 |
+
line.style.background = 'rgba(240,246,252,0.08)';
|
| 386 |
+
timeline.appendChild(line);
|
| 387 |
+
|
| 388 |
+
(data.milestones || []).forEach(m => {
|
| 389 |
+
const row = document.createElement('div');
|
| 390 |
+
row.className = 'relative';
|
| 391 |
+
|
| 392 |
+
const dot = document.createElement('div');
|
| 393 |
+
dot.className = 'absolute -left-[28px] top-1.5 w-4 h-4 rounded-full flex items-center justify-center flex-shrink-0';
|
| 394 |
+
if (m.status === 'shipped') {
|
| 395 |
+
dot.style.cssText = 'background:#238636; border:1px solid rgba(63,185,80,0.4);';
|
| 396 |
+
const chk = document.createElement('span');
|
| 397 |
+
chk.className = 'material-symbols-outlined';
|
| 398 |
+
chk.style.cssText = 'font-size:10px; color:#fff; font-variation-settings:"FILL" 1,"wght" 600,"GRAD" 0,"opsz" 20;';
|
| 399 |
+
chk.textContent = 'check';
|
| 400 |
+
dot.appendChild(chk);
|
| 401 |
+
} else {
|
| 402 |
+
dot.style.cssText = 'background:#1C2128; border:2px solid #58A6FF;';
|
| 403 |
+
const pulse = document.createElement('div');
|
| 404 |
+
pulse.className = 'w-1.5 h-1.5 rounded-full';
|
| 405 |
+
pulse.style.cssText = 'background:#58A6FF; animation:pulse 1.4s ease-in-out infinite;';
|
| 406 |
+
dot.appendChild(pulse);
|
| 407 |
+
}
|
| 408 |
+
|
| 409 |
+
const card = document.createElement('div');
|
| 410 |
+
card.className = 'card p-5';
|
| 411 |
+
|
| 412 |
+
const badge = document.createElement('span');
|
| 413 |
+
badge.className = 'pill mb-3 inline-flex';
|
| 414 |
+
badge.style.cssText = m.status === 'shipped'
|
| 415 |
+
? 'background:rgba(63,185,80,0.10); color:#3FB950;'
|
| 416 |
+
: 'background:rgba(88,166,255,0.10); color:#58A6FF;';
|
| 417 |
+
badge.textContent = (m.status || 'planned').toUpperCase();
|
| 418 |
+
|
| 419 |
+
const h4 = document.createElement('h4');
|
| 420 |
+
h4.className = 'text-sm font-semibold text-gd-text mb-2';
|
| 421 |
+
h4.style.letterSpacing = '-0.01em';
|
| 422 |
+
h4.textContent = m.title || '';
|
| 423 |
+
|
| 424 |
+
const p = document.createElement('p');
|
| 425 |
+
p.className = 'text-xs leading-relaxed text-gd-text-2';
|
| 426 |
+
p.textContent = m.narrative || '';
|
| 427 |
+
|
| 428 |
+
card.append(badge, h4, p);
|
| 429 |
+
|
| 430 |
+
if (m.contributors && m.contributors.length) {
|
| 431 |
+
const contrib = document.createElement('div');
|
| 432 |
+
contrib.className = 'flex items-center gap-2 mt-3 pt-3';
|
| 433 |
+
contrib.style.cssText = 'border-top:1px solid rgba(240,246,252,0.07);';
|
| 434 |
+
const avatars = document.createElement('div');
|
| 435 |
+
avatars.className = 'flex -space-x-1';
|
| 436 |
+
m.contributors.slice(0, 4).forEach(name => {
|
| 437 |
+
const av = document.createElement('div');
|
| 438 |
+
av.className = 'w-5 h-5 rounded-full flex items-center justify-center text-[8px] font-bold font-mono';
|
| 439 |
+
av.style.cssText = 'background:#238636; color:#fff; border:2px solid #1C2128;';
|
| 440 |
+
av.textContent = (name[0] || '?').toUpperCase();
|
| 441 |
+
avatars.appendChild(av);
|
| 442 |
+
});
|
| 443 |
+
const names = document.createElement('span');
|
| 444 |
+
names.className = 'text-[10px] text-gd-text-3';
|
| 445 |
+
names.textContent = m.contributors.join(', ');
|
| 446 |
+
contrib.append(avatars, names);
|
| 447 |
+
card.appendChild(contrib);
|
| 448 |
+
}
|
| 449 |
+
|
| 450 |
+
row.append(dot, card);
|
| 451 |
+
timeline.appendChild(row);
|
| 452 |
+
});
|
| 453 |
+
ms.appendChild(timeline);
|
| 454 |
+
}
|
| 455 |
+
|
| 456 |
+
_narrativeText = buildCopyText(data);
|
| 457 |
+
const copyBtn = document.getElementById('copy-btn');
|
| 458 |
+
copyBtn.classList.remove('hidden');
|
| 459 |
+
copyBtn.onclick = () => {
|
| 460 |
+
navigator.clipboard.writeText(_narrativeText).then(() => {
|
| 461 |
+
const icon = document.getElementById('copy-icon');
|
| 462 |
+
icon.textContent = 'check';
|
| 463 |
+
setTimeout(() => { icon.textContent = 'content_copy'; }, 2000);
|
| 464 |
+
});
|
| 465 |
+
};
|
| 466 |
+
|
| 467 |
+
document.getElementById('narrative-output').classList.remove('hidden');
|
| 468 |
+
document.getElementById('narrative-output').scrollIntoView({ behavior:'smooth' });
|
| 469 |
+
}
|
| 470 |
+
|
| 471 |
+
function makeSectionHeader(title, iconName, color) {
|
| 472 |
+
const wrap = document.createElement('div');
|
| 473 |
+
wrap.className = 'section-header';
|
| 474 |
+
const icon = document.createElement('span');
|
| 475 |
+
icon.className = 'material-symbols-outlined text-[15px]';
|
| 476 |
+
icon.style.color = color;
|
| 477 |
+
icon.textContent = iconName;
|
| 478 |
+
const h = document.createElement('h2');
|
| 479 |
+
h.className = 'text-sm font-semibold text-gd-text';
|
| 480 |
+
h.style.letterSpacing = '-0.01em';
|
| 481 |
+
h.textContent = title;
|
| 482 |
+
const line = document.createElement('div');
|
| 483 |
+
line.className = 'section-header-line';
|
| 484 |
+
wrap.append(icon, h, line);
|
| 485 |
+
return wrap;
|
| 486 |
+
}
|
| 487 |
+
|
| 488 |
+
function buildCopyText(data) {
|
| 489 |
+
const lines = ['# ' + (data.period_label || 'Engineering Narrative') + '\n'];
|
| 490 |
+
(data.highlights || []).forEach(h => {
|
| 491 |
+
lines.push('## ' + h.title + '\n' + h.narrative + '\n');
|
| 492 |
+
if (h.impact) lines.push('Impact: ' + h.impact + '\n');
|
| 493 |
+
});
|
| 494 |
+
if ((data.tech_debt || []).length) {
|
| 495 |
+
lines.push('## Technical Debt\n');
|
| 496 |
+
data.tech_debt.forEach(t => lines.push('### ' + t.title + '\n' + t.description + '\n'));
|
| 497 |
+
}
|
| 498 |
+
if ((data.milestones || []).length) {
|
| 499 |
+
lines.push('## Milestones\n');
|
| 500 |
+
data.milestones.forEach(m => lines.push('[' + (m.status||'').toUpperCase() + '] ' + m.title + '\n' + m.narrative + '\n'));
|
| 501 |
+
}
|
| 502 |
+
return lines.join('\n');
|
| 503 |
+
}
|
| 504 |
+
|
| 505 |
+
document.addEventListener('keydown', e => {
|
| 506 |
+
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') doNarrate();
|
| 507 |
+
});
|
| 508 |
+
</script>
|
| 509 |
+
{% endblock %}
|
app/tools/schema_detective/__init__.py
ADDED
|
File without changes
|
app/tools/schema_detective/__pycache__/__init__.cpython-314.pyc
ADDED
|
Binary file (180 Bytes). View file
|
|
|
app/tools/schema_detective/__pycache__/detective.cpython-314.pyc
ADDED
|
Binary file (12.8 kB). View file
|
|
|
app/tools/schema_detective/__pycache__/routes.cpython-314.pyc
ADDED
|
Binary file (2.69 kB). View file
|
|
|
app/tools/schema_detective/detective.py
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Schema Detective — audits database schemas for design issues."""
|
| 2 |
+
import re
|
| 3 |
+
from app.core.ai import call_ai_json, call_ai
|
| 4 |
+
|
| 5 |
+
_SYSTEM = """You are a senior database architect and security engineer with deep expertise in SQL,
|
| 6 |
+
normalization, indexing strategies, database security, and production reliability.
|
| 7 |
+
You audit schemas with a critical eye — catching design flaws, security vulnerabilities,
|
| 8 |
+
and data integrity risks that cause real production incidents.
|
| 9 |
+
Return ONLY valid JSON — no markdown fences, no preamble."""
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
_PROMPT_TMPL = """Audit the following database schema (and any embedded SQL) for ALL design issues, security vulnerabilities, and integrity risks.
|
| 13 |
+
{pre_context}
|
| 14 |
+
SCHEMA / SQL:
|
| 15 |
+
---
|
| 16 |
+
{schema}
|
| 17 |
+
---
|
| 18 |
+
|
| 19 |
+
Return a JSON object with EXACTLY these keys:
|
| 20 |
+
{{
|
| 21 |
+
"health_score": <integer 0-100 — overall schema health; 100 = perfect, 0 = completely broken>,
|
| 22 |
+
"summary": "<1-2 sentence plain-language verdict — what's the biggest risk and overall state>",
|
| 23 |
+
"findings": [
|
| 24 |
+
{{
|
| 25 |
+
"severity": "<exactly one of: critical | warning | advice>",
|
| 26 |
+
"title": "<short issue title, max 6 words>",
|
| 27 |
+
"description": "<1-2 sentence explanation of the problem and why it matters in production>",
|
| 28 |
+
"location": "<exact table name, column name, procedure name, or SQL clause where the issue appears>",
|
| 29 |
+
"suggestion": "<concrete fix — include the actual corrected SQL snippet when applicable>"
|
| 30 |
+
}}
|
| 31 |
+
]
|
| 32 |
+
}}
|
| 33 |
+
|
| 34 |
+
Severity definitions:
|
| 35 |
+
- critical: causes data loss, security breach, or catastrophic data corruption. Examples:
|
| 36 |
+
* SQL injection via dynamic query string concatenation with user input
|
| 37 |
+
* DELETE or UPDATE statements with no WHERE clause (wipes entire table)
|
| 38 |
+
* Passwords or secrets stored in plaintext (no hashing)
|
| 39 |
+
* Missing PRIMARY KEY on a business entity table
|
| 40 |
+
* Circular or missing FOREIGN KEY constraints breaking referential integrity
|
| 41 |
+
* NULL allowed on columns that are business-critical (email, user_id, amount)
|
| 42 |
+
- warning: degrades performance, risks integrity under load, or creates operational risk. Examples:
|
| 43 |
+
* Unindexed foreign key columns (causes full table scans on joins)
|
| 44 |
+
* TEXT type where VARCHAR(n) is appropriate (no length constraint)
|
| 45 |
+
* Missing NOT NULL on required business fields
|
| 46 |
+
* Missing updated_at / created_at audit columns
|
| 47 |
+
* Redundant or overlapping indexes (e.g. index on (a), (a,b), (a,b,c) — first is redundant)
|
| 48 |
+
* Missing transactions around multi-step operations
|
| 49 |
+
- advice: maintainability and future-proofing. Examples:
|
| 50 |
+
* Vague column or table names (t1, col1, data, misc, info, temp — not what they represent)
|
| 51 |
+
* Single-letter or numeric table aliases in stored procedures
|
| 52 |
+
* Missing soft-delete pattern (no is_deleted / deleted_at column)
|
| 53 |
+
* No CHECK constraints on enum-like columns
|
| 54 |
+
|
| 55 |
+
FK direction rule: the child table (many side) holds the FK column that references the parent (one side).
|
| 56 |
+
Correct: products.category_id REFERENCES categories(id) — products is the child, categories is the parent.
|
| 57 |
+
Wrong: reversed direction or FK defined on the parent side.
|
| 58 |
+
|
| 59 |
+
Naming convention rule: the real problem is meaningless names (t1, t2, col1, data, misc).
|
| 60 |
+
NEVER suggest appending _table, _tbl, _record — that is an anti-pattern. Suggest descriptive real names instead.
|
| 61 |
+
|
| 62 |
+
Rules:
|
| 63 |
+
- Prioritize: list critical findings first, then warnings, then advice
|
| 64 |
+
- The CONFIRMED CRITICAL ISSUES above MUST appear in findings as severity=critical — expand on each with real-world impact
|
| 65 |
+
- Be specific: reference exact table/column names from the schema — do not generalise
|
| 66 |
+
- suggestion must be actionable: if it is a SQL change, show the corrected SQL
|
| 67 |
+
- health_score: start at 100, deduct 20-25 per critical, 5-10 per warning, 1-3 per advice; floor at 0
|
| 68 |
+
- Minimum 3 findings if any issues exist; maximum 15
|
| 69 |
+
- Do NOT fabricate issues that are not present in the schema"""
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def _pre_scan(sql: str) -> list[dict]:
|
| 73 |
+
"""Deterministic regex scan for the most dangerous SQL patterns.
|
| 74 |
+
Returns findings that are guaranteed regardless of what the AI notices."""
|
| 75 |
+
findings = []
|
| 76 |
+
|
| 77 |
+
# 1. SQL Injection — dynamic exec of a variable built by string concatenation
|
| 78 |
+
# Covers: EXEC(@sql), EXECUTE(@sql), sp_executesql @sql
|
| 79 |
+
exec_pattern = re.compile(
|
| 80 |
+
r'(?i)(?:EXEC(?:UTE)?\s*\(\s*@\w+|sp_executesql\s+@\w+)', re.IGNORECASE
|
| 81 |
+
)
|
| 82 |
+
# Also catch: SET @var = '...' + @user_input (dynamic SQL building)
|
| 83 |
+
concat_pattern = re.compile(
|
| 84 |
+
r"(?i)SET\s+@\w+\s*=\s*(?:'[^']*'\s*\+|@\w+\s*\+)", re.IGNORECASE
|
| 85 |
+
)
|
| 86 |
+
lines = sql.splitlines()
|
| 87 |
+
injection_lines = []
|
| 88 |
+
for i, line in enumerate(lines, 1):
|
| 89 |
+
if exec_pattern.search(line) or concat_pattern.search(line):
|
| 90 |
+
injection_lines.append(f"line {i}: {line.strip()[:80]}")
|
| 91 |
+
if injection_lines:
|
| 92 |
+
findings.append({
|
| 93 |
+
"severity": "critical",
|
| 94 |
+
"title": "SQL Injection Vulnerability",
|
| 95 |
+
"description": (
|
| 96 |
+
"Dynamic SQL is constructed by concatenating user-supplied variables into a "
|
| 97 |
+
"query string and then executed with EXEC/EXECUTE. An attacker can inject "
|
| 98 |
+
"arbitrary SQL, bypassing all access controls and exfiltrating or destroying data."
|
| 99 |
+
),
|
| 100 |
+
"location": "; ".join(injection_lines[:3]),
|
| 101 |
+
"suggestion": (
|
| 102 |
+
"Replace string concatenation with parameterized queries. "
|
| 103 |
+
"Use sp_executesql with typed parameters:\n"
|
| 104 |
+
" EXEC sp_executesql N'SELECT * FROM users WHERE id = @id', "
|
| 105 |
+
"N'@id INT', @id = @user_input;"
|
| 106 |
+
),
|
| 107 |
+
})
|
| 108 |
+
|
| 109 |
+
# 2. DELETE without WHERE — bare DELETE FROM <table> ;
|
| 110 |
+
for m in re.finditer(r'(?im)^\s*DELETE\s+FROM\s+(\w+)\s*;', sql):
|
| 111 |
+
table = m.group(1)
|
| 112 |
+
findings.append({
|
| 113 |
+
"severity": "critical",
|
| 114 |
+
"title": "DELETE Without WHERE Clause",
|
| 115 |
+
"description": (
|
| 116 |
+
f"DELETE FROM {table} has no WHERE clause — this permanently deletes "
|
| 117 |
+
f"every row in {table} on execution. One accidental or malicious call "
|
| 118 |
+
"destroys the entire table's data with no automatic rollback."
|
| 119 |
+
),
|
| 120 |
+
"location": f"DELETE FROM {table}",
|
| 121 |
+
"suggestion": (
|
| 122 |
+
f"Always scope deletes: DELETE FROM {table} WHERE <condition>;\n"
|
| 123 |
+
f"If a full wipe is intentional, use TRUNCATE TABLE {table} explicitly "
|
| 124 |
+
"to make the intent obvious."
|
| 125 |
+
),
|
| 126 |
+
})
|
| 127 |
+
|
| 128 |
+
# 3. UPDATE without WHERE — UPDATE <table> SET ... ; (no WHERE before semicolon)
|
| 129 |
+
for m in re.finditer(
|
| 130 |
+
r'(?is)UPDATE\s+(\w+)\s+SET\s+(?:(?!WHERE).)*?;', sql
|
| 131 |
+
):
|
| 132 |
+
table = m.group(1)
|
| 133 |
+
stmt_preview = m.group(0).strip()[:80]
|
| 134 |
+
findings.append({
|
| 135 |
+
"severity": "critical",
|
| 136 |
+
"title": "UPDATE Without WHERE Clause",
|
| 137 |
+
"description": (
|
| 138 |
+
f"UPDATE {table} SET ... has no WHERE clause — this overwrites every "
|
| 139 |
+
f"row in {table}. Any typo or accidental execution corrupts the entire table."
|
| 140 |
+
),
|
| 141 |
+
"location": stmt_preview,
|
| 142 |
+
"suggestion": (
|
| 143 |
+
f"Add a WHERE clause: UPDATE {table} SET ... WHERE <condition>;"
|
| 144 |
+
),
|
| 145 |
+
})
|
| 146 |
+
|
| 147 |
+
# 4. Plaintext passwords in INSERT statements
|
| 148 |
+
# Look for INSERT ... (... password ...) VALUES (... 'literal' ...)
|
| 149 |
+
for m in re.finditer(
|
| 150 |
+
r"(?is)INSERT\s+INTO\s+\w+[^;]*?(?:password|passwd|pwd)[^;]*?VALUES[^;]*?'([^']{1,100})'[^;]*?;",
|
| 151 |
+
sql,
|
| 152 |
+
):
|
| 153 |
+
findings.append({
|
| 154 |
+
"severity": "critical",
|
| 155 |
+
"title": "Plaintext Password Stored",
|
| 156 |
+
"description": (
|
| 157 |
+
"Passwords are inserted as plaintext string literals. Any database breach "
|
| 158 |
+
"or log exposure immediately reveals all user credentials with zero effort."
|
| 159 |
+
),
|
| 160 |
+
"location": m.group(0).strip()[:80],
|
| 161 |
+
"suggestion": (
|
| 162 |
+
"Hash passwords before storage using bcrypt, argon2id, or scrypt. "
|
| 163 |
+
"Never store or log plaintext passwords:\n"
|
| 164 |
+
" password_hash = bcrypt.hashpw(password.encode(), bcrypt.gensalt())\n"
|
| 165 |
+
" INSERT INTO users (password) VALUES (:password_hash)"
|
| 166 |
+
),
|
| 167 |
+
})
|
| 168 |
+
|
| 169 |
+
return findings
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
def analyze_schema(schema: str) -> dict:
|
| 173 |
+
"""Audit a database schema and return structured findings."""
|
| 174 |
+
pre_findings = _pre_scan(schema)
|
| 175 |
+
|
| 176 |
+
# Inject pre-detected issues into the prompt so the AI elaborates on them
|
| 177 |
+
if pre_findings:
|
| 178 |
+
titles = "\n".join(f" - {f['title']} (at: {f['location'][:60]})" for f in pre_findings)
|
| 179 |
+
pre_context = (
|
| 180 |
+
f"\n\nCONFIRMED CRITICAL ISSUES (detected by static analysis — "
|
| 181 |
+
f"you MUST include ALL of these in findings as severity=critical "
|
| 182 |
+
f"and explain their full real-world impact):\n{titles}\n"
|
| 183 |
+
)
|
| 184 |
+
else:
|
| 185 |
+
pre_context = ""
|
| 186 |
+
|
| 187 |
+
prompt = _PROMPT_TMPL.format(schema=schema[:8000], pre_context=pre_context)
|
| 188 |
+
result = call_ai_json(
|
| 189 |
+
[{"role": "user", "content": prompt}],
|
| 190 |
+
system=_SYSTEM,
|
| 191 |
+
max_tokens=3072,
|
| 192 |
+
)
|
| 193 |
+
|
| 194 |
+
if not result or not isinstance(result, dict):
|
| 195 |
+
# AI failed — return pre-findings only
|
| 196 |
+
return {
|
| 197 |
+
"health_score": max(0, 100 - 22 * len(pre_findings)),
|
| 198 |
+
"summary": "Static analysis detected critical security issues. AI elaboration unavailable.",
|
| 199 |
+
"findings": pre_findings,
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
if pre_findings:
|
| 203 |
+
# Deduplicate: remove any AI findings whose title overlaps with pre-detected ones
|
| 204 |
+
pre_titles = {f["title"].lower() for f in pre_findings}
|
| 205 |
+
ai_only = [
|
| 206 |
+
f for f in result.get("findings", [])
|
| 207 |
+
if f.get("title", "").lower() not in pre_titles
|
| 208 |
+
]
|
| 209 |
+
# Pre-findings lead the list (guaranteed, most critical)
|
| 210 |
+
result["findings"] = pre_findings + ai_only
|
| 211 |
+
# Re-anchor health_score downward for guaranteed criticals
|
| 212 |
+
result["health_score"] = max(0, result.get("health_score", 100) - 22 * len(pre_findings))
|
| 213 |
+
|
| 214 |
+
return result
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
_FIX_SYSTEM = (
|
| 218 |
+
"You are a senior database architect. Given a SQL schema with known issues, "
|
| 219 |
+
"produce a complete corrected version that fixes all listed problems. "
|
| 220 |
+
"Return ONLY valid SQL — no markdown fences, no preamble, no trailing commentary."
|
| 221 |
+
)
|
| 222 |
+
|
| 223 |
+
_FIX_PROMPT = """Fix the following SQL schema by addressing every listed issue.
|
| 224 |
+
|
| 225 |
+
ORIGINAL SCHEMA:
|
| 226 |
+
---
|
| 227 |
+
{schema}
|
| 228 |
+
---
|
| 229 |
+
|
| 230 |
+
ISSUES TO FIX:
|
| 231 |
+
{issues}
|
| 232 |
+
|
| 233 |
+
Rules:
|
| 234 |
+
- Return the COMPLETE corrected schema — all original tables must be present
|
| 235 |
+
- Fix every issue listed: add missing PKs, hash passwords, add WHERE clauses, remove SQL injection, add indexes, etc.
|
| 236 |
+
- Add brief inline comments only where a fix is non-obvious (e.g. -- hashed via bcrypt)
|
| 237 |
+
- Do NOT add features that weren't in the original schema
|
| 238 |
+
- Output valid SQL only"""
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
def fix_schema(schema: str, findings: list) -> str:
|
| 242 |
+
"""Return a corrected SQL schema that addresses all audit findings."""
|
| 243 |
+
issues = "\n".join(
|
| 244 |
+
f"- [{f.get('severity','').upper()}] {f.get('title','')}: {f.get('suggestion','')[:300]}"
|
| 245 |
+
for f in findings
|
| 246 |
+
if f.get("suggestion")
|
| 247 |
+
)
|
| 248 |
+
if not issues:
|
| 249 |
+
issues = "General cleanup — apply best practices for normalization, naming, and security."
|
| 250 |
+
|
| 251 |
+
prompt = _FIX_PROMPT.format(schema=schema[:6000], issues=issues)
|
| 252 |
+
return call_ai([{"role": "user", "content": prompt}], system=_FIX_SYSTEM, max_tokens=3000).strip()
|
app/tools/schema_detective/routes.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Schema Detective routes."""
|
| 2 |
+
from flask import Blueprint, render_template, request, jsonify
|
| 3 |
+
from .detective import analyze_schema, fix_schema
|
| 4 |
+
|
| 5 |
+
bp = Blueprint("schema_detective", __name__, template_folder="templates")
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
@bp.route("/")
|
| 9 |
+
def index():
|
| 10 |
+
return render_template("schema_detective/index.html")
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
@bp.route("/api/analyze", methods=["POST"])
|
| 14 |
+
def api_analyze():
|
| 15 |
+
body = request.get_json(silent=True) or {}
|
| 16 |
+
schema = (body.get("schema") or "").strip()
|
| 17 |
+
|
| 18 |
+
if not schema:
|
| 19 |
+
return jsonify({"error": "Schema is required — paste your SQL or table definitions"}), 400
|
| 20 |
+
if len(schema) < 30:
|
| 21 |
+
return jsonify({"error": "Schema too short — paste at least one CREATE TABLE statement"}), 400
|
| 22 |
+
|
| 23 |
+
result = analyze_schema(schema)
|
| 24 |
+
if not result:
|
| 25 |
+
return jsonify({"error": "AI failed to analyze schema — please try again"}), 502
|
| 26 |
+
return jsonify(result)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
@bp.route("/api/fix", methods=["POST"])
|
| 30 |
+
def api_fix():
|
| 31 |
+
body = request.get_json(silent=True) or {}
|
| 32 |
+
schema = (body.get("schema") or "").strip()
|
| 33 |
+
findings = body.get("findings") or []
|
| 34 |
+
if not schema:
|
| 35 |
+
return jsonify({"error": "Schema is required"}), 400
|
| 36 |
+
try:
|
| 37 |
+
fixed = fix_schema(schema, findings)
|
| 38 |
+
return jsonify({"fixed_schema": fixed})
|
| 39 |
+
except Exception as e:
|
| 40 |
+
return jsonify({"error": f"Fix generation failed: {str(e)}"}), 502
|