Spaces:
Sleeping
Sleeping
Replace with updated NoteGenie version
Browse files- .dockerignore +14 -0
- .gitattributes +0 -35
- .gitignore +5 -2
- Dockerfile +4 -2
- README.md +24 -0
- app.py +66 -15
- static/js/main.js +73 -7
- templates/index.html +24 -0
- utils/error_handler.py +0 -24
.dockerignore
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__pycache__/
|
| 2 |
+
*.py[cod]
|
| 3 |
+
*$py.class
|
| 4 |
+
*.so
|
| 5 |
+
.env
|
| 6 |
+
.venv
|
| 7 |
+
env/
|
| 8 |
+
venv/
|
| 9 |
+
ENV/
|
| 10 |
+
.idea/
|
| 11 |
+
.vscode/
|
| 12 |
+
.git/
|
| 13 |
+
flask_session/
|
| 14 |
+
*.log
|
.gitattributes
DELETED
|
@@ -1,35 +0,0 @@
|
|
| 1 |
-
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
-
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
-
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
-
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
-
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
-
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
-
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
-
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
-
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
-
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
-
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
-
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
-
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
-
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
-
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
-
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
-
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
-
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
-
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
-
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
-
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
-
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
-
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
-
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
-
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
-
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
-
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
-
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
-
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
-
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
-
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
-
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
-
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
-
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
-
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.gitignore
CHANGED
|
@@ -1,2 +1,5 @@
|
|
| 1 |
-
|
| 2 |
-
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Ignore Flask session data
|
| 2 |
+
/flask_session/
|
| 3 |
+
|
| 4 |
+
# Ignore Python cache files
|
| 5 |
+
/utils/__pycache__/
|
Dockerfile
CHANGED
|
@@ -12,8 +12,10 @@ COPY . .
|
|
| 12 |
# Create necessary directories
|
| 13 |
RUN mkdir -p flask_session
|
| 14 |
|
| 15 |
-
# Set permissions for flask_session directory
|
| 16 |
-
RUN chmod -R 777 flask_session
|
|
|
|
|
|
|
| 17 |
|
| 18 |
# Set environment variables
|
| 19 |
ENV FLASK_APP=app.py
|
|
|
|
| 12 |
# Create necessary directories
|
| 13 |
RUN mkdir -p flask_session
|
| 14 |
|
| 15 |
+
# Set permissions for flask_session directory and touch API key file
|
| 16 |
+
RUN chmod -R 777 flask_session && \
|
| 17 |
+
touch api_key.txt && \
|
| 18 |
+
chmod 666 api_key.txt
|
| 19 |
|
| 20 |
# Set environment variables
|
| 21 |
ENV FLASK_APP=app.py
|
README.md
CHANGED
|
@@ -21,6 +21,15 @@ NoteGenie is an AI-powered Jupyter notebook generator that uses Google's Gemini
|
|
| 21 |
- Download notebooks as .ipynb files
|
| 22 |
- Streaming responses for a better user experience
|
| 23 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
## Local Development
|
| 25 |
|
| 26 |
1. Install dependencies:
|
|
@@ -35,6 +44,21 @@ NoteGenie is an AI-powered Jupyter notebook generator that uses Google's Gemini
|
|
| 35 |
|
| 36 |
3. Open http://localhost:5000 in your browser
|
| 37 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
## API Key Setup
|
| 39 |
|
| 40 |
NoteGenie requires a Google Gemini API key. Users can set their own API key in the web interface.
|
|
|
|
| 21 |
- Download notebooks as .ipynb files
|
| 22 |
- Streaming responses for a better user experience
|
| 23 |
|
| 24 |
+
## Deployment on Hugging Face Spaces
|
| 25 |
+
|
| 26 |
+
1. Create a new Space on Hugging Face
|
| 27 |
+
2. Choose Docker template
|
| 28 |
+
3. Upload this repository to the Space
|
| 29 |
+
4. Set the following environment variables in your Space settings:
|
| 30 |
+
- SECRET_KEY: A secure random string for Flask sessions
|
| 31 |
+
- PORT: 7860 (default for Hugging Face Spaces)
|
| 32 |
+
|
| 33 |
## Local Development
|
| 34 |
|
| 35 |
1. Install dependencies:
|
|
|
|
| 44 |
|
| 45 |
3. Open http://localhost:5000 in your browser
|
| 46 |
|
| 47 |
+
## Using Docker
|
| 48 |
+
|
| 49 |
+
Build and run the Docker container:
|
| 50 |
+
|
| 51 |
+
```bash
|
| 52 |
+
docker build -t notegenie .
|
| 53 |
+
docker run -p 7860:7860 -e SECRET_KEY=your_secret_key notegenie
|
| 54 |
+
```
|
| 55 |
+
|
| 56 |
+
## Environment Variables
|
| 57 |
+
|
| 58 |
+
- SECRET_KEY: Secret key for Flask session encryption
|
| 59 |
+
- PORT: Port to run the application on (defaults to 5000 locally, 7860 for Hugging Face)
|
| 60 |
+
- FLASK_ENV: Set to "development" for debug mode, "production" for production
|
| 61 |
+
|
| 62 |
## API Key Setup
|
| 63 |
|
| 64 |
NoteGenie requires a Google Gemini API key. Users can set their own API key in the web interface.
|
app.py
CHANGED
|
@@ -4,9 +4,17 @@ import google.generativeai as genai
|
|
| 4 |
import json
|
| 5 |
import uuid
|
| 6 |
import os
|
|
|
|
| 7 |
from utils.ai_helpers import generate_notebook, stream_notebook_generation, stream_notebook_edit, edit_notebook
|
| 8 |
from utils.notebook_helpers import format_notebook, extract_notebook_info
|
| 9 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
app = Flask(__name__)
|
| 11 |
app.config["SECRET_KEY"] = os.environ.get("SECRET_KEY", "notegenie-secret-key-change-in-production")
|
| 12 |
app.config["SESSION_TYPE"] = "filesystem"
|
|
@@ -16,6 +24,9 @@ app.config["PERMANENT_SESSION_LIFETIME"] = 60 * 60 * 24 * 30 # 30 days
|
|
| 16 |
app.config["SESSION_FILE_DIR"] = os.path.join(os.path.dirname(os.path.abspath(__file__)), "flask_session")
|
| 17 |
os.makedirs(app.config["SESSION_FILE_DIR"], exist_ok=True) # Ensure directory exists
|
| 18 |
|
|
|
|
|
|
|
|
|
|
| 19 |
Session(app)
|
| 20 |
|
| 21 |
# Map front-end model names to API model names
|
|
@@ -28,8 +39,22 @@ MODEL_MAPPING = {
|
|
| 28 |
def get_api_model_name(frontend_model_name):
|
| 29 |
return MODEL_MAPPING.get(frontend_model_name, frontend_model_name)
|
| 30 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
@app.route("/", methods=["GET"])
|
| 32 |
def index():
|
|
|
|
|
|
|
|
|
|
| 33 |
return render_template("index.html")
|
| 34 |
|
| 35 |
@app.route("/set_api_key", methods=["POST"])
|
|
@@ -44,19 +69,26 @@ def set_api_key():
|
|
| 44 |
model = genai.GenerativeModel("gemini-2.0-pro-exp-02-05")
|
| 45 |
response = model.generate_content("Say 'API key is valid'")
|
| 46 |
|
| 47 |
-
# Store API key in session
|
| 48 |
session.permanent = True
|
| 49 |
session["api_key"] = api_key
|
| 50 |
|
|
|
|
| 51 |
return jsonify({"success": True})
|
| 52 |
except Exception as e:
|
|
|
|
| 53 |
return jsonify({"success": False, "message": str(e)}), 400
|
| 54 |
|
| 55 |
@app.route("/generate_notebook", methods=["GET", "POST"])
|
| 56 |
def generate_notebook_route():
|
| 57 |
-
|
|
|
|
|
|
|
| 58 |
return jsonify({"success": False, "message": "API key not set"}), 401
|
| 59 |
|
|
|
|
|
|
|
|
|
|
| 60 |
# Handle both GET (for streaming) and POST requests
|
| 61 |
if request.method == "GET":
|
| 62 |
prompt = request.args.get("prompt")
|
|
@@ -75,8 +107,6 @@ def generate_notebook_route():
|
|
| 75 |
# Map the frontend model name to the API model name
|
| 76 |
api_model_name = get_api_model_name(model_name)
|
| 77 |
|
| 78 |
-
genai.configure(api_key=session["api_key"])
|
| 79 |
-
|
| 80 |
try:
|
| 81 |
# OPTIMIZATION: If format_only is True, skip the AI call and just format the provided content
|
| 82 |
if request.method == "POST" and format_only:
|
|
@@ -105,17 +135,14 @@ def generate_notebook_route():
|
|
| 105 |
"description": notebook_info["description"]
|
| 106 |
})
|
| 107 |
except Exception as e:
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
import traceback
|
| 111 |
-
traceback.print_exc()
|
| 112 |
-
# Still raise the user-friendly message to the UI if needed
|
| 113 |
-
raise Exception("Failed to generate notebook. Please check terminal for details.")
|
| 114 |
|
| 115 |
@app.route("/prepare_edit_notebook", methods=["POST"])
|
| 116 |
def prepare_edit_notebook():
|
| 117 |
"""Store the notebook in the session for editing."""
|
| 118 |
-
|
|
|
|
| 119 |
return jsonify({"success": False, "message": "API key not set"}), 401
|
| 120 |
|
| 121 |
data = request.json
|
|
@@ -125,15 +152,19 @@ def prepare_edit_notebook():
|
|
| 125 |
return jsonify({"success": False, "message": "Notebook content is required"}), 400
|
| 126 |
|
| 127 |
# Store the notebook in the session for later access
|
| 128 |
-
session["current_notebook"] = notebook_json
|
| 129 |
|
| 130 |
return jsonify({"success": True})
|
| 131 |
|
| 132 |
@app.route("/edit_notebook", methods=["GET", "POST"])
|
| 133 |
def edit_notebook_route():
|
| 134 |
-
|
|
|
|
| 135 |
return jsonify({"success": False, "message": "API key not set"}), 401
|
| 136 |
|
|
|
|
|
|
|
|
|
|
| 137 |
# Get edit prompt and current notebook
|
| 138 |
if request.method == "GET":
|
| 139 |
edit_prompt = request.args.get("edit_prompt")
|
|
@@ -141,6 +172,8 @@ def edit_notebook_route():
|
|
| 141 |
stream = request.args.get("stream", "true").lower() == "true"
|
| 142 |
# For GET streaming requests, get notebook from session
|
| 143 |
notebook_json = session.get("current_notebook")
|
|
|
|
|
|
|
| 144 |
else:
|
| 145 |
data = request.json
|
| 146 |
edit_prompt = data.get("edit_prompt")
|
|
@@ -157,8 +190,6 @@ def edit_notebook_route():
|
|
| 157 |
# Map the frontend model name to the API model name
|
| 158 |
api_model_name = get_api_model_name(model_name)
|
| 159 |
|
| 160 |
-
genai.configure(api_key=session["api_key"])
|
| 161 |
-
|
| 162 |
try:
|
| 163 |
if stream:
|
| 164 |
return stream_notebook_edit(edit_prompt, notebook_json, api_model_name)
|
|
@@ -200,6 +231,26 @@ def download_notebook():
|
|
| 200 |
|
| 201 |
return response
|
| 202 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 203 |
if __name__ == "__main__":
|
| 204 |
port = int(os.environ.get("PORT", 5000))
|
| 205 |
app.run(host="0.0.0.0", port=port, debug=(os.environ.get("FLASK_ENV") == "development"))
|
|
|
|
| 4 |
import json
|
| 5 |
import uuid
|
| 6 |
import os
|
| 7 |
+
import logging
|
| 8 |
from utils.ai_helpers import generate_notebook, stream_notebook_generation, stream_notebook_edit, edit_notebook
|
| 9 |
from utils.notebook_helpers import format_notebook, extract_notebook_info
|
| 10 |
|
| 11 |
+
# Configure logging
|
| 12 |
+
logging.basicConfig(
|
| 13 |
+
level=logging.INFO,
|
| 14 |
+
format='%(asctime)s - notegenie - %(levelname)s - %(message)s'
|
| 15 |
+
)
|
| 16 |
+
logger = logging.getLogger()
|
| 17 |
+
|
| 18 |
app = Flask(__name__)
|
| 19 |
app.config["SECRET_KEY"] = os.environ.get("SECRET_KEY", "notegenie-secret-key-change-in-production")
|
| 20 |
app.config["SESSION_TYPE"] = "filesystem"
|
|
|
|
| 24 |
app.config["SESSION_FILE_DIR"] = os.path.join(os.path.dirname(os.path.abspath(__file__)), "flask_session")
|
| 25 |
os.makedirs(app.config["SESSION_FILE_DIR"], exist_ok=True) # Ensure directory exists
|
| 26 |
|
| 27 |
+
# Set a more permissive file mode for session files to avoid permission issues
|
| 28 |
+
app.config["SESSION_FILE_MODE"] = 0o666
|
| 29 |
+
|
| 30 |
Session(app)
|
| 31 |
|
| 32 |
# Map front-end model names to API model names
|
|
|
|
| 39 |
def get_api_model_name(frontend_model_name):
|
| 40 |
return MODEL_MAPPING.get(frontend_model_name, frontend_model_name)
|
| 41 |
|
| 42 |
+
# Function to get API key from different sources
|
| 43 |
+
def get_api_key():
|
| 44 |
+
# Try to get from session first
|
| 45 |
+
api_key = session.get("api_key")
|
| 46 |
+
|
| 47 |
+
# Try to get from request header or param (for direct API calls)
|
| 48 |
+
if not api_key:
|
| 49 |
+
api_key = request.headers.get("X-API-Key") or request.args.get("api_key")
|
| 50 |
+
|
| 51 |
+
return api_key
|
| 52 |
+
|
| 53 |
@app.route("/", methods=["GET"])
|
| 54 |
def index():
|
| 55 |
+
# Test session functionality
|
| 56 |
+
session["session_test"] = True
|
| 57 |
+
logger.info(f"Session check - variables: {list(session.keys())}")
|
| 58 |
return render_template("index.html")
|
| 59 |
|
| 60 |
@app.route("/set_api_key", methods=["POST"])
|
|
|
|
| 69 |
model = genai.GenerativeModel("gemini-2.0-pro-exp-02-05")
|
| 70 |
response = model.generate_content("Say 'API key is valid'")
|
| 71 |
|
| 72 |
+
# Store API key in session only
|
| 73 |
session.permanent = True
|
| 74 |
session["api_key"] = api_key
|
| 75 |
|
| 76 |
+
logger.info("API key successfully set and validated")
|
| 77 |
return jsonify({"success": True})
|
| 78 |
except Exception as e:
|
| 79 |
+
logger.error(f"API key validation error: {str(e)}")
|
| 80 |
return jsonify({"success": False, "message": str(e)}), 400
|
| 81 |
|
| 82 |
@app.route("/generate_notebook", methods=["GET", "POST"])
|
| 83 |
def generate_notebook_route():
|
| 84 |
+
api_key = get_api_key()
|
| 85 |
+
if not api_key:
|
| 86 |
+
logger.warning("Generate notebook request without API key")
|
| 87 |
return jsonify({"success": False, "message": "API key not set"}), 401
|
| 88 |
|
| 89 |
+
# Always configure genai with the API key for each request
|
| 90 |
+
genai.configure(api_key=api_key)
|
| 91 |
+
|
| 92 |
# Handle both GET (for streaming) and POST requests
|
| 93 |
if request.method == "GET":
|
| 94 |
prompt = request.args.get("prompt")
|
|
|
|
| 107 |
# Map the frontend model name to the API model name
|
| 108 |
api_model_name = get_api_model_name(model_name)
|
| 109 |
|
|
|
|
|
|
|
| 110 |
try:
|
| 111 |
# OPTIMIZATION: If format_only is True, skip the AI call and just format the provided content
|
| 112 |
if request.method == "POST" and format_only:
|
|
|
|
| 135 |
"description": notebook_info["description"]
|
| 136 |
})
|
| 137 |
except Exception as e:
|
| 138 |
+
logger.error(f"Error generating notebook: {str(e)}")
|
| 139 |
+
return jsonify({"success": False, "message": str(e)}), 500
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
|
| 141 |
@app.route("/prepare_edit_notebook", methods=["POST"])
|
| 142 |
def prepare_edit_notebook():
|
| 143 |
"""Store the notebook in the session for editing."""
|
| 144 |
+
api_key = get_api_key()
|
| 145 |
+
if not api_key:
|
| 146 |
return jsonify({"success": False, "message": "API key not set"}), 401
|
| 147 |
|
| 148 |
data = request.json
|
|
|
|
| 152 |
return jsonify({"success": False, "message": "Notebook content is required"}), 400
|
| 153 |
|
| 154 |
# Store the notebook in the session for later access
|
| 155 |
+
session["current_notebook"] = json.dumps(notebook_json) # Store as JSON string
|
| 156 |
|
| 157 |
return jsonify({"success": True})
|
| 158 |
|
| 159 |
@app.route("/edit_notebook", methods=["GET", "POST"])
|
| 160 |
def edit_notebook_route():
|
| 161 |
+
api_key = get_api_key()
|
| 162 |
+
if not api_key:
|
| 163 |
return jsonify({"success": False, "message": "API key not set"}), 401
|
| 164 |
|
| 165 |
+
# Always configure genai with the API key for each request
|
| 166 |
+
genai.configure(api_key=api_key)
|
| 167 |
+
|
| 168 |
# Get edit prompt and current notebook
|
| 169 |
if request.method == "GET":
|
| 170 |
edit_prompt = request.args.get("edit_prompt")
|
|
|
|
| 172 |
stream = request.args.get("stream", "true").lower() == "true"
|
| 173 |
# For GET streaming requests, get notebook from session
|
| 174 |
notebook_json = session.get("current_notebook")
|
| 175 |
+
if notebook_json:
|
| 176 |
+
notebook_json = json.loads(notebook_json) # Parse JSON string back to dict
|
| 177 |
else:
|
| 178 |
data = request.json
|
| 179 |
edit_prompt = data.get("edit_prompt")
|
|
|
|
| 190 |
# Map the frontend model name to the API model name
|
| 191 |
api_model_name = get_api_model_name(model_name)
|
| 192 |
|
|
|
|
|
|
|
| 193 |
try:
|
| 194 |
if stream:
|
| 195 |
return stream_notebook_edit(edit_prompt, notebook_json, api_model_name)
|
|
|
|
| 231 |
|
| 232 |
return response
|
| 233 |
|
| 234 |
+
# Add a session diagnostic endpoint
|
| 235 |
+
@app.route("/check_session", methods=["GET"])
|
| 236 |
+
def check_session():
|
| 237 |
+
# For debugging only - would be disabled in production
|
| 238 |
+
session_data = {
|
| 239 |
+
"has_api_key": "api_key" in session,
|
| 240 |
+
"session_vars": list(session.keys()),
|
| 241 |
+
"session_file_dir_exists": os.path.exists(app.config["SESSION_FILE_DIR"]),
|
| 242 |
+
"session_file_dir_writable": os.access(app.config["SESSION_FILE_DIR"], os.W_OK),
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
# Check if running on Hugging Face Spaces
|
| 246 |
+
is_hf_space = "SPACE_ID" in os.environ
|
| 247 |
+
session_data["is_huggingface_space"] = is_hf_space
|
| 248 |
+
|
| 249 |
+
if is_hf_space:
|
| 250 |
+
logger.info("Running on Hugging Face Spaces environment")
|
| 251 |
+
|
| 252 |
+
return jsonify(session_data)
|
| 253 |
+
|
| 254 |
if __name__ == "__main__":
|
| 255 |
port = int(os.environ.get("PORT", 5000))
|
| 256 |
app.run(host="0.0.0.0", port=port, debug=(os.environ.get("FLASK_ENV") == "development"))
|
static/js/main.js
CHANGED
|
@@ -111,6 +111,14 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
| 111 |
// Prevent multiple rapid clicks
|
| 112 |
if (isActionInProgress) return;
|
| 113 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
// Get the prompt text and check if it's empty
|
| 115 |
const prompt = promptInputEl.value.trim();
|
| 116 |
|
|
@@ -253,12 +261,26 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
| 253 |
const aiMessageId = 'ai-typing-' + Date.now();
|
| 254 |
addTypingIndicator(aiMessageId);
|
| 255 |
|
|
|
|
|
|
|
|
|
|
| 256 |
handleEditStreamingResponse(editPrompt, currentNotebook, modelName, aiMessageId);
|
| 257 |
|
| 258 |
// Clear the input field after sending
|
| 259 |
promptInputEl.value = '';
|
| 260 |
}
|
| 261 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 262 |
function handleStreamingResponse(prompt, modelName, aiMessageId) {
|
| 263 |
aiResponseText = '';
|
| 264 |
|
|
@@ -295,12 +317,29 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
| 295 |
}
|
| 296 |
}, 5000); // Check every 5 seconds
|
| 297 |
|
| 298 |
-
//
|
| 299 |
-
|
|
|
|
|
|
|
|
|
|
| 300 |
prompt: prompt,
|
| 301 |
model: modelName,
|
| 302 |
stream: true
|
| 303 |
-
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 304 |
|
| 305 |
eventSource.onmessage = function(event) {
|
| 306 |
// Update our last-activity timestamp
|
|
@@ -411,14 +450,24 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
| 411 |
let lastPreviewUpdate = 0;
|
| 412 |
const PREVIEW_UPDATE_INTERVAL = 1000; // Update preview every 1 second during streaming, same as generate
|
| 413 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 414 |
// First, send the notebook data to the server so it's available in the session
|
| 415 |
fetch('/prepare_edit_notebook', {
|
| 416 |
method: 'POST',
|
| 417 |
headers: {
|
| 418 |
'Content-Type': 'application/json',
|
|
|
|
|
|
|
| 419 |
},
|
| 420 |
body: JSON.stringify({
|
| 421 |
-
notebook:
|
| 422 |
})
|
| 423 |
})
|
| 424 |
.then(response => {
|
|
@@ -429,12 +478,28 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
| 429 |
})
|
| 430 |
.then(data => {
|
| 431 |
if (data.success) {
|
| 432 |
-
|
| 433 |
-
|
|
|
|
|
|
|
| 434 |
edit_prompt: editPrompt,
|
| 435 |
model: modelName,
|
| 436 |
stream: true
|
| 437 |
-
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 438 |
|
| 439 |
// Track the last time we got a chunk - for timeout detection
|
| 440 |
let lastChunkTime = Date.now();
|
|
@@ -569,6 +634,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
| 569 |
.catch(error => {
|
| 570 |
console.error('Error preparing notebook for edit:', error);
|
| 571 |
updateAiMessage(aiMessageId, '**Error:** ' + error.message);
|
|
|
|
| 572 |
setGeneratingState(false);
|
| 573 |
});
|
| 574 |
}
|
|
|
|
| 111 |
// Prevent multiple rapid clicks
|
| 112 |
if (isActionInProgress) return;
|
| 113 |
|
| 114 |
+
// Check if API key is set
|
| 115 |
+
const apiKey = localStorage.getItem('notegenie_api_key');
|
| 116 |
+
if (!apiKey) {
|
| 117 |
+
addSystemMessage('<i class="bi bi-exclamation-triangle"></i> API key is not set. Please set your API key first.');
|
| 118 |
+
showApiKeyModal();
|
| 119 |
+
return;
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
// Get the prompt text and check if it's empty
|
| 123 |
const prompt = promptInputEl.value.trim();
|
| 124 |
|
|
|
|
| 261 |
const aiMessageId = 'ai-typing-' + Date.now();
|
| 262 |
addTypingIndicator(aiMessageId);
|
| 263 |
|
| 264 |
+
// Update AI message to show we're starting the edit
|
| 265 |
+
updateAiMessage(aiMessageId, "**NoteGenie:** Starting to edit your notebook...");
|
| 266 |
+
|
| 267 |
handleEditStreamingResponse(editPrompt, currentNotebook, modelName, aiMessageId);
|
| 268 |
|
| 269 |
// Clear the input field after sending
|
| 270 |
promptInputEl.value = '';
|
| 271 |
}
|
| 272 |
|
| 273 |
+
// Create a custom EventSource that supports headers
|
| 274 |
+
function createEventSourceWithHeaders(url, headers) {
|
| 275 |
+
if (typeof EventSourcePolyfill !== 'undefined') {
|
| 276 |
+
return new EventSourcePolyfill(url, { headers: headers });
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
// If native EventSource is all we have but we need headers,
|
| 280 |
+
// we fall back to using URL parameters for authentication
|
| 281 |
+
return new EventSource(url);
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
function handleStreamingResponse(prompt, modelName, aiMessageId) {
|
| 285 |
aiResponseText = '';
|
| 286 |
|
|
|
|
| 317 |
}
|
| 318 |
}, 5000); // Check every 5 seconds
|
| 319 |
|
| 320 |
+
// Get API key from localStorage as a backup
|
| 321 |
+
const backupApiKey = localStorage.getItem('notegenie_api_key');
|
| 322 |
+
|
| 323 |
+
// Create URL parameters including API key as fallback
|
| 324 |
+
const urlParams = new URLSearchParams({
|
| 325 |
prompt: prompt,
|
| 326 |
model: modelName,
|
| 327 |
stream: true
|
| 328 |
+
});
|
| 329 |
+
|
| 330 |
+
// Add API key to URL params if available from localStorage (as a fallback)
|
| 331 |
+
if (backupApiKey) {
|
| 332 |
+
urlParams.append('api_key', backupApiKey);
|
| 333 |
+
}
|
| 334 |
+
|
| 335 |
+
// Set headers with API key if available
|
| 336 |
+
const headers = {};
|
| 337 |
+
if (backupApiKey) {
|
| 338 |
+
headers['X-API-Key'] = backupApiKey;
|
| 339 |
+
}
|
| 340 |
+
|
| 341 |
+
// Create a new event source with API key included in both URL and headers
|
| 342 |
+
eventSource = createEventSourceWithHeaders(`/generate_notebook?${urlParams.toString()}`, headers);
|
| 343 |
|
| 344 |
eventSource.onmessage = function(event) {
|
| 345 |
// Update our last-activity timestamp
|
|
|
|
| 450 |
let lastPreviewUpdate = 0;
|
| 451 |
const PREVIEW_UPDATE_INTERVAL = 1000; // Update preview every 1 second during streaming, same as generate
|
| 452 |
|
| 453 |
+
// Get API key from localStorage as a backup
|
| 454 |
+
const backupApiKey = localStorage.getItem('notegenie_api_key');
|
| 455 |
+
|
| 456 |
+
// Make sure notebook is in proper format before sending
|
| 457 |
+
const notebookToSend = typeof notebook === 'string' ? JSON.parse(notebook) : notebook;
|
| 458 |
+
|
| 459 |
+
console.log('Sending notebook for editing:', notebookToSend); // Debug log
|
| 460 |
+
|
| 461 |
// First, send the notebook data to the server so it's available in the session
|
| 462 |
fetch('/prepare_edit_notebook', {
|
| 463 |
method: 'POST',
|
| 464 |
headers: {
|
| 465 |
'Content-Type': 'application/json',
|
| 466 |
+
// Add API key as header if available
|
| 467 |
+
...(backupApiKey && {'X-API-Key': backupApiKey})
|
| 468 |
},
|
| 469 |
body: JSON.stringify({
|
| 470 |
+
notebook: notebookToSend
|
| 471 |
})
|
| 472 |
})
|
| 473 |
.then(response => {
|
|
|
|
| 478 |
})
|
| 479 |
.then(data => {
|
| 480 |
if (data.success) {
|
| 481 |
+
updateAiMessage(aiMessageId, "**NoteGenie:** Processing your edit request...");
|
| 482 |
+
|
| 483 |
+
// Create URL parameters including API key as fallback
|
| 484 |
+
const urlParams = new URLSearchParams({
|
| 485 |
edit_prompt: editPrompt,
|
| 486 |
model: modelName,
|
| 487 |
stream: true
|
| 488 |
+
});
|
| 489 |
+
|
| 490 |
+
// Add API key to URL params if available from localStorage (as a fallback)
|
| 491 |
+
if (backupApiKey) {
|
| 492 |
+
urlParams.append('api_key', backupApiKey);
|
| 493 |
+
}
|
| 494 |
+
|
| 495 |
+
// Set headers with API key if available
|
| 496 |
+
const headers = {};
|
| 497 |
+
if (backupApiKey) {
|
| 498 |
+
headers['X-API-Key'] = backupApiKey;
|
| 499 |
+
}
|
| 500 |
+
|
| 501 |
+
// Now create a new event source for editing with the API key included in both URL and headers
|
| 502 |
+
eventSource = createEventSourceWithHeaders(`/edit_notebook?${urlParams.toString()}`, headers);
|
| 503 |
|
| 504 |
// Track the last time we got a chunk - for timeout detection
|
| 505 |
let lastChunkTime = Date.now();
|
|
|
|
| 634 |
.catch(error => {
|
| 635 |
console.error('Error preparing notebook for edit:', error);
|
| 636 |
updateAiMessage(aiMessageId, '**Error:** ' + error.message);
|
| 637 |
+
notebookPreviewEl.innerHTML = '<div class="alert alert-danger">Error: Failed to prepare notebook for editing. Please try regenerating the notebook.</div>';
|
| 638 |
setGeneratingState(false);
|
| 639 |
});
|
| 640 |
}
|
templates/index.html
CHANGED
|
@@ -16,6 +16,30 @@
|
|
| 16 |
<link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css" rel="stylesheet">
|
| 17 |
<!-- Custom styles -->
|
| 18 |
<link href="{{ url_for('static', filename='css/style.css') }}" rel="stylesheet">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
</head>
|
| 20 |
<body>
|
| 21 |
<div class="app-container">
|
|
|
|
| 16 |
<link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css" rel="stylesheet">
|
| 17 |
<!-- Custom styles -->
|
| 18 |
<link href="{{ url_for('static', filename='css/style.css') }}" rel="stylesheet">
|
| 19 |
+
<link rel="icon" type="image/png" href="{{ url_for('static', filename='images/favicon.png') }}">
|
| 20 |
+
<script>
|
| 21 |
+
document.fonts.ready.then(() => {
|
| 22 |
+
var canvas = document.createElement('canvas');
|
| 23 |
+
canvas.width = 64;
|
| 24 |
+
canvas.height = 64;
|
| 25 |
+
var ctx = canvas.getContext('2d');
|
| 26 |
+
ctx.fillStyle = '#4285f4'; // use desired color matching the logo-icon
|
| 27 |
+
ctx.font = '48px "Material Icons"';
|
| 28 |
+
ctx.textAlign = 'center';
|
| 29 |
+
ctx.textBaseline = 'middle';
|
| 30 |
+
ctx.fillText('auto_awesome', 32, 32);
|
| 31 |
+
var dataURL = canvas.toDataURL();
|
| 32 |
+
var link = document.querySelector('link[rel="icon"]');
|
| 33 |
+
if(link) {
|
| 34 |
+
link.href = dataURL;
|
| 35 |
+
} else {
|
| 36 |
+
var newLink = document.createElement('link');
|
| 37 |
+
newLink.rel = 'icon';
|
| 38 |
+
newLink.href = dataURL;
|
| 39 |
+
document.head.appendChild(newLink);
|
| 40 |
+
}
|
| 41 |
+
});
|
| 42 |
+
</script>
|
| 43 |
</head>
|
| 44 |
<body>
|
| 45 |
<div class="app-container">
|
utils/error_handler.py
DELETED
|
@@ -1,24 +0,0 @@
|
|
| 1 |
-
import logging
|
| 2 |
-
import traceback
|
| 3 |
-
import sys
|
| 4 |
-
|
| 5 |
-
# Configure logging to output to terminal
|
| 6 |
-
logging.basicConfig(
|
| 7 |
-
level=logging.DEBUG,
|
| 8 |
-
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
| 9 |
-
stream=sys.stderr
|
| 10 |
-
)
|
| 11 |
-
|
| 12 |
-
logger = logging.getLogger('NoteGenie')
|
| 13 |
-
|
| 14 |
-
def handle_error(func):
|
| 15 |
-
"""Decorator to handle exceptions and log them to terminal"""
|
| 16 |
-
def wrapper(*args, **kwargs):
|
| 17 |
-
try:
|
| 18 |
-
return func(*args, **kwargs)
|
| 19 |
-
except Exception as e:
|
| 20 |
-
logger.error(f"Error in {func.__name__}: {str(e)}")
|
| 21 |
-
logger.error(traceback.format_exc())
|
| 22 |
-
# You can still raise a user-friendly error for the UI
|
| 23 |
-
raise Exception("Failed to generate notebook. Please check terminal for details.")
|
| 24 |
-
return wrapper
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|