Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion backend/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ datasource db {

model Image {
id Int @id @default(autoincrement())
name String
name String
image_url String
}

Expand All @@ -26,6 +26,7 @@ model User {
courses Course[]
CourseStudent CourseStudent[]
books Book[]
bookScores UserBookScore[] //Added new relation
}

enum AccountType {
Expand Down Expand Up @@ -89,6 +90,7 @@ model Book {
courses BookCourse[]
owner User @relation(fields: [ownerId], references: [id])
ownerId Int @default(1)
bookScores UserBookScore[] //Added new relation
}

model Page {
Expand Down Expand Up @@ -129,3 +131,14 @@ model BookCourse {
course Course @relation(fields: [courseId], references: [id])
courseId Int
}

model UserBookScore {
id Int @id @default(autoincrement())
userId Int
bookId Int
score Float
user User @relation(fields: [userId], references: [id])
book Book @relation(fields: [bookId], references: [id])

@@map("user_book_scores")
}
36 changes: 26 additions & 10 deletions backend/src/routers/books.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
from fastapi import APIRouter, Depends, HTTPException
from prisma import Json
from pydantic import BaseModel
from src.auth import get_user
from src.auth import get_user, get_user_from_api_key
from src.db import db
from prisma.models import Book, User
from prisma.models import Book, User, UserBookScore
from typing import Annotated, Optional, List
from prisma.enums import BookCategory, AccountType
from prisma.types import BookWhereInput, BookUpdateInput
Expand All @@ -19,6 +19,7 @@ async def search_books(
owner_id: Optional[int] = None,
published: Optional[bool] = None,
query: Optional[str] = None,
user_token: Optional[str] = None,
) -> List[Book]:
where: BookWhereInput = {}
if category:
Expand All @@ -28,12 +29,25 @@ async def search_books(
if published is not None:
where["published"] = published

# Fetch from db
books = await db.book.find_many(
take=limit,
include={"courses": True, "pages": True},
where=where,
order={"category": "asc"},
)
user = None
if user_token:
user = await get_user_from_api_key(user_token)

user_scores = {}
if user:
# Fetches user's book scores if user is logged in
scores = await db.userbookscore.find_many(
where={"userId": user.id}
)
# Creates a dictionary of book IDs to scores for the user
user_scores = {score.bookId: score.score for score in scores}

if query:
filtered_and_sorted_books = [
Expand All @@ -59,18 +73,23 @@ async def search_books(
re.IGNORECASE,
)
),
user_scores.get(book.id, 0) # Added user score as a secondary sort key
)
for book in books
],
key=lambda x: x[1],
key=lambda x: (x[1], x[2]), # Sorts by query match count first, then by the user score
reverse=True,
)
if _ > 0
]
return filtered_and_sorted_books

return books

else:
sorted_books = sorted(
books,
key=lambda book: user_scores.get(book.id, 0), # Sorts by user score
reverse=True
)
return sorted_books

@books_router.get("/books/{book_id}", tags=["books"])
async def get_book(book_id: int) -> Book:
Expand All @@ -79,7 +98,6 @@ async def get_book(book_id: int) -> Book:
raise HTTPException(status_code=404, detail="Book not found")
return book


class CreateBookRequest(BaseModel):
title: str
category: BookCategory
Expand All @@ -90,7 +108,6 @@ class CreateBookRequest(BaseModel):
blurb: Optional[str] = None
readyForPublish: Optional[bool] = False


