diff --git a/.env.example b/.env.example index eb33d4ef..64b0027c 100644 --- a/.env.example +++ b/.env.example @@ -12,4 +12,5 @@ PROXY_URL=your_proxy_url POSTGRES_PORT=placeholder POSTGRES_DB=placeholder POSTGRES_USER=placeholder -POSTGRES_PASSWORD=placeholder \ No newline at end of file +POSTGRES_PASSWORD=placeholder +SECRET_KEY='your_secret_key' diff --git a/API.md b/API.md index 5d7155d3..5a5bd380 100644 --- a/API.md +++ b/API.md @@ -7,6 +7,78 @@ For full API documentation, see [localhost:8100/docs](localhost:8100/docs) after If you want to see the API docs before deployment, check out the [hosted docs here](https://opengpts-example-vz4y4ooboq-uc.a.run.app/docs). +## Register a New User + +To register a new user, you can use the following API endpoint: + +```python +import requests + +response = requests.post('http://127.0.0.1:8100/users/register', json={ + "username": "example_user", + "password_hash": "example_password_hash", + "email": "user@example.com", + "full_name": "Example User", + "address": "123 Example St, City", +}).content +``` + +## Login User + +To login with a new user, you can use the following API endpoint + +```python +import requests +response = requests.post('http://127.0.0.1:8100/users/login', json={ + "username": "example_user", + "password_hash": "example_password_hash" +}).content +``` +## Get All Active User +To Fetch all the active User, you can use the following API endpoint:- + +```python +import requests + +response = requests.get('http://127.0.0.1:8100/users') +``` + +## Get User by ID +To Fetch the User by ID, you can use the following API endpoint:- +Replace {user_id} with the actual ID of the user you want to delete. + +```python +import requests + +response = requests.get('http://127.0.0.1:8100/users/{user_id}') +``` + +## Update User by ID +To Update the User, you can use the following API endpoint:- +Replace {user_id} with the actual ID of the user you want to delete. + +```python +import requests + +response = requests.put('http://127.0.0.1:8100/users/{user_id}', json={ + "username": "new_username", + "password_hash": "new_password_hash", + "email": "new_email@example.com", + "full_name": "New Name", + "address": "456 New St, City", +}).content +``` + +## Delete User by ID +To delete a user by user ID, you can use the following API endpoint: +Replace {user_id} with the actual ID of the user you want to delete. +```python +import requests + +response = requests.delete('http://127.0.0.1:8100/users/{user_id}') + +``` + ## Create an Assistant First, let's use the API to create an assistant. diff --git a/backend/app/api/__init__.py b/backend/app/api/__init__.py index 47d463e5..45a91b7c 100644 --- a/backend/app/api/__init__.py +++ b/backend/app/api/__init__.py @@ -3,7 +3,7 @@ from app.api.assistants import router as assistants_router from app.api.runs import router as runs_router from app.api.threads import router as threads_router - +from app.api.users import router as users_router router = APIRouter() @@ -27,3 +27,9 @@ async def ok(): prefix="/threads", tags=["threads"], ) + +router.include_router( + users_router, + prefix="/users", + tags=["users"], +) \ No newline at end of file diff --git a/backend/app/api/assistants.py b/backend/app/api/assistants.py index 1667c5f4..20344ef1 100644 --- a/backend/app/api/assistants.py +++ b/backend/app/api/assistants.py @@ -1,7 +1,8 @@ from typing import Annotated, List, Optional from uuid import uuid4 -from fastapi import APIRouter, HTTPException, Path, Query +from app.api.security import verify_token +from fastapi import APIRouter, HTTPException, Path, Query, Depends from pydantic import BaseModel, Field import app.storage as storage @@ -9,6 +10,7 @@ router = APIRouter() + FEATURED_PUBLIC_ASSISTANTS = [] diff --git a/backend/app/api/runs.py b/backend/app/api/runs.py index 70a72968..dc0cb578 100644 --- a/backend/app/api/runs.py +++ b/backend/app/api/runs.py @@ -1,8 +1,9 @@ import json from typing import Optional, Sequence +from app.api.security import verify_token import langsmith.client -from fastapi import APIRouter, BackgroundTasks, HTTPException, Request +from fastapi import APIRouter, BackgroundTasks, HTTPException, Request, Depends from fastapi.exceptions import RequestValidationError from langchain.pydantic_v1 import ValidationError from langchain_core.messages import AnyMessage diff --git a/backend/app/api/security.py b/backend/app/api/security.py new file mode 100644 index 00000000..05961d80 --- /dev/null +++ b/backend/app/api/security.py @@ -0,0 +1,30 @@ +import datetime +import os +import jwt + +from fastapi import HTTPException + +# Get the secret key from environment variable +SECRET_KEY = os.environ.get("SECRET_KEY") +if not SECRET_KEY: + raise ValueError("SECRET_KEY not set on your environment.") + +# Token expiration time (1 hour) +TOKEN_EXPIRATION = datetime.timedelta(hours=1) + +def create_token(username: str) -> str: + """Create JWT token.""" + payload = { + 'username': username, + 'exp': datetime.datetime.utcnow() + TOKEN_EXPIRATION + } + token = jwt.encode(payload, SECRET_KEY, algorithm="HS256") + return token + +def verify_token(token: str) -> dict: + """Verify JWT token and return payload.""" + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"]) + return payload + except jwt.PyJWTError: + raise HTTPException(status_code=403, detail="Could not validate credentials") diff --git a/backend/app/api/threads.py b/backend/app/api/threads.py index 31fe6584..415a1675 100644 --- a/backend/app/api/threads.py +++ b/backend/app/api/threads.py @@ -1,7 +1,8 @@ from typing import Annotated, List, Sequence from uuid import uuid4 -from fastapi import APIRouter, HTTPException, Path +from app.api.security import verify_token +from fastapi import APIRouter, HTTPException, Path, Depends from langchain.schema.messages import AnyMessage from pydantic import BaseModel, Field diff --git a/backend/app/api/users.py b/backend/app/api/users.py new file mode 100644 index 00000000..195932de --- /dev/null +++ b/backend/app/api/users.py @@ -0,0 +1,92 @@ +from typing import Optional, List + +from app.api.security import create_token, verify_token +from fastapi import APIRouter, HTTPException, Depends +from pydantic import BaseModel, Field + +import app.storage as storage +from app.schema import User + +router = APIRouter() + +class UserID(str): + """Type annotation for user ID.""" + +class UserRegisterRequest(BaseModel): + """Payload for registering a new user.""" + username: str = Field(..., description="The username of the user.") + password_hash: str = Field(..., description="The hashed password of the user.") + email: str = Field(..., description="The email of the user.") + full_name: str = Field(..., description="The full name of the user.") + address: str = Field(..., description="The address of the user.") + +class UserLoginRequest(BaseModel): + """Payload for logging in a user.""" + username: str = Field(..., description="The username of the user.") + password_hash: str = Field(..., description="The hashed password of the user.") + +class UserResponse(BaseModel): + """Response model for registering a new user.""" + token: str + message: str + + +@router.post("/register", response_model=UserResponse, status_code=201) +async def register_user(user_register_request: UserRegisterRequest) -> UserResponse: + """Register a new user.""" + user = await storage.register_user(**user_register_request.dict()) + if user: + # Generate token + token = create_token(user.username) + return {'token': token, "message": "Register successful"} + else: + raise HTTPException(status_code=401, detail="Invalid username or password") + +@router.post("/login", response_model=Optional[UserResponse]) +async def login_user(user_login_request: UserLoginRequest) -> Optional[UserResponse]: + """Login a user.""" + user = await storage.login_user(**user_login_request.dict()) + if user: + # Generate token + token = create_token(user.username) + return {"token": token, "message": 'Login successful'} + else: + raise HTTPException(status_code=401, detail="Invalid username or password") + +@router.post("/logout") +async def logout_user(): + # You may add additional logic here if needed, such as invalidating sessions, etc. + return {"message": "Logout successful"} + +@router.get("/{user_id}", response_model=User) +async def get_user_by_id(user_id: UserID) -> User: + """Get a user by ID.""" + user = await storage.get_user(user_id) + if not user: + raise HTTPException(status_code=404, detail="User not found") + return user + +@router.get("/", response_model=List[User], status_code=200) +async def list_active_users(): + """List all active users.""" + users = await storage.list_active_users() + if users: + return users + else: + raise HTTPException(status_code=404, detail="No active users found") + + +@router.put("/{user_id}", response_model=User) +async def update_user_by_id(user_id: UserID, user_update_request: UserRegisterRequest) -> User: + """Update a user by ID.""" + user = await storage.update_user(user_id, **user_update_request.dict()) + if not user: + raise HTTPException(status_code=404, detail="User not found") + return user + +@router.delete("/{user_id}", status_code=204) +async def delete_user_by_id(user_id: UserID): + """Delete a user by ID.""" + deleted = await storage.delete_user(user_id) + if not deleted: + raise HTTPException(status_code=404, detail="User not found") diff --git a/backend/app/schema.py b/backend/app/schema.py index 0c0e5923..af2081c7 100644 --- a/backend/app/schema.py +++ b/backend/app/schema.py @@ -1,10 +1,12 @@ from datetime import datetime from typing import Annotated, Optional from uuid import UUID - from fastapi import Cookie from typing_extensions import TypedDict +from pydantic import BaseModel +from datetime import datetime + class Assistant(TypedDict): """Assistant model.""" @@ -35,6 +37,30 @@ class Thread(TypedDict): updated_at: datetime """The last time the thread was updated.""" +class User(BaseModel): + """User model""" + + user_id: UUID + """The ID of the user.""" + username: str + """The username of the user.""" + password_hash: str + """The hashed password of the user.""" + email: str + """The email address of the user.""" + full_name: str + """The full name of the user.""" + address: str + """The address of the user.""" + creation_date: datetime + """The date and time when the user account was created.""" + last_login_date: Optional[datetime] = None + """The date and time when the user last logged in. Can be None initially.""" + is_active: bool + """Boolean flag indicating whether the user account is active.""" + is_deleted: bool = False + """indicate if the user is deleted""" + OpengptsUserId = Annotated[ str, diff --git a/backend/app/server.py b/backend/app/server.py index 3213cf2f..3576fb47 100644 --- a/backend/app/server.py +++ b/backend/app/server.py @@ -5,6 +5,7 @@ import orjson from fastapi import FastAPI, Form, UploadFile from fastapi.staticfiles import StaticFiles +from fastapi.middleware.cors import CORSMiddleware from app.api import router as api_router from app.lifespan import lifespan @@ -14,6 +15,19 @@ app = FastAPI(title="OpenGPTs API", lifespan=lifespan) +# CORS settings +origins = [ + "http://localhost:5173", # Add additional URLs if needed +] + +app.add_middleware( + CORSMiddleware, + allow_origins=origins, + allow_credentials=True, + allow_methods=["GET", "POST", "PUT", "DELETE"], + allow_headers=["Authorization", "Content-Type"], +) + # Get root of app, used to point to directory containing static files ROOT = Path(__file__).parent.parent diff --git a/backend/app/storage.py b/backend/app/storage.py index bd87bfaa..20df81ba 100644 --- a/backend/app/storage.py +++ b/backend/app/storage.py @@ -1,11 +1,12 @@ +import uuid from datetime import datetime, timezone from typing import List, Optional, Sequence - +from fastapi import HTTPException, status from langchain_core.messages import AnyMessage from app.agent import AgentType, get_agent_executor from app.lifespan import get_pg_pool -from app.schema import Assistant, Thread +from app.schema import Assistant, Thread, User from app.stream import map_chunk_to_msg @@ -161,3 +162,140 @@ async def put_thread( "name": name, "updated_at": updated_at, } + + +async def get_user(user_id: str) -> Optional[User]: + """Get a user by ID.""" + async with get_pg_pool().acquire() as conn: + return await conn.fetchrow( + "SELECT * FROM \"user\" WHERE user_id = $1 AND is_deleted = FALSE", + user_id, + ) + + +async def list_active_users() -> List[User]: + """List all active users.""" + async with get_pg_pool().acquire() as conn: + return await conn.fetch( + "SELECT * FROM \"user\" WHERE is_active = TRUE AND is_deleted = FALSE" + ) + + +async def register_user( + username: str, + password_hash: str, + email: str, + full_name: str, + address: str, +) -> User: + """Register a new user.""" + user_id = uuid.uuid4() # Generate a unique user ID + creation_date = datetime.now(timezone.utc) + last_login_date = None # Assuming no login has occurred yet + is_active = True + + async with get_pg_pool().acquire() as conn: + try: + async with conn.transaction(): + await conn.execute( + "INSERT INTO \"user\" (username, password_hash, email, full_name, address, creation_date, last_login_date, is_active, is_deleted) " + "VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + username, password_hash, email, full_name, address, + creation_date, last_login_date, is_active, False + ) + return User( + user_id=user_id, + username=username, + password_hash=password_hash, + email=email, + full_name=full_name, + address=address, + creation_date=creation_date, + last_login_date=last_login_date, + is_active=is_active + ) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to register user {e}", + ) + +async def login_user( + username: str, + password_hash: str +) -> Optional[User]: + async with get_pg_pool().acquire() as conn: + try: + user_record = await conn.fetchrow( + "SELECT * FROM \"user\" WHERE username = $1 AND password_hash = $2 AND is_deleted = FALSE", + username, password_hash + ) + + if user_record is not None: + last_login_date = datetime.now(timezone.utc) + await conn.execute( + "UPDATE \"user\" SET last_login_date = $1 WHERE username = $2", + last_login_date, username + ) + return User( + user_id=user_record['user_id'], + username=user_record['username'], + password_hash=user_record['password_hash'], + email=user_record['email'], + full_name=user_record['full_name'], + address=user_record['address'], + creation_date=user_record['creation_date'], + last_login_date=last_login_date, + is_active=user_record['is_active'] + ) + else: + return None + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to login user {e}", + ) + + +async def update_user( + user_id: str, + username: str, + password_hash: str, + email: str, + full_name: str, + address: str, +) -> Optional[User]: + """Update a user.""" + async with get_pg_pool().acquire() as conn: + try: + async with conn.transaction(): + await conn.execute( + "UPDATE \"user\" SET username = $1, password_hash = $2, email = $3, full_name = $4, address = $5, WHERE user_id = $6 AND is_deleted = FALSE", + username, password_hash, email, full_name, address, user_id + ) + # Retrieve the updated user to return + updated_user = await get_user(user_id) + return updated_user + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to update user", + ) + +async def delete_user(user_id: str) -> bool: + """Soft delete a user.""" + async with get_pg_pool().acquire() as conn: + try: + async with conn.transaction(): + result = await conn.execute( + "UPDATE \"user\" SET is_deleted = TRUE WHERE user_id = $1 AND is_deleted = FALSE", + user_id + ) + # Check if a row was affected + return result == 'UPDATE 1' + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to delete user", + ) + diff --git a/backend/migrations/000003_create_user_table.up.sql b/backend/migrations/000003_create_user_table.up.sql new file mode 100644 index 00000000..7b406a88 --- /dev/null +++ b/backend/migrations/000003_create_user_table.up.sql @@ -0,0 +1,13 @@ +CREATE TABLE IF NOT EXISTS "user" ( + user_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + username VARCHAR(255) UNIQUE NOT NULL, + password_hash VARCHAR(255) NOT NULL, + email VARCHAR(255) UNIQUE NOT NULL, + full_name VARCHAR(255) NOT NULL, + address VARCHAR(255) NOT NULL, + role VARCHAR(50) NOT NULL, + creation_date TIMESTAMP WITH TIME ZONE DEFAULT (CURRENT_TIMESTAMP AT TIME ZONE 'UTC'), + last_login_date TIMESTAMP WITH TIME ZONE, + is_active BOOLEAN NOT NULL DEFAULT TRUE, + is_deleted BOOLEAN NOT NULL DEFAULT FALSE +); diff --git a/backend/migrations/000004_update_user_table.up.sql b/backend/migrations/000004_update_user_table.up.sql new file mode 100644 index 00000000..ea1cb3bc --- /dev/null +++ b/backend/migrations/000004_update_user_table.up.sql @@ -0,0 +1,13 @@ + +-- Update the column definition to allow NULL values for last_login_date +ALTER TABLE "user" +ALTER COLUMN last_login_date DROP NOT NULL; + +-- Set default values for existing rows where last_login_date is NULL +UPDATE "user" +SET last_login_date = CURRENT_TIMESTAMP +WHERE last_login_date IS NULL; + +-- Modify the last_login_date column to use CURRENT_TIMESTAMP as the default value +ALTER TABLE "user" +ALTER COLUMN last_login_date SET DEFAULT CURRENT_TIMESTAMP; \ No newline at end of file diff --git a/backend/migrations/000005_update_user_table1.down.sql b/backend/migrations/000005_update_user_table1.down.sql new file mode 100644 index 00000000..935c7764 --- /dev/null +++ b/backend/migrations/000005_update_user_table1.down.sql @@ -0,0 +1,2 @@ +-- Drop the role column from the user table +ALTER TABLE "user" DROP COLUMN role; \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index cc40c377..0b4bcd34 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -25,7 +25,10 @@ "react-dom": "^18.2.0", "react-dropzone": "^14.2.3", "tailwind-merge": "^2.0.0", - "uuid": "^9.0.1" + "uuid": "^9.0.1", + "react-router-dom": "^6.22.3", + "@material-ui/core": "^4.12.3", + "react-spring": "^9.7.3" }, "devDependencies": { "@types/dompurify": "^3.0.4", diff --git a/frontend/src/App.css b/frontend/src/App.css new file mode 100644 index 00000000..274456b7 --- /dev/null +++ b/frontend/src/App.css @@ -0,0 +1,170 @@ +/* Reset styles */ +html, +body, +body * { + box-sizing: border-box; + font-family: "Open Sans", sans-serif; + margin: 0; + padding: 0; + color: inherit; + background-color: inherit; + /* border:2px solid orange; */ +} + +#loginBtn{ + margin-right: 10px; +} + +.style { + /* background-color: red; */ + background-image: url("ai.jpg"); + +} +body { + /* background-image: url(pic3.jpeg); */ + background-size: cover; + background-position: center center; + height: min-content; + +} +.container { + width: 100%; + padding-top: 60px; + padding-bottom: 100px; +} + +.login-register-wrapper { + height: 684px; + width: 500px; + margin-left: auto; + margin-right: auto; + padding: 0 2em; + border-radius: 5px; + box-shadow: 0px 2px 7px rgba(0, 0, 0, 0.2); + overflow: hidden; + color: #ffffff; + background: linear-gradient( + 180deg, + rgba(19, 38, 79, 1) 0%, + rgba(22, 147, 198, 1) 52%, + rgba(255, 255, 255, 0) 82% + ); +} + +/* Nav buttons */ +.nav-buttons { + width: 100%; + height: 100px; + padding-top: 40px; + opacity: 1; + margin-bottom: 3em; +} +button { + font-size: 1.5em; + margin-right: 1em; + border: 0px; + cursor: pointer; + +} +button:focus { + outline: 0px; +} +button.active { + padding-bottom: 10px; + border-bottom: solid 2px #1059ff; +} + + + +/* footer: forgot section */ +.forgot-panel { + position: relative; + height: 100px; + margin-left: auto; + margin-right: auto; + text-align: center; + padding-top: 24px; + border-top: solid 1px rgba(255, 255, 255, 0.3); +} + +/* form styling */ +.form-group input { + width: 90%; + height: 35px; + padding-left: 15px; + border: none; + border-radius: 5px; + margin-bottom: 20px; + background: rgba(255, 255, 255, 0.5); +} +.form-group label { + text-transform: uppercase; +} + +.form-group select { + width: 100%; + height: 35px; + padding-left: 15px; + border: none; + border-radius: 5px; + margin-bottom: 20px; + background: rgba(255, 255, 255, 0.5); + color: #000; /* Set the text color for the select options */ +} + +input.submit { + font-weight: 700; + text-transform: uppercase; + font-size: 1em; + text-align: center; + color: rgba(255, 255, 255, 1); + padding: 8px 0px; + width: 60%; + height: 35px; + border: none; + border-radius: 20px; + margin-top: 23px; + margin-left: 60px; + background-color: rgba(16, 89, 255, 1); +} + +/*Form positioning and animation */ + +form#registerform { + left: 500px; + top: -280px; + position: relative; +} + +.forgot-panel { + margin-top: -300px; + color: gray; +} + +form, +.forgot-panel { + position: relative; + transition: all 0.5s ease; +} +#loginBtn, +#registerBtn { + transition: all 0.5s ease; +} + +/* Positioning and animation for select field */ +form#registerform select { + /* Adjust the positioning as needed */ + width: calc(96% - 30px); + margin-right: 5px; +} + +/* Add a margin-top to create space between the select field and the following input field */ +.form-group select + input { + margin-top: 10px; +} + +/* Add styles for the options in the select field */ +.form-group select option { + background-color: #fff; + color: #000; +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index df449b19..ea7c27de 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,196 +1,25 @@ -import { useCallback, useEffect, useMemo, useState } from "react"; -import { InformationCircleIcon } from "@heroicons/react/24/outline"; -import { Chat } from "./components/Chat"; -import { ChatList } from "./components/ChatList"; -import { Layout } from "./components/Layout"; -import { NewChat } from "./components/NewChat"; -import { Chat as ChatType, useChatList } from "./hooks/useChatList"; -import { useSchemas } from "./hooks/useSchemas"; -import { useStreamState } from "./hooks/useStreamState"; -import { useConfigList } from "./hooks/useConfigList"; -import { Config } from "./components/Config"; -import { MessageWithFiles } from "./utils/formTypes.ts"; -import { TYPE_NAME } from "./constants.ts"; - -function App() { - const [sidebarOpen, setSidebarOpen] = useState(false); - const { configSchema, configDefaults } = useSchemas(); - const { chats, currentChat, createChat, enterChat } = useChatList(); - const { configs, currentConfig, saveConfig, enterConfig } = useConfigList(); - const { startStream, stopStream, stream } = useStreamState(); - const [isDocumentRetrievalActive, setIsDocumentRetrievalActive] = - useState(false); - - useEffect(() => { - let configurable = null; - if (currentConfig) { - configurable = currentConfig?.config?.configurable; - } - if (currentChat && configs) { - const conf = configs.find( - (c) => c.assistant_id === currentChat.assistant_id, - ); - configurable = conf?.config?.configurable; - } - const agent_type = configurable?.["type"] as TYPE_NAME | null; - if (agent_type === null || agent_type === "chatbot") { - setIsDocumentRetrievalActive(false); - return; - } - if (agent_type === "chat_retrieval") { - setIsDocumentRetrievalActive(true); - return; - } - const tools = - (configurable?.["type==agent/tools"] as { name: string }[]) ?? []; - setIsDocumentRetrievalActive(tools.some((t) => t.name === "Retrieval")); - }, [currentConfig, currentChat, configs]); - - const startTurn = useCallback( - async (message?: MessageWithFiles, chat: ChatType | null = currentChat) => { - if (!chat) return; - const config = configs?.find( - (c) => c.assistant_id === chat.assistant_id, - )?.config; - if (!config) return; - const files = message?.files || []; - if (files.length > 0) { - const formData = files.reduce((formData, file) => { - formData.append("files", file); - return formData; - }, new FormData()); - formData.append( - "config", - JSON.stringify({ configurable: { thread_id: chat.thread_id } }), - ); - await fetch(`/ingest`, { - method: "POST", - body: formData, - }); - } - await startStream( - message - ? [ - { - content: message.message, - additional_kwargs: {}, - type: "human", - example: false, - }, - ] - : null, - chat.assistant_id, - chat.thread_id, - ); - }, - [currentChat, startStream, configs], - ); - - const startChat = useCallback( - async (message: MessageWithFiles) => { - if (!currentConfig) return; - const chat = await createChat( - message.message, - currentConfig.assistant_id, - ); - return startTurn(message, chat); - }, - [createChat, startTurn, currentConfig], - ); - - const selectChat = useCallback( - async (id: string | null) => { - if (currentChat) { - stopStream?.(true); - } - enterChat(id); - if (!id) { - enterConfig(configs?.[0]?.assistant_id ?? null); - window.scrollTo({ top: 0 }); - } - if (sidebarOpen) { - setSidebarOpen(false); - } - }, - [enterChat, stopStream, sidebarOpen, currentChat, enterConfig, configs], - ); - - const selectConfig = useCallback( - (id: string | null) => { - enterConfig(id); - enterChat(null); - }, - [enterConfig, enterChat], - ); - - const content = currentChat ? ( - - ) : currentConfig ? ( - - ) : ( - - ); - - const currentChatConfig = configs?.find( - (c) => c.assistant_id === currentChat?.assistant_id, - ); - +import React from "react"; +import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; +// import LoginPage from "./pages/LoginPage"; +// import RegisterPage from "./pages/RegisterPage"; +import Home from "./components/Home"; +import LoginSignUp from "./components/LoginSignUp"; + +const App: React.FC = () => { return ( - - {currentChatConfig.name} - { - selectConfig(currentChatConfig.assistant_id); - }} - /> - - ) : null - } - sidebarOpen={sidebarOpen} - setSidebarOpen={setSidebarOpen} - sidebar={ - { - if (configs === null || chats === null) return null; - return chats.filter((c) => - configs.some((config) => config.assistant_id === c.assistant_id), - ); - }, [chats, configs])} - currentChat={currentChat} - enterChat={selectChat} - currentConfig={currentConfig} - enterConfig={selectConfig} + + + + } /> - } - > - {configSchema ? content : null} - + } /> + {/* } /> */} + + ); -} +}; export default App; diff --git a/frontend/src/ai.jpg b/frontend/src/ai.jpg new file mode 100644 index 00000000..20d56dd3 Binary files /dev/null and b/frontend/src/ai.jpg differ diff --git a/frontend/src/components/AuthRoute.tsx b/frontend/src/components/AuthRoute.tsx new file mode 100644 index 00000000..05687b4c --- /dev/null +++ b/frontend/src/components/AuthRoute.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { Route, Redirect } from 'react-router-dom'; + +const AuthRoute = ({ component: Component, isAuthenticated, ...rest }) => ( + + isAuthenticated ? ( + + ) : ( + + ) + } + /> +); + +export default AuthRoute; +z \ No newline at end of file diff --git a/frontend/src/components/Home.tsx b/frontend/src/components/Home.tsx new file mode 100644 index 00000000..2dc3e2c0 --- /dev/null +++ b/frontend/src/components/Home.tsx @@ -0,0 +1,196 @@ +import { useCallback, useEffect, useMemo, useState } from "react"; +import { InformationCircleIcon } from "@heroicons/react/24/outline"; +import { Chat } from "./Chat"; +import { ChatList } from "./ChatList"; +import { Layout } from "./Layout"; +import { NewChat } from "./NewChat"; +import { Chat as ChatType, useChatList } from "../hooks/useChatList.ts"; +import { useSchemas } from "../hooks/useSchemas"; +import { useStreamState } from "../hooks/useStreamState"; +import { useConfigList } from "../hooks/useConfigList"; +import { Config } from "./Config"; +import { MessageWithFiles } from "../utils/formTypes.ts"; +import { TYPE_NAME } from "../constants.ts"; + +function Home() { + const [sidebarOpen, setSidebarOpen] = useState(false); + const { configSchema, configDefaults } = useSchemas(); + const { chats, currentChat, createChat, enterChat } = useChatList(); + const { configs, currentConfig, saveConfig, enterConfig } = useConfigList(); + const { startStream, stopStream, stream } = useStreamState(); + const [isDocumentRetrievalActive, setIsDocumentRetrievalActive] = + useState(false); + + useEffect(() => { + let configurable = null; + if (currentConfig) { + configurable = currentConfig?.config?.configurable; + } + if (currentChat && configs) { + const conf = configs.find( + (c) => c.assistant_id === currentChat.assistant_id, + ); + configurable = conf?.config?.configurable; + } + const agent_type = configurable?.["type"] as TYPE_NAME | null; + if (agent_type === null || agent_type === "chatbot") { + setIsDocumentRetrievalActive(false); + return; + } + if (agent_type === "chat_retrieval") { + setIsDocumentRetrievalActive(true); + return; + } + const tools = + (configurable?.["type==agent/tools"] as { name: string }[]) ?? []; + setIsDocumentRetrievalActive(tools.some((t) => t.name === "Retrieval")); + }, [currentConfig, currentChat, configs]); + + const startTurn = useCallback( + async (message?: MessageWithFiles, chat: ChatType | null = currentChat) => { + if (!chat) return; + const config = configs?.find( + (c) => c.assistant_id === chat.assistant_id, + )?.config; + if (!config) return; + const files = message?.files || []; + if (files.length > 0) { + const formData = files.reduce((formData, file) => { + formData.append("files", file); + return formData; + }, new FormData()); + formData.append( + "config", + JSON.stringify({ configurable: { thread_id: chat.thread_id } }), + ); + await fetch(`/ingest`, { + method: "POST", + body: formData, + }); + } + await startStream( + message + ? [ + { + content: message.message, + additional_kwargs: {}, + type: "human", + example: false, + }, + ] + : null, + chat.assistant_id, + chat.thread_id, + ); + }, + [currentChat, startStream, configs], + ); + + const startChat = useCallback( + async (message: MessageWithFiles) => { + if (!currentConfig) return; + const chat = await createChat( + message.message, + currentConfig.assistant_id, + ); + return startTurn(message, chat); + }, + [createChat, startTurn, currentConfig], + ); + + const selectChat = useCallback( + async (id: string | null) => { + if (currentChat) { + stopStream?.(true); + } + enterChat(id); + if (!id) { + enterConfig(configs?.[0]?.assistant_id ?? null); + window.scrollTo({ top: 0 }); + } + if (sidebarOpen) { + setSidebarOpen(false); + } + }, + [enterChat, stopStream, sidebarOpen, currentChat, enterConfig, configs], + ); + + const selectConfig = useCallback( + (id: string | null) => { + enterConfig(id); + enterChat(null); + }, + [enterConfig, enterChat], + ); + + const content = currentChat ? ( + + ) : currentConfig ? ( + + ) : ( + + ); + + const currentChatConfig = configs?.find( + (c) => c.assistant_id === currentChat?.assistant_id, + ); + + return ( + + {currentChatConfig.name} + { + selectConfig(currentChatConfig.assistant_id); + }} + /> + + ) : null + } + sidebarOpen={sidebarOpen} + setSidebarOpen={setSidebarOpen} + sidebar={ + { + if (configs === null || chats === null) return null; + return chats.filter((c) => + configs.some((config) => config.assistant_id === c.assistant_id), + ); + }, [chats, configs])} + currentChat={currentChat} + enterChat={selectChat} + currentConfig={currentConfig} + enterConfig={selectConfig} + /> + } + > + {configSchema ? content : null} + + ); +} + +export default Home; diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index 551527a1..91b0849c 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -1,6 +1,8 @@ -import { Fragment } from "react"; +import { Fragment , useEffect} from "react"; import { Dialog, Transition } from "@headlessui/react"; import { Bars3Icon, XMarkIcon } from "@heroicons/react/24/outline"; +import { useNavigate } from 'react-router-dom'; // Import useHistory hook from React Router + export function Layout(props: { sidebarOpen: boolean; @@ -9,6 +11,46 @@ export function Layout(props: { children: React.ReactNode; subtitle?: React.ReactNode; }) { + const navigate = useNavigate(); // Initialize useHistory hook + const authToken = sessionStorage.getItem('authToken'); + + useEffect(() => { + if (!sessionStorage.getItem('authToken')) { + navigate("/"); + }},[]) + + + const handleLogout = async (e) => { + e.preventDefault(); + + try { + const response = await fetch("http://localhost:8100/users/logout", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + + }); + + if (response.ok) { + // Login successful + const data = await response.json(); + + console.log("Login successful:", data.token); + // If authentication is successful: + sessionStorage.removeItem("authToken"); + // Redirect user to /home + navigate('/'); + } else { + // Login failed + console.error("Login failed"); + // Handle login failure (e.g., display error message to user) + } + } catch (error) { + console.error("Error:", error); + // Handle error (e.g., display error message to user) + } + }; return ( <> @@ -98,12 +140,14 @@ export function Layout(props: { Open sidebar