├── backend
├── app
│ ├── __init__.py
│ ├── db
│ │ ├── __init__.py
│ │ └── db_config.py
│ ├── logs
│ │ ├── __init__.py
│ │ └── logger.py
│ ├── logic
│ │ ├── __init__.py
│ │ └── document_indexer.py
│ ├── models
│ │ ├── __init__.py
│ │ └── data_structures.py
│ ├── controllers
│ │ ├── __init__.py
│ │ └── chat_router.py
│ ├── middleware
│ │ ├── rate_limit_middleware.py
│ │ └── rate_limiter.py
│ ├── startup
│ │ └── documents
│ │ │ └── init_documents.py
│ └── factory.py
├── tests
│ ├── __init__.py
│ ├── conftest.py
│ ├── db
│ │ └── test_db_config.py
│ ├── controllers
│ │ └── test_chat_router.py
│ └── logic
│ │ └── test_document_indexer.py
├── .dockerignore
├── pytest.ini
├── Pipfile
├── Dockerfile.postgres
├── Dockerfile
├── fly.toml
├── run_server.py
├── docker-compose.yml
├── docs
│ └── example_resume.md
└── README.md
├── frontend
├── public
│ ├── profile.jpg
│ ├── about-me.md
│ ├── icon.svg
│ └── config.json
├── postcss.config.js
├── vercel.json
├── vite.config.js
├── src
│ ├── main.jsx
│ ├── services
│ │ ├── github.js
│ │ ├── chatService.js
│ │ └── github.test.js
│ ├── components
│ │ ├── sections
│ │ │ ├── Blog
│ │ │ │ ├── index.jsx
│ │ │ │ └── index.test.jsx
│ │ │ ├── Home
│ │ │ │ ├── RateLimitCountdown.jsx
│ │ │ │ ├── TypingIndicator.jsx
│ │ │ │ ├── index.test.jsx
│ │ │ │ ├── IntroSection.test.jsx
│ │ │ │ ├── index.jsx
│ │ │ │ ├── ChatInput.jsx
│ │ │ │ ├── ChatMessage.jsx
│ │ │ │ ├── RateLimitCountdown.test.jsx
│ │ │ │ ├── TypingIndicator.test.jsx
│ │ │ │ ├── IntroSection.jsx
│ │ │ │ ├── ChatInput.test.jsx
│ │ │ │ └── ChatMessage.test.jsx
│ │ │ ├── Projects
│ │ │ │ ├── index.jsx
│ │ │ │ └── index.test.jsx
│ │ │ └── About
│ │ │ │ ├── index.jsx
│ │ │ │ └── index.test.jsx
│ │ └── layout
│ │ │ ├── MainLayout.jsx
│ │ │ ├── NavBar
│ │ │ ├── DesktopNav.jsx
│ │ │ ├── index.jsx
│ │ │ ├── MobileNav.jsx
│ │ │ ├── DesktopNav.test.jsx
│ │ │ ├── index.test.jsx
│ │ │ └── MobileNav.test.jsx
│ │ │ ├── shared
│ │ │ ├── SocialLinks.jsx
│ │ │ └── SocialLinks.test.jsx
│ │ │ ├── config
│ │ │ ├── navigationConfig.js
│ │ │ └── navigationConfig.test.js
│ │ │ └── MainLayout.test.jsx
│ ├── hooks
│ │ ├── useGithubRepos.js
│ │ ├── chat
│ │ │ ├── useSession.js
│ │ │ ├── useMessages.js
│ │ │ ├── useSession.test.js
│ │ │ └── useMessages.test.js
│ │ ├── useTypingEffect.js
│ │ ├── useStreamingChat.js
│ │ ├── useTypingEffect.test.js
│ │ ├── useGithubRepos.test.js
│ │ └── useStreamingChat.test.js
│ ├── App.jsx
│ ├── test
│ │ ├── mocks
│ │ │ └── config.json
│ │ ├── setup.js
│ │ ├── mocks.js
│ │ └── testUtils.jsx
│ ├── config
│ │ ├── ConfigProvider.test.jsx
│ │ ├── configLoader.js
│ │ ├── ConfigProvider.jsx
│ │ └── configLoader.test.js
│ ├── index.css
│ └── App.test.jsx
├── index.html
├── vitest.config.js
├── .dockerignore
├── tailwind.config.js
├── Dockerfile
├── eslint.config.js
├── package.json
├── README.md
└── CONFIGURATION.md
├── env.example
├── LICENSE
├── .github
└── workflows
│ └── run-tests.yml
├── docker-compose.yml
├── README.md
└── .gitignore
/backend/app/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/app/db/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/app/logs/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/app/logic/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/app/models/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/app/controllers/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/tests/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | Test package for the backend application.
3 | """
--------------------------------------------------------------------------------
/frontend/public/profile.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlonXt/ai-portfolio/HEAD/frontend/public/profile.jpg
--------------------------------------------------------------------------------
/backend/.dockerignore:
--------------------------------------------------------------------------------
1 | # Python
2 | __pycache__
3 | app.egg-info
4 | *.pyc
5 | .mypy_cache
6 | .coverage
7 | htmlcov
8 | .venv
9 |
--------------------------------------------------------------------------------
/frontend/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/frontend/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "rewrites": [
3 | {
4 | "source": "/(.*)",
5 | "destination": "/index.html"
6 | }
7 | ]
8 | }
--------------------------------------------------------------------------------
/frontend/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import react from '@vitejs/plugin-react'
3 |
4 | // https://vite.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | })
8 |
--------------------------------------------------------------------------------
/frontend/src/main.jsx:
--------------------------------------------------------------------------------
1 | import { StrictMode } from 'react'
2 | import { createRoot } from 'react-dom/client'
3 | import { inject } from '@vercel/analytics'
4 | import './index.css'
5 | import App from './App.jsx'
6 |
7 | // Initialize Vercel analytics
8 | inject();
9 |
10 | createRoot(document.getElementById('root')).render(
11 |
12 |
13 | ,
14 | )
15 |
--------------------------------------------------------------------------------
/frontend/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | alontrugman.dev
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/backend/pytest.ini:
--------------------------------------------------------------------------------
1 | [pytest]
2 | testpaths = tests
3 | python_files = test_*.py
4 | python_classes = Test*
5 | python_functions = test_*
6 | addopts = -v --cov=app --cov-report=term-missing -W ignore::DeprecationWarning -W ignore::RuntimeWarning
7 | pythonpath = .
8 | asyncio_mode = auto
9 | asyncio_default_fixture_loop_scope = function
10 | markers =
11 | unit: mark test as a unit test
12 | integration: mark test as an integration test
--------------------------------------------------------------------------------
/frontend/vitest.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vitest/config'
2 | import react from '@vitejs/plugin-react'
3 | import { resolve } from 'path'
4 |
5 | export default defineConfig({
6 | plugins: [react()],
7 | test: {
8 | environment: 'jsdom',
9 | globals: true,
10 | setupFiles: ['./src/test/setup.js'],
11 | },
12 | resolve: {
13 | alias: {
14 | '@': resolve(__dirname, './src'),
15 | },
16 | },
17 | })
--------------------------------------------------------------------------------
/frontend/src/services/github.js:
--------------------------------------------------------------------------------
1 | const GITHUB_API_BASE = 'https://api.github.com'
2 |
3 | export const fetchUserRepos = async (username) => {
4 | try {
5 | const response = await fetch(`${GITHUB_API_BASE}/users/${username}/repos`)
6 | if (!response.ok) throw new Error('Failed to fetch repos')
7 | return await response.json()
8 | } catch (error) {
9 | console.error('Error fetching repos:', error)
10 | throw error
11 | }
12 | }
--------------------------------------------------------------------------------
/backend/app/db/db_config.py:
--------------------------------------------------------------------------------
1 | from urllib.parse import quote_plus
2 | from pydantic import BaseModel
3 |
4 |
5 | class PostgresConfig(BaseModel):
6 | server: str = "localhost"
7 | port: int = 5432
8 | db_name: str
9 | user: str
10 | password: str
11 |
12 | def get_connection_url(self) -> str:
13 | encoded_password = quote_plus(self.password)
14 | return f"postgresql://{self.user}:{encoded_password}@{self.server}:{self.port}/{self.db_name}"
--------------------------------------------------------------------------------
/frontend/src/components/sections/Blog/index.jsx:
--------------------------------------------------------------------------------
1 | import { motion } from 'framer-motion'
2 |
3 | export const BlogSection = () => {
4 | return (
5 |
11 | Blog
12 | Coming Soon!
13 |
14 | )
15 | }
--------------------------------------------------------------------------------
/frontend/src/components/layout/MainLayout.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types'
2 | import { NavBar } from './NavBar'
3 |
4 | export const MainLayout = ({ children }) => {
5 | return (
6 |
7 |
8 |
9 |
10 | {children}
11 |
12 |
13 | )
14 | }
15 |
16 | MainLayout.propTypes = {
17 | children: PropTypes.node.isRequired
18 | }
--------------------------------------------------------------------------------
/frontend/.dockerignore:
--------------------------------------------------------------------------------
1 | # Python
2 | __pycache__
3 | app.egg-info
4 | *.pyc
5 | .mypy_cache
6 | .coverage
7 | htmlcov
8 | .venv
9 |
10 | # Dependencies
11 | node_modules
12 | npm-debug.log
13 | yarn-debug.log
14 | yarn-error.log
15 |
16 | # Testing
17 | coverage
18 | .nyc_output
19 |
20 | # Build
21 | dist
22 | build
23 |
24 | # Environment
25 | .env
26 | .env.local
27 | .env.*.local
28 |
29 | # Editor directories and files
30 | .idea
31 | .vscode
32 | *.suo
33 | *.ntvs*
34 | *.njsproj
35 | *.sln
36 | *.sw?
37 |
38 | # System Files
39 | .DS_Store
40 | Thumbs.db
41 |
--------------------------------------------------------------------------------
/frontend/tailwind.config.js:
--------------------------------------------------------------------------------
1 | import plugin from 'tailwindcss/plugin'
2 |
3 | /** @type {import('tailwindcss').Config} */
4 | export default {
5 | content: [
6 | "./index.html",
7 | "./src/**/*.{js,ts,jsx,tsx}",
8 | ],
9 | theme: {
10 | extend: {},
11 | },
12 | plugins: [
13 | plugin(function({ addUtilities }) {
14 | addUtilities({
15 | '.bg-grid-pattern': {
16 | 'background-image': 'radial-gradient(rgba(255, 255, 255, 0.1) 1px, transparent 1px)',
17 | 'background-size': '20px 20px',
18 | },
19 | })
20 | }),
21 | ],
22 | }
23 |
--------------------------------------------------------------------------------
/backend/tests/conftest.py:
--------------------------------------------------------------------------------
1 | """
2 | Pytest configuration file containing shared fixtures.
3 | """
4 | import pytest
5 | from unittest.mock import Mock
6 | from app.logic.chat_service import ChatService
7 | from app.controllers.chat_router import ChatRouter
8 |
9 | @pytest.fixture
10 | def mock_chat_service():
11 | """
12 | Creates a mock ChatService for testing.
13 | """
14 | mock = Mock(spec=ChatService)
15 | return mock
16 |
17 | @pytest.fixture
18 | def chat_router(mock_chat_service):
19 | """
20 | Creates a ChatRouter instance with mock dependencies.
21 | """
22 | return ChatRouter(chat_service=mock_chat_service)
--------------------------------------------------------------------------------
/backend/Pipfile:
--------------------------------------------------------------------------------
1 | [[source]]
2 | url = "https://pypi.org/simple"
3 | verify_ssl = true
4 | name = "pypi"
5 |
6 | [packages]
7 | fastapi = "==0.115.8"
8 | uvicorn = "==0.34.0"
9 | pydantic = "==2.10.6"
10 | openai = "==1.64.0"
11 | sqlmodel = "==0.0.22"
12 | psycopg2-binary = "==2.9.10"
13 | python-dotenv = "==1.0.1"
14 | pgvector = "==0.3.6"
15 | numpy = "==2.2.3"
16 | langchain-openai = "==0.3.6"
17 | langchain-community = "==0.3.18"
18 | redis = "==5.2.1"
19 | slowapi = "==0.1.9"
20 |
21 | [dev-packages]
22 | pytest = "==8.3.4"
23 | pytest-asyncio = "==0.25.3"
24 | pytest-cov = "==6.0.0"
25 |
26 | [requires]
27 | python_version = "3.12"
28 | python_full_version = "3.12.5"
29 |
--------------------------------------------------------------------------------
/backend/Dockerfile.postgres:
--------------------------------------------------------------------------------
1 | FROM postgres:16.1
2 |
3 | RUN apt-get update && \
4 | apt-get install -y --no-install-recommends \
5 | git \
6 | build-essential \
7 | postgresql-server-dev-16 \
8 | ca-certificates && \
9 | update-ca-certificates
10 |
11 | WORKDIR /home
12 | RUN git clone --branch v0.7.4 https://github.com/pgvector/pgvector.git && \
13 | cd pgvector && \
14 | make && \
15 | make install
16 |
17 | # Cleanup to reduce image size
18 | RUN apt-get remove -y git build-essential postgresql-server-dev-16 && \
19 | apt-get autoremove -y && \
20 | apt-get clean && \
21 | rm -rf /var/lib/apt/lists/* /home/pgvector
--------------------------------------------------------------------------------
/frontend/src/components/sections/Blog/index.test.jsx:
--------------------------------------------------------------------------------
1 | import { describe, it, expect } from 'vitest';
2 | import { screen } from '@testing-library/react';
3 | import { BlogSection } from './index';
4 | import React from 'react';
5 | import '../../../test/mocks';
6 | import { renderWithConfig } from '../../../test/testUtils';
7 |
8 | describe('BlogSection', () => {
9 | it('renders the blog section with coming soon message', () => {
10 | renderWithConfig();
11 |
12 | // Check for heading and coming soon message
13 | expect(screen.getByText('Blog')).toBeInTheDocument();
14 | expect(screen.getByText('Coming Soon!')).toBeInTheDocument();
15 | });
16 | });
--------------------------------------------------------------------------------
/frontend/public/about-me.md:
--------------------------------------------------------------------------------
1 | Hi, I'm Alon Trugman 👋 As a software developer with a passion for startups, I thrive when turning ideas into impactful products.
2 | At Wix, I improved developer experience & securtiy through Git infrastructure solutions.
3 | Previously at Planck (now Applied Systems), I built scalable data processing systems that transformed insurance industry data.
4 | My background in Industrial Engineering and Data Science brings an analytical perspective to engineering challenges.
5 | I specialize in modern tech stacks and excel at simplifying complex problems.
6 | Outside work, I train for triathlons, practice Vipassana meditation, and mentor developers through Kravi-Tech & Genesis.
7 |
8 | Tech enthusiast • People Person • Sports Junkie • Meditator
--------------------------------------------------------------------------------
/frontend/public/icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/backend/app/logs/logger.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import sys
3 | from functools import lru_cache
4 |
5 | FORMATTER = logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s")
6 |
7 | @lru_cache(maxsize=None)
8 | def get_console_handler() -> logging.StreamHandler:
9 | """Configure and cache console logging handler"""
10 | console_handler = logging.StreamHandler(sys.stdout)
11 | console_handler.setFormatter(FORMATTER)
12 | return console_handler
13 |
14 | def get_logger(logger_name: str) -> logging.Logger:
15 | logger = logging.getLogger(logger_name)
16 | logger.setLevel(logging.DEBUG)
17 |
18 | if not logger.handlers:
19 | logger.addHandler(get_console_handler())
20 |
21 | logger.propagate = False
22 | return logger
23 |
24 |
--------------------------------------------------------------------------------
/backend/app/middleware/rate_limit_middleware.py:
--------------------------------------------------------------------------------
1 | from fastapi import Request
2 | from starlette.middleware.base import BaseHTTPMiddleware
3 | from app.middleware.rate_limiter import RateLimiter
4 |
5 | class RateLimitMiddleware(BaseHTTPMiddleware):
6 | def __init__(self, app, rate_limiter: RateLimiter):
7 | super().__init__(app)
8 | self.rate_limiter = rate_limiter
9 |
10 | async def dispatch(self, request: Request, call_next):
11 | # Check rate limits
12 | rate_limit_response = await self.rate_limiter.check_rate_limit(request, None)
13 | if rate_limit_response:
14 | return rate_limit_response
15 |
16 | # Proceed with the request if within limits
17 | response = await call_next(request)
18 | return response
--------------------------------------------------------------------------------
/frontend/src/components/sections/Home/RateLimitCountdown.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import { Clock } from 'lucide-react';
3 |
4 | export const RateLimitCountdown = ({ seconds }) => {
5 | const [timeLeft, setTimeLeft] = useState(seconds);
6 |
7 | useEffect(() => {
8 | if (timeLeft <= 0) return;
9 |
10 | const timer = setInterval(() => {
11 | setTimeLeft(prev => Math.max(0, prev - 1));
12 | }, 1000);
13 |
14 | return () => clearInterval(timer);
15 | }, [timeLeft]);
16 |
17 | if (timeLeft <= 0) return null;
18 |
19 | return (
20 |
21 |
22 | Try again in {timeLeft} seconds
23 |
24 | );
25 | };
--------------------------------------------------------------------------------
/frontend/src/hooks/useGithubRepos.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react'
2 | import { fetchUserRepos } from '../services/github'
3 |
4 | export const useGithubRepos = (username, limit = 6) => {
5 | const [repos, setRepos] = useState([])
6 | const [loading, setLoading] = useState(true)
7 | const [error, setError] = useState(null)
8 |
9 | useEffect(() => {
10 | const loadRepos = async () => {
11 | try {
12 | const data = await fetchUserRepos(username)
13 | setRepos(data.filter(repo => !repo.fork).slice(0, limit))
14 | } catch (err) {
15 | setError(err)
16 | } finally {
17 | setLoading(false)
18 | }
19 | }
20 |
21 | loadRepos()
22 | }, [username, limit])
23 |
24 | return { repos, loading, error }
25 | }
--------------------------------------------------------------------------------
/env.example:
--------------------------------------------------------------------------------
1 | # Backend
2 | UVICORN_IP=0.0.0.0
3 | UVICORN_PORT=8000
4 | # I'm using https://requesty.ai/ for an LLM router but you can use the default: https://api.openai.com/v1
5 | LLM_ROUTER_URL="https://router.requesty.ai/v1"
6 | LLM_ROUTER_API_KEY=
7 | # Change this to an OpenAI model if not using requesty.ai
8 | LLM_MODEL="anthropic/claude-3-5-sonnet-latest"
9 | EMBEDDING_MODEL="text-embedding-3-small"
10 | OPENAI_API_KEY=
11 | FRONTEND_URL=http://localhost:5173
12 |
13 | # Postgres
14 | POSTGRES_SERVER=localhost
15 | POSTGRES_PORT=5432
16 | POSTGRES_DB=pgdb
17 | POSTGRES_USER=pguser
18 | POSTGRES_PASSWORD=docker
19 |
20 | # Redis and Rate Limiting
21 | REDIS_URL=redis://localhost:6379
22 | GLOBAL_RATE_LIMIT=1000/hour
23 | CHAT_RATE_LIMIT=30/minute
24 |
25 | # Frontend
26 | VITE_BACKEND_URL=http://localhost:8000
--------------------------------------------------------------------------------
/frontend/Dockerfile:
--------------------------------------------------------------------------------
1 | # Build stage
2 | FROM node:20-slim AS builder
3 |
4 | # Set working directory
5 | WORKDIR /app
6 |
7 | # Copy package files
8 | COPY package*.json ./
9 |
10 | # Install dependencies
11 | RUN npm ci
12 |
13 | # Copy the rest of the application
14 | COPY . .
15 |
16 | # Build the application
17 | RUN npm run build
18 |
19 | # Production stage
20 | FROM nginx:alpine
21 |
22 | # Copy custom nginx config to handle SPA routing
23 | RUN echo 'server { \
24 | listen 80; \
25 | location / { \
26 | root /usr/share/nginx/html; \
27 | index index.html; \
28 | try_files $uri $uri/ /index.html; \
29 | } \
30 | }' > /etc/nginx/conf.d/default.conf
31 |
32 | # Copy built assets from build stage
33 | COPY --from=builder /app/dist /usr/share/nginx/html
34 |
35 | # Expose port 80
36 | EXPOSE 80
37 |
38 | # Start nginx
39 | CMD ["nginx", "-g", "daemon off;"]
--------------------------------------------------------------------------------
/frontend/src/hooks/chat/useSession.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import { v4 as uuidv4 } from 'uuid';
3 |
4 | export const useSession = () => {
5 | const [sessionId, setSessionId] = useState('');
6 |
7 | useEffect(() => {
8 | initializeSession();
9 | }, []);
10 |
11 | const initializeSession = () => {
12 | const existingSessionId = localStorage.getItem('chatSessionId');
13 | if (existingSessionId) {
14 | setSessionId(existingSessionId);
15 | } else {
16 | const newSessionId = uuidv4();
17 | localStorage.setItem('chatSessionId', newSessionId);
18 | setSessionId(newSessionId);
19 | }
20 | };
21 |
22 | const startNewSession = () => {
23 | const newSessionId = uuidv4();
24 | localStorage.setItem('chatSessionId', newSessionId);
25 | setSessionId(newSessionId);
26 | return newSessionId;
27 | };
28 |
29 | return { sessionId, startNewSession };
30 | };
--------------------------------------------------------------------------------
/backend/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.12-slim
2 |
3 | # Set environment variables
4 | ENV PYTHONUNBUFFERED=1 \
5 | PYTHONDONTWRITEBYTECODE=1 \
6 | PIP_NO_CACHE_DIR=off \
7 | PIP_DISABLE_PIP_VERSION_CHECK=on \
8 | PIP_DEFAULT_TIMEOUT=100
9 |
10 | # Set working directory
11 | WORKDIR /app
12 |
13 | # Install system dependencies
14 | RUN apt-get update \
15 | && apt-get install -y --no-install-recommends \
16 | curl \
17 | build-essential \
18 | libpq-dev \
19 | && apt-get clean \
20 | && rm -rf /var/lib/apt/lists/*
21 |
22 | # Install pipenv
23 | RUN pip install --no-cache-dir pipenv
24 |
25 | # Copy Pipfile and Pipfile.lock
26 | COPY Pipfile* ./
27 |
28 | # Install dependencies
29 | RUN pipenv install --system --deploy
30 |
31 | # Copy the rest of the application
32 | COPY . .
33 |
34 | # Expose the port the app runs on
35 | EXPOSE 8000
36 |
37 | # Command to run the application
38 | CMD ["python", "-m", "run_server"]
--------------------------------------------------------------------------------
/frontend/src/hooks/useTypingEffect.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react'
2 |
3 | export const useTypingEffect = (content, speed = 30) => {
4 | const [displayedContent, setDisplayedContent] = useState('')
5 | const [isTyping, setIsTyping] = useState(true)
6 |
7 | useEffect(() => {
8 | if (!content) {
9 | setDisplayedContent('')
10 | setIsTyping(true)
11 | return
12 | }
13 |
14 | if (!isTyping) return
15 |
16 | let currentIndex = 0
17 | const textLength = content.length
18 |
19 | const typingInterval = setInterval(() => {
20 | currentIndex++
21 | if (currentIndex <= textLength) {
22 | setDisplayedContent(content.slice(0, currentIndex))
23 | } else {
24 | clearInterval(typingInterval)
25 | setIsTyping(false)
26 | }
27 | }, speed)
28 |
29 | return () => clearInterval(typingInterval)
30 | }, [content, isTyping, speed])
31 |
32 | return { displayedContent, isTyping, setIsTyping, setDisplayedContent }
33 | }
--------------------------------------------------------------------------------
/backend/fly.toml:
--------------------------------------------------------------------------------
1 | # fly.toml app configuration file generated for first-production on 2025-01-19T15:07:05+02:00
2 | #
3 | # See https://fly.io/docs/reference/configuration/ for information about how to use this file.
4 | #
5 |
6 | app = 'example-portfolio'
7 | primary_region = 'fra'
8 |
9 | [build]
10 |
11 | [env]
12 | LLM_MODEL = 'anthropic/claude-3-5-sonnet-latest'
13 | LLM_ROUTER_URL = 'https://router.requesty.ai/v1'
14 | UVICORN_IP = '0.0.0.0'
15 | UVICORN_PORT = '8000'
16 | POSTGRES_SERVER="example-postgres-with-pg-vector.flycast"
17 | POSTGRES_PORT="5432"
18 | POSTGRES_DB="postgres"
19 | POSTGRES_USER="postgres"
20 | FRONTEND_URL='https://www.example.com'
21 | EMBEDDING_MODEL="text-embedding-3-small"
22 | GLOBAL_RATE_LIMIT="1000/hour"
23 | CHAT_RATE_LIMIT="30/minute"
24 |
25 | [http_service]
26 | internal_port = 8000
27 | force_https = true
28 | auto_stop_machines = 'stop'
29 | auto_start_machines = true
30 | min_machines_running = 1
31 | processes = ['app']
32 |
33 | [[vm]]
34 | memory = '512mb'
35 | cpu_kind = 'shared'
36 | cpus = 1
37 |
--------------------------------------------------------------------------------
/frontend/src/components/layout/NavBar/DesktopNav.jsx:
--------------------------------------------------------------------------------
1 | import { NavLink } from 'react-router-dom'
2 | import { sections } from '../config/navigationConfig'
3 | import { SocialLinks } from '../shared/SocialLinks'
4 |
5 | export const DesktopNav = () => {
6 | return (
7 |
8 | {/* Navigation Links */}
9 |
10 | {Object.entries(sections).map(([key, { icon: Icon, title, path }]) => (
11 | `
15 | px-4 py-2 rounded-lg flex items-center space-x-2 transition-colors
16 | ${isActive
17 | ? 'bg-white/10 text-white'
18 | : 'text-gray-400 hover:text-[#00C8DC] hover:bg-white/5'}
19 | `}
20 | >
21 |
22 | {title}
23 |
24 | ))}
25 |
26 |
27 | {/* Social Links */}
28 |
29 |
30 | )
31 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Alon Trugman
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/frontend/src/App.jsx:
--------------------------------------------------------------------------------
1 | import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
2 | import { MainLayout } from './components/layout/MainLayout'
3 | import { HomeSection } from './components/sections/Home'
4 | import { AboutSection } from './components/sections/About'
5 | import { ProjectsSection } from './components/sections/Projects'
6 | import { BlogSection } from './components/sections/Blog'
7 | import { ConfigProvider } from './config/ConfigProvider'
8 |
9 | const App = () => {
10 | return (
11 |
12 |
13 |
14 |
15 | } />
16 | } />
17 | } />
18 | } />
19 | } />
20 | } />
21 |
22 |
23 |
24 |
25 | )
26 | }
27 |
28 | export default App
--------------------------------------------------------------------------------
/frontend/eslint.config.js:
--------------------------------------------------------------------------------
1 | import js from '@eslint/js'
2 | import globals from 'globals'
3 | import react from 'eslint-plugin-react'
4 | import reactHooks from 'eslint-plugin-react-hooks'
5 | import reactRefresh from 'eslint-plugin-react-refresh'
6 |
7 | export default [
8 | { ignores: ['dist'] },
9 | {
10 | files: ['**/*.{js,jsx}'],
11 | languageOptions: {
12 | ecmaVersion: 2020,
13 | globals: globals.browser,
14 | parserOptions: {
15 | ecmaVersion: 'latest',
16 | ecmaFeatures: { jsx: true },
17 | sourceType: 'module',
18 | },
19 | },
20 | settings: { react: { version: '18.3' } },
21 | plugins: {
22 | react,
23 | 'react-hooks': reactHooks,
24 | 'react-refresh': reactRefresh,
25 | },
26 | rules: {
27 | ...js.configs.recommended.rules,
28 | ...react.configs.recommended.rules,
29 | ...react.configs['jsx-runtime'].rules,
30 | ...reactHooks.configs.recommended.rules,
31 | 'react/jsx-no-target-blank': 'off',
32 | 'react-refresh/only-export-components': [
33 | 'warn',
34 | { allowConstantExport: true },
35 | ],
36 | },
37 | },
38 | ]
39 |
--------------------------------------------------------------------------------
/frontend/src/components/layout/shared/SocialLinks.jsx:
--------------------------------------------------------------------------------
1 | import { motion } from 'framer-motion'
2 | import { getSocialLinksWithIcons, navConfig } from '../config/navigationConfig'
3 |
4 | export const SocialLinks = ({ isMobile = false }) => {
5 | // Initialize socialLinks directly instead of using useState + useEffect
6 | const socialLinks = getSocialLinksWithIcons();
7 |
8 | return (
9 |
10 | {socialLinks.map(({ href, icon: Icon, label, color }) => (
11 | e.stopPropagation()}
23 | >
24 |
25 | {isMobile && {label}}
26 |
27 | ))}
28 |
29 | )
30 | }
--------------------------------------------------------------------------------
/frontend/src/test/mocks/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "personal": {
3 | "name": "Test User",
4 | "email": "test@example.com",
5 | "profileImageAlt": "Test User"
6 | },
7 | "social": {
8 | "github": {
9 | "url": "https://github.com/testuser"
10 | },
11 | "linkedin": {
12 | "url": "https://linkedin.com/in/alon-trugman"
13 | },
14 | "email": {
15 | "url": "mailto:alon.xt@gmail.com"
16 | }
17 | },
18 |
19 | "content": {
20 | "intro": {
21 | "cards": [
22 | {
23 | "title": "Years of Growth",
24 | "description": "Documenting my journey from 2.5 years of therapy",
25 | "icon": "Brain"
26 | },
27 | {
28 | "title": "Knowledge Collection",
29 | "description": "My insights from books, podcasts, blogs and social media posts",
30 | "icon": "BookOpen"
31 | },
32 | {
33 | "title": "AI Conversations",
34 | "description": "Based on my real experince and content",
35 | "icon": "MessageSquareText"
36 | }
37 | ],
38 | "paragraphs": [
39 | "Test paragraph",
40 | "Test paragraph 2"
41 | ]
42 | }
43 | },
44 |
45 | "chat": {
46 | "inputPlaceholder": "Ask me anything about Alon...",
47 | "initialMessage": "Hello!"
48 | }
49 | }
--------------------------------------------------------------------------------
/.github/workflows/run-tests.yml:
--------------------------------------------------------------------------------
1 | name: Run Tests
2 |
3 | on:
4 | pull_request:
5 | branches: [main]
6 | push:
7 | branches: [main]
8 |
9 | jobs:
10 | backend-tests:
11 | name: Backend Tests
12 | runs-on: ubuntu-latest
13 | defaults:
14 | run:
15 | working-directory: ./backend
16 |
17 | steps:
18 | - uses: actions/checkout@v4
19 |
20 | - name: Set up Python
21 | uses: actions/setup-python@v5
22 | with:
23 | python-version: '3.12.5'
24 |
25 | - name: Install pipenv
26 | run: pip install pipenv
27 |
28 | - name: Install dependencies
29 | run: pipenv install --dev
30 |
31 | - name: Run tests
32 | run: pipenv run pytest
33 |
34 | frontend-tests:
35 | name: Frontend Tests
36 | runs-on: ubuntu-latest
37 | defaults:
38 | run:
39 | working-directory: ./frontend
40 |
41 | steps:
42 | - uses: actions/checkout@v4
43 |
44 | - name: Set up Node.js
45 | uses: actions/setup-node@v4
46 | with:
47 | node-version: '20'
48 | cache: 'npm'
49 | cache-dependency-path: './frontend/package-lock.json'
50 |
51 | - name: Install dependencies
52 | run: npm ci
53 |
54 | - name: Run tests
55 | run: npm test
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frontend",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "vite build",
9 | "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
10 | "preview": "vite preview",
11 | "test": "vitest run",
12 | "test:watch": "vitest"
13 | },
14 | "dependencies": {
15 | "@vercel/analytics": "1.4.1",
16 | "framer-motion": "11.18.1",
17 | "lucide-react": "0.473.0",
18 | "react": "18.3.1",
19 | "react-dom": "18.3.1",
20 | "react-markdown": "9.0.3",
21 | "react-router-dom": "7.1.3",
22 | "uuid": "11.0.5"
23 | },
24 | "devDependencies": {
25 | "@eslint/js": "9.17.0",
26 | "@testing-library/jest-dom": "6.6.3",
27 | "@testing-library/react": "16.2.0",
28 | "@types/react": "18.3.18",
29 | "@types/react-dom": "18.3.5",
30 | "@vitejs/plugin-react": "4.3.4",
31 | "autoprefixer": "10.4.20",
32 | "eslint": "9.17.0",
33 | "eslint-plugin-react": "7.37.2",
34 | "eslint-plugin-react-hooks": "5.0.0",
35 | "eslint-plugin-react-refresh": "0.4.16",
36 | "globals": "15.14.0",
37 | "jsdom": "26.0.0",
38 | "postcss": "8.5.1",
39 | "tailwind-scrollbar": "3.0.4",
40 | "tailwindcss": "3.4.17",
41 | "vite": "6.0.5",
42 | "vitest": "3.0.7"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/backend/app/logic/document_indexer.py:
--------------------------------------------------------------------------------
1 | from langchain.text_splitter import MarkdownTextSplitter
2 | from langchain_openai import OpenAIEmbeddings
3 | from typing import List
4 | import hashlib
5 |
6 | class DocumentIndexer:
7 | def __init__(self, embeddings: OpenAIEmbeddings):
8 | self.embeddings = embeddings
9 | self.text_splitter = MarkdownTextSplitter(
10 | chunk_size=1000,
11 | chunk_overlap=200
12 | )
13 |
14 | def calculate_content_hash(self, content: str) -> str:
15 | """Calculate MD5 hash of document content"""
16 | return hashlib.md5(content.encode('utf-8')).hexdigest()
17 |
18 | def process_markdown(self, file_path: str) -> List[dict]:
19 | """Process markdown file into chunks with embeddings"""
20 | with open(file_path, 'r') as file:
21 | content = file.read()
22 |
23 | content_hash = self.calculate_content_hash(content)
24 | chunks = self.text_splitter.split_text(content)
25 | embeddings = self.embeddings.embed_documents(chunks)
26 |
27 | return [{
28 | 'content': chunk,
29 | 'embedding': embedding,
30 | 'metadata': {
31 | 'source': file_path,
32 | 'content_hash': content_hash
33 | }
34 | } for chunk, embedding in zip(chunks, embeddings)]
--------------------------------------------------------------------------------
/backend/run_server.py:
--------------------------------------------------------------------------------
1 | import os
2 | from app import factory
3 | from app.controllers.chat_router import ChatRouter
4 | from app.logs.logger import get_logger
5 | from app.startup.documents.init_documents import init_documents
6 | from dotenv import load_dotenv
7 | import uvicorn
8 | import asyncio
9 |
10 | logger = get_logger(__name__)
11 |
12 | async def initialize_services():
13 | """Initialize all required services and data"""
14 | document_indexer = factory.document_indexer()
15 | db_handler = factory.database_handler()
16 | await init_documents(document_indexer, db_handler)
17 |
18 | async def create_application():
19 | """Create and configure the FastAPI application"""
20 | app = factory.create_app()
21 | chat_service = factory.chat_service()
22 | router = ChatRouter(chat_service=chat_service)
23 | app.include_router(router=router.router)
24 |
25 | return app
26 |
27 | async def run_server():
28 | """Main function to start the server"""
29 | load_dotenv()
30 |
31 | app = await create_application()
32 | await initialize_services()
33 |
34 | logger.info("Starting the server")
35 | config = uvicorn.Config(
36 | app=app,
37 | host=os.getenv("UVICORN_IP"),
38 | port=int(os.getenv("UVICORN_PORT"))
39 | )
40 | server = uvicorn.Server(config)
41 | await server.serve()
42 |
43 | if __name__ == '__main__':
44 | asyncio.run(run_server())
45 |
--------------------------------------------------------------------------------
/frontend/src/config/ConfigProvider.test.jsx:
--------------------------------------------------------------------------------
1 | import { describe, it, expect, vi } from 'vitest';
2 | import { render, screen, waitFor } from '@testing-library/react';
3 | import React from 'react';
4 | import mockConfig from '../test/mocks/config.json';
5 | import { ConfigProvider, useConfig } from './ConfigProvider';
6 |
7 | // Mock the configLoader module
8 | vi.mock('./configLoader', () => ({
9 | loadConfig: vi.fn().mockResolvedValue(mockConfig)
10 | }));
11 |
12 | describe('ConfigProvider', () => {
13 | it('provides config to children when loading succeeds', async () => {
14 | // Define a test component that uses the useConfig hook
15 | const TestComponent = () => {
16 | const { config, loading, error } = useConfig();
17 |
18 | if (loading) return Loading...
;
19 | if (error) return Error: {error}
;
20 |
21 | return (
22 |
23 |
{config.personal.name}
24 |
{config.personal.email}
25 |
26 | );
27 | };
28 |
29 | render(
30 |
31 |
32 |
33 | );
34 |
35 | // Wait for the config to be loaded and displayed
36 | await waitFor(() => {
37 | expect(screen.getByTestId('name')).toHaveTextContent(mockConfig.personal.name);
38 | expect(screen.getByTestId('email')).toHaveTextContent(mockConfig.personal.email);
39 | });
40 | });
41 | });
--------------------------------------------------------------------------------
/frontend/src/hooks/chat/useMessages.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import { getChatConfig } from '../../config/configLoader';
3 |
4 | export const useMessages = () => {
5 | const [initialMessage, setInitialMessage] = useState({
6 | role: 'assistant',
7 | content: 'Loading...'
8 | });
9 |
10 | const [messages, setMessages] = useState([]);
11 |
12 | // Load the initial message from the configuration
13 | useEffect(() => {
14 | const chatConfig = getChatConfig();
15 | const initialMessage = chatConfig?.initialMessage || 'Hello! How can I help you today?';
16 |
17 | const message = {
18 | role: 'assistant',
19 | content: initialMessage
20 | };
21 |
22 | setInitialMessage(message);
23 | setMessages([message]);
24 | }, []);
25 |
26 | const addMessage = (message) => {
27 | setMessages(prev => [...prev, message]);
28 | };
29 |
30 | const updateLastMessage = (content, isTyping = true) => {
31 | setMessages(prev => {
32 | const newMessages = [...prev];
33 | if (newMessages.length > 0) {
34 | const lastMessage = newMessages[newMessages.length - 1];
35 | newMessages[newMessages.length - 1] = {
36 | ...lastMessage,
37 | content,
38 | isTyping
39 | };
40 | }
41 | return newMessages;
42 | });
43 | };
44 |
45 | const resetMessages = () => {
46 | setMessages([initialMessage]);
47 | };
48 |
49 | return {
50 | messages,
51 | addMessage,
52 | updateLastMessage,
53 | resetMessages
54 | };
55 | };
--------------------------------------------------------------------------------
/frontend/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer components {
6 | .markdown-content {
7 | @apply text-left w-full;
8 | }
9 |
10 | .markdown-content p {
11 | @apply mb-4 last:mb-0;
12 | }
13 |
14 | .markdown-content h1,
15 | .markdown-content h2,
16 | .markdown-content h3 {
17 | @apply font-bold mb-2 mt-6 first:mt-0;
18 | }
19 |
20 | .markdown-content h1 {
21 | @apply text-2xl;
22 | }
23 |
24 | .markdown-content h2 {
25 | @apply text-xl;
26 | }
27 |
28 | .markdown-content h3 {
29 | @apply text-lg;
30 | }
31 |
32 | .markdown-content pre {
33 | @apply bg-gray-700/50 rounded-lg p-3 my-4 overflow-x-auto;
34 | }
35 |
36 | .markdown-content code {
37 | @apply bg-gray-700/50 rounded px-1.5 py-0.5 font-mono text-sm;
38 | }
39 |
40 | .markdown-content pre code {
41 | @apply bg-transparent p-0;
42 | }
43 |
44 | .markdown-content ul {
45 | @apply list-disc pl-6 mb-4 space-y-2;
46 | }
47 |
48 | .markdown-content ol {
49 | @apply list-decimal pl-6 mb-4 space-y-2;
50 | }
51 |
52 | .markdown-content li {
53 | @apply pl-2;
54 | }
55 |
56 | .markdown-content li > p {
57 | @apply inline;
58 | }
59 |
60 | .markdown-content blockquote {
61 | @apply border-l-4 border-purple-500/50 pl-4 mb-4 italic;
62 | }
63 | }
64 |
65 | @layer utilities {
66 | .animate-pulse {
67 | animation: pulse 1s cubic-bezier(0.4, 0, 0.6, 1) infinite;
68 | }
69 | }
70 |
71 | @keyframes pulse {
72 | 0%, 100% { opacity: 1; }
73 | 50% { opacity: 0; }
74 | }
--------------------------------------------------------------------------------
/frontend/src/components/layout/NavBar/index.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 | import { motion } from 'framer-motion'
3 | import { Terminal, Menu, X } from 'lucide-react'
4 | import { Link } from 'react-router-dom'
5 | import { DesktopNav } from './DesktopNav'
6 | import { MobileNav } from './MobileNav'
7 |
8 | export const NavBar = () => {
9 | const [isMenuOpen, setIsMenuOpen] = useState(false)
10 |
11 | return (
12 |
41 | )
42 | }
--------------------------------------------------------------------------------
/frontend/public/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "personal": {
3 | "name": "Alon Trugman",
4 | "email": "alon.xt@gmail.com",
5 | "profileImageAlt": "Alon Trugman"
6 | },
7 | "social": {
8 | "github": {
9 | "url": "https://github.com/alonxt"
10 | },
11 | "linkedin": {
12 | "url": "https://linkedin.com/in/alon-trugman"
13 | },
14 | "email": {
15 | "url": "mailto:alon.xt@gmail.com"
16 | }
17 | },
18 | "content": {
19 | "intro": {
20 | "cards": [
21 | {
22 | "title": "Years of Growth",
23 | "description": "Documenting my life's journey",
24 | "icon": "Brain"
25 | },
26 | {
27 | "title": "Knowledge Collection",
28 | "description": "My insights from books, podcasts, blogs and social media posts",
29 | "icon": "BookOpen"
30 | },
31 | {
32 | "title": "AI Conversations",
33 | "description": "Based on my real experince and content",
34 | "icon": "MessageSquareText"
35 | }
36 | ],
37 | "paragraphs": [
38 | "For years, I've been documenting my journey. This space is where I share the intersection of my professional expertise and personal growth.",
39 | "Using my authentic writings and reflections, you can explore my documented insights and experiences. While AI isn't perfect, I hope it will share my story as genuinely as possible."
40 | ]
41 | }
42 | },
43 | "chat": {
44 | "inputPlaceholder": "Ask me anything about Alon...",
45 | "initialMessage": "Hey there! 👋 I'm Alon's AI assistant, I have access to his writings, and life insights. Feel free to ask and explore about his professional path or personal growth!"
46 | }
47 | }
--------------------------------------------------------------------------------
/backend/app/controllers/chat_router.py:
--------------------------------------------------------------------------------
1 | from fastapi import APIRouter, Depends, HTTPException
2 | from fastapi.responses import StreamingResponse
3 |
4 | from app.factory import chat_service
5 | from app.logic.chat_service import ChatService
6 | from app.logs.logger import get_logger
7 | from app.models.data_structures import ChatRequest
8 |
9 | logger = get_logger(__name__)
10 |
11 |
12 | class ChatRouter:
13 | def __init__(self,
14 | chat_service: ChatService = Depends(chat_service)
15 | ):
16 | self.router = APIRouter()
17 | self.chat_service = chat_service
18 | self.add_routes()
19 |
20 | async def _home(self):
21 | """Home endpoint"""
22 | return {"Data": "Working!"}
23 |
24 | async def _health_check(self):
25 | """Health check endpoint"""
26 | return {"status": "healthy"}
27 |
28 | async def _chat(self, chat_request: ChatRequest) -> StreamingResponse:
29 | """Chat endpoint that streams responses"""
30 | try:
31 | response = StreamingResponse(
32 | self.chat_service.stream_chat(chat_request),
33 | media_type="text/event-stream"
34 | )
35 | return response
36 |
37 | except Exception:
38 | logger.exception("Unexpected error during chat")
39 | raise HTTPException(
40 | status_code=500,
41 | detail="An internal server error occurred."
42 | )
43 |
44 | def add_routes(self):
45 | self.router.add_api_route("/", self._home, methods=["GET"])
46 | self.router.add_api_route("/health", self._health_check, methods=["GET"])
47 | self.router.add_api_route("/chat", self._chat, methods=["POST"])
48 |
--------------------------------------------------------------------------------
/frontend/src/App.test.jsx:
--------------------------------------------------------------------------------
1 | import { describe, it, expect, vi } from 'vitest';
2 | import { screen } from '@testing-library/react';
3 | import App from './App';
4 | import React from 'react';
5 | import './test/mocks';
6 | import { renderWithConfig } from './test/testUtils';
7 |
8 | // Mock the components
9 | vi.mock('./components/layout/MainLayout', () => ({
10 | MainLayout: ({ children }) => {children}
11 | }));
12 |
13 | vi.mock('./components/sections/Home', () => ({
14 | HomeSection: () => Home Section
15 | }));
16 |
17 | vi.mock('./components/sections/About', () => ({
18 | AboutSection: () => About Section
19 | }));
20 |
21 | vi.mock('./components/sections/Projects', () => ({
22 | ProjectsSection: () => Projects Section
23 | }));
24 |
25 | vi.mock('./components/sections/Blog', () => ({
26 | BlogSection: () => Blog Section
27 | }));
28 |
29 | // Mock useNavigate for testing routes
30 | vi.mock('react-router-dom', async () => {
31 | const actual = await vi.importActual('react-router-dom');
32 | return {
33 | ...actual,
34 | useNavigate: () => vi.fn()
35 | };
36 | });
37 |
38 | describe('App', () => {
39 | it('renders the main layout', () => {
40 | renderWithConfig();
41 | expect(screen.getByTestId('main-layout')).toBeInTheDocument();
42 | });
43 |
44 | it('renders the home section by default', () => {
45 | renderWithConfig();
46 | expect(screen.getByTestId('home-section')).toBeInTheDocument();
47 | });
48 |
49 | // Note: Testing route changes would require more complex setup with MemoryRouter
50 | // and is typically done in integration tests rather than unit tests
51 | });
--------------------------------------------------------------------------------
/backend/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.8'
2 |
3 | services:
4 | db:
5 | build:
6 | context: .
7 | dockerfile: Dockerfile.postgres
8 | restart: always
9 | healthcheck:
10 | test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
11 | interval: 10s
12 | retries: 5
13 | start_period: 30s
14 | timeout: 10s
15 | volumes:
16 | - app-db-data:/var/lib/postgresql/data/pgdata
17 | env_file:
18 | - .env
19 | environment:
20 | - PGDATA=/var/lib/postgresql/data/pgdata
21 | networks:
22 | - backend-network
23 | # Uncomment ports if needed in development
24 | ports:
25 | - "5432:5432"
26 |
27 | redis:
28 | image: redis:7-alpine
29 | restart: always
30 | healthcheck:
31 | test: ["CMD", "redis-cli", "ping"]
32 | interval: 10s
33 | timeout: 5s
34 | retries: 5
35 | volumes:
36 | - redis-data:/data
37 | networks:
38 | - backend-network
39 |
40 | backend:
41 | build:
42 | context: .
43 | dockerfile: Dockerfile
44 | restart: always
45 | depends_on:
46 | db:
47 | condition: service_healthy
48 | redis:
49 | condition: service_healthy
50 | env_file:
51 | - .env
52 | environment:
53 | - POSTGRES_SERVER=db
54 | - REDIS_URL=redis://redis:6379
55 | - UVICORN_IP=0.0.0.0
56 | - FRONTEND_URL=http://localhost:5173
57 | healthcheck:
58 | test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
59 | interval: 30s
60 | timeout: 10s
61 | retries: 3
62 | start_period: 20s
63 | networks:
64 | - backend-network
65 | ports:
66 | - "8000:8000"
67 |
68 | networks:
69 | backend-network:
70 | driver: bridge
71 |
72 | volumes:
73 | app-db-data:
74 | redis-data:
--------------------------------------------------------------------------------
/frontend/src/components/layout/config/navigationConfig.js:
--------------------------------------------------------------------------------
1 | import { Github, Linkedin, Mail, BookOpen, Code, BookOpen as Blog } from 'lucide-react'
2 | import { getSocialLinks } from '../../../config/configLoader'
3 |
4 | // Social links are now loaded from the configuration
5 | // The icons are still defined here since they are React components
6 | export const getSocialLinksWithIcons = () => {
7 | const social = getSocialLinks();
8 |
9 | // Create the social links array with icons
10 | return [
11 | {
12 | href: social?.github?.url || "https://github.com",
13 | icon: Github,
14 | label: "GitHub",
15 | color: "hover:text-[#2DA44E]" // GitHub color
16 | },
17 | {
18 | href: social?.linkedin?.url || "https://linkedin.com",
19 | icon: Linkedin,
20 | label: "LinkedIn",
21 | color: "hover:text-[#0A66C2]" // LinkedIn color
22 | },
23 | {
24 | href: social?.email?.url || "mailto:example@example.com",
25 | icon: Mail,
26 | label: "Email",
27 | color: "hover:text-[#00C8DC]" // Matching site theme
28 | }
29 | ];
30 | };
31 |
32 | export const navConfig = {
33 | mobileMenuTransition: {
34 | initial: { opacity: 0, height: 0 },
35 | animate: { opacity: 1, height: 'auto' },
36 | exit: { opacity: 0, height: 0 }
37 | },
38 | iconAnimation: {
39 | whileHover: { scale: 1.1 },
40 | whileTap: { scale: 0.95 }
41 | }
42 | }
43 |
44 | export const sections = {
45 | about: {
46 | icon: BookOpen,
47 | title: 'About Me',
48 | path: '/about-me',
49 | color: 'from-blue-500 to-cyan-500'
50 | },
51 | projects: {
52 | icon: Code,
53 | title: 'Projects',
54 | path: '/projects',
55 | color: 'from-emerald-500 to-green-500'
56 | },
57 | blog: {
58 | icon: Blog,
59 | title: 'Blog',
60 | path: '/blog',
61 | color: 'from-pink-500 to-rose-500'
62 | }
63 | }
--------------------------------------------------------------------------------
/frontend/src/components/sections/Home/TypingIndicator.jsx:
--------------------------------------------------------------------------------
1 | import { Bot } from 'lucide-react';
2 | import { motion } from 'framer-motion';
3 |
4 |
5 | export const TypingIndicator = ({ inline = false }) => {
6 | // If inline, render just the dots
7 | if (inline) {
8 | return (
9 |
10 | {[0, 0.15, 0.3].map((delay, i) => (
11 |
16 | ))}
17 |
18 | );
19 | }
20 |
21 | // Otherwise render the full message bubble with avatar
22 | return (
23 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | {[0, 0.15, 0.3].map((delay, i) => (
39 |
44 | ))}
45 |
46 |
47 |
48 |
49 |
50 | );
51 | };
--------------------------------------------------------------------------------
/frontend/src/components/layout/MainLayout.test.jsx:
--------------------------------------------------------------------------------
1 | import { describe, it, expect, vi } from 'vitest';
2 | import { screen } from '@testing-library/react';
3 | import { MainLayout } from './MainLayout';
4 | import React from 'react';
5 | import '../../test/mocks';
6 | import { renderWithConfig } from '../../test/testUtils';
7 |
8 | // Mock the NavBar component
9 | vi.mock('./NavBar', () => ({
10 | NavBar: () => Navbar Mock
11 | }));
12 |
13 | describe('MainLayout', () => {
14 | it('renders the NavBar component', () => {
15 | renderWithConfig(
16 |
17 | Test Content
18 |
19 | );
20 |
21 | expect(screen.getByTestId('navbar-mock')).toBeInTheDocument();
22 | });
23 |
24 | it('renders the children content', () => {
25 | renderWithConfig(
26 |
27 | Test Content
28 |
29 | );
30 |
31 | expect(screen.getByTestId('test-content')).toBeInTheDocument();
32 | expect(screen.getByText('Test Content')).toBeInTheDocument();
33 | });
34 |
35 | it('applies the correct styling classes', () => {
36 | renderWithConfig(
37 |
38 | Test Content
39 |
40 | );
41 |
42 | // Check for the main container with the correct classes
43 | const mainContainer = screen.getByText('Test Content').closest('.min-h-screen');
44 | expect(mainContainer).toHaveClass('min-h-screen');
45 | expect(mainContainer).toHaveClass('bg-black');
46 | expect(mainContainer).toHaveClass('text-white');
47 |
48 | // Check for the main content area
49 | const mainContent = screen.getByText('Test Content').closest('main');
50 | expect(mainContent).toHaveClass('pt-24');
51 | expect(mainContent).toHaveClass('px-6');
52 | expect(mainContent).toHaveClass('max-w-7xl');
53 | expect(mainContent).toHaveClass('mx-auto');
54 | });
55 | });
--------------------------------------------------------------------------------
/frontend/src/components/layout/NavBar/MobileNav.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types'
2 | import { motion, AnimatePresence } from 'framer-motion'
3 | import { NavLink } from 'react-router-dom'
4 | import { sections } from '../config/navigationConfig'
5 | import { navConfig } from '../config/navigationConfig'
6 | import { SocialLinks } from '../shared/SocialLinks'
7 |
8 | export const MobileNav = ({ isMenuOpen, setIsMenuOpen }) => {
9 | return (
10 |
11 | {isMenuOpen && (
12 |
16 | {/* Navigation Links */}
17 |
18 | {Object.entries(sections).map(([key, { icon: Icon, title, path }]) => (
19 | setIsMenuOpen(false)}
23 | className={({ isActive }) => `
24 | px-4 py-3 rounded-lg flex items-center space-x-2 transition-colors w-full
25 | ${isActive
26 | ? 'bg-white/10 text-white'
27 | : 'text-gray-400 hover:text-[#00C8DC] hover:bg-white/5'}
28 | `}
29 | >
30 |
31 | {title}
32 |
33 | ))}
34 |
35 |
36 | {/* Social Links */}
37 |
38 |
Connect with me
39 |
40 |
41 |
42 |
43 |
44 | )}
45 |
46 | )
47 | }
48 |
49 | MobileNav.propTypes = {
50 | isMenuOpen: PropTypes.bool.isRequired,
51 | setIsMenuOpen: PropTypes.func.isRequired
52 | }
--------------------------------------------------------------------------------
/frontend/src/components/sections/Home/index.test.jsx:
--------------------------------------------------------------------------------
1 | import { describe, it, expect, vi } from 'vitest';
2 | import { screen } from '@testing-library/react';
3 | import { HomeSection } from './index';
4 | import React from 'react';
5 | import '../../../test/mocks';
6 | import { renderWithConfig } from '../../../test/testUtils';
7 |
8 | // Mock the child components
9 | vi.mock('./ChatBox', () => ({
10 | ChatBox: () => React.createElement('div', { 'data-testid': 'chat-box' })
11 | }));
12 |
13 | vi.mock('./IntroSection', () => ({
14 | IntroSection: () => React.createElement('div', { 'data-testid': 'intro-section' })
15 | }));
16 |
17 | // Mock the Lucide React icon
18 | vi.mock('lucide-react', () => ({
19 | Terminal: () => React.createElement('div', { 'data-testid': 'terminal-icon' })
20 | }));
21 |
22 | describe('HomeSection', () => {
23 | it('renders the profile image and name', () => {
24 | renderWithConfig();
25 |
26 | // Check that the profile image is rendered
27 | const profileImage = screen.getByAltText('Test User');
28 | expect(profileImage).toBeInTheDocument();
29 | expect(profileImage.tagName).toBe('IMG');
30 | expect(profileImage).toHaveAttribute('src', '/profile.jpg');
31 |
32 | // Check that the name is rendered
33 | expect(screen.getByText('Test User')).toBeInTheDocument();
34 | });
35 |
36 | it('renders the Terminal icon', () => {
37 | renderWithConfig();
38 |
39 | // Check that the Terminal icon is rendered
40 | expect(screen.getByTestId('terminal-icon')).toBeInTheDocument();
41 | });
42 |
43 | it('renders the IntroSection component', () => {
44 | renderWithConfig();
45 |
46 | // Check that the IntroSection component is rendered
47 | expect(screen.getByTestId('intro-section')).toBeInTheDocument();
48 | });
49 |
50 | it('renders the ChatBox component', () => {
51 | renderWithConfig();
52 |
53 | // Check that the ChatBox component is rendered
54 | expect(screen.getByTestId('chat-box')).toBeInTheDocument();
55 | });
56 | });
--------------------------------------------------------------------------------
/frontend/src/components/sections/Home/IntroSection.test.jsx:
--------------------------------------------------------------------------------
1 | import { describe, it, expect, vi } from 'vitest';
2 | import { screen } from '@testing-library/react';
3 | import { IntroSection } from './IntroSection';
4 | import React from 'react';
5 | import '../../../test/mocks';
6 | import { renderWithConfig } from '../../../test/testUtils';
7 |
8 | // Mock the Lucide React icons
9 | vi.mock('lucide-react', () => ({
10 | Brain: () => React.createElement('div', { 'data-testid': 'brain-icon' }),
11 | BookOpen: () => React.createElement('div', { 'data-testid': 'book-open-icon' }),
12 | MessageSquareText: () => React.createElement('div', { 'data-testid': 'message-square-text-icon' })
13 | }));
14 |
15 | describe('IntroSection', () => {
16 | it('renders the section with all feature cards', () => {
17 | renderWithConfig();
18 |
19 | // Check that all three cards are rendered
20 | expect(screen.getByText('Years of Growth')).toBeInTheDocument();
21 | expect(screen.getByText('Knowledge Collection')).toBeInTheDocument();
22 | expect(screen.getByText('AI Conversations')).toBeInTheDocument();
23 |
24 | // Check that all descriptions are rendered
25 | expect(screen.getByText('Documenting my journey from 2.5 years of therapy')).toBeInTheDocument();
26 | expect(screen.getByText('My insights from books, podcasts, blogs and social media posts')).toBeInTheDocument();
27 | expect(screen.getByText('Based on my real experince and content')).toBeInTheDocument();
28 |
29 | // Check that all icons are rendered
30 | expect(screen.getByTestId('brain-icon')).toBeInTheDocument();
31 | expect(screen.getByTestId('book-open-icon')).toBeInTheDocument();
32 | expect(screen.getByTestId('message-square-text-icon')).toBeInTheDocument();
33 | });
34 |
35 | it('renders the content section with paragraphs', () => {
36 | renderWithConfig();
37 |
38 | // Check that the content paragraphs are rendered
39 | expect(screen.getByText("Test paragraph")).toBeInTheDocument();
40 | expect(screen.getByText("Test paragraph 2")).toBeInTheDocument();
41 | });
42 | });
--------------------------------------------------------------------------------
/frontend/src/components/sections/Projects/index.jsx:
--------------------------------------------------------------------------------
1 | import { motion } from 'framer-motion'
2 | import { Github } from 'lucide-react'
3 | import { useGithubRepos } from '../../../hooks/useGithubRepos'
4 |
5 | export const ProjectsSection = () => {
6 | const { repos, loading, error } = useGithubRepos('alonxt', 6)
7 |
8 | if (loading) return Loading...
9 | if (error) return Error loading projects
10 |
11 | return (
12 |
19 | {repos.map((repo, index) => (
20 |
27 |
28 |
29 |
{repo.name}
30 |
31 | {repo.description || 'No description available'}
32 |
33 |
44 |
45 |
46 | ))}
47 |
48 | )
49 | }
--------------------------------------------------------------------------------
/frontend/src/components/sections/Home/index.jsx:
--------------------------------------------------------------------------------
1 | import { motion } from 'framer-motion'
2 | import { Terminal } from 'lucide-react'
3 | import { ChatBox } from './ChatBox'
4 | import { IntroSection } from './IntroSection'
5 | import { getPersonalInfo } from '../../../config/configLoader'
6 |
7 | export const HomeSection = () => {
8 | const personalInfo = getPersonalInfo();
9 |
10 | return (
11 |
18 |
24 |
25 |

30 |
31 |
32 |
37 |
38 |
39 |
40 |
41 |
47 | {personalInfo.name}
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | )
56 | }
--------------------------------------------------------------------------------
/frontend/README.md:
--------------------------------------------------------------------------------
1 | # React Frontend Service 🎨
2 |
3 | [](https://reactjs.org/)
4 | [](https://vitejs.dev/)
5 | [](https://tailwindcss.com/)
6 |
7 |
8 | ## 🚀 Local Setup Instructions
9 |
10 | 1. Ensure Node.js 20.x is installed
11 |
12 | 2. Install dependencies:
13 | ```bash
14 | npm install
15 | ```
16 |
17 | 3. Configure environment:
18 | ```bash
19 | # Create .env file with:
20 | VITE_BACKEND_URL=http://localhost:8000 # or your backend URL
21 | ```
22 |
23 | 4. Add your personal content:
24 | - Add `about-me.md` to public folder
25 | - Add `icon.svg` to public folder
26 | - Add `profile.jpg` to public folder
27 | - Update `config.json` following the [Config Setup Guide](CONFIGURATION.md)
28 |
29 | 5. Start development server:
30 | ```bash
31 | npm run dev
32 | ```
33 |
34 | 6. Visit http://localhost:5173 to view the app 🎉
35 |
36 | ## 🛠 Development Scripts
37 |
38 | ```bash
39 | # Start development server
40 | npm run dev
41 |
42 | # Build for production
43 | npm run build
44 |
45 | # Preview production build
46 | npm run preview
47 | ```
48 |
49 | ## 🧪 Testing
50 |
51 | This project uses Vitest and React Testing Library for unit testing. Tests are organized alongside their components.
52 |
53 | ### Running Tests
54 |
55 | ```bash
56 | # Run all tests
57 | npm test
58 |
59 | # Run tests in watch mode
60 | npm run test:watch
61 |
62 | # Run tests with coverage
63 | npm test -- --coverage
64 | ```
65 |
66 | ## 🚀 Deployment on Vercel
67 |
68 | 1. Create an account on [Vercel.com](https://vercel.com)
69 |
70 | 2. Create a new project and connect to your repository
71 |
72 | 3. Configure environment variables:
73 | ```bash
74 | VITE_BACKEND_URL=https://your-backend-url
75 | ```
76 |
77 | 4. Deploy! Your app will be available at https://your-project-name.vercel.app 🎉
78 |
79 | ## 🛠 Tech Stack
80 |
81 | - React 18
82 | - Vite
83 | - TailwindCSS
84 | - Framer Motion
85 | - React Router DOM
86 | - ESLint
87 |
88 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | services:
2 | db:
3 | build:
4 | context: ./backend
5 | dockerfile: Dockerfile.postgres
6 | restart: always
7 | healthcheck:
8 | test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
9 | interval: 10s
10 | retries: 5
11 | start_period: 30s
12 | timeout: 10s
13 | volumes:
14 | - app-db-data:/var/lib/postgresql/data/pgdata
15 | env_file:
16 | - .env
17 | environment:
18 | - PGDATA=/var/lib/postgresql/data/pgdata
19 | networks:
20 | - backend-network
21 | # Only expose ports in development
22 | # ports:
23 | # - "${POSTGRES_PORT}:5432"
24 |
25 | redis:
26 | image: redis:7-alpine
27 | restart: always
28 | healthcheck:
29 | test: ["CMD", "redis-cli", "ping"]
30 | interval: 10s
31 | timeout: 5s
32 | retries: 5
33 | networks:
34 | - backend-network
35 | volumes:
36 | - redis-data:/data
37 |
38 | backend:
39 | build:
40 | context: ./backend
41 | dockerfile: Dockerfile
42 | restart: always
43 | depends_on:
44 | db:
45 | condition: service_healthy
46 | redis:
47 | condition: service_healthy
48 | env_file:
49 | - .env
50 | environment:
51 | - POSTGRES_SERVER=db
52 | - REDIS_URL=redis://redis:6379
53 | - UVICORN_IP=0.0.0.0
54 | - FRONTEND_URL=http://localhost:3000
55 | healthcheck:
56 | test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
57 | interval: 30s
58 | timeout: 10s
59 | retries: 3
60 | start_period: 20s
61 | networks:
62 | - backend-network
63 | - frontend-network
64 | ports:
65 | - "8000:8000"
66 |
67 |
68 | frontend:
69 | build:
70 | context: ./frontend
71 | dockerfile: Dockerfile
72 | restart: always
73 | depends_on:
74 | backend:
75 | condition: service_healthy
76 | networks:
77 | - frontend-network
78 | ports:
79 | - "3000:80"
80 |
81 |
82 | networks:
83 | backend-network:
84 | internal: true # This network is not accessible from outside Docker
85 | frontend-network:
86 | # External access is allowed for the frontend network
87 |
88 | volumes:
89 | app-db-data:
90 | redis-data:
--------------------------------------------------------------------------------
/backend/app/startup/documents/init_documents.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 | from app.logs.logger import get_logger
3 | from app.db.db_handler import DatabaseHandler
4 | from app.logic.document_indexer import DocumentIndexer
5 |
6 | logger = get_logger(__name__)
7 |
8 |
9 | async def init_documents(document_indexer: DocumentIndexer, db_handler: DatabaseHandler):
10 | """
11 | Process markdown documents found in the docs folder by reading each file,
12 | chunking it with DocumentService and storing it into the database.
13 | Updates existing documents if their content has changed.
14 | """
15 |
16 | docs_dir = Path(__file__).resolve().parents[3] / "docs"
17 |
18 | logger.info(f"Looking for documents in: {docs_dir}")
19 |
20 | if not docs_dir.exists():
21 | logger.error(f"Docs directory {docs_dir} does not exist.")
22 | return
23 |
24 | for file_path in docs_dir.glob("*.md"):
25 | str_path = str(file_path)
26 | logger.info(f"Checking file: {str_path}")
27 |
28 | # Read the current file content and calculate its hash
29 | with open(str_path, 'r') as file:
30 | content = file.read()
31 | current_hash = document_indexer.calculate_content_hash(content)
32 |
33 | # Check if document exists and get its stored hash
34 | if await db_handler.document_exists(str_path):
35 | stored_hash = await db_handler.get_document_hash(str_path)
36 |
37 | if stored_hash == current_hash:
38 | logger.debug(f"Skipping unchanged file: {str_path}")
39 | continue
40 |
41 | logger.info(f"Updating modified file: {str_path}")
42 | # Delete existing chunks before processing updated content
43 | await db_handler.delete_document_chunks(str_path)
44 | else:
45 | logger.info(f"Processing new file: {str_path}")
46 |
47 | # Process and store the document chunks
48 | chunks = document_indexer.process_markdown(str_path)
49 | for chunk in chunks:
50 | await db_handler.store_document_chunk(
51 | content=chunk['content'],
52 | embedding=chunk['embedding'],
53 | metadata=chunk['metadata']
54 | )
--------------------------------------------------------------------------------
/frontend/src/config/configLoader.js:
--------------------------------------------------------------------------------
1 | // Cache for the loaded configuration
2 | let configCache = null;
3 | // Flag to track if we've started loading the config
4 | let isLoading = false;
5 | // Promise that resolves when the config is loaded
6 | let configLoadingPromise = null;
7 |
8 | /**
9 | * Load the configuration from the config.json file
10 | * @returns {Promise