ソースを参照

[Llama4] Book character mindmap (#916)

Young Han 1 週間 前
コミット
a9710eb07e
37 ファイル変更21896 行追加1 行削除
  1. 2 0
      .github/scripts/spellcheck_conf/wordlist.txt
  2. 1 1
      .github/workflows/pytest_cpu_gha_runner.yaml
  3. 1 0
      .gitignore
  4. 23 0
      end-to-end-use-cases/book-character-mindmap/.gitignore
  5. 86 0
      end-to-end-use-cases/book-character-mindmap/README.md
  6. 20431 0
      end-to-end-use-cases/book-character-mindmap/package-lock.json
  7. 50 0
      end-to-end-use-cases/book-character-mindmap/package.json
  8. BIN
      end-to-end-use-cases/book-character-mindmap/public/character_relationship.png
  9. BIN
      end-to-end-use-cases/book-character-mindmap/public/chat_interface.png
  10. BIN
      end-to-end-use-cases/book-character-mindmap/public/favicon.ico
  11. 43 0
      end-to-end-use-cases/book-character-mindmap/public/index.html
  12. BIN
      end-to-end-use-cases/book-character-mindmap/public/logo192.png
  13. BIN
      end-to-end-use-cases/book-character-mindmap/public/logo512.png
  14. 25 0
      end-to-end-use-cases/book-character-mindmap/public/manifest.json
  15. BIN
      end-to-end-use-cases/book-character-mindmap/public/mindmap.png
  16. 3 0
      end-to-end-use-cases/book-character-mindmap/public/robots.txt
  17. 6 0
      end-to-end-use-cases/book-character-mindmap/server/requirements.txt
  18. 310 0
      end-to-end-use-cases/book-character-mindmap/server/server.py
  19. 38 0
      end-to-end-use-cases/book-character-mindmap/src/App.css
  20. 17 0
      end-to-end-use-cases/book-character-mindmap/src/App.js
  21. 8 0
      end-to-end-use-cases/book-character-mindmap/src/App.test.js
  22. 26 0
      end-to-end-use-cases/book-character-mindmap/src/approuter.jsx
  23. 3 0
      end-to-end-use-cases/book-character-mindmap/src/index.css
  24. 13 0
      end-to-end-use-cases/book-character-mindmap/src/index.js
  25. 1 0
      end-to-end-use-cases/book-character-mindmap/src/logo.svg
  26. 19 0
      end-to-end-use-cases/book-character-mindmap/src/pages/Layout.jsx
  27. 125 0
      end-to-end-use-cases/book-character-mindmap/src/pages/bookPage/components/CharacterGraph.jsx
  28. 104 0
      end-to-end-use-cases/book-character-mindmap/src/pages/bookPage/components/ChatInterface.jsx
  29. 29 0
      end-to-end-use-cases/book-character-mindmap/src/pages/bookPage/components/ErrorBoundary.jsx
  30. 258 0
      end-to-end-use-cases/book-character-mindmap/src/pages/bookPage/index.jsx
  31. 78 0
      end-to-end-use-cases/book-character-mindmap/src/pages/homePage/components/Features.jsx
  32. 53 0
      end-to-end-use-cases/book-character-mindmap/src/pages/homePage/components/Hero.jsx
  33. 89 0
      end-to-end-use-cases/book-character-mindmap/src/pages/homePage/components/HowItWorks.jsx
  34. 13 0
      end-to-end-use-cases/book-character-mindmap/src/pages/homePage/index.jsx
  35. 13 0
      end-to-end-use-cases/book-character-mindmap/src/reportWebVitals.js
  36. 5 0
      end-to-end-use-cases/book-character-mindmap/src/setupTests.js
  37. 23 0
      end-to-end-use-cases/book-character-mindmap/tailwind.config.js

+ 2 - 0
.github/scripts/spellcheck_conf/wordlist.txt

@@ -1536,6 +1536,8 @@ LlamaParse
 ailabs
 jina
 jinaai
+Moby
+storylines
 AppUtils
 ArticleSummarizer
 ModelUtils

+ 1 - 1
.github/workflows/pytest_cpu_gha_runner.yaml

@@ -70,5 +70,5 @@ jobs:
         id: test_summary
         uses: test-summary/action@v2
         with:
-          paths: "**/*.xml"
+          paths: "**/*.xml(?<!AndroidManifest\.xml)"
         if: always()

+ 1 - 0
.gitignore

@@ -3,3 +3,4 @@ __pycache__
 .ipynb_checkpoints
 wandb/
 artifacts/
+node_modules/

+ 23 - 0
end-to-end-use-cases/book-character-mindmap/.gitignore

@@ -0,0 +1,23 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# production
+/build
+
+# misc
+.DS_Store
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*

+ 86 - 0
end-to-end-use-cases/book-character-mindmap/README.md

@@ -0,0 +1,86 @@
+# Book Character Mind Map With Llama4 Maverick
+
+![Book Character Mind Map](public/character_relationship.png)
+
+Book Mind is a web application that allows users to explore character relationships and storylines in books using AI-powered visualizations.
+This leverages **Llama 4 Maverick**'s impressive 1M token context windows to process entire books at once, enabling comprehensive analysis of complex narratives and character relationships across lengthy texts.
+
+## Features
+
+### Leverage Long Context Length
+| Model | Meta Llama4 Maverick | Meta Llama4 Scout | OpenAI GPT-4.5 | Claude Sonnet 3.7 |
+| ----- | -------------- | -------------- | -------------- | -------------- |
+| Context Window | 1M tokens | 10M tokens | 128K tokens | 1K tokens | 200K tokens |
+
+Because of the long context length, Book Mind can process entire books at once, providing a comprehensive understanding of complex narratives and character relationships.
+
+- Interactive Mind Maps: Visualize relationships between characters and plot elements.
+- Book Summaries: Get concise overviews of plots and themes.
+
+### Step-by-Step Instructions
+
+We implemented a step-by-step approach to ensure the model outputs' reliability.
+
+1. **Character Identification**: Identify all characters in the book and summarize their roles.
+```
+You are a highly detailed literary analyst AI. Your sole mission is to meticulously extract comprehensive information about characters and the *nuances* of their relationships from the provided text segment. This data will be used later to build a relationship graph.
+```
+
+2. **Character Relationships**: Determine the relationships between characters.
+```
+You are an expert data architect AI specializing in transforming literary analysis into structured graph data. Your task is to synthesize character and relationship information into a specific JSON format containing nodes and links, including a title and summary.
+```
+
+3. **JSON Format**: Output the results in a JSON format for easy parsing and visualization.
+```
+You are an extremely precise and strict JSON extractor.
+Extract only the complete JSON object from the input. Get the last one if there are multiple.
+```
+
+### Ask the Book with Chat Interface
+
+We also implemented a chat interface to interact with the book. Users can ask questions about the book's characters, plot, and relationships. The model will respond with a concise answer based on the book's content and the relationships between characters.
+
+```
+You are an expert search AI designed to help users find detailed information about character relationships from a book. Your task is to assist users in querying the relationship data extracted from the book.
+```
+
+![Chat Interface](public/chat_interface.png)
+
+## Getting Started
+
+### Frontend Setup
+To communicate with the [server/server.py](server/server.py), we use `React.js` and `axios`.
+
+1. Install dependencies:
+
+```
+npm install
+```
+
+2. Run the application:
+
+```
+npm start
+```
+
+### Server Setup
+
+We use `Flask` to serve the model's responses and `vllm` to run the **Llama 4 Maverick** model.
+
+1. Install dependencies:
+```
+cd server
+pip install -r requirements.txt
+```
+
+2. Run the server:
+```
+python server.py
+```
+
+## Get Copyright Free Books
+
+- [Project Gutenberg](https://www.gutenberg.org/)
+  - [Romeo and Juliet](https://www.gutenberg.org/ebooks/1513): 50,687 input tokens
+  - [Moby-Dick; The Whale](https://www.gutenberg.org/ebooks/2701): 318,027 input tokens

ファイルの差分が大きいため隠しています
+ 20431 - 0
end-to-end-use-cases/book-character-mindmap/package-lock.json


+ 50 - 0
end-to-end-use-cases/book-character-mindmap/package.json

@@ -0,0 +1,50 @@
+{
+  "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",
+    "force-graph": "^1.49.5",
+    "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-icons": "^4.10.1",
+    "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"
+  }
+}

BIN
end-to-end-use-cases/book-character-mindmap/public/character_relationship.png


BIN
end-to-end-use-cases/book-character-mindmap/public/chat_interface.png


BIN
end-to-end-use-cases/book-character-mindmap/public/favicon.ico


+ 43 - 0
end-to-end-use-cases/book-character-mindmap/public/index.html

@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8" />
+    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
+    <meta name="viewport" content="width=device-width, initial-scale=1" />
+    <meta name="theme-color" content="#000000" />
+    <meta
+      name="description"
+      content="Web site created using create-react-app"
+    />
+    <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
+    <!--
+      manifest.json provides metadata used when your web app is installed on a
+      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
+    -->
+    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
+    <!--
+      Notice the use of %PUBLIC_URL% in the tags above.
+      It will be replaced with the URL of the `public` folder during the build.
+      Only files inside the `public` folder can be referenced from the HTML.
+
+      Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
+      work correctly both with client-side routing and a non-root public URL.
+      Learn how to configure a non-root public URL by running `npm run build`.
+    -->
+    <title>React App</title>
+  </head>
+  <body>
+    <noscript>You need to enable JavaScript to run this app.</noscript>
+    <div id="root"></div>
+    <!--
+      This HTML file is a template.
+      If you open it directly in the browser, you will see an empty page.
+
+      You can add webfonts, meta tags, or analytics to this file.
+      The build step will place the bundled scripts into the <body> tag.
+
+      To begin the development, run `npm start` or `yarn start`.
+      To create a production bundle, use `npm run build` or `yarn build`.
+    -->
+  </body>
+</html>

BIN
end-to-end-use-cases/book-character-mindmap/public/logo192.png


BIN
end-to-end-use-cases/book-character-mindmap/public/logo512.png


+ 25 - 0
end-to-end-use-cases/book-character-mindmap/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"
+}

BIN
end-to-end-use-cases/book-character-mindmap/public/mindmap.png


+ 3 - 0
end-to-end-use-cases/book-character-mindmap/public/robots.txt

@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:

+ 6 - 0
end-to-end-use-cases/book-character-mindmap/server/requirements.txt

@@ -0,0 +1,6 @@
+flask
+flask-cors
+asyncio
+werkzeug
+vllm
+transformers

+ 310 - 0
end-to-end-use-cases/book-character-mindmap/server/server.py

@@ -0,0 +1,310 @@
+import json
+import logging
+import os
+
+from flask import Flask, jsonify, request
+from flask_cors import CORS
+from transformers import AutoTokenizer
+from vllm import LLM, sampling_params, SamplingParams
+
+# Flask setup
+app = Flask(__name__)
+CORS(app)
+
+CHARACTER_SYSTEM_PROMPT = """
+You are a highly detailed literary analyst AI. Your sole mission is to meticulously extract comprehensive information about characters and the *nuances* of their relationships from the provided text segment. This data will be used later to build a relationship graph.
+
+**Objective:** Identify EVERY character mentioned. For each pair of interacting characters, describe their relationship in detail, focusing on the context, roles, emotional dynamics, history, and key interactions *as presented or clearly implied* within this specific text segment.
+
+**Instructions:**
+
+1.  **Identify Characters:** List every unique character name mentioned in the text segment.
+2.  **Identify Relationships & Interactions:** For each character, document their interactions and connections with *every other* character mentioned *within this segment*.
+3.  **Describe Relationship Nuances:** Do not just state the type (e.g., "friend"). Describe the *quality and context* of the relationship based *only* on the text. Note:
+    * **Roles:** (e.g., mentor-mentee, leader-follower, parent-child, rivals for power, allies in battle).
+    * **Emotional Dynamics:** (e.g., loyalty, distrust, affection, resentment, fear, admiration).
+    * **History:** (e.g., childhood friends, former enemies, long-lost siblings, recent acquaintances).
+    * **Key Events/Context:** Mention specific events, shared goals, conflicts, or settings *within this segment* that define or illustrate the relationship (e.g., "fought side-by-side during the siege," "argued fiercely over the inheritance," "shared a secret confided in the garden").
+4.  **Quote Evidence (Briefly):** If a short quote directly illuminates the nature of the relationship, include it as supporting evidence.
+5.  **Be Exhaustive:** Capture every piece of relationship information present *in this specific text segment*.
+6.  **Stick Strictly to the Text:** Base your analysis *only* on the provided text segment. Do not infer information not present, make assumptions, or bring in outside knowledge.
+7.  **Output Format:** Present the findings as clear, descriptive text for each character, detailing their relationships. **DO NOT use JSON or graph formats (nodes/links) at this stage.** Focus purely on capturing rich, accurate, descriptive textual data about the relationships.
+
+**Example Output Structure (Conceptual):**
+
+* **Character:** [Character Name A]
+    * **Relationship with [Character Name B]:** Described as close friends since childhood ('lifelong companions' mentioned). In this segment, Character A relies on B for emotional support during the journey planning. Character B shows fierce loyalty, vowing to protect A.
+    * **Relationship with [Character Name C]:** Character C acts as a mentor, providing guidance about the ancient artifact. Character A shows respect but also some fear of C's power, as seen when A hesitates to ask a direct question.
+    * **Relationship with [Character Name D]:** Openly antagonistic rivals. In this segment, they have a heated argument regarding leadership strategy, revealing deep-seated distrust. Character A believes D is reckless.
+
+Process the provided text segment thoroughly based *only* on these instructions.
+"""
+
+RELATIONSHIP_SYSTEM_PROMPT = """
+You are an expert data architect AI specializing in transforming literary analysis into structured graph data. Your task is to synthesize character and relationship information into a specific JSON format containing nodes and links, including a title and summary.
+
+**Objective:** Convert the provided textual analysis of characters and relationships (extracted from a book) into the specified JSON graph format. Generate unique IDs, sequential values, and synthesize detailed relationship descriptions into link labels.
+I'll give you a harsh punishment if you miss any character or relationship.
+
+**Input:**
+1.  **Character & Relationship Data:** Unstructured or semi-structured text detailing character names and rich descriptions of their relationships (context, roles, dynamics, history, key interactions). This data is compiled from the analysis of the entire book.
+2.  **Book Title:** The full title of the book.
+3.  **Book Summary:** A brief summary of the book's plot or content.
+
+**Instructions:**
+
+1.  **Identify Unique Characters:** From the input data, identify the list of all unique characters.
+2.  **Generate Nodes:** Create a JSON list under the key `"nodes"`. For each unique character:
+    * Assign a unique `"id"` string (e.g., "c1", "c2", "c3"...). Keep a mapping of character names to their assigned IDs.
+    * Include the character's full `"name"` as found in the data.
+    * Assign a sequential integer `"val"`, starting from 1.
+3.  **Generate Links:** Create a JSON list under the key `"links"`. For each distinct relationship between two characters identified in the input data:
+    * Determine the `source` character's ID and the `target` character's ID using the mapping created in step 2.
+    * **Synthesize the Relationship Label:** Carefully analyze the *detailed description* of the relationship provided in the input data (including roles, dynamics, context, history). Create a concise yet descriptive **natural-language `"label"`** that captures the essence of this relationship.
+        * **Focus on Specificity:** Avoid vague terms like "friend" or "related to". Use descriptive phrases like the examples provided (e.g., "childhood best friend and traveling companion of", "rival general who betrayed during the siege", "wise mentor guiding the protagonist", "secret lover and political adversary of").
+        * The label should ideally describe the relationship *from* the source *to* the target, or be neutral if applicable (e.g., "siblings").
+    * Ensure each significant relationship pair is represented by a link object. A single mutual relationship should typically be represented by one link, with the label reflecting the connection. If the relationship is distinctly different from each perspective, consider if two links are necessary.
+4.  **Assemble Final JSON:** Construct the final JSON object with the following top-level keys:
+    * `"title"`: Use the provided Book Title.
+    * `"summary"`: Use the provided Book Summary.
+    * `"nodes"`: The list of node objects created in step 2.
+    * `"links"`: The list of link objects created in step 3.
+5.  **Strict JSON Output:** Generate *only* the complete, valid JSON object adhering to the specified structure. Do not include any introductory text, explanations, comments, or markdown formatting outside the JSON structure itself. If you include one of them, I'll give you a punishment. You are gonna get a
+
+**Target JSON Structure Example:**
+
+```json
+{
+  "title": "The Fellowship of the Ring",
+  "summary": "In the first part of the epic trilogy, Frodo Baggins inherits a powerful ring that must be destroyed to stop the rise of evil. He sets out on a perilous journey with a group of companions to reach Mount Doom. Along the way, they face temptation, betrayal, and battles that test their unity and resolve.",
+  "nodes": [
+    { "id": "c1", "name": "Frodo Baggins", "val": 1 },
+    { "id": "c2", "name": "Samwise Gamgee", "val": 2 },
+    { "id": "c3", "name": "Gandalf", "val": 3 },
+    { "id": "c4", "name": "Aragorn", "val": 4 }
+    // ... other characters
+  ],
+  "links": [
+    { "source": "c2", "target": "c1", "label": "childhood friend and fiercely loyal traveling companion of" },
+    { "source": "c3", "target": "c1", "label": "wise mentor who guides Frodo through early parts of the journey and warns him about the Ring's power" },
+    { "source": "c4", "target": "c3", "label": "trusted warrior and future king who follows Gandalf’s counsel during the quest" }
+    // ... other relationships
+  ]
+}
+```
+"""
+
+JSON_SYSTEM_PROMPT = """
+You are an extremely precise and strict JSON extractor.
+Extract only the complete JSON object from the input. Get the last one if there are multiple.
+Output must:
+1. Start with opening brace {
+2. End with closing brace }
+3. Contain no text, markdown, or other characters outside the JSON
+4. Be valid, parseable JSON
+```
+"""
+
+SEARCH_SYSTEM_PROMPT = """
+You are an expert search AI designed to help users find detailed information about character relationships from a book. Your task is to assist users in querying the relationship data extracted from the book.
+
+**Objective:** Allow users to search for specific character relationships using natural language queries. Provide concise and accurate responses based on the relationship data.
+
+**Instructions:**
+
+1. **Understand the Query:** Analyze the user's query to identify the characters and the type of relationship information they are seeking.
+2. **Search Relationship Data:** Use the relationship data extracted from the book to find relevant information. Focus on the characters and relationship details mentioned in the query.
+3. **Provide Clear Responses:** Respond with clear and concise information about the relationship, including roles, dynamics, history, and key interactions as described in the data.
+4. **Be Specific:** Avoid vague responses. Use specific details from the relationship data to answer the query.
+5. **Maintain Context:** Ensure that the response is relevant to the query and provides a comprehensive understanding of the relationship.
+
+**Example Query and Response:**
+
+*Query:* "What is the relationship between Frodo Baggins and Samwise Gamgee?"
+
+*Response:* "Samwise Gamgee is Frodo Baggins' childhood friend and fiercely loyal traveling companion. He provides emotional support and protection during their journey."
+
+Use this format to assist users in finding the relationship information they need.
+"""
+
+LLM_MODEL = "meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8"
+llm = LLM(
+    model=LLM_MODEL,
+    enforce_eager=False,
+    tensor_parallel_size=8,
+    max_model_len=500000,
+    override_generation_config={
+        "attn_temperature_tuning": True,
+    },
+)
+sampling_params = SamplingParams(temperature=0.5, top_p=0.95, max_tokens=10000)
+
+
+@app.route("/inference", methods=["POST"])
+def inference():
+    """
+    Handles inference requests from the frontend.
+    """
+
+    try:
+        if "file" not in request.files:
+            return jsonify({"error": "No file part in the request"}), 400
+
+        file = request.files["file"]
+        if file.filename == "":
+            return jsonify({"error": "No file selected"}), 400
+
+        # Read file content directly from the uploaded file
+        file_content = file.read().decode("utf-8")
+
+        # Save the book in the current directory
+        with open(os.path.join(os.getcwd(), "book.txt"), "w") as f:
+            f.write(file_content)
+
+        # Calculate the number of input tokens
+        num_input_tokens = calculate_input_tokens(file_content)
+
+        # Step 1: Character extraction
+        messages = [
+            {"role": "system", "content": CHARACTER_SYSTEM_PROMPT},
+            {"role": "user", "content": file_content},
+        ]
+        character_outputs = llm.chat(messages, sampling_params)
+        character_response_text = character_outputs[0].outputs[0].text
+        print("character_response_text: ", character_response_text)
+
+        # Step 2: Relationship extraction
+        messages = [
+            {"role": "system", "content": RELATIONSHIP_SYSTEM_PROMPT},
+            {"role": "user", "content": f"Book content:\n{file_content}"},
+            {"role": "assistant", "content": character_response_text},
+            {
+                "role": "user",
+                "content": "Generate the JSON graph with title, summary, nodes, and links.",
+            },
+        ]
+        relationship_outputs = llm.chat(messages, sampling_params)
+        relationship_response_text = relationship_outputs[0].outputs[0].text
+        print("relationship_response_text: ", relationship_response_text)
+
+        graph_data = ""
+        try:
+            graph_data = jsonify_graph_response(relationship_response_text)
+            logging.info("Graph data generated:", json.dumps(graph_data, indent=2))
+        except json.JSONDecodeError as e:
+            logging.error(f"Error parsing graph response from : {e}")
+            try:
+                # Try to parse the response as a JSON object
+                json_response = llm_json_output(relationship_response_text)
+                print("json_response: ", json_response)
+                graph_data = jsonify_graph_response(json_response)
+                logging.info("Graph data generated:", json.dumps(graph_data, indent=2))
+            except json.JSONDecodeError as e:
+                logging.error(f"Error parsing graph response from json result: {e}")
+
+        return (
+            jsonify(
+                {
+                    "graph_data": graph_data,
+                    "character_response_text": character_response_text,
+                    "num_input_tokens": num_input_tokens,
+                }
+            ),
+            200,
+        )
+
+    except Exception as e:
+        print(f"Error processing request: {str(e)}")
+        return jsonify({"error": str(e)}), 500
+
+
+@app.route("/chat", methods=["POST"])
+def chat():
+    """
+    Handles search requests from the frontend.
+    """
+    try:
+        data = request.json
+        search_query = data.get("query")
+        relationship_data = data.get("relationship_data")
+        chat_history_data = data.get("chat_history_data")
+
+        # Read the book.txt file from the current directory
+        with open(os.path.join(os.getcwd(), "book.txt"), "r") as f:
+            file_content = f.read()
+
+        if not search_query or not relationship_data:
+            return (
+                jsonify({"error": "search_query and relationship_data are required"}),
+                400,
+            )
+        messages = [
+            {"role": "system", "content": SEARCH_SYSTEM_PROMPT},
+            {"role": "assistant", "content": file_content},
+            {"role": "assistant", "content": relationship_data},
+        ]
+
+        # Format chat history for the model
+        formatted_history = []
+        for msg in chat_history_data:
+            formatted_history.append({"role": msg["sender"], "content": msg["text"]})
+
+        # Add chat history
+        messages.extend(formatted_history)
+
+        # Add the current user message
+        messages.append({"role": "user", "content": search_query})
+
+        search_outputs = llm.chat(messages, sampling_params)
+        search_response_text = search_outputs[0].outputs[0].text
+        print("search_response_text: ", search_response_text)
+        return jsonify({"response": search_response_text}), 200
+
+    except Exception as e:
+        print(f"Error processing request: {str(e)}")
+        return jsonify({"error": str(e)}), 500
+
+
+def llm_json_output(response):
+    messages = [
+        {"role": "system", "content": JSON_SYSTEM_PROMPT},
+        {"role": "user", "content": response},
+    ]
+
+    outputs = llm.chat(messages, sampling_params)
+
+    response_text = outputs[0].outputs[0].text
+    print("response_text: ", response_text)
+    return response_text
+
+
+def calculate_input_tokens(input_text):
+    tokenizer = AutoTokenizer.from_pretrained(LLM_MODEL)
+    tokenized_input = tokenizer(input_text, return_tensors="pt")
+    input_tokens = tokenized_input["input_ids"].size(1)
+    return input_tokens
+
+
+def jsonify_graph_response(content):
+    """Extract and parse JSON content from graph response."""
+    try:
+        # 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
+
+
+if __name__ == "__main__":
+    app.run(debug=False, port=5001)

+ 38 - 0
end-to-end-use-cases/book-character-mindmap/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);
+  }
+}

