samu commited on
Commit
7c7ef49
·
1 Parent(s): c7f8ac1
.gitignore ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ .env
2
+ .env.*
3
+ __pycache__/
4
+ *.pyc
5
+ *.pyo
6
+ *.pyd
Dockerfile ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ # Set working directory
4
+ WORKDIR /app
5
+
6
+ # Install system dependencies
7
+ RUN apt-get update && \
8
+ apt-get install -y curl python3-pip
9
+
10
+ # Copy requirements first for better caching
11
+ COPY requirements.txt .
12
+ RUN pip install --no-cache-dir -r requirements.txt
13
+
14
+ # Copy application code
15
+ COPY backend ./backend
16
+ COPY frontend ./frontend
17
+
18
+ # Create startup script in a standard location
19
+ RUN echo '#!/bin/bash\n\
20
+ # Start backend server\n\
21
+ uvicorn backend.main:app --host 0.0.0.0 --port 8002 &\n\
22
+ \n\
23
+ # Start frontend server\n\
24
+ cd frontend && python -m http.server 8080 &\n\
25
+ \n\
26
+ # Wait for either process to exit\n\
27
+ wait' > /usr/local/bin/start.sh && \
28
+ chmod +x /usr/local/bin/start.sh
29
+
30
+ # Expose ports for both services
31
+ EXPOSE 8002 8080
32
+
33
+ # Start both servers
34
+ CMD ["/usr/local/bin/start.sh"]
backend/category_config.py ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Dict, TypedDict
2
+
3
+ class CategoryConfig(TypedDict):
4
+ name: str
5
+ description: str
6
+ style_guide: str
7
+ conventions: list[str]
8
+ common_elements: list[str]
9
+
10
+ CATEGORY_CONFIGS: Dict[str, CategoryConfig] = {
11
+ "mechanical": {
12
+ "name": "Mechanical Engineering",
13
+ "description": "Focuses on machine components, mechanisms, and mechanical systems design",
14
+ "style_guide": "Use isometric views for 3D components. Include detailed cross-sections for complex parts.",
15
+ "conventions": [
16
+ "Center lines for symmetric parts",
17
+ "Hidden lines for internal features",
18
+ "Section views for internal details",
19
+ "Dimensioning with tolerances"
20
+ ],
21
+ "common_elements": [
22
+ "Gears and transmission systems",
23
+ "Bearings and shafts",
24
+ "Fasteners and joints",
25
+ "Hydraulic/pneumatic components"
26
+ ]
27
+ },
28
+ "structural": {
29
+ "name": "Structural Engineering",
30
+ "description": "Focuses on building structures, load-bearing elements, and structural analysis",
31
+ "style_guide": "Use clear section markers. Include detailed connection points.",
32
+ "conventions": [
33
+ "Grid lines and axes",
34
+ "Member sizing annotations",
35
+ "Connection details",
36
+ "Load indicators"
37
+ ],
38
+ "common_elements": [
39
+ "Beams and columns",
40
+ "Foundation details",
41
+ "Structural connections",
42
+ "Reinforcement details"
43
+ ]
44
+ },
45
+ "civil": {
46
+ "name": "Civil Engineering",
47
+ "description": "Focuses on infrastructure, site plans, and civil structures",
48
+ "style_guide": "Use plan views with clear elevation markers. Include site context.",
49
+ "conventions": [
50
+ "Site orientation",
51
+ "Elevation markers",
52
+ "Drainage indicators",
53
+ "Material specifications"
54
+ ],
55
+ "common_elements": [
56
+ "Road sections",
57
+ "Drainage systems",
58
+ "Site grading",
59
+ "Utilities layout"
60
+ ]
61
+ },
62
+ "architectural": {
63
+ "name": "Architectural Engineering",
64
+ "description": "Focuses on building designs, spatial layouts, and architectural elements",
65
+ "style_guide": "Use multiple views (plan, elevation, section). Include material patterns.",
66
+ "conventions": [
67
+ "Room labels",
68
+ "Door/window schedules",
69
+ "Material hatching",
70
+ "Dimensional guidelines"
71
+ ],
72
+ "common_elements": [
73
+ "Floor plans",
74
+ "Elevations",
75
+ "Building sections",
76
+ "Detail drawings"
77
+ ]
78
+ },
79
+ "electrical": {
80
+ "name": "Electrical Engineering",
81
+ "description": "Focuses on electrical systems, circuits, and power distribution",
82
+ "style_guide": "Use standard electrical symbols. Include system block diagrams.",
83
+ "conventions": [
84
+ "Circuit symbols",
85
+ "Wire numbering",
86
+ "Component labels",
87
+ "Power ratings"
88
+ ],
89
+ "common_elements": [
90
+ "Circuit diagrams",
91
+ "Wiring layouts",
92
+ "Panel schedules",
93
+ "Single-line diagrams"
94
+ ]
95
+ },
96
+ "aerospace": {
97
+ "name": "Aerospace Engineering",
98
+ "description": "Focuses on aircraft, spacecraft, and aerospace systems",
99
+ "style_guide": "Use multiple projection views. Include aerodynamic profiles.",
100
+ "conventions": [
101
+ "Station numbering",
102
+ "Flow indicators",
103
+ "System interfaces",
104
+ "Zoning diagrams"
105
+ ],
106
+ "common_elements": [
107
+ "Airframe structures",
108
+ "Propulsion systems",
109
+ "Control surfaces",
110
+ "System integration"
111
+ ]
112
+ }
113
+ }
backend/category_instructions.py ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Dict, List
2
+
3
+ ALLOWED_CATEGORIES = [
4
+ "mechanical",
5
+ "structural",
6
+ "civil",
7
+ "architectural",
8
+ "electrical",
9
+ "aerospace"
10
+ ]
11
+
12
+ CATEGORY_INSTRUCTIONS: Dict[str, str] = {
13
+ "mechanical": """You are a mechanical engineering schematic generator specializing in machine components,
14
+ mechanisms, and mechanical systems. Generate precise technical drawings with detailed views of moving parts,
15
+ assembly sequences, and mechanical connections. For each schematic, provide:
16
+ 1. Materials List: Specify required components, materials, and standard parts with specifications
17
+ 2. Assembly Instructions: Step-by-step assembly procedure with critical tolerances and alignments
18
+ 3. Operating Parameters: Working conditions, load limits, and maintenance requirements
19
+ 4. Safety Considerations: Key safety measures and potential failure points to monitor""",
20
+
21
+ "structural": """You are a structural engineering schematic generator focusing on building structures,
22
+ load-bearing elements, and structural components. Generate detailed structural drawings emphasizing connections,
23
+ member details, and load paths. For each schematic, provide:
24
+ 1. Material Specifications: Required structural materials with grades and standards
25
+ 2. Load Calculations: Design loads, safety factors, and critical stress points
26
+ 3. Construction Sequence: Step-by-step construction methodology with critical checks
27
+ 4. Testing Protocol: Required structural tests and inspection points""",
28
+
29
+ "civil": """You are a civil engineering schematic generator specializing in infrastructure, site plans,
30
+ and civil structures. Generate detailed drawings showing construction details, earthworks, and systems.
31
+ For each schematic, provide:
32
+ 1. Site Requirements: Ground conditions, drainage needs, and utilities requirements
33
+ 2. Material Specifications: Required materials with grades and testing standards
34
+ 3. Construction Sequence: Detailed construction steps with quality control points
35
+ 4. Environmental Considerations: Impact mitigation and sustainability measures""",
36
+
37
+ "architectural": """You are an architectural schematic generator focusing on building designs, spatial layouts,
38
+ and architectural details. Generate drawings emphasizing building elements and spatial relationships.
39
+ For each schematic, provide:
40
+ 1. Space Program: Room dimensions, functional requirements, and circulation paths
41
+ 2. Material Schedule: Finishing materials, fixtures, and architectural elements
42
+ 3. Construction Details: Critical junctions, weatherproofing, and installation methods
43
+ 4. Code Compliance: Relevant building codes and accessibility requirements""",
44
+
45
+ "electrical": """You are an electrical engineering schematic generator specializing in electrical systems,
46
+ circuits, and components. Generate clear circuit diagrams and system layouts. For each schematic, provide:
47
+ 1. Component List: Required components with specifications and ratings
48
+ 2. Installation Guide: Step-by-step wiring and assembly instructions
49
+ 3. Testing Procedure: Circuit validation and troubleshooting steps
50
+ 4. Safety Protocols: Required safety measures and regulatory compliance notes""",
51
+
52
+ "aerospace": """You are an aerospace engineering schematic generator focusing on aircraft, spacecraft,
53
+ and aerospace systems. Generate detailed technical drawings of aerospace components and systems.
54
+ For each schematic, provide:
55
+ 1. Materials List: Aerospace-grade materials with specifications and certifications
56
+ 2. Assembly Sequence: Critical assembly steps with tolerance requirements
57
+ 3. Testing Requirements: Required validation tests and quality control measures
58
+ 4. Performance Parameters: Operating conditions, limits, and safety margins"""
59
+ }
60
+
61
+ DEFAULT_INSTRUCTION = """You are a versatile engineering schematic generator capable of creating technical drawings
62
+ across various engineering disciplines. Generate clear, precise technical representations following standard
63
+ engineering drawing conventions. For each schematic, provide:
64
+ 1. Overview: Brief description of the system or component purpose and function
65
+ 2. Materials and Components: Comprehensive list of required materials with specifications
66
+ 3. Implementation Guide: Step-by-step instructions for construction or assembly
67
+ 4. Testing and Validation: Required tests and quality control measures
68
+ 5. Safety Guidelines: Important safety considerations and regulatory requirements"""
69
+
70
+ def get_instruction_for_category(category: str | None) -> str:
71
+ """Get the system instruction for a given category."""
72
+ if category is None:
73
+ return DEFAULT_INSTRUCTION
74
+ category = category.lower()
75
+ if category not in ALLOWED_CATEGORIES:
76
+ return DEFAULT_INSTRUCTION
77
+ return CATEGORY_INSTRUCTIONS[category]
backend/config.py ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ SYSTEM_INSTRUCTION = """
2
+ You're a useful assistant
3
+ """
4
+
5
+ DEFAULT_TEXT = """
6
+ A blueprint schematic of a [subject], drawn in the style of early 20th-century industrial patents.
7
+ Rendered in crisp blue ink with white technical lines, featuring exploded views, angular labels, and stamped diagram codes.
8
+ """
backend/logging_utils.py ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from datetime import datetime
3
+ import json
4
+ from pathlib import Path
5
+ import os
6
+ from typing import Optional
7
+
8
+ # Configure logging
9
+ log_dir = Path(__file__).parent.parent / "logs"
10
+ log_dir.mkdir(exist_ok=True)
11
+
12
+ # Configure file handler for general logs
13
+ logging.basicConfig(
14
+ level=logging.INFO,
15
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
16
+ handlers=[
17
+ logging.FileHandler(log_dir / "app.log"),
18
+ logging.StreamHandler()
19
+ ]
20
+ )
21
+
22
+ # Create a specific logger for category usage
23
+ category_logger = logging.getLogger('category_usage')
24
+ category_logger.setLevel(logging.INFO)
25
+
26
+ # Stats file for category usage
27
+ STATS_FILE = log_dir / "category_stats.json"
28
+
29
+ def load_category_stats():
30
+ """Load existing category usage statistics."""
31
+ try:
32
+ if STATS_FILE.exists():
33
+ with open(STATS_FILE, 'r') as f:
34
+ return json.load(f)
35
+ except Exception as e:
36
+ logging.error(f"Error loading category stats: {e}")
37
+ return {}
38
+
39
+ def save_category_stats(stats):
40
+ """Save category usage statistics."""
41
+ try:
42
+ with open(STATS_FILE, 'w') as f:
43
+ json.dump(stats, f, indent=2)
44
+ except Exception as e:
45
+ logging.error(f"Error saving category stats: {e}")
46
+
47
+ def log_category_usage(category: Optional[str], endpoint: str, success: bool):
48
+ """Log category usage with detailed metrics."""
49
+ timestamp = datetime.now().isoformat()
50
+
51
+ # Load existing stats
52
+ stats = load_category_stats()
53
+
54
+ # Initialize category if not exists
55
+ category_key = category or "default"
56
+ if category_key not in stats:
57
+ stats[category_key] = {
58
+ "total_requests": 0,
59
+ "successful_requests": 0,
60
+ "failed_requests": 0,
61
+ "endpoints": {},
62
+ "last_used": None
63
+ }
64
+
65
+ # Update category stats
66
+ stats[category_key]["total_requests"] += 1
67
+ if success:
68
+ stats[category_key]["successful_requests"] += 1
69
+ else:
70
+ stats[category_key]["failed_requests"] += 1
71
+
72
+ # Update endpoint stats
73
+ if endpoint not in stats[category_key]["endpoints"]:
74
+ stats[category_key]["endpoints"][endpoint] = 0
75
+ stats[category_key]["endpoints"][endpoint] += 1
76
+
77
+ # Update timestamp
78
+ stats[category_key]["last_used"] = timestamp
79
+
80
+ # Save updated stats
81
+ save_category_stats(stats)
82
+
83
+ # Log the event
84
+ category_logger.info(
85
+ f"Category usage - Category: {category_key}, "
86
+ f"Endpoint: {endpoint}, "
87
+ f"Success: {success}"
88
+ )
89
+
90
+ def get_category_statistics():
91
+ """Get category usage statistics."""
92
+ return load_category_stats()
backend/main.py ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, HTTPException
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ from pydantic import BaseModel
4
+ from typing import List, Literal, Optional, Dict, Any, Union
5
+ from backend.utils import async_generate_text_and_image, async_generate_with_image_input
6
+ from backend.category_config import CATEGORY_CONFIGS
7
+ from backend.logging_utils import log_category_usage, get_category_statistics
8
+ import backend.config as config # keep for reference if needed
9
+ import traceback
10
+
11
+ app = FastAPI()
12
+ app.add_middleware(
13
+ CORSMiddleware,
14
+ allow_origins=["*"],
15
+ allow_methods=["*"],
16
+ allow_headers=["*"],
17
+ )
18
+
19
+ class TextGenerateRequest(BaseModel):
20
+ prompt: str
21
+ category: Optional[str] = None
22
+
23
+ class ImageTextGenerateRequest(BaseModel):
24
+ text: Optional[str] = None
25
+ image: str
26
+ category: Optional[str] = None
27
+
28
+ class Part(BaseModel):
29
+ type: Literal["text", "image"]
30
+ data: Union[str, Dict[str, str]] # Can be either a string (for image) or dict (for text)
31
+
32
+ class GenerationResponse(BaseModel):
33
+ results: List[Part]
34
+
35
+ @app.post("/generate", response_model=GenerationResponse)
36
+ async def generate(request: TextGenerateRequest):
37
+ """
38
+ Generate text and image from a text prompt with optional category.
39
+ """
40
+ success = False
41
+ try:
42
+ results = []
43
+ async for part in async_generate_text_and_image(request.prompt, request.category):
44
+ results.append(part)
45
+ success = True
46
+ return GenerationResponse(results=results)
47
+ except Exception as e:
48
+ traceback.print_exc()
49
+ raise HTTPException(status_code=500, detail=f"Internal error: {e}")
50
+ finally:
51
+ log_category_usage(request.category, "/generate", success)
52
+
53
+ @app.post("/generate_with_image", response_model=GenerationResponse)
54
+ async def generate_with_image(request: ImageTextGenerateRequest):
55
+ """
56
+ Generate text and image given a text and base64 image with optional category.
57
+ """
58
+ success = False
59
+ try:
60
+ results = []
61
+ text = request.text if request.text else config.DEFAULT_TEXT
62
+ async for part in async_generate_with_image_input(text, request.image, request.category):
63
+ results.append(part)
64
+ success = True
65
+ return GenerationResponse(results=results)
66
+ except Exception as e:
67
+ traceback.print_exc()
68
+ raise HTTPException(status_code=500, detail=f"Internal error: {e}")
69
+ finally:
70
+ log_category_usage(request.category, "/generate_with_image", success)
71
+
72
+ @app.get("/categories")
73
+ async def get_categories():
74
+ """
75
+ Get all available engineering categories with their descriptions and configurations.
76
+ """
77
+ return CATEGORY_CONFIGS
78
+
79
+ @app.get("/category-stats")
80
+ async def get_usage_statistics():
81
+ """
82
+ Get usage statistics for all categories.
83
+ """
84
+ return get_category_statistics()
85
+
86
+ @app.get("/")
87
+ async def read_root():
88
+ return {"message": "Image generation API is up"}
backend/utils.py ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from google import genai
2
+ from google.genai import types
3
+ from typing import Union, List, Generator, Dict, Optional
4
+ from PIL import Image
5
+ from io import BytesIO
6
+ import base64
7
+ import requests
8
+ import asyncio
9
+ import os
10
+ from dotenv import load_dotenv
11
+ from .category_instructions import get_instruction_for_category
12
+ from .category_config import CATEGORY_CONFIGS
13
+ load_dotenv()
14
+
15
+ client = genai.Client(
16
+ api_key=os.getenv("API_KEY")
17
+ )
18
+
19
+ def bytes_to_base64(data: bytes, with_prefix: bool = True) -> str:
20
+ encoded = base64.b64encode(data).decode("utf-8")
21
+ return f"data:image/png;base64,{encoded}" if with_prefix else encoded
22
+
23
+ def decode_base64_image(base64_str: str) -> Image.Image:
24
+ # Remove the prefix if present (e.g., "data:image/png;base64,")
25
+ if base64_str.startswith("data:image"):
26
+ base64_str = base64_str.split(",")[1]
27
+ image_data = base64.b64decode(base64_str)
28
+ image = Image.open(BytesIO(image_data))
29
+ return image
30
+
31
+ async def async_generate_text_and_image(prompt, category: Optional[str] = None):
32
+ # Get the appropriate instruction and configuration
33
+ instruction = get_instruction_for_category(category)
34
+ config = CATEGORY_CONFIGS.get(category.lower() if category else "", {})
35
+
36
+ # Enhance the prompt with category-specific guidance if available
37
+ if config:
38
+ style_guide = config.get("style_guide", "")
39
+ conventions = config.get("conventions", [])
40
+ common_elements = config.get("common_elements", [])
41
+
42
+ enhanced_prompt = (
43
+ f"{instruction}\n\n"
44
+ f"Style Guide: {style_guide}\n"
45
+ f"Drawing Conventions to Follow:\n- " + "\n- ".join(conventions) + "\n"
46
+ f"Consider Including These Elements:\n- " + "\n- ".join(common_elements) + "\n\n"
47
+ f"User Request: {prompt}"
48
+ )
49
+ else:
50
+ enhanced_prompt = f"{instruction}\n\nUser Request: {prompt}"
51
+
52
+ response = await client.aio.models.generate_content(
53
+ model=os.getenv("MODEL"),
54
+ contents=enhanced_prompt,
55
+ config=types.GenerateContentConfig(
56
+ response_modalities=['TEXT', 'IMAGE']
57
+ )
58
+ )
59
+ for part in response.candidates[0].content.parts:
60
+ if hasattr(part, 'text') and part.text is not None:
61
+ # Try to parse the text into sections
62
+ try:
63
+ text_sections = {}
64
+ current_section = "overview"
65
+ lines = part.text.split('\n')
66
+
67
+ for line in lines:
68
+ line = line.strip()
69
+ if not line:
70
+ continue
71
+
72
+ # Check for section headers
73
+ if any(line.lower().startswith(f"{i}.") for i in range(1, 6)):
74
+ section_name = line.split('.', 1)[1].split(':', 1)[0].strip().lower()
75
+ section_name = section_name.replace(' ', '_')
76
+ current_section = section_name
77
+ text_sections[current_section] = []
78
+ else:
79
+ if current_section not in text_sections:
80
+ text_sections[current_section] = []
81
+ text_sections[current_section].append(line)
82
+
83
+ # Clean up the sections
84
+ for section in text_sections:
85
+ text_sections[section] = '\n'.join(text_sections[section]).strip()
86
+
87
+ yield {'type': 'text', 'data': text_sections}
88
+ except Exception as e:
89
+ # Fallback to raw text if parsing fails
90
+ yield {'type': 'text', 'data': {'raw_text': part.text}}
91
+ elif hasattr(part, 'inline_data') and part.inline_data is not None:
92
+ yield {'type': 'image', 'data': bytes_to_base64(part.inline_data.data)}
93
+
94
+ async def async_generate_with_image_input(text: Optional[str], image_path: str, category: Optional[str] = None):
95
+ # Validate that the image input is a base64 data URI
96
+ if not isinstance(image_path, str) or not image_path.startswith("data:image/"):
97
+ raise ValueError("Invalid image input: expected a base64 Data URI starting with 'data:image/'")
98
+ # Decode the base64 string into a PIL Image
99
+ image = decode_base64_image(image_path)
100
+
101
+ # Get the appropriate instruction for the category
102
+ instruction = get_instruction_for_category(category)
103
+
104
+ contents = []
105
+ if text:
106
+ # Combine the instruction with the user's text input
107
+ combined_text = f"{instruction}\n\nUser Request: {text}"
108
+ contents.append(combined_text)
109
+ else:
110
+ contents.append(instruction)
111
+ contents.append(image)
112
+ response = await client.aio.models.generate_content(
113
+ model=os.getenv("MODEL"),
114
+ contents=contents,
115
+ config=types.GenerateContentConfig(
116
+ response_modalities=['TEXT', 'IMAGE']
117
+ )
118
+ )
119
+ for part in response.candidates[0].content.parts:
120
+ if hasattr(part, 'text') and part.text is not None:
121
+ # Try to parse the text into sections
122
+ try:
123
+ text_sections = {}
124
+ current_section = "overview"
125
+ lines = part.text.split('\n')
126
+
127
+ for line in lines:
128
+ line = line.strip()
129
+ if not line:
130
+ continue
131
+
132
+ # Check for section headers
133
+ if any(line.lower().startswith(f"{i}.") for i in range(1, 6)):
134
+ section_name = line.split('.', 1)[1].split(':', 1)[0].strip().lower()
135
+ section_name = section_name.replace(' ', '_')
136
+ current_section = section_name
137
+ text_sections[current_section] = []
138
+ else:
139
+ if current_section not in text_sections:
140
+ text_sections[current_section] = []
141
+ text_sections[current_section].append(line)
142
+
143
+ # Clean up the sections
144
+ for section in text_sections:
145
+ text_sections[section] = '\n'.join(text_sections[section]).strip()
146
+
147
+ yield {'type': 'text', 'data': text_sections}
148
+ except Exception as e:
149
+ # Fallback to raw text if parsing fails
150
+ yield {'type': 'text', 'data': {'raw_text': part.text}}
151
+ elif hasattr(part, 'inline_data') and part.inline_data is not None:
152
+ yield {'type': 'image', 'data': bytes_to_base64(part.inline_data.data)}
docker-compose.yml ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: '3.8'
2
+ services:
3
+ app:
4
+ build:
5
+ context: .
6
+ dockerfile: Dockerfile
7
+ ports:
8
+ - "8002:8002" # Backend API
9
+ - "8080:8080" # Frontend static server
10
+ environment:
11
+ - API_KEY=${API_KEY}
12
+ - MODEL=${MODEL}
13
+ volumes:
14
+ - .:/app
15
+ healthcheck:
16
+ test: ["CMD", "curl", "-f", "http://localhost:8002"]
17
+ interval: 30s
18
+ timeout: 10s
19
+ retries: 3
frontend/index.html ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Schematic Generation AI</title>
7
+ <link rel="stylesheet" href="style.css">
8
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap">
9
+ </head>
10
+ <body>
11
+ <div class="app-container">
12
+ <!-- Side Navigation -->
13
+ <nav class="side-nav">
14
+ <div class="nav-header">
15
+ <h2>Generation Mode</h2>
16
+ </div>
17
+ <div class="nav-items">
18
+ <button class="nav-item active" data-mode="text2img">
19
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
20
+ <path d="M12 5v14M5 12h14"/>
21
+ </svg>
22
+ Text to Image
23
+ </button>
24
+ <button class="nav-item" data-mode="img2img">
25
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
26
+ <rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
27
+ <path d="M3 15l6-6 9 9"/>
28
+ <path d="M13 9l3-3 5 5"/>
29
+ </svg>
30
+ Image with Text
31
+ </button>
32
+ </div>
33
+ </nav>
34
+
35
+ <!-- Main Content -->
36
+ <main class="main-content">
37
+ <div class="content-area">
38
+ <h1>Schematic Generation AI</h1>
39
+
40
+ <!-- Text to Image Interface -->
41
+ <section id="text2img" class="interface active">
42
+ <div class="form-group">
43
+ <div class="category-wrapper">
44
+ <select id="category" class="category-select">
45
+ <option value="">Select Engineering Category (Optional)</option>
46
+ </select>
47
+ <div id="categoryInfo" class="category-info"></div>
48
+ </div>
49
+ <textarea
50
+ id="prompt"
51
+ placeholder="Describe the schematic you want to generate..."
52
+ rows="4"
53
+ ></textarea>
54
+ <button id="generate">Generate Schematic</button>
55
+ </div>
56
+ <div id="results"></div>
57
+ </section>
58
+
59
+ <!-- Image with Text Interface -->
60
+ <section id="img2img" class="interface">
61
+ <div class="form-group">
62
+ <div class="file-input-wrapper">
63
+ <input type="file" id="inputImage" accept="image/*">
64
+ <label for="inputImage">
65
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
66
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
67
+ <polyline points="17 8 12 3 7 8"/>
68
+ <line x1="12" y1="3" x2="12" y2="15"/>
69
+ </svg>
70
+ <span>Choose an image or drag & drop here</span>
71
+ </label>
72
+ </div>
73
+ <div class="category-wrapper">
74
+ <select id="imageCategory" class="category-select">
75
+ <option value="">Select Engineering Category (Optional)</option>
76
+ </select>
77
+ <div id="imageCategoryInfo" class="category-info"></div>
78
+ </div>
79
+ <textarea
80
+ id="imagePrompt"
81
+ placeholder="Optional: Describe any modifications or specific requirements..."
82
+ rows="3"
83
+ ></textarea>
84
+ <button id="generateWithImage">Generate Modified Schematic</button>
85
+ </div>
86
+ <div id="resultsImg"></div>
87
+ </section>
88
+ </div>
89
+
90
+ <!-- Results Panel -->
91
+ <div id="resultsPanel" class="results-panel"></div>
92
+ </main>
93
+ </div>
94
+ <script src="script.js"></script>
95
+ </body>
96
+ </html>
frontend/script.js ADDED
@@ -0,0 +1,361 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Get the current hostname (will work both locally and in Docker)
2
+ const API_BASE = `${window.location.protocol}//${window.location.hostname}:8002`;
3
+
4
+ // Interface switching
5
+ function setupNavigation() {
6
+ const navItems = document.querySelectorAll('.nav-item');
7
+
8
+ navItems.forEach(item => {
9
+ item.addEventListener('click', () => {
10
+ const mode = item.dataset.mode;
11
+
12
+ // Update navigation state
13
+ navItems.forEach(nav => nav.classList.remove('active'));
14
+ item.classList.add('active');
15
+
16
+ // Update interface visibility
17
+ document.querySelectorAll('.interface').forEach(interface => {
18
+ interface.classList.remove('active');
19
+ });
20
+ document.getElementById(mode).classList.add('active');
21
+ });
22
+ });
23
+ }
24
+
25
+ // Helper functions
26
+ function setLoading(button, isLoading) {
27
+ if (isLoading) {
28
+ button.classList.add('loading');
29
+ button.disabled = true;
30
+ } else {
31
+ button.classList.remove('loading');
32
+ button.disabled = false;
33
+ }
34
+ }
35
+
36
+ function showError(container, message) {
37
+ const errorDiv = document.createElement('div');
38
+ errorDiv.className = 'error-message';
39
+ errorDiv.textContent = message || 'An error occurred. Please try again.';
40
+ container.innerHTML = '';
41
+ container.appendChild(errorDiv);
42
+ updateResultsPanel(null); // Clear the results panel
43
+ }
44
+
45
+ function updateResultsPanel(textData) {
46
+ const panel = document.getElementById('resultsPanel');
47
+ const mainContent = document.querySelector('.main-content');
48
+
49
+ if (!textData) {
50
+ panel.classList.remove('visible');
51
+ mainContent.classList.remove('with-panel');
52
+ return;
53
+ }
54
+
55
+ panel.innerHTML = ''; // Clear previous content
56
+
57
+ // Add title
58
+ const title = document.createElement('h2');
59
+ title.textContent = 'Generated Information';
60
+ title.style.marginBottom = '1.5rem';
61
+ panel.appendChild(title);
62
+
63
+ if (typeof textData === 'string') {
64
+ // Handle legacy string data
65
+ const p = document.createElement('p');
66
+ p.textContent = textData;
67
+ panel.appendChild(p);
68
+ } else {
69
+ // Handle structured data
70
+ Object.entries(textData).forEach(([section, content]) => {
71
+ const sectionDiv = document.createElement('div');
72
+ sectionDiv.className = 'text-section';
73
+
74
+ const title = document.createElement('h3');
75
+ title.className = 'section-title';
76
+ title.textContent = section.split('_').map(word =>
77
+ word.charAt(0).toUpperCase() + word.slice(1)
78
+ ).join(' ');
79
+
80
+ const content_p = document.createElement('p');
81
+ content_p.textContent = content;
82
+
83
+ sectionDiv.appendChild(title);
84
+ sectionDiv.appendChild(content_p);
85
+ panel.appendChild(sectionDiv);
86
+ });
87
+ }
88
+
89
+ panel.classList.add('visible');
90
+ mainContent.classList.add('with-panel');
91
+ }
92
+
93
+ function createResultElement(item) {
94
+ if (item.type === 'text') {
95
+ // Update the panel with text data
96
+ updateResultsPanel(item.data);
97
+ return null; // Don't create an element in the main content area
98
+ } else if (item.type === 'image') {
99
+ const wrapper = document.createElement('div');
100
+ wrapper.className = 'image-wrapper';
101
+
102
+ const img = document.createElement('img');
103
+ img.src = item.data;
104
+ img.addEventListener('load', () => wrapper.classList.add('loaded'));
105
+
106
+ wrapper.appendChild(img);
107
+ return wrapper;
108
+ }
109
+ return null;
110
+ }
111
+
112
+ // File input handling
113
+ function setupFileInput() {
114
+ const fileInput = document.getElementById('inputImage');
115
+ const dropZone = document.querySelector('.file-input-wrapper');
116
+
117
+ ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
118
+ dropZone.addEventListener(eventName, preventDefaults, false);
119
+ });
120
+
121
+ function preventDefaults(e) {
122
+ e.preventDefault();
123
+ e.stopPropagation();
124
+ }
125
+
126
+ ['dragenter', 'dragover'].forEach(eventName => {
127
+ dropZone.addEventListener(eventName, () => {
128
+ dropZone.classList.add('highlight');
129
+ });
130
+ });
131
+
132
+ ['dragleave', 'drop'].forEach(eventName => {
133
+ dropZone.addEventListener(eventName, () => {
134
+ dropZone.classList.remove('highlight');
135
+ });
136
+ });
137
+
138
+ dropZone.addEventListener('drop', (e) => {
139
+ const dt = e.dataTransfer;
140
+ const files = dt.files;
141
+ fileInput.files = files;
142
+ updateFileLabel(files[0]?.name);
143
+ });
144
+
145
+ fileInput.addEventListener('change', (e) => {
146
+ updateFileLabel(e.target.files[0]?.name);
147
+ });
148
+ }
149
+
150
+ function updateFileLabel(filename) {
151
+ const label = document.querySelector('label[for="inputImage"] span');
152
+ label.textContent = filename || 'Choose an image or drag & drop here';
153
+ }
154
+
155
+ // Text to Image Generation
156
+ document.getElementById('generate').addEventListener('click', async () => {
157
+ const prompt = document.getElementById('prompt').value.trim();
158
+ const category = document.getElementById('category').value;
159
+ const generateButton = document.getElementById('generate');
160
+ const resultsDiv = document.getElementById('results');
161
+
162
+ if (!prompt) {
163
+ showError(resultsDiv, 'Please enter a description for the schematic.');
164
+ return;
165
+ }
166
+
167
+ resultsDiv.innerHTML = '';
168
+ setLoading(generateButton, true);
169
+
170
+ try {
171
+ const res = await fetch(`${API_BASE}/generate`, {
172
+ method: 'POST',
173
+ headers: { 'Content-Type': 'application/json' },
174
+ body: JSON.stringify({ prompt, category })
175
+ });
176
+
177
+ if (!res.ok) {
178
+ throw new Error(`Server responded with ${res.status}`);
179
+ }
180
+
181
+ const data = await res.json();
182
+ const resultElements = data.results
183
+ .map(createResultElement)
184
+ .filter(element => element !== null);
185
+
186
+ if (resultElements.length === 0) {
187
+ showError(resultsDiv, 'No results generated. Please try again.');
188
+ return;
189
+ }
190
+
191
+ resultElements.forEach(element => resultsDiv.appendChild(element));
192
+
193
+ } catch (err) {
194
+ console.error(err);
195
+ showError(resultsDiv, 'Error generating schematic. Please try again.');
196
+ } finally {
197
+ setLoading(generateButton, false);
198
+ }
199
+ });
200
+
201
+ // Image with Text Generation
202
+ document.getElementById('generateWithImage').addEventListener('click', async () => {
203
+ const fileInput = document.getElementById('inputImage');
204
+ const prompt = document.getElementById('imagePrompt').value.trim();
205
+ const generateButton = document.getElementById('generateWithImage');
206
+ const resultsDiv = document.getElementById('resultsImg');
207
+
208
+ if (!fileInput.files.length) {
209
+ showError(resultsDiv, 'Please select an image file.');
210
+ return;
211
+ }
212
+
213
+ const file = fileInput.files[0];
214
+ if (!file.type.startsWith('image/')) {
215
+ showError(resultsDiv, 'Please select a valid image file.');
216
+ return;
217
+ }
218
+
219
+ resultsDiv.innerHTML = '';
220
+ setLoading(generateButton, true);
221
+
222
+ try {
223
+ const base64 = await new Promise((resolve, reject) => {
224
+ const reader = new FileReader();
225
+ reader.onload = e => resolve(e.target.result);
226
+ reader.onerror = reject;
227
+ reader.readAsDataURL(file);
228
+ });
229
+
230
+ const category = document.getElementById('imageCategory').value;
231
+
232
+ const res = await fetch(`${API_BASE}/generate_with_image`, {
233
+ method: 'POST',
234
+ headers: { 'Content-Type': 'application/json' },
235
+ body: JSON.stringify({
236
+ text: prompt || '', // Send empty string if no prompt provided
237
+ image: base64,
238
+ category
239
+ })
240
+ });
241
+
242
+ if (!res.ok) {
243
+ throw new Error(`Server responded with ${res.status}`);
244
+ }
245
+
246
+ const data = await res.json();
247
+ const resultElements = data.results
248
+ .map(createResultElement)
249
+ .filter(element => element !== null);
250
+
251
+ if (resultElements.length === 0) {
252
+ showError(resultsDiv, 'No results generated. Please try again.');
253
+ return;
254
+ }
255
+
256
+ resultElements.forEach(element => resultsDiv.appendChild(element));
257
+
258
+ } catch (err) {
259
+ console.error(err);
260
+ showError(resultsDiv, 'Error generating modified schematic. Please try again.');
261
+ } finally {
262
+ setLoading(generateButton, false);
263
+ }
264
+ });
265
+
266
+ // Category handling
267
+ async function fetchCategories() {
268
+ try {
269
+ const res = await fetch(`${API_BASE}/categories`);
270
+ if (!res.ok) throw new Error('Failed to fetch categories');
271
+ return await res.json();
272
+ } catch (error) {
273
+ console.error('Error fetching categories:', error);
274
+ return null;
275
+ }
276
+ }
277
+
278
+ function updateCategoryInfo(categoryData, infoDiv) {
279
+ if (!infoDiv) {
280
+ console.error('Category info div not found');
281
+ return;
282
+ }
283
+
284
+ if (!categoryData) {
285
+ infoDiv.innerHTML = '';
286
+ infoDiv.classList.remove('visible');
287
+ return;
288
+ }
289
+
290
+ const html = `
291
+ <h4>${categoryData.name}</h4>
292
+ <p>${categoryData.description}</p>
293
+ <p><strong>Style Guide:</strong> ${categoryData.style_guide}</p>
294
+
295
+ <h4>Drawing Conventions:</h4>
296
+ <div class="conventions-list">
297
+ ${categoryData.conventions.map(conv => `
298
+ <span class="convention-tag">${conv}</span>
299
+ `).join('')}
300
+ </div>
301
+
302
+ <h4>Common Elements:</h4>
303
+ <div class="elements-list">
304
+ ${categoryData.common_elements.map(elem => `
305
+ <span class="element-tag">${elem}</span>
306
+ `).join('')}
307
+ </div>
308
+ `;
309
+
310
+ infoDiv.innerHTML = html;
311
+ infoDiv.classList.add('visible');
312
+ }
313
+
314
+ async function setupCategories() {
315
+ try {
316
+ const categories = await fetchCategories();
317
+ if (!categories) return;
318
+
319
+ // Setup for both category selects
320
+ [
321
+ { selectId: 'category', infoId: 'categoryInfo' },
322
+ { selectId: 'imageCategory', infoId: 'imageCategoryInfo' }
323
+ ].forEach(({ selectId, infoId }) => {
324
+ const select = document.getElementById(selectId);
325
+ if (!select) {
326
+ console.error(`Select element with id ${selectId} not found`);
327
+ return;
328
+ }
329
+
330
+ // Clear existing options
331
+ select.innerHTML = '<option value="">Select Engineering Category (Optional)</option>';
332
+
333
+ // Add category options
334
+ Object.entries(categories).forEach(([key, data]) => {
335
+ const option = document.createElement('option');
336
+ option.value = key;
337
+ option.textContent = data.name;
338
+ select.appendChild(option);
339
+ });
340
+
341
+ // Add change event listener
342
+ select.addEventListener('change', (e) => {
343
+ const selectedCategory = e.target.value;
344
+ const infoElement = document.getElementById(infoId);
345
+
346
+ if (selectedCategory && categories[selectedCategory]) {
347
+ updateCategoryInfo(categories[selectedCategory], infoElement);
348
+ } else {
349
+ updateCategoryInfo(null, infoElement);
350
+ }
351
+ });
352
+ });
353
+ } catch (error) {
354
+ console.error('Error setting up categories:', error);
355
+ }
356
+ }
357
+
358
+ // Initialize functionality
359
+ setupNavigation();
360
+ setupFileInput();
361
+ setupCategories();
frontend/style.css ADDED
@@ -0,0 +1,545 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --primary-color: #6366f1;
3
+ --primary-hover: #4f46e5;
4
+ --bg-color: #f8fafc;
5
+ --card-bg: #ffffff;
6
+ --text-color: #1e293b;
7
+ --border-color: #e2e8f0;
8
+ --radius: 12px;
9
+ --divider-color: #e2e8f0;
10
+ --icon-color: #64748b;
11
+ --nav-width: 240px;
12
+ }
13
+
14
+ * {
15
+ margin: 0;
16
+ padding: 0;
17
+ box-sizing: border-box;
18
+ }
19
+
20
+ body {
21
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
22
+ background: var(--bg-color);
23
+ color: var(--text-color);
24
+ line-height: 1.5;
25
+ min-height: 100vh;
26
+ }
27
+
28
+ .app-container {
29
+ display: flex;
30
+ min-height: 100vh;
31
+ }
32
+
33
+ h1 {
34
+ text-align: center;
35
+ font-size: 2.5rem;
36
+ margin-bottom: 2rem;
37
+ background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
38
+ -webkit-background-clip: text;
39
+ -webkit-text-fill-color: transparent;
40
+ background-clip: text;
41
+ }
42
+
43
+ h2 {
44
+ text-align: center;
45
+ font-size: 1.5rem;
46
+ margin-bottom: 1.5rem;
47
+ color: var(--text-color);
48
+ }
49
+
50
+ section {
51
+ background: var(--card-bg);
52
+ border: 1px solid var(--border-color);
53
+ border-radius: var(--radius);
54
+ padding: 2rem;
55
+ margin-bottom: 2rem;
56
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);
57
+ transition: transform 0.2s ease;
58
+ }
59
+
60
+ section:hover {
61
+ transform: translateY(-2px);
62
+ }
63
+
64
+ textarea, input[type="text"], .category-select {
65
+ width: 100%;
66
+ padding: 1rem;
67
+ margin-bottom: 1rem;
68
+ border: 2px solid var(--border-color);
69
+ border-radius: var(--radius);
70
+ font-size: 1rem;
71
+ transition: border-color 0.2s ease;
72
+ background: var(--bg-color);
73
+ resize: vertical;
74
+ min-height: 100px;
75
+ }
76
+
77
+ .category-wrapper {
78
+ margin-bottom: 1.5rem;
79
+ }
80
+
81
+ .category-select {
82
+ min-height: auto;
83
+ cursor: pointer;
84
+ appearance: none;
85
+ background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
86
+ background-repeat: no-repeat;
87
+ background-position: right 1rem center;
88
+ background-size: 1em;
89
+ padding-right: 2.5rem;
90
+ }
91
+
92
+ .category-select:focus {
93
+ outline: none;
94
+ border-color: var(--primary-color);
95
+ }
96
+
97
+ .category-info {
98
+ margin-top: 0.5rem;
99
+ padding: 1rem;
100
+ background: var(--bg-color);
101
+ border-radius: var(--radius);
102
+ font-size: 0.9rem;
103
+ color: var(--text-color);
104
+ opacity: 0;
105
+ transform: translateY(-10px);
106
+ transition: all 0.3s ease;
107
+ }
108
+
109
+ .category-info.visible {
110
+ opacity: 1;
111
+ transform: translateY(0);
112
+ }
113
+
114
+ .category-info h4 {
115
+ margin: 0 0 0.5rem;
116
+ color: var(--primary-color);
117
+ }
118
+
119
+ .category-info ul {
120
+ margin: 0.5rem 0;
121
+ padding-left: 1.5rem;
122
+ }
123
+
124
+ .category-info li {
125
+ margin-bottom: 0.25rem;
126
+ }
127
+
128
+ .conventions-list, .elements-list {
129
+ display: flex;
130
+ flex-wrap: wrap;
131
+ gap: 0.5rem;
132
+ margin-top: 0.5rem;
133
+ }
134
+
135
+ .convention-tag, .element-tag {
136
+ background: var(--primary-color);
137
+ color: white;
138
+ padding: 0.25rem 0.75rem;
139
+ border-radius: 1rem;
140
+ font-size: 0.8rem;
141
+ }
142
+
143
+ input[type="text"] {
144
+ min-height: auto;
145
+ }
146
+
147
+ textarea:focus, input[type="text"]:focus {
148
+ outline: none;
149
+ border-color: var(--primary-color);
150
+ }
151
+
152
+ input[type="file"] {
153
+ width: 100%;
154
+ padding: 0.5rem;
155
+ margin-bottom: 1rem;
156
+ cursor: pointer;
157
+ }
158
+
159
+ button {
160
+ width: 100%;
161
+ padding: 1rem;
162
+ background: var(--primary-color);
163
+ color: white;
164
+ border: none;
165
+ border-radius: var(--radius);
166
+ cursor: pointer;
167
+ font-size: 1rem;
168
+ font-weight: 600;
169
+ transition: all 0.2s ease;
170
+ margin-top: 1rem;
171
+ }
172
+
173
+ button:hover {
174
+ background: var(--primary-hover);
175
+ transform: translateY(-1px);
176
+ }
177
+
178
+ button:active {
179
+ transform: translateY(0);
180
+ }
181
+
182
+ button.loading {
183
+ opacity: 0.7;
184
+ cursor: not-allowed;
185
+ position: relative;
186
+ }
187
+
188
+ button.loading::after {
189
+ content: "";
190
+ position: absolute;
191
+ width: 20px;
192
+ height: 20px;
193
+ top: 50%;
194
+ left: 50%;
195
+ margin: -10px 0 0 -10px;
196
+ border: 3px solid rgba(255, 255, 255, 0.3);
197
+ border-top-color: white;
198
+ border-radius: 50%;
199
+ animation: spin 1s linear infinite;
200
+ }
201
+
202
+ .input-section {
203
+ position: relative;
204
+ margin: 2rem 0;
205
+ }
206
+
207
+ .divider {
208
+ position: relative;
209
+ text-align: center;
210
+ margin: 1.5rem 0;
211
+ }
212
+
213
+ .divider::before {
214
+ content: "";
215
+ position: absolute;
216
+ top: 50%;
217
+ left: 0;
218
+ right: 0;
219
+ height: 1px;
220
+ background: var(--divider-color);
221
+ }
222
+
223
+ .divider span {
224
+ position: relative;
225
+ background: var(--card-bg);
226
+ padding: 0 1rem;
227
+ color: var(--icon-color);
228
+ font-size: 0.9rem;
229
+ }
230
+
231
+ .file-input-wrapper {
232
+ position: relative;
233
+ width: 100%;
234
+ min-height: 150px;
235
+ border: 2px dashed var(--border-color);
236
+ border-radius: var(--radius);
237
+ display: flex;
238
+ align-items: center;
239
+ justify-content: center;
240
+ transition: all 0.2s ease;
241
+ cursor: pointer;
242
+ background: var(--bg-color);
243
+ }
244
+
245
+ .file-input-wrapper:hover,
246
+ .file-input-wrapper.highlight {
247
+ border-color: var(--primary-color);
248
+ background: rgba(99, 102, 241, 0.05);
249
+ }
250
+
251
+ .file-input-wrapper input[type="file"] {
252
+ position: absolute;
253
+ width: 100%;
254
+ height: 100%;
255
+ opacity: 0;
256
+ cursor: pointer;
257
+ }
258
+
259
+ .file-input-wrapper label {
260
+ display: flex;
261
+ flex-direction: column;
262
+ align-items: center;
263
+ gap: 1rem;
264
+ color: var(--icon-color);
265
+ font-size: 0.95rem;
266
+ text-align: center;
267
+ padding: 2rem;
268
+ pointer-events: none;
269
+ }
270
+
271
+ .file-input-wrapper svg {
272
+ stroke: var(--icon-color);
273
+ transition: stroke 0.2s ease;
274
+ }
275
+
276
+ .file-input-wrapper:hover svg {
277
+ stroke: var(--primary-color);
278
+ }
279
+
280
+ textarea {
281
+ min-height: 120px;
282
+ line-height: 1.6;
283
+ }
284
+
285
+ .error-message {
286
+ color: #dc2626;
287
+ background: #fef2f2;
288
+ padding: 1rem;
289
+ border-radius: var(--radius);
290
+ margin-bottom: 1rem;
291
+ border: 1px solid #fecaca;
292
+ }
293
+
294
+ .result-text {
295
+ background: var(--bg-color);
296
+ padding: 1rem;
297
+ border-radius: var(--radius);
298
+ margin-bottom: 1rem;
299
+ }
300
+
301
+ .text-section {
302
+ margin-bottom: 1.5rem;
303
+ padding: 1rem;
304
+ background: var(--card-bg);
305
+ border: 1px solid var(--border-color);
306
+ border-radius: var(--radius);
307
+ }
308
+
309
+ .text-section:last-child {
310
+ margin-bottom: 0;
311
+ }
312
+
313
+ .section-title {
314
+ color: var(--primary-color);
315
+ font-size: 1.1rem;
316
+ margin: 0 0 0.5rem 0;
317
+ font-weight: 600;
318
+ }
319
+
320
+ .text-section p {
321
+ margin: 0;
322
+ line-height: 1.6;
323
+ white-space: pre-wrap;
324
+ }
325
+
326
+ .image-wrapper {
327
+ position: relative;
328
+ opacity: 0;
329
+ transform: translateY(10px);
330
+ transition: all 0.3s ease;
331
+ }
332
+
333
+ .image-wrapper.loaded {
334
+ opacity: 1;
335
+ transform: translateY(0);
336
+ }
337
+
338
+ @keyframes spin {
339
+ to { transform: rotate(360deg); }
340
+ }
341
+
342
+ #results, #resultsImg {
343
+ margin-top: 1.5rem;
344
+ display: grid;
345
+ gap: 1rem;
346
+ }
347
+
348
+ #results img, #resultsImg img {
349
+ width: 100%;
350
+ height: auto;
351
+ border-radius: var(--radius);
352
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
353
+ transition: transform 0.2s ease;
354
+ }
355
+
356
+ #results img:hover, #resultsImg img:hover {
357
+ transform: scale(1.02);
358
+ }
359
+
360
+ .form-group {
361
+ display: flex;
362
+ flex-direction: column;
363
+ gap: 1rem;
364
+ }
365
+
366
+ .side-nav {
367
+ width: var(--nav-width);
368
+ background: var(--card-bg);
369
+ border-right: 1px solid var(--border-color);
370
+ padding: 2rem 1rem;
371
+ flex-shrink: 0;
372
+ }
373
+
374
+ .nav-header {
375
+ padding: 0 1rem 1rem;
376
+ border-bottom: 1px solid var(--border-color);
377
+ }
378
+
379
+ .nav-header h2 {
380
+ font-size: 1.1rem;
381
+ margin: 0;
382
+ text-align: left;
383
+ color: var(--text-color);
384
+ }
385
+
386
+ .nav-items {
387
+ padding-top: 1rem;
388
+ }
389
+
390
+ .nav-item {
391
+ display: flex;
392
+ align-items: center;
393
+ gap: 0.75rem;
394
+ width: 100%;
395
+ padding: 0.75rem 1rem;
396
+ background: transparent;
397
+ border: none;
398
+ border-radius: var(--radius);
399
+ color: var(--text-color);
400
+ font-size: 0.95rem;
401
+ font-weight: 500;
402
+ cursor: pointer;
403
+ transition: all 0.2s ease;
404
+ }
405
+
406
+ .nav-item svg {
407
+ stroke: var(--icon-color);
408
+ transition: stroke 0.2s ease;
409
+ }
410
+
411
+ .nav-item:hover {
412
+ background: var(--bg-color);
413
+ transform: translateX(4px);
414
+ }
415
+
416
+ .nav-item.active {
417
+ background: var(--primary-color);
418
+ color: white;
419
+ }
420
+
421
+ .nav-item.active svg {
422
+ stroke: white;
423
+ }
424
+
425
+ .main-content {
426
+ flex-grow: 1;
427
+ position: relative;
428
+ display: flex;
429
+ }
430
+
431
+ .content-area {
432
+ flex-grow: 1;
433
+ padding: 2rem;
434
+ max-width: calc(100% - var(--nav-width));
435
+ transition: max-width 0.3s ease;
436
+ }
437
+
438
+ .results-panel {
439
+ position: fixed;
440
+ right: 0;
441
+ top: 0;
442
+ bottom: 0;
443
+ width: 400px;
444
+ background: var(--card-bg);
445
+ border-left: 1px solid var(--border-color);
446
+ padding: 2rem;
447
+ overflow-y: auto;
448
+ transform: translateX(100%);
449
+ transition: transform 0.3s ease;
450
+ box-shadow: -4px 0 16px rgba(0, 0, 0, 0.1);
451
+ }
452
+
453
+ .results-panel.visible {
454
+ transform: translateX(0);
455
+ }
456
+
457
+ .main-content.with-panel .content-area {
458
+ max-width: calc(100% - var(--nav-width) - 400px);
459
+ }
460
+
461
+ .interface {
462
+ display: none;
463
+ }
464
+
465
+ .interface.active {
466
+ display: block;
467
+ }
468
+
469
+ @media (max-width: 600px) {
470
+ body {
471
+ padding: 1rem;
472
+ }
473
+
474
+ section {
475
+ padding: 1.5rem;
476
+ }
477
+
478
+ h1 {
479
+ font-size: 2rem;
480
+ }
481
+
482
+ .file-input-wrapper {
483
+ min-height: 100px;
484
+ }
485
+ }
486
+
487
+ @media (max-width: 1200px) {
488
+ .results-panel {
489
+ width: 350px;
490
+ }
491
+
492
+ .main-content.with-panel {
493
+ max-width: calc(100% - var(--nav-width) - 350px);
494
+ }
495
+ }
496
+
497
+ @media (max-width: 768px) {
498
+ .app-container {
499
+ flex-direction: column;
500
+ }
501
+
502
+ .side-nav {
503
+ width: 100%;
504
+ border-right: none;
505
+ border-bottom: 1px solid var(--border-color);
506
+ padding: 1rem;
507
+ }
508
+
509
+ .nav-items {
510
+ display: flex;
511
+ gap: 1rem;
512
+ }
513
+
514
+ .nav-item {
515
+ flex: 1;
516
+ justify-content: center;
517
+ }
518
+
519
+ .nav-item svg {
520
+ display: none;
521
+ }
522
+
523
+ .main-content {
524
+ max-width: 100%;
525
+ padding: 1rem;
526
+ }
527
+
528
+ .results-panel {
529
+ position: fixed;
530
+ width: 100%;
531
+ height: 50%;
532
+ top: auto;
533
+ bottom: 0;
534
+ transform: translateY(100%);
535
+ }
536
+
537
+ .results-panel.visible {
538
+ transform: translateY(0);
539
+ }
540
+
541
+ .main-content.with-panel .content-area {
542
+ max-width: 100%;
543
+ padding-bottom: calc(50vh + 2rem);
544
+ }
545
+ }
logs/app.log ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 2025-05-28 07:25:24,649 - root - INFO - AFC is enabled with max remote calls: 10.
2
+ 2025-05-28 07:25:31,893 - category_usage - INFO - Category usage - Category: architectural, Endpoint: /generate, Success: True
3
+ 2025-05-28 07:25:42,787 - root - INFO - AFC is enabled with max remote calls: 10.
4
+ 2025-05-28 07:25:48,667 - category_usage - INFO - Category usage - Category: aerospace, Endpoint: /generate, Success: True
5
+ 2025-05-28 07:30:42,593 - root - INFO - AFC is enabled with max remote calls: 10.
6
+ 2025-05-28 07:30:49,823 - category_usage - INFO - Category usage - Category: default, Endpoint: /generate, Success: True
7
+ 2025-05-28 07:31:01,466 - root - INFO - AFC is enabled with max remote calls: 10.
8
+ 2025-05-28 07:31:06,847 - category_usage - INFO - Category usage - Category: aerospace, Endpoint: /generate, Success: True
9
+ 2025-05-28 07:36:55,689 - root - INFO - AFC is enabled with max remote calls: 10.
10
+ 2025-05-28 07:37:03,976 - category_usage - INFO - Category usage - Category: mechanical, Endpoint: /generate, Success: True
11
+ 2025-05-28 07:45:33,489 - root - INFO - AFC is enabled with max remote calls: 10.
12
+ 2025-05-28 07:45:39,003 - category_usage - INFO - Category usage - Category: aerospace, Endpoint: /generate, Success: True
13
+ 2025-05-28 08:02:06,281 - root - INFO - AFC is enabled with max remote calls: 10.
14
+ 2025-05-28 08:02:16,462 - category_usage - INFO - Category usage - Category: aerospace, Endpoint: /generate, Success: True
15
+ 2025-05-28 08:05:30,940 - root - INFO - AFC is enabled with max remote calls: 10.
16
+ 2025-05-28 08:05:40,735 - category_usage - INFO - Category usage - Category: architectural, Endpoint: /generate_with_image, Success: True
17
+ 2025-05-28 08:06:37,294 - root - INFO - AFC is enabled with max remote calls: 10.
18
+ 2025-05-28 08:06:44,399 - category_usage - INFO - Category usage - Category: architectural, Endpoint: /generate_with_image, Success: True
19
+ 2025-05-28 08:07:11,815 - root - INFO - AFC is enabled with max remote calls: 10.
20
+ 2025-05-28 08:07:20,047 - category_usage - INFO - Category usage - Category: architectural, Endpoint: /generate_with_image, Success: True
21
+ 2025-05-28 08:11:49,088 - root - INFO - AFC is enabled with max remote calls: 10.
22
+ 2025-05-28 08:11:56,770 - category_usage - INFO - Category usage - Category: default, Endpoint: /generate_with_image, Success: True
23
+ 2025-05-28 08:12:18,972 - root - INFO - AFC is enabled with max remote calls: 10.
24
+ 2025-05-28 08:12:29,866 - category_usage - INFO - Category usage - Category: architectural, Endpoint: /generate_with_image, Success: True
25
+ 2025-05-28 08:17:01,735 - root - INFO - AFC is enabled with max remote calls: 10.
26
+ 2025-05-28 08:17:07,524 - category_usage - INFO - Category usage - Category: aerospace, Endpoint: /generate, Success: True
27
+ 2025-05-28 08:40:29,100 - root - INFO - AFC is enabled with max remote calls: 10.
28
+ 2025-05-28 08:40:35,726 - category_usage - INFO - Category usage - Category: aerospace, Endpoint: /generate, Success: True
logs/category_stats.json ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectural": {
3
+ "total_requests": 5,
4
+ "successful_requests": 5,
5
+ "failed_requests": 0,
6
+ "endpoints": {
7
+ "/generate": 1,
8
+ "/generate_with_image": 4
9
+ },
10
+ "last_used": "2025-05-28T08:12:29.863971"
11
+ },
12
+ "aerospace": {
13
+ "total_requests": 6,
14
+ "successful_requests": 6,
15
+ "failed_requests": 0,
16
+ "endpoints": {
17
+ "/generate": 6
18
+ },
19
+ "last_used": "2025-05-28T08:40:35.722978"
20
+ },
21
+ "default": {
22
+ "total_requests": 2,
23
+ "successful_requests": 2,
24
+ "failed_requests": 0,
25
+ "endpoints": {
26
+ "/generate": 1,
27
+ "/generate_with_image": 1
28
+ },
29
+ "last_used": "2025-05-28T08:11:56.766848"
30
+ },
31
+ "mechanical": {
32
+ "total_requests": 1,
33
+ "successful_requests": 1,
34
+ "failed_requests": 0,
35
+ "endpoints": {
36
+ "/generate": 1
37
+ },
38
+ "last_used": "2025-05-28T07:37:03.972773"
39
+ }
40
+ }
plan.md ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Backend Development Plan
2
+
3
+ ## 1. Category-Based System Instructions
4
+
5
+ - **Goal:** Support system instructions tailored to specific engineering categories (e.g., mechanical, structural, civil, architectural, electrical, aerospace).
6
+ - **Approach:**
7
+ - Define a set of system instructions for each category.
8
+ - **Allowed Categories:**
9
+ - mechanical
10
+ - structural
11
+ - civil
12
+ - architectural
13
+ - electrical
14
+ - aerospace
15
+ - Store these instructions in a configuration file (e.g., JSON or YAML) or a database.
16
+ - Update API endpoints to accept a `category` parameter.
17
+ - Use the selected category in the generation logic to apply corresponding instructions.
18
+ - Provide a fallback to default instructions if the category isn't recognized.
19
+
20
+ ## 2. API Changes
21
+
22
+ - **Add `category` field** to relevant request models (e.g., `TextGenerateRequest`, `ImageTextGenerateRequest`).
23
+ - **Validate the category** against the allowed values.
24
+ - **Pass category** to generation utilities to select appropriate instructions.
25
+ - **Logging and Error Handling:**
26
+ - Log usage statistics for different categories.
27
+ - Return clear error messages if an invalid category is provided.
28
+
29
+ ## 3. Future Ideas and Enhancements
30
+
31
+ - Allow users to define custom categories or instructions.
32
+ - Add endpoints to list available categories along with their descriptions.
33
+ - Implement advanced logging and analytics to observe category usage.
34
+ - Support multi-category or hybrid instructions.
35
+ - **Extend the configuration:**
36
+ - Provide detailed parameters per category (e.g., style, tone, structural guidelines).
37
+ - Create a configuration file to manage these parameters centrally.
38
+ - Explore caching mechanisms for repeated generation requests per category.
39
+
40
+ ---
41
+
42
+ ## Next Steps
43
+
44
+ 1. Define allowed categories and specify instructions per category.
45
+ 2. Refactor request models and endpoints to include a `category` parameter.
46
+ 3. Update generation logic to utilize category-specific instructions.
47
+ 4. Develop a configuration file for system instructions per category.
48
+ 5. Enhance error handling and logging for better maintainability.
requirements.txt ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ google-genai
2
+ aioredis
3
+ redis==5.2.1
4
+ python-docx
5
+ fastapi==0.115.11
6
+ uvicorn==0.34.0
7
+ pydantic==2.10.6
8
+ aiocache
9
+ openai==1.67.0
10
+ httpx
11
+ python-dotenv
12
+ psycopg2-binary==2.9.9
13
+ python-jose[cryptography]
14
+ passlib[bcrypt]
15
+ python-multipart
16
+ python-jose[cryptography]
17
+ passlib[bcrypt]
18
+ python-multipart
19
+ pytest==7.4.4
20
+ pytest-asyncio==0.23.3
21
+ httpx==0.26.0 # Required for TestClient
22
+ pillow