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
10 changes: 10 additions & 0 deletions backend/app/chat_history.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,13 @@ async def update_title(chat_id: str, title: str):
{"_id": ObjectId(chat_id)},
{"$set": {"title": title}}
)

@staticmethod
async def delete_chat(chat_id: str, user_id: str):
if db.db is None: return None
result = await db.db.chats.delete_one({
"_id": ObjectId(chat_id),
"user_id": ObjectId(user_id)
})
return result.deleted_count > 0

75 changes: 55 additions & 20 deletions backend/app/routes/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@
from ..users import User
from pydantic import BaseModel, EmailStr
from typing import Dict, Any, Optional
import os
import shutil
import uuid
import base64

router = APIRouter()

Expand Down Expand Up @@ -54,6 +52,14 @@ async def login(form_data: OAuth2PasswordRequestForm = Depends()):
user_data = {k: v for k, v in user.items() if k != "hashed_password"}
if "_id" in user_data:
user_data["_id"] = str(user_data["_id"])

# Convert avatar data to data URL if present
if "avatar_data" in user_data and "avatar_content_type" in user_data:
avatar_data_url = f"data:{user_data['avatar_content_type']};base64,{user_data['avatar_data']}"
user_data["avatar_url"] = avatar_data_url
# Remove raw data from response
del user_data["avatar_data"]
del user_data["avatar_content_type"]

return {
"access_token": access_token,
Expand All @@ -66,6 +72,15 @@ async def read_users_me(current_user = Depends(get_current_user)):
# Convert ObjectId to str for JSON serialization
if "_id" in current_user:
current_user["_id"] = str(current_user["_id"])

# Convert avatar data to data URL if present
if "avatar_data" in current_user and "avatar_content_type" in current_user:
avatar_data_url = f"data:{current_user['avatar_content_type']};base64,{current_user['avatar_data']}"
current_user["avatar_url"] = avatar_data_url
# Remove raw data from response
del current_user["avatar_data"]
del current_user["avatar_content_type"]

return current_user

@router.put("/profile")
Expand All @@ -89,6 +104,13 @@ async def update_profile(user_update: UserUpdate, current_user = Depends(get_cur
# Remove password
if "hashed_password" in updated_user:
del updated_user["hashed_password"]

# Convert avatar data to data URL if present
if "avatar_data" in updated_user and "avatar_content_type" in updated_user:
avatar_data_url = f"data:{updated_user['avatar_content_type']};base64,{updated_user['avatar_data']}"
updated_user["avatar_url"] = avatar_data_url
del updated_user["avatar_data"]
del updated_user["avatar_content_type"]

return updated_user
except Exception as e:
Expand All @@ -100,30 +122,43 @@ async def upload_avatar(file: UploadFile = File(...), current_user = Depends(get
try:
email = current_user["email"]

# Ensure uploads directory exists
current_dir = os.path.dirname(os.path.abspath(__file__))
storage_dir = os.path.abspath(os.path.join(current_dir, "..", "..", "storage", "avatars"))
os.makedirs(storage_dir, exist_ok=True)
# Validate file type
allowed_types = ["image/jpeg", "image/jpg", "image/png", "image/gif", "image/webp"]
if file.content_type not in allowed_types:
raise HTTPException(status_code=400, detail="Invalid file type. Only JPEG, PNG, GIF, and WebP images are allowed.")

# Generate unique filename
file_extension = os.path.splitext(file.filename)[1]
filename = f"{uuid.uuid4()}{file_extension}"
file_path = os.path.join(storage_dir, filename)
# Read file content
file_content = await file.read()

# Save file
with open(file_path, "wb") as buffer:
shutil.copyfileobj(file.file, buffer)

# Update user avatar URL
# URL should be relative path served by static
avatar_url = f"/static/avatars/{filename}"
# Validate file size (max 5MB)
max_size = 5 * 1024 * 1024 # 5MB
if len(file_content) > max_size:
raise HTTPException(status_code=400, detail="File too large. Maximum size is 5MB.")

success = await User.update_user(email, {"avatar_url": avatar_url})
# Convert to base64
import base64
avatar_base64 = base64.b64encode(file_content).decode('utf-8')

# Create data URL for immediate use
avatar_data_url = f"data:{file.content_type};base64,{avatar_base64}"

# Update user with avatar data in database
update_data = {
"avatar_data": avatar_base64,
"avatar_content_type": file.content_type
}

success = await User.update_user(email, update_data)
if not success:
raise HTTPException(status_code=400, detail="Failed to update user avatar")

return {"avatar_url": avatar_url}
return {
"avatar_url": avatar_data_url,
"message": "Avatar uploaded successfully"
}

except HTTPException:
raise
except Exception as e:
print(f"Avatar upload error: {e}")
raise HTTPException(status_code=500, detail=str(e))
37 changes: 37 additions & 0 deletions backend/app/routes/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,40 @@ async def get_chat(chat_id: str, current_user = Depends(get_current_user)):
return chat
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))

@router.patch("/{chat_id}")
async def rename_chat(chat_id: str, request: dict = Body(...), current_user = Depends(get_current_user)):
try:
user_id = str(current_user["_id"])
# Verify chat belongs to user
chat = await ChatHistory.get_chat(chat_id, user_id)
if not chat:
raise HTTPException(status_code=404, detail="Chat not found")

new_title = request.get("title")
if not new_title:
raise HTTPException(status_code=400, detail="Title is required")

await ChatHistory.update_title(chat_id, new_title)
return {"success": True, "chat_id": chat_id, "title": new_title}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))

@router.delete("/{chat_id}")
async def delete_chat(chat_id: str, current_user = Depends(get_current_user)):
try:
user_id = str(current_user["_id"])
# Verify chat belongs to user
chat = await ChatHistory.get_chat(chat_id, user_id)
if not chat:
raise HTTPException(status_code=404, detail="Chat not found")

await ChatHistory.delete_chat(chat_id, user_id)
return {"success": True, "chat_id": chat_id}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))