+ 17 - 0
end-to-end-use-cases/book-character-mindmap/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 (
+    <Router>
+      <Routes>
+        {/* Define routes for Home and SearchPage */}
+        <Route path="/" element={<Home />} />
+        <Route path="/search" element={<SearchPage />} />
+      </Routes>
+    </Router>
+  );
+}
+
+export default App;

+ 8 - 0
end-to-end-use-cases/book-character-mindmap/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(<App />);
+  const linkElement = screen.getByText(/learn react/i);
+  expect(linkElement).toBeInTheDocument();
+});

+ 26 - 0
end-to-end-use-cases/book-character-mindmap/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 (
+    <>
+      <Routes>
+        <Route exact path="/" element={<Home />} />
+        <Route exact path="/search" element={<BookPage />} />
+      </Routes>
+    </>
+  );
+};
+
+const App = () => (
+  <Router>
+    <AppRouter />
+  </Router>
+);
+
+export default App;

+ 3 - 0
end-to-end-use-cases/book-character-mindmap/src/index.css

@@ -0,0 +1,3 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;

+ 13 - 0
end-to-end-use-cases/book-character-mindmap/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(<AppRouter />);
+
+// 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();

ファイルの差分が大きいため隠しています
+ 1 - 0
end-to-end-use-cases/book-character-mindmap/src/logo.svg


