mirror of
https://github.com/kuhyx/WUT_Computer_Science.git
synced 2026-07-04 16:43:12 +02:00
Merge branch 'main' into frontend_AI_communication
This commit is contained in:
commit
52e509404e
30
Dockerfile
30
Dockerfile
@ -1,25 +1,15 @@
|
|||||||
# Use an official Python runtime as a parent image
|
FROM node:latest
|
||||||
FROM python:3.10-slim
|
|
||||||
|
|
||||||
# Set the working directory to /app
|
# Create app directory
|
||||||
WORKDIR /app
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
# Install necessary OS packages
|
# Install app dependencies
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
||||||
build-essential \
|
|
||||||
libpq-dev \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Copy the Python script, requirements, and constants.ini file into the container at /app
|
# If you also need http-server globally
|
||||||
COPY connector/Include/frontend_AI_connector.py /app/
|
RUN npm install -g http-server
|
||||||
COPY connector/Include/requirements.txt /app/
|
|
||||||
COPY connector/Include/init_scripts/constants.ini /app/init_scripts/
|
|
||||||
|
|
||||||
# Modify requirements.txt to use psycopg2-binary
|
# Bundle app source
|
||||||
RUN sed -i 's/psycopg2==2.9.9/psycopg2-binary==2.9.9/' requirements.txt
|
COPY . .
|
||||||
|
|
||||||
# Install any needed packages specified in requirements.txt
|
EXPOSE 8080
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
CMD ["http-server", "-p 8080"]
|
||||||
|
|
||||||
# Run frontend_AI_connector.py when the container launches
|
|
||||||
CMD ["python", "./frontend_AI_connector.py"]
|
|
||||||
|
|||||||
25
Dockerfile_backend
Normal file
25
Dockerfile_backend
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# Use an official Python runtime as a parent image
|
||||||
|
FROM python:3.10-slim
|
||||||
|
|
||||||
|
# Set the working directory to /app
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install necessary OS packages
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
build-essential \
|
||||||
|
libpq-dev \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Copy the Python script, requirements, and constants.ini file into the container at /app
|
||||||
|
COPY connector/Include/frontend_AI_connector.py /app/
|
||||||
|
COPY connector/Include/requirements.txt /app/
|
||||||
|
COPY connector/Include/init_scripts/constants.ini /app/init_scripts/
|
||||||
|
|
||||||
|
# Modify requirements.txt to use psycopg2-binary
|
||||||
|
RUN sed -i 's/psycopg2==2.9.9/psycopg2-binary==2.9.9/' requirements.txt
|
||||||
|
|
||||||
|
# Install any needed packages specified in requirements.txt
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Run frontend_AI_connector.py when the container launches
|
||||||
|
CMD ["python", "./frontend_AI_connector.py"]
|
||||||
BIN
docs/Architecture_Requirements.docx
Normal file
BIN
docs/Architecture_Requirements.docx
Normal file
Binary file not shown.
0
docs/IndividualServices/AI.txt
Normal file
0
docs/IndividualServices/AI.txt
Normal file
12
docs/IndividualServices/analytics_service.txt
Normal file
12
docs/IndividualServices/analytics_service.txt
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
1. Receive data from database
|
||||||
|
2. Calculate data:
|
||||||
|
a) User Count
|
||||||
|
b) Movies Count
|
||||||
|
c) Rating Count
|
||||||
|
d) Total data size
|
||||||
|
e) "Hot" movies -> movies that received most ratings during last
|
||||||
|
week
|
||||||
|
f) System logs (keeps tracks of all messages exchanged by all services)
|
||||||
|
1. timestamp (when was a message send)
|
||||||
|
2. Message raw data
|
||||||
|
3. Send requests to frondend upon request
|
||||||
0
docs/IndividualServices/backend.txt
Normal file
0
docs/IndividualServices/backend.txt
Normal file
0
docs/IndividualServices/frontend.txt
Normal file
0
docs/IndividualServices/frontend.txt
Normal file
0
docs/IndividualServices/notification.txt
Normal file
0
docs/IndividualServices/notification.txt
Normal file
BIN
docs/Meetings/first_meeting.docx
Normal file
BIN
docs/Meetings/first_meeting.docx
Normal file
Binary file not shown.
BIN
docs/project_management_meeting_one.docx
Normal file
BIN
docs/project_management_meeting_one.docx
Normal file
Binary file not shown.
25
docs/toDos/deployment_script.txt
Normal file
25
docs/toDos/deployment_script.txt
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
One script upon being ran should deploy the entire solution to some
|
||||||
|
cloud service
|
||||||
|
|
||||||
|
It should:
|
||||||
|
1. Build everything
|
||||||
|
2. Connect to cloud service (Azure?)
|
||||||
|
3. Send the data
|
||||||
|
|
||||||
|
After it will be run website should be accessible under some address
|
||||||
|
(cloud service should provide this address?)
|
||||||
|
|
||||||
|
Decide:
|
||||||
|
What cloud service? (Azure?) Requirements:
|
||||||
|
a. Free (https://github.com/cloudcommunity/Cloud-Free-Tier-Comparison)
|
||||||
|
b. Popular
|
||||||
|
AWS:
|
||||||
|
+ Most popular
|
||||||
|
+ "Always" free
|
||||||
|
+ AWS CDK available
|
||||||
|
Azure
|
||||||
|
-Microsoft
|
||||||
|
Google Cloud
|
||||||
|
+We have google accounts anyway
|
||||||
|
What technology for script (Ansible?)
|
||||||
|
|
||||||
12
docs/toDos/monitoring_service.txt
Normal file
12
docs/toDos/monitoring_service.txt
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
Monitoring service
|
||||||
|
Monitoring service should keep track of all communication
|
||||||
|
send between all services
|
||||||
|
Store logs with:
|
||||||
|
1. timestamp (when was a message send)
|
||||||
|
2. Message raw data
|
||||||
|
|
||||||
|
To decide:
|
||||||
|
What to use for Monitoring service?
|
||||||
|
Maybe cloud service will provide us with functioning one?
|
||||||
|
Maybe deployment tool already has one?
|
||||||
|
|
||||||
19
frontend/.eslintrc.cjs
Normal file
19
frontend/.eslintrc.cjs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
env: { browser: true, es2020: true },
|
||||||
|
extends: [
|
||||||
|
'eslint:recommended',
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
'plugin:react-hooks/recommended',
|
||||||
|
'prettier',
|
||||||
|
],
|
||||||
|
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
plugins: ['react-refresh'],
|
||||||
|
rules: {
|
||||||
|
'react-refresh/only-export-components': [
|
||||||
|
'warn',
|
||||||
|
{ allowConstantExport: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
24
frontend/.gitignore
vendored
Normal file
24
frontend/.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
3
frontend/.prettierrc.json
Normal file
3
frontend/.prettierrc.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": true
|
||||||
|
}
|
||||||
19
frontend/Dockerfile
Normal file
19
frontend/Dockerfile
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
FROM node:18-alpine AS build-stage
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package.json .
|
||||||
|
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
FROM nginx:stable-alpine
|
||||||
|
|
||||||
|
COPY --from=build-stage /app/dist /usr/share/nginx/html
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
13
frontend/index.html
Normal file
13
frontend/index.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Movie Recommendation</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root" class="flex min-h-dvh flex-col bg-gray-800 text-white"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
4887
frontend/package-lock.json
generated
Normal file
4887
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
37
frontend/package.json
Normal file
37
frontend/package.json
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"name": "movie-recommendation-client",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"firebase": "^10.11.1",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-firebase-hooks": "^5.1.1",
|
||||||
|
"react-loader-spinner": "^6.1.6",
|
||||||
|
"react-router-dom": "^6.23.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.2.66",
|
||||||
|
"@types/react-dom": "^18.2.22",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^7.2.0",
|
||||||
|
"@typescript-eslint/parser": "^7.2.0",
|
||||||
|
"@vitejs/plugin-react-swc": "^3.5.0",
|
||||||
|
"autoprefixer": "^10.4.19",
|
||||||
|
"eslint": "^8.57.0",
|
||||||
|
"eslint-config-prettier": "^9.1.0",
|
||||||
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.6",
|
||||||
|
"postcss": "^8.4.38",
|
||||||
|
"prettier": "^3.2.5",
|
||||||
|
"tailwindcss": "^3.4.3",
|
||||||
|
"typescript": "^5.2.2",
|
||||||
|
"vite": "^5.2.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
frontend/postcss.config.js
Normal file
6
frontend/postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
1
frontend/public/vite.svg
Normal file
1
frontend/public/vite.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
33
frontend/src/App.tsx
Normal file
33
frontend/src/App.tsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { Outlet, RouterProvider, createBrowserRouter } from 'react-router-dom';
|
||||||
|
import Navbar from './components/Navbar';
|
||||||
|
import Home from './pages/Home';
|
||||||
|
import Recommendations from './pages/Recommendations';
|
||||||
|
import Rate from './pages/Rate';
|
||||||
|
import Analytics from './pages/Analytics';
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const Layout = (
|
||||||
|
<>
|
||||||
|
<Navbar />
|
||||||
|
<Outlet />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const router = createBrowserRouter([
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
element: Layout,
|
||||||
|
children: [
|
||||||
|
{ path: '/', element: <Home /> },
|
||||||
|
{ path: '/recommendations', element: <Recommendations /> },
|
||||||
|
{ path: '/rate', element: <Rate /> },
|
||||||
|
{ path: '/analytics', element: <Analytics /> },
|
||||||
|
{ path: '*', element: <code>404</code> },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
return <RouterProvider router={router} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
1
frontend/src/assets/google.svg
Normal file
1
frontend/src/assets/google.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><path fill="#fff" d="M44.59 4.21a63.28 63.28 0 004.33 120.9 67.6 67.6 0 0032.36.35 57.13 57.13 0 0025.9-13.46 57.44 57.44 0 0016-26.26 74.33 74.33 0 001.61-33.58H65.27v24.69h34.47a29.72 29.72 0 01-12.66 19.52 36.16 36.16 0 01-13.93 5.5 41.29 41.29 0 01-15.1 0A37.16 37.16 0 0144 95.74a39.3 39.3 0 01-14.5-19.42 38.31 38.31 0 010-24.63 39.25 39.25 0 019.18-14.91A37.17 37.17 0 0176.13 27a34.28 34.28 0 0113.64 8q5.83-5.8 11.64-11.63c2-2.09 4.18-4.08 6.15-6.22A61.22 61.22 0 0087.2 4.59a64 64 0 00-42.61-.38z"/><path fill="#e33629" d="M44.59 4.21a64 64 0 0142.61.37 61.22 61.22 0 0120.35 12.62c-2 2.14-4.11 4.14-6.15 6.22Q95.58 29.23 89.77 35a34.28 34.28 0 00-13.64-8 37.17 37.17 0 00-37.46 9.74 39.25 39.25 0 00-9.18 14.91L8.76 35.6A63.53 63.53 0 0144.59 4.21z"/><path fill="#f8bd00" d="M3.26 51.5a62.93 62.93 0 015.5-15.9l20.73 16.09a38.31 38.31 0 000 24.63q-10.36 8-20.73 16.08a63.33 63.33 0 01-5.5-40.9z"/><path fill="#587dbd" d="M65.27 52.15h59.52a74.33 74.33 0 01-1.61 33.58 57.44 57.44 0 01-16 26.26c-6.69-5.22-13.41-10.4-20.1-15.62a29.72 29.72 0 0012.66-19.54H65.27c-.01-8.22 0-16.45 0-24.68z"/><path fill="#319f43" d="M8.75 92.4q10.37-8 20.73-16.08A39.3 39.3 0 0044 95.74a37.16 37.16 0 0014.08 6.08 41.29 41.29 0 0015.1 0 36.16 36.16 0 0013.93-5.5c6.69 5.22 13.41 10.4 20.1 15.62a57.13 57.13 0 01-25.9 13.47 67.6 67.6 0 01-32.36-.35 63 63 0 01-23-11.59A63.73 63.73 0 018.75 92.4z"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
55
frontend/src/components/Navbar.tsx
Normal file
55
frontend/src/components/Navbar.tsx
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { useAuthState, useSignOut } from 'react-firebase-hooks/auth';
|
||||||
|
import { Link, NavLink } from 'react-router-dom';
|
||||||
|
import { auth } from '../firebase';
|
||||||
|
|
||||||
|
function Navbar() {
|
||||||
|
const [user, userLoading] = useAuthState(auth);
|
||||||
|
const [signOut] = useSignOut(auth);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header className="px-6 py-4 flex justify-between items-center">
|
||||||
|
<Link to="/">
|
||||||
|
<h1 className="text-xl sm:text-2xl">Movie Recommendation</h1>
|
||||||
|
</Link>
|
||||||
|
{userLoading && <p>Loading user info</p>}
|
||||||
|
{!userLoading && !user && (
|
||||||
|
<p className="text-gray-300 text-lg">Logged out</p>
|
||||||
|
)}
|
||||||
|
{!!user && (
|
||||||
|
<div className="flex gap-8">
|
||||||
|
<nav>
|
||||||
|
<ul className="flex flex-col text-center gap-4 md:flex-row">
|
||||||
|
<li>
|
||||||
|
<NavLink
|
||||||
|
to="/recommendations"
|
||||||
|
className={({ isActive }) => (isActive ? 'font-medium' : '')}
|
||||||
|
>
|
||||||
|
Recommendations
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<NavLink
|
||||||
|
to="/rate"
|
||||||
|
className={({ isActive }) => (isActive ? 'font-medium' : '')}
|
||||||
|
>
|
||||||
|
Rate
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<NavLink
|
||||||
|
to="/analytics"
|
||||||
|
className={({ isActive }) => (isActive ? 'font-medium' : '')}
|
||||||
|
>
|
||||||
|
Analytics
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
<button onClick={() => signOut()}>Log out</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Navbar;
|
||||||
55
frontend/src/components/Welcome.tsx
Normal file
55
frontend/src/components/Welcome.tsx
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import {
|
||||||
|
collection,
|
||||||
|
getCountFromServer,
|
||||||
|
query,
|
||||||
|
where,
|
||||||
|
} from 'firebase/firestore';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useAuthState } from 'react-firebase-hooks/auth';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { auth, db } from '../firebase';
|
||||||
|
|
||||||
|
const ratingsRef = collection(db, 'ratings');
|
||||||
|
|
||||||
|
function Welcome() {
|
||||||
|
const [user] = useAuthState(auth);
|
||||||
|
const [ratingsCount, setRatingsCount] = useState<number | null>(null);
|
||||||
|
|
||||||
|
if (!user) throw new Error('No user');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
const ratingsQuery = query(ratingsRef, where('userId', '==', user.uid));
|
||||||
|
const snapshot = await getCountFromServer(ratingsQuery);
|
||||||
|
|
||||||
|
setRatingsCount(snapshot.data().count);
|
||||||
|
})();
|
||||||
|
}, [user.uid]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main>
|
||||||
|
<p className="text-lg mb-8">Hello {user.displayName}!</p>
|
||||||
|
{ratingsCount === null && <p>Loading your data</p>}
|
||||||
|
{Number.isInteger(ratingsCount) &&
|
||||||
|
(ratingsCount === 0 ? (
|
||||||
|
<p>
|
||||||
|
<span>You don't have any ratings yet. Please </span>
|
||||||
|
<Link to="/rate" className="underline">
|
||||||
|
rate
|
||||||
|
</Link>
|
||||||
|
<span> your first movie.</span>
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<p>
|
||||||
|
<span>Go to </span>
|
||||||
|
<Link to="/recommendations" className="underline">
|
||||||
|
recommendations
|
||||||
|
</Link>
|
||||||
|
<span> to get inspired.</span>
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Welcome;
|
||||||
18
frontend/src/firebase.ts
Normal file
18
frontend/src/firebase.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { initializeApp } from 'firebase/app';
|
||||||
|
import { getAuth } from 'firebase/auth';
|
||||||
|
import { getFirestore } from 'firebase/firestore';
|
||||||
|
|
||||||
|
const firebaseConfig = {
|
||||||
|
apiKey: 'AIzaSyAF34ovW0qaDlV68sbpCjylz4TQ0he_XEM',
|
||||||
|
authDomain: 'movie-recommendation-2024.firebaseapp.com',
|
||||||
|
projectId: 'movie-recommendation-2024',
|
||||||
|
storageBucket: 'movie-recommendation-2024.appspot.com',
|
||||||
|
messagingSenderId: '777010585410',
|
||||||
|
appId: '1:777010585410:web:7db1436b72879d512032db',
|
||||||
|
};
|
||||||
|
|
||||||
|
const app = initializeApp(firebaseConfig);
|
||||||
|
const auth = getAuth(app);
|
||||||
|
const db = getFirestore(app);
|
||||||
|
|
||||||
|
export { auth, db };
|
||||||
3
frontend/src/index.css
Normal file
3
frontend/src/index.css
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
10
frontend/src/main.tsx
Normal file
10
frontend/src/main.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import App from './App.tsx';
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>,
|
||||||
|
);
|
||||||
5
frontend/src/pages/Analytics.tsx
Normal file
5
frontend/src/pages/Analytics.tsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
function Analytics() {
|
||||||
|
return <div>Analytics</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Analytics;
|
||||||
35
frontend/src/pages/Home.tsx
Normal file
35
frontend/src/pages/Home.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { useAuthState, useSignInWithGoogle } from 'react-firebase-hooks/auth';
|
||||||
|
import { Triangle } from 'react-loader-spinner';
|
||||||
|
import Welcome from '../components/Welcome';
|
||||||
|
import { auth } from '../firebase';
|
||||||
|
import googleLogo from '../assets/google.svg';
|
||||||
|
|
||||||
|
function Home() {
|
||||||
|
const [user, userLoading] = useAuthState(auth);
|
||||||
|
const [signInWithGoogle] = useSignInWithGoogle(auth);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="flex flex-col items-center mt-16 mx-4 text-center">
|
||||||
|
<h2 className="text-2xl font-medium mb-8">
|
||||||
|
Find the perfect movie to watch tonight.
|
||||||
|
</h2>
|
||||||
|
{userLoading && <Triangle />}
|
||||||
|
{!userLoading && !user && (
|
||||||
|
<>
|
||||||
|
<p className="mt-4">But first you need to log in:</p>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => signInWithGoogle()}
|
||||||
|
className="mt-10 flex items-center gap-2 border rounded-full py-2 px-4 border-gray-400 hover:bg-gray-700 duration-100"
|
||||||
|
>
|
||||||
|
<img src={googleLogo} alt="Google" className="size-8" />
|
||||||
|
<span>Log in with Google</span>
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{!!user && <Welcome />}
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Home;
|
||||||
5
frontend/src/pages/Rate.tsx
Normal file
5
frontend/src/pages/Rate.tsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
function Rate() {
|
||||||
|
return <div>Rate</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Rate;
|
||||||
5
frontend/src/pages/Recommendations.tsx
Normal file
5
frontend/src/pages/Recommendations.tsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
function Recommendations() {
|
||||||
|
return <div>Recommendations</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Recommendations;
|
||||||
1
frontend/src/vite-env.d.ts
vendored
Normal file
1
frontend/src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
9
frontend/tailwind.config.ts
Normal file
9
frontend/tailwind.config.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import type { Config } from 'tailwindcss';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
} satisfies Config;
|
||||||
25
frontend/tsconfig.json
Normal file
25
frontend/tsconfig.json
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
}
|
||||||
11
frontend/tsconfig.node.json
Normal file
11
frontend/tsconfig.node.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"strict": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
7
frontend/vite.config.ts
Normal file
7
frontend/vite.config.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react-swc';
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
});
|
||||||
4804
movie_recommendations/datasets/tmdb_5000_credits.csv
Normal file
4804
movie_recommendations/datasets/tmdb_5000_credits.csv
Normal file
File diff suppressed because one or more lines are too long
4804
movie_recommendations/datasets/tmdb_5000_movies.csv
Normal file
4804
movie_recommendations/datasets/tmdb_5000_movies.csv
Normal file
File diff suppressed because one or more lines are too long
119
movie_recommendations/movie_recommender.py
Normal file
119
movie_recommendations/movie_recommender.py
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
import pandas as pd
|
||||||
|
import numpy as np
|
||||||
|
from ast import literal_eval
|
||||||
|
from sklearn.feature_extraction.text import CountVectorizer
|
||||||
|
from sklearn.metrics.pairwise import cosine_similarity
|
||||||
|
|
||||||
|
|
||||||
|
def get_director(x):
|
||||||
|
for i in x:
|
||||||
|
if i['job'] == 'Director':
|
||||||
|
return i['name']
|
||||||
|
return np.nan
|
||||||
|
|
||||||
|
|
||||||
|
def get_list(x):
|
||||||
|
if isinstance(x, list):
|
||||||
|
names = [i['name'] for i in x]
|
||||||
|
if len(names) > 3:
|
||||||
|
names = names[:3]
|
||||||
|
return names
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def clean_data(x):
|
||||||
|
if isinstance(x, list):
|
||||||
|
return [str.lower(i.replace(" ", "")) for i in x]
|
||||||
|
else:
|
||||||
|
if isinstance(x, str):
|
||||||
|
return str.lower(x.replace(" ", ""))
|
||||||
|
else:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
|
||||||
|
def create_soup(x):
|
||||||
|
return ' '.join(x['keywords']) + ' ' + ' '.join(x['cast']) + ' ' + x['director'] + ' ' + ' '.join(x['genres'])
|
||||||
|
|
||||||
|
|
||||||
|
class MovieRecommender:
|
||||||
|
def __init__(self):
|
||||||
|
self.df = None
|
||||||
|
self.cosine_sim = None
|
||||||
|
|
||||||
|
def fit(self, credits_file, movies_file):
|
||||||
|
"""
|
||||||
|
Fittuje AI do przekazanych danych
|
||||||
|
:param credits_file: csv z creditsami
|
||||||
|
:param movies_file: csv z filmami
|
||||||
|
:return: Nic
|
||||||
|
"""
|
||||||
|
df1 = pd.read_csv(credits_file)
|
||||||
|
df2 = pd.read_csv(movies_file)
|
||||||
|
df1.columns = ['id', 'tittle', 'cast', 'crew']
|
||||||
|
df2 = df2.merge(df1, on='id')
|
||||||
|
df2['overview'] = df2['overview'].fillna('')
|
||||||
|
self.df = df2
|
||||||
|
|
||||||
|
features = ['cast', 'crew', 'keywords', 'genres']
|
||||||
|
for feature in features:
|
||||||
|
df2[feature] = df2[feature].apply(literal_eval)
|
||||||
|
|
||||||
|
df2['director'] = df2['crew'].apply(get_director)
|
||||||
|
|
||||||
|
features = ['cast', 'keywords', 'genres']
|
||||||
|
for feature in features:
|
||||||
|
df2[feature] = df2[feature].apply(get_list)
|
||||||
|
|
||||||
|
features = ['cast', 'keywords', 'director', 'genres']
|
||||||
|
for feature in features:
|
||||||
|
df2[feature] = df2[feature].apply(clean_data)
|
||||||
|
|
||||||
|
df2['soup'] = df2.apply(create_soup, axis=1)
|
||||||
|
|
||||||
|
count = CountVectorizer(stop_words='english')
|
||||||
|
count_matrix = count.fit_transform(df2['soup'])
|
||||||
|
self.cosine_sim = cosine_similarity(count_matrix, count_matrix)
|
||||||
|
|
||||||
|
self.df = df2.reset_index()
|
||||||
|
|
||||||
|
def _get_recommendations_one_input(self, movie_id):
|
||||||
|
"""
|
||||||
|
Tworzy rekomendacje, bazując na jednym filmie
|
||||||
|
:param movie_id: id filmu, dla którego ma zrobić rekomendację
|
||||||
|
:return: Zwraca listę [movie_ids, similarity_scores] gdzie oba argumenty są np.array
|
||||||
|
"""
|
||||||
|
indices = pd.Series(self.df.index, index=self.df['id']).drop_duplicates()
|
||||||
|
idx = indices[movie_id]
|
||||||
|
sim_scores = list(enumerate(self.cosine_sim[idx]))
|
||||||
|
sim_scores = sorted(sim_scores, key=lambda x: x[1], reverse=True)
|
||||||
|
sim_scores = sim_scores[1:101]
|
||||||
|
movie_indices = [i[0] for i in sim_scores]
|
||||||
|
sim_scores = np.array([t[1] for t in sim_scores])
|
||||||
|
return [self.df['id'].iloc[movie_indices].values, sim_scores]
|
||||||
|
|
||||||
|
def get_recommendations(self, movie_ids: list) -> {}:
|
||||||
|
"""
|
||||||
|
Tworzy listę rekomendacji bazującą na id podanych filmów
|
||||||
|
:param movie_ids: id filmów, na podstawie których ma wybrać rekomendowane filmy
|
||||||
|
:return: Zwraca dicta {movie_id: similarity_scores}
|
||||||
|
"""
|
||||||
|
recommended_movies = {}
|
||||||
|
for movie_id in movie_ids:
|
||||||
|
recommended_ids, sim_scores = self._get_recommendations_one_input(movie_id)
|
||||||
|
for recommended_id, sim_score in zip(recommended_ids, sim_scores):
|
||||||
|
if recommended_id in movie_ids:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if recommended_movies.get(recommended_id) is None:
|
||||||
|
recommended_movies[recommended_id] = sim_score / len(movie_ids)
|
||||||
|
else:
|
||||||
|
recommended_movies[recommended_id] += sim_score / len(movie_ids)
|
||||||
|
return recommended_movies
|
||||||
|
|
||||||
|
|
||||||
|
# Przykładowe użycie:
|
||||||
|
if __name__ == "__main__":
|
||||||
|
recommender = MovieRecommender()
|
||||||
|
recommender.fit('datasets/tmdb_5000_credits.csv', 'datasets/tmdb_5000_movies.csv')
|
||||||
|
recommendations = recommender.get_recommendations([49026, 155, 312113])
|
||||||
|
print(recommendations)
|
||||||
Loading…
Reference in New Issue
Block a user