diff --git a/examples/bookmind_app/README.md b/examples/bookmind_app/README.md new file mode 100644 index 000000000..6d4404e47 --- /dev/null +++ b/examples/bookmind_app/README.md @@ -0,0 +1,112 @@ +# BookMind + +[![thumbnail](https://github.com/user-attachments/assets/11c6f1f3-59db-4638-9b1a-68f1d25efec4)](https://youtu.be/DL4-DswxfEM) + +BookMind is a web application that allows users to explore character relationships and storylines in books using AI-powered visualizations. The application provides interactive mind maps, AI chatbots for deep questions, book summaries, and community contributions. + +## Features + +- Interactive Mind Maps: Visualize relationships between characters and plot elements. +- AI Chatbot: Ask deep questions about the book and get insightful answers. +- Book Summaries: Get concise overviews of plots and themes. +- Community Contributions: Add and refine maps with fellow book lovers. + +## Prerequisites + +- Node.js +- Python >= 3.10 +- LlamaStack server running locally +- Environment variables: + - LLAMA_STACK_PORT + - INFERENCE_MODEL + - REACT_APP_GOOGLE_BOOKS_API_KEY + +## Getting Started + +### Run llama-stack + +1. Setting up Ollama server + Please check the [Ollama Documentation](https://github.com/ollama/ollama) on how to install and run Ollama. After installing Ollama, you need to run `ollama serve` to start the server. + +``` +export INFERENCE_MODEL="meta-llama/Llama-3.2-3B-Instruct" + +# ollama names this model differently, and we must use the ollama name when loading the model +export OLLAMA_INFERENCE_MODEL="llama3.2:3b-instruct-fp16" +ollama run $OLLAMA_INFERENCE_MODEL --keepalive 60m +``` + +2. Running `llama-stack` server + +``` +pip install llama-stack + +export LLAMA_STACK_PORT=5000 + +# This builds llamastack-ollama conda environment +llama stack build --template ollama --image-type conda + +conda activate llamastack-ollama + +llama stack run \ + --port $LLAMA_STACK_PORT \ + --env INFERENCE_MODEL=$INFERENCE_MODEL \ + --env OLLAMA_URL=http://localhost:11434 \ + ollama +``` + +### Backend Setup + +1. Install dependencies: + +``` +cd server +pip install -r requirements.txt +``` + +2. Set `.env` in the `server` directory + +You should modify the name of `example.env` to `.env` in the `server` directory. +**Modify INFERENCE_MODEL in example.env with yours.** + +3. Run the server: + +``` +python server.py +``` + +### Frontend Setup + +1. Set up GOOGLE_BOOKS_API_KEY: + +You should rename `example.env` with `.env` and replace `{YOUR_API_KEY}` with your [google_books_api_key](https://developers.google.com/books/docs/v1/using) after getting your api key. + +``` +REACT_APP_GOOGLE_BOOKS_API_KEY={YOUR_API_KEY} +``` + +2. Install dependencies and run the application: + +``` +npm install +npm start +``` + +## Usage + +1. Initialize Memory: Upload your book or choose from the library to initialize memory. +2. AI Analysis: The AI analyzes the book and generates a mind map. +3. Explore Insights: Explore relationships, themes, and Q&A insights. + +## What did we use Llama-stack in BookMind? + +1️⃣ Llama Inference models: You can start a LLM application using various LLM services easily. +2️⃣ RAG with FAISS: We leveraged FAISS in Llama-stack for Retrieval-Augmented Generation, enabling real-time responses to character relationship questions. +3️⃣ Multi-Hop Reasoning: Our system performs sequential inference—first extracting characters and relationships, then generating graphized mind map data in JSON for visual storytelling. + +## Contributors + +[Original Repo](https://github.com/seyeong-han/BookMind) +[seyeong-han](https://github.com/seyeong-han) +[sunjinj](https://github.com/SunjinJ) +[WonHaLee](https://github.com/WonHaLee) diff --git a/examples/bookmind_app/example.env b/examples/bookmind_app/example.env new file mode 100644 index 000000000..41d80c076 --- /dev/null +++ b/examples/bookmind_app/example.env @@ -0,0 +1 @@ +REACT_APP_GOOGLE_BOOKS_API_KEY={YOUR_API_KEY} \ No newline at end of file diff --git a/examples/bookmind_app/package.json b/examples/bookmind_app/package.json new file mode 100644 index 000000000..ec233d2bb --- /dev/null +++ b/examples/bookmind_app/package.json @@ -0,0 +1,48 @@ +{ + "name": "bookmind", + "version": "0.1.0", + "private": true, + "dependencies": { + "@testing-library/jest-dom": "^5.17.0", + "@testing-library/react": "^13.4.0", + "@testing-library/user-event": "^13.5.0", + "axios": "^1.7.7", + "fs": "^0.0.1-security", + "lottie-react": "^2.4.0", + "lucide-react": "^0.460.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-force-graph": "^1.44.7", + "react-force-graph-2d": "^1.25.8", + "react-router-dom": "^7.0.1", + "react-scripts": "^5.0.1", + "web-vitals": "^2.1.4" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "devDependencies": { + "tailwindcss": "^3.4.15" + } +} diff --git a/examples/bookmind_app/public/favicon.ico b/examples/bookmind_app/public/favicon.ico new file mode 100644 index 000000000..a11777cc4 Binary files /dev/null and b/examples/bookmind_app/public/favicon.ico differ diff --git a/examples/bookmind_app/public/index.html b/examples/bookmind_app/public/index.html new file mode 100644 index 000000000..aa069f27c --- /dev/null +++ b/examples/bookmind_app/public/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + React App + + + +
+ + + diff --git a/examples/bookmind_app/public/logo192.png b/examples/bookmind_app/public/logo192.png new file mode 100644 index 000000000..fc44b0a37 Binary files /dev/null and b/examples/bookmind_app/public/logo192.png differ diff --git a/examples/bookmind_app/public/logo512.png b/examples/bookmind_app/public/logo512.png new file mode 100644 index 000000000..a4e47a654 Binary files /dev/null and b/examples/bookmind_app/public/logo512.png differ diff --git a/examples/bookmind_app/public/manifest.json b/examples/bookmind_app/public/manifest.json new file mode 100644 index 000000000..080d6c77a --- /dev/null +++ b/examples/bookmind_app/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/examples/bookmind_app/public/robots.txt b/examples/bookmind_app/public/robots.txt new file mode 100644 index 000000000..e9e57dc4d --- /dev/null +++ b/examples/bookmind_app/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/examples/bookmind_app/server/README.md b/examples/bookmind_app/server/README.md new file mode 100644 index 000000000..15c62a8e1 --- /dev/null +++ b/examples/bookmind_app/server/README.md @@ -0,0 +1,27 @@ +# Book Character Graph API + +A Flask-based API server that analyzes books to create character relationship graphs and provides an interactive query interface using LlamaStack. + +## Features + +- Character and relationship extraction from books +- Graph generation of character relationships +- Memory-based query system for book details +- Interactive Q&A about book characters and plots + +## Prerequisites + +- Python 3.x +- LlamaStack server running locally +- Environment variables: + - `LLAMA_STACK_PORT` + - `INFERENCE_MODEL` + +## Get Started + +```bash +# Install dependencies +pip install -r requirements.txt + +python server.py +``` diff --git a/examples/bookmind_app/server/example.env b/examples/bookmind_app/server/example.env new file mode 100644 index 000000000..ebf82724c --- /dev/null +++ b/examples/bookmind_app/server/example.env @@ -0,0 +1,3 @@ +LLAMA_STACK_PORT=5000 +INFERENCE_MODEL="meta-llama/Llama-3.2-3B-Instruct" +SERVER_PORT=5001 \ No newline at end of file diff --git a/examples/bookmind_app/server/requirements.txt b/examples/bookmind_app/server/requirements.txt new file mode 100644 index 000000000..fdbf142b4 --- /dev/null +++ b/examples/bookmind_app/server/requirements.txt @@ -0,0 +1,5 @@ +flask +flask-cors +llama_stack_client +asyncio +werkzeug \ No newline at end of file diff --git a/examples/bookmind_app/server/server.py b/examples/bookmind_app/server/server.py new file mode 100644 index 000000000..d474a0011 --- /dev/null +++ b/examples/bookmind_app/server/server.py @@ -0,0 +1,281 @@ +import os +import json +import asyncio +import logging +from flask import Flask, request, jsonify +from flask_cors import CORS +from llama_stack_client import LlamaStackClient +from llama_stack_client.lib.agents.agent import Agent +from llama_stack_client.lib.agents.event_logger import EventLogger +from llama_stack_client.types.agent_create_params import AgentConfig + +# Get env variables +INFERENCE_MODEL = os.environ["INFERENCE_MODEL"] +LLAMA_STACK_PORT = os.environ["LLAMA_STACK_PORT"] +SERVER_PORT = os.environ["SERVER_PORT"] + +# Flask setup +app = Flask(__name__) +CORS(app) + +# Memory initialization +active_sessions = {} + + +@app.route("/initialize", methods=["POST"]) +def initialize_memory(): + """ + Initialize memory for a new book. Queries LlamaStack for characters and relationships, + and sets up memory for the user to interact with. + """ + data = request.json + book_title = data.get("title") + if not book_title: + return jsonify({"error": "Book title is required"}), 400 + + # Clear the previous session if any + active_sessions.clear() + + # Create a new memory agent and process the book + response = asyncio.run(process_book(book_title)) + return jsonify(response), 200 + + +async def async_generator_wrapper(sync_gen): + for item in sync_gen: + yield item + + +async def get_graph_response(text_response, client): + # Create prompt for graph conversion + graph_prompt = f""" + Convert this character description into a graph format with nodes and links. + Format the response as a JSON object with "nodes" and "links" arrays. + Each node should have "id", "name", and "val" properties. + Each link should have "source" and "target" properties using the node ids. + Set all node "val" to 10. + + Text to convert: + {text_response} + + Expected format example (return only the JSON object, no additional text): + {{ + "nodes": [ + {{"id": id1, "name": "Character Name", "val": 10}} + ], + "links": [ + {{"source": id1, "target": "id2"}} + ] + }} + id1 and id2 are example variables and you should not generate those exact values. + """ + + # Get graph structure from LlamaStack + graph_response = client.inference.chat_completion( + model_id=INFERENCE_MODEL, + messages=[ + {"role": "system", "content": "You are a data structure expert. Convert text descriptions into graph JSON format."}, + {"role": "user", "content": graph_prompt} + ] + ) + return graph_response + + +def jsonify_graph_response(response): + """Extract and parse JSON content from graph response.""" + try: + content = response.completion_message.content + print("content: ", content) + # Find indices of first { and last } + start_idx = content.find('{') + end_idx = content.rfind('}') + + if start_idx == -1 or end_idx == -1: + raise ValueError("No valid JSON object found in response") + + # Extract JSON string + json_str = content[start_idx:end_idx + 1] + + # Parse JSON + return json.loads(json_str) + + except Exception as e: + logging.error(f"Error parsing graph response: {e}") + return None + + +async def process_book(book_title): + """ + Process the book title, query LlamaStack for characters and relationships, + and initialize memory. + """ + client = LlamaStackClient(base_url=f"http://localhost:{LLAMA_STACK_PORT}") + agent_config = AgentConfig( + model=INFERENCE_MODEL, + instructions="You are a helpful assistant", + tools=[{"type": "memory"}], # Enable memory + enable_session_persistence=True, + max_infer_iters=5, + ) + + # Create the agent and session + agent = Agent(client, agent_config) + session_id = agent.create_session(f"{book_title}-session") + active_sessions["agent"] = agent + active_sessions["session_id"] = session_id + logging.info(f"Created session_id={session_id} for book: {book_title}") + + # Query LlamaStack for characters and relationships + prompt = f"Who are the characters in the book '{book_title}', and what are their relationships?" + + response = client.inference.chat_completion( + model_id=os.environ["INFERENCE_MODEL"], + messages=[ + {"role": "system", "content": "You are a knowledgeable book expert. Provide detailed information about characters and their relationships in the book."}, + {"role": "user", "content": prompt} + ] + ) + text_response = response.completion_message.content + + file_name = f"{book_title.replace(' ', '_').lower()}_memory.txt" + with open(file_name, "w") as f: + f.write(text_response) + + graph_response = await get_graph_response(text_response, client) + + print("graph_response: ", graph_response) + + graph_data = "" + try: + graph_data = jsonify_graph_response(graph_response) + logging.info("Graph data generated:", json.dumps(graph_data, indent=2)) + except json.JSONDecodeError as e: + logging.error(f"Error parsing graph response: {e}") + + # Push to memory agent (optional if further memory setup is needed) + memory_prompt = "Save this knowledge about the book into memory for future questions." + memory_response = agent.create_turn( + messages=[{"role": "user", "content": memory_prompt}], + attachments=[ + {"content": text_response, "mime_type": "text/plain"} + ], + session_id=session_id, + ) + + async for log in async_generator_wrapper(EventLogger().log(memory_response)): + log.print() + + return graph_data + + +def convert_to_graph_format(entities, relationships): + """ + Converts entities and relationships into a graph dictionary format. + """ + nodes = [] + links = [] + node_id_map = {} + node_counter = 1 + + # Add entities as nodes + for entity in entities: + name = entity["text"] + if name not in node_id_map: + node_id_map[name] = f"id{node_counter}" + nodes.append({"id": f"id{node_counter}", "name": name, "val": 10}) + node_counter += 1 + + # Add relationships as links + for relationship in relationships.split("\n"): # Assuming relationships are line-separated + parts = relationship.split(" and ") + if len(parts) == 2: + source_name = parts[0].strip() + target_name = parts[1].split(" are")[0].strip() + + if source_name in node_id_map and target_name in node_id_map: + links.append({ + "source": node_id_map[source_name], + "target": node_id_map[target_name] + }) + + return {"nodes": nodes, "links": links} + + +def convert_to_graph_format_old(response): + """ + Converts LlamaStack's text response into a graph dictionary format. + """ + nodes = [] + links = [] + + # Simplified parsing logic (replace with actual NLP or regex parsing) + lines = [line for line in response if line.strip()] + node_id_map = {} + node_counter = 1 + + for line in lines: + if " and " in line: # Assumes relationships are described as "X and Y are friends" + parts = line.split(" and ") + if len(parts) == 2: + source_name = parts[0].strip() + target_name = parts[1].split(" are")[0].strip() + + # Add nodes if they don't already exist + for name in [source_name, target_name]: + if name not in node_id_map: + node_id_map[name] = f"id{node_counter}" + nodes.append({"id": f"id{node_counter}", "name": name, "val": 10}) + node_counter += 1 + + # Add the relationship as a link + links.append({ + "source": node_id_map[source_name], + "target": node_id_map[target_name] + }) + + return {"nodes": nodes, "links": links} + + +@app.route("/query", methods=["POST"]) +def query_memory(): + """ + Handles user queries and returns answers based on memory. + """ + data = request.json + query = data.get("query") + if not query: + return jsonify({"error": "Query parameter is missing"}), 400 + + # Query the memory agent + response = asyncio.run(query_llama_stack(query)) + return jsonify({"response": response}) + + +async def query_llama_stack(prompt): + """ + Queries the active LlamaStack session with a user prompt. + """ + if "agent" not in active_sessions: + return "No active agent session. Please initialize a book first." + + agent = active_sessions["agent"] + session_id = active_sessions["session_id"] + + response = agent.create_turn( + messages=[{"role": "user", "content": prompt}], + session_id=session_id, + ) + + # Process response logs + result = [] + async for log in async_generator_wrapper(EventLogger().log(response)): + result.append(str(log)) + + inference_start_idx = result.index("inference> ") + inference_logs = result[inference_start_idx + 1:] + + return "\n".join(inference_logs) + + +if __name__ == "__main__": + app.run(debug=True, port=SERVER_PORT) diff --git a/examples/bookmind_app/server/test.py b/examples/bookmind_app/server/test.py new file mode 100644 index 000000000..e69de29bb diff --git a/examples/bookmind_app/src/App.css b/examples/bookmind_app/src/App.css new file mode 100644 index 000000000..74b5e0534 --- /dev/null +++ b/examples/bookmind_app/src/App.css @@ -0,0 +1,38 @@ +.App { + text-align: center; +} + +.App-logo { + height: 40vmin; + pointer-events: none; +} + +@media (prefers-reduced-motion: no-preference) { + .App-logo { + animation: App-logo-spin infinite 20s linear; + } +} + +.App-header { + background-color: #282c34; + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-size: calc(10px + 2vmin); + color: white; +} + +.App-link { + color: #61dafb; +} + +@keyframes App-logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} diff --git a/examples/bookmind_app/src/App.js b/examples/bookmind_app/src/App.js new file mode 100644 index 000000000..d3271465c --- /dev/null +++ b/examples/bookmind_app/src/App.js @@ -0,0 +1,17 @@ +import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; +import Home from "./homePage/index"; +import SearchPage from "./bookPage/components/SearchPage"; + +function App() { + return ( + + + {/* Define routes for Home and SearchPage */} + } /> + } /> + + + ); +} + +export default App; diff --git a/examples/bookmind_app/src/App.test.js b/examples/bookmind_app/src/App.test.js new file mode 100644 index 000000000..1f03afeec --- /dev/null +++ b/examples/bookmind_app/src/App.test.js @@ -0,0 +1,8 @@ +import { render, screen } from '@testing-library/react'; +import App from './App'; + +test('renders learn react link', () => { + render(); + const linkElement = screen.getByText(/learn react/i); + expect(linkElement).toBeInTheDocument(); +}); diff --git a/examples/bookmind_app/src/approuter.jsx b/examples/bookmind_app/src/approuter.jsx new file mode 100644 index 000000000..7a80f93cf --- /dev/null +++ b/examples/bookmind_app/src/approuter.jsx @@ -0,0 +1,26 @@ +/* eslint-disable no-extra-semi */ +/* eslint-disable react/prop-types */ +/* eslint-disable no-unused-vars */ +import { Route, BrowserRouter as Router, Routes } from "react-router-dom"; + +import Home from "./pages/homePage"; +import BookPage from "./pages/bookPage"; + +const AppRouter = function () { + return ( + <> + + } /> + } /> + + + ); +}; + +const App = () => ( + + + +); + +export default App; diff --git a/examples/bookmind_app/src/index.css b/examples/bookmind_app/src/index.css new file mode 100644 index 000000000..b5c61c956 --- /dev/null +++ b/examples/bookmind_app/src/index.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/examples/bookmind_app/src/index.js b/examples/bookmind_app/src/index.js new file mode 100644 index 000000000..6e917b174 --- /dev/null +++ b/examples/bookmind_app/src/index.js @@ -0,0 +1,13 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import "./index.css"; +import AppRouter from "./approuter"; +import reportWebVitals from "./reportWebVitals"; + +const root = ReactDOM.createRoot(document.getElementById("root")); +root.render(); + +// If you want to start measuring performance in your app, pass a function +// to log results (for example: reportWebVitals(console.log)) +// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals +reportWebVitals(); diff --git a/examples/bookmind_app/src/logo.svg b/examples/bookmind_app/src/logo.svg new file mode 100644 index 000000000..9dfc1c058 --- /dev/null +++ b/examples/bookmind_app/src/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/bookmind_app/src/pages/Layout.jsx b/examples/bookmind_app/src/pages/Layout.jsx new file mode 100644 index 000000000..da6868d42 --- /dev/null +++ b/examples/bookmind_app/src/pages/Layout.jsx @@ -0,0 +1,19 @@ +import "../index.css"; + +export const metadata = { + title: "BookMind - Unravel Stories, One Map at a Time", + description: + "Explore character relationships and storylines with AI-powered visualizations.", +}; + +export default function RootLayout({ children }) { + return ( + + + {metadata.title} + + + {children} + + ); +} diff --git a/examples/bookmind_app/src/pages/MindMap/index.jsx b/examples/bookmind_app/src/pages/MindMap/index.jsx new file mode 100644 index 000000000..a621fc396 --- /dev/null +++ b/examples/bookmind_app/src/pages/MindMap/index.jsx @@ -0,0 +1,127 @@ +import React, { useState } from "react"; +import CytoscapeComponent from "react-cytoscapejs"; + +const MindMap = () => { + const [hoveredNode, setHoveredNode] = useState(null); + + // Graph data: Nodes and Edges + const elements = [ + { data: { id: "Harry", label: "Harry Potter" } }, + { data: { id: "Hermione", label: "Hermione Granger" } }, + { data: { id: "Ron", label: "Ron Weasley" } }, + { data: { id: "Dumbledore", label: "Albus Dumbledore" } }, + { + data: { + id: "friendship1", + source: "Harry", + target: "Hermione", + label: "Friends", + }, + }, + { + data: { + id: "friendship2", + source: "Harry", + target: "Ron", + label: "Best Friends", + }, + }, + { + data: { + id: "mentor", + source: "Dumbledore", + target: "Harry", + label: "Mentor", + }, + }, + ]; + + // Cytoscape Styles for Nodes and Edges + const style = [ + { + selector: "node", + style: { + "background-color": "#0074D9", + label: "data(label)", + "text-valign": "center", + "text-halign": "center", + color: "#ffffff", + "font-size": "10px", + width: "40px", + height: "40px", + }, + }, + { + selector: "edge", + style: { + "line-color": "#AAAAAA", + "target-arrow-color": "#AAAAAA", + "target-arrow-shape": "triangle", + "curve-style": "bezier", + label: "data(label)", + "font-size": "8px", + color: "#333333", + "text-outline-color": "#ffffff", + "text-outline-width": "1px", + }, + }, + { + selector: ":selected", + style: { + "background-color": "#FF4136", + "line-color": "#FF4136", + "target-arrow-color": "#FF4136", + "source-arrow-color": "#FF4136", + }, + }, + ]; + + // Handle hover events + const handleMouseOver = (event) => { + const node = event.target.data(); + setHoveredNode(node); + }; + + const handleMouseOut = () => { + setHoveredNode(null); + }; + + return ( +
+
+ { + // Add event listeners for hover + cy.on("mouseover", "node", handleMouseOver); + cy.on("mouseout", "node", handleMouseOut); + }} + /> +
+ {hoveredNode && ( +
+ Node Details: +

ID: {hoveredNode.id}

+

Label: {hoveredNode.label}

+
+ )} +
+ ); +}; + +export default MindMap; diff --git a/examples/bookmind_app/src/pages/bookPage/components/AISearch.jsx b/examples/bookmind_app/src/pages/bookPage/components/AISearch.jsx new file mode 100644 index 000000000..042198b92 --- /dev/null +++ b/examples/bookmind_app/src/pages/bookPage/components/AISearch.jsx @@ -0,0 +1,71 @@ +import { useState } from "react"; +import { FaSearch, FaSpinner } from "react-icons/fa"; +import axios from "axios"; + +export default function AISearch({ bookTitle, onQueryResponse }) { + const [query, setQuery] = useState(""); + const [response, setResponse] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const handleSearch = async () => { + if (!query.trim()) return; + + setIsLoading(true); + setError(null); + + try { + const response = await axios.post("http://localhost:5001/query", { + query: `About ${bookTitle}: ${query}`, + }); + + setResponse(response.data.response); + onQueryResponse?.(response.data); + } catch (error) { + console.error("Error in AI search:", error); + setError("Failed to get response. Please try again."); + } finally { + setIsLoading(false); + } + }; + + return ( +
+

AI-Powered Search

+
+ setQuery(e.target.value)} + className="w-full px-4 py-2 rounded-md border-gray-300 + focus:border-blue-500 focus:ring focus:ring-blue-200 + transition duration-200" + disabled={isLoading} + /> + +
+ + {error && ( +
{error}
+ )} + + {response && !error && ( +
+

AI Response:

+

{response}

+
+ )} +
+ ); +} diff --git a/examples/bookmind_app/src/pages/bookPage/components/CharacterGraph.jsx b/examples/bookmind_app/src/pages/bookPage/components/CharacterGraph.jsx new file mode 100644 index 000000000..09578b526 --- /dev/null +++ b/examples/bookmind_app/src/pages/bookPage/components/CharacterGraph.jsx @@ -0,0 +1,73 @@ +import { useState, useEffect, useRef } from "react"; +import ForceGraph2D from "react-force-graph-2d"; + +export default function CharacterGraph({ graphData }) { + const containerRef = useRef(null); + const [dimensions, setDimensions] = useState({ width: 300, height: 300 }); + + useEffect(() => { + const updateDimensions = () => { + if (containerRef.current) { + setDimensions({ + width: containerRef.current.offsetWidth, + height: Math.max(300, containerRef.current.offsetHeight), + }); + } + }; + + updateDimensions(); + window.addEventListener("resize", updateDimensions); + return () => window.removeEventListener("resize", updateDimensions); + }, []); + + return ( +
+

+ Character Relationship Graph +

+
+ { + const label = node.name; + const fontSize = 12 / globalScale; + ctx.font = `${fontSize}px Arial`; + const textWidth = ctx.measureText(label).width; + const bckgDimensions = [textWidth, fontSize].map( + (n) => n + fontSize * 0.2 + ); + + ctx.fillStyle = "rgba(255, 255, 255, 0.8)"; + ctx.fillRect( + node.x - bckgDimensions[0] / 2, + node.y - bckgDimensions[1] / 2, + ...bckgDimensions + ); + + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + ctx.fillStyle = "#3b82f6"; + ctx.fillText(label, node.x, node.y); + + node.__bckgDimensions = bckgDimensions; + }} + nodePointerAreaPaint={(node, color, ctx) => { + ctx.fillStyle = color; + const bckgDimensions = node.__bckgDimensions; + bckgDimensions && + ctx.fillRect( + node.x - bckgDimensions[0] / 2, + node.y - bckgDimensions[1] / 2, + ...bckgDimensions + ); + }} + linkColor={() => "#9ca3af"} + linkWidth={1} + backgroundColor="#ffffff" + width={dimensions.width} + height={dimensions.height} + /> +
+
+ ); +} diff --git a/examples/bookmind_app/src/pages/bookPage/components/ErrorBoundary.jsx b/examples/bookmind_app/src/pages/bookPage/components/ErrorBoundary.jsx new file mode 100644 index 000000000..a88032894 --- /dev/null +++ b/examples/bookmind_app/src/pages/bookPage/components/ErrorBoundary.jsx @@ -0,0 +1,29 @@ +'use client' + +import { Component } from 'react' + +class ErrorBoundary extends Component { + constructor(props) { + super(props) + this.state = { hasError: false } + } + + static getDerivedStateFromError(error) { + return { hasError: true } + } + + componentDidCatch(error, errorInfo) { + console.log('ErrorBoundary caught an error:', error, errorInfo) + } + + render() { + if (this.state.hasError) { + return

Something went wrong.

+ } + + return this.props.children + } +} + +export default ErrorBoundary + diff --git a/examples/bookmind_app/src/pages/bookPage/index.jsx b/examples/bookmind_app/src/pages/bookPage/index.jsx new file mode 100644 index 000000000..9e3b9f88d --- /dev/null +++ b/examples/bookmind_app/src/pages/bookPage/index.jsx @@ -0,0 +1,233 @@ +import React, { useState } from "react"; +import { FaSearch, FaBook, FaHome } from "react-icons/fa"; +import { Link } from "react-router-dom"; +import AISearch from "./components/AISearch"; +import CharacterGraph from "./components/CharacterGraph"; +import axios from "axios"; + +export default function BookPage() { + const [searchTerm, setSearchTerm] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const [graphData, setGraphData] = useState(null); + const [bookData, setBookData] = useState(null); + const [searchComplete, setSearchComplete] = useState(false); + + const verificationGraphData = (graphData) => { + try { + // Create Set of valid node IDs + const nodeIds = new Set(graphData.nodes.map((node) => node.id)); + + // Filter links to only include valid node references + const validLinks = graphData.links.filter( + (link) => nodeIds.has(link.source) && nodeIds.has(link.target) + ); + + return { + nodes: graphData.nodes, + links: validLinks, + }; + } catch (error) { + console.error("Error validating graph data:", error); + return graphData; // Return original data if validation fails + } + }; + + const initializeMemory = async (title) => { + setIsLoading(true); + try { + const response = await axios.post("http://localhost:5001/initialize", { + title: title, + }); + + // Verify and set graph data + const verifiedData = verificationGraphData(response.data); + setGraphData(verifiedData); + + return verifiedData; + } catch (error) { + console.error("Initialization error:", error); + throw error; + } finally { + setIsLoading(false); + } + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + if (!searchTerm.trim()) { + return; + } + + setIsLoading(true); + try { + // Initialize memory and fetch book data in parallel + const [memoryResponse, bookInfo] = await Promise.all([ + initializeMemory(searchTerm), + fetchBookData(searchTerm), + ]); + + setBookData({ + title: bookInfo.title, + subtitle: bookInfo.summary, + posterUrl: bookInfo.coverUrl, + author: bookInfo.author, + publishedDate: bookInfo.publishedDate, + pageCount: bookInfo.pageCount, + }); + + setGraphData(memoryResponse); + setSearchComplete(true); + } catch (error) { + console.error("Search error:", error); + } finally { + setIsLoading(false); + } + }; + // Add new function to fetch book cover + const fetchBookData = async (bookTitle) => { + try { + const response = await axios.get( + `https://www.googleapis.com/books/v1/volumes`, + { + params: { + q: bookTitle, + key: process.env.REACT_APP_GOOGLE_BOOKS_API_KEY, + }, + } + ); + + if (response.data.items && response.data.items[0]) { + const volumeInfo = response.data.items[0].volumeInfo; + const imageLinks = volumeInfo.imageLinks || {}; + + return { + coverUrl: + imageLinks.extraLarge || + imageLinks.large || + imageLinks.medium || + imageLinks.thumbnail || + "/placeholder.jpg", + summary: volumeInfo.description || "No summary available", + title: volumeInfo.title, + author: volumeInfo.authors?.[0] || "Unknown Author", + publishedDate: volumeInfo.publishedDate, + pageCount: volumeInfo.pageCount, + }; + } + + return { + coverUrl: "/placeholder.jpg", + summary: "No summary available", + title: bookTitle, + author: "Unknown Author", + publishedDate: "", + pageCount: 0, + }; + } catch (error) { + console.error("Error fetching book data:", error); + return { + coverUrl: "/placeholder.jpg", + summary: "Failed to load book information", + title: bookTitle, + author: "Unknown Author", + publishedDate: "", + pageCount: 0, + }; + } + }; + + return ( +
+
+ {/* Home Button */} +
+ + + Home + +
+ + {/* Search Section */} +
+

+ + Character Mind Map + +

+
+
+
+ setSearchTerm(e.target.value)} + placeholder="Enter book or movie title..." + className="w-full px-5 py-3 rounded-lg border-2 border-gray-200 + focus:border-blue-500 focus:ring-2 focus:ring-blue-200 + transition-all duration-200 bg-white/90 + placeholder-gray-400 text-gray-700" + disabled={isLoading} + /> +
+ +
+
+

+ Search for any book or movie to explore character relationships +

+
+ + {/* Info Section - Only show when search is complete */} + {searchComplete && bookData && ( +
+
+
+
+ {bookData.title} +
+
+
+ +

+ {bookData.title} +

+
+

{bookData.subtitle}

+
+
+
+ +
+
+ +
+
+ +
+
+
+ )} +
+
+ ); +} diff --git a/examples/bookmind_app/src/pages/homePage/components/Features.jsx b/examples/bookmind_app/src/pages/homePage/components/Features.jsx new file mode 100644 index 000000000..a9eb2ec0a --- /dev/null +++ b/examples/bookmind_app/src/pages/homePage/components/Features.jsx @@ -0,0 +1,78 @@ +import { BookOpen, MessageSquare, FileText, Users } from "lucide-react"; + +const features = [ + { + icon: BookOpen, + title: "Interactive Mind Maps", + description: + "Visualize relationships between characters and plot elements.", + }, + { + icon: MessageSquare, + title: "AI Chatbot", + description: + "Ask deep questions about the book and get insightful answers.", + }, + { + icon: FileText, + title: "Book Summaries", + description: "Get concise overviews of plots and themes.", + }, + { + icon: Users, + title: "Community Contributions", + description: "Add and refine maps with fellow book lovers.", + }, +]; + +export default function Features() { + return ( +
+
+
+

+ Key Features +

+

+ Discover the power of AI-driven book analysis +

+
+ +
+ {features.map((feature, index) => ( +
+
+ +

+ {feature.title} +

+

+ {feature.description} +

+
+ ))} +
+
+
+ ); +} diff --git a/examples/bookmind_app/src/pages/homePage/components/Hero.jsx b/examples/bookmind_app/src/pages/homePage/components/Hero.jsx new file mode 100644 index 000000000..17f479eb7 --- /dev/null +++ b/examples/bookmind_app/src/pages/homePage/components/Hero.jsx @@ -0,0 +1,53 @@ +import { useNavigate } from "react-router-dom"; +import { useState, useEffect } from "react"; +import Lottie from "lottie-react"; + +export default function Hero() { + const navigate = useNavigate(); + const [animationData, setAnimationData] = useState(null); + + useEffect(() => { + const fetchAnimationData = async () => { + try { + const response = await fetch( + "https://lottie.host/909e50db-34ef-47ac-8384-db31f6fc0654/e2xX6qZZ7i.json" + ); + const data = await response.json(); + setAnimationData(data); + } catch (error) { + console.error("Error fetching Lottie animation:", error); + } + }; + + fetchAnimationData(); + }, []); + + return ( +
+
+ {animationData && ( + + )} +
+
+
+

+ Unravel Stories
One Map at a Time +

+

+ Explore character relationships and storylines with AI-powered + visualizations. +

+ +
+
+ ); +} diff --git a/examples/bookmind_app/src/pages/homePage/components/HowItWorks.jsx b/examples/bookmind_app/src/pages/homePage/components/HowItWorks.jsx new file mode 100644 index 000000000..ecdbe7810 --- /dev/null +++ b/examples/bookmind_app/src/pages/homePage/components/HowItWorks.jsx @@ -0,0 +1,89 @@ +import { useState, useEffect } from "react"; +import { Brain, Search, MessageSquare } from "lucide-react"; + +const steps = [ + { + icon: Search, + title: "Search for a Book", + description: "Enter the title of the book you want to explore.", + }, + { + icon: Brain, + title: "AI Analysis", + description: "The AI analyzes the book and generates a mind map.", + }, + { + icon: MessageSquare, + title: "Explore Insights", + description: + "Ask questions and explore relationships, themes, and insights.", + }, +]; + +export default function HowItWorks() { + const [activeStep, setActiveStep] = useState(0); + + useEffect(() => { + const interval = setInterval(() => { + setActiveStep((prevStep) => (prevStep + 1) % steps.length); + }, 3000); + return () => clearInterval(interval); + }, []); + + return ( +
+
+
+

+ How It Works +

+

+ Discover the power of AI-driven book analysis +

+
+
+ {steps.map((step, index) => ( +
+
+ +

+ {step.title} +

+

+ {step.description} +

+
+ ))} +
+
+
+ ); +} diff --git a/examples/bookmind_app/src/pages/homePage/index.jsx b/examples/bookmind_app/src/pages/homePage/index.jsx new file mode 100644 index 000000000..aab121236 --- /dev/null +++ b/examples/bookmind_app/src/pages/homePage/index.jsx @@ -0,0 +1,13 @@ +import Hero from "./components/Hero"; +import Features from "./components/Features"; +import HowItWorks from "./components/HowItWorks"; + +export default function Home() { + return ( +
+ + + +
+ ); +} diff --git a/examples/bookmind_app/src/pages/searchTestPage/GraphVisualization.jsx b/examples/bookmind_app/src/pages/searchTestPage/GraphVisualization.jsx new file mode 100644 index 000000000..2590c8497 --- /dev/null +++ b/examples/bookmind_app/src/pages/searchTestPage/GraphVisualization.jsx @@ -0,0 +1,54 @@ +import React from "react"; +import CytoscapeComponent from "react-cytoscapejs"; + +const GraphVisualization = ({ graphData }) => { + const elements = [ + ...graphData.nodes.map((node) => ({ + data: { id: node.id, label: node.name }, + })), + ...graphData.links.map((link) => ({ + data: { source: link.source, target: link.target }, + })), + ]; + + const layout = { name: "circle" }; + + const style = [ + { + selector: "node", + style: { + "background-color": "#0074D9", + label: "data(label)", + "text-valign": "center", + "text-halign": "center", + color: "#fff", + "font-size": "10px", + width: "40px", + height: "40px", + }, + }, + { + selector: "edge", + style: { + "line-color": "#AAAAAA", + "target-arrow-color": "#AAAAAA", + "target-arrow-shape": "triangle", + "curve-style": "bezier", + }, + }, + ]; + + return ( +
+

Graph Visualization

+ +
+ ); +}; + +export default GraphVisualization; diff --git a/examples/bookmind_app/src/pages/searchTestPage/InitializeMemory.jsx b/examples/bookmind_app/src/pages/searchTestPage/InitializeMemory.jsx new file mode 100644 index 000000000..751e71b63 --- /dev/null +++ b/examples/bookmind_app/src/pages/searchTestPage/InitializeMemory.jsx @@ -0,0 +1,40 @@ +import React, { useState } from "react"; +import axios from "axios"; + +const InitializeMemory = ({ setGraphData }) => { + const [bookTitle, setBookTitle] = useState(""); + const [message, setMessage] = useState(""); + + const handleInitialize = async () => { + if (!bookTitle) { + setMessage("Please enter a book title."); + return; + } + try { + const response = await axios.post("http://localhost:5001/initialize", { + title: bookTitle, + }); + setGraphData(response.data); + setMessage("Memory initialized successfully!"); + } catch (error) { + console.error(error); + setMessage("Error initializing memory."); + } + }; + + return ( +
+

Initialize Memory

+ setBookTitle(e.target.value)} + placeholder="Enter book title" + /> + + {message &&

{message}

} +
+ ); +}; + +export default InitializeMemory; diff --git a/examples/bookmind_app/src/pages/searchTestPage/QueryMemory.jsx b/examples/bookmind_app/src/pages/searchTestPage/QueryMemory.jsx new file mode 100644 index 000000000..3ce148501 --- /dev/null +++ b/examples/bookmind_app/src/pages/searchTestPage/QueryMemory.jsx @@ -0,0 +1,41 @@ +import React, { useState } from "react"; +import axios from "axios"; + +const QueryMemory = () => { + const [query, setQuery] = useState(""); + const [response, setResponse] = useState(""); + + const handleQuery = async () => { + if (!query) { + setResponse("Please enter a query."); + return; + } + try { + const result = await axios.post("http://localhost:5001/query", { query }); + setResponse(result.data.response); + } catch (error) { + console.error(error); + setResponse("Error querying memory."); + } + }; + + return ( +
+

Ask a Question

+ setQuery(e.target.value)} + placeholder="Enter your question" + /> + + {response && ( +

+ Response: {response} +

+ )} +
+ ); +}; + +export default QueryMemory; diff --git a/examples/bookmind_app/src/pages/searchTestPage/index.jsx b/examples/bookmind_app/src/pages/searchTestPage/index.jsx new file mode 100644 index 000000000..42664876b --- /dev/null +++ b/examples/bookmind_app/src/pages/searchTestPage/index.jsx @@ -0,0 +1,21 @@ +import React, { useState } from "react"; +import InitializeMemory from "./InitializeMemory"; +import QueryMemory from "./QueryMemory"; +import GraphVisualization from "./GraphVisualization"; + +const BookSearch = () => { + const [graphData, setGraphData] = useState(null); + + return ( +
+

Book Memory Agent

+ +
+ {graphData && } +
+ +
+ ); +}; + +export default BookSearch; diff --git a/examples/bookmind_app/src/reportWebVitals.js b/examples/bookmind_app/src/reportWebVitals.js new file mode 100644 index 000000000..5253d3ad9 --- /dev/null +++ b/examples/bookmind_app/src/reportWebVitals.js @@ -0,0 +1,13 @@ +const reportWebVitals = onPerfEntry => { + if (onPerfEntry && onPerfEntry instanceof Function) { + import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { + getCLS(onPerfEntry); + getFID(onPerfEntry); + getFCP(onPerfEntry); + getLCP(onPerfEntry); + getTTFB(onPerfEntry); + }); + } +}; + +export default reportWebVitals; diff --git a/examples/bookmind_app/src/setupTests.js b/examples/bookmind_app/src/setupTests.js new file mode 100644 index 000000000..8f2609b7b --- /dev/null +++ b/examples/bookmind_app/src/setupTests.js @@ -0,0 +1,5 @@ +// jest-dom adds custom jest matchers for asserting on DOM nodes. +// allows you to do things like: +// expect(element).toHaveTextContent(/react/i) +// learn more: https://github.com/testing-library/jest-dom +import '@testing-library/jest-dom'; diff --git a/examples/bookmind_app/tailwind.config.js b/examples/bookmind_app/tailwind.config.js new file mode 100644 index 000000000..8aec68540 --- /dev/null +++ b/examples/bookmind_app/tailwind.config.js @@ -0,0 +1,23 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: ["./src/**/*.{js,jsx,ts,tsx}"], + theme: { + extend: { + keyframes: { + fadeInUp: { + "0%": { opacity: 0, transform: "translateY(20px)" }, + "100%": { opacity: 1, transform: "translateY(0)" }, + }, + fadeOutDown: { + "0%": { opacity: 1, transform: "translateY(0)" }, + "100%": { opacity: 0, transform: "translateY(-20px)" }, + }, + }, + animation: { + fadeInUp: "fadeInUp 0.3s ease-out", + fadeOutDown: "fadeOutDown 0.3s ease-in", + }, + }, + }, + plugins: [], +};