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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
# Git and OS files
.git
.DS_Store
*.json
!server/*.json

# Secrets
.env
server/.env

# VSCode
.vscode/
Expand All @@ -15,9 +18,10 @@ __pycache__/
.venv/
server/.venv/

# Tests
# Tests & Infrastructure
server/tests/
pytest.ini
.github/

# Client folder
client/
39 changes: 34 additions & 5 deletions client/components/Chat.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"use client";
import { useState } from "react";
import { useEffect, useState } from "react";
import ReactMarkdown from "react-markdown";
import { useAuth } from "@/context/AuthContext";

Expand All @@ -8,11 +8,40 @@ interface Message {
content: string;
}

const API_URL = process.env.NEXT_PUBLIC_API_URL;

export default function Chat() {
const { user } = useAuth();
const [input, setInput] = useState("");
const [messages, setMessages] = useState<Message[]>([]);
const [isLoading, setIsLoading] = useState(false);

useEffect(() => {
const fetchChatHistory = async () => {
if (!user) return;
setIsLoading(true);
try{
const res = await fetch(`${API_URL}/chats/${user.uid}`)
if (!res.ok) {throw new Error("Failed to load history")}
const history = await res.json();

const historicalMessages = history.flatMap((session: any) =>
session.messages.map((m: any) => ({
role: m.role,
content: m.content
}))
).reverse(); // if the api orders newest-first but ui wants oldest-first

setMessages(historicalMessages);
} catch (error) {
console.error("History fetch error:", error)
} finally {
setIsLoading(false)
}
}

fetchChatHistory()
}, [user])

const handleSend = async () => {
if (!input.trim()) return;
Expand All @@ -24,21 +53,21 @@ export default function Chat() {
let res;
try {
if (!user) {
res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/chat`, {
res = await fetch(`${API_URL}/chat`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
message: userMessage.content,
userId: "None",
user_id: "None",
}),
});
} else {
res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/chat`, {
res = await fetch(`${API_URL}/chat`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
message: userMessage.content,
userId: user?.uid,
user_id: user?.uid,
}),
});
}
Expand Down
47 changes: 44 additions & 3 deletions client/components/ResumeBuilder.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
"use client";
import { useState } from "react";
import { useEffect, useState } from "react";
import type { MouseEvent } from "react";
import { useAuth } from "@/context/AuthContext";
import ResumeDisplay from "./ResumeDisplay";

// The URL for the deployed backend.
const API_URL = process.env.NEXT_PUBLIC_API_URL;

export default function ResumeBuilder() {
Expand All @@ -17,6 +16,7 @@ export default function ResumeBuilder() {
const [tailoredResume, setTailoredResume] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [history, setHistory] = useState([])

const handleSubmit = async (e: MouseEvent<HTMLButtonElement>) => {
if (!user) return;
Expand All @@ -29,7 +29,7 @@ export default function ResumeBuilder() {
const formData = new FormData();
formData.append("base_resume", resume);
formData.append("job_description", jobDescription);
formData.append("userId", user.uid);
formData.append("user_id", user.uid);

try {
// Make the API call to /resumes endpoint
Expand Down Expand Up @@ -60,6 +60,47 @@ export default function ResumeBuilder() {
}
};

useEffect(() => {
if (!user) {
setHistory([]); // Clear history on logout
return
};

const fetchResumeHistory = async () => {
setIsLoading(true);

try {
const res = await fetch(`${API_URL}/resumes/${user.uid}`)

if (!res.ok) throw new Error("Filed to fetch resume history")

const history = await res.json()

const historicalMessages = history.map((session: any) => ({
jobDescription: session.jobDescription,
originalResume: session.originalResume,
tailoredResume: session.tailoredResume,
})
).reverse()

setHistory(historicalMessages)

// Show latest resume on load
if (historicalMessages.length > 0) {
setTailoredResume(historicalMessages[0].tailored_resume)
}

} catch (error) {
console.error("History fetch error:", error)
} finally {
setIsLoading(false)
}
};

fetchResumeHistory()
}, [user])


return (
<form className="flex w-full max-w-4xl flex-col gap-8 animate-in">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
Expand Down
6 changes: 3 additions & 3 deletions server/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ WORKDIR /app
# This fixes "ModuleNotFoundError" by allowing main.py to import utils.py
ENV PYTHONPATH "${PYTHONPATH}:/app"

# Copy the requirements file *from the server subdirectory*
COPY server/requirements.txt .
# Copy the requirements file from the server subdirectory*
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copy the entire server directory *into* the /app directory
COPY server/ .
COPY . .

# Tell Docker that the container will listen on port 8000
EXPOSE 8000
Expand Down
5 changes: 5 additions & 0 deletions server/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ async def lifespan(app: FastAPI):

# --- API Endpoints ---
# --- Post Chat Endpoint ---
@app.get("/")
async def root():
return {"status": "ApplyAI API is online", "version": "1.0.0"}


@app.post(
"/chat", response_model=ChatResponse, summary="Handles Chat with Gemini AI Agent"
)
Expand Down
4 changes: 2 additions & 2 deletions server/tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,10 @@ def sample_job_data():
def test_client_is_working(client):
"""
A simple "sanity check" test to make sure the client is working.
We don't have a "/" route, so we expect a 404 "Not Found" error.
We now have a "/" route, so we expect a 200 "OK" success code.
"""
response = client.get("/")
assert response.status_code == 404
assert response.status_code == 200


def test_chat_endpoint(client, mocker):
Expand Down
Loading