Skip to content
Open
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
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ wheels/

# Virtual environments
.venv

# Custom
*_data/
*.epub
36 changes: 35 additions & 1 deletion server.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,51 @@
import pickle
from functools import lru_cache
from typing import Optional

from groq import Groq
from pydantic import BaseModel
import dotenv
dotenv.load_dotenv()
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import HTMLResponse, FileResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from fastapi.responses import StreamingResponse


from reader3 import Book, BookMetadata, ChapterContent, TOCEntry

app = FastAPI()
templates = Jinja2Templates(directory="templates")

class ChatRequest(BaseModel):
message: str

client = Groq(api_key=os.getenv("GROQ_KEY"))

@app.post("/chat-stream")
async def chat_stream(req: ChatRequest):
def generate():
stream = client.chat.completions.create(
model="llama-3.1-8b-instant",
stream=True,
messages=[{"role": "user", "content": req.message}],
)

for chunk in stream:
if chunk.choices and chunk.choices[0].delta:
delta = chunk.choices[0].delta.content

if delta:
# Groq may return list or string
if isinstance(delta, list):
for item in delta:
if "text" in item:
yield item["text"]
else:
yield delta

return StreamingResponse(generate(), media_type="text/plain")

# Where are the book folders located?
BOOKS_DIR = "."

Expand Down
133 changes: 132 additions & 1 deletion templates/reader.html
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,84 @@
.nav-btn { text-decoration: none; color: #3498db; font-weight: bold; padding: 10px 20px; border: 1px solid #3498db; border-radius: 4px; transition: all 0.2s; }
.nav-btn:hover { background: #3498db; color: white; }
.nav-btn.disabled { opacity: 0.5; pointer-events: none; border-color: #ccc; color: #ccc; }
#chatbox {
position: fixed;
bottom: 20px;
right: 20px;
width: 320px;
background: #ffffffee;
backdrop-filter: saturate(180%) blur(20px);
border: 1px solid #e5e5e5;
border-radius: 14px;
box-shadow: 0 10px 25px rgba(0,0,0,0.08);
overflow: hidden;
}

#chat-header {
padding: 12px 16px;
font-size: 15px;
font-weight: 600;
color: #111;
border-bottom: 1px solid #eaeaea;
}

#messages {
height: 230px;
overflow-y: auto;
padding: 12px;
font-size: 14px;
color: #222;
}

.bubble-user {
background: #007aff;
color: white;
padding: 8px 12px;
border-radius: 14px;
margin: 8px 0;
max-width: 80%;
margin-left: auto;
display: inline-block;
}

.bubble-bot {
background: #f2f2f7;
color: #333;
padding: 8px 12px;
border-radius: 14px;
margin: 8px 0;
max-width: 80%;
display: inline-block;
}

#chat-input-area {
display: flex;
gap: 8px;
padding: 10px;
border-top: 1px solid #eaeaea;
background: white;
}

#chatInput {
flex: 1;
padding: 10px;
border-radius: 12px;
border: 1px solid #ccc;
font-size: 14px;
}

#sendBtn {
padding: 10px 14px;
background: #007aff;
color: white;
border: none;
border-radius: 12px;
cursor: pointer;
font-size: 14px;
transition: 0.2s;
}

#sendBtn:hover { background: #0063d6; }
</style>
</head>
<body>
Expand Down Expand Up @@ -120,8 +197,62 @@
</div>
</div>
</div>
<div id="chatbox">
<div id="chat-header">Chat with Groq</div>

<div id="messages"></div>

<div id="chat-input-area">
<input id="chatInput" placeholder="Message…" />
<button id="sendBtn" onclick="sendMessage()">➤</button>
</div>
</div>

<script>

async function sendMessage() {
const input = document.getElementById("chatInput");
const text = input.value.trim();
if (!text) return;

const messages = document.getElementById("messages");

// User bubble
const userBubble = document.createElement("div");
userBubble.className = "bubble-user";
userBubble.textContent = text;
messages.appendChild(userBubble);

messages.scrollTop = messages.scrollHeight;

input.value = "";

// Fetch stream
const response = await fetch("/chat-stream", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message: text })
});

const botBubble = document.createElement("div");
botBubble.className = "bubble-bot";
messages.appendChild(botBubble);

const reader = response.body.getReader();
const decoder = new TextDecoder();

while (true) {
const { value, done } = await reader.read();
if (done) break;

botBubble.textContent += decoder.decode(value);
messages.scrollTop = messages.scrollHeight;
}
}

document.getElementById("chatInput").addEventListener("keydown", e => {
if (e.key === "Enter") sendMessage();
});
// Helper to map TOC filenames to Spine Indices
// Pass the spine data from python to JS
const spineMap = {
Expand Down Expand Up @@ -151,4 +282,4 @@
}
</script>
</body>
</html>
</html>