68 changes: 65 additions & 3 deletions frontend/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Sheet, SheetContent, SheetTrigger } from "./components/ui/sheet";
import { DialogTitle, DialogDescription } from "./components/ui/dialog";
import UserMenu from "./components/UserMenu";
import ProfileModal from "./components/ProfileModal";
import ChatHistoryItem from "./components/ChatHistoryItem";
import { PanelLeft, X, Database, LogOut, SquarePen, Sun, Moon } from "lucide-react";

function App() {
Expand Down Expand Up @@ -200,6 +201,42 @@ function App() {
}
};

const handleRenameChat = async (chatId, newTitle) => {
try {
const res = await fetch(`${API_BASE_URL}/api/chat/${chatId}`, {
method: 'PATCH',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ title: newTitle })
});
if (res.ok) {
fetchHistory(); // Refresh list
}
} catch (e) {
console.error("Failed to rename chat", e);
}
};

const handleDeleteChat = async (chatId) => {
try {
const res = await fetch(`${API_BASE_URL}/api/chat/${chatId}`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${token}` }
});
if (res.ok) {
fetchHistory(); // Refresh list
// If deleting current chat, start new one
if (currentChatId === chatId) {
startNewChat();
}
}
} catch (e) {
console.error("Failed to delete chat", e);
}
};

const handleSend = async (text) => {
setMessages((prev) => [...prev, { role: "user", content: text }]);
setIsTyping(true);
Expand Down Expand Up @@ -345,7 +382,20 @@ function App() {
</button>
</div>


{/* Chat History List */}
<div className="flex-1 overflow-y-auto space-y-1 mb-2">
{chatHistory.map((chat) => (
<ChatHistoryItem
key={chat.id}
chat={chat}
isActive={currentChatId === chat.id}
onClick={() => loadChat(chat.id)}
onRename={(newTitle) => handleRenameChat(chat.id, newTitle)}
onDelete={() => handleDeleteChat(chat.id)}
isDarkMode={isDarkMode}
/>
))}
</div>

{/* User Area */}
<div className={`border-t pt-2 pb-2 ${isDarkMode ? "border-gray-700" : "border-gray-200"}`}>
Expand Down Expand Up @@ -376,8 +426,20 @@ function App() {
<span>New chat</span>
</button>



{/* Chat History List */}
<div className="flex-1 overflow-y-auto space-y-1">
{chatHistory.map((chat) => (
<ChatHistoryItem
key={chat.id}
chat={chat}
isActive={currentChatId === chat.id}
onClick={() => loadChat(chat.id)}
onRename={(newTitle) => handleRenameChat(chat.id, newTitle)}
onDelete={() => handleDeleteChat(chat.id)}
isDarkMode={isDarkMode}
/>
))}
</div>

</div>

Expand Down
Loading
Loading