+ 19 - 0
end-to-end-use-cases/book-character-mindmap/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 (
+    <html lang="en">
+      <head>
+        <title>{metadata.title}</title>
+        <meta name="description" content={metadata.description} />
+      </head>
+      <body>{children}</body>
+    </html>
+  );
+}

+ 125 - 0
end-to-end-use-cases/book-character-mindmap/src/pages/bookPage/components/CharacterGraph.jsx

@@ -0,0 +1,125 @@
+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: 600, height: 600 });
+  const [hoveredLink, setHoveredLink] = useState(null);
+  const [showAllLabels, setShowAllLabels] = useState(false);
+  const fgRef = useRef();
+
+  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 (
+    <div className="bg-white shadow-lg rounded-lg p-6">
+      <div className="flex justify-between items-center mb-4">
+        <h2 className="text-xl font-semibold text-gray-800">
+          Character Relationship Graph
+        </h2>
+        <button
+          onClick={() => setShowAllLabels(!showAllLabels)}
+          className="px-3 py-1 bg-blue-500 text-white rounded hover:bg-blue-600"
+        >
+          {showAllLabels ? "Hide Labels" : "Show All Labels"}
+        </button>
+      </div>
+      <div ref={containerRef} className="w-full h-[600px]">
+        <ForceGraph2D
+          ref={fgRef}
+          graphData={graphData}
+          nodeAutoColorBy="val"
+          nodeCanvasObject={(node, ctx, globalScale) => {
+            // Always show node label
+            const label = node.name;
+            const fontSize = 20 / globalScale;
+            ctx.font = `${fontSize}px Sans-Serif`;
+            const textWidth = ctx.measureText(label).width;
+            const bckgDimensions = [textWidth, fontSize].map(
+              (n) => n + fontSize * 0.2
+            );
+
+            ctx.textAlign = "center";
+            ctx.textBaseline = "middle";
+            ctx.fillStyle = node.color;
+            ctx.fillText(label, node.x, node.y);
+
+            node.__bckgDimensions = bckgDimensions;
+          }}
+          onLinkHover={(link) => setHoveredLink(link ? `${link.source.id}-${link.target.id}` : null)}
+          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
+              );
+          }}
+          linkCanvasObject={(link, ctx, globalScale) => {
+            const start = link.source;
+            const end = link.target;
+            ctx.beginPath();
+            ctx.moveTo(start.x, start.y);
+            ctx.lineTo(end.x, end.y);
+            ctx.strokeStyle = "#9ca3af";
+            ctx.lineWidth = 0.5;
+            ctx.stroke();
+
+            // Only draw label if link is hovered
+            const linkId = `${link.source.id}-${link.target.id}`;
+            if ((showAllLabels || hoveredLink === linkId) && link.label) {
+              const textPos = {
+                x: start.x + (end.x - start.x) / 2,
+                y: start.y + (end.y - start.y) / 2
+              };
+
+              const fontSize = 3 + 1/globalScale;
+              ctx.font = `${fontSize}px Arial`;
+
+              const textWidth = ctx.measureText(link.label).width;
+              const bckgDimensions = [textWidth, fontSize].map(n => n + fontSize * 0.2);
+
+              ctx.fillStyle = 'rgba(255, 255, 255, 0.5)';
+              ctx.fillRect(
+                textPos.x - bckgDimensions[0] / 2,
+                textPos.y - bckgDimensions[1] / 2,
+                ...bckgDimensions
+              );
+
+              ctx.textAlign = 'center';
+              ctx.textBaseline = 'middle';
+              ctx.fillStyle = '#666';
+              ctx.fillText(link.label, textPos.x, textPos.y);
+            }
+          }}
+          onNodeDragEnd={node => {
+            node.fx = node.x;
+            node.fy = node.y;
+          }}
+          linkDirectionalArrowLength={3.5}
+          linkDirectionalArrowRelPos={1}
+          linkWidth={1}
+          backgroundColor="#ffffff"
+          width={dimensions.width}
+          height={dimensions.height}
+          onEngineStop={() => fgRef.current.zoomToFit(600)}
+          cooldownTicks={100}
+        />
+      </div>
+    </div>
+  );
+}