@books_router.post("/books", tags=["books"])
async def create_book(
req: CreateBookRequest,
Expand Down Expand Up @@ -118,7 +135,6 @@ async def create_book(
)
return book


@books_router.put("/books/{book_id}", tags=["books"])
async def edit_book(
book_id: int,
Expand Down Expand Up @@ -153,4 +169,4 @@ async def edit_book(
)
if not book:
raise HTTPException(status_code=404, detail="Book not found")
return book
return book
73 changes: 62 additions & 11 deletions backend/src/routers/interactions.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,19 @@
from prisma import Prisma
from datetime import datetime
from typing import Annotated, Optional, List
from fastapi import APIRouter
from fastapi import APIRouter, BackgroundTasks, HTTPException, Depends
from src.db import db
from prisma.models import CourseStudent, BookCourse
from prisma.models import CourseStudent, BookCourse, UserBookScore
from pydantic import BaseModel
from typing import Annotated, Optional
from fastapi import APIRouter, Depends
from fastapi import HTTPException
from src.db import db
from prisma.models import User, CourseStudent, BookCourse, Course, Interaction


from prisma.enums import AccountType, InteractionType
from src.auth import get_user
from pydantic import BaseModel
from prisma.types import InteractionCreateInput

import numpy as np

interactions_router = APIRouter()


class InteractionCreateRequest(BaseModel):
interaction_type: InteractionType
time_since_load: int
Expand All @@ -30,14 +24,70 @@ class InteractionCreateRequest(BaseModel):
bookId: Optional[int] = None
pageId: Optional[int] = None


class InteractionCreateResponse(BaseModel):
id: int

alpha = 0.1
gamma = 0.6
epsilon = 0.1
Q = {}

def get_state(interaction_data: InteractionCreateRequest):
return (interaction_data.user_id, interaction_data.bookId, interaction_data.pageId)

def get_action():
if np.random.uniform(0, 1) < epsilon:
return "explore"
else:
return "exploit"

def update_q_table(state, action, reward, next_state):
if state not in Q:
Q[state] = {}
if action not in Q[state]:
Q[state][action] = 0

max_future_q = max(Q[next_state].values()) if next_state in Q else 0
current_q = Q[state][action]

Q[state][action] = current_q + alpha * (reward + gamma * max_future_q - current_q)

def calculate_score(interaction_data: InteractionCreateRequest) -> float:
state = get_state(interaction_data)
action = get_action()
reward = 10.0 if interaction_data.correct else -5.0

next_state = get_state(interaction_data) #Placeholder for now, define accordingly!!!

update_q_table(state, action, reward, next_state)

return reward

async def run_ml_inference(interaction_data: InteractionCreateRequest):
score = calculate_score(interaction_data)

existing_score = await db.userbookscore.find_first(
where={"userId": interaction_data.user_id, "bookId": interaction_data.bookId}
)
if existing_score:
new_score = existing_score.score + score
await db.userbookscore.update(
where={"id": existing_score.id},
data={"score": new_score}
)
else:
await db.userbookscore.create(
data={
"userId": interaction_data.user_id,
"bookId": interaction_data.bookId,
"score": score,
},
)

@interactions_router.post("/interactions", tags=["interactions"])
async def create_interaction(
interaction_data: InteractionCreateRequest,
background_tasks: BackgroundTasks
) -> InteractionCreateResponse:
try:
interaction = await db.interaction.create(
Expand All @@ -53,6 +103,7 @@ async def create_interaction(
"pageId": interaction_data.pageId,
}
)
background_tasks.add_task(run_ml_inference, interaction_data)
except Exception as e:
print(e)
raise HTTPException(status_code=400, detail="Unable to create interaction")
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/api/core/OpenAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export type OpenAPIConfig = {
};

export const OpenAPI: OpenAPIConfig = {
BASE: "https://codekids-backend.endeavour.cs.vt.edu",
BASE: "http://localhost:8080",
VERSION: "0.1.0",
WITH_CREDENTIALS: false,
CREDENTIALS: "include",
Expand Down
1 change: 1 addition & 0 deletions frontend/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export type { SignupRequest } from "./models/SignupRequest";
export type { UpdatePage } from "./models/UpdatePage";
export type { UpdateUserRequest } from "./models/UpdateUserRequest";
export type { User } from "./models/User";
export type { UserBookScore } from "./models/UserBookScore";
export type { UserLightNoPassword } from "./models/UserLightNoPassword";
export type { ValidationError } from "./models/ValidationError";

Expand Down
2 changes: 2 additions & 0 deletions frontend/src/api/models/Book.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { BookCategory } from "./BookCategory";
import type { BookCourse } from "./BookCourse";
import type { Page } from "./Page";
import type { User } from "./User";
import type { UserBookScore } from "./UserBookScore";
/**
* Represents a Book record
*/
Expand All @@ -24,4 +25,5 @@ export type Book = {
courses?: Array<BookCourse> | null;
owner?: User | null;
ownerId: number;
bookScores?: Array<UserBookScore> | null;
};
2 changes: 2 additions & 0 deletions frontend/src/api/models/Interaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export type Interaction = {
correct?: boolean | null;
date: string;
timeSinceLoad: number;
bookId?: number | null;
pageId?: number | null;
question?: Question | null;
questionId?: number | null;
};
2 changes: 2 additions & 0 deletions frontend/src/api/models/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { Book } from "./Book";
import type { Course } from "./Course";
import type { CourseStudent } from "./CourseStudent";
import type { Interaction } from "./Interaction";
import type { UserBookScore } from "./UserBookScore";
/**
* Represents a User record
*/
Expand All @@ -21,4 +22,5 @@ export type User = {
courses?: Array<Course> | null;
CourseStudent?: Array<CourseStudent> | null;
books?: Array<Book> | null;
bookScores?: Array<UserBookScore> | null;
};
17 changes: 17 additions & 0 deletions frontend/src/api/models/UserBookScore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { Book } from "./Book";
import type { User } from "./User";
/**
* Represents a UserBookScore record
*/
export type UserBookScore = {
id: number;
userId: number;
bookId: number;
score: number;
user?: User | null;
book?: Book | null;
};
3 changes: 3 additions & 0 deletions frontend/src/api/services/BooksService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export class BooksService {
* @param ownerId
* @param published
* @param query
* @param userToken
* @returns Book Successful Response
* @throws ApiError
*/
Expand All @@ -25,6 +26,7 @@ export class BooksService {
ownerId?: number | null,
published?: boolean | null,
query?: string | null,
userToken?: string | null,
): CancelablePromise<Array<Book>> {
return __request(OpenAPI, {
method: "GET",
Expand All @@ -35,6 +37,7 @@ export class BooksService {
owner_id: ownerId,
published: published,
query: query,
user_token: userToken,
},
errors: {
422: `Validation Error`,
Expand Down
15 changes: 12 additions & 3 deletions frontend/src/pages/HomePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import Navbar from "../components/Navbar";
import useSound from "use-sound";
import { Book, BookCategory, BooksService } from "../api";
import ActivityBookList from "../components/ActivityBookList";
import { useAuth } from "../context/AuthContext";

export default function HomePage() {
const { user } = useAuth();
const [playSound] = useSound("/sounds/low-click.mp3", { volume: 0.5 });
const [query, setQuery] = useState("");
const [results, setResults] = useState<Book[]>([]);
Expand All @@ -23,7 +25,14 @@ export default function HomePage() {
const loadBookResults = useCallback(
(category: BookCategory | null, query: string | null) => {
setLoading(true);
BooksService.searchBooksBooksGet(category, null, null, true, query)
BooksService.searchBooksBooksGet(
category,
null,
null,
true,
query,
user?.token,
)
.then((response) => {
setResults(response);
setLoading(false);
Expand All @@ -33,12 +42,12 @@ export default function HomePage() {
setLoading(false);
});
},
[setResults, setLoading],
[setResults, setLoading, user],
);

useEffect(() => {
loadBookResults(null, null);
}, [loadBookResults]);
}, [loadBookResults, user]);

return (
<>
Expand Down