clean old

This commit is contained in:
turtle89431 2026-03-23 20:37:58 -07:00
parent e344431ca1
commit 5efd333fab
30 changed files with 0 additions and 4264 deletions

View File

@ -1 +0,0 @@
# Moxiegen Backend Application

View File

@ -1 +0,0 @@
# API module

View File

@ -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
)

View File

@ -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

View File

@ -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"}

View File

@ -1 +0,0 @@
# Core module

View File

@ -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

View File

@ -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()

View File

@ -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()

View File

@ -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"}

View File

@ -1 +0,0 @@
# Models module

View File

@ -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")

View File

@ -1 +0,0 @@
# Schemas module

View File

@ -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

View File

@ -1,10 +0,0 @@
fastapi
uvicorn
sqlalchemy
pydantic
python-jose
passlib
python-multipart
aiofiles
httpx
openai

View File

@ -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"
}

View File

@ -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%);
}

View File

@ -1,12 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/logo.jpg" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

View File

@ -1,202 +0,0 @@
const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:8000/api';
class ApiClient {
constructor() {
this.token = null;
if (typeof window !== 'undefined') {
this.token = localStorage.getItem('token');
}
}
setToken(token) {
this.token = token;
if (typeof window !== 'undefined') {
if (token) {
localStorage.setItem('token', token);
} else {
localStorage.removeItem('token');
}
}
}
async request(endpoint, options = {}) {
const headers = {
'Content-Type': 'application/json',
...options.headers,
};
if (this.token) {
headers['Authorization'] = `Bearer ${this.token}`;
}
const response = await fetch(`${API_BASE}${endpoint}`, {
...options,
headers,
});
if (response.status === 401) {
this.setToken(null);
if (typeof window !== 'undefined') {
window.location.href = '/login';
}
throw new Error('Unauthorized');
}
if (!response.ok) {
const errorData = await response.json().catch(() => ({ detail: 'Request failed' }));
// Handle FastAPI validation errors
if (errorData.detail && typeof errorData.detail === 'object') {
throw new Error(JSON.stringify(errorData.detail));
}
throw new Error(errorData.detail || 'Request failed');
}
if (response.status === 204) {
return null;
}
return response.json();
}
async login(email, password) {
const response = await fetch(`${API_BASE}/auth/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ email, password }),
});
const data = await response.json().catch(() => ({}));
if (!response.ok) {
if (data.detail) {
if (typeof data.detail === 'string') {
throw new Error(data.detail);
}
// Validation errors
if (Array.isArray(data.detail)) {
const messages = data.detail.map(e => e.msg).join(', ');
throw new Error(messages);
}
throw new Error(JSON.stringify(data.detail));
}
throw new Error('Login failed');
}
this.setToken(data.access_token);
return data;
}
async register(email, username, password) {
return this.request('/auth/register', {
method: 'POST',
body: JSON.stringify({ email, username, password }),
});
}
async getMe() {
return this.request('/auth/me');
}
async updateMe(data) {
return this.request('/auth/me', {
method: 'PUT',
body: JSON.stringify(data),
});
}
async getEndpoints() {
return this.request('/chat/endpoints');
}
async sendMessage(message, endpointId = null, fileIds = [], conversationHistory = []) {
return this.request('/chat/message', {
method: 'POST',
body: JSON.stringify({
message,
endpoint_id: endpointId,
file_ids: fileIds,
conversation_history: conversationHistory,
}),
});
}
async getChatHistory(limit = 50) {
return this.request(`/chat/history?limit=${limit}`);
}
async uploadFile(file) {
const formData = new FormData();
formData.append('file', file);
const headers = {};
if (this.token) {
headers['Authorization'] = `Bearer ${this.token}`;
}
const response = await fetch(`${API_BASE}/chat/upload`, {
method: 'POST',
headers,
body: formData,
});
if (!response.ok) {
const error = await response.json().catch(() => ({ detail: 'Upload failed' }));
throw new Error(error.detail || 'Upload failed');
}
return response.json();
}
async getFiles() {
return this.request('/chat/files');
}
async deleteFile(fileId) {
return this.request(`/chat/files/${fileId}`, { method: 'DELETE' });
}
async getAdminStats() {
return this.request('/admin/stats');
}
async getUsers() {
return this.request('/admin/users');
}
async updateUser(userId, data) {
return this.request(`/admin/users/${userId}`, {
method: 'PUT',
body: JSON.stringify(data),
});
}
async deleteUser(userId) {
return this.request(`/admin/users/${userId}`, { method: 'DELETE' });
}
async getAdminEndpoints() {
return this.request('/admin/endpoints');
}
async createEndpoint(data) {
return this.request('/admin/endpoints', {
method: 'POST',
body: JSON.stringify(data),
});
}
async updateEndpoint(endpointId, data) {
return this.request(`/admin/endpoints/${endpointId}`, {
method: 'PUT',
body: JSON.stringify(data),
});
}
async deleteEndpoint(endpointId) {
return this.request(`/admin/endpoints/${endpointId}`, { method: 'DELETE' });
}
}
export const api = new ApiClient();

View File

@ -1,39 +0,0 @@
import { api } from './api.js';
// Svelte 5 runes-based stores
let user = $state(null);
let isLoading = $state(true);
export function getUser() {
return user;
}
export function getIsLoading() {
return isLoading;
}
export async function initializeAuth() {
try {
if (api.token) {
const userData = await api.getMe();
user = userData;
}
} catch (e) {
api.setToken(null);
user = null;
} finally {
isLoading = false;
}
}
export function setUser(userData) {
user = userData;
}
export function logout() {
api.setToken(null);
user = null;
if (typeof window !== 'undefined') {
window.location.href = '/login';
}
}

View File

@ -1,2 +0,0 @@
export const ssr = false;
export const prerender = false;

View File

@ -1,436 +0,0 @@
<script>
import '../app.css';
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { getUser, getIsLoading, initializeAuth, logout, setUser } from '$lib/stores.svelte';
import { api } from '$lib/api';
let sidebarOpen = $state(false);
let user = $derived(getUser());
let isLoading = $derived(getIsLoading());
onMount(() => {
initializeAuth();
});
async function handleLogout() {
logout();
}
</script>
{#if isLoading}
<div class="loading-screen">
<div class="loading-content">
<img src="/logo.jpg" alt="Moxiegen" class="loading-logo" />
<div class="loading-spinner"></div>
<p>Loading Moxiegen...</p>
</div>
</div>
{:else if user}
<div class="app-layout">
<!-- Header -->
<header class="header">
<button class="menu-toggle" onclick={() => sidebarOpen = !sidebarOpen}>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="3" y1="12" x2="21" y2="12"></line>
<line x1="3" y1="6" x2="21" y2="6"></line>
<line x1="3" y1="18" x2="21" y2="18"></line>
</svg>
</button>
<div class="logo">
<img src="/logo.jpg" alt="Moxiegen" class="logo-img" />
<span class="logo-text">Moxiegen</span>
</div>
<div class="header-right">
<div class="user-badge">
<span class="user-icon">👤</span>
<span class="user-name">{user.username}</span>
</div>
<button class="btn btn-secondary btn-sm" onclick={handleLogout}>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path>
<polyline points="16 17 21 12 16 7"></polyline>
<line x1="21" y1="12" x2="9" y2="12"></line>
</svg>
Logout
</button>
</div>
</header>
<!-- Sidebar -->
<aside class="sidebar" class:open={sidebarOpen}>
<div class="sidebar-header">
<h3>Navigation</h3>
</div>
<nav class="sidebar-nav">
<a href="/chat" class="nav-link" class:active={$page.url.pathname === '/chat'}>
<span class="nav-icon">💬</span>
<span class="nav-text">Chat</span>
<span class="nav-glow"></span>
</a>
{#if user.role === 'admin'}
<a href="/admin" class="nav-link" class:active={$page.url.pathname.startsWith('/admin')}>
<span class="nav-icon">⚙️</span>
<span class="nav-text">Admin Panel</span>
<span class="nav-glow"></span>
</a>
{/if}
</nav>
<div class="sidebar-footer">
<div class="neural-decoration">
<div class="node n1"></div>
<div class="node n2"></div>
<div class="node n3"></div>
<div class="connection c1"></div>
<div class="connection c2"></div>
</div>
</div>
</aside>
<!-- Main Content -->
<main class="main-content">
<slot />
</main>
<!-- Overlay for mobile -->
{#if sidebarOpen}
<div class="sidebar-overlay" onclick={() => sidebarOpen = false}></div>
{/if}
</div>
<div class="neural-bg"></div>
{:else}
<slot />
<div class="neural-bg"></div>
{/if}
<style>
.loading-screen {
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
background: var(--background);
}
.loading-content {
text-align: center;
}
.loading-logo {
width: 100px;
height: 100px;
border-radius: 20px;
margin-bottom: 1.5rem;
animation: float 3s ease-in-out infinite;
}
.loading-spinner {
width: 50px;
height: 50px;
margin: 0 auto 1rem;
border: 4px solid var(--border);
border-top-color: var(--primary);
border-right-color: var(--accent-1);
border-radius: 50%;
animation: spin 1s linear infinite;
}
.loading-content p {
color: var(--text-muted);
font-size: 1rem;
}
.app-layout {
display: grid;
grid-template-areas:
"header header"
"sidebar main";
grid-template-columns: var(--sidebar-width) 1fr;
grid-template-rows: var(--header-height) 1fr;
height: 100vh;
}
/* Header */
.header {
grid-area: header;
display: flex;
align-items: center;
gap: 1rem;
padding: 0 2rem;
background: linear-gradient(135deg, var(--surface) 0%, var(--background-light) 100%);
border-bottom: 2px solid transparent;
border-image: var(--gradient-sunset) 1;
z-index: 100;
}
.menu-toggle {
display: none;
background: transparent;
border: 2px solid var(--secondary);
color: var(--secondary);
padding: 0.5rem;
border-radius: 10px;
cursor: pointer;
transition: all 0.3s ease;
}
.menu-toggle:hover {
background: var(--secondary);
color: var(--background);
}
.logo {
display: flex;
align-items: center;
gap: 1rem;
}
.logo-img {
width: 50px;
height: 50px;
border-radius: 15px;
object-fit: cover;
box-shadow: 0 4px 15px rgba(255, 107, 107, 0.3);
transition: all 0.3s ease;
}
.logo-img:hover {
transform: scale(1.1) rotate(5deg);
box-shadow: 0 6px 25px rgba(255, 107, 107, 0.5);
}
.logo-text {
font-size: 1.5rem;
font-weight: 800;
background: var(--gradient-sunset);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
letter-spacing: 1px;
}
.header-right {
margin-left: auto;
display: flex;
align-items: center;
gap: 1.5rem;
}
.user-badge {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: rgba(78, 205, 196, 0.1);
border: 1px solid var(--secondary);
border-radius: 50px;
}
.user-icon {
font-size: 1.2rem;
}
.user-name {
color: var(--secondary);
font-weight: 600;
font-size: 0.9rem;
}
/* Sidebar */
.sidebar {
grid-area: sidebar;
background: linear-gradient(180deg, var(--surface) 0%, var(--background-light) 100%);
border-right: 2px solid transparent;
border-image: var(--gradient-sky) 1;
padding: 1.5rem;
overflow-y: auto;
position: relative;
}
.sidebar-header h3 {
font-size: 0.75rem;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 2px;
margin-bottom: 1rem;
padding: 0 0.5rem;
}
.sidebar-nav {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.nav-link {
position: relative;
display: flex;
align-items: center;
gap: 1rem;
padding: 1rem 1.25rem;
border-radius: 15px;
color: var(--text-muted);
text-decoration: none;
transition: all 0.3s ease;
overflow: hidden;
}
.nav-link:hover {
color: var(--text);
background: rgba(78, 205, 196, 0.1);
text-decoration: none;
}
.nav-link:hover .nav-glow {
opacity: 1;
}
.nav-link.active {
background: var(--gradient-sunset);
color: var(--background);
box-shadow: 0 4px 20px rgba(255, 107, 107, 0.4);
}
.nav-link.active .nav-icon {
transform: scale(1.2);
}
.nav-icon {
font-size: 1.5rem;
transition: transform 0.3s ease;
}
.nav-text {
font-weight: 600;
font-size: 1rem;
}
.nav-glow {
position: absolute;
inset: 0;
background: radial-gradient(circle at center, rgba(78, 205, 196, 0.2), transparent);
opacity: 0;
transition: opacity 0.3s ease;
}
/* Neural Network Decoration */
.sidebar-footer {
position: absolute;
bottom: 2rem;
left: 50%;
transform: translateX(-50%);
}
.neural-decoration {
position: relative;
width: 100px;
height: 60px;
}
.node {
position: absolute;
width: 12px;
height: 12px;
border-radius: 50%;
animation: pulse-glow 2s ease-in-out infinite;
}
.n1 {
left: 10px;
top: 10px;
background: var(--primary);
animation-delay: 0s;
}
.n2 {
left: 44px;
top: 35px;
background: var(--secondary);
animation-delay: 0.5s;
}
.n3 {
right: 10px;
top: 10px;
background: var(--accent-1);
animation-delay: 1s;
}
.connection {
position: absolute;
height: 2px;
background: linear-gradient(90deg, var(--primary), var(--secondary));
opacity: 0.5;
transform-origin: left center;
}
.c1 {
left: 16px;
top: 16px;
width: 35px;
transform: rotate(45deg);
}
.c2 {
left: 50px;
top: 40px;
width: 35px;
transform: rotate(-45deg);
background: linear-gradient(90deg, var(--secondary), var(--accent-1));
}
/* Main Content */
.main-content {
grid-area: main;
overflow-y: auto;
padding: 2rem;
}
.sidebar-overlay {
display: none;
}
@media (max-width: 768px) {
.app-layout {
grid-template-columns: 1fr;
}
.sidebar {
position: fixed;
left: 0;
top: var(--header-height);
bottom: 0;
width: var(--sidebar-width);
transform: translateX(-100%);
transition: transform 0.3s ease;
z-index: 90;
}
.sidebar.open {
transform: translateX(0);
}
.sidebar-overlay {
display: block;
position: fixed;
inset: 0;
top: var(--header-height);
background: rgba(26, 26, 46, 0.9);
backdrop-filter: blur(5px);
z-index: 80;
}
.menu-toggle {
display: flex;
}
.main-content {
padding: 1rem;
}
}
</style>

View File

@ -1,48 +0,0 @@
<script>
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { getUser, getIsLoading, initializeAuth } from '$lib/stores.svelte';
let user = $derived(getUser());
let isLoading = $derived(getIsLoading());
onMount(async () => {
await initializeAuth();
if (user) {
goto('/chat');
} else {
goto('/login');
}
});
</script>
<div class="loading-container">
<div class="spinner"></div>
<p>Loading Moxiegen...</p>
</div>
<style>
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
gap: 1rem;
background: var(--background);
color: var(--text-muted);
}
.spinner {
width: 40px;
height: 40px;
border: 3px solid var(--border);
border-top-color: var(--primary);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
</style>

View File

@ -1,669 +0,0 @@
<script>
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { api } from '$lib/api';
import { getUser, initializeAuth } from '$lib/stores.svelte';
let stats = $state({ total_users: 0, total_endpoints: 0, total_messages: 0, active_endpoints: 0 });
let users = $state([]);
let endpoints = $state([]);
let showUserModal = $state(false);
let editingUser = $state(null);
let userForm = $state({ email: '', username: '', role: 'user', is_active: true });
let showEndpointModal = $state(false);
let editingEndpoint = $state(null);
let endpointForm = $state({
name: '',
endpoint_type: 'openai',
base_url: '',
api_key: '',
model_name: '',
is_active: true,
is_default: false
});
let activeTab = $state('stats');
let loading = $state(true);
let user = $derived(getUser());
onMount(async () => {
await initializeAuth();
if (!user || user.role !== 'admin') {
goto('/chat');
return;
}
await loadData();
});
async function loadData() {
loading = true;
try {
const [statsData, usersData, endpointsData] = await Promise.all([
api.getAdminStats(),
api.getUsers(),
api.getAdminEndpoints()
]);
stats = statsData;
users = usersData;
endpoints = endpointsData;
} catch (e) {
console.error('Failed to load admin data:', e);
} finally {
loading = false;
}
}
function openUserModal(user = null) {
editingUser = user;
if (user) {
userForm = {
email: user.email,
username: user.username,
role: user.role,
is_active: user.is_active
};
} else {
userForm = { email: '', username: '', role: 'user', is_active: true };
}
showUserModal = true;
}
async function saveUser() {
try {
if (editingUser) {
await api.updateUser(editingUser.id, userForm);
}
showUserModal = false;
await loadData();
} catch (e) {
alert('Failed to save user: ' + e.message);
}
}
async function deleteUser(user) {
if (!confirm(`Delete user "${user.username}"?`)) return;
try {
await api.deleteUser(user.id);
await loadData();
} catch (e) {
alert('Failed to delete user: ' + e.message);
}
}
function openEndpointModal(endpoint = null) {
editingEndpoint = endpoint;
if (endpoint) {
endpointForm = {
name: endpoint.name,
endpoint_type: endpoint.endpoint_type,
base_url: endpoint.base_url,
api_key: endpoint.api_key || '',
model_name: endpoint.model_name,
is_active: endpoint.is_active,
is_default: endpoint.is_default
};
} else {
endpointForm = {
name: '',
endpoint_type: 'openai',
base_url: '',
api_key: '',
model_name: '',
is_active: true,
is_default: false
};
}
showEndpointModal = true;
}
async function saveEndpoint() {
try {
if (editingEndpoint) {
await api.updateEndpoint(editingEndpoint.id, endpointForm);
} else {
await api.createEndpoint(endpointForm);
}
showEndpointModal = false;
await loadData();
} catch (e) {
alert('Failed to save endpoint: ' + e.message);
}
}
async function deleteEndpoint(endpoint) {
if (!confirm(`Delete endpoint "${endpoint.name}"?`)) return;
try {
await api.deleteEndpoint(endpoint.id);
await loadData();
} catch (e) {
alert('Failed to delete endpoint: ' + e.message);
}
}
</script>
<div class="admin-page">
<h1>Admin Panel</h1>
<div class="tabs">
<button
class="tab"
class:active={activeTab === 'stats'}
onclick={() => activeTab = 'stats'}
>
Dashboard
</button>
<button
class="tab"
class:active={activeTab === 'users'}
onclick={() => activeTab = 'users'}
>
Users
</button>
<button
class="tab"
class:active={activeTab === 'endpoints'}
onclick={() => activeTab = 'endpoints'}
>
AI Endpoints
</button>
</div>
{#if loading}
<div class="loading">
<div class="spinner"></div>
</div>
{:else}
{#if activeTab === 'stats'}
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value">{stats.total_users}</div>
<div class="stat-label">Total Users</div>
</div>
<div class="stat-card">
<div class="stat-value">{stats.total_endpoints}</div>
<div class="stat-label">AI Endpoints</div>
</div>
<div class="stat-card">
<div class="stat-value">{stats.active_endpoints}</div>
<div class="stat-label">Active Endpoints</div>
</div>
<div class="stat-card">
<div class="stat-value">{stats.total_messages}</div>
<div class="stat-label">Total Messages</div>
</div>
</div>
{/if}
{#if activeTab === 'users'}
<div class="section-header">
<h2>Users</h2>
</div>
<div class="table-container">
<table class="data-table">
<thead>
<tr>
<th>ID</th>
<th>Username</th>
<th>Email</th>
<th>Role</th>
<th>Status</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{#each users as userRow}
<tr>
<td>{userRow.id}</td>
<td>{userRow.username}</td>
<td>{userRow.email}</td>
<td>
<span class="badge" class:admin={userRow.role === 'admin'}>
{userRow.role}
</span>
</td>
<td>
<span class="status" class:active={userRow.is_active}>
{userRow.is_active ? 'Active' : 'Inactive'}
</span>
</td>
<td>{new Date(userRow.created_at).toLocaleDateString()}</td>
<td>
<div class="actions">
<button class="btn btn-secondary btn-sm" onclick={() => openUserModal(userRow)}>
Edit
</button>
{#if userRow.role !== 'admin'}
<button class="btn btn-danger btn-sm" onclick={() => deleteUser(userRow)}>
Delete
</button>
{/if}
</div>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
{#if activeTab === 'endpoints'}
<div class="section-header">
<h2>AI Endpoints</h2>
<button class="btn btn-primary" onclick={() => openEndpointModal()}>
+ Add Endpoint
</button>
</div>
<div class="endpoints-grid">
{#each endpoints as endpoint}
<div class="endpoint-card">
<div class="endpoint-header">
<h3>{endpoint.name}</h3>
<div class="endpoint-badges">
{#if endpoint.is_default}
<span class="badge default">Default</span>
{/if}
{#if endpoint.is_active}
<span class="badge active">Active</span>
{:else}
<span class="badge inactive">Inactive</span>
{/if}
</div>
</div>
<div class="endpoint-details">
<p><strong>Type:</strong> {endpoint.endpoint_type}</p>
<p><strong>Model:</strong> {endpoint.model_name}</p>
<p><strong>URL:</strong> {endpoint.base_url}</p>
</div>
<div class="endpoint-actions">
<button class="btn btn-secondary btn-sm" onclick={() => openEndpointModal(endpoint)}>
Edit
</button>
<button class="btn btn-danger btn-sm" onclick={() => deleteEndpoint(endpoint)}>
Delete
</button>
</div>
</div>
{:else}
<p class="no-data">No endpoints configured. Add one to get started.</p>
{/each}
</div>
{/if}
{/if}
</div>
{#if showUserModal}
<div class="modal-overlay" onclick={() => showUserModal = false}>
<div class="modal" onclick={(e) => e.stopPropagation()}>
<h3>{editingUser ? 'Edit User' : 'New User'}</h3>
<form onsubmit={(e) => { e.preventDefault(); saveUser(); }}>
<div class="form-group">
<label class="label">Email</label>
<input type="email" class="input" bind:value={userForm.email} required />
</div>
<div class="form-group">
<label class="label">Username</label>
<input type="text" class="input" bind:value={userForm.username} required />
</div>
<div class="form-group">
<label class="label">Role</label>
<select class="input" bind:value={userForm.role}>
<option value="user">User</option>
<option value="admin">Admin</option>
</select>
</div>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" bind:checked={userForm.is_active} />
Active
</label>
</div>
<div class="modal-actions">
<button type="button" class="btn btn-secondary" onclick={() => showUserModal = false}>
Cancel
</button>
<button type="submit" class="btn btn-primary">Save</button>
</div>
</form>
</div>
</div>
{/if}
{#if showEndpointModal}
<div class="modal-overlay" onclick={() => showEndpointModal = false}>
<div class="modal" onclick={(e) => e.stopPropagation()}>
<h3>{editingEndpoint ? 'Edit Endpoint' : 'Add AI Endpoint'}</h3>
<form onsubmit={(e) => { e.preventDefault(); saveEndpoint(); }}>
<div class="form-group">
<label class="label">Name</label>
<input type="text" class="input" bind:value={endpointForm.name} placeholder="My OpenAI Endpoint" required />
</div>
<div class="form-group">
<label class="label">Type</label>
<select class="input" bind:value={endpointForm.endpoint_type}>
<option value="openai">OpenAI Compatible</option>
<option value="ollama">Ollama</option>
</select>
</div>
<div class="form-group">
<label class="label">Base URL</label>
<input
type="text"
class="input"
bind:value={endpointForm.base_url}
placeholder="https://api.openai.com/v1"
required
/>
</div>
<div class="form-group">
<label class="label">API Key (optional for Ollama)</label>
<input
type="password"
class="input"
bind:value={endpointForm.api_key}
placeholder="sk-..."
/>
</div>
<div class="form-group">
<label class="label">Model Name</label>
<input
type="text"
class="input"
bind:value={endpointForm.model_name}
placeholder="gpt-3.5-turbo or llama2"
required
/>
</div>
<div class="form-row">
<label class="checkbox-label">
<input type="checkbox" bind:checked={endpointForm.is_active} />
Active
</label>
<label class="checkbox-label">
<input type="checkbox" bind:checked={endpointForm.is_default} />
Default
</label>
</div>
<div class="modal-actions">
<button type="button" class="btn btn-secondary" onclick={() => showEndpointModal = false}>
Cancel
</button>
<button type="submit" class="btn btn-primary">Save</button>
</div>
</form>
</div>
</div>
{/if}
<style>
.admin-page {
max-width: 1200px;
margin: 0 auto;
}
h1 {
margin-bottom: 1.5rem;
}
.tabs {
display: flex;
gap: 0.5rem;
margin-bottom: 1.5rem;
}
.tab {
padding: 0.75rem 1.5rem;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 0.5rem;
color: var(--text-muted);
cursor: pointer;
transition: all 0.2s;
}
.tab:hover {
background: var(--surface-hover);
color: var(--text);
}
.tab.active {
background: var(--primary);
border-color: var(--primary);
color: white;
}
.loading {
display: flex;
justify-content: center;
padding: 3rem;
}
.spinner {
width: 32px;
height: 32px;
border: 3px solid var(--border);
border-top-color: var(--primary);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-bottom: 2rem;
}
.stat-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 0.75rem;
padding: 1.5rem;
text-align: center;
}
.stat-value {
font-size: 2.5rem;
font-weight: 700;
color: var(--primary);
}
.stat-label {
color: var(--text-muted);
font-size: 0.875rem;
margin-top: 0.5rem;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.section-header h2 {
font-size: 1.25rem;
}
.table-container {
overflow-x: auto;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 0.75rem;
}
.data-table {
width: 100%;
border-collapse: collapse;
}
.data-table th,
.data-table td {
padding: 0.75rem 1rem;
text-align: left;
border-bottom: 1px solid var(--border);
}
.data-table th {
background: var(--background);
font-weight: 600;
color: var(--text-muted);
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.data-table tr:last-child td {
border-bottom: none;
}
.badge {
display: inline-block;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.75rem;
font-weight: 500;
background: var(--surface-hover);
color: var(--text-muted);
}
.badge.admin {
background: var(--primary);
color: white;
}
.badge.active {
background: var(--success);
color: white;
}
.badge.inactive {
background: var(--danger);
color: white;
}
.badge.default {
background: var(--warning);
color: black;
}
.status {
font-size: 0.75rem;
color: var(--text-muted);
}
.status.active {
color: var(--success);
}
.actions {
display: flex;
gap: 0.5rem;
}
.endpoints-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1rem;
}
.endpoint-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 0.75rem;
padding: 1.25rem;
}
.endpoint-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 1rem;
}
.endpoint-header h3 {
font-size: 1rem;
margin: 0;
}
.endpoint-badges {
display: flex;
gap: 0.25rem;
}
.endpoint-details {
font-size: 0.875rem;
color: var(--text-muted);
margin-bottom: 1rem;
}
.endpoint-details p {
margin-bottom: 0.25rem;
}
.endpoint-actions {
display: flex;
gap: 0.5rem;
}
.no-data {
color: var(--text-muted);
font-style: italic;
grid-column: 1 / -1;
}
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 1rem;
}
.modal {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 0.75rem;
padding: 1.5rem;
width: 100%;
max-width: 500px;
max-height: 90vh;
overflow-y: auto;
}
.modal h3 {
margin-bottom: 1rem;
}
.form-row {
display: flex;
gap: 1rem;
margin-bottom: 1rem;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
font-size: 0.875rem;
}
.checkbox-label input {
accent-color: var(--primary);
}
.modal-actions {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
margin-top: 1rem;
}
</style>

View File

@ -1,812 +0,0 @@
<script>
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { api } from '$lib/api';
import { getUser, getIsLoading, initializeAuth } from '$lib/stores.svelte';
import { marked } from 'marked';
import hljs from 'highlight.js';
let messages = $state([]);
let inputMessage = $state('');
let endpoints = $state([]);
let selectedEndpoint = $state(null);
let loading = $state(true);
let sending = $state(false);
let uploadedFiles = $state([]);
let selectedFiles = $state([]);
let fileInput;
let user = $derived(getUser());
let isLoading = $derived(getIsLoading());
marked.setOptions({
highlight: function(code, lang) {
if (lang && hljs.getLanguage(lang)) {
return hljs.highlight(code, { language: lang }).value;
}
return hljs.highlightAuto(code).value;
}
});
onMount(async () => {
await initializeAuth();
if (!user) {
goto('/login');
return;
}
await loadData();
});
async function loadData() {
loading = true;
try {
const [endpointsData, historyData, filesData] = await Promise.all([
api.getEndpoints(),
api.getChatHistory(50),
api.getFiles()
]);
endpoints = endpointsData;
uploadedFiles = filesData;
const defaultEp = endpoints.find(e => e.is_default);
if (defaultEp) {
selectedEndpoint = defaultEp.id;
} else if (endpoints.length > 0) {
selectedEndpoint = endpoints[0].id;
}
messages = historyData.map(m => ({
role: m.role,
content: m.content,
timestamp: new Date(m.created_at)
}));
} catch (e) {
console.error('Failed to load data:', e);
} finally {
loading = false;
}
}
async function sendMessage() {
if (!inputMessage.trim() || sending) return;
const userMessage = inputMessage;
inputMessage = '';
messages = [...messages, {
role: 'user',
content: userMessage,
timestamp: new Date()
}];
const conversationHistory = messages.slice(-10).map(m => ({
role: m.role,
content: m.content
}));
sending = true;
try {
const response = await api.sendMessage(
userMessage,
selectedEndpoint,
selectedFiles.map(f => f.id),
conversationHistory
);
messages = [...messages, {
role: 'assistant',
content: response.response,
model: response.model_used,
timestamp: new Date()
}];
selectedFiles = [];
} catch (e) {
messages = [...messages, {
role: 'assistant',
content: `Error: ${e.message}`,
isError: true,
timestamp: new Date()
}];
} finally {
sending = false;
scrollToBottom();
}
}
async function handleFileUpload(e) {
const file = e.target.files[0];
if (!file) return;
try {
const uploaded = await api.uploadFile(file);
uploadedFiles = [...uploadedFiles, uploaded];
selectedFiles = [...selectedFiles, uploaded];
} catch (e) {
alert('Failed to upload file: ' + e.message);
}
e.target.value = '';
}
function removeSelectedFile(file) {
selectedFiles = selectedFiles.filter(f => f.id !== file.id);
}
function scrollToBottom() {
setTimeout(() => {
const container = document.querySelector('.messages-container');
if (container) {
container.scrollTop = container.scrollHeight;
}
}, 10);
}
function renderMarkdown(content) {
return marked.parse(content);
}
function formatFileSize(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
}
</script>
<div class="chat-page">
<!-- Sidebar -->
<aside class="chat-sidebar">
<div class="sidebar-section">
<h3>🎈 AI Endpoints</h3>
{#if endpoints.length === 0}
<p class="no-data">No endpoints configured. <a href="/admin">Add one</a></p>
{:else}
<div class="endpoint-list">
{#each endpoints as endpoint}
<label class="endpoint-option" class:selected={selectedEndpoint === endpoint.id}>
<input
type="radio"
name="endpoint"
value={endpoint.id}
bind:group={selectedEndpoint}
/>
<div class="endpoint-info">
<span class="endpoint-name">{endpoint.name}</span>
<span class="endpoint-model">{endpoint.model}</span>
</div>
{#if endpoint.is_default}
<span class="default-badge"></span>
{/if}
</label>
{/each}
</div>
{/if}
</div>
<div class="sidebar-section">
<h4>📎 Recent Files</h4>
{#if uploadedFiles.length === 0}
<p class="no-data">No files uploaded yet</p>
{:else}
<ul class="files-list">
{#each uploadedFiles.slice(0, 5) as file}
<li>
<span class="file-icon">📄</span>
<span class="file-name">{file.original_filename}</span>
</li>
{/each}
</ul>
{/if}
</div>
</aside>
<!-- Main Chat Area -->
<div class="chat-main">
<!-- Messages -->
<div class="messages-container">
{#if loading}
<div class="loading-state">
<div class="loading-balloon"></div>
<p>Loading your conversations...</p>
</div>
{:else if messages.length === 0}
<div class="empty-state">
<div class="empty-illustration">
<div class="balloon-small"></div>
<div class="balloon-small b2"></div>
<div class="balloon-small b3"></div>
</div>
<h2>Start a Conversation</h2>
<p>Send a message to begin chatting with AI</p>
<div class="suggestions">
<button onclick={() => inputMessage = 'Explain quantum computing'} class="suggestion-chip">
🔬 Explain quantum computing
</button>
<button onclick={() => inputMessage = 'Write a poem about technology'} class="suggestion-chip">
✍️ Write a poem
</button>
<button onclick={() => inputMessage = 'Help me brainstorm ideas'} class="suggestion-chip">
💡 Brainstorm ideas
</button>
</div>
</div>
{:else}
{#each messages as message}
<div class="message {message.role}" class:error={message.isError}>
<div class="message-avatar">
{#if message.role === 'user'}
👤
{:else}
🤖
{/if}
</div>
<div class="message-body">
<div class="message-header">
<span class="message-role">
{message.role === 'user' ? 'You' : 'Assistant'}
</span>
{#if message.model}
<span class="message-model">{message.model}</span>
{/if}
<span class="message-time">
{message.timestamp.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}
</span>
</div>
<div class="message-content">
{#if message.role === 'user'}
{message.content}
{:else}
<div class="markdown-content">
{@html renderMarkdown(message.content)}
</div>
{/if}
</div>
</div>
</div>
{/each}
{/if}
</div>
<!-- Input Area -->
<div class="input-area">
{#if selectedFiles.length > 0}
<div class="selected-files">
{#each selectedFiles as file}
<div class="file-chip">
<span>📎 {file.original_filename}</span>
<button onclick={() => removeSelectedFile(file)}>×</button>
</div>
{/each}
</div>
{/if}
<form onsubmit={(e) => { e.preventDefault(); sendMessage(); }} class="input-form">
<div class="input-wrapper">
<button
type="button"
class="attach-btn"
onclick={() => fileInput.click()}
title="Attach file"
>
📎
</button>
<input
type="file"
bind:this={fileInput}
onchange={handleFileUpload}
accept=".txt,.pdf,.png,.jpg,.jpeg,.gif,.doc,.docx,.xls,.xlsx,.csv,.json,.md"
style="display: none"
/>
<textarea
class="message-input"
placeholder="Type your message..."
bind:value={inputMessage}
onkeydown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
}}
rows="1"
></textarea>
<button
type="submit"
class="send-btn"
disabled={!inputMessage.trim() || sending || endpoints.length === 0}
>
{#if sending}
<div class="sending-dots">
<span></span><span></span><span></span>
</div>
{:else}
🚀
{/if}
</button>
</div>
</form>
</div>
</div>
</div>
<style>
@import 'highlight.js/styles/github-dark.css';
.chat-page {
display: flex;
gap: 1.5rem;
height: calc(100vh - var(--header-height) - 4rem);
}
/* Sidebar */
.chat-sidebar {
width: 260px;
display: flex;
flex-direction: column;
gap: 1.5rem;
flex-shrink: 0;
}
.sidebar-section {
background: var(--surface-card);
border: 1px solid var(--border);
border-radius: 20px;
padding: 1.25rem;
backdrop-filter: blur(10px);
}
.sidebar-section h3 {
font-size: 0.8rem;
color: var(--accent-1);
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 1rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.sidebar-section h4 {
font-size: 0.75rem;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 0.75rem;
}
.no-data {
color: var(--text-muted);
font-size: 0.85rem;
font-style: italic;
}
.endpoint-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.endpoint-option {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem;
border-radius: 12px;
cursor: pointer;
transition: all 0.3s ease;
border: 1px solid transparent;
}
.endpoint-option:hover {
background: rgba(78, 205, 196, 0.1);
border-color: var(--border);
}
.endpoint-option.selected {
background: linear-gradient(135deg, rgba(255, 107, 107, 0.2), rgba(255, 230, 109, 0.2));
border-color: var(--primary);
}
.endpoint-option input {
display: none;
}
.endpoint-info {
flex: 1;
display: flex;
flex-direction: column;
}
.endpoint-name {
font-size: 0.9rem;
color: var(--text);
font-weight: 600;
}
.endpoint-model {
font-size: 0.75rem;
color: var(--text-muted);
}
.default-badge {
font-size: 1rem;
}
.files-list {
list-style: none;
}
.files-list li {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0;
border-bottom: 1px solid var(--border);
font-size: 0.85rem;
}
.files-list li:last-child {
border-bottom: none;
}
.file-icon {
font-size: 1rem;
}
.file-name {
color: var(--text-muted);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Main Chat */
.chat-main {
flex: 1;
display: flex;
flex-direction: column;
background: var(--surface-card);
border: 1px solid var(--border);
border-radius: 25px;
overflow: hidden;
backdrop-filter: blur(10px);
}
.messages-container {
flex: 1;
overflow-y: auto;
padding: 1.5rem;
}
.loading-state, .empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
text-align: center;
color: var(--text-muted);
}
.loading-balloon {
width: 60px;
height: 75px;
background: var(--gradient-sunset);
border-radius: 50% 50% 50% 50%;
margin-bottom: 1rem;
animation: float 2s ease-in-out infinite;
}
.empty-illustration {
display: flex;
gap: 1rem;
margin-bottom: 1.5rem;
}
.balloon-small {
width: 40px;
height: 50px;
background: var(--gradient-sunset);
border-radius: 50% 50% 50% 50%;
animation: float 3s ease-in-out infinite;
}
.balloon-small.b2 {
background: var(--gradient-sky);
animation-delay: 0.5s;
transform: translateY(10px);
}
.balloon-small.b3 {
background: var(--gradient-balloon);
animation-delay: 1s;
}
.empty-state h2 {
background: var(--gradient-sunset);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
font-size: 1.5rem;
margin-bottom: 0.5rem;
}
.suggestions {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: 1.5rem;
justify-content: center;
}
.suggestion-chip {
background: var(--surface);
border: 1px solid var(--border);
color: var(--text);
padding: 0.5rem 1rem;
border-radius: 50px;
font-size: 0.85rem;
cursor: pointer;
transition: all 0.3s ease;
}
.suggestion-chip:hover {
background: var(--gradient-sunset);
color: var(--background);
border-color: transparent;
transform: translateY(-2px);
}
/* Messages */
.message {
display: flex;
gap: 1rem;
margin-bottom: 1.5rem;
max-width: 85%;
}
.message.user {
margin-left: auto;
flex-direction: row-reverse;
}
.message-avatar {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.25rem;
border-radius: 50%;
flex-shrink: 0;
}
.message.user .message-avatar {
background: var(--gradient-sunset);
}
.message.assistant .message-avatar {
background: var(--gradient-sky);
}
.message-body {
flex: 1;
}
.message-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.message-role {
font-size: 0.8rem;
font-weight: 600;
color: var(--text);
}
.message-model {
font-size: 0.7rem;
padding: 0.2rem 0.5rem;
background: var(--secondary);
color: var(--background);
border-radius: 20px;
}
.message-time {
font-size: 0.7rem;
color: var(--text-muted);
}
.message-content {
padding: 1rem 1.25rem;
border-radius: 20px;
font-size: 0.95rem;
line-height: 1.7;
}
.message.user .message-content {
background: var(--gradient-sunset);
color: var(--background);
border-bottom-right-radius: 5px;
}
.message.assistant .message-content {
background: var(--surface);
border: 1px solid var(--border);
border-bottom-left-radius: 5px;
}
.message.error .message-content {
background: rgba(255, 107, 107, 0.1);
border: 1px solid var(--danger);
color: var(--danger);
}
/* Input Area */
.input-area {
border-top: 1px solid var(--border);
padding: 1.25rem;
background: var(--background);
}
.selected-files {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.file-chip {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.4rem 0.8rem;
background: var(--gradient-sky);
color: var(--background);
border-radius: 20px;
font-size: 0.8rem;
font-weight: 500;
}
.file-chip button {
background: none;
border: none;
color: var(--background);
font-size: 1rem;
cursor: pointer;
opacity: 0.7;
line-height: 1;
}
.file-chip button:hover {
opacity: 1;
}
.input-wrapper {
display: flex;
align-items: flex-end;
gap: 0.75rem;
background: var(--surface);
border: 2px solid var(--border);
border-radius: 25px;
padding: 0.5rem;
transition: all 0.3s ease;
}
.input-wrapper:focus-within {
border-color: var(--secondary);
box-shadow: 0 0 20px rgba(78, 205, 196, 0.2);
}
.attach-btn {
width: 44px;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: none;
font-size: 1.25rem;
cursor: pointer;
border-radius: 50%;
transition: all 0.3s ease;
}
.attach-btn:hover {
background: rgba(78, 205, 196, 0.2);
}
.message-input {
flex: 1;
background: transparent;
border: none;
color: var(--text);
font-size: 1rem;
resize: none;
min-height: 44px;
max-height: 150px;
padding: 0.75rem 0;
}
.message-input:focus {
outline: none;
}
.message-input::placeholder {
color: var(--text-muted);
}
.send-btn {
width: 44px;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
background: var(--gradient-sunset);
border: none;
border-radius: 50%;
font-size: 1.25rem;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(255, 107, 107, 0.4);
}
.send-btn:hover:not(:disabled) {
transform: scale(1.1);
box-shadow: 0 6px 20px rgba(255, 107, 107, 0.5);
}
.send-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
.sending-dots {
display: flex;
gap: 4px;
}
.sending-dots span {
width: 6px;
height: 6px;
background: white;
border-radius: 50%;
animation: bounce 1.4s ease-in-out infinite;
}
.sending-dots span:nth-child(1) { animation-delay: 0s; }
.sending-dots span:nth-child(2) { animation-delay: 0.2s; }
.sending-dots span:nth-child(3) { animation-delay: 0.4s; }
@keyframes bounce {
0%, 80%, 100% { transform: translateY(0); }
40% { transform: translateY(-8px); }
}
@media (max-width: 768px) {
.chat-page {
flex-direction: column;
height: auto;
min-height: calc(100vh - var(--header-height) - 2rem);
}
.chat-sidebar {
width: 100%;
flex-direction: row;
overflow-x: auto;
}
.sidebar-section {
min-width: 200px;
}
.message {
max-width: 95%;
}
}
</style>

View File

@ -1,357 +0,0 @@
<script>
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { api } from '$lib/api';
import { getUser, initializeAuth } from '$lib/stores.svelte';
let email = $state('');
let password = $state('');
let error = $state('');
let loading = $state(false);
let user = $derived(getUser());
onMount(async () => {
await initializeAuth();
if (user) {
goto('/chat');
}
});
async function handleLogin(e) {
e.preventDefault();
error = '';
loading = true;
try {
await api.login(email, password);
goto('/chat');
} catch (e) {
error = e.message || 'Login failed. Please try again.';
} finally {
loading = false;
}
}
</script>
<div class="auth-page">
<!-- Floating decorations -->
<div class="balloon b1"></div>
<div class="balloon b2"></div>
<div class="balloon b3"></div>
<div class="node n1"></div>
<div class="node n2"></div>
<div class="node n3"></div>
<div class="node n4"></div>
<div class="node n5"></div>
<div class="auth-card">
<div class="auth-header">
<div class="logo-wrapper">
<img src="/logo.jpg" alt="Moxiegen" class="auth-logo" />
<div class="logo-glow"></div>
</div>
<h1>Welcome Back</h1>
<p>Sign in to your account to continue</p>
</div>
<form onsubmit={handleLogin} class="auth-form">
{#if error}
<div class="error-message">
<span class="error-icon">⚠️</span>
{error}
</div>
{/if}
<div class="form-group">
<label class="label" for="email">Email</label>
<input
type="email"
id="email"
class="input"
placeholder="Enter your email"
bind:value={email}
required
/>
</div>
<div class="form-group">
<label class="label" for="password">Password</label>
<input
type="password"
id="password"
class="input"
placeholder="Enter your password"
bind:value={password}
required
/>
</div>
<button type="submit" class="btn btn-primary btn-full" disabled={loading}>
{#if loading}
<span class="loading-dots">
<span></span><span></span><span></span>
</span>
{:else}
🚀 Sign In
{/if}
</button>
</form>
<div class="auth-footer">
<p>Don't have an account? <a href="/register">Create one</a></p>
</div>
<div class="demo-credentials">
<div class="demo-header">🎯 Demo Admin Credentials</div>
<div class="demo-row">
<span class="demo-label">Email:</span>
<span class="demo-value">admin@moxiegen.com</span>
</div>
<div class="demo-row">
<span class="demo-label">Password:</span>
<span class="demo-value">admin123</span>
</div>
</div>
</div>
</div>
<style>
.auth-page {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 1rem;
position: relative;
overflow: hidden;
}
/* Floating Balloons */
.balloon {
position: absolute;
border-radius: 50% 50% 50% 50%;
opacity: 0.15;
animation: float 8s ease-in-out infinite;
}
.b1 {
width: 200px;
height: 250px;
top: 10%;
left: 5%;
background: linear-gradient(135deg, #FF6B6B, #FCBAD3);
animation-delay: 0s;
}
.b2 {
width: 150px;
height: 180px;
top: 60%;
right: 10%;
background: linear-gradient(135deg, #4ECDC4, #95E1D3);
animation-delay: 2s;
}
.b3 {
width: 100px;
height: 120px;
bottom: 10%;
left: 15%;
background: linear-gradient(135deg, #FFE66D, #FF9F43);
animation-delay: 4s;
}
/* Neural Network Nodes */
.node {
position: absolute;
width: 20px;
height: 20px;
border-radius: 50%;
opacity: 0.3;
animation: pulse-glow 3s ease-in-out infinite;
}
.n1 { top: 20%; left: 20%; background: #FF6B6B; animation-delay: 0s; }
.n2 { top: 40%; right: 15%; background: #4ECDC4; animation-delay: 0.5s; }
.n3 { top: 70%; left: 10%; background: #FFE66D; animation-delay: 1s; }
.n4 { bottom: 20%; right: 25%; background: #AA96DA; animation-delay: 1.5s; }
.n5 { top: 30%; right: 30%; background: #FCBAD3; animation-delay: 2s; }
.auth-card {
width: 100%;
max-width: 420px;
background: linear-gradient(135deg, rgba(15, 52, 96, 0.9), rgba(22, 33, 62, 0.95));
border: 2px solid transparent;
border-image: var(--gradient-sunset) 1;
border-radius: 30px;
padding: 2.5rem;
backdrop-filter: blur(20px);
box-shadow:
0 25px 50px rgba(0, 0, 0, 0.4),
0 0 100px rgba(78, 205, 196, 0.1);
position: relative;
z-index: 1;
}
.auth-header {
text-align: center;
margin-bottom: 2rem;
}
.logo-wrapper {
position: relative;
display: inline-block;
margin-bottom: 1.5rem;
}
.auth-logo {
width: 80px;
height: 80px;
border-radius: 20px;
object-fit: cover;
position: relative;
z-index: 1;
transition: transform 0.3s ease;
box-shadow: 0 10px 30px rgba(255, 107, 107, 0.4);
}
.auth-logo:hover {
transform: scale(1.1) rotate(5deg);
}
.logo-glow {
position: absolute;
inset: -10px;
background: var(--gradient-sunset);
border-radius: 30px;
opacity: 0.3;
filter: blur(20px);
animation: pulse-glow 2s ease-in-out infinite;
}
.auth-header h1 {
font-size: 2rem;
font-weight: 800;
background: var(--gradient-sunset);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 0.5rem;
}
.auth-header p {
color: var(--text-muted);
font-size: 1rem;
}
.auth-form {
margin-bottom: 1.5rem;
}
.error-message {
display: flex;
align-items: center;
gap: 0.75rem;
background: rgba(255, 107, 107, 0.1);
border: 1px solid var(--danger);
color: var(--danger);
padding: 1rem;
border-radius: 15px;
margin-bottom: 1rem;
font-size: 0.9rem;
}
.error-icon {
font-size: 1.25rem;
}
.btn-full {
width: 100%;
margin-top: 1.5rem;
padding: 1rem 1.5rem;
font-size: 1rem;
}
.loading-dots {
display: flex;
gap: 0.5rem;
}
.loading-dots span {
width: 8px;
height: 8px;
background: currentColor;
border-radius: 50%;
animation: bounce 1.4s ease-in-out infinite;
}
.loading-dots span:nth-child(1) { animation-delay: 0s; }
.loading-dots span:nth-child(2) { animation-delay: 0.2s; }
.loading-dots span:nth-child(3) { animation-delay: 0.4s; }
@keyframes bounce {
0%, 80%, 100% { transform: translateY(0); }
40% { transform: translateY(-10px); }
}
.auth-footer {
text-align: center;
margin-bottom: 1.5rem;
}
.auth-footer p {
color: var(--text-muted);
font-size: 0.9rem;
}
.auth-footer a {
color: var(--secondary);
font-weight: 600;
}
.auth-footer a:hover {
color: var(--accent-1);
}
.demo-credentials {
background: linear-gradient(135deg, rgba(78, 205, 196, 0.1), rgba(149, 225, 211, 0.1));
border: 1px solid var(--secondary);
border-radius: 15px;
padding: 1rem 1.25rem;
}
.demo-header {
font-weight: 700;
color: var(--secondary);
margin-bottom: 0.75rem;
font-size: 0.85rem;
}
.demo-row {
display: flex;
justify-content: space-between;
padding: 0.25rem 0;
font-size: 0.85rem;
}
.demo-label {
color: var(--text-muted);
}
.demo-value {
color: var(--accent-1);
font-family: monospace;
font-weight: 600;
}
@media (max-width: 480px) {
.auth-card {
padding: 1.5rem;
}
.auth-header h1 {
font-size: 1.5rem;
}
}
</style>

View File

@ -1,188 +0,0 @@
<script>
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { api } from '$lib/api';
import { getUser, initializeAuth } from '$lib/stores.svelte';
let email = $state('');
let username = $state('');
let password = $state('');
let confirmPassword = $state('');
let error = $state('');
let loading = $state(false);
let user = $derived(getUser());
onMount(async () => {
await initializeAuth();
if (user) {
goto('/chat');
}
});
async function handleRegister(e) {
e.preventDefault();
error = '';
if (password !== confirmPassword) {
error = 'Passwords do not match';
return;
}
if (password.length < 6) {
error = 'Password must be at least 6 characters';
return;
}
loading = true;
try {
await api.register(email, username, password);
await api.login(email, password);
goto('/chat');
} catch (e) {
error = e.message || 'Registration failed. Please try again.';
} finally {
loading = false;
}
}
</script>
<div class="auth-page">
<div class="auth-card">
<div class="auth-header">
<img src="/logo.jpg" alt="Moxiegen" class="auth-logo" />
<h1>Create Account</h1>
<p>Sign up to start using Moxiegen</p>
</div>
<form onsubmit={handleRegister} class="auth-form">
{#if error}
<div class="error-message">{error}</div>
{/if}
<div class="form-group">
<label class="label" for="email">Email</label>
<input
type="email"
id="email"
class="input"
placeholder="Enter your email"
bind:value={email}
required
/>
</div>
<div class="form-group">
<label class="label" for="username">Username</label>
<input
type="text"
id="username"
class="input"
placeholder="Choose a username"
bind:value={username}
required
/>
</div>
<div class="form-group">
<label class="label" for="password">Password</label>
<input
type="password"
id="password"
class="input"
placeholder="Create a password"
bind:value={password}
required
/>
</div>
<div class="form-group">
<label class="label" for="confirmPassword">Confirm Password</label>
<input
type="password"
id="confirmPassword"
class="input"
placeholder="Confirm your password"
bind:value={confirmPassword}
required
/>
</div>
<button type="submit" class="btn btn-primary btn-full" disabled={loading}>
{loading ? 'Creating account...' : 'Create Account'}
</button>
</form>
<div class="auth-footer">
<p>Already have an account? <a href="/login">Sign in</a></p>
</div>
</div>
</div>
<style>
.auth-page {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 1rem;
background: var(--background);
}
.auth-card {
width: 100%;
max-width: 400px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 1rem;
padding: 2rem;
}
.auth-header {
text-align: center;
margin-bottom: 2rem;
}
.auth-logo {
width: 64px;
height: 64px;
border-radius: 0.75rem;
margin-bottom: 1rem;
}
.auth-header h1 {
font-size: 1.5rem;
margin-bottom: 0.5rem;
}
.auth-header p {
color: var(--text-muted);
font-size: 0.875rem;
}
.auth-form {
margin-bottom: 1.5rem;
}
.btn-full {
width: 100%;
margin-top: 1rem;
}
.error-message {
background: rgba(239, 68, 68, 0.1);
border: 1px solid var(--danger);
color: var(--danger);
padding: 0.75rem 1rem;
border-radius: 0.5rem;
margin-bottom: 1rem;
font-size: 0.875rem;
}
.auth-footer {
text-align: center;
color: var(--text-muted);
font-size: 0.875rem;
}
</style>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

View File

@ -1,10 +0,0 @@
import adapter from '@sveltejs/adapter-auto';
/** @type {import('@sveltejs/kit').Config} */
const config = {
kit: {
adapter: adapter()
}
};
export default config;

View File

@ -1,6 +0,0 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [sveltekit()]
});