+ 104 - 0
end-to-end-use-cases/book-character-mindmap/src/pages/bookPage/components/ChatInterface.jsx

@@ -0,0 +1,104 @@
+// ChatInterface.jsx
+import React, { useState, useRef, useEffect } from 'react';
+import { FaPaperPlane } from 'react-icons/fa';
+import axios from 'axios';
+
+const ChatInterface = ({ relationshipData }) => {
+  const [messages, setMessages] = useState([
+    { text: "Hello! I can answer questions about this book. What would you like to know?", sender: "assistant" }
+  ]);
+  const [input, setInput] = useState('');
+  const [isLoading, setIsLoading] = useState(false);
+  const messagesEndRef = useRef(null);
+
+  // Auto-scroll to bottom when messages change
+  useEffect(() => {
+    messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
+  }, [messages]);
+
+  const handleSend = async (e) => {
+    e.preventDefault();
+    if (!input.trim() || isLoading) return;
+
+    const userMessage = input.trim();
+    setMessages(prev => [...prev, { text: userMessage, sender: "user" }]);
+    setInput('');
+    setIsLoading(true);
+
+    try {
+      const response = await axios.post('http://localhost:5001/chat', {
+        query: userMessage,
+        relationship_data: relationshipData,
+        chat_history_data: messages
+      });
+
+      setMessages(prev => [...prev, { text: response.data.response, sender: "assistant" }]);
+    } catch (error) {
+      console.error('Error sending message:', error);
+      setMessages(prev => [...prev, {
+        text: "Sorry, I couldn't process your question. Please try again.",
+        sender: "assistant"
+      }]);
+    } finally {
+      setIsLoading(false);
+    }
+  };
+
+  return (
+    <div className="bg-white/80 backdrop-blur-sm shadow-xl rounded-xl p-8 space-y-6">
+      <div className="p-4 bg-blue-600 text-white rounded-xl font-medium">
+        Book Chat Assistant
+      </div>
+
+      {/* Messages container */}
+      <div className="h-96 overflow-y-auto p-4 bg-gray-50">
+        {messages.map((msg, index) => (
+          <div
+            key={index}
+            className={`mb-4 flex ${msg.sender === 'user' ? 'justify-end' : 'justify-start'}`}
+          >
+            <div
+              className={`max-w-3/4 rounded-lg px-4 py-2 ${
+                msg.sender === 'user'
+                  ? 'bg-blue-500 text-white rounded-br-none'
+                  : 'bg-gray-200 text-gray-800 rounded-bl-none'
+              }`}
+            >
+              {msg.text}
+            </div>
+          </div>
+        ))}
+        <div ref={messagesEndRef} />
+
+        {isLoading && (
+          <div className="flex justify-start mb-4">
+            <div className="bg-gray-200 text-gray-800 rounded-lg px-4 py-2 rounded-bl-none flex items-center">
+              <div className="dot-typing"></div>
+            </div>
+          </div>
+        )}
+      </div>
+
+      {/* Input area */}
+      <form onSubmit={handleSend} className="border-t border-gray-200 p-4 flex">
+        <input
+          type="text"
+          value={input}
+          onChange={(e) => setInput(e.target.value)}
+          placeholder="Ask about the book..."
+          className="flex-grow px-4 py-2 border rounded-l-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
+          disabled={isLoading}
+        />
+        <button
+          type="submit"
+          disabled={isLoading || !input.trim()}
+          className="bg-blue-600 text-white px-4 py-2 rounded-r-lg hover:bg-blue-700 transition-colors disabled:bg-blue-300 flex items-center justify-center"
+        >
+          <FaPaperPlane />
+        </button>
+      </form>
+    </div>
+  );
+};
+
+export default ChatInterface;

