ai-project-builder 2.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ai_project_builder-2.0.0/PKG-INFO +16 -0
- ai_project_builder-2.0.0/README.md +8 -0
- ai_project_builder-2.0.0/pyproject.toml +19 -0
- ai_project_builder-2.0.0/setup.cfg +4 -0
- ai_project_builder-2.0.0/src/ai_project_builder/__init__.py +1 -0
- ai_project_builder-2.0.0/src/ai_project_builder/__main__.py +8 -0
- ai_project_builder-2.0.0/src/ai_project_builder/core/__init__.py +5 -0
- ai_project_builder-2.0.0/src/ai_project_builder/core/decoder.py +110 -0
- ai_project_builder-2.0.0/src/ai_project_builder/core/encoder.py +80 -0
- ai_project_builder-2.0.0/src/ai_project_builder/core/escapes.py +69 -0
- ai_project_builder-2.0.0/src/ai_project_builder/core/project_io.py +144 -0
- ai_project_builder-2.0.0/src/ai_project_builder/core/report.py +172 -0
- ai_project_builder-2.0.0/src/ai_project_builder/gui/__init__.py +0 -0
- ai_project_builder-2.0.0/src/ai_project_builder/gui/app.py +520 -0
- ai_project_builder-2.0.0/src/ai_project_builder/prompt/base_prompt.txt +12 -0
- ai_project_builder-2.0.0/src/ai_project_builder.egg-info/PKG-INFO +16 -0
- ai_project_builder-2.0.0/src/ai_project_builder.egg-info/SOURCES.txt +19 -0
- ai_project_builder-2.0.0/src/ai_project_builder.egg-info/dependency_links.txt +1 -0
- ai_project_builder-2.0.0/src/ai_project_builder.egg-info/entry_points.txt +2 -0
- ai_project_builder-2.0.0/src/ai_project_builder.egg-info/requires.txt +1 -0
- ai_project_builder-2.0.0/src/ai_project_builder.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ai-project-builder
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: Fast Python project creation with AI
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: fpdf2>=2.7.0
|
|
8
|
+
|
|
9
|
+
# AI Project Builder
|
|
10
|
+
|
|
11
|
+
Create Python projects instantly using AI.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pipx install . --system-site-packages
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ai-project-builder"
|
|
7
|
+
version = "2.0.0"
|
|
8
|
+
description = "Fast Python project creation with AI"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"fpdf2>=2.7.0",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[project.scripts]
|
|
16
|
+
apb = "ai_project_builder.__main__:main"
|
|
17
|
+
|
|
18
|
+
[tool.setuptools.packages.find]
|
|
19
|
+
where = ["src"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.0.0"
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
from .escapes import escape_content, unescape_content
|
|
2
|
+
from .encoder import encode_project, encode_project_from_directory
|
|
3
|
+
from .decoder import decode_project, DecodeError
|
|
4
|
+
from .project_io import deploy_project, read_project, ProjectIOResult
|
|
5
|
+
from .report import generate_report
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Декодирование APB-формата в структуру проекта.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from .escapes import unescape_content
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DecodeError(Exception):
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def decode_project(text: str) -> dict:
|
|
14
|
+
"""
|
|
15
|
+
Декодирует APB-формат в словарь проекта.
|
|
16
|
+
|
|
17
|
+
Формат:
|
|
18
|
+
===APB_START===
|
|
19
|
+
PROJECT: name
|
|
20
|
+
MODE: create
|
|
21
|
+
|
|
22
|
+
---FILE: path/to/file.py---
|
|
23
|
+
content here
|
|
24
|
+
---END_FILE---
|
|
25
|
+
|
|
26
|
+
===APB_END===
|
|
27
|
+
"""
|
|
28
|
+
text = text.strip()
|
|
29
|
+
|
|
30
|
+
# Ищем границы
|
|
31
|
+
start_match = re.search(r'={3,}APB_START={3,}', text)
|
|
32
|
+
end_match = re.search(r'={3,}APB_END={3,}', text)
|
|
33
|
+
|
|
34
|
+
if not start_match:
|
|
35
|
+
raise DecodeError(
|
|
36
|
+
"Не найден маркер начала ===APB_START===\n"
|
|
37
|
+
"Убедитесь, что ИИ выдала правильный формат."
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
if not end_match:
|
|
41
|
+
raise DecodeError(
|
|
42
|
+
"Не найден маркер конца ===APB_END===\n"
|
|
43
|
+
"Возможно, ответ ИИ был обрезан."
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Извлекаем содержимое между маркерами
|
|
47
|
+
content = text[start_match.end():end_match.start()].strip()
|
|
48
|
+
|
|
49
|
+
# Парсим заголовок
|
|
50
|
+
project_match = re.search(r'PROJECT:\s*(.+)', content)
|
|
51
|
+
mode_match = re.search(r'MODE:\s*(\w+)', content)
|
|
52
|
+
|
|
53
|
+
if not project_match:
|
|
54
|
+
raise DecodeError("Не найдено поле PROJECT:")
|
|
55
|
+
|
|
56
|
+
project_name = project_match.group(1).strip()
|
|
57
|
+
mode = mode_match.group(1).strip().lower() if mode_match else "create"
|
|
58
|
+
|
|
59
|
+
# Парсим файлы
|
|
60
|
+
files = []
|
|
61
|
+
|
|
62
|
+
# Паттерн для поиска файлов: ---FILE: path--- ... ---END_FILE---
|
|
63
|
+
file_pattern = re.compile(
|
|
64
|
+
r'-{3,}FILE:\s*(.+?)\s*-{3,}\n(.*?)\n-{3,}END_FILE-{3,}',
|
|
65
|
+
re.DOTALL
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
for match in file_pattern.finditer(content):
|
|
69
|
+
file_path = match.group(1).strip()
|
|
70
|
+
file_content = match.group(2)
|
|
71
|
+
|
|
72
|
+
# Убираем один leading/trailing newline если есть
|
|
73
|
+
if file_content.startswith('\n'):
|
|
74
|
+
file_content = file_content[1:]
|
|
75
|
+
if file_content.endswith('\n'):
|
|
76
|
+
file_content = file_content[:-1]
|
|
77
|
+
|
|
78
|
+
# Применяем unescape
|
|
79
|
+
file_content = unescape_content(file_content)
|
|
80
|
+
file_path = unescape_content(file_path)
|
|
81
|
+
|
|
82
|
+
files.append({
|
|
83
|
+
"path": file_path,
|
|
84
|
+
"action": "write",
|
|
85
|
+
"content": file_content
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
# Ищем файлы для удаления
|
|
89
|
+
delete_pattern = re.compile(r'-{3,}DELETE:\s*(.+?)\s*-{3,}')
|
|
90
|
+
for match in delete_pattern.finditer(content):
|
|
91
|
+
file_path = unescape_content(match.group(1).strip())
|
|
92
|
+
files.append({
|
|
93
|
+
"path": file_path,
|
|
94
|
+
"action": "delete"
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
if not files:
|
|
98
|
+
raise DecodeError(
|
|
99
|
+
"Не найдено ни одного файла.\n"
|
|
100
|
+
"Файлы должны быть в формате:\n"
|
|
101
|
+
"---FILE: path/to/file.py---\n"
|
|
102
|
+
"content\n"
|
|
103
|
+
"---END_FILE---"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
"project": project_name,
|
|
108
|
+
"mode": mode,
|
|
109
|
+
"files": files
|
|
110
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Кодирование проекта в APB-формат.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from .escapes import escape_content
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def encode_project(project_name: str, files: list, mode: str = "create") -> str:
|
|
10
|
+
"""
|
|
11
|
+
Кодирует проект в APB-формат.
|
|
12
|
+
"""
|
|
13
|
+
lines = [
|
|
14
|
+
"===APB_START===",
|
|
15
|
+
f"PROJECT: {project_name}",
|
|
16
|
+
f"MODE: {mode}",
|
|
17
|
+
""
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
for f in files:
|
|
21
|
+
path = f["path"]
|
|
22
|
+
action = f.get("action", "write")
|
|
23
|
+
|
|
24
|
+
if action == "delete":
|
|
25
|
+
lines.append(f"---DELETE: {path}---")
|
|
26
|
+
lines.append("")
|
|
27
|
+
else:
|
|
28
|
+
content = f.get("content", "")
|
|
29
|
+
escaped_content = escape_content(content)
|
|
30
|
+
|
|
31
|
+
lines.append(f"---FILE: {path}---")
|
|
32
|
+
lines.append(escaped_content)
|
|
33
|
+
lines.append("---END_FILE---")
|
|
34
|
+
lines.append("")
|
|
35
|
+
|
|
36
|
+
lines.append("===APB_END===")
|
|
37
|
+
|
|
38
|
+
return "\n".join(lines)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def encode_project_from_directory(project_dir: str, mode: str = "update") -> str:
|
|
42
|
+
"""
|
|
43
|
+
Читает проект с диска и кодирует в APB-формат.
|
|
44
|
+
"""
|
|
45
|
+
project_name = os.path.basename(os.path.normpath(project_dir))
|
|
46
|
+
files = []
|
|
47
|
+
|
|
48
|
+
skip_dirs = {".git", "__pycache__", "node_modules", ".venv", "venv", ".idea", ".vscode", "dist", "build", "*.egg-info"}
|
|
49
|
+
skip_extensions = {".pyc", ".pyo", ".so", ".dll", ".exe", ".bin"}
|
|
50
|
+
|
|
51
|
+
for root, dirs, filenames in os.walk(project_dir):
|
|
52
|
+
# Фильтруем директории
|
|
53
|
+
dirs[:] = [d for d in dirs if d not in skip_dirs and not d.endswith(".egg-info")]
|
|
54
|
+
|
|
55
|
+
for filename in sorted(filenames):
|
|
56
|
+
# Пропускаем скрытые и бинарные файлы
|
|
57
|
+
if filename.startswith("."):
|
|
58
|
+
continue
|
|
59
|
+
|
|
60
|
+
ext = os.path.splitext(filename)[1].lower()
|
|
61
|
+
if ext in skip_extensions:
|
|
62
|
+
continue
|
|
63
|
+
|
|
64
|
+
filepath = os.path.join(root, filename)
|
|
65
|
+
relpath = os.path.relpath(filepath, project_dir).replace(os.sep, "/")
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
with open(filepath, "r", encoding="utf-8") as f:
|
|
69
|
+
content = f.read()
|
|
70
|
+
|
|
71
|
+
files.append({
|
|
72
|
+
"path": relpath,
|
|
73
|
+
"action": "write",
|
|
74
|
+
"content": content
|
|
75
|
+
})
|
|
76
|
+
except (UnicodeDecodeError, PermissionError, OSError):
|
|
77
|
+
# Пропускаем бинарные/недоступные файлы
|
|
78
|
+
continue
|
|
79
|
+
|
|
80
|
+
return encode_project(project_name, files, mode)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Escape-последовательности для безопасной передачи кода через ИИ.
|
|
3
|
+
Заменяем символы, которые Markdown может испортить.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
# Порядок важен! Сначала длинные последовательности, потом короткие.
|
|
7
|
+
ESCAPE_MAP = [
|
|
8
|
+
# Тройные последовательности (сначала!)
|
|
9
|
+
("```", "{{TICK3}}"),
|
|
10
|
+
("---", "{{DASH3}}"),
|
|
11
|
+
("===", "{{EQUAL3}}"),
|
|
12
|
+
("***", "{{STAR3}}"),
|
|
13
|
+
("___", "{{UNDER3}}"),
|
|
14
|
+
|
|
15
|
+
# Двойные
|
|
16
|
+
("**", "{{STAR2}}"),
|
|
17
|
+
("__", "{{UNDER2}}"),
|
|
18
|
+
("~~", "{{TILDE2}}"),
|
|
19
|
+
|
|
20
|
+
# Одиночные (в конце)
|
|
21
|
+
("`", "{{TICK}}"),
|
|
22
|
+
("#", "{{HASH}}"),
|
|
23
|
+
("*", "{{STAR}}"),
|
|
24
|
+
("_", "{{UNDER}}"),
|
|
25
|
+
(">", "{{GT}}"),
|
|
26
|
+
("<", "{{LT}}"),
|
|
27
|
+
("[", "{{LBRACKET}}"),
|
|
28
|
+
("]", "{{RBRACKET}}"),
|
|
29
|
+
("(", "{{LPAREN}}"),
|
|
30
|
+
(")", "{{RPAREN}}"),
|
|
31
|
+
("|", "{{PIPE}}"),
|
|
32
|
+
("\\", "{{BACKSLASH}}"),
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
# Обратная карта для декодирования
|
|
36
|
+
UNESCAPE_MAP = [(escaped, original) for original, escaped in ESCAPE_MAP]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def escape_content(text: str) -> str:
|
|
40
|
+
"""
|
|
41
|
+
Заменяет опасные символы на escape-последовательности.
|
|
42
|
+
Используется при кодировании проекта для отправки в ИИ.
|
|
43
|
+
"""
|
|
44
|
+
result = text
|
|
45
|
+
for original, escaped in ESCAPE_MAP:
|
|
46
|
+
result = result.replace(original, escaped)
|
|
47
|
+
return result
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def unescape_content(text: str) -> str:
|
|
51
|
+
"""
|
|
52
|
+
Заменяет escape-последов��тельности обратно на символы.
|
|
53
|
+
Используется при декодировании ответа от ИИ.
|
|
54
|
+
"""
|
|
55
|
+
result = text
|
|
56
|
+
for escaped, original in UNESCAPE_MAP:
|
|
57
|
+
result = result.replace(escaped, original)
|
|
58
|
+
return result
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def get_escape_reference() -> str:
|
|
62
|
+
"""
|
|
63
|
+
Возвращает справку по escape-последовательностям для промпта.
|
|
64
|
+
"""
|
|
65
|
+
lines = ["ESCAPE SEQUENCES (use these instead of actual characters):"]
|
|
66
|
+
for original, escaped in ESCAPE_MAP:
|
|
67
|
+
display_original = repr(original)[1:-1] # Убираем кавычки
|
|
68
|
+
lines.append(f" {escaped} = {display_original}")
|
|
69
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Создание и обновление проектов на диске.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ProjectIOResult:
|
|
9
|
+
def __init__(self):
|
|
10
|
+
self.created_files: list[str] = []
|
|
11
|
+
self.updated_files: list[str] = []
|
|
12
|
+
self.deleted_files: list[str] = []
|
|
13
|
+
self.created_dirs: list[str] = []
|
|
14
|
+
self.errors: list[str] = []
|
|
15
|
+
|
|
16
|
+
def summary(self) -> str:
|
|
17
|
+
lines = []
|
|
18
|
+
|
|
19
|
+
if self.created_dirs:
|
|
20
|
+
lines.append(f"📁 Создано папок: {len(self.created_dirs)}")
|
|
21
|
+
|
|
22
|
+
if self.created_files:
|
|
23
|
+
lines.append(f"✅ Создано файлов: {len(self.created_files)}")
|
|
24
|
+
for f in self.created_files[:10]:
|
|
25
|
+
lines.append(f" + {f}")
|
|
26
|
+
if len(self.created_files) > 10:
|
|
27
|
+
lines.append(f" ... и ещё {len(self.created_files) - 10}")
|
|
28
|
+
|
|
29
|
+
if self.updated_files:
|
|
30
|
+
lines.append(f"📝 Обновлено файлов: {len(self.updated_files)}")
|
|
31
|
+
for f in self.updated_files[:10]:
|
|
32
|
+
lines.append(f" ~ {f}")
|
|
33
|
+
if len(self.updated_files) > 10:
|
|
34
|
+
lines.append(f" ... и ещё {len(self.updated_files) - 10}")
|
|
35
|
+
|
|
36
|
+
if self.deleted_files:
|
|
37
|
+
lines.append(f"🗑️ Удалено файлов: {len(self.deleted_files)}")
|
|
38
|
+
for f in self.deleted_files:
|
|
39
|
+
lines.append(f" - {f}")
|
|
40
|
+
|
|
41
|
+
if self.errors:
|
|
42
|
+
lines.append(f"❌ Ошибки: {len(self.errors)}")
|
|
43
|
+
for e in self.errors:
|
|
44
|
+
lines.append(f" ! {e}")
|
|
45
|
+
|
|
46
|
+
return "\n".join(lines) if lines else "Нет изменений."
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def deploy_project(package: dict, base_dir: str) -> ProjectIOResult:
|
|
50
|
+
"""
|
|
51
|
+
Разворачивает проект из декодированного пакета.
|
|
52
|
+
"""
|
|
53
|
+
result = ProjectIOResult()
|
|
54
|
+
project_name = package["project"]
|
|
55
|
+
project_dir = os.path.join(base_dir, project_name)
|
|
56
|
+
|
|
57
|
+
# Создаём корневую папку
|
|
58
|
+
if not os.path.exists(project_dir):
|
|
59
|
+
os.makedirs(project_dir)
|
|
60
|
+
result.created_dirs.append(project_name)
|
|
61
|
+
|
|
62
|
+
for file_info in package["files"]:
|
|
63
|
+
path = file_info["path"]
|
|
64
|
+
action = file_info.get("action", "write")
|
|
65
|
+
|
|
66
|
+
# Защита от path traversal
|
|
67
|
+
full_path = os.path.normpath(os.path.join(project_dir, path))
|
|
68
|
+
if not full_path.startswith(os.path.normpath(project_dir)):
|
|
69
|
+
result.errors.append(f"Опасный путь: {path}")
|
|
70
|
+
continue
|
|
71
|
+
|
|
72
|
+
if action == "delete":
|
|
73
|
+
if os.path.exists(full_path):
|
|
74
|
+
try:
|
|
75
|
+
os.remove(full_path)
|
|
76
|
+
result.deleted_files.append(path)
|
|
77
|
+
except OSError as e:
|
|
78
|
+
result.errors.append(f"Не удалось удалить {path}: {e}")
|
|
79
|
+
|
|
80
|
+
elif action == "write":
|
|
81
|
+
content = file_info.get("content", "")
|
|
82
|
+
|
|
83
|
+
# Создаём поддиректории
|
|
84
|
+
file_dir = os.path.dirname(full_path)
|
|
85
|
+
if file_dir and not os.path.exists(file_dir):
|
|
86
|
+
os.makedirs(file_dir, exist_ok=True)
|
|
87
|
+
rel_dir = os.path.relpath(file_dir, project_dir)
|
|
88
|
+
if rel_dir not in result.created_dirs:
|
|
89
|
+
result.created_dirs.append(rel_dir)
|
|
90
|
+
|
|
91
|
+
is_update = os.path.exists(full_path)
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
with open(full_path, "w", encoding="utf-8", newline="\n") as f:
|
|
95
|
+
f.write(content)
|
|
96
|
+
|
|
97
|
+
if is_update:
|
|
98
|
+
result.updated_files.append(path)
|
|
99
|
+
else:
|
|
100
|
+
result.created_files.append(path)
|
|
101
|
+
except OSError as e:
|
|
102
|
+
result.errors.append(f"Ошибка записи {path}: {e}")
|
|
103
|
+
|
|
104
|
+
return result
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def read_project(project_dir: str) -> dict | None:
|
|
108
|
+
"""
|
|
109
|
+
Читает существующий проект с диска.
|
|
110
|
+
"""
|
|
111
|
+
if not os.path.isdir(project_dir):
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
project_name = os.path.basename(os.path.normpath(project_dir))
|
|
115
|
+
files = []
|
|
116
|
+
|
|
117
|
+
skip_dirs = {".git", "__pycache__", "node_modules", ".venv", "venv", ".idea", ".vscode"}
|
|
118
|
+
|
|
119
|
+
for root, dirs, filenames in os.walk(project_dir):
|
|
120
|
+
dirs[:] = [d for d in dirs if d not in skip_dirs and not d.startswith(".")]
|
|
121
|
+
|
|
122
|
+
for filename in sorted(filenames):
|
|
123
|
+
if filename.startswith("."):
|
|
124
|
+
continue
|
|
125
|
+
|
|
126
|
+
filepath = os.path.join(root, filename)
|
|
127
|
+
relpath = os.path.relpath(filepath, project_dir).replace(os.sep, "/")
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
with open(filepath, "r", encoding="utf-8") as f:
|
|
131
|
+
content = f.read()
|
|
132
|
+
files.append({
|
|
133
|
+
"path": relpath,
|
|
134
|
+
"action": "write",
|
|
135
|
+
"content": content
|
|
136
|
+
})
|
|
137
|
+
except (UnicodeDecodeError, PermissionError):
|
|
138
|
+
continue
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
"project": project_name,
|
|
142
|
+
"mode": "update",
|
|
143
|
+
"files": files
|
|
144
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Генерация компактного PDF-отчёта по проекту.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from fpdf import FPDF
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ProjectReport(FPDF):
|
|
10
|
+
def __init__(self):
|
|
11
|
+
super().__init__()
|
|
12
|
+
self._font_ready = False
|
|
13
|
+
self._has_unicode = False
|
|
14
|
+
# Компактные поля страницы
|
|
15
|
+
self.set_margins(10, 10, 10)
|
|
16
|
+
self.set_auto_page_break(auto=True, margin=10)
|
|
17
|
+
|
|
18
|
+
def _setup_fonts(self):
|
|
19
|
+
if self._font_ready:
|
|
20
|
+
return
|
|
21
|
+
|
|
22
|
+
font_paths = [
|
|
23
|
+
"/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf",
|
|
24
|
+
"/usr/share/fonts/TTF/DejaVuSansMono.ttf",
|
|
25
|
+
"C:/Windows/Fonts/consola.ttf",
|
|
26
|
+
"C:/Windows/Fonts/lucon.ttf",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
for fp in font_paths:
|
|
30
|
+
if os.path.exists(fp):
|
|
31
|
+
try:
|
|
32
|
+
self.add_font("Mono", "", fp, uni=True)
|
|
33
|
+
self._has_unicode = True
|
|
34
|
+
break
|
|
35
|
+
except:
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
self._font_ready = True
|
|
39
|
+
|
|
40
|
+
def set_mono(self, size=7):
|
|
41
|
+
self._setup_fonts()
|
|
42
|
+
if self._has_unicode:
|
|
43
|
+
self.set_font("Mono", "", size)
|
|
44
|
+
else:
|
|
45
|
+
self.set_font("Courier", "", size)
|
|
46
|
+
|
|
47
|
+
def header(self):
|
|
48
|
+
# Минимальный заголовок
|
|
49
|
+
self.set_mono(6)
|
|
50
|
+
self.set_text_color(128, 128, 128)
|
|
51
|
+
self.cell(0, 3, "AI Project Builder Report", 0, 1, "R")
|
|
52
|
+
self.set_text_color(0, 0, 0)
|
|
53
|
+
|
|
54
|
+
def footer(self):
|
|
55
|
+
self.set_y(-8)
|
|
56
|
+
self.set_mono(6)
|
|
57
|
+
self.set_text_color(128, 128, 128)
|
|
58
|
+
self.cell(0, 3, f"{self.page_no()}", 0, 0, "C")
|
|
59
|
+
self.set_text_color(0, 0, 0)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def generate_report(package: dict, output_path: str) -> str:
|
|
63
|
+
"""
|
|
64
|
+
Генерирует компактный PDF-отчёт по проекту.
|
|
65
|
+
Все файлы идут подряд без разрывов страниц.
|
|
66
|
+
"""
|
|
67
|
+
pdf = ProjectReport()
|
|
68
|
+
pdf.add_page()
|
|
69
|
+
|
|
70
|
+
# === Заголовок проекта (компактный) ===
|
|
71
|
+
pdf.set_mono(10)
|
|
72
|
+
pdf.cell(0, 5, f"PROJECT: {package['project']}", 0, 1)
|
|
73
|
+
pdf.set_mono(7)
|
|
74
|
+
pdf.cell(0, 3, f"Mode: {package.get('mode', 'create')} | Files: {len(package['files'])}", 0, 1)
|
|
75
|
+
|
|
76
|
+
# === Структура файлов (компактный список) ===
|
|
77
|
+
pdf.ln(2)
|
|
78
|
+
pdf.set_mono(6)
|
|
79
|
+
pdf.set_text_color(80, 80, 80)
|
|
80
|
+
|
|
81
|
+
structure_lines = []
|
|
82
|
+
for f in package["files"]:
|
|
83
|
+
icon = "+" if f.get("action") == "write" else "-"
|
|
84
|
+
lines = f.get("content", "").count("\n") + 1 if f.get("content") else 0
|
|
85
|
+
structure_lines.append(f"[{icon}] {f['path']} ({lines}L)")
|
|
86
|
+
|
|
87
|
+
# Выводим структуру в 2-3 колонки если много файлов
|
|
88
|
+
struct_text = " | ".join(structure_lines)
|
|
89
|
+
pdf.multi_cell(0, 3, struct_text)
|
|
90
|
+
|
|
91
|
+
pdf.set_text_color(0, 0, 0)
|
|
92
|
+
pdf.ln(3)
|
|
93
|
+
|
|
94
|
+
# Разделитель
|
|
95
|
+
pdf.set_draw_color(200, 200, 200)
|
|
96
|
+
pdf.line(10, pdf.get_y(), 200, pdf.get_y())
|
|
97
|
+
pdf.ln(2)
|
|
98
|
+
|
|
99
|
+
# === Содержимое файлов (подряд, без разрывов) ===
|
|
100
|
+
for f in package["files"]:
|
|
101
|
+
if f.get("action") == "delete":
|
|
102
|
+
continue
|
|
103
|
+
|
|
104
|
+
content = f.get("content", "")
|
|
105
|
+
path = f["path"]
|
|
106
|
+
|
|
107
|
+
# Заголовок файла
|
|
108
|
+
pdf.set_fill_color(240, 240, 240)
|
|
109
|
+
pdf.set_mono(7)
|
|
110
|
+
pdf.cell(0, 4, f"=== {path} ===", 0, 1, "L", True)
|
|
111
|
+
|
|
112
|
+
if not content.strip():
|
|
113
|
+
pdf.set_mono(6)
|
|
114
|
+
pdf.set_text_color(128, 128, 128)
|
|
115
|
+
pdf.cell(0, 3, "(empty)", 0, 1)
|
|
116
|
+
pdf.set_text_color(0, 0, 0)
|
|
117
|
+
else:
|
|
118
|
+
pdf.set_mono(5) # Мелкий шрифт для кода
|
|
119
|
+
|
|
120
|
+
lines = content.split("\n")
|
|
121
|
+
line_height = 2.5 # Очень компактная высота строки
|
|
122
|
+
|
|
123
|
+
for i, line in enumerate(lines, 1):
|
|
124
|
+
# Обрезаем очень длинные строки
|
|
125
|
+
display = line.replace("\t", " ") # Табы в 2 пробела
|
|
126
|
+
if len(display) > 120:
|
|
127
|
+
display = display[:117] + "..."
|
|
128
|
+
|
|
129
|
+
# Номер строки + код
|
|
130
|
+
line_text = f"{i:3d}|{display}"
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
pdf.cell(0, line_height, line_text, 0, 1)
|
|
134
|
+
except:
|
|
135
|
+
# Если символ не поддерживается
|
|
136
|
+
safe = line_text.encode("ascii", errors="replace").decode()
|
|
137
|
+
pdf.cell(0, line_height, safe, 0, 1)
|
|
138
|
+
|
|
139
|
+
# Минимальный отступ между файлами
|
|
140
|
+
pdf.ln(1)
|
|
141
|
+
|
|
142
|
+
pdf.output(output_path)
|
|
143
|
+
return output_path
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def generate_text_report(package: dict) -> str:
|
|
147
|
+
"""
|
|
148
|
+
Генерирует компактный текстовый отчёт.
|
|
149
|
+
"""
|
|
150
|
+
lines = [
|
|
151
|
+
f"PROJECT: {package['project']}",
|
|
152
|
+
f"MODE: {package.get('mode', 'create')} | FILES: {len(package['files'])}",
|
|
153
|
+
"=" * 70,
|
|
154
|
+
]
|
|
155
|
+
|
|
156
|
+
# Компактная структура
|
|
157
|
+
for f in package["files"]:
|
|
158
|
+
icon = "+" if f.get("action") == "write" else "-"
|
|
159
|
+
lc = f.get("content", "").count("\n") + 1 if f.get("content") else 0
|
|
160
|
+
lines.append(f"[{icon}] {f['path']} ({lc}L)")
|
|
161
|
+
|
|
162
|
+
lines.append("=" * 70)
|
|
163
|
+
|
|
164
|
+
# Файлы подряд
|
|
165
|
+
for f in package["files"]:
|
|
166
|
+
if f.get("action") == "delete":
|
|
167
|
+
continue
|
|
168
|
+
|
|
169
|
+
lines.append(f"\n### {f['path']} ###")
|
|
170
|
+
lines.append(f.get("content", "") or "(empty)")
|
|
171
|
+
|
|
172
|
+
return "\n".join(lines)
|
|
File without changes
|
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GUI приложение на Tkinter.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import tkinter as tk
|
|
7
|
+
from tkinter import ttk, filedialog, messagebox, scrolledtext
|
|
8
|
+
|
|
9
|
+
from ai_project_builder.core import (
|
|
10
|
+
encode_project_from_directory,
|
|
11
|
+
decode_project,
|
|
12
|
+
DecodeError,
|
|
13
|
+
deploy_project,
|
|
14
|
+
read_project,
|
|
15
|
+
generate_report,
|
|
16
|
+
)
|
|
17
|
+
from ai_project_builder.core.escapes import get_escape_reference
|
|
18
|
+
from ai_project_builder.core.report import generate_text_report
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
PROMPT_TEMPLATE = '''You are a code generator. Create a Python project based on my description.
|
|
22
|
+
|
|
23
|
+
OUTPUT FORMAT - Follow EXACTLY:
|
|
24
|
+
|
|
25
|
+
===APB_START===
|
|
26
|
+
PROJECT: project_name_here
|
|
27
|
+
MODE: create
|
|
28
|
+
|
|
29
|
+
---FILE: path/to/file.py---
|
|
30
|
+
file content here
|
|
31
|
+
---END_FILE---
|
|
32
|
+
|
|
33
|
+
---FILE: another/file.py---
|
|
34
|
+
another file content
|
|
35
|
+
---END_FILE---
|
|
36
|
+
|
|
37
|
+
===APB_END===
|
|
38
|
+
|
|
39
|
+
{escape_reference}
|
|
40
|
+
|
|
41
|
+
EXAMPLE of correct Python code in this format:
|
|
42
|
+
|
|
43
|
+
---FILE: main.py---
|
|
44
|
+
def hello():
|
|
45
|
+
print("Hello, World!")
|
|
46
|
+
|
|
47
|
+
if {{UNDER}}{{UNDER}}name{{UNDER}}{{UNDER}} == "{{UNDER}}{{UNDER}}main{{UNDER}}{{UNDER}}":
|
|
48
|
+
hello()
|
|
49
|
+
---END_FILE---
|
|
50
|
+
|
|
51
|
+
Notice: parentheses () and brackets [] are written directly, NOT escaped!
|
|
52
|
+
|
|
53
|
+
CRITICAL RULES:
|
|
54
|
+
1. Start with ===APB_START=== end with ===APB_END===
|
|
55
|
+
2. Each file: ---FILE: path--- content ---END_FILE---
|
|
56
|
+
3. Escape ONLY: * _ ` # > < | (see list above)
|
|
57
|
+
4. Do NOT escape: ( ) [ ] {{ }} = : ; , . / \\ " '
|
|
58
|
+
5. Include ALL files: main.py, __init__.py, requirements.txt, README.md
|
|
59
|
+
6. Code must be complete and runnable
|
|
60
|
+
|
|
61
|
+
PROJECT DESCRIPTION:
|
|
62
|
+
{description}
|
|
63
|
+
|
|
64
|
+
Generate the complete project now:'''
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
UPDATE_PROMPT_TEMPLATE = '''You are a code editor. Modify the existing project based on my request.
|
|
68
|
+
|
|
69
|
+
CURRENT PROJECT:
|
|
70
|
+
{current_project}
|
|
71
|
+
|
|
72
|
+
REQUESTED CHANGES:
|
|
73
|
+
{changes}
|
|
74
|
+
|
|
75
|
+
OUTPUT FORMAT - Include ONLY changed files:
|
|
76
|
+
|
|
77
|
+
===APB_START===
|
|
78
|
+
PROJECT: project_name_here
|
|
79
|
+
MODE: update
|
|
80
|
+
|
|
81
|
+
---FILE: path/to/changed_file.py---
|
|
82
|
+
complete new content of file
|
|
83
|
+
---END_FILE---
|
|
84
|
+
|
|
85
|
+
---DELETE: path/to/removed_file.py---
|
|
86
|
+
|
|
87
|
+
===APB_END===
|
|
88
|
+
|
|
89
|
+
{escape_reference}
|
|
90
|
+
|
|
91
|
+
RULES:
|
|
92
|
+
1. Only include files that need to be changed or deleted
|
|
93
|
+
2. For changed files, include the COMPLETE new content
|
|
94
|
+
3. Use ---DELETE: path--- for files to remove
|
|
95
|
+
4. Use escape sequences for special characters
|
|
96
|
+
|
|
97
|
+
Generate the changes now:'''
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class App:
|
|
101
|
+
def __init__(self):
|
|
102
|
+
self.root = tk.Tk()
|
|
103
|
+
self.root.title("AI Project Builder v2")
|
|
104
|
+
self.root.geometry("950x750")
|
|
105
|
+
self.root.minsize(800, 600)
|
|
106
|
+
|
|
107
|
+
self.output_dir = tk.StringVar(value=os.path.expanduser("~/Projects"))
|
|
108
|
+
self.project_dir = tk.StringVar()
|
|
109
|
+
self.encode_dir = tk.StringVar()
|
|
110
|
+
|
|
111
|
+
self._build_ui()
|
|
112
|
+
|
|
113
|
+
def _build_ui(self):
|
|
114
|
+
notebook = ttk.Notebook(self.root)
|
|
115
|
+
notebook.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
|
116
|
+
|
|
117
|
+
self._build_prompt_tab(notebook)
|
|
118
|
+
self._build_deploy_tab(notebook)
|
|
119
|
+
self._build_report_tab(notebook)
|
|
120
|
+
self._build_encode_tab(notebook)
|
|
121
|
+
self._build_help_tab(notebook)
|
|
122
|
+
|
|
123
|
+
self.status = tk.StringVar(value="Ready")
|
|
124
|
+
ttk.Label(
|
|
125
|
+
self.root,
|
|
126
|
+
textvariable=self.status,
|
|
127
|
+
relief=tk.SUNKEN,
|
|
128
|
+
anchor=tk.W,
|
|
129
|
+
padding=(10, 5)
|
|
130
|
+
).pack(fill=tk.X, padx=10, pady=(0, 10))
|
|
131
|
+
|
|
132
|
+
# ==================== PROMPT TAB ====================
|
|
133
|
+
def _build_prompt_tab(self, notebook):
|
|
134
|
+
tab = ttk.Frame(notebook, padding=10)
|
|
135
|
+
notebook.add(tab, text=" 📝 Generate Prompt ")
|
|
136
|
+
|
|
137
|
+
ttk.Label(tab, text="Describe your project:", font=("", 11, "bold")).pack(anchor=tk.W)
|
|
138
|
+
|
|
139
|
+
self.description = scrolledtext.ScrolledText(tab, height=5, font=("Consolas", 10), wrap=tk.WORD)
|
|
140
|
+
self.description.pack(fill=tk.X, pady=(5, 10))
|
|
141
|
+
self.description.insert("1.0", "Example: A CLI tool that converts CSV files to JSON with filtering support")
|
|
142
|
+
|
|
143
|
+
btn_frame = ttk.Frame(tab)
|
|
144
|
+
btn_frame.pack(fill=tk.X, pady=5)
|
|
145
|
+
|
|
146
|
+
ttk.Button(btn_frame, text="🆕 Create Project Prompt", command=self._gen_create_prompt).pack(side=tk.LEFT, padx=(0, 5))
|
|
147
|
+
ttk.Button(btn_frame, text="✏️ Update Project Prompt", command=self._gen_update_prompt).pack(side=tk.LEFT, padx=(0, 5))
|
|
148
|
+
ttk.Button(btn_frame, text="📋 Copy to Clipboard", command=self._copy_prompt).pack(side=tk.RIGHT)
|
|
149
|
+
|
|
150
|
+
ttk.Label(tab, text="Generated prompt (copy to AI):", font=("", 11, "bold")).pack(anchor=tk.W, pady=(15, 5))
|
|
151
|
+
|
|
152
|
+
self.prompt_output = scrolledtext.ScrolledText(tab, font=("Consolas", 9), wrap=tk.WORD)
|
|
153
|
+
self.prompt_output.pack(fill=tk.BOTH, expand=True)
|
|
154
|
+
|
|
155
|
+
def _gen_create_prompt(self):
|
|
156
|
+
desc = self.description.get("1.0", tk.END).strip()
|
|
157
|
+
if not desc:
|
|
158
|
+
messagebox.showwarning("Warning", "Enter project description")
|
|
159
|
+
return
|
|
160
|
+
|
|
161
|
+
prompt = PROMPT_TEMPLATE.format(
|
|
162
|
+
description=desc,
|
|
163
|
+
escape_reference=get_escape_reference()
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
self.prompt_output.delete("1.0", tk.END)
|
|
167
|
+
self.prompt_output.insert("1.0", prompt)
|
|
168
|
+
self.status.set("Create prompt generated - copy and paste to AI")
|
|
169
|
+
|
|
170
|
+
def _gen_update_prompt(self):
|
|
171
|
+
desc = self.description.get("1.0", tk.END).strip()
|
|
172
|
+
if not desc:
|
|
173
|
+
messagebox.showwarning("Warning", "Enter change description")
|
|
174
|
+
return
|
|
175
|
+
|
|
176
|
+
path = filedialog.askdirectory(title="Select existing project folder")
|
|
177
|
+
if not path:
|
|
178
|
+
return
|
|
179
|
+
|
|
180
|
+
try:
|
|
181
|
+
encoded = encode_project_from_directory(path, mode="update")
|
|
182
|
+
except Exception as e:
|
|
183
|
+
messagebox.showerror("Error", f"Failed to read project:\n{e}")
|
|
184
|
+
return
|
|
185
|
+
|
|
186
|
+
prompt = UPDATE_PROMPT_TEMPLATE.format(
|
|
187
|
+
current_project=encoded,
|
|
188
|
+
changes=desc,
|
|
189
|
+
escape_reference=get_escape_reference()
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
self.prompt_output.delete("1.0", tk.END)
|
|
193
|
+
self.prompt_output.insert("1.0", prompt)
|
|
194
|
+
self.status.set(f"Update prompt generated for: {os.path.basename(path)}")
|
|
195
|
+
|
|
196
|
+
def _copy_prompt(self):
|
|
197
|
+
content = self.prompt_output.get("1.0", tk.END).strip()
|
|
198
|
+
if not content:
|
|
199
|
+
messagebox.showwarning("Warning", "No prompt to copy")
|
|
200
|
+
return
|
|
201
|
+
|
|
202
|
+
self.root.clipboard_clear()
|
|
203
|
+
self.root.clipboard_append(content)
|
|
204
|
+
self.status.set("Prompt copied to clipboard!")
|
|
205
|
+
|
|
206
|
+
# ==================== DEPLOY TAB ====================
|
|
207
|
+
def _build_deploy_tab(self, notebook):
|
|
208
|
+
tab = ttk.Frame(notebook, padding=10)
|
|
209
|
+
notebook.add(tab, text=" 🚀 Deploy Project ")
|
|
210
|
+
|
|
211
|
+
dir_frame = ttk.LabelFrame(tab, text="Output Directory", padding=10)
|
|
212
|
+
dir_frame.pack(fill=tk.X, pady=(0, 10))
|
|
213
|
+
|
|
214
|
+
dir_row = ttk.Frame(dir_frame)
|
|
215
|
+
dir_row.pack(fill=tk.X)
|
|
216
|
+
ttk.Entry(dir_row, textvariable=self.output_dir, font=("Consolas", 10)).pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 10))
|
|
217
|
+
ttk.Button(dir_row, text="Browse...", command=self._browse_output).pack(side=tk.RIGHT)
|
|
218
|
+
|
|
219
|
+
ttk.Label(tab, text="Paste AI response here:", font=("", 11, "bold")).pack(anchor=tk.W)
|
|
220
|
+
|
|
221
|
+
self.ai_response = scrolledtext.ScrolledText(tab, height=15, font=("Consolas", 9))
|
|
222
|
+
self.ai_response.pack(fill=tk.BOTH, expand=True, pady=(5, 10))
|
|
223
|
+
|
|
224
|
+
btn_frame = ttk.Frame(tab)
|
|
225
|
+
btn_frame.pack(fill=tk.X, pady=5)
|
|
226
|
+
|
|
227
|
+
ttk.Button(btn_frame, text="🔍 Validate", command=self._validate).pack(side=tk.LEFT, padx=(0, 5))
|
|
228
|
+
ttk.Button(btn_frame, text="🚀 Deploy Project", command=self._deploy).pack(side=tk.LEFT, padx=(0, 5))
|
|
229
|
+
ttk.Button(btn_frame, text="🗑️ Clear", command=lambda: self.ai_response.delete("1.0", tk.END)).pack(side=tk.RIGHT)
|
|
230
|
+
|
|
231
|
+
ttk.Label(tab, text="Result:", font=("", 10)).pack(anchor=tk.W, pady=(10, 0))
|
|
232
|
+
self.deploy_log = scrolledtext.ScrolledText(tab, height=6, font=("Consolas", 9), state=tk.DISABLED)
|
|
233
|
+
self.deploy_log.pack(fill=tk.X)
|
|
234
|
+
|
|
235
|
+
def _browse_output(self):
|
|
236
|
+
path = filedialog.askdirectory(title="Select output directory")
|
|
237
|
+
if path:
|
|
238
|
+
self.output_dir.set(path)
|
|
239
|
+
|
|
240
|
+
def _validate(self):
|
|
241
|
+
text = self.ai_response.get("1.0", tk.END).strip()
|
|
242
|
+
if not text:
|
|
243
|
+
messagebox.showwarning("Warning", "Paste AI response first")
|
|
244
|
+
return
|
|
245
|
+
|
|
246
|
+
try:
|
|
247
|
+
pkg = decode_project(text)
|
|
248
|
+
|
|
249
|
+
files_info = []
|
|
250
|
+
for f in pkg["files"]:
|
|
251
|
+
icon = "📄" if f.get("action") == "write" else "🗑️"
|
|
252
|
+
lines = f.get("content", "").count("\n") + 1 if f.get("content") else 0
|
|
253
|
+
files_info.append(f" {icon} {f['path']} ({lines} lines)")
|
|
254
|
+
|
|
255
|
+
self._log_deploy(
|
|
256
|
+
f"✅ Validation successful!\n\n"
|
|
257
|
+
f"Project: {pkg['project']}\n"
|
|
258
|
+
f"Mode: {pkg['mode']}\n"
|
|
259
|
+
f"Files: {len(pkg['files'])}\n\n"
|
|
260
|
+
+ "\n".join(files_info)
|
|
261
|
+
)
|
|
262
|
+
self.status.set("Validation passed")
|
|
263
|
+
|
|
264
|
+
except DecodeError as e:
|
|
265
|
+
self._log_deploy(f"❌ Validation failed:\n\n{e}")
|
|
266
|
+
self.status.set("Validation failed")
|
|
267
|
+
|
|
268
|
+
def _deploy(self):
|
|
269
|
+
text = self.ai_response.get("1.0", tk.END).strip()
|
|
270
|
+
output = self.output_dir.get().strip()
|
|
271
|
+
|
|
272
|
+
if not text:
|
|
273
|
+
messagebox.showwarning("Warning", "Paste AI response first")
|
|
274
|
+
return
|
|
275
|
+
if not output:
|
|
276
|
+
messagebox.showwarning("Warning", "Select output directory")
|
|
277
|
+
return
|
|
278
|
+
|
|
279
|
+
try:
|
|
280
|
+
pkg = decode_project(text)
|
|
281
|
+
except DecodeError as e:
|
|
282
|
+
messagebox.showerror("Decode Error", str(e))
|
|
283
|
+
return
|
|
284
|
+
|
|
285
|
+
project_path = os.path.join(output, pkg["project"])
|
|
286
|
+
|
|
287
|
+
if os.path.exists(project_path):
|
|
288
|
+
if not messagebox.askyesno(
|
|
289
|
+
"Project Exists",
|
|
290
|
+
f"'{pkg['project']}' already exists.\n\nUpdate existing files?"
|
|
291
|
+
):
|
|
292
|
+
return
|
|
293
|
+
|
|
294
|
+
os.makedirs(output, exist_ok=True)
|
|
295
|
+
|
|
296
|
+
try:
|
|
297
|
+
result = deploy_project(pkg, output)
|
|
298
|
+
self._log_deploy(
|
|
299
|
+
f"✅ Project deployed!\n\n"
|
|
300
|
+
f"Location: {project_path}\n\n"
|
|
301
|
+
f"{result.summary()}"
|
|
302
|
+
)
|
|
303
|
+
self.status.set(f"Deployed: {project_path}")
|
|
304
|
+
except Exception as e:
|
|
305
|
+
messagebox.showerror("Deploy Error", str(e))
|
|
306
|
+
|
|
307
|
+
def _log_deploy(self, text: str):
|
|
308
|
+
self.deploy_log.config(state=tk.NORMAL)
|
|
309
|
+
self.deploy_log.delete("1.0", tk.END)
|
|
310
|
+
self.deploy_log.insert("1.0", text)
|
|
311
|
+
self.deploy_log.config(state=tk.DISABLED)
|
|
312
|
+
|
|
313
|
+
# ==================== REPORT TAB ====================
|
|
314
|
+
def _build_report_tab(self, notebook):
|
|
315
|
+
tab = ttk.Frame(notebook, padding=10)
|
|
316
|
+
notebook.add(tab, text=" 📄 Report ")
|
|
317
|
+
|
|
318
|
+
ttk.Label(tab, text="Generate PDF report from existing project", font=("", 12, "bold")).pack(anchor=tk.W)
|
|
319
|
+
|
|
320
|
+
dir_frame = ttk.Frame(tab)
|
|
321
|
+
dir_frame.pack(fill=tk.X, pady=15)
|
|
322
|
+
|
|
323
|
+
ttk.Label(dir_frame, text="Project folder:").pack(anchor=tk.W)
|
|
324
|
+
row = ttk.Frame(dir_frame)
|
|
325
|
+
row.pack(fill=tk.X, pady=5)
|
|
326
|
+
ttk.Entry(row, textvariable=self.project_dir, font=("Consolas", 10)).pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 10))
|
|
327
|
+
ttk.Button(row, text="Browse...", command=lambda: self._browse_var(self.project_dir)).pack(side=tk.RIGHT)
|
|
328
|
+
|
|
329
|
+
btn_frame = ttk.Frame(tab)
|
|
330
|
+
btn_frame.pack(fill=tk.X, pady=10)
|
|
331
|
+
|
|
332
|
+
ttk.Button(btn_frame, text="💾 Save PDF Report", command=self._save_pdf).pack(side=tk.LEFT, padx=(0, 5))
|
|
333
|
+
ttk.Button(btn_frame, text="📋 Copy Text Report", command=self._copy_text_report).pack(side=tk.LEFT)
|
|
334
|
+
|
|
335
|
+
ttk.Label(tab, text="Preview:", font=("", 10, "bold")).pack(anchor=tk.W, pady=(10, 5))
|
|
336
|
+
self.report_preview = scrolledtext.ScrolledText(tab, font=("Consolas", 9), state=tk.DISABLED)
|
|
337
|
+
self.report_preview.pack(fill=tk.BOTH, expand=True)
|
|
338
|
+
|
|
339
|
+
def _save_pdf(self):
|
|
340
|
+
path = self.project_dir.get().strip()
|
|
341
|
+
if not path or not os.path.isdir(path):
|
|
342
|
+
messagebox.showwarning("Warning", "Select valid project folder")
|
|
343
|
+
return
|
|
344
|
+
|
|
345
|
+
pkg = read_project(path)
|
|
346
|
+
if not pkg or not pkg["files"]:
|
|
347
|
+
messagebox.showwarning("Warning", "Project is empty or unreadable")
|
|
348
|
+
return
|
|
349
|
+
|
|
350
|
+
output = filedialog.asksaveasfilename(
|
|
351
|
+
title="Save PDF Report",
|
|
352
|
+
defaultextension=".pdf",
|
|
353
|
+
filetypes=[("PDF", "*.pdf")],
|
|
354
|
+
initialfile=f"{pkg['project']}_report.pdf"
|
|
355
|
+
)
|
|
356
|
+
if not output:
|
|
357
|
+
return
|
|
358
|
+
|
|
359
|
+
try:
|
|
360
|
+
generate_report(pkg, output)
|
|
361
|
+
self._show_preview(pkg)
|
|
362
|
+
messagebox.showinfo("Success", f"Report saved:\n{output}")
|
|
363
|
+
self.status.set(f"Report saved: {output}")
|
|
364
|
+
except Exception as e:
|
|
365
|
+
messagebox.showerror("Error", str(e))
|
|
366
|
+
|
|
367
|
+
def _copy_text_report(self):
|
|
368
|
+
path = self.project_dir.get().strip()
|
|
369
|
+
if not path or not os.path.isdir(path):
|
|
370
|
+
messagebox.showwarning("Warning", "Select valid project folder")
|
|
371
|
+
return
|
|
372
|
+
|
|
373
|
+
pkg = read_project(path)
|
|
374
|
+
if not pkg:
|
|
375
|
+
return
|
|
376
|
+
|
|
377
|
+
text = generate_text_report(pkg)
|
|
378
|
+
self.root.clipboard_clear()
|
|
379
|
+
self.root.clipboard_append(text)
|
|
380
|
+
self._show_preview(pkg)
|
|
381
|
+
self.status.set("Text report copied to clipboard")
|
|
382
|
+
|
|
383
|
+
def _show_preview(self, pkg: dict):
|
|
384
|
+
text = generate_text_report(pkg)
|
|
385
|
+
self.report_preview.config(state=tk.NORMAL)
|
|
386
|
+
self.report_preview.delete("1.0", tk.END)
|
|
387
|
+
self.report_preview.insert("1.0", text)
|
|
388
|
+
self.report_preview.config(state=tk.DISABLED)
|
|
389
|
+
|
|
390
|
+
# ==================== ENCODE TAB ====================
|
|
391
|
+
def _build_encode_tab(self, notebook):
|
|
392
|
+
tab = ttk.Frame(notebook, padding=10)
|
|
393
|
+
notebook.add(tab, text=" 🔒 Encode Project ")
|
|
394
|
+
|
|
395
|
+
ttk.Label(tab, text="Encode existing project for AI editing", font=("", 12, "bold")).pack(anchor=tk.W)
|
|
396
|
+
|
|
397
|
+
dir_frame = ttk.Frame(tab)
|
|
398
|
+
dir_frame.pack(fill=tk.X, pady=15)
|
|
399
|
+
|
|
400
|
+
ttk.Label(dir_frame, text="Project folder:").pack(anchor=tk.W)
|
|
401
|
+
row = ttk.Frame(dir_frame)
|
|
402
|
+
row.pack(fill=tk.X, pady=5)
|
|
403
|
+
ttk.Entry(row, textvariable=self.encode_dir, font=("Consolas", 10)).pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 10))
|
|
404
|
+
ttk.Button(row, text="Browse...", command=lambda: self._browse_var(self.encode_dir)).pack(side=tk.RIGHT)
|
|
405
|
+
|
|
406
|
+
btn_frame = ttk.Frame(tab)
|
|
407
|
+
btn_frame.pack(fill=tk.X, pady=5)
|
|
408
|
+
|
|
409
|
+
ttk.Button(btn_frame, text="🔒 Encode", command=self._encode).pack(side=tk.LEFT, padx=(0, 5))
|
|
410
|
+
ttk.Button(btn_frame, text="📋 Copy", command=self._copy_encoded).pack(side=tk.LEFT)
|
|
411
|
+
|
|
412
|
+
ttk.Label(tab, text="Encoded project:", font=("", 10, "bold")).pack(anchor=tk.W, pady=(10, 5))
|
|
413
|
+
self.encoded_output = scrolledtext.ScrolledText(tab, font=("Consolas", 9))
|
|
414
|
+
self.encoded_output.pack(fill=tk.BOTH, expand=True)
|
|
415
|
+
|
|
416
|
+
def _encode(self):
|
|
417
|
+
path = self.encode_dir.get().strip()
|
|
418
|
+
if not path or not os.path.isdir(path):
|
|
419
|
+
messagebox.showwarning("Warning", "Select valid project folder")
|
|
420
|
+
return
|
|
421
|
+
|
|
422
|
+
try:
|
|
423
|
+
encoded = encode_project_from_directory(path)
|
|
424
|
+
self.encoded_output.delete("1.0", tk.END)
|
|
425
|
+
self.encoded_output.insert("1.0", encoded)
|
|
426
|
+
|
|
427
|
+
size_kb = len(encoded) / 1024
|
|
428
|
+
self.status.set(f"Encoded: {size_kb:.1f} KB")
|
|
429
|
+
except Exception as e:
|
|
430
|
+
messagebox.showerror("Error", str(e))
|
|
431
|
+
|
|
432
|
+
def _copy_encoded(self):
|
|
433
|
+
content = self.encoded_output.get("1.0", tk.END).strip()
|
|
434
|
+
if not content:
|
|
435
|
+
return
|
|
436
|
+
|
|
437
|
+
self.root.clipboard_clear()
|
|
438
|
+
self.root.clipboard_append(content)
|
|
439
|
+
self.status.set("Encoded project copied to clipboard")
|
|
440
|
+
|
|
441
|
+
# ==================== HELP TAB ====================
|
|
442
|
+
def _build_help_tab(self, notebook):
|
|
443
|
+
tab = ttk.Frame(notebook, padding=10)
|
|
444
|
+
notebook.add(tab, text=" ❓ Help ")
|
|
445
|
+
|
|
446
|
+
help_text = """
|
|
447
|
+
AI PROJECT BUILDER v2.0
|
|
448
|
+
=======================
|
|
449
|
+
|
|
450
|
+
HOW TO USE:
|
|
451
|
+
|
|
452
|
+
1. GENERATE PROMPT
|
|
453
|
+
- Write a description of your project
|
|
454
|
+
- Click "Create Project Prompt"
|
|
455
|
+
- Copy the generated prompt
|
|
456
|
+
|
|
457
|
+
2. ASK AI
|
|
458
|
+
- Paste the prompt into ChatGPT, Claude, or any AI
|
|
459
|
+
- The AI will generate code in APB format
|
|
460
|
+
|
|
461
|
+
3. DEPLOY
|
|
462
|
+
- Copy the AI response
|
|
463
|
+
- Paste into "Deploy Project" tab
|
|
464
|
+
- Click "Validate" to check
|
|
465
|
+
- Click "Deploy Project" to create files
|
|
466
|
+
|
|
467
|
+
4. UPDATE EXISTING PROJECT
|
|
468
|
+
- Use "Update Project Prompt" button
|
|
469
|
+
- Select existing project folder
|
|
470
|
+
- Describe changes you want
|
|
471
|
+
|
|
472
|
+
APB FORMAT:
|
|
473
|
+
===========
|
|
474
|
+
===APB_START===
|
|
475
|
+
PROJECT: my_project
|
|
476
|
+
MODE: create
|
|
477
|
+
|
|
478
|
+
---FILE: main.py---
|
|
479
|
+
print("Hello!")
|
|
480
|
+
---END_FILE---
|
|
481
|
+
|
|
482
|
+
===APB_END===
|
|
483
|
+
|
|
484
|
+
ESCAPE SEQUENCES:
|
|
485
|
+
=================
|
|
486
|
+
The AI uses special sequences to avoid Markdown issues:
|
|
487
|
+
|
|
488
|
+
{{STAR}} → *
|
|
489
|
+
{{STAR2}} → **
|
|
490
|
+
{{HASH}} → #
|
|
491
|
+
{{TICK}} → `
|
|
492
|
+
{{TICK3}} → ```
|
|
493
|
+
{{GT}} → >
|
|
494
|
+
{{LT}} → <
|
|
495
|
+
|
|
496
|
+
Parentheses () [] {} are NOT escaped - they are safe!
|
|
497
|
+
|
|
498
|
+
These are automatically converted when deploying.
|
|
499
|
+
|
|
500
|
+
TIPS:
|
|
501
|
+
=====
|
|
502
|
+
• Use Claude or GPT-4 for best results
|
|
503
|
+
• For complex projects, generate in parts
|
|
504
|
+
• Always validate before deploying
|
|
505
|
+
• Check the "Encode" tab to share existing projects
|
|
506
|
+
"""
|
|
507
|
+
|
|
508
|
+
text = scrolledtext.ScrolledText(tab, font=("Consolas", 10), wrap=tk.WORD)
|
|
509
|
+
text.pack(fill=tk.BOTH, expand=True)
|
|
510
|
+
text.insert("1.0", help_text.strip())
|
|
511
|
+
text.config(state=tk.DISABLED)
|
|
512
|
+
|
|
513
|
+
# ==================== HELPERS ====================
|
|
514
|
+
def _browse_var(self, var):
|
|
515
|
+
path = filedialog.askdirectory()
|
|
516
|
+
if path:
|
|
517
|
+
var.set(path)
|
|
518
|
+
|
|
519
|
+
def run(self):
|
|
520
|
+
self.root.mainloop()
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
You are a project generator. Create a complete Python project.
|
|
2
|
+
|
|
3
|
+
OUTPUT FORMAT:
|
|
4
|
+
1. JSON: {"project":"<name>","mode":"create","files":[{"path":"<path>","action":"write","content":"<content>"}]}
|
|
5
|
+
2. Convert JSON to UTF-8 bytes -> hex string (lowercase)
|
|
6
|
+
3. Prepend: APBPACK1
|
|
7
|
+
4. Output ONLY: APBPACK1<hex>
|
|
8
|
+
|
|
9
|
+
No markdown. No explanations. Only the hex string.
|
|
10
|
+
|
|
11
|
+
PROJECT:
|
|
12
|
+
{description}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ai-project-builder
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: Fast Python project creation with AI
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: fpdf2>=2.7.0
|
|
8
|
+
|
|
9
|
+
# AI Project Builder
|
|
10
|
+
|
|
11
|
+
Create Python projects instantly using AI.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pipx install . --system-site-packages
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/ai_project_builder/__init__.py
|
|
4
|
+
src/ai_project_builder/__main__.py
|
|
5
|
+
src/ai_project_builder.egg-info/PKG-INFO
|
|
6
|
+
src/ai_project_builder.egg-info/SOURCES.txt
|
|
7
|
+
src/ai_project_builder.egg-info/dependency_links.txt
|
|
8
|
+
src/ai_project_builder.egg-info/entry_points.txt
|
|
9
|
+
src/ai_project_builder.egg-info/requires.txt
|
|
10
|
+
src/ai_project_builder.egg-info/top_level.txt
|
|
11
|
+
src/ai_project_builder/core/__init__.py
|
|
12
|
+
src/ai_project_builder/core/decoder.py
|
|
13
|
+
src/ai_project_builder/core/encoder.py
|
|
14
|
+
src/ai_project_builder/core/escapes.py
|
|
15
|
+
src/ai_project_builder/core/project_io.py
|
|
16
|
+
src/ai_project_builder/core/report.py
|
|
17
|
+
src/ai_project_builder/gui/__init__.py
|
|
18
|
+
src/ai_project_builder/gui/app.py
|
|
19
|
+
src/ai_project_builder/prompt/base_prompt.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
fpdf2>=2.7.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ai_project_builder
|