Spaces:
Sleeping
Sleeping
EL GHAFRAOUI AYOUB
commited on
Commit
Β·
6f14d8b
1
Parent(s):
d2aae1f
C'
Browse files- .env +3 -2
- .gitattributes +0 -35
- .gitignore +0 -1
- tests/__init__.py β 0.26.0 +0 -0
- tests/test_api.py β 0.26.0' +0 -0
- alembic/__pycache__/env.cpython-312.pyc +0 -0
- alembic/env.py +0 -66
- alembic/script.py.mako +0 -26
- app.log +22 -0
- app/.env +3 -0
- app/__init__.py +1 -0
- app/__pycache__/__init__.cpython-312.pyc +0 -0
- app/__pycache__/main.cpython-312.pyc +0 -0
- tests/test_services.py β app/controllers/__init__.py +0 -0
- app/controllers/__pycache__/__init__.cpython-312.pyc +0 -0
- app/controllers/__pycache__/f5_model.cpython-312.pyc +0 -0
- app/controllers/__pycache__/plan_chat_controller.cpython-312.pyc +0 -0
- app/controllers/__pycache__/scraper_controller.cpython-312.pyc +0 -0
- app/controllers/f5_model.py +86 -0
- app/controllers/plan_chat_controller.py +64 -0
- app/controllers/scraper_controller.py +45 -0
- app/helpers/__init__.py +0 -0
- app/helpers/__pycache__/__init__.cpython-312.pyc +0 -0
- app/helpers/__pycache__/plan_chat.cpython-312.pyc +0 -0
- app/helpers/__pycache__/plan_parser.cpython-312.pyc +0 -0
- app/helpers/chat.py +38 -0
- app/helpers/generate_features.py +32 -0
- app/helpers/generate_plan.py +256 -0
- app/helpers/generate_soluction_stack.py +39 -0
- app/helpers/plan_chat.py +83 -0
- app/helpers/plan_parser.py +91 -0
- app/main.py +280 -84
- app/models/__init__.py +0 -0
- app/models/project_plan.py +24 -0
- app/models/scrape_log.py +0 -0
- app/services/__init__.py +0 -0
- app/services/__pycache__/__init__.cpython-312.pyc +0 -0
- app/services/__pycache__/flan_t5_service.cpython-312.pyc +0 -0
- app/services/__pycache__/scraper_service.cpython-312.pyc +0 -0
- app/services/flan_t5_service.py +18 -0
- app/services/scraper_service.py +45 -0
- app/static/js/main.js +45 -0
- app/templates/index.html +302 -1337
- migrations/versions/xxxx_add_invoice_sequence.py +0 -28
- re.md +27 -0
.env
CHANGED
@@ -1,2 +1,3 @@
|
|
1 |
-
|
2 |
-
|
|
|
|
1 |
+
AWS_ACCESS_KEY_ID="AKIAVRUVQMZRVWTZE4N4"
|
2 |
+
AWS_SECRET_ACCESS_KEY="K/YPo3hmFOcQcqnX2So00s1j1nUXfi/NgMaPph8o"
|
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="K/YPo3hmFOcQcqnX2So00s1j1nUXfi/NgMaPph8o"
|
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,
|
|
|
2 |
from fastapi.staticfiles import StaticFiles
|
3 |
from fastapi.templating import Jinja2Templates
|
4 |
-
from
|
5 |
-
from app.
|
6 |
-
import
|
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
|
12 |
-
import time
|
13 |
|
14 |
-
#
|
15 |
-
logging.basicConfig(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
16 |
logger = logging.getLogger(__name__)
|
17 |
|
18 |
-
|
19 |
-
|
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 |
-
|
52 |
-
|
53 |
-
description="API for generating invoices",
|
54 |
-
version="1.0.0"
|
55 |
-
)
|
56 |
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
allow_credentials=True,
|
65 |
-
allow_methods=["*"],
|
66 |
-
allow_headers=["*"],
|
67 |
-
max_age=3600, # Cache preflight requests for 1 hour
|
68 |
-
)
|
69 |
|
70 |
-
|
71 |
-
|
72 |
|
73 |
-
|
74 |
-
|
|
|
75 |
|
76 |
-
|
77 |
-
|
78 |
|
79 |
-
|
80 |
-
|
81 |
-
|
82 |
-
|
83 |
-
|
|
|
84 |
|
85 |
-
|
86 |
-
|
87 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
88 |
|
89 |
-
# Root endpoint to serve the HTML page
|
90 |
@app.get("/")
|
91 |
-
async def
|
92 |
return templates.TemplateResponse("index.html", {"request": request})
|
93 |
|
94 |
-
|
95 |
-
|
96 |
-
|
97 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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>
|
7 |
-
|
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 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
}
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
.
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
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-
|
210 |
-
|
211 |
-
|
212 |
-
|
213 |
-
|
214 |
-
|
215 |
-
|
|
|
|
|
|
|
|
|
216 |
</div>
|
217 |
-
<div
|
218 |
-
<
|
219 |
-
<
|
220 |
-
<i class="fas fa-history"></i> Historique des Devis
|
221 |
-
</a>
|
222 |
</div>
|
223 |
-
<div
|
224 |
-
<
|
225 |
-
|
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 |
-
|
478 |
-
|
479 |
-
|
480 |
-
|
481 |
-
|
482 |
-
|
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
|
493 |
-
<
|
494 |
-
|
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 |
-
|
511 |
-
|
512 |
-
|
513 |
-
|
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 |
-
|
540 |
-
|
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 |
-
|
572 |
-
|
573 |
-
|
574 |
-
<div class="
|
575 |
-
<div class="
|
576 |
-
|
577 |
-
<
|
|
|
|
|
578 |
</div>
|
579 |
-
<div class="
|
580 |
-
<
|
581 |
-
|
582 |
-
|
583 |
-
|
584 |
-
|
585 |
-
|
586 |
-
|
587 |
-
|
588 |
-
|
589 |
-
|
590 |
-
|
591 |
-
</thead>
|
592 |
-
<tbody></tbody>
|
593 |
-
</table>
|
594 |
-
</div>
|
595 |
</div>
|
596 |
</div>
|
597 |
</div>
|
598 |
-
</div>
|
599 |
|
600 |
-
|
601 |
-
|
602 |
-
<
|
603 |
-
|
604 |
-
|
605 |
-
|
606 |
-
|
607 |
-
<button
|
608 |
-
|
609 |
-
|
610 |
-
|
611 |
</div>
|
612 |
-
<div class="
|
613 |
-
<
|
614 |
-
<
|
615 |
-
<
|
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 |
-
|
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 |
-
|
652 |
-
|
653 |
-
|
654 |
-
if (
|
655 |
-
|
656 |
-
|
657 |
-
|
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 |
-
//
|
667 |
-
|
668 |
-
|
669 |
-
updateTotal(input);
|
670 |
-
}
|
671 |
-
}
|
672 |
|
673 |
-
|
674 |
try {
|
675 |
-
|
676 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
677 |
|
678 |
-
|
679 |
-
|
680 |
-
|
681 |
-
const unitPriceInput = row.querySelector('input[name="unitPrice"]');
|
682 |
-
const totalInput = row.querySelector('input[name="totalHT"]');
|
683 |
|
684 |
-
|
685 |
-
|
686 |
-
|
687 |
-
|
688 |
-
|
689 |
-
|
690 |
-
|
691 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
692 |
}
|
693 |
} catch (error) {
|
694 |
-
console.error('Error
|
|
|
|
|
|
|
|
|
695 |
}
|
696 |
}
|
697 |
|
698 |
-
|
699 |
-
|
700 |
-
|
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 |
-
|
715 |
-
|
716 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
717 |
} catch (error) {
|
718 |
-
console.error('Error
|
|
|
|
|
|
|
|
|
|
|
719 |
}
|
720 |
}
|
721 |
|
722 |
-
|
723 |
-
|
724 |
-
|
725 |
-
|
726 |
-
const agglosTable = document.getElementById("agglosTable");
|
727 |
-
const invoiceForm = document.getElementById("invoiceForm");
|
728 |
-
const invoiceNumberInput = document.getElementById("invoiceNumber");
|
729 |
|
730 |
-
|
731 |
-
|
732 |
-
|
733 |
-
|
734 |
-
|
735 |
-
|
736 |
-
|
737 |
-
|
738 |
-
],
|
739 |
-
panneaux: [
|
740 |
-
{ description: "PTS SISMIQUE 5*3,5", unit: "U", quantity: 0, length: 0, unit_price: 0 }
|
741 |
-
]
|
742 |
-
};
|
743 |
|
744 |
-
|
745 |
-
|
746 |
-
|
747 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
748 |
|
749 |
-
|
750 |
-
|
751 |
-
|
752 |
-
|
753 |
-
|
754 |
-
|
|
|
|
|
|
|
755 |
|
756 |
-
|
757 |
-
|
758 |
-
|
759 |
-
|
760 |
-
|
761 |
-
|
762 |
-
|
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 |
-
|
774 |
-
|
775 |
-
|
776 |
-
|
777 |
-
|
|
|
778 |
|
779 |
-
|
|
|
|
|
780 |
}
|
781 |
|
782 |
-
|
783 |
-
|
784 |
-
|
785 |
-
|
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 |
-
|
814 |
-
|
815 |
-
|
816 |
-
|
817 |
-
|
818 |
-
|
819 |
-
|
820 |
-
return typeA - typeB;
|
821 |
-
}
|
822 |
-
|
823 |
-
// Finally sort by length
|
824 |
-
return a.length - b.length;
|
825 |
});
|
826 |
-
}
|
827 |
|
828 |
-
|
829 |
-
|
830 |
-
|
831 |
-
|
832 |
-
|
833 |
-
|
834 |
-
|
835 |
-
|
836 |
-
|
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 |
-
|
904 |
-
|
905 |
-
|
906 |
-
|
907 |
|
908 |
-
|
909 |
-
|
910 |
-
|
911 |
-
|
912 |
-
|
913 |
-
|
914 |
-
document.querySelector("#totalHT").value = formattedTotalHT;
|
915 |
-
document.querySelector("#tax").value = tax;
|
916 |
-
document.querySelector("#totalTTC").value = totalTTC;
|
917 |
}
|
|
|
918 |
|
919 |
-
|
920 |
-
|
921 |
-
|
922 |
-
|
923 |
-
|
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 |
-
|
966 |
-
|
967 |
-
|
968 |
-
|
969 |
-
|
970 |
|
971 |
-
|
|
|
|
|
972 |
|
973 |
-
|
974 |
-
|
975 |
-
|
976 |
-
|
977 |
-
|
978 |
-
|
979 |
-
|
980 |
-
|
981 |
-
|
982 |
-
|
983 |
-
|
984 |
-
|
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 |
-
|
1291 |
-
|
1292 |
-
|
1293 |
-
|
1294 |
-
|
1295 |
-
|
1296 |
-
|
1297 |
-
|
1298 |
-
|
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 |
-
|
1354 |
-
|
1355 |
-
|
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
|