NOT-OMEGA commited on
Commit
6a0ff33
·
verified ·
1 Parent(s): b62efbf

Upload 68 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .env.example +18 -0
  2. .gitignore +44 -0
  3. CMakeLists.txt +4 -0
  4. Dockerfile +41 -0
  5. backend/__init__.py +1 -0
  6. backend/__pycache__/__init__.cpython-312.pyc +0 -0
  7. backend/agents/__init__.py +1 -0
  8. backend/analysis/__init__.py +1 -0
  9. backend/api/__init__.py +1 -0
  10. backend/api/auth.py +110 -0
  11. backend/api/dependencies.py +49 -0
  12. backend/api/deps.py +46 -0
  13. backend/api/health.py +17 -0
  14. backend/api/workflows.py +81 -0
  15. backend/config/__init__.py +1 -0
  16. backend/config/__pycache__/__init__.cpython-312.pyc +0 -0
  17. backend/config/__pycache__/settings.cpython-312.pyc +0 -0
  18. backend/config/settings.py +39 -0
  19. backend/db/__init__.py +1 -0
  20. backend/db/__pycache__/__init__.cpython-312.pyc +0 -0
  21. backend/db/__pycache__/database.cpython-312.pyc +0 -0
  22. backend/db/database.py +41 -0
  23. backend/db/models.py +109 -0
  24. backend/db/repository.py +124 -0
  25. backend/events/__init__.py +1 -0
  26. backend/execution/__init__.py +1 -0
  27. backend/llm/__init__.py +1 -0
  28. backend/llm/gemini.py +121 -0
  29. backend/llm/provider.py +55 -0
  30. backend/main.py +64 -0
  31. backend/orchestration/__init__.py +1 -0
  32. benchmark/.gitkeep +1 -0
  33. cpp/CMakeLists.txt +34 -0
  34. cpp/build/CMakeCache.txt +98 -0
  35. cpp/build/CMakeFiles/3.29.3/CMakeSystem.cmake +15 -0
  36. cpp/build/CMakeFiles/CMakeConfigureLog.yaml +11 -0
  37. cpp/build/CMakeFiles/cmake.check_cache +1 -0
  38. cpp/include/dag_engine.h +57 -0
  39. cpp/src/dag_engine.cpp +91 -0
  40. cpp/src/main.cpp +32 -0
  41. cpp/tests/test_dag.cpp +75 -0
  42. data/.gitkeep +1 -0
  43. docker-compose.yml +16 -0
  44. frontend/css/main.css +891 -0
  45. frontend/css/style.css +894 -0
  46. frontend/dashboard.html +121 -0
  47. frontend/history.html +104 -0
  48. frontend/index.html +22 -0
  49. frontend/js/api.js +51 -0
  50. frontend/js/app.js +174 -0
