Making UI changes

This commit is contained in:
Ryan Chen
2025-12-24 17:12:56 -08:00
parent 70799ffb7d
commit f5e2d68cd2
17 changed files with 3262 additions and 1747 deletions

110
DEV-README.md Normal file
View File

@@ -0,0 +1,110 @@
# Development Environment Setup
This guide explains how to run the application in development mode with hot reload enabled.
## Quick Start
### Development Mode (Hot Reload)
```bash
# Start all services in development mode
docker-compose -f docker-compose.dev.yml up --build
# Or run in detached mode
docker-compose -f docker-compose.dev.yml up -d --build
```
### Production Mode
```bash
# Start production services
docker-compose up --build
```
## What's Different in Dev Mode?
### Backend (Quart/Flask)
- **Hot Reload**: Python code changes are automatically detected and the server restarts
- **Source Mounted**: Your local `services/raggr` directory is mounted as a volume
- **Debug Mode**: Flask runs with `debug=True` for better error messages
- **Environment**: `FLASK_ENV=development` and `PYTHONUNBUFFERED=1` for immediate log output
### Frontend (React + rsbuild)
- **Auto Rebuild**: Frontend automatically rebuilds when files change
- **Watch Mode**: rsbuild runs in watch mode, rebuilding to `dist/` on save
- **Source Mounted**: Your local `services/raggr/raggr-frontend` directory is mounted as a volume
- **Served by Backend**: Built files are served by the backend, no separate dev server
## Ports
- **Application**: 8080 (accessible at `http://localhost:8080` or `http://YOUR_IP:8080`)
The backend serves both the API and the auto-rebuilt frontend, making it accessible from other machines on your network.
## Useful Commands
```bash
# View logs
docker-compose -f docker-compose.dev.yml logs -f
# View logs for specific service
docker-compose -f docker-compose.dev.yml logs -f raggr-backend
docker-compose -f docker-compose.dev.yml logs -f raggr-frontend
# Rebuild after dependency changes
docker-compose -f docker-compose.dev.yml up --build
# Stop all services
docker-compose -f docker-compose.dev.yml down
# Stop and remove volumes (fresh start)
docker-compose -f docker-compose.dev.yml down -v
```
## Making Changes
### Backend Changes
1. Edit any Python file in `services/raggr/`
2. Save the file
3. The Quart server will automatically restart
4. Check logs to confirm reload
### Frontend Changes
1. Edit any file in `services/raggr/raggr-frontend/src/`
2. Save the file
3. The browser will automatically refresh (Hot Module Replacement)
4. No need to rebuild
### Dependency Changes
**Backend** (pyproject.toml):
```bash
# Rebuild the backend service
docker-compose -f docker-compose.dev.yml up --build raggr-backend
```
**Frontend** (package.json):
```bash
# Rebuild the frontend service
docker-compose -f docker-compose.dev.yml up --build raggr-frontend
```
## Troubleshooting
### Port Already in Use
If you see port binding errors, make sure no other services are running on ports 8080 or 3000.
### Changes Not Reflected
1. Check if the file is properly mounted (check docker-compose.dev.yml volumes)
2. Verify the file isn't in an excluded directory (node_modules, __pycache__)
3. Check container logs for errors
### Frontend Not Connecting to Backend
Make sure your frontend API calls point to the correct backend URL. If accessing from the same machine, use `http://localhost:8080`. If accessing from another device on the network, use `http://YOUR_IP:8080`.
## Notes
- Both services bind to `0.0.0.0` and expose ports, making them accessible on your network
- Node modules and Python cache are excluded from volume mounts to use container versions
- Database and ChromaDB data persist in Docker volumes across restarts
- Access the app from any device on your network using your host machine's IP address

45
docker-compose.dev.yml Normal file
View File

