EL GHAFRAOUI AYOUB commited on
Commit
6f14d8b
Β·
1 Parent(s): d2aae1f
Files changed (45) hide show
  1. .env +3 -2
  2. .gitattributes +0 -35
  3. .gitignore +0 -1
  4. tests/__init__.py β†’ 0.26.0 +0 -0
  5. tests/test_api.py β†’ 0.26.0' +0 -0
  6. alembic/__pycache__/env.cpython-312.pyc +0 -0
  7. alembic/env.py +0 -66
  8. alembic/script.py.mako +0 -26
  9. app.log +22 -0
  10. app/.env +3 -0
  11. app/__init__.py +1 -0
  12. app/__pycache__/__init__.cpython-312.pyc +0 -0
  13. app/__pycache__/main.cpython-312.pyc +0 -0
  14. tests/test_services.py β†’ app/controllers/__init__.py +0 -0
  15. app/controllers/__pycache__/__init__.cpython-312.pyc +0 -0
  16. app/controllers/__pycache__/f5_model.cpython-312.pyc +0 -0
  17. app/controllers/__pycache__/plan_chat_controller.cpython-312.pyc +0 -0
  18. app/controllers/__pycache__/scraper_controller.cpython-312.pyc +0 -0
  19. app/controllers/f5_model.py +86 -0
  20. app/controllers/plan_chat_controller.py +64 -0
  21. app/controllers/scraper_controller.py +45 -0
  22. app/helpers/__init__.py +0 -0
  23. app/helpers/__pycache__/__init__.cpython-312.pyc +0 -0
  24. app/helpers/__pycache__/plan_chat.cpython-312.pyc +0 -0
  25. app/helpers/__pycache__/plan_parser.cpython-312.pyc +0 -0
  26. app/helpers/chat.py +38 -0
  27. app/helpers/generate_features.py +32 -0
  28. app/helpers/generate_plan.py +256 -0
  29. app/helpers/generate_soluction_stack.py +39 -0
  30. app/helpers/plan_chat.py +83 -0
  31. app/helpers/plan_parser.py +91 -0
  32. app/main.py +280 -84
  33. app/models/__init__.py +0 -0
  34. app/models/project_plan.py +24 -0
  35. app/models/scrape_log.py +0 -0
  36. app/services/__init__.py +0 -0
  37. app/services/__pycache__/__init__.cpython-312.pyc +0 -0
  38. app/services/__pycache__/flan_t5_service.cpython-312.pyc +0 -0
  39. app/services/__pycache__/scraper_service.cpython-312.pyc +0 -0
  40. app/services/flan_t5_service.py +18 -0
  41. app/services/scraper_service.py +45 -0
  42. app/static/js/main.js +45 -0
  43. app/templates/index.html +302 -1337
  44. migrations/versions/xxxx_add_invoice_sequence.py +0 -28
  45. re.md +27 -0
.env CHANGED
@@ -1,2 +1,3 @@
1
- DATABASE_URL=sqlite+aiosqlite:///./app.db
2
- ENVIRONMENT=production
 
 
1
+ AWS_ACCESS_KEY_ID="AKIAVRUVQMZRVWTZE4N4"
2
+ AWS_SECRET_ACCESS_KEY="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"
3
+ AWS_DEFAULT_REGION="eu-west-3"
.gitattributes DELETED
@@ -1,35 +0,0 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.gitignore DELETED
@@ -1 +0,0 @@
1
- sql_app.db
 
 
tests/__init__.py β†’ 0.26.0 RENAMED
File without changes
tests/test_api.py β†’ 0.26.0' RENAMED
File without changes
alembic/__pycache__/env.cpython-312.pyc DELETED
Binary file (3.1 kB)
 
