diff --git a/backend/app/__init__.py b/backend/app/__init__.py deleted file mode 100644 index ba81ad6..0000000 --- a/backend/app/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# Moxiegen Backend Application diff --git a/backend/app/api/__init__.py b/backend/app/api/__init__.py deleted file mode 100644 index 9c7f58e..0000000 --- a/backend/app/api/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# API module diff --git a/backend/app/api/admin.py b/backend/app/api/admin.py deleted file mode 100644 index a5263dd..0000000 --- a/backend/app/api/admin.py +++ /dev/null @@ -1,206 +0,0 @@ -from typing import List -from fastapi import APIRouter, Depends, HTTPException, status -from sqlalchemy.orm import Session -from app.core.database import get_db -from app.core.auth import get_current_admin_user, get_password_hash -from app.models.models import User, AIEndpoint, ChatMessage, UploadedFile -from app.schemas.schemas import ( - UserResponse, - UserUpdate, - AIEndpointCreate, - AIEndpointUpdate, - AIEndpointResponse, - AdminStats -) - -router = APIRouter(prefix="/admin", tags=["Admin"]) - - -# User Management -@router.get("/users", response_model=List[UserResponse]) -def list_users( - skip: int = 0, - limit: int = 100, - current_user = Depends(get_current_admin_user), - db: Session = Depends(get_db) -): - users = db.query(User).offset(skip).limit(limit).all() - return users - - -@router.get("/users/{user_id}", response_model=UserResponse) -def get_user( - user_id: int, - current_user = Depends(get_current_admin_user), - db: Session = Depends(get_db) -): - user = db.query(User).filter(User.id == user_id).first() - if not user: - raise HTTPException(status_code=404, detail="User not found") - return user - - -@router.put("/users/{user_id}", response_model=UserResponse) -def update_user( - user_id: int, - user_data: UserUpdate, - current_user = Depends(get_current_admin_user), - db: Session = Depends(get_db) -): - user = db.query(User).filter(User.id == user_id).first() - if not user: - raise HTTPException(status_code=404, detail="User not found") - - if user_data.email: - existing_user = db.query(User).filter( - User.email == user_data.email, - User.id != user_id - ).first() - if existing_user: - raise HTTPException(status_code=400, detail="Email already registered") - user.email = user_data.email - - if user_data.username: - existing_user = db.query(User).filter( - User.username == user_data.username, - User.id != user_id - ).first() - if existing_user: - raise HTTPException(status_code=400, detail="Username already taken") - user.username = user_data.username - - if user_data.password: - user.hashed_password = get_password_hash(user_data.password) - - if user_data.role: - user.role = user_data.role - - if user_data.is_active is not None: - user.is_active = user_data.is_active - - db.commit() - db.refresh(user) - return user - - -@router.delete("/users/{user_id}") -def delete_user( - user_id: int, - current_user = Depends(get_current_admin_user), - db: Session = Depends(get_db) -): - user = db.query(User).filter(User.id == user_id).first() - if not user: - raise HTTPException(status_code=404, detail="User not found") - - if user.id == current_user.id: - raise HTTPException(status_code=400, detail="Cannot delete yourself") - - db.delete(user) - db.commit() - return {"message": "User deleted successfully"} - - -# AI Endpoint Management -@router.get("/endpoints", response_model=List[AIEndpointResponse]) -def list_endpoints( - skip: int = 0, - limit: int = 100, - current_user = Depends(get_current_admin_user), - db: Session = Depends(get_db) -): - endpoints = db.query(AIEndpoint).offset(skip).limit(limit).all() - return endpoints - - -@router.post("/endpoints", response_model=AIEndpointResponse) -def create_endpoint( - endpoint_data: AIEndpointCreate, - current_user = Depends(get_current_admin_user), - db: Session = Depends(get_db) -): - # If this is set as default, unset other defaults - if endpoint_data.is_default: - db.query(AIEndpoint).filter(AIEndpoint.is_default == True).update({"is_default": False}) - - new_endpoint = AIEndpoint(**endpoint_data.model_dump()) - db.add(new_endpoint) - db.commit() - db.refresh(new_endpoint) - return new_endpoint - - -@router.get("/endpoints/{endpoint_id}", response_model=AIEndpointResponse) -def get_endpoint( - endpoint_id: int, - current_user = Depends(get_current_admin_user), - db: Session = Depends(get_db) -): - endpoint = db.query(AIEndpoint).filter(AIEndpoint.id == endpoint_id).first() - if not endpoint: - raise HTTPException(status_code=404, detail="Endpoint not found") - return endpoint - - -@router.put("/endpoints/{endpoint_id}", response_model=AIEndpointResponse) -def update_endpoint( - endpoint_id: int, - endpoint_data: AIEndpointUpdate, - current_user = Depends(get_current_admin_user), - db: Session = Depends(get_db) -): - endpoint = db.query(AIEndpoint).filter(AIEndpoint.id == endpoint_id).first() - if not endpoint: - raise HTTPException(status_code=404, detail="Endpoint not found") - - update_data = endpoint_data.model_dump(exclude_unset=True) - - # If setting as default, unset other defaults - if update_data.get("is_default"): - db.query(AIEndpoint).filter( - AIEndpoint.is_default == True, - AIEndpoint.id != endpoint_id - ).update({"is_default": False}) - - for key, value in update_data.items(): - setattr(endpoint, key, value) - - db.commit() - db.refresh(endpoint) - return endpoint - - -@router.delete("/endpoints/{endpoint_id}") -def delete_endpoint( - endpoint_id: int, - current_user = Depends(get_current_admin_user), - db: Session = Depends(get_db) -): - endpoint = db.query(AIEndpoint).filter(AIEndpoint.id == endpoint_id).first() - if not endpoint: - raise HTTPException(status_code=404, detail="Endpoint not found") - - db.delete(endpoint) - db.commit() - return {"message": "Endpoint deleted successfully"} - - -# Admin Statistics -@router.get("/stats", response_model=AdminStats) -def get_stats( - current_user = Depends(get_current_admin_user), - db: Session = Depends(get_db) -): - total_users = db.query(User).count() - total_endpoints = db.query(AIEndpoint).count() - total_messages = db.query(ChatMessage).count() - total_files = db.query(UploadedFile).count() - active_users = db.query(User).filter(User.is_active == True).count() - - return AdminStats( - total_users=total_users, - total_endpoints=total_endpoints, - total_messages=total_messages, - total_files=total_files, - active_users=active_users - ) diff --git a/backend/app/api/auth.py b/backend/app/api/auth.py deleted file mode 100644 index 682d2b1..0000000 --- a/backend/app/api/auth.py +++ /dev/null @@ -1,126 +0,0 @@ -from datetime import timedelta -from fastapi import APIRouter, Depends, HTTPException, status -from sqlalchemy.orm import Session -from app.core.database import get_db -from app.core.auth import ( - verify_password, - get_password_hash, - create_access_token, - get_current_user -) -from app.core.config import settings -from app.models.models import User -from app.schemas.schemas import ( - UserCreate, - UserLogin, - UserResponse, - UserUpdate, - Token -) - -router = APIRouter(prefix="/auth", tags=["Authentication"]) - - -@router.post("/register", response_model=UserResponse) -def register(user_data: UserCreate, db: Session = Depends(get_db)): - # Check if email already exists - db_user = db.query(User).filter(User.email == user_data.email).first() - if db_user: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="Email already registered" - ) - - # Check if username already exists - db_user = db.query(User).filter(User.username == user_data.username).first() - if db_user: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="Username already taken" - ) - - # Create new user - hashed_password = get_password_hash(user_data.password) - new_user = User( - email=user_data.email, - username=user_data.username, - hashed_password=hashed_password, - role="user", - is_active=True - ) - db.add(new_user) - db.commit() - db.refresh(new_user) - - return new_user - - -@router.post("/login", response_model=Token) -def login(login_data: UserLogin, db: Session = Depends(get_db)): - # Find user by email - user = db.query(User).filter(User.email == login_data.email).first() - - if not user or not verify_password(login_data.password, user.hashed_password): - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Incorrect email or password", - headers={"WWW-Authenticate": "Bearer"}, - ) - - if not user.is_active: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="Inactive user" - ) - - access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) - access_token = create_access_token( - data={"sub": str(user.id), "email": user.email, "role": user.role}, - expires_delta=access_token_expires - ) - - return {"access_token": access_token, "token_type": "bearer"} - - -@router.get("/me", response_model=UserResponse) -def get_me(current_user: User = Depends(get_current_user)): - return current_user - - -@router.put("/me", response_model=UserResponse) -def update_me( - user_data: UserUpdate, - current_user: User = Depends(get_current_user), - db: Session = Depends(get_db) -): - if user_data.email: - existing_user = db.query(User).filter( - User.email == user_data.email, - User.id != current_user.id - ).first() - if existing_user: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="Email already registered" - ) - current_user.email = user_data.email - - if user_data.username: - existing_user = db.query(User).filter( - User.username == user_data.username, - User.id != current_user.id - ).first() - if existing_user: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="Username already taken" - ) - current_user.username = user_data.username - - if user_data.password: - current_user.hashed_password = get_password_hash(user_data.password) - - db.commit() - db.refresh(current_user) - - return current_user diff --git a/backend/app/api/chat.py b/backend/app/api/chat.py deleted file mode 100644 index d64cf89..0000000 --- a/backend/app/api/chat.py +++ /dev/null @@ -1,231 +0,0 @@ -import os -import uuid -from typing import List, Optional -from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form -from sqlalchemy.orm import Session -import httpx -from openai import AsyncOpenAI -from app.core.database import get_db -from app.core.auth import get_current_user -from app.core.config import settings -from app.models.models import User, AIEndpoint, ChatMessage, UploadedFile -from app.schemas.schemas import ( - ChatRequest, - ChatResponse, - ChatMessageResponse, - UploadedFileResponse -) - -router = APIRouter(prefix="/chat", tags=["Chat"]) - - -# Ensure upload directory exists -os.makedirs(settings.UPLOAD_DIR, exist_ok=True) - - -@router.post("/upload", response_model=UploadedFileResponse) -async def upload_file( - file: UploadFile = File(...), - current_user: User = Depends(get_current_user), - db: Session = Depends(get_db) -): - # Check file size - contents = await file.read() - if len(contents) > settings.MAX_FILE_SIZE: - raise HTTPException( - status_code=400, - detail=f"File size exceeds maximum allowed size of {settings.MAX_FILE_SIZE} bytes" - ) - - # Generate unique filename - file_extension = os.path.splitext(file.filename)[1] - unique_filename = f"{uuid.uuid4()}{file_extension}" - file_path = os.path.join(settings.UPLOAD_DIR, unique_filename) - - # Save file - with open(file_path, "wb") as f: - f.write(contents) - - # Create database record - uploaded_file = UploadedFile( - user_id=current_user.id, - filename=unique_filename, - original_filename=file.filename, - file_path=file_path, - file_size=len(contents), - file_type=file.content_type - ) - db.add(uploaded_file) - db.commit() - db.refresh(uploaded_file) - - return uploaded_file - - -@router.get("/files", response_model=List[UploadedFileResponse]) -def list_files( - current_user: User = Depends(get_current_user), - db: Session = Depends(get_db) -): - files = db.query(UploadedFile).filter(UploadedFile.user_id == current_user.id).all() - return files - - -@router.delete("/files/{file_id}") -def delete_file( - file_id: int, - current_user: User = Depends(get_current_user), - db: Session = Depends(get_db) -): - uploaded_file = db.query(UploadedFile).filter( - UploadedFile.id == file_id, - UploadedFile.user_id == current_user.id - ).first() - - if not uploaded_file: - raise HTTPException(status_code=404, detail="File not found") - - # Delete file from filesystem - if os.path.exists(uploaded_file.file_path): - os.remove(uploaded_file.file_path) - - db.delete(uploaded_file) - db.commit() - - return {"message": "File deleted successfully"} - - -@router.post("/message", response_model=ChatResponse) -async def send_message( - request: ChatRequest, - current_user: User = Depends(get_current_user), - db: Session = Depends(get_db) -): - # Get the AI endpoint - endpoint = None - if request.endpoint_id: - endpoint = db.query(AIEndpoint).filter( - AIEndpoint.id == request.endpoint_id, - AIEndpoint.is_active == True - ).first() - else: - # Get default endpoint - endpoint = db.query(AIEndpoint).filter( - AIEndpoint.is_default == True, - AIEndpoint.is_active == True - ).first() - - if not endpoint: - raise HTTPException( - status_code=400, - detail="No active AI endpoint available" - ) - - # Save user message - user_message = ChatMessage( - user_id=current_user.id, - role="user", - content=request.message, - endpoint_id=endpoint.id - ) - db.add(user_message) - db.commit() - - # Build messages for API call - messages = [] - if request.conversation_history: - for msg in request.conversation_history: - messages.append({"role": msg.role, "content": msg.content}) - messages.append({"role": "user", "content": request.message}) - - # Call AI endpoint - try: - response_content = await call_ai_endpoint(endpoint, messages) - except Exception as e: - raise HTTPException( - status_code=500, - detail=f"Error calling AI endpoint: {str(e)}" - ) - - # Save assistant message - assistant_message = ChatMessage( - user_id=current_user.id, - role="assistant", - content=response_content, - endpoint_id=endpoint.id - ) - db.add(assistant_message) - db.commit() - - return ChatResponse( - response=response_content, - endpoint_id=endpoint.id, - model=endpoint.model_name - ) - - -async def call_ai_endpoint(endpoint: AIEndpoint, messages: List[dict]) -> str: - if endpoint.endpoint_type == "openai": - return await call_openai_compatible(endpoint, messages) - elif endpoint.endpoint_type == "ollama": - return await call_ollama(endpoint, messages) - else: - raise ValueError(f"Unknown endpoint type: {endpoint.endpoint_type}") - - -async def call_openai_compatible(endpoint: AIEndpoint, messages: List[dict]) -> str: - """Call OpenAI-compatible API (works with OpenAI, local AI servers, etc.)""" - client = AsyncOpenAI( - api_key=endpoint.api_key or "not-needed", - base_url=endpoint.base_url - ) - - response = await client.chat.completions.create( - model=endpoint.model_name, - messages=messages - ) - - return response.choices[0].message.content - - -async def call_ollama(endpoint: AIEndpoint, messages: List[dict]) -> str: - """Call Ollama API""" - async with httpx.AsyncClient() as client: - response = await client.post( - f"{endpoint.base_url.rstrip('/')}/api/chat", - json={ - "model": endpoint.model_name, - "messages": messages, - "stream": False - }, - timeout=60.0 - ) - response.raise_for_status() - data = response.json() - return data["message"]["content"] - - -@router.get("/history", response_model=List[ChatMessageResponse]) -def get_history( - limit: int = 50, - endpoint_id: Optional[int] = None, - current_user: User = Depends(get_current_user), - db: Session = Depends(get_db) -): - query = db.query(ChatMessage).filter(ChatMessage.user_id == current_user.id) - - if endpoint_id: - query = query.filter(ChatMessage.endpoint_id == endpoint_id) - - messages = query.order_by(ChatMessage.created_at.desc()).limit(limit).all() - return list(reversed(messages)) - - -@router.delete("/history") -def clear_history( - current_user: User = Depends(get_current_user), - db: Session = Depends(get_db) -): - db.query(ChatMessage).filter(ChatMessage.user_id == current_user.id).delete() - db.commit() - return {"message": "Chat history cleared successfully"} diff --git a/backend/app/core/__init__.py b/backend/app/core/__init__.py deleted file mode 100644 index 3e83c63..0000000 --- a/backend/app/core/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# Core module diff --git a/backend/app/core/auth.py b/backend/app/core/auth.py deleted file mode 100644 index 3d798ef..0000000 --- a/backend/app/core/auth.py +++ /dev/null @@ -1,79 +0,0 @@ -from datetime import datetime, timedelta -from typing import Optional -from jose import JWTError, jwt -from passlib.context import CryptContext -from fastapi import Depends, HTTPException, status -from fastapi.security import OAuth2PasswordBearer -from sqlalchemy.orm import Session -from app.core.config import settings -from app.core.database import get_db - -pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") -oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login") - - -def verify_password(plain_password: str, hashed_password: str) -> bool: - return pwd_context.verify(plain_password, hashed_password) - - -def get_password_hash(password: str) -> str: - return pwd_context.hash(password) - - -def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str: - to_encode = data.copy() - if expires_delta: - expire = datetime.utcnow() + expires_delta - else: - expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) - to_encode.update({"exp": expire}) - encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM) - return encoded_jwt - - -def decode_token(token: str) -> Optional[dict]: - try: - payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]) - return payload - except JWTError: - return None - - -async def get_current_user( - token: str = Depends(oauth2_scheme), - db: Session = Depends(get_db) -): - from app.models.models import User - - credentials_exception = HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Could not validate credentials", - headers={"WWW-Authenticate": "Bearer"}, - ) - payload = decode_token(token) - if payload is None: - raise credentials_exception - - user_id: int = payload.get("sub") - if user_id is None: - raise credentials_exception - - user = db.query(User).filter(User.id == user_id).first() - if user is None: - raise credentials_exception - - if not user.is_active: - raise HTTPException(status_code=400, detail="Inactive user") - - return user - - -async def get_current_admin_user( - current_user = Depends(get_current_user) -): - if current_user.role != "admin": - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail="Not enough permissions" - ) - return current_user diff --git a/backend/app/core/config.py b/backend/app/core/config.py deleted file mode 100644 index 6d4e55f..0000000 --- a/backend/app/core/config.py +++ /dev/null @@ -1,19 +0,0 @@ -from pydantic_settings import BaseSettings -from typing import Optional - - -class Settings(BaseSettings): - SECRET_KEY: str = "your-secret-key-change-in-production" - DATABASE_URL: str = "sqlite:///./moxiegen.db" - UPLOAD_DIR: str = "./uploads" - MAX_FILE_SIZE: int = 10 * 1024 * 1024 # 10MB - DEFAULT_ADMIN_EMAIL: str = "admin@moxiegen.com" - DEFAULT_ADMIN_PASSWORD: str = "admin123" - ALGORITHM: str = "HS256" - ACCESS_TOKEN_EXPIRE_MINUTES: int = 30 - - class Config: - env_file = ".env" - - -settings = Settings() diff --git a/backend/app/core/database.py b/backend/app/core/database.py deleted file mode 100644 index b231c75..0000000 --- a/backend/app/core/database.py +++ /dev/null @@ -1,21 +0,0 @@ -from sqlalchemy import create_engine -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import sessionmaker -from app.core.config import settings - -engine = create_engine( - settings.DATABASE_URL, - connect_args={"check_same_thread": False} if "sqlite" in settings.DATABASE_URL else {} -) - -SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) - -Base = declarative_base() - - -def get_db(): - db = SessionLocal() - try: - yield db - finally: - db.close() diff --git a/backend/app/main.py b/backend/app/main.py deleted file mode 100644 index 9a012ce..0000000 --- a/backend/app/main.py +++ /dev/null @@ -1,98 +0,0 @@ -import os -from contextlib import asynccontextmanager -from fastapi import FastAPI -from fastapi.middleware.cors import CORSMiddleware -from app.core.config import settings -from app.core.database import engine, Base, SessionLocal -from app.core.auth import get_password_hash -from app.models.models import User, AIEndpoint -from app.api import auth, admin, chat - - -# Create database tables -def init_db(): - Base.metadata.create_all(bind=engine) - - -# Create default admin user and default AI endpoint -def create_defaults(): - db = SessionLocal() - try: - # Check if admin exists - admin = db.query(User).filter(User.email == settings.DEFAULT_ADMIN_EMAIL).first() - if not admin: - admin = User( - email=settings.DEFAULT_ADMIN_EMAIL, - username="admin", - hashed_password=get_password_hash(settings.DEFAULT_ADMIN_PASSWORD), - role="admin", - is_active=True - ) - db.add(admin) - print(f"Created default admin user: {settings.DEFAULT_ADMIN_EMAIL}") - - # Check if default endpoint exists - default_endpoint = db.query(AIEndpoint).filter(AIEndpoint.is_default == True).first() - if not default_endpoint: - # Create a default Ollama endpoint - default_endpoint = AIEndpoint( - name="Default Ollama", - endpoint_type="ollama", - base_url="http://localhost:11434", - model_name="llama2", - is_active=True, - is_default=True - ) - db.add(default_endpoint) - print("Created default Ollama endpoint") - - db.commit() - finally: - db.close() - - -@asynccontextmanager -async def lifespan(app: FastAPI): - # Startup - init_db() - create_defaults() - - # Ensure upload directory exists - os.makedirs(settings.UPLOAD_DIR, exist_ok=True) - - yield - - # Shutdown (if needed) - pass - - -app = FastAPI( - title="Moxiegen API", - description="Backend API for Moxiegen application", - version="1.0.0", - lifespan=lifespan -) - -# CORS middleware -app.add_middleware( - CORSMiddleware, - allow_origins=["*"], # Configure this properly in production - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - -# Include routers -app.include_router(auth.router, prefix="/api") -app.include_router(admin.router, prefix="/api") -app.include_router(chat.router, prefix="/api") - - -@app.get("/") -def root(): - return {"message": "Welcome to Moxiegen API", "version": "1.0.0"} - - -@app.get("/health") -def health_check(): - return {"status": "healthy"} diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py deleted file mode 100644 index e2313c5..0000000 --- a/backend/app/models/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# Models module diff --git a/backend/app/models/models.py b/backend/app/models/models.py deleted file mode 100644 index e95cae6..0000000 --- a/backend/app/models/models.py +++ /dev/null @@ -1,64 +0,0 @@ -from datetime import datetime -from sqlalchemy import Column, Integer, String, Boolean, DateTime, Text, ForeignKey -from sqlalchemy.orm import relationship -from app.core.database import Base - - -class User(Base): - __tablename__ = "users" - - id = Column(Integer, primary_key=True, index=True) - email = Column(String, unique=True, index=True, nullable=False) - username = Column(String, unique=True, index=True, nullable=False) - hashed_password = Column(String, nullable=False) - role = Column(String, default="user") # "user" or "admin" - is_active = Column(Boolean, default=True) - created_at = Column(DateTime, default=datetime.utcnow) - updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) - - chat_messages = relationship("ChatMessage", back_populates="user") - uploaded_files = relationship("UploadedFile", back_populates="user") - - -class AIEndpoint(Base): - __tablename__ = "ai_endpoints" - - id = Column(Integer, primary_key=True, index=True) - name = Column(String, nullable=False) - endpoint_type = Column(String, nullable=False) # "openai" or "ollama" - base_url = Column(String, nullable=False) - api_key = Column(String, nullable=True) # Optional for Ollama - model_name = Column(String, nullable=False) - is_active = Column(Boolean, default=True) - is_default = Column(Boolean, default=False) - created_at = Column(DateTime, default=datetime.utcnow) - updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) - - -class ChatMessage(Base): - __tablename__ = "chat_messages" - - id = Column(Integer, primary_key=True, index=True) - user_id = Column(Integer, ForeignKey("users.id"), nullable=False) - role = Column(String, nullable=False) # "user" or "assistant" - content = Column(Text, nullable=False) - endpoint_id = Column(Integer, ForeignKey("ai_endpoints.id"), nullable=True) - created_at = Column(DateTime, default=datetime.utcnow) - - user = relationship("User", back_populates="chat_messages") - endpoint = relationship("AIEndpoint") - - -class UploadedFile(Base): - __tablename__ = "uploaded_files" - - id = Column(Integer, primary_key=True, index=True) - user_id = Column(Integer, ForeignKey("users.id"), nullable=False) - filename = Column(String, nullable=False) - original_filename = Column(String, nullable=False) - file_path = Column(String, nullable=False) - file_size = Column(Integer, nullable=False) - file_type = Column(String, nullable=True) - created_at = Column(DateTime, default=datetime.utcnow) - - user = relationship("User", back_populates="uploaded_files") diff --git a/backend/app/schemas/__init__.py b/backend/app/schemas/__init__.py deleted file mode 100644 index fff1fac..0000000 --- a/backend/app/schemas/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# Schemas module diff --git a/backend/app/schemas/schemas.py b/backend/app/schemas/schemas.py deleted file mode 100644 index 657b533..0000000 --- a/backend/app/schemas/schemas.py +++ /dev/null @@ -1,139 +0,0 @@ -from datetime import datetime -from typing import Optional, List -from pydantic import BaseModel, EmailStr - - -# User schemas -class UserBase(BaseModel): - email: EmailStr - username: str - - -class UserCreate(UserBase): - password: str - - -class UserLogin(BaseModel): - email: EmailStr - password: str - - -class UserUpdate(BaseModel): - email: Optional[EmailStr] = None - username: Optional[str] = None - password: Optional[str] = None - role: Optional[str] = None - is_active: Optional[bool] = None - - -class UserResponse(UserBase): - id: int - role: str - is_active: bool - created_at: datetime - - class Config: - from_attributes = True - - -# Token schemas -class Token(BaseModel): - access_token: str - token_type: str = "bearer" - - -class TokenData(BaseModel): - user_id: Optional[int] = None - - -# AIEndpoint schemas -class AIEndpointBase(BaseModel): - name: str - endpoint_type: str # "openai" or "ollama" - base_url: str - api_key: Optional[str] = None - model_name: str - is_active: bool = True - is_default: bool = False - - -class AIEndpointCreate(AIEndpointBase): - pass - - -class AIEndpointUpdate(BaseModel): - name: Optional[str] = None - endpoint_type: Optional[str] = None - base_url: Optional[str] = None - api_key: Optional[str] = None - model_name: Optional[str] = None - is_active: Optional[bool] = None - is_default: Optional[bool] = None - - -class AIEndpointResponse(AIEndpointBase): - id: int - created_at: datetime - - class Config: - from_attributes = True - - -# ChatMessage schemas -class ChatMessageBase(BaseModel): - role: str - content: str - - -class ChatMessageCreate(ChatMessageBase): - endpoint_id: Optional[int] = None - - -class ChatMessageResponse(ChatMessageBase): - id: int - user_id: int - endpoint_id: Optional[int] - created_at: datetime - - class Config: - from_attributes = True - - -# UploadedFile schemas -class UploadedFileBase(BaseModel): - original_filename: str - file_type: Optional[str] = None - - -class UploadedFileResponse(UploadedFileBase): - id: int - user_id: int - filename: str - file_path: str - file_size: int - created_at: datetime - - class Config: - from_attributes = True - - -# Chat schemas -class ChatRequest(BaseModel): - message: str - endpoint_id: Optional[int] = None - file_ids: Optional[List[int]] = None - conversation_history: Optional[List[dict]] = None - - -class ChatResponse(BaseModel): - response: str - endpoint_used: Optional[str] = None - model_used: Optional[str] = None - - -# AdminStats schema -class AdminStats(BaseModel): - total_users: int - total_endpoints: int - total_messages: int - active_endpoints: int diff --git a/backend/requirements.txt b/backend/requirements.txt deleted file mode 100644 index 0028e23..0000000 --- a/backend/requirements.txt +++ /dev/null @@ -1,10 +0,0 @@ -fastapi -uvicorn -sqlalchemy -pydantic -python-jose -passlib -python-multipart -aiofiles -httpx -openai diff --git a/frontend/package.json b/frontend/package.json deleted file mode 100644 index 25e87e3..0000000 --- a/frontend/package.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "moxiegen-frontend", - "version": "1.0.0", - "private": true, - "scripts": { - "dev": "vite dev", - "build": "vite build", - "preview": "vite preview" - }, - "devDependencies": { - "@sveltejs/adapter-auto": "^6.0.0", - "@sveltejs/kit": "^2.0.0", - "@sveltejs/vite-plugin-svelte": "^5.0.0", - "svelte": "^5.0.0", - "vite": "^6.0.0" - }, - "dependencies": { - "marked": "^15.0.0", - "highlight.js": "^11.11.0" - }, - "type": "module" -} diff --git a/frontend/src/app.css b/frontend/src/app.css deleted file mode 100644 index d2a0d18..0000000 --- a/frontend/src/app.css +++ /dev/null @@ -1,463 +0,0 @@ -:root { - /* Vibrant Color Palette - Hot Air Balloon Theme */ - --primary: #FF6B6B; /* Coral Red */ - --primary-hover: #FF5252; - --secondary: #4ECDC4; /* Teal */ - --accent-1: #FFE66D; /* Sunny Yellow */ - --accent-2: #95E1D3; /* Mint */ - --accent-3: #F38181; /* Salmon */ - --accent-4: #AA96DA; /* Lavender */ - --accent-5: #FCBAD3; /* Pink */ - --accent-6: #A8D8EA; /* Sky Blue */ - --accent-7: #FF9F43; /* Orange */ - --accent-8: #5F27CD; /* Purple */ - - /* Gradients */ - --gradient-sunset: linear-gradient(135deg, #FF6B6B 0%, #FFE66D 50%, #95E1D3 100%); - --gradient-balloon: linear-gradient(135deg, #FF6B6B 0%, #FCBAD3 50%, #AA96DA 100%); - --gradient-sky: linear-gradient(135deg, #A8D8EA 0%, #95E1D3 50%, #FFE66D 100%); - --gradient-aurora: linear-gradient(135deg, #5F27CD 0%, #4ECDC4 50%, #FFE66D 100%); - - /* Background */ - --background: #1A1A2E; /* Deep Space */ - --background-light: #16213E; - --surface: #0F3460; /* Ocean Blue */ - --surface-hover: #1A4B8C; - --surface-card: rgba(15, 52, 96, 0.8); - --border: rgba(78, 205, 196, 0.3); - --border-bright: #4ECDC4; - - /* Text */ - --text: #F7F7F7; - --text-muted: #B8B8D1; - --text-bright: #FFFFFF; - - /* Status Colors */ - --success: #95E1D3; - --danger: #FF6B6B; - --warning: #FFE66D; - --info: #A8D8EA; - - /* Layout */ - --sidebar-width: 280px; - --header-height: 70px; - - /* Shadows */ - --shadow-glow: 0 0 30px rgba(78, 205, 196, 0.3); - --shadow-card: 0 8px 32px rgba(0, 0, 0, 0.3); - --shadow-button: 0 4px 15px rgba(255, 107, 107, 0.4); -} - -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -html, body { - height: 100%; - font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', sans-serif; - background: var(--background); - background-image: - radial-gradient(circle at 20% 80%, rgba(170, 150, 218, 0.15) 0%, transparent 50%), - radial-gradient(circle at 80% 20%, rgba(78, 205, 196, 0.15) 0%, transparent 50%), - radial-gradient(circle at 40% 40%, rgba(255, 107, 107, 0.1) 0%, transparent 40%); - color: var(--text); - line-height: 1.6; - overflow-x: hidden; -} - -a { - color: var(--secondary); - text-decoration: none; - transition: all 0.3s ease; -} - -a:hover { - color: var(--accent-1); - text-shadow: 0 0 10px rgba(255, 230, 109, 0.5); -} - -button { - cursor: pointer; - font-family: inherit; - border: none; - outline: none; -} - -input, textarea, select { - font-family: inherit; - font-size: inherit; -} - -/* Scrollbar */ -::-webkit-scrollbar { - width: 10px; - height: 10px; -} - -::-webkit-scrollbar-track { - background: var(--background); - border-radius: 5px; -} - -::-webkit-scrollbar-thumb { - background: var(--gradient-sunset); - border-radius: 5px; -} - -::-webkit-scrollbar-thumb:hover { - background: var(--gradient-balloon); -} - -/* ============ BUTTONS ============ */ -.btn { - display: inline-flex; - align-items: center; - justify-content: center; - gap: 0.5rem; - padding: 0.75rem 1.5rem; - font-size: 0.9rem; - font-weight: 600; - border-radius: 50px; - border: none; - transition: all 0.3s ease; - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.btn-primary { - background: var(--gradient-sunset); - color: var(--background); - box-shadow: var(--shadow-button); -} - -.btn-primary:hover { - transform: translateY(-2px); - box-shadow: 0 6px 20px rgba(255, 107, 107, 0.5); -} - -.btn-secondary { - background: transparent; - color: var(--secondary); - border: 2px solid var(--secondary); - box-shadow: 0 0 15px rgba(78, 205, 196, 0.2); -} - -.btn-secondary:hover { - background: var(--secondary); - color: var(--background); - box-shadow: 0 0 25px rgba(78, 205, 196, 0.4); -} - -.btn-danger { - background: linear-gradient(135deg, #FF6B6B, #F38181); - color: white; - box-shadow: 0 4px 15px rgba(255, 107, 107, 0.4); -} - -.btn-danger:hover { - transform: translateY(-2px); - box-shadow: 0 6px 20px rgba(255, 107, 107, 0.5); -} - -.btn-sm { - padding: 0.5rem 1rem; - font-size: 0.75rem; -} - -.btn-icon { - padding: 0.75rem; - min-width: 48px; - border-radius: 50%; -} - -.btn:disabled { - opacity: 0.6; - cursor: not-allowed; - transform: none !important; -} - -/* ============ INPUTS ============ */ -.input { - width: 100%; - padding: 1rem 1.25rem; - background: rgba(22, 33, 62, 0.8); - border: 2px solid transparent; - border-radius: 15px; - color: var(--text); - font-size: 1rem; - transition: all 0.3s ease; - backdrop-filter: blur(10px); -} - -.input:focus { - outline: none; - border-color: var(--secondary); - box-shadow: 0 0 20px rgba(78, 205, 196, 0.3); - background: rgba(22, 33, 62, 1); -} - -.input::placeholder { - color: var(--text-muted); -} - -.label { - display: block; - margin-bottom: 0.5rem; - font-size: 0.875rem; - font-weight: 600; - color: var(--secondary); - text-transform: uppercase; - letter-spacing: 1px; -} - -/* ============ CARDS ============ */ -.card { - background: var(--surface-card); - border: 1px solid var(--border); - border-radius: 20px; - padding: 1.5rem; - backdrop-filter: blur(20px); - box-shadow: var(--shadow-card); - transition: all 0.3s ease; -} - -.card:hover { - border-color: var(--border-bright); - box-shadow: var(--shadow-glow); -} - -.form-group { - margin-bottom: 1.25rem; -} - -/* ============ TABS ============ */ -.tabs { - display: flex; - gap: 0.5rem; - margin-bottom: 1.5rem; - padding: 0.5rem; - background: var(--surface); - border-radius: 50px; - box-shadow: inset 0 2px 10px rgba(0, 0, 0, 0.2); -} - -.tab { - padding: 0.75rem 1.5rem; - background: transparent; - border: none; - border-radius: 50px; - color: var(--text-muted); - font-weight: 600; - cursor: pointer; - transition: all 0.3s ease; -} - -.tab:hover { - color: var(--text); - background: rgba(78, 205, 196, 0.1); -} - -.tab.active { - background: var(--gradient-sunset); - color: var(--background); - box-shadow: 0 4px 15px rgba(255, 107, 107, 0.4); -} - -/* ============ BADGES ============ */ -.badge { - display: inline-flex; - align-items: center; - padding: 0.375rem 0.875rem; - border-radius: 50px; - font-size: 0.75rem; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.badge.admin { - background: var(--gradient-balloon); - color: white; -} - -.badge.active { - background: linear-gradient(135deg, #95E1D3, #4ECDC4); - color: var(--background); -} - -.badge.inactive { - background: linear-gradient(135deg, #FF6B6B, #F38181); - color: white; -} - -.badge.default { - background: linear-gradient(135deg, #FFE66D, #FF9F43); - color: var(--background); -} - -/* ============ TABLES ============ */ -.data-table { - width: 100%; - border-collapse: separate; - border-spacing: 0; -} - -.data-table th, -.data-table td { - padding: 1rem 1.25rem; - text-align: left; -} - -.data-table th { - background: var(--background-light); - font-weight: 700; - color: var(--secondary); - font-size: 0.75rem; - text-transform: uppercase; - letter-spacing: 1px; - border-bottom: 2px solid var(--border); -} - -.data-table tr { - transition: all 0.3s ease; -} - -.data-table tbody tr:hover { - background: rgba(78, 205, 196, 0.05); -} - -.data-table td { - border-bottom: 1px solid var(--border); -} - -/* ============ ANIMATIONS ============ */ -@keyframes float { - 0%, 100% { transform: translateY(0px); } - 50% { transform: translateY(-10px); } -} - -@keyframes pulse-glow { - 0%, 100% { box-shadow: 0 0 20px rgba(78, 205, 196, 0.3); } - 50% { box-shadow: 0 0 40px rgba(78, 205, 196, 0.6); } -} - -@keyframes gradient-shift { - 0% { background-position: 0% 50%; } - 50% { background-position: 100% 50%; } - 100% { background-position: 0% 50%; } -} - -@keyframes spin { - to { transform: rotate(360deg); } -} - -/* ============ MARKDOWN CONTENT ============ */ -.markdown-content { - line-height: 1.8; -} - -.markdown-content p { - margin-bottom: 1rem; -} - -.markdown-content code { - background: var(--background); - padding: 0.25rem 0.5rem; - border-radius: 6px; - font-size: 0.9em; - color: var(--accent-1); - border: 1px solid var(--border); -} - -.markdown-content pre { - background: var(--background); - padding: 1.25rem; - border-radius: 15px; - overflow-x: auto; - margin-bottom: 1rem; - border: 1px solid var(--border); - box-shadow: inset 0 2px 10px rgba(0, 0, 0, 0.2); -} - -.markdown-content pre code { - background: none; - padding: 0; - border: none; -} - -.markdown-content ul, .markdown-content ol { - margin-bottom: 1rem; - padding-left: 1.5rem; -} - -.markdown-content li { - margin-bottom: 0.5rem; -} - -.markdown-content h1, .markdown-content h2, .markdown-content h3 { - margin-bottom: 1rem; - margin-top: 1.5rem; - background: var(--gradient-sunset); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} - -.markdown-content h1 { font-size: 1.5rem; } -.markdown-content h2 { font-size: 1.25rem; } -.markdown-content h3 { font-size: 1.1rem; } - -/* ============ MODAL ============ */ -.modal-overlay { - position: fixed; - inset: 0; - background: rgba(26, 26, 46, 0.9); - backdrop-filter: blur(10px); - display: flex; - align-items: center; - justify-content: center; - z-index: 1000; - padding: 1rem; -} - -.modal { - background: linear-gradient(135deg, var(--surface) 0%, var(--background-light) 100%); - border: 2px solid var(--border-bright); - border-radius: 25px; - padding: 2rem; - width: 100%; - max-width: 500px; - max-height: 90vh; - overflow-y: auto; - box-shadow: - 0 0 50px rgba(78, 205, 196, 0.3), - 0 25px 50px rgba(0, 0, 0, 0.5); - animation: float 3s ease-in-out infinite; -} - -.modal h3 { - background: var(--gradient-sunset); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; - font-size: 1.5rem; - margin-bottom: 1.5rem; -} - -/* ============ NEURAL NETWORK BACKGROUND DECORATION ============ */ -.neural-bg { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - pointer-events: none; - z-index: -1; - background-image: - radial-gradient(circle at 10% 20%, rgba(255, 107, 107, 0.1) 0%, transparent 20%), - radial-gradient(circle at 30% 80%, rgba(78, 205, 196, 0.1) 0%, transparent 20%), - radial-gradient(circle at 70% 30%, rgba(255, 230, 109, 0.1) 0%, transparent 20%), - radial-gradient(circle at 90% 70%, rgba(170, 150, 218, 0.1) 0%, transparent 20%); -} diff --git a/frontend/src/app.html b/frontend/src/app.html deleted file mode 100644 index 9ef94c0..0000000 --- a/frontend/src/app.html +++ /dev/null @@ -1,12 +0,0 @@ - - -
- - - - %sveltekit.head% - - -
-
- Loading Moxiegen...
-
- Moxiegen
- Loading Moxiegen...
-| ID | -Username | -Role | -Status | -Created | -Actions | -|
|---|---|---|---|---|---|---|
| {userRow.id} | -{userRow.username} | -{userRow.email} | -- - {userRow.role} - - | -- - {userRow.is_active ? 'Active' : 'Inactive'} - - | -{new Date(userRow.created_at).toLocaleDateString()} | -
-
-
- {#if userRow.role !== 'admin'}
-
- {/if}
-
- |
-
Type: {endpoint.endpoint_type}
-Model: {endpoint.model_name}
-URL: {endpoint.base_url}
-No endpoints configured. Add one to get started.
- {/each} -
-
- Sign in to your account to continue
-
- Sign up to start using Moxiegen
-