@@ -0,0 +1,45 @@
version: "3.8"
services:
raggr-backend:
build:
context: ./services/raggr
dockerfile: Dockerfile.dev
image: torrtle/simbarag:dev
ports:
- "8080:8080"
environment:
- PAPERLESS_TOKEN=${PAPERLESS_TOKEN}
- BASE_URL=${BASE_URL}
- OLLAMA_URL=${OLLAMA_URL:-http://localhost:11434}
- CHROMADB_PATH=/app/chromadb
- OPENAI_API_KEY=${OPENAI_API_KEY}
- FLASK_ENV=development
- PYTHONUNBUFFERED=1
volumes:
# Mount source code for hot reload
- ./services/raggr:/app
# Exclude node_modules and Python cache
- /app/raggr-frontend/node_modules
- /app/__pycache__
# Persist data
- chromadb_data:/app/chromadb
- database_data:/app/database
command: sh -c "chmod +x /app/startup-dev.sh && /app/startup-dev.sh"
raggr-frontend:
build:
context: ./services/raggr/raggr-frontend
dockerfile: Dockerfile.dev
environment:
- NODE_ENV=development
volumes:
# Mount source code for hot reload
- ./services/raggr/raggr-frontend:/app
# Exclude node_modules to use container's version
- /app/node_modules
command: sh -c "yarn build && yarn watch:build"
volumes:
chromadb_data:
database_data:

View File

@@ -0,0 +1,33 @@
FROM python:3.13-slim
WORKDIR /app
# Install system dependencies and uv
RUN apt-get update && apt-get install -y \
build-essential \
curl \
&& rm -rf /var/lib/apt/lists/* \
&& curl -LsSf https://astral.sh/uv/install.sh | sh
# Add uv to PATH
ENV PATH="/root/.local/bin:$PATH"
# Copy dependency files
COPY pyproject.toml ./
# Install Python dependencies using uv
RUN uv pip install --system -e .
# Create ChromaDB and database directories
RUN mkdir -p /app/chromadb /app/database
# Expose port
EXPOSE 8080
# Set environment variables
ENV PYTHONPATH=/app
ENV CHROMADB_PATH=/app/chromadb
ENV PYTHONUNBUFFERED=1
# The actual source code will be mounted as a volume
# No CMD here - will be specified in docker-compose

View File

@@ -0,0 +1,9 @@
.git
.gitignore
README.md
.DS_Store
node_modules
dist
.cache
coverage
*.log

View File

@@ -6,6 +6,7 @@
# Dist # Dist
node_modules node_modules
dist/ dist/
.yarn
# Profile # Profile
.rspack-profile-*/ .rspack-profile-*/

View File

@@ -0,0 +1 @@
nodeLinker: node-modules

View File

@@ -0,0 +1,15 @@
FROM node:20-slim
WORKDIR /app
# Copy package files
COPY package.json yarn.lock* ./
# Install dependencies
RUN yarn install
# Expose rsbuild dev server port (default 3000)
EXPOSE 3000
# The actual source code will be mounted as a volume
# CMD will be specified in docker-compose

View File

@@ -20,6 +20,7 @@
"watch": "^1.0.2" "watch": "^1.0.2"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "2.3.10",
"@rsbuild/core": "^1.5.6", "@rsbuild/core": "^1.5.6",
"@rsbuild/plugin-react": "^1.4.0", "@rsbuild/plugin-react": "^1.4.0",
"@tailwindcss/postcss": "^4.0.0", "@tailwindcss/postcss": "^4.0.0",

View File

@@ -3,4 +3,5 @@
body { body {
margin: 0; margin: 0;
font-family: Inter, Avenir, Helvetica, Arial, sans-serif; font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
background-color: #F9F5EB;
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@@ -7,7 +7,7 @@ type AnswerBubbleProps = {
export const AnswerBubble = ({ text, loading }: AnswerBubbleProps) => { export const AnswerBubble = ({ text, loading }: AnswerBubbleProps) => {
return ( return (
<div className="rounded-md bg-orange-100 p-3 sm:p-4"> <div className="rounded-md bg-orange-100 p-3 sm:p-4 w-2/3">
{loading ? ( {loading ? (
<div className="flex flex-col w-full animate-pulse gap-2"> <div className="flex flex-col w-full animate-pulse gap-2">
<div className="flex flex-row gap-2 w-full"> <div className="flex flex-row gap-2 w-full">
@@ -20,8 +20,8 @@ export const AnswerBubble = ({ text, loading }: AnswerBubbleProps) => {
</div> </div>
</div> </div>
) : ( ) : (
<div className="flex flex-col break-words overflow-wrap-anywhere"> <div className=" flex flex-col break-words overflow-wrap-anywhere text-sm sm:text-base [&>*]:break-words">
<ReactMarkdown className="text-sm sm:text-base [&>*]:break-words"> <ReactMarkdown>
{"🐈: " + text} {"🐈: " + text}
</ReactMarkdown> </ReactMarkdown>
</div> </div>

View File

@@ -2,8 +2,9 @@ import { useEffect, useState, useRef } from "react";
import { conversationService } from "../api/conversationService"; import { conversationService } from "../api/conversationService";
import { QuestionBubble } from "./QuestionBubble"; import { QuestionBubble } from "./QuestionBubble";
import { AnswerBubble } from "./AnswerBubble"; import { AnswerBubble } from "./AnswerBubble";
import { MessageInput } from "./MessageInput";
import { ConversationList } from "./ConversationList"; import { ConversationList } from "./ConversationList";
import { parse } from "node:path/win32"; import catIcon from "../assets/cat.png";
type Message = { type Message = {
text: string; text: string;
@@ -38,6 +39,7 @@ export const ChatScreen = ({ setAuthenticated }: ChatScreenProps) => {
const [showConversations, setShowConversations] = useState<boolean>(false); const [showConversations, setShowConversations] = useState<boolean>(false);
const [selectedConversation, setSelectedConversation] = const [selectedConversation, setSelectedConversation] =
useState<Conversation | null>(null); useState<Conversation | null>(null);
const [sidebarCollapsed, setSidebarCollapsed] = useState<boolean>(false);
const messagesEndRef = useRef<HTMLDivElement>(null); const messagesEndRef = useRef<HTMLDivElement>(null);
const simbaAnswers = ["meow.", "hiss...", "purrrrrr", "yowOWROWWowowr"]; const simbaAnswers = ["meow.", "hiss...", "purrrrrr", "yowOWROWWowowr"];
@@ -176,37 +178,81 @@ export const ChatScreen = ({ setAuthenticated }: ChatScreenProps) => {
}; };
return ( return (
<div className="h-screen bg-opacity-20"> <div className="h-screen flex flex-row bg-[#F9F5EB]">
<div className="bg-white/85 h-screen"> {/* Sidebar - Expanded */}
<div className="flex flex-row justify-center py-4"> <aside className={`hidden md:flex md:flex-col bg-white border-r border-gray-200 p-4 overflow-y-auto transition-all duration-300 ${sidebarCollapsed ? 'w-20' : 'w-64'}`}>
<div className="flex flex-col gap-4 w-full px-4 sm:w-11/12 sm:max-w-2xl lg:max-w-4xl sm:px-0"> {!sidebarCollapsed ? (
<div className="flex flex-col sm:flex-row gap-3 sm:gap-0 sm:justify-between"> <>
<header className="flex flex-row justify-center gap-2 sticky top-0 z-10 bg-white"> <div className="flex flex-row items-center gap-2 mb-6">
<h1 className="text-2xl sm:text-3xl">ask simba!</h1> <img
</header> src={catIcon}
<div className="flex flex-row gap-2 justify-center sm:justify-end"> alt="Simba"
<button className="cursor-pointer hover:opacity-80"
className="p-2 h-11 border border-green-400 bg-green-200 hover:bg-green-400 cursor-pointer rounded-md text-sm sm:text-base" onClick={() => setSidebarCollapsed(true)}
onClick={() => setShowConversations(!showConversations)} />
> <h2 className="text-3xl font-semibold">asksimba!</h2>
{showConversations
? "hide conversations"
: "show conversations"}
</button>
<button
className="p-2 h-11 border border-red-400 bg-red-200 hover:bg-red-400 cursor-pointer rounded-md text-sm sm:text-base"
onClick={() => setAuthenticated(false)}
>
logout
</button>
</div> </div>
</div>
{showConversations && (
<ConversationList <ConversationList
conversations={conversations} conversations={conversations}
onCreateNewConversation={handleCreateNewConversation} onCreateNewConversation={handleCreateNewConversation}
onSelectConversation={handleSelectConversation} onSelectConversation={handleSelectConversation}
/> />
<div className="mt-auto pt-4">
<button
className="w-full p-2 border border-red-400 bg-red-200 hover:bg-red-400 cursor-pointer rounded-md text-sm"
onClick={() => setAuthenticated(false)}
>
logout
</button>
</div>
</>
) : (
<div className="flex flex-col items-center gap-4">
<img
src={catIcon}
alt="Simba"
className="cursor-pointer hover:opacity-80"
onClick={() => setSidebarCollapsed(false)}
/>
</div>
)}
</aside>
{/* Main chat area */}
<div className="flex-1 flex flex-col h-screen overflow-hidden">
{/* Mobile header */}
<header className="md:hidden flex flex-row justify-between items-center gap-3 p-4 border-b border-gray-200 bg-white">
<div className="flex flex-row items-center gap-2">
<img src={catIcon} alt="Simba" className="w-10 h-10" />
<h1 className="text-xl">asksimba!</h1>
</div>
<div className="flex flex-row gap-2">
<button
className="p-2 border border-green-400 bg-green-200 hover:bg-green-400 cursor-pointer rounded-md text-sm"
onClick={() => setShowConversations(!showConversations)}
>
{showConversations ? "hide" : "show"}
</button>
<button
className="p-2 border border-red-400 bg-red-200 hover:bg-red-400 cursor-pointer rounded-md text-sm"
onClick={() => setAuthenticated(false)}
>
logout
</button>
</div>
</header>
{/* Messages area */}
<div className="flex-1 overflow-y-auto px-4 py-4">
<div className="max-w-2xl mx-auto flex flex-col gap-4">
{showConversations && (
<div className="md:hidden">
<ConversationList
conversations={conversations}
onCreateNewConversation={handleCreateNewConversation}
onSelectConversation={handleSelectConversation}
/>
</div>
)} )}
{messages.map((msg, index) => { {messages.map((msg, index) => {
if (msg.speaker === "simba") { if (msg.speaker === "simba") {
@@ -215,38 +261,22 @@ export const ChatScreen = ({ setAuthenticated }: ChatScreenProps) => {
return <QuestionBubble key={index} text={msg.text} />; return <QuestionBubble key={index} text={msg.text} />;
})} })}
<div ref={messagesEndRef} /> <div ref={messagesEndRef} />
<footer className="flex flex-col gap-2 sticky bottom-0">
<div className="flex flex-row justify-between gap-2 grow">
<textarea
className="p-3 sm:p-4 border border-blue-200 rounded-md grow bg-white min-h-[44px] resize-y"
onChange={handleQueryChange}
onKeyDown={handleKeyDown}
value={query}
rows={2}
placeholder="Type your message... (Press Enter to send, Shift+Enter for new line)"
/>
</div> </div>
<div className="flex flex-row justify-between gap-2 grow">
<button
className="p-3 sm:p-4 min-h-[44px] border border-blue-400 bg-blue-200 hover:bg-blue-400 cursor-pointer rounded-md flex-grow text-sm sm:text-base"
onClick={() => handleQuestionSubmit()}
type="submit"
>
Submit
</button>
</div> </div>
<div className="flex flex-row justify-center gap-2 grow items-center">
<input {/* Input area */}
type="checkbox" <footer className="p-4 bg-[#F9F5EB]">
onChange={(event) => setSimbaMode(event.target.checked)} <div className="max-w-2xl mx-auto">
className="w-5 h-5 cursor-pointer" <MessageInput
query={query}
handleQueryChange={handleQueryChange}
handleKeyDown={handleKeyDown}
handleQuestionSubmit={handleQuestionSubmit}
setSimbaMode={setSimbaMode}
/> />
<p className="text-sm sm:text-base">simba mode?</p>
</div> </div>
</footer> </footer>
</div> </div>
</div> </div>
</div>
</div>
); );
}; };

View File

@@ -52,7 +52,7 @@ export const ConversationList = ({
className="border-blue-400 bg-indigo-300 hover:bg-indigo-200 cursor-pointer rounded-md p-3 min-h-[44px] flex items-center" className="border-blue-400 bg-indigo-300 hover:bg-indigo-200 cursor-pointer rounded-md p-3 min-h-[44px] flex items-center"
onClick={() => onSelectConversation(conversation)} onClick={() => onSelectConversation(conversation)}
> >
<p className="text-sm sm:text-base break-words"> <p className="text-sm sm:text-base truncate w-full">
{conversation.title} {conversation.title}
</p> </p>
</div> </div>

View File

@@ -0,0 +1,43 @@
import { useEffect, useState, useRef } from "react";
type MessageInputProps = {
handleQueryChange: (event: React.ChangeEvent<HTMLTextAreaElement>) => void;
handleKeyDown: (event: React.ChangeEvent<HTMLTextAreaElement>) => void;
handleQuestionSubmit: () => void;
setSimbaMode: (sdf: boolean) => void;
query: string;
}
export const MessageInput = ({query, handleKeyDown, handleQueryChange, handleQuestionSubmit, setSimbaMode}: MessageInputProps) => {
return (
<div className="flex flex-col gap-4 sticky bottom-0 bg-[#3D763A] p-6 rounded-xl">
<div className="flex flex-row justify-between grow">
<textarea
className="p-3 sm:p-4 border border-blue-200 rounded-md grow bg-[#F9F5EB] min-h-[44px] resize-y"
onChange={handleQueryChange}
onKeyDown={handleKeyDown}
value={query}
rows={2}
placeholder="Type your message... (Press Enter to send, Shift+Enter for new line)"
/>
</div>
<div className="flex flex-row justify-between gap-2 grow">
<button
className="p-3 sm:p-4 min-h-[44px] border border-blue-400 bg-[#EDA541] hover:bg-blue-400 cursor-pointer rounded-md flex-grow text-sm sm:text-base"
onClick={() => handleQuestionSubmit()}
type="submit"
>
Submit
</button>
</div>
<div className="flex flex-row justify-center gap-2 grow items-center">
<input
type="checkbox"
onChange={(event) => setSimbaMode(event.target.checked)}
className="w-5 h-5 cursor-pointer"
/>
<p className="text-sm sm:text-base">simba mode?</p>
</div>
</div>
);
}

View File

@@ -4,7 +4,7 @@ type QuestionBubbleProps = {
export const QuestionBubble = ({ text }: QuestionBubbleProps) => { export const QuestionBubble = ({ text }: QuestionBubbleProps) => {
return ( return (
<div className="rounded-md bg-stone-200 p-3 sm:p-4 break-words overflow-wrap-anywhere text-sm sm:text-base"> <div className="w-2/3 rounded-md bg-stone-200 p-3 sm:p-4 break-words overflow-wrap-anywhere text-sm sm:text-base ml-auto">
🤦: {text} 🤦: {text}
</div> </div>
); );

File diff suppressed because it is too large Load Diff

31
services/raggr/startup-dev.sh Executable file
View File

@@ -0,0 +1,31 @@
#!/bin/bash
set -e
echo "Initializing database directories..."
mkdir -p /app/chromadb /app/database
echo "Waiting for frontend to build..."
while [ ! -f /app/raggr-frontend/dist/index.html ]; do
sleep 1
done
echo "Frontend built successfully!"
echo "Running database migrations..."
aerich upgrade
echo "Initializing visited.db with indexed_documents table..."
python3 -c "
import sqlite3
conn = sqlite3.connect('database/visited.db')
c = conn.cursor()
c.execute('CREATE TABLE IF NOT EXISTS indexed_documents (id INTEGER PRIMARY KEY AUTOINCREMENT, paperless_id INTEGER)')
conn.commit()
conn.close()
print('Database initialized successfully')
"
echo "Starting reindex process..."
python main.py "" --reindex || echo "Reindex failed, continuing anyway..."
echo "Starting Flask application in debug mode..."
python app.py