alembic/env.py DELETED
@@ -1,66 +0,0 @@
1
- import asyncio
2
- from logging.config import fileConfig
3
-
4
- from sqlalchemy import pool
5
- from sqlalchemy.engine import Connection
6
- from sqlalchemy.ext.asyncio import async_engine_from_config
7
-
8
- from alembic import context
9
-
10
- # this is the Alembic Config object, which provides
11
- # access to the values within the .ini file in use.
12
- config = context.config
13
-
14
- # Interpret the config file for Python logging.
15
- # This line sets up loggers basically.
16
- if config.config_file_name is not None:
17
- fileConfig(config.config_file_name)
18
-
19
- # add your model's MetaData object here
20
- # for 'autogenerate' support
21
- from app.db.models import Base
22
- target_metadata = Base.metadata
23
-
24
- def run_migrations_offline() -> None:
25
- """Run migrations in 'offline' mode."""
26
- url = config.get_main_option("sqlalchemy.url")
27
- context.configure(
28
- url=url,
29
- target_metadata=target_metadata,
30
- literal_binds=True,
31
- dialect_opts={"paramstyle": "named"},
32
- )
33
-
34
- with context.begin_transaction():
35
- context.run_migrations()
36
-
37
- def do_run_migrations(connection: Connection) -> None:
38
- context.configure(connection=connection, target_metadata=target_metadata)
39
-
40
- with context.begin_transaction():
41
- context.run_migrations()
42
-
43
- async def run_async_migrations() -> None:
44
- """In this scenario we need to create an Engine
45
- and associate a connection with the context."""
46
-
47
- connectable = async_engine_from_config(
48
- config.get_section(config.config_ini_section, {}),
49
- prefix="sqlalchemy.",
50
- poolclass=pool.NullPool,
51
- )
52
-
53
- async with connectable.connect() as connection:
54
- await connection.run_sync(do_run_migrations)
55
-
56
- await connectable.dispose()
57
-
58
- def run_migrations_online() -> None:
59
- """Run migrations in 'online' mode."""
60
-
61
- asyncio.run(run_async_migrations())
62
-
63
- if context.is_offline_mode():
64
- run_migrations_offline()
65
- else:
66
- run_migrations_online()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
alembic/script.py.mako DELETED
@@ -1,26 +0,0 @@
1
- """${message}
2
-
3
- Revision ID: ${up_revision}
4
- Revises: ${down_revision | comma,n}
5
- Create Date: ${create_date}
6
-
7
- """
8
- from typing import Sequence, Union
9
-
10
- from alembic import op
11
- import sqlalchemy as sa
12
- ${imports if imports else ""}
13
-
14
- # revision identifiers, used by Alembic.
15
- revision: str = ${repr(up_revision)}
16
- down_revision: Union[str, None] = ${repr(down_revision)}
17
- branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
18
- depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
19
-
20
-
21
- def upgrade() -> None:
22
- ${upgrades if upgrades else "pass"}
23
-
24
-
25
- def downgrade() -> None:
26
- ${downgrades if downgrades else "pass"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app.log ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 2025-02-28 18:27:16,754 - INFO - Chat request received with 1 messages
2
+ 2025-02-28 18:27:16,755 - INFO - Formatted prompt: give me saas app
3
+
4
+ 2025-02-28 18:27:29,908 - INFO - Generated response: i want to download saas app
5
+ 2025-02-28 18:28:08,376 - INFO - Feature generation request received with requirements: gerneate me 10 feature to use in saas
6
+ 2025-02-28 18:28:08,377 - INFO - Generated prompt: Generate 5 features based on the following requirements. Format each feature as a JSON object with 'feature' and 'short_description' fields.
7
+
8
+ Requirements: gerneate me 10 feature to use in saas
9
+ 2025-02-28 18:29:29,392 - INFO - Model response: i would like 10 feature to use in saas
10
+ 2025-02-28 18:29:29,392 - INFO - Returning features: [Feature(feature='Feature 1', short_description='Description 1'), Feature(feature='Feature 2', short_description='Description 2')]
11
+ 2025-02-28 18:31:00,786 - INFO - Feature generation request received with requirements: gerneate me 10 feature to use in saas
12
+ 2025-02-28 18:31:00,787 - INFO - Generated prompt: Generate 5 detailed SaaS features based on the following requirements. Each feature should be practical and implementation-ready. Format your response as a list of JSON objects, each with 'feature' and 'short_description' fields.
13
+
14
+ Example format:
15
+ {
16
+ 'feature': 'User Authentication',
17
+ 'short_description': 'Secure login system with OAuth2 and MFA support'
18
+ }
19
+
20
+ Requirements: gerneate me 10 feature to use in saas
21
+
22
+ Provide 5 features in the exact JSON format shown above.
app/.env ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ AWS_ACCESS_KEY_ID="AKIAVRUVQMZRVWTZE4N4"
2
+ AWS_SECRET_ACCESS_KEY="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"
3
+ AWS_DEFAULT_REGION="eu-west-3"
app/__init__.py CHANGED
@@ -0,0 +1 @@
 
 
1
+ #uvicorn app.main:app --host 0.0.0.0 --port 8000
app/__pycache__/__init__.cpython-312.pyc CHANGED
Binary files a/app/__pycache__/__init__.cpython-312.pyc and b/app/__pycache__/__init__.cpython-312.pyc differ
 
app/__pycache__/main.cpython-312.pyc CHANGED
Binary files a/app/__pycache__/main.cpython-312.pyc and b/app/__pycache__/main.cpython-312.pyc differ
 
tests/test_services.py β†’ app/controllers/__init__.py RENAMED
File without changes
app/controllers/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (172 Bytes). View file
 
app/controllers/__pycache__/f5_model.cpython-312.pyc ADDED
Binary file (4.17 kB). View file
 
app/controllers/__pycache__/plan_chat_controller.cpython-312.pyc ADDED
Binary file (3.56 kB). View file
 
app/controllers/__pycache__/scraper_controller.cpython-312.pyc ADDED
Binary file (2.62 kB). View file
 
app/controllers/f5_model.py ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List, Optional
2
+ from pydantic import BaseModel
3
+ import torch
4
+ import logging
5
+ from transformers import pipeline
6
+
7
+ class F5ModelHandler:
8
+ def __init__(self):
9
+ logging.info("Initializing F5ModelHandler...")
10
+ try:
11
+ logging.info("Loading model 'google/flan-t5-small'...")
12
+ self.model_name = "google/flan-t5-small"
13
+ # Use pipeline for simpler model loading
14
+ self.generator = pipeline(
15
+ "text2text-generation",
16
+ model=self.model_name,
17
+ device="cuda" if torch.cuda.is_available() else "cpu",
18
+ torch_dtype=torch.float16 if torch.cuda.is_available() else torch.float32
19
+ )
20
+ logging.info(f"Model loaded successfully on {self.generator.device}")
21
+ except Exception as e:
22
+ logging.error(f"Error loading model: {str(e)}")
23
+ raise
24
+
25
+ async def generate_response(self, prompt: str, max_length: int = 2048) -> str:
26
+ try:
27
+ logging.info(f"Generating response for prompt: {prompt[:100]}...")
28
+
29
+ # Generate with more focused parameters
30
+ response = self.generator(
31
+ prompt,
32
+ max_length=max_length,
33
+ num_beams=5,
34
+ temperature=0.7,
35
+ top_p=0.95,
36
+ top_k=50,
37
+ repetition_penalty=1.2,
38
+ length_penalty=1.0,
39
+ do_sample=True,
40
+ num_return_sequences=1
41
+ )[0]['generated_text']
42
+
43
+ # Clean up the response
44
+ response = response.strip()
45
+
46
+ # Ensure minimum content length
47
+ if len(response) < 100:
48
+ logging.warning("Response too short, regenerating...")
49
+ return await self.generate_response(prompt, max_length)
50
+
51
+ logging.info(f"Generated response successfully: {response[:100]}...")
52
+ return response
53
+
54
+ except Exception as e:
55
+ logging.error(f"Error generating response: {str(e)}")
56
+ raise
57
+
58
+ async def stream_response(self, prompt: str, max_length: int = 1000):
59
+ try:
60
+ response = self.generator(
61
+ prompt,
62
+ max_length=max_length,
63
+ num_beams=4,
64
+ temperature=0.7,
65
+ top_p=0.9,
66
+ do_sample=True,
67
+ return_full_text=False
68
+ )[0]['generated_text']
69
+
70
+ # Simulate streaming by yielding chunks of the response
71
+ chunk_size = 20
72
+ for i in range(0, len(response), chunk_size):
73
+ chunk = response[i:i + chunk_size]
74
+ yield chunk
75
+
76
+ except Exception as e:
77
+ logging.error(f"Error in stream_response: {str(e)}")
78
+ raise
79
+
80
+ # Initialize the model handler
81
+ logging.basicConfig(
82
+ level=logging.INFO,
83
+ format='%(asctime)s - %(levelname)s - %(message)s'
84
+ )
85
+
86
+ f5_model = F5ModelHandler()
app/controllers/plan_chat_controller.py ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, HTTPException
2
+ from fastapi.responses import StreamingResponse, PlainTextResponse
3
+ from pydantic import BaseModel, Field
4
+ from typing import List, Literal
5
+ from app.helpers.plan_chat import ask_plan_question
6
+ from app.helpers.token_auth import get_token
7
+ from app.helpers.get_current_uesr import get_user_from_token
8
+ from app.models.project import Project
9
+ from app.helpers.vectorization import search_similar
10
+
11
+ router = APIRouter()
12
+
13
+ class HistoryItem(BaseModel):
14
+ message: str
15
+ from_: Literal["user", "ai"]
16
+
17
+ class PlanChatPayload(BaseModel):
18
+ query: str
19
+ history: List[HistoryItem]
20
+ project_id: str
21
+
22
+ @router.post("/plan-chat")
23
+ async def plan_chat(data: PlanChatPayload, token: str = Depends(get_token)):
24
+ """
25
+ Handle chat messages for plan generation with context from scraped content
26
+ """
27
+ try:
28
+ # Validate user
29
+ user = await get_user_from_token(token=token)
30
+ if not user:
31
+ raise HTTPException(status_code=401, detail="Invalid token")
32
+
33
+ # Get project context
34
+ project = await Project.get_or_none(id=data.project_id)
35
+ if not project:
36
+ raise HTTPException(status_code=404, detail="Project not found")
37
+
38
+ # Get relevant context from vectorstore
39
+ context = await search_similar(data.query)
40
+
41
+ # Prepare system prompt
42
+ system_prompt = (
43
+ "You are a solution architect assistant specialized in cloud architecture. "
44
+ "Use the following context to help answer questions about the project plan. "
45
+ "Focus on providing specific, actionable advice based on the project requirements "
46
+ "and scraped documentation.\n\n"
47
+ f"Project Context: {context}\n"
48
+ f"Project Requirements: {project.requirements}\n"
49
+ f"Project Features: {project.features}\n"
50
+ f"Solution Stack: {project.solution_stack}\n"
51
+ )
52
+
53
+ async def response_stream():
54
+ async for chunk in ask_plan_question(
55
+ question=data.query,
56
+ history=data.history,
57
+ project_context=system_prompt
58
+ ):
59
+ yield chunk
60
+
61
+ return StreamingResponse(response_stream(), media_type="text/plain")
62
+
63
+ except Exception as e:
64
+ return PlainTextResponse(str(e), status_code=500)
app/controllers/scraper_controller.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Request, HTTPException
2
+ from fastapi.templating import Jinja2Templates
3
+ from pydantic import BaseModel, HttpUrl
4
+ from app.services.scraper_service import ScraperService
5
+ from app.services.flan_t5_service import FlanT5Service
6
+ from typing import Optional
7
+
8
+ router = APIRouter()
9
+ templates = Jinja2Templates(directory="app/templates")
10
+
11
+ scraper_service = ScraperService()
12
+ flan_t5_service = FlanT5Service()
13
+
14
+ class ScrapeRequest(BaseModel):
15
+ url: HttpUrl
16
+ prompt_template: Optional[str] = "Summarize the following text: {text}"
17
+
18
+ @router.post("/api/scrape")
19
+ async def scrape_url(data: ScrapeRequest):
20
+ try:
21
+ # Scrape and process the URL
22
+ text, chunks = await scraper_service.scrape_and_process(str(data.url))
23
+
24
+ # Process each chunk with Flan-T5
25
+ results = []
26
+ for chunk in chunks:
27
+ prompt = data.prompt_template.format(text=chunk.page_content)
28
+ response = await flan_t5_service.generate_response(prompt)
29
+ results.append(response)
30
+
31
+ # Combine results
32
+ final_result = " ".join(results)
33
+
34
+ return {
35
+ "success": True,
36
+ "result": final_result,
37
+ "url": str(data.url)
38
+ }
39
+ except Exception as e:
40
+ raise HTTPException(status_code=500, detail=str(e))
41
+
42
+ @router.get("/history")
43
+ async def show_history(request: Request):
44
+ # You can add history functionality later if needed
45
+ return templates.TemplateResponse("history.html", {"request": request})
app/helpers/__init__.py ADDED
File without changes
app/helpers/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (168 Bytes). View file
 
app/helpers/__pycache__/plan_chat.cpython-312.pyc ADDED
Binary file (2.63 kB). View file
 
app/helpers/__pycache__/plan_parser.cpython-312.pyc ADDED
Binary file (3.88 kB). View file
 
app/helpers/chat.py ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from langchain_core.prompts import ChatPromptTemplate
2
+ from langchain_core.output_parsers import StrOutputParser
3
+ from helpers.generate_embbedings import vector_store
4
+ from helpers.f5_model import f5_model
5
+
6
+ def make_prompt(history, prompt, context=None):
7
+ formatted_history = ""
8
+
9
+ if context:
10
+ formatted_history += f"[CONTEXT] {context} [/CONTEXT]\n"
11
+
12
+ for history_item in history:
13
+ if history_item.from_ == 'user':
14
+ formatted_history += f"[INST] {history_item.message} [/INST]\n"
15
+ else:
16
+ formatted_history += f"{history_item.message}\n"
17
+
18
+ formatted_history += f"[INST] {prompt} [/INST]\n"
19
+
20
+ return formatted_history
21
+
22
+ async def ask_question(question: str, history: list = [], project_id=None):
23
+ """
24
+ Generate a response using F5 model based on history and project-specific context.
25
+ """
26
+ try:
27
+ context = ""
28
+ if project_id is not None:
29
+ context = vector_store.similarity_search(
30
+ query=question, k=4, filter={"project_id": project_id}
31
+ )
32
+
33
+ prompt = make_prompt(history, question, context)
34
+
35
+ async for chunk in f5_model.stream_response(prompt):
36
+ yield chunk
37
+ except Exception as e:
38
+ raise RuntimeError(f"Error generating response: {str(e)}")
app/helpers/generate_features.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel
2
+ from models.features import Feature as FeatureModel
3
+ from typing import List
4
+ from helpers.f5_model import f5_model
5
+
6
+ class Feature(BaseModel):
7
+ feature: str
8
+ short_description: str
9
+
10
+ class Features(BaseModel):
11
+ features: List[Feature]
12
+
13
+ async def generate_features(requirements: str):
14
+ query = (
15
+ "See the user requirements and propose him the features (it should be 20 features). Feature names should be short. "
16
+ "The user will then choose one or more needed features. \n"
17
+ "User Requirements:\n"
18
+ f"{requirements}"
19
+ )
20
+
21
+ response = await f5_model.generate_response(query)
22
+ # Parse the response into Features structure
23
+ # You might need to add additional parsing logic here
24
+ features_dict = parse_features_response(response)
25
+ return Features(**features_dict)
26
+
27
+ def parse_features_response(response: str) -> dict:
28
+ # Add parsing logic here to convert F5 model output to Features format
29
+ # This is a placeholder implementation
30
+ features_list = []
31
+ # Parse the response and create Feature objects
32
+ return {"features": features_list}
app/helpers/generate_plan.py ADDED
@@ -0,0 +1,256 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+ class ScopeObjective(BaseModel):
4
+ scope: str = Field(description="Define the core functionalities of the system")
5
+ objectives: List[str] = Field(description="List at least three objectives focusing on user needs and system capabilities")
6
+
7
+ class ArchitectureObjectives(BaseModel):
8
+ component: str = Field(description="architecture component for the project")
9
+ objectives: str = Field(description="project objectives, each associated with a key architecture component")
10
+
11
+ class ComponenetDesign(BaseModel):
12
+ component: str = Field(description="core component for the project")
13
+ purpose: str = Field(description="purpose of the core component")
14
+ interactions: str = Field(description="interactions between the core components")
15
+ specifications: str = Field(description="specifications for the core components")
16
+
17
+ class TeamRolesSkillsRequirements(BaseModel):
18
+ role: str = Field(description="role for the team member")
19
+ skills: str = Field(description="skills required for this particular job")
20
+
21
+ class CostEstimatesOptimization(BaseModel):
22
+ estimated_costs: str = Field(description="estimated cost for the project")
23
+ optimization_strategies: List[str] = Field(description="Optimized Strategy for the project, it will always be a list of strings")
24
+
25
+ class Tasks(BaseModel):
26
+ name: str = Field(description="Name of the task")
27
+ subtasks: List[str] = Field(description="sub tasks relevant for the task")
28
+
29
+ class Phases(BaseModel):
30
+ name: str = Field(description="name of the phase")
31
+ tasks: List[Tasks] = Field(description="Tasks for this phase")
32
+
33
+ class ProjectTasksMileStones(BaseModel):
34
+ phases: List[Phases] = Field(description="Phases for the project")
35
+
36
+ class PlanOutput(BaseModel):
37
+ executive_summary: str = Field(description="concise overview of the project objectives, highlighting key features, targeted users, and platform (e.g., AWS)")
38
+ scope_objectives: ScopeObjective = Field(description="Scope and Objective for the project")
39
+ architecture_overview: str = Field(description="Overview of the system architecture, detailing the primary components and how they interact")
40
+ architecture_objectives: List[ArchitectureObjectives] = Field(description="List at least three objectives, each associated with a key architecture component, such as compute, storage, or network.")
41
+ component_design: List[ComponenetDesign] = Field(description="Detail at least two core components, including their purpose, interactions, and specifications")
42
+ security_and_compliance: List[str] = Field(description="List at least three security and compliance measures as strings, it should always be list of strings")
43
+ deployment_testing_monitoring: List[str] = Field(description="List of different points of deployment, testing, and monitoring, describing CI/CD, testing types, and monitoring approaches. it should always be list of strings")
44
+ team_roles_skills_requirements: List[TeamRolesSkillsRequirements] = Field(description="Define the roles needed for the project and their respective skill sets")
45
+ cost_estimates_optimization: CostEstimatesOptimization = Field(description="Cost Estimates for the project and strategies to optimize resources: don't include the cost just the explanation for the cost")
46
+ project_tasks_milestones: ProjectTasksMileStones = Field(description="Major project tasks and milestones, structured by phases")
47
+
48
+ async def generate_rough_plan(data, token):
49
+ prompt = (
50
+ f"Requirements: {data.requirements}\n"
51
+ f"Backend: {data.backend}\n"
52
+ f"Frontend: {data.frontend}\n"
53
+ f"Database: {data.database}\n"
54
+ f"Features: {data.features} \n"
55
+ f"Additional Features: {data.additional_feature} \n"
56
+ + "\n".join([
57
+ f"{qa.question}\n{qa.answer}"
58
+ for qa in data.question_answers
59
+ ])
60
+ + "\n\n"
61
+ "Based on the provided details, please generate a comprehensive project description with the following sections:\n"
62
+ "1. Executive Summary\n"
63
+ f" Provide a concise overview of the project objectives, highlighting key features, targeted users, and platform (e.g., AWS) it should be atleast 4, 5 lines long also add 3 key outcomes. it should also focus of platform {data.platform}\n"
64
+ "2. Project Scope and Objectives\n"
65
+ f" - Scope: Define the core functionalities of the system 3 scopes and 3 objectives. focusing of platform {data.platform}\n"
66
+ f" - Objectives: List at least three objectives focusing on user needs and system capabilities focusing on platofrm {data.platform}.\n"
67
+ "3. Architecture Overview\n"
68
+ f" - Provide an overview of the system architecture, detailing the primary components and how they interact.focusing mostly on {data.platform}\n"
69
+ "4. Architecture Objectives\n"
70
+ f" - List at least three objectives, each associated with a key architecture component, such as frontend, backend, database, media storage, Authentication, compute, storage, or network. include all 6, 7 points focusing mostly on platofrm {data.platform}\n"
71
+ "5. Component Design\n"
72
+ f" - Detail at least two core components, including their purpose, interactions, and specifications. focus mostly on {data.platform}\n"
73
+ "6. Security and Compliance\n"
74
+ f" - List at least three security and compliance measures, such as data encryption, authentication, and compliance standards. focus mostly on {data.platform}\n"
75
+ "7. Deployment, Testing, and Monitoring\n"
76
+ f" - Include at least three aspects of deployment, testing, and monitoring, describing CI/CD, testing types, and monitoring approaches. focus mostly on {data.platform}\n"
77
+ "8. Team Roles and Skills Requirements\n"
78
+ f" - Define the roles needed for the project and their respective skill sets.focus mostly on {data.platform}\n"
79
+ "9. Cost Estimates and Optimization\n"
80
+ f" - Provide explanation for the cost (not including any digits or estimate just the explanation) and strategies to optimize resources {data.platform} (e.g., autoscaling, storage management).\n"
81
+ "10. Project Tasks and Milestones\n"
82
+ f" - Outline major project tasks and milestones, structured by phases (e.g., architecture design, development, testing, deployment). {data.platform}\n\n"
83
+ "11. Architecture Design Phase \n"
84
+ " - Define Architecture Design Phase for the project with main task and subtasks (Include exactly 3 tasks and 3 subtasks for each tasks) \n"
85
+ "12. Development Phase \n"
86
+ " - Define Development Phase for the project with main task that can be represented on the kanman, (Include exactly 4 tasks and 3 subtasks for each tasks) \n"
87
+ "13. Testing Phase \n"
88
+ " - Define Testing Phase for the project with main task that can be represented on the kanman, (Include exactly 2 tasks and 3 subtasks for each tasks)"
89
+ "14. Deployment Phase \n"
90
+ " - Define Deployment Phase for the project with main task that can be represented with the kanman (Include exactly 3 tasks and 2 subtasks for each tasks)"
91
+ "Output format:\n"
92
+ "The output should be a valid JSON structure with each section represented as a JSON object. For example:\n\n"
93
+
94
+ f"Note: the description or content for each section should foces on platform and include data sepcific to that platoform{data.platform}"
95
+ f"{json_output_format}"
96
+ )
97
+
98
+
99
+ output = ""
100
+ user = await get_user_from_token(token=token)
101
+ async for chunk in ask_question(question= prompt, history=[], use_context=False):
102
+ output += chunk
103
+ project_title = data.project_title
104
+ json_response = parse_and_return_json(output)
105
+ if (data.project_title == "" or data.project_title == None):
106
+ generated_project_title = await generate_project_title(requirements=data.requirements)
107
+ project_title = generated_project_title.split(":")[0].strip('"')
108
+ try:
109
+ print(f"Json Response before generate_plan_html: {json_response}")
110
+ plan_html = generate_plan_html(data=json.loads(json_response))
111
+ print(f"Json Response before generate_plan_html:")
112
+ project_id = await save_final_plan(project_title=project_title,
113
+ user=user,
114
+ data=data,
115
+ json_response=json_response,
116
+ plan_html=plan_html)
117
+
118
+ print(f"Project ID: {project_id}")
119
+ return {
120
+ "project_title": project_title,
121
+ "project_id": project_id,
122
+ "plan_html": plan_html,
123
+ "data": json.loads(json_response)
124
+ }
125
+ except Exception as e:
126
+ raise Exception(str(e))
127
+
128
+
129
+ async def generate_final_plan(data, token):
130
+ print(f"Step 1111111")
131
+ user = await get_user_from_token(token= token)
132
+ output = ""
133
+ # async for chunk in ask_question(question=prompt, history=[], use_context=False):
134
+ # output += chunk
135
+ # json_response = parse_and_return_json(output)
136
+ # json_response = json.loads(json_response)
137
+ json_response = await extract_data_from_html(data.rough_plan_html)
138
+ print(f"JSON RESPONSE after extract_data_from_html.......")
139
+ json_response = json.loads(json_response)
140
+ project_title = data.project_title
141
+ if (data.project_title == "" or data.project_title == None):
142
+ generated_project_title = await generate_project_title(requirements=data.requirements)
143
+ project_title = generated_project_title.split(":")[0].strip('"')
144
+ project_name = project_title
145
+ # tasks_applications = "".join([phase['name'] for phase in json_response['project_tasks_milestones']['phases']])
146
+ tasks_applications = generate_task_appplication(json_response['project_tasks_milestones'])
147
+ business_objectives = ", ".join([obj for obj in json_response['scope_objectives']['objectives']])
148
+ existing_infrastructure = 'AWS',
149
+ scalability_performance = ", ".join([item['objective'] for item in json_response['architecture_objectives']])
150
+ other_requirements = "".join([feature for feature in data.features])
151
+ user_id = "e16575cd-e9d3-47d5-b3ba-d3ef612f5683"
152
+ request_body = {
153
+ "project_name": project_name,
154
+ "tasks_applications": tasks_applications,
155
+ "business_objectives": business_objectives,
156
+ "existing_infrastructure": "AWS",
157
+ "scalability_performance":scalability_performance,
158
+ "security_compliance": scalability_performance,
159
+ "other_requirements": other_requirements,
160
+ "user_id": "e16575cd-e9d3-47d5-b3ba-d3ef612f5683",
161
+ }
162
+ async with httpx.AsyncClient(timeout=60.0) as client:
163
+ try:
164
+ external_response = await client.post(
165
+ "https://auto-board-workspace.vercel.app/api/plan/generate",
166
+ json=request_body,
167
+ timeout=60.0
168
+ )
169
+ except Exception as e:
170
+ raise Exception(f"Error generating auto-board:::::::::{e}")
171
+ if external_response.status_code != 200:
172
+ raise Exception("Error generating auto-board....")
173
+ else:
174
+ response_data = external_response.json()
175
+ print(f"Response Data: {response_data}")
176
+ print(f"Response Data: {response_data['data']}")
177
+ print(f"Response Data: {response_data['data']['id']}")
178
+ print(f"First API called.............")
179
+ gantt_request_body = {
180
+ "projectId": str(response_data['data']['id']),
181
+ "userId": "e16575cd-e9d3-47d5-b3ba-d3ef612f5683"
182
+ }
183
+ print(f"gantt request body: {gantt_request_body}")
184
+ try:
185
+ kamban = await client.post(
186
+ "https://auto-board-workspace.vercel.app/api/kanbans/generate",
187
+ json=gantt_request_body,
188
+ timeout=60.0
189
+ )
190
+ external_response = await client.post(
191
+ "https://auto-board-workspace.vercel.app/api/gantt/generate",
192
+ json=gantt_request_body,
193
+ timeout=60.0
194
+ )
195
+ except Exception as e:
196
+ raise Exception(f"Error generating gantt: {e}")
197
+ print(f"Second and Third API called.............")
198
+ kamban_response = kamban.json()
199
+ async with httpx.AsyncClient() as client:
200
+ if external_response.status_code == 200:
201
+ response_json = external_response.json()
202
+ db_instance = ProjectModel(requirements=data.requirements, features=data.features,
203
+ solution_stack = data.solution_stack, rough_plan = data.rough_plan,
204
+ final_plan = json_response, user_id = user.id,
205
+ project_title = project_title,
206
+ gantt_project_id = str(response_data['data']['id']),
207
+ user_uuid ="e16575cd-e9d3-47d5-b3ba-d3ef612f5683",
208
+ ganttDataID=str(response_json['data']['id']),
209
+ boardId=str(kamban_response['board']['id']),
210
+ rough_plan_html=data.rough_plan_html,
211
+ final_plan_html=data.rough_plan_html
212
+ )
213
+ await db_instance.save()
214
+ print(f"DB Instance: {db_instance.id}")
215
+ # str_data = ""
216
+ # if isinstance(json_response, dict):
217
+ # str_data = json.dumps(json_response)
218
+ # else:
219
+ # str_data = json.loads(json_response)
220
+ # await generate_embeddings(data=str_data, project_id=db_instance.id)
221
+ print(f"Finalizing............")
222
+ return {
223
+ "project_title": project_title,
224
+ "project_id": int(db_instance.id),
225
+ "data": json_response,
226
+ "final_plan": data.rough_plan_html
227
+ }
228
+ else:
229
+ print(f"Gantt Chart Task: External API responded with status code {external_response.status_code}: {external_response.text}")
230
+ return {
231
+ "success": False,
232
+ "status_code": external_response.status_code,
233
+ "message": external_response.text
234
+ }
235
+
236
+
237
+ async def update_final_plan_fun(project_id, data, token):
238
+ try:
239
+ project , json_response = await asyncio.gather(
240
+ ProjectModel.get(id=project_id),
241
+ extract_data_from_html(data.final_plan_html)
242
+ )
243
+ project.final_plan_html = data.final_plan_html
244
+ print(f"saving html content.........")
245
+ project.final_plan = json.loads(json_response)
246
+ await project.save()
247
+
248
+ return {
249
+ "success": True,
250
+ "message": "Final plan HTML updated successfully.",
251
+ "project_id": project.id,
252
+ "final_plan_html": project.final_plan_html
253
+ }
254
+
255
+ except Exception as e:
256
+ raise Exception(f"Error updating final plan HTML: {str(e)}")
app/helpers/generate_soluction_stack.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from helpers.f5_model import f5_model
2
+ from pydantic import BaseModel, Field
3
+ from typing import List
4
+
5
+ class StackComponent(BaseModel):
6
+ service: str
7
+ description: str
8
+
9
+ class SoluctionStack(BaseModel):
10
+ computer_processing: List[StackComponent]
11
+ data_management_storage: List[StackComponent]
12
+ network_security: List[StackComponent]
13
+ app_integration_management: List[StackComponent]
14
+
15
+ async def generate_soluction_stack(data):
16
+ prompt = (
17
+ f"Requirements: {data.requirements}\n"
18
+ f"Additional Features: {data.additional_feature} \n"
19
+ "Generate a comprehensive cloud solution stack with the following components:\n"
20
+ "1. Computer Processing\n"
21
+ "2. Data Management and Storage\n"
22
+ "3. Network Security\n"
23
+ "4. Application Integration and Management"
24
+ )
25
+
26
+ response = await f5_model.generate_response(prompt)
27
+ # Parse the response into SoluctionStack structure
28
+ stack_dict = parse_stack_response(response)
29
+ return SoluctionStack(**stack_dict)
30
+
31
+ def parse_stack_response(response: str) -> dict:
32
+ # Add parsing logic here to convert F5 model output to SoluctionStack format
33
+ # This is a placeholder implementation
34
+ return {
35
+ "computer_processing": [],
36
+ "data_management_storage": [],
37
+ "network_security": [],
38
+ "app_integration_management": []
39
+ }
app/helpers/plan_chat.py ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from langchain_core.prompts import ChatPromptTemplate
2
+ from langchain_core.output_parsers import StrOutputParser
3
+ from langchain_openai import ChatOpenAI
4
+ from helpers.generate_embbedings import vector_store
5
+ from langchain_aws import ChatBedrock
6
+ import os
7
+
8
+ def make_prompt(history, prompt, context=None):
9
+ formatted_history = ""
10
+
11
+ if context:
12
+ formatted_history += f"[CONTEXT] {context} [/CONTEXT]\n"
13
+
14
+ for history_item in history:
15
+ if history_item.from_ == 'user':
16
+ formatted_history += f"[INST] {history_item.message} [/INST]\n"
17
+ else:
18
+ formatted_history += f"{history_item.message}\n"
19
+
20
+ formatted_history += f"[INST] {prompt} [/INST]\n"
21
+
22
+ return formatted_history
23
+
24
+ prompt = ChatPromptTemplate.from_template("{prompt}")
25
+
26
+ model = ChatBedrock(
27
+ model="mistral.mistral-7b-instruct-v0:2",
28
+ aws_access_key_id=os.environ.get("AWS_ACCESS_KEY_ID"),
29
+ aws_secret_access_key=os.environ.get("AWS_SECRET_ACCESS_KEY"),
30
+ region=os.environ.get("AWS_DEFAULT_REGION"),
31
+ max_tokens=8000,
32
+ temperature=0
33
+ )
34
+
35
+ output_parser = StrOutputParser()
36
+
37
+ chain = prompt | model | output_parser
38
+
39
+ async def ask_question(question: str, history: list = [], project_id=None):
40
+ """
41
+ Generate a response for a given question based on history and project-specific context.
42
+ """
43
+ try:
44
+ context = ""
45
+ if project_id is not None:
46
+ context = vector_store.similarity_search(
47
+ query=question, k=4, filter={"project_id": project_id}
48
+ )
49
+
50
+ prompt = make_prompt(history, question, context)
51
+
52
+ stream = chain.astream({"prompt": prompt})
53
+ async for chunk in stream:
54
+ yield chunk
55
+ except Exception as e:
56
+ raise RuntimeError(f"Error generating response: {str(e)}")
57
+
58
+
59
+ gpt_model = ChatOpenAI(
60
+ temperature=0.7,
61
+ model='gpt-4o-mini'
62
+ )
63
+ chain = prompt | gpt_model | StrOutputParser()
64
+
65
+ async def ask_openai(question: str, history: list = []):
66
+ """
67
+ Generate a response for a given question based on history and project-specific context.
68
+ """
69
+ ai_response = "I am the CloudMod Solutions Architect, an expert in AWS, Azure & GCP. How can I help you?"
70
+ try:
71
+ context = ("You are a AI Assistant for CloudMod Soluctions Architect, an expert in AWS, Azure & GCP \n"
72
+ "If asked question such as `what the chat does, what they are`\n"
73
+ "Answer question as per the context \n\n"
74
+ f"Here is the user query : {question}"
75
+ f"here is the previous chat history: {history}"
76
+ )
77
+
78
+ prompt = context
79
+ stream = chain.astream({"prompt": prompt})
80
+ async for chunk in stream:
81
+ yield chunk
82
+ except Exception as e:
83
+ raise RuntimeError(f"Error generating response: {str(e)}")
app/helpers/plan_parser.py ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ def parse_plan_sections(content: str) -> dict:
2
+ """Parse the generated plan content into structured sections."""
3
+ sections = {
4
+ "executive_summary": "No content generated",
5
+ "scope_objectives": "No content generated",
6
+ "architecture_overview": "No content generated",
7
+ "component_design": "No content generated",
8
+ "security_compliance": "No content generated",
9
+ "deployment_testing": "No content generated",
10
+ "team_roles": "No content generated",
11
+ "cost_estimates": "No content generated",
12
+ "project_phases": "No content generated"
13
+ }
14
+
15
+ current_section = None
16
+ lines = content.split('\n')
17
+ section_content = []
18
+
19
+ for line in lines:
20
+ line = line.strip()
21
+ if not line:
22
+ continue
23
+
24
+ # Check for section headers
25
+ if line.lower().startswith('1. executive summary') or line.lower().startswith('executive summary'):
26
+ if current_section and section_content:
27
+ sections[current_section] = '\n'.join(section_content)
28
+ current_section = 'executive_summary'
29
+ section_content = []
30
+ continue
31
+ elif line.lower().startswith('2. project scope') or line.lower().startswith('scope'):
32
+ if current_section and section_content:
33
+ sections[current_section] = '\n'.join(section_content)
34
+ current_section = 'scope_objectives'
35
+ section_content = []
36
+ continue
37
+ elif line.lower().startswith('3. architecture') or line.lower().startswith('architecture'):
38
+ if current_section and section_content:
39
+ sections[current_section] = '\n'.join(section_content)
40
+ current_section = 'architecture_overview'
41
+ section_content = []
42
+ continue
43
+ elif line.lower().startswith('4. component') or line.lower().startswith('component'):
44
+ if current_section and section_content:
45
+ sections[current_section] = '\n'.join(section_content)
46
+ current_section = 'component_design'
47
+ section_content = []
48
+ continue
49
+ elif line.lower().startswith('5. security') or line.lower().startswith('security'):
50
+ if current_section and section_content:
51
+ sections[current_section] = '\n'.join(section_content)
52
+ current_section = 'security_compliance'
53
+ section_content = []
54
+ continue
55
+ elif line.lower().startswith('6. deployment') or line.lower().startswith('deployment'):
56
+ if current_section and section_content:
57
+ sections[current_section] = '\n'.join(section_content)
58
+ current_section = 'deployment_testing'
59
+ section_content = []
60
+ continue
61
+ elif line.lower().startswith('7. team') or line.lower().startswith('team'):
62
+ if current_section and section_content:
63
+ sections[current_section] = '\n'.join(section_content)
64
+ current_section = 'team_roles'
65
+ section_content = []
66
+ continue
67
+ elif line.lower().startswith('8. cost') or line.lower().startswith('cost'):
68
+ if current_section and section_content:
69
+ sections[current_section] = '\n'.join(section_content)
70
+ current_section = 'cost_estimates'
71
+ section_content = []
72
+ continue
73
+ elif line.lower().startswith('9. project') or line.lower().startswith('project'):
74
+ if current_section and section_content:
75
+ sections[current_section] = '\n'.join(section_content)
76
+ current_section = 'project_phases'
77
+ section_content = []
78
+ continue
79
+ elif current_section:
80
+ section_content.append(line)
81
+
82
+ # Add the last section's content
83
+ if current_section and section_content:
84
+ sections[current_section] = '\n'.join(section_content)
85
+
86
+ # Clean up empty sections
87
+ for key, value in sections.items():
88
+ if not value or value.isspace():
89
+ sections[key] = "No content generated for this section."
90
+
91
+ return sections
app/main.py CHANGED
@@ -1,102 +1,298 @@
1
- from fastapi import FastAPI, Request, HTTPException
 
2
  from fastapi.staticfiles import StaticFiles
3
  from fastapi.templating import Jinja2Templates
4
- from .routes import invoices
5
- from app.db.database import init_db, rate_limiter
6
- import os
7
- from fastapi.middleware.cors import CORSMiddleware
8
- from starlette.middleware.base import BaseHTTPMiddleware
9
- from dotenv import load_dotenv
10
  import logging
11
- from typing import Callable
12
- import time
13
 
14
- # Set up logging
15
- logging.basicConfig(level=logging.INFO)
 
 
 
 
 
 
 
16
  logger = logging.getLogger(__name__)
17
 
18
- # Load environment variables
19
- load_dotenv()
20
-
21
- # Get the absolute path to the app directory
22
- BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
23
-
24
- class RateLimitMiddleware(BaseHTTPMiddleware):
25
- async def dispatch(self, request: Request, call_next: Callable):
26
- # Get client IP
27
- client_ip = request.client.host
28
-
29
- # Check rate limit
30
- if not rate_limiter.is_allowed(client_ip):
31
- logger.warning(f"Rate limit exceeded for IP: {client_ip}")
32
- raise HTTPException(
33
- status_code=429,
34
- detail="Too many requests. Please try again later."
35
- )
36
-
37
- # Process request
38
- start_time = time.time()
39
- response = await call_next(request)
40
- process_time = time.time() - start_time
41
-
42
- # Log request details
43
- logger.info(
44
- f"Request: {request.method} {request.url.path} "
45
- f"Client: {client_ip} "
46
- f"Process time: {process_time:.2f}s"
47
- )
48
-
49
- return response
50
 
51
- app = FastAPI(
52
- title="Invoice Generator",
53
- description="API for generating invoices",
54
- version="1.0.0"
55
- )
56
 
57
- # Add rate limiting middleware
58
- app.add_middleware(RateLimitMiddleware)
59
-
60
- # Configure CORS with more specific settings
61
- app.add_middleware(
62
- CORSMiddleware,
63
- allow_origins=["*"], # In production, replace with specific domains
64
- allow_credentials=True,
65
- allow_methods=["*"],
66
- allow_headers=["*"],
67
- max_age=3600, # Cache preflight requests for 1 hour
68
- )
69
 
70
- # Mount static files with absolute path
71
- app.mount("/static", StaticFiles(directory=os.path.join(BASE_DIR, "app/static")), name="static")
72
 
73
- # Templates with absolute path
74
- templates = Jinja2Templates(directory=os.path.join(BASE_DIR, "app/templates"))
 
75
 
76
- # Include routers
77
- app.include_router(invoices.router)
78
 
79
- @app.on_event("startup")
80
- async def startup_event():
81
- logger.info("Starting application...")
82
- await init_db()
83
- logger.info("Application started successfully")
 
84
 
85
- @app.on_event("shutdown")
86
- async def shutdown_event():
87
- logger.info("Shutting down application...")
 
 
 
 
 
 
 
 
 
 
 
88
 
89
- # Root endpoint to serve the HTML page
90
  @app.get("/")
91
- async def root(request: Request):
92
  return templates.TemplateResponse("index.html", {"request": request})
93
 
94
- # Health check endpoint
95
- @app.get("/health")
96
- async def health_check():
97
- return {"status": "healthy", "timestamp": time.time()}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
 
99
- @app.get("/history")
100
- async def history_page(request: Request):
101
- return templates.TemplateResponse("history.html", {"request": request})
102
-
 
1
+ from fastapi import FastAPI, HTTPException, Request
2
+ from fastapi.responses import StreamingResponse
3
  from fastapi.staticfiles import StaticFiles
4
  from fastapi.templating import Jinja2Templates
5
+ from pydantic import BaseModel
6
+ from app.controllers.f5_model import F5ModelHandler
7
+ from typing import List, Optional
 
 
 
8
  import logging
9
+ from app.helpers.plan_parser import parse_plan_sections
 
10
 
11
+ # Configure logging
12
+ logging.basicConfig(
13
+ level=logging.INFO,
14
+ format='%(asctime)s - %(levelname)s - %(message)s',
15
+ handlers=[
16
+ logging.StreamHandler(),
17
+ logging.FileHandler('app.log')
18
+ ]
19
+ )
20
  logger = logging.getLogger(__name__)
21
 
22
+ app = FastAPI(title="F5 Model Test Application")
23
+ templates = Jinja2Templates(directory="app/templates")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
 
25
+ # Initialize the F5 model
26
+ model_handler = F5ModelHandler()
 
 
 
27
 
28
+ class ChatMessage(BaseModel):
29
+ role: str
30
+ content: str
31
+
32
+ class ChatRequest(BaseModel):
33
+ messages: List[ChatMessage]
34
+ stream: Optional[bool] = False
 
 
 
 
 
35
 
36
+ class FeatureRequest(BaseModel):
37
+ requirements: str
38
 
39
+ class Feature(BaseModel):
40
+ feature: str
41
+ short_description: str
42
 
43
+ class FeaturesResponse(BaseModel):
44
+ features: List[Feature]
45
 
46
+ class ProjectPlanRequest(BaseModel):
47
+ project_title: str
48
+ requirements: str
49
+ features: List[str]
50
+ platform: str = "AWS" # Default to AWS
51
+ additional_requirements: str = ""
52
 
53
+ class ProjectSection(BaseModel):
54
+ title: str
55
+ content: str
56
+
57
+ class ProjectPlan(BaseModel):
58
+ executive_summary: str
59
+ scope_objectives: dict
60
+ architecture_overview: str
61
+ component_design: List[dict]
62
+ security_compliance: List[str]
63
+ deployment_testing: List[str]
64
+ team_roles: List[dict]
65
+ cost_estimates: dict
66
+ project_phases: List[dict]
67
 
 
68
  @app.get("/")
69
+ async def index(request: Request):
70
  return templates.TemplateResponse("index.html", {"request": request})
71
 
72
+ @app.post("/chat")
73
+ async def chat(request: ChatRequest):
74
+ try:
75
+ logger.info(f"Chat request received with {len(request.messages)} messages")
76
+
77
+ # Improve the prompt with better context
78
+ prompt = (
79
+ "You are a helpful AI assistant specializing in SaaS applications and software development. "
80
+ "Please provide detailed and professional responses.\n\n"
81
+ )
82
+
83
+ for msg in request.messages:
84
+ if msg.role == "user":
85
+ prompt += f"[INST] {msg.content} [/INST]\n"
86
+ else:
87
+ prompt += f"{msg.content}\n"
88
+
89
+ logger.info(f"Formatted prompt: {prompt}")
90
+
91
+ if request.stream:
92
+ logger.info("Starting streaming response")
93
+ async def generate():
94
+ async for chunk in model_handler.stream_response(prompt):
95
+ logger.debug(f"Streaming chunk: {chunk}")
96
+ yield f"data: {chunk}\n\n"
97
+ return StreamingResponse(generate(), media_type="text/event-stream")
98
+ else:
99
+ response = await model_handler.generate_response(prompt)
100
+ logger.info(f"Generated response: {response}")
101
+ return {"response": response}
102
+ except Exception as e:
103
+ logger.error(f"Error in chat endpoint: {str(e)}", exc_info=True)
104
+ raise HTTPException(status_code=500, detail=str(e))
105
+
106
+ @app.post("/generate-features", response_model=FeaturesResponse)
107
+ async def generate_features(request: FeatureRequest):
108
+ try:
109
+ logger.info(f"Feature generation request received with requirements: {request.requirements}")
110
+
111
+ # Improved prompt for better feature generation
112
+ prompt = (
113
+ "You are a SaaS product expert. Generate 20 practical features for a SaaS application.\n"
114
+ "For each feature:\n"
115
+ "1. Provide a short, clear feature name\n"
116
+ "2. Write a concise description explaining its value\n"
117
+ "Format each feature as:\n"
118
+ "Feature Name\n"
119
+ "Clear description of what the feature does and its benefits.\n\n"
120
+ f"Requirements: {request.requirements}\n\n"
121
+ "Generate 20 features in this exact format."
122
+ )
123
+
124
+ logger.info(f"Generated prompt: {prompt}")
125
+
126
+ response = await model_handler.generate_response(prompt)
127
+ logger.info(f"Model response: {response}")
128
+
129
+ # Parse the response into features
130
+ features = []
131
+ lines = response.split('\n')
132
+ current_feature = None
133
+ current_description = []
134
+
135
+ for line in lines:
136
+ line = line.strip()
137
+ if not line:
138
+ if current_feature and current_description:
139
+ features.append(Feature(
140
+ feature=current_feature,
141
+ short_description=' '.join(current_description)
142
+ ))
143
+ current_feature = None
144
+ current_description = []
145
+ elif not current_feature:
146
+ current_feature = line
147
+ else:
148
+ current_description.append(line)
149
+
150
+ # Add the last feature if exists
151
+ if current_feature and current_description:
152
+ features.append(Feature(
153
+ feature=current_feature,
154
+ short_description=' '.join(current_description)
155
+ ))
156
+
157
+ # If no features were parsed, provide fallback features
158
+ if not features:
159
+ features = [
160
+ Feature(
161
+ feature="User Management",
162
+ short_description="Complete user authentication and authorization system"
163
+ ),
164
+ Feature(
165
+ feature="Subscription Billing",
166
+ short_description="Automated billing and subscription management"
167
+ ),
168
+ Feature(
169
+ feature="Analytics Dashboard",
170
+ short_description="Real-time metrics and usage analytics"
171
+ ),
172
+ Feature(
173
+ feature="API Integration",
174
+ short_description="RESTful API endpoints for third-party integration"
175
+ ),
176
+ Feature(
177
+ feature="Multi-tenant Architecture",
178
+ short_description="Secure data isolation for multiple customers"
179
+ )
180
+ ]
181
+
182
+ logger.info(f"Returning features: {features}")
183
+ return FeaturesResponse(features=features)
184
+ except Exception as e:
185
+ logger.error(f"Error in generate-features endpoint: {str(e)}", exc_info=True)
186
+ raise HTTPException(status_code=500, detail=str(e))
187
+
188
+ @app.post("/generate-plan")
189
+ async def generate_plan(request: ProjectPlanRequest):
190
+ try:
191
+ logger.info(f"Plan generation request received for project: {request.project_title}")
192
+
193
+ # Enhanced prompt for more detailed output
194
+ prompt = f"""Generate a comprehensive technical project plan for '{request.project_title}' with detailed sections.
195
+ Include specific {request.platform} services and implementation details in each section.
196
+
197
+ 1. Executive Summary
198
+ - Provide a detailed overview of the project (4-5 paragraphs)
199
+ - Include project goals, target users, and key outcomes
200
+ - Highlight main {request.platform} services to be used
201
+ - Explain expected business impact
202
+
203
+ 2. Project Scope and Objectives
204
+ - Define detailed scope including:
205
+ * Core functionalities
206
+ * System boundaries
207
+ * Integration points
208
+ - List at least 5 specific objectives
209
+ - Include measurable success criteria
210
+
211
+ 3. Architecture Overview
212
+ - Detailed {request.platform} architecture including:
213
+ * Frontend architecture
214
+ * Backend services
215
+ * Database design
216
+ * Integration patterns
217
+ * Network topology
218
+ - Explain how components interact
219
+ - Include scalability considerations
220
+
221
+ 4. Component Design
222
+ - Detail at least 3 core components with:
223
+ * Purpose and functionality
224
+ * Technical specifications
225
+ * Data flow
226
+ * Integration points
227
+ * Performance requirements
228
+
229
+ 5. Security and Compliance
230
+ - List specific {request.platform} security services
231
+ - Detail authentication and authorization
232
+ - Describe data protection measures
233
+ - Include compliance requirements
234
+ - Specify security monitoring
235
+
236
+ 6. Deployment, Testing, and Monitoring
237
+ - Detailed CI/CD pipeline
238
+ - Test strategy including:
239
+ * Unit testing
240
+ * Integration testing
241
+ * Performance testing
242
+ - Monitoring setup with {request.platform} tools
243
+ - Alerting and logging strategy
244
+
245
+ 7. Team Roles and Skills
246
+ - List all required roles
247
+ - Detail specific skills needed
248
+ - Include {request.platform} certifications
249
+ - Define team structure
250
+ - Specify responsibilities
251
+
252
+ 8. Cost Estimates and Optimization
253
+ - Break down {request.platform} service costs
254
+ - Resource optimization strategies
255
+ - Scaling considerations
256
+ - Cost monitoring approach
257
+ - Budget optimization tips
258
+
259
+ 9. Project Tasks and Milestones
260
+ - Detailed project phases
261
+ - Specific tasks for each phase
262
+ - Timeline estimates
263
+ - Dependencies
264
+ - Critical path items
265
+
266
+ Project Requirements:
267
+ {request.requirements}
268
+
269
+ Features to implement:
270
+ {', '.join(request.features)}
271
+
272
+ Additional Requirements:
273
+ {request.additional_requirements}
274
+
275
+ Generate a detailed plan following this structure with specific {request.platform} implementation details."""
276
+
277
+ logger.info("Generating plan with model...")
278
+ response = await model_handler.generate_response(prompt)
279
+ logger.info(f"Response generated successfully: {response[:100]}...")
280
+
281
+ # Parse the response into structured sections
282
+ sections = parse_plan_sections(response)
283
+
284
+ logger.info("Plan generated successfully")
285
+ return {
286
+ "project_title": request.project_title,
287
+ "sections": sections,
288
+ "raw_content": response
289
+ }
290
+
291
+ except Exception as e:
292
+ logger.error(f"Error generating plan: {str(e)}", exc_info=True)
293
+ raise HTTPException(status_code=500, detail=str(e))
294
+
295
+ if __name__ == "__main__":
296
+ import uvicorn
297
+ uvicorn.run(app, host="0.0.0.0", port=8000)
298
 
 
 
 
 
app/models/__init__.py ADDED
File without changes
app/models/project_plan.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, Field
2
+ from typing import List
3
+
4
+ class ProjectPlanRequest(BaseModel):
5
+ project_title: str
6
+ requirements: str
7
+ features: List[str]
8
+ platform: str = "AWS" # Default to AWS
9
+ additional_requirements: str = ""
10
+
11
+ class ProjectSection(BaseModel):
12
+ title: str
13
+ content: str
14
+
15
+ class ProjectPlan(BaseModel):
16
+ executive_summary: str
17
+ scope_objectives: dict
18
+ architecture_overview: str
19
+ component_design: List[dict]
20
+ security_compliance: List[str]
21
+ deployment_testing: List[str]
22
+ team_roles: List[dict]
23
+ cost_estimates: dict
24
+ project_phases: List[dict]
app/models/scrape_log.py ADDED
File without changes
app/services/__init__.py ADDED
File without changes
app/services/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (169 Bytes). View file
 
app/services/__pycache__/flan_t5_service.cpython-312.pyc ADDED
Binary file (1.56 kB). View file
 
app/services/__pycache__/scraper_service.cpython-312.pyc ADDED
Binary file (2.8 kB). View file
 
app/services/flan_t5_service.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from transformers import T5ForConditionalGeneration, T5Tokenizer
2
+
3
+ class FlanT5Service:
4
+ def __init__(self):
5
+ self.model_name = "google/flan-t5-base"
6
+ self.tokenizer = T5Tokenizer.from_pretrained(self.model_name)
7
+ self.model = T5ForConditionalGeneration.from_pretrained(self.model_name)
8
+
9
+ async def generate_response(self, prompt: str, max_length: int = 512) -> str:
10
+ inputs = self.tokenizer(prompt, return_tensors="pt", max_length=512, truncation=True)
11
+ outputs = self.model.generate(
12
+ **inputs,
13
+ max_length=max_length,
14
+ num_beams=4,
15
+ temperature=0.7,
16
+ top_p=0.9
17
+ )
18
+ return self.tokenizer.decode(outputs[0], skip_special_tokens=True)
app/services/scraper_service.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from httpx import AsyncClient
2
+ from bs4 import BeautifulSoup
3
+ from typing import Tuple
4
+ from langchain_text_splitters import RecursiveCharacterTextSplitter
5
+
6
+ class ScraperService:
7
+ def __init__(self):
8
+ self.text_splitter = RecursiveCharacterTextSplitter(
9
+ chunk_size=1000,
10
+ chunk_overlap=20,
11
+ length_function=len,
12
+ is_separator_regex=False,
13
+ )
14
+
15
+ async def scrape_website(self, url: str) -> str:
16
+ async with AsyncClient() as client:
17
+ chrome_headers = {
18
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36",
19
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
20
+ "Accept-Language": "en-US,en;q=0.9",
21
+ }
22
+ response = await client.get(url, headers=chrome_headers)
23
+ return response.text
24
+
25
+ def extract_text_from_html(self, html: str) -> str:
26
+ soup = BeautifulSoup(html, 'html.parser')
27
+
28
+ # Remove script and style elements
29
+ for element in soup(['script', 'style', 'header', 'footer', 'nav']):
30
+ element.decompose()
31
+
32
+ text = soup.get_text(separator='\n', strip=True)
33
+ return text
34
+
35
+ async def scrape_and_process(self, url: str) -> Tuple[str, list]:
36
+ # Scrape the website
37
+ html = await self.scrape_website(url)
38
+
39
+ # Extract text
40
+ text = self.extract_text_from_html(html)
41
+
42
+ # Split into chunks
43
+ documents = self.text_splitter.create_documents([text])
44
+
45
+ return text, documents
app/static/js/main.js ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ document.addEventListener('DOMContentLoaded', function() {
2
+ const form = document.getElementById('scrapeForm');
3
+ const loadingIndicator = document.getElementById('loadingIndicator');
4
+ const results = document.getElementById('results');
5
+ const resultContent = document.getElementById('resultContent');
6
+
7
+ form.addEventListener('submit', async function(e) {
8
+ e.preventDefault();
9
+
10
+ const url = document.getElementById('url').value;
11
+ const promptTemplate = document.getElementById('promptTemplate').value;
12
+
13
+ loadingIndicator.classList.remove('hidden');
14
+ results.classList.add('hidden');
15
+
16
+ try {
17
+ const response = await fetch('/api/scrape', {
18
+ method: 'POST',
19
+ headers: {
20
+ 'Content-Type': 'application/json',
21
+ },
22
+ body: JSON.stringify({
23
+ url: url,
24
+ prompt_template: promptTemplate
25
+ })
26
+ });
27
+
28
+ const data = await response.json();
29
+
30
+ if (data.success) {
31
+ resultContent.textContent = data.result;
32
+ } else {
33
+ resultContent.textContent = 'Failed to analyze content: ' + data.detail;
34
+ }
35
+
36
+ results.classList.remove('hidden');
37
+ } catch (error) {
38
+ console.error('Error:', error);
39
+ resultContent.textContent = 'An error occurred while processing your request.';
40
+ results.classList.remove('hidden');
41
+ } finally {
42
+ loadingIndicator.classList.add('hidden');
43
+ }
44
+ });
45
+ });
app/templates/index.html CHANGED
@@ -3,1407 +3,372 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Invoice Generator</title>
7
- <!-- Bootstrap CSS -->
8
- <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
9
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
10
  <style>
11
- :root {
12
- --primary-color: #2c3e50;
13
- --secondary-color: #34495e;
14
- --accent-color: #3498db;
15
- --light-gray: #f8f9fa;
16
- --border-radius: 8px;
17
- }
18
-
19
- body {
20
- background-color: #f5f6fa;
21
- color: var(--primary-color);
22
- }
23
-
24
- .container {
25
- max-width: 1400px;
26
- padding: 2rem;
27
- }
28
-
29
- .card {
30
- border: none;
31
- box-shadow: 0 2px 15px rgba(0, 0, 0, 0.1);
32
- border-radius: var(--border-radius);
33
- margin-bottom: 1.5rem;
34
- }
35
-
36
- .card-header {
37
- background-color: var(--primary-color);
38
- color: white;
39
- border-radius: var(--border-radius) var(--border-radius) 0 0 !important;
40
- padding: 1rem;
41
- font-weight: 500;
42
- }
43
-
44
- .card-body {
45
- padding: 1.5rem;
46
- }
47
-
48
- .form-control {
49
- border: 1px solid #dee2e6;
50
- border-radius: var(--border-radius);
51
- padding: 0.75rem;
52
- transition: all 0.3s ease;
53
- }
54
-
55
- .form-control:focus {
56
- border-color: var(--accent-color);
57
- box-shadow: 0 0 0 0.2rem rgba(52, 152, 219, 0.25);
58
- }
59
-
60
- .btn-primary {
61
- background-color: var(--accent-color);
62
- border: none;
63
- padding: 0.75rem 1.5rem;
64
- border-radius: var(--border-radius);
65
- transition: all 0.3s ease;
66
- }
67
-
68
- .btn-primary:hover {
69
- background-color: #2980b9;
70
- transform: translateY(-1px);
71
- }
72
-
73
- .btn-success {
74
- background-color: #2ecc71;
75
- border: none;
76
- padding: 1rem 2rem;
77
- border-radius: var(--border-radius);
78
- font-weight: 500;
79
- transition: all 0.3s ease;
80
- }
81
-
82
- .btn-success:hover {
83
- background-color: #27ae60;
84
- transform: translateY(-2px);
85
- }
86
-
87
- .table {
88
- background-color: white;
89
- border-radius: var(--border-radius);
90
- overflow: hidden;
91
- }
92
-
93
- .table-primary {
94
- background-color: var(--primary-color);
95
- color: white;
96
- }
97
-
98
- .table th {
99
- font-weight: 500;
100
- padding: 1rem;
101
- }
102
-
103
- .table td {
104
- padding: 0.75rem;
105
- vertical-align: middle;
106
- }
107
-
108
- .section-title {
109
- background-color: var(--secondary-color);
110
- color: white;
111
- padding: 1rem;
112
- margin: 2rem 0 1rem;
113
- border-radius: var(--border-radius);
114
- display: flex;
115
- align-items: center;
116
- justify-content: space-between;
117
- }
118
-
119
- .section-title h4 {
120
- margin: 0;
121
- font-weight: 500;
122
- }
123
-
124
- .btn-group {
125
- background-color: white;
126
- border-radius: var(--border-radius);
127
- padding: 0.25rem;
128
- }
129
-
130
- .btn-check + .btn-outline-primary {
131
- color: var(--primary-color);
132
- border-color: var(--primary-color);
133
- }
134
-
135
- .btn-check:checked + .btn-outline-primary {
136
- background-color: var (--primary-color);
137
- color: white;
138
- }
139
-
140
- .modal-content {
141
- border-radius: var(--border-radius);
142
- border: none;
143
- box-shadow: 0 5px 25px rgba(0, 0, 0, 0.2);
144
- }
145
-
146
- .modal-header {
147
- background-color: var(--primary-color);
148
- color: white;
149
- border-radius: var(--border-radius) var (--border-radius) 0 0;
150
- }
151
-
152
- .btn-close {
153
- filter: brightness(0) invert(1);
154
- }
155
-
156
- .btn-danger {
157
- background-color: #e74c3c;
158
- border: none;
159
- border-radius: var(--border-radius);
160
- transition: all 0.3s ease;
161
- }
162
-
163
- .btn-danger:hover {
164
- background-color: #c0392b;
165
- }
166
-
167
- /* Responsive adjustments */
168
- @media (max-width: 768px) {
169
- .container {
170
- padding: 1rem;
171
- }
172
-
173
- .card-body {
174
- padding: 1rem;
175
- }
176
-
177
- .btn {
178
- padding: 0.5rem 1rem;
179
- }
180
- }
181
-
182
- .preview-modal .modal-dialog {
183
- max-width: 80%;
184
- }
185
- .status-pending { color: #ffc107; }
186
- .status-completed { color: #28a745; }
187
-
188
- header {
189
- background: linear-gradient(135deg, #0d6efd 0%, #0a58ca 100%);
190
- }
191
-
192
- header .btn-light {
193
- transition: all 0.3s ease;
194
- border-radius: 20px;
195
- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
196
- }
197
-
198
- header .btn-light:hover {
199
- transform: translateY(-2px);
200
- box-shadow: 0 4px 8px rgba(0,0,0,0.2);
201
- }
202
-
203
- header h1 {
204
- font-size: 2rem;
205
- text-shadow: 1px 1px 2px rgba(0,0,0,0.1);
206
  }
207
  </style>
208
  </head>
209
- <body class="bg-light">
210
- <!-- Header -->
211
- <header class="bg-primary py-4 mb-4 shadow-sm">
212
- <div class="container">
213
- <div class="row align-items-center justify-content-between">
214
- <div class="col-md-3">
215
- <img src="/static/logo.png" alt="Logo" height="50" class="img-fluid">
 
 
 
 
216
  </div>
217
- <div class="col-md-6 text-center">
218
- <h1 class="text-white mb-0 fw-bold">GΓ©nΓ©rateur de Devis</h1>
219
- <a href="/history" class="btn btn-light mt-2 px-4">
220
- <i class="fas fa-history"></i> Historique des Devis
221
- </a>
222
  </div>
223
- <div class="col-md-3 text-end">
224
- <div class="text-white">
225
- <small class="d-block">Date: <span id="currentDate"></span></small>
226
- <small class="d-block">Version 1.0</small>
227
- </div>
228
- </div>
229
- </div>
230
- </div>
231
- </header>
232
-
233
- <div class="container">
234
- <!-- Main Content -->
235
- <div class="row">
236
- <div class="col-12">
237
- <div class="card shadow-sm mb-4">
238
- <div class="card-body">
239
- <form id="invoiceForm">
240
- <!-- Client and Invoice Information -->
241
- <div class="row mb-4">
242
- <!-- Client Information Card -->
243
- <div class="col-md-6">
244
- <div class="card h-100">
245
- <div class="card-header d-flex justify-content-between align-items-center">
246
- <h5 class="mb-0">Information Client</h5>
247
- <i class="fas fa-user"></i>
248
- </div>
249
- <div class="card-body">
250
- <div class="mb-3">
251
- <label for="clientName" class="form-label">Nom du Client</label>
252
- <input type="text" class="form-control" id="clientName" required>
253
- </div>
254
- <div class="mb-3">
255
- <label for="clientPhone" class="form-label">TΓ©lΓ©phone</label>
256
- <div class="input-group">
257
- <input type="tel" class="form-control" id="clientPhone" name="phone1" placeholder="Premier numΓ©ro" required>
258
- <span class="input-group-text">/</span>
259
- <input type="tel" class="form-control" id="clientPhone2" name="phone2" placeholder="Deuxième numéro (optionnel)">
260
- </div>
261
- </div>
262
- <div class="mb-3">
263
- <label for="clientAddress" class="form-label">Adresse</label>
264
- <input type="text" class="form-control" id="clientAddress" required>
265
- </div>
266
- <div class="mb-3">
267
- <label for="plancher" class="form-label">PLANCHER</label>
268
- <input type="text" class="form-control" id="plancher" placeholder="PH RDC" value="PH RDC">
269
- </div>
270
- <div class="mb-3">
271
- <label class="form-label">Type Client</label>
272
- <div class="btn-group w-100" role="group">
273
- <input type="radio" class="btn-check" name="clientType" id="EE" value="EE" autocomplete="off">
274
- <label class="btn btn-outline-primary" for="EE">EE</label>
275
-
276
- <input type="radio" class="btn-check" name="clientType" id="MED" value="MED" autocomplete="off">
277
- <label class="btn btn-outline-primary" for="MED">MED</label>
278
-
279
- <input type="radio" class="btn-check" name="clientType" id="AM" value="AM" autocomplete="off">
280
- <label class="btn btn-outline-primary" for="AM">AM</label>
281
-
282
- <input type="radio" class="btn-check" name="clientType" id="DIV" value="DIV" autocomplete="off">
283
- <label class="btn btn-outline-primary" for="DIV">DIV</label>
284
- </div>
285
- </div>
286
- <div class="mb-3">
287
- <label class="form-label">Commercial</label>
288
- <select class="form-select" id="commercial" required>
289
- <option value="">SΓ©lectionner un commercial</option>
290
- <option value="khaled">Khaled</option>
291
- <option value="salah">Salah</option>
292
- <option value="ismail">Ismail</option>
293
- <option value="jamal">Jamal</option>
294
- <option value="divers">Divers</option>
295
- </select>
296
- </div>
297
- </div>
298
- </div>
299
- </div>
300
-
301
- <!-- Invoice Information Card -->
302
- <div class="col-md-6">
303
- <div class="card h-100">
304
- <div class="card-header d-flex justify-content-between align-items-center">
305
- <h5 class="mb-0">Information Devis</h5>
306
- <i class="fas fa-file-invoice"></i>
307
- </div>
308
- <div class="card-body">
309
- <div class="mb-3">
310
- <label for="invoiceNumber" class="form-label">NΒ° Devis</label>
311
- <input type="text" class="form-control" id="invoiceNumber" required>
312
- </div>
313
- <div class="mb-3">
314
- <label for="date" class="form-label">Date</label>
315
- <input type="date" class="form-control" id="date" readonly>
316
- </div>
317
- <div class="mb-3">
318
- <label for="project" class="form-label">Type d'ouvrage</label>
319
- <input type="text" class="form-control" id="project" required>
320
- </div>
321
- </div>
322
- </div>
323
- </div>
324
- </div>
325
-
326
- <!-- Items Sections -->
327
- <div class="card shadow-sm mb-4">
328
- <div class="card-body">
329
- <!-- Poutrelles Section -->
330
- <div class="section-title">
331
- <h4>POUTRELLES</h4>
332
- <button type="button" class="btn btn-light" id="addPoutrelles">
333
- <i class="fas fa-plus"></i> Ajouter Poutrelle
334
- </button>
335
- </div>
336
- <div class="table-responsive">
337
- <table class="table table-bordered table-hover">
338
- <thead class="table-primary">
339
- <tr>
340
- <th style="width: 30%">Description</th>
341
- <th style="width: 10%">UnitΓ©</th>
342
- <th style="width: 10%">QuantitΓ©</th>
343
- <th style="width: 15%">Longueur</th>
344
- <th style="width: 15%">P.U</th>
345
- <th style="width: 15%">Total HT</th>
346
- <th style="width: 5%"></th>
347
- </tr>
348
- </thead>
349
- <tbody id="poutrellesTable"></tbody>
350
- </table>
351
- </div>
352
-
353
- <!-- Hourdis Section -->
354
- <div class="section-title mt-4">
355
- <h4>HOURDIS</h4>
356
- <button type="button" class="btn btn-light" id="addHourdis">
357
- <i class="fas fa-plus"></i> Ajouter Hourdis
358
- </button>
359
- </div>
360
- <div class="table-responsive">
361
- <table class="table table-bordered table-hover">
362
- <thead class="table-primary">
363
- <tr>
364
- <th style="width: 30%">Description</th>
365
- <th style="width: 10%">UnitΓ©</th>
366
- <th style="width: 10%">QuantitΓ©</th>
367
- <th style="width: 15%">Longueur</th>
368
- <th style="width: 15%">P.U</th>
369
- <th style="width: 15%">Total HT</th>
370
- <th style="width: 5%"></th>
371
- </tr>
372
- </thead>
373
- <tbody id="hourdisTable"></tbody>
374
- </table>
375
- </div>
376
-
377
- <!-- Panneau Section -->
378
- <div class="section-title mt-4">
379
- <h4>PANNEAU TREILLIS SOUDES</h4>
380
- <button type="button" class="btn btn-light" id="addPanneau">
381
- <i class="fas fa-plus"></i> Ajouter Panneau
382
- </button>
383
- </div>
384
- <div class="table-responsive">
385
- <table class="table table-bordered table-hover">
386
- <thead class="table-primary">
387
- <tr>
388
- <th style="width: 30%">Description</th>
389
- <th style="width: 10%">UnitΓ©</th>
390
- <th style="width: 10%">QuantitΓ©</th>
391
- <th style="width: 15%">Longueur</th>
392
- <th style="width: 15%">P.U</th>
393
- <th style="width: 15%">Total HT</th>
394
- <th style="width: 5%"></th>
395
- </tr>
396
- </thead>
397
- <tbody id="panneauTable"></tbody>
398
- </table>
399
- </div>
400
-
401
- <!-- Agglos Section -->
402
- <div class="section-title mt-4">
403
- <h4>AGGLOS</h4>
404
- <button type="button" class="btn btn-light" id="addAgglos">
405
- <i class="fas fa-plus"></i> Ajouter Agglos
406
- </button>
407
- </div>
408
- <div class="table-responsive">
409
- <table class="table table-bordered table-hover">
410
- <thead class="table-primary">
411
- <tr>
412
- <th style="width: 30%">Description</th>
413
- <th style="width: 10%">UnitΓ©</th>
414
- <th style="width: 10%">QuantitΓ©</th>
415
- <th style="width: 15%">Longueur</th>
416
- <th style="width: 15%">P.U</th>
417
- <th style="width: 15%">Total HT</th>
418
- <th style="width: 5%"></th>
419
- </tr>
420
- </thead>
421
- <tbody id="agglosTable"></tbody>
422
- </table>
423
- </div>
424
- </div>
425
- </div>
426
-
427
- <!-- Totals Section -->
428
- <div class="row">
429
- <div class="col-md-6">
430
- <!-- Empty for spacing -->
431
- </div>
432
- <div class="col-md-6">
433
- <div class="card shadow-sm">
434
- <div class="card-header">
435
- <h5 class="mb-0">Totaux</h5>
436
- </div>
437
- <div class="card-body">
438
- <div class="mb-3 form-check form-switch">
439
- <input class="form-check-input" type="checkbox" id="tvaSwitch" checked>
440
- <label class="form-check-label" for="tvaSwitch">Appliquer TVA 20%</label>
441
- </div>
442
- <div class="mb-3">
443
- <label for="totalHT" class="form-label">Total HT</label>
444
- <input type="number" class="form-control" id="totalHT" readonly>
445
- </div>
446
- <div class="mb-3">
447
- <label for="tax" class="form-label">TVA 20%</label>
448
- <input type="number" class="form-control" id="tax" readonly>
449
- </div>
450
- <div class="mb-3">
451
- <label for="totalTTC" class="form-label">Total TTC</label>
452
- <input type="number" class="form-control" id="totalTTC" readonly>
453
- </div>
454
- </div>
455
- </div>
456
- </div>
457
- </div>
458
-
459
- <!-- Submit Button -->
460
- <div class="d-grid gap-2 col-md-6 mx-auto mt-4">
461
- <div class="row">
462
- <div class="col-md-6">
463
- <button type="submit" class="btn btn-success btn-lg w-100">
464
- <i class="fas fa-file-pdf"></i> GΓ©nΓ©rer PDF
465
- </button>
466
- </div>
467
- <div class="col-md-6">
468
- <button type="button" class="btn btn-primary btn-lg w-100" id="generateExcel">
469
- <i class="fas fa-file-excel"></i> GΓ©nΓ©rer Excel
470
- </button>
471
- </div>
472
- </div>
473
- </div>
474
- </form>
475
- </div>
476
  </div>
477
- </div>
478
- </div>
479
- </div>
480
-
481
- <!-- Add Font Awesome for icons -->
482
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
483
-
484
- <!-- Poutrelles Modal -->
485
- <div class="modal fade" id="poutrellesModal" tabindex="-1">
486
- <div class="modal-dialog modal-lg">
487
- <div class="modal-content">
488
- <div class="modal-header">
489
- <h5 class="modal-title">SΓ©lectionner Poutrelle</h5>
490
- <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
491
  </div>
492
- <div class="modal-body">
493
- <div class="table-responsive">
494
- <table class="table">
495
- <thead>
496
- <tr>
497
- <th>Description</th>
498
- <th>UnitΓ©</th>
499
- <th>QuantitΓ©</th>
500
- <th>Longueur</th>
501
- <th>P.U</th>
502
- <th>Action</th>
503
- </tr>
504
- </thead>
505
- <tbody></tbody>
506
- </table>
507
- </div>
508
  </div>
 
 
 
509
  </div>
510
- </div>
511
- </div>
512
-
513
- <!-- Hourdis Modal -->
514
- <div class="modal fade" id="hourdisModal" tabindex="-1">
515
- <div class="modal-dialog modal-lg">
516
- <div class="modal-content">
517
- <div class="modal-header">
518
- <h5 class="modal-title">SΓ©lectionner Hourdis</h5>
519
- <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
520
- </div>
521
- <div class="modal-body">
522
- <div class="table-responsive">
523
- <table class="table">
524
- <thead>
525
- <tr>
526
- <th>Description</th>
527
- <th>UnitΓ©</th>
528
- <th>QuantitΓ©</th>
529
- <th>Longueur</th>
530
- <th>P.U</th>
531
- <th>Action</th>
532
- </tr>
533
- </thead>
534
- <tbody></tbody>
535
- </table>
536
- </div>
537
- </div>
538
  </div>
539
- </div>
540
- </div>
541
-
542
- <!-- Panneau Modal -->
543
- <div class="modal fade" id="panneauModal" tabindex="-1">
544
- <div class="modal-dialog modal-lg">
545
- <div class="modal-content">
546
- <div class="modal-header">
547
- <h5 class="modal-title">SΓ©lectionner Panneau</h5>
548
- <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
549
- </div>
550
- <div class="modal-body">
551
- <div class="table-responsive">
552
- <table class="table">
553
- <thead>
554
- <tr>
555
- <th>Description</th>
556
- <th>UnitΓ©</th>
557
- <th>QuantitΓ©</th>
558
- <th>Longueur</th>
559
- <th>P.U</th>
560
- <th>Action</th>
561
- </tr>
562
- </thead>
563
- <tbody></tbody>
564
- </table>
565
- </div>
566
- </div>
567
  </div>
568
  </div>
569
- </div>
570
 
571
- <!-- Add Agglos Modal -->
572
- <div class="modal fade" id="agglosModal" tabindex="-1">
573
- <div class="modal-dialog modal-lg">
574
- <div class="modal-content">
575
- <div class="modal-header">
576
- <h5 class="modal-title">SΓ©lectionner Agglos</h5>
577
- <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
 
 
578
  </div>
579
- <div class="modal-body">
580
- <div class="table-responsive">
581
- <table class="table">
582
- <thead>
583
- <tr>
584
- <th>Description</th>
585
- <th>UnitΓ©</th>
586
- <th>QuantitΓ©</th>
587
- <th>Longueur</th>
588
- <th>P.U</th>
589
- <th>Action</th>
590
- </tr>
591
- </thead>
592
- <tbody></tbody>
593
- </table>
594
- </div>
595
  </div>
596
  </div>
597
  </div>
598
- </div>
599
 
600
-
601
- <!-- Confirmation Modal -->
602
- <div class="modal fade" id="confirmationModal" tabindex="-1">
603
- <div class="modal-dialog">
604
- <div class="modal-content">
605
- <div class="modal-header">
606
- <h5 class="modal-title">Article Existant</h5>
607
- <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
608
- </div>
609
- <div class="modal-body">
610
- <p>Cet article existe dΓ©jΓ  dans le tableau. Que souhaitez-vous faire ?</p>
611
  </div>
612
- <div class="modal-footer">
613
- <button type="button" class="btn btn-danger" id="replaceItem">Remplacer</button>
614
- <button type="button" class="btn btn-warning" id="addItem">Ajouter</button>
615
- <button type="button" class="btn btn-info" id="sumItem">Ajouter et Somme</button>
616
- <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuler</button>
617
  </div>
 
618
  </div>
619
  </div>
620
- </div>
621
 
622
-
623
- <!-- Bootstrap JS -->
624
- <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
625
-
626
  <script>
627
- // Define default items at the start of your script
628
- const defaultItems = {
629
- poutrelles: [
630
- { description: 'PCP 158B SISMIQUE', unit: 'ML', quantity: 0, length: 0, unit_price: 0 },
631
- { description: 'PCP 158B', unit: 'ML', quantity: 0, length: 0, unit_price: 0 },
632
- { description: 'PCP 158', unit: 'ML', quantity: 0, length: 0, unit_price: 0 },
633
- { description: 'PCP 130', unit: 'ML', quantity: 0, length: 0, unit_price: 0 },
634
- { description: 'PCP 110', unit: 'ML', quantity: 0, length: 0, unit_price: 0 }
635
- ],
636
- hourdis: [
637
- { description: 'HOURDIS 13+4', unit: 'U', quantity: 0, length: 0, unit_price: 0 },
638
- { description: 'HOURDIS 16+4', unit: 'U', quantity: 0, length: 0, unit_price: 0 },
639
- { description: 'HOURDIS 20+4', unit: 'U', quantity: 0, length: 0, unit_price: 0 }
640
- ],
641
- panneaux: [
642
- { description: 'PTS 3.5X2.1', unit: 'U', quantity: 0, length: 0, unit_price: 0 },
643
- { description: 'PTS 4.2X2.1', unit: 'U', quantity: 0, length: 0, unit_price: 0 }
644
- ],
645
- agglos: [
646
- { description: 'AGGLOS 15', unit: 'U', quantity: 0, length: 0, unit_price: 0 },
647
- { description: 'AGGLOS 20', unit: 'U', quantity: 0, length: 0, unit_price: 0 }
648
- ]
649
- };
650
 
651
- // Add this function at the start of your script, after defaultItems definition
652
- function adjustToNearestStep(input, step) {
653
- // If this is a quantity input, force integer
654
- if (input.name === "quantity") {
655
- const value = parseInt(input.value) || 0;
656
- input.value = value;
657
- } else {
658
- // For other inputs (like length), use decimal step
659
- const value = parseFloat(input.value);
660
- if (!isNaN(value)) {
661
- const nearestStep = Math.round(value / step) * step;
662
- input.value = nearestStep.toFixed(2);
663
- }
664
- }
665
 
666
- // Only call updateTotal if we're in a table row
667
- const row = input.closest('tr');
668
- if (row) {
669
- updateTotal(input);
670
- }
671
- }
672
 
673
- function updateTotal(input) {
674
  try {
675
- const row = input.closest('tr');
676
- if (!row) return;
 
 
 
 
 
 
 
677
 
678
- // Get all required inputs, with null checks
679
- const quantityInput = row.querySelector('input[name="quantity"]');
680
- const lengthInput = row.querySelector('input[name="length"]');
681
- const unitPriceInput = row.querySelector('input[name="unitPrice"]');
682
- const totalInput = row.querySelector('input[name="totalHT"]');
683
 
684
- // Only proceed if all required inputs are found
685
- if (quantityInput && lengthInput && unitPriceInput && totalInput) {
686
- const quantity = parseFloat(quantityInput.value) || 0;
687
- const length = parseFloat(lengthInput.value) || 0;
688
- const unitPrice = parseFloat(unitPriceInput.value) || 0;
689
- const total = (quantity * length * unitPrice).toFixed(2);
690
- totalInput.value = total;
691
- updateGrandTotal();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
692
  }
693
  } catch (error) {
694
- console.error('Error in updateTotal:', error);
 
 
 
 
695
  }
696
  }
697
 
698
- // Function to update the grand total
699
- function updateGrandTotal() {
700
- try {
701
- const totalHT = Array.from(document.querySelectorAll('input[name="totalHT"]'))
702
- .reduce((sum, input) => sum + (parseFloat(input.value) || 0), 0);
703
-
704
- const formattedTotalHT = totalHT.toFixed(2);
705
- const tvaEnabled = document.querySelector("#tvaSwitch")?.checked;
706
-
707
- const tax = tvaEnabled ? (totalHT * 0.20).toFixed(2) : "0.00";
708
- const totalTTC = tvaEnabled ? (totalHT * 1.20).toFixed(2) : formattedTotalHT;
709
-
710
- const totalHTInput = document.querySelector("#totalHT");
711
- const taxInput = document.querySelector("#tax");
712
- const totalTTCInput = document.querySelector("#totalTTC");
713
 
714
- if (totalHTInput) totalHTInput.value = formattedTotalHT;
715
- if (taxInput) taxInput.value = tax;
716
- if (totalTTCInput) totalTTCInput.value = totalTTC;
 
 
 
 
 
 
 
 
 
717
  } catch (error) {
718
- console.error('Error in updateGrandTotal:', error);
 
 
 
 
 
719
  }
720
  }
721
 
722
- document.addEventListener("DOMContentLoaded", function() {
723
- const poutrellesTable = document.getElementById("poutrellesTable");
724
- const hourdisTable = document.getElementById("hourdisTable");
725
- const panneauTable = document.getElementById("panneauTable");
726
- const agglosTable = document.getElementById("agglosTable");
727
- const invoiceForm = document.getElementById("invoiceForm");
728
- const invoiceNumberInput = document.getElementById("invoiceNumber");
729
 
730
- // Default items data - Reset all default values to zero
731
- const defaultItems = {
732
- poutrelles: [
733
- { description: "PCP 114N", unit: "ML", quantity: 0, length: 0, unit_price: 0 },
734
- { description: "PCP 113N", unit: "ML", quantity: 0, length: 0, unit_price: 0 }
735
- ],
736
- hourdis: [
737
- { description: "HOURDIS TYPE 12", unit: "U", quantity: 0, length: 0, unit_price: 0 }
738
- ],
739
- panneaux: [
740
- { description: "PTS SISMIQUE 5*3,5", unit: "U", quantity: 0, length: 0, unit_price: 0 }
741
- ]
742
- };
743
 
744
-
745
- // Function to add a new item row
746
- function addItemRow(item, table) {
747
- console.log('Adding item to table:', item);
 
 
 
 
 
 
 
 
748
 
749
- // Check for existing item with same description AND length
750
- const existingRow = Array.from(table.querySelectorAll('tr')).find(row => {
751
- const existingDesc = row.querySelector('input[name="description"]').value;
752
- const existingLength = parseFloat(row.querySelector('input[name="length"]').value);
753
- return existingDesc === item.description && existingLength === item.length;
754
- });
 
 
 
755
 
756
- if (existingRow) {
757
- // Show confirmation modal for duplicate items
758
- const confirmationModal = new bootstrap.Modal(document.getElementById('confirmationModal'));
759
- document.querySelector('#confirmationModal .modal-body').innerHTML = `
760
- <p>Un article avec les mΓͺmes caractΓ©ristiques existe dΓ©jΓ  :</p>
761
- <p><strong>Description :</strong> ${item.description}</p>
762
- <p><strong>Longueur :</strong> ${item.length.toFixed(2)}</p>
763
- <p>Voulez-vous remplacer l'existant ou annuler ?</p>
764
- `;
765
-
766
- // Handle replace action
767
- document.getElementById('replaceItem').onclick = () => {
768
- existingRow.remove();
769
- insertNewRow(item, table);
770
- confirmationModal.hide();
771
- };
772
 
773
- confirmationModal.show();
774
- } else {
775
- // If no duplicate, add the new row
776
- insertNewRow(item, table);
777
- }
 
778
 
779
- updateGrandTotal();
 
 
780
  }
781
 
782
- // Modify the sort function to handle both type and length
783
- function sortItemsByDescriptionAndLength(items) {
784
- return items.sort((a, b) => {
785
- // Helper function to extract numbers from description
786
- const getNumber = (desc) => {
787
- const match = desc.match(/\d+/);
788
- return match ? parseInt(match[0]) : 0;
789
- };
790
-
791
- // Helper function to get the type (SISMIQUE or not)
792
- const getType = (desc) => {
793
- return desc.includes('SISMIQUE') ? 1 : 0;
794
- };
795
-
796
- // Helper function to check if item ends with N
797
- const endsWithN = (desc) => {
798
- return desc.trim().endsWith('N') ? 0 : 1; // 0 for N (to sort first), 1 for others
799
- };
800
-
801
- const numA = getNumber(a.description);
802
- const numB = getNumber(b.description);
803
- const typeA = getType(a.description);
804
- const typeB = getType(b.description);
805
- const nEndingA = endsWithN(a.description);
806
- const nEndingB = endsWithN(b.description);
807
-
808
- // First sort by N ending
809
- if (nEndingA !== nEndingB) {
810
- return nEndingA - nEndingB;
811
- }
812
 
813
- // Then sort by description number
814
- if (numA !== numB) {
815
- return numA - numB;
816
- }
817
-
818
- // Then sort by type (non-SISMIQUE first)
819
- if (typeA !== typeB) {
820
- return typeA - typeB;
821
- }
822
-
823
- // Finally sort by length
824
- return a.length - b.length;
825
  });
826
- }
827
 
828
- function insertNewRow(item, table) {
829
- // Get all existing rows as array of items
830
- const existingItems = Array.from(table.querySelectorAll('tr')).map(row => ({
831
- description: row.querySelector('input[name="description"]').value,
832
- unit: row.querySelector('input[name="unit"]').value,
833
- quantity: parseInt(row.querySelector('input[name="quantity"]').value, 10),
834
- length: parseFloat(row.querySelector('input[name="length"]').value),
835
- unit_price: parseFloat(row.querySelector('input[name="unitPrice"]').value),
836
- total_price: parseFloat(row.querySelector('input[name="totalHT"]').value)
837
- }));
838
-
839
- // Add new item to array
840
- existingItems.push(item);
841
-
842
- // Sort all items
843
- const sortedItems = sortItemsByDescriptionAndLength(existingItems);
844
-
845
- // Clear table
846
- table.innerHTML = '';
847
-
848
- // Insert all items in sorted order
849
- sortedItems.forEach(sortedItem => {
850
- const row = document.createElement('tr');
851
- const isPoutrelles = sortedItem.description.includes('PCP');
852
-
853
- let lengthAttrs;
854
- if (isPoutrelles) {
855
- lengthAttrs = 'step="0.05" type="number" min="0" oninput="adjustToNearestStep(this, 0.05)"';
856
- } else {
857
- lengthAttrs = 'step="1" type="number" min="0" onkeydown="return event.keyCode !== 190" oninput="this.value=Math.floor(this.value)"';
858
- }
859
-
860
- const total = ((sortedItem.quantity || 0) * (sortedItem.length || 0) * (sortedItem.unit_price || 0)).toFixed(2);
861
-
862
- row.innerHTML = `
863
- <td>
864
- <input type="text" class="form-control" name="description" value="${sortedItem.description || ''}" readonly>
865
- </td>
866
- <td>
867
- <input type="text" class="form-control" name="unit" value="${sortedItem.unit || ''}" readonly>
868
- </td>
869
- <td>
870
- <input step="1" type="number" min="0" class="form-control" name="quantity"
871
- value="${sortedItem.quantity || 0}"
872
- onkeydown="return event.keyCode !== 190 && event.keyCode !== 188"
873
- oninput="this.value=Math.floor(this.value)"
874
- onchange="updateTotal(this)">
875
- </td>
876
- <td>
877
- <input ${lengthAttrs} class="form-control" name="length"
878
- value="${sortedItem.length || 0}"
879
- onchange="updateTotal(this)">
880
- </td>
881
- <td>
882
- <input type="number" class="form-control" name="unitPrice"
883
- value="${sortedItem.unit_price || 0}"
884
- step="0.01" min="0"
885
- onchange="updateTotal(this)">
886
- </td>
887
- <td>
888
- <input type="number" class="form-control" name="totalHT" value="${total}" readonly>
889
- </td>
890
- <td>
891
- <button type="button" class="btn btn-danger btn-sm" onclick="this.closest('tr').remove(); updateGrandTotal();">
892
- <i class="fas fa-trash"></i>
893
- </button>
894
- </td>
895
- `;
896
-
897
- table.appendChild(row);
898
  });
899
-
900
- updateGrandTotal();
901
- }
902
 
903
- // Function to update totals
904
- function updateTotals() {
905
- const totalHT = Array.from(document.querySelectorAll('input[name="totalHT"]'))
906
- .reduce((sum, input) => sum + (parseFloat(input.value) || 0), 0);
907
 
908
- const formattedTotalHT = totalHT.toFixed(2);
909
- const tvaEnabled = document.querySelector("#tvaSwitch").checked;
910
-
911
- const tax = tvaEnabled ? (totalHT * 0.20).toFixed(2) : "0.00";
912
- const totalTTC = tvaEnabled ? (totalHT * 1.20).toFixed(2) : "0.00";
913
-
914
- document.querySelector("#totalHT").value = formattedTotalHT;
915
- document.querySelector("#tax").value = tax;
916
- document.querySelector("#totalTTC").value = totalTTC;
917
  }
 
918
 
919
- // Add event listener for TVA switch
920
- document.querySelector("#tvaSwitch").addEventListener('change', updateTotals);
921
-
922
- // Remove the automatic initialization of tables with default items
923
- // Comment out or remove these lines:
924
- /*
925
- defaultItems.poutrelles.forEach(item => addItemRow(item, poutrellesTable));
926
- defaultItems.hourdis.forEach(item => addItemRow(item, hourdisTable));
927
- defaultItems.panneaux.forEach(item => addItemRow(item, panneauTable));
928
- */
929
-
930
- // Predefined items data - Reset all default values to zero
931
- const predefinedItems = {
932
- poutrelles: [
933
- { description: "PCP 113N", unit: "ML", quantity: 0, length: 0, unit_price: 0 },
934
- { description: "PCP 114N", unit: "ML", quantity: 0, length: 0, unit_price: 0 },
935
- { description: "PCP 113B SISMIQUE", unit: "ML", quantity: 0, length: 0, unit_price: 0 },
936
- { description: "PCP 114B SISMIQUE", unit: "ML", quantity: 0, length: 0, unit_price: 0 },
937
- { description: "PCP 135 SISMIQUE", unit: "ML", quantity: 0, length: 0, unit_price: 0 },
938
- { description: "PCP 156 SISMIQUE", unit: "ML", quantity: 0, length: 0, unit_price: 0 },
939
- { description: "PCP 157 SISMIQUE", unit: "ML", quantity: 0, length: 0, unit_price: 0 },
940
- { description: "PCP 158 SISMIQUE", unit: "ML", quantity: 0, length: 0, unit_price: 0 },
941
- { description: "PCP 158B SISMIQUE", unit: "ML", quantity: 0, length: 0, unit_price: 0 }
942
- ],
943
- hourdis: [
944
- { description: "HOURDIS TYPE 08", unit: "U", quantity: 0, length: 0, unit_price: 0 },
945
- { description: "HOURDIS TYPE 12", unit: "U", quantity: 0, length: 0, unit_price: 0 },
946
- { description: "HOURDIS TYPE 16", unit: "U", quantity: 0, length: 0, unit_price: 0 },
947
- { description: "HOURDIS TYPE 20", unit: "U", quantity: 0, length: 0, unit_price: 0 },
948
- { description: "HOURDIS TYPE 25", unit: "U", quantity: 0, length: 0, unit_price: 0 },
949
- { description: "HOURDIS TYPE 30", unit: "U", quantity: 0, length: 0, unit_price: 0 }
950
- ],
951
- panneaux: [
952
- { description: "PTS Normal 3,5*3,5", unit: "U", quantity: 0, length: 0, unit_price: 0 },
953
- { description: "PTS SISMIQUE 5,0*3,5", unit: "U", quantity: 0, length: 0, unit_price: 0 }
954
- ],
955
- agglos: [
956
- { description: "AGGLOS TYPE 07", unit: "U", quantity: 0, length: 0, unit_price: 0 },
957
- { description: "AGGLOS TYPE 10", unit: "U", quantity: 0, length: 0, unit_price: 0 },
958
- { description: "AGGLOS TYPE 15", unit: "U", quantity: 0, length: 0, unit_price: 0 },
959
- { description: "AGGLOS TYPE 20", unit: "U", quantity: 0, length: 0, unit_price: 0 },
960
- { description: "AGGLOS TYPE 25", unit: "U", quantity: 0, length: 0, unit_price: 0 },
961
- { description: "AGGLOS A BRANCHER 20", unit: "U", quantity: 0, length: 0, unit_price: 0 }
962
- ]
963
- };
964
 
965
- // Function to populate modal with items
966
- function populateModal(modalId, items, targetTable) {
967
- const modal = document.querySelector(modalId);
968
- const tbody = modal.querySelector('tbody');
969
- tbody.innerHTML = '';
970
 
971
- const sortedItems = sortItemsByDescriptionAndLength([...items]);
 
 
972
 
973
- sortedItems.forEach((item, index) => {
974
- const row = document.createElement('tr');
975
- const isPoutrelles = item.description.includes('PCP');
976
- const baseTabIndex = (index + 1) * 10; // Create unique tabindex groups for each row
977
-
978
- row.innerHTML = `
979
- <td>${item.description}</td>
980
- <td>${item.unit}</td>
981
- <td>
982
- <input type="number"
983
- class="form-control form-control-sm"
984
- value=""
985
- name="modal-quantity"
986
- min="0"
987
- step="1"
988
- placeholder="QuantitΓ©"
989
- tabindex="${baseTabIndex}"
990
- onkeydown="if(event.key === 'Enter') { event.preventDefault(); this.closest('tr').querySelector('[name=modal-length]').focus(); }">
991
- <div class="invalid-feedback">Veuillez entrer une quantitΓ© valide.</div>
992
- </td>
993
- <td>
994
- <input type="text"
995
- class="form-control form-control-sm"
996
- value=""
997
- name="modal-length"
998
- placeholder="Longueur"
999
- tabindex="${baseTabIndex + 1}"
1000
- onkeydown="if(event.key === 'Enter') { event.preventDefault(); this.closest('tr').querySelector('[name=modal-price]').focus(); }"
1001
- oninput="this.value = this.value.replace(',', '.');">
1002
- <div class="invalid-feedback">Veuillez entrer une longueur valide.</div>
1003
- </td>
1004
- <td>
1005
- <input type="text"
1006
- class="form-control form-control-sm"
1007
- value=""
1008
- name="modal-price"
1009
- placeholder="Prix Unitaire"
1010
- tabindex="${baseTabIndex + 2}"
1011
- onkeydown="if(event.key === 'Enter') { event.preventDefault(); this.closest('tr').querySelector('.add-item').click(); }"
1012
- oninput="this.value = this.value.replace(',', '.');">
1013
- <div class="invalid-feedback">Veuillez entrer un prix unitaire valide.</div>
1014
- </td>
1015
- <td>
1016
- <button class="btn btn-primary btn-sm add-item" tabindex="${baseTabIndex + 3}">
1017
- Ajouter
1018
- </button>
1019
- </td>
1020
- `;
1021
-
1022
- // Add click handler for the add button
1023
- row.querySelector('.add-item').addEventListener('click', () => {
1024
- const quantityInput = row.querySelector('[name="modal-quantity"]');
1025
- const lengthInput = row.querySelector('[name="modal-length"]');
1026
- const unitPriceInput = row.querySelector('[name="modal-price"]');
1027
-
1028
- const quantity = parseInt(quantityInput.value) || 0;
1029
- const length = parseFloat(lengthInput.value.replace(',', '.')) || 0;
1030
- const unitPrice = parseFloat(unitPriceInput.value.replace(',', '.')) || 0;
1031
-
1032
- let isValid = true;
1033
-
1034
- if (quantity === 0) {
1035
- quantityInput.classList.add('is-invalid');
1036
- isValid = false;
1037
- } else {
1038
- quantityInput.classList.remove('is-invalid');
1039
- }
1040
-
1041
- if (length === 0) {
1042
- lengthInput.classList.add('is-invalid');
1043
- isValid = false;
1044
- } else {
1045
- lengthInput.classList.remove('is-invalid');
1046
- }
1047
-
1048
- if (unitPrice === 0) {
1049
- unitPriceInput.classList.add('is-invalid');
1050
- isValid = false;
1051
- } else {
1052
- unitPriceInput.classList.remove('is-invalid');
1053
- }
1054
-
1055
- if (!isValid) {
1056
- return;
1057
- }
1058
-
1059
- const newItem = {
1060
- ...item,
1061
- quantity: quantity,
1062
- length: length,
1063
- unit_price: unitPrice
1064
- };
1065
- addItemRow(newItem, targetTable);
1066
- // The modal will remain open, allowing the user to add more items
1067
- });
1068
-
1069
- tbody.appendChild(row);
1070
- });
1071
- }
1072
-
1073
- // Update button click handlers
1074
- document.querySelector("#addPoutrelles").addEventListener("click", () => {
1075
- populateModal('#poutrellesModal', predefinedItems.poutrelles, poutrellesTable);
1076
- new bootstrap.Modal('#poutrellesModal').show();
1077
- });
1078
-
1079
- document.querySelector("#addHourdis").addEventListener("click", () => {
1080
- populateModal('#hourdisModal', predefinedItems.hourdis, hourdisTable);
1081
- new bootstrap.Modal('#hourdisModal').show();
1082
- });
1083
-
1084
- document.querySelector("#addPanneau").addEventListener("click", () => {
1085
- populateModal('#panneauModal', predefinedItems.panneaux, panneauTable);
1086
- new bootstrap.Modal('#panneauModal').show();
1087
- });
1088
-
1089
- document.querySelector("#addAgglos").addEventListener("click", () => {
1090
- populateModal('#agglosModal', predefinedItems.agglos, agglosTable);
1091
- new bootstrap.Modal('#agglosModal').show();
1092
- });
1093
-
1094
- // Form submission
1095
- invoiceForm.addEventListener("submit", async (event) => {
1096
- event.preventDefault();
1097
-
1098
- const urlParams = new URLSearchParams(window.location.search);
1099
- const editId = urlParams.get('edit');
1100
-
1101
- const getAllItems = () => {
1102
- const items = [];
1103
- [poutrellesTable, hourdisTable, panneauTable, agglosTable].forEach(table => {
1104
- items.push(...Array.from(table.querySelectorAll("tr")).map(row => ({
1105
- description: row.querySelector("input[name='description']").value,
1106
- unit: row.querySelector("input[name='unit']").value,
1107
- quantity: parseInt(row.querySelector("input[name='quantity']").value, 10),
1108
- length: parseFloat(row.querySelector("input[name='length']").value),
1109
- unit_price: parseFloat(row.querySelector("input[name='unitPrice']").value),
1110
- total_price: parseFloat(row.querySelector("input[name='totalHT']").value)
1111
- })));
1112
- });
1113
- return items;
1114
- };
1115
-
1116
- const data = {
1117
- invoice_number: document.querySelector("#invoiceNumber").value,
1118
- date: new Date(document.querySelector("#date").value).toISOString(),
1119
- project: document.querySelector("#project").value,
1120
- client_name: document.querySelector("#clientName").value,
1121
- phone1: document.querySelector("#clientPhone").value,
1122
- phone2: document.querySelector("#clientPhone2").value || null,
1123
- address: document.querySelector("#clientAddress").value,
1124
- total_ht: parseFloat(document.querySelector("#totalHT").value || 0),
1125
- tax: parseFloat(document.querySelector("#tax").value || 0),
1126
- total_ttc: parseFloat(document.querySelector("#totalTTC").value || 0),
1127
- items: getAllItems(),
1128
- frame_number: document.querySelector("#plancher").value || "PH RDC",
1129
- status: "pending",
1130
- created_at: new Date().toISOString(),
1131
- commercial: document.querySelector("#commercial").value || "divers"
1132
- };
1133
-
1134
- console.log("Sending invoice data:", data);
1135
-
1136
- try {
1137
- // Determine if we're creating or updating
1138
- const url = editId ? `/api/invoices/${editId}` : "/api/invoices/";
1139
- const method = editId ? "PUT" : "POST";
1140
-
1141
- const response = await fetch(url, {
1142
- method: method,
1143
- headers: { "Content-Type": "application/json" },
1144
- body: JSON.stringify(data)
1145
- });
1146
-
1147
- if (!response.ok) {
1148
- const errorData = await response.json();
1149
- console.error("Server error:", errorData);
1150
- throw new Error(`Failed to ${editId ? 'update' : 'create'} invoice: ${JSON.stringify(errorData)}`);
1151
- }
1152
-
1153
- const invoice = await response.json();
1154
- console.log(editId ? "Updated invoice:" : "Created invoice:", invoice);
1155
-
1156
- // Generate PDF
1157
- const pdfResponse = await fetch(`/api/invoices/${invoice.id}/generate-pdf`, {
1158
- method: "POST",
1159
- headers: { "Content-Type": "application/json" },
1160
- body: JSON.stringify(invoice)
1161
- });
1162
-
1163
- if (!pdfResponse.ok) {
1164
- const errorData = await pdfResponse.text();
1165
- console.error("PDF generation error:", errorData);
1166
- throw new Error(`Failed to generate PDF: ${errorData}`);
1167
- }
1168
-
1169
- // Handle PDF download
1170
- const blob = await pdfResponse.blob();
1171
- const downloadUrl = window.URL.createObjectURL(blob);
1172
- const a = document.createElement("a");
1173
- a.href = downloadUrl;
1174
- a.download = `devis_${invoice.invoice_number}.pdf`;
1175
- document.body.appendChild(a);
1176
- a.click();
1177
- document.body.removeChild(a);
1178
- window.URL.revokeObjectURL(downloadUrl);
1179
-
1180
- // Update status to success after PDF generation
1181
- try {
1182
- const updateStatusResponse = await fetch(`/api/invoices/${invoice.id}/status`, {
1183
- method: "PUT",
1184
- headers: { "Content-Type": "application/json" },
1185
- body: JSON.stringify({ status: "completed" })
1186
- });
1187
-
1188
- if (!updateStatusResponse.ok) {
1189
- console.error("Failed to update status:", await updateStatusResponse.text());
1190
- } else {
1191
- console.log("Status updated successfully");
1192
- }
1193
- } catch (error) {
1194
- console.error("Error updating status:", error);
1195
- }
1196
-
1197
- // Redirect to history page after successful update
1198
- if (editId) {
1199
- window.location.href = '/history';
1200
- }
1201
-
1202
- } catch (error) {
1203
- console.error("Error:", error);
1204
- alert(`Error: ${error.message}`);
1205
- }
1206
- });
1207
-
1208
- // Set today's date automatically
1209
- const today = new Date();
1210
- const formattedDate = today.toISOString().split('T')[0];
1211
- document.querySelector("#date").value = formattedDate;
1212
-
1213
- // Function to get and update the invoice number
1214
- async function updateInvoiceNumber(selectedType) {
1215
- try {
1216
- // Disable the input while fetching
1217
- invoiceNumberInput.disabled = true;
1218
-
1219
- const response = await fetch(`/api/invoices/last-number/${selectedType}`);
1220
-
1221
- if (response.ok) {
1222
- const data = await response.json();
1223
- invoiceNumberInput.value = data.formatted_number;
1224
- } else {
1225
- throw new Error('Failed to get invoice number');
1226
- }
1227
- } catch (error) {
1228
- console.error('Error:', error);
1229
- alert('Error getting invoice number');
1230
- } finally {
1231
- // Re-enable the input
1232
- invoiceNumberInput.disabled = false;
1233
  }
1234
- }
1235
-
1236
- // Update invoice number when client type is selected
1237
- document.querySelectorAll('input[name="clientType"]').forEach(input => {
1238
- input.addEventListener('change', () => {
1239
- updateInvoiceNumber(input.value);
1240
- });
1241
- });
1242
-
1243
- // Update the number when the page loads if a type is already selected
1244
- const selectedType = document.querySelector('input[name="clientType"]:checked')?.value;
1245
- if (selectedType) {
1246
- updateInvoiceNumber(selectedType);
1247
- }
1248
-
1249
- // Replace the Excel generation handler with this updated version
1250
- document.getElementById('generateExcel').addEventListener('click', async () => {
1251
- try {
1252
- console.log('Starting Excel generation process...');
1253
-
1254
- // Get all form data
1255
- const getAllItems = () => {
1256
- const items = [];
1257
- [poutrellesTable, hourdisTable, panneauTable, agglosTable].forEach(table => {
1258
- const tableItems = Array.from(table.querySelectorAll("tr")).map(row => ({
1259
- description: row.querySelector("input[name='description']").value,
1260
- unit: row.querySelector("input[name='unit']").value,
1261
- quantity: parseInt(row.querySelector("input[name='quantity']").value, 10),
1262
- length: parseFloat(row.querySelector("input[name='length']").value),
1263
- unit_price: parseFloat(row.querySelector("input[name='unitPrice']").value),
1264
- total_price: parseFloat(row.querySelector("input[name='totalHT']").value)
1265
- }));
1266
- console.log(`Found ${tableItems.length} items in ${table.id}`);
1267
- items.push(...tableItems);
1268
- });
1269
- return items;
1270
- };
1271
-
1272
- const data = {
1273
- invoice_number: document.querySelector("#invoiceNumber").value,
1274
- date: new Date(document.querySelector("#date").value).toISOString(),
1275
- project: document.querySelector("#project").value,
1276
- client_name: document.querySelector("#clientName").value,
1277
- phone1: document.querySelector("#clientPhone").value,
1278
- phone2: document.querySelector("#clientPhone2").value || null,
1279
- address: document.querySelector("#clientAddress").value,
1280
- total_ht: parseFloat(document.querySelector("#totalHT").value || 0),
1281
- tax: parseFloat(document.querySelector("#tax").value || 0),
1282
- total_ttc: parseFloat(document.querySelector("#totalTTC").value || 0),
1283
- items: getAllItems(),
1284
- frame_number: document.querySelector("#plancher").value || "PH RDC",
1285
- status: "pending",
1286
- created_at: new Date().toISOString(),
1287
- commercial: document.querySelector("#commercial").value || "divers"
1288
- };
1289
 
1290
- console.log('Form data collected:', {
1291
- invoice_number: data.invoice_number,
1292
- client_name: data.client_name,
1293
- items_count: data.items.length,
1294
- total_ttc: data.total_ttc
1295
- });
1296
-
1297
- // First create the invoice if it doesn't exist
1298
- console.log('Creating invoice...');
1299
- const createResponse = await fetch("/api/invoices/", {
1300
- method: "POST",
1301
- headers: { "Content-Type": "application/json" },
1302
- body: JSON.stringify(data)
1303
- });
1304
-
1305
- if (!createResponse.ok) {
1306
- throw new Error('Failed to create invoice');
1307
- }
1308
-
1309
- const invoice = await createResponse.json();
1310
- console.log("Created invoice:", invoice.id);
1311
-
1312
- // Then generate the Excel file
1313
- console.log('Generating Excel file...');
1314
- const response = await fetch(`/api/invoices/${invoice.id}/generate-excel`, {
1315
- method: 'POST',
1316
- headers: { 'Content-Type': 'application/json' },
1317
- body: JSON.stringify(invoice)
1318
- });
1319
-
1320
- if (!response.ok) {
1321
- throw new Error('Failed to generate Excel file');
1322
- }
1323
-
1324
- console.log('Excel file generated successfully');
1325
-
1326
- // Handle file download
1327
- const blob = await response.blob();
1328
- const url = window.URL.createObjectURL(blob);
1329
- const a = document.createElement('a');
1330
- a.href = url;
1331
- a.download = `devis_${invoice.invoice_number}.xlsx`;
1332
- document.body.appendChild(a);
1333
- a.click();
1334
- document.body.removeChild(a);
1335
- window.URL.revokeObjectURL(url);
1336
- console.log('Excel file downloaded');
1337
-
1338
- // Update invoice number after successful generation
1339
- const selectedType = document.querySelector('input[name="clientType"]:checked')?.value;
1340
- if (selectedType) {
1341
- console.log('Updating invoice number...');
1342
- await updateInvoiceNumber(selectedType);
1343
- }
1344
-
1345
- console.log('Excel generation process completed successfully');
1346
-
1347
- } catch (error) {
1348
- console.error('Error in Excel generation:', error);
1349
- alert('Error generating Excel file: ' + error.message);
1350
- }
1351
  });
1352
 
1353
- // Check for edit mode
1354
- const urlParams = new URLSearchParams(window.location.search);
1355
- const editId = urlParams.get('edit');
1356
-
1357
- if (editId) {
1358
- console.log('Loading invoice for editing, ID:', editId);
1359
- fetch(`/api/invoices/${editId}`)
1360
- .then(response => response.json())
1361
- .then(invoice => {
1362
- console.log('Loaded invoice data:', invoice);
1363
-
1364
- // Populate form fields
1365
- document.querySelector("#clientName").value = invoice.client_name;
1366
- document.querySelector("#clientPhone").value = invoice.phone1;
1367
- document.querySelector("#clientPhone2").value = invoice.phone2;
1368
- document.querySelector("#clientAddress").value = invoice.address;
1369
- document.querySelector("#plancher").value = invoice.frame_number || 'PH RDC';
1370
- document.querySelector("#invoiceNumber").value = invoice.invoice_number;
1371
- document.querySelector("#date").value = invoice.date.split('T')[0];
1372
-
1373
- // Clear existing tables first
1374
- console.log('Clearing existing tables');
1375
- [poutrellesTable, hourdisTable, panneauTable, agglosTable].forEach(table => {
1376
- table.innerHTML = '';
1377
- });
1378
-
1379
- // Populate items
1380
- console.log('Populating items:', invoice.items);
1381
- invoice.items.forEach(item => {
1382
- console.log('Processing item:', item);
1383
- if (item.description.includes('PCP')) {
1384
- console.log('Adding to poutrelles table');
1385
- addItemRow(item, poutrellesTable);
1386
- } else if (item.description.includes('HOURDIS')) {
1387
- console.log('Adding to hourdis table');
1388
- addItemRow(item, hourdisTable);
1389
- } else if (item.description.includes('PTS')) {
1390
- console.log('Adding to panneau table');
1391
- addItemRow(item, panneauTable);
1392
- } else {
1393
- console.log('Adding to agglos table');
1394
- addItemRow(item, agglosTable);
1395
- }
1396
- });
1397
- })
1398
- .catch(error => {
1399
- console.error("Error loading invoice:", error);
1400
- alert("Error loading invoice data");
1401
- });
1402
- }
1403
-
1404
- // Update current date in header
1405
- document.getElementById('currentDate').textContent = new Date().toLocaleDateString('fr-FR');
1406
- });
1407
  </script>
1408
  </body>
1409
  </html>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>F5 Model Test Interface</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
 
 
8
  <style>
9
+ .loading {
10
+ display: none;
11
+ position: relative;
12
+ width: 80px;
13
+ height: 80px;
14
+ margin: 0 auto;
15
+ }
16
+ .loading div {
17
+ position: absolute;
18
+ width: 16px;
19
+ height: 16px;
20
+ border-radius: 50%;
21
+ background: #4B5563;
22
+ animation: loading 1.2s linear infinite;
23
+ }
24
+ .loading div:nth-child(1) {
25
+ top: 8px;
26
+ left: 8px;
27
+ animation-delay: 0s;
28
+ }
29
+ .loading div:nth-child(2) {
30
+ top: 8px;
31
+ left: 32px;
32
+ animation-delay: -0.4s;
33
+ }
34
+ .loading div:nth-child(3) {
35
+ top: 8px;
36
+ left: 56px;
37
+ animation-delay: -0.8s;
38
+ }
39
+ @keyframes loading {
40
+ 0%, 100% { opacity: 1; }
41
+ 50% { opacity: 0.5; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  }
43
  </style>
44
  </head>
45
+ <body class="bg-gray-100">
46
+ <div class="container mx-auto px-4 py-8">
47
+ <h1 class="text-3xl font-bold mb-8">F5 Model Test Interface</h1>
48
+
49
+ <!-- Project Plan Generation Section -->
50
+ <div class="bg-white rounded-lg shadow-md p-6 mb-8">
51
+ <h2 class="text-xl font-semibold mb-4">Project Plan Generation</h2>
52
+ <div class="space-y-4">
53
+ <div>
54
+ <label class="block text-sm font-medium text-gray-700 mb-1">Project Title</label>
55
+ <input type="text" id="projectTitle" class="w-full border rounded px-3 py-2" placeholder="Enter project title">
56
  </div>
57
+ <div>
58
+ <label class="block text-sm font-medium text-gray-700 mb-1">Requirements</label>
59
+ <textarea id="planRequirements" class="w-full border rounded px-3 py-2 h-32" placeholder="Enter project requirements"></textarea>
 
 
60
  </div>
61
+ <div>
62
+ <label class="block text-sm font-medium text-gray-700 mb-1">Features (comma-separated)</label>
63
+ <input type="text" id="planFeatures" class="w-full border rounded px-3 py-2" placeholder="Feature 1, Feature 2, Feature 3">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  </div>
65
+ <div>
66
+ <label class="block text-sm font-medium text-gray-700 mb-1">Platform</label>
67
+ <select id="platform" class="w-full border rounded px-3 py-2">
68
+ <option value="AWS">AWS</option>
69
+ <option value="Azure">Azure</option>
70
+ <option value="GCP">Google Cloud</option>
71
+ </select>
 
 
 
 
 
 
 
72
  </div>
73
+ <div>
74
+ <label class="block text-sm font-medium text-gray-700 mb-1">Additional Requirements</label>
75
+ <textarea id="additionalRequirements" class="w-full border rounded px-3 py-2 h-24" placeholder="Enter any additional requirements"></textarea>
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  </div>
77
+ <button onclick="generatePlan()" class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">
78
+ Generate Plan
79
+ </button>
80
  </div>
81
+ <div id="planLoading" class="loading mt-4">
82
+ <div></div>
83
+ <div></div>
84
+ <div></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  </div>
86
+ <div id="planOutput" class="mt-6 bg-gray-50 rounded p-4 hidden">
87
+ <h3 class="text-lg font-semibold mb-2" id="planTitle"></h3>
88
+ <div id="planSections" class="space-y-4"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  </div>
90
  </div>
 
91
 
92
+ <!-- Chat Section -->
93
+ <div class="bg-white rounded-lg shadow-md p-6 mb-8">
94
+ <h2 class="text-xl font-semibold mb-4">Chat Test</h2>
95
+ <div class="mb-4">
96
+ <div id="chatHistory" class="bg-gray-50 rounded p-4 h-64 overflow-y-auto mb-4"></div>
97
+ <div id="chatLoading" class="loading mb-4">
98
+ <div></div>
99
+ <div></div>
100
+ <div></div>
101
  </div>
102
+ <div class="flex gap-2">
103
+ <input type="text" id="chatInput"
104
+ class="flex-1 border rounded px-3 py-2"
105
+ placeholder="Type your message...">
106
+ <button onclick="sendMessage()"
107
+ class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">
108
+ Send
109
+ </button>
110
+ <label class="flex items-center gap-2">
111
+ <input type="checkbox" id="streamToggle">
112
+ Stream
113
+ </label>
 
 
 
 
114
  </div>
115
  </div>
116
  </div>
 
117
 
118
+ <!-- Feature Generation Section -->
119
+ <div class="bg-white rounded-lg shadow-md p-6">
120
+ <h2 class="text-xl font-semibold mb-4">Feature Generation Test</h2>
121
+ <div class="mb-4">
122
+ <textarea id="requirementsInput"
123
+ class="w-full border rounded p-3 mb-4 h-32"
124
+ placeholder="Enter requirements..."></textarea>
125
+ <button onclick="generateFeatures()"
126
+ class="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600">
127
+ Generate Features
128
+ </button>
129
  </div>
130
+ <div id="featuresLoading" class="loading mb-4">
131
+ <div></div>
132
+ <div></div>
133
+ <div></div>
 
134
  </div>
135
+ <div id="featuresOutput" class="bg-gray-50 rounded p-4 min-h-32"></div>
136
  </div>
137
  </div>
 
138
 
 
 
 
 
139
  <script>
140
+ let chatHistory = [];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
 
142
+ async function sendMessage() {
143
+ const input = document.getElementById('chatInput');
144
+ const message = input.value.trim();
145
+ if (!message) return;
146
+
147
+ // Show loading indicator
148
+ document.getElementById('chatLoading').style.display = 'block';
 
 
 
 
 
 
 
149
 
150
+ // Add user message to chat
151
+ appendMessage('user', message);
152
+ input.value = '';
 
 
 
153
 
154
+ const streamMode = document.getElementById('streamToggle').checked;
155
  try {
156
+ if (streamMode) {
157
+ const response = await fetch('/chat', {
158
+ method: 'POST',
159
+ headers: { 'Content-Type': 'application/json' },
160
+ body: JSON.stringify({
161
+ messages: chatHistory,
162
+ stream: true
163
+ })
164
+ });
165
 
166
+ const reader = response.body.getReader();
167
+ const decoder = new TextDecoder();
168
+ let assistantMessage = '';
 
 
169
 
170
+ while (true) {
171
+ const { value, done } = await reader.read();
172
+ if (done) break;
173
+
174
+ const chunk = decoder.decode(value);
175
+ const lines = chunk.split('\n');
176
+ for (const line of lines) {
177
+ if (line.startsWith('data: ')) {
178
+ const content = line.slice(6);
179
+ assistantMessage += content;
180
+ updateLastMessage('assistant', assistantMessage);
181
+ }
182
+ }
183
+ }
184
+ } else {
185
+ const response = await fetch('/chat', {
186
+ method: 'POST',
187
+ headers: { 'Content-Type': 'application/json' },
188
+ body: JSON.stringify({
189
+ messages: chatHistory,
190
+ stream: false
191
+ })
192
+ });
193
+ const data = await response.json();
194
+ appendMessage('assistant', data.response);
195
  }
196
  } catch (error) {
197
+ console.error('Error:', error);
198
+ appendMessage('system', 'Error: Failed to get response');
199
+ } finally {
200
+ // Hide loading indicator
201
+ document.getElementById('chatLoading').style.display = 'none';
202
  }
203
  }
204
 
205
+ async function generateFeatures() {
206
+ const requirements = document.getElementById('requirementsInput').value.trim();
207
+ if (!requirements) return;
 
 
 
 
 
 
 
 
 
 
 
 
208
 
209
+ // Show loading indicator
210
+ document.getElementById('featuresLoading').style.display = 'block';
211
+ document.getElementById('featuresOutput').innerHTML = '';
212
+
213
+ try {
214
+ const response = await fetch('/generate-features', {
215
+ method: 'POST',
216
+ headers: { 'Content-Type': 'application/json' },
217
+ body: JSON.stringify({ requirements })
218
+ });
219
+ const data = await response.json();
220
+ displayFeatures(data.features);
221
  } catch (error) {
222
+ console.error('Error:', error);
223
+ document.getElementById('featuresOutput').innerHTML =
224
+ '<p class="text-red-500">Error generating features</p>';
225
+ } finally {
226
+ // Hide loading indicator
227
+ document.getElementById('featuresLoading').style.display = 'none';
228
  }
229
  }
230
 
231
+ function appendMessage(role, content) {
232
+ chatHistory.push({ role, content });
233
+ updateChatDisplay();
234
+ }
 
 
 
235
 
236
+ function updateLastMessage(role, content) {
237
+ if (chatHistory.length > 0 && chatHistory[chatHistory.length - 1].role === role) {
238
+ chatHistory[chatHistory.length - 1].content = content;
239
+ } else {
240
+ chatHistory.push({ role, content });
241
+ }
242
+ updateChatDisplay();
243
+ }
 
 
 
 
 
244
 
245
+ function updateChatDisplay() {
246
+ const chatDiv = document.getElementById('chatHistory');
247
+ chatDiv.innerHTML = chatHistory.map(msg => `
248
+ <div class="mb-2">
249
+ <strong class="${msg.role === 'user' ? 'text-blue-600' : 'text-green-600'}">
250
+ ${msg.role}:
251
+ </strong>
252
+ <span class="ml-2">${msg.content}</span>
253
+ </div>
254
+ `).join('');
255
+ chatDiv.scrollTop = chatDiv.scrollHeight;
256
+ }
257
 
258
+ function displayFeatures(features) {
259
+ const output = document.getElementById('featuresOutput');
260
+ output.innerHTML = features.map(feature => `
261
+ <div class="mb-4 p-4 bg-white rounded shadow">
262
+ <h3 class="font-semibold text-lg mb-2">${feature.feature}</h3>
263
+ <p class="text-gray-600">${feature.short_description}</p>
264
+ </div>
265
+ `).join('');
266
+ }
267
 
268
+ // Handle Enter key in chat input
269
+ document.getElementById('chatInput').addEventListener('keypress', function(e) {
270
+ if (e.key === 'Enter' && !e.shiftKey) {
271
+ e.preventDefault();
272
+ sendMessage();
273
+ }
274
+ });
 
 
 
 
 
 
 
 
 
275
 
276
+ async function generatePlan() {
277
+ const projectTitle = document.getElementById('projectTitle').value.trim();
278
+ const requirements = document.getElementById('planRequirements').value.trim();
279
+ const features = document.getElementById('planFeatures').value.split(',').map(f => f.trim()).filter(f => f);
280
+ const platform = document.getElementById('platform').value;
281
+ const additionalRequirements = document.getElementById('additionalRequirements').value.trim();
282
 
283
+ if (!projectTitle || !requirements || features.length === 0) {
284
+ alert('Please fill in all required fields');
285
+ return;
286
  }
287
 
288
+ const loading = document.getElementById('planLoading');
289
+ const output = document.getElementById('planOutput');
290
+ loading.style.display = 'block';
291
+ output.classList.add('hidden');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
292
 
293
+ try {
294
+ console.log('Sending request with data:', {
295
+ project_title: projectTitle,
296
+ requirements,
297
+ features,
298
+ platform,
299
+ additional_requirements: additionalRequirements
 
 
 
 
 
300
  });
 
301
 
302
+ const response = await fetch('/generate-plan', {
303
+ method: 'POST',
304
+ headers: { 'Content-Type': 'application/json' },
305
+ body: JSON.stringify({
306
+ project_title: projectTitle,
307
+ requirements,
308
+ features,
309
+ platform,
310
+ additional_requirements: additionalRequirements
311
+ })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
312
  });
 
 
 
313
 
314
+ const data = await response.json();
315
+ console.log('Raw API Response:', data);
316
+ console.log('Sections:', data.sections);
317
+ console.log('Raw Content:', data.raw_content);
318
 
319
+ displayPlan(data);
320
+ } catch (error) {
321
+ console.error('Error generating plan:', error);
322
+ alert('Error generating plan');
323
+ } finally {
324
+ loading.style.display = 'none';
 
 
 
325
  }
326
+ }
327
 
328
+ function displayPlan(data) {
329
+ console.log('Displaying plan data:', data);
330
+ const output = document.getElementById('planOutput');
331
+ const title = document.getElementById('planTitle');
332
+ const sections = document.getElementById('planSections');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
333
 
334
+ title.textContent = `Technical Project Plan: ${data.project_title}`;
335
+ console.log('Set title to:', title.textContent);
336
+
337
+ // Clear previous sections
338
+ sections.innerHTML = '';
339
 
340
+ // Display each section
341
+ Object.entries(data.sections).forEach(([key, content]) => {
342
+ console.log(`Processing section ${key}:`, content);
343
 
344
+ const sectionTitle = key
345
+ .split('_')
346
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1))
347
+ .join(' ');
348
+
349
+ // Format the content based on type
350
+ let formattedContent = content;
351
+ if (typeof content === 'object') {
352
+ console.log(`Section ${key} is an object:`, content);
353
+ formattedContent = Object.entries(content)
354
+ .map(([k, v]) => `<strong>${k}:</strong> ${v}`)
355
+ .join('<br>');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
356
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
357
 
358
+ const section = document.createElement('div');
359
+ section.className = 'mb-6 bg-white p-4 rounded shadow';
360
+ section.innerHTML = `
361
+ <h4 class="text-lg font-semibold mb-3 text-blue-600">${sectionTitle}</h4>
362
+ <div class="whitespace-pre-wrap text-gray-700 prose max-w-none">
363
+ ${formattedContent}
364
+ </div>
365
+ `;
366
+ sections.appendChild(section);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
367
  });
368
 
369
+ console.log('Final HTML output:', sections.innerHTML);
370
+ output.classList.remove('hidden');
371
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
372
  </script>
373
  </body>
374
  </html>
migrations/versions/xxxx_add_invoice_sequence.py DELETED
@@ -1,28 +0,0 @@
1
- """add invoice sequence and client type
2
-
3
- Revision ID: xxxx
4
- Revises: previous_revision_id
5
- Create Date: 2024-xx-xx
6
- """
7
- from alembic import op
8
- import sqlalchemy as sa
9
-
10
- def upgrade():
11
- # Create sequence for invoice IDs
12
- op.execute('CREATE SEQUENCE IF NOT EXISTS invoice_id_seq START 1')
13
-
14
- # Add client_type column if not exists
15
- op.add_column('invoices', sa.Column('client_type', sa.String(10), nullable=True))
16
-
17
- # Make invoice_number unique
18
- op.create_unique_constraint('uq_invoice_number', 'invoices', ['invoice_number'])
19
-
20
- def downgrade():
21
- # Remove unique constraint
22
- op.drop_constraint('uq_invoice_number', 'invoices')
23
-
24
- # Drop client_type column
25
- op.drop_column('invoices', 'client_type')
26
-
27
- # Drop sequence
28
- op.execute('DROP SEQUENCE IF EXISTS invoice_id_seq')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
re.md ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Flan-T5 Testing Architecture
2
+
3
+ ## Project Structure
4
+ ```
5
+ flan-t5-testing/
6
+ β”œβ”€β”€ backend/
7
+ β”‚ β”œβ”€β”€ app/
8
+ β”‚ β”‚ β”œβ”€β”€ main.py # FastAPI application entry point
9
+ β”‚ β”‚ β”œβ”€β”€ controllers/
10
+ β”‚ β”‚ β”‚ └── scraper_controller.py
11
+ β”‚ β”‚ β”œβ”€β”€ services/
12
+ β”‚ β”‚ β”‚ β”œβ”€β”€ scraper_service.py
13
+ β”‚ β”‚ β”‚ └── flan_t5_service.py
14
+ β”‚ β”‚ └── models/
15
+ β”‚ β”‚ └── scrape_log.py
16
+ β”‚ β”œβ”€β”€ utils/
17
+ β”‚ β”‚ β”œβ”€β”€ text_extractor.py
18
+ β”‚ β”‚ └── html_cleaner.py
19
+ β”‚ └── requirements.txt
20
+ └── frontend/
21
+ β”œβ”€β”€ src/
22
+ β”‚ β”œβ”€β”€ components/
23
+ β”‚ β”‚ β”œβ”€β”€ ScrapingForm.jsx
24
+ β”‚ β”‚ └── ResultDisplay.jsx
25
+ β”‚ β”œβ”€β”€ App.jsx
26
+ β”‚ └── main.jsx
27
+ └── package.json