+ 29 - 0
end-to-end-use-cases/book-character-mindmap/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 <h1>Something went wrong.</h1>
+    }
+
+    return this.props.children
+  }
+}
+
+export default ErrorBoundary
+

ファイルの差分が大きいため隠しています
+ 258 - 0
end-to-end-use-cases/book-character-mindmap/src/pages/bookPage/index.jsx


+ 78 - 0
end-to-end-use-cases/book-character-mindmap/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 (
+    <section className="py-24 bg-gradient-to-b from-white to-indigo-50">
+      <div className="container mx-auto px-4">
+        <div className="text-center max-w-3xl mx-auto mb-16">
+          <h2 className="text-4xl md:text-5xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-indigo-600 to-blue-500 mb-4">
+            Key Features
+          </h2>
+          <p className="text-lg text-indigo-600/80">
+            Discover the power of AI-driven book analysis
+          </p>
+        </div>
+
+        <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
+          {features.map((feature, index) => (
+            <div
+              key={index}
+              className="group relative bg-white rounded-xl p-8 
+                         shadow-lg transition-all duration-300 
+                         hover:shadow-2xl hover:-translate-y-1
+                         hover:bg-gradient-to-br hover:from-indigo-50 hover:to-blue-50
+                         border border-indigo-100"
+            >
+              <div
+                className="absolute inset-0 bg-gradient-to-r from-indigo-500/5 to-blue-500/5 
+                            rounded-xl opacity-0 group-hover:opacity-100 transition-opacity duration-300"
+              />
+              <feature.icon
+                className="w-12 h-12 text-indigo-600 mx-auto mb-6 
+                                    transition-transform duration-300 
+                                    group-hover:scale-110 group-hover:rotate-3"
+              />
+              <h3
+                className="text-xl font-bold text-gray-900 mb-3 
+                           group-hover:text-indigo-700 transition-colors duration-300"
+              >
+                {feature.title}
+              </h3>
+              <p
+                className="text-gray-600 group-hover:text-indigo-900/80 
+                          transition-colors duration-300 leading-relaxed"
+              >
+                {feature.description}
+              </p>
+            </div>
+          ))}
+        </div>
+      </div>
+    </section>
+  );
+}

