diff --git a/backend/app/main.py b/backend/app/main.py index ac9c7b2918fa444aaa85a7fca0091552beb2b841..45496391af06cfde03255c5fc801558f36b5bce7 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -28,6 +28,7 @@ import config from security import jwt_cookie, get_jwt_user from routes.tests import testRouter from routes.studies import studiesRouter +from routes.chat import chatRouter websocket_users = defaultdict(lambda: defaultdict(set)) websocket_users_global = defaultdict(set) @@ -1057,5 +1058,6 @@ v1Router.include_router(studyRouter) v1Router.include_router(websocketRouter) v1Router.include_router(testRouter) v1Router.include_router(studiesRouter) +v1Router.include_router(chatRouter) apiRouter.include_router(v1Router) app.include_router(apiRouter) diff --git a/backend/app/routes/chat.py b/backend/app/routes/chat.py new file mode 100644 index 0000000000000000000000000000000000000000..13bd01dee150c3171db976cd2c88dd671a99bb29 --- /dev/null +++ b/backend/app/routes/chat.py @@ -0,0 +1,46 @@ +from fastapi import APIRouter, HTTPException, Depends +from pydantic import BaseModel +import requests +import os +from dotenv import load_dotenv + +load_dotenv() +API_KEY = os.getenv("OPENROUTER_API_KEY") + +MODEL_NAME = "mistralai/mistral-small-24b-instruct-2501:free" +API_URL = "https://openrouter.ai/api/v1/chat/completions" + +chatRouter = APIRouter(prefix="/chat", tags=["chat"]) + + +class ChatMessage(BaseModel): + session_id: str + role: str + content: str + + +chat_sessions = {} + + +@chatRouter.post("/") +async def chat_with_ai(message: ChatMessage): + session_id = message.session_id + user_message = {"role": message.role, "content": message.content} + + if session_id not in chat_sessions: + chat_sessions[session_id] = [] + + chat_sessions[session_id].append(user_message) + + headers = {"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"} + + data = {"model": MODEL_NAME, "messages": chat_sessions[session_id]} + + response = requests.post(API_URL, headers=headers, json=data) + + if response.status_code == 200: + bot_response = response.json()["choices"][0]["message"]["content"] + chat_sessions[session_id].append({"role": "assistant", "content": bot_response}) + return {"response": bot_response} + else: + raise HTTPException(status_code=response.status_code, detail=response.text) diff --git a/backend/requirements.txt b/backend/requirements.txt index b1a43b0a21db07ac0c0fdc136c874c25c90706a7..9eb2045946b35bf2cd227dc74c67c30296d7937a 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,7 +1,43 @@ -uvicorn[standard]>=0.27.0,<0.28.0 -fastapi>=0.110.0,<0.111.0 -fastapi_jwt>=0.2.0,<0.3.0 -sqlalchemy>=2.0.0,<2.1.0 -passlib>=1.7.0,<1.8.0 -python-jose>=3.3.0,<3.4.0 -python-multipart>=0.0.0,<0.1.0 +alembic==1.14.1 +annotated-types==0.7.0 +anyio==4.7.0 +bcrypt==3.2.0 +black==24.10.0 +certifi==2025.1.31 +cffi==1.17.1 +charset-normalizer==3.4.1 +click==8.1.7 +cryptography==44.0.0 +ecdsa==0.19.0 +fastapi==0.110.3 +fastapi-jwt==0.2.0 +h11==0.14.0 +httptools==0.6.4 +idna==3.10 +Mako==1.3.9 +MarkupSafe==3.0.2 +mypy-extensions==1.0.0 +packaging==24.2 +passlib==1.7.4 +pathspec==0.12.1 +platformdirs==4.3.6 +pyasn1==0.6.1 +pycparser==2.22 +pydantic==2.10.3 +pydantic_core==2.27.1 +python-dotenv==1.0.1 +python-jose==3.3.0 +python-multipart==0.0.19 +PyYAML==6.0.2 +requests==2.32.3 +rsa==4.9 +six==1.17.0 +sniffio==1.3.1 +SQLAlchemy==2.0.36 +starlette==0.37.2 +typing_extensions==4.12.2 +urllib3==2.3.0 +uvicorn==0.27.1 +uvloop==0.21.0 +watchfiles==1.0.3 +websockets==14.1 diff --git a/frontend/src/lib/api/sessions.ts b/frontend/src/lib/api/sessions.ts index 5e056ac89932e7fa7060a786bbc6a2969e9b03aa..52674c99b69e6262098183d11a02e4595674a681 100644 --- a/frontend/src/lib/api/sessions.ts +++ b/frontend/src/lib/api/sessions.ts @@ -53,6 +53,26 @@ export async function createMessageAPI( return await response.json(); } +export async function createAIMessageAPI( + fetch: fetchType, + sessionId: string, + content: string +): Promise<any | null> { + const response = await fetch(`/api/chat/`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + session_id: sessionId, + role: "user", + content: content + }) + }); + + if (!response.ok) return null; + + return await response.json(); +} + export async function updateMessageAPI( fetch: fetchType, id: number, diff --git a/frontend/src/lib/components/tests/typingbox.svelte b/frontend/src/lib/components/tests/typingbox.svelte index 9bdd821c2dde54bb11606b522d05c8d1851b8a97..9f0052da79d48bec7821afa64ed536d50dab3f64 100644 --- a/frontend/src/lib/components/tests/typingbox.svelte +++ b/frontend/src/lib/components/tests/typingbox.svelte @@ -62,6 +62,7 @@ rid, user?.id || null, typingTest.id, + user?.study_id ?? 0, position, downtime, uptime, diff --git a/frontend/src/lib/components/tests/typingtest.svelte b/frontend/src/lib/components/tests/typingtest.svelte index 8dc8a3ac64439efd2575501c2c7242cc66523ffc..3b8762cb30afe68ffa11b418315fd60e758ea6af 100644 --- a/frontend/src/lib/components/tests/typingtest.svelte +++ b/frontend/src/lib/components/tests/typingtest.svelte @@ -119,20 +119,20 @@ {@const j = step - 1} {#each exercices as _, i (i)} {#if i === j} - <Typingbox - exerciceId={i} - initialDuration={exercices[i].duration} - explications={exercices[i].explications} - text={exercices[i].text} - bind:data - bind:inProgress - onFinish={() => { - inProgress = false; - setTimeout(() => { - step++; - }, 3000); - }} - /> + <Typingbox + exerciceId={i} + initialDuration={exercices[i].duration} + explications={exercices[i].explications} + text={exercices[i].text} + bind:data + bind:inProgress + onFinish={() => { + inProgress = false; + setTimeout(() => { + step++; + }, 3000); + }} + /> {/if} {/each} {:else} diff --git a/frontend/src/lib/types/session.ts b/frontend/src/lib/types/session.ts index 254dce9feba8477403447640ed980c6ecc23f72b..afa20dccbbdf6247f9b321701e7ae3b72f5143b1 100644 --- a/frontend/src/lib/types/session.ts +++ b/frontend/src/lib/types/session.ts @@ -12,7 +12,8 @@ import { patchSessionAPI, removeUserFromSessionAPI, sendPresenceAPI, - sendTypingAPI + sendTypingAPI, + createAIMessageAPI } from '$lib/api/sessions'; import Message from './message'; import config from '$lib/config'; @@ -204,6 +205,8 @@ export default class Session { replyTo: string | null ): Promise<Message | null> { const json = await createMessageAPI(fetch, this.id, content, metadata, replyTo); + const ai_message = await createAIMessageAPI(fetch, this.id.toString(), content); + console.log("AI Message: ", ai_message); if (json == null || json.id == null || json.message_id == null) { toastAlert('Failed to parse message'); return null; diff --git a/frontend/src/lib/types/user.ts b/frontend/src/lib/types/user.ts index b1acf04ec6dda0e0b07676c98a78f9713994b444..5e432d6815c056d71e0999dd357b17b301cece59 100644 --- a/frontend/src/lib/types/user.ts +++ b/frontend/src/lib/types/user.ts @@ -162,7 +162,9 @@ export default class User { return this._tutor_list; } - get availabilities(): { day: string; start: string; end: string }[] { + get availabilities(): { + avaibility: number; day: string; start: string; end: string +}[] { return this._availabilities; } diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index 963b4208c4d15d51aeac72dac32a4b97ac200c0c..29c8065d1ed4bcbbd463e9d341c62d3dab80a01d 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -129,6 +129,19 @@ ); } + async function createSoloSession() { + let session = await Session.create(); + if (!session) { + console.warn("Failed to create solo session."); + return; + } + + contactSessions = [...contactSessions, session].sort( + (a, b) => b.start_time.getTime() - a.start_time.getTime() + ); +} + + async function searchNickname() { if (!user || !nickname || !nickname.includes('@')) { toastWarning('Please enter a valid email address'); @@ -191,6 +204,16 @@ {$t('home.createSession')} </button> <button + onclick={(e) => { + e.preventDefault(); + createSoloSession(); + }} + class="button float-start mr-2" +> + Solo session +</button> + + <button class="button float-start" class:btn-disabled={!contact || !contact.calcom_link} data-cal-link={`${contact.calcom_link}?email=${user?.email}&name=${user?.nickname}`} diff --git a/frontend/src/routes/register/[[studyId]]/+page.svelte b/frontend/src/routes/register/[[studyId]]/+page.svelte index 84b646af6a0f51dd17fb79dd2aa6a4cbc1105802..1c4223dca0015f7f33750f444439d990632d2284 100644 --- a/frontend/src/routes/register/[[studyId]]/+page.svelte +++ b/frontend/src/routes/register/[[studyId]]/+page.svelte @@ -615,7 +615,7 @@ <p class="text-center"> {@html $t('register.continue')} </p> - <button class="button mt-4 w-full" onclick={() => (current_step = 6)}> + <button class="button mt-4 w-full" onclick={() => (current_step = 8)}> {$t('register.continueButton')} </button> <button class="button mt-4 w-full" onclick={() => (document.location.href = '/')}>