.env.example ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # AutoForge Configuration
2
+ # Copy this file to .env and fill in values
3
+
4
+ # Application
5
+ SECRET_KEY=change-this-to-a-random-secret-key
6
+ DEBUG=true
7
+ HOST=0.0.0.0
8
+ PORT=8000
9
+
10
+ # Database
11
+ DATABASE_URL=sqlite+aiosqlite:///data/autoforge.db
12
+
13
+ # Gemini LLM
14
+ GEMINI_API_KEY=
15
+ GEMINI_MODEL=gemini-2.0-flash
16
+
17
+ # Docker Sandbox
18
+ DOCKER_ENABLED=false
.gitignore ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ *.egg-info/
7
+ dist/
8
+ build/
9
+ .eggs/
10
+ *.egg
11
+ .venv/
12
+ venv/
13
+ env/
14
+
15
+ # Environment
16
+ .env
17
+
18
+ # Database
19
+ data/autoforge.db
20
+
21
+ # C++ build
22
+ cpp/build/
23
+ *.o
24
+ *.d
25
+ *.exe
26
+
27
+ # IDE
28
+ .vscode/
29
+ .idea/
30
+ *.swp
31
+ *.swo
32
+ *~
33
+
34
+ # OS
35
+ .DS_Store
36
+ Thumbs.db
37
+
38
+ # Docker
39
+ *.log
40
+
41
+ # Testing
42
+ .pytest_cache/
43
+ htmlcov/
44
+ .coverage
CMakeLists.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ cmake_minimum_required(VERSION 3.16)
2
+ project(AutoForge VERSION 0.1.0 LANGUAGES CXX)
3
+
4
+ add_subdirectory(cpp)
Dockerfile ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ── Stage 1: C++ Build ───────────────────────────────────────────────
2
+ FROM gcc:13 AS cpp-builder
3
+
4
+ RUN apt-get update && apt-get install -y cmake && rm -rf /var/lib/apt/lists/*
5
+
6
+ WORKDIR /build
7
+ COPY cpp/ ./cpp/
8
+ COPY CMakeLists.txt .
9
+
10
+ RUN cmake -B build -S . -DCMAKE_BUILD_TYPE=Release \
11
+ && cmake --build build --target autoforge-dag
12
+
13
+
14
+ # ── Stage 2: Python Runtime ─────────────────────────────────────────
15
+ FROM python:3.12-slim
16
+
17
+ WORKDIR /app
18
+
19
+ # Install Python dependencies
20
+ COPY requirements.txt .
21
+ RUN pip install --no-cache-dir -r requirements.txt
22
+
23
+ # Copy backend
24
+ COPY backend/ ./backend/
25
+
26
+ # Copy frontend
27
+ COPY frontend/ ./frontend/
28
+
29
+ # Copy C++ binary from build stage
30
+ COPY --from=cpp-builder /build/build/cpp/autoforge-dag /app/bin/autoforge-dag
31
+
32
+ # Create data directory
33
+ RUN mkdir -p /app/data
34
+
35
+ # Environment
36
+ ENV PYTHONUNBUFFERED=1
37
+ ENV DATABASE_URL=sqlite+aiosqlite:///data/autoforge.db
38
+
39
+ EXPOSE 8000
40
+
41
+ CMD ["python", "-m", "backend.main"]
backend/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """AutoForge backend package."""
backend/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (199 Bytes). View file
 
backend/agents/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """AutoForge agents package."""
backend/analysis/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """AutoForge analysis package."""
backend/api/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """AutoForge API package."""
backend/api/auth.py ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Authentication API: signup, login, and current-user retrieval."""
2
+
3
+ from datetime import datetime, timedelta, timezone
4
+
5
+ from fastapi import APIRouter, Depends, HTTPException, status
6
+ from jose import jwt
7
+ from passlib.context import CryptContext
8
+ from pydantic import BaseModel, EmailStr, Field
9
+ from sqlalchemy.ext.asyncio import AsyncSession
10
+
11
+ from backend.api.deps import get_current_user, get_db
12
+ from backend.config.settings import get_settings
13
+ from backend.db.models import User
14
+ from backend.db.repository import create_user, get_user_by_email, get_user_by_username
15
+
16
+ router = APIRouter(prefix="/api/auth", tags=["auth"])
17
+
18
+ pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
19
+
20
+
21
+ # ── Schemas ──────────────────────────────────────────────────────────
22
+
23
+ class SignupRequest(BaseModel):
24
+ username: str = Field(..., min_length=3, max_length=80)
25
+ email: EmailStr
26
+ password: str = Field(..., min_length=6)
27
+
28
+
29
+ class LoginRequest(BaseModel):
30
+ username: str
31
+ password: str
32
+
33
+
34
+ class TokenResponse(BaseModel):
35
+ access_token: str
36
+ token_type: str = "bearer"
37
+
38
+
39
+ class UserResponse(BaseModel):
40
+ id: str
41
+ username: str
42
+ email: str
43
+ created_at: datetime
44
+
45
+ model_config = {"from_attributes": True}
46
+
47
+
48
+ # ── Helpers ──────────────────────────────────────────────────────────
49
+
50
+ def _hash_password(password: str) -> str:
51
+ return pwd_context.hash(password)
52
+
53
+
54
+ def _verify_password(plain: str, hashed: str) -> bool:
55
+ return pwd_context.verify(plain, hashed)
56
+
57
+
58
+ def _create_token(user_id: str) -> str:
59
+ settings = get_settings()
60
+ expire = datetime.now(timezone.utc) + timedelta(minutes=settings.jwt_expire_minutes)
61
+ payload = {"sub": user_id, "exp": expire}
62
+ return jwt.encode(payload, settings.secret_key, algorithm=settings.jwt_algorithm)
63
+
64
+
65
+ # ── Routes ───────────────────────────────────────────────────────────
66
+
67
+ @router.post("/signup", response_model=TokenResponse, status_code=status.HTTP_201_CREATED)
68
+ async def signup(body: SignupRequest, session: AsyncSession = Depends(get_db)):
69
+ """Register a new user account."""
70
+ # Check for duplicate username
71
+ if await get_user_by_username(session, body.username):
72
+ raise HTTPException(
73
+ status_code=status.HTTP_409_CONFLICT,
74
+ detail="Username already taken",
75
+ )
76
+
77
+ # Check for duplicate email
78
+ if await get_user_by_email(session, body.email):
79
+ raise HTTPException(
80
+ status_code=status.HTTP_409_CONFLICT,
81
+ detail="Email already registered",
82
+ )
83
+
84
+ user = await create_user(
85
+ session,
86
+ username=body.username,
87
+ email=body.email,
88
+ hashed_password=_hash_password(body.password),
89
+ )
90
+
91
+ return TokenResponse(access_token=_create_token(user.id))
92
+
93
+
94
+ @router.post("/login", response_model=TokenResponse)
95
+ async def login(body: LoginRequest, session: AsyncSession = Depends(get_db)):
96
+ """Authenticate and receive a JWT."""
97
+ user = await get_user_by_username(session, body.username)
98
+ if not user or not _verify_password(body.password, user.hashed_password):
99
+ raise HTTPException(
100
+ status_code=status.HTTP_401_UNAUTHORIZED,
101
+ detail="Invalid username or password",
102
+ )
103
+
104
+ return TokenResponse(access_token=_create_token(user.id))
105
+
106
+
107
+ @router.get("/me", response_model=UserResponse)
108
+ async def me(current_user: User = Depends(get_current_user)):
109
+ """Return the currently authenticated user."""
110
+ return current_user
backend/api/dependencies.py ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Authentication dependencies for FastAPI.
3
+
4
+ Extracts and validates JWT from the Authorization header.
5
+ """
6
+
7
+ from fastapi import Depends, HTTPException, status
8
+ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
9
+ from jose import JWTError, jwt
10
+ from sqlalchemy.orm import Session
11
+
12
+ from backend.config.settings import get_settings
13
+ from backend.db.database import get_db
14
+ from backend.db.models import User
15
+
16
+ security = HTTPBearer()
17
+
18
+
19
+ def get_current_user(
20
+ credentials: HTTPAuthorizationCredentials = Depends(security),
21
+ db: Session = Depends(get_db),
22
+ ) -> User:
23
+ """Decode the JWT and return the authenticated User, or raise 401."""
24
+ settings = get_settings()
25
+ token = credentials.credentials
26
+
27
+ credentials_exception = HTTPException(
28
+ status_code=status.HTTP_401_UNAUTHORIZED,
29
+ detail="Invalid or expired token",
30
+ headers={"WWW-Authenticate": "Bearer"},
31
+ )
32
+
33
+ try:
34
+ payload = jwt.decode(
35
+ token,
36
+ settings.SECRET_KEY,
37
+ algorithms=[settings.JWT_ALGORITHM],
38
+ )
39
+ user_id: int | None = payload.get("sub")
40
+ if user_id is None:
41
+ raise credentials_exception
42
+ except JWTError:
43
+ raise credentials_exception
44
+
45
+ user = db.query(User).filter(User.id == int(user_id)).first()
46
+ if user is None:
47
+ raise credentials_exception
48
+
49
+ return user
backend/api/deps.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Shared FastAPI dependencies."""
2
+
3
+ from fastapi import Depends, HTTPException, status
4
+ from fastapi.security import OAuth2PasswordBearer
5
+ from jose import JWTError, jwt
6
+ from sqlalchemy.ext.asyncio import AsyncSession
7
+
8
+ from backend.config.settings import get_settings
9
+ from backend.db.database import async_session_factory
10
+ from backend.db.repository import get_user_by_id
11
+
12
+ oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login")
13
+
14
+
15
+ async def get_db() -> AsyncSession:
16
+ """Yield an async database session for request scope."""
17
+ async with async_session_factory() as session:
18
+ yield session
19
+
20
+
21
+ async def get_current_user(
22
+ token: str = Depends(oauth2_scheme),
23
+ session: AsyncSession = Depends(get_db),
24
+ ):
25
+ """Decode JWT and return the authenticated user."""
26
+ settings = get_settings()
27
+ credentials_exception = HTTPException(
28
+ status_code=status.HTTP_401_UNAUTHORIZED,
29
+ detail="Invalid or expired token",
30
+ headers={"WWW-Authenticate": "Bearer"},
31
+ )
32
+
33
+ try:
34
+ payload = jwt.decode(
35
+ token, settings.secret_key, algorithms=[settings.jwt_algorithm]
36
+ )
37
+ user_id: str = payload.get("sub")
38
+ if user_id is None:
39
+ raise credentials_exception
40
+ except JWTError:
41
+ raise credentials_exception
42
+
43
+ user = await get_user_by_id(session, user_id)
44
+ if user is None:
45
+ raise credentials_exception
46
+ return user
backend/api/health.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Health check endpoint."""
2
+
3
+ from fastapi import APIRouter
4
+ from pydantic import BaseModel
5
+
6
+ router = APIRouter(tags=["health"])
7
+
8
+
9
+ class HealthResponse(BaseModel):
10
+ status: str
11
+ version: str
12
+
13
+
14
+ @router.get("/health", response_model=HealthResponse)
15
+ async def health():
16
+ """Return application health status."""
17
+ return HealthResponse(status="ok", version="0.1.0")
backend/api/workflows.py ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Workflow API endpoints."""
2
+
3
+ from datetime import datetime
4
+ from typing import Optional
5
+
6
+ from fastapi import APIRouter, Depends, HTTPException, status
7
+ from pydantic import BaseModel, Field
8
+ from sqlalchemy.ext.asyncio import AsyncSession
9
+
10
+ from backend.api.deps import get_current_user, get_db
11
+ from backend.db.models import User
12
+ from backend.db.repository import create_workflow, get_workflow_by_id, get_workflows_by_user
13
+
14
+ router = APIRouter(prefix="/api/workflows", tags=["workflows"])
15
+
16
+
17
+ # ── Schemas ──────────────────────────────────────────────────────────
18
+
19
+ class WorkflowCreateRequest(BaseModel):
20
+ prompt: str = Field(..., min_length=1)
21
+
22
+
23
+ class TaskResponse(BaseModel):
24
+ id: str
25
+ task_type: str
26
+ status: str
27
+ created_at: datetime
28
+
29
+ model_config = {"from_attributes": True}
30
+
31
+
32
+ class WorkflowResponse(BaseModel):
33
+ id: str
34
+ prompt: str
35
+ status: str
36
+ created_at: datetime
37
+ updated_at: datetime
38
+
39
+ model_config = {"from_attributes": True}
40
+
41
+
42
+ class WorkflowDetailResponse(WorkflowResponse):
43
+ tasks: list[TaskResponse] = []
44
+
45
+
46
+ # ── Routes ───────────────────────────────────────────────────────────
47
+
48
+ @router.post("", response_model=WorkflowResponse, status_code=status.HTTP_201_CREATED)
49
+ async def create(
50
+ body: WorkflowCreateRequest,
51
+ current_user: User = Depends(get_current_user),
52
+ session: AsyncSession = Depends(get_db),
53
+ ):
54
+ """Create a new workflow (status=pending, no execution yet)."""
55
+ workflow = await create_workflow(session, user_id=current_user.id, prompt=body.prompt)
56
+ return workflow
57
+
58
+
59
+ @router.get("", response_model=list[WorkflowResponse])
60
+ async def list_workflows(
61
+ current_user: User = Depends(get_current_user),
62
+ session: AsyncSession = Depends(get_db),
63
+ ):
64
+ """List all workflows for the current user."""
65
+ workflows = await get_workflows_by_user(session, current_user.id)
66
+ return workflows
67
+
68
+
69
+ @router.get("/{workflow_id}", response_model=WorkflowDetailResponse)
70
+ async def get_workflow(
71
+ workflow_id: str,
72
+ current_user: User = Depends(get_current_user),
73
+ session: AsyncSession = Depends(get_db),
74
+ ):
75
+ """Get a single workflow with its tasks."""
76
+ workflow = await get_workflow_by_id(session, workflow_id)
77
+ if not workflow:
78
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Workflow not found")
79
+ if workflow.user_id != current_user.id:
80
+ raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Access denied")
81
+ return workflow
backend/config/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """AutoForge configuration package."""
backend/config/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (212 Bytes). View file
 
backend/config/__pycache__/settings.cpython-312.pyc ADDED
Binary file (1.64 kB). View file
 
backend/config/settings.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Application settings loaded from environment variables."""
2
+
3
+ from pydantic_settings import BaseSettings
4
+ from pydantic import Field
5
+
6
+
7
+ class Settings(BaseSettings):
8
+ """AutoForge configuration."""
9
+
10
+ # Application
11
+ secret_key: str = Field(default="change-this-to-a-random-secret-key")
12
+ debug: bool = Field(default=True)
13
+ host: str = Field(default="0.0.0.0")
14
+ port: int = Field(default=8000)
15
+
16
+ # Database
17
+ database_url: str = Field(default="sqlite+aiosqlite:///data/autoforge.db")
18
+
19
+ # Gemini LLM
20
+ gemini_api_key: str = Field(default="")
21
+ gemini_model: str = Field(default="gemini-2.0-flash")
22
+
23
+ # Docker
24
+ docker_enabled: bool = Field(default=False)
25
+
26
+ # JWT
27
+ jwt_algorithm: str = Field(default="HS256")
28
+ jwt_expire_minutes: int = Field(default=1440) # 24 hours
29
+
30
+ model_config = {
31
+ "env_file": ".env",
32
+ "env_file_encoding": "utf-8",
33
+ "case_sensitive": False,
34
+ }
35
+
36
+
37
+ def get_settings() -> Settings:
38
+ """Return application settings singleton."""
39
+ return Settings()
backend/db/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """AutoForge database package."""
backend/db/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (203 Bytes). View file
 
backend/db/__pycache__/database.cpython-312.pyc ADDED
Binary file (2.06 kB). View file
 
backend/db/database.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """SQLAlchemy async database engine and session management."""
2
+
3
+ from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
4
+ from sqlalchemy.orm import DeclarativeBase
5
+
6
+ from backend.config.settings import get_settings
7
+
8
+
9
+ class Base(DeclarativeBase):
10
+ """SQLAlchemy declarative base for all models."""
11
+ pass
12
+
13
+
14
+ _settings = get_settings()
15
+
16
+ engine = create_async_engine(
17
+ _settings.database_url,
18
+ echo=_settings.debug,
19
+ connect_args={"check_same_thread": False},
20
+ )
21
+
22
+ async_session_factory = async_sessionmaker(
23
+ engine,
24
+ class_=AsyncSession,
25
+ expire_on_commit=False,
26
+ )
27
+
28
+
29
+ async def init_db() -> None:
30
+ """Create all database tables."""
31
+ # Import models so they register with Base.metadata
32
+ import backend.db.models # noqa: F401
33
+
34
+ async with engine.begin() as conn:
35
+ await conn.run_sync(Base.metadata.create_all)
36
+
37
+
38
+ async def get_async_session() -> AsyncSession:
39
+ """Yield an async database session."""
40
+ async with async_session_factory() as session:
41
+ yield session
backend/db/models.py ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """SQLAlchemy ORM models for AutoForge."""
2
+
3
+ import uuid
4
+ from datetime import datetime, timezone
5
+
6
+ from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, Text
7
+ from sqlalchemy.orm import relationship
8
+
9
+ from backend.db.database import Base
10
+
11
+
12
+ def _utcnow() -> datetime:
13
+ return datetime.now(timezone.utc)
14
+
15
+
16
+ def _generate_uuid() -> str:
17
+ return str(uuid.uuid4())
18
+
19
+
20
+ class User(Base):
21
+ """User account."""
22
+
23
+ __tablename__ = "users"
24
+
25
+ id = Column(String(36), primary_key=True, default=_generate_uuid)
26
+ username = Column(String(80), unique=True, nullable=False, index=True)
27
+ email = Column(String(255), unique=True, nullable=False, index=True)
28
+ hashed_password = Column(String(255), nullable=False)
29
+ created_at = Column(DateTime(timezone=True), default=_utcnow, nullable=False)
30
+
31
+ workflows = relationship("Workflow", back_populates="user", lazy="selectin")
32
+
33
+ def __repr__(self) -> str:
34
+ return f"<User {self.username}>"
35
+
36
+
37
+ class Workflow(Base):
38
+ """A code-generation workflow initiated by a user."""
39
+
40
+ __tablename__ = "workflows"
41
+
42
+ id = Column(String(36), primary_key=True, default=_generate_uuid)
43
+ user_id = Column(String(36), ForeignKey("users.id"), nullable=False, index=True)
44
+ prompt = Column(Text, nullable=False)
45
+ status = Column(String(20), nullable=False, default="pending")
46
+ created_at = Column(DateTime(timezone=True), default=_utcnow, nullable=False)
47
+ updated_at = Column(DateTime(timezone=True), default=_utcnow, onupdate=_utcnow, nullable=False)
48
+
49
+ user = relationship("User", back_populates="workflows")
50
+ tasks = relationship("Task", back_populates="workflow", lazy="selectin")
51
+ events = relationship("Event", back_populates="workflow", lazy="selectin")
52
+
53
+ def __repr__(self) -> str:
54
+ return f"<Workflow {self.id} [{self.status}]>"
55
+
56
+
57
+ class Task(Base):
58
+ """A single task within a workflow DAG."""
59
+
60
+ __tablename__ = "tasks"
61
+
62
+ id = Column(String(36), primary_key=True, default=_generate_uuid)
63
+ workflow_id = Column(String(36), ForeignKey("workflows.id"), nullable=False, index=True)
64
+ task_type = Column(String(50), nullable=False)
65
+ status = Column(String(20), nullable=False, default="pending")
66
+ input_data = Column(Text, nullable=True)
67
+ output_data = Column(Text, nullable=True)
68
+ created_at = Column(DateTime(timezone=True), default=_utcnow, nullable=False)
69
+
70
+ workflow = relationship("Workflow", back_populates="tasks")
71
+ attempts = relationship("Attempt", back_populates="task", lazy="selectin")
72
+
73
+ def __repr__(self) -> str:
74
+ return f"<Task {self.id} [{self.task_type}]>"
75
+
76
+
77
+ class Attempt(Base):
78
+ """A single execution attempt for a task (supports retry logic)."""
79
+
80
+ __tablename__ = "attempts"
81
+
82
+ id = Column(String(36), primary_key=True, default=_generate_uuid)
83
+ task_id = Column(String(36), ForeignKey("tasks.id"), nullable=False, index=True)
84
+ attempt_number = Column(Integer, nullable=False, default=1)
85
+ status = Column(String(20), nullable=False, default="pending")
86
+ error_message = Column(Text, nullable=True)
87
+ created_at = Column(DateTime(timezone=True), default=_utcnow, nullable=False)
88
+
89
+ task = relationship("Task", back_populates="attempts")
90
+
91
+ def __repr__(self) -> str:
92
+ return f"<Attempt {self.id} #{self.attempt_number}>"
93
+
94
+
95
+ class Event(Base):
96
+ """A workflow event for streaming to the frontend."""
97
+
98
+ __tablename__ = "events"
99
+
100
+ id = Column(String(36), primary_key=True, default=_generate_uuid)
101
+ workflow_id = Column(String(36), ForeignKey("workflows.id"), nullable=False, index=True)
102
+ event_type = Column(String(50), nullable=False)
103
+ payload = Column(Text, nullable=True)
104
+ created_at = Column(DateTime(timezone=True), default=_utcnow, nullable=False)
105
+
106
+ workflow = relationship("Workflow", back_populates="events")
107
+
108
+ def __repr__(self) -> str:
109
+ return f"<Event {self.id} [{self.event_type}]>"
backend/db/repository.py ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Repository layer for database CRUD operations."""
2
+
3
+ from typing import Optional
4
+
5
+ from sqlalchemy import select
6
+ from sqlalchemy.ext.asyncio import AsyncSession
7
+
8
+ from backend.db.models import Attempt, Event, Task, User, Workflow
9
+
10
+
11
+ # ── User operations ──────────────────────────────────────────────────
12
+
13
+ async def create_user(
14
+ session: AsyncSession,
15
+ username: str,
16
+ email: str,
17
+ hashed_password: str,
18
+ ) -> User:
19
+ """Create and persist a new user."""
20
+ user = User(
21
+ username=username,
22
+ email=email,
23
+ hashed_password=hashed_password,
24
+ )
25
+ session.add(user)
26
+ await session.commit()
27
+ await session.refresh(user)
28
+ return user
29
+
30
+
31
+ async def get_user_by_username(session: AsyncSession, username: str) -> Optional[User]:
32
+ """Look up a user by username."""
33
+ result = await session.execute(select(User).where(User.username == username))
34
+ return result.scalar_one_or_none()
35
+
36
+
37
+ async def get_user_by_email(session: AsyncSession, email: str) -> Optional[User]:
38
+ """Look up a user by email."""
39
+ result = await session.execute(select(User).where(User.email == email))
40
+ return result.scalar_one_or_none()
41
+
42
+
43
+ async def get_user_by_id(session: AsyncSession, user_id: str) -> Optional[User]:
44
+ """Look up a user by ID."""
45
+ result = await session.execute(select(User).where(User.id == user_id))
46
+ return result.scalar_one_or_none()
47
+
48
+
49
+ # ── Workflow operations ──────────────────────────────────────────────
50
+
51
+ async def create_workflow(
52
+ session: AsyncSession,
53
+ user_id: str,
54
+ prompt: str,
55
+ ) -> Workflow:
56
+ """Create a new workflow record."""
57
+ workflow = Workflow(user_id=user_id, prompt=prompt)
58
+ session.add(workflow)
59
+ await session.commit()
60
+ await session.refresh(workflow)
61
+ return workflow
62
+
63
+
64
+ async def get_workflows_by_user(session: AsyncSession, user_id: str) -> list[Workflow]:
65
+ """Return all workflows for a given user, newest first."""
66
+ result = await session.execute(
67
+ select(Workflow)
68
+ .where(Workflow.user_id == user_id)
69
+ .order_by(Workflow.created_at.desc())
70
+ )
71
+ return list(result.scalars().all())
72
+
73
+
74
+ async def get_workflow_by_id(session: AsyncSession, workflow_id: str) -> Optional[Workflow]:
75
+ """Return a single workflow by ID."""
76
+ result = await session.execute(select(Workflow).where(Workflow.id == workflow_id))
77
+ return result.scalar_one_or_none()
78
+
79
+
80
+ # ── Task operations ──────────────────────────────────────────────────
81
+
82
+ async def create_task(
83
+ session: AsyncSession,
84
+ workflow_id: str,
85
+ task_type: str,
86
+ input_data: Optional[str] = None,
87
+ ) -> Task:
88
+ """Create a new task within a workflow."""
89
+ task = Task(workflow_id=workflow_id, task_type=task_type, input_data=input_data)
90
+ session.add(task)
91
+ await session.commit()
92
+ await session.refresh(task)
93
+ return task
94
+
95
+
96
+ # ── Attempt operations ───────────────────────────────────────────────
97
+
98
+ async def create_attempt(
99
+ session: AsyncSession,
100
+ task_id: str,
101
+ attempt_number: int = 1,
102
+ ) -> Attempt:
103
+ """Create an execution attempt for a task."""
104
+ attempt = Attempt(task_id=task_id, attempt_number=attempt_number)
105
+ session.add(attempt)
106
+ await session.commit()
107
+ await session.refresh(attempt)
108
+ return attempt
109
+
110
+
111
+ # ── Event operations ─────────────────────────────────────────────────
112
+
113
+ async def create_event(
114
+ session: AsyncSession,
115
+ workflow_id: str,
116
+ event_type: str,
117
+ payload: Optional[str] = None,
118
+ ) -> Event:
119
+ """Create a workflow event."""
120
+ event = Event(workflow_id=workflow_id, event_type=event_type, payload=payload)
121
+ session.add(event)
122
+ await session.commit()
123
+ await session.refresh(event)
124
+ return event
backend/events/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """AutoForge events package."""
backend/execution/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """AutoForge execution package."""
backend/llm/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # AutoForge LLM Providers
backend/llm/gemini.py ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Google Gemini LLM provider."""
2
+
3
+ import json
4
+ from typing import Any, Optional
5
+
6
+ import httpx
7
+
8
+ from backend.config.settings import get_settings
9
+ from backend.llm.provider import LLMProvider
10
+
11
+
12
+ class GeminiProviderError(Exception):
13
+ """Raised when the Gemini API returns an error."""
14
+ pass
15
+
16
+
17
+ class GeminiNotConfiguredError(GeminiProviderError):
18
+ """Raised when the Gemini API key is not set."""
19
+ pass
20
+
21
+
22
+ class GeminiProvider(LLMProvider):
23
+ """LLM provider backed by Google Gemini.
24
+
25
+ Reads ``GEMINI_API_KEY`` and ``GEMINI_MODEL`` from application
26
+ settings. Communicates with the Gemini REST API via ``httpx``.
27
+ """
28
+
29
+ BASE_URL = "https://generativelanguage.googleapis.com/v1beta"
30
+
31
+ def __init__(self) -> None:
32
+ settings = get_settings()
33
+ self.api_key = settings.gemini_api_key
34
+ self.model = settings.gemini_model
35
+
36
+ if not self.api_key:
37
+ raise GeminiNotConfiguredError(
38
+ "GEMINI_API_KEY is not set. "
39
+ "Please configure it in your .env file."
40
+ )
41
+
42
+ # ── Public API ───────────────────────────────────────────────────
43
+
44
+ async def generate(
45
+ self,
46
+ prompt: str,
47
+ *,
48
+ system_prompt: Optional[str] = None,
49
+ temperature: float = 0.7,
50
+ max_tokens: int = 4096,
51
+ ) -> str:
52
+ """Send a prompt to Gemini and return the generated text."""
53
+ contents = []
54
+ if system_prompt:
55
+ contents.append({"role": "user", "parts": [{"text": system_prompt}]})
56
+ contents.append({"role": "model", "parts": [{"text": "Understood."}]})
57
+ contents.append({"role": "user", "parts": [{"text": prompt}]})
58
+
59
+ body = {
60
+ "contents": contents,
61
+ "generationConfig": {
62
+ "temperature": temperature,
63
+ "maxOutputTokens": max_tokens,
64
+ },
65
+ }
66
+
67
+ data = await self._request(body)
68
+ return self._extract_text(data)
69
+
70
+ async def generate_structured(
71
+ self,
72
+ prompt: str,
73
+ schema: dict[str, Any],
74
+ *,
75
+ system_prompt: Optional[str] = None,
76
+ ) -> dict[str, Any]:
77
+ """Send a prompt to Gemini and parse the response as JSON."""
78
+ schema_instruction = (
79
+ f"Respond ONLY with valid JSON matching this schema:\n"
80
+ f"```json\n{json.dumps(schema, indent=2)}\n```"
81
+ )
82
+ full_prompt = f"{prompt}\n\n{schema_instruction}"
83
+ raw = await self.generate(full_prompt, system_prompt=system_prompt, temperature=0.2)
84
+
85
+ # Strip markdown code fences if present
86
+ text = raw.strip()
87
+ if text.startswith("```"):
88
+ text = text.split("\n", 1)[1] if "\n" in text else text[3:]
89
+ if text.endswith("```"):
90
+ text = text[:-3]
91
+ text = text.strip()
92
+
93
+ try:
94
+ return json.loads(text)
95
+ except json.JSONDecodeError as exc:
96
+ raise GeminiProviderError(f"Failed to parse Gemini response as JSON: {exc}")
97
+
98
+ # ── Internals ────────────────────────────────────────────────────
99
+
100
+ async def _request(self, body: dict) -> dict:
101
+ """Make a POST request to the Gemini generateContent endpoint."""
102
+ url = (
103
+ f"{self.BASE_URL}/models/{self.model}:generateContent"
104
+ f"?key={self.api_key}"
105
+ )
106
+
107
+ async with httpx.AsyncClient(timeout=60.0) as client:
108
+ response = await client.post(url, json=body)
109
+ if response.status_code != 200:
110
+ raise GeminiProviderError(
111
+ f"Gemini API error {response.status_code}: {response.text}"
112
+ )
113
+ return response.json()
114
+
115
+ @staticmethod
116
+ def _extract_text(data: dict) -> str:
117
+ """Extract generated text from the Gemini response payload."""
118
+ try:
119
+ return data["candidates"][0]["content"]["parts"][0]["text"]
120
+ except (KeyError, IndexError) as exc:
121
+ raise GeminiProviderError(f"Unexpected Gemini response structure: {exc}")
backend/llm/provider.py ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Abstract LLM provider interface."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Any, Optional
5
+
6
+
7
+ class LLMProvider(ABC):
8
+ """Base class for all LLM providers.
9
+
10
+ Every concrete provider must implement ``generate`` and
11
+ ``generate_structured`` so the rest of AutoForge can remain
12
+ provider-agnostic.
13
+ """
14
+
15
+ @abstractmethod
16
+ async def generate(
17
+ self,
18
+ prompt: str,
19
+ *,
20
+ system_prompt: Optional[str] = None,
21
+ temperature: float = 0.7,
22
+ max_tokens: int = 4096,
23
+ ) -> str:
24
+ """Generate a free-form text completion.
25
+
26
+ Args:
27
+ prompt: The user-facing prompt.
28
+ system_prompt: Optional system-level instruction.
29
+ temperature: Sampling temperature.
30
+ max_tokens: Maximum tokens in the response.
31
+
32
+ Returns:
33
+ The generated text.
34
+ """
35
+ ...
36
+
37
+ @abstractmethod
38
+ async def generate_structured(
39
+ self,
40
+ prompt: str,
41
+ schema: dict[str, Any],
42
+ *,
43
+ system_prompt: Optional[str] = None,
44
+ ) -> dict[str, Any]:
45
+ """Generate output conforming to a JSON schema.
46
+
47
+ Args:
48
+ prompt: The user-facing prompt.
49
+ schema: A JSON Schema the response must match.
50
+ system_prompt: Optional system-level instruction.
51
+
52
+ Returns:
53
+ A dictionary matching the provided schema.
54
+ """
55
+ ...
backend/main.py ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """AutoForge FastAPI application entry point."""
2
+
3
+ import os
4
+ from contextlib import asynccontextmanager
5
+ from pathlib import Path
6
+
7
+ from fastapi import FastAPI
8
+ from fastapi.middleware.cors import CORSMiddleware
9
+ from fastapi.staticfiles import StaticFiles
10
+
11
+ from backend.api.auth import router as auth_router
12
+ from backend.api.health import router as health_router
13
+ from backend.api.workflows import router as workflows_router
14
+ from backend.config.settings import get_settings
15
+ from backend.db.database import init_db
16
+
17
+
18
+ @asynccontextmanager
19
+ async def lifespan(app: FastAPI):
20
+ """Startup and shutdown lifecycle."""
21
+ # Ensure data directory exists
22
+ os.makedirs("data", exist_ok=True)
23
+ # Initialize database tables
24
+ await init_db()
25
+ yield
26
+
27
+
28
+ app = FastAPI(
29
+ title="AutoForge",
30
+ description="Autonomous AI coding system",
31
+ version="0.1.0",
32
+ lifespan=lifespan,
33
+ )
34
+
35
+ # CORS — permissive for local development
36
+ app.add_middleware(
37
+ CORSMiddleware,
38
+ allow_origins=["*"],
39
+ allow_credentials=True,
40
+ allow_methods=["*"],
41
+ allow_headers=["*"],
42
+ )
43
+
44
+ # API routers
45
+ app.include_router(health_router)
46
+ app.include_router(auth_router)
47
+ app.include_router(workflows_router)
48
+
49
+ # Serve frontend static files
50
+ frontend_path = Path(__file__).resolve().parent.parent / "frontend"
51
+ if frontend_path.is_dir():
52
+ app.mount("/", StaticFiles(directory=str(frontend_path), html=True), name="frontend")
53
+
54
+
55
+ if __name__ == "__main__":
56
+ import uvicorn
57
+
58
+ settings = get_settings()
59
+ uvicorn.run(
60
+ "backend.main:app",
61
+ host=settings.host,
62
+ port=settings.port,
63
+ reload=settings.debug,
64
+ )
backend/orchestration/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """AutoForge orchestration package."""
benchmark/.gitkeep ADDED
@@ -0,0 +1 @@
 
 
1
+
cpp/CMakeLists.txt ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ cmake_minimum_required(VERSION 3.16)
2
+ project(AutoForgeDAG VERSION 0.1.0 LANGUAGES CXX)
3
+
4
+ set(CMAKE_CXX_STANDARD 17)
5
+ set(CMAKE_CXX_STANDARD_REQUIRED ON)
6
+ set(CMAKE_CXX_EXTENSIONS OFF)
7
+
8
+ # ── Main executable ──────────────────────────
9
+
10
+ add_executable(autoforge-dag
11
+ src/main.cpp
12
+ src/dag_engine.cpp
13
+ )
14
+
15
+ target_include_directories(autoforge-dag PRIVATE include)
16
+
17
+ # ── Test executable ──────────────────────────
18
+
19
+ add_executable(autoforge-dag-test
20
+ tests/test_dag.cpp
21
+ src/dag_engine.cpp
22
+ )
23
+
24
+ target_include_directories(autoforge-dag-test PRIVATE include)
25
+
26
+ # Threading support
27
+ find_package(Threads REQUIRED)
28
+ target_link_libraries(autoforge-dag Threads::Threads)
29
+ target_link_libraries(autoforge-dag-test Threads::Threads)
30
+
31
+ # ── CTest ────────────────────────────────────
32
+
33
+ enable_testing()
34
+ add_test(NAME DAGEngineTest COMMAND autoforge-dag-test)
cpp/build/CMakeCache.txt ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # This is the CMakeCache file.
2
+ # For build in directory: c:/Users/Admin/OneDrive/Desktop/AutoForge/cpp/build
3
+ # It was generated by CMake: C:/Program Files/CMake/bin/cmake.exe
4
+ # You can edit this file to change values found and used by cmake.
5
+ # If you do not want to change any of the values, simply exit the editor.
6
+ # If you do want to change a value, simply edit, save, and exit the editor.
7
+ # The syntax for the file is as follows:
8
+ # KEY:TYPE=VALUE
9
+ # KEY is the name of a variable in the cache.
10
+ # TYPE is a hint to GUIs for the type of VALUE, DO NOT EDIT TYPE!.
11
+ # VALUE is the current value for the KEY.
12
+
13
+ ########################
14
+ # EXTERNAL cache entries
15
+ ########################
16
+
17
+ //Value Computed by CMake
18
+ AutoForgeDAG_BINARY_DIR:STATIC=C:/Users/Admin/OneDrive/Desktop/AutoForge/cpp/build
19
+
20
+ //Value Computed by CMake
21
+ AutoForgeDAG_IS_TOP_LEVEL:STATIC=ON
22
+
23
+ //Value Computed by CMake
24
+ AutoForgeDAG_SOURCE_DIR:STATIC=C:/Users/Admin/OneDrive/Desktop/AutoForge/cpp
25
+
26
+ //Value Computed by CMake.
27
+ CMAKE_FIND_PACKAGE_REDIRECTS_DIR:STATIC=C:/Users/Admin/OneDrive/Desktop/AutoForge/cpp/build/CMakeFiles/pkgRedirects
28
+
29
+ //Program used to build from makefiles.
30
+ CMAKE_MAKE_PROGRAM:STRING=nmake
31
+
32
+ //Value Computed by CMake
33
+ CMAKE_PROJECT_DESCRIPTION:STATIC=
34
+
35
+ //Value Computed by CMake
36
+ CMAKE_PROJECT_HOMEPAGE_URL:STATIC=
37
+
38
+ //Value Computed by CMake
39
+ CMAKE_PROJECT_NAME:STATIC=AutoForgeDAG
40
+
41
+ //Value Computed by CMake
42
+ CMAKE_PROJECT_VERSION:STATIC=0.1.0
43
+
44
+ //Value Computed by CMake
45
+ CMAKE_PROJECT_VERSION_MAJOR:STATIC=0
46
+
47
+ //Value Computed by CMake
48
+ CMAKE_PROJECT_VERSION_MINOR:STATIC=1
49
+
50
+ //Value Computed by CMake
51
+ CMAKE_PROJECT_VERSION_PATCH:STATIC=0
52
+
53
+ //Value Computed by CMake
54
+ CMAKE_PROJECT_VERSION_TWEAK:STATIC=
55
+
56
+
57
+ ########################
58
+ # INTERNAL cache entries
59
+ ########################
60
+
61
+ //This is the directory where this CMakeCache.txt was created
62
+ CMAKE_CACHEFILE_DIR:INTERNAL=c:/Users/Admin/OneDrive/Desktop/AutoForge/cpp/build
63
+ //Major version of cmake used to create the current loaded cache
64
+ CMAKE_CACHE_MAJOR_VERSION:INTERNAL=3
65
+ //Minor version of cmake used to create the current loaded cache
66
+ CMAKE_CACHE_MINOR_VERSION:INTERNAL=29
67
+ //Patch version of cmake used to create the current loaded cache
68
+ CMAKE_CACHE_PATCH_VERSION:INTERNAL=3
69
+ //Path to CMake executable.
70
+ CMAKE_COMMAND:INTERNAL=C:/Program Files/CMake/bin/cmake.exe
71
+ //Path to cpack program executable.
72
+ CMAKE_CPACK_COMMAND:INTERNAL=C:/Program Files/CMake/bin/cpack.exe
73
+ //Path to ctest program executable.
74
+ CMAKE_CTEST_COMMAND:INTERNAL=C:/Program Files/CMake/bin/ctest.exe
75
+ //Path to cache edit program executable.
76
+ CMAKE_EDIT_COMMAND:INTERNAL=C:/Program Files/CMake/bin/cmake-gui.exe
77
+ //Name of external makefile project generator.
78
+ CMAKE_EXTRA_GENERATOR:INTERNAL=
79
+ //Name of generator.
80
+ CMAKE_GENERATOR:INTERNAL=NMake Makefiles
81
+ //Generator instance identifier.
82
+ CMAKE_GENERATOR_INSTANCE:INTERNAL=
83
+ //Name of generator platform.
84
+ CMAKE_GENERATOR_PLATFORM:INTERNAL=
85
+ //Name of generator toolset.
86
+ CMAKE_GENERATOR_TOOLSET:INTERNAL=
87
+ //Source directory with the top level CMakeLists.txt file for this
88
+ // project
89
+ CMAKE_HOME_DIRECTORY:INTERNAL=C:/Users/Admin/OneDrive/Desktop/AutoForge/cpp
90
+ //ADVANCED property for variable: CMAKE_MAKE_PROGRAM
91
+ CMAKE_MAKE_PROGRAM-ADVANCED:INTERNAL=1
92
+ //number of local generators
93
+ CMAKE_NUMBER_OF_MAKEFILES:INTERNAL=1
94
+ //Platform information initialized
95
+ CMAKE_PLATFORM_INFO_INITIALIZED:INTERNAL=1
96
+ //Path to CMake installation.
97
+ CMAKE_ROOT:INTERNAL=C:/Program Files/CMake/share/cmake-3.29
98
+
cpp/build/CMakeFiles/3.29.3/CMakeSystem.cmake ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ set(CMAKE_HOST_SYSTEM "Windows-10.0.26200")
2
+ set(CMAKE_HOST_SYSTEM_NAME "Windows")
3
+ set(CMAKE_HOST_SYSTEM_VERSION "10.0.26200")
4
+ set(CMAKE_HOST_SYSTEM_PROCESSOR "AMD64")
5
+
6
+
7
+
8
+ set(CMAKE_SYSTEM "Windows-10.0.26200")
9
+ set(CMAKE_SYSTEM_NAME "Windows")
10
+ set(CMAKE_SYSTEM_VERSION "10.0.26200")
11
+ set(CMAKE_SYSTEM_PROCESSOR "AMD64")
12
+
13
+ set(CMAKE_CROSSCOMPILING "FALSE")
14
+
15
+ set(CMAKE_SYSTEM_LOADED 1)
cpp/build/CMakeFiles/CMakeConfigureLog.yaml ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ ---
3
+ events:
4
+ -
5
+ kind: "message-v1"
6
+ backtrace:
7
+ - "C:/Program Files/CMake/share/cmake-3.29/Modules/CMakeDetermineSystem.cmake:205 (message)"
8
+ - "CMakeLists.txt:2 (project)"
9
+ message: |
10
+ The system is: Windows - 10.0.26200 - AMD64
11
+ ...
cpp/build/CMakeFiles/cmake.check_cache ADDED
@@ -0,0 +1 @@
 
 
1
+ # This file is generated by cmake for dependency checking of the CMakeCache.txt file
cpp/include/dag_engine.h ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #pragma once
2
+
3
+ /**
4
+ * AutoForge DAG Engine
5
+ *
6
+ * Minimal skeleton for Phase 1.
7
+ * Full task scheduling will be implemented in Phase 2.
8
+ */
9
+
10
+ #include <string>
11
+ #include <vector>
12
+ #include <unordered_map>
13
+ #include <functional>
14
+ #include <mutex>
15
+
16
+ namespace autoforge {
17
+
18
+ /// Represents a single node in the execution DAG.
19
+ struct DAGNode {
20
+ std::string id;
21
+ std::string name;
22
+ std::string type; // e.g., "plan", "code", "test", "analyze"
23
+ std::vector<std::string> deps; // IDs of dependency nodes
24
+ };
25
+
26
+ /// DAG execution engine — schedules and runs task nodes.
27
+ class DAGEngine {
28
+ public:
29
+ DAGEngine();
30
+ ~DAGEngine();
31
+
32
+ /// Get the engine version string.
33
+ static std::string version();
34
+
35
+ /// Add a node to the DAG.
36
+ void add_node(const DAGNode& node);
37
+
38
+ /// Get the number of nodes in the DAG.
39
+ size_t node_count() const;
40
+
41
+ /// Clear all nodes.
42
+ void clear();
43
+
44
+ /// Validate that the DAG has no cycles. Returns true if valid.
45
+ bool validate() const;
46
+
47
+ /// Execute the DAG (Phase 2 — currently a no-op).
48
+ /// Returns true on success.
49
+ bool execute();
50
+
51
+ private:
52
+ std::vector<DAGNode> nodes_;
53
+ std::unordered_map<std::string, size_t> index_;
54
+ mutable std::mutex mutex_;
55
+ };
56
+
57
+ } // namespace autoforge
cpp/src/dag_engine.cpp ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * AutoForge DAG Engine — Implementation
3
+ */
4
+
5
+ #include "dag_engine.h"
6
+ #include <algorithm>
7
+ #include <queue>
8
+ #include <unordered_set>
9
+ #include <iostream>
10
+
11
+ namespace autoforge {
12
+
13
+ DAGEngine::DAGEngine() = default;
14
+ DAGEngine::~DAGEngine() = default;
15
+
16
+ std::string DAGEngine::version() {
17
+ return "0.1.0";
18
+ }
19
+
20
+ void DAGEngine::add_node(const DAGNode& node) {
21
+ std::lock_guard<std::mutex> lock(mutex_);
22
+ index_[node.id] = nodes_.size();
23
+ nodes_.push_back(node);
24
+ }
25
+
26
+ size_t DAGEngine::node_count() const {
27
+ std::lock_guard<std::mutex> lock(mutex_);
28
+ return nodes_.size();
29
+ }
30
+
31
+ void DAGEngine::clear() {
32
+ std::lock_guard<std::mutex> lock(mutex_);
33
+ nodes_.clear();
34
+ index_.clear();
35
+ }
36
+
37
+ bool DAGEngine::validate() const {
38
+ std::lock_guard<std::mutex> lock(mutex_);
39
+
40
+ // Kahn's algorithm for cycle detection
41
+ std::unordered_map<std::string, int> in_degree;
42
+ for (const auto& node : nodes_) {
43
+ if (in_degree.find(node.id) == in_degree.end()) {
44
+ in_degree[node.id] = 0;
45
+ }
46
+ for (const auto& dep : node.deps) {
47
+ in_degree[node.id]++;
48
+ }
49
+ }
50
+
51
+ std::queue<std::string> ready;
52
+ for (const auto& [id, deg] : in_degree) {
53
+ if (deg == 0) {
54
+ ready.push(id);
55
+ }
56
+ }
57
+
58
+ size_t processed = 0;
59
+ while (!ready.empty()) {
60
+ auto current = ready.front();
61
+ ready.pop();
62
+ processed++;
63
+
64
+ // Find nodes that depend on current
65
+ for (const auto& node : nodes_) {
66
+ for (const auto& dep : node.deps) {
67
+ if (dep == current) {
68
+ in_degree[node.id]--;
69
+ if (in_degree[node.id] == 0) {
70
+ ready.push(node.id);
71
+ }
72
+ }
73
+ }
74
+ }
75
+ }
76
+
77
+ return processed == nodes_.size();
78
+ }
79
+
80
+ bool DAGEngine::execute() {
81
+ // Phase 2: actual concurrent task execution
82
+ if (!validate()) {
83
+ std::cerr << "[DAGEngine] Invalid DAG — contains cycles" << std::endl;
84
+ return false;
85
+ }
86
+
87
+ std::cout << "[DAGEngine] DAG validated. Execution not yet implemented (Phase 2)." << std::endl;
88
+ return true;
89
+ }
90
+
91
+ } // namespace autoforge
cpp/src/main.cpp ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * AutoForge DAG Engine — Entry Point
3
+ *
4
+ * Proves the C++ project compiles and links.
5
+ */
6
+
7
+ #include "dag_engine.h"
8
+ #include <iostream>
9
+
10
+ int main() {
11
+ std::cout << "========================================" << std::endl;
12
+ std::cout << " AutoForge DAG Engine v"
13
+ << autoforge::DAGEngine::version() << std::endl;
14
+ std::cout << "========================================" << std::endl;
15
+
16
+ autoforge::DAGEngine engine;
17
+
18
+ // Add some demo nodes
19
+ engine.add_node({"plan", "Planning", "plan", {}});
20
+ engine.add_node({"code", "Code Generation", "code", {"plan"}});
21
+ engine.add_node({"test", "Test Generation", "test", {"plan"}});
22
+ engine.add_node({"analyze", "Static Analysis", "analyze", {"code"}});
23
+ engine.add_node({"run", "Execute Tests", "execute", {"code", "test"}});
24
+
25
+ std::cout << "Nodes: " << engine.node_count() << std::endl;
26
+ std::cout << "Valid: " << (engine.validate() ? "yes" : "no") << std::endl;
27
+ std::cout << std::endl;
28
+
29
+ engine.execute();
30
+
31
+ return 0;
32
+ }
cpp/tests/test_dag.cpp ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * AutoForge DAG Engine — Basic Tests
3
+ *
4
+ * Minimal test suite without a framework — uses assert.
5
+ */
6
+
7
+ #include "dag_engine.h"
8
+ #include <cassert>
9
+ #include <iostream>
10
+
11
+ void test_version() {
12
+ std::string ver = autoforge::DAGEngine::version();
13
+ assert(!ver.empty());
14
+ std::cout << "[PASS] version(): " << ver << std::endl;
15
+ }
16
+
17
+ void test_add_and_count() {
18
+ autoforge::DAGEngine engine;
19
+ assert(engine.node_count() == 0);
20
+
21
+ engine.add_node({"a", "Node A", "test", {}});
22
+ assert(engine.node_count() == 1);
23
+
24
+ engine.add_node({"b", "Node B", "test", {"a"}});
25
+ assert(engine.node_count() == 2);
26
+
27
+ std::cout << "[PASS] add_node / node_count" << std::endl;
28
+ }
29
+
30
+ void test_clear() {
31
+ autoforge::DAGEngine engine;
32
+ engine.add_node({"a", "Node A", "test", {}});
33
+ engine.add_node({"b", "Node B", "test", {}});
34
+ assert(engine.node_count() == 2);
35
+
36
+ engine.clear();
37
+ assert(engine.node_count() == 0);
38
+
39
+ std::cout << "[PASS] clear()" << std::endl;
40
+ }
41
+
42
+ void test_validate_acyclic() {
43
+ autoforge::DAGEngine engine;
44
+ engine.add_node({"a", "A", "test", {}});
45
+ engine.add_node({"b", "B", "test", {"a"}});
46
+ engine.add_node({"c", "C", "test", {"a", "b"}});
47
+ assert(engine.validate() == true);
48
+
49
+ std::cout << "[PASS] validate() — acyclic" << std::endl;
50
+ }
51
+
52
+ void test_execute() {
53
+ autoforge::DAGEngine engine;
54
+ engine.add_node({"a", "A", "test", {}});
55
+ engine.add_node({"b", "B", "test", {"a"}});
56
+ assert(engine.execute() == true);
57
+
58
+ std::cout << "[PASS] execute()" << std::endl;
59
+ }
60
+
61
+ int main() {
62
+ std::cout << "==============================" << std::endl;
63
+ std::cout << " DAG Engine Tests" << std::endl;
64
+ std::cout << "==============================" << std::endl;
65
+
66
+ test_version();
67
+ test_add_and_count();
68
+ test_clear();
69
+ test_validate_acyclic();
70
+ test_execute();
71
+
72
+ std::cout << std::endl;
73
+ std::cout << "All tests passed!" << std::endl;
74
+ return 0;
75
+ }
data/.gitkeep ADDED
@@ -0,0 +1 @@
 
 
1
+
docker-compose.yml ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: "3.9"
2
+
3
+ services:
4
+ autoforge:
5
+ build:
6
+ context: .
7
+ dockerfile: Dockerfile
8
+ ports:
9
+ - "8000:8000"
10
+ volumes:
11
+ - ./data:/app/data
12
+ env_file:
13
+ - .env
14
+ environment:
15
+ - DATABASE_URL=sqlite+aiosqlite:///data/autoforge.db
16
+ restart: unless-stopped
frontend/css/main.css ADDED
@@ -0,0 +1,891 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ============================================
2
+ AutoForge — Design System
3
+ Dark developer-tool UI with glassmorphism
4
+ ============================================ */
5
+
6
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
7
+
8
+ /* ── CSS Variables ─────────────────────────── */
9
+
10
+ :root {
11
+ /* Base palette */
12
+ --bg-primary: #0a0e17;
13
+ --bg-secondary: #111827;
14
+ --bg-tertiary: #1a2236;
15
+ --bg-card: rgba(17, 24, 39, 0.7);
16
+ --bg-glass: rgba(17, 24, 39, 0.5);
17
+
18
+ /* Accent palette */
19
+ --accent-primary: #6366f1;
20
+ --accent-secondary: #818cf8;
21
+ --accent-glow: rgba(99, 102, 241, 0.3);
22
+ --accent-success: #34d399;
23
+ --accent-warning: #fbbf24;
24
+ --accent-danger: #f87171;
25
+ --accent-info: #38bdf8;
26
+
27
+ /* Gradients */
28
+ --gradient-brand: linear-gradient(135deg, #6366f1 0%, #a855f7 50%, #ec4899 100%);
29
+ --gradient-card: linear-gradient(135deg, rgba(99,102,241,0.08) 0%, rgba(168,85,247,0.05) 100%);
30
+ --gradient-glow: radial-gradient(600px circle at var(--mouse-x, 50%) var(--mouse-y, 50%), rgba(99,102,241,0.06), transparent 40%);
31
+
32
+ /* Text */
33
+ --text-primary: #f1f5f9;
34
+ --text-secondary: #94a3b8;
35
+ --text-muted: #64748b;
36
+ --text-on-accent: #ffffff;
37
+
38
+ /* Borders */
39
+ --border-primary: rgba(148, 163, 184, 0.1);
40
+ --border-secondary: rgba(148, 163, 184, 0.06);
41
+ --border-accent: rgba(99, 102, 241, 0.3);
42
+
43
+ /* Sizing */
44
+ --sidebar-width: 260px;
45
+ --header-height: 64px;
46
+ --radius-sm: 6px;
47
+ --radius-md: 10px;
48
+ --radius-lg: 16px;
49
+ --radius-xl: 20px;
50
+
51
+ /* Shadows */
52
+ --shadow-sm: 0 1px 2px rgba(0,0,0,0.3);
53
+ --shadow-md: 0 4px 12px rgba(0,0,0,0.3);
54
+ --shadow-lg: 0 8px 30px rgba(0,0,0,0.4);
55
+ --shadow-glow: 0 0 20px var(--accent-glow);
56
+
57
+ /* Timing */
58
+ --transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1);
59
+ --transition-normal: 250ms cubic-bezier(0.4, 0, 0.2, 1);
60
+ --transition-slow: 400ms cubic-bezier(0.4, 0, 0.2, 1);
61
+
62
+ /* Typography */
63
+ --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
64
+ --font-mono: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
65
+ }
66
+
67
+
68
+ /* ── Reset ─────────────────────────────────── */
69
+
70
+ *, *::before, *::after {
71
+ box-sizing: border-box;
72
+ margin: 0;
73
+ padding: 0;
74
+ }
75
+
76
+ html {
77
+ font-size: 16px;
78
+ scroll-behavior: smooth;
79
+ -webkit-font-smoothing: antialiased;
80
+ -moz-osx-font-smoothing: grayscale;
81
+ }
82
+
83
+ body {
84
+ font-family: var(--font-sans);
85
+ background: var(--bg-primary);
86
+ color: var(--text-primary);
87
+ line-height: 1.6;
88
+ min-height: 100vh;
89
+ overflow-x: hidden;
90
+ }
91
+
92
+ a {
93
+ color: var(--accent-secondary);
94
+ text-decoration: none;
95
+ transition: color var(--transition-fast);
96
+ }
97
+
98
+ a:hover {
99
+ color: var(--accent-primary);
100
+ }
101
+
102
+ button {
103
+ font-family: inherit;
104
+ cursor: pointer;
105
+ }
106
+
107
+ input, textarea, select {
108
+ font-family: inherit;
109
+ }
110
+
111
+
112
+ /* ── Animated Background ───────────────────── */
113
+
114
+ .bg-mesh {
115
+ position: fixed;
116
+ inset: 0;
117
+ z-index: -1;
118
+ background: var(--bg-primary);
119
+ overflow: hidden;
120
+ }
121
+
122
+ .bg-mesh::before {
123
+ content: '';
124
+ position: absolute;
125
+ top: -50%;
126
+ left: -50%;
127
+ width: 200%;
128
+ height: 200%;
129
+ background:
130
+ radial-gradient(ellipse 600px 600px at 20% 30%, rgba(99,102,241,0.08) 0%, transparent 70%),
131
+ radial-gradient(ellipse 500px 500px at 80% 20%, rgba(168,85,247,0.06) 0%, transparent 70%),
132
+ radial-gradient(ellipse 400px 400px at 60% 80%, rgba(236,72,153,0.04) 0%, transparent 70%);
133
+ animation: meshFloat 20s ease-in-out infinite;
134
+ }
135
+
136
+ @keyframes meshFloat {
137
+ 0%, 100% { transform: translate(0, 0) rotate(0deg); }
138
+ 33% { transform: translate(2%, -2%) rotate(1deg); }
139
+ 66% { transform: translate(-1%, 1%) rotate(-0.5deg); }
140
+ }
141
+
142
+
143
+ /* ── Login Page ────────────────────────────── */
144
+
145
+ .login-container {
146
+ display: flex;
147
+ align-items: center;
148
+ justify-content: center;
149
+ min-height: 100vh;
150
+ padding: 2rem;
151
+ }
152
+
153
+ .login-card {
154
+ width: 100%;
155
+ max-width: 420px;
156
+ background: var(--bg-card);
157
+ backdrop-filter: blur(20px);
158
+ -webkit-backdrop-filter: blur(20px);
159
+ border: 1px solid var(--border-primary);
160
+ border-radius: var(--radius-xl);
161
+ padding: 2.5rem;
162
+ box-shadow: var(--shadow-lg);
163
+ animation: cardAppear 0.6s var(--transition-slow) both;
164
+ }
165
+
166
+ @keyframes cardAppear {
167
+ from {
168
+ opacity: 0;
169
+ transform: translateY(20px) scale(0.97);
170
+ }
171
+ to {
172
+ opacity: 1;
173
+ transform: translateY(0) scale(1);
174
+ }
175
+ }
176
+
177
+ .login-logo {
178
+ text-align: center;
179
+ margin-bottom: 2rem;
180
+ }
181
+
182
+ .login-logo h1 {
183
+ font-size: 1.75rem;
184
+ font-weight: 700;
185
+ background: var(--gradient-brand);
186
+ -webkit-background-clip: text;
187
+ -webkit-text-fill-color: transparent;
188
+ background-clip: text;
189
+ letter-spacing: -0.5px;
190
+ }
191
+
192
+ .login-logo p {
193
+ color: var(--text-muted);
194
+ font-size: 0.875rem;
195
+ margin-top: 0.25rem;
196
+ }
197
+
198
+ .login-tabs {
199
+ display: flex;
200
+ gap: 0;
201
+ margin-bottom: 1.5rem;
202
+ border-radius: var(--radius-md);
203
+ overflow: hidden;
204
+ border: 1px solid var(--border-primary);
205
+ }
206
+
207
+ .login-tab {
208
+ flex: 1;
209
+ padding: 0.625rem;
210
+ background: transparent;
211
+ border: none;
212
+ color: var(--text-muted);
213
+ font-size: 0.875rem;
214
+ font-weight: 500;
215
+ transition: all var(--transition-fast);
216
+ }
217
+
218
+ .login-tab.active {
219
+ background: var(--accent-primary);
220
+ color: var(--text-on-accent);
221
+ }
222
+
223
+ .login-tab:hover:not(.active) {
224
+ color: var(--text-primary);
225
+ background: var(--bg-tertiary);
226
+ }
227
+
228
+
229
+ /* ── Forms ─────────────────────────────────── */
230
+
231
+ .form-group {
232
+ margin-bottom: 1.25rem;
233
+ }
234
+
235
+ .form-label {
236
+ display: block;
237
+ font-size: 0.8125rem;
238
+ font-weight: 500;
239
+ color: var(--text-secondary);
240
+ margin-bottom: 0.375rem;
241
+ letter-spacing: 0.02em;
242
+ }
243
+
244
+ .form-input {
245
+ width: 100%;
246
+ padding: 0.75rem 1rem;
247
+ background: var(--bg-tertiary);
248
+ border: 1px solid var(--border-primary);
249
+ border-radius: var(--radius-md);
250
+ color: var(--text-primary);
251
+ font-size: 0.9375rem;
252
+ transition: all var(--transition-fast);
253
+ outline: none;
254
+ }
255
+
256
+ .form-input::placeholder {
257
+ color: var(--text-muted);
258
+ }
259
+
260
+ .form-input:focus {
261
+ border-color: var(--accent-primary);
262
+ box-shadow: 0 0 0 3px var(--accent-glow);
263
+ }
264
+
265
+ .form-input:hover:not(:focus) {
266
+ border-color: rgba(148, 163, 184, 0.2);
267
+ }
268
+
269
+ .form-error {
270
+ display: none;
271
+ margin-top: 1rem;
272
+ padding: 0.75rem 1rem;
273
+ background: rgba(248, 113, 113, 0.1);
274
+ border: 1px solid rgba(248, 113, 113, 0.2);
275
+ border-radius: var(--radius-sm);
276
+ color: var(--accent-danger);
277
+ font-size: 0.8125rem;
278
+ animation: shake 0.3s ease-in-out;
279
+ }
280
+
281
+ .form-error.visible {
282
+ display: block;
283
+ }
284
+
285
+ @keyframes shake {
286
+ 0%, 100% { transform: translateX(0); }
287
+ 25% { transform: translateX(-4px); }
288
+ 75% { transform: translateX(4px); }
289
+ }
290
+
291
+
292
+ /* ── Buttons ───────────────────────────────── */
293
+
294
+ .btn {
295
+ display: inline-flex;
296
+ align-items: center;
297
+ justify-content: center;
298
+ gap: 0.5rem;
299
+ padding: 0.75rem 1.5rem;
300
+ border: none;
301
+ border-radius: var(--radius-md);
302
+ font-size: 0.9375rem;
303
+ font-weight: 600;
304
+ transition: all var(--transition-fast);
305
+ position: relative;
306
+ overflow: hidden;
307
+ }
308
+
309
+ .btn::after {
310
+ content: '';
311
+ position: absolute;
312
+ inset: 0;
313
+ background: rgba(255,255,255,0);
314
+ transition: background var(--transition-fast);
315
+ }
316
+
317
+ .btn:hover::after {
318
+ background: rgba(255,255,255,0.06);
319
+ }
320
+
321
+ .btn:active {
322
+ transform: scale(0.97);
323
+ }
324
+
325
+ .btn-primary {
326
+ background: var(--gradient-brand);
327
+ color: var(--text-on-accent);
328
+ box-shadow: var(--shadow-glow);
329
+ }
330
+
331
+ .btn-primary:hover {
332
+ box-shadow: 0 0 30px var(--accent-glow);
333
+ }
334
+
335
+ .btn-primary:disabled {
336
+ opacity: 0.5;
337
+ cursor: not-allowed;
338
+ box-shadow: none;
339
+ }
340
+
341
+ .btn-secondary {
342
+ background: var(--bg-tertiary);
343
+ color: var(--text-primary);
344
+ border: 1px solid var(--border-primary);
345
+ }
346
+
347
+ .btn-secondary:hover {
348
+ border-color: var(--border-accent);
349
+ }
350
+
351
+ .btn-block {
352
+ width: 100%;
353
+ }
354
+
355
+ .btn-sm {
356
+ padding: 0.5rem 1rem;
357
+ font-size: 0.8125rem;
358
+ }
359
+
360
+
361
+ /* ── App Layout ────────────────────────────── */
362
+
363
+ .app-layout {
364
+ display: flex;
365
+ min-height: 100vh;
366
+ }
367
+
368
+ .sidebar {
369
+ position: fixed;
370
+ top: 0;
371
+ left: 0;
372
+ width: var(--sidebar-width);
373
+ height: 100vh;
374
+ background: var(--bg-card);
375
+ backdrop-filter: blur(20px);
376
+ -webkit-backdrop-filter: blur(20px);
377
+ border-right: 1px solid var(--border-primary);
378
+ display: flex;
379
+ flex-direction: column;
380
+ z-index: 100;
381
+ transition: transform var(--transition-normal);
382
+ }
383
+
384
+ .sidebar-header {
385
+ padding: 1.5rem;
386
+ border-bottom: 1px solid var(--border-primary);
387
+ }
388
+
389
+ .sidebar-brand {
390
+ font-size: 1.25rem;
391
+ font-weight: 700;
392
+ background: var(--gradient-brand);
393
+ -webkit-background-clip: text;
394
+ -webkit-text-fill-color: transparent;
395
+ background-clip: text;
396
+ }
397
+
398
+ .sidebar-subtitle {
399
+ font-size: 0.75rem;
400
+ color: var(--text-muted);
401
+ margin-top: 0.125rem;
402
+ }
403
+
404
+ .sidebar-nav {
405
+ flex: 1;
406
+ padding: 1rem 0.75rem;
407
+ display: flex;
408
+ flex-direction: column;
409
+ gap: 0.25rem;
410
+ }
411
+
412
+ .nav-link {
413
+ display: flex;
414
+ align-items: center;
415
+ gap: 0.75rem;
416
+ padding: 0.75rem 1rem;
417
+ border-radius: var(--radius-md);
418
+ color: var(--text-secondary);
419
+ font-size: 0.875rem;
420
+ font-weight: 500;
421
+ transition: all var(--transition-fast);
422
+ text-decoration: none;
423
+ position: relative;
424
+ }
425
+
426
+ .nav-link:hover {
427
+ background: var(--bg-tertiary);
428
+ color: var(--text-primary);
429
+ }
430
+
431
+ .nav-link.active {
432
+ background: rgba(99, 102, 241, 0.12);
433
+ color: var(--accent-secondary);
434
+ }
435
+
436
+ .nav-link.active::before {
437
+ content: '';
438
+ position: absolute;
439
+ left: 0;
440
+ top: 50%;
441
+ transform: translateY(-50%);
442
+ width: 3px;
443
+ height: 60%;
444
+ background: var(--accent-primary);
445
+ border-radius: 0 2px 2px 0;
446
+ }
447
+
448
+ .nav-icon {
449
+ font-size: 1.125rem;
450
+ width: 1.25rem;
451
+ text-align: center;
452
+ }
453
+
454
+ .sidebar-footer {
455
+ padding: 1rem 0.75rem;
456
+ border-top: 1px solid var(--border-primary);
457
+ }
458
+
459
+ .user-badge {
460
+ display: flex;
461
+ align-items: center;
462
+ gap: 0.75rem;
463
+ padding: 0.625rem 0.75rem;
464
+ border-radius: var(--radius-md);
465
+ transition: background var(--transition-fast);
466
+ cursor: pointer;
467
+ }
468
+
469
+ .user-badge:hover {
470
+ background: var(--bg-tertiary);
471
+ }
472
+
473
+ .user-avatar {
474
+ width: 36px;
475
+ height: 36px;
476
+ border-radius: 50%;
477
+ background: var(--gradient-brand);
478
+ display: flex;
479
+ align-items: center;
480
+ justify-content: center;
481
+ font-weight: 600;
482
+ font-size: 0.875rem;
483
+ color: var(--text-on-accent);
484
+ flex-shrink: 0;
485
+ }
486
+
487
+ .user-info {
488
+ overflow: hidden;
489
+ }
490
+
491
+ .user-name {
492
+ font-size: 0.8125rem;
493
+ font-weight: 600;
494
+ color: var(--text-primary);
495
+ white-space: nowrap;
496
+ overflow: hidden;
497
+ text-overflow: ellipsis;
498
+ }
499
+
500
+ .user-role {
501
+ font-size: 0.6875rem;
502
+ color: var(--text-muted);
503
+ }
504
+
505
+
506
+ /* ── Main Content ──────────────────────────── */
507
+
508
+ .main-content {
509
+ flex: 1;
510
+ margin-left: var(--sidebar-width);
511
+ min-height: 100vh;
512
+ }
513
+
514
+ .page-header {
515
+ padding: 2rem 2.5rem 1.5rem;
516
+ border-bottom: 1px solid var(--border-secondary);
517
+ }
518
+
519
+ .page-title {
520
+ font-size: 1.5rem;
521
+ font-weight: 700;
522
+ letter-spacing: -0.5px;
523
+ }
524
+
525
+ .page-subtitle {
526
+ font-size: 0.875rem;
527
+ color: var(--text-muted);
528
+ margin-top: 0.25rem;
529
+ }
530
+
531
+ .page-body {
532
+ padding: 2rem 2.5rem;
533
+ }
534
+
535
+
536
+ /* ── Cards ─────────────────────────────────── */
537
+
538
+ .card {
539
+ background: var(--bg-card);
540
+ backdrop-filter: blur(12px);
541
+ border: 1px solid var(--border-primary);
542
+ border-radius: var(--radius-lg);
543
+ padding: 1.5rem;
544
+ transition: all var(--transition-normal);
545
+ }
546
+
547
+ .card:hover {
548
+ border-color: var(--border-accent);
549
+ box-shadow: var(--shadow-glow);
550
+ }
551
+
552
+ .card-header {
553
+ display: flex;
554
+ align-items: center;
555
+ justify-content: space-between;
556
+ margin-bottom: 1rem;
557
+ }
558
+
559
+ .card-title {
560
+ font-size: 1rem;
561
+ font-weight: 600;
562
+ }
563
+
564
+
565
+ /* ── Stats Grid ────────────────────────────── */
566
+
567
+ .stats-grid {
568
+ display: grid;
569
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
570
+ gap: 1.25rem;
571
+ margin-bottom: 2rem;
572
+ }
573
+
574
+ .stat-card {
575
+ background: var(--gradient-card);
576
+ border: 1px solid var(--border-primary);
577
+ border-radius: var(--radius-lg);
578
+ padding: 1.25rem 1.5rem;
579
+ transition: all var(--transition-normal);
580
+ }
581
+
582
+ .stat-card:hover {
583
+ border-color: var(--border-accent);
584
+ transform: translateY(-2px);
585
+ box-shadow: var(--shadow-glow);
586
+ }
587
+
588
+ .stat-label {
589
+ font-size: 0.75rem;
590
+ font-weight: 500;
591
+ color: var(--text-muted);
592
+ text-transform: uppercase;
593
+ letter-spacing: 0.06em;
594
+ }
595
+
596
+ .stat-value {
597
+ font-size: 2rem;
598
+ font-weight: 700;
599
+ margin-top: 0.25rem;
600
+ background: var(--gradient-brand);
601
+ -webkit-background-clip: text;
602
+ -webkit-text-fill-color: transparent;
603
+ background-clip: text;
604
+ }
605
+
606
+ .stat-change {
607
+ font-size: 0.75rem;
608
+ color: var(--accent-success);
609
+ margin-top: 0.25rem;
610
+ }
611
+
612
+
613
+ /* ── Workflow List ─────────────────────────── */
614
+
615
+ .workflow-list {
616
+ display: flex;
617
+ flex-direction: column;
618
+ gap: 0.75rem;
619
+ }
620
+
621
+ .workflow-item {
622
+ display: flex;
623
+ align-items: center;
624
+ gap: 1rem;
625
+ padding: 1rem 1.25rem;
626
+ background: var(--bg-card);
627
+ border: 1px solid var(--border-primary);
628
+ border-radius: var(--radius-md);
629
+ transition: all var(--transition-fast);
630
+ cursor: pointer;
631
+ }
632
+
633
+ .workflow-item:hover {
634
+ border-color: var(--border-accent);
635
+ background: var(--bg-tertiary);
636
+ }
637
+
638
+ .workflow-status {
639
+ width: 10px;
640
+ height: 10px;
641
+ border-radius: 50%;
642
+ flex-shrink: 0;
643
+ }
644
+
645
+ .workflow-status.pending { background: var(--accent-warning); box-shadow: 0 0 8px rgba(251,191,36,0.4); }
646
+ .workflow-status.running { background: var(--accent-info); box-shadow: 0 0 8px rgba(56,189,248,0.4); animation: pulse 2s infinite; }
647
+ .workflow-status.success { background: var(--accent-success); box-shadow: 0 0 8px rgba(52,211,153,0.4); }
648
+ .workflow-status.failed { background: var(--accent-danger); box-shadow: 0 0 8px rgba(248,113,113,0.4); }
649
+
650
+ @keyframes pulse {
651
+ 0%, 100% { opacity: 1; }
652
+ 50% { opacity: 0.4; }
653
+ }
654
+
655
+ .workflow-details {
656
+ flex: 1;
657
+ min-width: 0;
658
+ }
659
+
660
+ .workflow-prompt {
661
+ font-size: 0.875rem;
662
+ font-weight: 500;
663
+ white-space: nowrap;
664
+ overflow: hidden;
665
+ text-overflow: ellipsis;
666
+ }
667
+
668
+ .workflow-meta {
669
+ font-size: 0.75rem;
670
+ color: var(--text-muted);
671
+ margin-top: 0.125rem;
672
+ }
673
+
674
+ .workflow-arrow {
675
+ color: var(--text-muted);
676
+ transition: transform var(--transition-fast);
677
+ }
678
+
679
+ .workflow-item:hover .workflow-arrow {
680
+ transform: translateX(4px);
681
+ color: var(--accent-secondary);
682
+ }
683
+
684
+
685
+ /* ── Create Workflow ───────────────────────── */
686
+
687
+ .create-section {
688
+ margin-bottom: 2rem;
689
+ }
690
+
691
+ .prompt-input {
692
+ width: 100%;
693
+ min-height: 120px;
694
+ padding: 1rem;
695
+ background: var(--bg-tertiary);
696
+ border: 1px solid var(--border-primary);
697
+ border-radius: var(--radius-md);
698
+ color: var(--text-primary);
699
+ font-family: var(--font-mono);
700
+ font-size: 0.875rem;
701
+ resize: vertical;
702
+ transition: all var(--transition-fast);
703
+ outline: none;
704
+ }
705
+
706
+ .prompt-input::placeholder {
707
+ color: var(--text-muted);
708
+ }
709
+
710
+ .prompt-input:focus {
711
+ border-color: var(--accent-primary);
712
+ box-shadow: 0 0 0 3px var(--accent-glow);
713
+ }
714
+
715
+ .create-actions {
716
+ display: flex;
717
+ justify-content: flex-end;
718
+ margin-top: 1rem;
719
+ }
720
+
721
+
722
+ /* ── Empty State ───────────────────────────── */
723
+
724
+ .empty-state {
725
+ display: flex;
726
+ flex-direction: column;
727
+ align-items: center;
728
+ justify-content: center;
729
+ padding: 3rem;
730
+ text-align: center;
731
+ }
732
+
733
+ .empty-icon {
734
+ font-size: 3rem;
735
+ margin-bottom: 1rem;
736
+ opacity: 0.3;
737
+ }
738
+
739
+ .empty-title {
740
+ font-size: 1.125rem;
741
+ font-weight: 600;
742
+ margin-bottom: 0.5rem;
743
+ }
744
+
745
+ .empty-text {
746
+ font-size: 0.875rem;
747
+ color: var(--text-muted);
748
+ max-width: 360px;
749
+ }
750
+
751
+
752
+ /* ── Status Badge ──────────────────────────── */
753
+
754
+ .badge {
755
+ display: inline-flex;
756
+ align-items: center;
757
+ padding: 0.25rem 0.625rem;
758
+ border-radius: 100px;
759
+ font-size: 0.6875rem;
760
+ font-weight: 600;
761
+ text-transform: uppercase;
762
+ letter-spacing: 0.04em;
763
+ }
764
+
765
+ .badge-pending { background: rgba(251,191,36,0.15); color: var(--accent-warning); }
766
+ .badge-running { background: rgba(56,189,248,0.15); color: var(--accent-info); }
767
+ .badge-success { background: rgba(52,211,153,0.15); color: var(--accent-success); }
768
+ .badge-failed { background: rgba(248,113,113,0.15); color: var(--accent-danger); }
769
+
770
+
771
+ /* ── Spinner ───────────────────────────────── */
772
+
773
+ .spinner {
774
+ width: 20px;
775
+ height: 20px;
776
+ border: 2px solid rgba(255,255,255,0.1);
777
+ border-top-color: var(--accent-primary);
778
+ border-radius: 50%;
779
+ animation: spin 0.6s linear infinite;
780
+ }
781
+
782
+ @keyframes spin {
783
+ to { transform: rotate(360deg); }
784
+ }
785
+
786
+
787
+ /* ── Responsive ────────────────────────────── */
788
+
789
+ @media (max-width: 768px) {
790
+ .sidebar {
791
+ transform: translateX(-100%);
792
+ }
793
+
794
+ .sidebar.open {
795
+ transform: translateX(0);
796
+ }
797
+
798
+ .main-content {
799
+ margin-left: 0;
800
+ }
801
+
802
+ .page-header,
803
+ .page-body {
804
+ padding-left: 1.25rem;
805
+ padding-right: 1.25rem;
806
+ }
807
+
808
+ .stats-grid {
809
+ grid-template-columns: 1fr;
810
+ }
811
+ }
812
+
813
+
814
+ /* ── Scrollbar ─────────────────────────────── */
815
+
816
+ ::-webkit-scrollbar {
817
+ width: 6px;
818
+ height: 6px;
819
+ }
820
+
821
+ ::-webkit-scrollbar-track {
822
+ background: transparent;
823
+ }
824
+
825
+ ::-webkit-scrollbar-thumb {
826
+ background: var(--border-primary);
827
+ border-radius: 3px;
828
+ }
829
+
830
+ ::-webkit-scrollbar-thumb:hover {
831
+ background: var(--text-muted);
832
+ }
833
+
834
+
835
+ /* ── Toast ─────────────────────────────────── */
836
+
837
+ .toast-container {
838
+ position: fixed;
839
+ top: 1.5rem;
840
+ right: 1.5rem;
841
+ z-index: 9999;
842
+ display: flex;
843
+ flex-direction: column;
844
+ gap: 0.75rem;
845
+ }
846
+
847
+ .toast {
848
+ padding: 0.875rem 1.25rem;
849
+ background: var(--bg-card);
850
+ backdrop-filter: blur(20px);
851
+ border: 1px solid var(--border-primary);
852
+ border-radius: var(--radius-md);
853
+ box-shadow: var(--shadow-lg);
854
+ font-size: 0.875rem;
855
+ animation: toastIn 0.3s ease-out;
856
+ max-width: 380px;
857
+ }
858
+
859
+ .toast.success { border-left: 3px solid var(--accent-success); }
860
+ .toast.error { border-left: 3px solid var(--accent-danger); }
861
+ .toast.info { border-left: 3px solid var(--accent-info); }
862
+
863
+ @keyframes toastIn {
864
+ from { opacity: 0; transform: translateX(20px); }
865
+ to { opacity: 1; transform: translateX(0); }
866
+ }
867
+
868
+ .toast-exit {
869
+ animation: toastOut 0.2s ease-in forwards;
870
+ }
871
+
872
+ @keyframes toastOut {
873
+ to { opacity: 0; transform: translateX(20px); }
874
+ }
875
+
876
+
877
+ /* ── Page Transitions ──────────────────────── */
878
+
879
+ .page-section {
880
+ display: none;
881
+ }
882
+
883
+ .page-section.active {
884
+ display: block;
885
+ animation: fadeIn 0.3s ease-out;
886
+ }
887
+
888
+ @keyframes fadeIn {
889
+ from { opacity: 0; transform: translateY(8px); }
890
+ to { opacity: 1; transform: translateY(0); }
891
+ }
frontend/css/style.css ADDED
@@ -0,0 +1,894 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ═══════════════════════════════════════════════════════════════════
2
+ AutoForge — Design System
3
+ Dark developer-tool aesthetic with glassmorphism
4
+ ═══════════════════════════════════════════════════════════════════ */
5
+
6
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
7
+
8
+ /* ── CSS Custom Properties ───────────────────────────────────────── */
9
+
10
+ :root {
11
+ /* Background */
12
+ --bg-primary: #0a0a0f;
13
+ --bg-secondary: #12121a;
14
+ --bg-tertiary: #1a1a28;
15
+ --bg-surface: rgba(255, 255, 255, 0.03);
16
+ --bg-surface-hover: rgba(255, 255, 255, 0.06);
17
+ --bg-glass: rgba(18, 18, 26, 0.75);
18
+
19
+ /* Accent gradient */
20
+ --accent-blue: #00d4ff;
21
+ --accent-purple: #7b2ff7;
22
+ --accent-pink: #f72fff;
23
+ --gradient-primary: linear-gradient(135deg, var(--accent-blue), var(--accent-purple));
24
+ --gradient-secondary: linear-gradient(135deg, var(--accent-purple), var(--accent-pink));
25
+ --gradient-glow: linear-gradient(135deg, rgba(0, 212, 255, 0.15), rgba(123, 47, 247, 0.15));
26
+
27
+ /* Text */
28
+ --text-primary: #e8e8f0;
29
+ --text-secondary: #9898b0;
30
+ --text-muted: #5e5e78;
31
+ --text-accent: var(--accent-blue);
32
+
33
+ /* Status */
34
+ --status-success: #00e88f;
35
+ --status-warning: #ffb800;
36
+ --status-error: #ff4060;
37
+ --status-pending: #5e5e78;
38
+
39
+ /* Borders */
40
+ --border-subtle: rgba(255, 255, 255, 0.06);
41
+ --border-medium: rgba(255, 255, 255, 0.1);
42
+ --border-accent: rgba(0, 212, 255, 0.3);
43
+
44
+ /* Shadows */
45
+ --shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.3);
46
+ --shadow-md: 0 4px 20px rgba(0, 0, 0, 0.4);
47
+ --shadow-lg: 0 8px 40px rgba(0, 0, 0, 0.5);
48
+ --shadow-glow: 0 0 30px rgba(0, 212, 255, 0.1);
49
+
50
+ /* Spacing */
51
+ --space-xs: 0.25rem;
52
+ --space-sm: 0.5rem;
53
+ --space-md: 1rem;
54
+ --space-lg: 1.5rem;
55
+ --space-xl: 2rem;
56
+ --space-2xl: 3rem;
57
+
58
+ /* Radius */
59
+ --radius-sm: 6px;
60
+ --radius-md: 10px;
61
+ --radius-lg: 16px;
62
+ --radius-xl: 24px;
63
+
64
+ /* Typography */
65
+ --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
66
+ --font-mono: 'JetBrains Mono', 'Fira Code', monospace;
67
+
68
+ /* Transitions */
69
+ --transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1);
70
+ --transition-base: 250ms cubic-bezier(0.4, 0, 0.2, 1);
71
+ --transition-slow: 400ms cubic-bezier(0.4, 0, 0.2, 1);
72
+
73
+ /* Layout */
74
+ --sidebar-width: 260px;
75
+ --header-height: 64px;
76
+ }
77
+
78
+ /* ── Reset ───────────────────────────────────────────────────────── */
79
+
80
+ *, *::before, *::after {
81
+ margin: 0;
82
+ padding: 0;
83
+ box-sizing: border-box;
84
+ }
85
+
86
+ html {
87
+ font-size: 16px;
88
+ -webkit-font-smoothing: antialiased;
89
+ -moz-osx-font-smoothing: grayscale;
90
+ }
91
+
92
+ body {
93
+ font-family: var(--font-sans);
94
+ background: var(--bg-primary);
95
+ color: var(--text-primary);
96
+ line-height: 1.6;
97
+ min-height: 100vh;
98
+ overflow-x: hidden;
99
+ }
100
+
101
+ a {
102
+ color: var(--text-accent);
103
+ text-decoration: none;
104
+ transition: color var(--transition-fast);
105
+ }
106
+
107
+ a:hover {
108
+ color: var(--accent-purple);
109
+ }
110
+
111
+ /* ── Scrollbar ───────────────────────────────────────────────────── */
112
+
113
+ ::-webkit-scrollbar {
114
+ width: 8px;
115
+ height: 8px;
116
+ }
117
+
118
+ ::-webkit-scrollbar-track {
119
+ background: var(--bg-secondary);
120
+ }
121
+
122
+ ::-webkit-scrollbar-thumb {
123
+ background: var(--bg-tertiary);
124
+ border-radius: 4px;
125
+ }
126
+
127
+ ::-webkit-scrollbar-thumb:hover {
128
+ background: rgba(255, 255, 255, 0.15);
129
+ }
130
+
131
+ /* ── Glass Panel ─────────────────────────────────────────────────── */
132
+
133
+ .glass {
134
+ background: var(--bg-glass);
135
+ backdrop-filter: blur(16px);
136
+ -webkit-backdrop-filter: blur(16px);
137
+ border: 1px solid var(--border-subtle);
138
+ border-radius: var(--radius-lg);
139
+ }
140
+
141
+ .glass-strong {
142
+ background: rgba(18, 18, 26, 0.9);
143
+ backdrop-filter: blur(24px);
144
+ -webkit-backdrop-filter: blur(24px);
145
+ border: 1px solid var(--border-medium);
146
+ border-radius: var(--radius-lg);
147
+ }
148
+
149
+ /* ── Layout ──────────────────────────────────────────────────────── */
150
+
151
+ .app-layout {
152
+ display: flex;
153
+ min-height: 100vh;
154
+ }
155
+
156
+ .sidebar {
157
+ position: fixed;
158
+ left: 0;
159
+ top: 0;
160
+ width: var(--sidebar-width);
161
+ height: 100vh;
162
+ background: var(--bg-glass);
163
+ backdrop-filter: blur(20px);
164
+ -webkit-backdrop-filter: blur(20px);
165
+ border-right: 1px solid var(--border-subtle);
166
+ display: flex;
167
+ flex-direction: column;
168
+ z-index: 100;
169
+ transition: transform var(--transition-base);
170
+ }
171
+
172
+ .sidebar-header {
173
+ padding: var(--space-xl);
174
+ border-bottom: 1px solid var(--border-subtle);
175
+ }
176
+
177
+ .sidebar-brand {
178
+ display: flex;
179
+ align-items: center;
180
+ gap: var(--space-sm);
181
+ }
182
+
183
+ .sidebar-brand .logo {
184
+ width: 36px;
185
+ height: 36px;
186
+ background: var(--gradient-primary);
187
+ border-radius: var(--radius-sm);
188
+ display: flex;
189
+ align-items: center;
190
+ justify-content: center;
191
+ font-weight: 700;
192
+ font-size: 1.1rem;
193
+ color: #fff;
194
+ }
195
+
196
+ .sidebar-brand h1 {
197
+ font-size: 1.2rem;
198
+ font-weight: 700;
199
+ background: var(--gradient-primary);
200
+ -webkit-background-clip: text;
201
+ -webkit-text-fill-color: transparent;
202
+ background-clip: text;
203
+ }
204
+
205
+ .sidebar-nav {
206
+ flex: 1;
207
+ padding: var(--space-md);
208
+ display: flex;
209
+ flex-direction: column;
210
+ gap: var(--space-xs);
211
+ }
212
+
213
+ .nav-item {
214
+ display: flex;
215
+ align-items: center;
216
+ gap: var(--space-md);
217
+ padding: var(--space-sm) var(--space-md);
218
+ border-radius: var(--radius-md);
219
+ color: var(--text-secondary);
220
+ font-size: 0.9rem;
221
+ font-weight: 500;
222
+ transition: all var(--transition-fast);
223
+ cursor: pointer;
224
+ border: 1px solid transparent;
225
+ text-decoration: none;
226
+ }
227
+
228
+ .nav-item:hover {
229
+ background: var(--bg-surface-hover);
230
+ color: var(--text-primary);
231
+ border-color: var(--border-subtle);
232
+ }
233
+
234
+ .nav-item.active {
235
+ background: var(--gradient-glow);
236
+ color: var(--text-accent);
237
+ border-color: var(--border-accent);
238
+ }
239
+
240
+ .nav-item .icon {
241
+ width: 20px;
242
+ height: 20px;
243
+ display: flex;
244
+ align-items: center;
245
+ justify-content: center;
246
+ font-size: 1.1rem;
247
+ }
248
+
249
+ .sidebar-footer {
250
+ padding: var(--space-md) var(--space-xl);
251
+ border-top: 1px solid var(--border-subtle);
252
+ }
253
+
254
+ .main-content {
255
+ margin-left: var(--sidebar-width);
256
+ flex: 1;
257
+ padding: var(--space-2xl);
258
+ min-height: 100vh;
259
+ }
260
+
261
+ /* ── Page Header ─────────────────────────────────────────────────── */
262
+
263
+ .page-header {
264
+ margin-bottom: var(--space-2xl);
265
+ }
266
+
267
+ .page-header h2 {
268
+ font-size: 1.75rem;
269
+ font-weight: 700;
270
+ margin-bottom: var(--space-xs);
271
+ }
272
+
273
+ .page-header p {
274
+ color: var(--text-secondary);
275
+ font-size: 0.95rem;
276
+ }
277
+
278
+ /* ── Cards ───────────────────────────────────────────────────────── */
279
+
280
+ .card {
281
+ background: var(--bg-surface);
282
+ border: 1px solid var(--border-subtle);
283
+ border-radius: var(--radius-lg);
284
+ padding: var(--space-xl);
285
+ transition: all var(--transition-base);
286
+ }
287
+
288
+ .card:hover {
289
+ border-color: var(--border-medium);
290
+ box-shadow: var(--shadow-md);
291
+ transform: translateY(-2px);
292
+ }
293
+
294
+ .card-grid {
295
+ display: grid;
296
+ grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
297
+ gap: var(--space-lg);
298
+ }
299
+
300
+ .card-header {
301
+ display: flex;
302
+ align-items: center;
303
+ justify-content: space-between;
304
+ margin-bottom: var(--space-md);
305
+ }
306
+
307
+ .card-title {
308
+ font-size: 1rem;
309
+ font-weight: 600;
310
+ }
311
+
312
+ /* ── Status Badges ───────────────────────────────────────────────── */
313
+
314
+ .badge {
315
+ display: inline-flex;
316
+ align-items: center;
317
+ padding: var(--space-xs) var(--space-sm);
318
+ border-radius: 999px;
319
+ font-size: 0.75rem;
320
+ font-weight: 600;
321
+ text-transform: uppercase;
322
+ letter-spacing: 0.05em;
323
+ }
324
+
325
+ .badge-pending {
326
+ background: rgba(94, 94, 120, 0.2);
327
+ color: var(--status-pending);
328
+ border: 1px solid rgba(94, 94, 120, 0.3);
329
+ }
330
+
331
+ .badge-running {
332
+ background: rgba(0, 212, 255, 0.1);
333
+ color: var(--accent-blue);
334
+ border: 1px solid rgba(0, 212, 255, 0.3);
335
+ animation: pulse-glow 2s infinite;
336
+ }
337
+
338
+ .badge-completed {
339
+ background: rgba(0, 232, 143, 0.1);
340
+ color: var(--status-success);
341
+ border: 1px solid rgba(0, 232, 143, 0.3);
342
+ }
343
+
344
+ .badge-failed {
345
+ background: rgba(255, 64, 96, 0.1);
346
+ color: var(--status-error);
347
+ border: 1px solid rgba(255, 64, 96, 0.3);
348
+ }
349
+
350
+ /* ── Buttons ─────────────────────────────────────────────────────── */
351
+
352
+ .btn {
353
+ display: inline-flex;
354
+ align-items: center;
355
+ justify-content: center;
356
+ gap: var(--space-sm);
357
+ padding: 0.65rem 1.5rem;
358
+ border-radius: var(--radius-md);
359
+ font-family: var(--font-sans);
360
+ font-size: 0.9rem;
361
+ font-weight: 600;
362
+ cursor: pointer;
363
+ border: 1px solid transparent;
364
+ transition: all var(--transition-fast);
365
+ outline: none;
366
+ }
367
+
368
+ .btn:active {
369
+ transform: scale(0.97);
370
+ }
371
+
372
+ .btn-primary {
373
+ background: var(--gradient-primary);
374
+ color: #fff;
375
+ border: none;
376
+ box-shadow: 0 4px 15px rgba(0, 212, 255, 0.25);
377
+ }
378
+
379
+ .btn-primary:hover {
380
+ box-shadow: 0 6px 25px rgba(0, 212, 255, 0.35);
381
+ transform: translateY(-1px);
382
+ }
383
+
384
+ .btn-secondary {
385
+ background: var(--bg-surface);
386
+ color: var(--text-primary);
387
+ border: 1px solid var(--border-medium);
388
+ }
389
+
390
+ .btn-secondary:hover {
391
+ background: var(--bg-surface-hover);
392
+ border-color: var(--border-accent);
393
+ }
394
+
395
+ .btn-ghost {
396
+ background: transparent;
397
+ color: var(--text-secondary);
398
+ border: none;
399
+ }
400
+
401
+ .btn-ghost:hover {
402
+ color: var(--text-primary);
403
+ background: var(--bg-surface);
404
+ }
405
+
406
+ .btn-danger {
407
+ background: rgba(255, 64, 96, 0.15);
408
+ color: var(--status-error);
409
+ border: 1px solid rgba(255, 64, 96, 0.3);
410
+ }
411
+
412
+ .btn-danger:hover {
413
+ background: rgba(255, 64, 96, 0.25);
414
+ }
415
+
416
+ .btn-lg {
417
+ padding: 0.85rem 2rem;
418
+ font-size: 1rem;
419
+ }
420
+
421
+ .btn-sm {
422
+ padding: 0.4rem 0.9rem;
423
+ font-size: 0.8rem;
424
+ }
425
+
426
+ /* ── Forms ────────────────────────────────────────────────────────── */
427
+
428
+ .form-group {
429
+ margin-bottom: var(--space-lg);
430
+ }
431
+
432
+ .form-label {
433
+ display: block;
434
+ font-size: 0.85rem;
435
+ font-weight: 500;
436
+ color: var(--text-secondary);
437
+ margin-bottom: var(--space-sm);
438
+ text-transform: uppercase;
439
+ letter-spacing: 0.05em;
440
+ }
441
+
442
+ .form-input {
443
+ width: 100%;
444
+ padding: 0.75rem 1rem;
445
+ background: var(--bg-surface);
446
+ border: 1px solid var(--border-subtle);
447
+ border-radius: var(--radius-md);
448
+ color: var(--text-primary);
449
+ font-family: var(--font-sans);
450
+ font-size: 0.95rem;
451
+ transition: all var(--transition-fast);
452
+ outline: none;
453
+ }
454
+
455
+ .form-input:focus {
456
+ border-color: var(--accent-blue);
457
+ box-shadow: 0 0 0 3px rgba(0, 212, 255, 0.1);
458
+ }
459
+
460
+ .form-input::placeholder {
461
+ color: var(--text-muted);
462
+ }
463
+
464
+ textarea.form-input {
465
+ resize: vertical;
466
+ min-height: 120px;
467
+ font-family: var(--font-mono);
468
+ font-size: 0.9rem;
469
+ line-height: 1.7;
470
+ }
471
+
472
+ /* ── Auth Layout ─────────────────────────────────────────────────── */
473
+
474
+ .auth-layout {
475
+ display: flex;
476
+ align-items: center;
477
+ justify-content: center;
478
+ min-height: 100vh;
479
+ padding: var(--space-xl);
480
+ position: relative;
481
+ }
482
+
483
+ .auth-layout::before {
484
+ content: '';
485
+ position: absolute;
486
+ top: -50%;
487
+ left: -50%;
488
+ width: 200%;
489
+ height: 200%;
490
+ background: radial-gradient(ellipse at 30% 20%, rgba(0, 212, 255, 0.08) 0%, transparent 50%),
491
+ radial-gradient(ellipse at 70% 80%, rgba(123, 47, 247, 0.08) 0%, transparent 50%);
492
+ animation: bg-drift 20s ease-in-out infinite alternate;
493
+ pointer-events: none;
494
+ }
495
+
496
+ .auth-card {
497
+ width: 100%;
498
+ max-width: 440px;
499
+ padding: var(--space-2xl);
500
+ position: relative;
501
+ z-index: 1;
502
+ }
503
+
504
+ .auth-header {
505
+ text-align: center;
506
+ margin-bottom: var(--space-2xl);
507
+ }
508
+
509
+ .auth-header .logo-large {
510
+ width: 64px;
511
+ height: 64px;
512
+ background: var(--gradient-primary);
513
+ border-radius: var(--radius-md);
514
+ display: flex;
515
+ align-items: center;
516
+ justify-content: center;
517
+ font-size: 1.8rem;
518
+ font-weight: 700;
519
+ color: #fff;
520
+ margin: 0 auto var(--space-lg);
521
+ box-shadow: 0 8px 30px rgba(0, 212, 255, 0.25);
522
+ }
523
+
524
+ .auth-header h2 {
525
+ font-size: 1.5rem;
526
+ font-weight: 700;
527
+ margin-bottom: var(--space-xs);
528
+ }
529
+
530
+ .auth-header p {
531
+ color: var(--text-secondary);
532
+ font-size: 0.9rem;
533
+ }
534
+
535
+ .auth-toggle {
536
+ text-align: center;
537
+ margin-top: var(--space-xl);
538
+ color: var(--text-secondary);
539
+ font-size: 0.9rem;
540
+ }
541
+
542
+ .auth-toggle a {
543
+ font-weight: 600;
544
+ }
545
+
546
+ .form-error {
547
+ color: var(--status-error);
548
+ font-size: 0.85rem;
549
+ margin-top: var(--space-sm);
550
+ display: none;
551
+ }
552
+
553
+ .form-error.visible {
554
+ display: block;
555
+ }
556
+
557
+ /* ── Workflow Card ───────────────────────────────────────────────── */
558
+
559
+ .workflow-card {
560
+ cursor: pointer;
561
+ }
562
+
563
+ .workflow-card .prompt-preview {
564
+ color: var(--text-secondary);
565
+ font-size: 0.9rem;
566
+ line-height: 1.5;
567
+ margin-bottom: var(--space-md);
568
+ display: -webkit-box;
569
+ -webkit-line-clamp: 3;
570
+ -webkit-box-orient: vertical;
571
+ overflow: hidden;
572
+ }
573
+
574
+ .workflow-card .meta {
575
+ display: flex;
576
+ align-items: center;
577
+ justify-content: space-between;
578
+ font-size: 0.8rem;
579
+ color: var(--text-muted);
580
+ }
581
+
582
+ /* ── Create Workflow Card ────────────────────────────────────────── */
583
+
584
+ .create-card {
585
+ border: 2px dashed var(--border-medium);
586
+ background: transparent;
587
+ display: flex;
588
+ flex-direction: column;
589
+ align-items: center;
590
+ justify-content: center;
591
+ min-height: 200px;
592
+ cursor: pointer;
593
+ transition: all var(--transition-base);
594
+ }
595
+
596
+ .create-card:hover {
597
+ border-color: var(--accent-blue);
598
+ background: var(--gradient-glow);
599
+ }
600
+
601
+ .create-card .icon-large {
602
+ width: 48px;
603
+ height: 48px;
604
+ border-radius: 50%;
605
+ background: var(--gradient-primary);
606
+ display: flex;
607
+ align-items: center;
608
+ justify-content: center;
609
+ font-size: 1.5rem;
610
+ color: #fff;
611
+ margin-bottom: var(--space-md);
612
+ }
613
+
614
+ .create-card p {
615
+ color: var(--text-secondary);
616
+ font-weight: 500;
617
+ }
618
+
619
+ /* ── Modal ────────────────────────────────────────────────────────── */
620
+
621
+ .modal-overlay {
622
+ position: fixed;
623
+ inset: 0;
624
+ background: rgba(0, 0, 0, 0.6);
625
+ backdrop-filter: blur(4px);
626
+ display: flex;
627
+ align-items: center;
628
+ justify-content: center;
629
+ z-index: 1000;
630
+ opacity: 0;
631
+ visibility: hidden;
632
+ transition: all var(--transition-base);
633
+ }
634
+
635
+ .modal-overlay.active {
636
+ opacity: 1;
637
+ visibility: visible;
638
+ }
639
+
640
+ .modal {
641
+ width: 100%;
642
+ max-width: 560px;
643
+ padding: var(--space-2xl);
644
+ transform: translateY(20px) scale(0.95);
645
+ transition: transform var(--transition-base);
646
+ }
647
+
648
+ .modal-overlay.active .modal {
649
+ transform: translateY(0) scale(1);
650
+ }
651
+
652
+ .modal-header {
653
+ display: flex;
654
+ align-items: center;
655
+ justify-content: space-between;
656
+ margin-bottom: var(--space-xl);
657
+ }
658
+
659
+ .modal-header h3 {
660
+ font-size: 1.25rem;
661
+ font-weight: 700;
662
+ }
663
+
664
+ .modal-close {
665
+ width: 32px;
666
+ height: 32px;
667
+ border-radius: 50%;
668
+ background: var(--bg-surface);
669
+ border: 1px solid var(--border-subtle);
670
+ color: var(--text-secondary);
671
+ cursor: pointer;
672
+ display: flex;
673
+ align-items: center;
674
+ justify-content: center;
675
+ font-size: 1.1rem;
676
+ transition: all var(--transition-fast);
677
+ }
678
+
679
+ .modal-close:hover {
680
+ background: var(--bg-surface-hover);
681
+ color: var(--text-primary);
682
+ }
683
+
684
+ /* ── Stats Bar ───────────────────────────────────────────────────── */
685
+
686
+ .stats-bar {
687
+ display: grid;
688
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
689
+ gap: var(--space-lg);
690
+ margin-bottom: var(--space-2xl);
691
+ }
692
+
693
+ .stat-card {
694
+ padding: var(--space-lg);
695
+ }
696
+
697
+ .stat-card .stat-value {
698
+ font-size: 2rem;
699
+ font-weight: 700;
700
+ background: var(--gradient-primary);
701
+ -webkit-background-clip: text;
702
+ -webkit-text-fill-color: transparent;
703
+ background-clip: text;
704
+ }
705
+
706
+ .stat-card .stat-label {
707
+ font-size: 0.8rem;
708
+ color: var(--text-muted);
709
+ text-transform: uppercase;
710
+ letter-spacing: 0.06em;
711
+ margin-top: var(--space-xs);
712
+ }
713
+
714
+ /* ── Empty State ─────────────────────────────────────────────────── */
715
+
716
+ .empty-state {
717
+ text-align: center;
718
+ padding: var(--space-2xl) * 2;
719
+ color: var(--text-muted);
720
+ }
721
+
722
+ .empty-state .icon-empty {
723
+ font-size: 3rem;
724
+ margin-bottom: var(--space-lg);
725
+ opacity: 0.5;
726
+ }
727
+
728
+ .empty-state h3 {
729
+ font-size: 1.1rem;
730
+ color: var(--text-secondary);
731
+ margin-bottom: var(--space-sm);
732
+ }
733
+
734
+ /* ── Loading Spinner ─────────────────────────────────────────────── */
735
+
736
+ .spinner {
737
+ width: 24px;
738
+ height: 24px;
739
+ border: 3px solid var(--border-subtle);
740
+ border-top-color: var(--accent-blue);
741
+ border-radius: 50%;
742
+ animation: spin 0.8s linear infinite;
743
+ }
744
+
745
+ .spinner-sm {
746
+ width: 16px;
747
+ height: 16px;
748
+ border-width: 2px;
749
+ }
750
+
751
+ /* ── Notifications ───────────────────────────────────────────────── */
752
+
753
+ .toast {
754
+ position: fixed;
755
+ bottom: var(--space-xl);
756
+ right: var(--space-xl);
757
+ padding: var(--space-md) var(--space-lg);
758
+ border-radius: var(--radius-md);
759
+ font-size: 0.9rem;
760
+ font-weight: 500;
761
+ z-index: 2000;
762
+ transform: translateY(100px);
763
+ opacity: 0;
764
+ transition: all var(--transition-base);
765
+ }
766
+
767
+ .toast.visible {
768
+ transform: translateY(0);
769
+ opacity: 1;
770
+ }
771
+
772
+ .toast-success {
773
+ background: rgba(0, 232, 143, 0.15);
774
+ border: 1px solid rgba(0, 232, 143, 0.3);
775
+ color: var(--status-success);
776
+ }
777
+
778
+ .toast-error {
779
+ background: rgba(255, 64, 96, 0.15);
780
+ border: 1px solid rgba(255, 64, 96, 0.3);
781
+ color: var(--status-error);
782
+ }
783
+
784
+ /* ── Placeholder Pages ───────────────────────────────────────────── */
785
+
786
+ .placeholder-content {
787
+ display: flex;
788
+ flex-direction: column;
789
+ align-items: center;
790
+ justify-content: center;
791
+ min-height: 400px;
792
+ text-align: center;
793
+ }
794
+
795
+ .placeholder-content .icon-placeholder {
796
+ font-size: 4rem;
797
+ margin-bottom: var(--space-lg);
798
+ opacity: 0.3;
799
+ }
800
+
801
+ .placeholder-content h3 {
802
+ font-size: 1.3rem;
803
+ margin-bottom: var(--space-sm);
804
+ }
805
+
806
+ .placeholder-content p {
807
+ color: var(--text-muted);
808
+ max-width: 400px;
809
+ }
810
+
811
+ /* ── User Menu ───────────────────────────────────────────────────── */
812
+
813
+ .user-menu {
814
+ display: flex;
815
+ align-items: center;
816
+ gap: var(--space-sm);
817
+ padding: var(--space-sm) var(--space-md);
818
+ border-radius: var(--radius-md);
819
+ cursor: pointer;
820
+ transition: background var(--transition-fast);
821
+ }
822
+
823
+ .user-menu:hover {
824
+ background: var(--bg-surface-hover);
825
+ }
826
+
827
+ .user-avatar {
828
+ width: 32px;
829
+ height: 32px;
830
+ border-radius: 50%;
831
+ background: var(--gradient-primary);
832
+ display: flex;
833
+ align-items: center;
834
+ justify-content: center;
835
+ font-weight: 700;
836
+ font-size: 0.85rem;
837
+ color: #fff;
838
+ }
839
+
840
+ .user-name {
841
+ font-size: 0.85rem;
842
+ font-weight: 500;
843
+ color: var(--text-secondary);
844
+ }
845
+
846
+ /* ── Animations ──────────────────────────────────────────────────── */
847
+
848
+ @keyframes spin {
849
+ to { transform: rotate(360deg); }
850
+ }
851
+
852
+ @keyframes pulse-glow {
853
+ 0%, 100% { opacity: 1; }
854
+ 50% { opacity: 0.7; }
855
+ }
856
+
857
+ @keyframes bg-drift {
858
+ 0% { transform: translate(0, 0) rotate(0deg); }
859
+ 100% { transform: translate(-2%, -2%) rotate(1deg); }
860
+ }
861
+
862
+ @keyframes fade-in {
863
+ from { opacity: 0; transform: translateY(10px); }
864
+ to { opacity: 1; transform: translateY(0); }
865
+ }
866
+
867
+ .fade-in {
868
+ animation: fade-in 0.4s ease-out forwards;
869
+ }
870
+
871
+ /* ── Responsive ──────────────────────────────────────────────────── */
872
+
873
+ @media (max-width: 768px) {
874
+ .sidebar {
875
+ transform: translateX(-100%);
876
+ }
877
+
878
+ .sidebar.open {
879
+ transform: translateX(0);
880
+ }
881
+
882
+ .main-content {
883
+ margin-left: 0;
884
+ padding: var(--space-lg);
885
+ }
886
+
887
+ .card-grid {
888
+ grid-template-columns: 1fr;
889
+ }
890
+
891
+ .stats-bar {
892
+ grid-template-columns: repeat(2, 1fr);
893
+ }
894
+ }
frontend/dashboard.html ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <meta name="description" content="AutoForge Dashboard — Manage your AI coding workflows">
7
+ <title>AutoForge — Dashboard</title>
8
+ <link rel="stylesheet" href="/css/style.css">
9
+ </head>
10
+ <body>
11
+ <div class="app-layout">
12
+ <!-- Sidebar -->
13
+ <nav class="sidebar" id="sidebar">
14
+ <div class="sidebar-header">
15
+ <div class="sidebar-brand">
16
+ <div class="logo">⚡</div>
17
+ <h1>AutoForge</h1>
18
+ </div>
19
+ </div>
20
+
21
+ <div class="sidebar-nav">
22
+ <a href="dashboard.html" class="nav-item active">
23
+ <span class="icon">📊</span>
24
+ <span>Dashboard</span>
25
+ </a>
26
+ <a href="workflow.html" class="nav-item">
27
+ <span class="icon">⚙️</span>
28
+ <span>Workflow</span>
29
+ </a>
30
+ <a href="history.html" class="nav-item">
31
+ <span class="icon">📜</span>
32
+ <span>History</span>
33
+ </a>
34
+ <a href="settings.html" class="nav-item">
35
+ <span class="icon">🔧</span>
36
+ <span>Settings</span>
37
+ </a>
38
+ </div>
39
+
40
+ <div class="sidebar-footer">
41
+ <div class="user-menu" id="logout-btn">
42
+ <div class="user-avatar" id="user-avatar">U</div>
43
+ <span class="user-name" id="user-display-name">User</span>
44
+ </div>
45
+ </div>
46
+ </nav>
47
+
48
+ <!-- Main Content -->
49
+ <main class="main-content">
50
+ <div class="page-header">
51
+ <h2>Dashboard</h2>
52
+ <p>Overview of your autonomous coding workflows</p>
53
+ </div>
54
+
55
+ <!-- Stats -->
56
+ <div class="stats-bar">
57
+ <div class="stat-card card glass">
58
+ <div class="stat-value" id="stat-total">0</div>
59
+ <div class="stat-label">Total Workflows</div>
60
+ </div>
61
+ <div class="stat-card card glass">
62
+ <div class="stat-value" id="stat-pending">0</div>
63
+ <div class="stat-label">Pending</div>
64
+ </div>
65
+ <div class="stat-card card glass">
66
+ <div class="stat-value" id="stat-completed">0</div>
67
+ <div class="stat-label">Completed</div>
68
+ </div>
69
+ <div class="stat-card card glass">
70
+ <div class="stat-value" id="stat-failed">0</div>
71
+ <div class="stat-label">Failed</div>
72
+ </div>
73
+ </div>
74
+
75
+ <!-- Workflows Grid -->
76
+ <div class="card-header" style="margin-bottom: 1rem;">
77
+ <h3 class="card-title">Recent Workflows</h3>
78
+ <button class="btn btn-primary" onclick="App.openCreateModal()">
79
+ + New Workflow
80
+ </button>
81
+ </div>
82
+
83
+ <div class="card-grid" id="workflows-list">
84
+ <div class="spinner" style="margin: 3rem auto;"></div>
85
+ </div>
86
+ </main>
87
+ </div>
88
+
89
+ <!-- Create Workflow Modal -->
90
+ <div class="modal-overlay" id="create-modal">
91
+ <div class="modal glass-strong">
92
+ <div class="modal-header">
93
+ <h3>Create New Workflow</h3>
94
+ <button class="modal-close" onclick="App.closeCreateModal()">✕</button>
95
+ </div>
96
+
97
+ <div class="form-group">
98
+ <label class="form-label" for="workflow-prompt">What would you like to build?</label>
99
+ <textarea class="form-input" id="workflow-prompt"
100
+ placeholder="e.g. Create a small Python calculator package with add, subtract, multiply and divide functions, including tests and proper division-by-zero handling."></textarea>
101
+ </div>
102
+
103
+ <div style="display: flex; gap: 0.75rem; justify-content: flex-end;">
104
+ <button class="btn btn-secondary" onclick="App.closeCreateModal()">Cancel</button>
105
+ <button class="btn btn-primary" onclick="App.createWorkflow()">Create Workflow</button>
106
+ </div>
107
+ </div>
108
+ </div>
109
+
110
+ <script src="/js/api.js"></script>
111
+ <script src="/js/auth.js"></script>
112
+ <script src="/js/router.js"></script>
113
+ <script src="/js/app.js"></script>
114
+ <script>
115
+ if (Router.init()) {
116
+ App.initSidebar();
117
+ App.loadWorkflows();
118
+ }
119
+ </script>
120
+ </body>
121
+ </html>
frontend/history.html ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <meta name="description" content="AutoForge Workflow History">
7
+ <title>AutoForge — History</title>
8
+ <link rel="stylesheet" href="/css/style.css">
9
+ </head>
10
+ <body>
11
+ <div class="app-layout">
12
+ <!-- Sidebar -->
13
+ <nav class="sidebar">
14
+ <div class="sidebar-header">
15
+ <div class="sidebar-brand">
16
+ <div class="logo">⚡</div>
17
+ <h1>AutoForge</h1>
18
+ </div>
19
+ </div>
20
+ <div class="sidebar-nav">
21
+ <a href="dashboard.html" class="nav-item">
22
+ <span class="icon">📊</span><span>Dashboard</span>
23
+ </a>
24
+ <a href="workflow.html" class="nav-item">
25
+ <span class="icon">⚙️</span><span>Workflow</span>
26
+ </a>
27
+ <a href="history.html" class="nav-item active">
28
+ <span class="icon">📜</span><span>History</span>
29
+ </a>
30
+ <a href="settings.html" class="nav-item">
31
+ <span class="icon">🔧</span><span>Settings</span>
32
+ </a>
33
+ </div>
34
+ <div class="sidebar-footer">
35
+ <div class="user-menu" id="logout-btn">
36
+ <div class="user-avatar" id="user-avatar">U</div>
37
+ <span class="user-name" id="user-display-name">User</span>
38
+ </div>
39
+ </div>
40
+ </nav>
41
+
42
+ <!-- Main Content -->
43
+ <main class="main-content">
44
+ <div class="page-header">
45
+ <h2>History</h2>
46
+ <p>Browse all past workflow executions</p>
47
+ </div>
48
+
49
+ <div id="history-list">
50
+ <div class="spinner" style="margin: 3rem auto;"></div>
51
+ </div>
52
+ </main>
53
+ </div>
54
+
55
+ <script src="/js/api.js"></script>
56
+ <script src="/js/auth.js"></script>
57
+ <script src="/js/router.js"></script>
58
+ <script src="/js/app.js"></script>
59
+ <script>
60
+ if (Router.init()) {
61
+ App.initSidebar();
62
+ loadHistory();
63
+ }
64
+
65
+ async function loadHistory() {
66
+ try {
67
+ const workflows = await API.get('/api/workflows');
68
+ const container = document.getElementById('history-list');
69
+
70
+ if (workflows.length === 0) {
71
+ container.innerHTML = `
72
+ <div class="placeholder-content">
73
+ <div class="icon-placeholder">📜</div>
74
+ <h3>No history yet</h3>
75
+ <p>Your completed workflows will appear here.</p>
76
+ </div>
77
+ `;
78
+ return;
79
+ }
80
+
81
+ container.innerHTML = `
82
+ <div class="card-grid">
83
+ ${workflows.map((wf, i) => `
84
+ <div class="card workflow-card fade-in" style="animation-delay: ${i * 0.05}s"
85
+ onclick="window.location.href='/workflow.html?id=${wf.id}'">
86
+ <div class="card-header">
87
+ <span class="badge badge-${wf.status}">${wf.status}</span>
88
+ <span class="meta">${new Date(wf.created_at).toLocaleDateString()}</span>
89
+ </div>
90
+ <div class="prompt-preview">${wf.prompt}</div>
91
+ <div class="meta">
92
+ <span>ID: ${wf.id.substring(0, 8)}…</span>
93
+ </div>
94
+ </div>
95
+ `).join('')}
96
+ </div>
97
+ `;
98
+ } catch (err) {
99
+ App.showToast(err.message, 'error');
100
+ }
101
+ }
102
+ </script>
103
+ </body>
104
+ </html>
frontend/index.html ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <meta name="description" content="AutoForge — Autonomous AI Coding System">
7
+ <title>AutoForge</title>
8
+ <link rel="stylesheet" href="/css/style.css">
9
+ </head>
10
+ <body>
11
+ <script src="/js/api.js"></script>
12
+ <script src="/js/auth.js"></script>
13
+ <script>
14
+ // Redirect based on auth state
15
+ if (Auth.isAuthenticated()) {
16
+ window.location.replace('/dashboard.html');
17
+ } else {
18
+ window.location.replace('/login.html');
19
+ }
20
+ </script>
21
+ </body>
22
+ </html>
frontend/js/api.js ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * AutoForge API Client
3
+ *
4
+ * Wraps fetch() with JWT injection, error handling, and base URL management.
5
+ */
6
+
7
+ const API = (() => {
8
+ const BASE_URL = '';
9
+
10
+ /**
11
+ * Make an authenticated API request.
12
+ */
13
+ async function request(method, path, body = null) {
14
+ const headers = { 'Content-Type': 'application/json' };
15
+
16
+ const token = localStorage.getItem('autoforge_token');
17
+ if (token) {
18
+ headers['Authorization'] = `Bearer ${token}`;
19
+ }
20
+
21
+ const options = { method, headers };
22
+ if (body) {
23
+ options.body = JSON.stringify(body);
24
+ }
25
+
26
+ const response = await fetch(`${BASE_URL}${path}`, options);
27
+
28
+ if (response.status === 401) {
29
+ localStorage.removeItem('autoforge_token');
30
+ localStorage.removeItem('autoforge_user');
31
+ window.location.href = '/login.html';
32
+ throw new Error('Session expired');
33
+ }
34
+
35
+ const data = await response.json();
36
+
37
+ if (!response.ok) {
38
+ const message = data.detail || `Request failed (${response.status})`;
39
+ throw new Error(message);
40
+ }
41
+
42
+ return data;
43
+ }
44
+
45
+ return {
46
+ get: (path) => request('GET', path),
47
+ post: (path, body) => request('POST', path, body),
48
+ put: (path, body) => request('PUT', path, body),
49
+ delete: (path) => request('DELETE', path),
50
+ };
51
+ })();
frontend/js/app.js ADDED
@@ -0,0 +1,174 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * AutoForge Dashboard Application Logic
3
+ */
4
+
5
+ const App = (() => {
6
+ let workflows = [];
7
+
8
+ // ── Toast Notifications ─────────────────────────────────────────
9
+
10
+ function showToast(message, type = 'success') {
11
+ // Remove existing toasts
12
+ document.querySelectorAll('.toast').forEach(t => t.remove());
13
+
14
+ const toast = document.createElement('div');
15
+ toast.className = `toast toast-${type}`;
16
+ toast.textContent = message;
17
+ document.body.appendChild(toast);
18
+
19
+ requestAnimationFrame(() => toast.classList.add('visible'));
20
+
21
+ setTimeout(() => {
22
+ toast.classList.remove('visible');
23
+ setTimeout(() => toast.remove(), 300);
24
+ }, 3000);
25
+ }
26
+
27
+ // ── Sidebar ─────────────────────────────────────────────────────
28
+
29
+ function initSidebar() {
30
+ const user = Auth.getUser();
31
+ const userNameEl = document.getElementById('user-display-name');
32
+ const userAvatarEl = document.getElementById('user-avatar');
33
+ const logoutBtn = document.getElementById('logout-btn');
34
+
35
+ if (user && userNameEl) {
36
+ userNameEl.textContent = user.username;
37
+ }
38
+ if (user && userAvatarEl) {
39
+ userAvatarEl.textContent = user.username.charAt(0).toUpperCase();
40
+ }
41
+ if (logoutBtn) {
42
+ logoutBtn.addEventListener('click', (e) => {
43
+ e.preventDefault();
44
+ Auth.logout();
45
+ });
46
+ }
47
+
48
+ // Highlight current nav item
49
+ const currentPage = window.location.pathname.split('/').pop() || 'dashboard.html';
50
+ document.querySelectorAll('.nav-item').forEach(item => {
51
+ const href = item.getAttribute('href');
52
+ if (href && currentPage === href) {
53
+ item.classList.add('active');
54
+ }
55
+ });
56
+ }
57
+
58
+ // ── Workflows ───────────────────────────────────────────────────
59
+
60
+ async function loadWorkflows() {
61
+ try {
62
+ workflows = await API.get('/api/workflows');
63
+ renderWorkflows();
64
+ updateStats();
65
+ } catch (err) {
66
+ showToast(err.message, 'error');
67
+ }
68
+ }
69
+
70
+ function renderWorkflows() {
71
+ const container = document.getElementById('workflows-list');
72
+ if (!container) return;
73
+
74
+ if (workflows.length === 0) {
75
+ container.innerHTML = `
76
+ <div class="empty-state fade-in">
77
+ <div class="icon-empty">⚡</div>
78
+ <h3>No workflows yet</h3>
79
+ <p>Create your first workflow to get started with autonomous code generation.</p>
80
+ </div>
81
+ `;
82
+ return;
83
+ }
84
+
85
+ container.innerHTML = workflows.map((wf, i) => `
86
+ <div class="card workflow-card fade-in" style="animation-delay: ${i * 0.05}s" onclick="App.viewWorkflow('${wf.id}')">
87
+ <div class="card-header">
88
+ <span class="badge badge-${wf.status}">${wf.status}</span>
89
+ <span class="meta">${_formatDate(wf.created_at)}</span>
90
+ </div>
91
+ <div class="prompt-preview">${_escapeHtml(wf.prompt)}</div>
92
+ <div class="meta">
93
+ <span>ID: ${wf.id.substring(0, 8)}…</span>
94
+ </div>
95
+ </div>
96
+ `).join('');
97
+ }
98
+
99
+ function updateStats() {
100
+ const totalEl = document.getElementById('stat-total');
101
+ const pendingEl = document.getElementById('stat-pending');
102
+ const completedEl = document.getElementById('stat-completed');
103
+ const failedEl = document.getElementById('stat-failed');
104
+
105
+ if (totalEl) totalEl.textContent = workflows.length;
106
+ if (pendingEl) pendingEl.textContent = workflows.filter(w => w.status === 'pending').length;
107
+ if (completedEl) completedEl.textContent = workflows.filter(w => w.status === 'completed').length;
108
+ if (failedEl) failedEl.textContent = workflows.filter(w => w.status === 'failed').length;
109
+ }
110
+
111
+ // ── Create Workflow ─────────────────────────────────────────────
112
+
113
+ function openCreateModal() {
114
+ const overlay = document.getElementById('create-modal');
115
+ if (overlay) overlay.classList.add('active');
116
+ }
117
+
118
+ function closeCreateModal() {
119
+ const overlay = document.getElementById('create-modal');
120
+ if (overlay) overlay.classList.remove('active');
121
+ const input = document.getElementById('workflow-prompt');
122
+ if (input) input.value = '';
123
+ }
124
+
125
+ async function createWorkflow() {
126
+ const input = document.getElementById('workflow-prompt');
127
+ const prompt = input ? input.value.trim() : '';
128
+ if (!prompt) {
129
+ showToast('Please enter a prompt', 'error');
130
+ return;
131
+ }
132
+
133
+ try {
134
+ const wf = await API.post('/api/workflows', { prompt });
135
+ showToast('Workflow created successfully!');
136
+ closeCreateModal();
137
+ await loadWorkflows();
138
+ } catch (err) {
139
+ showToast(err.message, 'error');
140
+ }
141
+ }
142
+
143
+ function viewWorkflow(id) {
144
+ window.location.href = `/workflow.html?id=${id}`;
145
+ }
146
+
147
+ // ── Helpers ─────────────────────────────────────────────────────
148
+
149
+ function _formatDate(iso) {
150
+ const d = new Date(iso);
151
+ return d.toLocaleDateString('en-US', {
152
+ month: 'short', day: 'numeric', year: 'numeric',
153
+ hour: '2-digit', minute: '2-digit',
154
+ });
155
+ }
156
+
157
+ function _escapeHtml(str) {
158
+ const div = document.createElement('div');
159
+ div.textContent = str;
160
+ return div.innerHTML;
161
+ }
162
+
163
+ // ── Public API ──────────────────────────────────────────────────
164
+
165
+ return {
166
+ showToast,
167
+ initSidebar,
168
+ loadWorkflows,
169
+ openCreateModal,
170
+ closeCreateModal,
171
+ createWorkflow,
172
+ viewWorkflow,
173
+ };
174
+ })();