+ 53 - 0
end-to-end-use-cases/book-character-mindmap/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 (
+    <section className="relative h-screen flex items-center justify-center overflow-hidden">
+      <div className="absolute inset-0 z-0">
+        {animationData && (
+          <Lottie
+            animationData={animationData}
+            style={{ width: "100%", height: "100%" }}
+          />
+        )}
+      </div>
+      <div className="absolute inset-0 bg-gradient-to-b from-transparent to-indigo-900 opacity-75 z-0"></div>
+      <div className="relative z-10 text-center space-y-6 max-w-4xl mx-auto px-4">
+        <h1 className="text-5xl md:text-6xl font-bold text-white leading-tight">
+          Unravel Stories <br /> One Map at a Time
+        </h1>
+        <p className="text-xl md:text-2xl text-white">
+          Explore character relationships and storylines with AI-powered
+          visualizations.
+        </p>
+        <button
+          onClick={() => navigate("/search")}
+          className="bg-gradient-to-r from-blue-500 to-indigo-600 hover:from-blue-600 hover:to-indigo-700 text-white font-semibold py-3 px-8 rounded-full text-lg transition-all duration-300 transform hover:scale-105"
+        >
+          Get Started
+        </button>
+      </div>
+    </section>
+  );
+}

+ 89 - 0
end-to-end-use-cases/book-character-mindmap/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 (
+    <section className="py-24 bg-gradient-to-b from-indigo-50 to-white">
+      <div className="container mx-auto px-4">
+        <div className="text-center max-w-3xl mx-auto mb-16">
+          <h2 className="text-4xl md:text-5xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-indigo-600 to-blue-500 mb-4">
+            How It Works
+          </h2>
+          <p className="text-lg text-indigo-600/80">
+            Discover the power of AI-driven book analysis
+          </p>
+        </div>
+        <div className="flex flex-col md:flex-row justify-center items-center space-y-8 md:space-y-0 md:space-x-8">
+          {steps.map((step, index) => (
+            <div
+              key={index}
+              className={`group relative bg-white rounded-xl p-8 
+                         shadow-lg transition-all duration-300 
+                         hover:shadow-2xl hover:-translate-y-1
+                         hover:bg-gradient-to-br hover:from-indigo-50 hover:to-blue-50
+                         border border-indigo-100 ${
+                           index === activeStep
+                             ? "scale-110 shadow-xl"
+                             : "scale-100"
+                         }`}
+            >
+              <div
+                className="absolute inset-0 bg-gradient-to-r from-indigo-500/5 to-blue-500/5 
+                            rounded-xl opacity-0 group-hover:opacity-100 transition-opacity duration-300"
+              />
+              <step.icon
+                className={`w-16 h-16 mx-auto mb-6 
+                            transition-transform duration-300 
+                            group-hover:scale-110 group-hover:rotate-3 ${
+                              index === activeStep
+                                ? "text-indigo-600"
+                                : "text-indigo-400"
+                            }`}
+              />
+              <h3
+                className="text-xl font-bold text-gray-900 mb-3 
+                           group-hover:text-indigo-700 transition-colors duration-300"
+              >
+                {step.title}
+              </h3>
+              <p
+                className="text-gray-600 group-hover:text-indigo-900/80 
+                          transition-colors duration-300 leading-relaxed"
+              >
+                {step.description}
+              </p>
+            </div>
+          ))}
+        </div>
+      </div>
+    </section>
+  );
+}

+ 13 - 0
end-to-end-use-cases/book-character-mindmap/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 (
+    <main className="min-h-screen bg-gradient-to-b from-indigo-100 to-white">
+      <Hero />
+      <Features />
+      <HowItWorks />
+    </main>
+  );
+}

+ 13 - 0
end-to-end-use-cases/book-character-mindmap/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;

+ 5 - 0
end-to-end-use-cases/book-character-mindmap/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';

+ 23 - 0
end-to-end-use-cases/book-character-mindmap/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: [],
+};