"""
HTML generator for project page generation.
Generates the final HTML project page from planned content.
"""
import json
import yaml
import os
import io
import re
import json
import yaml
from pathlib import Path
from urllib.parse import urlparse
from datetime import datetime
from jinja2 import Environment, StrictUndefined
from camel.models import ModelFactory
from camel.agents import ChatAgent
from utils.wei_utils import get_agent_config, account_token
from utils.src.utils import get_json_from_response, extract_html_code_block
from ProjectPageAgent.css_checker import check_css
from utils.src.utils import run_sync_screenshots
from PIL import Image
from camel.messages import BaseMessage
from camel.models import ModelFactory
def to_url(input_path_or_url: str) -> str:
parsed = urlparse(input_path_or_url)
if parsed.scheme in ("http", "https", "file"):
return input_path_or_url
p = Path(input_path_or_url).expanduser().resolve()
if not p.exists():
raise FileNotFoundError(f"Input not found: {p}")
return p.as_uri() # file://...
def crop_image_to_max_size(image_path, max_bytes=8*1024*1024, output_path=None):
img = Image.open(image_path)
img_format = img.format
if output_path is None:
output_path = image_path
buffer = io.BytesIO()
img.save(buffer, format=img_format)
size = buffer.getbuffer().nbytes
if size <= max_bytes:
img.save(output_path, format=img_format)
return output_path
width, height = img.size
scale = max_bytes / size
new_height = max(int(height * scale), 1)
img_cropped = img.crop((0, 0, width, new_height))
img_cropped.save(output_path, format=img_format)
return output_path
class ProjectPageHTMLGenerator:
"""Generates HTML project pages from planned content."""
def __init__(self, agent_config,args):
self.agent_config = agent_config
self.args = args
self.html_agent = self._create_html_agent()
self.review_agent = self._create_review_agent()
self.table_agent = self._create_table_agent()
self.long_agent = self._create_long_agent()
# self.client = OpenAI(api_key=api_key,base_url=api_url)
def _create_html_agent(self):
"""Create the HTML generation agent."""
model_type = str(self.agent_config['model_type'])
# Get API key from environment variables
api_key = None
if self.args.model_name_t in ['4o', '4o-mini', 'gpt-4.1', 'gpt-4.1-mini', 'o1', 'o3', 'o3-mini']:
api_key = os.environ.get('OPENAI_API_KEY')
elif self.args.model_name_t in ['gemini', 'gemini-2.5-pro', 'gemini-2.5-flash']:
api_key = os.environ.get('GEMINI_API_KEY')
elif self.args.model_name_t in ['qwen', 'qwen-plus', 'qwen-max', 'qwen-long']:
api_key = os.environ.get('QWEN_API_KEY')
elif self.args.model_name_t.startswith('openrouter_'):
api_key = os.environ.get('OPENROUTER_API_KEY')
elif self.args.model_name_t in ['zhipuai']:
api_key = os.environ.get('ZHIPUAI_API_KEY')
if model_type.startswith('vllm_qwen') or 'vllm' in model_type.lower():
model = ModelFactory.create(
model_platform=self.agent_config['model_platform'],
model_type=self.agent_config['model_type'],
model_config_dict=self.agent_config['model_config'],
url=self.agent_config.get('url', None),
api_key=api_key,
)
else:
model = ModelFactory.create(
model_platform=self.agent_config['model_platform'],
model_type=self.agent_config['model_type'],
model_config_dict=self.agent_config['model_config'],
api_key=api_key,
)
system_message = """You are an expert web developer specializing in creating professional project pages for research papers.
You have extensive experience in HTML5, CSS3, responsive design, and academic content presentation.
Your goal is to create engaging, well-structured, and visually appealing project pages."""
return ChatAgent(
system_message=system_message,
model=model,
message_window_size=10
)
def _create_review_agent(self):
with open('utils/prompt_templates/page_templates/html_review.yaml', 'r') as f:
prompt_config = yaml.safe_load(f)
jinja_env = Environment(undefined=StrictUndefined)
system_message_template = jinja_env.from_string(prompt_config["system_prompt"])
system_message = system_message_template.render()
model_type = self.args.model_name_v
# Get API key from environment variables
api_key = None
if self.args.model_name_v in ['4o', '4o-mini', 'gpt-4.1', 'gpt-4.1-mini', 'o1', 'o3', 'o3-mini']:
api_key = os.environ.get('OPENAI_API_KEY')
elif self.args.model_name_v in ['gemini', 'gemini-2.5-pro', 'gemini-2.5-flash']:
api_key = os.environ.get('GEMINI_API_KEY')
elif self.args.model_name_v in ['qwen', 'qwen-plus', 'qwen-max', 'qwen-long']:
api_key = os.environ.get('QWEN_API_KEY')
elif self.args.model_name_v.startswith('openrouter_'):
api_key = os.environ.get('OPENROUTER_API_KEY')
elif self.args.model_name_v in ['zhipuai']:
api_key = os.environ.get('ZHIPUAI_API_KEY')
config = get_agent_config(model_type)
model = ModelFactory.create(
model_platform=config['model_platform'],
model_type=config['model_type'],
model_config_dict=config['model_config'],
url=config.get('url', None),
api_key=api_key,
)
return ChatAgent(
system_message=system_message,
model=model,
message_window_size=10
)
def _create_table_agent(self):
model_type = self.args.model_name_v
# Get API key from environment variables
api_key = None
if self.args.model_name_v in ['4o', '4o-mini', 'gpt-4.1', 'gpt-4.1-mini', 'o1', 'o3', 'o3-mini']:
api_key = os.environ.get('OPENAI_API_KEY')
elif self.args.model_name_v in ['gemini', 'gemini-2.5-pro', 'gemini-2.5-flash']:
api_key = os.environ.get('GEMINI_API_KEY')
elif self.args.model_name_v in ['qwen', 'qwen-plus', 'qwen-max', 'qwen-long']:
api_key = os.environ.get('QWEN_API_KEY')
elif self.args.model_name_v.startswith('openrouter_'):
api_key = os.environ.get('OPENROUTER_API_KEY')
elif self.args.model_name_v in ['zhipuai']:
api_key = os.environ.get('ZHIPUAI_API_KEY')
vlm_config = get_agent_config(model_type)
vlm_model = ModelFactory.create(
model_platform=vlm_config['model_platform'],
model_type=vlm_config['model_type'],
model_config_dict=vlm_config['model_config'],
url=vlm_config.get('url', None),
api_key=api_key,
)
return ChatAgent(
system_message=None,
model=vlm_model,
message_window_size=10,
)
def _create_long_agent(self):
model_type = self.args.model_name_t
# Get API key from environment variables
api_key = None
if self.args.model_name_t in ['4o', '4o-mini', 'gpt-4.1', 'gpt-4.1-mini', 'o1', 'o3', 'o3-mini']:
api_key = os.environ.get('OPENAI_API_KEY')
elif self.args.model_name_t in ['gemini', 'gemini-2.5-pro', 'gemini-2.5-flash']:
api_key = os.environ.get('GEMINI_API_KEY')
elif self.args.model_name_t in ['qwen', 'qwen-plus', 'qwen-max', 'qwen-long']:
api_key = os.environ.get('QWEN_API_KEY')
elif self.args.model_name_t.startswith('openrouter_'):
api_key = os.environ.get('OPENROUTER_API_KEY')
elif self.args.model_name_t in ['zhipuai']:
api_key = os.environ.get('ZHIPUAI_API_KEY')
long_config = get_agent_config(model_type)
long_model = ModelFactory.create(
model_platform=long_config['model_platform'],
model_type=long_config['model_type'],
model_config_dict=long_config['model_config'],
url=long_config.get('url', None),
api_key=api_key,
)
return ChatAgent(
system_message=None,
model=long_model,
message_window_size=10,
token_limit=long_config.get('token_limit', None)
)
def render_html_to_png(self, iter, html_content, project_output_dir) -> str:
import time
tmp_html = Path(project_output_dir) / f"index_iter{iter}.html"
tmp_html.write_text(html_content, encoding="utf-8")
url = tmp_html.resolve().as_uri()
image_path = str(Path(project_output_dir) / f"page_iter{iter}.png")
run_sync_screenshots(url, image_path)
return image_path
def get_revision_suggestions(self, image_path: str, html_path) -> str:
def crop_image_max_width(img, max_width=1280):
width, height = img.size
if width > max_width:
img = img.crop((0, 0, max_width, height)) # (left, top, right, bottom)
return img
img = Image.open(image_path)
img = crop_image_max_width(img, max_width=1280)
img.save(image_path,format='PNG')
crop_image_to_max_size(image_path=image_path,output_path=image_path)
img =Image.open(image_path)
message = BaseMessage.make_user_message(
role_name="User",
content = '\nHere is the image of the generated project page.',
image_list=[img]
)
response = self.review_agent.step(message)
return get_json_from_response(response.msgs[0].content.strip())
def modify_html_table(self, html_content: str,html_dir: str):
in_tokens, out_tokens = 0, 0
print("Starting table modification...")
def replace_tables_in_html(html_content, table_html_map, paper_name):
pattern = rf']*src="(assets/{paper_name}-table-\d+\.png)"[^>]*>'
def repl(match):
img_path = match.group(1) # e.g. assets/MambaFusion-table-10.png
if img_path in table_html_map:
return table_html_map[img_path]
return match.group(0)
return re.sub(pattern, repl, html_content)
# ============ step 1 extract table ============
pattern = rf"assets/{self.args.paper_name}-table-\d+\.png"
with open(os.path.join(self.args.output_dir,self.args.paper_name, html_dir,'index_no_modify_table.html'), 'r', encoding='utf-8') as f:
html_content = f.read()
matches = re.findall(pattern, html_content)
if matches is None:
print("No table images found, skipping modification.")
return None, 0, 0
model_type = self.args.model_name_v
print(f"Starting table modification phase 1: Table Extraction with {model_type}...")
with open('utils/prompt_templates/page_templates/extract_table.yaml', 'r') as f:
table_extraction_config = yaml.safe_load(f)
content = table_extraction_config["system_prompt"]
init_message = BaseMessage.make_user_message(
role_name="User",
content=content
)
response = self.table_agent.step(init_message)
in_tok , out_tok = account_token(response)
in_tokens += in_tok
out_tokens += out_tok
# Step 2
table_html_map = {}
matches = list(set(matches))
for match in matches:
img_path =os.path.join(self.args.output_dir,self.args.paper_name, html_dir,match)
print(f"Processing table image: {img_path}")
img = Image.open(img_path)
msg = BaseMessage.make_user_message(
role_name="User",
content=f'''Here is table image: {match}
Please output its HTML table (